@dsh-workbench/core 0.2.5 → 0.2.6

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import type { RouteHandler } from '@dsh-workbench/shared/host';
2
+ import type { ConfigSyncRow, ConfigSyncSnapshot } from '../config-sync.js';
3
+ export interface PatchBlock {
4
+ readonly key: string;
5
+ readonly id: string;
6
+ readonly insert: boolean;
7
+ readonly body: string;
8
+ readonly names: readonly string[];
9
+ }
10
+ export declare function patchBlocks(text: string): PatchBlock[];
11
+ export declare function normalizeBlock(body: string): string;
12
+ export declare function syncRows(source: readonly PatchBlock[], own: readonly PatchBlock[], missing: (block: PatchBlock) => readonly string[]): ConfigSyncRow[];
13
+ export declare function mergeBlocks(ownText: string, incoming: readonly PatchBlock[], overwrite: ReadonlySet<string>): {
14
+ ok: true;
15
+ text: string;
16
+ written: string[];
17
+ skipped: string[];
18
+ } | {
19
+ ok: false;
20
+ reason: string;
21
+ };
22
+ export declare function readPatchBlocks(path: string): {
23
+ ok: true;
24
+ text: string;
25
+ blocks: PatchBlock[];
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ };
30
+ export declare function writePatchFile(path: string, text: string): void;
31
+ export interface ConfigSyncDeps {
32
+ readonly home: string;
33
+ readonly self: string;
34
+ readonly profileDir: string | null;
35
+ }
36
+ export declare function configSyncHandler(deps: ConfigSyncDeps): RouteHandler;
37
+ export declare function configSyncSnapshot(deps: ConfigSyncDeps, asked?: string): ConfigSyncSnapshot;
38
+ export declare function configSyncApply(deps: ConfigSyncDeps, payload: unknown): {
39
+ ok: true;
40
+ written: string[];
41
+ skipped: string[];
42
+ restart: true;
43
+ } | {
44
+ ok: false;
45
+ text: string;
46
+ };
package/docs/HANDOFF.md CHANGED
@@ -58,16 +58,19 @@ npx @dsh-workbench/core
58
58
  > `chmod +x ~/.npm/_npx/*/node_modules/@dsh-workbench/core/{scripts/seed-profile.sh,bin/opencli-isolated}`。
59
59
  > 0.1.7 起启动器已不吃 x 位,重装那个版本即可根治。
60
60
 
61
- ### 设置从哪来:**两边都要的在 `dsh web` 里配**(单向、实时)
61
+ ### 设置从哪来:**工作台自己那份说了算**,想要 web 的行就同步一次(ADR-0044)
62
62
 
63
- 工作台启动时会用 `--patch` 把 `profiles/web/cordis.patch.yml` 叠在自己的差异层之上
64
- (`bin/dsh-workbench` 里就在做这件事)。所以:
63
+ 工作台**不再**把 `profiles/web/cordis.patch.yml` 叠在自己上面 —— 叠层是最高层,
64
+ 它盖住的行在 `:3081` 的 Settings 里**改不动**(dsh 的 config-editor 会被那句
65
+ `overridden by a home patch or command-line overlay` 挡在写文件之前)。
66
+ 现在的工作方式:
65
67
 
66
- - **模型供应商 / API key / 搜索源 / MCP** 这类"机器级、两边都要"的设置 ⇒ **在 `:3080` 的 Settings 里配一次**,工作台自动同源;
67
- - **只有工作台要的**(面板显示哪些模块、会话隔离)⇒ 在 `:3081` 里配,落在它自己的 profile patch,web 看不到;
68
- - 被 web 声明过的行,在工作台的 Settings 页里会显示 `overridden by a home patch or command-line overlay` —— 那是"这行由 web 管"的**提示**,不是故障。
69
-
70
- (这是 `--patch` 单向叠加,不是双向同步:工作台里配的**不会**流回 web。)
68
+ - **要配什么就在 `:3081` 配**,落在 `profiles/workbench/cordis.patch.yml` —— 那是它唯一的那份设置文档;
69
+ - **想用 web 那边已经配好的行**(模型供应商 / API key 引用 / 搜索源 / MCP)⇒ `:3081` →
70
+ `Meta+,` → 「配置同步」→ 选来源 `web` → 勾行 → 同步 → **重启工作台**。
71
+ 它读的是那个 profile 的 `cordis.patch.yml`,按行搬,只搬**显式配过**的行;
72
+ 同 id 两边内容不同时**默认不覆盖**,要覆盖得自己勾;行点名的包没装会当场说出来。
73
+ - **web 后来改了不会自动过来** —— 再打开这一页,那几行会重新以「差异」标签出现在这一页,拉一次即可。
71
74
 
