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/lib/features.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本机特征文件夹(`<工作区>/.dsh-sync/sessions/<本机标识>/`)的采纳、撤销与巡检判据。
|
|
3
|
+
*
|
|
4
|
+
* 判据只有一条:**磁盘上有没有那三个东西** —— 别人的特征文件夹、我的特征文件夹、
|
|
5
|
+
* 对应的工作区(DSH 的 workspaceRegistry 注册)。
|
|
6
|
+
* 用户 2026-11-14 定的巡检矩阵(这一层就是它的唯一实现):
|
|
7
|
+
*
|
|
8
|
+
* | 别人的格子 | 我的格子 | 工作区 | 动作 |
|
|
9
|
+
* |---|---|---|---|
|
|
10
|
+
* | 有 | 没有 | 没有 | **问人**:要不要在本机建特征文件夹(建了就顺带注册工作区) |
|
|
11
|
+
* | — | 有 | 有 | 正常,参与备份与拉取 |
|
|
12
|
+
* | — | 有 | 没有 | **补工作区**:把目录注册回侧边栏(不建也不删文件) |
|
|
13
|
+
* | — | 没有 | 有 | **补文件夹**:给这个工作区建本机特征文件夹 |
|
|
14
|
+
*
|
|
15
|
+
* 删除的语义(同样是用户定的):
|
|
16
|
+
*
|
|
17
|
+
* - 删工作区时,**先看有没有别人的格子**:
|
|
18
|
+
* - 只有本机这一格 → 连 `.dsh-sync/` 一起收掉,巡检**再也扫不到**它(不留痕、不追问)。
|
|
19
|
+
* - 还有别人的格子 → 只删我这一格(连同格里备份的对话记录),`.dsh-sync/` 留着,
|
|
20
|
+
* 巡检据此认为「本机没有这个共享工作区」,进人工队列问要不要重建。
|
|
21
|
+
* - 插件**不提供**「只删格子、留着工作区」的入口:那会让工作区变成
|
|
22
|
+
* 「已注册但没格子」,下一轮必然被补回来,看起来就是「删了又冒出来」。
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { TARGET_DIR_NAME, normSlash, ownRoot, pathKey, safeStat } from './core.js';
|
|
27
|
+
|
|
28
|
+
/* --------------------------------------------------------------- 基础判定 */
|
|
29
|
+
|
|
30
|
+
/** 本机特征文件夹的绝对路径(就是写边界里那个 ownRoot)。 */
|
|
31
|
+
export function featurePath(workspacePath, machineId) {
|
|
32
|
+
return ownRoot(workspacePath, machineId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 本机特征文件夹是否已存在(必须真的是目录)。 */
|
|
36
|
+
export function hasFeature(workspacePath, machineId) {
|
|
37
|
+
const st = safeStat(featurePath(workspacePath, machineId));
|
|
38
|
+
return !!st && st.isDirectory();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 这个工作区里,除了本机之外还有哪些机器的特征文件夹。 */
|
|
42
|
+
export function otherFeatures(workspacePath, machineId) {
|
|
43
|
+
const sessions = path.join(normSlash(workspacePath), TARGET_DIR_NAME);
|
|
44
|
+
let names = [];
|
|
45
|
+
try {
|
|
46
|
+
names = fs
|
|
47
|
+
.readdirSync(sessions, { withFileTypes: true })
|
|
48
|
+
.filter((d) => d.isDirectory())
|
|
49
|
+
.map((d) => d.name);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const me = String(machineId || '');
|
|
54
|
+
return names.filter((n) => n !== me);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 目录里是否还有 `.dsh-sync/` 这套结构(含 sessions)。 */
|
|
58
|
+
export function hasSyncStructure(workspacePath) {
|
|
59
|
+
const st = safeStat(path.join(normSlash(workspacePath), TARGET_DIR_NAME.split(path.sep)[0]));
|
|
60
|
+
return !!st && st.isDirectory();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 建本机特征文件夹。
|
|
65
|
+
*
|
|
66
|
+
* 只建 `<工作区>/.dsh-sync/sessions/<本机标识>/` 这一格,
|
|
67
|
+
* 不碰别人的目录,也不动任何会话文件。
|
|
68
|
+
*/
|
|
69
|
+
export function adoptFeature(workspacePath, machineId) {
|
|
70
|
+
if (!machineId) throw new Error('缺少本机标识,拒绝建特征文件夹');
|
|
71
|
+
const target = featurePath(workspacePath, machineId);
|
|
72
|
+
// 防 machineId 里带 `..` 或分隔符把目标打穿到别处
|
|
73
|
+
if (path.basename(target) !== String(machineId)) {
|
|
74
|
+
throw new Error(`特征文件夹名与本机标识不一致:${target}`);
|
|
75
|
+
}
|
|
76
|
+
fs.mkdirSync(target, { recursive: true });
|
|
77
|
+
return target;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 删本机特征文件夹(**连同格里备份的对话记录**)。
|
|
82
|
+
*
|
|
83
|
+
* 三条约束:
|
|
84
|
+
* - 只删本机那一格;别人的格子一个字节都不动;
|
|
85
|
+
* - `sessions/` 空了才把 `sessions/` 与 `.dsh-sync/` 一起收掉 ——
|
|
86
|
+
* 全收掉之后巡检就再也扫不到这个目录,符合「只有本机格子时删干净」;
|
|
87
|
+
* - 目标不存在算成功(幂等)。
|
|
88
|
+
*
|
|
89
|
+
* @returns {{removed:boolean, target?:string, keptStructure?:boolean, others?:string[], reason?:string}}
|
|
90
|
+
*/
|
|
91
|
+
export function dropFeature(workspacePath, machineId, { log = () => {} } = {}) {
|
|
92
|
+
if (!machineId) return { removed: false, reason: '缺少本机标识' };
|
|
93
|
+
const target = featurePath(workspacePath, machineId);
|
|
94
|
+
if (path.basename(target) !== String(machineId)) {
|
|
95
|
+
return { removed: false, reason: '目标不是本机特征文件夹' };
|
|
96
|
+
}
|
|
97
|
+
const others = otherFeatures(workspacePath, machineId);
|
|
98
|
+
const st = safeStat(target);
|
|
99
|
+
if (!st) {
|
|
100
|
+
return { removed: false, reason: '本来就没有', others, keptStructure: hasSyncStructure(workspacePath) };
|
|
101
|
+
}
|
|
102
|
+
if (!st.isDirectory()) return { removed: false, reason: '不是目录', others };
|
|
103
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
104
|
+
log(`删除本机特征文件夹(含其中备份的对话):${target}`);
|
|
105
|
+
|
|
106
|
+
// 收尾:空了才收,避免把别人的备份目录一起删掉
|
|
107
|
+
const sessionsDir = path.join(normSlash(workspacePath), TARGET_DIR_NAME);
|
|
108
|
+
const dshSyncDir = path.dirname(sessionsDir);
|
|
109
|
+
for (const dir of [sessionsDir, dshSyncDir]) {
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
112
|
+
} catch {
|
|
113
|
+
/* 收尾失败不影响主结果 */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
removed: true,
|
|
118
|
+
target,
|
|
119
|
+
others,
|
|
120
|
+
keptStructure: hasSyncStructure(workspacePath),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ----------------------------------------------------------- 决策与基线 */
|
|
125
|
+
|
|
126
|
+
/** 决策键:路径的大小写不敏感写法(Windows 下同一个目录只有一份)。 */
|
|
127
|
+
const keyOf = (p) => {
|
|
128
|
+
try {
|
|
129
|
+
return pathKey(p);
|
|
130
|
+
} catch {
|
|
131
|
+
return String(p || '').toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** 人为某个路径表过态吗?返回 'adopt' | 'decline' | null。 */
|
|
136
|
+
export function decisionOf(state, workspacePath) {
|
|
137
|
+
const bag = (state && state.features && state.features.decisions) || {};
|
|
138
|
+
const hit = bag[keyOf(workspacePath)];
|
|
139
|
+
if (!hit) return null;
|
|
140
|
+
return hit.action === 'decline' ? 'decline' : 'adopt';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 记一次人为表态。
|
|
145
|
+
*
|
|
146
|
+
* adopt 人点了「建格子」(之后缺了就自动补)
|
|
147
|
+
* decline 人点了「不要」(之后只列出来,不自动建)
|
|
148
|
+
*/
|
|
149
|
+
export function rememberDecision(state, workspacePath, action) {
|
|
150
|
+
if (!state.features || typeof state.features !== 'object') {
|
|
151
|
+
state.features = { initialized: false, decisions: {}, known: {} };
|
|
152
|
+
}
|
|
153
|
+
if (!state.features.decisions || typeof state.features.decisions !== 'object') {
|
|
154
|
+
state.features.decisions = {};
|
|
155
|
+
}
|
|
156
|
+
state.features.decisions[keyOf(workspacePath)] = {
|
|
157
|
+
action: action === 'decline' ? 'decline' : 'adopt',
|
|
158
|
+
path: normSlash(workspacePath),
|
|
159
|
+
at: Date.now(),
|
|
160
|
+
};
|
|
161
|
+
return state;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 清掉某路径的表态。 */
|
|
165
|
+
export function forgetDecision(state, workspacePath) {
|
|
166
|
+
const bag = (state.features && state.features.decisions) || null;
|
|
167
|
+
if (!bag) return state;
|
|
168
|
+
delete bag[keyOf(workspacePath)];
|
|
169
|
+
return state;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* --------------------------------------------------------------- 巡检计划 */
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 算本轮该对特征文件夹做什么。**纯函数:不落盘、不改任何文件、不注册工作区。**
|
|
176
|
+
*
|
|
177
|
+
* 输入:
|
|
178
|
+
* folders 特定文件夹前缀
|
|
179
|
+
* workspaces 当前工作区清单(id/path/title)
|
|
180
|
+
* discover 巡检扫出、「有 .dsh-sync 结构但没注册成工作区」的目录
|
|
181
|
+
* state 插件状态(读 features.known / decisions)
|
|
182
|
+
* machineId 本机标识
|
|
183
|
+
*
|
|
184
|
+
* 输出(正好对上那张矩阵):
|
|
185
|
+
* active 有我的格子 + 有工作区 → 正常
|
|
186
|
+
* createFolder 有工作区 + 没我的格子 → 补文件夹
|
|
187
|
+
* restoreWorkspace 有我的格子 + 没工作区 → 补工作区
|
|
188
|
+
* ask 没我的格子 + 没工作区 + 目录有 .dsh-sync → 问人
|
|
189
|
+
* drop 基线里有、现在清单里没有 → 工作区被删了,格子要跟着删
|
|
190
|
+
* seed 有格子的工作区(进基线,之后靠它判「被删除」)
|
|
191
|
+
*/
|
|
192
|
+
export function planFeatures({
|
|
193
|
+
folders = [],
|
|
194
|
+
workspaces = [],
|
|
195
|
+
discover = [],
|
|
196
|
+
state = {},
|
|
197
|
+
machineId = '',
|
|
198
|
+
} = {}) {
|
|
199
|
+
const features = (state && state.features) || {};
|
|
200
|
+
const known = features.known || {};
|
|
201
|
+
const seenNow = {};
|
|
202
|
+
const active = [];
|
|
203
|
+
const createFolder = [];
|
|
204
|
+
const restoreWorkspace = [];
|
|
205
|
+
const ask = [];
|
|
206
|
+
const drop = [];
|
|
207
|
+
const seed = [];
|
|
208
|
+
|
|
209
|
+
const underFolders = (p) => {
|
|
210
|
+
const key = keyOf(p);
|
|
211
|
+
return (folders || []).some((f) => {
|
|
212
|
+
const fk = keyOf(f);
|
|
213
|
+
return key === fk || key.startsWith(fk + path.sep) || key.startsWith(fk + '/');
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
const alreadyAsk = (p) => ask.some((x) => keyOf(x.path) === keyOf(p));
|
|
217
|
+
|
|
218
|
+
for (const ws of workspaces || []) {
|
|
219
|
+
if (!ws || !ws.path) continue;
|
|
220
|
+
const id = ws.id === undefined || ws.id === null ? keyOf(ws.path) : String(ws.id);
|
|
221
|
+
seenNow[id] = normSlash(ws.path);
|
|
222
|
+
const entry = { id, path: ws.path, title: ws.title || path.basename(ws.path) };
|
|
223
|
+
|
|
224
|
+
if (hasFeature(ws.path, machineId)) {
|
|
225
|
+
// 有我的格子 + 有工作区 → 正常
|
|
226
|
+
active.push(entry);
|
|
227
|
+
seed.push(entry);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 不在特定文件夹里的本来就不归本插件管
|
|
232
|
+
if (!underFolders(ws.path)) continue;
|
|
233
|
+
|
|
234
|
+
if (decisionOf(state, ws.path) === 'decline') {
|
|
235
|
+
// 人明确说过这个目录不要建 → 只列出来,不自作主张
|
|
236
|
+
ask.push({ ...entry, why: 'declined' });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 有工作区 + 没我的格子 → 补文件夹
|
|
241
|
+
createFolder.push({ ...entry, why: 'workspace-without-folder' });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 有 .dsh-sync 结构、但没注册成工作区的目录
|
|
245
|
+
const knownWorkspacePaths = new Set(Object.values(seenNow).map((p) => keyOf(p)));
|
|
246
|
+
for (const d of discover || []) {
|
|
247
|
+
if (!d || !d.path) continue;
|
|
248
|
+
if (alreadyAsk(d.path)) continue;
|
|
249
|
+
// 已经在「工作区清单」里的,前面那一轮已经按「有工作区没格子 → 补文件夹」处理过了,
|
|
250
|
+
// 这里必须跳过:同一个目录不能既补文件夹又问人(scanFolders 会把它同时扫进 discover)。
|
|
251
|
+
if (knownWorkspacePaths.has(keyOf(d.path))) continue;
|
|
252
|
+
const title = d.title || path.basename(d.path);
|
|
253
|
+
if (decisionOf(state, d.path) === 'decline') {
|
|
254
|
+
ask.push({ id: null, path: d.path, title, why: 'declined' });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (hasFeature(d.path, machineId)) {
|
|
258
|
+
// 有我的格子 + 没工作区 → 补工作区
|
|
259
|
+
restoreWorkspace.push({ id: null, path: d.path, title, why: 'folder-without-workspace' });
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// 没我的格子 + 没工作区 + 目录有结构 → 问人
|
|
263
|
+
const others = otherFeatures(d.path, machineId);
|
|
264
|
+
ask.push({
|
|
265
|
+
id: null,
|
|
266
|
+
path: d.path,
|
|
267
|
+
title,
|
|
268
|
+
why: others.length ? 'others-only' : 'structure-only',
|
|
269
|
+
others,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 基线里有、这一轮清单里没有 = 工作区被删了 → 格子跟着删
|
|
274
|
+
for (const [id, p] of Object.entries(known)) {
|
|
275
|
+
if (Object.prototype.hasOwnProperty.call(seenNow, id)) continue;
|
|
276
|
+
if (!p) continue;
|
|
277
|
+
if (!hasFeature(p, machineId)) continue;
|
|
278
|
+
drop.push({ id, path: p, title: path.basename(p) });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { active, createFolder, restoreWorkspace, ask, drop, seed, seenNow };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 执行计划里「不需要人点头」的部分:补文件夹、删格子,并回写基线。
|
|
286
|
+
*
|
|
287
|
+
* **不处理 `restoreWorkspace`**:注册工作区要走宿主接口(async),由调用方做。
|
|
288
|
+
* `ask` 更不能在这里动。
|
|
289
|
+
*/
|
|
290
|
+
export function applyFeaturePlan({ plan, state, machineId, log = () => {} } = {}) {
|
|
291
|
+
const created = [];
|
|
292
|
+
const dropped = [];
|
|
293
|
+
const errors = [];
|
|
294
|
+
|
|
295
|
+
for (const item of plan.createFolder || []) {
|
|
296
|
+
try {
|
|
297
|
+
adoptFeature(item.path, machineId);
|
|
298
|
+
created.push(item);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
errors.push({ path: item.path, message: String((err && err.message) || err) });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const item of plan.drop || []) {
|
|
304
|
+
try {
|
|
305
|
+
const out = dropFeature(item.path, machineId, { log });
|
|
306
|
+
if (out.removed) dropped.push({ ...item, others: out.others, keptStructure: out.keptStructure });
|
|
307
|
+
// 删干净之后不能再留任何记录,否则巡检会「记住」一个已经不该存在的目录
|
|
308
|
+
forgetDecision(state, item.path);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
errors.push({ path: item.path, message: String((err && err.message) || err) });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!state.features || typeof state.features !== 'object') {
|
|
315
|
+
state.features = { initialized: false, decisions: {}, known: {} };
|
|
316
|
+
}
|
|
317
|
+
const keep = {};
|
|
318
|
+
for (const item of plan.seed || []) keep[item.id] = item.path;
|
|
319
|
+
for (const item of created) keep[item.id] = item.path;
|
|
320
|
+
// 已有但本轮没扫到的(前缀被改掉等)保留,避免误判成「被删除」
|
|
321
|
+
const live = new Set(Object.keys(plan.seenNow || {}).map(String));
|
|
322
|
+
for (const [id, p] of Object.entries(state.features.known || {})) {
|
|
323
|
+
if (keep[id]) continue;
|
|
324
|
+
if (live.has(String(id))) continue;
|
|
325
|
+
if ((plan.drop || []).some((d) => String(d.id) === String(id))) continue;
|
|
326
|
+
keep[id] = p;
|
|
327
|
+
}
|
|
328
|
+
state.features.known = keep;
|
|
329
|
+
state.features.initialized = true;
|
|
330
|
+
state.features.lastPlanAt = Date.now();
|
|
331
|
+
|
|
332
|
+
return { created, dropped, errors };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 挑出「确实已经被删除」的工作区 id。**纯函数**,不碰磁盘。
|
|
337
|
+
*
|
|
338
|
+
* 为什么要二次确认:workspaceRegistry 在启动早期、注册表重建、或宿主正在
|
|
339
|
+
* 切换 profile 时可能短暂返回空清单/缺项。只凭一次快照就删特征文件夹,
|
|
340
|
+
* 等于把这个工作区的同步历史一把抹掉。规矩:
|
|
341
|
+
* - 清单为空 → 本轮什么都不确认(清除计时);
|
|
342
|
+
* - 同一个 id 连续两轮都不在,且间隔 ≥ minAgeMs,才确认删除。
|
|
343
|
+
*/
|
|
344
|
+
export function pickRemovedIds({ known = {}, liveIds = [], missingSince, now = Date.now(), minAgeMs = 30000 }) {
|
|
345
|
+
const next = missingSince instanceof Map ? missingSince : new Map();
|
|
346
|
+
const confirmed = [];
|
|
347
|
+
const live = new Set((liveIds || []).map(String));
|
|
348
|
+
|
|
349
|
+
if (!live.size) {
|
|
350
|
+
next.clear();
|
|
351
|
+
return { confirmed, missingSince: next };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (const id of [...next.keys()]) {
|
|
355
|
+
if (live.has(id) || !Object.prototype.hasOwnProperty.call(known, id)) next.delete(id);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const id of Object.keys(known)) {
|
|
359
|
+
if (live.has(id)) continue;
|
|
360
|
+
const since = next.get(id);
|
|
361
|
+
if (since === undefined) {
|
|
362
|
+
next.set(id, now);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (now - since >= minAgeMs) confirmed.push(id);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const id of confirmed) next.delete(id);
|
|
369
|
+
return { confirmed, missingSince: next };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 删掉「被删除的工作区」对应的本机格子(连同格里备份的对话),并清掉基线。
|
|
374
|
+
*
|
|
375
|
+
* 只做这一件事,不碰别人的格子;`.dsh-sync/` 是否被一起收掉由 dropFeature 判断。
|
|
376
|
+
* 删完不再留任何记录 —— 只有本机格子时巡检必须再也看不到它。
|
|
377
|
+
*/
|
|
378
|
+
export function reconcileRemoved(state, machineId, confirmedIds, { log = () => {} } = {}) {
|
|
379
|
+
const dropped = [];
|
|
380
|
+
const errors = [];
|
|
381
|
+
if (!state.features || typeof state.features !== 'object') return { dropped, errors };
|
|
382
|
+
const known = state.features.known || {};
|
|
383
|
+
for (const id of confirmedIds || []) {
|
|
384
|
+
const key = String(id);
|
|
385
|
+
const p = known[key];
|
|
386
|
+
if (!p) continue;
|
|
387
|
+
try {
|
|
388
|
+
const out = dropFeature(p, machineId, { log });
|
|
389
|
+
dropped.push({ id: key, path: p, others: out.others, keptStructure: out.keptStructure });
|
|
390
|
+
delete known[key];
|
|
391
|
+
forgetDecision(state, p);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
errors.push({ id: key, path: p, message: String((err && err.message) || err) });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { dropped, errors };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** 过滤:只留下「有本机特征文件夹」的工作区。备份/拉取/展示都用它。 */
|
|
400
|
+
export function onlyActive(list, machineId) {
|
|
401
|
+
return (list || []).filter((ws) => ws && ws.path && hasFeature(ws.path, machineId));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 某个工作区为什么没参与,给面板显示用。 */
|
|
405
|
+
export function inactiveReason(workspacePath, machineId, state) {
|
|
406
|
+
if (hasFeature(workspacePath, machineId)) return '';
|
|
407
|
+
if (decisionOf(state, workspacePath) === 'decline') return '你选了不在这个文件夹建本机特征文件夹';
|
|
408
|
+
return '还没有建本机特征文件夹(下次巡检会自动补上)';
|
|
409
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ygc-dsh-sync",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH 同步插件:会话按工作区落地、按机器隔离的跨机备份与拉取;技能本机↔母版双向同步(含记忆插件技能优化闭环);插件清单共享(只存清单与来源,不存源码,只能联网装)。",
|
|
6
6
|
"icon": "icon.svg",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"index.js",
|
|
25
25
|
"client.js",
|
|
26
26
|
"lib/core.js",
|
|
27
|
+
"lib/features.js",
|
|
27
28
|
"lib/plugins.js",
|
|
28
29
|
"lib/backup.js",
|
|
29
30
|
"lib/attach.js",
|
|
@@ -58,4 +59,4 @@
|
|
|
58
59
|
]
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
}
|
|
62
|
+
}
|
package/tools/draft-live.mjs
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
17
|
|
|
19
18
|
const args = process.argv.slice(2);
|
|
20
19
|
const has = (f) => args.includes(f);
|
|
@@ -31,11 +30,8 @@ const REPLY = val('--reply');
|
|
|
31
30
|
const LEDGER = path.join(process.env.USERPROFILE, '.mnemon', 'skills', 'ledger.json');
|
|
32
31
|
const LOCAL = path.join(process.env.USERPROFILE, '.agents', 'skills');
|
|
33
32
|
|
|
34
|
-
// 优先用源码仓(YGC_SYNC_SRC),否则用已装副本里的同一份 —— 两边逐字一致。
|
|
35
|
-
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
|
-
const SRC_ROOT = process.env.YGC_SYNC_SRC || PKG_ROOT;
|
|
37
33
|
const P = await import(
|
|
38
|
-
|
|
34
|
+
'file:///C:/Users/DSH/Documents/deepseek-harness/default-workspace/projects/dsh-sync-backup/lib/mnemon-pipeline.js'
|
|
39
35
|
);
|
|
40
36
|
const log = (m) => console.log(m);
|
|
41
37
|
|
package/tools/release-local.mjs
CHANGED
|
@@ -11,18 +11,10 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import crypto from 'node:crypto';
|
|
14
|
-
import os from 'node:os';
|
|
15
14
|
import { execFileSync } from 'node:child_process';
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const ROOT = process.env.YGC_SYNC_SRC || process.argv[2] || process.cwd();
|
|
20
|
-
const PROFILE = process.env.DSH_PROFILE_DIR
|
|
21
|
-
|| path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', process.env.DSH_PROFILE || 'desktop');
|
|
22
|
-
if (!fs.existsSync(path.join(ROOT, 'package.json'))) {
|
|
23
|
-
console.error('找不到源码根:' + ROOT + '(设 YGC_SYNC_SRC 或在仓库根运行)');
|
|
24
|
-
process.exit(2);
|
|
25
|
-
}
|
|
16
|
+
const ROOT = 'C:/Users/DSH/Documents/deepseek-harness/default-workspace/projects/dsh-sync-backup';
|
|
17
|
+
const PROFILE = 'C:/Users/DSH/.dsh/profiles/desktop';
|
|
26
18
|
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
27
19
|
const NAME = PKG.name;
|
|
28
20
|
|
package/tools/sync-lock.mjs
CHANGED
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
|
-
import os from 'node:os';
|
|
14
13
|
|
|
15
14
|
const tgz = process.argv[2];
|
|
16
|
-
const PROFILE = process.argv[3] ||
|
|
17
|
-
|| path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', process.env.DSH_PROFILE || 'desktop');
|
|
15
|
+
const PROFILE = process.argv[3] || 'C:/Users/DSH/.dsh/profiles/desktop';
|
|
18
16
|
if (!tgz || !fs.existsSync(tgz)) {
|
|
19
17
|
console.error('usage: node tools/sync-lock.mjs <tgz> [profile]');
|
|
20
18
|
process.exit(2);
|
|
@@ -18,8 +18,7 @@ if (!HASH) {
|
|
|
18
18
|
process.exit(2);
|
|
19
19
|
}
|
|
20
20
|
const NAME = 'ygc-dsh-sync-backup';
|
|
21
|
-
const PROFILE =
|
|
22
|
-
|| path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', process.env.DSH_PROFILE || 'desktop');
|
|
21
|
+
const PROFILE = 'C:/Users/DSH/.dsh/profiles/desktop';
|
|
23
22
|
|
|
24
23
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dsb-verify-'));
|
|
25
24
|
process.env.DSH_SYNC_SKILLS_ROOT = path.join(tmp, 'skills');
|