draftgo-cli 4.0.22 → 4.0.24

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 (38) hide show
  1. package/README.md +2 -2
  2. package/bin/draftgo.js +8 -8
  3. package/package.json +72 -72
  4. package/resources/custom-service-sdk/auth_test.go +56 -0
  5. package/resources/custom-service-sdk/manifest.json +14 -9
  6. package/resources/custom-service-sdk/platform.go +19 -27
  7. package/resources/custom-service-sdk/resources.go +1 -0
  8. package/resources/custom-service-sdk/resources_scope_test.go +10 -5
  9. package/resources/custom-service-sdk/sdk.go +6 -5
  10. package/resources/skill/SKILL.md +1 -1
  11. package/resources/skill/manifest.json +1 -1
  12. package/resources/skill/references/aihub.md +74 -74
  13. package/resources/skill/references/app-api.md +78 -78
  14. package/resources/skill/references/architecture.md +40 -40
  15. package/resources/skill/references/checkout.md +105 -105
  16. package/resources/skill/references/custom-services.md +6 -6
  17. package/resources/skill/references/data.md +168 -168
  18. package/resources/skill/references/methods.md +3 -0
  19. package/resources/skill/references/modules.md +48 -48
  20. package/resources/skill/references/runtime.md +95 -96
  21. package/resources/skill/story/SKILL.md +264 -264
  22. package/src/commands/help.js +72 -72
  23. package/src/commands/listTargets.js +12 -12
  24. package/src/commands/status.js +2 -2
  25. package/src/commands/uninstall.js +45 -45
  26. package/src/commands/update.js +20 -20
  27. package/src/customServices.js +5 -4
  28. package/src/detect.js +14 -14
  29. package/src/fsx.js +67 -67
  30. package/src/index.js +25 -25
  31. package/src/localRuntime/detect.js +76 -76
  32. package/src/localRuntime/mysqlClient.js +138 -138
  33. package/src/logger.js +37 -37
  34. package/src/mcp/client.js +586 -595
  35. package/src/mcp/hosts.js +520 -520
  36. package/src/mcp/protocol.js +184 -164
  37. package/src/prompt.js +94 -94
  38. package/src/updateCheck.js +16 -16
@@ -1,7 +1,7 @@
1
- ---
2
- read_when: 编辑 pages、navigation 或 docs 正文时 · 查看 checkout manifest 时 · 处理 409/412 冲突时
3
- ---
4
-
1
+ ---
2
+ read_when: 编辑 pages、navigation 或 docs 正文时 · 查看 checkout manifest 时 · 处理 409/412 冲突时
3
+ ---
4
+
5
5
  # Checkout / Commit
6
6
 
7
7
  ## 页面与内容最短流程
@@ -21,120 +21,120 @@ draftgo commit pages 42
21
21
  The checkout set includes `pages`, `navigations`, `docs/articles`, and `custom_services`. A custom-service checkout contains exactly `service.go` and `service.json`, plus one complete `.base` directory. Runner-managed `go.mod/go.sum` never participate in diff or commit. `draftgo commit custom-services <id>` performs the complete delivery (`commit -> validate -> publish`) and prints an explicit published confirmation. Use `draftgo test custom-services <id>` for server Runner validation/execution; `draftgo publish custom-services <id>` remains as a direct compatibility entry point. DB Meta remains a live MCP/API resource and is never checked out.
22
22
 
23
23
  `draftgo refresh <type> <id...>` is a safe checkout shortcut. It updates only a clean local worktree; local changes stop it. A successful commit keeps current local files and one base only. Cloud version storage is the sole history source.
24
-
25
- > 根 `SKILL.md` 在 Skill 触发时会自动加载。使用本文件前,先完成根 Skill 的“强制预读:Reference 优先于 MCP”任务路由。本文件只说明长正文的传输、版本和冲突规则,不能替代页面、前端、运行时或安全资料。
26
-
27
- ## 适用范围
28
-
29
- 只有长正文使用 worktree:
30
-
31
- | 输入类型 | 规范类型 | 本地目录 | 文件前缀 |
32
- |---|---|---|---|
33
- | `page` / `pages` | `pages` | `.draftgo/worktree/pages/` | `page_` |
34
- | `nav` / `navigation` / `navigations` | `navigations` | `.draftgo/worktree/navigations/` | `nav_` |
35
- | `doc` / `docs` / `article(s)` / `docs/articles` | `docs` | `.draftgo/worktree/docs/` | `article_` |
36
-
24
+
25
+ > 根 `SKILL.md` 在 Skill 触发时会自动加载。使用本文件前,先完成根 Skill 的“强制预读:Reference 优先于 MCP”任务路由。本文件只说明长正文的传输、版本和冲突规则,不能替代页面、前端、运行时或安全资料。
26
+
27
+ ## 适用范围
28
+
29
+ 只有长正文使用 worktree:
30
+
31
+ | 输入类型 | 规范类型 | 本地目录 | 文件前缀 |
32
+ |---|---|---|---|
33
+ | `page` / `pages` | `pages` | `.draftgo/worktree/pages/` | `page_` |
34
+ | `nav` / `navigation` / `navigations` | `navigations` | `.draftgo/worktree/navigations/` | `nav_` |
35
+ | `doc` / `docs` / `article(s)` / `docs/articles` | `docs` | `.draftgo/worktree/docs/` | `article_` |
36
+
37
37
  db_meta、AIHub、system_config、roles、users、doc_categories 和普通配置使用 MCP 实时 API,不 checkout。
