mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-19 00:37:23 +08:00
- Simplify skill-creator installation flow - Refine skill selection prompt for better matching - Add parameter alias and env variable hints for tools - Skip linkai-agent when unconfigured - Create skills/ dir in workspace on init
287 lines
12 KiB
Python
287 lines
12 KiB
Python
"""
|
|
Environment Configuration Tool - Manage API keys and environment variables
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
from typing import Dict, Any
|
|
from pathlib import Path
|
|
|
|
from agent.tools.base_tool import BaseTool, ToolResult
|
|
from common.log import logger
|
|
from common.utils import expand_path
|
|
|
|
|
|
# API Key 知识库:常见的环境变量及其描述
|
|
API_KEY_REGISTRY = {
|
|
# AI 模型服务
|
|
"OPENAI_API_KEY": "OpenAI API 密钥 (用于GPT模型、Embedding模型)",
|
|
"GEMINI_API_KEY": "Google Gemini API 密钥",
|
|
"CLAUDE_API_KEY": "Claude API 密钥 (用于Claude模型)",
|
|
"LINKAI_API_KEY": "LinkAI智能体平台 API 密钥,支持多种模型切换",
|
|
# 搜索服务
|
|
"BOCHA_API_KEY": "博查 AI 搜索 API 密钥 ",
|
|
}
|
|
|
|
class EnvConfig(BaseTool):
|
|
"""Tool for managing environment variables (API keys, etc.)"""
|
|
|
|
name: str = "env_config"
|
|
description: str = (
|
|
"Manage API keys and skill configurations securely. "
|
|
"Use this tool when user wants to configure API keys (like BOCHA_API_KEY, OPENAI_API_KEY), "
|
|
"view configured keys, or manage skill settings. "
|
|
"Actions: 'set' (add/update key), 'get' (view specific key), 'list' (show all configured keys), 'delete' (remove key). "
|
|
"Values are automatically masked for security. Changes take effect immediately via hot reload."
|
|
)
|
|
|
|
params: dict = {
|
|
"type": "object",
|
|
"properties": {
|
|
"action": {
|
|
"type": "string",
|
|
"description": "Action to perform: 'set', 'get', 'list', 'delete'",
|
|
"enum": ["set", "get", "list", "delete"]
|
|
},
|
|
"key": {
|
|
"type": "string",
|
|
"description": (
|
|
"Environment variable key name. Common keys:\n"
|
|
"- OPENAI_API_KEY: OpenAI API (GPT models)\n"
|
|
"- OPENAI_API_BASE: OpenAI API base URL\n"
|
|
"- CLAUDE_API_KEY: Anthropic Claude API\n"
|
|
"- GEMINI_API_KEY: Google Gemini API\n"
|
|
"- LINKAI_API_KEY: LinkAI platform\n"
|
|
"- BOCHA_API_KEY: Bocha AI search (博查搜索)\n"
|
|
"Use exact key names (case-sensitive, all uppercase with underscores)"
|
|
)
|
|
},
|
|
"value": {
|
|
"type": "string",
|
|
"description": "Value to set for the environment variable (for 'set' action)"
|
|
}
|
|
},
|
|
"required": ["action"]
|
|
}
|
|
|
|
def __init__(self, config: dict = None):
|
|
self.config = config or {}
|
|
# Store env config in ~/.cow directory (outside workspace for security)
|
|
self.env_dir = expand_path("~/.cow")
|
|
self.env_path = os.path.join(self.env_dir, '.env')
|
|
self.agent_bridge = self.config.get("agent_bridge") # Reference to AgentBridge for hot reload
|
|
# Don't create .env file in __init__ to avoid issues during tool discovery
|
|
# It will be created on first use in execute()
|
|
|
|
def _ensure_env_file(self):
|
|
"""Ensure the .env file exists"""
|
|
# Create ~/.cow directory if it doesn't exist
|
|
os.makedirs(self.env_dir, exist_ok=True)
|
|
|
|
if not os.path.exists(self.env_path):
|
|
Path(self.env_path).touch()
|
|
logger.info(f"[EnvConfig] Created .env file at {self.env_path}")
|
|
|
|
def _mask_value(self, value: str) -> str:
|
|
"""Mask sensitive parts of a value for logging"""
|
|
if not value or len(value) <= 10:
|
|
return "***"
|
|
return f"{value[:6]}***{value[-4:]}"
|
|
|
|
def _read_env_file(self) -> Dict[str, str]:
|
|
"""Read all key-value pairs from .env file"""
|
|
env_vars = {}
|
|
if os.path.exists(self.env_path):
|
|
with open(self.env_path, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
# Skip empty lines and comments
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
# Parse KEY=VALUE
|
|
match = re.match(r'^([^=]+)=(.*)$', line)
|
|
if match:
|
|
key, value = match.groups()
|
|
env_vars[key.strip()] = value.strip()
|
|
return env_vars
|
|
|
|
def _write_env_file(self, env_vars: Dict[str, str]):
|
|
"""Write all key-value pairs to .env file"""
|
|
with open(self.env_path, 'w', encoding='utf-8') as f:
|
|
f.write("# Environment variables for agent skills\n")
|
|
f.write("# Auto-managed by env_config tool\n\n")
|
|
for key, value in sorted(env_vars.items()):
|
|
f.write(f"{key}={value}\n")
|
|
|
|
def _reload_env(self):
|
|
"""Reload environment variables from .env file"""
|
|
env_vars = self._read_env_file()
|
|
for key, value in env_vars.items():
|
|
os.environ[key] = value
|
|
logger.debug(f"[EnvConfig] Reloaded {len(env_vars)} environment variables")
|
|
|
|
def _refresh_skills(self):
|
|
"""Refresh skills after environment variable changes"""
|
|
if self.agent_bridge:
|
|
try:
|
|
# Reload .env file
|
|
self._reload_env()
|
|
|
|
# Refresh skills in all agent instances
|
|
refreshed = self.agent_bridge.refresh_all_skills()
|
|
logger.info(f"[EnvConfig] Refreshed skills in {refreshed} agent instance(s)")
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"[EnvConfig] Failed to refresh skills: {e}")
|
|
return False
|
|
return False
|
|
|
|
def execute(self, args: Dict[str, Any]) -> ToolResult:
|
|
"""
|
|
Execute environment configuration operation
|
|
|
|
:param args: Contains action, key, and value parameters
|
|
:return: Result of the operation
|
|
"""
|
|
# Ensure .env file exists on first use
|
|
self._ensure_env_file()
|
|
|
|
action = args.get("action")
|
|
key = args.get("key")
|
|
value = args.get("value")
|
|
|
|
try:
|
|
if action == "set":
|
|
if not key or not value:
|
|
return ToolResult.fail("Error: 'key' and 'value' are required for 'set' action.")
|
|
|
|
# Read current env vars
|
|
env_vars = self._read_env_file()
|
|
|
|
# Update the key
|
|
env_vars[key] = value
|
|
|
|
# Write back to file
|
|
self._write_env_file(env_vars)
|
|
|
|
# Update current process env
|
|
os.environ[key] = value
|
|
|
|
logger.info(f"[EnvConfig] Set {key}={self._mask_value(value)}")
|
|
|
|
# Try to refresh skills immediately
|
|
refreshed = self._refresh_skills()
|
|
|
|
result = {
|
|
"message": f"Successfully set {key}",
|
|
"key": key,
|
|
"value": self._mask_value(value),
|
|
}
|
|
|
|
if refreshed:
|
|
result["note"] = "✅ Skills refreshed automatically - changes are now active"
|
|
else:
|
|
result["note"] = "⚠️ Skills not refreshed - restart agent to load new skills"
|
|
|
|
return ToolResult.success(result)
|
|
|
|
elif action == "get":
|
|
if not key:
|
|
return ToolResult.fail("Error: 'key' is required for 'get' action.")
|
|
|
|
# Check in file first, then in current env
|
|
env_vars = self._read_env_file()
|
|
value = env_vars.get(key) or os.getenv(key)
|
|
|
|
# Get description from registry
|
|
description = API_KEY_REGISTRY.get(key, "未知用途的环境变量")
|
|
|
|
if value is not None:
|
|
logger.info(f"[EnvConfig] Got {key}={self._mask_value(value)}")
|
|
return ToolResult.success({
|
|
"key": key,
|
|
"value": self._mask_value(value),
|
|
"description": description,
|
|
"exists": True,
|
|
"note": f"Value is masked for security. In bash, use ${key} directly — it is auto-injected."
|
|
})
|
|
else:
|
|
return ToolResult.success({
|
|
"key": key,
|
|
"description": description,
|
|
"exists": False,
|
|
"message": f"Environment variable '{key}' is not set"
|
|
})
|
|
|
|
elif action == "list":
|
|
env_vars = self._read_env_file()
|
|
|
|
# Build detailed variable list with descriptions
|
|
variables_with_info = {}
|
|
for key, value in env_vars.items():
|
|
variables_with_info[key] = {
|
|
"value": self._mask_value(value),
|
|
"description": API_KEY_REGISTRY.get(key, "未知用途的环境变量")
|
|
}
|
|
|
|
logger.info(f"[EnvConfig] Listed {len(env_vars)} environment variables")
|
|
|
|
if not env_vars:
|
|
return ToolResult.success({
|
|
"message": "No environment variables configured",
|
|
"variables": {},
|
|
"note": "常用的 API 密钥可以通过 env_config(action='set', key='KEY_NAME', value='your-key') 来配置"
|
|
})
|
|
|
|
return ToolResult.success({
|
|
"message": f"Found {len(env_vars)} environment variable(s)",
|
|
"variables": variables_with_info
|
|
})
|
|
|
|
elif action == "delete":
|
|
if not key:
|
|
return ToolResult.fail("Error: 'key' is required for 'delete' action.")
|
|
|
|
# Read current env vars
|
|
env_vars = self._read_env_file()
|
|
|
|
if key not in env_vars:
|
|
return ToolResult.success({
|
|
"message": f"Environment variable '{key}' was not set",
|
|
"key": key
|
|
})
|
|
|
|
# Remove the key
|
|
del env_vars[key]
|
|
|
|
# Write back to file
|
|
self._write_env_file(env_vars)
|
|
|
|
# Remove from current process env
|
|
if key in os.environ:
|
|
del os.environ[key]
|
|
|
|
logger.info(f"[EnvConfig] Deleted {key}")
|
|
|
|
# Try to refresh skills immediately
|
|
refreshed = self._refresh_skills()
|
|
|
|
result = {
|
|
"message": f"Successfully deleted {key}",
|
|
"key": key,
|
|
}
|
|
|
|
if refreshed:
|
|
result["note"] = "✅ Skills refreshed automatically - changes are now active"
|
|
else:
|
|
result["note"] = "⚠️ Skills not refreshed - restart agent to apply changes"
|
|
|
|
return ToolResult.success(result)
|
|
|
|
else:
|
|
return ToolResult.fail(f"Error: Unknown action '{action}'. Use 'set', 'get', 'list', or 'delete'.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"[EnvConfig] Error: {e}", exc_info=True)
|
|
return ToolResult.fail(f"EnvConfig tool error: {str(e)}")
|