fix: complete whiteboard skill examples

This commit is contained in:
chichuan
2026-08-04 21:19:45 +08:00
parent 287b079c18
commit d7c28bcfef
10 changed files with 840 additions and 13 deletions
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:98a61c88e23f4c223f4aae341f0d812545f6154cf18736e1f90bed88b6b47bca",
"source_hash": "sha256:2fb34ad56cc801af92a057aeda1b8af7e886f6d129bedb643b8d08e4f59c5177",
"surface_hash": "sha256:942309e0671ad009203266b2d6dade23a0afbbda5de77df393964b0e5cef39f6",
"coverage": {
"surface_products": 27,
@@ -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 文件,用户确认后要追加图形、文本、连接线或整页替换时"
@@ -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,
+2 -2
View File
@@ -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,
@@ -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",
+20 -2
View File
@@ -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": "方案"}]
}
]
}
}
]
}
}
```
@@ -0,0 +1,307 @@
# 钉钉白板常用 Recipes
以下文件都传给。执行任何远端写入前,必须先向用户展示影响并取得明确确认;确认后
才可添加 `--yes`:
```bash
dws whiteboard update \
--node <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--source <FILE.json> \
--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 <DOC_NODE_ID> \
--file ./icon.svg \
--mime-type image/svg+xml \
--yes \
--format json
```
从成功输出取 `resourceId` 和 `resourceUrl`:
```json
{
"nodeId": "<DOC_NODE_ID>",
"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 <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--source ./whiteboard-vector.json \
--yes \
--format json
```
最后独立回读,不以 update 的成功响应替代验证:
```bash
dws whiteboard query \
--node <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--format json
```
禁止跨 nodeId 复用资源,也不要把本地 SVG 路径、独立文件节点 URL 或临时
`uploadUrl` 写入 `resource.url`。
## 5. 整页替换或清空
把文件设为 `overwrite: true` 后,命令必须加 `--yes`:
```bash
dws whiteboard update \
--node <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--source ./overwrite.json \
--yes \
--format json
```
清空整页:
```json
{
"overwrite": true,
"source": {
"schemaVersion": "1.0",
"catalogVersion": "dml-v1",
"nodes": []
}
}
```
仅当用户明确要求清空时使用。执行前必须 query、展示影响摘要并获得确认。
@@ -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": "方案"}]
}
]
}
}
]
}
}
@@ -0,0 +1,307 @@
# 钉钉白板常用 Recipes
以下文件都传给。执行任何远端写入前,必须先向用户展示影响并取得明确确认;确认后
才可添加 `--yes`:
```bash
dws whiteboard update \
--node <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--source <FILE.json> \
--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 <DOC_NODE_ID> \
--file ./icon.svg \
--mime-type image/svg+xml \
--yes \
--format json
```
从成功输出取 `resourceId` 和 `resourceUrl`:
```json
{
"nodeId": "<DOC_NODE_ID>",
"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 <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--source ./whiteboard-vector.json \
--yes \
--format json
```
最后独立回读,不以 update 的成功响应替代验证:
```bash
dws whiteboard query \
--node <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--format json
```
禁止跨 nodeId 复用资源,也不要把本地 SVG 路径、独立文件节点 URL 或临时
`uploadUrl` 写入 `resource.url`。
## 5. 整页替换或清空
把文件设为 `overwrite: true` 后,命令必须加 `--yes`:
```bash
dws whiteboard update \
--node <DOC_NODE_ID> \
--part-id <WHITEBOARD_PART_ID> \
--source ./overwrite.json \
--yes \
--format json
```
清空整页:
```json
{
"overwrite": true,
"source": {
"schemaVersion": "1.0",
"catalogVersion": "dml-v1",
"nodes": []
}
}
```
仅当用户明确要求清空时使用。执行前必须 query、展示影响摘要并获得确认。
+127
View File
@@ -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
}