news 2026/9/9 13:52:42

Puter `puter.apps.list()` 深度指南:列出账号应用、分页游标与流式遍历全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Puter `puter.apps.list()` 深度指南:列出账号应用、分页游标与流式遍历全解析

Puterputer.apps.list()深度指南:列出账号应用、分页游标与流式遍历全解析

【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter

puter.apps.list()是 Puter JavaScript SDK 中用于获取"当前用户拥有或有权限访问的应用"列表的核心方法,广泛适用于网站、Web App、Node.js 与 Workers 等各类客户端场景。本文以官方文档 list.md 为骨架,结合 SDK 源码实现 与 后端 AppDriver,系统讲解其调用语法、六类可选参数、三种返回形态(纯数组 / 分页信封 / 异步迭代器)、底层分页协议及完整可运行示例,帮你彻底掌握应用列表的获取与大规模遍历方案。

方法定位:list 返回什么

根据 list.md 的说明,puter.apps.list()返回一个包含所有归属于当前用户、且当前应用有权访问App对象数组。若用户当前没有任何应用,则返回空数组。

官方文档 frontmatter 声明该方法支持的平台为platforms: [websites, apps, nodejs, workers],即四类 Puter 客户端均可直接调用,无需额外鉴权配置——SDK 会自动携带当前会话身份。

从 SDK 源码看,真正决定"能列出什么"的是底层驱动请求中的过滤谓词:

const select = utils.makeDriverMethod({ iface: 'puter-apps', driver: 'es:app', method: 'select', argNames: ['uid'], puter, readonly: true, }); const base = { predicate: ['user-can-edit'] };

这段代码位于 list.js 的实现:SDK 通过puter-apps接口调用es:app驱动的select方法,并使用user-can-edit谓词过滤——即返回当前用户拥有编辑权(拥有或被授予权限)的应用集合。调用被标记为readonly: true,说明它属于纯查询操作,不会产生副作用或计费写入。

快速上手:创建 3 个应用并列出

基础语法

puter.apps.list(); puter.apps.list(options);

两种调用形态中,options均为可选。不带任何参数时,方法返回一个 Promise,resolve 为一个扁平数组,语义最直观:

const apps = await puter.apps.list(); console.log(apps.map(app => app.name));

官方完整示例(含创建与清理)

原文档给出了一个"先造数据、再列表、最后清理"的完整自洽示例,这里完整继承(在浏览器 HTML 页面中引入 Puter SDK v2 后执行;SDK 亦可通过 src/puter-js 本地构建使用):

