@dsh-workbench/core 1.0.0-beta.1 → 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.
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ copyFileSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ renameSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join, resolve } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { appendBlocks, parseBlocks, removeBlocks } from './patch-blocks.mjs';
18
+
19
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
20
+ const PKG_ROOT = dirname(SCRIPT_DIR);
21
+ const HOME = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
22
+ const PROFILE = process.env.WORKBENCH_PROFILE?.trim() || 'workbench';
23
+ const PROFILE_DIR = join(HOME, 'profiles', PROFILE);
24
+ const RUNTIME_ROOT = process.env.DSH_WORKBENCH_HOME?.trim() || join(HOME, PROFILE);
25
+ const PATCH_PATH = join(PROFILE_DIR, 'cordis.patch.yml');
26
+ const SANDBOX_PATCH = join(PKG_ROOT, 'profile', 'sandbox.patch.yml');
27
+ const MARKER = join(RUNTIME_ROOT, '.data-root-shared');
28
+
29
+ const DATA_ROOT_IDS = new Set(['session-persistence-jsonl', 'storage-json', 'attachment-local']);
30
+ const OWN_ROOT = /DSH_WORKBENCH_HOME|dshHomePath\(\s*['"]workbench/;
31
+
32
+ function say(message) {
33
+ process.stdout.write(`→ ${message}\n`);
34
+ }
35
+
36
+ function warn(message) {
37
+ process.stderr.write(`dsh-workbench: ${message}\n`);
38
+ }
39
+
40
+ function readText(file) {
41
+ try {
42
+ return readFileSync(file, 'utf8');
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ function writeAtomic(file, text) {
49
+ const tmp = `${file}.tmp`;
50
+ writeFileSync(tmp, text, 'utf8');
51
+ renameSync(tmp, file);
52
+ }
53
+
54
+ function mergeTree(from, to, skip = new Set()) {
55
+ let stat;
56
+ try {
57
+ stat = statSync(from);
58
+ } catch {
59
+ return 0;
60
+ }
61
+ if (!stat.isDirectory()) return 0;
62
+ mkdirSync(to, { recursive: true });
63
+ let count = 0;
64
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
65
+ if (entry.name === '.DS_Store' || skip.has(entry.name)) continue;
66
+ const source = join(from, entry.name);
67
+ const target = join(to, entry.name);
68
+ if (entry.isDirectory()) {
69
+ count += mergeTree(source, target);
70
+ } else if (entry.isFile() && !existsSync(target)) {
71
+ copyFileSync(source, target);
72
+ count += 1;
73
+ }
74
+ }
75
+ return count;
76
+ }
77
+
78
+ function unionList(left, right) {
79
+ return [...new Set([...(Array.isArray(left) ? left : []), ...(Array.isArray(right) ? right : [])])];
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
+
134
+ function mergeWorkspaceList(sourceFile, targetFile) {
135
+ let source;
136
+ try {
137
+ source = JSON.parse(readFileSync(sourceFile, 'utf8'));
138
+ } catch {
139
+ return 0;
140
+ }
141
+ if (source?.unit?.name !== 'workspace') return 0;
142
+ if (!existsSync(targetFile)) {
143
+ mkdirSync(dirname(targetFile), { recursive: true });
144
+ copyFileSync(sourceFile, targetFile);
145
+ return 1;
146
+ }
147
+ let target;
148
+ try {
149
+ target = JSON.parse(readFileSync(targetFile, 'utf8'));
150
+ } catch {
151
+ warn(`${targetFile} 读不出来,这次没并项目列表,下次启动再试。`);
152
+ return null;
153
+ }
154
+ if (target?.unit?.name !== 'workspace') return 0;
155
+ const sourceTable = source.tables?.workspaces ?? {};
156
+ const targetTable = target.tables?.workspaces ?? {};
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
+ }
170
+ }
171
+ const order = orderedIds(targetGlobal, targetTable);
172
+ const idMap = new Map();
173
+ let added = 0;
174
+ for (const sourceId of orderedIds(sourceGlobal, sourceTable)) {
175
+ const entry = sourceTable[sourceId];
176
+ if (typeof entry?.path !== 'string') continue;
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);
192
+ continue;
193
+ }
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);
201
+ }
202
+ targetTable[newId] = { ...entry, sessionIds };
203
+ idByPath.set(entry.path, newId);
204
+ order.push(newId);
205
+ added += 1;
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;
215
+ target.tables = { ...(target.tables ?? {}), workspaces: targetTable };
216
+ target.global = {
217
+ ...targetGlobal,
218
+ workspaceIds: order,
219
+ archivedSessionIds: unionList(targetGlobal.archivedSessionIds, sourceGlobal.archivedSessionIds),
220
+ pinnedSessionIds: unionList(targetGlobal.pinnedSessionIds, sourceGlobal.pinnedSessionIds),
221
+ };
222
+ if (defaultWorkspaceId === undefined) delete target.global.defaultWorkspaceId;
223
+ else target.global.defaultWorkspaceId = defaultWorkspaceId;
224
+ writeAtomic(targetFile, `${JSON.stringify(target, null, 2)}\n`);
225
+ return added;
226
+ }
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
+
274
+ function mergeDataRoot() {
275
+ if (resolve(RUNTIME_ROOT) === resolve(HOME)) return { added: 0, files: 0 };
276
+ const targetWorkspace = join(HOME, 'storages', 'workspace.json');
277
+ const sourceStorages = join(RUNTIME_ROOT, 'storages');
278
+ let added;
279
+ let files = 0;
280
+ try {
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']));
286
+ } catch (error) {
287
+ warn(`并回 ${HOME} 时出错:${error instanceof Error ? error.message : String(error)}`);
288
+ return null;
289
+ }
290
+ repairTarget(targetWorkspace);
291
+ return { added, files };
292
+ }
293
+
294
+ function writeMarker() {
295
+ mkdirSync(dirname(MARKER), { recursive: true });
296
+ writeFileSync(
297
+ MARKER,
298
+ `${JSON.stringify({ at: new Date().toISOString(), from: RUNTIME_ROOT, to: HOME }, null, 2)}\n`,
299
+ 'utf8',
300
+ );
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;
394
+ }
395
+
396
+ function stripIsolationHeader(text, blockStart) {
397
+ const lines = text.split('\n');
398
+ let marker = -1;
399
+ for (let i = blockStart - 1; i >= 0; i -= 1) {
400
+ const line = lines[i] ?? '';
401
+ if (/^#.*会话\s*\/\s*存储隔离层/.test(line)) {
402
+ marker = i;
403
+ break;
404
+ }
405
+ if (line.trim() !== '' && !line.startsWith('#')) break;
406
+ }
407
+ if (marker === -1) return text;
408
+ lines.splice(marker, blockStart - marker);
409
+ return lines.join('\n');
410
+ }
411
+
412
+ const MIGRATE = process.argv.includes('--migrate') || process.argv.includes('migrate');
413
+ const FORCE = process.argv.includes('--force');
414
+
415
+ const profileManifest = join(PROFILE_DIR, 'package.json');
416
+ if (!existsSync(profileManifest)) process.exit(0);
417
+
418
+ if (MIGRATE) process.exit(migrate());
419
+ configureDefault();
@@ -7,7 +7,7 @@
7
7
 
