mu-agent-bridge 1.0.34 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +4 -2
  2. package/dist/src-ts/channel-sessions.js +90 -24
  3. package/dist/src-ts/control-server.js +48 -7
  4. package/dist/src-ts/dsh-agent.js +141 -46
  5. package/dist/src-ts/headless-account.patch.yml +8 -1
  6. package/dist/src-ts/plugin-manager.js +69 -3
  7. package/dist/src-ts/plugins/aun/attachments.js +44 -13
  8. package/dist/src-ts/plugins/aun/index.js +5 -6
  9. package/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  10. package/dist/src-ts/plugins/chatgpt/index.js +127 -7
  11. package/dist/src-ts/plugins/desktop-launch.js +19 -0
  12. package/dist/src-ts/plugins/doubao/attachments.js +42 -0
  13. package/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  14. package/dist/src-ts/plugins/doubao/index.js +114 -25
  15. package/dist/src-ts/plugins/doubao/session-history.js +123 -0
  16. package/dist/src-ts/plugins/dsh/index.js +18 -2
  17. package/dist/src-ts/plugins/dsh/session-history.js +371 -0
  18. package/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  19. package/dist/src-ts/plugins/qwenwork/index.js +42 -3
  20. package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  21. package/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  22. package/dist/src-ts/plugins/workbuddy/index.js +23 -3
  23. package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  24. package/dist/src-ts/session-archive.js +509 -0
  25. package/dist/src-ts/session-history.js +2 -0
  26. package/dist/src-ts/session-management.js +665 -0
  27. package/dist/src-ts/session-summary.js +171 -0
  28. package/dist/tests-ts/aun-attachments.test.js +41 -3
  29. package/dist/tests-ts/aun-plugin.test.js +28 -5
  30. package/dist/tests-ts/bundled-package.test.js +3 -1
  31. package/dist/tests-ts/channel-sessions.test.js +255 -2
  32. package/dist/tests-ts/chatgpt-history.test.js +197 -0
  33. package/dist/tests-ts/chatgpt-plugin.test.js +279 -2
  34. package/dist/tests-ts/control-server.test.js +39 -0
  35. package/dist/tests-ts/desktop-launch.test.js +41 -0
  36. package/dist/tests-ts/doubao-history.test.js +172 -0
  37. package/dist/tests-ts/doubao-plugin.test.js +549 -2
  38. package/dist/tests-ts/dsh-agent.test.js +199 -0
  39. package/dist/tests-ts/dsh-history.test.js +165 -0
  40. package/dist/tests-ts/native-session-management.test.js +195 -0
  41. package/dist/tests-ts/plugin-manager.test.js +354 -4
  42. package/dist/tests-ts/qwenwork-history.test.js +339 -0
  43. package/dist/tests-ts/qwenwork-images.test.js +166 -0
  44. package/dist/tests-ts/qwenwork-plugin.test.js +40 -1
  45. package/dist/tests-ts/session-archive.test.js +422 -0
  46. package/dist/tests-ts/session-management.test.js +323 -0
  47. package/dist/tests-ts/session-summary.test.js +199 -0
  48. package/dist/tests-ts/workbuddy-history.test.js +256 -0
  49. package/dist/tests-ts/workbuddy-plugin.test.js +43 -0
  50. package/package.json +1 -1
  51. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +44 -13
  52. package/plugins/aun/dist/src-ts/plugins/aun/index.js +5 -6
  53. package/plugins/aun/package.json +1 -1
  54. package/plugins/aun/plugin.json +1 -1
  55. package/plugins/chatgpt/dist/src-ts/attachments.js +62 -0
  56. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +342 -9
  57. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +127 -7
  58. package/plugins/chatgpt/package.json +1 -1
  59. package/plugins/chatgpt/plugin.json +14 -1
  60. package/plugins/doubao/config.schema.json +3 -0
  61. package/plugins/doubao/dist/src-ts/attachments.js +62 -0
  62. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +19 -0
  63. package/plugins/doubao/dist/src-ts/plugins/doubao/attachments.js +42 -0
  64. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +530 -48
  65. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +114 -25
  66. package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
  67. package/plugins/doubao/package.json +1 -1
  68. package/plugins/doubao/plugin.json +1 -1
  69. package/plugins/dsh/config.schema.json +2 -1
  70. package/plugins/dsh/dist/src-ts/attachments.js +62 -0
  71. package/plugins/dsh/dist/src-ts/dsh-agent.js +141 -46
  72. package/plugins/dsh/dist/src-ts/headless-account.patch.yml +8 -1
  73. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +18 -2
  74. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
  75. package/plugins/dsh/package.json +5 -2
  76. package/plugins/dsh/plugin.json +1 -1
  77. package/plugins/qwenwork/dist/src-ts/attachments.js +62 -0
  78. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +19 -0
  79. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +271 -10
  80. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +42 -3
  81. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
  82. package/plugins/qwenwork/package.json +1 -1
  83. package/plugins/qwenwork/plugin.json +1 -1
  84. package/plugins/workbuddy/config.schema.json +1 -1
  85. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +42 -2
  86. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +23 -3
  87. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  88. package/plugins/workbuddy/package.json +1 -1
  89. package/plugins/workbuddy/plugin.json +1 -1
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DoubaoPlugin = exports.doubaoManifest = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
4
6
  const cdp_client_1 = require("./cdp-client");
