mingdao-harness 0.2.4 → 0.2.6

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 (66) hide show
  1. package/docs/EVALUATION-ROUND2.md +58 -0
  2. package/docs/ROADMAP-NEXT.md +5 -5
  3. package/package.json +2 -2
  4. package/src/agent.js +74 -36
  5. package/src/atomic-write.js +8 -2
  6. package/src/audit.js +2 -2
  7. package/src/batch.js +7 -7
  8. package/src/cachestats.js +9 -9
  9. package/src/cli.js +42 -688
  10. package/src/commands/desktop.js +1 -1
  11. package/src/commands/key.js +1 -1
  12. package/src/commands/repl.js +711 -0
  13. package/src/commands/schedule.js +7 -7
  14. package/src/commands/skill.js +18 -16
  15. package/src/commands/sync.js +5 -5
  16. package/src/commands/update.js +7 -6
  17. package/src/commands/workspace.js +2 -2
  18. package/src/compact.js +6 -6
  19. package/src/config.js +7 -4
  20. package/src/context.js +37 -21
  21. package/src/cost-guard.js +14 -6
  22. package/src/credentials.js +6 -0
  23. package/src/hooks.js +11 -11
  24. package/src/mcp-presets.js +5 -5
  25. package/src/mcp.js +19 -19
  26. package/src/memory.js +16 -16
  27. package/src/model-discovery.js +16 -15
  28. package/src/models.js +4 -4
  29. package/src/notify.js +2 -2
  30. package/src/permissions.js +7 -3
  31. package/src/pricing.js +35 -1
  32. package/src/prompts.js +1 -1
  33. package/src/providers/index.js +7 -7
  34. package/src/providers/openai-compatible.js +13 -11
  35. package/src/routing.js +8 -8
  36. package/src/schedule.js +30 -30
  37. package/src/session-index.js +5 -1
  38. package/src/session.js +11 -2
  39. package/src/skill-lib.js +62 -3
  40. package/src/skill-registry.js +10 -9
  41. package/src/skills.js +14 -12
  42. package/src/sync-server.js +86 -3
  43. package/src/sync.js +53 -20
  44. package/src/tasks/worker.js +126 -0
  45. package/src/tasks.js +14 -14
  46. package/src/titles.js +5 -5
  47. package/src/tokenizer.js +23 -5
  48. package/src/tools/bash.js +12 -12
  49. package/src/tools/fs-tools.js +66 -9
  50. package/src/tools/index.js +7 -7
  51. package/src/ui.js +83 -6
  52. package/src/update.js +8 -8
  53. package/src/web/app.js +16 -5
  54. package/src/web/attachments.js +1 -1
  55. package/src/web/index.html +21 -1
  56. package/src/web/routes/api.js +38 -786
  57. package/src/web/routes/domains/config.js +280 -0
  58. package/src/web/routes/domains/misc.js +129 -0
  59. package/src/web/routes/domains/schedule.js +127 -0
  60. package/src/web/routes/domains/sessions.js +105 -0
  61. package/src/web/routes/domains/skills.js +79 -0
  62. package/src/web/routes/domains/sync.js +101 -0
  63. package/src/web/routes/domains/workspace.js +119 -0
  64. package/src/web/server.js +73 -31
  65. package/src/web/web-io.js +54 -12
  66. package/src/workspace.js +14 -14
@@ -16,7 +16,7 @@ import { modelPreset, providerPreset } from '../models.js';
16
16
  import { mingdaoHome } from '../config.js';
17
17
  import { resolveApiKey } from '../credentials.js';
18
18
 
