feat(web-shell): add Channel configuration flows - #7893
Conversation
E2E test reportBrowser flow
Visual coverage
Supporting validation
|
|
Thanks for the PR! Template looks good ✓ Problem: This is a feature addition — part of #7209, following the merged #7793 Channel manager stage. The "no reproduction" rule doesn't apply to features. The motivation is clear: the previous stage made Channel status and lifecycle controls visible, but configuration still required editing settings outside Web Shell. This stage completes the create/edit/delete loop. Direction: Aligned. Channel management is an established roadmap item, the previous stage is already merged, and this is the natural next step. CHANGELOG has no direct reference but the area is clearly relevant given the merged predecessor. Size: All changes are in Approach: The scope feels right. Clean three-layer architecture: Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是一个功能新增——属于 #7209 的一部分,承接已合入的 #7793 频道管理阶段。"无复现"规则不适用于功能新增。动机明确:上一阶段让频道状态和生命周期控制在 Web Shell 中可见,但配置仍需在 Web Shell 外修改设置。本阶段补齐新建/编辑/删除闭环。 方向:对齐。频道管理是已确认的路线图项目,上一阶段已合入,本阶段是自然的下一步。 规模:所有改动在 方案:范围合理。干净的三层架构:纯状态逻辑、表单组件、页面集成。diff 中的每一处改动都服务于既定目标——没有无关改动或顺手重构。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code ReviewClean implementation with a well-structured architecture. The three-layer split works well: What stands out positively:
No critical blockers found. The implementation follows existing Web Shell conventions (CSS Modules with semantic tokens, shared UI primitives, collocated tests, bilingual i18n). Files changed (12 of 12 shown)
Testing
All relevant checks pass. macOS/Windows tests and CLI integration tests are skipped (expected for a web-shell-only change). The visual preview job posted before/after composites for the Channel editor in both themes. Not verified: real-scenario tmux testing — this is a Web Shell (browser) feature, not a TUI surface, so terminal capture does not apply. The E2E Playwright test covers the full create → revision refresh → delete flow with exact mutation body assertions. 中文说明代码审查实现干净,架构清晰。三层拆分效果好: 亮点:密钥处理安全——已保存密钥不会在 UI 中暴露,preserve/replace/clear 操作给用户明确控制权;工作区切换安全—— 未发现阻塞性问题。实现遵循现有 Web Shell 约定。 测试所有相关检查通过。macOS/Windows 测试和 CLI 集成测试跳过(web-shell 专属改动,符合预期)。视觉预览任务发布了频道编辑器在两种主题下的前后对比图。 未验证:真实场景 tmux 测试——这是 Web Shell(浏览器)功能,不是 TUI 界面,终端截图不适用。E2E Playwright 测试覆盖了完整的新建 → revision 刷新 → 删除流程。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — Solid, well-structured feature with thorough tests and clean CI; holding back the last point only because the feature surface is large enough that a human should give the UX a once-over in a real browser. This is a strong stage in the Channel manager roadmap. The architecture is exactly what I'd propose: pure state logic separated from React, descriptor-driven forms so future platform types come for free, and revision-guarded mutations with a clear stale-write recovery path. The secret handling is the hardest part of this feature and it's done right — preserve by default, explicit replace/clear, never exposing stored values. The test coverage is thorough across all three layers: pure state unit tests, component tests for the dialog (including the stale-write and reload-failure paths), and an E2E test that asserts exact mutation bodies. The workspace-change invalidation is a nice safety touch that prevents cross-workspace data leaks. No blocking concerns. The code is straightforward to maintain — the descriptor-driven approach means adding a new platform type is a data change, not a code change. 中文说明置信度:4/5 ——结构良好、测试充分、CI 全绿的功能新增;保留最后一点是因为功能面较大,建议人工在真实浏览器中过一遍 UX。 这是频道管理路线图中的一个扎实阶段。架构正是我会提出的方案:纯状态逻辑与 React 分离、描述驱动的表单让未来平台类型零成本接入、revision 保护的写操作带有清晰的过期恢复路径。密钥处理是本功能最难的部分,做得正确——默认保留、明确替换/清除、从不暴露已保存值。 测试覆盖三层:纯状态单测、对话框组件测试(含过期写入和重载失败路径)、断言精确写请求体的 E2E 测试。工作区切换失效是一个很好的安全设计,防止跨工作区数据泄漏。 无阻塞性顾虑。代码易于维护——描述驱动的方式意味着新增平台类型是数据变更,不是代码变更。 — Qwen Code · qwen3.8-max-preview Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
| if (secret?.operation === 'preserve') return false; | ||
| if (secret?.operation === 'clear') return true; | ||
| return !secret?.value?.trim(); |
There was a problem hiding this comment.
[Critical] isMissingField returns true for the 'clear' operation on required secrets, so validateChannelEditorDraft blocks saving with a "required" error — even though the UI unconditionally offers a [Clear] button and buildChannelUpsertRequest correctly serializes {operation: 'clear'} (with a dedicated passing test). — Failure scenario: user opens editor for a channel with a stored required secret (e.g. DingTalk clientSecret), clicks [Clear], then [Save]. Validation shows "This field is required" and refuses to submit. The Clear button becomes a dead control.
| if (secret?.operation === 'preserve') return false; | |
| if (secret?.operation === 'clear') return true; | |
| return !secret?.value?.trim(); | |
| if (secret?.operation === 'preserve') return false; | |
| if (secret?.operation === 'clear') return false; | |
| return !secret?.value?.trim(); |
中文说明
[Critical] isMissingField 对 'clear' 操作返回 true,导致 validateChannelEditorDraft 以"必填"错误阻止保存——即使 UI 无条件提供 [清除] 按钮,且 buildChannelUpsertRequest 已正确序列化 {operation: 'clear'}(有专门的通过测试)。—— 失败场景:用户打开带有已保存必填密钥(如钉钉 clientSecret)的频道编辑器,点击 [清除] 后点击 [保存],验证显示"此字段为必填"并拒绝提交。清除按钮变成了无效控件。
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
已修复:required secret 不再展示无效的 Clear 操作,optional secret 仍保留 Clear。验证证据:Web Shell 聚焦测试 11/11 通过;服务端 required-secret 清除拒绝契约测试 1/1 通过;npm run build、npm run typecheck、ESLint、Prettier、git diff --check 均通过。commit: 5712fc1
| onClick={() => { | ||
| void reload().then( | ||
| () => { | ||
| setDeleteTarget(undefined); | ||
| setDeleteError(undefined); | ||
| }, | ||
| (reloadError: unknown) => { | ||
| setDeleteError(extractErrorDetail(reloadError)); | ||
| }, | ||
| ); | ||
| }} |
There was a problem hiding this comment.
[Suggestion] The delete dialog's "Reload latest" error-recovery path has no test, while the analogous save-error reload path in ChannelEditorDialog is tested. — Failure scenario: a user deletes a channel, receives a 409 conflict, and clicks "Reload latest". The inline .then() handler is untested — if the success callback failed to clear deleteTarget/deleteError, or the rejection callback mis-handled the error, the dialog would be stuck showing stale state.
中文说明
[Suggestion] 删除对话框的"加载最新"错误恢复路径没有测试,而 ChannelEditorDialog 中类似的保存错误恢复路径已有测试。—— 失败场景:用户删除频道时收到 409 冲突,点击"加载最新"。内联 .then() 处理程序未经测试——如果成功回调未清除 deleteTarget/deleteError,或拒绝回调错误处理了错误,对话框将停留在过时状态。
— qwen3.7-max via Qwen Code /review
| <Button | ||
| className="mt-2 w-fit" | ||
| size="sm" | ||
| variant="outline" | ||
| onClick={() => { | ||
| void reload().then( |
There was a problem hiding this comment.
[Suggestion] The delete dialog's "Reload latest" button has no loading indicator or disabled state during the reload operation, unlike the identical button in ChannelEditorDialog which shows a spinner and sets disabled={reloading}. — Failure scenario: a user clicks "Reload latest" and the button remains fully interactive with no visual feedback while the request is in flight, allowing repeated clicks that send concurrent reload requests.
中文说明
[Suggestion] 删除对话框的"加载最新"按钮在请求进行中缺少加载指示器或 disabled 状态,而 ChannelEditorDialog 中的同类按钮显示了 spinner 并设置了 disabled={reloading}。—— 失败场景:用户点击"加载最新"后按钮保持完全可交互且无视觉反馈,可能导致重复点击发送并发请求。
— qwen3.7-max via Qwen Code /review
| onOpenChange={(open) => { | ||
| if (!open) setEditor(undefined); | ||
| }} |
There was a problem hiding this comment.
[Suggestion] The editor dialog's onOpenChange handler unconditionally dismisses the dialog (Escape, backdrop click) even while a save is in flight. The sibling delete dialog correctly guards with if (!open && !deleting), but the editor dialog lacks an equivalent guard because the saving state is internal to ChannelEditorDialog. — Failure scenario: user presses Escape while onSave is pending. The dialog unmounts; if the save later rejects, setSubmitError fires on an unmounted component and the error is silently lost.
中文说明
[Suggestion] 编辑器对话框的 onOpenChange 处理程序无条件关闭对话框(Escape 键、点击背景),即使保存操作正在进行中。相邻的删除对话框正确地使用了 if (!open && !deleting) 守卫,但编辑器缺少等效守卫,因为 saving 状态在 ChannelEditorDialog 内部。—— 失败场景:用户在 onSave 进行中按 Escape。对话框卸载;如果保存随后失败,setSubmitError 在已卸载的组件上触发,错误被静默丢失。
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.7-max via Qwen Code /review
| const submit = async (event: FormEvent<HTMLFormElement>) => { | ||
| event.preventDefault(); | ||
| const validation = validateChannelEditorDraft( |
There was a problem hiding this comment.
[Suggestion] The submit handler has no early-return guard against re-entry when saving or reloading is already true. The submit button is disabled={saving || reloading}, which prevents click-based double submission, but pressing Enter in any form <input> fires a submit event on the <form> regardless of button disabled state. Two rapid Enter keypresses before the re-render from setSaving(true) would invoke onSave twice with the same expectedRevision — the first succeeds and advances the daemon revision; the second fails with a revision conflict, displaying a confusing error.
| const submit = async (event: FormEvent<HTMLFormElement>) => { | |
| event.preventDefault(); | |
| const validation = validateChannelEditorDraft( | |
| const submit = async (event: FormEvent<HTMLFormElement>) => { | |
| event.preventDefault(); | |
| if (saving || reloading) return; | |
| const validation = validateChannelEditorDraft( |
中文说明
[Suggestion] submit 处理函数缺少对 saving 或 reloading 状态的提前返回保护。虽然提交按钮设置了 disabled={saving || reloading} 防止点击重复提交,但在表单 <input> 中按回车键会无视按钮禁用状态触发 <form> 的 submit 事件。在 setSaving(true) 触发重新渲染之前快速按两次回车,会以相同的 expectedRevision 调用两次 onSave——第一次成功并推进 daemon revision,第二次因 revision 冲突失败,向用户显示一个意外的错误。
— qwen3.7-max via Qwen Code /review
Local build & runtime verification reportVerified this PR on Linux against a real full stack — no mocked daemon: Verdict: LGTM. All flows behave as described; 30/30 runtime checks passed, the new tests are mutation-tested and have teeth. Two non-blocking observations below. Static & suite results (PR branch
|
| Check | Result |
|---|---|
vitest channels suite (4 files added/updated by this PR) |
✅ 22/22 |
Full web-shell unit suite |
✅ 2387 passed, 1 failed — the composerTag icon-URL failure also fails on the base commit 6a432ad2e (environment-specific, unrelated) |
tsc --noEmit |
✅ 0 errors |
eslint on all changed files |
✅ clean |
PR's Playwright e2e (web-shell.channels.spec.ts) |
✅ passed |
| Full web-shell e2e suite (chromium + mobile-chromium) | ✅ 37 passed |
Runtime verification against a real daemon (30/30 checks)
The real catalog served by the daemon lists telegram/weixin/qq as manageable: false and exactly dingtalk/wecom/feishu as manageable — the UI's allowlist and the server's manageable flag agree on real data.
| # | Scenario | Evidence |
|---|---|---|
| S1 | Catalog gating: only DingTalk/WeCom/Feishu offered; Telegram/WeChat/QQ hidden | DOM |
| S2 | Create DingTalk: required-field + reserved-name (all) validation blocks the submit client-side (no request on invalid submit); valid save sends exactly {expectedRevision, config:{type,clientId,senderPolicy:'pairing'}, secrets:{clientSecret:{operation:'replace',value}}} and the channel lands in the workspace settings.json |
wire + disk |
| S2 | Secret redaction: after save, GET /channels contains {present:true, source:'literal'} and never the secret value |
API |
| S3 | Edit: instance name immutable, "Stored securely" badge, Keep preselected, secret ops exactly Keep/Replace (no Clear for a required secret — the 5712fc1f6 fix holds), stored secret value never present in the DOM |
DOM |
| S3 | Saving public-field edits sends {operation:'preserve'} — on-disk secret byte-identical after the edit |
wire + disk |
| S4 | Replace flow: password input appears only after explicit Replace; new secret persisted | disk |
| S5 | Stale write: second browser client mutates first → first client's save gets a real HTTP 409, dialog stays open with the server detail + Reload latest; the stale value never reaches disk; after reload the reopened editor shows the winner's data |
wire + DOM + disk |
| S6 | Delete: destructive confirmation, DELETE carries expectedRevision, card and settings.json entry both removed |
wire + disk |
| S7 | WeCom typed form: optional empty wsUrl omitted from the config payload |
wire |
| S8 | No bearer token → read-only banner, all Configure/Edit/Delete controls disabled (canManage guard) |
DOM |
| S9 | Zero unexpected console/page errors across all flows | console |
Mutation testing (do the new tests have teeth?)
| Mutation | Outcome |
|---|---|
Stored secret defaults to replace instead of preserve |
🗡 killed (4 tests) |
| Drop duplicate-name validation | 🗡 killed |
Stop spreading instance.config (lose unexposed public fields) |
🗡 killed |
Offer Clear for a required secret (revert the follow-up fix) |
🗡 killed |
| Remove both workspace-invalidation layers (reset effect + render guard) | 🗡 killed by closes an editor when the selected workspace changes |
| Remove either workspace-invalidation layer alone | survives — the two layers are individually redundant (belt-and-braces; fine) |
| Close the editor on save failure (break the stale-write UX) | survives the unit suite — structurally untestable there because the dialog's open is parent-controlled and mocked; caught by the real-daemon run (S5 fails under this mutation) |
Non-blocking observations
- Reviewer test plan step 6 (workspace switch) is not reachable via the composer selector. Web Shell's top-level
DaemonWorkspaceProviderresolvesworkspaceCwdfromcapabilities.workspaceCwd(the daemon primary workspace); the composer's workspace picker changes where sessions are created but never rescopes the Channels panel — verified at runtime: after switching the composer to the second workspace the panel still showswsA · N configured, and writes land in the primary workspace'ssettings.json. That is pre-existing feat(web-shell): add Channel management page #7793 architecture, not a regression of this PR, and the invalidation guard is still correct for its real trigger (capability refreshes changing the primary in multi-client setups), which the PR pins with a real-component jsdom test. Worth knowing when following the test plan manually. - The stale-write "dialog stays open" contract is only enforceable end-to-end. The
ChannelEditorDialogunit test can't pin it (mockedonOpenChange, fixedopen); a page-level test would close that gap. Verified here against the real daemon, so not blocking.
Screenshots (real daemon, Chromium 1440×900)
| Create (dark) | Validation |
|---|---|
![]() |
![]() |
| Edit — stored secret, Keep/Replace only | Stale write — 409 + Reload latest |
|---|---|
![]() |
![]() |
| Configured card | Delete confirmation |
|---|---|
![]() |
![]() |
More screenshots: empty manager, WeCom card, light theme, tokenless read-only state
| Manager (empty, dark) | WeCom configured |
|---|---|
![]() |
![]() |
| Edit dialog (light theme) | No bearer token → read-only |
|---|---|
![]() |
![]() |
中文版(Chinese version)
本地构建与运行时验证报告
在 Linux 上对真实完整链路验证了本 PR——未使用 mock daemon:packages/cli dist(qwen serve --port 4171 --token …,两个 --workspace)+ Vite dev 代理 + 无头 Chromium(Playwright)驱动真实 Web Shell UI。断言同时覆盖四个层面:DOM/ARIA、PUT/DELETE /workspaces/:ws/channels/:name 的精确请求体、daemon 的 GET /channels 快照、以及频道设置存储写入磁盘的 <workspace>/.qwen/settings.json。
**结论:LGTM。**所有流程行为符合描述;30/30 运行时检查通过,新增测试经变异测试确认有效。两条不阻塞合并的观察见下。
静态与套件结果(PR 分支 5712fc1f6)
| 检查 | 结果 |
|---|---|
vitest channels 套件(本 PR 新增/修改的 4 个文件) |
✅ 22/22 |
| web-shell 全量单测 | ✅ 2387 通过,1 失败——composerTag 图标 URL 用例在基底提交 6a432ad2e 上同样失败(环境相关,与本 PR 无关) |
tsc --noEmit |
✅ 0 错误 |
变更文件 eslint |
✅ 通过 |
PR 自带 Playwright e2e(web-shell.channels.spec.ts) |
✅ 通过 |
| web-shell 全量 e2e(chromium + mobile-chromium) | ✅ 37 通过 |
真实 daemon 运行时验证(30/30 检查)
真实 daemon 返回的目录中 telegram/weixin/qq 为 manageable: false,恰好只有钉钉/企微/飞书可管理——UI 白名单与服务端 manageable 标志在真实数据上一致。
| # | 场景 | 证据层 |
|---|---|---|
| S1 | 目录门控:仅提供钉钉/企微/飞书;Telegram/微信/QQ 隐藏 | DOM |
| S2 | 新建钉钉:必填项与保留名(all)校验在客户端拦截(无效提交不发请求);有效保存的请求体精确为 {expectedRevision, config:{type,clientId,senderPolicy:'pairing'}, secrets:{clientSecret:{operation:'replace',value}}},并落盘到工作区 settings.json |
请求体 + 磁盘 |
| S2 | 密钥脱敏:保存后 GET /channels 仅含 {present:true, source:'literal'},不含密钥明文 |
API |
| S3 | 编辑:实例名不可改,"已安全保存"徽标,默认选中"保留",密钥操作仅 保留/替换(必填密钥无"清除"——5712fc1f6 修复生效),已存密钥值从不出现在 DOM |
DOM |
| S3 | 仅改公开字段时发送 {operation:'preserve'}——编辑后磁盘上的密钥逐字节不变 |
请求体 + 磁盘 |
| S4 | 替换流程:仅在明确点"替换"后出现密码输入框;新密钥已持久化 | 磁盘 |
| S5 | 过期写入:第二个浏览器客户端先修改 → 第一个客户端保存收到真实 HTTP 409,弹窗保持打开并显示服务端详情 + 加载最新配置;过期值未落盘;重载后重开编辑器显示对方数据 |
请求体 + DOM + 磁盘 |
| S6 | 删除:危险操作确认,DELETE 携带 expectedRevision,卡片与 settings.json 条目同时移除 |
请求体 + 磁盘 |
| S7 | 企微类型化表单:可选的 wsUrl 留空时不出现在 config 中 |
请求体 |
| S8 | 无 bearer token → 只读提示,所有配置/编辑/删除控件禁用(canManage 守卫) |
DOM |
| S9 | 全流程无预期外的控制台/页面错误 | 控制台 |
变异测试(新测试是否有效?)
| 变异 | 结果 |
|---|---|
已存密钥默认 replace 而非 preserve |
🗡 被击杀(4 个用例) |
| 移除重名校验 | 🗡 被击杀 |
不再展开 instance.config(丢失未暴露公开字段) |
🗡 被击杀 |
| 为必填密钥提供"清除"(还原后续修复) | 🗡 被击杀 |
| 同时移除两层工作区失效保护(reset effect + 渲染守卫) | 🗡 被 closes an editor when the selected workspace changes 击杀 |
| 只移除任一层 | 存活——两层各自冗余(双保险,合理) |
| 保存失败时关闭编辑器(破坏过期写入体验) | 单测存活——结构上无法在单测钉住(open 由父组件控制且被 mock);被真实 daemon 场景捕获(该变异下 S5 失败) |
不阻塞合并的观察
- **Reviewer 测试计划第 6 步(切换工作区)无法通过 composer 选择器触达。**Web Shell 顶层
DaemonWorkspaceProvider的workspaceCwd取自capabilities.workspaceCwd(daemon 主工作区);composer 的工作区选择只影响新会话去向,不会改变频道面板的作用域——运行时已验证:切到第二工作区后面板仍显示wsA · N configured,写入仍落主工作区的settings.json。这是 feat(web-shell): add Channel management page #7793 的既有架构而非本 PR 回归;失效守卫针对其真实触发场景(多客户端下 capabilities 刷新导致主工作区变化)依然正确,且 PR 用真实组件的 jsdom 测试钉住了该行为。手动照测试计划操作时需要知道这一点。 - 过期写入"弹窗保持打开"契约只能端到端保证。
ChannelEditorDialog单测无法钉住它(onOpenChange为 mock、open固定);后续可补一个页面级测试。本次已对真实 daemon 验证,不阻塞。
(截图见上方英文部分,共 10 张:创建/校验/编辑/409 冲突/删除确认/企微/亮色主题/无 token 只读等,均为真实 daemon + Chromium 1440×900 实拍。)
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Released in v0.21.1. |
















What this PR does
Adds the first writable Channel configuration experience to Web Shell for DingTalk, WeCom, and Feishu. Users can create typed configurations from the daemon-provided platform catalog, choose Pairing or Open access, edit public fields without exposing stored credentials, explicitly Keep, Replace, or Clear secrets, and delete a configured Channel after confirmation.
Every mutation uses the current workspace revision. Stale writes remain in the dialog with an actionable reload path, and changing workspaces invalidates open editors and delete confirmations so data from one workspace cannot be submitted to another.
Why it's needed
The merged Channel manager stage made workspace-scoped status and lifecycle controls visible, but configuration still required editing settings outside Web Shell. This stage completes the core create, edit, and delete loop while keeping the initial release limited to the three requested enterprise platforms.
Reviewer Test Plan
How to verify
Evidence (Before & After)
The Web Shell visual preview job posts before/after composites for the Channel manager plus new and existing Channel editors in both dark and light themes. The local Chromium run also exercised a complete create → revision refresh → delete flow and asserted the exact mutation bodies.
Tested on
Environment (optional)
Node.js 22 workspace install, Web Shell Vite development server, and Playwright Chromium.
Risk & Scope
Linked Issues
Part of #7209. Follows #7793.
中文说明
本 PR 做了什么
为 Web Shell 增加首个可写的频道配置体验,目前覆盖钉钉、企业微信和飞书。用户可以根据 daemon 提供的平台目录创建类型化配置,选择配对或开放准入策略,编辑公开字段而不暴露已保存凭据,明确选择保留、替换或清除密钥,并在确认后删除已配置频道。
每次写操作都会携带当前工作区 revision。过期写入会让弹窗保持打开并提供加载最新配置的操作;切换工作区时会使已打开的编辑器和删除确认失效,避免把一个工作区的数据提交到另一个工作区。
为什么需要
已合入的频道管理阶段让工作区级状态和生命周期操作在 Web Shell 中可见,但配置仍需要在 Web Shell 外修改设置。本阶段补齐核心的新建、编辑和删除闭环,同时把首发范围限制在已确认的三个企业平台。
Reviewer 测试计划
如何验证
证据(修改前后)
Web Shell 视觉预览任务会自动发布频道管理页、新建频道编辑器和已有频道编辑器在暗色/亮色主题下的前后对比图。本地 Chromium 还完整执行了新建 → revision 刷新 → 删除流程,并断言了精确的写请求内容。
测试平台
环境(可选)
Node.js 22 workspace 安装、Web Shell Vite 开发服务器和 Playwright Chromium。
风险与范围
关联 Issue
属于 #7209 的一部分,承接 #7793。