@lark-apaas/coding-steering 0.1.32-dev.b12eab4 → 0.1.32-dev.b6564ba

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 (35) hide show
  1. package/package.json +1 -1
  2. package/steering/design-html/skills/charts/SKILL.md +11 -4
  3. package/steering/design-html/skills/data-viz/SKILL.md +1 -1
  4. package/steering/design-html/skills/hi-fi-design/SKILL.md +1 -1
  5. package/steering/design-html/skills/interactive-prototype/SKILL.md +1 -17
  6. package/steering/design-html/skills/slide-deck/SKILL.md +5 -3
  7. package/steering/design-html/skills/social-card/SKILL.md +263 -0
  8. package/steering/nestjs-react-fullstack/skills/app-init-feasibility-guide/SKILL.md +3 -0
  9. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +1 -0
  10. package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +2 -0
  11. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +6 -0
  12. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +56 -14
  13. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +67 -4
  14. package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +40 -51
  15. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +75 -0
  16. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +10 -4
  17. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +57 -14
  18. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
  19. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
  20. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
  21. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +82 -0
  22. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
  23. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
  24. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +249 -0
  25. package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +3 -1
  26. package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +2 -0
  27. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +6 -0
  28. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +49 -22
  29. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +7 -3
  30. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +57 -14
  31. package/steering/vite-react/skills/plugin-guide/SKILL.md +1 -0
  32. package/steering/vite-react/skills/react-three-fiber/SKILL.md +1 -0
  33. package/steering/vite-react/tech.md +3 -0
  34. package/steering/design-html/skills/preflight/SKILL.md +0 -51
  35. package/steering/design-html/skills/preflight/scripts/probe.sh +0 -108
