@shgroup/dsh-serenity-hooks 1.50.1 → 1.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 标签式图表 DSL —— 解析器(只收集,不做几何)。
3
+ *
4
+ * 为什么是标签形式(owner 2026-09-29 裁决):图是正文的附加层,一段
5
+ * <diagram>…</diagram> 嵌在文档里就能出图,作者不必为画一张图切到另一种文档结构去维护坐标;
6
+ * 属性 / 文本双形态(<rect .../> 声明几何,<text ...>正文</text> 承载文案)覆盖两类信息,
7
+ * 与 HTML/SVG 直觉一致,人写机器读都省事;自闭合 + 未知标签只报错不崩 = 前向兼容。
8
+ * 本文件只「收集」:不解析锚点(`ID.锚点[±dx,±dy]`)、不做几何——那是渲染侧的事;绝不抛异常。
9
+ */
10
+ export interface DslRect {
11
+ id: string;
12
+ x0: number;
13
+ y0: number;
14
+ x1: number;
15
+ y1: number;
16
+ label?: string;
17
+ size: number;
18
+ fill: string;
19
+ }
20
+ export interface DslSeg {
21
+ kind: 'line' | 'arrow';
22
+ from: string;
23
+ to: string;
24
+ color?: string;
25
+ width?: number;
26
+ dash?: string;
27
+ free: boolean;
28
+ ln: number;
29
+ }
30
+ export interface DslText {
31
+ x: number;
32
+ y: number;
33
+ text: string;
34
+ align: 'left' | 'center' | 'right';
35
+ size: number;
36
+ color?: string;
37
+ ln: number;
38
+ }
39
+ export interface DslLegend {
40
+ x: number;
41
+ y: number;
42
+ size: number;
43
+ }
44
+ export interface DslModel {
45
+ W: number;
46
+ H: number;
47
+ x0: number;
48
+ y0: number;
49
+ x1: number;
50
+ y1: number;
51
+ pad: number;
52
+ title: string;
53
+ subtitle: string;
54
+ colors: Record<string, string>;
55
+ rects: DslRect[];
56
+ segments: DslSeg[];
57
+ texts: DslText[];
58
+ legend?: DslLegend;
59
+ }
60
+ export interface DslParse {
61
+ model: DslModel;
62
+ errors: string[];
63
+ }
64
+ /** 解析标签格式源码。**绝不抛异常**:看不懂的行记进 errors 并跳过,其余照收。 */
65
+ export declare function parseDsl(source: string): DslParse;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * diagram 字体事实源(**唯一真相源**:资产路径 + 家族名 + CSS 家族栈)
3
+ *
4
+ * 为什么单独成模块(R↓):两处都要它,而它们的依赖面**必须**保持不同 ——
5
+ * · `dsl-render.ts` 只生成 SVG 字符串(**纯函数、零 fs**),它需要在 SVG 里写 `font-family` 栈;
6
+ * · `rasterize.ts` 要读字体文件喂给 wasm,并设 `defaultFontFamily`。
7
+ * ⇒ 谁都不该从对方 import(前者会被拖进 `node:fs`/wasm,后者不该定义 SVG 措辞)。
8
+ *
9
+ * ## 为什么是**三个**字型(每一个都由一张实测图证明,不是偏好,也不是"多带点保险")
10
+ *
11
+ * | 字型 | 覆盖 | 它修好了什么(实测) |
12
+ * |---|---|---|
13
+ * | `DroidSansFallbackFull.ttf` | 汉字 | 没有它 ⇒ 中文全是方框(docker 实测:wasm 对系统字体**完全不可见**) |
14
+ * | `NotoSans-Regular.ttf` | 拉丁 | 没有它 ⇒ `diagram` / `parseDsl` 全变方框(Droid 是 **CJK 兜底字型**,**不含拉丁**) |
15
+ * | `DejaVuSans.ttf` | 符号 | 没有它 ⇒ `① ② ③` / `⇒ ⇒` / `→` 仍是方框(**前两个都没有这些字形**,实测对照图见 `docs/diagram-tool-feasibility.md`) |
16
+ *
17
+ * 🔴 **上一版的判据漏洞**(v1.51.1 修的):那条"有字 vs 无字逐字节不同"只证明**有墨**,
18
+ * 没证明**墨是那个字** —— 方框也是墨 ⇒ 拉丁全变方框时它**照样绿**。
19
+ * ⇒ 现在每条覆盖声明都配一条**点名字符集**的差分用例:同长度不同字形 ⇒ 必须出不同墨。
20
+ *
21
+ * ## 家族名是**量出来的**,不是猜的
22
+ * 下表取自各文件 `name` 表 `nameID=1`(平台 3 / UTF-16BE):
23
+ * `Droid Sans Fallback` / `Noto Sans` / `DejaVu Sans`。
24
+ * 写错名字时 resvg 的自动回退仍可能把字画出来(**图上看不出来**)⇒ 名字正确与否只能这样证明。
25
+ */
26
+ export declare const DIAGRAM_CJK_FONT_ASSET = "assets/diagram/DroidSansFallbackFull.ttf";
27
+ export declare const DIAGRAM_LATIN_FONT_ASSET = "assets/diagram/NotoSans-Regular.ttf";
28
+ export declare const DIAGRAM_SYMBOL_FONT_ASSET = "assets/diagram/DejaVuSans.ttf";
29
+ /** 汉字字型的家族名(**字体文件自报的族名**,写错就取不到) */
30
+ export declare const DIAGRAM_CJK_FONT_FAMILY = "Droid Sans Fallback";
31
+ /** 拉丁字型的家族名 */
32
+ export declare const DIAGRAM_LATIN_FONT_FAMILY = "Noto Sans";
33
+ /** 符号字型的家族名 */
34
+ export declare const DIAGRAM_SYMBOL_FONT_FAMILY = "DejaVu Sans";
35
+ /**
36
+ * SVG 里写的家族栈(**顺序即回退顺序**):拉丁优先取 Noto,符号落 DejaVu,其余字形落到 Droid。
37
+ * ⚠️ 末尾的通用名(`sans-serif`)只是兜底惯例:本部署里 resvg **不带系统字体**
38
+ * (`loadSystemFonts: false`)⇒ 它取不到任何东西,真正的兜底是上面三个内置字型。
39
+ */
40
+ export declare const DIAGRAM_FONT_STACK = "'Noto Sans', 'DejaVu Sans', 'Droid Sans Fallback', sans-serif";
@@ -0,0 +1,35 @@
1
+ /** 汉字字型资产(相对包根的路径)——**必须出现在 package.json 的 files 白名单里**(pack-check 会核对) */
2
+ export declare const DIAGRAM_FONT_ASSET = "assets/diagram/DroidSansFallbackFull.ttf";
3
+ /** 拉丁字型资产(v1.51.1 新增:CJK 字型**不含拉丁** ⇒ 只带一个时英文字母全渲染成方框) */
4
+ export declare const DIAGRAM_FONT_ASSET_LATIN = "assets/diagram/NotoSans-Regular.ttf";
5
+ /** 符号字型资产(v1.51.1 新增:前两个**都没有** `① ② ③` / `⇒` / `→` 的字形 ⇒ 只带前两个时它们仍是方框) */
6
+ export declare const DIAGRAM_FONT_ASSET_SYMBOL = "assets/diagram/DejaVuSans.ttf";
7
+ /** 汉字字型绝对路径(导出以便测试与 pack-check 共用同一判据来源) */
8
+ export declare function diagramFontPath(): string;
9
+ /** 拉丁字型绝对路径 */
10
+ export declare function diagramLatinFontPath(): string;
11
+ /** 符号字型绝对路径 */
12
+ export declare function diagramSymbolFontPath(): string;
13
+ /**
14
+ * 兜底字型的族名 = **汉字字型**(本工具的图以中文为主)。
15
+ * 两个内置字型的家族名与 CSS 家族栈统一定义在 `fonts.ts`(单一真相源),此处只透出兜底那一个。
16
+ */
17
+ export declare const DIAGRAM_FONT_FAMILY = "Droid Sans Fallback";
18
+ export interface RasterResult {
19
+ png: Uint8Array;
20
+ /** 实际输出像素宽(= `fitTo width` 的结果;供工具回执如实报尺寸) */
21
+ width: number;
22
+ /** 实际输出像素高(按 SVG 自身比例推出) */
23
+ height: number;
24
+ /** 渲染耗时(ms)—— 供工具自述与诊断用 */
25
+ ms: number;
26
+ }
27
+ /**
28
+ * SVG → PNG。
29
+ * @param width 输出像素宽(矢量无损放大;高度按 SVG 自身比例推出)
30
+ */
31
+ export declare function rasterizeSvg(svg: string, width: number): Promise<RasterResult>;
32
+ /** 测试用:清本模块的缓存(**三个字体缓冲 + 就绪 promise**)。
33
+ * ⚠️ **wasm 本身无法"重置"**(`initWasm` 进程级一次性)⇒ 清掉后下一次 `rasterizeSvg`
34
+ * 会**重新走一遍 init 并命中共用态**("already initialized" 由 `ensureReady` 吞掉)。 */
35
+ export declare function resetRasterCache(): void;
@@ -82,7 +82,7 @@ export declare function buildRoundPrompt(opts: {
82
82
  * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
83
83
  * 阅读/文字编写类 handyman 内部也加载 eap。
84
84
  */
85
- export declare const HANDYMAN_GUIDE = "# handyman \u2014 Scale-Up Usage Guide (guide)\n\n## Two modes (v1.31.3; default = foreground)\n| | foreground (default) | background |\n|---|---|---|\n| What | child agent(s) run the task ONCE and return their final text (jobs=[...] \u21D2 parallel, cap 5) | loop-validated worker: hard while-loop + random completion code |\n| Use when | you need result(s) back now (per-case testers, single conversions, parallel fan-out) | long/unattended work needing anti-early-finish, resumability, or parallel jobs |\n| Loop / validation | none | stop-token is the ONLY completion condition; round cap (default 100); auto-restart (\u2264100) |\n| Progress file | none | AGENT_SESSIONS/handyman-<label>.md/.json (same label resumes) |\n| Parallel jobs | **yes** \u2014 jobs=[{task,label?,model?},...], **cap 5 (fixed)** | yes (jobs=[...], handyman.maxParallel default 10) |\n| Mechanism | host delegation service ctx.subagents.start(\"spawn\") + agentOptions | ctx.agents.create + internal while-loop |\n| Model source | CCC handyman.models whitelist (same for both) | same |\n\n## \u26A0\uFE0F Before using: load eap and design the plan\nBefore calling handyman, load eap (acc-eap skill) and design the \"scale-up handyman plan\" based on the EAP framework.\n\n### 1. Task decomposition (E\u2191 Explicit)\n- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria\n- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel (both modes)\n\n### 2. Prompt design (handyman's task parameter)\n- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria\n- foreground mode: the child does NOT share your conversation \u2014 restate everything it needs in task\n- Anti-example \"handle this file\" \u2014 ambiguous; good example \"read <path>, extract all rows of the\u300C\u5173\u952E\u51B3\u7B56\u300Dtable,\n output a JSON array (fields id/conclusion/evidence), do not modify the original file\"\n- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):\n the handyman-internal agent is also required to load eap and organize output per the EAP standard\n\n### 3. Model whitelist (CCC-configured, mandatory)\n- handyman only uses models listed in .opencode/serenity.json \"handyman.models\" \u2014 never arbitrary models\n- The CCC is expected to point this whitelist at LOW-COST models: handyman is the bulk-execution channel\n- background mode: recursive subagents inside a worker inherit the worker's model automatically (DSH native)\n- foreground mode: the child is started through the host delegation service with the resolved model\n\n### 3b. \uD83D\uDD34 Provider availability (per-machine \u2014 the whitelist is NOT enough)\n- The whitelist lives in the **CCC** (\"serenity.json\", it **travels with the repository**), but provider\n **adapters are registered per machine by the host** (profile config / settings namespace). An entry that\n works on one machine can fail on another with: \"no adapter registered for provider <p>\".\n- Before spawning, handyman verifies the provider of the resolved model against the adapters registered\n **on this machine**; if it is missing it reports the requested model, where it is configured, **the\n providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the\n exact fix \u2014 foreground puts this in \"diagnostic\", background throws it.\n- \u21D2 If you see \"has no adapter registered on this machine\": edit \".opencode/serenity.json\" \"handyman.models\"\n (and \"handyman.defaultModel\") to a provider listed as available, or activate/register the provider on this\n machine. **Do not retry the same call unchanged** \u2014 it fails identically.\n\n### 4. Parallel strategy (both modes)\n- Independent subtasks can run in parallel:\n \u00B7 **foreground** \u2014 handyman(jobs=[{task,label?,model?},...]) \u21D2 fan-out of custom-model subagents, **cap 5**,\n results come back **in the order you passed them** (label is only a display name here; per-job \"model\" allowed)\n \u00B7 **background** \u2014 handyman(mode=\"background\", jobs=[...]): each job gets its own label + task + stop token + progress file\n- Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label\n (AGENT_SESSIONS/handyman-<label>.json) \u2014 same label resumes, different labels never interfere\n- Parallel cap: **foreground 5 (fixed)** / background handyman.maxParallel (default 10 \u2014 cheap models are cheap)\n- Aggregation: foreground returns all outputs in one result; background: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)\n- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead\n\n## Completion criteria\n- foreground: the child's run settles; done=true only when stopReason=\"completed\" (otherwise read diagnostic)\n- background (osp loop standard): the only completion condition = the worker echoes this round's random verification\n code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable);\n automatic restart on abnormal agent stop (\u2264100 restarts, anti-infinite-loop)\n\n## Waiting UI\n- The WebUI session-header Serenity detail card shows running background handymen's progress\n (label / round / last response), one line per parallel job, ~3s refresh (foreground mode writes no progress)\n";
85
+ export declare const HANDYMAN_GUIDE = "# handyman \u2014 Scale-Up Usage Guide (guide)\n\n## Two modes (v1.31.3; default = foreground)\n| | foreground (default) | background |\n|---|---|---|\n| What | child agent(s) run the task ONCE and return their final text (jobs=[...] \u21D2 parallel, cap 10) | loop-validated worker: hard while-loop + random completion code |\n| Use when | you need result(s) back now (per-case testers, single conversions, parallel fan-out) | long/unattended work needing anti-early-finish, resumability, or parallel jobs |\n| Loop / validation | none | stop-token is the ONLY completion condition; round cap (default 100); auto-restart (\u2264100) |\n| Progress file | none | AGENT_SESSIONS/handyman-<label>.md/.json (same label resumes) |\n| Parallel jobs | **yes** \u2014 jobs=[{task,label?,model?},...], **cap 10 (fixed)** | yes (jobs=[...], handyman.maxParallel default 10) |\n| Mechanism | host delegation service ctx.subagents.start(\"spawn\") + agentOptions | ctx.agents.create + internal while-loop |\n| Model source | CCC handyman.models whitelist (same for both) | same |\n\n## \u26A0\uFE0F Before using: load eap and design the plan\nBefore calling handyman, load eap (acc-eap skill) and design the \"scale-up handyman plan\" based on the EAP framework.\n\n### 1. Task decomposition (E\u2191 Explicit)\n- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria\n- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel (both modes)\n\n### 2. Prompt design (handyman's task parameter)\n- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria\n- foreground mode: the child does NOT share your conversation \u2014 restate everything it needs in task\n- Anti-example \"handle this file\" \u2014 ambiguous; good example \"read <path>, extract all rows of the\u300C\u5173\u952E\u51B3\u7B56\u300Dtable,\n output a JSON array (fields id/conclusion/evidence), do not modify the original file\"\n- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):\n the handyman-internal agent is also required to load eap and organize output per the EAP standard\n\n### 3. Model whitelist (CCC-configured, mandatory)\n- handyman only uses models listed in .opencode/serenity.json \"handyman.models\" \u2014 never arbitrary models\n- The CCC is expected to point this whitelist at LOW-COST models: handyman is the bulk-execution channel\n- background mode: recursive subagents inside a worker inherit the worker's model automatically (DSH native)\n- foreground mode: the child is started through the host delegation service with the resolved model\n\n### 3b. \uD83D\uDD34 Provider availability (per-machine \u2014 the whitelist is NOT enough)\n- The whitelist lives in the **CCC** (\"serenity.json\", it **travels with the repository**), but provider\n **adapters are registered per machine by the host** (profile config / settings namespace). An entry that\n works on one machine can fail on another with: \"no adapter registered for provider <p>\".\n- Before spawning, handyman verifies the provider of the resolved model against the adapters registered\n **on this machine**; if it is missing it reports the requested model, where it is configured, **the\n providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the\n exact fix \u2014 foreground puts this in \"diagnostic\", background throws it.\n- \u21D2 If you see \"has no adapter registered on this machine\": edit \".opencode/serenity.json\" \"handyman.models\"\n (and \"handyman.defaultModel\") to a provider listed as available, or activate/register the provider on this\n machine. **Do not retry the same call unchanged** \u2014 it fails identically.\n\n### 4. Parallel strategy (both modes)\n- Independent subtasks can run in parallel:\n \u00B7 **foreground** \u2014 handyman(jobs=[{task,label?,model?},...]) \u21D2 fan-out of custom-model subagents, **cap 10**,\n results come back **in the order you passed them** (label is only a display name here; per-job \"model\" allowed)\n \u00B7 **background** \u2014 handyman(mode=\"background\", jobs=[...]): each job gets its own label + task + stop token + progress file\n- Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label\n (AGENT_SESSIONS/handyman-<label>.json) \u2014 same label resumes, different labels never interfere\n- Parallel cap: **foreground 10 (fixed)** / background handyman.maxParallel (default 10 \u2014 cheap models are cheap)\n- Aggregation: foreground returns all outputs in one result; background: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)\n- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead\n\n## Completion criteria\n- foreground: the child's run settles; done=true only when stopReason=\"completed\" (otherwise read diagnostic)\n- background (osp loop standard): the only completion condition = the worker echoes this round's random verification\n code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable);\n automatic restart on abnormal agent stop (\u2264100 restarts, anti-infinite-loop)\n\n## Waiting UI\n- The WebUI session-header Serenity detail card shows running background handymen's progress\n (label / round / last response), one line per parallel job, ~3s refresh (foreground mode writes no progress)\n";
86
86
  /** handyman 运行状态(进度文件摘要;WebUI 等待界面数据源) */
87
87
  interface HandymanRunInfo {
88
88
  label: string;
package/lib/index.d.ts CHANGED
@@ -156,6 +156,13 @@ export interface Config {
156
156
  visionPatch?: {
157
157
  enabled?: boolean;
158
158
  };
159
+ /**
160
+ * diagram 工具(实验性 demo;2026-09-29 owner 具名令 **D104**):标签格式源码 → 进程内
161
+ * 光栅化成 PNG(**不借浏览器**)。缺省关;部署层同义键 = 面板层 `diagramEnabled`。
162
+ */
163
+ diagram?: {
164
+ enabled?: boolean;
165
+ };
159
166
  /** human-channel(人机信道;**机器级**;2026-09-26 owner 定案)—— 见 `HumanChannelConfig` */
160
167
  humanChannel?: HumanChannelConfig;
161
168
  /** F1 双端口网关总开关(面板层;部署层同义键 = `gateway.enabled`) */
@@ -176,6 +183,8 @@ export interface Config {
176
183
  croEnabled?: Volatile<boolean | undefined>;
177
184
  /** 无人值守代理回复总闸(面板层;无部署层同义键;缺省关) */
178
185
  unattendedEnabled?: Volatile<boolean | undefined>;
186
+ /** **diagram 工具总闸**(面板层;部署层同义键 = `diagram.enabled`;缺省关;D104) */
187
+ diagramEnabled?: Volatile<boolean | undefined>;
179
188
  }
180
189
  /** schemastery 的 schema 类型(别名只为可读;**名字取自全局命名空间**,见上方 TS2742 说明) */
181
190
  type SchemaOf<S, T = S> = Schemastery<S, T>;
@@ -234,6 +243,11 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
234
243
  }>>, Schemastery.ObjectT<NoInfer<{
235
244
  enabled: z<boolean, boolean, "defined">;
236
245
  }>>, "plain">;
246
+ diagram: z<Schemastery.ObjectS<NoInfer<{
247
+ enabled: z<boolean, boolean, "defined">;
248
+ }>>, Schemastery.ObjectT<NoInfer<{
249
+ enabled: z<boolean, boolean, "defined">;
250
+ }>>, "plain">;
237
251
  humanChannel: z<Schemastery.ObjectS<NoInfer<{
238
252
  accounts: z<HumanChannelAccountConfig[], HumanChannelAccountConfig[], "defined">;
239
253
  subscriptions: z<HumanChannelSubscriptionConfig[], HumanChannelSubscriptionConfig[], "defined">;
@@ -270,6 +284,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
270
284
  publicAskEnabled: z<boolean, boolean, "volatile">;
271
285
  croEnabled: z<boolean, boolean, "volatile">;
272
286
  unattendedEnabled: z<boolean, boolean, "volatile">;
287
+ diagramEnabled: z<boolean, boolean, "volatile">;
273
288
  }>>, Schemastery.ObjectT<NoInfer<{
274
289
  serenityConfigPaths: z<string[], string[], "defined">;
275
290
  tools: z<boolean, boolean, "defined">;
@@ -321,6 +336,11 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
321
336
  }>>, Schemastery.ObjectT<NoInfer<{
322
337
  enabled: z<boolean, boolean, "defined">;
323
338
  }>>, "plain">;
339
+ diagram: z<Schemastery.ObjectS<NoInfer<{
340
+ enabled: z<boolean, boolean, "defined">;
341
+ }>>, Schemastery.ObjectT<NoInfer<{
342
+ enabled: z<boolean, boolean, "defined">;
343
+ }>>, "plain">;
324
344
  humanChannel: z<Schemastery.ObjectS<NoInfer<{
325
345
  accounts: z<HumanChannelAccountConfig[], HumanChannelAccountConfig[], "defined">;
326
346
  subscriptions: z<HumanChannelSubscriptionConfig[], HumanChannelSubscriptionConfig[], "defined">;
@@ -357,6 +377,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
357
377
  publicAskEnabled: z<boolean, boolean, "volatile">;
358
378
  croEnabled: z<boolean, boolean, "volatile">;
359
379
  unattendedEnabled: z<boolean, boolean, "volatile">;
380
+ diagramEnabled: z<boolean, boolean, "volatile">;
360
381
  }>>, "plain">;
361
382
  export declare function apply(ctx: Context, config: Config): void;
362
383
  export {};