ygc-dsh-sync 1.1.0 → 1.2.1
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/client.js +266 -119
- package/index.js +141 -32
- package/lib/api.js +1815 -1510
- package/lib/core.js +484 -443
- package/lib/features.js +409 -0
- package/package.json +3 -2
- package/tools/draft-live.mjs +1 -5
- package/tools/release-local.mjs +2 -10
- package/tools/sync-lock.mjs +1 -3
- package/tools/verify-installed.mjs +1 -2
package/index.js
CHANGED
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
backupWorkspace,
|
|
14
14
|
pickWorkspaces,
|
|
15
15
|
projectKeysFor,
|
|
16
|
+
pullWorkspace,
|
|
16
17
|
scanLocalSessions,
|
|
17
18
|
} from './lib/backup.js';
|
|
18
19
|
import { attachMany, findPulledUnattached } from './lib/attach.js';
|
|
19
|
-
import {
|
|
20
|
+
import { scanFolders } from './lib/patrol.js';
|
|
21
|
+
import { applyFeaturePlan, onlyActive, pickRemovedIds, planFeatures, reconcileRemoved } from './lib/features.js';
|
|
20
22
|
import {
|
|
21
23
|
LINEAGE_FILE,
|
|
22
24
|
LOCAL_SKILLS_ROOT,
|
|
@@ -107,6 +109,26 @@ export function apply(ctx, config = {}) {
|
|
|
107
109
|
return { id: ws?.id, created: true };
|
|
108
110
|
};
|
|
109
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 取消注册一个工作区(只删注册,不删目录与会话)。
|
|
114
|
+
*
|
|
115
|
+
* 走宿主自己的 workspaceRegistry.delete:user 在侧边栏删工作区是同一个入口,
|
|
116
|
+
* 这里只是让「删特征文件夹」能顺手把注册也清掉(/features/drop 的选项)。
|
|
117
|
+
*/
|
|
118
|
+
const deleteWorkspace = async (id) => {
|
|
119
|
+
const registry = (() => {
|
|
120
|
+
try {
|
|
121
|
+
return ctx.get('workspaceRegistry') || ctx.workspaceRegistry;
|
|
122
|
+
} catch {
|
|
123
|
+
return ctx.workspaceRegistry;
|
|
124
|
+
}
|
|
125
|
+
})();
|
|
126
|
+
if (!registry || typeof registry.delete !== 'function') {
|
|
127
|
+
throw new Error('宿主没有提供 workspaceRegistry.delete');
|
|
128
|
+
}
|
|
129
|
+
return registry.delete(id);
|
|
130
|
+
};
|
|
131
|
+
|
|
110
132
|
// ---- 插件清单同步(1.0.0 从自动更新器迁来)----
|
|
111
133
|
|
|
112
134
|
/** 把本机清单写进自己那一格,再汇集他人清单。幂等:没变就不写文件。 */
|
|
@@ -195,6 +217,7 @@ export function apply(ctx, config = {}) {
|
|
|
195
217
|
workspaces,
|
|
196
218
|
machineId,
|
|
197
219
|
registerWorkspace,
|
|
220
|
+
deleteWorkspace,
|
|
198
221
|
wsEntity,
|
|
199
222
|
entities,
|
|
200
223
|
// 起草器:宿主的默认模型。没有 llm 服务时返回 null,流水线自动跳过起草。
|
|
@@ -229,7 +252,9 @@ export function apply(ctx, config = {}) {
|
|
|
229
252
|
if (!state.folders.length) return;
|
|
230
253
|
const mid = config.machineId || state.machineId;
|
|
231
254
|
|
|
232
|
-
|
|
255
|
+
// 只备份「本机特征文件夹存在」的工作区:没建格子 = 本机不参与,
|
|
256
|
+
// 自动备份也不许往里写(1.2.0 起的硬规矩)。
|
|
257
|
+
const matched = onlyActive(pickWorkspaces(workspaces(), state.folders), mid);
|
|
233
258
|
if (!matched.length) return;
|
|
234
259
|
|
|
235
260
|
const local = scanLocalSessions();
|
|
@@ -270,56 +295,97 @@ export function apply(ctx, config = {}) {
|
|
|
270
295
|
});
|
|
271
296
|
|
|
272
297
|
// ---- 定时巡检 ----
|
|
273
|
-
//
|
|
274
|
-
//
|
|
298
|
+
// 判据(用户 2026-11-14 定的矩阵,只认磁盘上有没有那三个东西):
|
|
299
|
+
// 别人的格子 有 / 我的格子 没有 / 工作区 没有 → 问人要不要在本机建
|
|
300
|
+
// 我的格子 有 / 工作区 有 → 正常,参与备份与拉取
|
|
301
|
+
// 我的格子 有 / 工作区 没有 → 补工作区(注册回侧边栏)
|
|
302
|
+
// 我的格子 没有 / 工作区 有 → 补文件夹
|
|
303
|
+
// 工作区被删(基线里有、清单里没有) → 删我的格子(含格里对话)
|
|
304
|
+
// 补工作区这一步要调宿主接口(async),所以放在这里做,不在纯函数里。
|
|
275
305
|
const runPatrol = async (reason) => {
|
|
276
306
|
try {
|
|
277
307
|
const state = loadState();
|
|
278
308
|
const mid = config.machineId || state.machineId;
|
|
279
|
-
const
|
|
309
|
+
const all = workspaces();
|
|
310
|
+
const scan = scanFolders({
|
|
280
311
|
folders: state.folders,
|
|
281
|
-
workspaces:
|
|
312
|
+
workspaces: all,
|
|
282
313
|
machineId: mid,
|
|
283
314
|
});
|
|
284
315
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
316
|
+
const plan = planFeatures({
|
|
317
|
+
folders: state.folders,
|
|
318
|
+
workspaces: all,
|
|
319
|
+
discover: scan.discover,
|
|
320
|
+
state: loadState(),
|
|
321
|
+
machineId: mid,
|
|
322
|
+
});
|
|
323
|
+
const next = loadState();
|
|
324
|
+
// 补文件夹 + 删格子(这两件是同步的、不涉及注册)
|
|
325
|
+
const applied = applyFeaturePlan({ plan, state: next, machineId: mid, log });
|
|
326
|
+
saveState(next);
|
|
288
327
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
328
|
+
// 补工作区:有我的格子、但侧边栏里没有它
|
|
329
|
+
const restoredWs = [];
|
|
330
|
+
const restoreErrors = [];
|
|
331
|
+
if (typeof registerWorkspace === 'function') {
|
|
332
|
+
for (const item of plan.restoreWorkspace || []) {
|
|
333
|
+
try {
|
|
334
|
+
const out = await registerWorkspace(item.path, item.title);
|
|
335
|
+
restoredWs.push({ path: item.path, id: out?.id });
|
|
336
|
+
} catch (err) {
|
|
337
|
+
restoreErrors.push({ path: item.path, message: String((err && err.message) || err) });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (restoredWs.length) {
|
|
341
|
+
const s2 = loadState();
|
|
342
|
+
for (const r of restoredWs) {
|
|
343
|
+
if (r.id !== undefined && r.id !== null) s2.features.known[String(r.id)] = String(r.path).replace(/[\\/]+/g, '\\');
|
|
344
|
+
}
|
|
345
|
+
saveState(s2);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
293
348
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
349
|
+
const attachment = await repairAttachments();
|
|
350
|
+
|
|
351
|
+
const after = loadState();
|
|
352
|
+
after.patrol = {
|
|
353
|
+
...after.patrol,
|
|
297
354
|
lastRunAt: Date.now(),
|
|
298
355
|
lastResult: {
|
|
299
|
-
discover:
|
|
300
|
-
repair:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
356
|
+
discover: scan.discover.length,
|
|
357
|
+
repair: scan.repair.length,
|
|
358
|
+
ask: plan.ask.length,
|
|
359
|
+
createdFolder: applied.created.length,
|
|
360
|
+
restoredWorkspace: restoredWs.length,
|
|
361
|
+
dropped: applied.dropped.length,
|
|
362
|
+
attached: attachment.attached,
|
|
363
|
+
attachFailed: attachment.failed,
|
|
304
364
|
reason,
|
|
305
365
|
},
|
|
306
366
|
};
|
|
307
367
|
pushHistory(
|
|
308
|
-
|
|
368
|
+
after,
|
|
309
369
|
'patrol',
|
|
310
|
-
`巡检(${reason}
|
|
311
|
-
|
|
312
|
-
|
|
370
|
+
`巡检(${reason}):待人工确认 ${plan.ask.length} 个、补文件夹 ${applied.created.length} 个、` +
|
|
371
|
+
`补工作区 ${restoredWs.length} 个、删格子 ${applied.dropped.length} 个` +
|
|
372
|
+
`${attachment.attached ? `、补登记 ${attachment.attached} 个会话归属` : ''}`,
|
|
313
373
|
);
|
|
314
|
-
saveState(
|
|
315
|
-
|
|
316
|
-
if (
|
|
374
|
+
saveState(after);
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
plan.ask.length ||
|
|
378
|
+
applied.created.length ||
|
|
379
|
+
restoredWs.length ||
|
|
380
|
+
applied.dropped.length ||
|
|
381
|
+
attachment.attached
|
|
382
|
+
) {
|
|
317
383
|
log(
|
|
318
|
-
|
|
319
|
-
|
|
384
|
+
`巡检:待人工确认 ${plan.ask.length},补文件夹 ${applied.created.length},补工作区 ${restoredWs.length},` +
|
|
385
|
+
`删格子 ${applied.dropped.length},补登记 ${attachment.attached}`,
|
|
320
386
|
);
|
|
321
387
|
}
|
|
322
|
-
return
|
|
388
|
+
return after.patrol;
|
|
323
389
|
} catch (err) {
|
|
324
390
|
log(`巡检失败:${err && err.message ? err.message : err}`);
|
|
325
391
|
return null;
|
|
@@ -332,6 +398,9 @@ export function apply(ctx, config = {}) {
|
|
|
332
398
|
* 只认拉取产物(别的机器备份目录里有它),不按 cwd 无差别补——
|
|
333
399
|
* 自动化插件和子代理留下的会话也命中 cwd,一起补会让侧栏一次多出上百条。
|
|
334
400
|
* 幂等;单条失败不打断。
|
|
401
|
+
*
|
|
402
|
+
* 1.2.0 起只处理「有本机特征文件夹」的工作区:没建格子的本机压根没在同步它,
|
|
403
|
+
* 往里补登记只会让侧栏凭空多出一堆会话。
|
|
335
404
|
*/
|
|
336
405
|
const repairAttachments = async () => {
|
|
337
406
|
let attached = 0;
|
|
@@ -339,7 +408,7 @@ export function apply(ctx, config = {}) {
|
|
|
339
408
|
try {
|
|
340
409
|
const state = loadState();
|
|
341
410
|
const mid = config.machineId || state.machineId;
|
|
342
|
-
const matched = pickWorkspaces(workspaces(), state.folders);
|
|
411
|
+
const matched = onlyActive(pickWorkspaces(workspaces(), state.folders), mid);
|
|
343
412
|
if (!matched.length) return { attached, failed };
|
|
344
413
|
|
|
345
414
|
const localIndex = scanLocalSessions();
|
|
@@ -385,10 +454,50 @@ export function apply(ctx, config = {}) {
|
|
|
385
454
|
});
|
|
386
455
|
};
|
|
387
456
|
|
|
457
|
+
/**
|
|
458
|
+
* 删除对账的二次确认记录:工作区 id → 第一次发现「清单里没有它」的时间。
|
|
459
|
+
* 判据与取整逻辑在 pickRemovedIds(纯函数,有测试)。
|
|
460
|
+
*/
|
|
461
|
+
let missingSince = new Map();
|
|
462
|
+
|
|
388
463
|
const tick = () => {
|
|
389
464
|
try {
|
|
390
465
|
const state = loadState();
|
|
391
466
|
const patrol = state.patrol || {};
|
|
467
|
+
|
|
468
|
+
// 轻量对账(每 30 秒,与巡检间隔无关):本机删掉工作区后要尽快把
|
|
469
|
+
// 特征文件夹一起清掉,不能等满一个巡检周期。只比对清单与基线,
|
|
470
|
+
// 不扫盘、不建目录,成本接近零。
|
|
471
|
+
try {
|
|
472
|
+
const liveIds = workspaces().map((w) => String(w.id));
|
|
473
|
+
const nextState = loadState();
|
|
474
|
+
const known = (nextState.features && nextState.features.known) || {};
|
|
475
|
+
const picked = pickRemovedIds({
|
|
476
|
+
known,
|
|
477
|
+
liveIds,
|
|
478
|
+
missingSince,
|
|
479
|
+
now: Date.now(),
|
|
480
|
+
minAgeMs: PATROL_TICK_MS,
|
|
481
|
+
});
|
|
482
|
+
missingSince = picked.missingSince;
|
|
483
|
+
if (picked.confirmed.length) {
|
|
484
|
+
const rec = reconcileRemoved(nextState, config.machineId || nextState.machineId, picked.confirmed, {
|
|
485
|
+
log,
|
|
486
|
+
});
|
|
487
|
+
if (rec.dropped.length || rec.errors.length) {
|
|
488
|
+
pushHistory(
|
|
489
|
+
nextState,
|
|
490
|
+
'features',
|
|
491
|
+
`工作区已删除,同步删掉 ${rec.dropped.length} 个本机特征文件夹`,
|
|
492
|
+
);
|
|
493
|
+
saveState(nextState);
|
|
494
|
+
log(`工作区删除:同步清掉 ${rec.dropped.length} 个本机特征文件夹`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
} catch (err) {
|
|
498
|
+
log(`特征文件夹对账失败:${err && err.message ? err.message : err}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
392
501
|
if (!patrol.enabled) {
|
|
393
502
|
// 关闭期间不积累待办;重新开启时从下一次 tick 重新起算
|
|
394
503
|
booted = false;
|