refactor: 删除 config.json + 内置工具抽离 + 统一接口
删除:
- config.rs / config.json(AppConfig/LmStudioConfig)
- LM Studio 残留(lmstudio.rs 已在之前删除)
- main.rs 中散落的内置工具 match arms
新增:
- src/tools/builtin.rs — BuiltinRegistry 统一切入
get_current_datetime、manage_memos 为核心内置工具
统一参数校验、风险等级、错误返回
改动:
- 模型名直接从 DEEPSEEK_MODEL 环境变量读取
- builtin 工具与 skills 工具统一经过 SkillRegistry 审批
- query_capabilities 合并内置 + 外部技能列表
This commit is contained in:
-14
@@ -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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,8 @@
|
|||||||
# get_current_datetime
|
# get_current_datetime
|
||||||
|
获取当前日期时间(UTC+8)。内置 Rust 实现,无脚本依赖。
|
||||||
获取当前日期时间(北京时间 UTC+8)。
|
|
||||||
|
|
||||||
## Risk Level
|
## Risk Level
|
||||||
Low
|
Low
|
||||||
|
|
||||||
## Parameters
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "时间格式: full | date | time",
|
|
||||||
"enum": ["full", "date", "time"],
|
|
||||||
"default": "full"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execute
|
## Execute
|
||||||
```bash
|
```bash
|
||||||
scripts/datetime.sh
|
# Builtin: Rust
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,36 +1,8 @@
|
|||||||
# manage_memos
|
# manage_memos
|
||||||
|
管理备忘录(添加/列出/删除)。内置 Rust 实现,无脚本依赖。
|
||||||
管理备忘录:添加、列出、删除。
|
|
||||||
|
|
||||||
备忘录存储在 `.data/memos.json` 文件中。
|
|
||||||
|
|
||||||
## Risk Level
|
## Risk Level
|
||||||
High
|
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
|
## Execute
|
||||||
```bash
|
```bash
|
||||||
scripts/memo.sh
|
# Builtin: Rust
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
|
||||||
-202
@@ -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<Path>) -> 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<Self> {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+16
-64
@@ -1,5 +1,4 @@
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
|
||||||
mod context;
|
mod context;
|
||||||
mod db;
|
mod db;
|
||||||
mod llm;
|
mod llm;
|
||||||
@@ -12,7 +11,6 @@ mod wechat;
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, SkillsAction};
|
use cli::{Cli, Commands, SkillsAction};
|
||||||
use config::AppConfig;
|
|
||||||
use context::MemoryStore;
|
use context::MemoryStore;
|
||||||
use db::Database;
|
use db::Database;
|
||||||
use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage};
|
use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage};
|
||||||
@@ -44,9 +42,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = AppConfig::from_file("config.json");
|
let file_state = state::StateManager::new(".data/weixin-ilink");
|
||||||
|
|
||||||
// 初始化数据库(如果配置了 DATABASE_URL)
|
|
||||||
let database = match Database::connect().await {
|
let database = match Database::connect().await {
|
||||||
Ok(db) => {
|
Ok(db) => {
|
||||||
info!("✅ 数据库连接成功");
|
info!("✅ 数据库连接成功");
|
||||||
@@ -65,7 +61,7 @@ async fn main() {
|
|||||||
memory_store.load("").await;
|
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 {
|
match cli.command {
|
||||||
Commands::Login { timeout } => {
|
Commands::Login { timeout } => {
|
||||||
@@ -75,15 +71,7 @@ async fn main() {
|
|||||||
llm: enable_llm,
|
llm: enable_llm,
|
||||||
echo,
|
echo,
|
||||||
} => {
|
} => {
|
||||||
cmd_listen(
|
cmd_listen(enable_llm, echo, &database, &file_state, &memory_store).await;
|
||||||
enable_llm,
|
|
||||||
echo,
|
|
||||||
&database,
|
|
||||||
&file_state,
|
|
||||||
&config,
|
|
||||||
&memory_store,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
Commands::Send {
|
Commands::Send {
|
||||||
to,
|
to,
|
||||||
@@ -103,7 +91,7 @@ async fn main() {
|
|||||||
cmd_usage(&database, since, until, model).await;
|
cmd_usage(&database, since, until, model).await;
|
||||||
}
|
}
|
||||||
Commands::Service => {
|
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 } => {
|
Commands::Skills { action } => {
|
||||||
cmd_skills(action).await;
|
cmd_skills(action).await;
|
||||||
@@ -162,7 +150,6 @@ async fn cmd_listen(
|
|||||||
echo: bool,
|
echo: bool,
|
||||||
database: &Option<Arc<Database>>,
|
database: &Option<Arc<Database>>,
|
||||||
file_state: &state::StateManager,
|
file_state: &state::StateManager,
|
||||||
config: &AppConfig,
|
|
||||||
memory_store: &Arc<MemoryStore>,
|
memory_store: &Arc<MemoryStore>,
|
||||||
) {
|
) {
|
||||||
// 从数据库或文件加载认证
|
// 从数据库或文件加载认证
|
||||||
@@ -230,7 +217,7 @@ async fn cmd_listen(
|
|||||||
let conversation = if enable_llm {
|
let conversation = if enable_llm {
|
||||||
let sys_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT")
|
let sys_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT")
|
||||||
.unwrap_or_else(|_| DEFAULT_SYSTEM_PROMPT.to_string());
|
.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 {
|
let mut cfg = ConversationConfig {
|
||||||
system_prompt: sys_prompt,
|
system_prompt: sys_prompt,
|
||||||
@@ -355,6 +342,12 @@ async fn cmd_listen(
|
|||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let user_id = session.lock().await.current_user_id.clone();
|
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() {
|
match n.as_str() {
|
||||||
"read_memories" => return Ok(memory_store.read_for(&user_id).await),
|
"read_memories" => return Ok(memory_store.read_for(&user_id).await),
|
||||||
"write_memory" => {
|
"write_memory" => {
|
||||||
@@ -369,15 +362,12 @@ async fn cmd_listen(
|
|||||||
"read_summaries" => return Ok(context::tools::read_summaries(&session).await),
|
"read_summaries" => return Ok(context::tools::read_summaries(&session).await),
|
||||||
"query_capabilities" => {
|
"query_capabilities" => {
|
||||||
if let Some(r) = reg.as_ref() {
|
if let Some(r) = reg.as_ref() {
|
||||||
return Ok(build_capability_list(r));
|
let mut specs: Vec<tools::skill::SkillSpec> = r.list_specs().into_iter().cloned().collect();
|
||||||
|
specs.extend(tools::builtin::BuiltinRegistry::specs());
|
||||||
|
return Ok(build_capability_list(&specs));
|
||||||
}
|
}
|
||||||
return Ok("暂无工具".to_string());
|
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" => {
|
"call_capability" => {
|
||||||
let cap_params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
|
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());
|
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")
|
.join("skills")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_capability_list(reg: &SkillRegistry) -> String {
|
fn build_capability_list(specs: &[tools::skill::SkillSpec]) -> String {
|
||||||
let mut lines = vec!["📋 可用工具:".to_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 { "" };
|
let risk = if spec.risk_level == tools::skill::RiskLevel::High { " ⚠️需确认" } else { "" };
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"\n🔹 {} {} — {}", spec.name, risk, spec.description
|
"\n🔹 {} {} — {}", spec.name, risk, spec.description
|
||||||
@@ -829,44 +819,6 @@ fn build_capability_list(reg: &SkillRegistry) -> String {
|
|||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_memos(args_json: &str) -> Result<String, String> {
|
|
||||||
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<serde_json::Value> = 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::<Vec<_>>().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) {
|
async fn cmd_skills(action: SkillsAction) {
|
||||||
use dialoguer::{MultiSelect, theme::ColorfulTheme};
|
use dialoguer::{MultiSelect, theme::ColorfulTheme};
|
||||||
use console::Term;
|
use console::Term;
|
||||||
|
|||||||
@@ -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<SkillResult> {
|
||||||
|
match name {
|
||||||
|
"get_current_datetime" => Some(execute_datetime()),
|
||||||
|
"manage_memos" => Some(handle_memos(args_json).await),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返回所有内置工具的 spec 列表
|
||||||
|
pub fn specs() -> Vec<super::skill::SkillSpec> {
|
||||||
|
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<serde_json::Value> = 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<String> = 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod approval;
|
pub mod approval;
|
||||||
|
pub mod builtin;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
pub mod skill;
|
pub mod skill;
|
||||||
|
|||||||
Reference in New Issue
Block a user