mingdao-harness 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ROADMAP-NEXT.md +5 -5
- package/package.json +1 -1
- package/src/agent.js +19 -19
- package/src/audit.js +2 -2
- package/src/batch.js +7 -7
- package/src/cachestats.js +9 -9
- package/src/cli.js +29 -679
- package/src/commands/desktop.js +1 -1
- package/src/commands/key.js +1 -1
- package/src/commands/repl.js +711 -0
- package/src/commands/schedule.js +7 -7
- package/src/commands/skill.js +18 -16
- package/src/commands/sync.js +5 -5
- package/src/commands/update.js +7 -6
- package/src/commands/workspace.js +2 -2
- package/src/compact.js +6 -6
- package/src/config.js +7 -4
- package/src/context.js +11 -11
- package/src/credentials.js +6 -0
- package/src/hooks.js +11 -11
- package/src/mcp-presets.js +5 -5
- package/src/mcp.js +19 -19
- package/src/memory.js +16 -16
- package/src/model-discovery.js +16 -15
- package/src/models.js +4 -4
- package/src/notify.js +2 -2
- package/src/permissions.js +7 -3
- package/src/pricing.js +35 -1
- package/src/prompts.js +1 -1
- package/src/providers/index.js +7 -7
- package/src/providers/openai-compatible.js +8 -8
- package/src/routing.js +5 -5
- package/src/schedule.js +28 -28
- package/src/session-index.js +5 -1
- package/src/session.js +11 -2
- package/src/skill-lib.js +62 -3
- package/src/skill-registry.js +10 -9
- package/src/skills.js +10 -10
- package/src/sync-server.js +86 -3
- package/src/sync.js +53 -20
- package/src/tasks/worker.js +126 -0
- package/src/tasks.js +13 -13
- package/src/titles.js +5 -5
- package/src/tokenizer.js +22 -4
- package/src/tools/bash.js +12 -12
- package/src/tools/fs-tools.js +66 -9
- package/src/tools/index.js +7 -7
- package/src/ui.js +83 -6
- package/src/update.js +8 -8
- package/src/web/attachments.js +1 -1
- package/src/web/routes/api.js +38 -786
- package/src/web/routes/domains/config.js +280 -0
- package/src/web/routes/domains/misc.js +128 -0
- package/src/web/routes/domains/schedule.js +127 -0
- package/src/web/routes/domains/sessions.js +105 -0
- package/src/web/routes/domains/skills.js +79 -0
- package/src/web/routes/domains/sync.js +101 -0
- package/src/web/routes/domains/workspace.js +113 -0
- package/src/web/server.js +29 -14
- package/src/web/web-io.js +54 -12
- package/src/workspace.js +14 -14
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)) {
|
|
@@ -108,7 +108,7 @@ export function listSkills(workingDir) {
|
|
|
108
108
|
return out;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export function loadSkill(workingDir, name) {
|
|
111
|
+
export function loadSkill(/** @type {any} */ workingDir, /** @type {any} */ name) {
|
|
112
112
|
const s = listSkills(workingDir).find((x) => x.name === name);
|
|
113
113
|
if (!s) return null;
|
|
114
114
|
try {
|
|
@@ -118,14 +118,14 @@ export function loadSkill(workingDir, name) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function sourceLabel(source) {
|
|
121
|
+
function sourceLabel(/** @type {any} */ source) {
|
|
122
122
|
if (source === 'user') return '(用户级)';
|
|
123
123
|
if (source === 'builtin') return '(内置)';
|
|
124
124
|
return '';
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
// 注入系统提示的技能清单(仅名称+描述)
|
|
128
|
-
export function skillsRegistryBlock(workingDir) {
|
|
128
|
+
export function skillsRegistryBlock(/** @type {any} */ workingDir) {
|
|
129
129
|
const skills = listSkills(workingDir);
|
|
130
130
|
if (!skills.length) return '';
|
|
131
131
|
return (
|
package/src/sync-server.js
CHANGED
|
@@ -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
|
};
|
package/src/sync.js
CHANGED
|
@@ -23,15 +23,21 @@ const insecureOn = () => syncSettings()?.insecure === true;
|
|
|
23
23
|
|
|
24
24
|
// 请求级不安全 TLS 传输(P1-6):为本次请求单独建立 rejectUnauthorized:false 的 https.Agent,
|
|
25
25
|
// 与并发的 provider 请求(各自的 TLS 校验)互不干扰;安全路径仍走 fetch(连接复用)。
|
|
26
|
+
/**
|
|
27
|
+
* @param {any} target
|
|
28
|
+
* @param {{ headers?: any, body?: any, timeoutMs?: any, insecure?: any }} opts
|
|
29
|
+
*/
|
|
26
30
|
function rawRequest(target, { headers, body, timeoutMs, insecure }) {
|
|
27
31
|
return new Promise((resolve, reject) => {
|
|
28
32
|
const mod = target.protocol === 'http:' ? http : https;
|
|
33
|
+
/** @type {any} */
|
|
29
34
|
const opts = { method: 'POST', headers };
|
|
30
35
|
if (insecure && target.protocol === 'https:') opts.agent = new https.Agent({ rejectUnauthorized: false });
|
|
31
36
|
const req = mod.request(target, opts, (res) => {
|
|
37
|
+
/** @type {any[]} */
|
|
32
38
|
const chunks = [];
|
|
33
39
|
let size = 0;
|
|
34
|
-
res.on('data', (d) => {
|
|
40
|
+
res.on('data', (/** @type {any} */ d) => {
|
|
35
41
|
size += d.length;
|
|
36
42
|
if (size > 25 * 1024 * 1024) {
|
|
37
43
|
req.destroy();
|
|
@@ -56,6 +62,12 @@ function rawRequest(target, { headers, body, timeoutMs, insecure }) {
|
|
|
56
62
|
});
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @param {any} baseUrl
|
|
67
|
+
* @param {any} method
|
|
68
|
+
* @param {any} payload
|
|
69
|
+
* @param {any} token
|
|
70
|
+
*/
|
|
59
71
|
async function apiCall(baseUrl, method, payload, token, timeoutMs = TIMEOUT_MS, insecure = false) {
|
|
60
72
|
const target = new URL(baseUrl.replace(/\/+$/, '') + method);
|
|
61
73
|
const body = JSON.stringify(payload || {});
|
|
@@ -92,7 +104,7 @@ async function apiCall(baseUrl, method, payload, token, timeoutMs = TIMEOUT_MS,
|
|
|
92
104
|
clearTimeout(timer);
|
|
93
105
|
}
|
|
94
106
|
} catch (e) {
|
|
95
|
-
if (e.name === 'AbortError' || e.message === '请求超时') throw new Error('请求超时(20s)');
|
|
107
|
+
if (/** @type {any} */ (e).name === 'AbortError' || /** @type {any} */ (e).message === '请求超时') throw new Error('请求超时(20s)');
|
|
96
108
|
throw e;
|
|
97
109
|
}
|
|
98
110
|
}
|
|
@@ -120,6 +132,9 @@ export function syncStatus() {
|
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
// 登录/注册 + 设备配对:注册失败(用户不存在)先注册再配对
|
|
135
|
+
/**
|
|
136
|
+
* @param {{ url?: any, username?: any, password?: any, deviceName?: any, insecure?: any }} opts
|
|
137
|
+
*/
|
|
123
138
|
export async function syncLogin({ url, username, password, deviceName, insecure = false }) {
|
|
124
139
|
const base = String(url || '').trim().replace(/\/+$/, '');
|
|
125
140
|
if (!/^https?:\/\//i.test(base)) return { error: '服务器地址需以 http(s):// 开头' };
|
|
@@ -133,8 +148,8 @@ export async function syncLogin({ url, username, password, deviceName, insecure
|
|
|
133
148
|
try {
|
|
134
149
|
pair = await apiCall(base, '/api/pair', { username: name, password, deviceName: dev }, undefined, TIMEOUT_MS, loginInsecure);
|
|
135
150
|
} catch (e) {
|
|
136
|
-
if (e.status === 401) return { error: '用户名或密码错误' };
|
|
137
|
-
if (e.status !== 404) return { error: `连接失败:${e.message}` };
|
|
151
|
+
if (/** @type {any} */ (e).status === 401) return { error: '用户名或密码错误' };
|
|
152
|
+
if (/** @type {any} */ (e).status !== 404) return { error: `连接失败:${/** @type {any} */ (e).message}` };
|
|
138
153
|
pair = { notFound: true }; // 用户不存在 → 先注册
|
|
139
154
|
}
|
|
140
155
|
if (pair.notFound) {
|
|
@@ -152,9 +167,9 @@ export async function syncLogin({ url, username, password, deviceName, insecure
|
|
|
152
167
|
saveCredentials(creds);
|
|
153
168
|
return { ok: true, username: name, deviceName: dev, url: base };
|
|
154
169
|
} catch (e) {
|
|
155
|
-
if (e.status === 401) return { error: '用户名或密码错误' };
|
|
156
|
-
if (e.status === 404) return { error: '服务器接口不存在(确认是 mingdao 同步服务端)' };
|
|
157
|
-
return { error: `连接失败:${e.message}` };
|
|
170
|
+
if (/** @type {any} */ (e).status === 401) return { error: '用户名或密码错误' };
|
|
171
|
+
if (/** @type {any} */ (e).status === 404) return { error: '服务器接口不存在(确认是 mingdao 同步服务端)' };
|
|
172
|
+
return { error: `连接失败:${/** @type {any} */ (e).message}` };
|
|
158
173
|
}
|
|
159
174
|
}
|
|
160
175
|
|
|
@@ -190,15 +205,20 @@ export async function syncRemoteList() {
|
|
|
190
205
|
const r = await apiCall(g.url, '/api/sessions/list', {}, g.token, TIMEOUT_MS, insecureOn());
|
|
191
206
|
return { ok: true, sessions: r.sessions || [] };
|
|
192
207
|
} catch (e) {
|
|
193
|
-
return { error: e.message };
|
|
208
|
+
return { error: /** @type {any} */ (e).message };
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
|
|
197
212
|
// 会话名白名单(与 sync-server 同款):远端返回的名字不满足即拒绝落盘,防恶意服务器任意文件写
|
|
213
|
+
/** @param {any} n */
|
|
198
214
|
function isValidRemoteName(n) {
|
|
199
215
|
return typeof n === 'string' && /^[\w\u4e00-\u9fa5.-]{1,140}\.jsonl$/.test(n) && !n.includes('..');
|
|
200
216
|
}
|
|
201
217
|
// 冲突副本名:时间戳 + 随机后缀,防同一毫秒冲突静默覆盖
|
|
218
|
+
/**
|
|
219
|
+
* @param {any} base
|
|
220
|
+
* @param {any} side
|
|
221
|
+
*/
|
|
202
222
|
function conflictCopyName(base, side) {
|
|
203
223
|
return base.replace(/\.jsonl$/, `.${side}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.jsonl`);
|
|
204
224
|
}
|
|
@@ -214,6 +234,7 @@ function readState() {
|
|
|
214
234
|
return {};
|
|
215
235
|
}
|
|
216
236
|
}
|
|
237
|
+
/** @param {any} state */
|
|
217
238
|
function writeState(state) {
|
|
218
239
|
ensureHome();
|
|
219
240
|
const target = stateFile();
|
|
@@ -221,6 +242,7 @@ function writeState(state) {
|
|
|
221
242
|
}
|
|
222
243
|
|
|
223
244
|
// 推送单个/全部会话。仅当远端被其他设备改过(mtime 与本地记录不一致且内容不同)才视为冲突并备份远端。
|
|
245
|
+
/** @param {any} [name] */
|
|
224
246
|
export async function syncPush(name) {
|
|
225
247
|
const g = tokenGuard();
|
|
226
248
|
if (g.error) return { error: g.error };
|
|
@@ -257,7 +279,7 @@ export async function syncPush(name) {
|
|
|
257
279
|
try {
|
|
258
280
|
remote = await apiCall(g.url, '/api/sessions/pull', { name: s.name }, g.token, TIMEOUT_MS, insecureOn());
|
|
259
281
|
} catch (e) {
|
|
260
|
-
if (e.status !== 404) return { error: `读取远端 ${s.name} 失败:${e.message}` };
|
|
282
|
+
if (/** @type {any} */ (e).status !== 404) return { error: `读取远端 ${s.name} 失败:${/** @type {any} */ (e).message}` };
|
|
261
283
|
}
|
|
262
284
|
if (remote?.ok) {
|
|
263
285
|
const lastMtime = state[s.name]?.remoteMtime;
|
|
@@ -275,7 +297,7 @@ export async function syncPush(name) {
|
|
|
275
297
|
state[s.name] = { remoteMtime: r.mtime, localMtime: localStat.mtimeMs };
|
|
276
298
|
}
|
|
277
299
|
} catch (e) {
|
|
278
|
-
return { error: `推送 ${s.name} 失败:${e.message}` };
|
|
300
|
+
return { error: `推送 ${s.name} 失败:${/** @type {any} */ (e).message}` };
|
|
279
301
|
}
|
|
280
302
|
}
|
|
281
303
|
writeState(state);
|
|
@@ -283,6 +305,7 @@ export async function syncPush(name) {
|
|
|
283
305
|
}
|
|
284
306
|
|
|
285
307
|
// 拉取单个/全部会话。本地有不同内容时保留本地,远端写入 .remote- 副本。
|
|
308
|
+
/** @param {any} [name] */
|
|
286
309
|
export async function syncPull(name) {
|
|
287
310
|
const g = tokenGuard();
|
|
288
311
|
if (g.error) return { error: g.error };
|
|
@@ -290,9 +313,9 @@ export async function syncPull(name) {
|
|
|
290
313
|
let sessions;
|
|
291
314
|
try {
|
|
292
315
|
const r = await apiCall(g.url, '/api/sessions/list', {}, g.token, TIMEOUT_MS, insecureOn());
|
|
293
|
-
sessions = (r.sessions || []).filter((s) => !name || s.name === name);
|
|
316
|
+
sessions = (r.sessions || []).filter((/** @type {any} */ s) => !name || s.name === name);
|
|
294
317
|
} catch (e) {
|
|
295
|
-
return { error: `获取远端清单失败:${e.message}` };
|
|
318
|
+
return { error: `获取远端清单失败:${/** @type {any} */ (e).message}` };
|
|
296
319
|
}
|
|
297
320
|
if (!sessions.length) return { error: name ? `远端没有会话 ${name}` : '远端没有会话' };
|
|
298
321
|
const state = readState();
|
|
@@ -330,6 +353,9 @@ export async function syncPull(name) {
|
|
|
330
353
|
}
|
|
331
354
|
|
|
332
355
|
// ---------- 密码修改 ----------
|
|
356
|
+
/**
|
|
357
|
+
* @param {{ oldPassword?: any, newPassword?: any }} opts
|
|
358
|
+
*/
|
|
333
359
|
export async function syncChangePassword({ oldPassword, newPassword }) {
|
|
334
360
|
const g = tokenGuard();
|
|
335
361
|
if (g.error) return { error: g.error };
|
|
@@ -338,12 +364,13 @@ export async function syncChangePassword({ oldPassword, newPassword }) {
|
|
|
338
364
|
const r = await apiCall(g.url, '/api/password', { oldPassword: oldPassword || '', newPassword }, g.token, TIMEOUT_MS, insecureOn());
|
|
339
365
|
return r.ok ? { ok: true } : { error: r.error || '修改失败' };
|
|
340
366
|
} catch (e) {
|
|
341
|
-
if (e.status === 401) return { error: '旧密码错误' };
|
|
342
|
-
return { error: `修改失败:${e.message}` };
|
|
367
|
+
if (/** @type {any} */ (e).status === 401) return { error: '旧密码错误' };
|
|
368
|
+
return { error: `修改失败:${/** @type {any} */ (e).message}` };
|
|
343
369
|
}
|
|
344
370
|
}
|
|
345
371
|
|
|
346
372
|
// ---------- 会话分享与协作 ----------
|
|
373
|
+
/** @param {any} name */
|
|
347
374
|
export async function syncShareCreate(name) {
|
|
348
375
|
const g = tokenGuard();
|
|
349
376
|
if (g.error) return { error: g.error };
|
|
@@ -351,7 +378,7 @@ export async function syncShareCreate(name) {
|
|
|
351
378
|
const r = await apiCall(g.url, '/api/share/create', { name }, g.token, TIMEOUT_MS, insecureOn());
|
|
352
379
|
return r.ok ? { ok: true, shareId: r.shareId, name: r.name } : { error: r.error };
|
|
353
380
|
} catch (e) {
|
|
354
|
-
return { error: `分享失败:${e.status === 404 ? '你还没有这个会话' : e.message}` };
|
|
381
|
+
return { error: `分享失败:${/** @type {any} */ (e).status === 404 ? '你还没有这个会话' : /** @type {any} */ (e).message}` };
|
|
355
382
|
}
|
|
356
383
|
}
|
|
357
384
|
|
|
@@ -362,10 +389,11 @@ export async function syncShareList() {
|
|
|
362
389
|
const r = await apiCall(g.url, '/api/share/list', {}, g.token, TIMEOUT_MS, insecureOn());
|
|
363
390
|
return { ok: true, mine: r.mine || [], accepted: r.accepted || [] };
|
|
364
391
|
} catch (e) {
|
|
365
|
-
return { error: `获取分享列表失败:${e.message}` };
|
|
392
|
+
return { error: `获取分享列表失败:${/** @type {any} */ (e).message}` };
|
|
366
393
|
}
|
|
367
394
|
}
|
|
368
395
|
|
|
396
|
+
/** @param {any} shareId */
|
|
369
397
|
export async function syncShareAccept(shareId) {
|
|
370
398
|
const g = tokenGuard();
|
|
371
399
|
if (g.error) return { error: g.error };
|
|
@@ -382,10 +410,11 @@ export async function syncShareAccept(shareId) {
|
|
|
382
410
|
writeState(state);
|
|
383
411
|
return { ok: true, shareId, savedAs: r.savedAs, conflict: r.conflict || false };
|
|
384
412
|
} catch (e) {
|
|
385
|
-
return { error: `接受分享失败:${e.status === 404 ? '分享不存在(可能已撤销)' : e.message}` };
|
|
413
|
+
return { error: `接受分享失败:${/** @type {any} */ (e).status === 404 ? '分享不存在(可能已撤销)' : /** @type {any} */ (e).message}` };
|
|
386
414
|
}
|
|
387
415
|
}
|
|
388
416
|
|
|
417
|
+
/** @param {any} shareId */
|
|
389
418
|
export async function syncShareRevoke(shareId) {
|
|
390
419
|
const g = tokenGuard();
|
|
391
420
|
if (g.error) return { error: g.error };
|
|
@@ -393,7 +422,7 @@ export async function syncShareRevoke(shareId) {
|
|
|
393
422
|
const r = await apiCall(g.url, '/api/share/revoke', { shareId }, g.token, TIMEOUT_MS, insecureOn());
|
|
394
423
|
return r.ok ? { ok: true } : { error: r.error };
|
|
395
424
|
} catch (e) {
|
|
396
|
-
return { error: `撤销失败:${e.status === 404 ? '分享不存在' : e.status === 403 ? '只能撤销自己的分享' : e.message}` };
|
|
425
|
+
return { error: `撤销失败:${/** @type {any} */ (e).status === 404 ? '分享不存在' : /** @type {any} */ (e).status === 403 ? '只能撤销自己的分享' : /** @type {any} */ (e).message}` };
|
|
397
426
|
}
|
|
398
427
|
}
|
|
399
428
|
|
|
@@ -421,12 +450,16 @@ export function listSyncConflicts() {
|
|
|
421
450
|
.map(([base, entries]) => ({
|
|
422
451
|
base,
|
|
423
452
|
localExists: fs.existsSync(path.join(home, 'sessions', base)),
|
|
424
|
-
entries: entries.sort((a, b) => b.ts - a.ts),
|
|
453
|
+
entries: entries.sort((/** @type {any} */ a, /** @type {any} */ b) => b.ts - a.ts),
|
|
425
454
|
}))
|
|
426
455
|
.sort((a, b) => a.base.localeCompare(b.base));
|
|
427
456
|
}
|
|
428
457
|
|
|
429
458
|
// choice: local(保留本地,删除备份)| remote(采用远端备份替换本地)| both(把最新备份转正为可见会话)
|
|
459
|
+
/**
|
|
460
|
+
* @param {any} base
|
|
461
|
+
* @param {any} choice
|
|
462
|
+
*/
|
|
430
463
|
export function resolveSyncConflict(base, choice) {
|
|
431
464
|
const home = mingdaoHome();
|
|
432
465
|
if (!/^[\w\u4e00-\u9fa5.-]{1,140}\.jsonl$/.test(base)) return { error: '会话名非法' };
|
|
@@ -443,7 +476,7 @@ export function resolveSyncConflict(base, choice) {
|
|
|
443
476
|
const mm = f.match(m);
|
|
444
477
|
return mm && `${mm[1]}.jsonl` === base;
|
|
445
478
|
})
|
|
446
|
-
.sort((a, b) => Number(b.match(m)[3]) - Number(a.match(m)[3]));
|
|
479
|
+
.sort((a, b) => Number(/** @type {any} */ (b.match(m))[3]) - Number(/** @type {any} */ (a.match(m))[3]));
|
|
447
480
|
if (!backups.length) return { error: `没有找到 ${base} 的冲突备份` };
|
|
448
481
|
const newest = backups[0];
|
|
449
482
|
if (choice === 'local') {
|