@@ -0,0 +1,249 @@
1
+ # 工具类写法
2
+
3
+ API 形状以 `node_modules/@lark-apaas/nestjs-mcp/dist/index.d.ts` 为准。下面是当前版本的写法与约束;字段名对不上时以 d.ts 为准。
4
+
5
+ ## 工具面规模
6
+
7
+ 工具面不是越全越好:每个工具的 schema 在消费方 Agent 的**每一轮**都占上下文,面越大它选错越多。
8
+
9
+ | 工具数 | 做法 |
10
+ |---|---|
11
+ | ≤ 15 | 一个动作一个工具 |
12
+ | 15–30 | 仍可用;把只有参数差别的近重复工具并掉 |
13
+ | > 30 | 收敛成「检索 + 执行」几个通用工具,高频的三五个再单独开 |
14
+
15
+ 用户没说清要开放哪些能力、给谁用时,先问清再写,不要把 Service 层能做的全铺出去。
16
+
17
+ ## 要不要界面
18
+
19
+ 结果要用户动手挑、看、或确认的工具才配界面;只是给模型继续推理的数据不配。要配的话,界面类型与写法见 `mcp-apps.md`。
20
+
21
+ ## 骨架
22
+
23
+ ```typescript
24
+ // server/mcp/tools/order.tools.ts
25
+ import { z } from 'zod';
26
+ import {
27
+ McpTools,
28
+ McpTool,
29
+ McpToolError,
30
+ type Infer,
31
+ type McpContext,
32
+ type McpToolResult,
33
+ } from '@lark-apaas/fullstack-nestjs-core';
34
+ import { OrderService } from '@server/modules/order/order.service';
35
+
36
+ // schema 提成常量:方法签名用 Infer<typeof …> 引用
37
+ export const SearchOrdersInput = {
38
+ keyword: z.string().optional().describe('客户名关键字,模糊匹配'),
39
+ limit: z.number().int().min(1).max(50).default(20).describe('单页条数'),
40
+ cursor: z.string().optional().describe('上一页返回的 nextCursor;首页不传'),
41
+ };
42
+ export const SearchOrdersOutput = {
43
+ items: z.array(
44
+ z.object({
45
+ id: z.string(),
46
+ customer: z.string(),
47
+ amount: z.number().describe('金额,单位元'),
48
+ status: z.enum(['pending', 'paid', 'cancelled']),
49
+ }),
50
+ ),
51
+ nextCursor: z.string().nullable().describe('还有下一页时非空,原样传回 cursor'),
52
+ };
53
+
54
+ export const CancelOrderInput = { orderId: z.string().describe('订单 ID,来自 order_search 的 items[].id') };
55
+ export const CancelOrderOutput = { orderId: z.string(), status: z.literal('cancelled') };
56
+
57
+ @McpTools({ prefix: 'order_' })
58
+ export class OrderMcpTools {
59
+ constructor(private readonly orders: OrderService) {}
60
+
61
+ @McpTool({
62
+ title: '查询订单',
63
+ description:
64
+ '按客户名关键字分页查询当前用户可见的订单。用户想找订单、看订单列表时调用;一页最多 50 条,超出用 cursor 翻页。',
65
+ inputSchema: SearchOrdersInput,
66
+ outputSchema: SearchOrdersOutput,
67
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
68
+ })
69
+ async search(
70
+ input: Infer<typeof SearchOrdersInput>,
71
+ ctx: McpContext,
72
+ ): Promise<McpToolResult<typeof SearchOrdersOutput>> {
73
+ const userId = ctx.user.userId!; // requireUser 默认开启,执行到这里必有值;取一次往下传
74
+ const page = await this.orders.search({ ...input, userId });
75
+ return {
76
+ // 按 outputSchema 挑字段,不把 Service 查出来的整行记录原样回传
77
+ structuredContent: {
78
+ items: page.items.map((o) => ({ id: o.id, customer: o.customer, amount: o.amount, status: o.status })),
79
+ nextCursor: page.nextCursor,
80
+ },
81
+ content: [{ type: 'text', text: `找到 ${page.items.length} 条订单` }],
82
+ };
83
+ }
84
+
85
+ @McpTool({
86
+ title: '取消订单',
87
+ description:
88
+ '取消一个待支付订单。仅在用户明确要求取消、且订单状态为 pending 时调用;订单不存在返回 ORDER_NOT_FOUND,状态不允许返回 ORDER_NOT_CANCELLABLE。重复取消同一订单返回同一结果。',
89
+ inputSchema: CancelOrderInput,
90
+ outputSchema: CancelOrderOutput,
91
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
92
+ })
93
+ async cancel(
94
+ input: Infer<typeof CancelOrderInput>,
95
+ ctx: McpContext,
96
+ ): Promise<McpToolResult<typeof CancelOrderOutput>> {
97
+ const userId = ctx.user.userId!;
98
+ const order = await this.orders.findVisibleTo(input.orderId, userId);
99
+ if (!order) throw new McpToolError('订单不存在,用 order_search 按客户名重查一个有效 ID', { code: 'ORDER_NOT_FOUND' });
100
+ if (order.status === 'cancelled') return { structuredContent: { orderId: order.id, status: 'cancelled' } };
101
+ if (order.status !== 'pending') {
102
+ throw new McpToolError(`订单状态为 ${order.status},只有 pending 能取消,不要重试`, { code: 'ORDER_NOT_CANCELLABLE' });
103
+ }
104
+ await this.orders.cancel(order.id, userId);
105
+ return { structuredContent: { orderId: order.id, status: 'cancelled' } };
106
+ }
107
+ }
108
+ ```
109
+
110
+ ```typescript
111
+ // server/modules/order/order.module.ts —— 工具类和它依赖的 Service 注册在同一个模块
112
+ import { Module } from '@nestjs/common';
113
+ // OrderController / OrderService / OrderMcpTools 的 import 略
114
+
115
+ @Module({
116
+ controllers: [OrderController],
117
+ providers: [OrderService, OrderMcpTools],
118
+ })
119
+ export class OrderModule {}
120
+ ```
121
+
122
+ `@McpTools()` 自带 `@Injectable()`。SDK 启动时用 DiscoveryService 扫所有 provider;类没进任何模块的 `providers` 就扫不到它,启动日志不会有任何提示,工具静默缺席——启动日志「已注册 N 个 MCP 工具:…」列出的名字才是真相,对不上就是没注册。工具类及其依赖必须是单例:request / transient 作用域会在启动时抛「MCP 工具必须使用单例及单例依赖」,整个应用起不来。单例意味着所有请求共用一个实例:当前用户、本次入参、分页游标只放方法内的局部变量,写进类字段或模块级变量会在并发请求间串号。
123
+
124
+ ## 装饰器选项
125
+
126
+ `@McpTool(options)`:
127
+
128
+ | 字段 | 说明 |
129
+ |---|---|
130
+ | `name?` | 默认取方法名;最终工具名 = `prefix + name`,需匹配 `^[A-Za-z0-9_.-]{1,128}$`,全应用唯一 |
131
+ | `title?` | 面向人的展示名,面板里显示 |
132
+ | `description` | 必填,面向模型 |
133
+ | `inputSchema?` / `outputSchema?` | zod:原始形状 `{ a: z.string() }` 或 `z.object({...})` 都行;顶层必须是对象 |
134
+ | `annotations?` | `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`,四项都写 |
135
+ | `ui?` | `{ resourceUri: 'ui://…', visibility?: ['model' \| 'app'] }`,见 `mcp-apps.md` |
136
+ | `meta?` | 额外 `_meta`,与 `ui` 生成的字段合并 |
137
+
138
+ `@McpTools({ prefix? })`:给类下所有工具名加前缀,如 `order_`。
139
+
140
+ ## 方法签名与返回值
141
+
142
+ - 固定 `(input, ctx)`。无入参的工具 `input` 为 `Record<string, never>`,仍要写第二个参数 `ctx`。
143
+ - 声明了 `outputSchema`:返回 `{ structuredContent }`,类型由 schema 推导;`content` 可选,SDK 会自动补一份 JSON 文本给不支持结构化输出的客户端。加一句业务摘要的 `content` 让文本客户端也能读。
144
+ - 未声明 `outputSchema`:返回 `{ content: [{ type: 'text', text }] }`;`content` 还支持 `image` / `audio` / `resource_link` 块。
145
+ - `isError: true` 由 SDK 在捕获 `McpToolError` 时设置,不需要手动返回。
146
+
147
+ ## 返回体积
148
+
149
+ SDK 不截断,返回多少消费方 Agent 就吃多少,超了是它的上下文被挤掉。`mcp_inspect` 里看到的截断与落盘是调试通道的显示预算,与工具该返回多少无关。
150
+
151
+ - `structuredContent` 只放对方推理或界面渲染要用的字段。Service 查出来的整行记录、外键、审计字段先挑一遍再返回。
152
+ - 列表工具 `limit` 默认不超过 20、上限写进 zod(`.max(50)`);要更多靠 `cursor` 翻页,不靠调大 `limit`。
153
+ - 结果仍可能很大时自己截断,把截断标记和出路一起返回,让对方知道怎么缩小范围。两个字段都要写进 `outputSchema`,否则与声明对不上;位置放在数据字段**前面**,按尾部截断时先没的才是数组尾巴而不是这两个字段:
154
+
155
+ ```typescript
156
+ return {
157
+ structuredContent: {
158
+ truncated: true,
159
+ hint: '命中 847 条,只返回前 20 条;加 status 过滤或换更具体的关键字',
160
+ items: page.items,
161
+ },
162
+ content: [{ type: 'text', text: '命中 847 条,返回前 20 条' }],
163
+ };
164
+ ```
165
+
166
+ - 图片、附件、长文本用 `content` 里的 `resource_link` 给指针,不内联进结果。`mcp_inspect` 只渲染文本块,这类块在调试结果里不显示,别据此以为工具什么都没返回。
167
+
168
+ ## McpContext
169
+
170
+ | 字段 | 用途 |
171
+ |---|---|
172
+ | `user` | 与 REST 里的 `req.userContext` 同源:`userId` / `tenantId` / `roles` / `env`(`preview` \| `runtime`)等,字段全部可选(`userId?: string`)。默认 `requireUser: true`,缺 `userId` 时 SDK 直接返回 `MCP_USER_REQUIRED`、方法不执行,所以方法体开头 `const userId = ctx.user.userId!;` 取一次往下传;不要为迁就类型去改 Service 签名 |
173
+ | `request` | 原始 HTTP 请求,读 header、logid |
174
+ | `signal` | 客户端取消时触发,长任务把它传给下游 |
175
+ | `requestId` | MCP 请求 id |
176
+ | `extra` | SDK 的 `RequestHandlerExtra`(`sessionId` / `requestId` / `authInfo` 等)。它有 `sendNotification`,但端点是无状态 JSON 模式,`notifications/progress` 发不到客户端——长任务别靠进度通知续命,拆成「发起 + 查状态」两个工具 |
177
+
178
+ 工具执行中途要不到用户输入:SDK 没有 elicitation,无状态 JSON 模式下服务端发往客户端的请求也出不去(与进度通知同因)。需要用户确认或补参数时,把它做成入参交给模型去问,或者做确认型界面(见 `mcp-apps.md`),不要设计成「工具跑一半停下来等人」。
179
+
180
+ ## 错误处理
181
+
182
+ - 可预期的业务失败抛 `new McpToolError(message, { code?, data? })`:SDK 转成 `isError: true`,`message` / `code` / `data` 原样进 Agent 上下文。`code` 用 `SCREAMING_SNAKE`,并在 `description` 里预告,Agent 才知道怎么应对。
183
+ - `message` 写成「哪里错了 + 下一步做什么」:「订单不存在,用 order_search 按客户名重查一个有效 ID」。错误消息必达,应用 Skill 里的处理说明要消费方主动取 Prompt 才看得到,出路别只写在那边。
184
+ - 其他异常:SDK 记日志、返回通用失败提示,Agent 看不到原因。调试时看到「通用失败提示」就去读 Trace-ID 日志。
185
+ - 入参校验交给 zod,方法体不重复判空;zod 失败由 SDK 返回,方法不执行。
186
+
187
+ ## description 怎么写
188
+
189
+ 消费方 Agent 只靠 `description` 与字段 `.describe()` 决定调不调、怎么填。写全五件事:用途、何时调用(含前置条件)、参数从哪来(「来自 order_search 的 items[].id」)、返回什么、失败会返回哪些 code。写操作再加一句幂等语义(「重复取消返回同一结果」)。
190
+
191
+ 字段描述写单位、格式、取值来源;枚举用 `z.enum`;入参不用 `z.any()` / `z.unknown()`。
192
+
193
+ ## 开通(首次)
194
+
195
+ 1. 前置检查通过(见 SKILL.md)。
196
+ 2. 建 `server/mcp/tools/<域>.tools.ts`,至少一个只读工具复用现有 Service。
197
+ 3. 把工具类加进所属业务模块的 `providers`。
198
+ 4. 写 `server/mcp/skills/<name>/SKILL.md`(下节)。
199
+ 5. 跑 `lark-cli apps +mcp-key-create --app-id "$app_id"` 备好运行态凭证。
200
+ 6. 进 `debugging.md` 的调试回路。
201
+
202
+ ## 应用 Skill:`server/mcp/skills/<name>/SKILL.md`
203
+
204
+ 给消费方 Agent 的使用说明。一个业务流程一个目录,目录里只认固定文件名 `SKILL.md`。SDK 把每份注册为同名 MCP Prompt:`prompts/list` 给 `name` / `description`,`prompts/get` 返回去掉 frontmatter 的正文(一条 `role: user` 文本消息)。它不是 Resource、没有 `skill://` URI,`resources/list` 里看不到它是正常的。每次请求重新读文件,改完不用重启。
205
+
206
+ frontmatter 规则。SDK 每次请求都会校验,不合法时整个 MCP 请求 500,不会静默跳过这一份:
207
+
208
+ - `name` 必填且等于目录名:1–64 位小写字母、数字与单个连字符,不能首尾连字符、不能连续 `--`。
209
+ - `description` 必填非空,不超过 1024 字符;消费方只靠它决定要不要取这份 Prompt,写清适用场景。
210
+ - 全应用不重名;最多 100 份,单文件不超过 1 MiB,合计不超过 8 MiB;必须是工程内普通文件,不接受符号链接。
211
+ - 只有 `SKILL.md` 会被读取和交付,同目录的图片、附件不会。
212
+
213
+ 正文必含:适用场景、实际工具名与关键参数、调用顺序、需要用户确认的边界、身份与权限要求、每个错误 code 的处理、界面 URI(如有)。只写已实现的能力;不写凭证、用户数据、内部实现。
214
+
215
+ 示例 `server/mcp/skills/orders/SKILL.md`。`---` 必须是文件第一行,前面不能有任何字符,注释也不行:
216
+
217
+ ```markdown
218
+ ---
219
+ name: orders
220
+ description: 查询当前用户可见的订单,并在用户明确要求后取消待支付订单。
221
+ ---
222
+ # 订单能力
223
+ - 需要已登录用户身份;只返回当前用户可见的订单。
224
+ - 找订单:order_search,keyword 模糊匹配客户名;nextCursor 非空时用 cursor 翻页。
225
+ - 取消订单:只有用户明确要求取消、且 status 为 pending 时调用 order_cancel;orderId 取自 order_search 的 items[].id。
226
+ - ORDER_NOT_FOUND:请用户核对订单 ID。ORDER_NOT_CANCELLABLE:告知当前状态,不重试。
227
+ - 支持界面的宿主会用 ui://order/list 展示查询结果。
228
+ ```
229
+
230
+ ## 定义校验
231
+
232
+ 工具名格式与重名、资源 URI 的 `ui://` 前缀与重名、`ui.resourceUri` 能否找到对应的 `@McpUiResource()`——这些在应用启动时统一校验,失败直接阻断启动并打印「MCP 定义校验失败」,逐条列出问题与出错的类和方法。一个方法不能同时标 `@McpTool()` 与 `@McpUiResource()`。改完等 dev server 重启看日志即可,没有单独的离线校验命令。
233
+
234
+ ## 模块配置
235
+
236
+ ```typescript
237
+ PlatformModule.forRoot({
238
+ mcp: {
239
+ serverName: 'travel-approval', // 首次开通 MCP 时按用途设置,别留缺省
240
+ serverVersion: '1.0.0',
241
+ instructions: '…', // initialize 响应里的说明
242
+ },
243
+ });
244
+ // 关闭端点:PlatformModule.forRoot({ mcp: false })
245
+ ```
246
+
247
+ **`serverName` 首次开通时就设上。** `initialize` 把它返回给连上来的外部 Agent,`mcp_inspect` 的 `list_tools` 也显示它;不设则是 `miaoda-app`,谁看都不知道是哪个应用。取材于本 MCP 做什么(差旅审批 → `travel-approval`,库存查询 → `inventory`),小写字母 + 数字 + `-`/`_`,两三个词以内。
248
+
249
+ 其余项一般不动。`requireUser` 保持默认 `true`。零工具、零资源、无应用 Skill 时端点返回 404,这是正常状态不是故障。
@@ -93,12 +93,14 @@ match-template-name: nestjs-react-fullstack
93
93
 
