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.
Files changed (152) hide show
  1. package/README.md +12 -8
  2. package/dist/src-ts/cli.js +67 -37
  3. package/dist/src-ts/control-server.js +7 -28
  4. package/dist/src-ts/core-updater.js +141 -33
  5. package/dist/src-ts/daemon-process.js +2 -1
  6. package/dist/src-ts/dsh-agent.js +7 -2
  7. package/dist/src-ts/http-agent.js +36 -0
  8. package/dist/src-ts/plugin-manager.js +185 -58
  9. package/dist/src-ts/plugins/abweb/index.js +1 -1
  10. package/dist/src-ts/plugins/abweb/public/app.js +8 -6
  11. package/dist/src-ts/plugins/abweb/public/index.html +3 -3
  12. package/dist/src-ts/plugins/aun/attachments.js +3 -21
  13. package/dist/src-ts/plugins/aun/index.js +89 -11
  14. package/dist/src-ts/plugins/aun/messages.js +1 -1
  15. package/dist/src-ts/plugins/aun/public/app.js +2 -1
  16. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  17. package/dist/src-ts/plugins/cdp-draft.js +5 -2
  18. package/dist/src-ts/plugins/chatgpt/index.js +3 -2
  19. package/dist/src-ts/plugins/claude-cli/index.js +1 -1
  20. package/dist/src-ts/plugins/desktop-launch.js +44 -3
  21. package/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
  22. package/dist/src-ts/plugins/doubao/index.js +119 -7
  23. package/dist/src-ts/plugins/doubao/native-task.js +134 -0
  24. package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  25. package/dist/src-ts/plugins/dsh/index.js +102 -23
  26. package/dist/src-ts/plugins/dsh/session-history.js +31 -3
  27. package/dist/src-ts/plugins/feishu/index.js +1 -1
  28. package/dist/src-ts/plugins/personal-assistant/index.js +29 -8
  29. package/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
  30. package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  31. package/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
  32. package/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
  33. package/dist/src-ts/plugins/qwenwork/index.js +52 -18
  34. package/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
  35. package/dist/src-ts/plugins/workbuddy/index.js +24 -12
  36. package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  37. package/dist/src-ts/plugins/zcode/index.js +97 -0
  38. package/dist/src-ts/session-management.js +27 -8
  39. package/dist/src-ts/version-manager.js +6 -3
  40. package/dist/tests-ts/abweb-model-config.test.js +44 -41
  41. package/dist/tests-ts/aun-attachments.test.js +19 -17
  42. package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
  43. package/dist/tests-ts/aun-messages.test.js +14 -0
  44. package/dist/tests-ts/aun-plugin.test.js +207 -19
  45. package/dist/tests-ts/bundled-package.test.js +1 -1
  46. package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
  47. package/dist/tests-ts/chatgpt-plugin.test.js +19 -0
  48. package/dist/tests-ts/control-server.test.js +20 -0
  49. package/dist/tests-ts/core-updater.test.js +215 -46
  50. package/dist/tests-ts/daemon-process.test.js +126 -1
  51. package/dist/tests-ts/desktop-launch.test.js +73 -0
  52. package/dist/tests-ts/doubao-native-task.test.js +237 -0
  53. package/dist/tests-ts/doubao-plugin.test.js +688 -19
  54. package/dist/tests-ts/dsh-agent.test.js +91 -19
  55. package/dist/tests-ts/dsh-cdp.test.js +148 -0
  56. package/dist/tests-ts/dsh-history.test.js +37 -0
  57. package/dist/tests-ts/feishu-plugin.test.js +1 -1
  58. package/dist/tests-ts/http-agent.test.js +18 -0
  59. package/dist/tests-ts/local-cli.test.js +29 -2
  60. package/dist/tests-ts/model-provider-cancellation.test.js +2 -0
  61. package/dist/tests-ts/native-session-management.test.js +8 -7
  62. package/dist/tests-ts/personal-assistant-coordination.test.js +16 -0
  63. package/dist/tests-ts/personal-assistant-extensions.test.js +7 -1
  64. package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
  65. package/dist/tests-ts/personal-assistant.test.js +277 -3
  66. package/dist/tests-ts/platform-integration.test.js +60 -1
  67. package/dist/tests-ts/plugin-manager.test.js +78 -6
  68. package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
  69. package/dist/tests-ts/publish-packages.test.js +1 -1
  70. package/dist/tests-ts/qwenwork-plugin.test.js +30 -0
  71. package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
  72. package/dist/tests-ts/qwenwork-sse.test.js +33 -0
  73. package/dist/tests-ts/session-access-defaults.test.js +112 -0
  74. package/dist/tests-ts/session-management.test.js +8 -8
  75. package/dist/tests-ts/version-manager.test.js +40 -0
  76. package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
  77. package/dist/tests-ts/workbuddy-plugin.test.js +356 -13
  78. package/dist/tests-ts/zcode-cdp.test.js +894 -0
  79. package/dist/tests-ts/zcode-plugin.test.js +396 -0
  80. package/package.json +1 -1
  81. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  82. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +8 -6
  83. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +7 -5
  84. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -3
  85. package/plugins/abweb/package.json +1 -1
  86. package/plugins/abweb/plugin.json +1 -1
  87. package/plugins/aun/config.schema.json +1 -1
  88. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
  89. package/plugins/aun/dist/src-ts/plugins/aun/index.js +89 -11
  90. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +1 -1
  91. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
  92. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  93. package/plugins/aun/package.json +1 -1
  94. package/plugins/aun/plugin.json +1 -1
  95. package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
  96. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +3 -2
  97. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
  98. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +1 -1
  99. package/plugins/claude-cli/package.json +1 -1
  100. package/plugins/claude-cli/plugin.json +1 -1
  101. package/plugins/doubao/config.schema.json +2 -1
  102. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
  103. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
  104. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +119 -7
  105. package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
  106. package/plugins/doubao/package.json +1 -1
  107. package/plugins/doubao/plugin.json +1 -1
  108. package/plugins/dsh/config.schema.json +5 -2
  109. package/plugins/dsh/dist/src-ts/dsh-agent.js +7 -2
  110. package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
  111. package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  112. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +102 -23
  113. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
  114. package/plugins/dsh/package.json +1 -1
  115. package/plugins/dsh/plugin.json +1 -1
  116. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
  117. package/plugins/feishu/package.json +1 -1
  118. package/plugins/feishu/plugin.json +1 -1
  119. package/plugins/personal-assistant/config.schema.json +3 -2
  120. package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
  121. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +29 -8
  122. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
  123. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  124. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
  125. package/plugins/personal-assistant/package.json +2 -2
  126. package/plugins/personal-assistant/plugin.json +1 -1
  127. package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
  128. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
  129. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
  130. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +52 -18
  131. package/plugins/qwenwork/package.json +1 -1
  132. package/plugins/qwenwork/plugin.json +1 -1
  133. package/plugins/workbuddy/config.schema.json +1 -1
  134. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
  135. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
  136. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +24 -12
  137. package/plugins/workbuddy/package.json +1 -1
  138. package/plugins/workbuddy/plugin.json +1 -1
  139. package/plugins/zcode/config.schema.json +13 -0
  140. package/plugins/zcode/dist/src-ts/agent.js +69 -0
  141. package/plugins/zcode/dist/src-ts/attachments.js +62 -0
  142. package/plugins/zcode/dist/src-ts/plugin.js +13 -0
  143. package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
  144. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
  145. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  146. package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  147. package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
  148. package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  149. package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  150. package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
  151. package/plugins/zcode/package.json +16 -0
  152. package/plugins/zcode/plugin.json +11 -0
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DshCdpClient = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const node_os_1 = require("node:os");
8
+ const attachments_1 = require("../../attachments");
9
+ const dsh_agent_1 = require("../../dsh-agent");
10
+ const stateExpression = `JSON.stringify((() => {
11
+ const content = document.querySelector('[data-conversation-content][data-conversation-session]');
12
+ const editor = document.querySelector('[data-composer-input][contenteditable="true"]');
13
+ const transcript = [...(content?.querySelectorAll('[data-chat-flow-kind="user"], [data-chat-flow-kind="assistant-step"]:not([data-chat-group-part="reasoning"])') || [])].map(node => node.getAttribute('data-chat-flow-kind') === 'user' ? node.querySelector('[data-slot="conversation.user.text.node"]')?.innerText.trim() || '' : node.innerText.trim()).filter(Boolean).join('\\n\\n');
14
+ const labels = button => [button.getAttribute('aria-label'), button.title, button.innerText].filter(Boolean).join(' ').trim();
15
+ const busy = [...document.querySelectorAll('button,[role="button"]')].some(button => /停止|stop|interrupt/i.test(labels(button)) && !button.disabled && button.getAttribute('aria-disabled') !== 'true');
16
+ const pending = document.querySelector('[data-composer-seat] [role="group"][aria-label*="Pending attachments"], [data-composer-seat] [role="group"][aria-label*="待发送附件"]');
17
+ const pendingAttachments = pending ? [...pending.querySelectorAll('[title],[alt]')].map(item => item.getAttribute('title') || item.getAttribute('alt')).filter(Boolean) : [];
18
+ return {
19
+ sessionId: content?.getAttribute('data-conversation-session') || '',
20
+ editorText: (editor?.innerText || '').replace(/[\\u200b\\ufeff]/g, '').trim(),
21
+ conversationText: transcript,
22
+ busy,
23
+ turnCount: content?.querySelectorAll('[data-turn-tail]').length || 0,
24
+ blank: content?.getAttribute('data-content-phase') === 'hero',
25
+ pendingAttachments,
26
+ };
27
+ })())`;
28
+ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
29
+ function normalize(value) { return value.replace(/[\u200b\ufeff]/g, "").replace(/\s+/g, " ").trim(); }
30
+ class DshCdpClient {
31
+ options;
32
+ port;
33
+ timeoutMs;
34
+ connectionTimeoutMs;
35
+ fetchImpl;
36
+ webSocketFactory;
37
+ socket;
38
+ sequence = 0;
39
+ pending = new Map();
40
+ constructor(options = {}) {
41
+ this.options = options;
42
+ this.port = options.port ?? 9226;
43
+ this.timeoutMs = options.timeoutMs ?? 240_000;
44
+ this.connectionTimeoutMs = options.connectionTimeoutMs ?? 10_000;
45
+ this.fetchImpl = options.fetchImpl ?? fetch;
46
+ this.webSocketFactory = options.webSocketFactory ?? (url => new WebSocket(url));
47
+ }
48
+ connected() { return this.socket?.readyState === 1; }
49
+ async connect() {
50
+ if (this.connected())
51
+ return;
52
+ this.close();
53
+ const response = await this.fetchImpl(`http://127.0.0.1:${this.port}/json`, { signal: AbortSignal.timeout(this.connectionTimeoutMs) });
54
+ if (!response.ok)
55
+ throw new Error(`DSH CDP /json HTTP ${response.status}`);
56
+ const targets = await response.json();
57
+ const page = targets.find(item => item.type === "page" && item.url?.startsWith("dsh-app://app/") && item.webSocketDebuggerUrl);
58
+ if (!page?.webSocketDebuggerUrl)
59
+ throw new Error(`未找到 DSH 桌面会话页(CDP ${this.port})`);
60
+ const socket = this.webSocketFactory(page.webSocketDebuggerUrl);
61
+ this.socket = socket;
62
+ let opened = false;
63
+ try {
64
+ await new Promise((resolve, reject) => {
65
+ const timer = setTimeout(() => reject(new Error("DSH CDP WebSocket 连接超时")), this.connectionTimeoutMs);
66
+ socket.onopen = () => { opened = true; clearTimeout(timer); resolve(); };
67
+ socket.onerror = () => { clearTimeout(timer); reject(new Error("DSH CDP WebSocket 连接失败")); };
68
+ socket.onclose = () => {
69
+ if (this.socket === socket)
70
+ this.socket = undefined;
71
+ this.rejectPending(new Error("DSH CDP WebSocket 已关闭"));
72
+ if (!opened) {
73
+ clearTimeout(timer);
74
+ reject(new Error("DSH CDP WebSocket 已关闭"));
75
+ }
76
+ };
77
+ socket.onerror = () => {
78
+ this.rejectPending(new Error("DSH CDP WebSocket 连接错误"));
79
+ if (!opened) {
80
+ clearTimeout(timer);
81
+ reject(new Error("DSH CDP WebSocket 连接失败"));
82
+ }
83
+ };
84
+ socket.onmessage = event => this.onMessage(String(event.data));
85
+ });
86
+ await this.cdp("Runtime.enable");
87
+ await this.waitForState(state => Boolean(state.sessionId), "DSH 桌面会话输入框未就绪", this.connectionTimeoutMs);
88
+ }
89
+ catch (error) {
90
+ this.close();
91
+ throw error;
92
+ }
93
+ }
94
+ async activeSession() { return (await this.state()).sessionId; }
95
+ async createSession() {
96
+ const before = await this.state();
97
+ this.assertIdle(before, "新建会话");
98
+ if (before.blank && before.turnCount === 0)
99
+ return before.sessionId;
100
+ const clicked = await this.evaluate(`(() => {
101
+ const labels = /^(?:新建会话|新会话|new session|new chat)$/i;
102
+ const button = [...document.querySelectorAll('button,[role="button"]')].find(item => {
103
+ const label = [item.getAttribute('aria-label'), item.title, item.innerText].filter(Boolean).join(' ').trim();
104
+ return labels.test(label) && !item.disabled && item.getAttribute('aria-disabled') !== 'true';
105
+ });
106
+ if (!button) return false;
107
+ button.click(); return true;
108
+ })()`);
109
+ if (clicked !== true)
110
+ throw new Error("DSH 桌面未找到可用的新建会话入口");
111
+ const state = await this.waitForState(value => value.sessionId !== before.sessionId && value.blank && !value.busy && !value.editorText && value.turnCount === 0 && !value.pendingAttachments.length, "DSH 新会话状态未确认");
112
+ if (!state.sessionId)
113
+ throw new Error("DSH 未返回真实新会话标识");
114
+ return state.sessionId;
115
+ }
116
+ async activateSession(sessionId) {
117
+ if (!sessionId.trim())
118
+ throw new Error("DSH 会话标识不能为空");
119
+ const before = await this.state();
120
+ if (before.sessionId === sessionId)
121
+ return;
122
+ this.assertIdle(before, "切换会话");
123
+ const clicked = await this.evaluate(`(() => {
124
+ const id = ${JSON.stringify(sessionId)};
125
+ const item = [...document.querySelectorAll('[data-row-key],[data-session-id],[data-session],[data-conversation-session]')]
126
+ .find(node => node.getAttribute('data-row-key') === 'session:' + id
127
+ || ['data-session-id', 'data-session', 'data-conversation-session'].some(key => node.getAttribute(key) === id));
128
+ const button = item?.matches('button,[role="button"],[role="treeitem"],a') ? item : item?.closest('button,[role="button"],[role="treeitem"],a');
129
+ if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false;
130
+ button.click(); return true;
131
+ })()`);
132
+ if (clicked !== true)
133
+ throw new Error(`DSH 桌面未找到可切换的会话 ${sessionId},未发送消息`);
134
+ await this.waitForState(state => state.sessionId === sessionId, "DSH 会话切换未确认,未发送消息");
135
+ }
136
+ async complete(request) {
137
+ if (!request.task.trim())
138
+ throw new Error("DSH 消息内容不能为空");
139
+ (0, attachments_1.validateAttachments)(request.attachments);
140
+ const before = await this.state();
141
+ if (!before.sessionId)
142
+ throw new Error("DSH 当前没有可用会话");
143
+ this.assertIdle(before, "发送消息");
144
+ const deliveryBefore = request.attachmentOutput ? await this.readDeliveryState(before.sessionId) : undefined;
145
+ const workDirectory = request.attachments?.length ? await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-dsh-cdp-")) : undefined;
146
+ let outputDirectory;
147
+ let outputCwd;
148
+ let failed = true;
149
+ try {
150
+ if (deliveryBefore) {
151
+ if (!(0, node_path_1.isAbsolute)(deliveryBefore.cwd))
152
+ throw new Error("DSH 工作目录不是绝对路径,无法安全回传文件");
153
+ const cwd = await (0, promises_1.realpath)(deliveryBefore.cwd);
154
+ const info = await (0, promises_1.lstat)(cwd);
155
+ if (!info.isDirectory() || info.isSymbolicLink())
156
+ throw new Error("DSH 工作目录不是受信任的目录");
157
+ outputCwd = cwd;
158
+ outputDirectory = (0, node_path_1.join)(cwd, `.agent-bridge-dsh-${(0, node_crypto_1.randomUUID)()}`);
159
+ await (0, promises_1.mkdir)(outputDirectory, { mode: 0o700 });
160
+ const outputRelative = (0, node_path_1.relative)(cwd, await (0, promises_1.realpath)(outputDirectory));
161
+ if (outputRelative === ".." || outputRelative.startsWith(`..${node_path_1.sep}`) || (0, node_path_1.isAbsolute)(outputRelative))
162
+ throw new Error("DSH 回传目录超出工作目录");
163
+ }
164
+ const filenames = [];
165
+ if (request.attachments?.length && workDirectory) {
166
+ const paths = [];
167
+ for (const [index, item] of request.attachments.entries()) {
168
+ const filename = (0, attachments_1.attachmentFilename)(item.filename);
169
+ filenames.push(filename);
170
+ const directory = (0, node_path_1.join)(workDirectory, String(index));
171
+ await (0, promises_1.mkdir)(directory, { mode: 0o700 });
172
+ const path = (0, node_path_1.join)(directory, filename);
173
+ await (0, promises_1.writeFile)(path, item.data, { mode: 0o600, flag: "wx" });
174
+ paths.push(path);
175
+ }
176
+ await this.setFiles(paths);
177
+ await this.waitForState(state => filenames.every(filename => state.pendingAttachments.includes(filename)), "DSH 未确认收到上传附件");
178
+ }
179
+ let task = request.task;
180
+ if (filenames.length)
181
+ task += `\n\n请读取本条已上传附件:${filenames.join("、")}。`;
182
+ if (outputDirectory)
183
+ task += `\n\n如需回传文件,将文件写入 ${outputDirectory} 并调用 present 声明。`;
184
+ await this.evaluate(`(() => { const editor = document.querySelector('[data-composer-input][contenteditable="true"]'); if (!editor || editor.innerText.trim()) return false; editor.focus(); return document.activeElement === editor; })()`);
185
+ await this.cdp("Input.insertText", { text: task });
186
+ const entered = await this.state();
187
+ if (normalize(entered.editorText) !== normalize(task))
188
+ throw new Error("DSH 输入框内容校验失败,未发送消息");
189
+ const turnCount = entered.turnCount;
190
+ await this.cdp("Input.dispatchKeyEvent", { type: "keyDown", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
191
+ await this.cdp("Input.dispatchKeyEvent", { type: "keyUp", key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 });
192
+ const startedAt = Date.now();
193
+ let stable = 0;
194
+ let previous = "";
195
+ let observed = "";
196
+ while (Date.now() - startedAt < (request.timeoutMs ?? this.timeoutMs)) {
197
+ await sleep(600);
198
+ const state = await this.state();
199
+ if (state.sessionId !== before.sessionId)
200
+ throw new Error("DSH 发送后会话发生变化,结果未确认且未重发");
201
+ if (state.conversationText.includes(task))
202
+ observed = state.conversationText;
203
+ const signature = `${state.turnCount}:${state.busy}:${state.conversationText}`;
204
+ stable = !state.busy && state.turnCount > turnCount && signature === previous ? stable + 1 : 0;
205
+ previous = signature;
206
+ if (stable >= 2 && observed) {
207
+ const userIndex = observed.lastIndexOf(task);
208
+ const reply = userIndex >= 0 ? observed.slice(userIndex + task.length).trim() : "";
209
+ const presented = request.attachmentOutput && deliveryBefore
210
+ ? (await this.readDeliveryState(before.sessionId)).presented.filter(item => item.sequence > deliveryBefore.sequence).flatMap(item => item.paths)
211
+ : [];
212
+ const attachments = outputDirectory && presented.length
213
+ ? await (0, dsh_agent_1.readPresentedFiles)(outputDirectory, presented, outputCwd, request.attachments ?? [])
214
+ : [];
215
+ if (!reply && !attachments.length)
216
+ throw new Error("DSH 本轮历史中没有可确认的回复内容");
217
+ failed = false;
218
+ return { text: reply, ...(attachments.length ? { attachments } : {}) };
219
+ }
220
+ }
221
+ throw new Error(`DSH 桌面回复超时;任务已提交但未确认完成,未重发(会话 ${before.sessionId})`);
222
+ }
223
+ finally {
224
+ if (workDirectory)
225
+ await (0, promises_1.rm)(workDirectory, { recursive: true, force: true });
226
+ if (outputDirectory) {
227
+ try {
228
+ await (0, promises_1.rm)(outputDirectory, { recursive: true, force: true });
229
+ }
230
+ catch (error) {
231
+ if (!failed)
232
+ throw error;
233
+ }
234
+ }
235
+ }
236
+ }
237
+ close() {
238
+ const socket = this.socket;
239
+ this.socket = undefined;
240
+ socket?.close();
241
+ this.rejectPending(new Error("DSH CDP 已关闭"));
242
+ }
243
+ async setFiles(files) {
244
+ const document = await this.cdp("DOM.getDocument", { depth: -1, pierce: true });
245
+ const result = await this.cdp("DOM.querySelector", { nodeId: document.root.nodeId, selector: 'input[type="file"]' });
246
+ if (!result.nodeId)
247
+ throw new Error("DSH 桌面原生附件输入控件未找到");
248
+ await this.cdp("DOM.setFileInputFiles", { nodeId: result.nodeId, files });
249
+ }
250
+ assertIdle(state, action) {
251
+ if (state.busy)
252
+ throw new Error(`DSH 正在生成回复,不能${action}`);
253
+ if (state.editorText || state.pendingAttachments.length)
254
+ throw new Error(`DSH 输入框有未发送内容或附件,不能${action}`);
255
+ }
256
+ async state() {
257
+ const value = await this.evaluate(stateExpression);
258
+ if (typeof value !== "string")
259
+ throw new Error("DSH 桌面状态响应无效");
260
+ const state = JSON.parse(value);
261
+ if (!state || typeof state.sessionId !== "string" || typeof state.editorText !== "string" || typeof state.conversationText !== "string" || typeof state.busy !== "boolean" || !Number.isInteger(state.turnCount) || typeof state.blank !== "boolean" || !Array.isArray(state.pendingAttachments))
262
+ throw new Error("DSH 桌面页面结构不符合预期");
263
+ return state;
264
+ }
265
+ async waitForState(predicate, message, timeoutMs = this.timeoutMs) {
266
+ const deadline = Date.now() + timeoutMs;
267
+ let lastError;
268
+ while (Date.now() < deadline) {
269
+ try {
270
+ const state = await this.state();
271
+ if (predicate(state))
272
+ return state;
273
+ }
274
+ catch (error) {
275
+ lastError = error;
276
+ }
277
+ await sleep(250);
278
+ }
279
+ throw new Error(message, lastError ? { cause: lastError } : undefined);
280
+ }
281
+ async evaluate(expression) {
282
+ const result = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true, userGesture: true });
283
+ if (result.exceptionDetails)
284
+ throw new Error(`DSH CDP 页面执行失败:${result.exceptionDetails.text ?? result.exceptionDetails.exception?.description ?? "未知错误"}`);
285
+ return result.result?.value;
286
+ }
287
+ async cdp(method, params = {}) {
288
+ const socket = this.socket;
289
+ if (!socket || socket.readyState !== 1)
290
+ throw new Error("DSH CDP 未连接");
291
+ const id = ++this.sequence;
292
+ const promise = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
293
+ socket.send(JSON.stringify({ id, method, params }));
294
+ let timer;
295
+ try {
296
+ const timeout = new Promise((_, reject) => { timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`DSH CDP ${method} 超时`)); }, this.connectionTimeoutMs); });
297
+ const response = await Promise.race([promise, timeout]);
298
+ if (response.error)
299
+ throw new Error(response.error.message ?? `DSH CDP ${method} 失败`);
300
+ return response.result ?? {};
301
+ }
302
+ finally {
303
+ if (timer)
304
+ clearTimeout(timer);
305
+ }
306
+ }
307
+ onMessage(raw) {
308
+ let message;
309
+ try {
310
+ message = JSON.parse(raw);
311
+ }
312
+ catch {
313
+ return;
314
+ }
315
+ if (typeof message.id !== "number")
316
+ return;
317
+ const pending = this.pending.get(message.id);
318
+ if (!pending)
319
+ return;
320
+ this.pending.delete(message.id);
321
+ pending.resolve(message);
322
+ }
323
+ rejectPending(error) {
324
+ for (const pending of this.pending.values())
325
+ pending.reject(error);
326
+ this.pending.clear();
327
+ }
328
+ async readDeliveryState(sessionId) {
329
+ if (!this.options.readDeliveryState)
330
+ throw new Error("DSH 附件回传需要可读取的持久化会话日志");
331
+ return this.options.readDeliveryState(sessionId);
332
+ }
333
+ }
334
+ exports.DshCdpClient = DshCdpClient;
@@ -6,11 +6,13 @@ const node_path_1 = require("node:path");
6
6
  const agent_1 = require("../../agent");
