diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index df716601..6e49ae7a 100644 --- a/internal/cli/schema_agent_metadata/index.json +++ b/internal/cli/schema_agent_metadata/index.json @@ -1,6 +1,6 @@ { "version": 1, - "source_hash": "sha256:98a61c88e23f4c223f4aae341f0d812545f6154cf18736e1f90bed88b6b47bca", + "source_hash": "sha256:2fb34ad56cc801af92a057aeda1b8af7e886f6d129bedb643b8d08e4f59c5177", "surface_hash": "sha256:942309e0671ad009203266b2d6dade23a0afbbda5de77df393964b0e5cef39f6", "coverage": { "surface_products": 27, diff --git a/internal/cli/schema_agent_metadata/whiteboard.json b/internal/cli/schema_agent_metadata/whiteboard.json index f563d363..a207b84b 100644 --- a/internal/cli/schema_agent_metadata/whiteboard.json +++ b/internal/cli/schema_agent_metadata/whiteboard.json @@ -278,7 +278,8 @@ "dws-wukong@e2da8ab947c6:wukong/products/whiteboard.go", "internal/cli/schema_hints/metadata/whiteboard.json", "internal/cli/schema_hints/selection/whiteboard.json", - "skills/mono/references/products/whiteboard/open-nodes-v1/02-query.md" + "skills/mono/references/products/whiteboard/open-nodes-v1/02-query.md", + "skills/mono/references/products/whiteboard/recipes.md" ], "use_when": [ "已知承载文档 nodeId 和白板 partId,需要检查当前白板节点、布局或写入支持时" @@ -363,6 +364,12 @@ "precedence": "reviewed_explicit", "selected": true, "review_reason": "Migrated from dws-wukong e2da8ab947c6 and tightened for the public command-level safety model: every remote whiteboard update requires --yes." + }, + { + "value": "not_required", + "source": "risk-default", + "precedence": "inference_or_default", + "selected": false } ] }, @@ -379,6 +386,12 @@ "precedence": "reviewed_explicit", "selected": true, "review_reason": "Migrated from dws-wukong e2da8ab947c6 and tightened for the public command-level safety model: every remote whiteboard update requires --yes." + }, + { + "value": "write", + "source": "command-verb", + "precedence": "inference_or_default", + "selected": false } ] }, @@ -415,6 +428,12 @@ "precedence": "reviewed_explicit", "selected": true, "review_reason": "Migrated from dws-wukong e2da8ab947c6 and tightened for the public command-level safety model: every remote whiteboard update requires --yes." + }, + { + "value": "unknown", + "source": "effect-default", + "precedence": "inference_or_default", + "selected": false } ] }, @@ -502,6 +521,12 @@ "precedence": "reviewed_explicit", "selected": true, "review_reason": "Migrated from dws-wukong e2da8ab947c6 and tightened for the public command-level safety model: every remote whiteboard update requires --yes." + }, + { + "value": "medium", + "source": "effect-default", + "precedence": "inference_or_default", + "selected": false } ] }, @@ -536,7 +561,8 @@ "cobra-help:dws whiteboard update", "dws-wukong@e2da8ab947c6:wukong/products/whiteboard.go", "internal/cli/schema_hints/metadata/whiteboard.json", - "internal/cli/schema_hints/selection/whiteboard.json" + "internal/cli/schema_hints/selection/whiteboard.json", + "skills/mono/references/products/whiteboard/recipes.md" ], "use_when": [ "已有 nodeId、partId 和合规 OpenNodes V1 文件,用户确认后要追加图形、文本、连接线或整页替换时" diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index a581b57f..d3c81010 100644 --- a/internal/cli/schema_agent_metadata_audit.json +++ b/internal/cli/schema_agent_metadata_audit.json @@ -1,8 +1,8 @@ { "version": 1, - "source_hash": "sha256:98a61c88e23f4c223f4aae341f0d812545f6154cf18736e1f90bed88b6b47bca", + "source_hash": "sha256:2fb34ad56cc801af92a057aeda1b8af7e886f6d129bedb643b8d08e4f59c5177", "surface_hash": "sha256:942309e0671ad009203266b2d6dade23a0afbbda5de77df393964b0e5cef39f6", - "source_files": 174, + "source_files": 175, "hint_files": 56, "hint_products": 44, "hint_tools": 1860, diff --git a/internal/cli/schema_catalog/catalog.json b/internal/cli/schema_catalog/catalog.json index 537306cc..ad8de016 100644 --- a/internal/cli/schema_catalog/catalog.json +++ b/internal/cli/schema_catalog/catalog.json @@ -1,12 +1,12 @@ { "version": 1, "surface_hash": "sha256:942309e0671ad009203266b2d6dade23a0afbbda5de77df393964b0e5cef39f6", - "source_hash": "sha256:ed39f80f8291a9e3098941cfde122cd536b574a50614fbbff302dbb6a8653ead", + "source_hash": "sha256:f9e4ac0213cd4863b20cfb1af7d680662070020c4e2be2d9aa40fce3b7ea5702", "catalog": { "agent_metadata": { "products_with_metadata": 27, "source": "embedded-skill-metadata", - "source_hash": "sha256:98a61c88e23f4c223f4aae341f0d812545f6154cf18736e1f90bed88b6b47bca", + "source_hash": "sha256:2fb34ad56cc801af92a057aeda1b8af7e886f6d129bedb643b8d08e4f59c5177", "surface_hash": "sha256:942309e0671ad009203266b2d6dade23a0afbbda5de77df393964b0e5cef39f6", "surface_products": 27, "surface_tools": 854, diff --git a/internal/cli/schema_catalog/tools/whiteboard.json b/internal/cli/schema_catalog/tools/whiteboard.json index a14719c4..996c3b26 100644 --- a/internal/cli/schema_catalog/tools/whiteboard.json +++ b/internal/cli/schema_catalog/tools/whiteboard.json @@ -9,7 +9,8 @@ "dws-wukong@e2da8ab947c6:wukong/products/whiteboard.go", "internal/cli/schema_hints/metadata/whiteboard.json", "internal/cli/schema_hints/selection/whiteboard.json", - "skills/mono/references/products/whiteboard/open-nodes-v1/02-query.md" + "skills/mono/references/products/whiteboard/open-nodes-v1/02-query.md", + "skills/mono/references/products/whiteboard/recipes.md" ], "agent_summary": "读取钉钉文档内已有白板的 OpenNodes 内容", "agent_summary_source": "dws-agent-selection/whiteboard", @@ -556,7 +557,8 @@ "cobra-help:dws whiteboard update", "dws-wukong@e2da8ab947c6:wukong/products/whiteboard.go", "internal/cli/schema_hints/metadata/whiteboard.json", - "internal/cli/schema_hints/selection/whiteboard.json" + "internal/cli/schema_hints/selection/whiteboard.json", + "skills/mono/references/products/whiteboard/recipes.md" ], "agent_summary": "经用户确认后向已有白板追加 OpenNodes 或整页重建", "agent_summary_source": "dws-agent-selection/whiteboard", @@ -660,6 +662,12 @@ "selected": true, "source": "internal/cli/schema_hints/metadata/whiteboard.json", "value": "user_required" + }, + { + "precedence": "inference_or_default", + "selected": false, + "source": "risk-default", + "value": "not_required" } ], "precedence": "reviewed_explicit", @@ -690,6 +698,12 @@ "selected": true, "source": "internal/cli/schema_hints/metadata/whiteboard.json", "value": "write" + }, + { + "precedence": "inference_or_default", + "selected": false, + "source": "command-verb", + "value": "write" } ], "precedence": "reviewed_explicit", @@ -726,6 +740,12 @@ "selected": true, "source": "internal/cli/schema_hints/metadata/whiteboard.json", "value": "unknown" + }, + { + "precedence": "inference_or_default", + "selected": false, + "source": "effect-default", + "value": "unknown" } ], "precedence": "reviewed_explicit", @@ -813,6 +833,12 @@ "selected": true, "source": "internal/cli/schema_hints/metadata/whiteboard.json", "value": "high" + }, + { + "precedence": "inference_or_default", + "selected": false, + "source": "effect-default", + "value": "medium" } ], "precedence": "reviewed_explicit", diff --git a/skills/mono/references/products/whiteboard.md b/skills/mono/references/products/whiteboard.md index 15127e53..306d3ea5 100644 --- a/skills/mono/references/products/whiteboard.md +++ b/skills/mono/references/products/whiteboard.md @@ -5,7 +5,8 @@ OpenNodes V1 的完整字段、节点类型、目录枚举和错误语义按需读取 [协议索引](./whiteboard/open-nodes-v1.md);不要根据本页概要猜测节点字段或 -`geometry`、`catalogId` 等枚举值。 +`geometry`、`catalogId` 等枚举值。渐变卡片、Frame 分支、SVG/Vector 等完整 +工作流见 [常用 Recipes](./whiteboard/recipes.md)。 ## 标准流程 @@ -24,7 +25,24 @@ OpenNodes V1 的完整字段、节点类型、目录枚举和错误语义按需 "source": { "schemaVersion": "1.0", "catalogVersion": "dml-v1", - "nodes": [{"id":"n1","type":"text","x":40,"y":40,"text":"方案"}] + "nodes": [ + { + "id": "n1", + "type": "text", + "x": 40, + "y": 40, + "width": 240, + "height": 48, + "text": { + "blocks": [ + { + "type": "paragraph", + "runs": [{"text": "方案"}] + } + ] + } + } + ] } } ``` diff --git a/skills/mono/references/products/whiteboard/recipes.md b/skills/mono/references/products/whiteboard/recipes.md new file mode 100644 index 00000000..2360ef07 --- /dev/null +++ b/skills/mono/references/products/whiteboard/recipes.md @@ -0,0 +1,307 @@ +# 钉钉白板常用 Recipes + +以下文件都传给。执行任何远端写入前,必须先向用户展示影响并取得明确确认;确认后 +才可添加 `--yes`: + +```bash +dws whiteboard update \ + --node \ + --part-id \ + --source \ + --yes \ + --format json +``` + +每次更新后都要再次执行 `dws whiteboard query ... --format json` 回读验证。 + +## 1. 追加两个流程节点和一条箭头 + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "start", + "type": "shape", + "x": 80, + "y": 100, + "width": 160, + "height": 72, + "geometry": "dml:roundRect", + "text": { + "blocks": [ + { + "type": "paragraph", + "horizontalAlign": "center", + "runs": [{ "text": "读取需求", "marks": { "bold": true } }] + } + ], + "verticalAlign": "center" + } + }, + { + "id": "finish", + "type": "shape", + "x": 360, + "y": 100, + "width": 160, + "height": 72, + "geometry": "dml:roundRect", + "text": { + "blocks": [ + { + "type": "paragraph", + "horizontalAlign": "center", + "runs": [{ "text": "输出结果", "marks": { "bold": true } }] + } + ], + "verticalAlign": "center" + } + }, + { + "type": "connector", + "start": { + "type": "node", + "nodeRef": { "scope": "request", "id": "start" }, + "anchor": { "mode": "fixed", "side": "right" } + }, + "end": { + "type": "node", + "nodeRef": { "scope": "request", "id": "finish" }, + "anchor": { "mode": "fixed", "side": "left" }, + "marker": { "catalogId": "arrow.filled" } + }, + "routing": "straight" + } + ] + } +} +``` + +## 2. 追加带渐变和阴影的卡片 + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "styled-card", + "type": "shape", + "x": 80, + "y": 260, + "width": 260, + "height": 120, + "geometry": "dml:roundRect", + "style": { + "fill": { + "type": "linearGradient", + "angle": 35, + "stops": [ + { "offset": 0, "color": "#DBEAFE" }, + { "offset": 100, "color": "#A7F3D0" } + ] + }, + "stroke": { + "paint": { "type": "solid", "color": "#2563EB" }, + "width": 2 + }, + "effects": [ + { + "type": "shadow", + "offsetX": 5, + "offsetY": 7, + "blur": 18, + "color": "#0F172A", + "opacity": 0.22 + } + ] + }, + "text": { + "blocks": [ + { + "type": "paragraph", + "horizontalAlign": "center", + "runs": [ + { + "text": "复杂样式", + "marks": { "fontSize": 20, "bold": true, "color": "#0F172A" } + } + ] + } + ], + "verticalAlign": "center" + } + } + ] + } +} +``` + +## 3. Frame 中放置分支流程 + +先创建 frame,再让子节点通过 `parentId` 引用 frame 的临时 ID。子节点坐标相对 +frame 左上角: + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "pipeline", + "type": "frame", + "x": 60, + "y": 440, + "width": 720, + "height": 300, + "title": { + "text": { + "blocks": [ + { + "type": "paragraph", + "runs": [{ "text": "生成流水线", "marks": { "bold": true } }] + } + ] + } + } + }, + { + "id": "branch-a", + "type": "shape", + "parentId": "pipeline", + "x": 60, + "y": 80, + "width": 180, + "height": 72, + "geometry": "dml:rect" + }, + { + "id": "branch-b", + "type": "shape", + "parentId": "pipeline", + "x": 420, + "y": 80, + "width": 180, + "height": 72, + "geometry": "dml:rect" + } + ] + } +} +``` + +## 4. 上传 SVG 并追加 Vector + +Vector 固定使用“上传 → 字段映射 → update → query”流程,并且所有命令使用同一个 +`DOC_NODE_ID`。 + +先上传 SVG。该命令只准备资源,不会插入文档正文: + +```bash +dws doc media upload \ + --node \ + --file ./icon.svg \ + --mime-type image/svg+xml \ + --yes \ + --format json +``` + +从成功输出取 `resourceId` 和 `resourceUrl`: + +```json +{ + "nodeId": "", + "resourceId": "resource-stable-id", + "resourceUrl": "https://resource.example/resource-stable-id?resourceId=resource-stable-id", + "fileName": "icon.svg", + "mimeType": "image/svg+xml", + "size": 1024 +} +``` + +写入 `whiteboard-vector.json`,其中 `resourceId` 原样映射到 +`resource.resourceId`,`resourceUrl` 映射到 `resource.url`: + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "uploaded-vector", + "type": "vector", + "x": 80, + "y": 80, + "width": 160, + "height": 160, + "resource": { + "kind": "managed", + "resourceId": "resource-stable-id", + "url": "https://resource.example/resource-stable-id?resourceId=resource-stable-id" + } + } + ] + } +} +``` + +执行更新: + +```bash +dws whiteboard update \ + --node \ + --part-id \ + --source ./whiteboard-vector.json \ + --yes \ + --format json +``` + +最后独立回读,不以 update 的成功响应替代验证: + +```bash +dws whiteboard query \ + --node \ + --part-id \ + --format json +``` + +禁止跨 nodeId 复用资源,也不要把本地 SVG 路径、独立文件节点 URL 或临时 +`uploadUrl` 写入 `resource.url`。 + +## 5. 整页替换或清空 + +把文件设为 `overwrite: true` 后,命令必须加 `--yes`: + +```bash +dws whiteboard update \ + --node \ + --part-id \ + --source ./overwrite.json \ + --yes \ + --format json +``` + +清空整页: + +```json +{ + "overwrite": true, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [] + } +} +``` + +仅当用户明确要求清空时使用。执行前必须 query、展示影响摘要并获得确认。 diff --git a/skills/multi/dingtalk-misc/references/whiteboard.md b/skills/multi/dingtalk-misc/references/whiteboard.md index b76b6b7e..125c88fc 100644 --- a/skills/multi/dingtalk-misc/references/whiteboard.md +++ b/skills/multi/dingtalk-misc/references/whiteboard.md @@ -5,7 +5,8 @@ OpenNodes V1 的完整字段、节点类型、目录枚举和错误语义按需读取 [协议索引](./whiteboard/open-nodes-v1.md);不要根据本页概要猜测节点字段或 -`geometry`、`catalogId` 等枚举值。 +`geometry`、`catalogId` 等枚举值。渐变卡片、Frame 分支、SVG/Vector 等完整 +工作流见 [常用 Recipes](./whiteboard/recipes.md)。 ## 定位白板 @@ -37,7 +38,22 @@ CLI 会把服务端 `resultJson` 字符串解析为结构化 JSON。白板命令 "schemaVersion": "1.0", "catalogVersion": "dml-v1", "nodes": [ - {"id":"title","type":"text","x":40,"y":40,"text":"方案"} + { + "id": "title", + "type": "text", + "x": 40, + "y": 40, + "width": 240, + "height": 48, + "text": { + "blocks": [ + { + "type": "paragraph", + "runs": [{"text": "方案"}] + } + ] + } + } ] } } diff --git a/skills/multi/dingtalk-misc/references/whiteboard/recipes.md b/skills/multi/dingtalk-misc/references/whiteboard/recipes.md new file mode 100644 index 00000000..2360ef07 --- /dev/null +++ b/skills/multi/dingtalk-misc/references/whiteboard/recipes.md @@ -0,0 +1,307 @@ +# 钉钉白板常用 Recipes + +以下文件都传给。执行任何远端写入前,必须先向用户展示影响并取得明确确认;确认后 +才可添加 `--yes`: + +```bash +dws whiteboard update \ + --node \ + --part-id \ + --source \ + --yes \ + --format json +``` + +每次更新后都要再次执行 `dws whiteboard query ... --format json` 回读验证。 + +## 1. 追加两个流程节点和一条箭头 + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "start", + "type": "shape", + "x": 80, + "y": 100, + "width": 160, + "height": 72, + "geometry": "dml:roundRect", + "text": { + "blocks": [ + { + "type": "paragraph", + "horizontalAlign": "center", + "runs": [{ "text": "读取需求", "marks": { "bold": true } }] + } + ], + "verticalAlign": "center" + } + }, + { + "id": "finish", + "type": "shape", + "x": 360, + "y": 100, + "width": 160, + "height": 72, + "geometry": "dml:roundRect", + "text": { + "blocks": [ + { + "type": "paragraph", + "horizontalAlign": "center", + "runs": [{ "text": "输出结果", "marks": { "bold": true } }] + } + ], + "verticalAlign": "center" + } + }, + { + "type": "connector", + "start": { + "type": "node", + "nodeRef": { "scope": "request", "id": "start" }, + "anchor": { "mode": "fixed", "side": "right" } + }, + "end": { + "type": "node", + "nodeRef": { "scope": "request", "id": "finish" }, + "anchor": { "mode": "fixed", "side": "left" }, + "marker": { "catalogId": "arrow.filled" } + }, + "routing": "straight" + } + ] + } +} +``` + +## 2. 追加带渐变和阴影的卡片 + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "styled-card", + "type": "shape", + "x": 80, + "y": 260, + "width": 260, + "height": 120, + "geometry": "dml:roundRect", + "style": { + "fill": { + "type": "linearGradient", + "angle": 35, + "stops": [ + { "offset": 0, "color": "#DBEAFE" }, + { "offset": 100, "color": "#A7F3D0" } + ] + }, + "stroke": { + "paint": { "type": "solid", "color": "#2563EB" }, + "width": 2 + }, + "effects": [ + { + "type": "shadow", + "offsetX": 5, + "offsetY": 7, + "blur": 18, + "color": "#0F172A", + "opacity": 0.22 + } + ] + }, + "text": { + "blocks": [ + { + "type": "paragraph", + "horizontalAlign": "center", + "runs": [ + { + "text": "复杂样式", + "marks": { "fontSize": 20, "bold": true, "color": "#0F172A" } + } + ] + } + ], + "verticalAlign": "center" + } + } + ] + } +} +``` + +## 3. Frame 中放置分支流程 + +先创建 frame,再让子节点通过 `parentId` 引用 frame 的临时 ID。子节点坐标相对 +frame 左上角: + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "pipeline", + "type": "frame", + "x": 60, + "y": 440, + "width": 720, + "height": 300, + "title": { + "text": { + "blocks": [ + { + "type": "paragraph", + "runs": [{ "text": "生成流水线", "marks": { "bold": true } }] + } + ] + } + } + }, + { + "id": "branch-a", + "type": "shape", + "parentId": "pipeline", + "x": 60, + "y": 80, + "width": 180, + "height": 72, + "geometry": "dml:rect" + }, + { + "id": "branch-b", + "type": "shape", + "parentId": "pipeline", + "x": 420, + "y": 80, + "width": 180, + "height": 72, + "geometry": "dml:rect" + } + ] + } +} +``` + +## 4. 上传 SVG 并追加 Vector + +Vector 固定使用“上传 → 字段映射 → update → query”流程,并且所有命令使用同一个 +`DOC_NODE_ID`。 + +先上传 SVG。该命令只准备资源,不会插入文档正文: + +```bash +dws doc media upload \ + --node \ + --file ./icon.svg \ + --mime-type image/svg+xml \ + --yes \ + --format json +``` + +从成功输出取 `resourceId` 和 `resourceUrl`: + +```json +{ + "nodeId": "", + "resourceId": "resource-stable-id", + "resourceUrl": "https://resource.example/resource-stable-id?resourceId=resource-stable-id", + "fileName": "icon.svg", + "mimeType": "image/svg+xml", + "size": 1024 +} +``` + +写入 `whiteboard-vector.json`,其中 `resourceId` 原样映射到 +`resource.resourceId`,`resourceUrl` 映射到 `resource.url`: + +```json +{ + "overwrite": false, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [ + { + "id": "uploaded-vector", + "type": "vector", + "x": 80, + "y": 80, + "width": 160, + "height": 160, + "resource": { + "kind": "managed", + "resourceId": "resource-stable-id", + "url": "https://resource.example/resource-stable-id?resourceId=resource-stable-id" + } + } + ] + } +} +``` + +执行更新: + +```bash +dws whiteboard update \ + --node \ + --part-id \ + --source ./whiteboard-vector.json \ + --yes \ + --format json +``` + +最后独立回读,不以 update 的成功响应替代验证: + +```bash +dws whiteboard query \ + --node \ + --part-id \ + --format json +``` + +禁止跨 nodeId 复用资源,也不要把本地 SVG 路径、独立文件节点 URL 或临时 +`uploadUrl` 写入 `resource.url`。 + +## 5. 整页替换或清空 + +把文件设为 `overwrite: true` 后,命令必须加 `--yes`: + +```bash +dws whiteboard update \ + --node \ + --part-id \ + --source ./overwrite.json \ + --yes \ + --format json +``` + +清空整页: + +```json +{ + "overwrite": true, + "source": { + "schemaVersion": "1.0", + "catalogVersion": "dml-v1", + "nodes": [] + } +} +``` + +仅当用户明确要求清空时使用。执行前必须 query、展示影响摘要并获得确认。 diff --git a/test/unit/whiteboard_skill_docs_test.go b/test/unit/whiteboard_skill_docs_test.go new file mode 100644 index 00000000..917ae4cd --- /dev/null +++ b/test/unit/whiteboard_skill_docs_test.go @@ -0,0 +1,127 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package unit_test + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestWhiteboardQuickExamplesUseWritableTextNodes(t *testing.T) { + paths := []string{ + "../../skills/mono/references/products/whiteboard.md", + "../../skills/multi/dingtalk-misc/references/whiteboard.md", + } + for _, path := range paths { + path := path + t.Run(filepath.Base(filepath.Dir(path))+"/"+filepath.Base(path), func(t *testing.T) { + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + payload := firstJSONFence(t, string(data)) + source, _ := payload["source"].(map[string]any) + nodes, _ := source["nodes"].([]any) + if len(nodes) == 0 { + t.Fatalf("quick example has no source.nodes: %#v", payload) + } + node, _ := nodes[0].(map[string]any) + if node["type"] != "text" { + t.Fatalf("quick example first node type = %#v, want text", node["type"]) + } + for _, dimension := range []string{"width", "height"} { + value, ok := node[dimension].(float64) + if !ok || value <= 0 { + t.Errorf("quick example %s = %#v, want positive number", dimension, node[dimension]) + } + } + text, ok := node["text"].(map[string]any) + if !ok { + t.Fatalf("quick example text = %#v, want OpenNodes text object", node["text"]) + } + blocks, _ := text["blocks"].([]any) + if len(blocks) == 0 { + t.Fatalf("quick example text.blocks = %#v, want non-empty array", text["blocks"]) + } + }) + } +} + +func TestWhiteboardRecipesAreDeliveredToBothSkillSurfaces(t *testing.T) { + monoPath := "../../skills/mono/references/products/whiteboard/recipes.md" + multiPath := "../../skills/multi/dingtalk-misc/references/whiteboard/recipes.md" + mono, err := os.ReadFile(monoPath) + if err != nil { + t.Fatal(err) + } + multi, err := os.ReadFile(multiPath) + if err != nil { + t.Fatal(err) + } + if string(mono) != string(multi) { + t.Fatal("mono and multi whiteboard recipes differ") + } + content := string(mono) + for _, required := range []string{ + "## 1. 追加两个流程节点和一条箭头", + "## 2. 追加带渐变和阴影的卡片", + "## 3. Frame 中放置分支流程", + "## 4. 上传 SVG 并追加 Vector", + "## 5. 整页替换或清空", + "执行任何远端写入前,必须先向用户展示影响并取得明确确认", + `"resourceUrl": "https://resource.example/resource-stable-id?resourceId=resource-stable-id"`, + `"url": "https://resource.example/resource-stable-id?resourceId=resource-stable-id"`, + } { + if !strings.Contains(content, required) { + t.Errorf("whiteboard recipes missing %q", required) + } + } + for _, fence := range bashFences(content) { + if strings.Contains(fence, "dws whiteboard update") || strings.Contains(fence, "dws doc media upload") { + if !strings.Contains(fence, "--yes") { + t.Errorf("whiteboard write example lacks --yes:\n%s", fence) + } + } + } +} + +func bashFences(markdown string) []string { + const marker = "```bash\n" + var fences []string + for { + start := strings.Index(markdown, marker) + if start < 0 { + return fences + } + markdown = markdown[start+len(marker):] + end := strings.Index(markdown, "\n```") + if end < 0 { + return fences + } + fences = append(fences, markdown[:end]) + markdown = markdown[end+len("\n```"):] + } +} + +func firstJSONFence(t *testing.T, markdown string) map[string]any { + t.Helper() + const marker = "```json\n" + start := strings.Index(markdown, marker) + if start < 0 { + t.Fatal("markdown has no JSON fence") + } + start += len(marker) + end := strings.Index(markdown[start:], "\n```") + if end < 0 { + t.Fatal("JSON fence is not closed") + } + var payload map[string]any + if err := json.Unmarshal([]byte(markdown[start:start+end]), &payload); err != nil { + t.Fatalf("decode first JSON fence: %v", err) + } + return payload +}