@lark-apaas/coding-steering 0.1.32-beta.0 → 0.1.32-dev.0f5e9dd

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 (82) hide show
  1. package/package.json +6 -6
  2. package/steering/design-html/skills/charts/SKILL.md +15 -4
  3. package/steering/design-html/skills/data-viz/SKILL.md +1 -1
  4. package/steering/design-html/skills/hi-fi-design/SKILL.md +1 -1
  5. package/steering/design-html/skills/interactive-prototype/SKILL.md +1 -17
  6. package/steering/design-html/skills/pptx-style-extract/SKILL.md +71 -26
  7. package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +3 -3
  8. package/steering/design-html/skills/pptx-style-extract/scripts/census.py +26 -14
  9. package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +153 -8
  10. package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2831 -293
  11. package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +544 -22
  12. package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +19 -2
  13. package/steering/design-html/skills/pptx-style-extract/scripts/package.py +991 -165
  14. package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +22 -3
  15. package/steering/design-html/skills/pptx-style-extract/scripts/query.py +4 -9
  16. package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +20 -12
  17. package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
  18. package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +364 -0
  19. package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +60 -0
  20. package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +76 -0
  21. package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +528 -0
  22. package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1814 -0
  23. package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +600 -0
  24. package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
  25. package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +315 -0
  26. package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +421 -0
  27. package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
  28. package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +27 -15
  29. package/steering/design-html/skills/slide-deck/SKILL.md +5 -3
  30. package/steering/design-html/skills/social-card/SKILL.md +263 -0
  31. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +4 -0
  32. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +6 -0
  33. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +5 -5
  34. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +1 -1
  35. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +38 -113
  36. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +15 -2
  37. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +13 -7
  38. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +205 -38
  39. package/steering/nestjs-react-fullstack/skills/connections-sdk/SKILL.md +202 -0
  40. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +67 -4
  41. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +254 -0
  42. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +75 -0
  43. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +164 -543
  44. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +72 -15
  45. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +30 -14
  46. package/steering/nestjs-react-fullstack/skills/raw-sql-boundary-audit/SKILL.md +63 -0
  47. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +1 -1
  48. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
  49. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
  50. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
  51. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +90 -0
  52. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
  53. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
  54. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +248 -0
  55. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +284 -12
  56. package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +3 -1
  57. package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +2 -0
  58. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +6 -0
  59. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +49 -22
  60. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +11 -3
  61. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +57 -14
  62. package/steering/vite-react/skills/plugin-guide/SKILL.md +4 -1
  63. package/steering/vite-react/skills/react-three-fiber/SKILL.md +5 -0
  64. package/steering/vite-react/tech.md +3 -0
  65. package/steering/design-html/skills/preflight/SKILL.md +0 -51
  66. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -108
  67. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  68. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -269
  69. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +0 -214
  70. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +0 -163
  71. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +0 -311
  72. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +0 -190
  73. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +0 -160
  74. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +0 -257
  75. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +0 -104
  76. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +0 -199
  77. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +0 -128
  78. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +0 -207
  79. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +0 -165
  80. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +0 -91
  81. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +0 -165
  82. package/steering/nestjs-react-fullstack/skills_common/trigger-guide/references/trigger-lifecycle.md +0 -301
@@ -9,6 +9,25 @@ unavailable-agents:
9
9
  - AppInit
10
10
  - SpecDoc
11
11
  ---
12
+
13
+ # 平台关键文件红线(CRITICAL — 任何修改 / 排查前先读)
14
+
15
+ 下列文件 / 目录由平台模板与 SDK 预置,是应用运行的地基,**不建议改 / 非必要不改**——尤其禁止删除或重写(删坏会直接导致白屏 / 启动失败 / 鉴权链断):
16
+
17
+ - `server/main.ts`
18
+ - `client/src/index.tsx`
19
+ - `client/index.html`
20
+ - `client/src/app.tsx`(加路由 / Provider 可改,但**禁止删除其中的平台根组件 / Provider 包裹**,禁止用 `eslint-disable` 关掉平台校验规则)
21
+ - `client/src/components/business-ui/*`
22
+ - `server/modules/view/view.controller.ts`
23
+ - `vite.config.ts`
24
+
25
+ **修改 / 排查上列文件时**:
26
+
27
+ 1. **禁删、禁重写**;类型与平台代码不兼容时,在**调用点写 adapter / 包一层**消化差异,禁止改平台文件去迁就(更禁止 `as any`)。
28
+ 2. 文件疑似被**清空 / 损坏**时,**从模板恢复原版**(对照同模板初始版本还原),禁止凭记忆从零重写——空文件不是「可以自由发挥」的信号。
29
+ 3. 同一症状已尝试 **3 种以上修法仍未解决** → 停手回溯根因,不要再叠第 4 种 hack(3+ 次失败通常是把平台关键文件改坏了)。
30
+
12
31
  # 项目结构
13
32
 
14
33
  ## 根目录组织
@@ -28,7 +47,7 @@ unavailable-agents:
28
47
 