7
7
  const dsh_agent_1 = require("../../dsh-agent");
8
8
  const dsh_paths_1 = require("../../dsh-paths");
9
+ const plugin_1 = require("../../plugin");
9
10
  const openai_adapter_1 = require("../qwenwork/openai-adapter");
10
11
  const session_history_1 = require("./session-history");
12
+ const cdp_client_1 = require("./cdp-client");
11
13
  exports.dshManifest = {
12
14
  id: "dsh",
13
- version: "1.0.12",
15
+ version: "1.0.15",
14
16
  displayName: "DeepSeek Harness",
15
17
  entry: "dist/index.js",
16
18
  apiVersion: "1",
@@ -19,21 +21,30 @@ exports.dshManifest = {
19
21
  configSchema: "config.schema.json",
20
22
  };
21
23
  class DshPlugin {
24
+ options;
22
25
  manifest = exports.dshManifest;
23
26
  modelProvider = { complete: (request) => this.completeModelProvider(request) };
24
27
  attachmentInput = true;
25
28
  sessionOperations = ["new"];
26
29
  sessionHistory;
30
+ client;
27
31
  agent;
32
+ modelConfig = {};
33
+ modelSessions = new Map();
34
+ modelSessionsPath;
28
35
  status = "failed";
29
36
  message = "插件尚未启动";
30
37
  adapter;
31
38
  activeRequests = 0;
39
+ port = 9226;
32
40
  queue = Promise.resolve();
33
- modelSessions = new Map();
34
- modelSessionsPath;
41
+ pendingSessions = new Set();
35
42
  unregister = [];
43
+ constructor(options = {}) {
44
+ this.options = options;
45
+ }
36
46
  async activate(context) {
47
+ this.modelConfig = context.config;
37
48
  this.modelSessions.clear();
38
49
  this.modelSessionsPath = context.dataDir ? (0, node_path_1.join)(context.dataDir, "model-provider-sessions.json") : undefined;
39
50
  if (this.modelSessionsPath) {
@@ -50,18 +61,34 @@ class DshPlugin {
50
61
  throw error;
51
62
  }
52
63
  }
53
- const configured = context.config.patchPath;
54
- const patchPath = typeof configured === "string" && configured.trim()
55
- ? configured
56
- : (0, node_path_1.resolve)(__dirname, "..", "..", "headless-account.patch.yml");
57
- const paths = await (0, dsh_paths_1.resolveDshPaths)(patchPath, typeof context.config.executablePath === "string" ? context.config.executablePath : undefined);
58
- this.agent = new dsh_agent_1.DshAgent({ patchPath, paths });
59
- context.logger?.info?.("DSH 客户端路径已确认", { executablePath: paths.executable });
64
+ this.port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "dsh", "DSH_CDP_PORT", 9226);
65
+ const sessionsRoot = typeof context.config.sessionsRoot === "string" ? context.config.sessionsRoot : undefined;
60
66
  this.sessionHistory = new session_history_1.DshSessionHistoryProvider({
61
- sessionsRoot: typeof context.config.sessionsRoot === "string" ? context.config.sessionsRoot : undefined,
67
+ ...(sessionsRoot ? { sessionsRoot } : {}),
62
68
  unavailableReason: () => this.status !== "ready" ? "DSH 插件尚未就绪" : this.activeRequests ? "DSH 正在处理任务,暂不读取持久化历史" : "",
63
69
  });
64
- this.adapter = new openai_adapter_1.OpenAIAdapter((prompt) => this.handle({ task: prompt, timeoutMs: 240_000 }).then((result) => result.text), { model: "dsh", prefix: "/dsh/v1", onExchange: (direction, text, peer, agentAid) => context.logger?.info?.("DSH 通信", { direction, text, peer, agentAid }) });
70
+ this.client = this.options.client ?? new cdp_client_1.DshCdpClient({
71
+ port: this.port,
72
+ timeoutMs: typeof context.config.timeoutMs === "number" && context.config.timeoutMs > 0 ? context.config.timeoutMs : undefined,
73
+ connectionTimeoutMs: typeof context.config.startupTimeoutMs === "number" && context.config.startupTimeoutMs > 0 ? context.config.startupTimeoutMs : undefined,
74
+ readDeliveryState: sessionId => this.sessionHistory.readDeliveryState(sessionId),
75
+ });
76
+ this.adapter = new openai_adapter_1.OpenAIAdapter((prompt) => this.handle({ task: prompt, timeoutMs: 240_000 }).then(result => result.text), {
77
+ model: "dsh", prefix: "/dsh/v1",
78
+ onExchange: (direction, text, peer, agentAid) => context.logger?.info?.("DSH 通信", { direction, text, peer, agentAid }),
79
+ });
80
+ try {
81
+ await this.client.connect();
82
+ this.status = "ready";
83
+ this.message = "";
84
+ }
85
+ catch (error) {
86
+ this.status = "failed";
87
+ this.message = error instanceof Error ? error.message : String(error);
88
+ context.logger?.error?.(this.message);
89
+ this.client.close();
90
+ throw error;
91
+ }
65
92
  if (context.http) {
66
93
  for (const [method, path] of [["GET", "/dsh/v1/models"], ["POST", "/dsh/v1/chat/completions"], ["OPTIONS", "/dsh/v1/chat/completions"]]) {
67
94
  const dispose = context.http.register({ method, path, handler: (req, res) => this.adapter.handle(req, res) });
@@ -69,38 +96,75 @@ class DshPlugin {
69
96
  this.unregister.push(dispose);
70
97
  }
71
98
  }
72
- this.status = "ready";
73
- this.message = "";
74
- context.logger?.info?.("dsh plugin activated");
99
+ context.logger?.info?.("DSH 桌面 CDP 已连接", { port: this.port });
75
100
  }
76
101
  async deactivate() {
102
+ this.status = "failed";
103
+ this.message = "插件正在停止";
77
104
  await this.queue;
78
105
  for (const dispose of this.unregister.splice(0))
79
106
  dispose();
80
107
  await this.adapter?.close();
81
108
  this.adapter = undefined;
109
+ this.client?.close();
110
+ this.client = undefined;
82
111
  this.agent = undefined;
83
112
  this.sessionHistory = undefined;
113
+ this.pendingSessions.clear();
84
114
  this.status = "failed";
85
115
  this.message = "插件已停止";
86
116
  }
87
117
  async health() {
88
- return { status: this.status, ...(this.message ? { message: this.message } : {}), details: { adapter: "DshAgent" } };
118
+ const connected = this.client?.connected() === true;
119
+ return {
120
+ status: connected ? "ready" : this.status === "failed" ? "failed" : "degraded",
121
+ ...(!connected ? { message: this.message || "DSH CDP 已断开" } : {}),
122
+ details: { transport: "cdp", port: this.port, connected },
123
+ };
124
+ }
125
+ async controlSession(request) {
126
+ if (request.operation !== "new")
127
+ throw new Error(`DSH 不支持会话操作:${String(request.operation)}`);
128
+ if (!request.sessionId?.trim())
129
+ throw new Error("DSH 新会话缺少 sessionId");
130
+ return this.enqueue(async () => {
131
+ this.assertConnected();
132
+ const sessionId = await this.client.createSession();
133
+ this.pendingSessions.add(sessionId);
134
+ return { text: "DSH 已创建新会话", sessionId };
135
+ });
89
136
  }
90
137
  async handle(request) {
91
- if (!this.agent || this.status !== "ready")
92
- throw new Error("dsh 插件未就绪");
93
- const run = this.queue.then(async () => {
138
+ return this.enqueue(async () => {
139
+ this.assertConnected();
94
140
  this.activeRequests++;
95
141
  try {
96
- return await this.agent.send(request);
142
+ const current = await this.client.activeSession();
143
+ let sessionId = request.sessionId?.trim();
144
+ if (request.newSession) {
145
+ if (sessionId && (this.pendingSessions.has(sessionId) || current === sessionId)) {
146
+ if (current !== sessionId)
147
+ await this.client.activateSession(sessionId);
148
+ this.pendingSessions.delete(sessionId);
149
+ }
150
+ else {
151
+ sessionId = await this.client.createSession();
152
+ }
153
+ }
154
+ else if (sessionId) {
155
+ if (current !== sessionId)
156
+ await this.client.activateSession(sessionId);
157
+ }
158
+ else {
159
+ sessionId = await this.client.createSession();
160
+ }
161
+ const response = await this.client.complete(request);
162
+ return { ...response, sessionId };
97
163
  }
98
164
  finally {
99
165
  this.activeRequests--;
100
166
  }
101
167
  });
102
- this.queue = run.then(() => undefined, () => undefined);
103
- return run;
104
168
  }
105
169
  async completeModelProvider(request) {
106
170
  let stage = "DSH 模型排队(queue)";
@@ -109,8 +173,14 @@ class DshPlugin {
109
173
  const run = this.queue.then(async () => {
110
174
  request.signal.throwIfAborted();
111
175
  stage = "连接 DSH 模型(connect)";
112
- if (!this.agent || this.status !== "ready")
176
+ if (this.status !== "ready")
113
177
  throw new Error("DSH 插件未就绪");
178
+ if (!this.agent) {
179
+ const configured = this.modelConfig.patchPath;
180
+ const patchPath = typeof configured === "string" && configured.trim() ? configured : (0, node_path_1.resolve)(__dirname, "..", "..", "headless-account.patch.yml");
181
+ const paths = await (0, dsh_paths_1.resolveDshPaths)(patchPath, typeof this.modelConfig.executablePath === "string" ? this.modelConfig.executablePath : undefined);
182
+ this.agent = new dsh_agent_1.DshAgent({ patchPath, paths });
183
+ }
114
184
  this.activeRequests++;
115
185
  try {
116
186
  const sessionId = this.modelSessions.get(request.sessionId);
@@ -147,6 +217,15 @@ class DshPlugin {
147
217
  throw new agent_1.AgentOperationError("DSH_MODEL_FAILED", stage, error);
148
218
  }
149
219
  }
220
+ enqueue(work) {
221
+ const run = this.queue.then(work);
222
+ this.queue = run.then(() => undefined, () => undefined);
223
+ return run;
224
+ }
225
+ assertConnected() {
226
+ if (!this.client || this.status !== "ready" || !this.client.connected())
227
+ throw new Error(this.message || `DSH CDP ${this.port} 未连接`);
228
+ }
150
229
  }
151
230
  exports.DshPlugin = DshPlugin;
152
231
  exports.default = DshPlugin;
@@ -16,7 +16,7 @@ const KNOWN_EVENTS = new Set([
16
16
  "agent-preset/selected", "agent/inbox/spliced", "approval/asked", "approval/decided", "approval/policy", "assistant/attempt", "assistant/message",
17
17
  "command/done", "command/run", "compaction/end", "compaction/prune", "compaction/start", "compaction/summary", "deliverables/presented",
18
18
  "developer/message", "feedback/message-delete", "feedback/message-put", "feedback/record", "goal/change", "hook/invoked", "hook/result",
19
- "image/offload", "llm/retry", "llm/retry-started", "model/selection", "permission/preset", "plan/mode", "request/context", "request/header",
19
+ "image/offload", "job/registered", "job/settled", "llm/retry", "llm/retry-started", "model/selection", "permission/preset", "plan/mode", "request/context", "request/header",
20
20
  "sandbox/mode", "schedule/change", "session-log-deepseek/delivery-accepted", "session/end-seed", "session/title", "session/title-llm-request",
21
21
  "step/end", "step/start", "subagent/catalog", "subagent/descriptor", "subagent/model-selection-policy", "system/message", "team/member",
22
22
  "team/message/delivered", "team/message/queued", "team/task", "todo/write", "tool-workflow/agent-end", "tool-workflow/agent-start",
@@ -134,6 +134,21 @@ class DshSessionHistoryProvider {
134
134
  readAt: new Date().toISOString(),
135
135
  };
136
136
  }
137
+ async readDeliveryState(sessionId) {
138
+ if (typeof sessionId !== "string" || !sessionId.trim() || sessionId.length > 512)
139
+ throw new Error("DSH 会话标识无效");
140
+ const root = await this.realRoot();
141
+ if (typeof root !== "string")
142
+ throw new Error(root.reason);
143
+ const discovered = await this.discover(root);
144
+ const entry = discovered.entries.find(item => item.id === sessionId);
145
+ if (!entry)
146
+ throw new Error("DSH 会话尚无可读取的持久化日志");
147
+ const parsed = await this.readEntry(root, entry);
148
+ if (!parsed.complete || !parsed.cwd)
149
+ throw new Error(parsed.reasons.join(";") || "DSH 会话工作目录无法验证");
150
+ return { cwd: parsed.cwd, sequence: parsed.sequence, presented: parsed.presented };
151
+ }
137
152
  async realRoot() {
138
153
  try {
139
154
  const resolved = await (0, promises_1.realpath)(this.root);
@@ -251,7 +266,7 @@ class DshSessionHistoryProvider {
251
266
  }
252
267
  async readEntry(root, entry) {
253
268
  const reasons = [];
254
- const fail = (reason) => ({ session: { id: entry.id, title: entry.id }, turns: [], complete: false, reasons: [...new Set([...reasons, reason])] });
269
+ const fail = (reason) => ({ session: { id: entry.id, title: entry.id }, turns: [], complete: false, reasons: [...new Set([...reasons, reason])], sequence: -1, presented: [] });
255
270
  try {
256
271
  if (!await this.inside(root, entry.path))
257
272
  return fail("DSH 会话日志路径越界或为符号链接");
@@ -285,7 +300,9 @@ class DshSessionHistoryProvider {
285
300
  if (header.type !== "session" || header.version !== 4 || header.id !== entry.id || !validTime(header.createdAt) || typeof header.isSeeded !== "boolean" || !Number.isSafeInteger(header.delegationDepth) || header.delegationDepth < 0)
286
301
  return fail("DSH 会话头格式或身份无法验证");
287
302
  const turns = [];
303
+ const presented = [];
288
304
  let complete = true;
305
+ let sequence = -1;
289
306
  for (let index = 1; index < rows.length; index++) {
290
307
  let event;
291
308
  try {
@@ -296,11 +313,22 @@ class DshSessionHistoryProvider {
296
313
  }
297
314
  if (!event || event.seq !== index - 1 || typeof event.type !== "string" || !validTime(event.time) || typeof event.data !== "object" || event.data === null)
298
315
  return fail("DSH 会话事件序号、时间或结构无法验证");
316
+ sequence = event.seq;
299
317
  if (!KNOWN_EVENTS.has(event.type) && event.ignorable !== true) {
300
318
  complete = false;
301
319
  reasons.push(`存在未知且不可安全忽略的 DSH 事件:${event.type}`);
302
320
  continue;
303
321
  }
322
+ if (event.type === "deliverables/presented") {
323
+ const files = event.data.files;
324
+ if (!Array.isArray(files) || files.some((file) => !file || typeof file.path !== "string" || !file.path.trim())) {
325
+ complete = false;
326
+ reasons.push("DSH present 事件缺少有效文件路径");
327
+ continue;
328
+ }
329
+ presented.push({ sequence, paths: files.map((file) => file.path) });
330
+ continue;
331
+ }
304
332
  if (event.type !== "user/message" && event.type !== "assistant/message")
305
333
  continue;
306
334
  const declaredRole = event.type === "user/message" ? event.data.role : event.data.message?.role;
@@ -335,7 +363,7 @@ class DshSessionHistoryProvider {
335
363
  if (message)
336
364
  turns.push({ id: `${entry.id}:${event.seq}`, role: event.type === "user/message" ? "user" : "assistant", text: message, time: new Date(event.time).toISOString() });
337
365
  }
338
- return { session: { id: entry.id, title: entry.id, updatedAt: new Date(before.mtimeMs).toISOString() }, turns, complete, reasons: [...new Set(reasons)] };
366
+ return { session: { id: entry.id, title: entry.id, updatedAt: new Date(before.mtimeMs).toISOString() }, turns, complete, reasons: [...new Set(reasons)], ...(typeof header.cwd === "string" ? { cwd: header.cwd } : {}), sequence, presented };
339
367
  }
340
368
  catch (error) {
341
369
  return fail(error instanceof Error && /maximum output length|larger than/.test(error.message) ? "DSH 解压日志超过读取上限" : "DSH 会话日志无法按官方 v4 格式完整解析");
@@ -5,7 +5,7 @@ const promises_1 = require("node:fs/promises");
5
5
  const node_path_1 = require("node:path");
6
6
  const qrcode_1 = require("qrcode");
7
7
  const client_1 = require("./client");
8
- const manifest = { id: "feishu", version: "1.0.7", type: "channel", displayName: "飞书通道", entry: "dist/src-ts/plugins/feishu/index.js", apiVersion: "1", process: "in-process", capabilities: ["channel", "channel.bot.setup", "channel.receive", "channel.send", "health", "cli"], configSchema: "config.schema.json", managementPage: "/abweb/channels/feishu" };
8
+ const manifest = { id: "feishu", version: "1.0.8", type: "channel", displayName: "飞书通道", entry: "dist/src-ts/plugins/feishu/index.js", apiVersion: "1", process: "in-process", capabilities: ["channel", "channel.bot.setup", "channel.receive", "channel.send", "health", "cli"], configSchema: "config.schema.json", managementPage: "/abweb/channels/feishu" };
9
9
  const newSessionCommand = /^\/new(?:[ \t]+[^\r\n]+)?$/;
10
10
  const commands = [
11
11
  { name: "status", summary: "查看飞书通道状态", usage: "mab feishu status" },