@lark-apaas/coding-steering 0.1.57 → 0.1.59
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 +1 -1
- package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +16 -12
- package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +2 -2
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +20 -5
- package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +46 -32
- package/steering/nestjs-react-fullstack/skills/design-guide/SKILL.md +0 -3
- package/steering/nestjs-react-fullstack/skills/design-guide/references/token-mapping.md +3 -3
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +28 -59
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +17 -2
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +127 -2
- package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +210 -177
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +82 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +249 -0
- package/steering/nestjs-react-fullstack/skills_common/server-contacts-contract/SKILL.md +19 -0
- package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +5 -1
- package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +2 -2
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +18 -3
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +3 -0
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +93 -2
- package/steering/vite-react/skills/plugin-guide/SKILL.md +2 -0
- package/steering/nestjs-react-fullstack/skills/design-guide/references/corporate-blueprint.md +0 -252
package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md
CHANGED
|
@@ -287,6 +287,40 @@ function MultiPluginStreamExample({ recordId }: { recordId: string }) {
|
|
|
287
287
|
4. 若无法执行真实调用,必须明确写明阻塞原因,禁止直接标记“开发完成”
|
|
288
288
|
5. 若用户触发 AI 生成的请求超时、连接断开或工具返回超时,必须判定为验收失败;只有触发请求快速返回、随后能通过页面或接口读到明确完成或失败终态和结果,才允许标记通过
|
|
289
289
|
|
|
290
|
+
### 服务端 capability 手动 HTTP 调试与排障
|
|
291
|
+
|
|
292
|
+
仅绕过 SDK 做请求级复测或排障时使用本节,应用业务代码仍调用 SDK:Client 侧 `capabilityClient.load(id).call(actionKey, input)` / `callStream(actionKey, input)`;Server 侧兜底 `capabilityService.load(id).call(actionKey, input)`。
|
|
293
|
+
|
|
294
|
+
#### 1. 手动 HTTP/debug body 合同
|
|
295
|
+
|
|
296
|
+
调试入口与 body:
|
|
297
|
+
|
|
298
|
+
| 场景 | URL | body |
|
|
299
|
+
|------|-----|------|
|
|
300
|
+
| 运行态 unary | `POST /api/capability/<capabilityId>` | `{ "action": "<actionKey>", "params": { ... } }` |
|
|
301
|
+
| 运行态 stream | `POST /api/capability/<capabilityId>/stream` | `{ "action": "<actionKey>", "params": { ... } }` |
|
|
302
|
+
| 内部 debug unary | `POST /__innerapi__/capability/debug/<capabilityId>` | `{ "action": "<actionKey>", "params": { ... } }`,可按调试需要附带 `capability` |
|
|
303
|
+
| 内部 debug stream | `POST /__innerapi__/capability/debug/<capabilityId>/stream` | `{ "action": "<actionKey>", "params": { ... } }`,可按调试需要附带 `capability` |
|
|
304
|
+
|
|
305
|
+
- `action` 仍来自 `get_plugin_ai_json(...).actions[].key`;`params` 仍按 `actions[].inputSchema`。
|
|
306
|
+
- HTTP body 用 action/params;禁止写成 `{ "actionKey": "...", "input": { ... } }`。
|
|
307
|
+
- `Action 'undefined' not found` 先查 HTTP body 顶层字段:确认 body 顶层有 `action`,且插件输入放在 `params` 下;再查 action 名称是否存在。
|
|
308
|
+
|
|
309
|
+
#### 2. 先读 server 日志再归因
|
|
310
|
+
|
|
311
|
+
插件调用失败时,先读 `server` 日志,记录同一次请求的 `path`、`action`、`input`、`pluginName`、`actionName`、`status_code`、`detail.code`、`logID`。只看前端报错或只说“应检查日志”不算完成排障。
|
|
312
|
+
|
|
313
|
+
按日志分层:
|
|
314
|
+
|
|
315
|
+
| 日志信号 | 归因 |
|
|
316
|
+
|----------|------|
|
|
317
|
+
| `path` 命中 capability HTTP/debug 入口,`action` 为空或缺失,错误类似 `Action 'undefined' not found` | 协议错误:手动 HTTP body 没按 action/params 写 |
|
|
318
|
+
| `action` 正确,但 `input` 缺必填字段、类型不符或数组/文件格式错误 | 参数错误:按 `inputSchema` 修正 `params` |
|
|
319
|
+
| `pluginName` / `actionName` 已命中,`detail.code` 或下游日志指向插件服务、模型、TOS、限流等 | 插件后端错误:保留 `logID` 和下游错误码,避免改应用代码掩盖 |
|
|
320
|
+
| capability 调用成功,但应用接口、持久化、状态流转或 UI 读取失败 | 应用代码错误:排查业务接口、数据库和前端状态,不改插件合同 |
|
|
321
|
+
|
|
322
|
+
手动 HTTP/debug 复测只用于确认请求级合同和日志归因;最终实现仍回到 SDK 调用或既定 Server 侧编排。
|
|
323
|
+
|
|
290
324
|
### Server 侧调用方式(仅兜底场景)
|
|
291
325
|
|
|
292
326
|
> 以下场景适合使用 Server 侧调用;若前端 `callStream` + 既有 CRUD 保存即可满足展示和持久化,不要优先引入后端后台任务。
|
|
@@ -332,7 +366,97 @@ Please make sure that the argument Function at index [0] is available in the Xxx
|
|
|
332
366
|
|
|
333
367
|
同一规则适用于从 `@lark-apaas/fullstack-nestjs-core` 注入的其他平台服务(`AuthNPaasService`、`FileService` 等)。
|
|
334
368
|
|
|
335
|
-
#### 3.
|
|
369
|
+
#### 3. Server 侧已持久化文件进入 AI capability
|
|
370
|
+
|
|
371
|
+
Client 侧拿到用户刚选择的原始 `File` / `Blob` 时,直接传 `capabilityClient`,不要为了调用插件先上传到应用存储。文件/图片已作为业务数据持久化,且必须由 Server 侧 `CapabilityService` 调 `ai-doc-parser`、`ai-image-understanding` 或其它 `file` / `picture` / `plugin-file-url` / `plugin-image-url` 参数时,才在服务端把存储路径换成临时 http(s) URL 后传入插件。
|
|
372
|
+
|
|
373
|
+
优先从业务记录读取存储 `file_path`;字段类型是 `file_attachment` 时,同时读取 `bucket_id` 与 `file_path`。旧记录只有标准 `/app` 或 `/spark/app/.../runtime/api/v1/storage/object/<bucket>/<key>` download URL 时,可以解析出 bucket 与 file path 后再签名;不能把它直接交给默认 `createSignedUrl()`,因为该入口只使用默认 bucket,不会使用 URL 里的 bucket。签名 URL 才能交给 AI plugin。禁止给标准路径拼接 `localhost`、`window.location.origin` 或 `/app/<app_id>`,否则会绕过内部路径解析并被当成不安全的外部 URL。非标准相对路径、前端展示 URL、非法 base64 data URL 不应作为 Server 侧插件稳定入参。
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
import { FileService } from '@lark-apaas/fullstack-nestjs-core';
|
|
377
|
+
|
|
378
|
+
type StoredFileRef = {
|
|
379
|
+
file_path?: string | null;
|
|
380
|
+
bucket_id?: string | null;
|
|
381
|
+
download_url?: string | null;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const STORAGE_DOWNLOAD_URL_RE =
|
|
385
|
+
/^\/(?:spark\/)?app\/app_[\w]+\/runtime\/api\/v1\/storage\/object\/([^/]+)\/(.+)$/;
|
|
386
|
+
|
|
387
|
+
function parseStorageDownloadUrl(
|
|
388
|
+
downloadUrl: string,
|
|
389
|
+
): { bucketId: string; filePath: string } | null {
|
|
390
|
+
const match = STORAGE_DOWNLOAD_URL_RE.exec(downloadUrl);
|
|
391
|
+
if (!match) return null;
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const bucketId = decodeURIComponent(match[1]);
|
|
395
|
+
const filePath = decodeURIComponent(match[2]);
|
|
396
|
+
return bucketId && !bucketId.includes('/') && filePath
|
|
397
|
+
? { bucketId, filePath }
|
|
398
|
+
: null;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function createTemporaryFileUrl(
|
|
405
|
+
fileService: FileService,
|
|
406
|
+
file: StoredFileRef,
|
|
407
|
+
): Promise<string | null> {
|
|
408
|
+
try {
|
|
409
|
+
let signedUrl: string;
|
|
410
|
+
if (file.file_path) {
|
|
411
|
+
signedUrl = file.bucket_id
|
|
412
|
+
? await fileService
|
|
413
|
+
.from(file.bucket_id)
|
|
414
|
+
.createSignedUrl(file.file_path, 3600)
|
|
415
|
+
: await fileService.createSignedUrl(file.file_path, 3600);
|
|
416
|
+
} else {
|
|
417
|
+
const parsed = file.download_url
|
|
418
|
+
? parseStorageDownloadUrl(file.download_url)
|
|
419
|
+
: null;
|
|
420
|
+
if (!parsed) return null;
|
|
421
|
+
signedUrl = await fileService
|
|
422
|
+
.from(parsed.bucketId)
|
|
423
|
+
.createSignedUrl(parsed.filePath, 3600);
|
|
424
|
+
}
|
|
425
|
+
return signedUrl || null;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
// 这里不要返回伪成功 URL;由调用方把业务状态置为 failed/pending/null 等可见状态
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
调用 AI 插件前先签名并校验 URL:
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const docParserPluginInstanceId = '<doc-parser-plugin-instance-id>';
|
|
437
|
+
const signedUrl = await createTemporaryFileUrl(this.fileService, storedFile);
|
|
438
|
+
if (!signedUrl) {
|
|
439
|
+
this.logger.warn('missing file path or create signed url failed before AI capability call', {
|
|
440
|
+
filePath: storedFile.file_path,
|
|
441
|
+
bucketId: storedFile.bucket_id,
|
|
442
|
+
hasDownloadUrl: Boolean(storedFile.download_url),
|
|
443
|
+
pluginInstanceId: docParserPluginInstanceId,
|
|
444
|
+
actionKey: 'parseDocToMarkdown',
|
|
445
|
+
});
|
|
446
|
+
await this.updateRecord(recordId, { parseStatus: 'failed', parsedText: null });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const output = await this.capabilityService
|
|
451
|
+
.load(docParserPluginInstanceId)
|
|
452
|
+
.call('parseDocToMarkdown', {
|
|
453
|
+
fileUrl: [signedUrl], // 是否数组必须以 inputSchema 为准
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
稳定契约:已持久化文件统一使用签名函数返回的 URL,不要先下载后转 base64,也不要把原始 URL 作为备用值传给插件。缺少可识别的文件路径、签名失败或返回空 URL 时记录警告,写入 `failed` / `pending` / `null`,并停止调用插件。
|
|
458
|
+
|
|
459
|
+
#### 4. 调用示例
|
|
336
460
|
|
|
337
461
|
服务端 `CapabilityExecutor` 的**三个方法全都没有泛型参数**(`@lark-apaas/nestjs-capability`):
|
|
338
462
|
|
|
@@ -366,11 +490,12 @@ try {
|
|
|
366
490
|
}
|
|
367
491
|
```
|
|
368
492
|
|
|
369
|
-
####
|
|
493
|
+
#### 5. Server 侧编排与容错原则
|
|
370
494
|
|
|
371
495
|
- PluginInstance 调用在 Server 侧通常属于 **外部依赖 / side-effect**
|
|
372
496
|
- 除非业务明确要求强一致性,**默认不应阻塞主业务流程**
|
|
373
497
|
- 已选择 Server 侧承接的高耗时 AI capability 必须有可观测状态:创建任务时记录处理进度、完成终态、失败终态、输入摘要、错误信息和结果引用;触发接口只返回任务标识与当前状态,前端通过短轮询读取进度和最终结果
|
|
498
|
+
- 文件/图片签名 URL 生成失败时必须 `warn` 并把业务记录保持在 `failed` / `pending` / `null` 等用户可见状态,禁止构造假 URL 或写入“解析成功”
|
|
374
499
|
|
|
375
500
|
推荐写法:异步触发 + catch 兜底:
|
|
376
501
|
|
package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md
CHANGED
|
@@ -1,177 +1,210 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: server-builtins-file-storage-service
|
|
3
|
-
description: "Use when server-side code needs to upload or download files programmatically, such as file format conversion, automated report generation, or background tasks that produce files. NOT for frontend file operations. 触发词:服务端文件上传, 服务端文件下载, FileService, nestjs file, 后端文件处理, 文件格式转换, 生成文件上传"
|
|
4
|
-
steering: true
|
|
5
|
-
steering-topic: server_builtins_file_storage_service
|
|
6
|
-
match-template-name: nestjs-react-fullstack
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# NestJS File Service SDK
|
|
10
|
-
|
|
11
|
-
在 NestJS 服务端代码中使用 `FileService` 进行文件上传、下载、删除和管理。
|
|
12
|
-
|
|
13
|
-
> **使用注意**: 仅用于服务端文件处理场景,非必要文件上传下载场景请使用前端 `client-builtins-file-storage-service` skill。
|
|
14
|
-
> **入口边界**:本 SDK 是**服务端代码**读写应用存储的入口;Agent 在对话 / 开发中自己上传或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准。二者与本 SDK 操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进服务端代码**。
|
|
15
|
-
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const { content, metadata } = await this.fileService
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
---
|
|
2
|
+
name: server-builtins-file-storage-service
|
|
3
|
+
description: "Use when server-side code needs to upload or download files programmatically, such as file format conversion, automated report generation, or background tasks that produce files. NOT for frontend file operations. 触发词:服务端文件上传, 服务端文件下载, FileService, nestjs file, 后端文件处理, 文件格式转换, 生成文件上传"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: server_builtins_file_storage_service
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# NestJS File Service SDK
|
|
10
|
+
|
|
11
|
+
在 NestJS 服务端代码中使用 `FileService` 进行文件上传、下载、删除和管理。
|
|
12
|
+
|
|
13
|
+
> **使用注意**: 仅用于服务端文件处理场景,非必要文件上传下载场景请使用前端 `client-builtins-file-storage-service` skill。
|
|
14
|
+
> **入口边界**:本 SDK 是**服务端代码**读写应用存储的入口;Agent 在对话 / 开发中自己上传或调试文件,先加载应用文件存储操作 skill(按「应用文件存储 / 文件上传 / 文件下载」召回),用其 CLI 命令操作,具体命令一律以该 skill 为准。二者与本 SDK 操作同一个应用存储桶,但**该 CLI 命令只供 Agent 在沙箱终端使用,禁止写进服务端代码**。
|
|
15
|
+
|
|
16
|
+
## 展示 URL 边界
|
|
17
|
+
|
|
18
|
+
- 服务端上传或读取文件元信息后,`FileMeta.downloadURL` 是前端展示 / 下载的权威 URL。普通 `text` / `varchar` / `JSON` 业务字段回显文件时,接口应返回这个 URL(字段名如 `fileUrl` / `downloadUrl`)。
|
|
19
|
+
- 后台异步解析、格式转换等流程可保存稳定 `filePath` 作内部定位;列表 / 详情接口仍必须同时返回 `downloadURL`,或用 `getFileMetadata(filePath).downloadURL` 换出展示 URL。禁止只把普通 path 字段交给前端再生成 URL。
|
|
20
|
+
- `filePath` 不是前端展示 URL,不得让前端手拼 `/runtime/api/v1/storage/object`、`/app/{appId}/runtime`、`/spark/app/{appId}/runtime`、`__runtime__` 等运行态前缀。只有数据库字段类型明确为 `file_attachment` 且保存 `bucket_id + file_path` 时,才由前端按 file_attachment 规则换 URL。
|
|
21
|
+
|
|
22
|
+
## 使用场景
|
|
23
|
+
|
|
24
|
+
| 场景 | 说明 |
|
|
25
|
+
|------|------|
|
|
26
|
+
| 服务端文件处理 | 下载文件到本地处理后重新上传,如图片格式转换、docx 格式转换等 |
|
|
27
|
+
| 自动化任务生成文件 | 定时任务或事件触发后,根据数据库/插件内容生成文件并上传,如定时生成 PDF 报告、图片海报等 |
|
|
28
|
+
|
|
29
|
+
## Quick Reference
|
|
30
|
+
|
|
31
|
+
| 方法 | 说明 | 返回值 |
|
|
32
|
+
|------|------|--------|
|
|
33
|
+
| `upload(file, options?)` | 上传文件 | `FileMeta` |
|
|
34
|
+
| `download(path)` | 下载文件(≤50MB) | `FileDownloadBuilder` |
|
|
35
|
+
| `createSignedUrl(filePath, expiresIn)` | 默认 bucket 下生成临时 http(s) 签名 URL | `string` |
|
|
36
|
+
| `from(bucketId).createSignedUrl(filePath, expiresIn)` | 指定 bucket 生成临时 http(s) 签名 URL | `string` |
|
|
37
|
+
| `remove(filePaths)` | 删除文件 | `RemoveResponse` |
|
|
38
|
+
| `getFileMetadata(filePath)` | 获取文件元信息 | `FileMeta` |
|
|
39
|
+
|
|
40
|
+
## 注入 FileService
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { FileService } from '@lark-apaas/fullstack-nestjs-core';
|
|
44
|
+
|
|
45
|
+
@Injectable()
|
|
46
|
+
export class MyService {
|
|
47
|
+
constructor(private readonly fileService: FileService) {}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 公共类型
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
interface FileMeta {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
filePath: string;
|
|
58
|
+
metadata: { contentLength: string; mimeType: string };
|
|
59
|
+
downloadURL: string;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
updatedAt: string;
|
|
62
|
+
bucketID: string;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 核心操作
|
|
67
|
+
|
|
68
|
+
### 1. 上传文件
|
|
69
|
+
|
|
70
|
+
**入参:**
|
|
71
|
+
|
|
72
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
73
|
+
|------|------|------|------|
|
|
74
|
+
| `file` | `FileBody` | 是 | 文件内容(支持 `Buffer`、`ReadableStream`、`ArrayBuffer`、`Blob`、`string` 等) |
|
|
75
|
+
| `options` | `UploadOptions` | 否 | 上传选项 |
|
|
76
|
+
|
|
77
|
+
**UploadOptions:**
|
|
78
|
+
|
|
79
|
+
| 字段 | 类型 | 必需 | 说明 |
|
|
80
|
+
|------|------|------|------|
|
|
81
|
+
| `fileName` | `string` | 否 | 文件名称 |
|
|
82
|
+
| `contentType` | `string` | 否 | MIME 类型 |
|
|
83
|
+
| `cacheControl` | `string \| number` | 否 | 缓存控制 |
|
|
84
|
+
| `upsert` | `boolean` | 否 | 是否覆盖已有文件 |
|
|
85
|
+
|
|
86
|
+
**返回值:** `FileMeta`
|
|
87
|
+
|
|
88
|
+
`FileMeta.downloadURL` 可直接返回给前端展示 / 下载。业务需后台处理文件时,可同时保存 `filePath` 作内部定位;接口给前端的展示字段仍应是 `downloadURL`。
|
|
89
|
+
|
|
90
|
+
**示例:**
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// 基本上传
|
|
94
|
+
const result = await this.fileService.upload(buffer);
|
|
95
|
+
|
|
96
|
+
// 带选项上传
|
|
97
|
+
const result = await this.fileService.upload(fileBody, {
|
|
98
|
+
fileName: 'report.pdf',
|
|
99
|
+
contentType: 'application/pdf',
|
|
100
|
+
upsert: false,
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 2. 下载文件
|
|
105
|
+
|
|
106
|
+
**入参:**
|
|
107
|
+
|
|
108
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
109
|
+
|------|------|------|------|
|
|
110
|
+
| `path` | `string` | 是 | downloadURL 或文件存储路径 |
|
|
111
|
+
|
|
112
|
+
**返回值 `DownloadResult`:**
|
|
113
|
+
|
|
114
|
+
| 字段 | 类型 | 说明 |
|
|
115
|
+
|------|------|------|
|
|
116
|
+
| `content` | `Blob`(默认)/ `ReadableStream`(流式) | 文件内容 |
|
|
117
|
+
| `metadata` | `FileMeta` | 文件元信息 |
|
|
118
|
+
|
|
119
|
+
> **推荐始终使用 `.asStream()`**,避免大文件导致内存溢出。直接 `await download()` 有 50MB 限制。
|
|
120
|
+
|
|
121
|
+
**示例:**
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// 推荐:使用 downloadURL + 流式下载
|
|
125
|
+
const downloadURL = fileMeta.downloadURL; // 从上传返回值或数据库获取
|
|
126
|
+
const { content, metadata } = await this.fileService
|
|
127
|
+
.download(downloadURL)
|
|
128
|
+
.asStream();
|
|
129
|
+
|
|
130
|
+
// 也可以使用文件存储路径
|
|
131
|
+
const { content, metadata } = await this.fileService
|
|
132
|
+
.download('file.pdf')
|
|
133
|
+
.asStream();
|
|
134
|
+
|
|
135
|
+
// 不推荐:Blob 下载(限制 50MB,会将整个文件加载到内存)
|
|
136
|
+
const { content, metadata } = await this.fileService.download(downloadURL);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3. 生成临时签名 URL
|
|
140
|
+
|
|
141
|
+
服务端把应用存储中的文件/图片传给 AI capability(如 `ai-doc-parser`、`ai-image-understanding` 或 `plugin-file-url` / `plugin-image-url` 入参)时,不要把站内 download URL 直接作为插件最终入参。优先用数据库保存的 `file_path`,必要时带上 `bucket_id`。旧记录只有标准 `/app` 或 `/spark/app/.../runtime/api/v1/storage/object/<bucket>/<key>` download URL 时,先解析其中的 bucket 与 file path,再从对应 bucket 生成临时 http(s) 签名 URL。默认 `createSignedUrl()` 只使用默认 bucket;它即使能从 download URL 提取 file path,也不会使用 URL 里的 bucket。误拼 origin 的路径、非标准相对路径、前端展示 URL 或 base64 data URL 不作稳定输入。
|
|
142
|
+
|
|
143
|
+
`download()` 是服务端自己读取文件内容的接口,不是给 AI capability 准备 URL 的默认方案;需要给插件一个可访问 URL 时用 `createSignedUrl()`。
|
|
144
|
+
|
|
145
|
+
**示例:**
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// 默认 bucket
|
|
149
|
+
const signedUrl = await this.fileService.createSignedUrl(filePath, 3600);
|
|
150
|
+
|
|
151
|
+
// 指定 file_attachment 记录里的 bucket
|
|
152
|
+
const signedUrl = await this.fileService
|
|
153
|
+
.from(bucketId)
|
|
154
|
+
.createSignedUrl(filePath, 3600);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
签名失败时记录 `warn`,业务状态保持为 `failed`、`pending` 或结果字段 `null`,让前端能看到失败/待处理;禁止吞掉错误后写入“已解析成功”。
|
|
158
|
+
|
|
159
|
+
### 4. 删除文件
|
|
160
|
+
|
|
161
|
+
**入参:**
|
|
162
|
+
|
|
163
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
164
|
+
|------|------|------|------|
|
|
165
|
+
| `filePaths` | `string[]` | 是 | downloadURL 或文件存储路径数组 |
|
|
166
|
+
|
|
167
|
+
**返回值:** `FileMeta[]` - 删除成功的文件元信息数组
|
|
168
|
+
|
|
169
|
+
**示例:**
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// 推荐:使用 downloadURL
|
|
173
|
+
const result = await this.fileService.remove([fileMeta.downloadURL]);
|
|
174
|
+
|
|
175
|
+
// 也可以使用文件存储路径
|
|
176
|
+
const result = await this.fileService.remove(['file1.pdf', 'file2.png']);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 5. 获取文件元信息
|
|
180
|
+
|
|
181
|
+
**入参:**
|
|
182
|
+
|
|
183
|
+
| 参数 | 类型 | 必需 | 说明 |
|
|
184
|
+
|------|------|------|------|
|
|
185
|
+
| `filePath` | `string` | 是 | downloadURL 或文件存储路径 |
|
|
186
|
+
|
|
187
|
+
**返回值:** `FileMeta | null` - 文件元信息或 `null` 表示文件不存在或删除失败。内部已有 `filePath` 且需前端回显时,用 `getFileMetadata(filePath).downloadURL` 换出展示 URL,并随列表 / 详情接口返回。
|
|
188
|
+
|
|
189
|
+
**示例:**
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// 推荐:使用 downloadURL
|
|
193
|
+
const meta = await this.fileService.getFileMetadata(fileMeta.downloadURL);
|
|
194
|
+
|
|
195
|
+
// 后台保存 filePath 时,先换出展示 URL 再返回前端
|
|
196
|
+
const meta = await this.fileService.getFileMetadata('file.pdf');
|
|
197
|
+
const fileUrl = meta?.downloadURL;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Common Mistakes
|
|
201
|
+
|
|
202
|
+
| 错误 | 正确做法 |
|
|
203
|
+
|------|----------|
|
|
204
|
+
| 直接 `await download()` 导致内存溢出 | 始终使用 `.asStream()` 流式下载 |
|
|
205
|
+
| 把 `download()` 结果或站内 `downloadURL` 直接当作 AI capability 最终入参 | 优先用 `file_path + bucket_id` 签名;只有 download URL 时先解析 bucket 与 file path,再从对应 bucket 生成签名 URL |
|
|
206
|
+
| 忘记设置 `contentType` 导致浏览器无法预览 | 上传时明确指定 `contentType` |
|
|
207
|
+
| 直接 `new FileService()` 手动实例化 | 通过 NestJS DI 注入 `FileService` |
|
|
208
|
+
| 在异步回调中调用 `download()` 导致上下文丢失 | 在请求处理函数中立即调用,SDK 内部已处理上下文捕获 |
|
|
209
|
+
| 删除时传单个字符串 | `remove()` 参数为 `string[]` 数组 |
|
|
210
|
+
| 普通业务字段只返回 filePath,让前端二次生成展示 URL | 接口另返 `downloadURL`,前端直接消费该 URL |
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-guide
|
|
3
|
+
description: "Use when 用户要把应用能力开放给外部 Agent / 工作伙伴 / MCP 客户端调用,新增或修改 MCP 工具,给工具结果加界面(MCP Apps / ui:// 资源),维护应用 Skill(server/mcp/skills/<name>/SKILL.md,SDK 注册为 MCP Prompt),要在开发态核对、调用、调试 MCP 工具,或要把本应用的 MCP 接到自己的 Agent 客户端上使用。触发词:MCP, MCP Server, MCP 工具, MCP Apps, ui://, @McpTools, @McpTool, @McpUiResource, McpToolError, readMcpUiTemplate, tools/list, tools/call, resources/read, prompts/list, prompts/get, mcp_inspect, 开放给 Agent, 工作伙伴调用, Agent 调用应用, 应用 Skill, server/mcp, server/mcp/ui, server/mcp/skills, 接入 MCP, 连接 MCP, 怎么用这个 MCP, MCP 客户端, Claude Code, Codex, mcp add, mcp login, config.toml, X-Mcp-Token, MCP 凭证, 可用范围, 连不上。不负责 HTTP 对外开放接口(走 openapi-guide)、自动化触发器(走 trigger-guide)。"
|
|
4
|
+
steering: true
|
|
5
|
+
steering-topic: mcp_guide
|
|
6
|
+
match-template-name: nestjs-react-fullstack
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# MCP 开发与调试
|
|
10
|
+
|
|
11
|
+
应用的 MCP Server 是全栈工程里多出来的一层出口:同仓、同域名、同版本。SDK(`@lark-apaas/fullstack-nestjs-core` 重导出 `@lark-apaas/nestjs-mcp`)已经内置端点 `POST /__innerapi__/mcp`、Streamable HTTP 协议、zod 校验、身份读取、`ui://` 资源与应用 Skill(MCP Prompt)下发。你只写三种文件:工具类、界面入口、应用 Skill。
|
|
12
|
+
|
|
13
|
+
**真值顺序**:工程内 `node_modules/@lark-apaas/nestjs-mcp/dist/index.d.ts` > 本文。装饰器选项、类型名、返回值形状以它为准;本文与它冲突时按包改,不按本文改。**读它、但不从它 import**:公开 API 一律从 `@lark-apaas/fullstack-nestjs-core` 导入,直接 import 会被 ESLint 判「不在 dependencies 中」,那时改 import 源、不要加进 `dependencies`。
|
|
14
|
+
|
|
15
|
+
## 固定路径(不探测)
|
|
16
|
+
|
|
17
|
+
| 路径 | 内容 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `server/mcp/tools/<域>.tools.ts` | 工具类:一个业务域一个文件,`@McpTools()` 类 + 多个 `@McpTool()` 方法 |
|
|
20
|
+
| `server/mcp/ui/<entry>/index.html`(+ `main.tsx`)、`server/mcp/ui/tsconfig.json` | MCP Apps 界面源码:独立的浏览器代码,Vite 预设自动打成 `dist/mcp-ui/<entry>.html` |
|
|
21
|
+
| `server/mcp/skills/<name>/SKILL.md` | 应用 Skill:一个业务流程一份,给消费方 Agent 看的用法说明,SDK 注册为同名 MCP Prompt |
|
|
22
|
+
|
|
23
|
+
`server/mcp/` 不存在即为首次开通;开通只需建上述文件并把工具类注册为所属业务模块的 provider。`McpModule` 已由 `PlatformModule.forRoot()` 引入,SDK 启动时扫已加载模块的 provider 自动发现工具类——挂在已有业务模块上就不用动 `app.module.ts`,新建的模块仍要接进去,否则整个模块不会实例化、工具静默缺席;不写注册表,不写 `tools/index.ts`。
|
|
24
|
+
|
|
25
|
+
两份同名的 SKILL.md 别混:本文教你写代码,`server/mcp/skills/*/SKILL.md` 是产物、教下游 Agent 用工具。不把本文复制成应用 Skill,应用 Skill 不加 `steering: true`。
|
|
26
|
+
|
|
27
|
+
## 前置检查(一条命令)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ls -d node_modules/@lark-apaas/nestjs-mcp node_modules/@lark-apaas/coding-preset-vite-react/lib/plugins/mcp-ui.js
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- 缺 `@lark-apaas/nestjs-mcp` → 当前 `@lark-apaas/fullstack-nestjs-core` 不含 MCP 层(`1.1.62` 起含)。告知用户升级 core,停止;不装 `@modelcontextprotocol/sdk`、不手写端点。
|
|
34
|
+
- 缺 `mcp-ui.js` → 只能做工具、不能做界面(`@lark-apaas/coding-preset-vite-react` `1.0.23` 起含)。要做界面先让用户升级该预设。Rspack 预设不支持 MCP Apps。
|
|
35
|
+
|
|
36
|
+
## 任务路由
|
|
37
|
+
|
|
38
|
+
`$SKILL_DIR` 取技能清单里本 skill 的 location 所在目录。
|
|
39
|
+
|
|
40
|
+
| 任务 | 读 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| 新增 / 修改工具、开通 MCP、维护应用 Skill | `$SKILL_DIR/references/tool-authoring.md` |
|
|
43
|
+
| 给工具结果加界面、改界面、界面与工具联动 | `$SKILL_DIR/references/mcp-apps.md` |
|
|
44
|
+
| 用 `mcp_inspect` 核对与调用、按错误定位、本地模式调试 | `$SKILL_DIR/references/debugging.md` |
|
|
45
|
+
| 用户要在自己的 Agent(Claude Code / Codex / 其它客户端)里连本应用的 MCP | `$SKILL_DIR/references/client-onboarding.md` |
|
|
46
|
+
|
|
47
|
+
前三类是改代码,改完等 dev server 重启(定义有问题会在启动日志里报),再按 `$SKILL_DIR/references/debugging.md` 的回路用 `mcp_inspect` 验证。`mcp_inspect` 报「当前运行环境不支持」是本地 CLI 宿主,走该文件的本地模式。向用户汇报用业务表述(「找到 4 个演示航班」),不贴 JSON。
|
|
48
|
+
|
|
49
|
+
## 不变量
|
|
50
|
+
|
|
51
|
+
1. 一个工具对应一件用户说得出口的事(找订单、取消订单),不按库表铺增删改查;读和写分在不同工具里。
|
|
52
|
+
2. schema 用 zod,提成常量放类上方;方法签名固定 `(input: Infer<typeof In>, ctx: McpContext)`,返回 `McpToolResult<typeof Out>`。签名写错在 `npm run type:check:server` 暴露。
|
|
53
|
+
3. `description` 面向模型:用途、何时调用、参数来源、返回什么、失败怎样。每个 zod 字段 `.describe()`。消费方 Agent 只靠这些选工具、填参数。
|
|
54
|
+
4. 四项 `annotations` 显式写出。只读工具必须 `readOnlyHint: true`:缺省会被当成可能写数据,调试时每次都要 `safe: true`,消费方也会额外确认。
|
|
55
|
+
5. 声明了 `outputSchema` 就返回匹配的 `structuredContent`,对不上时 `mcp_inspect` 报「输出与其声明的 outputSchema 不符」;列表工具入参带 `limit`(默认不超过 20)与 `cursor`;写操作的幂等由 Service 保证,不引入幂等键字段。
|
|
56
|
+
6. 返回值按消费方够用来裁:`structuredContent` 只放它推理或界面渲染要用的字段。SDK 不截断,返回多少对方就吃多少,详见 `$SKILL_DIR/references/tool-authoring.md` 的「返回体积」。
|
|
57
|
+
7. 业务失败抛 `McpToolError(message, { code })`,Agent 原样收到;`message` 连下一步一起写(「订单不存在,用 order_search 按客户名重查一个有效 ID」),只陈述现象会让对方原样重试或直接放弃。其他异常只落日志、返回通用提示,Agent 看不到原因。
|
|
58
|
+
8. 身份只从 `ctx.user` 读;权限与行级过滤留在 Service。工具类作为业务模块 provider 注册,单例——请求态(当前用户、本次入参、游标)只放方法内的局部变量,写进类字段会在并发请求间串号。
|
|
59
|
+
9. 工具名 = `prefix + 方法名`,匹配 `^[A-Za-z0-9_.-]{1,128}$`,全应用唯一;用 `域_动作`。
|
|
60
|
+
10. 界面源码只放 `server/mcp/ui/<entry>/`,TS 与 ESLint 配置从 `$SKILL_DIR/assets/` 拷、不手写(见 `$SKILL_DIR/references/mcp-apps.md`)。用 `@McpUiResource` + `readMcpUiTemplate('<entry>')` 下发,工具用 `ui: { resourceUri }` 关联。
|
|
61
|
+
11. 新增、改名、删除工具后同步改对应的 `server/mcp/skills/<name>/SKILL.md`;frontmatter 的 `name` 必须等于目录名。
|
|
62
|
+
|
|
63
|
+
## 禁止
|
|
64
|
+
|
|
65
|
+
- 从入参接收用户 ID、token、连接串——身份来自网关,入参里的身份可伪造。
|
|
66
|
+
- 自己拼一个 `x-larkgw-suda-webuser` 头来让调用通过——SDK 会把它当真实用户,写工具会以这个身份落库、发通知。
|
|
67
|
+
- 把 `requireUser` 设为 `false` 绕开身份检查——SDK 只是跳过检查,方法照样执行,`ctx.user.userId` 是 undefined,跟着 `ctx.user.userId!` 一路进 Service 的权限过滤。
|
|
68
|
+
- 服务端 import `server/mcp/ui/` 里的源码,或界面 import `client/`、`server/` 里的业务代码、工具类、Node 模块——子构建的边界检查直接报错,服务端只经 `readMcpUiTemplate` 读构建产物。
|
|
69
|
+
- 界面里存凭证、直连业务系统——沙箱 iframe 是独立 origin,用户登录态带不进来;数据走宿主转发的工具调用。
|
|
70
|
+
- `McpToolError.data` 与 `structuredContent` 里放密钥、内部连接信息——两者原样进 Agent 上下文。
|
|
71
|
+
- 自己实现 MCP 协议、JSON-RPC、`/mcp` 路由、凭证校验——SDK 与网关已做,重复实现会与网关映射冲突。
|
|
72
|
+
|
|
73
|
+
## 交付前核对
|
|
74
|
+
|
|
75
|
+
1. `list_tools` 与源码里的工具、资源一一对应。
|
|
76
|
+
2. 每个工具有面向模型的 `description`、四项 `annotations`;写工具有 `outputSchema`。
|
|
77
|
+
3. 每个 `ui.resourceUri` 能 `read_resource` 读到,界面拿到 `structuredContent` 后能渲染;`npx tsc --noEmit -p server/mcp/ui/tsconfig.json` 与 `npx eslint -c eslint.mcp-ui.config.cjs server/mcp/ui` 通过。
|
|
78
|
+
4. `server/mcp/skills/` 下每个业务流程一份 SKILL.md,合起来覆盖全部工具:何时调用、如何选工具、调用顺序、约束、失败处理、界面 URI;目录接口的 `prompts[]` 与目录一一对应。
|
package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const tseslint = require('typescript-eslint');
|
|
2
|
+
const globals = require('globals');
|
|
3
|
+
const hooks = require('eslint-plugin-react-hooks');
|
|
4
|
+
const { builtinModules } = require('node:module');
|
|
5
|
+
|
|
6
|
+
module.exports = tseslint.config({
|
|
7
|
+
files: ['server/mcp/ui/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}'],
|
|
8
|
+
extends: [...tseslint.configs.recommended],
|
|
9
|
+
languageOptions: { globals: globals.browser },
|
|
10
|
+
plugins: { 'react-hooks': hooks },
|
|
11
|
+
rules: {
|
|
12
|
+
...hooks.configs.recommended.rules,
|
|
13
|
+
'no-restricted-globals': ['error', 'process', 'Buffer', '__dirname', '__filename'],
|
|
14
|
+
'no-restricted-imports': ['error', {
|
|
15
|
+
paths: [...new Set(builtinModules.flatMap(name => [name, `node:${name.replace(/^node:/, '')}`]))],
|
|
16
|
+
patterns: ['@client/*', '@server/*', '@/*', '**/client/**'],
|
|
17
|
+
}],
|
|
18
|
+
},
|
|
19
|
+
});
|