feat(m174): 助手(AI)提供商配置真接线 + 「测试连接」
设置页新增第 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 字符」的只读状态(仍然不显示值)。
This commit is contained in:
1 parent
82f3bbbff6
commit
9cb605f316
23 files changed
+2939
-73
No files matched your search
@@ -1,39 +1,75 @@
|
||||
//! 设置页「AI / About」分组:**预留**(页面上还没有这个分组,左列 4 项里没有它)。
|
||||
//! 设置页「助手(AI)」分组(M174):左列第 6 组的**挂载点**与本线的消息口。
|
||||
//!
|
||||
//! 为什么先留一个空文件:设置页正文接下来由四条线并行接(java / download / language+appearance /
|
||||
//! ai+about)。文件位置先占好,四条线就不会在同一天抢同一个新文件名;本文件现在是**只有文档**的
|
||||
//! 空模块(不产生任何代码,`-D warnings` 不会红)。
|
||||
//! 本文件只留三样东西,正文/状态/读写都在 `pages/settings/ai/` 里:
|
||||
//!
|
||||
//! ## 接线步骤(ai+about 线照做)
|
||||
//! | 这里 | 内容 |
|
||||
//! |---|---|
|
||||
//! | [`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` 一行不用改) |
|
||||
//!
|
||||
//! 1. 在本文件写 `pub(super) const GROUP: Group = Group { … }`,形状抄 `java.rs`;
|
||||
//! 2. 到 `settings/mod.rs` 的 `GROUPS` 里加一行 `ai_about::GROUP,`,并把数组长度 `4` 改成 `5`;
|
||||
//! 3. 文案 key 写进 `crates/i18n/src/catalog/settings/ai.rs`(那边已经接进装配链,同样只改一个文件)。
|
||||
//! ## 为什么 `body` 是 `None` 而不是 `Some(..)`
|
||||
//!
|
||||
//! 界面上「多了一组」这件事本身是行为变化,所以**只在真有那一组内容时才做**,不要为占位先加。
|
||||
//! `Group.body` 的签名是 `fn(&Ui, States) -> Option<Element>`:拿不到 `Lang`,也就画不出中英两套
|
||||
//! 文案(本组的硬要求是「不得硬编码中文」)。M106 的语言行与 M159 的更新组是同一个理由走了
|
||||
//! 按下标分发;本组沿用同一条路,代价是 `settings/mod.rs` 里多一行 `else if`。
|
||||
//!
|
||||
//! ## M101 起本文件还装本线的状态与消息口
|
||||
//! ## 密钥纪律
|
||||
//!
|
||||
//! - 状态:[`State`](`Groups.ai_about` 的载荷,加字段只改本文件);
|
||||
//! - 消息:[`Message`](落点 `app/update/settings/ai_about.rs`)。
|
||||
//!
|
||||
//! 上第 5 组时**额外**要动的共享文件(按上面的步骤 2 + 测试装配):
|
||||
//! - `settings/mod.rs` 的 `GROUPS`(加一行,长度 +1);
|
||||
//! - `settings/tests/mod.rs` 顶部加一行 `mod ai_section;`(新测试文件不挂 `mod` 就不编译),
|
||||
//! 并且左列顺序那条字面量断言会红——那是刻意的(页面上多一组就是观感变了,要人确认)。
|
||||
//!
|
||||
//! 注意 i18n 那边是按**分组**命名的,文案文件叫 `ai.rs` 而不是 `ai_about.rs`。
|
||||
//! [`Message::EditKey`] 的载荷是用户刚敲的明文(输入框必须有它),它**只**活在这个进程里:
|
||||
//! 落盘走 `ai::store::save`(不经 `AppCommand::SetSetting` 那条可序列化通道),
|
||||
//! 上屏默认掩码(`State::reveal` 才明文),`Debug` 由 `ai::state::Secret` 掩码。
|
||||
|
||||
/// AI / About 分组的运行期状态(M101 r2:**只归 ai+about 线**)。
|
||||
///
|
||||
/// 加字段只改本文件:`Groups` 只认 `Groups.ai_about` 这一个字段。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct State {}
|
||||
pub use super::ai::State;
|
||||
|
||||
/// AI / About 线的消息(**只归本线**)。
|
||||
///
|
||||
/// 首版是空枚举:这一条线的分组还没上页面。空枚举的好处是
|
||||
/// `app/update/settings/ai_about.rs` 里的 `match message {}` 天然穷尽——加了变体不接线就报
|
||||
/// E0004,不会静默。
|
||||
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 {}
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user