draftgo-cli 3.0.29 → 3.0.33
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.
- package/LICENSE +21 -0
- package/README.md +38 -139
- package/package.json +10 -2
- package/resources/skill/SKILL.md +61 -184
- package/resources/skill/init/SKILL.md +18 -66
- package/resources/skill/manifest.json +27 -0
- package/resources/skill/pull/SKILL.md +18 -52
- package/resources/skill/push/SKILL.md +30 -282
- package/resources/skill/references/aihub.md +86 -0
- package/resources/skill/{quickref → references}/api-endpoints.md +39 -13
- package/resources/skill/references/api.json +20248 -0
- package/resources/skill/{quickref → references}/app-api.md +40 -0
- package/resources/skill/{core → references}/architecture.md +2 -2
- package/resources/skill/references/chat-sdk.md +201 -0
- package/resources/skill/references/custom-services.md +308 -0
- package/resources/skill/{specs → references}/data.md +5 -5
- package/resources/skill/{rules → references}/frontend.md +41 -11
- package/resources/skill/{core → references}/modules.md +7 -5
- package/resources/skill/references/parallel.md +48 -0
- package/resources/skill/{specs → references}/runtime.md +1 -1
- package/resources/skill/scripts/draftgo_push.py +80 -12
- package/resources/skill/story/SKILL.md +11 -16
- package/src/cli.js +13 -7
- package/src/commandRegistry.js +34 -0
- package/src/commands/api.js +153 -8
- package/src/commands/help.js +24 -29
- package/src/commands/init.js +17 -18
- package/src/commands/local.js +9 -3
- package/src/commands/sync.js +1 -1
- package/src/commands/update.js +40 -12
- package/src/index.js +13 -57
- package/src/localdev/compose.js +44 -200
- package/src/localdev/index.js +116 -216
- package/src/localdev/mysqlClient.js +12 -9
- package/src/localdev/services.js +163 -0
- package/src/projectConfig.js +1 -1
- package/src/projectMap.js +17 -80
- package/src/skill.js +1 -1
- package/src/updateCheck.js +2 -12
- package/resources/skill/practices/anti-patterns.md +0 -80
- package/resources/skill/practices/best-practices.md +0 -60
- package/resources/skill/practices/dev-declaration.md +0 -114
- package/resources/skill/quickref/api.json +0 -17784
- package/resources/skill/rules/dev-workflow.md +0 -749
- package/resources/skill/rules/parallel.md +0 -263
- package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
- package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
- package/resources/skill/specs/custom-services.md +0 -199
- package/src/commands/doctor.js +0 -54
- package/src/commands/new.js +0 -186
- package/src/commands/projectScript.js +0 -37
- package/src/commands/upgrade.js +0 -52
- /package/resources/skill/{specs → references}/db-relations.md +0 -0
- /package/resources/skill/{rules → references}/debugging-syntax.md +0 -0
- /package/resources/skill/{specs → references}/security.md +0 -0
- /package/resources/skill/{specs → references}/ui-protocol.md +0 -0
|
@@ -8,13 +8,11 @@ version: 2.0.0
|
|
|
8
8
|
|
|
9
9
|
> 根 `SKILL.md` 是完整核心规则源;本文件只展开前端实现方法。若表述冲突,以根 `SKILL.md` 为准,不额外叠加固定验收动作。
|
|
10
10
|
|
|
11
|
-
> 本文件只规定前端**操作性规则**,已剥离的内容见:
|
|
12
|
-
> 架构原理 → `{{SKILL_DIR}}/
|
|
13
|
-
> App API / Token / 路由 / 运行时 → `{{SKILL_DIR}}/
|
|
14
|
-
> 动态 DB / filters → `{{SKILL_DIR}}/
|
|
15
|
-
> 开发禁区 → `{{SKILL_DIR}}/
|
|
16
|
-
|
|
17
|
-
新建/重做前端界面、明显 UI 设计任务或需要整体视觉判断时,若本地 Agent 存在前端 UI Skills,优先调用;小修、文案、链接、局部样式调整不必调用。
|
|
11
|
+
> 本文件只规定前端**操作性规则**,已剥离的内容见:
|
|
12
|
+
> 架构原理 → `{{SKILL_DIR}}/references/architecture.md`
|
|
13
|
+
> App API / Token / 路由 / 运行时 → `{{SKILL_DIR}}/references/runtime.md` · `{{SKILL_DIR}}/references/app-api.md`
|
|
14
|
+
> 动态 DB / filters → `{{SKILL_DIR}}/references/data.md`
|
|
15
|
+
> 开发禁区 → `{{SKILL_DIR}}/references/security.md`
|
|
18
16
|
|
|
19
17
|
---
|
|
20
18
|
|
|
@@ -321,7 +319,7 @@ antd Modal:`width="min(90vw, 520px)"`;抽屉窄屏优先 `placement="bottom"
|
|
|
321
319
|
- 导航栏通常要同时考虑未登录 / 已登录 / 管理员三种状态,以及收起 / 展开状态;普通用户不显示管理后台入口,管理员额外显示管理后台入口。
|
|
322
320
|
- 管理端侧边栏通常基于现有内置侧边栏修改,新增业务管理路由优先追加或局部调整;删除系统内置页面入口前先确认影响。
|
|
323
321
|
- 业务页面内可以做局部二级导航或侧边栏,但必须考虑外部导航已存在且可单独配置,避免重复渲染全局导航。
|
|
324
|
-
-
|
|
322
|
+
- 系统内置页面通常不改;确需修改登录、设置、权限、用户、系统配置等页面时,先说明影响、验证方式和保留的管理员能力。
|
|
325
323
|
- 页面风格不要照搬管理侧内置页面;业务前台按业务用户和品牌语境设计,管理侧按操作效率和信息密度设计。
|
|
326
324
|
|
|
327
325
|
### 新增页面绑定(强制)
|
|
@@ -384,6 +382,7 @@ antd Modal:`width="min(90vw, 520px)"`;抽屉窄屏优先 `placement="bottom"
|
|
|
384
382
|
| `/assets/vendor/prism/prism.min.js` | 轻量代码高亮(Prism.js) |
|
|
385
383
|
| `/assets/vendor/dompurify/purify.min.js` | HTML 净化,渲染用户输入前必须过一遍 |
|
|
386
384
|
| `/assets/vendor/html2canvas/html2canvas.min.js` | html2canvas 1.4.1,页面截图 / DOM 导出图片 |
|
|
385
|
+
| `/assets/draftgo-chat.js` | DraftGo Chat SDK 完整版,注册 `<dg-chat>` 与 `DraftGoChat` |
|
|
387
386
|
| `/assets/vendor/gsap/gsap.min.js` | GSAP 核心(先于其他 GSAP 插件加载) |
|
|
388
387
|
| `/assets/vendor/gsap/ScrollTrigger.min.js` | GSAP ScrollTrigger |
|
|
389
388
|
| `/assets/vendor/gsap/ScrollToPlugin.min.js` | GSAP ScrollTo |
|
|
@@ -421,6 +420,8 @@ AI 模型 / Provider 品牌图标可以使用 LobeHub 静态图标的 npmmirror
|
|
|
421
420
|
|
|
422
421
|
Ant Design 5.29.3 已内置为 UMD 包(`window.antd`),可优先选择使用。
|
|
423
422
|
|
|
423
|
+
> `Segmented` 的默认样式视觉厚重、组件库感过强,禁止原样用于成品界面;优先根据语义改用 `Tabs`、`Radio.Group` 等更合适的控件,确需使用时必须通过 `ConfigProvider`、design token 或组件公开 API 调整为与页面一致的轻量样式。
|
|
424
|
+
|
|
424
425
|
### ① 依赖加载顺序(缺一不可,顺序不可乱)
|
|
425
426
|
|
|
426
427
|
```html
|
|
@@ -697,18 +698,47 @@ App.setColorScheme('deep-blue-white');
|
|
|
697
698
|
|
|
698
699
|
## AIHub 页面 SDK
|
|
699
700
|
|
|
701
|
+
新页面需要 AI 对话 UI 时,加载完整版脚本并使用原生 `<dg-chat>`:
|
|
702
|
+
|
|
703
|
+
```html
|
|
704
|
+
<script src="/assets/draftgo-chat.js"></script>
|
|
705
|
+
<dg-chat protocol="draftgo-agent" agent-id="AGENT_ID"></dg-chat>
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
需要动态创建时使用 `DraftGoChat.create()`:
|
|
709
|
+
|
|
700
710
|
```javascript
|
|
701
|
-
|
|
711
|
+
const chat = DraftGoChat.create('#chat-host', {
|
|
712
|
+
protocol: 'draftgo-agent',
|
|
713
|
+
agentId,
|
|
714
|
+
surface: 'inline',
|
|
715
|
+
view: 'conversation'
|
|
716
|
+
});
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
完整配置、协议、事件、历史、扩展与安全约束见 `references/chat-sdk.md`。
|
|
720
|
+
|
|
721
|
+
`DraftGoAI` 是同一脚本提供的旧代码兼容门面,只用于没有对话 UI 的轻量调用或图片生成:
|
|
722
|
+
|
|
723
|
+
```javascript
|
|
724
|
+
// 聊天(默认流式;onDelta(delta, full) 每个增量触发,Promise resolve 完整文本)
|
|
702
725
|
const text = await DraftGoAI.chat(agentId, '你好', (delta, full) => render(full));
|
|
703
726
|
|
|
727
|
+
// 多轮:传 sessionId 复用会话(→ body.session_id);Agent 开启“持续对话”后服务端续写历史。不传 = 无状态单轮
|
|
728
|
+
await DraftGoAI.chat(agentId, '接着上一条', handler, { sessionId: threadKey });
|
|
729
|
+
|
|
704
730
|
// 图片生成(必须用 /images 接口,不要用 /chat)
|
|
705
731
|
const result = await DraftGoAI.images(agentId, '生成主图', { size: '1024x1024', n: 1 });
|
|
706
732
|
|
|
707
733
|
// 用户选模型
|
|
708
734
|
const { user_selectable, models } = await DraftGoAI.getSelectableModels(agentId);
|
|
709
|
-
await DraftGoAI.chat(agentId, '你好', handler, { model: selectedModel });
|
|
735
|
+
await DraftGoAI.chat(agentId, '你好', handler, { model: selectedModel, sessionId: threadKey });
|
|
710
736
|
```
|
|
711
737
|
|
|
738
|
+
- 使用 `DraftGoAI` 前同样必须先加载 `/assets/draftgo-chat.js`;壳层不会默认注入。
|
|
739
|
+
- `DraftGoAI.chat` 内部创建隐藏 `<dg-chat>`,始终流式。新页面的可见对话、附件、模型选择器、推理、历史和重生成直接使用 `<dg-chat>`。
|
|
740
|
+
- Agent 的能力与 `spec` 字段(工具/子智能体/记忆/结构化输出/多模态/持续对话/ttft failover)见 `references/aihub.md`。
|
|
741
|
+
|
|
712
742
|
---
|
|
713
743
|
|
|
714
744
|
## 退出登录
|
|
@@ -755,4 +785,4 @@ const dbRes = await App.get('db/order', params);
|
|
|
755
785
|
const q = (window.__DG_ROUTE_CONTEXT__ || window.__DG_GET_ROUTE_CONTEXT__?.() || App?.getCurrentRouteContext?.() || { query: {} }).query;
|
|
756
786
|
```
|
|
757
787
|
|
|
758
|
-
完整 API 表 → `{{SKILL_DIR}}/
|
|
788
|
+
完整 API 表 → `{{SKILL_DIR}}/references/app-api.md`
|
|
@@ -12,7 +12,7 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
|
|
|
12
12
|
| 导航栏 | 数据库 HTML(`navigation.html`) | `.draftgo/navigations/` |
|
|
13
13
|
| 动态 DB | db_meta 定义 schema + `/api/db/{type}` 操作数据 | `.draftgo/db_meta/` |
|
|
14
14
|
| 自定义服务 | Go `Register` 服务,支持 route/event/scheduled 混合注册 | `.draftgo/custom_scripts/` |
|
|
15
|
-
| AIHub | 配置 AI Agent
|
|
15
|
+
| AIHub | 配置 AI Agent(模型/编排/能力/子智能体/记忆);页面对话使用 `<dg-chat>`,图片模式使用 `DraftGoAI.images`;见 `references/chat-sdk.md` 与 `references/aihub.md` | `.draftgo/aihub/` |
|
|
16
16
|
| 文档中心 | Markdown 文章 + 分类树 | `.draftgo/docs/articles/` |
|
|
17
17
|
| 系统配置 | KV 存储,含全局前端层槽位 | `.draftgo/system_config/` |
|
|
18
18
|
|
|
@@ -35,11 +35,13 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
|
|
|
35
35
|
→ 仅需 KV → sys_config(category 自定义)
|
|
36
36
|
|
|
37
37
|
要调用 AI?
|
|
38
|
-
→ 聊天/问答 → AIHub +
|
|
38
|
+
→ 聊天/问答 UI → AIHub + `<dg-chat>`(自动隔离 thread/session)
|
|
39
|
+
→ 无 UI 的旧代码文本调用 → AIHub + DraftGoAI.chat()(兼容门面)
|
|
39
40
|
→ 图片生成 → AIHub + DraftGoAI.images()
|
|
41
|
+
→ 需要工具/子智能体/记忆/结构化输出/多模态 → 都是 Agent spec 开关,见 references/aihub.md
|
|
40
42
|
|
|
41
43
|
要调用第三方服务?
|
|
42
|
-
→ 自定义服务(用 `
|
|
44
|
+
→ 自定义服务(用 `draftgo.HTTP` 请求;需要时可用 route/event/scheduled 加工)
|
|
43
45
|
|
|
44
46
|
要展示内容文档?
|
|
45
47
|
→ 文档中心(Markdown + 分类树)
|
|
@@ -53,7 +55,7 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
|
|
|
53
55
|
|
|
54
56
|
## 自定义服务边界
|
|
55
57
|
|
|
56
|
-
- 新服务使用 Go `Register(app *sdk.App)`
|
|
58
|
+
- 新服务使用 Go `Register(app *sdk.App)` 自动注册路由、事件和定时任务;完整语言、`draftgo` 和 SDK 契约见 `references/custom-services.md`。
|
|
57
59
|
- `route`:对外暴露 HTTP 端点,运行时路径为 `/api/x/{slug}/{path}`;同一 Go 服务可用多个 `app.Route(method, path, handler)` 注册多个端点,推送后要真实请求验证。
|
|
58
60
|
- `event`:响应平台事件,如 `db.created` / `db.updated` / `user.registered`;用 `app.On(event, handler)` 注册,可为同一事件注册多个 handler。
|
|
59
61
|
- `scheduled`:用 `app.Schedule("分 时 日 月 周", handler)` 注册 cron;同一服务可声明多个定时 handler。
|
|
@@ -61,4 +63,4 @@ read_when: 评估功能可行性时 · 选择开发路径时 · 进入新项目
|
|
|
61
63
|
- 管理面由角色 RBAC 的 `scripts:*` 动作控制;Route 调用面继续由每个服务的 `permission` / `route_security` 独立控制,不要把两层权限混为一谈。
|
|
62
64
|
- Go 服务在独立子进程中构建/运行;只有可信角色才能获得 `scripts:create` / `scripts:update`。
|
|
63
65
|
- 自定义服务适合服务端加工、鉴权后聚合、第三方回调、定时任务和事件响应;普通 CRUD 管理界面优先用“页面 + 动态 DB”,不要把所有业务后台都塞进 route 脚本。
|
|
64
|
-
- 脚本内读动态 DB 用 `
|
|
66
|
+
- 脚本内读动态 DB 用 `draftgo.DB.Query(type, sdk.QueryOptions{...})`;返回 `sdk.QueryResult`,分页和筛选见 `references/custom-services.md`。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# DraftGo 并行执行
|
|
2
|
+
|
|
3
|
+
## 资源依赖
|
|
4
|
+
|
|
5
|
+
先为每个工作单元记录:
|
|
6
|
+
|
|
7
|
+
- `depends`:开始前必须完成的工作单元。
|
|
8
|
+
- `resource_lock`:本单元会修改的文件或 DraftGo 资源。
|
|
9
|
+
- `evidence`:完成后返回的验证证据。
|
|
10
|
+
|
|
11
|
+
按以下关系建立依赖:
|
|
12
|
+
|
|
13
|
+
| 资源关系 | 执行顺序 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 多个实现依赖同一 schema、API、权限或路由 | 先确定共享契约,再并行实现消费者。 |
|
|
16
|
+
| 两个工作单元修改同一文件或资源 | 合并为一个单元,或安排到连续 wave。 |
|
|
17
|
+
| 页面与对应导航入口 | 页面 route 确定后接入导航。 |
|
|
18
|
+
| 不同文件且没有数据或接口依赖 | 放入同一 wave 并行执行。 |
|
|
19
|
+
|
|
20
|
+
## 工作单元
|
|
21
|
+
|
|
22
|
+
分发内容包括:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
objective: 要完成的结果
|
|
26
|
+
inputs: 当前资源和共享契约
|
|
27
|
+
resource_lock: [允许修改的文件或资源]
|
|
28
|
+
evidence: 返回的检查或运行证据
|
|
29
|
+
delivery_owner: main
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
工作单元返回修改结果、验证证据和阻塞信息。主代理维护共享契约、推进 wave、汇总资源并执行交付。
|
|
33
|
+
|
|
34
|
+
## Wave 执行
|
|
35
|
+
|
|
36
|
+
1. 确定 schema、服务端点、权限、route 和导航结构等共享契约。
|
|
37
|
+
2. 将 `depends` 已满足且 `resource_lock` 不冲突的工作单元放入同一 wave。
|
|
38
|
+
3. 并行执行当前 wave,汇总后回读所有变更资源。
|
|
39
|
+
4. 处理接口接线和入口绑定,再启动依赖已满足的下一 wave。
|
|
40
|
+
5. 统一运行 `draftgo check`、必要的 API/UI 验证,并按交付意图执行批量推送。
|
|
41
|
+
|
|
42
|
+
某个工作单元失败时,其依赖项等待处理;其余独立分支继续汇总。发现资源锁冲突时,将冲突单元合并或移到后续 wave。
|
|
43
|
+
|
|
44
|
+
批量交付示例:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
draftgo auto-push --batch pages 1,2,3 nav 4 custom_scripts 7
|
|
48
|
+
```
|
|
@@ -36,7 +36,7 @@ read_when: 需要理解运行时机制时 · 处理 token/路由/事件相关问
|
|
|
36
36
|
|
|
37
37
|
`app` 组成:`api.js`(请求)+ `feedback.js`(弹窗/Toast)+ `runtime.js`(路由/主题)+ `i18n.js`(国际化)+ state(currentUser/isAdmin/config/theme)
|
|
38
38
|
|
|
39
|
-
完整 API 见 `{{SKILL_DIR}}/
|
|
39
|
+
完整 API 见 `{{SKILL_DIR}}/references/app-api.md`
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
@@ -17,13 +17,14 @@ DraftGo Push Script
|
|
|
17
17
|
python draftgo_push.py roles [role_id ...]
|
|
18
18
|
python draftgo_push.py users [user_id ...]
|
|
19
19
|
"""
|
|
20
|
-
import json, sys, re
|
|
20
|
+
import json, sys, re, datetime
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
import urllib.request, urllib.error
|
|
23
23
|
|
|
24
24
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
25
25
|
PROBE_ROUTES = False
|
|
26
26
|
DRY_RUN = False
|
|
27
|
+
RUN_FAILURES = 0
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def find_project_root(start: Path) -> Path:
|
|
@@ -67,7 +68,9 @@ def _lessons_reminder(cfg):
|
|
|
67
68
|
print(" 未命中则跳过。")
|
|
68
69
|
|
|
69
70
|
|
|
70
|
-
def api_call(method, server, token, path, body=None):
|
|
71
|
+
def api_call(method, server, token, path, body=None, acceptable_errors=()):
|
|
72
|
+
"""发起 API 调用,并记录调用方不会恢复的失败。"""
|
|
73
|
+
global RUN_FAILURES
|
|
71
74
|
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
|
|
72
75
|
headers = {
|
|
73
76
|
"Authorization": f"Bearer {token}",
|
|
@@ -89,8 +92,12 @@ def api_call(method, server, token, path, body=None):
|
|
|
89
92
|
with urllib.request.urlopen(req, timeout=15) as r:
|
|
90
93
|
return True, r.status, r.read().decode("utf-8", errors="replace")
|
|
91
94
|
except urllib.error.HTTPError as e:
|
|
92
|
-
|
|
95
|
+
body_text = e.read().decode(errors="replace")
|
|
96
|
+
if e.code not in acceptable_errors:
|
|
97
|
+
RUN_FAILURES += 1
|
|
98
|
+
return False, e.code, body_text
|
|
93
99
|
except Exception as e:
|
|
100
|
+
RUN_FAILURES += 1
|
|
94
101
|
return False, None, str(e)
|
|
95
102
|
|
|
96
103
|
|
|
@@ -201,6 +208,57 @@ def _response_items(body):
|
|
|
201
208
|
return []
|
|
202
209
|
|
|
203
210
|
|
|
211
|
+
def _response_item(body):
|
|
212
|
+
"""解析单资源响应的统一信封。"""
|
|
213
|
+
try:
|
|
214
|
+
raw = json.loads(body)
|
|
215
|
+
except Exception:
|
|
216
|
+
return None
|
|
217
|
+
if isinstance(raw, dict) and "data" in raw:
|
|
218
|
+
raw = raw["data"]
|
|
219
|
+
return raw if isinstance(raw, dict) else None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _cloud_version_allows_push(server, token, path, local_item, label):
|
|
223
|
+
"""云端优先:本地 pull 基线落后于云端时禁止静默覆盖。
|
|
224
|
+
|
|
225
|
+
没有历史 updated_at 的旧缓存保持兼容;404 由调用方按默认重建策略处理。
|
|
226
|
+
"""
|
|
227
|
+
global RUN_FAILURES
|
|
228
|
+
baseline = local_item.get("updated_at")
|
|
229
|
+
if not baseline:
|
|
230
|
+
return True
|
|
231
|
+
ok, status, body = api_call("GET", server, token, path, acceptable_errors=(404,))
|
|
232
|
+
if not ok and status == 404:
|
|
233
|
+
return True
|
|
234
|
+
if not ok:
|
|
235
|
+
return False
|
|
236
|
+
remote = _response_item(body)
|
|
237
|
+
remote_updated = remote.get("updated_at") if remote else None
|
|
238
|
+
if remote_updated and str(remote_updated) != str(baseline):
|
|
239
|
+
RUN_FAILURES += 1
|
|
240
|
+
conflict_dir = DEFAULT_ROOT / ".draftgo" / "sync-conflicts"
|
|
241
|
+
conflict_dir.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
identity = str(local_item.get("id") or label).replace("/", "_").replace("\\", "_")
|
|
243
|
+
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
244
|
+
conflict_path = conflict_dir / f"{identity}-{stamp}.json"
|
|
245
|
+
conflict_path.write_text(json.dumps({
|
|
246
|
+
"kind": "remote-newer",
|
|
247
|
+
"resource_path": path,
|
|
248
|
+
"baseline_updated_at": baseline,
|
|
249
|
+
"remote_updated_at": remote_updated,
|
|
250
|
+
"local": local_item,
|
|
251
|
+
"remote": remote,
|
|
252
|
+
}, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
|
253
|
+
print(
|
|
254
|
+
f" ERR [{label}] 云端版本已变化(local={baseline}, remote={remote_updated});"
|
|
255
|
+
f"云端优先,已保存冲突快照:{conflict_path.relative_to(DEFAULT_ROOT)}。",
|
|
256
|
+
file=sys.stderr,
|
|
257
|
+
)
|
|
258
|
+
return False
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
|
|
204
262
|
def _recover_missing_index_entries(
|
|
205
263
|
server, token, items, ids, id_key, type_name, rel_index, endpoint,
|
|
206
264
|
file_spec=None, drop_fields=(),
|
|
@@ -359,7 +417,9 @@ def sync_pages(server, token, ids=None):
|
|
|
359
417
|
"value": {"html": html},
|
|
360
418
|
}
|
|
361
419
|
if pid:
|
|
362
|
-
|
|
420
|
+
if not _cloud_version_allows_push(server, token, f"/api/pages/{pid}", page, title):
|
|
421
|
+
continue
|
|
422
|
+
ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload, acceptable_errors=(404,))
|
|
363
423
|
# PUT 404:本地 id 与云端不一致(删了重建 / 跨环境),按 route 自动创建
|
|
364
424
|
if not ok and status == 404:
|
|
365
425
|
print(f" WARN [{title}] page_id={pid} 不存在,尝试创建")
|
|
@@ -406,7 +466,7 @@ def sync_db_meta(server, token, ids=None):
|
|
|
406
466
|
"extra": meta.get("extra"),
|
|
407
467
|
}
|
|
408
468
|
if mid:
|
|
409
|
-
ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload)
|
|
469
|
+
ok, status, body = api_call("PUT", server, token, f"/api/db-meta/{mid}", payload, acceptable_errors=(404,))
|
|
410
470
|
# 本地 index 的 id 与云端不一致(删了重建 / 跨环境同步)时 PUT 404,按 type 创建
|
|
411
471
|
if not ok and status == 404:
|
|
412
472
|
print(f" WARN [{label}] db_meta_id={mid} 不存在,尝试按 type 创建")
|
|
@@ -451,7 +511,7 @@ def sync_nav(server, token, ids=None):
|
|
|
451
511
|
continue
|
|
452
512
|
html = html_file.read_text(encoding="utf-8")
|
|
453
513
|
if nid:
|
|
454
|
-
ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html})
|
|
514
|
+
ok, status, body = api_call("PUT", server, token, f"/api/navigations/{nid}", {"html": html}, acceptable_errors=(404,))
|
|
455
515
|
if not ok and status == 404:
|
|
456
516
|
print(f" WARN [{name}] nav_id={nid} 不存在,尝试创建")
|
|
457
517
|
nid = None
|
|
@@ -502,7 +562,7 @@ def sync_aihub(server, token, ids=None):
|
|
|
502
562
|
"tags", "describe", "permission", "status",
|
|
503
563
|
) if it.get(k) is not None}
|
|
504
564
|
if iid:
|
|
505
|
-
ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload)
|
|
565
|
+
ok, status, body = api_call("PUT", server, token, f"/api/aihub/{iid}", payload, acceptable_errors=(404,))
|
|
506
566
|
if not ok and status == 404:
|
|
507
567
|
print(f" WARN [{name}] aihub_id={iid} 不存在,尝试创建")
|
|
508
568
|
iid = None
|
|
@@ -553,10 +613,10 @@ def sync_system_config(server, token, keys=None):
|
|
|
553
613
|
# 前端全局层属于系统默认配置,默认字段的描述/分类/状态由基座维护。
|
|
554
614
|
# 推送时只更新值,避免旧 index 中的元信息触发“系统默认字段不允许修改字段描述”。
|
|
555
615
|
payload = value_payload if _is_frontend_global_config(it) else meta_payload
|
|
556
|
-
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload)
|
|
616
|
+
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", payload, acceptable_errors=(404,))
|
|
557
617
|
if not ok and _is_protected_system_config_error(body) and payload != value_payload:
|
|
558
618
|
print(f" WARN [{ck}] 系统默认字段元信息受保护,改为仅推送 config_value")
|
|
559
|
-
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", value_payload)
|
|
619
|
+
ok, status, body = api_call("PUT", server, token, f"/api/system/{ck}", value_payload, acceptable_errors=(404,))
|
|
560
620
|
if not ok and status == 404:
|
|
561
621
|
print(f" WARN [{ck}] system_config 不存在,尝试创建")
|
|
562
622
|
create_payload = _system_config_create_payload(ck, it, value)
|
|
@@ -675,7 +735,7 @@ def sync_docs(server, token, ids=None):
|
|
|
675
735
|
payload["content"] = content
|
|
676
736
|
payload["content_type"] = "html"
|
|
677
737
|
if aid:
|
|
678
|
-
ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload)
|
|
738
|
+
ok, status, body = api_call("PUT", server, token, f"/api/docs/articles/{aid}", payload, acceptable_errors=(404,))
|
|
679
739
|
if not ok and status == 404:
|
|
680
740
|
print(f" WARN [{title}] article_id={aid} 不存在,尝试创建")
|
|
681
741
|
aid = None
|
|
@@ -716,7 +776,7 @@ def sync_doc_categories(server, token, ids=None):
|
|
|
716
776
|
"name", "slug", "description", "icon", "parent_id", "sort_order", "status",
|
|
717
777
|
) if it.get(k) is not None}
|
|
718
778
|
if cid:
|
|
719
|
-
ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload)
|
|
779
|
+
ok, status, body = api_call("PUT", server, token, f"/api/docs/categories/{cid}", payload, acceptable_errors=(404,))
|
|
720
780
|
if not ok and status == 404:
|
|
721
781
|
print(f" WARN [{name}] category_id={cid} 不存在,尝试创建")
|
|
722
782
|
cid = None
|
|
@@ -783,12 +843,14 @@ def sync_custom_scripts(server, token, ids=None):
|
|
|
783
843
|
continue
|
|
784
844
|
code = code_path.read_text(encoding="utf-8")
|
|
785
845
|
if sid:
|
|
846
|
+
if not _cloud_version_allows_push(server, token, f"/api/scripts/{sid}", it, name):
|
|
847
|
+
continue
|
|
786
848
|
# ScriptUpdate 接受字段(不含 slug/mode,避免误改启停/路由)
|
|
787
849
|
payload = {k: it.get(k) for k in (
|
|
788
850
|
"name", "description", "config", "permission", "go_mod", "go_sum",
|
|
789
851
|
) if it.get(k) is not None}
|
|
790
852
|
payload["code"] = code
|
|
791
|
-
ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload)
|
|
853
|
+
ok, status, body = api_call("PUT", server, token, f"/api/scripts/{sid}", payload, acceptable_errors=(404,))
|
|
792
854
|
if not ok and status == 404:
|
|
793
855
|
print(f" WARN [{name}] script_id={sid} 不存在,尝试创建")
|
|
794
856
|
sid = None
|
|
@@ -938,6 +1000,9 @@ def main():
|
|
|
938
1000
|
server, token, cfg = load_config()
|
|
939
1001
|
run_batch(server, token, args[1:])
|
|
940
1002
|
_lessons_reminder(cfg)
|
|
1003
|
+
if RUN_FAILURES:
|
|
1004
|
+
print(f"ERR: push finished with {RUN_FAILURES} failed request(s)", file=sys.stderr)
|
|
1005
|
+
sys.exit(1)
|
|
941
1006
|
return
|
|
942
1007
|
|
|
943
1008
|
if args[0] not in HANDLERS:
|
|
@@ -948,6 +1013,9 @@ def main():
|
|
|
948
1013
|
server, token, cfg = load_config()
|
|
949
1014
|
HANDLERS[mode](server, token, ids)
|
|
950
1015
|
_lessons_reminder(cfg)
|
|
1016
|
+
if RUN_FAILURES:
|
|
1017
|
+
print(f"ERR: push finished with {RUN_FAILURES} failed request(s)", file=sys.stderr)
|
|
1018
|
+
sys.exit(1)
|
|
951
1019
|
|
|
952
1020
|
|
|
953
1021
|
if __name__ == "__main__":
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: draftgo-story
|
|
3
|
-
description: "Use this skill to build, load, evolve, or conflict-check the project's Story file (.draftgo/story.yaml).
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
allowed-tools: Read, Write, Glob, Bash(find:*)
|
|
3
|
+
description: "Use this skill to build, load, evolve, or conflict-check the project's Story file (.draftgo/story.yaml). Use it when the user requests product-story work, when an existing Story conflicts with a requested product decision, or when preserving a durable product decision is valuable."
|
|
6
4
|
---
|
|
7
5
|
|
|
8
6
|
# DraftGo Story System
|
|
9
7
|
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
> Story 不是文档,不是规范,是创作者对"这个系统是什么、为什么存在、不做什么"的持续叙述。
|
|
13
|
-
> 文件只是理解的副产品。
|
|
8
|
+
> Story 记录产品身份、设计总述、长期决策和当前方向。项目已有 Story 且请求触及定位或既有决策时加载;用户要求或形成需要长期保留的产品决策时创建或更新。
|
|
14
9
|
|
|
15
10
|
---
|
|
16
11
|
|
|
@@ -18,11 +13,11 @@ allowed-tools: Read, Write, Glob, Bash(find:*)
|
|
|
18
13
|
|
|
19
14
|
| 原则 | 含义 |
|
|
20
15
|
|------|------|
|
|
21
|
-
|
|
|
16
|
+
| Decision record | 记录会影响后续取舍的产品定位与决策 |
|
|
22
17
|
| AI 维护,人确认 | 开发者只聊天,AI 负责提炼和写入 |
|
|
23
18
|
| 三层结构 | 身份(稳) → 决策(只增) → 当前(频变) |
|
|
24
19
|
| 决策日志最值钱 | 取舍依据是代码永远表达不了的信息 |
|
|
25
|
-
| 工具无关 |
|
|
20
|
+
| 工具无关 | 使用可移植的 YAML 文件 |
|
|
26
21
|
|
|
27
22
|
---
|
|
28
23
|
|
|
@@ -43,7 +38,7 @@ identity:
|
|
|
43
38
|
what: "一句话讲清这是什么,不超过 30 字"
|
|
44
39
|
why: |
|
|
45
40
|
解决什么具体痛点,给谁用。
|
|
46
|
-
|
|
41
|
+
用两三句具体描述说明使用者和场景。
|
|
47
42
|
not:
|
|
48
43
|
- "不是 XX"
|
|
49
44
|
- "不变成 YY"
|
|
@@ -71,7 +66,7 @@ decisions:
|
|
|
71
66
|
# === 第四层:当前方向(频繁更新)===
|
|
72
67
|
now:
|
|
73
68
|
focus: "当前阶段在做什么"
|
|
74
|
-
next: "
|
|
69
|
+
next: "下一步的大致方向"
|
|
75
70
|
open_questions:
|
|
76
71
|
- "还没想清楚的事"
|
|
77
72
|
```
|
|
@@ -81,12 +76,12 @@ now:
|
|
|
81
76
|
## 触发协议
|
|
82
77
|
|
|
83
78
|
```
|
|
84
|
-
|
|
79
|
+
需要 Story 时:
|
|
85
80
|
|
|
86
81
|
1. 静默检查 .draftgo/story.yaml
|
|
87
82
|
2. 分流:
|
|
88
|
-
├─ 不存在 →
|
|
89
|
-
├─ maturity = seed →
|
|
83
|
+
├─ 不存在 → 用户要求创建或需要沉淀关键决策时,进入「构建流程」
|
|
84
|
+
├─ maturity = seed → 加载并把不确定项作为后续判断背景
|
|
90
85
|
└─ growing / stable → 静默加载,直接开始
|
|
91
86
|
3. 整个对话期间,Story 作为最高优先级上下文锚定
|
|
92
87
|
```
|
|
@@ -141,9 +136,9 @@ now:
|
|
|
141
136
|
```
|
|
142
137
|
输出:[ 检测到全新项目,先聊一下你想做什么 ]
|
|
143
138
|
|
|
144
|
-
|
|
139
|
+
通过简短对话采集,信息足够后即进入提炼:
|
|
145
140
|
|
|
146
|
-
Q1:
|
|
141
|
+
Q1: 这个系统的整体定位和存在理由是什么?
|
|
147
142
|
Q2: 谁会用它?什么场景下用?
|
|
148
143
|
Q3: 类似的东西可能有哪些?你想做出什么不同?
|
|
149
144
|
Q4: 大致有哪些模块/功能块?它们之间怎么串起来?
|
package/src/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ const VALUE_FLAGS = new Set([
|
|
|
24
24
|
|
|
25
25
|
function parse(argv) {
|
|
26
26
|
const args = argv.slice();
|
|
27
|
-
const out = { command: null, positional: [], flags: {} };
|
|
27
|
+
const out = { command: null, positional: [], flags: {}, errors: [] };
|
|
28
28
|
while (args.length) {
|
|
29
29
|
const a = args.shift();
|
|
30
30
|
if (a === '--') { out.positional.push(...args); break; }
|
|
@@ -32,16 +32,22 @@ function parse(argv) {
|
|
|
32
32
|
const raw = a.slice(2);
|
|
33
33
|
const eq = raw.indexOf('=');
|
|
34
34
|
if (eq >= 0) {
|
|
35
|
-
|
|
35
|
+
const key = raw.slice(0, eq);
|
|
36
|
+
const value = raw.slice(eq + 1);
|
|
37
|
+
if (VALUE_FLAGS.has(key) && !value) {
|
|
38
|
+
out.errors.push(`选项 --${key} 需要一个值。`);
|
|
39
|
+
} else {
|
|
40
|
+
out.flags[key] = value;
|
|
41
|
+
}
|
|
36
42
|
} else {
|
|
37
43
|
const next = args[0];
|
|
38
|
-
if (
|
|
39
|
-
//
|
|
40
|
-
if (
|
|
44
|
+
if (VALUE_FLAGS.has(raw)) {
|
|
45
|
+
// A declared value flag must never silently become boolean true.
|
|
46
|
+
if (next === undefined || next === '--' || next.startsWith('--')) {
|
|
47
|
+
out.errors.push(`选项 --${raw} 需要一个值。`);
|
|
48
|
+
} else {
|
|
41
49
|
out.flags[raw] = next;
|
|
42
50
|
args.shift();
|
|
43
|
-
} else {
|
|
44
|
-
out.flags[raw] = true;
|
|
45
51
|
}
|
|
46
52
|
} else {
|
|
47
53
|
out.flags[raw] = true;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Keep command names, aliases, and dispatch behavior in one place. This avoids
|
|
4
|
+
// aliases silently drifting away from the implementation as the CLI grows.
|
|
5
|
+
const commands = [
|
|
6
|
+
{ name: 'init', run: (dir, args, flags) => require('./commands/init')(dir, args, flags) },
|
|
7
|
+
{ name: 'update', run: (dir, args, flags) => require('./commands/update')(dir, args, flags) },
|
|
8
|
+
{ name: 'uninstall', aliases: ['remove'], run: (dir, args, flags) => require('./commands/uninstall')(dir, args, flags) },
|
|
9
|
+
{ name: 'status', run: (dir) => require('./commands/status')(dir) },
|
|
10
|
+
{ name: 'map', run: (dir, _args, flags) => require('./commands/map')(dir, flags) },
|
|
11
|
+
{ name: 'check', run: (dir, _args, flags) => require('./commands/check')(dir, flags) },
|
|
12
|
+
{ name: 'verify-ui', aliases: ['verifyui'], run: (dir, args, flags) => require('./commands/verifyUi')(dir, args, flags) },
|
|
13
|
+
{ name: 'api', run: (_dir, args, flags) => require('./commands/api')(args, flags) },
|
|
14
|
+
{ name: 'delete', aliases: ['del', 'rm'], run: (dir, args, flags) => require('./commands/delete')(dir, args, flags) },
|
|
15
|
+
{ name: 'deploy', run: (dir, args, flags) => require('./commands/deploy')(dir, args, flags) },
|
|
16
|
+
{ name: 'auto-push', aliases: ['autopush'], run: (dir, args, flags) => require('./commands/autoPush')(dir, args, flags) },
|
|
17
|
+
{ name: 'pull', run: (dir, args, flags) => require('./commands/sync')(dir, 'pull', args, flags) },
|
|
18
|
+
{ name: 'push', run: (dir, args, flags) => require('./commands/sync')(dir, 'push', args, flags) },
|
|
19
|
+
{ name: 'local', run: (dir, args, flags) => require('./commands/local')(dir, args, flags) },
|
|
20
|
+
{ name: 'list-targets', aliases: ['targets'], run: () => require('./commands/listTargets')() },
|
|
21
|
+
{ name: 'local-dev', aliases: ['localdev'], run: (dir, args, flags) => require('./commands/localDev')(dir, args, flags) },
|
|
22
|
+
{ name: 'connect', aliases: ['login'], run: (dir, args, flags) => require('./commands/connect')(dir, args, flags) },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const aliases = new Map();
|
|
26
|
+
for (const command of commands) {
|
|
27
|
+
for (const alias of [command.name, ...(command.aliases || [])]) aliases.set(alias, command);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveCommand(name) {
|
|
31
|
+
return aliases.get(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { commands, resolveCommand };
|