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
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionManagement = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const session_archive_1 = require("./session-archive");
|
|
8
|
+
const session_summary_1 = require("./session-summary");
|
|
9
|
+
class ManagementError extends Error {
|
|
10
|
+
}
|
|
11
|
+
const usage = "用法:/sessions today [qwenwork|doubao|dsh] [--summary];/sessions list [插件ID] [--page 页码 --snapshot 编号];/sessions show <编号>;/sessions native-list <插件ID>;/sessions native-show <编号>;/sessions summary --from <编号,...> [主题]";
|
|
12
|
+
class SessionManagement {
|
|
13
|
+
directory;
|
|
14
|
+
audit;
|
|
15
|
+
bindings;
|
|
16
|
+
nativeProviders;
|
|
17
|
+
archive;
|
|
18
|
+
settings = { enabled: false, grants: [] };
|
|
19
|
+
loaded;
|
|
20
|
+
writes = Promise.resolve();
|
|
21
|
+
snapshots = new Map();
|
|
22
|
+
nativeSnapshots = new Map();
|
|
23
|
+
revision = 0;
|
|
24
|
+
archiveEpoch = 0;
|
|
25
|
+
sourceEpochs = new Map();
|
|
26
|
+
constructor(directory, audit = () => { }, bindings, nativeProviders = () => []) {
|
|
27
|
+
this.directory = directory;
|
|
28
|
+
this.audit = audit;
|
|
29
|
+
this.bindings = bindings;
|
|
30
|
+
this.nativeProviders = nativeProviders;
|
|
31
|
+
this.archive = new session_archive_1.SessionArchive((0, node_path_1.join)(directory, "session-archive"), { enabled: false });
|
|
32
|
+
}
|
|
33
|
+
async load() {
|
|
34
|
+
this.loaded ??= (async () => {
|
|
35
|
+
try {
|
|
36
|
+
this.settings = this.validate(JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.directory, "session-management.json"), "utf8")));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT")
|
|
40
|
+
this.audit("配置读取", { actor: "local", count: 0, status: "invalid" });
|
|
41
|
+
}
|
|
42
|
+
this.archive.setEnabled(this.settings.enabled);
|
|
43
|
+
})();
|
|
44
|
+
await this.loaded;
|
|
45
|
+
}
|
|
46
|
+
async config() {
|
|
47
|
+
await this.load();
|
|
48
|
+
const { summarizer, ...rest } = structuredClone(this.settings);
|
|
49
|
+
return { ...rest, ...(summarizer ? { summarizer: { endpoint: summarizer.endpoint, model: summarizer.model, hasApiKey: Boolean(summarizer.apiKey) } } : {}) };
|
|
50
|
+
}
|
|
51
|
+
async saveConfig(value, actor = "local") {
|
|
52
|
+
const write = this.writes.catch(() => { }).then(async () => {
|
|
53
|
+
await this.load();
|
|
54
|
+
const next = this.validate(value, this.settings.summarizer?.apiKey, this.settings.summarizer?.endpoint);
|
|
55
|
+
await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
|
|
56
|
+
const target = (0, node_path_1.join)(this.directory, "session-management.json");
|
|
57
|
+
const temporary = `${target}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
await (0, promises_1.writeFile)(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
60
|
+
await (0, promises_1.rename)(temporary, target);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
await (0, promises_1.rm)(temporary, { force: true });
|
|
64
|
+
}
|
|
65
|
+
this.settings = next;
|
|
66
|
+
this.archive.setEnabled(next.enabled);
|
|
67
|
+
this.invalidate();
|
|
68
|
+
this.audit("配置保存", { actor, count: next.grants.length, status: "ok" });
|
|
69
|
+
});
|
|
70
|
+
this.writes = write;
|
|
71
|
+
await write;
|
|
72
|
+
}
|
|
73
|
+
async ready() { await this.load(); }
|
|
74
|
+
archiveWriteToken(source) { return { epoch: this.archiveEpoch, sourceEpoch: this.sourceEpochs.get(source) ?? 0 }; }
|
|
75
|
+
recordIngressTurns(source, token, turns) {
|
|
76
|
+
if (token.epoch !== this.archiveEpoch || token.sourceEpoch !== (this.sourceEpochs.get(source) ?? 0))
|
|
77
|
+
return Promise.resolve(false);
|
|
78
|
+
return this.archive.recordTurns(turns).then(() => true);
|
|
79
|
+
}
|
|
80
|
+
async deleteSession(sessionId, actor = "local") {
|
|
81
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 256)
|
|
82
|
+
throw new ManagementError("会话标识无效");
|
|
83
|
+
// 删除时让已接收的请求失效,避免完成后重建归档。
|
|
84
|
+
this.archiveEpoch++;
|
|
85
|
+
this.invalidate();
|
|
86
|
+
await this.archive.deleteSession(sessionId);
|
|
87
|
+
this.audit("会话删除", { actor, count: 1, status: "ok" });
|
|
88
|
+
}
|
|
89
|
+
async deleteSource(source, actor = "local") {
|
|
90
|
+
validateSource(source);
|
|
91
|
+
this.sourceEpochs.set(source, (this.sourceEpochs.get(source) ?? 0) + 1);
|
|
92
|
+
this.invalidate();
|
|
93
|
+
const { sessions } = await this.catalog([source]);
|
|
94
|
+
for (const session of sessions)
|
|
95
|
+
await this.archive.deleteSession(session.sessionId);
|
|
96
|
+
this.audit("来源删除", { actor, count: sessions.length, status: "ok" });
|
|
97
|
+
}
|
|
98
|
+
async clear(actor = "local") {
|
|
99
|
+
this.archiveEpoch++;
|
|
100
|
+
this.invalidate();
|
|
101
|
+
await this.archive.clear();
|
|
102
|
+
this.audit("归档清空", { actor, count: 0, status: "ok" });
|
|
103
|
+
}
|
|
104
|
+
invalidate() { this.revision++; this.snapshots.clear(); this.nativeSnapshots.clear(); }
|
|
105
|
+
checkRevision(revision) {
|
|
106
|
+
if (revision !== this.revision)
|
|
107
|
+
throw new ManagementError("权限或归档设置已变更,请重新执行 /sessions list");
|
|
108
|
+
}
|
|
109
|
+
sources(context) {
|
|
110
|
+
const grant = this.settings.grants.find(item => item.managerSource === context.source && item.managerSessionId === context.sessionId);
|
|
111
|
+
return [...new Set([context.source, ...(grant?.sources ?? [])])];
|
|
112
|
+
}
|
|
113
|
+
async catalog(sources, agentPlugin) {
|
|
114
|
+
const result = await this.archive.snapshotSessions();
|
|
115
|
+
return { sessions: result.sessions.filter(item => sources.includes(item.source) && (!agentPlugin || item.agentPlugin === agentPlugin)), complete: result.complete };
|
|
116
|
+
}
|
|
117
|
+
async command(text, context) {
|
|
118
|
+
if (!/^\/sessions(?:\s|$)/.test(text.trimStart()))
|
|
119
|
+
return undefined;
|
|
120
|
+
await this.load();
|
|
121
|
+
const revision = this.revision;
|
|
122
|
+
const cutoff = Date.now();
|
|
123
|
+
let auditRefs = [];
|
|
124
|
+
let summarizer;
|
|
125
|
+
try {
|
|
126
|
+
validateSource(context.source);
|
|
127
|
+
if (!context.sessionId)
|
|
128
|
+
throw new ManagementError("当前逻辑会话无效");
|
|
129
|
+
const input = text.trim();
|
|
130
|
+
const today = input.match(/^\/sessions today(?: (qwenwork|doubao|dsh))?( --summary)?$/);
|
|
131
|
+
if (today)
|
|
132
|
+
return await this.summarizeToday(context, today[1], Boolean(today[2]));
|
|
133
|
+
const nativeList = input.match(/^\/sessions native-list ([a-z][a-z0-9-]*)(?: --page ([1-9]\d*))?$/);
|
|
134
|
+
if (nativeList)
|
|
135
|
+
return await this.listNative(context, nativeList[1], nativeList[2] ? Number(nativeList[2]) : undefined);
|
|
136
|
+
const nativeShow = input.match(/^\/sessions native-show ([a-f0-9]{12}\.\d+)$/);
|
|
137
|
+
if (nativeShow) {
|
|
138
|
+
const selected = this.resolveNative(nativeShow[1], context);
|
|
139
|
+
auditRefs = [selected.id];
|
|
140
|
+
const result = await this.readNative(nativeShow[1], context);
|
|
141
|
+
this.checkRevision(revision);
|
|
142
|
+
this.resolveNative(nativeShow[1], context);
|
|
143
|
+
const status = result.complete ? "完整" : `不完整${result.hasNext ? ",仅显示前100条" : ""};${result.incompleteReasons.join("、") || "来源未完整读取"}`;
|
|
144
|
+
const output = [`${nativeShow[1]} ${selected.pluginId} ${selected.id}(${status})`, ...result.turns.map(turn => `${turn.time || "未知时间"} ${turn.role}: ${turn.text}`)].join("\n");
|
|
145
|
+
if (output.length > 60_000)
|
|
146
|
+
throw new ManagementError("读取内容超过60000字符,请缩小会话范围");
|
|
147
|
+
this.audit("原生会话读取", { actor: context.source, count: 1, status: result.complete ? "complete" : "incomplete", refs: auditRefs });
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
const list = input.match(/^\/sessions list(?: ([a-z][a-z0-9-]*))?(?: --page ([1-9]\d*))?(?: --snapshot ([a-f0-9]{12}))?$/);
|
|
151
|
+
if (list) {
|
|
152
|
+
const result = await this.list(context, list[1], list[2] ? Number(list[2]) : undefined, list[3]);
|
|
153
|
+
this.checkRevision(revision);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
const show = input.match(/^\/sessions show ([a-f0-9]{12}\.\d+)$/);
|
|
157
|
+
if (show) {
|
|
158
|
+
const selected = this.resolve(show[1], context);
|
|
159
|
+
auditRefs = [selected.sessionId];
|
|
160
|
+
const result = await this.archive.readSession(selected.sessionId, { limit: 100 });
|
|
161
|
+
if (!result.session)
|
|
162
|
+
throw new ManagementError("该会话仅有绑定元数据,暂无可读取的归档正文");
|
|
163
|
+
this.checkRevision(revision);
|
|
164
|
+
const status = (result.complete && !result.hasNext ? "完整" : `不完整${result.hasNext ? ",仅显示前100条" : ""}`) + (this.bindings?.isBusy(selected.source) ? ";正在运行,仅包含已完成记录" : "");
|
|
165
|
+
const output = [`${show[1]} ${selected.sessionId} ${selected.agentPlugin} ${selected.channel}(${status})`, ...result.turns.filter(turn => Date.parse(turn.time) <= cutoff).map(turn => `${turn.time} ${turn.role}: ${turn.text}`)].join("\n");
|
|
166
|
+
if (output.length > 60_000)
|
|
167
|
+
throw new ManagementError("读取内容超过60000字符,请缩小会话范围");
|
|
168
|
+
this.audit("会话读取", { actor: context.source, count: 1, status, refs: [selected.sessionId] });
|
|
169
|
+
return output;
|
|
170
|
+
}
|
|
171
|
+
const summary = input.match(/^\/sessions summary --from ([a-f0-9.]+(?:,[a-f0-9.]+)*)(?: ([^\r\n]+))?$/);
|
|
172
|
+
if (!summary)
|
|
173
|
+
return usage;
|
|
174
|
+
const refs = summary[1].split(",");
|
|
175
|
+
if (refs.length > 10 || new Set(refs).size !== refs.length)
|
|
176
|
+
throw new ManagementError("每次总结须选择1至10个不同会话");
|
|
177
|
+
const selected = refs.map(ref => this.isNativeRef(ref) ? { ref, native: true, sessionId: this.resolveNative(ref, context).id } : { ...this.resolve(ref, context), ref, native: false });
|
|
178
|
+
auditRefs = selected.map(item => item.sessionId);
|
|
179
|
+
const sessions = [];
|
|
180
|
+
let characters = 0;
|
|
181
|
+
for (const item of selected) {
|
|
182
|
+
if (item.native) {
|
|
183
|
+
const result = await this.readNative(item.ref, context);
|
|
184
|
+
if (result.hasNext)
|
|
185
|
+
throw new ManagementError("选中原生会话超过100条记录,请缩小总结范围");
|
|
186
|
+
characters += result.turns.reduce((size, turn) => size + turn.text.length, 0);
|
|
187
|
+
if (characters > 60_000)
|
|
188
|
+
throw new ManagementError("总结输入超过60000字符,请缩小范围");
|
|
189
|
+
sessions.push(result);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const result = await this.archive.readSession(item.sessionId, { limit: 100 });
|
|
193
|
+
if (!result.session)
|
|
194
|
+
throw new ManagementError("该会话仅有绑定元数据,暂无可读取的归档正文");
|
|
195
|
+
if (result.hasNext)
|
|
196
|
+
throw new ManagementError("选中会话超过100条记录,请缩小总结范围");
|
|
197
|
+
const turns = result.turns.filter(turn => Date.parse(turn.time) <= cutoff);
|
|
198
|
+
characters += turns.reduce((size, turn) => size + turn.text.length, 0);
|
|
199
|
+
if (characters > 60_000)
|
|
200
|
+
throw new ManagementError("总结输入超过60000字符,请缩小范围");
|
|
201
|
+
const busy = this.bindings?.isBusy(item.source);
|
|
202
|
+
sessions.push({ ...result.session, ref: item.ref, complete: result.complete && !busy, incompleteReasons: [...result.missingReasons, ...(busy ? ["源会话正在运行,仅包含已完成轮次"] : [])], turns });
|
|
203
|
+
}
|
|
204
|
+
this.checkRevision(revision);
|
|
205
|
+
for (const ref of refs)
|
|
206
|
+
this.isNativeRef(ref) ? this.resolveNative(ref, context) : this.resolve(ref, context);
|
|
207
|
+
summarizer = this.settings.summarizer?.model;
|
|
208
|
+
const result = await new session_summary_1.SessionSummarizer(this.settings.summarizer).summarizeSessions({ sessions, ...(summary[2] ? { topic: summary[2] } : {}) }).catch(error => {
|
|
209
|
+
if (error instanceof session_summary_1.SessionSummaryError && error.code === "limit")
|
|
210
|
+
throw new ManagementError(error.message);
|
|
211
|
+
throw new ManagementError("只读总结失败,请检查本机配置与模型服务");
|
|
212
|
+
});
|
|
213
|
+
this.checkRevision(revision);
|
|
214
|
+
for (const ref of refs)
|
|
215
|
+
this.isNativeRef(ref) ? this.resolveNative(ref, context) : this.resolve(ref, context);
|
|
216
|
+
this.audit("会话总结", { actor: context.source, count: sessions.length, status: result.supported ? "ok" : "unsupported", refs: selected.map(item => item.sessionId), summarizer: this.settings.summarizer?.model });
|
|
217
|
+
return `${this.settings.summarizer ? `总结模型:${this.settings.summarizer.model};已选${sessions.length}个会话\n` : ""}${result.text}`;
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
this.audit("管理命令", { actor: context.source, count: auditRefs.length, status: "failed", ...(auditRefs.length ? { refs: auditRefs } : {}), ...(summarizer ? { summarizer } : {}) });
|
|
221
|
+
return error instanceof ManagementError ? error.message : "会话管理读取失败,请检查本机归档状态";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
providers() {
|
|
225
|
+
return this.nativeProviders().map(({ pluginId, provider, status }) => ({ pluginId, scope: provider.scope, mode: provider.mode, limitation: provider.limitation, status }));
|
|
226
|
+
}
|
|
227
|
+
async summarizeToday(context, onlyPlugin, summarize = false) {
|
|
228
|
+
const revision = this.revision;
|
|
229
|
+
const cutoff = Date.now();
|
|
230
|
+
const configured = this.settings.summarizer?.model;
|
|
231
|
+
if (summarize && !this.settings.summarizer)
|
|
232
|
+
throw new ManagementError("未配置本机只读总结模型,请先使用 /sessions today 获取今日全部对话材料");
|
|
233
|
+
const plugins = onlyPlugin ? [onlyPlugin] : ["qwenwork", "doubao", "dsh"];
|
|
234
|
+
const expected = plugins.map(pluginId => {
|
|
235
|
+
const current = this.currentNative(context, pluginId);
|
|
236
|
+
return { ...current, scope: current.provider.scope };
|
|
237
|
+
});
|
|
238
|
+
const check = () => { this.checkRevision(revision); for (const record of expected)
|
|
239
|
+
this.currentNative(context, record.pluginId, record); };
|
|
240
|
+
const start = new Date(new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date()) + "T00:00:00+08:00").getTime();
|
|
241
|
+
const end = start + 24 * 60 * 60 * 1000;
|
|
242
|
+
const date = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(start));
|
|
243
|
+
const sources = [];
|
|
244
|
+
const failures = [];
|
|
245
|
+
let totalCharacters = 0;
|
|
246
|
+
for (const pluginId of plugins) {
|
|
247
|
+
const current = expected.find(record => record.pluginId === pluginId);
|
|
248
|
+
if (current.provider.mode !== "all") {
|
|
249
|
+
failures.push(`${pluginId}:不支持完整会话目录`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const sessions = [];
|
|
253
|
+
let cursor;
|
|
254
|
+
const cursors = new Set();
|
|
255
|
+
let catalogComplete = true;
|
|
256
|
+
for (let page = 0; page < 20; page++) {
|
|
257
|
+
const raw = await nativeTimeout(() => current.provider.listSessions({ limit: 50, ...(cursor ? { cursor } : {}) }));
|
|
258
|
+
check();
|
|
259
|
+
if (raw.supported === false) {
|
|
260
|
+
failures.push(`${pluginId}:${displayMetadata(raw.reason)}`);
|
|
261
|
+
catalogComplete = false;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
const result = validateNativePage(raw, false);
|
|
265
|
+
catalogComplete &&= result.complete === true;
|
|
266
|
+
result.missingReasons.forEach(reason => failures.push(`${pluginId}目录:${reason}`));
|
|
267
|
+
for (const session of result.sessions) {
|
|
268
|
+
if (sessions.some(known => known.id === session.id))
|
|
269
|
+
throw new ManagementError(`${pluginId}目录会话 ID 重复`);
|
|
270
|
+
sessions.push(session);
|
|
271
|
+
}
|
|
272
|
+
if (!result.nextCursor)
|
|
273
|
+
break;
|
|
274
|
+
if (cursors.has(result.nextCursor) || page === 19 || sessions.length >= 1000)
|
|
275
|
+
throw new ManagementError(`${pluginId}会话目录超过读取上限或游标异常`);
|
|
276
|
+
cursors.add(result.nextCursor);
|
|
277
|
+
cursor = result.nextCursor;
|
|
278
|
+
}
|
|
279
|
+
if (!catalogComplete)
|
|
280
|
+
failures.push(`${pluginId}:会话目录不完整`);
|
|
281
|
+
const todays = sessions;
|
|
282
|
+
const turns = [];
|
|
283
|
+
for (const session of todays) {
|
|
284
|
+
const cursors = new Set();
|
|
285
|
+
const turnIds = new Set();
|
|
286
|
+
let cursor;
|
|
287
|
+
let bodyComplete = true;
|
|
288
|
+
for (let page = 0; page < 20; page++) {
|
|
289
|
+
const raw = await nativeTimeout(() => current.provider.readSession({ sessionId: session.id, limit: 100, since: new Date(start).toISOString(), ...(cursor ? { cursor } : {}) }));
|
|
290
|
+
check();
|
|
291
|
+
if (raw.supported === false) {
|
|
292
|
+
failures.push(`${pluginId}/${session.id}:${displayMetadata(raw.reason)}`);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
const result = validateNativePage(raw, true);
|
|
296
|
+
bodyComplete &&= result.complete === true;
|
|
297
|
+
if (result.session && result.session.id !== session.id)
|
|
298
|
+
throw new ManagementError(`${pluginId}返回了不匹配的会话正文`);
|
|
299
|
+
result.missingReasons.forEach(reason => failures.push(`${pluginId}/${session.id}:${reason}`));
|
|
300
|
+
for (const turn of result.turns) {
|
|
301
|
+
if (turn.id && turnIds.has(turn.id))
|
|
302
|
+
throw new ManagementError(`${pluginId}/${session.id}正文轮次 ID 重复`);
|
|
303
|
+
if (turn.id)
|
|
304
|
+
turnIds.add(turn.id);
|
|
305
|
+
const time = turn.time ? Date.parse(turn.time) : NaN;
|
|
306
|
+
if (!Number.isFinite(time) || time < start || time >= end || time > cutoff) {
|
|
307
|
+
if (!Number.isFinite(time))
|
|
308
|
+
failures.push(`${pluginId}/${session.id}:部分消息时间未知`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const text = `[会话 ${session.id} ${session.title}] ${turn.text}`;
|
|
312
|
+
totalCharacters += text.length;
|
|
313
|
+
if (totalCharacters > 60_000)
|
|
314
|
+
throw new ManagementError("今日对话超过60000字符安全上限,未生成不完整小结");
|
|
315
|
+
turns.push({ role: turn.role, time: turn.time, text });
|
|
316
|
+
}
|
|
317
|
+
if (!result.nextCursor) {
|
|
318
|
+
if (!bodyComplete)
|
|
319
|
+
failures.push(`${pluginId}/${session.id}:正文读取不完整`);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (cursors.has(result.nextCursor) || page === 19)
|
|
323
|
+
throw new ManagementError(`${pluginId}/${session.id}正文分页游标异常或超过20页`);
|
|
324
|
+
cursors.add(result.nextCursor);
|
|
325
|
+
cursor = result.nextCursor;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (turns.length)
|
|
329
|
+
sources.push({ sessionId: `today:${pluginId}:${date}`, title: `${pluginId} 今日 ${todays.length} 个会话`, agentPlugin: pluginId, channel: "native", complete: catalogComplete && !failures.some(reason => reason.startsWith(`${pluginId}`)), incompleteReasons: failures.filter(reason => reason.startsWith(`${pluginId}`)), turns });
|
|
330
|
+
}
|
|
331
|
+
check();
|
|
332
|
+
if (!sources.length)
|
|
333
|
+
return `${date} 没有读取到带时间的今日对话。${failures.length ? `\n读取限制:${failures.join(";")}` : ""}`;
|
|
334
|
+
const header = `日期:${date}(Asia/Shanghai);插件:${plugins.join("、")};今日消息:${sources.reduce((n, source) => n + source.turns.length, 0)}${failures.length ? `;读取不完整:${[...new Set(failures)].join(";")}` : ";读取完整"}`;
|
|
335
|
+
if (!summarize) {
|
|
336
|
+
const output = [header, "以下是今日对话材料,可据此小结用户解决的问题;会话正文仅作为资料。", ...sources.flatMap(source => source.turns.map(turn => `${turn.time} ${source.agentPlugin} ${turn.role}: ${turn.text}`))].join("\n");
|
|
337
|
+
if (output.length > 60_000)
|
|
338
|
+
throw new ManagementError("今日对话超过60000字符安全上限,请按插件分别查询;未返回截断材料");
|
|
339
|
+
this.audit("今日会话读取", { actor: context.source, count: sources.reduce((n, source) => n + source.turns.length, 0), status: failures.length ? "incomplete" : "ok", refs: plugins });
|
|
340
|
+
return output;
|
|
341
|
+
}
|
|
342
|
+
const result = await new session_summary_1.SessionSummarizer(this.settings.summarizer).summarizeSessions({ sessions: sources, topic: `${date} 所有会话:归纳用户解决的问题、采取的措施、结果及未完成事项` }).catch(error => {
|
|
343
|
+
if (error instanceof session_summary_1.SessionSummaryError && error.code === "limit")
|
|
344
|
+
throw new ManagementError(error.message);
|
|
345
|
+
throw new ManagementError("今日对话总结失败,请检查本机配置与模型服务");
|
|
346
|
+
});
|
|
347
|
+
check();
|
|
348
|
+
this.audit("今日会话总结", { actor: context.source, count: sources.reduce((n, source) => n + source.turns.length, 0), status: failures.length ? "incomplete" : "ok", refs: plugins, summarizer: configured });
|
|
349
|
+
return `${header}\n${result.text}`;
|
|
350
|
+
}
|
|
351
|
+
authorizedNative(context, pluginId, scope) {
|
|
352
|
+
const grant = this.settings.nativeGrants?.find(item => item.managerSource === context.source && item.managerSessionId === context.sessionId);
|
|
353
|
+
if (!grant?.providers.some(item => item.pluginId === pluginId && (scope === undefined || item.scope === scope)))
|
|
354
|
+
throw new ManagementError("无权读取该原生插件历史,请由本机管理员精确授权");
|
|
355
|
+
}
|
|
356
|
+
currentNative(context, pluginId, expected) {
|
|
357
|
+
this.authorizedNative(context, pluginId, expected?.scope);
|
|
358
|
+
const current = this.nativeProviders().find(item => item.pluginId === pluginId);
|
|
359
|
+
if (!current || !["ready", "degraded"].includes(current.status))
|
|
360
|
+
throw new ManagementError("原生历史插件离线或尚未就绪,请稍后重试");
|
|
361
|
+
this.authorizedNative(context, pluginId, current.provider.scope);
|
|
362
|
+
if (expected && (expected.provider !== current.provider || expected.identity !== current.identity || expected.scope !== current.provider.scope))
|
|
363
|
+
throw new ManagementError("插件实例或 scope 已变更,编号失效,请重新列举");
|
|
364
|
+
return current;
|
|
365
|
+
}
|
|
366
|
+
isNativeRef(ref) { return this.nativeSnapshots.has(ref.split(".")[0]); }
|
|
367
|
+
resolveNative(ref, context) {
|
|
368
|
+
const match = ref.match(/^([a-f0-9]{12})\.([1-9]\d*)$/);
|
|
369
|
+
const snapshot = match && this.nativeSnapshots.get(match[1]);
|
|
370
|
+
if (!snapshot || snapshot.revision !== this.revision || snapshot.expires < Date.now() || snapshot.context.source !== context.source || snapshot.context.sessionId !== context.sessionId)
|
|
371
|
+
throw new ManagementError("原生编号无效或已过期,请重新执行 /sessions native-list <插件ID>");
|
|
372
|
+
const selected = snapshot.sessions[Number(match[2]) - 1];
|
|
373
|
+
if (!selected)
|
|
374
|
+
throw new ManagementError("原生编号无效,请重新列举");
|
|
375
|
+
this.currentNative(context, selected.pluginId, selected);
|
|
376
|
+
return selected;
|
|
377
|
+
}
|
|
378
|
+
async listNative(context, pluginId, page) {
|
|
379
|
+
const revision = this.revision;
|
|
380
|
+
const current = this.currentNative(context, pluginId);
|
|
381
|
+
const expected = { ...current, scope: current.provider.scope };
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
for (const [key, snapshot] of this.nativeSnapshots)
|
|
384
|
+
if (snapshot.expires < now)
|
|
385
|
+
this.nativeSnapshots.delete(key);
|
|
386
|
+
let entry;
|
|
387
|
+
if (page !== undefined)
|
|
388
|
+
entry = [...this.nativeSnapshots].find(([, item]) => item.context.source === context.source && item.context.sessionId === context.sessionId && item.pluginId === pluginId && item.revision === revision);
|
|
389
|
+
else {
|
|
390
|
+
const sessions = [];
|
|
391
|
+
const ids = new Set();
|
|
392
|
+
const cursors = new Set();
|
|
393
|
+
let cursor;
|
|
394
|
+
let complete = true;
|
|
395
|
+
const reasons = new Set();
|
|
396
|
+
for (let index = 0; index < 20; index++) {
|
|
397
|
+
this.checkRevision(revision);
|
|
398
|
+
this.currentNative(context, pluginId, expected);
|
|
399
|
+
const raw = await nativeTimeout(() => current.provider.listSessions({ limit: 50, ...(cursor ? { cursor } : {}) }));
|
|
400
|
+
this.checkRevision(revision);
|
|
401
|
+
this.currentNative(context, pluginId, expected);
|
|
402
|
+
const result = validateNativePage(raw, false);
|
|
403
|
+
complete &&= result.complete;
|
|
404
|
+
result.missingReasons.forEach(reason => reasons.add(reason));
|
|
405
|
+
for (const session of result.sessions) {
|
|
406
|
+
if (ids.has(session.id))
|
|
407
|
+
throw new ManagementError("原生列表会话 ID 重复,请稍后重新列举");
|
|
408
|
+
ids.add(session.id);
|
|
409
|
+
sessions.push({ ...session, ...expected, complete: false, reasons: [] });
|
|
410
|
+
}
|
|
411
|
+
if (!result.nextCursor)
|
|
412
|
+
break;
|
|
413
|
+
if (cursors.has(result.nextCursor))
|
|
414
|
+
throw new ManagementError("原生列表游标未前进,请稍后重新列举");
|
|
415
|
+
if (index === 19 || sessions.length >= 1000)
|
|
416
|
+
throw new ManagementError("原生列表超过20页或1000项,请缩小来源范围");
|
|
417
|
+
cursors.add(result.nextCursor);
|
|
418
|
+
cursor = result.nextCursor;
|
|
419
|
+
}
|
|
420
|
+
for (const item of sessions) {
|
|
421
|
+
item.complete = complete;
|
|
422
|
+
item.reasons = [...reasons];
|
|
423
|
+
}
|
|
424
|
+
for (const [key, snapshot] of this.nativeSnapshots)
|
|
425
|
+
if (snapshot.context.source === context.source && snapshot.context.sessionId === context.sessionId && snapshot.pluginId === pluginId)
|
|
426
|
+
this.nativeSnapshots.delete(key);
|
|
427
|
+
const key = (0, node_crypto_1.randomUUID)().replace(/-/g, "").slice(0, 12);
|
|
428
|
+
entry = [key, { context: { ...context }, expires: Date.now() + 5 * 60_000, revision, pluginId, sessions }];
|
|
429
|
+
if (this.nativeSnapshots.size >= 1000)
|
|
430
|
+
this.nativeSnapshots.delete(this.nativeSnapshots.keys().next().value);
|
|
431
|
+
this.nativeSnapshots.set(...entry);
|
|
432
|
+
}
|
|
433
|
+
if (!entry)
|
|
434
|
+
throw new ManagementError("原生编号已过期,请重新列举");
|
|
435
|
+
const [key, snapshot] = entry;
|
|
436
|
+
const currentPage = page ?? 1;
|
|
437
|
+
if (!Number.isSafeInteger(currentPage) || (currentPage - 1) * 50 >= Math.max(snapshot.sessions.length, 1))
|
|
438
|
+
throw new ManagementError("清单页码无效");
|
|
439
|
+
for (const item of snapshot.sessions)
|
|
440
|
+
this.currentNative(context, pluginId, item);
|
|
441
|
+
this.checkRevision(revision);
|
|
442
|
+
const lines = snapshot.sessions.slice((currentPage - 1) * 50, currentPage * 50).map((item, index) => `${key}.${(currentPage - 1) * 50 + index + 1} ${item.pluginId} ${displayMetadata(item.id)} ${displayMetadata(item.title)} ${item.updatedAt ?? "未知时间"}${item.complete ? "" : ` 不完整:${item.reasons.map(displayMetadata).join("、") || "目录未完整读取"}`}`);
|
|
443
|
+
const output = [...lines, `原生目录第${currentPage}页,共${snapshot.sessions.length}项;编号5分钟有效;${displayMetadata(current.provider.limitation)}${snapshot.sessions.length > currentPage * 50 ? `;下一页:/sessions native-list ${pluginId} --page ${currentPage + 1}` : ""}`].join("\n");
|
|
444
|
+
if (output.length > 60_000)
|
|
445
|
+
throw new ManagementError("原生目录超过60000字符,请缩小来源范围");
|
|
446
|
+
this.audit("原生会话列举", { actor: context.source, count: lines.length, status: "ok" });
|
|
447
|
+
return output;
|
|
448
|
+
}
|
|
449
|
+
async readNative(ref, context) {
|
|
450
|
+
const revision = this.revision;
|
|
451
|
+
const selected = this.resolveNative(ref, context);
|
|
452
|
+
const raw = await nativeTimeout(() => selected.provider.readSession({ sessionId: selected.id, limit: 100 }));
|
|
453
|
+
this.checkRevision(revision);
|
|
454
|
+
this.resolveNative(ref, context);
|
|
455
|
+
const result = validateNativePage(raw, true);
|
|
456
|
+
const session = result.session;
|
|
457
|
+
if (session.id !== selected.id)
|
|
458
|
+
throw new ManagementError("原生会话身份不匹配,请重新列举");
|
|
459
|
+
const turns = result.turns;
|
|
460
|
+
const ids = new Set();
|
|
461
|
+
let characters = 0;
|
|
462
|
+
for (const turn of turns) {
|
|
463
|
+
if (turn.id && ids.has(turn.id))
|
|
464
|
+
throw new ManagementError("原生正文轮次 ID 重复");
|
|
465
|
+
if (turn.id)
|
|
466
|
+
ids.add(turn.id);
|
|
467
|
+
characters += turn.text.length;
|
|
468
|
+
if (characters > 60_000)
|
|
469
|
+
throw new ManagementError("原生正文超过60000字符,请缩小会话范围");
|
|
470
|
+
}
|
|
471
|
+
const reasons = new Set([...selected.reasons, ...result.missingReasons]);
|
|
472
|
+
const hasNext = result.nextCursor !== undefined;
|
|
473
|
+
if (hasNext)
|
|
474
|
+
reasons.add("仅读取前100条,后续正文未读取");
|
|
475
|
+
if (turns.some(turn => !turn.time))
|
|
476
|
+
reasons.add("部分历史时间未知");
|
|
477
|
+
return { sessionId: selected.id, ref, title: session.title, agentPlugin: selected.pluginId, channel: "native", ...(session.updatedAt ? { updatedAt: session.updatedAt } : {}), complete: result.complete === true && !hasNext && reasons.size === 0, hasNext, incompleteReasons: [...reasons], turns: turns.map(turn => ({ role: turn.role, text: turn.text, time: turn.time ?? "" })) };
|
|
478
|
+
}
|
|
479
|
+
async list(context, plugin, page, snapshotId) {
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
const revision = this.revision;
|
|
482
|
+
for (const [key, value] of this.snapshots)
|
|
483
|
+
if (value.expires < now)
|
|
484
|
+
this.snapshots.delete(key);
|
|
485
|
+
let entry;
|
|
486
|
+
if (page !== undefined) {
|
|
487
|
+
if (!snapshotId)
|
|
488
|
+
throw new ManagementError("续页必须携带原快照编号");
|
|
489
|
+
entry = this.snapshots.has(snapshotId) ? [snapshotId, this.snapshots.get(snapshotId)] : undefined;
|
|
490
|
+
if (!entry || entry[1].context.source !== context.source || entry[1].context.sessionId !== context.sessionId || entry[1].revision !== this.revision)
|
|
491
|
+
throw new ManagementError("清单快照无效或已过期,请重新列举");
|
|
492
|
+
if (entry[1].plugin !== plugin)
|
|
493
|
+
throw new ManagementError("清单快照与插件过滤条件不匹配");
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const sources = this.sources(context);
|
|
497
|
+
const catalog = await this.catalog(sources, plugin);
|
|
498
|
+
const archived = catalog.sessions;
|
|
499
|
+
const known = new Set(archived.map(item => item.sessionId));
|
|
500
|
+
const bound = (await this.bindings?.list() ?? []).filter(item => sources.includes(item.source) && (!plugin || item.agentPlugin === plugin) && !known.has(item.logicalSessionId)).map(item => ({ sessionId: item.logicalSessionId, source: item.source, agentPlugin: item.agentPlugin, channel: JSON.parse(item.source)[0], updatedAt: item.updatedAt }));
|
|
501
|
+
const sessions = [...archived, ...bound].filter(item => item.sessionId !== context.sessionId).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.sessionId.localeCompare(b.sessionId));
|
|
502
|
+
this.checkRevision(revision);
|
|
503
|
+
const key = (0, node_crypto_1.randomUUID)().replace(/-/g, "").slice(0, 12);
|
|
504
|
+
entry = [key, { context: { ...context }, expires: now + 5 * 60_000, complete: catalog.complete, revision, plugin, sessions }];
|
|
505
|
+
if (this.snapshots.size >= 1000)
|
|
506
|
+
this.snapshots.delete(this.snapshots.keys().next().value);
|
|
507
|
+
this.snapshots.set(...entry);
|
|
508
|
+
}
|
|
509
|
+
if (!entry)
|
|
510
|
+
throw new ManagementError("编号已过期,请重新执行 /sessions list");
|
|
511
|
+
const [key, snapshot] = entry;
|
|
512
|
+
const currentPage = page ?? 1;
|
|
513
|
+
if (!Number.isSafeInteger(currentPage) || (currentPage - 1) * 50 >= Math.max(snapshot.sessions.length, 1))
|
|
514
|
+
throw new ManagementError("清单页码无效");
|
|
515
|
+
const lines = snapshot.sessions.slice((currentPage - 1) * 50, currentPage * 50).map((item, index) => `${key}.${(currentPage - 1) * 50 + index + 1} ${item.sessionId} ${item.agentPlugin} ${item.channel} ${item.updatedAt}${item.title ? ` ${item.title}` : ""}${this.bindings?.isBusy(item.source) ? " 正在运行,仅包含已完成记录" : ""}`);
|
|
516
|
+
this.audit("会话列举", { actor: context.source, count: lines.length, status: "ok" });
|
|
517
|
+
return [...lines, `第${currentPage}页,共${snapshot.sessions.length}个会话${snapshot.complete ? "" : ";目录不完整,仅列保留记录"};编号5分钟有效;只读总结目标:${this.settings.summarizer?.model ?? "未配置"};快照: ${key}${snapshot.sessions.length > currentPage * 50 ? `;下一页:/sessions list${snapshot.plugin ? ` ${snapshot.plugin}` : ""} --page ${currentPage + 1} --snapshot ${key}` : ""}`].join("\n");
|
|
518
|
+
}
|
|
519
|
+
resolve(ref, context) {
|
|
520
|
+
const match = ref.match(/^([a-f0-9]{12})\.([1-9]\d*)$/);
|
|
521
|
+
const snapshot = match && this.snapshots.get(match[1]);
|
|
522
|
+
if (!snapshot || snapshot.revision !== this.revision || snapshot.expires < Date.now() || snapshot.context.source !== context.source || snapshot.context.sessionId !== context.sessionId)
|
|
523
|
+
throw new ManagementError("编号无效或已过期,请重新执行 /sessions list");
|
|
524
|
+
const session = snapshot.sessions[Number(match[2]) - 1];
|
|
525
|
+
if (!session || !this.sources(context).includes(session.source) || session.sessionId === context.sessionId)
|
|
526
|
+
throw new ManagementError("无权读取所选会话,请重新列举");
|
|
527
|
+
return session;
|
|
528
|
+
}
|
|
529
|
+
validate(value, previousKey, previousEndpoint) {
|
|
530
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
531
|
+
throw new ManagementError("归档设置必须是对象");
|
|
532
|
+
const input = value;
|
|
533
|
+
if (Object.keys(input).some(key => !["enabled", "grants", "nativeGrants", "summarizer"].includes(key)) || typeof input.enabled !== "boolean" || !Array.isArray(input.grants) || input.grants.length > 500)
|
|
534
|
+
throw new ManagementError("归档设置字段无效");
|
|
535
|
+
const managers = new Set();
|
|
536
|
+
const grants = input.grants.map((raw) => {
|
|
537
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
538
|
+
throw new ManagementError("来源授权无效");
|
|
539
|
+
const grant = raw;
|
|
540
|
+
if (Object.keys(grant).some(key => !["managerSource", "managerSessionId", "sources"].includes(key)))
|
|
541
|
+
throw new ManagementError("来源授权字段无效");
|
|
542
|
+
validateSource(grant.managerSource);
|
|
543
|
+
if (typeof grant.managerSessionId !== "string" || !grant.managerSessionId || grant.managerSessionId.length > 256 || !Array.isArray(grant.sources) || grant.sources.length > 1000)
|
|
544
|
+
throw new ManagementError("来源授权无效");
|
|
545
|
+
grant.sources.forEach(validateSource);
|
|
546
|
+
const key = JSON.stringify([grant.managerSource, grant.managerSessionId]);
|
|
547
|
+
if (managers.has(key))
|
|
548
|
+
throw new ManagementError("管理会话授权重复");
|
|
549
|
+
managers.add(key);
|
|
550
|
+
return { managerSource: grant.managerSource, managerSessionId: grant.managerSessionId, sources: [...new Set(grant.sources)] };
|
|
551
|
+
});
|
|
552
|
+
let nativeGrants;
|
|
553
|
+
if (input.nativeGrants !== undefined) {
|
|
554
|
+
if (!Array.isArray(input.nativeGrants) || input.nativeGrants.length > 500)
|
|
555
|
+
throw new ManagementError("原生授权无效");
|
|
556
|
+
const nativeManagers = new Set();
|
|
557
|
+
nativeGrants = input.nativeGrants.map(raw => {
|
|
558
|
+
const grant = objectFields(raw, ["managerSource", "managerSessionId", "providers"]);
|
|
559
|
+
validateSource(grant.managerSource);
|
|
560
|
+
if (!exactIdentifier(grant.managerSessionId, 256) || !Array.isArray(grant.providers) || grant.providers.length > 1000)
|
|
561
|
+
throw new ManagementError("原生授权无效");
|
|
562
|
+
const key = JSON.stringify([grant.managerSource, grant.managerSessionId]);
|
|
563
|
+
if (nativeManagers.has(key))
|
|
564
|
+
throw new ManagementError("原生管理会话授权重复");
|
|
565
|
+
nativeManagers.add(key);
|
|
566
|
+
const providers = new Set();
|
|
567
|
+
return { managerSource: grant.managerSource, managerSessionId: grant.managerSessionId, providers: grant.providers.map(rawProvider => {
|
|
568
|
+
const item = objectFields(rawProvider, ["pluginId", "scope"]);
|
|
569
|
+
if (typeof item.pluginId !== "string" || !/^[a-z][a-z0-9-]*$/.test(item.pluginId) || item.pluginId.length > 128 || !exactIdentifier(item.scope, 4096))
|
|
570
|
+
throw new ManagementError("原生插件授权或 scope 无效");
|
|
571
|
+
const providerKey = JSON.stringify([item.pluginId, item.scope]);
|
|
572
|
+
if (providers.has(providerKey))
|
|
573
|
+
throw new ManagementError("原生插件授权重复");
|
|
574
|
+
providers.add(providerKey);
|
|
575
|
+
return { pluginId: item.pluginId, scope: item.scope };
|
|
576
|
+
}) };
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
let summarizer;
|
|
580
|
+
if (input.summarizer !== undefined && input.summarizer !== null) {
|
|
581
|
+
const config = input.summarizer;
|
|
582
|
+
if (!config || typeof config !== "object" || Array.isArray(config) || Object.keys(config).some(key => !["endpoint", "model", "apiKey", "hasApiKey"].includes(key)))
|
|
583
|
+
throw new ManagementError("总结器配置无效");
|
|
584
|
+
let endpoint;
|
|
585
|
+
try {
|
|
586
|
+
endpoint = new URL(config.endpoint);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
throw new ManagementError("总结器地址无效");
|
|
590
|
+
}
|
|
591
|
+
if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.hash || typeof config.model !== "string" || !config.model.trim() || config.model.length > 256 || /[\r\n]/.test(config.model) || (config.apiKey !== undefined && (typeof config.apiKey !== "string" || config.apiKey.length > 8192 || /[\r\n]/.test(config.apiKey))))
|
|
592
|
+
throw new ManagementError("总结器配置无效");
|
|
593
|
+
const apiKey = config.apiKey === undefined && config.endpoint === previousEndpoint ? previousKey : config.apiKey;
|
|
594
|
+
summarizer = { endpoint: config.endpoint, model: config.model, ...(apiKey ? { apiKey } : {}) };
|
|
595
|
+
}
|
|
596
|
+
return { enabled: input.enabled, grants, ...(nativeGrants ? { nativeGrants } : {}), ...(summarizer ? { summarizer } : {}) };
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
exports.SessionManagement = SessionManagement;
|
|
600
|
+
function validateSource(value) {
|
|
601
|
+
if (typeof value !== "string" || value.length > 4096 || value.includes("*"))
|
|
602
|
+
throw new ManagementError("来源必须是完整且精确的通道来源键");
|
|
603
|
+
let source;
|
|
604
|
+
try {
|
|
605
|
+
source = JSON.parse(value);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
throw new ManagementError("来源格式无效");
|
|
609
|
+
}
|
|
610
|
+
if (!Array.isArray(source) || source.length !== 5 || source.some(item => typeof item !== "string" || /[\u0000-\u001f]/.test(item)) || !["aun", "feishu"].includes(source[0]) || !source[3] || JSON.stringify(source) !== value)
|
|
611
|
+
throw new ManagementError("来源格式无效");
|
|
612
|
+
}
|
|
613
|
+
function exactIdentifier(value, maximum) {
|
|
614
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum && !/[\u0000-\u001f*]/.test(value);
|
|
615
|
+
}
|
|
616
|
+
function objectFields(value, fields) {
|
|
617
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !fields.includes(key)))
|
|
618
|
+
throw new ManagementError("原生历史响应或授权字段结构无效");
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
621
|
+
function displayMetadata(value) { return value.replace(/[\r\n\u0000-\u001f]/g, " "); }
|
|
622
|
+
function validateNativeSession(value) {
|
|
623
|
+
const session = objectFields(value, ["id", "title", "updatedAt"]);
|
|
624
|
+
if (!exactIdentifier(session.id, 1024) || typeof session.title !== "string" || session.title.length > 4096 || session.updatedAt !== undefined && !validHistoryTime(session.updatedAt))
|
|
625
|
+
throw new ManagementError("原生会话元数据结构无效");
|
|
626
|
+
}
|
|
627
|
+
function validHistoryTime(value) { return typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value)); }
|
|
628
|
+
function validateNativePage(raw, read) {
|
|
629
|
+
const result = objectFields(raw, ["supported", "reason", "sessions", "session", "turns", "nextCursor", "complete", "missingReasons", "readAt"]);
|
|
630
|
+
if (result.supported === false) {
|
|
631
|
+
objectFields(raw, ["supported", "reason"]);
|
|
632
|
+
if (typeof result.reason !== "string" || !result.reason || result.reason.length > 4096)
|
|
633
|
+
throw new ManagementError("原生历史不支持响应结构无效");
|
|
634
|
+
throw new ManagementError(`原生历史不可用:${displayMetadata(result.reason)}`);
|
|
635
|
+
}
|
|
636
|
+
objectFields(raw, ["supported", ...(read ? ["session", "turns"] : ["sessions"]), "nextCursor", "complete", "missingReasons", "readAt"]);
|
|
637
|
+
if (result.supported !== true || typeof result.complete !== "boolean" || !validHistoryTime(result.readAt) || !Array.isArray(result.missingReasons) || result.missingReasons.length > 50 || result.missingReasons.some(value => typeof value !== "string" || value.length > 1024) || result.nextCursor !== undefined && !exactIdentifier(result.nextCursor, 4096))
|
|
638
|
+
throw new ManagementError("原生历史分页响应结构无效");
|
|
639
|
+
if (read) {
|
|
640
|
+
validateNativeSession(result.session);
|
|
641
|
+
if (!Array.isArray(result.turns) || result.turns.length > 100)
|
|
642
|
+
throw new ManagementError("原生历史每页正文不得超过100条,响应结构无效");
|
|
643
|
+
for (const value of result.turns) {
|
|
644
|
+
const turn = objectFields(value, ["id", "role", "text", "time"]);
|
|
645
|
+
if (!["user", "assistant"].includes(turn.role) || typeof turn.text !== "string" || turn.text.length > 60_000 || turn.id !== undefined && !exactIdentifier(turn.id, 1024) || turn.time !== undefined && !validHistoryTime(turn.time))
|
|
646
|
+
throw new ManagementError(typeof turn.text === "string" && turn.text.length > 60_000 ? "原生正文超过60000字符" : "原生历史正文结构无效");
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
if (!Array.isArray(result.sessions) || result.sessions.length > 50)
|
|
651
|
+
throw new ManagementError("原生历史每页不得超过50项,响应结构无效");
|
|
652
|
+
result.sessions.forEach(validateNativeSession);
|
|
653
|
+
}
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
async function nativeTimeout(operation) {
|
|
657
|
+
let timer;
|
|
658
|
+
try {
|
|
659
|
+
return await Promise.race([Promise.resolve().then(operation), new Promise((_, reject) => { timer = setTimeout(() => reject(new ManagementError("原生历史读取超时,请稍后重试")), 10_000); })]);
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
if (timer)
|
|
663
|
+
clearTimeout(timer);
|
|
664
|
+
}
|
|
665
|
+
}
|