Skip to content

feat(provider-form): soften validation with "save anyway" prompt#2307

Merged
farion1231 merged 3 commits into
farion1231:mainfrom
allenxln:fix/soft-validation-bedrock
Apr 25, 2026
Merged

feat(provider-form): soften validation with "save anyway" prompt#2307
farion1231 merged 3 commits into
farion1231:mainfrom
allenxln:fix/soft-validation-bedrock

Conversation

@allenxln

Copy link
Copy Markdown
Contributor

背景

起因:#2196(Bedrock / Gemini 自定义 BASE_URL 被硬拒绝保存)、#1204(Vertex AI 支持)。详细讨论见 #2196 的评论区。

问题

ProviderForm.tsxhandleSubmit 里有 24 处 toast.error + return 一刀切拒绝保存,但:

  • 后端 src-tauri/src/gemini_config.rs 已经有「宽松保存 / 严格切换」分层:validate_gemini_settings(保存)vs validate_gemini_settings_strict(切换)
  • 前端却没跟上,导致 Bedrock、Vertex、自定义 Gemini BASE_URL 等合法配置明明后端能接受,UI 却拒绝保存

本 PR 让前端对齐后端现有模式。

改动思路

把校验分两类:

A 类:业务约束(软化 → 弹确认框)

空值、缺项,放行后数据结构仍正确,只是切换时后端 strict 校验会再兜底一次:

  • 供应商名为空
  • 非官方 claude / codex / gemini 的 API 端点为空
  • 非官方 claude / codex / gemini 的 API Key 为空
  • opencode 未选 model
  • Claude 模板变量未填
  • opencode / openclaw / hermes providerKey 为空

B 类:完整性约束(保持硬拒绝)

放行后会产生损坏数据或根本无法工作:

  • providerKey 正则不合法(会影响文件路径)
  • providerKey 与其它供应商重复(会互相覆盖)
  • Copilot / Codex OAuth 未登录(没 token 根本建立不了)
  • omo Other Fields JSON 不是对象 / JSON 解析失败
  • providerKey lock 状态加载中

