@lark-apaas/coding-steering 0.1.49-alpha.20260908071836 → 0.1.50

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 CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-steering",
3
- "version": "0.1.49-alpha.20260908071836",
3
+ "version": "0.1.50",
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
- },
12
9
  "devDependencies": {
13
10
  "markdownlint-cli": "^0.47.0"
14
11
  },
@@ -20,5 +17,8 @@
20
17
  "miaoda",
21
18
  "coding-steering"
22
19
  ],
23
- "license": "MIT"
24
- }
20
+ "license": "MIT",
21
+ "scripts": {
22
+ "lint:md": "markdownlint 'steering/**/*.md' --ignore 'steering/**/skills/**' --ignore 'steering/**/skills_common/**' --ignore 'steering/**/skills_local/**'"
23
+ }
24
+ }
@@ -83,6 +83,7 @@ available-agents:
83
83
  | 无持久化状态、纯展示或前端工具 | 纯前端 |
84
84
  | 需要数据存储、用户隔离、后端逻辑 | 全栈 |
85
85
  | 涉及文件上传与管理 | 全栈 + 文件服务 |
86
+ | 服务端图像、音视频处理(易引入 native addon) | 先确认能否改用 client 处理、平台服务或纯 JS/WASM |
86
87
  | 以飞书多维表格为数据源 | 插件优先;复杂查询场景评估是否同步至本地库 |
87
88
  | 需要飞书通知、群组、AI 能力 | 插件集成,标记插件实例需求 |
88
89
  | 需要定时或事件驱动的后台处理 | 全栈 + 自动化任务 |
@@ -113,6 +114,7 @@ available-agents:
113
114
  - 集成点:飞书、多维表格、AI、内网接口、OpenAPI、自动化触发。
114
115
  - 边界与限制:不支持的能力、外部依赖、数据权限边界。
115
116
  - 交给 Code agent 时标明需要加载的专项 skill(表格、表单、图表、插件、权限、文件、自动化)。
117
+ - 需要 native addon 时:确认它提供覆盖 linux-x64 / glibc 的预编译二进制并记录包名与版本;没有预编译二进制、装的时候要现场编译的不能用。
116
118
 
117
119
  ## Common Mistakes
118
120
 
@@ -21,6 +21,7 @@ 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 接口」。
24
25
  - 上传成功后,最重要的返回值是 `data.download_url`。需要将此URL保存到你的业务数据库中
25
26
  - **⚠️ 场景区分(重要)**:`dataloom.storage` 仅适用于需要持久化存储文件或获取 `download_url` 保存到数据库的场景。如果文件仅作为插件输入(传给 `capabilityClient`),**必须直接传 File/Blob 对象,禁止先走 dataloom 上传再传 URL**;插件调用(capability)不属于 dataloom,详见 plugin-guide
26
27
  - **download_url 格式说明**:`download_url` 返回的可能是相对路径(如 `/spark/app/.../storage/object/...`),这是正常行为。**禁止**在前面拼接 `window.location.origin` 或其他域名前缀,平台会自动解析相对路径。直接使用原始值即可。
@@ -14,6 +14,8 @@ match-template-name: nestjs-react-fullstack
14
14
 
15
15
  > **边界说明**:`authClient.session` 仅用于用户登录/登出/获取用户信息等鉴权操作。**插件调用(capability)不属于账户 SDK**,须使用独立的 `capabilityClient`(参见 plugin-guide)。
16
16
  >
17
+ > **沙箱 dev 限制**:本 skill 的能力底层打 `/app/<appId>/__runtime__/api/v1/account/*`,这类端点**在沙箱 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
+ >
17
19
  > **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
18
20
 
19
21
  ## 怎么选(决策指引)
@@ -155,8 +155,9 @@ shared/ # 前后端共享的目录
155
155
 
156
156
  ## 依赖使用规范
157
157
 
