Skip to content

feat(web-shell): add Channel configuration flows - #7893

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
qqqys:codex/issue-7209-channel-config-page
Jul 28, 2026
Merged

feat(web-shell): add Channel configuration flows#7893
wenshao merged 2 commits into
QwenLM:mainfrom
qqqys:codex/issue-7209-channel-config-page

Conversation

@qqqys

@qqqys qqqys commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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

  1. Start Web Shell with a bearer token, open a workspace, and choose Channels. Confirm only DingTalk, WeCom, and Feishu are offered.
  2. Choose DingTalk and confirm the form shows instance identity, Client ID, Client Secret, and Pairing/Open access. Save a valid configuration and confirm it appears in Configured channels.
  3. Edit the configuration and confirm the instance name is immutable, stored secrets are not revealed, Keep is selected by default, and Replace/Clear require an explicit choice.
  4. Delete the configuration and confirm the destructive confirmation removes it.
  5. With two clients on the same workspace, mutate the configuration in one client and submit the stale editor in the other. Confirm the editor stays open and offers Reload latest.
  6. Switch workspaces while an editor or delete confirmation is open. Confirm the pending interaction closes instead of carrying old workspace data forward.

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

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Node.js 22 workspace install, Web Shell Vite development server, and Playwright Chromium.

Risk & Scope

  • Main risk or tradeoff: The editor follows daemon field descriptors and preserves unexposed public configuration during edits; future descriptor changes therefore remain server-validated.
  • Not validated / out of scope: QR-based application registration, pairing-request approval management, advanced shared Channel fields, and Channel types other than DingTalk, WeCom, and Feishu.
  • Breaking changes / migration notes: None.

Linked Issues

Part of #7209. Follows #7793.

中文说明

本 PR 做了什么

为 Web Shell 增加首个可写的频道配置体验,目前覆盖钉钉、企业微信和飞书。用户可以根据 daemon 提供的平台目录创建类型化配置,选择配对或开放准入策略,编辑公开字段而不暴露已保存凭据,明确选择保留、替换或清除密钥,并在确认后删除已配置频道。

每次写操作都会携带当前工作区 revision。过期写入会让弹窗保持打开并提供加载最新配置的操作;切换工作区时会使已打开的编辑器和删除确认失效,避免把一个工作区的数据提交到另一个工作区。

为什么需要

已合入的频道管理阶段让工作区级状态和生命周期操作在 Web Shell 中可见,但配置仍需要在 Web Shell 外修改设置。本阶段补齐核心的新建、编辑和删除闭环,同时把首发范围限制在已确认的三个企业平台。

Reviewer 测试计划

如何验证

  1. 携带 bearer token 启动 Web Shell,打开一个工作区并进入频道页,确认只提供钉钉、企业微信和飞书。
  2. 选择钉钉,确认表单包含实例标识、Client ID、Client Secret 和配对/开放准入策略。保存有效配置后,确认它出现在已配置频道中。
  3. 编辑该配置,确认实例名称不可修改、已保存密钥不会显示、默认选择保留,并且替换/清除必须由用户明确选择。
  4. 删除该配置,确认危险操作确认后会将其移除。
  5. 使用两个客户端连接同一工作区,在一个客户端修改配置后,从另一个客户端提交过期编辑器,确认编辑器保持打开并提供加载最新配置的操作。
  6. 在编辑器或删除确认打开时切换工作区,确认待处理交互会关闭,不会携带旧工作区数据。

证据(修改前后)

Web Shell 视觉预览任务会自动发布频道管理页、新建频道编辑器和已有频道编辑器在暗色/亮色主题下的前后对比图。本地 Chromium 还完整执行了新建 → revision 刷新 → 删除流程,并断言了精确的写请求内容。

测试平台

系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

Node.js 22 workspace 安装、Web Shell Vite 开发服务器和 Playwright Chromium。

风险与范围

  • 主要风险或取舍:编辑器遵循 daemon 字段描述,并在编辑时保留未暴露的公开配置;未来字段描述变化仍由服务端校验兜底。
  • 未验证 / 不在范围内:扫码创建平台应用、配对请求审批管理、高级共享频道字段,以及钉钉、企业微信、飞书之外的频道类型。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

