@lark-apaas/coding-steering 0.1.14 → 0.1.15-alpha.0

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/anycross-forward/SKILL.md +180 -0
  3. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +124 -0
  4. package/steering/nestjs-react-fullstack/skills/authn-guide/SKILL.md +3 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +11 -2
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +0 -6
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +1 -4
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +5 -3
  9. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +17 -4
  10. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +4 -31
  11. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +226 -116
  12. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +319 -0
  13. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +666 -0
  14. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +222 -0
  15. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -1
  16. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +87 -36
  17. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +1 -4
  18. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +5 -6
  19. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +5 -4
  20. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +0 -1
  21. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +3 -2
  22. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +86 -87
  23. package/steering/nestjs-react-fullstack/skills_local/openapi-guide/SKILL.md +262 -0
  24. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +580 -0
  25. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +322 -0
  26. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/scripts/plugin-hydrate.js +152 -0
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: contacts-service
3
+ description: "Use when 搜人/选人/人员选择器/部门选择器/群组选择、获取或展示用户与部门信息、把用户/部门/群组 ID 传给飞书内置插件或飞书开放平台 API、人员字段入库或导出。统一妙搭 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, 通讯录, 选负责人, 选审批人, 传插件, 传开放平台, 选中的人传给, 把选中的人"
4
+ steering: true
5
+ steering-topic: contacts_service
6
+ match-template-name: nestjs-react-fullstack
7
+ ---
8
+
9
+ # 妙搭通讯录服务 — 用户与部门信息获取指南
10
+
11
+ > 本 Skill 定义妙搭 Agent 处理**用户 / 部门 / 群组信息**的完整规范:ID 体系、接口返回结构、ID 选择决策、字段获取分级与权限引导。
12
+ >
13
+ > **相关 skill**:当前登录用户("我是谁"、useCurrentUserProfile、req.userContext)见 [`user-identity`](../user-identity/SKILL.md);选择器/展示组件的 props 与用法见 [`client-builtins-user-service`](../client-builtins-user-service/SKILL.md);飞书原生接口(通讯录 API、id_convert)见 [`feishu`](../../../feishu/SKILL.md)。
14
+
15
+ ## 命名约定(必读)
16
+
17
+ **以服务端字段为准。** 同一个 ID 在不同层有不同书写:
18
+
19
+ | 层 | 命名 | 例 |
20
+ |---|---|---|
21
+ | 服务端 wire / 原始 SDK(client-toolkit) | 小驼峰 + ID 全大写 | `employeeID` `openDepartmentID` `openChatID` |
22
+ | 组件 normalize 后 / Agent 写的应用代码 | 下划线 | `employee_id` `open_department_id` `open_chat_id` |
23
+
24
+ 下文用下划线(Agent 面)书写;直接用 client-toolkit 原始 service 时换成小驼峰。
25
+
26
+ ---
27
+
28
+ ## 一、ID 体系规范(全局强制)
29
+
30
+ ### 1.1 用户 ID
31
+
32
+ | 标准名 | 含义 | 适用用户 | 默认返回 | 可直传飞书 API |
33
+ |---|---|---|:---:|:---:|
34
+ | **miaoda_user_id** | 妙搭用户全局唯一 ID | 所有用户 | 是 | 否 |
35
+ | **employee_id** | 飞书企业内 user_id | 仅内部飞书用户 | 是 | **是** |
36
+ | **open_id** | 飞书应用级用户 ID | 内部 + 外部飞书 | 否(需 id_convert) | 是 |
37
+ | **union_id** | 飞书开发者级用户 ID | 内部 + 外部飞书 | 否(需 id_convert) | 是 |
38
+
39
+ ### 1.2 部门 ID
40
+
41
+ | 标准名 | 含义 | 可直传飞书 API |
42
+ |---|---|:---:|
43
+ | **open_department_id** | 飞书部门 ID(od- 开头) | **是** |
44
+
45
+ ### 1.3 群组 ID
46
+
47
+ | 标准名 | 含义 | 可直传飞书 API |
48
+ |---|---|:---:|
49
+ | **open_chat_id** | 飞书群组 ID(oc_ 开头) | **是** |
50
+
51
+ > 群组接口可能同时返回旧字段 `chat_id` 和新增 `open_chat_id`。**按前缀判断**:`oc_` 开头可直传飞书 IM API;纯数字即 `lark_chat_id`(禁用)。优先用 `open_chat_id`。
52
+
53
+ ### 1.4 历史兼容字段(仍返回,但**禁止使用**)
54
+
55
+ | 兼容字段 | 标准替代 | 为什么禁用 |
56
+ |---|---|---|
57
+ | **lark_id** | employee_id | 纯数字内部 ID,飞书开放平台 API 无法识别,传入报错或返回空 |
58
+ | **lark_department_id** | open_department_id | 纯数字内部 ID,飞书 API 只接受 od- 格式 |
59
+ | **lark_chat_id** | open_chat_id | 纯数字内部 ID,飞书 IM API 只接受 oc_ 格式 |
60
+
61
+ > 搜人返回里同时有 `lark_id` 和 `employee_id` 时,**必须取 `employee_id`**;用户提到 lark_id 时主动纠正。
62
+
63
+ ### 1.5 废弃名称映射
64
+
65
+ | 废弃名称 | 应理解为 |
66
+ |---|---|
67
+ | userID / user_id / suda_user_id | miaoda_user_id |
68
+ | larkUserID / larkUserId(纯数字) | lark_id(禁用)→ 飞书用 employee_id |
69
+ | departmentID / larkDepartmentID(纯数字) | open_department_id |
70
+ | lark_user_id / 飞书用户ID / 飞书ID | 用户口中的这些 = **可用的飞书 user_id**:搜人/选择器场景用 `employee_id`,当前登录用户用 `useCurrentUserProfile().lark_user_id`(二者 == employee_id)。**绝不是**禁用的纯数字 `lark_id` |
71
+
72
+ ---
73
+
74
+ ## 二、搜人 / 选择器返回结构
75
+
76
+ ### 2.1 内部用户(user_type = "_employee")
77
+
78
+ ```json
79
+ {
80
+ "miaoda_user_id": "17xxxxxxxxxx",
81
+ "user_type": "_employee",
82
+ "name": "张三",
83
+ "avatar": "https://xxx/avatar.png",
84
+ "employee_id": "abcdef",
85
+ "email": "zhangsan@company.com",
86
+ "mobile": "138xxxx0001",
87
+ "open_department_id": "od-xxxxxxx",
88
+ "job_title": "产品经理",
89
+ "employee_no": "10086",
90
+ "lark_id": "700001" // ⚠️ 兼容字段,禁止使用
91
+ }
92
+ ```
93
+
94
+ ### 2.2 外部用户(user_type = "_externalUser")
95
+
96
+ ```json
97
+ {
98
+ "miaoda_user_id": "17xxxxxxxxxx",
99
+ "user_type": "_externalUser",
100
+ "name": "李四",
101
+ "avatar": "https://xxx/avatar.png"
102
+ }
103
+ ```
104
+
105
+ > 外部用户**没有 employee_id**;需要飞书 ID 时走 id_convert 拿 open_id(见第三节)。
106
+
107
+ ---
108
+
109
+ ## 三、ID 使用决策逻辑
110
+
111
+ ```
112
+ 用户请求涉及用户 ID
113
+
114
+ ├─ 目标:飞书内置插件(发消息/审批/多维表格写人员)?
115
+ │ └─ 是 → 传 miaoda_user_id(插件内部自动转换,对调用方透明)
116
+
117
+ └─ 目标:飞书开放平台 API?
118
+ ├─ 内部用户(_employee) → 直接用 employee_id(零额外调用)
119
+ └─ 外部用户(_externalUser) → 先 id_convert(miaoda_user_id → open_id)再传
120
+ ```
121
+
122
+ ### 规则总结
123
+
124
+ | 场景 | Agent 行为 |
125
+ |---|---|
126
+ | 调飞书内置插件 | 传 **miaoda_user_id** |
127
+ | 调飞书开放平台 API · 内部用户 | 用 **employee_id** |
128
+ | 调飞书开放平台 API · 外部用户 | 先 id_convert 拿 **open_id** 再传 |
129
+ | 跨应用识别用户 | id_convert 拿 **union_id** |
130
+ | 人员字段入库 | 存 **miaoda_user_id** |
131
+ | 部门相关操作 | 用 **open_department_id** |
132
+ | 群组相关操作 | 用 **open_chat_id** |
133
+
134
+ ### ID 转换接口
135
+
136
+ 外部用户 / 取 open_id·union_id 时用 `id_convert`,详见 [`feishu` skill › references/id-convert.md](../../../feishu/references/id-convert.md)。
137
+
138
+ ---
139
+
140
+ ## 四、字段获取路径分级
141
+
142
+ ### Level 1 — 默认返回(搜人接口直接返回,needFullFields=false)
143
+
144
+ `miaoda_user_id`、`user_type`、`name`、`avatar`(所有用户);`employee_id`、`email`、`open_department_id`(仅内部用户,follow 飞书 admin「成员字段可见范围」)。
145
+
146
+ ### Level 2 — 搜人 full 字段(**必须 needFullFields=true**)
147
+
148
+ `nickname`、`mobile`(手机)、`gender`、`country`、`work_station`、`employee_no`(工号)、`city`、`job_title`(职位)、`employee_type`、`leader`(直属上级)、`dotted_line_leaders`(虚线上级)。
149
+
150
+ > **命中方式**:`UserService.searchUsers({ query, needFullFields: true })` → SDK 入 body `{"needFullFields":true}` → 后端 `DirectorySearchUserRequest.NeedFullFields=true` 返回;仍 follow admin 字段权限。
151
+ > ⚠️ 默认 false——**工号/手机/职位/上级 不传 needFullFields 就拿不到**。dataloom 的 search 暂不支持该参数,需要 full 字段走 toolkit `UserService`。
152
+
153
+ ### Level 3 — 需 id_convert(open_id / union_id)
154
+
155
+ | 字段 | 获取方式 |
156
+ |---|---|
157
+ | open_id / union_id | id_convert 接口 + 开放平台应用凭证(见 feishu › id-convert.md) |
158
+
159
+ ### Level 4 — 搜人拿不到的字段(需开放平台通讯录 API + 字段权限)
160
+
161
+ 如 `status`(在职状态)、`join_time`、`enterprise_email`、`custom_attrs`、`department_path` 等。`GET /open-apis/contact/v3/users/:user_id`(user_id 传 employee_id),**字段 ↔ 权限点映射见 [`feishu` skill › references/contacts.md](../../../feishu/references/contacts.md)**。
162
+
163
+ ---
164
+
165
+ ## 五、用户引导策略
166
+
167
+ ### 5.1 核心原则
168
+ - ID 转换对用户透明,Agent 自动处理
169
+ - Level 1 字段零配置,直接用,**禁止**为它引导开平流程
170
+ - 仅特定场景才引导创建/使用飞书开放平台应用
171
+ - 精准映射:按用户请求的字段精确告知所需权限
172
+
173
+ ### 5.2 决策流程
174
+ ```
175
+ 是否非用户触发获取(批量/定时/任务)?
176
+ ├─ 是 → 场景 A:开放平台应用 + TAT(应用身份)
177
+ └─ 否 → 需要默认外/无权限字段?
178
+ ├─ 是 → 场景 B:开放平台应用 + UAT(用户身份)
179
+ └─ 否 → 场景 C:直接搜人接口查询
180
+ ```
181
+ > TAT = tenant_access_token(应用身份,后端/无交互场景);UAT = user_access_token(用户身份,取用户有权数据)。
182
+
183
+ **场景 A(TAT 批量/定时)**:批量拉部门/全员、定时同步组织架构、任务回调取人、外部用户 id_convert。引导:飞书开放平台建应用 → app_id+app_secret 取 tenant_access_token → 调对应 API。
184
+
185
+ **场景 B(UAT,搜人拿不到的字段)**:请求 Level 4 字段(如在职状态/入职时间/自定义字段),搜人接口即便 needFullFields=true 也不返回。引导:建应用 → 申请字段权限(映射见 contacts.md)→ OAuth 取 user_access_token → `contact/v3/users/:user_id`(user_id 传 employee_id)。
186
+
187
+ **场景 C(妙搭搜人直接拿,无需开平应用)**:Level 1 字段直接取;Level 2 字段(工号/手机/职位/性别/上级)**带 `needFullFields: true`** 取——两者都不用开平应用,follow admin 字段权限即可。传内置插件取 miaoda_user_id、调开放平台内部用户取 employee_id、部门取 open_department_id。
188
+
189
+ ### 5.3 常见话术
190
+ | 用户需求 | 策略 |
191
+ |---|---|
192
+ | 工号 / 手机号 / 职位 / 性别 / 直属上级 | 搜人带 **needFullFields=true**(Level 2,无需开平,follow admin 权限) |
193
+ | 在职状态 / 入职时间 / 自定义字段 | 引导开平应用 → 通讯录 API(场景 B,Level 4) |
194
+ | open_id / union_id | 引导配置应用 → id_convert(场景 A,Level 3) |
195
+ | 部门信息 | 直接取 open_department_id |
196
+ | 发消息给某人 | 取 miaoda_user_id 传插件 |
197
+ | 批量同步部门/全员、定时同步 | 引导配置应用 → 通讯录 API(场景 A) |
198
+
199
+ ---
200
+
201
+ ## 六、禁止行为
202
+ 1. **禁用兼容字段**:任何调用不得用 lark_id / lark_department_id / lark_chat_id
203
+ 2. **禁止混用废弃名**:不得用 userID / suda_user_id / larkUserId 等旧名
204
+ 3. **禁止给飞书内置插件传 employee_id**:插件入参必须是 miaoda_user_id
205
+ 4. **禁止让用户手动做 ID 转换**:Agent 自动判断并执行
206
+ 5. **禁止不看 user_type 就猜 ID 类型**:先判断内部/外部
207
+ 6. **禁止对 Level 1 字段引导开平流程**
208
+
209
+ ---
210
+
211
+ ## 七、错误处理
212
+ | 错误场景 | Agent 行为 |
213
+ |---|---|
214
+ | 用户说"获取 user_id" | 追问:妙搭 miaoda_user_id 还是飞书 employee_id? |
215
+ | 用户说要 `lark_user_id` / "飞书用户ID" / "飞书 ID" | 多数指**可用的飞书 user_id = `employee_id`**。搜人/选择器结果里**没有**叫 lark_user_id 的字段、可用的就是 `employee_id`,**别返回禁用的纯数字 `lark_id`**;当前登录用户场景取 `useCurrentUserProfile().lark_user_id`(== employee_id)。歧义时确认"是调飞书 API 用吗",是则一律 employee_id |
216
+ | 用户用了 lark_id | 纠正:"lark_id 是禁用兼容字段,飞书 API 不认,请用 employee_id(搜人已默认返回)" |
217
+ | 用户用了 lark_department_id | 纠正:"请用 open_department_id(od- 格式)" |
218
+ | 用户用了 lark_chat_id | 纠正:"请用 open_chat_id(oc_ 格式)" |
219
+ | 外部用户请求 employee_id | 说明外部用户无 employee_id,建议 open_id(需 id_convert) |
220
+ | id_convert 报错 | 检查 miaoda_user_id 是否正确、开放平台应用凭证是否有效 |
221
+ | 通讯录 API 权限不足 | 引导检查开放平台应用字段权限(场景 B) |
222
+ | 批量拉取但只会搜人接口 | 说明搜人是交互式,批量走通讯录 API(场景 A) |
@@ -72,7 +72,6 @@ modules/orders/
72
72
  ```
73
73
 
74
74
  在 `orders.module.ts` 中注册:
75
-
76
75
  ```typescript
