draftgo-cli 2.0.5 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,675 +1,210 @@
1
- ---
1
+ ---
2
2
  name: draftgo-frontend-rules
3
3
  description: DraftGo frontend page development rules.
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  ---
6
6
 
7
7
  # DraftGo 前端开发规范
8
8
 
9
- ## 架构认知
10
-
11
- DraftGo 前端**不是传统 SPA**:
12
- - 平台壳层使用 React + Vite 编译,shadcn/ui + Tailwind CSS 已作为默认前端范式引入
13
- - 业务页面 HTML 存在数据库,运行在 `iframe.srcdoc`
14
- - 导航栏 HTML 存在数据库,由壳层按需加载
15
- - 页面通过 `window.parent.App` 调用平台能力
16
-
17
- ---
18
-
19
- ## 前端 UI 能力调用声明
20
-
21
- 平台前端开发默认采用 React + shadcn/ui + Tailwind CSS;新增壳层管理界面、组件、弹窗、表单、表格和设置页时,优先复用 `frontend/src/components/ui` 下的 shadcn 组件与 Tailwind token。若本地 Agent 环境存在前端 UI 相关 Skills,前端界面开发时优先调用;本文件同时规定 DraftGo 前端运行时、资源、数据、路由、入口绑定和验证方法。
22
-
23
- ---
24
-
25
- ## dg-* = shadcn(最高优先级)
26
-
27
- 在 DraftGo 中,`dg-*` 就意味着 shadcn。
28
-
29
- - `dg-*` 是 shadcn/ui 在数据库 HTML 页面机制里的协议化表达,不是一套自研 UI 库。
30
- - 数据库页面仍然写 HTML,不进入 Vite/React 编译链;因此不能把 TSX 版 shadcn 组件直接写进 `page.value.html`。
31
- - 需要 shadcn 组件能力时,优先写对应 `dg-*` 标签,例如 `dg-button`、`dg-card`、`dg-form`、`dg-table`、`dg-tabs`、`dg-dialog`、`dg-dropdown-menu`、`dg-sheet`。
32
- - 若 shadcn 有组件,DraftGo 必须有对应 `dg-*` 映射。当前 runtime 已按映射表提供基础协议覆盖;复杂组件在 iframe 中以 HTML runtime 方式复用 shadcn 语义,而不是直接运行 React/Radix 组件。
33
- - `dg-*` 的属性、状态、交互预期和 token 命名应跟随 shadcn:例如 `variant`、`size`、`disabled`、`default-value`、`data-state`、`aria-*`。
34
- - `dg-dialog`、`dg-sheet`、`dg-dropdown-menu`、`dg-popover`、`dg-tooltip` 等复杂组件使用 trigger/content/open-state 协议;`dg-table[source]` 支持基础数据拉取和 `data-dg-props.columns` 渲染。
35
-
36
- AI 开发时的判断句:**看到 `dg-*`,必须读作 shadcn;要用 shadcn,数据库页面就写 `dg-*`。**
37
-
38
- ---
39
-
40
- ## 运行时机制(Runtime)
41
-
42
- ### 壳层启动流程
43
-
44
- 壳层入口由 Vite/React 工程加载,当前兼容 runtime 仍通过 `frontend/src/core/runtime.js` 的 `createAppRuntime()` 完成以下初始化:
45
-
46
- 1. `reloadSystemConfig()` — 拉取 `/api/system/config`,填充 `state.config`
47
- 2. `validateToken()` — 验证 `localStorage.dg_access_token`,成功后设置 `currentUser`
48
- 3. `loadSetupStatus()` — 检查系统是否已初始化(`/api/system/setup/status`)
49
- 4. `loadPage()` — 根据当前路由拉取页面 HTML,注入 iframe
50
-
51
- ### iframe 注入机制
52
-
53
- 壳层通过 `decorateFrameHtml(html, routeContext, theme)` 处理数据库页面 HTML,注入:
54
- - `window.__DG_ROUTE_CONTEXT__` — 当前路由上下文(含 `query` 参数)
55
- - 主题 CSS 变量
56
- - 静态资源(数据库页面使用的本地 Tailwind runtime、FontAwesome、GSAP 等)
57
-
58
- 页面 HTML 以 `iframe.srcdoc` 方式渲染,**不是独立 URL**,因此:
59
- - `window.location` 指向壳层地址,不可用于读取路由参数
60
- - `window.parent.App` 是壳层暴露的能力对象
61
-
62
- ### App 对象来源
63
-
64
- 壳层将 `app` 对象赋值给 `window.App`(通过 `Object.assign(app, ...)`),页面内通过 `window.parent.App` 访问。
65
-
66
- `app` 对象包含以下模块(均已合并到顶层):
67
-
68
- | 属性/方法 | 来源模块 | 说明 |
69
- |---|---|---|
70
- | `get/post/put/patch/delete` | `api.js` | HTTP 请求,自动携带 token |
71
- | `uploadFile(file, onProgress)` | `api.js` | 文件上传 |
72
- | `toast(msg, type?, duration?)` | `feedback.js` | Toast 通知(type: success/error/warning/info,默认 info) |
73
- | `showSuccess(msg)` | `feedback.js` | 成功 Toast 3000ms ✓ |
74
- | `showError(msg)` | `feedback.js` | 错误 Toast 4000ms ✕ |
75
- | `showWarning(msg)` | `feedback.js` | 警告 Toast 3000ms ⚠ |
76
- | `showInfo(msg)` | `feedback.js` | 信息 Toast 3000ms ℹ |
77
- | `confirm(msg, title?)` | `feedback.js` | 确认弹窗,返回 Promise\<boolean\> |
78
- | `showModal(msg, title?)` | `feedback.js` | 信息模态框,带确定按钮 |
79
- | `showLoading() / hideLoading()` | `feedback.js` | 全局 loading 蒙层 |
80
- | `navigate(route)` | `runtime.js` | 路由跳转(pushState) |
81
- | `getCurrentRoute()` | `runtime.js` | 当前路径字符串 |
82
- | `getCurrentRouteContext()` | `runtime.js` | 完整路由上下文(含 query) |
83
- | `reloadGlobalLayer()` | `runtime.js` | 重载前端全局层 |
84
- | `openGlobalWidget(name)` / `closeGlobalWidget(name)` | `runtime.js` | 触发全局挂件打开/关闭事件 |
85
- | `logout()` | `runtime.js` | 登出并跳转登录页 |
86
- | `setAuthTokens({access_token, refresh_token})` | `runtime.js` | 登录后写入 token |
87
- | `applyTheme(theme)` | `runtime.js` | 切换主题(light/dark),同步写 localStorage + iframe |
88
- | `t(key, fallback?)` | `i18n.js` | 国际化文本 |
89
- | `currentUser` | state | 当前用户对象(未登录为 null) |
90
- | `isAdmin` | state | 是否管理员 |
91
- | `isAuthenticated` | state | 是否已认证 |
92
- | `hasToken` | state | 是否有 token(含未验证) |
93
- | `config` | state | 系统配置键值对 |
94
- | `theme` | state | 当前主题名(`'light'` \| `'dark'`) |
95
-
96
- ### Token 存储
97
-
98
- | key | 说明 |
99
- |---|---|
100
- | `localStorage.dg_access_token` | 访问 token |
101
- | `localStorage.dg_refresh_token` | 刷新 token |
102
-
103
- 登录成功后调用 `App.setAuthTokens({ access_token, refresh_token })` 写入并触发验证。
104
-
105
- ### 禁止使用浏览器默认弹窗(强制)
106
-
107
- **禁止**在页面中使用 `alert()`、`confirm()`、`prompt()`。它们会阻塞主线程、与平台交互协议不一致,且在 iframe 环境中行为不可预期。
108
-
109
- 使用 App 提供的替代方案:
110
-
111
- | 浏览器原生 | DraftGo 替代 | 说明 |
112
- |---|---|---|
113
- | `alert(msg)` | `App.showModal(msg, title?)` | 信息展示,带确定按钮 |
114
- | `confirm(msg)` | `await App.confirm(msg, title?)` | 二次确认,返回 `Promise<boolean>`,注意必须 `await` |
115
- | `prompt(msg)` | 自行构建输入弹窗 | 平台无内置 prompt,需在页面内自建 modal + input,并保持与当前页面和平台交互一致 |
116
-
117
- ```javascript
118
- // ❌ 禁止
119
- if (confirm('确认删除?')) { doDelete(); }
120
-
121
- // ✅ 正确
122
- const ok = await App.confirm('确认删除?', '删除确认');
123
- if (ok) { doDelete(); }
124
-
125
- // ❌ 禁止
126
- const name = prompt('请输入名称');
127
-
128
- // ✅ 正确:自建输入弹窗
129
- showInputModal('请输入名称', async (value) => {
130
- if (value) { await save(value); }
131
- });
132
- ```
133
-
134
- ### 前端全局层(强制)
135
-
136
- 全局浮窗、全局客服/反馈入口、统计监控脚本、Toast 二开、全站挂件等,属于前端全局层,不属于单个业务页面。全局层通过 `sys_config.category = frontend_global` 存储,并在页面管理的「全局」分类编辑固定槽位。
137
-
138
- 规则:
139
-
140
- - 不要在业务页面内重复实现全局浮窗、客服、反馈入口或统计脚本。
141
- - 不允许新增全局项;只能编辑系统内置固定槽位。
142
- - 如果不了解用途,不要修改全局层;它会影响所有页面。
143
- - Toast 调用协议保持稳定:继续用 `App.toast()` / `App.showSuccess()` / `App.showError()`;默认时长可通过全局项二开。
144
- - 保存全局层后调用 `App.reloadSystemConfig()` 或 `App.reloadGlobalLayer()` 让运行时立即重载。
145
-
146
- 常见槽位:
147
-
148
- | config_key | 用途 |
149
- |---|---|
150
- | `frontend_global_head_html` | 注入父级壳层 head |
151
- | `frontend_global_body_html` | 注入父级壳层 body 末尾 |
152
- | `frontend_global_css` / `frontend_global_js` | 父级壳层全局 CSS/JS |
153
- | `frontend_global_iframe_head_html` | 注入每个业务页面 iframe head |
154
- | `frontend_global_widget_html/css/js` | 全局挂件层 |
155
- | `frontend_global_toast_css/position/duration_*` | Toast 二开 |
156
-
157
- ### 全局事件
158
-
159
- | 事件名 | 触发时机 |
160
- |---|---|
161
- | `dg:auth-ready` | token 验证完成(成功或失败) |
162
- | `dg:auth-changed` | token 变更(登录/登出) |
163
-
164
- 页面内监听:`window.parent.addEventListener('dg:auth-ready', handler)`
9
+ > 本文件只规定前端**操作性规则**,已剥离的内容见:
10
+ > 架构原理 → `{{SKILL_DIR}}/core/architecture.md`
11
+ > App API / Token / 路由 / 运行时 → `{{SKILL_DIR}}/specs/runtime.md` · `{{SKILL_DIR}}/quickref/app-api.md`
12
+ > dg-* 映射 `{{SKILL_DIR}}/specs/ui-protocol.md` · `{{SKILL_DIR}}/quickref/dg-components.md`
13
+ > 动态 DB / filters → `{{SKILL_DIR}}/specs/data.md`
14
+ > 开发禁区 `{{SKILL_DIR}}/specs/security.md`
165
15
 
