@dsh-workbench/core 1.0.0 → 1.0.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/README.md CHANGED
@@ -34,6 +34,15 @@ npx @dsh-workbench/core --profile wb2
34
34
 
35
35
  全局装过就不用 `npx`:`npm i -g @dsh-workbench/core` 之后直接 `dsh-workbench`。
36
36
 
37
+ 从 1.0.0 之前的独立实例升上来:旧数据**不会**自动并。先退出所有 dsh,再跑:
38
+
39
+ ```bash
40
+ dsh-workbench migrate # 切共用根 + 并旧会话/项目/附件
41
+ dsh-workbench migrate --force # 已经迁过一次、想再补并一次
42
+ ```
43
+
44
+ 只会补缺、不覆盖;旧目录保留。npx 用户把 `dsh-workbench` 换成 `npx @dsh-workbench/core`。
45
+
37
46
  先决条件(就三条):
38
47
 
39
48
  | 要有 | 为什么 |
@@ -48,6 +57,7 @@ npx @dsh-workbench/core --profile wb2
48
57
  |---|---|
49
58
  | `dsh-workbench create` | 交互式新建一个工作台实例 |
50
59
  | `dsh-workbench setup` | 幂等建默认的 `workbench` profile;配 `--from` 建别的名字 |
60
+ | `dsh-workbench migrate` | 把旧实例的会话 / 项目 / 附件并到共用数据根(`--force` 可重跑) |
51
61
  | `dsh-workbench` | 起工作台(默认端口 `:3081`,其余参数透传给 `dsh web`) |
52
62
  | `dsh-workbench --profile <名字>` | 起指定实例 |
53
63
  | `dsh-workbench setup --from <来源> --profile <新名>` | 以某个现有 profile 为模板建一个 |
@@ -78,7 +88,7 @@ npx @dsh-workbench/core --profile wb2
78
88
  | `profile/` | 出厂 profile 模板:可写根那一行(启动器 `--patch` 用,seed 也抄进 profile) |
79
89
  | `skills/` | 模块 DIY 指引,装好后挂给 agent;漏了不报错,只是 agent 不知道要怎么做模块 |
80
90
  | `bin/` | `dsh-workbench` 启动器、隔离 opencli 的入口 |
81
- | `scripts/` | 随包出货的脚本:`seed-profile.sh` / `seed-workshop.sh` / `create-workbench.mjs` / `clone-profile.mjs` |
91
+ | `scripts/` | 随包出货的脚本:`seed-profile.sh` / `seed-workshop.sh` / `create-workbench.mjs` / `clone-profile.mjs` / `share-data-root.mjs` |
82
92
 
83
93
  ## 数据落在哪
84
94
 
@@ -92,8 +102,8 @@ npx @dsh-workbench/core --profile wb2
92
102
  | DIY 工坊依赖安装日志 | `~/.dsh/workbench/seed.log` |
93
103
 
94
104
  独立实例**只分 profile,不分数据根**:会话、项目、附件跟 `dsh web` 和 Desktop 是同一份,
95
- 只有设置与插件各一份。旧版本分过家的实例,下次启动会把 `~/.dsh/workbench/{sessions,storages,attachments}`
96
- 并回共用根(只补不覆盖,旧目录保留)。
105
+ 只有设置与插件各一份。旧版本分过家的实例,启动时**不会**自动并;先退出所有 dsh,再跑
106
+ `dsh-workbench migrate`(只补不覆盖,旧目录保留)。
97
107
 
98
108
  设置只有一份,写在哪都看得见:
99
109
 
package/bin/dsh-workbench CHANGED
@@ -8,6 +8,7 @@
8
8
  # # 以 web 为模板建一个独立工作台
9
9
  # ./bin/dsh-workbench --profile wb2 # ② 起指定 profile,默认 :3081
10
10
  # ./bin/dsh-workbench --no-open # 其余参数透传给 dsh web
11
+ # ./bin/dsh-workbench migrate # 把旧实例的会话/项目/附件并到共用数据根(--force 可重跑)
11
12
  # ./bin/dsh-workbench --help
12
13
  #
13
14
  # 与 `dsh web` 并存:不同的 profile、不同的 node_modules、不同的端口、独立的进程。
@@ -35,6 +36,8 @@ PROFILE_EXPLICIT=0
35
36
  [ -n "${WORKBENCH_PROFILE:-}" ] && PROFILE_EXPLICIT=1
36
37
  SETUP=0
37
38
  CREATE=0
39
+ MIGRATE=0
40
+ FORCE=0
38
41
  DSH_ARGS=()
39
42
 
40
43
  die() {
@@ -43,7 +46,7 @@ die() {
43
46
  }
44
47
 
45
48
  usage() {
46
- sed -n '3,15p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
49
+ sed -n '3,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
47
50
  }
48
51
 
49
52
  if [ "${1:-}" = "create" ]; then
@@ -54,6 +57,10 @@ if [ "${1:-}" = "setup" ]; then
54
57
  SETUP=1
55
58
  shift
56
59
  fi
60
+ if [ "${1:-}" = "migrate" ]; then
61
+ MIGRATE=1
62
+ shift
63
+ fi
57
64
 
58
65
  while [ $# -gt 0 ]; do
59
66
  case "$1" in
@@ -75,6 +82,11 @@ while [ $# -gt 0 ]; do
75
82
  echo "dsh-workbench: 会话与项目跟 dsh web 共用一份数据根,$1 不再需要(忽略)。" >&2
76
83
  shift
77
84
  ;;
85
+ --force)
86
+ [ "$MIGRATE" = "1" ] || die "--force 只用于 migrate"
87
+ FORCE=1
88
+ shift
89
+ ;;
78
90
  --no-settings) WB_COPY_SETTINGS=0; shift ;;
79
91
  help | --help | -h) usage; exit 0 ;;
80
92
  --) shift; DSH_ARGS+=("$@"); break ;;
