ygc-dsh-sync 1.2.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 +28 -17
- package/index.js +43 -16
- package/lib/api.js +1815 -1781
- package/lib/core.js +484 -484
- package/lib/features.js +154 -154
- package/package.json +62 -62
package/lib/features.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 本机特征文件夹(`<工作区>/.dsh-sync/sessions
|
|
2
|
+
* 本机特征文件夹(`<工作区>/.dsh-sync/sessions/<本机标识>/`)的采纳、撤销与巡检判据。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 判据只有一条:**磁盘上有没有那三个东西** —— 别人的特征文件夹、我的特征文件夹、
|
|
5
|
+
* 对应的工作区(DSH 的 workspaceRegistry 注册)。
|
|
6
|
+
* 用户 2026-11-14 定的巡检矩阵(这一层就是它的唯一实现):
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 4. **删工作区就删格**:本机把某个工作区从侧边栏删掉后,它的特征文件夹跟着删。
|
|
13
|
-
* 只删本机那一格,别人的格子一个字节都不动。
|
|
14
|
-
* 5. **没格子的不干活**:备份、拉取、自动备份、自动拉取、补登记,
|
|
15
|
-
* 一律只对「特征文件夹存在」的工作区生效。
|
|
8
|
+
* | 别人的格子 | 我的格子 | 工作区 | 动作 |
|
|
9
|
+
* |---|---|---|---|
|
|
10
|
+
* | 有 | 没有 | 没有 | **问人**:要不要在本机建特征文件夹(建了就顺带注册工作区) |
|
|
11
|
+
* | — | 有 | 有 | 正常,参与备份与拉取 |
|
|
12
|
+
* | — | 有 | 没有 | **补工作区**:把目录注册回侧边栏(不建也不删文件) |
|
|
13
|
+
* | — | 没有 | 有 | **补文件夹**:给这个工作区建本机特征文件夹 |
|
|
16
14
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* 删除的语义(同样是用户定的):
|
|
16
|
+
*
|
|
17
|
+
* - 删工作区时,**先看有没有别人的格子**:
|
|
18
|
+
* - 只有本机这一格 → 连 `.dsh-sync/` 一起收掉,巡检**再也扫不到**它(不留痕、不追问)。
|
|
19
|
+
* - 还有别人的格子 → 只删我这一格(连同格里备份的对话记录),`.dsh-sync/` 留着,
|
|
20
|
+
* 巡检据此认为「本机没有这个共享工作区」,进人工队列问要不要重建。
|
|
21
|
+
* - 插件**不提供**「只删格子、留着工作区」的入口:那会让工作区变成
|
|
22
|
+
* 「已注册但没格子」,下一轮必然被补回来,看起来就是「删了又冒出来」。
|
|
21
23
|
*/
|
|
22
24
|
import fs from 'node:fs';
|
|
23
25
|
import path from 'node:path';
|
|
@@ -30,12 +32,34 @@ export function featurePath(workspacePath, machineId) {
|
|
|
30
32
|
return ownRoot(workspacePath, machineId);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/**
|
|
35
|
+
/** 本机特征文件夹是否已存在(必须真的是目录)。 */
|
|
34
36
|
export function hasFeature(workspacePath, machineId) {
|
|
35
37
|
const st = safeStat(featurePath(workspacePath, machineId));
|
|
36
38
|
return !!st && st.isDirectory();
|
|
37
39
|
}
|
|
38
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
|
+
|
|
39
63
|
/**
|
|
40
64
|
* 建本机特征文件夹。
|
|
41
65
|
*
|
|
@@ -54,12 +78,15 @@ export function adoptFeature(workspacePath, machineId) {
|
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
/**
|
|
57
|
-
*
|
|
81
|
+
* 删本机特征文件夹(**连同格里备份的对话记录**)。
|
|
58
82
|
*
|
|
59
83
|
* 三条约束:
|
|
60
|
-
* -
|
|
61
|
-
* - `sessions/`
|
|
84
|
+
* - 只删本机那一格;别人的格子一个字节都不动;
|
|
85
|
+
* - `sessions/` 空了才把 `sessions/` 与 `.dsh-sync/` 一起收掉 ——
|
|
86
|
+
* 全收掉之后巡检就再也扫不到这个目录,符合「只有本机格子时删干净」;
|
|
62
87
|
* - 目标不存在算成功(幂等)。
|
|
88
|
+
*
|
|
89
|
+
* @returns {{removed:boolean, target?:string, keptStructure?:boolean, others?:string[], reason?:string}}
|
|
63
90
|
*/
|
|
64
91
|
export function dropFeature(workspacePath, machineId, { log = () => {} } = {}) {
|
|
65
92
|
if (!machineId) return { removed: false, reason: '缺少本机标识' };
|
|
@@ -67,11 +94,14 @@ export function dropFeature(workspacePath, machineId, { log = () => {} } = {}) {
|
|
|
67
94
|
if (path.basename(target) !== String(machineId)) {
|
|
68
95
|
return { removed: false, reason: '目标不是本机特征文件夹' };
|
|
69
96
|
}
|
|
97
|
+
const others = otherFeatures(workspacePath, machineId);
|
|
70
98
|
const st = safeStat(target);
|
|
71
|
-
if (!st)
|
|
72
|
-
|
|
99
|
+
if (!st) {
|
|
100
|
+
return { removed: false, reason: '本来就没有', others, keptStructure: hasSyncStructure(workspacePath) };
|
|
101
|
+
}
|
|
102
|
+
if (!st.isDirectory()) return { removed: false, reason: '不是目录', others };
|
|
73
103
|
fs.rmSync(target, { recursive: true, force: true });
|
|
74
|
-
log(
|
|
104
|
+
log(`删除本机特征文件夹(含其中备份的对话):${target}`);
|
|
75
105
|
|
|
76
106
|
// 收尾:空了才收,避免把别人的备份目录一起删掉
|
|
77
107
|
const sessionsDir = path.join(normSlash(workspacePath), TARGET_DIR_NAME);
|
|
@@ -83,7 +113,12 @@ export function dropFeature(workspacePath, machineId, { log = () => {} } = {}) {
|
|
|
83
113
|
/* 收尾失败不影响主结果 */
|
|
84
114
|
}
|
|
85
115
|
}
|
|
86
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
removed: true,
|
|
118
|
+
target,
|
|
119
|
+
others,
|
|
120
|
+
keptStructure: hasSyncStructure(workspacePath),
|
|
121
|
+
};
|
|
87
122
|
}
|
|
88
123
|
|
|
89
124
|
/* ----------------------------------------------------------- 决策与基线 */
|
|
@@ -97,23 +132,19 @@ const keyOf = (p) => {
|
|
|
97
132
|
}
|
|
98
133
|
};
|
|
99
134
|
|
|
100
|
-
/** 人为某个路径表过态吗?返回 'adopt' | 'decline' |
|
|
135
|
+
/** 人为某个路径表过态吗?返回 'adopt' | 'decline' | null。 */
|
|
101
136
|
export function decisionOf(state, workspacePath) {
|
|
102
137
|
const bag = (state && state.features && state.features.decisions) || {};
|
|
103
138
|
const hit = bag[keyOf(workspacePath)];
|
|
104
139
|
if (!hit) return null;
|
|
105
|
-
|
|
106
|
-
if (hit.action === 'ask') return 'ask';
|
|
107
|
-
return 'adopt';
|
|
140
|
+
return hit.action === 'decline' ? 'decline' : 'adopt';
|
|
108
141
|
}
|
|
109
142
|
|
|
110
143
|
/**
|
|
111
|
-
*
|
|
144
|
+
* 记一次人为表态。
|
|
112
145
|
*
|
|
113
146
|
* adopt 人点了「建格子」(之后缺了就自动补)
|
|
114
147
|
* decline 人点了「不要」(之后只列出来,不自动建)
|
|
115
|
-
* ask 已经在面板上问过人、还等回话 —— 不写这一条,下一轮巡检
|
|
116
|
-
* 会把「等待表态」误当成「本机新建」,直接替人建了格子。
|
|
117
148
|
*/
|
|
118
149
|
export function rememberDecision(state, workspacePath, action) {
|
|
119
150
|
if (!state.features || typeof state.features !== 'object') {
|
|
@@ -122,16 +153,15 @@ export function rememberDecision(state, workspacePath, action) {
|
|
|
122
153
|
if (!state.features.decisions || typeof state.features.decisions !== 'object') {
|
|
123
154
|
state.features.decisions = {};
|
|
124
155
|
}
|
|
125
|
-
const normalized = action === 'decline' ? 'decline' : action === 'ask' ? 'ask' : 'adopt';
|
|
126
156
|
state.features.decisions[keyOf(workspacePath)] = {
|
|
127
|
-
action:
|
|
157
|
+
action: action === 'decline' ? 'decline' : 'adopt',
|
|
128
158
|
path: normSlash(workspacePath),
|
|
129
159
|
at: Date.now(),
|
|
130
160
|
};
|
|
131
161
|
return state;
|
|
132
162
|
}
|
|
133
163
|
|
|
134
|
-
/**
|
|
164
|
+
/** 清掉某路径的表态。 */
|
|
135
165
|
export function forgetDecision(state, workspacePath) {
|
|
136
166
|
const bag = (state.features && state.features.decisions) || null;
|
|
137
167
|
if (!bag) return state;
|
|
@@ -142,22 +172,22 @@ export function forgetDecision(state, workspacePath) {
|
|
|
142
172
|
/* --------------------------------------------------------------- 巡检计划 */
|
|
143
173
|
|
|
144
174
|
/**
|
|
145
|
-
*
|
|
175
|
+
* 算本轮该对特征文件夹做什么。**纯函数:不落盘、不改任何文件、不注册工作区。**
|
|
146
176
|
*
|
|
147
177
|
* 输入:
|
|
148
178
|
* folders 特定文件夹前缀
|
|
149
179
|
* workspaces 当前工作区清单(id/path/title)
|
|
150
|
-
* discover
|
|
151
|
-
* state 插件状态(读 features.
|
|
180
|
+
* discover 巡检扫出、「有 .dsh-sync 结构但没注册成工作区」的目录
|
|
181
|
+
* state 插件状态(读 features.known / decisions)
|
|
152
182
|
* machineId 本机标识
|
|
153
183
|
*
|
|
154
|
-
*
|
|
155
|
-
* active
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* seed
|
|
184
|
+
* 输出(正好对上那张矩阵):
|
|
185
|
+
* active 有我的格子 + 有工作区 → 正常
|
|
186
|
+
* createFolder 有工作区 + 没我的格子 → 补文件夹
|
|
187
|
+
* restoreWorkspace 有我的格子 + 没工作区 → 补工作区
|
|
188
|
+
* ask 没我的格子 + 没工作区 + 目录有 .dsh-sync → 问人
|
|
189
|
+
* drop 基线里有、现在清单里没有 → 工作区被删了,格子要跟着删
|
|
190
|
+
* seed 有格子的工作区(进基线,之后靠它判「被删除」)
|
|
161
191
|
*/
|
|
162
192
|
export function planFeatures({
|
|
163
193
|
folders = [],
|
|
@@ -168,13 +198,12 @@ export function planFeatures({
|
|
|
168
198
|
} = {}) {
|
|
169
199
|
const features = (state && state.features) || {};
|
|
170
200
|
const known = features.known || {};
|
|
171
|
-
const initialized = features.initialized === true;
|
|
172
201
|
const seenNow = {};
|
|
173
|
-
const adopt = [];
|
|
174
|
-
const restore = [];
|
|
175
|
-
const drop = [];
|
|
176
|
-
const pending = [];
|
|
177
202
|
const active = [];
|
|
203
|
+
const createFolder = [];
|
|
204
|
+
const restoreWorkspace = [];
|
|
205
|
+
const ask = [];
|
|
206
|
+
const drop = [];
|
|
178
207
|
const seed = [];
|
|
179
208
|
|
|
180
209
|
const underFolders = (p) => {
|
|
@@ -184,6 +213,7 @@ export function planFeatures({
|
|
|
184
213
|
return key === fk || key.startsWith(fk + path.sep) || key.startsWith(fk + '/');
|
|
185
214
|
});
|
|
186
215
|
};
|
|
216
|
+
const alreadyAsk = (p) => ask.some((x) => keyOf(x.path) === keyOf(p));
|
|
187
217
|
|
|
188
218
|
for (const ws of workspaces || []) {
|
|
189
219
|
if (!ws || !ws.path) continue;
|
|
@@ -192,49 +222,55 @@ export function planFeatures({
|
|
|
192
222
|
const entry = { id, path: ws.path, title: ws.title || path.basename(ws.path) };
|
|
193
223
|
|
|
194
224
|
if (hasFeature(ws.path, machineId)) {
|
|
225
|
+
// 有我的格子 + 有工作区 → 正常
|
|
195
226
|
active.push(entry);
|
|
196
|
-
// 已有格子的一律进基线:首次运行靠它建档,之后靠它判「被删除」
|
|
197
227
|
seed.push(entry);
|
|
198
228
|
continue;
|
|
199
229
|
}
|
|
200
230
|
|
|
201
|
-
//
|
|
231
|
+
// 不在特定文件夹里的本来就不归本插件管
|
|
202
232
|
if (!underFolders(ws.path)) continue;
|
|
203
233
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
pending.push({ ...entry, why: 'declined' });
|
|
209
|
-
} else if (said === 'ask') {
|
|
210
|
-
// 已经在面板上问过、还没回话 → 继续等着,别替人决定
|
|
211
|
-
pending.push({ ...entry, why: 'awaiting' });
|
|
212
|
-
} else if (said === 'adopt' || wasKnown) {
|
|
213
|
-
// 人点过同意 / 曾经建过又没了 → 自动补
|
|
214
|
-
restore.push({ ...entry, why: wasKnown ? 'vanished' : 'adopted' });
|
|
215
|
-
} else if (!initialized) {
|
|
216
|
-
// 1.2.0 首次运行:存量工作区,交给人决定
|
|
217
|
-
pending.push({ ...entry, why: 'existing' });
|
|
218
|
-
} else {
|
|
219
|
-
// 基线建档之后才出现 = 本机新建的,默认建格子
|
|
220
|
-
adopt.push({ ...entry, why: 'new' });
|
|
234
|
+
if (decisionOf(state, ws.path) === 'decline') {
|
|
235
|
+
// 人明确说过这个目录不要建 → 只列出来,不自作主张
|
|
236
|
+
ask.push({ ...entry, why: 'declined' });
|
|
237
|
+
continue;
|
|
221
238
|
}
|
|
239
|
+
|
|
240
|
+
// 有工作区 + 没我的格子 → 补文件夹
|
|
241
|
+
createFolder.push({ ...entry, why: 'workspace-without-folder' });
|
|
222
242
|
}
|
|
223
243
|
|
|
224
|
-
//
|
|
244
|
+
// 有 .dsh-sync 结构、但没注册成工作区的目录
|
|
245
|
+
const knownWorkspacePaths = new Set(Object.values(seenNow).map((p) => keyOf(p)));
|
|
225
246
|
for (const d of discover || []) {
|
|
226
247
|
if (!d || !d.path) continue;
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
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({
|
|
230
265
|
id: null,
|
|
231
266
|
path: d.path,
|
|
232
|
-
title
|
|
233
|
-
why: '
|
|
267
|
+
title,
|
|
268
|
+
why: others.length ? 'others-only' : 'structure-only',
|
|
269
|
+
others,
|
|
234
270
|
});
|
|
235
271
|
}
|
|
236
272
|
|
|
237
|
-
//
|
|
273
|
+
// 基线里有、这一轮清单里没有 = 工作区被删了 → 格子跟着删
|
|
238
274
|
for (const [id, p] of Object.entries(known)) {
|
|
239
275
|
if (Object.prototype.hasOwnProperty.call(seenNow, id)) continue;
|
|
240
276
|
if (!p) continue;
|
|
@@ -242,53 +278,24 @@ export function planFeatures({
|
|
|
242
278
|
drop.push({ id, path: p, title: path.basename(p) });
|
|
243
279
|
}
|
|
244
280
|
|
|
245
|
-
return { active,
|
|
281
|
+
return { active, createFolder, restoreWorkspace, ask, drop, seed, seenNow };
|
|
246
282
|
}
|
|
247
283
|
|
|
248
284
|
/**
|
|
249
|
-
*
|
|
285
|
+
* 执行计划里「不需要人点头」的部分:补文件夹、删格子,并回写基线。
|
|
250
286
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*/
|
|
254
|
-
export function markAsked(state, plan) {
|
|
255
|
-
if (!state.features) state.features = { initialized: false, decisions: {}, known: {} };
|
|
256
|
-
let n = 0;
|
|
257
|
-
for (const item of (plan && plan.pending) || []) {
|
|
258
|
-
if (item.why !== 'existing' && item.why !== 'awaiting') continue;
|
|
259
|
-
if (decisionOf(state, item.path) === 'ask') continue;
|
|
260
|
-
rememberDecision(state, item.path, 'ask');
|
|
261
|
-
n++;
|
|
262
|
-
}
|
|
263
|
-
return n;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 执行计划里「不需要人点头」的部分(adopt / restore / drop + 基线),
|
|
268
|
-
* 并把首次运行标记打上。
|
|
269
|
-
*
|
|
270
|
-
* `pending` 不在这里处理:那一档必须由人显式调用 /features/adopt 才动。
|
|
287
|
+
* **不处理 `restoreWorkspace`**:注册工作区要走宿主接口(async),由调用方做。
|
|
288
|
+
* `ask` 更不能在这里动。
|
|
271
289
|
*/
|
|
272
290
|
export function applyFeaturePlan({ plan, state, machineId, log = () => {} } = {}) {
|
|
273
|
-
const
|
|
274
|
-
const restored = [];
|
|
291
|
+
const created = [];
|
|
275
292
|
const dropped = [];
|
|
276
293
|
const errors = [];
|
|
277
294
|
|
|
278
|
-
for (const item of plan.
|
|
279
|
-
try {
|
|
280
|
-
adoptFeature(item.path, machineId);
|
|
281
|
-
adopted.push(item);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
errors.push({ path: item.path, message: String((err && err.message) || err) });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
for (const item of plan.restore || []) {
|
|
295
|
+
for (const item of plan.createFolder || []) {
|
|
287
296
|
try {
|
|
288
297
|
adoptFeature(item.path, machineId);
|
|
289
|
-
|
|
290
|
-
forgetDecision(state, item.path);
|
|
291
|
-
log(`补建本机特征文件夹 ${item.path}`);
|
|
298
|
+
created.push(item);
|
|
292
299
|
} catch (err) {
|
|
293
300
|
errors.push({ path: item.path, message: String((err && err.message) || err) });
|
|
294
301
|
}
|
|
@@ -296,62 +303,33 @@ export function applyFeaturePlan({ plan, state, machineId, log = () => {} } = {}
|
|
|
296
303
|
for (const item of plan.drop || []) {
|
|
297
304
|
try {
|
|
298
305
|
const out = dropFeature(item.path, machineId, { log });
|
|
299
|
-
if (out.removed) dropped.push(item);
|
|
306
|
+
if (out.removed) dropped.push({ ...item, others: out.others, keptStructure: out.keptStructure });
|
|
307
|
+
// 删干净之后不能再留任何记录,否则巡检会「记住」一个已经不该存在的目录
|
|
308
|
+
forgetDecision(state, item.path);
|
|
300
309
|
} catch (err) {
|
|
301
310
|
errors.push({ path: item.path, message: String((err && err.message) || err) });
|
|
302
311
|
}
|
|
303
312
|
}
|
|
304
313
|
|
|
305
|
-
// 基线:只记「这一轮真的在清单里」且「现在有格子」的工作区
|
|
306
314
|
if (!state.features || typeof state.features !== 'object') {
|
|
307
315
|
state.features = { initialized: false, decisions: {}, known: {} };
|
|
308
316
|
}
|
|
309
|
-
// 这一轮问过人的,打上 ask 标记——否则下一轮 initialized 已是 true,
|
|
310
|
-
// 会被当成「本机新建」直接建格子,等于替人做了决定。
|
|
311
|
-
markAsked(state, plan);
|
|
312
317
|
const keep = {};
|
|
313
|
-
const live = new Set((plan.seenNow ? Object.keys(plan.seenNow) : []).map(String));
|
|
314
318
|
for (const item of plan.seed || []) keep[item.id] = item.path;
|
|
315
|
-
for (const item of
|
|
316
|
-
for (const item of restored) keep[item.id] = item.path;
|
|
319
|
+
for (const item of created) keep[item.id] = item.path;
|
|
317
320
|
// 已有但本轮没扫到的(前缀被改掉等)保留,避免误判成「被删除」
|
|
321
|
+
const live = new Set(Object.keys(plan.seenNow || {}).map(String));
|
|
318
322
|
for (const [id, p] of Object.entries(state.features.known || {})) {
|
|
319
323
|
if (keep[id]) continue;
|
|
320
324
|
if (live.has(String(id))) continue;
|
|
321
|
-
if (plan.drop
|
|
325
|
+
if ((plan.drop || []).some((d) => String(d.id) === String(id))) continue;
|
|
322
326
|
keep[id] = p;
|
|
323
327
|
}
|
|
324
328
|
state.features.known = keep;
|
|
325
329
|
state.features.initialized = true;
|
|
326
330
|
state.features.lastPlanAt = Date.now();
|
|
327
331
|
|
|
328
|
-
return {
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* 只做「删掉的工作区 → 删格子」这一件事。
|
|
333
|
-
*
|
|
334
|
-
* 供 30 秒一次的轻量巡检用:不扫盘、不建目录,只按给定 id 清单清格子。
|
|
335
|
-
* `confirmedIds` 由 pickRemovedIds 选出来(已经过二次确认)。
|
|
336
|
-
*/
|
|
337
|
-
export function reconcileRemoved(state, machineId, confirmedIds, { log = () => {} } = {}) {
|
|
338
|
-
const dropped = [];
|
|
339
|
-
const errors = [];
|
|
340
|
-
if (!state.features || typeof state.features !== 'object') return { dropped, errors };
|
|
341
|
-
const known = state.features.known || {};
|
|
342
|
-
for (const id of confirmedIds || []) {
|
|
343
|
-
const key = String(id);
|
|
344
|
-
const p = known[key];
|
|
345
|
-
if (!p) continue;
|
|
346
|
-
try {
|
|
347
|
-
const out = dropFeature(p, machineId, { log });
|
|
348
|
-
if (out.removed) dropped.push({ id: key, path: p });
|
|
349
|
-
delete known[key];
|
|
350
|
-
} catch (err) {
|
|
351
|
-
errors.push({ id: key, path: p, message: String((err && err.message) || err) });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return { dropped, errors };
|
|
332
|
+
return { created, dropped, errors };
|
|
355
333
|
}
|
|
356
334
|
|
|
357
335
|
/**
|
|
@@ -362,10 +340,6 @@ export function reconcileRemoved(state, machineId, confirmedIds, { log = () => {
|
|
|
362
340
|
* 等于把这个工作区的同步历史一把抹掉。规矩:
|
|
363
341
|
* - 清单为空 → 本轮什么都不确认(清除计时);
|
|
364
342
|
* - 同一个 id 连续两轮都不在,且间隔 ≥ minAgeMs,才确认删除。
|
|
365
|
-
*
|
|
366
|
-
* @param {{known:Object<string,string>, liveIds:string[], missingSince:Map<string,number>,
|
|
367
|
-
* now:number, minAgeMs:number}} input
|
|
368
|
-
* @returns {{confirmed:string[], missingSince:Map<string,number>}}
|
|
369
343
|
*/
|
|
370
344
|
export function pickRemovedIds({ known = {}, liveIds = [], missingSince, now = Date.now(), minAgeMs = 30000 }) {
|
|
371
345
|
const next = missingSince instanceof Map ? missingSince : new Map();
|
|
@@ -377,7 +351,6 @@ export function pickRemovedIds({ known = {}, liveIds = [], missingSince, now = D
|
|
|
377
351
|
return { confirmed, missingSince: next };
|
|
378
352
|
}
|
|
379
353
|
|
|
380
|
-
// 又回来了 / 基线里已经没有它了 → 清掉计时
|
|
381
354
|
for (const id of [...next.keys()]) {
|
|
382
355
|
if (live.has(id) || !Object.prototype.hasOwnProperty.call(known, id)) next.delete(id);
|
|
383
356
|
}
|
|
@@ -396,7 +369,34 @@ export function pickRemovedIds({ known = {}, liveIds = [], missingSince, now = D
|
|
|
396
369
|
return { confirmed, missingSince: next };
|
|
397
370
|
}
|
|
398
371
|
|
|
399
|
-
/**
|
|
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
400
|
export function onlyActive(list, machineId) {
|
|
401
401
|
return (list || []).filter((ws) => ws && ws.path && hasFeature(ws.path, machineId));
|
|
402
402
|
}
|
|
@@ -405,5 +405,5 @@ export function onlyActive(list, machineId) {
|
|
|
405
405
|
export function inactiveReason(workspacePath, machineId, state) {
|
|
406
406
|
if (hasFeature(workspacePath, machineId)) return '';
|
|
407
407
|
if (decisionOf(state, workspacePath) === 'decline') return '你选了不在这个文件夹建本机特征文件夹';
|
|
408
|
-
return '
|
|
408
|
+
return '还没有建本机特征文件夹(下次巡检会自动补上)';
|
|
409
409
|
}
|
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ygc-dsh-sync",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "DSH 同步插件:会话按工作区落地、按机器隔离的跨机备份与拉取;技能本机↔母版双向同步(含记忆插件技能优化闭环);插件清单共享(只存清单与来源,不存源码,只能联网装)。",
|
|
6
|
-
"icon": "icon.svg",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"dsh",
|
|
9
|
-
"deepseek-harness",
|
|
10
|
-
"dsh-plugin",
|
|
11
|
-
"session",
|
|
12
|
-
"sync",
|
|
13
|
-
"backup",
|
|
14
|
-
"plugin-manifest"
|
|
15
|
-
],
|
|
16
|
-
"license": "MIT",
|
|
17
|
-
"exports": {
|
|
18
|
-
".": "./index.js",
|
|
19
|
-
"./client": "./client.js",
|
|
20
|
-
"./locale/*.json": "./locale/*.json",
|
|
21
|
-
"./icon": "./icon.svg"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"index.js",
|
|
25
|
-
"client.js",
|
|
26
|
-
"lib/core.js",
|
|
27
|
-
"lib/features.js",
|
|
28
|
-
"lib/plugins.js",
|
|
29
|
-
"lib/backup.js",
|
|
30
|
-
"lib/attach.js",
|
|
31
|
-
"lib/api.js",
|
|
32
|
-
"lib/patrol.js",
|
|
33
|
-
"lib/skills.js",
|
|
34
|
-
"lib/mnemon.js",
|
|
35
|
-
"lib/mnemon-optimize.js",
|
|
36
|
-
"lib/changelog.js",
|
|
37
|
-
"lib/zstd-frame.js",
|
|
38
|
-
"cordis.patch.yml",
|
|
39
|
-
"locale/*.json",
|
|
40
|
-
"icon.svg",
|
|
41
|
-
"README.md",
|
|
42
|
-
"tools/*.mjs",
|
|
43
|
-
"lib/mnemon-pipeline.js",
|
|
44
|
-
"lib/mnemon-drafter.js"
|
|
45
|
-
],
|
|
46
|
-
"publishConfig": {
|
|
47
|
-
"registry": "https://registry.npmjs.org/",
|
|
48
|
-
"access": "public"
|
|
49
|
-
},
|
|
50
|
-
"dsh": {
|
|
51
|
-
"bundle": {
|
|
52
|
-
"patch": "./cordis.patch.yml"
|
|
53
|
-
},
|
|
54
|
-
"client": {
|
|
55
|
-
"platform": "web",
|
|
56
|
-
"immediately": true,
|
|
57
|
-
"inject": [
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-conversation"
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ygc-dsh-sync",
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "DSH 同步插件:会话按工作区落地、按机器隔离的跨机备份与拉取;技能本机↔母版双向同步(含记忆插件技能优化闭环);插件清单共享(只存清单与来源,不存源码,只能联网装)。",
|
|
6
|
+
"icon": "icon.svg",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"dsh-plugin",
|
|
11
|
+
"session",
|
|
12
|
+
"sync",
|
|
13
|
+
"backup",
|
|
14
|
+
"plugin-manifest"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./index.js",
|
|
19
|
+
"./client": "./client.js",
|
|
20
|
+
"./locale/*.json": "./locale/*.json",
|
|
21
|
+
"./icon": "./icon.svg"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"client.js",
|
|
26
|
+
"lib/core.js",
|
|
27
|
+
"lib/features.js",
|
|
28
|
+
"lib/plugins.js",
|
|
29
|
+
"lib/backup.js",
|
|
30
|
+
"lib/attach.js",
|
|
31
|
+
"lib/api.js",
|
|
32
|
+
"lib/patrol.js",
|
|
33
|
+
"lib/skills.js",
|
|
34
|
+
"lib/mnemon.js",
|
|
35
|
+
"lib/mnemon-optimize.js",
|
|
36
|
+
"lib/changelog.js",
|
|
37
|
+
"lib/zstd-frame.js",
|
|
38
|
+
"cordis.patch.yml",
|
|
39
|
+
"locale/*.json",
|
|
40
|
+
"icon.svg",
|
|
41
|
+
"README.md",
|
|
42
|
+
"tools/*.mjs",
|
|
43
|
+
"lib/mnemon-pipeline.js",
|
|
44
|
+
"lib/mnemon-drafter.js"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"registry": "https://registry.npmjs.org/",
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dsh": {
|
|
51
|
+
"bundle": {
|
|
52
|
+
"patch": "./cordis.patch.yml"
|
|
53
|
+
},
|
|
54
|
+
"client": {
|
|
55
|
+
"platform": "web",
|
|
56
|
+
"immediately": true,
|
|
57
|
+
"inject": [
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|