mu-agent-bridge 1.0.71 → 1.0.73

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 (120) hide show
  1. package/README.md +9 -6
  2. package/dist/src-ts/agent.js +35 -0
  3. package/dist/src-ts/channel-sessions.js +3 -2
  4. package/dist/src-ts/control-server.js +5 -2
  5. package/dist/src-ts/dsh-agent.js +25 -12
  6. package/dist/src-ts/plugin-catalog.js +11 -2
  7. package/dist/src-ts/plugin-manager.js +183 -9
  8. package/dist/src-ts/plugins/abweb/index.js +1 -1
  9. package/dist/src-ts/plugins/abweb/public/app.js +68 -2
  10. package/dist/src-ts/plugins/abweb/public/index.html +15 -0
  11. package/dist/src-ts/plugins/abweb/public/style.css +4 -0
  12. package/dist/src-ts/plugins/aun/index.js +66 -10
  13. package/dist/src-ts/plugins/aun/messages.js +3 -0
  14. package/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
  15. package/dist/src-ts/plugins/chatgpt/index.js +77 -17
  16. package/dist/src-ts/plugins/claude-cli/index.js +19 -2
  17. package/dist/src-ts/plugins/codex-cli/index.js +19 -2
  18. package/dist/src-ts/plugins/doubao/cdp-client.js +5 -2
  19. package/dist/src-ts/plugins/doubao/index.js +36 -4
  20. package/dist/src-ts/plugins/dsh/index.js +78 -6
  21. package/dist/src-ts/plugins/local-cli/agent.js +74 -9
  22. package/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
  23. package/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
  24. package/dist/src-ts/plugins/personal-assistant/index.js +242 -0
  25. package/dist/src-ts/plugins/personal-assistant/model-client.js +167 -0
  26. package/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
  27. package/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
  28. package/dist/src-ts/plugins/personal-assistant/tools.js +166 -0
  29. package/dist/src-ts/plugins/qwenwork/cdp-client.js +47 -11
  30. package/dist/src-ts/plugins/qwenwork/index.js +89 -8
  31. package/dist/src-ts/plugins/workbuddy/cdp-client.js +9 -4
  32. package/dist/src-ts/plugins/workbuddy/index.js +104 -33
  33. package/dist/src-ts/session-management.js +105 -6
  34. package/dist/tests-ts/abweb-cli.test.js +10 -2
  35. package/dist/tests-ts/abweb-model-config.test.js +70 -0
  36. package/dist/tests-ts/agent.test.js +27 -0
  37. package/dist/tests-ts/aun-messages.test.js +6 -0
  38. package/dist/tests-ts/aun-plugin.test.js +85 -0
  39. package/dist/tests-ts/bundled-package.test.js +12 -2
  40. package/dist/tests-ts/channel-sessions.test.js +27 -0
  41. package/dist/tests-ts/chatgpt-plugin.test.js +99 -0
  42. package/dist/tests-ts/control-server.test.js +36 -0
  43. package/dist/tests-ts/doubao-plugin.test.js +92 -0
  44. package/dist/tests-ts/dsh-agent.test.js +77 -0
  45. package/dist/tests-ts/feishu-plugin.test.js +1 -1
  46. package/dist/tests-ts/installer-assets.test.js +1 -1
  47. package/dist/tests-ts/local-cli.test.js +119 -1
  48. package/dist/tests-ts/model-provider-cancellation.test.js +150 -0
  49. package/dist/tests-ts/personal-assistant-coordination.test.js +76 -0
  50. package/dist/tests-ts/personal-assistant-extensions.test.js +29 -0
  51. package/dist/tests-ts/personal-assistant-owner-context.test.js +107 -0
  52. package/dist/tests-ts/personal-assistant-remote-ability.test.js +123 -0
  53. package/dist/tests-ts/personal-assistant.test.js +389 -0
  54. package/dist/tests-ts/platform-integration.test.js +124 -1
  55. package/dist/tests-ts/plugin-catalog.test.js +12 -2
  56. package/dist/tests-ts/plugin-manager.test.js +280 -0
  57. package/dist/tests-ts/publish-packages.test.js +42 -0
  58. package/dist/tests-ts/qwenwork-plugin.test.js +105 -1
  59. package/dist/tests-ts/session-management.test.js +155 -0
  60. package/dist/tests-ts/workbuddy-plugin.test.js +154 -2
  61. package/package.json +1 -1
  62. package/plugins/abweb/dist/src-ts/agent.js +35 -0
  63. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  64. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +68 -2
  65. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +53 -3
  66. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +15 -0
  67. package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +4 -0
  68. package/plugins/abweb/package.json +1 -1
  69. package/plugins/abweb/plugin.json +1 -1
  70. package/plugins/aun/dist/src-ts/agent.js +35 -0
  71. package/plugins/aun/dist/src-ts/plugins/aun/index.js +66 -10
  72. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +3 -0
  73. package/plugins/chatgpt/dist/src-ts/agent.js +35 -0
  74. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
  75. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +77 -17
  76. package/plugins/chatgpt/package.json +1 -1
  77. package/plugins/chatgpt/plugin.json +1 -1
  78. package/plugins/claude-cli/dist/src-ts/agent.js +35 -0
  79. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +19 -2
  80. package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
  81. package/plugins/claude-cli/package.json +1 -1
  82. package/plugins/claude-cli/plugin.json +3 -2
  83. package/plugins/codex-cli/dist/src-ts/agent.js +35 -0
  84. package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +19 -2
  85. package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
  86. package/plugins/codex-cli/package.json +1 -1
  87. package/plugins/codex-cli/plugin.json +3 -2
  88. package/plugins/doubao/dist/src-ts/agent.js +35 -0
  89. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +5 -2
  90. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +36 -4
  91. package/plugins/doubao/package.json +1 -1
  92. package/plugins/doubao/plugin.json +3 -2
  93. package/plugins/dsh/dist/src-ts/agent.js +35 -0
  94. package/plugins/dsh/dist/src-ts/dsh-agent.js +25 -12
  95. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +78 -6
  96. package/plugins/dsh/package.json +1 -1
  97. package/plugins/dsh/plugin.json +3 -2
  98. package/plugins/feishu/dist/src-ts/agent.js +35 -0
  99. package/plugins/personal-assistant/config.schema.json +22 -0
  100. package/plugins/personal-assistant/dist/src-ts/agent.js +69 -0
  101. package/plugins/personal-assistant/dist/src-ts/plugin.js +13 -0
  102. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
  103. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
  104. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +242 -0
  105. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +167 -0
  106. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
  107. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
  108. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +166 -0
  109. package/plugins/personal-assistant/package.json +39 -0
  110. package/plugins/personal-assistant/plugin.json +15 -0
  111. package/plugins/qwenwork/dist/src-ts/agent.js +35 -0
  112. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +47 -11
  113. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +89 -8
  114. package/plugins/qwenwork/package.json +1 -1
  115. package/plugins/qwenwork/plugin.json +3 -2
  116. package/plugins/workbuddy/dist/src-ts/agent.js +35 -0
  117. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +9 -4
  118. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +104 -33
  119. package/plugins/workbuddy/package.json +1 -1
  120. package/plugins/workbuddy/plugin.json +3 -2