38
38
 
39
39
  Checkout 只为已存在且已确认 ID 的资源建立本地正文与 base,不创建页面、导航或文档。新增资源先按 MCP 实时 API 契约创建并取得 ID;需要编辑完整正文时再 checkout。只需元数据或正文片段即可完成判断时,不必 checkout。
40
-
41
- ## 命令
42
-
43
- ```bash
44
- draftgo checkout <pages|nav|docs> <id...>
40
+
41
+ ## 命令
42
+
43
+ ```bash
44
+ draftgo checkout <pages|nav|docs> <id...>
45
45
  draftgo commit <pages|nav|docs> <id...>
46
46
  draftgo reconcile <pages|nav|docs> <id...>
47
47
  draftgo diff <pages|nav|docs|custom-services> <id> [--stat|--summary]
48
- draftgo conflicts
49
- draftgo conflict show <pages|nav|docs> <id>
50
- draftgo conflict resolve <pages|nav|docs> <id>
51
- ```
52
-
48
+ draftgo conflicts
49
+ draftgo conflict show <pages|nav|docs> <id>
50
+ draftgo conflict resolve <pages|nav|docs> <id>
51
+ ```
52
+
53
53
  `checkout --force` 只用于用户明确允许丢弃未提交本地修改的情况。默认 checkout 检测到 worktree 文件相对
54
54
  base 已变化时必须拒绝覆盖。
55
55
 
56
56
  `diff --stat` 只显示文件与增删行数;`diff --summary` 显示资源、基线版本和变化概要。两者均不输出正文 diff,先用它们确认范围,再按需展开完整 `diff`。`--output json` 时 stdout 只包含 UTF-8 JSON,诊断走 stderr。
57
-
58
- ## Checkout 流程
59
-
60
- 1. CLI 通过 MCP `draftgo_resource_get_metadata` 取得规范类型、content_type、SHA-256、大小、版本/revision、
61
- ETag 和受信任的下载/提交 URL。
57
+
58
+ ## Checkout 流程
59
+
60
+ 1. CLI 通过 MCP `draftgo_resource_get_metadata` 取得规范类型、content_type、SHA-256、大小、版本/revision、
61
+ ETag 和受信任的下载/提交 URL。
62
62
  2. CLI 使用 `.draftgo/config.json` 中的用户 API Key 通过专用 HTTP 下载完整正文;API Key 不进入 MCP 参数或日志。
