@lark-apaas/coding-steering 0.1.58 → 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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/steering/nestjs-react-fullstack/skills/client-builtins-file-storage-service/SKILL.md +15 -13
  3. package/steering/nestjs-react-fullstack/skills/code-fix/SKILL.md +2 -2
  4. package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +20 -5
  5. package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +46 -32
  6. package/steering/nestjs-react-fullstack/skills/design-guide/SKILL.md +0 -3
  7. package/steering/nestjs-react-fullstack/skills/design-guide/references/token-mapping.md +3 -3
  8. package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +28 -59
  9. package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +16 -4
  10. package/steering/nestjs-react-fullstack/skills/plugin-guide/references/plugin-coding-guide.md +75 -14
  11. package/steering/nestjs-react-fullstack/skills/server-builtins-file-storage-service/SKILL.md +178 -168
  12. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +78 -0
  13. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/eslint.mcp-ui.config.cjs +19 -0
  14. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/assets/ui-tsconfig.json +19 -0
  15. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/client-onboarding.md +82 -0
  16. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +81 -0
  17. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/mcp-apps.md +197 -0
  18. package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/tool-authoring.md +249 -0
  19. package/steering/nestjs-react-fullstack/skills_common/server-contacts-contract/SKILL.md +19 -0
  20. package/steering/nestjs-react-fullstack/skills_common/user-identity/SKILL.md +5 -1
  21. package/steering/nestjs-react-fullstack/skills_local/code-fix/SKILL.md +2 -2
  22. package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +18 -3
  23. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +3 -3
  24. package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/plugin-coding-guide.md +93 -2
  25. package/steering/vite-react/skills/plugin-guide/SKILL.md +2 -0
  26. package/steering/nestjs-react-fullstack/skills/design-guide/references/corporate-blueprint.md +0 -252
@@ -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 保存即可满足展示和持久化,不要优先引入后端后台任务。
@@ -336,7 +370,7 @@ Please make sure that the argument Function at index [0] is available in the Xxx
336
370
 
337
371
  Client 侧拿到用户刚选择的原始 `File` / `Blob` 时,直接传 `capabilityClient`,不要为了调用插件先上传到应用存储。文件/图片已作为业务数据持久化,且必须由 Server 侧 `CapabilityService` 调 `ai-doc-parser`、`ai-image-understanding` 或其它 `file` / `picture` / `plugin-file-url` / `plugin-image-url` 参数时,才在服务端把存储路径换成临时 http(s) URL 后传入插件。
338
372
 
339
- 优先从业务记录读取存储 `file_path`;字段类型是 `file_attachment` 时,同时读取 `bucket_id` 与 `file_path`。`createSignedUrl` 接收的是 bucket 内的 `file_path`,不要把完整的 storage object path 直接传入。标准 `/app` 或 `/spark/app/.../runtime/api/v1/storage/object/<bucket>/<key>` 原样经过平台 resolver 时可以消费;禁止给它拼接 `localhost`、`window.location.origin` 或 `/app/<app_id>`,否则会绕过内部路径解析并被当成不安全的外部 URL。非标准相对路径、前端展示 URL、非法 base64 data URL 不应作为 Server 侧插件稳定入参。
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 侧插件稳定入参。
340
374
 
341
375
  ```typescript
342
376
  import { FileService } from '@lark-apaas/fullstack-nestjs-core';
@@ -344,21 +378,51 @@ import { FileService } from '@lark-apaas/fullstack-nestjs-core';
344
378
  type StoredFileRef = {
345
379
  file_path?: string | null;
346
380
  bucket_id?: string | null;
381
+ download_url?: string | null;
347
382
  };
348
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
+
349
404
  async function createTemporaryFileUrl(
350
405
  fileService: FileService,
351
406
  file: StoredFileRef,
352
407
  ): Promise<string | null> {
353
- if (!file.file_path) return null;
354
-
355
408
  try {
356
- if (file.bucket_id) {
357
- return await fileService
358
- .from(file.bucket_id)
359
- .createSignedUrl(file.file_path, 3600);
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);
360
424
  }
361
- return await fileService.createSignedUrl(file.file_path, 3600);
425
+ return signedUrl || null;
362
426
  } catch (error) {
363
427
  // 这里不要返回伪成功 URL;由调用方把业务状态置为 failed/pending/null 等可见状态
364
428
  return null;
@@ -372,9 +436,10 @@ async function createTemporaryFileUrl(
372
436
  const docParserPluginInstanceId = '<doc-parser-plugin-instance-id>';
373
437
  const signedUrl = await createTemporaryFileUrl(this.fileService, storedFile);
374
438
  if (!signedUrl) {
375
- this.logger.warn('missing file_path or create signed url failed before AI capability call', {
439
+ this.logger.warn('missing file path or create signed url failed before AI capability call', {
376
440
  filePath: storedFile.file_path,
377
441
  bucketId: storedFile.bucket_id,
442
+ hasDownloadUrl: Boolean(storedFile.download_url),
378
443
  pluginInstanceId: docParserPluginInstanceId,
379
444
  actionKey: 'parseDocToMarkdown',
380
445
  });
@@ -389,11 +454,7 @@ const output = await this.capabilityService
389
454
  });
390
455
  ```