77
76
  @Module({
78
77
  controllers: [OrdersController, OpenApiOrdersController],
@@ -45,6 +45,26 @@ match-template-name: nestjs-react-fullstack
45
45
  - 构建搜索过滤条件(filter / conditions)时
46
46
  - 使用多维表格相关 UI 组件(UserDisplay、Hyperlink、Select 等)时
47
47
 
48
+ ### 多维表格应用的数据架构选择
49
+
50
+ 当应用需要读取飞书多维表格数据时,根据**查询模式**选择架构:
51
+
52
+ | 查询模式 | 架构选择 | 实现方式 |
53
+ |---------|---------|---------|
54
+ | 展示/编辑单条记录 | 纯插件 | `getRecord` / `batchUpdateRecords` |
55
+ | 列表分页浏览 | 纯插件 | `searchRecords` + `pageToken` 游标分页 |
56
+ | 统计聚合(计数/求和/平均) | **纯插件 + aggregateQuery** | 禁止 searchRecords 全量拉取后内存计算 |
57
+ | 统计 + 明细下钻 | 纯插件 | 聚合用 `aggregateQuery`,下钻用 `searchRecords` + filter |
58
+ | 排行榜 / TOP N | 纯插件 | `searchRecords` + sort + pageSize=N |
59
+ | 复杂排序/多表关联/全文搜索 | 插件同步 + 本地数据库 | 定时/Webhook 同步到 postgres,复杂查询走数据库 |
60
+ | 高频写入 + 读取 | 本地数据库为主 | 多维表格仅作展示/备份 |
61
+
62
+ **快捷判断:**
63
+ - 用户说"仪表盘/dashboard/驾驶舱/看板/统计" → **必须用 `aggregateQuery`**
64
+ - 用户说"列表/明细/详情" → `searchRecords` 分页
65
+ - 用户说"排行榜/TOP N" → `searchRecords` + sort + pageSize=N
66
+ - 需要跨表关联或复杂计算 → 建本地数据库表 + 同步
67
+
48
68
  ## Plugin 链式调用(Plugin Chain)
49
69
 
50
70
  很多业务场景需要多个插件串联完成,**禁止用正则/字符串解析替代 AI 插件做结构化输出处理**(包括提取和生成场景)。
@@ -134,20 +154,17 @@ const structured = await capabilityClient
134
154
  ```
135
155
 
136
156
  > **Client 侧提示**:`capabilityClient` 支持直接传 File/Blob 对象作为文件参数,无需先上传到 dataloom 获取 URL:
137
- >
138
157
  > ```typescript
139
158
  > // Client 侧:直接传 File 对象,SDK 自动处理上传
140
159
  > const rawResult = await capabilityClient
141
160
  > .load('doc_parser_instance')
142
161
  > .call('parseDocToMarkdown', { fileUrl: [file] }); // file 为 File/Blob 对象
143
162
  > ```
144
- >
145
163
  > ⚠️ 仅 `capabilityClient`(Client 侧)支持此能力,Server 侧 `CapabilityService` 仅支持 URL 字符串。
146
164
 
147
165
  ### 创建结构化提取 PluginInstance 的关键要求
148
166
 
149
167
  创建 `ai-text-to-json` 或 `ai-image-to-json` 类型的 PluginInstance 时:
150
-
151
168
  1. **必须一次性定义所有需要提取的字段**(参考数据库 schema / 表单定义 / UI 设计),宁多勿漏
152
169
  2. 字段类型仅支持 String / Number / Boolean,最多 20 个字段
153
170
  3. 先调用 `get_plugin_ai_json` 确认上游插件的 `outputSchema`,确保输入格式正确
@@ -193,7 +210,6 @@ const structured = await capabilityClient
193
210
  2. **明确报错**:plugin 未配置/未就绪时直接 `toast.error('未配置飞书多维表格,请前往设置页配置')` + 跳转配置页,**不**编一个假的"同步成功"
194
211
 
195
212
  ## 核心概念
196
-
197
213
  新版链路中,**Plugin(插件)**、**PluginInstance(插件实例配置)**、**PluginInstanceAIJson(运行时投影:pluginInstance.ai.json)** 的关系如下:
198
214
 
199
215
  - **Plugin(插件)**:底层承载单元,包含插件元信息与表单定义(form.schema)。模型侧只感知插件及其表单字段,不感知插件内部实现细节。
@@ -205,10 +221,9 @@ const structured = await capabilityClient
205
221
  - Code Agent 在生成**调用代码**前,必须读取它作为权威依据(Server 侧用 `CapabilityService`,Client 侧用 `capabilityClient`)
206
222
 
207
223
  ### 插件 Plugin
208
-
209
224
  插件是插件实例的承载单元,包含:
210
- 插件元信息(tags/name/description/version/...)
211
- 插件表单定义(form.schema),用于描述"这个插件需要哪些表单字段"。
225
+ 插件元信息(tags/name/description/version/...)
226
+ 插件表单定义(form.schema),用于描述"这个插件需要哪些表单字段"。
212
227
  重要:模型侧只感知插件与其表单 schema,不感知插件内部实现(如 Action的实现、API 细节等)。
213
228
 
214
229
  Plugin 的具体内容以JSON格式给出,例如:
@@ -232,19 +247,17 @@ Plugin 的具体内容以JSON格式给出,例如:
232
247
  ```
233
248
 
234
249
  ### 插件实例 PluginInstance
235
-
236
250
  开发框架内置 `plugin` 工具,用于创建和管理基于 **Plugin(插件表单)** 的业务插件实例(PluginInstance)。
237
251
 
238
252
  **重要说明**:
239
-
240
253
  - PluginInstance 的配置以"单文件 JSON"形式存储在 `server/capabilities/`(每个插件实例一个文件,逻辑上对应 server/capabilities/<id>.json)。
241
254
  - 运行时调用前,Code Agent 需要通过 get_plugin_ai_json 获取对应插件实例的 pluginInstance.ai.json,再基于其中的 actions/schema/outputMode 生成调用代码。
242
255
  - 运行时调用入口统一走 SDK/Service:
243
- - Server 侧:CapabilityService.load(pluginInstanceId).call(actionKey, input)
244
- - Client 侧:capabilityClient.load(pluginInstanceId).call(actionKey, input)(流式用 callStream)
256
+ - Server 侧:CapabilityService.load(pluginInstanceId).call(actionKey, input)
257
+ - Client 侧:capabilityClient.load(pluginInstanceId).call(actionKey, input)(流式用 callStream)
245
258
 
246
- PluginInstance 的配置以 JSON 形式输出,例如:
247
259
 
260
+ PluginInstance 的配置以 JSON 形式输出,例如:
248
261
  ```json
249
262
  {
250
263
  "id": "create_feishu_group", // 全局唯一语义化 ID
@@ -270,7 +283,6 @@ PluginInstance 的配置以 JSON 形式输出,例如:
270
283
  **注意**paramsSchema 支持以下 4 种参数类型,需要按下面规定的格式进行填充:
271
284
 
272
285
  1. **文本** - 单行或多行文本输入
273
-
274
286
  ```json
275
287
  {
276
288
  "type": "string",
@@ -279,7 +291,6 @@ PluginInstance 的配置以 JSON 形式输出,例如:
279
291
  ```
280
292
 
281
293
  2. **数组** - 字符串数组(如 ID 列表、标签列表等)
282
-
283
294
  ```json
284
295
  {
285
296
  "type": "array",
@@ -292,7 +303,6 @@ PluginInstance 的配置以 JSON 形式输出,例如:
292
303
  ```
293
304
 
294
305
  3. **图片** - 图片资源(需指定 format 为 picture)
295
-
296
306
  ```json
297
307
  {
298
308
  "type": "string",
@@ -302,7 +312,6 @@ PluginInstance 的配置以 JSON 形式输出,例如:
302
312
  ```
303
313
 
304
314
  4. **文件** - 文件资源(需指定 format 为 file)
305
-
306
315
  ```json
307
316
  {
308
317
  "type": "string",
@@ -312,20 +321,17 @@ PluginInstance 的配置以 JSON 形式输出,例如:
312
321
  ```
313
322
 
314
323
  > **注意**:`format` 为 `file`、`picture` 或 `plugin-file-url` 的字段在 Client 侧调用时均支持直接传入 File/Blob 对象,`capabilityClient` SDK 会自动处理上传;Server 侧 `CapabilityService` 仅支持 URL 字符串。**禁止**Client 侧先通过 dataloom 上传文件拿 URL 再传给插件——dataloom 的 `download_url` 是内部存储路径,插件服务端可能无法访问。
315
- >
316
324
  #### PluginInstanceAIJson(运行时投影 / 工程转化层产物:pluginInstance.ai.json)
317
-
318
325
  pluginInstance.ai.json 是从 PluginInstance 配置派生出的运行时插件实例说明(Runtime Spec),用于 Code Agent 动态生成调用代码。
319
326
  它包含:
320
- 插件实例元数据(id/pluginKey/pluginVersion/name/description)
321
- 可执行入口列表 actions[](每个入口包含 key/inputSchema/outputSchema/outputMode)
322
- 详细说明 readme
323
- type:单入口/多入口(single_action | multi_action)
327
+ 插件实例元数据(id/pluginKey/pluginVersion/name/description)
328
+ 可执行入口列表 actions[](每个入口包含 key/inputSchema/outputSchema/outputMode)
329
+ 详细说明 readme
330
+ type:单入口/多入口(single_action | multi_action)
324
331
 
325
332
  **重要**:模型不能自行猜测某个插件实例有哪些 action、入参/出参结构;在生成调用代码前必须通过工具读取该 pluginInstance.ai.json。
326
333
 
327
334
  PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
328
-
329
335
  ```json
330
336
  {
331
337
  "type": "multi_action", // 插件实例类型:single_action 表示仅 1 个 action;multi_action 表示多个 action(调用前需选择 actionKey)
@@ -361,21 +367,17 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
361
367
  ```
362
368
 
363
369
  ## 可用的 Plugin
364
-
365
370
  ```
366
371
  {{available_plugins}}
367
372
  ```
368
-
369
373
  说明:先从可用的 PluginInstance 进行选择,如果无法满足需求,看可用的 Plugin,如果有满足的插件,调用插件生成工具进行生成,如果没有,直接拒答。
370
374
 
371
375
  ## 可用的 PluginInstance
372
-
373
376
  ```
374
377
  {{available_plugin_instances}}
375
378
  ```
376
379
 
377
380
  ### 使用方式
378
-
379
381
  1. **创建/修改 PluginInstance(配置层)**:调用 `plugin_instance` 工具生成/更新单文件 PluginInstance JSON(基于插件表单封装)。
380
382
  2. **查询已有 PluginInstance(配置层)**:优先使用可用的 PluginInstance;如仍需核对细节,再读取对应插件实例配置(调用'get_plugin_ai_json')。
381
383
  3. **生成运行时代码(调用层)**:
@@ -384,19 +386,17 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
384
386
  - 仔细阅读返回的 readme,必须严格遵循里面制定的规则
385
387
 
386
388
  ### 典型场景示例
387
-
388
389
  - **消息通知类**:封装"发送飞书消息"相关插件为业务插件实例
389
390
  - **群组管理类**:封装"创建飞书群组"相关插件为业务插件实例
390
391
  - **AI 生成类**:封装"AI 生文/生图/图片理解"相关插件为业务插件实例
391
392
 
392
393
  ### 使用限制
393
-
394
394
  - PluginInstance 必须通过 `plugin_instance` 工具创建/更新,不支持 agent 直接手改 `server/capabilities/` 下的配置文件
395
395
  - 调用前必须通过 `get_plugin_ai_json` 获取权威 schema,禁止猜测入参/出参结构
396
396
  - PluginInstance 配置信息存储在 `server/capabilities/` 目录
397
397
  - 运行时调用统一走 SDK/Service,不再为每个插件实例预生成固定的 call 文件
398
- - Server 侧:CapabilityService.load(capabilityId).call(actionKey, input)
399
- - Client 侧:capabilityClient.load(capabilityId).call(actionKey, input)(流式用 callStream)
398
+ - Server 侧:CapabilityService.load(capabilityId).call(actionKey, input)
399
+ - Client 侧:capabilityClient.load(capabilityId).call(actionKey, input)(流式用 callStream)
400
400
 
401
401
  ## PluginInstance 生成约束
402
402
 
@@ -416,6 +416,38 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
416
416
  - 必须基于 `get_plugin_ai_json` 返回的 schema 生成真实调用代码
417
417
  - 如果调用失败或参数不明确,必须反馈给用户并补齐关键参数
418
418
 
419
+ **特别禁止的 Mock 反模式**(以下代码特征命中任何一个即为违规):
420
+
421
+ | 代码特征 | 为什么错 |
422
+ |---------|---------|
423
+ | `setTimeout(() => resolve(硬编码数据))` | 用延时模拟 AI 响应,实际没调用插件 |
424
+ | `Math.random()` 替代 AI 分类/评分/推荐结果 | 调了插件但丢弃返回值,用随机数代替 |
425
+ | `logger.log(input); return { success: true }` | 通知方法是占位空壳,没调 CapabilityService |
426
+ | `const PRESET_DATA = [...]` 替代插件调用 | 用硬编码预置数据架空 AI 能力 |
427
+
428
+ > **铁律覆盖的插件范围**:除 AI 类(生文/生图/翻译/结构化等),**外部服务类插件**(feishu-bitable / send-feishu-message / feishu-group-create / wiki / docx 等)铁律同样适用,不属于"展示数据 mock"豁免范围。"速度优先 / 快速迭代" 不豁免铁律——用户明确请求接入实际能力时(飞书写入 / 消息通知 / AI 生图等),必须真实调用,不接受 `setTimeout` / `sessionStorage` / 硬编码作为 fallback。
429
+ >
430
+ > **prototype 场景的精化豁免**:当用户在 prompt 中**显式**说明"原型 / 不接后端 / 不接业务 / 仅做 UI / 示例数据即可 / 占位图就行 / 纯前端"等表述时,整页 mock(含表格示例行、随机占位图、setTimeout 模拟同步)**允许保留**,**不要**仅因数据动词命中(如"同步""写入""导出")就强行接入真实插件。判定优先级:用户明示的"原型/不接业务" > 数据动词暗示的"真实接入"。
431
+
432
+ ## ⚠️ plugin_instance CREATE 失败的恢复路径(最高优先级)
433
+
434
+ **这是 CREATE-time 错误处理的最高优先级规则——优先于"速度优先 / 快速迭代 / 立刻给用户看效果"等任何主 prompt 通用约束。**
435
+
436
+ `plugin_instance` 工具 CREATE 返回平台**缺必填配置**类错误(如多维表格返回"请前往预览右边的插件配置页面配置多维表格插件并提交"、send-feishu-message 返回"缺少 receive_id"等),**严禁**:
437
+
438
+ - ❌ 静默回退到 `setTimeout` / `sessionStorage` / 硬编码 mock 实现
439
+ - ❌ 假装"配置已生效"继续写下游代码
440
+ - ❌ 把已知的必填配置值(用户在对话中提供的 AppToken / TableID / receive_id 等)丢弃
441
+
442
+ **必须**按以下 4 步处理:
443
+
444
+ 1. **原样转告** — 把平台返回的 actionable 错误信息**完整**呈现给用户(包括"请前往预览右边的插件配置页面..."这类操作指令)
445
+ 2. **等待用户在 UI 完成配置** — 用户配置完毕会通知 agent(典型用语:"刚刚更新了 PluginInstance 配置" / "已经配置好了")
446
+ 3. **重新调用** `plugin_instance(operType=UPDATE)` 拿到完整的 PluginInstance 配置
447
+ 4. **调** `get_plugin_ai_json` → 生成 `capabilityClient.load(id).call(...)` 或 `capabilityService.load(id).call(...)` 调用代码,**禁止跳过**
448
+
449
+ > **已知必填配置值的传入**:如果用户在对话中已经给出必填配置值(如飞书 base URL 含 AppToken/TableID),调用 `plugin_instance(CREATE)` 时**必须**把这些值**显式列入** `requirementsSummary` 字段,让平台 LLM 能解析并装配到 formValue。例:`requirementsSummary: "创建飞书多维表格写入实例,appToken=ZeHhbA4MxxxX, tableID=tblMQ9OgxxxW, 用于把 Excel 行批量插入"`,而不是只说"配置写入数据到指定的表格"。
450
+
419
451
  ---
420
452
 
421
453
  ## GetPluginInstanceAIJson 工具使用指南
@@ -461,7 +493,11 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
461
493
  ### 第一步:检查现有 PluginInstance(复用优先)
462
494
 
463
495
  1) 用户描述需求后,优先基于上下文提供的插件实例列表检索是否已存在可复用插件实例。
464
- 2) 若存在候选插件实例但你无法确认其是否满足需求,必须调用 `get_plugin_ai_json` 获取该插件实例的运行时投影(pluginInstance.ai.json),根据其中的 `actions[].key`、`actions[].inputSchema / outputSchema`、`actions[].outputMode` 判断是否可复用以及如何调用。
496
+ 2) 若存在候选插件实例但你无法确认其是否满足需求,必须调用 `get_plugin_ai_json` 获取该插件实例的运行时投影(pluginInstance.ai.json),根据其中的:
497
+ - `actions[].key`
498
+ - `actions[].inputSchema / outputSchema`
499
+ - `actions[].outputMode`
500
+ 来判断是否可复用以及如何调用。
465
501
  3) 禁止按旧链路去读取/维护 `server/capabilities/capabilities.json` 来做复用判断。
466
502
 
467
503
  > 结论:**复用判断以插件实例列表 + get_plugin_ai_json 为准**,禁止猜测 action、入参/出参、输出模式。
@@ -475,9 +511,8 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
475
511
  - 其他情况:告知用户该需求目前无法满足
476
512
 
477
513
  **强制约束**:
478
-
479
514
  - 创建/更新 PluginInstance **必须**通过 `plugin_instance` 工具完成,绝对禁止直接修改 `server/capabilities/` 下的配置文件。
480
- - UPDATE 场景严禁修改保护字段:`id / pluginKey / pluginVersion / createdAt`。
515
+ - UPDATE 场景严禁修改保护字段:`id / pluginKey / pluginVersion / createdAt `。
481
516
 
482
517
  ### 第三步:生成调用代码
483
518
 
@@ -541,7 +576,6 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
541
576
  用户会用「AI 生文」「AI 生图」「发送飞书消息」等**业务语言**描述需求;这些关键词必须被识别为**待使用或待创建的 PluginInstance**。
542
577
 
543
578
  开发流程:
544
-
545
579
  1. 收到需求后,先看可用的 PluginInstance 是否有可以直接使用的插件实例
546
580
  2. 若有候选但不确定是否满足,调用 `get_plugin_ai_json` 查看其 actions/schema/outputMode 再决策
547
581
  3. 若无,立即调用 `plugin_instance` 工具新建
@@ -558,6 +592,7 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
558
592
  | `ExecutionError` | 插件执行失败 | 记录日志 + 降级方案(如规则计算)+ 通知用户 |
559
593
  | `OutputValidationError` | 返回值不符合 schema | 记录异常返回 + 使用默认值或降级 |
560
594
  | 网络超时 | 请求超时 | 重试 + 超时后降级 |
595
+ | **CREATE-time 平台返回缺必填配置** | `plugin_instance(CREATE)` 返回"请前往预览右边的插件配置页面..."类错误 | 原样转告用户 + 等待用户在 UI 完成配置 + 用户通知后调 `plugin_instance(UPDATE)` 重试 + 调 `get_plugin_ai_json` → `capabilityClient` / `capabilityService` 真实调用;**严禁 mock fallback** |
561
596
 
562
597
  ### 必须遵守的规则
563
598
 
@@ -566,6 +601,23 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
566
601
  - 触发补偿机制(重试 / 降级 / 记录待处理列表)
567
602
  2. **异步操作必须有终态**: 如果插件调用是异步的(不阻塞主流程),必须在 DB 中维护状态(pending → success / failed),前端必须展示 failed 状态,不能永远停在 pending/loading。
568
603
  3. **通知类插件失败必须有补偿**: 如 `send-feishu-message` 失败,至少记录到"待发送"列表,或在 UI 中提示"通知发送失败,请手动联系"。
604
+ 4. **CREATE 失败严禁 mock fallback**: `plugin_instance(CREATE)` 失败时(特别是平台返回"缺必填配置"类错误)必须按上表 CREATE-time 行的 4 步处理,不接受 `setTimeout` / `sessionStorage` / 硬编码作为应急 fallback。
605
+
606
+ ### 插件配置完整性(load 前必查)
607
+
608
+ `capabilityClient.load(pluginInstanceId)` 前必须确认对应 PluginInstance 配置已存在(通过 `plugin_instance` 工具创建/更新,禁止手写 `server/capabilities/`),且 id 与代码中 `pluginInstanceId` 完全匹配;否则抛 `CapabilityNotFoundError`(开发态 Top 错误)。
609
+
610
+ ```typescript
611
+ // ✅ load/call 失败时停止后续请求,避免重复 load 放大错误(单应用可达每页 4-6 次失败)
612
+ try {
613
+ const result = await plugin.call('read_records', input);
614
+ } catch (err) {
615
+ if (err.name === 'CapabilityNotFoundError') { setConfigError(true); return; }
616
+ throw err;
617
+ }
618
+ ```
619
+
620
+ **缓存 load 结果**,同一 pluginInstanceId 不重复 load。
569
621
 
570
622
  ## 缓存与幂等性
571
623
 
@@ -595,7 +647,6 @@ PluginInstanceAIJson 的配置以 JSON 形式输出,例如:
595
647
  > **关键区分**:`formValue` 中配置固定值 ≠ 代码中硬编码。`formValue` 是插件实例的声明式配置,修改不需要改代码;而代码中硬编码的值散落在业务逻辑中,难以维护。
596
648
 
597
649
  当接收人/配置值是动态的,获取途径:
598
-
599
650
  1. 通过平台角色 API 获取(如"所有 admin_hr 角色的用户")
600
651
  2. 存入应用配置表,通过 API 读取
601
652
  3. 通过环境变量注入
@@ -58,9 +58,7 @@
58
58
  ### Client 侧调用方式(默认首选)
59
59
 
60
60
  #### 1. 调用前获取权威依据
61
-
62
61
  在为某个插件实例生成调用代码前,必须先通过 `get_plugin_ai_json` 工具获取该插件实例的运行时投影(plugin_Instance.ai.json),并以其中信息为准:
63
-
64
62
  - `actions[].key`:调用时要传的 `actionKey`
65
63
  - `actions[].inputSchema / outputSchema`:入参/出参结构
66
64
  - `actions[].outputMode`:`unary | stream`(决定调用与结果处理方式)
@@ -177,7 +175,7 @@ function readFirstStringField(
177
175
 
178
176
  **核心原则**:在插件设计阶段按「原子化拆解」拆分,避免单插件返回多字段 JSON。
179
177
 
180
- ##### 推荐:多插件并行流式
178
+ ##### 推荐:多插件并行流式
181
179
 
182
180
  适用于需求涉及多种输出(标题、正文、图片等),各输出相对独立。
183
181
 
@@ -352,7 +350,6 @@ this.somePluginInstanceSideEffect(input).catch(error => {
352
350
  | 任意(兜底场景) | Server 侧 | `capabilityService.load(id).call(actionKey, input)` |
353
351
 
354
352
  **选择原则**:
355
-
356
353
  - 不涉及持久化时,优先在 Client 侧直接调用
357
354
  - `outputMode = stream` 时,Client 侧使用 `callStream` 做渐进式渲染
358
355
  - 涉及持久化、触发器、敏感凭证、事务编排等场景时,使用 Server 侧
@@ -12,8 +12,8 @@
12
12
 
13
13
  在查看本插件的 Action 时,你将同时获得两部分信息:
14
14
 
15
- 1. **本文档 (README)**: 提供高级指引、业务逻辑、使用场景、重要约束。
16
- 2. **Action 的 `inputSchema` 和 `outputSchema`**: 提供精确的 JSON Schema 格式的输入/输出结构。
15
+ 1. **本文档 (README)**: 提供高级指引、业务逻辑、使用场景、重要约束。
16
+ 2. **Action 的 `inputSchema` 和 `outputSchema`**: 提供精确的 JSON Schema 格式的输入/输出结构。
17
17
 
18
18
  **请遵循以下原则:**
19
19
 
@@ -200,6 +200,7 @@ aggregateQuery 的 filter 格式与 searchRecords **完全相同**,遵循下
200
200
 
201
201
  ```typescript
202
202
  import { capabilityClient } from '@lark-apaas/client-toolkit';
203
+ import { logger } from '@lark-apaas/client-toolkit/logger';
203
204
 
204
205
  // 搜索记录
205
206
  const response = await capabilityClient.load('plugin_instance_id').call<{
@@ -219,7 +220,7 @@ const response = await capabilityClient.load('plugin_instance_id').call<{
219
220
  // ✅ 正确:response.records / response.hasMore / response.total / response.pageToken
220
221
  // ❌ 错误:response.data.records / response.data.output.records
221
222
  const { records, hasMore, total, pageToken } = response;
222
- console.log(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${hasMore}`);
223
+ logger.info(`共 ${total} 条,本页 ${records.length} 条,还有更多: ${hasMore}`);
223
224
 
224
225
  // 获取单条记录
225
226
  const detail = await capabilityClient.load('plugin_instance_id').call<{
@@ -282,7 +283,7 @@ const chartData =
282
283
 
283
284
  ## 代码实现注意事项
284
285
 
285
- 1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,先用 `console.log` 打印完整响应再编码,禁止凭猜测修改数据路径。
286
+ 1. **响应数据直接访问,禁止猜测嵌套路径**。`capabilityClient.call()` 返回的就是最终数据,直接用 `response.records`、`response.total` 等。**禁止**加 `.data`、`.output`、`.data.output` 等前缀。如果不确定响应结构,用 `logger.info('response', response)` 临时输出查看(或在浏览器开发者工具 Network 面板看)。
286
287
  2. 在进行写入操作时,比如确认提交时,应该有loading效果,禁止用户重复点击。
287
288
  3. 如果capabilityClient call失败,命中了error逻辑,要打印日志,同时把error message提示给用户。
288
289
  4. 在进行更新操作时,只更新需要更新的字段即可,不需要对全量字段进行更新。
@@ -464,7 +465,6 @@ const columns = [
464
465
  **2. Formula 字段不支持 filter 和 sum/avg 聚合**
465
466
 
466
467
  同一数据常有两个字段:原始 Number 字段(如 `金额`,单位元)和计算 Formula 字段(如 `金额(万元)`)。聚合和过滤**必须用 Number 字段**,在代码里做单位转换:
467
-
468
468
  ```typescript
469
469
  // ❌ 错误:金额(万元) 是 Formula,不能聚合也不能过滤
470
470
  { fieldName: '金额(万元)', aggregation: 'sum' }
@@ -480,7 +480,6 @@ const columns = [
480
480
  与 searchRecords 不同,aggregateQuery 中 `isNot` 会把字段值为空的记录也排除。如果很多记录的该字段为空,结果会远少于预期甚至为 0。
481
481
 
482
482
  解决方案:不在 filter 里用 `isNot` 排除 Text 值,改为在 dimensions 里加上该字段,在结果侧用 if 跳过不要的分组:
483
-
484
483
  ```typescript
485
484
  // ❌ 错误:isNot 会把「目前进展」为空的记录也排掉
486
485
  filter: { conditions: [{ fieldName: '目前进展', operator: 'isNot', value: ['已完成'] }] }
@@ -10,10 +10,10 @@ match-template-name: nestjs-react-fullstack
10
10
 
11
11
  ## ⚡️ 核心原则 (TL;DR)
12
12
 
13
- 1. **优先 React 19 新特性**: 用 `use()` 读取异步数据,用 `useActionState` 管理表单,替代繁琐的 `useEffect` + `useState`。
14
- 2. **拒绝冗余 State**: 能计算得到的变量(派生状态),绝不存入 State,直接计算 or `useMemo`。
15
- 3. **事件驱动 > Effects**: 用户交互(点击、提交)产生的逻辑写在事件处理函数中,`useEffect` 仅用于同步外部系统(订阅、DOM)。
16
- 4. **依赖诚实**: `useEffect/useCallback/useMemo` 的依赖数组必须包含所有引用的响应式变量,禁止欺骗 Linter。
13
+ 1. **优先 React 19 新特性**: 用 `use()` 读取异步数据,用 `useActionState` 管理表单,替代繁琐的 `useEffect` + `useState`。
14
+ 2. **拒绝冗余 State**: 能计算得到的变量(派生状态),绝不存入 State,直接计算 or `useMemo`。
15
+ 3. **事件驱动 > Effects**: 用户交互(点击、提交)产生的逻辑写在事件处理函数中,`useEffect` 仅用于同步外部系统(订阅、DOM)。
16
+ 4. **依赖诚实**: `useEffect/useCallback/useMemo` 的依赖数组必须包含所有引用的响应式变量,禁止欺骗 Linter。
17
17
 
18
18
  ---
19
19
 
@@ -114,5 +114,6 @@ return <div>{data.name}</div>;
114
114
  - [ ] **依赖数组**: `useEffect`, `useMemo`, `useCallback` 包含所有外部变量。
115
115
  - [ ] **清理工作**: `useEffect` 中是否清理了定时器/订阅?
116
116
  - [ ] **竞态处理**: 异步 Effect 是否处理了组件卸载或 id 变更的情况?(如 `ignore` 标志)。
117
+ - [ ] **autosave/防抖保存竞态**: ① 组件卸载时禁止强制保存当前 state(卸载触发的保存常用未初始化/空值覆盖已有数据);② id/资源切换时必须用 `key` 或 effect 重置 state,避免上一资源的脏 state 串写;③ **修一个模块的 autosave 竞态后,必须 grep 同构模块(其他 autosave/防抖保存)全部横展同一修复**——单模块局部修补是数据反复丢失复发的主因。
117
118
  - [ ] **引用稳定**: `Context` value 或自定义 Hook 返回的对象,是否做了 `useMemo` 缓存?
118
119
  - [ ] **React 19 升级**: 是否还在手动写 `loading` state?能否用 `useActionState` 或 `Suspense` 替代?
@@ -86,7 +86,6 @@ interface WebhookEvent {
86
86
  ```
87
87
 
88
88
  ### 指定值限制
89
-
90
89
  1. Webhook 触发器不可以设置指定值,并且告知用户。
91
90
 
92
91
  ### 代码示例