@@ -0,0 +1,166 @@
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
+ exports.TOOL_NAMES = void 0;
7
+ exports.toolDefinitions = toolDefinitions;
8
+ exports.parseToolArguments = parseToolArguments;
9
+ exports.executeTool = executeTool;
10
+ exports.parseExplicitCommand = parseExplicitCommand;
11
+ exports.stringifyToolResult = stringifyToolResult;
12
+ const ajv_1 = __importDefault(require("ajv"));
13
+ const coordination_1 = require("./coordination");
14
+ exports.TOOL_NAMES = ["agents_list", "agents_call", "aun_identity_list", "sessions_command", "sessions_today", "self_status", "ability_discover", "ability_call", "ability_request_get", "coordination_dispatch", "coordination_status", "owner_context_read"];
15
+ const ajv = new ajv_1.default({ allErrors: false, strict: false });
16
+ const schemas = {
17
+ agents_list: ajv.compile({ type: "object", additionalProperties: false, properties: {} }),
18
+ agents_call: ajv.compile({ type: "object", additionalProperties: false, required: ["id", "task"], properties: { id: { type: "string", minLength: 1, maxLength: 128 }, task: { type: "string", minLength: 1, maxLength: 20_000 } } }),
19
+ aun_identity_list: ajv.compile({ type: "object", additionalProperties: false, properties: {} }),
20
+ sessions_command: ajv.compile({ type: "object", additionalProperties: false, required: ["command"], properties: { command: { type: "string", minLength: 1, maxLength: 512 } } }),
21
+ sessions_today: ajv.compile({ type: "object", additionalProperties: false, properties: {} }),
22
+ self_status: ajv.compile({ type: "object", additionalProperties: false, properties: {} }),
23
+ ability_discover: ajv.compile({ type: "object", additionalProperties: false, required: ["providerAid"], properties: { providerAid: { type: "string", minLength: 3, maxLength: 256 } } }),
24
+ ability_call: ajv.compile({ type: "object", additionalProperties: false, required: ["providerAid", "path", "params"], properties: { providerAid: { type: "string", minLength: 3, maxLength: 256 }, path: { type: "string", minLength: 1, maxLength: 256 }, params: { type: "object" } } }),
25
+ ability_request_get: ajv.compile({ type: "object", additionalProperties: false, required: ["providerAid", "requestId"], properties: { providerAid: { type: "string", minLength: 3, maxLength: 256 }, requestId: { type: "string", minLength: 1, maxLength: 256 } } }),
26
+ coordination_dispatch: ajv.compile({ type: "object", additionalProperties: false, required: ["agentId", "aid", "task"], properties: { agentId: { type: "string", minLength: 1, maxLength: 64 }, aid: { type: "string", minLength: 3, maxLength: 256 }, task: { type: "string", minLength: 1, maxLength: 20000 } } }),
27
+ coordination_status: ajv.compile({ type: "object", additionalProperties: false, required: ["aid", "threadId"], properties: { aid: { type: "string", minLength: 3, maxLength: 256 }, threadId: { type: "string", minLength: 1, maxLength: 256 } } }),
28
+ owner_context_read: ajv.compile({ type: "object", additionalProperties: false, properties: {} }),
29
+ };
30
+ const toolDescriptions = {
31
+ agents_list: { name: "agents_list", description: "列出当前可调用的 Agent 及其 AID。", parameters: { type: "object", additionalProperties: false, properties: {} } },
32
+ agents_call: { name: "agents_call", description: "向白名单中的 Agent 发送一项独立任务。", parameters: { type: "object", additionalProperties: false, required: ["id", "task"], properties: { id: { type: "string", minLength: 1, maxLength: 128 }, task: { type: "string", minLength: 1, maxLength: 20_000 } } } },
33
+ aun_identity_list: { name: "aun_identity_list", description: "列出本机已知 AUN 身份及绑定状态。", parameters: { type: "object", additionalProperties: false, properties: {} } },
34
+ sessions_command: { name: "sessions_command", description: "执行只读的 /sessions 管理命令。", parameters: { type: "object", additionalProperties: false, required: ["command"], properties: { command: { type: "string", minLength: 1, maxLength: 512 } } } },
35
+ sessions_today: { name: "sessions_today", description: "生成今天工作的小结。", parameters: { type: "object", additionalProperties: false, properties: {} } },
36
+ self_status: { name: "self_status", description: "报告个人助理自身状态。", parameters: { type: "object", additionalProperties: false, properties: {} } },
37
+ ability_discover: { name: "ability_discover", description: "发现已配置 Provider 的只读 AUN Ability。", parameters: { type: "object", additionalProperties: false, required: ["providerAid"], properties: { providerAid: { type: "string", minLength: 3, maxLength: 256 } } } },
38
+ ability_call: { name: "ability_call", description: "调用无需认证和确认的纯只读 AUN Ability。", parameters: { type: "object", additionalProperties: false, required: ["providerAid", "path", "params"], properties: { providerAid: { type: "string", minLength: 3, maxLength: 256 }, path: { type: "string", minLength: 1, maxLength: 256 }, params: { type: "object" } } } },
39
+ ability_request_get: { name: "ability_request_get", description: "查询已发送 AUN Ability 的请求状态,不重新发送。", parameters: { type: "object", additionalProperties: false, required: ["providerAid", "requestId"], properties: { providerAid: { type: "string", minLength: 3, maxLength: 256 }, requestId: { type: "string", minLength: 1, maxLength: 256 } } } },
40
+ coordination_dispatch: { name: "coordination_dispatch", description: "显式向白名单 Agent 派发任务并创建独立 thread。", parameters: { type: "object", additionalProperties: false, required: ["agentId", "aid", "task"], properties: { agentId: { type: "string", minLength: 1, maxLength: 64 }, aid: { type: "string", minLength: 3, maxLength: 256 }, task: { type: "string", minLength: 1, maxLength: 20000 } } } },
41
+ coordination_status: { name: "coordination_status", description: "读取派发任务对应 thread 的当前状态和历史。", parameters: { type: "object", additionalProperties: false, required: ["aid", "threadId"], properties: { aid: { type: "string", minLength: 3, maxLength: 256 }, threadId: { type: "string", minLength: 1, maxLength: 256 } } } },
42
+ owner_context_read: { name: "owner_context_read", description: "读取管理员配置的 Owner 共享资料,仅作为参考。", parameters: { type: "object", additionalProperties: false, properties: {} } },
43
+ };
44
+ function toolDefinitions() {
45
+ return exports.TOOL_NAMES.map((name) => ({ type: "function", function: { ...toolDescriptions[name], parameters: { ...toolDescriptions[name].parameters } } }));
46
+ }
47
+ function parseToolArguments(name, raw) {
48
+ if (!exports.TOOL_NAMES.includes(name))
49
+ throw new Error("未知的管理工具");
50
+ let value;
51
+ try {
52
+ value = JSON.parse(raw);
53
+ }
54
+ catch {
55
+ throw new Error("管理工具参数不是有效 JSON");
56
+ }
57
+ const validate = schemas[name];
58
+ if (!validate(value))
59
+ throw new Error("管理工具参数不符合要求");
60
+ return value;
61
+ }
62
+ async function executeTool(name, args, management, allowedAgents, runtime = {}) {
63
+ if (!exports.TOOL_NAMES.includes(name))
64
+ throw new Error("未知的管理工具");
65
+ if (!schemas[name](args))
66
+ throw new Error("管理工具参数不符合要求");
67
+ switch (name) {
68
+ case "agents_list": {
69
+ const agents = await management.listAgents();
70
+ return allowedAgents ? agents.map((agent) => allowedAgents.includes(agent.id) ? agent : { ...agent, callable: false, reason: "未加入个人助理 Agent 白名单" }) : agents;
71
+ }
72
+ case "agents_call": {
73
+ if (allowedAgents && !allowedAgents.includes(String(args.id)))
74
+ throw new Error("目标 Agent 未加入个人助理白名单");
75
+ const result = await management.callAgent(String(args.id), String(args.task));
76
+ return {
77
+ text: result.text,
78
+ ...(result.sessionId ? { sessionId: result.sessionId } : {}),
79
+ };
80
+ }
81
+ case "aun_identity_list": return management.listAunIdentities();
82
+ case "sessions_command": {
83
+ const command = String(args.command).trim();
84
+ if (!/^\/sessions(?:\s|$)/.test(command) || /[\r\n]/.test(command))
85
+ throw new Error("只支持单行 /sessions 只读命令");
86
+ return management.runSessionCommand(command);
87
+ }
88
+ case "sessions_today": return management.summarizeToday();
89
+ case "self_status": return { status: "ready", capabilities: exports.TOOL_NAMES };
90
+ case "ability_discover": {
91
+ const providerAid = String(args.providerAid);
92
+ if (!runtime.allowedProviders?.includes(providerAid) || !runtime.remoteAbility)
93
+ throw new Error("Provider 未加入远程 Ability 白名单");
94
+ return runtime.remoteAbility.discover(providerAid);
95
+ }
96
+ case "ability_call": {
97
+ const providerAid = String(args.providerAid);
98
+ if (!runtime.allowedProviders?.includes(providerAid) || !runtime.remoteAbility)
99
+ throw new Error("Provider 未加入远程 Ability 白名单");
100
+ return runtime.remoteAbility.call(providerAid, String(args.path), args.params);
101
+ }
102
+ case "ability_request_get": {
103
+ const providerAid = String(args.providerAid);
104
+ if (!runtime.allowedProviders?.includes(providerAid) || !runtime.remoteAbility)
105
+ throw new Error("Provider 未加入远程 Ability 白名单");
106
+ return runtime.remoteAbility.getRequest(providerAid, String(args.requestId));
107
+ }
108
+ case "coordination_dispatch": {
109
+ if (!allowedAgents?.includes(String(args.agentId)) || !runtime.selfAid)
110
+ throw new Error("目标 Agent 未加入个人助理白名单");
111
+ const abilities = await management.listAgents();
112
+ const agent = abilities.find((item) => item.id === args.agentId && item.callable && item.aids.includes(String(args.aid)));
113
+ if (!agent)
114
+ throw new Error("目标 Agent/AID 当前不可派活");
115
+ const coordinator = (0, coordination_1.createCoordinator)({ selfAid: runtime.selfAid, ecPath: runtime.ecPath, allowedAgents: [{ id: agent.id, aids: agent.aids }] });
116
+ const result = await coordinator.dispatch({ agentId: agent.id, aid: String(args.aid), task: String(args.task) });
117
+ if (runtime.threads && runtime.threads.size >= 128)
118
+ runtime.threads.delete(runtime.threads.keys().next().value);
119
+ runtime.threads?.set(result.threadId, result.aid);
120
+ return result;
121
+ }
122
+ case "coordination_status": {
123
+ if (!runtime.selfAid)
124
+ throw new Error("个人助理 AID 未配置");
125
+ if (runtime.threads?.get(String(args.threadId)) !== String(args.aid))
126
+ throw new Error("thread 不属于本个人助理已派发的任务");
127
+ const coordinator = (0, coordination_1.createCoordinator)({ selfAid: runtime.selfAid, ecPath: runtime.ecPath, allowedAgents: [] });
128
+ return coordinator.getStatus({ aid: String(args.aid), threadId: String(args.threadId) });
129
+ }
130
+ case "owner_context_read": {
131
+ if (!runtime.ownerContext)
132
+ throw new Error("Owner 共享上下文尚未配置");
133
+ return runtime.ownerContext.get();
134
+ }
135
+ }
136
+ }
137
+ function parseExplicitCommand(task) {
138
+ if (!task.startsWith("/assistant ") && !task.startsWith("/sessions"))
139
+ return undefined;
140
+ if (/[\r\n]/.test(task))
141
+ return { kind: "invalid" };
142
+ if (task.startsWith("/sessions"))
143
+ return /^\/sessions(?:\s|$)/.test(task) ? { kind: "sessions", command: task } : { kind: "invalid" };
144
+ if (task === "/assistant context refresh")
145
+ return { kind: "context-refresh" };
146
+ const agentCall = task.match(/^\/assistant agent call ([a-z0-9][a-z0-9-]{0,63}) ([\s\S]{1,20000})$/);
147
+ if (agentCall)
148
+ return { kind: "agent-call", agentId: agentCall[1], task: agentCall[2] };
149
+ const create = task.match(/^\/assistant aid create ([A-Za-z0-9][A-Za-z0-9._-]{2,255})(?: --owner ([A-Za-z0-9][A-Za-z0-9._-]{2,255}))?$/);
150
+ if (create)
151
+ return { kind: "create", aid: create[1], ...(create[2] ? { ownerAid: create[2] } : {}) };
152
+ const bind = task.match(/^\/assistant aid bind ([A-Za-z0-9][A-Za-z0-9._-]{2,255}) ([A-Za-z0-9][A-Za-z0-9._-]{0,127})( --replace)?$/);
153
+ if (bind)
154
+ return { kind: "bind", aid: bind[1], agentId: bind[2], replace: Boolean(bind[3]) };
155
+ return { kind: "invalid" };
156
+ }
157
+ function stringifyToolResult(value) {
158
+ let text;
159
+ try {
160
+ text = typeof value === "string" ? value : JSON.stringify(value);
161
+ }
162
+ catch {
163
+ text = "工具返回结果不可序列化";
164
+ }
165
+ return text.length > 64_000 ? `${text.slice(0, 64_000)}\n[结果已截断]` : text;
166
+ }
@@ -127,19 +127,40 @@ class QwenCdpClient {
127
127
  const reply = await this.direct(`这是后台健康检测。请只回复 ${token},不要添加任何其它文字。`, 45_000, undefined, token);
128
128
  verifyQwenHeartbeatReply(token, reply);
129
129
  }
