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:
@@ -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
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user