@@ -85,6 +97,9 @@ done
85
97
  if [ "$SETUP" = "1" ] && [ ${#DSH_ARGS[@]} -gt 0 ]; then
86
98
  die "setup 不认识这些参数:${DSH_ARGS[*]}"
87
99
  fi
100
+ if [ "$MIGRATE" = "1" ] && [ ${#DSH_ARGS[@]} -gt 0 ]; then
101
+ die "migrate 不认识这些参数:${DSH_ARGS[*]}"
102
+ fi
88
103
  if [ "${WB_CLONE:-0}" = "1" ] && [ "$PROFILE_EXPLICIT" = "0" ]; then
89
104
  die "用 --from 时还要指定 --profile <新名字>(例如 --from web --profile wb2)"
90
105
  fi
@@ -272,6 +287,22 @@ if [ ! -f "$PROFILE_DIR/package.json" ]; then
272
287
  exit 1
273
288
  fi
274
289
 
290
+ if [ "$MIGRATE" = "1" ]; then
291
+ MIGRATE_ARGS=(--migrate)
292
+ [ "$FORCE" = "1" ] && MIGRATE_ARGS+=(--force)
293
+ WORKBENCH_PROFILE="$PROFILE" DSH_HOME="$DSH_HOME" DSH_WORKBENCH_HOME="$WORKBENCH_HOME" \
294
+ node "$REPO_DIR/scripts/share-data-root.mjs" "${MIGRATE_ARGS[@]}" || exit 1
295
+ if [ "$PROFILE" = "workbench" ]; then
296
+ START_CMD="$REPO_DIR/bin/dsh-workbench"
297
+ else
298
+ START_CMD="$REPO_DIR/bin/dsh-workbench --profile $PROFILE"
299
+ fi
300
+ echo
301
+ echo "迁移结束。现在起工作台:$START_CMD"
302
+ echo "如果 dsh web 刚才也开着,先重启它,让它从共用根重新读一遍。"
303
+ exit 0
304
+ fi
305
+
275
306
  # ── 硬门禁:工作台 bundle 必须**被选中**(在 `dsh.profile.bundles` 里)─────────
276
307
  #
277
308
  # 判据只能是 `dsh.profile.bundles`,**不能** grep 整个 package.json:依赖装着 ≠ 被选中。
@@ -307,15 +338,16 @@ if [ "$BUNDLE_GUARD" -ne 0 ]; then
307
338
  exit 1
308
339
  fi
309
340
 
310
- # ── 已有实例:把会话 / 项目 / 附件收回到共用数据根(幂等,每次启动都过一遍)
341
+ # ── 已有实例:只整理配置,旧数据不自动迁移
311
342
  #
312
343
  # 旧版本给 workbench profile 追加过隔离的三行(sessions / storages / attachment 指到
313
- # `$DSH_HOME/workbench/**`)。现在改成跟 Desktop 一样「只分 profile、不分数据根」,
314
- # 那三行与旧数据都在这条命令里收回来;新 profile 上它只补可写根那一行。
315
- # 必须排在这里(exec dsh 之前):那几行要在这一代配置读进去之前就改好。
344
+ # `$DSH_HOME/workbench/**`)。现在缺省跟 Desktop 一样「只分 profile、不分数据根」;
345
+ # 但如果旧目录里还有数据、且没有 `.data-root-shared`,这里只打印 migrate 提示,
346
+ # 不改 profile、不动数据 —— 由用户自己决定什么时候并。
347
+ # 必须排在这里(exec dsh 之前):配置要在这一代 dsh 读进去之前就定好。
316
348
  WORKBENCH_PROFILE="$PROFILE" DSH_HOME="$DSH_HOME" DSH_WORKBENCH_HOME="$WORKBENCH_HOME" \
317
349
  node "$REPO_DIR/scripts/share-data-root.mjs" ||
318
- echo "dsh-workbench: 提醒 —— 数据根没整理成(见上一行),这次仍按现在的 profile 起。" >&2
350
+ echo "dsh-workbench: 提醒 —— 数据根配置没整理成(见上一行),这次仍按现在的 profile 起。" >&2
319
351
 
320
352
  # 清掉从父 dsh 会话继承来的身份变量。
321
353
  #
package/client.js CHANGED
@@ -3086,6 +3086,7 @@ window.__ModuleLoader__.load({
3086
3086
  "panels.market.selfUpdateFailed": "Self-update failed: {reason}",
3087
3087
  "panels.market.selfUpdateUnknown": "the outside process left no reason (see ~/.dsh/workbench/self-update.log)",
3088
3088
  "panels.market.selfUpdateRolledBack": "Not swapped; rolled back to {version}.",
3089
+ "panels.market.selfUpdateNeedsMigrate": "Workbench updated. Old sessions and projects are not merged automatically: quit every dsh, then run dsh-workbench migrate (--force to merge again).",
3089
3090
  "panels.market.uninstall": "Uninstall",
3090
3091
  "panels.market.uninstallLabel": "Uninstall {name}",
3091
3092
  "panels.market.installing": "Installing…",
@@ -3718,6 +3719,7 @@ window.__ModuleLoader__.load({
3718
3719
  "panels.market.selfUpdateFailed": "自更新没成:{reason}",
3719
3720
  "panels.market.selfUpdateUnknown": "外部进程没留下原因(看 ~/.dsh/workbench/self-update.log)",
3720
3721
  "panels.market.selfUpdateRolledBack": "没换成,已回滚到 {version}。",
3722
+ "panels.market.selfUpdateNeedsMigrate": "工作台已更新。旧会话 / 项目不会自动并:先退出所有 dsh,再运行 dsh-workbench migrate(--force 可补并)。",
3721
3723
  "panels.market.uninstall": "卸载",
3722
3724
  "panels.market.uninstallLabel": "卸载 {name}",
3723
3725
  "panels.market.installing": "正在安装…",
@@ -6498,6 +6500,9 @@ window.__ModuleLoader__.load({
6498
6500
  "remove-bundle",
6499
6501
  "uninstall-plugin"
6500
6502
  ];
6503
+ function marketMigrationNotice(t, update) {
6504
+ return update?.migration === "needed" ? t("panels.market.selfUpdateNeedsMigrate") : null;
6505
+ }
6501
6506
  function ConfirmUninstall({ t, name, onConfirm, onCancel }) {
6502
6507
  const [acknowledged, setAcknowledged] = (0, react.useState)(false);
6503
6508
  return /* @__PURE__ */ (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.RiskConfirmation, {
@@ -6674,18 +6679,21 @@ window.__ModuleLoader__.load({
6674
6679
  send: (body) => provision.send(body),
6675
6680
  onChanged
6676
6681
  });
6682
+ const landed = pendingLanded(pending, updateMap, effective, workbench?.installed, shared, pluginSource === null ? null : pluginList, {
6683
+ units: installed === void 0 ? null : new Set(installed.map((module) => module.unit)),
6684
+ plugins: pluginSource === null ? null : new Set(pluginList.map((row) => row.id))
6685
+ });
6686
+ const effectiveSelfUpdate = mock?.selfUpdate ?? selfUpdate;
6677
6687
  useSettleHold({
6678
6688
  pending,
6679
- landed: pendingLanded(pending, updateMap, effective, workbench?.installed, shared, pluginSource === null ? null : pluginList, {
6680
- units: installed === void 0 ? null : new Set(installed.map((module) => module.unit)),
6681
- plugins: pluginSource === null ? null : new Set(pluginList.map((row) => row.id))
6682
- }),
6689
+ landed,
6683
6690
  t,
6684
6691
  onChanged,
6685
- selfUpdate: mock?.selfUpdate ?? selfUpdate,
6692
+ selfUpdate: effectiveSelfUpdate,
6686
6693
  setPending,
6687
6694
  setFailed
6688
6695
  });
6696
+ const migrationNotice = marketMigrationNotice(t, effectiveSelfUpdate);
6689
6697
  const settled = marketSettled(freshness, installedRead);
6690
6698
  const nothing = shown.length === 0 && self === void 0 && installedList.length === 0 && pluginList.length === 0;
6691
6699
  if (!settled || (settling?.size ?? 0) > 0 && shown.length === 0 && installedList.length === 0) return /* @__PURE__ */ (0, react.createElement)(MarketChecking, { t });
@@ -6699,7 +6707,7 @@ window.__ModuleLoader__.load({
6699
6707
  shared,
6700
6708
  pending,
6701
6709
  failed,
6702
- notice,
6710
+ notice: notice ?? migrationNotice,
6703
6711
  act,
6704
6712
  onConfirming: setConfirming,
6705
6713
  setDefault,
@@ -3079,6 +3079,7 @@ var en = {
3079
3079
  "panels.market.selfUpdateFailed": "Self-update failed: {reason}",
3080
3080
  "panels.market.selfUpdateUnknown": "the outside process left no reason (see ~/.dsh/workbench/self-update.log)",
3081
3081
  "panels.market.selfUpdateRolledBack": "Not swapped; rolled back to {version}.",
3082
+ "panels.market.selfUpdateNeedsMigrate": "Workbench updated. Old sessions and projects are not merged automatically: quit every dsh, then run dsh-workbench migrate (--force to merge again).",
3082
3083
  "panels.market.uninstall": "Uninstall",
3083
3084
  "panels.market.uninstallLabel": "Uninstall {name}",
3084
3085
  "panels.market.installing": "Installing…",
@@ -3711,6 +3712,7 @@ var dictionaries = {
3711
3712
  "panels.market.selfUpdateFailed": "自更新没成:{reason}",
3712
3713
  "panels.market.selfUpdateUnknown": "外部进程没留下原因(看 ~/.dsh/workbench/self-update.log)",
3713
3714
  "panels.market.selfUpdateRolledBack": "没换成,已回滚到 {version}。",
3715
+ "panels.market.selfUpdateNeedsMigrate": "工作台已更新。旧会话 / 项目不会自动并:先退出所有 dsh,再运行 dsh-workbench migrate(--force 可补并)。",
3714
3716
  "panels.market.uninstall": "卸载",
3715
3717
  "panels.market.uninstallLabel": "卸载 {name}",
3716
3718
  "panels.market.installing": "正在安装…",
@@ -6491,6 +6493,9 @@ var CONFIRM_UNINSTALL = [
6491
6493
  "remove-bundle",
6492
6494
  "uninstall-plugin"
6493
6495
  ];
6496
+ function marketMigrationNotice(t, update) {
6497
+ return update?.migration === "needed" ? t("panels.market.selfUpdateNeedsMigrate") : null;
6498
+ }
6494
6499
  function ConfirmUninstall({ t, name, onConfirm, onCancel }) {
6495
6500
  const [acknowledged, setAcknowledged] = (0, react.useState)(false);
6496
6501
  return /* @__PURE__ */ (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.RiskConfirmation, {
@@ -6667,18 +6672,21 @@ function MarketView({ t, manifests, installable, capabilities, freshness, onChan
6667
6672
  send: (body) => provision.send(body),
6668
6673
  onChanged
6669
6674
  });
6675
+ const landed = pendingLanded(pending, updateMap, effective, workbench?.installed, shared, pluginSource === null ? null : pluginList, {
6676
+ units: installed === void 0 ? null : new Set(installed.map((module) => module.unit)),
6677
+ plugins: pluginSource === null ? null : new Set(pluginList.map((row) => row.id))
6678
+ });
6679
+ const effectiveSelfUpdate = mock?.selfUpdate ?? selfUpdate;
6670
6680
  useSettleHold({
6671
6681
  pending,
6672
- landed: pendingLanded(pending, updateMap, effective, workbench?.installed, shared, pluginSource === null ? null : pluginList, {
6673
- units: installed === void 0 ? null : new Set(installed.map((module) => module.unit)),
6674
- plugins: pluginSource === null ? null : new Set(pluginList.map((row) => row.id))
6675
- }),
6682
+ landed,
6676
6683
  t,
6677
6684
  onChanged,
6678
- selfUpdate: mock?.selfUpdate ?? selfUpdate,
6685
+ selfUpdate: effectiveSelfUpdate,
6679
6686
  setPending,
6680
6687
  setFailed
6681
6688
  });
6689
+ const migrationNotice = marketMigrationNotice(t, effectiveSelfUpdate);
6682
6690
  const settled = marketSettled(freshness, installedRead);
6683
6691
  const nothing = shown.length === 0 && self === void 0 && installedList.length === 0 && pluginList.length === 0;
6684
6692
  if (!settled || (settling?.size ?? 0) > 0 && shown.length === 0 && installedList.length === 0) return /* @__PURE__ */ (0, react.createElement)(MarketChecking, { t });
@@ -6692,7 +6700,7 @@ function MarketView({ t, manifests, installable, capabilities, freshness, onChan
6692
6700
  shared,
6693
6701
  pending,
6694
6702
  failed,
6695
- notice,
6703
+ notice: notice ?? migrationNotice,
6696
6704
  act,
6697
6705
  onConfirming: setConfirming,
6698
6706
  setDefault,
@@ -3082,6 +3082,7 @@ var en = {
3082
3082
  "panels.market.selfUpdateFailed": "Self-update failed: {reason}",
3083
3083
  "panels.market.selfUpdateUnknown": "the outside process left no reason (see ~/.dsh/workbench/self-update.log)",
3084
3084
  "panels.market.selfUpdateRolledBack": "Not swapped; rolled back to {version}.",
3085
+ "panels.market.selfUpdateNeedsMigrate": "Workbench updated. Old sessions and projects are not merged automatically: quit every dsh, then run dsh-workbench migrate (--force to merge again).",
3085
3086
  "panels.market.uninstall": "Uninstall",
3086
3087
  "panels.market.uninstallLabel": "Uninstall {name}",
3087
3088
  "panels.market.installing": "Installing…",
@@ -3714,6 +3715,7 @@ var dictionaries = {
3714
3715
  "panels.market.selfUpdateFailed": "自更新没成:{reason}",
3715
3716
  "panels.market.selfUpdateUnknown": "外部进程没留下原因(看 ~/.dsh/workbench/self-update.log)",
3716
3717
  "panels.market.selfUpdateRolledBack": "没换成,已回滚到 {version}。",
3718
+ "panels.market.selfUpdateNeedsMigrate": "工作台已更新。旧会话 / 项目不会自动并:先退出所有 dsh,再运行 dsh-workbench migrate(--force 可补并)。",
3717
3719
  "panels.market.uninstall": "卸载",
3718
3720
  "panels.market.uninstallLabel": "卸载 {name}",
3719
3721
  "panels.market.installing": "正在安装…",
@@ -6494,6 +6496,9 @@ var CONFIRM_UNINSTALL = [
6494
6496
  "remove-bundle",
6495
6497
  "uninstall-plugin"
6496
6498
  ];
6499
+ function marketMigrationNotice(t, update) {
6500
+ return update?.migration === "needed" ? t("panels.market.selfUpdateNeedsMigrate") : null;
6501
+ }
6497
6502
  function ConfirmUninstall({ t, name, onConfirm, onCancel }) {
6498
6503
  const [acknowledged, setAcknowledged] = useState(false);
6499
6504
  return /* @__PURE__ */ createElement(RiskConfirmation, {
@@ -6670,18 +6675,21 @@ function MarketView({ t, manifests, installable, capabilities, freshness, onChan
6670
6675
  send: (body) => provision.send(body),
6671
6676
  onChanged
6672
6677
  });
6678
+ const landed = pendingLanded(pending, updateMap, effective, workbench?.installed, shared, pluginSource === null ? null : pluginList, {
6679
+ units: installed === void 0 ? null : new Set(installed.map((module) => module.unit)),
6680
+ plugins: pluginSource === null ? null : new Set(pluginList.map((row) => row.id))
6681
+ });
6682
+ const effectiveSelfUpdate = mock?.selfUpdate ?? selfUpdate;
6673
6683
  useSettleHold({
6674
6684
  pending,
6675
- landed: pendingLanded(pending, updateMap, effective, workbench?.installed, shared, pluginSource === null ? null : pluginList, {
6676
- units: installed === void 0 ? null : new Set(installed.map((module) => module.unit)),
6677
- plugins: pluginSource === null ? null : new Set(pluginList.map((row) => row.id))
6678
- }),
6685
+ landed,
6679
6686
  t,
6680
6687
  onChanged,
6681
- selfUpdate: mock?.selfUpdate ?? selfUpdate,
6688
+ selfUpdate: effectiveSelfUpdate,
6682
6689
  setPending,
6683
6690
  setFailed
6684
6691
  });
6692
+ const migrationNotice = marketMigrationNotice(t, effectiveSelfUpdate);
6685
6693
  const settled = marketSettled(freshness, installedRead);
6686
6694
  const nothing = shown.length === 0 && self === void 0 && installedList.length === 0 && pluginList.length === 0;
6687
6695
  if (!settled || (settling?.size ?? 0) > 0 && shown.length === 0 && installedList.length === 0) return /* @__PURE__ */ createElement(MarketChecking, { t });
@@ -6695,7 +6703,7 @@ function MarketView({ t, manifests, installable, capabilities, freshness, onChan
6695
6703
  shared,
6696
6704
  pending,
6697
6705
  failed,
6698
- notice,
6706
+ notice: notice ?? migrationNotice,
6699
6707
  act,
6700
6708
  onConfirming: setConfirming,
6701
6709
  setDefault,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "个人工作台:Agent 工作台(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -100,8 +100,8 @@
100
100
  "proper-lockfile": "^4.1.2",
101
101
  "which": "^7.0.0",
102
102
  "write-file-atomic": "^8.0.0",
103
- "@dsh-workbench/shared": "1.0.0",
104
- "@dsh-workbench/adapters-default": "1.0.0"
103
+ "@dsh-workbench/shared": "1.0.1",
104
+ "@dsh-workbench/adapters-default": "1.0.1"
105
105
  },
106
106
  "license": "MIT",
107
107
  "repository": {
@@ -2,14 +2,14 @@
2
2
  #
3
3
  # 一次性引导:从 shipped 的 web 模板创建 workbench profile,
4
4
  # 继承 web 的 **bundle 清单**(这台机器上装了哪些 bundle),
5
- # 补上可写根那一行,并把会话 / 项目 / 附件指回跟 web 共用的数据根。
5
+ # 补上可写根那一行;新 profile 直接跟 web 共用数据根,旧 profile 保持原状,等用户跑 migrate。
6
6
  #
7
7
  # **设置不在这里继承**(这里曾经把 web 的 cordis.patch.yml 快照一份过来):
8
8
  # 工作台自己的 patch 是唯一真相,想同步 web 的哪几行由用户走「设置 → 配置同步」。
9
9
  # 为什么这么分(安装 vs 设置)、为什么不再是自动的:内部设计记录
10
10
  #
11
- # 幂等 / 可续跑:profile 已存在就跳过创建,数据根已经是共用的就什么都不写
12
- # —— 重复运行不会覆盖任何东西。
11
+ # 幂等 / 可续跑:profile 已存在就跳过创建;数据根已经是共用的、或旧数据还没迁
12
+ # (只提示 migrate)就什么都不写 —— 重复运行不会覆盖任何东西。
13
13
  # 安全:全程只**读** web 的 profile 目录,只**写** workbench 自己的目录。
14
14
  #
15
15
  # 用法:
@@ -176,10 +176,11 @@ console.log("→ profile 里那条 link: 指的是我这份装出来的副本(
176
176
  '
177
177
  fi
178
178
 
179
- # ── 第 3 步:数据根跟 web 共用 + 可写根那一行 ───────────────────────────
179
+ # ── 第 3 步:数据根配置 + 可写根那一行 ─────────────────────────────────
180
180
  #
181
- # 会话 / 项目 / 附件不再按 profile 分家:跟 `dsh web` 和 Desktop 一样只分 profile,
182
- # 数据都在 `$DSH_HOME` 下(旧实例的那三行隔离补丁与旧数据由 share-data-root.mjs 收回来)。
181
+ # 新 profile 跟 `dsh web` / Desktop 一样只分 profile,数据落在 `$DSH_HOME`。
182
+ # 旧 profile 如果还有没迁的数据,这里只打印 migrate 提示,保持隔离、不动数据;
183
+ # 用户自己跑 `dsh-workbench migrate` 才切共用根并并数据。
183
184
  # 每一步都幂等:已经是共用数据根的 profile 一个字节都不写。
184
185
  WORKBENCH_PROFILE="$PROFILE" DSH_HOME="$DSH_HOME" DSH_WORKBENCH_HOME="$WORKBENCH_HOME" \
185
186
  node "$REPO_DIR/scripts/share-data-root.mjs" || die "整理数据根失败(见上一行)"
@@ -202,7 +203,8 @@ fi
202
203
 
203
204
  cat <<EOF
204
205
 
205
- profile '$DST_PROFILE' 的骨架好了(bundle 清单 + 可写根;会话 / 项目 / 附件跟 web 共用)。
206
+ profile '$DST_PROFILE' 的骨架好了(bundle 清单 + 可写根)。
207
+ 旧实例如果还有数据没迁,启动时会提示;要切共用根并并旧数据,跑 dsh-workbench migrate。
206
208
 
207
209
  设置按创建时的勾选从 $SRC_PROFILE_NAME 继承(只同步改已有插件的行;挂新插件的行不同步);
208
210
  直接跑 setup 时只继承 bundle 清单,想要 web 那边的模型供应商 / 搜索源 / MCP,起工作台后去
@@ -10,6 +10,7 @@ import {
10
10
  statSync,
11
11
  writeFileSync,
12
12
  } from 'node:fs';
13
+ import { randomUUID } from 'node:crypto';
13
14
  import { homedir } from 'node:os';
14
15
  import { dirname, join, resolve } from 'node:path';
15
16
  import { fileURLToPath } from 'node:url';
@@ -78,6 +79,58 @@ function unionList(left, right) {
78
79
  return [...new Set([...(Array.isArray(left) ? left : []), ...(Array.isArray(right) ? right : [])])];
79
80
  }
80
81
 
82
+ function sessionIdsOf(entry) {
83
+ return Array.isArray(entry?.sessionIds) ? entry.sessionIds : [];
84
+ }
85
+
86
+ function orderedIds(global, table) {
87
+ const seen = new Set();
88
+ const ids = [];
89
+ for (const id of Array.isArray(global?.workspaceIds) ? global.workspaceIds : []) {
90
+ if (table[id] === undefined || seen.has(id)) continue;
91
+ ids.push(id);
92
+ seen.add(id);
93
+ }
94
+ for (const id of Object.keys(table)) {
95
+ if (seen.has(id)) continue;
96
+ ids.push(id);
97
+ seen.add(id);
98
+ }
99
+ return ids;
100
+ }
101
+
102
+ function repairWorkspaceOrder(targetFile) {
103
+ let target;
104
+ try {
105
+ target = JSON.parse(readFileSync(targetFile, 'utf8'));
106
+ } catch {
107
+ return false;
108
+ }
109
+ if (target?.unit?.name !== 'workspace') return false;
110
+ const table = target.tables?.workspaces ?? {};
111
+ const global = target.global ?? {};
112
+ if (global.pendingMutation !== undefined) return false;
113
+ const before = Array.isArray(global.workspaceIds) ? global.workspaceIds : [];
114
+ const seen = new Set();
115
+ const order = [];
116
+ for (const id of before) {
117
+ if (table[id] === undefined || seen.has(id)) continue;
118
+ order.push(id);
119
+ seen.add(id);
120
+ }
121
+ if (global.initialized === true) {
122
+ for (const id of Object.keys(table)) {
123
+ if (seen.has(id)) continue;
124
+ order.push(id);
125
+ seen.add(id);
126
+ }
127
+ }
128
+ if (order.length === before.length && order.every((id, index) => id === before[index])) return false;
129
+ target.global = { ...global, workspaceIds: order };
130
+ writeAtomic(targetFile, `${JSON.stringify(target, null, 2)}\n`);
131
+ return true;
132
+ }
133
+
81
134
  function mergeWorkspaceList(sourceFile, targetFile) {
82
135
  let source;
83
136
  try {
@@ -95,67 +148,249 @@ function mergeWorkspaceList(sourceFile, targetFile) {
95
148
  try {
96
149
  target = JSON.parse(readFileSync(targetFile, 'utf8'));
97
150
  } catch {
98
- warn(`${targetFile} 读不出来,这次没并项目列表。`);
99
- return 0;
151
+ warn(`${targetFile} 读不出来,这次没并项目列表,下次启动再试。`);
152
+ return null;
100
153
  }
101
154
  if (target?.unit?.name !== 'workspace') return 0;
102
155
  const sourceTable = source.tables?.workspaces ?? {};
103
156
  const targetTable = target.tables?.workspaces ?? {};
104
- const byPath = new Map();
105
- for (const entry of Object.values(targetTable)) {
106
- if (typeof entry?.path === 'string') byPath.set(entry.path, entry);
157
+ const sourceGlobal = source.global ?? {};
158
+ const targetGlobal = target.global ?? {};
159
+ if (targetGlobal.pendingMutation !== undefined) {
160
+ warn(`${targetFile} 里有一次没做完的项目操作,这次先不并项目列表,下次启动再试。`);
161
+ return null;
162
+ }
163
+ const idByPath = new Map();
164
+ const accounted = new Map();
165
+ for (const [id, entry] of Object.entries(targetTable)) {
166
+ if (typeof entry?.path === 'string') idByPath.set(entry.path, id);
167
+ for (const sessionId of sessionIdsOf(entry)) {
168
+ if (!accounted.has(sessionId)) accounted.set(sessionId, id);
169
+ }
107
170
  }
171
+ const order = orderedIds(targetGlobal, targetTable);
172
+ const idMap = new Map();
108
173
  let added = 0;
109
- for (const [id, entry] of Object.entries(sourceTable)) {
174
+ for (const sourceId of orderedIds(sourceGlobal, sourceTable)) {
175
+ const entry = sourceTable[sourceId];
110
176
  if (typeof entry?.path !== 'string') continue;
111
- const hit = byPath.get(entry.path);
112
- if (hit === undefined) {
113
- targetTable[id] = entry;
114
- byPath.set(entry.path, entry);
115
- added += 1;
177
+ const hitId = idByPath.get(entry.path);
178
+ if (hitId !== undefined) {
179
+ const hit = targetTable[hitId];
180
+ const incoming = sessionIdsOf(entry).filter((sessionId) => {
181
+ const holder = accounted.get(sessionId);
182
+ return holder === undefined || holder === hitId;
183
+ });
184
+ hit.sessionIds = unionList(sessionIdsOf(hit), incoming);
185
+ for (const sessionId of hit.sessionIds) {
186
+ if (!accounted.has(sessionId)) accounted.set(sessionId, hitId);
187
+ }
188
+ if (typeof entry.updatedAt === 'string' && (typeof hit.updatedAt !== 'string' || entry.updatedAt > hit.updatedAt)) {
189
+ hit.updatedAt = entry.updatedAt;
190
+ }
191
+ idMap.set(sourceId, hitId);
116
192
  continue;
117
193
  }
118
- hit.sessionIds = unionList(hit.sessionIds, entry.sessionIds);
119
- if (typeof entry.updatedAt === 'string' && (typeof hit.updatedAt !== 'string' || entry.updatedAt > hit.updatedAt)) {
120
- hit.updatedAt = entry.updatedAt;
194
+ let newId = sourceId;
195
+ if (targetTable[newId] !== undefined) newId = randomUUID();
196
+ const sessionIds = [];
197
+ for (const sessionId of sessionIdsOf(entry)) {
198
+ if (accounted.has(sessionId)) continue;
199
+ accounted.set(sessionId, newId);
200
+ sessionIds.push(sessionId);
121
201
  }
202
+ targetTable[newId] = { ...entry, sessionIds };
203
+ idByPath.set(entry.path, newId);
204
+ order.push(newId);
205
+ added += 1;
122
206
  }
207
+ const preferred = targetGlobal.defaultWorkspaceId;
208
+ const fromSource = idMap.get(sourceGlobal.defaultWorkspaceId) ?? sourceGlobal.defaultWorkspaceId;
209
+ const defaultWorkspaceId =
210
+ typeof preferred === 'string' && targetTable[preferred] !== undefined
211
+ ? preferred
212
+ : typeof fromSource === 'string' && targetTable[fromSource] !== undefined
213
+ ? fromSource
214
+ : undefined;
123
215
  target.tables = { ...(target.tables ?? {}), workspaces: targetTable };
124
- const global = target.global ?? {};
125
- const sourceGlobal = source.global ?? {};
126
- const known = new Set(Array.isArray(global.workspaceIds) ? global.workspaceIds : []);
127
- for (const id of Array.isArray(sourceGlobal.workspaceIds) ? sourceGlobal.workspaceIds : []) known.add(id);
128
216
  target.global = {
129
- ...global,
130
- workspaceIds: [...known],
131
- archivedSessionIds: unionList(global.archivedSessionIds, sourceGlobal.archivedSessionIds),
132
- pinnedSessionIds: unionList(global.pinnedSessionIds, sourceGlobal.pinnedSessionIds),
217
+ ...targetGlobal,
218
+ workspaceIds: order,
219
+ archivedSessionIds: unionList(targetGlobal.archivedSessionIds, sourceGlobal.archivedSessionIds),
220
+ pinnedSessionIds: unionList(targetGlobal.pinnedSessionIds, sourceGlobal.pinnedSessionIds),
133
221
  };
222
+ if (defaultWorkspaceId === undefined) delete target.global.defaultWorkspaceId;
223
+ else target.global.defaultWorkspaceId = defaultWorkspaceId;
134
224
  writeAtomic(targetFile, `${JSON.stringify(target, null, 2)}\n`);
135
225
  return added;
136
226
  }
137
227
 
228
+ function repairTarget(targetFile) {
229
+ if (repairWorkspaceOrder(targetFile)) say(`项目顺序里有对不上的条目,已按实际项目重排(${targetFile})。`);
230
+ }
231
+
232
+ function hasEntries(dir) {
233
+ try {
234
+ return readdirSync(dir).some((name) => name !== '.DS_Store');
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+
240
+ function legacyDataExists() {
241
+ if (resolve(RUNTIME_ROOT) === resolve(HOME)) return false;
242
+ return (
243
+ existsSync(join(RUNTIME_ROOT, 'storages', 'workspace.json')) ||
244
+ hasEntries(join(RUNTIME_ROOT, 'sessions')) ||
245
+ hasEntries(join(RUNTIME_ROOT, 'attachments'))
246
+ );
247
+ }
248
+
249
+ function missingWorkspacePaths() {
250
+ const sourceFile = join(RUNTIME_ROOT, 'storages', 'workspace.json');
251
+ const targetFile = join(HOME, 'storages', 'workspace.json');
252
+ if (!existsSync(sourceFile) || !existsSync(targetFile)) return [];
253
+ let source;
254
+ let target;
255
+ try {
256
+ source = JSON.parse(readFileSync(sourceFile, 'utf8'));
257
+ target = JSON.parse(readFileSync(targetFile, 'utf8'));
258
+ } catch {
259
+ return [];
260
+ }
261
+ if (source?.unit?.name !== 'workspace' || target?.unit?.name !== 'workspace') return [];
262
+ const have = new Set();
263
+ for (const entry of Object.values(target.tables?.workspaces ?? {})) {
264
+ if (typeof entry?.path === 'string') have.add(entry.path);
265
+ }
266
+ const missing = [];
267
+ for (const entry of Object.values(source.tables?.workspaces ?? {})) {
268
+ if (typeof entry?.path !== 'string' || have.has(entry.path)) continue;
269
+ missing.push(typeof entry.title === 'string' && entry.title !== '' ? entry.title : entry.path);
270
+ }
271
+ return missing;
272
+ }
273
+
138
274
  function mergeDataRoot() {
139
- if (resolve(RUNTIME_ROOT) === resolve(HOME)) return;
140
- if (existsSync(MARKER)) return;
275
+ if (resolve(RUNTIME_ROOT) === resolve(HOME)) return { added: 0, files: 0 };
276
+ const targetWorkspace = join(HOME, 'storages', 'workspace.json');
141
277
  const sourceStorages = join(RUNTIME_ROOT, 'storages');
142
- let count = 0;
278
+ let added;
279
+ let files = 0;
143
280
  try {
144
- count += mergeWorkspaceList(join(sourceStorages, 'workspace.json'), join(HOME, 'storages', 'workspace.json'));
145
- count += mergeTree(join(RUNTIME_ROOT, 'sessions'), join(HOME, 'sessions'));
146
- count += mergeTree(join(RUNTIME_ROOT, 'attachments'), join(HOME, 'attachments'));
147
- count += mergeTree(sourceStorages, join(HOME, 'storages'), new Set(['workspace.json']));
281
+ added = mergeWorkspaceList(join(sourceStorages, 'workspace.json'), targetWorkspace);
282
+ if (added === null) return null;
283
+ files += mergeTree(join(RUNTIME_ROOT, 'sessions'), join(HOME, 'sessions'));
284
+ files += mergeTree(join(RUNTIME_ROOT, 'attachments'), join(HOME, 'attachments'));
285
+ files += mergeTree(sourceStorages, join(HOME, 'storages'), new Set(['workspace.json']));
148
286
  } catch (error) {
149
- warn(`并回 ${HOME} 时出错,下次启动再试:${error instanceof Error ? error.message : String(error)}`);
150
- return;
287
+ warn(`并回 ${HOME} 时出错:${error instanceof Error ? error.message : String(error)}`);
288
+ return null;
151
289
  }
290
+ repairTarget(targetWorkspace);
291
+ return { added, files };
292
+ }
293
+
294
+ function writeMarker() {
152
295
  mkdirSync(dirname(MARKER), { recursive: true });
153
296
  writeFileSync(
154
297
  MARKER,
155
298
  `${JSON.stringify({ at: new Date().toISOString(), from: RUNTIME_ROOT, to: HOME }, null, 2)}\n`,
156
299
  'utf8',
157
300
  );
158
- if (count > 0) say(`已把 ${RUNTIME_ROOT} 里的会话 / 项目 / 附件并回 ${HOME}(${count} 个文件,旧目录保留)。`);
301
+ }
302
+
303
+ function inspectProfile() {
304
+ const patch = readText(PATCH_PATH);
305
+ if (patch === null) return null;
306
+ const legacy = parseBlocks(patch).filter((block) => DATA_ROOT_IDS.has(block.id));
307
+ return {
308
+ patch,
309
+ ours: legacy.filter((block) => OWN_ROOT.test(block.body)),
310
+ custom: legacy.filter((block) => !OWN_ROOT.test(block.body)),
311
+ };
312
+ }
313
+
314
+ function customRootMessage(state) {
315
+ return `${PROFILE} 的会话 / 存储根是你自己改过的(${state.custom.map((block) => block.id).join(' / ')})—— 这次不动它,也不并数据。`;
316
+ }
317
+
318
+ function applySharedConfig(state) {
319
+ if (state.custom.length > 0) {
320
+ warn(customRootMessage(state));
321
+ return false;
322
+ }
323
+ let patch = state.patch;
324
+ if (state.ours.length > 0) {
325
+ const first = Math.min(...state.ours.map((block) => block.lineStart));
326
+ patch = stripIsolationHeader(patch, first);
327
+ patch = removeBlocks(patch, (block) => DATA_ROOT_IDS.has(block.id) && OWN_ROOT.test(block.body));
328
+ writeAtomic(PATCH_PATH, patch);
329
+ say(`数据根改回共用:从 ${PROFILE} 的 profile 里去掉 ${state.ours.map((block) => block.id).join(' / ')}。`);
330
+ }
331
+ const hasSandbox = parseBlocks(patch).some((block) => block.id === 'sandbox-policy');
332
+ if (!hasSandbox) {
333
+ const sandbox = readText(SANDBOX_PATCH);
334
+ const row = sandbox === null ? undefined : parseBlocks(sandbox).find((block) => block.id === 'sandbox-policy');
335
+ if (row !== undefined) {
336
+ patch = appendBlocks(patch, row.body);
337
+ writeAtomic(PATCH_PATH, patch);
338
+ }
339
+ }
340
+ repairTarget(join(HOME, 'storages', 'workspace.json'));
341
+ return true;
342
+ }
343
+
344
+ function configureDefault() {
345
+ const state = inspectProfile();
346
+ if (state === null) return;
347
+ if (state.custom.length > 0) {
348
+ warn(customRootMessage(state));
349
+ return;
350
+ }
351
+ if (legacyDataExists() && !existsSync(MARKER)) {
352
+ warn(`检测到 ${RUNTIME_ROOT} 里还有旧会话 / 项目 / 附件,这次没有自动并。`);
353
+ warn('要切到共用数据根,先退出所有 dsh,再运行: dsh-workbench migrate');
354
+ return;
355
+ }
356
+ applySharedConfig(state);
357
+ }
358
+
359
+ function migrate() {
360
+ const state = inspectProfile();
361
+ if (state === null) {
362
+ warn(`找不到 ${PATCH_PATH},先运行 dsh-workbench setup。`);
363
+ return 1;
364
+ }
365
+ if (state.custom.length > 0) {
366
+ warn(customRootMessage(state));
367
+ return 1;
368
+ }
369
+ if (!legacyDataExists()) {
370
+ applySharedConfig(state);
371
+ say(`没有发现 ${RUNTIME_ROOT} 里的旧数据,数据根已经是共用的。`);
372
+ return 0;
373
+ }
374
+ if (existsSync(MARKER) && !FORCE) {
375
+ const missing = missingWorkspacePaths();
376
+ if (missing.length > 0) {
377
+ say(`已经迁过一次;旧目录里还有 ${missing.length} 个项目不在共用根:${missing.join('、')}。要补并加 --force(这次 profile 保持原样)。`);
378
+ return 0;
379
+ }
380
+ applySharedConfig(state);
381
+ say('已经迁过一次;要再并一次加 --force。');
382
+ return 0;
383
+ }
384
+ warn('迁移前先退出所有 dsh(:3080 / :3081);否则正在运行的实例可能覆盖刚并好的数据。');
385
+ const result = mergeDataRoot();
386
+ if (result === null) {
387
+ warn('这次没有迁移;profile 和旧数据都保持原样。');
388
+ return 1;
389
+ }
390
+ writeMarker();
391
+ applySharedConfig(state);
392
+ say(`迁移完成:新增 ${result.added} 个项目,补了 ${result.files} 个文件;旧目录保留。`);
393
+ return 0;
159
394
  }
160
395
 
161
396
  function stripIsolationHeader(text, blockStart) {
@@ -174,38 +409,11 @@ function stripIsolationHeader(text, blockStart) {
174
409
  return lines.join('\n');
175
410
  }
176
411
 
412
+ const MIGRATE = process.argv.includes('--migrate') || process.argv.includes('migrate');
413
+ const FORCE = process.argv.includes('--force');
414
+
177
415
  const profileManifest = join(PROFILE_DIR, 'package.json');
178
416
  if (!existsSync(profileManifest)) process.exit(0);
179
417
 
180
- let patch = readText(PATCH_PATH);
181
- if (patch === null) process.exit(0);
182
-
183
- const legacy = parseBlocks(patch).filter((block) => DATA_ROOT_IDS.has(block.id));
184
- const ours = legacy.filter((block) => OWN_ROOT.test(block.body));
185
- const custom = legacy.filter((block) => !OWN_ROOT.test(block.body));
186
- if (ours.length > 0) {
187
- const first = Math.min(...ours.map((block) => block.lineStart));
188
- patch = stripIsolationHeader(patch, first);
189
- patch = removeBlocks(patch, (block) => DATA_ROOT_IDS.has(block.id) && OWN_ROOT.test(block.body));
190
- writeAtomic(PATCH_PATH, patch);
191
- say(`数据根改回共用:从 ${PROFILE} 的 profile 里去掉 ${ours.map((block) => block.id).join(' / ')}。`);
192
- }
193
-
194
- const hasSandbox = parseBlocks(patch).some((block) => block.id === 'sandbox-policy');
195
- if (!hasSandbox) {
196
- const sandbox = readText(SANDBOX_PATCH);
197
- const row = sandbox === null ? undefined : parseBlocks(sandbox).find((block) => block.id === 'sandbox-policy');
198
- if (row !== undefined) {
199
- patch = appendBlocks(patch, row.body);
200
- writeAtomic(PATCH_PATH, patch);
201
- }
202
- }
203
-
204
- if (custom.length > 0) {
205
- warn(
206
- `${PROFILE} 的会话 / 存储根是你自己改过的(${custom.map((block) => block.id).join(' / ')})—— 这次不动它,也不并数据。`,
207
- );
208
- process.exit(0);
209
- }
210
-
211
- mergeDataRoot();
418
+ if (MIGRATE) process.exit(migrate());
419
+ configureDefault();