@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.68
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/README.en.md +86 -241
- package/README.ja.md +86 -241
- package/README.md +86 -241
- package/dist/{src-RDQ2EIBT.js → chunk-FI56GONY.js} +32 -2669
- package/dist/chunk-FI56GONY.js.map +1 -0
- package/dist/chunk-MBPB37FR.js +1348 -0
- package/dist/chunk-MBPB37FR.js.map +1 -0
- package/dist/{chunk-QYX6V7H2.js → chunk-ZFNBUF77.js} +347 -276
- package/dist/chunk-ZFNBUF77.js.map +1 -0
- package/dist/cli-config-templates-W2LLPMB6.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-US5CCAJ4.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
- package/dist/index.js +1304 -532
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -0
- package/dist/src-DSJJXM2F.js +3849 -0
- package/dist/src-DSJJXM2F.js.map +1 -0
- package/package.json +9 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/messages/ja-JP.json +38 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
- package/templates/apps/api/.dockerignore.hbs +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +33 -8
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +6 -1
- package/templates/apps/api/playwright.config.ts.hbs +15 -7
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -9
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +32 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +40 -14
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +6 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +13 -14
- package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
- package/templates/apps/h5/messages/ja-JP.json +40 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +16 -5
- package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +15 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/messages/ja-JP.json +32 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +13 -5
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +23 -8
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +11 -3
- package/templates/base/playwright.config.ts +15 -4
- package/templates/base/src/hooks.server.ts.hbs +15 -6
- package/templates/base/src/lib/server/init.ts.hbs +139 -30
- package/templates/base/vite.config.ts.hbs +5 -3
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
- package/templates/i18n/project.inlang/settings.json +1 -1
- package/templates/shared/.gitattributes +6 -0
- package/templates/shared/e2e-env.ts +98 -0
- package/templates/shared/run-e2e.mjs +51 -12
- package/templates/skills/AGENTS.md +7 -3
- package/templates/skills/bridges/admin/AGENTS.md +6 -2
- package/templates/skills/bridges/api/AGENTS.md +3 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +5 -1
- package/templates/skills/bridges/h5/AGENTS.md +6 -2
- package/templates/skills/bridges/mobile-app/AGENTS.md +6 -2
- package/templates/skills/bridges/website/AGENTS.md +6 -2
- package/templates/skills/hai-ai/SKILL.md +8 -2
- package/templates/skills/hai-ai/guide.md +134 -60
- package/templates/skills/hai-ai/reference.md +51 -11
- package/templates/skills/hai-api-client/SKILL.md +15 -160
- package/templates/skills/hai-api-client/reference.md +178 -0
- package/templates/skills/hai-api-contract/SKILL.md +16 -273
- package/templates/skills/hai-api-contract/reference.md +280 -0
- package/templates/skills/hai-app-create/SKILL.md +6 -2
- package/templates/skills/hai-app-create/development.md +24 -8
- package/templates/skills/hai-app-create/reference.md +7 -5
- package/templates/skills/hai-app-review/SKILL.md +7 -3
- package/templates/skills/hai-app-review/differential.md +34 -0
- package/templates/skills/hai-app-tests/SKILL.md +6 -3
- package/templates/skills/hai-app-tests/reference.md +5 -5
- package/templates/skills/hai-app-tests/validation.md +38 -0
- package/templates/skills/hai-audit/SKILL.md +24 -147
- package/templates/skills/hai-audit/reference.md +148 -0
- package/templates/skills/hai-build/SKILL.md +14 -8
- package/templates/skills/hai-build/reference.md +16 -13
- package/templates/skills/hai-cache/SKILL.md +20 -181
- package/templates/skills/hai-cache/reference.md +229 -0
- package/templates/skills/hai-capacitor/SKILL.md +7 -4
- package/templates/skills/hai-capacitor/reference.md +15 -9
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +7 -3
- package/templates/skills/hai-core/reference.md +25 -5
- package/templates/skills/hai-crypto/SKILL.md +22 -250
- package/templates/skills/hai-crypto/reference.md +310 -0
- package/templates/skills/hai-datapipe/SKILL.md +16 -195
- package/templates/skills/hai-datapipe/reference.md +204 -0
- package/templates/skills/hai-deploy/SKILL.md +24 -54
- package/templates/skills/hai-deploy/middleware.md +51 -43
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +25 -11
- package/templates/skills/hai-iam/reference.md +181 -154
- package/templates/skills/hai-kit/SKILL.md +35 -174
- package/templates/skills/hai-kit/reference.md +190 -0
- package/templates/skills/hai-payment/SKILL.md +33 -263
- package/templates/skills/hai-payment/reference.md +295 -0
- package/templates/skills/hai-pr-review/SKILL.md +21 -88
- package/templates/skills/hai-reach/SKILL.md +6 -3
- package/templates/skills/hai-reach/reference.md +83 -22
- package/templates/skills/hai-reldb/SKILL.md +6 -3
- package/templates/skills/hai-reldb/reference.md +68 -25
- package/templates/skills/hai-scheduler/SKILL.md +18 -195
- package/templates/skills/hai-scheduler/reference.md +216 -0
- package/templates/skills/hai-serv/SKILL.md +25 -10
- package/templates/skills/hai-serv/reference.md +123 -87
- package/templates/skills/hai-spec-implement/SKILL.md +5 -1
- package/templates/skills/hai-spec-implement/evaluation.md +18 -0
- package/templates/skills/hai-spec-implement/handoff.md +47 -0
- package/templates/skills/hai-spec-implement/templates.md +5 -0
- package/templates/skills/hai-spec-implement/workflow.md +5 -3
- package/templates/skills/hai-spec-review/SKILL.md +4 -0
- package/templates/skills/hai-storage/SKILL.md +23 -238
- package/templates/skills/hai-storage/reference.md +282 -0
- package/templates/skills/hai-ui/SKILL.md +17 -3
- package/templates/skills/hai-ui/guide.md +3 -3
- package/templates/skills/hai-ui/reference.md +26 -11
- package/templates/skills/hai-ui-design/SKILL.md +31 -0
- package/templates/skills/hai-ui-design/patterns.md +26 -0
- package/templates/skills/hai-ui-review/SKILL.md +30 -0
- package/templates/skills/hai-ui-review/checklist.md +22 -0
- package/templates/skills/hai-vecdb/SKILL.md +14 -10
- package/templates/skills/hai-vecdb/reference.md +33 -3
- package/dist/chunk-FWN5B54L.js +0 -468
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -1,104 +1,37 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-pr-review
|
|
3
|
-
description: "审查 PR
|
|
3
|
+
description: "审查 PR、提交或分支的实际差异、调用影响、需求符合性、风险与验证证据,评估交付准备度。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# hai-pr-review
|
|
6
|
+
# hai-pr-review
|
|
7
7
|
|
|
8
8
|
## 能力契约
|
|
9
9
|
|
|
10
10
|
| 项目 | 契约 |
|
|
11
11
|
| --- | --- |
|
|
12
|
-
| 能力 |
|
|
13
|
-
| 适用场景 |
|
|
14
|
-
| 输入 |
|
|
15
|
-
| 输出 |
|
|
16
|
-
| 限制 |
|
|
12
|
+
| 能力 | 对固定 PR/提交/分支差异进行审查,核对需求、调用影响与交付条件 |
|
|
13
|
+
| 适用场景 | 用户要求差异审查、开发后的交付自查或 PR 准备度评估 |
|
|
14
|
+
| 输入 | 实际 base/head、需求与计划、完整 diff、相关实现和 CI/运行证据 |
|
|
15
|
+
| 输出 | 可定位发现、实际覆盖范围、验证缺口和交付结论 |
|
|
16
|
+
| 限制 | 只读审查不自动改代码或发布评论;已有修复授权持续有效。审查不赋予远端推送、合并或发布授权。 |
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 审查路径
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
1. 读取仓库入口和 PR/任务目标,确认比较对象。按 [差异审查](../hai-app-review/differential.md) 固定 base/head、列全文件与重命名,说明 staged/unstaged 和截断限制。
|
|
21
|
+
2. 先将需求、验收与实际 diff 对照,检查遗漏、范围扩张和陈旧模板断言。代码符合性、安全、框架复用和生命周期沿 [hai-app-review](../hai-app-review/SKILL.md);界面涉及交互或布局时调用已交付的 hai-ui-review,纯 API 不需要它。
|
|
22
|
+
3. 按风险读取调用方、类型、配置、迁移、历史和测试;删除校验、事务、资源归属检查时核验真实替代约束。两行权限改动仍可能需要深入分析。
|
|
23
|
+
4. 核对测试证明了什么、是否对应最终代码和真实环境。CI required checks 查询实际状态:成功、失败、运行中、未执行或无权读取分别报告。本地通过不能替代远端 CI;分支规则不可读取时保留该限制。
|
|
24
|
+
5. 对每条发现复核触发路径和反证,合并同根因问题;按 [工作交接契约](../hai-spec-implement/handoff.md) 报告。已授权修复时完成本次范围修复,重验受影响项并针对最终 head 更新结论。
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
- 审查 Pull Request 的 scope、风险、验证方式、issue 关联与合并准备度
|
|
24
|
-
- 判断 AI Review 是 advisory 还是 blocking,以及是否需要人工复核
|
|
25
|
-
- 新增或修改 `.github/pull_request_template.md`
|
|
26
|
-
- 配置 CODEOWNERS、required review、branch protection
|
|
27
|
-
- 设计 AI PR Review 方案
|
|
28
|
-
- 选择 Copilot、CodeRabbit、reviewdog、DangerJS、Claude Code Action、Codex Action 等工具
|
|
26
|
+
## 交付条件
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
- 需求与验收有完成/已有满足/排除/阻塞的实际结论,公共变化同步服务、客户端、迁移、配置、UI/i18n、测试、README 和 skills。
|
|
29
|
+
- PR 描述围绕具体问题、最终行为、验证和剩余边界,按仓库规则关联 issue 或明确需求与验收。要求披露 AI 使用时说明辅助范围与实际复核,不粘贴聊天原文、凭据或私有上下文。
|
|
30
|
+
- 适用门禁对最终代码有效通过;已确认的阻塞发现已解决;项目所需人工审批和 required checks 状态真实可查。未完成项明确列出,不能仅由 AI 建议数量判定可合并。
|
|
31
|
+
- workflow、依赖或发布流程变化按 [hai-ci](../hai-ci/SKILL.md) 检查来源、权限、secret 和执行边界。不得在不可信 PR 代码执行环境暴露密钥。
|
|
31
32
|
|
|
32
|
-
##
|
|
33
|
+
## 报告格式
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
先写实际结论和比较范围,再按严重程度列可行动发现;每条包含文件/当前差异行、触发条件、后果、依据和最小验收。最后列已检查路径、CI/本地/浏览器的实际证据和未验证边界。无发现时直接说明本次范围未确认问题,不补泛泛建议凑数。
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
- 具体 workflow、权限、secret scan 与 required check 配置由 `hai-ci` 定义。
|
|
38
|
-
- 测试覆盖策略与失败用例设计由 `hai-app-tests` 定义。
|
|
39
|
-
|
|
40
|
-
### 第 2 层:结构化人工审查(必须)
|
|
41
|
-
|
|
42
|
-
- scope 是否清晰且与 issue 对齐
|
|
43
|
-
- 测试是否覆盖成功路径、失败路径、边界条件
|
|
44
|
-
- 本次需求是否复用适用框架能力/feature/配置,自定义或新依赖是否有已安装版本的缺口依据(见 [development.md](../hai-app-create/development.md))
|
|
45
|
-
- 是否符合实际返回契约、i18n、日志、分层、当前 lint 风格,以及输入/越权/失败等适用行为验收;不能以导入包数量或 CI 全绿替代代码与安全审查
|
|
46
|
-
- 是否引入密钥、权限扩大、workflow 风险或两仓漂移
|
|
47
|
-
|
|
48
|
-
### 第 3 层:AI Review(默认 advisory)
|
|
49
|
-
|
|
50
|
-
- AI Review 默认只评论、不阻塞 merge。
|
|
51
|
-
- 只有在 CI 稳定、误报可控、团队明确同意后,才考虑将特定静态规则设为 blocking。
|
|
52
|
-
- AI 的建议必须由人类判断,不得自动 merge。
|
|
53
|
-
|
|
54
|
-
## AI-assisted PR 要求
|
|
55
|
-
|
|
56
|
-
PR 描述必须说明:
|
|
57
|
-
|
|
58
|
-
- 是否使用 AI 辅助
|
|
59
|
-
- AI 负责了哪部分:搜索、实现、测试、review、文档
|
|
60
|
-
- 人类如何验证:命令、截图、日志或 reviewer 重点
|
|
61
|
-
- 是否复制了聊天记录;若有,只能复制脱敏后的结论,禁止包含 token、cookie、私有 URL、账号密码
|
|
62
|
-
|
|
63
|
-
## Issue 关联与上下文交接
|
|
64
|
-
|
|
65
|
-
- PR 必须关联 issue 或说明为什么不需要 issue。
|
|
66
|
-
- PR scope 必须与 issue 的目标、验收标准和明确不做事项一致。
|
|
67
|
-
- AI-assisted PR 必须说明 AI 负责了哪些阶段,以及人类如何复核。
|
|
68
|
-
- 多人或多 agent 协作时,PR 描述必须列出 reviewer focus,避免重复 review。
|
|
69
|
-
|
|
70
|
-
## 自动 Review 工具选择
|
|
71
|
-
|
|
72
|
-
| 工具 | 推荐定位 | 是否适合 blocking |
|
|
73
|
-
| --- | --- | --- |
|
|
74
|
-
| reviewdog | 把 lint/static analyzer 结果贴到 PR | 适合,前提是规则稳定 |
|
|
75
|
-
| DangerJS | PR 元数据、模板、标签、变更范围规则 | 适合,前提是规则明确 |
|
|
76
|
-
| GitHub Copilot Code Review | 通用 AI 代码建议 | 默认 advisory |
|
|
77
|
-
| CodeRabbit | 上下文型 AI review | 默认 advisory |
|
|
78
|
-
| Claude Code Action | 定制化深度 review / 修改建议 | 默认 advisory,严格限制权限 |
|
|
79
|
-
| Codex Action | 定制化 review / 修复建议 | 默认 advisory,严格控制 API key |
|
|
80
|
-
|
|
81
|
-
## 权限与安全红线
|
|
82
|
-
|
|
83
|
-
- ❌ 不在 `pull_request_target` 中运行 AI agent 读取 PR 代码和 secrets。
|
|
84
|
-
- ❌ 不给 AI Review workflow 默认 `contents: write`、`pull-requests: write`,除非只用于评论且触发源可信。
|
|
85
|
-
- ❌ 不允许 AI 自动合并 PR。
|
|
86
|
-
- ❌ 不把私有提示词、聊天原文、token、cookie、连接串写入 PR。
|
|
87
|
-
- ✅ AI Review 的 prompt 必须限制任务范围、禁止执行 secrets、要求引用具体文件/行。
|
|
88
|
-
|
|
89
|
-
## PR Review 检查清单
|
|
90
|
-
|
|
91
|
-
- [ ] PR 关联 issue,scope 单一
|
|
92
|
-
- [ ] PR 模板完整填写
|
|
93
|
-
- [ ] CI required checks 全绿
|
|
94
|
-
- [ ] 新行为有测试,失败路径有覆盖
|
|
95
|
-
- [ ] 框架能力选择、代码风格与适用安全边界已由 `hai-app-review` 覆盖,有行为证据或明确未验证项
|
|
96
|
-
- [ ] 文档 / skill / README 与代码同步
|
|
97
|
-
- [ ] workflow 权限或 secrets 变化已由 `hai-ci` 审查
|
|
98
|
-
- [ ] AI-assisted 内容已由人类复核
|
|
99
|
-
|
|
100
|
-
## 相关 Skills
|
|
101
|
-
|
|
102
|
-
- `hai-ci` — CI/CD 与 workflow 安全
|
|
103
|
-
- `hai-app-review` — 应用代码质量与框架复用审查
|
|
104
|
-
- `hai-framework-sync` — 框架升级差异与双仓同步审查
|
|
37
|
+
仅要求审查时在报告处结束;已获修复授权则修复并复核。提交评论、创建 PR、远端推送、合并与发布按用户的实际授权继续,遵守项目规则。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-reach
|
|
3
|
-
description: "
|
|
3
|
+
description: "接入邮件、短信、API 通知与模板,处理发送结果、免打扰和可靠延迟入队。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hai-reach
|
|
@@ -17,12 +17,14 @@ description: "使用 @h-ai/reach 配置邮件、短信、API 通知和多渠道
|
|
|
17
17
|
|
|
18
18
|
## 使用路径
|
|
19
19
|
|
|
20
|
-
- 先配置并注册 Provider,再保存绑定 Provider 的模板;模板发送只传 template/to/vars 即可,显式 provider 可覆盖;直接发送须提供 provider
|
|
20
|
+
- 先配置并注册 Provider,再保存绑定 Provider 的模板;模板发送只传 template/to/vars 即可,显式 provider 可覆盖;直接发送须提供 provider。启用 DND delay 必须先初始化 reldb 并确保队列表可用;成功时 `deferred: true` 与 `queueId` 表示可靠入队,尚未投递;缺库、断连或插入失败须按 HaiResult 失败处理。即时投递与排队状态须区分。
|
|
21
21
|
- 验证模板替换和错误分支时使用隔离测试收件人;真实通知须在用户授权范围内。
|
|
22
|
+
- 缓存锁只减少扫描,按实际执行逐条领取并在途续租。API 只有接收端真正具备原子持久去重时才能声明 idempotent=true,全部请求固定用 queueId 作为 Idempotency-Key;旧节点迟到请求也必须去重。SMTP/短信/默认 API 的在途失败或过期转 unknown,禁止自动重发;可信服务端用 queue.get/resolveUnknown 在人工隔离旧 worker、核对外部记录后裁定 sent/discarded。
|
|
23
|
+
- queue 重试策略随消息持久保存,默认最多领取 5 次,指数退避 60 秒至 1 小时;永久失败/超限进 dead,满批失败不会原地自旋。可信服务端用 queue.list 查询 dead/unknown;修复根因后以最新 updatedAt 调用 requeueDead,只能恢复 dead,保留同一 queueId 和接收端幂等要求。unknown 不能重排。
|
|
22
24
|
|
|
23
25
|
## 按需参考
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
按任务读取 [reference.md](reference.md) 的对应小节;签名以当前源码或安装版本的公开类型为准,不默认加载全部资料。示例中的业务变量由应用提供。
|
|
26
28
|
|
|
27
29
|
- 运行环境
|
|
28
30
|
- 依赖
|
|
@@ -31,6 +33,7 @@ description: "使用 @h-ai/reach 配置邮件、短信、API 通知和多渠道
|
|
|
31
33
|
- 核心 API
|
|
32
34
|
- 错误码
|
|
33
35
|
- 常见模式
|
|
36
|
+
- 租约、接收端幂等与未知结果
|
|
34
37
|
- 相关 Skills
|
|
35
38
|
|
|
36
39
|
IAM 通知使用 `otp_email`、`otp_sms`、`password_reset`、`password_reset_sms` 标准模板;将已初始化的 `reach` 传入 `iam.init`。Console Provider 只记录元数据,不输出验证码/令牌,不代替实际邮件/短信发送。
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
| 模块 | 用途 | 是否必需 | 初始化要求 |
|
|
14
14
|
| --- | --- | --- | --- |
|
|
15
|
-
| `@h-ai/reldb` | 数据库(发送日志与模板持久化) |
|
|
15
|
+
| `@h-ai/reldb` | 数据库(发送日志与模板持久化) | DND delay 必需;其他场景可选 | 启用 delay 前先初始化,队列表不可用则 reach.init 返回 CONFIG_ERROR |
|
|
16
16
|
| `@h-ai/cache` | 缓存(DND delay 策略分布式锁) | 可选 | 已初始化时自动启用分布式锁 |
|
|
17
17
|
|
|
18
18
|
## 适用场景
|
|
@@ -48,6 +48,7 @@ providers:
|
|
|
48
48
|
- name: webhook
|
|
49
49
|
type: api
|
|
50
50
|
url: ${HAI_REACH_WEBHOOK_URL:}
|
|
51
|
+
idempotent: false # 只有接收端按 Idempotency-Key 原子、持久化去重时才能设 true
|
|
51
52
|
|
|
52
53
|
# 模板(可选,也可通过代码注册)
|
|
53
54
|
templates:
|
|
@@ -62,7 +63,7 @@ templates:
|
|
|
62
63
|
# 免打扰(可选)
|
|
63
64
|
dnd:
|
|
64
65
|
enabled: true
|
|
65
|
-
strategy: delay #
|
|
66
|
+
strategy: delay # 必须先初始化 reldb;确认持久化成功后才返回 deferred 与 queueId
|
|
66
67
|
start: '22:00'
|
|
67
68
|
end: '08:00'
|
|
68
69
|
```
|
|
@@ -80,12 +81,18 @@ import { core } from '@h-ai/core'
|
|
|
80
81
|
import { reach } from '@h-ai/reach'
|
|
81
82
|
import { reldb } from '@h-ai/reldb'
|
|
82
83
|
|
|
83
|
-
//
|
|
84
|
-
await reldb.init(core.config.get('db'))
|
|
85
|
-
|
|
84
|
+
// 当前配置启用了 delay:数据库和队列表为必需依赖。
|
|
85
|
+
const database = await reldb.init(core.config.get('db'))
|
|
86
|
+
if (!database.success)
|
|
87
|
+
throw new Error(database.error.message)
|
|
88
|
+
const cached = await cache.init(core.config.get('cache')) // 可选,启用 flush 分布式锁
|
|
89
|
+
if (!cached.success)
|
|
90
|
+
throw new Error(cached.error.message)
|
|
86
91
|
|
|
87
92
|
// 再初始化触达模块(自动检测已初始化的 reldb/cache 单例)
|
|
88
|
-
await reach.init(core.config.get('reach'))
|
|
93
|
+
const initialized = await reach.init(core.config.get('reach'))
|
|
94
|
+
if (!initialized.success)
|
|
95
|
+
throw new Error(initialized.error.message)
|
|
89
96
|
// ... 使用触达服务
|
|
90
97
|
await reach.close()
|
|
91
98
|
```
|
|
@@ -119,6 +126,12 @@ const result = await reach.send({
|
|
|
119
126
|
template: 'verification_code',
|
|
120
127
|
vars: { code: '123456', minutes: '5' },
|
|
121
128
|
})
|
|
129
|
+
if (!result.success)
|
|
130
|
+
throw new Error(result.error.message)
|
|
131
|
+
if (result.data.deferred) {
|
|
132
|
+
// queueId 是 hai_reach_send_log.queue_id 中的回执 UUID;此时尚未投递。
|
|
133
|
+
await savePendingReceipt(result.data.queueId)
|
|
134
|
+
}
|
|
122
135
|
|
|
123
136
|
// 直接发送邮件(无模板)
|
|
124
137
|
await reach.send({
|
|
@@ -152,6 +165,10 @@ await reach.send({
|
|
|
152
165
|
| --------------------- | -------------------------------------------------------- | ---------------------------------- |
|
|
153
166
|
| `reach.init` | `(config: ReachConfigInput) => Promise<HaiResult<void>>` | 初始化(注册多个 Provider) |
|
|
154
167
|
| `reach.send` | `(message: ReachMessage) => Promise<HaiResult<SendResult>>` | 发送消息(通过 provider 字段路由) |
|
|
168
|
+
| `reach.queue.get` | `(queueId: string) => Promise<HaiResult<ReachQueueEntry \| null>>` | 不含正文/收件人的队列状态 |
|
|
169
|
+
| `reach.queue.list` | `(options: ReachQueueListOptions) => Promise<HaiResult<ReachQueueEntry[]>>` | 可信服务端分页查询 dead/unknown,每页至多 100 条 |
|
|
170
|
+
| `reach.queue.requeueDead` | `(queueId: string, options: ReachDeadRequeue) => Promise<HaiResult<void>>` | 按 expectedUpdatedAt 条件恢复 dead;unknown 禁止重排 |
|
|
171
|
+
| `reach.queue.resolveUnknown` | `(queueId: string, resolution: ReachUnknownResolution) => Promise<HaiResult<void>>` | 人工核对后将 unknown 裁定为 sent/discarded,不发送 |
|
|
155
172
|
| `reach.template` | `ReachTemplateRegistry` | 模板注册表 |
|
|
156
173
|
| `reach.config` | `ReachConfig \| null` | 当前脱敏配置快照 |
|
|
157
174
|
| `reach.isInitialized` | `boolean` | 是否已初始化 |
|
|
@@ -183,6 +200,7 @@ interface ReachConfigInput {
|
|
|
183
200
|
| `template` | `string` | — | 模板名称(模板发送时) |
|
|
184
201
|
| `vars` | `Record<string, string>` | — | 模板变量 |
|
|
185
202
|
| `extra` | `Record<string, unknown>` | — | Provider 扩展参数(如短信模板编码) |
|
|
203
|
+
| `idempotencyKey` | `string` | — | 直接 API 请求可指定;队列投递由框架固定为 queueId |
|
|
186
204
|
|
|
187
205
|
### ReachTemplate(模板绑定 Provider)
|
|
188
206
|
|
|
@@ -198,18 +216,63 @@ interface ReachConfigInput {
|
|
|
198
216
|
| 错误码 | code | 说明 |
|
|
199
217
|
|--------|------|------|
|
|
200
218
|
| `HaiReachError.SEND_FAILED` | `hai:reach:001` | 发送失败 |
|
|
219
|
+
| `HaiReachError.DELIVERY_REJECTED` | `hai:reach:020` | API 明确永久拒绝 |
|
|
201
220
|
| `HaiReachError.TEMPLATE_NOT_FOUND` | `hai:reach:002` | 模板未找到 |
|
|
202
221
|
| `HaiReachError.TEMPLATE_RENDER_FAILED` | `hai:reach:003` | 模板渲染失败 |
|
|
203
222
|
| `HaiReachError.INVALID_RECIPIENT` | `hai:reach:004` | 无效接收方 |
|
|
204
223
|
| `HaiReachError.PROVIDER_NOT_FOUND` | `hai:reach:005` | Provider 未找到 |
|
|
205
224
|
| `HaiReachError.DND_BLOCKED` | `hai:reach:006` | 免打扰丢弃 |
|
|
206
|
-
| `HaiReachError.DND_DEFERRED` | `hai:reach:007` | 免打扰延时暂存 |
|
|
207
225
|
| `HaiReachError.NOT_INITIALIZED` | `hai:reach:010` | 模块未初始化 |
|
|
208
226
|
| `HaiReachError.UNSUPPORTED_TYPE` | `hai:reach:011` | 不支持的类型 |
|
|
209
227
|
| `HaiReachError.CONFIG_ERROR` | `hai:reach:012` | 配置错误 |
|
|
210
228
|
|
|
211
229
|
## 常见模式
|
|
212
230
|
|
|
231
|
+
### 可靠延迟入队
|
|
232
|
+
|
|
233
|
+
启用 delay 后,只有数据库确认插入一条 pending 记录才返回 `deferred: true` 和 `queueId`。缺库、断连、插入失败返回失败 HaiResult,不能向用户报告已入队。`queueId` 对应 `hai_reach_send_log.queue_id`,同一行的 `status` 为 pending / processing / sending / unknown / sent / discarded / dead。节点重启后连接同一数据库、配置同名 Provider,可在 DND 时段外恢复投递。即时发送后的普通日志仍为可选,日志失败不会使已发送消息返回失败。
|
|
234
|
+
|
|
235
|
+
验证码和密码重置等有时效的通知应明确选择 DND 策略;可靠入队成功不代表用户已收到验证码。
|
|
236
|
+
|
|
237
|
+
队列 `created_at` / `updated_at` / `locked_until` / `next_attempt_at` 均为 Unix 毫秒 `BIGINT`(SQLite 为 64 位 INTEGER);仓库把 driver 的字符串/数字归一化为安全整数 number,并拒绝负数、小数和不安全整数。模板仓库的 TIMESTAMP 时间返回 Date。已有表须在部署时同步列结构,包含次数、退避策略和失败分类,模块不自动迁移旧表;跨库验收必须运行 PostgreSQL/MySQL 用例,SQLite 无法证明 32 位溢出已经消除。
|
|
238
|
+
|
|
239
|
+
### 租约、接收端幂等与未知结果
|
|
240
|
+
|
|
241
|
+
每轮最多 100 条,逐条 claim,发送前原子写入 sending 并在途续租;过期 owner 的开始、续租、确认和释放均被拒绝。DND 时段外每分钟恢复遗留任务。cache 锁只是扫描优化,不能提供外部副作用的 exactly-once。
|
|
242
|
+
|
|
243
|
+
API `idempotent: true` 是接收端能力声明:接收方必须把 queueId 对应的去重记录与业务副作用原子、持久化提交。所有尝试用相同 `Idempotency-Key` 头和 `idempotencyKey` 正文,覆盖响应丢失和旧节点恢复;去重记录须覆盖所有迟到请求,迟到无上界时不能过期。同名 Provider 的目标与幂等存储保持一致。只有此模式能自动接管已开始的 API 投递。
|
|
244
|
+
|
|
245
|
+
SMTP、短信和默认 API 一旦开始投递,失败或过期转 unknown,不自动重发。未连接按持久策略退避;发送前解析失败直接进入 dead。unknown 必须在可信服务端管理流程里,先停止/隔离旧 worker 并核对第三方投递记录,再执行以下二选一裁定;discarded 不撤销已开始请求。需要补发时,先确保旧请求不可能继续生效,再创建新消息。
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const state = await reach.queue.get(queueId)
|
|
249
|
+
const resolved = await reach.queue.resolveUnknown(queueId, { status: 'sent', messageId: verifiedMessageId })
|
|
250
|
+
if (!resolved.success)
|
|
251
|
+
throw new Error(resolved.error.message)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
管理入口校验回执 UUID、status 与 messageId 长度,只能从 unknown 条件更新为 sent/discarded;不提供重排队选项,并发裁定仅一个成功。
|
|
255
|
+
|
|
256
|
+
### 有限重试与死信运维
|
|
257
|
+
|
|
258
|
+
`queue.maxAttempts` 默认 5(1–100),`retryDelayMs` 默认 60000,`maxRetryDelayMs` 默认 3600000;两项毫秒配置范围 1000–86400000,最大退避不得小于首次退避。策略随入队记录保存,不随新节点配置重置;领取(含崩溃恢复)原子累加 attemptCount。暂时故障保存 nextAttemptAt,按 `min(maxRetryDelayMs, retryDelayMs × 2^(attemptCount−1))` 退避。DND 外每分钟扫描,一轮最多 100 条且固定资格时间,失败行不会在同一轮反复领取。缺失/未连接 Provider、网络错误、HTTP 408/425/429/5xx 允许有限重试;坏 JSON、幂等 API 其它 4xx 直接 dead;超限(含最后一次领取后崩溃)停止自动处理。非幂等在途仍隔离 unknown,不能借重试预算重发。
|
|
259
|
+
|
|
260
|
+
queue.get/list 返回 attemptCount、maxAttempts、nextAttemptAt、lastFailure、updatedAt 等安全元数据。lastFailure 为 provider_unavailable、invalid_message、permanent_rejection、transient_failure 或 attempts_exhausted,不保存响应正文。可信服务端以 `list({ status: 'dead', limit: 50, offset: 0 })` 分页查询,status 也可选 unknown;limit 1–100,offset 0–1000000。
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
// 应用先校验运维权限并修复根因;不向浏览器直接暴露队列管理。
|
|
264
|
+
const state = await reach.queue.get(queueId)
|
|
265
|
+
if (!state.success)
|
|
266
|
+
throw new Error(state.error.message)
|
|
267
|
+
if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
|
|
268
|
+
const restored = await reach.queue.requeueDead(queueId, { expectedUpdatedAt: state.data.updatedAt })
|
|
269
|
+
if (!restored.success)
|
|
270
|
+
throw new Error(restored.error.message)
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
恢复仅匹配当前 dead 版本,过时/并发请求失败;重置次数但保留 queueId 和原策略。unknown 永远不能通过此入口重排。dead 不代表外部副作用未发生,旧幂等 API 请求仍可能迟到,恢复前确认接收端持久去重;不可修复的坏消息留在 dead 人工排查。
|
|
275
|
+
|
|
213
276
|
### 多渠道验证码发送
|
|
214
277
|
|
|
215
278
|
```typescript
|
|
@@ -244,7 +307,7 @@ import { iam } from '@h-ai/iam'
|
|
|
244
307
|
import { reach } from '@h-ai/reach'
|
|
245
308
|
|
|
246
309
|
// 初始化 reach(注册 email 和 sms Provider)
|
|
247
|
-
await reach.init({
|
|
310
|
+
const initializedReach = await reach.init({
|
|
248
311
|
providers: [
|
|
249
312
|
{ name: 'email', type: 'smtp', host: 'smtp.example.com', from: 'noreply@example.com' },
|
|
250
313
|
{ name: 'sms', type: 'aliyun-sms', accessKeyId: '...', accessKeySecret: '...', signName: '...' },
|
|
@@ -255,23 +318,17 @@ await reach.init({
|
|
|
255
318
|
{ name: 'otp_sms', provider: 'sms', body: 'Your code is {code}' },
|
|
256
319
|
],
|
|
257
320
|
})
|
|
321
|
+
if (!initializedReach.success)
|
|
322
|
+
throw new Error(initializedReach.error.message)
|
|
258
323
|
|
|
259
|
-
//
|
|
260
|
-
await iam.init({
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
onPasswordResetRequest: async (user, token, expiresAt) => {
|
|
264
|
-
await reach.send({ to: user.email ?? '', template: 'password_reset', vars: { token, expiresAt: expiresAt.toISOString() } })
|
|
265
|
-
},
|
|
266
|
-
onOtpSendEmail: async (email, code) => {
|
|
267
|
-
await reach.send({ to: email, template: 'otp_email', vars: { code } })
|
|
268
|
-
},
|
|
269
|
-
onOtpSendSms: async (phone, code) => {
|
|
270
|
-
await reach.send({ to: phone, template: 'otp_sms', vars: { code } })
|
|
271
|
-
},
|
|
272
|
-
})
|
|
324
|
+
// 先确认 reldb/cache/Reach 已初始化成功,再注入 Reach;内置适配器传播发送失败。
|
|
325
|
+
const initializedIam = await iam.init({ ...iamSettings, reach })
|
|
326
|
+
if (!initializedIam.success)
|
|
327
|
+
throw new Error(initializedIam.error.message)
|
|
273
328
|
```
|
|
274
329
|
|
|
330
|
+
若使用显式通知回调覆盖内置适配器,必须检查 `reach.send()` 的 HaiResult,失败时拒绝 Promise;不得吞掉入队失败。需要即时送达的验证码不应启用延迟策略。
|
|
331
|
+
|
|
275
332
|
### 错误处理
|
|
276
333
|
|
|
277
334
|
```typescript
|
|
@@ -300,3 +357,7 @@ if (!result.success) {
|
|
|
300
357
|
- `hai-core` — 配置加载、日志、HaiResult 模式
|
|
301
358
|
- `hai-reldb` — 数据库操作(存储发送记录等)
|
|
302
359
|
- `hai-cache` — 缓存操作、分布式锁(DND flush 互斥)
|
|
360
|
+
|
|
361
|
+
## 生产共享状态
|
|
362
|
+
|
|
363
|
+
生产拒绝 console Provider,须配置真实 SMTP/短信/API 服务。console 的日志成功不是外部投递成功;非幂等服务的未知结果必须核对,不能自动重发证明 exactly-once。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化成功前检查,只有 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试豁免。不要将租约当作最终写入条件,外部未知效果须按稳定业务键核对。
|
|
@@ -13,18 +13,19 @@ description: "使用 @h-ai/reldb 访问 SQLite/PostgreSQL/MySQL,编写 SQL、C
|
|
|
13
13
|
| 适用场景 | 关系数据读写、仓库封装或事务一致性 |
|
|
14
14
|
| 输入 | ReldbConfigInput、参数化 SQL、CRUD 配置、事务句柄 |
|
|
15
15
|
| 输出 | DB 操作为 HaiResult;crud.table 和分页/JSON 工具按签名返回 |
|
|
16
|
-
| 限制 |
|
|
16
|
+
| 限制 | 运行时仅服务端,纯 schemas 入口可用于浏览器;tx.wrap 只在回调抛错时回滚,返回失败 HaiResult 不会触发回滚。动态值参数化,标识符用白名单。 |
|
|
17
17
|
|
|
18
18
|
## 使用路径
|
|
19
19
|
|
|
20
|
-
- `_db.yml` 对应 core.config 的 db;先校验 ReldbConfigSchema,再 init 并检查 success
|
|
20
|
+
- `_db.yml` 对应 core.config 的 db;先校验 ReldbConfigSchema,再 init 并检查 success。仅做配置校验可从 `@h-ai/reldb/schemas` 导入,纯入口只依赖 Zod,不加载数据库实例/驱动,可用于 CLI 和浏览器;运行时数据库操作仍仅服务端。
|
|
21
21
|
- `reldb.crud.table` / `tx.crud.table` 的 `dbType` 必填并与实际后端一致;`BaseReldbCrudRepository` 自动读取已初始化的配置。
|
|
22
22
|
- 事务每步检查结果,避免把失败当普通返回值提交;同一事务操作须共享 tx。
|
|
23
23
|
- SQL 值参数化;where/orderBy/json 列名不是任意用户输入。分页使用 PaginatedResult。
|
|
24
|
+
- MySQL 的 pool 仅 max/acquireTimeout/queueLimit;超时返回 TIMEOUT 并移除排队项,队列满返回 POOL_EXHAUSTED,迟到建连结算前仍占槽位。min/idleTimeout/timezone 不支持。URL 优先于分字段且禁止 query/fragment;TLS 仅 false 或严格对象,默认验证证书和 DNS 身份,验证开启的 IP 目标拒绝,cert/key 成对。详见 reference 的配置章节。
|
|
24
25
|
|
|
25
26
|
## 按需参考
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
按任务读取 [reference.md](reference.md) 的对应小节;签名以当前源码或安装版本的公开类型为准,不默认加载全部资料。示例中的业务变量由应用提供。
|
|
28
29
|
|
|
29
30
|
- 运行环境
|
|
30
31
|
- 适用场景
|
|
@@ -33,3 +34,5 @@ description: "使用 @h-ai/reldb 访问 SQLite/PostgreSQL/MySQL,编写 SQL、C
|
|
|
33
34
|
- 错误码 — `HaiReldbError`
|
|
34
35
|
- 常见模式
|
|
35
36
|
- 相关 Skills
|
|
37
|
+
|
|
38
|
+
JSON 对象合并使用 RFC 7396 补丁,必须将返回的 SQL/params 放在同一次 UPDATE 内;禁止先读对象再覆盖整个列来模拟跨节点原子合并。路径函数的后端差异和完整可类型检查示例见 [reference.md](reference.md)。
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
> `@h-ai/reldb` 提供统一的数据库操作接口,支持 SQLite、PostgreSQL、MySQL,包含 DDL、SQL、CRUD 抽象、事务与分页。
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
派生 `BaseReldbCrudRepository` 的自定义 SQL 方法先检查 `await this.ensureReady()`;构造函数启动自动建表不等于仓库就绪,服务 init 必须等待候选仓库的就绪入口再发布。PostgreSQL 并发首次 `CREATE TABLE IF NOT EXISTS` 的 `23505/pg_type_typname_nsp_index` 目录冲突仅重执行相同 DDL 一次,其他错误继续失败。
|
|
9
|
+
|
|
7
10
|
## 运行环境
|
|
8
11
|
|
|
9
12
|
> ⚠️ **服务端模块(Node.js only)。** 浏览器端请通过 `@h-ai/api-contract` + `@h-ai/serv` 暴露的 API 端点间接访问数据库。
|
|
@@ -20,6 +23,8 @@
|
|
|
20
23
|
|
|
21
24
|
### 1. 配置
|
|
22
25
|
|
|
26
|
+
只做校验时从 `@h-ai/reldb/schemas` 导入同一组 Schema/类型;该入口只依赖 Zod,无数据库实例或驱动。MySQL `ssl` 只接受 `false` 或严格对象;`{}` 默认开启证书链和 DNS 身份验证,不能用 `true` 或 PostgreSQL 字符串模式。实际连接仍需服务端驱动。
|
|
27
|
+
|
|
23
28
|
```yaml
|
|
24
29
|
# config/_db.yml
|
|
25
30
|
type: sqlite
|
|
@@ -39,20 +44,46 @@ operationLog:
|
|
|
39
44
|
约定环境变量优先于 YAML,例如 `HAI_DB_TYPE`、`HAI_DB_DATABASE`、
|
|
40
45
|
`HAI_DB_OPERATIONLOG_LEVEL`。
|
|
41
46
|
|
|
42
|
-
`operationLog` 在 reldb provider-base 的 DML 操作层统一输出日志,不要在 `reldb-main.ts`、各 raw provider 或调用方重复包装。`read` 覆盖 `query/get/queryPage`,`write` 覆盖 DDL、`execute/batch` 与 `tx.begin/commit/rollback`,`maxLength`
|
|
47
|
+
`operationLog` 在 reldb provider-base 的 DML 操作层统一输出日志,不要在 `reldb-main.ts`、各 raw provider 或调用方重复包装。`read` 覆盖 `query/get/queryPage`,`write` 覆盖 DDL、`execute/batch` 与 `tx.begin/commit/rollback`,`maxLength` 限制安全数量摘要的字符数(0省略摘要);SQL原文、参数值、DDL默认值和驱动错误内容不写入日志,`level` 默认 `debug`。
|
|
43
48
|
|
|
44
49
|
### 2. 初始化与关闭
|
|
45
50
|
|
|
51
|
+
MySQL `pool` 仅支持 `{ max: 10, acquireTimeout: 10000, queueLimit: 1000 }`(也是省略时默认值)。`acquireTimeout` 为 1–2147483647 毫秒的整数,覆盖排队和建连;queueLimit 为正整数。SQL/DDL/CRUD/batch/tx 共用有界 FIFO,超时移除并返回 `TIMEOUT`,满额返回 `POOL_EXHAUSTED`;迟到建连结算前仍占槽位,只归还连接,不执行 SQL。close 立即取消排队,再等待借用/事务和驱动有界建连完成,不中止已启动 SQL。min/idleTimeout/mysql.timezone 明确拒绝,不承诺 MySQL 最小保留或可配置空闲回收;PostgreSQL 保留自己的 min/idleTimeout/acquireTimeout=0 契约,min 不会预建连接。
|
|
52
|
+
|
|
53
|
+
MySQL 仅传 mysql:// URL 即可连接,优先于 host/port/database/user/password;无 URL 时 database 必填。禁止 URL query/fragment,pool/mysql.charset/ssl/operationLog 仍独立适用。ssl 仅 false 或严格对象,空对象默认校验证书链和 DNS 身份;ca/cert/key 为 PEM 内容,cert/key 必须成对。当前驱动不验证 IP 身份,验证开启的 IP 目标返回 CONFIG_ERROR;关闭验证有固定风险日志,加密不代表身份认证。环境变量为 HAI_DB_POOL_* / HAI_DB_SSL_*,min/idleTimeout 仅适用 PostgreSQL,queueLimit/verifyIdentity 仅适用 MySQL。
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// trustedCaPem 从部署信任配置提供。
|
|
57
|
+
const result = await reldb.init({ type: 'mysql', host: 'db.example.com', database: 'app', pool: { max: 10, acquireTimeout: 5000, queueLimit: 100 }, ssl: { ca: trustedCaPem } })
|
|
58
|
+
if (!result.success) return result
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
PostgreSQL 空闲连接错误已监听,由池移除并在下一次查询替换;日志仅含固定说明与安全 code/name,不记录驱动错误原文。关闭会等待已借出事务完成,应用必须提交/回滚并释放句柄。
|
|
62
|
+
|
|
46
63
|
```typescript
|
|
47
64
|
import { core } from '@h-ai/core'
|
|
48
65
|
import { reldb } from '@h-ai/reldb'
|
|
49
66
|
|
|
50
67
|
await reldb.init(core.config.get('db'))
|
|
51
68
|
// ... 使用数据库
|
|
52
|
-
await reldb.close()
|
|
69
|
+
const closed = await reldb.close()
|
|
70
|
+
if (!closed.success) throw new Error(closed.error.message)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`reldb.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const closed = await reldb.close()
|
|
78
|
+
if (!closed.success) {
|
|
79
|
+
// 处理清理故障后有限重试;再次失败应向应用报告。
|
|
80
|
+
const retried = await reldb.close()
|
|
81
|
+
if (!retried.success) throw new Error(retried.error.message)
|
|
82
|
+
}
|
|
53
83
|
```
|
|
54
84
|
|
|
55
|
-
|
|
85
|
+
|
|
86
|
+
`reldb.config` 返回脱敏后的配置快照;数据库密码、URL 内嵌凭证和 TLS 私钥 `ssl.key` 会隐藏,不能用快照回放原始连接配置。
|
|
56
87
|
|
|
57
88
|
### 3. 选择操作接口
|
|
58
89
|
|
|
@@ -103,7 +134,7 @@ const columns: ReldbTableDef = {
|
|
|
103
134
|
|
|
104
135
|
```typescript
|
|
105
136
|
interface ReldbColumnDef {
|
|
106
|
-
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'BOOLEAN' | 'TIMESTAMP' | 'JSON'
|
|
137
|
+
type: 'TEXT' | 'INTEGER' | 'BIGINT' | 'REAL' | 'BLOB' | 'BOOLEAN' | 'TIMESTAMP' | 'JSON'
|
|
107
138
|
primaryKey?: boolean
|
|
108
139
|
autoIncrement?: boolean
|
|
109
140
|
notNull?: boolean
|
|
@@ -118,7 +149,8 @@ interface ReldbColumnDef {
|
|
|
118
149
|
| 类型 | SQLite | PostgreSQL | MySQL |
|
|
119
150
|
| --------- | ------- | ---------------- | ------------ |
|
|
120
151
|
| TEXT | TEXT | TEXT | VARCHAR(255) |
|
|
121
|
-
| INTEGER | INTEGER | INTEGER/
|
|
152
|
+
| INTEGER | INTEGER | INTEGER/BIGSERIAL | INT/BIGINT |
|
|
153
|
+
| BIGINT | INTEGER(64 位) | BIGINT | BIGINT |
|
|
122
154
|
| REAL | REAL | DOUBLE PRECISION | DOUBLE |
|
|
123
155
|
| BLOB | BLOB | BYTEA | BLOB |
|
|
124
156
|
| BOOLEAN | INTEGER | BOOLEAN | TINYINT(1) |
|
|
@@ -127,6 +159,8 @@ interface ReldbColumnDef {
|
|
|
127
159
|
|
|
128
160
|
> MySQL 将 TEXT 映射为 VARCHAR(255) 以支持索引和 UNIQUE 约束。INTEGER + autoIncrement 在 MySQL 映射为 BIGINT。
|
|
129
161
|
|
|
162
|
+
Unix 毫秒使用 BIGINT,不能使用 PostgreSQL/MySQL 的普通 INTEGER。原始 SQL/CRUD 与 BaseReldbCrudRepository 的 BIGINT 保留 driver 的 number/string/bigint 类型;业务需要 number 时须在读取边界校验非负整数和 Number.isSafeInteger,拒绝精度丢失。TIMESTAMP 经 BaseReldbCrudRepository 映射为 Date。
|
|
163
|
+
|
|
130
164
|
### SQL — `reldb.sql`
|
|
131
165
|
|
|
132
166
|
| 方法 | 签名 | 说明 |
|
|
@@ -356,7 +390,7 @@ interface PaginatedResult<T> {
|
|
|
356
390
|
|
|
357
391
|
通过 `reldb.json` 构建跨数据库统一的 JSON 路径操作 SQL 表达式,返回 `{ sql, params }` 可直接嵌入 `reldb.sql.*`。
|
|
358
392
|
|
|
359
|
-
|
|
393
|
+
支持简单路径 `$.key`、`$.key.subkey`、`$[0]`。
|
|
360
394
|
|
|
361
395
|
| 操作 | SQLite | PostgreSQL | MySQL |
|
|
362
396
|
| --------- | ------------------ | ------------------------ | ---------------------- |
|
|
@@ -364,30 +398,33 @@ interface PaginatedResult<T> {
|
|
|
364
398
|
| `set` | `json_set` | `jsonb_set` | `JSON_SET` |
|
|
365
399
|
| `insert` | `json_insert` | `jsonb_insert` | `JSON_INSERT` |
|
|
366
400
|
| `remove` | `json_remove` | `#-` (text[]) | `JSON_REMOVE` |
|
|
367
|
-
| `merge` | `json_patch` |
|
|
401
|
+
| `merge` | `json_patch` | 参数化递归 CTE | `JSON_MERGE_PATCH` |
|
|
402
|
+
|
|
403
|
+
`merge` 的补丁根限于可 JSON 序列化对象:三种后端均按 RFC 7396 递归保留未变键、用 null 删除指定键、整体替换数组;PostgreSQL 使用参数化递归 CTE,在同一次 SQL 内更新当前行,不做应用先读再写。路径和值全部参数化,column 只能由开发者静态编写。路径解析只支持简单点号/方括号形式,不是完整 SQL/JSON Path 校验器;PostgreSQL set 要求父路径存在,insert 对已有对象键报错且数组按原生位置插入,不应把不同后端的原生路径行为视为完全相同。
|
|
368
404
|
|
|
369
405
|
```typescript
|
|
370
|
-
|
|
371
|
-
const { sql, params } = reldb.json.extract('settings', '$.theme')
|
|
372
|
-
const rows = await reldb.sql.query(
|
|
373
|
-
`SELECT * FROM users WHERE ${sql} = ?`,
|
|
374
|
-
[...params, '"dark"'],
|
|
375
|
-
)
|
|
406
|
+
import { reldb, type ReldbFunctions } from '@h-ai/reldb'
|
|
376
407
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
`
|
|
381
|
-
|
|
382
|
-
)
|
|
408
|
+
// db 已初始化,users 表已有 settings/profile JSON 列。
|
|
409
|
+
async function updateSettings(db: ReldbFunctions, userId: string) {
|
|
410
|
+
const extracted = db.json.extract('settings', '$.theme')
|
|
411
|
+
const rows = await db.sql.query(`SELECT * FROM users WHERE ${extracted.sql} = ?`, [...extracted.params, '"dark"'])
|
|
412
|
+
if (!rows.success) return rows
|
|
383
413
|
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
414
|
+
const changed = db.json.set('settings', '$.theme', 'dark')
|
|
415
|
+
const updated = await db.sql.execute(`UPDATE users SET settings = ${changed.sql} WHERE id = ?`, [...changed.params, userId])
|
|
416
|
+
if (!updated.success) return updated
|
|
387
417
|
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
418
|
+
const removed = db.json.remove('settings', '$.deprecated')
|
|
419
|
+
const deleted = await db.sql.execute(`UPDATE users SET settings = ${removed.sql} WHERE id = ?`, [...removed.params, userId])
|
|
420
|
+
if (!deleted.success) return deleted
|
|
421
|
+
|
|
422
|
+
const patch = db.json.merge('profile', { bio: '新简介', avatar: null })
|
|
423
|
+
return db.sql.execute(`UPDATE users SET profile = ${patch.sql} WHERE id = ?`, [...patch.params, userId])
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const result = await updateSettings(reldb, 'u1')
|
|
427
|
+
if (!result.success) throw new Error(result.error.message)
|
|
391
428
|
```
|
|
392
429
|
|
|
393
430
|
> `column` 参数为列名,**禁止传入用户输入**(开发者负责安全性)。
|
|
@@ -453,3 +490,9 @@ const result = await reldb.tx.wrap(async (tx) => {
|
|
|
453
490
|
- `hai-core`:配置管理、HaiResult 模型
|
|
454
491
|
- `hai-iam`:IAM 模块内部使用 reldb 进行用户/角色/权限存储
|
|
455
492
|
- `hai-cache`:缓存穿透保护中配合 reldb 使用
|
|
493
|
+
|
|
494
|
+
## 生产共享状态
|
|
495
|
+
|
|
496
|
+
生产仅使用 PostgreSQL/MySQL;SQLite(含磁盘文件)只用于开发/明确测试,不能作为跨主机共享状态。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化成功前检查,只有 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试豁免。不要将租约当作最终写入条件,外部未知效果须按稳定业务键核对。
|
|
497
|
+
|
|
498
|
+
MySQL 空 url(含空环境变量占位)按未配置处理,使用分字段;非空仍严格校验。
|