chore: wave-0 baseline (Tauri2+React shell, platform detect, fake catalog)
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "agentdock-adapter"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "适配器 schema 加载、版本校验、dry-run、执行器接口(架构 §3)"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_yaml = "0.9"
|
||||
@@ -0,0 +1,110 @@
|
||||
//! 目录索引与占位条目加载(Wave 0)
|
||||
//!
|
||||
//! 从 `adapters/catalog.yaml` 读取索引,再逐个读取 `tools/*.yaml`,
|
||||
//! 返回目录条目。仅消费 `id / name / name_zh / vendor / status` 五个字段。
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::AdapterError;
|
||||
|
||||
/// 目录索引(adapters/catalog.yaml)
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Catalog {
|
||||
#[serde(rename = "catalog_version")]
|
||||
pub version: u32,
|
||||
pub tools: Vec<CatalogRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct CatalogRef {
|
||||
pub id: String,
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
/// 目录条目(占位 schema,Wave 0 仅五字段;完整字段见架构 §3.1)
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct CatalogEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(rename = "name_zh")]
|
||||
pub name_zh: String,
|
||||
pub vendor: String,
|
||||
/// available | watch
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// 加载目录索引与全部工具占位 YAML
|
||||
pub fn load_catalog<P: AsRef<Path>>(adapters_dir: P) -> Result<Vec<CatalogEntry>, AdapterError> {
|
||||
let dir = adapters_dir.as_ref();
|
||||
let catalog_text = fs::read_to_string(dir.join("catalog.yaml"))
|
||||
.map_err(|e| AdapterError::Io(format!("读取 catalog.yaml 失败: {e}")))?;
|
||||
let catalog: Catalog = serde_yaml::from_str(&catalog_text)
|
||||
.map_err(|e| AdapterError::Parse(format!("解析 catalog.yaml 失败: {e}")))?;
|
||||
|
||||
let mut entries = Vec::with_capacity(catalog.tools.len());
|
||||
for r in &catalog.tools {
|
||||
let text = fs::read_to_string(dir.join(&r.file))
|
||||
.map_err(|e| AdapterError::Io(format!("读取 {} 失败: {e}", r.file)))?;
|
||||
let entry: CatalogEntry = serde_yaml::from_str(&text)
|
||||
.map_err(|e| AdapterError::Parse(format!("解析 {} 失败: {e}", r.file)))?;
|
||||
if entry.id != r.id {
|
||||
return Err(AdapterError::Parse(format!(
|
||||
"索引 id 与文件内 id 不一致: 索引={} 文件={}",
|
||||
r.id, entry.id
|
||||
)));
|
||||
}
|
||||
if entry.status != "available" && entry.status != "watch" {
|
||||
return Err(AdapterError::Parse(format!(
|
||||
"{} 的 status 非法: {}(应为 available | watch)",
|
||||
entry.id, entry.status
|
||||
)));
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_minimal_tool_yaml() {
|
||||
let yaml = "id: codex\nname: Codex CLI\nname_zh: Codex CLI\nvendor: OpenAI\nstatus: available\n";
|
||||
let entry: CatalogEntry = serde_yaml::from_str(yaml).expect("应能解析");
|
||||
assert_eq!(entry.id, "codex");
|
||||
assert_eq!(entry.name_zh, "Codex CLI");
|
||||
assert_eq!(entry.vendor, "OpenAI");
|
||||
assert_eq!(entry.status, "available");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_status() {
|
||||
let yaml = "id: x\nname: X\nname_zh: X\nvendor: V\nstatus: unknown\n";
|
||||
let entry: Result<CatalogEntry, _> = serde_yaml::from_str(yaml);
|
||||
// 解析本身成功,非法 status 由 load_catalog 校验;此处确认 schema 字段可读
|
||||
assert!(entry.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_real_catalog_from_repo() {
|
||||
// 以真实 adapters/ 目录做集成测试(相对本 crate 位于 ../../adapters)
|
||||
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../adapters");
|
||||
let entries = load_catalog(&dir).expect("真实目录应可加载");
|
||||
assert_eq!(entries.len(), 14, "第一批应为 14 个工具");
|
||||
for e in &entries {
|
||||
assert!(!e.id.is_empty());
|
||||
assert!(!e.name.is_empty());
|
||||
assert!(!e.name_zh.is_empty());
|
||||
assert!(!e.vendor.is_empty());
|
||||
}
|
||||
let ids: Vec<&str> = entries.iter().map(|e| e.id.as_str()).collect();
|
||||
for want in ["codex", "claude-code", "gemini", "copilot", "kimi", "qwen", "codebuddy",
|
||||
"opencode", "crush", "goose", "aider", "cursor", "cline", "warp"] {
|
||||
assert!(ids.contains(&want), "目录应包含 {want}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! 适配器层错误类型
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AdapterError {
|
||||
Io(String),
|
||||
Parse(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for AdapterError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AdapterError::Io(m) => write!(f, "IO: {m}"),
|
||||
AdapterError::Parse(m) => write!(f, "Parse: {m}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AdapterError {}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! agentdock-adapter —— 适配器层
|
||||
//!
|
||||
//! 负责适配器 schema 加载、版本校验、dry-run 与执行器接口(架构 §3)。
|
||||
//! Wave 0:仅落地目录索引与占位条目加载(`catalog` 模块);
|
||||
//! 完整 schema 校验、执行器接口随 Wave 1 实现。
|
||||
|
||||
pub mod catalog;
|
||||
pub mod error;
|
||||
|
||||
pub use catalog::{Catalog, CatalogEntry, CatalogRef, load_catalog};
|
||||
pub use error::AdapterError;
|
||||
Reference in New Issue
Block a user