diff --git a/config.json b/config.json deleted file mode 100644 index 85c1cee..0000000 --- a/config.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "storage": { - "state_dir": ".data/weixin-ilink", - "database_url": "" - }, - "llm": { - "provider": "${WEIXIN_LLM_PROVIDER}", - "deepseek": { - "api_key": "${DEEPSEEK_API_KEY}", - "model": "${DEEPSEEK_MODEL}", - "base_url": "${DEEPSEEK_BASE_URL}" - } - } -} diff --git a/resources/skills/get_current_datetime/SKILL.md b/resources/skills/get_current_datetime/SKILL.md index 7296481..acfdc7e 100644 --- a/resources/skills/get_current_datetime/SKILL.md +++ b/resources/skills/get_current_datetime/SKILL.md @@ -1,26 +1,8 @@ # get_current_datetime - -获取当前日期时间(北京时间 UTC+8)。 - +获取当前日期时间(UTC+8)。内置 Rust 实现,无脚本依赖。 ## Risk Level Low - -## Parameters -```json -{ - "type": "object", - "properties": { - "format": { - "type": "string", - "description": "时间格式: full | date | time", - "enum": ["full", "date", "time"], - "default": "full" - } - } -} -``` - ## Execute ```bash -scripts/datetime.sh +# Builtin: Rust ``` diff --git a/resources/skills/get_current_datetime/scripts/datetime.sh b/resources/skills/get_current_datetime/scripts/datetime.sh deleted file mode 100755 index 39fc3d2..0000000 --- a/resources/skills/get_current_datetime/scripts/datetime.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# 读取 stdin JSON 参数 -read -r input -format=$(echo "$input" | python3 -c " -import sys, json -try: - d = json.load(sys.stdin) - print(d.get('format', 'full')) -except: print('full') -" 2>/dev/null || echo "full") - -case "$format" in - date) TZ='Asia/Shanghai' date '+{"datetime": "%Y-%m-%d"}' ;; - time) TZ='Asia/Shanghai' date '+{"datetime": "%H:%M:%S"}' ;; - *) TZ='Asia/Shanghai' date '+{"datetime": "%Y-%m-%d %H:%M:%S"}' ;; -esac diff --git a/resources/skills/manage_memos/SKILL.md b/resources/skills/manage_memos/SKILL.md index 2cc071a..d7ae6ce 100644 --- a/resources/skills/manage_memos/SKILL.md +++ b/resources/skills/manage_memos/SKILL.md @@ -1,36 +1,8 @@ # manage_memos - -管理备忘录:添加、列出、删除。 - -备忘录存储在 `.data/memos.json` 文件中。 - +管理备忘录(添加/列出/删除)。内置 Rust 实现,无脚本依赖。 ## Risk Level High - -## Parameters -```json -{ - "type": "object", - "properties": { - "action": { - "type": "string", - "description": "操作类型", - "enum": ["add", "list", "delete"] - }, - "content": { - "type": "string", - "description": "备忘内容(add 时需要)" - }, - "id": { - "type": "integer", - "description": "备忘编号(delete 时需要)" - } - }, - "required": ["action"] -} -``` - ## Execute ```bash -scripts/memo.sh +# Builtin: Rust ``` diff --git a/resources/skills/manage_memos/scripts/memo.sh b/resources/skills/manage_memos/scripts/memo.sh deleted file mode 100755 index 8092d11..0000000 --- a/resources/skills/manage_memos/scripts/memo.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# 备忘录管理 — 存储 .data/memos.json -MEMO_FILE=".data/memos.json" -mkdir -p "$(dirname "$MEMO_FILE")" -[ ! -f "$MEMO_FILE" ] && echo '[]' > "$MEMO_FILE" - -read -r INPUT -export MEMO_INPUT="$INPUT" - -python3 - "$MEMO_FILE" << 'PYEOF' -import sys, json, os, datetime - -path = sys.argv[1] -input_raw = os.environ.get('MEMO_INPUT', '{}') - -try: req = json.loads(input_raw) -except: - print('无效的 JSON 输入') - sys.exit(1) - -action = req.get('action', 'list') -content = req.get('content', '') -memo_id = req.get('id', '') - -data = json.load(open(path)) if os.path.exists(path) and os.path.getsize(path) > 0 else [] - -if action == 'add': - if not content: - print('请提供 content 参数') - sys.exit(1) - next_id = max([m.get('id', 0) for m in data], default=0) + 1 - data.append({'id': next_id, 'content': content, 'created_at': datetime.datetime.now().isoformat()}) - json.dump(data, open(path, 'w'), ensure_ascii=False, indent=2) - print(f'已添加备忘录 #{next_id}') - -elif action == 'list': - if not data: - print('暂无备忘录') - else: - for m in data: - ts = m.get('created_at', '?')[:16] - print(f"#{m['id']} [{ts}] {m['content']}") - -elif action == 'delete': - if not memo_id: - print('请提供 id 参数') - sys.exit(1) - new = [m for m in data if str(m.get('id', '')) != str(memo_id)] - if len(new) == len(data): - print(f'未找到备忘 #{memo_id}') - else: - json.dump(new, open(path, 'w'), ensure_ascii=False, indent=2) - print(f'已删除备忘 #{memo_id}') - -else: - print(f'未知操作: {action}(支持: add, list, delete)') - sys.exit(1) -PYEOF diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 7579b8e..0000000 --- a/src/config.rs +++ /dev/null @@ -1,202 +0,0 @@ -// 允许未来阶段使用的字段 -#![allow(dead_code)] - -use serde::Deserialize; -use std::path::Path; - -/// iAs 全局配置 -#[derive(Debug, Clone, Deserialize)] -pub struct AppConfig { - #[serde(default)] - pub llm: LlmConfig, - #[serde(default)] - pub storage: StorageConfig, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct LlmConfig { - #[serde(default = "default_llm_provider")] - pub provider: String, - - #[serde(default)] - pub deepseek: DeepSeekConfig, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct DeepSeekConfig { - #[serde(default = "default_deepseek_api_key")] - pub api_key: String, - #[serde(default = "default_deepseek_model")] - pub model: String, - #[serde(default = "default_deepseek_base_url")] - pub base_url: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct StorageConfig { - /// 状态文件目录 - #[serde(default = "default_state_dir")] - pub state_dir: String, - /// PostgreSQL 连接串(可选,默认为文件存储) - #[serde(default)] - pub database_url: String, -} - -// ─── 默认值 ─── - -fn default_llm_provider() -> String { - "deepseek".to_string() -} - -fn default_deepseek_api_key() -> String { - env_or_default("DEEPSEEK_API_KEY", "") -} - -fn default_deepseek_model() -> String { - env_or_default("DEEPSEEK_MODEL", "deepseek-v4-flash") -} - -fn default_deepseek_base_url() -> String { - env_or_default("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") -} - -fn default_state_dir() -> String { - ".data/weixin-ilink".to_string() -} - -fn env_or_default(key: &str, default: &str) -> String { - std::env::var(key).unwrap_or_else(|_| default.to_string()) -} - -impl Default for LlmConfig { - fn default() -> Self { - Self { - provider: default_llm_provider(), - deepseek: DeepSeekConfig::default(), - } - } -} - -impl Default for DeepSeekConfig { - fn default() -> Self { - Self { - api_key: default_deepseek_api_key(), - model: default_deepseek_model(), - base_url: default_deepseek_base_url(), - } - } -} - -impl Default for StorageConfig { - fn default() -> Self { - Self { - state_dir: default_state_dir(), - database_url: String::new(), - } - } -} - -impl Default for AppConfig { - fn default() -> Self { - Self { - llm: LlmConfig::default(), - storage: StorageConfig::default(), - } - } -} - -impl AppConfig { - /// 从文件加载配置,缺失字段回退到环境变量 - /// 尝试顺序:path → ~/.ias/config.json → 默认 - pub fn from_file(path: impl AsRef) -> Self { - let path = path.as_ref(); - - // 尝试指定路径 - if path.exists() { - match Self::try_load(path) { - Some(cfg) => return cfg, - None => tracing::warn!("解析配置文件失败: {},尝试回退", path.display()), - } - } - - // 回退到 ~/.ias/config.json - let home = dirs::home_dir(); - if let Some(home) = home { - let fallback = home.join(".ias").join("config.json"); - if fallback.exists() { - tracing::info!("使用全局配置: {}", fallback.display()); - match Self::try_load(&fallback) { - Some(cfg) => return cfg, - None => tracing::warn!("解析全局配置失败: {}", fallback.display()), - } - } - } - - tracing::info!("使用默认配置"); - Self::default() - } - - fn try_load(path: &Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - let resolved = resolve_env_vars(&content); - match serde_json::from_str(&resolved) { - Ok(cfg) => Some(cfg), - Err(e) => { - tracing::warn!("解析配置文件失败 ({}): {}", path.display(), e); - None - } - } - } -} - -/// 替换字符串中的 `${VAR_NAME}` 为环境变量值 -fn resolve_env_vars(input: &str) -> String { - let mut result = String::with_capacity(input.len()); - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '$' && chars.peek() == Some(&'{') { - chars.next(); // 跳过 '{' - let mut var_name = String::new(); - while let Some(&ch) = chars.peek() { - if ch == '}' { - chars.next(); // 跳过 '}' - break; - } - var_name.push(ch); - chars.next(); - } - let value = std::env::var(&var_name).unwrap_or_default(); - result.push_str(&value); - } else { - result.push(c); - } - } - - result -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_resolve_env_vars() { - // set_var is unsafe in edition 2024; skip env test - let result = resolve_env_vars("prefix_${TEST_VAR}_suffix"); - // TEST_VAR might not be set, so result could be "prefix__suffix" - assert_eq!(result, "prefix__suffix"); - } - - #[test] - fn test_resolve_missing_env_var() { - let result = resolve_env_vars("prefix_${MISSING_VAR}_suffix"); - assert_eq!(result, "prefix__suffix"); - } - - #[test] - fn test_resolve_no_vars() { - let result = resolve_env_vars("plain text"); - assert_eq!(result, "plain text"); - } -} diff --git a/src/main.rs b/src/main.rs index fcbc670..c7a68cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod cli; -mod config; mod context; mod db; mod llm; @@ -12,7 +11,6 @@ mod wechat; use clap::Parser; use cli::{Cli, Commands, SkillsAction}; -use config::AppConfig; use context::MemoryStore; use db::Database; use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage}; @@ -44,9 +42,7 @@ async fn main() { } } - let config = AppConfig::from_file("config.json"); - - // 初始化数据库(如果配置了 DATABASE_URL) + let file_state = state::StateManager::new(".data/weixin-ilink"); let database = match Database::connect().await { Ok(db) => { info!("✅ 数据库连接成功"); @@ -65,7 +61,7 @@ async fn main() { memory_store.load("").await; // 文件状态管理器(降级方案) - let file_state = state::StateManager::new(&config.storage.state_dir); + let file_state = state::StateManager::new(".data/weixin-ilink"); match cli.command { Commands::Login { timeout } => { @@ -75,15 +71,7 @@ async fn main() { llm: enable_llm, echo, } => { - cmd_listen( - enable_llm, - echo, - &database, - &file_state, - &config, - &memory_store, - ) - .await; + cmd_listen(enable_llm, echo, &database, &file_state, &memory_store).await; } Commands::Send { to, @@ -103,7 +91,7 @@ async fn main() { cmd_usage(&database, since, until, model).await; } Commands::Service => { - cmd_listen(true, false, &database, &file_state, &config, &memory_store).await + cmd_listen(true, false, &database, &file_state, &memory_store).await } Commands::Skills { action } => { cmd_skills(action).await; @@ -162,7 +150,6 @@ async fn cmd_listen( echo: bool, database: &Option>, file_state: &state::StateManager, - config: &AppConfig, memory_store: &Arc, ) { // 从数据库或文件加载认证 @@ -230,7 +217,7 @@ async fn cmd_listen( let conversation = if enable_llm { let sys_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT") .unwrap_or_else(|_| DEFAULT_SYSTEM_PROMPT.to_string()); - let model = config.llm.deepseek.model.clone(); + let model = std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| "deepseek-v4-flash".to_string()); let mut cfg = ConversationConfig { system_prompt: sys_prompt, @@ -355,6 +342,12 @@ async fn cmd_listen( Box::pin(async move { let user_id = session.lock().await.current_user_id.clone(); + + // 内置工具优先 + if let Some(result) = tools::builtin::BuiltinRegistry::execute(&n, &args).await { + return Ok(result.output); + } + match n.as_str() { "read_memories" => return Ok(memory_store.read_for(&user_id).await), "write_memory" => { @@ -369,15 +362,12 @@ async fn cmd_listen( "read_summaries" => return Ok(context::tools::read_summaries(&session).await), "query_capabilities" => { if let Some(r) = reg.as_ref() { - return Ok(build_capability_list(r)); + let mut specs: Vec = r.list_specs().into_iter().cloned().collect(); + specs.extend(tools::builtin::BuiltinRegistry::specs()); + return Ok(build_capability_list(&specs)); } return Ok("暂无工具".to_string()); } - "get_current_datetime" => { - let now = chrono::Local::now(); - return Ok(serde_json::json!({"datetime": now.format("%Y-%m-%d %H:%M:%S").to_string()}).to_string()); - } - "manage_memos" => return handle_memos(&args).await, "call_capability" => { let cap_params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default(); let cap_name = cap_params.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()); @@ -818,9 +808,9 @@ fn global_skills_dir() -> std::path::PathBuf { .join("skills") } -fn build_capability_list(reg: &SkillRegistry) -> String { +fn build_capability_list(specs: &[tools::skill::SkillSpec]) -> String { let mut lines = vec!["📋 可用工具:".to_string()]; - for spec in reg.list_specs() { + for spec in specs { let risk = if spec.risk_level == tools::skill::RiskLevel::High { " ⚠️需确认" } else { "" }; lines.push(format!( "\n🔹 {} {} — {}", spec.name, risk, spec.description @@ -829,44 +819,6 @@ fn build_capability_list(reg: &SkillRegistry) -> String { lines.join("\n") } -async fn handle_memos(args_json: &str) -> Result { - use std::io::{Read, Write}; - let params: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default(); - let action = params.get("action").and_then(|v| v.as_str()).unwrap_or("list"); - let path = ".data/memos.json"; - std::fs::create_dir_all(".data").ok(); - - let mut data: Vec = if std::path::Path::new(path).exists() { - serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default()).unwrap_or_default() - } else { - vec![] - }; - - match action { - "add" => { - let content = params.get("content").and_then(|v| v.as_str()).unwrap_or(""); - if content.is_empty() { return Ok(r#"{"error":"请提供 content 参数"}"#.to_string()); } - let next_id = data.iter().filter_map(|m| m.get("id").and_then(|v| v.as_i64())).max().unwrap_or(0) + 1; - data.push(serde_json::json!({"id": next_id, "content": content, "created_at": chrono::Local::now().format("%Y-%m-%d %H:%M").to_string()})); - std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok(); - Ok(format!("已添加备忘录 #{}", next_id)) - } - "list" => { - if data.is_empty() { return Ok("暂无备忘录".to_string()); } - Ok(data.iter().map(|m| format!("#{} [{}] {}", m["id"], m.get("created_at").and_then(|v| v.as_str()).unwrap_or("?"), m["content"].as_str().unwrap_or("?"))).collect::>().join("\n")) - } - "delete" => { - let id = params.get("id").and_then(|v| v.as_i64()).unwrap_or(0); - let before = data.len(); - data.retain(|m| m.get("id").and_then(|v| v.as_i64()) != Some(id)); - if data.len() == before { return Ok(format!("未找到备忘录 #{}", id)); } - std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok(); - Ok(format!("已删除备忘录 #{}", id)) - } - _ => Ok("未知操作,支持: add, list, delete".to_string()) - } -} - async fn cmd_skills(action: SkillsAction) { use dialoguer::{MultiSelect, theme::ColorfulTheme}; use console::Term; diff --git a/src/tools/builtin.rs b/src/tools/builtin.rs new file mode 100644 index 0000000..6ac149a --- /dev/null +++ b/src/tools/builtin.rs @@ -0,0 +1,85 @@ +use crate::tools::skill::{RiskLevel, SkillResult}; +use chrono::Local; + +/// 内置工具执行器:统一处理参数校验、执行、错误返回 +pub struct BuiltinRegistry; + +impl BuiltinRegistry { + /// 执行内置工具,返回 Some(SkillResult) 或 None(不是内置工具) + pub async fn execute(name: &str, args_json: &str) -> Option { + match name { + "get_current_datetime" => Some(execute_datetime()), + "manage_memos" => Some(handle_memos(args_json).await), + _ => None, + } + } + + /// 返回所有内置工具的 spec 列表 + pub fn specs() -> Vec { + vec![ + super::skill::SkillSpec { + name: "get_current_datetime".into(), + description: "获取当前日期时间(北京时间 UTC+8)".into(), + risk_level: RiskLevel::Low, + parameters: serde_json::json!({"type":"object","properties":{"format":{"type":"string"}}}), + timeout_secs: 5, + }, + super::skill::SkillSpec { + name: "manage_memos".into(), + description: "管理备忘录:添加/列出/删除。高风险操作(add/delete)需确认".into(), + risk_level: RiskLevel::High, + parameters: serde_json::json!({"type":"object","properties":{"action":{"type":"string","enum":["add","list","delete"]},"content":{"type":"string"},"id":{"type":"integer"}}}), + timeout_secs: 10, + }, + ] + } +} + +fn execute_datetime() -> SkillResult { + let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + SkillResult::ok(serde_json::json!({"datetime": now}).to_string()) +} + +async fn handle_memos(args_json: &str) -> SkillResult { + let params: serde_json::Value = serde_json::from_str(args_json).unwrap_or_default(); + let action = params.get("action").and_then(|v| v.as_str()).unwrap_or("list"); + let path = ".data/memos.json"; + std::fs::create_dir_all(".data").ok(); + + let mut data: Vec = if std::path::Path::new(path).exists() { + serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default()).unwrap_or_default() + } else { + vec![] + }; + + match action { + "add" => { + let content = params.get("content").and_then(|v| v.as_str()).unwrap_or(""); + if content.is_empty() { return SkillResult::error("请提供 content 参数"); } + let next_id = data.iter().filter_map(|m| m.get("id").and_then(|v| v.as_i64())).max().unwrap_or(0) + 1; + data.push(serde_json::json!({ + "id": next_id, "content": content, + "created_at": Local::now().format("%Y-%m-%d %H:%M").to_string() + })); + std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok(); + SkillResult::ok(format!("已添加备忘录 #{}", next_id)) + } + "list" => { + if data.is_empty() { return SkillResult::ok("暂无备忘录".to_string()); } + let lines: Vec = data.iter().map(|m| format!( + "#{} [{}] {}", + m["id"], m.get("created_at").and_then(|v| v.as_str()).unwrap_or("?"), m["content"].as_str().unwrap_or("?") + )).collect(); + SkillResult::ok(lines.join("\n")) + } + "delete" => { + let id = params.get("id").and_then(|v| v.as_i64()).unwrap_or(0); + let before = data.len(); + data.retain(|m| m.get("id").and_then(|v| v.as_i64()) != Some(id)); + if data.len() == before { return SkillResult::error(format!("未找到备忘录 #{}", id)); } + std::fs::write(path, serde_json::to_string_pretty(&data).unwrap()).ok(); + SkillResult::ok(format!("已删除备忘录 #{}", id)) + } + _ => SkillResult::error("未知操作,支持: add, list, delete"), + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index ed92896..835410f 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,4 +1,5 @@ pub mod approval; +pub mod builtin; pub mod parser; pub mod registry; pub mod skill;