@panda-agent/panda-app-sdk 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,53 @@
2
2
 
3
3
  Panda 智能应用 SDK — 在 iframe 或 Utility Process 中调用 Panda Desktop 提供的应用平台 API。
4
4
 
5
- SDK 为纯客户端 RPC 封装,**不包含** Panda Core 运行时。应用须在 Panda Desktop 中安装并运行。
5
+ - **纯客户端 RPC 封装**,不包含宿主运行时;应用必须在 Panda Desktop 中安装并运行
6
+ - 双入口:`browser`(iframe UI)/ `node`(Standalone / Headless 后端)
7
+ - 当前版本:**0.4.0**
8
+
9
+ 本次平台变更新增 `conversation.create`:应用可创建**用户会话**(落盘后出现在主界面侧栏、用户可接手继续对话),`createForApp` 保持应用私有语义;`conversation.attach` 支持在会话未加载时按其记录工作区后台恢复后再绑定。使用这两个能力的应用需声明 `engines.pandaAppSdk: "0.4.0"`。
10
+
11
+ 上一版平台变更移除了 App 专有函数通道。旧 manifest 的 `agentFuncs`(包括空声明)和 `agent.funcs.receive` 不再支持,必须迁移到标准 MCP。没有兼容代发层;App UI、storage、普通 conversation 订阅和 Agent API 保留。
12
+
13
+ Plugin 可声明 `mcpScope: "app"`(缺省 `"shared"`),App 私有 Agent 自动绑定 owning plugin 的实例 MCP。`invokeSubagent` 的 `allowedTools` 可进一步限制 builtin 精确 ID 或本 App MCP 的无歧义工具名,`readableFiles` 仅限制 `read_file` 的精确文件参数;它们不授予额外权限,不绕过 readonly、审批或资源边界。带策略字段的调用会先检查本轮新增的 `runtime.getHost().capabilities.invocationToolPolicy`,旧宿主缺少此位时拒绝派发,不回退为普通不受限 Agent;协商期间也可取消。`runtime.supports(version)` 仍只比较版本,不能代替此检查。
14
+
15
+ App MCP 后台需要动态 endpoint 时,宿主新增通用可信环境 `PANDA_APP_INSTANCE_ID` 与 `PANDA_APP_MCP_DISCOVERY_FILE`。这不是 `.mcp.json` 的既有模板变量。发现文件只能由所属实例使用;transport key 不应进入 prompt、模型参数或 UI 状态。详细生命周期与实现边界见仓库设计文档。
16
+
17
+ ---
18
+
19
+ ## 目录
20
+
21
+ - [安装](#安装)
22
+ - [入口](#入口)
23
+ - [快速开始](#快速开始)
24
+ - [示例 1:Session 应用前端](#示例-1session-应用前端)
25
+ - [示例 2:Standalone / Headless 后端](#示例-2standalone--headless-后端)
26
+ - [示例 3:能力检测(runtime.supports)](#示例-3能力检测runtimesupports)
27
+ - [API 参考](#api-参考)
28
+ - [auth — 用户](#auth--用户)
29
+ - [model — 模型](#model--模型)
30
+ - [workspace — 工作区](#workspace--工作区)
31
+ - [artifacts — 制品](#artifacts--制品)
32
+ - [conversation — 会话](#conversation--会话)
33
+ - [session — 会话上下文与挂载](#session--会话上下文与挂载)
34
+ - [todo — 任务清单](#todo--任务清单)
35
+ - [memory — 会话记忆](#memory--会话记忆)
36
+ - [ingress — 消息提交](#ingress--消息提交)
37
+ - [agent — 智能体能力](#agent--智能体能力)
38
+ - [ui — 界面](#ui--界面)
39
+ - [storage — 存储](#storage--存储)
40
+ - [tools — 工具](#tools--工具)
41
+ - [runtime — 运行时与宿主](#runtime--运行时与宿主)
42
+ - [dialog — 原生对话框](#dialog--原生对话框)
43
+ - [事件订阅参考](#事件订阅参考)
44
+ - [应用类型对照](#应用类型对照)
45
+ - [版本与兼容性](#版本与兼容性)
46
+ - [高级用法](#高级用法)
47
+ - [从零构建一个 Panda 应用](#从零构建一个-panda-应用)
48
+ - [构建与发布 SDK](#构建与发布-sdk)
49
+ - [示例演示](#示例演示)
50
+
51
+ ---
6
52
 
7
53
  ## 安装
8
54
 
@@ -14,41 +60,600 @@ npm install @panda-agent/panda-app-sdk
14
60
 
15
61
  | 子路径 | 运行环境 | 用途 |
16
62
  |--------|----------|------|
17
- | `@panda-agent/panda-app-sdk/browser` | iframe(Session / Standalone UI) | `PandaApp.connectIframe()` |
18
- | `@panda-agent/panda-app-sdk/node` | Utility Process(Standalone 后端 / Headless) | `PandaApp.connectUtilityProcess()` |
63
+ | `@panda-agent/panda-app-sdk/browser` | iframe(Session / Standalone UI) | `PandaApp.connect()` |
64
+ | `@panda-agent/panda-app-sdk/node` | Utility Process(Standalone 后端 / Headless) | `PandaApp.connect()` |
65
+
66
+ 使用 Vite / webpack / esbuild 将对应入口打进应用 bundle。
67
+
68
+ ### 启动就绪(0.3.0)
69
+
70
+ 应用 manifest 声明 `"engines": { "pandaAppSdk": "0.3.0" }` 后采用显式启动契约。宿主立即显示 Loading;有后台时等待后台和当前页面分别就绪,纯前端只等待前端,headless 只等待后台。未声明新契约的旧 App 保持旧启动路径,不以固定延时伪装 ready。
71
+
72
+ `PandaApp.connect()` 同步创建 SDK 客户端,不代表就绪。后台在必需服务初始化后、前端在首个可用界面渲染后调用 `await app.ready()`。调用幂等,ACK 只确认当前这一端,不能等待整体 App 就绪。初始化期间的 SDK RPC 仍可使用。MCP client、登录与全部远端数据不属于首屏条件。
73
+
74
+ 前端示例:在 React 首屏 bootstrap 完成后的 effect 中调用 `app.ready()`;不要在模块加载时调用。页面刷新需要重新报告,切换会话隐藏保活页面则不重复初始化。SDK 在每份新文档中建立内部握手,宿主确认当前文档后接受 ready;页面自行刷新也会重新进入 Loading,旧文档的迟到 ready 不会完成新文档。该握手不增加公开 API,也不要求重启健康后台。宿主启动失败或超时提供重试/关闭。
75
+
76
+ 旧 `PandaApp.connectIframe()` 与 `PandaApp.connectUtilityProcess()` 标记 `@deprecated`,仍委托同一连接实现。初始化包路径继续使用 `/browser`、`/node`,不嗅探运行环境。
77
+
78
+ ---
19
79
 
20
80
  ## 快速开始
21
81
 
22
- ### Session / Standalone 前端
82
+ ### 示例 1:Session 应用前端
23
83
 
24
84
  ```typescript
25
85
  import { PandaApp } from '@panda-agent/panda-app-sdk/browser';
26
86
 
27
- const app = PandaApp.connectIframe();
28
- const conv = await app.conversation.createForApp({ title: 'My task', ephemeral: true });
29
- await app.conversation.submitMessage(conv.conversationId, 'Hello');
87
+ const app = PandaApp.connect();
88
+
89
+ // 1. 创建应用私有会话(ephemeral:用完即焚)
90
+ const conv = await app.conversation.createForApp({
91
+ title: 'My task',
92
+ ephemeral: true,
93
+ });
94
+
95
+ // 2. 订阅 turn / message 事件(Push 通道)
96
+ const sub = await app.conversation.subscribe({
97
+ conversationId: conv.conversationId,
98
+ topics: ['turn', 'message'],
99
+ onEvent: (ev) => {
100
+ if (ev.type === 'message.streaming') { /* 流式更新 UI */ }
101
+ if (ev.type === 'turn.completed') { /* 完成 */ }
102
+ },
103
+ });
104
+
105
+ // 3. 提交消息 → Agent turn
106
+ await app.conversation.submitMessage(conv.conversationId, '帮我写一份周报');
107
+
108
+ // 4. 或阻塞等待本轮终态
109
+ const turn = await app.conversation.awaitTurn({
110
+ conversationId: conv.conversationId,
111
+ timeoutMs: 60_000,
112
+ });
113
+
114
+ await sub.unsubscribe();
30
115
  ```
31
116
 
32
- 使用 Vite / webpack 将 browser 入口打进前端 bundle。
117
+ ### 示例 2:Standalone / Headless 后端
33
118
 
34
- ### Standalone / Headless 后端(`package/main.mjs`)
119
+ `package/main.mjs`(Utility Process 入口,建议 esbuild 打成单文件):
35
120
 
36
121
  ```typescript
37
- import { PandaApp, APP_UTILITY_READY } from '@panda-agent/panda-app-sdk/node';
122
+ import { PandaApp } from '@panda-agent/panda-app-sdk/node';
38
123
 
39
- const app = PandaApp.connectUtilityProcess();
124
+ const app = PandaApp.connect();
40
125
 
41
- process.parentPort.postMessage({
42
- type: APP_UTILITY_READY,
43
- appId: process.env.PANDA_APP_ID ?? 'unknown',
126
+ // 初始化必要的本地存储、服务与处理器后,报告后台就绪。
127
+ await app.ready();
128
+
129
+ // 直接执行工具 / 调用模型
130
+ const result = await app.tools.execute({
131
+ toolName: 'run_shell',
132
+ workspace: { kind: 'default' },
133
+ args: { command: 'echo ok', description: 'smoke' },
44
134
  });
135
+ ```
136
+
137
+ ### 示例 3:能力检测(runtime.supports)
138
+
139
+ 宿主(Panda Desktop)内置 SDK 版本可能落后于应用依赖的版本,用 `runtime.supports()` 在运行时判断:
140
+
141
+ ```typescript
142
+ import { PandaApp } from '@panda-agent/panda-app-sdk/browser';
143
+
144
+ const app = PandaApp.connect();
145
+
146
+ if (await app.runtime.supports('0.2.0')) {
147
+ // 使用 0.2.0 引入的能力
148
+ } else {
149
+ // 降级提示:请升级 Panda Desktop
150
+ console.warn('当前 Panda Desktop 版本过低,部分功能不可用');
151
+ }
152
+
153
+ // 也可以读取宿主环境与版本
154
+ const host = await app.runtime.getHost();
155
+ console.log(host.pandaVersion, host.appSdkVersion, host.platform);
156
+ ```
157
+
158
+ ---
159
+
160
+ ## API 参考
161
+
162
+ > `PandaApp.connect()` 同步返回 SDK 客户端;`app.ready()` 和宿主调用返回 `Promise`。订阅事件处理函数同步注册为回调,订阅创建及宿主取消订阅为异步。invoke 失败时 reject,错误信息为宿主返回的 `[AppPlatform]` / `[panda-runtime]` 前缀错误。
163
+ > 「所需权限」指应用 `panda-app.json` 的 `permissions` 中必须声明的权限;未授权调用会被拒绝。
164
+
165
+ ### auth — 用户
166
+
167
+ #### `auth.getCurrentUser()`
168
+
169
+ ```typescript
170
+ getCurrentUser(): Promise<AppUser | null>
171
+ ```
172
+
173
+ 返回当前登录用户;未登录时返回 `null`。
174
+
175
+ `AppUser`:`{ id, username, email, displayName?: string | null, avatar?: string | null }`
176
+
177
+ - 所需权限:`auth.read`
178
+
179
+ ### model — 模型
180
+
181
+ #### `model.list(options?)`
182
+
183
+ ```typescript
184
+ list(options?: { enabledOnly?: boolean; visibleOnly?: boolean; scene?: string }): Promise<ModelListItem[]>
185
+ ```
186
+
187
+ 列出可用模型(默认仅已启用且可见的模型)。`scene` 可过滤特定场景(如 `chat`)。
188
+
189
+ `ModelListItem`:`{ id, displayName, providerName, description? }`
190
+
191
+ - 所需权限:`model.read`
192
+
193
+ #### `model.resolveForScene(request?)`
194
+
195
+ ```typescript
196
+ resolveForScene(request?: { scene?: string; preferredModelId?: string }): Promise<ModelListItem | null>
197
+ ```
198
+
199
+ 按场景解析最高优先级模型;`preferredModelId` 指定偏好模型时优先返回。`scene` 缺省为 `chat`。
200
+
201
+ - 所需权限:`model.read`
202
+
203
+ #### `model.chat(request)`
204
+
205
+ ```typescript
206
+ chat(request: ModelChatRequest): Promise<ModelChatResult>
207
+ ```
208
+
209
+ 非流式直调模型(适合工作流 LLM 节点等场景,不经过 Agent turn)。
210
+
211
+ `ModelChatRequest`:`{ modelId?, messages: { role: 'system'|'user'|'assistant'; content: string }[], temperature?, maxTokens? }`
212
+
213
+ `ModelChatResult`:`{ content, modelId, displayName? }`
214
+
215
+ - 所需权限:`model.chat`
216
+
217
+ ### workspace — 工作区
218
+
219
+ #### `workspace.list(request)`
220
+
221
+ ```typescript
222
+ list(request: { limit: number; includeEphemeral?: boolean }): Promise<WorkspaceListItem[]>
223
+ ```
224
+
225
+ 最近工作区列表,按最近打开时间降序。`limit` 必填(1–100)。
226
+
227
+ `WorkspaceListItem`:`{ workspaceId, displayName, path?, lastOpened?, firstAdded, bindingKind: 'real'|'ephemeral'|'unknown', registeredInProcess }`
228
+
229
+ - 所需权限:`workspace.read`
230
+
231
+ #### `workspace.ensureLocal(request)`
232
+
233
+ ```typescript
234
+ ensureLocal(request: { path: string; touch?: boolean }): Promise<WorkspaceListItem>
235
+ ```
236
+
237
+ 将本地路径注册为工作区并返回其信息。`touch` 默认 `true`(更新最近打开时间)。
238
+
239
+ - 所需权限:`workspace.bind`
240
+
241
+ ### artifacts — 制品
242
+
243
+ 工作区制品的只读访问(列表 / 快照 / 文本读取 / Data URL 读取)。
244
+
245
+ ```typescript
246
+ list(request: ArtifactsListRequest): Promise<ArtifactsListResult>
247
+ snapshot(request: ArtifactsListRequest): Promise<ArtifactsSnapshotResult>
248
+ readText(request: ArtifactsReadRequest): Promise<ArtifactReadTextResult>
249
+ readDataUrl(request: ArtifactsReadRequest): Promise<ArtifactReadDataUrlResult>
250
+ ```
251
+
252
+ - `ArtifactsListRequest`:`{ workspace: WorkspaceRef, root?, path?, depth? }`(`depth` 递归深度 1–5)
253
+ - `ArtifactsReadRequest`:`{ workspace: WorkspaceRef, root?, path }`
254
+ - `WorkspaceRef`:`{ kind: 'id', workspaceId } | { kind: 'localPath', path } | { kind: 'default' }`
255
+ - `ArtifactsListResult`:`{ entries: { path, size, mtime, kind: 'file'|'directory' }[], version }`
256
+ - `ArtifactReadTextResult`:`{ path, content, byteLength, mtime }`
257
+ - `ArtifactReadDataUrlResult`:`{ path, dataUrl, mimeType, byteLength, mtime }`
258
+
259
+ - 所需权限:`files.read.workspace`(使用非会话工作区时另需 `workspace.bind`)
260
+
261
+ ### conversation — 会话
262
+
263
+ 会话是应用与 Agent 交互的核心对象。应用可以创建用户会话(`create`)或私有会话(`createForApp`)、绑定已有会话(`attach`)、提交消息并订阅事件。
264
+
265
+ #### `conversation.create(options?)`
266
+
267
+ ```typescript
268
+ create(options?: CreateConversationOptions): Promise<CreateConversationResult>
269
+ ```
270
+
271
+ 创建**用户会话**:会话归属用户,落盘后出现在主界面侧栏,用户可打开继续对话;应用自动获得该会话的读写绑定。
272
+
273
+ `CreateConversationOptions`:
274
+
275
+ | 字段 | 类型 | 说明 |
276
+ |------|------|------|
277
+ | `title?` | `string` | 会话标题;省略时由宿主生成默认标题 |
278
+ | `selectedModelId?` | `string` | 指定模型;省略时沿用该工作区的模型选择与全局默认 |
279
+ | `workspace?` | `WorkspaceRef` | 工作目录;省略时使用宿主默认工作区 |
280
+
281
+ `CreateConversationResult`:`{ conversationId, title, workspaceId }`。`title` 回显实际标题;省略 `title` 时可能为空字符串(宿主默认标题已落盘,但那时会话还没有显式标题),需要固定标题时请显式传入。
282
+
283
+ - 返回时保证会话已落盘(transcript 头与摘要),可被侧栏列出并在重启后恢复。
284
+ - 不写入 `appOrigin`:不自动挂载应用私有 MCP 工具,也不改变应用主 Agent 选择;需要应用工具时请把应用挂到该会话(见 `session` 命名空间)。
285
+ - session 呈现(`session.getContext().presentation === 'session'`)的实例不能调用:它只能操作被挂载的那个会话,新建的会话对它不可读写,调用会报 `conversation.create is not available to session tokens`。
286
+ - 所需权限:`conversation.create`(使用 `localPath` 工作区时另需 `workspace.bind`)。
287
+
288
+ 重启 Panda 后应用需重新 `attach` 并 `subscribe`(token、会话绑定与订阅都是内存态);建议应用用 `storage.global` 保存业务 ID 与 `conversationId` 的对应关系。
289
+
290
+ #### `conversation.createForApp(options?)`
291
+
292
+ ```typescript
293
+ createForApp(options?: CreateForAppOptions): Promise<CreateForAppResult>
294
+ ```
295
+
296
+ 创建应用私有会话。`CreateForAppOptions`:
297
+
298
+ | 字段 | 类型 | 说明 |
299
+ |------|------|------|
300
+ | `title?` | `string` | 会话标题 |
301
+ | `selectedModelId?` | `string` | 指定模型 |
302
+ | `ephemeral?` | `boolean` | `true` 时创建用完即焚的临时会话(可被 `disposeForApp` 销毁) |
303
+ | `workspace?` | `WorkspaceRef` | 关联工作区 |
304
+ | `workspaceId?` | `string` | **已废弃**,请用 `workspace` |
305
+
306
+ `CreateForAppResult`:`{ conversationId, title, ephemeral, workspaceId }`
307
+
308
+ - 所需权限:`conversation.create`(使用工作区时另需 `workspace.bind`)
309
+
310
+ #### `conversation.disposeForApp(conversationId)`
311
+
312
+ 销毁本应用拥有的 ephemeral 会话(非 ephemeral 或正忙时会失败;运行中的 turn 会先尝试取消)。
313
+
314
+ - 所需权限:`conversation.create`
315
+
316
+ #### `conversation.listForApp(workspaceId?)`
317
+
318
+ ```typescript
319
+ listForApp(workspaceId?: string): Promise<unknown>
320
+ ```
321
+
322
+ 列出本应用创建的会话(可按工作区过滤)。
323
+
324
+ - 所需权限:`conversation.read`
325
+
326
+ #### `conversation.attach(conversationId)` / `conversation.detach(conversationId)`
327
+
328
+ ```typescript
329
+ attach(conversationId: string): Promise<{ conversationId: string }>
330
+ detach(conversationId: string): Promise<unknown>
331
+ ```
332
+
333
+ 将已有会话绑定/解绑到本应用(`attach` 后应用可读写该会话)。目标会话尚未加载时,宿主按其记录的工作区从磁盘恢复后再绑定,不切换主界面焦点。私有会话(`createForApp` 创建)只在此应用实例内可用:其他应用的私有会话会被拒绝,自己的私有会话也不能冷恢复,重启后请重新 `createForApp`。`attach` 所需权限:`conversation.read`。
334
+
335
+ #### `conversation.submitMessage(conversationId, prompt)`
336
+
337
+ ```typescript
338
+ submitMessage(conversationId: string, prompt: string): Promise<{ conversationId: string; accepted: boolean; deferred: boolean }>
339
+ ```
340
+
341
+ 向会话提交用户消息并触发 Agent turn。返回 `deferred: true` 表示会话忙,消息已入队稍后处理。
342
+
343
+ - 所需权限:`conversation.write`
344
+
345
+ #### `conversation.getMessages(conversationId)`
346
+
347
+ 获取会话消息快照(数组形式,非订阅)。
348
+
349
+ - 所需权限:`conversation.read`
350
+
351
+ #### `conversation.subscribe(options)`
352
+
353
+ ```typescript
354
+ subscribe(options: ConversationSubscribeOptions & { onEvent?: AppEventHandler }): Promise<ConversationSubscription>
355
+ ```
356
+
357
+ 订阅会话事件流(Push 通道)。`ConversationSubscribeOptions`:
358
+
359
+ | 字段 | 类型 | 说明 |
360
+ |------|------|------|
361
+ | `conversationId` | `string` | 会话 ID |
362
+ | `topics?` | `AppEventTopic[]` | 事件主题,缺省 `turn` + `message`(见[事件订阅参考](#事件订阅参考)) |
363
+ | `fromTurnId?` | `string` | 从指定 turn 开始回放 |
364
+ | `includeMessagesOnTurnEnd?` | `boolean` | turn 结束时附带消息快照 |
365
+
366
+ `onEvent` 回调接收 `AppEventEnvelope`:`{ topic, type, conversationId, turnId?, messageId?, timestamp, payload, isLast? }`。
367
+
368
+ 返回 `{ subscriptionId, unsubscribe: () => Promise<void> }`,用毕务必 `unsubscribe()`。
369
+
370
+ - 所需权限:`conversation.read`
371
+
372
+ #### `conversation.awaitTurn(options)`
373
+
374
+ ```typescript
375
+ awaitTurn(options: { conversationId: string; turnId?: string; timeoutMs?: number }): Promise<unknown>
376
+ ```
377
+
378
+ 阻塞至指定 turn 到达终态(含 `uiMessages` 快照),适合「提交后等结果」的简化流程;超时 reject。
379
+
380
+ - 所需权限:`conversation.read`
381
+
382
+ ### session — 会话上下文与挂载
383
+
384
+ #### `session.getContext()`
385
+
386
+ ```typescript
387
+ getContext(): Promise<SessionContext>
388
+ ```
389
+
390
+ 返回当前应用实例上下文:`{ appId, presentation: 'standalone'|'session'|'headless', conversationId?, workspaceId? }`。
391
+
392
+ - 所需权限:隐式(有效会话 token 即可)
393
+
394
+ #### `session.mount(conversationId, appId)` / `session.unmount(conversationId, appId)` / `session.listMounts(conversationId)`
395
+
396
+ Session 应用挂载管理:把应用挂载到指定会话(同一 `conversationId + appId` 幂等)、解除挂载、列出会话已挂载应用。
397
+
398
+ ### todo — 任务清单
399
+
400
+ #### `todo.get(conversationId)`
401
+
402
+ ```typescript
403
+ get(conversationId: string): Promise<TodoSnapshot>
404
+ ```
405
+
406
+ 会话的任务清单快照。`TodoSnapshot`:`{ content, items: { id, content, completed, current }[] }`
407
+
408
+ - 所需权限:`conversation.read`
409
+
410
+ ### memory — 会话记忆
411
+
412
+ #### `memory.listSessionEntries(conversationId)`
413
+
414
+ ```typescript
415
+ listSessionEntries(conversationId: string): Promise<MemorySessionEntries>
416
+ ```
417
+
418
+ 会话记忆条目列表。`MemorySessionEntries`:`{ conversationId, workspaceId?, workspacePath?, workspaceRealm?, entries: { filename, type: 'user'|'feedback'|'project'|'reference', description, modifiedAt }[] }`
419
+
420
+ - 所需权限:`conversation.read`
421
+
422
+ ### ingress — 消息提交
423
+
424
+ #### `ingress.submit(conversationId, prompt)`
425
+
426
+ ```typescript
427
+ submit(conversationId: string, prompt: string): Promise<{ conversationId: string; status: string }>
428
+ ```
429
+
430
+ 向会话提交消息(与 `submitMessage` 同源,返回 `status`:`fired` / `deferred`)。
431
+
432
+ - 所需权限:`conversation.write`
433
+
434
+ ### agent — 智能体能力
435
+
436
+ #### `agent.invokeSkill(request)`
437
+
438
+ ```typescript
439
+ invokeSkill(request: InvokeSkillRequest): Promise<InvokeSkillResult>
440
+ ```
441
+
442
+ 在指定会话中调用 Skill(应用只能调用自己声明拥有的 Skill)。
443
+
444
+ `InvokeSkillRequest`:`{ conversationId, skillName, action, payload, displayText?, idempotencyKey?, projectRef? }`
445
+
446
+ `InvokeSkillResult`:`{ conversationId, status: 'fired' | 'deferred' }`
447
+
448
+ - 所需权限:`agent.invoke`
45
449
 
46
- // 使用 app.conversation / app.tools 等 API
450
+ #### `agent.invokeSubagent(request)`
451
+
452
+ ```typescript
453
+ invokeSubagent(request: InvokeSubagentRequest): Promise<InvokeSubagentResult>
454
+ ```
455
+
456
+ 在一次性临时会话中按 `subagentType` 同步执行子代理,返回结果载荷(适合工作流「子 Agent 节点」)。
457
+
458
+ `InvokeSubagentRequest`:`{ subagentType, prompt, description?, model?, readonly?, workspace?, invocationId?, timeoutMs? }`(`timeoutMs` 默认 5 分钟;`invocationId` 可用于提前取消)
459
+
460
+ `InvokeSubagentResult`:`{ content, outputPreview, outputFilePath?, workId, agentType, subTranscriptPath? }`
461
+
462
+ - 所需权限:`agent.invokeSubagent`
463
+
464
+ #### `agent.cancelSubagent(invocationId)`
465
+
466
+ ```typescript
467
+ cancelSubagent(invocationId: string): Promise<{ cancelled: boolean; workId?: string }>
47
468
  ```
48
469
 
49
- `APP_UTILITY_READY` 为可选握手消息(宿主仅记录日志,不阻塞启动)。
470
+ 取消进行中的子代理执行。
471
+
472
+ - 所需权限:`agent.invokeSubagent`
473
+
474
+ ### ui — 界面
475
+
476
+ #### `ui.theme()`
477
+
478
+ ```typescript
479
+ theme(): Promise<{ theme: 'light' | 'dark' | 'system' }>
480
+ ```
481
+
482
+ 宿主 UI 明暗主题偏好,内嵌应用据此跟随宿主。
483
+
484
+ - 所需权限:`ui.theme`
485
+
486
+ #### `ui.sessionSurface.getState()` / `ui.sessionSurface.show(options?)` / `ui.sessionSurface.collapse()`
487
+
488
+ Session 面板(右侧会话面板)状态读取 / 展开 / 收起。
50
489
 
51
- 建议用 esbuild 将 node 入口打成单文件 `main.mjs` 再随应用 zip 分发。
490
+ `SessionSurfaceState`:`{ supported, mounted, visible, active, maximized }`
491
+
492
+ `show(options?: { activate?: boolean; reason?: string })`
493
+
494
+ ### storage — 存储
495
+
496
+ 应用私有 KV 存储,值须为 JSON 可序列化数据(`AppJsonValue`)。
497
+
498
+ ```typescript
499
+ global: {
500
+ get(key: string): Promise<AppJsonValue | undefined>
501
+ set(key: string, value: AppJsonValue): Promise<unknown>
502
+ delete(key: string): Promise<unknown>
503
+ list(prefix?: string): Promise<AppStorageListItem[]>
504
+ }
505
+ session: {
506
+ get(key: string): Promise<AppJsonValue | undefined>
507
+ set(key: string, value: AppJsonValue): Promise<unknown>
508
+ delete(key: string): Promise<unknown>
509
+ list(prefix?: string): Promise<AppStorageListItem[]>
510
+ }
511
+ ```
512
+
513
+ - `global`:应用级存储,跨会话共享
514
+ - `session`:会话级存储,仅当前 Session 会话可用(非 Session 上下文会报错)
515
+ - `AppStorageListItem`:`{ key, value }`
516
+
517
+ - 所需权限:`storage.app`
518
+
519
+ ### tools — 工具
520
+
521
+ 不经过 Agent turn,直接调用宿主内置工具。
522
+
523
+ #### `tools.list(request)`
524
+
525
+ ```typescript
526
+ list(request: { workspace?: WorkspaceRef; workspaceId?: string }): Promise<Array<{ name: string; displayName: string; description: string; kind: string }>>
527
+ ```
528
+
529
+ 列出应用可用的工具(宿主按 denylist 过滤后的集合)。
530
+
531
+ - 所需权限:`tools.execute`
532
+
533
+ #### `tools.execute(request)`
534
+
535
+ ```typescript
536
+ execute(request: {
537
+ toolName: string;
538
+ args?: Record<string, unknown>;
539
+ workspace?: WorkspaceRef;
540
+ workspaceId?: string;
541
+ }): Promise<{
542
+ toolName: string;
543
+ workspaceId: string;
544
+ ok: boolean;
545
+ llmContent?: string;
546
+ error?: { message: string; type?: string };
547
+ displayData?: unknown;
548
+ }>
549
+ ```
550
+
551
+ 直接执行工具。`ok: false` 时 `error` 携带失败信息。
552
+
553
+ - 所需权限:`tools.execute`(安装授权后运行时不逐次确认)
554
+
555
+ ### runtime — 运行时与宿主
556
+
557
+ #### `runtime.now()`
558
+
559
+ ```typescript
560
+ now(): Promise<{ timestamp: number; iso: string; timeZone: string; utcOffsetMinutes: number }>
561
+ ```
562
+
563
+ 宿主当前时间与时区。
564
+
565
+ - 所需权限:隐式(有效会话 token 即可)
566
+
567
+ #### `runtime.getHost()`
568
+
569
+ ```typescript
570
+ getHost(): Promise<{
571
+ platform: string; arch: string; osName: string; hostProduct: string;
572
+ shell: string; shellKind: string; homeDir: string;
573
+ pandaVersion: string; appSdkVersion: string;
574
+ }>
575
+ ```
576
+
577
+ 宿主只读环境信息。`appSdkVersion` 与 npm 包版本对齐。
578
+
579
+ - 所需权限:隐式
580
+
581
+ #### `runtime.supports(requiredVersion)`
582
+
583
+ ```typescript
584
+ supports(requiredVersion: string): Promise<boolean>
585
+ ```
586
+
587
+ 判断宿主 SDK 版本是否满足最低要求(`>= requiredVersion`)。用于调用新版 API 前的能力检测与降级提示(见[示例 3](#示例-3能力检测runtimesupports))。
588
+
589
+ - 所需权限:隐式
590
+
591
+ ### dialog — 原生对话框
592
+
593
+ iframe 无法可靠触发系统下载/文件选择,宿主提供 **Electron 原生对话框 + token 保护 IO**:选择路径后凭 token 读写,路径不可绕过。
594
+
595
+ #### 组合便捷方法(推荐)
596
+
597
+ ```typescript
598
+ pickAndReadFile(options?: DialogPickAndReadFileOptions): Promise<DialogReadPickedFileResult | null>
599
+ saveFile(options: DialogSaveFileOptions): Promise<string | null>
600
+ ```
601
+
602
+ - `pickAndReadFile`:打开文件选择框并读取选中文件;取消返回 `null`。`DialogPickAndReadFileOptions` = `DialogShowOpenRequest & { encoding?: 'utf8'|'base64' }`
603
+ - `saveFile`:选择保存路径并写入内容;取消返回 `null`,成功返回 `filePath`。`DialogSaveFileOptions`:`{ content, suggestedName?, defaultPath?, title?, encoding?, filters? }`
604
+
605
+ #### 分步方法(需要更多控制时)
606
+
607
+ ```typescript
608
+ showOpen(request?: DialogShowOpenRequest): Promise<DialogShowOpenResult>
609
+ readPickedFile(request: DialogReadPickedFileRequest): Promise<DialogReadPickedFileResult>
610
+ releasePick(pickToken: string): Promise<unknown>
611
+ showSave(request?: DialogShowSaveRequest): Promise<DialogShowSaveResult>
612
+ writeSaveFile(request: DialogWriteSaveFileRequest): Promise<DialogWriteSaveFileResult>
613
+ ```
614
+
615
+ - `DialogShowOpenRequest`:`{ title?, defaultPath?, filters?, multiSelections?, openDirectory?, openFile? }`
616
+ - `DialogShowOpenResult`:`{ canceled: true } | { canceled: false, filePaths, pickToken }`(`pickToken` 一次性,需凭它读取文件,读后 `releasePick` 释放)
617
+ - `DialogReadPickedFileResult`:`{ filePath, encoding, content, byteLength }`
618
+ - `DialogShowSaveRequest`:`{ title?, defaultPath?, suggestedName?, filters? }`
619
+ - `DialogShowSaveResult`:`{ canceled: true } | { canceled: false, filePath, saveToken }`(`saveToken` 一次性)
620
+ - `DialogWriteSaveFileResult`:`{ filePath, byteLength }`
621
+ - 单次 token IO 有大小上限(50 MB);`readPickedFile` / `writeSaveFile` 前务必先持有对应 token
622
+
623
+ - 所需权限:`native.dialog.open`(打开/读取);`native.dialog.save`(保存/写入)
624
+
625
+ ---
626
+
627
+ ## 事件订阅参考
628
+
629
+ `conversation.subscribe` 的 `topics` 取值(白名单):
630
+
631
+ | topic | 事件 type 示例 | 说明 |
632
+ |-------|----------------|------|
633
+ | `turn` | `turn.started` / `turn.completed` / `turn.failed` / `turn.cancelled` | 轮次生命周期 |
634
+ | `message` | `message.user_added` / `message.assistant_started` / `message.streaming` / `message.completed` / `messages.snapshot` | 消息与流式输出 |
635
+ | `tool` | `tool.*` | 工具调用(需相应权限) |
636
+ | `metrics` | `metrics.updated` | 上下文指标 |
637
+ | `conversation` | — | 会话级事件 |
638
+
639
+ 事件包结构 `AppEventEnvelope`:
640
+
641
+ ```typescript
642
+ {
643
+ topic: string;
644
+ type: string; // 稳定公开名,如 'turn.completed'
645
+ conversationId: string;
646
+ turnId?: string;
647
+ messageId?: string;
648
+ timestamp: number;
649
+ payload: unknown;
650
+ isLast?: boolean; // 流式最后一帧
651
+ }
652
+ ```
653
+
654
+ > 默认不暴露 `thinking` 类内容;流式 `message.streaming` 由宿主按 50ms 节流合并。
655
+
656
+ ---
52
657
 
53
658
  ## 应用类型对照
54
659
 
@@ -58,6 +663,32 @@ process.parentPort.postMessage({
58
663
  | `standalone` | UI 需要 | `runtime.processEntry` 需要 |
59
664
  | `headless` | 不需要 | 必须 |
60
665
 
666
+ ---
667
+
668
+ ## 版本与兼容性
669
+
670
+ - 版本遵循 **semver**。`0.x` 阶段为快速演进期:**minor(如 0.1 → 0.2)可能包含新增 API**,破坏性变更会发布说明并给出迁移指引;进入 `1.0.0` 后承诺「稳定 API 不破坏」。
671
+ - **manifest 声明最低 SDK 版本**:在应用 `panda-app.json` 中声明 `engines.pandaAppSdk`(格式必须为 `x.y.z`,语义等价 `>=`):
672
+
673
+ ```json
674
+ {
675
+ "id": "acme.demo",
676
+ "name": "Demo",
677
+ "version": "1.0.0",
678
+ "publisher": "acme",
679
+ "presentation": "session",
680
+ "runtime": { "packageEntry": "package/index.html" },
681
+ "engines": { "pandaAppSdk": "0.1.0" },
682
+ "permissions": ["conversation.create", "conversation.read", "conversation.write"]
683
+ }
684
+ ```
685
+
686
+ - **安装拦截**:当宿主 SDK 版本低于应用声明的 `engines.pandaAppSdk` 时,安装会被拒绝并提示升级 Panda Desktop。
687
+ - **运行时检测**:即使安装了,也建议用 `runtime.supports()` 做能力检测(见示例 3),避免调用不存在的 API。
688
+ - `runtime.getHost()` 返回的 `appSdkVersion` 与 npm 包版本对齐;发布新版 SDK 时宿主内置版本会同步更新。
689
+
690
+ ---
691
+
61
692
  ## 高级用法
62
693
 
63
694
  需要裸 `invoke`(例如共享验收脚本)时:
@@ -70,31 +701,113 @@ const app = createAppSdk(transport);
70
701
  const invoke = (path: string, args?: unknown[]) => transport.invoke(path, args);
71
702
  ```
72
703
 
73
- ## 开发与构建(monorepo 内)
704
+ ---
705
+
706
+ ## 从零构建一个 Panda 应用
707
+
708
+ 1. **准备应用目录**(以下结构):
709
+
710
+ ```
711
+ my-app/
712
+ ├── panda-app/panda-app.json # manifest(声明 id / 入口 / 权限 / engines)
713
+ ├── package/
714
+ │ ├── index.html # UI 入口(Session / Standalone)
715
+ │ ├── src/… # 前端源码
716
+ │ └── main.mjs # Standalone / Headless 后端(esbuild 打包产物)
717
+ └── resources/ # 展示用图标、封面(可选)
718
+ ```
719
+
720
+ 2. **编写 manifest**(见[版本与兼容性](#版本与兼容性)示例),声明 `runtime.packageEntry`、`permissions`、可选 `engines.pandaAppSdk`。
721
+
722
+ 3. **安装 SDK 并编写代码**:
723
+
724
+ ```bash
725
+ npm install @panda-agent/panda-app-sdk
726
+ ```
727
+
728
+ 4. **打包前端 bundle**:将 `browser` 入口打进 `index.html` 引用的 JS(Vite / webpack / esbuild 均可);node 后端用 esbuild 打成单文件:
729
+
730
+ ```bash
731
+ npx esbuild src/main.ts --bundle --platform=node --format=esm --outfile=package/main.mjs
732
+ ```
733
+
734
+ 5. **在 Panda Desktop 中安装**:打开「智能应用」→ 右上角「本地安装」→ 选择应用目录(或打包成 zip 选择压缩包)→ 授权权限 → 启用。
735
+
736
+ ---
737
+
738
+ ## 构建与发布 SDK
739
+
740
+ > 以下步骤在 **monorepo 内** 的 `packages/app-sdk` 目录执行。
741
+
742
+ ### 1. 准备环境
74
743
 
75
744
  ```bash
76
745
  cd packages/app-sdk
77
746
  npm install
78
- npm run build # 产出 dist/browser、dist/node
79
- npm run vendor-fixtures # 复制到 panda-desktop/fixtures/*/package/vendor/
80
- npm test
81
747
  ```
82
748
 
83
- 修改 SDK 后需重新 `build` 并 `vendor-fixtures`,桌面 demo 才会更新。
749
+ ### 2. 本地构建(产出 `dist/browser`、`dist/node`)
750
+
751
+ ```bash
752
+ npm run build
753
+ ```
754
+
755
+ `build` 会:
756
+ - esbuild 打包 browser / node 双入口(ESM,`es2022` 目标)
757
+ - `tsc` 生成类型声明 `dist/*/index.d.ts`
84
758
 
85
- ## 发布到 npm
759
+ ### 3. 产物自检
86
760
 
87
761
  ```bash
88
- cd packages/app-sdk
89
- npm run release:publish:dry-run # 干跑
90
- npm run release:publish # 发布到 @panda-agent(需 npm 组织权限)
91
- npm run release:publish:tester # 先发 tester tag
762
+ npm run verify-dist
92
763
  ```
93
764
 
94
- 发布前会校验 `package.json` version 与 Core `PANDA_APP_SDK_VERSION` 一致。本包 `.npmrc` 指向 `registry.npmjs.org`(勿使用仓库根目录的 npmmirror 配置)。
765
+ 校验项:browser bundle 不得引用 `node:*` 内置模块;node bundle 必须包含 `parentPort` 传输;不发布 sourcemap;两个 bundle 均可直接 import。
766
+
767
+ ### 4. 单元测试
768
+
769
+ ```bash
770
+ npm test # = npm run build && vitest run
771
+ ```
772
+
773
+ ### 5.(monorepo 内)同步桌面内置 demo 的 vendor 拷贝
774
+
775
+ ```bash
776
+ npm run vendor-fixtures
777
+ ```
778
+
779
+ 将 `dist` 复制到 `panda-desktop/fixtures/*/package/vendor/` 与内置插件目录,使桌面 demo 使用新 SDK。
780
+
781
+ ### 6. 打包 tarball(可分发 / 本地安装验证)
782
+
783
+ ```bash
784
+ npm pack # 触发 prepack → build + verify-dist,产出 *.tgz
785
+ ```
786
+
787
+ ### 7. 发布到 npm
788
+
789
+ ```bash
790
+ npm run release:publish:dry-run # 干跑,确认产物与校验通过
791
+ npm run release:publish:tester # 先发布到 tester tag 供内测
792
+ npm run release:publish # 发布到 latest(需 @panda-agent 组织权限)
793
+ ```
794
+
795
+ 发布前会自动校验:**SDK 版本与宿主声明的 SDK 版本必须一致**(仓库根 `node scripts/verify/check-app-sdk-version.mjs`)。升级 SDK 版本时,请同步更新宿主侧版本声明后一并发布,否则发布会被拦截。
796
+
797
+ > 本包 `.npmrc` 指向 `registry.npmjs.org`,请勿使用仓库根目录的 npmmirror 配置。
798
+
799
+ ---
800
+
801
+ ## 示例演示
95
802
 
96
- ## 版本
803
+ 仓库内置了可直接演示的示例应用(源码在 `panda-desktop/fixtures/`,已通过 `vendor-fixtures` 携带最新 SDK):
97
804
 
98
- `runtime.getHost().appSdkVersion` 与 npm 包版本应对齐。发布时同步更新 Core 中 `PANDA_APP_SDK_VERSION`(`app-platform-runtime-types.ts`)。
805
+ | 示例 | 类型 | 演示内容 |
806
+ |------|------|----------|
807
+ | `demo-hello-app` | Standalone | 全能力冒烟:上下文 / 工具直连 / 事件订阅 / 原生对话框,UI 上有「运行验收」按钮 |
808
+ | `demo-session-app` | Session | 会话内挂载:创建会话、ingress 提交、事件流渲染 |
809
+ | `demo-headless-app` | Headless | 无 UI 后端:UP 启动握手 + 会话 API |
810
+ | `npm-sdk-test-app` | Standalone | 通过 npm 安装 SDK(非 vendor 拷贝)的独立应用示例 |
811
+ | `memory-viewer-app` | Standalone | 会话记忆读取展示 |
99
812
 
100
- 建议 Panda Desktop 版本不低于 SDK 发布说明中的最低宿主版本。
813
+ 演示步骤:Panda Desktop → 智能应用 → 本地安装 → 选择对应 fixture 目录(如 `panda-desktop/fixtures/demo-hello-app`)→ 授权 → 启用 → 从侧栏「应用」打开。