166
- ### API 请求路径解析规则
167
-
168
- `App.get/post/...` 的路径参数解析(来自 `api.js`):
169
-
170
- | 传入值 | 实际请求 |
171
- |---|---|
172
- | `'pages'` | `{apiBase}/api/pages` |
173
- | `'pages/123'` | `{apiBase}/api/pages/123` |
174
- | `'/api/pages'` | `{apiBase}/api/pages` |
175
- | `'https://...'` | 直接请求(绝对 URL) |
176
-
177
- ### GET 请求筛选范式
178
-
179
- **分页**(绝大多数列表接口):
180
-
181
- ```javascript
182
- App.get('users', { page: 1, page_size: 20 })
183
- App.get('pages/', { page: 1, page_size: 20 })
184
- ```
185
-
186
- > 分页参数统一使用 `page` 与 `page_size`。GET 列表请求若不携带这两个参数,则返回全量数据;当数据量超过 10000 条时后端会拒绝并返回 400,此时必须传入 `page` 与 `page_size`。
187
-
188
- **通用筛选参数**:
189
-
190
- | 参数 | 类型 | 说明 |
191
- |---|---|---|
192
- | `page` | int | 页码,从 1 开始 |
193
- | `page_size` | int | 每页条数 |
194
- | `search` | string | 全文搜索(users/roles/pages/navigations/feedback —— **注意:动态 DB 不用 search,见下方**) |
195
- | `status` | string | 状态过滤(`active`/`inactive`/`published` 等,按资源而定) |
196
- | `type` | string | 类型过滤(notices/feedback/aihub) |
197
- | `tag` | string | 标签过滤(pages/aihub) |
198
-
199
- **典型示例**:
200
-
201
- ```javascript
202
- // 用户列表,带搜索+状态+角色过滤
203
- App.get('users', { page: 1, page_size: 20, search: 'alice', status: 'active', role_id: 3 })
204
-
205
- // 页面列表,带标签+状态过滤
206
- App.get('pages/', { page: 1, page_size: 20, search: 'home', tag: 'blog', status: 'published' })
207
- ```
208
-
209
- ### 动态 DB(`/api/db/{type}`)检索范式
210
-
211
- 动态 DB 的检索**不用 `search` 参数**,改用 **`filters` 结构化条件**,直接对记录的 `data` JSON 字段做查询。
212
-
213
- ```javascript
214
- // 动态 DB 列表查询:filters 每项形如 "字段:操作符:值",可传多个(数组),多个为 AND
215
- App.get(`db/${type}`, {
216
- page: 1, page_size: 20,
217
- filters: ['title:like:公告', 'views:gte:100', 'status:eq:paid'],
218
- order_by: 'views', order: 'desc',
219
- })
220
-
221
- // 动态 DB 创建:业务字段必须放在 data 包裹里,不要平铺到 body 顶层
222
- App.post(`db/${type}`, { data: { name: '张三', age: 30 }, status: 1 })
223
-
224
- // 动态 DB 更新:同样用 data 包裹
225
- App.put(`db/${type}/${id}`, { data: { age: 31 } })
226
- ```
227
-
228
- **filters 操作符**(字段必须在 db_meta schema 里标 `searchable`,且操作符要匹配字段的检索模式):
229
-
230
- | 操作符 | 含义 | 适用 searchable 模式 |
231
- |---|---|---|
232
- | `eq` | 精确等于 | exact / fuzzy / range |
233
- | `like` | 模糊包含(`%`/`_` 会被自动转义,按字面匹配) | fuzzy |
234
- | `gte` / `lte` / `gt` / `lt` | 数值范围 | range |
235
- | `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy / range |
236
- | `contains` | 数组字段包含某值 | contains |
237
-
238
- - 省略操作符时(`filters: ['title:公告']`)默认 `like`。
239
- - 字段间互相隔离,按 `data.字段` 精确定位,不存在跨字段串扰;中文可正常检索。
240
- - `order_by` 只能用 searchable 字段;`order` 取 `asc`/`desc`,默认 `desc`。
241
- - 字段不可检索、或操作符与字段模式不匹配时,后端返回 400。
242
-
243
- **DB Meta 说明**:涉及动态数据库操作前,先读取 `db_meta/index.json` 了解当前项目有哪些 DB 类型(`type`)以及各字段的 `searchable` 模式,再调用 `/api/db/{type}`。不要硬编码 type 值,也不要对未标 searchable 的字段做 filters。
244
-
245
- ---
16
+ 若本地 Agent 存在前端 UI Skills,前端界面开发时优先调用。
246
17
 
247
18
  ---
248
19
 
249
- ## 页面资产开发
20
+ ## 页面开发强制规则
250
21
 
251
- ### 页面默认按完整页面功能处理(强制)
22
+ ### 真实闭环(强制)
252
23
 
253
- 用户说“做 / 新建 / 增加一个页面”时,默认不是静态稿,而是一个可被真实使用的页面功能:
254
- - 页面必须有可访问 route,且 route 与 page_id / 本地文件对应清楚。
255
- - 页面必须从真实入口可达:导航栏、首页模块、后台菜单、列表操作按钮或相关页面链接至少绑定一个。
256
- - 页面里的按钮、表单、搜索、筛选、分页、详情跳转、提交、保存、删除等交互默认要真实有效;只有用户明确说“静态 / 纯页面 / demo / mock / 假数据 / 伪功能 / 先看效果”时,才允许做静态。
257
- - 页面必须有加载态、空态、错误态、成功态,不能只堆静态卡片。
258
- - 若页面展示可维护内容(案例、新闻、产品、招聘、资料等),主动判断是否需要后台管理页和同一份真实数据;不要只写死 HTML 或前端数组。
259
- - 若 DraftGo 动态 DB、已有 db_meta、custom_scripts、外部 API、AIHub 等能力都无法完成真实闭环,不要写伪功能交差;停止开发,向用户说明阻塞点,并按 dev-workflow 写入 `.draftgo/lessons/`。
24
+ - 页面必须有可访问 route,从真实入口可达(导航/首页/后台菜单/相关按钮至少一处)
25
+ - 按钮、表单、搜索、筛选、分页、提交、删除等交互默认真实有效
26
+ - 必须有加载态/空态/错误态/成功态,不能只堆静态卡片
27
+ - 展示可维护内容时主动判断是否需要后台管理页 + 同一份真实数据
28
+ - 无法实现真实闭环时停止开发,向用户说明阻塞点,写入 `.draftgo/lessons/`
260
29
 
261
- ### 操作型页面空间利用(强制)
30
+ ### 操作型页面布局(强制)
262
31
 
263
- 后台管理、表格、列表、审批、配置、内容维护等操作型页面,优先按**工作台布局**处理,而不是普通内容文档流。先分配页面空间,再放组件:
264
- - 页面根容器应占满可用视口 / iframe 内容区,使用 `display:flex; flex-direction:column; min-height:100vh` 或等效结构。
265
- - 顶部筛选、搜索、批量入口、标题操作区保持稳定高度,不应随数据量漂移。
266
- - 主体数据区使用 `flex:1; min-height:0; overflow:auto` 承接剩余空间,数据少时保留工作区空白,不让页面高度随行数塌陷。
267
- - 分页、批量操作栏、保存栏、流程按钮等控制区应固定在当前工作区底部或保持稳定位置,不能跟随 1-2 条数据上浮。
268
- - 内部滚动优先发生在数据区 / 表格区,避免整个后台页面因为少量内容显得松散,也避免大量内容把操作区挤出视野。
269
-
270
- 参考结构:
32
+ 后台管理、表格、列表等操作型页面用**工作台布局**,不用文档流堆叠:
271
33
 
272
34
  ```css
