diff --git a/skills/feishu-cli-htmlbox/SKILL.md b/skills/feishu-cli-htmlbox/SKILL.md index 14c23c7..d856124 100644 --- a/skills/feishu-cli-htmlbox/SKILL.md +++ b/skills/feishu-cli-htmlbox/SKILL.md @@ -34,6 +34,7 @@ allowed-tools: Bash(feishu-cli doc:*), Bash(feishu-cli perm:*), Bash(feishu-cli | 饼 / 漏斗 / 桑基 Sankey / 主题河流 / 仪表盘 gauge / 水球 liquidFill | ECharts(+扩展) | `references/gallery.md` › 构成流向 | | 力导向关系图(可拖拽)/ 组织树 tree / 旭日 sunburst / 矩形树图 treemap | ECharts | `references/gallery.md` › 关系层级 | | 时序 / 状态机 / 甘特 / CI流水线 / 看板流动 | ECharts custom / CSS | `references/gallery.md` › 流程时序 | +| 拓扑 / Agent 编排动画 / 多 Agent 协作回放 | `scripts/animate_diagram.py`(结构化 JSON → 自包含 SVG 动画 HTML) | `references/animated-flowchart.md` | | 词云 wordCloud | echarts-wordcloud | `references/gallery.md` › 构成流向 | | 纯 CSS 动画(旋转/脉动/进度条/打字机/变色) | CSS `@keyframes`(最稳,不依赖外网) | `references/gallery.md` › 创意动画 | | 粒子流 / 星空 / 自绘动画 | Canvas + `requestAnimationFrame` | `references/gallery.md` › 创意动画 | @@ -96,7 +97,9 @@ allowed-tools: Bash(feishu-cli doc:*), Bash(feishu-cli perm:*), Bash(feishu-cli ## 参考文档与脚本 - `scripts/verify.sh [等待秒数]` — **落库前验证脚本**(工作流第 2 步用它):全新 session 打开 → 抓 page error/console → 数 canvas/svg → 截图 → 给通过判定;退出码 0 才算初步通过,仍须肉眼看截图 +- `scripts/animate_diagram.py --pattern pattern.json --out x.html` — 拓扑 / Agent 编排动画生成器:结构化 JSON → 自包含 SVG 动画 HTML - `references/gallery.md` — **主力配方库**:4 种通用骨架(ECharts/Canvas/Three.js/SVG-CSS)+ 按图表类型的可直接用配方 +- `references/animated-flowchart.md` — 拓扑 / Agent 编排动画 recipe,配套输入格式见 `references/pattern-schema.md` - `references/geo-3d.md` — 地图 / echarts-gl 3D / Three.js 的完整可跑模板(这几类有 CDN/registerMap/坐标系/着色坑) - `references/window-magic.md` — **文档小程序运行时**:`window.magic` 能力配方(用户身份 / 读文档 / 持久化 / 多维表 / AI),含判存兜底范式与活数据 Dashboard、文档内 AI 卡等组合配方 - `references/pitfalls.md` — 画图避坑与白屏排查(来自真实创建一篇 47 图大文档) diff --git a/skills/feishu-cli-htmlbox/examples/supervisor.json b/skills/feishu-cli-htmlbox/examples/supervisor.json new file mode 100644 index 0000000..d5760a0 --- /dev/null +++ b/skills/feishu-cli-htmlbox/examples/supervisor.json @@ -0,0 +1,61 @@ +{ + "title": "Supervisor / Manager", + "sub": "Agents-as-tools · centralized orchestration", + "nodes": [ + { "id": "user", "x": 40, "y": 240, "w": 100, "label": "User", "kind": "user" }, + { "id": "manager", "x": 230, "y": 230, "w": 180, "label": "Manager", "sub": "supervisor", "kind": "accent" }, + { "id": "research", "x": 580, "y": 50, "w": 170, "label": "Research", "sub": "specialist" }, + { "id": "coder", "x": 580, "y": 230, "w": 170, "label": "Coder", "sub": "specialist" }, + { "id": "reviewer", "x": 580, "y": 410, "w": 170, "label": "Reviewer", "sub": "specialist" }, + { "id": "final", "x": 230, "y": 430, "w": 180, "label": "Final Answer", "kind": "dark" } + ], + "edges": { + "u-m": { "from": "user", "to": "manager", "label": "request" }, + "m-r": { "from": "manager", "to": "research", "label": "call as tool", "curve": -20 }, + "m-c": { "from": "manager", "to": "coder", "label": "call" }, + "m-rv": { "from": "manager", "to": "reviewer", "label": "call", "curve": 20 }, + "m-f": { "from": "manager", "to": "final" } + }, + "timeline": [ + { + "caption": "User sends request; Manager takes control of the conversation.", + "fire": ["u-m"], + "activate": ["user", "manager"] + }, + { + "caption": "Manager delegates research to Research specialist (agent-as-tool).", + "fire": ["m-r"], + "activate": ["manager", "research"] + }, + { + "caption": "Research returns results; conversation stays with Manager.", + "fire": ["!m-r"], + "activate": ["manager"] + }, + { + "caption": "Manager calls Coder to write / edit code.", + "fire": ["m-c"], + "activate": ["manager", "coder"] + }, + { + "caption": "Coder returns code snippet.", + "fire": ["!m-c"], + "activate": ["manager"] + }, + { + "caption": "Manager calls Reviewer to check quality.", + "fire": ["m-rv"], + "activate": ["manager", "reviewer"] + }, + { + "caption": "Reviewer returns review verdict.", + "fire": ["!m-rv"], + "activate": ["manager"] + }, + { + "caption": "Manager aggregates all results, outputs Final Answer. Intermediate steps hidden from user.", + "fire": ["m-f"], + "activate": ["manager", "final"] + } + ] +} diff --git a/skills/feishu-cli-htmlbox/references/animated-flowchart.md b/skills/feishu-cli-htmlbox/references/animated-flowchart.md new file mode 100644 index 0000000..fa52134 --- /dev/null +++ b/skills/feishu-cli-htmlbox/references/animated-flowchart.md @@ -0,0 +1,71 @@ +# Recipe: Animated Flowchart + +把结构化的图数据(节点 / 连线 / 时间线)生成一份**自包含的单文件动画 HTML**,再用 `feishu-cli doc htmlbox create` 嵌入飞书文档。适合流程图、架构图、拓扑图、Agent 编排 / 多 Agent 协作、时序 / 数据流的「会动」可视化。 + +## 产出物 + +零依赖单文件 HTML: + +- 从 `nodes` 和 `edges` 渲染的 SVG 拓扑。 +- 自动播放的时间线:节点高亮、连线高亮、消息 token 流动、字幕、进度点。 +- 极简控件:上一步、播放 / 暂停、下一步、可点击的进度点。 +- 默认固定浅色主题,适配飞书文档。 +- iframe 内无竖向滚动条(内滚动会干扰文档滚动)。 +- 响应式全屏:飞书 HTML Box 全屏打开时拓扑会放大。 + +默认**不用** React、Framer Motion、Mermaid 运行时、外部 CDN、倍速控件、重播按钮、纯键盘控件,也不用内部可滚动 iframe。 + +## 工作流 + +### 1. 准备 pattern.json + +按 [pattern-schema.md](pattern-schema.md) 组织一个 JSON,参考 [../examples/supervisor.json](../examples/supervisor.json)。 + +### 2. 生成 HTML + +```bash +python3 skills/feishu-cli-htmlbox/scripts/animate_diagram.py \ + --pattern pattern.json \ + --out animated-diagram.html +``` + +### 3. 本地预览(可选) + +```bash +python3 -m http.server 8799 --bind 127.0.0.1 +``` + +打开 `http://127.0.0.1:8799/animated-diagram.html`,确认: + +- 自动播放走完所有时间线步骤。 +- 播放 / 暂停、上一步 / 下一步、进度点都可用。 +- 背景是浅色,不是黑色。 +- iframe 内容无竖向滚动条。 +- 全屏预览用更大的视口,而不是卡在小卡片宽度。 +- token 流动方向正确;`!edgeId` 表示反向流动。 + +### 4. 嵌入飞书文档 + +新建文档后嵌入: + +```bash +feishu-cli doc create --title "Animated Diagram" --output json +feishu-cli doc htmlbox create --html-file animated-diagram.html +``` + +嵌入已有文档: + +```bash +feishu-cli doc htmlbox create --html-file animated-diagram.html +``` + +发布机制与铁律见上级 [SKILL.md](../SKILL.md) 的工作流、命令速记和避坑说明。 + +## Pattern 数据规则 + +- 画布坐标基于 `viewBox="0 0 900 540"`。 +- 节点需要稳定的 `id`、`x`、`y`、`label`;`w`、`h`、`sub`、`kind` 可选。 +- 连线以 edge ID 为 key。时间线的 `fire` 用这些 ID 引用连线。 +- 时间线 `fire` 项前缀 `!` 表示 token 反向流动。 +- 字幕保持简短;允许内联 `` 和 ``。 +- `kind: "accent"` 用于当前编排者,`kind: "dark"` 用于最终 / 输出节点,`kind: "user"` 用于用户气泡。 diff --git a/skills/feishu-cli-htmlbox/references/gallery.md b/skills/feishu-cli-htmlbox/references/gallery.md index 894cd3a..0bdfaa1 100644 --- a/skills/feishu-cli-htmlbox/references/gallery.md +++ b/skills/feishu-cli-htmlbox/references/gallery.md @@ -11,7 +11,7 @@ - [分布统计](#分布统计):涟漪散点 / 热力 / 日历热力 / 箱线 / K线 / 平行坐标 - [构成流向](#构成流向):漏斗 / 桑基 / 主题河流 / 仪表盘 / 水球 / 词云 - [关系层级](#关系层级):力导向关系图 / 组织树 / 旭日 / 矩形树图 -- [流程时序](#流程时序):状态机 / 看板流动 / 甘特 +- [流程时序](#流程时序):拓扑 / 编排动画 / 状态机 / 看板流动 / 甘特 - [创意动画](#创意动画):CSS 三件套 / Canvas 粒子 / SVG 路径 / 维恩 / 像素柱 / KPI 大屏 --- @@ -300,6 +300,10 @@ OPT = { backgroundColor:'#0f1729', series:[{type:'tree',data:[TREE],top:30,left: ## 流程时序 +| 你要画的图 | 引擎 / 方案 | 配方位置 | +|---|---|---| +| 拓扑 / 编排动画 / Agent 协作回放 | `scripts/animate_diagram.py`(结构化 JSON → 自包含 SVG 动画 HTML) | `references/animated-flowchart.md` | + 时序图 / 状态机 / CI 流水线这类「流程」,ECharts 没有现成 series,两条路: 1. **简单的用 graph**(节点 + 有向边 `edgeSymbol:['none','arrow']`,`layout:'none'` 手摆坐标),见上方力导向改 `layout:'none'`。 2. **要动感的用纯 CSS**(节点高亮沿流程流动),见下例。 diff --git a/skills/feishu-cli-htmlbox/references/pattern-schema.md b/skills/feishu-cli-htmlbox/references/pattern-schema.md new file mode 100644 index 0000000..905b687 --- /dev/null +++ b/skills/feishu-cli-htmlbox/references/pattern-schema.md @@ -0,0 +1,80 @@ +# Animated Flowchart — Pattern Schema + +`scripts/animate_diagram.py` 接受如下形状的 JSON 对象: + +```json +{ + "title": "Supervisor / Manager", + "sub": "Agents-as-tools · centralized orchestration", + "nodes": [ + { + "id": "manager", + "x": 230, + "y": 230, + "w": 180, + "h": 54, + "label": "Manager", + "sub": "supervisor", + "kind": "accent" + } + ], + "edges": { + "m-c": { + "from": "manager", + "to": "coder", + "label": "call", + "curve": 0, + "dashed": false + } + }, + "timeline": [ + { + "caption": "Manager calls Coder to write code.", + "fire": ["m-c"], + "activate": ["manager", "coder"], + "dim": [], + "duration": 1500 + } + ] +} +``` + +## 必填字段 + +- `title`:图标题。 +- `nodes`:节点对象数组。 +- `edges`:以 edge ID 为 key 的对象。 +- `timeline`:有序的动画步骤。 + +## 节点字段 + +- `id` 必填,唯一。 +- `x`、`y` 必填,`900 x 540` 画布内的左上角坐标。 +- `label` 必填。 +- `w` 可选,默认 `140`。 +- `h` 可选,默认 `54`。 +- `sub` 可选,小号大写副标题。 +- `kind` 可选:`plain`、`accent`、`dark`、`user`、`store`、`bus`。 + +## 连线字段 + +- `from` 必填,节点 ID。 +- `to` 必填,节点 ID。 +- `label` 可选。 +- `curve` 可选数字。正负值让连线朝相反方向弯曲。 +- `dashed` 可选布尔值。 + +## 时间线字段 + +- `caption` 必填。允许内联 `` 和 ``。 +- `fire` 可选,edge ID 数组。前缀 `!` 表示 token 反向流动。 +- `activate` 可选,要脉冲 / 高亮的节点 ID 数组。 +- `dim` 可选,要淡出的节点 ID 数组。 +- `duration` 可选,毫秒,默认 `1500`。 + +## 设计默认值 + +- 固定浅色主题。 +- 默认自动播放。 +- 极简控件:上一步、播放 / 暂停、下一步、进度点。 +- 默认无键盘快捷键、倍速控件、重播按钮、外部资源或运行时依赖。 diff --git a/skills/feishu-cli-htmlbox/scripts/animate_diagram.py b/skills/feishu-cli-htmlbox/scripts/animate_diagram.py new file mode 100755 index 0000000..b2b6f97 --- /dev/null +++ b/skills/feishu-cli-htmlbox/scripts/animate_diagram.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python3 +"""Generate a self-contained animated diagram HTML from pattern JSON.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +HTML_TEMPLATE = r""" + + + + +__TITLE__ + + + +
+
+ Animated topology + + +
+
+
·Diagram
+
+ + Live +
+ +
+
+
1 / 1
+

-

+
+
+ + + +
+
+
+ + + + +""" + + +def validate_pattern(pattern: dict) -> None: + required = ["title", "nodes", "edges", "timeline"] + missing = [key for key in required if key not in pattern] + if missing: + raise ValueError(f"missing required field(s): {', '.join(missing)}") + + node_ids = set() + for i, node in enumerate(pattern["nodes"]): + for key in ["id", "x", "y", "label"]: + if key not in node: + raise ValueError(f"nodes[{i}] missing {key!r}") + if node["id"] in node_ids: + raise ValueError(f"duplicate node id: {node['id']}") + node_ids.add(node["id"]) + + for edge_id, edge in pattern["edges"].items(): + for key in ["from", "to"]: + if key not in edge: + raise ValueError(f"edge {edge_id!r} missing {key!r}") + if edge["from"] not in node_ids: + raise ValueError(f"edge {edge_id!r} references missing from node {edge['from']!r}") + if edge["to"] not in node_ids: + raise ValueError(f"edge {edge_id!r} references missing to node {edge['to']!r}") + + edge_ids = set(pattern["edges"].keys()) + for i, step in enumerate(pattern["timeline"]): + if "caption" not in step: + raise ValueError(f"timeline[{i}] missing 'caption'") + for raw in step.get("fire", []): + edge_id = raw[1:] if raw.startswith("!") else raw + if edge_id not in edge_ids: + raise ValueError(f"timeline[{i}] references missing edge {edge_id!r}") + for field in ["activate", "dim"]: + for node_id in step.get(field, []): + if node_id not in node_ids: + raise ValueError(f"timeline[{i}].{field} references missing node {node_id!r}") + + +def _escape_json_for_inline_script(pattern_json: str) -> str: + """Keep JSON literals from accidentally closing the surrounding script tag.""" + return ( + pattern_json + .replace("&", "\\u0026") + .replace("<", "\\u003c") + .replace(">", "\\u003e") + ) + + +def render(pattern: dict) -> str: + pattern_json = json.dumps(pattern, ensure_ascii=False, separators=(",", ":")) + title = str(pattern.get("title", "Animated Diagram")) + return ( + HTML_TEMPLATE + .replace("__TITLE__", title.replace("&", "&").replace("<", "<").replace(">", ">")) + .replace("__PATTERN_JSON__", _escape_json_for_inline_script(pattern_json)) + ) + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--pattern", required=True, help="Path to pattern JSON") + parser.add_argument("--out", required=True, help="Output HTML path") + parser.add_argument("--no-validate", action="store_true", help="Skip schema validation") + args = parser.parse_args(argv) + + pattern_path = Path(args.pattern) + out_path = Path(args.out) + pattern = json.loads(pattern_path.read_text(encoding="utf-8")) + if not args.no_validate: + validate_pattern(pattern) + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(render(pattern), encoding="utf-8") + print(f"Wrote {out_path} ({out_path.stat().st_size} bytes)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))