@lark-apaas/coding-steering 0.1.6-alpha.1 → 0.1.6-alpha.11

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 (39) hide show
  1. package/README.md +11 -2
  2. package/package.json +1 -1
  3. package/steering/design-stack/skills/.gitkeep +0 -0
  4. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +122 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +621 -0
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +505 -0
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
  9. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
  10. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
  11. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +139 -0
  12. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +405 -0
  13. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +628 -0
  14. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  15. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +270 -0
  16. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +214 -0
  17. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +163 -0
  18. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +309 -0
  19. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +190 -0
  20. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +160 -0
  21. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +256 -0
  22. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +103 -0
  23. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +198 -0
  24. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +128 -0
  25. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +207 -0
  26. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +164 -0
  27. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +90 -0
  28. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +164 -0
  29. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +267 -0
  30. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +582 -0
  31. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +357 -0
  32. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +513 -0
  33. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  34. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  35. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +452 -0
  36. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +300 -0
  37. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  38. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +253 -0
  39. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +585 -0
@@ -0,0 +1,90 @@
1
+ # 权限管理 (Permission)
2
+
3
+ > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/permission/overview
4
+
5
+ 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中管理飞书云文档的协作者权限。
6
+
7
+ ## 所需权限
8
+
9
+ | 权限标识 | 说明 |
10
+ |----------|------|
11
+ | `drive:permission` | 管理文档/文件的协作者权限 |
12
+
13
+ > **敏感操作警告**:权限管理涉及文档访问控制,添加/移除协作者会直接影响用户的文档可见性。操作前请确认目标对象和权限级别。
14
+
15
+ ## 列出协作者
16
+
17
+ ```typescript
18
+ const res = await client.drive.permissionMember.list({
19
+ path: { token: 'ABC123' },
20
+ params: { type: 'docx' },
21
+ });
22
+ const members = res.data?.items ?? [];
23
+ // members: [{member_type, member_id, perm, name}, ...]
24
+ ```
25
+
26
+ ## 添加协作者
27
+
28
+ ```typescript
29
+ const res = await client.drive.permissionMember.create({
30
+ path: { token: 'ABC123' },
31
+ params: { type: 'docx', need_notification: false },
32
+ data: {
33
+ member_type: 'email',
34
+ member_id: 'user@example.com',
35
+ perm: 'edit',
36
+ },
37
+ });
38
+ ```
39
+
40
+ ## 移除协作者
41
+
42
+ ```typescript
43
+ await client.drive.permissionMember.delete({
44
+ path: { token: 'ABC123', member_id: 'user@example.com' },
45
+ params: { type: 'docx', member_type: 'email' },
46
+ });
47
+ ```
48
+
49
+ ## Token 类型参考
50
+
51
+ | 类型 | 说明 |
52
+ |------|------|
53
+ | `doc` | 旧版文档 |
54
+ | `docx` | 新版文档 |
55
+ | `sheet` | 电子表格 |
56
+ | `bitable` | 多维表格 |
57
+ | `folder` | 文件夹 |
58
+ | `file` | 上传的文件 |
59
+ | `wiki` | 知识库节点 |
60
+ | `mindnote` | 思维导图 |
61
+
62
+ ## 成员类型参考
63
+
64
+ | 类型 | 说明 |
65
+ |------|------|
66
+ | `email` | 邮箱地址 |
67
+ | `openid` | 用户 open_id |
68
+ | `userid` | 用户 user_id |
69
+ | `unionid` | 用户 union_id |
70
+ | `openchat` | 群聊 open_id |
71
+ | `opendepartmentid` | 部门 open_id |
72
+ | `groupid` | 用户组 ID |
73
+ | `wikispaceid` | 知识空间 ID |
74
+
75
+ ## 权限级别参考
76
+
77
+ | 权限值 | 说明 |
78
+ |--------|------|
79
+ | `view` | 仅查看 |
80
+ | `edit` | 可编辑 |
81
+ | `full_access` | 完全访问(可管理权限) |
82
+
83
+ ## Common Mistakes
84
+
85
+ | 错误 | 正确做法 |
86
+ |------|----------|
87
+ | token 类型与文件实际类型不匹配 | `type` 参数必须与文件实际类型一致(docx/sheet/bitable 等) |
88
+ | 用 wiki URL 的 token 直接操作权限 | Wiki 节点需用 `wiki` 类型,或先获取 `obj_token` 用对应类型 |
89
+ | 添加协作者时成员不存在 | 确认 member_id 正确,email 需要是飞书注册邮箱 |
90
+ | 移除自身的 full_access 权限 | 文档至少需要一个管理员,避免移除最后一个 full_access 成员 |
@@ -0,0 +1,164 @@
1
+ # 知识库 (Wiki)
2
+
3
+ > 开放平台文档(Markdown 版):https://open.larkoffice.com/document/server-docs/docs/wiki-v2/wiki-overview
4
+
5
+ 使用 `@larksuiteoapi/node-sdk` 在 NestJS 中操作飞书知识库。
6
+
7
+ ## 所需权限
8
+
9
+ | 权限标识 | 说明 |
10
+ |----------|------|
11
+ | `wiki:wiki` | 读写知识库 |
12
+ | `wiki:wiki:readonly` | 只读知识库 |
13
+
14
+ > 机器人需被添加为知识空间成员才能访问:知识空间 → 设置 → 成员管理 → 添加机器人。
15
+
16
+ ## 从 URL 提取 Token
17
+
18
+ URL 格式:`https://xxx.feishu.cn/wiki/{token}`
19
+
20
+ ```typescript
21
+ const url = 'https://xxx.feishu.cn/wiki/ABC123def';
22
+ const token = new URL(url).pathname.split('/wiki/')[1]; // 'ABC123def'
23
+ ```
24
+
25
+ ## 列出知识空间
26
+
27
+ ```typescript
28
+ const res = await client.wiki.space.list({});
29
+ const spaces = res.data?.items ?? [];
30
+ // spaces: [{space_id, name, description, visibility}, ...]
31
+ ```
32
+
33
+ > 如果返回空列表,说明机器人未被添加到任何知识空间。
34
+
35
+ ## 列出节点
36
+
37
+ ```typescript
38
+ // 列出空间根节点
39
+ const res = await client.wiki.spaceNode.list({
40
+ path: { space_id: '7xxx' },
41
+ });
42
+ const nodes = res.data?.items ?? [];
43
+ // nodes: [{node_token, obj_token, obj_type, title, has_child}, ...]
44
+
45
+ // 列出子节点
46
+ const childRes = await client.wiki.spaceNode.list({
47
+ path: { space_id: '7xxx' },
48
+ params: { parent_node_token: 'wikcnXXX' },
49
+ });
50
+ ```
51
+
52
+ ## 获取节点详情
53
+
54
+ ```typescript
55
+ const res = await client.wiki.space.getNode({
56
+ params: { token: 'ABC123def' }, // 从 URL 提取的 token
57
+ });
58
+ const node = res.data?.node;
59
+ // node: {node_token, space_id, obj_token, obj_type, title, parent_node_token, has_child, creator}
60
+ ```
61
+
62
+ > **关键**:返回的 `obj_token` 是实际文档/表格的 token,需用它来调用 docx/bitable 等 API。
63
+
64
+ ## 创建节点
65
+
66
+ ```typescript
67
+ const res = await client.wiki.spaceNode.create({
68
+ path: { space_id: '7xxx' },
69
+ data: {
70
+ obj_type: 'docx', // 节点类型
71
+ node_type: 'origin', // 固定值
72
+ title: '新页面',
73
+ // parent_node_token: 'wikcnXXX', // 可选:父节点
74
+ },
75
+ });
76
+ const node = res.data?.node;
77
+ // node: {node_token, obj_token, obj_type, title}
78
+ ```
79
+
80
+ ### obj_type 取值
81
+
82
+ | 值 | 说明 |
83
+ |------|------|
84
+ | `docx` | 新版文档(默认) |
85
+ | `doc` | 旧版文档 |
86
+ | `sheet` | 电子表格 |
87
+ | `bitable` | 多维表格 |
88
+ | `mindnote` | 思维导图 |
89
+ | `file` | 文件 |
90
+ | `slides` | 幻灯片 |
91
+
92
+ ## 移动节点
93
+
94
+ ```typescript
95
+ await client.wiki.spaceNode.move({
96
+ path: { space_id: '7xxx', node_token: 'wikcnXXX' },
97
+ data: {
98
+ target_space_id: '7yyy', // 目标空间(不传则同空间内移动)
99
+ target_parent_token: 'wikcnYYY', // 目标父节点
100
+ },
101
+ });
102
+ ```
103
+
104
+ ## 重命名节点
105
+
106
+ ```typescript
107
+ await client.wiki.spaceNode.updateTitle({
108
+ path: { space_id: '7xxx', node_token: 'wikcnXXX' },
109
+ data: { title: '新标题' },
110
+ });
111
+ ```
112
+
113
+ ## Wiki-Doc 工作流(关键)
114
+
115
+ 知识库页面的内容读写必须通过 docx API,流程:
116
+
117
+ ```typescript
118
+ // 1. 获取节点详情 → 拿到 obj_token
119
+ const nodeRes = await client.wiki.space.getNode({
120
+ params: { token: wikiToken },
121
+ });
122
+ const objToken = nodeRes.data?.node?.obj_token;
123
+
124
+ // 2. 用 obj_token 作为 doc_token 读取文档
125
+ const contentRes = await client.docx.document.rawContent({
126
+ path: { document_id: objToken },
127
+ });
128
+
129
+ // 3. 用 obj_token 作为 doc_token 写入文档
130
+ const convertRes = await client.docx.document.convert({
131
+ data: { content_type: 'markdown', content: '# 新内容\n\n正文...' },
132
+ });
133
+ await client.docx.documentBlockChildren.create({
134
+ path: { document_id: objToken, block_id: objToken },
135
+ data: { children: convertRes.data?.blocks ?? [] },
136
+ });
137
+ ```
138
+
139
+ > **重要**:不要用 `node_token` 或 URL 中的 `token` 直接调用 docx API,必须用 `getNode()` 返回的 `obj_token`。
140
+
141
+ ## 搜索不可用
142
+
143
+ Wiki API 不提供搜索功能。获取内容需通过以下方式:
144
+
145
+ - 通过 `spaceNode.list()` 浏览节点树
146
+ - 通过 `space.getNode()` + URL 中的 token 直接查询
147
+
148
+ ## 知识库访问设置
149
+
150
+ 机器人需要被添加为知识空间成员才能访问:
151
+
152
+ 1. 打开知识空间 → 设置 → 成员管理
153
+ 2. 添加机器人应用
154
+ 3. 参考:https://open.feishu.cn/document/server-docs/docs/wiki-v2/wiki-qa
155
+
156
+ ## Common Mistakes
157
+
158
+ | 错误 | 正确做法 |
159
+ |------|----------|
160
+ | 用 wiki URL 中的 token 直接调用 docx API | 必须先 `getNode()` 获取 `obj_token`,再用 `obj_token` 调用 docx API |
161
+ | 列出空间返回空但实际有内容 | 机器人未被添加为空间成员 |
162
+ | 尝试搜索知识库内容 | Wiki API 不支持搜索,只能通过 `list` 浏览或 `getNode` 查询 |
163
+ | 创建节点忘记传 `node_type: 'origin'` | `node_type` 是必填字段,值固定为 `'origin'` |
164
+ | 混淆 `node_token` 和 `obj_token` | `node_token` 是知识库节点标识,`obj_token` 是实际文档标识 |
@@ -0,0 +1,267 @@
1
+ ---
2
+ name: openapi-guide
3
+ description: "OpenAPI 对外开放接口编码规范 + docs/openapi.json 产物维护。覆盖鉴权、用户身份、模块组织、OpenAPI 3.0 JSON 的结构与写入规则。Use when: 创建或修改 /openapi 路由、编写对外开放接口、维护 docs/openapi.json。触发词:openapi, 开放接口, 对外接口, 对外 API, open api, openapi controller, docs/openapi.json"
4
+ steering: true
5
+ steering-inclusion: fileMatch
6
+ steering-topic: openapi_guide
7
+ match-template-name: nestjs-react-fullstack
8
+ file-match-pattern:
9
+ - "**/*.openapi.controller.ts"
10
+ ---
11
+
12
+ # OpenAPI 对外开放接口编码规范
13
+
14
+ 本规范适用于以 `/openapi` 为前缀的对外开放接口。**除下述差异外,均遵循 `/api` 的接口编码规范**。与 `/api` 的差异速查:
15
+
16
+ | 维度 | `/api`(内部业务接口) | `/openapi`(对外开放接口) |
17
+ |------|----------------------|--------------------------|
18
+ | 鉴权 | 写操作加 `@NeedLogin()` | **不加** `@NeedLogin()`,鉴权在网关层通过 API Key 完成 |
19
+ | 用户身份 | `req.userContext.userId` 区分用户 | 统一走系统身份,不依赖 `userId` 做业务区分 |
20
+ | Controller 文件 | `xxx.controller.ts` | `xxx.openapi.controller.ts`,放在同一 module 下 |
21
+ | OpenAPI 文档 | 不需要 | **必须**同步维护 `docs/openapi.json`(见下文) |
22
+
23
+ ## 鉴权
24
+
25
+ `/openapi` 路由的鉴权完全在网关层通过 API Key 完成,Controller 层不需要任何鉴权相关代码。
26
+
27
+ ```typescript
28
+ // ✅ 正确:/openapi 路由不加鉴权装饰器
29
+ @Controller('openapi/orders')
30
+ export class OpenApiOrdersController {
31
+ @Post()
32
+ create(@Body() body: CreateOrderRequest) { ... }
33
+ }
34
+
35
+ // ❌ 错误:给 /openapi 路由加 @NeedLogin()
36
+ @Controller('openapi/orders')
37
+ export class OpenApiOrdersController {
38
+ @NeedLogin() // 不需要!
39
+ @Post()
40
+ create(@Body() body: CreateOrderRequest) { ... }
41
+ }
42
+ ```
43
+
44
+ ## 用户身份
45
+
46
+ OpenAPI 场景下不区分用户,统一走系统身份。禁止在 `/openapi` Controller 中使用 `req.userContext.userId` 做业务逻辑区分。
47
+
48
+ ```typescript
49
+ // ✅ 正确:不依赖用户身份
50
+ @Get()
51
+ findAll(@Query() query: FindOrdersQuery) {
52
+ return this.ordersService.findAll(query);
53
+ }
54
+
55
+ // ❌ 错误:用 userId 过滤数据
56
+ @Get()
57
+ findAll(@Req() req: Request) {
58
+ return this.ordersService.findByUser(req.userContext.userId); // OpenAPI 下无意义
59
+ }
60
+ ```
61
+
62
+ ## 模块组织
63
+
64
+ `/openapi` Controller 和 `/api` Controller 放在**同一个 module** 下,共享 Service 层。用文件名 `xxx.openapi.controller.ts` 区分。
65
+
66
+ ```
67
+ modules/orders/
68
+ ├── orders.module.ts # 同时注册两个 Controller
69
+ ├── orders.controller.ts # @Controller('api/orders') — 内部接口
70
+ ├── orders.openapi.controller.ts # @Controller('openapi/orders') — 开放接口
71
+ └── orders.service.ts # 共享业务逻辑
72
+ ```
73
+
74
+ 在 `orders.module.ts` 中注册:
75
+ ```typescript
76
+ @Module({
77
+ controllers: [OrdersController, OpenApiOrdersController],
78
+ providers: [OrdersService],
79
+ })
80
+ export class OrdersModule {}
81
+ ```
82
+
83
+ ## OpenAPI 文档产物(必须维护)
84
+
85
+ 整个应用所有 `/openapi` 路由**汇总在一份** `docs/openapi.json` 文件里(仓库根目录)。
86
+
87
+ - 路径 = 仓库根 `docs/openapi.json`
88
+ - 所有 `*.openapi.controller.ts` 的 endpoint 共享这一个文件的 `paths`
89
+ - 新仓库若没有该文件,Agent 自行创建
90
+ - 该文件是对外 OpenAPI 的**权威 spec**(运行时被读取并对外暴露,不是纯文档)——精确性要求高,写错/漏写会直接影响外部调用方
91
+ - 内部 `/api` 路由**不**写进来
92
+
93
+ ## 何时创建/更新
94
+
95
+ | 修改场景 | 文档动作 |
96
+ |---|---|
97
+ | 新建 `*.openapi.controller.ts` | 在 `docs/openapi.json` 的 `paths` 下新增对应 path 项 |
98
+ | 已有 openapi controller 上增删 endpoint、改方法/路径/参数 | 精准增删/改对应 `paths["/openapi/..."]` 条目 |
99
+ | 修改 `shared/api.interface.ts` 里被 openapi 路由引用的 interface / type | 同步更新所有受影响 path 的 schema |
100
+ | 修改 service 返回值或 drizzle schema,且类型变化反映到 openapi 路由的响应 | 同步更新对应 path 的 responses schema |
101
+
102
+ > 编辑 service / interface / schema 时本 skill 不会被 file-match 自动注入;此时 `coding-guide` 的 `## API 规范` 第 8 条会提醒 Agent 主动加载本 skill 完成同步。
103
+
104
+ ## 安全更新协议(避免误删其它 path)
105
+
106
+ 单文件聚合场景下最大的风险是 Agent 编辑某个 path 时意外丢掉其它 controller 的 path。**必须**按以下步骤:
107
+
108
+ 1. **先 Read**:完整读入 `docs/openapi.json`
109
+ 2. **再 Edit**:用精确 `Edit` 改特定 `"/openapi/<path>": { ... }` 块,不要 `Write` 全文件
110
+ 3. **最后自检**:见末尾「写后自检」小节——确认 `paths` 下所有条目都在、且每条都有 `operationId` + `responses`
111
+
112
+ ## 文件格式
113
+
114
+ 顶层结构**固定**:
115
+
116
+ ```json
117
+ {
118
+ "paths": {
119
+ "/openapi/<feature>": { <pathItem> },
120
+ "/openapi/<feature>/{id}": { <pathItem> }
121
+ }
122
+ }
123
+ ```
124
+
125
+ 硬性规定:
126
+
127
+ - **path 键**写完整绝对路径(含 `/openapi/` 前缀),与 `@Controller(...)` + `@Get/Post/...(...)` 拼接结果一致
128
+ - **不**放 `basePath`、**不**放 `components`、**不**放 `info`/`servers`——所有 schema 内联,中间件会补齐其它顶层字段
129
+ - 每个 operation 对象**必须**有:`operationId`(= controller 方法名,驼峰)、`summary`(一句中文说明)、`responses`(至少 `"200"`)
130
+ - 路径参数、查询参数、请求体、响应体的位置由 controller 装饰器决定(见下)
131
+ - 同一 URL 上的多个 HTTP 方法共用一个 path 键(如 `@Controller('openapi/tickets')` 下的 `@Post()` 与 `@Get()` 都映射到 `/openapi/tickets`,共享 pathItem)
132
+
133
+ ## Controller 装饰器 → OpenAPI 位置
134
+
135
+ **位置看装饰器,类型看 TS**——两者缺一不可。
136
+
137
+ | Nest 装饰器 | OpenAPI 位置 | 备注 |
138
+ |---|---|---|
139
+ | `@Body() body: T` | `requestBody.content['application/json'].schema` = `T` 的 schema | `T` 通常是 `shared/api.interface.ts` 的 interface |
140
+ | `@Query('foo') foo: T` | `parameters[]` 一项:`in: 'query'`, `name: 'foo'`, `schema: T`;`?` → `required: false` | 每个 `@Query('x')` 产一项 |
141
+ | `@Query() q: T` | `parameters[]` 每字段一项:`in: 'query'`, `name: <field>`, `schema: <field 类型>` | 整个 interface 被摊平到 query params |
142
+ | `@Param('id') id: T` | `parameters[]` 一项:`in: 'path'`, `name: 'id'`, **`required: true`**, `schema: T` | path 参数恒 `required: true` |
143
+ | `@Headers('x-foo') v: T` | `parameters[]` 一项:`in: 'header'`, `name: 'x-foo'`, `schema: T` | `X-Api-Key` 等鉴权头走网关层,不写进 spec |
144
+
145
+ ## Schema 权威来源
146
+
147
+ 对外接口的请求/响应类型统一在 `shared/api.interface.ts` 用 TS `interface` / `type` 维护,前后端共享。
148
+
149
+ 三层优先级(从高到低):
150
+
151
+ 1. **`shared/api.interface.ts` 中的 TS interface** — 请求体类型 / controller 返回值类型的唯一权威。从 controller 方法签名找到对应 interface 名称,再递归展开成 JSON Schema。
152
+ 2. **controller 方法签名** — 各参数的 TS 类型与方法返回值类型,就是对应 OpenAPI 位置的 schema 来源(装饰器到 OpenAPI 位置的映射见上表)。类型可以是 `shared/api.interface.ts` 里的 interface、primitive、或 controller 文件内的内联类型声明;方法返回值类型即 OpenAPI 响应 schema 所描述的对象。
153
+ 3. **drizzle schema** — 仅作字段底层类型的辅助参考;**绝不**用来扩展 interface 里没有的字段(interface 已显式定义对外字段集合;drizzle 里多出来的内部字段如 `userId`/`internalRemark` 不得出现在 `docs/openapi.json` 里)。
154
+
155
+ 若 service 实现与 interface 脱节,属代码 bug;JSON schema 一律以 interface 为准。
156
+
157
+ ## TS → JSON Schema 映射
158
+
159
+ | TS 写法 | JSON Schema |
160
+ |---|---|
161
+ | `foo: string` | `{ "type": "string" }`,`"foo"` 进 `required` |
162
+ | `foo?: string` / `foo: string \| undefined` | `{ "type": "string" }`,**不**进 `required` |
163
+ | `foo: number` | `{ "type": "number" }` 或 `"integer"`(看 drizzle 是 `integer()` 还是 `real()`) |
164
+ | `foo: boolean` | `{ "type": "boolean" }` |
165
+ | `type X = 'a' \| 'b' \| 'c'` 或直接字面量联合 | `{ "type": "string", "enum": ["a","b","c"] }` |
166
+ | `foo: SomeInterface` | 递归展开 `{ "type": "object", "required": [...], "properties": { ... } }` |
167
+ | `foo: T[]` | `{ "type": "array", "items": <T 的 schema> }` |
168
+ | `foo: Date` | `{ "type": "string", "format": "date-time" }`(序列化多为 ISO 字符串) |
169
+ | `foo: T \| null`(可空,非"可选") | 在 `T` 的 schema 上加 `"nullable": true`;`"foo"` 仍进 `required` |
170
+
171
+ ## `required` 三个位置
172
+
173
+ OpenAPI 3.0 里 `required` 按上下文有三种写法,判定依据**统一是 TS `?` 修饰符**。
174
+
175
+ | 上下文 | 写法 | 放在哪 |
176
+ |---|---|---|
177
+ | Parameter Object(query/path/header) | `"required": true/false` | 参数对象顶层 |
178
+ | RequestBody Object(请求体整体) | `"required": true` | requestBody 顶层 |
179
+ | Object Schema(对象字段) | `"required": ["field1","field2"]` 字段名数组 | object schema 顶层,**不**在每个 property 内 |
180
+
181
+ ## description 与 example
182
+
183
+ TS interface 本身不带 `description` 和 `example`。**按字段名语义自行生成**,不改 interface 源码或加 JSDoc hack。
184
+
185
+ - `description`:一句中文解释,聚焦业务含义,别复述类型
186
+ - `example`:选一个**真实合理**的值(`"13800000000"` 而不是 `"xxx"`)
187
+
188
+ ## 深层嵌套省略规则
189
+
190
+ 响应/请求结构很深时允许偷懒:
191
+
192
+ - **第一层 properties** — 永远不省略
193
+ - **深层嵌套对象** — 允许只写 `{ "type": "object" }` 不列 properties,但**必须**在这一层写 `example`(swagger-ui 合成不到深层)
194
+ - **展开了 properties 的 object** — 不写顶层 `example`(swagger-ui 会从 property-level 合成)
195
+ - **primitive 字段**(string/number/boolean) — 必须写 property-level `example`
196
+ - **数组** — `items` 写一个元素的 schema + example,数组外层不写 `example`(由 `items` 合成)
197
+
198
+ ## 规则表讲不透的片段示例
199
+
200
+ TS interface 怎么翻译、装饰器映射到 parameter 的哪个 `in`、`required` 怎么标——全部看上面各节的表。**本节只示范规则表替代不了的 3 件事**:混用装饰器形成的 parameters 数组、第一层完整展开 vs 深层对象省略的对照、description / example 的风格。
201
+
202
+ ```json
203
+ {
204
+ "paths": {
205
+ "/openapi/tickets/{assigneeId}": {
206
+ "get": {
207
+ "operationId": "listByAssignee",
208
+ "summary": "按受理人查询工单列表(游标分页)",
209
+ "parameters": [
210
+ { "in": "path", "name": "assigneeId", "required": true,
211
+ "schema": { "type": "string" }, "description": "受理人 ID", "example": "usr_007" },
212
+ { "in": "query", "name": "priority", "required": false,
213
+ "schema": { "type": "string", "enum": ["low","normal","urgent"] },
214
+ "description": "按优先级过滤", "example": "urgent" },
215
+ { "in": "query", "name": "cursor", "required": false,
216
+ "schema": { "type": "string" },
217
+ "description": "游标(首次请求省略)", "example": "tk_abc" }
218
+ ],
219
+ "responses": {
220
+ "200": {
221
+ "description": "ok",
222
+ "content": {
223
+ "application/json": {
224
+ "schema": {
225
+ "type": "object",
226
+ "required": ["items", "hasMore"],
227
+ "properties": {
228
+ "items": {
229
+ "type": "array",
230
+ "items": {
231
+ "type": "object",
232
+ "description": "Ticket 详情(深层省略,schema 见 example)",
233
+ "example": {
234
+ "id": "tk_abc", "title": "空调故障报修", "priority": "urgent",
235
+ "assigneeCount": 2, "location": { "building": "A 栋", "floor": 3 }
236
+ }
237
+ }
238
+ },
239
+ "nextCursor": { "type": "string", "description": "下一页游标(最后一页省略)", "example": "tk_abc" },
240
+ "hasMore": { "type": "boolean", "description": "是否还有下一页", "example": true }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ ```
252
+
253
+ 要点:
254
+
255
+ - **装饰器混用 → parameters 数组**:`@Param('assigneeId') + @Query('priority') + @Query('cursor')` 合并成 3 项 parameter,path 参数 `required: true`、query 参数 `required: false`
256
+ - **第一层完整展开 + 深层对象省略**:响应外层对象展开,`items` / `hasMore` 进 `required` 数组而 `nextCursor` 不进;数组元素是 `Ticket`,这一层用深层省略——`type: 'object'` + `example` 示范元素结构,不重复翻译 Ticket 的 schema
257
+ - **description / example 风格**:一句中文语义("按优先级过滤" 而非"过滤参数")、真实合理值(`"usr_007"` / `"tk_abc"` / `"A 栋"`,不是 `"xxx"` / `"foo"`)
258
+
259
+ ## 写后自检(强制)
260
+
261
+ 每次 Write/Edit 完 `docs/openapi.json` 之后,**必须**立即在项目根执行下面这条自检命令。未通过要当场修到通过为止,才视为本次任务完成:
262
+
263
+ ```bash
264
+ node -e "const M=['get','post','put','patch','delete','head','options'];const s=require('fs').readFileSync('docs/openapi.json','utf8');const o=JSON.parse(s);if(!o.paths||typeof o.paths!=='object')throw new Error('missing paths');for(const [p,item] of Object.entries(o.paths))for(const [m,op] of Object.entries(item)){if(!M.includes(m))continue;if(!op.operationId||!op.responses)throw new Error(p+' '+m+' missing operationId/responses');}console.log('ok, '+Object.keys(o.paths).length+' paths');"
265
+ ```
266
+
267
+ 覆盖:JSON 语法错 / 顶层缺 `paths` / 某个 operation 缺 `operationId` 或 `responses`;最后打印 path 数量方便和修改前做比对,防止误删。