<html> <body> <script src="https://js.puter.com/v2/"></script> <script> (async () => { // (1) Generate 3 random app names let appName_1 = puter.randName(); let appName_2 = puter.randName(); let appName_3 = puter.randName(); // (2) Create 3 apps await puter.apps.create(appName_1, 'https://example.com'); await puter.apps.create(appName_2, 'https://example.com'); await puter.apps.create(appName_3, 'https://example.com'); // (3) Get all apps (list) let apps = await puter.apps.list(); // (4) Display the names of the apps puter.print(JSON.stringify(apps.map(app => app.name))); // (5) Delete the 3 apps we created earlier (cleanup) await puter.apps.delete(appName_1); await puter.apps.delete(appName_2); await puter.apps.delete(appName_3); })(); </script> </body> </html>

示例覆盖了 create、list、delete 三个方法,其中puter.randName()用于生成随机应用名,puter.print()用于把结果显示到 UI。注意最后的 delete 步骤是良好的清理习惯,避免测试数据残留到账号中。

options参数全解

当传入对象形态参数时,SDK 会将其中的分页相关字段(limit/offset/cursor/includeTotal/stream)单独解构出来用于驱动请求,其余字段(stats_periodicon_size)作为params原样透传给后端:

const { limit, offset, cursor, includeTotal, stream, ...params } = opts; if ( isObjectForm ) base.params = params; if ( limit !== undefined ) base.limit = limit;

以下逐项说明各参数的取值与语义:

stats_period(可选,String)

指定open_count(打开次数)与user_count(访问用户数)的统计周期。可能取值如下,默认值为all(全时段累计):

取值含义
today今天
yesterday昨天
7d最近 7 天
30d最近 30 天
this_month本月
last_month上月
this_year今年
last_year去年
month_to_date本月至今
year_to_date今年至今
last_12_months最近 12 个月
all(默认)全时段

从后端实现看,统计周期对应 AppDriver.js 中的统计查询逻辑:SDK 将 stats 选项打包在params之下(源码注释明确说明 puter-js 的makeDriverMethod/Apps.get会把 options 打包到params),后端同时兼容顶层扁平形状:

const stats_period = params.stats_period ?? rest.stats_period; const stats_grouping = params.stats_grouping ?? rest.stats_grouping; const needsStats = params.stats !== false && (stats_period || stats_grouping); // Detailed period/grouping is per-app only — skip the batch cache // and go straight to the live query. const hasDetailed = Boolean(stats_period || stats_grouping);

可以推断:未指定stats_period,后端走默认的缓存批量统计路径(性能更优);指定了具体周期后则进入getAppStatsDetailed的实时明细查询路径,换取按周期的精确计数。

icon_size(可选,Integer)

返回应用图标的尺寸(像素),决定App.icon字段输出多大的 Data URL 图片。可选值:null163264128256512。默认值为null,表示返回图标原始尺寸。当需要渲染列表缩略图时,传64128可显著降低传输体积。

limit(可选,Number)

单次调用最多返回的应用数量。在流式与游标分页模式下,它控制每一页的大小。

offset(可选,Number)

跳过指定数量的应用后开始返回,用于传统"偏移量"分页。官方文档明确提示:遍历大型列表时优先使用cursor,因为 offset 分页在大数据量下性能较差且容易因数据变动产生重复/遗漏。

cursor(可选,String)

显式启用游标分页。第一次请求传null(代表第一页),之后把每次响应中的cursor字段传给下一次调用,直到响应中不再包含cursor(说明已到最后一页)。注意:只要options对象里出现了cursor键(即使值为null),返回形态就会从数组切换为分页信封对象——这是 SDK 源码中通过hasOwnProperty.call(opts, 'cursor')精确判断的:

const hasCursor = Object.prototype.hasOwnProperty.call(opts, 'cursor');

includeTotal(可选,Boolean)

true时,分页响应会额外携带total字段,表示该用户应用的总数。官方实现与文档一致地指出:只有请求包含cursor(含 null)、offsetincludeTotal之一时,返回值才是分页信封;同时从 pagination.js 的实现可以看出,includeTotal只在第一页请求上发送,因为"总数统计在条目越多时成本越高,且总数不会随翻页变化"。

stream(可选,Boolean)

true时,方法不再返回 Promise,而是返回一个异步迭代器,可直接配合for await ... of逐页消费。可与limit组合控制页大小,也可传cursor从指定页继续;不能与offset组合使用,若同时传入会抛出PuterJSError(错误码invalid_request):

if ( stream === true ) { if ( offset !== undefined ) { throw new PuterJSError( '`offset` cannot be combined with `stream`; pass `cursor` to resume from a position.', 'invalid_request', ); } // ...async generator 逐页 yield }

开启includeTotal时,只有第一页会携带total

返回值:三种形态与分页信封

list()的返回形态由是否携带分页参数决定,这是理解本方法的关键:

形态一:纯数组(未携带任何分页参数)

返回 Promise,resolve 为所有App对象组成的数组。官方文档特别强调:不带分页参数的请求仍返回完整的扁平数组,旧代码完全不受影响——SDK 只是在底层把它拆成了逐页请求再合并:

// Unbound listing: fetch page by page under the hood so no single request // carries the whole result, then return the legacy array. return fetchAllPages(fetchPage).then(items => addUserIterationToApps(puter, items));

对应 fetchAllPages 的实现,其内部本质是"把所有分页信封的 items 依次拼接":

async function fetchAllPages (fetchPage) { const items = []; for await ( const page of iteratePages(fetchPage) ) { items.push(...(page.items ?? [])); } return items; }

这就保证了:无论请求是否传limit,最终对调用方呈现的都是传统数组形态,既避免了单请求携带全部结果的压力,又不破坏既有集成。

形态二:分页信封对象(请求包含cursor(含 null)、offsetincludeTotal之一)

Promise resolve 为一个 page 对象,包含:

字段类型说明
itemsArray本页的App对象数组
cursorString(可选)仅在还有更多页时出现;将其传给下一次调用即可获取下一页
totalNumber(可选)用户应用总数,仅在设置includeTotal时出现

在 SDK 源码中,当请求命中分页参数时,select的返回若为信封形态(非数组且含items),则原样返回该信封:

const result = await select(driverArgs); if ( result && !Array.isArray(result) && Array.isArray(result.items) ) { addUserIterationToApps(puter, result.items); return result; }

若后端返回的是裸数组(例如旧版后端忽略分页参数),SDK 也会优雅兼容。

形态三:异步迭代器(stream: true

返回 async iterator,逐页产出 page 对象。官方示例如下,page.items上的每个元素仍是App对象:

for await (const page of puter.apps.list({ stream: true })) { for (const app of page.items) { console.log(app.name); } }

其底层由 iteratePages 驱动——一个标准的 async generator,从cursor: null发起请求,yield 当前页后依据page.cursor是否存在决定是否继续拉取:

async function* iteratePages (fetchPage, opts = {}) { let pageParams = { cursor: opts.cursor ?? null, ...(opts.includeTotal === true ? { includeTotal: true } : {}), }; while ( true ) { const result = await fetchPage(pageParams); const page = Array.isArray(result) ? { items: result } : (result ?? { items: [] }); yield page; if ( ! page.cursor ) return; pageParams = { cursor: page.cursor }; } }

注意这里对忽略分页参数的后端做了兼容:后端返回裸数组时,该数组被当作唯一的一页;若完全无返回则视为空页。

另外,无论哪种形态,SDK 都会对每个应用调用addUserIterationToApps注入用户遍历辅助方法,使返回的App对象可以继续调用app.users()/app.getUsers()

返回的App对象字段说明

list()返回的每个元素都是标准 App 对象,核心字段如下:

字段类型说明
uidStringPuter 在应用创建时生成的全局唯一标识
nameString应用名称(应用 API 调用中的唯一键)
iconString应用图标的 Data URL(base64 编码图片),尺寸受icon_size影响
descriptionString应用描述
titleString应用显示标题
maximize_on_startBoolean启动时是否最大化窗口,默认false
index_urlString应用启动时加载的入口文件 URL
created_atString创建时间,格式YYYY-MM-DDTHH:MM:SSZ
backgroundBoolean是否作为后台应用运行,默认false
filetype_associationsArray应用可打开的文件类型,形如[".txt", "image/png"],目录关联用".directory"
open_countNumber应用被打开的次数;设置stats_period后为该周期内次数
user_countNumber有权访问该应用的用户数;设置stats_period后为该周期内统计
metadataObject应用自定义元数据,任意键值对

对列表元素做用户遍历

列表接口返回的App对象额外支持两种用户迭代方法(这正是前文提到的addUserIterationToApps所注入的):

// 逐页迭代全部有权用户(默认每页 100,可传 pageSize) for await (const user of app.users()) { console.log(user); // { username, user_uuid, user_email? } } // 按 limit/offset 获取一页用户 const users = await app.getUsers({ limit: 2, offset: 0 });

其中user_email仅当用户授予了当前应用user:<uuid>:email:read权限(例如通过puter.perms.request('email'))时才会出现,否则被省略;若用户授权但没有登记邮箱则可能为null。这使"列出应用 → 遍历应用用户"成为一个完整的工作流。

实战:五种典型调用组合

1. 简单列出全部应用名

const apps = await puter.apps.list(); console.log(apps.map(app => ({ name: app.name, uid: app.uid })));

2. 游标手动翻页(含总数)

let cursor = null; do { const page = await puter.apps.list({ cursor, // 首轮传 null 表示第一页 limit: 10, includeTotal: true, }); for (const app of page.items) { console.log(page.total, app.name); // 仅第一页可读 total } cursor = page.cursor; // 无 cursor 即最后一页 } while (cursor);

3. 流式遍历全部应用

for await (const page of puter.apps.list({ stream: true, limit: 20, })) { for (const app of page.items) { await doSomethingWith(app); } }

4. 从某页继续(断点续传)

// 第一次:拿到 cursor const first = await puter.apps.list({ cursor: null, limit: 20 }); // 后续进程恢复:直接带着 cursor 继续 const next = await puter.apps.list({ cursor: first.cursor, limit: 20 });

5. 携带统计周期与图标尺寸

const apps = await puter.apps.list({ stats_period: '7d', // open_count / user_count 取最近 7 天 icon_size: 128, // 图标压缩到 128px }); for (const app of apps) { console.log(app.name, app.open_count, app.user_count); }

边界行为与注意事项

  • 无应用时:返回空数组([]),而非报错或null
  • 兼容性设计:不带分页参数的调用行为与旧版本完全一致(返回纯数组),只是底层改为逐页拉取——官方文档原话:"Requests without pagination params keep returning the full list as a plain array, so existing code is unaffected — under the hood the SDK now fetches it page by page."
  • offsetstream互斥:同时传入会抛出invalid_request错误;流式场景下要用cursor恢复位置。
  • includeTotal成本:总数统计成本随条目数量上升,SDK 只在第一页请求发送includeTotal,后续页不再重复统计。
  • cursor键存在即切换形态:即使显式传cursor: null,返回值也是分页信封{ items, ... }而非扁平数组,写代码时不要假设传null就返回数组。
  • 大列表首选 cursor:官方明确提示 offset 仅适合小规模分页,遍历大列表应使用cursor

相关实现与文档索引

  • 官方 API 文档原文:src/docs/src/Apps/list.md
  • App 对象属性与方法:src/docs/src/Objects/app.md
  • 配套方法:create、get、update、delete、checkName
  • SDK 端分页与列表实现:src/puter-js/src/modules/apps/list.js、src/puter-js/src/lib/pagination.js
  • 后端统计/查询实现:src/backend/drivers/apps/AppDriver.js
  • 通用分页信封协议说明({ items, cursor?, total? }):doc/pagination.md

综上,puter.apps.list()在设计上同时照顾了"简单列出"与"大规模遍历"两类诉求:日常场景直接await list()拿数组即可;面对海量应用或需要增量同步时,则可用cursor游标、includeTotalstream异步迭代三件套实现稳健、可续传、后端友好的分页消费。

【免费下载链接】puter🌐 The Internet Computer! Free, Open-Source, and Self-Hostable.项目地址: https://gitcode.com/GitHub_Trending/pu/puter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 13:52:32

软件测试Bug全生命周期管理:从发现到关闭的实战指南

1. 软件测试里的Bug&#xff0c;不止是“找茬”那么简单 1.1 第一次提交Bug被驳回&#xff1a;缺陷与Bug的区别 我入行第一周就闹了个笑话。当时测一个后台管理系统&#xff0c;发现某个输入框输入超过50个字符后&#xff0c;页面会弹出一个英文报错。我觉得这是Bug&#xff0…

作者头像 李华
网站建设 2026/9/9 13:51:31

从碎片笔记到技术博文:内容创作与SEO优化完整指南

抱歉&#xff0c;我目前无法基于空内容生成文章。您提供的项目标题为“【无标题】”&#xff0c;且项目正文、关键词、摘要描述、相关热搜词、最新网络热词均为空白。这意味着没有任何实质信息可供提取和延展。为了帮您生成一篇有干货、有结构、能直接发布的博文&#xff0c;麻…

作者头像 李华
网站建设 2026/9/9 13:50:26

银行外拓服务全流程拆解:从社保卡激活到“送服务上门”的实战经验

1. 服务项目整体拆解&#xff1a;从“等客上门”到“送服务上门”的转型逻辑 这类“步履不停送服务、金融为民践初心”主题行动&#xff0c;在银行系统内早已不是新鲜提法&#xff0c;但真正落到实处的分支机构并不算多。我参与过几次类似的外拓服务专项活动&#xff0c;包括社…

作者头像 李华
网站建设 2026/9/9 13:48:58

推理阶段不同batch size对大模型推理结果的影响

SGLang最新版本提供了确定性推理的方法:SGLang的确定性推理 !!!Thinking Machines Lab对这个问题基本上画上了句号&#xff0c;在其官方blog 大模型推理阶段&#xff0c;进行batch inference批处理推理解码&#xff0c;会像预期的那样速度很快推完吗&#xff1f;会不会有什么问…

作者头像 李华
网站建设 2026/9/9 13:48:40

编码智能体崛起:从代码补全到自主完成任务

Simon Willison 是我在开发者社区里一直比较信任的一个观察者。他不是那种只会转发新闻稿的人&#xff0c;而是真的会把手上的工具拆开、试用、写测试、然后告诉你哪里好用哪里难用。最近他连着好几期内容都在聊同一个话题&#xff1a;OpenAI 内部的研究节奏明显加快了&#xf…

作者头像 李华