实现

  • handleSubmit 中将 A 类改为 issues.push(...),B 类保留 toast.error + return
  • issues.length > 0,弹 ConfirmDialog(variant=info)列出所有问题
  • 用户选择「仍要保存」则调 performSubmit(values);「取消」则清空 state,不影响原表单
  • 抽出 performSubmit 承载原 handleSubmit 后半段(构造 payload、组装 meta、调用 onSubmit
  • 顺手把 omo Other Fields 的 JSON 格式校验(B 类)前置到入口处,避免用户点「仍要保存」后又被弹错

手动验证步骤

(由于 ProviderForm 已有 2000+ 行依赖外部 Tauri API 和多个 react-query hook,仓库现有的 AddProviderDialog.test.tsx 也是把整个 ProviderForm mock 掉的,所以 PR 未新增单测;现有 213 个单测全部通过。)

场景 1:Claude + Bedrock / Vertex AI(原 issue 核心诉求)

  1. 新建 Claude 供应商 → 选「自定义」类型
  2. 只填 ANTHROPIC_AUTH_TOKEN 的值为空,或仅通过 env 中的 AWS_* / GOOGLE_APPLICATION_CREDENTIALS 等环境变量认证
  3. 点「添加」
  4. ✅ 预期:弹出确认框列出「非官方供应商请填写 API 端点 / API Key」;点「仍要保存」后成功保存

场景 2:Gemini 自定义 BASE_URL

  1. 新建 Gemini 供应商 → 选「自定义」
  2. 填入 GEMINI_API_KEYGEMINI_API_BASE_URL(不填 UI 里的端点输入框)
  3. ✅ 预期:弹确认框 → 仍要保存 → 成功

场景 3:正常配置不被干扰

  1. 完整填写一个官方 Claude 供应商配置
  2. ✅ 预期:直接保存,不弹确认框

场景 4:B 类硬约束保持原有拒绝

  1. 新建 OpenCode 供应商 → 填入非法 providerKey(如大写 Foo_Bar
  2. ✅ 预期:显示原有 providerKeyInvalid 错误 toast,无法进入确认框流程
  3. Copilot 预设但未登录 → ✅ 仍弹「请先登录 GitHub Copilot」

自动化校验

  • pnpm typecheck
  • pnpm test:unit ✅(34 files / 213 tests all passing)
  • pnpm format

三语 i18n

新增 providerForm.softValidation.{title, hint, saveAnyway},覆盖 zh / en / ja。

替代方案与取舍

曾考虑:

  • A. 全部软化:所有 24 处都弹确认框。太激进,可能让新手保存完全坏的数据。
  • C. 只精准放行 Bedrock / Vertex:识别 env-based 认证跳过端点必填。太窄,每个新场景都要改。
  • B. 本 PR 采用:按「数据是否仍有效」分类;与后端现有「relaxed save / strict switch」呼应;默认体验不变、给进阶用户一个逃生口。

相关 issue / PR:

如果方向需要调整(比如更偏向 C),欢迎在 review 中指出,我来改动。

@farion1231 farion1231 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

感谢您的贡献,目前看方向上是没有问题的,只是对校验的放宽导致了一个意外的情况:

OpenCode / OpenClaw / Hermes 这三个应用的供应商里面,有一个内部用来区分供应商的一个 Provider Key,#2307 在 ProviderForm.tsx 把 OpenCode / OpenClaw / Hermes 的空 providerKey 改成 soft issue,但新增保存链路里 AddProviderDialog.tsx:108-114 只有 truthy 才传 providerKey,随后 src/lib/query/mutations.ts:26-35 会直接抛 Provider key is required for ${appId}。结果是用户点“仍要保存”后仍然保存失败,只是从明确的字段错误变成后续通用失败 toast。建议把空 providerKey 继续保留为硬拒绝,或者在确认保存前生成合法 key。

[P2] 确认框的提交路径绕过了 react-hook-form 的提交状态。
ProviderForm.tsx:2149-2155 里 void performSubmit(values) 不会进入 RHF 的 isSubmitting 生命周期,也没有 await/catch。保存慢或失败时,外层按钮状态不会反映真实提交中状态,失败时也更容易出现未处理 Promise。建议给软确认单独加提交中状态,或让 confirm handler await performSubmit 并处理异常。

@allenxln

Copy link
Copy Markdown
Contributor Author

感谢这么细致的 review!两条反馈都是真实的 bug,确认会修:

P1:同意你的建议 a(保留硬拒绝)。重新想了一下,providerKey 其实就是 OpenCode / OpenClaw / Hermes 三个 App 的主键 ID,本质和「providerKey 正则不合法 / 重复」一样属于完整性约束;我最初把"空"归 A 类是分类错误。自动生成 key 虽然也能走通,但会留下用户下次编辑时看不懂的陌生 ID,反而不如明确报错。

P2:会给软确认框单独加一个 isConfirmSubmitting state,onConfirmawait performSubmittry/catch/finally 处理异常;同时把这个状态反映到取消按钮和 saveAnyway 按钮的 disabled 上,避免保存中被重复点击。

修好后再 push 一版过来,请再帮忙看看。

allenxln pushed a commit to allenxln/cc-switch that referenced this pull request Apr 25, 2026
P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
Hermes. Since providerKey is the primary identity for these apps and the
mutations layer throws "Provider key is required" when absent, letting users
click "save anyway" would surface a generic error toast instead of a
precise, actionable one. Treat empty providerKey as an integrity constraint
alongside regex / duplicate checks.

P2: give the soft-confirm submit path its own submitting state. The
confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
slow or failing saves left the outer submit button responsive and could
spawn unhandled rejections. Now the confirm handler awaits performSubmit
inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
confirm and cancel clicks, and folds the flag into the outer disabled
state and onSubmittingChange callback.

Refs: farion1231#2307 review comments
@allenxln

Copy link
Copy Markdown
Contributor Author

已推 910ccc98 修复两条 review:

P1:opencode / openclaw / hermes 的空 providerKey 移回硬拒绝。regex / duplicate 的原有保护一并保留(之前顺带加的 `.trim() &&` 短路也移除了,避免和「空即硬拒绝」语义重叠)。

P2

  • 新增 isConfirmSubmitting state
  • 确认框 onConfirm 改为 async,内部 await performSubmit(values)try/catch/finally
  • onConfirmonCancel 都在 isConfirmSubmitting 为 true 时 early-return,防止提交中被重复点击
  • isConfirmSubmitting 合并进外层 submit 按钮的 disabled,以及 onSubmittingChange 通知的信号(让 AddProviderDialog 的 Add 按钮也能禁用)
  • 保存失败时保留确认框和 pending values,用户可以重试或取消

验证:pnpm typecheck ✅ / pnpm test:unit 213 passing ✅ / pnpm format ✅。

再麻烦 review 一下。如果方向认可我就转 Ready。

Allen added 3 commits April 25, 2026 09:04
…y" prompt

Refactor handleSubmit so empty-field / missing-item validations (provider
name, endpoint, API key, opencode model, template variables, provider key
required) no longer hard-reject with toast.error. Instead they are collected
into an issues list and presented via a ConfirmDialog; the user can cancel
or choose "Save anyway" to proceed.

Integrity constraints stay as hard rejections:
- providerKey regex / duplicate (would corrupt other providers)
- Copilot / Codex OAuth not authenticated (no token, cannot establish)
- omo Other Fields JSON not an object / parse failure

This aligns the frontend with the backend's existing "relaxed save / strict
switch" split (see gemini_config.rs: validate_gemini_settings vs
validate_gemini_settings_strict) and unblocks legitimate configs such as
AWS Bedrock, Vertex AI, and custom Gemini base URLs that the UI previously
refused to save.

Refs: farion1231#2196, farion1231#1204
P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
Hermes. Since providerKey is the primary identity for these apps and the
mutations layer throws "Provider key is required" when absent, letting users
click "save anyway" would surface a generic error toast instead of a
precise, actionable one. Treat empty providerKey as an integrity constraint
alongside regex / duplicate checks.

P2: give the soft-confirm submit path its own submitting state. The
confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
slow or failing saves left the outer submit button responsive and could
spawn unhandled rejections. Now the confirm handler awaits performSubmit
inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
confirm and cancel clicks, and folds the flag into the outer disabled
state and onSubmittingChange callback.

Refs: farion1231#2307 review comments
Clippy 1.95 added single_char_add_str which flagged the push_str("…")
in truncate_body. Rebased onto latest upstream/main and applied the
suggested fix so the Backend Checks clippy job passes.

Unrelated to this PR's core changes; bundled in so the PR is mergeable
without waiting for a separate upstream fix.
@allenxln
allenxln force-pushed the fix/soft-validation-bedrock branch from 910ccc9 to a67c960 Compare April 25, 2026 01:05
@allenxln

Copy link
Copy Markdown
Contributor Author

补充一条说明:CI Backend Checks 的 Clippy 报错和本 PR 的改动无关——是 Rust 1.95 新加的 clippy::single_char_add_str lint 打在 upstream 已有的 src-tauri/src/services/model_fetch.rs:195s.push_str("…") 上(在最新 main 分支上也会挂)。

为了让 PR 能合,我把这个 PR rebase 到了最新 upstream/main(带上了 7 个新 commit),并顺手做了 1 行修复:push_str("…") → push('…')

这条 clippy 修复严格说不在本 PR 范围内,如果你觉得应该单独走,我可以把它拎出来发新 PR,本 PR 回滚那 1 行。不过合并进来更简便——两个 commit 历史清晰分开。

@farion1231
farion1231 marked this pull request as ready for review April 25, 2026 01:21

@farion1231 farion1231 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

感谢您的贡献!

@farion1231
farion1231 merged commit 85f0be9 into farion1231:main Apr 25, 2026
2 checks passed
ManLOK-Chu pushed a commit to ManLOK-Chu/cc-switch that referenced this pull request May 11, 2026
…ion1231#2307)

* feat(provider-form): soften business-rule validation with "save anyway" prompt

Refactor handleSubmit so empty-field / missing-item validations (provider
name, endpoint, API key, opencode model, template variables, provider key
required) no longer hard-reject with toast.error. Instead they are collected
into an issues list and presented via a ConfirmDialog; the user can cancel
or choose "Save anyway" to proceed.

Integrity constraints stay as hard rejections:
- providerKey regex / duplicate (would corrupt other providers)
- Copilot / Codex OAuth not authenticated (no token, cannot establish)
- omo Other Fields JSON not an object / parse failure

This aligns the frontend with the backend's existing "relaxed save / strict
switch" split (see gemini_config.rs: validate_gemini_settings vs
validate_gemini_settings_strict) and unblocks legitimate configs such as
AWS Bedrock, Vertex AI, and custom Gemini base URLs that the UI previously
refused to save.

