mingdao-harness 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/docs/ROADMAP-NEXT.md +5 -5
  2. package/package.json +1 -1
  3. package/src/agent.js +19 -19
  4. package/src/audit.js +2 -2
  5. package/src/batch.js +7 -7
  6. package/src/cachestats.js +9 -9
  7. package/src/cli.js +29 -679
  8. package/src/commands/desktop.js +1 -1
  9. package/src/commands/key.js +1 -1
  10. package/src/commands/repl.js +711 -0
  11. package/src/commands/schedule.js +7 -7
  12. package/src/commands/skill.js +18 -16
  13. package/src/commands/sync.js +5 -5
  14. package/src/commands/update.js +7 -6
  15. package/src/commands/workspace.js +2 -2
  16. package/src/compact.js +6 -6
  17. package/src/config.js +7 -4
  18. package/src/context.js +11 -11
  19. package/src/credentials.js +6 -0
  20. package/src/hooks.js +11 -11
  21. package/src/mcp-presets.js +5 -5
  22. package/src/mcp.js +19 -19
  23. package/src/memory.js +16 -16
  24. package/src/model-discovery.js +16 -15
  25. package/src/models.js +4 -4
  26. package/src/notify.js +2 -2
  27. package/src/permissions.js +7 -3
  28. package/src/pricing.js +35 -1
  29. package/src/prompts.js +1 -1
  30. package/src/providers/index.js +7 -7
  31. package/src/providers/openai-compatible.js +8 -8
  32. package/src/routing.js +5 -5
  33. package/src/schedule.js +28 -28
  34. package/src/session-index.js +5 -1
  35. package/src/session.js +11 -2
  36. package/src/skill-lib.js +62 -3
  37. package/src/skill-registry.js +10 -9
  38. package/src/skills.js +10 -10
  39. package/src/sync-server.js +86 -3
  40. package/src/sync.js +53 -20
  41. package/src/tasks/worker.js +126 -0
  42. package/src/tasks.js +13 -13
  43. package/src/titles.js +5 -5
  44. package/src/tokenizer.js +22 -4
  45. package/src/tools/bash.js +12 -12
  46. package/src/tools/fs-tools.js +66 -9
  47. package/src/tools/index.js +7 -7
  48. package/src/ui.js +83 -6
  49. package/src/update.js +8 -8
  50. package/src/web/attachments.js +1 -1
  51. package/src/web/routes/api.js +38 -786
  52. package/src/web/routes/domains/config.js +280 -0
  53. package/src/web/routes/domains/misc.js +128 -0
  54. package/src/web/routes/domains/schedule.js +127 -0
  55. package/src/web/routes/domains/sessions.js +105 -0
  56. package/src/web/routes/domains/skills.js +79 -0
  57. package/src/web/routes/domains/sync.js +101 -0
  58. package/src/web/routes/domains/workspace.js +113 -0
  59. package/src/web/server.js +29 -14
  60. package/src/web/web-io.js +54 -12
  61. package/src/workspace.js +14 -14
