mu-agent-bridge 1.0.73 → 1.0.78
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 +12 -8
- package/dist/src-ts/cli.js +67 -37
- package/dist/src-ts/control-server.js +7 -28
- package/dist/src-ts/core-updater.js +141 -33
- package/dist/src-ts/daemon-process.js +2 -1
- package/dist/src-ts/dsh-agent.js +7 -2
- package/dist/src-ts/http-agent.js +36 -0
- package/dist/src-ts/plugin-manager.js +185 -58
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +8 -6
- package/dist/src-ts/plugins/abweb/public/index.html +3 -3
- package/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/dist/src-ts/plugins/aun/index.js +89 -11
- package/dist/src-ts/plugins/aun/messages.js +1 -1
- package/dist/src-ts/plugins/aun/public/app.js +2 -1
- package/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/dist/src-ts/plugins/chatgpt/index.js +3 -2
- package/dist/src-ts/plugins/claude-cli/index.js +1 -1
- package/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
- package/dist/src-ts/plugins/doubao/index.js +119 -7
- package/dist/src-ts/plugins/doubao/native-task.js +134 -0
- package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
- package/dist/src-ts/plugins/dsh/index.js +102 -23
- package/dist/src-ts/plugins/dsh/session-history.js +31 -3
- package/dist/src-ts/plugins/feishu/index.js +1 -1
- package/dist/src-ts/plugins/personal-assistant/index.js +29 -8
- package/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
- package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
- package/dist/src-ts/plugins/qwenwork/index.js +52 -18
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
- package/dist/src-ts/plugins/workbuddy/index.js +24 -12
- package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
- package/dist/src-ts/plugins/zcode/index.js +97 -0
- package/dist/src-ts/session-management.js +27 -8
- package/dist/src-ts/version-manager.js +6 -3
- package/dist/tests-ts/abweb-model-config.test.js +44 -41
- package/dist/tests-ts/aun-attachments.test.js +19 -17
- package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
- package/dist/tests-ts/aun-messages.test.js +14 -0
- package/dist/tests-ts/aun-plugin.test.js +207 -19
- package/dist/tests-ts/bundled-package.test.js +1 -1
- package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
- package/dist/tests-ts/chatgpt-plugin.test.js +19 -0
- package/dist/tests-ts/control-server.test.js +20 -0
- package/dist/tests-ts/core-updater.test.js +215 -46
- package/dist/tests-ts/daemon-process.test.js +126 -1
- package/dist/tests-ts/desktop-launch.test.js +73 -0
- package/dist/tests-ts/doubao-native-task.test.js +237 -0
- package/dist/tests-ts/doubao-plugin.test.js +688 -19
- package/dist/tests-ts/dsh-agent.test.js +91 -19
- package/dist/tests-ts/dsh-cdp.test.js +148 -0
- package/dist/tests-ts/dsh-history.test.js +37 -0
- package/dist/tests-ts/feishu-plugin.test.js +1 -1
- package/dist/tests-ts/http-agent.test.js +18 -0
- package/dist/tests-ts/local-cli.test.js +29 -2
- package/dist/tests-ts/model-provider-cancellation.test.js +2 -0
- package/dist/tests-ts/native-session-management.test.js +8 -7
- package/dist/tests-ts/personal-assistant-coordination.test.js +16 -0
- package/dist/tests-ts/personal-assistant-extensions.test.js +7 -1
- package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
- package/dist/tests-ts/personal-assistant.test.js +277 -3
- package/dist/tests-ts/platform-integration.test.js +60 -1
- package/dist/tests-ts/plugin-manager.test.js +78 -6
- package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
- package/dist/tests-ts/publish-packages.test.js +1 -1
- package/dist/tests-ts/qwenwork-plugin.test.js +30 -0
- package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
- package/dist/tests-ts/qwenwork-sse.test.js +33 -0
- package/dist/tests-ts/session-access-defaults.test.js +112 -0
- package/dist/tests-ts/session-management.test.js +8 -8
- package/dist/tests-ts/version-manager.test.js +40 -0
- package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +356 -13
- package/dist/tests-ts/zcode-cdp.test.js +894 -0
- package/dist/tests-ts/zcode-plugin.test.js +396 -0
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +8 -6
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +7 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -3
- package/plugins/abweb/package.json +1 -1
- package/plugins/abweb/plugin.json +1 -1
- package/plugins/aun/config.schema.json +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +89 -11
- package/plugins/aun/dist/src-ts/plugins/aun/messages.js +1 -1
- package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
- package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +3 -2
- package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +1 -1
- package/plugins/claude-cli/package.json +1 -1
- package/plugins/claude-cli/plugin.json +1 -1
- package/plugins/doubao/config.schema.json +2 -1
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +119 -7
- package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/config.schema.json +5 -2
- package/plugins/dsh/dist/src-ts/dsh-agent.js +7 -2
- package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +102 -23
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
- package/plugins/feishu/package.json +1 -1
- package/plugins/feishu/plugin.json +1 -1
- package/plugins/personal-assistant/config.schema.json +3 -2
- package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +29 -8
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
- package/plugins/personal-assistant/package.json +2 -2
- package/plugins/personal-assistant/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +52 -18
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +24 -12
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
- package/plugins/zcode/config.schema.json +13 -0
- package/plugins/zcode/dist/src-ts/agent.js +69 -0
- package/plugins/zcode/dist/src-ts/attachments.js +62 -0
- package/plugins/zcode/dist/src-ts/plugin.js +13 -0
- package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
- package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
- package/plugins/zcode/package.json +16 -0
- package/plugins/zcode/plugin.json +11 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkbuddySessionHistoryProvider = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const unsupported = (reason) => ({ supported: false, reason });
|
|
6
|
+
class WorkbuddySessionHistoryProvider {
|
|
7
|
+
options;
|
|
8
|
+
scope;
|
|
9
|
+
limitation = "仅支持配置实例中当前已选任务的已加载完成正文;不枚举、切换任务,无法证明完整历史。";
|
|
10
|
+
listedSessionId;
|
|
11
|
+
snapshot;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.scope = options.scope;
|
|
15
|
+
}
|
|
16
|
+
get mode() { return this.options.supported() ? "current" : "unsupported"; }
|
|
17
|
+
async listSessions(query) {
|
|
18
|
+
const invalid = this.validateQuery(query, 50);
|
|
19
|
+
if (invalid)
|
|
20
|
+
return unsupported(invalid);
|
|
21
|
+
if (query.cursor !== undefined)
|
|
22
|
+
return unsupported("WorkBuddy 当前任务列表没有后续页");
|
|
23
|
+
return this.options.run(async () => {
|
|
24
|
+
this.listedSessionId = undefined;
|
|
25
|
+
this.snapshot = undefined;
|
|
26
|
+
const client = this.options.client();
|
|
27
|
+
const state = await client.historyAvailability();
|
|
28
|
+
if (state.waiting)
|
|
29
|
+
return unsupported("WorkBuddy 正在等待用户选择,暂不读取历史");
|
|
30
|
+
const before = state.conversationId;
|
|
31
|
+
const after = await client.conversationId();
|
|
32
|
+
if (!before || before !== after)
|
|
33
|
+
return unsupported("WorkBuddy 当前任务身份不可核验或读取期间已切换");
|
|
34
|
+
this.listedSessionId = before;
|
|
35
|
+
return { supported: true, sessions: [{ id: before, title: "WorkBuddy 当前任务" }], complete: false, missingReasons: [this.limitation], readAt: new Date().toISOString() };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async readSession(query) {
|
|
39
|
+
const invalid = this.validateQuery(query, 100);
|
|
40
|
+
if (invalid)
|
|
41
|
+
return unsupported(invalid);
|
|
42
|
+
if (!this.listedSessionId || query.sessionId !== this.listedSessionId)
|
|
43
|
+
return unsupported("WorkBuddy 仅支持读取本实例刚列出的当前任务");
|
|
44
|
+
return this.options.run(async () => {
|
|
45
|
+
const client = this.options.client();
|
|
46
|
+
const before = await client.conversationId();
|
|
47
|
+
if (!before || before !== query.sessionId) {
|
|
48
|
+
this.snapshot = undefined;
|
|
49
|
+
return unsupported("WorkBuddy 当前任务已切换,请重新列出任务");
|
|
50
|
+
}
|
|
51
|
+
const current = await client.historyAvailability();
|
|
52
|
+
if (current.waiting) {
|
|
53
|
+
this.snapshot = undefined;
|
|
54
|
+
return unsupported("WorkBuddy 正在等待用户选择,暂不读取历史");
|
|
55
|
+
}
|
|
56
|
+
if (current.conversationId !== before) {
|
|
57
|
+
this.snapshot = undefined;
|
|
58
|
+
return unsupported("WorkBuddy 读取前后任务身份发生变化");
|
|
59
|
+
}
|
|
60
|
+
let state = this.snapshot;
|
|
61
|
+
if (query.cursor !== undefined) {
|
|
62
|
+
if (!state || state.sessionId !== before || !state.cursor || query.cursor !== state.cursor)
|
|
63
|
+
return unsupported("WorkBuddy 历史游标无效或已过期");
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.snapshot = undefined;
|
|
67
|
+
const raw = await client.readHistorySnapshot();
|
|
68
|
+
if (raw.unavailableReason)
|
|
69
|
+
return unsupported(raw.unavailableReason);
|
|
70
|
+
if (raw.conversationId !== before)
|
|
71
|
+
return unsupported("WorkBuddy 正文与当前任务身份不一致");
|
|
72
|
+
if (!Array.isArray(raw.messages) || !Array.isArray(raw.missingReasons) || raw.missingReasons.some(reason => typeof reason !== "string"))
|
|
73
|
+
return unsupported("WorkBuddy 历史快照格式无效");
|
|
74
|
+
const turns = [];
|
|
75
|
+
const missingReasons = [this.limitation, ...raw.missingReasons];
|
|
76
|
+
const ids = new Set();
|
|
77
|
+
let characters = 0;
|
|
78
|
+
for (const message of raw.messages) {
|
|
79
|
+
if (!message || typeof message.id !== "string" || !message.id.trim() || typeof message.text !== "string" || (message.role !== "user" && message.role !== "assistant")) {
|
|
80
|
+
missingReasons.push("部分消息缺少稳定 ID 或可核验角色,已排除。");
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (message.complete !== true) {
|
|
84
|
+
missingReasons.push("当前任务存在未完成消息,已排除未完成正文。");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ids.has(message.id))
|
|
88
|
+
return unsupported("WorkBuddy 历史出现重复消息 ID,无法核验正文");
|
|
89
|
+
ids.add(message.id);
|
|
90
|
+
const text = message.text.trim();
|
|
91
|
+
if (!text)
|
|
92
|
+
continue;
|
|
93
|
+
characters += text.length;
|
|
94
|
+
if (characters > 60_000)
|
|
95
|
+
return unsupported("WorkBuddy 历史正文超过 60000 字符,拒绝读取而不截断");
|
|
96
|
+
turns.push({ id: message.id, role: message.role, text });
|
|
97
|
+
}
|
|
98
|
+
if (!turns.some(turn => turn.role === "user"))
|
|
99
|
+
missingReasons.push("当前 DOM 没有可核验的用户消息,不能证明完整对话。");
|
|
100
|
+
state = { sessionId: before, turns, missingReasons: [...new Set(missingReasons)], readAt: new Date().toISOString(), offset: 0 };
|
|
101
|
+
}
|
|
102
|
+
if (await client.conversationId() !== before) {
|
|
103
|
+
this.snapshot = undefined;
|
|
104
|
+
return unsupported("WorkBuddy 读取前后任务身份发生变化");
|
|
105
|
+
}
|
|
106
|
+
const turns = state.turns.slice(state.offset, state.offset + query.limit);
|
|
107
|
+
state.offset += turns.length;
|
|
108
|
+
state.cursor = state.offset < state.turns.length ? (0, node_crypto_1.randomUUID)() : undefined;
|
|
109
|
+
this.snapshot = state;
|
|
110
|
+
return { supported: true, session: { id: before, title: "WorkBuddy 当前任务" }, turns, ...(state.cursor ? { nextCursor: state.cursor } : {}), complete: false, missingReasons: state.missingReasons, readAt: state.readAt };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
validateQuery(query, maximum) {
|
|
114
|
+
if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > maximum)
|
|
115
|
+
return `WorkBuddy 每页数量必须为 1–${maximum}`;
|
|
116
|
+
if (query.cursor !== undefined && (typeof query.cursor !== "string" || !query.cursor.trim()))
|
|
117
|
+
return "WorkBuddy 历史游标无效";
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.WorkbuddySessionHistoryProvider = WorkbuddySessionHistoryProvider;
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZcodeCdpClient = exports.zcodeReplyArtifactsScript = exports.zcodeSnapshotScript = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const agent_1 = require("../../agent");
|
|
9
|
+
const attachments_1 = require("../../attachments");
|
|
10
|
+
const cdp_client_1 = require("../workbuddy/cdp-client");
|
|
11
|
+
const editorSelector = '[data-testid="v4-composer-input"]';
|
|
12
|
+
const paneSelector = '[data-testid="v4-pane-shell-workspace-main"]';
|
|
13
|
+
const sessionSelector = '[data-testid="v4-session-pane-workspace-main"]';
|
|
14
|
+
const sendSelector = '[data-testid="v4-composer-send"]';
|
|
15
|
+
const busySelector = 'button[aria-label="停止生成"],button[aria-label="停止"],button[aria-label="Stop generating"],[data-testid="chat-loading"],[data-query-row-id][data-running="true"]';
|
|
16
|
+
const permissionSelector = 'button[data-permission-option-kind]';
|
|
17
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
// shortcut: 依据 ZCode v4 的可测试 DOM 适配,桌面改版时需重新完成真机验收。
|
|
19
|
+
exports.zcodeSnapshotScript = `(() => {
|
|
20
|
+
const root = document.querySelector(${JSON.stringify(paneSelector)});
|
|
21
|
+
const editor = root?.querySelector(${JSON.stringify(editorSelector)});
|
|
22
|
+
const running = root?.closest('[data-testid="side-pane-full-view-host"]')?.getAttribute('data-running');
|
|
23
|
+
const id = root?.querySelector(${JSON.stringify(sessionSelector)})?.getAttribute('data-session-id');
|
|
24
|
+
const error = root?.querySelector('[role="alert"],[data-testid="chat-error"],[data-testid="chat-error-banner"],[data-testid="v4-session-quota-banner"]');
|
|
25
|
+
const list = root?.querySelector(${JSON.stringify(permissionSelector)})?.closest('[role="listbox"]');
|
|
26
|
+
const interaction = list ? {
|
|
27
|
+
// React 以权限请求 ID 为 key 重建面板,节点标识可拒绝文案相同的新请求。
|
|
28
|
+
id: list.__agentBridgePermissionId ??= crypto.randomUUID(),
|
|
29
|
+
prompt: list.parentElement?.previousElementSibling?.innerText.trim() || 'ZCode 需要权限',
|
|
30
|
+
options: [...list.querySelectorAll(${JSON.stringify(permissionSelector)})].map(node => node.innerText.replace(/^\\s*\\d+[.、]\\s*/, '').trim()),
|
|
31
|
+
labels: [...list.querySelectorAll(${JSON.stringify(permissionSelector)})].map(node => node.getAttribute('aria-label') || ''),
|
|
32
|
+
} : undefined;
|
|
33
|
+
const rows = [...(root?.querySelectorAll('[data-row-id]') || [])].flatMap(row => {
|
|
34
|
+
const user = row.querySelector('[data-v4-user-input-collapsible-content]');
|
|
35
|
+
const assistant = row.classList.contains('group/assistant-row') && row.querySelector('[data-conversation-selectable="true"]');
|
|
36
|
+
const content = user || assistant;
|
|
37
|
+
const id = row.getAttribute('data-row-id');
|
|
38
|
+
let text = content ? content.innerText.trim() : '';
|
|
39
|
+
if (user && id) {
|
|
40
|
+
for (let fiber = user[Object.keys(user).find(key => key.startsWith('__reactFiber'))]; fiber; fiber = fiber.return) {
|
|
41
|
+
if (String(fiber.memoizedProps?.rowId) === id && typeof fiber.memoizedProps?.contentText === 'string') { text = fiber.memoizedProps.contentText.trim(); break; }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return content && id ? [{ id, role: user ? 'user' : 'assistant', text,
|
|
45
|
+
...(assistant ? { finished: Boolean(root.querySelector('[data-testid="v4-feedback-like-' + id + '"]')) } : {}) }] : [];
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
sessionId: id && id !== 'draft' ? id : undefined,
|
|
49
|
+
editor: editor?.getAttribute('contenteditable') === 'true', draft: editor?.innerText.replace(/[\\uFEFF\\u200B]/g, '').trim() || '',
|
|
50
|
+
running: running === 'true' || Boolean(root?.querySelector(${JSON.stringify(busySelector)})),
|
|
51
|
+
send: Boolean(root?.querySelector(${JSON.stringify(sendSelector)})),
|
|
52
|
+
error: error?.innerText.trim() || '', errorId: error ? (error.__agentBridgeErrorId ??= crypto.randomUUID()) : undefined, rows,
|
|
53
|
+
completed: running === 'false', interaction,
|
|
54
|
+
};
|
|
55
|
+
})()`;
|
|
56
|
+
exports.zcodeReplyArtifactsScript = `(expected, rowId) => {
|
|
57
|
+
const state = ${exports.zcodeSnapshotScript};
|
|
58
|
+
if (state.error || state.sessionId !== expected.sessionId || JSON.stringify(state.rows.filter(row => row.role === 'user')) !== JSON.stringify(expected.rows.filter(row => row.role === 'user'))) throw new Error('ZCode 任务或回复轮次已变化,未读取附件');
|
|
59
|
+
// 完成标记出现后正文仍可能重绘,同一轮先等下一次稳定快照。
|
|
60
|
+
if (state.running || state.interaction || !state.completed || JSON.stringify(state.rows) !== JSON.stringify(expected.rows)) return null;
|
|
61
|
+
const root = document.querySelector(${JSON.stringify(paneSelector)});
|
|
62
|
+
const row = [...(root?.querySelectorAll('[data-row-id]') || [])].find(item => item.getAttribute('data-row-id') === rowId);
|
|
63
|
+
const content = row?.querySelector('[data-conversation-selectable="true"]');
|
|
64
|
+
const nodes = content?.querySelectorAll('img[data-markdown-image][data-streamdown="image"]') || [];
|
|
65
|
+
if (nodes.length > ${attachments_1.MAX_ATTACHMENTS}) throw new Error('ZCode 回复图片数量超过 8 个');
|
|
66
|
+
let total = 0;
|
|
67
|
+
const images = [...nodes].map(image => {
|
|
68
|
+
const src = image.getAttribute('src') || '';
|
|
69
|
+
const size = (src.length - src.indexOf(',') - 1) / 4 * 3 - (src.endsWith('==') ? 2 : src.endsWith('=') ? 1 : 0);
|
|
70
|
+
if (size > ${attachments_1.MAX_ATTACHMENT_BYTES}) throw new Error('ZCode 单张图片超过 32 MiB');
|
|
71
|
+
total += size;
|
|
72
|
+
if (total > ${attachments_1.MAX_TOTAL_ATTACHMENT_BYTES}) throw new Error('ZCode 图片总大小超过 64 MiB');
|
|
73
|
+
return { src, title: image.closest('[data-image-thumbnail-trigger]')?.getAttribute('title') || undefined };
|
|
74
|
+
});
|
|
75
|
+
const files = new Set();
|
|
76
|
+
for (const node of content?.querySelectorAll('button[title]') || []) {
|
|
77
|
+
const key = Object.keys(node).find(key => key.startsWith('__reactFiber'));
|
|
78
|
+
// 只读取正式 Markdown 文件按钮的已解析路径,不扫描回复或工具输出中的任意路径。
|
|
79
|
+
for (let fiber = node[key]; fiber; fiber = fiber.return) {
|
|
80
|
+
const link = fiber.memoizedProps?.fileLink;
|
|
81
|
+
if (!link) continue;
|
|
82
|
+
if (link.workspaceRemoteSessionId) throw new Error('ZCode 远程工作区文件无法在本机读取');
|
|
83
|
+
if (typeof link.path !== 'string' || link.path !== node.getAttribute('title')) throw new Error('ZCode 文件链接路径已变化');
|
|
84
|
+
files.add(link.path);
|
|
85
|
+
if (files.size > ${attachments_1.MAX_ATTACHMENTS}) throw new Error('单条消息最多包含 8 个附件');
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { images, files: [...files] };
|
|
90
|
+
}`;
|
|
91
|
+
class ZcodeCdpClient {
|
|
92
|
+
socket;
|
|
93
|
+
sequence = 0;
|
|
94
|
+
pending = new Map();
|
|
95
|
+
port;
|
|
96
|
+
timeoutMs;
|
|
97
|
+
fetchImpl;
|
|
98
|
+
pendingInteraction;
|
|
99
|
+
constructor(options = {}) {
|
|
100
|
+
this.port = options.port ?? 9231;
|
|
101
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
102
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
103
|
+
}
|
|
104
|
+
connected() { return this.socket?.readyState === WebSocket.OPEN; }
|
|
105
|
+
pendingInteractionSessionId() { return this.pendingInteraction?.turn.sessionId; }
|
|
106
|
+
async connect() {
|
|
107
|
+
if (this.connected())
|
|
108
|
+
return;
|
|
109
|
+
this.close();
|
|
110
|
+
const url = `http://127.0.0.1:${this.port}/json`;
|
|
111
|
+
const signal = AbortSignal.timeout(this.timeoutMs);
|
|
112
|
+
const response = await this.fetchImpl(url, { signal }).catch(error => {
|
|
113
|
+
// 重载桌面后连接池可能残留旧连接,仅重试只读发现请求。
|
|
114
|
+
if (error.cause?.code !== "ECONNRESET")
|
|
115
|
+
throw error;
|
|
116
|
+
return this.fetchImpl(url, { signal });
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok)
|
|
119
|
+
throw new Error(`ZCode CDP /json HTTP ${response.status}`);
|
|
120
|
+
const targets = await response.json();
|
|
121
|
+
const page = Array.isArray(targets) ? targets.find(target => target.type === "page" && /zcode/i.test(`${target.title} ${target.url}`)) : undefined;
|
|
122
|
+
if (!page?.webSocketDebuggerUrl)
|
|
123
|
+
throw new Error(`未找到 ZCode 页面 target(端口 ${this.port})`);
|
|
124
|
+
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
125
|
+
this.socket = socket;
|
|
126
|
+
try {
|
|
127
|
+
await new Promise((resolve, reject) => {
|
|
128
|
+
const timer = setTimeout(() => reject(new Error("ZCode CDP WebSocket 连接超时")), this.timeoutMs);
|
|
129
|
+
const finish = (error) => { clearTimeout(timer); error ? reject(error) : resolve(); };
|
|
130
|
+
socket.onopen = () => finish();
|
|
131
|
+
socket.onerror = () => { finish(new Error("ZCode CDP WebSocket 连接失败")); if (this.socket === socket)
|
|
132
|
+
this.close(); };
|
|
133
|
+
socket.onclose = () => { finish(new Error("ZCode CDP 已断开")); if (this.socket === socket)
|
|
134
|
+
this.close(); };
|
|
135
|
+
socket.onmessage = event => {
|
|
136
|
+
let message;
|
|
137
|
+
try {
|
|
138
|
+
message = JSON.parse(String(event.data));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!message || typeof message !== "object")
|
|
144
|
+
return;
|
|
145
|
+
const pending = this.pending.get(message.id);
|
|
146
|
+
if (!pending)
|
|
147
|
+
return;
|
|
148
|
+
this.pending.delete(message.id);
|
|
149
|
+
clearTimeout(pending.timer);
|
|
150
|
+
if (message.error)
|
|
151
|
+
pending.reject(new Error(`ZCode CDP 调用失败:${message.error.message ?? message.error.code}`));
|
|
152
|
+
else
|
|
153
|
+
pending.resolve(message.result);
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.close();
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
close() {
|
|
163
|
+
const socket = this.socket;
|
|
164
|
+
this.socket = undefined;
|
|
165
|
+
for (const pending of this.pending.values()) {
|
|
166
|
+
clearTimeout(pending.timer);
|
|
167
|
+
pending.reject(new Error("ZCode CDP 已关闭"));
|
|
168
|
+
}
|
|
169
|
+
this.pending.clear();
|
|
170
|
+
socket?.close();
|
|
171
|
+
}
|
|
172
|
+
async conversationId() { return (await this.snapshot()).sessionId; }
|
|
173
|
+
async createTask() {
|
|
174
|
+
await this.waitFor(async () => (await this.snapshot()).editor, "ZCode 输入框未就绪,请等待任务加载或先登录");
|
|
175
|
+
if (!await this.clickIdle('[data-testid="task-new-button"]'))
|
|
176
|
+
throw new Error("ZCode 新建任务入口未找到");
|
|
177
|
+
await this.waitFor(async () => { const state = await this.snapshot(); return state.editor && !state.sessionId && !state.rows.length && !state.draft && !state.running; }, "ZCode 新任务状态校验失败");
|
|
178
|
+
this.pendingInteraction = undefined;
|
|
179
|
+
}
|
|
180
|
+
async activateConversation(conversationId) {
|
|
181
|
+
if (!conversationId.trim())
|
|
182
|
+
throw new Error("ZCode 会话标识不能为空");
|
|
183
|
+
if ((await this.snapshot()).sessionId === conversationId)
|
|
184
|
+
return;
|
|
185
|
+
await this.waitFor(async () => (await this.snapshot()).editor, "ZCode 输入框未就绪,请等待任务加载或先登录");
|
|
186
|
+
const selector = `[data-testid=${JSON.stringify(`task-item-${conversationId}`)}]`;
|
|
187
|
+
await this.waitFor(async () => {
|
|
188
|
+
if (await this.clickIdle(selector))
|
|
189
|
+
return true;
|
|
190
|
+
if (!await this.clickIdle('[data-testid="conversation-section"] [data-task-list-show-more="true"]'))
|
|
191
|
+
throw new Error("ZCode 未找到原任务,请先在桌面任务列表中加载该任务");
|
|
192
|
+
return false;
|
|
193
|
+
}, "ZCode 原任务列表展开超时");
|
|
194
|
+
await this.waitFor(async () => (await this.snapshot()).sessionId === conversationId, "ZCode 原任务切换未完成");
|
|
195
|
+
}
|
|
196
|
+
async direct(text, timeoutMs, expert, newTask) {
|
|
197
|
+
return (await this.complete({ task: text, timeoutMs }, expert, newTask)).text;
|
|
198
|
+
}
|
|
199
|
+
async complete(request, expert, newTask = false, expectedConversationId) {
|
|
200
|
+
let stage = "准备 ZCode 桌面任务";
|
|
201
|
+
try {
|
|
202
|
+
if (typeof request.task !== "string" || !request.task.trim())
|
|
203
|
+
throw new Error("ZCode 消息不能为空");
|
|
204
|
+
if (request.attachments?.length)
|
|
205
|
+
throw new Error("ZCode 当前仅支持文本消息");
|
|
206
|
+
if (expert)
|
|
207
|
+
throw new Error("ZCode 当前不支持 WorkBuddy 专家选择");
|
|
208
|
+
request.signal?.throwIfAborted();
|
|
209
|
+
if (this.pendingInteraction) {
|
|
210
|
+
stage = "确认 ZCode 权限选择";
|
|
211
|
+
if (this.pendingInteraction.turn.sessionId !== request.sessionId)
|
|
212
|
+
throw new Error("ZCode 正在等待另一个会话的选择");
|
|
213
|
+
return await this.continueInteraction(request, expectedConversationId);
|
|
214
|
+
}
|
|
215
|
+
let before = await this.snapshot();
|
|
216
|
+
if (!before.editor)
|
|
217
|
+
await this.waitFor(async () => { before = await this.snapshot(); return before.editor; }, "ZCode 输入框未就绪,请等待任务加载或先登录", request.signal);
|
|
218
|
+
if (newTask) {
|
|
219
|
+
await this.createTask();
|
|
220
|
+
before = await this.snapshot();
|
|
221
|
+
}
|
|
222
|
+
this.assertIdle(before);
|
|
223
|
+
if (expectedConversationId !== undefined && (before.sessionId ?? null) !== expectedConversationId)
|
|
224
|
+
throw new Error("ZCode 桌面任务已切换,与逻辑会话预期任务不符,未输入消息");
|
|
225
|
+
const beforeIds = before.rows.map(row => row.id);
|
|
226
|
+
stage = "填写 ZCode 输入框";
|
|
227
|
+
await this.evaluate(`document.querySelector(${JSON.stringify(paneSelector)}).querySelector(${JSON.stringify(editorSelector)}).focus()`);
|
|
228
|
+
await this.cdp("Input.insertText", { text: request.task });
|
|
229
|
+
const filled = await this.snapshot();
|
|
230
|
+
if (filled.draft !== request.task.trim() || filled.sessionId !== before.sessionId)
|
|
231
|
+
throw new Error("ZCode 输入内容或任务校验失败,未发送消息");
|
|
232
|
+
request.signal?.throwIfAborted();
|
|
233
|
+
stage = "向 ZCode 提交任务(尚未确认发送,请勿重复提交)";
|
|
234
|
+
await this.waitFor(async () => Boolean(await this.evaluate(`(() => { const root = document.querySelector(${JSON.stringify(paneSelector)}); const button = root?.querySelector(${JSON.stringify(sendSelector)}); return button && !button.disabled; })()`)), "ZCode 发送按钮未就绪", request.signal);
|
|
235
|
+
request.signal?.throwIfAborted();
|
|
236
|
+
const sent = await this.evaluate(`(() => {
|
|
237
|
+
const root = document.querySelector(${JSON.stringify(paneSelector)});
|
|
238
|
+
const editor = root?.querySelector(${JSON.stringify(editorSelector)});
|
|
239
|
+
const button = root?.querySelector(${JSON.stringify(sendSelector)});
|
|
240
|
+
const id = root?.querySelector(${JSON.stringify(sessionSelector)})?.getAttribute('data-session-id');
|
|
241
|
+
if (!button || button.disabled || root.closest('[data-testid="side-pane-full-view-host"]')?.getAttribute('data-running') === 'true' || root.querySelector(${JSON.stringify(busySelector)}) || root.querySelector(${JSON.stringify(permissionSelector)}) || editor?.innerText.trim() !== ${JSON.stringify(request.task.trim())} || (id && id !== 'draft' ? id : undefined) !== ${JSON.stringify(before.sessionId)}) return false;
|
|
242
|
+
button.click(); return true;
|
|
243
|
+
})()`);
|
|
244
|
+
if (!sent)
|
|
245
|
+
throw new Error("ZCode 页面或输入发生变化,未发送消息");
|
|
246
|
+
stage = "等待 ZCode 回复(任务已提交,桌面未被停止,请勿重复提交)";
|
|
247
|
+
return await this.waitForReply(request, { beforeIds, task: request.task.trim(), conversationId: before.sessionId, sessionId: request.sessionId,
|
|
248
|
+
previousError: before.error ? { id: before.errorId, text: before.error } : undefined });
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
throw new agent_1.AgentOperationError("ZCODE_TASK_FAILED", stage, error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async continueInteraction(request, expectedConversationId) {
|
|
255
|
+
const { interaction, turn } = this.pendingInteraction;
|
|
256
|
+
const state = await this.snapshot();
|
|
257
|
+
if (state.sessionId !== turn.conversationId || expectedConversationId !== undefined && (state.sessionId ?? null) !== expectedConversationId)
|
|
258
|
+
throw new Error("ZCode 桌面任务已切换,未确认权限");
|
|
259
|
+
if (!state.interaction) {
|
|
260
|
+
this.pendingInteraction = undefined;
|
|
261
|
+
return this.waitForReply(request, turn);
|
|
262
|
+
}
|
|
263
|
+
if (JSON.stringify(state.interaction) !== JSON.stringify(interaction))
|
|
264
|
+
throw new Error("ZCode 权限面板已变化,请在桌面核对,未确认权限");
|
|
265
|
+
const index = (0, cdp_client_1.interactionChoice)(request.task, interaction.options) ?? (0, cdp_client_1.interactionChoice)(request.task, interaction.labels);
|
|
266
|
+
if (index === undefined)
|
|
267
|
+
return { text: (0, cdp_client_1.formatInteraction)(interaction) };
|
|
268
|
+
// 聚焦只改变选中项;下一次 CDP 调用等 React 更新后才确认,避免误触默认选项。
|
|
269
|
+
const action = (click) => `(() => {
|
|
270
|
+
const state = ${exports.zcodeSnapshotScript};
|
|
271
|
+
if (state.error || state.draft || state.sessionId !== ${JSON.stringify(turn.conversationId)} || JSON.stringify(state.interaction) !== ${JSON.stringify(JSON.stringify(interaction))}) throw new Error('ZCode 任务、草稿或权限面板已变化,未确认权限');
|
|
272
|
+
const user = state.rows.findIndex(row => row.role === 'user' && !${JSON.stringify(turn.beforeIds)}.includes(row.id) && row.text === ${JSON.stringify(turn.task)});
|
|
273
|
+
if (user < 0 || state.rows.slice(user + 1).some(row => row.role === 'user')) throw new Error('ZCode 出现人工追加的用户消息,未确认权限');
|
|
274
|
+
const option = document.querySelector(${JSON.stringify(paneSelector)}).querySelectorAll(${JSON.stringify(permissionSelector)})[${index}];
|
|
275
|
+
if (!option || option.disabled || option.getAttribute('aria-disabled') === 'true') throw new Error('ZCode 权限选项不可用');
|
|
276
|
+
${click ? "if (option.getAttribute('aria-selected') !== 'true') throw new Error('ZCode 权限选项尚未选中'); option.click();" : "option.focus();"}
|
|
277
|
+
return true;
|
|
278
|
+
})()`;
|
|
279
|
+
request.signal?.throwIfAborted();
|
|
280
|
+
await this.evaluate(action(false));
|
|
281
|
+
request.signal?.throwIfAborted();
|
|
282
|
+
await this.evaluate(action(true));
|
|
283
|
+
this.pendingInteraction = undefined;
|
|
284
|
+
return this.waitForReply(request, turn, interaction.id);
|
|
285
|
+
}
|
|
286
|
+
async waitForReply(request, turn, confirmedId) {
|
|
287
|
+
const beforeIds = new Set(turn.beforeIds);
|
|
288
|
+
const deadline = Date.now() + (request.timeoutMs ?? 3_600_000);
|
|
289
|
+
let previous = "";
|
|
290
|
+
let previousSince = 0;
|
|
291
|
+
while (Date.now() < deadline) {
|
|
292
|
+
request.signal?.throwIfAborted();
|
|
293
|
+
await sleep(200);
|
|
294
|
+
const current = await this.snapshot();
|
|
295
|
+
request.signal?.throwIfAborted();
|
|
296
|
+
if (turn.conversationId && current.sessionId !== turn.conversationId)
|
|
297
|
+
throw new Error("ZCode 桌面任务已切换,未返回其他任务的消息");
|
|
298
|
+
turn.conversationId ??= current.sessionId;
|
|
299
|
+
if (turn.previousError && current.errorId === turn.previousError.id && current.error === turn.previousError.text)
|
|
300
|
+
continue;
|
|
301
|
+
turn.previousError = undefined;
|
|
302
|
+
if (current.error)
|
|
303
|
+
throw new Error(current.error);
|
|
304
|
+
const userIndex = current.rows.findIndex(row => row.role === "user" && !beforeIds.has(row.id) && row.text === turn.task);
|
|
305
|
+
const following = userIndex < 0 ? [] : current.rows.slice(userIndex + 1);
|
|
306
|
+
if (following.some(row => row.role === "user"))
|
|
307
|
+
throw new Error("ZCode 桌面追加了新的用户消息,未混入其他轮次回复");
|
|
308
|
+
const reply = following.filter(row => row.role === "assistant" && !beforeIds.has(row.id)).at(-1);
|
|
309
|
+
if (current.interaction) {
|
|
310
|
+
if (current.interaction.id !== confirmedId && userIndex >= 0 && turn.conversationId) {
|
|
311
|
+
this.pendingInteraction = { interaction: current.interaction, turn };
|
|
312
|
+
return { text: (0, cdp_client_1.formatInteraction)(current.interaction) };
|
|
313
|
+
}
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const completed = current.completed && reply?.finished;
|
|
317
|
+
if (!current.running && current.send && completed && reply) {
|
|
318
|
+
const artifacts = await this.evaluate(`(${exports.zcodeReplyArtifactsScript})(${JSON.stringify(current)}, ${JSON.stringify(reply.id)})`);
|
|
319
|
+
request.signal?.throwIfAborted();
|
|
320
|
+
if (!artifacts) {
|
|
321
|
+
previous = "";
|
|
322
|
+
previousSince = 0;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (reply.text || artifacts.images.length || artifacts.files.length) {
|
|
326
|
+
const signature = JSON.stringify({ text: reply.text, artifacts });
|
|
327
|
+
if (signature === previous) {
|
|
328
|
+
if (Date.now() - previousSince < 1_000)
|
|
329
|
+
continue;
|
|
330
|
+
const attachments = await this.replyFileAttachments(artifacts.files, this.replyAttachments(artifacts.images));
|
|
331
|
+
const verified = await this.evaluate(`(${exports.zcodeReplyArtifactsScript})(${JSON.stringify(current)}, ${JSON.stringify(reply.id)})`);
|
|
332
|
+
if (!verified) {
|
|
333
|
+
previous = "";
|
|
334
|
+
previousSince = 0;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (JSON.stringify(verified) !== JSON.stringify(artifacts))
|
|
338
|
+
throw new Error("ZCode 回复附件已变化,未回传其他文件");
|
|
339
|
+
request.signal?.throwIfAborted();
|
|
340
|
+
return { text: reply.text || "附件已生成。", ...(attachments.length ? { attachments } : {}) };
|
|
341
|
+
}
|
|
342
|
+
previous = signature;
|
|
343
|
+
previousSince = Date.now();
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
previous = "";
|
|
348
|
+
previousSince = 0;
|
|
349
|
+
}
|
|
350
|
+
throw new Error("ZCode 回复超时,未确认本轮任务完成;桌面任务未被停止,请勿重复提交");
|
|
351
|
+
}
|
|
352
|
+
assertIdle(state) {
|
|
353
|
+
// 旧轮次的错误横幅不阻止用户重试,本轮错误在提交后回传。
|
|
354
|
+
if (!state.editor)
|
|
355
|
+
throw new Error("ZCode 输入框未就绪,请先登录并打开主任务页面");
|
|
356
|
+
if (state.draft)
|
|
357
|
+
throw new Error("ZCode 有未发送的用户草稿,请先在桌面处理");
|
|
358
|
+
if (state.interaction)
|
|
359
|
+
throw new Error("ZCode 正在等待权限确认,请先处理原任务的选择");
|
|
360
|
+
if (state.running)
|
|
361
|
+
throw new Error("ZCode 正在生成回复,请等待当前任务结束");
|
|
362
|
+
}
|
|
363
|
+
replyAttachments(images) {
|
|
364
|
+
if (!images?.length)
|
|
365
|
+
return [];
|
|
366
|
+
if (images.length > attachments_1.MAX_ATTACHMENTS)
|
|
367
|
+
throw new Error("ZCode 回复图片数量超过 8 个");
|
|
368
|
+
let total = 0;
|
|
369
|
+
const attachments = images.map((image) => {
|
|
370
|
+
if (image.src.length > Math.ceil(attachments_1.MAX_ATTACHMENT_BYTES / 3) * 4 + 64)
|
|
371
|
+
throw new Error("ZCode 单张图片超过 32 MiB");
|
|
372
|
+
const match = /^data:(image\/(?:png|jpeg|webp));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(image.src);
|
|
373
|
+
if (!match)
|
|
374
|
+
throw new Error("ZCode 图片地址不支持");
|
|
375
|
+
const contentType = match[1].toLowerCase();
|
|
376
|
+
const size = match[2].length / 4 * 3 - (match[2].endsWith("==") ? 2 : match[2].endsWith("=") ? 1 : 0);
|
|
377
|
+
if (size > attachments_1.MAX_ATTACHMENT_BYTES)
|
|
378
|
+
throw new Error("ZCode 单张图片超过 32 MiB");
|
|
379
|
+
total += size;
|
|
380
|
+
if (total > attachments_1.MAX_TOTAL_ATTACHMENT_BYTES)
|
|
381
|
+
throw new Error("ZCode 图片总大小超过 64 MiB");
|
|
382
|
+
const data = Buffer.from(match[2], "base64");
|
|
383
|
+
if (data.toString("base64") !== match[2])
|
|
384
|
+
throw new Error("ZCode 图片编码无效");
|
|
385
|
+
const basename = image.title?.split(/[\\/]/).pop() || `zcode-${(0, node_crypto_1.randomUUID)()}.${contentType.slice(6)}`;
|
|
386
|
+
return { kind: "image", data, filename: (0, attachments_1.attachmentFilename)(basename), contentType };
|
|
387
|
+
});
|
|
388
|
+
(0, attachments_1.validateAttachments)(attachments);
|
|
389
|
+
return attachments;
|
|
390
|
+
}
|
|
391
|
+
async replyFileAttachments(paths, attachments) {
|
|
392
|
+
const mimeTypes = {
|
|
393
|
+
".txt": "text/plain", ".md": "text/markdown", ".csv": "text/csv", ".json": "application/json", ".html": "text/html", ".pdf": "application/pdf",
|
|
394
|
+
".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".zip": "application/zip",
|
|
395
|
+
".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/quicktime",
|
|
396
|
+
};
|
|
397
|
+
const digest = (data) => (0, node_crypto_1.createHash)("sha256").update(data).digest("hex");
|
|
398
|
+
const seen = new Set();
|
|
399
|
+
for (const path of paths) {
|
|
400
|
+
if (!(0, node_path_1.isAbsolute)(path) || path.startsWith("\\") || path.startsWith("//") || process.platform === "win32" && path.startsWith("/") || /[\u0000-\u001f\u007f]/.test(path))
|
|
401
|
+
throw new Error("ZCode 文件链接路径无效");
|
|
402
|
+
const key = (0, node_path_1.normalize)(path);
|
|
403
|
+
if (seen.has(key))
|
|
404
|
+
continue;
|
|
405
|
+
seen.add(key);
|
|
406
|
+
const filename = (0, attachments_1.attachmentFilename)(path);
|
|
407
|
+
let data;
|
|
408
|
+
try {
|
|
409
|
+
const before = await (0, promises_1.lstat)(path);
|
|
410
|
+
if (!before.isFile())
|
|
411
|
+
throw new Error("ZCode 附件不是普通文件");
|
|
412
|
+
const handle = await (0, promises_1.open)(path, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
|
|
413
|
+
try {
|
|
414
|
+
const info = await handle.stat();
|
|
415
|
+
if (!info.isFile())
|
|
416
|
+
throw new Error("ZCode 附件不是普通文件");
|
|
417
|
+
if (before.dev !== info.dev || before.ino > 0 && before.ino !== info.ino)
|
|
418
|
+
throw new Error("ZCode 附件路径在读取前已变化");
|
|
419
|
+
if (info.size < 1 || info.size > attachments_1.MAX_ATTACHMENT_BYTES)
|
|
420
|
+
throw new Error("ZCode 附件大小无效,单个附件不能超过 32 MiB");
|
|
421
|
+
const buffer = Buffer.alloc(info.size + 1);
|
|
422
|
+
let offset = 0;
|
|
423
|
+
while (offset < buffer.length) {
|
|
424
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
425
|
+
if (!bytesRead)
|
|
426
|
+
break;
|
|
427
|
+
offset += bytesRead;
|
|
428
|
+
}
|
|
429
|
+
const after = await handle.stat();
|
|
430
|
+
if (offset !== info.size || after.size !== info.size || after.mtimeMs !== info.mtimeMs)
|
|
431
|
+
throw new Error("ZCode 附件在读取时发生变化");
|
|
432
|
+
data = buffer.subarray(0, offset);
|
|
433
|
+
}
|
|
434
|
+
finally {
|
|
435
|
+
await handle.close();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
throw new Error(`ZCode 文件读取失败:${filename}(${error instanceof Error ? error.message : String(error)})`);
|
|
440
|
+
}
|
|
441
|
+
const imageType = (0, attachments_1.detectImageContentType)(data);
|
|
442
|
+
const contentType = imageType ?? mimeTypes[(0, node_path_1.extname)(path).toLowerCase()] ?? "application/octet-stream";
|
|
443
|
+
if (contentType === "video/mp4" && (data.length < 12 || data.subarray(4, 8).toString() !== "ftyp"))
|
|
444
|
+
throw new Error("ZCode 视频内容与类型不符");
|
|
445
|
+
const duplicate = imageType && attachments.find(item => item.kind === "image" && digest(item.data) === digest(data));
|
|
446
|
+
if (duplicate) {
|
|
447
|
+
duplicate.filename = filename;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
attachments.push({ kind: imageType ? "image" : "file", data, filename, contentType });
|
|
451
|
+
(0, attachments_1.validateAttachments)(attachments);
|
|
452
|
+
}
|
|
453
|
+
return attachments;
|
|
454
|
+
}
|
|
455
|
+
async snapshot() { return await this.evaluate(exports.zcodeSnapshotScript); }
|
|
456
|
+
async clickIdle(selector) {
|
|
457
|
+
return await this.evaluate(`(() => {
|
|
458
|
+
const state = ${exports.zcodeSnapshotScript};
|
|
459
|
+
if (!state.editor || state.draft || state.running || state.interaction) throw new Error('ZCode 有草稿、待确认权限、正在生成或页面未就绪,未切换任务');
|
|
460
|
+
const item = document.querySelector(${JSON.stringify(selector)});
|
|
461
|
+
if (!item || item.disabled || item.getAttribute('aria-disabled') === 'true') return false;
|
|
462
|
+
item.click(); return true;
|
|
463
|
+
})()`);
|
|
464
|
+
}
|
|
465
|
+
async waitFor(check, message, signal) {
|
|
466
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
467
|
+
do {
|
|
468
|
+
signal?.throwIfAborted();
|
|
469
|
+
if (await check())
|
|
470
|
+
return;
|
|
471
|
+
signal?.throwIfAborted();
|
|
472
|
+
await sleep(100);
|
|
473
|
+
} while (Date.now() < deadline);
|
|
474
|
+
throw new Error(message);
|
|
475
|
+
}
|
|
476
|
+
async cdp(method, params = {}) {
|
|
477
|
+
if (!this.connected())
|
|
478
|
+
throw new Error("ZCode CDP 尚未连接");
|
|
479
|
+
const id = ++this.sequence;
|
|
480
|
+
return new Promise((resolve, reject) => {
|
|
481
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`ZCode CDP ${method} 超时`)); }, this.timeoutMs);
|
|
482
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
483
|
+
try {
|
|
484
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
clearTimeout(timer);
|
|
488
|
+
this.pending.delete(id);
|
|
489
|
+
reject(error);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async evaluate(expression) {
|
|
494
|
+
const result = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
495
|
+
if (result.exceptionDetails)
|
|
496
|
+
throw new Error(`ZCode 页面 JS 异常:${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
|
|
497
|
+
return result.result?.value;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
exports.ZcodeCdpClient = ZcodeCdpClient;
|