273
- .workbench-page {
274
- min-height: 100vh;
275
- display: flex;
276
- flex-direction: column;
277
- overflow: hidden;
278
- }
279
- .workbench-toolbar {
280
- flex-shrink: 0;
281
- }
282
- .workbench-body {
283
- flex: 1;
284
- min-height: 0;
285
- display: flex;
286
- flex-direction: column;
287
- }
288
- .workbench-data {
289
- flex: 1;
290
- min-height: 0;
291
- overflow: auto;
292
- }
293
- .workbench-footer {
294
- flex-shrink: 0;
295
- }
35
+ .workbench-page { min-height:100vh; display:flex; flex-direction:column; overflow:hidden; }
36
+ .workbench-toolbar { flex-shrink:0; }
37
+ .workbench-body { flex:1; min-height:0; display:flex; flex-direction:column; }
38
+ .workbench-data { flex:1; min-height:0; overflow:auto; }
39
+ .workbench-footer { flex-shrink:0; }
296
40
  ```
297
41
 
298
- 不要让分页、保存栏、批量操作栏跟随少量数据上浮;不要用纯文档流堆叠后台管理页面。
42
+ - 分页/操作栏/保存栏固定在工作区底部,不随数据量上浮
43
+ - 滚动发生在数据区,不是整页
299
44
 
300
45
  ### 新增页面绑定(强制)
301
46
 
302
- 创建或新增页面后,必须继续处理“绑定”:
303
- - 公开前台页面:绑定到顶部/侧边导航、首页入口模块、相关列表/详情按钮之一。
304
- - 后台管理页面:绑定到后台导航、管理菜单或现有后台入口。
305
- - 多页面功能:列表、详情、新建、编辑、管理等页面之间必须互相能走通。
306
- - 导航链接必须使用 `data-page-route`,例如 `<a href="/cases" data-page-route="/cases">案例库</a>`。
307
- - 推送时同时推送页面和被修改的导航栏 / 入口页面。
308
- - 验证时必须从真实入口点击进入目标页面;只手动输入 route 打开不算完整验证。
309
-
310
- 如果用户明确要求创建“暂不公开页面 / 隐藏页 / 草稿页”,可以不绑定导航,但必须在 Task / changelog / 完成说明里写明“暂不绑定入口”的原因。
47
+ - 公开页 → 顶部/侧边导航、首页入口、相关按钮之一
48
+ - 后台页 → 后台导航、管理菜单或现有后台入口
49
+ - 多页面功能:列表/详情/新建/编辑/管理必须互相走通
50
+ - 导航链接必须用 `data-page-route`:`<a href="/orders" data-page-route="/orders">订单</a>`
51
+ - 用户明确要求"隐藏页/草稿页"才可不绑定,但必须在 changelog / Task 说明原因
311
52
 
312
53
  ### `page_1_root.html` 首页特例(强制)
313
54
 
314
- `draftgo init` 会把内置系统页面拉取到 `.draftgo/pages/`。这些系统页只用于理解平台能力和默认资源结构,不能作为新项目首页的直接改写模板。
55
+ - 仍是默认内置首页 按用户需求和本地 UI Skills 重新实现,不沿用内置文案和结构
56
+ - 已被用户改过 → 只做要求范围内的修改,不强制重做
57
+ - 判断依据:看 HTML 内容是否保留 DraftGo 默认文案/系统介绍/未定制品牌,不要只凭文件名判断
315
58
 
316
- 当目标页面是首页(常见标识:`id=1`、文件名为 `.draftgo/pages/page_1_root.html`、route 为 `/` 或标题为「首页」)时,先判断它是否仍是 init 拉取下来的**默认内置首页**:
317
- - 如果仍是默认内置首页,把它视为项目自己的首页资源,按用户需求、Story 和本地 UI Skills 重新实现,不沿用内置页的文案和业务结构。
318
- - 如果首页已经被用户改过,只做用户要求范围内的修改,不强制重做整页。
319
- - 判断是否仍为默认内置首页时,优先看 HTML 内容是否明显保留 DraftGo 默认首页的文案、结构、系统介绍、默认模块和未定制品牌;不要只凭文件名或 `id=1` 下结论。
320
- - 修改前先读取 `.draftgo/story.yaml`(如存在)和用户需求;若 Story 不存在且用户只要求“重做首页/设计首页”,按高风险任务处理,先确认方向再做。
321
- - 无论首页如何实现,都必须遵守本文件的资源、主题、弹窗、路由和响应格式规则。
322
- - 验证时除常规 console / push 外,还要检查桌面和移动宽度下无明显布局破损,且用户路径入口可达。
59
+ ---
323
60
 
324
- 一句话:**`page_1_root.html` 是项目自己的首页资源;默认内置首页不要直接套改,已定制首页不要强制重做。**
61
+ ## 必须 / 禁止
325
62
 
326
63
  ### 必须
327
64
  - 完整 HTML 文档结构(`<html><head><body>`)
328
- - 新增页面必须处理入口绑定;若修改入口页或导航栏,也要同步 push
329
- - 静态资源优先使用本地路径:
330
- ```html
331
- <script src="/assets/tailwindcss.js"></script>
332
- <link href="/assets/fontawesome/css/all.min.css" rel="stylesheet">
333
- ```
334
- - 颜色全部用语义 token(见下方"颜色 Token")
335
- - 弹窗用 `App.confirm()` / `App.toast()`
336
- - 页面初始渲染必须展示默认状态,不能因网络请求延迟导致空白
337
- - 需要 shadcn 组件能力时优先使用对应 `dg-*` 标签;`dg-*` 必须映射 shadcn 组件语义
65
+ - 静态资源用本地路径(见下方资源清单)
66
+ - 颜色全用语义 token `var(--dg-*)`
67
+ - 弹窗用 `App.confirm()` / `App.toast()`,禁止 `window.alert/confirm/prompt`
68
+ - 页面初始渲染展示默认状态(骨架屏),不能因请求延迟空白
69
+ - 需要组件能力时用对应 `dg-*` 标签
338
70
 
339
71
  ### 禁止
340
- - 禁止引用境外 CDN(`fonts.googleapis.com`、`cdn.jsdelivr.net`、`cdnjs.cloudflare.com`、`unpkg.com` 等)
341
- - 如需外部 CDN,只允许国内镜像(`npmmirror.com`、`staticfile.net`),且优先用本地资源
342
- - 禁止 `window.alert()` / `window.confirm()` / `window.prompt()`
343
- - 禁止在页面内渲染系统 Header、Logo、用户头像下拉等全局导航元素
344
- - 禁止 `App()` 写法(`App` 是对象不是函数)
345
- - 禁止 `const App = () => window.parent?.App`(正确:`const App = window.parent?.App`)
346
- - 禁止把 React/TSX 版 shadcn 组件直接写进数据库 HTML
347
- - 禁止把 `dg-*` 当作 daisyUI / Bootstrap / Ant Design / Element Plus / 自研组件库;`dg-*` 只能是 shadcn 的 HTML 协议形态
72
+ - 境外 CDNgoogleapis / jsdelivr / cdnjs / unpkg)→ ✅ 国内镜像或本地资源
73
+ - 外部 CDN 引入图标 → ✅ `/assets/icons/{name}.svg`
74
+ - `window.alert/confirm/prompt`
75
+ - `App()` 写法 → ✅ `const App = window.parent?.App`
76
+ - `window.location.search` 读参数 → ✅ `window.__DG_ROUTE_CONTEXT__.query`
77
+ - 页面内 `window.location.href=...` 跳转 `window.parent.location.href=...`
78
+ - TSX/React 版 shadcn 组件写进数据库 HTML
79
+ - `dg-*` daisyUI / Bootstrap / Ant Design
80
+ - ❌ 硬编码颜色值 → ✅ `var(--dg-*)`
81
+ - ❌ 在业务页面实现全局浮窗/客服/统计脚本 → ✅ `frontend_global_*` 固定槽位
82
+
83
+ ---
348
84
 
349
- ### 本地静态资源清单
85
+ ## 本地静态资源清单
350
86
 
351
87
  | 路径 | 说明 |
352
88
  |---|---|
353
89
  | `/assets/tailwindcss.js` | Tailwind CSS 运行时 |
354
- | `/assets/fontawesome/css/all.min.css` | FontAwesome 6 图标库 |
90
+ | `/assets/fontawesome/css/all.min.css` | FontAwesome 6 |
91
+ | `/assets/icons/{name}.svg` | 内置精选 SVG 图标库(kebab-case 命名) |
92
+ | `/assets/icons/manifest.json` | 图标库映射清单 |
355
93
  | `/assets/fonts/inter.css` | Inter 字体 |
356
94
  | `/assets/fonts/lexend.css` | Lexend 字体 |
357
- | `/assets/fonts/plus-jakarta-sans.css` | Plus Jakarta Sans 字体 |
358
- | `/assets/fonts/plus-jakarta-sans-jetbrains-mono.css` | Plus Jakarta Sans + JetBrains Mono 等宽 |
95
+ | `/assets/fonts/plus-jakarta-sans.css` | Plus Jakarta Sans |
96
+ | `/assets/fonts/plus-jakarta-sans-jetbrains-mono.css` | Plus Jakarta Sans + JetBrains Mono |
359
97
  | `/assets/vendor/marked/marked.min.js` | Markdown 解析 |
360
- | `/assets/vendor/prism/prism.min.js` | 代码语法高亮 |
361
- | `/assets/vendor/prism/themes/prism-tomorrow.min.css` | Prism 主题 |
362
- | `/assets/vendor/prism/components/prism-python.min.js` | Prism Python 语言支持 |
363
- | `/assets/vendor/prism/components/prism-json.min.js` | Prism JSON 语言支持 |
364
- | `/assets/vendor/prism/components/prism-yaml.min.js` | Prism YAML 语言支持 |
365
- | `/assets/vendor/gsap/gsap.min.js` | GSAP 3.15.0 动画核心库 |
366
- | `/assets/vendor/gsap/ScrollTrigger.min.js` | GSAP 滚动触发插件 |
367
- | `/assets/vendor/gsap/ScrollToPlugin.min.js` | GSAP 滚动定位插件 |
368
- | `/assets/vendor/gsap/Observer.min.js` | GSAP 输入事件观察插件 |
369
- | `/assets/vendor/gsap/Draggable.min.js` | GSAP 拖拽插件 |
370
- | `/assets/vendor/gsap/Flip.min.js` | GSAP 布局过渡插件(FLIP 动画) |
371
- | `/assets/vendor/gsap/MotionPathPlugin.min.js` | GSAP 路径动画插件 |
372
- | `/assets/vendor/gsap/SplitText.min.js` | GSAP 文字拆分动画插件 |
373
- | `/assets/vendor/gsap/TextPlugin.min.js` | GSAP 文字逐字动画插件 |
374
- | `/assets/vendor/gsap/CustomEase.min.js` | GSAP 自定义缓动曲线 |
375
- | `/assets/vendor/gsap/EasePack.min.js` | GSAP 扩展缓动集合(Bounce/Elastic 等) |
376
- | `/assets/vendor/gsap/DrawSVGPlugin.min.js` | GSAP SVG 路径描边动画插件 |
377
- | `/assets/vendor/gsap/MorphSVGPlugin.min.js` | GSAP SVG 形变动画插件 |
378
-
379
- > **GSAP 使用规范**:如需使用 GSAP,禁止引用任何境外 CDN(gsap.com/cdn.jsdelivr.net 等),必须使用上表本地路径。按需引入所需插件,注册插件后方可使用:`gsap.registerPlugin(ScrollTrigger, CustomEase)`。
380
-
381
- ---
382
-
383
- ## URL 参数读取
384
-
385
- 页面运行在 `iframe.srcdoc` 中,`window.location.search` 不可靠——它指向壳层地址,不是页面路由。
386
-
387
- ### 注入机制
388
-
389
- 壳层通过 `decorateFrameHtml()` 向每个页面注入 `<script data-dg-route-bridge>`,该脚本将路由上下文写入 iframe 的 `window`:
390
-
391
- - `window.__DG_ROUTE_CONTEXT__` — 完整路由上下文(冻结对象),含 `query`、`route`、`params` 等
392
- - `window.__DG_QUERY__` — `routeContext.query` 的快捷方式
393
- - `window.__DG_GET_ROUTE_CONTEXT__()` — 函数形式的兜底读取
394
-
395
- ### 标准读取方式(三阶回落)
396
-
397
- ```javascript
398
- const routeContext =
399
- window.__DG_ROUTE_CONTEXT__ // 首选:bridge 注入的主变量
400
- || window.__DG_GET_ROUTE_CONTEXT__?.() // 备选:函数兜底
401
- || window.parent?.App?.getCurrentRouteContext?.() // 三选:壳层 API
402
- || { query: {} }; // 兜底:空对象防崩溃
403
- const query = routeContext.query || {};
404
-
405
- // 使用示例
406
- // 路由 /doctor/prescription-estimate?patientId=42&visitId=7
407
- const patientId = query.patientId; // "42"
408
- const visitId = query.visitId; // "7"
409
- ```
410
-
411
- ### 禁止
412
-
413
- - `new URLSearchParams(window.location.search)` — iframe 中取不到壳层 URL
414
- - `window.location.search` 直接解析 — 同上
415
-
416
- ### 常见陷阱
417
-
418
- 页面 JS 中引用 `window.__DG_ROUTE_CONTEXT__` 是**读取**操作。框架注入 bridge 的检测基于 `<script data-dg-route-bridge>` 标记,不会因页面代码读取该变量而误判跳过注入。
419
-
420
- ---
421
-
422
- ## 平台能力调用(App API)
423
-
424
- ```javascript
425
- const App = window.parent?.App;
426
-
427
- // ─── 响应格式 ───
428
- // App.get/post/put/patch/delete 原样返回后端 JSON,不做任何剥壳。
429
- // 后端统一信封:{ code: 200, message: "success", data: ... }
430
- //
431
- // 统一消费模式:
432
- // const res = await App.get('some/endpoint');
433
- // if (res.code !== 200) { App.showError(res.message || '操作失败'); return; }
434
- // const payload = res.data; // 业务数据
435
- // // 分页列表:payload.items / payload.total / payload.page / payload.page_size
436
- // // 非分页列表:payload 本身是数组
437
-
438
- // GET(第二参数为 query params)
439
- const res = await App.get('pages', { page: 1, page_size: 20 });
440
- const pages = res.data.items; // Array
441
-
442
- // POST / PUT / PATCH / DELETE
443
- await App.post('feedback', { type: 'bug', title: '标题' });
444
- await App.put('pages/123', { title: '新标题' });
445
- await App.delete('pages/123');
446
-
447
- // Toast(推荐用便捷方法)
448
- App.toast('操作成功', 'success'); // 通用方法,type 默认 'info'
449
- App.showSuccess('操作成功'); // 成功提示
450
- App.showError('操作失败'); // 错误提示
451
- App.showWarning('请注意'); // 警告提示
452
- App.showInfo('提示信息'); // 信息提示
453
-
454
- // 确认弹窗
455
- const ok = await App.confirm('确认删除?', '删除确认');
456
- if (!ok) return;
457
- // App.confirm(msg, title?) — 返回 Promise<boolean>,取消/点击蒙层返回 false
458
-
459
- // 信息模态框
460
- App.showModal('Token 详情内容', 'Token 明细');
461
- // App.showModal(msg, title?) — 替代 window.alert()
462
-
463
- // Loading 蒙层
464
- App.showLoading(); await someAsyncOp(); App.hideLoading();
465
-
466
- // 文件上传(返回格式同标准信封)
467
- const res = await App.uploadFile(file, progress => console.log(progress));
468
- const url = res.data?.url || res.data?.path;
469
-
470
- // 外部 API 调用(已注册到「外部 API 接入」)
471
- // 认证由后端注入;不要在页面里写 API Key / Bearer
472
- const r = await App.callApi('weather-now', {
473
- path_params: { city: 'beijing' }, // 替换 path 模板里的 {city}
474
- query_params: { unit: 'metric' },
475
- // body: { ... }, // POST/PUT/PATCH 才生效
476
- // headers: { 'X-Trace': 't1' }, // 不会覆盖后端注入的认证头
477
- });
478
- // 返回:{ status_code, headers, body, duration_ms, error }
479
- // body 已按 Content-Type 自动解析;上游业务错误看 status_code,不是 error
480
-
481
- // 列出当前用户可调用的外部 API(含 code / name / 各 JSON Schema)
482
- const apis = await App.listApis();
483
- ```
484
-
485
- 路径解析:`https://...` 直接请求 · `/api/...` 拼接 origin · 其他相对路径拼接 apiBase
486
-
487
- ### AIHub 页面 SDK
488
-
489
- ```javascript
490
- // 聊天型 Agent
491
- const text = await DraftGoAI.chat(agentId, '你好', (delta, full) => {
492
- render(full);
493
- });
494
-
495
- // 图片生成型 Agent
496
- const imageResult = await DraftGoAI.images(agentId, '生成一张产品主图', {
497
- size: '1024x1024',
498
- quality: 'auto',
499
- response_format: 'url',
500
- n: 1,
501
- });
502
-
503
- // 用户自主选择模型
504
- const selectable = await DraftGoAI.getSelectableModels(agentId);
505
- if (selectable.user_selectable && selectable.models.length) {
506
- renderModelPicker(selectable.models);
507
- }
508
-
509
- await DraftGoAI.chat(agentId, '你好', (delta, full) => {
510
- render(full);
511
- }, {
512
- model: getSelectedModel(),
513
- });
98
+ | `/assets/vendor/prism/prism.min.js` | 代码高亮 |
99
+ | `/assets/vendor/gsap/gsap.min.js` | GSAP 核心 |
100
+ | `/assets/vendor/gsap/ScrollTrigger.min.js` | GSAP ScrollTrigger |
101
+ | `/assets/vendor/gsap/ScrollToPlugin.min.js` | GSAP ScrollTo |
102
+ | `/assets/vendor/gsap/Draggable.min.js` | GSAP Draggable |
103
+ | `/assets/vendor/gsap/Flip.min.js` | GSAP Flip |
104
+ | `/assets/vendor/gsap/SplitText.min.js` | GSAP SplitText |
105
+ | `/assets/vendor/gsap/CustomEase.min.js` | GSAP CustomEase |
106
+
107
+ **图标用法**:
108
+ ```html
109
+ <!-- 跟随文字颜色(推荐) -->
110
+ <span aria-hidden="true" style="width:16px;height:16px;display:inline-block;background:currentColor;-webkit-mask:url('/assets/icons/fingerprint.svg') center/contain no-repeat;mask:url('/assets/icons/fingerprint.svg') center/contain no-repeat;"></span>
111
+ <!-- 保留原色 -->
112
+ <img src="/assets/icons/fingerprint.svg" alt="" width="16" height="16">
514
113
  ```
515
114
 
516
- 图片生成型 Agent 必须调用 `/api/agents/{id}/images`,不要用 `/chat` 代替。模型是否支持生图由真实上游调用决定,管理端图片诊断只是可选参考。
517
-
518
- `DraftGoAI.getSelectableModels(agentId)` 返回 `{ user_selectable, models }`。`models` 是 Agent 主模型 + 备用模型白名单,不是供应商全量模型列表;只有管理端开启“用户选模型”后才会返回非空列表。用户选择值通过 `options.model` 传给 `DraftGoAI.chat/images`,后端仍会按 Agent 白名单校验。
519
-
520
- **外部 API 注意事项:**
521
- - code 不知道时先 `await App.listApis()` 查询
522
- - `r.error` 仅在代理层错误(超时 / 网络 / 校验失败)时非空
523
- - API 未注册 / 已禁用 / 无权限 → 后端以 4xx 抛错,会被 `App.callApi` 当成异常 throw
115
+ GSAP:禁止引用任何境外 CDN,必须用本地路径,注册插件后才能使用:`gsap.registerPlugin(ScrollTrigger, CustomEase)`
524
116
 
525
117
  ---
526
118
 
527
119
  ## 颜色 Token(强制)
528
120
 
529
- 禁止硬编码任何 hex / rgb / rgba / hsl 值。
530
-
531
121
  | Token | 用途 |
532
122
  |---|---|
533
- | `--dg-bg-base` | 页面底色 |
534
- | `--dg-bg-page` | 内容区背景 |
535
- | `--dg-bg-surface` | 卡片/面板背景 |
536
- | `--dg-text-primary` | 主文字 |
537
- | `--dg-text-secondary` | 次要文字 |
538
- | `--dg-text-muted` | 弱化文字 |
539
- | `--dg-accent` | 主题色 |
540
- | `--dg-accent-hover` | 主题色 hover |
541
- | `--dg-border` | 边框 |
542
- | `--dg-success/error/warning/info` | 状态色 |
543
-
544
- 例外:SVG 的 `fill`/`stroke` 可用 `currentColor` 或 `var(--dg-accent)`。
545
-
546
- ---
123
+ | `--dg-bg-base` / `--dg-bg-page` / `--dg-bg-surface` | 背景层级 |
124
+ | `--dg-text-primary` / `--dg-text-secondary` / `--dg-text-muted` | 文字层级 |
125
+ | `--dg-accent` / `--dg-accent-hover` / `--dg-accent-subtle` | 主题色 |
126
+ | `--dg-border` / `--dg-success` / `--dg-error` / `--dg-warning` | 功能色 |
547
127
 
548
- ## 主题机制(App.theme / App.colorScheme)
128
+ 例外:品牌色、图表色。硬编码时必须同时提供 `[data-theme="dark"]` 覆盖,对比度达 WCAG AA(文字 4.5:1)。
549
129
 
550
- ### 概念分层
551
-
552
- | 概念 | 键 | 取值 | 说明 |
553
- |---|---|---|---|
554
- | 显示模式 | `App.theme` / `dg_theme` | `'light'` \| `'dark'` | 亮色/暗色 |
555
- | 配色方案 | `App.colorScheme` / `dg_color_scheme` | `'dark-gray-white'` \| `'deep-blue-white'` \| `'orange-white'` \| `'custom'` | light 模式下的色彩搭配 |
556
-
557
- ### 内置配色方案
558
-
559
- | 方案 | 键 | 主题色 | 特点 |
560
- |---|---|---|---|
561
- | 深灰白(默认) | `dark-gray-white` | #27272a |
562
- | 深蓝白 | `deep-blue-white` | #1e3a5f |
563
- | 橙白 | `orange-white` | #ea580c |
564
- | 自定义 | `custom` | 用户自选 |
565
-
566
- ### 入场主题
567
-
568
- 页面以 `iframe.srcdoc` 渲染,壳层在注入 HTML 时已将当前主题的 CSS 变量写入 `<head>` 最顶部。
569
-
570
- **页面无需任何初始化代码**,CSS 变量在 DOM 解析时已生效,直接用 `var(--dg-*)` 即可。
571
-
572
- ### 读取当前主题
130
+ 主题机制:壳层注入时已把 CSS 变量写入 `<head>`,页面无需初始化,直接用 `var(--dg-*)` 即可。
573
131
 
574
132
  ```javascript
