@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.7
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/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +38 -2
- package/cordis.patch.yml +6 -0
- package/dist/client.js +6 -0
- package/package.json +83 -4
- package/personal-assistant.plugin.json +31 -0
- package/personal-assistant.plugin.schema.json +43 -0
- package/runtime/assistant-runtime.mjs +317 -0
- package/runtime/check-manifest.mjs +21 -0
- package/runtime/cloud-browser-cdp.mjs +313 -0
- package/runtime/cloud-browser.mjs +972 -0
- package/runtime/cloud-execution.mjs +320 -0
- package/runtime/cloud-session-channel.mjs +45 -0
- package/runtime/computer-agentd.mjs +488 -0
- package/runtime/desktop-clipboard-limit.mjs +2 -0
- package/runtime/desktop-launcher.mjs +106 -0
- package/runtime/desktop-permit.mjs +42 -0
- package/runtime/desktop-session.mjs +840 -0
- package/runtime/desktop-ws.mjs +371 -0
- package/runtime/prepare-dsh-profile.mjs +17 -0
- package/runtime/welcome-server.mjs +144 -0
- package/scripts/assistant-runtime-launcher.sh +3 -0
- package/scripts/build-artifacts.mjs +81 -0
- package/scripts/build-client.mjs +82 -0
- package/scripts/computer-agentd-launcher.sh +3 -0
- package/scripts/deterministic_tar.py +21 -0
- package/scripts/install-local.mjs +262 -0
- package/scripts/scan-secrets.mjs +41 -0
- package/scripts/sync-public.mjs +52 -0
- package/scripts/verification/gateway-desktop-client.mjs +142 -0
- package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
- package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
- package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
- package/scripts/verify-cloud-turn.mjs +18 -0
- package/scripts/verify-computer-broker.mjs +10 -0
- package/src/automation-tools.mjs +18 -0
- package/src/bot-private-tools.mjs +23 -0
- package/src/bot-registry.mjs +157 -0
- package/src/bot-tool-policy.mjs +48 -0
- package/src/bot-tool-scope.mjs +40 -0
- package/src/client/bot-directory.mjs +23 -0
- package/src/client/components/desktop-clipboard.mjs +194 -0
- package/src/client/conversation.mjs +558 -0
- package/src/client/desktop-control.mjs +70 -0
- package/src/client/desktop-view.mjs +68 -0
- package/src/client/entry.mjs +8 -0
- package/src/client/index.mjs +733 -0
- package/src/client/markdown.mjs +130 -0
- package/src/client/native-workbench.mjs +62 -0
- package/src/cloud-computer-tools.mjs +245 -0
- package/src/cloud-files.mjs +57 -0
- package/src/cloud-model.mjs +10 -0
- package/src/cloud-session-transport.mjs +76 -0
- package/src/computer-broker-protocol.mjs +116 -0
- package/src/computer-broker.mjs +220 -0
- package/src/desktop-tools.mjs +64 -0
- package/src/dsh-session-bridge.mjs +207 -0
- package/src/dsh-web-bridge.mjs +127 -0
- package/src/host-gateway.mjs +281 -0
- package/src/host-session-channel.mjs +410 -0
- package/src/implementation-capabilities.mjs +10 -0
- package/src/index.mjs +148 -0
- package/src/persistent-state.mjs +201 -0
- package/src/platform-manifest.mjs +24 -0
- package/src/plugin-manifest.mjs +82 -0
- package/src/schedule-manager.mjs +88 -0
- package/src/system-fetch.mjs +72 -0
- package/src/workspace-tools.mjs +22 -0
- package/src/workspace.mjs +22 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 极简 Markdown 渲染:只覆盖模型回答里真正常见的语法,不引入第三方依赖。
|
|
3
|
+
*
|
|
4
|
+
* 支持:围栏代码块、行内代码、粗体、斜体、链接、无序/有序列表、引用、标题、
|
|
5
|
+
* 分隔线、段落内换行。已知限制:不解析表格、嵌套列表、HTML(一律按文本转义显示)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const FENCE = /^```\s*([\w+#.-]*)\s*$/u;
|
|
9
|
+
const HEADING = /^(#{1,6})\s+(.*)$/u;
|
|
10
|
+
const UL = /^\s*[-*+]\s+(.*)$/u;
|
|
11
|
+
const OL = /^\s*(\d{1,3})[.)]\s+(.*)$/u;
|
|
12
|
+
const QUOTE = /^\s*>\s?(.*)$/u;
|
|
13
|
+
const RULE = /^\s*([-*_])\s*(?:\1\s*){2,}$/u;
|
|
14
|
+
|
|
15
|
+
/** 把 Markdown 源码切成块;纯函数,方便单测。 */
|
|
16
|
+
export function parseMarkdown(source) {
|
|
17
|
+
const text = typeof source === 'string' ? source.replace(/\r\n?/gu, '\n') : '';
|
|
18
|
+
const lines = text.split('\n');
|
|
19
|
+
const blocks = [];
|
|
20
|
+
let index = 0;
|
|
21
|
+
const flushParagraph = buffer => {
|
|
22
|
+
if (buffer.length === 0) return;
|
|
23
|
+
blocks.push({ type: 'paragraph', text: buffer.join('\n') });
|
|
24
|
+
buffer.length = 0;
|
|
25
|
+
};
|
|
26
|
+
const paragraph = [];
|
|
27
|
+
while (index < lines.length) {
|
|
28
|
+
const line = lines[index];
|
|
29
|
+
const fence = FENCE.exec(line);
|
|
30
|
+
if (fence) {
|
|
31
|
+
flushParagraph(paragraph);
|
|
32
|
+
const body = [];
|
|
33
|
+
index += 1;
|
|
34
|
+
while (index < lines.length && !/^```\s*$/u.test(lines[index])) { body.push(lines[index]); index += 1; }
|
|
35
|
+
index += 1; // 跳过收尾围栏(缺失时按到文末处理)
|
|
36
|
+
blocks.push({ type: 'code', lang: fence[1] || '', text: body.join('\n') });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (line.trim() === '') { flushParagraph(paragraph); index += 1; continue; }
|
|
40
|
+
const heading = HEADING.exec(line);
|
|
41
|
+
if (heading) {
|
|
42
|
+
flushParagraph(paragraph);
|
|
43
|
+
blocks.push({ type: 'heading', level: heading[1].length, text: heading[2].trim() });
|
|
44
|
+
index += 1; continue;
|
|
45
|
+
}
|
|
46
|
+
if (RULE.test(line)) { flushParagraph(paragraph); blocks.push({ type: 'rule' }); index += 1; continue; }
|
|
47
|
+
const quote = QUOTE.exec(line);
|
|
48
|
+
if (quote) {
|
|
49
|
+
flushParagraph(paragraph);
|
|
50
|
+
const body = [quote[1]];
|
|
51
|
+
index += 1;
|
|
52
|
+
while (index < lines.length && QUOTE.test(lines[index])) { body.push(QUOTE.exec(lines[index])[1]); index += 1; }
|
|
53
|
+
blocks.push({ type: 'quote', text: body.join('\n') });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const bullet = UL.exec(line), ordered = OL.exec(line);
|
|
57
|
+
if (bullet || ordered) {
|
|
58
|
+
flushParagraph(paragraph);
|
|
59
|
+
const list = { type: ordered ? 'ordered' : 'unordered', items: [], start: ordered ? Number(ordered[1]) : 1 };
|
|
60
|
+
while (index < lines.length) {
|
|
61
|
+
const current = UL.exec(lines[index]), numbered = OL.exec(lines[index]);
|
|
62
|
+
if (!current && !numbered) break;
|
|
63
|
+
list.items.push((current ? current[1] : numbered[2]).trim());
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
blocks.push(list);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
paragraph.push(line);
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
flushParagraph(paragraph);
|
|
73
|
+
return blocks;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 行内语法:先按转义字符切分,再逐段套用 `code` / **bold** / *italic* / [text](url)。 */
|
|
77
|
+
export function parseInline(source) {
|
|
78
|
+
const text = typeof source === 'string' ? source : '';
|
|
79
|
+
const nodes = [];
|
|
80
|
+
const pushText = value => { if (value) nodes.push({ type: 'text', text: value }); };
|
|
81
|
+
let rest = text;
|
|
82
|
+
while (rest.length > 0) {
|
|
83
|
+
const code = /^`([^`]+)`/u.exec(rest);
|
|
84
|
+
if (code) { nodes.push({ type: 'code', text: code[1] }); rest = rest.slice(code[0].length); continue; }
|
|
85
|
+
const bold = /^\*\*([^*\n]+)\*\*/u.exec(rest) || /^__([^_\n]+)__/u.exec(rest);
|
|
86
|
+
if (bold) { nodes.push({ type: 'strong', text: bold[1] }); rest = rest.slice(bold[0].length); continue; }
|
|
87
|
+
const italic = /^\*([^*\s][^*\n]*)\*/u.exec(rest) || /^_([^_\s][^_\n]*)_/u.exec(rest);
|
|
88
|
+
if (italic) { nodes.push({ type: 'em', text: italic[1] }); rest = rest.slice(italic[0].length); continue; }
|
|
89
|
+
const link = /^\[([^\]]+)\]\(([^)\s]+)\)/u.exec(rest);
|
|
90
|
+
if (link) { nodes.push({ type: 'link', text: link[1], href: link[2] }); rest = rest.slice(link[0].length); continue; }
|
|
91
|
+
const next = rest.slice(1).search(/[`*_[\]\n]/u);
|
|
92
|
+
const take = next < 0 ? rest.length : next + 1;
|
|
93
|
+
pushText(rest.slice(0, take));
|
|
94
|
+
rest = rest.slice(take);
|
|
95
|
+
}
|
|
96
|
+
// 相邻纯文本合并,避免不成对的 * / _ 被切成多个片段(渲染结果等价,便于断言与调试)。
|
|
97
|
+
return nodes.reduce((merged, node) => {
|
|
98
|
+
const last = merged[merged.length - 1];
|
|
99
|
+
if (node.type === 'text' && last && last.type === 'text') merged[merged.length - 1] = { type: 'text', text: last.text + node.text };
|
|
100
|
+
else merged.push(node);
|
|
101
|
+
return merged;
|
|
102
|
+
}, []);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 渲染为 React 元素树;`h` 是 createElement,`styles` 由视图传入。 */
|
|
106
|
+
export function renderMarkdown(h, source, styles = {}) {
|
|
107
|
+
const blocks = parseMarkdown(source);
|
|
108
|
+
if (blocks.length === 0) return null;
|
|
109
|
+
const inline = (text, keyPrefix) => parseInline(text).map((node, order) => {
|
|
110
|
+
const key = `${keyPrefix}-i${order}`;
|
|
111
|
+
if (node.type === 'code') return h('code', { key, style: styles.inlineCode }, node.text);
|
|
112
|
+
if (node.type === 'strong') return h('strong', { key }, node.text);
|
|
113
|
+
if (node.type === 'em') return h('em', { key }, node.text);
|
|
114
|
+
if (node.type === 'link') return /^(https?:|mailto:|cloud-file:)/iu.test(node.href) ? h('a', { key, href: node.href, target: '_blank', rel: 'noreferrer noopener', style: styles.link }, node.text) : h('span', { key }, node.text);
|
|
115
|
+
return h('span', { key }, node.text);
|
|
116
|
+
});
|
|
117
|
+
return blocks.map((block, order) => {
|
|
118
|
+
const key = `b${order}`;
|
|
119
|
+
if (block.type === 'code') return h('pre', { key, style: styles.codeBlock }, h('code', null, block.text));
|
|
120
|
+
if (block.type === 'heading') return h('div', { key, style: { ...styles.heading, ...(block.level <= 2 ? styles.headingStrong : null) } }, inline(block.text, key));
|
|
121
|
+
if (block.type === 'rule') return h('hr', { key, style: styles.rule });
|
|
122
|
+
if (block.type === 'quote') return h('blockquote', { key, style: styles.quote }, inline(block.text, key));
|
|
123
|
+
if (block.type === 'unordered' || block.type === 'ordered') {
|
|
124
|
+
const tag = block.type === 'ordered' ? 'ol' : 'ul';
|
|
125
|
+
return h(tag, { key, style: styles.list, start: block.type === 'ordered' && block.start !== 1 ? block.start : undefined },
|
|
126
|
+
block.items.map((item, position) => h('li', { key: `${key}-l${position}` }, inline(item, `${key}-l${position}`))));
|
|
127
|
+
}
|
|
128
|
+
return h('p', { key, style: styles.paragraph }, inline(block.text, key));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** 云电脑只注册原生 DSH 右侧栏内容,不拥有布局、全屏或本地执行会话。 */
|
|
2
|
+
export const ASSISTANT_COMPUTER_KIND = 'personal-assistant-computer';
|
|
3
|
+
export const ASSISTANT_PANEL_ID = 'personal-assistant';
|
|
4
|
+
const unavailableReason = '当前 Studio 的原生侧栏组件需要更新,请安装本次随插件提供的宿主组件包。';
|
|
5
|
+
const unavailable = () => Object.assign(new Error(unavailableReason), { code: 'PERSONAL_ASSISTANT_NATIVE_SIDEBAR_REQUIRED' });
|
|
6
|
+
|
|
7
|
+
function ComputerTab(props) { return props.workbench.render(props); }
|
|
8
|
+
|
|
9
|
+
export function createAssistantWorkbench({ sidebarRight, slots, panelId = ASSISTANT_PANEL_ID } = {}) {
|
|
10
|
+
const available = typeof sidebarRight?.registerPanelSurface === 'function'
|
|
11
|
+
&& typeof sidebarRight?.openPanelTab === 'function' && typeof sidebarRight?.setPanelExpanded === 'function'
|
|
12
|
+
&& typeof slots?.inject === 'function' && typeof slots?.register === 'function';
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let binding, disposed = false;
|
|
15
|
+
let snapshot = Object.freeze({ binding: undefined });
|
|
16
|
+
const emit = () => { snapshot = Object.freeze({ binding }); for (const listener of [...listeners]) listener(); };
|
|
17
|
+
const requireAvailable = () => {
|
|
18
|
+
if (!available) throw unavailable();
|
|
19
|
+
if (disposed) throw new Error('云电脑工作面板已关闭');
|
|
20
|
+
};
|
|
21
|
+
let releaseSurface, releaseTab;
|
|
22
|
+
const api = {
|
|
23
|
+
available, unavailableReason: available ? null : unavailableReason,
|
|
24
|
+
subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener); },
|
|
25
|
+
getSnapshot: () => snapshot,
|
|
26
|
+
bind(scope, request) {
|
|
27
|
+
requireAvailable();
|
|
28
|
+
if (!scope?.agentId || !scope?.botId || typeof request !== 'function') throw new Error('云端助理尚未连接');
|
|
29
|
+
const changed = binding && (binding.scope.agentId !== scope.agentId || binding.scope.botId !== scope.botId);
|
|
30
|
+
if (changed) { releaseSurface(); releaseSurface = sidebarRight.registerPanelSurface(panelId); }
|
|
31
|
+
binding = Object.freeze({ scope: Object.freeze({ ...scope }), request }); emit();
|
|
32
|
+
api.open();
|
|
33
|
+
},
|
|
34
|
+
open() {
|
|
35
|
+
requireAvailable();
|
|
36
|
+
if (binding) sidebarRight.openPanelTab(panelId, ASSISTANT_COMPUTER_KIND);
|
|
37
|
+
},
|
|
38
|
+
collapse() { requireAvailable(); sidebarRight.setPanelExpanded(panelId, false); },
|
|
39
|
+
dispose() {
|
|
40
|
+
if (disposed) return;
|
|
41
|
+
disposed = true; releaseTab?.(); releaseSurface?.(); listeners.clear();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
if (available) {
|
|
45
|
+
releaseSurface = sidebarRight.registerPanelSurface(panelId);
|
|
46
|
+
releaseTab = slots.inject('sidebar.right.panel.tab', () => slots.register({
|
|
47
|
+
name: 'sidebar.right.panel.tab', key: ASSISTANT_COMPUTER_KIND, inject: () => ({ workbench: api }),
|
|
48
|
+
}, ComputerTab));
|
|
49
|
+
}
|
|
50
|
+
return api;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 仅渲染已授权原生 tab 的内容;窗口与全屏均归 DSH 所有。 */
|
|
54
|
+
export function AssistantNativeRightbar({ React: R, workbench, Panel, active = true, fullscreen = false }) {
|
|
55
|
+
const { createElement: h, useSyncExternalStore } = R;
|
|
56
|
+
const value = useSyncExternalStore(workbench.subscribe, workbench.getSnapshot, workbench.getSnapshot);
|
|
57
|
+
if (!active || !value.binding) return null;
|
|
58
|
+
const { scope, request } = value.binding;
|
|
59
|
+
return h(Panel, { key: `${scope.agentId}:${scope.botId}:${scope.sessionId || ''}`,
|
|
60
|
+
request, initialBotId: scope.botId, initialSessionId: scope.sessionId, agentId: scope.agentId,
|
|
61
|
+
variant: 'native-tab', active, nativeFullscreen: fullscreen });
|
|
62
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH tool adapters for the dedicated Cloud Computer.
|
|
3
|
+
*
|
|
4
|
+
* The Personal Assistant runtime never opens Chromium or executes a host
|
|
5
|
+
* command itself. Every browser/terminal operation crosses the versioned
|
|
6
|
+
* Computer Broker protocol and is bound to one Bot task lease. The computer
|
|
7
|
+
* image owns `computer-agentd`, Chromium/CDP and the terminal sandbox.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
COMPUTER_ACTION_NAMES,
|
|
11
|
+
createActionRequest,
|
|
12
|
+
createLeaseRequest,
|
|
13
|
+
createReleaseRequest,
|
|
14
|
+
} from './computer-broker-protocol.mjs';
|
|
15
|
+
import { createDesktopTool } from './desktop-tools.mjs';
|
|
16
|
+
import { createWorkspaceTool } from './workspace-tools.mjs';
|
|
17
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
18
|
+
|
|
19
|
+
export const PERSONAL_ASSISTANT_BROWSER_CAPABILITY = 'personal-assistant-browser/v1';
|
|
20
|
+
export const PERSONAL_ASSISTANT_CODE_EXECUTION_CAPABILITY = 'personal-assistant-code-execution/v1';
|
|
21
|
+
export const CLOUD_COMPUTER_CAPABILITY = 'cloud-computer/v1';
|
|
22
|
+
export const COMPUTER_AGENTD_CONTRACT = 'computer-agentd';
|
|
23
|
+
export const COMPUTER_BROKER_URL_ENV = 'COMPUTER_BROKER_URL';
|
|
24
|
+
// computer-agentd owns the real Chromium/CDP process and the terminal
|
|
25
|
+
// executor. These explicit contracts keep capability discovery tied to the
|
|
26
|
+
// implementation instead of to a capability-name string alone.
|
|
27
|
+
export const CLOUD_BROWSER_BACKEND = Object.freeze({ engine: 'chromium', protocol: 'cdp', owner: COMPUTER_AGENTD_CONTRACT });
|
|
28
|
+
export const CLOUD_EXECUTION_BACKEND = Object.freeze({ command: 'runCommand', code: 'executeCode', owner: COMPUTER_AGENTD_CONTRACT });
|
|
29
|
+
|
|
30
|
+
export const CLOUD_BROWSER_TOOL_NAMES = Object.freeze([
|
|
31
|
+
'cloud_browser_open',
|
|
32
|
+
'cloud_browser_snapshot',
|
|
33
|
+
'cloud_browser_click',
|
|
34
|
+
'cloud_browser_type',
|
|
35
|
+
'cloud_browser_press',
|
|
36
|
+
'cloud_browser_screenshot',
|
|
37
|
+
'cloud_browser_download',
|
|
38
|
+
'cloud_browser_close',
|
|
39
|
+
]);
|
|
40
|
+
export const CLOUD_EXECUTION_TOOL_NAMES = Object.freeze(['cloud_bash', 'cloud_run_code']);
|
|
41
|
+
export const CLOUD_DESKTOP_TOOL_NAMES = Object.freeze(['cloud_computer', 'cloud_workspace']);
|
|
42
|
+
|
|
43
|
+
const CODE_LANGUAGES = Object.freeze(['javascript', 'python']);
|
|
44
|
+
const KEY_ALLOWLIST = Object.freeze([
|
|
45
|
+
'Enter', 'Tab', 'Escape', 'Backspace', 'Delete', 'ArrowUp', 'ArrowDown',
|
|
46
|
+
'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Space',
|
|
47
|
+
]);
|
|
48
|
+
const ACTIONS = new Set(COMPUTER_ACTION_NAMES);
|
|
49
|
+
const text = (value, field) => {
|
|
50
|
+
const result = String(value ?? '').trim();
|
|
51
|
+
if (!result) throw new TypeError(`${field} must be a non-empty string`);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
const bounded = (value, field, maxBytes) => {
|
|
55
|
+
const result = text(value, field);
|
|
56
|
+
if (Buffer.byteLength(result, 'utf8') > maxBytes) throw new TypeError(`${field} exceeds ${maxBytes} bytes`);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
const taskIdOf = (value) => bounded(value ?? 'interactive', 'taskId', 128);
|
|
60
|
+
const output = value => [{ type: 'text', text: String(value?.result || '') }];
|
|
61
|
+
const resultSchema = { type: 'object', properties: { result: { type: 'string' } }, required: ['result'] };
|
|
62
|
+
const envelope = value => ({ result: typeof value === 'string' ? value : JSON.stringify(value) ?? '' });
|
|
63
|
+
const schema = (properties = {}, required = []) => ({
|
|
64
|
+
type: 'object', properties, required, additionalProperties: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function actionDefinition(name, description, properties, required, operation, operationExecutor, mapArgs = args => ({ ...args })) {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
description,
|
|
71
|
+
parameters: schema(properties, required),
|
|
72
|
+
output: { schema: resultSchema, render: (_args, value) => output(value) },
|
|
73
|
+
execute: async (args, execution) => envelope(await operationExecutor(operation, mapArgs(args || {}), execution)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create broker-backed tools for one Bot. A lease is acquired lazily on the
|
|
79
|
+
* first action, so simply loading the plugin cannot claim a user's computer.
|
|
80
|
+
*/
|
|
81
|
+
export function createComputerToolset({
|
|
82
|
+
broker,
|
|
83
|
+
userId,
|
|
84
|
+
assistantId,
|
|
85
|
+
botId,
|
|
86
|
+
computerId,
|
|
87
|
+
taskId = 'interactive',
|
|
88
|
+
leaseTtlMs = 60_000,
|
|
89
|
+
now = () => Date.now(),
|
|
90
|
+
agent,
|
|
91
|
+
} = {}) {
|
|
92
|
+
if (!broker || typeof broker.action !== 'function' || typeof broker.acquireLease !== 'function' || typeof broker.renewLease !== 'function') {
|
|
93
|
+
throw new TypeError('A connected Computer Broker client is required');
|
|
94
|
+
}
|
|
95
|
+
const identity = {
|
|
96
|
+
...(userId === undefined ? {} : { userId: text(userId, 'userId') }),
|
|
97
|
+
assistantId: text(assistantId, 'assistantId'),
|
|
98
|
+
botId: text(botId, 'botId'),
|
|
99
|
+
computerId: text(computerId, 'computerId'),
|
|
100
|
+
};
|
|
101
|
+
const initialTaskId = taskIdOf(taskId);
|
|
102
|
+
let activeLease = null;
|
|
103
|
+
let disposed = false;
|
|
104
|
+
|
|
105
|
+
async function ensureLease(requestedTaskId = initialTaskId, signal) {
|
|
106
|
+
if (disposed) throw new Error('COMPUTER_TOOLSET_DISPOSED');
|
|
107
|
+
const activeTaskId = taskIdOf(requestedTaskId);
|
|
108
|
+
// 即使本地缓存未过期,也向 Broker 幂等确认一次;用户可能已经接管了旧租约。
|
|
109
|
+
if (activeLease && (activeLease.taskId !== activeTaskId || activeLease.expiresAt <= now())) await release().catch(() => {});
|
|
110
|
+
const idempotencyKey = `${identity.assistantId}:${identity.botId}:${identity.computerId}:${activeTaskId}`;
|
|
111
|
+
const deadline = Date.now() + 120000;
|
|
112
|
+
let response;
|
|
113
|
+
for (;;) {
|
|
114
|
+
signal?.throwIfAborted();
|
|
115
|
+
if (disposed) throw new Error('COMPUTER_TOOLSET_DISPOSED');
|
|
116
|
+
try { response = await broker.acquireLease(createLeaseRequest({ ...identity, taskId: activeTaskId, idempotencyKey, ttlMs: leaseTtlMs })); break; }
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error?.code !== 'COMPUTER_USER_CONTROLLED' || Date.now() >= deadline) throw error;
|
|
119
|
+
activeLease = null;
|
|
120
|
+
// 只等待取得新租约,不重放已经开始或结果未知的输入。
|
|
121
|
+
await sleep(500, undefined, { signal });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const lease = response?.lease;
|
|
125
|
+
if (!lease?.leaseId) throw new Error('COMPUTER_LEASE_INVALID');
|
|
126
|
+
activeLease = lease;
|
|
127
|
+
return lease;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function release() {
|
|
131
|
+
if (!activeLease) return false;
|
|
132
|
+
const lease = activeLease;
|
|
133
|
+
activeLease = null;
|
|
134
|
+
const response = await broker.releaseLease(createReleaseRequest({ ...identity, taskId: lease.taskId, leaseId: lease.leaseId, botId: identity.botId }));
|
|
135
|
+
return response?.released === true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function action(name, args = {}, execution = {}) {
|
|
139
|
+
if (!ACTIONS.has(name)) throw new TypeError(`Unsupported computer action: ${name}`);
|
|
140
|
+
const lease = await ensureLease(execution?.taskId, execution?.signal);
|
|
141
|
+
const request = createActionRequest({
|
|
142
|
+
...identity,
|
|
143
|
+
taskId: lease.taskId,
|
|
144
|
+
leaseId: lease.leaseId,
|
|
145
|
+
action: name,
|
|
146
|
+
args,
|
|
147
|
+
});
|
|
148
|
+
const cancellation = new AbortController();
|
|
149
|
+
const signal = execution.signal ? AbortSignal.any([execution.signal, cancellation.signal]) : cancellation.signal;
|
|
150
|
+
let renewing = false, heartbeat;
|
|
151
|
+
const renew = async () => {
|
|
152
|
+
if (disposed) throw new Error('COMPUTER_TOOLSET_DISPOSED');
|
|
153
|
+
const response = await broker.renewLease({ ...identity, taskId: lease.taskId, leaseId: lease.leaseId, ttlMs: leaseTtlMs }, { signal });
|
|
154
|
+
if (response?.lease?.leaseId !== lease.leaseId) throw new Error('COMPUTER_LEASE_INVALID');
|
|
155
|
+
if (activeLease?.leaseId === lease.leaseId) activeLease = response.lease;
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
signal.throwIfAborted();
|
|
159
|
+
await renew();
|
|
160
|
+
signal.throwIfAborted();
|
|
161
|
+
heartbeat = setInterval(() => {
|
|
162
|
+
if (renewing || signal.aborted) return;
|
|
163
|
+
renewing = true;
|
|
164
|
+
void renew().catch(error => cancellation.abort(error)).finally(() => { renewing = false; });
|
|
165
|
+
}, Math.max(50, Math.min(20_000, Math.floor(leaseTtlMs / 3))));
|
|
166
|
+
heartbeat.unref?.();
|
|
167
|
+
return await broker.action(request, { signal });
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
// 旧租约已被用户接管或撤销。只清缓存,不重放可能已经执行的动作。
|
|
171
|
+
if (Number(error?.status) === 409) activeLease = null;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
finally { clearInterval(heartbeat); cancellation.abort(new Error('COMPUTER_ACTION_FINISHED')); }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const definitions = [
|
|
178
|
+
createDesktopTool({ action, agent }),
|
|
179
|
+
createWorkspaceTool({ action }),
|
|
180
|
+
actionDefinition('cloud_browser_open', 'Open a public URL in the dedicated Cloud Computer browser.', { url: { type: 'string' } }, ['url'], 'open', action, args => ({ url: bounded(args.url, 'url', 4096) })),
|
|
181
|
+
actionDefinition('cloud_browser_snapshot', 'Read bounded text and links from the current Cloud Computer page.', {}, [], 'snapshot', action),
|
|
182
|
+
actionDefinition('cloud_browser_click', 'Click a CSS selector in the Cloud Computer browser.', { selector: { type: 'string' } }, ['selector'], 'click', action, args => ({ selector: bounded(args.selector, 'selector', 1024) })),
|
|
183
|
+
actionDefinition('cloud_browser_type', 'Type bounded text into the focused Cloud Computer browser element.', { text: { type: 'string' }, selector: { type: 'string' } }, ['text'], 'type', action, args => ({ text: bounded(args.text, 'text', 16 * 1024), ...(args.selector === undefined ? {} : { selector: bounded(args.selector, 'selector', 1024) }) })),
|
|
184
|
+
actionDefinition('cloud_browser_press', 'Press one allowlisted key in the Cloud Computer browser.', { key: { type: 'string' } }, ['key'], 'press', action, args => {
|
|
185
|
+
const key = bounded(args.key, 'key', 64);
|
|
186
|
+
if (!KEY_ALLOWLIST.includes(key)) throw new TypeError(`Key is not allowlisted: ${key}`);
|
|
187
|
+
return { key };
|
|
188
|
+
}),
|
|
189
|
+
actionDefinition('cloud_browser_screenshot', 'Capture the current Cloud Computer browser viewport.', {}, [], 'screenshot', action),
|
|
190
|
+
actionDefinition('cloud_browser_download', 'Download a bounded file through the Cloud Computer browser.', { url: { type: 'string' }, selector: { type: 'string' }, timeoutMs: { type: 'number' } }, [], 'download', action, args => ({
|
|
191
|
+
...(args.url === undefined ? {} : { url: bounded(args.url, 'url', 4096) }),
|
|
192
|
+
...(args.selector === undefined ? {} : { selector: bounded(args.selector, 'selector', 1024) }),
|
|
193
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
194
|
+
})),
|
|
195
|
+
actionDefinition('cloud_browser_close', 'Close the Cloud Computer browser session.', {}, [], 'stop', action),
|
|
196
|
+
actionDefinition('cloud_bash', 'Run bash, git, ssh and skill scripts in the Cloud Computer workspace. Public internet (DNS/HTTPS/SSH) is available by default; host credentials and private desktop control endpoints remain isolated.', { command: { type: 'string' }, timeoutMs: { type: 'number' } }, ['command'], 'terminal.exec', action, args => ({
|
|
197
|
+
language: 'shell', command: bounded(args.command, 'command', 16 * 1024), ...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }),
|
|
198
|
+
})),
|
|
199
|
+
actionDefinition('cloud_run_code', 'Run bounded JavaScript or Python inside the Cloud Computer terminal sandbox.', { language: { type: 'string', enum: [...CODE_LANGUAGES] }, code: { type: 'string' }, timeoutMs: { type: 'number' } }, ['language', 'code'], 'terminal.exec', action, args => {
|
|
200
|
+
const language = bounded(args.language, 'language', 32);
|
|
201
|
+
if (!CODE_LANGUAGES.includes(language)) throw new TypeError(`Unsupported code language: ${language}`);
|
|
202
|
+
return { language, code: bounded(args.code, 'code', 64 * 1024), ...(args.timeoutMs === undefined ? {} : { timeoutMs: Number(args.timeoutMs) }) };
|
|
203
|
+
}),
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
return Object.freeze({
|
|
207
|
+
identity: Object.freeze({ ...identity }),
|
|
208
|
+
capabilities: Object.freeze([PERSONAL_ASSISTANT_BROWSER_CAPABILITY, PERSONAL_ASSISTANT_CODE_EXECUTION_CAPABILITY, CLOUD_COMPUTER_CAPABILITY]),
|
|
209
|
+
tools: Object.freeze(definitions),
|
|
210
|
+
get lease() { return activeLease; },
|
|
211
|
+
execute(name, args, execution) {
|
|
212
|
+
const definition = definitions.find(item => item.name === name);
|
|
213
|
+
if (!definition) throw new TypeError(`Unknown personal assistant computer tool: ${name}`);
|
|
214
|
+
return definition.execute(args, execution);
|
|
215
|
+
},
|
|
216
|
+
async executeRaw(name, args, execution) {
|
|
217
|
+
const result = await this.execute(name, args, execution);
|
|
218
|
+
try { return JSON.parse(result.result); } catch { return result.result; }
|
|
219
|
+
},
|
|
220
|
+
async release() { return release(); },
|
|
221
|
+
async dispose() { disposed = true; await release().catch(() => {}); },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Execute a bounded command through computer-agentd's terminal executor. */
|
|
226
|
+
export function runCommand(toolset, command, options = {}, execution) {
|
|
227
|
+
if (!toolset || typeof toolset.execute !== 'function') throw new TypeError('toolset is required');
|
|
228
|
+
return toolset.execute('cloud_bash', { command, ...options }, execution);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Execute bounded JavaScript/Python code through computer-agentd. */
|
|
232
|
+
export function executeCode(toolset, language, code, options = {}, execution) {
|
|
233
|
+
if (!toolset || typeof toolset.execute !== 'function') throw new TypeError('toolset is required');
|
|
234
|
+
return toolset.execute('cloud_run_code', { language, code, ...options }, execution);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Register broker-backed tools through the public DSH tools service. */
|
|
238
|
+
export function registerComputerTools({ agent, register, toolset } = {}) {
|
|
239
|
+
if (!toolset || typeof toolset.tools?.[Symbol.iterator] !== 'function') throw new TypeError('toolset is required');
|
|
240
|
+
const injected = agent?.ctx?.get?.('tools');
|
|
241
|
+
const add = typeof register === 'function' ? register : injected?.register?.bind(injected);
|
|
242
|
+
if (typeof add !== 'function') throw new TypeError('A public DSH tools.register function is required');
|
|
243
|
+
const disposers = toolset.tools.map(definition => add(definition)).filter(item => typeof item === 'function');
|
|
244
|
+
return Object.freeze({ names: Object.freeze(toolset.tools.map(item => item.name)), definitions: toolset.tools, disposers });
|
|
245
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { open, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
export const CLOUD_ATTACHMENT_MAX_BYTES = 512 * 1024;
|
|
6
|
+
const fail = message => Object.assign(new Error(message), { code: 'CLOUD_FILE_INVALID', status: 400 });
|
|
7
|
+
const segments = path => {
|
|
8
|
+
if (typeof path !== 'string' || path.length > 2048) throw fail('文件路径无效');
|
|
9
|
+
const parts = path.split('/');
|
|
10
|
+
if (!parts.length || parts.length > 32 || parts.some(part => !part || part === '.' || part === '..' || /[\\\u0000-\u001f]/u.test(part))) throw fail('文件必须位于云电脑工作区内');
|
|
11
|
+
return parts;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** 逐层固定目录 FD,拒绝符号链接;命令并发换名不能把 Runtime 的文件访问引到私有目录。 */
|
|
15
|
+
export function createCloudFileStore(root) {
|
|
16
|
+
const ownedPrefix = (botId, sessionId) => `uploads/${createHash('sha256').update(JSON.stringify([botId, sessionId])).digest('hex').slice(0, 32)}/`;
|
|
17
|
+
const withParent = async (path, create, task) => {
|
|
18
|
+
const parts = segments(path); let parent = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
19
|
+
try {
|
|
20
|
+
for (const part of parts.slice(0, -1)) {
|
|
21
|
+
const target = `/proc/self/fd/${parent.fd}/${part}`;
|
|
22
|
+
if (create) await mkdir(target, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
|
|
23
|
+
const next = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
24
|
+
await parent.close(); parent = next;
|
|
25
|
+
}
|
|
26
|
+
return await task(`/proc/self/fd/${parent.fd}/${parts.at(-1)}`);
|
|
27
|
+
} finally { await parent.close(); }
|
|
28
|
+
};
|
|
29
|
+
const read = path => withParent(path, false, async target => {
|
|
30
|
+
const file = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
31
|
+
try {
|
|
32
|
+
const info = await file.stat();
|
|
33
|
+
if (!info.isFile() || info.size > CLOUD_ATTACHMENT_MAX_BYTES) throw fail('仅支持不超过 512 KiB 的普通文件');
|
|
34
|
+
const bytes = Buffer.alloc(CLOUD_ATTACHMENT_MAX_BYTES + 1);
|
|
35
|
+
const { bytesRead } = await file.read(bytes, 0, bytes.length, 0);
|
|
36
|
+
if (bytesRead > CLOUD_ATTACHMENT_MAX_BYTES) throw fail('文件超过 512 KiB');
|
|
37
|
+
return bytes.subarray(0, bytesRead);
|
|
38
|
+
} finally { await file.close(); }
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
async upload({ botId, sessionId, name, dataUrl }) {
|
|
42
|
+
if (typeof name !== 'string' || name.length > 120 || /[\/\\\u0000-\u001f]/u.test(name)) throw fail('文件名无效');
|
|
43
|
+
const match = typeof dataUrl === 'string' && dataUrl.match(/^data:([a-z0-9.+-]+\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/]*={0,2})$/iu);
|
|
44
|
+
if (!match || match[2].length > Math.ceil(CLOUD_ATTACHMENT_MAX_BYTES / 3) * 4) throw fail('附件格式无效或超过 512 KiB');
|
|
45
|
+
const bytes = Buffer.from(match[2], 'base64');
|
|
46
|
+
if (bytes.length > CLOUD_ATTACHMENT_MAX_BYTES || bytes.toString('base64') !== match[2]) throw fail('附件编码无效');
|
|
47
|
+
const path = ownedPrefix(botId, sessionId) + randomUUID() + '-' + name;
|
|
48
|
+
await withParent(path, true, async target => { const file = await open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); try { await file.writeFile(bytes); } finally { await file.close(); } });
|
|
49
|
+
return { path, name, bytes: bytes.length, mediaType: match[1].toLowerCase() };
|
|
50
|
+
},
|
|
51
|
+
async attachment({ botId, sessionId, path }) {
|
|
52
|
+
if (typeof path !== 'string' || !path.startsWith(ownedPrefix(botId, sessionId))) throw fail('附件不属于当前云端会话');
|
|
53
|
+
return read(path);
|
|
54
|
+
},
|
|
55
|
+
async download(path) { const data = await read(path); return { path, name: path.split('/').at(-1), bytes: data.length, data: data.toString('base64') }; },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 个人助理新建声明的默认模型;显式指定其他模型时仍尊重声明。 */
|
|
2
|
+
export const DEFAULT_PERSONAL_ASSISTANT_MODEL = 'deepseek-v4.1-flash-1017';
|
|
3
|
+
|
|
4
|
+
export function cloudModelDefinition(model = DEFAULT_PERSONAL_ASSISTANT_MODEL) {
|
|
5
|
+
return {
|
|
6
|
+
id: model, name: model, contextWindow: 131072, maxTokens: 8192,
|
|
7
|
+
// 只有经云网关实际图像输入验证的默认模型声明视觉能力。
|
|
8
|
+
input: model === DEFAULT_PERSONAL_ASSISTANT_MODEL ? ['text', 'image'] : ['text'],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const fail = (code, message, status = 503) => Object.assign(new Error(message), { code, status });
|
|
4
|
+
|
|
5
|
+
/** 独立于桌面画面的云会话连接;只使用 Host 签发的 Agent/Bot control 票据。 */
|
|
6
|
+
export function createCloudSessionTransport({ issueTicket, WebSocketImpl, ca, timeoutMs = 15_000 }) {
|
|
7
|
+
const connections = new Map();
|
|
8
|
+
async function connect(scope, signal) {
|
|
9
|
+
const key = JSON.stringify([scope.agentId, scope.botId]);
|
|
10
|
+
if (connections.has(key)) return connections.get(key);
|
|
11
|
+
const promise = (async () => {
|
|
12
|
+
const ticket = await issueTicket({ ...scope, mode: 'control' }, signal);
|
|
13
|
+
if (typeof WebSocketImpl !== 'function') throw fail('PERSONAL_ASSISTANT_CLOUD_SESSION_UNAVAILABLE', '云端会话连接组件不可用');
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let socket, opened = false, idle;
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
const error = () => fail('PERSONAL_ASSISTANT_CLOUD_SESSION_UNAVAILABLE', '云端会话连接中断;请核对发送状态后重试');
|
|
18
|
+
const invalidate = failure => {
|
|
19
|
+
clearTimeout(idle); clearTimeout(openTimer);
|
|
20
|
+
if (connections.get(key) === promise) connections.delete(key);
|
|
21
|
+
for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(failure); }
|
|
22
|
+
pending.clear();
|
|
23
|
+
if (!opened) reject(failure);
|
|
24
|
+
};
|
|
25
|
+
const touch = () => { clearTimeout(idle); idle = setTimeout(() => socket.close(1000, 'session idle'), 300_000); idle.unref?.(); };
|
|
26
|
+
const openTimer = setTimeout(() => { invalidate(error()); socket?.close(); }, timeoutMs);
|
|
27
|
+
openTimer.unref?.();
|
|
28
|
+
try { socket = new WebSocketImpl(ticket.wsUrl, ['agentkit.personal-assistant.desktop.v1', `agentkit.desktop.ticket.${ticket.ticket}`], { ca }); }
|
|
29
|
+
catch { invalidate(error()); return; }
|
|
30
|
+
socket.onopen = () => {
|
|
31
|
+
opened = true; clearTimeout(openTimer); touch();
|
|
32
|
+
resolve({
|
|
33
|
+
close: () => { invalidate(error()); socket.close(1000, 'Host disposed'); },
|
|
34
|
+
request(operation, payload, signal) {
|
|
35
|
+
if (socket.readyState !== 1) return Promise.reject(error());
|
|
36
|
+
const requestId = randomUUID();
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const finish = (failure, value) => { const entry = pending.get(requestId); if (!entry) return; pending.delete(requestId); clearTimeout(entry.timer); signal?.removeEventListener('abort', abort); failure ? reject(failure) : resolve(value); };
|
|
39
|
+
const abort = () => finish(fail('PERSONAL_ASSISTANT_REQUEST_ABORTED', '请求已取消;已提交的云端任务会继续运行', 499));
|
|
40
|
+
if (signal?.aborted) { reject(signal.reason || error()); return; }
|
|
41
|
+
const timer = setTimeout(() => finish(fail('PERSONAL_ASSISTANT_CLOUD_SESSION_TIMEOUT', '云端响应超时;发送结果待确认,请勿重复创建任务', 504)), timeoutMs);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
pending.set(requestId, { timer, reject: failure => finish(failure), resolve: value => finish(null, value) });
|
|
44
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
45
|
+
touch();
|
|
46
|
+
try { socket.send(JSON.stringify({ type: 'session.request', requestId, agentId: scope.agentId, botId: scope.botId, operation, payload })); }
|
|
47
|
+
catch { finish(error()); }
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
socket.onmessage = event => {
|
|
53
|
+
let value; try { value = JSON.parse(String(event.data)); } catch { return; }
|
|
54
|
+
if (value.type === 'error') {
|
|
55
|
+
const failure = fail(value.code === 'DESKTOP_MESSAGE_UNSUPPORTED' ? 'PERSONAL_ASSISTANT_CLOUD_SESSION_UPGRADE_REQUIRED' : value.code || 'PERSONAL_ASSISTANT_CLOUD_SESSION_UNAVAILABLE', value.code === 'DESKTOP_MESSAGE_UNSUPPORTED' ? '当前云端版本不支持对话,请升级云端助理;不会使用本机执行' : '云端拒绝会话请求', value.status || 409);
|
|
56
|
+
invalidate(failure); socket.close(); return;
|
|
57
|
+
}
|
|
58
|
+
if (value.type !== 'session.response') return;
|
|
59
|
+
const entry = pending.get(value.requestId);
|
|
60
|
+
if (!entry) return;
|
|
61
|
+
if (value.agentId !== scope.agentId || value.botId !== scope.botId) { entry.reject(fail('PERSONAL_ASSISTANT_AGENT_MISMATCH', '云端会话身份不一致', 403)); return; }
|
|
62
|
+
if (value.ok) entry.resolve(value.value);
|
|
63
|
+
else entry.reject(fail(value.error?.code || 'PERSONAL_ASSISTANT_CLOUD_SESSION_FAILED', value.error?.message || '云端会话请求失败', value.error?.status || 503));
|
|
64
|
+
};
|
|
65
|
+
socket.onclose = () => invalidate(error());
|
|
66
|
+
socket.onerror = () => { invalidate(error()); try { socket.close(); } catch {} };
|
|
67
|
+
});
|
|
68
|
+
})();
|
|
69
|
+
connections.set(key, promise);
|
|
70
|
+
try { return await promise; } catch (error) { if (connections.get(key) === promise) connections.delete(key); throw error; }
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
async request(scope, operation, payload, signal) { return (await connect(scope, signal)).request(operation, payload, signal); },
|
|
74
|
+
dispose() { for (const connection of connections.values()) void connection.then(value => value.close()).catch(() => {}); connections.clear(); },
|
|
75
|
+
};
|
|
76
|
+
}
|