@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.6

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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +38 -2
  4. package/cordis.patch.yml +6 -0
  5. package/dist/client.js +6 -0
  6. package/package.json +83 -4
  7. package/personal-assistant.plugin.json +31 -0
  8. package/personal-assistant.plugin.schema.json +43 -0
  9. package/runtime/assistant-runtime.mjs +317 -0
  10. package/runtime/check-manifest.mjs +21 -0
  11. package/runtime/cloud-browser-cdp.mjs +313 -0
  12. package/runtime/cloud-browser.mjs +972 -0
  13. package/runtime/cloud-execution.mjs +320 -0
  14. package/runtime/cloud-session-channel.mjs +45 -0
  15. package/runtime/computer-agentd.mjs +488 -0
  16. package/runtime/desktop-clipboard-limit.mjs +2 -0
  17. package/runtime/desktop-launcher.mjs +106 -0
  18. package/runtime/desktop-permit.mjs +42 -0
  19. package/runtime/desktop-session.mjs +840 -0
  20. package/runtime/desktop-ws.mjs +371 -0
  21. package/runtime/prepare-dsh-profile.mjs +17 -0
  22. package/runtime/welcome-server.mjs +144 -0
  23. package/scripts/assistant-runtime-launcher.sh +3 -0
  24. package/scripts/build-artifacts.mjs +81 -0
  25. package/scripts/build-client.mjs +82 -0
  26. package/scripts/computer-agentd-launcher.sh +3 -0
  27. package/scripts/deterministic_tar.py +21 -0
  28. package/scripts/install-local.mjs +262 -0
  29. package/scripts/scan-secrets.mjs +41 -0
  30. package/scripts/sync-public.mjs +52 -0
  31. package/scripts/verification/gateway-desktop-client.mjs +142 -0
  32. package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
  33. package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
  34. package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
  35. package/scripts/verify-cloud-turn.mjs +18 -0
  36. package/scripts/verify-computer-broker.mjs +10 -0
  37. package/src/automation-tools.mjs +18 -0
  38. package/src/bot-private-tools.mjs +23 -0
  39. package/src/bot-registry.mjs +157 -0
  40. package/src/bot-tool-policy.mjs +48 -0
  41. package/src/bot-tool-scope.mjs +40 -0
  42. package/src/client/bot-directory.mjs +23 -0
  43. package/src/client/components/desktop-clipboard.mjs +194 -0
  44. package/src/client/conversation.mjs +558 -0
  45. package/src/client/desktop-control.mjs +70 -0
  46. package/src/client/desktop-view.mjs +68 -0
  47. package/src/client/entry.mjs +8 -0
  48. package/src/client/index.mjs +693 -0
  49. package/src/client/markdown.mjs +130 -0
  50. package/src/client/native-workbench.mjs +62 -0
  51. package/src/cloud-computer-tools.mjs +245 -0
  52. package/src/cloud-files.mjs +57 -0
  53. package/src/cloud-model.mjs +10 -0
  54. package/src/cloud-session-transport.mjs +76 -0
  55. package/src/computer-broker-protocol.mjs +116 -0
  56. package/src/computer-broker.mjs +220 -0
  57. package/src/desktop-tools.mjs +64 -0
  58. package/src/dsh-session-bridge.mjs +207 -0
  59. package/src/dsh-web-bridge.mjs +127 -0
  60. package/src/host-gateway.mjs +228 -0
  61. package/src/host-session-channel.mjs +410 -0
  62. package/src/implementation-capabilities.mjs +10 -0
  63. package/src/index.mjs +148 -0
  64. package/src/persistent-state.mjs +201 -0
  65. package/src/platform-manifest.mjs +24 -0
  66. package/src/plugin-manifest.mjs +82 -0
  67. package/src/schedule-manager.mjs +88 -0
  68. package/src/system-fetch.mjs +72 -0
  69. package/src/workspace-tools.mjs +22 -0
  70. package/src/workspace.mjs +22 -0