575
- const App = window.parent?.App;
133
+ // 读取(仅需 JS 分支时才读)
576
134
  const theme = App?.theme; // 'light' | 'dark'
577
- const scheme = App?.colorScheme; // 'dark-gray-white' | 'deep-blue-white' | 'orange-white' | 'custom'
578
- ```
135
+ const scheme = App?.colorScheme; // 'dark-gray-white' | 'deep-blue-white' | ...
579
136
 
580
- 仅在需要**按主题做 JS 逻辑分支**时才读取,纯样式差异用 CSS 变量解决。
581
-
582
- ### 切换主题
583
-
584
- ```javascript
585
- App.applyTheme('dark'); // 切换显示模式
586
- App.setColorScheme('deep-blue-white'); // 切换为预设方案
587
- App.setColorScheme('custom', customVarsObject); // 应用自定义配色
588
- ```
589
-
590
- 调用后:壳层更新 CSS 变量 → 写入 localStorage → 同步注入当前 iframe。
591
-
592
- ### 获取方案详情
593
-
594
- ```javascript
595
- const info = App.getColorScheme();
596
- // { name: 'dark-gray-white', vars: { ... }, schemes: { ... } }
137
+ // 切换
138
+ App.applyTheme('dark');
139
+ App.setColorScheme('deep-blue-white');
597
140
  ```
598
141
 
599
- ### 监听主题变化
600
-
601
- 壳层不广播主题变更事件。如果页面需要响应用户切换主题,用 `MutationObserver` 监听根元素 CSS 变量变化,或在切换按钮的回调里手动处理。
602
-
603
- ### 禁止
604
-
605
- - 禁止在页面内读取 `localStorage.dg_theme`(通过 `App.theme` 获取)
606
- - 禁止在页面内读取 `localStorage.dg_color_scheme`(通过 `App.colorScheme` 获取)
607
- - 禁止在页面内调用 `document.documentElement.style.setProperty` 设置主题变量
608
- - 禁止硬编码任何颜色值作为主题分支的输出
609
-
610
142
  ---
611
143
 
612
- ## 空态容器
144
+ ## 导航栏开发
613
145
 
614
- 当页面需要展示“暂无数据”“列表为空”“加载失败”等状态时,必须渲染明确的状态容器、文案和后续操作入口,不能让页面空白或只有不可解释的 loading。
146
+ | 属性 | 说明 |
147
+ |---|---|
148
+ | `data-nav-position="top\|side"` | 根元素必填 |
149
+ | `data-nav-width="220px"` | 侧边栏展开宽度(可选,默认 260px) |
150
+ | `data-nav-collapsed-width="64px"` | 收起宽度(可选,默认 72px) |
615
151
 
616
- 若父级是 flex 工作区,空态容器应能承接剩余空间,避免数据区高度塌陷。具体呈现交给本地 UI Skills 或 Agent 自身判断。
152
+ ```html
153
+ <aside data-nav-position="side" data-nav-width="220px">
154
+ <a href="/dashboard" data-page-route="/dashboard">仪表盘</a>
155
+ </aside>
156
+ ```
617
157
 
618
- ---
158
+ 所有导航链接必须用 `data-page-route`,不要硬编 `onclick` 跳转。
619
159
 
620
- ## 导航栏开发
160
+ ---
621
161
 
622
- 导航栏是**数据库资产**,HTML 存储在 `navigation.html` 字段,由前端 runtime 动态挂载。
162
+ ## AIHub 页面 SDK
623
163
 
624
- ### 导航类型
164
+ ```javascript
165
+ // 聊天
166
+ const text = await DraftGoAI.chat(agentId, '你好', (delta, full) => render(full));
625
167
 
