mu-agent-bridge 1.0.35 → 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.
- package/README.md +2 -0
- package/dist/src-ts/channel-sessions.js +90 -24
- package/dist/src-ts/control-server.js +48 -7
- package/dist/src-ts/plugin-manager.js +47 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
- package/dist/src-ts/plugins/chatgpt/index.js +122 -4
- package/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
- package/dist/src-ts/plugins/doubao/index.js +81 -5
- package/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/dist/src-ts/plugins/dsh/index.js +16 -2
- package/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
- package/dist/src-ts/plugins/qwenwork/index.js +12 -1
- package/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
- package/dist/src-ts/plugins/workbuddy/index.js +21 -1
- package/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/dist/src-ts/session-archive.js +509 -0
- package/dist/src-ts/session-history.js +2 -0
- package/dist/src-ts/session-management.js +665 -0
- package/dist/src-ts/session-summary.js +171 -0
- package/dist/tests-ts/channel-sessions.test.js +233 -2
- package/dist/tests-ts/chatgpt-history.test.js +197 -0
- package/dist/tests-ts/control-server.test.js +39 -0
- package/dist/tests-ts/doubao-history.test.js +172 -0
- package/dist/tests-ts/doubao-plugin.test.js +124 -3
- package/dist/tests-ts/dsh-history.test.js +165 -0
- package/dist/tests-ts/native-session-management.test.js +195 -0
- package/dist/tests-ts/plugin-manager.test.js +254 -3
- package/dist/tests-ts/qwenwork-history.test.js +339 -0
- package/dist/tests-ts/session-archive.test.js +422 -0
- package/dist/tests-ts/session-management.test.js +323 -0
- package/dist/tests-ts/session-summary.test.js +199 -0
- package/dist/tests-ts/workbuddy-history.test.js +256 -0
- package/package.json +1 -1
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +72 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +122 -4
- package/plugins/chatgpt/package.json +1 -1
- package/plugins/chatgpt/plugin.json +14 -1
- package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +243 -18
- package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +81 -5
- package/plugins/doubao/dist/src-ts/plugins/doubao/session-history.js +123 -0
- package/plugins/doubao/package.json +1 -1
- package/plugins/doubao/plugin.json +1 -1
- package/plugins/dsh/config.schema.json +2 -1
- package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +16 -2
- package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +371 -0
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/plugin.json +1 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +8 -0
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +12 -1
- package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/session-history.js +245 -0
- package/plugins/qwenwork/package.json +1 -1
- package/plugins/qwenwork/plugin.json +1 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +33 -0
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +21 -1
- package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
- package/plugins/workbuddy/package.json +1 -1
- package/plugins/workbuddy/plugin.json +1 -1
|
@@ -4,9 +4,10 @@ exports.DshPlugin = exports.dshManifest = void 0;
|
|
|
4
4
|
const node_path_1 = require("node:path");
|
|
5
5
|
const dsh_agent_1 = require("../../dsh-agent");
|
|
6
6
|
const openai_adapter_1 = require("../qwenwork/openai-adapter");
|
|
7
|
+
const session_history_1 = require("./session-history");
|
|
7
8
|
exports.dshManifest = {
|
|
8
9
|
id: "dsh",
|
|
9
|
-
version: "1.0.
|
|
10
|
+
version: "1.0.9",
|
|
10
11
|
displayName: "DeepSeek Harness",
|
|
11
12
|
entry: "dist/index.js",
|
|
12
13
|
apiVersion: "1",
|
|
@@ -18,10 +19,12 @@ class DshPlugin {
|
|
|
18
19
|
manifest = exports.dshManifest;
|
|
19
20
|
attachmentInput = true;
|
|
20
21
|
sessionOperations = ["new"];
|
|
22
|
+
sessionHistory;
|
|
21
23
|
agent;
|
|
22
24
|
status = "failed";
|
|
23
25
|
message = "插件尚未启动";
|
|
24
26
|
adapter;
|
|
27
|
+
activeRequests = 0;
|
|
25
28
|
unregister = [];
|
|
26
29
|
async activate(context) {
|
|
27
30
|
const configured = context.config.patchPath;
|
|
@@ -29,6 +32,10 @@ class DshPlugin {
|
|
|
29
32
|
? configured
|
|
30
33
|
: (0, node_path_1.resolve)(__dirname, "..", "..", "headless-account.patch.yml");
|
|
31
34
|
this.agent = new dsh_agent_1.DshAgent({ patchPath });
|
|
35
|
+
this.sessionHistory = new session_history_1.DshSessionHistoryProvider({
|
|
36
|
+
sessionsRoot: typeof context.config.sessionsRoot === "string" ? context.config.sessionsRoot : undefined,
|
|
37
|
+
unavailableReason: () => this.status !== "ready" ? "DSH 插件尚未就绪" : this.activeRequests ? "DSH 正在处理任务,暂不读取持久化历史" : "",
|
|
38
|
+
});
|
|
32
39
|
this.adapter = new openai_adapter_1.OpenAIAdapter((prompt) => this.handle({ task: prompt, timeoutMs: 240_000 }).then((result) => result.text), { model: "dsh", prefix: "/dsh/v1", onExchange: (direction, text, peer, agentAid) => context.logger?.info?.("DSH 通信", { direction, text, peer, agentAid }) });
|
|
33
40
|
if (context.http) {
|
|
34
41
|
for (const [method, path] of [["GET", "/dsh/v1/models"], ["POST", "/dsh/v1/chat/completions"], ["OPTIONS", "/dsh/v1/chat/completions"]]) {
|
|
@@ -47,6 +54,7 @@ class DshPlugin {
|
|
|
47
54
|
await this.adapter?.close();
|
|
48
55
|
this.adapter = undefined;
|
|
49
56
|
this.agent = undefined;
|
|
57
|
+
this.sessionHistory = undefined;
|
|
50
58
|
this.status = "failed";
|
|
51
59
|
this.message = "插件已停止";
|
|
52
60
|
}
|
|
@@ -56,7 +64,13 @@ class DshPlugin {
|
|
|
56
64
|
async handle(request) {
|
|
57
65
|
if (!this.agent || this.status !== "ready")
|
|
58
66
|
throw new Error("dsh 插件未就绪");
|
|
59
|
-
|
|
67
|
+
this.activeRequests++;
|
|
68
|
+
try {
|
|
69
|
+
return await this.agent.send(request);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
this.activeRequests--;
|
|
73
|
+
}
|
|
60
74
|
}
|
|
61
75
|
}
|
|
62
76
|
exports.DshPlugin = DshPlugin;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DshSessionHistoryProvider = void 0;
|
|
4
|
+
exports.decodeDshZstdFrames = decodeDshZstdFrames;
|
|
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_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const node_zlib_1 = require("node:zlib");
|
|
11
|
+
const MAX_COMPRESSED_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const MAX_EXPANDED_BYTES = 128 * 1024 * 1024;
|
|
13
|
+
const MAX_SESSIONS = 10_000;
|
|
14
|
+
const MAX_TURNS = 100_000;
|
|
15
|
+
const KNOWN_EVENTS = new Set([
|
|
16
|
+
"agent-preset/selected", "agent/inbox/spliced", "approval/asked", "approval/decided", "approval/policy", "assistant/attempt", "assistant/message",
|
|
17
|
+
"command/done", "command/run", "compaction/end", "compaction/prune", "compaction/start", "compaction/summary", "deliverables/presented",
|
|
18
|
+
"developer/message", "feedback/message-delete", "feedback/message-put", "feedback/record", "goal/change", "hook/invoked", "hook/result",
|
|
19
|
+
"image/offload", "llm/retry", "llm/retry-started", "model/selection", "permission/preset", "plan/mode", "request/context", "request/header",
|
|
20
|
+
"sandbox/mode", "schedule/change", "session-log-deepseek/delivery-accepted", "session/end-seed", "session/title", "session/title-llm-request",
|
|
21
|
+
"step/end", "step/start", "subagent/catalog", "subagent/descriptor", "subagent/model-selection-policy", "system/message", "team/member",
|
|
22
|
+
"team/message/delivered", "team/message/queued", "team/task", "todo/write", "tool-workflow/agent-end", "tool-workflow/agent-start",
|
|
23
|
+
"tool-workflow/run-end", "tool-workflow/run-start", "tool/call", "tool/ptc-dispatch", "tool/ptc-dispatch-start", "tool/result", "turn/end",
|
|
24
|
+
"turn/start", "user/message", "web/deepseek-search-llm-request", "workspace/changes",
|
|
25
|
+
]);
|
|
26
|
+
const unsupported = (reason) => ({ supported: false, reason });
|
|
27
|
+
const validTime = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && Number.isFinite(new Date(value).getTime());
|
|
28
|
+
/** DSH 将头部与每批事件编码为独立 zstd 帧;Node 的解压器只消费首帧,必须逐帧处理。 */
|
|
29
|
+
function decodeDshZstdFrames(bytes) {
|
|
30
|
+
const decoded = [];
|
|
31
|
+
let offset = 0, total = 0;
|
|
32
|
+
const requireBytes = (position, length) => { if (position + length > bytes.length)
|
|
33
|
+
throw new Error("DSH zstd 帧被截断"); };
|
|
34
|
+
while (offset < bytes.length) {
|
|
35
|
+
if (decoded.length > MAX_TURNS)
|
|
36
|
+
throw new Error("DSH zstd 帧数量超过读取上限");
|
|
37
|
+
const start = offset;
|
|
38
|
+
requireBytes(offset, 5);
|
|
39
|
+
if (bytes.readUInt32LE(offset) !== 0xfd2fb528)
|
|
40
|
+
throw new Error("DSH zstd 帧魔数无效");
|
|
41
|
+
const descriptor = bytes[offset + 4];
|
|
42
|
+
if (descriptor & 0x18)
|
|
43
|
+
throw new Error("DSH zstd 帧含保留标记");
|
|
44
|
+
const single = Boolean(descriptor & 0x20), sizeFlag = descriptor >>> 6, checksum = Boolean(descriptor & 4);
|
|
45
|
+
offset += 5 + (single ? 0 : 1) + [0, 1, 2, 4][descriptor & 3] + (sizeFlag ? [0, 2, 4, 8][sizeFlag] : single ? 1 : 0);
|
|
46
|
+
requireBytes(offset, 0);
|
|
47
|
+
let last = false;
|
|
48
|
+
while (!last) {
|
|
49
|
+
requireBytes(offset, 3);
|
|
50
|
+
const block = bytes.readUIntLE(offset, 3);
|
|
51
|
+
last = Boolean(block & 1);
|
|
52
|
+
const kind = (block >>> 1) & 3, size = block >>> 3;
|
|
53
|
+
if (kind === 3)
|
|
54
|
+
throw new Error("DSH zstd 块类型无效");
|
|
55
|
+
offset += 3 + (kind === 1 ? 1 : size);
|
|
56
|
+
requireBytes(offset, 0);
|
|
57
|
+
}
|
|
58
|
+
offset += checksum ? 4 : 0;
|
|
59
|
+
requireBytes(offset, 0);
|
|
60
|
+
const frame = (0, node_zlib_1.zstdDecompressSync)(bytes.subarray(start, offset), { maxOutputLength: MAX_EXPANDED_BYTES - total });
|
|
61
|
+
total += frame.length;
|
|
62
|
+
if (total > MAX_EXPANDED_BYTES)
|
|
63
|
+
throw new Error("DSH 解压日志超过读取上限");
|
|
64
|
+
decoded.push(frame);
|
|
65
|
+
}
|
|
66
|
+
return Buffer.concat(decoded, total);
|
|
67
|
+
}
|
|
68
|
+
class DshSessionHistoryProvider {
|
|
69
|
+
options;
|
|
70
|
+
scope;
|
|
71
|
+
limitation = "读取的是 DSH 官方 v4 持久化会话日志;实时尚未落盘的事件、未知格式与损坏日志会标为不完整。";
|
|
72
|
+
mode = "all";
|
|
73
|
+
root;
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
this.options = options;
|
|
76
|
+
this.root = (0, node_path_1.resolve)(options.sessionsRoot ?? (0, node_path_1.join)(process.env.DSH_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), ".dsh"), "sessions"));
|
|
77
|
+
this.scope = `dsh:persisted:${(0, node_crypto_1.createHash)("sha256").update(this.root).digest("hex").slice(0, 24)}`;
|
|
78
|
+
}
|
|
79
|
+
async listSessions(query) {
|
|
80
|
+
if (this.options.unavailableReason?.())
|
|
81
|
+
return unsupported(this.options.unavailableReason());
|
|
82
|
+
const invalid = this.validate(query, 100);
|
|
83
|
+
if (invalid)
|
|
84
|
+
return unsupported(invalid);
|
|
85
|
+
const root = await this.realRoot();
|
|
86
|
+
if (typeof root !== "string")
|
|
87
|
+
return root;
|
|
88
|
+
const discovered = await this.discover(root);
|
|
89
|
+
const fingerprint = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(discovered.entries.map(item => [item.id, item.generation, item.mtime]))).digest("hex");
|
|
90
|
+
const offset = this.cursor(query.cursor, fingerprint, discovered.entries.length, "dsh-list");
|
|
91
|
+
if (offset instanceof Error)
|
|
92
|
+
throw offset;
|
|
93
|
+
const sessions = discovered.entries.slice(offset, offset + Math.min(query.limit, 100)).map(item => ({ id: item.id, title: item.id, updatedAt: new Date(item.mtime).toISOString() }));
|
|
94
|
+
return {
|
|
95
|
+
supported: true,
|
|
96
|
+
sessions,
|
|
97
|
+
...(offset + sessions.length < discovered.entries.length ? { nextCursor: `dsh-list:${fingerprint}:${offset + sessions.length}` } : {}),
|
|
98
|
+
complete: discovered.reasons.length === 0,
|
|
99
|
+
missingReasons: discovered.reasons,
|
|
100
|
+
readAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async readSession(query) {
|
|
104
|
+
if (this.options.unavailableReason?.())
|
|
105
|
+
return unsupported(this.options.unavailableReason());
|
|
106
|
+
const invalid = this.validate(query, 1000);
|
|
107
|
+
if (invalid)
|
|
108
|
+
return unsupported(invalid);
|
|
109
|
+
const since = this.parseSince(query.since);
|
|
110
|
+
if (since instanceof Error)
|
|
111
|
+
return unsupported(since.message);
|
|
112
|
+
const root = await this.realRoot();
|
|
113
|
+
if (typeof root !== "string")
|
|
114
|
+
return root;
|
|
115
|
+
const discovered = await this.discover(root);
|
|
116
|
+
const entry = discovered.entries.find(item => item.id === query.sessionId);
|
|
117
|
+
if (!entry)
|
|
118
|
+
return unsupported("DSH 会话不在当前可验证的持久化日志列表中");
|
|
119
|
+
const parsed = await this.readEntry(root, entry);
|
|
120
|
+
// 先完整解析并校验日志,再按时间筛选,避免过滤掩盖损坏或未知事件。
|
|
121
|
+
const filteredTurns = since === undefined ? parsed.turns : parsed.turns.filter(turn => turn.time !== undefined && Date.parse(turn.time) >= Date.parse(since));
|
|
122
|
+
const fingerprint = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(["dsh-read", entry.id, entry.generation, entry.mtime, since ?? null, filteredTurns])).digest("hex");
|
|
123
|
+
const offset = this.cursor(query.cursor, fingerprint, filteredTurns.length, "dsh-read");
|
|
124
|
+
if (offset instanceof Error)
|
|
125
|
+
throw offset;
|
|
126
|
+
const turns = filteredTurns.slice(offset, offset + Math.min(query.limit, 1000));
|
|
127
|
+
return {
|
|
128
|
+
supported: true,
|
|
129
|
+
session: parsed.session,
|
|
130
|
+
turns,
|
|
131
|
+
...(offset + turns.length < filteredTurns.length ? { nextCursor: `dsh-read:${fingerprint}:${offset + turns.length}` } : {}),
|
|
132
|
+
complete: parsed.complete,
|
|
133
|
+
missingReasons: parsed.reasons,
|
|
134
|
+
readAt: new Date().toISOString(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async realRoot() {
|
|
138
|
+
try {
|
|
139
|
+
const resolved = await (0, promises_1.realpath)(this.root);
|
|
140
|
+
const info = await (0, promises_1.lstat)(this.root);
|
|
141
|
+
if (!info.isDirectory() || info.isSymbolicLink() || resolved !== this.root)
|
|
142
|
+
return unsupported("DSH 会话目录不是受信任的真实目录");
|
|
143
|
+
return resolved;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return unsupported("DSH 持久化会话目录不存在或不可读");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async discover(root) {
|
|
150
|
+
const entries = [];
|
|
151
|
+
const reasons = [];
|
|
152
|
+
let projects;
|
|
153
|
+
try {
|
|
154
|
+
projects = await (0, promises_1.readdir)(root, { withFileTypes: true });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { entries, reasons: ["无法完整枚举 DSH 项目会话目录"] };
|
|
158
|
+
}
|
|
159
|
+
for (const project of projects) {
|
|
160
|
+
if (!project.isDirectory()) {
|
|
161
|
+
if (project.isSymbolicLink())
|
|
162
|
+
reasons.push("会话目录包含符号链接,已略过");
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const projectPath = (0, node_path_1.join)(root, project.name);
|
|
166
|
+
if (!await this.inside(root, projectPath)) {
|
|
167
|
+
reasons.push("项目会话目录路径越界,已略过");
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
let directories;
|
|
171
|
+
try {
|
|
172
|
+
directories = await (0, promises_1.readdir)(projectPath, { withFileTypes: true });
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
reasons.push("部分项目会话目录不可读");
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const directory of directories) {
|
|
179
|
+
if (!directory.isDirectory()) {
|
|
180
|
+
if (directory.isSymbolicLink())
|
|
181
|
+
reasons.push("会话目录包含符号链接,已略过");
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const dir = (0, node_path_1.join)(projectPath, directory.name);
|
|
185
|
+
if (!await this.inside(root, dir)) {
|
|
186
|
+
reasons.push("会话文件目录路径越界,已略过");
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
let files;
|
|
190
|
+
try {
|
|
191
|
+
files = await (0, promises_1.readdir)(dir, { withFileTypes: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
reasons.push("部分会话文件目录不可读");
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const candidates = files.flatMap(file => {
|
|
198
|
+
const match = file.name.match(/^session(?:\.v(\d+))?\.jsonl(\.zstd)?$/);
|
|
199
|
+
return match ? [{ file, generation: match[1] ? Number(match[1]) : 0, compressed: Boolean(match[2]) }] : [];
|
|
200
|
+
}).sort((a, b) => b.generation - a.generation);
|
|
201
|
+
const canonical = candidates[0];
|
|
202
|
+
if (!canonical) {
|
|
203
|
+
if (files.some(file => file.name.startsWith("session.")))
|
|
204
|
+
reasons.push("会话目录使用无法识别的日志格式");
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (canonical.generation !== 4) {
|
|
208
|
+
reasons.push(`会话 ${directory.name} 使用不支持的 v${canonical.generation} 持久化代际`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (candidates.length > 1)
|
|
212
|
+
reasons.push(`会话 ${directory.name} 存在多个日志代际,无法确认规范版本`);
|
|
213
|
+
if (canonical.file.isSymbolicLink() || !canonical.file.isFile()) {
|
|
214
|
+
reasons.push(`会话 ${directory.name} 的日志不是普通文件`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const path = (0, node_path_1.join)(dir, canonical.file.name);
|
|
218
|
+
if (!await this.inside(root, path)) {
|
|
219
|
+
reasons.push(`会话 ${directory.name} 的日志路径越界`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const stat = await (0, promises_1.lstat)(path);
|
|
224
|
+
if (stat.size > MAX_COMPRESSED_BYTES) {
|
|
225
|
+
reasons.push(`会话 ${directory.name} 超过压缩日志大小上限`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
entries.push({ path, id: directory.name, mtime: stat.mtimeMs, generation: canonical.generation, compressed: canonical.compressed });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
reasons.push(`会话 ${directory.name} 日志无法读取`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
entries.sort((a, b) => b.mtime - a.mtime || a.id.localeCompare(b.id));
|
|
236
|
+
if (entries.length > MAX_SESSIONS) {
|
|
237
|
+
entries.length = MAX_SESSIONS;
|
|
238
|
+
reasons.push(`会话数超过 ${MAX_SESSIONS} 条上限`);
|
|
239
|
+
}
|
|
240
|
+
return { entries, reasons: [...new Set(reasons)] };
|
|
241
|
+
}
|
|
242
|
+
async inside(root, path) {
|
|
243
|
+
try {
|
|
244
|
+
const real = await (0, promises_1.realpath)(path);
|
|
245
|
+
const rel = (0, node_path_1.relative)(root, real);
|
|
246
|
+
return rel !== ".." && !rel.startsWith(`..${node_path_1.sep}`) && !(0, node_path_1.isAbsolute)(rel);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async readEntry(root, entry) {
|
|
253
|
+
const reasons = [];
|
|
254
|
+
const fail = (reason) => ({ session: { id: entry.id, title: entry.id }, turns: [], complete: false, reasons: [...new Set([...reasons, reason])] });
|
|
255
|
+
try {
|
|
256
|
+
if (!await this.inside(root, entry.path))
|
|
257
|
+
return fail("DSH 会话日志路径越界或为符号链接");
|
|
258
|
+
const before = await (0, promises_1.lstat)(entry.path);
|
|
259
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_COMPRESSED_BYTES)
|
|
260
|
+
return fail("DSH 会话日志不是普通文件或超过读取上限");
|
|
261
|
+
const handle = await (0, promises_1.open)(entry.path, node_fs_1.constants.O_RDONLY | (node_fs_1.constants.O_NOFOLLOW ?? 0));
|
|
262
|
+
let bytes;
|
|
263
|
+
try {
|
|
264
|
+
const opened = await handle.stat();
|
|
265
|
+
if (!opened.isFile() || opened.size > MAX_COMPRESSED_BYTES || opened.ino !== before.ino)
|
|
266
|
+
return fail("DSH 会话日志在打开期间发生变化");
|
|
267
|
+
bytes = await handle.readFile();
|
|
268
|
+
const after = await handle.stat();
|
|
269
|
+
if (opened.ino !== after.ino || opened.size !== after.size || opened.mtimeMs !== after.mtimeMs || bytes.byteLength !== after.size)
|
|
270
|
+
return fail("DSH 会话日志在读取期间发生变化");
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
await handle.close();
|
|
274
|
+
}
|
|
275
|
+
const data = entry.compressed ? decodeDshZstdFrames(bytes) : bytes;
|
|
276
|
+
if (data.byteLength > MAX_EXPANDED_BYTES)
|
|
277
|
+
return fail("DSH 解压日志超过读取上限");
|
|
278
|
+
const text = data.toString("utf8");
|
|
279
|
+
if (!text.endsWith("\n"))
|
|
280
|
+
return fail("DSH 会话日志最后一条记录不完整");
|
|
281
|
+
const rows = text.slice(0, -1).split("\n");
|
|
282
|
+
if (!rows.length || rows.length > MAX_TURNS + 1)
|
|
283
|
+
return fail("DSH 会话记录数量超过读取上限");
|
|
284
|
+
const header = JSON.parse(rows[0]);
|
|
285
|
+
if (header.type !== "session" || header.version !== 4 || header.id !== entry.id || !validTime(header.createdAt) || typeof header.isSeeded !== "boolean" || !Number.isSafeInteger(header.delegationDepth) || header.delegationDepth < 0)
|
|
286
|
+
return fail("DSH 会话头格式或身份无法验证");
|
|
287
|
+
const turns = [];
|
|
288
|
+
let complete = true;
|
|
289
|
+
for (let index = 1; index < rows.length; index++) {
|
|
290
|
+
let event;
|
|
291
|
+
try {
|
|
292
|
+
event = JSON.parse(rows[index]);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return fail("DSH 会话日志包含损坏记录");
|
|
296
|
+
}
|
|
297
|
+
if (!event || event.seq !== index - 1 || typeof event.type !== "string" || !validTime(event.time) || typeof event.data !== "object" || event.data === null)
|
|
298
|
+
return fail("DSH 会话事件序号、时间或结构无法验证");
|
|
299
|
+
if (!KNOWN_EVENTS.has(event.type) && event.ignorable !== true) {
|
|
300
|
+
complete = false;
|
|
301
|
+
reasons.push(`存在未知且不可安全忽略的 DSH 事件:${event.type}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (event.type !== "user/message" && event.type !== "assistant/message")
|
|
305
|
+
continue;
|
|
306
|
+
const declaredRole = event.type === "user/message" ? event.data.role : event.data.message?.role;
|
|
307
|
+
if (declaredRole !== undefined && declaredRole !== (event.type === "user/message" ? "user" : "assistant")) {
|
|
308
|
+
complete = false;
|
|
309
|
+
reasons.push("DSH 消息事件与角色声明冲突,已排除");
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const content = event.type === "user/message" ? event.data.content : event.data.message?.content;
|
|
313
|
+
if (!Array.isArray(content)) {
|
|
314
|
+
complete = false;
|
|
315
|
+
reasons.push("部分 DSH 消息缺少可验证正文");
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const parts = [];
|
|
319
|
+
for (const block of content) {
|
|
320
|
+
if (!block || typeof block !== "object" || typeof block.type !== "string") {
|
|
321
|
+
complete = false;
|
|
322
|
+
reasons.push("部分 DSH 消息包含无法解释的正文块");
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (block.type === "reasoning" || block.type === "tool-call")
|
|
326
|
+
continue;
|
|
327
|
+
if (block.type === "text" && typeof block.text === "string")
|
|
328
|
+
parts.push(block.text);
|
|
329
|
+
else {
|
|
330
|
+
complete = false;
|
|
331
|
+
reasons.push(`DSH ${block.type} 正文块未纳入文本历史读取`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const message = parts.join("\n").trim();
|
|
335
|
+
if (message)
|
|
336
|
+
turns.push({ id: `${entry.id}:${event.seq}`, role: event.type === "user/message" ? "user" : "assistant", text: message, time: new Date(event.time).toISOString() });
|
|
337
|
+
}
|
|
338
|
+
return { session: { id: entry.id, title: entry.id, updatedAt: new Date(before.mtimeMs).toISOString() }, turns, complete, reasons: [...new Set(reasons)] };
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return fail(error instanceof Error && /maximum output length|larger than/.test(error.message) ? "DSH 解压日志超过读取上限" : "DSH 会话日志无法按官方 v4 格式完整解析");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
cursor(cursor, fingerprint, total, prefix) {
|
|
345
|
+
if (cursor === undefined)
|
|
346
|
+
return 0;
|
|
347
|
+
const match = cursor.match(/^(dsh-list|dsh-read):([a-f0-9]{64}):(\d+)$/);
|
|
348
|
+
if (!match || match[1] !== prefix || match[2] !== fingerprint)
|
|
349
|
+
return new Error("DSH 历史游标无效或内容已变化");
|
|
350
|
+
const offset = Number(match[3]);
|
|
351
|
+
return Number.isSafeInteger(offset) && offset >= 0 && offset < total ? offset : new Error("DSH 历史游标位置无效");
|
|
352
|
+
}
|
|
353
|
+
validate(query, max) {
|
|
354
|
+
if (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > max)
|
|
355
|
+
return `DSH 每页数量必须为 1–${max}`;
|
|
356
|
+
if (query.cursor !== undefined && (typeof query.cursor !== "string" || !query.cursor.trim()))
|
|
357
|
+
return "DSH 历史游标无效";
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
parseSince(value) {
|
|
361
|
+
if (value === undefined)
|
|
362
|
+
return undefined;
|
|
363
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.test(value))
|
|
364
|
+
return new Error("DSH since 必须是 ISO 8601 时间");
|
|
365
|
+
const timestamp = Date.parse(value);
|
|
366
|
+
if (!Number.isFinite(timestamp))
|
|
367
|
+
return new Error("DSH since 必须是有效的 ISO 8601 时间");
|
|
368
|
+
return new Date(timestamp).toISOString();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
exports.DshSessionHistoryProvider = DshSessionHistoryProvider;
|
|
@@ -512,6 +512,14 @@ class QwenCdpClient {
|
|
|
512
512
|
}
|
|
513
513
|
throw new Error(`tRPC ${path} 超时`);
|
|
514
514
|
}
|
|
515
|
+
/** 仅开放已核实的会话历史只读查询,不允许从历史 provider 调用 mutation。 */
|
|
516
|
+
async queryHistory(path, input) {
|
|
517
|
+
const result = await this.trpc(path, "query", input);
|
|
518
|
+
const envelope = result;
|
|
519
|
+
if (envelope?.result?.type === "data")
|
|
520
|
+
return envelope.result.data?.json;
|
|
521
|
+
throw new Error(envelope?.result?.error?.json?.message ?? `千问历史查询 ${path} 失败`);
|
|
522
|
+
}
|
|
515
523
|
async waitFor(expression, message) {
|
|
516
524
|
const deadline = Date.now() + this.options.timeoutMs;
|
|
517
525
|
while (Date.now() < deadline) {
|
|
@@ -8,10 +8,11 @@ const openai_adapter_1 = require("./openai-adapter");
|
|
|
8
8
|
const cdp_client_1 = require("./cdp-client");
|
|
9
9
|
const plugin_1 = require("../../plugin");
|
|
10
10
|
const desktop_launch_1 = require("../desktop-launch");
|
|
11
|
+
const session_history_1 = require("./session-history");
|
|
11
12
|
const CHAT_HEARTBEAT_INTERVAL_MS = 2 * 60 * 60_000;
|
|
12
13
|
exports.qwenworkManifest = {
|
|
13
14
|
id: "qwenwork",
|
|
14
|
-
version: "1.0.
|
|
15
|
+
version: "1.0.20",
|
|
15
16
|
displayName: "千问办公",
|
|
16
17
|
entry: "dist/index.js",
|
|
17
18
|
apiVersion: "1",
|
|
@@ -26,6 +27,7 @@ function asNumber(value, fallback) {
|
|
|
26
27
|
/** 千问插件由后台守护,所有通道共用请求队列和恢复流程。 */
|
|
27
28
|
class QwenworkPlugin {
|
|
28
29
|
manifest = exports.qwenworkManifest;
|
|
30
|
+
sessionHistory;
|
|
29
31
|
get attachmentInput() { return Boolean(this.relayTarget || this.client?.attachmentInput); }
|
|
30
32
|
sessionOperations = [];
|
|
31
33
|
injectedClient;
|
|
@@ -81,6 +83,15 @@ class QwenworkPlugin {
|
|
|
81
83
|
this.callPlugin = context.callPlugin;
|
|
82
84
|
this.pluginStatus = context.pluginStatus;
|
|
83
85
|
this.activeCdpPort = (0, plugin_1.resolveRuntimePort)(config, context.runtime, "qwenwork", "QWEN_CDP_PORT", this.configuredCdpPort ?? exports.qwenworkManifest.defaults.cdpPort);
|
|
86
|
+
this.sessionHistory = new session_history_1.QwenworkSessionHistoryProvider({
|
|
87
|
+
scope: `qwenwork:cdp:${this.activeCdpPort}`,
|
|
88
|
+
unavailableReason: () => this.relayTarget ? "千问办公中继模式不支持原生历史读取" : this.busy ? "千问办公当前忙,暂不读取历史" : this.recovery ? "千问办公正在恢复,暂不读取历史" : this.client && !this.client.connected() ? "千问办公当前离线,暂不读取历史" : this.state !== "ready" ? "千问办公当前未就绪,暂不读取历史" : "",
|
|
89
|
+
query: async (path, input) => {
|
|
90
|
+
if (!this.client?.queryHistory)
|
|
91
|
+
throw new Error("当前千问客户端没有只读会话查询能力");
|
|
92
|
+
return this.client.queryHistory(path, input);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
84
95
|
if (!this.relayTarget) {
|
|
85
96
|
const client = this.injectedClient ?? new cdp_client_1.QwenCdpClient({
|
|
86
97
|
port: this.activeCdpPort,
|