mu-agent-bridge 1.0.56 → 1.0.58
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/cli.js +18 -0
- package/dist/src-ts/control-server.js +1 -1
- package/dist/src-ts/core-updater.js +1 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +6 -0
- package/dist/src-ts/plugins/qwenwork/openai-adapter.js +7 -8
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +1 -1
- package/dist/src-ts/plugins/workbuddy/index.js +27 -11
- package/dist/tests-ts/chatgpt-plugin.test.js +5 -3
- package/dist/tests-ts/control-server.test.js +25 -2
- package/dist/tests-ts/core-updater.test.js +5 -3
- package/dist/tests-ts/installer-assets.test.js +2 -0
- package/dist/tests-ts/workbuddy-plugin.test.js +52 -13
- package/package.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +6 -0
- package/plugins/dsh/dist/src-ts/plugins/qwenwork/openai-adapter.js +7 -8
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/openai-adapter.js +7 -8
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +27 -11
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
package/dist/src-ts/cli.js
CHANGED
|
@@ -207,6 +207,17 @@ async function startUpdatedDaemon() {
|
|
|
207
207
|
};
|
|
208
208
|
return { healthy };
|
|
209
209
|
}
|
|
210
|
+
let coreUpdateCheckRunning = false;
|
|
211
|
+
function scheduleCoreUpdateCheck() {
|
|
212
|
+
if (coreUpdateCheckRunning)
|
|
213
|
+
return;
|
|
214
|
+
coreUpdateCheckRunning = true;
|
|
215
|
+
const env = { ...process.env, AGENT_BRIDGE_CORE_UPDATE_ONLY: "1" };
|
|
216
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.resolve)(__dirname, "cli.js"), "start"], { detached: true, stdio: "ignore", windowsHide: true, env });
|
|
217
|
+
child.once("exit", () => { coreUpdateCheckRunning = false; });
|
|
218
|
+
child.once("error", () => { coreUpdateCheckRunning = false; });
|
|
219
|
+
child.unref();
|
|
220
|
+
}
|
|
210
221
|
async function stop() {
|
|
211
222
|
await stopDaemon();
|
|
212
223
|
console.log("旧 daemon 已停止");
|
|
@@ -304,6 +315,8 @@ async function serve() {
|
|
|
304
315
|
const intervalMs = Math.max(60_000, Number(process.env.AGENT_BRIDGE_PLUGIN_UPDATE_INTERVAL_MS ?? 300_000) || 300_000);
|
|
305
316
|
const updateTimer = setInterval(() => void checkAndUpgradePlugins(manager), intervalMs);
|
|
306
317
|
updateTimer.unref?.();
|
|
318
|
+
const coreUpdateTimer = setInterval(scheduleCoreUpdateCheck, intervalMs);
|
|
319
|
+
coreUpdateTimer.unref?.();
|
|
307
320
|
let stopping = false;
|
|
308
321
|
const shutdown = async () => {
|
|
309
322
|
if (stopping)
|
|
@@ -325,6 +338,7 @@ async function serve() {
|
|
|
325
338
|
}
|
|
326
339
|
catch { /* 地址文件可能已被清理 */ }
|
|
327
340
|
clearInterval(updateTimer);
|
|
341
|
+
clearInterval(coreUpdateTimer);
|
|
328
342
|
};
|
|
329
343
|
process.once("SIGINT", () => void shutdown().finally(() => process.exit(0)));
|
|
330
344
|
process.once("SIGTERM", () => void shutdown().finally(() => process.exit(0)));
|
|
@@ -399,6 +413,10 @@ async function main() {
|
|
|
399
413
|
await logs.flush();
|
|
400
414
|
return 0;
|
|
401
415
|
}
|
|
416
|
+
if (process.env.AGENT_BRIDGE_CORE_UPDATE_ONLY === "1") {
|
|
417
|
+
await logs.flush();
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
402
420
|
await stopDaemon(false);
|
|
403
421
|
return serve();
|
|
404
422
|
}
|
|
@@ -167,7 +167,7 @@ class ControlServer {
|
|
|
167
167
|
}
|
|
168
168
|
if (parts[0] === "api" && parts[1] === "agents" && parts[2] && parts[3] === "v1") {
|
|
169
169
|
const id = decodeURIComponent(parts[2]);
|
|
170
|
-
if (
|
|
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
172
|
const adapter = new openai_adapter_1.OpenAIAdapter(async (task, _signal, context) => {
|
|
173
173
|
if (!context)
|
|
@@ -66,6 +66,7 @@ async function updateCoreAtStartup(options) {
|
|
|
66
66
|
await (0, promises_1.rm)(packageRoot, { recursive: true, force: true });
|
|
67
67
|
await (0, promises_1.cp)(backup, packageRoot, { recursive: true });
|
|
68
68
|
options.log?.("error", "daemon 自动升级失败,已恢复旧版本", { error: error instanceof Error ? error.message : String(error), version: metadata.version });
|
|
69
|
+
await options.startDaemon().catch((restartError) => options.log?.("error", "旧 daemon 恢复启动失败", { error: restartError instanceof Error ? restartError.message : String(restartError), version: metadata.version }));
|
|
69
70
|
await (0, promises_1.rm)(backupRoot, { recursive: true, force: true });
|
|
70
71
|
return false;
|
|
71
72
|
}
|
|
@@ -169,6 +169,7 @@ class ChatgptCdpClient {
|
|
|
169
169
|
return { supported: true, turns: result.turns, complete: false, missingReasons };
|
|
170
170
|
}
|
|
171
171
|
async switchSession(session) {
|
|
172
|
+
await this.waitForGeneration();
|
|
172
173
|
await this.recoverDraft();
|
|
173
174
|
const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
|
|
174
175
|
item.getAttribute('data-app-action-sidebar-thread-id') === ${JSON.stringify(session.id)}
|
|
@@ -192,6 +193,7 @@ class ChatgptCdpClient {
|
|
|
192
193
|
})()`, "ChatGPT 会话切换未确认,未发送消息");
|
|
193
194
|
}
|
|
194
195
|
async createSession(_name) {
|
|
196
|
+
await this.waitForGeneration();
|
|
195
197
|
await this.recoverDraft();
|
|
196
198
|
const before = await this.activeSession();
|
|
197
199
|
const state = await this.evaluate(`JSON.stringify({ editor: document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText ?? null, turns: document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length, generating: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) })`);
|
|
@@ -336,6 +338,7 @@ class ChatgptCdpClient {
|
|
|
336
338
|
}
|
|
337
339
|
async directTurn(text, timeoutMs, attachments, slot, outputDirectory, startedAt) {
|
|
338
340
|
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
|
|
341
|
+
await this.waitForGeneration();
|
|
339
342
|
await this.recoverDraft();
|
|
340
343
|
const beforeTurnsJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})].map(turn => ({ id: turn.getAttribute(${JSON.stringify(turnIdAttribute)}), users: turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length })))`);
|
|
341
344
|
const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
|
|
@@ -658,6 +661,9 @@ class ChatgptCdpClient {
|
|
|
658
661
|
}
|
|
659
662
|
throw new Error(message);
|
|
660
663
|
}
|
|
664
|
+
waitForGeneration() {
|
|
665
|
+
return this.waitFor(`!Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}))`, "ChatGPT 桌面端仍在生成回复,等待超时;未清理输入或切换会话");
|
|
666
|
+
}
|
|
661
667
|
rejectPending(error) {
|
|
662
668
|
for (const pending of this.pending.values())
|
|
663
669
|
pending.reject(error);
|
|
@@ -73,13 +73,11 @@ class OpenAIAdapter {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
const session = req.headers["x-evolcore-session-id"];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
76
|
+
for (const name of ["x-evolcore-session-id", "x-aun-peer", "x-evolcore-agent-aid"]) {
|
|
77
|
+
const value = req.headers[name];
|
|
78
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim() || value.length > 256 || /[\x00-\x1f\x7f-\x9f]/.test(value))) {
|
|
79
|
+
json(res, 400, { error: { message: `${name} 必须是 1 到 256 个字符且不含控制字符的字符串`, type: "invalid_request_error" } });
|
|
80
|
+
return;
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
const abort = new AbortController();
|
|
@@ -100,7 +98,8 @@ class OpenAIAdapter {
|
|
|
100
98
|
}
|
|
101
99
|
this.onLog?.(`chat request model=${payload.model ?? this.model} stream=${Boolean(payload.stream)}`);
|
|
102
100
|
this.onExchange?.("request", prompt, peerId, agentAid);
|
|
103
|
-
const
|
|
101
|
+
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);
|
|
104
103
|
this.onExchange?.("reply", content, peerId, agentAid);
|
|
105
104
|
if (abort.signal.aborted || res.destroyed)
|
|
106
105
|
return;
|
|
@@ -120,7 +120,7 @@ class WorkbuddyCdpClient {
|
|
|
120
120
|
const before = await this.evaluate(`(() => { const state = ${snapshot}; const loginRequired = [...document.querySelectorAll('a,button,[role="button"]')].some(item => /^(?:登录|立即登录|sign in)$/i.test(item.textContent?.trim() ?? '')); return { ...state, loginRequired }; })()`);
|
|
121
121
|
if (before.loginRequired)
|
|
122
122
|
throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
|
|
123
|
-
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find(item => { const label = [item.innerText, item.textContent, item.getAttribute('aria-label'), item.getAttribute('title'), item.getAttribute('data-testid')].filter(Boolean).map(value => String(value).normalize('NFKC').replace(/\\s+/g, ' ').trim().toLowerCase()); return label.some(value => /^(?:新建任务|新建会话|新建对话|开始新任务|new (?:task|chat|conversation))$/.test(value)) && (!item.getClientRects || item.getClientRects().length > 0); }); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
|
|
123
|
+
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"],[role="tab"]')].find(item => { const label = [item.innerText, item.textContent, item.getAttribute('aria-label'), item.getAttribute('title'), item.getAttribute('data-testid')].filter(Boolean).map(value => String(value).normalize('NFKC').replace(/\\s+/g, ' ').trim().toLowerCase()); return label.some(value => /^(?:新建任务|新建会话|新建对话|开始新任务|new (?:task|chat|conversation))$/.test(value)) && (!item.getClientRects || item.getClientRects().length > 0); }); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
|
|
124
124
|
throw new Error("WorkBuddy 新建任务入口未找到或不可用");
|
|
125
125
|
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
126
126
|
await this.waitFor(`(() => { const state = ${snapshot}; const blank = state.editor !== null && state.editor.trim() === '' && state.messages === 0; return blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected)); })()`, "WorkBuddy 新任务状态校验失败");
|
|
@@ -15,7 +15,7 @@ const session_history_1 = require("./session-history");
|
|
|
15
15
|
const desktop_launch_1 = require("../desktop-launch");
|
|
16
16
|
const desktop_launch_2 = require("./desktop-launch");
|
|
17
17
|
exports.workbuddyManifest = {
|
|
18
|
-
id: "workbuddy", version: "1.1.
|
|
18
|
+
id: "workbuddy", version: "1.1.19", displayName: "WorkBuddy", type: "agent",
|
|
19
19
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
20
20
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
21
21
|
};
|
|
@@ -52,6 +52,7 @@ class WorkbuddyPlugin {
|
|
|
52
52
|
/** 重载后待恢复的会话身份。 */
|
|
53
53
|
recoverableSessionId;
|
|
54
54
|
recoverableConversationId;
|
|
55
|
+
sessionBindings = new Map();
|
|
55
56
|
activeDesktopConversationId;
|
|
56
57
|
desktopSessionValid = true;
|
|
57
58
|
knownSessions = new Set();
|
|
@@ -81,6 +82,7 @@ class WorkbuddyPlugin {
|
|
|
81
82
|
this.activeSessionId = undefined;
|
|
82
83
|
this.recoverableSessionId = undefined;
|
|
83
84
|
this.recoverableConversationId = undefined;
|
|
85
|
+
this.sessionBindings.clear();
|
|
84
86
|
this.activeDesktopConversationId = undefined;
|
|
85
87
|
this.activeExpert = undefined;
|
|
86
88
|
this.expertSession = false;
|
|
@@ -94,6 +96,8 @@ class WorkbuddyPlugin {
|
|
|
94
96
|
const saved = await this.loadKnownSessions(this.sessionDataDir);
|
|
95
97
|
for (const sessionId of saved.sessionIds)
|
|
96
98
|
this.knownSessions.add(sessionId);
|
|
99
|
+
for (const [sessionId, binding] of Object.entries(saved.bindings ?? {}))
|
|
100
|
+
this.sessionBindings.set(sessionId, binding);
|
|
97
101
|
savedBinding = saved.active;
|
|
98
102
|
}
|
|
99
103
|
this.sessionOperations = [];
|
|
@@ -265,22 +269,22 @@ class WorkbuddyPlugin {
|
|
|
265
269
|
const timeout = request.timeoutMs ?? this.timeoutMs;
|
|
266
270
|
const expert = extractExpertQuery(request.task);
|
|
267
271
|
const usesCli = Boolean(this.cliPath && this.transport !== "cdp" && !expert);
|
|
268
|
-
// 内部恢复消息可能没有 sessionId,沿用当前桌面会话,避免恢复任务被会话保护拒绝。
|
|
269
|
-
const sessionId = request.sessionId ?? this.activeSessionId;
|
|
270
272
|
const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
|
|
271
273
|
if (hasAttachments && !this.attachmentInput)
|
|
272
274
|
throw new Error("WorkBuddy 当前传输不支持附件输入");
|
|
273
275
|
if (!usesCli && !this.client?.connected())
|
|
274
276
|
await this.reconnect?.();
|
|
275
277
|
const pendingInteractionSessionId = this.client?.pendingInteractionSessionId?.();
|
|
276
|
-
if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== sessionId)
|
|
278
|
+
if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== request.sessionId)
|
|
277
279
|
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
278
280
|
if (usesCli && request.sessionId)
|
|
279
281
|
throw new Error("CodeBuddy CLI 会话隔离未验证,不能处理带 sessionId 的请求");
|
|
280
|
-
if (!usesCli && !sessionId &&
|
|
282
|
+
if (!usesCli && !request.sessionId && this.activeSessionId)
|
|
283
|
+
throw new Error("WorkBuddy 已绑定逻辑会话,请使用相同 sessionId 或先执行 /new");
|
|
284
|
+
if (!usesCli && !request.sessionId && !this.desktopSessionValid)
|
|
281
285
|
throw new Error("WorkBuddy 桌面任务状态未知,请先创建新会话");
|
|
282
286
|
if (!usesCli)
|
|
283
|
-
await this.ensureSession(sessionId);
|
|
287
|
+
await this.ensureSession(request.sessionId);
|
|
284
288
|
if (expert) {
|
|
285
289
|
if (!this.client?.connected())
|
|
286
290
|
throw new Error(`专家请求需要已连接的 WorkBuddy 桌面端:${expert}`);
|
|
@@ -336,11 +340,14 @@ class WorkbuddyPlugin {
|
|
|
336
340
|
return;
|
|
337
341
|
}
|
|
338
342
|
if (this.knownSessions.has(sessionId)) {
|
|
339
|
-
|
|
343
|
+
const binding = this.sessionBindings.get(sessionId);
|
|
344
|
+
const conversationId = binding?.conversationId ?? (this.recoverableSessionId === sessionId ? this.recoverableConversationId : undefined);
|
|
345
|
+
if (!conversationId || !this.client.activateConversation)
|
|
340
346
|
throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
|
|
341
|
-
await this.client.activateConversation(
|
|
347
|
+
await this.client.activateConversation(conversationId);
|
|
342
348
|
this.activeSessionId = sessionId;
|
|
343
|
-
this.activeDesktopConversationId =
|
|
349
|
+
this.activeDesktopConversationId = conversationId;
|
|
350
|
+
this.activeExpert = binding?.expert;
|
|
344
351
|
this.desktopSessionValid = true;
|
|
345
352
|
this.recoverableSessionId = undefined;
|
|
346
353
|
this.recoverableConversationId = undefined;
|
|
@@ -374,7 +381,14 @@ class WorkbuddyPlugin {
|
|
|
374
381
|
const active = parsed.active;
|
|
375
382
|
if (active !== undefined && (!active || typeof active.sessionId !== "string" || !sessionIds.includes(active.sessionId) || typeof active.conversationId !== "string" || !active.conversationId.trim() || (active.expert !== undefined && active.expert !== null && typeof active.expert !== "string")))
|
|
376
383
|
throw new Error("活动会话绑定无效");
|
|
377
|
-
|
|
384
|
+
const activeBinding = active;
|
|
385
|
+
const bindingsValue = parsed.bindings;
|
|
386
|
+
if (bindingsValue !== undefined && (!bindingsValue || typeof bindingsValue !== "object" || Array.isArray(bindingsValue) || Object.entries(bindingsValue).some(([sessionId, binding]) => !sessionIds.includes(sessionId) || !binding || typeof binding !== "object" || typeof binding.conversationId !== "string" || !binding.conversationId.trim() || (binding.expert !== undefined && binding.expert !== null && typeof binding.expert !== "string"))))
|
|
387
|
+
throw new Error("会话绑定列表无效");
|
|
388
|
+
const bindings = (bindingsValue ?? {});
|
|
389
|
+
if (activeBinding && !bindings[activeBinding.sessionId])
|
|
390
|
+
bindings[activeBinding.sessionId] = { conversationId: activeBinding.conversationId, expert: activeBinding.expert };
|
|
391
|
+
return { sessionIds: new Set(sessionIds), ...(activeBinding ? { active: activeBinding } : {}), bindings };
|
|
378
392
|
}
|
|
379
393
|
catch (error) {
|
|
380
394
|
if (error.code === "ENOENT")
|
|
@@ -391,7 +405,9 @@ class WorkbuddyPlugin {
|
|
|
391
405
|
await (0, promises_1.mkdir)(this.sessionDataDir, { recursive: true });
|
|
392
406
|
const file = (0, node_path_1.join)(this.sessionDataDir, "known-sessions.json");
|
|
393
407
|
const active = this.desktopSessionValid && this.activeSessionId && this.activeDesktopConversationId ? { sessionId: this.activeSessionId, conversationId: this.activeDesktopConversationId, expert: this.activeExpert } : undefined;
|
|
394
|
-
|
|
408
|
+
if (active)
|
|
409
|
+
this.sessionBindings.set(active.sessionId, { conversationId: active.conversationId, expert: active.expert });
|
|
410
|
+
await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify({ sessionIds: [...this.knownSessions].sort(), active, bindings: Object.fromEntries(this.sessionBindings) })}\n`, { mode: 0o600 });
|
|
395
411
|
await (0, promises_1.rename)(`${file}.tmp`, file);
|
|
396
412
|
});
|
|
397
413
|
this.sessionWrites = write;
|
|
@@ -367,7 +367,7 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
|
|
|
367
367
|
strict_1.default.equal(sends, 1);
|
|
368
368
|
await plugin.deactivate();
|
|
369
369
|
});
|
|
370
|
-
(0, node_test_1.default)("ChatGPT 固定 CDP
|
|
370
|
+
(0, node_test_1.default)("ChatGPT 固定 CDP 操作按主机和 ID 切换,生成中等待且超时不切换", async () => {
|
|
371
371
|
const { runInNewContext } = await Promise.resolve().then(() => __importStar(require("node:vm")));
|
|
372
372
|
const entries = [{ id: 'a"\\', host: "h1", title: "旧会话" }, { id: 'a"\\', host: "h2", title: "目标会话" }];
|
|
373
373
|
let selected = 0;
|
|
@@ -384,6 +384,7 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
|
|
|
384
384
|
};
|
|
385
385
|
let onMessage = null;
|
|
386
386
|
const client = new chatgpt_1.ChatgptCdpClient({
|
|
387
|
+
timeoutMs: 300,
|
|
387
388
|
fetchImpl: async () => new Response(JSON.stringify([{ type: "page", url: "app://-/index.html", webSocketDebuggerUrl: "ws://localhost:9230/a" }])),
|
|
388
389
|
webSocketFactory: () => ({
|
|
389
390
|
set onmessage(fn) { onMessage = fn; },
|
|
@@ -409,8 +410,9 @@ const chatgpt_1 = require("../src-ts/plugins/chatgpt");
|
|
|
409
410
|
strict_1.default.equal(selected, 1);
|
|
410
411
|
strict_1.default.deepEqual(await client.activeSession(), entries[1]);
|
|
411
412
|
generating = true;
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
setTimeout(() => { generating = false; }, 100);
|
|
414
|
+
await client.switchSession(entries[0]);
|
|
415
|
+
strict_1.default.equal(selected, 0);
|
|
414
416
|
}
|
|
415
417
|
finally {
|
|
416
418
|
client.close();
|
|
@@ -447,16 +447,39 @@ const plugin_manager_1 = require("../src-ts/plugin-manager");
|
|
|
447
447
|
await server.stop();
|
|
448
448
|
}
|
|
449
449
|
});
|
|
450
|
+
(0, node_test_1.default)("OpenAI 未提供会话头时按对端和本端 AID建立稳定会话", async () => {
|
|
451
|
+
const calls = [];
|
|
452
|
+
const server = new control_server_1.ControlServer({ manager: { handle: async (id, request) => { calls.push({ id, request }); return { text: "回复" }; }, recordMessage() { } }, host: "127.0.0.1", port: 0 });
|
|
453
|
+
const address = await server.start();
|
|
454
|
+
try {
|
|
455
|
+
const send = async (peer, aid = "workassists.agentid.cn") => {
|
|
456
|
+
const response = await fetch(`http://127.0.0.1:${address.port}/api/agents/workbuddy/v1/chat/completions`, {
|
|
457
|
+
method: "POST", headers: { "x-aun-peer": peer, "x-evolcore-agent-aid": aid },
|
|
458
|
+
body: JSON.stringify({ messages: [{ role: "user", content: "继续当前任务" }] }),
|
|
459
|
+
});
|
|
460
|
+
strict_1.default.equal(response.status, 200);
|
|
461
|
+
return calls.at(-1).request.sessionId;
|
|
462
|
+
};
|
|
463
|
+
const first = await send("peer-a.agentid.cn");
|
|
464
|
+
strict_1.default.match(first, /^openai:[a-f0-9]{64}$/);
|
|
465
|
+
strict_1.default.equal(await send("peer-a.agentid.cn"), first);
|
|
466
|
+
strict_1.default.notEqual(await send("peer-b.agentid.cn"), first);
|
|
467
|
+
strict_1.default.notEqual(await send("peer-a.agentid.cn", "other.agentid.cn"), first);
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
await server.stop();
|
|
471
|
+
}
|
|
472
|
+
});
|
|
450
473
|
(0, node_test_1.default)("未认证远程调用不能使用会话身份,旧请求仍兼容", async () => {
|
|
451
474
|
const calls = [];
|
|
452
475
|
const server = new control_server_1.ControlServer({ manager: { handle: async (_id, request) => { calls.push(request); return { text: "回复" }; }, recordMessage() { } } });
|
|
453
|
-
for (const headers of [{ "x-evolcore-session-id": "client-id" }, { "x-evolcore-session-id": "client-id", authorization: "Bearer undefined" }, {}]) {
|
|
476
|
+
for (const headers of [{ "x-evolcore-session-id": "client-id" }, { "x-evolcore-session-id": "client-id", authorization: "Bearer undefined" }, { "x-aun-peer": "peer.agentid.cn", "x-evolcore-agent-aid": "local.agentid.cn" }, {}]) {
|
|
454
477
|
const req = Object.assign(node_stream_1.Readable.from([JSON.stringify({ messages: [{ role: "user", content: "测试" }] })]), {
|
|
455
478
|
method: "POST", url: "/api/agents/workbuddy/v1/chat/completions", headers, socket: { remoteAddress: "192.168.31.10" },
|
|
456
479
|
});
|
|
457
480
|
const res = Object.assign(new node_events_1.EventEmitter(), { statusCode: 0, writableEnded: false, setHeader() { }, end() { this.writableEnded = true; } });
|
|
458
481
|
await server.handle(req, res);
|
|
459
|
-
strict_1.default.equal(res.statusCode, "x-evolcore-session-id" in headers ? 401 : 200);
|
|
482
|
+
strict_1.default.equal(res.statusCode, "x-evolcore-session-id" in headers || "x-aun-peer" in headers ? 401 : 200);
|
|
460
483
|
}
|
|
461
484
|
strict_1.default.deepEqual(calls, [{ task: "测试" }]);
|
|
462
485
|
});
|
|
@@ -48,6 +48,7 @@ async function fixture() {
|
|
|
48
48
|
});
|
|
49
49
|
(0, node_test_1.default)("新 daemon 健康检查失败时恢复旧核心包并继续旧进程", async () => {
|
|
50
50
|
const value = await fixture();
|
|
51
|
+
const events = [];
|
|
51
52
|
try {
|
|
52
53
|
const updated = await (0, core_updater_1.updateCoreAtStartup)({
|
|
53
54
|
packageRoot: value.packageRoot,
|
|
@@ -59,12 +60,13 @@ async function fixture() {
|
|
|
59
60
|
await (0, promises_1.writeFile)((0, node_path_1.join)(value.packageRoot, "package.json"), JSON.stringify({ name: "mu-agent-bridge", version: "1.1.0" }));
|
|
60
61
|
return "";
|
|
61
62
|
},
|
|
62
|
-
stopDaemon: async () =>
|
|
63
|
-
startDaemon: async () =>
|
|
64
|
-
waitForHealth: async () => false,
|
|
63
|
+
stopDaemon: async () => { events.push("stop"); },
|
|
64
|
+
startDaemon: async () => { events.push("start"); },
|
|
65
|
+
waitForHealth: async () => { events.push("health"); return false; },
|
|
65
66
|
});
|
|
66
67
|
strict_1.default.equal(updated, false);
|
|
67
68
|
strict_1.default.equal(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(value.packageRoot, "package.json"), "utf8")).version, "1.0.0");
|
|
69
|
+
strict_1.default.deepEqual(events, ["stop", "start", "health", "stop", "start"]);
|
|
68
70
|
}
|
|
69
71
|
finally {
|
|
70
72
|
await (0, promises_1.rm)(value.root, { recursive: true, force: true });
|
|
@@ -44,6 +44,8 @@ const node_util_1 = require("node:util");
|
|
|
44
44
|
strict_1.default.match(setup, /\$npmCommand\.Source prefix --global/);
|
|
45
45
|
strict_1.default.match(setup, /@\(\$nodeDirectory, \$npmDirectory, \$npmPrefix\)/);
|
|
46
46
|
strict_1.default.match(setup, /mu-agent-bridge@latest --engine-strict/);
|
|
47
|
+
strict_1.default.match(setup, /--prefer-online/);
|
|
48
|
+
strict_1.default.match(setup, /npm 安装遇到版本尚未同步或文件占用.*重试/);
|
|
47
49
|
strict_1.default.doesNotMatch(setup, /mu-agent-bridge@\d+\.\d+\.\d+/);
|
|
48
50
|
strict_1.default.match(setup, /api\/agents\/availability/);
|
|
49
51
|
strict_1.default.match(setup, /function Find-AgentExecutable/);
|
|
@@ -209,19 +209,6 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
209
209
|
strict_1.default.equal((await plugin.health()).status, "ready");
|
|
210
210
|
await plugin.deactivate("stop");
|
|
211
211
|
});
|
|
212
|
-
(0, node_test_1.default)("workbuddy 内部恢复消息无 sessionId 时沿用当前桌面会话", async () => {
|
|
213
|
-
const calls = [];
|
|
214
|
-
const client = {
|
|
215
|
-
connect: async () => undefined, close() { }, connected: () => true,
|
|
216
|
-
direct: async (task) => { calls.push(task); return "恢复成功"; },
|
|
217
|
-
};
|
|
218
|
-
const plugin = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
219
|
-
await plugin.activate({ config: { transport: "cdp" }, logger: { info() { }, warn() { }, error() { } } });
|
|
220
|
-
plugin.activeSessionId = "session-active";
|
|
221
|
-
await strict_1.default.doesNotReject(() => plugin.handle({ task: "继续当前任务" }));
|
|
222
|
-
strict_1.default.deepEqual(calls, ["继续当前任务"]);
|
|
223
|
-
await plugin.deactivate("stop");
|
|
224
|
-
});
|
|
225
212
|
(0, node_test_1.default)("workbuddy 结构化客户端接收附件并保留附件响应和逻辑会话", async () => {
|
|
226
213
|
const attachment = { kind: "file", data: new Uint8Array([1, 2]), filename: "输入.txt", contentType: "text/plain" };
|
|
227
214
|
const output = { kind: "file", data: new Uint8Array([3]), filename: "输出.txt", contentType: "text/plain" };
|
|
@@ -991,6 +978,22 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
991
978
|
strict_1.default.equal(conversationId, null);
|
|
992
979
|
strict_1.default.equal(messages, 0);
|
|
993
980
|
});
|
|
981
|
+
(0, node_test_1.default)("workbuddy CDP 识别新版侧栏 role=tab 新建任务入口", async () => {
|
|
982
|
+
let conversationId = "old-conversation";
|
|
983
|
+
let messages = 1;
|
|
984
|
+
const button = { innerText: "新建任务", disabled: false, getAttribute: () => null, getClientRects: () => [{}], click: () => { conversationId = null; messages = 0; } };
|
|
985
|
+
const editor = { innerText: "今天帮你做些什么?", querySelector: () => conversationId === null ? {} : null, cloneNode: () => ({ textContent: "", querySelectorAll: () => [] }) };
|
|
986
|
+
const document = {
|
|
987
|
+
querySelectorAll: (selector) => selector === '.cr-agent[data-message-id]' ? Array(messages).fill({}) : selector === '[data-conversation-id]' ? conversationId ? [{ className: "_selected_old", getAttribute: () => conversationId }] : [] : selector.includes('[role="tab"]') ? [button] : [],
|
|
988
|
+
querySelector: () => editor,
|
|
989
|
+
};
|
|
990
|
+
const client = new cdp_client_1.WorkbuddyCdpClient({ timeoutMs: 1 });
|
|
991
|
+
const internal = client;
|
|
992
|
+
internal.evaluate = async (expression) => (0, node_vm_1.runInNewContext)(expression, { document, location: { href: "app://workbuddy/task/old" } });
|
|
993
|
+
await client.createTask();
|
|
994
|
+
strict_1.default.equal(conversationId, null);
|
|
995
|
+
strict_1.default.equal(messages, 0);
|
|
996
|
+
});
|
|
994
997
|
(0, node_test_1.default)("workbuddy CDP 新建不接受缺少任务身份的旧消息界面直接清空", async () => {
|
|
995
998
|
let messages = 1;
|
|
996
999
|
const button = { textContent: "新建任务", disabled: false, getAttribute: () => null, click: () => { messages = 0; } };
|
|
@@ -1134,6 +1137,42 @@ const cdp_client_1 = require("../src-ts/plugins/workbuddy/cdp-client");
|
|
|
1134
1137
|
await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
|
|
1135
1138
|
}
|
|
1136
1139
|
});
|
|
1140
|
+
(0, node_test_1.default)("workbuddy 多个 AUN 会话交替后重载仍分别恢复桌面会话", async () => {
|
|
1141
|
+
const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-multiple-sessions-"));
|
|
1142
|
+
let creates = 0;
|
|
1143
|
+
let desktopId = "desktop-initial";
|
|
1144
|
+
const calls = [];
|
|
1145
|
+
const client = {
|
|
1146
|
+
connect: async () => undefined, close() { }, connected: () => true,
|
|
1147
|
+
conversationId: async () => desktopId,
|
|
1148
|
+
activateConversation: async (id) => { desktopId = id; },
|
|
1149
|
+
createTask: async () => { desktopId = `desktop-${++creates}`; },
|
|
1150
|
+
direct: async (task) => { calls.push(`${desktopId}:${task}`); return task; },
|
|
1151
|
+
};
|
|
1152
|
+
const context = { dataDir, config: { transport: "cdp" }, logger: { info() { }, warn() { }, error() { } } };
|
|
1153
|
+
try {
|
|
1154
|
+
const first = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
1155
|
+
await first.activate(context);
|
|
1156
|
+
await first.handle({ task: "peer-a 首条", sessionId: "session-a" });
|
|
1157
|
+
await first.handle({ task: "peer-b 首条", sessionId: "session-b" });
|
|
1158
|
+
await first.handle({ task: "peer-a 续聊", sessionId: "session-a" });
|
|
1159
|
+
await first.deactivate("reload");
|
|
1160
|
+
desktopId = "desktop-after-restart";
|
|
1161
|
+
const second = new workbuddy_1.WorkbuddyPlugin({ client });
|
|
1162
|
+
await second.activate(context);
|
|
1163
|
+
await second.handle({ task: "peer-b 重载后续聊", sessionId: "session-b" });
|
|
1164
|
+
await second.handle({ task: "peer-a 重载后续聊", sessionId: "session-a" });
|
|
1165
|
+
strict_1.default.deepEqual(calls, [
|
|
1166
|
+
"desktop-1:peer-a 首条", "desktop-2:peer-b 首条", "desktop-1:peer-a 续聊",
|
|
1167
|
+
"desktop-2:peer-b 重载后续聊", "desktop-1:peer-a 重载后续聊",
|
|
1168
|
+
]);
|
|
1169
|
+
strict_1.default.equal(creates, 2);
|
|
1170
|
+
await second.deactivate("stop");
|
|
1171
|
+
}
|
|
1172
|
+
finally {
|
|
1173
|
+
await (0, promises_1.rm)(dataDir, { recursive: true, force: true });
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1137
1176
|
(0, node_test_1.default)("workbuddy 新建失败的会话 ID 重启后仍禁止重试", async () => {
|
|
1138
1177
|
const dataDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "wb-failed-session-"));
|
|
1139
1178
|
let creates = 0;
|
package/package.json
CHANGED
|
@@ -169,6 +169,7 @@ class ChatgptCdpClient {
|
|
|
169
169
|
return { supported: true, turns: result.turns, complete: false, missingReasons };
|
|
170
170
|
}
|
|
171
171
|
async switchSession(session) {
|
|
172
|
+
await this.waitForGeneration();
|
|
172
173
|
await this.recoverDraft();
|
|
173
174
|
const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
|
|
174
175
|
item.getAttribute('data-app-action-sidebar-thread-id') === ${JSON.stringify(session.id)}
|
|
@@ -192,6 +193,7 @@ class ChatgptCdpClient {
|
|
|
192
193
|
})()`, "ChatGPT 会话切换未确认,未发送消息");
|
|
193
194
|
}
|
|
194
195
|
async createSession(_name) {
|
|
196
|
+
await this.waitForGeneration();
|
|
195
197
|
await this.recoverDraft();
|
|
196
198
|
const before = await this.activeSession();
|
|
197
199
|
const state = await this.evaluate(`JSON.stringify({ editor: document.querySelector(${JSON.stringify(exports.editorSelector)})?.innerText ?? null, turns: document.querySelectorAll(${JSON.stringify(exports.turnSelector)}).length, generating: Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)})) })`);
|
|
@@ -336,6 +338,7 @@ class ChatgptCdpClient {
|
|
|
336
338
|
}
|
|
337
339
|
async directTurn(text, timeoutMs, attachments, slot, outputDirectory, startedAt) {
|
|
338
340
|
await this.waitFor(`Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}))`, "ChatGPT 桌面端输入框未就绪");
|
|
341
|
+
await this.waitForGeneration();
|
|
339
342
|
await this.recoverDraft();
|
|
340
343
|
const beforeTurnsJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.turnSelector)})].map(turn => ({ id: turn.getAttribute(${JSON.stringify(turnIdAttribute)}), users: turn.querySelectorAll(${JSON.stringify(exports.userBubbleSelector)}).length })))`);
|
|
341
344
|
const beforeTurns = JSON.parse(String(beforeTurnsJson || "[]"));
|
|
@@ -658,6 +661,9 @@ class ChatgptCdpClient {
|
|
|
658
661
|
}
|
|
659
662
|
throw new Error(message);
|
|
660
663
|
}
|
|
664
|
+
waitForGeneration() {
|
|
665
|
+
return this.waitFor(`!Boolean(document.querySelector(${JSON.stringify(stopButtonSelector)}))`, "ChatGPT 桌面端仍在生成回复,等待超时;未清理输入或切换会话");
|
|
666
|
+
}
|
|
661
667
|
rejectPending(error) {
|
|
662
668
|
for (const pending of this.pending.values())
|
|
663
669
|
pending.reject(error);
|
|
@@ -73,13 +73,11 @@ class OpenAIAdapter {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
const session = req.headers["x-evolcore-session-id"];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
76
|
+
for (const name of ["x-evolcore-session-id", "x-aun-peer", "x-evolcore-agent-aid"]) {
|
|
77
|
+
const value = req.headers[name];
|
|
78
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim() || value.length > 256 || /[\x00-\x1f\x7f-\x9f]/.test(value))) {
|
|
79
|
+
json(res, 400, { error: { message: `${name} 必须是 1 到 256 个字符且不含控制字符的字符串`, type: "invalid_request_error" } });
|
|
80
|
+
return;
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
const abort = new AbortController();
|
|
@@ -100,7 +98,8 @@ class OpenAIAdapter {
|
|
|
100
98
|
}
|
|
101
99
|
this.onLog?.(`chat request model=${payload.model ?? this.model} stream=${Boolean(payload.stream)}`);
|
|
102
100
|
this.onExchange?.("request", prompt, peerId, agentAid);
|
|
103
|
-
const
|
|
101
|
+
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);
|
|
104
103
|
this.onExchange?.("reply", content, peerId, agentAid);
|
|
105
104
|
if (abort.signal.aborted || res.destroyed)
|
|
106
105
|
return;
|
|
@@ -73,13 +73,11 @@ class OpenAIAdapter {
|
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
const session = req.headers["x-evolcore-session-id"];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
76
|
+
for (const name of ["x-evolcore-session-id", "x-aun-peer", "x-evolcore-agent-aid"]) {
|
|
77
|
+
const value = req.headers[name];
|
|
78
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim() || value.length > 256 || /[\x00-\x1f\x7f-\x9f]/.test(value))) {
|
|
79
|
+
json(res, 400, { error: { message: `${name} 必须是 1 到 256 个字符且不含控制字符的字符串`, type: "invalid_request_error" } });
|
|
80
|
+
return;
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
const abort = new AbortController();
|
|
@@ -100,7 +98,8 @@ class OpenAIAdapter {
|
|
|
100
98
|
}
|
|
101
99
|
this.onLog?.(`chat request model=${payload.model ?? this.model} stream=${Boolean(payload.stream)}`);
|
|
102
100
|
this.onExchange?.("request", prompt, peerId, agentAid);
|
|
103
|
-
const
|
|
101
|
+
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);
|
|
104
103
|
this.onExchange?.("reply", content, peerId, agentAid);
|
|
105
104
|
if (abort.signal.aborted || res.destroyed)
|
|
106
105
|
return;
|
|
@@ -120,7 +120,7 @@ class WorkbuddyCdpClient {
|
|
|
120
120
|
const before = await this.evaluate(`(() => { const state = ${snapshot}; const loginRequired = [...document.querySelectorAll('a,button,[role="button"]')].some(item => /^(?:登录|立即登录|sign in)$/i.test(item.textContent?.trim() ?? '')); return { ...state, loginRequired }; })()`);
|
|
121
121
|
if (before.loginRequired)
|
|
122
122
|
throw new Error("WorkBuddy 尚未登录,请先在桌面端登录后重试");
|
|
123
|
-
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"]')].find(item => { const label = [item.innerText, item.textContent, item.getAttribute('aria-label'), item.getAttribute('title'), item.getAttribute('data-testid')].filter(Boolean).map(value => String(value).normalize('NFKC').replace(/\\s+/g, ' ').trim().toLowerCase()); return label.some(value => /^(?:新建任务|新建会话|新建对话|开始新任务|new (?:task|chat|conversation))$/.test(value)) && (!item.getClientRects || item.getClientRects().length > 0); }); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
|
|
123
|
+
if (!await this.evaluate(`(() => { const button = [...document.querySelectorAll('button,[role="button"],[role="tab"]')].find(item => { const label = [item.innerText, item.textContent, item.getAttribute('aria-label'), item.getAttribute('title'), item.getAttribute('data-testid')].filter(Boolean).map(value => String(value).normalize('NFKC').replace(/\\s+/g, ' ').trim().toLowerCase()); return label.some(value => /^(?:新建任务|新建会话|新建对话|开始新任务|new (?:task|chat|conversation))$/.test(value)) && (!item.getClientRects || item.getClientRects().length > 0); }); if (!button || button.disabled || button.getAttribute('aria-disabled') === 'true') return false; button.click(); return true; })()`))
|
|
124
124
|
throw new Error("WorkBuddy 新建任务入口未找到或不可用");
|
|
125
125
|
// WorkBuddy 首页不分配任务 ID;此时仅接受无选中任务的空白首页。
|
|
126
126
|
await this.waitFor(`(() => { const state = ${snapshot}; const blank = state.editor !== null && state.editor.trim() === '' && state.messages === 0; return blank && ((state.conversationId && state.conversationId !== ${JSON.stringify(before.conversationId)}) || state.url !== ${JSON.stringify(before.url)} || (state.home && !state.selected)); })()`, "WorkBuddy 新任务状态校验失败");
|
|
@@ -15,7 +15,7 @@ const session_history_1 = require("./session-history");
|
|
|
15
15
|
const desktop_launch_1 = require("../desktop-launch");
|
|
16
16
|
const desktop_launch_2 = require("./desktop-launch");
|
|
17
17
|
exports.workbuddyManifest = {
|
|
18
|
-
id: "workbuddy", version: "1.1.
|
|
18
|
+
id: "workbuddy", version: "1.1.19", displayName: "WorkBuddy", type: "agent",
|
|
19
19
|
entry: "dist/index.js", apiVersion: "1", process: "in-process",
|
|
20
20
|
capabilities: ["chat", "health"], configSchema: "config.schema.json",
|
|
21
21
|
};
|
|
@@ -52,6 +52,7 @@ class WorkbuddyPlugin {
|
|
|
52
52
|
/** 重载后待恢复的会话身份。 */
|
|
53
53
|
recoverableSessionId;
|
|
54
54
|
recoverableConversationId;
|
|
55
|
+
sessionBindings = new Map();
|
|
55
56
|
activeDesktopConversationId;
|
|
56
57
|
desktopSessionValid = true;
|
|
57
58
|
knownSessions = new Set();
|
|
@@ -81,6 +82,7 @@ class WorkbuddyPlugin {
|
|
|
81
82
|
this.activeSessionId = undefined;
|
|
82
83
|
this.recoverableSessionId = undefined;
|
|
83
84
|
this.recoverableConversationId = undefined;
|
|
85
|
+
this.sessionBindings.clear();
|
|
84
86
|
this.activeDesktopConversationId = undefined;
|
|
85
87
|
this.activeExpert = undefined;
|
|
86
88
|
this.expertSession = false;
|
|
@@ -94,6 +96,8 @@ class WorkbuddyPlugin {
|
|
|
94
96
|
const saved = await this.loadKnownSessions(this.sessionDataDir);
|
|
95
97
|
for (const sessionId of saved.sessionIds)
|
|
96
98
|
this.knownSessions.add(sessionId);
|
|
99
|
+
for (const [sessionId, binding] of Object.entries(saved.bindings ?? {}))
|
|
100
|
+
this.sessionBindings.set(sessionId, binding);
|
|
97
101
|
savedBinding = saved.active;
|
|
98
102
|
}
|
|
99
103
|
this.sessionOperations = [];
|
|
@@ -265,22 +269,22 @@ class WorkbuddyPlugin {
|
|
|
265
269
|
const timeout = request.timeoutMs ?? this.timeoutMs;
|
|
266
270
|
const expert = extractExpertQuery(request.task);
|
|
267
271
|
const usesCli = Boolean(this.cliPath && this.transport !== "cdp" && !expert);
|
|
268
|
-
// 内部恢复消息可能没有 sessionId,沿用当前桌面会话,避免恢复任务被会话保护拒绝。
|
|
269
|
-
const sessionId = request.sessionId ?? this.activeSessionId;
|
|
270
272
|
const hasAttachments = Array.isArray(request.attachments) && request.attachments.length > 0;
|
|
271
273
|
if (hasAttachments && !this.attachmentInput)
|
|
272
274
|
throw new Error("WorkBuddy 当前传输不支持附件输入");
|
|
273
275
|
if (!usesCli && !this.client?.connected())
|
|
274
276
|
await this.reconnect?.();
|
|
275
277
|
const pendingInteractionSessionId = this.client?.pendingInteractionSessionId?.();
|
|
276
|
-
if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== sessionId)
|
|
278
|
+
if (!usesCli && pendingInteractionSessionId && pendingInteractionSessionId !== request.sessionId)
|
|
277
279
|
throw new Error("WorkBuddy 正在等待另一个会话的选择");
|
|
278
280
|
if (usesCli && request.sessionId)
|
|
279
281
|
throw new Error("CodeBuddy CLI 会话隔离未验证,不能处理带 sessionId 的请求");
|
|
280
|
-
if (!usesCli && !sessionId &&
|
|
282
|
+
if (!usesCli && !request.sessionId && this.activeSessionId)
|
|
283
|
+
throw new Error("WorkBuddy 已绑定逻辑会话,请使用相同 sessionId 或先执行 /new");
|
|
284
|
+
if (!usesCli && !request.sessionId && !this.desktopSessionValid)
|
|
281
285
|
throw new Error("WorkBuddy 桌面任务状态未知,请先创建新会话");
|
|
282
286
|
if (!usesCli)
|
|
283
|
-
await this.ensureSession(sessionId);
|
|
287
|
+
await this.ensureSession(request.sessionId);
|
|
284
288
|
if (expert) {
|
|
285
289
|
if (!this.client?.connected())
|
|
286
290
|
throw new Error(`专家请求需要已连接的 WorkBuddy 桌面端:${expert}`);
|
|
@@ -336,11 +340,14 @@ class WorkbuddyPlugin {
|
|
|
336
340
|
return;
|
|
337
341
|
}
|
|
338
342
|
if (this.knownSessions.has(sessionId)) {
|
|
339
|
-
|
|
343
|
+
const binding = this.sessionBindings.get(sessionId);
|
|
344
|
+
const conversationId = binding?.conversationId ?? (this.recoverableSessionId === sessionId ? this.recoverableConversationId : undefined);
|
|
345
|
+
if (!conversationId || !this.client.activateConversation)
|
|
340
346
|
throw new Error("WorkBuddy 不支持恢复旧 WorkBuddy 会话");
|
|
341
|
-
await this.client.activateConversation(
|
|
347
|
+
await this.client.activateConversation(conversationId);
|
|
342
348
|
this.activeSessionId = sessionId;
|
|
343
|
-
this.activeDesktopConversationId =
|
|
349
|
+
this.activeDesktopConversationId = conversationId;
|
|
350
|
+
this.activeExpert = binding?.expert;
|
|
344
351
|
this.desktopSessionValid = true;
|
|
345
352
|
this.recoverableSessionId = undefined;
|
|
346
353
|
this.recoverableConversationId = undefined;
|
|
@@ -374,7 +381,14 @@ class WorkbuddyPlugin {
|
|
|
374
381
|
const active = parsed.active;
|
|
375
382
|
if (active !== undefined && (!active || typeof active.sessionId !== "string" || !sessionIds.includes(active.sessionId) || typeof active.conversationId !== "string" || !active.conversationId.trim() || (active.expert !== undefined && active.expert !== null && typeof active.expert !== "string")))
|
|
376
383
|
throw new Error("活动会话绑定无效");
|
|
377
|
-
|
|
384
|
+
const activeBinding = active;
|
|
385
|
+
const bindingsValue = parsed.bindings;
|
|
386
|
+
if (bindingsValue !== undefined && (!bindingsValue || typeof bindingsValue !== "object" || Array.isArray(bindingsValue) || Object.entries(bindingsValue).some(([sessionId, binding]) => !sessionIds.includes(sessionId) || !binding || typeof binding !== "object" || typeof binding.conversationId !== "string" || !binding.conversationId.trim() || (binding.expert !== undefined && binding.expert !== null && typeof binding.expert !== "string"))))
|
|
387
|
+
throw new Error("会话绑定列表无效");
|
|
388
|
+
const bindings = (bindingsValue ?? {});
|
|
389
|
+
if (activeBinding && !bindings[activeBinding.sessionId])
|
|
390
|
+
bindings[activeBinding.sessionId] = { conversationId: activeBinding.conversationId, expert: activeBinding.expert };
|
|
391
|
+
return { sessionIds: new Set(sessionIds), ...(activeBinding ? { active: activeBinding } : {}), bindings };
|
|
378
392
|
}
|
|
379
393
|
catch (error) {
|
|
380
394
|
if (error.code === "ENOENT")
|
|
@@ -391,7 +405,9 @@ class WorkbuddyPlugin {
|
|
|
391
405
|
await (0, promises_1.mkdir)(this.sessionDataDir, { recursive: true });
|
|
392
406
|
const file = (0, node_path_1.join)(this.sessionDataDir, "known-sessions.json");
|
|
393
407
|
const active = this.desktopSessionValid && this.activeSessionId && this.activeDesktopConversationId ? { sessionId: this.activeSessionId, conversationId: this.activeDesktopConversationId, expert: this.activeExpert } : undefined;
|
|
394
|
-
|
|
408
|
+
if (active)
|
|
409
|
+
this.sessionBindings.set(active.sessionId, { conversationId: active.conversationId, expert: active.expert });
|
|
410
|
+
await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify({ sessionIds: [...this.knownSessions].sort(), active, bindings: Object.fromEntries(this.sessionBindings) })}\n`, { mode: 0o600 });
|
|
395
411
|
await (0, promises_1.rename)(`${file}.tmp`, file);
|
|
396
412
|
});
|
|
397
413
|
this.sessionWrites = write;
|