72
75
  ### 一条"看起来坏了、其实没坏"的情况
73
76
 
@@ -85,14 +88,16 @@ Settings → 插件里把 `@dsh-workbench/core` 关掉后,工作台会**退化
85
88
  ### `setup` 到底做了什么(幂等,可以重复跑)
86
89
 
87
90
  1. 拿 `~/.dsh/profiles/web` 当模板建 `workbench` profile;
88
- 2. 继承 web 的 bundle 清单 + 设置快照 + patch 点名的依赖;
91
+ 2. 继承 web 的 **bundle 清单**(装没装东西 —— 安装范畴,走 `dsh plugin` 通路);
89
92
  3. 追加**数据隔离层**(会话 / 存储 / 附件各走 `~/.dsh/workbench/**`);
90
93
  4. `dsh plugin --profile workbench add @dsh-workbench/core` —— 走 **dsh 自己的**
91
94
  安装通路(兼容性预检、profile 写锁、装完自动把 bundle 登记进 profile 清单)。
92
95
 
93
- 继承是**一次性快照**,不是订阅:web 之后改设置不会流过来(这正是"互不影响"的实现方式),
94
- 要重新同步得重跑 `setup`。有意共享的只有两样:**API 凭据**(`~/.dsh/.credentials.yaml`,
95
- 一机一把 key)和 `$DSH_HOME/cordis.patch.yml`(机器公共层,对所有 profile 实时生效)。
96
+ **设置不在这一步继承**(这里以前快照 web 的 patch、并继承被它点名的依赖):新机器上
97
+ `setup` 完的工作台**没有模型供应商**,第一次打开去 设置 → 「配置同步」拉一次即可 ——
98
+ 这样每一行都归工作台自己,改得动。有意共享的只有两样:**API 凭据**
99
+ (`~/.dsh/.credentials.yaml`,一机一把 key)和 `$DSH_HOME/cordis.patch.yml`(机器公共层,
100
+ 对所有 profile 实时生效)。
96
101
 
97
102
  ## 四、不想用 npx?仓库副本也可以
98
103
 
@@ -26,7 +26,8 @@
26
26
  `OPENCLI_PIN` 锁),但它连的 **daemon 是整机一个** —— 见下面「隔离边界」。
27
27
 
28
28
  前提:`dsh` 和 `pnpm` 在 PATH 上(`dsh plugin` 是转发给 pnpm 的),
29
- 且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)。
29
+ 且 `~/.dsh/profiles/web` 已存在(跑过一次 `dsh web` 即可)—— seed 拿它当**模板**
30
+ (bundle 清单),之后也是「配置同步」的默认来源。
30
31
 
31
32
  **给别人用**(两条路:插件页装进他自己的 web / 独立实例 `setup`;包在公网 npm 上)见 [`HANDOFF.md`](HANDOFF.md)。
32
33
 
@@ -58,9 +59,9 @@
58
59
  | 开工作台 | `./bin/dsh-workbench` |
59
60
  | 开发(监听改动 + 起工作台) | `pnpm run dev` —— profile 不对会先 `setup`;`:3081` 已在跑就只监听 |
60
61
  | 换端口 | `WORKBENCH_PORT=3091 ./bin/dsh-workbench` |