94
94
  > 完整的解析→守卫流程见 `authn-guide` skill 第二节"认证流程"。
95
95
 
96
+ 类型使用规则:Controller 直接用 `@Req() req: Request` 读取 SDK 增强的 `req.userContext`。测试、mock 或局部业务需要窄类型时,定义独立接口,只声明实际读取字段;不要继承 Express `Request`,也不要重声明平台增强的 `userContext`。未读取的 `tenantId` 省略;必须保留时用 `unknown` 或 SDK 兼容类型。
97
+
96
98
  ### 字段表
97
99
 
98
100
  | 字段 | 类型 | 说明 |
99
101
  |------|------|------|
100
102
  | `userId` | `string` | 妙搭用户 ID |
101
- | `tenantId` | `number` | 租户 ID |
103
+ | `tenantId` | `number \| undefined` | 租户 ID;按 SDK Express `Request.userContext` 增强:可选 number |
102
104
  | `appId` | `string` | 应用 ID |
103
105
  | `loginUrl` | `string` | 登录跳转 URL |
104
106
  | `userType` | `string` | 用户类型(如 `_employee`) |
@@ -14,6 +14,8 @@ match-template-name: nestjs-react-fullstack
14
14
 
15
15
  > **边界说明**:`authClient.session` 仅用于用户登录/登出/获取用户信息等鉴权操作。**插件调用(capability)不属于账户 SDK**,须使用独立的 `capabilityClient`(参见 plugin-guide)。
