@lark-apaas/coding-steering 0.1.77 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/steering/nestjs-react-fullstack/skills/anycross-forward/SKILL.md +132 -46
- 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/package.json
CHANGED
|
@@ -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()` 仍生效
|
|
@@ -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` 字段
|