63
- 3. 响应体直接流式写入同目录临时文件,校验 content_type、字节数和 SHA-256。
64
- 4. 校验成功后原子重命名到 worktree 文件,并保存相同字节的 `.base` 文件。
65
- 5. 最后原子更新 `.draftgo/worktree/manifest.json`。失败时不得留下半截正式文件或推进 manifest。
66
-
67
- 正文不做 HTML/Markdown 转换,也不改变编码。扩展名规则:
68
-
69
- - `text/html`、`application/xhtml+xml` -> `.html`
70
- - `text/markdown`、`text/x-markdown` -> `.md`
71
- - `text/plain` -> `.txt`
72
- - 其他类型只接受底座返回的安全扩展名;缺失或不安全时拒绝 checkout
73
-
74
- ## Manifest
75
-
76
- 路径固定为 `.draftgo/worktree/manifest.json`,schema version 当前为 `1`。条目键使用规范类型和 id:
77
-
78
- ```json
79
- {
80
- "schema_version": 1,
81
- "updated_at": "2026-07-30T12:00:00.000Z",
82
- "entries": {
83
- "pages:42": {
84
- "server": "https://draftgo.example",
85
- "resource_type": "pages",
86
- "resource_id": "42",
87
- "title": "Example",
88
- "route": "/example",
89
- "code": null,
90
- "slug": null,
91
- "local_path": ".draftgo/worktree/pages/page_42.html",
92
- "content_type": "text/html",
93
- "file_extension": ".html",
94
- "content_size": 123,
95
- "base_path": ".draftgo/worktree/.base/pages/page_42.html",
96
- "base_version": "7",
97
- "base_revision": null,
98
- "base_etag": null,
99
- "base_hash": "<sha256>",
100
- "checked_out_at": "2026-07-30T12:00:00.000Z"
101
- }
102
- }
103
- }
104
- ```
105
-
106
- `server` 必须和当前连接一致。`local_path`、`base_path` 必须是项目内相对路径。manifest、worktree `.base`
107
- 和冲突目录必须 gitignore;不要手工伪造版本或哈希。
108
-
109
- ## Commit 流程
110
-
111
- 1. 读取 manifest 指向的 worktree 文件,计算当前字节数和 SHA-256;未变化时返回 `unchanged`。
63
+ 3. 响应体直接流式写入同目录临时文件,校验 content_type、字节数和 SHA-256。
64
+ 4. 校验成功后原子重命名到 worktree 文件,并保存相同字节的 `.base` 文件。
65
+ 5. 最后原子更新 `.draftgo/worktree/manifest.json`。失败时不得留下半截正式文件或推进 manifest。
66
+
67
+ 正文不做 HTML/Markdown 转换,也不改变编码。扩展名规则:
68
+
69
+ - `text/html`、`application/xhtml+xml` -> `.html`
70
+ - `text/markdown`、`text/x-markdown` -> `.md`
71
+ - `text/plain` -> `.txt`
72
+ - 其他类型只接受底座返回的安全扩展名;缺失或不安全时拒绝 checkout
73
+
74
+ ## Manifest
75
+
76
+ 路径固定为 `.draftgo/worktree/manifest.json`,schema version 当前为 `1`。条目键使用规范类型和 id:
77
+
78
+ ```json
79
+ {
80
+ "schema_version": 1,
81
+ "updated_at": "2026-07-30T12:00:00.000Z",
82
+ "entries": {
83
+ "pages:42": {
84
+ "server": "https://draftgo.example",
85
+ "resource_type": "pages",
86
+ "resource_id": "42",
87
+ "title": "Example",
88
+ "route": "/example",
89
+ "code": null,
90
+ "slug": null,
91
+ "local_path": ".draftgo/worktree/pages/page_42.html",
92
+ "content_type": "text/html",
93
+ "file_extension": ".html",
94
+ "content_size": 123,
95
+ "base_path": ".draftgo/worktree/.base/pages/page_42.html",
96
+ "base_version": "7",
97
+ "base_revision": null,
98
+ "base_etag": null,
99
+ "base_hash": "<sha256>",
100
+ "checked_out_at": "2026-07-30T12:00:00.000Z"
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ `server` 必须和当前连接一致。`local_path`、`base_path` 必须是项目内相对路径。manifest、worktree `.base`
107
+ 和冲突目录必须 gitignore;不要手工伪造版本或哈希。
108
+
109
+ ## Commit 流程
110
+
111
+ 1. 读取 manifest 指向的 worktree 文件,计算当前字节数和 SHA-256;未变化时返回 `unchanged`。
112
112
  2. 按 content_type 执行本地结构和内联脚本检查。
113
- 3. 通过专用 HTTP 流式上传原始文件,携带 `If-Match`、base version/revision、content_type、长度和 SHA-256。
114
- 4. 完整正文不得作为 MCP tool 参数发送。
115
- 5. 底座确认 hash 和新版本后,CLI 原子更新 `.base` 与 manifest。返回 hash 不一致时不得推进基线。
116
-
113
+ 3. 通过专用 HTTP 流式上传原始文件,携带 `If-Match`、base version/revision、content_type、长度和 SHA-256。
114
+ 4. 完整正文不得作为 MCP tool 参数发送。
115
+ 5. 底座确认 hash 和新版本后,CLI 原子更新 `.base` 与 manifest。返回 hash 不一致时不得推进基线。
116
+
117
117
  本地正文已经等于远端、但 base/manifest 落后时,先用 `draftgo check --remote` 确认 `committed_unrecorded`,再运行 `draftgo reconcile`;不要手改 manifest。
118
118
 
119
119
  单个 commit 成功不自动完成 worklog 项。只有整个事项统一验证且全部 commit/MCP 交付成功后,主 Agent 才执行 `draftgo work complete <编号> --note "<完成结果>"`。任何检查失败、409/412 或交付失败都不得标记为完成。
120
-
121
- ## 409 / 412 冲突
122
-
123
- 版本冲突时 CLI 返回非零,不自动重试、不 force、不覆盖 worktree local,并写入:
124
-
125
- ```text
126
- .draftgo/conflicts/<pages|navigations|docs>/<id>/
127
- ├── conflict.json
128
- ├── base.<ext>
129
- ├── local.<ext>
130
- └── remote.<ext>
131
- ```
132
-
133
- - `base` 是 checkout 时的内容;`local` 是发生冲突时的本地快照;`remote` 是重新下载并校验的当前远端内容。
134
- - `conflict.json` 记录三份路径、版本、ETag 和哈希,不嵌入完整正文。
135
- - Agent 或用户在 worktree local 文件中完成合并;不要手写 HTML 自动合并器,也不要改动保存的三份证据。
120
+
121
+ ## 409 / 412 冲突
122
+
123
+ 版本冲突时 CLI 返回非零,不自动重试、不 force、不覆盖 worktree local,并写入:
124
+
125
+ ```text
126
+ .draftgo/conflicts/<pages|navigations|docs>/<id>/
127
+ ├── conflict.json
128
+ ├── base.<ext>
129
+ ├── local.<ext>
130
+ └── remote.<ext>
131
+ ```
132
+
133
+ - `base` 是 checkout 时的内容;`local` 是发生冲突时的本地快照;`remote` 是重新下载并校验的当前远端内容。
134
+ - `conflict.json` 记录三份路径、版本、ETag 和哈希,不嵌入完整正文。
135
+ - Agent 或用户在 worktree local 文件中完成合并;不要手写 HTML 自动合并器,也不要改动保存的三份证据。
136
136
  - 合并完成后运行 `draftgo verify`,再执行 `draftgo conflict resolve <type> <id>`。
137
- - resolve 校验 worktree 与 remote,采用 remote 版本作为新的 base,但保留合并后的 worktree;随后运行
138
- `draftgo diff` 并 `draftgo commit`。
139
-
140
- 存在 unresolved conflict 时 commit、deploy 或 auto-commit 必须停止。
137
+ - resolve 校验 worktree 与 remote,采用 remote 版本作为新的 base,但保留合并后的 worktree;随后运行
138
+ `draftgo diff` 并 `draftgo commit`。
139
+
140
+ 存在 unresolved conflict 时 commit、deploy 或 auto-commit 必须停止。
@@ -62,7 +62,7 @@ func health(ctx *sdk.Context) (any, error) {
62
62
 
63
63
  输入字段为 `method`、`headers`、`body`、`query_params`、`path_params`。身份由 `ctx.Auth.CurrentUser()` 获取。普通 `ctx.DB`、`ctx.Users`、`ctx.Billing` 等调用继承请求调用者权限;管理员创建服务、持有服务凭据或 `RequireAdmin` 都不会自动提升 SDK 调用。
64
64
 
65
- `ctx.Admin.*` 是可信服务显式选择的单次管理提升:保持服务主体与已验证执行上下文,但可执行平台级或跨用户/空间管理操作;每次调用都会进入审计,提升不会泄漏到后续普通调用。服务代码仍拿不到服务凭据、数据库连接或管理员凭据。公开 Route 不应无条件调用 Admin;先完成业务鉴权、参数校验和幂等设计,返回值仍需自行脱敏。
65
+ `ctx.Admin.*` 是自定义服务显式选择的单次系统内部授权:该次 RPC 使用 system actor_id=0,按 `*:*:all` 全权限执行,可跨用户/空间,不检查 AccessGrant;每次调用进入审计,内部授权不会泄漏到后续普通调用。Event/Scheduled 没有调用者,普通 `ctx.xxx` 返回 403。服务代码仍拿不到服务凭据、数据库连接或管理员凭据。公开 Route 不应无条件调用 Admin;先完成业务鉴权、参数校验和幂等设计,返回值仍需自行脱敏。
66
66
 
67
67
  ### Event
68
68
 
@@ -70,7 +70,7 @@ func health(ctx *sdk.Context) (any, error) {
70
70
 
71
71
  ### Scheduled
72
72
 
73
- `app.Schedule("0 2 * * *", cleanup)` 使用五字段 cron,也支持 `interval:5m`。定时任务没有用户调用者;执行空间来自服务持久化的 ResourceOwnership,服务 principal 还必须有覆盖目标资源的 AccessGrant。运行时不会猜测用户或回退到 platform。
73
+ `app.Schedule("0 2 * * *", cleanup)` 使用五字段 cron,也支持 `interval:5m`。定时任务没有用户调用者;执行空间来自服务持久化的 ResourceOwnership,普通 SDK 调用必须改用 `ctx.Admin.*`,运行时不会猜测用户或回退到 platform。
74
74
 
75
75
  ## 试运行
76
76
 
@@ -126,7 +126,7 @@ response, err := ctx.HTTP.Get(ctx.Context(), "https://api.example.com/health", n
126
126
  | 场景 | 方法 |
127
127
  |---|---|
128
128
  | 当前调用者操作服务归属内账务 | `ctx.Billing.*` |
129
- | 可信服务执行需要管理员权限的账务管理 | `ctx.Admin.Billing.*`;目标仍由服务端资源归属决定 |
129
+ | 自定义服务执行系统级账务管理 | `ctx.Admin.Billing.*`;单次调用按系统全权限执行并审计 |
130
130
  | 金额已确定且应立即扣除 | `DebitAccount` |
131
131
  | 最终金额不确定或业务可能失败 | `HoldFunds` -> `SettleHold`;失败时 `ReleaseHold` |
132
132
  | 更正已入账流水 | `ReverseJournal`,不要用反向充值伪造冲正 |
@@ -179,9 +179,9 @@ typed helper 未覆盖的新端点可使用 `ctx.AIHub.Request`,但 path 只
179
179
  - Role 不带作用域;服务必须通过覆盖持久化 ResourceOwnership 的有效 AccessGrant 授权。工作区成员关系不能单独授权。
180
180
  - platform Grant 可跨空间但只能使用显式权限;space Grant 不能跨根。请求中的范围不能覆盖服务已保存的归属。
181
181
  - 普通服务调用访问动态 DB 时仍受目标 DB 的 DataRange(`none` / `own` / `all`)限制;`ctx.Admin.*` 只应用于源码明确选择的单次可信管理操作,不能从请求参数隐式开启。
182
- - Route 仍受服务 `permission` `config.route_security` 控制。
183
- - 定时任务和无可解析用户的事件默认使用服务主体及其持久化空间;需要平台级或跨归属管理时必须在对应单次调用显式使用 `ctx.Admin.*`。
184
- - 服务 principal 由平台运行时注入;不要在 `service.json`、源码、测试参数或日志中保存/模拟服务身份凭据。Route/Event 取调用者与服务 Grant 的权限交集,Scheduled 使用服务持久化 ownership,而不是假造用户 API Key
182
+ - Route 不接受服务级 `permission`;入口由宿主认证、服务 ownership 和调用者 `scripts:execute` AccessGrant 控制,业务级公开/登录规则在 handler 内显式实现。
183
+ - 定时任务和事件没有调用者,普通 `ctx.xxx` 直接返回 403;必须在对应单次调用显式使用 `ctx.Admin.*`。服务归属仍由持久化 ResourceOwnership 提供,不创建服务主体授权。
184
+ - 平台运行时注入真实 user 或 system principal;不要在 `service.json`、源码、测试参数或日志中保存/模拟服务身份凭据。Route 普通调用按用户 AccessGrant,Event/Scheduled 仅允许 Admin RPC
185
185
  - `config.timeout`、`max_concurrency`、`queue_timeout_ms` 控制执行;Route 饱和返回 429。
186
186
  - 超时会终止独立子进程。运行器不是不可信多租户安全沙箱,只授予可信编辑者服务权限。
187
187
  - 构建键覆盖源码、依赖、SDK 和 Runner 协议;有效验证凭证绑定该键,任一部分变化都必须重新验证。
@@ -1,7 +1,7 @@
1
- ---
2
- read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检索时 · 定义关联关系时
3
- ---
4
-
1
+ ---
2
+ read_when: 操作动态 DB 之前 · 设计数据结构时 · 使用 filters 检索时 · 定义关联关系时
3
+ ---
4
+
5
5
  # 动态 DB & 数据层
6
6
 
7
7
  ## 最短管理流程
@@ -20,180 +20,180 @@ draftgo api search "dynamic db record"
20
20
  完成条件:schema 可按 `type` 回读,目标角色的最小 CRUD 与筛选行为通过;批量写入还要验证失败时整批回滚。
21
21
 
22
22
  ## DB Meta 结构
23
-
24
- ```json
25
- {
26
- "type": "order",
27
- "label": "订单",
28
- "schema": {
29
- "type": "object",
30
- "properties": {
31
- "name": { "type": "string", "title": "姓名", "required": true, "searchable": "fuzzy" },
32
- "status": { "type": "string", "title": "状态", "required": false, "searchable": "exact" },
33
- "amount": { "type": "number", "title": "金额", "required": false, "searchable": "range" },
34
- "paid_at": { "type": "datetime", "title": "支付时间", "required": false, "searchable": "range" },
35
- "tags": { "type": "array", "title": "标签", "required": false, "searchable": "contains" },
36
- "note": { "type": "string", "title": "备注", "required": false, "searchable": false }
37
- }
38
- },
39
- "permission": {
40
- "public": { "read": "none", "create": "none", "update": "none", "delete": "none" },
41
- "login": { "read": "all", "create": "all", "update": "owner", "delete": "owner" },
42
- "admin": { "read": "all", "create": "all", "update": "all", "delete": "all" }
43
- }
44
- }
45
- ```
46
-
47
- **searchable 模式**:`false`(不可检索)/ `"exact"`(精确)/ `"fuzzy"`(模糊)/ `"range"`(数值/时间范围)/ `"contains"`(数组包含)
48
-
49
- `date` 字段保存为 `YYYY-MM-DD`;`datetime` 字段保存为 ISO 8601 字符串,带时区的输入会规范化为 UTC。`searchable: true` 对 `number` / `date` / `datetime` 会自动推断为 `range`。
50
-
51
- **系统字段**:以下系统字段**无需在 schema 中定义**,可直接用于检索和排序:
52
-
53
- | 字段 | 类型 | 检索模式 | 说明 |
54
- |---|---|---|---|
55
- | `id` | number | range | 记录 ID,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
56
- | `userid` | number | range | 所属用户 ID,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
57
- | `created_at` | datetime | range | 创建时间,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
58
- | `updated_at` | datetime | range | 更新时间,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
59
-
60
- ⚠️ **注意**:虽然 DB 表有 `status` 系统列(1=正常 0=禁用 -1=删除),但由于业务常用此字段名,**需在 schema 中显式声明 `status` 的 searchable 才可检索**。
61
-
62
- ---
63
-
23
+
24
+ ```json
25
+ {
26
+ "type": "order",
27
+ "label": "订单",
28
+ "schema": {
29
+ "type": "object",
30
+ "properties": {
31
+ "name": { "type": "string", "title": "姓名", "required": true, "searchable": "fuzzy" },
32
+ "status": { "type": "string", "title": "状态", "required": false, "searchable": "exact" },
33
+ "amount": { "type": "number", "title": "金额", "required": false, "searchable": "range" },
34
+ "paid_at": { "type": "datetime", "title": "支付时间", "required": false, "searchable": "range" },
35
+ "tags": { "type": "array", "title": "标签", "required": false, "searchable": "contains" },
36
+ "note": { "type": "string", "title": "备注", "required": false, "searchable": false }
37
+ }
38
+ },
39
+ "permission": {
40
+ "public": { "read": "none", "create": "none", "update": "none", "delete": "none" },
41
+ "login": { "read": "all", "create": "all", "update": "owner", "delete": "owner" },
42
+ "admin": { "read": "all", "create": "all", "update": "all", "delete": "all" }
43
+ }
44
+ }
45
+ ```
46
+
47
+ **searchable 模式**:`false`(不可检索)/ `"exact"`(精确)/ `"fuzzy"`(模糊)/ `"range"`(数值/时间范围)/ `"contains"`(数组包含)
48
+
49
+ `date` 字段保存为 `YYYY-MM-DD`;`datetime` 字段保存为 ISO 8601 字符串,带时区的输入会规范化为 UTC。`searchable: true` 对 `number` / `date` / `datetime` 会自动推断为 `range`。
50
+
51
+ **系统字段**:以下系统字段**无需在 schema 中定义**,可直接用于检索和排序:
52
+
53
+ | 字段 | 类型 | 检索模式 | 说明 |
54
+ |---|---|---|---|
55
+ | `id` | number | range | 记录 ID,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
56
+ | `userid` | number | range | 所属用户 ID,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
57
+ | `created_at` | datetime | range | 创建时间,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
58
+ | `updated_at` | datetime | range | 更新时间,支持 `eq` / `gte` / `lte` / `gt` / `lt` / `in` |
59
+
60
+ ⚠️ **注意**:虽然 DB 表有 `status` 系统列(1=正常 0=禁用 -1=删除),但由于业务常用此字段名,**需在 schema 中显式声明 `status` 的 searchable 才可检索**。
61
+
62
+ ---
63
+
64
64
  ## 自定义服务内的 ctx.DB.Query
65
65
 
66
66
  Go 自定义服务使用 `ctx.DB.Query(type, sdk.QueryOptions{...})` 操作动态 DB,返回 `sdk.QueryResult`:
67
67
 
68
68
  ```go
