@crazx/dsh-mcp-client 0.1.0-rc.7.zw.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/mcp/mcp-client/README.md
5
+ README.md: 9f4f6fa204566ab0228ac6e7747c022f076c5bb3
6
+ README.zh.md: a81de7ef72676a6c87424e16163cf88c13d3e722
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @deepseek-ai/dsh-mcp-client
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ MCP client bridge plugin: connects to external [Model Context Protocol](https://modelcontextprotocol.io/) servers and registers their tools on `ctx.tools`, making them available to the model as native tools under server-qualified names (`mcp__<serverName>__<rawName>`).
6
+
7
+ ## Usage
8
+
9
+ One plugin instance per MCP server in `cordis.yml`:
10
+
11
+ ```yaml
12
+ - id: mcp-github
13
+ name: '@deepseek-ai/dsh-mcp-client'
14
+ config:
15
+ serverName: github
16
+ transport: stdio
17
+ command: npx
18
+ args: ['-y', '@modelcontextprotocol/server-github']
19
+ env:
20
+ GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
21
+
22
+ - id: mcp-web
23
+ name: '@deepseek-ai/dsh-mcp-client'
24
+ config:
25
+ serverName: web
26
+ transport: streamable-http
27
+ url: http://localhost:3000/mcp
28
+ headers:
29
+ Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'
30
+ ```
31
+
32
+ The model sees `mcp__github__create_issue`, `mcp__web__search`, … — the same server-qualified shape Claude Code and Codex use. HMR hot-swaps: editing the entry triggers disconnect + reconnect without process restart; an unchanged `serverName` reproduces identical tool names.
33
+
34
+ ## Config
35
+
36
+ | Field | Transport | Required | Description |
37
+ |---|---|---|---|
38
+ | `transport` | both | yes | `"stdio"` or `"streamable-http"` |
39
+ | `serverName` | both | yes | Namespace for this server's model-facing tool names; `[A-Za-z0-9_-]{1,32}`, unique across live instances |
40
+ | `command` | stdio | yes | Executable to spawn |
41
+ | `args` | stdio | no | Arguments passed to the command |
42
+ | `env` | stdio | no | Extra env vars merged on top of scrubbed ambient env |
43
+ | `cwd` | stdio | no | Working directory for the child process |
44
+ | `url` | http | yes | MCP server URL |
45
+ | `headers` | http | no | Extra headers (e.g. auth tokens) |
46
+ | `toolCallTimeoutMs` | both | no | Timeout per `callTool` invocation (default 60000) |
47
+ | `failOnStartupError` | both | no | Reject plugin activation when initial connection or tool synchronization fails (default `false`) |
48
+ | `reconnect.enabled` | both | no | Reconnect automatically after a lost connection (default `true`) |
49
+ | `reconnect.initialDelayMs` | both | no | First reconnect delay in ms; doubles per consecutive failed attempt (default 500) |
50
+ | `reconnect.maxDelayMs` | both | no | Backoff ceiling in ms; also the uptime after which the attempt budget resets (default 30000) |
51
+ | `reconnect.maxAttempts` | both | no | Consecutive failed attempts per outage before giving up for good (default 10) |
52
+
53
+ ## Tool naming
54
+
55
+ Every MCP tool has two names: the raw MCP name (sent on the wire in `tools/call`) and the public name `mcp__<serverName>__<rawName>` registered on `ctx.tools`. Public names are normalized to the DeepSeek function-name contract (64 chars, `[A-Za-z0-9_-]`); when replacement or truncation changes the name, a deterministic 12-hex-char hash of `(serverName, rawName)` is appended so distinct tools never collapse into one name. Names are pure functions of `(serverName, rawName)` — connection order, re-syncs, and other servers never rename a tool.
56
+
57
+ - Two servers publishing the same raw name (e.g. `search`) coexist under their namespaces.
58
+ - A duplicate `serverName` across live instances fails the later plugin instance at load.
59
+ - A server listing the same tool name twice is rejected as an invalid tool list.
60
+ - A foreign registration squatting on this server's namespace rolls back the whole generation (never a partial set), with a loud error.
61
+
62
+ ## Behavior
63
+
64
+ - On connect: plugin activation awaits `listTools()` and registers each tool via `ctx.tools.register()` under its public name before the composition starts its first turn. Initial connection, discovery, or registration failure is always logged; it rejects activation when `failOnStartupError` is true and otherwise activates with no tools.
65
+ - Listens for `notifications/tools/list_changed` → re-syncs; a fetch-phase failure keeps the previous generation registered, while a registration conflict rolls back the attempted generation and leaves no tools from that server.
66
+ - Tool execute: `client.callTool({ name: rawName, arguments }, { signal })` with timeout + abort support—the public name is never sent to the server.
67
+ - Canonical success is `{ content: JsonValue[], structuredContent? }`; complete JSON MCP blocks survive for programmatic callers. A supported advertised `outputSchema` validates `structuredContent`; unsupported schema vocabulary falls back to unconstrained `JsonValue`.
68
+ - Native/model rendering preserves MCP block order. Text-like runs join with newlines; resource links keep their name and URI as text; supported images become durable core image blocks only when `ctx.attachments` is mounted and the exact calling model route explicitly declares image input. The whole image batch is decoded and admitted before any member is saved. A malformed/refused image batch, audio, embedded resources, and unsupported blocks become explicit diagnostic text rather than disappearing.
69
+ - On disconnect/crash: the supervisor restarts the original server config with exponential backoff (`reconnect.initialDelayMs` doubling up to `reconnect.maxDelayMs`) and re-runs discovery on success — the recovered generation replaces the previous one, so tools neither duplicate nor leak. During the outage the last good generation stays registered; calls against it fail until recovery.
70
+ - Reconnection is budgeted per outage: after `reconnect.maxAttempts` consecutive failures the server's tools are unregistered and reconnection stops until an HMR reload or Host restart. A connection that survives past `maxDelayMs` resets the budget, so an occasionally-crashing server recovers indefinitely while a crash-looping one — even with briefly successful connects — still exhausts the cap instead of restarting forever.
71
+ - Reconnect states are user-visible in logs: reconnecting (warn, with attempt count and delay), recovered (info), final failure and disabled-loss (error). Disposal cancels any pending reconnect. With `reconnect.enabled: false`, a lost connection keeps tools registered but failing until a reload — the manual-recovery behavior.
72
+
73
+ ## Events
74
+
75
+ | Event | Mode | Payload |
76
+ |---|---|---|
77
+ | `mcp-client/status` | emit | `serverName`, `status: 'connecting' \| 'connected' \| 'reconnecting' \| 'failed' \| 'disposed'`, `toolCount` |
78
+
79
+ Published at each supervisor commit point: attempt start (`connecting`), connect + initial sync settled (`connected`), backoff armed (`reconnecting`), give-up / reconnect-disabled loss / failed generation that never closed (`failed`), and disposal (`disposed`). A completed tool sync also republishes the current status because `toolCount` changes without a status transition — including a second `failed`/`disposed` carrying `toolCount: 0` once the queued unregistration runs. The emitting fiber's context and every ancestor observe the event through the shared Cordis event bus; `serverName` disambiguates concurrent instances. Listener failures are contained and logged by the emitter, so an observer defect cannot disrupt the supervisor. The fiber lifecycle alone cannot stand in for this event: with `failOnStartupError: false` a failing server still reaches an `active` fiber inside its reconnect loop.
80
+
81
+ ## Services consumed
82
+
83
+ | Service | Usage |
84
+ |---|---|
85
+ | `ctx.tools` | Register/unregister MCP tools |
86
+ | `ctx.attachments` | Optionally validate and persist image result batches before model projection |
87
+ | `ctx.llm` | Optionally prove the exact calling route explicitly supports image input |
88
+
89
+ ## Model Experience
90
+
91
+ ### Discovered MCP tools
92
+
93
+ #### What the model sees
94
+
95
+ After initial discovery succeeds, each advertised MCP tool appears as a native tool named `mcp__<serverName>__<rawName>` (or its deterministic normalized form), with the server-provided description and input schema. A successful re-sync — including the one after an automatic reconnect — replaces the generation; plugin disposal or an exhausted reconnect budget removes it.
96
+
97
+ #### Token effect
98
+
99
+ Data-dependent schema cost is paid on every request while the tools are registered. Re-sync replaces rather than accumulates schemas, and the server-qualified name adds tokens to every tool definition and call.
100
+
101
+ #### KV Cache effect
102
+
103
+ Prefix-stable while the discovered tool set and schemas are unchanged. A re-sync that adds, removes, renames, or changes a tool replaces definitions and may invalidate reuse from the first changed schema token; a reconnect that recovers an unchanged list reproduces identical definitions and stays prefix-stable.
104
+
105
+ ### Tool-call history and results
106
+
107
+ #### What the model sees
108
+
109
+ The public tool name and JSON arguments remain in assistant history. The execution-local canonical value always retains the complete JSON MCP blocks and optional structured content for programmatic and Code Mode callers. In Native context, supported image blocks are durably projected beside text in their original order after exact route-capability proof; Code Mode additionally ferries that settled rich projection through the outer `run_code` result without changing the canonical binding value. Refused images, audio, embedded resources, resource links, and unknown blocks remain visible as bounded text diagnostics, and MCP `isError` rejects the call before image persistence.
110
+
111
+ #### Token effect
112
+
113
+ Arguments, mapped text, and durable image references are retained until compaction. Inline MCP base64 stays only in the execution-local canonical value and is never copied into a session event; the provider reads verified bytes from the attachment store. Audio and embedded-resource payloads stay out of model context.
114
+
115
+ #### KV Cache effect
116
+
117
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
118
+
119
+ ## Known Limitations and Deferred Work
120
+
121
+ - **Tools are the only bridged MCP capability** — Resources and Prompts have no harness consumer and are deferred.
122
+ - **Startup timeout is inherited from the MCP SDK** — DSH does not yet expose a connection/discovery timeout. Each initialize or paginated `tools/list` request uses the SDK's 60-second default, so an unresponsive server or cursor chain can delay both activation and teardown while the initial synchronization settles.
123
+ - **Reconnect triggers on transport close** — a crashed stdio child fires it; Streamable HTTP failures surface per request and through the SDK transport's own SSE-stream recovery, so an unreachable HTTP server is retried per call rather than respawned by the supervisor.
124
+ - **Image is the only durable rich-result bridge** — PNG, JPEG, WebP, and GIF can enter Native context after exact capability proof. Audio and embedded-resource payloads remain execution-local with explicit diagnostics, while resource links preserve only their name and URI as text.
125
+ - **Unsupported MCP output schemas are not enforced** — `structuredContent` falls back to `JsonValue` when the advertised schema uses vocabulary outside the harness subset.
package/README.zh.md ADDED
@@ -0,0 +1,125 @@
1
+ # @deepseek-ai/dsh-mcp-client
2
+
3
+ [English](README.md) | 中文
4
+
5
+ MCP 客户端桥接插件:连接外部 [Model Context Protocol](https://modelcontextprotocol.io/) 服务器,把它们的工具注册到 `ctx.tools`,使模型能够通过服务器限定名称(`mcp__<serverName>__<rawName>`)将其作为原生工具使用。
6
+
7
+ ## 用法
8
+
9
+ `cordis.yml` 中每个 MCP 服务器使用一个插件实例:
10
+
11
+ ```yaml
12
+ - id: mcp-github
13
+ name: '@deepseek-ai/dsh-mcp-client'
14
+ config:
15
+ serverName: github
16
+ transport: stdio
17
+ command: npx
18
+ args: ['-y', '@modelcontextprotocol/server-github']
19
+ env:
20
+ GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
21
+
22
+ - id: mcp-web
23
+ name: '@deepseek-ai/dsh-mcp-client'
24
+ config:
25
+ serverName: web
26
+ transport: streamable-http
27
+ url: http://localhost:3000/mcp
28
+ headers:
29
+ Authorization: !!js '`Bearer ${process.env.MCP_TOKEN}`'
30
+ ```
31
+
32
+ 模型会看到 `mcp__github__create_issue`、`mcp__web__search` 等工具,这与 Claude Code 和 Codex 使用的服务器限定形状相同。HMR(热模块替换)支持热替换:编辑配置项会触发断开 + 重新连接,无需重启进程;`serverName` 不变时会生成完全相同的工具名称。
33
+
34
+ ## 配置
35
+
36
+ | 字段 | 传输 | 必填 | 描述 |
37
+ |---|---|---|---|
38
+ | `transport` | 两者 | 是 | `"stdio"` 或 `"streamable-http"` |
39
+ | `serverName` | 两者 | 是 | 该服务器面向模型工具名称的 namespace;`[A-Za-z0-9_-]{1,32}`,在存活实例中唯一 |
40
+ | `command` | stdio | 是 | 要 spawn 的可执行文件 |
41
+ | `args` | stdio | 否 | 传给命令的参数 |
42
+ | `env` | stdio | 否 | 合并到已清理环境中的额外环境变量 |
43
+ | `cwd` | stdio | 否 | 子进程工作目录 |
44
+ | `url` | http | 是 | MCP 服务器 URL |
45
+ | `headers` | http | 否 | 额外标头(例如认证 token) |
46
+ | `toolCallTimeoutMs` | 两者 | 否 | 每次 `callTool` 调用的超时(默认 60000) |
47
+ | `failOnStartupError` | 两者 | 否 | 初始连接或工具同步失败时拒绝插件激活(默认 `false`) |
48
+ | `reconnect.enabled` | 两者 | 否 | 连接丢失后自动重新连接(默认 `true`) |
49
+ | `reconnect.initialDelayMs` | 两者 | 否 | 首次重连延迟(毫秒);每次连续失败尝试翻倍(默认 500) |
50
+ | `reconnect.maxDelayMs` | 两者 | 否 | 退避上限(毫秒);同时也是重置尝试预算所需的正常运行时长(默认 30000) |
51
+ | `reconnect.maxAttempts` | 两者 | 否 | 每次中断期间连续失败尝试次数上限,超出后彻底放弃(默认 10) |
52
+
53
+ ## 工具命名
54
+
55
+ 每个 MCP 工具都有两个名称:通过 `tools/call` 在协议上传送的原始 MCP 名称,以及公开名称 `mcp__<serverName>__<rawName>`,后者注册到 `ctx.tools`。公开名称会规范化为 DeepSeek 函数名称约定(64 个字符、`[A-Za-z0-9_-]`);如果替换或截断改变名称,就会追加 `(serverName, rawName)` 的确定性 12 位十六进制 hash,确保不同工具绝不会折叠为同一个名称。名称是 `(serverName, rawName)` 的纯函数:连接顺序、重新同步和其他服务器永远不会重命名工具。
56
+
57
+ - 发布相同原始名称(例如 `search`)的两个服务器会在各自 namespace 下共存。
58
+ - 存活实例中的重复 `serverName` 会使后加载的插件实例失败。
59
+ - 服务器在工具列表中两次列出同一工具名称时,该列表会作为无效工具列表被拒绝。
60
+ - 外部注册抢占该服务器 namespace 时,会回滚整个世代(绝不保留部分集合),并明确报错。
61
+
62
+ ## 行为
63
+
64
+ - 连接时:插件激活会等待 `listTools()`,并在组合开始首个轮次前通过 `ctx.tools.register()` 以公开名称注册每个工具。初始连接、发现或注册失败始终会记录日志;`failOnStartupError` 为 true 时拒绝激活,否则插件仍会激活但不注册工具。
65
+ - 监听 `notifications/tools/list_changed` → 重新同步;获取阶段失败时保留上一世代的注册,注册冲突则会回滚本次尝试的世代,并且不保留该服务器的任何工具。
66
+ - 工具执行:`client.callTool({ name: rawName, arguments }, { signal })`,支持超时 + 中止;公开名称绝不会发给服务器。
67
+ - 规范成功值是 `{ content: JsonValue[], structuredContent? }`;完整的 JSON MCP 块会保留给编程调用方。受支持且已声明的 `outputSchema` 会验证 `structuredContent`;不受支持的 schema 词汇会回退为不受约束的 `JsonValue`。
68
+ - Native/模型渲染会保留 MCP 块顺序。文本类连续块以换行连接;资源链接以文本保留名称和 URI;只有挂载 `ctx.attachments` 且确切调用模型路由明确声明支持图片输入时,受支持的图片才会成为持久核心图片块。整个图片批次会先完成解码与准入,再保存任一成员。格式错误或被拒绝的图片批次、音频、嵌入资源和不受支持的块会成为明确诊断文本,而不会消失。
69
+ - 断开/崩溃时:supervisor 以指数退避(`reconnect.initialDelayMs` 逐次翻倍,上限 `reconnect.maxDelayMs`)重启原始服务器配置,成功后重新执行发现——恢复的世代会替换前一个,因此工具既不会重复也不会泄漏。中断期间最后一个正常世代保持注册;针对它的调用在恢复前会失败。
70
+ - 重连按中断预算控制:连续失败达到 `reconnect.maxAttempts` 次后,该服务器的工具会被注销,重连停止,直到 HMR 重载或重启 Host。连接存活超过 `maxDelayMs` 会重置预算,因此偶尔崩溃的服务器可以无限恢复,而崩溃循环的服务器——即使短暂连接成功——仍会耗尽上限而非永远重启。
71
+ - 重连状态在日志中对用户可见:reconnecting(warn,含尝试次数和延迟)、recovered(info)、最终失败和 disabled-loss(error)。dispose(资源释放)会取消任何待执行的重连。设置 `reconnect.enabled: false` 时,连接丢失后工具保持注册但调用失败,直到重载——即手动恢复行为。
72
+
73
+ ## 事件
74
+
75
+ | 事件 | 模式 | 载荷 |
76
+ |---|---|---|
77
+ | `mcp-client/status` | emit | `serverName`、`status: 'connecting' \| 'connected' \| 'reconnecting' \| 'failed' \| 'disposed'`、`toolCount` |
78
+
79
+ 在 supervisor 的每个 commit(提交)点发布:尝试开始(`connecting`)、连接加初始同步完成(`connected`)、退避定时器已布防(`reconnecting`)、放弃/禁用重连后的丢失/世代未正常关闭(`failed`)、以及 dispose(资源释放,`disposed`)。一次完成的工具同步也会重新发布当前状态,因为 `toolCount` 会在状态不变时变化——包括排队注销执行后携带 `toolCount: 0` 的第二个 `failed`/`disposed`。发射 fiber 的上下文与每个祖先上下文都会通过共享的 Cordis 事件总线观察到该事件;`serverName` 用于区分并发实例。监听器失败由发射方包含并记录日志,观察者缺陷不会干扰 supervisor。仅凭 fiber 生命周期无法替代该事件:`failOnStartupError` 为 `false` 时,连接失败的服务器仍会在重连循环中达到 `active` 的 fiber。
80
+
81
+ ## 消费的服务
82
+
83
+ | 服务 | 用途 |
84
+ |---|---|
85
+ | `ctx.tools` | 注册/注销 MCP 工具 |
86
+ | `ctx.attachments` | 可选;在模型投影前校验并持久保存图片结果批次 |
87
+ | `ctx.llm` | 可选;证明确切调用路由明确支持图片输入 |
88
+
89
+ ## 模型体验
90
+
91
+ ### 已发现的 MCP 工具
92
+
93
+ #### 模型看到的内容
94
+
95
+ 初始发现成功后,每个已声明的 MCP 工具都会显示为名为 `mcp__<serverName>__<rawName>`(或其确定性规范化形式)的原生工具,并携带服务器提供的描述和输入 schema。成功的重新同步——包括自动重连后的同步——会替换整个世代;对插件执行 dispose(资源释放)或重连预算耗尽会移除该世代。
96
+
97
+ #### Token 影响
98
+
99
+ 工具注册期间,每次请求都会承担数据相关的 schema 成本。重新同步会替换而非累积 schema,服务器限定名称也会为每个工具定义和调用增加 token。
100
+
101
+ #### KV Cache 影响
102
+
103
+ 只要已发现工具集合及其 schema 不变,前缀就保持稳定。增加、移除、重命名或更改工具的重新同步会替换定义,并可能使从第一个变化的 schema token 起的复用失效;恢复了未变列表的重连会生成完全相同的定义,前缀保持稳定。
104
+
105
+ ### 工具调用历史与结果
106
+
107
+ #### 模型看到的内容
108
+
109
+ 公开工具名称和 JSON 参数会保留在 assistant 历史中。执行局部的规范值始终为程序化调用方和 Code Mode 保留完整 JSON MCP 块及可选结构化内容。在 Native 上下文中,受支持的图片块会在确切路由能力得到证明后,按原始顺序与文本一起持久投影;Code Mode 还会经外层 `run_code` 结果转运这份已经结算的丰富投影,而不改变规范绑定值。被拒绝的图片、音频、嵌入资源、资源链接和未知块会继续以有界文本诊断可见;MCP `isError` 会在持久化图片前拒绝调用。
110
+
111
+ #### Token 影响
112
+
113
+ 参数、映射后的文本和持久图片引用会保留到压缩(compaction)发生时。内联 MCP base64 只存在于执行局部的规范值中,绝不会复制进会话事件;提供方会从附件存储读取经过校验的字节。音频和嵌入资源载荷仍不会进入模型上下文。
114
+
115
+ #### KV Cache 影响
116
+
117
+ 仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV-cache 条目失效。
118
+
119
+ ## 已知限制与暂缓事项
120
+
121
+ - **只桥接 MCP 的工具能力**:资源和提示词没有 harness 消费接口,暂缓实现。
122
+ - **启动超时继承自 MCP SDK**:DSH 尚未公开连接/发现超时。每次 initialize 请求或分页 `tools/list` 请求都使用 SDK 默认的 60 秒,因此在初始同步完成期间,无响应的 server 或 cursor chain 可能同时延迟激活与 teardown。
123
+ - **重连在传输关闭时触发**:崩溃的 stdio 子进程会触发重连;Streamable HTTP 失败通过每次请求以及 SDK 传输自身的 SSE(Server-Sent Events)流恢复机制暴露,因此不可达的 HTTP 服务器会按调用重试,而非由 supervisor 重新 spawn。
124
+ - **图片是唯一的持久丰富结果桥接**:PNG、JPEG、WebP 和 GIF 可以在确切能力得到证明后进入 Native 上下文。音频和嵌入资源载荷仍只存在于执行局部,并配有明确诊断;资源链接只以文本保留名称和 URI。
125
+ - **不强制执行不受支持的 MCP 输出 schema**:已声明 schema 使用 harness 子集之外的词汇时,`structuredContent` 会回退到 `JsonValue`。