mu-agent-bridge 1.0.34 → 1.0.39

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 (89) hide show
  1. package/README.md +4 -2
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/control-server.js +48 -7
  4. package/dist/src-ts/dsh-agent.js +141 -46
  5. package/dist/src-ts/headless-account.patch.yml +8 -1
  6. package/dist/src-ts/plugin-manager.js +69 -3
  7. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  8. package/dist/src-ts/plugins/aun/index.js +5 -6
  9. package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  10. package/dist/src-ts/plugins/chatgpt/index.js +127 -7
  11. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  12. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  13. package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  14. package/dist/src-ts/plugins/doubao/index.js +114 -25
  15. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  16. package/dist/src-ts/plugins/dsh/index.js +18 -2
  17. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  18. package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  19. package/dist/src-ts/plugins/qwenwork/index.js +42 -3
  20. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  21. package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  22. package/dist/src-ts/plugins/workbuddy/index.js +23 -3
  23. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  24. package/dist/src-ts/session-archive.js +509 -0
  25. package/dist/src-ts/session-history.js +2 -0
  26. package/dist/src-ts/session-management.js +665 -0
  27. package/dist/src-ts/session-summary.js +171 -0
  28. package/dist/tests-ts/aun-attachments.test.js +41 -3
  29. package/dist/tests-ts/aun-plugin.test.js +28 -5
  30. package/dist/tests-ts/bundled-package.test.js +3 -1
  31. package/dist/tests-ts/channel-sessions.test.js +255 -2
  32. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  33. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  34. package/dist/tests-ts/control-server.test.js +39 -0
  35. package/dist/tests-ts/desktop-launch.test.js +41 -0
  36. package/dist/tests-ts/doubao-history.test.js +172 -0
  37. package/dist/tests-ts/doubao-plugin.test.js +549 -2
  38. package/dist/tests-ts/dsh-agent.test.js +199 -0
  39. package/dist/tests-ts/dsh-history.test.js +165 -0
  40. package/dist/tests-ts/native-session-management.test.js +195 -0
  41. package/dist/tests-ts/plugin-manager.test.js +354 -4
  42. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  43. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  44. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  45. package/dist/tests-ts/session-archive.test.js +422 -0
  46. package/dist/tests-ts/session-management.test.js +323 -0
  47. package/dist/tests-ts/session-summary.test.js +199 -0
  48. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  49. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  50. package/package.json +1 -1
  51. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  52. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  53. package/plugins/aun/package.json +1 -1
  54. package/plugins/aun/plugin.json +1 -1
  55. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  56. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  57. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
  58. package/plugins/chatgpt/package.json +1 -1
  59. package/plugins/chatgpt/plugin.json +14 -1
  60. package/plugins/doubao/config.schema.json +3 -0
  61. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  62. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  63. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  64. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  65. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
  66. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  67. package/plugins/doubao/package.json +1 -1
  68. package/plugins/doubao/plugin.json +1 -1
  69. package/plugins/dsh/config.schema.json +2 -1
  70. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  71. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  72. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  73. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
  74. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  75. package/plugins/dsh/package.json +5 -2
  76. package/plugins/dsh/plugin.json +1 -1
  77. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  78. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  79. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  80. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
  81. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  82. package/plugins/qwenwork/package.json +1 -1
  83. package/plugins/qwenwork/plugin.json +1 -1
  84. package/plugins/workbuddy/config.schema.json +1 -1
  85. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  86. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
  87. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  88. package/plugins/workbuddy/package.json +1 -1
  89. package/plugins/workbuddy/plugin.json +1 -1
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ const session_history_1 = require("../src-ts/plugins/qwenwork/session-history");
9
+ const now = "2026-10-21T03:48:50.000Z";
10
+ function provider(responses) {
11
+ const calls = [];
12
+ const history = new session_history_1.QwenworkSessionHistoryProvider({
13
+ scope: "qwenwork:cdp:9228",
14
+ unavailableReason: () => "",
15
+ query: async (path, input) => {
16
+ calls.push({ path, input });
17
+ const key = path === "chats.listBySubChat" ? `${path}:${String(input.beforeSequence ?? "latest")}` : path;
18
+ const value = responses[key] ?? responses[path];
19
+ if (value instanceof Error)
20
+ throw value;
21
+ return value;
22
+ },
23
+ });
24
+ return { history, calls };
25
+ }
26
+ (0, node_test_1.default)("千问列出全部活跃、归档和删除会话并分页返回", async () => {
27
+ const { history, calls } = provider({
28
+ "chats.list": [{ id: "today", name: "今日任务", updatedAt: now }],
29
+ "chats.listArchived": [{ id: "archived", name: "归档任务", updatedAt: "2026-10-20T03:00:00.000Z", archivedAt: now }],
30
+ "chats.listDeleted": [{ id: "deleted", name: "已删除任务", updatedAt: "2026-10-19T03:00:00.000Z", deletedAt: now }],
31
+ });
32
+ const first = await history.listSessions({ limit: 2 });
33
+ strict_1.default.equal(first.supported, true);
34
+ if (!first.supported)
35
+ return;
36
+ strict_1.default.deepEqual(first.sessions.map(session => session.id), ["today", "archived"]);
37
+ strict_1.default.ok(first.nextCursor);
38
+ strict_1.default.equal(first.complete, true);
39
+ const second = await history.listSessions({ limit: 2, cursor: first.nextCursor });
40
+ strict_1.default.equal(second.supported, true);
41
+ if (!second.supported)
42
+ return;
43
+ strict_1.default.equal(second.sessions[0]?.id, "deleted");
44
+ strict_1.default.equal(second.nextCursor, undefined);
45
+ strict_1.default.equal(calls.filter(call => call.path === "chats.list").length, 2);
46
+ });
47
+ (0, node_test_1.default)("千问读取所有 sub-chat,保留真实消息时间并支持正文分页", async () => {
48
+ const { history, calls } = provider({
49
+ "chats.get": { id: "today", name: "今日任务", updatedAt: now, subChats: [
50
+ { id: "sub-a", name: "主任务", createdAt: now, updatedAt: now },
51
+ { id: "sub-b", name: "子任务", createdAt: now, updatedAt: now },
52
+ ] },
53
+ "chats.listBySubChat": {
54
+ messages: [
55
+ { id: "m3", role: "assistant", parts: [{ type: "tool-Thinking", text: "隐藏推理" }, { type: "text", text: "处理完成" }], createdAt: "2026-10-21T04:00:00.000Z", sequence: 3 },
56
+ { id: "m2", role: "user", parts: [{ type: "text", text: "请处理这个问题" }], createdAt: "2026-10-21T03:50:00.000Z", sequence: 2 },
57
+ ], hasMoreBefore: true, hasMoreAfter: false, oldestSequence: 2, newestSequence: 3,
58
+ },
59
+ "chats.listBySubChat:2": {
60
+ messages: [{ id: "m1", role: "assistant", parts: [{ type: "text", text: "旧子任务结果" }], createdAt: "2026-10-21T03:40:00.000Z", sequence: 1 }],
61
+ hasMoreBefore: false, hasMoreAfter: true, oldestSequence: 1, newestSequence: 1,
62
+ },
63
+ });
64
+ const first = await history.readSession({ sessionId: "today", limit: 2 });
65
+ strict_1.default.equal(first.supported, true);
66
+ if (!first.supported)
67
+ return;
68
+ strict_1.default.deepEqual(first.turns.map(turn => [turn.id, turn.time, turn.text]), [
69
+ ["m1", "2026-10-21T03:40:00.000Z", "旧子任务结果"],
70
+ ["m2", "2026-10-21T03:50:00.000Z", "请处理这个问题"],
71
+ ]);
72
+ strict_1.default.ok(first.nextCursor);
73
+ strict_1.default.equal(first.complete, true);
74
+ const second = await history.readSession({ sessionId: "today", limit: 2, cursor: first.nextCursor });
75
+ strict_1.default.equal(second.supported, true);
76
+ if (!second.supported)
77
+ return;
78
+ strict_1.default.deepEqual(second.turns.map(turn => turn.id), ["m3"]);
79
+ strict_1.default.equal(second.nextCursor, undefined);
80
+ strict_1.default.equal(calls.filter(call => call.path === "chats.listBySubChat").length, 8);
81
+ });
82
+ (0, node_test_1.default)("千问未知可见正文块会标记不完整,思考块仍被排除", async () => {
83
+ const { history } = provider({
84
+ "chats.get": { id: "mixed", name: "混合正文", subChats: [{ id: "sub" }] },
85
+ "chats.listBySubChat": { messages: [
86
+ { id: "m1", role: "user", parts: [{ type: "text", text: "可见问题" }, { type: "file", uri: "private" }], createdAt: now },
87
+ { id: "m2", role: "assistant", parts: [{ type: "tool-Thinking", text: "不返回" }, { type: "text", text: "可见回答" }], createdAt: now },
88
+ ], hasMoreBefore: false },
89
+ });
90
+ const result = await history.readSession({ sessionId: "mixed", limit: 10 });
91
+ strict_1.default.equal(result.supported, true);
92
+ if (result.supported) {
93
+ strict_1.default.equal(result.complete, false);
94
+ strict_1.default.deepEqual(result.turns.map(turn => turn.text), ["可见问题", "可见回答"]);
95
+ strict_1.default.ok(result.missingReasons.some(reason => reason.includes("parts")));
96
+ strict_1.default.ok(!JSON.stringify(result).includes("不返回"));
97
+ }
98
+ });
99
+ (0, node_test_1.default)("千问会话或消息时间缺失时明确标记不完整", async () => {
100
+ const { history } = provider({
101
+ "chats.list": [{ id: "no-time", name: "未知时间" }],
102
+ "chats.listArchived": [],
103
+ "chats.listDeleted": [],
104
+ "chats.get": { id: "no-time", name: "未知时间", updatedAt: now, subChats: [{ id: "sub" }] },
105
+ "chats.listBySubChat": { messages: [{ id: "m", role: "user", parts: [{ type: "text", text: "问题" }] }], hasMoreBefore: false },
106
+ });
107
+ const listing = await history.listSessions({ limit: 10 });
108
+ strict_1.default.equal(listing.supported, true);
109
+ if (!listing.supported)
110
+ return;
111
+ strict_1.default.equal(listing.complete, false);
112
+ strict_1.default.match(listing.missingReasons.join(" "), /更新时间/);
113
+ const read = await history.readSession({ sessionId: "no-time", limit: 10 });
114
+ strict_1.default.equal(read.supported, true);
115
+ if (!read.supported)
116
+ return;
117
+ strict_1.default.equal(read.complete, false);
118
+ strict_1.default.match(read.missingReasons.join(" "), /创建时间/);
119
+ strict_1.default.deepEqual(read.turns, [{ id: "m", role: "user", text: "问题" }]);
120
+ });
121
+ (0, node_test_1.default)("千问畸形会话目录结构不会被当作完整空目录", async () => {
122
+ const malformed = provider({
123
+ "chats.list": [{ id: "valid", name: "有效会话", updatedAt: now }, null, { name: "缺少 ID" }],
124
+ "chats.listArchived": [],
125
+ "chats.listDeleted": [],
126
+ });
127
+ const partial = await malformed.history.listSessions({ limit: 10 });
128
+ strict_1.default.equal(partial.supported, true);
129
+ if (!partial.supported)
130
+ return;
131
+ strict_1.default.deepEqual(partial.sessions.map(session => session.id), ["valid"]);
132
+ strict_1.default.equal(partial.complete, false);
133
+ strict_1.default.match(partial.missingReasons.join(" "), /无法识别的条目/);
134
+ const invalid = provider({ "chats.list": "unexpected", "chats.listArchived": [], "chats.listDeleted": [] });
135
+ const unsupported = await invalid.history.listSessions({ limit: 10 });
136
+ strict_1.default.equal(unsupported.supported, false);
137
+ if (!unsupported.supported)
138
+ strict_1.default.match(unsupported.reason, /无法识别的结构/);
139
+ });
140
+ (0, node_test_1.default)("千问畸形 sub-chat 条目标记正文不完整,仍保留有效 sub-chat", async () => {
141
+ const { history } = provider({
142
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [null, { name: "缺少 ID" }, { id: "valid-sub" }] },
143
+ "chats.listBySubChat": { messages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "消息" }], createdAt: now }], hasMoreBefore: false },
144
+ });
145
+ const result = await history.readSession({ sessionId: "chat", limit: 10 });
146
+ strict_1.default.equal(result.supported, true);
147
+ if (!result.supported)
148
+ return;
149
+ strict_1.default.equal(result.complete, false);
150
+ strict_1.default.match(result.missingReasons.join(" "), /sub-chat 目录缺失或含有无法识别的条目/);
151
+ strict_1.default.deepEqual(result.turns.map(turn => turn.id), ["m1"]);
152
+ });
153
+ (0, node_test_1.default)("千问正文游标可覆盖超过1000条消息", async () => {
154
+ const messages = Array.from({ length: 1002 }, (_, index) => ({
155
+ id: `m${index}`, role: "user", parts: [{ type: "text", text: `消息 ${index}` }],
156
+ createdAt: new Date(Date.parse(now) + index * 1000).toISOString(), sequence: 1002 - index,
157
+ }));
158
+ const { history } = provider({
159
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
160
+ "chats.listBySubChat": { messages, hasMoreBefore: false },
161
+ });
162
+ let result = await history.readSession({ sessionId: "chat", limit: 200 });
163
+ for (let page = 0; page < 5; page++) {
164
+ strict_1.default.equal(result.supported, true);
165
+ if (!result.supported || !result.nextCursor)
166
+ return;
167
+ result = await history.readSession({ sessionId: "chat", limit: 200, cursor: result.nextCursor });
168
+ }
169
+ strict_1.default.equal(result.supported, true);
170
+ if (!result.supported)
171
+ return;
172
+ strict_1.default.deepEqual(result.turns.map(turn => turn.id), ["m1000", "m1001"]);
173
+ });
174
+ (0, node_test_1.default)("千问 user/assistant 畸形 parts 标记不完整,纯 thinking 消息仍忽略", async () => {
175
+ const { history } = provider({
176
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
177
+ "chats.listBySubChat": { messages: [
178
+ { id: "bad-parts", role: "user", parts: "错误结构", createdAt: now },
179
+ { id: "bad-entry", role: "assistant", parts: [{ type: "text", text: "可见回复" }, null], createdAt: now },
180
+ { id: "thinking-only", role: "assistant", parts: [{ type: "tool-Thinking", text: "隐藏思考" }], createdAt: now },
181
+ ], hasMoreBefore: false },
182
+ });
183
+ const result = await history.readSession({ sessionId: "chat", limit: 10 });
184
+ strict_1.default.equal(result.supported, true);
185
+ if (!result.supported)
186
+ return;
187
+ strict_1.default.equal(result.complete, false);
188
+ strict_1.default.match(result.missingReasons.join(" "), /parts 格式无效/);
189
+ strict_1.default.match(result.missingReasons.join(" "), /无法识别的 parts/);
190
+ strict_1.default.deepEqual(result.turns.map(turn => [turn.id, turn.text]), [["bad-entry", "可见回复"]]);
191
+ });
192
+ (0, node_test_1.default)("千问纯 thinking 消息不产生正文,也不造成错误的不完整标记", async () => {
193
+ const { history } = provider({
194
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
195
+ "chats.listBySubChat": { messages: [
196
+ { id: "thinking-only", role: "assistant", parts: [{ type: "tool-Thinking", input: { text: "隐藏思考" } }], createdAt: now },
197
+ ], hasMoreBefore: false },
198
+ });
199
+ const result = await history.readSession({ sessionId: "chat", limit: 10 });
200
+ strict_1.default.equal(result.supported, true);
201
+ if (!result.supported)
202
+ return;
203
+ strict_1.default.equal(result.complete, true);
204
+ strict_1.default.deepEqual(result.turns, []);
205
+ strict_1.default.deepEqual(result.missingReasons, []);
206
+ });
207
+ (0, node_test_1.default)("since 过滤正文前仍参与快照校验,旧消息畸形 parts 不影响今日完整性", async () => {
208
+ const { history } = provider({
209
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
210
+ "chats.listBySubChat": { messages: [
211
+ { id: "old", role: "assistant", parts: [{ unknownOldShape: true }], createdAt: "2026-10-20T23:59:59.000Z", sequence: 1 },
212
+ { id: "today", role: "assistant", parts: [{ type: "text", text: "今天完成" }], createdAt: "2026-10-21T00:00:00.000Z", sequence: 2 },
213
+ ], hasMoreBefore: false, oldestSequence: 1, newestSequence: 2 },
214
+ });
215
+ const result = await history.readSession({ sessionId: "chat", limit: 10, since: "2026-10-21T00:00:00+00:00" });
216
+ strict_1.default.equal(result.supported, true);
217
+ if (!result.supported)
218
+ return;
219
+ strict_1.default.equal(result.complete, true);
220
+ strict_1.default.deepEqual(result.turns.map(turn => [turn.id, turn.time, turn.text]), [["today", "2026-10-21T00:00:00.000Z", "今天完成"]]);
221
+ });
222
+ (0, node_test_1.default)("since 无效时拒绝;分页游标绑定规范化 since", async () => {
223
+ const { history } = provider({
224
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
225
+ "chats.listBySubChat": { messages: [
226
+ { id: "m1", role: "user", parts: [{ type: "text", text: "一" }], createdAt: "2026-10-21T01:00:00.000Z" },
227
+ { id: "m2", role: "assistant", parts: [{ type: "text", text: "二" }], createdAt: "2026-10-21T02:00:00.000Z" },
228
+ ], hasMoreBefore: false },
229
+ });
230
+ const invalid = await history.readSession({ sessionId: "chat", limit: 10, since: "今天" });
231
+ strict_1.default.equal(invalid.supported, false);
232
+ if (!invalid.supported)
233
+ strict_1.default.match(invalid.reason, /since 时间格式无效/);
234
+ const first = await history.readSession({ sessionId: "chat", limit: 1, since: "2026-10-21T00:00:00Z" });
235
+ strict_1.default.equal(first.supported, true);
236
+ if (!first.supported || !first.nextCursor)
237
+ return;
238
+ const same = await history.readSession({ sessionId: "chat", limit: 1, since: "2026-10-21T00:00:00+00:00", cursor: first.nextCursor });
239
+ strict_1.default.equal(same.supported, true);
240
+ const changed = await history.readSession({ sessionId: "chat", limit: 1, since: "2026-10-21T00:01:00Z", cursor: first.nextCursor });
241
+ strict_1.default.equal(changed.supported, false);
242
+ if (!changed.supported)
243
+ strict_1.default.match(changed.reason, /游标无效/);
244
+ });
245
+ (0, node_test_1.default)("缺少创建时间的消息即使带 since 也标记不完整", async () => {
246
+ const { history } = provider({
247
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
248
+ "chats.listBySubChat": { messages: [{ id: "missing-time", role: "user", parts: [{ type: "text", text: "未知时间内容" }] }], hasMoreBefore: false },
249
+ });
250
+ const result = await history.readSession({ sessionId: "chat", limit: 10, since: "2026-10-21T00:00:00.000Z" });
251
+ strict_1.default.equal(result.supported, true);
252
+ if (!result.supported)
253
+ return;
254
+ strict_1.default.equal(result.complete, false);
255
+ strict_1.default.match(result.missingReasons.join(" "), /缺少有效创建时间/);
256
+ });
257
+ (0, node_test_1.default)("千问附件和文件产物转换成可供总结使用的提示文本", async () => {
258
+ const { history } = provider({
259
+ "chats.get": { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] },
260
+ "chats.listBySubChat": { messages: [
261
+ { id: "input", role: "user", parts: [{ type: "data-file", filename: "输入.txt", mediaType: "text/plain" }], createdAt: now },
262
+ { id: "output", role: "assistant", parts: [{ type: "artifact", fileName: "结果.txt", mediaType: "file" }], createdAt: now },
263
+ ], hasMoreBefore: false },
264
+ });
265
+ const result = await history.readSession({ sessionId: "chat", limit: 10, since: "2026-10-21T00:00:00Z" });
266
+ strict_1.default.equal(result.supported, true);
267
+ if (!result.supported)
268
+ return;
269
+ strict_1.default.equal(result.complete, true);
270
+ strict_1.default.deepEqual(result.turns.map(turn => turn.text), ["[附件:输入.txt]", "[文件产物:结果.txt]"]);
271
+ });
272
+ (0, node_test_1.default)("千问列表分页发现目录变化时要求重新列举", async () => {
273
+ let changed = false;
274
+ const history = new session_history_1.QwenworkSessionHistoryProvider({
275
+ scope: "qwenwork:cdp:9228", unavailableReason: () => "",
276
+ query: async (path) => {
277
+ if (path === "chats.list")
278
+ return changed
279
+ ? [{ id: "new", name: "新会话", updatedAt: now }, { id: "a", name: "A", updatedAt: now }, { id: "b", name: "B", updatedAt: now }]
280
+ : [{ id: "a", name: "A", updatedAt: now }, { id: "b", name: "B", updatedAt: now }, { id: "c", name: "C", updatedAt: now }];
281
+ if (path === "chats.listArchived" || path === "chats.listDeleted")
282
+ return [];
283
+ throw new Error("unexpected");
284
+ },
285
+ });
286
+ const first = await history.listSessions({ limit: 2 });
287
+ strict_1.default.equal(first.supported, true);
288
+ if (!first.supported || !first.nextCursor)
289
+ return;
290
+ changed = true;
291
+ const next = await history.listSessions({ limit: 2, cursor: first.nextCursor });
292
+ strict_1.default.equal(next.supported, false);
293
+ if (!next.supported)
294
+ strict_1.default.match(next.reason, /发生变化,请重新列举/);
295
+ });
296
+ (0, node_test_1.default)("千问正文分页发现消息变化时要求重新读取", async () => {
297
+ let changed = false;
298
+ const history = new session_history_1.QwenworkSessionHistoryProvider({
299
+ scope: "qwenwork:cdp:9228", unavailableReason: () => "",
300
+ query: async (path, input) => {
301
+ if (path === "chats.get")
302
+ return { id: "chat", name: "任务", updatedAt: now, subChats: [{ id: "sub" }] };
303
+ if (path === "chats.listBySubChat") {
304
+ const messages = changed
305
+ ? [{ id: "new", role: "assistant", parts: [{ type: "text", text: "新" }], createdAt: now }]
306
+ : [
307
+ { id: "m1", role: "user", parts: [{ type: "text", text: "一" }], createdAt: "2026-10-21T03:00:00Z" },
308
+ { id: "m2", role: "assistant", parts: [{ type: "text", text: "二" }], createdAt: "2026-10-21T04:00:00Z" },
309
+ ];
310
+ return { messages, hasMoreBefore: false, oldestSequence: input.beforeSequence ? 1 : 2, newestSequence: 2 };
311
+ }
312
+ throw new Error("unexpected");
313
+ },
314
+ });
315
+ const first = await history.readSession({ sessionId: "chat", limit: 1 });
316
+ strict_1.default.equal(first.supported, true);
317
+ if (!first.supported || !first.nextCursor)
318
+ return;
319
+ changed = true;
320
+ const next = await history.readSession({ sessionId: "chat", limit: 1, cursor: first.nextCursor });
321
+ strict_1.default.equal(next.supported, false);
322
+ if (!next.supported)
323
+ strict_1.default.match(next.reason, /发生变化,请重新读取/);
324
+ });
325
+ (0, node_test_1.default)("千问无效游标和不可用状态不会访问客户端", async () => {
326
+ let requests = 0;
327
+ const history = new session_history_1.QwenworkSessionHistoryProvider({
328
+ scope: "qwenwork:cdp:9228", unavailableReason: () => "千问当前忙",
329
+ query: async () => { requests++; return []; },
330
+ });
331
+ const result = await history.listSessions({ limit: 10, cursor: "tampered" });
332
+ strict_1.default.equal(result.supported, false);
333
+ strict_1.default.match(result.reason, /当前忙/);
334
+ strict_1.default.equal(requests, 0);
335
+ const available = provider({});
336
+ const invalid = await available.history.readSession({ sessionId: "x", limit: 10, cursor: "tampered" });
337
+ strict_1.default.equal(invalid.supported, false);
338
+ strict_1.default.equal(available.calls.length, 0);
339
+ });
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const strict_1 = __importDefault(require("node:assert/strict"));
40
+ const node_test_1 = __importDefault(require("node:test"));
41
+ const node_vm_1 = require("node:vm");
42
+ const cdp_client_1 = require("../src-ts/plugins/qwenwork/cdp-client");
43
+ const attachments_1 = require("../src-ts/attachments");
44
+ (0, node_test_1.default)("千问图片链接兼容编码的 Windows 路径并拒绝远端文件地址", () => {
45
+ strict_1.default.equal((0, cdp_client_1.normalizeQwenImageUrl)("file://D:%5Cwork%5C%E7%8C%AB.jpg"), "file:///D:/work/%E7%8C%AB.jpg");
46
+ for (const value of ["file://server/share/a.jpg", "file:////server/share/a.jpg", "https://example.com/a.jpg", "file:///tmp/a.jpg?x=1", "file:///%2fserver/a.jpg"])
47
+ strict_1.default.equal((0, cdp_client_1.normalizeQwenImageUrl)(value), undefined, value);
48
+ });
49
+ (0, node_test_1.default)("图片附件按实际字节识别类型,文本或伪装扩展名不会发送", () => {
50
+ strict_1.default.equal((0, attachments_1.detectImageContentType)(new Uint8Array([255, 216, 255, 1])), "image/jpeg");
51
+ strict_1.default.equal((0, attachments_1.detectImageContentType)(new TextEncoder().encode("不是图片")), undefined);
52
+ });
53
+ (0, node_test_1.default)("解析千问展开后的 data 图片并校验真实字节", () => {
54
+ const image = (0, cdp_client_1.decodeQwenDataImage)(`data:image/jpeg;base64,${Buffer.from([255, 216, 255, 1]).toString("base64")}`);
55
+ strict_1.default.equal(image?.contentType, "image/jpeg");
56
+ strict_1.default.deepEqual([...image.data], [255, 216, 255, 1]);
57
+ strict_1.default.equal((0, cdp_client_1.decodeQwenDataImage)("data:image/jpeg;base64,bm90LWltYWdl"), undefined);
58
+ });
59
+ (0, node_test_1.default)("只从最终文件工具参数提取 Windows 文件路径", () => {
60
+ const details = (0, cdp_client_1.parseQwenFinalFileDetails)(JSON.stringify({ files: [
61
+ { file_path: "D:\\\\work\\\\result.txt" },
62
+ { file_path: "D:\\\\work\\\\result.mp4" },
63
+ { file_path: "https://example.com/not-local" },
64
+ { file_path: "\\\\server\\\\share\\\\escape.txt" },
65
+ ] }));
66
+ strict_1.default.deepEqual(details.map((item) => [item.address, item.filename]), [
67
+ ["file:///D:/work/result.txt", "result.txt"],
68
+ ["file:///D:/work/result.mp4", "result.mp4"],
69
+ ]);
70
+ });
71
+ (0, node_test_1.default)("千问实际文件产物保留普通文件和视频 MIME,不局限于图片", async () => {
72
+ const { QwenCdpClient } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/qwenwork/cdp-client")));
73
+ const client = new QwenCdpClient();
74
+ const encoded = [Buffer.from("校验内容").toString("base64"), Buffer.from([0, 0, 0, 20, 102, 116, 121, 112, 105, 115, 111, 109]).toString("base64")];
75
+ client.evaluate = async (expression) => {
76
+ if (expression.includes("const urls ="))
77
+ return [
78
+ { url: "file:///D:/result.txt", data: encoded[0], contentType: "text/plain" },
79
+ { url: "file:///D:/result.mp4", data: encoded[1], contentType: "video/mp4" },
80
+ ];
81
+ if (expression.includes("const files =")) {
82
+ strict_1.default.ok(!expression.includes('a[href^="file://"]'), "不能读取消息正文的本地文件链接");
83
+ strict_1.default.ok(expression.includes('location.href'), "产物采集必须绑定当前会话");
84
+ return JSON.stringify([{ details: JSON.stringify({ files: [{ file_path: "D:/result.txt" }, { file_path: "D:/result.mp4" }] }) }]);
85
+ }
86
+ return true;
87
+ };
88
+ client.evaluateArtifactResult = client.evaluate;
89
+ const result = await client.readArtifacts(new Set(), "file:///D:/qwen/index.html?chat=验收");
90
+ strict_1.default.deepEqual(result.map((item) => [item.kind, item.filename, item.contentType]), [["file", "result.txt", "text/plain"], ["file", "result.mp4", "video/mp4"]]);
91
+ });
92
+ (0, node_test_1.default)("千问上传部分成功后失败只清理本轮唯一名附件", async () => {
93
+ const { QwenCdpClient } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/qwenwork/cdp-client")));
94
+ const client = new QwenCdpClient();
95
+ const removed = [];
96
+ const props = {
97
+ files: [],
98
+ async onAddAttachments(files) {
99
+ strict_1.default.match(files[0].name, /^校验-mab-[a-f0-9-]+\.txt$/);
100
+ props.files.push({ id: "bridge", filename: files[0].name }, { id: "user", filename: "校验.txt" });
101
+ throw new Error("原生上传失败");
102
+ },
103
+ onRemoveFile(id) { removed.push(id); },
104
+ };
105
+ const editor = { __reactFiberTest: { memoizedProps: props } };
106
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: { querySelector: () => editor }, File, atob });
107
+ await strict_1.default.rejects(client.injectAttachments([{ kind: "file", filename: "校验.txt", contentType: "text/plain", data: Buffer.from("随机内容") }]), /原生上传失败/);
108
+ strict_1.default.deepEqual(removed, ["bridge"]);
109
+ });
110
+ (0, node_test_1.default)("千问发送确认失败会清理本轮附件,清理失败保留原错", async () => {
111
+ const { QwenCdpClient } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/qwenwork/cdp-client")));
112
+ for (const cleanupFails of [false, true]) {
113
+ const client = new QwenCdpClient();
114
+ client.ensureVisible = client.recoverDraft = async () => undefined;
115
+ client.evaluate = async (expression) => expression.includes("JSON.stringify([...document.querySelectorAll") ? "[]" : true;
116
+ client.cdp = async () => undefined;
117
+ client.injectAttachments = async () => ["bridge-only"];
118
+ client.waitFor = async () => { throw new Error("提交确认失败"); };
119
+ let cleaned = [];
120
+ client.cleanupAttachments = async (ids) => { cleaned = ids; if (cleanupFails)
121
+ throw new Error("CDP 已断开"); };
122
+ await strict_1.default.rejects(client.complete("任务", 1, undefined, undefined, [{ kind: "file", filename: "输入.txt", contentType: "text/plain", data: Buffer.from("内容") }]), /提交确认失败/);
123
+ strict_1.default.deepEqual(cleaned, ["bridge-only"]);
124
+ }
125
+ });
126
+ (0, node_test_1.default)("千问大视频和内联图片分块读取,正常与失败均清理页面缓存", async () => {
127
+ for (const media of ["video", "image"]) {
128
+ for (const mode of ["normal", "truncated", "switched"]) {
129
+ const { QwenCdpClient } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/qwenwork/cdp-client")));
130
+ const client = new QwenCdpClient();
131
+ const video = Buffer.alloc(5_287_422, 37);
132
+ if (media === "image")
133
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(video);
134
+ const inline = { getAttribute: (name) => name === "src" ? `data:image/png;base64,${video.toString("base64")}` : "result.png" };
135
+ const button = { innerText: "已展示最终文件", getAttribute: () => "true", parentElement: { parentElement: { querySelectorAll: () => [{ textContent: JSON.stringify({ files: [{ file_path: "D:/result.mp4" }] }) }] } } };
136
+ const part = { getAttribute: () => "new", closest() { return this; }, querySelectorAll: (selector) => media === "image" ? selector.startsWith("img") ? [inline] : [] : selector === "button" ? [button] : [] };
137
+ const window = {};
138
+ const context = (0, node_vm_1.createContext)({ window, location: { href: "file:///index.html?chat=验收" }, document: { querySelectorAll: () => [part] }, fetch: async () => new Response(video, { headers: { "content-type": "video/mp4" } }), Uint8Array, btoa });
139
+ let chunks = 0;
140
+ client.evaluate = async (expression) => {
141
+ const readingChunk = expression.includes("?.slice(");
142
+ if (readingChunk) {
143
+ chunks++;
144
+ if (mode === "switched" && chunks > 1)
145
+ context.location.href = "file:///index.html?chat=其他任务";
146
+ }
147
+ const value = await (0, node_vm_1.runInContext)(expression, context);
148
+ if (readingChunk && chunks > 1 && mode === "truncated")
149
+ return value.slice(1);
150
+ if (Buffer.byteLength(JSON.stringify(value) ?? "") > 1_000_000)
151
+ throw new Error("模拟CDP响应超过上限");
152
+ return value;
153
+ };
154
+ if (mode === "normal") {
155
+ const result = await client.readArtifacts(new Set(), "file:///index.html?chat=验收");
156
+ strict_1.default.equal(result.length, 1);
157
+ strict_1.default.deepEqual(Buffer.from(result[0].data), video);
158
+ strict_1.default.ok(chunks > 1);
159
+ }
160
+ else {
161
+ await strict_1.default.rejects(client.readArtifacts(new Set(), "file:///index.html?chat=验收"), mode === "truncated" ? /不完整/ : /会话已切换/);
162
+ }
163
+ strict_1.default.deepEqual(Object.keys(window), []);
164
+ }
165
+ }
166
+ });
@@ -337,7 +337,8 @@ const cdp_client_1 = require("../src-ts/plugins/qwenwork/cdp-client");
337
337
  querySelector: (selector) => selector.includes("contenteditable") ? editor : selector.includes('aria-label="发送消息"') ? { disabled: false, getAttribute() { return null; }, click() { sent = true; editor.innerText = ""; } } : null,