391
456
 
392
- 改造已有调用时:
393
-
394
- - 检查所有文档和图片插件调用点,统一使用签名函数返回的 URL。
395
- - 删除下载后转 base64 的旧逻辑,也不要把原始 URL 作为备用值传给插件。
396
- - 缺少 `file_path` 或签名失败时记录警告,写入 `failed` / `pending` / `null`,并停止调用插件。
457
+ 稳定契约:已持久化文件统一使用签名函数返回的 URL,不要先下载后转 base64,也不要把原始 URL 作为备用值传给插件。缺少可识别的文件路径、签名失败或返回空 URL 时记录警告,写入 `failed` / `pending` / `null`,并停止调用插件。
397
458
 
398
459
  #### 4. 调用示例
399
460
 
@@ -1,136 +1,144 @@
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
- | 服务端文件处理 | 下载文件到本地处理后重新上传,如图片格式转换、docx 格式转换等 |
21
- | 自动化任务生成文件 | 定时任务或事件触发后,根据数据库/插件内容生成文件并上传,如定时生成 PDF 报告、图片海报等 |
22
-
23
- ## Quick Reference
24
-
25
- | 方法 | 说明 | 返回值 |
26
- |------|------|--------|
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
+ |------|------|--------|
27
33
  | `upload(file, options?)` | 上传文件 | `FileMeta` |
28
34
  | `download(path)` | 下载文件(≤50MB) | `FileDownloadBuilder` |
29
35
  | `createSignedUrl(filePath, expiresIn)` | 默认 bucket 下生成临时 http(s) 签名 URL | `string` |
30
36
  | `from(bucketId).createSignedUrl(filePath, expiresIn)` | 指定 bucket 生成临时 http(s) 签名 URL | `string` |
31
37
  | `remove(filePaths)` | 删除文件 | `RemoveResponse` |
32
38
  | `getFileMetadata(filePath)` | 获取文件元信息 | `FileMeta` |
33
-
34
- ## 注入 FileService
35
-
36
- ```typescript
37
- import { FileService } from '@lark-apaas/fullstack-nestjs-core';
38
-
39
- @Injectable()
40
- export class MyService {
41
- constructor(private readonly fileService: FileService) {}
42
- }
43
- ```
44
-
45
- ## 公共类型
46
-
47
- ```typescript
48
- interface FileMeta {
49
- id: string;
50
- name: string;
51
- filePath: string;
52
- metadata: { contentLength: string; mimeType: string };
53
- downloadURL: string;
54
- createdAt: string;
55
- updatedAt: string;
56
- bucketID: string;
57
- }
58
- ```
59
-
60
- ## 核心操作
61
-
62
- ### 1. 上传文件
63
-
64
- **入参:**
65
-
66
- | 参数 | 类型 | 必需 | 说明 |
67
- |------|------|------|------|
68
- | `file` | `FileBody` | 是 | 文件内容(支持 `Buffer`、`ReadableStream`、`ArrayBuffer`、`Blob`、`string` 等) |
69
- | `options` | `UploadOptions` | 否 | 上传选项 |
70
-
71
- **UploadOptions:**
72
-
73
- | 字段 | 类型 | 必需 | 说明 |
74
- |------|------|------|------|
75
- | `fileName` | `string` | 否 | 文件名称 |
76
- | `contentType` | `string` | 否 | MIME 类型 |
77
- | `cacheControl` | `string \| number` | 否 | 缓存控制 |
78
- | `upsert` | `boolean` | 否 | 是否覆盖已有文件 |
79
-
80
- **返回值:** `FileMeta`
81
-
82
- **示例:**
83
-
84
- ```typescript
85
- // 基本上传
86
- const result = await this.fileService.upload(buffer);
87
-
88
- // 带选项上传
89
- const result = await this.fileService.upload(fileBody, {
90
- fileName: 'report.pdf',
91
- contentType: 'application/pdf',
92
- upsert: false,
93
- });
94
- ```
95
-
96
- ### 2. 下载文件
97
-
98
- **入参:**
99
-
100
- | 参数 | 类型 | 必需 | 说明 |
101
- |------|------|------|------|
102
- | `path` | `string` | | downloadURL 或文件存储路径 |
103
-
104
- **返回值 `DownloadResult`:**
105
-
106
- | 字段 | 类型 | 说明 |
107
- |------|------|------|
108
- | `content` | `Blob`(默认)/ `ReadableStream`(流式) | 文件内容 |
109
- | `metadata` | `FileMeta` | 文件元信息 |
110
-
111
- > **推荐始终使用 `.asStream()`**,避免大文件导致内存溢出。直接 `await download()` 50MB 限制。
112
-
113
- **示例:**
114
-
115
- ```typescript
116
- // 推荐:使用 downloadURL + 流式下载
117
- const downloadURL = fileMeta.downloadURL; // 从上传返回值或数据库获取
118
- const { content, metadata } = await this.fileService
119
- .download(downloadURL)
120
- .asStream();
121
-
122
- // 也可以使用文件存储路径
123
- const { content, metadata } = await this.fileService
124
- .download('file.pdf')
125
- .asStream();
126
-
127
- // 不推荐:Blob 下载(限制 50MB,会将整个文件加载到内存)
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,会将整个文件加载到内存)
128
136
  const { content, metadata } = await this.fileService.download(downloadURL);
129
137
  ```
