ooodc a7883dbed9 refactor(todo): 重构待办事项管理逻辑及更新状态规则
- 移除 TodoItem 中的 priority、created_at 和 updated_at 字段
- 强制每个任务都必须有唯一 id,且由用户负责生成
- 修改合并模式逻辑,merge=true 下保留未提及的旧任务
- 支持已完成和已取消任务重新激活(状态改回 pending 或 in_progress)
- 禁止 in_progress 状态退回到 pending,必须标记为 completed 或 cancelled
- 优化状态转换校验,允许特定状态间合法切换
- 简化任务变更消息,移除详细的新增/更新/移除统计
- 更新文档和示例,明确 id 必须由用户生成和使用
- 修复和补充测试,增强状态转换和合并模式验证
- 调整任务时间戳生成逻辑,统一使用当前时间及索引
- 该变更提供更合理的任务状态机械及管理模式,提升稳定性和易用性
2026-06-13 09:22:33 +08:00

358 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DSL Schema
> 本文件只说明 **DSL 里能写什么**节点类型、字段、枚举值、硬约束。布局策略、组合方法、Dagre/Flex 心智模型统一放在 `elements/layout.md`。
> `?` 表示该字段在 schema 层是 optional若需要稳定产出再参考对应 scene 或 layout 文件中的最佳实践。
**📝 布局引擎核心法则**
- **基本行为与 Flexbox 等同**Frame 布局基于 Yoga 引擎。`layout: 'horizontal'` = `flex-direction: row``fill-container` = `flex: 1``fit-content` = `width: auto``gap` / `padding` / `alignItems` / `justifyContent` 语义相同。
- **枚举值无 flex- 前缀**:一律使用 `'start'` / `'end'` 而非原生 CSS 的 `'flex-start'` / `'flex-end'`
- **默认对齐的差异**`alignItems` 的默认值是 `'start'`(原生 CSS 默认是 `stretch`)。所以同排卡片需要等高时,**必须显式声名** `alignItems: 'stretch'`
- **Dagre 引擎的特殊性**`layout: 'dagre'` 作为专属拓扑连线引擎,自身不支持 `fill-container` 宽高,对其父容器而言,它是一个自适应(打包裹)的黑盒。
## WBDocument
```typescript
interface WBDocument {
version: 2;
nodes: WBNode[]; // 顶层节点。connector 必须放在这里,不能嵌套在 children 中
}
```
## 节点类型
### Frame容器
唯一可以包含子节点的类型。用于分组、布局、背景。
```typescript
{
type: 'frame';
id?: string;
x?: number; y?: number; // Flex 子节点不需要 x/y
width: WBSizeValue;
height: WBSizeValue;
layout: 'horizontal' | 'vertical' | 'none' | 'dagre'; // 布局模式
gap: number; // 必须显式写(不写节点会粘连,容易出 bug
padding: number | [number, number] | [number, number, number, number]; // 必须显式写(不写内容贴边)
justifyContent?: 'start' | 'center' | 'end' | 'space-between' | 'space-around';
alignItems?: 'start' | 'center' | 'end' | 'stretch';
layoutOptions?: { // 仅当 layout 为 'dagre' 时生效
rankdir?: 'TB' | 'BT' | 'LR' | 'RL';
nodesep?: number;
edgesep?: number;
ranksep?: number;
edges?: Array<[string, string] | [string, string, string]>; // [fromId, toId, label?] 引擎自动排版子节点并生成贝塞尔曲线连线
isCluster?: boolean; // 透明子图。为 true 时子节点参与父级 Dagre 拓扑运算,连线可穿越边界
clusterTitle?: string; // 子图悬浮标题(自动吸附左上角)
clusterTitleColor?: string; // 标题颜色 (HEX格式如 "#8B5CF6")
};
fillColor?: string;
borderColor?: string;
borderWidth?: number;
borderDash?: 'solid' | 'dashed' | 'dotted';
borderRadius?: number;
children?: WBNode[]; // 不能包含 connector
}
```
**Dagre 嵌套排版规则**
1. **不透明节点Opaque Node**Dagre 内的子容器,无论 `layout``flex``absolute` 还是 `dagre`,只要未声明 `isCluster: true`,对外层 Dagre 就是具有确定宽高的不透明原子节点。外层连线无法寻址其内部子节点。
2. **连线兜底重定向Edge Redirect Fallback**:当 `edges` 引用了某不透明节点内部的子节点 ID 时,引擎自动将该连线端点重定向至其最近的不透明祖先节点。不报错,不产生悬空连线。
3. **透明子图Compound Cluster**:子容器同时声明 `layout: "dagre"``layoutOptions: { isCluster: true }` 时,成为外层 Dagre 的复合子图。其内部子节点直接参与外层拓扑运算,连线可穿越子图边界。子图自身不执行独立排版,尺寸由外层 Dagre 根据内部节点包围盒自动撑开。
**isCluster 最小用法**
```json
{
"type": "frame", "id": "cluster_a",
"layout": "dagre", "layoutOptions": { "isCluster": true },
"fillColor": "#F0FDF4", "borderColor": "#86EFAC", "borderWidth": 2, "borderDash": "dashed", "borderRadius": 16,
"children": [
{ "type": "text", "text": "区域标题", "fontSize": 11, "textColor": "#15803D" },
{ "type": "rect", "id": "node_inside", "width": 120, "height": 40, "text": "内部节点" }
]
}
```
> 注意:`edges` 必须写在**最外层的根 Dagre** 的 `layoutOptions` 中,不要写在 cluster 内部。
**其他约束**
- `layout / gap / padding` 在 schema 层是 optional但实际生成时推荐显式写出避免依赖默认行为。
- `layoutOptions` 仅在 `layout: 'dagre'` 时生效。
- `children` 里不能出现 `connector`
> **虚拟 frame 陷阱**:没有 `fillColor`、`borderColor`、`borderWidth` 的 frame 在编译时可能被当作纯布局容器跳过(子节点直接提升到父级)。如果给这种 frame 设了 `id` 并让外部 connector 连接它,编译后 frame 消失connector 引用会失效。需要保留这个 frame 时,请给它加上不会被优化掉的外观属性。
### 基础图形
```typescript
{
type: 'rect' | 'ellipse' | 'cylinder' | 'diamond' | 'triangle' | 'trapezoid';
id?: string;
x?: number; y?: number;
opacity?: number; // 0-1仅影响 fillColor 的透明度(对 frame/text/stickyNote 无效)
vFlip?: boolean;
hFlip?: boolean;
width: WBSizeValue;
height: WBSizeValue;
fillColor?: string;
borderColor?: string;
borderWidth?: number;
borderDash?: 'solid' | 'dashed' | 'dotted';
borderRadius?: number;
topWidth?: number; // 仅对 triangle / trapezoid 有效,梯形顶边宽度或三角形顶角截断宽度
text?: string | WBTextRun[]; // 纯文本或富文本
fontSize?: number;
textColor?: string;
textAlign?: 'left' | 'center' | 'right'; // Shape 默认 'center'(与 CSS 不同)
verticalAlign?: 'top' | 'middle' | 'bottom'; // Shape 默认 'middle'(与 CSS 不同)
}
```
> **cylinder 约束**cylinder 的弧度固定 16px不随宽度缩放。宽度过大会变成扁椭圆。禁止 `width: "fill-container"`,必须用固定宽度 + `height: "fit-content"`。宽度根据文字长度选择,通常 120-200px。
> **Shape 内边距TEXT_INSET**Shape 节点有强制内边距fit-content 会自动补偿。
> - rect / ellipse / diamond / triangle上下左右各 12px
> - cylinder顶部弧形 32px + 底部弧形 10px垂直 +42px水平各 7px
>
> 需要手算固定尺寸时:`实际文字宽/高 + 对应 inset`。
> rect 内 14px 字号两行文字高 ~32px → `height >= 32 + 24 = 56px`
### Image图片节点
图片节点用于在画板中展示图片。图片不能直接使用 URL必须先上传到飞书获取 media token。
```typescript
{
type: 'image';
id?: string;
x?: number; y?: number;
width: WBSizeValue; // 固定宽度,推荐 240 或 200
height: WBSizeValue; // 固定高度,推荐按 3:2 比例(如 240×160 或 200×133
image: {
src: string; // media token通过 docs +media-upload --parent-type whiteboard 上传获取)
};
}
```
> **关键约束**
> - `image.src` 必须是通过 `docs +media-upload --parent-type whiteboard --parent-node <画板token>` 上传后返回的 **media token**,不能是 URL 或 Drive file token
> - 图片必须上传到**目标画板**,跨画板的 token 不可用
> - 同一画板内所有 image 节点应使用统一的 width/height保持视觉一致
> - 图片宽高比推荐 3:2如 240×160避免变形
> - 详细上传流程见 [`elements/image.md`](../elements/image.md)
### Text纯文本节点
```typescript
{
type: 'text';
id?: string;
x?: number; y?: number;
width: WBSizeValue;
height: WBSizeValue;
text?: string | WBTextRun[];
fontSize?: number;
textColor?: string;
textAlign?: 'left' | 'center' | 'right';
verticalAlign?: 'top' | 'middle' | 'bottom';
}
```
### StickyNote便签
```typescript
{
type: 'stickyNote';
id?: string;
x?: number; y?: number;
width: WBSizeValue;
height: WBSizeValue;
fillColor?: '#FEF1CE' | '#F5D1A7' | '#DFF5E5' | '#CDF7CC' | '#C9E8EF' | '#D6DCF3' | '#D3CCEE' | '#F1C5E7' | '#F6C8C8'; // 便签底色(仅支持这 9 种)
text?: string | WBTextRun[];
fontSize?: number;
textColor?: string;
textAlign?: 'left' | 'center' | 'right';
verticalAlign?: 'top' | 'middle' | 'bottom';
}
```
### Connector连线
必须放在顶层 `nodes` 数组中,不能嵌套在 frame 的 `children` 里。
```typescript
{
type: 'connector';
id?: string;
connector: {
from: string | { x: number; y: number }; // 节点 id 或坐标
to: string | { x: number; y: number };
fromAnchor?: 'top' | 'right' | 'bottom' | 'left';
toAnchor?: 'top' | 'right' | 'bottom' | 'left';
lineShape?: 'straight' | 'polyline' | 'curve' | 'rightAngle'; // 直线、圆角折线、曲线、直角折线
lineColor?: string;
lineWidth?: number;
lineStyle?: 'solid' | 'dashed' | 'dotted';
startArrow?: 'none' | 'arrow' | 'triangle' | 'circle' | 'diamond';
endArrow?: 'none' | 'arrow' | 'triangle' | 'circle' | 'diamond';
waypoints?: { x: number; y: number }[]; // polyline 途经点
label?: string; // 连线中间的标签文字
labelPosition?: number; // 标签位置0-1默认 0.5(中点)
};
}
```
### SVG
```typescript
{
type: 'svg';
id?: string;
x?: number; y?: number;
opacity?: number;
width: WBSizeValue;
height: WBSizeValue;
svg: { code: string }; // SVG 代码字符串
}
```
#### 渲染规范
SVG 通过 `image/svg+xml` Blob 加载到画布,**不在 HTML DOM 中**,因此存在严格限制:
**必须**
- 包含 `viewBox` 属性(如 `viewBox="0 0 24 24"`),引擎依赖它确定坐标系
- 包含 `xmlns="http://www.w3.org/2000/svg"`SVG 作为独立 `image/svg+xml` 解析时XML 规范要求声明命名空间)
**允许的元素**(纯几何绘制):
- 基本图形:`<rect>` `<circle>` `<ellipse>` `<line>` `<polyline>` `<polygon>` `<path>`
- 渐变/滤镜:`<defs>` `<linearGradient>` `<radialGradient>` `<filter>` `<feGaussianBlur>` `<feMerge>`
- 结构:`<g>` `<clipPath>` `<mask>` `<use>`
**禁止的元素**(字体和外部资源在 Blob 沙箱中无法加载):
- `<text>` `<tspan>`(用同层 DSL rect 节点 + text 属性替代)
- `<image>`(用同层 DSL image 节点替代)
- `<foreignObject>`
- 任何引用外部 URL 的属性(`xlink:href` 指向远程资源等)
#### 两种典型用法
**1. 背景装饰 SVG**(大尺寸,与 frame 同大小)
用于绘制连线、曲线、发光效果等几何背景。文字信息通过同一 frame 内的 rect 节点叠加:
```json
{
"type": "frame", "width": 1400, "height": 680, "layout": "none",
"children": [
{ "type": "svg", "x": 0, "y": 0, "width": 1400, "height": 680,
"svg": { "code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1400 680\" ...>...</svg>" } },
{ "type": "rect", "x": 100, "y": 50, "width": 200, "height": 40,
"text": "Label", "fillColor": "transparent" }
]
}
```
**2. 内联图标 SVG**24-48pxFeather/Lucide 风格)
用于卡片/按钮中的小图标,纯 stroke 线条:
```json
{ "type": "svg", "width": 32, "height": 32,
"svg": { "code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#3B82F6\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg>" } }
```
### Icon内置图标
引用画板内置图标库的图标。比手写 SVG 更简单——只需指定 `name`
```typescript
{
type: 'icon';
id?: string;
x?: number; y?: number;
width?: WBSizeValue; // 默认 48
height?: WBSizeValue; // 默认 48保持正方形
name: string; // 图标名称,从 npx -y @larksuite/whiteboard-cli@^0.2.11 --icons 输出中选取
color?: string; // 可选颜色覆盖hex 格式如 '#FF6600'
}
```
**获取可用图标**:规划好内容和布局后,运行以下命令查看所有可用图标名,从中选取:
```bash
npx -y @larksuite/whiteboard-cli@^0.2.11 --icons
```
用法:
```json
{ "type": "icon", "id": "db", "name": "database", "width": 48, "height": 48 }
```
**使用建议**
- 当图表中的节点代表具体事物(服务器、用户、数据库等)时,用图标比纯文字方块更直观
- 一张图 3-8 个图标为宜,为关键组件配图标,次要节点用普通形状
-`color` 为图标指定合适的颜色, 比如与所在容器的配色一致
- 图标可放在 frame 子元素中参与 flex 布局,连线可通过 id 连接到图标
- 图标+文字组合frame(vertical) 中放 icon + text形成富组件
```json
{
"type": "frame", "layout": "vertical", "gap": 8, "padding": 12,
"alignItems": "center", "fillColor": "#F0F5FF", "borderColor": "#ADC6FF",
"children": [
{ "type": "icon", "id": "db-icon", "name": "database", "width": 36, "height": 36 },
{ "type": "text", "text": "PostgreSQL", "fontSize": 12, "width": "fit-content", "height": "fit-content" }
]
}
```
---
## 富文本 WBTextRun
`text` 字段可以是纯字符串或 `WBTextRun[]` 数组。类似 HTML 内联样式bold 对应 `<b>`italic 对应 `<i>`listType 对应 `<ol>/<ul>`。每个 run 是一段带样式的文字:
```typescript
interface WBTextRun {
content: string; // 文字内容,可含 \n 换行
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikeThrough?: boolean;
fontSize?: number;
color?: string; // 文字颜色
backgroundColor?: string; // 文字高亮背景
hyperlink?: string;
listType?: 'none' | 'ordered' | 'unordered';
indent?: number; // 缩进级数
quote?: boolean; // 引用块
}
```
示例:
```json
{
"text": [
{ "content": "标题文字\n", "bold": true, "fontSize": 16 },
{ "content": "正文内容,", "fontSize": 14 },
{ "content": "高亮部分", "backgroundColor": "#FEF1CE", "fontSize": 14 }
]
}
```
`text``content` 中出现的双引号必须写成 `\"`,这是 JSON 规范要求。换行用 `\n`JSON 中写为 `"第一行\n第二行"`,不要双重转义为 `\\n`)。
---
## 尺寸值 WBSizeValue
| 值 | 含义 | 注意 |
| --------------------- | --------------------------- | -------------------------------------- |
| `number` | 固定像素 | 任何场景 |
| `'fit-content'` | 由内容决定大小 | 父级需要 Flex 布局 |
| `'fit-content(N)'` | 同上,无内容时 fallback N | 同上 |
| `'fill-container'` | 填满父级剩余空间 | 父级需要 Flex 布局,且祖先链有固定宽度 |
| `'fill-container(N)'` | 同上,无 Flex 时 fallback N | — |
`fill-container``layout: 'none'`(绝对定位)下无效。`fit-content` 仍可用于含文字节点(引擎通过 Yoga measureFunc 测量文字尺寸)。