设置页新增第 6 组「助手」:提供商四态 / 端点 base_url / 模型名 / API 密钥(默认掩码, 点「显示」才明文)+ 保存 + 测试连接,四类失败(鉴权 / 模型不存在 / 不可达 / 超时)各自 一句文案 + 目标与耗时。 取舍三条: 1. 读写走 `core::setting` 的 schema API(`SettingStore::load` 现读 → `set` → `save`), 不经 `AppCommand::SetSetting`——那条指令是可序列化 DTO,`ai.api_key` 走它等于让明文进 事件流/日志。与 `SettingsBridge::persist` 写外观是同一条做法。 2. 「测试连接」不另写一套 HTTP:`unlucky_cli::commands::agent::probe` 现读设置文件、 交给 `RigProvider`(真 provider)发一次最小补全,带 8 秒时限;分类只看错误文本 (rig 只给 Display),四类各有一条真 TCP 假服务器用例钉住。 3. `ProviderKind` 补 `from_setting` / `setting_value`:serde 的 `rename_all = snake_case` 产出 `open_ai_compatible`,与 schema 的 `openai_compatible` 不是同一个词——这正是 M174 之前「agent 侧一次都没读过设置」的根因。 守卫(都做变异验证,见 mission 报告):密钥不出 `Debug`/`detail`/DTO;未配置时工厂构造 计数为 0;改完 base_url 后探测打到新地址;空/非法值被 schema 拦下且文件不动。 i18n:49 条中英各一(金表按 `Key::ALL` 位置插入);presenter 的 ai_group 补上 schema 允许 的 `disabled` 选项与一行「已配置 N 字符」的只读状态(仍然不显示值)。
76 lines
3.2 KiB
Rust
76 lines
3.2 KiB
Rust
//! 设置页「助手(AI)」分组(M174):左列第 6 组的**挂载点**与本线的消息口。
|
||
//!
|
||
//! 本文件只留三样东西,正文/状态/读写都在 `pages/settings/ai/` 里:
|
||
//!
|
||
//! | 这里 | 内容 |
|
||
//! |---|---|
|
||
//! | [`GROUP`] | 左列名 / 图标 / 演示行(产品路径走 [`super::ai::view::body`]) |
|
||
//! | [`AI_GROUP`] | 本组在 `GROUPS` 里的下标(与 General / Update 同一条**按下标分发**:本组要 i18n 文案与 `Lang` 形参,`Group.body` 的签名里没有它) |
|
||
//! | [`Message`] | 本线的消息口(落点 `app/update/settings/ai_about.rs`) |
|
||
//! | [`State`] | 转发 `ai::State`(`Groups.ai_about` 的载荷类型不变,`settings/state.rs` 一行不用改) |
|
||
//!
|
||
//! ## 为什么 `body` 是 `None` 而不是 `Some(..)`
|
||
//!
|
||
//! `Group.body` 的签名是 `fn(&Ui, States) -> Option<Element>`:拿不到 `Lang`,也就画不出中英两套
|
||
//! 文案(本组的硬要求是「不得硬编码中文」)。M106 的语言行与 M159 的更新组是同一个理由走了
|
||
//! 按下标分发;本组沿用同一条路,代价是 `settings/mod.rs` 里多一行 `else if`。
|
||
//!
|
||
//! ## 密钥纪律
|
||
//!
|
||
//! [`Message::EditKey`] 的载荷是用户刚敲的明文(输入框必须有它),它**只**活在这个进程里:
|
||
//! 落盘走 `ai::store::save`(不经 `AppCommand::SetSetting` 那条可序列化通道),
|
||
//! 上屏默认掩码(`State::reveal` 才明文),`Debug` 由 `ai::state::Secret` 掩码。
|
||
|
||
pub use super::ai::State;
|
||
|
||
use unlucky_design::icons::IconKind;
|
||
|
||
use super::Group;
|
||
|
||
/// 本组在 [`super::GROUPS`] 里的下标(**必须是最后一个**:加组只追加不重排)。
|
||
pub(crate) const AI_GROUP: usize = 5;
|
||
|
||
/// 「助手」分组(左列名固定英文:左列是导航,与导航轨同源,不进 catalog)。
|
||
pub(super) const GROUP: Group = Group {
|
||
name: "Assistant",
|
||
icon: IconKind::Ai,
|
||
// 只给 harness / 窄窗「按组堆叠」路径用的演示行:产品路径走 `ai::view::body`。
|
||
rows: &[
|
||
("Provider", "OpenAI compatible"),
|
||
("Endpoint", "https://api.deepseek.com/v1"),
|
||
("Model", "deepseek-chat"),
|
||
],
|
||
body: None,
|
||
};
|
||
|
||
/// 本线的消息(**只归本线**):加变体只改本文件 + `app/update/settings/ai_about.rs`。
|
||
#[derive(Debug, Clone)]
|
||
pub enum Message {
|
||
/// 启用开关(`ai.enabled`)
|
||
ToggleEnabled(bool),
|
||
/// 选提供商(`ai.provider`)
|
||
PickProvider(super::ai::ProviderId),
|
||
/// 端点草稿(`ai.base_url`)
|
||
EditBaseUrl(String),
|
||
/// 模型名草稿(`ai.model`)
|
||
EditModel(String),
|
||
/// 新密钥草稿(明文只在内存里;空 = 保留已存的那把)
|
||
EditKey(String),
|
||
/// 密钥输入框明文/掩码
|
||
Reveal(bool),
|
||
/// 保存(校验 + 落盘)
|
||
Save,
|
||
/// 测试连接(异步;结果走 [`Message::ProbeDone`])
|
||
Test,
|
||
/// 测试连接的结果(分类 + 数值 + 脱敏详情)
|
||
ProbeDone(Box<super::ai::ProbeResult>),
|
||
}
|
||
|
||
impl Message {
|
||
/// 一条本组消息 → 设置页的投递口(`pages::settings::GroupMessage::AiAbout`)。
|
||
#[must_use]
|
||
pub fn into_group(self) -> super::GroupMessage {
|
||
super::GroupMessage::AiAbout(self)
|
||
}
|
||
}
|