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.
- package/docs/ROADMAP-NEXT.md +5 -5
- package/package.json +1 -1
- package/src/agent.js +19 -19
- package/src/audit.js +2 -2
- package/src/batch.js +7 -7
- package/src/cachestats.js +9 -9
- package/src/cli.js +29 -679
- package/src/commands/desktop.js +1 -1
- package/src/commands/key.js +1 -1
- package/src/commands/repl.js +711 -0
- package/src/commands/schedule.js +7 -7
- package/src/commands/skill.js +18 -16
- package/src/commands/sync.js +5 -5
- package/src/commands/update.js +7 -6
- package/src/commands/workspace.js +2 -2
- package/src/compact.js +6 -6
- package/src/config.js +7 -4
- package/src/context.js +11 -11
- package/src/credentials.js +6 -0
- package/src/hooks.js +11 -11
- package/src/mcp-presets.js +5 -5
- package/src/mcp.js +19 -19
- package/src/memory.js +16 -16
- package/src/model-discovery.js +16 -15
- package/src/models.js +4 -4
- package/src/notify.js +2 -2
- package/src/permissions.js +7 -3
- package/src/pricing.js +35 -1
- package/src/prompts.js +1 -1
- package/src/providers/index.js +7 -7
- package/src/providers/openai-compatible.js +8 -8
- package/src/routing.js +5 -5
- package/src/schedule.js +28 -28
- package/src/session-index.js +5 -1
- package/src/session.js +11 -2
- package/src/skill-lib.js +62 -3
- package/src/skill-registry.js +10 -9
- package/src/skills.js +10 -10
- package/src/sync-server.js +86 -3
- package/src/sync.js +53 -20
- package/src/tasks/worker.js +126 -0
- package/src/tasks.js +13 -13
- package/src/titles.js +5 -5
- package/src/tokenizer.js +22 -4
- package/src/tools/bash.js +12 -12
- package/src/tools/fs-tools.js +66 -9
- package/src/tools/index.js +7 -7
- package/src/ui.js +83 -6
- package/src/update.js +8 -8
- package/src/web/attachments.js +1 -1
- package/src/web/routes/api.js +38 -786
- package/src/web/routes/domains/config.js +280 -0
- package/src/web/routes/domains/misc.js +128 -0
- package/src/web/routes/domains/schedule.js +127 -0
- package/src/web/routes/domains/sessions.js +105 -0
- package/src/web/routes/domains/skills.js +79 -0
- package/src/web/routes/domains/sync.js +101 -0
- package/src/web/routes/domains/workspace.js +113 -0
- package/src/web/server.js +29 -14
- package/src/web/web-io.js +54 -12
- 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);
|