8
8
  | 要做什么 | 用 |
9
9
  |---|---|
10
- | 认路、装没装、起不起得来 | `_status`(`unit` 可省);它的「下一步」就是下一件事 |
10
+ | 认路、装没装、起不起得来 | `_status`(`unit` 可省);它的「下一步」就是下一件事;带 `unit` 时 text 末尾多一条「发布出口:…」 |
11
11
  | 卡片空白 / 崩了 / 读不到 | `_panel` |
12
12
  | 找 opencli 命令、看命令形状 | `_recon` → `_spec`(`--help -f json` 原样) |
13
13
  | kit 有什么 / 令牌叫什么 | `_components`(`bucket` / `name` 取声明;`scope: tokens` 现读 `--dsw-*` / `--wb-*`) |
@@ -45,8 +45,6 @@
45
45
 
46
46
  ## 4 发布
47
47
 
48
- 只在用户明确说「帮我发布」后调。它一次做完预检 → 全门禁 → 按出口发布 → 发后核包,审批一次。
49
- 发布前先 `_changelog` 把这一版写进 `CHANGELOG.md`:没有这一版的记录,`_publish` 会拦下。
50
- 审批屏会把这一版的更新日志按 markdown 展示给用户。
51
- 需要用户接管时返回 `needsUser: { step, text, command? }` 而不抛异常:把 `text` 说给用户、给命令,
52
- 等「好了」再跑;不向用户要 token,不展示 `package.json`。
48
+ 只在用户明确说「帮我发布」后调;先 `_changelog` 把这一版写进 `CHANGELOG.md`。
49
+ 出口、署名、标题、安装命令由工具与审批屏处理,不用问、不用补。
50
+ 需要用户接管时返回 `needsUser: { step, text, command? }`:把 `text` 说给用户,等「好了」再跑;不向用户要 token。
@@ -45,10 +45,9 @@ description: 在工作台里把当前面板调明白,或者给它造一块新
45
45
  看 / 动屏前 `workbench_diy_debug_start`(否则整族拒;Desktop 跳过,改用 `read=` / 录制);`_panel` / `_status` / `read=` 档不用。
