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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -139
  3. package/package.json +10 -2
  4. package/resources/skill/SKILL.md +61 -184
  5. package/resources/skill/init/SKILL.md +18 -66
  6. package/resources/skill/manifest.json +27 -0
  7. package/resources/skill/pull/SKILL.md +18 -52
  8. package/resources/skill/push/SKILL.md +30 -282
  9. package/resources/skill/references/aihub.md +86 -0
  10. package/resources/skill/{quickref → references}/api-endpoints.md +39 -13
  11. package/resources/skill/references/api.json +20248 -0
  12. package/resources/skill/{quickref → references}/app-api.md +40 -0
  13. package/resources/skill/{core → references}/architecture.md +2 -2
  14. package/resources/skill/references/chat-sdk.md +201 -0
  15. package/resources/skill/references/custom-services.md +308 -0
  16. package/resources/skill/{specs → references}/data.md +5 -5
  17. package/resources/skill/{rules → references}/frontend.md +41 -11
  18. package/resources/skill/{core → references}/modules.md +7 -5
  19. package/resources/skill/references/parallel.md +48 -0
  20. package/resources/skill/{specs → references}/runtime.md +1 -1
  21. package/resources/skill/scripts/draftgo_push.py +80 -12
  22. package/resources/skill/story/SKILL.md +11 -16
  23. package/src/cli.js +13 -7
  24. package/src/commandRegistry.js +34 -0
  25. package/src/commands/api.js +153 -8
  26. package/src/commands/help.js +24 -29
  27. package/src/commands/init.js +17 -18
  28. package/src/commands/local.js +9 -3
  29. package/src/commands/sync.js +1 -1
  30. package/src/commands/update.js +40 -12
  31. package/src/index.js +13 -57
  32. package/src/localdev/compose.js +44 -200
  33. package/src/localdev/index.js +116 -216
  34. package/src/localdev/mysqlClient.js +12 -9
  35. package/src/localdev/services.js +163 -0
  36. package/src/projectConfig.js +1 -1
  37. package/src/projectMap.js +17 -80
  38. package/src/skill.js +1 -1
  39. package/src/updateCheck.js +2 -12
  40. package/resources/skill/practices/anti-patterns.md +0 -80
  41. package/resources/skill/practices/best-practices.md +0 -60
  42. package/resources/skill/practices/dev-declaration.md +0 -114
  43. package/resources/skill/quickref/api.json +0 -17784
  44. package/resources/skill/rules/dev-workflow.md +0 -749
  45. package/resources/skill/rules/parallel.md +0 -263
  46. package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
  47. package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
  48. package/resources/skill/specs/custom-services.md +0 -199
  49. package/src/commands/doctor.js +0 -54
  50. package/src/commands/new.js +0 -186
  51. package/src/commands/projectScript.js +0 -37
  52. package/src/commands/upgrade.js +0 -52
  53. /package/resources/skill/{specs → references}/db-relations.md +0 -0
  54. /package/resources/skill/{rules → references}/debugging-syntax.md +0 -0
  55. /package/resources/skill/{specs → references}/security.md +0 -0
  56. /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}}/core/architecture.md`
13
- > App API / Token / 路由 / 运行时 → `{{SKILL_DIR}}/specs/runtime.md` · `{{SKILL_DIR}}/quickref/app-api.md`
14
- > 动态 DB / filters → `{{SKILL_DIR}}/specs/data.md`
15
- > 开发禁区 → `{{SKILL_DIR}}/specs/security.md`
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}}/quickref/app-api.md`
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,页面调用 `DraftGoAI.chat/images` | `.draftgo/aihub/` |
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 + DraftGoAI.chat()
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
- → 自定义服务(用 `ctx.HTTP` 请求;需要时可用 route/event/scheduled 加工)
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)` 自动注册路由、事件和定时任务;完整语言、ctx 和 SDK 契约见 `specs/custom-services.md`。
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 用 `ctx.DB.Query(type, sdk.QueryOptions{...})`;返回 `sdk.QueryResult`,分页和筛选见 `specs/custom-services.md`。
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}}/quickref/app-api.md`
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
- return False, e.code, e.read().decode(errors="replace")
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
- ok, status, body = api_call("PUT", server, token, f"/api/pages/{pid}", payload)
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). Required for high-risk development tasks when story.yaml is missing, and used whenever a development decision conflicts with the existing Story."
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
- > **Story 是系统的灵魂档案。高风险开发必须先确认 Story 存在;功能开发若 Story 已存在则静默加载,小修不强制触发 Story。**
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
- | Gate, not Doc | 高风险任务没有 Story 就不开发;普通功能把 Story 作为上下文而不是阻塞器 |
16
+ | Decision record | 记录会影响后续取舍的产品定位与决策 |
22
17
  | AI 维护,人确认 | 开发者只聊天,AI 负责提炼和写入 |
23
18
  | 三层结构 | 身份(稳) → 决策(只增) → 当前(频变) |
24
19
  | 决策日志最值钱 | 取舍依据是代码永远表达不了的信息 |
25
- | 工具无关 | yaml 文件谁都能读,不绑定任何 AI 生态 |
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: "下一步大致方向(粗粒度,不是 TODO)"
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 → 加载 + 提示"Story 还稚嫩,开发中会持续补充"
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
- out.flags[raw.slice(0, eq)] = raw.slice(eq + 1);
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 (next !== undefined && !next.startsWith('-')) {
39
- // Only consume next token as value for a known set of flags.
40
- if (VALUE_FLAGS.has(raw)) {
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 };