Puter.js Apps API 完整开发指南:在 Puter 桌面操作系统中创建、管理与分发应用
Apps API 是 Puter.js 为开发者提供的一套应用生命周期管理接口,用于在 Puter 桌面操作系统中创建、查询、更新、删除以及分发应用。无论你构建的是网页应用、Puter 原生 App、Node.js 脚本还是 Worker,都可以通过同一套 puter.apps.* 方法管理属于自己的应用清单。读完本文,你将掌握全部 6 个 API 方法的完整语法、每个可选参数的业务含义、分页与流式读取的正确姿势,并能基于仓库源码理解这些方法背后的调用链。
本文以 Puter 官方文档 Apps 及其各方法子页(create、list、delete、update、get、checkName)为骨架撰写,并对照 Puter.js SDK 源码 逐层印证底层实现。
Apps API 能做什么
在 Puter 的生态中,一个"App"就是一条指向某个网页入口(index_url)的注册记录。Puter.js 的 puter.apps 模块开箱即用地支持以下操作(对应源码中的 AppsModule,见 modules/apps/index.js):
| 方法 | 作用 | 对应详细文档 |
|---|---|---|
puter.apps.create() |
创建一个新应用 | 支持位置参数与 options 对象两种调用形式 |
puter.apps.list() |
列出当前用户全部应用 | 内置分页(cursor/offset)、流式遍历与统计选项 |
puter.apps.get() |
获取单个应用详情 | 可按需附带 stats_period、icon_size |
puter.apps.update() |
更新应用属性 | 可改名称、入口 URL、图标、文件关联等 |
puter.apps.delete() |
删除一个应用 | 删除后 get() 将失败 |
puter.apps.checkName() |
预检应用名是否可用 | 不创建任何实体,避免 create() 因重名被拒 |
关于方法实现的几个值得注意的实现事实:SDK 中每个方法都保留"未绑定函数 + 构造器内 bind"的双保险设计,因此既可以 puter.apps.create(...) 链式调用,也可以解构为 const { create } = puter.apps 后独立调用(见 index.js);所有方法的 JSDoc 声明是公开签名的唯一事实来源,类型声明由其生成,无需手工维护。客户端的参数校验失败会抛出形状为 { success: false, error: { code: 'invalid_request', message } } 的错误对象,同时在顶层暴露 message/code,兼容新旧两种捕获写法(见 lib/validate.js)。
快速接入与运行前提
所有示例都通过引入 Puter.js 开始。官方示例统一在 HTML 中加载:
<script src="https://js.puter.com/v2/"></script>
关于平台的适用范围,各方法文档的 front matter 均声明 platforms: [websites, apps, nodejs, workers]——即网页站点、Puter 应用本身、Node.js 服务端与 Worker 环境都能调用这些接口。对于自托管 Puter,你完全可以改为加载并初始化仓库中自托管的 puter-js SDK,接口语义保持一致。
此外,所有示例都会用到 puter.randName()(随机生成互不冲突的名字)与 puter.print()(向页面打印输出),它们是官方 Playground 示例的标准工具函数。
创建应用:puter.apps.create()
create() 用于在用户的 app 列表中创建一个新应用。创建出的应用默认不拥有任何权限,在被授予权限前无法访问任何用户数据。
语法
puter.apps.create(name, indexURL);
puter.apps.create(name, indexURL, title);
puter.apps.create(options);
参数说明
位置参数形式
name(String,必填):应用名称,必须在该用户的 app 列表中唯一;若已存在同名应用,Promise 会被 reject。indexURL(String,必填):应用首页 URL,即应用启动时加载的页面,必须对用户可访问;缺失时 Promise 被拒绝。title(String,可选):应用的可读标题,省略时默认使用name作为标题。
indexURL 有一个硬性约束——URL 必须以 http:// 或 https:// 开头,其余协议一律不允许:
- ✅
https://example.com/app/index.html - ✅
http://localhost:3000/index.html - ❌
file:///path/to/index.html - ❌
ftp://example.com/index.html
options 对象形式
options 支持把位置参数展开为结构化属性,同时新增一批行为选项:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
String | 是 | 应用名,须对当前用户唯一 |
indexURL |
String | 是 | 首页 URL,必须可访问且为 http/https |
title |
String | 否 | 人类可读标题,省略时回退为 name |
description |
String | 否 | 面向终端用户的应用描述 |
icon |
String | 否 | 应用新图标 |
maximizeOnStart |
Boolean | 否 | 启动时是否最大化,默认 false |
filetypeAssociations |
Array<String> | 否 | 应用可打开的文件夹联,支持文件扩展名与 MIME 类型,默认 []。例如 [".txt", ".md", "application/pdf"] 允许应用打开 .txt、.md 与 PDF 文件 |
dedupeName |
Boolean | 否 | 名称已存在时是否自动去重,默认 false |
background |
Boolean | 否 | 应用是否后台运行,默认 false |
feedbackEnabled |
Boolean | 否 | 用户能否通过 puter.ui.showFeedbackDialog() 向你发送反馈,默认 false |
metadata |
Object | 否 | 自定义元数据对象,可存储任意键值对 |
返回值
Promise,resolve 为 CreateAppResult 对象。
底层实现要点
对照 SDK 的 create.js 可以看到:
- 当第一个参数是字符串时,SDK 内部构造
{ object: { name, index_url, title } };当传入对象时,先经toAppObject()把indexURL/maximizeOnStart等驼峰属性映射为后端使用的index_url/maximize_on_start下划线字段,title缺省则用name填充; dedupeName会被转换为下划线形式的dedupe_name放进options透传;name或index_url缺失时,客户端直接抛出invalid_request错误,根本不会发出网络请求;- 网络调用经由统一的
makeDriverMethod({ iface: 'puter-apps', driver: 'es:app', method: 'create', argNames: ['object'] })驱动方法完成,即请求会路由到puter-apps接口下的es:app驱动器。后端侧对应实现在 backend/controllers/apps/AppController.js 及其测试 AppController.test.ts。
列出应用:puter.apps.list()
list() 返回一个数组,包含属于当前用户且当前应用有权访问的全部应用;用户没有任何应用时返回空数组。
语法
puter.apps.list();
puter.apps.list(options);
参数说明
options(可选对象)支持下列属性:
stats_period(String,可选):统计周期,用于获取指定时间段内的打开次数与用户数。可取值为today、yesterday、7d、30d、this_month、last_month、this_year、last_year、month_to_date、year_to_date、last_12_months,默认all(全部时间)。icon_size(Integer,可选):返回图标尺寸,可取null、16、32、64、128、256、512,默认null(原始尺寸)。limit(Number,可选):单次调用返回的最大应用数。offset(Number,可选):跳过指定数量的应用。大型列表分页时优先使用cursor。cursor(String,可选):接入分页模式。首页传null,随后把每页返回的cursor传给下一次调用以获取下一页。includeTotal(Boolean,可选):为true时,分页结果会附带当前用户的应用总数total。stream(Boolean,可选):为true时不再返回 Promise,而是返回分页对象的异步迭代器,配合for await ... of使用。可与limit(控制页大小)或cursor(从中断页继续)组合,不能与offset组合;启用includeTotal时只有第一页携带total。
返回值
- 不带任何分页参数:Promise resolve 为当前用户全部 App 对象构成的数组;
- 带
cursor(含null)、offset或includeTotal任一参数:Promise resolve 为分页对象:items(Array):本页的 App 对象;cursor(String,可选):还有更多页时出现,回传给下一次调用;total(Number,可选):应用总数,仅在设置了includeTotal时出现。
因此,不带分页参数的存量代码完全不受影响——SDK 只是改为在底层自动逐页抓取,最后仍拼成普通数组返回。
stream: true 的典型用法:
for await (const page of puter.apps.list({ stream: true })) {
for (const app of page.items) {
console.log(app.name);
}
}
底层实现要点
list.js 的实现非常清晰地揭示了返回值的形态切换逻辑:
- 调用的驱动方法实际是
method: 'select',并使用predicate: ['user-can-edit']限定为"当前用户可编辑"的应用集合; stats_period、icon_size等非分页参数被解构出来后透传为params;stream === true时走iteratePages生成异步生成器,若同时传了offset会直接抛出invalid_request(提示改用cursor续页);- 任何分页参数存在时,走单请求路径并返回页信封对象;
- 一个分页参数都没有时,走
fetchAllPages在底层逐页取完全部数据,再以旧式的扁平数组形态返回,保证向后兼容。
获取单个应用:puter.apps.get()
返回指定名称的应用;若该应用不存在,Promise 会被 reject。
语法
puter.apps.get(name);
puter.apps.get(name, options);
参数说明
name(String,必填):要获取的应用名称。options(Object,可选):stats_period(可选):与list()相同的周期取值,用于获取统计口径内的open_count/user_count;icon_size(可选):与list()相同的图标尺寸取值。
返回值
Promise resolve 为指定名称对应的 App 对象。
关于 App 对象:它包含 uid(创建时由 Puter 生成的唯一标识)、name、icon(base64 Data URL)、description、title、maximize_on_start(默认 false)、index_url、created_at(格式 YYYY-MM-DDTHH:MM:SSZ)、background(默认 false)、filetype_associations、open_count、user_count 与 metadata 等属性;当 stats_period 被设置时,open_count/user_count 会按周期口径返回。
App 对象还内建了用户遍历方法:app.users(pageSize)(异步迭代器,默认每页 100 个,逐项 { username, user_uuid },当用户通过 puter.perms.request() 授予了 user:<uuid>:email:read 权限时会额外携带 user_email)以及 app.getUsers({ limit, offset })(基于 limit/offset 的一页式拉取)。
更新应用:puter.apps.update()
按名称更新应用的属性。
语法
puter.apps.update(name, attributes);
参数说明
name(String,必填):要更新的应用名。attributes(Object,必填):需要更新的属性集合,可包含:name(可选):新应用名,必须对当前用户唯一,若被占用 Promise 会被拒绝;indexURL(可选):新的首页 URL,须对用户可访问;title(可选):新标题;description(可选):面向终端用户的新描述;icon(可选):新图标;maximizeOnStart(可选):启动时是否最大化,默认false;background(可选):是否后台运行,默认false;feedbackEnabled(可选):是否允许用户通过puter.ui.showFeedbackDialog()反馈;省略该字段则保持当前值不变(区别于create()的默认false);filetypeAssociations(可选):可打开的文件类型数组,语义同create();metadata(可选):关联的自定义元数据键值对象。
返回值
Promise resolve 为更新后的 App 对象。
删除应用:puter.apps.delete()
删除指定名称的应用。删除操作属于不可逆的清理动作,官方所有示例都会在演示完创建/更新后立即删除应用以回收名称。
语法
puter.apps.delete(name);
参数说明与返回值
name(String,必填):要删除的应用名称。- 返回值:Promise resolve 为
{ success: true, uid: <app uid> },uid为被删除应用的标识,可用于确认删除目标。
名称预检:puter.apps.checkName()
在不创建任何东西的前提下,检查一个应用名对你是否可用。它最适合放在 puter.apps.create() 之前调用,因为 create() 遇到重名会直接 reject,而预检可以提前让 UI 给出友好提示。
语法
puter.apps.checkName(name);
参数说明与返回值
name(String,必填):要检查的应用名;缺失或为空字符串时 Promise 会以invalid_request错误被拒绝。- 返回值:Promise resolve 为描述该名称可用性的对象(官方文档未固定其字段形状,使用前建议以实际响应为准;前端校验由 lib/validate.js 保证)。
官方示例:从创建到清理的完整工作流
以下 5 个代码块是官方文档在 Playground 中的完整可运行示例(同名源码存放在 playground/examples 目录,对应 app-create.html、app-list.html、app-delete.html、app-update.html、app-get.html),这里原样保留并配以步骤注释。
(1)创建指向 example.com 的应用
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) Generate a random app name
let appName = puter.randName();
// (2) Create the app and prints its UID to the page
let app = await puter.apps.create(appName, "https://example.com");
puter.print(`Created app "${app.name}". UID: ${app.uid}`);
// (3) Delete the app (cleanup)
await puter.apps.delete(appName);
})();
</script>
</body>
</html>
(2)创建 3 个随机应用再统一列出
<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>
(3)创建随机应用后删除,并验证 get() 已失效
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) Generate a random app name to make sure it doesn't already exist
let appName = puter.randName();
// (2) Create the app
await puter.apps.create(appName, "https://example.com");
puter.print(`"${appName}" created<br>`);
// (3) Delete the app
await puter.apps.delete(appName);
puter.print(`"${appName}" deleted<br>`);
// (4) Try to retrieve the app (should fail)
puter.print(`Trying to retrieve "${appName}"...<br>`);
try {
await puter.apps.get(appName);
} catch (e) {
puter.print(`"${appName}" could not be retrieved<br>`);
}
})();
</script>
</body>
</html>
(4)创建随机应用后修改其标题
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) Create a random app
let appName = puter.randName();
await puter.apps.create(appName, "https://example.com")
puter.print(`"${appName}" created<br>`);
// (2) Update the app
let updated_app = await puter.apps.update(appName, {title: "My Updated Test App!"})
puter.print(`Changed title to "${updated_app.title}"<br>`);
// (3) Delete the app (cleanup)
await puter.apps.delete(appName)
})();
</script>
</body>
</html>
(5)创建随机应用后读取其信息
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
// (1) Generate a random app name to make sure it doesn't already exist
let appName = puter.randName();
// (2) Create the app
await puter.apps.create(appName, "https://example.com");
puter.print(`"${appName}" created<br>`);
// (3) Retrieve the app using get()
let app = await puter.apps.get(appName);
puter.print(`"${appName}" retrieved using get(): id: ${app.uid}<br>`);
// (4) Delete the app (cleanup)
await puter.apps.delete(appName);
})();
</script>
</body>
</html>
checkName() 的组合示例(文档子页附带,最贴近真实注册/命名流程):
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<script>
(async () => {
const appName = puter.randName();
// (1) A freshly generated name is available
puter.print(JSON.stringify(await puter.apps.checkName(appName)) + '<br>');
// (2) Create the app, which takes the name
await puter.apps.create(appName, 'https://example.com');
// (3) The same name now reports differently
puter.print(JSON.stringify(await puter.apps.checkName(appName)) + '<br>');
// (4) Delete the app (cleanup)
await puter.apps.delete(appName);
})();
</script>
</body>
</html>
命名唯一性:贯穿全部写操作的核心约束
从上述 API 可以看出,Puter 的 app 体系以"名称(name)"作为操作句柄,而不是先查 uid 再传 uid——create/get/update/delete/checkName 全部直接以名称寻址。这带来几条实用结论:
- 名称是用户级唯一键:
create()与update()都要求新名称对该用户唯一,否则 Promise 被拒绝;删除重名之外的唯一途径。 - 重名场景有两个出口:要么先
checkName()预检并引导用户换名;要么在create()中传dedupeName: true让系统自动生成去重后的名称。 - 写操作前先读:
update()并不支持"部分字段未知也可全量覆盖"的语义,建议在修改前用get(name)拉取现状,只传需要变更的字段,尤其在保持feedbackEnabled等字段既有取值时(省略即不改变)。 - uid 仅用于记录与回溯:
create()与delete()的返回中会携带uid,可以用于日志审计或后续跨模块引用。
深入阅读与可用资源
- 接口总览:Apps API 官方文档;各方法细节:create / list / get / update / delete / checkName
- 对象模型:App、CreateAppResult
- SDK 源码:模块装配见 modules/apps/index.js,
create/list/delete/get/update/checkName的分文件实现及校验逻辑 lib/validate.js、对象字段映射 lib/appObject.js - 后端实现与测试:AppController.js、AppController.test.ts、应用驱动器 AppDriver.js
- 可运行的完整示例:Playground 目录下的 app-create.html、app-list.html、app-delete.html、app-update.html、app-get.html;以及更完整的实战样例 To-Do List、AI Chat、Camera Photo Describer、Text Summarizer
- 相关交互能力:通过
puter.ui.showFeedbackDialog()收集用户反馈、通过puter.perms.request()申请邮件读取等细粒度权限
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00