tianshu-mcp 0.7.9 → 0.7.10
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/CHANGELOG.en.md +38 -0
- package/CHANGELOG.md +33 -0
- package/dist/version.generated.js +1 -1
- package/dist/visual/manage.js +3 -0
- package/dist/visual/runtime.js +8 -1
- package/dist/visual/schema.js +27 -2
- package/docs/visual-acceptance.en.md +11 -7
- package/docs/visual-acceptance.md +15 -9
- package/package.json +1 -1
- package/skills/tianshu-mcp/SKILL.md +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -60,6 +60,44 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
## [0.7.10] - 2026-10-06
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- **Precise semantics for the visual content-egress gate (issue #29)**: with `allowRemote` defaulting to
|
|
68
|
+
`false`, `<image:base64:file>` is rejected by the schema, while `<image:path>` / `<expect:file>` are
|
|
69
|
+
unconstrained — and the docs implied the contract layer blocked *every* way of handing an image to a
|
|
70
|
+
command, leading readers to believe the channels were sealed. Probes and counter-evidence show that
|
|
71
|
+
**gating the path channel yields zero security**: the judgement command runs with `shell:false` inside the
|
|
72
|
+
**project directory** and can already read project files itself; forcing an opt-in would also make users
|
|
73
|
+
set `allowRemote` just to pass the schema, which **opens up the inline base64 channel too**. This release
|
|
74
|
+
therefore does not change the gate — it states the boundary precisely and makes the channels visible.
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- **New `contentChannelUsage()` (`src/visual/schema.ts`) as the single source of channel semantics**: the
|
|
79
|
+
gate check and the doctor/probe display share the same logic, so the two cannot drift apart.
|
|
80
|
+
- **`visual doctor` now reports each rule's actually-used placeholder channels and whether they are
|
|
81
|
+
constrained** (`GATED` / `NOT gated`) — surfacing what the command really receives (absence never raises
|
|
82
|
+
an alarm by itself).
|
|
83
|
+
- **`visual content probe` output gains `egressConstrained` / `pathChannels`** for programmatic checks.
|
|
84
|
+
- Wording synced across `SECURITY.md` / `SECURITY.en.md`, `docs/visual-acceptance.md` / `.en.md`, and
|
|
85
|
+
`skills/tianshu-mcp/SKILL.md`: `allowRemote` constrains **only** the inline-byte shape, and the contract
|
|
86
|
+
layer is **not** a complete block on image egress.
|
|
87
|
+
- `<expect:file>` nature clarified: it delivers a temp file holding the **expectation text** and is **not**
|
|
88
|
+
an image-egress channel (the issue conflated the two).
|
|
89
|
+
|
|
90
|
+
### Verification
|
|
91
|
+
|
|
92
|
+
- 5 new channel/admission contract tests (`test/unit/visual-content-schema.test.ts`), 2 doctor
|
|
93
|
+
channel-semantics tests (`test/unit/visual-runtime.test.ts`), plus the probe field assertion.
|
|
94
|
+
- Counter-evidence probe: the command read a project file with **zero placeholders**
|
|
95
|
+
(`READ 4187 bytes with zero placeholders`), proving gating the path channel has no benefit.
|
|
96
|
+
- Full `npm test` **1582 passed / 12 skipped, 0 failed**; `typecheck` / `lint` / `build` / `pack:check` /
|
|
97
|
+
`check:stdio` all green.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
63
101
|
## [0.7.9] - 2026-10-06
|
|
64
102
|
|
|
65
103
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -58,6 +58,39 @@
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
+
## [0.7.10] - 2026-10-06
|
|
62
|
+
|
|
63
|
+
### 修复
|
|
64
|
+
|
|
65
|
+
- **视觉内容判定的外发闸门语义精确化(issue #29)**:`allowRemote` 默认 `false` 时,
|
|
66
|
+
`<image:base64:file>` 被 schema 拒绝,但 `<image:path>` / `<expect:file>` 不受任何约束——
|
|
67
|
+
文档措辞又暗示「契约层封死了所有把图交给命令的形态」,读者会以为通道已被堵死。
|
|
68
|
+
经探针实测与反证(见下),**门控路径通道零安全收益**:判定命令以 `shell:false` 在**项目目录内**
|
|
69
|
+
执行,本就能自读项目内文件;且强制放行会因「必须开 `allowRemote` 才能过 schema」而**反向连带
|
|
70
|
+
放行 base64 内联**。故本版不改门控、只把边界说清并把通道可见化。
|
|
71
|
+
|
|
72
|
+
### 变更
|
|
73
|
+
|
|
74
|
+
- **新增 `contentChannelUsage()`(`src/visual/schema.ts`)作为通道语义的单一来源**:
|
|
75
|
+
门控判定与 doctor/probe 展示共用同一段逻辑,避免两处实现分叉。
|
|
76
|
+
- **`visual doctor` 逐规则输出实际使用的占位符通道与是否受约束**(`GATED` / `NOT gated`)——
|
|
77
|
+
把「命令实际拿到什么」摆到台面(缺席不会自己报警)。
|
|
78
|
+
- **`visual content probe` 输出新增 `egressConstrained` / `pathChannels` 字段**,便于调用方可编程核对。
|
|
79
|
+
- `SECURITY.md` / `SECURITY.en.md`、`docs/visual-acceptance.md` / `.en.md`、`skills/tianshu-mcp/SKILL.md`
|
|
80
|
+
同步措辞:明确 `allowRemote` **只**约束内联字节形态,契约层**不是**对图片外发的完备拦截。
|
|
81
|
+
- `<expect:file>` 性质澄清:它交付的是**期望文本**临时文件,**不是图片外发通道**(issue 原文在此处有误归并)。
|
|
82
|
+
|
|
83
|
+
### 验证
|
|
84
|
+
|
|
85
|
+
- 新增 5 个 `contentChannelUsage` / 准入边界契约用例(`test/unit/visual-content-schema.test.ts`),
|
|
86
|
+
2 个 doctor 通道语义用例(`test/unit/visual-runtime.test.ts`),probe 字段断言同步。
|
|
87
|
+
- 反证探针实测:命令在**零占位符**下即读到项目内文件字节(`READ 4187 bytes with zero placeholders`),
|
|
88
|
+
证明门控路径通道无收益。
|
|
89
|
+
- 全量 `npm test` **1582 passed / 12 skipped,0 失败**;`typecheck` / `lint` / `build` / `pack:check` /
|
|
90
|
+
`check:stdio` 全绿。
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
61
94
|
## [0.7.9] - 2026-10-06
|
|
62
95
|
|
|
63
96
|
### 修复
|
package/dist/visual/manage.js
CHANGED
|
@@ -15,6 +15,7 @@ import { VisualBrowser } from "./capture.js";
|
|
|
15
15
|
import { VisualServices } from "./services.js";
|
|
16
16
|
import { clearContentCache } from "./content-cache.js";
|
|
17
17
|
import { resolveCommandPath } from "./content-command.js";
|
|
18
|
+
import { contentChannelUsage } from "./schema.js";
|
|
18
19
|
import { contentCommandParts, hasContentRules, probeContent, } from "./content.js";
|
|
19
20
|
import { isDefaultWorkspace } from "../tasks/task.js";
|
|
20
21
|
function taskDirectory(home, taskId) {
|
|
@@ -159,6 +160,7 @@ export async function probeContentRules(home, projectPath, ruleId) {
|
|
|
159
160
|
command: effective.command,
|
|
160
161
|
resolved: (await resolveCommandPath(effective.command, await projectFile(projectPath, effective.cwd))) !== null,
|
|
161
162
|
allowRemote: effective.allowRemote,
|
|
163
|
+
...contentChannelUsage(effective.argsTemplate),
|
|
162
164
|
});
|
|
163
165
|
for (const [fileIndex, file] of rule.files.entries())
|
|
164
166
|
results.push(await probeContent({ config, project: projectPath, budget, tempDir: tempRoot }, {
|
|
@@ -175,6 +177,7 @@ export async function probeContentRules(home, projectPath, ruleId) {
|
|
|
175
177
|
command: effective.command,
|
|
176
178
|
resolved: (await resolveCommandPath(effective.command, await projectFile(projectPath, effective.cwd))) !== null,
|
|
177
179
|
allowRemote: effective.allowRemote,
|
|
180
|
+
...contentChannelUsage(effective.argsTemplate),
|
|
178
181
|
});
|
|
179
182
|
for (const viewport of config.viewports.filter((v) => page.viewports === undefined || page.viewports.includes(v.id))) {
|
|
180
183
|
const captured = await browser.capture(projectPath, page, viewport, await services.get(page.source));
|
package/dist/visual/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import { VisualError } from "./errors.js";
|
|
|
5
5
|
import { projectFile } from "./paths.js";
|
|
6
6
|
import { contentCommandParts, hasContentRules, contentRulesOf } from "./content.js";
|
|
7
7
|
import { resolveCommandPath } from "./content-command.js";
|
|
8
|
+
import { contentChannelUsage } from "./schema.js";
|
|
8
9
|
export function assertVisualRuntime(version = process.versions.node) {
|
|
9
10
|
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
10
11
|
if (major < 20 || (major === 20 && minor < 3))
|
|
@@ -117,6 +118,8 @@ export async function doctor(projectPath, home) {
|
|
|
117
118
|
await check("browser", async () => resolveBrowser((await readAcceptanceConfig(projectPath))?.visual?.browser ?? { mode: "managed" }, home));
|
|
118
119
|
// 内容校验诊断(issue #13 F 组):逐条有效命令的解析结果 + allowRemote 声明清单、
|
|
119
120
|
// 规则数 × samples × timeoutMs 与 roundTimeoutMs 的预算对比(超预算给出建议值,不自动改配置)
|
|
121
|
+
// issue #29:额外显性化**通道语义**——命令实际使用哪些占位符、哪些受 allowRemote 约束,
|
|
122
|
+
// 避免「allowRemote=false 即封死一切外发」的错误安全感(缺席不会自己报警)。
|
|
120
123
|
await check("content command", async () => {
|
|
121
124
|
const visual = (await readAcceptanceConfig(projectPath))?.visual;
|
|
122
125
|
if (!visual?.content.enabled || !hasContentRules(visual))
|
|
@@ -129,7 +132,11 @@ export async function doctor(projectPath, home) {
|
|
|
129
132
|
const resolved = await resolveCommandPath(effective.command, cwd);
|
|
130
133
|
if (!resolved)
|
|
131
134
|
unresolved.push(rule.label);
|
|
132
|
-
|
|
135
|
+
// allowRemote 的实际约束范围只有内联字节通道;路径通道不受约束(issue #29)
|
|
136
|
+
const channels = contentChannelUsage(effective.argsTemplate);
|
|
137
|
+
const gated = channels.egressConstrained ? "GATED" : "NOT gated";
|
|
138
|
+
const used = [...(channels.egressConstrained ? ["<image:base64:file>"] : []), ...channels.pathChannels];
|
|
139
|
+
lines.push(`${rule.label}: ${effective.command} -> ${resolved ?? "UNRESOLVED (will block the whole round)"}; allowRemote=${effective.allowRemote} (constrains only <image:base64:file>); channels used: ${used.length ? used.join(", ") : "(none)"} — ${gated} by allowRemote`);
|
|
133
140
|
}
|
|
134
141
|
// 有效命令不可解析会让整轮配置错误(assertContentReady 抛错),诊断必须据实报失败
|
|
135
142
|
if (unresolved.length)
|
package/dist/visual/schema.js
CHANGED
|
@@ -132,6 +132,29 @@ export const ContentVerdictSchema = z
|
|
|
132
132
|
const envReference = z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string().min(1));
|
|
133
133
|
/** 占位符白名单:argsTemplate 中形如 <...> 的 token 只允许这三个 */
|
|
134
134
|
export const CONTENT_PLACEHOLDERS = ["<image:path>", "<expect:file>", "<image:base64:file>"];
|
|
135
|
+
/**
|
|
136
|
+
* 外发闸门的通道边界(issue #29)——**唯一的语义源**,schema 校验与 doctor/probe 展示共用:
|
|
137
|
+
* - `<image:base64:file>`:把图片字节**内联**进临时文件。这是「明确要把图片交给命令」的强信号,
|
|
138
|
+
* 受 `allowRemote` 约束(未放行即拒绝配置)。定位是**防无意/防误配**,不是防有意外发。
|
|
139
|
+
* - `<image:path>` / `<expect:file>`:交付路径/期望文本,**不受** `allowRemote` 约束。
|
|
140
|
+
* 命令的 cwd 本就在项目内、可自读文件,门控 path 无安全收益(见计划 §2.1 探针);
|
|
141
|
+
* 且强制放行会反向扩大外发面(用户为过 schema 而开 allowRemote 会连带放行 base64)。
|
|
142
|
+
*/
|
|
143
|
+
export const EGRESS_CONSTRAINED_PLACEHOLDER = "<image:base64:file>";
|
|
144
|
+
/** 按 argsTemplate 统计通道使用情况:doctor/probe 据此把「命令实际拿到什么」显性化 */
|
|
145
|
+
export function contentChannelUsage(argsTemplate) {
|
|
146
|
+
const pathChannels = [];
|
|
147
|
+
let egressConstrained = false;
|
|
148
|
+
for (const token of argsTemplate)
|
|
149
|
+
for (const match of token.matchAll(/<[^<>\s]*>/g)) {
|
|
150
|
+
const channel = match[0];
|
|
151
|
+
if (channel === EGRESS_CONSTRAINED_PLACEHOLDER)
|
|
152
|
+
egressConstrained = true;
|
|
153
|
+
else if (!pathChannels.includes(channel))
|
|
154
|
+
pathChannels.push(channel);
|
|
155
|
+
}
|
|
156
|
+
return { egressConstrained, pathChannels };
|
|
157
|
+
}
|
|
135
158
|
/** 页面/规则共用的内容检查声明(pages[].content 与 contents[] 条目) */
|
|
136
159
|
export const ContentCheckSchema = z
|
|
137
160
|
.object({
|
|
@@ -296,9 +319,11 @@ export const VisualConfigSchema = z
|
|
|
296
319
|
const argsTemplate = check.argsTemplate ?? v.content.argsTemplate;
|
|
297
320
|
if (!command || !argsTemplate)
|
|
298
321
|
issue(`${label}: content checks require command and argsTemplate`);
|
|
299
|
-
//
|
|
322
|
+
// 外发闸门:仅内联字节通道(<image:base64:file>)需逐规则显式放行;
|
|
323
|
+
// 路径通道(<image:path>/<expect:file>)不受约束——边界见 EGRESS_CONSTRAINED_PLACEHOLDER 注释
|
|
300
324
|
const allowRemote = check.allowRemote ?? v.content.allowRemote;
|
|
301
|
-
|
|
325
|
+
const channels = contentChannelUsage(argsTemplate ?? []);
|
|
326
|
+
if (channels.egressConstrained && allowRemote !== true)
|
|
302
327
|
issue(`${label}: <image:base64:file> requires allowRemote = true`);
|
|
303
328
|
// 预算自洽:roundTimeoutMs 是硬总闸,超预算配置必然整轮 ROUND_TIMEOUT,必须在配置期拦截
|
|
304
329
|
const samples = check.samples ?? v.content.samples;
|
|
@@ -166,13 +166,15 @@ client. Judgement is fully delegated to a local command you supply, which uses i
|
|
|
166
166
|
|
|
167
167
|
- **Placeholders** (a placeholder absent from the template produces no temporary file):
|
|
168
168
|
|
|
169
|
-
| Placeholder | Expands to | Extra condition |
|
|
170
|
-
|
|
171
|
-
| `<image:path>` | absolute path of the inspected image (through the project path gate) | — |
|
|
172
|
-
| `<expect:file>` | absolute path of a temporary file holding the expectation as UTF-8 | — |
|
|
173
|
-
| `<image:base64:file>` | absolute path of a temporary file holding the image's base64 | **requires** the rule's effective `allowRemote === true`, else the schema rejects it |
|
|
169
|
+
| Placeholder | Expands to | Constrained by `allowRemote`? | Extra condition |
|
|
170
|
+
|---|---|---|---|
|
|
171
|
+
| `<image:path>` | absolute path of the inspected image (through the project path gate) | **No** | — |
|
|
172
|
+
| `<expect:file>` | absolute path of a temporary file holding the expectation as UTF-8 (**not** an image-egress channel; it carries expectation text only) | **No** | — |
|
|
173
|
+
| `<image:base64:file>` | absolute path of a temporary file holding the image's base64 (inlines the image **bytes**) | **Yes** | **requires** the rule's effective `allowRemote === true`, else the schema rejects it |
|
|
174
174
|
|
|
175
|
-
Any other `<...>` token is rejected at configuration time.
|
|
175
|
+
Any other `<...>` token is rejected at configuration time. `allowRemote` constrains **only** the
|
|
176
|
+
inline-byte shape `<image:base64:file>`; the two path channels are unconstrained (see “Egress statement”
|
|
177
|
+
below), and `visual doctor` marks each rule's actually-used channels.
|
|
176
178
|
|
|
177
179
|
- **stdout**: the **last non-empty line** is parsed as JSON: `{ "passed": boolean, "confidence"?: 0..1, "reason": string }` (strict mode; unknown fields rejected).
|
|
178
180
|
- **Exit code**: `0` means the command ran normally (**not** that the judgement passed — read the JSON); non-`0` means the command failed.
|
|
@@ -221,7 +223,9 @@ There is no separate cap on judgement count or spend. Cost is bounded entirely b
|
|
|
221
223
|
|
|
222
224
|
### Egress statement
|
|
223
225
|
|
|
224
|
-
Whether images leave the machine **depends on the behaviour of your command**; the MCP cannot block that at the system level. Its enforcement is contract-level only
|
|
226
|
+
Whether images leave the machine **depends on the behaviour of your command**; the MCP cannot block that at the system level. Its enforcement is contract-level only, and **covers just the `allowRemote` constraint on `<image:base64:file>`** (inlining the image bytes into a temp file; the schema rejects it unless the rule explicitly opts in).
|
|
227
|
+
|
|
228
|
+
**The contract layer is not a complete block on image egress** (issue #29): `<image:path>` / `<expect:file>` are **not** constrained by `allowRemote`. The judgement command runs with `shell:false` inside the **project directory** and can read the inspected project image by itself, so gating the path channel adds no security while forcing users to set `allowRemote` just to pass the schema — which would also open up the inline base64 channel. `allowRemote` is about **preventing accidental/misconfigured inline egress**, not “blocking every way of handing an image to a command”. `visual doctor` lists each rule's `allowRemote` declaration and the placeholder channels it **actually uses** (marking which are constrained). Confirm your command's actual behaviour yourself.
|
|
225
229
|
|
|
226
230
|
### Testing your command
|
|
227
231
|
|
|
@@ -168,13 +168,14 @@ tianshu-mcp visual rules approve TASK_ID REVIEW_ID DIGEST "用户确认的批准
|
|
|
168
168
|
|
|
169
169
|
- **占位符**(未在模板中出现的占位符不会生成对应临时文件):
|
|
170
170
|
|
|
171
|
-
| 占位符 | 展开为 | 附加条件 |
|
|
172
|
-
|
|
173
|
-
| `<image:path>` | 被检图片的绝对路径(经项目路径闸门) | — |
|
|
174
|
-
| `<expect:file>` | 写入 UTF-8
|
|
175
|
-
| `<image:base64:file>` | 写入该图片 base64
|
|
171
|
+
| 占位符 | 展开为 | 是否受 `allowRemote` 约束 | 附加条件 |
|
|
172
|
+
|---|---|---|---|
|
|
173
|
+
| `<image:path>` | 被检图片的绝对路径(经项目路径闸门) | **否** | — |
|
|
174
|
+
| `<expect:file>` | 写入 UTF-8 期望原文的临时文件绝对路径(**不是图片外发通道**,只交付期望文本) | **否** | — |
|
|
175
|
+
| `<image:base64:file>` | 写入该图片 base64 的临时文件绝对路径(把图片**字节**内联进文件) | **是** | **必须**该规则有效 `allowRemote === true`,否则 schema 拒绝 |
|
|
176
176
|
|
|
177
|
-
出现任何其他 `<...>` token
|
|
177
|
+
出现任何其他 `<...>` token 直接拒绝配置。`allowRemote` **只**约束 `<image:base64:file>` 这一内联字节
|
|
178
|
+
形态;两个路径通道不受约束(原因见下方「数据外发声明」),`visual doctor` 会逐规则标出实际使用的通道。
|
|
178
179
|
|
|
179
180
|
- **stdout**:取**最后一行非空文本**解析 JSON:`{ "passed": boolean, "confidence"?: 0..1, "reason": string }`(严格模式,未知字段拒绝)。
|
|
180
181
|
- **退出码**:`0` 表示命令正常执行(**不代表判定通过**,通过与否看 JSON);非 `0` 表示命令执行失败。
|
|
@@ -230,9 +231,14 @@ tianshu-mcp visual rules approve TASK_ID REVIEW_ID DIGEST "用户确认的批准
|
|
|
230
231
|
|
|
231
232
|
### 数据外发声明
|
|
232
233
|
|
|
233
|
-
图片是否离开本机取决于**用户自备命令的行为**,MCP 无法在系统层拦截。MCP
|
|
234
|
-
`allowRemote`
|
|
235
|
-
|
|
234
|
+
图片是否离开本机取决于**用户自备命令的行为**,MCP 无法在系统层拦截。MCP 的强制力仅在契约层,且
|
|
235
|
+
**只覆盖 `allowRemote` 对 `<image:base64:file>` 的约束**(把图片字节内联进临时文件;未显式放行即 schema 拒绝)。
|
|
236
|
+
|
|
237
|
+
**契约层不是对图片外发的完备拦截**(issue #29):`<image:path>` / `<expect:file>` 不受 `allowRemote`
|
|
238
|
+
约束。判定命令以 `shell:false` 在**项目目录内**执行,本身就能读取项目内的被检图片,因此门控路径通道
|
|
239
|
+
既无安全收益,又会因「必须开 `allowRemote` 才能过 schema」而反向连带放行 base64 内联。`allowRemote`
|
|
240
|
+
的定位是**防无意/防误配的内联外发**,不是「封死一切把图交给命令的形态」。`visual doctor` 列出各规则的
|
|
241
|
+
`allowRemote` 声明与**实际使用的占位符通道**(标明哪些通道受约束)。请自行确认命令的实际行为。
|
|
236
242
|
|
|
237
243
|
### 验证自备命令
|
|
238
244
|
|
package/package.json
CHANGED
|
@@ -274,7 +274,7 @@ meta 的 `needsUserKind` 给出等待类型,`pendingQuestion` 给出问题原
|
|
|
274
274
|
- **整轮阻塞**:任一规则的**有效**命令不可解析(`CONTENT_COMMAND_MISSING`)或宿主环境变量缺失(`CONTENT_ENV_MISSING`)会让整轮进 `needs_attention` 且**不产出任何视觉结果行**。这是 fail-closed,不是告警。
|
|
275
275
|
- **`uncertain` 不是失败**:票不集中或低于 `minConfidence` 时判 `uncertain`,永不阻塞、不触发返修;命令不报 confidence 时 `minConfidence` 不生效。
|
|
276
276
|
- 排查:`tianshu-mcp visual doctor <project>`、`visual content probe <project> [ruleId]`、`visual content cache clear <taskId>`。
|
|
277
|
-
- **数据外发**:`allowRemote` 默认 `false
|
|
277
|
+
- **数据外发**:`allowRemote` 默认 `false`,**只**禁止受约束的内联字节通道 `<image:base64:file>`(未放行即 schema 拒绝);`<image:path>` / `<expect:file>` 是**不受约束**的路径通道(命令在项目内执行、本就能自读文件,门控无收益)。契约层不是对图片外发的完备拦截——图片是否离开本机取决于用户命令的行为,MCP 无法在系统层拦截。`visual doctor` 逐规则标出实际使用的通道与是否 `GATED`。
|
|
278
278
|
|
|
279
279
|
---
|
|
280
280
|
|