@dsh-workbench/module-tools 1.2.7 → 1.2.8-beta.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 +10 -8
- package/bin/dsh-wb-module.mjs +1 -1
- package/package.json +2 -2
- package/src/checks/contracts.mjs +1 -1
- package/src/checks/source.mjs +1 -1
- package/src/design.mjs +265 -21
- package/src/init.mjs +7 -3
- package/src/release-journal.mjs +2 -0
- package/src/release-target.mjs +1 -1
- package/src/release.mjs +1 -1
- package/template-diy/AGENTS.md.tpl +5 -3
- package/template-diy/DEVELOPING.md.tpl +185 -0
- package/template-diy/README.md.tpl +2 -180
- package/template-diy/package.json.tpl +1 -1
- package/template-diy/readme.json.tpl +27 -0
- package/template-diy/scripts/smoke.mjs.tpl +3 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
pnpm exec dsh-wb-module init my-unit # 建骨架(唯一模板,已默认)
|
|
10
10
|
pnpm build # dist/host.js + 套了 loader 壳的 client.js
|
|
11
11
|
pnpm check # 十四条判据
|
|
12
|
-
pnpm design check # 设计底稿有没有空格、README 有没有落后
|
|
12
|
+
pnpm design check # 设计底稿有没有空格、readme.json / README / DEVELOPING 有没有落后
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
要发货再加一步 `pnpm exec dsh-wb-module publish`:跑门禁、核 `files` 与 `npm pack` 清单,默认 **只 dry run,不真发**。
|
|
@@ -55,7 +55,7 @@ pnpm design check # 设计底稿有没有空格、README 有没有落后
|
|
|
55
55
|
| `dsh-wb-module check [--only <判据>] [--package <包名或 unit>]` | 十四条判据;`--only` 单跑,也当查询口用;`--package` 只报这个模块的问题(见下);`--list [--json\|--markdown]` 只列判据元数据 |
|
|
56
56
|
| `dsh-wb-module lint [--fix]` | 用模块自己那份 `eslint.config.mjs`;`--fix` 写回文件 |
|
|
57
57
|
| `dsh-wb-module sync` | 按清单生成 `src/declared-tools.ts`,按 `design.json` 的 `key + recipe` 生成 `src/datasource.ts` |
|
|
58
|
-
| `dsh-wb-module design <init\|check\|render\|accept>` |
|
|
58
|
+
| `dsh-wb-module design <init\|migrate\|check\|render\|accept>` | 设计底稿:填、拆旧 README、查空格、生成 README 与 DEVELOPING、验收打勾 |
|
|
59
59
|
| `dsh-wb-module migrate [--dry-run]` | 把模块清单迁到当前代:`apiVersion` → 3、缺/空的 `workbenchFaces` 补默认;源码类问题只给提示(check 会点名) |
|
|
60
60
|
| `dsh-wb-module changelog check [版本]` | 查这一版有没有记录;发布时用 `release --changelog-note` 写进去 |
|
|
61
61
|
| `dsh-wb-module publish` | 发货前清单:门禁 + `files` + `npm pack`(只 dry run) |
|
|
@@ -196,10 +196,12 @@ export default [
|
|
|
196
196
|
`dsh-wb-module lint --fix` 先写回再跑一遍看还剩什么。**`check` 永远不动手改文件** ——
|
|
197
197
|
门禁是判据,不是修复器。
|
|
198
198
|
|
|
199
|
+
模板生成三份文档:`readme.json` 是终端用户文案源(标题、简介、卡片布局草图 `shots[]`、会看到什么、能做什么、agent 能做什么、数据从哪来、排障、FAQ,后五段是短条目),`README.md` 只由它生成;`DEVELOPING.md` 是作者文档(实现、路由、判据、数据源与三张表),由 `design.json` 生成。`design render` 刷新这两份;`design check` 会拦住空文案、占位符和技术词;旧模块先跑 `design migrate`,它把旧 `README.md` 原样保存为 `DEVELOPING.md`,再生成 `readme.json` 与用户 README。
|
|
200
|
+
|
|
199
201
|
## `design.json`:三张表
|
|
200
202
|
|
|
201
203
|
面板 / 能力 / 联动三张表、状态矩阵、数据源记录、写通道记录、验收清单都住在模块根目录的
|
|
202
|
-
`design.json`,
|
|
204
|
+
`design.json`,DEVELOPING.md 里那一段由它生成(夹在 `<!-- dsh-wb:design:start/end -->` 之间)。README.md 的用户文案不看 design.json,只看模块根的 `readme.json`。
|
|
203
205
|
|
|
204
206
|
它是**设计底稿**:三张表与探明记录装人话(`oneLine` / `shape` / `approvalText` / 三张表的文字);
|
|
205
207
|
读源那一条可以多带 `key` 与 `recipe` 两个机器字段 —— 它们只用于生成 `src/datasource.ts`,
|
|
@@ -208,7 +210,7 @@ export default [
|
|
|
208
210
|
`design.json` 的模块不查。
|
|
209
211
|
|
|
210
212
|
有写能力的模块另在 `writes[]` 里落命令 / 入参 / 出参 / 预览档 / 回读 / 失败形状,
|
|
211
|
-
渲染成
|
|
213
|
+
渲染成 DEVELOPING.md 里的「写通道探明记录」。没有写通道就不用写,不会因此变红。
|
|
212
214
|
写也能落配方:`writes[]` 里加一对 `key` + `recipe`,`sync` 把它和读一起生成进
|
|
213
215
|
`DATASOURCE`(`data.submit(DATASOURCE.<key>)`);配方盖不住的写写 `custom: { why }`(warn)。
|
|
214
216
|
|
|
@@ -262,8 +264,8 @@ export default [
|
|
|
262
264
|
- 形状与六种配方见模块 DIY 的 [`DATASOURCE.md`](../core/skills/workbench-module-diy/DATASOURCE.md)。
|
|
263
265
|
|
|
264
266
|
```bash
|
|
265
|
-
dsh-wb-module design check # 空着的格一格格点名叫出来 + README 有没有落后
|
|
266
|
-
dsh-wb-module design render # 用
|
|
267
|
+
dsh-wb-module design check # 空着的格一格格点名叫出来 + README / DEVELOPING 有没有落后
|
|
268
|
+
dsh-wb-module design render # 用 readme.json 重新生成 README,用 design.json 刷新 DEVELOPING 那一段
|
|
267
269
|
dsh-wb-module design accept # 在真卡片上逐条走通后打勾
|
|
268
270
|
```
|
|
269
271
|
|
|
@@ -325,7 +327,7 @@ dsh-wb-module design accept # 在真卡片上逐条走通后打勾
|
|
|
325
327
|
`eslint.config.mjs`。
|
|
326
328
|
|
|
327
329
|
`init` 连模板里的 `*.tpl` 一起拷、按已知 token 名逐个盖章(`__UNIT__` / `__NAME__` / `__DESC__` /
|
|
328
|
-
`__SHELL_RANGE__` / `__DSH_UI_VERSION__`)、剥掉后缀,再用 `
|
|
330
|
+
`__SHELL_RANGE__` / `__DSH_UI_VERSION__`)、剥掉后缀,再用 `readme.json` 生成 README、用 `design.json` 生成 DEVELOPING 那一段。
|
|
329
331
|
`__SHELL_RANGE__` 取 init 所在工程实装的那份外壳版本;读不到就退回 module-tools 自己的版本;
|
|
330
332
|
两者都读不到才保持 `^0.1.0` 并在 stderr 上说一句。
|
|
331
333
|
|
|
@@ -336,7 +338,7 @@ node scripts/selfcheck/index.mjs
|
|
|
336
338
|
```
|
|
337
339
|
|
|
338
340
|
工具箱的回归网:模板 `init` 一遍、跑全套;判据的正例与负例都要对;打包清单不含
|
|
339
|
-
`.tpl` 残留;`design` 的空格、README 同步、`accept`;`publish` 的清单;`release` 的验收闸、
|
|
341
|
+
`.tpl` 残留;`design` 的空格、README / DEVELOPING 同步、`accept`;`publish` 的清单;`release` 的验收闸、
|
|
340
342
|
dry-run、真发后的元数据与发布失败回滚。
|
|
341
343
|
|
|
342
344
|
## 相关文档
|
package/bin/dsh-wb-module.mjs
CHANGED
|
@@ -105,7 +105,7 @@ const COMMANDS = {
|
|
|
105
105
|
check: '十四条判据;--only 单跑,也当查询口用;--list 只列判据元数据',
|
|
106
106
|
init: '建模块骨架(--dir / --template diy / --description;只有 diy 一个模板)',
|
|
107
107
|
'market-init': '初始化市场索引仓库(写 market/ 与 README,推首个 commit)',
|
|
108
|
-
design: '设计底稿:init 填 / check 查空格 / render 生成 README / accept 打勾',
|
|
108
|
+
design: '设计底稿:init 填 / migrate 拆旧 README / check 查空格 / render 生成 README 与 DEVELOPING / accept 打勾',
|
|
109
109
|
migrate: '把模块清单迁到当前代(apiVersion / workbenchFaces);源码类问题只给提示',
|
|
110
110
|
publish: '发货前清单:门禁 + files + npm pack(只 dry run)',
|
|
111
111
|
release: '真发布:全门禁 + 验收闸 + 预检接管 + 发到出口 + 发后核包 + 更新市场索引(中断可 --resume)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8-beta.1",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"semver": "^7.8.5",
|
|
34
34
|
"typescript": "5.9.3",
|
|
35
35
|
"typescript-eslint": "^8.71.0",
|
|
36
|
-
"@dsh-workbench/shared": "1.2.
|
|
36
|
+
"@dsh-workbench/shared": "1.2.8-beta.1"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -727,7 +727,7 @@ export async function run() {
|
|
|
727
727
|
for (const name0 of inDeclaration.filter((one) => !inDesign.includes(one))) {
|
|
728
728
|
problems.push(
|
|
729
729
|
`${name}: 清单的 tools[] 里声明了「${name0}」,design.json 的能力行里却没有它 —— ` +
|
|
730
|
-
'底稿是给人读的那半(它干嘛、要不要点头,`design render` 拿它出
|
|
730
|
+
'底稿是给人读的那半(它干嘛、要不要点头,`design render` 拿它出 DEVELOPING.md):' +
|
|
731
731
|
`要么在 design.json 的 capabilities[] 里为「${name0}」补一行,要么把它从清单里删掉`,
|
|
732
732
|
);
|
|
733
733
|
}
|
package/src/checks/source.mjs
CHANGED
|
@@ -218,7 +218,7 @@ export async function run() {
|
|
|
218
218
|
const text = readFileSync(file, 'utf8');
|
|
219
219
|
for (const line of commentViolationsOf(text, { css: file.endsWith('.css') })) {
|
|
220
220
|
problems.push(
|
|
221
|
-
`${name}: ${rel}:${line} 有注释 ——
|
|
221
|
+
`${name}: ${rel}:${line} 有注释 —— 模块源码零注释(用户说明写 README.md,作者说明写 DEVELOPING.md 或 spec;eslint / ts / vitest 指令注释除外)`,
|
|
222
222
|
);
|
|
223
223
|
}
|
|
224
224
|
}
|
package/src/design.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { callsProblemOf, describeRecipe, recipeProblemOf } from '@dsh-workbench/shared/host';
|
|
4
4
|
|
|
5
5
|
export const DESIGN_FILE = 'design.json';
|
|
6
|
+
export const DEVELOPING_FILE = 'DEVELOPING.md';
|
|
7
|
+
export const README_SOURCE_FILE = 'readme.json';
|
|
6
8
|
export const START_MARK = '<!-- dsh-wb:design:start -->';
|
|
7
9
|
export const END_MARK = '<!-- dsh-wb:design:end -->';
|
|
8
10
|
|
|
@@ -430,6 +432,184 @@ export function renderDesign(design) {
|
|
|
430
432
|
].join('\n');
|
|
431
433
|
}
|
|
432
434
|
|
|
435
|
+
function bodyLinesOf(value) {
|
|
436
|
+
if (Array.isArray(value)) {
|
|
437
|
+
return value.flatMap((one) => {
|
|
438
|
+
const line = text(one);
|
|
439
|
+
return line === '' ? [] : [`- ${line}`];
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const line = text(value);
|
|
443
|
+
return line === '' ? [] : [line];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function sectionOf(heading, value) {
|
|
447
|
+
const lines = bodyLinesOf(value);
|
|
448
|
+
return lines.length === 0 ? [] : [`## ${heading}`, '', ...lines, ''];
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function shotsOf(source) {
|
|
452
|
+
const listed = Array.isArray(source?.shots) ? source.shots : [];
|
|
453
|
+
const single = text(source?.ascii) === '' ? [] : [{ caption: '', ascii: source.ascii }];
|
|
454
|
+
return [...single, ...listed].flatMap((one) => {
|
|
455
|
+
const drawing = typeof one?.ascii === 'string' ? one.ascii.replace(/\s+$/, '') : '';
|
|
456
|
+
if (drawing.trim() === '') return [];
|
|
457
|
+
return [{ caption: text(one?.caption), drawing }];
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function renderUserReadme(source) {
|
|
462
|
+
const title = text(source?.title) || '模块';
|
|
463
|
+
const summary = text(source?.summary);
|
|
464
|
+
const faq = asRows(source?.faq);
|
|
465
|
+
const install = text(source?.install) === ''
|
|
466
|
+
? '在市场「已安装」里装或卸。装完在当前项目的面板里勾选这个模块,卡片才会出现;卸掉后卡片消失。'
|
|
467
|
+
: source?.install;
|
|
468
|
+
const shots = shotsOf(source);
|
|
469
|
+
const shotLines = shots.length === 0
|
|
470
|
+
? []
|
|
471
|
+
: [
|
|
472
|
+
'## 卡片长这样',
|
|
473
|
+
'',
|
|
474
|
+
...shots.flatMap((one) => [
|
|
475
|
+
...(one.caption === '' ? [] : [`**${one.caption}**`, '']),
|
|
476
|
+
'```text',
|
|
477
|
+
one.drawing,
|
|
478
|
+
'```',
|
|
479
|
+
'',
|
|
480
|
+
]),
|
|
481
|
+
];
|
|
482
|
+
const faqLines = faq.length === 0
|
|
483
|
+
? []
|
|
484
|
+
: [
|
|
485
|
+
'## 常见问题',
|
|
486
|
+
'',
|
|
487
|
+
...faq.flatMap((one) => [`### ${text(one?.q)}`, '', text(one?.a), '']),
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
return [
|
|
491
|
+
`# ${title}`,
|
|
492
|
+
'',
|
|
493
|
+
summary,
|
|
494
|
+
'',
|
|
495
|
+
...shotLines,
|
|
496
|
+
...sectionOf('你会看到什么', source?.see),
|
|
497
|
+
...sectionOf('你能做什么', source?.do),
|
|
498
|
+
...sectionOf('agent 能替你做什么', source?.agent),
|
|
499
|
+
...sectionOf('数据从哪来', source?.data),
|
|
500
|
+
...sectionOf('装和卸', install),
|
|
501
|
+
...sectionOf('出问题先看', source?.trouble),
|
|
502
|
+
...faqLines,
|
|
503
|
+
].join('\n');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function readUserSource(root) {
|
|
507
|
+
const file = join(root, README_SOURCE_FILE);
|
|
508
|
+
if (!existsSync(file)) return undefined;
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
511
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error(`✖ ${README_SOURCE_FILE} 不是合法 JSON:${error.message}`);
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function writeUserSource(root, value) {
|
|
519
|
+
writeFileSync(join(root, README_SOURCE_FILE), `${JSON.stringify(value, null, 2)}\n`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function manifestTitleOf(manifest) {
|
|
523
|
+
const title = manifest?.dsh?.workbench?.title;
|
|
524
|
+
if (typeof title === 'string' && title.trim() !== '') return title.trim();
|
|
525
|
+
if (title !== null && typeof title === 'object') {
|
|
526
|
+
const zh = text(title.zh);
|
|
527
|
+
if (zh !== '') return zh;
|
|
528
|
+
const en = text(title.en);
|
|
529
|
+
if (en !== '') return en;
|
|
530
|
+
}
|
|
531
|
+
return text(manifest?.name);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function userSourceSkeleton(manifest, design) {
|
|
535
|
+
return {
|
|
536
|
+
title: manifestTitleOf(manifest) || text(design?.name) || '模块',
|
|
537
|
+
summary: text(design?.oneLine) || text(manifest?.description),
|
|
538
|
+
shots: [
|
|
539
|
+
{
|
|
540
|
+
caption: '(可选:图注,比如「卡片」「设置面」「审批」)',
|
|
541
|
+
ascii: '(待填:一张卡片布局草图,用界面真实文案,按屏幕从上到下的顺序画)',
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
see: ['(待填:用户在卡片上看到的第一样东西)'],
|
|
545
|
+
do: ['(待填:用户自己能点的一个按钮或能改的一项设置)'],
|
|
546
|
+
agent: ['(待填:agent 能替用户做的一件事;写之前要不要批准)'],
|
|
547
|
+
data: ['(待填:数据从哪来、用谁的登录、读还是写)'],
|
|
548
|
+
install: '',
|
|
549
|
+
trouble: ['(待填:出问题时用户看到什么、下一步点哪)'],
|
|
550
|
+
faq: [],
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const USER_COPY_FIELDS = ['title', 'summary', 'see', 'do', 'agent', 'data', 'trouble'];
|
|
555
|
+
const USER_COPY_FORBIDDEN = [
|
|
556
|
+
['反引号 / 代码片段', /`/],
|
|
557
|
+
['代码块', /```/],
|
|
558
|
+
['源码路径', /\bsrc\//],
|
|
559
|
+
['设计底稿文件名', /\bdesign\.json\b/],
|
|
560
|
+
['文案底稿文件名', /\breadme\.json\b/],
|
|
561
|
+
['工具链命令', /\bdsh-wb-module\b/],
|
|
562
|
+
['运行时名', /\bopencli\b/],
|
|
563
|
+
['内部错误码', /\bauth-required\b|\bcli-[a-z-]+\b/],
|
|
564
|
+
['客户端实现名', /\bapproval\.render\b/],
|
|
565
|
+
['平台内部字段', /\bglobal_key\b/],
|
|
566
|
+
];
|
|
567
|
+
|
|
568
|
+
const BULLET_LIMIT = 70;
|
|
569
|
+
|
|
570
|
+
function copyTextsOf(value) {
|
|
571
|
+
if (Array.isArray(value)) return value.map(text).filter((one) => one !== '');
|
|
572
|
+
const one = text(value);
|
|
573
|
+
return one === '' ? [] : [one];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export function userCopyProblemsOf(source) {
|
|
577
|
+
const problems = [];
|
|
578
|
+
if (source === undefined) return [`没有 ${README_SOURCE_FILE} —— 先跑 dsh-wb-module design init 或 design migrate`];
|
|
579
|
+
for (const field of USER_COPY_FIELDS) {
|
|
580
|
+
const values = copyTextsOf(source?.[field]);
|
|
581
|
+
if (values.length === 0) problems.push(`${field} 是空的 —— README 的「${field}」还没有用户文案`);
|
|
582
|
+
for (const value of values) {
|
|
583
|
+
if (value.includes('(待填')) problems.push(`${field} 还留着「(待填)」`);
|
|
584
|
+
if (Array.isArray(source?.[field]) && value.length > BULLET_LIMIT) {
|
|
585
|
+
problems.push(`${field} 有一条 ${String(value.length)} 字 —— 超过 ${String(BULLET_LIMIT)} 字就拆成两条,或者把细节放进图里`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const shots = asRows(source?.shots);
|
|
590
|
+
if (shots.length === 0) problems.push('shots 是空的 —— 至少画一张卡片布局图');
|
|
591
|
+
for (const [index, one] of shots.entries()) {
|
|
592
|
+
if (text(one?.ascii).includes('(待填')) problems.push(`shots[${index}] 还留着「(待填)」`);
|
|
593
|
+
}
|
|
594
|
+
for (const field of ['title', 'summary', 'see', 'do', 'agent', 'data', 'install', 'trouble']) {
|
|
595
|
+
for (const value of copyTextsOf(source?.[field])) {
|
|
596
|
+
for (const [label, pattern] of USER_COPY_FORBIDDEN) {
|
|
597
|
+
if (pattern.test(value)) problems.push(`${field} 里有${label} —— 这段只给终端用户看`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const [index, one] of asRows(source?.faq).entries()) {
|
|
602
|
+
if (text(one?.q) === '') problems.push(`faq[${index}].q 是空的`);
|
|
603
|
+
if (text(one?.a) === '') problems.push(`faq[${index}].a 是空的`);
|
|
604
|
+
for (const [label, pattern] of USER_COPY_FORBIDDEN) {
|
|
605
|
+
if (pattern.test(text(one?.q)) || pattern.test(text(one?.a))) {
|
|
606
|
+
problems.push(`faq[${index}] 里有${label} —— 这段只给终端用户看`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return problems;
|
|
611
|
+
}
|
|
612
|
+
|
|
433
613
|
function regionOf(readme) {
|
|
434
614
|
const start = readme.indexOf(START_MARK);
|
|
435
615
|
const end = readme.indexOf(END_MARK);
|
|
@@ -437,11 +617,11 @@ function regionOf(readme) {
|
|
|
437
617
|
return { start, end };
|
|
438
618
|
}
|
|
439
619
|
|
|
440
|
-
export function
|
|
441
|
-
const file = join(root,
|
|
620
|
+
export function applyToDeveloping(root, markdown) {
|
|
621
|
+
const file = join(root, DEVELOPING_FILE);
|
|
442
622
|
const block = `${START_MARK}\n${markdown}\n${END_MARK}`;
|
|
443
623
|
if (!existsSync(file)) {
|
|
444
|
-
writeFileSync(file, `# ${text(manifestOf(root).name) || '模块'}
|
|
624
|
+
writeFileSync(file, `# ${text(manifestOf(root).name) || '模块'} · 作者文档\n\n${block}\n`);
|
|
445
625
|
return 'created';
|
|
446
626
|
}
|
|
447
627
|
const text0 = readFileSync(file, 'utf8');
|
|
@@ -464,6 +644,15 @@ export function applyToReadme(root, markdown) {
|
|
|
464
644
|
return 'inserted';
|
|
465
645
|
}
|
|
466
646
|
|
|
647
|
+
export function writeUserReadme(root, source) {
|
|
648
|
+
const file = join(root, 'README.md');
|
|
649
|
+
const next = renderUserReadme(source);
|
|
650
|
+
const before = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
651
|
+
if (before === next) return 'same';
|
|
652
|
+
writeFileSync(file, next);
|
|
653
|
+
return before === '' ? 'created' : 'updated';
|
|
654
|
+
}
|
|
655
|
+
|
|
467
656
|
function manifestOf(root) {
|
|
468
657
|
try {
|
|
469
658
|
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
@@ -484,9 +673,13 @@ function checkDesign(root) {
|
|
|
484
673
|
const acceptance = asRows(design.acceptance);
|
|
485
674
|
const accepted = acceptance.filter((one) => one?.checked === true).length;
|
|
486
675
|
|
|
487
|
-
const
|
|
488
|
-
const at = regionOf(
|
|
489
|
-
const fresh = at === undefined ? false :
|
|
676
|
+
const developing = existsSync(join(root, DEVELOPING_FILE)) ? readFileSync(join(root, DEVELOPING_FILE), 'utf8') : '';
|
|
677
|
+
const at = regionOf(developing);
|
|
678
|
+
const fresh = at === undefined ? false : developing.slice(at.start, at.end) === `${START_MARK}\n${renderDesign(design)}\n`;
|
|
679
|
+
const source = readUserSource(root);
|
|
680
|
+
const copyProblems = userCopyProblemsOf(source);
|
|
681
|
+
const user = existsSync(join(root, 'README.md')) ? readFileSync(join(root, 'README.md'), 'utf8') : '';
|
|
682
|
+
const userFresh = source !== undefined && user === renderUserReadme(source);
|
|
490
683
|
|
|
491
684
|
console.log(`design.json:面板 ${rows('panel')} 行 · 状态 ${STATE_KEYS.length} 态 · 配置 ${rows('config')} 项 · 能力 ${rows('capabilities')} 条 · 联动 ${rows('linkage')} 行 · 验收 ${accepted}/${acceptance.length}`);
|
|
492
685
|
for (const one of warnings) console.log(`⚠ ${one}`);
|
|
@@ -494,13 +687,22 @@ function checkDesign(root) {
|
|
|
494
687
|
console.error(`✖ 还空着 ${cells.length} 格("空着的格就是还没定的事",别带着空格往下做):`);
|
|
495
688
|
for (const one of cells) console.error(` · ${one}`);
|
|
496
689
|
}
|
|
497
|
-
if (
|
|
498
|
-
console.error(
|
|
690
|
+
if (developing === '') {
|
|
691
|
+
console.error(`✖ 没有 ${DEVELOPING_FILE} —— 旧模块先跑 dsh-wb-module design migrate;新模块跑 design init`);
|
|
692
|
+
} else if (!fresh) {
|
|
693
|
+
console.error(`✖ ${DEVELOPING_FILE} 里那一段落后于 design.json(或者没有标记)—— 跑 dsh-wb-module design render`);
|
|
499
694
|
}
|
|
500
|
-
if (
|
|
501
|
-
console.
|
|
695
|
+
if (copyProblems.length > 0) {
|
|
696
|
+
console.error(`✖ ${README_SOURCE_FILE} 还不能生成终端用户 README:`);
|
|
697
|
+
for (const one of copyProblems) console.error(` · ${one}`);
|
|
502
698
|
}
|
|
503
|
-
|
|
699
|
+
if (!userFresh) {
|
|
700
|
+
console.error(`✖ README.md 落后于 ${README_SOURCE_FILE}(或还没生成用户说明)—— 跑 dsh-wb-module design render`);
|
|
701
|
+
}
|
|
702
|
+
if (cells.length === 0 && fresh && copyProblems.length === 0 && userFresh) {
|
|
703
|
+
console.log(`✔ 三张表填全了,README 与 ${DEVELOPING_FILE} 都是最新的${accepted === acceptance.length ? ',验收也打完了' : `;验收还差 ${acceptance.length - accepted} 条(自己在真卡片上逐条走通后跑 design accept)`}`);
|
|
704
|
+
}
|
|
705
|
+
process.exit(cells.length === 0 && fresh && copyProblems.length === 0 && userFresh ? 0 : 1);
|
|
504
706
|
}
|
|
505
707
|
|
|
506
708
|
export async function design({ root, args = [] }) {
|
|
@@ -511,12 +713,16 @@ export async function design({ root, args = [] }) {
|
|
|
511
713
|
console.error(`✖ ${DESIGN_FILE} 已经在了;要重来先删掉它`);
|
|
512
714
|
process.exit(1);
|
|
513
715
|
}
|
|
514
|
-
const
|
|
716
|
+
const manifest = manifestOf(root);
|
|
717
|
+
const design0 = skeleton(manifest);
|
|
515
718
|
writeDesign(root, design0);
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
719
|
+
const source = readUserSource(root) ?? userSourceSkeleton(manifest, design0);
|
|
720
|
+
writeUserSource(root, source);
|
|
721
|
+
const how = applyToDeveloping(root, renderDesign(design0));
|
|
722
|
+
writeUserReadme(root, source);
|
|
723
|
+
console.log(`✔ 写好 ${DESIGN_FILE} 与 ${README_SOURCE_FILE}(工程底稿 + 用户文案骨架)`);
|
|
724
|
+
console.log(` ${DEVELOPING_FILE} 的那一段:${how === 'created' ? '新建' : how === 'inserted' ? '插进「需求底稿」那一节' : '已更新'}`);
|
|
725
|
+
console.log(' 接着把两边的空格填成"用户看得见的东西",然后 dsh-wb-module design check');
|
|
520
726
|
return;
|
|
521
727
|
}
|
|
522
728
|
|
|
@@ -526,8 +732,19 @@ export async function design({ root, args = [] }) {
|
|
|
526
732
|
console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
|
|
527
733
|
process.exit(1);
|
|
528
734
|
}
|
|
529
|
-
|
|
530
|
-
|
|
735
|
+
if (!existsSync(join(root, DEVELOPING_FILE))) {
|
|
736
|
+
console.error(`✖ 没有 ${DEVELOPING_FILE} —— 旧模块先跑 dsh-wb-module design migrate;新模块跑 design init`);
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
const source = readUserSource(root);
|
|
740
|
+
if (source === undefined) {
|
|
741
|
+
console.error(`✖ 没有 ${README_SOURCE_FILE} —— 旧模块先跑 dsh-wb-module design migrate;新模块跑 design init`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
const how = applyToDeveloping(root, renderDesign(design0));
|
|
745
|
+
const user = writeUserReadme(root, source);
|
|
746
|
+
console.log(`✔ ${DEVELOPING_FILE} 的那一段:${how === 'same' ? '本来就是最新的' : how === 'updated' ? '更新了' : how === 'inserted' ? '插进「需求底稿」那一节' : '已更新'}`);
|
|
747
|
+
console.log(` README.md:${user === 'same' ? '本来就是最新的' : user === 'created' ? '新建' : '重生成'}`);
|
|
531
748
|
return;
|
|
532
749
|
}
|
|
533
750
|
|
|
@@ -545,16 +762,43 @@ export async function design({ root, args = [] }) {
|
|
|
545
762
|
design0.acceptance = acceptance.map((one) => ({ ...one, checked: true }));
|
|
546
763
|
design0.acceptedAt = new Date().toISOString();
|
|
547
764
|
writeDesign(root, design0);
|
|
548
|
-
|
|
765
|
+
applyToDeveloping(root, renderDesign(design0));
|
|
766
|
+
const source = readUserSource(root);
|
|
767
|
+
if (source !== undefined) writeUserReadme(root, source);
|
|
549
768
|
console.log(`✔ ${acceptance.length} 条验收全部打勾(只在真的在真卡片上逐条走通过之后再跑这条)`);
|
|
550
769
|
return;
|
|
551
770
|
}
|
|
552
771
|
|
|
772
|
+
if (action === 'migrate') {
|
|
773
|
+
const design0 = readDesign(root);
|
|
774
|
+
if (design0 === undefined) {
|
|
775
|
+
console.error(`✖ 没有 ${DESIGN_FILE} —— 先跑 dsh-wb-module design init`);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
if (existsSync(join(root, DEVELOPING_FILE))) {
|
|
779
|
+
console.error(`✖ ${DEVELOPING_FILE} 已经在了;拆过的模块不用再 migrate`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
const readmeFile = join(root, 'README.md');
|
|
783
|
+
if (existsSync(readmeFile)) {
|
|
784
|
+
copyFileSync(readmeFile, join(root, DEVELOPING_FILE));
|
|
785
|
+
console.log(`✔ 旧 README 已保存为 ${DEVELOPING_FILE}`);
|
|
786
|
+
}
|
|
787
|
+
const source = readUserSource(root) ?? userSourceSkeleton(manifestOf(root), design0);
|
|
788
|
+
writeUserSource(root, source);
|
|
789
|
+
const how = applyToDeveloping(root, renderDesign(design0));
|
|
790
|
+
const user = writeUserReadme(root, source);
|
|
791
|
+
console.log(`✔ ${DEVELOPING_FILE} 的那一段:${how === 'same' ? '本来就是最新的' : how === 'updated' ? '更新了' : '已更新'}`);
|
|
792
|
+
console.log(`✔ ${README_SOURCE_FILE} 与 README.md 已生成(${user === 'created' ? '新建' : '重生成'})`);
|
|
793
|
+
console.log(` 接着把 ${README_SOURCE_FILE} 里的(待填)按终端用户视角填完,再 dsh-wb-module design check`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
553
797
|
if (action === 'check') {
|
|
554
798
|
checkDesign(root);
|
|
555
799
|
return;
|
|
556
800
|
}
|
|
557
801
|
|
|
558
|
-
console.error(`不认识的 design 动作:${action}(可用:init / check / render / accept)`);
|
|
802
|
+
console.error(`不认识的 design 动作:${action}(可用:init / migrate / check / render / accept)`);
|
|
559
803
|
process.exit(2);
|
|
560
804
|
}
|
package/src/init.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cpSync, existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { DESIGN_FILE,
|
|
2
|
+
import { DESIGN_FILE, applyToDeveloping, readDesign, readUserSource, renderDesign, writeUserReadme } from './design.mjs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -109,10 +109,14 @@ export async function init({ root, args = [] }) {
|
|
|
109
109
|
};
|
|
110
110
|
walk(target);
|
|
111
111
|
const declared = readDesign(target);
|
|
112
|
-
if (declared !== undefined)
|
|
112
|
+
if (declared !== undefined) {
|
|
113
|
+
applyToDeveloping(target, renderDesign(declared));
|
|
114
|
+
const source = readUserSource(target);
|
|
115
|
+
if (source !== undefined) writeUserReadme(target, source);
|
|
116
|
+
}
|
|
113
117
|
const tables = existsSync(join(target, DESIGN_FILE)) ? `${DESIGN_FILE} 里那份三张表` : '还没有三张表(dsh-wb-module design init 生成)';
|
|
114
118
|
console.log(`${target} 建好了:${unit} 模块骨架(模板 ${which};build / check / design / selfcheck 已接工具)`);
|
|
115
|
-
console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,README
|
|
119
|
+
console.log(` 需求底稿在 ${tables};空格用 dsh-wb-module design check 查,readme.json 出用户 README,design.json 出 DEVELOPING.md`);
|
|
116
120
|
console.log(` 模板默认带了 ${DSH_UI}@${dshUi.version}(${dshUi.source})—— 这是 dsh 原语的类型面 + external 声明两处;`);
|
|
117
121
|
console.log(' 确实用不到就连 devDependencies 与 dsh.client.external 两处一起删,判据不拦。');
|
|
118
122
|
}
|
package/src/release-journal.mjs
CHANGED
|
@@ -7,10 +7,12 @@ import { workbenchRootOf } from './release-util.mjs';
|
|
|
7
7
|
const FINGERPRINT_FILES = [
|
|
8
8
|
'package.json',
|
|
9
9
|
'design.json',
|
|
10
|
+
'readme.json',
|
|
10
11
|
'dist/host.js',
|
|
11
12
|
'client.js',
|
|
12
13
|
'cordis.patch.yml',
|
|
13
14
|
'README.md',
|
|
15
|
+
'DEVELOPING.md',
|
|
14
16
|
'CHANGELOG.md',
|
|
15
17
|
];
|
|
16
18
|
|
package/src/release-target.mjs
CHANGED
|
@@ -229,7 +229,7 @@ function gitInfoOf(root, manifest, args, exec) {
|
|
|
229
229
|
});
|
|
230
230
|
if (status.status !== 0) throw new ReleaseError(`读不了 git 状态:${tailOf(status.stderr)}`);
|
|
231
231
|
const prefix = packagePath === '.' ? '' : `${packagePath}/`;
|
|
232
|
-
const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json'];
|
|
232
|
+
const allowed = ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'DEVELOPING.md', 'CHANGELOG.md', 'design.json', 'readme.json'];
|
|
233
233
|
const dirty = status.stdout
|
|
234
234
|
.trim()
|
|
235
235
|
.split('\n')
|
package/src/release.mjs
CHANGED
|
@@ -139,7 +139,7 @@ function runGate(exec, root, options) {
|
|
|
139
139
|
|
|
140
140
|
function gitPathsOf(info) {
|
|
141
141
|
const prefix = info.packagePath === '.' ? '' : `${info.packagePath}/`;
|
|
142
|
-
return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'CHANGELOG.md', 'design.json']
|
|
142
|
+
return ['dist', 'client.js', 'cordis.patch.yml', 'package.json', 'README.md', 'DEVELOPING.md', 'CHANGELOG.md', 'design.json', 'readme.json']
|
|
143
143
|
.map((one) => `${prefix}${one}`)
|
|
144
144
|
.filter((one) => existsSync(join(info.toplevel, one)));
|
|
145
145
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# __NAME__ · agent 工作说明
|
|
2
2
|
|
|
3
|
-
工作台模块(单元 `__UNIT__
|
|
3
|
+
工作台模块(单元 `__UNIT__`)。终端用户说明在 `README.md`(文案源 `readme.json`);实现、数据源、设计底稿在 `DEVELOPING.md`;这份只说工具链怎么用。
|
|
4
4
|
|
|
5
5
|
## 干活循环
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
**全绿**才算这件活做完。怎么读红、常见红:`@dsh-workbench/core/skills/workbench-module-diy/REVIEW.md`。
|
|
12
12
|
3. `pnpm test` —— `src/**/__tests__/` 下、与改动同目录的 `*.test.ts`;新逻辑配对应用例,**全绿**才算完。
|
|
13
13
|
|
|
14
|
-
`pnpm selfcheck` = build + `design check` + smoke;`design.json` 里的(待填)填完才绿。
|
|
14
|
+
`pnpm selfcheck` = build + `design check` + smoke;`design.json` 与 `readme.json` 里的(待填)填完才绿。
|
|
15
15
|
|
|
16
16
|
## 工具链约定
|
|
17
17
|
|
|
@@ -21,5 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
## 先读
|
|
23
23
|
|
|
24
|
-
- `
|
|
24
|
+
- `readme.json`:终端用户文案源(标题、简介、会看到什么、能做什么、agent 能做什么、数据从哪来、排障、FAQ)。
|
|
25
|
+
- `README.md`:由 `readme.json` 生成,不要手改。
|
|
26
|
+
- `DEVELOPING.md`:实现、路由、判据、数据源与三张表。
|
|
25
27
|
- `@dsh-workbench/core/skills/workbench-module-diy/`:路由 / 工具 / 卡片 / 文案的写法与约定(随外壳出货)。
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# __NAME__ · 作者文档
|
|
2
|
+
|
|
3
|
+
终端用户看 [README.md](README.md)(文案源 `readme.json`);这份讲实现、路由、判据和设计底稿。
|
|
4
|
+
|
|
5
|
+
`__UNIT__` 模块:一个独立的 dsh 插件包,只认外壳的服务名,不认识外壳源码。
|
|
6
|
+
|
|
7
|
+
这份是 **唯一模板**(`--template diy`,也是默认)生成的骨架:一张**读当前项目目录的只读卡**,
|
|
8
|
+
数据从宿主侧**真读**一个目录算出来(`node:fs`),另带一条**能跑**的流程 / 长任务 / 真工具 /
|
|
9
|
+
动作通道样板;不是一个写死的样板。
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh-wb-module init <unit> --dir <目录> # 生成这份骨架(只有这一个模板)
|
|
13
|
+
dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
|
|
14
|
+
dsh-wb-module check # 十四条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 执行缝 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
|
|
15
|
+
dsh-wb-module check --only cards # 十四条可以单跑
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm build # 构建产物
|
|
22
|
+
pnpm typecheck # tsc --noEmit
|
|
23
|
+
pnpm test # vitest:路由真读数 + 卡片五态 + 长任务/工具契约
|
|
24
|
+
pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 真跑一趟流程 + 走 scripts/smoke.assert.mjs
|
|
25
|
+
pnpm check # dsh-wb-module check
|
|
26
|
+
pnpm lint # dsh-wb-module lint(`--fix` 就地改文件;`check` 永不动手)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 需求底稿(做之前填,做完自己逐条走一遍)
|
|
30
|
+
|
|
31
|
+
三张表既是**要问清的需求**,也是**最后的验收单** —— 表里每一格 agent 都要在真卡片上见过或点过。
|
|
32
|
+
**空着的格就是还没定的事**,别带着空格往下做:落点是同目录的 `design.json`
|
|
33
|
+
(`dsh-wb-module design check` 会把空格一格一格列出来),**下面标了 start/end 的那一段就是由它生成的**
|
|
34
|
+
—— 要改三张表就改 `design.json`,再 `dsh-wb-module design render`;手改那一段会被 next 一次 check 判红。
|
|
35
|
+
所以下次他说"改一下"不用从头问。
|
|
36
|
+
|
|
37
|
+
**非生成段(这一节与下文)不会被 render 覆盖**:需求变了要自己改这里,`design render` 只管标记之间那一段。
|
|
38
|
+
|
|
39
|
+
怎么问、怎么填:指引 `SKILL.md` 的七步 · `DESIGN.md`(判域 / 两条分工 / 三张表)·
|
|
40
|
+
`STEPS.md`(每步问什么、什么时候算做完)· `PANEL.md` · `TOOLS.md` · `LINKAGE.md`。
|
|
41
|
+
|
|
42
|
+
<!-- dsh-wb:design:start -->
|
|
43
|
+
(这一段由 `design.json` 生成:改需求改 `design.json`,再 `dsh-wb-module design render`;手改会被下一次 check 判红。)
|
|
44
|
+
<!-- dsh-wb:design:end -->
|
|
45
|
+
|
|
46
|
+
## 生成出来的形状
|
|
47
|
+
|
|
48
|
+
**分层多文件**,不是单文件:数字越小越底层,上层只许 import 下层的(`arch.config.mjs` 那条
|
|
49
|
+
`arch` 判据盯着,`dsh-wb-module check` 会跑)。目录**按需要出现**:这份有状态逻辑,所以有 `model/`。
|
|
50
|
+
宿主侧的辅助实现放 `src/host/`(`moduleArch()` 默认就认这一层;
|
|
51
|
+
`src/host.ts` 仍是对外的宿主面,两个面只认 `dsh.workbenchFaces` 里那两个文件)。
|
|
52
|
+
|
|
53
|
+
| 文件 | 是什么 | 层 |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `package.json` | 自述清单:`dsh.workbench.unit` = `__UNIT__`、两个面(`src/host.ts` / `src/client.tsx`)、八个脚本、peer 下限 | — |
|
|
56
|
+
| `arch.config.mjs` | 本模块的架构方言:`moduleArch()`(`src/host/**` 默认在层序里)—— 层序表 / 两个运行时 / 阈值只有 `@dsh-workbench/module-tools/arch` 那一份,随工具链同代 | — |
|
|
57
|
+
| `eslint.config.mjs` | 本模块的**语法卫生方言**,同样两行:`moduleLint()` —— 规则表只有 `@dsh-workbench/module-tools/lint` 那一份(`console` / `as any` / hooks 那几条 / 手搓颜色与 `!important`);要开口子就 `moduleLint({ rules })` | — |
|
|
58
|
+
| `cordis.patch.yml` | 插进 dsh 组合的补丁 | — |
|
|
59
|
+
| `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**,上面那段由它生成;`dsh-wb-module design check/render/accept` 读写它) | — |
|
|
60
|
+
| `src/model/overview.ts` | 纯逻辑:类型 + 五态状态机 + `formatBytes`。两个运行时都读得到,不碰 react / kit / dsh / `node:*` | 1 |
|
|
61
|
+
| `src/model/__tests__/overview.test.ts` | 模型层的配对测试(有 `src/model/**` 就必须有 —— `arch` 的 M08) | 99 |
|
|
62
|
+
| `src/model/flow.ts` | 样板流程的纯逻辑:`FlowDefinition` + 五态标题 / 是否还在跑;不碰宿主 | 1 |
|
|
63
|
+
| `src/model/__tests__/flow.test.ts` | 流程模型的配对测试 | 99 |
|
|
64
|
+
| `src/model/notes.ts` | 笔记的纯逻辑:校验 / 读回 / 最多 20 条 | 1 |
|
|
65
|
+
| `src/model/__tests__/notes.test.ts` | 笔记模型的配对测试 | 99 |
|
|
66
|
+
| `src/model/prefs.ts` | 配置的纯逻辑:按工作区存 `limit`(1..50),坏值回默认 | 1 |
|
|
67
|
+
| `src/model/__tests__/prefs.test.ts` | 配置模型的配对测试 | 99 |
|
|
68
|
+
| `src/messages.ts` | 卡片那几句文案(zh / en),键带 `<unit>.` 前缀 | 1 |
|
|
69
|
+
| `src/declared-tools.ts` | 清单里工具名的生成物(`dsh-wb-module sync` 写;代码只引 `DECLARED_TOOLS.run.name` / `DECLARED_TOOLS.addNote.name`,不写字面量) | 1 |
|
|
70
|
+
| `src/components/OverviewCard.tsx` | 卡片:`Card` 壳 + 五态 + 最大的几个文件 + 长任务行 + 笔记(写)/「显示几个」配置 | 2 |
|
|
71
|
+
| `src/components/OverviewBody.tsx` | 五态各自那一屏(待机 / 加载 / 就绪 / 空 / 出错) | 2 |
|
|
72
|
+
| `src/components/OverviewBody.module.css` | 就绪态「最大的几个文件」列表的布局 | 2 |
|
|
73
|
+
| `src/components/RunSection.tsx` | 长任务那一块:跑流程的钮 + `LongTaskRow`(五态 / 停止) | 2 |
|
|
74
|
+
| `src/components/NotesSection.tsx` | 笔记那一块:输入 + 存一条 + 列表 | 2 |
|
|
75
|
+
| `src/components/PrefsSection.tsx` | 「最大的几个文件」配置(`SelectField`,按工作区存) | 2 |
|
|
76
|
+
| `src/components/OverviewCard.module.css` | 这个组件自己的样式(CSS Modules,构建时类名按路径哈希)—— **样式跟组件同目录** | 2 |
|
|
77
|
+
| `src/components/__tests__/OverviewCard.test.ts` | 这张卡五态各画得出来 + 写失败留在卡上(测试住同目录的 `__tests__/`);流程 / 笔记 / 配置的接线由 `plugin.test.ts` 与 smoke 覆盖 | 99 |
|
|
78
|
+
| `src/host.ts` | 宿主面:接线 + 三条只读路由(`overview` / `notes` / `prefs`)+ 挂工具 / 长任务 / 动作通道 / `prefs` / 审批口径 | 10 |
|
|
79
|
+
| `src/host/overview.ts` | 宿主侧内部实现:`readOverview()`(真读目录)+ 路由回答 | 10 |
|
|
80
|
+
| `src/host/sample.ts` | 宿主侧内部实现:样板流程的动作、长任务、只读工具 | 10 |
|
|
81
|
+
| `src/host/notes.ts` | 宿主侧内部实现:笔记读写(`deps.store`)+ 写工具 + 动作通道 | 10 |
|
|
82
|
+
| `src/client.tsx` | 浏览器面:**只剩面** —— 注册 + 把 props 交给 `components/OverviewCard` | 10 |
|
|
83
|
+
| `src/__tests__/plugin.test.ts` | 面注册 + 路由真读 + 工具注册 + 流程真跑 + 笔记/配置/审批 | 99 |
|
|
84
|
+
| `scripts/smoke.mjs` | 接线自检:卡片进没进外壳、工具注册、流程真跑、笔记真写、配置存得住;真读断言在 `smoke.assert.mjs` | — |
|
|
85
|
+
| `scripts/smoke.assert.mjs` | **这个模块自己的**真读断言:换数据源时改这份,别删 smoke 里的调用 | — |
|
|
86
|
+
| `CHANGELOG.md` | 版本记录(`dsh-wb-module release` 追加;`package.json.files` 里带它) | — |
|
|
87
|
+
| `tsconfig.json` / `vitest.config.ts` / `types/globals.d.ts` / `types/styles.d.ts` | 经典 JSX 运行时(`jsxFactory: h`)、vitest 的别名、浏览器半侧的运行时垫片、`*.module.css` 的类名表声明 | — |
|
|
88
|
+
| `DEVELOPING.md` | 就是本文(作者文档;用户说明在 `README.md`) | — |
|
|
89
|
+
|
|
90
|
+
## 长任务、工具、写通道与配置(样板)
|
|
91
|
+
|
|
92
|
+
这份骨架故意带了一组**可跑**的能力,示范五件事:
|
|
93
|
+
|
|
94
|
+
- **宿主面接线**:`name` / `inject` / `apply` 用 `@dsh-workbench/shared/host` 的 `defineUnitHostPlugin`
|
|
95
|
+
一次给齐;写工具的审批口径用 `writeApprovalOf`;`recordOf` / `textOf` 也从那里取,别每个文件抄一份。
|
|
96
|
+
- **只读工具**:`registerTools()` 是**调用时才注册**、返回的是清理函数;工具名必须和 `package.json` 的
|
|
97
|
+
`dsh.workbench.tools` 一字不差。`__UNIT___run` 起一趟流程,传 `runId` 则只看同一趟。
|
|
98
|
+
- **长任务**:`deps.longTasks.create()` 交回一张注册表,`run` 里调 `deps.flow.run()` 并把
|
|
99
|
+
`onStep` 交给 `longTaskStepOf`(shared/host 的默认映射);卡片用 `useLongTask` 读、`LongTaskRow` 画。
|
|
100
|
+
- **写通道 + 审批**:`__UNIT___add_note` 在清单里写 `writes: true`,`mount()` 返回 `needsApproval`(这次要点头)
|
|
101
|
+
与 `approvalDetail`(审批屏上那句"会写什么");agent 调用前壳会拦一道。卡片那颗钮走动作通道
|
|
102
|
+
`<unit>-add-note`(点击即确认,不审批),与写工具共用同一份 `deps.store` 数据;网络 / 校验失败
|
|
103
|
+
留在按钮下面那一行,不静默。
|
|
104
|
+
- **配置(prefs)**:`mount()` 返回 `prefs: { resource, handle }`,按工作区把"显示几个文件"存进
|
|
105
|
+
`deps.store`;卡片用 `useHostWrite({ channel: 'prefs' })` 写、用 `<unit>/prefs` 读。
|
|
106
|
+
- **动作通道**:用户直接点的单步动作走 `<unit>-…` 资源;与工具共用实现,不另写一份。
|
|
107
|
+
|
|
108
|
+
换成你自己的能力时,五处一起改:`src/model/*` 的纯逻辑、`src/host/*` 的实现、
|
|
109
|
+
`src/host.ts` 的接线(路由 / 工具 / 动作 / prefs / 审批)、`src/components/OverviewCard.tsx` 的展示、
|
|
110
|
+
`design.json` 的三张表与能力行。
|
|
111
|
+
|
|
112
|
+
## 数据是在哪算出来的
|
|
113
|
+
|
|
114
|
+
宿主侧,`src/host/overview.ts` 的 `readOverview(path, limit)`:
|
|
115
|
+
|
|
116
|
+
* `readdirSync(path, { withFileTypes: true })` 数出**目录数**;
|
|
117
|
+
* 每个普通文件 `statSync` 一次,累加**总字节**、记下 `name / bytes / mtime`(mtime 是 ISO 串);
|
|
118
|
+
* 按字节降序(同字节按名字)排,取前 `limit` 个当**最大的几个文件**。
|
|
119
|
+
|
|
120
|
+
已知的取舍(不是 bug,是这份骨架刻意保持简单的地方):
|
|
121
|
+
|
|
122
|
+
* **符号链接不计数**:既不算文件也不算目录(想要跟随就自己换成 `statSync` + `lstatSync` 判断);
|
|
123
|
+
* **同步读**:模板优先让代码一眼看得懂;真接大目录时改成 `node:fs/promises` 版本更合适;
|
|
124
|
+
* 只读**直接子项**,不递归。
|
|
125
|
+
|
|
126
|
+
## 那条只读路由
|
|
127
|
+
|
|
128
|
+
键是 `<unit>/overview`(`__UNIT__` 就是本模块的单元 id)——**必须住在自己那一格的命名空间里**,
|
|
129
|
+
外壳装 / 升 / 卸时要能点名到它;写成别的名字注册当场就被判红。
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
GET /api/__UNIT__/overview?path=<绝对路径>&limit=<1..50>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
查询参数:
|
|
136
|
+
|
|
137
|
+
| 参数 | 缺省 | 说明 |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `path` | 无(必填) | 要读的目录。卡片把 `props.world.current.workspace.path` 塞进来;缺了不回数据,直接回 `no-path` |
|
|
140
|
+
| `limit` | `5` | 返回几个"最大的文件",夹在 `1..50` 之间 |
|
|
141
|
+
|
|
142
|
+
成功:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{ "ok": true, "path": "/…", "files": 128, "dirs": 12, "bytes": 3145728,
|
|
146
|
+
"largest": [{ "name": "client.js", "bytes": 182400, "mtime": "2026-01-05T09:12:44.000Z" }] }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
读不到(路径不存在 / 没权限 / 不是目录):**HTTP 200 + `ok:false`**,错误码 `overview-unreadable`。
|
|
150
|
+
压根没给 `path`:同样 `ok:false`,错误码 `no-path` —— 别拿宿主进程的工作目录当默认值:
|
|
151
|
+
那会让"我没说要看哪儿"变成"屏幕上是 dsh 服务启动目录",两个都不像用户想要的东西。
|
|
152
|
+
之所以不让它抛:抛出去会被外壳的传输层转成 HTTP 500,卡片只剩一句"宿主返回 HTTP 500",
|
|
153
|
+
用户看不到是哪条路径出了什么事。
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "ok": false, "error": { "code": "overview-unreadable", "message": "ENOENT: no such file or directory, scandir '/…'" } }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
卡片侧读数的 hook 是 `kit.hooks.useApiResource`(`route` 就是 `__UNIT__/overview`)。
|
|
160
|
+
注意它的 `value` 是**整个响应对象**(成功时形如 `{ ok: true, ...payload }`),不是 `payload` 本身。
|
|
161
|
+
|
|
162
|
+
## 卡片画哪五态
|
|
163
|
+
|
|
164
|
+
`src/model/overview.ts` 的 `overviewStateOf({ phase, value, error })` 是一条纯函数,
|
|
165
|
+
每个分支各对应一屏(`src/model/__tests__/overview.test.ts` 直接断言这条函数,
|
|
166
|
+
`src/components/__tests__/OverviewCard.test.ts` 再把每一屏画一遍):
|
|
167
|
+
|
|
168
|
+
| 态 | 什么时候 | 画什么 |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| 待机 | 没有选中工作区(`path === ''`),请求压根不发(`useApiResource(…, { enabled: false })`,`phase` 停在 `idle`) | `CenteredState`:先打开一个项目 |
|
|
171
|
+
| 加载 | 还没有值(首读),或 `phase === 'error'` 之外什么都没来 | `CenteredState`:正在读目录 |
|
|
172
|
+
| 就绪 | 有值,且文件数 + 目录数 > 0 | `StateBlock`:路径 + 「N 个文件 · M 个目录 · 总字节」+ 最大的几个文件列表 |
|
|
173
|
+
| 就绪但空 | 有值,但文件数 = 目录数 = 0 | `EmptyState`:这个目录是空的 |
|
|
174
|
+
| 出错 | `phase === 'error'` | `CenteredState`:读不到这个目录 + 宿主的原话 |
|
|
175
|
+
|
|
176
|
+
刷新时 `useApiResource` 会把 `phase` 打成 `loading` **同时保留上一次的 `value`** ——
|
|
177
|
+
这时刻意继续画旧数据(右上角的转圈由 `Card` 的 `refreshing` 表示),不退回骨架:
|
|
178
|
+
一次刷新不该让整张卡闪一下。
|
|
179
|
+
|
|
180
|
+
## 开发与发布
|
|
181
|
+
|
|
182
|
+
开发命令见上面的六个脚本;dsh / 工作台的模块约束见工作台模块指引(随外壳出货);agent 的工作说明见 `AGENTS.md`。
|
|
183
|
+
|
|
184
|
+
发布(维护者确认之后):`dsh-wb-module release --target team|public|tarball|git`;
|
|
185
|
+
`--dry-run` 只预览,`--verify` 只核已发布的包。
|
|
@@ -1,183 +1,5 @@
|
|
|
1
1
|
# __NAME__
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
__DESC__
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
数据从宿主侧**真读**一个目录算出来(`node:fs`),另带一条**能跑**的流程 / 长任务 / 真工具 /
|
|
7
|
-
动作通道样板;不是一个写死的样板。
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
dsh-wb-module init <unit> --dir <目录> # 生成这份骨架(只有这一个模板)
|
|
11
|
-
dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
|
|
12
|
-
dsh-wb-module check # 十四条判据:契约 / 声明协议 / 产物边界 / 源码边界 / 执行缝 / 架构 / dsh 原语 / kit 面 / 卡片渲染 / 独立构建 / 内部标识 / 样式归属 / 不指外壳 / 代码卫生(eslint)
|
|
13
|
-
dsh-wb-module check --only cards # 十四条可以单跑
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
包自带的六个脚本(`build` / `typecheck` / `test` / `selfcheck` 是 `check` 的契约判据要求的那四个;`check` 跑全部判据,`lint` 只跑源码卫生那一条):
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pnpm build # 构建产物
|
|
20
|
-
pnpm typecheck # tsc --noEmit
|
|
21
|
-
pnpm test # vitest:路由真读数 + 卡片五态 + 长任务/工具契约
|
|
22
|
-
pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 真跑一趟流程 + 走 scripts/smoke.assert.mjs
|
|
23
|
-
pnpm check # dsh-wb-module check
|
|
24
|
-
pnpm lint # dsh-wb-module lint(`--fix` 就地改文件;`check` 永不动手)
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## 需求底稿(做之前填,做完自己逐条走一遍)
|
|
28
|
-
|
|
29
|
-
三张表既是**要问清的需求**,也是**最后的验收单** —— 表里每一格 agent 都要在真卡片上见过或点过。
|
|
30
|
-
**空着的格就是还没定的事**,别带着空格往下做:落点是同目录的 `design.json`
|
|
31
|
-
(`dsh-wb-module design check` 会把空格一格一格列出来),**下面标了 start/end 的那一段就是由它生成的**
|
|
32
|
-
—— 要改三张表就改 `design.json`,再 `dsh-wb-module design render`;手改那一段会被 next 一次 check 判红。
|
|
33
|
-
所以下次他说"改一下"不用从头问。
|
|
34
|
-
|
|
35
|
-
**非生成段(这一节与下文)不会被 render 覆盖**:需求变了要自己改这里,`design render` 只管标记之间那一段。
|
|
36
|
-
|
|
37
|
-
怎么问、怎么填:指引 `SKILL.md` 的七步 · `DESIGN.md`(判域 / 两条分工 / 三张表)·
|
|
38
|
-
`STEPS.md`(每步问什么、什么时候算做完)· `PANEL.md` · `TOOLS.md` · `LINKAGE.md`。
|
|
39
|
-
|
|
40
|
-
<!-- dsh-wb:design:start -->
|
|
41
|
-
(这一段由 `design.json` 生成:改需求改 `design.json`,再 `dsh-wb-module design render`;手改会被下一次 check 判红。)
|
|
42
|
-
<!-- dsh-wb:design:end -->
|
|
43
|
-
|
|
44
|
-
## 生成出来的形状
|
|
45
|
-
|
|
46
|
-
**分层多文件**,不是单文件:数字越小越底层,上层只许 import 下层的(`arch.config.mjs` 那条
|
|
47
|
-
`arch` 判据盯着,`dsh-wb-module check` 会跑)。目录**按需要出现**:这份有状态逻辑,所以有 `model/`。
|
|
48
|
-
宿主侧的辅助实现放 `src/host/`(`moduleArch()` 默认就认这一层;
|
|
49
|
-
`src/host.ts` 仍是对外的宿主面,两个面只认 `dsh.workbenchFaces` 里那两个文件)。
|
|
50
|
-
|
|
51
|
-
| 文件 | 是什么 | 层 |
|
|
52
|
-
|---|---|---|
|
|
53
|
-
| `package.json` | 自述清单:`dsh.workbench.unit` = `__UNIT__`、两个面(`src/host.ts` / `src/client.tsx`)、八个脚本、peer 下限 | — |
|
|
54
|
-
| `arch.config.mjs` | 本模块的架构方言:`moduleArch()`(`src/host/**` 默认在层序里)—— 层序表 / 两个运行时 / 阈值只有 `@dsh-workbench/module-tools/arch` 那一份,随工具链同代 | — |
|
|
55
|
-
| `eslint.config.mjs` | 本模块的**语法卫生方言**,同样两行:`moduleLint()` —— 规则表只有 `@dsh-workbench/module-tools/lint` 那一份(`console` / `as any` / hooks 那几条 / 手搓颜色与 `!important`);要开口子就 `moduleLint({ rules })` | — |
|
|
56
|
-
| `cordis.patch.yml` | 插进 dsh 组合的补丁 | — |
|
|
57
|
-
| `design.json` | 三张表 + 状态矩阵 + 数据源记录 + 验收清单(**唯一真相**,上面那段由它生成;`dsh-wb-module design check/render/accept` 读写它) | — |
|
|
58
|
-
| `src/model/overview.ts` | 纯逻辑:类型 + 五态状态机 + `formatBytes`。两个运行时都读得到,不碰 react / kit / dsh / `node:*` | 1 |
|
|
59
|
-
| `src/model/__tests__/overview.test.ts` | 模型层的配对测试(有 `src/model/**` 就必须有 —— `arch` 的 M08) | 99 |
|
|
60
|
-
| `src/model/flow.ts` | 样板流程的纯逻辑:`FlowDefinition` + 五态标题 / 是否还在跑;不碰宿主 | 1 |
|
|
61
|
-
| `src/model/__tests__/flow.test.ts` | 流程模型的配对测试 | 99 |
|
|
62
|
-
| `src/model/notes.ts` | 笔记的纯逻辑:校验 / 读回 / 最多 20 条 | 1 |
|
|
63
|
-
| `src/model/__tests__/notes.test.ts` | 笔记模型的配对测试 | 99 |
|
|
64
|
-
| `src/model/prefs.ts` | 配置的纯逻辑:按工作区存 `limit`(1..50),坏值回默认 | 1 |
|
|
65
|
-
| `src/model/__tests__/prefs.test.ts` | 配置模型的配对测试 | 99 |
|
|
66
|
-
| `src/messages.ts` | 卡片那几句文案(zh / en),键带 `<unit>.` 前缀 | 1 |
|
|
67
|
-
| `src/declared-tools.ts` | 清单里工具名的生成物(`dsh-wb-module sync` 写;代码只引 `DECLARED_TOOLS.run.name` / `DECLARED_TOOLS.addNote.name`,不写字面量) | 1 |
|
|
68
|
-
| `src/components/OverviewCard.tsx` | 卡片:`Card` 壳 + 五态 + 最大的几个文件 + 长任务行 + 笔记(写)/「显示几个」配置 | 2 |
|
|
69
|
-
| `src/components/OverviewBody.tsx` | 五态各自那一屏(待机 / 加载 / 就绪 / 空 / 出错) | 2 |
|
|
70
|
-
| `src/components/OverviewBody.module.css` | 就绪态「最大的几个文件」列表的布局 | 2 |
|
|
71
|
-
| `src/components/RunSection.tsx` | 长任务那一块:跑流程的钮 + `LongTaskRow`(五态 / 停止) | 2 |
|
|
72
|
-
| `src/components/NotesSection.tsx` | 笔记那一块:输入 + 存一条 + 列表 | 2 |
|
|
73
|
-
| `src/components/PrefsSection.tsx` | 「最大的几个文件」配置(`SelectField`,按工作区存) | 2 |
|
|
74
|
-
| `src/components/OverviewCard.module.css` | 这个组件自己的样式(CSS Modules,构建时类名按路径哈希)—— **样式跟组件同目录** | 2 |
|
|
75
|
-
| `src/components/__tests__/OverviewCard.test.ts` | 这张卡五态各画得出来 + 写失败留在卡上(测试住同目录的 `__tests__/`);流程 / 笔记 / 配置的接线由 `plugin.test.ts` 与 smoke 覆盖 | 99 |
|
|
76
|
-
| `src/host.ts` | 宿主面:接线 + 三条只读路由(`overview` / `notes` / `prefs`)+ 挂工具 / 长任务 / 动作通道 / `prefs` / 审批口径 | 10 |
|
|
77
|
-
| `src/host/overview.ts` | 宿主侧内部实现:`readOverview()`(真读目录)+ 路由回答 | 10 |
|
|
78
|
-
| `src/host/sample.ts` | 宿主侧内部实现:样板流程的动作、长任务、只读工具 | 10 |
|
|
79
|
-
| `src/host/notes.ts` | 宿主侧内部实现:笔记读写(`deps.store`)+ 写工具 + 动作通道 | 10 |
|
|
80
|
-
| `src/client.tsx` | 浏览器面:**只剩面** —— 注册 + 把 props 交给 `components/OverviewCard` | 10 |
|
|
81
|
-
| `src/__tests__/plugin.test.ts` | 面注册 + 路由真读 + 工具注册 + 流程真跑 + 笔记/配置/审批 | 99 |
|
|
82
|
-
| `scripts/smoke.mjs` | 接线自检:卡片进没进外壳、工具注册、流程真跑、笔记真写、配置存得住;真读断言在 `smoke.assert.mjs` | — |
|
|
83
|
-
| `scripts/smoke.assert.mjs` | **这个模块自己的**真读断言:换数据源时改这份,别删 smoke 里的调用 | — |
|
|
84
|
-
| `CHANGELOG.md` | 版本记录(`dsh-wb-module release` 追加;`package.json.files` 里带它) | — |
|
|
85
|
-
| `tsconfig.json` / `vitest.config.ts` / `types/globals.d.ts` / `types/styles.d.ts` | 经典 JSX 运行时(`jsxFactory: h`)、vitest 的别名、浏览器半侧的运行时垫片、`*.module.css` 的类名表声明 | — |
|
|
86
|
-
| `README.md` | 就是本文 | — |
|
|
87
|
-
|
|
88
|
-
## 长任务、工具、写通道与配置(样板)
|
|
89
|
-
|
|
90
|
-
这份骨架故意带了一组**可跑**的能力,示范五件事:
|
|
91
|
-
|
|
92
|
-
- **宿主面接线**:`name` / `inject` / `apply` 用 `@dsh-workbench/shared/host` 的 `defineUnitHostPlugin`
|
|
93
|
-
一次给齐;写工具的审批口径用 `writeApprovalOf`;`recordOf` / `textOf` 也从那里取,别每个文件抄一份。
|
|
94
|
-
- **只读工具**:`registerTools()` 是**调用时才注册**、返回的是清理函数;工具名必须和 `package.json` 的
|
|
95
|
-
`dsh.workbench.tools` 一字不差。`__UNIT___run` 起一趟流程,传 `runId` 则只看同一趟。
|
|
96
|
-
- **长任务**:`deps.longTasks.create()` 交回一张注册表,`run` 里调 `deps.flow.run()` 并把
|
|
97
|
-
`onStep` 交给 `longTaskStepOf`(shared/host 的默认映射);卡片用 `useLongTask` 读、`LongTaskRow` 画。
|
|
98
|
-
- **写通道 + 审批**:`__UNIT___add_note` 在清单里写 `writes: true`,`mount()` 返回 `needsApproval`(这次要点头)
|
|
99
|
-
与 `approvalDetail`(审批屏上那句"会写什么");agent 调用前壳会拦一道。卡片那颗钮走动作通道
|
|
100
|
-
`<unit>-add-note`(点击即确认,不审批),与写工具共用同一份 `deps.store` 数据;网络 / 校验失败
|
|
101
|
-
留在按钮下面那一行,不静默。
|
|
102
|
-
- **配置(prefs)**:`mount()` 返回 `prefs: { resource, handle }`,按工作区把"显示几个文件"存进
|
|
103
|
-
`deps.store`;卡片用 `useHostWrite({ channel: 'prefs' })` 写、用 `<unit>/prefs` 读。
|
|
104
|
-
- **动作通道**:用户直接点的单步动作走 `<unit>-…` 资源;与工具共用实现,不另写一份。
|
|
105
|
-
|
|
106
|
-
换成你自己的能力时,五处一起改:`src/model/*` 的纯逻辑、`src/host/*` 的实现、
|
|
107
|
-
`src/host.ts` 的接线(路由 / 工具 / 动作 / prefs / 审批)、`src/components/OverviewCard.tsx` 的展示、
|
|
108
|
-
`design.json` 的三张表与能力行。
|
|
109
|
-
|
|
110
|
-
## 数据是在哪算出来的
|
|
111
|
-
|
|
112
|
-
宿主侧,`src/host/overview.ts` 的 `readOverview(path, limit)`:
|
|
113
|
-
|
|
114
|
-
* `readdirSync(path, { withFileTypes: true })` 数出**目录数**;
|
|
115
|
-
* 每个普通文件 `statSync` 一次,累加**总字节**、记下 `name / bytes / mtime`(mtime 是 ISO 串);
|
|
116
|
-
* 按字节降序(同字节按名字)排,取前 `limit` 个当**最大的几个文件**。
|
|
117
|
-
|
|
118
|
-
已知的取舍(不是 bug,是这份骨架刻意保持简单的地方):
|
|
119
|
-
|
|
120
|
-
* **符号链接不计数**:既不算文件也不算目录(想要跟随就自己换成 `statSync` + `lstatSync` 判断);
|
|
121
|
-
* **同步读**:模板优先让代码一眼看得懂;真接大目录时改成 `node:fs/promises` 版本更合适;
|
|
122
|
-
* 只读**直接子项**,不递归。
|
|
123
|
-
|
|
124
|
-
## 那条只读路由
|
|
125
|
-
|
|
126
|
-
键是 `<unit>/overview`(`__UNIT__` 就是本模块的单元 id)——**必须住在自己那一格的命名空间里**,
|
|
127
|
-
外壳装 / 升 / 卸时要能点名到它;写成别的名字注册当场就被判红。
|
|
128
|
-
|
|
129
|
-
```
|
|
130
|
-
GET /api/__UNIT__/overview?path=<绝对路径>&limit=<1..50>
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
查询参数:
|
|
134
|
-
|
|
135
|
-
| 参数 | 缺省 | 说明 |
|
|
136
|
-
|---|---|---|
|
|
137
|
-
| `path` | 无(必填) | 要读的目录。卡片把 `props.world.current.workspace.path` 塞进来;缺了不回数据,直接回 `no-path` |
|
|
138
|
-
| `limit` | `5` | 返回几个"最大的文件",夹在 `1..50` 之间 |
|
|
139
|
-
|
|
140
|
-
成功:
|
|
141
|
-
|
|
142
|
-
```json
|
|
143
|
-
{ "ok": true, "path": "/…", "files": 128, "dirs": 12, "bytes": 3145728,
|
|
144
|
-
"largest": [{ "name": "client.js", "bytes": 182400, "mtime": "2026-01-05T09:12:44.000Z" }] }
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
读不到(路径不存在 / 没权限 / 不是目录):**HTTP 200 + `ok:false`**,错误码 `overview-unreadable`。
|
|
148
|
-
压根没给 `path`:同样 `ok:false`,错误码 `no-path` —— 别拿宿主进程的工作目录当默认值:
|
|
149
|
-
那会让"我没说要看哪儿"变成"屏幕上是 dsh 服务启动目录",两个都不像用户想要的东西。
|
|
150
|
-
之所以不让它抛:抛出去会被外壳的传输层转成 HTTP 500,卡片只剩一句"宿主返回 HTTP 500",
|
|
151
|
-
用户看不到是哪条路径出了什么事。
|
|
152
|
-
|
|
153
|
-
```json
|
|
154
|
-
{ "ok": false, "error": { "code": "overview-unreadable", "message": "ENOENT: no such file or directory, scandir '/…'" } }
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
卡片侧读数的 hook 是 `kit.hooks.useApiResource`(`route` 就是 `__UNIT__/overview`)。
|
|
158
|
-
注意它的 `value` 是**整个响应对象**(成功时形如 `{ ok: true, ...payload }`),不是 `payload` 本身。
|
|
159
|
-
|
|
160
|
-
## 卡片画哪五态
|
|
161
|
-
|
|
162
|
-
`src/model/overview.ts` 的 `overviewStateOf({ phase, value, error })` 是一条纯函数,
|
|
163
|
-
每个分支各对应一屏(`src/model/__tests__/overview.test.ts` 直接断言这条函数,
|
|
164
|
-
`src/components/__tests__/OverviewCard.test.ts` 再把每一屏画一遍):
|
|
165
|
-
|
|
166
|
-
| 态 | 什么时候 | 画什么 |
|
|
167
|
-
|---|---|---|
|
|
168
|
-
| 待机 | 没有选中工作区(`path === ''`),请求压根不发(`useApiResource(…, { enabled: false })`,`phase` 停在 `idle`) | `CenteredState`:先打开一个项目 |
|
|
169
|
-
| 加载 | 还没有值(首读),或 `phase === 'error'` 之外什么都没来 | `CenteredState`:正在读目录 |
|
|
170
|
-
| 就绪 | 有值,且文件数 + 目录数 > 0 | `StateBlock`:路径 + 「N 个文件 · M 个目录 · 总字节」+ 最大的几个文件列表 |
|
|
171
|
-
| 就绪但空 | 有值,但文件数 = 目录数 = 0 | `EmptyState`:这个目录是空的 |
|
|
172
|
-
| 出错 | `phase === 'error'` | `CenteredState`:读不到这个目录 + 宿主的原话 |
|
|
173
|
-
|
|
174
|
-
刷新时 `useApiResource` 会把 `phase` 打成 `loading` **同时保留上一次的 `value`** ——
|
|
175
|
-
这时刻意继续画旧数据(右上角的转圈由 `Card` 的 `refreshing` 表示),不退回骨架:
|
|
176
|
-
一次刷新不该让整张卡闪一下。
|
|
177
|
-
|
|
178
|
-
## 开发与发布
|
|
179
|
-
|
|
180
|
-
开发命令见上面的六个脚本;dsh / 工作台的模块约束见工作台模块指引(随外壳出货);agent 的工作说明见 `AGENTS.md`。
|
|
181
|
-
|
|
182
|
-
发布(维护者确认之后):`dsh-wb-module release --target team|public|tarball|git`;
|
|
183
|
-
`--dry-run` 只预览,`--verify` 只核已发布的包。
|
|
5
|
+
<!-- 这份说明由 `dsh-wb-module design render` 从 readme.json 生成;作者文档在 DEVELOPING.md。 -->
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
".": "./dist/host.js",
|
|
9
9
|
"./client": "./client.js"
|
|
10
10
|
},
|
|
11
|
-
"files": ["dist", "client.js", "cordis.patch.yml", "CHANGELOG.md"],
|
|
11
|
+
"files": ["dist", "client.js", "cordis.patch.yml", "DEVELOPING.md", "CHANGELOG.md"],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "dsh-wb-module build",
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "__UNIT__ 模块",
|
|
3
|
+
"summary": "__DESC__",
|
|
4
|
+
"shots": [
|
|
5
|
+
{
|
|
6
|
+
"caption": "卡片",
|
|
7
|
+
"ascii": "(待填:一张卡片布局草图,用界面真实文案,按屏幕从上到下的顺序画)"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"see": [
|
|
11
|
+
"(待填:用户在卡片上看到的第一样东西)"
|
|
12
|
+
],
|
|
13
|
+
"do": [
|
|
14
|
+
"(待填:用户自己能点的一个按钮或能改的一项设置)"
|
|
15
|
+
],
|
|
16
|
+
"agent": [
|
|
17
|
+
"(待填:agent 能替用户做的一件事;写之前要不要批准)"
|
|
18
|
+
],
|
|
19
|
+
"data": [
|
|
20
|
+
"(待填:数据从哪来、用谁的登录、读还是写)"
|
|
21
|
+
],
|
|
22
|
+
"install": "",
|
|
23
|
+
"trouble": [
|
|
24
|
+
"(待填:出问题时用户看到什么、下一步点哪)"
|
|
25
|
+
],
|
|
26
|
+
"faq": []
|
|
27
|
+
}
|
|
@@ -337,13 +337,13 @@ check('另一个工作区没被带上(还是默认 5)', otherPrefs?.prefs?.l
|
|
|
337
337
|
const { assertModule } = await import('./smoke.assert.mjs');
|
|
338
338
|
await assertModule({ check, routes, mounted, UNIT_ID });
|
|
339
339
|
|
|
340
|
-
// ── 需求底稿在不在:这份骨架的"设计记录"住
|
|
340
|
+
// ── 需求底稿在不在:这份骨架的"设计记录"住 DEVELOPING.md,别只留在对话里 ─────
|
|
341
341
|
//
|
|
342
342
|
// 只查**形状在不在**(那几节有没有被删掉),不查内容填得好不好 —— 内容由指引守,
|
|
343
343
|
// 判据硬管内容只会逼出"把占位符原样留着"这种应付。
|
|
344
|
-
const
|
|
344
|
+
const developing = readFileSync(new URL('../DEVELOPING.md', import.meta.url), 'utf8');
|
|
345
345
|
for (const section of ['## 需求底稿', '### 表 1 面板', '### 表 2 agent', '### 表 3 联动', '### 数据源探明记录']) {
|
|
346
|
-
check(`
|
|
346
|
+
check(`DEVELOPING.md 里有「${section}」(需求与验收的落点)`, developing.includes(section));
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
console.log(`\n${failures.length === 0 ? '模块自检通过' : `${failures.length} 条没过`}`);
|