7
+ const session_history_1 = require("./session-history");
5
8
  const plugin_1 = require("../../plugin");
6
9
  const desktop_launch_1 = require("../desktop-launch");
7
10
  exports.doubaoManifest = {
8
11
  id: "doubao",
9
- version: "1.0.14",
12
+ version: "1.0.20",
10
13
  displayName: "豆包",
11
14
  entry: "dist/index.js",
12
15
  apiVersion: "1",
@@ -17,14 +20,36 @@ exports.doubaoManifest = {
17
20
  class DoubaoPlugin {
18
21
  injectedClient;
19
22
  manifest = exports.doubaoManifest;
23
+ attachmentInput = true;
24
+ sessionOperations = [];
25
+ sessionHistory;
20
26
  client;
21
27
  state = "stopped";
22
28
  message = "";
29
+ queue = Promise.resolve();
30
+ pending = 0;
31
+ bindings = new Map();
32
+ dataDir;
23
33
  constructor(injectedClient) {
24
34
  this.injectedClient = injectedClient;
25
35
  }
26
36
  disposer;
37
+ reconnect;
27
38
  async activate(context) {
39
+ this.dataDir = context.dataDir;
40
+ this.bindings.clear();
41
+ if (this.dataDir) {
42
+ try {
43
+ const saved = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.dataDir, "sessions.json"), "utf8"));
44
+ if (!saved || typeof saved !== "object" || Array.isArray(saved) || Object.values(saved).some(value => typeof value !== "string" || !/^\d*$/.test(value)))
45
+ throw new Error("豆包会话绑定记录无效");
46
+ this.bindings = new Map(Object.entries(saved));
47
+ }
48
+ catch (error) {
49
+ if (error.code !== "ENOENT")
50
+ throw error;
51
+ }
52
+ }
28
53
  this.client = this.injectedClient ?? new cdp_client_1.DoubaoCdpClient({
29
54
  port: (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227),
30
55
  conversationId: typeof context.config.conversationId === "string" ? context.config.conversationId : undefined,
@@ -32,41 +57,54 @@ class DoubaoPlugin {
32
57
  pluginList: Array.isArray(context.config.pluginList) ? context.config.pluginList.filter((value) => typeof value === "string") : undefined,
33
58
  timeoutMs: typeof context.config.timeoutMs === "number" ? context.config.timeoutMs : undefined,
34
59
  });
60
+ this.sessionHistory = new session_history_1.DoubaoSessionHistoryProvider({
61
+ scope: `doubao:${(0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227)}`,
62
+ client: () => this.client,
63
+ });
64
+ this.sessionOperations = this.client.createSession && this.client.complete ? ["new"] : [];
65
+ this.reconnect = async () => {
66
+ try {
67
+ await this.client.connect();
68
+ }
69
+ catch (first) {
70
+ if (context.config.autoStart !== false && !this.injectedClient && process.platform === "win32") {
71
+ const port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227);
72
+ const executable = await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", typeof context.config.executablePath === "string" ? context.config.executablePath : undefined);
73
+ if (executable) {
74
+ context.logger?.warn?.(`正在启动豆包:${executable},等待 CDP ${port}`);
75
+ try {
76
+ await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${port}`], () => this.client.connect(), typeof context.config.startupTimeoutMs === "number" ? context.config.startupTimeoutMs : 30_000);
77
+ return;
78
+ }
79
+ catch (error) {
80
+ first = new Error(`已启动豆包,但 CDP ${port} 未就绪:${error instanceof Error ? error.message : String(error)}`);
81
+ }
82
+ }
83
+ else
84
+ first = new Error("未找到 Doubao.exe,请配置 executablePath");
85
+ }
86
+ throw first;
87
+ }
88
+ };
35
89
  try {
36
- await this.client.connect();
90
+ await this.reconnect();
37
91
  if (context.http)
38
92
  this.disposer = context.http.register({ method: "POST", path: "/doubao/v1/chat/completions", handler: (request, response) => this.handleHttp(request, response) }) || undefined;
39
93
  this.state = "ready";
40
94
  this.message = "";
41
95
  }
42
- catch (first) {
43
- if (context.config.autoStart !== false && !this.injectedClient && process.platform === "win32") {
44
- const port = (0, plugin_1.resolveRuntimePort)(context.config, context.runtime, "doubao", "DOUBAO_CDP_PORT", 9227);
45
- const executable = await (0, desktop_launch_1.findWindowsExecutable)("Doubao.exe", typeof context.config.executablePath === "string" ? context.config.executablePath : undefined);
46
- if (executable) {
47
- context.logger?.warn?.(`豆包已启动,等待 CDP ${port}`);
48
- try {
49
- await (0, desktop_launch_1.launchAndWait)(executable, [`--remote-debugging-port=${port}`], () => this.client.connect(), typeof context.config.startupTimeoutMs === "number" ? context.config.startupTimeoutMs : 30_000);
50
- this.state = "ready";
51
- this.message = "";
52
- return;
53
- }
54
- catch (error) {
55
- first = new Error(`已启动豆包,但 CDP ${port} 未就绪:${error instanceof Error ? error.message : String(error)}`);
56
- }
57
- }
58
- else
59
- first = new Error("未找到 Doubao.exe,请配置 executablePath");
60
- }
96
+ catch (error) {
61
97
  this.state = "failed";
62
- this.message = first instanceof Error ? first.message : String(first);
98
+ this.message = error instanceof Error ? error.message : String(error);
63
99
  context.logger?.warn?.(this.message);
64
- throw first;
100
+ throw error;
65
101
  }
66
102
  }
67
103
  async deactivate() {
104
+ await this.queue;
68
105
  this.disposer?.();
69
106
  this.disposer = undefined;
107
+ this.reconnect = undefined;
70
108
  await this.client?.close();
71
109
  this.client = undefined;
72
110
  this.state = "stopped";
@@ -76,10 +114,61 @@ class DoubaoPlugin {
76
114
  ? { status: "ready", details: { adapter: "doubao", cdp: true } }
77
115
  : { status: "degraded", message: this.message || "豆包 CDP 未连接", details: { adapter: "doubao", cdp: false } };
78
116
  }
79
- async handle(request) {
117
+ async controlSession(request) {
118
+ if (request.operation !== "new")
119
+ throw new Error(`豆包不支持会话操作:${String(request.operation)}`);
120
+ if (!request.sessionId?.trim())
121
+ throw new Error("豆包新会话缺少 sessionId");
122
+ if (this.pending)
123
+ throw new Error("豆包当前忙,无法创建新会话");
124
+ if (!this.sessionOperations.includes("new"))
125
+ throw new Error("豆包当前传输不支持新建会话");
126
+ return this.enqueue(async () => {
127
+ await this.ensureConnected();
128
+ await this.client.createSession(request.name);
129
+ await this.remember(request.sessionId, "");
130
+ return { text: "豆包已创建新会话", sessionId: request.sessionId };
131
+ });
132
+ }
133
+ async ensureConnected() {
80
134
  if (this.state !== "ready" || !this.client)
81
135
  throw new Error("豆包插件未就绪");
82
- return this.client.complete ? this.client.complete(request.task) : { text: await this.client.direct(request.task) };
136
+ if (!this.client.connected())
137
+ await this.reconnect?.();
138
+ }
139
+ async handle(request) {
140
+ return this.enqueue(async () => {
141
+ await this.ensureConnected();
142
+ const client = this.client;
143
+ if (request.attachments?.length && !client.complete)
144
+ throw new Error("当前豆包客户端不支持附件输入");
145
+ if (!client.complete)
146
+ return { text: await client.direct(request.task) };
147
+ const conversationId = request.sessionId ? this.bindings.get(request.sessionId) ?? "" : undefined;
148
+ const response = await client.complete(request.task, undefined, request.attachments, conversationId);
149
+ if (!request.sessionId)
150
+ return response;
151
+ if (!response.sessionId || !/^\d+$/.test(response.sessionId))
152
+ throw new Error("豆包未返回有效的服务端会话标识");
153
+ await this.remember(request.sessionId, response.sessionId);
154
+ return { ...response, sessionId: request.sessionId };
155
+ });
156
+ }
157
+ enqueue(task) {
158
+ this.pending++;
159
+ const run = this.queue.then(task);
160
+ this.queue = run.then(() => undefined, () => undefined);
161
+ return run.finally(() => { this.pending--; });
162
+ }
163
+ async remember(sessionId, conversationId) {
164
+ const next = new Map(this.bindings).set(sessionId, conversationId);
165
+ if (this.dataDir) {
166
+ await (0, promises_1.mkdir)(this.dataDir, { recursive: true });
167
+ const file = (0, node_path_1.join)(this.dataDir, "sessions.json");
168
+ await (0, promises_1.writeFile)(`${file}.tmp`, `${JSON.stringify(Object.fromEntries(next), null, 2)}\n`, { mode: 0o600 });
169
+ await (0, promises_1.rename)(`${file}.tmp`, file);
170
+ }
171
+ this.bindings = next;
83
172
  }
84
173
  async handleHttp(request, response) {
85
174
  let raw = "";
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DoubaoSessionHistoryProvider = void 0;
4
+ const unsupported = (reason) => ({ supported: false, reason });
5
+ class DoubaoSessionHistoryProvider {
6
+ options;
7
+ mode = "all";
8
+ limitation = "豆包通过原生历史目录和单会话消息链分页只读读取对话正文。";
9
+ listed = new Map();
10
+ listedSessions = new Map();
11
+ readAt = "";
12
+ snapshotAt = 0;
13
+ constructor(options) {
14
+ this.options = options;
15
+ }
16
+ get scope() { return this.options.scope; }
17
+ async listSessions(query) {
18
+ if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 50)
19
+ return unsupported("豆包每页会话数量必须为 1–50");
20
+ if (query.cursor !== undefined && (typeof query.cursor !== "string" || !query.cursor.trim()))
21
+ return unsupported("豆包会话目录游标无效");
22
+ const client = this.options.client();
23
+ if (!client?.connected() || !client.listHistory)
24
+ return unsupported("豆包 CDP 未连接或当前客户端不提供只读历史目录接口");
25
+ try {
26
+ const page = await client.listHistory(query.cursor, query.limit);
27
+ if (!Array.isArray(page.conversations) || typeof page.complete !== "boolean" || !Array.isArray(page.missingReasons))
28
+ return unsupported("豆包会话目录格式无效");
29
+ if (query.cursor === undefined) {
30
+ this.listed.clear();
31
+ this.listedSessions.clear();
32
+ }
33
+ this.readAt = new Date().toISOString();
34
+ this.snapshotAt = Date.now();
35
+ for (const conversation of page.conversations) {
36
+ if (!conversation || typeof conversation.id !== "string" || !conversation.id || !Array.isArray(conversation.messages))
37
+ return unsupported("豆包会话目录包含无效条目");
38
+ this.listed.set(conversation.id, conversation);
39
+ this.listedSessions.set(conversation.id, { id: conversation.id, title: conversation.title, ...(conversation.updatedAt ? { updatedAt: conversation.updatedAt } : {}) });
40
+ }
41
+ const missingReasons = [...new Set(page.missingReasons)];
42
+ return {
43
+ supported: true,
44
+ sessions: page.conversations.map(item => ({ id: item.id, title: item.title, ...(item.updatedAt ? { updatedAt: item.updatedAt } : {}) })),
45
+ ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}),
46
+ complete: page.complete,
47
+ missingReasons,
48
+ readAt: this.readAt,
49
+ };
50
+ }
51
+ catch (error) {
52
+ return unsupported("豆包历史目录读取失败,请检查桌面端接口或版本兼容性");
53
+ }
54
+ }
55
+ async readSession(query) {
56
+ if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 100)
57
+ return unsupported("豆包每页消息数量必须为 1–100");
58
+ const since = query.since;
59
+ const sinceMs = since === undefined ? undefined : Date.parse(since);
60
+ if (since !== undefined && !Number.isFinite(sinceMs))
61
+ return unsupported("豆包会话正文起始时间无效");
62
+ let nativeCursor;
63
+ if (query.cursor !== undefined) {
64
+ const bound = query.cursor.match(/^db-read:(\d+):([1-9]\d*)$/);
65
+ if (bound) {
66
+ if (sinceMs !== Number(bound[1]))
67
+ return unsupported("豆包会话正文游标与起始时间不匹配");
68
+ nativeCursor = bound[2];
69
+ }
70
+ else if (/^[1-9]\d*$/.test(query.cursor) && sinceMs === undefined)
71
+ nativeCursor = query.cursor;
72
+ else
73
+ return unsupported("豆包会话正文游标无效");
74
+ }
75
+ const session = this.listedSessions.get(query.sessionId);
76
+ if (!session)
77
+ return unsupported("请先分页列出豆包会话目录,再读取该会话");
78
+ if (Date.now() - this.snapshotAt > 120_000) {
79
+ this.listed.clear();
80
+ this.listedSessions.clear();
81
+ return unsupported("豆包历史快照已过期,请重新列出会话目录");
82
+ }
83
+ const client = this.options.client();
84
+ if (!client?.connected() || !client.readHistory)
85
+ return unsupported("豆包 CDP 未连接或当前客户端不提供只读会话正文分页接口");
86
+ let page;
87
+ try {
88
+ page = await client.readHistory(query.sessionId, nativeCursor, query.limit, since);
89
+ }
90
+ catch {
91
+ return unsupported("豆包会话正文读取失败,请检查桌面端接口或版本兼容性");
92
+ }
93
+ if (page.conversationId !== query.sessionId || !Array.isArray(page.messages) || typeof page.hasMore !== "boolean" || typeof page.complete !== "boolean" || !Array.isArray(page.missingReasons))
94
+ return unsupported("豆包会话正文格式或会话身份无效");
95
+ if (page.nextCursor !== undefined && (!/^[1-9]\d*$/.test(page.nextCursor) || page.nextCursor === nativeCursor))
96
+ return unsupported("豆包会话正文返回无效或重复游标");
97
+ const pageTimes = page.messages.map(message => message.time ? Date.parse(message.time) : NaN).filter(Number.isFinite);
98
+ const oldest = pageTimes.length ? Math.min(...pageTimes) : undefined;
99
+ const reachedSince = sinceMs !== undefined && (page.reachedSince === true || oldest !== undefined && oldest < sinceMs);
100
+ const visible = sinceMs === undefined ? page.messages : page.messages.filter(message => !message.time || Date.parse(message.time) >= sinceMs);
101
+ const turns = visible.map(message => ({
102
+ ...(message.id ? { id: message.id } : {}),
103
+ role: message.role,
104
+ text: message.text,
105
+ ...(message.time ? { time: message.time } : {}),
106
+ }));
107
+ const missingReasons = [...page.missingReasons];
108
+ if (page.hasMore && !page.nextCursor && !reachedSince)
109
+ missingReasons.push("豆包会话正文仍有后续消息,但分页游标缺失。");
110
+ if (!turns.every(turn => Boolean(turn.id && turn.time)))
111
+ missingReasons.push("部分豆包消息缺少稳定标识或发送时间,无法确认正文完整性。");
112
+ return {
113
+ supported: true,
114
+ session,
115
+ turns,
116
+ ...(page.nextCursor && !reachedSince ? { nextCursor: sinceMs === undefined ? page.nextCursor : `db-read:${sinceMs}:${page.nextCursor}` } : {}),
117
+ complete: page.complete && (!page.hasMore || Boolean(page.nextCursor) || reachedSince) && missingReasons.length === 0,
118
+ missingReasons: [...new Set(missingReasons)],
119
+ readAt: this.readAt,
120
+ };
121
+ }
122
+ }
123
+ exports.DoubaoSessionHistoryProvider = DoubaoSessionHistoryProvider;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-doubao",
3
- "version": "1.0.14",
3
+ "version": "1.0.20",
4
4
  "description": "agent-bridge 的豆包插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "doubao",
3
- "version": "1.0.14",
3
+ "version": "1.0.20",
4
4
  "type": "agent",
5
5
  "displayName": "豆包",
6
6
  "entry": "dist/src-ts/plugins/doubao/index.js",
@@ -2,7 +2,8 @@
2
2
  "type": "object",
3
3
  "properties": {
4
4
  "aid": { "type": "string", "description": "复用的 EvolCore 已有 AID" },
5
- "patchPath": { "type": "string", "description": "DeepSeek Harness patch 文件路径" }
5
+ "patchPath": { "type": "string", "description": "DeepSeek Harness patch 文件路径" },
6
+ "sessionsRoot": { "type": "string", "description": "DSH 官方持久化会话目录;默认使用 DSH_HOME/sessions 或用户目录下的 .dsh/sessions" }
6
7
  },
7
8
  "additionalProperties": false
8
9
  }
@@ -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
+ }
@@ -2,8 +2,64 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DshAgent = void 0;
4
4
  const node_child_process_1 = require("node:child_process");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = require("node:path");
9
+ const node_os_1 = require("node:os");
5
10
  const agent_1 = require("./agent");
11
+ const attachments_1 = require("./attachments");
6
12
  const dsh_paths_1 = require("./dsh-paths");
13
+ const outputMimeTypes = {
14
+ ".csv": "text/csv", ".html": "text/html", ".json": "application/json", ".md": "text/markdown",
15
+ ".pdf": "application/pdf", ".txt": "text/plain", ".doc": "application/msword", ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
16
+ ".mp4": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp",
17
+ };
18
+ function digest(data) { return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex"); }
19
+ function outputContentType(filename, data) {
20
+ return (0, attachments_1.detectImageContentType)(data) ?? outputMimeTypes[(0, node_path_1.extname)(filename).toLowerCase()] ?? "application/octet-stream";
21
+ }
22
+ async function readPresentedFiles(directory, paths, cwd, inputs, returnPaths) {
23
+ if (paths.length > attachments_1.MAX_ATTACHMENTS)
24
+ throw new Error("单条消息最多包含 8 个附件");
25
+ const result = [];
26
+ const root = (0, node_path_1.resolve)(directory);
27
+ if (await (0, promises_1.realpath)(directory) !== root || !(await (0, promises_1.lstat)(directory)).isDirectory())
28
+ throw new Error("DSH 输出目录已被替换");
29
+ let total = 0;
30
+ for (const value of paths) {
31
+ const target = (0, node_path_1.resolve)(cwd, value);
32
+ if ((0, node_path_1.dirname)(target) !== root || (0, node_path_1.dirname)(await (0, promises_1.realpath)(target)) !== root)
33
+ throw new Error("DSH 回传附件路径超出输出目录");
34
+ if (!(await (0, promises_1.lstat)(target)).isFile())
35
+ throw new Error("DSH 回传附件不是普通文件");
36
+ const handle = await (0, promises_1.open)(target, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
37
+ let data;
38
+ try {
39
+ const info = await handle.stat();
40
+ if (!info.isFile() || info.size < 1 || info.size > attachments_1.MAX_ATTACHMENT_BYTES)
41
+ throw new Error("DSH 回传附件大小无效,单个附件不能超过 32 MiB");
42
+ total += info.size;
43
+ if (total > attachments_1.MAX_TOTAL_ATTACHMENT_BYTES)
44
+ throw new Error("单条消息附件总大小不能超过 64 MiB");
45
+ const buffer = Buffer.alloc(info.size + 1);
46
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
47
+ if (bytesRead !== info.size)
48
+ throw new Error("DSH 回传附件大小发生变化");
49
+ data = new Uint8Array(buffer.subarray(0, bytesRead));
50
+ }
51
+ finally {
52
+ await handle.close();
53
+ }
54
+ const source = inputs[returnPaths.indexOf(target)];
55
+ const original = source && digest(source.data) === digest(data) ? source : inputs.find((input) => input.filename === (0, node_path_1.basename)(target) && digest(input.data) === digest(data));
56
+ const filename = (0, attachments_1.attachmentFilename)(original?.filename ?? (0, node_path_1.basename)(target));
57
+ const contentType = original?.contentType ?? outputContentType(filename, data);
58
+ result.push({ kind: contentType.startsWith("image/") ? "image" : "file", data, filename, contentType });
59
+ }
60
+ (0, attachments_1.validateAttachments)(result);
61
+ return result;
62
+ }
7
63
  class DshAgent {
8
64
  name = "dsh";
9
65
  paths;
@@ -17,54 +73,93 @@ class DshAgent {
17
73
  async send(request) {
18
74
  if (!request.task.trim())
19
75
  throw new agent_1.AgentError("DSH task must not be empty");
20
- const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
21
- if (request.sessionId)
22
- args.push("--session-id", request.sessionId);
23
- args.push("-");
24
- const child = this.spawnProcess(this.paths.executable, args, {
25
- cwd: request.cwd,
26
- env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
27
- stdio: ["pipe", "pipe", "pipe"],
28
- });
29
- child.stdin.end(request.task, "utf8");
30
- let output = "";
31
- let errors = "";
32
- let sessionId = request.sessionId;
33
- let finalText = "";
34
- child.stdout.setEncoding?.("utf8");
35
- child.stderr.setEncoding?.("utf8");
36
- child.stdout.on("data", (chunk) => {
37
- output += chunk;
38
- for (const line of output.split(/\r?\n/).slice(0, -1)) {
39
- try {
40
- const event = JSON.parse(line);
41
- if (event.type === "session" && event.sessionId)
42
- sessionId = event.sessionId;
43
- if (event.type === "final" && event.text)
44
- finalText = event.text.trim();
76
+ (0, attachments_1.validateAttachments)(request.attachments);
77
+ const workDirectory = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "agent-bridge-dsh-"));
78
+ let failed = false;
79
+ try {
80
+ const outputDirectory = (0, node_path_1.join)(workDirectory, "output");
81
+ await (0, promises_1.mkdir)(outputDirectory, { recursive: true });
82
+ for (const [index, item] of (request.attachments ?? []).entries())
83
+ await (0, promises_1.writeFile)((0, node_path_1.join)(workDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), item.data, { mode: 0o600 });
84
+ const inputPaths = (request.attachments ?? []).map((item, index) => ({ path: (0, node_path_1.join)(workDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), returnPath: (0, node_path_1.join)(outputDirectory, `${index}-${(0, attachments_1.attachmentFilename)(item.filename)}`), filename: item.filename, contentType: item.contentType }));
85
+ for (const [index, item] of (request.attachments ?? []).entries())
86
+ await (0, promises_1.writeFile)(inputPaths[index].returnPath, item.data, { mode: 0o600 });
87
+ const task = `${request.task}\n\n请使用真实文件工具读取输入附件;生成的回传文件必须写入 outputDirectory 的直接子文件,并调用 present 工具声明文件。原附件已准备 returnPath 副本,原样回传时直接 present 此路径,无需 shell 复制。图片或视频无法理解时请明确说明,仍可原样回传。\n附件传输:\n${JSON.stringify({ inputs: inputPaths, outputDirectory })}`;
88
+ const args = ["--expose-internals", this.paths.cli, "--profile", "headless", "--patch", this.paths.patch, "--json"];
89
+ if (request.sessionId && !request.newSession)
90
+ args.push("--session-id", request.sessionId);
91
+ args.push("-");
92
+ const child = this.spawnProcess(this.paths.executable, args, {
93
+ cwd: request.cwd,
94
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
95
+ stdio: ["pipe", "pipe", "pipe"],
96
+ });
97
+ let output = "";
98
+ let errors = "";
99
+ let sessionId = request.newSession ? undefined : request.sessionId;
100
+ let sessionCwd = request.cwd ?? process.cwd();
101
+ let finalText = "";
102
+ const presented = new Set();
103
+ const presentCalls = new Map();
104
+ child.stdout.setEncoding?.("utf8");
105
+ child.stderr.setEncoding?.("utf8");
106
+ child.stdout.on("data", (chunk) => {
107
+ output += chunk;
108
+ for (const line of output.split(/\r?\n/).slice(0, -1)) {
109
+ try {
110
+ const event = JSON.parse(line);
111
+ if (event.type === "session" && event.sessionId)
112
+ sessionId = event.sessionId;
113
+ if (event.type === "session" && event.cwd)
114
+ sessionCwd = event.cwd;
115
+ if (event.type === "final" && event.text)
116
+ finalText = event.text.trim();
117
+ if (event.type === "tool_call" && event.tool === "present" && event.callId)
118
+ presentCalls.set(event.callId, (event.input?.files ?? []).map((file) => file.path).filter((path) => typeof path === "string"));
119
+ if (event.type === "tool_result" && event.status === "completed" && event.callId)
120
+ for (const path of presentCalls.get(event.callId) ?? [])
121
+ presented.add(path);
122
+ }
123
+ catch { /* 忽略诊断输出和未完成的 JSON 行。 */ }
45
124
  }
46
- catch { /* diagnostics and partial lines are ignored */ }
47
- }
48
- output = output.split(/\r?\n/).at(-1) ?? "";
49
- });
50
- child.stderr.on("data", (chunk) => { errors += chunk; });
51
- await new Promise((resolve, reject) => {
52
- const timer = setTimeout(() => {
53
- child.kill();
54
- reject(new agent_1.AgentError(`DSH task timed out after ${request.timeoutMs ?? this.defaultTimeoutMs}ms`));
55
- }, request.timeoutMs ?? this.defaultTimeoutMs);
56
- child.once("error", (error) => { clearTimeout(timer); reject(new agent_1.AgentError("failed to start DSH", { cause: error })); });
57
- child.once("close", (code) => {
58
- clearTimeout(timer);
59
- if (code !== 0 && !finalText)
60
- reject(new agent_1.AgentError(errors.trim() || `DSH exited with code ${code}`));
61
- else
62
- resolve();
125
+ output = output.split(/\r?\n/).at(-1) ?? "";
126
+ });
127
+ child.stderr.on("data", (chunk) => { errors += chunk; });
128
+ await new Promise((resolve, reject) => {
129
+ const timer = setTimeout(() => {
130
+ child.kill();
131
+ reject(new agent_1.AgentError(`DSH task timed out after ${request.timeoutMs ?? this.defaultTimeoutMs}ms`));
132
+ }, request.timeoutMs ?? this.defaultTimeoutMs);
133
+ child.once("error", (error) => { clearTimeout(timer); reject(new agent_1.AgentError("failed to start DSH", { cause: error })); });
134
+ child.once("close", (code) => {
135
+ clearTimeout(timer);
136
+ if (code !== 0 && !finalText)
137
+ reject(new agent_1.AgentError(errors.trim() || `DSH exited with code ${code}`));
138
+ else
139
+ resolve();
140
+ });
141
+ child.stdin.end(task, "utf8");
63
142
  });
64
- });
65
- if (!finalText)
66
- throw new agent_1.AgentError("DSH returned no final text");
67
- return { text: finalText, sessionId };
143
+ if (request.newSession && !sessionId)
144
+ throw new agent_1.AgentError("DSH 未返回真实新会话标识,无法确认后续续聊目标");
145
+ const attachments = await readPresentedFiles(outputDirectory, [...presented], sessionCwd, request.attachments ?? [], inputPaths.map((item) => item.returnPath));
146
+ if (!finalText && !attachments.length)
147
+ throw new agent_1.AgentError("DSH 未返回正文或附件");
148
+ return { text: finalText, sessionId, ...(attachments?.length ? { attachments } : {}) };
149
+ }
150
+ catch (error) {
151
+ failed = true;
152
+ throw error;
153
+ }
154
+ finally {
155
+ try {
156
+ await (0, promises_1.rm)(workDirectory, { recursive: true, force: true });
157
+ }
158
+ catch (error) {
159
+ if (!failed)
160
+ throw error;
161
+ }
162
+ }
68
163
  }
69
164
  }
70
165
  exports.DshAgent = DshAgent;
@@ -1,5 +1,12 @@
1
- # Select the account-backed model used by the DeepSeek Harness desktop login.
1
+ # 复用 DeepSeek Harness 桌面端登录账号的模型。
2
2
  - id: agent-default-model
3
3
  config:
4
4
  provider: deepseek-account
5
5
  model: deepseek-chat
6
+
7
+ # headless 默认未挂载交付工具,显式启用真实文件交付事件。
8
+ - insert:
9
+ - id: tool-present
10
+ name: '@deepseek-ai/dsh-tool-present'
11
+ config:
12
+ maxFiles: 8