130
- async createSession(_name) {
130
+ async createSession(_name, forceNew = false, preserveDraft = false) {
131
131
  await this.ensureVisible();
132
- await this.recoverDraft();
132
+ if (!preserveDraft)
133
+ await this.recoverDraft();
133
134
  const snapshot = `(() => ({ url: location.href, messages: document.querySelectorAll('[data-message-id]').length, input: document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText ?? null, blank: Boolean(document.querySelector('.new-task-model-selector')) }))()`;
134
135
  const before = await this.evaluate(snapshot);
135
136
  if (before.input?.trim())
136
137
  throw new Error("千问输入框含有未发送内容,不能新建会话");
137
- if (before.input !== null && before.messages === 0 && before.blank)
138
- return;
139
- const clicked = await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="新任务"]'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
138
+ if (!forceNew && before.input !== null && before.messages === 0 && before.blank)
139
+ return this.currentSessionId();
140
+ const clicked = await this.evaluate(`(() => { if (document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText.trim() || document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问当前有未发送内容或正在生成回复,不能新建会话'); const button = document.querySelector('button[aria-label="新任务"]'); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
140
141
  if (clicked !== true)
141
142
  throw new Error("千问新任务入口未找到或不可用");
142
143
  await this.waitFor(`(() => { const state = ${snapshot}; return state.input !== null && state.input.trim() === '' && state.messages === 0 && state.url !== ${JSON.stringify(before.url)}; })()`, "千问新会话状态校验失败");
144
+ return this.currentSessionId();
145
+ }
146
+ async currentSessionId() {
147
+ const href = String(await this.evaluate("location.href"));
148
+ try {
149
+ return new URL(href).searchParams.get("chat") ?? undefined;
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ }
155
+ async activateSession(sessionId) {
156
+ if (!sessionId.trim() || sessionId.length > 512 || /[\u0000-\u001f]/.test(sessionId))
157
+ throw new Error("千问模型会话标识无效");
158
+ await this.ensureVisible();
159
+ const target = new URL(String(await this.evaluate("location.href")));
160
+ target.searchParams.set("chat", sessionId);
161
+ if (target.href !== String(await this.evaluate("location.href")))
162
+ await this.evaluate(`(() => { if (document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText.trim() || document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问当前有未发送内容或正在生成回复,不能切换会话'); location.href = ${JSON.stringify(target.href)}; })()`);
163
+ await this.waitFor(`location.href === ${JSON.stringify(target.href)} && Boolean(document.querySelector(${JSON.stringify(exports.inputSelector)}))`, "千问个人助理模型会话恢复失败");
143
164
  }
144
165
  async connect() {
145
166
  if (this.connected())
@@ -204,7 +225,7 @@ class QwenCdpClient {
204
225
  async direct(text, timeoutMs = 240_000, signal, expectedReply) {
205
226
  return (await this.complete(text, timeoutMs, signal, expectedReply)).text;
206
227
  }
207
- async complete(text, timeoutMs = 240_000, signal, expectedReply, attachments, onMessage) {
228
+ async complete(text, timeoutMs = 240_000, signal, expectedReply, attachments, onMessage, preserveDraft = false) {
208
229
  let stage = "准备千问办公桌面任务";
209
230
  let code = "QWEN_TASK_FAILED";
210
231
  try {
@@ -215,7 +236,8 @@ class QwenCdpClient {
215
236
  throw new Error("千问输入框有用户草稿,跳过后台健康检测");
216
237
  }
217
238
  await this.ensureVisible();
218
- await this.recoverDraft();
239
+ if (!preserveDraft)
240
+ await this.recoverDraft();
219
241
  const ready = await this.evaluate(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); return Boolean(input && !input.innerText.trim() && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')); })()`);
220
242
  if (!ready)
221
243
  throw new Error("千问输入框含有未发送内容或正在生成回复,未发送新消息");
@@ -254,23 +276,37 @@ class QwenCdpClient {
254
276
  code = "QWEN_REPLY_FAILED";
255
277
  let previousReply = "";
256
278
  let stablePolls = 0;
279
+ let seenGenerating = false;
280
+ let stoppedPolls = 0;
257
281
  const forwarded = new Set();
258
282
  const deadline = Date.now() + timeoutMs;
259
- while (Date.now() < deadline) {
260
- await wait(2_000, signal);
283
+ while (Date.now() < deadline || signal?.aborted) {
284
+ // 上游取消不停止桌面生成;确认结束前保留插件队列。
285
+ await wait(2_000);
261
286
  const messagesJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
262
- const fresh = freshAssistantMessages(beforeIds, JSON.parse(messagesJson || "[]"));
287
+ const messages = JSON.parse(messagesJson || "[]");
288
+ const fresh = freshAssistantMessages(beforeIds, messages);
263
289
  const reply = fresh.at(-1)?.text.trim() ?? "";
264
290
  stablePolls = reply && reply === previousReply ? stablePolls + 1 : 0;
265
291
  previousReply = reply;
266
292
  if (reply && stablePolls >= 3 && isQwenApiUnavailableReply(reply))
267
293
  throw new Error("QWEN_REMOTE_ERROR:千问生成接口不可用;桌面仅返回通用错误,未提供服务端错误码;CDP 仍连接,未重启或重新提交任务");
268
294
  const generating = await this.evaluate(`Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]'))`);
295
+ seenGenerating ||= generating === true;
296
+ stoppedPolls = seenGenerating && generating === false ? stoppedPolls + 1 : 0;
297
+ if (signal?.aborted && !reply && stoppedPolls >= 2 && messages.some(message => message.id && !beforeIds.has(message.id))) {
298
+ const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
299
+ if (attachments.length)
300
+ return { text: "", sessionId: new URL(sessionUrl).searchParams.get("chat") ?? undefined, attachments };
301
+ code = "QWEN_REQUEST_CANCELLED";
302
+ throw new Error("请求已取消,已确认本轮桌面生成结束,未返回正文或附件");
303
+ }
269
304
  if (!generating && (expectedReply ? isQwenHeartbeatReplyComplete(reply, expectedReply) && stablePolls >= 1 : reply && stablePolls >= 3)) {
270
305
  stage = "读取千问办公已完成任务的产物(尚未上传 AUN FS)";
271
306
  code = "QWEN_ARTIFACT_READ_FAILED";
272
307
  const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
273
- return { text: reply, ...(attachments.length ? { attachments } : {}) };
308
+ const sessionId = new URL(sessionUrl).searchParams.get("chat") ?? undefined;
309
+ return { text: reply, ...(sessionId ? { sessionId } : {}), ...(attachments.length ? { attachments } : {}) };
274
310
  }
275
311
  if (onMessage && generating && reply && stablePolls >= 1 && !forwarded.has(reply) && !isQwenApiUnavailableReply(reply)) {
276
312
  await onMessage(reply);
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QwenworkPlugin = exports.qwenworkManifest = void 0;
4
4
  exports.createQwenworkPlugin = createQwenworkPlugin;
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
5
7
  const agent_1 = require("../../agent");
6
8
  const openai_adapter_1 = require("./openai-adapter");
7
9
  const cdp_client_1 = require("./cdp-client");
@@ -10,21 +12,29 @@ const desktop_launch_1 = require("../desktop-launch");
10
12
  const session_history_1 = require("./session-history");
11
13
  exports.qwenworkManifest = {
12
14
  id: "qwenwork",
13
- version: "1.0.24",
15
+ version: "1.0.25",
14
16
  displayName: "千问办公",
15
17
  entry: "dist/index.js",
16
18
  apiVersion: "1",
17
19
  process: "in-process",
18
- capabilities: ["chat", "openai-compatible", "health"],
20
+ capabilities: ["chat", "openai-compatible", "health", "model-provider"],
19
21
  configSchema: "config.schema.json",
20
22
  defaults: { cdpPort: 9228, httpPort: 17893 },
21
23
  };
22
24
  function asNumber(value, fallback) {
23
25
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
24
26
  }
27
+ function validModelSessionId(value) {
28
+ return typeof value === "string" && Boolean(value.trim()) && value.length <= 512 && !/[\u0000-\u001f]/.test(value);
29
+ }
25
30
  /** 千问插件由后台守护,所有通道共用请求队列和恢复流程。 */
26
31
  class QwenworkPlugin {
27
32
  manifest = exports.qwenworkManifest;
33
+ get modelProvider() {
34
+ if (this.relayTarget || !this.client?.complete || !this.client.createSession || !this.client.activateSession)
35
+ return undefined;
36
+ return { complete: (request) => this.completeModelProvider(request) };
37
+ }
28
38
  sessionHistory;
29
39
  get attachmentInput() { return Boolean(this.relayTarget || this.client?.attachmentInput); }
30
40
  sessionOperations = [];
@@ -53,6 +63,8 @@ class QwenworkPlugin {
53
63
  busy = false;
54
64
  taskCount = 0;
55
65
  stopped = true;
66
+ modelSessions = new Map();
67
+ modelSessionsPath;
56
68
  constructor(options = {}) {
57
69
  this.injectedClient = options.client;
58
70
  this.configuredCdpPort = options.cdpPort;
@@ -68,6 +80,21 @@ class QwenworkPlugin {
68
80
  this.lastHeartbeat = undefined;
69
81
  this.context = context;
70
82
  try {
83
+ this.modelSessions.clear();
84
+ this.modelSessionsPath = typeof context.dataDir === "string" && context.dataDir ? (0, node_path_1.join)(context.dataDir, "model-provider-sessions.json") : undefined;
85
+ try {
86
+ if (!this.modelSessionsPath)
87
+ throw Object.assign(new Error("无持久化目录"), { code: "ENOENT" });
88
+ const saved = JSON.parse(await (0, promises_1.readFile)(this.modelSessionsPath, "utf8"));
89
+ if (!saved || typeof saved !== "object" || Array.isArray(saved) || Object.entries(saved).some(([key, value]) => !validModelSessionId(key) || !validModelSessionId(value)))
90
+ throw new Error("千问个人助理模型会话记录无效");
91
+ for (const [key, value] of Object.entries(saved))
92
+ this.modelSessions.set(key, value);
93
+ }
94
+ catch (error) {
95
+ if (error.code !== "ENOENT")
96
+ throw error;
97
+ }
71
98
  const config = context.config;
72
99
  const relayTarget = config.relayTarget;
73
100
  if (relayTarget !== undefined && relayTarget !== "dsh")
@@ -204,7 +231,7 @@ class QwenworkPlugin {
204
231
  throw error;
205
232
  }
206
233
  }
207
- async completeRequest(request) {
234
+ async completeRequest(request, modelRequest = false) {
208
235
  if (this.relayTarget) {
209
236
  return this.callPlugin(this.relayTarget, request);
210
237
  }
@@ -217,8 +244,8 @@ class QwenworkPlugin {
217
244
  throw new Error("千问当前客户端不支持附件输入");
218
245
  const timeoutMs = request.timeoutMs ?? asNumber(this.context?.config.timeoutMs, 240_000);
219
246
  const response = this.client.complete
220
- ? await this.client.complete(request.task, timeoutMs, undefined, undefined, request.attachments, request.onMessage)
221
- : { text: await this.complete(request.task, timeoutMs) };
247
+ ? await this.client.complete(request.task, timeoutMs, request.signal, undefined, request.attachments, request.onMessage, modelRequest)
248
+ : { text: await this.complete(request.task, timeoutMs, request.signal) };
222
249
  if ((0, cdp_client_1.isQwenApiUnavailableReply)(response.text))
223
250
  throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
224
251
  this.failures = 0;
@@ -227,12 +254,66 @@ class QwenworkPlugin {
227
254
  return response;
228
255
  }
229
256
  catch (error) {
230
- this.message = error instanceof Error ? error.message : String(error);
231
- this.failures = this.client.connected() ? 0 : 2;
232
- this.state = "degraded";
257
+ if (!request.signal?.aborted && !modelRequest) {
258
+ this.message = error instanceof Error ? error.message : String(error);
259
+ this.failures = this.client.connected() ? 0 : 2;
260
+ this.state = "degraded";
261
+ }
233
262
  throw error;
234
263
  }
235
264
  }
265
+ async completeModelProvider(request) {
266
+ let stage = "千问办公模型排队(queue)";
267
+ try {
268
+ request.signal.throwIfAborted();
269
+ const run = this.enqueueTask(async () => {
270
+ request.signal.throwIfAborted();
271
+ stage = "连接千问办公模型(connect)";
272
+ const client = this.client;
273
+ if (this.state !== "ready" || !client?.connected() || !client.createSession || !client.activateSession || !client.complete)
274
+ throw new Error("千问办公未连接或不支持隔离模型会话,模型请求不会启动或重启桌面");
275
+ const previousSessionId = await client.currentSessionId?.();
276
+ try {
277
+ stage = "准备千问办公模型会话(submit)";
278
+ const desktopSessionId = this.modelSessions.get(request.sessionId);
279
+ if (desktopSessionId)
280
+ await client.activateSession(desktopSessionId);
281
+ else
282
+ await client.createSession(undefined, true, true);
283
+ request.signal.throwIfAborted();
284
+ stage = "等待千问办公模型生成(generate)";
285
+ const response = await this.completeRequest({ task: request.prompt, sessionId: request.sessionId, timeoutMs: request.timeoutMs, signal: request.signal }, true);
286
+ const actualSessionId = response.sessionId ?? await client.currentSessionId?.();
287
+ if (!validModelSessionId(actualSessionId))
288
+ throw new Error("千问未返回可恢复的个人助理模型会话标识");
289
+ if (actualSessionId !== desktopSessionId) {
290
+ this.modelSessions.set(request.sessionId, actualSessionId);
291
+ const path = this.modelSessionsPath;
292
+ if (path) {
293
+ await (0, promises_1.mkdir)(this.context.dataDir ?? ".", { recursive: true });
294
+ await (0, promises_1.writeFile)(`${path}.tmp`, JSON.stringify(Object.fromEntries(this.modelSessions)), { mode: 0o600 });
295
+ await (0, promises_1.rename)(`${path}.tmp`, path);
296
+ }
297
+ }
298
+ return { text: response.text };
299
+ }
300
+ finally {
301
+ if (client.connected()) {
302
+ if (previousSessionId)
303
+ await client.activateSession(previousSessionId);
304
+ else
305
+ await client.createSession(undefined, false, true);
306
+ }
307
+ }
308
+ });
309
+ return await (0, agent_1.abortable)(run, request.signal);
310
+ }
311
+ catch (error) {
312
+ if (error instanceof agent_1.AgentOperationError)
313
+ throw error;
314
+ throw new agent_1.AgentOperationError("QWEN_MODEL_FAILED", stage, error);
315
+ }
316
+ }
236
317
  registerRoutes(http) {
237
318
  if (!this.adapter)
238
319
  return;
@@ -46,11 +46,12 @@ class WorkbuddyCdpClient {
46
46
  const value = await this.evaluate(`([...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_')))?.getAttribute('data-conversation-id')`);
47
47
  return typeof value === "string" && value.trim() ? value : undefined;
48
48
  }
49
- async activateConversation(conversationId) {
49
+ async activateConversation(conversationId, preserveDraft = false) {
50
50
  const id = conversationId.trim();
51
51
  if (!id)
52
52
  throw new Error("WorkBuddy 会话标识不能为空");
53
53
  const clicked = await this.evaluate(`(() => {
54
+ if (${preserveDraft}) { const editor = document.querySelector(${JSON.stringify(exports.editorSelector)})?.cloneNode(true); editor?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(item => item.remove()); if (editor?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim() || document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]') || document.querySelector(${JSON.stringify(interactionOptionSelector)})) throw new Error('WorkBuddy 当前有用户草稿、待确认操作或正在生成回复,模型请求未切换会话'); }
54
55
  const button = [...document.querySelectorAll('[data-conversation-id]')]
55
56
  .filter(element => element.getAttribute('data-conversation-id') === ${JSON.stringify(id)})
56
57
  .map(item => item.matches('[role="button"],button') ? item : item.querySelector('[role="button"],button') || item.closest('[role="button"],button'))
@@ -93,6 +94,8 @@ class WorkbuddyCdpClient {
93
94
  return await this.evaluate(`(() => ({
94
95
  conversationId: [...document.querySelectorAll('[data-conversation-id]')].find(item => String(item.className).includes('_selected_'))?.getAttribute('data-conversation-id') || undefined,
95
96
  waiting: Boolean(document.querySelector(${JSON.stringify(interactionOptionSelector)})),
97
+ generating: Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]')),
98
+ draft: (() => { const text = document.querySelector(${JSON.stringify(exports.editorSelector)})?.cloneNode(true); text?.querySelectorAll('[data-slate-placeholder="true"],[data-slate-zero-width]').forEach(item => item.remove()); return Boolean(text?.textContent?.replace(/[\\uFEFF\\u200B]/g, '').trim()); })(),
96
99
  }))()`);
97
100
  }
98
101
  async connect() {
@@ -248,7 +251,7 @@ class WorkbuddyCdpClient {
248
251
  if (!await this.evaluate(`(() => { const button = document.querySelector('button[aria-label="发送"]'); if (!button || button.disabled) return false; button.click(); return true; })()`))
249
252
  throw new Error("WorkBuddy 发送按钮不可用");
250
253
  stage = "等待 WorkBuddy 回复(任务已提交,桌面未被停止)";
251
- const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage);
254
+ const response = await this.waitForReply(beforeIds, timeoutMs, undefined, undefined, request.sessionId, request.onMessage, request.signal);
252
255
  if (this.pendingInteraction && temporaryPaths.length)
253
256
  this.pendingInteractionCleanup = temporaryPaths;
254
257
  return response;
@@ -314,12 +317,12 @@ class WorkbuddyCdpClient {
314
317
  }
315
318
  }
316
319
  }
317
- async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage) {
320
+ async waitForReply(beforeIds, timeoutMs, changedId, initialText, sessionId, onMessage, signal) {
318
321
  const deadline = Date.now() + timeoutMs;
319
322
  const previousText = new Map();
320
323
  const stablePolls = new Map();
321
324
  const forwarded = new Set();
322
- while (Date.now() < deadline) {
325
+ while (Date.now() < deadline || signal?.aborted) {
323
326
  await sleep(1_000);
324
327
  const current = JSON.parse(String(await this.evaluate(`JSON.stringify([...document.querySelectorAll('.cr-agent[data-message-id]')].map(element => { const content = element.querySelector('[data-assistant-content]'); let text = content?.innerText?.trim() || ''; for (const thought of content?.querySelectorAll?.('.cr-reasoning') || []) text = text.replace(thought.innerText || '', ''); text = text.trim(); const finished = Boolean(element.querySelector('[data-testid="conversation-finished-footer"]')); const stopped = Boolean(text) && !document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"],[aria-busy="true"]'); return { id: element.getAttribute('data-message-id'), text, finished: finished, stopped: stopped }; }))`)));
325
328
  const interaction = await this.readInteraction();
@@ -353,6 +356,8 @@ class WorkbuddyCdpClient {
353
356
  return { text: reply ?? "", attachments };
354
357
  if (reply || forwarded.size)
355
358
  return { text: reply };
359
+ if (signal?.aborted && latest.finished)
360
+ throw new agent_1.AgentOperationError("WORKBUDDY_REQUEST_CANCELLED", "等待 WorkBuddy 取消后的任务结束", new Error("请求已取消,已确认本轮桌面生成结束,未返回正文或附件"));
356
361
  }
357
362
  if (onMessage) {
358
363
  for (const item of candidates) {