16
16
  >
17
+ > **本地 dev 限制**:本 skill 的能力底层打 `/app/<appId>/__runtime__/api/v1/account/*`。这类端点只有在 `npm run dev:local` 成功 `env pull` 到 `MIAODA_DEV_PLATFORM_BASE`(或 `SANDBOX_PUBLIC_URL`)时才由 dev-proxy 反代到远端沙箱;**env pull 失败或未装 lark-cli 时该反代不挂**(启动脚本只 warn 不中断),表现为 POST 404 / GET 返回 200 + index.html。`useCurrentUserProfile()` 打的是 `account/login/user`;UserSelect / DepartmentSelect / UserDisplay 打 `search_user` / `list_users` / `user_profile` / `search_department`。先确认 `.env.local` 里有 `MIAODA_DEV_PLATFORM_BASE`;确实缺失时禁止改 `business-ui` 组件或换自研控件绕过,改由服务端出数据(`req.userContext` / `AuthNPaasService`)。
18
+ >
17
19
  > **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
18
20
 
19
21
  ## 怎么选(决策指引)
@@ -116,6 +116,12 @@ Module '"@client/src/api/gen"' has no exported member named 'DiscrepancyResponse
116
116
  - [ ] 已根据实际定义修正代码
117
117
  - [ ] 已验证修改后类型使用正确
118
118
 
119
+ ## 业务日期错位
120
+
121
+ **问题描述**:本地凌晨、周一或月初附近,日期落到前一天、上周或上月。
122
+
123
+ **解决步骤**:按 `coding-guide` 的「业务日期」一节改——自然日统一走业务时区 helper;精确时刻不动。
124
+
119
125
  ## JSX 和语法错误
120
126
 
121
127
  ### 特殊字符转义处理
@@ -129,7 +129,7 @@ shared/ # 前后端共享的目录
129
129
 
130
130
  1. 优先使用项目已有依赖,仅在无法实现时安装新依赖
131
131
  2. 使用前先查看 `package.json` 确保依赖已存在
132
- 3. **子包完整性检查**:部分库有多个子包(如 `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`),添加 import 后必须确认 package.json 中包含所有需要的子包
132
+ 3. **子包完整性检查**:部分库有多个子包(如 `@radix-ui/react-*` 系列每个组件都是独立子包),添加 import 后必须确认 package.json 中包含所有需要的子包
133
133
  4. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
134
134
 
135
135
  ## 文件命名约定
@@ -147,6 +147,39 @@ shared/ # 前后端共享的目录
147
147
  - `/api` 和 `/openapi` 代理已配置,前端 TS 严格模式已禁用,后端已启用
148
148
  - Rspack 用于构建,已配置好,**禁止修改**
149
149
 
