@lark-apaas/coding-steering 0.1.6-alpha.8 → 0.1.6-beta.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 (55) hide show
  1. package/package.json +1 -1
  2. package/steering/design-stack/skills/.gitkeep +0 -0
  3. package/steering/nestjs-react-fullstack/skills/authz-guide/SKILL.md +174 -0
  4. package/steering/nestjs-react-fullstack/skills/authz-guide/references/dynamic-permission-guide.md +627 -0
  5. package/steering/nestjs-react-fullstack/skills/authz-guide/references/management-page-spec.md +508 -0
  6. package/steering/nestjs-react-fullstack/skills/authz-guide/references/runtime-role-controller-spec.md +203 -0
  7. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-examples.md +90 -0
  8. package/steering/nestjs-react-fullstack/skills/authz-guide/references/sdk-types.md +216 -0
  9. package/steering/nestjs-react-fullstack/skills/client-add-aily-web-chat/SKILL.md +1 -1
  10. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +431 -0
  11. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +39 -127
  12. package/steering/nestjs-react-fullstack/skills/devops-guide/SKILL.md +119 -0
  13. package/steering/nestjs-react-fullstack/skills/feishu/SKILL.md +0 -1
  14. package/steering/nestjs-react-fullstack/skills/feishu/references/approval.md +1 -1
  15. package/steering/nestjs-react-fullstack/skills/feishu/references/attendance.md +1 -1
  16. package/steering/nestjs-react-fullstack/skills/feishu/references/bitable.md +3 -1
  17. package/steering/nestjs-react-fullstack/skills/feishu/references/calendar.md +1 -1
  18. package/steering/nestjs-react-fullstack/skills/feishu/references/contacts.md +1 -1
  19. package/steering/nestjs-react-fullstack/skills/feishu/references/doc.md +2 -1
  20. package/steering/nestjs-react-fullstack/skills/feishu/references/drive.md +2 -1
  21. package/steering/nestjs-react-fullstack/skills/feishu/references/events.md +2 -1
  22. package/steering/nestjs-react-fullstack/skills/feishu/references/id-convert.md +2 -2
  23. package/steering/nestjs-react-fullstack/skills/feishu/references/messaging.md +1 -1
  24. package/steering/nestjs-react-fullstack/skills/feishu/references/oauth.md +2 -1
  25. package/steering/nestjs-react-fullstack/skills/feishu/references/perm.md +2 -1
  26. package/steering/nestjs-react-fullstack/skills/feishu/references/wiki.md +3 -2
  27. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +1 -0
  28. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +601 -0
  29. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +360 -0
  30. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/table.md +515 -0
  31. package/steering/nestjs-react-fullstack/skills/react-hook-best-practices/SKILL.md +118 -0
  32. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +177 -0
  33. package/steering/nestjs-react-fullstack/skills/trigger-guide/SKILL.md +1 -0
  34. package/steering/nestjs-react-fullstack/skills/user-identity/SKILL.md +1 -0
  35. package/steering/nestjs-react-fullstack/skills/user-management-best-practices/SKILL.md +142 -0
  36. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +42 -28
  37. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +37 -149
  38. package/steering/design-stack/skills/client-add-aily-web-chat/SKILL.md +0 -139
  39. package/steering/design-stack/skills/client-builtins-user-service/SKILL.md +0 -628
  40. package/steering/design-stack/skills/code-fix/SKILL.md +0 -246
  41. package/steering/design-stack/skills/feishu/SKILL.md +0 -270
  42. package/steering/design-stack/skills/feishu/references/approval.md +0 -214
  43. package/steering/design-stack/skills/feishu/references/attendance.md +0 -163
  44. package/steering/design-stack/skills/feishu/references/bitable.md +0 -309
  45. package/steering/design-stack/skills/feishu/references/calendar.md +0 -190
  46. package/steering/design-stack/skills/feishu/references/contacts.md +0 -160
  47. package/steering/design-stack/skills/feishu/references/doc.md +0 -256
  48. package/steering/design-stack/skills/feishu/references/drive.md +0 -103
  49. package/steering/design-stack/skills/feishu/references/events.md +0 -198
  50. package/steering/design-stack/skills/feishu/references/id-convert.md +0 -128
  51. package/steering/design-stack/skills/feishu/references/messaging.md +0 -207
  52. package/steering/design-stack/skills/feishu/references/oauth.md +0 -164
  53. package/steering/design-stack/skills/feishu/references/perm.md +0 -90
  54. package/steering/design-stack/skills/feishu/references/wiki.md +0 -164
  55. package/steering/design-stack/skills/user-identity/SKILL.md +0 -300
