@lark-apaas/coding-steering 0.1.51 → 0.1.52-alpha.20260914045123
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.
- package/package.json +6 -6
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +0 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-user-service/SKILL.md +0 -2
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +10 -39
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +0 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +5 -37
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +261 -0
- package/steering/nestjs-react-fullstack/skills_local/client-builtins-user-service/SKILL.md +0 -2
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +13 -37
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +1 -1
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +5 -37
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/coding-steering",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52-alpha.20260914045123",
|
|
4
4
|
"description": "Stack-specific steering content for miaoda-coding templates",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"steering"
|
|
8
8
|
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
11
|
+
},
|
|
9
12
|
"devDependencies": {
|
|
10
13
|
"markdownlint-cli": "^0.47.0"
|
|
11
14
|
},
|
|
@@ -17,8 +20,5 @@
|
|
|
17
20
|
"miaoda",
|
|
18
21
|
"coding-steering"
|
|
19
22
|
],
|
|
20
|
-
"license": "MIT"
|
|
21
|
-
|
|
22
|
-
"lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
|
|
23
|
-
}
|
|
24
|
-
}
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md
CHANGED
|
@@ -21,7 +21,6 @@ match-template-name: nestjs-react-fullstack
|
|
|
21
21
|
## 使用注意
|
|
22
22
|
- **入口边界**:本 SDK 是**应用前端代码**读写应用存储的唯一入口。Agent 自己在对话 / 开发中上传、灌数据或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准——二者操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进页面代码**,页面里一律用本 SDK。
|
|
23
23
|
- @lark-apaas/client-toolkit/dataloom 这个SDK只适用于前端调用,禁止在服务端调用
|
|
24
|
-
- **沙箱 dev 限制**:`uploadFile` 打的 `/app/<appId>/__runtime__/api/v1/storage/object/<bucket>/pre_upload` **在沙箱 dev 下恒 404**,只有发布态可用。这是环境限制不是代码缺陷,按本 skill 写法即为正确,不要为它改代码;开发期要验证上传链路改走接口测试直接打后端接口,或复用库里已有的文件 URL。详见 coding-guide「沙箱 dev 不提供平台 runtime 接口」。
|
|
25
24
|
- 上传成功后,最重要的返回值是 `data.download_url`。需要将此URL保存到你的业务数据库中
|
|
26
25
|
- **⚠️ 场景区分(重要)**:`dataloom.storage` 仅适用于需要持久化存储文件或获取 `download_url` 保存到数据库的场景。如果文件仅作为插件输入(传给 `capabilityClient`),**必须直接传 File/Blob 对象,禁止先走 dataloom 上传再传 URL**;插件调用(capability)不属于 dataloom,详见 plugin-guide
|
|
27
26
|
- **download_url 格式说明**:`download_url` 返回的可能是相对路径(如 `/spark/app/.../storage/object/...`),这是正常行为。**禁止**在前面拼接 `window.location.origin` 或其他域名前缀,平台会自动解析相对路径。直接使用原始值即可。
|
|
@@ -14,8 +14,6 @@ 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/*`,这类端点**在沙箱 dev 下打不通**(POST 404;GET 落 SPA fallback 返回 200 + index.html),只有发布态可用。`useCurrentUserProfile()` 打的是 `account/login/user`;UserSelect / DepartmentSelect / UserDisplay 打 `search_user` / `list_users` / `user_profile` / `search_department`。看到 404 或选人列表为空时,禁止改 `business-ui` 组件、禁止加兜底请求或换自研控件绕过;开发期要验证「按当前用户过滤 / 展示姓名」改由服务端出数据(`req.userContext` / `AuthNPaasService`)。详见 coding-guide「沙箱 dev 不提供平台 runtime 接口」。
|
|
18
|
-
>
|
|
19
17
|
> **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
|
|
20
18
|
|
|
21
19
|
## 怎么选(决策指引)
|
|
@@ -155,7 +155,7 @@ shared/ # 前后端共享的目录
|
|
|
155
155
|
|
|
156
156
|
## 依赖使用规范
|
|
157
157
|
|
|
158
|
-
1. **子包完整性检查**:部分库有多个子包(如 `@
|
|
158
|
+
1. **子包完整性检查**:部分库有多个子包(如 `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`),添加 import 后必须确认 package.json 中包含所有需要的子包
|
|
159
159
|
2. **禁止安装时需现场编译的 native addon**(`node-gyp rebuild`、`binding.gyp`、需要系统头文件或编译器)。依赖按平台内置能力 / 浏览器能力 → 纯 JS 或 WASM → 有预编译二进制的 native addon 的顺序选型
|
|
160
160
|
3. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
|
|
161
161
|
|
|
@@ -180,36 +180,6 @@ shared/ # 前后端共享的目录
|
|
|
180
180
|
- `.eslintrc.js`、`.prettierrc`
|
|
181
181
|
- `node_modules/`、`.git/`、`dist/`、`build/`
|
|
182
182
|
|
|
183
|
-
### 沙箱 dev 不提供平台 runtime 接口(`__runtime__`)
|
|
184
|
-
|
|
185
|
-
dev server 只代理 `/api`、`/openapi`、`/__innerapi__`(外加 legacy 的 `/af/p/:appId/api`、`/af/p/:appId/__innerapi__`)。`/app/<appId>/__runtime__/*` 这一整类**平台 runtime 接口在沙箱 dev 下打不通**,只有发布态才由网关接管。
|
|
186
|
-
|
|
187
|
-
**症状分两种,按请求方法区分**:POST 直接 404;**GET 会落到 Vite 的 SPA fallback,拿回 200 + `index.html`**,业务侧表现为「返回了一坨 HTML,JSON 解析失败」——见下方「问题排查指引」里「API 测试返回 HTML 内容时」那条,两者是同一回事。
|
|
188
|
-
|
|
189
|
-
已知受影响端点:
|
|
190
|
-
|
|
191
|
-
| 端点 | 方法 | 谁会打到它 |
|
|
192
|
-
|------|------|-----------|
|
|
193
|
-
| `api/v1/permissions/roles` | GET | `AppContainer` **每次加载应用自动打**,不需要你写任何代码 |
|
|
194
|
-
| `api/v1/observability/{logs,traces,metrics}/collect`、`current_server_timestamp` | POST/GET | 平台埋点上报,**自动打** |
|
|
195
|
-
| `api/v1/account/login/user` | POST | `useCurrentUserProfile()` → `authClient.session.getUserInfo()` |
|
|
196
|
-
| `api/v1/account/search_user`、`list_users`、`user_profile`、`search_department`、`convert_lark_user` | POST / `user_profile` 是 GET | `business-ui` 的 UserSelect / DepartmentSelect / UserDisplay |
|
|
197
|
-
| `api/v1/account/search`、`api/v1/account/chat/list_chats` | POST | ChatSelect 选群组件 |
|
|
198
|
-
| `api/v1/studio/user/profile` | GET | `getUserProfile()` |
|
|
199
|
-
| `api/v1/storage/object/<bucket>/pre_upload` | POST | `dataloom.storage.uploadFile()` 前端上传 |
|
|
200
|
-
| `api/v1/studio/plugins/tmp_files/acquire_upload_url`、`acquire_download_url` | POST | `capabilityClient` 的文件类入参 |
|
|
201
|
-
|
|
202
|
-
> `useCurrentUserProfile()` 打的是 `account/login/user`,**不是** `user_profile`/`search_user` 那一排;它另外还打一个 `GET /api/authnpaas/lark-user-id`,那条走 `/api`、被代理、正常。
|
|
203
|
-
|
|
204
|
-
**这是环境限制,不是应用缺陷,也不是组件坏了。** 看到这些 404 / HTML 响应时:
|
|
205
|
-
|
|
206
|
-
- **禁止**改 `client/src/components/business-ui/**`(平台保护文件)、**禁止**给组件加兜底请求或改用自研选人控件绕过——发布后这些端点是正常的,绕过写法反而是错的
|
|
207
|
-
- 需要在开发期验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。服务端的 `req.userContext` 和 `AuthNPaasService` 由网关注入,**不受此限制**,始终可用(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 在这里长期停在加载态就是这个原因
|
|
208
|
-
- 需要在开发期验证上传链路:改用接口测试直接打后端接口,或复用库里已有的文件 URL,不要在浏览器里点上传
|
|
209
|
-
- 依赖上述端点的 E2E 用例不要写成浏览器交互 Case,改走接口层往返验证
|
|
210
|
-
|
|
211
|
-
另外,`UserSelect` 弹层是**搜索驱动**的:不输关键词时列表为空属预期行为,不是接口挂了。
|
|
212
|
-
|
|
213
183
|
## 质量保障流程
|
|
214
184
|
|
|
215
185
|
通用提交前检查(代码检查 / 接口测试 / 读日志确认无错误)见系统任务流程约束;本栈具体落点:
|
|
@@ -590,9 +560,7 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
|
|
|
590
560
|
|
|
591
561
|
1. 使用 API 测试工具测试不依赖用户信息的接口
|
|
592
562
|
2. 积极使用 `read_logs` 工具,如无有效日志可增加 logger 打印
|
|
593
|
-
3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController
|
|
594
|
-
4. 请求路径含 `__runtime__` 的 404(POST)或 200 + HTML(GET):属沙箱 dev 环境限制,不是应用缺陷,见「沙箱 dev 不提供平台 runtime 接口」
|
|
595
|
-
5. 服务端启动即崩、`/api` 全部 502 且报 `UnknownDependenciesException ... argument Function at index [0]`:查构造函数有没有空参 `@Inject()`(见 plugin-guide),不要先去改 Module 的 imports / providers
|
|
563
|
+
3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController
|
|
596
564
|
|
|
597
565
|
---
|
|
598
566
|
|
|
@@ -613,8 +581,6 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
|
|
|
613
581
|
{% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
|
|
614
582
|
- **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
|
|
615
583
|
- **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
|
|
616
|
-
{% else %}
|
|
617
|
-
- **Markdown 渲染**: `components/ui/markdown`(react-markdown + remark-gfm,内置 prose 排版)
|
|
618
584
|
{% endif %}
|
|
619
585
|
|
|
620
586
|
## API 请求
|
|
@@ -652,8 +618,6 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
652
618
|
{% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
|
|
653
619
|
| TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
|
|
654
620
|
| Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
|
|
655
|
-
{% else %}
|
|
656
|
-
| Markdown | `components/ui/markdown` | Markdown 渲染(react-markdown + remark-gfm) |
|
|
657
621
|
{% endif %}
|
|
658
622
|
|
|
659
623
|
### 组件 Skill 召回规则(强制执行)
|
|
@@ -817,8 +781,15 @@ return <h1>{data?.title || '未知标题'}</h1>;
|
|
|
817
781
|
| 验证 | zod |
|
|
818
782
|
| 工具函数 | lodash |
|
|
819
783
|
| 样式 | clsx |
|
|
784
|
+
| Excel | xlsx(**仅前端实现,禁止服务端实现**。解析后将结构化数据传到服务端保存) |
|
|
785
|
+
| PDF 导出 | jspdf + html2canvas(**仅前端实现,禁止服务端实现**) |
|
|
786
|
+
| 文件上传 | react-dropzone |
|
|
787
|
+
| 二维码 | qrcode.react |
|
|
820
788
|
| 用户反馈 | sonner |
|
|
821
|
-
|
|
789
|
+
| 拖拽 | @dnd-kit/core |
|
|
790
|
+
| 数字动画 | react-countup |
|
|
791
|
+
| Base64 | js-base64 — `import { encode, decode } from 'js-base64'` |
|
|
792
|
+
| 3D 场景 | cobe |
|
|
822
793
|
|
|
823
794
|
## 滚动分页最佳实践
|
|
824
795
|
|
|
@@ -23,7 +23,6 @@ gate-tools:
|
|
|
23
23
|
| 长耗时 AI 结果 | 大体量/多字段/多份/多语言/文件或多模态串联等结构信号命中时,优先前端 `callStream` 渐进展示;需保存则流式结束后复用 CRUD 落库;仅在 Client 侧无法满足时使用后端任务记录 + 状态查询 + 结果读取;禁止单个 HTTP 请求等待完整结果后才返回 |
|
|
24
24
|
| capabilityClient 导入 | `import { capabilityClient } from '@lark-apaas/client-toolkit'` |
|
|
25
25
|
| CapabilityService 导入 | `import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';` |
|
|
26
|
-
| CapabilityService 注入 | `@Inject(CapabilityService) private readonly capabilityService: CapabilityService`(**禁止空参 `@Inject()`**,会导致启动崩溃 502;业务 Module 无需 imports) |
|
|
27
26
|
|
|
28
27
|
## 必读 references
|
|
29
28
|
|
package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md
CHANGED
|
@@ -87,18 +87,12 @@
|
|
|
87
87
|
#### call / callStream 函数签名
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
|
-
//
|
|
91
|
-
.
|
|
92
|
-
.callStream<T = unknown>(actionKey: string, params?: Record<string, unknown>): AsyncIterable<T> // 流式
|
|
90
|
+
.call(actionKey: string, input: object) // 非流式,返回 Promise<output>
|
|
91
|
+
.callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
|
|
93
92
|
```
|
|
94
93
|
|
|
95
94
|
- **第一个参数 `actionKey`**:必须是字符串,值来自 `get_plugin_ai_json` 返回的 `actions[].key`(如 `'sendFeishuMessage'`、`'textGenerate'`)
|
|
96
|
-
- **第二个参数 `
|
|
97
|
-
- 上面是**前端**签名,带泛型。**服务端 `CapabilityService` 的 `call` / `callStream` / `callStreamWithEvents` 都没有泛型**,两者不通用,详见「Server 侧调用方式」
|
|
98
|
-
|
|
99
|
-
> **`Record<string, unknown>` 不接 `interface` 声明的对象**(interface 没有隐式索引签名),直接传会撞
|
|
100
|
-
> `Argument of type 'XxxInput' is not assignable to parameter of type 'Record<string, unknown>'. Index signature for type 'string' is missing in type 'XxxInput'.`
|
|
101
|
-
> 三种正确写法:① 直接传内联对象字面量;② 入参类型用 `type XxxInput = { ... }` 而非 `interface`(type 别名有隐式索引签名);③ 已有 interface 时在调用点显式 `input as unknown as Record<string, unknown>`。**不要为此改用 `as any`。**
|
|
95
|
+
- **第二个参数 `input`**:必须是对象,结构符合 `actions[].inputSchema`
|
|
102
96
|
|
|
103
97
|
```typescript
|
|
104
98
|
// ❌ 错误:把参数 JSON.stringify 后当作 actionKey
|
|
@@ -312,49 +306,23 @@ import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
|
|
|
312
306
|
export class XxxService {
|
|
313
307
|
private readonly logger = new Logger(XxxService.name);
|
|
314
308
|
|
|
315
|
-
// 注入 token 必须显式写成 @Inject(CapabilityService)
|
|
316
309
|
constructor(
|
|
317
|
-
@Inject(
|
|
310
|
+
@Inject() private readonly capabilityService: CapabilityService,
|
|
318
311
|
) {}
|
|
319
312
|
}
|
|
320
313
|
```
|
|
321
314
|
|
|
322
|
-
**禁止空参 `@Inject()`**。本 stack 的 `nest-cli.json` 用 `builder: swc`,空参 `@Inject()` 会把注入 token 置为 undefined,Nest 回退读 `design:type` 拿到 `Function`,服务端启动即崩溃、所有 `/api` 请求返回 502:
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
UnknownDependenciesException: Nest can't resolve dependencies of the XxxService (?).
|
|
326
|
-
Please make sure that the argument Function at index [0] is available in the XxxModule context.
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
见到这段报错先查构造函数有没有空参 `@Inject()`,不要去改 Module 的 `imports` / `providers`。
|
|
330
|
-
|
|
331
|
-
**业务 Module 不需要注册 CapabilityModule**。`PlatformModule` 是 `@Global()` 且已把 `CapabilityModule` 放进 `exports`,`CapabilityService` 全局可注入。不要在业务 Module 里写 `imports: [CapabilityModule.forRoot(...)]` 或 `imports: [PlatformModule.forRoot()]`。
|
|
332
|
-
|
|
333
|
-
同一规则适用于从 `@lark-apaas/fullstack-nestjs-core` 注入的其他平台服务(`AuthNPaasService`、`FileService` 等)。
|
|
334
|
-
|
|
335
315
|
#### 3. 调用示例
|
|
336
316
|
|
|
337
|
-
服务端 `CapabilityExecutor` 的**三个方法全都没有泛型参数**(`@lark-apaas/nestjs-capability`):
|
|
338
|
-
|
|
339
|
-
```typescript
|
|
340
|
-
call(actionName: string, input: unknown, context?: Partial<PluginActionContext>): Promise<unknown>;
|
|
341
|
-
callStream(actionName: string, input: unknown, context?): AsyncIterable<unknown>;
|
|
342
|
-
callStreamWithEvents(actionName: string, input: unknown, context?): AsyncIterable<StreamEvent<unknown>>;
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
写 `.call<T>(...)` / `.callStream<T>(...)` / `.callStreamWithEvents<T>(...)` 都会报「应有 0 个类型参数,但获得 1 个」。泛型只存在于前端 `capabilityClient`。注意服务端 `input` 是**必传**(没有 `?`),且类型是 `unknown` 而非前端的 `Record<string, unknown>`。服务端接 `unknown` 后在运行时收窄,禁止 `as any`:
|
|
346
|
-
|
|
347
317
|
```typescript
|
|
348
318
|
const inputParams = {
|
|
349
319
|
// 严格按 get_plugin_ai_json.actions[].inputSchema 构造
|
|
350
320
|
};
|
|
351
321
|
|
|
352
322
|
try {
|
|
353
|
-
const output
|
|
323
|
+
const output = await this.capabilityService
|
|
354
324
|
.load('')
|
|
355
325
|
.call('', inputParams);
|
|
356
|
-
// 运行时收窄后再取字段,例如:
|
|
357
|
-
// if (typeof output === 'object' && output !== null && 'content' in output) { ... }
|
|
358
326
|
return output;
|
|
359
327
|
} catch (error) {
|
|
360
328
|
this.logger.error('pluginInstance call failed', {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-guide
|
|
3
|
+
description: "Use when 用户要把应用能力开放给外部 Agent / 工作伙伴 / MCP 客户端调用,或提到 MCP Server、MCP 工具、MCP Apps、tools/call、@McpTools、@McpTool、manifest.json。覆盖工具类目录与命名、装饰器用法、zod schema、身份读取、错误处理、MCP Apps 界面、清单校验与调试方式。触发词:MCP, MCP Server, MCP 工具, MCP Apps, 应用 Skill, server/mcp/SKILL.md, resources/read, mcp tool, tools/list, tools/call, @McpTools, @McpTool, @McpUiResource, miaoda-mcp, manifest.json, 开放给 Agent, 工作伙伴调用, Agent 调用应用"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: mcp_guide
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# MCP Server 开发指引
|
|
10
|
+
|
|
11
|
+
应用通过 MCP(Model Context Protocol)把业务能力开放给外部 Agent。平台 SDK(`@lark-apaas/fullstack-nestjs-core`)已内置 MCP Server:**只需要用装饰器把业务方法声明为工具**,协议、装配、入参校验、身份读取与清单导出全部由 SDK 完成。
|
|
12
|
+
|
|
13
|
+
## 一、边界(先读)
|
|
14
|
+
|
|
15
|
+
| 由谁负责 | 内容 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| **你(业务代码)** | 在 `server/mcp/tools/*.tools.ts` 里用 `@McpTools()` / `@McpTool()` 声明工具,调用现有 Service |
|
|
18
|
+
| **SDK(已内置)** | `POST /__innerapi__/mcp` 端点、Streamable HTTP 协议、zod 校验、`ctx.user` 身份、在线 manifest 与资源读取、离线清单导出 |
|
|
19
|
+
| **平台网关** | 凭证校验、OAuth、把当前用户身份注入请求头 |
|
|
20
|
+
|
|
21
|
+
**严禁**:自行实现 MCP 协议或 JSON-RPC、自行挂载 `/mcp` 路由、在应用内做凭证校验、把 `@modelcontextprotocol/sdk` 加进 `package.json`(SDK 已带)。
|
|
22
|
+
|
|
23
|
+
## 二、目录与命名
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
server
|
|
27
|
+
└── mcp
|
|
28
|
+
├── SKILL.md # 应用能力使用指南,由 AI 随业务维护
|
|
29
|
+
└── tools
|
|
30
|
+
├── order.tools.ts # 一个业务域一个文件:{域名}.tools.ts
|
|
31
|
+
└── inventory.tools.ts
|
|
32
|
+
client
|
|
33
|
+
└── mcp-ui # 仅 MCP Apps 需要(见第六节)
|
|
34
|
+
└── order-detail
|
|
35
|
+
├── index.html
|
|
36
|
+
└── main.tsx
|
|
37
|
+
.spark/mcp/manifest.json # SDK 自动生成,不要手改
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- 工具类命名 `XxxMcpTools`,注册为**所属业务模块**的 provider;SDK 启动时自动发现,**不需要改 `app.module.ts`**。
|
|
41
|
+
- 工具名 = 类前缀 + 方法名(或 `name` 覆盖),只允许 `A-Z a-z 0-9 _ . -`,全应用唯一。用 `{域}_{动作}` 风格:`order_get`、`order_list`、`order_cancel`。
|
|
42
|
+
|
|
43
|
+
## 三、编写工具
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// server/mcp/tools/order.tools.ts
|
|
47
|
+
import { z } from 'zod';
|
|
48
|
+
import {
|
|
49
|
+
McpTools,
|
|
50
|
+
McpTool,
|
|
51
|
+
McpToolError,
|
|
52
|
+
type Infer,
|
|
53
|
+
type McpContext,
|
|
54
|
+
type McpToolResult,
|
|
55
|
+
} from '@lark-apaas/fullstack-nestjs-core';
|
|
56
|
+
import { OrderService } from '@server/modules/order/order.service';
|
|
57
|
+
|
|
58
|
+
// schema 用 const 声明并导出,供类型推导与清单导出
|
|
59
|
+
export const GetOrderInput = { orderId: z.string().describe('订单 ID') };
|
|
60
|
+
export const GetOrderOutput = {
|
|
61
|
+
orderId: z.string(),
|
|
62
|
+
amount: z.number().describe('金额,单位元'),
|
|
63
|
+
status: z.enum(['pending', 'paid', 'cancelled']),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
@McpTools({ prefix: 'order_' })
|
|
67
|
+
export class OrderMcpTools {
|
|
68
|
+
constructor(private readonly orders: OrderService) {}
|
|
69
|
+
|
|
70
|
+
@McpTool({
|
|
71
|
+
title: '查询订单',
|
|
72
|
+
description: '按订单 ID 查询金额与状态。用户询问某个订单的情况时调用;找不到订单会返回 ORDER_NOT_FOUND。',
|
|
73
|
+
inputSchema: GetOrderInput,
|
|
74
|
+
outputSchema: GetOrderOutput,
|
|
75
|
+
annotations: { readOnlyHint: true },
|
|
76
|
+
})
|
|
77
|
+
async get(input: Infer<typeof GetOrderInput>, ctx: McpContext): Promise<McpToolResult<typeof GetOrderOutput>> {
|
|
78
|
+
const order = await this.orders.findVisibleTo(input.orderId, ctx.user.userId);
|
|
79
|
+
if (!order) throw new McpToolError('订单不存在', { code: 'ORDER_NOT_FOUND' });
|
|
80
|
+
return { structuredContent: { orderId: order.id, amount: order.amount, status: order.status } };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@McpTool({
|
|
84
|
+
description: '取消订单。仅在用户明确要求取消且订单处于 pending 时调用。',
|
|
85
|
+
inputSchema: { orderId: z.string() },
|
|
86
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
87
|
+
})
|
|
88
|
+
async cancel(input: { orderId: string }, ctx: McpContext): Promise<McpToolResult> {
|
|
89
|
+
await this.orders.cancel(input.orderId, ctx.user.userId);
|
|
90
|
+
return { content: [{ type: 'text', text: `订单 ${input.orderId} 已取消` }] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// server/modules/order/order.module.ts —— 工具类和 Service 一起注册
|
|
97
|
+
@Module({ providers: [OrderService, OrderMcpTools], controllers: [OrderController] })
|
|
98
|
+
export class OrderModule {}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 规则
|
|
102
|
+
|
|
103
|
+
1. **`description` 必填**且面向模型:说清用途、何时调用、返回什么、失败会怎样。`title` 面向人。
|
|
104
|
+
2. **方法签名固定** `(input, ctx)`:`input` 用 `Infer<typeof InputSchema>` 标注;`ctx.user` 与 REST 里的 `req.userContext` 完全一致(`userId` / `tenantId` / `roles` …)。TypeScript 装饰器不能改写方法类型,所以入参与返回值类型必须自己写。
|
|
105
|
+
3. **返回值**:声明了 `outputSchema` → 返回 `{ structuredContent }`(SDK 自动补一份 JSON 文本);没声明 → 返回 `{ content: [{ type: 'text', text }] }`。
|
|
106
|
+
4. **业务错误抛 `McpToolError(message, { code?, data? })`**,Agent 会原样收到并据此调整;其他异常会被记日志并替换为通用失败提示,不要用它传业务信息。
|
|
107
|
+
5. **注解**如实填写:只读 `readOnlyHint`,删除/不可逆 `destructiveHint`,重复调用无副作用 `idempotentHint`。
|
|
108
|
+
6. **权限与数据隔离复用业务层**:工具只负责把 `ctx.user` 传给 Service,不在工具里拼 SQL 或绕过行级权限。
|
|
109
|
+
7. 工具方法保持薄:参数校验交给 zod,业务逻辑放 Service;一个工具只做一件事。
|
|
110
|
+
8. 工具类及依赖必须为单例,逐请求身份通过 ctx.user 传递;Nest HTTP Controller 上的 Guard/Pipe/Interceptor 不会自动应用于工具方法,业务权限检查放共享 Service。
|
|
111
|
+
9. 未携带用户身份的调用默认被 SDK 拒绝(`MCP_USER_REQUIRED`),不需要自己判空。
|
|
112
|
+
|
|
113
|
+
## 四、zod 用法约束
|
|
114
|
+
|
|
115
|
+
- 入参 / 出参都用**对象**:原始形状 `{ a: z.string() }` 或 `z.object({...})`,不要用数组 / 基础类型作为顶层。
|
|
116
|
+
- 每个字段尽量 `.describe()`,这是 Agent 理解参数的唯一依据。
|
|
117
|
+
- 可选字段用 `.optional()`;枚举用 `z.enum([...])`;不要用 `z.any()` / `z.unknown()` 作为入参。
|
|
118
|
+
- 项目里已安装 zod,`import { z } from 'zod'` 即可,不要额外安装。
|
|
119
|
+
|
|
120
|
+
## 五、校验与调试
|
|
121
|
+
|
|
122
|
+
**每次新增或修改工具后必须运行:**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx miaoda-mcp validate
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
它会加载 `server/mcp` 下的源码,检查工具名格式 / 重名 / 引用错误,并写入 `.spark/mcp/manifest.json`(工具列表与 JSON Schema,用于离线导出和校验)。校验失败要修到通过;`.spark/mcp/manifest.json` 由命令生成,不要手改,随代码一起提交。
|
|
129
|
+
|
|
130
|
+
**类型检查**:`npm run type:check:server`(工具方法签名错误会在这里暴露)。
|
|
131
|
+
|
|
132
|
+
**调试端点**:应用启动后(`npm run dev`),MCP 端点在开发服务入口的 `/__innerapi__/mcp`。
|
|
133
|
+
标准 MCP 客户端配置为 Streamable HTTP、URL 填 `<应用开发地址>/__innerapi__/mcp` 即可;本地开发与云端沙箱使用同一协议路径。本地代理从 SUDA_WEBUSER 注入身份;沙箱入口需由平台提供可信身份,不要伪造用户头。
|
|
134
|
+
也可以用 curl 快速验证(`Accept` 必须同时含两种类型):
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
curl -s -X POST '<应用开发地址>/__innerapi__/mcp' \
|
|
138
|
+
-H 'content-type: application/json' \
|
|
139
|
+
-H 'accept: application/json, text/event-stream' \
|
|
140
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- 返回 `404`:检查 MCP 模块是否启用、访问路径及应用前缀;工具缺失时检查所属模块的 `providers`。
|
|
144
|
+
- 返回 `405`:用了 GET/DELETE,端点只接受 POST。
|
|
145
|
+
- 工具结果 `isError` 且 code 为 `MCP_USER_REQUIRED`:请求没经过开发服务入口 / 网关,缺少用户身份。
|
|
146
|
+
|
|
147
|
+
## 六、MCP Apps(可选:工具结果带界面)
|
|
148
|
+
|
|
149
|
+
只有当结果**需要可视化交互**(详情卡片、图表、审批表单)时才做。做法:
|
|
150
|
+
|
|
151
|
+
1. 界面源码放 `client/mcp-ui/<entry>/index.html`(+ `main.tsx`,可用 React),构建预设会自动打成单文件 `dist/mcp-ui/<entry>.html`,开发态监听变更自动重建。界面内如需与宿主通信,用 `@modelcontextprotocol/ext-apps` 的 `App` / React hooks(SDK 已带)。
|
|
152
|
+
2. 在工具类里声明资源并关联工具:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { McpUiResource, readMcpUiTemplate } from '@lark-apaas/fullstack-nestjs-core';
|
|
156
|
+
|
|
157
|
+
@McpTools({ prefix: 'order_' })
|
|
158
|
+
export class OrderMcpTools {
|
|
159
|
+
@McpTool({ description: '…', inputSchema: GetOrderInput, outputSchema: GetOrderOutput, ui: { resourceUri: 'ui://order/detail' } })
|
|
160
|
+
async get(/* … */) { /* … */ }
|
|
161
|
+
|
|
162
|
+
@McpUiResource({ uri: 'ui://order/detail', title: '订单详情' })
|
|
163
|
+
detailView() {
|
|
164
|
+
return readMcpUiTemplate('order-detail'); // 对应 client/mcp-ui/order-detail
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `uri` 必须以 `ui://` 开头,`ui.resourceUri` 必须能找到对应的 `@McpUiResource()`(`miaoda-mcp validate` 会检查)。
|
|
170
|
+
- 界面代码不放在 `server/` 下(会被服务端 tsconfig 编译报 JSX 错误)。
|
|
171
|
+
|
|
172
|
+
## 七、常见错误
|
|
173
|
+
|
|
174
|
+
| 现象 | 原因 | 处理 |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| 启动报「工具名重复」 | 两个方法算出同名 | 调整 `prefix` 或 `name` |
|
|
177
|
+
| 启动报「未找到对应的 @McpUiResource」 | `ui.resourceUri` 拼错 | 与 `@McpUiResource({ uri })` 保持一致 |
|
|
178
|
+
| `validate` 报 Cannot find module | 工具文件 import 了未安装的包或错误路径 | 修正 import;路径别名用 `@server/*` |
|
|
179
|
+
| 工具能列出但调用总是失败 | 抛了非 `McpToolError` 的异常 | 看服务端日志;业务失败改抛 `McpToolError` |
|
|
180
|
+
| Agent 不会调用工具 | `description` 太简略或缺 `.describe()` | 补充何时调用、参数含义与返回内容 |
|
|
181
|
+
|
|
182
|
+
## 八、平台目录与验收
|
|
183
|
+
|
|
184
|
+
平台通过 `GET <应用开发地址>/__innerapi__/mcp/manifest` 获取当前运行能力。工具和资源定义来自运行注册表,Skill 正文读取 `server/mcp/SKILL.md`;不扫描或回退到 `.spark/mcp/manifest.json`。用户无需实现该接口。示例(省略具体工具和 UI 资源):
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"version": 1,
|
|
189
|
+
"generatedAt": "2026-09-12T08:00:00.000Z",
|
|
190
|
+
"endpoint": "/__innerapi__/mcp",
|
|
191
|
+
"tools": [],
|
|
192
|
+
"resources": [{ "uri": "skill://app/SKILL.md", "name": "app-skill", "mimeType": "text/markdown" }],
|
|
193
|
+
"skill": {
|
|
194
|
+
"uri": "skill://app/SKILL.md",
|
|
195
|
+
"path": "server/mcp/SKILL.md",
|
|
196
|
+
"content": "---\nname: orders\ndescription: 查询与取消订单\n---\n# 订单能力\n先查询订单,明确确认后再取消。\n"
|
|
197
|
+
},
|
|
198
|
+
"sources": { "tools": {}, "resources": {} }
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`tools` / `resources` 保留标准 MCP 定义,UI 关联在工具 `_meta.ui.resourceUri`;资源与工具独立列出。`skill` 文件缺失为 `null`,读取失败不能伪装为空。`sources` 按工具名 / 资源 URI 提供 `{ path, line? }`,定位不可用时省略对应键,生产无源码映射可为空。Skill 定位直接用 `skill.path`。`generatedAt` 是响应生成时间,不是源码版本;源码跳转需由平台核对工程版本。工具源码变更在服务成功重启后生效。
|
|
203
|
+
|
|
204
|
+
删除所有 MCP 工具、UI 资源声明及应用 Skill 文件后再次运行 `miaoda-mcp validate` 清理旧清单。标准 MCP 协议端点仍为 POST,不要把 GET 目录接口当成工具调用接口。
|
|
205
|
+
|
|
206
|
+
MCP Apps 使用 nrf 默认 Vite。生产构建同时输出 `dist/mcp-ui` 和发布包内 `dist/server/dist/mcp-ui`;界面可以使用普通 HTML/TypeScript,不强制 React。最小宿主通信示例:
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<!-- client/mcp-ui/order-detail/index.html -->
|
|
210
|
+
<!doctype html>
|
|
211
|
+
<html><head><meta charset="utf-8"></head><body>
|
|
212
|
+
<pre id="result">等待工具结果</pre>
|
|
213
|
+
<script type="module" src="./main.ts"></script>
|
|
214
|
+
</body></html>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// client/mcp-ui/order-detail/main.ts
|
|
219
|
+
import { App } from '@modelcontextprotocol/ext-apps';
|
|
220
|
+
const app = new App({ name: 'order-detail', version: '1.0.0' }, {});
|
|
221
|
+
app.ontoolresult = (result) => {
|
|
222
|
+
document.getElementById('result')!.textContent = JSON.stringify(result.structuredContent ?? result.content, null, 2);
|
|
223
|
+
};
|
|
224
|
+
await app.connect();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
界面通过宿主 SDK 接收结果,不要自行处理宿主 postMessage 协议;展示文本用 textContent,避免将业务返回值直接写成 HTML。
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
## 九、应用 Skill(开放 MCP 能力时一起维护)
|
|
231
|
+
|
|
232
|
+
创建或修改应用 MCP 能力时,**生成并同步更新 `server/mcp/SKILL.md`**,让外部 Agent 知道如何组合使用该应用。这个文件是应用交付内容;当前 `mcp-guide/SKILL.md` 是指导编码 AI 的 steering,不要把 mcp-guide 复制成应用 Skill,也不要给应用 Skill 加 `steering: true`。
|
|
233
|
+
|
|
234
|
+
应用 Skill 应含 `name` / `description` frontmatter、适用场景、实际工具名与参数、调用顺序、用户确认边界、身份权限要求、失败处理和对应 UI URI(如有)。只记录已实现能力,不写凭证、用户数据或内部秘密。示例:
|
|
235
|
+
|
|
236
|
+
```markdown
|
|
237
|
+
---
|
|
238
|
+
name: mcp
|
|
239
|
+
description: 查询订单详情,并在用户明确要求后取消待处理订单。
|
|
240
|
+
---
|
|
241
|
+
# 订单能力
|
|
242
|
+
- 需要已登录用户身份;只能访问当前用户可见的订单。
|
|
243
|
+
- 用户提供订单 ID 后调用 order_get,参数为 orderId。
|
|
244
|
+
- 只有用户明确要求取消且订单状态为 pending 时才调用 order_cancel。
|
|
245
|
+
- ORDER_NOT_FOUND 时请用户核对订单 ID;不要尝试查询其他用户的数据。
|
|
246
|
+
- 支持界面的宿主可展示 ui://order/detail。
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
修改入口是让 **AI 修改工程文件**,随后按应用流程校验、构建和发布;不要实现 GUI 保存 Skill 或运行时写源码接口。开发态读取当前文件,线上读取随应用版本部署的文件,开发修改不会自动改线上版本。
|
|
250
|
+
|
|
251
|
+
同一文件同时用于 manifest 的 `skill: { uri, path, content }` 和标准 MCP 资源。客户端先 `resources/list` 发现 `skill://app/SKILL.md`,再用 `resources/read` 读取完整 Markdown(含 frontmatter):
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"skill://app/SKILL.md"}}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
成功结果的 `contents` 包含 `{ uri: "skill://app/SKILL.md", mimeType: "text/markdown", text: "完整 Markdown" }`,正文与 manifest `skill.content` 来自同一文件。默认 `requireUser` 下,**Skill 和 UI 资源读取都需要可信用户身份**;不能因为可列出资源就假设可匿名读取。沿用平台网关注入身份,不伪造请求头。这是标准资源,不新增 MCP prompts 或私有 skills 协议扩展。
|
|
258
|
+
|
|
259
|
+
现有 nrf Vite 构建会自动把该固定文件复制到 `dist/server/server/mcp/SKILL.md`;生产 cwd 为 `dist/server`,因此仍能按 `server/mcp/SKILL.md` 读取。没有 UI 也会打包;删除源文件后重建会清理旧 Skill 产物。不需要额外 CLI、Nest assets 配置或整体复制 server 源码。源文件及产物路径不要使用符号链接。
|
|
260
|
+
|
|
261
|
+
验收至少覆盖:有身份读取 Skill 与 manifest 正文一致;无身份资源读取被拒绝;无 UI 仍交付 Skill;修改后重建更新正文;删除后重建不残留;从生产 cwd 能读取固定相对路径。没有文件时应返回 `skill: null` 且不列出 Skill 资源。
|
|
@@ -14,8 +14,6 @@ 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
|
-
>
|
|
19
17
|
> **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
|
|
20
18
|
|
|
21
19
|
## 怎么选(决策指引)
|
|
@@ -129,7 +129,7 @@ shared/ # 前后端共享的目录
|
|
|
129
129
|
|
|
130
130
|
1. 优先使用项目已有依赖,仅在无法实现时安装新依赖
|
|
131
131
|
2. 使用前先查看 `package.json` 确保依赖已存在
|
|
132
|
-
3. **子包完整性检查**:部分库有多个子包(如 `@
|
|
132
|
+
3. **子包完整性检查**:部分库有多个子包(如 `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`),添加 import 后必须确认 package.json 中包含所有需要的子包
|
|
133
133
|
4. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
|
|
134
134
|
|
|
135
135
|
## 文件命名约定
|
|
@@ -147,39 +147,6 @@ 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
|
-
|
|
183
150
|
## 质量保障流程
|
|
184
151
|
|
|
185
152
|
修改代码后:
|
|
@@ -491,7 +458,8 @@ NestJS 自己不读 env。直连 NestJS 端口 → header 缺失 → `req.userCo
|
|
|
491
458
|
- **图标**: Lucide React(唯一图标库,禁止 Emoji 和其他图标库)
|
|
492
459
|
- **表格/表单/图表**: 见下方"组件 Skill 召回规则",开发前必须先调用对应 Skill
|
|
493
460
|
- **用户**: 用户信息展示/选择必须用 `business-ui` 组件(阅读 README.md),禁止直接展示 userId
|
|
494
|
-
-
|
|
461
|
+
- **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
|
|
462
|
+
- **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
|
|
495
463
|
|
|
496
464
|
## API 请求
|
|
497
465
|
|
|
@@ -524,7 +492,8 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
524
492
|
| --------------------------------------------------- | --------------------------------------- | --------------------------------- |
|
|
525
493
|
| Table | `@lark-apaas/client-toolkit/antd-table` | 数据表格,**先调 `/table-skill`** |
|
|
526
494
|
| UserSelect/UserDisplay/UserProfile/DepartmentSelect | `business-ui/*` | 用户/部门选择展示 |
|
|
527
|
-
|
|
|
495
|
+
| TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
|
|
496
|
+
| Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
|
|
528
497
|
|
|
529
498
|
### 组件 Skill 召回规则(强制执行)
|
|
530
499
|
|
|
@@ -669,8 +638,15 @@ return <h1>{data?.title || '未知标题'}</h1>;
|
|
|
669
638
|
| 验证 | zod |
|
|
670
639
|
| 工具函数 | lodash |
|
|
671
640
|
| 样式 | clsx |
|
|
641
|
+
| Excel | xlsx(**仅前端实现,禁止服务端实现**。解析后将结构化数据传到服务端保存) |
|
|
642
|
+
| PDF 导出 | jspdf + html2canvas(**仅前端实现,禁止服务端实现**) |
|
|
643
|
+
| 文件上传 | react-dropzone |
|
|
644
|
+
| 二维码 | qrcode.react |
|
|
672
645
|
| 用户反馈 | sonner |
|
|
673
|
-
|
|
646
|
+
| 拖拽 | @dnd-kit/core |
|
|
647
|
+
| 数字动画 | react-countup |
|
|
648
|
+
| Base64 | js-base64 — `import { encode, decode } from 'js-base64'` |
|
|
649
|
+
| 3D 场景 | cobe |
|
|
674
650
|
|
|
675
651
|
## 滚动分页最佳实践
|
|
676
652
|
|
|
@@ -38,7 +38,7 @@ lark-cli apps --help 2>&1 | grep -q '+plugin-install' && echo "READY" || echo "M
|
|
|
38
38
|
| 长耗时 AI 结果 | 大体量/多字段/多份/多语言/文件或多模态串联等结构信号命中时,优先前端 `callStream` 渐进展示;需保存则流式结束后复用 CRUD 落库;仅在 Client 侧无法满足时使用后端任务记录 + 状态查询 + 结果读取;禁止单个 HTTP 请求等待完整结果后才返回 |
|
|
39
39
|
| capabilityClient 导入 | `import { capabilityClient } from '@lark-apaas/client-toolkit'` |
|
|
40
40
|
| CapabilityService 导入 | `import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';` |
|
|
41
|
-
| CapabilityService 注入 | `@Inject(
|
|
41
|
+
| CapabilityService 注入 | `@Inject() private readonly capabilityService: CapabilityService` |
|
|
42
42
|
| 插件实例配置位置 | `server/capabilities/<instance_id>.json`(默认全栈应用)。见「配置目录」段 |
|
|
43
43
|
|
|
44
44
|
> **capabilityClient 导入警告**:`capabilityClient` 是从 `@lark-apaas/client-toolkit` 直接导入的独立对象,**不是**从 `getDataloom()` 上获取的。正确且唯一的方式:`import { capabilityClient } from '@lark-apaas/client-toolkit'`。
|
package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md
CHANGED
|
@@ -76,18 +76,12 @@
|
|
|
76
76
|
#### call / callStream 函数签名
|
|
77
77
|
|
|
78
78
|
```typescript
|
|
79
|
-
//
|
|
80
|
-
.
|
|
81
|
-
.callStream<T = unknown>(actionKey: string, params?: Record<string, unknown>): AsyncIterable<T> // 流式
|
|
79
|
+
.call(actionKey: string, input: object) // 非流式,返回 Promise<output>
|
|
80
|
+
.callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
- **第一个参数 `actionKey`**:必须是字符串,值来自 `get_plugin_ai_json` 返回的 `actions[].key`(如 `'sendFeishuMessage'`、`'textGenerate'`)
|
|
85
|
-
- **第二个参数 `
|
|
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`。**
|
|
84
|
+
- **第二个参数 `input`**:必须是对象,结构符合 `actions[].inputSchema`
|
|
91
85
|
|
|
92
86
|
```typescript
|
|
93
87
|
// ❌ 错误:把参数 JSON.stringify 后当作 actionKey
|
|
@@ -274,49 +268,23 @@ import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
|
|
|
274
268
|
export class XxxService {
|
|
275
269
|
private readonly logger = new Logger(XxxService.name);
|
|
276
270
|
|
|
277
|
-
// 注入 token 必须显式写成 @Inject(CapabilityService)
|
|
278
271
|
constructor(
|
|
279
|
-
@Inject(
|
|
272
|
+
@Inject() private readonly capabilityService: CapabilityService,
|
|
280
273
|
) {}
|
|
281
274
|
}
|
|
282
275
|
```
|
|
283
276
|
|
|
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
|
-
|
|
297
277
|
#### 3. 调用示例
|
|
298
278
|
|
|
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
|
-
|
|
309
279
|
```typescript
|
|
310
280
|
const inputParams = {
|
|
311
281
|
// 严格按 get_plugin_ai_json.actions[].inputSchema 构造
|
|
312
282
|
};
|
|
313
283
|
|
|
314
284
|
try {
|
|
315
|
-
const output
|
|
285
|
+
const output = await this.capabilityService
|
|
316
286
|
.load('')
|
|
317
287
|
.call('', inputParams);
|
|
318
|
-
// 运行时收窄后再取字段,例如:
|
|
319
|
-
// if (typeof output === 'object' && output !== null && 'content' in output) { ... }
|
|
320
288
|
return output;
|
|
321
289
|
} catch (error) {
|
|
322
290
|
this.logger.error('pluginInstance call failed', {
|