ygc-dsh-sync 1.0.2 → 1.2.0
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 +255 -119
- package/index.js +113 -31
- package/lib/api.js +306 -35
- package/lib/core.js +42 -1
- package/lib/features.js +409 -0
- package/package.json +62 -61
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,70 @@ export function apply(ctx, config = {}) {
|
|
|
270
295
|
});
|
|
271
296
|
|
|
272
297
|
// ---- 定时巡检 ----
|
|
273
|
-
//
|
|
274
|
-
//
|
|
298
|
+
// 行为(1.2.0 起):
|
|
299
|
+
// 1. 扫描特定文件夹下的直接子目录,列出「符合插件结构、本机还没建格子」的
|
|
300
|
+
// → 只记录,等人到面板上点,绝不自己建、也不自己注册;
|
|
301
|
+
// 2. 本机新建的工作区默认建格子(保持 1.1.0 原有设置);
|
|
302
|
+
// 3. 曾经建过、现在磁盘上没有的(被外部删了)→ 自动补建;
|
|
303
|
+
// 4. 本机已删除的工作区 → 跟着删格子;
|
|
304
|
+
// 5. 补登记只对「有格子」的工作区做(没格子的本机根本没在同步它)。
|
|
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
|
+
// 特征文件夹计划:adopt / restore / drop 自动做,pending 留给人
|
|
317
|
+
const plan = planFeatures({
|
|
318
|
+
folders: state.folders,
|
|
319
|
+
workspaces: all,
|
|
320
|
+
discover: scan.discover,
|
|
321
|
+
state: loadState(),
|
|
322
|
+
machineId: mid,
|
|
323
|
+
});
|
|
324
|
+
const next = loadState();
|
|
325
|
+
const applied = applyFeaturePlan({ plan, state: next, machineId: mid, log });
|
|
326
|
+
saveState(next);
|
|
288
327
|
|
|
289
|
-
|
|
290
|
-
// 旧版拉取只写文件不登记,这些会话会一直躺在「未分组」里;
|
|
291
|
-
// 这里每次巡检顺手补上,是幂等的(已登记的不动)。
|
|
292
|
-
const attachStats = await repairAttachments();
|
|
328
|
+
const attachment = await repairAttachments();
|
|
293
329
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
...
|
|
330
|
+
const after = loadState();
|
|
331
|
+
after.patrol = {
|
|
332
|
+
...after.patrol,
|
|
297
333
|
lastRunAt: Date.now(),
|
|
298
334
|
lastResult: {
|
|
299
|
-
discover:
|
|
300
|
-
repair:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
335
|
+
discover: scan.discover.length,
|
|
336
|
+
repair: scan.repair.length,
|
|
337
|
+
pending: plan.pending.length,
|
|
338
|
+
adopted: applied.adopted.length,
|
|
339
|
+
restored: applied.restored.length,
|
|
340
|
+
dropped: applied.dropped.length,
|
|
341
|
+
attached: attachment.attached,
|
|
342
|
+
attachFailed: attachment.failed,
|
|
304
343
|
reason,
|
|
305
344
|
},
|
|
306
345
|
};
|
|
307
346
|
pushHistory(
|
|
308
|
-
|
|
347
|
+
after,
|
|
309
348
|
'patrol',
|
|
310
|
-
`巡检(${reason}
|
|
311
|
-
|
|
312
|
-
|
|
349
|
+
`巡检(${reason}):待人工确认 ${plan.pending.length} 个、新建特征文件夹 ${applied.adopted.length} 个、` +
|
|
350
|
+
`补建 ${applied.restored.length} 个、随工作区删除 ${applied.dropped.length} 个` +
|
|
351
|
+
`${attachment.attached ? `、补登记 ${attachment.attached} 个会话归属` : ''}`,
|
|
313
352
|
);
|
|
314
|
-
saveState(
|
|
353
|
+
saveState(after);
|
|
315
354
|
|
|
316
|
-
if (
|
|
355
|
+
if (plan.pending.length || applied.adopted.length || applied.restored.length || applied.dropped.length) {
|
|
317
356
|
log(
|
|
318
|
-
|
|
319
|
-
|
|
357
|
+
`巡检:待人工确认 ${plan.pending.length},新建 ${applied.adopted.length},补建 ${applied.restored.length},` +
|
|
358
|
+
`删除 ${applied.dropped.length},补登记 ${attachment.attached}`,
|
|
320
359
|
);
|
|
321
360
|
}
|
|
322
|
-
return
|
|
361
|
+
return after.patrol;
|
|
323
362
|
} catch (err) {
|
|
324
363
|
log(`巡检失败:${err && err.message ? err.message : err}`);
|
|
325
364
|
return null;
|
|
@@ -332,6 +371,9 @@ export function apply(ctx, config = {}) {
|
|
|
332
371
|
* 只认拉取产物(别的机器备份目录里有它),不按 cwd 无差别补——
|
|
333
372
|
* 自动化插件和子代理留下的会话也命中 cwd,一起补会让侧栏一次多出上百条。
|
|
334
373
|
* 幂等;单条失败不打断。
|
|
374
|
+
*
|
|
375
|
+
* 1.2.0 起只处理「有本机特征文件夹」的工作区:没建格子的本机压根没在同步它,
|
|
376
|
+
* 往里补登记只会让侧栏凭空多出一堆会话。
|
|
335
377
|
*/
|
|
336
378
|
const repairAttachments = async () => {
|
|
337
379
|
let attached = 0;
|
|
@@ -339,7 +381,7 @@ export function apply(ctx, config = {}) {
|
|
|
339
381
|
try {
|
|
340
382
|
const state = loadState();
|
|
341
383
|
const mid = config.machineId || state.machineId;
|
|
342
|
-
const matched = pickWorkspaces(workspaces(), state.folders);
|
|
384
|
+
const matched = onlyActive(pickWorkspaces(workspaces(), state.folders), mid);
|
|
343
385
|
if (!matched.length) return { attached, failed };
|
|
344
386
|
|
|
345
387
|
const localIndex = scanLocalSessions();
|
|
@@ -385,10 +427,50 @@ export function apply(ctx, config = {}) {
|
|
|
385
427
|
});
|
|
386
428
|
};
|
|
387
429
|
|
|
430
|
+
/**
|
|
431
|
+
* 删除对账的二次确认记录:工作区 id → 第一次发现「清单里没有它」的时间。
|
|
432
|
+
* 判据与取整逻辑在 pickRemovedIds(纯函数,有测试)。
|
|
433
|
+
*/
|
|
434
|
+
let missingSince = new Map();
|
|
435
|
+
|
|
388
436
|
const tick = () => {
|
|
389
437
|
try {
|
|
390
438
|
const state = loadState();
|
|
391
439
|
const patrol = state.patrol || {};
|
|
440
|
+
|
|
441
|
+
// 轻量对账(每 30 秒,与巡检间隔无关):本机删掉工作区后要尽快把
|
|
442
|
+
// 特征文件夹一起清掉,不能等满一个巡检周期。只比对清单与基线,
|
|
443
|
+
// 不扫盘、不建目录,成本接近零。
|
|
444
|
+
try {
|
|
445
|
+
const liveIds = workspaces().map((w) => String(w.id));
|
|
446
|
+
const nextState = loadState();
|
|
447
|
+
const known = (nextState.features && nextState.features.known) || {};
|
|
448
|
+
const picked = pickRemovedIds({
|
|
449
|
+
known,
|
|
450
|
+
liveIds,
|
|
451
|
+
missingSince,
|
|
452
|
+
now: Date.now(),
|
|
453
|
+
minAgeMs: PATROL_TICK_MS,
|
|
454
|
+
});
|
|
455
|
+
missingSince = picked.missingSince;
|
|
456
|
+
if (picked.confirmed.length) {
|
|
457
|
+
const rec = reconcileRemoved(nextState, config.machineId || nextState.machineId, picked.confirmed, {
|
|
458
|
+
log,
|
|
459
|
+
});
|
|
460
|
+
if (rec.dropped.length || rec.errors.length) {
|
|
461
|
+
pushHistory(
|
|
462
|
+
nextState,
|
|
463
|
+
'features',
|
|
464
|
+
`工作区已删除,同步删掉 ${rec.dropped.length} 个本机特征文件夹`,
|
|
465
|
+
);
|
|
466
|
+
saveState(nextState);
|
|
467
|
+
log(`工作区删除:同步清掉 ${rec.dropped.length} 个本机特征文件夹`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
log(`特征文件夹对账失败:${err && err.message ? err.message : err}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
392
474
|
if (!patrol.enabled) {
|
|
393
475
|
// 关闭期间不积累待办;重新开启时从下一次 tick 重新起算
|
|
394
476
|
booted = false;
|