29
48
  ```
30
49
  server/ # 符合 NestJS 项目基本规范
31
- ├── main.ts # 应用程序入口点,内容不可改动
50
+ ├── main.ts # 应用程序入口点,平台关键文件,禁删禁重写(见顶部红线)
32
51
  ├── app.module.ts # 根模块,模块需要在该文件中导入
33
52
  ├── config/ # 配置文件
34
53
  │ └── app.config.ts # 主要应用配置
@@ -38,7 +57,7 @@ server/ # 符合 NestJS 项目基本规范
38
57
  │ ├── hello.controller.ts # controller 示例
39
58
  │ ├── hello.module.ts # module 示例
40
59
  │ └── hello.service.ts # service 示例(可选)
41
- ├── database/ # Drizzle ORM 数据库相关。应用仅能进行 DML 操作。若用户需求设计表结构变更,DDL 相关操作需要使用 `ddl_sql` 工具进行。
60
+ ├── database/ # Drizzle ORM 数据库相关。应用仅能进行 DML 操作。若用户需求设计表结构变更,DDL 相关操作先加载数据库操作 skill(按「应用数据库 / 建表 / 改表 / SQL」召回),按其中的执行通道与建表规范操作。
42
61
  │ ├── schema.ts # Drizzle ORM 数据库 Schema 定义。会在执行完 DDL 操作后自动生成,必须从该文件导入数据库类型,禁止自行编写 schema 文件。如有需要可调用 CodeGen 工具手动生成。
43
62
  └── common/ # 共享工具和接口
44
63
  │ ├── filters/ # 通用错误处理。
@@ -55,7 +74,7 @@ client/
55
74
  ├── index.html # HTML 模板
56
75
  ├── public/ # 静态资源
57
76
  ├── src/
58
- │ ├── index.tsx # React 应用入口点,内容不可改动
77
+ │ ├── index.tsx # React 应用入口点,平台关键文件,禁删禁重写(见顶部红线)
59
78
  │ ├── index.css # 全局样式
60
79
  │ ├── tailwind-theme.css # tailwindcss 全局css主题变量定制
61
80
  │ ├── app.tsx # 主应用组件(包含路由定义)
@@ -136,8 +155,9 @@ shared/ # 前后端共享的目录
136
155
 
137
156
  ## 依赖使用规范
138
157
 
139
- 1. **子包完整性检查**:部分库有多个子包(如 `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`),添加 import 后必须确认 package.json 中包含所有需要的子包
140
- 2. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
158
+ 1. **子包完整性检查**:部分库有多个子包(如 `@radix-ui/react-*` 系列每个组件都是独立子包),添加 import 后必须确认 package.json 中包含所有需要的子包
159
+ 2. **禁止安装时需现场编译的 native addon**(`node-gyp rebuild`、`binding.gyp`、需要系统头文件或编译器)。依赖按平台内置能力 / 浏览器能力 → 纯 JS 或 WASM → 有预编译二进制的 native addon 的顺序选型
160
+ 3. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
141
161
 
142
162
  ## 文件命名约定
143
163
 
@@ -160,12 +180,42 @@ shared/ # 前后端共享的目录
160
180
  - `.eslintrc.js`、`.prettierrc`
161
181
  - `node_modules/`、`.git/`、`dist/`、`build/`
162
182
 
183
+ ### 沙箱 dev 不提供平台 runtime 接口(`__runtime__`)
184
+
185
+ dev server 只代理 `/api`、`/openapi`、`/__innerapi__`(外加 legacy 的 `/af/p/:appId/api`、`/af/p/:appId/__innerapi__`)。`/app/<appId>/__runtime__/*` 这一整类**平台 runtime 接口在沙箱 dev 下打不通**,只有发布态才由网关接管。
186
+
187
+ **症状分两种,按请求方法区分**:POST 直接 404;**GET 会落到 Vite 的 SPA fallback,拿回 200 + `index.html`**,业务侧表现为「返回了一坨 HTML,JSON 解析失败」——见下方「问题排查指引」里「API 测试返回 HTML 内容时」那条,两者是同一回事。
188
+
189
+ 已知受影响端点:
190
+
191
+ | 端点 | 方法 | 谁会打到它 |
192
+ |------|------|-----------|
193
+ | `api/v1/permissions/roles` | GET | `AppContainer` **每次加载应用自动打**,不需要你写任何代码 |
194
+ | `api/v1/observability/{logs,traces,metrics}/collect`、`current_server_timestamp` | POST/GET | 平台埋点上报,**自动打** |
195
+ | `api/v1/account/login/user` | POST | `useCurrentUserProfile()` → `authClient.session.getUserInfo()` |
196
+ | `api/v1/account/search_user`、`list_users`、`user_profile`、`search_department`、`convert_lark_user` | POST / `user_profile` 是 GET | `business-ui` 的 UserSelect / DepartmentSelect / UserDisplay |
197
+ | `api/v1/account/search`、`api/v1/account/chat/list_chats` | POST | ChatSelect 选群组件 |
198
+ | `api/v1/studio/user/profile` | GET | `getUserProfile()` |
199
+ | `api/v1/storage/object/<bucket>/pre_upload` | POST | `dataloom.storage.uploadFile()` 前端上传 |
200
+ | `api/v1/studio/plugins/tmp_files/acquire_upload_url`、`acquire_download_url` | POST | `capabilityClient` 的文件类入参 |
201
+
202
+ > `useCurrentUserProfile()` 打的是 `account/login/user`,**不是** `user_profile`/`search_user` 那一排;它另外还打一个 `GET /api/authnpaas/lark-user-id`,那条走 `/api`、被代理、正常。
203
+
204
+ **这是环境限制,不是应用缺陷,也不是组件坏了。** 看到这些 404 / HTML 响应时:
205
+
206
+ - **禁止**改 `client/src/components/business-ui/**`(平台保护文件)、**禁止**给组件加兜底请求或改用自研选人控件绕过——发布后这些端点是正常的,绕过写法反而是错的
207
+ - 需要在开发期验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。服务端的 `req.userContext` 和 `AuthNPaasService` 由网关注入,**不受此限制**,始终可用(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 在这里长期停在加载态就是这个原因
208
+ - 需要在开发期验证上传链路:改用接口测试直接打后端接口,或复用库里已有的文件 URL,不要在浏览器里点上传
209
+ - 依赖上述端点的 E2E 用例不要写成浏览器交互 Case,改走接口层往返验证
210
+
211
+ 另外,`UserSelect` 弹层是**搜索驱动**的:不输关键词时列表为空属预期行为,不是接口挂了。
212
+
163
213
  ## 质量保障流程
164
214
 
165
215
  通用提交前检查(代码检查 / 接口测试 / 读日志确认无错误)见系统任务流程约束;本栈具体落点:
166
216
 
167
217
  - **日志源**:服务端读 `server-devserver` / `server` 日志,客户端读 `client-devserver` 日志
168
- - **dev 服务无响应**:`pkill -f "dev:server"` / `pkill -f "dev:client"` 触发重启
218
+ - **dev 服务无响应**:`pkill -f "npm run dev"` 触发重启
169
219
 
170
220
  ---
171
221
 
@@ -213,8 +263,10 @@ shared/ # 前后端共享的目录
213
263
  - **三方集成**:调用第三方 API 需在后端实现,使用 @nestjs/axios
214
264
  - **能力边界**:服务端不支持文件上传(FaaS 限制),前端用 dataloom SDK 上传,服务端仅保存元信息
215
265
  - **环境判断**:`process.env.NODE_ENV === "production"` 表示生产环境
216
- - **文件系统**:**严禁使用 `fs` 写入非 `/tmp` 路径**(无状态 FaaS 容器)
266
+ - **文件系统**:临时文件、下载内容和处理中间产物统一写入 `/tmp` 下的独立目录,用完后清理
267
+ - **启动关键路径**:`bootstrap`、`onModuleInit` 和 `OnApplicationBootstrap` 只做必须的同步装配,不得执行 DDL、全量数据同步、长轮询或无界外部请求。应用应尽早 `listen()`;非关键预热放到后台任务,外部调用必须有超时、错误日志和降级路径
217
268
  - **服务端运行时资源文件**(字体 / 证书 / 模板 / wasm 等需在运行时读取的非代码文件,CRITICAL — 发布后静默失效根因):① 必须在 `nest-cli.json` 的 `assets` 中声明(如 `{"include": "assets/<dir>/**/*", "outDir": "dist/server"}`),否则不会进 `dist/` 构建产物;② 路径用 `__dirname` 相对**编译产物**定位(如 `path.join(__dirname, "../assets/...")`),**禁止 `process.cwd()` 相对源码路径**——dev 跑源码能命中、发布跑 `dist/` 会落空;③ 资源缺失或加载失败必须 **fail-loud**(抛错或明确错误日志),禁止 `catch` 后静默返回残缺产物
