@dsh-workbench/module-tools 1.1.19-beta.0 → 1.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "1.1.19-beta.0",
3
+ "version": "1.2.0",
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.1.19-beta.0"
36
+ "@dsh-workbench/shared": "1.2.0"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
 
6
6
  import { discoverModulePackages } from '../discover.mjs';
7
- import { shellClientPathOf, shellKitOf } from './shell-artifact.mjs';
7
+ import { shellClientPathOf, shellKitOf, shellSchemaCardOf } from './shell-artifact.mjs';
8
8
 
9
9
  export const title = '卡片渲染';
10
10
 
@@ -302,6 +302,265 @@ function render(node, depth, budget) {
302
302
  return count;
303
303
  }
304
304
 
305
+ const PROBE_MESSAGES = {
306
+ zh: {
307
+ 'probe.title': '探针',
308
+ 'probe.platform': '平台',
309
+ 'probe.idle': '先打开一个项目',
310
+ 'probe.metric': '指标',
311
+ 'probe.row': '一行',
312
+ 'probe.chat': '让 agent 看',
313
+ 'probe.action': '查看',
314
+ 'probe.activity': '动过',
315
+ 'probe.open': '打开',
316
+ 'probe.retry': '重试',
317
+ 'probe.settings': '设置',
318
+ 'probe.gear': '设置',
319
+ 'probe.cancel': '取消',
320
+ 'probe.save': '保存',
321
+ 'probe.saving': '保存中',
322
+ 'probe.failed': '保存失败',
323
+ 'probe.empty': '没有内容',
324
+ 'probe.env': '环境',
325
+ 'probe.dev': '开发',
326
+ },
327
+ en: {
328
+ 'probe.title': 'Probe',
329
+ 'probe.platform': 'Platform',
330
+ 'probe.idle': '先打开一个项目',
331
+ 'probe.metric': 'Metric',
332
+ 'probe.row': 'Row',
333
+ 'probe.chat': '让 agent 看',
334
+ 'probe.action': 'Open',
335
+ 'probe.activity': 'Moved',
336
+ 'probe.open': 'Open',
337
+ 'probe.retry': 'Retry',
338
+ 'probe.settings': 'Settings',
339
+ 'probe.gear': 'Settings',
340
+ 'probe.cancel': 'Cancel',
341
+ 'probe.save': 'Save',
342
+ 'probe.saving': 'Saving',
343
+ 'probe.failed': 'Save failed',
344
+ 'probe.empty': 'Nothing yet',
345
+ 'probe.env': 'Environment',
346
+ 'probe.dev': 'Development',
347
+ },
348
+ };
349
+
350
+ const PROBE_LONG_TASKS = [
351
+ {
352
+ unit: 'probe',
353
+ id: 'run-1',
354
+ workspace: '/tmp/repo',
355
+ target: 'probe-target',
356
+ status: 'needs-login',
357
+ step: '等待登录',
358
+ steps: ['等待登录'],
359
+ loginUrl: 'https://example.test/login',
360
+ },
361
+ {
362
+ unit: 'probe',
363
+ id: 'run-2',
364
+ workspace: '/tmp/repo',
365
+ target: 'probe-done',
366
+ status: 'ok',
367
+ step: '完成',
368
+ steps: [],
369
+ },
370
+ ];
371
+
372
+ const PROBE_VIEW = {
373
+ version: 1,
374
+ titleKey: 'probe.title',
375
+ idleKey: 'probe.idle',
376
+ platform: { href: 'https://example.test', labelKey: 'probe.platform' },
377
+ read: {
378
+ route: 'probe',
379
+ map: (payload) => {
380
+ const size = Number(payload?.length ?? 2);
381
+ return size === 0
382
+ ? { phase: 'empty', hintKey: 'probe.empty' }
383
+ : { phase: 'ready', model: { count: size } };
384
+ },
385
+ },
386
+ sections: {
387
+ metrics: () => [{ labelKey: 'probe.metric', value: '2', tone: 'ok' }],
388
+ rows: () => [
389
+ {
390
+ key: 'row',
391
+ titleKey: 'probe.row',
392
+ meta: 'meta',
393
+ tone: 'warn',
394
+ href: 'https://example.test/row',
395
+ action: { labelKey: 'probe.action', href: 'https://example.test/action' },
396
+ },
397
+ ],
398
+ activity: () => [{ key: 'act', textKey: 'probe.activity', at: 'just now', tone: 'bad' }],
399
+ longTask: () => [
400
+ {
401
+ id: 'run-1',
402
+ headline: '探针长任务',
403
+ links: [{ href: 'https://example.test/run-1', labelKey: 'probe.action' }],
404
+ },
405
+ ],
406
+ },
407
+ settings: {
408
+ gearKey: 'probe.gear',
409
+ titleKey: 'probe.settings',
410
+ fields: [
411
+ {
412
+ kind: 'select',
413
+ key: 'env',
414
+ labelKey: 'probe.env',
415
+ options: [{ value: 'dev', labelKey: 'probe.dev' }],
416
+ },
417
+ ],
418
+ values: () => ({ env: 'dev' }),
419
+ save: { route: 'probe/config', body: (values) => values },
420
+ labels: {
421
+ cancelKey: 'probe.cancel',
422
+ saveKey: 'probe.save',
423
+ savingKey: 'probe.saving',
424
+ failedKey: 'probe.failed',
425
+ },
426
+ },
427
+ actions: [
428
+ { kind: 'open-url', labelKey: 'probe.open', href: 'https://example.test' },
429
+ { kind: 'retry', labelKey: 'probe.retry' },
430
+ ],
431
+ custom: () => fakeReact().createElement('p', null, 'custom'),
432
+ };
433
+
434
+ function schemaProbeShell(modulePackages) {
435
+ const local = join(process.cwd(), 'packages/core/client.js');
436
+ if (existsSync(local)) return local;
437
+ const first = modulePackages[0];
438
+ return first === undefined ? undefined : shellClientPathOf(first.dir);
439
+ }
440
+
441
+ function runSchemaProbe(modulePackages, shellDictionaries, problems, rows, footnotes) {
442
+ if (shellDictionaries === undefined) {
443
+ footnotes.push('schema 卡探针跳过:取不出 @dsh-workbench/shared 的字典');
444
+ return false;
445
+ }
446
+ const shellClient = schemaProbeShell(modulePackages);
447
+ if (shellClient === undefined || !existsSync(shellClient)) {
448
+ footnotes.push('schema 卡探针跳过:找不到外壳 client.js');
449
+ return false;
450
+ }
451
+
452
+ let schemaCard;
453
+ let kit;
454
+ try {
455
+ schemaCard = shellSchemaCardOf(shellClient, {
456
+ react: fakeReact(),
457
+ dshPackage: fakeDshPackage(),
458
+ });
459
+ kit = shellKitOf(shellClient, { react: fakeReact(), dshPackage: fakeDshPackage() });
460
+ } catch (error) {
461
+ footnotes.push(`schema 卡探针跳过:取不出外壳(${error instanceof Error ? error.message : String(error)})`);
462
+ return false;
463
+ }
464
+ if (typeof schemaCard !== 'function') {
465
+ footnotes.push('schema 卡探针跳过:这份外壳还没导出 schemaCard');
466
+ return false;
467
+ }
468
+
469
+ const missing = [];
470
+ const counts = new Map();
471
+ const textsBySnapshot = new Map();
472
+ const classNames = new Set();
473
+ const stubs = new Set();
474
+
475
+ for (const snapshot of SNAPSHOTS) {
476
+ counts.set(snapshot.name, []);
477
+ for (const language of LANGUAGES) {
478
+ const budget = { left: 20000, maxDepth: 0, classNames: new Set(), stubs: new Set(), texts: [] };
479
+ const t = translatorOf(language, PROBE_MESSAGES, shellDictionaries, missing, snapshot.name);
480
+ const capability = snapshot.capability?.('probe') ?? null;
481
+ const blocker =
482
+ capability !== null && typeof kit?.lib?.blockerOfCapability === 'function'
483
+ ? kit.lib.blockerOfCapability(capability, t)
484
+ : snapshot.resource.phase === 'error' && typeof kit?.lib?.blockerOfError === 'function'
485
+ ? kit.lib.blockerOfError(snapshot.resource.error, t)
486
+ : undefined;
487
+ const where = `${snapshot.name}(${language})`;
488
+
489
+ let tree;
490
+ try {
491
+ tree = schemaCard({
492
+ unit: 'probe',
493
+ view: PROBE_VIEW,
494
+ state: snapshot.resource,
495
+ refreshing: snapshot.resource.phase === 'loading',
496
+ ...(blocker === undefined ? {} : { blocker }),
497
+ longTasks: PROBE_LONG_TASKS,
498
+ longTaskStopping: null,
499
+ longTaskStopFailed: null,
500
+ onLongTaskStop: () => {},
501
+ onLongTaskLogin: () => {},
502
+ world: { current: { sessionId: null, workspace: snapshot.workspace } },
503
+ t,
504
+ refreshToken: 0,
505
+ onRefresh: () => {},
506
+ onNavigate: () => {},
507
+ onOpenMarket: () => {},
508
+ onSummary: () => {},
509
+ });
510
+ } catch (error) {
511
+ problems.push(`schema 卡: ${where} 抛了 —— ${error.message}`);
512
+ continue;
513
+ }
514
+
515
+ let count;
516
+ try {
517
+ count = render(tree, 0, budget);
518
+ } catch (error) {
519
+ const stack = (error.stack ?? '').split('\n').slice(1, 4).join(' | ');
520
+ problems.push(`schema 卡: ${where} 渲染抛了 —— ${error.message}\n ${stack}`);
521
+ continue;
522
+ }
523
+ if (count <= 1) {
524
+ problems.push(`schema 卡: ${where} 只画出 ${count} 个节点 —— 这一态下卡体会没跑`);
525
+ continue;
526
+ }
527
+ counts.get(snapshot.name).push(count);
528
+ for (const one of budget.classNames) classNames.add(one);
529
+ for (const one of budget.stubs) stubs.add(one);
530
+ const bucket = textsBySnapshot.get(snapshot.name) ?? [];
531
+ bucket.push(...budget.texts);
532
+ textsBySnapshot.set(snapshot.name, bucket);
533
+ for (const hit of forbiddenHitsOf(budget.texts)) {
534
+ problems.push(
535
+ `schema 卡: ${where} 屏上出现了 ${hit.token} —— 命中的那串:${hit.text.slice(0, 120)}`,
536
+ );
537
+ }
538
+ }
539
+ }
540
+
541
+ if (missing.length > 0) {
542
+ const keys = [...new Set(missing.map((one) => `${one.language}:${one.key}`))];
543
+ problems.push(`schema 卡: 探针文案缺落点 —— ${keys.join('、')}`);
544
+ }
545
+ if (!(classNames.has('wb-card') || stubs.has('Card'))) {
546
+ problems.push('schema 卡: 探针没套外壳的卡壳(wb-card / Card)');
547
+ return true;
548
+ }
549
+
550
+ const per = SNAPSHOTS.map(
551
+ (snapshot) => `${snapshot.name} ${counts.get(snapshot.name).join('/') || '—'}`,
552
+ ).join(' · ');
553
+ rows.push(`schema 卡: ${SNAPSHOTS.length} 态 × zh/en 都画得出 —— ${per} 个节点`);
554
+
555
+ const shapes = new Set(
556
+ SNAPSHOTS.map((snapshot) => [...new Set(textsBySnapshot.get(snapshot.name) ?? [])].sort().join('\n')),
557
+ );
558
+ if (shapes.size === 1) {
559
+ footnotes.push('schema 卡: 8 个快照渲染出来的文本完全一样 —— 只当线索');
560
+ }
561
+ return true;
562
+ }
563
+
305
564
  async function shellDictionariesOf() {
306
565
  const require = createRequire(import.meta.url);
307
566
  const entry = require.resolve('@dsh-workbench/shared');
@@ -452,10 +711,13 @@ export async function run() {
452
711
  }
453
712
 
454
713
  const card = registered?.card;
714
+ const view = registered?.view;
715
+ const hasView = typeof view === 'object' && view !== null;
716
+ const unitId = registered?.unit ?? pkg.manifest.dsh?.workbench?.unit ?? name;
455
717
  if (!declaresCard) {
456
- if (typeof card === 'function') {
718
+ if (typeof card === 'function' || hasView) {
457
719
  problems.push(
458
- `${name}: 清单写的是 "card": null,产物却登记了一张卡片 —— 两处说法不一致:` +
720
+ `${name}: 清单写的是 "card": null,产物却登记了一张卡片 / 一条 view —— 两处说法不一致:` +
459
721
  '要么把清单补成一张卡(`"card": { "order": … }`),要么把这张卡从产物里去掉',
460
722
  );
461
723
  continue;
@@ -463,8 +725,8 @@ export async function run() {
463
725
  rows.push(`${name}: 清单写的是 "card": null —— 这个模块没有卡片,产物里也确实没登记(跳过渲染判定)`);
464
726
  continue;
465
727
  }
466
- if (typeof card !== 'function') {
467
- problems.push(`${name}: 清单写了 card,apply 之后却没登记卡片(registerUnit 没拿到 card)`);
728
+ if (typeof card !== 'function' && !hasView) {
729
+ problems.push(`${name}: 清单写了 card,apply 之后却没登记卡片 / view(registerUnit 没拿到 card)`);
468
730
  continue;
469
731
  }
470
732
 
@@ -478,6 +740,48 @@ export async function run() {
478
740
  onSummary: () => {},
479
741
  };
480
742
 
743
+ let schemaCard;
744
+ if (hasView && realKit !== undefined && hasShell) {
745
+ try {
746
+ schemaCard = shellSchemaCardOf(shellClient, { react: fakeReact(), dshPackage: fakeDshPackage() });
747
+ } catch (error) {
748
+ shellProblem = error instanceof Error ? error.message : String(error);
749
+ }
750
+ }
751
+
752
+ if (hasView && schemaCard === undefined) {
753
+ const why =
754
+ shellProblem !== undefined
755
+ ? `取不出这份外壳的 schema 探针(${shellClient ?? '没有外壳'})—— ${shellProblem}`
756
+ : '这份外壳还没导出 schemaCard 探针';
757
+ footnotes.push(`${name}: ${why} —— schema 卡逐态渲染跳过`);
758
+ rows.push(`${name}: schema 卡没有探针,单次全桩渲染也跳过`);
759
+ degraded += 1;
760
+ continue;
761
+ }
762
+
763
+ const renderCard =
764
+ schemaCard === undefined
765
+ ? card
766
+ : (p) => {
767
+ const snapshot = current.snapshot;
768
+ const capability = snapshot.capability?.(unitId) ?? null;
769
+ const blocker =
770
+ capability !== null
771
+ ? realKit.lib.blockerOfCapability(capability, p.t)
772
+ : snapshot.resource.phase === 'error'
773
+ ? realKit.lib.blockerOfError(snapshot.resource.error, p.t)
774
+ : undefined;
775
+ return schemaCard({
776
+ ...p,
777
+ unit: unitId,
778
+ view,
779
+ state: snapshot.resource,
780
+ refreshing: snapshot.resource.phase === 'loading',
781
+ ...(blocker === undefined ? {} : { blocker }),
782
+ });
783
+ };
784
+
481
785
  if (realKit === undefined) {
482
786
  const why =
483
787
  shellProblem !== undefined
@@ -487,7 +791,7 @@ export async function run() {
487
791
  : '没装外壳(@dsh-workbench/core)';
488
792
  let tree;
489
793
  try {
490
- tree = card(props);
794
+ tree = renderCard(props);
491
795
  } catch (error) {
492
796
  problems.push(`${name}: 空数据下卡片抛了 —— ${error.message}`);
493
797
  continue;
@@ -545,7 +849,7 @@ export async function run() {
545
849
 
546
850
  let tree;
547
851
  try {
548
- tree = card(props);
852
+ tree = renderCard(props);
549
853
  } catch (error) {
550
854
  problems.push(`${name}: ${where} 卡片抛了 —— ${error.message}`);
551
855
  continue;
@@ -624,6 +928,8 @@ export async function run() {
624
928
  }
625
929
  }
626
930
 
931
+ const schemaProbeRan = runSchemaProbe(modulePackages, shellDictionaries, problems, rows, footnotes);
932
+
627
933
  const shellClients = shellDictionaries === undefined ? [] : shellClientsOf(modulePackages);
628
934
 
629
935
  if (problems.length > 0) {
@@ -639,7 +945,7 @@ export async function run() {
639
945
  return {
640
946
  status: 'pass',
641
947
  notes: [
642
- `${rows.length} 个模块过了卡片这一关:` +
948
+ `${modulePackages.length} 个模块${schemaProbeRan ? ' + 1 条 schema 探针' : ''}过了卡片这一关:` +
643
949
  (degraded === 0
644
950
  ? `${SNAPSHOTS.length} 态 × zh/en 各渲染一遍`
645
951
  : `逐态那半这次有 ${degraded} 个模块跳过(没装外壳 / 取不出外壳,见下),其余 ${SNAPSHOTS.length} 态 × zh/en 各渲染一遍`) +
@@ -31,7 +31,7 @@ export function shellClientPathOf(pkgDir) {
31
31
  }
32
32
  }
33
33
 
34
- export function shellKitOf(clientPath, options = {}) {
34
+ function loadShellExports(clientPath, options = {}) {
35
35
  const react = options.react ?? defaultReact();
36
36
  const seed = options.dshPackage ?? defaultSeed();
37
37
  let exports;
@@ -47,5 +47,13 @@ export function shellKitOf(clientPath, options = {}) {
47
47
  };
48
48
  const window = { __ModuleLoader__: { load } };
49
49
  new Function('window', readFileSync(clientPath, 'utf8'))(window);
50
- return exports?.kit ?? {};
50
+ return exports;
51
+ }
52
+
53
+ export function shellKitOf(clientPath, options = {}) {
54
+ return loadShellExports(clientPath, options)?.kit ?? {};
55
+ }
56
+
57
+ export function shellSchemaCardOf(clientPath, options = {}) {
58
+ return loadShellExports(clientPath, options)?.schemaCard;
51
59
  }
@@ -1,17 +1,15 @@
1
1
  import type { DshToolDefinition, UnitMountDeps } from '@dsh-workbench/core/contract';
2
2
  import {
3
3
  TEXT_OUTPUT,
4
- longTaskStepOf,
4
+ createFlowTask,
5
5
  recordOf,
6
- sessionIdOf,
7
- sessionWorkspace,
8
6
  textOf,
9
7
  type LongTaskBase,
10
8
  type LongTaskRegistry,
11
9
  } from '@dsh-workbench/shared/host';
12
10
  import { LIST_LIMIT } from '../model/overview';
13
11
  import { DECLARED_TOOLS } from '../declared-tools';
14
- import { FLOW_TARGET, RUN_TASK, headlineOf, sampleFlow } from '../model/flow';
12
+ import { RUN_TASK, headlineOf, sampleFlow } from '../model/flow';
15
13
  import { readOverview } from './overview';
16
14
 
17
15
  export interface StartResult {
@@ -34,6 +32,8 @@ export interface Sample {
34
32
  export function createSample(deps: UnitMountDeps): Sample {
35
33
  const flowDefinition = sampleFlow();
36
34
  const actions = deps.flow.createActions();
35
+ const runDescription =
36
+ '跑一遍样板流程(读目录 → 汇总);只读本机目录,不写平台,不用点头;再传 runId 可只看同一趟。';
37
37
 
38
38
  actions.register(
39
39
  'read',
@@ -55,85 +55,36 @@ export function createSample(deps: UnitMountDeps): Sample {
55
55
  { title: '汇总' },
56
56
  );
57
57
 
58
- const checked = deps.flow.check(flowDefinition, actions);
59
- if (!checked.ok) {
60
- throw new Error(
61
- `流程定义有问题:${checked.problems.map((one) => `${one.path}: ${one.problem}`).join(';')}`,
62
- );
63
- }
64
-
65
- const runs = deps.longTasks?.create<LongTaskBase>({
66
- key: RUN_TASK,
67
- run: async ({ payload, onStep, signal }) => {
68
- const path = textOf(recordOf(payload)['path']);
69
- const result = await deps.flow.run({
70
- flow: flowDefinition,
71
- input: { path },
72
- actions,
73
- signal,
74
- runId: `${DECLARED_TOOLS.run.name}-${String(Date.now())}`,
75
- workspace: path,
76
- onStep: (event) => {
77
- onStep(longTaskStepOf(event));
78
- },
79
- });
80
- if (result.status === 'failed') {
81
- throw result.error instanceof Error
82
- ? result.error
83
- : new Error(`流程失败在 ${result.failedStep ?? '未知步骤'}`);
84
- }
85
- return result;
86
- },
87
- step: (step) => ({ step: step.text }),
88
- outcome: () => ({ step: '完成:读目录 → 汇总' }),
89
- stopped: (current) => ({ step: current.step === '' ? '已停止' : `停在「${current.step}」` }),
90
- view: (snapshot) => ({
91
- status: snapshot.status,
92
- step: snapshot.step,
93
- steps: snapshot.steps,
94
- ...(snapshot.why === undefined ? {} : { why: snapshot.why }),
95
- }),
96
- });
58
+ const flowTask =
59
+ deps.longTasks === undefined
60
+ ? undefined
61
+ : createFlowTask({
62
+ key: RUN_TASK,
63
+ flow: flowDefinition,
64
+ actions,
65
+ flowHost: deps.flow,
66
+ longTasks: deps.longTasks,
67
+ tool: { name: DECLARED_TOOLS.run.name, description: runDescription },
68
+ });
69
+ const runs = flowTask?.runs;
97
70
 
98
71
  const start = (workspace: string, session: string | null): StartResult => {
99
- if (runs === undefined) return { ok: false, text: '外壳没有给长任务服务:这一版起不了流程' };
100
- const run = runs.start({
72
+ if (flowTask === undefined) return { ok: false, text: '外壳没有给长任务服务:这一版起不了流程' };
73
+ const run = flowTask.start({
101
74
  workspace,
102
- target: FLOW_TARGET,
103
- payload: { path: workspace },
75
+ input: { path: workspace },
104
76
  ...(session === null ? {} : { session }),
105
77
  });
106
78
  return { ok: true, runId: run.id, status: run.status, text: headlineOf(run) };
107
79
  };
108
80
 
109
- const runTool: DshToolDefinition = {
81
+ const runTool: DshToolDefinition = flowTask?.tool ?? {
110
82
  name: DECLARED_TOOLS.run.name,
111
- description:
112
- '跑一遍样板流程(读目录 → 汇总);只读本机目录,不写平台,不用点头;再传 runId 可只看同一趟。',
113
- parameters: {
114
- type: 'object',
115
- properties: {
116
- runId: {
117
- type: 'string',
118
- description: '填上一次的 runId = 只看那条;不填 = 起一条新的。',
119
- },
120
- },
121
- additionalProperties: false,
122
- },
83
+ description: runDescription,
84
+ parameters: { type: 'object', properties: {}, additionalProperties: true },
123
85
  output: TEXT_OUTPUT,
124
- async execute(raw, exec) {
125
- const args = recordOf(raw);
126
- const runId = textOf(args['runId']);
127
- if (runId !== '') {
128
- const snapshot = runs?.snapshot(runId) ?? null;
129
- if (snapshot === null) return { ok: false, text: `没有这条 run:${runId}` };
130
- return { ok: true, runId, status: snapshot.status, text: headlineOf(snapshot) };
131
- }
132
- const workspace = sessionWorkspace(exec);
133
- if (workspace === null || workspace === '') {
134
- return { ok: false, text: '这个会话没有工作区:先打开一个项目再跑。' };
135
- }
136
- return start(workspace, sessionIdOf(exec));
86
+ async execute() {
87
+ return { ok: false, text: '外壳没有给长任务服务:这一版起不了流程' };
137
88
  },
138
89
  };
139
90