158
- 1. **子包完整性检查**:部分库有多个子包(如 `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`),添加 import 后必须确认 package.json 中包含所有需要的子包
159
- 2. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
158
+ 1. **子包完整性检查**:部分库有多个子包(如 `@radix-ui/react-*` 系列每个组件都是独立子包),添加 import 后必须确认 package.json 中包含所有需要的子包
159
+ 2. **禁止安装时需现场编译的 native addon**(`node-gyp rebuild`、`binding.gyp`、需要系统头文件或编译器)。依赖按平台内置能力 / 浏览器能力 → 纯 JS 或 WASM → 有预编译二进制的 native addon 的顺序选型
160
+ 3. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
160
161
 
161
162
  ## 文件命名约定
162
163
 
@@ -179,6 +180,36 @@ shared/ # 前后端共享的目录
179
180
  - `.eslintrc.js`、`.prettierrc`
180
181
  - `node_modules/`、`.git/`、`dist/`、`build/`
181
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
+
182
213
  ## 质量保障流程
183
214
 
184
215
  通用提交前检查(代码检查 / 接口测试 / 读日志确认无错误)见系统任务流程约束;本栈具体落点:
@@ -233,6 +264,7 @@ shared/ # 前后端共享的目录
233
264
  - **能力边界**:服务端不支持文件上传(FaaS 限制),前端用 dataloom SDK 上传,服务端仅保存元信息
234
265
  - **环境判断**:`process.env.NODE_ENV === "production"` 表示生产环境
235
266
  - **文件系统**:临时文件、下载内容和处理中间产物统一写入 `/tmp` 下的独立目录,用完后清理
267
+ - **启动关键路径**:`bootstrap`、`onModuleInit` 和 `OnApplicationBootstrap` 只做必须的同步装配,不得执行 DDL、全量数据同步、长轮询或无界外部请求。应用应尽早 `listen()`;非关键预热放到后台任务,外部调用必须有超时、错误日志和降级路径
236
268
  - **服务端运行时资源文件**(字体 / 证书 / 模板 / wasm 等需在运行时读取的非代码文件,CRITICAL — 发布后静默失效根因):① 必须在 `nest-cli.json` 的 `assets` 中声明(如 `{"include": "assets/<dir>/**/*", "outDir": "dist/server"}`),否则不会进 `dist/` 构建产物;② 路径用 `__dirname` 相对**编译产物**定位(如 `path.join(__dirname, "../assets/...")`),**禁止 `process.cwd()` 相对源码路径**——dev 跑源码能命中、发布跑 `dist/` 会落空;③ 资源缺失或加载失败必须 **fail-loud**(抛错或明确错误日志),禁止 `catch` 后静默返回残缺产物
237
269
  - **启动生命周期**:`constructor`、`onModuleInit` 和 `onApplicationBootstrap` 应快速完成,避免在其中下载大文件、全量同步或批量迁移;仅将钩子声明为 `async` 仍会被等待,无法缩短启动时间
238
270
 
@@ -475,10 +507,13 @@ await db.select().from(users).where(eq(users.adminUser, userId));
475
507
 
476
508
  - **必须从 `req.userContext` 获取**,禁止从前端传递,禁止硬编码(`const { userId } = req.userContext`)
477
509
  - 依赖用户信息的接口不要用 API 测试工具测试,用 `think` 工具思考
510
+ - 后端按 `user-identity` 和 `@lark-apaas/fullstack-nestjs-core` 的 Express `Request` 增强读取 `req.userContext`;`tenantId` 为可选 number,不能套用前端 `window.tenantId` / `viewContext` 字符串语义。
511
+ - Controller 直接用 `@Req() req: Request`;测试、mock 或业务窄类型用独立接口,只声明实际读取字段。不要继承 Express `Request` 或重声明 `userContext`;未读 `tenantId` 就省略,必须保留时用 `unknown` 或 SDK 兼容类型。
478
512
 
