@panda-agent/panda-app-sdk 0.1.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 ADDED
@@ -0,0 +1,100 @@
1
+ # @panda-agent/panda-app-sdk
2
+
3
+ Panda 智能应用 SDK — 在 iframe 或 Utility Process 中调用 Panda Desktop 提供的应用平台 API。
4
+
5
+ SDK 为纯客户端 RPC 封装,**不包含** Panda Core 运行时。应用须在 Panda Desktop 中安装并运行。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install @panda-agent/panda-app-sdk
11
+ ```
12
+
13
+ ## 入口
14
+
15
+ | 子路径 | 运行环境 | 用途 |
16
+ |--------|----------|------|
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()` |
19
+
20
+ ## 快速开始
21
+
22
+ ### Session / Standalone 前端
23
+
24
+ ```typescript
25
+ import { PandaApp } from '@panda-agent/panda-app-sdk/browser';
26
+
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');
30
+ ```
31
+
32
+ 使用 Vite / webpack 将 browser 入口打进前端 bundle。
33
+
34
+ ### Standalone / Headless 后端(`package/main.mjs`)
35
+
36
+ ```typescript
37
+ import { PandaApp, APP_UTILITY_READY } from '@panda-agent/panda-app-sdk/node';
38
+
39
+ const app = PandaApp.connectUtilityProcess();
40
+
41
+ process.parentPort.postMessage({
42
+ type: APP_UTILITY_READY,
43
+ appId: process.env.PANDA_APP_ID ?? 'unknown',
44
+ });
45
+
46
+ // 使用 app.conversation / app.tools 等 API
47
+ ```
48
+
49
+ `APP_UTILITY_READY` 为可选握手消息(宿主仅记录日志,不阻塞启动)。
50
+
51
+ 建议用 esbuild 将 node 入口打成单文件 `main.mjs` 再随应用 zip 分发。
52
+
53
+ ## 应用类型对照
54
+
55
+ | presentation | browser SDK | node SDK |
56
+ |--------------|-------------|----------|
57
+ | `session` | 必须 | 不需要 |
58
+ | `standalone` | UI 需要 | `runtime.processEntry` 需要 |
59
+ | `headless` | 不需要 | 必须 |
60
+
61
+ ## 高级用法
62
+
63
+ 需要裸 `invoke`(例如共享验收脚本)时:
64
+
65
+ ```typescript
66
+ import { createIframeBridgeTransport, createAppSdk } from '@panda-agent/panda-app-sdk/browser';
67
+
68
+ const transport = createIframeBridgeTransport();
69
+ const app = createAppSdk(transport);
70
+ const invoke = (path: string, args?: unknown[]) => transport.invoke(path, args);
71
+ ```
72
+
73
+ ## 开发与构建(monorepo 内)
74
+
75
+ ```bash
76
+ cd packages/app-sdk
77
+ npm install
78
+ npm run build # 产出 dist/browser、dist/node
79
+ npm run vendor-fixtures # 复制到 panda-desktop/fixtures/*/package/vendor/
80
+ npm test
81
+ ```
82
+
83
+ 修改 SDK 后需重新 `build` 并 `vendor-fixtures`,桌面 demo 才会更新。
84
+
85
+ ## 发布到 npm
86
+
87
+ ```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
92
+ ```
93
+
94
+ 发布前会校验 `package.json` version 与 Core `PANDA_APP_SDK_VERSION` 一致。本包 `.npmrc` 指向 `registry.npmjs.org`(勿使用仓库根目录的 npmmirror 配置)。
95
+
96
+ ## 版本
97
+
98
+ `runtime.getHost().appSdkVersion` 与 npm 包版本应对齐。发布时同步更新 Core 中 `PANDA_APP_SDK_VERSION`(`app-platform-runtime-types.ts`)。
99
+
100
+ 建议 Panda Desktop 版本不低于 SDK 发布说明中的最低宿主版本。
@@ -0,0 +1,115 @@
1
+ export { PANDA_APP_BRIDGE_EVENT, PANDA_APP_BRIDGE_INVOKE, PANDA_APP_BRIDGE_INVOKE_RESULT, } from './protocol.js';
2
+ export { createAppSdk } from './create-app-sdk.js';
3
+ export { createIframeBridgeTransport } from './transport/iframe.js';
4
+ export type * from './types.js';
5
+ export declare const PandaApp: {
6
+ connectIframe(): {
7
+ auth: {
8
+ getCurrentUser: () => Promise<import("./types.js").AppUser | null>;
9
+ };
10
+ model: {
11
+ list: (options?: {
12
+ enabledOnly?: boolean;
13
+ visibleOnly?: boolean;
14
+ scene?: string;
15
+ }) => Promise<import("./types.js").ModelListItem[]>;
16
+ resolveForScene: (request?: {
17
+ scene?: string;
18
+ preferredModelId?: string;
19
+ }) => Promise<import("./types.js").ModelListItem | null>;
20
+ };
21
+ workspace: {
22
+ list: (request: {
23
+ limit: number;
24
+ includeEphemeral?: boolean;
25
+ }) => Promise<import("./types.js").WorkspaceListItem[]>;
26
+ ensureLocal: (request: {
27
+ path: string;
28
+ touch?: boolean;
29
+ }) => Promise<import("./types.js").WorkspaceListItem>;
30
+ };
31
+ conversation: {
32
+ createForApp: (options?: import("./types.js").CreateForAppOptions) => Promise<import("./types.js").CreateForAppResult>;
33
+ disposeForApp: (conversationId: string) => Promise<unknown>;
34
+ submitMessage: (conversationId: string, prompt: string) => Promise<unknown>;
35
+ getMessages: (conversationId: string) => Promise<unknown>;
36
+ subscribe: (options: import("./types.js").ConversationSubscribeOptions & {
37
+ onEvent?: import("./types.js").AppEventHandler;
38
+ }) => Promise<{
39
+ subscriptionId: string;
40
+ unsubscribe: () => Promise<void>;
41
+ }>;
42
+ awaitTurn: (options: {
43
+ conversationId: string;
44
+ turnId?: string;
45
+ timeoutMs?: number;
46
+ }) => Promise<unknown>;
47
+ };
48
+ session: {
49
+ mount: (conversationId: string, appId: string, replaceConfirmed?: boolean) => Promise<unknown>;
50
+ unmount: (conversationId: string) => Promise<unknown>;
51
+ getMount: (conversationId: string) => Promise<unknown>;
52
+ };
53
+ ingress: {
54
+ submit: (conversationId: string, prompt: string) => Promise<{
55
+ conversationId: string;
56
+ status: string;
57
+ }>;
58
+ };
59
+ tools: {
60
+ list: (request: {
61
+ workspace?: import("./types.js").WorkspaceRef;
62
+ workspaceId?: string;
63
+ }) => Promise<Array<{
64
+ name: string;
65
+ displayName: string;
66
+ description: string;
67
+ kind: string;
68
+ }>>;
69
+ execute: (request: {
70
+ toolName: string;
71
+ args?: Record<string, unknown>;
72
+ workspace?: import("./types.js").WorkspaceRef;
73
+ workspaceId?: string;
74
+ }) => Promise<{
75
+ toolName: string;
76
+ workspaceId: string;
77
+ ok: boolean;
78
+ llmContent?: string;
79
+ error?: {
80
+ message: string;
81
+ type?: string;
82
+ };
83
+ displayData?: unknown;
84
+ }>;
85
+ };
86
+ runtime: {
87
+ now: () => Promise<{
88
+ timestamp: number;
89
+ iso: string;
90
+ timeZone: string;
91
+ utcOffsetMinutes: number;
92
+ }>;
93
+ getHost: () => Promise<{
94
+ platform: string;
95
+ arch: string;
96
+ osName: string;
97
+ hostProduct: string;
98
+ shell: string;
99
+ shellKind: string;
100
+ homeDir: string;
101
+ pandaVersion: string;
102
+ appSdkVersion: string;
103
+ }>;
104
+ };
105
+ dialog: {
106
+ showOpen: (request?: import("./types.js").DialogShowOpenRequest) => Promise<import("./types.js").DialogShowOpenResult>;
107
+ readPickedFile: (request: import("./types.js").DialogReadPickedFileRequest) => Promise<import("./types.js").DialogReadPickedFileResult>;
108
+ releasePick: (pickToken: string) => Promise<unknown>;
109
+ showSave: (request?: import("./types.js").DialogShowSaveRequest) => Promise<import("./types.js").DialogShowSaveResult>;
110
+ writeSaveFile: (request: import("./types.js").DialogWriteSaveFileRequest) => Promise<import("./types.js").DialogWriteSaveFileResult>;
111
+ pickAndReadFile: (options?: import("./types.js").DialogPickAndReadFileOptions) => Promise<import("./types.js").DialogReadPickedFileResult | null>;
112
+ saveFile: (options: import("./types.js").DialogSaveFileOptions) => Promise<string | null>;
113
+ };
114
+ };
115
+ };
@@ -0,0 +1,212 @@
1
+ // src/protocol.ts
2
+ var PANDA_APP_BRIDGE_INVOKE = "panda-app-bridge:invoke";
3
+ var PANDA_APP_BRIDGE_INVOKE_RESULT = "panda-app-bridge:invoke-result";
4
+ var PANDA_APP_BRIDGE_EVENT = "panda-app-bridge:event";
5
+
6
+ // src/create-app-sdk.ts
7
+ function createAppSdk(transport) {
8
+ return {
9
+ auth: {
10
+ getCurrentUser: () => transport.invoke("auth.getCurrentUser", [])
11
+ },
12
+ model: {
13
+ list: (options) => transport.invoke("model.list", [options ?? {}]),
14
+ resolveForScene: (request) => transport.invoke("model.resolveForScene", [request ?? {}])
15
+ },
16
+ workspace: {
17
+ list: (request) => transport.invoke("workspace.list", [request]),
18
+ ensureLocal: (request) => transport.invoke("workspace.ensureLocal", [request])
19
+ },
20
+ conversation: {
21
+ createForApp: (options) => transport.invoke("conversation.createForApp", [options ?? {}]),
22
+ disposeForApp: (conversationId) => transport.invoke("conversation.disposeForApp", [conversationId]),
23
+ submitMessage: (conversationId, prompt) => transport.invoke("conversation.submitMessage", [conversationId, prompt]),
24
+ getMessages: (conversationId) => transport.invoke("conversation.getMessages", [conversationId]),
25
+ subscribe: async (options) => {
26
+ const result = await transport.invoke("conversation.subscribe", [options]);
27
+ let offEvent;
28
+ if (options.onEvent) {
29
+ offEvent = transport.subscribe(result.subscriptionId, options.onEvent);
30
+ }
31
+ return {
32
+ subscriptionId: result.subscriptionId,
33
+ unsubscribe: async () => {
34
+ offEvent?.();
35
+ await transport.invoke("conversation.unsubscribe", [result.subscriptionId]);
36
+ }
37
+ };
38
+ },
39
+ awaitTurn: (options) => transport.invoke("conversation.awaitTurn", [options])
40
+ },
41
+ session: {
42
+ mount: (conversationId, appId, replaceConfirmed) => transport.invoke("mountSession", [conversationId, appId, replaceConfirmed === true]),
43
+ unmount: (conversationId) => transport.invoke("unmountSession", [conversationId]),
44
+ getMount: (conversationId) => transport.invoke("getSessionMount", [conversationId])
45
+ },
46
+ ingress: {
47
+ submit: (conversationId, prompt) => transport.invoke("ingress.submit", [conversationId, prompt])
48
+ },
49
+ tools: {
50
+ list: (request) => transport.invoke("tools.list", [request]),
51
+ execute: (request) => transport.invoke("tools.execute", [request])
52
+ },
53
+ runtime: {
54
+ now: () => transport.invoke("runtime.now", []),
55
+ getHost: () => transport.invoke("runtime.getHost", [])
56
+ },
57
+ dialog: {
58
+ showOpen: (request) => transport.invoke("dialog.showOpen", [request ?? {}]),
59
+ readPickedFile: (request) => transport.invoke("dialog.readPickedFile", [request]),
60
+ releasePick: (pickToken) => transport.invoke("dialog.releasePick", [pickToken]),
61
+ showSave: (request) => transport.invoke("dialog.showSave", [request ?? {}]),
62
+ writeSaveFile: (request) => transport.invoke("dialog.writeSaveFile", [request]),
63
+ pickAndReadFile: async (options) => {
64
+ const { encoding, ...openRequest } = options ?? {};
65
+ const opened = await transport.invoke("dialog.showOpen", [openRequest]);
66
+ if (opened.canceled || opened.filePaths.length === 0) {
67
+ return null;
68
+ }
69
+ const filePath = opened.filePaths[0];
70
+ try {
71
+ return await transport.invoke("dialog.readPickedFile", [
72
+ { pickToken: opened.pickToken, filePath, encoding }
73
+ ]);
74
+ } finally {
75
+ await transport.invoke("dialog.releasePick", [opened.pickToken]).catch(() => void 0);
76
+ }
77
+ },
78
+ saveFile: async (options) => {
79
+ const { content, encoding, ...saveRequest } = options;
80
+ const saved = await transport.invoke("dialog.showSave", [saveRequest]);
81
+ if (saved.canceled) {
82
+ return null;
83
+ }
84
+ await transport.invoke("dialog.writeSaveFile", [
85
+ {
86
+ saveToken: saved.saveToken,
87
+ filePath: saved.filePath,
88
+ content,
89
+ encoding
90
+ }
91
+ ]);
92
+ return saved.filePath;
93
+ }
94
+ }
95
+ };
96
+ }
97
+
98
+ // src/request-id.browser.ts
99
+ function newRequestId() {
100
+ return crypto.randomUUID();
101
+ }
102
+
103
+ // src/transport/shared/pending-invoke.ts
104
+ function createPendingInvoke(newRequestId2) {
105
+ const pending = /* @__PURE__ */ new Map();
106
+ function invoke(postMessage, invokeType, path, args) {
107
+ const requestId = newRequestId2();
108
+ return new Promise((resolve, reject) => {
109
+ pending.set(requestId, { resolve, reject });
110
+ postMessage({ type: invokeType, requestId, path, args });
111
+ });
112
+ }
113
+ function handleInvokeResult(message) {
114
+ if (!message.requestId) {
115
+ return;
116
+ }
117
+ const entry = pending.get(message.requestId);
118
+ if (!entry) {
119
+ return;
120
+ }
121
+ pending.delete(message.requestId);
122
+ if (message.ok) {
123
+ entry.resolve(message.result);
124
+ } else {
125
+ entry.reject(new Error(message.error ?? "invoke failed"));
126
+ }
127
+ }
128
+ return { invoke, handleInvokeResult };
129
+ }
130
+
131
+ // src/transport/shared/subscription-events.ts
132
+ function createSubscriptionEvents() {
133
+ const handlersBySubscriptionId = /* @__PURE__ */ new Map();
134
+ function subscribe(subscriptionId, handler) {
135
+ const handlers = handlersBySubscriptionId.get(subscriptionId) ?? /* @__PURE__ */ new Set();
136
+ handlers.add(handler);
137
+ handlersBySubscriptionId.set(subscriptionId, handlers);
138
+ return () => {
139
+ const current = handlersBySubscriptionId.get(subscriptionId);
140
+ if (!current) {
141
+ return;
142
+ }
143
+ current.delete(handler);
144
+ if (current.size === 0) {
145
+ handlersBySubscriptionId.delete(subscriptionId);
146
+ }
147
+ };
148
+ }
149
+ function dispatch(subscriptionId, event) {
150
+ const handlers = handlersBySubscriptionId.get(subscriptionId);
151
+ if (!handlers) {
152
+ return;
153
+ }
154
+ for (const handler of handlers) {
155
+ handler(event);
156
+ }
157
+ }
158
+ function remove(subscriptionId) {
159
+ handlersBySubscriptionId.delete(subscriptionId);
160
+ }
161
+ return { subscribe, dispatch, remove };
162
+ }
163
+
164
+ // src/transport/iframe.ts
165
+ function createIframeBridgeTransport() {
166
+ if (typeof window === "undefined") {
167
+ throw new Error("[app-sdk] iframe bridge requires browser context");
168
+ }
169
+ const { invoke: createInvoke, handleInvokeResult } = createPendingInvoke(newRequestId);
170
+ const subscriptions = createSubscriptionEvents();
171
+ window.addEventListener("message", (event) => {
172
+ const data = event.data;
173
+ if (!data || typeof data !== "object" || !data.type) {
174
+ return;
175
+ }
176
+ if (data.type === PANDA_APP_BRIDGE_EVENT && data.subscriptionId && data.event) {
177
+ subscriptions.dispatch(data.subscriptionId, data.event);
178
+ return;
179
+ }
180
+ if (data.type === PANDA_APP_BRIDGE_INVOKE_RESULT) {
181
+ handleInvokeResult(data);
182
+ }
183
+ });
184
+ return {
185
+ invoke(path, args) {
186
+ return createInvoke(
187
+ (message) => {
188
+ window.parent.postMessage(message, "*");
189
+ },
190
+ PANDA_APP_BRIDGE_INVOKE,
191
+ path,
192
+ args
193
+ );
194
+ },
195
+ subscribe: subscriptions.subscribe
196
+ };
197
+ }
198
+
199
+ // src/browser.ts
200
+ var PandaApp = {
201
+ connectIframe() {
202
+ return createAppSdk(createIframeBridgeTransport());
203
+ }
204
+ };
205
+ export {
206
+ PANDA_APP_BRIDGE_EVENT,
207
+ PANDA_APP_BRIDGE_INVOKE,
208
+ PANDA_APP_BRIDGE_INVOKE_RESULT,
209
+ PandaApp,
210
+ createAppSdk,
211
+ createIframeBridgeTransport
212
+ };
@@ -0,0 +1,115 @@
1
+ export { APP_UTILITY_EVENT, APP_UTILITY_INVOKE, APP_UTILITY_INVOKE_RESULT, APP_UTILITY_READY, } from './protocol.js';
2
+ export { createAppSdk } from './create-app-sdk.js';
3
+ export { createUtilityProcessTransport } from './transport/utility-process.js';
4
+ export type * from './types.js';
5
+ export declare const PandaApp: {
6
+ connectUtilityProcess(): {
7
+ auth: {
8
+ getCurrentUser: () => Promise<import("./types.js").AppUser | null>;
9
+ };
10
+ model: {
11
+ list: (options?: {
12
+ enabledOnly?: boolean;
13
+ visibleOnly?: boolean;
14
+ scene?: string;
15
+ }) => Promise<import("./types.js").ModelListItem[]>;
16
+ resolveForScene: (request?: {
17
+ scene?: string;
18
+ preferredModelId?: string;
19
+ }) => Promise<import("./types.js").ModelListItem | null>;
20
+ };
21
+ workspace: {
22
+ list: (request: {
23
+ limit: number;
24
+ includeEphemeral?: boolean;
25
+ }) => Promise<import("./types.js").WorkspaceListItem[]>;
26
+ ensureLocal: (request: {
27
+ path: string;
28
+ touch?: boolean;
29
+ }) => Promise<import("./types.js").WorkspaceListItem>;
30
+ };
31
+ conversation: {
32
+ createForApp: (options?: import("./types.js").CreateForAppOptions) => Promise<import("./types.js").CreateForAppResult>;
33
+ disposeForApp: (conversationId: string) => Promise<unknown>;
34
+ submitMessage: (conversationId: string, prompt: string) => Promise<unknown>;
35
+ getMessages: (conversationId: string) => Promise<unknown>;
36
+ subscribe: (options: import("./types.js").ConversationSubscribeOptions & {
37
+ onEvent?: import("./types.js").AppEventHandler;
38
+ }) => Promise<{
39
+ subscriptionId: string;
40
+ unsubscribe: () => Promise<void>;
41
+ }>;
42
+ awaitTurn: (options: {
43
+ conversationId: string;
44
+ turnId?: string;
45
+ timeoutMs?: number;
46
+ }) => Promise<unknown>;
47
+ };
48
+ session: {
49
+ mount: (conversationId: string, appId: string, replaceConfirmed?: boolean) => Promise<unknown>;
50
+ unmount: (conversationId: string) => Promise<unknown>;
51
+ getMount: (conversationId: string) => Promise<unknown>;
52
+ };
53
+ ingress: {
54
+ submit: (conversationId: string, prompt: string) => Promise<{
55
+ conversationId: string;
56
+ status: string;
57
+ }>;
58
+ };
59
+ tools: {
60
+ list: (request: {
61
+ workspace?: import("./types.js").WorkspaceRef;
62
+ workspaceId?: string;
63
+ }) => Promise<Array<{
64
+ name: string;
65
+ displayName: string;
66
+ description: string;
67
+ kind: string;
68
+ }>>;
69
+ execute: (request: {
70
+ toolName: string;
71
+ args?: Record<string, unknown>;
72
+ workspace?: import("./types.js").WorkspaceRef;
73
+ workspaceId?: string;
74
+ }) => Promise<{
75
+ toolName: string;
76
+ workspaceId: string;
77
+ ok: boolean;
78
+ llmContent?: string;
79
+ error?: {
80
+ message: string;
81
+ type?: string;
82
+ };
83
+ displayData?: unknown;
84
+ }>;
85
+ };
86
+ runtime: {
87
+ now: () => Promise<{
88
+ timestamp: number;
89
+ iso: string;
90
+ timeZone: string;
91
+ utcOffsetMinutes: number;
92
+ }>;
93
+ getHost: () => Promise<{
94
+ platform: string;
95
+ arch: string;
96
+ osName: string;
97
+ hostProduct: string;
98
+ shell: string;
99
+ shellKind: string;
100
+ homeDir: string;
101
+ pandaVersion: string;
102
+ appSdkVersion: string;
103
+ }>;
104
+ };
105
+ dialog: {
106
+ showOpen: (request?: import("./types.js").DialogShowOpenRequest) => Promise<import("./types.js").DialogShowOpenResult>;
107
+ readPickedFile: (request: import("./types.js").DialogReadPickedFileRequest) => Promise<import("./types.js").DialogReadPickedFileResult>;
108
+ releasePick: (pickToken: string) => Promise<unknown>;
109
+ showSave: (request?: import("./types.js").DialogShowSaveRequest) => Promise<import("./types.js").DialogShowSaveResult>;
110
+ writeSaveFile: (request: import("./types.js").DialogWriteSaveFileRequest) => Promise<import("./types.js").DialogWriteSaveFileResult>;
111
+ pickAndReadFile: (options?: import("./types.js").DialogPickAndReadFileOptions) => Promise<import("./types.js").DialogReadPickedFileResult | null>;
112
+ saveFile: (options: import("./types.js").DialogSaveFileOptions) => Promise<string | null>;
113
+ };
114
+ };
115
+ };
@@ -0,0 +1,216 @@
1
+ // src/protocol.ts
2
+ var APP_UTILITY_INVOKE = "app-utility:invoke";
3
+ var APP_UTILITY_INVOKE_RESULT = "app-utility:invoke-result";
4
+ var APP_UTILITY_READY = "app-utility:ready";
5
+ var APP_UTILITY_EVENT = "app-utility:event";
6
+
7
+ // src/create-app-sdk.ts
8
+ function createAppSdk(transport) {
9
+ return {
10
+ auth: {
11
+ getCurrentUser: () => transport.invoke("auth.getCurrentUser", [])
12
+ },
13
+ model: {
14
+ list: (options) => transport.invoke("model.list", [options ?? {}]),
15
+ resolveForScene: (request) => transport.invoke("model.resolveForScene", [request ?? {}])
16
+ },
17
+ workspace: {
18
+ list: (request) => transport.invoke("workspace.list", [request]),
19
+ ensureLocal: (request) => transport.invoke("workspace.ensureLocal", [request])
20
+ },
21
+ conversation: {
22
+ createForApp: (options) => transport.invoke("conversation.createForApp", [options ?? {}]),
23
+ disposeForApp: (conversationId) => transport.invoke("conversation.disposeForApp", [conversationId]),
24
+ submitMessage: (conversationId, prompt) => transport.invoke("conversation.submitMessage", [conversationId, prompt]),
25
+ getMessages: (conversationId) => transport.invoke("conversation.getMessages", [conversationId]),
26
+ subscribe: async (options) => {
27
+ const result = await transport.invoke("conversation.subscribe", [options]);
28
+ let offEvent;
29
+ if (options.onEvent) {
30
+ offEvent = transport.subscribe(result.subscriptionId, options.onEvent);
31
+ }
32
+ return {
33
+ subscriptionId: result.subscriptionId,
34
+ unsubscribe: async () => {
35
+ offEvent?.();
36
+ await transport.invoke("conversation.unsubscribe", [result.subscriptionId]);
37
+ }
38
+ };
39
+ },
40
+ awaitTurn: (options) => transport.invoke("conversation.awaitTurn", [options])
41
+ },
42
+ session: {
43
+ mount: (conversationId, appId, replaceConfirmed) => transport.invoke("mountSession", [conversationId, appId, replaceConfirmed === true]),
44
+ unmount: (conversationId) => transport.invoke("unmountSession", [conversationId]),
45
+ getMount: (conversationId) => transport.invoke("getSessionMount", [conversationId])
46
+ },
47
+ ingress: {
48
+ submit: (conversationId, prompt) => transport.invoke("ingress.submit", [conversationId, prompt])
49
+ },
50
+ tools: {
51
+ list: (request) => transport.invoke("tools.list", [request]),
52
+ execute: (request) => transport.invoke("tools.execute", [request])
53
+ },
54
+ runtime: {
55
+ now: () => transport.invoke("runtime.now", []),
56
+ getHost: () => transport.invoke("runtime.getHost", [])
57
+ },
58
+ dialog: {
59
+ showOpen: (request) => transport.invoke("dialog.showOpen", [request ?? {}]),
60
+ readPickedFile: (request) => transport.invoke("dialog.readPickedFile", [request]),
61
+ releasePick: (pickToken) => transport.invoke("dialog.releasePick", [pickToken]),
62
+ showSave: (request) => transport.invoke("dialog.showSave", [request ?? {}]),
63
+ writeSaveFile: (request) => transport.invoke("dialog.writeSaveFile", [request]),
64
+ pickAndReadFile: async (options) => {
65
+ const { encoding, ...openRequest } = options ?? {};
66
+ const opened = await transport.invoke("dialog.showOpen", [openRequest]);
67
+ if (opened.canceled || opened.filePaths.length === 0) {
68
+ return null;
69
+ }
70
+ const filePath = opened.filePaths[0];
71
+ try {
72
+ return await transport.invoke("dialog.readPickedFile", [
73
+ { pickToken: opened.pickToken, filePath, encoding }
74
+ ]);
75
+ } finally {
76
+ await transport.invoke("dialog.releasePick", [opened.pickToken]).catch(() => void 0);
77
+ }
78
+ },
79
+ saveFile: async (options) => {
80
+ const { content, encoding, ...saveRequest } = options;
81
+ const saved = await transport.invoke("dialog.showSave", [saveRequest]);
82
+ if (saved.canceled) {
83
+ return null;
84
+ }
85
+ await transport.invoke("dialog.writeSaveFile", [
86
+ {
87
+ saveToken: saved.saveToken,
88
+ filePath: saved.filePath,
89
+ content,
90
+ encoding
91
+ }
92
+ ]);
93
+ return saved.filePath;
94
+ }
95
+ }
96
+ };
97
+ }
98
+
99
+ // src/request-id.node.ts
100
+ import { randomUUID } from "node:crypto";
101
+ function newRequestId() {
102
+ return randomUUID();
103
+ }
104
+
105
+ // src/transport/shared/pending-invoke.ts
106
+ function createPendingInvoke(newRequestId2) {
107
+ const pending = /* @__PURE__ */ new Map();
108
+ function invoke(postMessage, invokeType, path, args) {
109
+ const requestId = newRequestId2();
110
+ return new Promise((resolve, reject) => {
111
+ pending.set(requestId, { resolve, reject });
112
+ postMessage({ type: invokeType, requestId, path, args });
113
+ });
114
+ }
115
+ function handleInvokeResult(message) {
116
+ if (!message.requestId) {
117
+ return;
118
+ }
119
+ const entry = pending.get(message.requestId);
120
+ if (!entry) {
121
+ return;
122
+ }
123
+ pending.delete(message.requestId);
124
+ if (message.ok) {
125
+ entry.resolve(message.result);
126
+ } else {
127
+ entry.reject(new Error(message.error ?? "invoke failed"));
128
+ }
129
+ }
130
+ return { invoke, handleInvokeResult };
131
+ }
132
+
133
+ // src/transport/shared/subscription-events.ts
134
+ function createSubscriptionEvents() {
135
+ const handlersBySubscriptionId = /* @__PURE__ */ new Map();
136
+ function subscribe(subscriptionId, handler) {
137
+ const handlers = handlersBySubscriptionId.get(subscriptionId) ?? /* @__PURE__ */ new Set();
138
+ handlers.add(handler);
139
+ handlersBySubscriptionId.set(subscriptionId, handlers);
140
+ return () => {
141
+ const current = handlersBySubscriptionId.get(subscriptionId);
142
+ if (!current) {
143
+ return;
144
+ }
145
+ current.delete(handler);
146
+ if (current.size === 0) {
147
+ handlersBySubscriptionId.delete(subscriptionId);
148
+ }
149
+ };
150
+ }
151
+ function dispatch(subscriptionId, event) {
152
+ const handlers = handlersBySubscriptionId.get(subscriptionId);
153
+ if (!handlers) {
154
+ return;
155
+ }
156
+ for (const handler of handlers) {
157
+ handler(event);
158
+ }
159
+ }
160
+ function remove(subscriptionId) {
161
+ handlersBySubscriptionId.delete(subscriptionId);
162
+ }
163
+ return { subscribe, dispatch, remove };
164
+ }
165
+
166
+ // src/transport/utility-process.ts
167
+ function createUtilityProcessTransport() {
168
+ const parentPort = process.parentPort;
169
+ if (!parentPort) {
170
+ throw new Error("[app-sdk] parentPort unavailable");
171
+ }
172
+ const { invoke: createInvoke, handleInvokeResult } = createPendingInvoke(newRequestId);
173
+ const subscriptions = createSubscriptionEvents();
174
+ parentPort.on("message", (event) => {
175
+ const msg = event.data;
176
+ if (!msg || typeof msg !== "object" || !msg.type) {
177
+ return;
178
+ }
179
+ if (msg.type === APP_UTILITY_EVENT && msg.subscriptionId && msg.event) {
180
+ subscriptions.dispatch(msg.subscriptionId, msg.event);
181
+ return;
182
+ }
183
+ if (msg.type === APP_UTILITY_INVOKE_RESULT) {
184
+ handleInvokeResult(msg);
185
+ }
186
+ });
187
+ return {
188
+ invoke(path, args) {
189
+ return createInvoke(
190
+ (message) => {
191
+ parentPort.postMessage(message);
192
+ },
193
+ APP_UTILITY_INVOKE,
194
+ path,
195
+ args
196
+ );
197
+ },
198
+ subscribe: subscriptions.subscribe
199
+ };
200
+ }
201
+
202
+ // src/node.ts
203
+ var PandaApp = {
204
+ connectUtilityProcess() {
205
+ return createAppSdk(createUtilityProcessTransport());
206
+ }
207
+ };
208
+ export {
209
+ APP_UTILITY_EVENT,
210
+ APP_UTILITY_INVOKE,
211
+ APP_UTILITY_INVOKE_RESULT,
212
+ APP_UTILITY_READY,
213
+ PandaApp,
214
+ createAppSdk,
215
+ createUtilityProcessTransport
216
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@panda-agent/panda-app-sdk",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Panda 智能应用 SDK — iframe / Utility Process 客户端",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "exports": {
14
+ "./browser": {
15
+ "types": "./dist/browser/index.d.ts",
16
+ "default": "./dist/browser/index.js"
17
+ },
18
+ "./node": {
19
+ "types": "./dist/node/index.d.ts",
20
+ "default": "./dist/node/index.js"
21
+ }
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "scripts": {
27
+ "build": "node scripts/build.mjs",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit",
29
+ "test": "npm run build && vitest run",
30
+ "verify-dist": "node scripts/verify-dist.mjs",
31
+ "vendor-fixtures": "node scripts/vendor-fixtures.mjs",
32
+ "prepack": "npm run build && npm run verify-dist",
33
+ "release:pack": "npm pack",
34
+ "release:publish": "node ./scripts/publish-app-sdk.mjs --tag latest",
35
+ "release:publish:dry-run": "node ./scripts/publish-app-sdk.mjs --tag latest --dry-run",
36
+ "release:publish:tester": "node ./scripts/publish-app-sdk.mjs --tag tester",
37
+ "release:publish:tester:dry-run": "node ./scripts/publish-app-sdk.mjs --tag tester --dry-run",
38
+ "prepublishOnly": "npm run typecheck && npm run test"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.0.0",
42
+ "esbuild": "^0.27.2",
43
+ "typescript": "~5.8.3",
44
+ "vitest": "^4.0.17"
45
+ }
46
+ }