626
- | 类型 | 根元素 | 必填属性 | 布局 |
627
- |---|---|---|---|
628
- | 顶栏 | `<header>` | `data-nav-position="top"` | 顶部 |
629
- | 侧边 | `<aside>` | `data-nav-position="side"` | 左侧,可声明自定义宽度 |
168
+ // 图片生成(必须用 /images 接口,不要用 /chat)
169
+ const result = await DraftGoAI.images(agentId, '生成主图', { size: '1024x1024', n: 1 });
630
170
 
631
- ### 必须
632
- - 根元素加 `data-nav-position="top|side"`
633
- - 所有导航链接必须用 `data-page-route`:
634
- ```html
635
- <a href="/dashboard" data-page-route="/dashboard">仪表盘</a>
636
- ```
171
+ // 用户选模型
172
+ const { user_selectable, models } = await DraftGoAI.getSelectableModels(agentId);
173
+ await DraftGoAI.chat(agentId, '你好', handler, { model: selectedModel });
174
+ ```
637
175
 
638
- ### 侧边栏宽度
176
+ ---
639
177
 
640
- 侧边导航宽度由壳层 `#nav-container` 控制,导航根元素内部应保持 `width:100%;height:100%`。需要自定义宽度时,在根 `<aside>` 上声明:
178
+ ## 退出登录
641
179
 