Refs: farion1231#2196, farion1231#1204

* fix(provider-form): address review feedback on soft-validation

P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
Hermes. Since providerKey is the primary identity for these apps and the
mutations layer throws "Provider key is required" when absent, letting users
click "save anyway" would surface a generic error toast instead of a
precise, actionable one. Treat empty providerKey as an integrity constraint
alongside regex / duplicate checks.

P2: give the soft-confirm submit path its own submitting state. The
confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
slow or failing saves left the outer submit button responsive and could
spawn unhandled rejections. Now the confirm handler awaits performSubmit
inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
confirm and cancel clicks, and folds the flag into the outer disabled
state and onSubmittingChange callback.

Refs: farion1231#2307 review comments

* chore(clippy): use push for single char '…' in truncate_body

Clippy 1.95 added single_char_add_str which flagged the push_str("…")
in truncate_body. Rebased onto latest upstream/main and applied the
suggested fix so the Backend Checks clippy job passes.

Unrelated to this PR's core changes; bundled in so the PR is mergeable
without waiting for a separate upstream fix.

---------

Co-authored-by: Allen <[email protected]>
dfbb pushed a commit to dfbb/cc-switch that referenced this pull request May 20, 2026
…ion1231#2307)

* feat(provider-form): soften business-rule validation with "save anyway" prompt

