@myassis/gateway 1.0.101 → 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.
@@ -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
- /** 截断一个 pair 内的 content / reasoning_content / tool_calls.arguments */
169
- function clipPair(messages, pair, limit) {
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
- messages[i].content = clip(messages[i].content, limit);
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
- if (Array.isArray(head.tool_calls)) {
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
- messages[i].content = skeletonOutput(messages[i].content);
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 (!ledger)
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${ledger}` };
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: ledger,
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;
@@ -1020,7 +1020,7 @@ class Session {
1020
1020
  while (true) {
1021
1021
  // 分级裁剪只截断旧工具负载(保留最近 turnCompactKeepPairs 轮完整内容),
1022
1022
  // 不主动做摘要压缩;只有模型返回上下文超限错误时才在 catch 中触发轮内压缩。
1023
- const built = (0, ContextBuilder_js_1.buildContext)(messages, messagesLength, effectiveContextMaxChars, trimLevel);
1023
+ const built = (0, ContextBuilder_js_1.buildContext)(messages, messagesLength, effectiveContextMaxChars, trimLevel, this.id);
1024
1024
  if (built.trimmed) {
1025
1025
  logger.debug(`上下文裁剪: level=${built.level} chars=${built.chars}`);
1026
1026
  }
@@ -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.101",
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
+ }
@@ -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 V36_BINARY = path.join(CACHE_DIR, 'v3.6', 'fetched-v18.20.8-win-x64');
15
- const PKG_BIN = path.resolve(__dirname, '../node_modules/@yao-pkg/pkg/lib-es5/bin.js');
16
-
17
- function log(msg) { console.error('[pkg-build] ' + msg); }
18
-
19
- async function main() {
20
- log(`Building for ${TARGET}`);
21
- log(`Output: ${OUTPUT}`);
22
-
23
- // Verify v3.6 binary exists
24
- if (!fs.existsSync(V36_BINARY)) {
25
- log(`ERROR: Patched binary not found at ${V36_BINARY}`);
26
- log('Please run: node node_modules/@yao-pkg/pkg/lib-es5/bin.js --targets node18-win-x64 first');
27
- process.exit(1);
28
- }
29
- const size = (fs.statSync(V36_BINARY).size / 1024 / 1024).toFixed(1);
30
- log(`Using patched binary: ${V36_BINARY} (${size} MB)`);
31
-
32
- // Use PKG_CACHE_PATH so pkg-fetch finds the correct v3.6 binary by hash
33
- const env = { ...process.env };
34
- delete env.PKG_NODE_PATH; // MUST NOT use PKG_NODE_PATH - it bypasses placeholders check
35
- env.PKG_CACHE_PATH = CACHE_DIR;
36
- log(`PKG_CACHE_PATH=${CACHE_DIR}`);
37
-
38
- log('Starting pkg...');
39
- return new Promise((resolve) => {
40
- const child = spawn(process.execPath, [PKG_BIN, '.', '--targets', TARGET, '--output', OUTPUT], {
41
- cwd: path.resolve(__dirname, '..'),
42
- env,
43
- stdio: 'inherit'
44
- });
45
- child.on('exit', (code) => process.exit(code || 0));
46
- child.on('error', (e) => { log('Spawn error: ' + e.message); process.exit(1); });
47
- });
48
- }
49
-
50
- main().catch(e => { console.error('[pkg-build] Fatal:', e.message); process.exit(1); });
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); });