初始化项目版本
This commit is contained in:
61
.claude/skills/canvas-workspace/SKILL.md
Normal file
61
.claude/skills/canvas-workspace/SKILL.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: canvas-workspace
|
||||
description: 仅当任务明确涉及 Axhub 画布、原型草稿、Excalidraw 画布文件、画布节点/批注/截图/图片,或需要把文档、原型页面、图片、流程图等产物落到画布上时使用。
|
||||
---
|
||||
|
||||
# Canvas Workspace — 画布工作区
|
||||
|
||||
仅当任务明确涉及 Axhub 画布、原型草稿,或需要把产物落到画布/Excalidraw 上时使用本技能。每个原型拥有自己的 Excalidraw 画布文件:
|
||||
|
||||
```text
|
||||
src/prototypes/<prototype-name>/canvas.excalidraw
|
||||
src/prototypes/<prototype-name>/canvas-assets/
|
||||
```
|
||||
|
||||
本技能按四类产物分流:文档、原型页面、图片、流程图。先判断产物类型;产物类型不清时先问一个问题。如果用户已在画布/草稿中工作,不再询问放在哪里,默认更新当前 `canvas.excalidraw`。
|
||||
|
||||
## 工具优先级
|
||||
|
||||
- 实时画布已连接 MCP 时,优先调用 `axhub-canvas` 的工具更新当前画布。
|
||||
- 生成 Mermaid 流程、关系、序列、状态、类、ER 或简单盒线架构图时,优先调用 `canvas_insert_mermaid`,传入 `mermaidCode` 和可选 `position`,由浏览器画布转换成可编辑 Excalidraw 元素并保存。
|
||||
- MCP 不可用、没有实时画布、或用户明确要求离线编辑文件时,直接更新对应 `.excalidraw` 文件;需要插入 Mermaid 时,先得到已转换的 Excalidraw elements/files,再写入 `elements` 和 `files`。
|
||||
- 只有需要读取状态、插入普通元素、刷新、截图、更新、删除或聚焦画布时,才改用 `canvas_get_state`、`canvas_insert_elements`、`canvas_refresh`、`canvas_capture`、`canvas_update_elements`、`canvas_delete_elements`、`canvas_focus`。
|
||||
|
||||
## 读取顺序
|
||||
|
||||
1. 用户指定画布名或画布链接时,先从名称或链接定位对应的 `canvas.excalidraw`。
|
||||
2. 查看 `elements`、`files` 和元素的 `customData`。
|
||||
3. 只有元素引用了持久化截图或图片文件时,才读取 `canvas-assets/`。
|
||||
4. 不使用 `axhub-make canvas` CLI;画布内容读取和修改仍以 `.excalidraw` 文件为准。
|
||||
|
||||
## 参考文档分流
|
||||
|
||||
- 读写画布文件本身仍不清楚时,才读 `references/canvas-read-write.md`。
|
||||
- 遇到 Axhub 专属节点或不确定 `customData` 字段含义时,才读 `references/axhub-nodes.md`。
|
||||
- 需要普通 Excalidraw 元素绘制时,才读 `references/excalidraw-basics.md`。
|
||||
- 确定要创建或编辑 Drawio 节点时,才读 `references/drawio/SKILL.md`。
|
||||
|
||||
## 产物分流
|
||||
|
||||
- 文档:用户要求生成文档、说明、PRD、清单、列表、报告或其他文本内容时,默认先生成 Markdown 文档到 `src/resources/`,再把该文档作为文档节点创建或更新到当前 `canvas.excalidraw`;不要把正文直接拆成大量画布文本框。
|
||||
- 原型页面:创建或更新 `src/prototypes/<prototype-name>/` 中的页面,再把原型页面作为预览节点放到画布;节点尺寸与网页内部视口分开处理,用 `customData.embedContentScale` 缩放显示。
|
||||
- 图片:先确认它是画布参考、画布节点,还是项目实现素材;需要持久化时放入当前原型的 `canvas-assets/`,再插入图片节点。
|
||||
- 流程图:先判断图表类型和可编辑载体。流程、关系、序列、状态、类、ER 和简单盒线架构优先用 Mermaid 作为中间结构并转普通 Excalidraw 元素;简单手绘式图也可直接画普通 Excalidraw。复杂泳道、排期/甘特、复杂云架构、网络拓扑或厂商图标等需要 Draw.io 语义或素材库的图,才按 `references/drawio/SKILL.md` 生成或编辑 Drawio 资产,并按 `references/axhub-nodes.md` 的 Drawio 节点结构更新画布;只有类型或载体重叠不确定时才询问用户。
|
||||
|
||||
## 默认规则
|
||||
|
||||
- 优先使用可用的 `axhub-canvas` MCP 工具更新当前画布;离线或 MCP 不可用时直接编辑 `.excalidraw` JSON。
|
||||
- 元素 `id` 必须唯一,并尽量沿用现有文件的 ID 风格。
|
||||
- 修改元素时同步更新 `version`、`versionNonce` 和 `updated`。
|
||||
- 结构性改动后检查绑定、容器、分组和 Frame 引用。
|
||||
- 除非用户需求要求修改,否则保留已有 Axhub `customData`。
|
||||
|
||||
## 回复要求
|
||||
|
||||
完成画布相关工作后,说明:
|
||||
|
||||
- 画布文件路径。
|
||||
- 修改了什么,或读取到了什么。
|
||||
- 相关节点 ID 或批注。
|
||||
- 是否使用了本地图片或 `canvas-assets/`。
|
||||
- 如果当前环境能确定,给出画布确认链接。
|
||||
4
.claude/skills/canvas-workspace/agents/openai.yaml
Normal file
4
.claude/skills/canvas-workspace/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "画布工作区"
|
||||
short_description: "把文档、原型页面、图片、流程图等产物创建或更新到 Axhub 画布"
|
||||
default_prompt: "使用 $canvas-workspace 处理这个画布需求,先判断产物类型再更新当前画布。"
|
||||
112
.claude/skills/canvas-workspace/references/axhub-nodes.md
Normal file
112
.claude/skills/canvas-workspace/references/axhub-nodes.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Axhub 画布节点
|
||||
|
||||
Axhub 画布节点本质上是标准 Excalidraw 元素,Axhub 扩展信息存放在 `customData` 中。
|
||||
|
||||
## 通用字段
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `customData.title` | 面向用户的节点标题 |
|
||||
| `customData.previewUrl` | 预览模式中渲染的 URL |
|
||||
| `customData.openUrl` | 节点操作中打开的 URL |
|
||||
| `customData.previewKind` | 渲染类型,例如 `web`、`doc`、`image`、`none` |
|
||||
| `customData.resourceType` | 资源类型:`prototype`、`doc` 或 `theme` |
|
||||
| `customData.resourceId` | 项目 metadata 中的资源 id 或名称 |
|
||||
| `customData.embedViewMode` | `link` 表示紧凑链接卡片,`preview` 表示渲染嵌入预览 |
|
||||
| `customData.embedContentScale` | 预览内容缩放比例,例如 `0.5` 表示画布节点按 50% 显示,但 iframe 使用 2 倍视口渲染 |
|
||||
| `customData.screenshotUrl` | 运行时已捕获的持久化预览截图 URL |
|
||||
| `customData.annotation` | 元素批注文本 |
|
||||
| `customData.annotationUpdatedAt` | 批注更新时间,ISO 8601 格式 |
|
||||
|
||||
## 嵌入资源节点
|
||||
|
||||
嵌入资源使用 `type: "embeddable"`。
|
||||
|
||||
### 原型节点
|
||||
|
||||
通过 `customData.resourceType: "prototype"` 识别;也可以结合指向原型的 `link` 或 `previewUrl` 判断。
|
||||
|
||||
常见字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "embeddable",
|
||||
"link": "/?resourceType=prototype&resourceId=<prototype-id>&view=demo&sidebar=collapsed",
|
||||
"customData": {
|
||||
"title": "原型标题",
|
||||
"previewUrl": "http://localhost:<port>/prototypes/<prototype-id>",
|
||||
"openUrl": "/?resourceType=prototype&resourceId=<prototype-id>&view=demo&sidebar=collapsed",
|
||||
"previewKind": "web",
|
||||
"resourceType": "prototype",
|
||||
"resourceId": "<prototype-id>",
|
||||
"embedViewMode": "link"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 文档节点
|
||||
|
||||
通过 `customData.type: "axhub-doc"` 或 `customData.resourceType: "doc"` 识别。
|
||||
当画布任务需要生成文档、说明、PRD、清单、列表、报告或其他文本内容时,优先把正文写成 `src/resources/` 下的 Markdown,再用文档节点引用该资源;画布只放摘要或入口。
|
||||
|
||||
常见字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "embeddable",
|
||||
"link": "/api/markdown-file?path=<encoded-path>",
|
||||
"customData": {
|
||||
"type": "axhub-doc",
|
||||
"title": "文档标题",
|
||||
"previewUrl": "/api/markdown-file?path=<encoded-path>",
|
||||
"previewKind": "doc",
|
||||
"resourceType": "doc",
|
||||
"resourceId": "<doc-id>",
|
||||
"embedViewMode": "link"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 主题节点
|
||||
|
||||
通过 `customData.resourceType: "theme"` 或 `customData.type: "axhub-theme"` 识别。
|
||||
|
||||
主题节点与原型/文档节点使用相同的 `embeddable` 结构,`resourceType` 为 `theme`,`previewKind` 通常为 `web` 或 `none`。
|
||||
|
||||
## Drawio 节点
|
||||
|
||||
Drawio 节点是图片元素。`files[fileId].dataURL` 保存带 Drawio XML 的 SVG 预览,`customData.type` 固定为 `axhub-drawio`。
|
||||
|
||||
只有用户明确要求 Draw.io、`.drawio`、diagrams.net、可编辑 Draw.io 资产,或 `canvas-workspace` 已选择 Drawio 节点时,才在当前原型的 `canvas.excalidraw` 中创建或更新这种节点。
|
||||
识别 Drawio 节点以 `customData.type: "axhub-drawio"` 为准;`previewKind` 只是预览展示元信息。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "image",
|
||||
"fileId": "drawio-file-<id>",
|
||||
"customData": {
|
||||
"type": "axhub-drawio",
|
||||
"title": "Drawio 图表",
|
||||
"previewKind": "drawio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
创建或更新 Drawio 节点时:
|
||||
|
||||
- 推荐持久化资源文件后缀为 `.drawio.svg`,例如 `src/prototypes/<prototype-name>/canvas-assets/diagrams/<diagram-id>.drawio.svg`。
|
||||
- `files[fileId].dataURL` 应是 `data:image/svg+xml;base64,...`。
|
||||
- SVG 根节点应使用 `data-drawio="<base64-encoded mxfile>"` 保存 Drawio XML,便于后续在 diagrams.net 编辑器里继续编辑。
|
||||
- 如果只是初始化一个空 Drawio 节点,可以使用默认空图 XML;如果已经确定使用 Draw.io 承载流程图或关系图,应把图结构写入 Drawio XML,而不是只写普通 Excalidraw 文本框。
|
||||
|
||||
## 图片文件
|
||||
|
||||
图片元素通过 `files[element.fileId]` 读取图片数据。
|
||||
|
||||
原型嵌入节点如果存在 `customData.screenshotUrl`,优先使用该截图地址。截图文件常见位置:
|
||||
|
||||
```text
|
||||
src/prototypes/<prototype-name>/canvas-assets/embed-<elementId>.png
|
||||
```
|
||||
|
||||
截图缓存不等同于页面实现素材;只有用户明确要求把它作为素材使用时,才把它当作实现资产处理。
|
||||
143
.claude/skills/canvas-workspace/references/canvas-read-write.md
Normal file
143
.claude/skills/canvas-workspace/references/canvas-read-write.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 画布读写能力参考
|
||||
|
||||
面向使用 Skill 的 Agent:优先读写本地 `.excalidraw` 文件。用户指定画布名或画布链接时,直接定位本地画布文件;不要使用 `axhub-make canvas` CLI。
|
||||
|
||||
## 快速判断
|
||||
|
||||
| 目标 | 做法 |
|
||||
|------|----------|
|
||||
| 读取画布元素、批注、节点信息 | 直接读 `.excalidraw` |
|
||||
| 修改画布内容 | 直接改 `.excalidraw` |
|
||||
| 从用户给的画布链接定位元素 | 从链接提取画布名和元素 ID,再读文件 |
|
||||
| 获取画布截图 | 优先使用已有 `canvas-assets` 截图;需要当前浏览器画布时用全局截图 API |
|
||||
|
||||
## 文件位置
|
||||
|
||||
常见路径:
|
||||
|
||||
```text
|
||||
src/prototypes/<prototype-name>/canvas.excalidraw
|
||||
src/prototypes/<prototype-name>/canvas-assets/embed-<elementId>.png
|
||||
```
|
||||
|
||||
`.excalidraw` 是 JSON。主要关注:
|
||||
|
||||
- `elements`:所有画布元素。
|
||||
- `files`:嵌入图片数据或图片元信息。
|
||||
- `appState.gridSize`:整理/对齐时参考。
|
||||
|
||||
## 读取画布
|
||||
|
||||
读取过程以本地 JSON 为准。
|
||||
|
||||
最常用字段:
|
||||
|
||||
| 字段 | 用途 |
|
||||
|------|------|
|
||||
| `id` | 元素唯一标识,链接定位和截图文件名会用到 |
|
||||
| `type` | 元素类型,如 `text`、`image`、`embeddable`、`arrow` |
|
||||
| `x` / `y` / `width` / `height` | 位置和尺寸 |
|
||||
| `isDeleted` | 为 true 时跳过 |
|
||||
| `link` | 嵌入节点链接 |
|
||||
| `customData` | 批注、标题、截图地址等 Axhub 扩展信息 |
|
||||
| `fileId` | 图片元素对应的 `files[fileId]` |
|
||||
|
||||
识别常见节点:
|
||||
|
||||
| 类型 | 判断方式 |
|
||||
|------|----------|
|
||||
| 原型节点 | `type == "embeddable"` 且 `customData.resourceType == "prototype"`,或 `link`/`previewUrl` 指向原型 |
|
||||
| 文档节点 | `type == "embeddable"` 且 `customData.type == "axhub-doc"` 或 `customData.resourceType == "doc"` |
|
||||
| 主题节点 | `type == "embeddable"` 且 `customData.resourceType == "theme"` 或 `customData.type == "axhub-theme"` |
|
||||
| Drawio 节点 | `type == "image"` 且 `customData.type == "axhub-drawio"` |
|
||||
| 图片元素 | `type == "image"` |
|
||||
| 批注元素 | `customData.annotation` 有值 |
|
||||
|
||||
Axhub 节点字段见 `axhub-nodes.md`。
|
||||
|
||||
## CLI
|
||||
|
||||
没有画布专用 CLI。读取元素、节点和批注时仍以 `.excalidraw` 文件为准;需要截图时,优先使用已有 `canvas-assets` 截图或浏览器页面能力。
|
||||
|
||||
## 浏览器截图 API
|
||||
|
||||
Excalidraw 官方暴露的是导出工具方法,例如 `exportToBlob`、`exportToCanvas`、`exportToSvg`,不是当前画布实例的一键截图命令。Axhub 在浏览器里的当前画布实例上封装了全局截图 API:
|
||||
|
||||
```js
|
||||
await window.__AXHUB_EXCALIDRAW_CAPTURE__.captureCanvas()
|
||||
await window.__AXHUB_EXCALIDRAW_CAPTURE__.captureElement('<elementId>')
|
||||
```
|
||||
|
||||
两个方法都返回:
|
||||
|
||||
```ts
|
||||
{
|
||||
blob: Blob
|
||||
dataUrl: string
|
||||
width?: number
|
||||
height?: number
|
||||
elementIds: string[]
|
||||
}
|
||||
```
|
||||
|
||||
可选参数:
|
||||
|
||||
```ts
|
||||
{
|
||||
exportBackground?: boolean
|
||||
exportPadding?: number
|
||||
maxWidthOrHeight?: number
|
||||
mimeType?: string
|
||||
quality?: number
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
```
|
||||
|
||||
默认导出 PNG、带背景、16px padding。`captureCanvas()` 导出当前画布所有未删除元素;`captureElement(elementId)` 只导出指定未删除元素。该能力只在画布页面打开并完成初始化后可用。
|
||||
|
||||
## 从链接定位
|
||||
|
||||
用户可能给一个带节点 ID 的画布链接。处理步骤:
|
||||
|
||||
1. 从 URL 中提取画布名和元素 ID。
|
||||
2. 找到对应 `.excalidraw` 文件。
|
||||
3. 在 `elements` 中找同 ID 元素。
|
||||
4. 如果是原型节点,预览截图通常在 `canvas-assets/embed-<elementId>.png`。
|
||||
5. 如果是图片元素,按 `fileId` 找 `files[fileId]`。
|
||||
6. 如果是嵌入节点,结合 `customData.resourceType`、`customData.previewUrl` 和 `customData.screenshotUrl` 判断资源来源。
|
||||
|
||||
## 写入画布
|
||||
|
||||
直接修改 `.excalidraw` 的 `elements` 数组。
|
||||
|
||||
### 添加元素
|
||||
|
||||
追加到 `elements`。必须有唯一 `id`,推荐沿用现有格式:`<timestamp>-<random>`。
|
||||
|
||||
### 修改元素
|
||||
|
||||
更新目标字段后,同时更新:
|
||||
|
||||
- `version` 加 1
|
||||
- `versionNonce` 换成新的随机整数
|
||||
- `updated` 设为当前毫秒时间戳
|
||||
|
||||
### 删除元素
|
||||
|
||||
默认直接从 `elements` 中移除。不要为了删除而设置 `isDeleted: true`,这会让画布文件持续膨胀。
|
||||
|
||||
如果遇到历史遗留的 `isDeleted: true` 元素,读取时跳过;整理画布时可以一并移除。
|
||||
|
||||
### 关系检查
|
||||
|
||||
修改连接、容器、分组时检查引用是否仍然存在:
|
||||
|
||||
- `boundElements`
|
||||
- `containerId`
|
||||
- `startBinding` / `endBinding`
|
||||
- `groupIds`
|
||||
|
||||
## 热更新
|
||||
|
||||
保存 `.excalidraw` 后,打开中的画布会通过热更新同步。完成画布写入时,交付说明里标明画布文件路径和改动内容。
|
||||
194
.claude/skills/canvas-workspace/references/drawio/SKILL.md
Normal file
194
.claude/skills/canvas-workspace/references/drawio/SKILL.md
Normal file
@@ -0,0 +1,194 @@
|
||||
---
|
||||
name: drawio
|
||||
version: "2.2.0"
|
||||
description: "Create, edit, replicate, import, and export draw.io diagrams with an offline YAML-first workflow. Use for general engineering and product diagrams: architecture, network topologies, flowcharts, UML/ER, org charts, Mermaid/CSV conversion, existing .drawio bundles, style presets, themes, and non-publication formula diagrams. For paper, thesis, journal, conference, IEEE/ACM, manuscript, camera-ready, or publication figures, prefer drawio-academic-skills; this base provides shared CLI, references, themes, schemas, styles, and optional Desktop export."
|
||||
license: MIT
|
||||
homepage: https://github.com/bahayonghang/drawio-skills
|
||||
compatibility: "Node 20+ for the YAML/CLI workflow. draw.io Desktop is optional and only needed for PNG/PDF/JPG or embedded .drawio.svg exports. No MCP server is required for offline authoring; the optional live-refinement backend needs a browser/MCP provider."
|
||||
platforms: [macos, linux, windows]
|
||||
metadata:
|
||||
category: visual-design
|
||||
tags:
|
||||
- diagram
|
||||
- drawio
|
||||
- architecture
|
||||
- flowchart
|
||||
- network-topology
|
||||
- uml
|
||||
- mermaid
|
||||
- csv
|
||||
- design-system
|
||||
- math
|
||||
argument-hint: [diagram-description-or-instruction]
|
||||
allowed-tools: Read, Write, Bash, AskUserQuestion
|
||||
---
|
||||
|
||||
# Draw.io Base Skill
|
||||
|
||||
Create, edit, validate, replicate, import, and export draw.io diagrams through the shared YAML-first Draw.io Base Skill.
|
||||
|
||||
This package is the single maintained base capability surface for sibling overlays. It owns the local CLI, schemas, shared references, themes, reusable examples, style presets, Desktop export helpers, diagrams.net URL fallback, and optional live-refinement backend.
|
||||
|
||||
## Scope
|
||||
|
||||
Use this base skill for general draw.io work:
|
||||
|
||||
- software and system architecture diagrams
|
||||
- network topologies and infrastructure maps
|
||||
- flowcharts, swimlanes, process maps, and org charts
|
||||
- UML class, sequence, state, and ER diagrams
|
||||
- Mermaid and CSV conversion into draw.io
|
||||
- structured redraw and non-academic replication
|
||||
- formula-bearing technical diagrams
|
||||
- `.drawio` import, sidecar export, and local validation
|
||||
|
||||
For paper, thesis, IEEE, journal, manuscript, or publication-ready figure requests, use `drawio-academic-skills` as the policy overlay. The overlay depends on this sibling base for execution; the base does not automatically apply academic publication gates.
|
||||
|
||||
## Runtime Stack
|
||||
|
||||
Use the lightest path that satisfies the request.
|
||||
|
||||
| Runtime | Role | Source of truth | Notes |
|
||||
| ----------------------- | ------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| Offline Authoring Path | Default create/edit/replicate/import/export | YAML spec in project work dir | Generates final `.drawio` and `.drawio.svg` locally; keeps `.spec.yaml` and `.arch.json` in a separate work dir unless explicitly requested beside the output. |
|
||||
| Desktop-Enhanced Export | Optional final export | Existing offline bundle | Adds PNG/PDF/JPG or embedded `.drawio.svg` when draw.io Desktop is available. |
|
||||
| Live Refinement Backend | Optional browser refinement provider | Offline bundle remains canonical | Use only when the user explicitly wants browser/inline iteration and required live capabilities exist. |
|
||||
| Direct XML Exception | Tiny one-off or raw mxGraph handoff | `.drawio` XML | Use only when YAML/CLI is unavailable or exact XML control is the real requirement. |
|
||||
|
||||
The optional MCP/live backend is a refinement provider only. Do not treat it as required for normal authoring, editing, import, replication, or export.
|
||||
|
||||
## Task Routing
|
||||
|
||||
Choose the route first, then load only the references needed for that route.
|
||||
|
||||
| Route | When to use | Required references |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `create` | New diagram from text, YAML, Mermaid, CSV, or a concise spec | `references/workflows/create.md`, `references/docs/design-system/README.md`, `references/docs/design-system/specification.md` |
|
||||
| `edit` | Modify an existing sidecar bundle or imported `.drawio` | `references/workflows/edit.md`, `references/docs/migration-readiness.md` |
|
||||
| `replicate` | Redraw an uploaded image, screenshot, SVG, or reference diagram | `references/workflows/replicate.md`, `references/docs/design-system/README.md`, `references/docs/design-system/specification.md`, `references/docs/design-system/color-guide.md` |
|
||||
| `math-formula` | Labels contain formulas, equations, LaTeX, AsciiMath, MathJax, or Chinese formula keywords | `references/docs/math-typesetting.md`, `references/docs/design-system/formulas.md` |
|
||||
| `stencil-heavy` | Cloud, provider icon, network gear, or exact draw.io shape work | `references/docs/stencil-library-guide.md`, `references/official/xml-reference.md`, `references/official/style-reference.md` |
|
||||
| `network-topology` | Network topology, VLAN / subnet / gateway, campus / data-center / cloud network maps(拓扑、子网、网关、VLAN) | `references/docs/ieee-network-diagrams.md`, `references/docs/stencil-library-guide.md`, `references/official/xml-reference.md` |
|
||||
| `edge-audit` | Dense diagrams or routing-sensitive diagrams | `references/docs/edge-quality-rules.md`, `references/official/xml-reference.md` |
|
||||
| `live-refinement` | Explicit browser/inline visual refinement | `references/docs/mcp-tools.md`, `references/docs/migration-readiness.md` |
|
||||
| `direct-xml` | Tiny XML-only handoff or raw mxGraph edits | `references/official/xml-reference.md`, `references/official/style-reference.md`, `references/docs/xml-format.md`, `references/upstream/pure-drawio-skill.md` |
|
||||
|
||||
Use `network-topology` when the diagram **is** a network/infrastructure map; use `stencil-heavy` when the focus is provider icons or exact draw.io shapes in any diagram type.
|
||||
|
||||
Academic triggers such as `paper`, `thesis`, `IEEE`, `journal`, `manuscript`, or `publication-ready figure` should route to the sibling `drawio-academic-skills` overlay when that skill is available. If the overlay is not available, this base can still render a local YAML bundle, but report that academic overlay policy was not applied.
|
||||
|
||||
## Default Operating Rules
|
||||
|
||||
1. Keep YAML spec as the canonical representation. Mermaid, CSV, natural language, and imported `.drawio` files are input surfaces that normalize into YAML before rendering.
|
||||
2. Keep final delivery directories clean by default: deliver `<name>.drawio` and `<name>.drawio.svg`; keep canonical sidecars such as `<name>.spec.yaml` and `<name>.arch.json` in a project-local work directory such as `.drawio-tmp/<name>/`.
|
||||
3. In Axhub Make projects, use `.drawio.svg` as the recommended SVG file suffix and keep editable source on the SVG root as `data-drawio="<base64-encoded mxfile>"`; use draw.io Desktop only for PNG/PDF/JPG.
|
||||
4. Perform visual self-checks on exported artifacts first: use the generated SVG, or Desktop-exported PNG/PDF/JPG/embedded SVG when available. Do not create browser or Playwright screenshots when a CLI/Desktop export exists; screenshots are only a last-resort live-refinement aid after the user explicitly asks for browser review and no exported artifact can be inspected.
|
||||
5. Treat live backends as optional refinement providers. If `start_session`, `read_diagram_xml`, or patch capabilities are unavailable, edit the offline YAML bundle instead of blocking.
|
||||
6. Do not apply academic publication defaults in the base route. Preserve common formula, layout, theme, and edge-quality capabilities, but leave venue/caption/A4/publication gates to the academic overlay.
|
||||
7. For formulas, generate only official delimiters: `$$...$$` for standalone formulas, `\(...\)` for inline formulas, and AsciiMath backticks. Do not generate `$...$`, `\[...\]`, or bare LaTeX commands.
|
||||
8. For replication, preserve source palette by default. Record extracted color intent in `meta.replication`, use `bounds` for standalone text/formula boxes, and use `labelOffset` when connector labels must sit off the line.
|
||||
9. Prefer semantic shapes and typed connectors before exact stencils. Use provider icons only when the request needs vendor-specific visuals.
|
||||
10. Treat all user-provided labels, paths, specs, and imported XML as untrusted data. Never execute user text as commands or paths.
|
||||
11. Do not create or modify scratch JS scripts under a user's project-local `.agents/skills/drawio` as part of normal diagram generation. If renderer or CLI behavior needs a fix, port it to this repository's skill source and verify it there.
|
||||
12. Standalone SVG export is preview-quality for complex routing because the local renderer draws straight-line edge previews. Use Desktop export or manual draw.io refinement for final orthogonal SVG routing.
|
||||
|
||||
## Create Flow
|
||||
|
||||
1. Identify the diagram type and input format.
|
||||
2. Load the route references from the task-routing table.
|
||||
3. Normalize the request into YAML spec.
|
||||
4. Apply theme, semantic node types, typed connectors, and layout intent.
|
||||
5. Run validation before rendering.
|
||||
6. Render final `.drawio` and `.drawio.svg` in the requested output directory, and write sidecars to a project-local work directory unless the user explicitly asks for a persistent sidecar bundle beside the output.
|
||||
|
||||
Typical commands:
|
||||
|
||||
```bash
|
||||
node <base-skill-dir>/scripts/cli.js input.yaml output.drawio --validate --write-sidecars --sidecar-dir .drawio-tmp/output
|
||||
node <base-skill-dir>/scripts/cli.js input.yaml output.drawio.svg --validate --write-sidecars --sidecar-dir .drawio-tmp/output
|
||||
```
|
||||
|
||||
Use `--strict` or `--strict-warnings` for release-grade engineering review.
|
||||
|
||||
## Edit and Import Flow
|
||||
|
||||
Prefer editing the sidecar bundle. If only a `.drawio` file exists, import it first:
|
||||
|
||||
```bash
|
||||
node <base-skill-dir>/scripts/cli.js existing.drawio --input-format drawio --export-spec --write-sidecars --sidecar-dir .drawio-tmp/existing
|
||||
```
|
||||
|
||||
After import, inspect the generated `.spec.yaml` in the work directory, edit YAML first, then regenerate the requested `.drawio` or `.svg` with sidecars directed to the work directory. Use beside-output sidecars only when the user asks for a reproducible editing bundle.
|
||||
|
||||
## Replicate Flow
|
||||
|
||||
Use `/drawio replicate` for uploaded images or screenshots that need structured redraw.
|
||||
|
||||
1. Extract structure, palette, and text-placement intent.
|
||||
2. Decide whether to preserve source colors or normalize to a theme.
|
||||
3. Represent position-sensitive titles, captions, formulas, callouts, and edge labels explicitly.
|
||||
4. Generate YAML spec with `meta.source: replicated`.
|
||||
5. Render and perform a text-position self-check against the exported SVG or Desktop-exported image before claiming completion.
|
||||
|
||||
## Desktop and Diagrams.net Export
|
||||
|
||||
Desktop-enhanced exports require draw.io Desktop:
|
||||
|
||||
```bash
|
||||
node <base-skill-dir>/scripts/cli.js input.yaml output.pdf --validate --use-desktop
|
||||
node <base-skill-dir>/scripts/cli.js input.yaml output.png --validate --use-desktop
|
||||
node <base-skill-dir>/scripts/cli.js input.yaml output.drawio.svg --validate --write-sidecars --sidecar-dir .drawio-tmp/output --use-desktop
|
||||
```
|
||||
|
||||
If Desktop is unavailable, still deliver the final `.drawio` and `.drawio.svg`, with sidecars in the work directory. For browser handoff, generate a diagrams.net URL from the `.drawio` file:
|
||||
|
||||
```bash
|
||||
node <base-skill-dir>/scripts/runtime/diagrams-net-url.js output.drawio
|
||||
```
|
||||
|
||||
The diagram content is encoded in the URL fragment after `#R` and is not sent as a server query parameter.
|
||||
|
||||
## Style Presets
|
||||
|
||||
The base owns shared bundled style presets under `styles/built-in/`. User presets should live outside the repository, for example `~/.drawio-skill/styles/` or an overlay-specific user directory.
|
||||
|
||||
To learn a reusable preset from an existing diagram ("learn my style from `<path>` as `<name>`") and render an approval sample, follow `references/docs/style-extraction.md`.
|
||||
|
||||
Never mutate bundled presets. Copy a bundled preset to the user preset directory before making it the default or editing it.
|
||||
|
||||
## Validation Policy
|
||||
|
||||
Validate before claiming completion.
|
||||
|
||||
- Structure validation: schema, IDs, theme/layout/profile correctness.
|
||||
- Layout validation: complexity, manual position consistency, overlap risk.
|
||||
- Quality validation: edge-quality rules, label clearance, connection-point policy, and text-placement checks for replication.
|
||||
- Visual verification: inspect exported SVG first, or Desktop-exported PNG/PDF/JPG/embedded SVG when that is the requested final artifact. Use browser/live screenshots only when the user explicitly requested live review and no exported artifact can be inspected.
|
||||
|
||||
If validation fails, fix the YAML or imported XML first and rerun validation. If an optional export cannot run because Desktop or a live backend is unavailable, report the missing provider and provide the offline bundle fallback.
|
||||
|
||||
## Completion Report
|
||||
|
||||
End with a concise report containing:
|
||||
|
||||
- deliverables written, with paths
|
||||
- intermediate work directory, when sidecars or diagnostics were generated
|
||||
- validation and export commands run
|
||||
- exported artifact used for visual verification, or why no visual check could be performed
|
||||
- unavailable optional exports or live-refinement providers
|
||||
- any remaining manual visual checks
|
||||
|
||||
## Reference Highlights
|
||||
|
||||
- `references/workflows/create.md`, `edit.md`, `replicate.md`: route playbooks
|
||||
- `references/docs/design-system/specification.md`: YAML schema and authoring contract
|
||||
- `references/docs/math-typesetting.md`: formula delimiters and export guidance
|
||||
- `references/docs/edge-quality-rules.md`: routing and label-clearance checks
|
||||
- `references/docs/stencil-library-guide.md`: provider-icon and stencil fallback rules
|
||||
- `references/docs/ieee-network-diagrams.md`: IEEE-style network topology and infrastructure reference
|
||||
- `references/docs/mcp-tools.md`: optional live-refinement capability vocabulary
|
||||
- `references/official/xml-reference.md`: upstream XML-generation mirror
|
||||
- `references/official/style-reference.md`: upstream style-property mirror
|
||||
- `references/upstream/pure-drawio-skill.md`: vendored upstream pure-XML skill, for the direct-XML exception path only
|
||||
- `references/docs/style-extraction.md`: learn a reusable style preset from an existing diagram
|
||||
- `references/examples/`: reusable YAML examples
|
||||
@@ -0,0 +1,578 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Draw.io YAML Specification",
|
||||
"description": "Schema for the draw.io skill YAML specification format. Validates diagram structure including nodes, edges, modules, and meta configuration.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"description": "Diagram-level configuration",
|
||||
"properties": {
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Theme name (e.g. tech-blue, academic, nature, dark, high-contrast)"
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
"hierarchical",
|
||||
"star",
|
||||
"mesh"
|
||||
],
|
||||
"description": "Layout direction or topology intent for automatic positioning"
|
||||
},
|
||||
"routing": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"orthogonal",
|
||||
"rounded"
|
||||
],
|
||||
"description": "Connector routing style"
|
||||
},
|
||||
"profile": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default",
|
||||
"academic-paper",
|
||||
"engineering-review"
|
||||
],
|
||||
"description": "Workflow profile that enables domain-specific validation and defaults"
|
||||
},
|
||||
"figureType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"architecture",
|
||||
"roadmap",
|
||||
"workflow"
|
||||
],
|
||||
"description": "Academic figure intent used for paper-mode guidance and validation"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"generated",
|
||||
"replicated",
|
||||
"edited"
|
||||
],
|
||||
"description": "How this spec was produced"
|
||||
},
|
||||
"canvas": {
|
||||
"type": "string",
|
||||
"description": "Canvas size (e.g. auto, 800x600, 1200x800)"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Diagram title"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Diagram description"
|
||||
},
|
||||
"legend": {
|
||||
"type": "string",
|
||||
"description": "Optional legend summary used by academic-paper validation"
|
||||
},
|
||||
"grid": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"snap": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"replication": {
|
||||
"type": "object",
|
||||
"description": "Optional metadata for image-driven redraws and source-palette preservation",
|
||||
"properties": {
|
||||
"colorMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"preserve-original",
|
||||
"theme-first"
|
||||
],
|
||||
"description": "Whether to preserve extracted source colors or normalize them to the selected theme"
|
||||
},
|
||||
"background": {
|
||||
"type": "string",
|
||||
"description": "Detected source background color"
|
||||
},
|
||||
"palette": {
|
||||
"type": "array",
|
||||
"description": "Detected flat colors from the source image",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hex": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"appliesTo": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"canvas",
|
||||
"nodes",
|
||||
"edges",
|
||||
"modules",
|
||||
"mixed"
|
||||
]
|
||||
},
|
||||
"confidence": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"low",
|
||||
"medium",
|
||||
"high"
|
||||
]
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"confidenceNotes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Freeform notes about low-confidence color extraction or normalization decisions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodes": {
|
||||
"type": "array",
|
||||
"description": "Diagram nodes/elements",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"label"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
|
||||
"description": "Unique node identifier"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"maxLength": 200,
|
||||
"description": "Display label"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"service",
|
||||
"database",
|
||||
"decision",
|
||||
"terminal",
|
||||
"queue",
|
||||
"user",
|
||||
"document",
|
||||
"formula",
|
||||
"text",
|
||||
"cloud",
|
||||
"process",
|
||||
"input",
|
||||
"output",
|
||||
"loss",
|
||||
"feature",
|
||||
"conv",
|
||||
"pool",
|
||||
"embed",
|
||||
"temporal",
|
||||
"attention",
|
||||
"gate",
|
||||
"norm",
|
||||
"graph",
|
||||
"matrix",
|
||||
"operator",
|
||||
"tensor3d",
|
||||
"router",
|
||||
"switch",
|
||||
"firewall",
|
||||
"server",
|
||||
"load_balancer",
|
||||
"subnet",
|
||||
"internet",
|
||||
"ap"
|
||||
],
|
||||
"description": "Semantic type for automatic shape selection"
|
||||
},
|
||||
"module": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
|
||||
"description": "Parent module ID"
|
||||
},
|
||||
"size": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xl"
|
||||
],
|
||||
"description": "Size preset"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9._-]*$",
|
||||
"description": "Icon identifier (e.g. aws.lambda, gcp.compute)"
|
||||
},
|
||||
"network": {
|
||||
"type": "object",
|
||||
"description": "Optional network-specific node metadata",
|
||||
"properties": {
|
||||
"device": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9._-]*$"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"vendor": {
|
||||
"type": "string"
|
||||
},
|
||||
"zone": {
|
||||
"type": "string"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string"
|
||||
},
|
||||
"cidr": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"position": {
|
||||
"type": "object",
|
||||
"description": "Manual position override",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"style": {
|
||||
"type": "object",
|
||||
"description": "Style overrides (fillColor, strokeColor, etc.)",
|
||||
"properties": {
|
||||
"fillColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"strokeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"strokeWidth": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"fontColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontSize": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"fontWeight": {
|
||||
"type": "number"
|
||||
},
|
||||
"fontFamily": {
|
||||
"type": "string"
|
||||
},
|
||||
"fontStyle": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 7
|
||||
},
|
||||
"italic": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"bold": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"align": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"left",
|
||||
"center",
|
||||
"right"
|
||||
]
|
||||
},
|
||||
"verticalAlign": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"top",
|
||||
"middle",
|
||||
"bottom"
|
||||
]
|
||||
},
|
||||
"spacingLeft": {
|
||||
"type": "number"
|
||||
},
|
||||
"spacingRight": {
|
||||
"type": "number"
|
||||
},
|
||||
"spacingTop": {
|
||||
"type": "number"
|
||||
},
|
||||
"spacingBottom": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bounds": {
|
||||
"type": "object",
|
||||
"description": "Explicit top-left bounds for high-fidelity replication of text boxes and annotations",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"width",
|
||||
"height"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"edges": {
|
||||
"type": "array",
|
||||
"description": "Connections between nodes",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"from",
|
||||
"to"
|
||||
],
|
||||
"properties": {
|
||||
"from": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
|
||||
"description": "Source node ID"
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
|
||||
"description": "Target node ID"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"primary",
|
||||
"data",
|
||||
"optional",
|
||||
"dependency",
|
||||
"bidirectional"
|
||||
],
|
||||
"description": "Connector semantic type"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Edge label text"
|
||||
},
|
||||
"labelPosition": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"start",
|
||||
"center",
|
||||
"end"
|
||||
],
|
||||
"description": "Label position along edge"
|
||||
},
|
||||
"bidirectional": {
|
||||
"type": "boolean",
|
||||
"description": "Two-way connection flag"
|
||||
},
|
||||
"srcInterface": {
|
||||
"type": "string",
|
||||
"description": "Source interface label for network links"
|
||||
},
|
||||
"dstInterface": {
|
||||
"type": "string",
|
||||
"description": "Target interface label for network links"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"description": "IP address or subnet label for the link"
|
||||
},
|
||||
"vlan": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
],
|
||||
"description": "VLAN identifier for the link"
|
||||
},
|
||||
"bandwidth": {
|
||||
"type": "string",
|
||||
"description": "Bandwidth/capacity label for the link"
|
||||
},
|
||||
"linkType": {
|
||||
"type": "string",
|
||||
"description": "Link media or semantic category (e.g. trunk, access, fiber)"
|
||||
},
|
||||
"style": {
|
||||
"type": "object",
|
||||
"description": "Style overrides for this edge",
|
||||
"properties": {
|
||||
"strokeColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"strokeWidth": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"dashed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dashPattern": {
|
||||
"type": "string"
|
||||
},
|
||||
"endArrow": {
|
||||
"type": "string"
|
||||
},
|
||||
"exitX": {
|
||||
"type": "number"
|
||||
},
|
||||
"exitY": {
|
||||
"type": "number"
|
||||
},
|
||||
"exitDx": {
|
||||
"type": "number"
|
||||
},
|
||||
"exitDy": {
|
||||
"type": "number"
|
||||
},
|
||||
"entryX": {
|
||||
"type": "number"
|
||||
},
|
||||
"entryY": {
|
||||
"type": "number"
|
||||
},
|
||||
"entryDx": {
|
||||
"type": "number"
|
||||
},
|
||||
"entryDy": {
|
||||
"type": "number"
|
||||
},
|
||||
"fontSize": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"fontColor": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"waypoints": {
|
||||
"type": "array",
|
||||
"description": "Optional explicit routing waypoints for orthogonal edges",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"labelOffset": {
|
||||
"type": "object",
|
||||
"description": "Explicit draw.io edge-label offset from the label anchor, in pixels",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number"
|
||||
},
|
||||
"y": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"type": "array",
|
||||
"description": "Container groups for organizing nodes",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"label"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
|
||||
"description": "Unique module identifier"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Module display name"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"description": "Fill color or theme token (e.g. $primary)"
|
||||
},
|
||||
"style": {
|
||||
"type": "object",
|
||||
"description": "Style overrides for this module"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
# Workflow: /drawio create
|
||||
|
||||
Create diagrams from text, Mermaid, CSV, or explicit YAML spec using the Draw.io design system.
|
||||
|
||||
## Trigger
|
||||
|
||||
- **Command**: `/drawio create ...`
|
||||
- **Keywords**: `create`, `generate`, `make`, `draw`, `生成`, `创建`
|
||||
|
||||
## Route Selection
|
||||
|
||||
Determine the route before asking questions:
|
||||
|
||||
1. **Fast Path**
|
||||
- Use when the request already specifies the diagram type and at least 3 of: audience/profile, theme, layout, complexity.
|
||||
- Use when the estimated graph is small (`<= 12` nodes) and not stencil-heavy.
|
||||
2. **Full Path**
|
||||
- Use for ambiguous, large, academic, replication-like, or routing-sensitive diagrams.
|
||||
3. **Academic Branch**
|
||||
- Force-enable when prompt contains `paper`, `academic`, `IEEE`, `journal`, `thesis`, `figure`, `manuscript`, `research`.
|
||||
- Default `meta.profile = academic-paper`.
|
||||
- Classify the figure as `architecture`, `roadmap`, or `workflow` before final layout and set `meta.figureType`.
|
||||
4. **Math / Formula Branch**
|
||||
- Enable when the prompt mentions `formula`, `equation`, `LaTeX`, `AsciiMath`, `MathJax`, `loss function`, `derivation`, `symbol legend`, `公式`, `行内公式`, or `行间公式`.
|
||||
- Load `references/docs/math-typesetting.md` as the syntax source of truth.
|
||||
- Load `references/docs/design-system/formulas.md` for formula-node placement and sizing.
|
||||
5. **Stencil Branch**
|
||||
- Enable when the prompt mentions AWS, Azure, GCP, Cisco, Kubernetes, or vendor icons.
|
||||
- Use `references/docs/stencil-library-guide.md` to decide whether `search_shape_catalog` would help or whether semantic/icon fallbacks are sufficient.
|
||||
|
||||
## Procedure
|
||||
|
||||
```text
|
||||
Step 1: Identify Input Mode
|
||||
├── Natural language
|
||||
├── YAML spec
|
||||
├── Mermaid (flowchart/sequence/class/state/ER/gantt)
|
||||
└── CSV hierarchy/org chart
|
||||
|
||||
Step 2: Determine profile and theme defaults
|
||||
├── academic-paper -> theme academic by default
|
||||
├── academic-paper + explicit color request -> academic-color
|
||||
├── engineering-review -> theme tech-blue by default
|
||||
└── otherwise -> theme from request or tech-blue
|
||||
|
||||
Step 3: Classify academic figure intent when profile=academic-paper
|
||||
├── structure / modules / runtime interaction -> meta.figureType=architecture
|
||||
├── stage progression / milestones / study phases -> meta.figureType=roadmap
|
||||
└── ordered execution / branching / fallback / loop -> meta.figureType=workflow
|
||||
|
||||
Step 4: Decide Fast Path vs Full Path
|
||||
├── Fast Path -> skip AskUserQuestion and skip ASCII confirmation
|
||||
└── Full Path -> continue to Step 5
|
||||
|
||||
Step 5: Design Consultation (Full Path only)
|
||||
├── Ask only unresolved questions:
|
||||
│ • audience/profile
|
||||
│ • theme
|
||||
│ • layout
|
||||
│ • figureType when academic intent is still ambiguous
|
||||
│ • expected complexity
|
||||
└── Store decisions in designIntent and pre-fill YAML meta
|
||||
|
||||
Step 6: Academic / Math / Stencil references
|
||||
├── math/formula request -> load math typesetting + formula integration guide
|
||||
├── academic-paper -> load academic figure playbook + export checklist + IEEE + math typesetting
|
||||
└── stencil-heavy -> decide whether shape search is needed
|
||||
├── if `search_shape_catalog` exists, use it for exact vendor/device lookup
|
||||
└── otherwise use design-system icons or semantic fallbacks
|
||||
|
||||
Step 7: Build the YAML spec
|
||||
├── Normalize Mermaid/CSV inputs to YAML spec
|
||||
├── Ensure meta.theme, meta.layout, meta.profile are present
|
||||
├── Ensure meta.figureType is present when profile=academic-paper
|
||||
├── Use semantic node types and typed connectors
|
||||
└── Add manual positions when branching or dense routing requires it
|
||||
|
||||
Step 8: ASCII Draft (Full Path only)
|
||||
├── Render semantic ASCII draft
|
||||
├── Include Design Summary:
|
||||
│ • theme
|
||||
│ • profile
|
||||
│ • figureType
|
||||
│ • layout
|
||||
│ • node/edge/module counts
|
||||
│ • validation status
|
||||
└── Pause for confirmation only when logic or structure is still ambiguous
|
||||
|
||||
Step 9: Validation
|
||||
├── validateColorScheme()
|
||||
├── validateLayoutConsistency()
|
||||
├── validateConnectionPointPolicy()
|
||||
├── validateEdgeQuality()
|
||||
├── validateAcademicProfile() when profile=academic-paper
|
||||
└── checkComplexity()
|
||||
|
||||
Step 10: Edge Audit
|
||||
├── No corner connection points
|
||||
├── No shared face slots on the same corridor
|
||||
├── Last segment >= 30px
|
||||
├── Labels offset from edge lines
|
||||
├── No waypoint + explicit connection-point mixing
|
||||
└── Prefer straight arrows when alignment allows it
|
||||
|
||||
Step 11: Render
|
||||
├── node <skill-dir>/scripts/cli.js input --input-format <yaml|mermaid|csv> output.drawio --validate --write-sidecars --sidecar-dir .drawio-tmp/output
|
||||
├── For paper-quality diagrams prefer output.svg --validate --write-sidecars --sidecar-dir .drawio-tmp/output
|
||||
├── For thesis / A4 / Word / PNG requests, add a matching PNG only when draw.io Desktop export is available
|
||||
├── Note: standalone SVG (without --use-desktop) is preview-quality (straight-line edges).
|
||||
│ For publication-grade vector output, add --use-desktop or export to .drawio and refine in draw.io.
|
||||
└── When embedded export matters and draw.io Desktop exists, add --use-desktop for SVG or export to PNG/PDF/JPG
|
||||
|
||||
Step 12: Exported-Artifact Verification / Optional Live Handoff
|
||||
├── Inspect the exported SVG first when it is available and readable by the current environment
|
||||
├── If a raster/final-fidelity check is needed and draw.io Desktop is available -> export PNG/PDF/JPG or embedded SVG through the CLI
|
||||
├── Do not create browser or Playwright screenshots when an exported SVG/PNG/PDF/JPG exists
|
||||
├── live backend has `replace_diagram_xml` + user wants browser or inline refinement
|
||||
│ └── use the provider-specific tool mapping from `references/docs/mcp-tools.md`
|
||||
├── browser/live screenshots are a last-resort review aid only when the user explicitly requested live review and no exported artifact can be inspected
|
||||
└── otherwise present .drawio + standalone SVG and report any remaining manual visual check
|
||||
```
|
||||
|
||||
## Academic Branch Rules
|
||||
|
||||
When `meta.profile = academic-paper`:
|
||||
|
||||
- `meta.figureType` is required and must be exactly `architecture`, `roadmap`, or `workflow`.
|
||||
- `meta.title` is required for figure captioning.
|
||||
- `meta.description` is recommended for figure context.
|
||||
- `meta.legend` is required when icons are used or connector types are mixed.
|
||||
- Prefer `academic` theme unless the request explicitly asks for a color paper figure.
|
||||
- Default final deliverables are `.drawio` and `.svg`; keep `.spec.yaml` and `.arch.json` in a project-local work directory unless a sidecar bundle is explicitly requested.
|
||||
- Add `.png` only for thesis, A4, Word, raster-first, screenshot rebuild, or explicit PNG requests.
|
||||
- Do not rely on color alone to distinguish semantics.
|
||||
- Treat A4 readability and grayscale print safety as final review gates, not optional polish.
|
||||
|
||||
## Math / Formula Branch Rules
|
||||
|
||||
When the request includes formulas, equations, or math-heavy labels:
|
||||
|
||||
- Use `$$...$$` only for standalone equations or labels that are entirely formula content.
|
||||
- Use `\(...\)` for sentence-level inline math inside a longer label.
|
||||
- Use `` `...` `` only when the user explicitly prefers AsciiMath or when the notation is simple.
|
||||
- Do not generate bare LaTeX, `$...$`, or `\[...\]` in final YAML/XML output.
|
||||
- Tell the user to enable `Extras > Mathematical Typesetting` when raw formulas may be edited in draw.io.
|
||||
- For PDF exports where selectable math matters, recommend `math-output=html`.
|
||||
|
||||
## Notes
|
||||
|
||||
- YAML remains the canonical intermediate representation.
|
||||
- `.drawio` is the editable final artifact; `.spec.yaml` and `.arch.json` remain the canonical offline sidecars in the work directory unless the user explicitly requests a beside-output bundle.
|
||||
- Mermaid and CSV inputs are convenience adapters, not separate rendering pipelines.
|
||||
- For formula-bearing labels, use only the three supported syntaxes: `$$...$$`, `\(...\)`, and `` `...` ``.
|
||||
- Stencil-heavy requests may use shape search when available, but the create flow must still succeed without it.
|
||||
- Academic figures should not blend structure, progression, and control flow into one ambiguous visual grammar.
|
||||
386
.claude/skills/canvas-workspace/references/drawio/scripts/cli.js
Normal file
386
.claude/skills/canvas-workspace/references/drawio/scripts/cli.js
Normal file
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI tool for converting YAML specifications to draw.io XML or SVG
|
||||
* Usage: node cli.js input.yaml [output.drawio|output.svg] [--theme name] [--strict] [--validate]
|
||||
*/
|
||||
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, extname, join, resolve } from 'node:path'
|
||||
import { parseSpecYaml, specToDrawioXml, validateSpec, validateXml } from './dsl/spec-to-drawio.js'
|
||||
import { parseMermaidToSpec, parseCsvToSpec } from './adapters/index.js'
|
||||
import { drawioToSpec } from './dsl/drawio-to-spec.js'
|
||||
import {
|
||||
buildArchMetadata,
|
||||
createDrawioFileContent,
|
||||
deriveArtifactPaths,
|
||||
serializeSpecYaml
|
||||
} from './runtime/artifacts.js'
|
||||
import { exportWithDrawioDesktop, isDesktopExportFormat } from './runtime/desktop.js'
|
||||
|
||||
/** draw.io format compatibility version */
|
||||
const DRAWIO_COMPAT_VERSION = '21.0.0'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Argument parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
||||
console.log(
|
||||
`
|
||||
draw.io YAML → XML/SVG Converter
|
||||
|
||||
Usage:
|
||||
node cli.js <input> [output.drawio|output.svg] [options]
|
||||
|
||||
Arguments:
|
||||
input Path to input file, or - for stdin
|
||||
output file Optional output file. Extension determines format:
|
||||
.drawio → draw.io XML file format
|
||||
.svg → Standalone SVG (or desktop SVG with --use-desktop)
|
||||
.png → PNG via draw.io Desktop CLI
|
||||
.pdf → PDF via draw.io Desktop CLI
|
||||
.jpg → JPG via draw.io Desktop CLI
|
||||
If omitted, XML is printed to stdout.
|
||||
|
||||
Options:
|
||||
--input-format <f> Input format: yaml (default), mermaid, csv, drawio
|
||||
--theme <name> Override theme (e.g. tech-blue, academic, nature, dark)
|
||||
--page <selector> drawio only: page index (0-based) or diagram name
|
||||
--export-spec Export the canonical YAML spec instead of generating XML/SVG
|
||||
--strict Fail on complexity and spec validation warnings
|
||||
--strict-warnings Alias of --strict (recommended for paper-grade validation)
|
||||
--validate Run XML validation and print results (also summarizes spec warnings)
|
||||
--write-sidecars Emit canonical .spec.yaml and .arch.json next to the output
|
||||
--sidecar-dir <dir> Emit sidecars in this directory when --write-sidecars is set
|
||||
--use-desktop Prefer draw.io Desktop CLI for SVG export; required for PNG/PDF/JPG
|
||||
--help, -h Show this help message
|
||||
`.trim()
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Extract positional arguments (non-flag args, excluding values of --flags)
|
||||
const flagsWithValues = new Set(['--theme', '--input-format', '--page', '--sidecar-dir'])
|
||||
const positional = []
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (flagsWithValues.has(args[i])) {
|
||||
i++ // skip the flag value
|
||||
} else if (!args[i].startsWith('--')) {
|
||||
positional.push(args[i])
|
||||
}
|
||||
}
|
||||
const inputFile = positional[0]
|
||||
const outputFile = positional[1] || null
|
||||
|
||||
// Extract flags
|
||||
const themeIndex = args.indexOf('--theme')
|
||||
const themeName = themeIndex !== -1 ? args[themeIndex + 1] : null
|
||||
const inputFormatIndex = args.indexOf('--input-format')
|
||||
const inputFormat = inputFormatIndex !== -1 ? args[inputFormatIndex + 1] : 'yaml'
|
||||
const strict = args.includes('--strict') || args.includes('--strict-warnings')
|
||||
const doValidate = args.includes('--validate')
|
||||
const writeSidecars = args.includes('--write-sidecars')
|
||||
const useDesktop = args.includes('--use-desktop')
|
||||
const exportSpec = args.includes('--export-spec')
|
||||
const pageIndex = args.indexOf('--page')
|
||||
const pageSelector = pageIndex !== -1 ? args[pageIndex + 1] : null
|
||||
const sidecarDirIndex = args.indexOf('--sidecar-dir')
|
||||
const sidecarDir = sidecarDirIndex !== -1 ? args[sidecarDirIndex + 1] : null
|
||||
const resolvedSidecarDir = sidecarDir ? resolve(sidecarDir) : null
|
||||
|
||||
if (sidecarDirIndex !== -1 && (!sidecarDir || sidecarDir.startsWith('--'))) {
|
||||
console.error('Error: --sidecar-dir requires a directory path.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (sidecarDir && !writeSidecars) {
|
||||
console.error('Error: --sidecar-dir requires --write-sidecars.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (resolvedSidecarDir) {
|
||||
try {
|
||||
mkdirSync(resolvedSidecarDir, { recursive: true })
|
||||
} catch (err) {
|
||||
console.error(`Error: Could not create sidecar directory "${sidecarDir}": ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SVG module (optional)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let drawioToSvg = null
|
||||
try {
|
||||
const svgModule = await import('./svg/drawio-to-svg.js')
|
||||
drawioToSvg = svgModule.drawioToSvg
|
||||
} catch {
|
||||
// SVG export not available
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Read and convert
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let inputText
|
||||
if (inputFile === '-' || (!inputFile && !process.stdin.isTTY)) {
|
||||
const chunks = []
|
||||
for await (const chunk of process.stdin) chunks.push(chunk)
|
||||
inputText = Buffer.concat(chunks).toString('utf-8')
|
||||
} else if (!inputFile) {
|
||||
console.error('Error: input file is required. Use - for stdin.')
|
||||
process.exit(1)
|
||||
} else {
|
||||
try {
|
||||
inputText = readFileSync(resolve(inputFile), 'utf-8')
|
||||
} catch (err) {
|
||||
console.error(`Error: Could not read input file "${inputFile}": ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
let spec
|
||||
try {
|
||||
if (inputFormat === 'yaml') {
|
||||
spec = parseSpecYaml(inputText)
|
||||
} else if (inputFormat === 'mermaid') {
|
||||
spec = parseMermaidToSpec(inputText, { profile: themeName?.startsWith('academic') ? 'academic-paper' : 'default' })
|
||||
} else if (inputFormat === 'csv') {
|
||||
spec = parseCsvToSpec(inputText, { profile: themeName?.startsWith('academic') ? 'academic-paper' : 'default' })
|
||||
} else if (inputFormat === 'drawio') {
|
||||
spec = drawioToSpec(inputText, { theme: themeName || undefined, page: pageSelector })
|
||||
} else {
|
||||
throw new Error(`Unsupported input format "${inputFormat}"`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error: Failed to parse ${inputFormat}: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
validateSpec(spec)
|
||||
} catch (err) {
|
||||
console.error(`Error: Spec validation failed: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Apply CLI theme override
|
||||
if (themeName) {
|
||||
spec.meta = spec.meta || {}
|
||||
spec.meta.theme = themeName
|
||||
}
|
||||
|
||||
let xml
|
||||
try {
|
||||
if (exportSpec) {
|
||||
xml = null
|
||||
} else if (doValidate) {
|
||||
const result = specToDrawioXml(spec, { strict, returnWarnings: true, silent: true })
|
||||
xml = result.xml
|
||||
const problems = (result.warnings || []).filter((w) => w.level && w.level !== 'fatal')
|
||||
if (problems.length === 0) {
|
||||
console.error('Spec validation: PASSED (no warnings)')
|
||||
} else {
|
||||
console.error(`Spec validation: WARNINGS (${problems.length})`)
|
||||
problems.forEach((w) => console.error(` • [${w.level}] ${w.message}`))
|
||||
}
|
||||
} else {
|
||||
xml = specToDrawioXml(spec, { strict })
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error: Conversion failed: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (doValidate && !exportSpec) {
|
||||
const result = validateXml(xml)
|
||||
if (result.valid) {
|
||||
console.error('XML validation: PASSED (no errors)')
|
||||
} else {
|
||||
console.error('XML validation: FAILED')
|
||||
for (const e of result.errors) {
|
||||
console.error(` - ${e}`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!exportSpec &&
|
||||
spec.meta?.profile === 'academic-paper' &&
|
||||
outputFile &&
|
||||
extname(outputFile).toLowerCase() !== '.svg'
|
||||
) {
|
||||
console.error('Validation: academic-paper profile recommends SVG export for paper-ready vector output.')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (exportSpec) {
|
||||
const yamlOut = serializeSpecYaml(spec)
|
||||
let specPath = outputFile
|
||||
if (!specPath && inputFormat === 'drawio' && inputFile && inputFile !== '-') {
|
||||
specPath = deriveArtifactPaths(inputFile).specPath
|
||||
}
|
||||
|
||||
if (specPath && resolvedSidecarDir) {
|
||||
specPath = resolve(resolvedSidecarDir, basename(specPath))
|
||||
}
|
||||
|
||||
if (!specPath) {
|
||||
process.stdout.write(yamlOut)
|
||||
if (!yamlOut.endsWith('\n')) process.stdout.write('\n')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(resolve(specPath), yamlOut, 'utf-8')
|
||||
console.error(`Saved spec: ${specPath}`)
|
||||
} catch (err) {
|
||||
console.error(`Error: Could not write spec file "${specPath}": ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (writeSidecars) {
|
||||
const normalized = specPath.replace(/\\/g, '/')
|
||||
let archPath = null
|
||||
if (/\.spec\.ya?ml$/i.test(normalized)) {
|
||||
archPath = normalized.replace(/\.spec\.ya?ml$/i, '.arch.json')
|
||||
} else if (/\.ya?ml$/i.test(normalized)) {
|
||||
archPath = normalized.replace(/\.ya?ml$/i, '.arch.json')
|
||||
}
|
||||
|
||||
if (archPath) {
|
||||
if (resolvedSidecarDir) {
|
||||
archPath = resolve(resolvedSidecarDir, basename(archPath))
|
||||
}
|
||||
const drawioPath = /\.arch\.json$/i.test(archPath) ? archPath.replace(/\.arch\.json$/i, '.drawio') : null
|
||||
try {
|
||||
writeFileSync(
|
||||
resolve(archPath),
|
||||
JSON.stringify(buildArchMetadata(spec, { outputFile: drawioPath || specPath }), null, 2) + '\n',
|
||||
'utf-8'
|
||||
)
|
||||
console.error(`Saved arch: ${archPath}`)
|
||||
} catch (err) {
|
||||
console.error(`Error: Could not write arch file "${archPath}": ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!outputFile) {
|
||||
process.stdout.write(xml)
|
||||
process.stdout.write('\n')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const ext = extname(outputFile).toLowerCase()
|
||||
const drawioContent = createDrawioFileContent(xml, { version: DRAWIO_COMPAT_VERSION })
|
||||
const artifactPaths = deriveArtifactPaths(outputFile)
|
||||
const sidecarArtifactPaths = resolvedSidecarDir
|
||||
? deriveArtifactPaths(resolve(resolvedSidecarDir, basename(artifactPaths.drawioPath)))
|
||||
: artifactPaths
|
||||
const needsDesktopExport = isDesktopExportFormat(ext.slice(1)) && (ext !== '.svg' || useDesktop)
|
||||
let tempDir = null
|
||||
let desktopInputPath = null
|
||||
|
||||
function writeCanonicalSidecars() {
|
||||
if (!writeSidecars) return
|
||||
|
||||
writeFileSync(resolve(sidecarArtifactPaths.specPath), serializeSpecYaml(spec), 'utf-8')
|
||||
writeFileSync(
|
||||
resolve(sidecarArtifactPaths.archPath),
|
||||
JSON.stringify(buildArchMetadata(spec, { outputFile }), null, 2) + '\n',
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
function ensureDesktopInput() {
|
||||
if (desktopInputPath) return desktopInputPath
|
||||
|
||||
if (writeSidecars) {
|
||||
desktopInputPath = resolve(artifactPaths.drawioPath)
|
||||
writeFileSync(desktopInputPath, drawioContent, 'utf-8')
|
||||
return desktopInputPath
|
||||
}
|
||||
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'drawio-skill-'))
|
||||
desktopInputPath = resolve(tempDir, 'export-input.drawio')
|
||||
writeFileSync(desktopInputPath, drawioContent, 'utf-8')
|
||||
return desktopInputPath
|
||||
}
|
||||
|
||||
let exitCode = 0
|
||||
|
||||
try {
|
||||
if (ext === '.drawio') {
|
||||
writeFileSync(resolve(outputFile), drawioContent, 'utf-8')
|
||||
writeCanonicalSidecars()
|
||||
console.error(`Saved: ${outputFile}`)
|
||||
} else if (needsDesktopExport) {
|
||||
try {
|
||||
exportWithDrawioDesktop({
|
||||
inputFile: ensureDesktopInput(),
|
||||
outputFile: resolve(outputFile),
|
||||
format: ext.slice(1)
|
||||
})
|
||||
writeCanonicalSidecars()
|
||||
console.error(`Saved: ${outputFile}`)
|
||||
} catch (err) {
|
||||
console.error(`Error: ${err.message}`)
|
||||
exitCode = 1
|
||||
}
|
||||
} else if (ext === '.svg') {
|
||||
if (!drawioToSvg) {
|
||||
console.error('Error: SVG export is not available (drawio-to-svg module not found).')
|
||||
exitCode = 1
|
||||
} else {
|
||||
let svg
|
||||
try {
|
||||
svg = drawioToSvg(xml)
|
||||
} catch (err) {
|
||||
console.error(`Error: SVG conversion failed: ${err.message}`)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
if (exitCode === 0) {
|
||||
try {
|
||||
writeFileSync(resolve(outputFile), svg, 'utf-8')
|
||||
if (writeSidecars) {
|
||||
writeFileSync(resolve(artifactPaths.drawioPath), drawioContent, 'utf-8')
|
||||
}
|
||||
writeCanonicalSidecars()
|
||||
console.error(`Saved SVG: ${outputFile}`)
|
||||
} catch (err) {
|
||||
console.error(`Error: Could not write output file "${outputFile}": ${err.message}`)
|
||||
exitCode = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
`Error: Unsupported output extension "${ext || '(none)'}". ` + 'Use .drawio, .svg, .png, .pdf, or .jpg/.jpeg.'
|
||||
)
|
||||
exitCode = 1
|
||||
}
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(exitCode)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"js-yaml": "^4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Excalidraw 基础指导
|
||||
|
||||
这份只说明如何把内容组织成普通 Excalidraw 图,不规定 Axhub 画布流程。流程图、关系图、架构图等请求先按 `canvas-workspace` 主文档分流;已确定要画成 Excalidraw 元素后,再读本文件。
|
||||
|
||||
## 先判断图类型
|
||||
|
||||
| 用户意图 | 适合图形 |
|
||||
|----------|----------|
|
||||
| 流程、步骤、审批、状态流转 | 流程图 |
|
||||
| 模块关系、依赖、信息结构 | 关系图 |
|
||||
| 概念拆解、头脑风暴 | 思维导图 |
|
||||
| 系统、页面、组件、服务结构 | 架构图 |
|
||||
| 数据输入、处理、输出 | 数据流图 |
|
||||
| 多角色协作流程 | 泳道图 |
|
||||
|
||||
如果画布内容涉及真实图片、UI 设计稿或素材,先按用户目标确认它是画布参考、画布节点,还是项目实现素材。
|
||||
|
||||
## 抽取信息
|
||||
|
||||
动手画之前先明确:
|
||||
|
||||
- 关键元素:节点、步骤、角色、页面、模块。
|
||||
- 关系:先后、依赖、父子、输入输出、跳转。
|
||||
- 复杂度:元素太多时拆成多个 Frame 或多张图。
|
||||
- 主次:优先画主路径,细节放到旁边补充。
|
||||
|
||||
## 布局原则
|
||||
|
||||
- 流程图:从左到右或从上到下。
|
||||
- 关系图:核心元素居中,相关元素围绕或分组。
|
||||
- 架构图:按层级、职责或数据流分区。
|
||||
- 思维导图:中心主题 + 4-6 个主分支。
|
||||
- 泳道图:角色用列或行,活动放进对应泳道。
|
||||
|
||||
相关内容使用 Frame 分组,组内元素用 `frameId` 归属到对应 Frame。
|
||||
|
||||
## 控制复杂度
|
||||
|
||||
- 单张图优先控制在 20 个核心元素以内。
|
||||
- 复杂请求先画高层图,再按模块拆详细图。
|
||||
- 避免把长文档逐句搬进画布;画结构和决策点。
|
||||
- 颜色只表达语义,不做装饰。
|
||||
|
||||
## 与 Axhub 规范的边界
|
||||
|
||||
- 画布文件仍写入 `src/prototypes/<prototype-name>/canvas.excalidraw`。
|
||||
- 字体、颜色、节点、资源和交付口径仍按本技能主文档与其他 references 执行。
|
||||
99
.claude/skills/explore-options/SKILL.md
Normal file
99
.claude/skills/explore-options/SKILL.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: explore-options
|
||||
description: Use when a Make client批注 or user request asks for 多方案探索, 多方案生成, 多方案对比, 方案对比, 设计决策, 比稿, 先出方案, or choosing among 2-3 UI/code modification directions before executing one.
|
||||
---
|
||||
|
||||
# 多方案探索
|
||||
|
||||
用于处理批注或需求里的多方案探索、生成和对比。目标是延续 Make client 的需求对齐和设计决策口径:先围绕当前问题发散出真实不同的设计方向,再通过方案对比收敛为一个可执行的设计决策。
|
||||
|
||||
## 流程
|
||||
|
||||
1. 先锁定共同约束:产品需求、当前页面目标、用户明确要求、不可改内容、设计基底和验收重点。
|
||||
2. 先发散:没有指定数量时默认给 3 个方案;用户指定数量时严格按用户要求。
|
||||
3. 方案差异必须来自页面骨架、信息组织、交互路径、内容密度、视觉反馈、动效状态或内容表达,不只换色换皮。
|
||||
4. 方案梯度建议覆盖:稳健继承现有模式、平衡优化当前体验、突破探索新表达。不要让多个方案只是同一想法的轻微改写。
|
||||
5. 所有方案都要遵守已确认的需求和设计决策,不能把锁定项当差异点。
|
||||
6. 再收敛:比较方案适合的用户任务、实现成本、设计一致性、风险和可验证性,形成一个设计决策。
|
||||
7. 执行最适合当前页面的一种;只有会改变需求范围、设计基底、核心流程或用户明确要求选择时,才停下来确认。
|
||||
|
||||
## 输出
|
||||
|
||||
- 多方案探索:每个方案一句话说明,体现真实差异。
|
||||
- 差异说明:覆盖骨架、信息/交互、视觉/反馈、动效/状态和主要风险。
|
||||
- 设计决策:写明采用哪个方案、为什么淘汰其他方案,以及取舍理由。
|
||||
- 执行结果:列出改了什么和如何验收。
|
||||
|
||||
## 方案切换落地
|
||||
|
||||
如果用户要求“看看不同方向”“能切换比较”,或当前实现适合在页面内切换方案,就把方案做成 tweak:
|
||||
|
||||
- React 原型优先使用 `@axhub/commentary-react` 的 `createCommentaryReactTweakStore` 和 `useRegisterCommentaryTweak`。
|
||||
- 复用项目现有 `schema / values / adapter / update` 模式,不另造平行配置。
|
||||
- 方案字段优先用 `card`,不要用普通下拉。
|
||||
- 每个 `options[]` 项至少包含 `label`、`description`、`value`。
|
||||
- `label` 写方案标题,`description` 写核心差异,`value` 写稳定标识。
|
||||
- 用户要求页面内切换时,必须落成 React tweak;否则只需要完成多方案探索、方案对比、设计决策和最终方案实现。
|
||||
|
||||
常用字段类型:
|
||||
|
||||
| type | 适用场景 |
|
||||
| --- | --- |
|
||||
| `card` | 多方案对比、布局方向、风格方向等带标题和描述的单选方案 |
|
||||
| `select` | 简单枚举,例如尺寸、密度、variant |
|
||||
| `switch` | 显示/隐藏、启用/禁用等布尔态 |
|
||||
| `slider` | 连续数值,例如透明度、比例、间距强度 |
|
||||
| `number` | 精确数值,例如列数、条目数、固定尺寸 |
|
||||
| `text` | 需要长期配置的短标题或按钮文案 |
|
||||
| `color` | 主题色、强调色、图表主色,优先使用语义色或 token |
|
||||
|
||||
最佳实践:
|
||||
|
||||
- 多方向单选用 `card`,这是方案对比的默认选择。
|
||||
- 布尔状态用 `switch`;简单枚举用 `select`;连续调节用 `slider`。
|
||||
- 默认只暴露 3-5 个最影响 UI/UX 的字段,不要把底层 props 全摊开。
|
||||
- 文案通常可以直接编辑;只有需要长期配置或和方案切换绑定时才放进 schema。
|
||||
- 字段名用用户能理解的语义,不直接暴露第三方库或实现细节。
|
||||
|
||||
React 最小形态:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
createCommentaryReactTweakStore,
|
||||
useCommentaryReactTweakStore,
|
||||
useRegisterCommentaryTweak,
|
||||
} from '@axhub/commentary-react';
|
||||
|
||||
const optionSchema = {
|
||||
title: '多方案探索',
|
||||
fields: [{
|
||||
key: 'variant',
|
||||
label: '方案',
|
||||
type: 'card',
|
||||
options: [
|
||||
{ label: '稳健型', description: '继承现有结构,小幅优化体验。', value: 'steady' },
|
||||
{ label: '平衡型', description: '调整信息组织和交互路径。', value: 'balanced' },
|
||||
{ label: '突破型', description: '探索更有差异的结构和反馈。', value: 'bold' },
|
||||
],
|
||||
}],
|
||||
};
|
||||
|
||||
function Example() {
|
||||
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const store = React.useMemo(
|
||||
() => createCommentaryReactTweakStore({ variant: 'balanced' }),
|
||||
[],
|
||||
);
|
||||
const values = useCommentaryReactTweakStore(store);
|
||||
|
||||
useRegisterCommentaryTweak({
|
||||
elementRef: rootRef,
|
||||
schema: optionSchema,
|
||||
store,
|
||||
onUpdate: async (patch) => store.update(patch),
|
||||
});
|
||||
|
||||
return <section ref={rootRef}>{renderVariant(values.variant)}</section>;
|
||||
}
|
||||
```
|
||||
4
.claude/skills/explore-options/agents/openai.yaml
Normal file
4
.claude/skills/explore-options/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "多方案探索"
|
||||
short_description: "批注或需求需要多方案、比稿或设计决策时,先比较 2-3 个方向"
|
||||
default_prompt: "使用 $explore-options 对当前批注做多方案探索,形成设计决策后执行最合适的一种。"
|
||||
77
.claude/skills/prototype-annotation/SKILL.md
Normal file
77
.claude/skills/prototype-annotation/SKILL.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: prototype-annotation
|
||||
description: 原型标注替代 PRD 时使用:把页面目录、组件说明、状态说明和补充文档接入可运行原型,供评审、交付和后续需求说明使用。
|
||||
---
|
||||
|
||||
# 原型标注
|
||||
|
||||
## 概览
|
||||
|
||||
这是 Axhub Make 客户端原型标注的技术使用指南。它面向 `src/prototypes/` 下的多原型项目,说明如何把 `@axhub/annotation` 接到任意原型里,并维护三类标注能力:目录、组件标注、组件状态。
|
||||
|
||||
这个技能只约束技术接入方式。不要在这里替用户决定标注文案、设计原则、交付场景或页面风格;这些内容以用户需求、原型资料和项目规范为准。
|
||||
|
||||
## 使用场景
|
||||
|
||||
| 场景 | 使用能力 | 数据位置 |
|
||||
| --- | --- | --- |
|
||||
| 目录 | 原型入口、文档入口、链接入口 | `directory.nodes` |
|
||||
| 组件标注 | 给页面元素挂 marker 和说明 | `data.nodes[]` + `locator` |
|
||||
| 组件状态 | 给某个组件提供可切换状态 | `data.nodes[].controls` + `useProtoDevState` |
|
||||
|
||||
## 主流程
|
||||
|
||||
1. 找到目标原型:`src/prototypes/<prototype-id>/`。
|
||||
2. 读取该原型的页面代码、已有 `annotation-source.json` 和相邻资料。
|
||||
3. 判断本次需要哪类能力:目录、组件标注、组件状态。
|
||||
4. 使用 `AnnotationSourceDocument` wire format 维护 `annotation-source.json`。
|
||||
5. 页面里通过 `AnnotationViewer` 静态导入同一份 JSON。
|
||||
6. 多页面或多状态原型要把当前页面/状态传给 `currentPageId`,或通过 `getCurrentPageId` 返回。
|
||||
7. 目录 `route` 节点只会回调宿主;在 `onDirectoryRoute` 里切换页面、状态、数据源或 URL。
|
||||
8. 按改动范围运行验证,通常是:
|
||||
```bash
|
||||
npm run typecheck
|
||||
node scripts/check-app-ready.mjs /prototypes/<prototype-name>
|
||||
```
|
||||
|
||||
需要字段结构、接入代码或控件示例时,读取 `references/axhub-annotation.md`。
|
||||
|
||||
## 目录
|
||||
|
||||
目录不是页面 marker,不需要 `locator`。它用于把多原型项目中的原型、文档和链接组织到标注面板里。
|
||||
|
||||
- `folder`:分组目录节点。
|
||||
- `route`:交给宿主处理,可切当前原型页面、状态、数据源或路由。
|
||||
- `markdown`:打开内联 Markdown 文档。
|
||||
- `markdownPath`:只用于目录 Markdown 文档,可指向当前原型目录内的 `docs/*.md`;客户端构建链路会内联为运行时读取的 `markdown`。
|
||||
- `link`:打开其他原型地址、资源地址或外部链接。
|
||||
|
||||
多原型入口优先用 `link` 指向 `/prototypes/<prototype-id>` 或完整 URL;当前原型内部页面/状态入口再用 `route`。
|
||||
|
||||
## 组件标注
|
||||
|
||||
组件标注使用 `data.nodes[]`。每个节点至少需要稳定 `id`、`locator`、正文字段和时间字段。
|
||||
|
||||
- 优先给目标元素加稳定选择器,例如 `data-annotation-id="<node-id>"`。
|
||||
- `pageId` 可省略;省略时该节点在所有当前页面上下文下显示。
|
||||
- `pageId` 可以是字符串或字符串数组,用于限制 marker 出现在哪些页面/状态。
|
||||
- `hasMarkdown: false` 使用 `annotationText` 和 `images`。
|
||||
- `hasMarkdown: true` 使用 `markdownMap[node.id]`,运行时会忽略 `annotationText` 和 `images`。
|
||||
|
||||
## 组件状态
|
||||
|
||||
组件状态使用节点上的 `controls`。运行时会把控件值写入 proto dev state,页面通过 `useProtoDevState` 读取并渲染对应状态。
|
||||
|
||||
- JSON 中的 `controls` 只写可序列化字段。
|
||||
- 支持控件类型包括 `input`、`inputNumber`、`select`、`segmented`、`switch`、`checkbox`、`slider`、`textarea`、`text`、`button`、`colorPicker`。
|
||||
- 三个或三个以下的离散选项通常用 `segmented`,让选项直接展示。
|
||||
- 如果目录 `route` 也要切状态,在 `onDirectoryRoute` 里读取 `node.payload` 并调用页面自己的状态切换逻辑或 `setProtoDevState`。
|
||||
|
||||
## 使用注意
|
||||
|
||||
- 不要写当前公开 API 里不存在的参数;`AnnotationViewerOptions` 以 `@axhub/annotation` 类型定义为准。
|
||||
- 不要把目录节点误写成组件标注节点;目录没有 marker。
|
||||
- 不要把组件状态只写进页面本地 state;需要出现在标注面板里的状态要写进节点 `controls`。
|
||||
- 不要依赖不稳定 CSS 选择器作为唯一定位方式;能补稳定属性时优先补。
|
||||
- `showBrandLink`、`defaultMarkerIndexVisible`、`renderToolbarActions` 这类展示增强选项只有在用户明确要求品牌入口、默认显示序号或工具栏自定义动作时才设置;常规标注接入保持默认配置。
|
||||
- Markdown 图片必须随原型发布:放到当前原型 `assets/` 并用最终可访问 URL;不要用本地路径、`/api/markdown-file` 或 `../assets/...`。
|
||||
4
.claude/skills/prototype-annotation/agents/openai.yaml
Normal file
4
.claude/skills/prototype-annotation/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "原型标注"
|
||||
short_description: "用原型替代 PRD 交付时,接入页面目录、组件说明和状态说明"
|
||||
default_prompt: "使用 $prototype-annotation 为这个原型接入或维护标注能力。"
|
||||
@@ -0,0 +1,239 @@
|
||||
# Axhub 标注参考
|
||||
|
||||
实现标注数据或把标注运行时接入 Make 客户端原型时,使用这份参考。当前推荐方式是每个原型维护 `annotation-source.json`,页面通过静态 import 传给 `AnnotationViewer`。
|
||||
|
||||
## React 接入
|
||||
|
||||
```tsx
|
||||
import {
|
||||
AnnotationViewer,
|
||||
setProtoDevState,
|
||||
useProtoDevState,
|
||||
} from '@axhub/annotation';
|
||||
import type {
|
||||
AnnotationDirectoryRouteNode,
|
||||
AnnotationSourceDocument,
|
||||
AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import annotationSourceDocument from './annotation-source.json';
|
||||
```
|
||||
|
||||
在原型里挂载一次 viewer。多页面原型把当前页面 id 传给 `currentPageId`;目录 route 的行为由宿主决定。
|
||||
|
||||
```tsx
|
||||
const options = useMemo<AnnotationViewerOptions>(() => ({
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
emptyWhenNoData: false,
|
||||
toolbarEdge: 'right',
|
||||
currentPageId: activePageId,
|
||||
onDirectoryRoute: (node: AnnotationDirectoryRouteNode) => {
|
||||
if (typeof node.route === 'string') setPage(node.route);
|
||||
},
|
||||
}), [activePageId, setPage]);
|
||||
|
||||
<AnnotationViewer
|
||||
source={annotationSourceDocument as AnnotationSourceDocument}
|
||||
options={options}
|
||||
/>
|
||||
```
|
||||
|
||||
当前公开的 `AnnotationViewerOptions` 包括:
|
||||
|
||||
- `showToolbar`
|
||||
- `showThemeToggle`
|
||||
- `showColorFilter`
|
||||
- `toolbarEdge`
|
||||
- `toolbarAutoHide`
|
||||
- `zIndex`
|
||||
- `emptyWhenNoData`
|
||||
- `currentPageId`
|
||||
- `getCurrentPageId`
|
||||
- `onDirectoryRoute`
|
||||
|
||||
## 数据源结构
|
||||
|
||||
```json
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": {
|
||||
"version": 2,
|
||||
"prototypeName": "prototype-id",
|
||||
"pageId": "default-page-id",
|
||||
"updatedAt": 1779496800000,
|
||||
"nodes": []
|
||||
},
|
||||
"markdownMap": {},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
"nodes": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 目录
|
||||
|
||||
`directory.nodes` 驱动标注面板里的目录。目录节点不绑定页面元素,不显示 marker。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "project-directory",
|
||||
"title": "项目目录",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "link",
|
||||
"id": "prototype-dashboard",
|
||||
"title": "数据看板原型",
|
||||
"href": "/prototypes/dashboard",
|
||||
"target": "self"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "doc-overview",
|
||||
"title": "需求说明",
|
||||
"markdown": "# 需求说明\n\n这里写内联文档。"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"id": "external-spec",
|
||||
"title": "外部资料",
|
||||
"href": "https://example.com/spec",
|
||||
"target": "blank"
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"id": "route-empty",
|
||||
"title": "切换空状态",
|
||||
"route": "orders",
|
||||
"payload": { "state": "empty" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `folder`:目录分组。
|
||||
- `link`:打开其他原型、资源或外链;多原型入口常用 `/prototypes/<prototype-id>`。
|
||||
- `markdown`:必须使用内联 `markdown` 字段;运行时不会通过 `markdownId` 自动去 `markdownMap` 查正文。
|
||||
- `route`:点击时调用 `options.onDirectoryRoute(node)`,运行时不替宿主跳转。
|
||||
|
||||
route 回调示例:
|
||||
|
||||
```tsx
|
||||
const options = useMemo<AnnotationViewerOptions>(() => ({
|
||||
currentPageId: activePageId,
|
||||
onDirectoryRoute: (node) => {
|
||||
if (typeof node.route === 'string') {
|
||||
setActivePageId(node.route);
|
||||
}
|
||||
const payload = node.payload as { state?: string } | undefined;
|
||||
if (payload?.state) {
|
||||
setProtoDevState({ order_state: payload.state });
|
||||
}
|
||||
},
|
||||
}), [activePageId]);
|
||||
```
|
||||
|
||||
## 组件标注
|
||||
|
||||
组件标注写在 `data.nodes[]`,并通过 `locator` 找到页面元素。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "order-table",
|
||||
"index": 1,
|
||||
"title": "订单表格",
|
||||
"pageId": "orders",
|
||||
"locator": {
|
||||
"selectors": ["[data-annotation-id=\"order-table\"]"],
|
||||
"fingerprint": "section|data-annotation-id=order-table",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "根据订单表格标注生成实现说明。",
|
||||
"annotationText": "",
|
||||
"hasMarkdown": true,
|
||||
"color": "#D97706",
|
||||
"images": [],
|
||||
"createdAt": 1779496800000,
|
||||
"updatedAt": 1779496800000
|
||||
}
|
||||
```
|
||||
|
||||
页面元素示例:
|
||||
|
||||
```tsx
|
||||
<section data-annotation-id="order-table">
|
||||
...
|
||||
</section>
|
||||
```
|
||||
|
||||
正文规则:
|
||||
|
||||
- `hasMarkdown: false`:显示 `annotationText` 和 `images`。
|
||||
- `hasMarkdown: true`:显示 `markdownMap[node.id]`,忽略 `annotationText` 和 `images`。
|
||||
- `pageId` 为空时是全局节点;字符串数组可以绑定多个页面或状态。
|
||||
|
||||
## 组件状态
|
||||
|
||||
组件状态写在标注节点的 `controls`。控件会出现在选中标注的运行时面板中,值会进入 proto dev state。
|
||||
|
||||
JSON 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "segmented",
|
||||
"attributeId": "order_state",
|
||||
"displayName": "订单状态",
|
||||
"info": "切换订单表格的展示状态。",
|
||||
"initialValue": "normal",
|
||||
"options": [
|
||||
{ "label": "普通", "value": "normal" },
|
||||
{ "label": "空数据", "value": "empty" },
|
||||
{ "label": "异常", "value": "error" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
页面读取示例:
|
||||
|
||||
```tsx
|
||||
type OrderState = 'normal' | 'empty' | 'error';
|
||||
|
||||
function normalizeOrderState(value: unknown): OrderState {
|
||||
return value === 'empty' || value === 'error' || value === 'normal'
|
||||
? value
|
||||
: 'normal';
|
||||
}
|
||||
|
||||
const protoState = useProtoDevState<{ order_state?: OrderState }>();
|
||||
const orderState = normalizeOrderState(protoState.order_state);
|
||||
```
|
||||
|
||||
可用控件类型:
|
||||
|
||||
- `input`
|
||||
- `inputNumber`
|
||||
- `select`
|
||||
- `segmented`
|
||||
- `switch`
|
||||
- `checkbox`
|
||||
- `slider`
|
||||
- `textarea`
|
||||
- `text`
|
||||
- `button`
|
||||
- `colorPicker`
|
||||
|
||||
JSON 数据源里只写可序列化字段。`button` 的函数型 `onClick` 不应写进 JSON。
|
||||
|
||||
## 验收清单
|
||||
|
||||
- `AnnotationViewer` 已挂载,并使用同一份 source document。
|
||||
- 每个可见节点都有稳定 selector,优先使用 `data-annotation-id`。
|
||||
- 多页面或多状态原型的 `currentPageId` 与当前页面/状态一致。
|
||||
- marker 可点击,选中后能看到短标注或 Markdown 正文。
|
||||
- directory 的 `link`、`markdown`、`route` 行为符合宿主回调和目标地址。
|
||||
- 颜色筛选展示当前页用到的所有 marker 颜色。
|
||||
- 组件状态控件变化后,页面通过 `useProtoDevState` 呈现对应状态。
|
||||
57
.claude/skills/prototype-comments/SKILL.md
Normal file
57
.claude/skills/prototype-comments/SKILL.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
name: prototype-comments
|
||||
description: 批注、微调、编辑原型时使用:读取本地原型批注并定位页面元素,修改文案、样式、布局或交互,完成后删除已处理批注任务。
|
||||
---
|
||||
|
||||
# 原型批注处理
|
||||
|
||||
当页面上存在原型批注,或 `/editor-todo` 要求处理当前项目的批注时使用本技能。
|
||||
|
||||
术语边界:
|
||||
|
||||
- 批注 / comment:Commentary 里的原型改稿意见,本技能只处理这类内容。
|
||||
- 标注 / annotation:AnnotationViewer 的原型说明层,例如 `annotation-source.json` 和 `@axhub/annotation`,不属于本技能处理范围。
|
||||
|
||||
## 默认读取顺序
|
||||
|
||||
1. 先定位目标原型目录:`src/prototypes/<prototype-id>/`。
|
||||
2. 读取本地文件:`src/prototypes/<prototype-id>/.spec/prototype-comments.json`。
|
||||
3. 若文件不存在,结合用户上下文和当前代码判断目标;不调用 CLI/API,也不依赖浏览器运行中的页面。
|
||||
|
||||
## 本地文件结构
|
||||
|
||||
批注记录固定在 `.spec/prototype-comments.json`,核心字段是 `comments/tasks/images`:
|
||||
|
||||
- `comments`:批注和修改记录,包含 locator、comment、marker,以及 text/style/tweak 的修改前后。
|
||||
- `tasks`:按 `elementKey` 保存待处理任务信息。
|
||||
- `images`:只记录 metadata 和 `images[].assetPath`。图片文件位于 `.spec/prototype-comment-assets/`。
|
||||
|
||||
读取图片时只使用本地 `images[].assetPath`,基于 `.spec/prototype-comment-assets/` 查找文件。不要把新的 base64 图片内容写回 JSON;需要新增图片素材时放入 assets 目录,并在 `images[].assetPath` 里引用。
|
||||
|
||||
## 处理流程
|
||||
|
||||
1. 读取 `.spec/prototype-comments.json`,按 `comments` 理解修改意图和定位信息。
|
||||
2. 如有批注图片,按 `images[].assetPath` 读取本地文件辅助理解。
|
||||
3. 修改 `src/prototypes/<prototype-id>/` 下的实现文件,保持改动范围聚焦。
|
||||
4. 完成一个批注任务后,只清理本地批注文档:删除对应批注记录和任务记录,不写任务进度字段。
|
||||
5. 按项目规则完成预览验证;无法验证时说明原因。
|
||||
|
||||
## 删除规则
|
||||
|
||||
- 用 `comments[].elementKey` 作为主键删除已完成批注。
|
||||
- 删除同 key 的 `tasks[elementKey]`。
|
||||
- 删除 `elementKey` 匹配且不再被其他剩余批注引用的 `images[]` 记录。
|
||||
- 对 `.spec/prototype-comment-assets/`,只删除与被移除 `images[].assetPath` 对应、且不再被 JSON 引用的文件。
|
||||
- 如果某条批注没有 `elementKey`,用 `locator`/`label` 辅助人工匹配;匹配不确定时保留,不误删。
|
||||
|
||||
清理规则示例:完成 `elementKey=hero` 后,移除 `comments` 中的 `hero` 批注、移除 `tasks.hero`、移除只属于 `hero` 的 `images` 记录及 `hero-only.png`;如果 `shared.png` 仍被其他剩余批注引用,则保留该图片记录和本地文件。
|
||||
|
||||
## 完成回复
|
||||
|
||||
面向用户用自然语言说明:
|
||||
|
||||
- 哪些批注对应的界面修改已完成。
|
||||
- 是否还有未处理或异常批注。
|
||||
- 做了哪些验证。
|
||||
|
||||
不要把回复写成 CLI 日志;技术细节只在确实影响用户理解时简短说明。
|
||||
4
.claude/skills/prototype-comments/agents/openai.yaml
Normal file
4
.claude/skills/prototype-comments/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "原型批注"
|
||||
short_description: "批注、微调或编辑原型时,定位元素并修改文案、样式、布局或交互"
|
||||
default_prompt: "使用 $prototype-comments 读取并处理当前原型批注。"
|
||||
96
.claude/skills/requirements-exploration/SKILL.md
Normal file
96
.claude/skills/requirements-exploration/SKILL.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: requirements-exploration
|
||||
description: Use only when the user explicitly asks to run demand exploration or requirements refinement, invokes $requirements-exploration, or asks to create/update confirmed requirement docs before prototype work. Do not trigger automatically for ordinary prototype generation, vague briefs, local edits, or bug fixes.
|
||||
---
|
||||
|
||||
# 需求探索
|
||||
|
||||
This is an explicit demand exploration workflow. Only enter it after the user clearly asks for demand exploration / requirements refinement, uses `$requirements-exploration`, or chooses this workflow from the product UI.
|
||||
|
||||
If the current request is an ordinary prototype generation or edit request, do not start this workflow just because the brief is incomplete. Ask at most the blocking questions needed to proceed, or state reasonable assumptions and implement.
|
||||
|
||||
Explore the plan until there is a shared understanding of the product goal, scope, users, scenarios, terms, constraints, and acceptance criteria. Walk down only the branches that materially affect scope, cost, or validation. For each question, provide your recommended answer.
|
||||
|
||||
Ask the questions one at a time, waiting for feedback on each question before continuing. Closely related 2-3 parameters can be grouped into one question.
|
||||
|
||||
If a question can be answered by exploring the project, explore the project instead.
|
||||
|
||||
## 项目感知
|
||||
|
||||
During project exploration, also look for existing documentation:
|
||||
|
||||
- `AGENTS.md`、`README.md`、`rules/`
|
||||
- `src/resources/`
|
||||
- `src/prototypes/<prototype-id>/.spec/`
|
||||
- `.axhub/make/project.json`
|
||||
|
||||
## 探索过程
|
||||
|
||||
### Challenge against existing language
|
||||
|
||||
When the user uses a term that conflicts with existing project language, call it out immediately. "Your docs define '发布' as X, but you seem to mean Y - which is it?"
|
||||
|
||||
### Sharpen fuzzy language
|
||||
|
||||
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying '项目' - do you mean the Make project, the prototype, or the business initiative?"
|
||||
|
||||
### Discuss concrete scenarios
|
||||
|
||||
When product relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
|
||||
|
||||
### Cross-reference with project
|
||||
|
||||
When the user states how something works, check whether the prototype, resources, specs, or metadata agree. If you find a contradiction, surface it.
|
||||
|
||||
### Recording cadence
|
||||
|
||||
Do not update files after every question.
|
||||
|
||||
Update the exploration snapshot only at these checkpoints:
|
||||
|
||||
- after every 10 answered questions;
|
||||
- when the user asks to pause, stop, summarize, or proceed to implementation;
|
||||
- when the exploration naturally ends;
|
||||
- when a major irreversible decision is confirmed and waiting would risk losing the decision.
|
||||
|
||||
Use Markdown. Keep it lean and decision-focused. Record only confirmed decisions, explicit user choices, unresolved open questions, and important assumptions.
|
||||
|
||||
### Long-session reminder
|
||||
|
||||
Keep a rough count of answered questions in this exploration session.
|
||||
|
||||
At every 50 answered questions, remind the user that the exploration has reached another 50-question checkpoint. Ask whether they want to continue exploring, pause and record the current snapshot, or enter wrap-up.
|
||||
|
||||
If the user wants to stop, switch to wrap-up mode:
|
||||
|
||||
- ask up to 5 final high-impact questions, prioritizing blockers and validation risks;
|
||||
- do not force all remaining branches to close;
|
||||
- record the confirmed decisions plus open questions;
|
||||
- summarize the recommended next implementation step.
|
||||
|
||||
If the user says to stop immediately, skip the final questions and record the current confirmed snapshot.
|
||||
|
||||
## 存储位置
|
||||
|
||||
All written exploration and requirements snapshot files must live under the target prototype's `.spec/` directory:
|
||||
|
||||
```text
|
||||
src/prototypes/<prototype-id>/.spec/YYYY-MM-DD-<topic>.md
|
||||
```
|
||||
|
||||
If no target prototype is identified, do not write a file yet. Ask the user to choose the prototype, or first create / identify the target prototype and then write into its `.spec/` directory.
|
||||
|
||||
Do not write confirmed exploration docs under root `docs/`, `src/resources/requirements/`, or `.axhub/make/`.
|
||||
Do not create `.axhub/make/exploration/`, `sessions/<session-id>.json`, or `index.json` for this workflow.
|
||||
|
||||
## 文档内容
|
||||
|
||||
Only record confirmed exploration decisions:
|
||||
|
||||
- resolved terms
|
||||
- scope and non-goals
|
||||
- concrete scenarios and edge cases
|
||||
- decisions and trade-offs
|
||||
- open questions
|
||||
|
||||
Do not treat the document as a scratch pad. Do not add implementation detail unless it affects the product decision.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "需求探索"
|
||||
short_description: "显式进入后,围绕原型和项目资料完善需求、术语、范围和决策"
|
||||
default_prompt: "使用 $requirements-exploration 对当前需求做探索和完善。"
|
||||
73
.claude/skills/screenshot-to-prototype/SKILL.md
Normal file
73
.claude/skills/screenshot-to-prototype/SKILL.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
name: screenshot-to-prototype
|
||||
description: Use only when 用户明确要求把本地截图、设计稿或高保真界面图还原成 Axhub Make client 可运行原型;或显式调用 $screenshot-to-prototype。仅提供图片作为素材、参考图、需求图或风格上下文时不要使用。
|
||||
---
|
||||
|
||||
# Screenshot To Prototype
|
||||
|
||||
用本地截图/设计稿还原 client 可运行原型:先提取必要素材,再写 React/CSS,最后做真实运行截图回归。正文保持中文、简洁。
|
||||
|
||||
## 退出规则
|
||||
|
||||
任一条件不满足就停止:
|
||||
|
||||
- 用户未提供源图。
|
||||
- 必须能获取源图的本地路径;如果源图没有本地路径,必须停止。
|
||||
- 图片生成能力可以来自 `ui-design-image`、系统 `imagegen`、ACP UI 图片 MCP、等价图片 MCP,或 Agent 图片配置。
|
||||
- 不能只因当前工具面板没有直接暴露图片生成工具就停止;停止前必须主动检查这些通道。
|
||||
- 确认所有图片生成通道都不可用或都不支持传入本地图片路径时,才停止。
|
||||
- 启动实现前必须确认存在视觉回归工具。
|
||||
- 视觉回归工具必须能获取产物真实运行截图;如果无法获取真实运行截图,必须停止。
|
||||
- 用户只是提供图片作为需求、内容、素材、风格上下文或普通参考图时,必须停止。
|
||||
- 普通建站、URL 克隆、主题提取、单纯图片生成不要使用本技能。
|
||||
|
||||
## 路径
|
||||
|
||||
所有路径都以 client 包根目录为基准,文档里不要写本机绝对路径、平台路径或外层仓库路径。
|
||||
|
||||
- 原型:`src/prototypes/<slug>/`
|
||||
- 素材:`src/prototypes/<slug>/assets/`
|
||||
- 素材清单:`src/prototypes/<slug>/assets/asset-manifest.json`
|
||||
- 临时文件:`.local/screenshot-to-prototype/<slug>/`
|
||||
|
||||
## 流程
|
||||
|
||||
1. 先应用退出规则,确认用户明确要求把截图/设计稿还原成可运行原型,并确认源图本地路径、图片生成通道、视觉回归工具。
|
||||
2. 若图片生成通道不明确,先按 `ui-design-image` 的工作流检查 ACP UI 图片 MCP、等价 MCP、Agent 图片配置和系统 `imagegen`,再决定是否停止。
|
||||
3. 所有素材提取、修复、高清化、设计分析都必须把用户本地图片路径作为参考图传入,不能只用文字描述生成素材。
|
||||
4. 让图片 AI 输出透明 PNG 素材矩阵;由图片 AI 判断具体提取对象,只说明筛选规则:保留可复用且 HTML/CSS 难快速稳定还原的视觉素材,包括背景图、背景纹理或复杂背景层;排除纯文本、简单布局容器、普通 CSS 形状和整页截图。
|
||||
5. 临时素材矩阵放 `.local/screenshot-to-prototype/<slug>/`,再切到 `src/prototypes/<slug>/assets/`:
|
||||
|
||||
```bash
|
||||
node .agents/skills/screenshot-to-prototype/scripts/slice-asset-sheet.mjs \
|
||||
--input .local/screenshot-to-prototype/<slug>/asset-sheet.png \
|
||||
--output-dir src/prototypes/<slug>/assets \
|
||||
--grid 4x3 \
|
||||
--names icon-search,logo-brand,avatar-user,banner-hero \
|
||||
--manifest src/prototypes/<slug>/assets/asset-manifest.json
|
||||
```
|
||||
|
||||
6. 审计素材:
|
||||
|
||||
```bash
|
||||
node .agents/skills/screenshot-to-prototype/scripts/audit-assets.mjs \
|
||||
--manifest src/prototypes/<slug>/assets/asset-manifest.json
|
||||
```
|
||||
|
||||
7. 对模糊、污染、不透明、尺寸不足或误切素材,允许用原始本地源图作为参考图单独生成或修复;必要时附带问题素材。
|
||||
8. 页面用真实文本、React 结构、Grid/Flex、CSS variables、稳定 `aspect-ratio` 和响应式约束还原;不要把整张截图当背景。
|
||||
9. 交互状态、颜色继承、hover/focus 或复用性强的图标,可参考切图后重绘为 SVG 或使用合适图标组件。
|
||||
10. 运行 `node scripts/check-app-ready.mjs /prototypes/<slug>`,再用视觉回归工具检查真实运行截图。
|
||||
11. 最终回复提供轻量偏差报告,不新建长文档:
|
||||
- 展示或链接原图与真实运行截图。
|
||||
- 按 P0-P3 列出偏差,重点写未还原到位的问题,不写泛泛总结。
|
||||
- P0:阻塞验收或页面不可用;P1:关键布局/比例/内容明显不符;P2:素材风格、间距、图标、阴影等显著偏差;P3:细节优化。
|
||||
- 明确等待用户反馈选择是否继续修,不擅自进入下一轮大改。
|
||||
|
||||
## 命名
|
||||
|
||||
素材名用 kebab-case:`icon-*`、`logo-*`、`avatar-*`、`image-*`、`banner-*`、`cover-*`、`background-*`、`decoration-*`、`border-*`。含义不清时用 `asset-01`。
|
||||
|
||||
## 提示词
|
||||
|
||||
写图片生成提示词时再读 `references/prompts.md`。
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Screenshot To Prototype"
|
||||
short_description: "将明确提供的本地截图或设计稿还原为 client 可运行原型,并做真实截图回归"
|
||||
default_prompt: "使用 $screenshot-to-prototype 将这张截图或设计稿还原为当前 client 的可运行原型。"
|
||||
39
.claude/skills/screenshot-to-prototype/references/prompts.md
Normal file
39
.claude/skills/screenshot-to-prototype/references/prompts.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Screenshot To Prototype 提示词
|
||||
|
||||
通用规则:始终附带用户本地源图路径作为参考图;不要只靠文字生成。若工具不支持本地图片路径,停止。
|
||||
|
||||
## 素材矩阵
|
||||
|
||||
```text
|
||||
请基于参考截图,生成一张透明背景 PNG 素材矩阵。
|
||||
|
||||
由你判断具体提取对象。只按这些筛选规则:保留可复用且 HTML/CSS 难快速稳定还原的视觉素材,包括背景图、背景纹理或复杂背景层;排除纯文本、简单布局容器、普通 CSS 形状、整页截图和编号标签。
|
||||
|
||||
素材按清晰网格排列,保留透明留白,保持原视觉风格、颜色、阴影、透明度和比例。
|
||||
```
|
||||
|
||||
## 单素材修复
|
||||
|
||||
```text
|
||||
请基于参考截图修复这个单独 UI 素材,输出干净透明 PNG。
|
||||
|
||||
保持原形状、颜色、阴影和比例;去除背景污染和边缘脏点;补足透明留白;不要添加标签、外框或新装饰。
|
||||
```
|
||||
|
||||
## Banner/封面高清化
|
||||
|
||||
```text
|
||||
请基于参考截图生成这个 banner/封面素材的高清版本。
|
||||
|
||||
保持原构图、主体、色彩、风格和比例;只提升清晰度,不改变设计意图;除非原素材自带文字,否则不要新增文字。
|
||||
```
|
||||
|
||||
## 设计分析
|
||||
|
||||
```text
|
||||
请分析参考截图,输出用于 React/Vite 原型还原的简洁说明。
|
||||
|
||||
包含:源图尺寸、主要布局区块、间距节奏、近似色值、字体估计、素材用途与位置、desktop/tablet/mobile 响应式策略、建议改为 SVG 的元素。
|
||||
|
||||
不要生成独立主题。
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { findAlphaBounds, readPng } from './png-utils.mjs';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const token = argv[index];
|
||||
if (!token.startsWith('--')) continue;
|
||||
const key = token.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith('--')) args[key] = true;
|
||||
else {
|
||||
args[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (!args.manifest) {
|
||||
console.error('Usage: node scripts/audit-assets.mjs --manifest src/prototypes/<slug>/assets/asset-manifest.json');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifestPath = path.resolve(String(args.manifest));
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const manifestDir = path.dirname(manifestPath);
|
||||
const results = [];
|
||||
|
||||
for (const asset of manifest.assets || []) {
|
||||
const file = String(asset.file || '');
|
||||
const assetPath = path.resolve(manifestDir, file);
|
||||
const issues = [];
|
||||
|
||||
if (!file || !fs.existsSync(assetPath)) {
|
||||
issues.push('missing-file');
|
||||
results.push({ id: asset.id || file, file, status: 'failed', issues });
|
||||
continue;
|
||||
}
|
||||
|
||||
const image = readPng(assetPath);
|
||||
const alphaBounds = findAlphaBounds(image);
|
||||
if (!image.hasAlphaChannel) issues.push('missing-alpha-channel');
|
||||
if (!alphaBounds) issues.push('empty-transparent-image');
|
||||
if (alphaBounds) {
|
||||
if (alphaBounds.x === 0 || alphaBounds.y === 0 || alphaBounds.x + alphaBounds.width === image.width || alphaBounds.y + alphaBounds.height === image.height) {
|
||||
issues.push('alpha-touches-edge');
|
||||
}
|
||||
const transparentCorners = [
|
||||
image.data[3],
|
||||
image.data[(image.width - 1) * 4 + 3],
|
||||
image.data[((image.height - 1) * image.width) * 4 + 3],
|
||||
image.data[((image.height * image.width) - 1) * 4 + 3],
|
||||
].filter((alpha) => alpha <= 8).length;
|
||||
if (transparentCorners < 3) issues.push('opaque-corners');
|
||||
}
|
||||
if (asset.width && Number(asset.width) !== image.width) issues.push('manifest-width-mismatch');
|
||||
if (asset.height && Number(asset.height) !== image.height) issues.push('manifest-height-mismatch');
|
||||
|
||||
results.push({
|
||||
id: asset.id || file,
|
||||
file,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
status: issues.length ? 'failed' : 'passed',
|
||||
issues,
|
||||
});
|
||||
}
|
||||
|
||||
const failed = results.filter((result) => result.status !== 'passed').length;
|
||||
const report = {
|
||||
status: failed ? 'failed' : 'passed',
|
||||
summary: {
|
||||
total: results.length,
|
||||
passed: results.length - failed,
|
||||
failed,
|
||||
},
|
||||
assets: results,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
if (failed) process.exitCode = 1;
|
||||
}
|
||||
|
||||
main();
|
||||
182
.claude/skills/screenshot-to-prototype/scripts/png-utils.mjs
Normal file
182
.claude/skills/screenshot-to-prototype/scripts/png-utils.mjs
Normal file
@@ -0,0 +1,182 @@
|
||||
import fs from 'node:fs';
|
||||
import zlib from 'node:zlib';
|
||||
|
||||
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||
|
||||
function crc32(buffer) {
|
||||
let crc = 0xffffffff;
|
||||
for (const byte of buffer) {
|
||||
crc ^= byte;
|
||||
for (let bit = 0; bit < 8; bit += 1) {
|
||||
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
||||
}
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function chunk(type, data) {
|
||||
const typeBuffer = Buffer.from(type, 'ascii');
|
||||
const length = Buffer.alloc(4);
|
||||
length.writeUInt32BE(data.length, 0);
|
||||
const checksum = Buffer.alloc(4);
|
||||
checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
|
||||
return Buffer.concat([length, typeBuffer, data, checksum]);
|
||||
}
|
||||
|
||||
function paeth(a, b, c) {
|
||||
const p = a + b - c;
|
||||
const pa = Math.abs(p - a);
|
||||
const pb = Math.abs(p - b);
|
||||
const pc = Math.abs(p - c);
|
||||
if (pa <= pb && pa <= pc) return a;
|
||||
if (pb <= pc) return b;
|
||||
return c;
|
||||
}
|
||||
|
||||
export function readPng(filePath) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
if (!buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
||||
throw new Error(`Unsupported PNG signature: ${filePath}`);
|
||||
}
|
||||
|
||||
let offset = 8;
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
let bitDepth = 0;
|
||||
let colorType = 0;
|
||||
const idatChunks = [];
|
||||
|
||||
while (offset < buffer.length) {
|
||||
const length = buffer.readUInt32BE(offset);
|
||||
const type = buffer.subarray(offset + 4, offset + 8).toString('ascii');
|
||||
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
||||
offset += 12 + length;
|
||||
|
||||
if (type === 'IHDR') {
|
||||
width = data.readUInt32BE(0);
|
||||
height = data.readUInt32BE(4);
|
||||
bitDepth = data[8];
|
||||
colorType = data[9];
|
||||
} else if (type === 'IDAT') {
|
||||
idatChunks.push(data);
|
||||
} else if (type === 'IEND') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bitDepth !== 8 || ![2, 6].includes(colorType)) {
|
||||
throw new Error(`Only 8-bit RGB/RGBA PNG files are supported: ${filePath}`);
|
||||
}
|
||||
|
||||
const channels = colorType === 6 ? 4 : 3;
|
||||
const bytesPerPixel = channels;
|
||||
const stride = width * channels;
|
||||
const inflated = zlib.inflateSync(Buffer.concat(idatChunks));
|
||||
const raw = Buffer.alloc(height * stride);
|
||||
let inputOffset = 0;
|
||||
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const filter = inflated[inputOffset];
|
||||
inputOffset += 1;
|
||||
const rowOffset = y * stride;
|
||||
const prevRowOffset = (y - 1) * stride;
|
||||
|
||||
for (let x = 0; x < stride; x += 1) {
|
||||
const value = inflated[inputOffset + x];
|
||||
const left = x >= bytesPerPixel ? raw[rowOffset + x - bytesPerPixel] : 0;
|
||||
const up = y > 0 ? raw[prevRowOffset + x] : 0;
|
||||
const upLeft = y > 0 && x >= bytesPerPixel ? raw[prevRowOffset + x - bytesPerPixel] : 0;
|
||||
|
||||
if (filter === 0) raw[rowOffset + x] = value;
|
||||
else if (filter === 1) raw[rowOffset + x] = (value + left) & 255;
|
||||
else if (filter === 2) raw[rowOffset + x] = (value + up) & 255;
|
||||
else if (filter === 3) raw[rowOffset + x] = (value + Math.floor((left + up) / 2)) & 255;
|
||||
else if (filter === 4) raw[rowOffset + x] = (value + paeth(left, up, upLeft)) & 255;
|
||||
else throw new Error(`Unsupported PNG filter ${filter}: ${filePath}`);
|
||||
}
|
||||
inputOffset += stride;
|
||||
}
|
||||
|
||||
const rgba = Buffer.alloc(width * height * 4);
|
||||
for (let index = 0; index < width * height; index += 1) {
|
||||
const sourceOffset = index * channels;
|
||||
const targetOffset = index * 4;
|
||||
rgba[targetOffset] = raw[sourceOffset];
|
||||
rgba[targetOffset + 1] = raw[sourceOffset + 1];
|
||||
rgba[targetOffset + 2] = raw[sourceOffset + 2];
|
||||
rgba[targetOffset + 3] = colorType === 6 ? raw[sourceOffset + 3] : 255;
|
||||
}
|
||||
|
||||
return { width, height, data: rgba, hasAlphaChannel: colorType === 6 };
|
||||
}
|
||||
|
||||
export function writePng(filePath, image) {
|
||||
const { width, height, data } = image;
|
||||
const header = Buffer.alloc(13);
|
||||
header.writeUInt32BE(width, 0);
|
||||
header.writeUInt32BE(height, 4);
|
||||
header[8] = 8;
|
||||
header[9] = 6;
|
||||
header[10] = 0;
|
||||
header[11] = 0;
|
||||
header[12] = 0;
|
||||
|
||||
const scanlines = Buffer.alloc(height * (1 + width * 4));
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const rowStart = y * (1 + width * 4);
|
||||
scanlines[rowStart] = 0;
|
||||
data.copy(scanlines, rowStart + 1, y * width * 4, (y + 1) * width * 4);
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, Buffer.concat([
|
||||
PNG_SIGNATURE,
|
||||
chunk('IHDR', header),
|
||||
chunk('IDAT', zlib.deflateSync(scanlines)),
|
||||
chunk('IEND', Buffer.alloc(0)),
|
||||
]));
|
||||
}
|
||||
|
||||
export function cropPng(image, bbox) {
|
||||
const width = Math.max(0, bbox.width);
|
||||
const height = Math.max(0, bbox.height);
|
||||
const data = Buffer.alloc(width * height * 4);
|
||||
for (let y = 0; y < height; y += 1) {
|
||||
const sourceStart = ((bbox.y + y) * image.width + bbox.x) * 4;
|
||||
const targetStart = y * width * 4;
|
||||
image.data.copy(data, targetStart, sourceStart, sourceStart + width * 4);
|
||||
}
|
||||
return { width, height, data, hasAlphaChannel: true };
|
||||
}
|
||||
|
||||
export function findAlphaBounds(image, bounds = { x: 0, y: 0, width: image.width, height: image.height }, alphaThreshold = 8) {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -1;
|
||||
let maxY = -1;
|
||||
const startX = Math.max(0, bounds.x);
|
||||
const startY = Math.max(0, bounds.y);
|
||||
const endX = Math.min(image.width, bounds.x + bounds.width);
|
||||
const endY = Math.min(image.height, bounds.y + bounds.height);
|
||||
|
||||
for (let y = startY; y < endY; y += 1) {
|
||||
for (let x = startX; x < endX; x += 1) {
|
||||
if (image.data[(y * image.width + x) * 4 + 3] > alphaThreshold) {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maxX < minX || maxY < minY) return null;
|
||||
return { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };
|
||||
}
|
||||
|
||||
export function expandBounds(bounds, padding, image) {
|
||||
const x = Math.max(0, bounds.x - padding);
|
||||
const y = Math.max(0, bounds.y - padding);
|
||||
const right = Math.min(image.width, bounds.x + bounds.width + padding);
|
||||
const bottom = Math.min(image.height, bounds.y + bounds.height + padding);
|
||||
return { x, y, width: right - x, height: bottom - y };
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { cropPng, expandBounds, findAlphaBounds, readPng, writePng } from './png-utils.mjs';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const token = argv[index];
|
||||
if (!token.startsWith('--')) continue;
|
||||
const key = token.slice(2);
|
||||
const next = argv[index + 1];
|
||||
if (!next || next.startsWith('--')) args[key] = true;
|
||||
else {
|
||||
args[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
'Usage:',
|
||||
' node scripts/slice-asset-sheet.mjs --input sheet.png --output-dir assets --grid 4x3 --names icon-a,banner-b --manifest assets/asset-manifest.json',
|
||||
'',
|
||||
'Options:',
|
||||
' --input Source transparent PNG sheet',
|
||||
' --output-dir Directory for extracted PNG assets',
|
||||
' --grid Grid size as COLSxROWS',
|
||||
' --names Optional comma-separated asset names',
|
||||
' --manifest Output manifest path',
|
||||
' --padding Transparent padding to keep around alpha bounds, default 1',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function toKebabName(input, fallback) {
|
||||
const normalized = String(input || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/gu, '-')
|
||||
.replace(/^-+|-+$/gu, '');
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function parseGrid(grid) {
|
||||
const match = String(grid || '').match(/^(\d+)x(\d+)$/iu);
|
||||
if (!match) throw new Error('--grid must use COLSxROWS, for example 4x3');
|
||||
const columns = Number(match[1]);
|
||||
const rows = Number(match[2]);
|
||||
if (!Number.isInteger(columns) || !Number.isInteger(rows) || columns < 1 || rows < 1) {
|
||||
throw new Error('--grid values must be positive integers');
|
||||
}
|
||||
return { columns, rows };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (!args.input || !args['output-dir'] || !args.grid) {
|
||||
console.error(usage());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const inputPath = path.resolve(String(args.input));
|
||||
const outputDir = path.resolve(String(args['output-dir']));
|
||||
const manifestPath = path.resolve(String(args.manifest || path.join(outputDir, 'asset-manifest.json')));
|
||||
const padding = Math.max(0, Number(args.padding ?? 1));
|
||||
const { columns, rows } = parseGrid(args.grid);
|
||||
const names = String(args.names || '').split(',').map((item) => item.trim()).filter(Boolean);
|
||||
const image = readPng(inputPath);
|
||||
const cellWidth = Math.floor(image.width / columns);
|
||||
const cellHeight = Math.floor(image.height / rows);
|
||||
|
||||
if (cellWidth < 1 || cellHeight < 1) {
|
||||
throw new Error('Grid creates empty cells; use fewer columns or rows');
|
||||
}
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const assets = [];
|
||||
let assetIndex = 0;
|
||||
|
||||
for (let row = 0; row < rows; row += 1) {
|
||||
for (let column = 0; column < columns; column += 1) {
|
||||
const cell = {
|
||||
x: column * cellWidth,
|
||||
y: row * cellHeight,
|
||||
width: column === columns - 1 ? image.width - column * cellWidth : cellWidth,
|
||||
height: row === rows - 1 ? image.height - row * cellHeight : cellHeight,
|
||||
};
|
||||
const alphaBounds = findAlphaBounds(image, cell);
|
||||
if (!alphaBounds) continue;
|
||||
|
||||
const paddedBounds = expandBounds(alphaBounds, padding, image);
|
||||
const id = toKebabName(names[assetIndex], `asset-${String(assetIndex + 1).padStart(2, '0')}`);
|
||||
const file = `${id}.png`;
|
||||
writePng(path.join(outputDir, file), cropPng(image, paddedBounds));
|
||||
|
||||
assets.push({
|
||||
id,
|
||||
file,
|
||||
width: paddedBounds.width,
|
||||
height: paddedBounds.height,
|
||||
sourceCell: { column, row },
|
||||
sourceBounds: paddedBounds,
|
||||
alphaBounds,
|
||||
});
|
||||
assetIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
source: path.relative(outputDir, inputPath) || path.basename(inputPath),
|
||||
grid: { columns, rows },
|
||||
assets,
|
||||
};
|
||||
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(JSON.stringify({ status: 'ok', manifest: manifestPath, assets: assets.length }, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
32
.claude/skills/ui-design-image/SKILL.md
Normal file
32
.claude/skills/ui-design-image/SKILL.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: ui-design-image
|
||||
description: Use when 为 Axhub Make client 项目生成 UI 设计图、高保真原型视觉、生产风格网站截图、整页界面稿、UI 素材、图标、占位图或参考位图;尤其是请求提到 Image Gen、AI 图片生成、设计图、UI assets 或 prototype visuals 时。
|
||||
---
|
||||
|
||||
# UI 设计图片
|
||||
|
||||
这是 Axhub Make client 对系统 `imagegen` 技能的轻量包装。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 如果可用,优先使用 ACP UI 图片 MCP:
|
||||
- 工具名称:`acp-ui-image-generation/generate_image`
|
||||
- 或使用当前环境中暴露的等价图片生成 MCP 工具。
|
||||
2. 仅当需要回退到系统 `imagegen` 技能或直接图片 API 时,才读取 Axhub Make 图片配置:
|
||||
- 优先读取 `<AXHUB_MAKE_HOME_DIR or user home>/.axhub/make/server.config.json`。
|
||||
- 再回退到项目内 `.axhub/make/axhub.config.json`。
|
||||
- 使用 `ai.imageGeneration.baseUrl`、`ai.imageGeneration.apiKey` 和 `ai.imageGeneration.model`。
|
||||
3. 如果 Make 配置缺失或不完整,再读取本地 Codex 配置/认证路径:
|
||||
- 始终检查 `CODEX_HOME`,然后检查用户 home 下的 `.codex`。
|
||||
- Windows 还要检查 AppData/ProgramData 下的 Codex 配置目录。
|
||||
- macOS/Linux 还要检查 XDG Codex 配置目录。
|
||||
- 从 `config.toml` 读取 provider `base_url`;从 `auth.json` 读取 API key。
|
||||
4. 将所有非空值作为 Image Gen provider settings(`baseUrl`、`apiKey`、`model`)传入,然后继续遵循系统 `imagegen` 技能。
|
||||
5. 如果当前 MCP、工具或 API 不支持单次生成多张图片,而用户需要多张图片,应发起多次生成请求,不要把需求降级成只生成一张。
|
||||
6. 生成派生产物时(例如基于现有图片/原型做变体、扩图、局部重绘、风格迁移、素材补图或素材拆分),必须把原图或相关原型截图作为参考图传给图片生成工具;传参使用本地文件路径,不要只在提示词里文字描述,也不要传远程 URL。如果当前只有页面或预览链接,先导出真实运行截图到本地,再把该本地路径传入。
|
||||
|
||||
如果没有项目配置或本地配置,则回退到系统 `imagegen` 的默认行为。
|
||||
|
||||
提示词应聚焦 UI 设计用途:目标画面、输出角色、尺寸/比例、视觉风格、精确文案、透明背景需求,以及输出保存位置。
|
||||
|
||||
写给第三方图片生成工具的提示词,应按真实产品或正式界面来描述,不要传递内部 `prototype` 概念。只有用户明确要求低保真、线框图、占位图或草稿时,才使用 `wireframe`、`placeholder`、`draft` 等词。
|
||||
4
.claude/skills/ui-design-image/agents/openai.yaml
Normal file
4
.claude/skills/ui-design-image/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "UI 设计图片"
|
||||
short_description: "生成整页设计图、高保真原型视觉、UI 素材、图标、占位图或视觉参考图时,优先使用图片 MCP 与 Make 图片配置"
|
||||
default_prompt: "使用 $ui-design-image 为当前项目生成 UI 设计图片或素材。"
|
||||
81
.claude/skills/write-prd/SKILL.md
Normal file
81
.claude/skills/write-prd/SKILL.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: write-prd
|
||||
description: Use when the user explicitly asks to write, draft, create, update, or synthesize a PRD for an Axhub Make client project, especially when the PRD may aggregate multiple prototypes, resources, canvas notes, or existing product context.
|
||||
---
|
||||
|
||||
# Write PRD
|
||||
|
||||
把当前对话、项目资源、原型和画布上下文整理成简洁 PRD。不要进行长轮需求访谈;如果缺少会影响范围或验收的关键决策,最多问一个聚焦问题,或写明合理假设。
|
||||
|
||||
## 上下文读取
|
||||
|
||||
优先看需求资料,不默认把工作流文档当成需求来源:
|
||||
|
||||
1. 用户当前说明、附件、截图,以及用户提供的模板。
|
||||
2. `src/resources/` 中已有产品资料、PRD、模板、素材和长期文档。
|
||||
3. 相关 `src/prototypes/<prototype-id>/.spec/` 文档。
|
||||
4. 相关原型页面、`annotation-source.json`、批注、状态定义和可见文案。
|
||||
5. 相关 `src/prototypes/<prototype-id>/canvas.excalidraw` 和 `canvas-assets/`,用于识别跨原型关系、流程草图和补充说明。
|
||||
|
||||
## 模板优先级
|
||||
|
||||
- 用户提供的模板优先,按其章节、字段和表达风格写。
|
||||
- 如果 `src/resources/` 里已有 PRD 或项目模板,沿用其结构。
|
||||
- 如果没有模板,使用下面的默认结构。
|
||||
- PRD 只写产品决策、用户体验、范围、规则和验收。不要堆易过期的文件路径、代码片段或实现清单;如果某个原型片段能比文字更准确地表达状态机、数据结构或流程决策,只摘取最小必要片段并说明来自原型。
|
||||
|
||||
## 默认结构
|
||||
|
||||
```markdown
|
||||
# <功能或产品名> PRD
|
||||
|
||||
## 背景与问题
|
||||
为什么要做,当前问题是什么,依据来自哪些上下文。
|
||||
|
||||
## 目标
|
||||
本 PRD 要达成的产品结果。
|
||||
|
||||
## 用户与场景
|
||||
谁会使用,在什么情况下使用,要支持哪些核心场景。
|
||||
|
||||
## 范围
|
||||
本次包含的能力、页面、流程或内容模块。
|
||||
|
||||
## 用户故事
|
||||
用编号列表描述:作为 <角色>,我希望 <能力>,从而 <价值>。
|
||||
|
||||
## 体验与内容要求
|
||||
页面、原型、标注、画布、内容、状态和交互层面的用户可见要求。
|
||||
|
||||
## 功能要求
|
||||
行为、数据、权限、集成、边界条件和异常状态。
|
||||
|
||||
## 验收标准
|
||||
产品、设计和实现评审时可以观察验证的检查项。
|
||||
|
||||
## 不在范围
|
||||
明确不做或延后的内容。
|
||||
|
||||
## 开放问题
|
||||
只保留会影响范围、验收或交付的问题。
|
||||
```
|
||||
|
||||
## 存储位置
|
||||
|
||||
PRD 默认写入 `src/resources/`,因为它可能聚合多个原型,而不只服务单个原型。使用清晰的 Markdown 文件名,例如:
|
||||
|
||||
```text
|
||||
src/resources/<topic>-prd.md
|
||||
src/resources/prd/<topic>.md
|
||||
```
|
||||
|
||||
只有用户明确要求 PRD 绑定单个原型、且不需要作为项目级资源沉淀时,才写入原型 `.spec/` 目录。
|
||||
|
||||
## 完成输出
|
||||
|
||||
完成后说明:
|
||||
|
||||
- PRD 路径。
|
||||
- 使用了哪些主要来源,包括资源、原型和画布文件。
|
||||
- 使用了用户模板、项目模板,还是默认结构。
|
||||
- 仍然存在的开放问题或关键假设。
|
||||
4
.claude/skills/write-prd/agents/openai.yaml
Normal file
4
.claude/skills/write-prd/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "写 PRD"
|
||||
short_description: "把对话、资源、原型和画布上下文整理成项目级 PRD,默认落入 src/resources"
|
||||
default_prompt: "使用 $write-prd 基于当前上下文写一份 PRD;如果已有模板,请优先按模板整理。"
|
||||
Reference in New Issue
Block a user