draftgo-cli 2.0.3 → 2.0.5

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.
@@ -9,17 +9,39 @@ version: 1.0.0
9
9
  ## 架构认知
10
10
 
11
11
  DraftGo 前端**不是传统 SPA**:
12
+ - 平台壳层使用 React + Vite 编译,shadcn/ui + Tailwind CSS 已作为默认前端范式引入
12
13
  - 业务页面 HTML 存在数据库,运行在 `iframe.srcdoc`
13
14
  - 导航栏 HTML 存在数据库,由壳层按需加载
14
15
  - 页面通过 `window.parent.App` 调用平台能力
15
16
 
16
17
  ---
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
+
18
40
  ## 运行时机制(Runtime)
19
41
 
20
42
  ### 壳层启动流程
21
43
 
22
- 壳层入口(`frontend/src/core/runtime.js`)调用 `createAppRuntime()` 完成以下初始化:
44
+ 壳层入口由 Vite/React 工程加载,当前兼容 runtime 仍通过 `frontend/src/core/runtime.js` `createAppRuntime()` 完成以下初始化:
23
45
 
24
46
  1. `reloadSystemConfig()` — 拉取 `/api/system/config`,填充 `state.config`
25
47
  2. `validateToken()` — 验证 `localStorage.dg_access_token`,成功后设置 `currentUser`
@@ -28,10 +50,10 @@ DraftGo 前端**不是传统 SPA**:
28
50
 
29
51
  ### iframe 注入机制
30
52
 
31
- 壳层通过 `decorateFrameHtml(html, routeContext, theme)` 处理页面 HTML,注入:
53
+ 壳层通过 `decorateFrameHtml(html, routeContext, theme)` 处理数据库页面 HTML,注入:
32
54
  - `window.__DG_ROUTE_CONTEXT__` — 当前路由上下文(含 `query` 参数)
33
55
  - 主题 CSS 变量
34
- - 静态资源(Tailwind、FontAwesome 等)
56
+ - 静态资源(数据库页面使用的本地 Tailwind runtime、FontAwesome、GSAP 等)
35
57
 
36
58
  页面 HTML 以 `iframe.srcdoc` 方式渲染,**不是独立 URL**,因此:
37
59
  - `window.location` 指向壳层地址,不可用于读取路由参数
@@ -58,6 +80,8 @@ DraftGo 前端**不是传统 SPA**:
58
80
  | `navigate(route)` | `runtime.js` | 路由跳转(pushState) |
59
81
  | `getCurrentRoute()` | `runtime.js` | 当前路径字符串 |
60
82
  | `getCurrentRouteContext()` | `runtime.js` | 完整路由上下文(含 query) |
83
+ | `reloadGlobalLayer()` | `runtime.js` | 重载前端全局层 |
84
+ | `openGlobalWidget(name)` / `closeGlobalWidget(name)` | `runtime.js` | 触发全局挂件打开/关闭事件 |
61
85
  | `logout()` | `runtime.js` | 登出并跳转登录页 |
62
86
  | `setAuthTokens({access_token, refresh_token})` | `runtime.js` | 登录后写入 token |
63
87
  | `applyTheme(theme)` | `runtime.js` | 切换主题(light/dark),同步写 localStorage + iframe |
@@ -80,7 +104,7 @@ DraftGo 前端**不是传统 SPA**:
80
104
 
81
105
  ### 禁止使用浏览器默认弹窗(强制)
82
106
 
83
- **禁止**在页面中使用 `alert()`、`confirm()`、`prompt()`。它们会阻塞主线程、风格与平台 UI 不一致、且在 iframe 环境中行为不可预期。
107
+ **禁止**在页面中使用 `alert()`、`confirm()`、`prompt()`。它们会阻塞主线程、与平台交互协议不一致,且在 iframe 环境中行为不可预期。
84
108
 
85
109
  使用 App 提供的替代方案:
86
110
 
@@ -88,7 +112,7 @@ DraftGo 前端**不是传统 SPA**:
88
112
  |---|---|---|
89
113
  | `alert(msg)` | `App.showModal(msg, title?)` | 信息展示,带确定按钮 |
90
114
  | `confirm(msg)` | `await App.confirm(msg, title?)` | 二次确认,返回 `Promise<boolean>`,注意必须 `await` |
91
- | `prompt(msg)` | 自行构建输入弹窗 | 平台无内置 prompt,需在页面内自建 modal + input,**样式必须与 App.confirm/showModal 保持一致**(圆角14px、backdrop-filter blur、同配色token、同动画) |
115
+ | `prompt(msg)` | 自行构建输入弹窗 | 平台无内置 prompt,需在页面内自建 modal + input,并保持与当前页面和平台交互一致 |
92
116
 