642
- ```html
643
- <aside data-nav-position="side" data-nav-width="220px" data-nav-collapsed-width="64px">
180
+ ```javascript
181
+ window.parent.location.href = '/login'; // ✅ 完整刷新,导航栏重新加载
182
+ // App.navigate('/login') ❌ 导航栏不会更新
644
183
  ```
645
184
 
646
- - `data-nav-width`:展开宽度,可选;不写默认 `260px`
647
- - `data-nav-collapsed-width`:收起宽度,可选;不写默认 `72px`
648
- - 支持纯数字(按 px 处理)或合法 CSS 长度,如 `220px`、`14rem`、`clamp(200px,20vw,260px)`
649
- - 不要在导航脚本里硬编码覆盖 `#nav-container` / `#page-container` 的 `260px`,优先用上述属性让 runtime 同步布局
185
+ ---
650
186
 
651
- ### 常见错误
187
+ ## 加载体验
652
188
 
653
- | 错误 | 原因 | 修复 |
654
- |---|---|---|
655
- | 导航不显示 | `status` 为 `inactive` | 改为 `active` |
656
- | 布局错乱 | 缺少 `data-nav-position` | 在根元素加上该属性 |
657
- | 链接点击无效 | 缺少 `data-page-route` | 在 `<a>` 上加该属性 |
658
- | 主题切换后颜色异常 | 使用了硬编码颜色 | 改用 `var(--dg-*)` 变量 |
189
+ - 初始渲染必须先展示骨架屏,再异步填数据
190
+ - 禁止因请求失败导致整页空白
191
+ - 空态必须有明确文案 + 后续操作入口,不能留空或只有 loading
192
+ - 若父级是 flex 工作区,空态容器应 `flex:1` 承接剩余空间,避免高度塌陷
659
193
 