@@ -0,0 +1,126 @@
1
+ // 后台任务 worker(Phase C C2):独立进程执行一轮任务并写状态文件。
2
+ // 由 cli.js 的内部入口 `mingdao run-worker <id> ...` 启动(startTask 拉起的子进程)。
3
+ // 无交互:ask 权限降级 readonly;支持避峰顺延、MCP、自动标题、费用入账、完成通知与自动同步。
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { loadConfig, ensureHome } from '../config.js';
7
+ import { createProvider, resolveProviderConfig, helperProvider } from '../providers/index.js';
8
+ import { startMcpServers } from '../mcp.js';
9
+ import { patchTask } from '../tasks.js';
10
+ import { notifyTaskDone } from '../notify.js';
11
+ import { createAgent } from '../agent.js';
12
+ import { createPermission } from '../permissions.js';
13
+ import { buildSystemPrompt } from '../prompts.js';
14
+ import { createSession, appendMessages, rewriteSession } from '../session.js';
15
+ import { recordUsage } from '../cachestats.js';
16
+ import { maybeAutoSync } from '../sync.js';
17
+ import { isPeakHour, deferToOffpeak } from '../pricing.js';
18
+ import { createIO } from '../ui.js';
19
+ import { generateTitle, renameSessionFile, titleModel } from '../titles.js';
20
+
21
+ /**
22
+ * 执行一轮后台任务:加载配置 → 建 agent → 跑回合 → 回写状态文件。
23
+ * @param {string} id 任务 id
24
+ * @param {string} question 任务内容
25
+ * @param {{permission?: any, model?: any, offpeak?: any}} opts
26
+ */
27
+ export async function runWorkerTask(id, question, { permission, model, offpeak }) {
28
+ const home = ensureHome();
29
+ const finish = (/** @type {any} */ patch) => patchTask(home, id, patch);
30
+ let mcpFacade = null;
31
+ let cfg = null; // 提到 try 外:catch 分支也要读 cfg.notify
32
+ try {
33
+ cfg = loadConfig();
34
+ if (!cfg) throw new Error('未初始化配置,请先运行 mingdao init');
35
+ const modelName = model || cfg.model || 'deepseek-v4-flash';
36
+ const pc = resolveProviderConfig(cfg, modelName);
37
+ if (!pc.apiKey) throw new Error(`模型 ${modelName} 没有可用 API Key`);
38
+ const workingDir = process.cwd();
39
+ // 后台无交互:ask 权限降级为 readonly 并注明(需要写权限请 mingdao run --permission auto)
40
+ let perm = permission || cfg.permission || 'ask';
41
+ let note = '';
42
+ if (perm === 'ask') {
43
+ perm = 'readonly';
44
+ note = 'ask 权限下后台任务按只读执行';
45
+ }
46
+ // 避峰(评估 A2/Kimi P-1):高峰时段(北京工作日 9:00–12:00、14:00–18:00)
47
+ // 自动顺延到最近闲时起点(12:00 / 18:00)执行,输入价省 50%
48
+ if (offpeak && isPeakHour(new Date())) {
49
+ const defer = deferToOffpeak(new Date());
50
+ finish({ note: `避峰等待至北京时间 ${defer.toISOString().slice(11, 16)}(闲时起执行,输入价省 50%)` });
51
+ await new Promise((r) => setTimeout(r, defer.getTime() - Date.now() + 2000));
52
+ finish({ note: '' });
53
+ }
54
+ const provider = await createProvider(cfg, modelName);
55
+ const io = createIO({ quiet: true });
56
+ const permissionObj = createPermission(perm, io);
57
+ /** @type {any} */
58
+ let mcpManager = null;
59
+ if (cfg.mcpServers && Object.keys(cfg.mcpServers).length) {
60
+ mcpFacade = {
61
+ toolSchemas: () => (mcpManager ? mcpManager.toolSchemas() : []),
62
+ call: (/** @type {any} */ n, /** @type {any} */ a) => (mcpManager ? mcpManager.call(n, a) : Promise.reject(new Error('MCP 未就绪'))),
63
+ isReadonly: (/** @type {any} */ n) => (mcpManager ? mcpManager.isReadonly(n) : false),
64
+ stop: () => {
65
+ if (mcpManager) mcpManager.stop();
66
+ },
67
+ };
68
+ startMcpServers(cfg.mcpServers, workingDir).then((m) => (mcpManager = m)).catch(() => {});
69
+ }
70
+ const sessionRef = /** @type {{name: any}} */ ({ name: null });
71
+ let persistedCount = 0;
72
+ const agent = createAgent({
73
+ provider,
74
+ permission: permissionObj,
75
+ io,
76
+ modelName,
77
+ workingDir,
78
+ cfg,
79
+ mcp: mcpFacade || undefined,
80
+ sessionRef,
81
+ onCompact: (/** @type {any} */ msgs) => {
82
+ rewriteSession(session.file, msgs);
83
+ persistedCount = msgs.length;
84
+ },
85
+ });
86
+ const session = createSession(home);
87
+ sessionRef.name = path.basename(session.file);
88
+ const messages = [
89
+ { role: 'system', content: buildSystemPrompt({ modelName, workingDir }) },
90
+ { role: 'user', content: question },
91
+ ];
92
+ appendMessages(session.file, messages);
93
+ persistedCount = messages.length;
94
+ const t0 = Date.now();
95
+ const res = await agent.runTurn(messages);
96
+ appendMessages(session.file, messages.slice(persistedCount));
97
+ if (cfg.autoTitle !== false && res.text) {
98
+ try {
99
+ const tModel = titleModel(cfg, modelName);
100
+ const title = await generateTitle(await helperProvider(cfg, tModel, provider), tModel, question);
101
+ if (title) renameSessionFile(fs, path, home, session, title);
102
+ } catch {}
103
+ }
104
+ const finalStatus = res.truncated ? 'failed' : res.aborted ? 'killed' : 'done';
105
+ recordUsage(res.perf?.usedModel || modelName, res.usage, /** @type {any} */ (res.perf));
106
+ finish({
107
+ status: finalStatus,
108
+ text: (res.text || '').slice(0, 2000),
109
+ usage: res.usage,
110
+ durationMs: Date.now() - t0,
111
+ session: path.basename(session.file),
112
+ note,
113
+ });
114
+ if (cfg.notify !== false && !process.env.MINGDAO_TASK_QUIET_NOTIFY) notifyTaskDone(question, finalStatus === 'killed' ? 'failed' : finalStatus);
115
+ try {
116
+ await maybeAutoSync();
117
+ } catch {}
118
+ process.exitCode = res.truncated ? 1 : 0;
119
+ } catch (/** @type {any} */ err) {
120
+ finish({ status: 'failed', error: String(err?.message || err) });
121
+ if (cfg?.notify !== false && !process.env.MINGDAO_TASK_QUIET_NOTIFY) notifyTaskDone(question, 'failed');
122
+ process.exitCode = 2;
123
+ } finally {
124
+ if (mcpFacade) mcpFacade.stop();
125
+ }
126
+ }
package/src/tasks.js CHANGED
@@ -11,11 +11,11 @@ import { atomicWriteFileSync, withFileLockSync } from './atomic-write.js';
11
11
 