479
513
  ```typescript
480
- // req.userContext 字段(由身份认证中间件挂载):
481
- // - userId/tenantId/appId: string
514
+ // req.userContext 字段(由身份认证中间件挂载,完整合同见 user-identity):
515
+ // - userId/appId: string
516
+ // - tenantId: SDK Request.userContext 的可选 number
482
517
  // - env: 'preview' | 'runtime' (preview=预览态, runtime=发布运行态)
483
518
  // - userName/userNameEn: string
484
519
  // - userNameI18n: string (多语名字, 如 {zh_cn: '用户', en_us: 'user'})
@@ -555,7 +590,9 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
555
590
 
556
591
  1. 使用 API 测试工具测试不依赖用户信息的接口
557
592
  2. 积极使用 `read_logs` 工具,如无有效日志可增加 logger 打印
558
- 3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController
593
+ 3. API 测试返回 HTML 内容时:检查模块注册顺序、路由顺序、请求路径。禁止修改内置 ViewController。若路径含 `__runtime__`,那就是下一条——GET 未被代理时会落 SPA fallback 返回 index.html
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
559
596
 
560
597
  ---
561
598
 
@@ -576,6 +613,8 @@ async createArticle(@Req() req: Request, @Body() dto: CreateArticleDto) {
576
613
  {% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
577
614
  - **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
578
615
  - **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
616
+ {% else %}
617
+ - **Markdown 渲染**: `components/ui/markdown`(react-markdown + remark-gfm,内置 prose 排版)
579
618
  {% endif %}
580
619
 
581
620
  ## API 请求
@@ -613,6 +652,8 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
613
652
  {% if projectMeta['flags']['supportTiptapAndStreamdown'] %}
614
653
  | TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
615
654
  | Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
655
+ {% else %}
656
+ | Markdown | `components/ui/markdown` | Markdown 渲染(react-markdown + remark-gfm) |
616
657
  {% endif %}
617
658
 
618
659
  ### 组件 Skill 召回规则(强制执行)
@@ -776,15 +817,8 @@ return <h1>{data?.title || '未知标题'}</h1>;
776
817
  | 验证 | zod |
777
818
  | 工具函数 | lodash |
778
819
  | 样式 | clsx |
779
- | Excel | xlsx(**仅前端实现,禁止服务端实现**。解析后将结构化数据传到服务端保存) |
780
- | PDF 导出 | jspdf + html2canvas(**仅前端实现,禁止服务端实现**) |
781
- | 文件上传 | react-dropzone |
782
- | 二维码 | qrcode.react |
783
820
  | 用户反馈 | sonner |
784
- | 拖拽 | @dnd-kit/core |
785
- | 数字动画 | react-countup |
786
- | Base64 | js-base64 — `import { encode, decode } from 'js-base64'` |
787
- | 3D 场景 | cobe |
821
+
788
822
 
789
823
  ## 滚动分页最佳实践
790
824
 
@@ -23,6 +23,7 @@ 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) |
26
27
 
27
28
  ## 必读 references
28
29
 
@@ -87,12 +87,18 @@
87
87
  #### call / callStream 函数签名
88
88
 
89
89
  ```typescript
90
- .call(actionKey: string, input: object) // 非流式,返回 Promise<output>
91
- .callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
90
+ // 前端 capabilityClient(@lark-apaas/client-toolkit,实际类型来自 @lark-apaas/client-capability)
91
+ .call<T = unknown>(actionKey: string, params?: Record<string, unknown>): Promise<T> // 非流式
92
+ .callStream<T = unknown>(actionKey: string, params?: Record<string, unknown>): AsyncIterable<T> // 流式
92
93
  ```
93
94
 
94
95
  - **第一个参数 `actionKey`**:必须是字符串,值来自 `get_plugin_ai_json` 返回的 `actions[].key`(如 `'sendFeishuMessage'`、`'textGenerate'`)
95
- - **第二个参数 `input`**:必须是对象,结构符合 `actions[].inputSchema`
96
+ - **第二个参数 `params`**:类型是 `Record<string, unknown>`,结构符合 `actions[].inputSchema`
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`。**
96
102
 