@@ -0,0 +1,360 @@
1
+ ## PluginInstance 代码编写指南
2
+
3
+ ### 核心原则:根据场景选择调用侧
4
+
5
+ **默认优先在 Client 侧调用 capabilityClient;但当插件结果需要持久化到数据库时,应考虑在 Server 侧调用或确保前端调用后及时通过后端接口保存。**
6
+ **严禁** import { capabilityClient } from '@lark-apaas/client-capability'。
7
+ **唯一指定**的导入方式是 import { capabilityClient } from '@lark-apaas/client-toolkit';
8
+
9
+ | 优先级 | 场景 | 调用方式 |
10
+ |-------|------|---------|
11
+ | **首选** | 绝大多数场景 | `capabilityClient.load(id).call()` |
12
+ | **首选** | 流式输出场景 | `capabilityClient.load(id).callStream()` |
13
+ | **兜底** | Client 侧无法满足时 | `CapabilityService.load(id).call()` |
14
+
15
+ #### 什么情况下应使用 Server 侧?
16
+
17
+ 以下场景适合在 Server 侧调用:
18
+
19
+ 1. **涉及敏感凭证**:调用需要服务端私密 token/secret,不适合暴露给前端
20
+ 2. **必须后端编排**:多个插件调用之间有强事务依赖,需要后端统一编排
21
+ 3. **触发器/定时任务场景**:没有前端上下文,只能由后端发起
22
+ 4. **插件结果需要持久化**:调用结果需要保存到数据库,在 Server 侧可以在同一方法中完成调用+落库,保证数据一致性
23
+
24
+ > **提示**:如果插件结果不需要存储、仅用于即时展示(如流式生成文本、发送消息),优先在前端调用。但当结果需要保存到数据库时,不要回避使用 Server 侧。
25
+
26
+ ---
27
+
28
+ ### 插件结果持久化决策
29
+
30
+ #### 何时必须持久化(强制规则)
31
+
32
+ 以下任一条件成立时,插件结果**必须**保存到数据库,不能只存在前端 state 中:
33
+
34
+ 1. **结果会在其他页面展示**:如列表页展示 AI 摘要、详情页展示分类标签
35
+ 2. **结果供后续功能消费**:如 AI 分析结果用于生成报表、AI 分类结果用于筛选过滤
36
+ 3. **用户再次访问时需要看到结果**:如刷新页面后结果不应消失
37
+ 4. **结果对应数据库中已有字段**:如表中有 `summary`、`tags`、`analysis_result` 等 AI 产出字段
38
+
39
+ 仅当插件结果**纯粹用于一次性即时展示**(如聊天式对话、临时预览)时,才可以不持久化。
40
+
41
+ #### 持久化方案选择
42
+
43
+ ```
44
+ 插件结果是否需要持久化到数据库?
45
+ ├── 否(一次性即时展示)→ Client 侧调用(默认)
46
+ └── 是 → 推荐方案 A:Server 侧调用,在 Service 中调用插件并在同一方法中落库
47
+ 备选方案 B:Client 侧调用插件 → 成功后通过已有 CRUD 接口保存结果
48
+ ```
49
+
50
+ | 应避免的做法 | 推荐做法 |
51
+ |------------|---------|
52
+ | 前端调用插件后不保存结果,导致数据丢失 | 插件调用成功后及时持久化 |
53
+ | 为保存插件结果单独新建 API(如 `PATCH /api/xxx/ai-analysis`) | 优先复用已有的 create/update 接口,扩展字段即可 |
54
+ | 仅在前端 state 中暂存插件结果,不写入数据库 | 通过后端接口保存到数据库 |
55
+
56
+ ---
57
+
58
+ ### Client 侧调用方式(默认首选)
59
+
60
+ #### 1. 调用前获取权威依据
61
+
62
+ 在为某个插件实例生成调用代码前,必须先通过 `get_plugin_ai_json` 工具获取该插件实例的运行时投影(plugin_Instance.ai.json),并以其中信息为准:
63
+
64
+ - `actions[].key`:调用时要传的 `actionKey`
65
+ - `actions[].inputSchema / outputSchema`:入参/出参结构
66
+ - `actions[].outputMode`:`unary | stream`(决定调用与结果处理方式)
67
+
68
+ **编码前闸门(必须)**:先产出 Schema 摘录卡,再开始代码编辑。
69
+
70
+ ```markdown
71
+ [Schema 摘录卡]
72
+ - pluginInstanceId / actionKey / outputMode
73
+ - input.required / output.fields / readme.constraints
74
+ - 调用侧决策: Client | Server
75
+ ```
76
+
77
+ 若摘录卡字段缺失,不得进入实现阶段。
78
+
79
+ #### call / callStream 函数签名
80
+
81
+ ```typescript
82
+ .call(actionKey: string, input: object) // 非流式,返回 Promise<output>
83
+ .callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
84
+ ```
85
+
86
+ - **第一个参数 `actionKey`**:必须是字符串,值来自 `get_plugin_ai_json` 返回的 `actions[].key`(如 `'sendFeishuMessage'`、`'textGenerate'`)
87
+ - **第二个参数 `input`**:必须是对象,结构符合 `actions[].inputSchema`
88
+
89
+ ```typescript
90
+ // ❌ 错误:把参数 JSON.stringify 后当作 actionKey
91
+ plugin.call(JSON.stringify({ meeting_title: '...' }));
92
+ // ❌ 错误:漏掉 actionKey,直接传参数对象
93
+ plugin.call({ meeting_title: '...' });
94
+
95
+ // ✅ 正确:第一个参数是 actionKey 字符串,第二个参数是 input 对象
96
+ plugin.call('send_feishu_message', { meeting_title: '...' });
97
+ ```
98
+
99
+ #### 2. 非流式调用(outputMode = "unary")
100
+
101
+ ```typescript
102
+ import { capabilityClient } from '@lark-apaas/client-toolkit';
103
+ import { logger } from "@lark-apaas/client-toolkit/logger";
104
+
105
+ const result = await capabilityClient
106
+ .load('create_feishu_group')
107
+ .call('createGroup', {
108
+ group_name: '项目讨论群',
109
+ members: ['user_001', 'user_002'],
110
+ });
111
+
112
+ logger.info(result);
113
+ ```
114
+
115
+ #### 3. 流式调用(outputMode = "stream")
116
+
117
+ ##### 流式 chunk 字段速查表(必须遵守)
118
+
119
+ capabilityClient `callStream` 返回的每个 chunk 是**扁平对象**,字段名与 `outputSchema` 一致。**禁止使用 `chunk.data?.text`、`chunk.choices[0]`、`chunk.message` 等非 capabilityClient 格式。**
120
+
121
+ | 插件 | chunk 字段 | 正确写法 | 错误写法 |
122
+ |------|-----------|---------|---------|
123
+ | ai-text-generate | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
124
+ | ai-translate | `translation` | `chunk.translation` | ~~`chunk.content`~~ ~~`chunk.text`~~ |
125
+ | ai-text-summary | `summary` | `chunk.summary` | ~~`chunk.content`~~ ~~`chunk.data?.text`~~ |
126
+ | ai-image-understanding | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
127
+ | ai-search-summary | `content` | `chunk.content` | ~~`chunk.data?.text`~~ |
128
+
129
+ > **⚠️ 同一应用多个页面调用同一插件时,所有页面必须使用一致的 chunk 字段名。** 常见错误:PageA 正确用 `chunk.content`,PageB 错误用 `chunk.data?.text`。parallel_write 并发生成多页面时尤其注意。
130
+
131
+ ##### 必须处理返回形态差异(重点)
132
+
133
+ `callStream()` 可能返回 `AsyncIterable<chunk>` 或 `{ output: AsyncIterable<chunk> }`,必须先归一化。
134
+
135
+ ```typescript
136
+ type AnyRecord = Record<string, unknown>;
137
+
138
+ function isAsyncIterable(value: unknown): value is AsyncIterable<AnyRecord> {
139
+ return !!value && typeof (value as AnyRecord)[Symbol.asyncIterator] === 'function';
140
+ }
141
+
142
+ function normalizeStream(resultOrStream: unknown): AsyncIterable<AnyRecord> {
143
+ if (isAsyncIterable(resultOrStream)) {
144
+ return resultOrStream;
145
+ }
146
+ if (
147
+ resultOrStream &&
148
+ typeof resultOrStream === 'object' &&
149
+ 'output' in (resultOrStream as AnyRecord) &&
150
+ isAsyncIterable((resultOrStream as AnyRecord).output)
151
+ ) {
152
+ return (resultOrStream as AnyRecord).output as AsyncIterable<AnyRecord>;
153
+ }
154
+ throw new Error('Invalid callStream result: cannot find AsyncIterable stream');
155
+ }
156
+
157
+ function readFirstStringField(
158
+ chunk: AnyRecord,
159
+ keys: string[],
160
+ ): string {
161
+ for (const key of keys) {
162
+ const value = chunk[key];
163
+ if (typeof value === 'string') {
164
+ return value;
165
+ }
166
+ }
167
+ return '';
168
+ }
169
+ ```
170
+
171
+ ##### 场景判断与推荐方案
172
+
173
+ | 场景 | 特征 | 推荐度 |
174
+ |-----|------|-------|
175
+ | **多插件并行流式** | 多个插件各返回单一输出,并行调用 | **优先推荐** |
176
+ | **单插件 JSON 流式解析** | 单插件返回结构化 JSON,需边接收边解析 | ⚠️ 仅在必要时 |
177
+
178
+ **核心原则**:在插件设计阶段按「原子化拆解」拆分,避免单插件返回多字段 JSON。
179
+
180
+ ##### 推荐:多插件并行流式
181
+
182
+ 适用于需求涉及多种输出(标题、正文、图片等),各输出相对独立。
183
+
184
+ ```tsx
185
+ import { logger } from "@lark-apaas/client-toolkit/logger";
186
+
187
+ function MultiPluginStreamExample({ recordId }: { recordId: string }) {
188
+ const [title, setTitle] = useState('');
189
+ const [content, setContent] = useState('');
190
+ const [coverUrl, setCoverUrl] = useState('');
191
+
192
+ const handleGenerate = async (keywords: string) => {
193
+ // 1. 封面图(非流式,异步不阻塞)
194
+ capabilityClient
195
+ .load('cover_generator')
196
+ .call<{ images: string[] }>('textToImage', { keywords })
197
+ .then(res => res?.images?.[0] && setCoverUrl(res.images[0]))
198
+ .catch(err => logger.warn('封面生成失败', err));
199
+
200
+ // 2. 标题(非流式)
201
+ const titleResult = await capabilityClient
202
+ .load('title_generator')
203
+ .call<{ content: string }>('textGenerate', { keywords });
204
+ const generatedTitle = titleResult?.content || '';
205
+ setTitle(generatedTitle);
206
+
207
+ // 3. 正文(流式,边生成边展示)
208
+ const streamResult = capabilityClient
209
+ .load('content_generator')
210
+ .callStream<{ content: string }>('textGenerate', { keywords });
211
+ const contentStream = normalizeStream(streamResult);
212
+
213
+ // 🎯 按 outputSchema 字段提取,禁止把 chunk 当字符串
214
+ let fullContent = '';
215
+ for await (const chunk of contentStream) {
216
+ const delta = readFirstStringField(
217
+ chunk as Record<string, unknown>,
218
+ ['content'], // 必须来自 get_plugin_ai_json.actions[].outputSchema
219
+ );
220
+ if (delta) {
221
+ fullContent += delta;
222
+ setContent(fullContent);
223
+ }
224
+ }
225
+
226
+ // 4. ⚠️ 流式完成后,必须将结果持久化到数据库
227
+ // 复用已有 CRUD 接口,不要单独建 API
228
+ await updateRecord(recordId, {
229
+ title: generatedTitle,
230
+ content: fullContent,
231
+ coverUrl,
232
+ });
233
+ };
234
+
235
+ return (/* 各字段独立渲染 */);
236
+ }
237
+ ```
238
+
239
+ > **⚠️ 前端流式调用的持久化义务**:前端 `callStream` 完成后,如果结果需要持久化(参见上方"何时必须持久化"),**必须在流式结束后调用已有的后端 CRUD 接口保存结果**。仅存在 `useState` 中的数据会在页面刷新后丢失。
240
+
241
+ **优点**:代码简洁、各插件独立、某个失败不影响其他。
242
+
243
+ ##### ⚠️ 兜底:单插件 JSON 流式解析
244
+
245
+ 当无法拆分为多插件时,需处理不完整 JSON 的逐字符到达,需实现 `parseStreamingStringField` 和 `extractJsonObject` 工具函数。**强烈建议在插件设计阶段拆分为多插件并行流式调用,避免此场景。**
246
+
247
+ ---
248
+
249
+ ### 失败日志最小集(必须)
250
+
251
+ 失败日志至少包含以下字段:
252
+
253
+ ```typescript
254
+ {
255
+ pluginInstanceId: string,
256
+ actionKey: string,
257
+ outputMode: 'unary' | 'stream',
258
+ inputKeys: string[],
259
+ resultType?: string,
260
+ resultKeys?: string[],
261
+ firstChunkKeys?: string[],
262
+ error: string
263
+ }
264
+ ```
265
+
266
+ ### 改后冒烟验证清单(必须)
267
+
268
+ 完成调用代码后,最少执行并记录:
269
+
270
+ 1. 一个 `unary` action 的真实调用结果(字段按 `outputSchema` 读取)
271
+ 2. 一个 `stream` action 的真实调用结果(chunk 按 `outputSchema` 字段读取)
272
+ 3. 调用失败时的最小日志字段齐全
273
+ 4. 若无法执行真实调用,必须明确写明阻塞原因,禁止直接标记“开发完成”
274
+
275
+ ### Server 侧调用方式(仅兜底场景)
276
+
277
+ > 以下场景适合使用 Server 侧调用,特别是涉及数据持久化时不要回避后端。
278
+
279
+ #### 1. 何时适合用 Server 侧?
280
+
281
+ | 场景 | 原因 | 示例 |
282
+ |------|------|------|
283
+ | 触发器/Webhook | 无前端上下文 | 数据变更时自动发送通知 |
284
+ | 定时任务 | 无前端上下文 | 每日定时生成报告 |
285
+ | 敏感凭证调用 | 凭证不能暴露给前端 | 调用需要 admin token 的 API |
286
+ | 强事务编排 | 多步骤需要原子性 | 创建记录 → 发通知 → 更新状态必须全成功或全回滚 |
287
+ | 插件结果需持久化 | 调用结果需保存到数据库 | AI 分类/摘要结果需落库、文档解析的结构化数据需入库、图片识别结果需关联业务记录、语音转文字结果需存档等 |
288
+
289
+ #### 2. NestJS 注入方式
290
+
291
+ ```typescript
292
+ import { Injectable, Inject, Logger } from '@nestjs/common';
293
+ import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
294
+
295
+ @Injectable()
296
+ export class XxxService {
297
+ private readonly logger = new Logger(XxxService.name);
298
+
299
+ constructor(
300
+ @Inject() private readonly capabilityService: CapabilityService,
301
+ ) {}
302
+ }
303
+ ```
304
+
305
+ #### 3. 调用示例
306
+
307
+ ```typescript
308
+ const inputParams = {
309
+ // 严格按 get_plugin_ai_json.actions[].inputSchema 构造
310
+ };
311
+
312
+ try {
313
+ const output = await this.capabilityService
314
+ .load('')
315
+ .call('', inputParams);
316
+ return output;
317
+ } catch (error) {
318
+ this.logger.error('pluginInstance call failed', {
319
+ pluginInstanceId: '',
320
+ actionKey: '',
321
+ error: error instanceof Error ? error.message : 'Unknown error',
322
+ });
323
+ throw error;
324
+ }
325
+ ```
326
+
327
+ #### 4. Server 侧编排与容错原则
328
+
329
+ - PluginInstance 调用在 Server 侧通常属于 **外部依赖 / side-effect**
330
+ - 除非业务明确要求强一致性,**默认不应阻塞主业务流程**
331
+
332
+ 推荐写法:异步触发 + catch 兜底:
333
+
334
+ ```typescript
335
+ this.somePluginInstanceSideEffect(input).catch(error => {
336
+ this.logger.warn('PluginInstance side-effect failed, ignored', {
337
+ error: error instanceof Error ? error.message : 'Unknown error'
338
+ });
339
+ });
340
+ ```
341
+
342
+ ---
343
+
344
+ ### outputMode 与调用侧选择
345
+
346
+ 先通过 `get_plugin_ai_json(pluginInstanceId)` 获取 `actions[].outputMode`:
347
+
348
+ | outputMode | 推荐调用侧 | 调用方式 |
349
+ |------------|-----------|---------|
350
+ | `unary` | **Client 侧优先** | `capabilityClient.load(id).call(actionKey, input)` |
351
+ | `stream` | **Client 侧优先** | `capabilityClient.load(id).callStream(actionKey, input)` |
352
+ | 任意(兜底场景) | Server 侧 | `capabilityService.load(id).call(actionKey, input)` |
353
+
354
+ **选择原则**:
355
+
356
+ - 不涉及持久化时,优先在 Client 侧直接调用
357
+ - `outputMode = stream` 时,Client 侧使用 `callStream` 做渐进式渲染
358
+ - 涉及持久化、触发器、敏感凭证、事务编排等场景时,使用 Server 侧
359
+
360
+ ---