69
69
  result, err := ctx.DB.Query("order", sdk.QueryOptions{
70
- Filters: map[string]any{"status": "paid"},
71
- Page: 1, PageSize: 20, OrderBy: "id", Order: "desc",
72
- })
73
- items := result.Items
74
- ```
75
-
76
- | 参数 | 说明 |
77
- |---|---|
78
- | `filters` | 字典形式结构化过滤;默认 `{field: value}` 是 `eq` 精确匹配 |
79
- | `Page` / `PageSize` | 分页参数;零值交由平台使用默认列表行为 |
80
- | `order_by` / `order` | 按 searchable 字段排序,`order` 为 `asc` / `desc` |
81
-
82
- ⚠️ 返回结构为 `sdk.QueryResult{Items, Total, Page, PageSize}`。需要完整数据时必须按 `Total` 分页读取,不能用超大 `PageSize` 假装全量。
83
-
84
- 普通用户调用时只读 `status=1` 数据;系统身份/管理员脚本可读全部状态数据,但仍受 db_meta permission 约束。不要把“拿不到禁用数据”和分页截断混在一起排查。
85
-
86
- `filters` 操作符示例:
87
-
88
- ```go
70
+ Filters: map[string]any{"status": "paid"},
71
+ Page: 1, PageSize: 20, OrderBy: "id", Order: "desc",
72
+ })
73
+ items := result.Items
74
+ ```
75
+
76
+ | 参数 | 说明 |
77
+ |---|---|
78
+ | `filters` | 字典形式结构化过滤;默认 `{field: value}` 是 `eq` 精确匹配 |
79
+ | `Page` / `PageSize` | 分页参数;零值交由平台使用默认列表行为 |
80
+ | `order_by` / `order` | 按 searchable 字段排序,`order` 为 `asc` / `desc` |
81
+
82
+ ⚠️ 返回结构为 `sdk.QueryResult{Items, Total, Page, PageSize}`。需要完整数据时必须按 `Total` 分页读取,不能用超大 `PageSize` 假装全量。
83
+
84
+ 普通用户调用时只读 `status=1` 数据;系统身份/管理员脚本可读全部状态数据,但仍受 db_meta permission 约束。不要把“拿不到禁用数据”和分页截断混在一起排查。
85
+
86
+ `filters` 操作符示例:
87
+
88
+ ```go
89
89
  ctx.DB.Query("order", sdk.QueryOptions{Filters: map[string]any{
90
- "status": "paid",
91
- "customer_name": map[string]any{"op": "like", "value": "张"},
92
- "amount": map[string]any{"op": "gte", "value": 100},
93
- "id": map[string]any{"op": "in", "value": []int{1, 2, 3}},
94
- }})
95
- ```
96
-
97
- 字段必须在 db_meta schema 中标记 `searchable`,系统字段 `id/userid/created_at/updated_at` 可直接检索和排序。`status` 若作为业务字段检索,仍需在 schema 中显式声明。
98
-
99
- ---
100
-
90
+ "status": "paid",
91
+ "customer_name": map[string]any{"op": "like", "value": "张"},
92
+ "amount": map[string]any{"op": "gte", "value": 100},
93
+ "id": map[string]any{"op": "in", "value": []int{1, 2, 3}},
94
+ }})
95
+ ```
96
+
97
+ 字段必须在 db_meta schema 中标记 `searchable`,系统字段 `id/userid/created_at/updated_at` 可直接检索和排序。`status` 若作为业务字段检索,仍需在 schema 中显式声明。
98
+
99
+ ---
100
+
101
101
  ## 关联关系(ref)