97
103
  ```typescript
98
104
  // ❌ 错误:把参数 JSON.stringify 后当作 actionKey
@@ -306,23 +312,49 @@ import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
306
312
  export class XxxService {
307
313
  private readonly logger = new Logger(XxxService.name);
308
314
 
315
+ // 注入 token 必须显式写成 @Inject(CapabilityService)
309
316
  constructor(
310
- @Inject() private readonly capabilityService: CapabilityService,
317
+ @Inject(CapabilityService) private readonly capabilityService: CapabilityService,
311
318
  ) {}
312
319
  }
313
320
  ```
314
321
 
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
+
315
335
  #### 3. 调用示例
316
336
 
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
+
317
347
  ```typescript
318
348
  const inputParams = {
319
349
  // 严格按 get_plugin_ai_json.actions[].inputSchema 构造
320
350
  };
321
351
 
322
352
  try {
323
- const output = await this.capabilityService
353
+ const output: unknown = await this.capabilityService
324
354
  .load('')
325
355
  .call('', inputParams);
356
+ // 运行时收窄后再取字段,例如:
357
+ // if (typeof output === 'object' && output !== null && 'content' in output) { ... }
326
358
  return output;
327
359
  } catch (error) {
328
360
  this.logger.error('pluginInstance call failed', {
@@ -93,12 +93,14 @@ match-template-name: nestjs-react-fullstack
93
93
 
94
94
  > 完整的解析→守卫流程见 `authn-guide` skill 第二节"认证流程"。
95
95
 
96
+ 类型使用规则:Controller 直接用 `@Req() req: Request` 读取 SDK 增强的 `req.userContext`。测试、mock 或局部业务需要窄类型时,定义独立接口,只声明实际读取字段;不要继承 Express `Request`,也不要重声明平台增强的 `userContext`。未读取的 `tenantId` 省略;必须保留时用 `unknown` 或 SDK 兼容类型。
97
+
96
98
  ### 字段表
97
99
 
98
100
  | 字段 | 类型 | 说明 |
99
101
  |------|------|------|
100
102
  | `userId` | `string` | 妙搭用户 ID |
101
- | `tenantId` | `number` | 租户 ID |
103
+ | `tenantId` | `number \| undefined` | 租户 ID;按 SDK Express `Request.userContext` 增强:可选 number |
102
104
  | `appId` | `string` | 应用 ID |
103
105
  | `loginUrl` | `string` | 登录跳转 URL |
104
106
  | `userType` | `string` | 用户类型(如 `_employee`) |
@@ -14,6 +14,8 @@ match-template-name: nestjs-react-fullstack
14
14
 
15
15
  > **边界说明**:`authClient.session` 仅用于用户登录/登出/获取用户信息等鉴权操作。**插件调用(capability)不属于账户 SDK**,须使用独立的 `capabilityClient`(参见 plugin-guide)。
16
16
  >
17
+ > **本地 dev 限制**:本 skill 的能力底层打 `/app/<appId>/__runtime__/api/v1/account/*`。这类端点只有在 `npm run dev:local` 成功 `env pull` 到 `MIAODA_DEV_PLATFORM_BASE`(或 `SANDBOX_PUBLIC_URL`)时才由 dev-proxy 反代到远端沙箱;**env pull 失败或未装 lark-cli 时该反代不挂**(启动脚本只 warn 不中断),表现为 POST 404 / GET 返回 200 + index.html。`useCurrentUserProfile()` 打的是 `account/login/user`;UserSelect / DepartmentSelect / UserDisplay 打 `search_user` / `list_users` / `user_profile` / `search_department`。先确认 `.env.local` 里有 `MIAODA_DEV_PLATFORM_BASE`;确实缺失时禁止改 `business-ui` 组件或换自研控件绕过,改由服务端出数据(`req.userContext` / `AuthNPaasService`)。
18
+ >
17
19
  > **运行时边界**:本 skill 所有能力(`authClient`、`useCurrentUserProfile`、UserSelect/UserDisplay 等)仅限前端代码使用,**严禁在 `server/**` 中 import**。服务端获取用户身份用 `req.userContext` / `AuthNPaasService`(见 `user-identity` skill),完整边界规则见 coding-guide。
18
20
 
19
21
  ## 怎么选(决策指引)
@@ -129,7 +129,7 @@ shared/ # 前后端共享的目录
129
129
 
130
130
  1. 优先使用项目已有依赖,仅在无法实现时安装新依赖
131
131
  2. 使用前先查看 `package.json` 确保依赖已存在
132
- 3. **子包完整性检查**:部分库有多个子包(如 `@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`),添加 import 后必须确认 package.json 中包含所有需要的子包
132
+ 3. **子包完整性检查**:部分库有多个子包(如 `@radix-ui/react-*` 系列每个组件都是独立子包),添加 import 后必须确认 package.json 中包含所有需要的子包
133
133
  4. 用法不清时查看 readme,可进一步搜索或网页访问获取信息
134
134
 
135
135
  ## 文件命名约定
@@ -147,6 +147,39 @@ shared/ # 前后端共享的目录
147
147
  - `/api` 和 `/openapi` 代理已配置,前端 TS 严格模式已禁用,后端已启用
148
148
  - Rspack 用于构建,已配置好,**禁止修改**
149
149
 
150
+ ### 平台 runtime 接口(`__runtime__`)在本地要靠 env pull 才通
151
+
152
+ dev server 自己只代理 `/api`、`/openapi`、`/__innerapi__`。`/app/<appId>/__runtime__/*` 这一整类平台 runtime 接口走的是另一条路:`npm run dev:local` 会先跑 `lark-cli apps +env-pull` 把沙箱身份/凭证拉进 `.env.local`,其中的 `MIAODA_DEV_PLATFORM_BASE`(或 legacy `SANDBOX_PUBLIC_URL`)才让 dev-proxy 把 `__runtime__` 反代到远端沙箱。
153
+
154
+ **env pull 失败、未装 lark-cli、或 `.spark/meta.json` 缺 app_id 时,启动脚本只 warn 不中断**,反代就不会挂上。此时这些端点的症状是:**POST 返回 404;GET 落到 Vite 的 SPA fallback,返回 200 + `index.html`**(业务侧表现为「返回一坨 HTML,JSON 解析失败」)。
155
+
156
+ 常踩到的端点:
157
+
158
+ | 端点 | 方法 | 谁会打到它 |
159
+ |------|------|-----------|
160
+ | `api/v1/permissions/roles` | GET | `AppContainer` 每次加载应用自动打 |
161
+ | `api/v1/observability/{logs,traces,metrics}/collect`、`current_server_timestamp` | POST/GET | 平台埋点上报,自动打 |
162
+ | `api/v1/account/login/user` | POST | `useCurrentUserProfile()` → `authClient.session.getUserInfo()` |
163
+ | `api/v1/account/search_user`、`list_users`、`user_profile`、`search_department`、`convert_lark_user` | POST / `user_profile` 是 GET | `business-ui` 的 UserSelect / DepartmentSelect / UserDisplay |
164
+ | `api/v1/account/search`、`api/v1/account/chat/list_chats` | POST | ChatSelect 选群组件 |
165
+ | `api/v1/studio/user/profile` | GET | `getUserProfile()` |
166
+ | `api/v1/storage/object/<bucket>/pre_upload` | POST | `dataloom.storage.uploadFile()` 前端上传 |
167
+ | `api/v1/studio/plugins/tmp_files/acquire_upload_url`、`acquire_download_url` | POST | `capabilityClient` 的文件类入参 |
168
+
169
+ > `useCurrentUserProfile()` 打的是 `account/login/user`,**不是** `user_profile`/`search_user` 那一排;它另外还打一个 `GET /api/authnpaas/lark-user-id`,那条走 `/api`、被代理、正常。
170
+
171
+ 排查顺序:先看 `.env.local` 里有没有 `MIAODA_DEV_PLATFORM_BASE`,没有就重跑 `env pull`。确认拉不到时:
172
+
173
+ - **禁止**改 `client/src/components/business-ui/**`(平台保护文件)、**禁止**给组件加兜底请求或改用自研选人控件绕过——发布后这些端点是正常的,绕过写法反而是错的
174
+ - 需要验证「按当前用户过滤」「展示用户姓名」这类逻辑:改由服务端出数据。服务端的 `req.userContext` 和 `AuthNPaasService` 由网关注入,不受此限制(见 `user-identity` / `contacts-service` skill)。`useCurrentUserProfile()` 长期停在加载态就是这个原因
175
+ - 需要验证上传链路:改用接口测试直接打后端接口,或复用库里已有的文件 URL
176
+
177
+ 另外,`UserSelect` 弹层是**搜索驱动**的:不输关键词时列表为空属预期行为,不是接口挂了。
178
+
179
+ ### 服务端启动即崩、`/api` 全部 502
180
+
181
+ 报错形如 `UnknownDependenciesException: Nest can't resolve dependencies of the XxxService (?)... argument Function at index [0]` 时,先查构造函数有没有空参 `@Inject()`(见 plugin-guide),不要先去改 Module 的 `imports` / `providers`。
182
+
150
183
  ## 质量保障流程
151
184
 
152
185
  修改代码后:
@@ -335,10 +368,13 @@ await db.select().from(users).where(eq(users.adminUser, userId));
335
368
 
336
369
  - **必须从 `req.userContext` 获取**,禁止从前端传递,禁止硬编码
337
370
  - 依赖用户信息的接口不要用 API 测试工具测试,用 `think` 工具思考
371
+ - 后端按 `user-identity` 和 `@lark-apaas/fullstack-nestjs-core` 的 Express `Request` 增强读取 `req.userContext`;`tenantId` 为可选 number,不能套用前端 `window.tenantId` / `viewContext` 字符串语义。
372
+ - Controller 直接用 `@Req() req: Request`;测试、mock 或业务窄类型用独立接口,只声明实际读取字段。不要继承 Express `Request` 或重声明 `userContext`;未读 `tenantId` 就省略,必须保留时用 `unknown` 或 SDK 兼容类型。
338
373
 
339
374
  ```typescript
340
- // req.userContext 字段(由身份认证中间件挂载):
341
- // - userId/tenantId/appId: string
375
+ // req.userContext 字段(由身份认证中间件挂载,完整合同见 user-identity):
376
+ // - userId/appId: string
377
+ // - tenantId: SDK Request.userContext 的可选 number
342
378
  // - env: 'preview' | 'runtime' (preview=预览态, runtime=发布运行态)
343
379
  // - userName/userNameEn: string
344
380
  // - userNameI18n: string (多语名字, 如 {zh_cn: '用户', en_us: 'user'})
@@ -455,8 +491,7 @@ NestJS 自己不读 env。直连 NestJS 端口 → header 缺失 → `req.userCo
455
491
  - **图标**: Lucide React(唯一图标库,禁止 Emoji 和其他图标库)
456
492
  - **表格/表单/图表**: 见下方"组件 Skill 召回规则",开发前必须先调用对应 Skill
457
493
  - **用户**: 用户信息展示/选择必须用 `business-ui` 组件(阅读 README.md),禁止直接展示 userId
458
- - **富文本**: `business-ui/tiptap-editor`(阅读 README.md)
459
- - **Markdown 渲染**: `components/ui/streamdown`(内置 prose 排版)
494
+ - **Markdown 渲染**: `components/ui/markdown`(react-markdown + remark-gfm,内置 prose 排版)
460
495
 
461
496
  ## API 请求
462
497
 
@@ -489,8 +524,7 @@ import { axiosForBackend } from '@lark-apaas/client-toolkit/utils/getAxiosForBac
489
524
  | --------------------------------------------------- | --------------------------------------- | --------------------------------- |
490
525
  | Table | `@lark-apaas/client-toolkit/antd-table` | 数据表格,**先调 `/table-skill`** |
491
526
  | UserSelect/UserDisplay/UserProfile/DepartmentSelect | `business-ui/*` | 用户/部门选择展示 |
492
- | TiptapEditorComplete | `business-ui/tiptap-editor` | 富文本编辑器 |
493
- | Streamdown | `components/ui/streamdown` | Markdown/流式渲染 |
527
+ | Markdown | `components/ui/markdown` | Markdown 渲染(react-markdown + remark-gfm) |
494
528
 
495
529
  ### 组件 Skill 召回规则(强制执行)
496
530
 
@@ -635,15 +669,8 @@ return <h1>{data?.title || '未知标题'}</h1>;
635
669
  | 验证 | zod |
636
670
  | 工具函数 | lodash |
637
671
  | 样式 | clsx |
638
- | Excel | xlsx(**仅前端实现,禁止服务端实现**。解析后将结构化数据传到服务端保存) |
639
- | PDF 导出 | jspdf + html2canvas(**仅前端实现,禁止服务端实现**) |
640
- | 文件上传 | react-dropzone |
641
- | 二维码 | qrcode.react |
642
672
  | 用户反馈 | sonner |
643
- | 拖拽 | @dnd-kit/core |
644
- | 数字动画 | react-countup |
645
- | Base64 | js-base64 — `import { encode, decode } from 'js-base64'` |
646
- | 3D 场景 | cobe |
673
+
647
674
 
648
675
  ## 滚动分页最佳实践
649
676
 
@@ -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() private readonly capabilityService: CapabilityService` |
41
+ | CapabilityService 注入 | `@Inject(CapabilityService) private readonly capabilityService: CapabilityService`(**禁止空参 `@Inject()`**,会导致启动崩溃 502;业务 Module 无需 imports) |
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'`。
@@ -76,12 +76,18 @@
76
76
  #### call / callStream 函数签名
77
77
 
78
78
  ```typescript
79
- .call(actionKey: string, input: object) // 非流式,返回 Promise<output>
80
- .callStream(actionKey: string, input: object) // 流式,返回 AsyncIterable<chunk>
79
+ // 前端 capabilityClient(@lark-apaas/client-toolkit,实际类型来自 @lark-apaas/client-capability)
80
+ .call<T = unknown>(actionKey: string, params?: Record<string, unknown>): Promise<T> // 非流式
81
+ .callStream<T = unknown>(actionKey: string, params?: Record<string, unknown>): AsyncIterable<T> // 流式
81
82
  ```
82
83
 
83
84
  - **第一个参数 `actionKey`**:必须是字符串,值来自 `get_plugin_ai_json` 返回的 `actions[].key`(如 `'sendFeishuMessage'`、`'textGenerate'`)
84
- - **第二个参数 `input`**:必须是对象,结构符合 `actions[].inputSchema`
85
+ - **第二个参数 `params`**:类型是 `Record<string, unknown>`,结构符合 `actions[].inputSchema`
86
+ - 上面是**前端**签名,带泛型。**服务端 `CapabilityService` 的 `call` / `callStream` / `callStreamWithEvents` 都没有泛型**,两者不通用,详见「Server 侧调用方式」
87
+
88
+ > **`Record<string, unknown>` 不接 `interface` 声明的对象**(interface 没有隐式索引签名),直接传会撞
89
+ > `Argument of type 'XxxInput' is not assignable to parameter of type 'Record<string, unknown>'. Index signature for type 'string' is missing in type 'XxxInput'.`
90
+ > 三种正确写法:① 直接传内联对象字面量;② 入参类型用 `type XxxInput = { ... }` 而非 `interface`(type 别名有隐式索引签名);③ 已有 interface 时在调用点显式 `input as unknown as Record<string, unknown>`。**不要为此改用 `as any`。**
85
91
 
86
92
  ```typescript
87
93
  // ❌ 错误:把参数 JSON.stringify 后当作 actionKey
@@ -268,23 +274,49 @@ import { CapabilityService } from '@lark-apaas/fullstack-nestjs-core';
268
274
  export class XxxService {
269
275
  private readonly logger = new Logger(XxxService.name);
270
276
 
277
+ // 注入 token 必须显式写成 @Inject(CapabilityService)
271
278
  constructor(
272
- @Inject() private readonly capabilityService: CapabilityService,
279
+ @Inject(CapabilityService) private readonly capabilityService: CapabilityService,
273
280
  ) {}
274
281
  }
275
282
  ```
276
283
 
284
+ **禁止空参 `@Inject()`**。本 stack 的 `nest-cli.json` 用 `builder: swc`,空参 `@Inject()` 会把注入 token 置为 undefined,Nest 回退读 `design:type` 拿到 `Function`,服务端启动即崩溃、所有 `/api` 请求返回 502:
285
+
286
+ ```
287
+ UnknownDependenciesException: Nest can't resolve dependencies of the XxxService (?).
288
+ Please make sure that the argument Function at index [0] is available in the XxxModule context.
289
+ ```
290
+
291
+ 见到这段报错先查构造函数有没有空参 `@Inject()`,不要去改 Module 的 `imports` / `providers`。
292
+
293
+ **业务 Module 不需要注册 CapabilityModule**。`PlatformModule` 是 `@Global()` 且已把 `CapabilityModule` 放进 `exports`,`CapabilityService` 全局可注入。不要在业务 Module 里写 `imports: [CapabilityModule.forRoot(...)]` 或 `imports: [PlatformModule.forRoot()]`。
294
+
295
+ 同一规则适用于从 `@lark-apaas/fullstack-nestjs-core` 注入的其他平台服务(`AuthNPaasService`、`FileService` 等)。
296
+
277
297
  #### 3. 调用示例
278
298
 
299
+ 服务端 `CapabilityExecutor` 的**三个方法全都没有泛型参数**(`@lark-apaas/nestjs-capability`):
300
+
301
+ ```typescript
302
+ call(actionName: string, input: unknown, context?: Partial<PluginActionContext>): Promise<unknown>;
303
+ callStream(actionName: string, input: unknown, context?): AsyncIterable<unknown>;
304
+ callStreamWithEvents(actionName: string, input: unknown, context?): AsyncIterable<StreamEvent<unknown>>;
305
+ ```
306
+
307
+ 写 `.call<T>(...)` / `.callStream<T>(...)` / `.callStreamWithEvents<T>(...)` 都会报「应有 0 个类型参数,但获得 1 个」。泛型只存在于前端 `capabilityClient`。注意服务端 `input` 是**必传**(没有 `?`),且类型是 `unknown` 而非前端的 `Record<string, unknown>`。服务端接 `unknown` 后在运行时收窄,禁止 `as any`:
308
+
279
309
  ```typescript
280
310
  const inputParams = {
281
311
  // 严格按 get_plugin_ai_json.actions[].inputSchema 构造
282
312
  };
283
313
 
284
314
  try {
285
- const output = await this.capabilityService
315
+ const output: unknown = await this.capabilityService
286
316
  .load('')
287
317
  .call('', inputParams);
318
+ // 运行时收窄后再取字段,例如:
319
+ // if (typeof output === 'object' && output !== null && 'content' in output) { ... }
288
320
  return output;
289
321
  } catch (error) {
290
322
  this.logger.error('pluginInstance call failed', {