@dsh-workbench/module-tools 0.3.5 → 0.3.7
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/package.json +2 -2
- package/src/checks/contracts.mjs +20 -8
- package/src/design.mjs +27 -0
- package/template-diy/README.md.tpl +27 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"eslint-plugin-react": "^7.37.5",
|
|
30
30
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
31
31
|
"typescript-eslint": "^8.71.0",
|
|
32
|
-
"@dsh-workbench/shared": "0.3.
|
|
32
|
+
"@dsh-workbench/shared": "0.3.7"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"selfcheck": "node scripts/selfcheck/index.mjs"
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -251,11 +251,15 @@ function fakeLongTasks(created) {
|
|
|
251
251
|
return {
|
|
252
252
|
create(options) {
|
|
253
253
|
created.push(options);
|
|
254
|
+
// 形状跟着真外壳那份注册表走(`createLongTaskRegistry`):假的那份少一个方法,
|
|
255
|
+
// 把注册表原样返回的模块就会被这条判据冤枉成"缺方法"——所以 `stop` / `live` 也在这里。
|
|
254
256
|
return {
|
|
255
257
|
start: () => ({}),
|
|
256
258
|
snapshot: () => null,
|
|
257
259
|
active: () => null,
|
|
258
260
|
current: () => [],
|
|
261
|
+
live: () => [],
|
|
262
|
+
stop: () => false,
|
|
259
263
|
onChange: () => () => {},
|
|
260
264
|
};
|
|
261
265
|
},
|
|
@@ -347,7 +351,7 @@ function mountOf(face, created, manifest, tools) {
|
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
|
|
350
|
-
const LONG_TASK_METHODS = ['start', 'snapshot', 'active', 'current', 'onChange'];
|
|
354
|
+
const LONG_TASK_METHODS = ['start', 'snapshot', 'active', 'current', 'live', 'stop', 'onChange'];
|
|
351
355
|
|
|
352
356
|
const LONG_TASK_RENDERERS = ['useLongTask', 'LongTaskRow', 'longTaskDot'];
|
|
353
357
|
|
|
@@ -788,12 +792,19 @@ export async function run() {
|
|
|
788
792
|
}
|
|
789
793
|
}
|
|
790
794
|
|
|
791
|
-
//
|
|
795
|
+
// 长任务:跑一会儿的事,注册了就得让用户看得见那一行。
|
|
792
796
|
//
|
|
793
797
|
// 外壳按 mount 结果里的 longTasks 投影出 `${unit}/runs`(kit 的 useLongTask 读的就是它)。
|
|
794
|
-
//
|
|
795
|
-
// 进行中 / 要登录 / 完成 /
|
|
796
|
-
//
|
|
798
|
+
// 契约里的状态是**五种**:running / needs-login / stopped / ok / failed(`longTaskDot` 把它们映成
|
|
799
|
+
// 进行中 / 要登录 / 灰点 / 完成 / 出错 —— `stopped` 落进默认分支,是"用户按停的",不是成功也不是出错)。
|
|
800
|
+
// 契约里也**有"停止"**:注册表多一个 `stop(id)`,`run` 多收一个 `signal`。
|
|
801
|
+
// `start` 还可以带一个**可选**的 `session`(起这一趟的那个会话 id,模块从 `sessionIdOf(exec)` 拿):
|
|
802
|
+
// 不记 = 用户在界面上按的 —— 外壳那两把 `workbench_long_tasks_*` 按它判"谁的"(自己起的免审批)。
|
|
803
|
+
// 判据**不查**这一条(它只查形状;"起的时候记没记来源"是形状之外的事)。
|
|
804
|
+
// 但"停止"在注册表这一侧的射程只有两件:外壳把 `signal` abort 掉、把这一趟标成 `stopped`;
|
|
805
|
+
// **真停不停得了看生产方认不认这个 signal** —— 那是模块的知识,判据查不了这一条
|
|
806
|
+
// (内部是 opencli 子进程那类编排,今天最多做到"我们不再等它")。判据能查的是形状与"画得出来":
|
|
807
|
+
// · 注册表形状要全(少了方法,外壳那条 runs 路由 / 那颗停止钮就按不动);
|
|
797
808
|
// · create 的 key 非空、run 与 view 都是函数(没有 view,卡片上那行就没有可渲染的快照);
|
|
798
809
|
// · 真的注册了长任务,客户端产物里就得出现 kit 那套(useLongTask / LongTaskRow / longTaskDot),
|
|
799
810
|
// 否则用户点完只看到"没反应"。
|
|
@@ -803,8 +814,9 @@ export async function run() {
|
|
|
803
814
|
const missing = LONG_TASK_METHODS.filter((method) => typeof registry[method] !== 'function');
|
|
804
815
|
if (missing.length > 0) {
|
|
805
816
|
problems.push(
|
|
806
|
-
`${name}: 长任务注册表缺方法 ${missing.join(' / ')} —— 外壳按它给这个单元投影出 /runs
|
|
807
|
-
`kit 的 useLongTask
|
|
817
|
+
`${name}: 长任务注册表缺方法 ${missing.join(' / ')} —— 外壳按它给这个单元投影出 /runs 路由(停止那颗钮也按它发),` +
|
|
818
|
+
`kit 的 useLongTask 与左栏会话行那颗 loading 点读的也是它(后者走 \`live()\`;注册表用 ` +
|
|
819
|
+
`@dsh-workbench/shared/host 的 createLongTaskRegistry 建)`,
|
|
808
820
|
);
|
|
809
821
|
}
|
|
810
822
|
}
|
|
@@ -827,7 +839,7 @@ export async function run() {
|
|
|
827
839
|
if (!LONG_TASK_RENDERERS.some((one) => clientText.includes(one))) {
|
|
828
840
|
problems.push(
|
|
829
841
|
`${name}: 注册了长任务,但客户端产物里没有 ${LONG_TASK_RENDERERS.join(' / ')}(kit 里那套)—— ` +
|
|
830
|
-
`用户点完只看得到"没反应",看不到进行中 / 要登录 / 完成 / 出错`,
|
|
842
|
+
`用户点完只看得到"没反应",看不到进行中 / 要登录 / 停下的 / 完成 / 出错`,
|
|
831
843
|
);
|
|
832
844
|
}
|
|
833
845
|
}
|
package/src/design.mjs
CHANGED
|
@@ -51,6 +51,26 @@ const ROWS = {
|
|
|
51
51
|
const DATASOURCE_FIELDS = ['read', 'params', 'credentials', 'notLoggedIn', 'timeout'];
|
|
52
52
|
const WRITE_FIELDS = ['command', 'params', 'credentials', 'preview', 'readback', 'undo', 'failure'];
|
|
53
53
|
|
|
54
|
+
/** `params` 里写了"必填",就得说清凭什么是必填 —— 来源只能是这五个词之一
|
|
55
|
+
* (`已证` 那句话的来源是**录下来的图**上那个红 `*`,或者**契约**里 `--help -f json` 的 `required`)。**气味判据不是解析器**:
|
|
56
|
+
* 只看来源有没有写出来,不看句式、不数个数、不认字段名,也不校验来源是不是真的 ——
|
|
57
|
+
* 真话假话只有人和复核能判(照 `undo` 那条的原话:逼作者想过,不逼他造假)。 */
|
|
58
|
+
const SOURCE_TAGS = ['已证', '声明', '人证', '契约', '未定'];
|
|
59
|
+
|
|
60
|
+
/** `params` 里有没有一处「必填」**没被否掉** —— `非必填` / `不必填`(只看紧邻的那个字符)才算被否掉。
|
|
61
|
+
* 这是**字符级守卫**,只挡这一种写法:不认"必填项""必填字段"以外的语法、不做否定句理解。
|
|
62
|
+
* 为什么非要有它:`design check` 是模块作者天天跑的门禁,`非必填` 这种**明确说不要**的写法
|
|
63
|
+
* 被误咬一次,他下次就把这条判据当噪音 —— 误红的代价比漏一两次大得多。 */
|
|
64
|
+
function hasBareRequired(value) {
|
|
65
|
+
let at = value.indexOf('必填');
|
|
66
|
+
while (at !== -1) {
|
|
67
|
+
const before = value[at - 1];
|
|
68
|
+
if (before !== '非' && before !== '不') return true;
|
|
69
|
+
at = value.indexOf('必填', at + 2);
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/** 一条卡背后可以有多条读(甚至跨站点):规范形状 `reads: [ … ]`,一条一行;
|
|
55
75
|
* `datasource: { … }` 是只有一条读时的写法(老模块都长这样,继续认)。 */
|
|
56
76
|
function readsOf(design) {
|
|
@@ -208,6 +228,13 @@ export function emptyCells(design) {
|
|
|
208
228
|
writes.forEach((write, index) => {
|
|
209
229
|
const label = writes.length === 1 ? 'write' : `writes[${index}]`;
|
|
210
230
|
for (const field of WRITE_FIELDS) need(cells, write?.[field], `${label}.${field}`);
|
|
231
|
+
// 写了"必填"却不带来源词 ⇒ 点名(`非必填` / `不必填` 那种**明确说不要**的不算,见 hasBareRequired)。
|
|
232
|
+
// 空着 / `(待填)` 是上一轮那条 `pending` 的事,两条不叠。
|
|
233
|
+
// 判的是**值**不是表头:渲染出来的行名「参数(必填 / 取值枚举 / 上限)」不在这条路上。
|
|
234
|
+
const params = text(write?.params);
|
|
235
|
+
if (!pending(params) && hasBareRequired(params) && !SOURCE_TAGS.some((tag) => params.includes(tag))) {
|
|
236
|
+
cells.push(`${label}.params.source`);
|
|
237
|
+
}
|
|
211
238
|
});
|
|
212
239
|
|
|
213
240
|
return cells;
|
|
@@ -194,7 +194,12 @@ approvalDetail: async (call) => ({ zh: '会覆盖 3 个模块的产物,不碰
|
|
|
194
194
|
mount({ longTasks, ... }) {
|
|
195
195
|
const runs = longTasks?.create({
|
|
196
196
|
key: 'rebuild',
|
|
197
|
-
run: async ({ payload, onStep
|
|
197
|
+
run: async ({ payload, onStep, signal }) => {
|
|
198
|
+
onStep({ kind: 'start', text: '开始…' });
|
|
199
|
+
// signal 被 abort = 有人按了停止(用户 / agent):每一轮都看一眼 `signal.aborted`,能收手就收手
|
|
200
|
+
/* … */
|
|
201
|
+
return {};
|
|
202
|
+
},
|
|
198
203
|
outcome: (result) => ({ why: '…' }),
|
|
199
204
|
view: (snapshot) => ({ steps: String(snapshot.steps.length) }),
|
|
200
205
|
});
|
|
@@ -202,9 +207,27 @@ mount({ longTasks, ... }) {
|
|
|
202
207
|
}
|
|
203
208
|
```
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
起这一趟的那把工具里(`execute(args, exec)` 拿得到现场)记下来源:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
runs.start({ workspace, target, payload, session: sessionIdOf(exec) ?? undefined });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`sessionIdOf`(`@dsh-workbench/shared/host`)说这次调用是哪个会话发的。**不记来源 = 用户按的**
|
|
217
|
+
(或这一版之前起的)—— 停它要走审批;外壳自带的那两把 `workbench_long_tasks_list` /
|
|
218
|
+
`workbench_long_tasks_stop` 就是这么判"谁的"(自己起的免审批,别人的 / 用户按的要审批,
|
|
219
|
+
不在当前工作区的直接拒绝)。
|
|
220
|
+
|
|
221
|
+
浏览器侧用 kit 那套画(`kit.hooks.useLongTask` + `<LongTaskRow …/>`):状态有五种 ——
|
|
222
|
+
`running` 进行中 / `needs-login` 要登录 / `stopped` 停下的(用户按停)/ `ok` 完成 / `failed` 出错。
|
|
223
|
+
|
|
224
|
+
要给用户那颗停止钮,就在卡片上给那一行的 `LongTaskRow` 传 `stop`
|
|
225
|
+
(词与取值照 dsh 任务列表那行:aria `停止任务 {label}` → 上膛 → 可见字 `确认停止`、
|
|
226
|
+
aria `再次点击确认停止`,3 秒落膛;**失败那半是我们自己的** —— 不往钮里写「停止失败」,
|
|
227
|
+
而是把这次请求失败的原话画在那一行下面,`failed` 给什么画什么);**要停就认 `signal`,不认就别画** ——
|
|
228
|
+
画了就是骗人:外壳只会把这一趟标成 `stopped`,你的活儿还在跑(内部是 opencli 子进程那类编排,
|
|
229
|
+
今天最多做到"我们不再等它")。要用这个新 prop 就得装带这次改动的那份外壳 ——
|
|
230
|
+
把自己 `peerDependencies['@dsh-workbench/core']` 的下限抬到那一版。
|
|
208
231
|
|
|
209
232
|
### 存起来:他能自己配什么(`prefs`)
|
|
210
233
|
|