属于 #7209 的一部分,承接 #7793

@qqqys

qqqys commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

E2E test report

Browser flow

  • ✅ Chromium: opened the Channel manager with bearer-token access
  • ✅ Created a DingTalk configuration through the typed form
  • ✅ Asserted the exact PUT body, including revision 1, Pairing access, and explicit secret replacement
  • ✅ Confirmed the created instance appeared after the resource reload
  • ✅ Deleted the instance and asserted the DELETE used refreshed revision 2

Visual coverage

  • ✅ Dark and light Channel manager captures
  • ✅ Dark and light new DingTalk editor captures
  • ✅ Dark and light existing DingTalk editor captures, including stored-secret Keep / Replace / Clear controls
  • The repository visual workflow will publish the before/after composites inline on this PR.

Supporting validation

  • ✅ 19 focused component/state tests
  • ✅ Web Shell lint and typecheck
  • ✅ Repository build and workspace-wide typecheck

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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 packages/web-shell/ — no core paths touched. Production logic is ~1044 lines (4 source files), test lines ~840 (5 test/spec files), CSS ~189 lines, E2E mock ~73 lines. The 1000+ production-line advisory applies: this is a sizable feature, but the scope is well-contained within the Channel manager surface and the split into pure state logic + dialog component + page integration keeps each file focused. No splitting needed.

Approach: The scope feels right. Clean three-layer architecture: channel-editor-state.ts (pure draft/validation/request logic), ChannelEditorDialog.tsx (form UI), and ChannelsManagerPage.tsx (integration with edit/delete buttons and workspace-change invalidation). Every edit in the diff serves the stated goal — no unrelated changes or drive-by refactors. The i18n additions are the expected EN/ZH pairs for the new UI strings.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个功能新增——属于 #7209 的一部分,承接已合入的 #7793 频道管理阶段。"无复现"规则不适用于功能新增。动机明确:上一阶段让频道状态和生命周期控制在 Web Shell 中可见,但配置仍需在 Web Shell 外修改设置。本阶段补齐新建/编辑/删除闭环。

方向:对齐。频道管理是已确认的路线图项目,上一阶段已合入,本阶段是自然的下一步。

规模:所有改动在 packages/web-shell/ 内——未触及核心路径。生产逻辑约 1044 行(4 个源文件),测试约 840 行(5 个测试文件),CSS 约 189 行,E2E mock 约 73 行。1000+ 生产行建议适用:这是一个较大的功能,但范围在频道管理界面内收得很好,三层拆分让每个文件职责清晰。无需拆分。

方案:范围合理。干净的三层架构:纯状态逻辑、表单组件、页面集成。diff 中的每一处改动都服务于既定目标——没有无关改动或顺手重构。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 5712fc1f6fee4444c77dbffdfcdae7536dec5f8e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Clean implementation with a well-structured architecture. The three-layer split works well: channel-editor-state.ts holds pure draft creation, validation, and request building (no React imports, fully unit-testable); ChannelEditorDialog.tsx is the form UI driven by daemon field descriptors; and ChannelsManagerPage.tsx wires everything together with edit/delete buttons, workspace-change invalidation, and revision-guarded mutations.