@@ -0,0 +1,410 @@
1
+ /**
2
+ * 个人助理 Host 侧「会话通道」。
3
+ *
4
+ * 把宿主 DSH 的会话/对话能力收敛成面板可用的 5 个方法,由 `host-gateway.mjs`
5
+ * 暴露为 `personal-assistant.sessions.list` / `.session.create` / `.session.snapshot`
6
+ * / `.session.events` / `.turn.submit`。
7
+ *
8
+ * ## 事件来源:已核对的宿主真实 API(未臆造)
9
+ *
10
+ * 宿主 `ctx.sessionController` 由 `@deepseek-ai/dsh-api-session-controller` 提供,实测位于
11
+ * `/Applications/AgentKit Studio.app/Contents/Resources/app.asar` 内的
12
+ * `dsh/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js`,公开方法有:
13
+ *
14
+ * list(request, signal) → { items: SessionSummary[] }(冷热会话均可读,不激活 Agent)
15
+ * inspect(sessionId, signal) → { meta, inheritedEventCount, events }
16
+ * (已挂载 = live snapshot;冷会话 = 持久化事件前缀;不激活 Agent)
17
+ * create({ sessionId?, cwd?, agentPreset? })→ { sessionId, agentPreset? }
18
+ * prompt({ sessionId, requestId, mode, content, clientTimeZone? }, signal) → { accepted: true }
19
+ * rename({ sessionId, title }) → { title, seq }
20
+ * page / follow / projections / cancel / updateQueue / fork / search / modelCatalog …
21
+ *
22
+ * 会话标题**不在** list 行里:由 `sessionTitle` 服务注册的 `title` projection 提供
23
+ * (list 行为 `row.projections.values.title`),事件日志里则是 durable `session/title`
24
+ * 事件(`data.title`)。因此本通道分别从这两处折叠标题。
25
+ *
26
+ * ## 实现选择、限制与后续替换点
27
+ *
28
+ * 1. 事件读取用 `inspect()` 全量事件前缀 + durable 事件 `seq` 做**单调游标**切片,
29
+ * 而不是 `follow()`。原因:`follow()` 是 Remote 流载体专用的长连接 async generator
30
+ * (request 形如 `{ address: { kind: 'session', sessionId } }`),并且对冷会话会
31
+ * promotion(**激活 Agent**),不适合面板这种请求/响应式 RPC。
32
+ * → 替换点:改为
33
+ * `controller.follow({ address: { kind: 'session', sessionId }, maxMessages, assistantStream: true }, signal)`,
34
+ * 把 `snapshot` / `event` / `assistant-stream` 帧灌进每个会话的推流缓冲;
35
+ * 需要 RPC 层支持服务端推送(当前 connection.fetch 只有请求/响应)。
36
+ * 2. 因此当前没有 token 级流式增量:assistant 文本按 durable `assistant/message`
37
+ * (每个 step 一条)投递,也不投递工具过程 `step` 行。客户端 `mergeEvents` 支持
38
+ * `delta` / `step`,接入 follow 后再补齐。
39
+ * 3. 每次读取都会整份物化事件日志(inspect 语义),长会话成本随日志线性增长;
40
+ * 这里用「单次增量条数 + 字节上限」兜底,保证 RPC 响应有界。
41
+ * 替换点同上,或改用 `page({ address, throughSeq, beforeSeq })` 只读尾部页。
42
+ * 4. Bot 归属只在进程内记忆(createSession 时记录,后续回合缺省时复用);
43
+ * Host 重启后退回 `defaultBotId`。持久归属属于云端 runtime 的 sessionOwnerResolver
44
+ * (见 `dsh-session-bridge.mjs` 的 `setSessionOwnerResolver`)。
45
+ * 5. 命名空间:只创建/列出 `sessionIdPrefix`(默认 `pa-`)前缀的会话,避免把用户
46
+ * 在 Studio 中的其它会话暴露到个人助理面板(DSH `list` 返回全部可见会话)。
47
+ */
48
+ import { randomUUID } from 'node:crypto';
49
+ import { DshSessionControllerUnavailableError } from './dsh-session-bridge.mjs';
50
+
51
+ const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
52
+ import { tmpdir } from 'node:os';
53
+
54
+ const SESSION_SUFFIX = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
55
+ const BOT_ID = /^[a-z][a-z0-9._-]{0,63}$/u;
56
+ const TITLE_BYTES = 512;
57
+ const TURN_BYTES = 32 * 1024;
58
+ const ROLE_OF_TYPE = Object.freeze({ 'user/message': 'user', 'assistant/message': 'assistant' });
59
+
60
+ const TEMP_ROOT = tmpdir().replace(/\/+$/u, '');
61
+ /** 判断会话工作区是否落在临时目录下(含 macOS 的 /private 别名)。 */
62
+ function isTempCwd(cwd) {
63
+ const normalized = String(cwd || '').replace(/\/+$/u, '');
64
+ if (!normalized) return false;
65
+ for (const root of new Set([TEMP_ROOT, TEMP_ROOT.startsWith('/private/') ? TEMP_ROOT : `/private${TEMP_ROOT}`, '/tmp', '/private/tmp', '/var/tmp', '/private/var/tmp'])) {
66
+ if (normalized === root || normalized.startsWith(`${root}/`)) return true;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ const fail = (code, message, status = 400) => Object.assign(new Error(message), { code, status });
72
+ const messageNotFound = /^session ".*" not found$/u;
73
+
74
+ function textOfContent(content) {
75
+ if (typeof content === 'string') return content;
76
+ if (!Array.isArray(content)) return '';
77
+ return content
78
+ .filter(part => part && part.type === 'text' && typeof part.text === 'string')
79
+ .map(part => part.text)
80
+ .join('');
81
+ }
82
+
83
+ /**
84
+ * 宿主会把环境说明、skill 目录、team 消息等作为 `user/message` 注入(`data.source.kind` 非 `user`)。
85
+ * DSH 自己的会话投影只把 `source.kind === 'user'`(以及 compact 检查点)当成真人消息,
86
+ * 面板同样只显示这些,否则会出现一大段 "Current DSH file policy…" 之类的内部提示。
87
+ */
88
+ function isHumanUserMessage(event) {
89
+ const source = event?.data?.source;
90
+ if (!source || typeof source !== 'object') return true; // 旧格式无 source 时按真人消息处理
91
+ return source.kind === 'user' || source.kind === 'compact';
92
+ }
93
+
94
+ /** 把一条 durable 会话事件收敛成客户端可渲染的 message 事件;不支持的隐私内容一律丢弃。 */
95
+ function messageEventOf(event) {
96
+ const role = ROLE_OF_TYPE[event?.type];
97
+ if (!role || !Number.isSafeInteger(event.seq) || event.seq < 0) return undefined;
98
+ if (role === 'user' && !isHumanUserMessage(event)) return undefined;
99
+ // user/message 的 data 就是消息本体,assistant/message 的 data 形如 { message }。
100
+ const message = role === 'user' ? event.data : event?.data?.message;
101
+ const text = textOfContent(message?.content).trim();
102
+ if (!text) return undefined;
103
+ return Object.freeze({ type: 'message', id: `m-${event.seq}`, role, text, done: true, seq: event.seq });
104
+ }
105
+
106
+ /** durable `session/title` 折叠出的最新标题。 */
107
+ const IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
108
+ const IMAGE_BYTES = 5 * 1024 * 1024;
109
+ const MAX_IMAGES = 4;
110
+
111
+ /**
112
+ * 把面板上传的图片(data URL)转成宿主 prompt content 的 image 部件。
113
+ * 只接受宿主明确支持的四种位图;非图片附件本通道不投递(宿主暂不支持文件部件)。
114
+ */
115
+ function imageParts(attachments) {
116
+ if (!Array.isArray(attachments)) return [];
117
+ const parts = [];
118
+ for (const item of attachments.slice(0, MAX_IMAGES)) {
119
+ if (!item || typeof item !== 'object') continue;
120
+ const mediaType = typeof item.mimeType === 'string' ? item.mimeType : '';
121
+ if (!IMAGE_MEDIA_TYPES.has(mediaType)) continue;
122
+ const dataUrl = typeof item.dataUrl === 'string' ? item.dataUrl : '';
123
+ const comma = dataUrl.indexOf(',');
124
+ if (!dataUrl.startsWith('data:') || comma < 0) continue;
125
+ const data = dataUrl.slice(comma + 1);
126
+ if (Buffer.byteLength(data, 'base64') > IMAGE_BYTES) continue;
127
+ parts.push({ type: 'image', mediaType, data, ...(typeof item.name === 'string' && item.name ? { name: item.name.slice(0, 120) } : {}) });
128
+ }
129
+ return parts;
130
+ }
131
+
132
+ function titleOfEvents(events) {
133
+ for (let index = events.length - 1; index >= 0; index -= 1) {
134
+ const event = events[index];
135
+ if (event?.type === 'session/title' && typeof event.data?.title === 'string' && event.data.title) return event.data.title;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function titleOfRow(row) {
141
+ const value = row?.projections?.values?.title;
142
+ return typeof value === 'string' && value ? value : null;
143
+ }
144
+
145
+ function totalOf(events, atLeast = 0) {
146
+ for (let index = events.length - 1; index >= 0; index -= 1) {
147
+ const seq = events[index]?.seq;
148
+ if (Number.isSafeInteger(seq) && seq >= 0) return Math.max(atLeast, seq + 1);
149
+ }
150
+ return atLeast;
151
+ }
152
+
153
+ /**
154
+ * 门面对象:可注入 sessionController(测试用假控制器),缺省从 DSH ctx 解析。
155
+ * 所有方法在控制器缺失时抛 `DshSessionControllerUnavailableError`
156
+ * (`code: 'DSH_SESSION_CONTROLLER_UNAVAILABLE'`, `status: 503`)。
157
+ *
158
+ * @param {object} options
159
+ * @param {object} [options.sessionController] 显式注入的 DSH sessionController
160
+ * @param {object} [options.ctx] DSH 插件上下文(用于解析 'sessionController' 服务)
161
+ * @param {object} [options.turnBridge] `createDshTurnBridge()` 结果;有 Bot 时优先走它(装 Bot 提示词与工具)
162
+ * @param {(botId:string)=>object|undefined} [options.resolveBot] Bot 解析器(BotRegistry.get 之类)
163
+ * @param {string} [options.defaultBotId] 客户端未指定 botId 时的缺省助理
164
+ * @param {string} [options.sessionIdPrefix] 个人助理会话命名空间前缀
165
+ * @param {function():string} [options.requestIdFactory] 会话/回合标识生成器
166
+ */
167
+ export function createHostSessionChannel(options = {}) {
168
+ const {
169
+ ctx, sessionController, turnBridge, resolveBot, defaultBotId,
170
+ sessionIdPrefix = 'pa-', requestIdFactory = randomUUID,
171
+ maxSessions = 50, maxSnapshotMessages = 200,
172
+ maxEventsPerRead = 200, maxEventsBytes = 256 * 1024, maxSnapshotBytes = 512 * 1024,
173
+ } = options;
174
+ if (typeof sessionIdPrefix !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,31}$/u.test(sessionIdPrefix)) {
175
+ throw new TypeError('sessionIdPrefix must be a short identifier prefix');
176
+ }
177
+ if (defaultBotId !== undefined && defaultBotId !== null && defaultBotId !== '' && !BOT_ID.test(String(defaultBotId))) {
178
+ throw new TypeError('defaultBotId must be a Bot identifier');
179
+ }
180
+ if (typeof requestIdFactory !== 'function') throw new TypeError('requestIdFactory must be a function');
181
+ // 上限必须是正整数:0 会让增量游标停滞,客户端会原地轮询。
182
+ for (const [field, value] of Object.entries({ maxSessions, maxSnapshotMessages, maxEventsPerRead, maxEventsBytes, maxSnapshotBytes })) {
183
+ if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${field} must be a positive safe integer`);
184
+ }
185
+ const fixedDefaultBot = defaultBotId === undefined || defaultBotId === null || defaultBotId === '' ? undefined : String(defaultBotId);
186
+ /** 进程内的会话 → Bot 归属;Host 重启后回退 defaultBotId(见文件头限制 4)。 */
187
+ const sessionBots = new Map();
188
+
189
+ function controller() {
190
+ if (sessionController) return sessionController;
191
+ if (ctx && typeof ctx.get === 'function') {
192
+ try { return ctx.get('sessionController'); } catch { return undefined; }
193
+ }
194
+ return undefined;
195
+ }
196
+
197
+ function requireController(method) {
198
+ const value = controller();
199
+ if (!value || typeof value[method] !== 'function') throw new DshSessionControllerUnavailableError();
200
+ return value;
201
+ }
202
+
203
+ function sessionIdOf(value) {
204
+ const text = String(value ?? '').trim();
205
+ if (!SESSION_ID.test(text)) throw fail('DSH_SESSION_ID_INVALID', '会话标识无效');
206
+ return text;
207
+ }
208
+
209
+ function botIdOf(value) {
210
+ const text = String(value ?? '').trim();
211
+ if (!BOT_ID.test(text)) throw fail('DSH_SESSION_BOT_INVALID', '助理标识无效');
212
+ return text;
213
+ }
214
+
215
+ function mintSuffix() {
216
+ const suffix = String(requestIdFactory() ?? '');
217
+ if (!SESSION_SUFFIX.test(suffix)) throw fail('DSH_SESSION_ID_INVALID', '会话标识生成失败', 500);
218
+ return suffix;
219
+ }
220
+
221
+ function botDefinition(botId) {
222
+ if (!botId || typeof resolveBot !== 'function') return undefined;
223
+ let bot;
224
+ try { bot = resolveBot(botId); } catch { bot = undefined; }
225
+ if (!bot) throw fail('DSH_SESSION_BOT_INVALID', `助理「${botId}」不可用,请重新选择`);
226
+ return bot;
227
+ }
228
+
229
+ function chooseBot(value, sessionId) {
230
+ if (value !== undefined && value !== null && String(value).trim() !== '') return botIdOf(value);
231
+ if (sessionId && sessionBots.has(sessionId)) return sessionBots.get(sessionId);
232
+ return fixedDefaultBot;
233
+ }
234
+
235
+ function failure(error, action) {
236
+ if (error?.code === 'DSH_SESSION_CONTROLLER_UNAVAILABLE') return error;
237
+ const code = error?.code;
238
+ if (code === 'SESSION_QUERY_SESSION_NOT_FOUND' || code === 'session/not-found' || messageNotFound.test(String(error?.message ?? ''))) {
239
+ return fail('DSH_SESSION_NOT_FOUND', '会话不存在或已被删除', 404);
240
+ }
241
+ if (code === 'session/title-invalid') return fail('DSH_SESSION_TITLE_INVALID', '会话标题无效');
242
+ if (code === 'session/conflict') return fail('DSH_SESSION_CONFLICT', '会话与宿主已有会话冲突', 409);
243
+ if (code === 'session/agent-busy' || code === 'session/request-conflict') return fail('DSH_SESSION_BUSY', 'DSH 拒绝了这一轮对话,请稍后重试', 409);
244
+ if (code === 'session/migration-required') return fail('DSH_SESSION_MIGRATION_REQUIRED', '会话需要先完成格式迁移', 409);
245
+ if (code === 'gateway/bad-request') return fail('DSH_SESSION_REQUEST_INVALID', '会话请求参数无效');
246
+ return fail('DSH_SESSION_CONTROLLER_UNAVAILABLE', `${action}暂时不可用,请稍后重试`, 503);
247
+ }
248
+
249
+ function turnFailure(error) {
250
+ const code = error?.code;
251
+ if (code === 'DSH_SESSION_CONTROLLER_UNAVAILABLE') return error;
252
+ if (code === 'DSH_SESSION_BOT_CONFLICT') return fail('DSH_SESSION_BOT_CONFLICT', '该会话属于另一个助理', 409);
253
+ if (code === 'DSH_SESSION_BUSY') return fail('DSH_SESSION_BUSY', '会话正在处理上一轮对话,请稍后重试', 409);
254
+ if (code === 'DSH_SESSION_MIGRATION_REQUIRED') return fail('DSH_SESSION_MIGRATION_REQUIRED', '会话需要先完成格式迁移', 409);
255
+ if (code === 'DSH_SESSION_NOT_FOUND') return fail('DSH_SESSION_NOT_FOUND', '会话不存在或已被删除', 404);
256
+ if (code === 'DSH_TURN_INPUT_INVALID') return fail('DSH_TURN_INPUT_INVALID', '消息内容无效');
257
+ if (code === 'DSH_TURN_NOT_ACCEPTED') return fail('DSH_TURN_NOT_ACCEPTED', 'DSH 未接受这一轮对话', 503);
258
+ if (/^BOT_NOT_FOUND/u.test(String(error?.message ?? ''))) return fail('DSH_SESSION_BOT_INVALID', '助理不存在,请重新选择');
259
+ return failure(error, '发送消息');
260
+ }
261
+
262
+ /** 会话列表:只回个人助理命名空间内的会话,并对返回字段做白名单(不透传 Host 内部投影)。 */
263
+ async function listSessions(signal) {
264
+ const control = requireController('list');
265
+ let result;
266
+ try { result = await control.list({}, signal); } catch (error) { throw failure(error, '读取会话列表'); }
267
+ const rows = Array.isArray(result?.items) ? result.items : [];
268
+ const sessions = [];
269
+ for (const row of rows) {
270
+ const sessionId = typeof row?.sessionId === 'string' ? row.sessionId : '';
271
+ if (!sessionId.startsWith(sessionIdPrefix)) continue;
272
+ // 会话库是全局的:临时工作区(/tmp、/private/tmp 等)里的会话属于测试/试跑,
273
+ // 不能被真实面板复用,否则对话会被绑定到一个临时目录(写入受限、模型上下文错乱)。
274
+ if (typeof row?.cwd === 'string' && isTempCwd(row.cwd)) continue;
275
+ sessions.push({
276
+ sessionId,
277
+ title: titleOfRow(row),
278
+ updatedAt: typeof row?.updatedAt === 'number' && Number.isFinite(row.updatedAt) ? row.updatedAt : 0,
279
+ });
280
+ if (sessions.length >= maxSessions) break;
281
+ }
282
+ return { sessions };
283
+ }
284
+
285
+ /** 新建个人助理会话;指定 title 时用宿主 `rename` 落成 durable `session/title`。 */
286
+ async function createSession({ botId, title } = {}, signal) {
287
+ const control = requireController('create');
288
+ const requestedTitle = title === undefined || title === null ? undefined : String(title).trim();
289
+ if (requestedTitle !== undefined && Buffer.byteLength(requestedTitle) > TITLE_BYTES) throw fail('DSH_SESSION_TITLE_INVALID', '会话标题过长');
290
+ const bot = chooseBot(botId, undefined);
291
+ const definition = botDefinition(bot);
292
+ const sessionId = `${sessionIdPrefix}${mintSuffix()}`;
293
+ const request = { sessionId, ...(typeof definition?.agentPreset === 'string' && definition.agentPreset ? { agentPreset: definition.agentPreset } : {}) };
294
+ let created;
295
+ try { created = await control.create(request); } catch (error) { throw failure(error, '新建会话'); }
296
+ const createdId = typeof created?.sessionId === 'string' && SESSION_ID.test(created.sessionId) ? created.sessionId : sessionId;
297
+ if (bot) sessionBots.set(createdId, bot);
298
+ if (!requestedTitle) return { sessionId: createdId, title: null };
299
+ const renamer = requireController('rename');
300
+ try {
301
+ const accepted = await renamer.rename({ sessionId: createdId, title: requestedTitle });
302
+ return { sessionId: createdId, title: typeof accepted?.title === 'string' && accepted.title ? accepted.title : requestedTitle };
303
+ } catch (error) { throw failure(error, '设置会话标题'); }
304
+ }
305
+
306
+ /** 会话快照:标题 + 最近若干轮消息 + 游标(游标之后的增量由 readEvents 继续拉取)。 */
307
+ async function getSnapshot({ sessionId } = {}, signal) {
308
+ const id = sessionIdOf(sessionId);
309
+ const control = requireController('inspect');
310
+ let inspection;
311
+ try { inspection = await control.inspect(id, signal); } catch (error) { throw failure(error, '读取会话'); }
312
+ const events = Array.isArray(inspection?.events) ? inspection.events : [];
313
+ const messages = [];
314
+ let bytes = 0;
315
+ for (let index = events.length - 1; index >= 0 && messages.length < maxSnapshotMessages; index -= 1) {
316
+ const message = messageEventOf(events[index]);
317
+ if (!message) continue;
318
+ const size = Buffer.byteLength(message.text);
319
+ if (messages.length > 0 && bytes + size > maxSnapshotBytes) break;
320
+ messages.push({ id: message.id, role: message.role, text: message.text, seq: message.seq });
321
+ bytes += size;
322
+ }
323
+ messages.reverse();
324
+ return { sessionId: id, title: titleOfEvents(events), messages, cursor: totalOf(events) };
325
+ }
326
+
327
+ /**
328
+ * 游标增量。`cursor` 是 durable 事件位置(不是消息条数):支持渲染的事件会消费游标,
329
+ * 不支持的事件(工具结果、请求头等)同样**前进**游标但不投递,客户端据 `done` 判断本轮是否还有新事件。
330
+ */
331
+ async function readEvents({ sessionId, cursor = 0 } = {}, signal) {
332
+ const id = sessionIdOf(sessionId);
333
+ if (!Number.isSafeInteger(cursor) || cursor < 0) throw fail('DSH_CURSOR_INVALID', '游标无效');
334
+ const control = requireController('inspect');
335
+ let inspection;
336
+ try { inspection = await control.inspect(id, signal); } catch (error) { throw failure(error, '读取会话事件'); }
337
+ const events = Array.isArray(inspection?.events) ? inspection.events : [];
338
+ const emitted = [];
339
+ let next = cursor;
340
+ let bytes = 0;
341
+ for (const event of events) {
342
+ const seq = event?.seq;
343
+ if (!Number.isSafeInteger(seq) || seq < 0 || seq < next) continue;
344
+ const message = messageEventOf(event);
345
+ if (message) {
346
+ const size = Buffer.byteLength(message.text);
347
+ if (emitted.length >= maxEventsPerRead || (emitted.length > 0 && bytes + size > maxEventsBytes)) break;
348
+ emitted.push(message);
349
+ bytes += size;
350
+ }
351
+ next = seq + 1;
352
+ }
353
+ return { events: emitted, cursor: next, done: next >= totalOf(events, cursor) };
354
+ }
355
+
356
+ /**
357
+ * 提交一轮对话。注入了 turnBridge 时优先用它(Bot 提示词、工具与 Busy 互斥都由 bridge 负责);
358
+ * 否则回退到原始 `prompt`(无 Bot 语义,供独立 Host / 测试使用)。
359
+ */
360
+ async function submitTurn({ sessionId, botId, text, attachments } = {}, signal) {
361
+ const id = sessionIdOf(sessionId);
362
+ const content = String(text ?? '').trim();
363
+ const images = imageParts(attachments);
364
+ if (!content && images.length === 0) throw fail('DSH_TURN_INPUT_INVALID', '请先输入要发送的内容');
365
+ if (Buffer.byteLength(content) > TURN_BYTES) throw fail('DSH_TURN_INPUT_INVALID', '消息过长,请分段发送', 413);
366
+ const parts = [...(content ? [{ type: 'text', text: content }] : []), ...images];
367
+ const control = requireController('prompt');
368
+ const turnId = mintSuffix();
369
+ const bot = chooseBot(botId, id);
370
+ // 宿主没有配置任何 Bot 时,对话仍然可用:直接走 DSH 会话控制器,
371
+ // 不因为「没有助理」把面板堵死(云端目录里的助理 id 与宿主 Bot 不是同一套命名空间)。
372
+ if (turnBridge && bot) {
373
+ if (turnBridge.available === false) throw new DshSessionControllerUnavailableError();
374
+ botDefinition(bot);
375
+ let result;
376
+ try {
377
+ result = await turnBridge.submitTurn({ botId: bot, sessionId: id, input: content, parts, requestId: turnId, signal });
378
+ } catch (error) { throw turnFailure(error); }
379
+ // dsh-session-bridge 的 busy 标记只在 waitForTurn 的 finally 里释放。面板是「提交即返回 +
380
+ // 轮询事件」的拉模型,不由本通道阻塞等结果,因此这里在后台观察这一轮直到 turn/end,
381
+ // 否则同一会话的第二次提交会被永久判为 DSH_SESSION_BUSY。
382
+ if (typeof turnBridge.waitForTurn === 'function') {
383
+ try {
384
+ const observed = turnBridge.waitForTurn({ botId: bot, sessionId: id, requestId: turnId });
385
+ if (observed && typeof observed.catch === 'function') observed.catch(() => {});
386
+ } catch {}
387
+ }
388
+ return { turnId: typeof result?.requestId === 'string' && SESSION_ID.test(result.requestId) ? result.requestId : turnId, sessionId: id };
389
+ }
390
+ if (bot) sessionBots.set(id, bot);
391
+ let result;
392
+ try {
393
+ result = await control.prompt({ sessionId: id, requestId: turnId, mode: 'queue', content: parts }, signal ?? new AbortController().signal);
394
+ } catch (error) { throw failure(error, '发送消息'); }
395
+ if (result?.accepted !== true) throw fail('DSH_TURN_NOT_ACCEPTED', 'DSH 未接受这一轮对话', 503);
396
+ return { turnId, sessionId: id };
397
+ }
398
+
399
+ return Object.freeze({
400
+ get available() {
401
+ const value = controller();
402
+ return Boolean(value && typeof value.inspect === 'function' && typeof value.list === 'function');
403
+ },
404
+ listSessions,
405
+ createSession,
406
+ getSnapshot,
407
+ readEvents,
408
+ submitTurn,
409
+ });
410
+ }
@@ -0,0 +1,10 @@
1
+ /** Capabilities with a concrete implementation in this project and artifacts. */
2
+ export const IMPLEMENTED_RUNTIME_CAPABILITIES = Object.freeze([
3
+ 'personal-assistant/v1',
4
+ 'personal-assistant-bots/v1',
5
+ 'personal-assistant-workspace-managed/v1',
6
+ 'personal-assistant-browser/v1',
7
+ 'personal-assistant-code-execution/v1',
8
+ 'cloud-computer/v1',
9
+ 'personal-assistant-schedule-manager/v1',
10
+ ]);
package/src/index.mjs ADDED
@@ -0,0 +1,148 @@
1
+ import { BotRegistry } from './bot-registry.mjs';
2
+ import { createComputerBrokerClient, createHttpComputerBrokerTransport } from './computer-broker.mjs';
3
+ import { startDshWebBridge } from './dsh-web-bridge.mjs';
4
+ import { createComputerToolset, registerComputerTools } from './cloud-computer-tools.mjs';
5
+ import { createScheduleManager } from './schedule-manager.mjs';
6
+ import { registerAutomationTools } from './automation-tools.mjs';
7
+ import { workspaceDir } from './workspace.mjs';
8
+ import { createDshTurnBridge, sessionControllerFromContext } from './dsh-session-bridge.mjs';
9
+ import { readFileSync } from 'node:fs';
10
+ import { registerPersonalAssistantHostGateway } from './host-gateway.mjs';
11
+ import { createHostSessionChannel } from './host-session-channel.mjs';
12
+ import { loadPersonalAssistantManifest } from './platform-manifest.mjs';
13
+ import { restrictPersonalAssistantToolScope } from './bot-tool-scope.mjs';
14
+ export { IMPLEMENTED_RUNTIME_CAPABILITIES } from './implementation-capabilities.mjs';
15
+ export { DshSessionControllerUnavailableError, createDshTurnBridge, sessionControllerFromContext } from './dsh-session-bridge.mjs';
16
+ export { createHostSessionChannel } from './host-session-channel.mjs';
17
+
18
+ export const name = '@kingsoftcloud/agentkit-personal-assistant';
19
+ export const inject = Object.freeze([
20
+ 'llm',
21
+ 'tools',
22
+ 'systemPrompt',
23
+ 'sessionController',
24
+ 'sessions',
25
+ 'connection',
26
+ ]);
27
+
28
+ function runtimeEnvironment() {
29
+ return typeof process === 'object' && process?.env ? process.env : {};
30
+ }
31
+
32
+ function configuredBots(value, env) {
33
+ if (Array.isArray(value)) return value;
34
+ const path = String(env.BOT_REGISTRY_PATH || '').trim();
35
+ if (!path) return [];
36
+ try {
37
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
38
+ return Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.bots) ? parsed.bots : []);
39
+ } catch (error) {
40
+ throw Object.assign(new Error('Personal Assistant Bot registry could not be loaded'), { code: 'PERSONAL_ASSISTANT_BOT_REGISTRY_INVALID', cause: error });
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 面板对话不总是携带 botId,需要一个确定性的缺省 Bot:配置/环境显式指定优先,
46
+ * 其次取注册表里的第一个 Bot(单 Bot 部署即「就是它」;多 Bot 部署应由调用方显式选择)。
47
+ */
48
+ function defaultPersonalAssistantBot(registry, config, env) {
49
+ for (const candidate of [config.botId, env.BOT_ID]) {
50
+ const botId = String(candidate ?? '').trim();
51
+ if (botId && registry.has(botId)) return botId;
52
+ }
53
+ return registry.list()[0]?.botId;
54
+ }
55
+
56
+ /**
57
+ * Construct the host runtime without reaching a real Computer by default.
58
+ * A deployment must explicitly provide a documented broker transport.
59
+ */
60
+ export function createRuntime(ctx, config = {}) {
61
+ const env = runtimeEnvironment();
62
+ const identity = {
63
+ userId: config.userId ?? env.USER_ID,
64
+ assistantId: config.assistantId ?? env.ASSISTANT_ID ?? env.AGENTENGINE_AGENT_ID,
65
+ computerId: config.computerId ?? env.COMPUTER_ID,
66
+ };
67
+ const brokerEndpoint = config.computerBrokerUrl ?? env.COMPUTER_BROKER_URL ?? env.AGENTKIT_COMPUTER_URL;
68
+ const computerTransport = config.computerTransport ?? (typeof brokerEndpoint === 'string' && brokerEndpoint.trim()
69
+ ? createHttpComputerBrokerTransport({ baseUrl: brokerEndpoint }) : undefined);
70
+ const registry = new BotRegistry({ profileId: config.profileId ?? identity.assistantId ?? 'default', bots: configuredBots(config.bots, env) });
71
+ const platformPrompt = env.AGENTENGINE_MANIFEST_PATH ? loadPersonalAssistantManifest(env).prompt : (config.platformSystemPrompt ?? '');
72
+ const computerBroker = config.computerBroker ?? createComputerBrokerClient({ transport: computerTransport });
73
+ const scheduleManager = config.scheduleManager ?? createScheduleManager({ now: config.now });
74
+ let automationService;
75
+ const workspace = workspaceDir(config.workspace);
76
+ const turnBridge = config.turnBridge ?? createDshTurnBridge({
77
+ ctx,
78
+ sessionController: config.sessionController ?? sessionControllerFromContext(ctx),
79
+ registry,
80
+ platformPrompt,
81
+ setupAgent: async (agent, botId) => {
82
+ // 只限制这个 PA Agent;Host 文件、shell、subagent 等全局工具不能绕过电脑租约或读私有状态。
83
+ restrictPersonalAssistantToolScope(agent, botId, { getPolicy: () => registry.require(botId).toolPolicy });
84
+ if (!identity.assistantId || !identity.computerId || !computerTransport) return;
85
+ registerAutomationTools({ agent, botId, service: () => automationService });
86
+ const toolset = createComputerToolset({ broker: computerBroker, agent, userId: identity.userId, assistantId: identity.assistantId, computerId: identity.computerId, botId, taskId: `dsh:${agent.id}` });
87
+ const registration = registerComputerTools({ agent, toolset });
88
+ agent.ctx.effect?.(() => () => { for (const dispose of registration.disposers) dispose(); void toolset.dispose(); }, 'personal-assistant.computer-tools');
89
+ // 回合结束即交还;不同会话/定时任务不应等待上一回合的 TTL 才能使用同一电脑。
90
+ return () => toolset.release();
91
+ },
92
+ });
93
+ const createBotComputerToolset = ({ botId, ...options } = {}) => createComputerToolset({
94
+ broker: computerBroker, userId: identity.userId, assistantId: identity.assistantId, computerId: identity.computerId,
95
+ botId: botId ?? config.botId, ...options,
96
+ });
97
+ return Object.freeze({
98
+ ctx,
99
+ config: Object.freeze({ ...config }),
100
+ enabled: config.enabled === undefined ? env.AGENTKIT_PERSONAL_ASSISTANT_ENABLED === '1' : config.enabled === true,
101
+ registry,
102
+ computerBroker,
103
+ scheduleManager,
104
+ setAutomationService(value) { automationService = value; },
105
+ workspace,
106
+ turnBridge,
107
+ submitTurn: turnBridge.submitTurn,
108
+ createComputerToolset: createBotComputerToolset,
109
+ });
110
+ }
111
+
112
+ export function apply(ctx, config = {}) {
113
+ const runtime = createRuntime(ctx, config);
114
+ const env = runtimeEnvironment();
115
+ if (typeof ctx.effect === 'function' && typeof ctx.connection?.fetch?.register === 'function') {
116
+ // 本地 Host 只鉴权和转发,云端会话不可退回本机 Session Controller。
117
+ ctx.effect(() => registerPersonalAssistantHostGateway(ctx, config), 'personal-assistant:gateway');
118
+ }
119
+ if (env.AGENTKIT_PERSONAL_ASSISTANT_SERVE === '1') {
120
+ if (!runtime.turnBridge.available) throw Object.assign(new Error('Cloud Personal Assistant requires the DSH Session Controller'), { code: 'DSH_SESSION_CONTROLLER_UNAVAILABLE' });
121
+ const serve = async () => {
122
+ const { startAssistantRuntime } = await import('../runtime/assistant-runtime.mjs');
123
+ const handle = await startAssistantRuntime({ runtime, env });
124
+ console.info(JSON.stringify({ event: 'personal-assistant.host.ready', assistantId: env.ASSISTANT_ID, port: Number(env.AGENTKIT_RUNTIME_PORT), dshSessionController: true }));
125
+ // 把本机云端 DSH 网页地址交给云电脑(欢迎页「云助理」磁贴用它打开网页化宿主)。
126
+ // token 只在 Pod 内传递,不落盘、不写日志;失败不影响宿主就绪。
127
+ const dshWeb = startDshWebBridge({ env, brokerUrl: env.COMPUTER_BROKER_URL || env.AGENTKIT_COMPUTER_URL });
128
+ return () => { dshWeb.stop(); return handle.close(); };
129
+ };
130
+ if (typeof ctx.effect !== 'function') throw new Error('DSH effect lifecycle is required');
131
+ ctx.effect(serve, 'personal-assistant.http');
132
+ }
133
+ return runtime;
134
+ }
135
+
136
+ export { BotRegistry, createBotRegistry, normalizeBotDefinition } from './bot-registry.mjs';
137
+ export {
138
+ ComputerBrokerUnavailableError,
139
+ ComputerLeaseConflictError,
140
+ ComputerLeaseManager,
141
+ createComputerBrokerClient,
142
+ createHttpComputerBrokerTransport,
143
+ createMockComputerBroker,
144
+ } from './computer-broker.mjs';
145
+ export * from './computer-broker-protocol.mjs';
146
+ export * from './cloud-computer-tools.mjs';
147
+ export * from './schedule-manager.mjs';
148
+ export * from './workspace.mjs';