660
194
  ---
661
195
 
662
- ## 退出登录
196
+ ## 平台能力调用快速参考
663
197
 
664
198
  ```javascript
665
- window.location.href = '/login'; // 正确:完整刷新,导航栏重新加载
666
- // 错误:navigate('/login') — 导航栏不会更新
667
- ```
199
+ const App = window.parent?.App;
668
200
 
669
- ---
201
+ // 响应信封:{ code: 200, data: <载荷>, message: "success" }
202
+ const res = await App.get('pages', { page: 1, page_size: 20 });
203
+ if (res.code !== 200) { App.showError(res.message); return; }
204
+ const items = res.data.items;
670
205
 
671
- ## 加载体验
206
+ // URL 参数(三阶回落)
207
+ const q = (window.__DG_ROUTE_CONTEXT__ || window.__DG_GET_ROUTE_CONTEXT__?.() || App?.getCurrentRouteContext?.() || { query: {} }).query;
208
+ ```
672
209
 
673
- - 禁止因网络请求失败导致整个页面空白
674
- - 页面初始化必须先渲染默认状态,再异步填充数据
675
- - 推荐使用 shadcn Skeleton 对应的 `dg-skeleton`,或使用与 shadcn token 对齐的骨架样式。
210
+ 完整 API 表 → `{{SKILL_DIR}}/quickref/app-api.md`