mu-agent-bridge 1.0.62 → 1.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src-ts/agent.js +25 -1
- package/dist/src-ts/control-server.js +3 -3
- package/dist/src-ts/daemon-process.js +8 -1
- package/dist/src-ts/plugins/aun/attachments.js +5 -2
- package/dist/src-ts/plugins/aun/index.js +27 -13
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +83 -57
- package/dist/src-ts/plugins/qwenwork/index.js +12 -40
- package/dist/src-ts/plugins/qwenwork/openai-adapter.js +38 -8
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +90 -70
- package/dist/src-ts/plugins/workbuddy/index.js +68 -60
- package/dist/tests-ts/aun-plugin.test.js +25 -3
- package/dist/tests-ts/daemon-process.test.js +48 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +63 -51
- package/dist/tests-ts/qwenwork-sse.test.js +70 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +50 -1
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/agent.js +25 -1
- package/plugins/aun/dist/src-ts/agent.js +25 -1
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +5 -2
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +27 -13
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/agent.js +25 -1
- package/plugins/claude-cli/dist/src-ts/agent.js +25 -1
- package/plugins/codex-cli/dist/src-ts/agent.js +25 -1
- package/plugins/doubao/dist/src-ts/agent.js +25 -1
- package/plugins/dsh/dist/src-ts/agent.js +25 -1
- package/plugins/dsh/dist/src-ts/plugins/qwenwork/openai-adapter.js +38 -8
- package/plugins/feishu/dist/src-ts/agent.js +25 -1
- package/plugins/qwenwork/dist/src-ts/agent.js +25 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +83 -57
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +12 -40
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/openai-adapter.js +38 -8
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/dist/src-ts/agent.js +25 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +90 -70
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +68 -60
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
package/dist/src-ts/agent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AgentError = void 0;
|
|
3
|
+
exports.AgentOperationError = exports.AgentError = void 0;
|
|
4
|
+
exports.errorDetails = errorDetails;
|
|
4
5
|
class AgentError extends Error {
|
|
5
6
|
constructor(message, options) {
|
|
6
7
|
super(message, options);
|
|
@@ -8,3 +9,26 @@ class AgentError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
11
|
exports.AgentError = AgentError;
|
|
12
|
+
/** 保留底层错误码,避免只留下 fetch failed 或超时这类表层提示。 */
|
|
13
|
+
function errorDetails(error) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
for (let current = error; current && !seen.has(current) && seen.size < 5;) {
|
|
17
|
+
seen.add(current);
|
|
18
|
+
const value = current;
|
|
19
|
+
const message = typeof value.message === "string" ? value.message : String(current);
|
|
20
|
+
parts.push(`${message}${value.code ? `(${String(value.code)})` : ""}`);
|
|
21
|
+
current = value.cause;
|
|
22
|
+
}
|
|
23
|
+
return parts.join(";原因:") || String(error);
|
|
24
|
+
}
|
|
25
|
+
class AgentOperationError extends AgentError {
|
|
26
|
+
code;
|
|
27
|
+
stage;
|
|
28
|
+
constructor(code, stage, cause) {
|
|
29
|
+
super(`${code}:${stage}失败:${errorDetails(cause)}`, { cause });
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.stage = stage;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.AgentOperationError = AgentOperationError;
|
|
@@ -169,13 +169,13 @@ class ControlServer {
|
|
|
169
169
|
const id = decodeURIComponent(parts[2]);
|
|
170
170
|
if (["x-evolcore-session-id", "x-aun-peer", "x-evolcore-agent-aid"].some((name) => req.headers[name] !== undefined) && !authorized && !isLoopback(req.socket.remoteAddress))
|
|
171
171
|
return this.json(res, 401, { error: "会话身份仅允许已认证或本机调用方使用" });
|
|
172
|
-
const adapter = new openai_adapter_1.OpenAIAdapter(async (task, _signal, context) => {
|
|
172
|
+
const adapter = new openai_adapter_1.OpenAIAdapter(async (task, _signal, context, onMessage) => {
|
|
173
173
|
if (!context)
|
|
174
|
-
return (await this.options.manager.handle(id, { task })).text;
|
|
174
|
+
return (await this.options.manager.handle(id, { task, ...(onMessage ? { onMessage } : {}) })).text;
|
|
175
175
|
// 客户端标识只在认证调用方或本机信任范围内生效,不能直接恢复其他入口的会话。
|
|
176
176
|
const caller = authorized ? (0, node_crypto_1.createHash)("sha256").update(authorization).digest("hex") : "loopback";
|
|
177
177
|
const sessionId = `openai:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify([caller, id, context.agentAid ?? "", context.peerId ?? "", context.sessionId])).digest("hex")}`;
|
|
178
|
-
return (await this.options.manager.handle(id, { task, sessionId })).text;
|
|
178
|
+
return (await this.options.manager.handle(id, { task, sessionId, ...(onMessage ? { onMessage } : {}) })).text;
|
|
179
179
|
}, { model: id, prefix: `/api/agents/${encodeURIComponent(id)}/v1`, onExchange: (direction, text, peer, aid) => this.options.manager.recordMessage(id, direction, text, peer, aid) });
|
|
180
180
|
await adapter.handle(req, res);
|
|
181
181
|
return;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isDaemonCommand = isDaemonCommand;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
4
5
|
/** 只识别直接运行本安装 CLI 的后台进程,避免旧地址文件中的 PID 被其他程序复用。 */
|
|
5
6
|
function isDaemonCommand(commandLine, cli) {
|
|
6
|
-
const normalize = (value) =>
|
|
7
|
+
const normalize = (value) => {
|
|
8
|
+
try {
|
|
9
|
+
value = node_fs_1.realpathSync.native(value);
|
|
10
|
+
}
|
|
11
|
+
catch { /* 不存在的路径仍按原始路径比较。 */ }
|
|
12
|
+
return value.replace(/[\\/]+/g, "/").toLowerCase();
|
|
13
|
+
};
|
|
7
14
|
const args = [...commandLine.matchAll(/"([^"]*)"|(\S+)/g)].map((match) => normalize(match[1] ?? match[2]));
|
|
8
15
|
return args[1] === normalize(cli) && ["start", "--serve"].includes(args[2]);
|
|
9
16
|
}
|
|
@@ -8,6 +8,7 @@ exports.uploadAunAttachments = uploadAunAttachments;
|
|
|
8
8
|
const node_crypto_1 = require("node:crypto");
|
|
9
9
|
const node_util_1 = require("node:util");
|
|
10
10
|
const attachments_1 = require("../../attachments");
|
|
11
|
+
const agent_1 = require("../../agent");
|
|
11
12
|
const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
|
|
12
13
|
const mimePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
|
|
13
14
|
const imageTypes = new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
@@ -252,13 +253,15 @@ async function uploadAunAttachments(client, ownerAid, peerAid, items) {
|
|
|
252
253
|
// 视频播放器不携带 AID 凭据,沿用 EvolCore 的标准可读对象上传方式。
|
|
253
254
|
const video = contentType.startsWith("video/");
|
|
254
255
|
const ref = { ownerAid, bucket: "default", objectKey, filename, contentType, sizeBytes: item.data.byteLength, sha256 };
|
|
255
|
-
const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video })
|
|
256
|
+
const uploaded = await client.storage.writeBytes(`/${objectKey}`, item.data, { contentType, public: video, overwrite: video })
|
|
257
|
+
.catch(error => { throw new agent_1.AgentOperationError("AUN_FS_UPLOAD_FAILED", `上传 AUN FS 文件“${filename}”`, error); });
|
|
256
258
|
checkedMetadata({ ...uploaded, size: uploaded.size ?? uploaded.size_bytes ?? item.data.byteLength }, ref);
|
|
257
259
|
if (video) {
|
|
258
260
|
result.push({ owner_aid: ownerAid, object_key: objectKey, filename, content_type: contentType, size_bytes: item.data.byteLength, sha256 });
|
|
259
261
|
continue;
|
|
260
262
|
}
|
|
261
|
-
const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 })
|
|
263
|
+
const share = await client.storage.lowlevel.createShareLink({ owner: ownerAid, bucket: "default", objectKey, allowedAids: [peerAid], expireInSeconds: 86400 })
|
|
264
|
+
.catch(error => { throw new agent_1.AgentOperationError("AUN_FS_SHARE_FAILED", `创建 AUN FS 文件“${filename}”的接收方授权`, error); });
|
|
262
265
|
if (share.owner_aid && share.owner_aid !== ownerAid || share.bucket && share.bucket !== "default" || share.object_key && share.object_key !== objectKey)
|
|
263
266
|
throw new Error("AUN 附件分享对象不一致");
|
|
264
267
|
if (share.allowed_aids !== undefined && (!Array.isArray(share.allowed_aids) || share.allowed_aids.length !== 1 || share.allowed_aids[0] !== peerAid))
|
|
@@ -8,10 +8,11 @@ const node_module_1 = require("node:module");
|
|
|
8
8
|
const node_path_1 = require("node:path");
|
|
9
9
|
const node_os_1 = require("node:os");
|
|
10
10
|
const node_url_1 = require("node:url");
|
|
11
|
+
const agent_1 = require("../../agent");
|
|
11
12
|
const messages_1 = require("./messages");
|
|
12
13
|
const attachments_1 = require("./attachments");
|
|
13
14
|
const manifest = {
|
|
14
|
-
id: "aun", version: "1.0.
|
|
15
|
+
id: "aun", version: "1.0.32", type: "channel", displayName: "AUN 通道",
|
|
15
16
|
entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
|
|
16
17
|
capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
|
|
17
18
|
configSchema: "config.schema.json",
|
|
@@ -799,7 +800,9 @@ class AunPlugin {
|
|
|
799
800
|
}
|
|
800
801
|
}
|
|
801
802
|
async processMessage(message, aid, agentPlugin) {
|
|
802
|
-
let
|
|
803
|
+
let deliveryFailed = false;
|
|
804
|
+
const trace = { aid, peer: message.peerId, threadId: message.threadId ?? "", messageId: message.messageId ?? "", agentPlugin };
|
|
805
|
+
this.context?.logger.info("AUN 收到任务", { ...trace, stage: "接收消息", attachments: message.attachments?.length ?? 0 });
|
|
803
806
|
const client = this.clients.get(aid)?.client;
|
|
804
807
|
let attachments = [];
|
|
805
808
|
let uploadedItems;
|
|
@@ -824,9 +827,9 @@ class AunPlugin {
|
|
|
824
827
|
}
|
|
825
828
|
}
|
|
826
829
|
catch (error) {
|
|
827
|
-
this.context?.logger.warn("AUN 附件下载失败", { error: safeError(error), ...(error instanceof attachments_1.AunDownloadUrlError ? { downloadUrl: error.diagnostic } : {}) });
|
|
830
|
+
this.context?.logger.warn("AUN 附件下载失败", { ...trace, stage: "下载入站附件", error: safeError((0, agent_1.errorDetails)(error)), ...(error instanceof attachments_1.AunDownloadUrlError ? { downloadUrl: error.diagnostic } : {}) });
|
|
828
831
|
try {
|
|
829
|
-
await this.sendText(aid, message.peerId,
|
|
832
|
+
await this.sendText(aid, message.peerId, `附件接收失败:下载入站附件失败:${safeError((0, agent_1.errorDetails)(error))}`, message.threadId);
|
|
830
833
|
}
|
|
831
834
|
catch { /* 尽力反馈 */ }
|
|
832
835
|
return;
|
|
@@ -838,13 +841,18 @@ class AunPlugin {
|
|
|
838
841
|
reply: async (text) => {
|
|
839
842
|
if (!text.trim())
|
|
840
843
|
return;
|
|
841
|
-
|
|
842
|
-
|
|
844
|
+
try {
|
|
845
|
+
await this.sendText(aid, message.peerId, text, message.threadId);
|
|
846
|
+
this.context?.logger.info("AUN 文本已发送", { ...trace, stage: "发送文本消息" });
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
deliveryFailed = true;
|
|
850
|
+
throw new agent_1.AgentOperationError("AUN_TEXT_SEND_FAILED", "发送 AUN 文本消息(结果未确认,不自动重发)", error);
|
|
851
|
+
}
|
|
843
852
|
},
|
|
844
853
|
replyAttachments: async (items, text) => {
|
|
845
854
|
if (!items.length)
|
|
846
855
|
return;
|
|
847
|
-
replyAttempted = true;
|
|
848
856
|
let sent = 0;
|
|
849
857
|
let groupsCount = 0;
|
|
850
858
|
let sending = false;
|
|
@@ -852,6 +860,7 @@ class AunPlugin {
|
|
|
852
860
|
if (!client?.storage)
|
|
853
861
|
throw new Error("AUN SDK 不支持附件存储接口");
|
|
854
862
|
const refs = uploadedRefs && uploadedItems === items ? uploadedRefs : await (0, attachments_1.uploadAunAttachments)({ storage: client.storage }, aid, message.peerId, items);
|
|
863
|
+
this.context?.logger.info("AUN FS 文件上传与授权完成", { ...trace, stage: "上传并授权附件", files: items.map(item => ({ filename: item.filename, sizeBytes: item.data.byteLength })) });
|
|
855
864
|
if (!uploadedRefs || uploadedItems !== items) {
|
|
856
865
|
uploadedItems = items;
|
|
857
866
|
uploadedRefs = refs;
|
|
@@ -873,32 +882,37 @@ class AunPlugin {
|
|
|
873
882
|
sending = true;
|
|
874
883
|
// message.send 失败时可能已送达,不自动重试或降级明文,避免重复发送和泄露引用。
|
|
875
884
|
await client.call("message.send", { to: message.peerId, payload, encrypt: true });
|
|
885
|
+
this.context?.logger.info("AUN 附件消息已发送", { ...trace, stage: "发送附件消息", kind, count: group.length });
|
|
876
886
|
sending = false;
|
|
877
887
|
sent++;
|
|
878
888
|
}
|
|
879
889
|
}
|
|
880
890
|
catch (error) {
|
|
891
|
+
deliveryFailed = true;
|
|
892
|
+
const failure = sending ? new agent_1.AgentOperationError("AUN_ATTACHMENT_SEND_FAILED", "发送 AUN 附件消息(FS 上传已完成)", error) : error;
|
|
881
893
|
const progress = sending ? `(已确认发送 ${sent}/${groupsCount} 组,当前组结果未确认,请勿重复发送)` : "";
|
|
882
894
|
try {
|
|
883
|
-
await this.sendText(aid, message.peerId, `附件回传失败${progress}:${safeError(
|
|
895
|
+
await this.sendText(aid, message.peerId, `附件回传失败${progress}:${safeError((0, agent_1.errorDetails)(failure))}`, message.threadId);
|
|
884
896
|
}
|
|
885
897
|
catch { /* 尽力反馈 */ }
|
|
886
|
-
throw
|
|
898
|
+
throw failure;
|
|
887
899
|
}
|
|
888
900
|
},
|
|
889
901
|
};
|
|
890
902
|
try {
|
|
903
|
+
this.context?.logger.info("AUN 开始调用 Agent", { ...trace, stage: "调用 Agent" });
|
|
891
904
|
await this.ingress.publish(channelMessage);
|
|
905
|
+
this.context?.logger.info("AUN 任务处理完成", { ...trace, stage: "回复已发送" });
|
|
892
906
|
}
|
|
893
907
|
catch (error) {
|
|
894
|
-
this.context?.logger.warn("AUN 消息处理失败", { error: safeError(error) });
|
|
895
|
-
if (!
|
|
908
|
+
this.context?.logger.warn("AUN 消息处理失败", { ...trace, stage: deliveryFailed ? "回传回复" : "执行 Agent 任务", error: safeError((0, agent_1.errorDetails)(error)) });
|
|
909
|
+
if (!deliveryFailed) {
|
|
896
910
|
const action = attachments.length > 0 ? "附件处理失败" : newSessionCommand.test(message.text.trim()) ? "新建会话失败" : "消息处理失败";
|
|
897
911
|
try {
|
|
898
|
-
await channelMessage.reply(`${action}:${safeError(error)}`);
|
|
912
|
+
await channelMessage.reply(`${action}:${safeError((0, agent_1.errorDetails)(error))}`);
|
|
899
913
|
}
|
|
900
914
|
catch (replyError) {
|
|
901
|
-
this.context?.logger.warn("AUN 错误回复失败", { error: safeError(replyError) });
|
|
915
|
+
this.context?.logger.warn("AUN 错误回复失败", { ...trace, stage: "发送最终错误消息", error: safeError((0, agent_1.errorDetails)(replyError)) });
|
|
902
916
|
}
|
|
903
917
|
}
|
|
904
918
|
}
|
|
@@ -11,6 +11,7 @@ exports.decodeQwenDataImage = decodeQwenDataImage;
|
|
|
11
11
|
exports.parseQwenFinalFileDetails = parseQwenFinalFileDetails;
|
|
12
12
|
const node_crypto_1 = require("node:crypto");
|
|
13
13
|
const cdp_draft_1 = require("../cdp-draft");
|
|
14
|
+
const agent_1 = require("../../agent");
|
|
14
15
|
const attachments_1 = require("../../attachments");
|
|
15
16
|
exports.assistantMessageSelector = '[data-message-id][data-part-type="text"]:not([data-conversation-user-text="true"])';
|
|
16
17
|
exports.inputSelector = '.chat-input-editor-text[contenteditable="true"]';
|
|
@@ -203,66 +204,87 @@ class QwenCdpClient {
|
|
|
203
204
|
async direct(text, timeoutMs = 240_000, signal, expectedReply) {
|
|
204
205
|
return (await this.complete(text, timeoutMs, signal, expectedReply)).text;
|
|
205
206
|
}
|
|
206
|
-
async complete(text, timeoutMs = 240_000, signal, expectedReply, attachments) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const draft = await this.evaluate(`document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText ?? ''`);
|
|
210
|
-
if (typeof draft === "string" && draft.trim() && !isQwenBridgeHealthPrompt(draft))
|
|
211
|
-
throw new Error("千问输入框有用户草稿,跳过后台健康检测");
|
|
212
|
-
}
|
|
213
|
-
await this.ensureVisible();
|
|
214
|
-
await this.recoverDraft();
|
|
215
|
-
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"]')); })()`);
|
|
216
|
-
if (!ready)
|
|
217
|
-
throw new Error("千问输入框含有未发送内容或正在生成回复,未发送新消息");
|
|
218
|
-
const snapshot = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
|
|
219
|
-
const before = JSON.parse(snapshot || "[]");
|
|
220
|
-
const beforeIds = new Set(before.map((message) => message.id).filter((id) => Boolean(id)));
|
|
221
|
-
const focused = await this.evaluate(`(() => {
|
|
222
|
-
const element = document.querySelector(${JSON.stringify(exports.inputSelector)});
|
|
223
|
-
if (!element) return false;
|
|
224
|
-
element.focus();
|
|
225
|
-
return true;
|
|
226
|
-
})()`);
|
|
227
|
-
if (!focused)
|
|
228
|
-
throw new Error("千问输入框未找到");
|
|
229
|
-
const addedIds = attachments?.length ? await this.injectAttachments(attachments) : [];
|
|
207
|
+
async complete(text, timeoutMs = 240_000, signal, expectedReply, attachments, onMessage) {
|
|
208
|
+
let stage = "准备千问办公桌面任务";
|
|
209
|
+
let code = "QWEN_TASK_FAILED";
|
|
230
210
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
211
|
+
(0, attachments_1.validateAttachments)(attachments);
|
|
212
|
+
if (expectedReply) {
|
|
213
|
+
const draft = await this.evaluate(`document.querySelector(${JSON.stringify(exports.inputSelector)})?.innerText ?? ''`);
|
|
214
|
+
if (typeof draft === "string" && draft.trim() && !isQwenBridgeHealthPrompt(draft))
|
|
215
|
+
throw new Error("千问输入框有用户草稿,跳过后台健康检测");
|
|
216
|
+
}
|
|
217
|
+
await this.ensureVisible();
|
|
218
|
+
await this.recoverDraft();
|
|
219
|
+
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
|
+
if (!ready)
|
|
221
|
+
throw new Error("千问输入框含有未发送内容或正在生成回复,未发送新消息");
|
|
222
|
+
const snapshot = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
|
|
223
|
+
const before = JSON.parse(snapshot || "[]");
|
|
224
|
+
const beforeIds = new Set(before.map((message) => message.id).filter((id) => Boolean(id)));
|
|
225
|
+
const focused = await this.evaluate(`(() => {
|
|
226
|
+
const element = document.querySelector(${JSON.stringify(exports.inputSelector)});
|
|
227
|
+
if (!element) return false;
|
|
228
|
+
element.focus();
|
|
229
|
+
return true;
|
|
230
|
+
})()`);
|
|
231
|
+
if (!focused)
|
|
232
|
+
throw new Error("千问输入框未找到");
|
|
233
|
+
stage = "向千问办公上传入站附件(任务尚未提交)";
|
|
234
|
+
const addedIds = attachments?.length ? await this.injectAttachments(attachments) : [];
|
|
235
|
+
stage = "向千问办公提交任务(尚未确认发送)";
|
|
236
|
+
code = "QWEN_SUBMIT_FAILED";
|
|
239
237
|
try {
|
|
240
|
-
await this.
|
|
238
|
+
await this.cdp("Input.insertText", { text });
|
|
239
|
+
const sent = await this.evaluate(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); const button = document.querySelector('button[aria-label="发送消息"]'); if (!input || input.innerText.trim() !== ${JSON.stringify(text.trim())} || !button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`);
|
|
240
|
+
if (sent !== true)
|
|
241
|
+
throw new Error("千问发送按钮未就绪或输入校验失败,未发送消息");
|
|
242
|
+
await this.waitFor(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); return Boolean(input && !input.innerText.trim()); })()`, "千问未确认消息已提交");
|
|
241
243
|
}
|
|
242
|
-
catch {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
while (Date.now() < deadline) {
|
|
250
|
-
await wait(2_000, signal);
|
|
251
|
-
const messagesJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
|
|
252
|
-
const fresh = freshAssistantMessages(beforeIds, JSON.parse(messagesJson || "[]"));
|
|
253
|
-
const reply = fresh.at(-1)?.text.trim() ?? "";
|
|
254
|
-
stablePolls = reply && reply === previousReply ? stablePolls + 1 : 0;
|
|
255
|
-
previousReply = reply;
|
|
256
|
-
if (reply && stablePolls >= 3 && isQwenApiUnavailableReply(reply))
|
|
257
|
-
throw new Error("千问生成接口不可用");
|
|
258
|
-
const generating = await this.evaluate(`Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]'))`);
|
|
259
|
-
if (!generating && (expectedReply ? isQwenHeartbeatReplyComplete(reply, expectedReply) && stablePolls >= 1 : reply && stablePolls >= 3)) {
|
|
260
|
-
const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
|
|
261
|
-
return { text: reply, ...(attachments.length ? { attachments } : {}) };
|
|
244
|
+
catch (error) {
|
|
245
|
+
// CDP 断开时清理也可能失败,保留最初的发送错误。
|
|
246
|
+
try {
|
|
247
|
+
await this.cleanupAttachments(addedIds);
|
|
248
|
+
}
|
|
249
|
+
catch { /* 清理失败不能覆盖原错。 */ }
|
|
250
|
+
throw error;
|
|
262
251
|
}
|
|
252
|
+
const sessionUrl = String(await this.evaluate("location.href"));
|
|
253
|
+
stage = "等待千问办公回复(任务已提交,桌面未被停止)";
|
|
254
|
+
code = "QWEN_REPLY_FAILED";
|
|
255
|
+
let previousReply = "";
|
|
256
|
+
let stablePolls = 0;
|
|
257
|
+
const forwarded = new Set();
|
|
258
|
+
const deadline = Date.now() + timeoutMs;
|
|
259
|
+
while (Date.now() < deadline) {
|
|
260
|
+
await wait(2_000, signal);
|
|
261
|
+
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 || "[]"));
|
|
263
|
+
const reply = fresh.at(-1)?.text.trim() ?? "";
|
|
264
|
+
stablePolls = reply && reply === previousReply ? stablePolls + 1 : 0;
|
|
265
|
+
previousReply = reply;
|
|
266
|
+
if (reply && stablePolls >= 3 && isQwenApiUnavailableReply(reply))
|
|
267
|
+
throw new Error("QWEN_REMOTE_ERROR:千问生成接口不可用;桌面仅返回通用错误,未提供服务端错误码;CDP 仍连接,未重启或重新提交任务");
|
|
268
|
+
const generating = await this.evaluate(`Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]'))`);
|
|
269
|
+
if (!generating && (expectedReply ? isQwenHeartbeatReplyComplete(reply, expectedReply) && stablePolls >= 1 : reply && stablePolls >= 3)) {
|
|
270
|
+
stage = "读取千问办公已完成任务的产物(尚未上传 AUN FS)";
|
|
271
|
+
code = "QWEN_ARTIFACT_READ_FAILED";
|
|
272
|
+
const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
|
|
273
|
+
return { text: reply, ...(attachments.length ? { attachments } : {}) };
|
|
274
|
+
}
|
|
275
|
+
if (onMessage && generating && reply && stablePolls >= 1 && !forwarded.has(reply) && !isQwenApiUnavailableReply(reply)) {
|
|
276
|
+
await onMessage(reply);
|
|
277
|
+
forwarded.add(reply);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const permission = await this.permissionStatus(sessionUrl);
|
|
281
|
+
throw new Error(permission ?? `QWEN_REPLY_TIMEOUT:千问办公 UI 超时未返回回复(已等待 ${Math.round(timeoutMs / 1000)} 秒);桌面任务未被停止`);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (error instanceof agent_1.AgentOperationError)
|
|
285
|
+
throw error;
|
|
286
|
+
throw new agent_1.AgentOperationError(code, stage, error);
|
|
263
287
|
}
|
|
264
|
-
const permission = await this.permissionStatus(sessionUrl);
|
|
265
|
-
throw new Error(permission ?? `QWEN_REPLY_TIMEOUT:千问办公 UI 超时未返回回复(已等待 ${Math.round(timeoutMs / 1000)} 秒);桌面任务未被停止`);
|
|
266
288
|
}
|
|
267
289
|
async permissionStatus(sessionUrl) {
|
|
268
290
|
try {
|
|
@@ -528,8 +550,12 @@ class QwenCdpClient {
|
|
|
528
550
|
}
|
|
529
551
|
async evaluate(expression) {
|
|
530
552
|
const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
531
|
-
if (response.
|
|
532
|
-
throw new Error(
|
|
553
|
+
if (response.error)
|
|
554
|
+
throw new Error(`千问 CDP Runtime.evaluate 失败:${response.error.message ?? response.error.code}`);
|
|
555
|
+
if (response.result?.exceptionDetails) {
|
|
556
|
+
const details = response.result.exceptionDetails;
|
|
557
|
+
throw new Error(`千问页面 JS 异常:${details.exception?.description ?? details.text ?? JSON.stringify(details)}`);
|
|
558
|
+
}
|
|
533
559
|
return response.result?.result?.value;
|
|
534
560
|
}
|
|
535
561
|
async trpc(path, type, input) {
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QwenworkPlugin = exports.qwenworkManifest = void 0;
|
|
4
4
|
exports.createQwenworkPlugin = createQwenworkPlugin;
|
|
5
|
-
const
|
|
6
|
-
const node_util_1 = require("node:util");
|
|
5
|
+
const agent_1 = require("../../agent");
|
|
7
6
|
const openai_adapter_1 = require("./openai-adapter");
|
|
8
7
|
const cdp_client_1 = require("./cdp-client");
|
|
9
8
|
const plugin_1 = require("../../plugin");
|
|
@@ -11,7 +10,7 @@ const desktop_launch_1 = require("../desktop-launch");
|
|
|
11
10
|
const session_history_1 = require("./session-history");
|
|
12
11
|
exports.qwenworkManifest = {
|
|
13
12
|
id: "qwenwork",
|
|
14
|
-
version: "1.0.
|
|
13
|
+
version: "1.0.23",
|
|
15
14
|
displayName: "千问办公",
|
|
16
15
|
entry: "dist/index.js",
|
|
17
16
|
apiVersion: "1",
|
|
@@ -43,10 +42,8 @@ class QwenworkPlugin {
|
|
|
43
42
|
state = "stopped";
|
|
44
43
|
message = "";
|
|
45
44
|
healthIntervalMs;
|
|
46
|
-
restartDesktop;
|
|
47
45
|
healthTimer;
|
|
48
46
|
recovery;
|
|
49
|
-
restart;
|
|
50
47
|
context;
|
|
51
48
|
checking = false;
|
|
52
49
|
failures = 0;
|
|
@@ -61,7 +58,6 @@ class QwenworkPlugin {
|
|
|
61
58
|
this.configuredCdpPort = options.cdpPort;
|
|
62
59
|
this.configuredHttpPort = options.httpPort;
|
|
63
60
|
this.healthIntervalMs = options.healthIntervalMs ?? 30_000;
|
|
64
|
-
this.restartDesktop = options.restartDesktop;
|
|
65
61
|
}
|
|
66
62
|
async activate(context) {
|
|
67
63
|
if (this.state === "ready")
|
|
@@ -100,26 +96,6 @@ class QwenworkPlugin {
|
|
|
100
96
|
workspace: typeof config.workspace === "string" ? config.workspace : undefined,
|
|
101
97
|
});
|
|
102
98
|
this.client = client;
|
|
103
|
-
this.restart = async () => {
|
|
104
|
-
if (config.autoStart === false)
|
|
105
|
-
throw new Error("千问自动启动已禁用");
|
|
106
|
-
if (process.platform !== "win32")
|
|
107
|
-
throw new Error("仅支持在 Windows 上自动重启千问办公");
|
|
108
|
-
const executable = await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", typeof config.executablePath === "string" ? config.executablePath : undefined);
|
|
109
|
-
if (!executable)
|
|
110
|
-
throw new Error("未找到 QwenWorkCN.exe,请配置 executablePath");
|
|
111
|
-
context.logger?.info("正在自动重启千问办公", { executablePath: executable });
|
|
112
|
-
await (0, node_util_1.promisify)(node_child_process_1.execFile)("taskkill", ["/IM", "QwenWorkCN.exe", "/T", "/F"], { timeout: 10_000 }).catch(() => undefined);
|
|
113
|
-
await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${this.activeCdpPort}`], async () => {
|
|
114
|
-
try {
|
|
115
|
-
await client.connect();
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
client.close();
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
}, asNumber(config.startupTimeoutMs, 60_000));
|
|
122
|
-
};
|
|
123
99
|
try {
|
|
124
100
|
await client.connect();
|
|
125
101
|
}
|
|
@@ -138,7 +114,7 @@ class QwenworkPlugin {
|
|
|
138
114
|
}
|
|
139
115
|
}
|
|
140
116
|
}
|
|
141
|
-
this.adapter = new openai_adapter_1.OpenAIAdapter((prompt, signal) => this.enqueueTask(() => this.complete(prompt, asNumber(config.timeoutMs, 240_000), signal)), {
|
|
117
|
+
this.adapter = new openai_adapter_1.OpenAIAdapter((prompt, signal, _context, onMessage) => this.enqueueTask(() => this.complete(prompt, asNumber(config.timeoutMs, 240_000), signal, onMessage)), {
|
|
142
118
|
onLog: (message) => context.logger?.info(message),
|
|
143
119
|
onExchange: (direction, text, peer, agentAid) => context.logger?.info("千问通信", { direction, text, peer, agentAid }),
|
|
144
120
|
});
|
|
@@ -201,7 +177,7 @@ class QwenworkPlugin {
|
|
|
201
177
|
return { text: "千问办公已创建新会话", sessionId };
|
|
202
178
|
});
|
|
203
179
|
}
|
|
204
|
-
async complete(prompt, timeoutMs, signal) {
|
|
180
|
+
async complete(prompt, timeoutMs, signal, onMessage) {
|
|
205
181
|
if (this.relayTarget) {
|
|
206
182
|
const response = await this.callPlugin(this.relayTarget, { task: prompt, timeoutMs });
|
|
207
183
|
return response.text;
|
|
@@ -211,9 +187,9 @@ class QwenworkPlugin {
|
|
|
211
187
|
try {
|
|
212
188
|
if (!this.client.connected())
|
|
213
189
|
throw new Error("千问 CDP 已断开,等待空闲后自动恢复");
|
|
214
|
-
const reply = await this.client.direct(prompt, timeoutMs, signal);
|
|
190
|
+
const reply = this.client.complete ? (await this.client.complete(prompt, timeoutMs, signal, undefined, undefined, onMessage)).text : await this.client.direct(prompt, timeoutMs, signal);
|
|
215
191
|
if ((0, cdp_client_1.isQwenApiUnavailableReply)(reply))
|
|
216
|
-
throw new Error("
|
|
192
|
+
throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
|
|
217
193
|
this.failures = 0;
|
|
218
194
|
this.state = "ready";
|
|
219
195
|
this.message = "";
|
|
@@ -240,10 +216,10 @@ class QwenworkPlugin {
|
|
|
240
216
|
if (request.attachments?.length && (!this.client.attachmentInput || !this.client.complete))
|
|
241
217
|
throw new Error("千问当前客户端不支持附件输入");
|
|
242
218
|
const response = this.client.complete
|
|
243
|
-
? await this.client.complete(request.task, request.timeoutMs ?? 240_000, undefined, undefined, request.attachments)
|
|
219
|
+
? await this.client.complete(request.task, request.timeoutMs ?? 240_000, undefined, undefined, request.attachments, request.onMessage)
|
|
244
220
|
: { text: await this.complete(request.task, request.timeoutMs ?? 240_000) };
|
|
245
221
|
if ((0, cdp_client_1.isQwenApiUnavailableReply)(response.text))
|
|
246
|
-
throw new Error("
|
|
222
|
+
throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
|
|
247
223
|
this.failures = 0;
|
|
248
224
|
this.state = "ready";
|
|
249
225
|
this.message = "";
|
|
@@ -286,7 +262,7 @@ class QwenworkPlugin {
|
|
|
286
262
|
this.pluginStatus = undefined;
|
|
287
263
|
}
|
|
288
264
|
startHealthWatch() {
|
|
289
|
-
if (this.relayTarget ||
|
|
265
|
+
if (this.relayTarget || !this.client || this.healthTimer)
|
|
290
266
|
return;
|
|
291
267
|
this.healthTimer = setInterval(() => { void this.checkHealth(); }, this.healthIntervalMs);
|
|
292
268
|
this.healthTimer.unref();
|
|
@@ -316,12 +292,8 @@ class QwenworkPlugin {
|
|
|
316
292
|
return;
|
|
317
293
|
this.recovery = (async () => {
|
|
318
294
|
this.state = "starting";
|
|
319
|
-
this.context?.logger?.info("
|
|
295
|
+
this.context?.logger?.info("千问 CDP 自动重连开始", { reason: this.message });
|
|
320
296
|
client.close();
|
|
321
|
-
if (this.restartDesktop)
|
|
322
|
-
await this.restartDesktop();
|
|
323
|
-
else if (this.restart)
|
|
324
|
-
await this.restart();
|
|
325
297
|
await client.connect();
|
|
326
298
|
await this.verifyConnection(client);
|
|
327
299
|
if (this.stopped) {
|
|
@@ -332,13 +304,13 @@ class QwenworkPlugin {
|
|
|
332
304
|
this.message = "";
|
|
333
305
|
this.failures = 0;
|
|
334
306
|
this.restarts += 1;
|
|
335
|
-
this.context?.logger?.info("
|
|
307
|
+
this.context?.logger?.info("千问 CDP 自动重连成功", { restarts: this.restarts });
|
|
336
308
|
})().catch((error) => {
|
|
337
309
|
if (this.stopped)
|
|
338
310
|
return;
|
|
339
311
|
this.state = "failed";
|
|
340
312
|
this.message = error instanceof Error ? error.message : String(error);
|
|
341
|
-
this.context?.logger?.error(
|
|
313
|
+
this.context?.logger?.error(`千问 CDP 重连失败,桌面未重启,下次空闲检查重试:${this.message}`);
|
|
342
314
|
}).finally(() => { this.recovery = undefined; });
|
|
343
315
|
return this.recovery;
|
|
344
316
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OpenAIAdapter = void 0;
|
|
4
4
|
exports.messageText = messageText;
|
|
5
|
+
const agent_1 = require("../../agent");
|
|
5
6
|
const node_http_1 = require("node:http");
|
|
6
7
|
const node_crypto_1 = require("node:crypto");
|
|
7
8
|
function json(res, status, value) {
|
|
@@ -88,6 +89,9 @@ class OpenAIAdapter {
|
|
|
88
89
|
const onClose = () => { if (!res.writableEnded)
|
|
89
90
|
abort.abort(); };
|
|
90
91
|
res.once("close", onClose);
|
|
92
|
+
let heartbeat;
|
|
93
|
+
let emit;
|
|
94
|
+
let finish;
|
|
91
95
|
try {
|
|
92
96
|
const payload = JSON.parse(await readBody(req));
|
|
93
97
|
const lastUser = [...(payload.messages ?? [])].reverse().find((message) => message.role === "user");
|
|
@@ -99,21 +103,37 @@ class OpenAIAdapter {
|
|
|
99
103
|
this.onLog?.(`chat request model=${payload.model ?? this.model} stream=${Boolean(payload.stream)}`);
|
|
100
104
|
this.onExchange?.("request", prompt, peerId, agentAid);
|
|
101
105
|
const hasSessionContext = typeof session === "string" || peerId !== undefined || agentAid !== undefined;
|
|
102
|
-
const content = await this.complete(prompt, abort.signal, hasSessionContext ? { sessionId: typeof session === "string" ? session : "", peerId, agentAid } : undefined);
|
|
103
|
-
this.onExchange?.("reply", content, peerId, agentAid);
|
|
104
|
-
if (abort.signal.aborted || res.destroyed)
|
|
105
|
-
return;
|
|
106
106
|
const now = Math.floor(Date.now() / 1000);
|
|
107
107
|
const id = `chatcmpl-${(0, node_crypto_1.randomUUID)()}`;
|
|
108
108
|
const model = payload.model ?? this.model;
|
|
109
|
+
const forwarded = new Set();
|
|
109
110
|
if (payload.stream) {
|
|
110
111
|
res.statusCode = 200;
|
|
111
112
|
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
112
113
|
res.setHeader("Cache-Control", "no-cache");
|
|
113
114
|
res.setHeader("Connection", "keep-alive");
|
|
114
|
-
res.
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
res.flushHeaders();
|
|
116
|
+
const chunk = (delta, finish_reason) => {
|
|
117
|
+
if (!abort.signal.aborted && !res.destroyed)
|
|
118
|
+
res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created: now, model, choices: [{ index: 0, delta, finish_reason }] })}\n\n`);
|
|
119
|
+
};
|
|
120
|
+
emit = text => { if (text.trim() && !forwarded.has(text)) {
|
|
121
|
+
chunk({ role: "assistant", content: `${forwarded.size ? "\n\n" : ""}${text}` }, null);
|
|
122
|
+
forwarded.add(text);
|
|
123
|
+
} };
|
|
124
|
+
finish = () => { chunk({}, "stop"); res.end("data: [DONE]\n\n"); };
|
|
125
|
+
// 只保持 HTTP 连接,不探测或操作桌面任务。
|
|
126
|
+
heartbeat = setInterval(() => { if (!res.destroyed && !abort.signal.aborted)
|
|
127
|
+
res.write(": keep-alive\n\n"); }, 15_000);
|
|
128
|
+
heartbeat.unref();
|
|
129
|
+
}
|
|
130
|
+
const content = await this.complete(prompt, abort.signal, hasSessionContext ? { sessionId: typeof session === "string" ? session : "", peerId, agentAid } : undefined, payload.stream ? async (text) => { emit?.(text); } : undefined);
|
|
131
|
+
this.onExchange?.("reply", content, peerId, agentAid);
|
|
132
|
+
if (abort.signal.aborted || res.destroyed)
|
|
133
|
+
return;
|
|
134
|
+
if (emit && finish) {
|
|
135
|
+
emit(content);
|
|
136
|
+
finish();
|
|
117
137
|
return;
|
|
118
138
|
}
|
|
119
139
|
json(res, 200, {
|
|
@@ -126,9 +146,19 @@ class OpenAIAdapter {
|
|
|
126
146
|
this.onExchange?.("error", error instanceof Error ? error.message : String(error), peerId, agentAid);
|
|
127
147
|
if (abort.signal.aborted || res.destroyed)
|
|
128
148
|
return;
|
|
129
|
-
|
|
149
|
+
const details = (0, agent_1.errorDetails)(error);
|
|
150
|
+
if (emit && finish) {
|
|
151
|
+
emit(`任务处理失败:${details}`);
|
|
152
|
+
finish();
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const operation = error;
|
|
156
|
+
json(res, operation.stage ? 422 : 500, { error: { message: details, type: operation.stage ? "agent_task_error" : "server_error", ...(operation.code ? { code: operation.code } : {}) } });
|
|
157
|
+
}
|
|
130
158
|
}
|
|
131
159
|
finally {
|
|
160
|
+
if (heartbeat)
|
|
161
|
+
clearInterval(heartbeat);
|
|
132
162
|
res.off("close", onClose);
|
|
133
163
|
}
|
|
134
164
|
}
|