12
12
  const CLI_PATH = fileURLToPath(new URL('./cli.js', import.meta.url));
13
13
 
14
- export function tasksDir(home) {
14
+ export function tasksDir(/** @type {any} */ home) {
15
15
  return path.join(home, 'tasks');
16
16
  }
17
17
 
18
- export function listTasks(home) {
18
+ export function listTasks(/** @type {any} */ home) {
19
19
  const dir = tasksDir(home);
20
20
  let files;
21
21
  try {
@@ -33,7 +33,7 @@ export function listTasks(home) {
33
33
  return out.sort((a, b) => b.startedAt - a.startedAt);
34
34
  }
35
35
 
36
- export function readTask(home, id) {
36
+ export function readTask(/** @type {any} */ home, /** @type {any} */ id) {
37
37
  if (!isValidTaskId(id)) return null; // 防 id 路径穿越(id 直接拼进文件路径)
38
38
  try {
39
39
  return JSON.parse(fs.readFileSync(path.join(tasksDir(home), id + '.json'), 'utf8'));
@@ -42,18 +42,18 @@ export function readTask(home, id) {
42
42
  }
43
43
  }
44
44
 
45
- export function writeTask(home, task) {
45
+ export function writeTask(/** @type {any} */ home, /** @type {any} */ task) {
46
46
  fs.mkdirSync(tasksDir(home), { recursive: true });
47
47
  // 原子写:先临时文件再改名,避免崩溃留下半截 JSON
48
48
  const target = path.join(tasksDir(home), task.id + '.json');
49
49
  atomicWriteFileSync(target, JSON.stringify(task, null, 2) + '\n'); // 质检 H4:tmp 名含 pid+随机
50
50
  }
51
51
 
52
- export function isValidTaskId(id) {
52
+ export function isValidTaskId(/** @type {any} */ id) {
53
53
  return typeof id === 'string' && /^[a-z0-9]+$/.test(id) && id.length >= 4 && id.length <= 40;
54
54
  }
55
55
 
56
- export function startTask(home, question, { permission, model, cwd, offpeak, quietNotify } = /** @type {any} */ ({})) {
56
+ export function startTask(/** @type {any} */ home, /** @type {any} */ question, { permission, model, cwd, offpeak, quietNotify } = /** @type {any} */ ({})) {
57
57
  const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6) + process.pid.toString(36);
58
58
  const task = {
59
59
  id,
@@ -83,16 +83,16 @@ export function startTask(home, question, { permission, model, cwd, offpeak, qui
83
83
  // 质检 M12:spawn 失败(ENOENT/ARG_MAX)必须有 error 监听,否则未捕获事件直接崩进程
84
84
  child.on('error', (err) => {
85
85
  try {
86
- patchTask(home, id, { status: 'failed', error: `worker 启动失败:${err?.message || err}`, durationMs: Date.now() - task.startedAt });
86
+ patchTask(home, id, { status: 'failed', error: `worker 启动失败:${(/** @type {any} */ (err))?.message || err}`, durationMs: Date.now() - task.startedAt });
87
87
  } catch {}
88
88
  });
89
- task.pid = child.pid;
89
+ task.pid = /** @type {any} */ (child.pid);
90
90
  writeTask(home, task);
91
91
  child.unref();
92
92
  return task;
93
93
  }
94
94
 
95
- export function patchTask(home, id, patch) {
95
+ export function patchTask(/** @type {any} */ home, /** @type {any} */ id, /** @type {any} */ patch) {
96
96
  // 质检 H3:读-改-写加锁(worker finish 与 CLI kill 互斥,防 killed/done 互相覆盖)
97
97
  return withFileLockSync(path.join(tasksDir(home), '.lock'), () => {
98
98
  const t = readTask(home, id);
@@ -101,12 +101,12 @@ export function patchTask(home, id, patch) {
101
101
  });
102
102
  }
103
103
 
104
- export function killTask(home, id) {
104
+ export function killTask(/** @type {any} */ home, /** @type {any} */ id) {
105
105
  if (!isValidTaskId(id)) return false;
106
106
  // 质检 H3:读-改-写加锁(与 worker 自身的状态写互斥)
107
107
  return withFileLockSync(path.join(tasksDir(home), '.lock'), () => killTaskInner(home, id));
108
108
  }
109
- function killTaskInner(home, id) {
109
+ function killTaskInner(/** @type {any} */ home, /** @type {any} */ id) {
110
110
  const t = readTask(home, id);
111
111
  if (!t) return false;
112
112
  if (t.status === 'running' && t.pid) {
@@ -132,8 +132,8 @@ function killTaskInner(home, id) {
132
132
 
133
133
  const MARK = { running: '▶', done: '✓', failed: '✖', killed: '■' };
134
134
 
135
- export function formatTaskRow(t) {
136
- const mark = MARK[t.status] || '?';
135
+ export function formatTaskRow(/** @type {any} */ t) {
136
+ const mark = /** @type {any} */ (MARK)[t.status] || '?';
137
137
  const elapsed =
138
138
  t.status === 'running'
139
139
  ? relativeTime(t.startedAt).replace('前', '')
package/src/titles.js CHANGED
@@ -4,21 +4,21 @@
4
4
  import { modelPreset } from './models.js';
5
5
  import { routingConfig } from './routing.js';
6
6
 
7
- export function titleModel(cfg, currentModel) {
7
+ export function titleModel(/** @type {any} */ cfg, /** @type {any} */ currentModel) {
8
8
  const rc = routingConfig(cfg);
9
9
  if (rc) return rc.executor;
10
10
  // 路由关闭:辅助调用用当前模型(评估 P2-2:硬编码 flash 会在自定义网关 404 静默失败)
11
11
  return currentModel;
12
12
  }
13
13
 
14
- function cleanTitle(s) {
14
+ function cleanTitle(/** @type {any} */ s) {
15
15
  return String(s ?? '')
16
16
  .trim()
17
17
  .replace(/["「」『』"'`*#\n\r]/g, '')
18
18
  .slice(0, 20);
19
19
  }
20
20
 
21
- export async function generateTitle(provider, model, firstUserText, { timeoutMs = 10000 } = {}) {
21
+ export async function generateTitle(/** @type {any} */ provider, /** @type {any} */ model, /** @type {any} */ firstUserText, { timeoutMs = 10000 } = {}) {
22
22
  // 硬超时护栏(审计:「第二问无反应」根因):标题生成发生在回合收尾阶段,若网关/网络层
23
23
  // 挂起(不抛错),回合永远无法 complete——前端停在生成态、后续发送全部静默。20s 兜底:
24
24
  // 超时放弃标题(会话仍以时间戳命名),绝不阻塞下一问。
@@ -65,13 +65,13 @@ export async function generateTitle(provider, model, firstUserText, { timeoutMs
65
65
  }
66
66
  }
67
67
 
68
- export function sanitizeTitle(s) {
68
+ export function sanitizeTitle(/** @type {any} */ s) {
69
69
  const clean = String(s).replace(/[^\w\u4e00-\u9fa5 .-]/g, '_').slice(0, 40).trim();
70
70
  return clean || '会话';
71
71
  }
72
72
 
73
73
  // 重命名会话文件(与 /title 一致:确保存在、同名加随机后缀)
74
- export function renameSessionFile(fs, pathMod, home, session, title) {
74
+ export function renameSessionFile(/** @type {any} */ fs, /** @type {any} */ pathMod, /** @type {any} */ home, /** @type {any} */ session, /** @type {any} */ title) {
75
75
  const safe = sanitizeTitle(title);
76
76
  let newFile = pathMod.join(home, 'sessions', safe + '.jsonl');
77
77
  try {
package/src/tokenizer.js CHANGED
@@ -22,6 +22,7 @@ const SPLIT_RES = [
22
22
  /[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~][A-Za-z]+|[^\r\n\p{L}\p{P}\p{S}]?[\p{L}\p{M}]+| ?[\p{P}\p{S}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+/gu,
23
23
  ];
24
24
 
25
+ /** @param {string} text */
25
26
  function pretokenize(text) {
26
27
  let pieces = [text];
27
28
  for (const re of SPLIT_RES) {
@@ -48,6 +49,7 @@ function pretokenize(text) {
48
49
  // 汉字会退化为逐字节计数(如「的」被计为 3 tokens 而非 1)。
49
50
  const BYTE_TO_UNICODE = (() => {
50
51
  const m = new Array(256);
52
+ /** @param {number} b */
51
53
  const self = (b) => (b >= 0x21 && b <= 0x7e) || (b >= 0xa1 && b <= 0xac) || (b >= 0xae);
52
54
  let n = 0;
53
55
  for (let b = 0; b < 256; b++) {
@@ -60,11 +62,14 @@ const BYTE_TO_UNICODE = (() => {
60
62
  return m;
61
63
  })();
62
64
 
65
+ /** @param {string} s */
63
66
  function escapeRe(s) {
64
67
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
65
68
  }
66
69
 
70
+ /** @type {any} */
67
71
  let data = null;
72
+ /** @type {any} */
68
73
  let loadError = null;
69
74
 
70
75
  function loadData() {
@@ -78,7 +83,7 @@ function loadData() {
78
83
  const [a, b] = parsed.merges[i];
79
84
  mergeRank.set(a + '\u0001' + b, i);
80
85
  }
81
- const added = (parsed.added || []).filter(Boolean).sort((a, b) => b.length - a.length);
86
+ const added = (parsed.added || []).filter(Boolean).sort((/** @type {any} */ a, /** @type {any} */ b) => b.length - a.length);
82
87
  // 全部 added token 合并成单个正则(按长度降序排列 → 左起最长优先,与逐 token startsWith 语义一致),
83
88
  // 一次 matchAll 定位所有特殊 token,把 O(文本长 × 818 个 indexOf) 降到 O(n)
84
89
  const addedRe = added.length ? new RegExp(added.map(escapeRe).join('|'), 'gu') : null;
@@ -89,6 +94,7 @@ function loadData() {
89
94
  return data;
90
95
  }
91
96
 
97
+ /** @param {any} modelName */
92
98
  export function isTokenizable(modelName) {
93
99
  if (typeof modelName !== 'string') return false;
94
100
  if (modelName.startsWith('deepseek')) return true;
@@ -124,15 +130,17 @@ const CJK_RANGES = [
124
130
  [0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xf900, 0xfaff], // CJK 扩展/基本区/兼容
125
131
  [0x3040, 0x30ff], [0xac00, 0xd7af], // 假名 / 谚文
126
132
  ];
133
+ /** @param {number} code */
127
134
  const isCjk = (code) => CJK_RANGES.some(([lo, hi]) => code >= lo && code <= hi);
128
135
 
136
+ /** @param {any} text */
129
137
  export function heuristicTokens(text) {
130
138
  if (!text) return 0;
131
139
  let ascii = 0;
132
140
  let cjk = 0;
133
141
  let other = 0;
134
142
  for (const ch of String(text)) {
135
- const code = ch.codePointAt(0);
143
+ const code = /** @type {number} */ (ch.codePointAt(0));
136
144
  if (code < 128) ascii += 1;
137
145
  else if (isCjk(code)) cjk += 1;
138
146
  else other += code > 0xffff ? 2 : 1; // 审计 B5:增补平面 emoji 按 2 token 保守计
@@ -144,6 +152,7 @@ export function heuristicTokens(text) {
144
152
 
145
153
  // 单个预分词片段的 BPE 计数(tiktoken 语义:优先合并 rank 最小的对,同 rank 取最左)。
146
154
  // 惰性最小堆实现(O(n log n)):大文本(中文长文/工具输出)不再 O(n²) 全表扫描。
155
+ /** @param {any} piece @param {any} d */
147
156
  function countPiece(piece, d) {
148
157
  const bytes = Buffer.from(piece, 'utf8');
149
158
  const syms = [];
@@ -151,17 +160,21 @@ function countPiece(piece, d) {
151
160
  if (syms.length <= 1) return syms.length;
152
161
 
153
162
  const alive = new Uint8Array(syms.length).fill(1);
163
+ /** @param {number} i */
154
164
  const leftOf = (i) => {
155
165
  for (let j = i - 1; j >= 0; j--) if (alive[j]) return j;
156
166
  return -1;
157
167
  };
168
+ /** @param {number} i */
158
169
  const rightOf = (i) => {
159
170
  for (let j = i + 1; j < syms.length; j++) if (alive[j]) return j;
160
171
  return -1;
161
172
  };
162
173
 
163
174
  // 最小堆(rank, leftIndex);过期条目惰性丢弃/重推
175
+ /** @type {any[]} */
164
176
  const heap = [];
177
+ /** @param {number} rank @param {number} idx */
165
178
  const push = (rank, idx) => {
166
179
  heap.push([rank, idx]);
167
180
  let c = heap.length - 1;
@@ -182,6 +195,7 @@ function countPiece(piece, d) {
182
195
  const l = c * 2 + 1;
183
196
  const r = l + 1;
184
197
  let m = c;
198
+ /** @param {number} x */
185
199
  const better = (x) => heap[x][0] < heap[m][0] || (heap[x][0] === heap[m][0] && heap[x][1] < heap[m][1]);
186
200
  if (l < heap.length && better(l)) m = l;
187
201
  if (r < heap.length && better(r)) m = r;
@@ -238,6 +252,7 @@ const TOKEN_CACHE_MAX_ENTRIES = 512;
238
252
  const TOKEN_CACHE_MAX_LEN = 50000;
239
253
  const tokenCache = new Map();
240
254
 
255
+ /** @param {string} s */
241
256
  function countDeepseek(s) {
242
257
  const d = loadData();
243
258
  if (!d) return heuristicTokens(s); // 词表缺失时优雅回退
@@ -254,12 +269,14 @@ function countDeepseek(s) {
254
269
  return total;
255
270
  }
256
271
 
272
+ /** @param {any} piece @param {any} d */
257
273
  function countGap(piece, d) {
258
274
  let total = 0;
259
275
  for (const m of pretokenize(piece)) total += countPiece(m, d);
260
276
  return total;
261
277
  }
262
278
 
279
+ /** @param {any} text @param {any} modelName */
263
280
  export function countTokens(text, modelName) {
264
281
  if (!text) return 0;
265
282
  const s = String(text);
@@ -279,9 +296,10 @@ export function countTokens(text, modelName) {
279
296
  }
280
297
 
281
298
  // 供上下文预算使用的计数器工厂
299
+ /** @param {any} modelName */
282
300
  export function makeTokenCounter(modelName) {
283
301
  if (isTokenizable(modelName)) {
284
- return (text) => countTokens(text, modelName);
302
+ return (/** @type {any} */ text) => countTokens(text, modelName);
285
303
  }
286
- return (text) => heuristicTokens(text);
304
+ return (/** @type {any} */ text) => heuristicTokens(text);
287
305
  }
package/src/tools/bash.js CHANGED
@@ -15,22 +15,22 @@ const MAX_TIMEOUT_SECONDS = 600;
15
15
  // 整体关闭(回到完全透传)。
16
16
  const SENSITIVE_ENV_PAIR = /(api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key)/i;
17
17
  const SENSITIVE_ENV_SEGMENT = /(^|_)(token|secret|password|passwd|credential|authorization|auth)(_|$)/i;
18
- const isSensitiveEnv = (k) => SENSITIVE_ENV_PAIR.test(k) || SENSITIVE_ENV_SEGMENT.test(k);
18
+ const isSensitiveEnv = (/** @type {any} */ k) => SENSITIVE_ENV_PAIR.test(k) || SENSITIVE_ENV_SEGMENT.test(k);
19
19
 
20
- function buildChildEnv(ctx, filterSensitive) {
20
+ function buildChildEnv(/** @type {any} */ ctx, /** @type {any} */ filterSensitive) {
21
21
  if (!filterSensitive) return process.env;
22
22
  const keep = new Set((ctx?.cfg?.bashEnvKeep || []).map(String));
23
23
  const env = {};
24
24
  for (const [k, v] of Object.entries(process.env)) {
25
- if (!isSensitiveEnv(k) || keep.has(k)) env[k] = v;
25
+ if (!isSensitiveEnv(k) || keep.has(k)) (/** @type {any} */ (env))[k] = v;
26
26
  }
27
27
  return env;
28
28
  }
29
29
 
30
- let sandboxSupport = null;
30
+ let /** @type {any} */ sandboxSupport = null;
31
31
 
32
32
  export function detectSandbox() {
33
- if (sandboxSupport !== null) return sandboxSupport;
33
+ if (/** @type {any} */ sandboxSupport !== null) return /** @type {any} */ sandboxSupport;
34
34
  sandboxSupport = 'none';
35
35
  if (process.platform === 'linux') {
36
36
  try {
@@ -46,12 +46,12 @@ export function detectSandbox() {
46
46
  // 输出折叠(审计 MiniMax §3.3-E / v0.1.48 P1-G):模型回填的 bash 输出先折叠再截断——
47
47
  // 1) 剥离 ANSI 转义序列(CSI/OSC);2) 连续重复行(>3 行相同)折叠为「首行 + 重复标记」。
48
48
  // npm install 类输出通常 30-50KB → 折叠后 5-10KB,单次工具回填省 60-70% prompt token。
49
- function stripAnsi(s) {
49
+ function stripAnsi(/** @type {any} */ s) {
50
50
  return s
51
51
  .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
52
52
  .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
53
53
  }
54
- function foldRepeats(s) {
54
+ function foldRepeats(/** @type {any} */ s) {
55
55
  const lines = s.split('\n');
56
56
  const out = [];
57
57
  let i = 0;
@@ -69,12 +69,12 @@ function foldRepeats(s) {
69
69
  }
70
70
  return out.join('\n');
71
71
  }
72
- function tail(s, n) {
72
+ function tail(/** @type {any} */ s, /** @type {any} */ n) {
73
73
  const folded = foldRepeats(stripAnsi(s));
74
74
  return folded.length > n ? `…[输出过长,已截断头部]\n${folded.slice(-n)}` : folded;
75
75
  }
76
76
 
77
- export function runBash(args, ctx) {
77
+ export function runBash(/** @type {any} */ args, /** @type {any} */ ctx) {
78
78
  const command = String(args.command ?? '');
79
79
  if (!command.trim()) return { ok: false, error: 'command 参数为空。' };
80
80
  const timeoutSec = Math.min(Number(args.timeout) || 120, MAX_TIMEOUT_SECONDS);
@@ -125,13 +125,13 @@ export function runBash(args, ctx) {
125
125
  let done = false;
126
126
  let timedOut = false;
127
127
  // 输出增量截断:超长输出只保留尾部,避免内存无限累积
128
- const cap = (s, d) => {
128
+ const cap = (/** @type {any} */ s, /** @type {any} */ d) => {
129
129
  const t = s + d;
130
130
  return t.length > MAX_OUTPUT * 2 ? t.slice(-MAX_OUTPUT * 2) : t;
131
131
  };
132
- const killGroup = (sig) => {
132
+ const killGroup = (/** @type {any} */ sig) => {
133
133
  try {
134
- process.kill(-child.pid, sig);
134
+ process.kill(-(/** @type {any} */ (child)).pid, sig);
135
135
  } catch {
136
136
  try {
137
137
  child.kill(sig);