@lark-apaas/coding-steering 0.1.58 → 0.1.59

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 (26) hide show
  1. package/package.json +1 -1
  2. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +15 -13
  3. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +2 -2
  4. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +20 -5
  5. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +46 -32
  6. package/steering/nestjs-react-fullstack/skills/design-guide/SKILL.md +0 -3
  7. package/steering/nestjs-react-fullstack/skills/design-guide/references/token-mapping.md +3 -3
  8. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +28 -59
  9. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +16 -4
  10. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +75 -14
  11. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +178 -168
  12. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
  13. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
  14. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
  15. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +82 -0
  16. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
  17. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
  18. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +249 -0
  19. package/steering/nestjs-react-fullstack/skills_common/server-contacts-contract/SKILL.md +19 -0
  20. package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +5 -1
  21. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +2 -2
  22. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +18 -3
  23. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +3 -3
  24. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +93 -2
  25. package/steering/vite-react/skills/plugin-guide/SKILL.md +2 -0
  26. package/steering/nestjs-react-fullstack/skills/design-guide/references/corporate-blueprint.md +0 -252
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-steering",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "description": "Stack-specific steering content for miaoda-coding templates",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: client-builtins-file-storage-service
3
- description: 前端文件存储服务指南,基于 dataloom.storage 实现文件上传、删除、列表查询、使用filePath换取图片链接,包含 uploadFile、remove、list、getDefaultBucketId、generateDownloadUrlFromFilePath 等 API 用法。Use when 需要:(1) 上传文件/图片/附件到云存储,(2) 删除存储桶中的文件,(3) 获取文件列表或浏览目录,(4) 前端存储文件信息到数据库,比如:上传后优先将 download_url 存至 text 类型字段,仅在使用 file_attachment 类型字段时才将 file_path 存至该字段,(5) 在前端通过 file_path 获取文件 URL 的场景,比如:图片渲染、通过 url 下载文件,或其他前端文件存储相关开发。
3
+ description: 前端文件存储服务指南,基于 dataloom.storage 上传、删除、列表查询和获取文件 URL,包含 uploadFile、remove、list、getDefaultBucketId、generateDownloadUrlFromFilePathUse when 需要:(1) 上传文件/图片/附件到云存储,(2) 删除存储桶文件,(3) 获取文件列表或浏览目录,(4) 前端把文件信息存入数据库:普通 text/varchar/JSON 字段默认保存 download_url;仅明确使用 file_attachment 类型字段时保存 bucket_id + file_path(5) file_attachment 字段需用 file_path 换展示 URL,或其他前端文件存储开发。
4
4
  steering: true
5
5
  steering-topic: client_builtins_file_storage_service
6
6
  match-template-name: nestjs-react-fullstack
@@ -8,11 +8,13 @@ match-template-name: nestjs-react-fullstack
8
8
 
9
9
  # 前端文件开发规范
10
10
 
11
- > **术语**:下文的 `file_attachment`、`text` 指的是数据库字段的**类型**,不是字段名。
11
+ > **术语**:下文的 `file_attachment`、`text`、`varchar`、`JSON` 指数据库字段的**类型**,不是字段名。
12
12
 
13
- - **优先用 `download_url` 形式(默认按这个来)**:上传后取 `data.download_url`,存到 **`text` 类型字段**;渲染 / 下载直接用这个 URL,最简单,绝大多数场景都用它。
14
- - 仅当业务明确使用 **`file_attachment` 类型字段**时才走 file_path:该字段存 `bucket_id` + `file_path`(`file_path` 只能是文件路径,**不可以存 `download_url`**);要拿 URL 用 dataloom SDK 的 `generateDownloadUrlFromFilePath(file_path)`,且 `file_path` 必须从该字段读出,禁止自己拼接。
15
- - 上传后的文件还要给后端 AI capability 解析/理解时,前端展示仍用 `download_url`;提交给后端的必须是 `file_path`(以及可用的 `bucket_id`)。后端用 `FileService.createSignedUrl(filePath, 3600)` `fileService.from(bucketId).createSignedUrl(filePath, 3600)` 生成临时 http(s) URL 再传插件。前端不要构造 signed URL;普通展示/下载仍默认用 `download_url`。
13
+ - **默认保存展示 URL**:上传后取 `data.download_url`,存到普通 `text` / `varchar` / `JSON` 业务字段;渲染 / 下载时直接作为 `src` / `href`。
14
+ - **后台留路径时仍返回 URL**:后端异步处理可保存 `file_path` 作内部定位;列表 / 详情接口仍必须另返 `download_url` / `downloadURL` 展示 URL,前端只消费该 URL
15
+ - **仅 `file_attachment` 字段从 file_path URL**:业务明确使用 `file_attachment` 类型字段时,该字段存 `bucket_id` + `file_path`(不能存 `download_url`);前端才用 dataloom SDK 的 `generateDownloadUrlFromFilePath(file_path)`,且 `file_path` 必须来自该字段。
16
+ - **禁止手拼运行态前缀**:不要把 `/runtime/api/v1/storage/object`、`/app/{appId}/runtime`、`/spark/app/{appId}/runtime`、`__runtime__` 当作 URL 合同拼接;使用上传返回值、后端返回值或 SDK 方法原始结果。
17
+ - 上传后的文件还要给后端 AI capability 解析/理解时,前端展示仍用 `download_url`;提交给后端时优先保留 `file_path`(以及可用的 `bucket_id`)。旧记录只有标准 storage `download_url` 时,后端必须先从中解析 `bucket_id + file_path`,再用 `fileService.from(bucketId).createSignedUrl(filePath, 3600)` 生成临时 http(s) URL;不得把 `download_url` 直接交给默认 bucket 的 `FileService.createSignedUrl()`。前端不要构造 signed URL;普通展示/下载仍默认用 `download_url`。
16
18
 
17
19
  # dataloom SDK 文件服务
18
20
 
@@ -23,11 +25,11 @@ match-template-name: nestjs-react-fullstack
23
25
  - **入口边界**:本 SDK 是**应用前端代码**读写应用存储的唯一入口。Agent 自己在对话 / 开发中上传、灌数据或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准——二者操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进页面代码**,页面里一律用本 SDK。
24
26
  - @lark-apaas/client-toolkit/dataloom 这个SDK只适用于前端调用,禁止在服务端调用
25
27
  - **沙箱 dev 限制**:`uploadFile` 打的 `/app/<appId>/__runtime__/api/v1/storage/object/<bucket>/pre_upload` **在沙箱 dev 下恒 404**,只有发布态可用。这是环境限制不是代码缺陷,按本 skill 写法即为正确,不要为它改代码;开发期要验证上传链路改走接口测试直接打后端接口,或复用库里已有的文件 URL。详见 coding-guide「沙箱 dev 不提供平台 runtime 接口」。
26
- - 上传成功后,最重要的返回值是 `data.download_url`。需要将此URL保存到你的业务数据库中
28
+ - 上传成功后,普通 `text` / `varchar` / `JSON` 业务字段默认保存 `data.download_url`,并把它作为页面展示 / 下载 URL
27
29
  - **⚠️ 场景区分(重要)**:`dataloom.storage` 仅适用于需要持久化存储文件或获取 `download_url` 保存到数据库的场景。如果文件仅作为插件输入(传给 `capabilityClient`),**必须直接传 File/Blob 对象,禁止先走 dataloom 上传再传 URL**;插件调用(capability)不属于 dataloom,详见 plugin-guide