93
117
  ```javascript
94
118
  // ❌ 禁止
@@ -107,6 +131,29 @@ showInputModal('请输入名称', async (value) => {
107
131
  });
108
132
  ```
109
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
+
110
157
  ### 全局事件
111
158
 
112
159
  | 事件名 | 触发时机 |
@@ -144,7 +191,7 @@ App.get('pages/', { page: 1, page_size: 20 })
144
191
  |---|---|---|
145
192
  | `page` | int | 页码,从 1 开始 |
146
193
  | `page_size` | int | 每页条数 |
147
- | `search` | string | 全文搜索(users/roles/pages/navigations/feedback/db) |
194
+ | `search` | string | 全文搜索(users/roles/pages/navigations/feedback —— **注意:动态 DB 不用 search,见下方**) |
148
195
  | `status` | string | 状态过滤(`active`/`inactive`/`published` 等,按资源而定) |
149
196
  | `type` | string | 类型过滤(notices/feedback/aihub) |
150
197
  | `tag` | string | 标签过滤(pages/aihub) |
@@ -157,9 +204,19 @@ App.get('users', { page: 1, page_size: 20, search: 'alice', status: 'active', ro
157
204
 
158
205
  // 页面列表,带标签+状态过滤
159
206
  App.get('pages/', { page: 1, page_size: 20, search: 'home', tag: 'blog', status: 'published' })
207
+ ```
208
+
209
+ ### 动态 DB(`/api/db/{type}`)检索范式
160
210
 
161
- // 动态 DB 数据
162
- App.get(`db/${type}`, { page: 1, page_size: 20, search: 'keyword' })
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
+ })
163
220
 
164
221
  // 动态 DB 创建:业务字段必须放在 data 包裹里,不要平铺到 body 顶层
165
222
  App.post(`db/${type}`, { data: { name: '张三', age: 30 }, status: 1 })
@@ -168,9 +225,22 @@ App.post(`db/${type}`, { data: { name: '张三', age: 30 }, status: 1 })
168
225
  App.put(`db/${type}/${id}`, { data: { age: 31 } })
169
226
  ```
170
227
 
171
- **DB Meta 说明**:涉及动态数据库操作前,先读取 `db_meta/index.json` 了解当前项目有哪些 DB 类型(`type` 字段),再调用 `/api/db/{type}` 进行 CRUD。不要硬编码 type 值。
228
+ **filters 操作符**(字段必须在 db_meta schema 里标 `searchable`,且操作符要匹配字段的检索模式):
172
229
 
173
- **search 过滤限制**:`/api/db/{type}` `search` 参数底层走 JSON LIKE,传入形如 `["key:value"]` 的关键字数组会拼成 `WHERE search LIKE '%key:value%'`,**只能子串匹配**,无法保证字段精确等值。需要按字段精确过滤时,建议在业务侧拉一批后内存过滤,或在后端走显式 SQL。
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。
174
244
 
175
245
  ---
176
246
 
@@ -183,9 +253,10 @@ App.put(`db/${type}/${id}`, { data: { age: 31 } })
183
253
  用户说“做 / 新建 / 增加一个页面”时,默认不是静态稿,而是一个可被真实使用的页面功能:
184
254
  - 页面必须有可访问 route,且 route 与 page_id / 本地文件对应清楚。
185
255
  - 页面必须从真实入口可达:导航栏、首页模块、后台菜单、列表操作按钮或相关页面链接至少绑定一个。
186
- - 页面里的按钮、表单、搜索、筛选、分页、详情跳转、提交、保存、删除等交互默认要真实有效;只有用户明确说“静态 / 纯页面 / demo / 先看效果”时,才允许做静态。
256
+ - 页面里的按钮、表单、搜索、筛选、分页、详情跳转、提交、保存、删除等交互默认要真实有效;只有用户明确说“静态 / 纯页面 / demo / mock / 假数据 / 伪功能 / 先看效果”时,才允许做静态。
187
257
  - 页面必须有加载态、空态、错误态、成功态,不能只堆静态卡片。
188
- - 若页面展示可维护内容(案例、新闻、产品、招聘、资料等),主动判断是否需要后台管理页和同一份真实数据;不要只写死 HTML
258
+ - 若页面展示可维护内容(案例、新闻、产品、招聘、资料等),主动判断是否需要后台管理页和同一份真实数据;不要只写死 HTML 或前端数组。
259
+ - 若 DraftGo 动态 DB、已有 db_meta、custom_scripts、外部 API、AIHub 等能力都无法完成真实闭环,不要写伪功能交差;停止开发,向用户说明阻塞点,并按 dev-workflow 写入 `.draftgo/lessons/`。
189
260
 
190
261
  ### 操作型页面空间利用(强制)
191
262
 
@@ -240,18 +311,17 @@ App.put(`db/${type}/${id}`, { data: { age: 31 } })
240
311
 
241
312
  ### `page_1_root.html` 首页特例(强制)
242
313
 
243
- `draftgo init` 会把内置系统页面拉取到 `.draftgo/pages/`。这些系统页只用于理解平台能力和默认资源结构,**不能作为首页视觉风格参考**。
314
+ `draftgo init` 会把内置系统页面拉取到 `.draftgo/pages/`。这些系统页只用于理解平台能力和默认资源结构,不能作为新项目首页的直接改写模板。
244
315
 
245
316
  当目标页面是首页(常见标识:`id=1`、文件名为 `.draftgo/pages/page_1_root.html`、route 为 `/` 或标题为「首页」)时,先判断它是否仍是 init 拉取下来的**默认内置首页**:
246
- - 如果仍是默认内置首页,**必须按全新风格 Web 处理**,把它视为当前项目的第一屏门面,而不是后台系统页的延续。
247
- - 如果首页已经被用户改成了项目自己的风格,**不要强制重做成另一套新风格**;应延续现有品牌、布局、动效和信息架构,只做用户要求的局部优化或迭代。
248
- - 判断是否仍为默认内置首页时,优先看 HTML 内容是否明显保留 DraftGo 默认首页的文案、结构、系统介绍、默认卡片/模块和未定制品牌;不要只凭文件名或 `id=1` 下结论。
249
- - 默认内置首页状态下,**禁止沿用内置首页的布局、配色、卡片样式、文案节奏和信息架构**;只能复用必要的 DraftGo 技术约束(App API、主题 token、本地静态资源、iframe 规则)。
250
- - 修改前先读取 `.draftgo/story.yaml`(如存在)和用户需求,提炼品牌/产品调性;若 Story 不存在且用户只要求“重做首页/设计首页”,按高风险任务处理,先确认方向再做。
251
- - 允许使用更强的视觉叙事、首屏 hero、图文/数据展示、动效和营销式结构,但仍必须遵守本文件的资源、主题、弹窗、路由和响应格式规则。
252
- - 验证时除常规 console / push 外,还要检查首屏在桌面和移动宽度下是否符合当前项目风格;若是从默认内置首页重做,还要确认它像独立产品官网,而不是 DraftGo 内置管理首页的改皮。
317
+ - 如果仍是默认内置首页,把它视为项目自己的首页资源,按用户需求、Story 和本地 UI Skills 重新实现,不沿用内置页的文案和业务结构。
318
+ - 如果首页已经被用户改过,只做用户要求范围内的修改,不强制重做整页。
319
+ - 判断是否仍为默认内置首页时,优先看 HTML 内容是否明显保留 DraftGo 默认首页的文案、结构、系统介绍、默认模块和未定制品牌;不要只凭文件名或 `id=1` 下结论。
320
+ - 修改前先读取 `.draftgo/story.yaml`(如存在)和用户需求;若 Story 不存在且用户只要求“重做首页/设计首页”,按高风险任务处理,先确认方向再做。
321
+ - 无论首页如何实现,都必须遵守本文件的资源、主题、弹窗、路由和响应格式规则。
322
+ - 验证时除常规 console / push 外,还要检查桌面和移动宽度下无明显布局破损,且用户路径入口可达。
253
323
 
254
- 一句话:**`page_1_root.html` 是项目自己的 Web 门面;默认内置首页要重塑,已定制首页要延续。**
324
+ 一句话:**`page_1_root.html` 是项目自己的首页资源;默认内置首页不要直接套改,已定制首页不要强制重做。**
255
325
 
256
326
  ### 必须
257
327
  - 完整 HTML 文档结构(`<html><head><body>`)
@@ -264,6 +334,7 @@ App.put(`db/${type}/${id}`, { data: { age: 31 } })
264
334
  - 颜色全部用语义 token(见下方"颜色 Token")
265
335
  - 弹窗用 `App.confirm()` / `App.toast()`
266
336
  - 页面初始渲染必须展示默认状态,不能因网络请求延迟导致空白
337
+ - 需要 shadcn 组件能力时优先使用对应 `dg-*` 标签;`dg-*` 必须映射 shadcn 组件语义
267
338
 
268
339
  ### 禁止
269
340
  - 禁止引用境外 CDN(`fonts.googleapis.com`、`cdn.jsdelivr.net`、`cdnjs.cloudflare.com`、`unpkg.com` 等)
@@ -272,6 +343,8 @@ App.put(`db/${type}/${id}`, { data: { age: 31 } })
272
343
  - 禁止在页面内渲染系统 Header、Logo、用户头像下拉等全局导航元素
273
344
  - 禁止 `App()` 写法(`App` 是对象不是函数)
274
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 协议形态
275
348
 
276
349
  ### 本地静态资源清单
277
350
 
@@ -303,9 +376,7 @@ App.put(`db/${type}/${id}`, { data: { age: 31 } })
303
376
  | `/assets/vendor/gsap/DrawSVGPlugin.min.js` | GSAP SVG 路径描边动画插件 |
304
377
  | `/assets/vendor/gsap/MorphSVGPlugin.min.js` | GSAP SVG 形变动画插件 |
305
378
 
306
- > **GSAP 使用规范**:禁止引用任何境外 CDN(gsap.com/cdn.jsdelivr.net 等),必须使用上表本地路径。按需引入所需插件,注册插件后方可使用:`gsap.registerPlugin(ScrollTrigger, CustomEase)`。
307
- >
308
- > **GSAP 体验原则**:开发前端页面时,优先考虑使用 GSAP 来提升前端体验,并由页面目标、交互复杂度和性能表现决定具体动效方案。
379
+ > **GSAP 使用规范**:如需使用 GSAP,禁止引用任何境外 CDN(gsap.com/cdn.jsdelivr.net 等),必须使用上表本地路径。按需引入所需插件,注册插件后方可使用:`gsap.registerPlugin(ScrollTrigger, CustomEase)`。
309
380
 
310
381
  ---
311
382
 
@@ -373,20 +444,19 @@ await App.post('feedback', { type: 'bug', title: '标题' });
373
444
  await App.put('pages/123', { title: '新标题' });
374
445
  await App.delete('pages/123');
375
446
 
376
- // Toast(推荐用便捷方法,默认时长更合理)
447
+ // Toast(推荐用便捷方法)
377
448
  App.toast('操作成功', 'success'); // 通用方法,type 默认 'info'
378
- App.showSuccess('操作成功'); // 绿色 ✓ 3000ms
379
- App.showError('操作失败'); // 红色 ✕ 4000ms
380
- App.showWarning('请注意'); // 黄色 ⚠ 3000ms
381
- App.showInfo('提示信息'); // 蓝色 ℹ 3000ms
382
- // 特性:玻璃质感、顶部居中弹入、进度条倒计时、hover 暂停、× 按钮关闭、明暗适配
449
+ App.showSuccess('操作成功'); // 成功提示
450
+ App.showError('操作失败'); // 错误提示
451
+ App.showWarning('请注意'); // 警告提示
452
+ App.showInfo('提示信息'); // 信息提示
383
453
 
384
- // 确认弹窗(玻璃蒙层 + 弹跳动画入、动画出,⚠ 图标,两个按钮)
454
+ // 确认弹窗
385
455
  const ok = await App.confirm('确认删除?', '删除确认');
386
456
  if (!ok) return;
387
457
  // App.confirm(msg, title?) — 返回 Promise<boolean>,取消/点击蒙层返回 false
388
458
 
389
- // 信息模态框(玻璃蒙层 + 弹跳动画,i 图标,单按钮)
459
+ // 信息模态框
390
460
  App.showModal('Token 详情内容', 'Token 明细');
391
461
  // App.showModal(msg, title?) — 替代 window.alert()
392
462
 
@@ -429,10 +499,24 @@ const imageResult = await DraftGoAI.images(agentId, '生成一张产品主图',
429
499
  response_format: 'url',
430
500
  n: 1,
431
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
+ });
432
514
  ```
433
515
 
434
516
  图片生成型 Agent 必须调用 `/api/agents/{id}/images`,不要用 `/chat` 代替。模型是否支持生图由真实上游调用决定,管理端图片诊断只是可选参考。
435
517
 
518
+ `DraftGoAI.getSelectableModels(agentId)` 返回 `{ user_selectable, models }`。`models` 是 Agent 主模型 + 备用模型白名单,不是供应商全量模型列表;只有管理端开启“用户选模型”后才会返回非空列表。用户选择值通过 `options.model` 传给 `DraftGoAI.chat/images`,后端仍会按 Agent 白名单校验。
519
+
436
520
  **外部 API 注意事项:**
437
521
  - code 不知道时先 `await App.listApis()` 查询
438
522
  - `r.error` 仅在代理层错误(超时 / 网络 / 校验失败)时非空
@@ -474,10 +558,10 @@ const imageResult = await DraftGoAI.images(agentId, '生成一张产品主图',
474
558
 
475
559
  | 方案 | 键 | 主题色 | 特点 |
476
560
  |---|---|---|---|
477
- | 深灰白(默认) | `dark-gray-white` | #27272a 深灰 | 冷调深灰,干净利落 |
478
- | 深蓝白 | `deep-blue-white` | #1e3a5f 深蓝 | 专业稳重,企业风格 |
479
- | 橙白 | `orange-white` | #ea580c 活力橙 | 温暖醒目,创意风格 |
480
- | 自定义 | `custom` | 用户自选 | 基于预设微调配色 |
561
+ | 深灰白(默认) | `dark-gray-white` | #27272a |
562
+ | 深蓝白 | `deep-blue-white` | #1e3a5f |
563
+ | 橙白 | `orange-white` | #ea580c |
564
+ | 自定义 | `custom` | 用户自选 |
481
565
 
482
566
  ### 入场主题
483
567
 
@@ -525,60 +609,11 @@ const info = App.getColorScheme();
525
609
 
526
610
  ---
527
611
 
528
- ## 样式规范
529
-
530
- - 默认使用 `rounded-md`(6px)保持系统一致;需要表达品牌感、内容卡片层级、营销视觉或更柔和的界面气质时,可使用更大圆角,但应保持同一页面内的圆角节奏一致
531
- - 自定义组件类名以 `-dg` 结尾(如 `input-dg`、`btn-dg-primary`)
532
-
533
- | 组件 | 类名 |
534
- |---|---|
535
- | 输入框 | `.input-dg` — `rounded-md border-slate-200 text-[13px] h-8 px-3` |
536
- | 主按钮 | `.btn-dg-primary` — Slate-900 风格 |
537
- | 次按钮 | `.btn-dg-secondary` — White/Slate-200 风格 |
538
- | 危险按钮 | `.btn-dg-danger` — White/Red-50 hover 风格 |
539
-
540
- ---
541
-
542
612
  ## 空态容器
543
613
 
544
- 当页面需要展示"暂无数据"、"列表为空"、"加载失败"等提示时,必须保证图标 / 文案 / 操作入口清晰,布局稳定,响应式不塌陷。以下写法是推荐参考,不是唯一结构:
545
-
546
- ```html
547
- <div class="empty-dg">
548
- <i class="fa-regular fa-inbox" style="font-size:36px;margin-bottom:12px;"></i>
549
- <span>暂无数据</span>
550
- </div>
551
- ```
552
-
553
- ```css
554
- .empty-dg {
555
- display: flex;
556
- flex-direction: column;
557
- align-items: center;
558
- justify-content: center;
559
- flex: 1;
560
- padding: 48px 24px;
561
- text-align: center;
562
- color: var(--dg-text-muted, #94a3b8);
563
- font-size: 13px;
564
- }
565
- ```
566
-
567
- 若使用全屏或容器级居中空态,推荐具备:`display:flex` + `flex-direction:column` + `align-items:center` + `flex:1`。
568
-
569
- ### 为什么不能省略
570
-
571
- | 漏写 | 后果 |
572
- |---|---|
573
- | 无 `align-items:center` | block 子元素(图标设了 `display:block`、SVG、img)不会水平居中,`text-align:center` 对 block 无效 |
574
- | 无 `flex:1` | 父容器为 flex 布局时,空态 div 高度=内容高度,`justify-content:center` 无剩余空间可分配,垂直不居中 |
575
- | 无 `flex-direction:column` | 图标和文字横排而非纵排 |
576
-
577
- ### 注意
614
+ 当页面需要展示“暂无数据”“列表为空”“加载失败”等状态时,必须渲染明确的状态容器、文案和后续操作入口,不能让页面空白或只有不可解释的 loading。
578
615
 
579
- - 仅用 `text-align:center` 无法让 block 图标 / SVG / img 水平居中
580
- - 父级是 flex 布局时,容器级空态通常需要 `flex:1` 才能获得可居中的剩余高度
581
- - 可使用其他布局方案,但要验证不同高度、移动端和内容换行时仍然居中稳定
616
+ 若父级是 flex 工作区,空态容器应能承接剩余空间,避免数据区高度塌陷。具体呈现交给本地 UI Skills Agent 自身判断。
582
617
 
583
618
  ---
584
619
 
@@ -590,8 +625,8 @@ const info = App.getColorScheme();
590
625
 
591
626
  | 类型 | 根元素 | 必填属性 | 布局 |
592
627
  |---|---|---|---|
593
- | 顶栏 | `<header>` | `data-nav-position="top"` | 顶部,高度 64px |
594
- | 侧边 | `<aside>` | `data-nav-position="side"` | 左侧,默认展开 260px / 收起 72px,可声明自定义宽度 |
628
+ | 顶栏 | `<header>` | `data-nav-position="top"` | 顶部 |
629
+ | 侧边 | `<aside>` | `data-nav-position="side"` | 左侧,可声明自定义宽度 |
595
630
 
596
631
  ### 必须
597
632
  - 根元素加 `data-nav-position="top|side"`
@@ -613,65 +648,6 @@ const info = App.getColorScheme();
613
648
  - 支持纯数字(按 px 处理)或合法 CSS 长度,如 `220px`、`14rem`、`clamp(200px,20vw,260px)`
614
649
  - 不要在导航脚本里硬编码覆盖 `#nav-container` / `#page-container` 的 `260px`,优先用上述属性让 runtime 同步布局
615
650
 
616
- ### 导航专用颜色 Token
617
-
618
- | 变量 | 用途 |
619
- |---|---|
620
- | `var(--dg-bg-topnav, #fff)` | 导航背景色 |
621
- | `var(--dg-bg-topnav-hover, #f3f4f6)` | 悬停背景色 |
622
- | `var(--dg-text-topnav, #111827)` | 主文字 |
623
- | `var(--dg-text-topnav-muted, #6b7280)` | 次要文字 |
624
- | `var(--dg-border-topnav, #e5e7eb)` | 边框 |
625
- | `var(--dg-bg-nav, #fff)` | 侧边栏背景(side 专用) |
626
-
627
- ### 顶栏模板
628
-
629
- ```html
630
- <header data-nav-position="top" style="
631
- width:100%; height:64px; box-sizing:border-box;
632
- display:flex; align-items:center; justify-content:space-between;
633
- padding:0 24px;
634
- background:var(--dg-bg-topnav,#fff);
635
- border-bottom:1px solid var(--dg-border-topnav,#e5e7eb);
636
- position:sticky; top:0; z-index:50;
637
- ">
638
- <div style="display:flex;align-items:center;gap:16px;">
639
- <span style="font-size:18px;font-weight:700;color:var(--dg-text-topnav,#111827);">应用名称</span>
640
- <nav style="display:flex;align-items:center;gap:4px;">
641
- <a href="/dashboard" data-page-route="/dashboard"
642
- style="padding:8px 14px;border-radius:6px;color:var(--dg-text-topnav-muted,#6b7280);text-decoration:none;font-size:14px;font-weight:500;">
643
- 仪表盘
644
- </a>
645
- </nav>
646
- </div>
647
- <div style="display:flex;align-items:center;gap:8px;"><!-- 右侧操作区 --></div>
648
- </header>
649
- ```
650
-
651
- ### 侧边栏模板
652
-
653
- ```html
654
- <aside data-nav-position="side" data-nav-width="220px" data-nav-collapsed-width="64px" style="
655
- width:100%; height:100%; box-sizing:border-box;
656
- background:var(--dg-bg-nav,#fff);
657
- display:flex; flex-direction:column;
658
- border-right:1px solid var(--dg-border-topnav,#e2e8f0);
659
- ">
660
- <div style="padding:20px;border-bottom:1px solid var(--dg-border-topnav,#e2e8f0);">
661
- <span style="font-size:16px;font-weight:700;color:var(--dg-text-topnav,#111827);">应用名称</span>
662
- </div>
663
- <nav style="flex:1;padding:12px;display:flex;flex-direction:column;gap:4px;overflow-y:auto;">
664
- <a href="/dashboard" data-page-route="/dashboard" style="
665
- display:flex;align-items:center;gap:10px;
666
- padding:10px 12px;border-radius:6px;
667
- color:var(--dg-text-topnav-muted,#6b7280);
668
- text-decoration:none;font-size:14px;font-weight:500;">
669
- 仪表盘
670
- </a>
671
- </nav>
672
- </aside>
673
- ```
674
-
675
651
  ### 常见错误
676
652
 
677
653
  | 错误 | 原因 | 修复 |
@@ -694,14 +670,6 @@ window.location.href = '/login'; // 正确:完整刷新,导航栏重新加
694
670
 
695
671
  ## 加载体验
696
672
 
697
- - 推荐骨架屏占位(`bg-slate-100`)
698
673
  - 禁止因网络请求失败导致整个页面空白
699
674
  - 页面初始化必须先渲染默认状态,再异步填充数据
700
-
701
- ---
702
-
703
- ## 数据列表/表格类页面
704
-
705
- **触发:开发数据列表、表格、分页数据管理类页面,且用户未特别指定 UIUX 时**
706
-
707
- → 可读取 `{{SKILL_DIR}}/rules/data-table.md` 作为参考,用它提升信息密度、筛选效率、状态完整和响应式体验;具体结构仍由业务目标、数据复杂度和页面风格决定。
675
+ - 推荐使用 shadcn Skeleton 对应的 `dg-skeleton`,或使用与 shadcn token 对齐的骨架样式。
@@ -77,9 +77,22 @@ def load_config(root=None):
77
77
  return cfg["server"].rstrip("/"), cfg["token"]
78
78
 
79
79
 
80
+ _UA = (
81
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
82
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
83
+ )
84
+
85
+
80
86
  def fetch(server, token, path):
81
87
  url = f"{server}{path}"
82
- req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
88
+ req = urllib.request.Request(
89
+ url,
90
+ headers={
91
+ "Authorization": f"Bearer {token}",
92
+ "User-Agent": _UA,
93
+ "Accept": "application/json",
94
+ },
95
+ )
83
96
  try:
84
97
  with urllib.request.urlopen(req, timeout=15) as r:
85
98
  return json.loads(r.read())
@@ -67,7 +67,14 @@ def _lessons_reminder(cfg):
67
67
 
68
68
  def api_call(method, server, token, path, body=None):
69
69
  data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
70
- headers = {"Authorization": f"Bearer {token}"}
70
+ headers = {
71
+ "Authorization": f"Bearer {token}",
72
+ "User-Agent": (
73
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
74
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
75
+ ),
76
+ "Accept": "application/json",
77
+ }
71
78
  if data is not None:
72
79
  headers["Content-Type"] = "application/json"
73
80
  req = urllib.request.Request(
@@ -435,6 +442,18 @@ def sync_doc_categories(server, token, ids=None):
435
442
  print(f" {'OK' if ok else 'ERR'} [{name}] category_id={cid}{'' if ok else ' -> ' + _info(ok, status, body)}")
436
443
 
437
444
 
445
+ def _normalize_script_triggers(mode, triggers):
446
+ """归一化 custom_script.triggers。
447
+
448
+ - scheduled:实际调度只读代码里的 @scheduled(...),CLI push 主动回写空 dict,
449
+ 避免把误导性的 cron 元数据继续推回云端。
450
+ - 其他模式:保留本地 triggers 元数据。
451
+ """
452
+ if str(mode or "").lower() == "scheduled":
453
+ return {}
454
+ return triggers
455
+
456
+
438
457
  def sync_custom_scripts(server, token, ids=None):
439
458
  """自定义脚本:从 .draftgo/custom_scripts/index.json + 同目录代码文件回填 code。"""
440
459
  all_items = _load_index("custom_scripts/index.json")
@@ -451,11 +470,13 @@ def sync_custom_scripts(server, token, ids=None):
451
470
  print(f" SKIP [{name}] code_file not found: {code_rel}")
452
471
  continue
453
472
  code = code_path.read_text(encoding="utf-8")
473
+ normalized_triggers = _normalize_script_triggers(it.get("mode"), it.get("triggers"))
454
474
  if sid:
455
475
  # ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
456
476
  payload = {k: it.get(k) for k in (
457
- "name", "description", "triggers", "config", "permission",
477
+ "name", "description", "config", "permission",
458
478
  ) if it.get(k) is not None}
479
+ payload["triggers"] = normalized_triggers
459
480
  payload["code"] = code
460
481
  ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload)
461
482
  if not ok and status == 404:
@@ -464,13 +485,13 @@ def sync_custom_scripts(server, token, ids=None):
464
485
  else:
465
486
  print(f" {'OK' if ok else 'ERR'} [{name}] script_id={sid}{'' if ok else ' -> ' + _info(ok, status, body)}")
466
487
  if not sid:
467
- # ScriptCreate 必填 name/slug/code/mode/triggers
488
+ # ScriptCreate 必填 name/slug/code/mode;scheduled 的 cron 以代码装饰器为准
468
489
  payload = {
469
490
  "name": it.get("name", ""),
470
491
  "slug": it.get("slug", ""),
471
492
  "code": code,
472
493
  "mode": it.get("mode", "route"),
473
- "triggers": it.get("triggers") or {},
494
+ "triggers": normalized_triggers,
474
495
  "config": it.get("config"),
475
496
  "permission": it.get("permission"),
476
497
  "description": it.get("description"),
@@ -57,14 +57,6 @@ design:
57
57
  modules:
58
58
  - name: "模块名"
59
59
  role: "这个模块在系统里的定位(核心/辅助/基座),一句话"
60
- style:
61
- personality: "系统'说话'像谁?一句话描述调性人格"
62
- keywords:
63
- - "设计关键词(如:叙事性交互、情感化、微动效、仪式感)"
64
- do:
65
- - "正面指引(什么该做)"
66
- dont:
67
- - "禁区(什么绝对不做)"
68
60
 
69
61
  # === 第三层:决策日志(只增不删)===
70
62
  decisions:
@@ -174,7 +166,7 @@ Q5: 第一版能用,最少要包含什么?
174
166
  ```
175
167
  1. 读取 .draftgo/story.yaml 全文 → 作为最高优先级上下文
176
168
  2. 解析 design → 理解系统核心流转和模块定位
177
- 3. 解析 decisions(status: active)→ 提取品味/取舍依据
169
+ 3. 解析 decisions(status: active)→ 提取产品取舍依据
178
170
  4. 解析 identity.not → 建立硬边界
179
171
  5. 解析 now → 知道当前重点
180
172
  6. open_questions → 适时主动追问
@@ -217,8 +209,7 @@ AI 在开发过程中检测到"方向性决策"时,主动提议沉淀:
217
209
  **触发条件:**
218
210
  - 开发者明确拒绝了某个建议("不要这样做")
219
211
  - 开发者在两个方案中做了选择
220
- - 开发者表达了对系统调性的偏好
221
- - 开发者描述了 UI/UX 的感觉、风格、动效、文案语气等审美偏好
212
+ - 开发者表达了对系统定位、功能边界或文案语气的偏好
222
213
 
223
214
  **AI 行为:**
224
215
 
@@ -229,23 +220,6 @@ AI 在开发过程中检测到"方向性决策"时,主动提议沉淀:
229
220
  → [是] / [否] / [改改措辞]
230
221
  ```
231
222
 
232
- **style 沉淀(渐进式,不单独提问):**
233
-
234
- 当开发者在 UI/UX 相关对话中表达了审美偏好时(如描述交互节奏、文案风格、动效喜好、视觉调性),AI 提议:
235
-
236
- ```
237
- 从你刚才的描述里,我提炼了几个设计偏好:
238
- - keyword: "叙事性交互"
239
- - do: "关键流程用动效创造起承转合"
240
- - dont: "不用系统腔提示语"
241
-
242
- 要写进 Story 的 design.style 里吗?以后做 UI 我会按这个调性来。
243
-
244
- → [写入] / [调整措辞] / [这次不记]
245
- ```
246
-
247
- style 不是一次性采集的,而是在持续开发中不断丰富——每次用户表达审美偏好都是一次沉淀机会。
248
-
249
223
  ---
250
224
 
251
225
  ## now 的更新
@@ -268,7 +242,6 @@ style 不是一次性采集的,而是在持续开发中不断丰富——每
268
242
  | `identity.*` | 极少 | 仅重大方向转向时(冲突检测中确认) |
269
243
  | `design.overview` | 里程碑级 | 核心流转发生结构性变化时更新 |
270
244
  | `design.modules` | 新增模块时 | 只增/改,不删已有模块(模块下线标注即可) |
271
- | `design.style` | 渐进沉淀 | 不在初始化时采集;开发过程中从用户的描述、选择、审美反馈中提炼 |
272
245
  | `decisions` | 只增不删 | ID 永不复用,supersede 时不删旧的只标记 |
273
246
  | `now.focus` | 周/里程碑 | 对话结束时 AI 提议更新 |
274
247
  | `now.next` | 周 | 同上 |
@@ -34,27 +34,6 @@ design:
34
34
  role: "数据可视化,让开发者快速感知趋势"
35
35
  - name: "数据源配置"
36
36
  role: "基座模块,连接开发者自己的后端 API"
37
- style:
38
- personality: "像一个有品味的年轻朋友,不是客服也不是机器"
39
- keywords:
40
- - "叙事性交互"
41
- - "情感化对话"
42
- - "微动效驱动节奏"
43
- - "仪式感"
44
- - "克制的丰富"
45
- do:
46
- - "用人话跟用户说话,有温度"
47
- - "关键流程要有起承转合的时序编排"
48
- - "用动效创造记忆点(渐显渐隐、灯亮进度、礼花)"
49
- - "重要时刻要有仪式感(注册=相识、完成=庆祝)"
50
- - "空态用一句有温度的话引导,不用冷冰冰的'暂无数据'"
51
- dont:
52
- - "不堆 emoji"
53
- - "不用系统腔提示语(如'操作成功'、'请稍后重试')"
54
- - "不极简到无趣"
55
- - "不炫技到喧宾夺主"
56
- - "敏感场景(支付/删除/错误)不抖机灵"
57
-
58
37
  # === 第三层:决策日志(只增不删)===
59
38
  decisions:
60
39
  - id: D001