338
338
  querySelectorAll: () => sent ? [reply] : [],
339
339
  };
340
- client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document });
340
+ const window = {};
341
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document, window, location: { href: "file:///index.html?chat=current" } });
341
342
  client.cdp = async (method, params) => {
342
343
  if (method === "Input.insertText") {
343
344
  inserted++;
@@ -560,3 +561,41 @@ const cdp_client_1 = require("../src-ts/plugins/qwenwork/cdp-client");
560
561
  globalThis.WebSocket = original;
561
562
  }
562
563
  });
564
+ (0, node_test_1.default)("千问结构化回复保留图片附件并经同一任务队列返回", async () => {
565
+ const response = { text: "图片已生成", attachments: [{ kind: "image", filename: "猫捉鱼.jpg", contentType: "image/jpeg", data: new Uint8Array([255, 216, 255]) }] };
566
+ const plugin = new qwenwork_1.QwenworkPlugin({ httpPort: 0, client: {
567
+ connect: async () => undefined, close: () => undefined, connected: () => true,
568
+ send: async () => undefined, direct: async () => { throw new Error("不应退回纯文本接口"); },
569
+ complete: async () => response,
570
+ } });
571
+ await plugin.activate({ config: {}, dataDir: "." });
572
+ try {
573
+ strict_1.default.deepEqual(await plugin.handle({ task: "生成图片" }), response);
574
+ }
575
+ finally {
576
+ await plugin.deactivate("stop");
577
+ }
578
+ });
579
+ (0, node_test_1.default)("千问将入站附件透传给桌面客户端,不能静默降为纯文本", async () => {
580
+ const attachments = [{ kind: "file", filename: "校验.txt", contentType: "text/plain", data: new TextEncoder().encode("随机内容") }];
581
+ let received;
582
+ const client = { connect: async () => undefined, close: () => undefined, connected: () => true, send: async () => undefined, direct: async () => "", complete: async (...args) => { received = args[4]; return { text: "已读取" }; }, attachmentInput: true };
583
+ const plugin = new qwenwork_1.QwenworkPlugin({ httpPort: 0, client });
584
+ await plugin.activate({ config: {}, dataDir: "." });
585
+ try {
586
+ strict_1.default.equal(plugin.attachmentInput, true);
587
+ await plugin.handle({ task: "读取附件", attachments });
588
+ strict_1.default.deepEqual(received, attachments);
589
+ }
590
+ finally {
591
+ await plugin.deactivate("stop");
592
+ }
593
+ });
594
+ (0, node_test_1.default)("千问新版停止按钮出现时禁止新建会话或发送,避免打断媒体生成", async () => {
595
+ const { QwenCdpClient } = await Promise.resolve().then(() => __importStar(require("../src-ts/plugins/qwenwork/cdp-client")));
596
+ const client = new QwenCdpClient();
597
+ let clicked = false;
598
+ client.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document: { querySelector: (selector) => selector.includes('aria-label="停止"') ? {} : selector.includes('aria-label="新任务"') ? { click() { clicked = true; } } : null }, window: {} });
599
+ await strict_1.default.rejects(client.createSession(), /正在生成回复/);
600
+ strict_1.default.equal(clicked, false);
601
+ });