@dsh-workbench/core 1.0.0 → 1.1.0

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.
Files changed (41) hide show
  1. package/README.md +16 -3
  2. package/bin/dsh-workbench +38 -6
  3. package/client.js +669 -305
  4. package/dist/client/client.body.js +669 -305
  5. package/dist/client/index.js +669 -305
  6. package/dist/host/index.js +445 -37
  7. package/dist/types/app/layouts/market/discover-sources-dialog.d.ts +4 -0
  8. package/dist/types/app/layouts/market/discover-states.d.ts +3 -0
  9. package/dist/types/app/layouts/market/use-add-module.d.ts +0 -1
  10. package/dist/types/app/layouts/market/use-discover.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/MarketView.d.ts +3 -1
  12. package/dist/types/app/layouts/rightbar/market-names.d.ts +8 -0
  13. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +11 -3
  14. package/dist/types/app/layouts/rightbar/market-rows.d.ts +6 -4
  15. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +4 -3
  16. package/dist/types/app/units/unit-titles.d.ts +3 -0
  17. package/dist/types/kernel/contract/bundle-install.d.ts +1 -1
  18. package/dist/types/kernel/contract/market-index.d.ts +1 -1
  19. package/dist/types/modules/panels/model/diy-tools.d.ts +1 -1
  20. package/dist/types/modules/panels/model/market-dispatch.d.ts +8 -0
  21. package/dist/types/modules/panels/model/market-follow-up.d.ts +16 -0
  22. package/dist/types/modules/panels/model/market-sources-act.d.ts +2 -0
  23. package/dist/types/modules/panels/model/market-types.d.ts +5 -0
  24. package/dist/types/modules/panels/model/market.d.ts +2 -0
  25. package/dist/types/modules/panels/model/publish-preview-routes.d.ts +1 -1
  26. package/dist/types/shared/api/host/diy-publish-preview.d.ts +11 -0
  27. package/dist/types/shared/api/host/follow-up-dismiss.d.ts +2 -0
  28. package/dist/types/shared/api/host/migrate-workbench.d.ts +20 -0
  29. package/dist/types/shared/api/host/migration-follow-up.d.ts +12 -0
  30. package/dist/types/shared/hooks/market-queries.d.ts +3 -2
  31. package/dist/types/shared/hooks/resource-view.d.ts +14 -0
  32. package/dist/types/shared/hooks/use-api-resource.d.ts +3 -7
  33. package/package.json +3 -3
  34. package/scripts/seed-profile.sh +9 -7
  35. package/scripts/share-data-root.mjs +272 -64
  36. package/skills/workbench-module-diy/FAMILY.md +1 -1
  37. package/skills/workbench-module-diy/REVIEW.md +2 -2
  38. package/skills/workbench-module-diy/SKILL.md +1 -0
  39. package/skills/workbench-module-diy/TOOLS.md +1 -1
  40. package/skills/workbench-module-diy/WIRING.md +3 -1
  41. package/skills/workbench-module-diy/WRITES.md +1 -1
@@ -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();
@@ -46,5 +46,5 @@
46
46
  ## 4 发布
47
47
 
48
48
  只在用户明确说「帮我发布」后调;先 `_changelog` 把这一版写进 `CHANGELOG.md`。
49
- 出口、署名、标题、安装命令由工具与审批屏处理,不用问、不用补。
49
+ 出口、署名、标题、安装命令由工具与审批屏处理,不用问、不用补;没写作者时审批屏显示推断的署名(市集里上次的 / 同工坊的 / 本机 git 身份),用户觉得不对就拒绝后再说,他点名了才传 `author=…`。
50
50
  需要用户接管时返回 `needsUser: { step, text, command? }`:把 `text` 说给用户,等「好了」再跑;不向用户要 token。
@@ -5,12 +5,12 @@
5
5
  | 判据 | 检查 | 修法 |
6
6
  |---|---|---|
7
7
  | `contracts` | 清单 ↔ 代码 ↔ `design.json` 漂移;dsh 组合期不校验清单 | 按报错改:`unit` 重名换名;`dsh.workbenchFaces` 两个源码入口;六个脚本;`private: true`。`dsh.workbench.tools` 与 `registerTools()` 一致;`design.json` 对键名;生成物过期跑 `sync` |
8
- | `protocol` | `dsh.workbench` 版本 / 形状 / 多余键(外壳静默忽略生字段) | 补 `"apiVersion": 2`;`unit` / `card` / `tools` / `host` 写全(无卡片 `null`、无工具 `[]`);删协议外键(`faces` 在 `dsh.workbenchFaces`;不写 `scope`) |
8
+ | `protocol` | `dsh.workbench` 版本 / 形状 / 多余键(外壳静默忽略生字段) | 补 `"apiVersion": 3`;`unit` / `title` / `card` / `tools` / `host` 写全(名字是 `{ zh, en }` 字面量;无卡片 `null`、无工具 `[]`);删协议外键(`faces` 在 `dsh.workbenchFaces`;不写 `scope`) |
9
9
  | `bundles` | 运行时互串:浏览器产物含 node 内置 / 宿主产物含 DOM、react | 不 import 对方那侧 |
10
10
  | `source` | 非种子 `@deepseek-ai/*`;宿主碰 dsh 浏览器包;`@/` 指外壳;根入口取运行值;客户端引 `/host`;**手写进程 / PATH / 文件锁 / 状态原子写**(import 首选库放行) | 宿主只用 Node 侧;种子词仅在 client 可达文件;`@/` 只指自己 `src/`;运行值:浏览器 `kit`、宿主 `shared/host`;进程/锁/状态写交给外壳 |
