mingdao-harness 0.1.54

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +246 -0
  3. package/assets/tokenizer-data.json.gz +0 -0
  4. package/docs/ARCHITECTURE.md +108 -0
  5. package/docs/CONFIG.md +257 -0
  6. package/docs/DESKTOP-EVALUATION.md +48 -0
  7. package/docs/PROVIDERS.md +98 -0
  8. package/docs/QA-REPORT.md +333 -0
  9. package/install.bat +8 -0
  10. package/install.ps1 +57 -0
  11. package/install.sh +154 -0
  12. package/package.json +61 -0
  13. package/skills/api-design/SKILL.md +21 -0
  14. package/skills/code-review/SKILL.md +31 -0
  15. package/skills/debugging/SKILL.md +21 -0
  16. package/skills/docker/SKILL.md +27 -0
  17. package/skills/docx/SKILL.md +28 -0
  18. package/skills/frontend-design/SKILL.md +31 -0
  19. package/skills/git-commit/SKILL.md +32 -0
  20. package/skills/pdf/SKILL.md +30 -0
  21. package/skills/pptx/SKILL.md +24 -0
  22. package/skills/refactoring/SKILL.md +24 -0
  23. package/skills/release-checklist/SKILL.md +20 -0
  24. package/skills/testing/SKILL.md +30 -0
  25. package/skills/webapp-testing/SKILL.md +27 -0
  26. package/skills/xlsx/SKILL.md +28 -0
  27. package/src/agent.js +472 -0
  28. package/src/audit.js +68 -0
  29. package/src/autostart.js +74 -0
  30. package/src/batch.js +182 -0
  31. package/src/cachestats.js +215 -0
  32. package/src/cli.js +1099 -0
  33. package/src/commands/key.js +75 -0
  34. package/src/commands/schedule.js +176 -0
  35. package/src/commands/skill.js +168 -0
  36. package/src/commands/sync.js +222 -0
  37. package/src/commands/update.js +157 -0
  38. package/src/commands/workspace.js +109 -0
  39. package/src/compact.js +112 -0
  40. package/src/config.js +134 -0
  41. package/src/context.js +86 -0
  42. package/src/cost-guard.js +58 -0
  43. package/src/credentials.js +69 -0
  44. package/src/hooks.js +123 -0
  45. package/src/index.js +42 -0
  46. package/src/mcp-presets.js +78 -0
  47. package/src/mcp.js +284 -0
  48. package/src/memory.js +242 -0
  49. package/src/model-discovery.js +153 -0
  50. package/src/models.js +186 -0
  51. package/src/notify.js +38 -0
  52. package/src/permissions.js +83 -0
  53. package/src/pricing.js +156 -0
  54. package/src/prompts.js +58 -0
  55. package/src/providers/index.js +135 -0
  56. package/src/providers/openai-compatible.js +171 -0
  57. package/src/routing.js +126 -0
  58. package/src/schedule.js +434 -0
  59. package/src/session-index.js +122 -0
  60. package/src/session.js +131 -0
  61. package/src/skill-lib.js +350 -0
  62. package/src/skill-registry.js +165 -0
  63. package/src/skills.js +135 -0
  64. package/src/sync-server.js +564 -0
  65. package/src/sync.js +479 -0
  66. package/src/tasks.js +126 -0
  67. package/src/titles.js +75 -0
  68. package/src/tokenizer.js +287 -0
  69. package/src/tools/bash.js +165 -0
  70. package/src/tools/fs-tools.js +346 -0
  71. package/src/tools/index.js +287 -0
  72. package/src/ui.js +643 -0
  73. package/src/update.js +222 -0
  74. package/src/web/attachments.js +49 -0
  75. package/src/web/index.html +993 -0
  76. package/src/web/server.js +1173 -0
  77. package/src/web/web-io.js +107 -0
  78. package/src/workspace.js +157 -0