130
138
 
131
139
  ### 3. 生成临时签名 URL
132
140
 
133
- 服务端把应用存储中的文件/图片传给 AI capability(如 `ai-doc-parser`、`ai-image-understanding` 或 `plugin-file-url` / `plugin-image-url` 入参)时,不要把 `downloadURL`、前端展示 URL、非标准相对路径或 base64 data URL 当稳定入参。优先用数据库保存的 `file_path`,必要时带上 `bucket_id`,生成临时 http(s) 签名 URL 后再传插件。
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 不作稳定输入。
134
142
 
135
143
  `download()` 是服务端自己读取文件内容的接口,不是给 AI capability 准备 URL 的默认方案;需要给插件一个可访问 URL 时用 `createSignedUrl()`。
136
144
 
@@ -149,52 +157,54 @@ const signedUrl = await this.fileService
149
157
  签名失败时记录 `warn`,业务状态保持为 `failed`、`pending` 或结果字段 `null`,让前端能看到失败/待处理;禁止吞掉错误后写入“已解析成功”。
150
158
 
151
159
  ### 4. 删除文件
152
-
153
- **入参:**
154
-
155
- | 参数 | 类型 | 必需 | 说明 |
156
- |------|------|------|------|
157
- | `filePaths` | `string[]` | 是 | downloadURL 或文件存储路径数组 |
158
-
159
- **返回值:** `FileMeta[]` - 删除成功的文件元信息数组
160
-
161
- **示例:**
162
-
163
- ```typescript
164
- // 推荐:使用 downloadURL
165
- const result = await this.fileService.remove([fileMeta.downloadURL]);
166
-
167
- // 也可以使用文件存储路径
168
- const result = await this.fileService.remove(['file1.pdf', 'file2.png']);
169
- ```
170
-
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
+
171
179
  ### 5. 获取文件元信息
172
-
173
- **入参:**
174
-
175
- | 参数 | 类型 | 必需 | 说明 |
176
- |------|------|------|------|
177
- | `filePath` | `string` | 是 | downloadURL 或文件存储路径 |
178
-
179
- **返回值:** `FileMeta | null` - 文件元信息或 `null` 表示文件不存在或删除失败
180
-
181
- **示例:**
182
-
183
- ```typescript
184
- // 推荐:使用 downloadURL
185
- const meta = await this.fileService.getFileMetadata(fileMeta.downloadURL);
186
-
187
- // 也可以使用文件存储路径
188
- const meta = await this.fileService.getFileMetadata('file.pdf');
189
- ```
190
-
191
- ## Common Mistakes
192
-
193
- | 错误 | 正确做法 |
194
- |------|----------|
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
+ |------|----------|
195
204
  | 直接 `await download()` 导致内存溢出 | 始终使用 `.asStream()` 流式下载 |
196
- | 把 `download()` 结果或前端 `downloadURL` 当作 AI capability URL 入参 | `createSignedUrl(filePath, 3600)` 生成临时 http(s) URL 后再传插件 |
205
+ | 把 `download()` 结果或站内 `downloadURL` 直接当作 AI capability 最终入参 | 优先用 `file_path + bucket_id` 签名;只有 download URL 时先解析 bucket 与 file path,再从对应 bucket 生成签名 URL |
197
206
  | 忘记设置 `contentType` 导致浏览器无法预览 | 上传时明确指定 `contentType` |
198
- | 直接 `new FileService()` 手动实例化 | 通过 NestJS DI 注入 `FileService` |
199
- | 在异步回调中调用 `download()` 导致上下文丢失 | 在请求处理函数中立即调用,SDK 内部已处理上下文捕获 |
200
- | 删除时传单个字符串 | `remove()` 参数为 `string[]` 数组 |
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[]` 与目录一一对应。
@@ -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
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "allowJs": true,
10
+ "checkJs": true,
11
+ "types": ["react", "react-dom"],
12
+ "skipLibCheck": true,
13
+ "noEmit": true,
14
+ "esModuleInterop": true,
15
+ "baseUrl": "../../..",
16
+ "paths": { "@shared/*": ["shared/*"] }
17
+ },
18
+ "include": ["./**/*.ts", "./**/*.tsx", "./**/*.js", "./**/*.jsx", "./**/*.mts", "./**/*.cts", "./**/*.mjs", "./**/*.cjs"]
19
+ }