102
102
 
103
103
  关系只在需要时读取 `references/db-relations.md`。核心不变量:`many-to-one` 与 `many-to-many` 在真实持有引用 ID 的字段上配置 `ref` 和 `onDelete`;`one-to-many` 是用于 `populate` 的虚拟反向关系。级联继承当前用户权限并在同一事务中执行,不要在前端用多次 DELETE 模拟。
104
-
105
- ---
106
-
107
- ## CRUD 操作范式
108
-
109
- ```javascript
110
- // 查询(分页 + 结构化检索)
111
- const res = await App.get(`db/order`, {
112
- page: 1, page_size: 20,
113
- filters: ['name:like:张', 'status:eq:paid', 'amount:gte:100'],
114
- order_by: 'amount', order: 'desc',
115
- });
116
- const { items, total } = res.data;
117
-
118
- // 非后台管理页面读取业务列表时,如果当前用户含 admin 角色,带 scope=mine
119
- // 这只影响 GET /api/db/{type} 列表,让管理员业务视角只看自己的 owner 数据
120
- const ownOrders = await App.get(`db/order`, {
121
- page: 1,
122
- page_size: 20,
123
- scope: 'mine',
124
- });
125
-
126
- // 系统字段检索和排序(无需在 schema 中定义)
127
- const res = await App.get(`db/order`, {
128
- filters: ['created_at:gte:2024-01-01', 'userid:eq:123'],
129
- order_by: 'created_at', order: 'desc',
130
- });
131
-
132
- // 创建(业务字段必须放在 data 包裹里)
133
- await App.post(`db/order`, { data: { name: '张三', amount: 200 }, status: 1 });
134
-
135
- // 更新
136
- await App.put(`db/order/${id}`, { data: { amount: 250 } });
137
-
138
- // 删除
139
- await App.delete(`db/order/${id}`);
140
-
141
- // 批量更新(原子事务,任一失败全批回滚)
142
- await App.patch(`db/order/batch`, [
143
- { id: 1, data: { status: 'paid' } },
144
- { id: 2, data: { status: 'paid' } },
145
- ]);
146
- ```
147
-
148
- ---
149
-
150
- ## filters 操作符
151
-
152
- | 操作符 | 含义 | 适用 searchable 模式 |
153
- |---|---|---|
154
- | `eq` | 精确等于 | exact / fuzzy / range |
155
- | `like` | 模糊包含 | fuzzy |
156
- | `gte` / `lte` / `gt` / `lt` | 数值/时间范围 | range |
157
- | `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy / range |
158
- | `contains` | 数组字段包含某值 | contains |
159
-
160
- - 省略操作符(`filters: ['name:张三']`)默认 `like`
161
- - 多个 filters 为 AND
162
- - 字段未标 searchable 或操作符不匹配 → 后端返回 400
163
- - **系统字段**(`id` / `userid` / `created_at` / `updated_at`)无需在 schema 中声明,可直接使用
164
- - `scope=mine` 只对拥有 admin 角色的用户在 `GET /api/db/{type}` 列表请求中生效;非后台管理页面若当前用户是管理员,读取动态 DB 业务列表时应带该参数;后台管理页不要带,详情和写操作也不要带
165
-
166
- ---
167
-
168
- ## db_meta 实时契约
169
-
104
+
105
+ ---
106
+
107
+ ## CRUD 操作范式
108
+
109
+ ```javascript
110
+ // 查询(分页 + 结构化检索)
111
+ const res = await App.get(`db/order`, {
112
+ page: 1, page_size: 20,
113
+ filters: ['name:like:张', 'status:eq:paid', 'amount:gte:100'],
114
+ order_by: 'amount', order: 'desc',
115
+ });
116
+ const { items, total } = res.data;
117
+
118
+ // 非后台管理页面读取业务列表时,如果当前用户含 admin 角色,带 scope=mine
119
+ // 这只影响 GET /api/db/{type} 列表,让管理员业务视角只看自己的 owner 数据
120
+ const ownOrders = await App.get(`db/order`, {
121
+ page: 1,
122
+ page_size: 20,
123
+ scope: 'mine',
124
+ });
125
+
126
+ // 系统字段检索和排序(无需在 schema 中定义)
127
+ const res = await App.get(`db/order`, {
128
+ filters: ['created_at:gte:2024-01-01', 'userid:eq:123'],
129
+ order_by: 'created_at', order: 'desc',
130
+ });
131
+
132
+ // 创建(业务字段必须放在 data 包裹里)
133
+ await App.post(`db/order`, { data: { name: '张三', amount: 200 }, status: 1 });
134
+
135
+ // 更新
136
+ await App.put(`db/order/${id}`, { data: { amount: 250 } });
137
+
138
+ // 删除
139
+ await App.delete(`db/order/${id}`);
140
+
141
+ // 批量更新(原子事务,任一失败全批回滚)
142
+ await App.patch(`db/order/batch`, [
143
+ { id: 1, data: { status: 'paid' } },
144
+ { id: 2, data: { status: 'paid' } },
145
+ ]);
146
+ ```
147
+
148
+ ---
149
+
150
+ ## filters 操作符
151
+
152
+ | 操作符 | 含义 | 适用 searchable 模式 |
153
+ |---|---|---|
154
+ | `eq` | 精确等于 | exact / fuzzy / range |
155
+ | `like` | 模糊包含 | fuzzy |
156
+ | `gte` / `lte` / `gt` / `lt` | 数值/时间范围 | range |
157
+ | `in` | 枚举命中(值逗号分隔:`status:in:paid,pending`) | exact / fuzzy / range |
158
+ | `contains` | 数组字段包含某值 | contains |
159
+
160
+ - 省略操作符(`filters: ['name:张三']`)默认 `like`
161
+ - 多个 filters 为 AND
162
+ - 字段未标 searchable 或操作符不匹配 → 后端返回 400
163
+ - **系统字段**(`id` / `userid` / `created_at` / `updated_at`)无需在 schema 中声明,可直接使用
164
+ - `scope=mine` 只对拥有 admin 角色的用户在 `GET /api/db/{type}` 列表请求中生效;非后台管理页面若当前用户是管理员,读取动态 DB 业务列表时应带该参数;后台管理页不要带,详情和写操作也不要带
165
+
166
+ ---
167
+
168
+ ## db_meta 实时契约
169
+
170
170
  db_meta 是结构化远端资源,不 checkout,也不生成本地索引。未知 operation 才通过 MCP
171
171
  `draftgo_api_search` 定位;首次使用或 registry revision 变化时 `draftgo_api_describe`,再用
172
172
  `draftgo_api_call` 查询目标 type 和 schema。
173
- 典型返回条目如下:
174
-
175
- ```json
176
- [
177
- {
178
- "id": 1,
179
- "type": "order",
180
- "label": "订单",
181
- "schema": { ... }
182
- }
183
- ]
184
- ```
185
-
186
- ⚠️ **GET `/api/db-meta/{type}` 用 type(如 `order`),不是 id。PUT/DELETE 才用 id。**
187
-
188
- ---
189
-
190
- ## 通用筛选参数(非动态 DB)
191
-
192
- 用于 users / roles / pages / navigations / feedback 等标准资源:
193
-
194
- | 参数 | 说明 |
195
- |---|---|
196
- | `page` / `page_size` | 分页(不传返回全量且无上限;任一传入则分页,缺失项按 `page=1` / `page_size=20` 兜底) |
197
- | `search` | 全文搜索(动态 DB 不用此参数) |
198
- | `status` | 状态过滤 |
199
- | `type` / `tag` | 类型/标签过滤 |
173
+ 典型返回条目如下:
174
+
175
+ ```json
176
+ [
177
+ {
178
+ "id": 1,
179
+ "type": "order",
180
+ "label": "订单",
181
+ "schema": { ... }
182
+ }
183
+ ]
184
+ ```
185
+
186
+ ⚠️ **GET `/api/db-meta/{type}` 用 type(如 `order`),不是 id。PUT/DELETE 才用 id。**
187
+
188
+ ---
189
+
190
+ ## 通用筛选参数(非动态 DB)
191
+
192
+ 用于 users / roles / pages / navigations / feedback 等标准资源:
193
+
194
+ | 参数 | 说明 |
195
+ |---|---|
196
+ | `page` / `page_size` | 分页(不传返回全量且无上限;任一传入则分页,缺失项按 `page=1` / `page_size=20` 兜底) |
197
+ | `search` | 全文搜索(动态 DB 不用此参数) |
198
+ | `status` | 状态过滤 |
199
+ | `type` / `tag` | 类型/标签过滤 |