Refactor handleSubmit so empty-field / missing-item validations (provider
name, endpoint, API key, opencode model, template variables, provider key
required) no longer hard-reject with toast.error. Instead they are collected
into an issues list and presented via a ConfirmDialog; the user can cancel
or choose "Save anyway" to proceed.

Integrity constraints stay as hard rejections:
- providerKey regex / duplicate (would corrupt other providers)
- Copilot / Codex OAuth not authenticated (no token, cannot establish)
- omo Other Fields JSON not an object / parse failure

This aligns the frontend with the backend's existing "relaxed save / strict
switch" split (see gemini_config.rs: validate_gemini_settings vs
validate_gemini_settings_strict) and unblocks legitimate configs such as
AWS Bedrock, Vertex AI, and custom Gemini base URLs that the UI previously
refused to save.

Refs: farion1231#2196, farion1231#1204

* fix(provider-form): address review feedback on soft-validation

P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
Hermes. Since providerKey is the primary identity for these apps and the
mutations layer throws "Provider key is required" when absent, letting users
click "save anyway" would surface a generic error toast instead of a
precise, actionable one. Treat empty providerKey as an integrity constraint
alongside regex / duplicate checks.

P2: give the soft-confirm submit path its own submitting state. The
confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
slow or failing saves left the outer submit button responsive and could
spawn unhandled rejections. Now the confirm handler awaits performSubmit
inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
confirm and cancel clicks, and folds the flag into the outer disabled
state and onSubmittingChange callback.

Refs: farion1231#2307 review comments

* chore(clippy): use push for single char '…' in truncate_body

Clippy 1.95 added single_char_add_str which flagged the push_str("…")
in truncate_body. Rebased onto latest upstream/main and applied the
suggested fix so the Backend Checks clippy job passes.

Unrelated to this PR's core changes; bundled in so the PR is mergeable
without waiting for a separate upstream fix.

---------

Co-authored-by: Allen <[email protected]>
delta-lo pushed a commit to delta-lo/cc-switch-mod that referenced this pull request May 31, 2026
…ion1231#2307)

* feat(provider-form): soften business-rule validation with "save anyway" prompt

Refactor handleSubmit so empty-field / missing-item validations (provider
name, endpoint, API key, opencode model, template variables, provider key
required) no longer hard-reject with toast.error. Instead they are collected
into an issues list and presented via a ConfirmDialog; the user can cancel
or choose "Save anyway" to proceed.

Integrity constraints stay as hard rejections:
- providerKey regex / duplicate (would corrupt other providers)
- Copilot / Codex OAuth not authenticated (no token, cannot establish)
- omo Other Fields JSON not an object / parse failure

This aligns the frontend with the backend's existing "relaxed save / strict
switch" split (see gemini_config.rs: validate_gemini_settings vs
validate_gemini_settings_strict) and unblocks legitimate configs such as
AWS Bedrock, Vertex AI, and custom Gemini base URLs that the UI previously
refused to save.

Refs: farion1231#2196, farion1231#1204

* fix(provider-form): address review feedback on soft-validation

P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
Hermes. Since providerKey is the primary identity for these apps and the
mutations layer throws "Provider key is required" when absent, letting users
click "save anyway" would surface a generic error toast instead of a
precise, actionable one. Treat empty providerKey as an integrity constraint
alongside regex / duplicate checks.

P2: give the soft-confirm submit path its own submitting state. The
confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
slow or failing saves left the outer submit button responsive and could
spawn unhandled rejections. Now the confirm handler awaits performSubmit
inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
confirm and cancel clicks, and folds the flag into the outer disabled
state and onSubmittingChange callback.

Refs: farion1231#2307 review comments

* chore(clippy): use push for single char '…' in truncate_body

Clippy 1.95 added single_char_add_str which flagged the push_str("…")
in truncate_body. Rebased onto latest upstream/main and applied the
suggested fix so the Backend Checks clippy job passes.

Unrelated to this PR's core changes; bundled in so the PR is mergeable
without waiting for a separate upstream fix.

---------

Co-authored-by: Allen <[email protected]>
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.

再一次被 CC 的各种奇怪限制整破防了。

2 participants