What stands out positively:

  • Secret handling is secure — stored secrets are never exposed in the UI. The preserve/replace/clear operations give users explicit control, and the default is preserve for existing instances. The buildChannelUpsertRequest function correctly spreads the existing instance config to preserve hidden public settings (like sessionScope, model) that the editor doesn't surface.
  • Workspace-change safety: the useEffect on workspace.workspaceCwd resets all pending state (editor, deleteTarget, busy, errors), and both the editor render and deleteChannel callback guard on workspaceCwd match — data from one workspace cannot be submitted to another.
  • Name validation blocks prototype pollution vectors (__proto__, constructor, prototype) and the reserved name all.
  • The E2E test (web-shell.channels.spec.ts) asserts exact mutation bodies including the revision increment across create → delete, which is thorough.
  • The mock daemon correctly implements revision checking and returns 409 on stale writes, enabling the stale-write UI path to be tested.

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)
File What changed
packages/web-shell/client/components/channels/channel-editor-state.ts New pure state module: draft creation from descriptors, validation, upsert request building with secret operations and config preservation
packages/web-shell/client/components/channels/channel-editor-state.test.ts Unit tests for draft creation, validation codes, secret operations, and config preservation across edit
packages/web-shell/client/components/channels/ChannelEditorDialog.tsx New dialog component: descriptor-driven form with secret keep/replace/clear, access policy radio cards, stale-write reload path
packages/web-shell/client/components/channels/ChannelEditorDialog.test.tsx Component tests: secret preservation default, clear for optional secrets, typed submission body, stale-write and reload-failure UI
packages/web-shell/client/components/channels/ChannelEditorDialog.module.css New styles for the editor dialog: form layout, secret state panel, policy card grid, responsive and reduced-motion support
packages/web-shell/client/components/channels/ChannelsManagerPage.tsx Integration: edit/delete buttons per channel, platform cards become clickable buttons, editor and delete confirmation dialogs, workspace-change reset
packages/web-shell/client/components/channels/ChannelsManagerPage.test.tsx New tests: typed editor open from platform, existing channel edit, delete with revision, workspace-change closes editor
packages/web-shell/client/components/channels/ChannelsManagerPage.module.css Platform card becomes interactive: cursor, hover, focus-visible, disabled states, reduced-motion
packages/web-shell/client/e2e/utils/mockDaemon.ts Mock daemon: PUT/DELETE channel routes with revision checking, 409 on stale writes, secret state tracking
packages/web-shell/client/e2e/web-shell.channels.spec.ts New E2E: full create-to-delete flow asserting exact mutation bodies and revision increments
packages/web-shell/client/e2e/visuals/screenshots.spec.ts Visual screenshots: channel editor (new and existing) in both themes, field descriptors for all three platforms
packages/web-shell/client/i18n.tsx EN/ZH strings for editor labels, secret operations, policy cards, validation messages, delete confirmation

Testing

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped

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.

中文说明

代码审查

实现干净,架构清晰。三层拆分效果好:channel-editor-state.ts 是纯状态逻辑(无 React 依赖,可独立单测);ChannelEditorDialog.tsx 是由 daemon 字段描述驱动的表单 UI;ChannelsManagerPage.tsx 负责集成,包括编辑/删除按钮、工作区切换失效、revision 保护的写操作。

亮点:密钥处理安全——已保存密钥不会在 UI 中暴露,preserve/replace/clear 操作给用户明确控制权;工作区切换安全——useEffect 重置所有待处理状态,编辑器和删除回调都校验 workspaceCwd 匹配;名称校验阻止原型链污染;E2E 测试断言了精确的写请求体和 revision 递增。

未发现阻塞性问题。实现遵循现有 Web Shell 约定。

测试

所有相关检查通过。macOS/Windows 测试和 CLI 集成测试跳过(web-shell 专属改动,符合预期)。视觉预览任务发布了频道编辑器在两种主题下的前后对比图。

未验证:真实场景 tmux 测试——这是 Web Shell(浏览器)功能,不是 TUI 界面,终端截图不适用。E2E Playwright 测试覆盖了完整的新建 → revision 刷新 → 删除流程。

Qwen Code · qwen3.8-max-preview

Reviewed at 5712fc1f6fee4444c77dbffdfcdae7536dec5f8e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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 5712fc1f6fee4444c77dbffdfcdae7536dec5f8e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 5712fc1. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

channel-editor-dark before/after

channel-editor-existing-dark before/after

channel-editor-existing-light before/after

channel-editor-light before/after

channel-manager-dark before/after

channel-manager-light before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship — CI landed green after the review. ✅

Comment on lines +90 to +92
if (secret?.operation === 'preserve') return false;
if (secret?.operation === 'clear') return true;
return !secret?.value?.trim();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复: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

