mu-agent-bridge 1.0.71 → 1.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/src-ts/agent.js +35 -0
- package/dist/src-ts/channel-sessions.js +3 -2
- package/dist/src-ts/cli.js +67 -37
- package/dist/src-ts/control-server.js +12 -30
- package/dist/src-ts/core-updater.js +141 -33
- package/dist/src-ts/daemon-process.js +2 -1
- package/dist/src-ts/dsh-agent.js +31 -13
- package/dist/src-ts/http-agent.js +36 -0
- package/dist/src-ts/plugin-catalog.js +11 -2
- package/dist/src-ts/plugin-manager.js +351 -50
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +73 -5
- package/dist/src-ts/plugins/abweb/public/index.html +15 -0
- package/dist/src-ts/plugins/abweb/public/style.css +4 -0
- package/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/dist/src-ts/plugins/aun/index.js +155 -21
- package/dist/src-ts/plugins/aun/messages.js +4 -1
- package/dist/src-ts/plugins/aun/public/app.js +2 -1
- package/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
- package/dist/src-ts/plugins/chatgpt/index.js +80 -19
- package/dist/src-ts/plugins/claude-cli/index.js +19 -2
- package/dist/src-ts/plugins/codex-cli/index.js +19 -2
- package/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
- package/dist/src-ts/plugins/doubao/index.js +152 -8
- package/dist/src-ts/plugins/doubao/native-task.js +134 -0
- package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
- package/dist/src-ts/plugins/dsh/index.js +172 -21
- package/dist/src-ts/plugins/dsh/session-history.js +31 -3
- package/dist/src-ts/plugins/feishu/index.js +1 -1
- package/dist/src-ts/plugins/local-cli/agent.js +74 -9
- package/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
- package/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
- package/dist/src-ts/plugins/personal-assistant/index.js +263 -0
- package/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
- package/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
- package/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
- package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
- package/dist/src-ts/plugins/qwenwork/index.js +139 -24
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
- package/dist/src-ts/plugins/workbuddy/index.js +124 -41
- package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
- package/dist/src-ts/plugins/zcode/index.js +97 -0
- package/dist/src-ts/session-management.js +132 -14
- package/dist/src-ts/version-manager.js +6 -3
- package/dist/tests-ts/abweb-cli.test.js +10 -2
- package/dist/tests-ts/abweb-model-config.test.js +73 -0
- package/dist/tests-ts/agent.test.js +27 -0
- package/dist/tests-ts/aun-attachments.test.js +19 -17
- package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
- package/dist/tests-ts/aun-messages.test.js +20 -0
- package/dist/tests-ts/aun-plugin.test.js +292 -19
- package/dist/tests-ts/bundled-package.test.js +12 -2
- package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
- package/dist/tests-ts/channel-sessions.test.js +27 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +118 -0
- package/dist/tests-ts/control-server.test.js +56 -0
- package/dist/tests-ts/core-updater.test.js +215 -46
- package/dist/tests-ts/daemon-process.test.js +126 -1
- package/dist/tests-ts/desktop-launch.test.js +73 -0
- package/dist/tests-ts/doubao-native-task.test.js +237 -0
- package/dist/tests-ts/doubao-plugin.test.js +773 -12
- package/dist/tests-ts/dsh-agent.test.js +157 -8
- package/dist/tests-ts/dsh-cdp.test.js +148 -0
- package/dist/tests-ts/dsh-history.test.js +37 -0
- package/dist/tests-ts/feishu-plugin.test.js +1 -1
- package/dist/tests-ts/http-agent.test.js +18 -0
- package/dist/tests-ts/installer-assets.test.js +1 -1
- package/dist/tests-ts/local-cli.test.js +146 -1
- package/dist/tests-ts/model-provider-cancellation.test.js +152 -0
- package/dist/tests-ts/native-session-management.test.js +8 -7
- package/dist/tests-ts/personal-assistant-coordination.test.js +92 -0
- package/dist/tests-ts/personal-assistant-extensions.test.js +35 -0
- package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
- package/dist/tests-ts/personal-assistant-owner-context.test.js +107 -0
- package/dist/tests-ts/personal-assistant-remote-ability.test.js +123 -0
- package/dist/tests-ts/personal-assistant.test.js +663 -0
- package/dist/tests-ts/platform-integration.test.js +183 -1
- package/dist/tests-ts/plugin-catalog.test.js +12 -2
- package/dist/tests-ts/plugin-manager.test.js +356 -4
- package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
- package/dist/tests-ts/publish-packages.test.js +42 -0
- package/dist/tests-ts/qwenwork-plugin.test.js +135 -1
- package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
- package/dist/tests-ts/qwenwork-sse.test.js +33 -0
- package/dist/tests-ts/session-access-defaults.test.js +112 -0
- package/dist/tests-ts/session-management.test.js +159 -4
- package/dist/tests-ts/version-manager.test.js +40 -0
- package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +499 -4
- package/dist/tests-ts/zcode-cdp.test.js +894 -0
- package/dist/tests-ts/zcode-plugin.test.js +396 -0
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/agent.js +35 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +73 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +57 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +15 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +4 -0
- package/plugins/abweb/package.json +1 -1
- package/plugins/abweb/plugin.json +1 -1
- package/plugins/aun/config.schema.json +1 -1
- package/plugins/aun/dist/src-ts/agent.js +35 -0
- package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +155 -21
- package/plugins/aun/dist/src-ts/plugins/aun/messages.js +4 -1
- package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
- package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
- package/plugins/aun/package.json +1 -1
- package/plugins/aun/plugin.json +1 -1
- package/plugins/chatgpt/dist/src-ts/agent.js +35 -0
- package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +80 -19
- package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +1 -1
- package/plugins/claude-cli/dist/src-ts/agent.js +35 -0
- package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +19 -2
- package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
- package/plugins/claude-cli/package.json +1 -1
- package/plugins/claude-cli/plugin.json +3 -2
- package/plugins/codex-cli/dist/src-ts/agent.js +35 -0
- package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +19 -2
- package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
- package/plugins/codex-cli/package.json +1 -1
- package/plugins/codex-cli/plugin.json +3 -2
- package/plugins/doubao/config.schema.json +2 -1
- package/plugins/doubao/dist/src-ts/agent.js +35 -0
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +152 -8
- package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +3 -2
- package/plugins/dsh/config.schema.json +5 -2
- package/plugins/dsh/dist/src-ts/agent.js +35 -0
- package/plugins/dsh/dist/src-ts/dsh-agent.js +31 -13
- package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +172 -21
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/plugin.json +3 -2
- package/plugins/feishu/dist/src-ts/agent.js +35 -0
- package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
- package/plugins/feishu/package.json +1 -1
- package/plugins/feishu/plugin.json +1 -1
- package/plugins/personal-assistant/config.schema.json +23 -0
- package/plugins/personal-assistant/dist/src-ts/agent.js +69 -0
- package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
- package/plugins/personal-assistant/dist/src-ts/plugin.js +13 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +263 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
- package/plugins/personal-assistant/package.json +39 -0
- package/plugins/personal-assistant/plugin.json +15 -0
- package/plugins/qwenwork/dist/src-ts/agent.js +35 -0
- package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
- package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +139 -24
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +3 -2
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/agent.js +35 -0
- package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +124 -41
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +3 -2
- package/plugins/zcode/config.schema.json +13 -0
- package/plugins/zcode/dist/src-ts/agent.js +69 -0
- package/plugins/zcode/dist/src-ts/attachments.js +62 -0
- package/plugins/zcode/dist/src-ts/plugin.js +13 -0
- package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
- package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
- package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
- package/plugins/zcode/package.json +16 -0
- package/plugins/zcode/plugin.json +11 -0
|
@@ -16,8 +16,8 @@ const node_path_1 = require("node:path");
|
|
|
16
16
|
const node_util_1 = require("node:util");
|
|
17
17
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
18
18
|
const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
|
|
19
|
-
/**
|
|
20
|
-
async function findWindowsExecutable(fileName, configured) {
|
|
19
|
+
/** 先从 Windows 注册信息和开始菜单解析,失败时再查常见安装目录。 */
|
|
20
|
+
async function findWindowsExecutable(fileName, configured, run = execFileAsync) {
|
|
21
21
|
if (configured?.trim()) {
|
|
22
22
|
const configuredPath = (0, node_path_1.resolve)(configured);
|
|
23
23
|
if (fileName.toLowerCase() === "doubao.exe") {
|
|
@@ -42,12 +42,16 @@ async function findWindowsExecutable(fileName, configured) {
|
|
|
42
42
|
}
|
|
43
43
|
if (process.platform !== "win32")
|
|
44
44
|
return undefined;
|
|
45
|
-
const
|
|
45
|
+
const registered = await findRegisteredWindowsExecutable(fileName, run);
|
|
46
|
+
if (registered)
|
|
47
|
+
return registered;
|
|
48
|
+
const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh|zcode/i;
|
|
46
49
|
const roots = [...new Set([
|
|
47
50
|
process.env.DSH_INSTALL_DIR,
|
|
48
51
|
"D:\\Programs\\DeepSeek Harness",
|
|
49
52
|
"D:\\DSH",
|
|
50
53
|
process.env.LOCALAPPDATA && (0, node_path_1.join)(process.env.LOCALAPPDATA, "Programs"),
|
|
54
|
+
process.env.LOCALAPPDATA,
|
|
51
55
|
process.env.ProgramFiles,
|
|
52
56
|
process.env["ProgramFiles(x86)"],
|
|
53
57
|
...Array.from({ length: 24 }, (_, index) => `${String.fromCharCode(67 + index)}:\\`),
|
|
@@ -59,6 +63,36 @@ async function findWindowsExecutable(fileName, configured) {
|
|
|
59
63
|
}
|
|
60
64
|
return undefined;
|
|
61
65
|
}
|
|
66
|
+
async function findRegisteredWindowsExecutable(fileName, run) {
|
|
67
|
+
const target = fileName.replace(/'/g, "''");
|
|
68
|
+
const script = `$ErrorActionPreference='SilentlyContinue';[Console]::OutputEncoding=[Text.Encoding]::UTF8;$target='${target}'
|
|
69
|
+
function Emit-Path($value){if(-not $value){return};$value=[Environment]::ExpandEnvironmentVariables(([string]$value).Trim());if($value.StartsWith('"')){$end=$value.IndexOf('"',1);if($end -gt 1){$value=$value.Substring(1,$end-1)}}elseif($value -match '^(\S+?\.exe)(?:\s|$)'){$value=$Matches[1]};if($value -match '(?i)\.exe$'){[Console]::WriteLine($value)}}
|
|
70
|
+
$keys=@("HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\$target","HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\$target","HKLM:\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\$target","HKCU:\\Software\\Classes\\Applications\\$target\\shell\\open\\command","HKLM:\\Software\\Classes\\Applications\\$target\\shell\\open\\command")
|
|
71
|
+
foreach($key in $keys){$item=Get-Item $key -ErrorAction SilentlyContinue;if($item){Emit-Path $item.GetValue('')}}
|
|
72
|
+
$dirs=@();if($env:APPDATA){$dirs+=[IO.Path]::Combine($env:APPDATA,'Microsoft\\Windows\\Start Menu\\Programs')};if($env:ProgramData){$dirs+=[IO.Path]::Combine($env:ProgramData,'Microsoft\\Windows\\Start Menu\\Programs')};$shell=New-Object -ComObject WScript.Shell
|
|
73
|
+
foreach($dir in $dirs){Get-ChildItem $dir -Filter '*.lnk' -File -Recurse -ErrorAction SilentlyContinue|ForEach-Object{$link=$shell.CreateShortcut($_.FullName);if($link.TargetPath -and [IO.Path]::GetFileName($link.TargetPath) -ieq $target){Emit-Path $link.TargetPath}}}`;
|
|
74
|
+
try {
|
|
75
|
+
const { stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { timeout: 10_000, windowsHide: true });
|
|
76
|
+
for (const candidate of stdout.split(/\r?\n/).map(value => value.trim()).filter(Boolean)) {
|
|
77
|
+
const found = await resolveRegisteredCandidate(fileName, candidate);
|
|
78
|
+
if (found)
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* 注册表或开始菜单不可读时继续使用目录兜底 */ }
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
async function resolveRegisteredCandidate(fileName, candidate) {
|
|
86
|
+
const candidatePath = (0, node_path_1.resolve)(candidate.replace(/^"|"$/g, ""));
|
|
87
|
+
if (fileName.toLowerCase() === "doubao.exe" && (0, node_path_1.basename)((0, node_path_1.dirname)(candidatePath)).toLowerCase() === "application") {
|
|
88
|
+
const client = (0, node_path_1.join)((0, node_path_1.dirname)(candidatePath), "app", fileName);
|
|
89
|
+
if ((0, node_fs_1.existsSync)(client))
|
|
90
|
+
return client;
|
|
91
|
+
}
|
|
92
|
+
if ((0, node_fs_1.existsSync)(candidatePath) && (0, node_path_1.basename)(candidatePath).toLowerCase() === fileName.toLowerCase())
|
|
93
|
+
return candidatePath;
|
|
94
|
+
return findExecutableInTree((0, node_path_1.dirname)(candidatePath), fileName, /^/, 3);
|
|
95
|
+
}
|
|
62
96
|
async function findExecutableInTree(directory, fileName, pattern, depth) {
|
|
63
97
|
return search(directory, fileName, pattern, depth, pattern.test(directory));
|
|
64
98
|
}
|
|
@@ -104,9 +138,16 @@ async function launchManagedAndWait(executable, args, profileDirectory, connect,
|
|
|
104
138
|
if (profileDirectory)
|
|
105
139
|
await (0, promises_1.mkdir)(profileDirectory, { recursive: true });
|
|
106
140
|
const child = launch(executable, profileDirectory ? [...args, `--user-data-dir=${profileDirectory}`] : args);
|
|
141
|
+
// 先等待 spawn,避免异步 error 终止 daemon 或在启动失败后继续轮询。
|
|
142
|
+
const events = child;
|
|
143
|
+
const started = typeof events.once === "function" ? new Promise((resolve, reject) => {
|
|
144
|
+
events.once("error", reject);
|
|
145
|
+
events.once("spawn", resolve);
|
|
146
|
+
}) : Promise.resolve();
|
|
107
147
|
child.unref();
|
|
108
148
|
onProcess(child);
|
|
109
149
|
try {
|
|
150
|
+
await started;
|
|
110
151
|
return await waitFor(connect, timeoutMs, 250);
|
|
111
152
|
}
|
|
112
153
|
catch (error) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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("千问输入框含有未发送内容或正在生成回复,未发送新消息");
|
|
@@ -234,15 +256,45 @@ class QwenCdpClient {
|
|
|
234
256
|
const addedIds = attachments?.length ? await this.injectAttachments(attachments) : [];
|
|
235
257
|
stage = "向千问办公提交任务(尚未确认发送)";
|
|
236
258
|
code = "QWEN_SUBMIT_FAILED";
|
|
259
|
+
let submitUrl = "";
|
|
260
|
+
let inserted = false;
|
|
261
|
+
let submitAttempted = false;
|
|
237
262
|
try {
|
|
263
|
+
submitUrl = String(await this.evaluate("location.href"));
|
|
264
|
+
const readyToSubmit = `(() => {
|
|
265
|
+
const input = document.querySelector(${JSON.stringify(exports.inputSelector)});
|
|
266
|
+
const button = document.querySelector('button[aria-label="发送消息"]');
|
|
267
|
+
const generating = document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]');
|
|
268
|
+
return Boolean(!document.hidden && location.href === ${JSON.stringify(submitUrl)} && input && input.innerText.trim() === ${JSON.stringify(text.trim())}
|
|
269
|
+
&& button && !button.disabled && button.getAttribute('aria-disabled') !== 'true' && !generating);
|
|
270
|
+
})()`;
|
|
238
271
|
await this.cdp("Input.insertText", { text });
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
272
|
+
inserted = true;
|
|
273
|
+
// 只有复查确认尚未点击时才再试一次,不能重发提交结果未知的任务。
|
|
274
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
275
|
+
await this.waitFor(readyToSubmit, "千问发送按钮未就绪或输入校验失败,未发送消息");
|
|
276
|
+
submitAttempted = true;
|
|
277
|
+
const sent = await this.evaluate(`(() => {
|
|
278
|
+
if (!${readyToSubmit}) return false;
|
|
279
|
+
document.querySelector('button[aria-label="发送消息"]').click();
|
|
280
|
+
return true;
|
|
281
|
+
})()`);
|
|
282
|
+
if (sent === true)
|
|
283
|
+
break;
|
|
284
|
+
submitAttempted = false;
|
|
285
|
+
if (attempt === 1)
|
|
286
|
+
throw new Error("千问发送按钮未就绪或输入校验失败,未发送消息");
|
|
287
|
+
}
|
|
242
288
|
await this.waitFor(`(() => { const input = document.querySelector(${JSON.stringify(exports.inputSelector)}); return Boolean(input && !input.innerText.trim()); })()`, "千问未确认消息已提交");
|
|
243
289
|
}
|
|
244
290
|
catch (error) {
|
|
245
291
|
// CDP 断开时清理也可能失败,保留最初的发送错误。
|
|
292
|
+
if (inserted && !submitAttempted) {
|
|
293
|
+
try {
|
|
294
|
+
await this.recoverDraft({ text, url: submitUrl });
|
|
295
|
+
}
|
|
296
|
+
catch { /* 清理失败不能覆盖原错。 */ }
|
|
297
|
+
}
|
|
246
298
|
try {
|
|
247
299
|
await this.cleanupAttachments(addedIds);
|
|
248
300
|
}
|
|
@@ -254,23 +306,38 @@ class QwenCdpClient {
|
|
|
254
306
|
code = "QWEN_REPLY_FAILED";
|
|
255
307
|
let previousReply = "";
|
|
256
308
|
let stablePolls = 0;
|
|
309
|
+
let seenGenerating = false;
|
|
310
|
+
let stoppedPolls = 0;
|
|
257
311
|
const forwarded = new Set();
|
|
258
312
|
const deadline = Date.now() + timeoutMs;
|
|
259
|
-
while (Date.now() < deadline) {
|
|
260
|
-
|
|
313
|
+
while (Date.now() < deadline || signal?.aborted) {
|
|
314
|
+
// 上游取消不停止桌面生成;确认结束前保留插件队列。
|
|
315
|
+
await wait(2_000);
|
|
316
|
+
await this.restoreVisible();
|
|
261
317
|
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
|
|
318
|
+
const messages = JSON.parse(messagesJson || "[]");
|
|
319
|
+
const fresh = freshAssistantMessages(beforeIds, messages);
|
|
263
320
|
const reply = fresh.at(-1)?.text.trim() ?? "";
|
|
264
321
|
stablePolls = reply && reply === previousReply ? stablePolls + 1 : 0;
|
|
265
322
|
previousReply = reply;
|
|
266
323
|
if (reply && stablePolls >= 3 && isQwenApiUnavailableReply(reply))
|
|
267
324
|
throw new Error("QWEN_REMOTE_ERROR:千问生成接口不可用;桌面仅返回通用错误,未提供服务端错误码;CDP 仍连接,未重启或重新提交任务");
|
|
268
325
|
const generating = await this.evaluate(`Boolean(document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]'))`);
|
|
326
|
+
seenGenerating ||= generating === true;
|
|
327
|
+
stoppedPolls = seenGenerating && generating === false ? stoppedPolls + 1 : 0;
|
|
328
|
+
if (signal?.aborted && !reply && stoppedPolls >= 2 && messages.some(message => message.id && !beforeIds.has(message.id))) {
|
|
329
|
+
const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
|
|
330
|
+
if (attachments.length)
|
|
331
|
+
return { text: "", sessionId: new URL(sessionUrl).searchParams.get("chat") ?? undefined, attachments };
|
|
332
|
+
code = "QWEN_REQUEST_CANCELLED";
|
|
333
|
+
throw new Error("请求已取消,已确认本轮桌面生成结束,未返回正文或附件");
|
|
334
|
+
}
|
|
269
335
|
if (!generating && (expectedReply ? isQwenHeartbeatReplyComplete(reply, expectedReply) && stablePolls >= 1 : reply && stablePolls >= 3)) {
|
|
270
336
|
stage = "读取千问办公已完成任务的产物(尚未上传 AUN FS)";
|
|
271
337
|
code = "QWEN_ARTIFACT_READ_FAILED";
|
|
272
338
|
const attachments = expectedReply ? [] : await this.readArtifacts(beforeIds, sessionUrl);
|
|
273
|
-
|
|
339
|
+
const sessionId = new URL(sessionUrl).searchParams.get("chat") ?? undefined;
|
|
340
|
+
return { text: reply, ...(sessionId ? { sessionId } : {}), ...(attachments.length ? { attachments } : {}) };
|
|
274
341
|
}
|
|
275
342
|
if (onMessage && generating && reply && stablePolls >= 1 && !forwarded.has(reply) && !isQwenApiUnavailableReply(reply)) {
|
|
276
343
|
await onMessage(reply);
|
|
@@ -510,9 +577,8 @@ class QwenCdpClient {
|
|
|
510
577
|
try {
|
|
511
578
|
await this.evaluate(`(async () => {
|
|
512
579
|
if (document.querySelector('button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]')) throw new Error('千问桌面端正在生成回复');
|
|
513
|
-
// 隐藏窗口会暂停消息渲染,使桌面端的提交前检查一直等不到确认。
|
|
514
|
-
if (document.hidden && typeof window.desktopApi?.focusMainWindow === 'function') await window.desktopApi.focusMainWindow();
|
|
515
580
|
})()`);
|
|
581
|
+
await this.restoreVisible();
|
|
516
582
|
}
|
|
517
583
|
catch (error) {
|
|
518
584
|
let permission;
|
|
@@ -525,8 +591,13 @@ class QwenCdpClient {
|
|
|
525
591
|
throw error;
|
|
526
592
|
}
|
|
527
593
|
}
|
|
528
|
-
|
|
529
|
-
|
|
594
|
+
async restoreVisible() {
|
|
595
|
+
await this.evaluate(`(async () => {
|
|
596
|
+
if (document.hidden && typeof window.desktopApi?.focusMainWindow === 'function') await window.desktopApi.focusMainWindow();
|
|
597
|
+
})()`);
|
|
598
|
+
}
|
|
599
|
+
recoverDraft(expected) {
|
|
600
|
+
return (0, cdp_draft_1.recoverCdpDraft)(this.evaluate.bind(this), exports.inputSelector, 'button[aria-label="停止"],button[aria-label="停止生成"],button[aria-label="Stop generating"]', this.options.dataDir, expected);
|
|
530
601
|
}
|
|
531
602
|
async cdp(method, params = {}, timeoutMs = this.options.timeoutMs) {
|
|
532
603
|
if (!this.socket)
|
|
@@ -588,6 +659,7 @@ class QwenCdpClient {
|
|
|
588
659
|
async waitFor(expression, message) {
|
|
589
660
|
const deadline = Date.now() + this.options.timeoutMs;
|
|
590
661
|
while (Date.now() < deadline) {
|
|
662
|
+
await this.restoreVisible();
|
|
591
663
|
if (await this.evaluate(expression))
|
|
592
664
|
return;
|
|
593
665
|
await wait(200);
|
|
@@ -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.
|
|
15
|
+
version: "1.0.28",
|
|
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 = [];
|
|
@@ -42,6 +52,7 @@ class QwenworkPlugin {
|
|
|
42
52
|
state = "stopped";
|
|
43
53
|
message = "";
|
|
44
54
|
healthIntervalMs;
|
|
55
|
+
restartDesktopHook;
|
|
45
56
|
healthTimer;
|
|
46
57
|
recovery;
|
|
47
58
|
context;
|
|
@@ -53,11 +64,14 @@ class QwenworkPlugin {
|
|
|
53
64
|
busy = false;
|
|
54
65
|
taskCount = 0;
|
|
55
66
|
stopped = true;
|
|
67
|
+
modelSessions = new Map();
|
|
68
|
+
modelSessionsPath;
|
|
56
69
|
constructor(options = {}) {
|
|
57
70
|
this.injectedClient = options.client;
|
|
58
71
|
this.configuredCdpPort = options.cdpPort;
|
|
59
72
|
this.configuredHttpPort = options.httpPort;
|
|
60
73
|
this.healthIntervalMs = options.healthIntervalMs ?? 30_000;
|
|
74
|
+
this.restartDesktopHook = options.restartDesktop;
|
|
61
75
|
}
|
|
62
76
|
async activate(context) {
|
|
63
77
|
if (this.state === "ready")
|
|
@@ -68,6 +82,21 @@ class QwenworkPlugin {
|
|
|
68
82
|
this.lastHeartbeat = undefined;
|
|
69
83
|
this.context = context;
|
|
70
84
|
try {
|
|
85
|
+
this.modelSessions.clear();
|
|
86
|
+
this.modelSessionsPath = typeof context.dataDir === "string" && context.dataDir ? (0, node_path_1.join)(context.dataDir, "model-provider-sessions.json") : undefined;
|
|
87
|
+
try {
|
|
88
|
+
if (!this.modelSessionsPath)
|
|
89
|
+
throw Object.assign(new Error("无持久化目录"), { code: "ENOENT" });
|
|
90
|
+
const saved = JSON.parse(await (0, promises_1.readFile)(this.modelSessionsPath, "utf8"));
|
|
91
|
+
if (!saved || typeof saved !== "object" || Array.isArray(saved) || Object.entries(saved).some(([key, value]) => !validModelSessionId(key) || !validModelSessionId(value)))
|
|
92
|
+
throw new Error("千问个人助理模型会话记录无效");
|
|
93
|
+
for (const [key, value] of Object.entries(saved))
|
|
94
|
+
this.modelSessions.set(key, value);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== "ENOENT")
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
71
100
|
const config = context.config;
|
|
72
101
|
const relayTarget = config.relayTarget;
|
|
73
102
|
if (relayTarget !== undefined && relayTarget !== "dsh")
|
|
@@ -106,6 +135,7 @@ class QwenworkPlugin {
|
|
|
106
135
|
if (!executable)
|
|
107
136
|
throw new Error("未找到 QwenWorkCN.exe,请配置 executablePath");
|
|
108
137
|
context.logger?.info(`正在启动千问办公,等待 CDP ${this.activeCdpPort}`, { executablePath: executable });
|
|
138
|
+
await this.restartDesktopProcess(executable);
|
|
109
139
|
try {
|
|
110
140
|
await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${this.activeCdpPort}`], () => client.connect(), typeof config.startupTimeoutMs === "number" ? config.startupTimeoutMs : 30_000);
|
|
111
141
|
}
|
|
@@ -140,8 +170,15 @@ class QwenworkPlugin {
|
|
|
140
170
|
throw error;
|
|
141
171
|
}
|
|
142
172
|
}
|
|
143
|
-
async deactivate(
|
|
173
|
+
async deactivate(reason, options) {
|
|
174
|
+
const reloadDesktop = !options?.preserveDesktop && reason === "reload" && Boolean(this.client) && !this.relayTarget;
|
|
144
175
|
await this.cleanup();
|
|
176
|
+
if (reloadDesktop) {
|
|
177
|
+
if (this.restartDesktopHook)
|
|
178
|
+
await this.restartDesktopHook();
|
|
179
|
+
else if (!this.injectedClient && process.platform === "win32")
|
|
180
|
+
await this.restartDesktopProcess();
|
|
181
|
+
}
|
|
145
182
|
this.state = "stopped";
|
|
146
183
|
this.stopped = true;
|
|
147
184
|
}
|
|
@@ -154,8 +191,6 @@ class QwenworkPlugin {
|
|
|
154
191
|
return { status: this.state, ...(this.message ? { message: this.message } : {}), details };
|
|
155
192
|
}
|
|
156
193
|
async handle(request) {
|
|
157
|
-
if (this.recovery)
|
|
158
|
-
await this.recovery;
|
|
159
194
|
if (this.state === "stopped" || (!this.relayTarget && !this.client))
|
|
160
195
|
throw new Error("qwenwork 插件未就绪");
|
|
161
196
|
return this.enqueueTask(() => this.completeRequest(request));
|
|
@@ -170,9 +205,8 @@ class QwenworkPlugin {
|
|
|
170
205
|
throw new Error("千问办公当前忙,无法创建新会话");
|
|
171
206
|
if (!this.client?.createSession)
|
|
172
207
|
throw new Error("千问办公当前传输不支持新建会话");
|
|
173
|
-
if (!this.client.connected())
|
|
174
|
-
throw new Error("千问办公 CDP 未连接");
|
|
175
208
|
return this.enqueueTask(async () => {
|
|
209
|
+
await this.ensureConnected();
|
|
176
210
|
await this.client.createSession(request.name);
|
|
177
211
|
return { text: "千问办公已创建新会话", sessionId };
|
|
178
212
|
});
|
|
@@ -185,8 +219,7 @@ class QwenworkPlugin {
|
|
|
185
219
|
if (!this.client)
|
|
186
220
|
throw new Error("qwenwork 插件未就绪");
|
|
187
221
|
try {
|
|
188
|
-
|
|
189
|
-
throw new Error("千问 CDP 已断开,等待空闲后自动恢复");
|
|
222
|
+
await this.ensureConnected(signal);
|
|
190
223
|
const reply = this.client.complete ? (await this.client.complete(prompt, timeoutMs, signal, undefined, undefined, onMessage)).text : await this.client.direct(prompt, timeoutMs, signal);
|
|
191
224
|
if ((0, cdp_client_1.isQwenApiUnavailableReply)(reply))
|
|
192
225
|
throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
|
|
@@ -196,7 +229,7 @@ class QwenworkPlugin {
|
|
|
196
229
|
return reply;
|
|
197
230
|
}
|
|
198
231
|
catch (error) {
|
|
199
|
-
if (!signal?.aborted) {
|
|
232
|
+
if (!this.stopped && !signal?.aborted) {
|
|
200
233
|
this.message = error instanceof Error ? error.message : String(error);
|
|
201
234
|
this.failures = this.client.connected() ? 0 : 2;
|
|
202
235
|
this.state = "degraded";
|
|
@@ -204,21 +237,23 @@ class QwenworkPlugin {
|
|
|
204
237
|
throw error;
|
|
205
238
|
}
|
|
206
239
|
}
|
|
207
|
-
async completeRequest(request) {
|
|
240
|
+
async completeRequest(request, modelRequest = false) {
|
|
208
241
|
if (this.relayTarget) {
|
|
209
242
|
return this.callPlugin(this.relayTarget, request);
|
|
210
243
|
}
|
|
211
244
|
if (!this.client)
|
|
212
245
|
throw new Error("qwenwork 插件未就绪");
|
|
213
246
|
try {
|
|
214
|
-
if (!this.client.connected())
|
|
215
|
-
throw new Error("千问 CDP
|
|
247
|
+
if (modelRequest && !this.client.connected())
|
|
248
|
+
throw new Error("千问 CDP 已断开,模型请求不会启动或重启桌面");
|
|
249
|
+
if (!modelRequest)
|
|
250
|
+
await this.ensureConnected(request.signal);
|
|
216
251
|
if (request.attachments?.length && (!this.client.attachmentInput || !this.client.complete))
|
|
217
252
|
throw new Error("千问当前客户端不支持附件输入");
|
|
218
253
|
const timeoutMs = request.timeoutMs ?? asNumber(this.context?.config.timeoutMs, 240_000);
|
|
219
254
|
const response = this.client.complete
|
|
220
|
-
? await this.client.complete(request.task, timeoutMs,
|
|
221
|
-
: { text: await this.complete(request.task, timeoutMs) };
|
|
255
|
+
? await this.client.complete(request.task, timeoutMs, request.signal, undefined, request.attachments, request.onMessage, modelRequest)
|
|
256
|
+
: { text: await this.complete(request.task, timeoutMs, request.signal) };
|
|
222
257
|
if ((0, cdp_client_1.isQwenApiUnavailableReply)(response.text))
|
|
223
258
|
throw new agent_1.AgentOperationError("QWEN_REMOTE_ERROR", "等待千问办公回复", new Error("千问生成接口不可用;桌面未提供具体服务端错误码,CDP 仍连接,未重启或重新提交任务"));
|
|
224
259
|
this.failures = 0;
|
|
@@ -227,12 +262,66 @@ class QwenworkPlugin {
|
|
|
227
262
|
return response;
|
|
228
263
|
}
|
|
229
264
|
catch (error) {
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
265
|
+
if (!this.stopped && !request.signal?.aborted && !modelRequest) {
|
|
266
|
+
this.message = error instanceof Error ? error.message : String(error);
|
|
267
|
+
this.failures = this.client.connected() ? 0 : 2;
|
|
268
|
+
this.state = "degraded";
|
|
269
|
+
}
|
|
233
270
|
throw error;
|
|
234
271
|
}
|
|
235
272
|
}
|
|
273
|
+
async completeModelProvider(request) {
|
|
274
|
+
let stage = "千问办公模型排队(queue)";
|
|
275
|
+
try {
|
|
276
|
+
request.signal.throwIfAborted();
|
|
277
|
+
const run = this.enqueueTask(async () => {
|
|
278
|
+
request.signal.throwIfAborted();
|
|
279
|
+
stage = "连接千问办公模型(connect)";
|
|
280
|
+
const client = this.client;
|
|
281
|
+
if (this.state !== "ready" || !client?.connected() || !client.createSession || !client.activateSession || !client.complete)
|
|
282
|
+
throw new Error("千问办公未连接或不支持隔离模型会话,模型请求不会启动或重启桌面");
|
|
283
|
+
const previousSessionId = await client.currentSessionId?.();
|
|
284
|
+
try {
|
|
285
|
+
stage = "准备千问办公模型会话(submit)";
|
|
286
|
+
const desktopSessionId = this.modelSessions.get(request.sessionId);
|
|
287
|
+
if (desktopSessionId)
|
|
288
|
+
await client.activateSession(desktopSessionId);
|
|
289
|
+
else
|
|
290
|
+
await client.createSession(undefined, true, true);
|
|
291
|
+
request.signal.throwIfAborted();
|
|
292
|
+
stage = "等待千问办公模型生成(generate)";
|
|
293
|
+
const response = await this.completeRequest({ task: request.prompt, sessionId: request.sessionId, timeoutMs: request.timeoutMs, signal: request.signal }, true);
|
|
294
|
+
const actualSessionId = response.sessionId ?? await client.currentSessionId?.();
|
|
295
|
+
if (!validModelSessionId(actualSessionId))
|
|
296
|
+
throw new Error("千问未返回可恢复的个人助理模型会话标识");
|
|
297
|
+
if (actualSessionId !== desktopSessionId) {
|
|
298
|
+
this.modelSessions.set(request.sessionId, actualSessionId);
|
|
299
|
+
const path = this.modelSessionsPath;
|
|
300
|
+
if (path) {
|
|
301
|
+
await (0, promises_1.mkdir)(this.context.dataDir ?? ".", { recursive: true });
|
|
302
|
+
await (0, promises_1.writeFile)(`${path}.tmp`, JSON.stringify(Object.fromEntries(this.modelSessions)), { mode: 0o600 });
|
|
303
|
+
await (0, promises_1.rename)(`${path}.tmp`, path);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { text: response.text };
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
if (client.connected()) {
|
|
310
|
+
if (previousSessionId)
|
|
311
|
+
await client.activateSession(previousSessionId);
|
|
312
|
+
else
|
|
313
|
+
await client.createSession(undefined, false, true);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return await (0, agent_1.abortable)(run, request.signal);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (error instanceof agent_1.AgentOperationError)
|
|
321
|
+
throw error;
|
|
322
|
+
throw new agent_1.AgentOperationError("QWEN_MODEL_FAILED", stage, error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
236
325
|
registerRoutes(http) {
|
|
237
326
|
if (!this.adapter)
|
|
238
327
|
return;
|
|
@@ -262,6 +351,14 @@ class QwenworkPlugin {
|
|
|
262
351
|
this.callPlugin = undefined;
|
|
263
352
|
this.pluginStatus = undefined;
|
|
264
353
|
}
|
|
354
|
+
async restartDesktopProcess(configuredExecutable) {
|
|
355
|
+
const config = this.context?.config;
|
|
356
|
+
const executable = configuredExecutable ?? await (0, desktop_launch_1.findWindowsExecutable)("QwenWorkCN.exe", typeof config?.executablePath === "string" ? config.executablePath : undefined);
|
|
357
|
+
if (!executable)
|
|
358
|
+
throw new Error("未找到 QwenWorkCN.exe,请配置 executablePath");
|
|
359
|
+
const processIds = await (0, desktop_launch_1.terminateWindowsDesktop)(executable);
|
|
360
|
+
this.context?.logger?.info("千问办公重载前已清理旧进程,将以 CDP 模式启动", { processIds, executable });
|
|
361
|
+
}
|
|
265
362
|
startHealthWatch() {
|
|
266
363
|
if (this.relayTarget || !this.client || this.healthTimer)
|
|
267
364
|
return;
|
|
@@ -284,9 +381,26 @@ class QwenworkPlugin {
|
|
|
284
381
|
});
|
|
285
382
|
}
|
|
286
383
|
async recover() {
|
|
384
|
+
if (this.stopped || this.busy)
|
|
385
|
+
return;
|
|
386
|
+
try {
|
|
387
|
+
await this.reconnect();
|
|
388
|
+
}
|
|
389
|
+
catch { /* 失败已记录,留待下次空闲检查。 */ }
|
|
390
|
+
}
|
|
391
|
+
async ensureConnected(signal) {
|
|
392
|
+
signal?.throwIfAborted();
|
|
393
|
+
if (this.stopped || !this.client)
|
|
394
|
+
throw new Error("qwenwork 插件未就绪");
|
|
395
|
+
await this.reconnect();
|
|
396
|
+
signal?.throwIfAborted();
|
|
397
|
+
if (this.stopped)
|
|
398
|
+
throw new Error("qwenwork 插件未就绪");
|
|
399
|
+
}
|
|
400
|
+
async reconnect() {
|
|
287
401
|
if (this.recovery)
|
|
288
402
|
return this.recovery;
|
|
289
|
-
if (this.stopped
|
|
403
|
+
if (this.stopped)
|
|
290
404
|
return;
|
|
291
405
|
const client = this.client;
|
|
292
406
|
if (!client || client.connected())
|
|
@@ -307,11 +421,12 @@ class QwenworkPlugin {
|
|
|
307
421
|
this.restarts += 1;
|
|
308
422
|
this.context?.logger?.info("千问 CDP 自动重连成功", { restarts: this.restarts });
|
|
309
423
|
})().catch((error) => {
|
|
310
|
-
if (this.stopped)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
424
|
+
if (!this.stopped) {
|
|
425
|
+
this.state = "failed";
|
|
426
|
+
this.message = error instanceof Error ? error.message : String(error);
|
|
427
|
+
this.context?.logger?.error(`千问 CDP 重连失败,桌面未重启,下次空闲检查重试:${this.message}`);
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
315
430
|
}).finally(() => { this.recovery = undefined; });
|
|
316
431
|
return this.recovery;
|
|
317
432
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "qwenwork",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"type": "agent",
|
|
5
5
|
"displayName": "千问办公",
|
|
6
6
|
"entry": "dist/src-ts/plugins/qwenwork/index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"capabilities": [
|
|
10
10
|
"chat",
|
|
11
11
|
"openai-compatible",
|
|
12
|
-
"health"
|
|
12
|
+
"health",
|
|
13
|
+
"model-provider"
|
|
13
14
|
],
|
|
14
15
|
"configSchema": "config.schema.json",
|
|
15
16
|
"clientDownloadUrl": "https://qwenwork.cn/download"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"cdpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9229 },
|
|
10
10
|
"autoStart": { "type": "boolean", "default": true, "description": "未连接时自动启动 WorkBuddy" },
|
|
11
11
|
"startupTimeoutMs": { "type": "integer", "minimum": 1000, "default": 30000 },
|
|
12
|
-
"timeoutMs": { "type": "integer", "minimum": 1, "default":
|
|
12
|
+
"timeoutMs": { "type": "integer", "minimum": 1, "default": 1200000, "description": "单次回复等待时间(毫秒),最多二十分钟;较短配置继续生效,超时不终止桌面任务" },
|
|
13
13
|
"dailyPoints": { "type": "boolean", "default": true, "description": "每天自动检查并领取页面明确提供的免费积分" },
|
|
14
14
|
"dailyPointsHour": { "type": "integer", "minimum": 0, "maximum": 23, "default": 9, "description": "每日免费积分领取时间(本机小时)" },
|
|
15
15
|
"dailyPointsMinute": { "type": "integer", "minimum": 0, "maximum": 59, "default": 0, "description": "每日免费积分领取时间(本机分钟)" }
|