当前环境
- 工作空间 文件夹(workspace_folder):可以在SystemMessage中找到 或者 在整个上下文中最近一次的user对话中找到,标签内有定义“Workspace Folder”的值, 如果找不到就取默认值
~/.wedata
智能看板搭建(声明式 Spec 架构)
LLM 唯一编写 kanban_spec.py(约 80–150 行),取数 / SQL / 切片 / DSL / Dataset 入库全部由 runner 完成。
🛑 P0 硬约束(违反 = 执行失败)
「流程类」代码无法兜底,必须 LLM 显式遵循;「数据类」DSL/runner 已构造期 raise,按报错文本改即可。
| # | 类 | 规则 |
|---|---|---|
| 1 | 流程 | 必须 spec 模式:构造 Spec(...) + 调 build_kanban(spec);禁止手写 df_to_slot_data/write_kanban_outputs 调用链 |
| 2 | 流程 | reference/ 默认只读:业务个性化走 Chart.chart_config 透传 / DSL widget extras,禁为单个 spec 改 reference |
| 3 | 数据 | raw_sql 列序契约:用工厂 raw_sql(title=..., sql=..., slot_columns=[...], kind=...)(内部自动 escape_hatch=True);slot_columns 顺序与 SELECT 严格一致;FROM/JOIN 后表名按 route_hint.connection_type 分派:lakehouse/StarRocks/Doris 三段 catalog.db.table;MySQL/PG/GaussDB 两段 db.table(禁占位名 _kb_src/main_data) |
| 4 | 流程 | 图表多样性:Spec.charts ≥ 5 种不同 chart_type(用户明确只要某几种除外) |
| 5 | 数据 | 同环比用 Compare:禁 chart('line', metrics=[..., 'mom_xxx']) 假装;必须 compare(dim=time_dim(...), metric=..., kinds=['mom']) |
| 6 | 流程 | Step E 不主动发布:runner 已自动调 UpdateAiKanBan 保存/更新 PREVIEW 并 print 追问语;禁主动调 SaveAiKanBan 发布态、禁 turn-id、禁二次总结,等用户输入走 Step G |
| 7 | 流程 | 修改必走 spec 重跑:禁手改 kanban_save_params.json / kanban_dsl.json,改 spec 重跑 build_kanban(runner 自动保存/更新 PREVIEW,三端统一入库 DSL+Dataset) |
| 8 | 流程 | 发布/预览 helper 分离:「保存/发布/上线/确认发布」→ save_to_kanban_list()(只调 SaveAiKanBan,入参仅 WorkspaceId + AccessKey,写发布态);「保存预览/更新预览/同步预览」→ update_to_kanban_list()(只调 UpdateAiKanBan,携带 DSL/Datasets/元信息写 PREVIEW);禁裸调 wedatacli + 拼 stdin JSON |
| 9 | 流程 | 不要查 workspace_id:spec 省略该字段即可(wedatacli 自动注入);禁 env | grep workspace / cat ~/.wedata/* / echo $TENCENTCLOUD_WORKSPACE_ID 等探测 |
| 10 | 数据 | DSL 报错按文本改:看到 [DSL] Chart "..." (kind=...) ... 直接按报错文本改 spec(dsl raise 文案已带正例与修法),不改 reference / 不写新脚本 |
| 11 | 流程 | stop 后发布/预览保存最高优先级:Pre-routing 命中 → 必须直跳 Step G;禁误判为 Step F 重跑、禁追问、禁闲聊 |
| 12 | 数据 | 数据保真自适应:能聚合不截断、要截断必配 order_by、时间型降粒度、分类基数爆炸换 kind;DSL 构造期硬约束 + Runner 软兜底(500 行/900 单元格/100 类目) |
| 13 | 流程 | prefetch 单次原则:每轮最多调 1 次;exit 2/3 必须直接 stop,严禁自行换关键词/扩词/翻译/拆意图重跑(唯一例外:用户回复选定表后重跑) |
| 14 | 流程 | spec 落地位置:文件名固定 kanban_spec.py,路径固定 <workspace_folder>/.kanban_output/kanban_spec.py(与产物同根:kanban_save_params.json / kanban_dsl.json / 数据快照 CSV);禁写 plugin 目录/cwd 根//tmp;Step F 直接覆盖同一路径 |
| 15 | 数据 | OLAP 数据源必须走 raw_sql:Step B route_hint.sql_type=3(MySQL/PG/GaussDB/StarRocks/Doris)时:① Spec.charts 所有项必须走 raw_sql(title=..., sql=..., slot_columns=[...], kind=...),SQL 用目标数据源原生方言手写;② compare(...) 也禁用(内部展开会引入 spark_safe_*),改用 raw_sql(kind='line', sql='WITH agg AS (...) SELECT time, curr, (curr-LAG(curr) OVER (ORDER BY time))/NULLIF(LAG(curr) OVER (ORDER BY time),0)*100 AS prev_pct FROM agg', slot_columns=['time','curr','prev_pct']) 手写同环比;③ Spec.kpis 可保留 kpi(expr, label) 但 expr 只允许通用聚合(SUM/COUNT/AVG/MIN/MAX + 目标方言函数),禁 spark_safe_* / percentile_approx;④ FROM/JOIN 后表名严格按目标方言段数(MySQL/PG/GaussDB 两段 db.table;StarRocks/Doris 三段 catalog.db.table)——cache key 双端已按尾两段归一,段数任选一致的写法都能命中 prefetch 缓存。违反 runner 写盘前 raise。lakehouse(sql_type=1)路径不受影响 |
| 16 | 流程 | 禁臆断数据源不支持:支持性判定唯一以 prefetch_table.py 输出为准——route_hint.sql_type ∈ {1,3} 即支持;只有脚本显式返回 status:"route_invalid" / status:"not_found" / status:"cross_source_not_allowed" 才是"不支持"。MySQL/PostgreSQL/GaussDB/StarRocks/Doris 全部属于 sql_type=3 支持路径。作用域:本条只约束「看板创建 / 修改流程」(Step A→G)中的对外表达;未跑 prefetch_table.py 或未拿到 route_hint 前禁出现「不支持 PostgreSQL / PG / MySQL / OLAP / 该数据源类型」「当前看板不支持…」「建议改用 lakehouse」等结论。若脚本已返回明确错误状态,可原样透传该状态给用户 |
| 17 | 流程 | prefetch 成功即直进 Step C→D:prefetch_table.py exit=0 拿到 schema JSON 后,本轮必须依序完成「Step B 数据概览表 → Step C 布局摘要 → Step D 写 spec + 跑 build_kanban」一次性交付,中途禁 stop / 追问 / 换工具探数。禁裸调 wedatacli query-data / wedatacli query-sql / wedatacli get data 等取数子命令做数据探查(这类命令仅 runner 内部使用);需要看真实分布只有一种合法做法:直接把该 SQL 写进 raw_sql(...) chart,由 runner 通过取数链路统一执行 |
🎯 P0-10 速查(DSL 不 raise 的真陷阱)
⚠️ 作用域:
spark_safe_*/DATEDIFF仅适用 lakehouse(sql_type=1)。OLAP(sql_type=3)遵 P0-15:raw_sql 用目标方言手写,禁spark_safe_*/percentile_approx。
时间列零返工门禁(lakehouse):
Spec.source.columns必须整列带type;time_dim()/compare()会在Spec构造期从裸列名回填时间类型:date/date32→date,timestamp/datetime系列 →timestamp;缺type时默认按string处理。type='date/timestamp/datetime':常规趋势/同环比优先 DSL(time_dim/compare);不要因旧版 DuckDBtry_strptime(DATE,...)经验盲目改raw_sql,也不要把真实类型伪装成string。若必须写 lakehouseraw_sql,物理日期列用原生DATE_FORMAT(date_col,'yyyy-MM'),不要套spark_safe_date_format。type='string':自写 metric/raw_sql 里的时间提取、比较、DATEDIFF先包spark_safe_to_timestamp(col);禁TO_TIMESTAMP。- 8/14 位紧凑日期字符串:不要直接用
time_dim;改raw_sql显式spark_safe_to_timestamp_extended(col)后再分桶。 - 10/13 位 Unix 字符串或数值时间戳:不要依赖
time_dim自动解析;需要分桶时用raw_sql显式FROM_UNIXTIME(col)(毫秒先/1000)后再格式化。 raw_sql不走 DSL 类型传播;时间函数、slot_columns与本地 DuckDB 兼容由 SQL 自己负责。- 🛑
spark_safe_*白名单仅 6 个:to_timestamp/to_date/datediff/date_format/week_format/to_timestamp_extended;禁止发明spark_safe_now/spark_safe_year/spark_safe_unix_timestamp。
🕒 lakehouse DATEDIFF(DSL raise,仅一种形态):DATEDIFF(end, start) 返回 end-start 天数;string 列先包 spark_safe_to_timestamp;Unix 秒/毫秒先显式 FROM_UNIXTIME 转时间。❌ 全 raise:DATEDIFF(DAY,...) / DATE_DIFF(...)。⚠️ 历史数据集不宜用 CURRENT_TIMESTAMP 锚点,完整模板见 kanban_spec_example.py 第 5.17 节。OLAP 对应:MySQL TIMESTAMPDIFF(DAY,s,e);PG/GaussDB (e::date-s::date);StarRocks/Doris datediff(e,s)。
💡 OLAP raw_sql 本地兼容:runner 已内置
DATE_FORMAT常见日期粒度格式、TO_CHAR / TIMESTAMPDIFF / STR_TO_DATE / x::type→ DuckDB 自动翻译或原生通过(仅本地体检,远端 sqlSlots 保持目标方言原文)。看到 stderr 有[runner] OLAP → DuckDB相关日志属正常,不要改 spec。
软告警告知项(看到不要改 spec,runner 已自动处理):order_by 含聚合→重写为实际输出别名;kpi(...) 传 emoji/span/slot_key→静默丢弃;title 含 emoji + emoji=→自动去重;raw_sql 中 CURRENT_TIMESTAMP / NOW() 在本地体检期自动改写为 current_localtimestamp()(远端目标方言保持原文),可直接使用不必因本地报错放弃时间过滤(历史数据集仍遵上文警示改用快照日锚点)。
硬告警必改项:runner 输出 ❌ [本轮 string 时间列解析失败清单] → 把 time_col 换成 Spec.source.columns 里的 DATE/TIMESTAMP 列;无真 DATE 列时改走 raw_sql。
Spec 重写门禁:若报 [DSL] Spec 全局契约预检失败,必须按清单一次性修完全部项再重跑;重点看 scatter 只认 x/y/category、time_dim 排序别名为 <col>_<gran>、普通 DSL 禁窗口/嵌套聚合(累计/自算同环比改 raw_sql + WITH)。
🔍 列引用强校验(DSL raise):source.columns 为 dict 时,kpi/dim/metric/compare expr 列名必须已声明;报错时二选一:①拼错→对照 Step B 改 expr;②漏写→补回 source.columns(带 type)。raw_sql / kpi(from_sql=) 不校验。
📐 数据保真自适应(P0-12 速查)
4 原则:① 能聚合不截断 ② 要截断必配 order_by ③ 时间型降粒度 ④ 基数爆炸换 kind。DSL 构造期 raise(违反报错带修法)+ Runner 软兜底(500 行/900 单元格/100 类目阈值,触发时按 Top 截断非随机抽样)。
| kind | 推荐写法 |
|---|---|
scatter |
N≤500 全量;>500 加 limit=500, order_by='-y';>50K 用 CASE WHEN 分桶聚合(dims[0] 必数值;裸列仅当 source.columns 已声明 type='double/decimal/bigint/int' 时合法,纯字符串列名场景写 dim('price * 1.0', alias='price')) |
table |
优先 [category, time_dim('time','month')] 聚合;无聚合 runner 自动 LIMIT 50 |
parallel |
前 N-1 维用 AVG/SUM 聚合,最后一维分类 |
graph 边模式 |
limit=30, order_by='-value' |
heatmap |
两维基数 ≤ 30 直出;高基数先 SQL 降粒度或 raw_sql + CTE Top30 |
treemap/sunburst |
单层 ≤ 100;多层用路径 |
boxplot |
lakehouse:C≤20 直出;20~100 可用 raw_sql + percentile_approx;OLAP:遵循 P0-15,用目标方言分位函数/近似方案,禁 percentile_approx;>100 换 bar |
时间趋势 line/candlestick |
time_dim(...,'month'/'week') 降粒度,不要 limit |
反例代码见 kanban_spec_pitfalls.py 反例 14~22;DSL raise 已含修法,按需查阅。
🔗 多表场景
用户明提 ≥2 数据来源 → 多表:Spec.source 为主表,其他表走 raw_sql chart(runner 自动注册为 DuckDB 伴随视图)。硬约束:Step B 一条 prefetch_table.py --tables "t1,t2,..." 搞定(拆调用违 P0-13);raw_sql 表名分派同 P0-3。三种写法(独立展示 / JOIN / UNION)见 kanban_spec_example.py 第 7.5 节。
💡 runner 打
🔗 多表伴随视图已注册:xxx是多表生效信号;Catalog "xxx" does not exist软告警原样重跑即愈。
🛑 概念二分:预览 = 入库 PREVIEW 态(runner 每次
build_kanban后自动调UpdateAiKanBan保存/更新,三端统一预览源);发布 = 写发布态(用户明说「保存/发布/上线/确认发布」时才调SaveAiKanBan)。单次取数→intelligent-query;看板 / 仪表盘 / dashboard(含平台、云端、AI、发布、分享、嵌出等复合诉求)→ 本 skill。
🚦 Pre-routing · stop 后意图识别门(强制)
Step E/F 的 stop 之后,每收到一条新消息,回答前必走一遍词表(子串匹配、大小写/中英文同效):
| 优先级 | 命中词(任一) | 走向 |
|---|---|---|
| P0,发布 | 保存/发布/上线/确认发布/发布到看板列表/入库/加到仪表盘;save/publish/release | Step G1 |
| P0,预览 | 保存预览/更新预览/同步预览/推送预览;preview/update preview/sync preview/push preview | Step G2 |
| P1 | 改/换/加/删/调整/重新生成/换风格/换颜色/加指标;modify/change/add/remove/adjust/regenerate/restyle | Step F |
| P2 | 看板/驾驶舱/大盘/仪表盘 + 表名或业务词;dashboard/cockpit/board + table/biz term(首次创建) | Step A → E |
他裁:P0+P1 默认 P0;「先改 x 再发布/保存/更新」只走 F;同时命中“发布”和“预览”默认发布(G1);都不命中 → 常规对话(本轮禁主动调 helper / 重跑 build_kanban / 裸调 wedatacli)。
P0 命中后三件事:调 helper → 贴 AccessKey 1–2 行总结 → stop。禁:重跑 build_kanban / 裸调 wedatacli 与 Save/Update RPC / 「好的正在为您保存」「保存还是更新?」等客套追问。
主流程(Step A → G)
🌐 语言一致性:跟随用户主语言(表名/列名/SQL 标识符例外,永远原样);单轮面向用户的字面量必同一语言(禁斩杆/括号并列)。Spec 的
title/label/emoji同步。标题对照(中/EN任取一列):
Step A · 需求理解=Requirements;Step B · 数据分析=Data Analysis;Step C · 布局摘要=Layout;Step D · 生成看板=Build;Step E · 验收与总结=Summary。字段对照:
数据来源/主题/同环比口径=Source/Topic/Compare;表头数据源\|时间字段\|关键维度\|关键指标=Source\|Time\|Dims\|Metrics;看板内容概览/数据特征提示/无=Overview/Data Notes/none。默认按月 mom,待 Step B 确认=monthly mom, TBD;追问请告知数据来源=Please specify data source。
Step A · 需求理解
🛑 输出契约:
- ✅ 先打小标题(取上方对照表 Step A 行),再进 Step B
- ✅ 标题下 1–3 行 bullet:①
**数据来源**:<原词原样>(禁翻译) ②**主题**:<...>③**同环比口径**:<...>(未指定 → 用对照表默认值) - ✅ 进 Step B 二元判定(机械):能抠出≥1 token(三段式/中英文裸名/模糊业务词)→ 直接进 Step B,原始 token 喂
prefetch_table.py;完全无数据线索 → 按对照表追问语 stop - ❌ 禁 LLM 自判业务词「够不够具体」;禁跳 Step B;禁从历史推断表名;禁人工切多意图
P0:表名解析已下沉到
prefetch_table.py,LLM 不要自行调wedatacli search找表。
Step B · 数据分析(对外标题;内部完成 schema 拉取 + 取数预取,无需暴露给用户)
唯一允许的命令(原样复制,把 <用户原始输入> 替换为用户给的表名/业务词,支持三段式 / 中文 / 英文裸名 / 多意图,、,与以及分隔;脚本内部已做表名解析 + Schema 拉取 + 后台预取 CSV,三件事一次完成):
# 任选其一执行(已以重要性排序:先 KANBAN_REFERENCE_DIR、后 CODEBUDDY_PLUGIN_ROOT):
export KANBAN_REFERENCE_DIR="<reference_dir>" && export WEDATA_WORKSPACE_FOLDER="<workspace_folder>" && python3 "$KANBAN_REFERENCE_DIR/prefetch_table.py" --tables "<用户原始输入>" 2>&1
# 或(DataBuddy / WorkBuddy plugin 根目录):
export CODEBUDDY_PLUGIN_ROOT="<plugin_root>" && export WEDATA_WORKSPACE_FOLDER="<workspace_folder>" && \
_REF=""; for sub in "scenarios/data-analysis/skills/intelligent-kanban/reference" "l3-skill-scenario/intelligent-kanban/reference" "intelligent-kanban/reference"; do \
[ -d "$CODEBUDDY_PLUGIN_ROOT/$sub" ] && _REF="$CODEBUDDY_PLUGIN_ROOT/$sub" && break; \
done; \
[ -n "$_REF" ] || { echo "❌ 未在 CODEBUDDY_PLUGIN_ROOT 下找到 intelligent-kanban/reference 目录" >&2; exit 1; }; \
python3 "$_REF/prefetch_table.py" --tables "<用户原始输入>" 2>&1
<reference_dir>= 当前 skill 目录同级的reference/绝对路径(包含prefetch_table.py/kanban_runner.py/kanban_dsl.py的目录);<plugin_root>= DataBuddy 沙箱下 plugin 根目录。两个参数按当前环境实际值填入后原样执行,不要探查 env。
三种返回处理(按脚本退出码 + stdout 内容分发):
| 退出码 | stdout | LLM 行为 |
|---|---|---|
| 0 | prefetch_table.py 输出的 schema JSON |
进入 Step C:每张表给 3 行简报 → 写 spec |
| 2 | {"status":"need_disambiguation","ambiguous":[{"query":"...","candidates":[...]}],"resolved":["catalog.db.table",...]?} |
直接 stop:先(若有 resolved)一行告知"已锁定: <三段式列表>",再把每个 query 的候选原样展示给用户(每个子意图一组,列出 full_name + comment),等用户选;用户回复后把已锁定 + 用户选定一起重跑同一条命令。禁止自行换关键词再跑一次 prefetch |
| 3 | {"status":"not_found","missing":["..."],"resolved":["catalog.db.table",...]?} |
直接 stop:先(若有 resolved)一行告知"已识别: <三段式列表>",再 未找到 "<query1>" / "<query2>",请确认或直接给 catalog.db.table。严禁自行翻译/扩词/猜表名再跑 prefetch(如把"零售"换成 retail/sales/order 试探) |
| 其它 | stderr 第一行 | stop 透传给用户 |
🧭 消歧回复机械处理:用户回复序号/表名后,只做映射并重跑上述
prefetch_table.py;禁止在重跑前按候选表的connection_type、catalog 前缀、comment里出现的 "PostgreSQL/MySQL/OLAP" 等字样或经验白名单判断能否创建看板。唯一判据是重跑后脚本的退出码与route_hint(P0-16)。📌
resolved字段:多意图部分已锚定、其他仍歧义/缺失时,脚本仍按"一票否决"返 exit 2/3,但通过resolved透出已锚定的三段式。LLM 展示给用户(避免重复),用户补全后一起用作下一次--tables输入。
🛡️
route_hint路由(必看):
sql_type=1(lakehouse: SPARK/DLC):LLM 无感知,直走 DSLsql_type=3(OLAP: MySQL/PG/GaussDB/StarRocks/Doris):遵 P0-15,所有 chart 走raw_sql且用目标方言跨源混用由 prefetch 硬闸门拦截(exit 2 提示拆分)。
🛑 硬约束:
- 唯一找表/拉 schema/预取入口 = 上述
prefetch_table.py;禁裸调wedatacli search、禁绕道脚本、禁cat/ls/echo $CODEBUDDY_PLUGIN_ROOT探查 - 失败只读 stderr 首个完整错误块(含
[DSL]/[Runner]/[OLAP sqlSlots 硬拦截]等 tag 前缀后的所有修法行)→ 按提示重跑同一条命令(不换工具、不拆调用) - 多表/多意图必须一条命令搞定(拆调用让缓存 miss)
- 🚫 P0-13 单次原则:每轮最多一次 prefetch;exit 2/3 一律 stop 等用户。反例:
--tables "零售"→exit 3 后换为"retail"/"sales,order,..."/"product"重跑都不允许(每多一次调用浪费 4–9s + 冗余后台 fork) - 🚧 必输出门槛(exit=0 后强制,缺任一项即视为 Step B 未完成、禁止进入 Step C):
- 打印
**Step B · 数据分析**(英文:**Step B · Data Analysis**)标题;对外仅出现「数据分析」或「Data Analysis」之一,禁 Schema/prefetch 等内部术语 - 紧跟以对照表「数据概览」开头 + 一张 Markdown 表(每表一行,表头取对照表);各列规范:
- 数据源列:完整三段式(如
DataLakeCatalog.default.tiny_orders),禁<br/>换行 - 时间字段列:
<col> · <type>;无时间列填对照表「无」对应词 - 维度列:离散列
col:type,,分隔,≤4 个(优先类目/地域/状态/ID) - 指标列:数值列
col:type,≤4 个(优先金额/数量/比率);无数值列用count(<主键>)兜底
- 数据源列:完整三段式(如
- 打印
- ✅
Spec.source.columns必须列与类型双完整:name逐列从 prefetch 返回原样抄入(禁只抄用得上的字段,禁改列名,禁漏列——列缺失会触发 DuckDB sniffer fallback 至 VARCHAR 保底,SUM(数值列)报sum(VARCHAR)binder error);type保持原始表达式(如decimal(20,2)、bigint、string,禁自行简化为decimal/int)。选定time_col/time_type(仅填 string/date/timestamp/unix;schema 里的datetime在time_type中按 timestamp 填);若时间列columns[].type是date/timestamp/datetime,保持真实类型,交给time_dim/compare自动回填,禁为绕过本地体检改假string - ❌ 禁罗列整张 schema 表(那是
Spec.source.columns的事);❌ 禁"prefetch 命令已完成 → 直接 Step C"——schema JSON 拿到后必须先渲染「数据概览」表 - 🎯 用户诉求锁定回执(数据概览表下方贴 1 行,Step C 之前最后一句,不换行不解释):
🎯 主诉求=<用户原句核心动词短语,≤12字> · 主图型=<line|bar|pie|scatter|radar|funnel|gauge|heatmap|candlestick|table 中选 1>;用户点名具体字段/指标时同行追加· 必用字段=<列表>;表 schema 缺该字段时追加· 字段回退=<用户词>→<实际列名>(chart title 保留用户原词,SQL 用实际列,禁SELECT AS <用户词>造字段幻觉)。主图型判定(无命中默认bar;多命中按下表从上到下取首个):趋势/走势/变化/每月/月度/时序/trend → line·漏斗/流失/转化/环节 → funnel·散点/相关性/xx与xx的关系 → scatter·达成/达标/完成率/进度 → gauge·K线/波动/箱线/分位 → candlestick|boxplot·热力/交叉分布 → heatmap·占比/份额/构成 → pie|treemap·多维对比/雷达 → radar·列一下/名单/明细 → table·排行/最高/Top → bar。回执落地后 Step C 表必满足:① 至少 1 张图kind= 主图型;② 必用字段每一个都要出现在某张图的metrics/dims列 - 分析空间由
~/.wedata/config.json:analysisSpaceKey固化注入,禁自行export TENCENTCLOUD_ANALYSIS_SPACE_KEY或在报错里建议用户改空间
⚠️ Spec 不要手写
CAST/TRY_CAST兜底 'null' 字面量——runner 已在_open_duck用read_csv(columns=..., nullstr=['null','NULL','None','\N',''])统一处理。
⚠️ GROUP BY 维度 NULL 自动过滤:runner 对走 GROUP BY 的图表自动追加
<dim_sql_expr> IS NOT NULL,消除时间列解析失败/分类 NULL/LAG 假同环比/多列 GROUP BY 污染四类静默失真。逃生口:Dim(expr=..., description='__keep_null__')显式保留 NULL 桶。
Step C · 设计看板布局
🔫 内化检查(脑内执行,只输出布局摘要):写表前选型查 kanban_spec_example.py 第 5 节「选型决策表」(A~Q 17 行);metric 字段必须能在 Step B 「关键指标/维度」中找到,对账失败直接换 kind。
🎯 聚焦收敛(脑内自检,防"1 诉求→8 张图"发散):按 Step B 回执行 主诉求 / 主图型 分档控图数:
- 单诉求(用户只提 1 个业务问题:趋势/排行/达成/明细/分布/漏斗):总图数 ≤ 5(1
2 张主图 + 12 张辅图 + 1 组 KPI),主图型占比 ≥ 40% - 双诉求("X 和 Y"/"看 X 也看 Y"):总图数 ≤ 7,每条诉求至少 1 张对应主图型
- 复合看板(用户原句含
看板/大盘/驾驶舱/多维/全域/综合/dashboard):本条不生效,按用户期望展开 - 反模式:主图型未在回执里出现时不主动加
radar/heatmap/candlestick/boxplot/gauge;不把趋势拆成饼图+雷达、排行拆成多张明细表
🔗 回执一致性(Step C 表格落地前脑内做,NO 则改表,不 stop 不重跑 prefetch):主图型在 kind 列出现 ≥ 1 次;必用字段每一个都在 dims/metrics 列出现(缺字段走回执行的字段回退);若发生字段回退,Step E 一句话总结加一行 用户词=<X> · 实际列名=<Y>(表内无 <X>,用同义列 <Y>,如需精确对齐请提供含 <X> 的表)
🧠 准入题清单(脑内自检,DSL 已 raise 的硬契约不重复列,仅列 DSL 不 raise 的业务语义题;任一 NO 即换 kind,不输出给用户):
| 场景 | 必答准入题 |
|---|---|
radar |
能列出 ≥3 个业务有意义的 normalize='max-norm' 数值轴? |
funnel |
同一表能拆出 ≥2 个语义递减的阶段?首阶段是转化起点全量基线(不带 where 预筛),各阶段计量口径一致? |
radar/heatmap 分组基数 |
分组维度实际 NDV ≥ 3?否则改 bar/table(低于 3 无对比意义或单元格退化) |
| lakehouse 时间趋势/同环比 | source.columns 已带真实 date/timestamp/datetime 类型?是则用 DSL time_dim/compare,不要因本地体检经验改 raw_sql |
时间维 order_by |
引用 time_dim(col, gran) 时用 <col>_<gran>(如 time_day)而非裸 <gran>? |
scatter order_by |
散点实际输出别名固定为 x/y/category;排序写 order_by='-y',不要写 SUM(...) 或 metric alias |
line 时间趋势内构 |
主图 line 时优先双轴/多序列同图(如销售额+订单数双线),别拆成两张独立折线;聚焦少量核心指标 |
table 明细内构 |
主图 table 时 dims 必须含实体主键/业务 ID(如 customer_id/order_id),禁只按地域/类目/时间等汇总维度分组代替明细 |
其它陷阱(boxplot 聚合 / scatter 裸分类轴 / candlestick 一字 K / parallel 行级混轴 / dim 含聚合 / scatter limit 无 order_by 等)DSL 构造期已 raise,直接按报错文本改 spec。
🛑 输出契约:唯一交付物 = **Step C · 布局摘要**(英文:**Step C · Layout**)标题 + 6 列表格(# / kind / title / dims / metrics / span),列全所有图,在 Step D 之前输出。禁:一句话概述代替表格;输出对账过程或 ✅ 短语。
输出模板(dims/metrics 列写自然语言简写,严格 DSL 在 Step D):
**Step C · 布局摘要**(共 N 张图,覆盖 M 种 chart_type)
- **KPI**:总额 / 用户数 / 转化率 / 毛利率
- **图表**:
| # | kind | title | dims | metrics | span |
|---|----------|------------------|-----------------------|-------------------------------|------|
| 1 | line | 日趋势 | time(day) | SUM(sales) | 4 |
| 2 | bar | TopN 类目 | category | SUM(sales) ↓ Top10 | 2 |
| 3 | pie | 类目占比 | category | SUM(sales) | 2 |
| 4 | radar | 类目多维 | category | 4× normalize 数值轴 | 2 |
| 5 | gauge | 毛利率 | — | 派生比率 (with target) | 2 |
| 6 | compare | 月度环比 | time(month) | SUM(sales) · mom | 2 |
| 7 | table | 类目月销 | category, time(month) | SUM(sales) ↓ | 4 |
输出完表立即进 Step D,不要额外解释/贴 spec/贴 SQL。
可用 kind 16 种(参考,不要在 Step C 输出里复述):line / bar / pie / scatter / radar / funnel / gauge / heatmap / candlestick / treemap / sankey / boxplot / sunburst / graph / parallel / table
Step D · 生成看板
本步只负责「写 spec → 跑 runner → 终端输出 🎉 看板创建完成并已写入 PREVIEW」,看到追问语后立即进 Step E;若输出 ❌ 看板本地产物已生成,但 PREVIEW 入库失败 则按错误修复并重跑,不进入 Step E。
🚨 runner Traceback 自愈回归(仅当 runner 未输出 🎉 看板创建完成并已写入 PREVIEW 且抛出 Traceback 中断时触发;每类最多重跑 1 次 build_kanban;禁死循环 / 禁 stop 追问 / 禁裸调 wedatacli 探数 / 禁重跑 prefetch):
ValueError: [SQL 体检失败]→ 按告警文本"错误"行改 spec(① 若是sum(VARCHAR)/ 列名/别名错,检查Spec.source.columns是否与 prefetch 返回列名/类型/列数完全一致;② 若是 Spark 独家函数报错,改用raw_sql(..., escape_hatch=True)),重跑一次ValueError: [Runner] <kind> 需要 dims/metrics ...→ 按报错要求补齐 dims/metrics 或换 kind,重跑一次LATERAL_COLUMN_ALIAS_IN_WINDOW/UNRESOLVED_COLUMN.WITH_SUGGESTION(Spark 兼容)→ SELECT 别名下移到 WITH/CTE 或把 LAG(...) 内表达式原样重写,重跑一次❌ [本轮 string 时间列解析失败清单]失败率 ≥ 60%(且导致 raise)→ ① 换Spec.source.columns里真date/timestamp列做time_col;② 或该列外包spark_safe_to_timestamp(col)走raw_sql;schema 无真日期列且用户要月/日粒度时用现有列(如order_year || '-Q' || order_quarter、substr(dt,1,6))构造时间轴,chart title 保留用户原词- 重跑仍报同类 → 原样透传给用户后 stop,禁反复重试
✅ runner 已输出
🎉 看板创建完成并已写入 PREVIEW时不重跑:即使伴随⚠️ 数据语义校验告警/❌ ERROR: 共 N 个核心图表数据为空/WARN: <slot> 仅含表头无数据行等软告警,均为本地 sample 体检提示(prefetch sample 可能天然为空),远端 PREVIEW 已入库、用户拿到可用看板,agent 直接进 Step E,把告警原样透传给用户(Step E 一句话总结加一行"本地体检提示 N 项软告警:xxx;远端 PREVIEW 已入库可预览"),禁自作主张删图或改 kind
🛑 输出契约:必须先打 **Step D · 生成看板**(英文:**Step D · Build**)标题;下方仅允许 ① 执行命令 ② runner 原始输出。禁提及任何 <workspace_folder>/.kanban_output/ 下的本地文件名或路径(包括但不限于 kanban_spec.py / kanban_save_params.json / kanban_dsl.json / 快照 CSV),也不要输出“Spec 文件已创建”/“Spec created”之类提示(产物就是看板链接本身,中间产物不展示)。禁末尾追加总结/验收文字。
唯一允许的 LLM 产物模板(文件名固定 kanban_spec.py,位置固定 <workspace_folder>/.kanban_output/kanban_spec.py,Step F 覆盖同名以保幂等):
# spec 落在 <workspace_folder>/.kanban_output/kanban_spec.py;5 件套产物同目录(含 kanban_dsl.json DSL 描述层)。
import os, sys
# reference 目录多档解析:显式目录 > plugin 根下新版、旧版、WorkBuddy 布局
_REF = os.environ.get('KANBAN_REFERENCE_DIR', '').strip()
if not _REF or not os.path.isdir(_REF):
_PLUGIN_ROOT = os.environ.get('CODEBUDDY_PLUGIN_ROOT', '')
for _cand in (
os.path.join(_PLUGIN_ROOT, 'scenarios', 'data-analysis', 'skills', 'intelligent-kanban', 'reference'),
os.path.join(_PLUGIN_ROOT, 'l3-skill-scenario', 'intelligent-kanban', 'reference'),
os.path.join(_PLUGIN_ROOT, 'intelligent-kanban', 'reference'),
):
if _PLUGIN_ROOT and os.path.isdir(_cand):
_REF = _cand
break
if not _REF or not os.path.isdir(_REF):
raise SystemExit('❌ 无法定位 intelligent-kanban/reference 目录;请设置 KANBAN_REFERENCE_DIR 或 CODEBUDDY_PLUGIN_ROOT')
sys.path.insert(0, _REF)
from kanban_dsl import (
Spec, source, kpi, metric, chart, compare, raw_sql,
Dim, dim, time_dim, Metric,
)
from kanban_runner import build_kanban
SPEC = Spec(
title='🏢 XXX 驾驶舱',
# ✅ 省略 workspace_id(wedatacli 自动注入),禁自行查 env / 目录
source=source(
table='catalog.db.table',
columns=[{'name':'time','type':'string'}, {'name':'amount','type':'double'}],
time_col='time', time_type='string',
),
kpis=[
kpi('SUM(amount)', '总额', prefix='¥', format=',.0f'),
kpi('COUNT(DISTINCT user_id)', '用户数', format=',.0f'),
# 跨表 KPI(主表无该字段)传 from_sql 即可,runner 合并为子查询一次性执行:
# kpi('SUM(oi.price)', '商品总额',
# from_sql='cat.db.orders o JOIN cat.db.items oi ON o.id=oi.oid',
# prefix='¥', format=',.0f'),
],
charts=[
chart('line', '日趋势',
dims=[time_dim('time','day')],
metrics=['SUM(amount)'],
emoji='📈', span=4),
chart('bar', 'TopN 类目',
dims=['category'], metrics=['SUM(amount)'],
order_by='-SUM(amount)', limit=10, emoji='📊', span=2),
chart('radar', '类目多维',
dims=['category'],
metrics=[
metric('SUM(amount)', label='金额', normalize='max-norm'),
metric('COUNT(*)', label='单数', normalize='max-norm'),
metric('AVG(amount)', label='均值', normalize='max-norm'),
],
span=2, emoji='🎯'),
chart('gauge', '完成率',
metrics=[
metric('SUM(done)*100.0/NULLIF(SUM(total),0)',
label='完成率', format='.1f', suffix='%', target=100.0),
],
span=2, emoji='⏱️'),
compare(title='月度环比',
dim=time_dim('time','month'),
metric=metric('SUM(amount)', label='金额'),
kinds=['mom'], span=3, emoji='📊'),
chart('table', '类目月销榜',
dims=['category', time_dim('time','month')],
metrics=[metric('SUM(amount)', label='金额')],
order_by='-SUM(amount)', span=4, emoji='📋'),
],
theme='retail',
)
if __name__ == '__main__':
build_kanban(SPEC)
执行(spec 写盘后允许且仅允许执行下面一条命令,任选其一):
# 优先:显式指定 reference 目录
export KANBAN_REFERENCE_DIR="<reference_dir>" && export WEDATA_WORKSPACE_FOLDER="<workspace_folder>" && export KANBAN_OUTPUT_DIR="<workspace_folder>/.kanban_output" && mkdir -p "<workspace_folder>/.kanban_output" && ln -sf /bin/bash /bin/sh 2>/dev/null; python3 "<workspace_folder>/.kanban_output/kanban_spec.py" 2>&1
# 或(DataBuddy 沙箱默认目录结构)
export CODEBUDDY_PLUGIN_ROOT="<plugin_root>" && export WEDATA_WORKSPACE_FOLDER="<workspace_folder>" && export KANBAN_OUTPUT_DIR="<workspace_folder>/.kanban_output" && mkdir -p "<workspace_folder>/.kanban_output" && ln -sf /bin/bash /bin/sh 2>/dev/null; python3 "<workspace_folder>/.kanban_output/kanban_spec.py" 2>&1
📌 路径说明:spec 与产物同根落在
<workspace_folder>/.kanban_output/下(已依赖 session 目录,WorkBuddy / DataBuddy 两端统一)。spec 文件路径固定<workspace_folder>/.kanban_output/kanban_spec.py,无需cd切换目录。kanban_dsl.json是本地完整排障态(含Datasets);PREVIEW/发布入库请求源是kanban_save_params.json(HtmlContent为不含Datasets的页面/组件 DSL,SqlSlots为已 Base64 的 lowerCamelCase Dataset 数组)。
🛑 执行前/失败后原样重跑上述命令,禁前置探查/试导入/pip install/查 env(runner 已内置 _ensure_duckdb() 三级兜底,用户视角只看「runner 执行 → 🎉 看板创建完成并已写入 PREVIEW」)。
失败处理:读 stderr 首个完整错误块(含 [DSL] / [Runner] / ❌ [OLAP sqlSlots ...] / [本轮 string 时间列解析失败清单] 等 tag 前缀后的所有修法行)→ 按提示改 spec → 重跑同一条命令;禁改 reference/,禁绕道写新脚本。
Step E · 验收与总结
PREVIEW 保存/更新完成后输出唯一一段概览(标题取对照表「看板内容概览」对应词)即结束,等用户输入。
🛑 输出契约:
- ✅ 顶层标题取对照表 Step E 行;下方一段精炼描述 + 图表列表,回答「展示什么 / 帮看清什么 / 同环比口径」(EN 时对应
what to show / what to see / compare basis) - ✅ 看板链接必贴:runner 输出里含
→ https://.../dashboard/aiBoard/<AccessKey>...一行时,必须将该链接原样贴入本段概览末尾(Markdown 链接形式,标题取「打开看板」/Open dashboard),供用户点击跳转 DataBuddy 控制台查看。runner 未打印链接(例如缺 workspaceId/regionId)时,退化为一行「AccessKey:<AccessKey>」告知用户 - ✅ 若 runner 输出含
🔄 [自动降级]/⚠️ [软告警]行 → 原样抄入小节,小节名取对照表「数据特征提示」对应词,禁解释/翻译/扩写 - ❌ 禁输出 ✅ 自检/验收确认/「Spec 构造无异常」「charts=N 种」等内部校验项
- ❌ 禁在正文提及任何
<workspace_folder>/.kanban_output/下的本地文件名或路径(kanban_spec.py/kanban_save_params.json/kanban_dsl.json/ 快照 CSV 等)。产物对用户而言只有看板链接,中间产物属于内部实现不暴露 - ❌ 禁主动调
SaveAiKanBan·save_to_kanban_list发布态;runner 已自动调UpdateAiKanBan保存/更新 PREVIEW 并 print 追问语,不要重复
Step F · 修改(任何变更指令统一走这里)
用户任何修改类指令(颜色/标题/布局/换 kind/增删组件/调 KPI/换风格等)唯一流程:改 spec → 重跑 build_kanban(SPEC) → runner 自动保存/更新 PREVIEW + print 发布追问语 → stop 等用户。
强约束:
- 🛑 复用已有
AccessKey:spec 不传任何 file_id 即可(runner 自动读kanban_save_params.json里的 AccessKey 走 UpdateAiKanBan 覆盖) - 🛑 禁手改
kanban_save_params.json/kanban_dsl.json,必须由 runner 产生;入库以UpdateAiKanBan.SqlSlots的Datasets[]lowerCamelCase 字段为准,HtmlContent只承载页面/组件 DSL - 🛑 runner 已自动保存/更新后端 PREVIEW;禁本轮主动覆盖发布态,等用户回复「保存/发布/上线/确认发布」再走 G1
Step G · 用户发布 / 预览保存指令处理
仅在 Pre-routing 命中 P0 后进入。本轮输出严格遵守 Pre-routing “三件事 + 禁止清单”。
G1. 发布到 AI 看板列表 · 触发:保存 / 发布 / 上线 / 确认发布 等
import os, sys
_REF = os.environ.get('KANBAN_REFERENCE_DIR', '').strip()
if not _REF or not os.path.isdir(_REF):
_PLUGIN_ROOT = os.environ.get('CODEBUDDY_PLUGIN_ROOT', '') or os.getcwd()
for _cand in (
os.path.join(_PLUGIN_ROOT, 'scenarios', 'data-analysis', 'skills', 'intelligent-kanban', 'reference'),
os.path.join(_PLUGIN_ROOT, 'l3-skill-scenario', 'intelligent-kanban', 'reference'),
os.path.join(_PLUGIN_ROOT, 'intelligent-kanban', 'reference'),
os.path.join(os.getcwd(), 'reference'),
):
if os.path.isdir(_cand):
_REF = _cand
break
if not _REF or not os.path.isdir(_REF):
raise SystemExit('❌ 无法定位 intelligent-kanban/reference 目录;请设置 KANBAN_REFERENCE_DIR 或 CODEBUDDY_PLUGIN_ROOT')
sys.path.insert(0, _REF)
from kanban_runner import _B # builder helper 命名空间
result = _B['save_to_kanban_list']() # 入参可省略,默认从 <workspace_folder>/.kanban_output 推断;只调 SaveAiKanBan
if result['status'] == 'success':
if result.get('dashboard_url'):
print(f'✅ 已发布:AccessKey={result["access_key"]} → {result["dashboard_url"]}')
else:
print(f'✅ 已发布:AccessKey={result["access_key"]}')
else:
print(f'❌ 失败:{result["error"]}')
输出后立即 stop。
G2. 保存 / 更新 PREVIEW · 触发:保存预览 / 更新预览 / 同步预览 / 推送预览 等
特别提醒:不要重跑
build_kanban(SPEC)——上一轮 Step D/F 已生成最新kanban_save_params.json;本 helper 只调UpdateAiKanBan,不会覆盖发布态。
import os, sys
_REF = os.environ.get('KANBAN_REFERENCE_DIR', '').strip()
if not _REF or not os.path.isdir(_REF):
_PLUGIN_ROOT = os.environ.get('CODEBUDDY_PLUGIN_ROOT', '') or os.getcwd()
for _cand in (
os.path.join(_PLUGIN_ROOT, 'scenarios', 'data-analysis', 'skills', 'intelligent-kanban', 'reference'),
os.path.join(_PLUGIN_ROOT, 'l3-skill-scenario', 'intelligent-kanban', 'reference'),
os.path.join(_PLUGIN_ROOT, 'intelligent-kanban', 'reference'),
os.path.join(os.getcwd(), 'reference'),
):
if os.path.isdir(_cand):
_REF = _cand
break
if not _REF or not os.path.isdir(_REF):
raise SystemExit('❌ 无法定位 intelligent-kanban/reference 目录;请设置 KANBAN_REFERENCE_DIR 或 CODEBUDDY_PLUGIN_ROOT')
sys.path.insert(0, _REF)
from kanban_runner import _B # builder helper 命名空间
result = _B['update_to_kanban_list']() # 入参可省略,默认从 <workspace_folder>/.kanban_output 推断;只调 UpdateAiKanBan
if result['status'] == 'success':
if result.get('dashboard_url'):
print(f'✅ 已保存预览:AccessKey={result["access_key"]} → {result["dashboard_url"]}')
else:
print(f'✅ 已保存预览:AccessKey={result["access_key"]}')
else:
print(f'❌ 失败:{result["error"]}')
输出后立即 stop。
DSL API 速查(L3 颗粒度)
# 数据源
source(table=..., columns=[...], time_col=..., time_type=..., where=..., limit=10_000)
# 维度
dim('product_type') # 裸列名
dim('CASE WHEN p<20 THEN "L" ELSE "H" END', alias='price_tier') # SQL 表达式
time_dim('time','month') # 时间分桶;裸列类型从 source.columns 自动回填
# ⚠️ time_dim 输出别名 = `<col>_<gran>`(如 `time_day`)不是 `<gran>`;order_by 写 `'-time_day'`,禁 `'-day'`
# 度量
metric('SUM(sales)', label='销售额')
metric('SUM(a)/NULLIF(SUM(b),0)*100', label='转化率', format='.1f', suffix='%') # 派生比率
metric('MIN(price)', role='low') # K 线角色
metric('SUM(x)', normalize='max-norm', label='X') # 雷达归一化
# string 时间列:所有时间提取/比较必包 spark_safe_to_timestamp(col),禁 TO_TIMESTAMP
metric('AVG(DATEDIFF(spark_safe_to_timestamp(end), spark_safe_to_timestamp(start)))', label='平均时长')
# KPI
kpi('SUM(amount)', '总额', prefix='¥', format=',.0f')
kpi('COUNT(DISTINCT id)', '用户数', format=',.0f')
# 图表(16 类共用 dims+metrics)
chart(kind, title, # title 不写 emoji,emoji 只走 emoji= 参数
dims=[...], metrics=[...],
span=2, emoji='', order_by='-SUM(x)|alias|-1',
limit=10, stacked=False, dual_axis=[1], smooth=True,
extras={...}) # 透传 echarts
# ↑ chart() 不接受 from_sql=/sql=;多表 JOIN / 手写 SELECT 请改用下面的 raw_sql()
# 逃生口(OLAP sql_type=3 必用;lakehouse 复杂 SQL 可用)
raw_sql(title='标题', kind='line',
sql='SELECT month, SUM(amt) AS amt FROM <按 P0-3 写表名> GROUP BY month ORDER BY month',
slot_columns=['month','amt'], # 顺序必须与 SELECT 严格一致
span=4, emoji='📈')
# 同环比
compare(title='...', dim=time_dim(...),
metric=metric('SUM(...)', label='...'), kinds=['mom'|'yoy'|'wow'])
常见图表数据约定(L3 统一抽象)
以下列出的 dims/metrics 数量、角色、表达式限制 均为
kanban_dsl.py里Chart._CONTRACTS的硬契约: 违反会在构造Chart(...)那一刻报[DSL] ...,不会拖到 runner / SQL / 前端。
| 图表 | dims | metrics | 硬契约(写错即报) |
|---|---|---|---|
kpi |
— | Spec.kpis=[kpi(expr,label,...)] |
label 必填;expr 必须含聚合或为常量;跨表/JOIN 聚合传 from_sql='cat.db.t1 a JOIN cat.db.t2 b ON ...' |
line/bar |
dims[0]=分组(+可选 group) | n 度量 → n 系列 | dims ∈ [1,2];metrics ≥ 1 |
pie |
dims[0]=分组 | 单度量 | dims ∈ [1,1];metrics ∈ [1,1] |
scatter |
dims[0]=x,dims[1?]=color | metrics[0]=y | dims ∈ [1,2];metrics ∈ [1,1];行级 limit 必配 order_by;dims[0] 禁裸分类 |
radar |
dims[0]=分组 | ≥3 个度量,label=轴名,normalize='max-norm' | dims ∈ [1,1];metrics ≥ 3 |
funnel |
— | 每度量一个阶段,label=阶段名 | dims = 0;metrics ≥ 2 |
gauge |
— | metrics[0],用 metric.target 设上限 | dims = 0;metrics ∈ [1,1];metric.target 必填(比率型给 100.0 / 绝对值给用户目标数) |
heatmap |
dims[0]=x, dims[1]=y | metrics[0]=值 | dims ∈ [2,2];metrics ∈ [1,1] |
candlestick |
dims[0]=时间轴 | 4 度量 role='open/close/low/high' | dims ∈ [1,1];metrics = 4 且四个角色齐备 |
treemap/sunburst |
dims=路径(多级) | metrics[0]=值 | dims ≥ 1;metrics ∈ [1,1] |
sankey |
dims[0,1]=source/target | metrics[0]=权重 | dims ∈ [2,2];metrics ∈ [1,1] |
boxplot |
dims[0]=分组 | metrics[0]=行级数值表达式 | dims ∈ [1,1];metrics[0].expr 不能含 SUM/AVG/COUNT/MIN/MAX(runner 内部已做 percentile_approx) |
graph |
dims=[节点] 或 [source,target] | 边模式 metrics[0] 必填 | dims ∈ [1,2];metrics ∈ [0,1];边模式(dims=2)必配 limit |
parallel |
dims[0..-2]=轴, dims[-1]=分类 | — | dims ≥ 3;metrics = 0;前 N-1 维必聚合 或 配 limit(禁行级表达式) |
table |
dims=列序 | metrics 可选聚合列 | dims ≥ 1;显式 limit 必配 order_by(无聚合时 runner 自动 LIMIT 50) |
⚠️ 常见误用
👉 22 类反例代码集中在 kanban_spec_pitfalls.py if False: 区块(文件头按报错关键字索引)——DSL raise 文本已含修法,多数无需查阅,仅在 stderr 出现 [DSL] ... 时按需定位。
🛡️ 兜底 / 关键文件
DSL + runner + builder 三层已拦 40+ 类陷阱。LLM 只需:[DSL] ... 按报错改 spec;⚠️ [Runner][软告警] 忽略;🔄 [自动降级] 原样抄入 Step E。
- kanban_spec_example.py 必读:正例骨架(16 类 kind + 多表 7.5 节)
- kanban_spec_pitfalls.py 按需:
[DSL] ...时按文件头索引 grep - reference/kanban_dsl.py 只读 DSL API;看板创建/修改流程不读不改其余 reference(维护 skill 时以 reference 为唯一真源)
🧬 DSL 描述层(第一版协议 · 三端统一入库源)
LLM 无需感知:DSL 生成、落盘、入库覆盖全部在 runner 内部完成,spec 形态零变化,看板创建/修改流程不需读写
kanban_dsl.json。此章节仅作维护/排障参考。
产物:<workspace_folder>/.kanban_output/kanban_dsl.json(Meta + UiSettings + Pages[Widgets/PageLayout] + Datasets,本地排障完整态),生成时刻 Meta.Status='DRAFT' / Meta.KanbanVersion=1。
入库映射(三端 studio / databuddy / ai 看板列表统一使用):
UpdateAiKanBan.HtmlContent= base64(不含Datasets的页面/组件 DSL)(不再是 HTML 原文),写入后端 PREVIEWUpdateAiKanBan.SqlSlots= base64(Datasets数组),是唯一 Dataset 入库源,写入后端 PREVIEWSaveAiKanBan发布入参只传WorkspaceId + AccessKey,后端从同AccessKey的 PREVIEW 同步HtmlContent/SqlSlots/DisplayName/ExecuteResourceId/RefreshSchedule/SessionTag到发布态Datasets[]内部字段是第一版 lowerCamelCase 协议:key/sql/metrics/dimensions/data/columns/refreshInterval/sqlType/dataSourceId/connectionType;生成端按服务端同口径在data总量超过 128KB 时整体剥离data- 覆盖动作由 emitter 内
_patch_save_params_with_dsl完成,kanban_save_params.json里 HtmlContent/SqlSlots 已是 PREVIEW 入库形态;runner 每次build_kanban成功后自动调用UpdateAiKanBan写后端 PREVIEW(无AccessKey时由后端创建 PREVIEW 并返回AccessKey)。
发布语义:Meta.Status='DRAFT' 只表示本地生成态;运行态以 GetAiKanBan.Status/ViewStatus/Version 为准。UpdateAiKanBan 只用于保存/更新 PREVIEW;用户明说「保存/发布/上线/确认发布」→ Step G1(save_to_kanban_list(),只调 SaveAiKanBan 且只提交 WorkspaceId + AccessKey)同步 PREVIEW 到发布态;用户明说「保存预览/更新预览/同步预览」→ Step G2(update_to_kanban_list(),只调 UpdateAiKanBan)不覆盖发布态。
排障:若终端未打印 📄 DSL 描述层已生成: .../kanban_dsl.json 或打印 ❌ 预览态同步失败 / ❌ 看板本地产物已生成,但 PREVIEW 入库失败 → runner 未成功写入 PREVIEW,会影响三端入库预览;按报错文本改 spec 重跑即可。
- reference/kanban_dsl_emitter.py 只读 DSL emitter 实现;触点仅两处:runner 编译循环里的
_dsl_widget_records收集 +write_kanban_outputs之后的emit_dsl调用。