269
+ - **启动生命周期**:`constructor`、`onModuleInit` 和 `onApplicationBootstrap` 应快速完成,避免在其中下载大文件、全量同步或批量迁移;仅将钩子声明为 `async` 仍会被等待,无法缩短启动时间
218
270
 
219
271
  ## 日志约定
220
272
 
@@ -236,7 +288,7 @@ export class TestService {
236
288
  ```
237
289
 
238
290
  - **每次编写或修改数据库操作代码前,必须重新读取 `server/database/schema.ts`**——该文件由系统在 DDL 执行后自动重新生成,内容随时可能变化。禁止凭记忆或之前读取的版本编写字段名和类型,否则会引用不存在的字段导致 TS2339 批量报错
239
- - **禁止事务**(`db.transaction`),每个操作独立原子
291
+ - **事务**:多条写操作需要整体成功/失败时(如扣库存 + 写审批流水),用 `db.transaction` 包裹;单条语句能完成的操作不要套事务
240
292
  - 条件查询用三元分支,禁止 `let query` 重赋值(类型不兼容):
241
293
 
242
294
  ```typescript
@@ -282,10 +334,17 @@ await db.select().from(users).where(eq(users.adminUser, userId));
282
334
  | 写(insert/update) | `ROW(${userId})::user_profile`(整体赋值) | 只更新复合类型单字段 |
283
335
  | 过滤(where) | `WHERE (col).user_id = ${userId}` | `col::text = ${id}` / 裸 `col = ${id}` / `col = ROW(${id})::user_profile` 比较 |
284
336
 
285
- > 权威来源 miaoda-sql §5.4。原生 SQL 现场遇到 user_profile 列直接套用本表,不要在 `ROW()::user_profile` / `::text` / 裸值之间反复试错。
337
+ > 权威来源是数据库操作 skill(按「应用数据库 / 建表 / 改表 / SQL」召回)的「`user_id` / `user_profile`」小节。原生 SQL 现场遇到 user_profile 列直接套用本表,不要在 `ROW()::user_profile` / `::text` / 裸值之间反复试错。
286
338
 
287
339
  ### 使用侧边界陷阱
288
340
 
341
+ - **Postgres 不做 MySQL 式隐式转换**:where 的参数类型要和 `schema.ts` 的列类型一致,否则报 `42883 operator does not exist`。
342
+
343
+ | 场景 | 写法 |
344
+ | --- | --- |
345
+ | 按月 / 按天筛 date、timestamptz | 半开区间 `and(gte(col, "2026-08-01"), lt(col, "2026-09-01"))`;日期列不能用 `like(col, "2026-08%")` |
346
+ | 数字列过滤(`@Query` 取到的是 string) | 先 `Number(q)` 再进 `eq` |
347
+
289
348
  - **`count()` 返回 string**(PostgreSQL bigint)。
290
349
 
291
350
  - **UUID 多值过滤不要直接插值数组到 `ANY(...::uuid[])`**:在 sql 模板中直接插入 ids 这类 JS 数组,可能被展开成 `($1, $2, ...)::uuid[]`,运行时报 `42846: cannot cast type record to uuid[]`。
@@ -306,12 +365,96 @@ await db.select().from(users).where(eq(users.adminUser, userId));
306
365
 
307
366
  多 ID 数据库接口(如按班级查课程/考试/成绩统计)写完后必须实际调用验证,避免隐藏 500。
308
367
 
309
- - **`customTimestamptz` 跨网络后是 string**:service 内查询返回 `Date`,但 API 响应经 JSON 序列化后前端拿到 ISO string。`shared/api.interface.ts` 中 timestamptz 字段声明为 `string`,前端需要 Date 时手动 `new Date(value)`。
368
+ - **Drizzle raw `sql` 参数不走列 encoder**:绑定 schema 列的 helper 会编码;raw `sql` 的 `${...}` 只是 driver 参数。Date / custom type 进 raw SQL 前先转 driver-safe 标量(时间用 ISO string)或改回 helper;`user_profile` 仍按上一小节专表处理。
369
+ - raw SQL 聚合、`filter (...)`、`CASE WHEN`、窗口函数或复杂 where 涉及 Date / custom type 时,按 `raw-sql-boundary-audit` 审计,并实际调用对应 API 验证 HTTP 200,避免隐藏 500。
370
+
371
+ - **时间列跨网络后都是 string**:`date` 列 `$inferSelect` 就是 `string`(原样透传,禁止 `new Date()` 包装);`timestamp` / `timestamptz` 是 `Date`,**必须在 service 出口 `.toISOString()`**。两类在 `shared/api.interface.ts` 中统一声明为 `string`,前端需要 Date 时手动 `new Date(value)`。
372
+
373
+ - **禁止删类型注解消除报错**(`as unknown as T` 同禁):典型症状是「改完 service,错误跑到 controller 了」——错误没有转移,是校验点被往外推了一层;推到最外层删完,报错归零而契约失守。
374
+
375
+ ### 业务日期
376
+
377
+ 需求里的日期是**自然日**(哪一天 / 哪一周 / 哪一月)而不是精确时刻时,按业务时区计算,未指定时默认 `Asia/Shanghai`。
378
+
379
+ 项目里放一个 helper 统一产出 `day` / `month` / `weekStart` / `shiftDay`;服务端写入、统计聚合、前端展示都比较它的返回值。
380
+
381
+ ### PostgreSQL SQLSTATE / Drizzle 异常处理(CRITICAL)
382
+
383
+ - 可预期幂等 / 冲突优先用数据库原子语义(`ON CONFLICT`、`UPDATE ... WHERE ... RETURNING`),不要靠异常流兜底。
384
+ - 必须 catch 数据库异常时,**禁止只读顶层 `error.code`**:Drizzle 可能把原始 PostgreSQL error 放在 `cause` 链里。判断 `23505` 等 SQLSTATE 时用 cause-aware helper:
385
+
386
+ ```typescript
387
+ function extractPostgresErrorCode(error: unknown): string | undefined {
388
+ let current: unknown = error;
389
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth += 1) {
390
+ const { code, cause } = current as { code?: unknown; cause?: unknown };
391
+ if (typeof code === "string") return code;
392
+ current = cause;
393
+ }
394
+ return undefined;
395
+ }
396
+ ```
397
+
398
+ - 只处理明确 SQLSTATE:`23505`(unique violation)可转业务态或 `ConflictException`;`23503` 外键、`42501` 权限 / RLS 按业务语义失败;未知错误继续抛出,禁止吞成成功。
399
+ - 依赖约束 / 权限分支的写接口,至少验证正常写入 + 冲突 / 重复写入,确认不是 500 且无重复 / 部分写入。
400
+
401
+ ### 写操作与批量读(CRITICAL)
402
+
403
+ - **PATCH/UPDATE 更新契约**:`PATCH` 和增量 `update` 接口只写入请求 DTO 中明确提供的可选字段,禁止用 `dto.field ?? 默认值`、空数组、空字符串等默认值覆盖未提供字段;用户明确清空时由 DTO 传入清空语义值(如空数组、空字符串或 `null`)并按业务规则写入。业务字段发生更新且接口/页面展示最后修改时间或最后修改人时,不能依赖 `DEFAULT CURRENT_TIMESTAMP` 等建表默认值刷新 UPDATE;必须在同一次 Drizzle update 的 `set(patch)` 调用中维护 `schema.ts` 暴露的更新时间和更新人字段,更新人从 `req.userContext.userId` 传入 service,禁止从前端传递。
404
+
405
+ ```typescript
406
+ const patch: Partial<typeof table.$inferInsert> = {};
407
+ if (dto.title !== undefined) patch.title = dto.title;
408
+ if (dto.labels !== undefined) patch.labels = JSON.stringify(dto.labels);
409
+ if (Object.keys(patch).length === 0) throw new BadRequestException('未提供可更新字段');
410
+
411
+ patch.updatedAt = new Date();
412
+ patch.updatedBy = userId;
413
+
414
+ const updated = await this.db.update(table)
415
+ .set(patch)
416
+ .where(eq(table.id, id))
417
+ .returning({ id: table.id });
418
+ if (updated.length === 0) throw new NotFoundException('记录不存在');
419
+ ```
420
+
421
+ - **`Partial<$inferInsert>` 仅用于 update 的 `set()`**:insert 的 `.values()` 要求必填字段齐全,套 `Partial` 会把必填字段变可选、报缺属性错误;create 直接构造完整对象(类型用 `typeof table.$inferInsert`),禁止把上面的 patch 写法复制到 create
422
+
423
+ - **禁止 N+1 查询**:列表接口禁止在循环 / `Promise.all` 里逐条查子记录。先收集主记录 ids,用 `inArray` 一次查回,内存中按外键 `Map` 分组回填:
424
+
425
+ ```typescript
426
+ // ❌ N+1:每条 order 单独查 items
427
+ for (const order of orders) {
428
+ order.items = await this.db.select().from(items).where(eq(items.orderId, order.id));
429
+ }
430
+ // ✅ 批量查 + Map 分组
431
+ const allItems: Item[] = await this.db.select().from(items)
432
+ .where(inArray(items.orderId, orders.map((o: Order) => o.id)));
433
+ const byOrder = new Map<string, Item[]>();
434
+ for (const it of allItems) {
435
+ byOrder.set(it.orderId, [...(byOrder.get(it.orderId) ?? []), it]);
436
+ }
437
+ ```
438
+
439
+ - **写操作必须校验生效行数**:update / delete 必须带 `.returning({ id })`,返回空数组 = 目标不存在或条件不满足 → 抛对应异常(见「异常处理」),禁止盲发 delete 后直接返回成功
440
+ - **禁止 SELECT→计算→UPDATE 写数值**:库存 / 余额 / 计数的增减必须用单条原子 UPDATE,把业务校验放进 WHERE。注意:仅用 `db.transaction` 包裹 SELECT+UPDATE **不能**防止并发超卖(两个事务可同时读到旧值),除非 SELECT 加 `FOR UPDATE` 行锁——优先用原子 UPDATE,简单且无锁等待:
441
+
442
+ ```typescript
443
+ // ❌ 竞态:先 select 库存,JS 里算完再 update(两请求并发会超卖)
444
+ // ✅ 原子条件更新 + 生效行数校验(同时满足上一条规则)
445
+ const updated = await this.db.update(products)
446
+ .set({ stock: sql`${products.stock} - ${qty}` })
447
+ .where(and(eq(products.id, id), gte(products.stock, qty)))
448
+ .returning({ id: products.id });
449
+ if (updated.length === 0) throw new ConflictException("库存不足或商品不存在");
450
+ ```
451
+
452
+ - 原子 UPDATE 需要与其他写操作保持一致时(如同时写审批流水),把原子 UPDATE 放进 `db.transaction`,生效行数为 0 时抛异常令事务回滚
310
453
 
311
454
  ### 数据库使用强约束
312
455
 
313
456
  - 优先通过 schema.ts 暴露的客户端和类型读写;**被迫手写 SQL 时 user_profile 列必须按上表三侧写法**,禁止临时 `::text` 转换或裸值比较
314
- - **变更流程(CRITICAL)**:DDL 通过 `miaoda db sql` 执行(SQL 建表/改表规则与执行通道见 `miaoda-sql` skill)→ 系统自动 codegen → **立即重新读取 schema.ts** → 再编写业务代码。跳过重新读取直接编码是 TS2339 批量错误的主要根因
457
+ - **变更流程(CRITICAL)**:DDL 先加载数据库操作 skill(按「应用数据库 / 建表 / 改表 / SQL」召回),按其中的 SQL 执行通道执行(建表 / 改表规则、具体命令与必填参数一律以该 skill 为准,不要凭记忆拼命令,也不要裸连数据库)→ 系统自动 codegen → **立即重新读取 schema.ts** → 再编写业务代码。跳过重新读取直接编码是 TS2339 批量错误的主要根因
315
458
  - 连接失败/SSL 错误:立即停机并提示用户联系技术支持
316
459
 
317
460
  ## API 规范
@@ -329,30 +472,48 @@ await db.select().from(users).where(eq(users.adminUser, userId));
329
472
  - 编写前端 API 时必须先读取对应 Controller 确认 method 和路径
330
473
  5. **路由注册验证**:创建新 Controller 后必须用接口测试工具验证路由是否生效。**遇到 404 排查路径**:① 检查 `@Controller(...)` 是否以 `api/` 或 `openapi/` 开头 ② 检查 Module 是否在 `app.module.ts` 注册 ③ 检查静态路由是否在动态路由 `/:id` 之前
331
474
  6. **@Query/@Param 类型转换**:默认 string,必须在 controller 层手动转换(如 `parseInt(limit, 10)`)
332
- 7. **写操作加 @NeedLogin**:POST/PUT/PATCH/DELETE 接口显式加 `@NeedLogin()` 装饰器:
475
+ 7. **写接口鉴权**:POST/PUT/PATCH/DELETE 不一刀切。个人/后台/依赖当前用户的写入加 `@NeedLogin()`;未登录访客表单/报名/反馈可公开,但每张被写入的 RLS 表都要有 `FOR INSERT TO anon WITH CHECK (true)` 或等价 Drizzle `withCheck`,并用未登录 HTTP POST 验证。`FOR SELECT TO anon` 只读、`FOR INSERT TO authenticated` 只登录态;公开只读页的增删改仍按登录态处理。
333
476
  ```typescript
334
477
  import { NeedLogin } from "@lark-apaas/fullstack-nestjs-core";
478
+
335
479
  @NeedLogin()
336
480
  @Post()
337
481
  async createItem(@Req() req, @Body() dto) { ... }
482
+
483
+ // 公开匿名写:不加 @NeedLogin(),DDL 需 anon INSERT WITH CHECK
484
+ @Post("public-submit")
485
+ async submitPublic(@Body() dto) { ... }
338
486
  ```
339
487
  8. **OpenAPI 文档同步**:改动 `*.openapi.controller.ts` 或其引用的 interface / service 返回值 / schema 字段时,加载 `openapi-guide` skill,同步更新 `docs/openapi.json`
488
+ 9. **服务端查用户信息(CRITICAL)**:**必须** `import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core'` 并用 `listUsersByIds`(`PlatformModule.forRoot()` 已注册 global 的 `AuthNPaasModule`,**不要**加 `providers`、**不要**加直接依赖)。**禁止**复用应用内已有的用户 service、裸调平台接口、从库表 join 人名头像、依赖 `req.userContext.userId`(`/openapi` 下恒为空)、`try/catch` 吞掉 SDK 抛的平台错误、用 `getBatchLarkUserIds` 判断用户是否存在(外部用户无 employeeId)、拿不到的字段用 `''` 占位。搜人 / 搜部门 / 搜群、手机号职位工号上级服务端**拿不到**,走飞书开放平台。动手前加载 `contacts-service` skill 第六节
340
489
 
341
490
  ## 异常处理
342
491
 
343
492
  | 分层 | 目标 |
344
493
  |------|------|
345
- | service | 抛出业务异常 |
494
+ | service | 抛业务异常,**必须用 NestJS 内置 HttpException 子类**(`@nestjs/common`),禁止裸 `throw new Error`(会被全局 Filter 兜成 500,前端拿不到语义状态码) |
346
495
  | controller | 不处理异常,交全局 Error Filter |
347
496
 
497
+ 场景 → 异常类型映射:
498
+
499
+ | 场景 | 异常 |
500
+ |------|------|
501
+ | 资源不存在(含 update / delete 未命中) | `NotFoundException` |
502
+ | 参数 / 状态非法 | `BadRequestException` |
503
+ | 并发冲突、库存 / 余额不足 | `ConflictException` |
504
+ | 无权限操作他人数据 | `ForbiddenException` |
505
+
348
506
  ## 当前用户信息
349
507
 
350
508
  - **必须从 `req.userContext` 获取**,禁止从前端传递,禁止硬编码(`const { userId } = req.userContext`)
351
509
  - 依赖用户信息的接口不要用 API 测试工具测试,用 `think` 工具思考
510
+ - 后端按 `user-identity` 和 `@lark-apaas/fullstack-nestjs-core` 的 Express `Request` 增强读取 `req.userContext`;`tenantId` 为可选 number,不能套用前端 `window.tenantId` / `viewContext` 字符串语义。
511
+ - Controller 直接用 `@Req() req: Request`;测试、mock 或业务窄类型用独立接口,只声明实际读取字段。不要继承 Express `Request` 或重声明 `userContext`;未读 `tenantId` 就省略,必须保留时用 `unknown` 或 SDK 兼容类型。
352
512
 
353
513
  ```typescript
354
- // req.userContext 字段(由身份认证中间件挂载):
355
- // - userId/tenantId/appId: string
514
+ // req.userContext 字段(由身份认证中间件挂载,完整合同见 user-identity):
515
+ // - userId/appId: string
516
+ // - tenantId: SDK Request.userContext 的可选 number
356
517
  // - env: 'preview' | 'runtime' (preview=预览态, runtime=发布运行态)
357
518
  // - userName/userNameEn: string
358
519
  // - userNameI18n: string (多语名字, 如 {zh_cn: '用户', en_us: 'user'})
@@ -395,15 +556,17 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
395
556
  | 语音识别、音频转文字、STT | AI语音转文字 |
396
557
  | 插件实例、PluginInstance、Capability | 通用插件调用 |
397
558
 
559
+ > ⚠️ **语义检索 / 相似推荐场景例外**:若需求是对**应用自有数据**做语义检索、向量检索、相似推荐、相关推荐、按内容找相似,**不要**用上表的「AI搜索总结 / AI文本转JSON / AI智能生文」等通用插件替代,必须召回 `/semantic-search`。上表的 AI 搜索/文本插件用于公网搜索、文本生成、结构化抽取,**不具备对自有数据库的向量检索能力**。
560
+
398
561
  ### 关键区分:多维表格 vs 数据库表
399
562
 
400
563
  | 场景 | 判断 | 操作 |
401
564
  |-----|------|-----|
402
- | 明确提到"多维表格/飞书/Base/bitable" | 飞书平台外部服务 | 召回 `plugin_guide` |
403
- | "建表/DDL/schema变更" | 应用内数据库结构 | 使用 `ddl_sql` 工具 |
565
+ | 明确提到"多维表格/飞书/Base/bitable" | 飞书平台外部服务 | 加载 `plugin-guide` skill |
566
+ | "建表/DDL/schema变更" | 应用内数据库结构 | 加载数据库操作 skill(按「应用数据库 / 建表 / 改表 / SQL」召回),按其执行通道操作 |
404
567
  | "往XX表插入数据"(无明确来源) | 检查 `schema.ts` | 有定义→Drizzle ORM;无定义→询问用户确认 |
405
568
 
406
- > 插件实例的复用/创建、`get_plugin_ai_json`、调用签名(`capabilityClient.load().call/callStream`)、配置完整性(`CapabilityNotFoundError`)、多维表格数据架构选择等**详细开发指南,见 `plugin_guide`**(按上表关键词召回)。
569
+ > 插件实例的复用/创建、`get_plugin_ai_json`、调用签名(`capabilityClient.load().call/callStream`)、配置完整性(`CapabilityNotFoundError`)、多维表格数据架构选择等**详细开发指南,见 `plugin-guide` skill**(按上表关键词召回)。
407
570
 
408
571
  ## 自动化任务
409
572
 
@@ -427,7 +590,9 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
427
590
 
428
591
  1. 使用 API 测试工具测试不依赖用户信息的接口
429
592
  2. 积极使用 `read_logs` 工具,如无有效日志可增加 logger 打印
430
- 3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController
593
+ 3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController。若路径含 `__runtime__`,那就是下一条——GET 未被代理时会落 SPA fallback 返回 index.html
594
+ 4. 请求路径含 `__runtime__` 的 404(POST)或 200 + HTML(GET):属沙箱 dev 环境限制,不是应用缺陷,见「沙箱 dev 不提供平台 runtime 接口」
595
+ 5. 服务端启动即崩、`/api` 全部 502 且报 `UnknownDependenciesException ... argument Function at index [0]`:查构造函数有没有空参 `@Inject()`(见 plugin-guide),不要先去改 Module 的 imports / providers
431
596
 
432
597
  ---
433
598
 
@@ -437,7 +602,7 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
437
602
 
438
603
  - **框架**: React 19 + TypeScript
439
604
  - **路由**: React Router DOM v6
440
- - **样式**: styled-jsx + tailwindcss(语义化 token)。styled-jsx 使用前提见下方"样式开发"
605
+ - **样式**: tailwindcss(语义化 token)
441
606
  - **UI 组件库**: shadcn/ui — Use components for functionality, heavily style them
442
607
  - **图表**: ReactECharts,**开发前必须调用 `/charts-skill`**
443
608
  - **图标**: Lucide React(唯一图标库,禁止 Emoji 和其他图标库)
@@ -448,16 +613,18 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
448
613
  {% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
449
614
  - **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
450
615
  - **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
616
+ {% else %}
617
+ - **Markdown 渲染**: `components/ui/markdown`(react-markdown + remark-gfm,内置 prose 排版)
451
618
  {% endif %}
452
619
 
453
620
  ## API 请求
454
621
 
455
- **禁止** `fetch`,必须使用 `axiosForBackend` **函数调用方式**(不用会报 `Tenant not found`):
622
+ **禁止** `fetch`,必须使用 `axiosForBackend`(不用会报 `Tenant not found`):
456
623
 
457
624
  ```typescript
458
625
  import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBackend';
459
626
  // ✅ axiosForBackend({ url: '/api/users', method: 'GET' })
460
- // ❌ axiosForBackend.get(...) / .post(...) / .put(...) / .delete(...) ← 不是 axios 实例,所有实例方法都会 TypeError
627
+ // ✅ axiosForBackend.get('/api/users') / .post(...) 等实例方法(axiosForBackend 是 axios.create 返回的实例)
461
628
  ```
462
629
 
463
630
  - **前后端联调**:编写前端 API 对接代码前**必须先读取后端接口定义**,禁止对后端接口请求进行兜底和过度封装
@@ -485,6 +652,8 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
485
652
  {% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
486
653
  | TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
487
654
  | Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
655
+ {% else %}
656
+ | Markdown | `components/ui/markdown` | Markdown 渲染(react-markdown + remark-gfm) |
488
657
  {% endif %}
489
658
 
490
659
  ### 组件 Skill 召回规则(强制执行)
@@ -497,6 +666,14 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
497
666
 
498
667
  禁止未调用 Skill 直接编写表单/图表/表格代码。
499
668
 
669
+ ### 检索 Skill 召回规则(强制执行)
670
+
671
+ | 场景 | Skill | 说明 |
672
+ |------|-------|------|
673
+ | 语义检索、向量检索、相似推荐、相关推荐、相似内容、按内容找相似、猜你喜欢 | `/semantic-search` | 对应用自有数据库的数据做向量检索 / 相似召回 |
674
+
675
+ 禁止未调用 Skill 直接用「AI搜索总结 / AI文本转JSON / AI智能生文」等通用插件实现对自有数据的语义检索。
676
+
500
677
  ### 组件使用规范
501
678
 
502
679
  - 优先使用 `client/src/components` 下已有组件(Card/Button/Badge 等)
@@ -509,7 +686,9 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
509
686
 
510
687
  ## @lark-apaas/client-toolkit
511
688
 
512
- **零假设原则**:绝不基于假设使用任何子模块。使用任何 `@lark-apaas/client-toolkit` 子模块前**必须**:① 查询 Skills 或 `steering_doc_search` → ② 等待响应获取文档 → ③ 严格按文档编码。**禁止直接调用 `@lark-apaas/client-toolkit` 任何函数/方法**(不基于查询到的文档)。该库**仅可在前端代码中使用,禁止在后端代码中引用**。
689
+ **零假设原则**:绝不基于假设使用任何子模块。使用任何 `@lark-apaas/client-toolkit` 子模块前**必须**:① 查询 Skills 或 `steering_doc_search` → ② 等待响应获取文档 → ③ 严格按文档编码。**禁止直接调用 `@lark-apaas/client-toolkit` 任何函数/方法**(不基于查询到的文档)。该库是**前端/浏览器侧 SDK**,仅可在 `client/**`、React 组件、hooks、前端工具函数中使用;**禁止在 `server/**`、NestJS controller/service/module、Node.js runtime、migration、script 中 import 或调用**。
690
+
691
+ **服务端替代原则**:后端需要当前用户身份时用 `req.userContext`;需要飞书 user_id 转换时用 `AuthNPaasService`(见 `user-identity`);没有明确 server-side API 时只保留/透传用户 ID,不要为了展示姓名头像在服务端引入前端 SDK。
513
692
 
514
693
  | 功能 | 导入路径 |
515
694
  |------|----------|
@@ -531,8 +710,8 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
531
710
 
532
711
  ```
533
712
  需要写样式?
534
- ├─ 基础布局/间距/颜色 → Tailwind ✅
535
- ├─ 复杂动画/伪元素/高级CSS → styled-jsx ✅
713
+ ├─ 基础布局/间距/颜色/动画/伪元素 → Tailwind ✅
714
+ ├─ 全局/复杂 CSS → index.css / tailwind-theme.css ✅
536
715
  └─ JS动态计算值 → 行内 style ✅
537
716
  ```
538
717
 
@@ -545,11 +724,6 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
545
724
  - **arbitrary values 中空格用下划线**:`from-[hsl(215_60%_18%)]` 非 `from-[hsl(215 60% 18%)]`
546
725
  - `tailwind-theme.css` 自定义属性用 `hsl(H, S%, L%)` 格式(非 `23 10% 23%`)
547
726
 
548
- ### styled-jsx 规范
549
-
550
- - **技术栈一致性**:仅在已配置 styled-jsx 插件的项目中使用。`package.json` 无 `styled-jsx` 依赖则**禁用**,否则运行时 SyntaxError
551
- - **禁止动态插值**:`<style jsx>` 内禁止 `${...}` 等表达式(会卡死)。动态值放 CSS 变量,用 `var(--xxx)` 引用
552
-
553
727
  ### 布局/排版
554
728
 
555
729
  - Spacing 保持一致(small/medium/large 三级),Panel 风格统一
@@ -643,15 +817,8 @@ return <h1>{data?.title || '未知标题'}</h1>;
643
817
  | 验证 | zod |
644
818
  | 工具函数 | lodash |
645
819
  | 样式 | clsx |
646
- | Excel | xlsx(**仅前端实现,禁止服务端实现**。解析后将结构化数据传到服务端保存) |
647
- | PDF 导出 | jspdf + html2canvas(**仅前端实现,禁止服务端实现**) |
648
- | 文件上传 | react-dropzone |
649
- | 二维码 | qrcode.react |
650
820
  | 用户反馈 | sonner |
651
- | 拖拽 | @dnd-kit/core |
652
- | 数字动画 | react-countup |
653
- | Base64 | js-base64 — `import { encode, decode } from 'js-base64'` |
654
- | 3D 场景 | cobe |
821
+
655
822
 
656
823
  ## 滚动分页最佳实践
657
824
 
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: connections-sdk
3
+ description: "用于 Miaoda fullstack app 的 NestJS 服务端代码读取用户在妙搭开发态配置的三方集成凭证(代码概念:Miaoda Connection;读取参数:connectionName),并通过 @lark-apaas/miaoda-connections-sdk、ConnectionsService 或 MiaodaConnectionsModule 使用。不要用于前端代码、非妙搭凭证配置、妙搭自身数据库/密钥配置或创建凭证配置。触发词:妙搭凭证, 三方集成凭证, Connection 凭证, 凭证 ID, connectionName, ConnectionsService, MiaodaConnectionsModule, @lark-apaas/miaoda-connections-sdk"
4
+ steering: true
5
+ steering-topic: connections_sdk
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
8
+
9
+ # Miaoda Connections SDK
10
+
11
+ 在 NestJS 服务端代码中使用 `@lark-apaas/miaoda-connections-sdk` 读取妙搭凭证。
12
+
13
+ ## 妙搭凭证是什么
14
+
15
+ 妙搭凭证是用户在妙搭开发态为三方系统配置的授权或认证资源。产品概念里也会叫三方集成凭证、凭证 ID 或 SDK 消费 key;代码里对应 `Miaoda Connection` / `Connection`,业务代码通过 `connectionName` 读取运行态凭证值。
16
+
17
+ 概念和代码标识对照:
18
+
19
+ | 中文概念 | 英文/代码标识 | 说明 |
20
+ |----------|---------------|------|
21
+ | 妙搭凭证、三方集成凭证 | `Miaoda Connection` / `Connection` | 用户在妙搭开发态配置并托管的三方系统凭证资源 |
22
+ | 凭证 ID、凭证名、SDK 消费 key | `connectionName` | 业务代码读取凭证时传入的名称 |
23
+ | 运行态凭证值 | `connection.value` / `ConnectionValue` | SDK 返回的类型化凭证数据 |
24
+ | 原始认证输入 | `authInput` | 创建凭证时的认证输入配置;可能包含敏感字段 |
25
+ | 自定义凭证数据 | `authData` | `custom` 或未知类型里的运行态字段 |
26
+
27
+ SDK 支持读取这些三方认证类型:
28
+
29
+ | 三方认证类型 | `connection.value.type` | 说明 |
30
+ |--------------|-------------------------|------|
31
+ | OAuth2 三方授权 | `oauth2` | 三方系统的 OAuth token,例如 OAuth Code 或 Client Credentials 建立后的访问凭证 |
32
+ | 三方接口密钥 | `api_key` | 三方 API 的 API Key 值;请求头名或 query 参数名来自三方 API 文档或 `authInput` 配置 |
33
+ | HTTP Bearer Token | `bearer_token` | 三方 HTTP API 的 Bearer token |
34
+ | Basic Auth | `basic` | 三方系统的用户名和密码 |
35
+ | 外部关系型数据库凭证 | `rdb` | 外部数据库的连接凭证,不是妙搭应用自身默认数据库 |
36
+ | 自定义或未知类型 | `custom` | SDK 未内置识别的类型会归到 `custom`,业务按实际字段读取 `authData` |
37
+
38
+ 妙搭凭证不是:
39
+
40
+ - 妙搭应用自身的数据表或默认数据库连接。
41
+ - 需要写入 `.env` 的环境变量配置。
42
+ - 业务代码里临时声明的 token、api key、password。
43
+ - Authentication、ConnectionPolicy 或授权流程本身。
44
+
45
+ ## 使用边界
46
+
47
+ | 场景 | 做法 |
48
+ |------|------|
49
+ | 服务端调用三方 API 或外部数据库,凭证来自用户已配置的妙搭凭证 | 使用本 SDK |
50
+ | 前端页面需要调用三方 API | 通过后端接口转发,前端不得读取妙搭凭证 |
51
+ | 用户还没创建妙搭凭证 | 提醒用户先在妙搭开发态配置凭证 |
52
+ | 需要创建 Authentication 或授权流程 | 不在业务代码里实现,交给妙搭开发态配置能力 |
53
+ | 只是配置妙搭应用自己的数据库、环境变量或专家模式密钥 | 不使用本 SDK |
54
+
55
+ 禁止把 access token、bearer token、api key、password 写入 `.env`、源码、日志或聊天回复。
56
+
57
+ ## 服务端接入
58
+
59
+ 在应用模块中注册 `MiaodaConnectionsModule`,然后在业务 Service 中注入 `ConnectionsService` 使用。
60
+
61
+ ```typescript
62
+ import { Injectable, Module } from '@nestjs/common';
63
+ import {
64
+ ConnectionsService,
65
+ MiaodaConnectionsModule,
66
+ } from '@lark-apaas/miaoda-connections-sdk';
67
+
68
+ @Module({
69
+ imports: [MiaodaConnectionsModule.forRoot()],
70
+ })
71
+ export class AppModule {}
72
+
73
+ @Injectable()
74
+ export class GithubService {
75
+ constructor(private readonly connections: ConnectionsService) {}
76
+ }
77
+ ```
78
+
79
+ ## 获取妙搭凭证
80
+
81
+ 使用用户在妙搭开发态配置好的凭证 ID / 凭证名作为 `connectionName`。不要在代码中创建或修改凭证配置。
82
+
83
+ ```typescript
84
+ const connection = await this.connections.getConnection({
85
+ connectionName: 'github-main',
86
+ });
87
+
88
+ if (connection.state !== 'connected') {
89
+ throw new Error(`Connection github-main requires ${connection.recoveryHint}`);
90
+ }
91
+ ```
92
+
93
+ 必须先判断 `connection.state === 'connected'`,再读取 `connection.value`。
94
+
95
+ ## 按凭证类型处理
96
+
97
+ 不要假设所有妙搭凭证都是 OAuth2。必须按 `connection.value.type` 分支处理。SDK 内置识别 `oauth2`、`api_key`、`bearer_token`、`basic`、`rdb`、`custom`;服务端新增但 SDK 尚未内置的类型会归一化为 `custom`。所有类型都有 `authInput`,表示原始认证输入;只能按业务需要读取,不要写入日志或错误信息。
98
+
99
+ ### OAuth2 三方授权
100
+
101
+ ```typescript
102
+ if (connection.value.type !== 'oauth2') {
103
+ throw new Error('github-main must use oauth2 connection');
104
+ }
105
+
106
+ const response = await fetch('https://api.github.com/user/repos', {
107
+ headers: {
108
+ Authorization: `Bearer ${connection.value.accessToken}`,
109
+ },
110
+ });
111
+ ```
112
+
113
+ ### 三方接口密钥
114
+
115
+ ```typescript
116
+ if (connection.value.type !== 'api_key') {
117
+ throw new Error('service-main must use api_key connection');
118
+ }
119
+
120
+ const response = await fetch('https://api.example.com/resources', {
121
+ headers: {
122
+ 'x-api-key': connection.value.apiKey,
123
+ },
124
+ });
125
+ ```
126
+
127
+ `api_key` 只保证返回三方接口密钥值 `apiKey`。header 名、query 参数名等组装方式来自三方 API 文档或业务约定;如后端在 `authInput` 中透传了相关配置,也要先按具体字段断言后再使用。这里的三方接口密钥不是妙搭应用自身的环境变量、专家模式密钥或平台访问密钥。
128
+
129
+ ### HTTP Bearer Token
130
+
131
+ ```typescript
132
+ if (connection.value.type !== 'bearer_token') {
133
+ throw new Error('partner-main must use bearer_token connection');
134
+ }
135
+
136
+ const response = await fetch('https://partner.example.com/resources', {
137
+ headers: {
138
+ Authorization: `Bearer ${connection.value.token}`,
139
+ },
140
+ });
141
+ ```
142
+
143
+ ### Basic Auth
144
+
145
+ ```typescript
146
+ if (connection.value.type !== 'basic') {
147
+ throw new Error('erp-basic must use basic connection');
148
+ }
149
+
150
+ const authorization = Buffer.from(
151
+ `${connection.value.userID}:${connection.value.password}`
152
+ ).toString('base64');
153
+
154
+ const response = await fetch('https://erp.example.com/orders', {
155
+ headers: {
156
+ Authorization: `Basic ${authorization}`,
157
+ },
158
+ });
159
+ ```
160
+
161
+ ### 外部关系型数据库凭证
162
+
163
+ ```typescript
164
+ if (connection.value.type !== 'rdb') {
165
+ throw new Error('sales-db must use rdb connection');
166
+ }
167
+
168
+ const config = {
169
+ host: connection.value.host,
170
+ port: connection.value.port,
171
+ database: connection.value.database,
172
+ user: connection.value.username,
173
+ password: connection.value.password,
174
+ };
175
+ ```
176
+
177
+ RDB 表示外部关系型数据库的连接凭证,不是妙搭应用自身默认数据库连接。RDB 的 `advanced` 用于特殊数据库类型的额外连接参数,例如 Oracle 的 `serviceName` 和驱动侧识别用的 `driverType`。Oracle SID 复用已有 `database` 字段,不要额外读取或生成 `advanced.sid`。
178
+
179
+ ### Custom / 未知类型
180
+
181
+ ```typescript
182
+ if (connection.value.type === 'custom') {
183
+ const authData = connection.value.authData as {
184
+ server: string;
185
+ username: string;
186
+ password: string;
187
+ };
188
+
189
+ return createMailClient(authData);
190
+ }
191
+ ```
192
+
193
+ 当服务端返回 SDK 尚不认识的 `ConnectionValue.type` 时,SDK 会把它归类为 `custom`:`authData` 保留原始 value 中除 `type` 和 `authInput` 外的运行态字段,`authInput` 保留在顶层。生成代码时可以基于 `authData` 的实际字段分支,但不要在日志或错误里输出 `authData` 或 `authInput` 明文。
194
+
195
+ ## 不要做的事
196
+
197
+ - 不要在前端代码里调用 Connections SDK。
198
+ - 不要让用户把 token、api key、password 填到 `.env`。
199
+ - 不要在日志或错误里输出明文凭证值、authInput 或 authData。
200
+ - 不要手写平台内部 API 的鉴权 header。
201
+ - 不要在业务代码里直接请求平台内部接口或手写 SDK transport。
202
+ - 首版不要传 `user` 参数;等 end user policy 支持后再扩展用户态 Connection 示例。