46
46
  工具全表 [FAMILY.md](FAMILY.md)。走通才 `dsh-wb-module design accept`;走不通如实记"未验"。
47
47
  审批项标注「要你真用一次」,不打勾。结论只报:已装、在面板哪一格。
48
- 8. **发布** —— 只在用户说「帮我发布」之后。先确认他已经在真卡片上说过「没问题」(`design accept` 已打勾),
49
- 并把这一版写进 `CHANGELOG.md`:`workbench_diy_changelog`(`unit=…`,`note=…`;
50
- 没写这一版 `release` 会拦下),再调 `workbench_diy_publish`(`unit=…`)。只问「给团队用、公开发、还是先给你一个安装包」;
51
- 不展示 `package.json` / npm / 版本字段,也不要问 token。出口与 `needsUser` 处理见 [WIRING.md](WIRING.md) §5。
48
+ 8. **发布** —— 只在用户说「帮我发布」之后:`workbench_diy_changelog`(`note=…`)记这一版 → `workbench_diy_publish`(`unit=…`)。
49
+ 不手改 `package.json`、不补标题/署名、不问出口:工具与审批屏已经处理;他明确点名安装包 / 公开 / git 才传 target。
50
+ 验收没过先 `design accept`;`needsUser` 照 `text` 转述。细节见 [WIRING.md](WIRING.md) §5。
52
51
 
53
52
  ## 索引
54
53
 
@@ -79,5 +78,5 @@ description: 在工作台里把当前面板调明白,或者给它造一块新
79
78
  ## 硬约束
80
79
 
81
80
  - dsh 已有的从 `kit` 拿(`@dsh-workbench/core/client`);不手搓 DOM + CSS,不直接 import dsh 的包。
82
- - DIY 模块默认 `private: true`,不发任何源;用户确认后走 `workbench_diy_publish` 发到团队源 / 公网 / 安装包 / git。
81
+ - DIY 模块默认 `private: true`,不发任何源;用户确认后走 `workbench_diy_publish`。
83
82
  - 源码零注释(`//`、块注释、`.css` 注释都不许);说明写模块 `README.md` 或 spec。
@@ -63,16 +63,7 @@ DIY 族仅在选了 DIY 模式的会话出现(按模式,不按工作区)
63
63
  默认 `private: true`,不发任何源。用户说「帮我发布」时,**不要手改 `package.json`、不要让他看 npm**,
64
64
  走 `workbench_diy_publish`(`unit=…`):
65
65
 