@@ -0,0 +1,75 @@
1
+ // 命令族:mingdao key(自 cli.js 拆出,评估 P0-1 拆包)
2
+ import { createIO, style, C } from '../ui.js';
3
+ import { ensureHome } from '../config.js';
4
+ import { setStoredKey, removeStoredKey, credentialsPath, loadCredentials, maskKey } from '../credentials.js';
5
+ import { PROVIDERS } from '../models.js';
6
+
7
+ export async function handleKey(cmd, args) {
8
+ const io = createIO();
9
+ try {
10
+ const sub = args[0] || 'status';
11
+ const target = args[1];
12
+ if (sub === 'status') {
13
+ ensureHome();
14
+ io.print(style(`本地凭证库:${credentialsPath()}`, C.bold));
15
+ const creds = loadCredentials();
16
+ const names = Object.keys(creds);
17
+ if (!names.length) io.print(' (空)');
18
+ for (const n of names) {
19
+ io.print(style(` ${n}: ${maskKey(creds[n])}`, C.dim));
20
+ }
21
+ for (const [k, pp] of Object.entries(PROVIDERS)) {
22
+ if (pp.envKey && process.env[pp.envKey]) {
23
+ io.print(style(` 环境变量 ${pp.envKey}: 已设置(未读取内容)`, C.dim));
24
+ }
25
+ }
26
+ if (process.env.MINGDAO_API_KEY) {
27
+ io.print(style(' 环境变量 MINGDAO_API_KEY: 已设置(未读取内容)', C.dim));
28
+ }
29
+ io.print('提示:密钥只存本机凭证库,config.json 可安全分享/提交仓库。');
30
+ } else if (sub === 'set') {
31
+ if (!target) {
32
+ io.print('用法:mingdao key set <服务商名> [key]');
33
+ return true;
34
+ }
35
+ let key = args[2] || '';
36
+ if (!key) {
37
+ if (!io.isTTY) {
38
+ io.print('非交互环境请直接传参:mingdao key set <服务商名> <key>');
39
+ return true;
40
+ }
41
+ key = await io.ask(`输入 ${target} 的 API Key(隐藏输入):`, { hidden: true });
42
+ }
43
+ if (!key) {
44
+ io.print('未输入,已取消。');
45
+ return true;
46
+ }
47
+ setStoredKey(target, key);
48
+ io.print(`已保存 ${target} → ${maskKey(key)}(${credentialsPath()},权限 600)。`);
49
+ io.print('注意:密钥不会写入 config.json,也不会进入项目仓库。');
50
+ } else if (sub === 'remove') {
51
+ if (!target) {
52
+ io.print('用法:mingdao key remove <服务商名>');
53
+ return true;
54
+ }
55
+ removeStoredKey(target);
56
+ io.print(`已移除 ${target} 的本地凭证。`);
57
+ } else if (sub === 'import') {
58
+ ensureHome();
59
+ let count = 0;
60
+ for (const [k, pp] of Object.entries(PROVIDERS)) {
61
+ if (pp.envKey && process.env[pp.envKey]) {
62
+ setStoredKey(k, process.env[pp.envKey]);
63
+ io.print(`已导入 ${k}(来自环境变量 ${pp.envKey})。`);
64
+ count += 1;
65
+ }
66
+ }
67
+ if (!count) io.print('没有可导入的环境变量(如 DEEPSEEK_API_KEY)。');
68
+ } else {
69
+ io.print('用法:mingdao key [status|set <服务商> [key]|remove <服务商>|import]');
70
+ }
71
+ } finally {
72
+ io.close();
73
+ }
74
+ return true;
75
+ }
@@ -0,0 +1,176 @@
1
+ // 命令族:mingdao tasks / schedule(自 cli.js 拆出,评估 P0-1 拆包)
2
+ import {
3
+ listSchedules,
4
+ addSchedule,
5
+ removeSchedule,
6
+ pauseSchedule,
7
+ resumeSchedule,
8
+ chainSchedules,
9
+ reconcileSchedules,
10
+ formatScheduleRow,
11
+ daemonAlive,
12
+ stopDaemon,
13
+ } from '../schedule.js';
14
+ import { listTasks, killTask, formatTaskRow } from '../tasks.js';
15
+ import { ensureHome } from '../config.js';
16
+
17
+ function printTasks(home) {
18
+ const tasks = listTasks(home);
19
+ if (!tasks.length) {
20
+ console.log('暂无任务。启动:mingdao run "<任务>"');
21
+ return;
22
+ }
23
+ console.log(`任务面板(共 ${tasks.length} 个,新→旧)`);
24
+ for (const t of tasks.slice(0, 20)) console.log(' ' + formatTaskRow(t));
25
+ const running = tasks.filter((t) => t.status === 'running').length;
26
+ console.log(running ? `\n${running} 个运行中 · mingdao tasks watch 实时刷新 · kill <id> 停止` : '\n无运行中任务');
27
+ }
28
+
29
+ async function watchTasks(home) {
30
+ if (!process.stdout.isTTY) {
31
+ printTasks(home);
32
+ return;
33
+ }
34
+ for (;;) {
35
+ const tasks = listTasks(home);
36
+ console.log('\n\x1b[2J\x1b[H' + `任务面板 ${new Date().toLocaleTimeString()}`);
37
+ if (!tasks.length) console.log(' 暂无任务。启动:mingdao run "<任务>"');
38
+ for (const t of tasks.slice(0, 20)) console.log(' ' + formatTaskRow(t));
39
+ const running = tasks.filter((t) => t.status === 'running');
40
+ if (!running.length) {
41
+ console.log('\n全部任务已结束');
42
+ return;
43
+ }
44
+ await new Promise((r) => setTimeout(r, 2000));
45
+ }
46
+ }
47
+
48
+ export async function handleTasks(cmd, args) {
49
+ const home0 = ensureHome();
50
+ reconcileSchedules(home0);
51
+ const sub = args[0];
52
+ if (sub === 'kill') {
53
+ const id = args[1];
54
+ if (!id) {
55
+ console.log('用法:mingdao tasks kill <id>');
56
+ process.exitCode = 1;
57
+ return true;
58
+ }
59
+ console.log(killTask(home0, id) ? `已请求停止任务 ${id}` : '任务不存在');
60
+ return true;
61
+ }
62
+ if (sub === 'watch') {
63
+ await watchTasks(home0);
64
+ return true;
65
+ }
66
+ printTasks(home0);
67
+ return true;
68
+ }
69
+
70
+ export async function handleSchedule(cmd, args) {
71
+ const home0 = ensureHome();
72
+ reconcileSchedules(home0);
73
+ const sub = args[0];
74
+ const rest = args.slice(1);
75
+ if (sub === 'daemon') {
76
+ if (rest[0] === 'stop') {
77
+ stopDaemon(home0);
78
+ console.log('✓ 调度守护进程已停止(有待执行任务时任意 schedule 命令会自动再拉起)');
79
+ } else {
80
+ console.log(daemonAlive(home0) ? '调度守护进程:运行中 ✓' : '调度守护进程:未运行(有任务时自动拉起)');
81
+ }
82
+ return true;
83
+ }
84
+ if (sub === 'add') {
85
+ let question = '';
86
+ let at = null;
87
+ let every = null;
88
+ let anchor = null;
89
+ let after = [];
90
+ let permission = null;
91
+ let model = null;
92
+ let offpeak = false;
93
+ for (let i = 0; i < rest.length; i++) {
94
+ const a = rest[i];
95
+ if (a === '--at') at = rest[++i];
96
+ else if (a === '--every') every = rest[++i];
97
+ else if (a === '--anchor') anchor = rest[++i];
98
+ else if (a === '--after') after = String(rest[++i]).split(',').map((x) => x.trim()).filter(Boolean);
99
+ else if (a === '--permission') permission = rest[++i];
100
+ else if (a === '--model') model = rest[++i];
101
+ else if (a === '--offpeak') offpeak = true;
102
+ else if (question === '') question = a;
103
+ }
104
+ if (!question) {
105
+ console.log('用法:mingdao schedule add "<任务>" [--at "YYYY-MM-DD HH:MM" | --every 2h [--anchor 09:00]] [--after 任务ID,...] [--permission auto] [--model 名] [--offpeak 高峰顺延至 14:00 后]');
106
+ process.exitCode = 1;
107
+ return true;
108
+ }
109
+ const r = addSchedule(home0, question, { at, every, after, permission, model, cwd: process.cwd(), anchor, offpeak });
110
+ if (r.error) {
111
+ console.log('[错误] ' + r.error);
112
+ process.exitCode = 1;
113
+ return true;
114
+ }
115
+ console.log(`✓ 调度任务已创建 ${r.id}`);
116
+ console.log(` 查看:mingdao schedule list · 删除:mingdao schedule remove ${r.id}`);
117
+ return true;
118
+ }
119
+ if (sub === 'list') {
120
+ const jobs = listSchedules(home0);
121
+ if (!jobs.length) {
122
+ console.log('暂无调度任务。创建:mingdao schedule add "<任务>" --at "2026-08-21 09:00" 或 --every 2h');
123
+ return true;
124
+ }
125
+ console.log(`调度队列(共 ${jobs.length} 个,按下次运行排序)`);
126
+ for (const j of jobs.slice(0, 30)) console.log(' ' + formatScheduleRow(j));
127
+ return true;
128
+ }
129
+ if (sub === 'remove') {
130
+ const id = rest[0];
131
+ if (!id) {
132
+ console.log('用法:mingdao schedule remove <id>');
133
+ process.exitCode = 1;
134
+ return true;
135
+ }
136
+ console.log(removeSchedule(home0, id) ? `已删除调度任务 ${id}` : '任务不存在');
137
+ return true;
138
+ }
139
+ if (sub === 'pause') {
140
+ const id = rest[0];
141
+ if (!id) {
142
+ console.log('用法:mingdao schedule pause <id>');
143
+ process.exitCode = 1;
144
+ return true;
145
+ }
146
+ console.log(pauseSchedule(home0, id) ? `已暂停 ${id}(mingdao schedule resume ${id} 恢复)` : '任务不存在或不可暂停');
147
+ return true;
148
+ }
149
+ if (sub === 'resume') {
150
+ const id = rest[0];
151
+ if (!id) {
152
+ console.log('用法:mingdao schedule resume <id>');
153
+ process.exitCode = 1;
154
+ return true;
155
+ }
156
+ console.log(resumeSchedule(home0, id) ? `已恢复 ${id}` : '任务不存在或未暂停');
157
+ return true;
158
+ }
159
+ if (sub === 'chain') {
160
+ if (rest.length < 2) {
161
+ console.log('用法:mingdao schedule chain "任务A" "任务B" "任务C"(按顺序执行,后者依赖前者成功)');
162
+ process.exitCode = 1;
163
+ return true;
164
+ }
165
+ const r = chainSchedules(home0, rest);
166
+ if (r.error) {
167
+ console.log('[错误] ' + r.error);
168
+ process.exitCode = 1;
169
+ return true;
170
+ }
171
+ console.log(`✓ 链式队列已创建:${r.ids.join(' → ')}`);
172
+ return true;
173
+ }
174
+ console.log('用法:mingdao schedule add|list|remove|pause|resume|chain');
175
+ return true;
176
+ }
@@ -0,0 +1,168 @@
1
+ // 命令族:mingdao skill / web / sessions(自 cli.js 拆出,评估 P0-1 拆包)
2
+ import { listSkills, tamperedSkillNames } from '../skills.js';
3
+ import { libraryList, searchLibrary, installSkill, uninstallSkill, reinstallSkill, trustSkill } from '../skill-lib.js';
4
+ import { searchRegistry } from '../skill-registry.js';
5
+ import { loadConfig } from '../config.js';
6
+ import { ensureHome } from '../config.js';
7
+ import { searchSessions, relativeTime } from '../session.js';
8
+ import { runWebServer } from '../web/server.js';
9
+
10
+ export async function handleSkill(cmd, args) {
11
+ const sub = args[0] || 'list';
12
+ const arg = args[1];
13
+ if (sub === 'search') {
14
+ const local = searchLibrary(arg || '');
15
+ const remote = await searchRegistry(arg || '');
16
+ const localNames = new Set(local.map((s) => s.name));
17
+ const remoteOnly = remote.skills ? remote.skills.filter((s) => !localNames.has(s.name)) : [];
18
+ console.log(
19
+ `技能库匹配:内置 ${local.length}${remote.error ? '' : ` + 线上 ${remoteOnly.length}`} · 安装:mingdao skill install <名称>`
20
+ );
21
+ for (const s of local) console.log(` ${s.name.padEnd(18)} ${s.description}${s.installed ? '(已安装)' : ''} [内置]`);
22
+ if (remote.error) {
23
+ console.log(` ✗ 线上 registry 不可达:${remote.error}`);
24
+ } else {
25
+ for (const s of remoteOnly) console.log(` ${s.name.padEnd(18)} ${s.description}${s.installed ? '(已安装)' : ''} [线上]`);
26
+ if (remote.stale) console.log(' (线上索引来自本地缓存,已过期)');
27
+ }
28
+ return true;
29
+ }
30
+ if (sub === 'install') {
31
+ const r = await installSkill(arg);
32
+ if (r.error) {
33
+ console.log('[错误] ' + r.error);
34
+ process.exitCode = 1;
35
+ return true;
36
+ }
37
+ if (r.names) {
38
+ console.log(`✓ 已从 git 仓库安装 ${r.names.length} 个技能:${r.names.join(', ')}`);
39
+ } else {
40
+ const srcLabel = r.host ? '(线上 registry)' : '';
41
+ console.log(`✓ 已安装技能 ${r.name}${srcLabel} → ~/.mingdao/skills/${r.name}/(可编辑/删除,下次会话生效)`);
42
+ }
43
+ return true;
44
+ }
45
+ if (sub === 'uninstall') {
46
+ if (!arg) {
47
+ console.log('用法:mingdao skill uninstall <名称>');
48
+ process.exitCode = 1;
49
+ return true;
50
+ }
51
+ const r = uninstallSkill(arg);
52
+ if (r.error) {
53
+ console.log('[错误] ' + r.error);
54
+ process.exitCode = 1;
55
+ return true;
56
+ }
57
+ console.log(`✓ 已卸载 ${r.name}(内置同名技能如有会自动重新可见)`);
58
+ return true;
59
+ }
60
+ if (sub === 'update') {
61
+ if (!arg) {
62
+ console.log('用法:mingdao skill update <名称>');
63
+ process.exitCode = 1;
64
+ return true;
65
+ }
66
+ const r = await reinstallSkill(arg);
67
+ if (r.error) {
68
+ console.log('[错误] ' + r.error);
69
+ process.exitCode = 1;
70
+ return true;
71
+ }
72
+ console.log(`✓ 已更新技能 ${r.name}`);
73
+ return true;
74
+ }
75
+ if (sub === 'trust') {
76
+ if (!arg) {
77
+ console.log('用法:mingdao skill trust <名称>(编辑过 registry/库安装的技能后,重新记录内容指纹)');
78
+ process.exitCode = 1;
79
+ return true;
80
+ }
81
+ const r = trustSkill(arg);
82
+ if (r.error) {
83
+ console.log('[错误] ' + r.error);
84
+ process.exitCode = 1;
85
+ return true;
86
+ }
87
+ console.log(`✓ 已信任技能 ${r.name} 的当前内容(指纹 ${r.sha256}…)`);
88
+ return true;
89
+ }
90
+ const skills = listSkills(process.cwd());
91
+ console.log(`已安装技能(${skills.length})· 三级来源:用户级 > 项目级 > 内置`);
92
+ for (const s of skills) {
93
+ const label = s.source === 'user' ? '(用户级)' : s.source === 'project' ? '(项目级)' : '(内置)';
94
+ console.log(` ${s.name.padEnd(18)} ${s.description || ''}${label}`);
95
+ }
96
+ const tampered = tamperedSkillNames(process.cwd());
97
+ for (const t of tampered) {
98
+ console.log(` ⚠ ${t.name.padEnd(18)} 内容与安装时不一致,已拒绝加载——确认是你改的就执行 mingdao skill trust ${t.name},否则 mingdao skill uninstall ${t.name} 后重装`);
99
+ }
100
+ const lib = libraryList();
101
+ console.log(`\n技能库共 ${lib.length} 个可安装技能:mingdao skill search [关键词] 搜索,mingdao skill install <名称> 安装`);
102
+ return true;
103
+ }
104
+
105
+ // WebUI:mingdao web [端口] [--auth-token <令牌>](评估 P3-1:参数结构不合法的按提问处理)
106
+ export async function handleWeb(cmd, args) {
107
+ let portIndex = -1;
108
+ let tokenSeen = false;
109
+ for (let i = 0; i < args.length; i++) {
110
+ const a = args[i];
111
+ if (a === '--auth-token') {
112
+ if (tokenSeen || i + 1 >= args.length) return false;
113
+ tokenSeen = true;
114
+ i += 1;
115
+ continue;
116
+ }
117
+ if (a.startsWith('--auth-token=')) {
118
+ if (tokenSeen) return false;
119
+ tokenSeen = true;
120
+ continue;
121
+ }
122
+ if (/^\d+$/.test(a) && portIndex === -1) {
123
+ portIndex = i; // 审计 P2-11:记录端口实际位置,而非默认读首参
124
+ continue;
125
+ }
126
+ return false;
127
+ }
128
+ const cfg0 = loadConfig();
129
+ const portArg = portIndex !== -1 ? Number(args[portIndex]) : NaN;
130
+ const port = Number.isFinite(portArg) && portArg > 0 ? portArg : cfg0?.web?.port || 3820;
131
+ const host = cfg0?.web?.host || '127.0.0.1';
132
+ // 访问令牌优先级:--auth-token 参数 > 环境变量 MINGDAO_WEB_TOKEN > config.json 的 web.token
133
+ let authToken = process.env.MINGDAO_WEB_TOKEN || cfg0?.web?.token || undefined;
134
+ const atIdx = args.findIndex((a) => typeof a === 'string' && a.startsWith('--auth-token'));
135
+ if (atIdx !== -1) {
136
+ const raw = args[atIdx];
137
+ authToken = raw.includes('=') ? raw.slice(raw.indexOf('=') + 1) : args[atIdx + 1];
138
+ if (!authToken) {
139
+ console.log('用法:mingdao web [端口] [--auth-token <令牌>]');
140
+ process.exitCode = 1;
141
+ return true;
142
+ }
143
+ }
144
+ await runWebServer({ host, port, authToken });
145
+ return true;
146
+ }
147
+
148
+ // 会话检索:mingdao sessions search <关键词>
149
+ export async function handleSessions(cmd, args) {
150
+ if (args[0] !== 'search') return false;
151
+ const kw = args.slice(1).join(' ').trim();
152
+ if (!kw) {
153
+ console.log('用法:mingdao sessions search <关键词>');
154
+ process.exitCode = 1;
155
+ return true;
156
+ }
157
+ const home0 = ensureHome();
158
+ const hits = searchSessions(home0, kw);
159
+ if (!hits.length) console.log(`未找到包含「${kw}」的会话。`);
160
+ else {
161
+ console.log(`找到 ${hits.length} 个会话:`);
162
+ for (const h of hits) {
163
+ console.log(` ${h.name}(${relativeTime(h.mtime)})\n ${h.snippet}`);
164
+ }
165
+ console.log(`\n恢复:mingdao --resume(选择器中可见全部会话)`);
166
+ }
167
+ return true;
168
+ }
@@ -0,0 +1,222 @@
1
+ // 命令族:mingdao sync(自 cli.js 拆出,评估 P0-1 拆包)
2
+ import readline from 'node:readline';
3
+ import {
4
+ syncStatus,
5
+ syncLogin,
6
+ syncLogout,
7
+ syncPush,
8
+ syncPull,
9
+ syncRemoteList,
10
+ syncChangePassword,
11
+ syncShareCreate,
12
+ syncShareList,
13
+ syncShareAccept,
14
+ syncShareRevoke,
15
+ listSyncConflicts,
16
+ resolveSyncConflict,
17
+ } from '../sync.js';
18
+
19
+ async function askHidden(question) {
20
+ return new Promise((resolve) => {
21
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
22
+ const orig = rl._writeToOutput;
23
+ rl._writeToOutput = () => {};
24
+ rl.question(question, (a) => {
25
+ if (typeof orig === 'function') rl._writeToOutput = orig;
26
+ rl.close();
27
+ resolve(a.trim());
28
+ });
29
+ });
30
+ }
31
+
32
+ export async function handleSync(cmd, args) {
33
+ const sub = args[0] || 'status';
34
+ if (sub === 'login') {
35
+ const username = args[1];
36
+ if (!username) {
37
+ console.log('用法:mingdao sync login <用户名> [密码] [服务器地址](地址默认取已配置项)');
38
+ process.exitCode = 1;
39
+ return true;
40
+ }
41
+ const s0 = syncStatus();
42
+ const url = args[3] || s0.url;
43
+ if (!url) {
44
+ console.log('缺少服务器地址:mingdao sync login <用户名> [密码] <http(s)://地址>');
45
+ process.exitCode = 1;
46
+ return true;
47
+ }
48
+ let password = args[2];
49
+ if (!password) password = await askHidden('密码(至少 8 位):');
50
+ const insecureFlag = args[4] === '--insecure' || args[5] === '--insecure';
51
+ const r = await syncLogin({ url, username, password, deviceName: args[4] === '--insecure' ? undefined : args[4], insecure: insecureFlag });
52
+ if (r.error) {
53
+ console.log('[错误] ' + r.error);
54
+ process.exitCode = 1;
55
+ return true;
56
+ }
57
+ if (insecureFlag) {
58
+ console.log(' 已启用 insecure(跳过证书校验,正式证书就绪后请在 config.sync 删除 insecure 字段)');
59
+ }
60
+ console.log(`✓ 已登录 ${r.username}(设备 ${r.deviceName})→ ${r.url}`);
61
+ console.log(' 推送:mingdao sync push · 拉取:mingdao sync pull · 会话结束自动同步(config.sync.auto)');
62
+ return true;
63
+ }
64
+ if (sub === 'logout') {
65
+ syncLogout();
66
+ console.log('✓ 已退出同步(配置保留,凭证已清除)');
67
+ return true;
68
+ }
69
+ if (sub === 'passwd') {
70
+ const newPassword = args[1];
71
+ if (!newPassword) {
72
+ console.log('用法:mingdao sync passwd <新密码>(将提示输入旧密码)');
73
+ process.exitCode = 1;
74
+ return true;
75
+ }
76
+ const oldPassword = await askHidden('旧密码:');
77
+ const r = await syncChangePassword({ oldPassword, newPassword });
78
+ if (r.error) {
79
+ console.log('[错误] ' + r.error);
80
+ process.exitCode = 1;
81
+ return true;
82
+ }
83
+ console.log('✓ 密码已修改(其他设备下次登录用新密码)');
84
+ return true;
85
+ }
86
+ if (sub === 'share') {
87
+ const name = args[1];
88
+ if (!name) {
89
+ console.log('用法:mingdao sync share <会话文件名>(列出:mingdao sync shares)');
90
+ process.exitCode = 1;
91
+ return true;
92
+ }
93
+ const r = await syncShareCreate(name);
94
+ if (r.error) {
95
+ console.log('[错误] ' + r.error);
96
+ process.exitCode = 1;
97
+ return true;
98
+ }
99
+ console.log(`✓ 已创建分享(会话 ${r.name})`);
100
+ console.log(` 分享码:${r.shareId}`);
101
+ console.log(` 对方接受:mingdao sync accept ${r.shareId}`);
102
+ return true;
103
+ }
104
+ if (sub === 'shares') {
105
+ const r = await syncShareList();
106
+ if (r.error) {
107
+ console.log('[错误] ' + r.error);
108
+ process.exitCode = 1;
109
+ return true;
110
+ }
111
+ console.log(`我分享的(${r.mine.length}):`);
112
+ for (const s of r.mine) console.log(` ${s.shareId.padEnd(12)} ${s.name} · 被接受 ${s.pulls} 次`);
113
+ console.log(`我接受的(${r.accepted.length}):`);
114
+ for (const s of r.accepted) console.log(` ${s.shareId.padEnd(12)} ${s.owner} 的 ${s.name} → 本地 ${s.savedAs}`);
115
+ if (!r.mine.length && !r.accepted.length) console.log(' 暂无分享');
116
+ return true;
117
+ }
118
+ if (sub === 'accept') {
119
+ const shareId = args[1];
120
+ if (!shareId) {
121
+ console.log('用法:mingdao sync accept <分享码>');
122
+ process.exitCode = 1;
123
+ return true;
124
+ }
125
+ const r = await syncShareAccept(shareId);
126
+ if (r.error) {
127
+ console.log('[错误] ' + r.error);
128
+ process.exitCode = 1;
129
+ return true;
130
+ }
131
+ console.log(`✓ 已接受分享 → 本地会话 ${r.savedAs}${r.conflict ? '(与你已有的同名会话不同,已另存副本)' : ''}`);
132
+ return true;
133
+ }
134
+ if (sub === 'unshare') {
135
+ const shareId = args[1];
136
+ if (!shareId) {
137
+ console.log('用法:mingdao sync unshare <分享码>');
138
+ process.exitCode = 1;
139
+ return true;
140
+ }
141
+ const r = await syncShareRevoke(shareId);
142
+ if (r.error) {
143
+ console.log('[错误] ' + r.error);
144
+ process.exitCode = 1;
145
+ return true;
146
+ }
147
+ console.log(`✓ 已撤销分享 ${shareId}(已接受者保留副本)`);
148
+ return true;
149
+ }
150
+ if (sub === 'conflicts') {
151
+ const list = listSyncConflicts();
152
+ if (!list.length) {
153
+ console.log('暂无冲突备份');
154
+ return true;
155
+ }
156
+ console.log(`冲突备份(${list.length} 个会话)· 解决:mingdao sync conflict-resolve <会话名> local|remote|both`);
157
+ for (const c of list) {
158
+ const localLabel = c.localExists ? '本地有' : '本地无';
159
+ const newest = c.entries[0];
160
+ console.log(` ${c.base.padEnd(44)} ${localLabel} · 备份 ${c.entries.length} 个(最新 ${newest.side}-${newest.ts})`);
161
+ }
162
+ return true;
163
+ }
164
+ if (sub === 'conflict-resolve') {
165
+ const base = args[1];
166
+ const choice = args[2];
167
+ if (!base || !['local', 'remote', 'both'].includes(choice)) {
168
+ console.log('用法:mingdao sync conflict-resolve <会话文件名> local|remote|both');
169
+ console.log(' local 保留本地,删除备份 · remote 采用远端版本覆盖本地 · both 两者都保留(备份转正)');
170
+ process.exitCode = 1;
171
+ return true;
172
+ }
173
+ const r = resolveSyncConflict(base, choice);
174
+ if (r.error) {
175
+ console.log('[错误] ' + r.error);
176
+ process.exitCode = 1;
177
+ return true;
178
+ }
179
+ console.log(`✓ 已解决:${r.base} → ${choice === 'local' ? '保留本地' : choice === 'remote' ? '采用 ' + r.applied : '保留两者(' + r.kept + ')'}`);
180
+ return true;
181
+ }
182
+ if (sub === 'push') {
183
+ const r = await syncPush(args[1]);
184
+ if (r.error) {
185
+ console.log('[错误] ' + r.error);
186
+ process.exitCode = 1;
187
+ return true;
188
+ }
189
+ console.log(`✓ 已推送 ${r.pushed.length} 个会话${r.skipped?.length ? `(跳过 ${r.skipped.length} 个空会话)` : ''}${r.conflicts.length ? `,远端 ${r.conflicts.length} 个不同版本已备份为 .server-*(本地覆盖远端)` : ''}`);
190
+ return true;
191
+ }
192
+ if (sub === 'pull') {
193
+ const r = await syncPull(args[1]);
194
+ if (r.error) {
195
+ console.log('[错误] ' + r.error);
196
+ process.exitCode = 1;
197
+ return true;
198
+ }
199
+ console.log(`✓ 已拉取 ${r.pulled.length} 个会话${r.conflicts.length ? `,${r.conflicts.length} 个与本地不同:远端内容已存为 .remote-*(本地保留)` : ''}`);
200
+ return true;
201
+ }
202
+ const st = syncStatus();
203
+ if (!st.configured) {
204
+ console.log('未配置云同步。登录:mingdao sync login <用户名> [密码] <http(s)://服务器地址>');
205
+ return true;
206
+ }
207
+ console.log(`同步服务器 ${st.url}`);
208
+ console.log(`账号 ${st.username || '(未登录)'} · 设备 ${st.deviceName || '(未登录)'}`);
209
+ console.log(`状态 ${st.loggedIn ? '✓ 已登录' : '✗ 未登录'} · 自动同步 ${st.auto ? '开' : '关'}`);
210
+ if (st.loggedIn) {
211
+ const remote = await syncRemoteList();
212
+ if (remote.error) {
213
+ console.log(`远端会话 ${remote.error}`);
214
+ } else {
215
+ console.log(`远端会话 ${remote.sessions.length} 个`);
216
+ for (const s of remote.sessions.slice(0, 10)) {
217
+ console.log(` ${s.name.padEnd(42)} ${new Date(s.mtime).toLocaleString()} · ${(s.size / 1024).toFixed(1)}KB`);
218
+ }
219
+ }
220
+ }
221
+ return true;
222
+ }