mu-agent-bridge 1.0.73 → 1.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/src-ts/cli.js +67 -37
- package/dist/src-ts/control-server.js +7 -28
- 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 +7 -2
- package/dist/src-ts/http-agent.js +36 -0
- package/dist/src-ts/plugin-manager.js +185 -58
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +8 -6
- package/dist/src-ts/plugins/abweb/public/index.html +3 -3
- package/dist/src-ts/plugins/aun/attachments.js +3 -21
- package/dist/src-ts/plugins/aun/index.js +89 -11
- package/dist/src-ts/plugins/aun/messages.js +1 -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/index.js +3 -2
- package/dist/src-ts/plugins/claude-cli/index.js +1 -1
- package/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
- package/dist/src-ts/plugins/doubao/index.js +119 -7
- 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 +102 -23
- 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/personal-assistant/index.js +29 -8
- package/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
- package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
- package/dist/src-ts/plugins/personal-assistant/tools.js +4 -3
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +43 -7
- package/dist/src-ts/plugins/qwenwork/index.js +52 -18
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
- package/dist/src-ts/plugins/workbuddy/index.js +24 -12
- 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 +27 -8
- package/dist/src-ts/version-manager.js +6 -3
- package/dist/tests-ts/abweb-model-config.test.js +44 -41
- 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 +14 -0
- package/dist/tests-ts/aun-plugin.test.js +207 -19
- package/dist/tests-ts/bundled-package.test.js +1 -1
- package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
- package/dist/tests-ts/chatgpt-plugin.test.js +19 -0
- package/dist/tests-ts/control-server.test.js +20 -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 +688 -19
- package/dist/tests-ts/dsh-agent.test.js +91 -19
- 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/local-cli.test.js +29 -2
- package/dist/tests-ts/model-provider-cancellation.test.js +2 -0
- package/dist/tests-ts/native-session-management.test.js +8 -7
- package/dist/tests-ts/personal-assistant-coordination.test.js +16 -0
- package/dist/tests-ts/personal-assistant-extensions.test.js +7 -1
- package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
- package/dist/tests-ts/personal-assistant.test.js +277 -3
- package/dist/tests-ts/platform-integration.test.js +60 -1
- package/dist/tests-ts/plugin-manager.test.js +78 -6
- package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
- package/dist/tests-ts/publish-packages.test.js +1 -1
- package/dist/tests-ts/qwenwork-plugin.test.js +30 -0
- 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 +8 -8
- 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 +356 -13
- 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/plugins/abweb/index.js +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +8 -6
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +7 -5
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +3 -3
- 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/plugins/aun/attachments.js +3 -21
- package/plugins/aun/dist/src-ts/plugins/aun/index.js +89 -11
- package/plugins/aun/dist/src-ts/plugins/aun/messages.js +1 -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/plugins/cdp-draft.js +5 -2
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +3 -2
- package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +1 -1
- package/plugins/claude-cli/package.json +1 -1
- package/plugins/claude-cli/plugin.json +1 -1
- package/plugins/doubao/config.schema.json +2 -1
- package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +231 -45
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +119 -7
- 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 +1 -1
- package/plugins/dsh/config.schema.json +5 -2
- package/plugins/dsh/dist/src-ts/dsh-agent.js +7 -2
- 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 +102 -23
- 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 +1 -1
- 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 +3 -2
- package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +29 -8
- package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +20 -6
- 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 +4 -3
- package/plugins/personal-assistant/package.json +2 -2
- package/plugins/personal-assistant/plugin.json +1 -1
- 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 +43 -7
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +52 -18
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/config.schema.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +349 -83
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +24 -12
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
- 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
|
@@ -17,7 +17,7 @@ const session_history_1 = require("./session-history");
|
|
|
17
17
|
const desktop_launch_1 = require("../desktop-launch");
|
|
18
18
|
const desktop_launch_2 = require("./desktop-launch");
|
|
19
19
|
exports.workbuddyManifest = {
|
|
20
|
-
id: "workbuddy", version: "1.1.
|
|
20
|
+
id: "workbuddy", version: "1.1.35", displayName: "WorkBuddy", type: "agent",
|
|
21
21
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
22
22
|
capabilities: ["chat", "health", "model-provider"], configSchema: "config.schema.json",
|
|
23
23
|
};
|
|
@@ -52,7 +52,8 @@ class WorkbuddyPlugin {
|
|
|
52
52
|
cliPath;
|
|
53
53
|
model;
|
|
54
54
|
transport = "auto";
|
|
55
|
-
|
|
55
|
+
replyTimeoutLimitMs;
|
|
56
|
+
timeoutMs = cdp_client_1.WORKBUDDY_MAX_WAIT_MS;
|
|
56
57
|
queue = Promise.resolve();
|
|
57
58
|
pending = 0;
|
|
58
59
|
activeSessionId;
|
|
@@ -76,6 +77,7 @@ class WorkbuddyPlugin {
|
|
|
76
77
|
sessionHistory;
|
|
77
78
|
constructor(options = {}) {
|
|
78
79
|
this.options = options;
|
|
80
|
+
this.replyTimeoutLimitMs = options.replyTimeoutLimitMs ?? cdp_client_1.WORKBUDDY_MAX_WAIT_MS;
|
|
79
81
|
}
|
|
80
82
|
get attachmentInput() { return Boolean(this.client?.attachmentInput && this.client.complete && this.transport !== "cli" && (!this.cliPath || this.transport === "cdp")); }
|
|
81
83
|
async activate(context) {
|
|
@@ -83,7 +85,7 @@ class WorkbuddyPlugin {
|
|
|
83
85
|
if (config.transport !== undefined && !["auto", "cli", "cdp"].includes(String(config.transport)))
|
|
84
86
|
throw new Error("transport 只能是 auto、cli 或 cdp");
|
|
85
87
|
this.transport = config.transport ?? "auto";
|
|
86
|
-
this.timeoutMs = typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 3_600_000;
|
|
88
|
+
this.timeoutMs = Math.min(typeof config.timeoutMs === "number" && config.timeoutMs > 0 ? config.timeoutMs : 3_600_000, this.replyTimeoutLimitMs);
|
|
87
89
|
this.model = typeof config.model === "string" && config.model.trim() ? config.model : undefined;
|
|
88
90
|
this.cliPath = (0, cli_client_1.findCli)(typeof config.cliPath === "string" ? config.cliPath : undefined);
|
|
89
91
|
this.failure = undefined;
|
|
@@ -237,18 +239,23 @@ class WorkbuddyPlugin {
|
|
|
237
239
|
return { status: "ready", details: { transport: "cdp" } };
|
|
238
240
|
if (this.transport !== "cdp" && this.cliPath && (0, node_fs_1.existsSync)(this.cliPath))
|
|
239
241
|
return { status: "ready", details: { transport: "cli" } };
|
|
240
|
-
return { status: "failed", message: this.failure ?? "WorkBuddy
|
|
242
|
+
return { status: "failed", message: this.failure ?? "WorkBuddy CDP 未连接;桌面升级后请在后台使用“重启桌面”恢复连接", details: { transport: this.transport } };
|
|
241
243
|
}
|
|
242
244
|
async handle(request) {
|
|
243
245
|
if (!request.task.trim())
|
|
244
246
|
throw new Error("消息内容不能为空");
|
|
245
247
|
(0, attachments_1.validateAttachments)(request.attachments);
|
|
246
248
|
const run = this.enqueue(async () => {
|
|
249
|
+
request.signal?.throwIfAborted();
|
|
247
250
|
try {
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
finally {
|
|
251
|
+
const response = await this.complete(request);
|
|
251
252
|
await this.synchronizeSession();
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
// 失败时桌面可能已切换,保留原绑定并要求重新校验或恢复。
|
|
257
|
+
this.desktopSessionValid = false;
|
|
258
|
+
throw error;
|
|
252
259
|
}
|
|
253
260
|
});
|
|
254
261
|
const response = await run;
|
|
@@ -257,7 +264,12 @@ class WorkbuddyPlugin {
|
|
|
257
264
|
async synchronizeSession() {
|
|
258
265
|
if (this.activeSessionId && this.desktopSessionValid && this.client?.conversationId) {
|
|
259
266
|
try {
|
|
260
|
-
|
|
267
|
+
const conversationId = await this.client.conversationId();
|
|
268
|
+
if (this.activeDesktopConversationId && conversationId !== this.activeDesktopConversationId) {
|
|
269
|
+
this.desktopSessionValid = false;
|
|
270
|
+
throw new Error("WorkBuddy 桌面任务已切换,已保留原会话绑定");
|
|
271
|
+
}
|
|
272
|
+
this.activeDesktopConversationId = conversationId;
|
|
261
273
|
if (this.activeDesktopConversationId)
|
|
262
274
|
this.sessionBindings.set(this.activeSessionId, { conversationId: this.activeDesktopConversationId, expert: this.activeExpert });
|
|
263
275
|
await this.persistKnownSessions();
|
|
@@ -337,8 +349,8 @@ class WorkbuddyPlugin {
|
|
|
337
349
|
async complete(request, modelRequest = false) {
|
|
338
350
|
let stage = "连接 WorkBuddy 桌面";
|
|
339
351
|
try {
|
|
340
|
-
const timeout = request.timeoutMs ?? this.timeoutMs;
|
|
341
|
-
const expert = modelRequest ? undefined : extractExpertQuery(request.task);
|
|
352
|
+
const timeout = Math.min(request.timeoutMs ?? this.timeoutMs, this.replyTimeoutLimitMs);
|
|
353
|
+
const expert = modelRequest || this.options.expertSelection === false ? undefined : extractExpertQuery(request.task);
|
|
342
354
|
const usesCli = Boolean(this.cliPath && this.transport !== "cdp" && !expert);
|
|
343
355
|
const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
|
|
344
356
|
if (hasAttachments && !this.attachmentInput)
|
|
@@ -372,7 +384,7 @@ class WorkbuddyPlugin {
|
|
|
372
384
|
this.expertSession = true;
|
|
373
385
|
try {
|
|
374
386
|
if (this.client.complete)
|
|
375
|
-
return await this.client.complete({ ...request, timeoutMs: timeout }, select, false);
|
|
387
|
+
return await this.client.complete({ ...request, timeoutMs: timeout }, select, false, request.sessionId ? this.activeDesktopConversationId ?? null : undefined);
|
|
376
388
|
return { text: await this.client.direct(request.task, timeout, select) };
|
|
377
389
|
}
|
|
378
390
|
catch (error) {
|
|
@@ -392,7 +404,7 @@ class WorkbuddyPlugin {
|
|
|
392
404
|
this.activeExpert = null;
|
|
393
405
|
try {
|
|
394
406
|
if (this.client.complete)
|
|
395
|
-
return await this.client.complete({ ...request, timeoutMs: timeout }, undefined, reset);
|
|
407
|
+
return await this.client.complete({ ...request, timeoutMs: timeout }, undefined, reset, request.sessionId ? this.activeDesktopConversationId ?? null : undefined);
|
|
396
408
|
return { text: await this.client.direct(request.task, timeout, undefined, reset) };
|
|
397
409
|
}
|
|
398
410
|
catch (error) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"aid": { "type": "string", "description": "关联的 AID" },
|
|
5
|
+
"autoStart": { "type": "boolean", "default": true, "description": "CDP 端口拒绝连接时自动发现并启动 ZCode" },
|
|
6
|
+
"executablePath": { "type": "string", "description": "可选的 ZCode.exe 路径,留空时从注册表、开始菜单和安装目录自动发现" },
|
|
7
|
+
"startupArgs": { "type": "array", "items": { "type": "string" }, "default": [], "description": "附加启动参数;CDP 地址和端口由插件自动传入" },
|
|
8
|
+
"startupTimeoutMs": { "type": "integer", "minimum": 1, "default": 30000, "description": "启动后等待 CDP 就绪的时间(毫秒)" },
|
|
9
|
+
"cdpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9231, "description": "ZCode CDP 端口" },
|
|
10
|
+
"timeoutMs": { "type": "integer", "minimum": 1, "default": 3600000, "description": "单次回复等待时间(毫秒)" }
|
|
11
|
+
},
|
|
12
|
+
"additionalProperties": false
|
|
13
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentOperationError = exports.AgentError = void 0;
|
|
4
|
+
exports.abortable = abortable;
|
|
5
|
+
exports.errorDetails = errorDetails;
|
|
6
|
+
async function abortable(promise, signal) {
|
|
7
|
+
if (!signal)
|
|
8
|
+
return promise;
|
|
9
|
+
if (signal.aborted) {
|
|
10
|
+
// promise 已在调用方启动,先观察其拒绝,避免预取消时产生未处理拒绝。
|
|
11
|
+
void promise.catch(() => undefined);
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let settled = false;
|
|
16
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
17
|
+
const abort = () => {
|
|
18
|
+
if (settled)
|
|
19
|
+
return;
|
|
20
|
+
settled = true;
|
|
21
|
+
cleanup();
|
|
22
|
+
reject(signal.reason);
|
|
23
|
+
};
|
|
24
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
25
|
+
promise.then((value) => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
|
+
cleanup();
|
|
30
|
+
resolve(value);
|
|
31
|
+
}, (error) => {
|
|
32
|
+
if (settled)
|
|
33
|
+
return;
|
|
34
|
+
settled = true;
|
|
35
|
+
cleanup();
|
|
36
|
+
reject(error);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
class AgentError extends Error {
|
|
41
|
+
constructor(message, options) {
|
|
42
|
+
super(message, options);
|
|
43
|
+
this.name = "AgentError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.AgentError = AgentError;
|
|
47
|
+
/** 保留底层错误码,避免只留下 fetch failed 或超时这类表层提示。 */
|
|
48
|
+
function errorDetails(error) {
|
|
49
|
+
const parts = [];
|
|
50
|
+
const seen = new Set();
|
|
51
|
+
for (let current = error; current && !seen.has(current) && seen.size < 5;) {
|
|
52
|
+
seen.add(current);
|
|
53
|
+
const value = current;
|
|
54
|
+
const message = typeof value.message === "string" ? value.message : String(current);
|
|
55
|
+
parts.push(`${message}${value.code ? `(${String(value.code)})` : ""}`);
|
|
56
|
+
current = value.cause;
|
|
57
|
+
}
|
|
58
|
+
return parts.join(";原因:") || String(error);
|
|
59
|
+
}
|
|
60
|
+
class AgentOperationError extends AgentError {
|
|
61
|
+
code;
|
|
62
|
+
stage;
|
|
63
|
+
constructor(code, stage, cause) {
|
|
64
|
+
super(`${code}:${stage}失败:${errorDetails(cause)}`, { cause });
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.stage = stage;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.AgentOperationError = AgentOperationError;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_TOTAL_ATTACHMENT_BYTES = exports.MAX_ATTACHMENTS = exports.MAX_ATTACHMENT_BYTES = void 0;
|
|
4
|
+
exports.detectImageContentType = detectImageContentType;
|
|
5
|
+
exports.attachmentFilename = attachmentFilename;
|
|
6
|
+
exports.validateAttachment = validateAttachment;
|
|
7
|
+
exports.validateAttachments = validateAttachments;
|
|
8
|
+
exports.asAttachments = asAttachments;
|
|
9
|
+
exports.MAX_ATTACHMENT_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
exports.MAX_ATTACHMENTS = 8;
|
|
11
|
+
exports.MAX_TOTAL_ATTACHMENT_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
function detectImageContentType(data) {
|
|
13
|
+
if (data.length >= 8 && data.slice(0, 8).every((value, index) => value === [137, 80, 78, 71, 13, 10, 26, 10][index]))
|
|
14
|
+
return "image/png";
|
|
15
|
+
if (data.length >= 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff)
|
|
16
|
+
return "image/jpeg";
|
|
17
|
+
if (data.length >= 12 && String.fromCharCode(...data.slice(0, 4)) === "RIFF" && String.fromCharCode(...data.slice(8, 12)) === "WEBP")
|
|
18
|
+
return "image/webp";
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function attachmentFilename(filename) {
|
|
22
|
+
if (typeof filename !== "string" || /[\u0000-\u001f\u007f]/.test(filename))
|
|
23
|
+
throw new Error("附件文件名无效");
|
|
24
|
+
const name = filename.split(/[\\/]/).pop() ?? "";
|
|
25
|
+
if (!name.trim() || name === "." || name === "..")
|
|
26
|
+
throw new Error("附件文件名无效");
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
function validateAttachment(item) {
|
|
30
|
+
if (!item || !(item.data instanceof Uint8Array) || item.data.byteLength === 0)
|
|
31
|
+
throw new Error("附件内容不能为空");
|
|
32
|
+
if (item.data.byteLength > exports.MAX_ATTACHMENT_BYTES)
|
|
33
|
+
throw new Error("单个附件不能超过 32 MiB");
|
|
34
|
+
attachmentFilename(item.filename);
|
|
35
|
+
if (item.kind !== "image" && item.kind !== "file")
|
|
36
|
+
throw new Error("附件类型无效");
|
|
37
|
+
if (typeof item.contentType !== "string" || !/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(item.contentType))
|
|
38
|
+
throw new Error("附件 MIME 类型无效");
|
|
39
|
+
if (item.kind === "image") {
|
|
40
|
+
const detected = detectImageContentType(item.data);
|
|
41
|
+
if (!detected || detected !== item.contentType.toLowerCase())
|
|
42
|
+
throw new Error("图片内容与 MIME 类型不一致");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function validateAttachments(items) {
|
|
46
|
+
if (!items)
|
|
47
|
+
return;
|
|
48
|
+
if (!Array.isArray(items))
|
|
49
|
+
throw new Error("附件列表无效");
|
|
50
|
+
if (items.length > exports.MAX_ATTACHMENTS)
|
|
51
|
+
throw new Error("单条消息最多包含 8 个附件");
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
validateAttachment(item);
|
|
55
|
+
total += item.data.byteLength;
|
|
56
|
+
}
|
|
57
|
+
if (total > exports.MAX_TOTAL_ATTACHMENT_BYTES)
|
|
58
|
+
throw new Error("单条消息附件总大小不能超过 64 MiB");
|
|
59
|
+
}
|
|
60
|
+
function asAttachments(images) {
|
|
61
|
+
return (images ?? []).map((image) => ({ ...image, kind: "image" }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRuntimePort = resolveRuntimePort;
|
|
4
|
+
function resolveRuntimePort(config, runtime, pluginId, envName, fallback) {
|
|
5
|
+
const injected = runtime?.ports?.[pluginId];
|
|
6
|
+
if (typeof injected === "number" && Number.isInteger(injected) && injected > 0)
|
|
7
|
+
return injected;
|
|
8
|
+
const configured = config.cdpPort;
|
|
9
|
+
if (typeof configured === "number" && Number.isInteger(configured) && configured > 0)
|
|
10
|
+
return configured;
|
|
11
|
+
const environment = Number(process.env[envName] ?? "");
|
|
12
|
+
return Number.isInteger(environment) && environment > 0 ? environment : fallback;
|
|
13
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findWindowsExecutable = findWindowsExecutable;
|
|
4
|
+
exports.findExecutableInTree = findExecutableInTree;
|
|
5
|
+
exports.launchAndWait = launchAndWait;
|
|
6
|
+
exports.launchManagedAndWait = launchManagedAndWait;
|
|
7
|
+
exports.restartOwnedAndWait = restartOwnedAndWait;
|
|
8
|
+
exports.terminateWindowsDesktop = terminateWindowsDesktop;
|
|
9
|
+
exports.terminateOwnedProcess = terminateOwnedProcess;
|
|
10
|
+
exports.findChatgptExecutable = findChatgptExecutable;
|
|
11
|
+
exports.waitFor = waitFor;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const promises_1 = require("node:fs/promises");
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const node_util_1 = require("node:util");
|
|
17
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
18
|
+
const versionDirectory = /^(?:app[-_ ]?)?v?\d+(?:[.-]\d+)+$/i;
|
|
19
|
+
/** 先从 Windows 注册信息和开始菜单解析,失败时再查常见安装目录。 */
|
|
20
|
+
async function findWindowsExecutable(fileName, configured, run = execFileAsync) {
|
|
21
|
+
if (configured?.trim()) {
|
|
22
|
+
const configuredPath = (0, node_path_1.resolve)(configured);
|
|
23
|
+
if (fileName.toLowerCase() === "doubao.exe") {
|
|
24
|
+
let root = (0, node_path_1.dirname)(configuredPath);
|
|
25
|
+
while (/^(?:app|launcher|doubao_update)$/i.test((0, node_path_1.basename)(root)) || versionDirectory.test((0, node_path_1.basename)(root)))
|
|
26
|
+
root = (0, node_path_1.dirname)(root);
|
|
27
|
+
const updated = (0, node_path_1.join)(root, "Doubao_update", fileName);
|
|
28
|
+
if ((0, node_fs_1.existsSync)(updated))
|
|
29
|
+
return updated;
|
|
30
|
+
const latest = await findExecutableInTree(root, fileName, /^/, 3);
|
|
31
|
+
if (latest)
|
|
32
|
+
return latest;
|
|
33
|
+
}
|
|
34
|
+
// 千问升级会保留或删除旧版本目录,从同一安装位置重新定位新版。
|
|
35
|
+
if (fileName.toLowerCase() === "qwenworkcn.exe" && /^v?\d+(?:[.-]\d+)+$/i.test((0, node_path_1.basename)((0, node_path_1.dirname)(configuredPath)))) {
|
|
36
|
+
const latest = await findExecutableInTree((0, node_path_1.dirname)((0, node_path_1.dirname)(configuredPath)), fileName, /^/, 1);
|
|
37
|
+
if (latest)
|
|
38
|
+
return latest;
|
|
39
|
+
}
|
|
40
|
+
if ((0, node_fs_1.existsSync)(configuredPath))
|
|
41
|
+
return configuredPath;
|
|
42
|
+
}
|
|
43
|
+
if (process.platform !== "win32")
|
|
44
|
+
return undefined;
|
|
45
|
+
const registered = await findRegisteredWindowsExecutable(fileName, run);
|
|
46
|
+
if (registered)
|
|
47
|
+
return registered;
|
|
48
|
+
const pattern = /doubao|豆包|qwen|qwwork|千问|deepseek|harness|dsh|zcode/i;
|
|
49
|
+
const roots = [...new Set([
|
|
50
|
+
process.env.DSH_INSTALL_DIR,
|
|
51
|
+
"D:\\Programs\\DeepSeek Harness",
|
|
52
|
+
"D:\\DSH",
|
|
53
|
+
process.env.LOCALAPPDATA && (0, node_path_1.join)(process.env.LOCALAPPDATA, "Programs"),
|
|
54
|
+
process.env.LOCALAPPDATA,
|
|
55
|
+
process.env.ProgramFiles,
|
|
56
|
+
process.env["ProgramFiles(x86)"],
|
|
57
|
+
...Array.from({ length: 24 }, (_, index) => `${String.fromCharCode(67 + index)}:\\`),
|
|
58
|
+
].filter((value) => typeof value === "string" && (0, node_fs_1.existsSync)(value)))];
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
const found = await findExecutableInTree(root, fileName, pattern, 6);
|
|
61
|
+
if (found)
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
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
|
+
}
|
|
96
|
+
async function findExecutableInTree(directory, fileName, pattern, depth) {
|
|
97
|
+
return search(directory, fileName, pattern, depth, pattern.test(directory));
|
|
98
|
+
}
|
|
99
|
+
async function search(directory, fileName, pattern, depth, insideProduct) {
|
|
100
|
+
try {
|
|
101
|
+
const entries = await (0, promises_1.readdir)(directory, { withFileTypes: true });
|
|
102
|
+
// 豆包上层文件是启动器,优先查找新版目录或 app 中的实际程序。
|
|
103
|
+
if (insideProduct && depth > 0 && fileName.toLowerCase() === "doubao.exe") {
|
|
104
|
+
const versions = entries.filter(entry => entry.isDirectory() && (versionDirectory.test(entry.name) || /^(?:app|doubao_update)$/i.test(entry.name)));
|
|
105
|
+
versions.sort((left, right) => {
|
|
106
|
+
const rank = (name) => /^doubao_update$/i.test(name) ? 2 : /^app$/i.test(name) ? 0 : 1;
|
|
107
|
+
return rank(right.name) - rank(left.name) || right.name.replace(/^\D+/, "").localeCompare(left.name.replace(/^\D+/, ""), "en", { numeric: true });
|
|
108
|
+
});
|
|
109
|
+
for (const entry of versions) {
|
|
110
|
+
const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, true);
|
|
111
|
+
if (found)
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const entry of entries)
|
|
116
|
+
if (entry.isFile() && insideProduct && entry.name.toLowerCase() === fileName.toLowerCase() && !(fileName.toLowerCase() === "doubao.exe" && /^(?:launcher|doubao)$/i.test((0, node_path_1.basename)(directory))))
|
|
117
|
+
return (0, node_path_1.join)(directory, entry.name);
|
|
118
|
+
if (depth <= 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
// 按数字比较版本目录,避免 1.2.5 排在 1.2.10 之前。
|
|
121
|
+
entries.sort((left, right) => right.name.localeCompare(left.name, "en", { numeric: true }));
|
|
122
|
+
for (const entry of entries)
|
|
123
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && (insideProduct || pattern.test(entry.name) || /^(?:Program Files(?: \(x86\))?|Programs)$/i.test(entry.name))) {
|
|
124
|
+
const found = await search((0, node_path_1.join)(directory, entry.name), fileName, pattern, depth - 1, insideProduct || pattern.test(entry.name));
|
|
125
|
+
if (found)
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { /* 无权限或目录在扫描期间消失时继续查找 */ }
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
async function launchAndWait(executable, args, connect, timeoutMs, launch = (file, argv) => (0, node_child_process_1.spawn)(file, argv, { detached: true, stdio: "ignore", windowsHide: false })) {
|
|
133
|
+
const child = launch(executable, args);
|
|
134
|
+
child.unref();
|
|
135
|
+
return waitFor(connect, timeoutMs, 250);
|
|
136
|
+
}
|
|
137
|
+
async function launchManagedAndWait(executable, args, profileDirectory, connect, timeoutMs, onProcess, launch = (file, argv) => (0, node_child_process_1.spawn)(file, argv, { detached: true, stdio: "ignore", windowsHide: false })) {
|
|
138
|
+
if (profileDirectory)
|
|
139
|
+
await (0, promises_1.mkdir)(profileDirectory, { recursive: true });
|
|
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();
|
|
147
|
+
child.unref();
|
|
148
|
+
onProcess(child);
|
|
149
|
+
try {
|
|
150
|
+
await started;
|
|
151
|
+
return await waitFor(connect, timeoutMs, 250);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
await terminateOwnedProcess(child).catch(() => undefined);
|
|
155
|
+
onProcess(undefined);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function restartOwnedAndWait(child, executable, args, profileDirectory, connect, timeoutMs, onProcess) {
|
|
160
|
+
await terminateOwnedProcess(child);
|
|
161
|
+
onProcess(undefined);
|
|
162
|
+
return launchManagedAndWait(executable, args, profileDirectory, connect, timeoutMs, onProcess);
|
|
163
|
+
}
|
|
164
|
+
/** 仅在用户确认重启后清理同一用户、同一安装路径的桌面进程。 */
|
|
165
|
+
async function terminateWindowsDesktop(executable) {
|
|
166
|
+
if (process.platform !== "win32")
|
|
167
|
+
return [];
|
|
168
|
+
const script = `
|
|
169
|
+
$ErrorActionPreference = 'Stop'
|
|
170
|
+
$target = '${(0, node_path_1.resolve)(executable).replace(/'/g, "''")}'
|
|
171
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
172
|
+
$processes = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -ieq $target } | Where-Object { (Invoke-CimMethod -InputObject $_ -MethodName GetOwnerSid).Sid -eq $sid })
|
|
173
|
+
$ids = @($processes.ProcessId)
|
|
174
|
+
$roots = @($processes | Where-Object { $ids -notcontains $_.ParentProcessId })
|
|
175
|
+
foreach ($item in $roots) {
|
|
176
|
+
$current = Get-CimInstance Win32_Process -Filter "ProcessId=$($item.ProcessId)"
|
|
177
|
+
if (-not $current -or $current.CreationDate -ne $item.CreationDate) { continue }
|
|
178
|
+
& taskkill.exe /PID $item.ProcessId /T /F | Out-Null
|
|
179
|
+
if ($LASTEXITCODE -ne 0 -and (Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue)) { throw "无法结束桌面进程 $($item.ProcessId)" }
|
|
180
|
+
$remaining = Get-Process -Id $item.ProcessId -ErrorAction SilentlyContinue
|
|
181
|
+
if ($remaining -and -not $remaining.WaitForExit(10000)) { throw "等待桌面进程退出超时 $($item.ProcessId)" }
|
|
182
|
+
}
|
|
183
|
+
ConvertTo-Json -Compress -InputObject @($roots | ForEach-Object { [int]$_.ProcessId })
|
|
184
|
+
`;
|
|
185
|
+
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { timeout: 30_000, windowsHide: true });
|
|
186
|
+
return JSON.parse(stdout.trim());
|
|
187
|
+
}
|
|
188
|
+
async function terminateOwnedProcess(child, platform = process.platform, run = async (file, args) => execFileAsync(file, args, { timeout: 10_000 })) {
|
|
189
|
+
const pid = child?.pid;
|
|
190
|
+
if (!Number.isSafeInteger(pid) || !pid || child?.exitCode !== null && child?.exitCode !== undefined || child?.signalCode)
|
|
191
|
+
return;
|
|
192
|
+
if (platform === "win32") {
|
|
193
|
+
try {
|
|
194
|
+
await run("taskkill.exe", ["/PID", String(pid), "/T", "/F"]);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (!child.exitCode && !child.signalCode && error.code !== "ESRCH")
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(pid, "SIGTERM");
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code !== "ESRCH")
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function findChatgptExecutable(configured) {
|
|
212
|
+
if (configured?.trim() && (0, node_fs_1.existsSync)((0, node_path_1.resolve)(configured)))
|
|
213
|
+
return (0, node_path_1.resolve)(configured);
|
|
214
|
+
if (process.platform !== "win32")
|
|
215
|
+
return undefined;
|
|
216
|
+
const script = "$candidates = foreach ($package in (Get-AppxPackage | Where-Object { $_.Name -like 'OpenAI.Codex*' })) { $appDir = Join-Path $package.InstallLocation 'app'; $path = Join-Path $appDir 'ChatGPT.exe'; if (-not (Test-Path -LiteralPath $path)) { $path = Get-ChildItem -LiteralPath $appDir -Filter 'ChatGPT*.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName }; if ($path) { [pscustomobject]@{ Version = [version]$package.Version; Executable = $path } } }; $candidate = $candidates | Sort-Object Version -Descending | Select-Object -First 1; if ($candidate) { $candidate.Executable }";
|
|
217
|
+
try {
|
|
218
|
+
const { stdout } = await execFileAsync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { timeout: 10_000 });
|
|
219
|
+
return stdout.trim().split(/\r?\n/).at(-1)?.trim() || undefined;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function waitFor(action, timeoutMs, intervalMs = 500) {
|
|
226
|
+
const deadline = Date.now() + timeoutMs;
|
|
227
|
+
let last;
|
|
228
|
+
while (Date.now() < deadline) {
|
|
229
|
+
try {
|
|
230
|
+
return await action();
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
last = error;
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
throw last instanceof Error ? last : new Error("桌面程序启动超时");
|
|
238
|
+
}
|