61
- | 改工作台设置 | 在 `:3081` 的 Settings 页改,落在 `profiles/workbench/cordis.patch.yml` |
62
- | 改 web 设置 | 在 `:3080` 改,落在 `profiles/web/cordis.patch.yml`。**两边都要的设置(模型供应商、搜索源、MCP)就在这一份里配** —— 工作台启动时会用 `--patch` 把它叠上来(web → 工作台,单向、实时),所以不必配两遍 |
63
- | 改只有工作台用的设置 | 在 `:3081` 改,落在 `profiles/workbench/cordis.patch.yml`。这个文件**只剩工作台自己的差异**(会话/存储/附件隔离 + 沙箱);被 web 覆盖过的行在这里改不动(编辑器会写 `overridden`,那就是"这行由 web 管"的提示) |
62
+ | 改工作台设置 | 在 `:3081` 的 Settings 页改,落在 `profiles/workbench/cordis.patch.yml`。**这是工作台唯一的那份设置文档**(没有别处叠在它上面),所以模型供应商 / 搜索源 / MCP 每一行都改得动 |
63
+ | 用 web 那边已经配好的行 | `:3081` → `Meta+,` → 「配置同步」→ 选来源(默认 `web`)→ 勾行 → 同步 → **重启工作台**。只搬你在那个 profile 里**显式配过**的行;行点名的包没装时页面会说出来(ADR-0044) |
64
+ | 改 web 设置 | 在 `:3080` 改,落在 `profiles/web/cordis.patch.yml`。工作台**不会**再自动跟着变 —— 要就跟上面那条同步一次;不想跟就不动 |
64
65
  | 重建 profile | `mv ~/.dsh/profiles/workbench ~/.dsh/profiles/workbench.bak` 后重跑 seed |
65
66
  | **别在设置里关掉工作台** | Settings → 插件里 `@dsh-workbench/core` 那个开关**不要关**。关掉=把它从 profile 的 `bundles` 里移除 → 我们那份 bundle patch(含"禁用出厂左栏")**整份不再应用** → 页面退化成**原版 dsh web**(左栏回来、右侧面板消失),**数据一个都不丢**。恢复:插件页把开关打开(左栏的「插件」入口也在;进不去设置就按 `Meta+,`)。实测过这条降级路径,不是推断 |
66
67
 
@@ -102,8 +103,9 @@ WORKBENCH_PORT=3091 pnpm run dev # 换端口(监听与实例同一个端口
102
103
 
103
104
  0. **代码接线** —— 槽位名 / key / id / locale 是否对得上(语法检查抓不到这类错)
104
105
  1. **web 是否被动过** —— 与 `--record` 的哈希逐一比对
105
- 2. **组合树差异** —— `dsh web --dump-config` vs `dsh workbench --dump-config`,
106
- 差异应恰好是隔离行 + 工作台行 + `ui-sidebar` 被禁用
106
+ 2. **组合树** —— `dsh workbench --dump-config`:工作台行在位、出厂 `ui-sidebar` 被禁用、
107
+ 启动器里没有 web 设置的叠层(脚本直接判,见 ADR-0044)。两棵树**本来就不该相同**
108
+ (工作台不再继承 web 的设置),差异只打出来给人看
107
109
  3. **数据是否分家** —— `~/.dsh/sessions` 与 `~/.dsh/workbench/sessions` 各自独立
108
110
  4. **隔离行是否在位** —— 三个根行都在 `profiles/workbench/cordis.patch.yml` 里
109
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/core",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "个人工作台:Agent 工作台外壳(左栏 + 常驻对话 + 自研右侧数据面板)。",
5
5
  "type": "module",
6
6
  "icon": "./icon.svg",
@@ -87,7 +87,7 @@
87
87
  "access": "public"
88
88
  },