66
- 1. 先确认他已经在真卡片上说过「没问题」,并已跑过 `design accept`(验收 N/N 全勾)。
67
- 还要把这一版写进 `CHANGELOG.md`:调 `workbench_diy_changelog`(`unit=…`,`note=…`);
68
- 没有这一版的记录(或包里没带 `CHANGELOG.md`)时 `release` 会拦下,并给出补的命令。
69
- 2. 工具一次做完预检(登录 / license / git 远端 / 工作区)→ 全门禁 → 按出口发布 → 发后核包。
70
- 发布审批屏会把这一版的更新日志按 markdown 展示给用户。
71
- 3. 出口只有四种:团队源(配了私源时默认)、公网 npm(必须用户明确说“公开”)、安装包(没私源时默认,
72
- 零凭据)、git(提交构建产物 + tag + push)。只问「给团队用、公开发、还是先给你一个安装包」;
73
- 凭据细节由预检接管。
74
- 4. 工具回 `needsUser` 时,照它那句 `text` 对用户说一句,给能照抄的命令,等他说「好了」再跑;
75
- **不在对话里要 token / OTP**,不把红字贴给用户。`otp` 交接后用 `workbench_diy_publish` 的 `verify=true`
76
- (或 `release --verify`)只核不发。
77
- 5. `dsh-wb-module publish` 仍是**发货前清单**(只 dry run);`release --dry-run` 只预览;
78
- `--verify` 只核已发布的包。公网源必须显式 `--target public` + license,默认只认团队源 / 安装包。
66
+ 1. 先把这一版写进 `CHANGELOG.md`(`workbench_diy_changelog`,`note=…`),真卡片验收过(`design accept`)。
67
+ 出口、署名、标题、安装命令都由工具与审批屏处理:不用问、不用补;用户没点名出口就不传 target。
68
+ 2. 工具回 `needsUser` 时照 `text` 转述,不在对话里要 token / OTP;`otp` 交接后 `verify=true` 只核不发。
69
+ 3. 只改市集元数据(标题 / 作者 / 说明)不升版本:`refresh=true`。
@@ -1 +0,0 @@
1
- export {};
@@ -1,46 +0,0 @@
1
- # workbench profile 的会话/存储隔离层(片段)。
2
- #
3
- # 这个文件是"补丁层片段",不是独立补丁文件:seed-profile.sh 会把它
4
- # 追加进 ~/.dsh/profiles/workbench/cordis.patch.yml。
5
- #
6
- # 为什么需要它:dsh-base 把会话与存储的根都挂在 $DSH_HOME 上
7
- # (dshHomePath('sessions') / dshHomePath('storages')),所以默认情况下
8
- # 两个 profile 共用同一份会话历史。工作台要提供自己的定制工具,等于一个
9
- # 独立的 agent 环境,共享会话没有意义 —— 这三行把数据分开。
10
- #
11
- # 隔离后(默认 profile;新 profile 由 DSH_WORKBENCH_HOME 指到自己的根):
12
- # 会话 $DSH_HOME/workbench/sessions/
13
- # 工作区 $DSH_HOME/workbench/storages/ (workspace 走 storage-domain)
14
- # 附件 $DSH_HOME/workbench/attachments/v1/
15
- # 有意不隔离:
16
- # 凭据 ~/.dsh/.credentials.yaml (两边共用同一把 API key)
17
- #
18
- # 注意:patch 的 config 是整体替换、不深合并。这三行原本各自只声明了
19
- # root / dshHome,所以只重述这一个字段是安全的;若你给这些行配过别的
20
- # 字段(例如 session-persistence-jsonl 的 compression),必须一并重述。
21
-
22
- - id: session-persistence-jsonl
23
- name: '@deepseek-ai/dsh-session-persistence-jsonl'
24
- config:
25
- root: !!js (process.env.DSH_WORKBENCH_HOME ?? dshHomePath('workbench')) + '/sessions'
26
-
27
- - id: storage-json
28
- name: '@deepseek-ai/dsh-storage-json'
29
- config:
30
- root: !!js (process.env.DSH_WORKBENCH_HOME ?? dshHomePath('workbench')) + '/storages'
31
-
32
- - id: attachment-local
33
- name: '@deepseek-ai/dsh-attachment-local'
34
- config:
35
- dshHome: !!js process.env.DSH_WORKBENCH_HOME ?? dshHomePath('workbench')
36
-
37
- # 可写根:**无会话**的插件命令(市场装模块、自举跑的那几条)回落到它。
38
- #
39
- # 跟启动器的 profile/sandbox.patch.yml 同值,两处都在是因为入口不同:启动器每次启动叠
40
- # 那一层;直接用 `dsh workbench` 起的没有 `--patch`,就靠这一行。两处同值 ⇒ 谁先谁后都不算变更,
41
- # 不会触发在线重组。**别把它放回 bundle 层**(在线启用会撞 session-controller 的重组竞态,
42
- # 见 ADR-0037 的 2026-10-22 补记)。
43
- - id: sandbox-policy
44
- config:
45
- mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
46
- workspaceRoot: !!js process.env.DSH_WORKBENCH_HOME ?? dshHomePath('workbench')