Comment on lines +592 to +602
onClick={() => {
void reload().then(
() => {
setDeleteTarget(undefined);
setDeleteError(undefined);
},
(reloadError: unknown) => {
setDeleteError(extractErrorDetail(reloadError));
},
);
}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment on lines +588 to +593
<Button
className="mt-2 w-fit"
size="sm"
variant="outline"
onClick={() => {
void reload().then(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment on lines +553 to +555
onOpenChange={(open) => {
if (!open) setEditor(undefined);
}}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 pushed a commit that referenced this pull request Jul 28, 2026

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline.

中文说明

已审查。 建议见行内评论。

— qwen3.7-max via Qwen Code /review

Comment on lines +173 to +175
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const validation = validateChannelEditorDraft(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
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 处理函数缺少对 savingreloading 状态的提前返回保护。虽然提交按钮设置了 disabled={saving || reloading} 防止点击重复提交,但在表单 <input> 中按回车键会无视按钮禁用状态触发 <form>submit 事件。在 setSaving(true) 触发重新渲染之前快速按两次回车,会以相同的 expectedRevision 调用两次 onSave——第一次成功并推进 daemon revision,第二次因 revision 冲突失败,向用户显示一个意外的错误。

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Local build & runtime verification report

Verified this PR on Linux against a real full stack — no mocked daemon: packages/cli dist (qwen serve --port 4171 --token … with two --workspace dirs), the Vite dev server proxying to it, and headless Chromium (Playwright) driving the actual Web Shell UI. Observables were asserted at four layers simultaneously: DOM/ARIA, the exact wire bodies of PUT/DELETE /workspaces/:ws/channels/:name, the daemon's GET /channels snapshot, and the on-disk <workspace>/.qwen/settings.json the channel settings store writes.

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 5712fc1f6)

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

  1. Reviewer test plan step 6 (workspace switch) is not reachable via the composer selector. Web Shell's top-level DaemonWorkspaceProvider resolves workspaceCwd from capabilities.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 shows wsA · N configured, and writes land in the primary workspace's settings.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.
  2. The stale-write "dialog stays open" contract is only enforceable end-to-end. The ChannelEditorDialog unit test can't pin it (mocked onOpenChange, fixed open); 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
new dingtalk editor validation errors
Edit — stored secret, Keep/Replace only Stale write — 409 + Reload latest
edit keep stale conflict
Configured card Delete confirmation
configured delete confirm
More screenshots: empty manager, WeCom card, light theme, tokenless read-only state
Manager (empty, dark) WeCom configured
manager empty wecom
Edit dialog (light theme) No bearer token → read-only
light readonly
中文版(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 失败)

不阻塞合并的观察

  1. **Reviewer 测试计划第 6 步(切换工作区)无法通过 composer 选择器触达。**Web Shell 顶层 DaemonWorkspaceProviderworkspaceCwd 取自 capabilities.workspaceCwd(daemon 工作区);composer 的工作区选择只影响新会话去向,不会改变频道面板的作用域——运行时已验证:切到第二工作区后面板仍显示 wsA · N configured,写入仍落主工作区的 settings.json。这是 feat(web-shell): add Channel management page #7793 的既有架构而非本 PR 回归;失效守卫针对其真实触发场景(多客户端下 capabilities 刷新导致主工作区变化)依然正确,且 PR 用真实组件的 jsdom 测试钉住了该行为。手动照测试计划操作时需要知道这一点。
  2. 过期写入"弹窗保持打开"契约只能端到端保证。ChannelEditorDialog 单测无法钉住它(onOpenChange 为 mock、open 固定);后续可补一个页面级测试。本次已对真实 daemon 验证,不阻塞。

(截图见上方英文部分,共 10 张:创建/校验/编辑/409 冲突/删除确认/企微/亮色主题/无 token 只读等,均为真实 daemon + Chromium 1440×900 实拍。)

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Jul 28, 2026
Merged via the queue into QwenLM:main with commit 2db663b Jul 28, 2026
93 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants