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 CHANGED
@@ -113,7 +113,8 @@ function makePanel(React) {
113
113
  const [note, setNote] = useState(null);
114
114
  const [midDraft, setMidDraft] = useState('');
115
115
  const [scan, setScan] = useState(null);
116
- const [registerPicks, setRegisterPicks] = useState([]);
116
+ // 特征文件夹采纳:面板上待人工决定的那些(路径数组)
117
+ const [featurePicks, setFeaturePicks] = useState([]);
117
118
  const [patrolTime, setPatrolTime] = useState('');
118
119
  const [skillScan, setSkillScan] = useState(null);
119
120
  const [skillPicks, setSkillPicks] = useState([]);
@@ -186,10 +187,19 @@ function makePanel(React) {
186
187
  setBusy('backup');
187
188
  setNote(null);
188
189
  try {
189
- const r = await call('/backup', { workspaceIds: selected || [] });
190
+ // 只执行「选中」的工作区;一个都没选就什么都不做(不默认全选)
191
+ const ids = selected || [];
192
+ if (!ids.length) {
193
+ setNote({ tone: 'err', text: '请先勾选要备份的工作区' });
194
+ return;
195
+ }
196
+ const r = await call('/backup', { workspaceIds: ids });
197
+ const skipped = (r.skipped || []).length;
190
198
  setNote({
191
199
  tone: 'ok',
192
- text: `备份完成:新增 ${r.summary.created}、更新 ${r.summary.updated},共 ${fmtBytes(r.summary.bytes)}`,
200
+ text:
201
+ `备份完成:新增 ${r.summary.created}、更新 ${r.summary.updated},共 ${fmtBytes(r.summary.bytes)}` +
202
+ (skipped ? `;跳过 ${skipped} 个没有本机特征文件夹的` : ''),
193
203
  });
194
204
  await load();
195
205
  } catch (e) {
@@ -203,17 +213,26 @@ function makePanel(React) {
203
213
  setBusy('pull');
204
214
  setNote(null);
205
215
  try {
206
- const r = await call('/pull', { workspaceIds: selected || [] });
216
+ const ids = selected || [];
217
+ if (!ids.length) {
218
+ setNote({ tone: 'err', text: '请先勾选要拉取的工作区' });
219
+ return;
220
+ }
221
+ const r = await call('/pull', { workspaceIds: ids });
207
222
  const s = r.summary;
208
223
  const parts = [`新增 ${s.imported}`, `更新 ${s.updated}`];
209
224
  if (s.forked) parts.push(`分叉 ${s.forked}`);
225
+ // 归属登记是拉取的一部分,不再单列按钮
210
226
  if (s.attached) parts.push(`登记归属 ${s.attached}`);
211
227
  if (s.attachFailed) parts.push(`登记失败 ${s.attachFailed}`);
228
+ if (r.unregistered && r.unregistered.length) {
229
+ parts.push(`${r.unregistered.length} 个目录要你先建特征文件夹`);
230
+ }
212
231
  setNote({
213
232
  tone: r.needsRestart ? 'warn' : 'ok',
214
233
  text: `拉取完成:${parts.join('、')}${
215
234
  r.needsRestart
216
- ? '。有会话没能登记进工作区,重启 DSH 后仍会落在「未分组」,请看状态栏提示。'
235
+ ? '。有会话没能登记进工作区,重启 DSH 后仍会落在「未分组」。'
217
236
  : '。会话已归到对应工作区,侧栏可直接看到。'
218
237
  }`,
219
238
  });
@@ -226,23 +245,84 @@ function makePanel(React) {
226
245
  };
227
246
 
228
247
  /**
229
- * 补登记:把旧版拉进来、但没进工作区归属账的会话修回去。
248
+ * 采纳:在选中的目录里建本机特征文件夹(未注册的顺带注册成工作区)。
230
249
  *
231
- * 默认只补拉取产物;自动化/子代理留下的会话不算,不会一次刷出上百条。
250
+ * 这是「检测到匹配的工作区文件夹 → 人工选择」的那个人工入口。
251
+ * 点完立刻生效:进侧边栏,参与备份与拉取。
232
252
  */
233
- const doAttachRepair = async () => {
234
- setBusy('attach');
253
+ const doAdopt = async (paths) => {
254
+ const list = (paths || featurePicks || []).filter(Boolean);
255
+ if (!list.length) {
256
+ setNote({ tone: 'err', text: '请先勾选要建特征文件夹的目录' });
257
+ return;
258
+ }
259
+ setBusy('adopt');
235
260
  setNote(null);
236
261
  try {
237
- const r = await call('/attach/repair', { workspaceIds: selected || [] });
238
- const s = r.summary;
262
+ const r = await call('/features/adopt', { paths: list });
263
+ const bits = [`已建本机特征文件夹 ${r.adopted.length} 个`];
264
+ if (r.registered && r.registered.length) bits.push(`注册工作区 ${r.registered.length} 个`);
265
+ if (r.errors && r.errors.length) bits.push(`${r.errors.length} 个失败`);
266
+ setNote({ tone: r.errors && r.errors.length ? 'warn' : 'ok', text: bits.join(',') });
267
+ setFeaturePicks([]);
268
+ await load();
269
+ } catch (e) {
270
+ setNote({ tone: 'err', text: String((e && e.message) || e) });
271
+ } finally {
272
+ setBusy('');
273
+ }
274
+ };
275
+
276
+ /** 跳过:这个目录本机不建格子,之后巡检不再反复问(可在面板上改主意)。 */
277
+ const doDecline = async (paths) => {
278
+ const list = (paths || featurePicks || []).filter(Boolean);
279
+ if (!list.length) {
280
+ setNote({ tone: 'err', text: '请先勾选要跳过的目录' });
281
+ return;
282
+ }
283
+ setBusy('decline');
284
+ setNote(null);
285
+ try {
286
+ await call('/features/decline', { paths: list });
287
+ setNote({ tone: 'ok', text: `已跳过 ${list.length} 个目录(本机不建特征文件夹)` });
288
+ setFeaturePicks([]);
289
+ await load();
290
+ } catch (e) {
291
+ setNote({ tone: 'err', text: String((e && e.message) || e) });
292
+ } finally {
293
+ setBusy('');
294
+ }
295
+ };
296
+
297
+ /**
298
+ * 删工作区(= 删本机特征文件夹,1.2.1 起两者是同一件事)。
299
+ *
300
+ * 插件里不再提供「只删格子、留着工作区」的入口:那条路会让工作区变成
301
+ * 「已注册但没格子」,下一轮巡检必然把它列回人工队列,用户看到的就是
302
+ * 「删了又自己冒出来」。所以这里只保留一个动作,且必须删注册。
303
+ */
304
+ const doDrop = async (paths) => {
305
+ const list = (paths || []).filter(Boolean);
306
+ if (!list.length) return;
307
+ if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
308
+ if (
309
+ !window.confirm(
310
+ '会从侧边栏删除这个工作区,并清掉本机特征文件夹(本机不再同步它)。\n目录内容、会话文件、其他机器的备份都不受影响。继续?',
311
+ )
312
+ ) {
313
+ return;
314
+ }
315
+ }
316
+ setBusy('drop');
317
+ setNote(null);
318
+ try {
319
+ const r = await call('/features/drop', { paths: list });
320
+ const errs = (r.errors || []).length;
239
321
  setNote({
240
- tone: s.failed ? 'warn' : s.attached ? 'ok' : 'ok',
241
- text: s.attached
242
- ? `补登记完成:${s.attached} 个会话已归回工作区${s.failed ? `,${s.failed} 个失败(路径不可达)` : ''}`
243
- : s.failed
244
- ? `没有可补的会话;${s.failed} 个登记失败,多半是工作区路径当前不可访问`
245
- : '补登记完成:没有需要补的会话(都已在正确的工作区里)',
322
+ tone: errs ? 'warn' : 'ok',
323
+ text: `已删除 ${r.dropped.length} 个工作区,并清掉对应本机特征文件夹${
324
+ errs ? `;${errs} 个失败:${r.errors[0].message}` : ''
325
+ }`,
246
326
  });
247
327
  await load();
248
328
  } catch (e) {
@@ -274,13 +354,14 @@ function makePanel(React) {
274
354
  try {
275
355
  const r = await call('/patrol/scan', {});
276
356
  setScan(r);
277
- const nd = r.discover.length;
278
- const nr = r.repair.length;
357
+ const np = (r.pending || []).length;
358
+ // 只勾选还没表态过的(被拒绝过的默认不勾,免得误操作)
359
+ setFeaturePicks((r.pending || []).filter((p) => p.why === 'unregistered' || p.why === 'existing').map((p) => p.path));
279
360
  setNote({
280
- tone: nd || nr ? 'warn' : 'ok',
281
- text: nd || nr
282
- ? `巡检完成:发现 ${nd} 个待注册文件夹、${nr} 个缺特征文件夹的工作区`
283
- : '巡检完成:没有需要处理的项目',
361
+ tone: np ? 'warn' : 'ok',
362
+ text: np
363
+ ? `巡检完成:${np} 个目录等你决定要不要在本机建特征文件夹`
364
+ : '巡检完成:没有需要你决定的项目',
284
365
  });
285
366
  await load();
286
367
  } catch (e) {
@@ -290,38 +371,18 @@ function makePanel(React) {
290
371
  }
291
372
  };
292
373
 
293
- const doRegister = async () => {
294
- const picked = (scan && scan.discover ? scan.discover : [])
295
- .filter((d) => (registerPicks || []).includes(d.path))
296
- .map((d) => d.path);
297
- if (!picked.length) {
298
- setNote({ tone: 'err', text: '请先勾选要注册的文件夹' });
374
+ /** 补建格子:只补面板上选中的工作区(不再「一点就全补」)。 */
375
+ const doRepair = async () => {
376
+ const ids = selected || [];
377
+ const targets = (state.workspaces || []).filter((w) => ids.includes(w.id)).map((w) => w.path);
378
+ if (!targets.length) {
379
+ setNote({ tone: 'err', text: '请先在「匹配的工作区」里勾选要补建特征文件夹的工作区' });
299
380
  return;
300
381
  }
301
- setBusy('register');
302
- try {
303
- const r = await call('/patrol/register', { paths: picked });
304
- setNote({
305
- tone: r.errors.length ? 'warn' : 'ok',
306
- text: `已注册 ${r.registered.length} 个工作区${
307
- r.errors.length ? `,${r.errors.length} 个失败` : ''
308
- },本机特征文件夹已补齐`,
309
- });
310
- setRegisterPicks([]);
311
- setScan(null);
312
- await load();
313
- } catch (e) {
314
- setNote({ tone: 'err', text: String((e && e.message) || e) });
315
- } finally {
316
- setBusy('');
317
- }
318
- };
319
-
320
- const doRepair = async () => {
321
382
  setBusy('repair');
322
383
  try {
323
- const r = await call('/patrol/repair', {});
324
- setNote({ tone: 'ok', text: `已补全 ${r.created.length} 个特征文件夹` });
384
+ const r = await call('/patrol/repair', { paths: targets });
385
+ setNote({ tone: 'ok', text: `已补建 ${r.created.length} 个本机特征文件夹` });
325
386
  await load();
326
387
  } catch (e) {
327
388
  setNote({ tone: 'err', text: String((e && e.message) || e) });
@@ -648,7 +709,7 @@ function makePanel(React) {
648
709
  h(
649
710
  'p',
650
711
  { className: 'dsb-sub', style: { marginBottom: 12 } },
651
- '扫描特定文件夹下的直接子目录:含 .dsh-sync/ 但未注册的会列出来等你确认;已注册但缺本机特征文件夹的自动补齐。',
712
+ '扫描特定文件夹下的直接子目录:含 .dsh-sync/ 但本机还没建特征文件夹的会列出来等你在下面勾选;建了格子才参与备份与拉取。被别人/别的工具删掉的格子会自动补回来。',
652
713
  ),
653
714
  h(
654
715
  'div',
@@ -660,8 +721,8 @@ function makePanel(React) {
660
721
  ),
661
722
  h(
662
723
  'button',
663
- { className: 'dsb-btn dsb-btn-sm', onClick: doRepair, disabled: !!busy },
664
- busy === 'repair' ? '补全中…' : '补全特征文件夹',
724
+ { className: 'dsb-btn dsb-btn-sm', onClick: doRepair, disabled: !!busy || !(selected || []).length },
725
+ busy === 'repair' ? '补建中…' : '给选中项补建格子',
665
726
  ),
666
727
  h(
667
728
  'label',
@@ -695,71 +756,103 @@ function makePanel(React) {
695
756
  h('span', { className: 'dsb-row-t' }, '分钟巡检一次'),
696
757
  ),
697
758
 
698
- // 扫描结果
699
- scan
700
- ? h(
701
- 'div',
702
- { style: { marginTop: 14 } },
703
- scan.discover.length
704
- ? h(
705
- 'div',
706
- null,
707
- h('div', { className: 'dsb-row-t', style: { marginBottom: 6 } }, `发现 ${scan.discover.length} 个待注册文件夹`),
708
- scan.discover.map((d) =>
759
+ // 巡检结果:待人工决定的采纳候选
760
+ (scan || (state.features && state.features.pendingCount))
761
+ ? (() => {
762
+ const pendingList = (scan && scan.pending) || (state.features && state.features.pending) || [];
763
+ const whyText = {
764
+ // 矩阵里唯一要人决定的两档
765
+ othersOnly: '别的机器有特征文件夹,本机没有',
766
+ structureOnly: '目录里有同步结构,本机没建特征文件夹',
767
+ unregistered: '检测到匹配目录,本机没建特征文件夹',
768
+ existing: '已注册成工作区,本机没建特征文件夹',
769
+ declined: '你之前选了不建(可改主意)',
770
+ };
771
+ return h(
772
+ 'div',
773
+ { style: { marginTop: 14 } },
774
+ pendingList.length
775
+ ? h(
776
+ 'div',
777
+ null,
709
778
  h(
710
779
  'div',
711
- { className: 'dsb-ws', key: d.path },
712
- h('input', {
713
- type: 'checkbox',
714
- className: 'dsb-chk',
715
- checked: registerPicks.includes(d.path),
716
- onChange: () =>
717
- setRegisterPicks((cur) =>
718
- cur.includes(d.path) ? cur.filter((x) => x !== d.path) : [...cur, d.path],
719
- ),
720
- }),
780
+ { className: 'dsb-row-t', style: { marginBottom: 6 } },
781
+ `${pendingList.length} 个目录等你决定:要不要在本机建特征文件夹`,
782
+ ),
783
+ pendingList.map((d) =>
721
784
  h(
722
785
  'div',
723
- { className: 'dsb-ws-body' },
724
- h('div', { className: 'dsb-row-t' }, d.title),
725
- h('div', { className: 'dsb-row-d' }, d.path),
786
+ { className: 'dsb-ws', key: d.path },
787
+ h('input', {
788
+ type: 'checkbox',
789
+ className: 'dsb-chk',
790
+ checked: featurePicks.includes(d.path),
791
+ onChange: () =>
792
+ setFeaturePicks((cur) =>
793
+ cur.includes(d.path) ? cur.filter((x) => x !== d.path) : [...cur, d.path],
794
+ ),
795
+ }),
796
+ h(
797
+ 'div',
798
+ { className: 'dsb-ws-body' },
799
+ h(
800
+ 'div',
801
+ { className: 'dsb-row-t' },
802
+ d.title,
803
+ h('span', { className: 'dsb-tag dsb-tag-warn' }, whyText[d.why] || d.why),
804
+ ),
805
+ h('div', { className: 'dsb-row-d' }, d.path),
806
+ d.others && d.others.length
807
+ ? h('div', { className: 'dsb-row-d' }, `别的机器:${d.others.join(' · ')}`)
808
+ : null,
809
+ ),
726
810
  ),
727
811
  ),
728
- ),
729
- h(
730
- 'div',
731
- { className: 'dsb-actions', style: { marginTop: 10 } },
732
812
  h(
733
- 'button',
734
- {
735
- className: 'dsb-btn dsb-btn-primary dsb-btn-sm',
736
- disabled: !!busy || !registerPicks.length,
737
- onClick: doRegister,
738
- },
739
- busy === 'register' ? '注册中…' : `注册选中(${registerPicks.length})`,
813
+ 'div',
814
+ { className: 'dsb-actions', style: { marginTop: 10 } },
815
+ h(
816
+ 'button',
817
+ {
818
+ className: 'dsb-btn dsb-btn-primary dsb-btn-sm',
819
+ disabled: !!busy || !featurePicks.length,
820
+ onClick: () => doAdopt(),
821
+ },
822
+ busy === 'adopt' ? '建立中…' : `建特征文件夹并注册(${featurePicks.length})`,
823
+ ),
824
+ h(
825
+ 'button',
826
+ {
827
+ className: 'dsb-btn dsb-btn-sm',
828
+ disabled: !!busy || !featurePicks.length,
829
+ onClick: () => doDecline(),
830
+ },
831
+ busy === 'decline' ? '记录中…' : '跳过这些目录',
832
+ ),
833
+ h(
834
+ 'button',
835
+ {
836
+ className: 'dsb-btn dsb-btn-sm',
837
+ onClick: () => setFeaturePicks(pendingList.map((d) => d.path)),
838
+ },
839
+ '全选',
840
+ ),
841
+ h(
842
+ 'button',
843
+ { className: 'dsb-btn dsb-btn-sm', onClick: () => setFeaturePicks([]) },
844
+ '清空选择',
845
+ ),
740
846
  ),
741
847
  h(
742
- 'button',
743
- {
744
- className: 'dsb-btn dsb-btn-sm',
745
- onClick: () => setRegisterPicks(scan.discover.map((d) => d.path)),
746
- },
747
- '全选',
848
+ 'p',
849
+ { className: 'dsb-sub', style: { marginTop: 8 } },
850
+ '建完立刻进侧边栏、参与备份与拉取;跳过的目录本机不建格子,也不参与同步。',
748
851
  ),
749
- ),
750
- )
751
- : h('div', { className: 'dsb-empty' }, '没有待注册的文件夹'),
752
- scan.repair.length
753
- ? h(
754
- 'div',
755
- { style: { marginTop: 12 } },
756
- h('div', { className: 'dsb-row-t' }, `缺本机特征文件夹:${scan.repair.length} 个`),
757
- scan.repair.map((r) =>
758
- h('div', { className: 'dsb-row-d', key: r.path }, `${r.title} — ${r.path}`),
759
- ),
760
- )
761
- : null,
762
- )
852
+ )
853
+ : h('div', { className: 'dsb-empty' }, '没有待你决定的目录'),
854
+ );
855
+ })()
763
856
  : null,
764
857
  ),
765
858
 
@@ -837,20 +930,21 @@ function makePanel(React) {
837
930
  { className: 'dsb-actions', style: { marginTop: 14 } },
838
931
  h(
839
932
  'button',
840
- { className: 'dsb-btn dsb-btn-primary', onClick: doBackup, disabled: !!busy || ws.length === 0 },
841
- busy === 'backup' ? '备份中…' : '立即备份全部',
933
+ { className: 'dsb-btn dsb-btn-primary', onClick: doBackup, disabled: !!busy || !(selected || []).length },
934
+ busy === 'backup' ? '备份中…' : `备份选中的 ${(selected || []).length} 个`,
842
935
  ),
843
936
  h(
844
937
  'button',
845
- { className: 'dsb-btn', onClick: doPull, disabled: !!busy || ws.length === 0 },
846
- busy === 'pull' ? '拉取中…' : '一键拉取',
938
+ { className: 'dsb-btn', onClick: doPull, disabled: !!busy || !(selected || []).length },
939
+ busy === 'pull' ? '拉取中…' : `拉取选中的 ${(selected || []).length} 个`,
847
940
  ),
848
941
  h(
849
- 'button',
850
- { className: 'dsb-btn', onClick: doAttachRepair, disabled: !!busy || ws.length === 0 },
851
- busy === 'attach' ? '补登记中…' : '补登记归属',
942
+ 'span',
943
+ { className: 'dsb-row-d' },
944
+ (selected || []).length
945
+ ? `已选 ${(selected || []).length} 个工作区(只对有本机特征文件夹的生效;拉取时自动登记归属)`
946
+ : '请先勾选工作区:按钮只执行选中项',
852
947
  ),
853
- h('span', { className: 'dsb-row-d' }, `已选 ${(selected || []).length} 个工作区`),
854
948
  ),
855
949
  ),
856
950
 
@@ -860,7 +954,7 @@ function makePanel(React) {
860
954
  h(
861
955
  'div',
862
956
  { className: 'dsb-card-h' },
863
- h('h3', { className: 'dsb-card-t' }, '匹配的工作区'),
957
+ h('h3', { className: 'dsb-card-t' }, '匹配的工作区(有本机特征文件夹)'),
864
958
  ws.length
865
959
  ? h(
866
960
  'button',
@@ -873,7 +967,11 @@ function makePanel(React) {
873
967
  : null,
874
968
  ),
875
969
  ws.length === 0
876
- ? h('div', { className: 'dsb-empty' }, '没有工作区落在已设置的文件夹里。')
970
+ ? h(
971
+ 'div',
972
+ { className: 'dsb-empty' },
973
+ '还没有工作区建好本机特征文件夹。在上面的巡检结果里勾选目录、点「建特征文件夹并注册」才会出现在这里。',
974
+ )
877
975
  : ws.map((w) =>
878
976
  h(
879
977
  'div',
@@ -912,9 +1010,58 @@ function makePanel(React) {
912
1010
  w.others.map((o) => `${o.machineId}:${o.sessions} 个`).join(' · '),
913
1011
  )
914
1012
  : null,
1013
+ h(
1014
+ 'div',
1015
+ { className: 'dsb-actions', style: { marginTop: 6 } },
1016
+ h(
1017
+ 'button',
1018
+ {
1019
+ className: 'dsb-btn dsb-btn-sm dsb-btn-danger',
1020
+ disabled: !!busy,
1021
+ title: '删除工作区,连带清掉本机特征文件夹;目录内容与别人的备份不动',
1022
+ onClick: () => doDrop([w.path]),
1023
+ },
1024
+ '删除工作区',
1025
+ ),
1026
+ ),
915
1027
  ),
916
1028
  ),
917
1029
  ),
1030
+
1031
+ // 匹配但没建格子的:列在这里,方便随时补上
1032
+ (state.inactive || []).length
1033
+ ? h(
1034
+ 'div',
1035
+ { style: { marginTop: 14 } },
1036
+ h(
1037
+ 'div',
1038
+ { className: 'dsb-row-t', style: { marginBottom: 6 } },
1039
+ `未参与同步:${state.inactive.length} 个(没有本机特征文件夹)`,
1040
+ ),
1041
+ (state.inactive || []).map((w) =>
1042
+ h(
1043
+ 'div',
1044
+ { className: 'dsb-row', key: w.id },
1045
+ h(
1046
+ 'div',
1047
+ { className: 'dsb-row-main' },
1048
+ h('div', { className: 'dsb-row-t' }, w.title),
1049
+ h('div', { className: 'dsb-row-d' }, w.reason || '未建本机特征文件夹'),
1050
+ h('div', { className: 'dsb-row-d' }, w.path),
1051
+ ),
1052
+ h(
1053
+ 'button',
1054
+ {
1055
+ className: 'dsb-btn dsb-btn-sm',
1056
+ disabled: !!busy,
1057
+ onClick: () => doAdopt([w.path]),
1058
+ },
1059
+ '建特征文件夹',
1060
+ ),
1061
+ ),
1062
+ ),
1063
+ )
1064
+ : null,
918
1065
  ),
919
1066
 
920
1067
  // 插件清单同步(1.0.0 从自动更新器迁入)