28
30
  - **Server 侧 AI capability 边界**:文件已上传并持久化后,若由后端调用 `CapabilityService` 做文档解析/图片理解,不要把前端展示用 `download_url` 当插件入参;把 `file_path` 和 `bucket_id` 传给后端,由后端 `FileService` 签成临时 http(s) URL。
29
- - **download_url 格式说明**:`download_url` 返回的可能是相对路径(如 `/spark/app/.../storage/object/...`),这是正常行为。**禁止**在前面拼接 `window.location.origin` 或其他域名前缀,平台会自动解析相对路径。直接使用原始值即可。
30
- - **文件URL**:通过 `generateDownloadUrlFromFilePath` 方法获取文件的链接时,传入的 file_path 必须是从数据库中 `file_attachment` 类型字段读出来的,禁止自己拼接路径。
31
+ - **download_url 格式说明**:`download_url` 可能是相对路径。**禁止**拼接 `window.location.origin` 或任何运行态前缀;平台会自动解析相对路径,直接使用原始值。
32
+ - **文件URL**:调用 `generateDownloadUrlFromFilePath` 时,`file_path` 必须来自数据库 `file_attachment` 类型字段,禁止拼接;普通业务字段已有 `download_url` 时不要调用本方法。
31
33
 
32
34
  ## bucketId
33
35
  关于bucketId的获取,在代码工程中已经预置了一个获取bucketid的方法,路径为`@lark-apaas/client-toolkit/tools/storage`中,直接具名导入即可使用。
@@ -83,8 +85,8 @@ type StorageError = DataLoomError | StorageError | StorageUnknownError;
83
85
  | 字段名 | 类型 | 说明 |
84
86
  |--------|------|---------|
85
87
  | `data.bucket_id` | `string` | 所属bucket ID |
86
- | `data.file_path` | `string` | 文件的路径,不是文件的url,可存到 `file_attachment` 类型字段 |
87
- | `data.download_url` | `string` | 文件url(可能是相对路径),可直接用于下载文件或渲染图片;**禁止**拼接域名前缀,见「使用注意」 |
88
+ | `data.file_path` | `string` | 文件路径,不是文件 URL;只写入 `file_attachment` 类型字段,或作为后端内部处理定位字段 |
89
+ | `data.download_url` | `string` | 文件 URL(可能是相对路径),普通字段默认保存它,可直接用于下载文件或渲染图片;**禁止**拼接域名前缀,见「使用注意」 |
88
90
  | `error` | `StorageError \| null` | 错误信息,成功时为null |
89
91
 
90
92
  ##### 使用示例
@@ -192,13 +194,13 @@ const { data, error } = await dataloom
192
194
 
193
195
  #### 4. 根据filePath生成downloadUrl接口 (generateDownloadUrlFromFilePath)
194
196
 
195
- **仅用于 `file_attachment` 类型字段场景**:当手头只有从该字段读出的 `file_path` 时,用它换取可渲染 / 下载的 URL。**优先直接用 `download_url`**——已有 download_url 就直接用,严禁再调本接口生成。
197
+ **仅用于 `file_attachment` 类型字段场景**:只有手头是从该字段读出的 `file_path` 时,才用它换可渲染 / 下载 URL。普通 `text` / `varchar` / `JSON` 字段上传文件回显,默认直接用已保存或后端返回的 `download_url` / `downloadURL`,禁止调用本接口。
196
198
 
197
199
  ##### 入参说明
198
200
 
199
201
  | 属性名 | 类型 | 必填 | 默认值 | 说明 |
200
202
  |--------|--------|---------|--------|---------|
201
- | `filePath` | `string` | ✅ | - | 文件的 filePath。**只可以从数据库中 `file_attachment` 类型字段读取,不允许自己拼接路径** |
203
+ | `filePath` | `string` | ✅ | - | 文件的 filePath。**只能从数据库 `file_attachment` 类型字段读取,不得从普通业务字段取值或拼接路径** |
202
204
 
203
205
  ##### 出参说明
204
206
  | 类型 | 说明 |
@@ -325,7 +327,7 @@ export default FileUploadDemo;
325
327
 
326
328
  | 要点 | 说明 |
327
329
  |------|------|
328
- | 上传流程 | 展示/下载:`uploadFile(file)` → 取 `data.download_url` → POST 后端保存;后端 AI 解析:同时保存/提交 `data.file_path` 与 `data.bucket_id` 供后端签名 |
330
+ | 上传流程 | 展示/下载:`uploadFile(file)` → 取 `data.download_url` → POST 后端保存为展示 URL;后端 AI 解析:同时保存/提交 `data.file_path` 与 `data.bucket_id` 供后端签名 |
329
331
  | 文件对象 | 直接传原始 `File`,**禁止**包装或修改文件名 |
330
332
  | 组件依赖 | shadcn/ui `Button`/`Card` + `lucide-react` + `sonner` |
331
333
  | 请求实例 | 必须用 `axiosForBackend`,禁止 `fetch` |
@@ -319,7 +319,7 @@ useEffect 无限循环、依赖数组管理、useMemo/useCallback 记忆化等
319
319
  | 场景 | 做法 | 注意事项 |
320
320
  |------|------|----------|
321
321
  | 用户反馈 | 使用 `toast` (sonner) 显示友好消息 | 消息简洁、可操作,避免暴露技术细节 |
322
- | 前端日志 | 使用 `logger` (`@lark-apaas/client-toolkit/logger`) | 禁止 console;参数为 string,对象需 `JSON.stringify` |
323
- | 后端日志 | 使用 `@nestjs/common` 的 Logger | 禁止 console;参数为 string,对象需 `JSON.stringify` |
322
+ | 前端日志 | 使用 `logger` (`@lark-apaas/client-toolkit/logger`) | 禁止 console;签名见 `coding-guide`「日志约定」,可多参,`logger.log` 只接 `{ level, args }` |
323
+ | 后端日志 | 使用 `@nestjs/common` 的 Logger | 禁止 console;签名见 `coding-guide`「日志约定」,可多参,无 `info` 方法 |
324
324
  | 业务错误 | 区分预期错误与意外错误 | 预期错误用 `logger.warn`,意外错误用 `logger.error` |
325
325
  | 异常处理 | 禁止静默处理异常 | 必须显示明确的错误信息,参考 `coding-guide` 相关规范 |