19
- export function resolveProviderConfig(cfg, modelName) {
19
+ export function resolveProviderConfig(/** @type {any} */ cfg, /** @type {any} */ modelName) {
20
20
  // 自定义模型(config.customModels,WebUI 可增删改):优先于内置预设
21
21
  const cm = cfg?.customModels?.[modelName];
22
22
  if (cm) {
@@ -47,16 +47,16 @@ export function resolveProviderConfig(cfg, modelName) {
47
47
  };
48
48
  }
49
49
 
50
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
+ const sleep = (/** @type {any} */ ms) => new Promise((r) => setTimeout(r, ms));
51
51
 
52
- function isTransient(err) {
52
+ function isTransient(/** @type {any} */ err) {
53
53
  const status = err?.status;
54
54
  if (status === 429 || (status >= 500 && status <= 504)) return true;
55
55
  // 注意:不含 abort —— 用户 Ctrl+C 主动中断不应触发重试
56
56
  return /timeout|超时|ECONNRESET|fetch failed/i.test(String(err?.message || ''));
57
57
  }
58
58
 
59
- export async function createProvider(cfg, modelName, { timeoutMs = 300000, retries = 2 } = {}) {
59
+ export async function createProvider(/** @type {any} */ cfg, /** @type {any} */ modelName, { timeoutMs = 300000, retries = 2 } = {}) {
60
60
  const pc = resolveProviderConfig(cfg, modelName);
61
61
 
62
62
  // 自定义 Provider 模块优先(仅普通自定义端点;custom:<模型名> 走 OpenAI 兼容直连)
@@ -77,7 +77,7 @@ export async function createProvider(cfg, modelName, { timeoutMs = 300000, retri
77
77
  return {
78
78
  name: pc.name,
79
79
  config: pc,
80
- async chat(opts) {
80
+ async chat(/** @type {any} */ opts) {
81
81
  let attempt = 0;
82
82
  for (;;) {
83
83
  const ac = new AbortController();
@@ -105,7 +105,7 @@ export async function createProvider(cfg, modelName, { timeoutMs = 300000, retri
105
105
  attempt += 1;
106
106
  // 指数退避 + 尊重 Retry-After(评估 P3-1):基础 1s/2s,服务端指定时取其值(封顶 30s)
107
107
  let backoff = 1000 * attempt;
108
- const ra = Number(err?.headers?.get?.('retry-after'));
108
+ const ra = Number((/** @type {any} */ (err))?.headers?.get?.('retry-after'));
109
109
  if (Number.isFinite(ra) && ra > 0) backoff = Math.max(backoff, ra * 1000);
110
110
  backoff = Math.min(backoff, 30000);
111
111
  await sleep(backoff);
@@ -121,7 +121,7 @@ export async function createProvider(cfg, modelName, { timeoutMs = 300000, retri
121
121
  // 辅助调用(标题生成/记忆提取等)的 provider 解析(评估 P2-2):辅助模型与当前模型
122
122
  // 分属不同服务商时单独创建 provider——避免拿 deepseek-v4-flash 模型名去自定义网关请求
123
123
  // 而 404 被静默吞掉(标题缺失、记忆永不沉淀且零感知)。
124
- export async function helperProvider(cfg, helperModel, currentProvider) {
124
+ export async function helperProvider(/** @type {any} */ cfg, /** @type {any} */ helperModel, /** @type {any} */ currentProvider) {
125
125
  try {
126
126
  const curName = String(currentProvider?.name || '');
127
127
  const helperName = String(resolveProviderConfig(cfg, helperModel)?.name || '');
@@ -6,9 +6,9 @@
6
6
  * @typedef {Error & { status?: number, headers?: Headers }} ApiError
7
7
  */
8
8
 
9
- export async function chat({ baseUrl, apiKey, model, messages, tools, temperature, maxTokens, signal, onDelta, includeUsage = true, responseFormat, reasoningEffort }) {
9
+ export async function chat(/** @type {any} */ { baseUrl, apiKey, model, messages, tools, temperature, maxTokens, signal, onDelta, includeUsage = true, responseFormat, reasoningEffort }) {
10
10
  const url = String(baseUrl).replace(/\/+$/, '') + '/chat/completions';
11
- const payload = { model, messages };
11
+ const payload = /** @type {Record<string, any>} */ ({ model, messages });
12
12
  if (temperature != null) payload.temperature = temperature;
13
13
  if (tools && tools.length) {
14
14
  payload.tools = tools;
@@ -16,7 +16,8 @@ export async function chat({ baseUrl, apiKey, model, messages, tools, temperatur
16
16
  }
17
17
  if (maxTokens) payload.max_tokens = maxTokens;
18
18
  if (responseFormat) payload.response_format = responseFormat;
19
- if (reasoningEffort) payload.reasoning_effort = reasoningEffort; // 思考强度(官方 thinking_mode)
19
+ if (reasoningEffort === 'off') payload.thinking = { type: 'disabled' }; // /think off:显式关闭思考(官方 thinking_mode)
20
+ else if (reasoningEffort) payload.reasoning_effort = reasoningEffort; // 思考强度(官方 thinking_mode)
20
21
  payload.stream = true;
21
22
  // 流式响应默认不带 usage,显式请求以便展示 token/费用统计
22
23
  // (部分网关不支持该字段,可在 config.json 设 "includeUsage": false 关闭)
@@ -36,7 +37,7 @@ export async function chat({ baseUrl, apiKey, model, messages, tools, temperatur
36
37
  });
37
38
  } catch (err) {
38
39
  /** @type {ApiError} */
39
- const e = new Error(`网络请求失败:${err?.message || err}`);
40
+ const e = new Error(`网络请求失败:${(/** @type {any} */ (err))?.message || err}`);
40
41
  e.status = 0;
41
42
  throw e;
42
43
  }
@@ -64,7 +65,7 @@ export async function chat({ baseUrl, apiKey, model, messages, tools, temperatur
64
65
  return parseStream(res.body, onDelta);
65
66
  }
66
67
 
67
- export function parseNonStream(json, onDelta) {
68
+ export function parseNonStream(/** @type {any} */ json, /** @type {any} */ onDelta) {
68
69
  const choice = json?.choices?.[0];
69
70
  const msg = choice?.message ?? {};
70
71
  if (msg.content) onDelta?.({ text: msg.content });
@@ -80,7 +81,7 @@ export function parseNonStream(json, onDelta) {
80
81
  }
81
82
 
82
83
  // 解析 SSE 流:处理跨 chunk 断行、增量 content / reasoning_content / tool_calls。
83
- export async function parseStream(body, onDelta) {
84
+ export async function parseStream(/** @type {any} */ body, /** @type {any} */ onDelta) {
84
85
  const reader = body.getReader();
85
86
  const decoder = new TextDecoder();
86
87
  let buf = '';
@@ -90,15 +91,15 @@ export async function parseStream(body, onDelta) {
90
91
  const indexKeys = new Map(); // index -> key(id 只在首片出现,后续分片按 index 找回同一条目)
91
92
  let autoKey = 0;
92
93
  let usage = null;
93
- let finish = null;
94
+ let /** @type {any} */ finish = null;
94
95
 
95
96
  let doneFlag = false;
96
- const handleLine = (lineRaw) => {
97
+ const handleLine = (/** @type {any} */ lineRaw) => {
97
98
  const line = lineRaw.trim();
98
99
  if (!line.startsWith('data:')) return;
99
100
  const data = line.slice(5).trim();
100
101
  if (data === '[DONE]') {
101
- finish = finish || 'stop';
102
+ finish = /** @type {any} */ finish || 'stop';
102
103
  doneFlag = true;
103
104
  return true; // 通知外层终止读取(审计 P2-4:[DONE] 后不再消费残帧、不挂到超时)
104
105
  }
@@ -153,8 +154,9 @@ export async function parseStream(body, onDelta) {
153
154
  if (handleLine(line)) break;
154
155
  }
155
156
  }
156
- // 收尾:残行(网关最后一帧不带换行)与多字节字符冲刷
157
- content += decoder.decode();
157
+ // 收尾:残行(网关最后一帧不带换行)与多字节字符冲刷——decode 残余并入 buf 统一走行解析,
158
+ // 不再直接拼进 content(此前尾字节绕过 doneFlag/行解析,流中途截断时正文混入杂质)
159
+ buf += decoder.decode();
158
160
  if (buf.trim()) handleLine(buf); // [DONE] 后残留 usage 尾帧仍吸收(正文/推理已被 doneFlag 拦截)
159
161
 
160
162
  const toolCalls = [...calls.entries()]
package/src/routing.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import crypto from 'node:crypto';
10
10
  import { modelPreset } from './models.js';
11
11
 
12
- export function routingConfig(cfg) {
12
+ export function routingConfig(/** @type {any} */ cfg) {
13
13
  const r = cfg?.routing;
14
14
  if (!r || r.enabled === false) return null;
15
15
  const planner = r.planner || 'deepseek-v4-pro';
@@ -19,19 +19,19 @@ export function routingConfig(cfg) {
19
19
  }
20
20
 
21
21
  const PLAN_HINTS =
22
- /设计|架构|重构|审查|规划|分析|方案|评估|优化|排查|疑难|报错|怎么修|修复|设计模式|选型|技术债|roadmap|review|design|refactor|plan|architecture|方案设计|评审/;
22
+ /设计|架构|重构|审查|规划|分析|方案|评估|优化|排查|疑难|报错|怎么修|修复|设计模式|选型|技术债|roadmap|review|design|refactor|plan|architecture|方案设计|评审|fix|analyze|analyse|evaluate|audit|optimize|bug|issue|error|performance/i;
23
23
 
24
24
  // 生成类任务(游戏/网页/文档等)需要大输出(planner 32K vs executor 8K),即使短句也路由 planner
25
25
  const GENERATION_HINTS =
26
- /(生成|制作|编写|创建|写|开发|实现|做).{0,30}(游戏|网页|页面|网站|应用|程序|文档|报告|简历|PPT|演示|完整|详细|小工具)|网页版|游戏/;
26
+ /(生成|制作|编写|创建|写|开发|实现|做|make|build|create|generate|write|implement|develop).{0,30}(游戏|网页|页面|网站|应用|程序|文档|报告|简历|PPT|演示|完整|详细|小工具|game|website|page|site|app|program|document|report|resume|ppt|tool)|网页版|游戏|webpage|website/i;
27
27
 
28
- export function heuristicRoute(text, rc) {
28
+ export function heuristicRoute(/** @type {any} */ text, /** @type {any} */ rc) {
29
29
  const s = String(text ?? '');
30
30
  if (GENERATION_HINTS.test(s)) return rc.planner; // 生成类:大输出优先
31
31
  if (PLAN_HINTS.test(s)) {
32
32
  // 规划类关键词优先(审计:此前要求 length>=40,导致「设计一个缓存方案」这类
33
33
  // 13 字短句被误路由到 flash 硬扛——关键词强度优先于文本长度)
34
- return s.length >= 40 || /设计|规划|分析|评估|审查|架构|重构|方案|优化|报错|修复/.test(s) ? rc.planner : null;
34
+ return s.length >= 40 || /设计|规划|分析|评估|审查|架构|重构|方案|优化|报错|修复|fix|analy|review|design|plan|optimiz|refactor|evaluat|audit|error|bug/i.test(s) ? rc.planner : null;
35
35
  }
36
36
  if (s.length <= 60) return rc.executor;
37
37
  return null; // 需要分类器
@@ -41,7 +41,7 @@ export function heuristicRoute(text, rc) {
41
41
  const ROUTE_CACHE_MAX = 100;
42
42
  const routeCache = new Map();
43
43
 
44
- export async function routeTask({ cfg, provider, currentModel, text, sticky = null, sessionStats = null }) {
44
+ export async function routeTask(/** @type {any} */ { cfg, provider, currentModel, text, sticky = null, sessionStats = null }) {
45
45
  const rc = routingConfig(cfg);
46
46
  if (!rc) return { model: currentModel, reason: null };
47
47
  // 当前模型已在路由池外(用户手动指定):不干预
@@ -59,7 +59,7 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
59
59
  // 会话粘滞 + 升级检测(Hermes C2):执行类会话不再逐轮分类(生成类关键词已在启发式拦截),
60
60
  // 但会话内复杂度信号累积(工具步数/截断次数)时允许升级 planner——「开头简单、中途复杂」不再被 flash 硬扛。
61
61
  if (sticky === rc.executor) {
62
- const stats = sessionStats || {};
62
+ const stats = /** @type {any} */ (sessionStats || {});
63
63
  const steps = Number(stats.steps) || 0;
64
64
  const truncated = Number(stats.truncated) || 0;
65
65
  const UPGRADE_STEPS = Number(cfg?.routing?.upgradeSteps) || 10;
@@ -140,7 +140,7 @@ export async function routeTask({ cfg, provider, currentModel, text, sticky = nu
140
140
  }
141
141
 
142
142
  // 子代理模型选择:路由开启时固定 executor
143
- export function subagentModel(cfg, currentModel) {
143
+ export function subagentModel(/** @type {any} */ cfg, /** @type {any} */ currentModel) {
144
144
  const rc = routingConfig(cfg);
145
145
  if (!rc) return currentModel;
146
146
  return modelPreset(rc.executor) ? rc.executor : currentModel;
package/src/schedule.js CHANGED
@@ -11,7 +11,7 @@ import { spawn } from 'node:child_process';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { startTask, readTask, killTask } from './tasks.js';
13
13
 
14
- function isRunningTask(home, taskId) {
14
+ function isRunningTask(/** @type {any} */ home, /** @type {any} */ taskId) {
15
15
  const t = readTask(home, taskId);
16
16
  return Boolean(t && t.status === 'running');
17
17
  }
@@ -21,21 +21,21 @@ const CLI_PATH = fileURLToPath(new URL('./cli.js', import.meta.url));
21
21
  import { isPeakHour, deferToOffpeak } from './pricing.js';
22
22
  import { atomicWriteFileSync, withFileLockSync } from './atomic-write.js';
23
23
 
24
- export function scheduleDir(home) {
24
+ export function scheduleDir(/** @type {any} */ home) {
25
25
  return path.join(home, 'schedule');
26
26
  }
27
27
 
28
- export function parseInterval(s) {
28
+ export function parseInterval(/** @type {any} */ s) {
29
29
  const m = /^(\d+)(s|m|h|d)$/.exec(String(s).trim());
30
30
  if (!m) return null;
31
31
  const n = Number(m[1]);
32
- const unit = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
32
+ const unit = /** @type {Record<string, number>} */ ({ s: 1000, m: 60000, h: 3600000, d: 86400000 })[m[2]];
33
33
  if (!(n > 0)) return null;
34
34
  return n * unit;
35
35
  }
36
36
 
37
37
  // 解析 --at:'YYYY-MM-DD HH:MM' 或 'HH:MM'(今天;已过则明天)
38
- export function parseAt(s) {
38
+ export function parseAt(/** @type {any} */ s) {
39
39
  const t = String(s).trim();
40
40
  const full = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})$/.exec(t);
41
41
  if (full) {
@@ -56,7 +56,7 @@ export function parseAt(s) {
56
56
  return null;
57
57
  }
58
58
 
59
- export function listSchedules(home) {
59
+ export function listSchedules(/** @type {any} */ home) {
60
60
  const dir = scheduleDir(home);
61
61
  let files;
62
62
  try {
@@ -74,11 +74,11 @@ export function listSchedules(home) {
74
74
  return out.sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0));
75
75
  }
76
76
 
77
- function isValidScheduleId(id) {
77
+ function isValidScheduleId(/** @type {any} */ id) {
78
78
  return typeof id === 'string' && /^[a-z0-9]+$/.test(id) && id.length >= 2 && id.length <= 40;
79
79
  }
80
80
 
81
- export function readSchedule(home, id) {
81
+ export function readSchedule(/** @type {any} */ home, /** @type {any} */ id) {
82
82
  if (!isValidScheduleId(id)) return null; // 防 id 路径穿越
83
83
  try {
84
84
  return JSON.parse(fs.readFileSync(path.join(scheduleDir(home), id + '.json'), 'utf8'));
@@ -87,7 +87,7 @@ export function readSchedule(home, id) {
87
87
  }
88
88
  }
89
89
 
90
- export function writeSchedule(home, job) {
90
+ export function writeSchedule(/** @type {any} */ home, /** @type {any} */ job) {
91
91
  if (!isValidScheduleId(job?.id)) return null;
92
92
  fs.mkdirSync(scheduleDir(home), { recursive: true });
93
93
  const target = path.join(scheduleDir(home), job.id + '.json');
@@ -96,7 +96,7 @@ export function writeSchedule(home, job) {
96
96
  }
97
97
 
98
98
  /** 新建调度任务;after: 依赖的任务 ID(全部成功后才启动,任一失败则跳过) */
99
- export function addSchedule(home, question, /** @type {any} */ { at, every, after, permission, model, cwd, anchor, offpeak }) {
99
+ export function addSchedule(/** @type {any} */ home, /** @type {any} */ question, /** @type {any} */ { at, every, after, permission, model, cwd, anchor, offpeak }) {
100
100
  const id = 'sc' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
101
101
  const interval = every != null ? parseInterval(every) : null;
102
102
  const afterList = Array.isArray(after) ? after.filter(Boolean).map(String) : after ? String(after).split(',').map((x) => x.trim()).filter(Boolean) : [];
@@ -147,7 +147,7 @@ export function addSchedule(home, question, /** @type {any} */ { at, every, afte
147
147
  // 质检 H2(pause 失效确定性回归):runOnce 返回后的状态决策抽为纯函数——
148
148
  // 用户在执行期间 pause/remove 的指令(paused/failed/文件已删)绝不被 pending 覆盖;
149
149
  // 连续失败 3 次熔断;否则排下一次(锚点对齐避免逐日漂移)。
150
- export function postRunStatus(cur2, result) {
150
+ export function postRunStatus(/** @type {any} */ cur2, /** @type {any} */ result) {
151
151
  if (!cur2) return null;
152
152
  if (cur2.status === 'paused' || cur2.status === 'failed') return null;
153
153
  if (result !== 'done' && (cur2.consecutiveFailures || 0) >= 3) {
@@ -157,14 +157,14 @@ export function postRunStatus(cur2, result) {
157
157
  return { status: 'pending', nextRunAt: next };
158
158
  }
159
159
 
160
- export function removeSchedule(home, id) {
160
+ export function removeSchedule(/** @type {any} */ home, /** @type {any} */ id) {
161
161
  // 质检 H3:读-改-写序列加锁,与 sleeper 循环的状态写互斥(防丢更新)
162
162
  return withFileLockSync(path.join(scheduleDir(home), '.lock'), () => {
163
163
  const job = readSchedule(home, id);
164
164
  if (!job) return false;
165
165
  if (job.pid) {
166
166
  const owned = pidOwnedBy(job.pid, id); // 质检 M11:cmdline 含调度 id 才 kill
167
- if (owned !== false) {
167
+ if (owned === true) { // 非 Linux/无法校验(null)不盲杀(CodeBuddy 报告:PID 复用误杀风险)
168
168
  try {
169
169
  process.kill(job.pid, 'SIGTERM');
170
170
  } catch {}
@@ -179,14 +179,14 @@ export function removeSchedule(home, id) {
179
179
  });
180
180
  }
181
181
 
182
- export function pauseSchedule(home, id) {
182
+ export function pauseSchedule(/** @type {any} */ home, /** @type {any} */ id) {
183
183
  // 质检 H3:读-改-写序列加锁(pause 与 sleeper 的 postRunStatus 写互斥,杜绝 pause 被覆盖)
184
184
  return withFileLockSync(path.join(scheduleDir(home), '.lock'), () => {
185
185
  const job = readSchedule(home, id);
186
186
  if (!job) return false;
187
187
  if (job.pid) {
188
188
  const owned = pidOwnedBy(job.pid, id); // 质检 M11:cmdline 含调度 id 才 kill
189
- if (owned !== false) {
189
+ if (owned === true) { // 非 Linux/无法校验(null)不盲杀(CodeBuddy 报告:PID 复用误杀风险)
190
190
  try {
191
191
  process.kill(job.pid, 'SIGTERM');
192
192
  } catch {}
@@ -198,7 +198,7 @@ export function pauseSchedule(home, id) {
198
198
  });
199
199
  }
200
200
 
201
- export function resumeSchedule(home, id) {
201
+ export function resumeSchedule(/** @type {any} */ home, /** @type {any} */ id) {
202
202
  const job = readSchedule(home, id);
203
203
  if (!job || job.status !== 'paused') return false;
204
204
  let next = job.nextRunAt;
@@ -215,7 +215,7 @@ export function resumeSchedule(home, id) {
215
215
  }
216
216
 
217
217
  // 每日锚点:锚点时刻(HH:MM)对齐到 now 之后的最近一次
218
- function nextAnchorAfter(anchor, interval) {
218
+ function nextAnchorAfter(/** @type {any} */ anchor, /** @type {any} */ interval) {
219
219
  const a = parseAt(anchor);
220
220
  if (a == null) return null;
221
221
  let n = a;
@@ -224,7 +224,7 @@ function nextAnchorAfter(anchor, interval) {
224
224
  }
225
225
 
226
226
  // 链式编排:A→B→C,后者依赖前者成功
227
- export function chainSchedules(home, questions, opts = {}) {
227
+ export function chainSchedules(/** @type {any} */ home, /** @type {any} */ questions, opts = {}) {
228
228
  const ids = [];
229
229
  let prev = null;
230
230
  for (const q of questions) {
@@ -237,7 +237,7 @@ export function chainSchedules(home, questions, opts = {}) {
237
237
  return { ids };
238
238
  }
239
239
 
240
- export function sleeperAlive(pid) {
240
+ export function sleeperAlive(/** @type {any} */ pid) {
241
241
  if (!pid) return false;
242
242
  try {
243
243
  process.kill(pid, 0);
@@ -247,7 +247,7 @@ export function sleeperAlive(pid) {
247
247
  }
248
248
  }
249
249
 
250
- function spawnSleeper(home, job) {
250
+ function spawnSleeper(/** @type {any} */ home, /** @type {any} */ job) {
251
251
  const child = spawn(process.execPath, [CLI_PATH, 'schedule-worker', job.id], {
252
252
  detached: true,
253
253
  stdio: 'ignore',
@@ -260,12 +260,12 @@ function spawnSleeper(home, job) {
260
260
  }
261
261
 
262
262
  // —— 单守护进程调度器(评估 P3-5):一进程监督全部任务 ——
263
- export function daemonPidFile(home) {
263
+ export function daemonPidFile(/** @type {any} */ home) {
264
264
  return path.join(scheduleDir(home), 'daemon.pid');
265
265
  }
266
266
  // pid 文件内容 "pid nonce":校验进程存在 + 命令行含同一 nonce,防止陈旧 PID 被无关进程复用而误判(审计 P2-7)
267
267
  // 质检 M11:kill 前校验 PID 归属(/proc/<pid>/cmdline 含 needle 才动手,防 PID 复用误杀)
268
- function pidOwnedBy(pid, needle) {
268
+ function pidOwnedBy(/** @type {any} */ pid, /** @type {any} */ needle) {
269
269
  try {
270
270
  const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
271
271
  return cmdline.includes(needle);
@@ -274,7 +274,7 @@ function pidOwnedBy(pid, needle) {
274
274
  }
275
275
  }
276
276
 
277
- export function daemonAlive(home) {
277
+ export function daemonAlive(/** @type {any} */ home) {
278
278
  try {
279
279
  const [pidStr, nonce] = fs.readFileSync(daemonPidFile(home), 'utf8').trim().split(/\s+/);
280
280
  const pid = Number(pidStr);
@@ -291,7 +291,7 @@ export function daemonAlive(home) {
291
291
  return false;
292
292
  }
293
293
  }
294
- export function stopDaemon(home) {
294
+ export function stopDaemon(/** @type {any} */ home) {
295
295
  try {
296
296
  const pid = Number(fs.readFileSync(daemonPidFile(home), 'utf8'));
297
297
  if (pid) {
@@ -305,7 +305,7 @@ export function stopDaemon(home) {
305
305
  } catch {}
306
306
  return true;
307
307
  }
308
- export function spawnDaemon(home) {
308
+ export function spawnDaemon(/** @type {any} */ home) {
309
309
  if (daemonAlive(home)) return true;
310
310
  const nonce = Math.random().toString(36).slice(2, 10);
311
311
  const child = spawn(process.execPath, [CLI_PATH, 'schedule-daemon', nonce], {
@@ -324,7 +324,7 @@ export function spawnDaemon(home) {
324
324
  }
325
325
 
326
326
  // 重启自愈:有非终态任务且 daemon 不在 → 拉起单守护(失败才回退旧式逐任务 sleeper)
327
- export function reconcileSchedules(home) {
327
+ export function reconcileSchedules(/** @type {any} */ home) {
328
328
  const jobs = listSchedules(home);
329
329
  const hasPending = jobs.some((j) => j.status === 'pending' || j.status === 'running');
330
330
  if (!hasPending) return;
@@ -343,14 +343,14 @@ export function reconcileSchedules(home) {
343
343
  spawnDaemon(home);
344
344
  }
345
345
  // —— sleeper 主循环(schedule-worker 进程内运行)——
346
- export async function runSleeper(home, id) {
346
+ export async function runSleeper(/** @type {any} */ home, /** @type {any} */ id) {
347
347
  const job = readSchedule(home, id);
348
348
  if (!job) return;
349
349
 
350
- const wait = (ms) => new Promise((r) => setTimeout(r, ms));
350
+ const wait = (/** @type {any} */ ms) => new Promise((r) => setTimeout(r, ms));
351
351
 
352
352
  // 依赖检查:支持两种依赖——调度任务 id(chain 编排)或后台任务 id(mingdao run 输出)
353
- async function depsSatisfied(deps) {
353
+ async function depsSatisfied(/** @type {any} */ deps) {
354
354
  for (const dep of deps) {
355
355
  const sch = readSchedule(home, dep);
356
356
  if (sch) {
@@ -476,7 +476,7 @@ export async function runSleeper(home, id) {
476
476
  }
477
477
  }
478
478
 
479
- export function formatScheduleRow(j) {
479
+ export function formatScheduleRow(/** @type {any} */ j) {
480
480
  const mark = j.status === 'pending' ? '⏳' : j.status === 'running' ? '▶' : j.status === 'paused' ? '⏸' : j.status === 'done' ? '✓' : '✖';
481
481
  const when =
482
482
  j.kind === 'every'
@@ -25,6 +25,7 @@ export function loadSessionIndex() {
25
25
  }
26
26
  }
27
27
 
28
+ /** @param {any} idx */
28
29
  export function saveSessionIndex(idx) {
29
30
  try {
30
31
  ensureHome();
@@ -34,9 +35,10 @@ export function saveSessionIndex(idx) {
34
35
  }
35
36
 
36
37
  // 分词:ASCII 词(小写)+ 中文 bigram + 中文单字;返回 Map<词, 次数>
38
+ /** @param {any} text */
37
39
  export function tokenize(text) {
38
40
  const terms = new Map();
39
- const add = (t) => {
41
+ const add = (/** @type {any} */ t) => {
40
42
  if (t) terms.set(t, (terms.get(t) || 0) + 1);
41
43
  };
42
44
  const s = String(text ?? '');
@@ -61,6 +63,7 @@ export function tokenize(text) {
61
63
  }
62
64
 
63
65
  // 从会话文件提取可检索文本(user/assistant 内容拼接)
66
+ /** @param {any} file */
64
67
  export function extractSessionText(file) {
65
68
  const parts = [];
66
69
  for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
@@ -79,6 +82,7 @@ export function extractSessionText(file) {
79
82
  }
80
83
 
81
84
  // 增量同步索引(对比 mtime/size,只重算变化的文件;清理已删除条目)
85
+ /** @param {any} home @param {any} sessions */
82
86
  export function syncSessionIndex(home, sessions) {
83
87
  const idx = loadSessionIndex();
84
88
  const dir = path.join(home, 'sessions');
package/src/session.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
 
7
+ /** @param {any} home */
7
8
  export function listSessions(home) {
8
9
  const dir = path.join(home, 'sessions');
9
10
  try {
@@ -20,10 +21,12 @@ export function listSessions(home) {
20
21
  }
21
22
  }
22
23
 
24
+ /** @param {any} home */
23
25
  export function latestSession(home) {
24
26
  return listSessions(home)[0] || null;
25
27
  }
26
28
 
29
+ /** @param {any} home */
27
30
  export function createSession(home) {
28
31
  const dir = path.join(home, 'sessions');
29
32
  fs.mkdirSync(dir, { recursive: true });
@@ -33,18 +36,21 @@ export function createSession(home) {
33
36
  return { file, name: path.basename(file) };
34
37
  }
35
38
 
39
+ /** @param {any} file @param {any} messages */
36
40
  export function appendMessages(file, messages) {
37
41
  if (!messages?.length) return;
38
- const lines = messages.map((m) => JSON.stringify(m)).join('\n') + '\n';
42
+ const lines = messages.map((/** @type {any} */ m) => JSON.stringify(m)).join('\n') + '\n';
39
43
  fs.appendFileSync(file, lines);
40
44
  }
41
45
 
42
46
  // 整文件原子重写:自动压缩后把会话文件同步为压缩形态(否则每次加载历史都会重新触发压缩)
47
+ /** @param {any} file @param {any} messages */
43
48
  export function rewriteSession(file, messages) {
44
- const lines = messages.map((m) => JSON.stringify(m)).join('\n');
49
+ const lines = messages.map((/** @type {any} */ m) => JSON.stringify(m)).join('\n');
45
50
  atomicWriteFileSync(file, lines + (lines ? '\n' : ''), { mode: 0o600 }); // 质检 H4:会话文件原子写
46
51
  }
47
52
 
53
+ /** @param {any} file */
48
54
  export function loadSession(file) {
49
55
  const raw = fs.readFileSync(file, 'utf8');
50
56
  const messages = [];
@@ -61,6 +67,7 @@ export function loadSession(file) {
61
67
  }
62
68
 
63
69
  // 会话预览:第一条用户消息(用于会话选择器)
70
+ /** @param {any} file */
64
71
  export function sessionPreview(file) {
65
72
  try {
66
73
  const raw = fs.readFileSync(file, 'utf8');
@@ -79,6 +86,7 @@ export function sessionPreview(file) {
79
86
  }
80
87
  }
81
88
 
89
+ /** @param {any} mtime */
82
90
  export function relativeTime(mtime) {
83
91
  const diff = Date.now() - mtime;
84
92
  if (diff < 60000) return '刚刚';
@@ -92,6 +100,7 @@ export function relativeTime(mtime) {
92
100
  import { syncSessionIndex, tokenize, extractSessionText } from './session-index.js';
93
101
  import { atomicWriteFileSync } from './atomic-write.js';
94
102
 
103
+ /** @param {any} home @param {any} keyword */
95
104
  export function searchSessions(home, keyword, { limit = 20 } = {}) {
96
105
  const kw = String(keyword ?? '').trim();
97
106
  if (!kw) return listSessions(home).slice(0, limit);