11
11
  | `primitives` | 声明 `external` 但产物无物证(原语被内联) | `dsh.client.external` 声明;devDependency 精确版对齐;产物中为 `require(...)` |
12
12
  | `kit` | 用了 peer 下限外壳没有的成员(首帧 `Element type is invalid`) | 只用 peer 范围内成员;下限抬到实际使用版 |
13
- | `cards` | 七种外壳侧输入 × `zh` / `en` 下崩溃 / 空白 / 脏值 | 五态可画;外层 `Card`;`titleKey` 与卡片用到的键双语齐。不断言具体态(归 `plugin.test.ts`) |
13
+ | `cards` | 七种外壳侧输入 × `zh` / `en` 下崩溃 / 空白 / 脏值 | 五态可画;外层 `Card`;卡片自己用到的文案键双语齐(名字走清单 `title`,不再是卡片键)。不断言具体态(归 `plugin.test.ts`) |
14
14
  | `standalone` | 依赖工作台源码才能构建 | 不引用工作台源码;`tsconfig.paths` 用相对路径 |
15
15
  | `markers` | 私有源 / 私网地址 / 内网域名进入公开包 | 不进包;配置落到模块配置面;组织特有的词表由 `DSH_WB_INTERNAL_MARKERS` 提供 |
16
16
  | `alias` | `@/` 指向工作台源码 | `@/` 只指自己 `src/` |
@@ -47,6 +47,7 @@ description: 在工作台里把当前面板调明白,或者给它造一块新
47
47
  审批项标注「要你真用一次」,不打勾。结论只报:已装、在面板哪一格。
48
48
  8. **发布** —— 只在用户说「帮我发布」之后:`workbench_diy_changelog`(`note=…`)记这一版 → `workbench_diy_publish`(`unit=…`)。
49
49
  不手改 `package.json`、不补标题/署名、不问出口:工具与审批屏已经处理;他明确点名安装包 / 公开 / git 才传 target。
50
+ 他说要换作者才传 `author="姓名 <邮箱>"`,别自己编一个;没写作者时审批屏显示推断的署名,他拒绝后再说。
50
51
  验收没过先 `design accept`;`needsUser` 照 `text` 转述。细节见 [WIRING.md](WIRING.md) §5。
51
52
 
52
53
  ## 索引
@@ -118,7 +118,7 @@
118
118
  未知 `kind` 或 `source.kind` 判红;need 里未知字段只警告,`x-` 前缀忽略。
119
119
  - 未装好文案由模块写:`libInstallCommand({ need, source })`(`@dsh-workbench/shared/host`)拼好源与缓存目录,
120
120
  原样输出;放进失败态。
121
- - `contracts` 判据:`apiVersion` 必须是 2;`needs` 走同一份 `NEED_KINDS` 注册表校验(`purpose` 必填、
121
+ - `contracts` 判据:`apiVersion` 必须是 3;`needs` 走同一份 `NEED_KINDS` 注册表校验(`purpose` 必填、
122
122
  `bin`/`env` 不许带 `sources`、`lib` 只认 npm 源);声明了 `lib` 必须用 `libInstallCommand`。
123
123
  未知 `runtime` 不判红:探针会给「这个运行时工作台还不认识」的 `unchecked`,等运行时 provider 补上。
124
124
 
@@ -65,5 +65,7 @@ DIY 族仅在选了 DIY 模式的会话出现(按模式,不按工作区)
65
65
 
66
66
  1. 先把这一版写进 `CHANGELOG.md`(`workbench_diy_changelog`,`note=…`),真卡片验收过(`design accept`)。
67
67
  出口、署名、标题、安装命令都由工具与审批屏处理:不用问、不用补;用户没点名出口就不传 target。
68
+ 用户要换署名(或在审批屏上说作者不对):传 `author="姓名 <邮箱>"`(或只给姓名),别手改 `package.json`,也别自己编。
69
+ 没写作者时审批屏显示推断的署名;他拒绝后说换成谁,再用 `author` 重发。
68
70
  2. 工具回 `needsUser` 时照 `text` 转述,不在对话里要 token / OTP;`otp` 交接后 `verify=true` 只核不发。
69
- 3. 只改市集元数据(标题 / 作者 / 说明)不升版本:`refresh=true`。
71
+ 3. 只改市集元数据(标题 / 作者 / 说明)不升版本:`refresh=true`(换作者时同时传 `author`)。
@@ -23,7 +23,7 @@
23
23
 
24
24
  3. **无现成命令 → 让用户录一次**(你准备,用户执行):
25
25
 
26
- - `workbench_diy_record_prepare({ unit, url, doing })`:开窗置前(**需审批一次**:站点、录哪一下、落点、
26
+ - `workbench_diy_record_prepare({ unit, url, doing })`:开窗置前(**需审批一次**:站点、录哪一下、保存到、
27
27
  录完不切回),装浮条。用户:① 开始录制 ② 做一次动作 ③ 结束录制。不要让用户点第二次。
28
28
  `url` 必须与模块读源同域(`design.json` 声明过读源时跨域当场拒)。
29
29
  - 表填好、提交前,请用户按浮条「截图」:不额外审批;产出整页图 + 页面快照(控件 `name` / `id`、