150
+ ### 平台 runtime 接口(`__runtime__`)在本地要靠 env pull 才通
151
+
152
+ dev server 自己只代理 `/api`、`/openapi`、`/__innerapi__`。`/app/<appId>/__runtime__/*` 这一整类平台 runtime 接口走的是另一条路:`npm run dev:local` 会先跑 `lark-cli apps +env-pull` 把沙箱身份/凭证拉进 `.env.local`,其中的 `MIAODA_DEV_PLATFORM_BASE`(或 legacy `SANDBOX_PUBLIC_URL`)才让 dev-proxy 把 `__runtime__` 反代到远端沙箱。
153
+
154
+ **env pull 失败、未装 lark-cli、或 `.spark/meta.json` 缺 app_id 时,启动脚本只 warn 不中断**,反代就不会挂上。此时这些端点的症状是:**POST 返回 404;GET 落到 Vite 的 SPA fallback,返回 200 + `index.html`**(业务侧表现为「返回一坨 HTML,JSON 解析失败」)。
155
+
156
+ 常踩到的端点:
157
+
158
+ | 端点 | 方法 | 谁会打到它 |
159
+ |------|------|-----------|
160
+ | `api/v1/permissions/roles` | GET | `AppContainer` 每次加载应用自动打 |
161
+ | `api/v1/observability/{logs,traces,metrics}/collect`、`current_server_timestamp` | POST/GET | 平台埋点上报,自动打 |
162
+ | `api/v1/account/login/user` | POST | `useCurrentUserProfile()` → `authClient.session.getUserInfo()` |
163
+ | `api/v1/account/search_user`、`list_users`、`user_profile`、`search_department`、`convert_lark_user` | POST / `user_profile` 是 GET | `business-ui` 的 UserSelect / DepartmentSelect / UserDisplay |
164
+ | `api/v1/account/search`、`api/v1/account/chat/list_chats` | POST | ChatSelect 选群组件 |
165
+ | `api/v1/studio/user/profile` | GET | `getUserProfile()` |
166
+ | `api/v1/storage/object/<bucket>/pre_upload` | POST | `dataloom.storage.uploadFile()` 前端上传 |
167
+ | `api/v1/studio/plugins/tmp_files/acquire_upload_url`、`acquire_download_url` | POST | `capabilityClient` 的文件类入参 |
168
+
169
+ > `useCurrentUserProfile()` 打的是 `account/login/user`,**不是** `user_profile`/`search_user` 那一排;它另外还打一个 `GET /api/authnpaas/lark-user-id`,那条走 `/api`、被代理、正常。
170
+
171
+ 排查顺序:先看 `.env.local` 里有没有 `MIAODA_DEV_PLATFORM_BASE`,没有就重跑 `env pull`。确认拉不到时:
172
+
173
+ - **禁止**改 `client/src/components/business-ui/**`(平台保护文件)、**禁止**给组件加兜底请求或改用自研选人控件绕过——发布后这些端点是正常的,绕过写法反而是错的
174
+ - 需要验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。服务端的 `req.userContext` 和 `AuthNPaasService` 由网关注入,不受此限制(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 长期停在加载态就是这个原因
175
+ - 需要验证上传链路:改用接口测试直接打后端接口,或复用库里已有的文件 URL
176
+
177
+ 另外,`UserSelect` 弹层是**搜索驱动**的:不输关键词时列表为空属预期行为,不是接口挂了。
178
+
179
+ ### 服务端启动即崩、`/api` 全部 502
180
+
181
+ 报错形如 `UnknownDependenciesException: Nest can't resolve dependencies of the XxxService (?)... argument Function at index [0]` 时,先查构造函数有没有空参 `@Inject()`(见 plugin-guide),不要先去改 Module 的 `imports` / `providers`。
182
+
150
183
  ## 质量保障流程
151
184
 
152
185
  修改代码后:
@@ -286,6 +319,12 @@ await db.select().from(users).where(eq(users.adminUser, userId));
286
319
 
287
320
  - **`customTimestamptz` 跨网络后是 string**:service 内查询返回 `Date`,但 API 响应经 JSON 序列化后前端拿到 ISO string。`shared/api.interface.ts` 中 timestamptz 字段声明为 `string`,前端需要 Date 时手动 `new Date(value)`。
288
321
 
322
+ ### 业务日期
323
+
324
+ 需求里的日期是**自然日**(哪一天 / 哪一周 / 哪一月)而不是精确时刻时,按业务时区计算,未指定时默认 `Asia/Shanghai`。
325
+
326
+ 项目里放一个 helper 统一产出 `day` / `month` / `weekStart` / `shiftDay`;服务端写入、统计聚合、前端展示都比较它的返回值。
327
+
289
328
  ### 数据库使用强约束
290
329
 
291
330
  - 仅通过 schema.ts 暴露的客户端和类型读写,禁止手写 SQL/临时类型
@@ -329,10 +368,13 @@ await db.select().from(users).where(eq(users.adminUser, userId));
329
368
 
330
369
  - **必须从 `req.userContext` 获取**,禁止从前端传递,禁止硬编码
331
370
  - 依赖用户信息的接口不要用 API 测试工具测试,用 `think` 工具思考
371
+ - 后端按 `user-identity` 和 `@lark-apaas/fullstack-nestjs-core` 的 Express `Request` 增强读取 `req.userContext`;`tenantId` 为可选 number,不能套用前端 `window.tenantId` / `viewContext` 字符串语义。
372
+ - Controller 直接用 `@Req() req: Request`;测试、mock 或业务窄类型用独立接口,只声明实际读取字段。不要继承 Express `Request` 或重声明 `userContext`;未读 `tenantId` 就省略,必须保留时用 `unknown` 或 SDK 兼容类型。
332
373
 
333
374
  ```typescript
334
- // req.userContext 字段(由身份认证中间件挂载):
335
- // - userId/tenantId/appId: string
375
+ // req.userContext 字段(由身份认证中间件挂载,完整合同见 user-identity):
376
+ // - userId/appId: string
377
+ // - tenantId: SDK Request.userContext 的可选 number
336
378
  // - env: 'preview' | 'runtime' (preview=预览态, runtime=发布运行态)
337
379
  // - userName/userNameEn: string
338
380
  // - userNameI18n: string (多语名字, 如 {zh_cn: '用户', en_us: 'user'})
@@ -443,14 +485,13 @@ NestJS 自己不读 env。直连 NestJS 端口 → header 缺失 → `req.userCo
443
485
 
444
486
  - **框架**: React 19 + TypeScript
445
487
  - **路由**: React Router DOM v6
446
- - **样式**: styled-jsx + tailwindcss(语义化 token)。styled-jsx 使用前提见下方"样式开发"
488
+ - **样式**: tailwindcss(语义化 token)
447
489
  - **UI 组件库**: shadcn/ui — Use components for functionality, heavily style them
448
490
  - **图表**: ReactECharts,**开发前必须调用 `/charts-skill`**
449
491
  - **图标**: Lucide React(唯一图标库,禁止 Emoji 和其他图标库)
450
492
  - **表格/表单/图表**: 见下方"组件 Skill 召回规则",开发前必须先调用对应 Skill
451
493
  - **用户**: 用户信息展示/选择必须用 `business-ui` 组件(阅读 README.md),禁止直接展示 userId
452
- - **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
453
- - **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
494
+ - **Markdown 渲染**: `components/ui/markdown`(react-markdown + remark-gfm,内置 prose 排版)
454
495
 
455
496
  ## API 请求
456
497
 
@@ -483,8 +524,7 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
483
524
  | --------------------------------------------------- | --------------------------------------- | --------------------------------- |
484
525
  | Table | `@lark-apaas/client-toolkit/antd-table` | 数据表格,**先调 `/table-skill`** |
485
526
  | UserSelect/UserDisplay/UserProfile/DepartmentSelect | `business-ui/*` | 用户/部门选择展示 |
486
- | TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
487
- | Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
527
+ | Markdown | `components/ui/markdown` | Markdown 渲染(react-markdown + remark-gfm) |
488
528
 
489
529
  ### 组件 Skill 召回规则(强制执行)
490
530
 
@@ -535,7 +575,6 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
535
575
  ```
536
576
  需要写样式?
537
577
  ├─ 基础布局/间距/颜色 → Tailwind ✅
538
- ├─ 复杂动画/伪元素/高级CSS → styled-jsx ✅
539
578
  └─ JS动态计算值 → 行内 style ✅
540
579
  ```
541
580
 
@@ -548,11 +587,6 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
548
587
  - **arbitrary values 中空格用下划线**:`from-[hsl(215_60%_18%)]` 非 `from-[hsl(215 60% 18%)]`
549
588
  - `tailwind-theme.css` 自定义属性用 `hsl(H, S%, L%)` 格式(非 `23 10% 23%`)
550
589
 
551
- ### styled-jsx 规范
552
-
553
- - **技术栈一致性**:仅在已配置 styled-jsx 插件的项目中使用。`package.json` 无 `styled-jsx` 依赖则**禁用**,否则运行时 SyntaxError
554
- - **禁止动态插值**:`<style jsx>` 内禁止 `${...}` 等表达式(会卡死)。动态值放 CSS 变量,用 `var(--xxx)` 引用
555
-
556
590
  ### 布局/排版
557
591
 
558
592
  - Spacing 保持一致(small/medium/large 三级),Panel 风格统一
@@ -635,15 +669,8 @@ return <h1>{data?.title || '未知标题'}</h1>;
635
669
  | 验证 | zod |
636
670
  | 工具函数 | lodash |
637
671
  | 样式 | clsx |
638
- | Excel | xlsx(**仅前端实现,禁止服务端实现**。解析后将结构化数据传到服务端保存) |
639
- | PDF 导出 | jspdf + html2canvas(**仅前端实现,禁止服务端实现**) |
640
- | 文件上传 | react-dropzone |
641
- | 二维码 | qrcode.react |
642
672
  | 用户反馈 | sonner |
643
- | 拖拽 | @dnd-kit/core |
644
- | 数字动画 | react-countup |
645
- | Base64 | js-base64 — `import { encode, decode } from 'js-base64'` |
646
- | 3D 场景 | cobe |
673
+
647
674
 
648
675
  ## 滚动分页最佳实践
649
676
 
@@ -35,9 +35,10 @@ lark-cli apps --help 2>&1 | grep -q '+plugin-install' && echo "READY" || echo "M
35
35
  | Client 侧非流式调用 | `capabilityClient.load(id).call(actionKey, input)` |
36
36
  | Client 侧流式调用 | `capabilityClient.load(id).callStream(actionKey, input)` |
37
37
  | Server 侧调用(仅兜底) | `capabilityService.load(id).call(actionKey, input)` |
38
+ | 长耗时 AI 结果 | 大体量/多字段/多份/多语言/文件或多模态串联等结构信号命中时,优先前端 `callStream` 渐进展示;需保存则流式结束后复用 CRUD 落库;仅在 Client 侧无法满足时使用后端任务记录 + 状态查询 + 结果读取;禁止单个 HTTP 请求等待完整结果后才返回 |
38
39
  | capabilityClient 导入 | `import { capabilityClient } from '@lark-apaas/client-toolkit'` |
39
40
  | CapabilityService 导入 | `import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';` |
40
- | CapabilityService 注入 | `@Inject() private readonly capabilityService: CapabilityService` |
41
+ | CapabilityService 注入 | `@Inject(CapabilityService) private readonly capabilityService: CapabilityService`(**禁止空参 `@Inject()`**,会导致启动崩溃 502;业务 Module 无需 imports) |
41
42
  | 插件实例配置位置 | `server/capabilities/<instance_id>.json`(默认全栈应用)。见「配置目录」段 |
42
43
 
43
44
  > **capabilityClient 导入警告**:`capabilityClient` 是从 `@lark-apaas/client-toolkit` 直接导入的独立对象,**不是**从 `getDataloom()` 上获取的。正确且唯一的方式:`import { capabilityClient } from '@lark-apaas/client-toolkit'`。
@@ -516,7 +517,9 @@ npx @lark-apaas/miaoda-cli plugin list --id <instance_id>
516
517
  2. 结果供后续功能消费
517
518
  3. 用户再次访问时需要看到结果
518
519
 
519
- **推荐**:Server 侧 Service 调用插件 + 同一方法落库。**备选**:Client 侧调用 → 流式结束后调已有 CRUD 接口保存。
520
+ **推荐**:能由前端承接时,Client 侧 `callStream` 渐进展示,流式结束后调已有 CRUD 接口保存。**必要时**:敏感凭证、触发器、强事务或必须由后端保证落库一致性时,Server 侧 Service 调用插件并落库。
521
+
522
+ **高耗时 AI capability 闸门**:若插件输出定义或功能设计显示输出规模大、输出字段多、需要多份结果、多语言/长文本、文件或多模态输入后继续生成、多个 capability 串联、或结果需后续查看/落库,禁止把完整生成放进一个同步 HTTP 请求等待;能由前端承接时优先 `callStream` 渐进展示,需保存则流式结束后复用 CRUD 落库;仅在 Client 侧无法满足时后端创建任务记录后快速返回、由前端短轮询状态/结果,或拆成多个独立小调用。
520
523
 
521
524
  ### 生成代码
522
525
 
@@ -535,7 +538,8 @@ npx @lark-apaas/miaoda-cli plugin list --id <instance_id>
535
538
 
536
539
  1. **禁止静默吞异常**:每个 `catch` 块必须向用户展示错误或触发补偿
537
540
  2. **异步操作必须有终态**:DB 中维护状态(pending → success / failed)
538
- 3. **插件失败必须有补偿**:至少记录到待处理列表或提示用户重试
541
+ 3. **长耗时 AI 生成不得同步等完**:大体量、多字段、多份、多语言、文件/多模态串联或需要持久化的 AI capability 调用,触发接口必须快速返回任务状态或采用前端流式展示;若接口请求、浏览器操作或页面请求出现超时/连接断开,不能只凭服务端最终日志或数据库最终写入宣告成功,必须证明用户侧存在可读取的终态结果
542
+ 4. **插件失败必须有补偿**:至少记录到待处理列表或提示用户重试
539
543
 
540
544
  ## 缓存与幂等性
541
545
 
@@ -2,15 +2,19 @@
2
2
 
3
3
  ### 核心原则:根据场景选择调用侧
4
4
 
5
- **默认优先在 Client 侧调用 capabilityClient;但当插件结果需要持久化到数据库时,应考虑在 Server 侧调用或确保前端调用后及时通过后端接口保存。**
5
+ **默认优先在 Client 侧调用 capabilityClient;流式或高耗时 AI 生成优先 `callStream` 渐进展示。结果需要持久化时,优先在流式结束后通过已有后端接口保存;只有 Client 侧无法满足时才切到 Server 侧。**
6
+
7
+ **高耗时 AI capability 先判调用形态**:只要输出规模大、输出字段多、需要多份结果、多语言/长文本、文件或多模态输入后继续生成、多个 capability 串联、或结果需后续查看/落库,就不得把完整 AI 结果塞进一个同步 HTTP 请求等待。能由前端承接时,优先用前端 `callStream` 渐进展示,并在流式结束后按需复用已有 CRUD 接口保存结果;只有 Client 侧无法满足(触发器/敏感凭证/强事务/必须由后端保证落库一致性等)时,才采用后端任务记录 + 状态/结果查询,避免把后台任务作为默认方案。
8
+
6
9
  **严禁** import { capabilityClient } from '@lark-apaas/client-capability'。
7
10
  **唯一指定**的导入方式是 import { capabilityClient } from '@lark-apaas/client-toolkit';
8
11
 
9
12
  | 优先级 | 场景 | 调用方式 |
10
13
  |-------|------|---------|
11
- | **首选** | 绝大多数场景 | `capabilityClient.load(id).call()` |
12
- | **首选** | 流式输出场景 | `capabilityClient.load(id).callStream()` |
13
- | **兜底** | Client 侧无法满足时 | `CapabilityService.load(id).call()` |
14
+ | **首选** | 绝大多数即时展示场景 | `capabilityClient.load(id).call()` |
15
+ | **首选** | 流式输出,或高耗时但可由前端承接的 AI 生成 | `capabilityClient.load(id).callStream()`(流式结束后按需持久化) |
16
+ | **必要时** | 高耗时且 Client 侧无法满足:触发器、敏感凭证、强事务、必须由后端保证落库一致性 | 后端任务记录 + 后台调用 `CapabilityService.load(id).call()` + 前端短轮询状态/结果 |
17
+ | **兜底** | Client 侧无法满足且单次调用可在交互边界内完成 | `CapabilityService.load(id).call()` |
14
18
 
15
19
  #### 什么情况下应使用 Server 侧?
16
20
 
@@ -19,7 +23,7 @@
19
23
  1. **涉及敏感凭证**:调用需要服务端私密 token/secret,不适合暴露给前端
20
24
  2. **必须后端编排**:多个插件调用之间有强事务依赖,需要后端统一编排
21
25
  3. **触发器/定时任务场景**:没有前端上下文,只能由后端发起
22
- 4. **插件结果需要持久化**:调用结果需要保存到数据库,在 Server 侧可以在同一方法中完成调用+落库,保证数据一致性
26
+ 4. **插件结果需要持久化**:调用结果需要保存到数据库;能由前端承接时,优先前端 `callStream` 渐进展示并在结束后复用已有 CRUD 接口保存;只有 Client 侧无法满足或必须由后端保证落库一致性时,才在 Server 侧调用并落库。若此时输出规模大、字段多、结果多份或需多步能力串联,必须改为任务记录 + 后台执行 + 状态/结果查询,避免一个 HTTP 请求等待完整生成
23
27
 
24
28
  > **提示**:如果插件结果不需要存储、仅用于即时展示(如流式生成文本、发送消息),优先在前端调用。但当结果需要保存到数据库时,不要回避使用 Server 侧。
25
29
 
@@ -31,9 +35,13 @@
31
35
 
32
36
  ```
33
37
  插件结果是否需要持久化到数据库?
34
- ├── 否 → Client 侧调用(默认)
35
- └── 是 → 推荐方案 A:Server 侧调用,在 Service 中调用插件并在同一方法中落库
36
- 备选方案 B:Client 侧调用插件 → 成功后通过已有 CRUD 接口保存结果
38
+ ├── 否
39
+ │ ├── `outputMode=stream` 或内容生成较慢 → Client 侧 `callStream()` 渐进展示
40
+ │ └── 单次短输出 → Client 侧 `call()`(默认)
41
+ └── 是
42
+ ├── `outputMode=stream` 且前端可承接 → 推荐方案 A:Client 侧 `callStream()` 渐进展示,成功后通过已有 CRUD 接口保存结果
43
+ ├── 输出较小且可在交互边界内完成、且必须由后端保证一致性 → Server 侧调用并在同一方法中落库
44
+ └── Client 侧无法满足且输出规模大/多字段/多份/多语言/多步骤 → 方案 B:Server 侧创建任务记录,快速返回任务状态;后台调用插件并落库;前端短轮询状态/结果
37
45
  ```
38
46
 
39
47
  | 应避免的做法 | 推荐做法 |
@@ -68,12 +76,18 @@
68
76
  #### call / callStream 函数签名
69
77
 
70
78
  ```typescript
71
- .call(actionKey: string, input: object) // 非流式,返回 Promise<output>
72
- .callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
79
+ // 前端 capabilityClient(@lark-apaas/client-toolkit,实际类型来自 @lark-apaas/client-capability)
80
+ .call<T = unknown>(actionKey: string, params?: Record<string, unknown>): Promise<T> // 非流式
81
+ .callStream<T = unknown>(actionKey: string, params?: Record<string, unknown>): AsyncIterable<T> // 流式
73
82
  ```
74
83
 
75
84
  - **第一个参数 `actionKey`**:必须是字符串,值来自 `get_plugin_ai_json` 返回的 `actions[].key`(如 `'sendFeishuMessage'`、`'textGenerate'`)
76
- - **第二个参数 `input`**:必须是对象,结构符合 `actions[].inputSchema`
85
+ - **第二个参数 `params`**:类型是 `Record<string, unknown>`,结构符合 `actions[].inputSchema`
86
+ - 上面是**前端**签名,带泛型。**服务端 `CapabilityService` 的 `call` / `callStream` / `callStreamWithEvents` 都没有泛型**,两者不通用,详见「Server 侧调用方式」
87
+
88
+ > **`Record<string, unknown>` 不接 `interface` 声明的对象**(interface 没有隐式索引签名),直接传会撞
89
+ > `Argument of type 'XxxInput' is not assignable to parameter of type 'Record<string, unknown>'. Index signature for type 'string' is missing in type 'XxxInput'.`
90
+ > 三种正确写法:① 直接传内联对象字面量;② 入参类型用 `type XxxInput = { ... }` 而非 `interface`(type 别名有隐式索引签名);③ 已有 interface 时在调用点显式 `input as unknown as Record<string, unknown>`。**不要为此改用 `as any`。**
77
91
 
78
92
  ```typescript
79
93
  // ❌ 错误:把参数 JSON.stringify 后当作 actionKey
@@ -233,10 +247,11 @@ function MultiPluginStreamExample() {
233
247
  2. 一个 `stream` action 的真实调用结果(chunk 按 `outputSchema` 字段读取)
234
248
  3. 调用失败时的最小日志字段齐全
235
249
  4. 若无法执行真实调用,必须明确写明阻塞原因,禁止直接标记"开发完成"
250
+ 5. 若用户触发 AI 生成的请求超时、连接断开或工具返回超时,必须判定为验收失败;只有触发请求快速返回、随后能通过页面或接口读到明确完成或失败终态和结果,才允许标记通过
236
251
 
237
252
  ### Server 侧调用方式(仅兜底场景)
238
253
 
239
- > 以下场景适合使用 Server 侧调用,特别是涉及数据持久化时不要回避后端。
254
+ > 以下场景适合使用 Server 侧调用;若前端 `callStream` + 既有 CRUD 保存即可满足展示和持久化,不要优先引入后端后台任务。
240
255
 
241
256
  #### 1. 何时适合用 Server 侧?
242
257
 
@@ -247,6 +262,7 @@ function MultiPluginStreamExample() {
247
262
  | 敏感凭证调用 | 凭证不能暴露给前端 | 调用需要 admin token 的 API |
248
263
  | 强事务编排 | 多步骤需要原子性 | 创建记录 → 发通知 → 更新状态必须全成功或全回滚 |
249
264
  | 插件结果需持久化 | 调用结果需保存到数据库 | AI 分类/摘要结果需落库、文档解析的结构化数据需入库、图片识别结果需关联业务记录、语音转文字结果需存档等 |
265
+ | 高耗时 AI 生成需后续查看 | 请求不能长期占用用户交互链路 | 批量、多版本、多语言、长文本、多字段结构化生成,或文件/多模态分析后再生成内容 |
250
266
 
251
267
  #### 2. NestJS 注入方式
252
268
 
@@ -258,23 +274,49 @@ import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
258
274
  export class XxxService {
259
275
  private readonly logger = new Logger(XxxService.name);
260
276
 
277
+ // 注入 token 必须显式写成 @Inject(CapabilityService)
261
278
  constructor(
262
- @Inject() private readonly capabilityService: CapabilityService,
279
+ @Inject(CapabilityService) private readonly capabilityService: CapabilityService,
263
280
  ) {}
264
281
  }
265
282
  ```
266
283
 
284
+ **禁止空参 `@Inject()`**。本 stack 的 `nest-cli.json` 用 `builder: swc`,空参 `@Inject()` 会把注入 token 置为 undefined,Nest 回退读 `design:type` 拿到 `Function`,服务端启动即崩溃、所有 `/api` 请求返回 502:
285
+
286
+ ```
287
+ UnknownDependenciesException: Nest can't resolve dependencies of the XxxService (?).
288
+ Please make sure that the argument Function at index [0] is available in the XxxModule context.
289
+ ```
290
+
291
+ 见到这段报错先查构造函数有没有空参 `@Inject()`,不要去改 Module 的 `imports` / `providers`。
292
+
293
+ **业务 Module 不需要注册 CapabilityModule**。`PlatformModule` 是 `@Global()` 且已把 `CapabilityModule` 放进 `exports`,`CapabilityService` 全局可注入。不要在业务 Module 里写 `imports: [CapabilityModule.forRoot(...)]` 或 `imports: [PlatformModule.forRoot()]`。
294
+
295
+ 同一规则适用于从 `@lark-apaas/fullstack-nestjs-core` 注入的其他平台服务(`AuthNPaasService`、`FileService` 等)。
296
+
267
297
  #### 3. 调用示例
268
298
 
299
+ 服务端 `CapabilityExecutor` 的**三个方法全都没有泛型参数**(`@lark-apaas/nestjs-capability`):
300
+
301
+ ```typescript
302
+ call(actionName: string, input: unknown, context?: Partial<PluginActionContext>): Promise<unknown>;
303
+ callStream(actionName: string, input: unknown, context?): AsyncIterable<unknown>;
304
+ callStreamWithEvents(actionName: string, input: unknown, context?): AsyncIterable<StreamEvent<unknown>>;
305
+ ```
306
+
307
+ 写 `.call<T>(...)` / `.callStream<T>(...)` / `.callStreamWithEvents<T>(...)` 都会报「应有 0 个类型参数,但获得 1 个」。泛型只存在于前端 `capabilityClient`。注意服务端 `input` 是**必传**(没有 `?`),且类型是 `unknown` 而非前端的 `Record<string, unknown>`。服务端接 `unknown` 后在运行时收窄,禁止 `as any`:
308
+
269
309
  ```typescript
270
310
  const inputParams = {
271
311
  // 严格按 get_plugin_ai_json.actions[].inputSchema 构造
272
312
  };
273
313
 
274
314
  try {
275
- const output = await this.capabilityService
315
+ const output: unknown = await this.capabilityService
276
316
  .load('')
277
317
  .call('', inputParams);
318
+ // 运行时收窄后再取字段,例如:
319
+ // if (typeof output === 'object' && output !== null && 'content' in output) { ... }
278
320
  return output;
279
321
  } catch (error) {
280
322
  this.logger.error('pluginInstance call failed', {
@@ -290,6 +332,7 @@ try {
290
332
 
291
333
  - PluginInstance 调用在 Server 侧通常属于 **外部依赖 / side-effect**
292
334
  - 除非业务明确要求强一致性,**默认不应阻塞主业务流程**
335
+ - 已选择 Server 侧承接的高耗时 AI capability 必须有可观测状态:创建任务时记录处理进度、完成终态、失败终态、输入摘要、错误信息和结果引用;触发接口只返回任务标识与当前状态,前端通过短轮询读取进度和最终结果
293
336
 
294
337
  推荐写法:异步触发 + catch 兜底:
295
338
 
@@ -3,6 +3,7 @@ name: plugin-guide
3
3
  description: "Use when 需要:(1) 创建或管理 PluginInstance 插件实例,(2) 调用 capabilityClient 生成插件调用代码,(3) 理解 Plugin、PluginInstance、PluginInstanceAIJson 三层关系,(4) 使用 get_plugin_ai_json 或 plugin_instance 工具。触发词:插件, plugin, 飞书消息, 飞书群组, 多维表格, AI生文, AI生图, 图片理解, capabilityClient, pluginInstance"
4
4
  steering: true
5
5
  steering-topic: plugin_guide
6
+ match-template-name: vite-react
6
7
  ---
7
8
 
8
9
  # Plugin 集成指南