@@ -284,9 +284,24 @@ dev server 只代理 `/api`、`/openapi`、`/__innerapi__`(外加 legacy 的 `
284
284
 
285
285
  ## 日志约定
286
286
 
287
- - 后端禁止 console,**必须总使用** `@nestjs/common` 的 Logger(无 info 方法,用 `logger.log` 代替)
288
- - Logger 参数必须为 string,对象需 `JSON.stringify`
289
- - 输出完整错误堆栈
287
+ 前后端是两个独立的 logger,签名不同,不要互相套用。
288
+
289
+ **前端** `@lark-apaas/client-toolkit/logger`:同一个 `logger` 对象,六个方法分两类签名。
290
+
291
+ - `debug` / `info` / `warn` / `error` / `success`: `(message: any, ...args: any[])`
292
+ 级别已在方法名里;可多参,`message` 不限 string。
293
+ 例:`logger.error('创建失败', error)`
294
+ - `log`: `({ level, args, meta? }: LogWithMeta)`
295
+ 级别不在方法名里必须显式传;只接对象,传裸字符串会 TS2345。
296
+ 例:`logger.log({ level: 'info', args: ['创建成功', id] })`
297
+
298
+ **后端** `@nestjs/common` 的 `Logger`:
299
+
300
+ - 没有 `info` 方法,用 `logger.log` 代替;有 `debug` / `verbose` / `fatal`
301
+ - `(message: any, ...optionalParams)`,可多参
302
+ - 最后一个 string 参数会被当作 `context`(`error` 是 `stack` + `context`),传对象前自己 `JSON.stringify`
303
+
304
+ 两端共同:禁止 `console`;错误日志输出完整堆栈。
290
305
 
291
306
  ## Database
292
307
 
@@ -511,7 +526,7 @@ await db.select().from(users).where(eq(users.adminUser, userId));
511
526
  async submitPublic(@Body() dto) { ... }
512
527
  ```
513
528
  8. **OpenAPI 文档同步**:改动 `*.openapi.controller.ts` 或其引用的 interface / service 返回值 / schema 字段时,加载 `openapi-guide` skill,同步更新 `docs/openapi.json`
514
- 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 第六节
529
+ 9. **服务端查用户信息(CRITICAL)**:先区分当前用户 `userId`、应用 `appId` 与人员 ID,再确认请求上下文是否已有非空 `appId`,最后选择查询能力和响应字段语义。仅有 `userId` 或给定人员 ID 都不能推导出 `appId`;只有已具备非空 `appId` 时才用 `AuthNPaasService.listUsersByIds`,并保留批量字段映射、平台错误与查无此人的区别。匿名入口无法证明该前置条件时,主体读取保持独立,返回已有稳定 ID,展示名 / 头像建模为可空或可选;明确要求匿名真实姓名但无受支持 `appId` 来源时报告能力阻塞。禁止凭空补身份头、裸调平台、持久化姓名副本、service 广域 `try/catch` 或固定“未知用户”降级。动手前加载 `contacts-service` skill 第六节
515
530
 
516
531
  ## 异常处理
517
532
 
@@ -826,7 +841,7 @@ return <h1>{data?.title || '未知标题'}</h1>;
826
841
 
827
842
  应用部署在 `/app/<appId>/` 路径前缀下,组件内**禁止用根绝对路径**引用静态资产(如 `src="/x.svg"`、`src="/bg.mp4"`)——根绝对路径脱离前缀,预览与线上必 404。
828
843
 
829
- - 用户上传的媒体要在页面展示 → dataloom storage URL(`generateDownloadUrlFromFilePath`,见 client-builtins-file-storage-service),不要先下载进 `public/` 再硬编码路径。
844
+ - 用户上传的媒体要在页面展示 → 默认用上传返回或后端返回的 `download_url` / `downloadURL`;仅明确使用 `file_attachment` 类型字段时,才按 client-builtins-file-storage-service `generateDownloadUrlFromFilePath` 从该字段的 `file_path` 换 URL。不要下载进 `public/` 再硬编码路径。
830
845
  - 项目自带的静态资产 → 放 `client/src/assets/` 用 ES import(`import logoUrl from '@/assets/x.svg'`),构建管线自动带前缀;新增 `.svg`/`.mp4` 等类型须在 `client/src/types/global.d.ts` 补 `declare module` 声明。
831
846
 
832
847
  ### 排查资源 404
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: contacts-service
3
- description: "Use when 搜人/选人/人员选择器/部门选择器/群组选择、获取或展示用户与部门信息、把用户/部门/群组 ID 传给飞书内置插件或飞书开放平台 API、人员字段入库或导出,以及在产物服务端 / OpenAPI 接口里按 ID 查用户信息。统一妙搭 ID 体系(miaoda_user_id / employee_id / open_department_id / open_chat_id;lark_* 内部 ID 禁用)、字段获取分级与权限引导、服务端可用与不可用的通讯录能力边界。触发词:搜人, 选人, 人员选择器, 部门选择器, 群组选择, UserSelect, DepartmentSelect, ChatSelect, 获取用户信息, 用户字段, 部门信息, 群组信息, employee_id, open_id, union_id, open_department_id, open_chat_id, miaoda_user_id, lark_id, 飞书用户ID, 飞书部门ID, 外部用户, 工号, 手机号, 直属上级, 人员入库, 人员导出, id_convert, 通讯录, 选负责人, 选审批人, 传插件, 传开放平台, 选中的人传给, 把选中的人, 服务端查用户, 后端查用户, OpenAPI 查用户, 批量查用户, 按ID查人, AuthNPaasService, listUsersByIds"
3
+ description: "服务端按 ID 批量查用户(AuthNPaasService.listUsersByIds,返回项只有 miaodaUserID / name / avatar,没有 userId / userName)+ 前端搜人选人。Use when 在产物服务端 / OpenAPI 接口里按 ID 查用户姓名头像、搜人/选人/人员选择器/部门选择器/群组选择、获取或展示用户与部门信息、把用户/部门/群组 ID 传给飞书内置插件或飞书开放平台 API、人员字段入库或导出。统一妙搭 ID 体系(miaoda_user_id / employee_id / open_department_id / open_chat_id;lark_* 内部 ID 禁用)、字段获取分级与权限引导、服务端可用与不可用的通讯录能力边界。触发词:AuthNPaasService, listUsersByIds, 服务端查用户, 后端查用户, 批量查用户, 按ID查人, 查用户姓名, 查用户头像, MiaodaUserInfo, 搜人, 选人, 人员选择器, 部门选择器, 群组选择, UserSelect, DepartmentSelect, ChatSelect, 获取用户信息, 用户字段, 部门信息, 群组信息, employee_id, open_id, union_id, open_department_id, open_chat_id, miaoda_user_id, lark_id, 飞书用户ID, 飞书部门ID, 外部用户, 工号, 手机号, 直属上级, 人员入库, 人员导出, id_convert, 通讯录, 选负责人, 选审批人, 传插件, 传开放平台, 选中的人传给, 把选中的人, OpenAPI 查用户"
4
4
  steering: true
5
5
  steering-topic: contacts_service
6
6
  match-template-name: nestjs-react-fullstack
@@ -14,6 +14,10 @@ match-template-name: nestjs-react-fullstack
14
14
  >
15
15
  > **⚠️ 先分流**:一~五节讲的是**浏览器里**的搜人 / 选择器口径。代码跑在**产物服务端**(NestJS service / controller,尤其 `*.openapi.controller.ts`)时,能力边界完全不同——直接看[第六节](#六服务端--openapi-态怎么查通讯录)。
16
16
 
17
+ > `AuthNPaasService` 分流:`getCurrentUserLarkUserId`、`getBatchLarkUserIds`、`getBatchMiaodaUserIds` 属于 ID 转换,见 [`user-identity`](../user-identity/SKILL.md) 第三节;`listUsersByIds` 属于按妙搭 userId 查他人姓名 / 头像,见本 skill 第六节。
18
+ >
19
+ > `listUsersByIds` 返回 `(MiaodaUserInfo | null)[]`,与入参等长同序;单项只有 `miaodaUserID` / `name`(I18nText)/ `avatar`,没有 `userId` / `userName`。字段以第六节或 `node_modules/@lark-apaas/nestjs-authnpaas/dist/index.d.ts` 为准,禁止按常见 SDK 命名猜字段。
20
+
17
21
  ## 命名约定(必读)
18
22
 
19
23
  **以服务端字段为准。** 同一个 ID 在不同层有不同书写:
@@ -202,15 +206,19 @@ match-template-name: nestjs-react-fullstack
202
206
 
203
207
  ## 六、服务端 / OpenAPI 态怎么查通讯录
204
208
 
205
- ### 6.1 为什么服务端不一样
209
+ ### 6.1 先确认两类上下文
206
210
 
207
- 前端靠 cookie,网关校验登录态后注入 `x-larkgw-suda-webuser`;OpenAPI 请求**没有 cookie**,`req.userContext.userId` 恒为空。所以判据很简单:**「返回什么取决于调用者是谁」的能力在 OpenAPI 态一律不可用,「给定 ID 取数据」的可用**。
211
+ `userId` 标识当前用户,`appId` 标识当前应用,两者不能互相替代。`AuthNPaasService.listUsersByIds` 不要求当前 `userId`,但要求请求上下文已有**非空 `appId`**;已有 `miaoda_user_id` 不能证明或推导出该前置条件。匿名 / OpenAPI 请求没有登录用户,也不能默认具有 `appId`。
208
212
 
209
- ### 6.2 服务端能做什么
213
+ 按以下顺序决策:
210
214
 
211
- > **强制规则**:服务端(尤其 `/openapi`)查用户信息,**唯一允许的实现是 `AuthNPaasService.listUsersByIds`**(禁止项见 6.4.0)。`import` 不到说明 core 版本没跟上,**报告阻塞**,不要加直接依赖绕过、也不要自实现。
215
+ 1. 区分业务数据中的人员 ID、当前 `userId` 与请求上下文 `appId`。
216
+ 2. 确认已支持的运行时机制是否为当前入口提供非空 `appId`,不得凭空补身份头。
217
+ 3. 再选择查询能力并定义响应字段语义。
212
218
 
213
- **按 ID 批量查人** —— 从项目统一入口 `@lark-apaas/fullstack-nestjs-core` 取 `AuthNPaasService`。`PlatformModule.forRoot()` 已注册 global `AuthNPaasModule`,**不需要**在业务 module 加 `providers`,也**不需要**加直接依赖:
219
+ ### 6.2 有非空 appId 时按 ID 批量查人
220
+
221
+ 请求上下文有非空 `appId` 时,从项目统一入口 `@lark-apaas/fullstack-nestjs-core` 取 `AuthNPaasService` 并调用 `listUsersByIds`。`PlatformModule.forRoot()` 已注册 global 的 `AuthNPaasModule`,不要在业务 module 加 `providers` 或增加直接依赖:
214
222
 
215
223
  ```ts
216
224
  import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
@@ -220,44 +228,50 @@ export class TicketService {
220
228
  constructor(private readonly authn: AuthNPaasService) {}
221
229
 
222
230
  async attachAssignees(ids: string[]) {
223
- // 返回与入参等长同序,未命中的位置是 null
224
231
  const users = await this.authn.listUsersByIds(ids);
225
- return users.map((u, i) => ({
226
- miaoda_user_id: ids[i],
227
- name: u?.name?.zh_cn ?? u?.name?.en_us ?? '', // name 是 I18nText,不是字符串
228
- avatar: u?.avatar?.image?.large ?? '', // 头像 URL 在 image.large 上
232
+ return users.map((user, index) => ({
233
+ miaoda_user_id: ids[index],
234
+ name: user?.name?.zh_cn ?? user?.name?.en_us,
235
+ avatar: user?.avatar?.image?.large,
229
236
  }));
230
237
  }
231
238
  }
232
239
  ```
233
240
 
234
- - 入参是 **miaoda_user_id**,单次最多 **100** 个;返回 `(MiaodaUserInfo | null)[]`,与入参等长同序,不丢项也不错位
235
- - 只有 `miaodaUserID` / `name` / `avatar` 三个字段——**没有**邮箱、手机号、工号、部门
236
- - 平台出错抛 `HttpException`(502),**不会**静默返回空数组——**不要 try/catch 吞掉**,「平台挂了」和「查无此人」必须能区分
237
- - 服务端拿不到的字段(部门 / 邮箱 / 工号 / 职位)**从响应类型里删掉**,别用 `''` 占位骗调用方;**并告诉用户**这些字段要走飞书开放平台通讯录 API(见 6.3),不是没实现
241
+ - 入参是 **miaoda_user_id**,单次最多 **100** 个;返回 `(MiaodaUserInfo | null)[]`,与入参等长同序,未命中的位置是 `null`
242
+ - 只有 `miaodaUserID` / `name` / `avatar` 三个字段;`name` 是 `I18nText`,按语言字段映射
243
+ - 平台错误抛 `HttpException`(502),不要吞掉;它与返回 `null` 的查无此人语义不同
244
+ - `import` 不到说明 core 版本未提供该能力:报告阻塞,不要增加直接依赖或自实现替代品
238
245
 
239
- **妙搭 ↔ 飞书 ID 转换** —— 同一个 `AuthNPaasService` 上的 `getBatchLarkUserIds()` / `getBatchMiaodaUserIds()`(双向批量,不依赖调用者身份)。**不要拿它判断用户是否存在**——外部用户本来就没有 employeeId,会被误判成「用户不存在」。判存在性用 `listUsersByIds` 的返回是不是 `null`。
246
+ 登录态内部 `/api` 请求若具有非空 `appId`,保留上述合法批量查询;是否有 `userId` 不是这项能力的判据。
240
247
 
241
- ### 6.3 服务端做不到什么,改走哪里
248
+ ### 6.3 无法保证 appId 的匿名入口
242
249
 
243
- | 想做的事 | 服务端 | 替代路径 |
244
- |---|:---:|---|
245
- | 按 ID 批量查人 | ✅ | `AuthNPaasService.listUsersByIds` |
246
- | 妙搭 ↔ 飞书 ID 转换 | ✅ | `AuthNPaasService.getBatchLarkUserIds` / `getBatchMiaodaUserIds` |
247
- | 搜人 / 搜部门 / 搜群(按关键词) | ❌ | 飞书开放平台通讯录 API(应用身份 + 显式授权的通讯录范围) |
248
- | 取手机号 / 职位 / 工号 / 直属上级 | ❌ | 同上,字段 ↔ 权限映射见 [`feishu` › contacts.md](../../../feishu/references/contacts.md) |
249
- | 知道「当前调用者是谁」 | ❌ | OpenAPI 语义上没有当前用户;内部 `/api` 路由才有 `req.userContext.userId` |
250
- | 按 ID 批量查群 | ❌ | 暂无服务端能力;群信息在前端取,或走开放平台 IM API |
250
+ 匿名 / OpenAPI 入口无法证明非空 `appId` 时,主体资源读取必须独立于人员查询:
251
251
 
252
- **为什么搜索类不给服务端**:结果取决于「谁在搜」,而 OpenAPI 没有登录用户。若平台此时按租户全量返回,外部系统就能通过产物 OpenAPI 把整个企业通讯录搜穿。开放平台那条路的可见范围由租户授权决定,语义上正好匹配。
252
+ - 返回业务数据中已有的稳定 `miaoda_user_id`,不要因展示名查询失败而让主体读取失败
253
+ - 在 `shared/api.interface.ts` 和 `docs/openapi.json` 中把 `name`、`avatar` 等展示值建模为可空或可选;不要用固定“未知用户”或空字符串伪装已取得展示值
254
+ - 用户明确要求匿名响应必须返回真实姓名、但当前没有受支持的 `appId` 来源时,报告运行时能力阻塞;不要把未完成实现当作成功
253
255
 
254
- ### 6.4 服务端禁止行为
256
+ ### 6.4 能力边界与禁止项
255
257
 
256
- 0. **禁止用 `AuthNPaasService.listUsersByIds` 之外的任何方式在服务端查用户信息** —— 包括:复用应用内已有的用户 / 用户资料 service、自己写 HTTP 裸调平台通讯录接口、从数据库表 join 出人名头像。这条优先级最高,与下面各条冲突时以本条为准
257
- 1. **禁止在 `*.openapi.controller.ts` 里依赖 `req.userContext.userId`** —— OpenAPI 态恒为空
258
- 2. **禁止用 `getCurrentUserLarkUserId()`** —— 它读 `userId`,取不到只打日志返回 `null`、**不报错**,静默失效。要飞书 ID 一律用 `getBatchLarkUserIds()`
259
- 3. **禁止把 `name` 当字符串**渲染或入库 —— 它是 `I18nText`,直接用会得到 `[object Object]`
260
- 4. **禁止 catch SDK 抛的平台错误当空结果** —— 「平台挂了」和「查无此人」必须区分开
258
+ | 想做的事 | 条件 / 路径 |
259
+ |---|---|
260
+ | miaoda_user_id 批量查人 | 请求上下文有非空 `appId` `AuthNPaasService.listUsersByIds` |
261
+ | 妙搭 飞书 ID 转换 | `AuthNPaasService.getBatchLarkUserIds` / `getBatchMiaodaUserIds`;不能用来判断外部用户是否存在 |
262
+ | 搜人 / 搜部门 / 搜群 | 飞书开放平台通讯录 API(应用身份 + 显式授权的通讯录范围) |
263
+ | 取手机号 / 职位 / 工号 / 直属上级 | 同上,字段 ↔ 权限映射见 [`feishu` › contacts.md](../../../feishu/references/contacts.md) |
264
+ | 知道当前调用者是谁 | 登录态内部 `/api` 才读取 `req.userContext.userId` |
265
+ | 按 ID 批量查群 | 暂无服务端能力;在前端获取或使用开放平台 IM API |
266
+
267
+ 以下做法不能作为匿名人员展示的替代方案:
268
+
269
+ 1. 凭空补身份头或把人员 ID 当作 `appId`
270
+ 2. 复用依赖登录态的用户资料 service,或自己写 HTTP 裸调平台通讯录接口
271
+ 3. 在数据库持久化姓名 / 头像副本再 join 返回;通讯录展示值会过期且受权限约束
272
+ 4. 用 service 广域 `try/catch` 吞掉人员查询错误,或固定返回“未知用户”
273
+ 5. 用 `getCurrentUserLarkUserId()` 查目标用户,或用 `getBatchLarkUserIds()` 判断用户是否存在
274
+ 6. 把 `name` 当字符串直接渲染或入库
261
275
 
262
276
  ---
263
277
 
@@ -152,9 +152,6 @@ frontend-design 自定义方向没有可复制的源文件,只把基础设计
152
152
  - `metabase` **湖光蓝调**
153
153
  视觉:纯白与极浅蓝平面,深蓝墨字,明亮蓝色用于主按钮和交互焦点 / Lato 人文无衬线字体,粗体标题与常规正文形成清晰层级 / 小到中圆角,1px 半透明中性边框,平铺卡片轻投影,浮层采用更深漫射阴影 / 浅蓝静区、实底蓝主按钮与描边次按钮 / 通透、清爽、平和
154
154
 
155
- - `corporate-blueprint` **蓝图**
156
- 视觉:冷浅灰画布、白色卡片、深蓝主强调,图表以蓝色深浅层级为主,状态标签保留语义色 / 无衬线大标题与小号全大写宽字距标签,数值列等宽对齐 / 主卡片零圆角、顶部 3px 深蓝边线,配轻阴影与细分隔线 / 深蓝渐变头部、斜切几何装饰与编号章节 / 秩序、严谨、精确
157
-
158
155
  - `pm-spec` **规格蓝本**
159
156
  视觉:冷灰蓝底、白色纸面与深墨文字,靛紫用于小面积结构性强调 / Charter 衬线标题与引用,无衬线正文,等宽体承载全大写微标签 / 10px 圆角卡片、1px 细边框,无投影,引用块配强调色左边线 / 单栏纸面文档流、紧凑正文、元信息条与局部分栏信息块 / 严肃、克制、秩序感
160
157
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  ## 1. 按角色读,不按名字读
18
18
 
19
- 风格的 token 名不可信:20 个风格共 169 个名字,**144 个只在一个风格里出现过**;`accent` 在 13 个风格里出现但含义分裂——蓝图的 `accent` `#0033A0` 是品牌主色,翡翠光晕的 `accent` `#27272A` 是一条深灰细线。**8 个风格根本没有 `primary` 这个名字**。
19
+ 风格的 token 名不可信:不同风格会用同一个 token 名表达完全不同的角色,也会用不同名字表达同一种角色;`accent` 就可能是品牌主色、细线或点睛色。不要按名字机械映射,必须先看值和使用位置。
20
20
 
21
21
  所以先判角色,再落槽位:
22
22
 
@@ -38,7 +38,7 @@
38
38
 
39
39
  - **`--x-foreground` 是「压在 x 上的文字色」**,不是 x 的浅色底。风格里的 `success-bg` / `danger-bg` 这类徽章底色角色相反,按 §3 另加变量。
40
40
  - **风格里叫 `secondary` 的通常是第二数据色**(图表第二序列),不是 shadcn 的次级面。`--secondary` 该由「比画布深/浅一档的面」来填。
41
- - **风格里叫 `accent` 的可能是任何东西**——品牌主色(蓝图)、细线(翡翠光晕)、点睛色(薄荷荧光)。看值和用法,别看名。
41
+ - **风格里叫 `accent` 的可能是任何东西**——品牌主色、细线或点睛色。看值和用法,别看名。
42
42
 
43
43
  ## 2. 缺源怎么办(这是多数情况,不是例外)
44
44
 
@@ -46,7 +46,7 @@
46
46
 
47
47
  - **缺 `primary`**:风格若明确宣称无强调色(暗夜鎏金、夜航灯塔那类),`--primary` 填最高对比的中性色(深色风格填最亮的 ink,浅色风格填最深的 ink),`--primary-foreground` 填画布色。**不要自己发明一个强调色。**
48
48
  - **缺 `success` / `danger` / `warning`**:保留语义可辨的最低要求,但把饱和度和明度拉到风格的水位——取模板默认的色相,饱和度与明度对齐风格里已有的彩色 token。**风格一个彩色 token 都没有时(暗夜鎏金那类),饱和度取 35% 为下限**,不要跟着中性色降到 7%——那样红绿橙互相分不出来。这种情况下状态色只用于 badge 与文字,不做面。风格明确禁止某类色(克莱因蓝的双色绝对主义、暗夜鎏金的严格中性)时,只在状态标记这一个位置破例,其余不用。
49
- - **缺 chart-1..5**:从 `--primary` 的色相出发生成 5 档单色阶(改明度与饱和度,不引入新色相)。蓝图就是这么做的——它给了 chart-2..5 全是蓝,缺的 chart-1 按同一色阶补。
49
+ - **缺 chart-1..5**:从 `--primary` 的色相出发生成 5 档单色阶(改明度与饱和度,不引入新色相),不要直接套模板默认图表色。
50
50
 
51
51
  ## 3. 风格专有 token 不塞进槽位
52
52
 
@@ -17,7 +17,7 @@ file-match-pattern:
17
17
  |------|----------------------|--------------------------|
18
18
  | 鉴权 | 写操作加 `@NeedLogin()` | **不加** `@NeedLogin()`,鉴权在网关层通过 API Key 完成 |
19
19
  | 用户身份 | `req.userContext.userId` 区分用户 | 统一走系统身份,不依赖 `userId` 做业务区分 |
20
- | 通讯录 | 前端搜人 / 选择器随便用 | **必须**用 `AuthNPaasService.listUsersByIds`(见下文「用户信息」强制规则),搜索类不可用 |
20
+ | 通讯录 | 前端按登录态能力取人 | 先确认非空 `appId`;无法确认时仅返回稳定 ID,展示值可空 / 可选 |
21
21
  | Controller 文件 | `xxx.controller.ts` | `xxx.openapi.controller.ts`,放在同一 module 下 |
22
22
  | OpenAPI 文档 | 不需要 | **必须**同步维护 `docs/openapi.json`(见下文) |
23
23
 
@@ -62,75 +62,44 @@ findAll(@Req() req: Request) {
62
62
 
63
63
  ## 用户信息(通讯录)—— 强制规则
64
64
 
65
- **`/openapi` 路由下查询用户信息,唯一允许的实现是 `AuthNPaasService.listUsersByIds`。没有例外。**
65
+ `userId` 标识当前用户,`appId` 标识当前应用,业务记录中的人员 ID 不是二者。按以下顺序处理 `/openapi` 人员字段:
66
66
 
67
- ```typescript
68
- // 唯一允许的写法。注意 import fullstack-nestjs-core(项目统一入口)
69
- import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
67
+ 1. 区分 `userId`、`appId` 与要展示的 `miaoda_user_id`。
68
+ 2. 确认已支持的运行时机制是否为请求上下文提供**非空 `appId`**;给定人员 ID 或没有当前 `userId` 都不能证明该条件,不得凭空补身份头。
69
+ 3. 再选择人员查询能力并定义响应字段语义。
70
70
 
71
- @Controller('openapi/xxx')
72
- export class XxxOpenApiController {
73
- constructor(private readonly authn: AuthNPaasService) {}
74
-
75
- @Get(':id')
76
- async get(@Param('id') id: string) {
77
- // 不要 try/catch 吞掉——平台故障必须暴露给调用方,别降级成「查无此人」
78
- const [user] = await this.authn.listUsersByIds([id]);
79
- if (!user) throw new NotFoundException(`用户 ${id} 不存在`);
80
- return {
81
- miaodaUserId: id,
82
- name: user.name?.zh_cn ?? user.name?.en_us ?? '', // name 是 I18nText,不是字符串
83
- avatar: user.avatar?.image?.large ?? '', // 头像 URL 在 image.large 上
84
- };
85
- }
86
- }
87
- ```
71
+ ### 请求上下文有非空 appId
88
72
 
89
- **不要做这两件多余的事**(`PlatformModule.forRoot()` 已经注册了 global 的 `AuthNPaasModule`):
73
+ 允许从统一入口调用 `AuthNPaasService.listUsersByIds`。它不要求当前 `userId`,但要求非空 `appId`;`PlatformModule.forRoot()` 已注册 global 的 `AuthNPaasModule`,不要另加 `providers` 或直接依赖。
90
74
 
91
- - ❌ 在业务 module 里写 `providers: [AuthNPaasService]` —— 会另建一个实例
92
- - `package.json` `@lark-apaas/nestjs-authnpaas` 直接依赖 —— 走 `fullstack-nestjs-core` 即可
75
+ ```typescript
76
+ import { AuthNPaasService } from '@lark-apaas/fullstack-nestjs-core';
93
77
 
94
- ### 禁止(以下任一出现即为错误实现)
78
+ const [user] = await this.authn.listUsersByIds([miaodaUserId]);
79
+ if (!user) throw new NotFoundException(`用户 ${miaodaUserId} 不存在`);
80
+ return {
81
+ miaodaUserId,
82
+ name: user.name?.zh_cn ?? user.name?.en_us,
83
+ avatar: user.avatar?.image?.large,
84
+ };
85
+ ```
95
86
 
96
- | 禁止 | 为什么 |
97
- |---|---|
98
- | 复用应用里已有的用户 / 用户资料 service | 那些多半是给浏览器写的,依赖登录态;`/openapi` 下没有登录用户,会静默返回空 |
99
- | 自己写 HTTP 请求裸调平台通讯录接口 | 鉴权 / 重试 / 日志 / trace 都在 SDK 里,裸调必然漏 |
100
- | 用 `req.userContext.userId` 做用户查询 | `/openapi` 下**恒为空** |
101
- | 用 `AuthNPaasService.getCurrentUserLarkUserId()` | 它读 `userId`,取不到只打日志返回 `null`、**不报错**,故障静默 |
102
- | 从数据库表里 join 出人名 / 头像 | 通讯录不是应用数据,会过期且不 follow 权限 |
103
- | 按关键词搜人 / 搜部门 / 搜群 | 结果取决于「谁在搜」,`/openapi` 下没有调用者视角,**服务端不提供** |
104
- | `try/catch` 吞掉 `listUsersByIds` 抛的错、返回空值 | 「平台挂了」和「查无此人」是两回事,吞掉就没人知道故障 |
105
- | 用 `getBatchLarkUserIds` 判断用户是否存在 | 它是 ID 转换,**外部用户本来就没有 employeeId**,会把外部用户误判成不存在 |
106
- | 服务端拿不到的字段用 `''` 占位塞进响应 | 调用方会以为字段存在只是没值。拿不到就**从响应类型里删掉**,并告诉用户改走飞书开放平台通讯录 API |
87
+ 批量调用的返回值与入参等长同序;`name` `I18nText`,需映射语言字段。SDK 平台错误与返回 `null` 的查无此人语义必须不同,不要 `try/catch` 吞掉平台错误。
107
88
 
108
- ```typescript
109
- // ❌ 错误:包了一层应用现有的用户服务
110
- const profile = await this.userProfileService.findById(id);
111
-
112
- // ❌ 错误:OpenAPI 态搜不了人
113
- const users = await someSearchApi({ query: keyword });
114
-
115
- // ❌ 错误:读 userId,OpenAPI 态恒为空且失败不报错
116
- const larkId = await this.authn.getCurrentUserLarkUserId();
117
-
118
- // ❌ 错误:吞掉平台故障 + 拿不到的字段用空串占位
119
- let name = '';
120
- try {
121
- const [u] = await this.authn.listUsersByIds([id]);
122
- name = u?.name?.zh_cn ?? '';
123
- } catch { /* 平台挂了也当查无此人 */ }
124
- return { userId: id, name, department: '', email: '', jobTitle: '' };
125
- ```
89
+ ### 匿名入口无法保证 appId
126
90
 
127
- ### `AuthNPaasService` import 不到时
91
+ 主体资源读取不得依赖人员查询。返回业务数据中已有的稳定 `miaodaUserId`;在 `shared/api.interface.ts` `docs/openapi.json` 中将 `name`、`avatar` 等展示值建模为可空或可选,不要用空字符串或固定“未知用户”冒充查询结果。用户明确要求匿名响应必须返回真实姓名、但当前没有受支持的 `appId` 来源时,报告运行时能力阻塞。
128
92
 
129
- 说明当前 `@lark-apaas/fullstack-nestjs-core` 版本还没带上这个能力。**报告阻塞、请人升级 core 版本**——不要加 `@lark-apaas/nestjs-authnpaas` 直接依赖绕过去,也不要退回上面任何一种禁止写法或自己实现替代品。
93
+ ### 禁止(以下任一出现即为错误实现)
130
94
 
131
- ### 服务端拿不到的字段
95
+ - 凭空补身份头,或从 `userId` / 人员 ID 推导 `appId`
96
+ - 复用依赖登录态的用户资料 service,或自己写 HTTP 裸调平台通讯录接口
97
+ - 在数据库持久化姓名 / 头像副本再 join 返回
98
+ - 用 service 广域 `try/catch` 吞掉查询错误,或固定返回“未知用户”
99
+ - 用 `getCurrentUserLarkUserId()` 查目标用户,或用 `getBatchLarkUserIds()` 判断用户是否存在
100
+ - 按关键词搜人 / 搜部门 / 搜群;这需要调用者视角,服务端不提供
132
101
 
133
- 手机号 / 职位 / 工号 / 直属上级 / 在职状态、以及按 ID 查群 —— 服务端**没有**这些能力。需要就走飞书开放平台通讯录 API,或把该能力放回前端做,**不要**在 `/openapi` 里自己拼一个。
102
+ `AuthNPaasService` import 不到时,报告阻塞并请人升级 core 版本;不要加 `@lark-apaas/nestjs-authnpaas` 直接依赖或自实现替代品。手机号 / 职位 / 工号 / 直属上级 / 在职状态以及按 ID 查群不属于该能力,需使用受支持的前端或飞书开放平台路径。
134
103
 
135
104
  完整能力边界与字段口径见 [`contacts-service` › 第六节](../contacts-service/SKILL.md)。
136
105
 
@@ -6,6 +6,15 @@ steering-topic: plugin_guide
6
6
  match-template-name: nestjs-react-fullstack
7
7
  gate-tools:
8
8
  - tool: plugin_instance
9
+ - tool: api_request
10
+ when-contains:
11
+ url: "/api/capability"
12
+ - tool: bash
13
+ when-contains:
14
+ command: "/api/capability"
15
+ - tool: bash
16
+ when-contains:
17
+ command: "__innerapi__/capability"
9
18
  ---
10
19
 
11
20
  # Plugin 集成指南
@@ -20,6 +29,7 @@ gate-tools:
20
29
  | 获取运行时投影 | 调用 `get_plugin_ai_json(pluginInstanceId)` |
21
30
  | Client 侧调用 | `capabilityClient.load(id).call(actionKey, input)`(流式用 `callStream`) |
22
31
  | Server 侧调用(仅兜底) | `capabilityService.load(id).call(actionKey, input)` |
32
+ | 手动 HTTP/debug 调试 | 仅调 `/api/capability/<id>`、`/api/capability/<id>/stream`、`/__innerapi__/capability/debug/<id>` 或 `/__innerapi__/capability/debug/<id>/stream` 时,body 用 action/params:`{ "action": "<actionKey>", "params": { ... } }`;禁止在 body 顶层放 `actionKey`、`input` |
23
33
  | 长耗时 AI 结果 | 大体量/多字段/多份/多语言/文件或多模态串联等结构信号命中时,优先前端 `callStream` 渐进展示;需保存则流式结束后复用 CRUD 落库;仅在 Client 侧无法满足时使用后端任务记录 + 状态查询 + 结果读取;禁止单个 HTTP 请求等待完整结果后才返回 |
24
34
  | capabilityClient 导入 | `import { capabilityClient } from '@lark-apaas/client-toolkit'` |
25
35
  | CapabilityService 导入 | `import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';` |
@@ -40,9 +50,9 @@ gate-tools:
40
50
 
41
51
  **paramsSchema 仅支持 4 种参数类型**:文本 `{ "type": "string" }`、字符串数组 `{ "type": "array", "items": { "type": "string" } }`、图片 `{ "type": "string", "format": "picture" }`、文件 `{ "type": "string", "format": "file" }`,均需带 `description`。
42
52
 
43
- > **文件类参数**:`file` / `picture` / `plugin-file-url` / `plugin-image-url` 字段,Client 侧原始 File/Blob 直接传 `capabilityClient`。已持久化文件/图片由 Server 侧 `CapabilityService` 调 AI 插件时,取 `file_path`(或 `bucket_id + file_path`)用 `FileService.createSignedUrl(filePath, 3600)` / `fileService.from(bucketId).createSignedUrl(filePath, 3600)` 签成临时 http(s) URL 后传入。签名失败要 `warn`,业务状态保持 `failed` / `pending` / `null`,不写成功态。标准 `/app` 或 `/spark/app/.../runtime/api/v1/storage/object/<bucket>/<key>` 是可被平台 resolver 消费的内部路径,但不是 `createSignedUrl` `filePath` 入参;禁止给它拼接 `localhost`、`window.location.origin` 或 `/app/<app_id>`。非标准相对路径、前端展示 URL、非法 data URL 不作稳定入参。
53
+ > **文件类参数**:`file` / `picture` / `plugin-file-url` / `plugin-image-url` 字段,Client 侧原始 File/Blob 直接传 `capabilityClient`,禁止先经 dataloom 上传再传 URL。已持久化文件/图片由 Server 侧 `CapabilityService` 调 AI 插件时,优先取 `file_path`(或 `bucket_id + file_path`)用 `FileService.createSignedUrl(filePath, 3600)` / `fileService.from(bucketId).createSignedUrl(filePath, 3600)` 签成临时 http(s) URL 后传入。旧记录只有标准 `/app` 或 `/spark/app/.../runtime/api/v1/storage/object/<bucket>/<key>` download URL 时,必须先解析其中的 bucket 与 file path,再调用 `fileService.from(bucketId).createSignedUrl(filePath, 3600)`;默认 `createSignedUrl()` 只使用默认 bucket,不能从 URL 切换 bucket。签名失败或返回空 URL 时要 `warn`,业务状态保持 `failed` / `pending` / `null`,不写成功态。禁止给标准路径拼接 `localhost`、`window.location.origin` 或 `/app/<app_id>`;非标准相对路径、前端展示 URL、非法 data URL 不作稳定入参。
44
54
  >
45
- > **改造已有调用**:检查所有文档和图片插件调用点,统一使用上面的签名 URL。删除下载后转 base64 的旧逻辑。缺少 `file_path` 或签名失败时记录警告,写入 `failed` / `pending` / `null`,并停止调用插件;不得改用原始 URL。完整示例见 [Server 侧已持久化文件进入 AI capability](references/plugin-coding-guide.md#3-server-侧已持久化文件进入-ai-capability)。
55
+ > **稳定契约**:已持久化文件不得先下载再转 base64;应统一使用签名 URL。缺少可识别的文件路径、签名失败或返回空 URL 时记录警告,写入 `failed` / `pending` / `null`,并停止调用插件;不得改用原始 URL。完整示例见 [Server 侧已持久化文件进入 AI capability](references/plugin-coding-guide.md#3-server-侧已持久化文件进入-ai-capability)。
46
56
 
47
57
  ## 可用的 Plugin
48
58
 
@@ -76,7 +86,7 @@ gate-tools:
76
86
  - 调用侧:优先 Client(`unary` → `call()`,`stream` → `callStream()`);触发器/定时任务、敏感凭证、强事务、结果需落库 → Server 侧
77
87
  - 高耗时 AI capability 闸门:若运行时投影或功能设计显示输出规模大、输出字段多、需要多份结果、多语言/长文本、文件或多模态输入后继续生成、多个 capability 串联、或结果需后续查看/落库,禁止把完整生成放进一个同步 HTTP 请求等待;能由前端承接时优先 `callStream` 渐进展示,需保存则流式结束后复用 CRUD 落库;仅在 Client 侧无法满足时后端创建任务记录后快速返回、由前端短轮询状态/结果,或拆成多个独立小调用
78
88
  4. **代码放置**:Client(默认,用户交互触发)→ `client/` 组件/hooks;Server(兜底)→ `server/` Service。
79
- 5. **真实调用冒烟(完成前必须)**:至少成功调用一次 `call()` 或 `callStream()`(按 outputSchema 读 chunk);失败日志含最小字段(字段清单见 `references/plugin-coding-guide.md`)。无冒烟结果不得宣告完成。
89
+ 5. **真实调用冒烟(完成前必须)**:至少成功调用一次 SDK `call(actionKey, input)` 或 `callStream(actionKey, input)`(按 outputSchema 读 chunk);失败日志含最小字段(见 `references/plugin-coding-guide.md`)。绕过 SDK 手动复测 capability HTTP/debug 入口前,先读 `references/plugin-coding-guide.md` 的“服务端 capability 手动 HTTP 调试与排障”;无冒烟结果不得宣告完成。
80
90
 
81
91
  ### Plugin Chain 调用示例
82
92
 
@@ -151,6 +161,8 @@ const structured = await capabilityClient
151
161
 
152
162
  **创建 `ai-text-to-json` 实例时**:一次性定义所有提取字段(参考 schema/表单/UI),最多 20 个。字段类型以当次 `formSchema.paramType.enum` 为准;静态指南/`readme` 只解释 enum 内类型,冲突时按 enum 建模并说明冲突,不得降级为 String;enum 缺失、不可读或缺业务所需类型时停止并报告合同冲突。基础字段用 String/Number/Boolean 等对应类型,同构列表用 Array,固定字段集合用 Object,嵌套成员写进 `paramDescription`。对象/列表结果不得序列化进 String;只有业务要求原样透传、不解析、不改写 JSON 文本时才用 String。创建后调用 `get_plugin_ai_json`,按 `outputSchema` 核验字段并生成调用代码;按契约直接消费结果,禁止无依据 `JSON.parse`。
153
163
 
164
+ `jsonStructure` 的顶级字段都会参与必填校验:模型漏任一 key 时整次调用抛 `INVALID_OUTPUT`。插件只用文本 prompt,不用 JSON Schema 强制生成;顶级字段越多,整体失败概率越高。N 套同构结果用一个 Array 字段承载,元素结构写进 `paramDescription`,不要摊成 N x M 个顶级字段。Array/Object 内部只做 `z.any()` 校验,`paramDescription` 仅影响 prompt;消费方仍须校验元素数量、成员字段和空值。
165
+
154
166
  **创建 `ai-image-to-json` 实例时**:必须一次性定义**所有**需提取字段(参考数据库 schema / 表单定义 / UI 设计),宁多勿漏;字段类型仅支持 String/Number/Boolean,最多 20 个;先调 `get_plugin_ai_json` 确认上游插件的 `outputSchema` 确保输入格式正确。
155
167
 
156
168
  ## 多维表格数据架构
@@ -273,6 +285,6 @@ const url = `https://${req.hostname}${process.env.CLIENT_BASE_PATH}${routePath}`
273
285
  | 流式 chunk 当字符串拼接(`text += chunk`) | chunk 是对象,按 outputSchema 解构:`text += chunk.content \|\| ''` |
274
286
  | formValue 用 `{% raw %}["{{input.xxx}}"]{% endraw %}` 包装已是 array 的 paramsSchema 参数 | paramsSchema 为 array 时 formValue 透传 `{% raw %}"{{input.xxx}}"{% endraw %}`,不再包一层数组 |
275
287
  | 前端调插件后不保存结果(页面刷新丢失),或为保存结果单独新建 API 端点 | 需持久化时 Server 侧调用直接落库(优先),或 Client 侧调用后立即经**已有** CRUD 接口保存 |
276
- | Server 侧把非标准相对路径、误拼 origin 的 storage object path、前端展示 URL 或非法 base64 data URL 传给 `file` / `picture` / `plugin-file-url` / `plugin-image-url` 参数 | 已持久化文件用 `file_path` 或 `bucket_id + file_path` 生成 1h 签名 http(s) URL 后再传;标准 storage object path 原样经过平台 resolver 时可消费,但不能直接作为 `createSignedUrl` `filePath` |
288
+ | Server 侧把非标准相对路径、误拼 origin 的 storage object path、前端展示 URL 或非法 base64 data URL 传给 `file` / `picture` / `plugin-file-url` / `plugin-image-url` 参数 | 已持久化文件优先用 `file_path` 或 `bucket_id + file_path` 生成 1h 签名 http(s) URL 后再传;标准 storage download URL 要先解析 bucket file path,再从对应 bucket 签名 |
277
289
  | 创建了 PluginInstance 但只建不调 | CREATE 后必须接 `get_plugin_ai_json` → 生成调用代码 → 集成业务逻辑 |
278
290
  | 插件返回值 `as any` 直接取字段 | 按 outputSchema 生成 TypeScript interface |