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
package/src/skill-lib.js CHANGED
@@ -22,8 +22,13 @@ const LIB_DIR = fileURLToPath(new URL('../skills-lib', import.meta.url));
22
22
  // ---------- 完整性(P3-3):技能目录联合哈希 ----------
23
23
  // 对目录内全部文件(排除 .mingdao-source.json 自身)按相对路径排序,
24
24
  // 逐文件 sha256 后拼成 "rel:hash\n" 再取一次 sha256,作为目录内容指纹。
25
+ /**
26
+ * @param {any} dir
27
+ */
25
28
  export function skillDirHash(dir) {
29
+ /** @type {any[]} */
26
30
  const out = [];
31
+ /** @type {(d: any, base: any) => void} */
27
32
  const walk = (d, base) => {
28
33
  let entries;
29
34
  try {
@@ -47,6 +52,9 @@ export function skillDirHash(dir) {
47
52
  .digest('hex');
48
53
  }
49
54
 
55
+ /**
56
+ * @param {any} dir
57
+ */
50
58
  export function readSourceMeta(dir) {
51
59
  try {
52
60
  return JSON.parse(fs.readFileSync(path.join(dir, '.mingdao-source.json'), 'utf8'));
@@ -55,11 +63,18 @@ export function readSourceMeta(dir) {
55
63
  }
56
64
  }
57
65
 
66
+ /**
67
+ * @param {any} dir
68
+ * @param {any} meta
69
+ */
58
70
  export function writeSourceMeta(dir, meta) {
59
71
  fs.writeFileSync(path.join(dir, '.mingdao-source.json'), JSON.stringify(meta, null, 2) + '\n', { mode: 0o600 });
60
72
  }
61
73
 
62
74
  // 显式信任当前内容(用户改过 registry/library 安装的技能后重新记录指纹)
75
+ /**
76
+ * @param {any} name
77
+ */
63
78
  export function trustSkill(name) {
64
79
  const target = path.join(userSkillsDir(), name);
65
80
  const meta = readSourceMeta(target);
@@ -79,6 +94,10 @@ export function userSkillsDir() {
79
94
  }
80
95
 
81
96
  // 从技能目录读取 name / description(frontmatter 优先,回退标题/目录名)
97
+ /**
98
+ * @param {any} dir
99
+ * @returns {any}
100
+ */
82
101
  function readSkillMeta(dir) {
83
102
  try {
84
103
  const text = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8');
@@ -116,6 +135,7 @@ export function installedUserSkillNames() {
116
135
 
117
136
  export function libraryList() {
118
137
  const installed = installedUserSkillNames();
138
+ /** @type {any[]} */
119
139
  const out = [];
120
140
  let entries;
121
141
  try {
@@ -132,6 +152,9 @@ export function libraryList() {
132
152
  return out.sort((a, b) => a.name.localeCompare(b.name));
133
153
  }
134
154
 
155
+ /**
156
+ * @param {any} kw
157
+ */
135
158
  export function searchLibrary(kw) {
136
159
  const k = String(kw || '').trim().toLowerCase();
137
160
  return libraryList().filter(
@@ -144,6 +167,10 @@ export function searchLibrary(kw) {
144
167
  // - name:字母/数字/连字符/点/下划线,1–64 位
145
168
  // - description:非空且 ≤ 200 字
146
169
  // 校验失败返回具体错误,绝不装入技能目录。
170
+ /**
171
+ * @param {any} text
172
+ * @param {any} hint
173
+ */
147
174
  export function validateSkillMarkdown(text, hint) {
148
175
  const src = hint ? `(${hint})` : '';
149
176
  if (!text || !String(text).trim()) return { error: `SKILL.md 为空${src}` };
@@ -159,6 +186,10 @@ export function validateSkillMarkdown(text, hint) {
159
186
  return { ok: true, name, description: desc };
160
187
  }
161
188
 
189
+ /**
190
+ * @param {any} dir
191
+ * @param {any} hint
192
+ */
162
193
  export function validateSkillDir(dir, hint) {
163
194
  const skillMd = path.join(dir, 'SKILL.md');
164
195
  let text;
@@ -170,6 +201,12 @@ export function validateSkillDir(dir, hint) {
170
201
  return validateSkillMarkdown(text, hint);
171
202
  }
172
203
 
204
+ /**
205
+ * @param {any} dir
206
+ * @param {any} name
207
+ * @param {any} source
208
+ * @param {any} extra
209
+ */
173
210
  function copySkillIntoUser(dir, name, source, extra) {
174
211
  ensureHome();
175
212
  const target = path.join(userSkillsDir(), name);
@@ -188,12 +225,18 @@ function copySkillIntoUser(dir, name, source, extra) {
188
225
  return { name, dir: target };
189
226
  }
190
227
 
228
+ /**
229
+ * @param {any} name
230
+ */
191
231
  export function installFromLibrary(name) {
192
232
  const found = libraryList().find((s) => s.name === name);
193
233
  if (!found) return { error: `技能库中没有 ${name}(mingdao skill search 查看全部)` };
194
234
  return copySkillIntoUser(found.dir, found.name, 'library', {});
195
235
  }
196
236
 
237
+ /**
238
+ * @param {any} dir
239
+ */
197
240
  export function installFromDir(dir) {
198
241
  const abs = path.resolve(dir);
199
242
  if (!fs.existsSync(path.join(abs, 'SKILL.md'))) {
@@ -204,6 +247,9 @@ export function installFromDir(dir) {
204
247
  return copySkillIntoUser(abs, meta.name, 'dir', { from: abs });
205
248
  }
206
249
 
250
+ /**
251
+ * @param {any} url
252
+ */
207
253
  export async function installFromUrl(url) {
208
254
  let u;
209
255
  try {
@@ -222,7 +268,7 @@ export async function installFromUrl(url) {
222
268
  if (!res.ok) return { error: `下载失败:HTTP ${res.status}` };
223
269
  text = await res.text();
224
270
  if (text.length > 512 * 1024) return { error: 'SKILL.md 超过 512KB 上限' };
225
- } catch (e) {
271
+ } catch (/** @type {any} */ e) {
226
272
  return { error: `下载失败:${e.name === 'AbortError' ? '30 秒超时' : e.message}` };
227
273
  } finally {
228
274
  clearTimeout(timer);
@@ -244,6 +290,9 @@ export async function installFromUrl(url) {
244
290
  return r;
245
291
  }
246
292
 
293
+ /**
294
+ * @param {any} gitUrl
295
+ */
247
296
  export function installFromGit(gitUrl) {
248
297
  if (typeof gitUrl !== 'string' || gitUrl.trim().startsWith('-')) {
249
298
  return { error: 'git 地址不能以 - 开头(防选项注入)' };
@@ -262,7 +311,8 @@ export function installFromGit(gitUrl) {
262
311
  const found = [];
263
312
  const stack = [tmp];
264
313
  while (stack.length && found.length < 20) {
265
- const dir = stack.pop();
314
+ const dir = /** @type {any} */ (stack.pop());
315
+ /** @type {any[]} */
266
316
  let entries = [];
267
317
  try {
268
318
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -297,6 +347,9 @@ export function installFromGit(gitUrl) {
297
347
  return { names: installed.map((i) => i.name), dirs: installed.map((i) => i.dir), skipped };
298
348
  }
299
349
 
350
+ /**
351
+ * @param {any} name
352
+ */
300
353
  export function uninstallSkill(name) {
301
354
  const key = String(name).trim();
302
355
  // '.'/'..' 会拼出上级目录,必须硬拒绝(正则 [A-Za-z0-9_.-]+ 会放行纯点号名)
@@ -310,9 +363,12 @@ export function uninstallSkill(name) {
310
363
  }
311
364
 
312
365
  // 按元数据来源重新安装(库:重新复制;url:重新下载;git:重新克隆)
366
+ /**
367
+ * @param {any} name
368
+ */
313
369
  export async function reinstallSkill(name) {
314
370
  const target = path.join(userSkillsDir(), name);
315
- let meta = {};
371
+ let meta = /** @type {any} */ ({});
316
372
  try {
317
373
  meta = JSON.parse(fs.readFileSync(path.join(target, '.mingdao-source.json'), 'utf8'));
318
374
  } catch {
@@ -330,6 +386,9 @@ export async function reinstallSkill(name) {
330
386
  }
331
387
 
332
388
  // 统一入口:自动识别 库名 | 本地目录 | SKILL.md URL | git 仓库
389
+ /**
390
+ * @param {any} arg
391
+ */
333
392
  export async function installSkill(arg) {
334
393
  const a = String(arg || '').trim();
335
394
  if (!a) return { error: '缺少参数:mingdao skill install <库名|目录|SKILL.md URL|git 仓库地址>' };
@@ -42,14 +42,14 @@ function loadCache() {
42
42
  }
43
43
  }
44
44
 
45
- function saveCache(data, host) {
45
+ function saveCache(/** @type {any} */ data, /** @type {any} */ host) {
46
46
  try {
47
47
  ensureHome();
48
48
  fs.writeFileSync(cacheFile(), JSON.stringify({ fetchedAt: Date.now(), host, data }, null, 2) + '\n', { mode: 0o600 });
49
49
  } catch {}
50
50
  }
51
51
 
52
- async function fetchText(url, timeoutMs = 20000, maxBytes = 2 * 1024 * 1024) {
52
+ async function fetchText(/** @type {any} */ url, timeoutMs = 20000, maxBytes = 2 * 1024 * 1024) {
53
53
  const ctrl = new AbortController();
54
54
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
55
55
  try {
@@ -91,26 +91,26 @@ export async function fetchRegistryIndex({ force = false, allowNetwork = true }
91
91
  }
92
92
  // 网络失败时回退旧缓存(过期也可用)
93
93
  if (cached?.data?.skills) return { data: cached.data, host: cached.host, fromCache: true, stale: true };
94
- return { error: `无法获取线上技能库:${lastErr?.message || '网络不可达'}(可用 MINGDAO_REGISTRY_URL 指向自建 registry)` };
94
+ return { error: `无法获取线上技能库:${(/** @type {any} */ (lastErr))?.message || '网络不可达'}(可用 MINGDAO_REGISTRY_URL 指向自建 registry)` };
95
95
  }
96
96
 
97
97
  // 远端搜索(合并展示:name/description/source/installed)
98
- export async function searchRegistry(kw, { force = false, allowNetwork = true } = {}) {
98
+ export async function searchRegistry(/** @type {any} */ kw, { force = false, allowNetwork = true } = {}) {
99
99
  const r = await fetchRegistryIndex({ force, allowNetwork });
100
100
  if (r.error) return { error: r.error };
101
101
  const k = String(kw || '').trim().toLowerCase();
102
102
  const installed = installedUserSkillNames();
103
103
  const skills = r.data.skills
104
- .filter((s) => !k || s.name.toLowerCase().includes(k) || (s.description || '').toLowerCase().includes(k))
105
- .map((s) => ({ name: s.name, description: s.description, source: 'registry', installed: installed.has(s.name) }));
104
+ .filter((/** @type {any} */ s) => !k || s.name.toLowerCase().includes(k) || (s.description || '').toLowerCase().includes(k))
105
+ .map((/** @type {any} */ s) => ({ name: s.name, description: s.description, source: 'registry', installed: installed.has(s.name) }));
106
106
  return { skills, host: r.host, updatedAt: r.data.updatedAt, fromCache: r.fromCache, stale: r.stale || false };
107
107
  }
108
108
 
109
109
  // 按索引安装(逐文件下载 + dry-run 校验)
110
- export async function installFromRegistry(name) {
110
+ export async function installFromRegistry(/** @type {any} */ name) {
111
111
  const r = await fetchRegistryIndex();
112
112
  if (r.error) return { error: r.error };
113
- const entry = r.data.skills.find((s) => s.name === name);
113
+ const entry = r.data.skills.find((/** @type {any} */ s) => s.name === name);
114
114
  if (!entry) return { error: `线上技能库中没有 ${name}(mingdao skill search 查看)` };
115
115
  if (!Array.isArray(entry.files) || !entry.files.length) return { error: `技能 ${name} 的索引缺少文件清单` };
116
116
 
@@ -134,7 +134,8 @@ export async function installFromRegistry(name) {
134
134
  text = await fetchText(`${host}/skills-lib/${encodeURI(name)}/${rel.split('/').map(encodeURIComponent).join('/')}`, 30000, MAX_FILE);
135
135
  break;
136
136
  } catch (e) {
137
- lastErr = e?.name === 'AbortError' ? '下载超时' : String(e?.message || e);
137
+ const ee = /** @type {any} */ (e);
138
+ lastErr = ee?.name === 'AbortError' ? '下载超时' : String(ee?.message || ee);
138
139
  }
139
140
  }
140
141
  if (text === null) return { error: `下载 ${name}/${rel} 失败:${lastErr}(已尝试全部镜像)` };
package/src/skills.js CHANGED
@@ -15,7 +15,7 @@ import { skillDirHash, readSourceMeta } from './skill-lib.js';
15
15
 
16
16
  const BUILTIN_DIR = fileURLToPath(new URL('../skills', import.meta.url));
17
17
 
18
- export function skillDirs(workingDir) {
18
+ export function skillDirs(/** @type {any} */ workingDir) {
19
19
  return [
20
20
  { dir: path.join(mingdaoHome(), 'skills'), source: 'user' },
21
21
  { dir: path.join(workingDir, '.mingdao', 'skills'), source: 'project' },
@@ -23,7 +23,7 @@ export function skillDirs(workingDir) {
23
23
  ];
24
24
  }
25
25
 
26
- function readDescription(skillMd) {
26
+ function readDescription(/** @type {any} */ skillMd) {
27
27
  try {
28
28
  const text = fs.readFileSync(skillMd, 'utf8');
29
29
  const fm = text.match(/^---\n([\s\S]*?)\n---/);
@@ -39,7 +39,7 @@ function readDescription(skillMd) {
39
39
  }
40
40
 
41
41
  // 完整性校验(P3-3):带指纹(sha256)来源记录的技能,内容与安装时不一致 → 拒绝加载
42
- function isTampered(dir) {
42
+ function isTampered(/** @type {any} */ dir) {
43
43
  const meta = readSourceMeta(dir);
44
44
  if (!meta?.sha256) return false; // 旧版安装(无指纹)不拦截
45
45
  try {
@@ -50,14 +50,14 @@ function isTampered(dir) {
50
50
  }
51
51
 
52
52
  // 被篡改的用户级技能清单(CLI/WebUI 提示用;这些技能已被 listSkills 排除加载)
53
- export function tamperedSkillNames(workingDir) {
53
+ export function tamperedSkillNames(/** @type {any} */ workingDir) {
54
54
  const dir = path.join(mingdaoHome(), 'skills');
55
- const out = [];
55
+ const /** @type {any} */ out = [];
56
56
  let entries;
57
57
  try {
58
58
  entries = fs.readdirSync(dir, { withFileTypes: true });
59
59
  } catch {
60
- return out;
60
+ return /** @type {any} */ out;
61
61
  }
62
62
  for (const e of entries) {
63
63
  if (!e.isDirectory()) continue;
@@ -75,7 +75,7 @@ export function tamperedSkillNames(workingDir) {
75
75
  return out;
76
76
  }
77
77
 
78
- export function listSkills(workingDir) {
78
+ export function listSkills(/** @type {any} */ workingDir) {
79
79
  const seen = new Set();
80
80
  const out = [];
81
81
  for (const { dir, source } of skillDirs(workingDir)) {
@@ -94,8 +94,10 @@ export function listSkills(workingDir) {
94
94
  continue;
95
95
  }
96
96
  seen.add(e.name); // 优先级:user > project > builtin,先出现的生效
97
- // P3-3:用户级技能指纹不符 → 拒绝加载(绝不静默执行被篡改的提示词)
98
- if (source === 'user' && isTampered(path.join(dir, e.name))) continue;
97
+ // P3-3:技能指纹不符 → 拒绝加载(绝不静默执行被篡改的提示词)。
98
+ // project 级(.mingdao/skills/)同样校验(CodeBuddy 报告:仓库投毒可直通提示词注入);
99
+ // 无 .source.json 指纹的旧仓库/手写技能不受影响(isTampered 返回 false)。
100
+ if ((source === 'user' || source === 'project') && isTampered(path.join(dir, e.name))) continue;
99
101
  out.push({
100
102
  name: e.name,
101
103
  dir: path.join(dir, e.name),
@@ -108,7 +110,7 @@ export function listSkills(workingDir) {
108
110
  return out;
109
111
  }
110
112
 
111
- export function loadSkill(workingDir, name) {
113
+ export function loadSkill(/** @type {any} */ workingDir, /** @type {any} */ name) {
112
114
  const s = listSkills(workingDir).find((x) => x.name === name);
113
115
  if (!s) return null;
114
116
  try {
@@ -118,14 +120,14 @@ export function loadSkill(workingDir, name) {
118
120
  }
119
121
  }
120
122
 
121
- function sourceLabel(source) {
123
+ function sourceLabel(/** @type {any} */ source) {
122
124
  if (source === 'user') return '(用户级)';
123
125
  if (source === 'builtin') return '(内置)';
124
126
  return '';
125
127
  }
126
128
 
127
129
  // 注入系统提示的技能清单(仅名称+描述)
128
- export function skillsRegistryBlock(workingDir) {
130
+ export function skillsRegistryBlock(/** @type {any} */ workingDir) {
129
131
  const skills = listSkills(workingDir);
130
132
  if (!skills.length) return '';
131
133
  return (
@@ -39,9 +39,14 @@ const INVITE_CODES = new Set(
39
39
  .filter(Boolean)
40
40
  );
41
41
 
42
+ /** @type {(...a: any[]) => void} */
42
43
  const log = (...a) => console.log(new Date().toISOString(), ...a);
43
44
 
44
45
  // ---------- 存储 ----------
46
+ /**
47
+ * @param {any} file
48
+ * @param {any} fallback
49
+ */
45
50
  function readJson(file, fallback) {
46
51
  try {
47
52
  return JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -49,6 +54,10 @@ function readJson(file, fallback) {
49
54
  return fallback;
50
55
  }
51
56
  }
57
+ /**
58
+ * @param {any} file
59
+ * @param {any} obj
60
+ */
52
61
  function writeJson(file, obj) {
53
62
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
54
63
  atomicWriteFileSync(file, JSON.stringify(obj, null, 2) + '\n', { mode: 0o600 }); // 质检 H4
@@ -59,12 +68,15 @@ function usersFile() {
59
68
  function devicesFile() {
60
69
  return path.join(ACTIVE_DIR, 'devices.json');
61
70
  }
71
+ /** @param {any} username */
62
72
  function userDir(username) {
63
73
  return path.join(ACTIVE_DIR, 'data', username);
64
74
  }
75
+ /** @param {any} username */
65
76
  function sessionsDir(username) {
66
77
  return path.join(userDir(username), 'sessions');
67
78
  }
79
+ /** @param {any} username */
68
80
  function metaFile(username) {
69
81
  return path.join(userDir(username), 'meta.json');
70
82
  }
@@ -75,8 +87,13 @@ function acceptedFile() {
75
87
  return path.join(ACTIVE_DIR, 'accepted.json');
76
88
  }
77
89
 
90
+ /** @type {(s: any) => string} */
78
91
  const sha = (s) => crypto.createHash('sha256').update(String(s)).digest('hex');
79
92
  // 常量时间比较(P3-10):token/密码哈希等值判断防时序侧信道
93
+ /**
94
+ * @param {any} a
95
+ * @param {any} b
96
+ */
80
97
  function safeEqualHex(a, b) {
81
98
  try {
82
99
  const ba = Buffer.from(String(a), 'hex');
@@ -87,9 +104,18 @@ function safeEqualHex(a, b) {
87
104
  }
88
105
  }
89
106
  // 密码存储用 scrypt(带盐 KDF);旧数据兼容:verify 时 sha256 回退
107
+ /**
108
+ * @param {any} password
109
+ * @param {any} salt
110
+ */
90
111
  function hashPassword(password, salt) {
91
112
  return crypto.scryptSync(String(password), String(salt), 32).toString('hex');
92
113
  }
114
+ /**
115
+ * @param {any} password
116
+ * @param {any} salt
117
+ * @param {any} hash
118
+ */
93
119
  function verifyPassword(password, salt, hash) {
94
120
  if (!hash) return false;
95
121
  try {
@@ -98,9 +124,11 @@ function verifyPassword(password, salt, hash) {
98
124
  // 兼容早期 sha256 存储
99
125
  return safeEqualHex(hash, sha(`${salt}:${password}`));
100
126
  }
127
+ /** @param {any} u */
101
128
  function isValidUsername(u) {
102
129
  return typeof u === 'string' && /^[A-Za-z0-9_.-]{2,32}$/.test(u) && u !== '.' && u !== '..';
103
130
  }
131
+ /** @param {any} n */
104
132
  function isValidSessionName(n) {
105
133
  return typeof n === 'string' && /^[\w\u4e00-\u9fa5.-]{1,140}\.jsonl$/.test(n) && !n.includes('..');
106
134
  }
@@ -108,6 +136,7 @@ function isValidSessionName(n) {
108
136
  // ---------- 限速(防爆破/枚举;内存表,进程级) ----------
109
137
  const TRUST_PROXY = process.env.SYNC_TRUST_PROXY === '1'; // 仅部署在可信反代后时开启
110
138
  const rateBuckets = new Map();
139
+ /** @param {any} req */
111
140
  function clientKey(req) {
112
141
  let ip = req.socket?.remoteAddress || 'x';
113
142
  if (TRUST_PROXY) {
@@ -116,6 +145,7 @@ function clientKey(req) {
116
145
  }
117
146
  return ip;
118
147
  }
148
+ /** @param {any} req */
119
149
  function rateLimited(req, limit = 20, extra = '') {
120
150
  // 质检 M5:键 = IP + 路由 + 用户名——单账号爆破按用户名限(分布式 IP 无法绕过);
121
151
  // 表满按最旧淘汰而非整表 clear(整表 clear 可被攻击者重置全员限额)
@@ -140,11 +170,13 @@ function rateLimited(req, limit = 20, extra = '') {
140
170
  }
141
171
 
142
172
  // ---------- HTTP 工具 ----------
173
+ /** @param {any} req */
143
174
  function readBody(req, limit = MAX_BODY) {
144
175
  return new Promise((resolve, reject) => {
176
+ /** @type {any[]} */
145
177
  const chunks = [];
146
178
  let size = 0;
147
- req.on('data', (d) => {
179
+ req.on('data', (/** @type {any} */ d) => {
148
180
  size += d.length;
149
181
  if (size > limit) {
150
182
  reject(new Error('body 超限'));
@@ -157,20 +189,27 @@ function readBody(req, limit = MAX_BODY) {
157
189
  req.on('error', reject);
158
190
  });
159
191
  }
192
+ /**
193
+ * @param {any} res
194
+ * @param {any} code
195
+ * @param {any} obj
196
+ */
160
197
  function json(res, code, obj) {
161
198
  const body = JSON.stringify(obj);
162
199
  res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body) });
163
200
  res.end(body);
164
201
  }
202
+ /** @param {any} req */
165
203
  async function parseBody(req) {
166
204
  try {
167
205
  return JSON.parse(await readBody(req) || '{}');
168
206
  } catch (e) {
169
- return { __error: e.message };
207
+ return { __error: /** @type {any} */ (e).message };
170
208
  }
171
209
  }
172
210
 
173
211
  // ---------- 认证 ----------
212
+ /** @param {any} req */
174
213
  function tokenOf(req) {
175
214
  const h = req.headers.authorization || '';
176
215
  const m = h.match(/^Bearer\s+(.+)$/i);
@@ -181,6 +220,7 @@ const deviceTokenCache = new Map();
181
220
  function invalidateDeviceCache() {
182
221
  deviceTokenCache.clear();
183
222
  }
223
+ /** @param {any} token */
184
224
  function findDeviceByToken(token) {
185
225
  if (!token) return null;
186
226
  const tokenHash = sha(token);
@@ -202,9 +242,12 @@ function findDeviceByToken(token) {
202
242
 
203
243
  // ---------- 业务 ----------
204
244
  // 质检 A1:通用写互斥(注册/pair/lastSeen/meta 更新串行化),防 devices.json/meta.json 读改写竞态
245
+ /** @type {any} */
205
246
  let writeLock = null;
247
+ /** @param {any} fn */
206
248
  async function withWriteLock(fn) {
207
249
  const prev = writeLock || Promise.resolve();
250
+ /** @type {any} */
208
251
  let release;
209
252
  writeLock = new Promise((resolve) => {
210
253
  release = resolve;
@@ -216,7 +259,9 @@ async function withWriteLock(fn) {
216
259
  release();
217
260
  }
218
261
  }
262
+ /** @type {any} */
219
263
  let registerLock = null;
264
+ /** @param {any} body */
220
265
  async function doRegister(body) {
221
266
  const username = String(body.username || '').trim();
222
267
  const password = String(body.password || '');
@@ -229,6 +274,7 @@ async function doRegister(body) {
229
274
  });
230
275
  }
231
276
  const prev = registerLock;
277
+ /** @type {any} */
232
278
  let release;
233
279
  registerLock = new Promise((resolve) => {
234
280
  release = resolve;
@@ -247,6 +293,7 @@ async function doRegister(body) {
247
293
  }
248
294
  }
249
295
 
296
+ /** @param {any} body */
250
297
  async function doPair(body) {
251
298
  const username = String(body.username || '').trim();
252
299
  const password = String(body.password || '');
@@ -268,10 +315,12 @@ async function doPair(body) {
268
315
  });
269
316
  }
270
317
 
318
+ /** @param {any} username */
271
319
  function doListSessions(username) {
272
320
  const dir = sessionsDir(username);
273
321
  const meta = readJson(metaFile(username), {});
274
322
  const out = [];
323
+ /** @type {any[]} */
275
324
  let entries = [];
276
325
  try {
277
326
  entries = fs.readdirSync(dir, { withFileTypes: true });
@@ -285,6 +334,10 @@ function doListSessions(username) {
285
334
  return out;
286
335
  }
287
336
 
337
+ /**
338
+ * @param {any} username
339
+ * @param {any} body
340
+ */
288
341
  function doPush(username, body) {
289
342
  const name = String(body.name || '').trim();
290
343
  const content = typeof body.content === 'string' ? body.content : '';
@@ -305,6 +358,10 @@ function doPush(username, body) {
305
358
  });
306
359
  }
307
360
 
361
+ /**
362
+ * @param {any} username
363
+ * @param {any} name
364
+ */
308
365
  function doPull(username, name) {
309
366
  if (!isValidSessionName(name)) return { notFound: '会话不存在' };
310
367
  const file = path.join(sessionsDir(username), name);
@@ -317,6 +374,10 @@ function doPull(username, name) {
317
374
  }
318
375
  }
319
376
 
377
+ /**
378
+ * @param {any} username
379
+ * @param {any} name
380
+ */
320
381
  function doDelete(username, name) {
321
382
  if (!isValidSessionName(name)) return { notFound: '会话不存在' };
322
383
  const dir = sessionsDir(username);
@@ -335,6 +396,10 @@ function doDelete(username, name) {
335
396
  }
336
397
 
337
398
  // ---------- 密码修改 ----------
399
+ /**
400
+ * @param {any} username
401
+ * @param {any} body
402
+ */
338
403
  function doChangePassword(username, body) {
339
404
  const oldPassword = String(body.oldPassword || '');
340
405
  const newPassword = String(body.newPassword || '');
@@ -356,6 +421,10 @@ function doChangePassword(username, body) {
356
421
  }
357
422
 
358
423
  // ---------- 会话分享 ----------
424
+ /**
425
+ * @param {any} username
426
+ * @param {any} name
427
+ */
359
428
  function doShareCreate(username, name) {
360
429
  if (!isValidSessionName(name)) return { error: `会话名非法:${name}` };
361
430
  if (!fs.existsSync(path.join(sessionsDir(username), name))) return { notFound: '你还没有这个会话' };
@@ -367,6 +436,7 @@ function doShareCreate(username, name) {
367
436
  return { ok: true, shareId, name };
368
437
  }
369
438
 
439
+ /** @param {any} username */
370
440
  function doShareList(username) {
371
441
  const shares = readJson(sharesFile(), {});
372
442
  const accepted = readJson(acceptedFile(), {})[username] || {};
@@ -383,6 +453,10 @@ function doShareList(username) {
383
453
  return { ok: true, mine, accepted: mineAccepted };
384
454
  }
385
455
 
456
+ /**
457
+ * @param {any} username
458
+ * @param {any} shareId
459
+ */
386
460
  function doShareRevoke(username, shareId) {
387
461
  const shares = readJson(sharesFile(), {});
388
462
  const s = shares[shareId];
@@ -396,6 +470,10 @@ function doShareRevoke(username, shareId) {
396
470
  // 接受/刷新分享:克隆(或更新)到接受者自己的会话列表。
397
471
  // - 首次接受 → 写入同名文件(同名已有不同内容则另存 .shared- 副本,绝不覆盖)
398
472
  // - 再次接受:接受者未修改过副本 → 就地刷新;已修改 → 另存新副本(冲突保留双方)
473
+ /**
474
+ * @param {any} username
475
+ * @param {any} shareId
476
+ */
399
477
  function doShareAccept(username, shareId) {
400
478
  const shares = readJson(sharesFile(), {});
401
479
  const s = shares[shareId];
@@ -453,6 +531,10 @@ function doShareAccept(username, shareId) {
453
531
  }
454
532
 
455
533
  // ---------- 路由 ----------
534
+ /**
535
+ * @param {any} req
536
+ * @param {any} res
537
+ */
456
538
  async function handle(req, res) {
457
539
  const u = new URL(req.url, 'http://x');
458
540
  const p = u.pathname;
@@ -569,7 +651,7 @@ async function handle(req, res) {
569
651
  }
570
652
  return json(res, 404, { error: '未知接口' });
571
653
  } catch (e) {
572
- log('error', p, e.message);
654
+ log('error', p, /** @type {any} */ (e).message);
573
655
  return json(res, 500, { error: '服务器内部错误' });
574
656
  }
575
657
  }
@@ -584,6 +666,7 @@ export function runSyncServer({ port, host, dataDir, cert, key } = {}) {
584
666
  const certFile = cert || CERT;
585
667
  const keyFile = key || KEY;
586
668
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
669
+ /** @type {(req: any, res: any) => void} */
587
670
  const handler = (req, res) => {
588
671
  handle(req, res).catch(() => {});
589
672
  };