Sync OneOS workspace with new prototypes, annotations, and Gitea remote fix.
Add vehicle-h2-fee-ledger, customer-management, lease and self-operated ledgers, annotation sources, agent skills, and vite annotation runtime support. Update vehicle management, contract templates, and lease contract flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
65
.claude/skills/axhub-annotation-standalone/SKILL.md
Normal file
65
.claude/skills/axhub-annotation-standalone/SKILL.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: axhub-annotation-standalone
|
||||
description: Use when adding @axhub/annotation to standalone React apps, plain HTML pages, Vite prototypes, or other web hosts that need annotation markers, directories, Markdown notes, or state controls.
|
||||
---
|
||||
|
||||
# Axhub Annotation Standalone
|
||||
|
||||
在独立 Web 项目中使用 `@axhub/annotation` 时,用这个技能。它只说明运行时接入:页面已有标注数据,只需要展示 marker、标注面板、目录和状态控件。
|
||||
|
||||
## 参考案例
|
||||
|
||||
| 宿主 | 使用方式 | 文件 |
|
||||
| --- | --- | --- |
|
||||
| React | `AnnotationViewer` 组件 | `references/react-example.tsx` |
|
||||
| 普通 HTML / DOM | `createAnnotationViewer` | `references/html-example.html` + `references/html-example.ts` |
|
||||
| 数据源 | `AnnotationSourceDocument` JSON | `references/annotation-source.json` |
|
||||
|
||||
## 接入前提
|
||||
|
||||
- 安装 `@axhub/annotation`,并确保项目里有 React 18 / ReactDOM 18。
|
||||
- 使用能导入 ESM/TS/JSON 的构建工具,例如 Vite。
|
||||
- 标注数据使用一份 `AnnotationSourceDocument`,静态 import 或由宿主 loader 返回。
|
||||
- 被标注元素优先加稳定属性,例如 `data-annotation-id`。
|
||||
|
||||
## React 接入
|
||||
|
||||
- 挂载 `AnnotationViewer`。
|
||||
- 多页面时传 `options.currentPageId`。
|
||||
- 目录 `route` 在 `options.onDirectoryRoute` 中交给宿主切页。
|
||||
- 状态标注用 `useProtoDevState()` 读取 `controls` 值。
|
||||
- 发布为带源码 HTML 后,发布产物会注入 `sourceReference`,指向 `source/manifest.json`;宿主接入代码不需要生成它。
|
||||
|
||||
## 普通 HTML 接入
|
||||
|
||||
- 用 `createAnnotationViewer()` 创建运行时。
|
||||
- 用 `getCurrentPageId` 返回当前页面。
|
||||
- 切页后调用 `viewer.refresh()`。
|
||||
- 状态控件可订阅 `window.__AXHUB_PROTO_DEV__`,从 `getState()` 读值并更新 DOM。
|
||||
|
||||
## 数据要点
|
||||
|
||||
- `directory.nodes` 放 `folder` / `route` / `markdown` / `link`,不需要 `locator`。
|
||||
- `type: "markdown"` 目录文档可以直接写 `markdown` 正文。
|
||||
- 需要把目录文档拆成 `.md` 文件时,可以写 `markdownPath`,例如 `docs/prd-03-status.md`;这是构建侧约定,运行时仍读取内联后的 `markdown`。
|
||||
- `markdownPath` 只用于目录文档,不用于 marker 标注节点。
|
||||
- `data.nodes[]` 放页面 marker,必须有能在宿主页面解析到的 `locator`。
|
||||
- marker 只属于某些页面或状态时,写 `pageId`。
|
||||
- 长正文用 `hasMarkdown: true` + `markdownMap[node.id]`。
|
||||
- 状态标注写节点 `controls`;JSON 里只放可序列化字段。
|
||||
- `sourceReference` 不放在 JSON 数据源里,只描述发布包中的源码清单位置,不内联源码文件。
|
||||
|
||||
## 验收
|
||||
|
||||
1. 启动宿主预览。
|
||||
2. 确认目标元素上出现 marker。
|
||||
3. 点击 marker,能看到短标注或 Markdown 正文。
|
||||
4. 打开目录,验证 `route`、`markdown`、`link`。
|
||||
5. 修改状态控件,确认 React 状态或普通 DOM 同步变化。
|
||||
6. 检查控制台是否有 import、peer dependency 或 locator 错误。
|
||||
|
||||
## 常见错误
|
||||
|
||||
- 不要把函数写进 JSON controls。
|
||||
- 不要依赖脆弱的生成 CSS 选择器;能加 `data-annotation-id` 就加。
|
||||
- 不要期待 `route` 自动跳转;宿主必须在 `onDirectoryRoute` 里处理。
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"documentVersion": 1,
|
||||
"format": "axhub-annotation-source",
|
||||
"data": {
|
||||
"version": 2,
|
||||
"prototypeName": "standalone-annotation-demo",
|
||||
"pageId": "overview",
|
||||
"updatedAt": 1779667200000,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "overview-hero",
|
||||
"index": 1,
|
||||
"title": "运行时总览",
|
||||
"pageId": "overview",
|
||||
"locator": {
|
||||
"selectors": ["[data-annotation-id=\"overview-hero\"]"],
|
||||
"fingerprint": "section|overview-hero",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "说明独立页面如何接入标注运行时。",
|
||||
"annotationText": "",
|
||||
"hasMarkdown": true,
|
||||
"color": "#D97706",
|
||||
"images": [],
|
||||
"createdAt": 1779667200000,
|
||||
"updatedAt": 1779667200000
|
||||
},
|
||||
{
|
||||
"id": "state-card",
|
||||
"index": 2,
|
||||
"title": "结果状态",
|
||||
"pageId": "states",
|
||||
"locator": {
|
||||
"selectors": ["[data-annotation-id=\"state-card\"]"],
|
||||
"fingerprint": "article|state-card",
|
||||
"path": []
|
||||
},
|
||||
"aiPrompt": "演示标注 controls 如何驱动页面状态。",
|
||||
"annotationText": "在标注面板里切换结果状态,页面应该同步展示成功或失败。",
|
||||
"hasMarkdown": false,
|
||||
"color": "#059669",
|
||||
"images": [],
|
||||
"controls": [
|
||||
{
|
||||
"type": "segmented",
|
||||
"attributeId": "result_state",
|
||||
"displayName": "结果状态",
|
||||
"initialValue": "success",
|
||||
"options": [
|
||||
{ "label": "成功", "value": "success" },
|
||||
{ "label": "失败", "value": "failure" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"createdAt": 1779667200000,
|
||||
"updatedAt": 1779667200000
|
||||
}
|
||||
]
|
||||
},
|
||||
"markdownMap": {
|
||||
"overview-hero": "# 独立接入说明\n\n`@axhub/annotation` 只负责运行时展示。宿主页面负责提供数据源、稳定选择器和目录 route 行为。"
|
||||
},
|
||||
"assetMap": {},
|
||||
"directory": {
|
||||
"nodes": [
|
||||
{
|
||||
"type": "folder",
|
||||
"id": "demo-root",
|
||||
"title": "示例目录",
|
||||
"defaultExpanded": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "route",
|
||||
"id": "route-overview",
|
||||
"title": "运行时总览",
|
||||
"route": "overview"
|
||||
},
|
||||
{
|
||||
"type": "route",
|
||||
"id": "route-states",
|
||||
"title": "状态标注",
|
||||
"route": "states"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"id": "doc-usage",
|
||||
"title": "接入说明",
|
||||
"markdown": "# 接入说明\n\nReact 使用 `AnnotationViewer`;普通 HTML 使用 `createAnnotationViewer`。"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"id": "docs-link",
|
||||
"title": "包文档",
|
||||
"href": "https://www.npmjs.com/package/@axhub/annotation",
|
||||
"target": "blank"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Axhub Annotation HTML Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<button type="button" data-route="overview">运行时总览</button>
|
||||
<button type="button" data-route="states">状态标注</button>
|
||||
</nav>
|
||||
|
||||
<section data-page="overview">
|
||||
<section data-annotation-id="overview-hero">
|
||||
<h1>@axhub/annotation</h1>
|
||||
<p>这是一个普通 HTML 宿主接入示例。</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section data-page="states" hidden>
|
||||
<article data-annotation-id="state-card">
|
||||
<strong data-result-label>成功</strong>
|
||||
<h2 data-result-title>发布完成</h2>
|
||||
<p>在标注面板里切换结果状态,页面会同步变化。</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<script type="module" src="./html-example.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
createAnnotationViewer,
|
||||
type AnnotationDirectoryRouteNode,
|
||||
type AnnotationSourceDocument,
|
||||
type ProtoDevState,
|
||||
} from '@axhub/annotation';
|
||||
import annotationSource from './annotation-source.json';
|
||||
|
||||
type PageId = 'overview' | 'states';
|
||||
|
||||
let currentPageId: PageId = 'overview';
|
||||
|
||||
function normalizePageId(value: unknown): PageId {
|
||||
return value === 'states' ? 'states' : 'overview';
|
||||
}
|
||||
|
||||
function renderPage(pageId: PageId): void {
|
||||
currentPageId = pageId;
|
||||
document.querySelectorAll<HTMLElement>('[data-page]').forEach((page) => {
|
||||
page.hidden = page.dataset.page !== pageId;
|
||||
});
|
||||
}
|
||||
|
||||
function renderState(state: ProtoDevState): void {
|
||||
const isFailure = state.result_state === 'failure';
|
||||
const label = document.querySelector('[data-result-label]');
|
||||
const title = document.querySelector('[data-result-title]');
|
||||
|
||||
if (label) label.textContent = isFailure ? '失败' : '成功';
|
||||
if (title) title.textContent = isFailure ? '发布失败' : '发布完成';
|
||||
}
|
||||
|
||||
const viewer = createAnnotationViewer({
|
||||
source: annotationSource as AnnotationSourceDocument,
|
||||
options: {
|
||||
getCurrentPageId: () => currentPageId,
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
onDirectoryRoute: (node: AnnotationDirectoryRouteNode) => {
|
||||
renderPage(normalizePageId(node.route));
|
||||
viewer.refresh();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-route]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
renderPage(normalizePageId(button.dataset.route));
|
||||
viewer.refresh();
|
||||
});
|
||||
});
|
||||
|
||||
void viewer.start().then(() => {
|
||||
const attach = () => {
|
||||
const protoDev = window.__AXHUB_PROTO_DEV__;
|
||||
if (!protoDev) {
|
||||
window.setTimeout(attach, 80);
|
||||
return;
|
||||
}
|
||||
|
||||
renderState(protoDev.getState());
|
||||
protoDev.subscribe(() => renderState(protoDev.getState()));
|
||||
};
|
||||
|
||||
attach();
|
||||
});
|
||||
|
||||
renderPage(currentPageId);
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AnnotationViewer,
|
||||
useProtoDevState,
|
||||
type AnnotationDirectoryRouteNode,
|
||||
type AnnotationSourceDocument,
|
||||
type AnnotationViewerOptions,
|
||||
} from '@axhub/annotation';
|
||||
import annotationSource from './annotation-source.json';
|
||||
|
||||
type PageId = 'overview' | 'states';
|
||||
type ResultState = 'success' | 'failure';
|
||||
|
||||
function normalizePageId(value: unknown): PageId {
|
||||
return value === 'states' ? 'states' : 'overview';
|
||||
}
|
||||
|
||||
function normalizeResultState(value: unknown): ResultState {
|
||||
return value === 'failure' ? 'failure' : 'success';
|
||||
}
|
||||
|
||||
function StateCard() {
|
||||
const protoState = useProtoDevState<{ result_state?: ResultState }>();
|
||||
const resultState = normalizeResultState(protoState.result_state);
|
||||
const isSuccess = resultState === 'success';
|
||||
|
||||
return (
|
||||
<article data-annotation-id="state-card">
|
||||
<strong>{isSuccess ? '成功' : '失败'}</strong>
|
||||
<h2>{isSuccess ? '发布完成' : '发布失败'}</h2>
|
||||
<p>{isSuccess ? '可以继续评审标注内容。' : '需要展示失败原因和重试入口。'}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnnotationStandaloneReactExample() {
|
||||
const [pageId, setPageId] = React.useState<PageId>('overview');
|
||||
|
||||
const options = React.useMemo<AnnotationViewerOptions>(() => ({
|
||||
currentPageId: pageId,
|
||||
showToolbar: true,
|
||||
showThemeToggle: true,
|
||||
showColorFilter: true,
|
||||
onDirectoryRoute: (node: AnnotationDirectoryRouteNode) => {
|
||||
setPageId(normalizePageId(node.route));
|
||||
},
|
||||
}), [pageId]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<nav>
|
||||
<button type="button" onClick={() => setPageId('overview')}>运行时总览</button>
|
||||
<button type="button" onClick={() => setPageId('states')}>状态标注</button>
|
||||
</nav>
|
||||
|
||||
{pageId === 'overview' ? (
|
||||
<section data-annotation-id="overview-hero">
|
||||
<h1>@axhub/annotation</h1>
|
||||
<p>这是一个脱离平台的 React 接入示例。</p>
|
||||
</section>
|
||||
) : (
|
||||
<StateCard />
|
||||
)}
|
||||
|
||||
<AnnotationViewer
|
||||
source={annotationSource as AnnotationSourceDocument}
|
||||
options={options}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,54 @@
|
||||
---
|
||||
name: canvas-workspace
|
||||
description: 当任务涉及 Axhub 画布、Excalidraw 文件、画布节点、批注、截图、画布图片、原型/文档/主题嵌入节点或 AI 生成节点时使用。
|
||||
description: 仅当任务明确涉及 Axhub 画布、原型草稿、Excalidraw 画布文件、画布节点/批注/截图/图片,或需要把文档、原型页面、图片、流程图等产物落到画布上时使用。
|
||||
---
|
||||
|
||||
# Canvas Workspace — 画布工作区
|
||||
|
||||
当任务涉及 Axhub 画布时使用本技能。每个原型拥有自己的 Excalidraw 画布文件:
|
||||
仅当任务明确涉及 Axhub 画布、原型草稿,或需要把产物落到画布/Excalidraw 上时使用本技能。每个原型拥有自己的 Excalidraw 画布文件:
|
||||
|
||||
```text
|
||||
src/prototypes/<prototype-name>/canvas.excalidraw
|
||||
src/prototypes/<prototype-name>/canvas-assets/
|
||||
```
|
||||
|
||||
本技能用于按 Axhub Make 约定读取和写入画布,重点关注 `customData`、嵌入资源节点、批注、图片文件和 AI 生成节点。
|
||||
本技能按四类产物分流:文档、原型页面、图片、流程图。先判断产物类型;产物类型不清时先问一个问题。如果用户已在画布/草稿中工作,不再询问放在哪里,默认更新当前 `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. CLI 用于获取当前浏览器会话信息或截图。
|
||||
4. 不使用 `axhub-make canvas` CLI;画布内容读取和修改仍以 `.excalidraw` 文件为准。
|
||||
|
||||
## 参考文档
|
||||
## 参考文档分流
|
||||
|
||||
- 文件路径、读写规则、CLI 命令和关系检查:`references/canvas-read-write.md`
|
||||
- Axhub 专属节点和 `customData` 字段:`references/axhub-nodes.md`
|
||||
- Excalidraw 图形结构与布局基础:`references/excalidraw-basics.md`
|
||||
- JSON 元素结构模板:`references/element-templates.md`
|
||||
- 读写画布文件本身仍不清楚时,才读 `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 节点结构更新画布;只有类型或载体重叠不确定时才询问用户。
|
||||
|
||||
## 默认规则
|
||||
|
||||
- 优先直接编辑 `.excalidraw` JSON。
|
||||
- 优先使用可用的 `axhub-canvas` MCP 工具更新当前画布;离线或 MCP 不可用时直接编辑 `.excalidraw` JSON。
|
||||
- 元素 `id` 必须唯一,并尽量沿用现有文件的 ID 风格。
|
||||
- 修改元素时同步更新 `version`、`versionNonce` 和 `updated`。
|
||||
- 结构性改动后检查绑定、容器、分组和 Frame 引用。
|
||||
- 除非用户需求要求修改,否则保留已有 Axhub `customData`。
|
||||
- 创建或替换 prototype 预览节点时,画布上的节点尺寸与网页内部视口要分开处理:节点可以用较小可视尺寸避免占满画布,但网页仍按真实浏览器尺寸设计,通过 `customData.embedContentScale` 缩放显示。
|
||||
|
||||
## 回复要求
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
interface:
|
||||
display_name: "画布工作区"
|
||||
short_description: "在 Axhub 画布上绘图、构思方案、整理节点,读取批注、截图或图片"
|
||||
default_prompt: "使用 $canvas-workspace 读取或整理 Axhub 画布内容。"
|
||||
short_description: "把文档、原型页面、图片、流程图等产物创建或更新到 Axhub 画布"
|
||||
default_prompt: "使用 $canvas-workspace 处理这个画布需求,先判断产物类型再更新当前画布。"
|
||||
|
||||
@@ -9,7 +9,7 @@ Axhub 画布节点本质上是标准 Excalidraw 元素,Axhub 扩展信息存
|
||||
| `customData.title` | 面向用户的节点标题 |
|
||||
| `customData.previewUrl` | 预览模式中渲染的 URL |
|
||||
| `customData.openUrl` | 节点操作中打开的 URL |
|
||||
| `customData.previewKind` | 渲染类型,例如 `web`、`doc`、`image`、`none`、`ai-image-generator`、`prototype-generator` |
|
||||
| `customData.previewKind` | 渲染类型,例如 `web`、`doc`、`image`、`none` |
|
||||
| `customData.resourceType` | 资源类型:`prototype`、`doc` 或 `theme` |
|
||||
| `customData.resourceId` | 项目 metadata 中的资源 id 或名称 |
|
||||
| `customData.embedViewMode` | `link` 表示紧凑链接卡片,`preview` 表示渲染嵌入预览 |
|
||||
@@ -44,19 +44,10 @@ Axhub 画布节点本质上是标准 Excalidraw 元素,Axhub 扩展信息存
|
||||
}
|
||||
```
|
||||
|
||||
由 AI 原型生成能力产出的原型节点还可能包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"generatedBy": "axhub-prototype-generator",
|
||||
"sourceTaskId": "<task-id>",
|
||||
"prompt": "<prompt>"
|
||||
}
|
||||
```
|
||||
|
||||
### 文档节点
|
||||
|
||||
通过 `customData.type: "axhub-doc"` 或 `customData.resourceType: "doc"` 识别。
|
||||
当画布任务需要生成文档、说明、PRD、清单、列表、报告或其他文本内容时,优先把正文写成 `src/resources/` 下的 Markdown,再用文档节点引用该资源;画布只放摘要或入口。
|
||||
|
||||
常见字段:
|
||||
|
||||
@@ -82,65 +73,31 @@ Axhub 画布节点本质上是标准 Excalidraw 元素,Axhub 扩展信息存
|
||||
|
||||
主题节点与原型/文档节点使用相同的 `embeddable` 结构,`resourceType` 为 `theme`,`previewKind` 通常为 `web` 或 `none`。
|
||||
|
||||
## AI 生成节点
|
||||
## Drawio 节点
|
||||
|
||||
AI 生成节点是图片元素。占位图或生成图片数据保存在 `files[fileId]`。
|
||||
Drawio 节点是图片元素。`files[fileId].dataURL` 保存带 Drawio XML 的 SVG 预览,`customData.type` 固定为 `axhub-drawio`。
|
||||
|
||||
### AI 图片生成节点
|
||||
只有用户明确要求 Draw.io、`.drawio`、diagrams.net、可编辑 Draw.io 资产,或 `canvas-workspace` 已选择 Drawio 节点时,才在当前原型的 `canvas.excalidraw` 中创建或更新这种节点。
|
||||
识别 Drawio 节点以 `customData.type: "axhub-drawio"` 为准;`previewKind` 只是预览展示元信息。
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "image",
|
||||
"fileId": "axhub-ai-image-placeholder-v2",
|
||||
"fileId": "drawio-file-<id>",
|
||||
"customData": {
|
||||
"type": "axhub-ai-image-generator",
|
||||
"title": "AI 生成图片",
|
||||
"previewKind": "ai-image-generator"
|
||||
"type": "axhub-drawio",
|
||||
"title": "Drawio 图表",
|
||||
"previewKind": "drawio"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AI 图片结果节点
|
||||
创建或更新 Drawio 节点时:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "image",
|
||||
"fileId": "<image-id>",
|
||||
"customData": {
|
||||
"type": "axhub-ai-image",
|
||||
"generatedBy": "axhub-ai-image",
|
||||
"sourceTaskId": "<task-id>",
|
||||
"prompt": "<prompt>",
|
||||
"previewKind": "image"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
多张生成图片可能共享同一个 `groupIds` 值。
|
||||
|
||||
### AI 原型生成节点
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "image",
|
||||
"fileId": "axhub-prototype-generator-placeholder-v1",
|
||||
"customData": {
|
||||
"type": "axhub-prototype-generator",
|
||||
"title": "AI 生成原型",
|
||||
"previewKind": "prototype-generator"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
生成完成后,占位节点会被替换为原型嵌入节点,并带有 `generatedBy: "axhub-prototype-generator"`。
|
||||
|
||||
AI 生成原型替换节点的推荐尺寸:
|
||||
|
||||
- 不要把网页内部布局做小;页面代码仍按正常浏览器视口设计。
|
||||
- `previewUrl`、`openUrl`、`link` 使用客户端原型运行时地址,例如 `/prototypes/<prototypeId>` 或带 hash/page 的同源 runtime URL;不要使用 Make 管理端首页 deep link,例如 `/?p=...` 或 `/?resourceType=prototype...`。
|
||||
- 为了避免画布被完整桌面尺寸占满,推荐生成节点可视尺寸为 `720 x 450`。
|
||||
- 同时设置 `customData.embedSizePreset: "desktop"`、`customData.embedContentScale: 0.5`、`customData.storedPreviewSize: { "width": 720, "height": 450 }`。这样画布显示为 720x450,iframe 与截图按 1440x900 视口渲染。
|
||||
- 新生成的 prototype embeddable 可设置 `customData.captureScreenshotOnMount: true`,让宿主首次渲染后自动捕获预览截图。截图成功后宿主会清除此字段并写入 `screenshotUrl`,不要手写 `screenshotUrl`。
|
||||
- 推荐持久化资源文件后缀为 `.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 文本框。
|
||||
|
||||
## 图片文件
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# 画布读写能力参考
|
||||
|
||||
面向使用 Skill 的 Agent:优先读写本地 `.excalidraw` 文件。用户指定画布名或画布链接时,直接定位本地画布文件;CLI 用于获取当前浏览器会话信息或截图。
|
||||
面向使用 Skill 的 Agent:优先读写本地 `.excalidraw` 文件。用户指定画布名或画布链接时,直接定位本地画布文件;不要使用 `axhub-make canvas` CLI。
|
||||
|
||||
## 快速判断
|
||||
|
||||
| 目标 | 优先方式 |
|
||||
| 目标 | 做法 |
|
||||
|------|----------|
|
||||
| 读取画布元素、批注、节点信息 | 直接读 `.excalidraw` |
|
||||
| 修改画布内容 | 直接改 `.excalidraw` |
|
||||
| 从用户给的画布链接定位元素 | 从链接提取画布名和元素 ID,再读文件 |
|
||||
| 获取当前浏览器里画布的截图 | `axhub-make canvas screenshot` |
|
||||
| 查看当前浏览器连接了哪些画布 | `axhub-make canvas info` |
|
||||
| 获取画布截图 | 优先使用已有 `canvas-assets` 截图;需要当前浏览器画布时用全局截图 API |
|
||||
|
||||
## 文件位置
|
||||
|
||||
@@ -50,31 +49,53 @@ src/prototypes/<prototype-name>/canvas-assets/embed-<elementId>.png
|
||||
| 原型节点 | `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"` |
|
||||
| AI 图片生成节点 | `type == "image"` 且 `customData.type == "axhub-ai-image-generator"` |
|
||||
| AI 图片结果节点 | `type == "image"` 且 `customData.type == "axhub-ai-image"` |
|
||||
| AI 原型生成节点 | `type == "image"` 且 `customData.type == "axhub-prototype-generator"` |
|
||||
| Drawio 节点 | `type == "image"` 且 `customData.type == "axhub-drawio"` |
|
||||
| 图片元素 | `type == "image"` |
|
||||
| 批注元素 | `customData.annotation` 有值 |
|
||||
|
||||
Axhub 节点字段见 `axhub-nodes.md`。
|
||||
|
||||
## CLI 读取
|
||||
## CLI
|
||||
|
||||
CLI 面向当前浏览器会话。读取元素、节点和批注时仍以 `.excalidraw` 文件为准。
|
||||
没有画布专用 CLI。读取元素、节点和批注时仍以 `.excalidraw` 文件为准;需要截图时,优先使用已有 `canvas-assets` 截图或浏览器页面能力。
|
||||
|
||||
查看当前浏览器连接的画布:
|
||||
## 浏览器截图 API
|
||||
|
||||
```bash
|
||||
axhub-make canvas info
|
||||
Excalidraw 官方暴露的是导出工具方法,例如 `exportToBlob`、`exportToCanvas`、`exportToSvg`,不是当前画布实例的一键截图命令。Axhub 在浏览器里的当前画布实例上封装了全局截图 API:
|
||||
|
||||
```js
|
||||
await window.__AXHUB_EXCALIDRAW_CAPTURE__.captureCanvas()
|
||||
await window.__AXHUB_EXCALIDRAW_CAPTURE__.captureElement('<elementId>')
|
||||
```
|
||||
|
||||
获取当前画布截图:
|
||||
两个方法都返回:
|
||||
|
||||
```bash
|
||||
axhub-make canvas screenshot -o ./canvas.png
|
||||
axhub-make canvas screenshot -c prototypes/my-proto/canvas -o ./canvas.png
|
||||
```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 的画布链接。处理步骤:
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
# Excalidraw 基础指导
|
||||
|
||||
这份只说明如何把内容组织成 Excalidraw 图,不规定 Axhub 画布流程。基础思路参考 `excalidraw-diagram-generator`:先判断图类型,再抽取元素、关系和复杂度,最后生成清晰布局。
|
||||
|
||||
参考来源:https://www.skills.sh/github/awesome-copilot/excalidraw-diagram-generator
|
||||
这份只说明如何把内容组织成普通 Excalidraw 图,不规定 Axhub 画布流程。流程图、关系图、架构图等请求先按 `canvas-workspace` 主文档分流;已确定要画成 Excalidraw 元素后,再读本文件。
|
||||
|
||||
## 先判断图类型
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ description: Use when a Make client批注 or user request asks for 多方案探
|
||||
|
||||
如果用户要求“看看不同方向”“能切换比较”,或当前实现适合在页面内切换方案,就把方案做成 tweak:
|
||||
|
||||
- React 原型优先使用 `axhub-genie-editor-react` 的 `createGenieEditorReactTweakStore` 和 `useRegisterGenieEditorTweak`。
|
||||
- React 原型优先使用 `@axhub/commentary-react` 的 `createCommentaryReactTweakStore` 和 `useRegisterCommentaryTweak`。
|
||||
- 复用项目现有 `schema / values / adapter / update` 模式,不另造平行配置。
|
||||
- 方案字段优先用 `card`,不要用普通下拉。
|
||||
- 每个 `options[]` 项至少包含 `label`、`description`、`value`。
|
||||
@@ -60,10 +60,10 @@ React 最小形态:
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import {
|
||||
createGenieEditorReactTweakStore,
|
||||
useGenieEditorReactTweakStore,
|
||||
useRegisterGenieEditorTweak,
|
||||
} from 'axhub-genie-editor-react';
|
||||
createCommentaryReactTweakStore,
|
||||
useCommentaryReactTweakStore,
|
||||
useRegisterCommentaryTweak,
|
||||
} from '@axhub/commentary-react';
|
||||
|
||||
const optionSchema = {
|
||||
title: '多方案探索',
|
||||
@@ -82,12 +82,12 @@ const optionSchema = {
|
||||
function Example() {
|
||||
const rootRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const store = React.useMemo(
|
||||
() => createGenieEditorReactTweakStore({ variant: 'balanced' }),
|
||||
() => createCommentaryReactTweakStore({ variant: 'balanced' }),
|
||||
[],
|
||||
);
|
||||
const values = useGenieEditorReactTweakStore(store);
|
||||
const values = useCommentaryReactTweakStore(store);
|
||||
|
||||
useRegisterGenieEditorTweak({
|
||||
useRegisterCommentaryTweak({
|
||||
elementRef: rootRef,
|
||||
schema: optionSchema,
|
||||
store,
|
||||
|
||||
48
.claude/skills/extract-annotation-source/SKILL.md
Normal file
48
.claude/skills/extract-annotation-source/SKILL.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: extract-annotation-source
|
||||
description: Use when an Axhub prototype URL needs its PRD, directory, or annotation context read from window.__AXHUB_ANNOTATION_SOURCE__, especially for prototype-as-PRD review, agent context gathering, or annotation extraction with Playwright, Browser, Chrome, or an equivalent page evaluator.
|
||||
---
|
||||
|
||||
# Extract Annotation Source
|
||||
|
||||
Read Axhub prototype context from the runtime snapshot. Treat the page as read-only.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Open the requested prototype URL with Playwright, Browser, Chrome, or an equivalent tool that can evaluate page JavaScript.
|
||||
2. Wait until the app renders, then poll briefly for `window.__AXHUB_ANNOTATION_SOURCE__`.
|
||||
3. Evaluate and return that value. Do not modify the object or write anything back to `window`.
|
||||
4. If the value is missing, report the URL, page title, relevant console errors, and that the annotation runtime snapshot was not published.
|
||||
|
||||
Minimal page evaluation:
|
||||
|
||||
```js
|
||||
await page.waitForFunction(() => window.__AXHUB_ANNOTATION_SOURCE__, { timeout: 10000 });
|
||||
const source = await page.evaluate(() => window.__AXHUB_ANNOTATION_SOURCE__);
|
||||
```
|
||||
|
||||
## What To Report
|
||||
|
||||
- Directory / PRD outline from `source.directory`.
|
||||
- Markdown or PRD entries from directory nodes with `type: "markdown"`.
|
||||
- Annotation count and the important node fields: `id`, `title`, `pageId`, `locator`, `annotationText`, `aiPrompt`, `color`, and `controls`.
|
||||
- Source handoff from `source.source` when present. Report `root` and `manifest`; when source is needed, read the manifest and relevant files via `root`.
|
||||
- Mention whether images are attached by checking `images.length`; do not download images unless the user asks.
|
||||
|
||||
## Data Shape
|
||||
|
||||
```ts
|
||||
type AnnotationSourceRuntimeSnapshot = {
|
||||
directory: AnnotationDirectory | null;
|
||||
nodes: AnnotationNode[];
|
||||
source?: {
|
||||
root?: string;
|
||||
manifest?: string;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
- `directory.nodes` is the prototype tree. Node types are `folder`, `route`, `link`, and `markdown`.
|
||||
- `nodes` is the full annotation list, independent of current page, selected state, and color filter.
|
||||
- Each annotation node includes `id`, `index`, optional `title`, optional `pageId`, `locator`, `aiPrompt`, `annotationText`, `hasMarkdown`, `color`, `images`, optional `controls`, `createdAt`, and `updatedAt`.
|
||||
- `source` is only a source-code discovery reference. In HTML published with source included, `manifest` points to `source/manifest.json`; resolve its paths relative to `source.root`.
|
||||
@@ -43,6 +43,7 @@ description: 原型标注替代 PRD 时使用:把页面目录、组件说明
|
||||
- `folder`:分组目录节点。
|
||||
- `route`:交给宿主处理,可切当前原型页面、状态、数据源或路由。
|
||||
- `markdown`:打开内联 Markdown 文档。
|
||||
- `markdownPath`:只用于目录 Markdown 文档,可指向当前原型目录内的 `docs/*.md`;客户端构建链路会内联为运行时读取的 `markdown`。
|
||||
- `link`:打开其他原型地址、资源地址或外部链接。
|
||||
|
||||
多原型入口优先用 `link` 指向 `/prototypes/<prototype-id>` 或完整 URL;当前原型内部页面/状态入口再用 `route`。
|
||||
@@ -72,3 +73,5 @@ description: 原型标注替代 PRD 时使用:把页面目录、组件说明
|
||||
- 不要把目录节点误写成组件标注节点;目录没有 marker。
|
||||
- 不要把组件状态只写进页面本地 state;需要出现在标注面板里的状态要写进节点 `controls`。
|
||||
- 不要依赖不稳定 CSS 选择器作为唯一定位方式;能补稳定属性时优先补。
|
||||
- `showBrandLink`、`defaultMarkerIndexVisible`、`renderToolbarActions` 这类展示增强选项只有在用户明确要求品牌入口、默认显示序号或工具栏自定义动作时才设置;常规标注接入保持默认配置。
|
||||
- Markdown 图片必须随原型发布:放到当前原型 `assets/` 并用最终可访问 URL;不要用本地路径、`/api/markdown-file` 或 `../assets/...`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: prototype-comments
|
||||
description: 批注、微调、编辑原型时使用:读取原型批注并定位页面元素,修改文案、样式、布局或交互,同步批注处理状态。
|
||||
description: 批注、微调、编辑原型时使用:读取本地原型批注并定位页面元素,修改文案、样式、布局或交互,完成后删除已处理批注任务。
|
||||
---
|
||||
|
||||
# 原型批注处理
|
||||
@@ -9,51 +9,42 @@ description: 批注、微调、编辑原型时使用:读取原型批注并定
|
||||
|
||||
术语边界:
|
||||
|
||||
- 批注 / comment:Genie Editor 里的原型改稿意见,本技能只处理这类内容。
|
||||
- 批注 / comment:Commentary 里的原型改稿意见,本技能只处理这类内容。
|
||||
- 标注 / annotation:AnnotationViewer 的原型说明层,例如 `annotation-source.json` 和 `@axhub/annotation`,不属于本技能处理范围。
|
||||
|
||||
## 默认读取顺序
|
||||
|
||||
1. 先定位目标原型目录:`src/prototypes/<prototype-id>/`。
|
||||
2. 优先读取本地文件:`src/prototypes/<prototype-id>/.spec/prototype-comments.json`。
|
||||
3. 若文件不存在,再结合当前页面、用户上下文或旧缓存提示判断;需要截图、导出图片或同步页面状态时才使用页面同步能力。
|
||||
2. 读取本地文件:`src/prototypes/<prototype-id>/.spec/prototype-comments.json`。
|
||||
3. 若文件不存在,结合用户上下文和当前代码判断目标;不调用 CLI/API,也不依赖浏览器运行中的页面。
|
||||
|
||||
## 本地文件结构
|
||||
|
||||
批注记录固定在 `.spec/prototype-comments.json`:
|
||||
批注记录固定在 `.spec/prototype-comments.json`,核心字段是 `comments/tasks/images`:
|
||||
|
||||
- `comments`:批注和修改记录,包含 locator、comment、marker,以及 text/style/tweak 的修改前后。
|
||||
- `tasks`:按 `elementKey` 记录 `idle`、`editing`、`completed`、`error` 状态。
|
||||
- `images`:只记录 metadata 和 `assetPath`。图片文件在 `.spec/prototype-comment-assets/`。
|
||||
- `tasks`:按 `elementKey` 保存待处理任务信息。
|
||||
- `images`:只记录 metadata 和 `images[].assetPath`。图片文件位于 `.spec/prototype-comment-assets/`。
|
||||
|
||||
不要把新的 base64 图片内容写回 JSON;需要新增图片素材时放入 assets 目录,并在 `images[].assetPath` 里引用。
|
||||
读取图片时只使用本地 `images[].assetPath`,基于 `.spec/prototype-comment-assets/` 查找文件。不要把新的 base64 图片内容写回 JSON;需要新增图片素材时放入 assets 目录,并在 `images[].assetPath` 里引用。
|
||||
|
||||
## 处理流程
|
||||
|
||||
1. 读取 `.spec/prototype-comments.json`,按 `comments` 理解修改意图和定位信息。
|
||||
2. 只在定位不清、需要检查页面现状、需要导出批注图片时,使用页面截图或同步命令。
|
||||
2. 如有批注图片,按 `images[].assetPath` 读取本地文件辅助理解。
|
||||
3. 修改 `src/prototypes/<prototype-id>/` 下的实现文件,保持改动范围聚焦。
|
||||
4. 修改前后都更新本地 JSON:
|
||||
- 开始处理某项时,把 `tasks[elementKey].state` 设为 `editing`,记录 `provider`、`requestId`、`sessionId`、`updatedAt`。
|
||||
- 成功后设为 `completed`。
|
||||
- 失败或阻塞时设为 `error`,写清 `message`。
|
||||
- 放弃处理时设为 `idle`。
|
||||
5. 页面状态同步只作为 best-effort。同步失败不阻塞代码修改和本地 JSON 记录。
|
||||
6. 按项目规则完成预览验证;无法验证时说明原因。
|
||||
4. 完成一个批注任务后,只清理本地批注文档:删除对应批注记录和任务记录,不写任务进度字段。
|
||||
5. 按项目规则完成预览验证;无法验证时说明原因。
|
||||
|
||||
## 页面同步辅助
|
||||
## 删除规则
|
||||
|
||||
需要时可以使用本地页面同步能力:
|
||||
- 用 `comments[].elementKey` 作为主键删除已完成批注。
|
||||
- 删除同 key 的 `tasks[elementKey]`。
|
||||
- 删除 `elementKey` 匹配且不再被其他剩余批注引用的 `images[]` 记录。
|
||||
- 对 `.spec/prototype-comment-assets/`,只删除与被移除 `images[].assetPath` 对应、且不再被 JSON 引用的文件。
|
||||
- 如果某条批注没有 `elementKey`,用 `locator`/`label` 辅助人工匹配;匹配不确定时保留,不误删。
|
||||
|
||||
```bash
|
||||
npx @axhub/genie status --json
|
||||
npx @axhub/genie editor clients list --channel make
|
||||
npx @axhub/genie editor node screenshot --channel <channel> --target-client-id <id> --element-key <key> --output-dir .local/genie-editor
|
||||
npx @axhub/genie editor context-images export --channel <channel> --target-client-id <id> --output-dir .local/genie-editor
|
||||
npx @axhub/genie editor editing set --channel <channel> --target-client-id <id> --element-key <key> --state completed --provider codex --task-request-id <request-id>
|
||||
```
|
||||
|
||||
`snapshot` 和 `nodes list` 只作为诊断页面同步异常的工具,不是默认读取步骤。
|
||||
清理规则示例:完成 `elementKey=hero` 后,移除 `comments` 中的 `hero` 批注、移除 `tasks.hero`、移除只属于 `hero` 的 `images` 记录及 `hero-only.png`;如果 `shared.png` 仍被其他剩余批注引用,则保留该图片记录和本地文件。
|
||||
|
||||
## 完成回复
|
||||
|
||||
|
||||
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