@lark-apaas/coding-steering 0.1.77 → 0.1.79-alpha.20261101040601
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/anycross-forward/SKILL.md +132 -46
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -1
- package/steering/nestjs-react-fullstack/skills/plugin-guide/references/record-comments.md +81 -0
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/SKILL.md +8 -1
- package/steering/nestjs-react-fullstack/skills_common/mcp-guide/references/debugging.md +2 -4
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +28 -5
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +5 -1
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/record-comments.md +81 -0
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.79-alpha.20261101040601",
|
|
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
|
+
}
|
|
@@ -16,14 +16,14 @@ unavailable-agents:
|
|
|
16
16
|
|
|
17
17
|
# anycross-forward 内网请求转发指南
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
通过平台内置的 http-forwarder 把请求转发到客户内网地址。后端 SDK 通过 `PlatformModule.forRoot()` **自动装配**,业务零接入。**默认在 node 侧用 `HttpForwarderService.fetch()` 调用**;前端 `axiosForBackend` 直调 `/api/__platform__/http-forward` 只用于"无凭证、只读、URL 可暴露"的例外场景。
|
|
20
20
|
|
|
21
21
|
> SDK 不感知代理 / 鉴权 / 隧道。实际通路由部署环境(沙箱 mihomo + iptables)处理,生成代码里**不要**拼 `proxy_group_id` / `jwtToken` / `anycross-host`。
|
|
22
22
|
|
|
23
23
|
## 一、决策树
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
|
|
26
|
+
需要请求一个 HTTP 接口?
|
|
27
27
|
│
|
|
28
28
|
├─ 目标是平台内置 capability / Dataloom?
|
|
29
29
|
│ └─ 是 ──→ 用对应 SDK,不走 forwarder
|
|
@@ -31,29 +31,91 @@ unavailable-agents:
|
|
|
31
31
|
├─ 目标是公网公共 API(github.com 等)?
|
|
32
32
|
│ └─ 是 ──→ 直接 fetch / axios(不走 forwarder)
|
|
33
33
|
│
|
|
34
|
-
├─ 目标是客户内网 HTTP/HTTPS API
|
|
35
|
-
│
|
|
34
|
+
├─ 目标是客户内网 HTTP/HTTPS API?(默认 node 侧)
|
|
35
|
+
│ ├─ 需要凭证(API key / 内网 token) ──→ ✅ node 侧 fetch(),凭证放 env
|
|
36
|
+
│ ├─ targetUrl 不应由用户控制(固定内网系统) ──→ ✅ node 侧 fetch(),URL 写死在 service
|
|
37
|
+
│ ├─ 定时任务 / trigger / 服务端聚合 ──→ ✅ node 侧 fetch()(必然)
|
|
38
|
+
│ └─ 公开给员工的只读接口、无凭证、URL 可暴露 ──→ ✅ 前端 axiosForBackend 直调(例外)
|
|
36
39
|
│
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
├─ 二进制?
|
|
41
|
+
│ ├─ 上传(body 为 Blob / FormData / Buffer / 流) ──→ ❌ 不支持,body 仅 string / JSON
|
|
42
|
+
│ └─ 下载 ──→ ✅ 前端 `responseType: 'blob' | 'arraybuffer'`;node 侧 `res.arrayBuffer()`
|
|
43
|
+
│
|
|
44
|
+
└─ 目标是 ws:// / file://?
|
|
45
|
+
└─ 是 ──→ ❌ 不支持,仅 http:// / https://
|
|
39
46
|
```
|
|
40
47
|
|
|
48
|
+
为什么默认 node 侧:前端直调时 `targetUrl` 由浏览器传入,**任何登录用户都能借 forwarder 打任意内网 URL**,内网凭证也只能写进前端代码。node 侧 URL 写死在 service、凭证从 env 读,前端只暴露业务接口。
|
|
49
|
+
|
|
41
50
|
## 二、Quick reference
|
|
42
51
|
|
|
43
|
-
| 项 | 值
|
|
44
|
-
| ------------ |
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
| 项 | 值 |
|
|
53
|
+
| ------------ | ---------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| node 侧 SDK | `HttpForwarderService` from `@lark-apaas/nestjs-http-forwarder`,`fetch(targetUrl, init?)` 返回 Web `Response` |
|
|
55
|
+
| 端点路径 | `ALL /api/__platform__/http-forward`(前端直调例外场景) |
|
|
56
|
+
| Query 参数 | `targetUrl` (必填,仅 `http://` / `https://`) |
|
|
57
|
+
| 前端 SDK | `axiosForBackend` from `@lark-apaas/client-toolkit/utils/getAxiosForBackend` |
|
|
58
|
+
| HTTP method | 透传:GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS |
|
|
59
|
+
| Body 形态 | string / JSON-able object(axios 与 `fetch()` 均自动序列化) / null |
|
|
60
|
+
| Headers | 自动透传(剔除 hop-by-hop / accept-encoding / host / content-length) |
|
|
61
|
+
| 响应 | 上游 `status` / `headers` / `data` 原样投射;二进制下载可用 |
|
|
62
|
+
| 审计上下文 | SDK 自动注入 `x-miaoda-ctx`(base64 JSON:app_id / tenant_id / user_id / is_system_account),不可覆盖 |
|
|
63
|
+
| 默认超时 | 30000 ms(可在 `PlatformModule.forRoot({ httpForwarder })` 改) |
|
|
64
|
+
| 默认响应上限 | 10485760 bytes (10 MB) |
|
|
65
|
+
| 鉴权 | SDK **不内置**;前端直调路由由业务方加 `@NeedLogin()` 等 guard |
|
|
66
|
+
|
|
67
|
+
## 三、node 侧调用(默认)
|
|
68
|
+
|
|
69
|
+
`HttpForwarderService` 由 `PlatformModule` 全局导出,构造器直接注入即可,**不要**在用户 `AppModule` 里 import `HttpForwarderModule`。
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// server/src/modules/corp-api/corp-api.service.ts
|
|
73
|
+
import { Injectable } from "@nestjs/common";
|
|
74
|
+
import { HttpForwarderService } from "@lark-apaas/nestjs-http-forwarder";
|
|
75
|
+
|
|
76
|
+
const BASE = "http://api.corp.com/v1"; // URL 写死在 service,不接受前端传入
|
|
77
|
+
|
|
78
|
+
@Injectable()
|
|
79
|
+
export class CorpApiService {
|
|
80
|
+
constructor(private readonly forwarder: HttpForwarderService) {}
|
|
81
|
+
|
|
82
|
+
// GET — 带内网凭证,凭证只从 env 读、只在 node 侧出现
|
|
83
|
+
async listOrders() {
|
|
84
|
+
const res = await this.forwarder.fetch(`${BASE}/orders`, {
|
|
85
|
+
headers: { "x-api-key": process.env.CORP_API_KEY! },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
// 上游 4xx/5xx 不抛错(ok=false);失败也要读完 body 再 throw,
|
|
89
|
+
// 否则连接会占用到 requestTimeoutMs(默认 30s)才释放
|
|
90
|
+
throw new Error(`corp api ${res.status}: ${await res.text()}`);
|
|
91
|
+
}
|
|
92
|
+
return res.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// POST — object body 自动 JSON.stringify 并补 content-type: application/json
|
|
96
|
+
async createOrder(payload: { qty: number }) {
|
|
97
|
+
const res = await this.forwarder.fetch(`${BASE}/orders`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: payload,
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) throw new Error(`corp api ${res.status}: ${await res.text()}`);
|
|
102
|
+
return res.json();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `fetch()` 返回 Web `Response`(`.json()` / `.text()` / `.arrayBuffer()`);上游 4xx/5xx 返回 `ok=false` **不抛**;forwarder 自身错(非法 URL / 上游不可达 / 超时 / 非法 header / 不支持的 body)抛 `HttpException`,`getResponse()` 为 `{ code, message }`。
|
|
108
|
+
- body 仅 `string` / JSON-able object / `null`;Buffer / FormData / URLSearchParams / Blob / 流会被拒绝为 400 `INVALID_REQUEST`。
|
|
109
|
+
- 不需要响应内容时 `await res.body?.cancel()`,否则连接占用到超时。
|
|
110
|
+
- **低层路径**:`forward(dto)` 返回 `{ status, headers, body: Readable }`,配 `toWebResponse(out)` 转成 Web `Response`。仅在自定义 controller 需要把上游流 pipe 给下游等流式场景使用,普通业务一律 `fetch()`。
|
|
111
|
+
|
|
112
|
+
**禁止在 node 侧用裸 `fetch` / `axios` 打内网**,三个原因:
|
|
113
|
+
|
|
114
|
+
1. Node 内置 fetch 不读 `HTTP_PROXY`,请求不会走出网代理;
|
|
115
|
+
2. 代理鉴权在首次 `forward()` / `fetch()` 才 lazy 初始化,绕过 SDK 就没有鉴权;
|
|
116
|
+
3. 不会带 `x-miaoda-ctx` 审计头,内网侧无法审计调用方。
|
|
117
|
+
|
|
118
|
+
## 四、前端直调(例外:无凭证、只读、URL 可暴露)
|
|
57
119
|
|
|
58
120
|
```ts
|
|
59
121
|
// client/src/api/corp-api.ts
|
|
@@ -62,7 +124,7 @@ import { axiosForBackend } from "@lark-apaas/client-toolkit/utils/getAxiosForBac
|
|
|
62
124
|
const FORWARD = "/api/__platform__/http-forward";
|
|
63
125
|
const BASE = "http://api.corp.com/v1";
|
|
64
126
|
|
|
65
|
-
// GET —
|
|
127
|
+
// GET — 拉公开只读列表
|
|
66
128
|
export async function fetchUsers() {
|
|
67
129
|
const res = await axiosForBackend.get(FORWARD, {
|
|
68
130
|
params: { targetUrl: `${BASE}/users` },
|
|
@@ -70,26 +132,28 @@ export async function fetchUsers() {
|
|
|
70
132
|
return res.data; // 上游真实响应 data
|
|
71
133
|
}
|
|
72
134
|
|
|
73
|
-
// POST —
|
|
135
|
+
// POST — body 直接传对象,自定义 headers 自动透传上游
|
|
74
136
|
export async function createOrder(payload: { qty: number }) {
|
|
75
137
|
const res = await axiosForBackend.post(FORWARD, payload, {
|
|
76
138
|
params: { targetUrl: `${BASE}/orders` },
|
|
77
|
-
headers: { "X-Tenant": "demo" },
|
|
139
|
+
headers: { "X-Tenant": "demo" },
|
|
78
140
|
});
|
|
79
141
|
return res.data;
|
|
80
142
|
}
|
|
81
143
|
|
|
82
|
-
//
|
|
83
|
-
export async function
|
|
84
|
-
await axiosForBackend.
|
|
85
|
-
params: { targetUrl: `${BASE}/
|
|
144
|
+
// 二进制下载 — axios 整包接收
|
|
145
|
+
export async function downloadReport(id: string) {
|
|
146
|
+
const res = await axiosForBackend.get(FORWARD, {
|
|
147
|
+
params: { targetUrl: `${BASE}/reports/${id}.pdf` },
|
|
148
|
+
responseType: "blob",
|
|
86
149
|
});
|
|
150
|
+
return res.data as Blob;
|
|
87
151
|
}
|
|
88
152
|
```
|
|
89
153
|
|
|
90
|
-
##
|
|
154
|
+
## 五、错误处理模板(前端)
|
|
91
155
|
|
|
92
|
-
> ⚠️ 模板的 `GlobalExceptionFilter`(`server/common/filters/exception.filter.ts`)会把 forwarder 抛的 `HttpException` 重新打包为 `{ error: { code: 'BAD_REQUEST' | 'BAD_GATEWAY' | ..., message, details } }`:顶层 `error.code` 是 HTTP 状态映射,**forwarder 自己的业务码(`INVALID_REQUEST` / `UPSTREAM_TIMEOUT` 等)被 `JSON.stringify` 进 `error.details`**,需要 `JSON.parse` 才能拿到。
|
|
156
|
+
> ⚠️ 模板的 `GlobalExceptionFilter`(`server/common/filters/exception.filter.ts`)会把 forwarder 抛的 `HttpException` 重新打包为 `{ error: { code: 'BAD_REQUEST' | 'BAD_GATEWAY' | ..., message, details } }`:顶层 `error.code` 是 HTTP 状态映射,**forwarder 自己的业务码(`INVALID_REQUEST` / `UPSTREAM_TIMEOUT` 等)被 `JSON.stringify` 进 `error.details`**,需要 `JSON.parse` 才能拿到。node 侧 `fetch()` 直接 catch `HttpException`,`err.getResponse()` 即 `{ code, message }`,无需解包。
|
|
93
157
|
|
|
94
158
|
```ts
|
|
95
159
|
import { axiosForBackend } from "@lark-apaas/client-toolkit/utils/getAxiosForBackend";
|
|
@@ -123,7 +187,7 @@ export async function safeForward(targetUrl: string) {
|
|
|
123
187
|
switch (inner?.code) {
|
|
124
188
|
case "INVALID_REQUEST":
|
|
125
189
|
case "INVALID_TARGET_PROTOCOL":
|
|
126
|
-
// targetUrl 缺失 / 格式非法 / 协议非 http(s)
|
|
190
|
+
// targetUrl 缺失 / 格式非法 / 协议非 http(s) / body 或 header 不合法
|
|
127
191
|
return { ok: false, reason: "targetUrl 配置错误" };
|
|
128
192
|
case "UPSTREAM_TIMEOUT":
|
|
129
193
|
return { ok: false, reason: "上游 30s 超时" };
|
|
@@ -133,6 +197,13 @@ export async function safeForward(targetUrl: string) {
|
|
|
133
197
|
return { ok: false, reason: inner.message ?? "上游不可达" };
|
|
134
198
|
case "RESPONSE_TOO_LARGE":
|
|
135
199
|
return { ok: false, reason: "上游响应 > 10MB" };
|
|
200
|
+
case "MISSING_APP_ID":
|
|
201
|
+
case "APP_ID_MISMATCH":
|
|
202
|
+
case "PROXY_TOKEN_FETCH_FAILED":
|
|
203
|
+
case "PROXY_CMD_EXEC_FAILED":
|
|
204
|
+
// 500:内网通道平台侧故障(代理鉴权 / 沙箱 appId 环境问题),
|
|
205
|
+
// 不是业务代码的错——提示用户联系平台,不要重试、不要改代码绕过
|
|
206
|
+
return { ok: false, reason: "内网通道暂不可用,请联系平台" };
|
|
136
207
|
default:
|
|
137
208
|
throw err;
|
|
138
209
|
}
|
|
@@ -140,41 +211,56 @@ export async function safeForward(targetUrl: string) {
|
|
|
140
211
|
}
|
|
141
212
|
```
|
|
142
213
|
|
|
143
|
-
##
|
|
214
|
+
## 六、内网侧看到的调用方信息
|
|
215
|
+
|
|
216
|
+
所有经 forwarder 出站的请求(node 侧 `fetch()` / `forward()`、前端直调)都自动带 `x-miaoda-ctx` 头:标准 base64 编码的 UTF-8 JSON,四个 key 永远存在,缺省 `""` / `false`:
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{ "app_id": "app_xxx", "tenant_id": "1234", "user_id": "ou_abc", "is_system_account": false }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`user_id` 匿名访问为 `""`;trigger / 定时任务场景 `is_system_account` 为 `true`。内容是 **app 自述、SDK 不签名**,下游只用于审计("是哪个 app / 租户 / 用户发起的"),**权威 app 身份以代理层鉴权结果为准**,不要拿此 header 做授权决策。业务代码对它完全透明:不需要读、不能改。
|
|
223
|
+
|
|
224
|
+
## 七、生成代码必须遵守的 7 条
|
|
144
225
|
|
|
145
|
-
1.
|
|
146
|
-
2.
|
|
147
|
-
3. **服务端零接入**——`PlatformModule.forRoot()` 已自动装配 `HttpForwarderModule
|
|
148
|
-
4. **前端只用 `axiosForBackend`**,它已带 401 → `x-login-url`
|
|
149
|
-
5. **响应原样投射**:`res.status` / `res.headers` / `res.data`
|
|
226
|
+
1. **内网调用默认在 node 侧**:注入 `HttpForwarderService` 用 `fetch()`,URL 写死在 service、凭证只从 `process.env` 读;前端直调仅限无凭证只读接口。
|
|
227
|
+
2. **前端直调路径恒为 `/api/__platform__/http-forward`**,`targetUrl` 永远放 `params`,不要拼到 URL 字符串里、不要放 body、不要放 header。
|
|
228
|
+
3. **服务端零接入**——`PlatformModule.forRoot()` 已自动装配 `HttpForwarderModule` 并全局导出 service,**不要**在用户 `AppModule` 里再写 `HttpForwarderModule.forRoot()`。
|
|
229
|
+
4. **前端只用 `axiosForBackend`**,它已带 401 → `x-login-url` 自动跳转拦截器,**不要**再实现登录跳转;**node 侧只用 `HttpForwarderService`**,不要裸 `fetch` / `axios`。
|
|
230
|
+
5. **响应原样投射**:`res.status` / `res.headers` / `res.data` 都来自上游真实响应,上游 4xx/5xx 不是转发失败。forwarder 自身错在前端被模板 `GlobalExceptionFilter` 包成 `error.response.data.error`,业务码需从 `error.details`(JSON 字符串)里 parse 出来,详见第五节。
|
|
150
231
|
6. **headers 直接走 `headers: {...}`**,无需手剔 `host` / `content-length` / hop-by-hop,service 端会处理。
|
|
232
|
+
7. **不要手动设置 `x-miaoda-ctx`**,SDK 会无条件覆盖;也不要在业务代码里读它做授权。
|
|
151
233
|
|
|
152
|
-
##
|
|
234
|
+
## 八、Common Mistakes
|
|
153
235
|
|
|
154
236
|
| ❌ 错误 | ✅ 正确 |
|
|
155
237
|
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
238
|
+
| node 侧用裸 `fetch(url)` / `axios.get(url)` 打内网 | 注入 `HttpForwarderService` 用 `fetch()`(裸 fetch 不走代理、无鉴权、无审计头) |
|
|
239
|
+
| 前端直调带内网 API key / token,或让用户输入 `targetUrl` | 改 node 侧 `fetch()`:URL 写死在 service,凭证从 `process.env` 读 |
|
|
156
240
|
| `axiosForBackend.post('/api/__platform__/http-forward?targetUrl=...')` 手拼 query | `params: { targetUrl }`,让 axios 做 URL 编码 |
|
|
157
241
|
| `axiosForBackend.post(targetUrl, body)` 把 targetUrl 当路径 | 路径永远 `/api/__platform__/http-forward`,targetUrl 走 `params` |
|
|
158
242
|
| 用裸 `axios.get('/api/__platform__/http-forward', ...)`(直接 import 'axios') | 用 `axiosForBackend`(带 baseURL / 401 拦截器) |
|
|
159
|
-
| body 传 `FormData` / `Blob` /
|
|
243
|
+
| body 传 `FormData` / `Blob` / `Buffer` / 流做上传 | 当前仅支持 string / JSON-able / null,否则 400 `INVALID_REQUEST`;二进制上传先 base64 成字符串,下载用 `responseType: 'blob'` |
|
|
160
244
|
| 上游 4xx/5xx 被当作转发失败 | 上游响应**原样投射**到 `res`;只有 forwarder 自身错才进 catch,业务码在 `error.response.data.error.details`(需 `JSON.parse`) |
|
|
245
|
+
| node 侧 `fetch()` 失败分支不读 body 直接 throw | 先 `await res.text()`(或 `await res.body?.cancel()`)再 throw,否则连接占用到超时 |
|
|
161
246
|
| 用户 `AppModule` 手写 `HttpForwarderModule.forRoot()` | 已被 `PlatformModule` 自动装配,重复注册冲突 |
|
|
162
247
|
| 拼 `?targetUrl=...&proxy_group_id=xxx&jwt=yyy` 等自定义参数 | 部署环境接管路由,SDK 不感知;唯一参数是 `targetUrl` |
|
|
163
248
|
| `targetUrl: 'ws://...'` / `'file://...'` | 仅 `http://` / `https://`,否则 `INVALID_TARGET_PROTOCOL` |
|
|
164
249
|
| 在生成代码里手动加 `Authorization` 模拟 anycross | 部署环境处理鉴权,业务代码只关心**业务**header |
|
|
165
|
-
|
|
|
250
|
+
| 手拼 `x-miaoda-ctx` header | 无效:SDK 无条件覆盖;审计上下文由 SDK 从请求上下文自动生成 |
|
|
251
|
+
| 收到 500 `PROXY_*` / `*_APP_ID*` 错误后重试或改代码绕过 | 是内网通道平台侧故障,提示用户联系平台 |
|
|
166
252
|
|
|
167
|
-
##
|
|
253
|
+
## 九、当前限制
|
|
168
254
|
|
|
169
|
-
- **Body**:仅 `string` / JSON-able object / `null`;不支持 binary / FormData /
|
|
170
|
-
-
|
|
255
|
+
- **Body**:仅 `string` / JSON-able object / `null`;不支持 binary / FormData / 流上传
|
|
256
|
+
- **响应**:二进制下载 OK(流式 pipe 到下游);前端 axios 整包接收,不承诺 SSE 式流消费;node 侧流式只能走 `forward()` 低层路径
|
|
171
257
|
- **不做请求重试**(透传语义,重试由业务方决定)
|
|
172
258
|
- **超时 / 响应上限可配**:业务方可在 `PlatformModule.forRoot({ httpForwarder: { requestTimeoutMs, maxResponseBytes } })` 处覆盖默认值
|
|
173
|
-
- **错误码 contract
|
|
259
|
+
- **错误码 contract**:400 `INVALID_REQUEST` / `INVALID_TARGET_PROTOCOL`;502 `UPSTREAM_UNREACHABLE` / `RESPONSE_TOO_LARGE`;504 `UPSTREAM_TIMEOUT`;500 `MISSING_APP_ID` / `APP_ID_MISMATCH` / `PROXY_TOKEN_FETCH_FAILED` / `PROXY_CMD_EXEC_FAILED`(平台侧故障)
|
|
174
260
|
|
|
175
|
-
##
|
|
261
|
+
## 十、相关
|
|
176
262
|
|
|
177
|
-
- 后端 SDK
|
|
263
|
+
- 后端 SDK:`@lark-apaas/nestjs-http-forwarder`(导出 `HttpForwarderService` / `toWebResponse` / `MIAODA_CTX_HEADER` / `MiaodaCtxPayload` / `ForwardFetchInit`)
|
|
178
264
|
- 前端 SDK:`axiosForBackend` from `@lark-apaas/client-toolkit/utils/getAxiosForBackend`
|
|
179
265
|
- 自动装配点:`@lark-apaas/nestjs-core` 的 `PlatformModule`
|
|
180
|
-
- 鉴权约束:参见 `user-identity`——forwarder
|
|
266
|
+
- 鉴权约束:参见 `user-identity`——forwarder 不内置鉴权,前端直调路由的 `@NeedLogin()` 仍生效
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plugin-guide
|
|
3
|
-
description: 飞书/AI 新版插件集成规范与 Plugin 链路指南——**给生成出来的应用写运行时代码用**,让应用跑起来后能访问飞书多维表格/Base、发送飞书消息、创建飞书群组,以及 AI 智能生文/生图/图片理解等。Use when 需要:(1) 创建或管理 PluginInstance 插件实例,(2) 调用 capabilityClient/CapabilityService 生成插件调用代码,(3) 理解 Plugin、PluginInstance、PluginInstanceAIJson 三层关系,(4) 使用 get_plugin_ai_json 或 plugin_instance 工具,(5) 排查插件相关报错或异常行为(调用报错、通知/消息发送失败、配置项不生效、参数类型与预期不符等)。NOT for Agent 自己在生成期读取飞书资源内容——用户给了多维表格/文档/表格/知识库链接、要求先读懂再建应用时,那是 lark-cli skill 的场景,用 bash 调 lark-cli 读;插件是写进应用代码、部署后由应用运行时调用的,不能拿来当 Agent 的读取工具。触发词:插件, plugin, 运行时访问飞书, 应用内发飞书消息, 应用内写多维表格, 飞书群组, AI生文, AI生图, 图片理解, capabilityClient, CapabilityService, pluginInstance, 插件报错, 配置不生效,
|
|
3
|
+
description: 飞书/AI 新版插件集成规范与 Plugin 链路指南——**给生成出来的应用写运行时代码用**,让应用跑起来后能访问飞书多维表格/Base、发送飞书消息、创建飞书群组,以及 AI 智能生文/生图/图片理解等。Use when 需要:(1) 创建或管理 PluginInstance 插件实例,(2) 调用 capabilityClient/CapabilityService 生成插件调用代码,(3) 理解 Plugin、PluginInstance、PluginInstanceAIJson 三层关系,(4) 使用 get_plugin_ai_json 或 plugin_instance 工具,(5) 排查插件相关报错或异常行为(调用报错、通知/消息发送失败、配置项不生效、参数类型与预期不符等)。NOT for Agent 自己在生成期读取飞书资源内容——用户给了多维表格/文档/表格/知识库链接、要求先读懂再建应用时,那是 lark-cli skill 的场景,用 bash 调 lark-cli 读;插件是写进应用代码、部署后由应用运行时调用的,不能拿来当 Agent 的读取工具。触发词:插件, plugin, 运行时访问飞书, 应用内发飞书消息, 应用内写多维表格, 飞书群组, AI生文, AI生图, 图片理解, capabilityClient, CapabilityService, pluginInstance, 插件报错, 配置不生效, 参数类型不匹配, 记录评论组件, 评论回复, 表情回应, 云文档引用
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: plugin_guide
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -166,6 +166,10 @@ const structured = await capabilityClient
|
|
|
166
166
|
|
|
167
167
|
**创建 `ai-image-to-json` 实例时**:必须一次性定义**所有**需提取字段(参考数据库 schema / 表单定义 / UI 设计),宁多勿漏;字段类型仅支持 String/Number/Boolean,最多 20 个;先调 `get_plugin_ai_json` 确认上游插件的 `outputSchema` 确保输入格式正确。
|
|
168
168
|
|
|
169
|
+
## 多维表格评论组件
|
|
170
|
+
|
|
171
|
+
需要记录评论/回复、编辑删除、@人员、表情回应或云文档引用时,复用项目内 `@/components/business-ui/record-comments` 的 `CommentPanel`;**先读 [评论组件接入指引](references/record-comments.md)** 和组件 `README.md`。组件保持现有样式并管理乐观更新、失败重试与分页触发;真实接口、权限、缓存与分页游标由宿主提供。图片未接入插件,不默认启用。
|
|
172
|
+
|
|
169
173
|
## 多维表格数据架构
|
|
170
174
|
|
|
171
175
|
应用需读取飞书多维表格数据时,按查询模式选架构(完整对照表见 `references/table.md`「数据架构选择」)。**快捷判断**:「仪表盘/dashboard/驾驶舱/看板/统计」→ 必须 `aggregateQuery`(禁止 searchRecords 全量拉取后内存计算);「列表/明细/详情」→ `searchRecords` 分页;「排行榜/TOP N」→ `searchRecords` + sort + pageSize=N;需要把 Base 当运行数据源单向读、且数据量大到要分片/多次分页或查询复杂/需持续同步 → 先用 `lark-cli apps +db-sync-create` 把 Base 表同步成应用数据库表(streaming 持续增量 / batch 只导一次),页面查应用库;这是全栈应用的方案,配置格式和 `--preview`→`--yes` 流程见 lark-apps-db skill;跨表关联或复杂计算 → 应用数据库 + 同步。
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 多维表格评论组件(Record comments)
|
|
2
|
+
|
|
3
|
+
需要记录评论、新增/回复、编辑/删除、@人员、表情回应或云文档引用时,先读项目内 `client/src/components/business-ui/record-comments/README.md` 与 `comment-panel.contract.ts`,复用 `CommentPanel`,不要重新实现编辑器、弹层或乐观更新。此组件随 **nestjs-react-fullstack** 模板分发;旧应用若没有该目录,需要先升级/同步组件及依赖,不能只复制入口文件。本指引不适用于缺少该资产的其他技术栈。
|
|
4
|
+
|
|
5
|
+
## 接入边界
|
|
6
|
+
|
|
7
|
+
- 从 `@/components/business-ui/record-comments` 导入 `CommentPanel` 和公共类型;所有内部文件、CSS、`assets/reaction-sprite.png` 必须保留。Tiptap 依赖及 `@floating-ui/dom` 已由模板配置,不混装 Tiptap 大版本。
|
|
8
|
+
- 组件管理显示、回复归属、编辑器、人员弹层、发送等待/失败/重试以及乐观更新;**评论 CRUD、插件实例、用户身份、读缓存、分页游标、表情及文档请求全部由页面/API hook 提供**。不要把插件凭据、实例 ID、表格配置或应用 ID 写进组件内部。
|
|
9
|
+
- `createAppCommentSource(scopeKey)` 是可选的模板人员目录适配器,只识别妙搭人员 ID;其他目录实现 `CommentDataSource`,不要把飞书 ID 冒充妙搭 ID。默认人员目录不提供空查询推荐;宿主可提供 `recommend`,例如从当前讨论参与者解析推荐列表。
|
|
10
|
+
- `scopeKey` 必须覆盖应用/租户/登录身份/插件实例/数据源与权限上下文,以上任一变化都应更换;`recordID` 切换会重建编辑会话。`mentionSource` 用 `useMemo` 稳定,避免每次渲染重建搜索和缓存。
|
|
11
|
+
|
|
12
|
+
## 真实接口先核对
|
|
13
|
+
|
|
14
|
+
按本 skill 的插件元数据流程拿到当前实例真实 Action schema。以下是已联调版本的映射,**实际 actionKey、字段和响应层级始终以当前元数据为准**,缺能力则隐藏该功能,不伪造成功、不回退 mock。
|
|
15
|
+
|
|
16
|
+
| 宿主能力 | 已联调 Action / 输入约定 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| 评论读取 | `listRecordComments`,传 `recordID`,需要回应时传 `needReaction:true`;按 `hasMore/pageToken` 加载下一页 |
|
|
19
|
+
| 新增/回复 | `addRecordComment`;新评论不传 `commentID`,回复保留原 `commentID`,返回真实 `commentID/replyID` |
|
|
20
|
+
| 编辑 | `updateRecordComment`;传 `recordID/commentID/replyID/content`,回读并传入回复级 `updateTime` |
|
|
21
|
+
| 删除 | `deleteRecordComment`;消息菜单必须传选中消息的 `replyID`(含首条),只删该消息;第二条顺位成为正文,其余回复保留。省略 replyID 仅兼容显式整串删除,按 `commentDeleted` 更新缓存 |
|
|
22
|
+
| 回应读取/写入 | `getRecordCommentReactions` / `updateRecordCommentReaction`;写入用显式 `add/delete`,组件回调的 `remove` 需映射成插件的 `delete` |
|
|
23
|
+
| 云文档推荐/搜索/解析 | `getCommentDocuments`;外部实现 `CommentDocumentSource`,用 `withDocumentSource(people, documents)` 合并,显式传 `enableDocuments` |
|
|
24
|
+
|
|
25
|
+
每条 `comments[]` 是讨论,`replies[0]` 是正文,其余 replies 是同一讨论下的消息;不要把回复拆成独立 comment。`commentID/replyID` 始终是字符串。`createTime/updateTime` 是 Unix **秒**,排序取创建时间,展示取 `updateTime ?? createTime`;缺更新时间不能伪造“编辑过”。`mention.userID` 缺失时保留缺失引用,不猜人。
|
|
26
|
+
|
|
27
|
+
## 受控组件的宿主实现
|
|
28
|
+
|
|
29
|
+
1. 宿主发起首屏读取,维护 `comments/loading/error`,传 `onRetry`。分页可全量拉取后不给 `pagination`,也可传 `{hasMore, loading, error, onLoadMore}`;组件接近底部会自动调用,宿主用自己的游标追加数据,追加后再 resolve,失败必须 reject。游标不前进时终止并报错。
|
|
30
|
+
2. 宿主提供 `onAddComment/onUpdateComment/onDeleteComment`。写成功后立即返回真实 ID/删除结果,再单独启动回读;**不能把后续回读失败当成写失败**,否则组件重试可能重复发表。不需要在宿主再插一份临时评论,也不必等待回读才关闭编辑器。
|
|
31
|
+
3. 编辑/删除必须显式传 `getReplyPermissions`,未提供就隐藏。前端作者判断只控制菜单,服务端仍检查当前用户权限;不能用 owner 身份补足。点击消息菜单“删除”直接发起请求,不追加二次确认;删除首条也只删它本身,不能扩大到其他人的回复;只有最后一条消失时移除空讨论。
|
|
32
|
+
4. 普通 `Error` 仅用于确认未应用的失败;超时/连接中断等可能已提交时,抛 `Object.assign(new Error(message), {outcome:'unknown'})`,组件保留内容并阻止盲目重试,宿主先刷新核对。只有请求尚未发送等已确认未写入的断网失败才标记 `not-applied`。
|
|
33
|
+
5. 宿主缓存要保护读取顺序,取消/忽略旧请求,切换身份时隔离缓存;组件在匹配快照接管后无法判断一个没有版本号的旧数组。不要通过延迟 toast、二次 loading 或本地时间 Map 掩盖回读错误。
|
|
34
|
+
|
|
35
|
+
最小受控接入(`read` 和 `commands` 是应用自己的真实 API hook,不是组件内部接口):
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useMemo } from 'react';
|
|
39
|
+
import {
|
|
40
|
+
CommentPanel, createAppCommentSource,
|
|
41
|
+
type CommentPanelProps,
|
|
42
|
+
} from '@/components/business-ui/record-comments';
|
|
43
|
+
|
|
44
|
+
type Props = Pick<CommentPanelProps,
|
|
45
|
+
'scopeKey' | 'recordID' | 'comments' | 'currentUserID' |
|
|
46
|
+
'loading' | 'error' | 'onRetry' | 'pagination' |
|
|
47
|
+
'onAddComment' | 'onUpdateComment' | 'onDeleteComment' |
|
|
48
|
+
'getReplyPermissions'>;
|
|
49
|
+
|
|
50
|
+
export function RecordComments(props: Props) {
|
|
51
|
+
const mentionSource = useMemo(
|
|
52
|
+
() => createAppCommentSource(props.scopeKey), [props.scopeKey],
|
|
53
|
+
);
|
|
54
|
+
return <CommentPanel {...props} mentionSource={mentionSource} />;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
真实调用继续使用 `capabilityClient.load(instanceID).call(actionKey, input)`,不要在组件里调用 CLI 或直接手写 OAuth;`call()` 的结果已由 SDK 解包,按实际 schema 使用,不要重复套 `data.output`。写入回调须返回合同中的真实结果,不能返回 `void`。
|
|
59
|
+
|
|
60
|
+
## 可选能力与权限
|
|
61
|
+
|
|
62
|
+
- **表情回应**:传 `reactions.getReactions/onChange`,可选 `canChange/loadUsers`。列表省略 reactions 表示未知,不能当作无人回应,`mine` 也不能根据截断的 `userIDs` 推断。宿主按回复串行写入,成功先更新当前回应缓存,再刷新;失败只回退该目标,刷新失败不能回退已提交动作。读取不到权威 membership 时禁用切换,保留历史表情键(如 `MediumThumbsup`)。
|
|
63
|
+
- **云文档引用**:搜索候选的 `description` 使用接口的 `editTime`(Unix 秒)格式化为“最近修改:10月26日 20:41”等元数据副标题;缺失时间就省略,不伪造。只查询当前用户可读的元数据,搜索与解析都应检查权限;不得以 owner/其他用户补查私密标题,插入引用不更改分享权限。写入是 `{type:'link',url}`;持久化 URL,不把标题/图标当作请求字段。请求被取消后检查 signal,丢弃过期结果。解析不可读时使用组件的灰色引用状态,不能假称可读。
|
|
64
|
+
- **图片**:当前多维表格插件未接入图片评论,不传 `images`,不展示上传入口。`images` 仅是宿主自备上传/持久化能力时的独立扩展;不得把 File/blob URL 塞入插件 content。
|
|
65
|
+
- **人员卡片/链接**:通过 `onMentionClick/getCommentLink` 由宿主承接;未提供对应回调时不展示无效操作。
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## 交互保持约束
|
|
69
|
+
|
|
70
|
+
复用完整组件及其状态流,不在宿主外包一层“增强交互”。
|
|
71
|
+
|
|
72
|
+
- 消息菜单“删除”直接请求选中 replyID,不加确认弹窗;新增、回复、编辑和删除成功后不追加成功 toast。复制链接成功后关闭菜单,不在正文下插入成功提示。
|
|
73
|
+
- 保留发送等待时的蓝色 loading、失败消息上的错误图标与“点击重试”;不要添加“发送中”“保存中”“已同步”“已显示全部评论”等额外可见文案。读屏用 aria-label/status 不属于额外可见提示,不要移除。
|
|
74
|
+
- 点击回复自动插入目标作者提及,回复自己也插入;不要按当前用户身份跳过。人员悬浮底色、提及搜索面板与 placeholder 由组件样式管理,宿主只控制外层尺寸,避免全局 button/a/p 样式覆盖。
|
|
75
|
+
- 只有确认未写入的失败才能重试;结果未知时保留内容并由宿主刷新核对,不能为追求视觉一致开放可能重复发表的重试。初次读取、分页、剪贴板拒绝等失败仍要保留必要的恢复入口,不得吞错。
|
|
76
|
+
|
|
77
|
+
新增交互或提示前先取得目标官方界面的截图/操作证据;没有证据的状态说明为接入扩展,不能声称已与官方完全对齐。验收要分别记录官方实测、真实 Action 回读、本地故障用例和未覆盖项。图片上传不在当前真实插件验收范围。
|
|
78
|
+
|
|
79
|
+
## 完成前验收
|
|
80
|
+
|
|
81
|
+
必须在真实目标环境完成新增→回复→编辑→刷新回读→删除;有回应能力时验证添加/撤回并刷新,有文档能力时验证搜索/插入/重新打开。检查键盘提及选择、单条/空结果、慢网络等待与失败重试、身份/记录切换、分页去重。只在本地 mock 页通过不能声称真实链路完成;图片未接入应明确说明。
|
|
@@ -44,7 +44,14 @@ ls -d node_modules/@lark-apaas/nestjs-mcp node_modules/@lark-apaas/coding-preset
|
|
|
44
44
|
| 用 `mcp_inspect` 核对与调用、按错误定位、本地模式调试 | `$SKILL_DIR/references/debugging.md` |
|
|
45
45
|
| 用户要在自己的 Agent(Claude Code / Codex / 其它客户端)里连本应用的 MCP | `$SKILL_DIR/references/client-onboarding.md` |
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
调用 `mcp_inspect` 前,先读 `$SKILL_DIR/references/debugging.md`。改工具、资源或界面后,等 dev server 重启再验证(定义有问题会在启动日志里报)。`mcp_inspect` 报「当前运行环境不支持」时,走该文件的本地模式。向用户汇报用业务表述(「找到 4 个演示航班」),不贴 JSON。
|
|
48
|
+
|
|
49
|
+
## 验证与汇报
|
|
50
|
+
|
|
51
|
+
- `mcp_inspect` 先用 `action: "list_tools"` 核对工具清单,再用 `action: "call"` 以真实入参验证所有只读工具。入参已确定的调用在同一轮发出,依赖前一步结果的调用后置。`tool_name` 取清单中的完整名,`arguments` 传 JSON 对象字符串(无入参用 `"{}"`)。正常调用应返回 `isError: false`,文本内容和 `structuredContent` 应包含预期业务数据;REST 接口或页面自测不能替代 MCP 调用。
|
|
52
|
+
- 写工具只在操作自建、且能自行删除或撤销的数据时传 `safe: true`;调用后读回确认变更并清理,汇报实际变更及清理情况。不能清理时只验证入参校验与业务错误分支,汇报「写入链路未真实验证」。
|
|
53
|
+
- 每个 `McpToolError` 分支构造一次触发,确认 `isError: true`,且 `code` 与工具描述一致。出现通用失败或结果不符合预期时按 `references/debugging.md` 定位,修复后复测失败项。
|
|
54
|
+
- 总结前对照工具清单与本轮实际调用结果,点名未调用的工具、未验证的分支和仍失败的项;只测了错误分支就只报错误分支通过,不写整个工具通过。有未覆盖项,不写「全部通过」。
|
|
48
55
|
|
|
49
56
|
## 不变量
|
|
50
57
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
1. 改工具 / 资源 / 界面。
|
|
25
25
|
2. 等 dev server 重启(`nest --watch` 会自动重启;界面改动由 `[mcp-ui]` 插件自动重建)。定义有问题会让应用起不来并打印「MCP 定义校验失败」,修完再往下走。
|
|
26
26
|
3. `list_tools` 核对清单与源码一致(`visibility: ['app']` 的工具也在清单里,属正常)。
|
|
27
|
-
4. `call`
|
|
27
|
+
4. `call` 以真实入参验证所有只读工具,调用顺序按 [顶层「验证与汇报」](../SKILL.md#验证与汇报) 组织。除了 `structuredContent`,单独看「文本内容」那段是否带全了业务数据:只读文本的客户端只看得到这段,只有「找到 N 条」之类的统计文案就是 `content` 写坏了(见 `tool-authoring.md`「方法签名与返回值」)。写工具只在能自己清掉痕迹(有对应删除 / 撤销工具,或业务上可撤回)时才真调并随手清理;否则只验证入参校验与 `McpToolError` 分支,汇报里写明「写入链路未真实验证」。
|
|
28
28
|
5. 带界面的工具 `read_resource`。
|
|
29
29
|
6. 每个 `McpToolError` 分支构造一次触发(不存在的 ID、非法状态),确认 `isError: true` 且 `code` 与 `description` 预告一致。
|
|
30
30
|
7. 更新 `server/mcp/skills/<name>/SKILL.md`,再 curl 目录接口(frontmatter 不合法这里会 500):`skills[]` 有该 `name`、`content` 是最新正文,`prompts[]` 的 `description` 一致。
|
|
@@ -77,6 +77,4 @@ curl -s -X POST 'http://localhost:<port><base>/__innerapi__/mcp' \
|
|
|
77
77
|
|
|
78
78
|
## 汇报
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
- 写工具真调过的,说明动了哪些数据、是否已清理;没真调的写明「写入链路未真实验证」。
|
|
82
|
-
- 未验证的分支明说未验证,不写「已全部通过」。
|
|
80
|
+
按 [SKILL.md 的「验证与汇报」](../SKILL.md#验证与汇报) 对照实际调用结果,说明验证范围与测试数据清理情况。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: trigger-guide
|
|
3
|
-
description: 自动化任务触发器配置与代码开发指南,支持 cron 定时触发器、record_change
|
|
3
|
+
description: 自动化任务触发器配置与代码开发指南,支持 cron 定时触发器、record_change 数据变更触发器、webhook 触发器和 feishu_approval 飞书审批触发器,包含 @Automation/@BindTrigger 装饰器用法和 Crontab 表达式规范。Use when 需要:(1) 创建或配置自动化任务/定时任务,(2) 编写 automation 代码绑定触发器,或其他自动化任务相关开发
|
|
4
4
|
steering: true
|
|
5
5
|
steering-topic: trigger_guide
|
|
6
6
|
match-template-name: nestjs-react-fullstack
|
|
@@ -45,19 +45,20 @@ server
|
|
|
45
45
|
|
|
46
46
|
### 触发器类型与入参
|
|
47
47
|
|
|
48
|
-
触发器类型(`triggerType
|
|
48
|
+
触发器类型(`triggerType`)有四种:
|
|
49
49
|
|
|
50
50
|
- `record_change`:记录变更触发器,**有入参**
|
|
51
51
|
- `cron`:定时触发器,**无入参**
|
|
52
52
|
- `webhook`:Webhook 触发器,**有入参**
|
|
53
|
+
- `feishu_approval`:飞书审批触发器,**有入参**
|
|
53
54
|
|
|
54
55
|
```typescript
|
|
55
|
-
//
|
|
56
|
+
// 有入参触发器的入参类型
|
|
56
57
|
interface TaskHandlerArgs {
|
|
57
58
|
attributes: {
|
|
58
59
|
trigger: string;
|
|
59
60
|
triggerID?: string;
|
|
60
|
-
triggerType: 'record_change' | 'cron' | 'webhook';
|
|
61
|
+
triggerType: 'record_change' | 'cron' | 'webhook' | 'feishu_approval';
|
|
61
62
|
instanceID: string;
|
|
62
63
|
startAt?: number;
|
|
63
64
|
};
|
|
@@ -81,6 +82,12 @@ interface DataChangeEventInput {
|
|
|
81
82
|
msg_id: string;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// feishu_approval:content.input 解析后的审批实例事件
|
|
86
|
+
type FeishuApprovalInstanceEventInput = {
|
|
87
|
+
uuid: string; event_type: string; app_id: string; approval_code: string;
|
|
88
|
+
instance_code: string; status: string; event_time: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
84
91
|
// webhook:input 解析后的数据结构
|
|
85
92
|
interface WebhookEvent {
|
|
86
93
|
method: 'GET' | 'POST';
|
|
@@ -102,6 +109,22 @@ function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
|
102
109
|
}
|
|
103
110
|
```
|
|
104
111
|
|
|
112
|
+
### 飞书审批实例事件
|
|
113
|
+
|
|
114
|
+
`feishu_approval` 的 `content.input` 是 JSON 字符串;解析后是上面的扁平审批实例事件,不是飞书原始 Webhook 外层,也不是实例详情响应。
|
|
115
|
+
|
|
116
|
+
- 审批未完成时由发起人撤回,审批实例事件状态为 `CANCELED`;`REVERTED` 用于撤销已通过的审批实例,不能外推到所有已结束状态。
|
|
117
|
+
- 获取单个审批实例详情时,详情 `status` 不返回 `REVERTED`,而以 `reverted`、`reverted_instance_code` 表达相关撤销信息;不得仅凭详情 `status` 排除事件状态。
|
|
118
|
+
- `approvalInstanceFilter.status` 是精确包含匹配的数组:事件状态命中任一元素即触发(OR),未列出的状态不触发。只有业务需要覆盖审批完成前取消和已通过后撤销两个阶段时才同时配置 `CANCELED`、`REVERTED`;只监听一个阶段时,单状态数组仍合法。
|
|
119
|
+
|
|
120
|
+
需要同时处理批准、拒绝和两个撤销阶段时,`triggerCondition` 可配置为:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{"approvalCode":"<审批定义唯一标识>","eventType":"approval_instance","approvalInstanceFilter":{"status":["APPROVED","REJECTED","CANCELED","REVERTED"]}}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
若项目代码、现有 filter 与外部说明冲突,先核验权威的 `approval_instance` 生命周期和事件合同:业务确需覆盖两个撤销阶段时补齐两个状态及对应处理逻辑;只针对一个阶段时保留合同对应的单状态监听,不要断言一个状态覆盖所有撤销场景。
|
|
127
|
+
|
|
105
128
|
### 指定值限制
|
|
106
129
|
1. Webhook 触发器不可以设置指定值,并且告知用户。
|
|
107
130
|
|
|
@@ -223,7 +246,7 @@ export class DemoAutomationTasksService {
|
|
|
223
246
|
- 需要查询数据库中的特定数据,给用户发消息:数据库中需要存储用户 id,使用从数据库中查询到的用户 id 进行后续操作
|
|
224
247
|
- 需要调用飞书能力给用户发消息:飞书能力不应该接受用户信息作为参数,而是应该在飞书能力配置里要求用户自己预先指定
|
|
225
248
|
|
|
226
|
-
2. 入参解析规范(仅 record_change 和
|
|
249
|
+
2. 入参解析规范(仅 record_change、webhook 和 feishu_approval 触发器):
|
|
227
250
|
- 有入参的触发器方法签名为 `async methodName(event: TaskHandlerArgs)`,`cron` 触发器无入参
|
|
228
251
|
- `content.input` 是 JSON 字符串,先用 `typeof input === 'string'` 检查类型,再用 `JSON.parse()` 解析,需添加 try-catch 错误处理
|
|
229
252
|
- `record_change`:根据操作类型获取数据:INSERT/UPDATE 使用 `after` 字段,DELETE 使用 `before` 字段
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plugin-guide
|
|
3
|
-
description: 本地开发场景下的 AI 插件集成规范。使用 lark-cli 命令管理插件包和实例,通过 capabilityClient/CapabilityService 生成调用代码。触发词:插件, plugin, AI生文, AI生图, 图片理解, 文档解析, 语音转文字, 结构化提取, capabilityClient, CapabilityService, pluginInstance, lark-cli plugin
|
|
3
|
+
description: 本地开发场景下的 AI 插件集成规范。使用 lark-cli 命令管理插件包和实例,通过 capabilityClient/CapabilityService 生成调用代码。触发词:插件, plugin, AI生文, AI生图, 图片理解, 文档解析, 语音转文字, 结构化提取, capabilityClient, CapabilityService, pluginInstance, lark-cli plugin, 记录评论组件, 评论回复, 表情回应, 云文档引用
|
|
4
4
|
compatibility: "requires: lark-cli with plugin commands (apps +plugin-install, +plugin-uninstall, +plugin-list). If not met, tell user: lark-cli 当前版本不支持插件能力,请升级到最新版本"
|
|
5
5
|
when_to_use: "lark-cli apps +plugin-* commands are available. If not, tell user: lark-cli 当前版本不支持插件能力,请升级到最新版本"
|
|
6
6
|
steering: true
|
|
@@ -25,6 +25,10 @@ lark-cli apps --help 2>&1 | grep -q '+plugin-install' && echo "READY" || echo "M
|
|
|
25
25
|
- **READY** → 继续
|
|
26
26
|
- **MISSING** → 当前 lark-cli 版本不支持插件命令,请更新到最新版后重试:
|
|
27
27
|
|
|
28
|
+
## 多维表格评论组件
|
|
29
|
+
|
|
30
|
+
需要记录评论/回复、编辑删除、@人员、表情回应或云文档引用时,复用项目内 `@/components/business-ui/record-comments` 的 `CommentPanel`;**先读 [评论组件接入指引](references/record-comments.md)** 和组件 `README.md`。真实接口、权限、缓存和分页游标留在宿主;组件负责现有样式、乐观更新、失败重试和分页触发。图片未接入插件,不默认启用。
|
|
31
|
+
|
|
28
32
|
## Quick Reference
|
|
29
33
|
|
|
30
34
|
| 操作 | 方式 |
|
package/steering/nestjs-react-fullstack/skills_local/plugin-guide/references/record-comments.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 多维表格评论组件(Record comments)
|
|
2
|
+
|
|
3
|
+
需要记录评论、新增/回复、编辑/删除、@人员、表情回应或云文档引用时,先读项目内 `client/src/components/business-ui/record-comments/README.md` 与 `comment-panel.contract.ts`,复用 `CommentPanel`,不要重新实现编辑器、弹层或乐观更新。此组件随 **nestjs-react-fullstack** 模板分发;旧应用若没有该目录,需要先升级/同步组件及依赖,不能只复制入口文件。本指引不适用于缺少该资产的其他技术栈。
|
|
4
|
+
|
|
5
|
+
## 接入边界
|
|
6
|
+
|
|
7
|
+
- 从 `@/components/business-ui/record-comments` 导入 `CommentPanel` 和公共类型;所有内部文件、CSS、`assets/reaction-sprite.png` 必须保留。Tiptap 依赖及 `@floating-ui/dom` 已由模板配置,不混装 Tiptap 大版本。
|
|
8
|
+
- 组件管理显示、回复归属、编辑器、人员弹层、发送等待/失败/重试以及乐观更新;**评论 CRUD、插件实例、用户身份、读缓存、分页游标、表情及文档请求全部由页面/API hook 提供**。不要把插件凭据、实例 ID、表格配置或应用 ID 写进组件内部。
|
|
9
|
+
- `createAppCommentSource(scopeKey)` 是可选的模板人员目录适配器,只识别妙搭人员 ID;其他目录实现 `CommentDataSource`,不要把飞书 ID 冒充妙搭 ID。默认人员目录不提供空查询推荐;宿主可提供 `recommend`,例如从当前讨论参与者解析推荐列表。
|
|
10
|
+
- `scopeKey` 必须覆盖应用/租户/登录身份/插件实例/数据源与权限上下文,以上任一变化都应更换;`recordID` 切换会重建编辑会话。`mentionSource` 用 `useMemo` 稳定,避免每次渲染重建搜索和缓存。
|
|
11
|
+
|
|
12
|
+
## 真实接口先核对
|
|
13
|
+
|
|
14
|
+
按本 skill 的插件元数据流程拿到当前实例真实 Action schema。以下是已联调版本的映射,**实际 actionKey、字段和响应层级始终以当前元数据为准**,缺能力则隐藏该功能,不伪造成功、不回退 mock。
|
|
15
|
+
|
|
16
|
+
| 宿主能力 | 已联调 Action / 输入约定 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| 评论读取 | `listRecordComments`,传 `recordID`,需要回应时传 `needReaction:true`;按 `hasMore/pageToken` 加载下一页 |
|
|
19
|
+
| 新增/回复 | `addRecordComment`;新评论不传 `commentID`,回复保留原 `commentID`,返回真实 `commentID/replyID` |
|
|
20
|
+
| 编辑 | `updateRecordComment`;传 `recordID/commentID/replyID/content`,回读并传入回复级 `updateTime` |
|
|
21
|
+
| 删除 | `deleteRecordComment`;消息菜单必须传选中消息的 `replyID`(含首条),只删该消息;第二条顺位成为正文,其余回复保留。省略 replyID 仅兼容显式整串删除,按 `commentDeleted` 更新缓存 |
|
|
22
|
+
| 回应读取/写入 | `getRecordCommentReactions` / `updateRecordCommentReaction`;写入用显式 `add/delete`,组件回调的 `remove` 需映射成插件的 `delete` |
|
|
23
|
+
| 云文档推荐/搜索/解析 | `getCommentDocuments`;外部实现 `CommentDocumentSource`,用 `withDocumentSource(people, documents)` 合并,显式传 `enableDocuments` |
|
|
24
|
+
|
|
25
|
+
每条 `comments[]` 是讨论,`replies[0]` 是正文,其余 replies 是同一讨论下的消息;不要把回复拆成独立 comment。`commentID/replyID` 始终是字符串。`createTime/updateTime` 是 Unix **秒**,排序取创建时间,展示取 `updateTime ?? createTime`;缺更新时间不能伪造“编辑过”。`mention.userID` 缺失时保留缺失引用,不猜人。
|
|
26
|
+
|
|
27
|
+
## 受控组件的宿主实现
|
|
28
|
+
|
|
29
|
+
1. 宿主发起首屏读取,维护 `comments/loading/error`,传 `onRetry`。分页可全量拉取后不给 `pagination`,也可传 `{hasMore, loading, error, onLoadMore}`;组件接近底部会自动调用,宿主用自己的游标追加数据,追加后再 resolve,失败必须 reject。游标不前进时终止并报错。
|
|
30
|
+
2. 宿主提供 `onAddComment/onUpdateComment/onDeleteComment`。写成功后立即返回真实 ID/删除结果,再单独启动回读;**不能把后续回读失败当成写失败**,否则组件重试可能重复发表。不需要在宿主再插一份临时评论,也不必等待回读才关闭编辑器。
|
|
31
|
+
3. 编辑/删除必须显式传 `getReplyPermissions`,未提供就隐藏。前端作者判断只控制菜单,服务端仍检查当前用户权限;不能用 owner 身份补足。点击消息菜单“删除”直接发起请求,不追加二次确认;删除首条也只删它本身,不能扩大到其他人的回复;只有最后一条消失时移除空讨论。
|
|
32
|
+
4. 普通 `Error` 仅用于确认未应用的失败;超时/连接中断等可能已提交时,抛 `Object.assign(new Error(message), {outcome:'unknown'})`,组件保留内容并阻止盲目重试,宿主先刷新核对。只有请求尚未发送等已确认未写入的断网失败才标记 `not-applied`。
|
|
33
|
+
5. 宿主缓存要保护读取顺序,取消/忽略旧请求,切换身份时隔离缓存;组件在匹配快照接管后无法判断一个没有版本号的旧数组。不要通过延迟 toast、二次 loading 或本地时间 Map 掩盖回读错误。
|
|
34
|
+
|
|
35
|
+
最小受控接入(`read` 和 `commands` 是应用自己的真实 API hook,不是组件内部接口):
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { useMemo } from 'react';
|
|
39
|
+
import {
|
|
40
|
+
CommentPanel, createAppCommentSource,
|
|
41
|
+
type CommentPanelProps,
|
|
42
|
+
} from '@/components/business-ui/record-comments';
|
|
43
|
+
|
|
44
|
+
type Props = Pick<CommentPanelProps,
|
|
45
|
+
'scopeKey' | 'recordID' | 'comments' | 'currentUserID' |
|
|
46
|
+
'loading' | 'error' | 'onRetry' | 'pagination' |
|
|
47
|
+
'onAddComment' | 'onUpdateComment' | 'onDeleteComment' |
|
|
48
|
+
'getReplyPermissions'>;
|
|
49
|
+
|
|
50
|
+
export function RecordComments(props: Props) {
|
|
51
|
+
const mentionSource = useMemo(
|
|
52
|
+
() => createAppCommentSource(props.scopeKey), [props.scopeKey],
|
|
53
|
+
);
|
|
54
|
+
return <CommentPanel {...props} mentionSource={mentionSource} />;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
真实调用继续使用 `capabilityClient.load(instanceID).call(actionKey, input)`,不要在组件里调用 CLI 或直接手写 OAuth;`call()` 的结果已由 SDK 解包,按实际 schema 使用,不要重复套 `data.output`。写入回调须返回合同中的真实结果,不能返回 `void`。
|
|
59
|
+
|
|
60
|
+
## 可选能力与权限
|
|
61
|
+
|
|
62
|
+
- **表情回应**:传 `reactions.getReactions/onChange`,可选 `canChange/loadUsers`。列表省略 reactions 表示未知,不能当作无人回应,`mine` 也不能根据截断的 `userIDs` 推断。宿主按回复串行写入,成功先更新当前回应缓存,再刷新;失败只回退该目标,刷新失败不能回退已提交动作。读取不到权威 membership 时禁用切换,保留历史表情键(如 `MediumThumbsup`)。
|
|
63
|
+
- **云文档引用**:搜索候选的 `description` 使用接口的 `editTime`(Unix 秒)格式化为“最近修改:10月26日 20:41”等元数据副标题;缺失时间就省略,不伪造。只查询当前用户可读的元数据,搜索与解析都应检查权限;不得以 owner/其他用户补查私密标题,插入引用不更改分享权限。写入是 `{type:'link',url}`;持久化 URL,不把标题/图标当作请求字段。请求被取消后检查 signal,丢弃过期结果。解析不可读时使用组件的灰色引用状态,不能假称可读。
|
|
64
|
+
- **图片**:当前多维表格插件未接入图片评论,不传 `images`,不展示上传入口。`images` 仅是宿主自备上传/持久化能力时的独立扩展;不得把 File/blob URL 塞入插件 content。
|
|
65
|
+
- **人员卡片/链接**:通过 `onMentionClick/getCommentLink` 由宿主承接;未提供对应回调时不展示无效操作。
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## 交互保持约束
|
|
69
|
+
|
|
70
|
+
复用完整组件及其状态流,不在宿主外包一层“增强交互”。
|
|
71
|
+
|
|
72
|
+
- 消息菜单“删除”直接请求选中 replyID,不加确认弹窗;新增、回复、编辑和删除成功后不追加成功 toast。复制链接成功后关闭菜单,不在正文下插入成功提示。
|
|
73
|
+
- 保留发送等待时的蓝色 loading、失败消息上的错误图标与“点击重试”;不要添加“发送中”“保存中”“已同步”“已显示全部评论”等额外可见文案。读屏用 aria-label/status 不属于额外可见提示,不要移除。
|
|
74
|
+
- 点击回复自动插入目标作者提及,回复自己也插入;不要按当前用户身份跳过。人员悬浮底色、提及搜索面板与 placeholder 由组件样式管理,宿主只控制外层尺寸,避免全局 button/a/p 样式覆盖。
|
|
75
|
+
- 只有确认未写入的失败才能重试;结果未知时保留内容并由宿主刷新核对,不能为追求视觉一致开放可能重复发表的重试。初次读取、分页、剪贴板拒绝等失败仍要保留必要的恢复入口,不得吞错。
|
|
76
|
+
|
|
77
|
+
新增交互或提示前先取得目标官方界面的截图/操作证据;没有证据的状态说明为接入扩展,不能声称已与官方完全对齐。验收要分别记录官方实测、真实 Action 回读、本地故障用例和未覆盖项。图片上传不在当前真实插件验收范围。
|
|
78
|
+
|
|
79
|
+
## 完成前验收
|
|
80
|
+
|
|
81
|
+
必须在真实目标环境完成新增→回复→编辑→刷新回读→删除;有回应能力时验证添加/撤回并刷新,有文档能力时验证搜索/插入/重新打开。检查键盘提及选择、单条/空结果、慢网络等待与失败重试、身份/记录切换、分页去重。只在本地 mock 页通过不能声称真实链路完成;图片未接入应明确说明。
|