@myassis/gateway 1.0.100 → 1.0.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/services/memory/ContextBuilder.js +123 -17
- package/dist/services/memory/ToolCallArchive.js +71 -0
- package/dist/services/session/Session.js +3 -8
- package/dist/services/session/SessionManager.js +2 -0
- package/dist/services/tools/detailTool.js +54 -0
- package/dist/services/tools/index.js +2 -1
- package/package.json +95 -95
- package/scripts/pkg-build-win.js +64 -50
|
@@ -4,6 +4,7 @@ exports.nextTrimLevel = exports.planTurnCompaction = exports.buildContext = expo
|
|
|
4
4
|
const shared_1 = require("@myassis/shared");
|
|
5
5
|
const index_js_1 = require("../../config/index.js");
|
|
6
6
|
const ToolLedger_js_1 = require("./ToolLedger.js");
|
|
7
|
+
const ToolCallArchive_js_1 = require("./ToolCallArchive.js");
|
|
7
8
|
const logger = (0, shared_1.getLogger)('ContextBuilder');
|
|
8
9
|
/**
|
|
9
10
|
* 上下文构造与分级裁剪
|
|
@@ -165,12 +166,38 @@ function collectToolPairs(messages, from) {
|
|
|
165
166
|
}
|
|
166
167
|
return pairs;
|
|
167
168
|
}
|
|
168
|
-
/**
|
|
169
|
-
function
|
|
169
|
+
/** 为缺少 tool_call_id 的工具结果生成稳定的 archiveId(同一对消息在不同裁剪级别上结果一致,避免孤儿条目) */
|
|
170
|
+
function stableArchiveId(sessionId, seed) {
|
|
171
|
+
let h = 2166136261;
|
|
172
|
+
const s = `${sessionId}:${seed}`;
|
|
173
|
+
for (let k = 0; k < s.length; k++) {
|
|
174
|
+
h ^= s.charCodeAt(k);
|
|
175
|
+
h = Math.imul(h, 16777619);
|
|
176
|
+
}
|
|
177
|
+
return `arc-${(h >>> 0).toString(36)}`;
|
|
178
|
+
}
|
|
179
|
+
/** 截断一个 pair 内的 content / reasoning_content / tool_calls.arguments,并在确有截断时归档完整内容 */
|
|
180
|
+
function clipPair(messages, pair, limit, sessionId) {
|
|
170
181
|
const head = messages[pair.start];
|
|
182
|
+
// 先收集每个工具调用的完整入参,避免裁剪覆盖后无法归档
|
|
183
|
+
const fullInputs = new Map();
|
|
184
|
+
if (Array.isArray(head?.tool_calls)) {
|
|
185
|
+
for (const tc of head.tool_calls) {
|
|
186
|
+
const id = tc?.id || tc?.function?.id;
|
|
187
|
+
const full = tc?.input ?? tc?.function?.arguments;
|
|
188
|
+
if (id && full != null) {
|
|
189
|
+
fullInputs.set(id, {
|
|
190
|
+
input: typeof full === 'string' ? full : JSON.stringify(full),
|
|
191
|
+
toolName: tc?.toolName || tc?.function?.name || 'unknown',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
171
196
|
head.content = clip(head.content, limit);
|
|
172
197
|
head.reasoning_content = clip(head.reasoning_content, limit);
|
|
173
|
-
// 工具参数往往是上下文膨胀的主因(写文件 / patch
|
|
198
|
+
// 工具参数往往是上下文膨胀的主因(写文件 / patch 类调用);其完整入参会在下方
|
|
199
|
+
// 工具结果被截断时,与完整输出一并归档(见工具结果循环),此处不再单独归档,
|
|
200
|
+
// 以避免在「未真正裁剪」时产生无提示可引用的孤儿存档。
|
|
174
201
|
if (Array.isArray(head.tool_calls)) {
|
|
175
202
|
for (const tc of head.tool_calls) {
|
|
176
203
|
tc.input = clipJsonArguments(tc.input, limit);
|
|
@@ -180,7 +207,18 @@ function clipPair(messages, pair, limit) {
|
|
|
180
207
|
}
|
|
181
208
|
}
|
|
182
209
|
for (let i = pair.start + 1; i < pair.end; i++) {
|
|
183
|
-
|
|
210
|
+
const msg = messages[i];
|
|
211
|
+
if (!msg || typeof msg.content !== 'string')
|
|
212
|
+
continue;
|
|
213
|
+
const original = msg.content;
|
|
214
|
+
if (original.length <= limit)
|
|
215
|
+
continue; // 仅当真正被截断才归档 + 提示
|
|
216
|
+
const archiveId = msg.tool_call_id
|
|
217
|
+
|| stableArchiveId(sessionId ?? '', `tool:${i}:${original.slice(0, 32)}`);
|
|
218
|
+
const meta = msg.tool_call_id ? fullInputs.get(msg.tool_call_id) : undefined;
|
|
219
|
+
const toolName = msg.tool_call_name || meta?.toolName || 'unknown';
|
|
220
|
+
(0, ToolCallArchive_js_1.archiveToolCall)(sessionId ?? '', archiveId, toolName, meta?.input, original);
|
|
221
|
+
msg.content = clip(original, limit) + '\n' + (0, ToolCallArchive_js_1.formatDetailNote)(archiveId);
|
|
184
222
|
}
|
|
185
223
|
}
|
|
186
224
|
/** 从工具参数中提取用于标识「这次调用做了什么」的关键字段 */
|
|
@@ -262,15 +300,25 @@ function skeletonOutput(content) {
|
|
|
262
300
|
* 将一个工具调用对退化为骨架。
|
|
263
301
|
*
|
|
264
302
|
* 保留 assistant.tool_calls 与配对的 tool 消息(协议要求且模型需要看到调用事实),
|
|
265
|
-
*
|
|
303
|
+
* 只把参数压成身份摘要、把输出压成一行结论;并在输出确有精简时归档完整内容。
|
|
266
304
|
*/
|
|
267
|
-
function skeletonizePair(messages, pair) {
|
|
305
|
+
function skeletonizePair(messages, pair, sessionId) {
|
|
268
306
|
const head = messages[pair.start];
|
|
269
307
|
head.content = clip(head.content, SKELETON_OUTPUT_LIMIT);
|
|
270
308
|
// 旧轮次的思维链对当前决策价值最低,直接丢弃
|
|
271
309
|
delete head.reasoning_content;
|
|
272
|
-
|
|
310
|
+
// 收集完整入参用于归档(在工具结果被精简时与完整输出一并归档)
|
|
311
|
+
const fullInputs = new Map();
|
|
312
|
+
if (Array.isArray(head?.tool_calls)) {
|
|
273
313
|
for (const tc of head.tool_calls) {
|
|
314
|
+
const id = tc?.id || tc?.function?.id;
|
|
315
|
+
const full = tc?.input ?? tc?.function?.arguments;
|
|
316
|
+
if (id && full != null) {
|
|
317
|
+
fullInputs.set(id, {
|
|
318
|
+
input: typeof full === 'string' ? full : JSON.stringify(full),
|
|
319
|
+
toolName: tc?.toolName || tc?.function?.name || 'unknown',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
274
322
|
tc.input = skeletonArguments(tc.input);
|
|
275
323
|
if (tc.function) {
|
|
276
324
|
tc.function.arguments = skeletonArguments(tc.function.arguments);
|
|
@@ -278,16 +326,35 @@ function skeletonizePair(messages, pair) {
|
|
|
278
326
|
}
|
|
279
327
|
}
|
|
280
328
|
for (let i = pair.start + 1; i < pair.end; i++) {
|
|
281
|
-
|
|
329
|
+
const msg = messages[i];
|
|
330
|
+
if (!msg)
|
|
331
|
+
continue;
|
|
332
|
+
const original = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content ?? '');
|
|
333
|
+
// 骨架化总是有损,但仅在原文较长时值得归档 + 提示
|
|
334
|
+
if (original.length <= SKELETON_OUTPUT_LIMIT) {
|
|
335
|
+
msg.content = skeletonOutput(original);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const archiveId = msg.tool_call_id
|
|
339
|
+
|| stableArchiveId(sessionId ?? '', `tool:${i}:${original.slice(0, 32)}`);
|
|
340
|
+
const meta = msg.tool_call_id ? fullInputs.get(msg.tool_call_id) : undefined;
|
|
341
|
+
const toolName = msg.tool_call_name || meta?.toolName || 'unknown';
|
|
342
|
+
(0, ToolCallArchive_js_1.archiveToolCall)(sessionId ?? '', archiveId, toolName, meta?.input, original);
|
|
343
|
+
msg.content = skeletonOutput(original) + '\n' + (0, ToolCallArchive_js_1.formatDetailNote)(archiveId);
|
|
282
344
|
}
|
|
283
345
|
}
|
|
284
346
|
/**
|
|
285
347
|
* 把最早的工具调用对折叠进账本。
|
|
286
348
|
*
|
|
287
349
|
* 与已废弃的「直接丢弃」不同:调用事实不会消失,而是转成
|
|
288
|
-
* 首条 system
|
|
350
|
+
* 首条 system 消息中的「已完成操作」清单。被折叠工具调用的完整内容会归档,
|
|
351
|
+
* 并在账本末尾附上 detailTool 调阅提示。
|
|
352
|
+
*
|
|
353
|
+
* 注意:调用本函数时 `working` 已被本级别的骨架化/截断先行处理,因此归档用的
|
|
354
|
+
* 完整内容必须取自 `originals`(buildContext 入参的未改动副本),否则会存下已被
|
|
355
|
+
* 压缩过的内容而非原文。
|
|
289
356
|
*/
|
|
290
|
-
function foldPairsToLedger(messages, pairs, foldCount, protectFrom) {
|
|
357
|
+
function foldPairsToLedger(messages, originals, pairs, foldCount, protectFrom, sessionId) {
|
|
291
358
|
if (foldCount <= 0)
|
|
292
359
|
return messages;
|
|
293
360
|
const foldIndexes = new Set();
|
|
@@ -298,18 +365,56 @@ function foldPairsToLedger(messages, pairs, foldCount, protectFrom) {
|
|
|
298
365
|
folded.push(messages[j]);
|
|
299
366
|
}
|
|
300
367
|
}
|
|
368
|
+
// 用 originals 收集折叠区间内工具调用的【完整】入参,供归档使用
|
|
369
|
+
const fullInputs = new Map();
|
|
370
|
+
for (const idx of foldIndexes) {
|
|
371
|
+
const m = originals[idx];
|
|
372
|
+
if (m?.role === 'assistant' && Array.isArray(m.tool_calls)) {
|
|
373
|
+
for (const tc of m.tool_calls) {
|
|
374
|
+
const id = tc?.id || tc?.function?.id;
|
|
375
|
+
const full = tc?.input ?? tc?.function?.arguments;
|
|
376
|
+
if (id && full != null) {
|
|
377
|
+
fullInputs.set(id, {
|
|
378
|
+
input: typeof full === 'string' ? full : JSON.stringify(full),
|
|
379
|
+
toolName: tc?.toolName || tc?.function?.name || 'unknown',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// 归档被折叠工具调用的【完整】内容(取自 originals),并在账本末尾列出可调阅的 archiveId
|
|
386
|
+
const ledgerLines = [];
|
|
387
|
+
for (const idx of foldIndexes) {
|
|
388
|
+
const m = messages[idx];
|
|
389
|
+
const orig = originals[idx];
|
|
390
|
+
if (m?.role !== 'tool')
|
|
391
|
+
continue;
|
|
392
|
+
const original = typeof orig?.content === 'string' ? orig.content : JSON.stringify(orig?.content ?? '');
|
|
393
|
+
if (original.length <= SKELETON_OUTPUT_LIMIT)
|
|
394
|
+
continue;
|
|
395
|
+
const archiveId = m.tool_call_id
|
|
396
|
+
|| stableArchiveId(sessionId ?? '', `fold:${m.seq ?? ''}:${original.slice(0, 32)}`);
|
|
397
|
+
const meta = m.tool_call_id ? fullInputs.get(m.tool_call_id) : undefined;
|
|
398
|
+
const toolName = m.tool_call_name || meta?.toolName || 'unknown';
|
|
399
|
+
(0, ToolCallArchive_js_1.archiveToolCall)(sessionId ?? '', archiveId, toolName, meta?.input, original);
|
|
400
|
+
ledgerLines.push(`- ${toolName}(完整内容请调阅工具 detailTool 查看,archiveId: "${archiveId}")`);
|
|
401
|
+
}
|
|
301
402
|
const ledger = (0, ToolLedger_js_1.renderRequestLedger)(folded);
|
|
403
|
+
let ledgerContent = ledger;
|
|
404
|
+
if (ledgerLines.length > 0) {
|
|
405
|
+
ledgerContent = `${ledger}\n\n【已折叠工具调用存档】以下调用已被折叠为账本,其完整入参/输出可用 detailTool 调阅:\n${ledgerLines.join('\n')}`;
|
|
406
|
+
}
|
|
302
407
|
const kept = messages.filter((_, index) => !foldIndexes.has(index));
|
|
303
|
-
if (!
|
|
408
|
+
if (!ledgerContent)
|
|
304
409
|
return kept;
|
|
305
410
|
// 并入首条 system 消息:部分厂商只允许 system 位于首位
|
|
306
411
|
if (kept[0]?.role === 'system' && typeof kept[0].content === 'string') {
|
|
307
|
-
kept[0] = { ...kept[0], content: `${kept[0].content}\n\n${
|
|
412
|
+
kept[0] = { ...kept[0], content: `${kept[0].content}\n\n${ledgerContent}` };
|
|
308
413
|
}
|
|
309
414
|
else {
|
|
310
415
|
kept.splice(Math.min(protectFrom, kept.length), 0, {
|
|
311
416
|
role: 'assistant',
|
|
312
|
-
content:
|
|
417
|
+
content: ledgerContent,
|
|
313
418
|
attachments: [],
|
|
314
419
|
});
|
|
315
420
|
}
|
|
@@ -341,8 +446,9 @@ function degradeAttachments(messages, keepLastN) {
|
|
|
341
446
|
* @param protectFrom 该下标之前的消息视为“历史/摘要”,工具对裁剪只作用于其后
|
|
342
447
|
* @param maxChars 字符预算
|
|
343
448
|
* @param minLevel 最低裁剪级别(用于超限重试时强制加重)
|
|
449
|
+
* @param sessionId 会话标识,用于裁剪时归档完整内容(缺省时不归档)
|
|
344
450
|
*/
|
|
345
|
-
function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.contextMaxChars, minLevel = TrimLevel.OldToolPayload) {
|
|
451
|
+
function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.contextMaxChars, minLevel = TrimLevel.OldToolPayload, sessionId) {
|
|
346
452
|
let working = messages.map(cloneMessage);
|
|
347
453
|
let level = TrimLevel.None;
|
|
348
454
|
const overBudget = () => estimateChars(working) > maxChars;
|
|
@@ -357,7 +463,7 @@ function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.con
|
|
|
357
463
|
if (target >= TrimLevel.SkeletonOldCalls) {
|
|
358
464
|
const skeletonUntil = Math.max(0, pairs.length - index_js_1.appConfig.minToolPairKeep);
|
|
359
465
|
for (let i = 0; i < skeletonUntil; i++) {
|
|
360
|
-
skeletonizePair(working, pairs[i]);
|
|
466
|
+
skeletonizePair(working, pairs[i], sessionId);
|
|
361
467
|
}
|
|
362
468
|
}
|
|
363
469
|
// 2) 截断超出 keep 轮的旧工具负载(骨架化过的 pair 已经很小,重复处理无害)
|
|
@@ -366,7 +472,7 @@ function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.con
|
|
|
366
472
|
? Math.max(200, Math.floor(index_js_1.appConfig.oldToolContentLimit / 2))
|
|
367
473
|
: index_js_1.appConfig.oldToolContentLimit;
|
|
368
474
|
for (let i = 0; i < clipUntil; i++) {
|
|
369
|
-
clipPair(working, pairs[i], argLimit);
|
|
475
|
+
clipPair(working, pairs[i], argLimit, sessionId);
|
|
370
476
|
}
|
|
371
477
|
// 3) 降级旧附件
|
|
372
478
|
if (target >= TrimLevel.Aggressive) {
|
|
@@ -374,7 +480,7 @@ function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.con
|
|
|
374
480
|
}
|
|
375
481
|
// 4) 最后手段:折叠为账本(调用事实保留在 system 清单中)
|
|
376
482
|
if (target >= TrimLevel.FoldToLedger && pairs.length > index_js_1.appConfig.minToolPairKeep) {
|
|
377
|
-
working = foldPairsToLedger(working, pairs, pairs.length - index_js_1.appConfig.minToolPairKeep, protectFrom);
|
|
483
|
+
working = foldPairsToLedger(working, messages, pairs, pairs.length - index_js_1.appConfig.minToolPairKeep, protectFrom, sessionId);
|
|
378
484
|
}
|
|
379
485
|
};
|
|
380
486
|
for (const target of [minLevel, TrimLevel.Aggressive, TrimLevel.SkeletonOldCalls, TrimLevel.FoldToLedger]) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatDetailNote = exports.clearSessionArchive = exports.getArchivedToolCall = exports.archiveToolCall = void 0;
|
|
4
|
+
const shared_1 = require("@myassis/shared");
|
|
5
|
+
/**
|
|
6
|
+
* 工具调用完整内容存档
|
|
7
|
+
*
|
|
8
|
+
* 背景:ContextBuilder 在上下文裁剪(截断 / 骨架化 / 折叠为账本)时会丢弃
|
|
9
|
+
* 工具调用的「完整内容」,只保留有损的事实。为了让模型在需要时能取回被丢弃的
|
|
10
|
+
* 原文,本模块在裁剪发生时把「完整入参 + 完整输出」按 archiveId 保存起来,
|
|
11
|
+
* 并在裁剪后的内容中追加「完整内容请调阅工具 detailTool 查看」的提示。
|
|
12
|
+
*
|
|
13
|
+
* 设计要点:
|
|
14
|
+
* - 以 sessionId -> (archiveId -> entry) 两级 Map 组织,detailTool 按会话隔离读取。
|
|
15
|
+
* - archiveId 默认取工具调用的 tool_call_id(天然稳定且唯一),因此多次裁剪同一
|
|
16
|
+
* 调用只会更新同一条记录(幂等合并),不会产生孤儿条目。
|
|
17
|
+
* - 仅在裁剪「真实发生」(原文超过阈值)时才写入,避免无谓占用内存。
|
|
18
|
+
*/
|
|
19
|
+
const logger = (0, shared_1.getLogger)('ToolCallArchive');
|
|
20
|
+
/** sessionId -> (archiveId -> entry) */
|
|
21
|
+
const store = new Map();
|
|
22
|
+
/** 单会话最多保留的存档条数,超出后丢弃最早插入者 */
|
|
23
|
+
const MAX_PER_SESSION = 1000;
|
|
24
|
+
/**
|
|
25
|
+
* 保存 / 合并一次工具调用的完整内容。
|
|
26
|
+
*
|
|
27
|
+
* 以 archiveId 为键做幂等合并:多次裁剪同一调用只会更新同一条记录,
|
|
28
|
+
* 因此即便 buildContext 在多个 trim level 上尝试裁剪,也不会产生孤儿条目。
|
|
29
|
+
*/
|
|
30
|
+
function archiveToolCall(sessionId, archiveId, toolName, input, output) {
|
|
31
|
+
if (!sessionId || !archiveId)
|
|
32
|
+
return;
|
|
33
|
+
let sessionMap = store.get(sessionId);
|
|
34
|
+
if (!sessionMap) {
|
|
35
|
+
sessionMap = new Map();
|
|
36
|
+
store.set(sessionId, sessionMap);
|
|
37
|
+
}
|
|
38
|
+
const existing = sessionMap.get(archiveId);
|
|
39
|
+
sessionMap.set(archiveId, {
|
|
40
|
+
archiveId,
|
|
41
|
+
sessionId,
|
|
42
|
+
toolName: toolName || existing?.toolName || 'unknown',
|
|
43
|
+
input: input && input.length ? input : existing?.input || '',
|
|
44
|
+
output: output && output.length ? output : existing?.output || '',
|
|
45
|
+
archivedAt: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
// 容量保护:超出后丢弃最早插入的条目
|
|
48
|
+
if (sessionMap.size > MAX_PER_SESSION) {
|
|
49
|
+
const oldest = sessionMap.keys().next().value;
|
|
50
|
+
if (oldest !== undefined)
|
|
51
|
+
sessionMap.delete(oldest);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.archiveToolCall = archiveToolCall;
|
|
55
|
+
/** 按会话 + archiveId 取回被保存的完整内容 */
|
|
56
|
+
function getArchivedToolCall(sessionId, archiveId) {
|
|
57
|
+
return store.get(sessionId)?.get(archiveId);
|
|
58
|
+
}
|
|
59
|
+
exports.getArchivedToolCall = getArchivedToolCall;
|
|
60
|
+
/** 会话结束时清理,避免内存常驻 */
|
|
61
|
+
function clearSessionArchive(sessionId) {
|
|
62
|
+
if (store.delete(sessionId)) {
|
|
63
|
+
logger.debug(`已清理会话 ${sessionId} 的工具调用存档`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.clearSessionArchive = clearSessionArchive;
|
|
67
|
+
/** 生成裁剪提示,引导模型通过 detailTool 调阅完整内容 */
|
|
68
|
+
function formatDetailNote(archiveId) {
|
|
69
|
+
return `完整内容请调阅工具 detailTool 查看(archiveId: "${archiveId}")`;
|
|
70
|
+
}
|
|
71
|
+
exports.formatDetailNote = formatDetailNote;
|
|
@@ -1018,17 +1018,12 @@ class Session {
|
|
|
1018
1018
|
const callModel = async () => {
|
|
1019
1019
|
let transientRetries = 0;
|
|
1020
1020
|
while (true) {
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
const built = (0, ContextBuilder_js_1.buildContext)(messages, messagesLength, effectiveContextMaxChars, trimLevel);
|
|
1021
|
+
// 分级裁剪只截断旧工具负载(保留最近 turnCompactKeepPairs 轮完整内容),
|
|
1022
|
+
// 不主动做摘要压缩;只有模型返回上下文超限错误时才在 catch 中触发轮内压缩。
|
|
1023
|
+
const built = (0, ContextBuilder_js_1.buildContext)(messages, messagesLength, effectiveContextMaxChars, trimLevel, this.id);
|
|
1025
1024
|
if (built.trimmed) {
|
|
1026
1025
|
logger.debug(`上下文裁剪: level=${built.level} chars=${built.chars}`);
|
|
1027
1026
|
}
|
|
1028
|
-
if (built.chars > effectiveContextMaxChars) {
|
|
1029
|
-
if (await compactInTurn())
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
1027
|
// 收尾阶段不再下发工具定义:只要工具还在,模型大概率继续调用而不收尾
|
|
1033
1028
|
const llmClient = new LLMClient_js_1.LLMClient(models, built.messages, this.abortController.signal, forceFinalize ? [] : tools);
|
|
1034
1029
|
llmClient.setPreferredModel(this.selectModelId);
|
|
@@ -7,6 +7,7 @@ const logger = (0, shared_1.getLogger)('SessionManager');
|
|
|
7
7
|
const SessionStore_js_1 = require("./SessionStore.js");
|
|
8
8
|
const authStore_js_1 = require("../../stores/authStore.js");
|
|
9
9
|
const Session_js_1 = require("./Session.js");
|
|
10
|
+
const ToolCallArchive_js_1 = require("../memory/ToolCallArchive.js");
|
|
10
11
|
/**
|
|
11
12
|
* SessionManager - Business logic layer
|
|
12
13
|
* Manages sessions in memory
|
|
@@ -157,6 +158,7 @@ class SessionManager {
|
|
|
157
158
|
SessionStore_js_1.sessionStore.deleteSession(sessionId);
|
|
158
159
|
});
|
|
159
160
|
this.sessions.delete(sessionId);
|
|
161
|
+
(0, ToolCallArchive_js_1.clearSessionArchive)(sessionId);
|
|
160
162
|
logger.info(`Deleted session ${sessionId}`);
|
|
161
163
|
return true;
|
|
162
164
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detailTool = void 0;
|
|
4
|
+
const ToolCallArchive_js_1 = require("../memory/ToolCallArchive.js");
|
|
5
|
+
/**
|
|
6
|
+
* detailTool —— 调阅被上下文裁剪的工具调用的完整内容。
|
|
7
|
+
*
|
|
8
|
+
* 当某次工具调用的结果 / 参数因上下文裁剪被压缩时,上下文里会附带形如
|
|
9
|
+
* 「完整内容请调阅工具 detailTool 查看(archiveId: "xxx")」的提示。
|
|
10
|
+
* 传入对应的 archiveId 即可取回该次调用的完整入参与输出。
|
|
11
|
+
*/
|
|
12
|
+
exports.detailTool = {
|
|
13
|
+
name: 'detailTool',
|
|
14
|
+
description: '查看此前因上下文裁剪(截断 / 骨架化 / 折叠为账本)而被压缩的工具调用的完整内容。' +
|
|
15
|
+
'上下文中被裁剪的工具结果会附带「完整内容请调阅工具 detailTool 查看(archiveId: "xxx")」的提示,' +
|
|
16
|
+
'传入其中的 archiveId 即可取回该次调用的完整入参(arguments)与输出(content)。仅在确实需要回看被裁剪内容时使用。',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
archiveId: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: '裁剪提示中给出的 archiveId,用于定位被保存的完整工具调用内容',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ['archiveId'],
|
|
26
|
+
},
|
|
27
|
+
handler: async (args, sessionId) => {
|
|
28
|
+
try {
|
|
29
|
+
const { archiveId } = args;
|
|
30
|
+
if (!archiveId || typeof archiveId !== 'string') {
|
|
31
|
+
return { success: false, errorMessage: '缺少 archiveId 参数' };
|
|
32
|
+
}
|
|
33
|
+
const entry = (0, ToolCallArchive_js_1.getArchivedToolCall)(String(sessionId || ''), archiveId);
|
|
34
|
+
if (!entry) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
errorMessage: `未找到 archiveId="${archiveId}" 对应的工具调用存档。` +
|
|
38
|
+
'该存档可能来自更早的进程或会话,或该次调用并未被裁剪。',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const output = JSON.stringify({
|
|
42
|
+
toolName: entry.toolName,
|
|
43
|
+
archiveId: entry.archiveId,
|
|
44
|
+
archivedAt: entry.archivedAt,
|
|
45
|
+
input: entry.input || '',
|
|
46
|
+
output: entry.output || '',
|
|
47
|
+
}, null, 2);
|
|
48
|
+
return { success: true, output };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return { success: false, errorMessage: `调阅工具调用存档失败: ${error?.message}` };
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -38,10 +38,11 @@ const webFetch_js_1 = require("./webFetch.js");
|
|
|
38
38
|
const sessionsSpawn_js_1 = require("./sessionsSpawn.js");
|
|
39
39
|
const setSessionTitle_js_1 = require("./setSessionTitle.js");
|
|
40
40
|
const plan_js_1 = require("./plan.js");
|
|
41
|
+
const detailTool_js_1 = require("./detailTool.js");
|
|
41
42
|
exports.tools = [
|
|
42
43
|
search_js_1.searchTool, calculator_js_1.calculatorTool, screenshot_js_1.screenshotTool, keyboard_js_1.keyboardTool, mouse_js_1.mouseTool,
|
|
43
44
|
skill_js_1.skillTool, exec_js_1.execTool, task_js_1.taskTool, model_js_1.modelTool, fetch_js_1.fetchTool,
|
|
44
|
-
edit_js_1.editTool, webFetch_js_1.webFetchTool, file_js_1.fileTool, sessionsSpawn_js_1.sessionsSpawnTool, setSessionTitle_js_1.setSessionTitleTool, plan_js_1.updatePlanTool
|
|
45
|
+
edit_js_1.editTool, webFetch_js_1.webFetchTool, file_js_1.fileTool, sessionsSpawn_js_1.sessionsSpawnTool, setSessionTitle_js_1.setSessionTitleTool, plan_js_1.updatePlanTool, detailTool_js_1.detailTool
|
|
45
46
|
];
|
|
46
47
|
function getToolByName(name) {
|
|
47
48
|
return exports.tools.find(tool => tool.name === name);
|
package/package.json
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@myassis/gateway",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"myassis-gateway": "dist/index.js",
|
|
8
|
-
"gateway": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"dist",
|
|
12
|
-
"scripts",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE",
|
|
15
|
-
"migrations"
|
|
16
|
-
],
|
|
17
|
-
"exports": {
|
|
18
|
-
".": {
|
|
19
|
-
"require": "./dist/index.js"
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
"scripts": {
|
|
23
|
-
"dev": "tsx watch src/index.ts",
|
|
24
|
-
"start": "node dist/index.js",
|
|
25
|
-
"build": "tsc --skipLibCheck",
|
|
26
|
-
"postbuild": "node scripts/add-shebang.js && npx tsc-alias",
|
|
27
|
-
"pkg:win": "node scripts/pkg-build-win.js node24-win-x64 myassis-gateway-win.exe",
|
|
28
|
-
"pkg:linux": "npx @yao-pkg/pkg . --targets node24-linux-x64 --output myassis-gateway-linux",
|
|
29
|
-
"pkg:all": "npm run pkg:win && npm run pkg:linux",
|
|
30
|
-
"pkg:linux-analyze": "npx @yao-pkg/pkg . --targets node24-linux-x64 --output myassis-gateway-linux --public-packages got --debug",
|
|
31
|
-
"test": "jest"
|
|
32
|
-
},
|
|
33
|
-
"keywords": [
|
|
34
|
-
"myassis",
|
|
35
|
-
"gateway",
|
|
36
|
-
"ai",
|
|
37
|
-
"local-ai",
|
|
38
|
-
"websocket",
|
|
39
|
-
"service"
|
|
40
|
-
],
|
|
41
|
-
"author": "duzhengjie",
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@myassis/shared": "1.0.19",
|
|
44
|
-
"@nut-tree/nut-js": "^4.2.0",
|
|
45
|
-
"axios": "^1.15.2",
|
|
46
|
-
"bcryptjs": "^2.4.3",
|
|
47
|
-
"better-sqlite3": "12.9.0",
|
|
48
|
-
"cheerio": "^1.2.0",
|
|
49
|
-
"compression": "^1.7.4",
|
|
50
|
-
"cors": "^2.8.5",
|
|
51
|
-
"dotenv": "^16.3.1",
|
|
52
|
-
"express": "^4.18.2",
|
|
53
|
-
"helmet": "^7.1.0",
|
|
54
|
-
"https-proxy-agent": "^9.0.0",
|
|
55
|
-
"jsonwebtoken": "^9.0.2",
|
|
56
|
-
"multer": "^2.1.1",
|
|
57
|
-
"screenshot-desktop": "^1.15.4",
|
|
58
|
-
"undici": "^8.2.0",
|
|
59
|
-
"uuid": "^9.0.1",
|
|
60
|
-
"ws": "^8.20.0"
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@types/bcryptjs": "^2.4.6",
|
|
64
|
-
"@types/cors": "^2.8.17",
|
|
65
|
-
"@types/express": "^4.17.21",
|
|
66
|
-
"@types/jsonwebtoken": "^9.0.6",
|
|
67
|
-
"@types/multer": "^1.4.11",
|
|
68
|
-
"@types/node": "^20.11.0",
|
|
69
|
-
"@types/uuid": "^9.0.8",
|
|
70
|
-
"@types/ws": "^8.5.10",
|
|
71
|
-
"@yao-pkg/pkg": "^6.20.0",
|
|
72
|
-
"tsc-alias": "^1.8.10",
|
|
73
|
-
"tsx": "^4.7.0",
|
|
74
|
-
"typescript": "^5.3.3",
|
|
75
|
-
"@yao-pkg/pkg-fetch": "3.6.3"
|
|
76
|
-
},
|
|
77
|
-
"pkg": {
|
|
78
|
-
"scripts": [
|
|
79
|
-
"dist/index.js"
|
|
80
|
-
],
|
|
81
|
-
"assets": [
|
|
82
|
-
"dist/**/*",
|
|
83
|
-
"migrations/**/*",
|
|
84
|
-
"README*",
|
|
85
|
-
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
|
86
|
-
"node_modules/.pnpm/@nut-tree+libnut-linux@2.7.1/node_modules/@nut-tree/libnut-linux/build/Release/libnut.node",
|
|
87
|
-
"node_modules/.pnpm/@nut-tree+libnut-win32@2.7.1/node_modules/@nut-tree/libnut-win32/build/Release/libnut.node",
|
|
88
|
-
"nssm.exe"
|
|
89
|
-
],
|
|
90
|
-
"targets": [
|
|
91
|
-
"node24-win-x64",
|
|
92
|
-
"node24-linux-x64"
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@myassis/gateway",
|
|
3
|
+
"version": "1.0.102",
|
|
4
|
+
"description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"myassis-gateway": "dist/index.js",
|
|
8
|
+
"gateway": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"scripts",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"migrations"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"require": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsx watch src/index.ts",
|
|
24
|
+
"start": "node dist/index.js",
|
|
25
|
+
"build": "tsc --skipLibCheck",
|
|
26
|
+
"postbuild": "node scripts/add-shebang.js && npx tsc-alias",
|
|
27
|
+
"pkg:win": "node scripts/pkg-build-win.js node24-win-x64 myassis-gateway-win.exe",
|
|
28
|
+
"pkg:linux": "npx @yao-pkg/pkg . --targets node24-linux-x64 --output myassis-gateway-linux",
|
|
29
|
+
"pkg:all": "npm run pkg:win && npm run pkg:linux",
|
|
30
|
+
"pkg:linux-analyze": "npx @yao-pkg/pkg . --targets node24-linux-x64 --output myassis-gateway-linux --public-packages got --debug",
|
|
31
|
+
"test": "jest"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"myassis",
|
|
35
|
+
"gateway",
|
|
36
|
+
"ai",
|
|
37
|
+
"local-ai",
|
|
38
|
+
"websocket",
|
|
39
|
+
"service"
|
|
40
|
+
],
|
|
41
|
+
"author": "duzhengjie",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@myassis/shared": "1.0.19",
|
|
44
|
+
"@nut-tree/nut-js": "^4.2.0",
|
|
45
|
+
"axios": "^1.15.2",
|
|
46
|
+
"bcryptjs": "^2.4.3",
|
|
47
|
+
"better-sqlite3": "12.9.0",
|
|
48
|
+
"cheerio": "^1.2.0",
|
|
49
|
+
"compression": "^1.7.4",
|
|
50
|
+
"cors": "^2.8.5",
|
|
51
|
+
"dotenv": "^16.3.1",
|
|
52
|
+
"express": "^4.18.2",
|
|
53
|
+
"helmet": "^7.1.0",
|
|
54
|
+
"https-proxy-agent": "^9.0.0",
|
|
55
|
+
"jsonwebtoken": "^9.0.2",
|
|
56
|
+
"multer": "^2.1.1",
|
|
57
|
+
"screenshot-desktop": "^1.15.4",
|
|
58
|
+
"undici": "^8.2.0",
|
|
59
|
+
"uuid": "^9.0.1",
|
|
60
|
+
"ws": "^8.20.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/bcryptjs": "^2.4.6",
|
|
64
|
+
"@types/cors": "^2.8.17",
|
|
65
|
+
"@types/express": "^4.17.21",
|
|
66
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
67
|
+
"@types/multer": "^1.4.11",
|
|
68
|
+
"@types/node": "^20.11.0",
|
|
69
|
+
"@types/uuid": "^9.0.8",
|
|
70
|
+
"@types/ws": "^8.5.10",
|
|
71
|
+
"@yao-pkg/pkg": "^6.20.0",
|
|
72
|
+
"tsc-alias": "^1.8.10",
|
|
73
|
+
"tsx": "^4.7.0",
|
|
74
|
+
"typescript": "^5.3.3",
|
|
75
|
+
"@yao-pkg/pkg-fetch": "3.6.3"
|
|
76
|
+
},
|
|
77
|
+
"pkg": {
|
|
78
|
+
"scripts": [
|
|
79
|
+
"dist/index.js"
|
|
80
|
+
],
|
|
81
|
+
"assets": [
|
|
82
|
+
"dist/**/*",
|
|
83
|
+
"migrations/**/*",
|
|
84
|
+
"README*",
|
|
85
|
+
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
|
86
|
+
"node_modules/.pnpm/@nut-tree+libnut-linux@2.7.1/node_modules/@nut-tree/libnut-linux/build/Release/libnut.node",
|
|
87
|
+
"node_modules/.pnpm/@nut-tree+libnut-win32@2.7.1/node_modules/@nut-tree/libnut-win32/build/Release/libnut.node",
|
|
88
|
+
"nssm.exe"
|
|
89
|
+
],
|
|
90
|
+
"targets": [
|
|
91
|
+
"node24-win-x64",
|
|
92
|
+
"node24-linux-x64"
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
}
|
package/scripts/pkg-build-win.js
CHANGED
|
@@ -1,50 +1,64 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// pkg-build-win.js - Windows pkg build using PKG_CACHE_PATH
|
|
3
|
-
// Points to ~/.pkg-cache which contains the correct v3.6 patched binary (40.6 MB, hash matches)
|
|
4
|
-
'use strict';
|
|
5
|
-
const { spawn } = require('child_process');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
|
|
10
|
-
const TARGET = process.argv[2] || 'node18-win-x64';
|
|
11
|
-
const OUTPUT = process.argv[3] || 'myassis-gateway-win.exe';
|
|
12
|
-
|
|
13
|
-
const CACHE_DIR = path.join(os.homedir(), '.pkg-cache');
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pkg-build-win.js - Windows pkg build using PKG_CACHE_PATH
|
|
3
|
+
// Points to ~/.pkg-cache which contains the correct v3.6 patched binary (40.6 MB, hash matches)
|
|
4
|
+
'use strict';
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const TARGET = process.argv[2] || 'node18-win-x64';
|
|
11
|
+
const OUTPUT = process.argv[3] || 'myassis-gateway-win.exe';
|
|
12
|
+
|
|
13
|
+
const CACHE_DIR = path.join(os.homedir(), '.pkg-cache');
|
|
14
|
+
const PKG_BIN = path.resolve(__dirname, '../node_modules/@yao-pkg/pkg/lib-es5/bin.js');
|
|
15
|
+
|
|
16
|
+
function log(msg) { console.error('[pkg-build] ' + msg); }
|
|
17
|
+
|
|
18
|
+
/** 根据 target (如 node24-win-x64) 在缓存目录中找到匹配的二进制 */
|
|
19
|
+
function findCachedBinary(target) {
|
|
20
|
+
const m = target.match(/^node(\d+)-(.+)$/);
|
|
21
|
+
if (!m) return null;
|
|
22
|
+
const [, major, plat] = m;
|
|
23
|
+
const v36dir = path.join(CACHE_DIR, 'v3.6');
|
|
24
|
+
if (!fs.existsSync(v36dir)) return null;
|
|
25
|
+
const candidates = fs.readdirSync(v36dir)
|
|
26
|
+
.filter(f => new RegExp(`^fetched-v${major}\\.\\d+\\.\\d+-${plat}$`).test(f))
|
|
27
|
+
.sort()
|
|
28
|
+
.reverse();
|
|
29
|
+
return candidates.length > 0 ? path.join(v36dir, candidates[0]) : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
log(`Building for ${TARGET}`);
|
|
34
|
+
log(`Output: ${OUTPUT}`);
|
|
35
|
+
|
|
36
|
+
// Verify cached binary exists (dynamically match by target)
|
|
37
|
+
const cachedBinary = findCachedBinary(TARGET);
|
|
38
|
+
if (!cachedBinary) {
|
|
39
|
+
log(`ERROR: No cached binary found for target ${TARGET} in ${CACHE_DIR}`);
|
|
40
|
+
log(`Please run: node node_modules/@yao-pkg/pkg/lib-es5/bin.js --targets ${TARGET} first`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const size = (fs.statSync(cachedBinary).size / 1024 / 1024).toFixed(1);
|
|
44
|
+
log(`Using cached binary: ${cachedBinary} (${size} MB)`);
|
|
45
|
+
|
|
46
|
+
// Use PKG_CACHE_PATH so pkg-fetch finds the correct v3.6 binary by hash
|
|
47
|
+
const env = { ...process.env };
|
|
48
|
+
delete env.PKG_NODE_PATH; // MUST NOT use PKG_NODE_PATH - it bypasses placeholders check
|
|
49
|
+
env.PKG_CACHE_PATH = CACHE_DIR;
|
|
50
|
+
log(`PKG_CACHE_PATH=${CACHE_DIR}`);
|
|
51
|
+
|
|
52
|
+
log('Starting pkg...');
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const child = spawn(process.execPath, [PKG_BIN, '.', '--targets', TARGET, '--output', OUTPUT], {
|
|
55
|
+
cwd: path.resolve(__dirname, '..'),
|
|
56
|
+
env,
|
|
57
|
+
stdio: 'inherit'
|
|
58
|
+
});
|
|
59
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
60
|
+
child.on('error', (e) => { log('Spawn error: ' + e.message); process.exit(1); });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(e => { console.error('[pkg-build] Fatal:', e.message); process.exit(1); });
|