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
package/src/update.js ADDED
@@ -0,0 +1,222 @@
1
+ // 自更新模块(零运行时依赖:仅 node:child_process 调用 git)。
2
+ // 安装形态:仓库 + 全局符号链接(mingdao 命令即指向 repo/src/cli.js,仓库更新 = 命令更新)。
3
+ // mingdao update 拉取最新远端 main → 跑冒烟测试 → 失败自动回滚
4
+ // mingdao update --check 只 fetch 对比版本,不改动工作区
5
+ // mingdao rollback 回滚到上次成功 update 之前的提交(~/.mingdao/update-state.json 记录)
6
+ // npm 安装形态(无 .git)给出对应提示。
7
+
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { mingdaoHome, ensureHome } from './config.js';
13
+
14
+ function git(args, cwd) {
15
+ const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 120000 });
16
+ return {
17
+ ok: r.status === 0 && !r.error,
18
+ out: String(r.stdout || '').trim(),
19
+ err: String(r.stderr || '').trim(),
20
+ error: r.error,
21
+ };
22
+ }
23
+
24
+ // 定位仓库根:全局安装 = 符号链接到仓库,realpath 拿到真实路径后向上找 .git
25
+ export function findRepoRoot() {
26
+ try {
27
+ let dir = path.dirname(fs.realpathSync(process.argv[1] || fileURLToPath(import.meta.url)));
28
+ for (let i = 0; i < 12; i++) {
29
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
30
+ const parent = path.dirname(dir);
31
+ if (parent === dir) break;
32
+ dir = parent;
33
+ }
34
+ } catch {}
35
+ return null;
36
+ }
37
+
38
+ function resolveRepo(override) {
39
+ return override || findRepoRoot();
40
+ }
41
+
42
+ function versionAt(repo, ref = 'HEAD') {
43
+ const r = git(['show', `${ref}:package.json`], repo);
44
+ if (!r.ok) return null;
45
+ try {
46
+ return JSON.parse(r.out).version || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function compareVersions(a, b) {
53
+ const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
54
+ const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
55
+ for (let i = 0; i < 3; i++) {
56
+ const x = pa[i] || 0;
57
+ const y = pb[i] || 0;
58
+ if (x !== y) return x > y ? 1 : -1;
59
+ }
60
+ return 0;
61
+ }
62
+
63
+ function stateFile() {
64
+ return path.join(mingdaoHome(), 'update-state.json');
65
+ }
66
+ function readState() {
67
+ try {
68
+ return JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ function writeState(s) {
74
+ ensureHome();
75
+ fs.writeFileSync(stateFile(), JSON.stringify(s, null, 2) + '\n', { mode: 0o600 });
76
+ }
77
+
78
+ // 直接向远端指定分支要版本(评估 D6):fetch 到 FETCH_HEAD 再读 package.json,
79
+ // 不依赖本地 refs 落盘——部分 Windows git 环境存在「fetch 成功但 refs 不更新」的异常,
80
+ // 按本地 refs 判断会误报「已是最新」。ls-remote 思路的等价实现。
81
+ function remoteVersion(repo, remote, branch) {
82
+ const r = git(['fetch', '--quiet', remote, branch], repo);
83
+ if (!r.ok) return null;
84
+ return versionAt(repo, 'FETCH_HEAD');
85
+ }
86
+
87
+ function listRemotes(repo) {
88
+ const r = git(['remote'], repo);
89
+ if (!r.ok) return [];
90
+ return r.out.split('\n').filter(Boolean);
91
+ }
92
+
93
+ // 各远端(main/master 任一存在)中版本号最大的(多镜像部署:github/gitee/gitcode 谁新跟谁)
94
+ function newestRemote(repo) {
95
+ let latest = '';
96
+ let ref = '';
97
+ for (const remote of listRemotes(repo)) {
98
+ for (const branch of ['main', 'master']) {
99
+ const v = remoteVersion(repo, remote, branch);
100
+ if (v && compareVersions(v, latest) > 0) {
101
+ latest = v;
102
+ ref = `${remote}/${branch}`;
103
+ }
104
+ }
105
+ }
106
+ return { version: latest, ref };
107
+ }
108
+
109
+ const NPM_HINT =
110
+ '✖ 未找到 git 仓库(npm 或源码包安装形态):npm 安装用 `npm update -g mingdao-harness`;' +
111
+ '源码包安装请重新运行安装脚本(install.sh);或从任意平台仓库克隆后 `npm link`:' +
112
+ 'https://gitee.com/MingDaoTCM/MingDao-harness · https://gitcode.com/MingDaoTCM/MingDao-Harness · https://github.com/MingDaoTCM/MingDao-Harness';
113
+
114
+ export async function updateCheck({ repo } = {}) {
115
+ const lines = [];
116
+ const root = resolveRepo(repo);
117
+ if (!root) return { ok: false, lines: [NPM_HINT] };
118
+ const local = versionAt(root) || '未知';
119
+ const newest = newestRemote(root);
120
+ if (!newest.version) {
121
+ return { ok: false, lines: [`当前版本 v${local}`, '✖ 无法连接远端(git fetch 全部失败),请检查网络。'] };
122
+ }
123
+ if (compareVersions(newest.version, local) <= 0) {
124
+ return { ok: true, behind: false, lines: [`当前已是最新版本 v${local} ✓`, '(如需升级:mingdao update)'] };
125
+ }
126
+ return {
127
+ ok: true,
128
+ behind: true,
129
+ lines: [`当前版本 v${local}`, `发现新版本 v${newest.version}(${newest.ref})`, '执行 mingdao update 一键升级(升级后自动跑冒烟测试,失败自动回滚)。'],
130
+ };
131
+ }
132
+
133
+ export async function mingdaoUpdate({ repo } = {}) {
134
+ const lines = [];
135
+ const root = resolveRepo(repo);
136
+ if (!root) return { ok: false, lines: [NPM_HINT] };
137
+ const local = versionAt(root) || '未知';
138
+ // 工作区必须干净:升级不动用户未提交的改动(--ff-only 也会因此拒绝)
139
+ const st = git(['status', '--porcelain'], root);
140
+ if (!st.ok) return { ok: false, lines: ['✖ git status 失败:' + (st.err || st.error?.message || '未知错误')] };
141
+ if (st.out) {
142
+ return {
143
+ ok: false,
144
+ lines: ['✖ 仓库有未提交改动,请先提交/暂存后再升级:', ...st.out.split('\n').slice(0, 5).map((l) => ' ' + l)],
145
+ };
146
+ }
147
+ const prev = git(['rev-parse', 'HEAD'], root);
148
+ if (!prev.ok) return { ok: false, lines: ['✖ 无法读取当前提交(git rev-parse HEAD)'] };
149
+ const newest = newestRemote(root);
150
+ if (!newest.version) return { ok: false, lines: ['✖ git fetch 失败:无法连接远端'] };
151
+ if (compareVersions(newest.version, local) <= 0) {
152
+ return { ok: true, lines: [`当前已是最新版本 v${local} ✓(远端最新 v${newest.version})`] };
153
+ }
154
+ // 从版本号最新的远端快进拉取(远程名/分支:优先当前分支名,回落 main)
155
+ const remoteName = newest.ref.split('/')[0];
156
+ const curBranch = git(['rev-parse', '--abbrev-ref', 'HEAD'], root);
157
+ const branch = ['main', 'master'].includes(curBranch.out) ? curBranch.out : 'main';
158
+ const pull = git(['pull', '--ff-only', '--quiet', remoteName, branch], root);
159
+ if (!pull.ok) {
160
+ return {
161
+ ok: false,
162
+ lines: [`✖ 升级失败(git pull --ff-only ${remoteName} ${branch}):${pull.err || '本地分支与远端分叉,请先手动处理'}`, '未改动任何文件。'],
163
+ };
164
+ }
165
+ const newVer = versionAt(root) || '未知';
166
+ // 升级后冒烟验证:失败自动回滚到升级前提交,绝不让坏版本常驻
167
+ const smoke = spawnSync(process.execPath, [path.join(root, 'test', 'smoke.js')], { cwd: root, encoding: 'utf8', timeout: 300000 });
168
+ // 审计 P2-9:spawn 失败(脚本缺失/进程崩溃)与测试非零区分——只有测试真实跑完且失败才回滚
169
+ if (smoke.error) {
170
+ return {
171
+ ok: false,
172
+ lines: [`✖ 升级已完成,但冒烟测试无法运行:${smoke.error?.message || smoke.error}`, ` 已保持在 v${newVer}(未回滚,因为不是测试失败)。`],
173
+ };
174
+ }
175
+ if (smoke.status !== 0) {
176
+ git(['reset', '--hard', prev.out], root);
177
+ const tail = String(smoke.stdout || smoke.stderr || '')
178
+ .split('\n')
179
+ .filter(Boolean)
180
+ .slice(-3)
181
+ .join(' | ');
182
+ return {
183
+ ok: false,
184
+ lines: [
185
+ `✖ 升级后冒烟测试失败,已自动回滚到 ${prev.out.slice(0, 8)}(v${local})。`,
186
+ ' 失败信息:' + (tail || '(无输出)'),
187
+ ' 当前仍是升级前的可用版本;请反馈该问题。',
188
+ ],
189
+ };
190
+ }
191
+ writeState({ prevSha: prev.out, prevVersion: local, at: Date.now() });
192
+ return {
193
+ ok: true,
194
+ lines: [`✓ 已从 v${local} 升级到 v${newVer}`, '✓ 冒烟测试通过;如遇问题可 mingdao rollback 回滚到升级前版本。'],
195
+ };
196
+ }
197
+
198
+ export function mingdaoRollback({ repo } = {}) {
199
+ const root = resolveRepo(repo);
200
+ if (!root) return { ok: false, lines: [NPM_HINT] };
201
+ const st = readState();
202
+ if (!st?.prevSha) {
203
+ return { ok: false, lines: ['✖ 没有可回滚的记录(只有成功 update 后才会写入回滚点)。'] };
204
+ }
205
+ const dirty = git(['status', '--porcelain'], root);
206
+ if (!dirty.ok) return { ok: false, lines: ['✖ git status 失败'] };
207
+ if (dirty.out) return { ok: false, lines: ['✖ 仓库有未提交改动,请先提交/暂存(回滚会覆盖工作区改动)。'] };
208
+ const cur = git(['rev-parse', 'HEAD'], root);
209
+ if (cur.ok && cur.out === st.prevSha) {
210
+ return { ok: false, lines: ['当前已在回滚目标提交上,无需回滚。'] };
211
+ }
212
+ const r = git(['reset', '--hard', st.prevSha], root);
213
+ if (!r.ok) return { ok: false, lines: ['✖ 回滚失败:' + (r.err || 'git reset --hard 出错')] };
214
+ const v = versionAt(root) || '未知';
215
+ try {
216
+ fs.unlinkSync(stateFile());
217
+ } catch {}
218
+ return {
219
+ ok: true,
220
+ lines: [`✓ 已回滚到 v${v}(${st.prevSha.slice(0, 8)},即 v${st.prevVersion || v} 升级前的提交)`],
221
+ };
222
+ }
@@ -0,0 +1,49 @@
1
+ // 聊天附件构造(纯函数,便于测试):
2
+ // - 图片:仅视觉模型支持(内置预设 supportsVision 或自定义模型 vision:true),
3
+ // 转 data:image/...;base64 内联(JPEG/PNG/GIF/WebP,单张 ≤5MB,最多 4 个附件)
4
+ // - 文本文件:直接拼接进消息文本(≤200KB),并留文件名标注
5
+ // - 返回 { content(模型消息内容:字符串或图文数组), persistText(落盘文本), error }
6
+
7
+ const MAX_IMAGE_DATAURL = 7 * 1024 * 1024; // base64 膨胀 1.33 倍,对应约 5MB 原图
8
+ const MAX_TEXT = 200 * 1024;
9
+
10
+ export function buildUserContent(message, attachments, visionSupported) {
11
+ const text = String(message ?? '').trim();
12
+ const list = Array.isArray(attachments) ? attachments.slice(0, 4) : [];
13
+ const imageParts = [];
14
+ const persistParts = [];
15
+ let finalText = text;
16
+ for (const a of list) {
17
+ if (!a || typeof a !== 'object') continue;
18
+ if (a.type === 'image') {
19
+ const dataUrl = String(a.dataUrl || '');
20
+ if (!/^data:image\/(png|jpeg|gif|webp);base64,/i.test(dataUrl)) {
21
+ return { error: `图片格式不支持:${a.name || '未命名'}(仅 JPEG/PNG/GIF/WebP)` };
22
+ }
23
+ if (dataUrl.length > MAX_IMAGE_DATAURL) {
24
+ return { error: `图片过大:${a.name || '未命名'}(单张 ≤5MB)` };
25
+ }
26
+ if (!visionSupported) {
27
+ return { error: '当前模型不支持图片输入(请切换到 deepseek-v4-flash-vision-exp 或支持视觉的自定义模型)' };
28
+ }
29
+ imageParts.push({ type: 'image_url', image_url: { url: dataUrl } });
30
+ persistParts.push(`[图片:${a.name || '未命名'}]`);
31
+ } else if (a.type === 'text') {
32
+ const content = String(a.content ?? '');
33
+ if (!content.trim()) continue;
34
+ if (content.length > MAX_TEXT) {
35
+ return { error: `文本文件过大:${a.name || '未命名'}(≤200KB)` };
36
+ }
37
+ finalText += `${finalText ? '\n\n' : ''}[文件 ${a.name || '未命名'}]\n${content}`;
38
+ persistParts.push(`[文件:${a.name || '未命名'}]`);
39
+ }
40
+ }
41
+ if (!finalText.trim() && !imageParts.length) {
42
+ return { error: '消息与附件不能同时为空' };
43
+ }
44
+ // 有图片 → 图文数组;纯文本 → 字符串(保持普通路径不变)
45
+ const content = imageParts.length
46
+ ? [...(finalText.trim() ? [{ type: 'text', text: finalText }] : []), ...imageParts]
47
+ : finalText;
48
+ return { content, persistText: `${finalText.trim()}${persistParts.length ? (finalText.trim() ? '\n' : '') + persistParts.join(' ') : ''}`.trim() };
49
+ }