89
89
  "dependencies": {
90
- "@dsh-workbench/shared": "0.2.5"
90
+ "@dsh-workbench/shared": "0.2.6"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "node scripts/build.mjs && node scripts/build-types.mjs",
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
3
  # 一次性引导:从 shipped 的 web 模板创建 workbench profile,
4
- # 继承 web 的配置(bundle 清单 + 设置快照 + 设置引用到的依赖),
4
+ # 继承 web 的 **bundle 清单**(这台机器上装了哪些 bundle),
5
5
  # 再加上会话 / 存储 / 附件的隔离层。
6
6
  #
7
- # 幂等 / 可续跑:profile 已存在就跳过创建,patch 已非空就跳过继承,
8
- # 隔离层已存在就跳过追加,依赖已在 node_modules 就跳过安装
7
+ # **设置不在这里继承**(这里曾经把 web 的 cordis.patch.yml 快照一份过来):
8
+ # 工作台自己的 patch 是唯一真相,想搬 web 的哪几行由用户走「设置 → 配置同步」。
9
+ # 为什么这么分(安装 vs 设置)、为什么不再是自动的:.scratch/config-sync/spec.md
10
+ #
11
+ # 幂等 / 可续跑:profile 已存在就跳过创建,隔离层已存在就跳过追加
9
12
  # —— 重复运行不会覆盖任何东西。
10
13
  # 安全:全程只**读** web 的 profile 目录,只**写** workbench 自己的目录。
11
14
  #
@@ -39,11 +42,6 @@ die() {
39
42
 
40
43
  DST_PATCH="$DST_PROFILE/cordis.patch.yml"
41
44
 
42
- # dsh 生成的 patch 文件带一段注释头,所以"是否还是空层"不能直接和 [] 比。
43
- is_pristine_patch() {
44
- [ "$(grep -v '^[[:space:]]*#' "$1" | tr -d '[:space:]')" = "[]" ]
45
- }
46
-
47
45
  # ── 第 1 步:创建 profile ────────────────────────────────────────────────
48
46
  # --from-default-profile web 只复制 web 模板的 bundle 列表
49
47
  # (dsh-base + dsh-web-app),**不**复制 web 的 cordis.patch.yml。
@@ -85,18 +83,7 @@ fs.writeFileSync(process.env.WB_MANIFEST, JSON.stringify(wb, null, 2) + "\n");
85
83
  console.log("→ 已继承 web 的 bundle 列表: " + merged.join(", "));
86
84
  '
87
85
 
88
- # ── 第 3 步:把 web 的设置文档复制过来,作为「继承」的起点 ──────────────
89
- # profiles/<name>/cordis.patch.yml 就是 Settings 页的活文档
90
- # (config-editor.documentPath === profileContext.patchPath)。
91
- # 复制一份快照,两边之后各自独立演进 —— 这正是「互不干扰」的实现方式。
92
- if is_pristine_patch "$DST_PATCH"; then
93
- cp "$SRC_PROFILE/cordis.patch.yml" "$DST_PATCH"
94
- echo "→ 已继承 web 的配置快照"
95
- else
96
- echo "→ profile 的 patch 已非空,跳过继承(避免覆盖你自己的改动)"
97
- fi
98
-
99
- # ── 第 4 步:追加会话 / 存储 / 附件隔离层 ───────────────────────────────
86
+ # ── 第 3 步:追加会话 / 存储 / 附件隔离层 ───────────────────────────────
100
87
  if grep -q 'id: session-persistence-jsonl' "$DST_PATCH"; then
101
88
  echo "→ 隔离层已存在,跳过"
102
89
  else
@@ -105,70 +92,7 @@ else
105
92
  echo "→ 已追加隔离层(sessions / storages / attachments 分家)"
106
93
  fi
107
94
 
108
- # ── 第 5 步:继承「被 patch 点名」的依赖 ────────────────────────────────
109
- # 第 3 步把 web 的设置文档整份复制了过来,而那份文档是**按 name: 点名**插件的。
110
- # 出厂 @deepseek-ai/* 由 dsh 安装本体解析,不必装;但自己装的第三方包
111
- # (例如 @deepseek-ai/dsh-web-search-exa)只住在 profile 的 node_modules 里
112
- # —— 不跟着继承的话,那一行会以 "failed to import" 静默不激活,功能凭空少一块。
113
- #
114
- # 只继承**被 patch 引用**的那些:web 的 dependencies 里可能有装了却没有任何行
115
- # 引用的包(本机就有 @aipanel/dsh-client 与 @aipanel/dsh-plugin),整份搬过来
116
- # 只是死重量。反过来说,**不能只写进 package.json** —— node_modules 得跟上,
117
- # 否则症状一模一样。
118
- WANTED="$(
119
- WB_MANIFEST="$DST_PROFILE/package.json" \
120
- WEB_MANIFEST="$SRC_PROFILE/package.json" \
121
- PATCH="$DST_PATCH" \
122
- node -e '
123
- const fs = require("node:fs");
124
- const read = (p) => JSON.parse(fs.readFileSync(p, "utf8"));
125
- const wb = read(process.env.WB_MANIFEST);
126
- const web = read(process.env.WEB_MANIFEST);
127
-
128
- // patch 里非注释行的 `name:` —— 注释掉的条目(# 开头)不算引用。
129
- const referenced = new Set();
130
- for (const line of fs.readFileSync(process.env.PATCH, "utf8").split("\n")) {
131
- if (/^\s*#/.test(line)) continue;
132
- const m = /^\s*-?\s*name:\s*"?([^"\s#]+)/.exec(line);
133
- if (m) referenced.add(m[1]);
134
- }
135
-
136
- // workbench 自己已有的声明优先 —— 重复 seed 不会覆盖你钉住的版本。
137
- const own = wb.dependencies || {};
138
- const missing = {};
139
- for (const [name, version] of Object.entries(web.dependencies || {})) {
140
- if (referenced.has(name) && !(name in own)) missing[name] = version;
141
- }
142
- if (Object.keys(missing).length > 0) {
143
- wb.dependencies = Object.assign({}, missing, own);
144
- fs.writeFileSync(process.env.WB_MANIFEST, JSON.stringify(wb, null, 2) + "\n");
145
- }
146
-
147
- // 报出全部「被点名」的依赖(不只是这次新加的):node_modules 也可能被清过。
148
- console.log(
149
- Object.keys(web.dependencies || {})
150
- .filter((name) => referenced.has(name))
151
- .join(" "),
152
- );
153
- '
154
- )"
155
-
156
- WANTED_MISSING=0
157
- if [ -n "$WANTED" ]; then
158
- echo "→ patch 引用的依赖(须在 profile 内可解析): $WANTED"
159
- for dep in $WANTED; do
160
- [ -d "$DST_PROFILE/node_modules/$dep" ] || WANTED_MISSING=1
161
- done
162
- fi
163
-
164
- if [ "$WANTED_MISSING" = "1" ]; then
165
- echo "→ node_modules 缺这些依赖,装进 profile(走 dsh 的包管理器)"
166
- dsh plugin --profile "$(basename "$DST_PROFILE")" install
167
- else
168
- echo "→ 依赖已齐备,跳过安装"
169
- fi
170
-
171
- # ── 第 6 步:建 DIY 工坊 ────────────────────────────────────────────────
95
+ # ── 第 4 步:建 DIY 工坊 ────────────────────────────────────────────────
172
96
  #
173
97
  # 工坊是 DIY 模式的落点:agent 在那里造模块(一个模块一个目录),而 **DIY 会话的 cwd 就是它**。
174
98
  # 骨架 + 依赖 + 稳定入口都交给 `scripts/seed-workshop.sh` —— 启动器(bin/dsh-workbench)
@@ -186,7 +110,10 @@ fi
186
110
 
187
111
  cat <<EOF
188
112
 
189
- profile '$DST_PROFILE' 的骨架好了(bundle 清单 + 设置快照 + 隔离层)。
113
+ profile '$DST_PROFILE' 的骨架好了(bundle 清单 + 会话/存储/附件隔离层)。
114
+
115
+ 设置**不在**这里继承:想要 web 那边的模型供应商 / 搜索源 / MCP,起工作台后去
116
+ 「设置 → 配置同步」按行勾(读的是那个 profile 的 cordis.patch.yml;搬完重启生效)。
190
117
 
191
118
  装 bundle —— 两条**等价**的路,任选一条(都走 dsh 的 runPluginCommand,装完会
192
119
  把 bundle 登记进 profile 清单):