Wave 2: 五件套打通全链路(安装/检测/配置/授权/诊断)

- 五个适配器 YAML 全字段补齐(codex/claude-code/gemini/kimi/opencode)
- agentdock-config:TOML/JSON 编解码 + 原子写 + 自动备份
- agentdock-diag:PATH/依赖版本/版本冲突/配置损坏四类规则
- agentdock-core:detectCli/previewAction/runAction 流式/readConfig/writeConfig/authStatus/diagnose
- Tauri IPC 命令 + CLI 详情页/安装确认弹窗/配置表单/诊断页
- cargo test 75 项通过(含 4 项真机 ignored 自测),前端 build 通过
This commit is contained in:
leefer
2026-08-25 08:58:38 +08:00
parent c47494180b
commit 01996a80fe
33 changed files with 9404 additions and 152 deletions
+10
View File
@@ -3,5 +3,15 @@ name = "agentdock-core"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "编排层:发现/安装/配置/授权/诊断流水线(架构 §2 核心编排)"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
regex = "1"
agentdock-adapter = { path = "../agentdock-adapter" }
agentdock-config = { path = "../agentdock-config" }
agentdock-secrets = { path = "../agentdock-secrets" }
agentdock-exec = { path = "../agentdock-exec" }
agentdock-diag = { path = "../agentdock-diag" }
agentdock-platform = { path = "../agentdock-platform" }
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
//! 编排层错误类型(中文)
use std::fmt;
#[derive(Debug)]
pub enum EngineError {
/// 未找到指定 CLI
NotFound(String),
/// 适配器层错误
Adapter(String),
/// 配置层错误
Config(String),
/// 进程执行错误
Exec(String),
/// 密钥库错误
Secrets(String),
/// 能力本波未实现
NotSupported(String),
}
impl fmt::Display for EngineError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EngineError::NotFound(m) => write!(f, "未找到: {m}"),
EngineError::Adapter(m) => write!(f, "适配器: {m}"),
EngineError::Config(m) => write!(f, "配置: {m}"),
EngineError::Exec(m) => write!(f, "执行: {m}"),
EngineError::Secrets(m) => write!(f, "密钥库: {m}"),
EngineError::NotSupported(m) => write!(f, "未支持: {m}"),
}
}
}
impl std::error::Error for EngineError {}
impl From<agentdock_config::ConfigError> for EngineError {
fn from(e: agentdock_config::ConfigError) -> Self {
EngineError::Config(e.to_string())
}
}
impl From<agentdock_exec::ExecError> for EngineError {
fn from(e: agentdock_exec::ExecError) -> Self {
EngineError::Exec(e.to_string())
}
}
impl From<std::io::Error> for EngineError {
fn from(e: std::io::Error) -> Self {
EngineError::Exec(e.to_string())
}
}
+13 -13
View File
@@ -1,17 +1,17 @@
//! agentdock-core —— 编排层(架构 §2 模块关系:UI --invoke--> commands --→ core orchestrator
//!
//! 职责:发现 / 安装 / 配置 / 授权 / 诊断 流水线编排。
//! Wave 0:空骨架,流水线自 Wave 1 起逐波落地。
//! 把 adapter/config/secrets/exec/diag/platform 串成完整流水线:
//! detectCli / previewAction / runAction / readConfig / writeConfig /
//! authStatus / authorize / diagnose。
/// 编排层能力标记(占位)
pub const LAYER: &str = "agentdock-core";
pub mod engine;
pub mod error;
pub mod process;
pub mod types;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layer_identity() {
assert_eq!(LAYER, "agentdock-core");
}
}
pub use engine::{ActionOpts, Engine, auth_status, authorize, detect_one, diagnose, parse_version, read_config, run_action, write_config};
pub use error::EngineError;
pub use types::{
ActionEvent, AuthStatus, ConfigFieldState, ConfigFileState, ConfigFormState, DetectResult,
EnvFieldState, WriteResult, now_secs,
};
+89
View File
@@ -0,0 +1,89 @@
//! 进程探测与流式执行辅助(内部)
//!
//! 所有命令经 `agentdock-exec::validate_argv` 校验后执行,禁止 shell 拼接。
//! 探测类命令(`--version`)隐藏控制台窗口(Windows CREATE_NO_WINDOW)。
use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// 按平台分隔符拆分 PATH。
fn path_entries() -> Vec<String> {
std::env::var("PATH")
.unwrap_or_default()
.split(if cfg!(windows) { ';' } else { ':' })
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
/// 在 PATH 中查找可执行文件(返回全部命中,用于版本冲突检测)。
/// Windows 优先 .exe/.cmd/.bat,最后回退无扩展名。
pub fn which_all(program: &str) -> Vec<PathBuf> {
let direct = Path::new(program);
if direct.is_absolute() && direct.is_file() {
return vec![direct.to_path_buf()];
}
let exts: &[&str] = if cfg!(windows) { &[".exe", ".cmd", ".bat", ""] } else { &[""] };
let mut found = Vec::new();
for dir in path_entries() {
for ext in exts {
let cand = Path::new(&dir).join(format!("{program}{ext}"));
if cand.is_file() && !found.contains(&cand) {
found.push(cand);
}
}
}
found
}
/// 运行只读探测命令并返回 (exit_ok, stdout+stderr 合并文本)。
pub fn run_capture(exe: &Path, args: &[String]) -> std::io::Result<(bool, String)> {
let mut cmd = Command::new(exe);
cmd.args(args);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
let out = cmd.output()?;
let mut text = String::from_utf8_lossy(&out.stdout).to_string();
if text.trim().is_empty() {
text = String::from_utf8_lossy(&out.stderr).to_string();
}
Ok((out.status.success(), text))
}
/// 流式执行命令,逐行回调(stdout 与 stderr 均按行输出)。返回是否成功。
/// 顺序读:先 stdout 后 stderr;对长任务足够,且实现简单可靠、无闭包 Send 负担。
pub fn run_streaming<F>(exe: &str, args: &[String], mut on_line: F) -> std::io::Result<bool>
where
F: FnMut(bool, &str),
{
let mut cmd = Command::new(exe);
cmd.args(args);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000);
}
let mut child = cmd.spawn()?;
if let Some(stdout) = child.stdout.take() {
let reader = std::io::BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
on_line(false, &line);
}
}
if let Some(stderr) = child.stderr.take() {
let reader = std::io::BufReader::new(stderr);
for line in reader.lines().map_while(Result::ok) {
on_line(true, &line);
}
}
let status = child.wait()?;
Ok(status.success())
}
+129
View File
@@ -0,0 +1,129 @@
//! IPC 契约数据类型(对齐架构 §2「关键 IPC 契约」)
//!
//! 字段命名与前端 TS 类型一一对应(serde 输出 snake_case)。
//! 所有时间戳统一用 Unix 秒(u64),由前端做相对时间展示,后端不做日期数学。
use serde::{Deserialize, Serialize};
/// 检测结果(detectCli)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectResult {
pub cli_id: String,
/// installed | not_installed | not_in_path | permission_denied | exec_failed | version_unparseable
pub status: String,
pub version: Option<String>,
/// 解析到的可执行文件绝对路径
pub executable: Option<String>,
/// 适配器声明「版本检测文档未确认」(UI 显示「实测兜底」)
pub version_unconfirmed: bool,
/// Unix 秒
pub checked_at: u64,
}
/// 授权状态(authStatus)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthStatus {
pub cli_id: String,
/// authorized | unauthorized | unknown | possibly_expired
pub status: String,
/// 结论来源:status_command | keyring | unknown
pub via: String,
pub detail_zh: String,
pub checked_at: u64,
}
/// 单个配置字段的读取状态(readConfig)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigFieldState {
pub id: String,
pub label_zh: String,
pub help_zh: Option<String>,
pub required: bool,
pub sensitive: bool,
/// string | url | enum | bool
pub field_type: String,
/// file | env | keyring
pub storage: String,
/// 敏感字段永不明文回显;值为 None 时结合 has_value 展示「已保存 / 未保存」
pub value: Option<String>,
/// 密钥库类字段:是否已有值
pub has_value: bool,
}
/// 配置文件解析状态。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigFileState {
/// 解析后的绝对路径
pub path: String,
pub format: String,
pub scope: Option<String>,
pub exists: bool,
pub parse_ok: bool,
pub error: Option<String>,
}
/// 环境变量映射(仅供说明与脱敏,本波不注入执行环境)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvFieldState {
pub key: String,
pub sensitive: bool,
pub maps_to_field: Option<String>,
}
/// 配置表单状态(readConfig 返回)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigFormState {
pub cli_id: String,
pub files: Vec<ConfigFileState>,
pub fields: Vec<ConfigFieldState>,
pub environment: Vec<EnvFieldState>,
}
/// 写配置结果(writeConfig 返回)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WriteResult {
/// 自动备份路径(无备份时为 None)
pub backup_path: Option<String>,
/// 实际写入的配置文件路径
pub written_file: Option<String>,
/// 成功写入的字段 id
pub written_fields: Vec<String>,
/// 逐字段错误(key=field id, value=中文错误)
pub errors: Vec<String>,
}
/// 动作流事件(runAction 流式输出)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionEvent {
/// step | stdout | stderr | done | error
pub kind: String,
/// 已脱敏的消息文本
pub message: String,
pub data: Option<serde_json::Value>,
}
impl ActionEvent {
pub fn step(msg: impl Into<String>) -> Self {
ActionEvent { kind: "step".into(), message: msg.into(), data: None }
}
pub fn stdout(line: impl Into<String>) -> Self {
ActionEvent { kind: "stdout".into(), message: line.into(), data: None }
}
pub fn stderr(line: impl Into<String>) -> Self {
ActionEvent { kind: "stderr".into(), message: line.into(), data: None }
}
pub fn done(data: Option<serde_json::Value>) -> Self {
ActionEvent { kind: "done".into(), message: String::new(), data }
}
pub fn error(msg: impl Into<String>) -> Self {
ActionEvent { kind: "error".into(), message: msg.into(), data: None }
}
}
/// 当前 Unix 秒。
pub fn now_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}