mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-28 17:01:08 +08:00
feat: model and agent config in web console
This commit is contained in:
@@ -65,30 +65,67 @@ class AgentLLMModel(LLMModel):
|
||||
LLM Model adapter that uses COW's existing bot infrastructure
|
||||
"""
|
||||
|
||||
_MODEL_BOT_TYPE_MAP = {
|
||||
"wenxin": const.BAIDU, "wenxin-4": const.BAIDU,
|
||||
"xunfei": const.XUNFEI, const.QWEN: const.QWEN,
|
||||
const.MODELSCOPE: const.MODELSCOPE,
|
||||
}
|
||||
_MODEL_PREFIX_MAP = [
|
||||
("qwen", const.QWEN_DASHSCOPE), ("qwq", const.QWEN_DASHSCOPE), ("qvq", const.QWEN_DASHSCOPE),
|
||||
("gemini", const.GEMINI), ("glm", const.ZHIPU_AI), ("claude", const.CLAUDEAPI),
|
||||
("moonshot", const.MOONSHOT), ("kimi", const.MOONSHOT),
|
||||
("doubao", const.DOUBAO),
|
||||
]
|
||||
|
||||
def __init__(self, bridge: Bridge, bot_type: str = "chat"):
|
||||
# Get model name directly from config
|
||||
from config import conf
|
||||
model_name = conf().get("model", const.GPT_41)
|
||||
super().__init__(model=model_name)
|
||||
super().__init__(model=conf().get("model", const.GPT_41))
|
||||
self.bridge = bridge
|
||||
self.bot_type = bot_type
|
||||
self._bot = None
|
||||
self._use_linkai = conf().get("use_linkai", False) and conf().get("linkai_api_key")
|
||||
|
||||
self._bot_model = None
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
from config import conf
|
||||
return conf().get("model", const.GPT_41)
|
||||
|
||||
@model.setter
|
||||
def model(self, value):
|
||||
pass
|
||||
|
||||
def _resolve_bot_type(self, model_name: str) -> str:
|
||||
"""Resolve bot type from model name, matching Bridge.__init__ logic."""
|
||||
from config import conf
|
||||
if conf().get("use_linkai", False) and conf().get("linkai_api_key"):
|
||||
return const.LINKAI
|
||||
if not model_name or not isinstance(model_name, str):
|
||||
return const.CHATGPT
|
||||
if model_name in self._MODEL_BOT_TYPE_MAP:
|
||||
return self._MODEL_BOT_TYPE_MAP[model_name]
|
||||
if model_name.lower().startswith("minimax") or model_name in ["abab6.5-chat"]:
|
||||
return const.MiniMax
|
||||
if model_name in [const.QWEN_TURBO, const.QWEN_PLUS, const.QWEN_MAX]:
|
||||
return const.QWEN_DASHSCOPE
|
||||
if model_name in [const.MOONSHOT, "moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"]:
|
||||
return const.MOONSHOT
|
||||
if model_name in [const.DEEPSEEK_CHAT, const.DEEPSEEK_REASONER]:
|
||||
return const.CHATGPT
|
||||
for prefix, btype in self._MODEL_PREFIX_MAP:
|
||||
if model_name.startswith(prefix):
|
||||
return btype
|
||||
return const.CHATGPT
|
||||
|
||||
@property
|
||||
def bot(self):
|
||||
"""Lazy load the bot and enhance it with tool calling if needed"""
|
||||
if self._bot is None:
|
||||
# If use_linkai is enabled, use LinkAI bot directly
|
||||
if self._use_linkai:
|
||||
self._bot = self.bridge.find_chat_bot(const.LINKAI)
|
||||
else:
|
||||
self._bot = self.bridge.get_bot(self.bot_type)
|
||||
# Automatically add tool calling support if not present
|
||||
self._bot = add_openai_compatible_support(self._bot)
|
||||
|
||||
# Log bot info
|
||||
bot_name = type(self._bot).__name__
|
||||
"""Lazy load the bot, re-create when model changes"""
|
||||
from models.bot_factory import create_bot
|
||||
cur_model = self.model
|
||||
if self._bot is None or self._bot_model != cur_model:
|
||||
bot_type = self._resolve_bot_type(cur_model)
|
||||
self._bot = create_bot(bot_type)
|
||||
self._bot = add_openai_compatible_support(self._bot)
|
||||
self._bot_model = cur_model
|
||||
return self._bot
|
||||
|
||||
def call(self, request: LLMRequest):
|
||||
|
||||
@@ -159,7 +159,7 @@ class FeiShuChanel(ChatChannel):
|
||||
self.feishu_app_id,
|
||||
self.feishu_app_secret,
|
||||
event_handler=event_handler,
|
||||
log_level=lark.LogLevel.DEBUG if conf().get("debug") else lark.LogLevel.WARNING
|
||||
log_level=lark.LogLevel.WARNING
|
||||
)
|
||||
|
||||
logger.debug("[FeiShu] Websocket client starting...")
|
||||
|
||||
@@ -294,68 +294,122 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-6">
|
||||
|
||||
<!-- Model Config Card -->
|
||||
<div class="placeholder-card bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="w-9 h-9 rounded-lg bg-primary-50 dark:bg-primary-900/30 flex items-center justify-center">
|
||||
<i class="fas fa-microchip text-primary-500 text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_model">Model Configuration</h3>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0">Model</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono" id="cfg-model">--</span>
|
||||
<div class="space-y-5">
|
||||
<!-- Provider -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_provider">Provider</label>
|
||||
<div id="cfg-provider" class="cfg-dropdown" tabindex="0">
|
||||
<div class="cfg-dropdown-selected">
|
||||
<span class="cfg-dropdown-text">--</span>
|
||||
<i class="fas fa-chevron-down cfg-dropdown-arrow"></i>
|
||||
</div>
|
||||
<div class="cfg-dropdown-menu"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Model -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_model_name">Model</label>
|
||||
<div id="cfg-model-select" class="cfg-dropdown" tabindex="0">
|
||||
<div class="cfg-dropdown-selected">
|
||||
<span class="cfg-dropdown-text">--</span>
|
||||
<i class="fas fa-chevron-down cfg-dropdown-arrow"></i>
|
||||
</div>
|
||||
<div class="cfg-dropdown-menu"></div>
|
||||
</div>
|
||||
<div id="cfg-model-custom-wrap" class="mt-2 hidden">
|
||||
<input id="cfg-model-custom" type="text"
|
||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors"
|
||||
data-i18n-placeholder="config_custom_model_hint" placeholder="Enter custom model name">
|
||||
</div>
|
||||
</div>
|
||||
<!-- API Key -->
|
||||
<div id="cfg-api-key-wrap">
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">API Key</label>
|
||||
<div class="relative">
|
||||
<input id="cfg-api-key" type="text" autocomplete="off" data-1p-ignore data-lpignore="true"
|
||||
class="w-full px-3 py-2 pr-10 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors cfg-key-masked"
|
||||
placeholder="sk-...">
|
||||
<button type="button" id="cfg-api-key-toggle"
|
||||
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600
|
||||
dark:hover:text-slate-300 cursor-pointer transition-colors p-1"
|
||||
onclick="toggleApiKeyVisibility()">
|
||||
<i class="fas fa-eye text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API Base -->
|
||||
<div id="cfg-api-base-wrap" class="hidden">
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5">API Base</label>
|
||||
<input id="cfg-api-base" type="text"
|
||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors"
|
||||
placeholder="https://...">
|
||||
</div>
|
||||
<!-- Save Model Button -->
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<span id="cfg-model-status" class="text-xs text-primary-500 opacity-0 transition-opacity duration-300"></span>
|
||||
<button id="cfg-model-save"
|
||||
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
|
||||
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick="saveModelConfig()" data-i18n="config_save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Config Card -->
|
||||
<div class="placeholder-card bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<div class="w-9 h-9 rounded-lg bg-emerald-50 dark:bg-emerald-900/30 flex items-center justify-center">
|
||||
<i class="fas fa-robot text-emerald-500 text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_agent">Agent Configuration</h3>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0" data-i18n="config_agent_enabled">Agent Mode</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1" id="cfg-agent">--</span>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_max_tokens">Max Context Tokens</label>
|
||||
<input id="cfg-max-tokens" type="number" min="1000" max="200000" step="1000"
|
||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors">
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0" data-i18n="config_max_tokens">Max Tokens</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono" id="cfg-max-tokens">--</span>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_max_turns">Max Context Turns</label>
|
||||
<input id="cfg-max-turns" type="number" min="1" max="100" step="1"
|
||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors">
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0" data-i18n="config_max_turns">Max Turns</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono" id="cfg-max-turns">--</span>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 dark:text-slate-400 mb-1.5" data-i18n="config_max_steps">Max Steps</label>
|
||||
<input id="cfg-max-steps" type="number" min="1" max="50" step="1"
|
||||
class="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600
|
||||
bg-slate-50 dark:bg-white/5 text-sm text-slate-800 dark:text-slate-100
|
||||
focus:outline-none focus:border-primary-500 font-mono transition-colors">
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0" data-i18n="config_max_steps">Max Steps</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono" id="cfg-max-steps">--</span>
|
||||
<div class="flex items-center justify-end gap-3 pt-1">
|
||||
<span id="cfg-agent-status" class="text-xs text-primary-500 opacity-0 transition-opacity duration-300"></span>
|
||||
<button id="cfg-agent-save"
|
||||
class="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white text-sm font-medium
|
||||
cursor-pointer transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onclick="saveAgentConfig()" data-i18n="config_save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Channel Config Card -->
|
||||
<div class="placeholder-card bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-9 h-9 rounded-lg bg-amber-50 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<i class="fas fa-tower-broadcast text-amber-500 text-sm"></i>
|
||||
</div>
|
||||
<h3 class="font-semibold text-slate-800 dark:text-slate-100" data-i18n="config_channel">Channel Configuration</h3>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0" data-i18n="config_channel_type">Channel Type</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono" id="cfg-channel">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Coming Soon Banner -->
|
||||
<div class="mt-6 p-4 rounded-xl bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800/50 flex items-center gap-3">
|
||||
<i class="fas fa-info-circle text-primary-500"></i>
|
||||
<span class="text-sm text-primary-700 dark:text-primary-300" data-i18n="config_coming_soon">Full editing capability coming soon. Currently displaying read-only configuration.</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,6 +222,121 @@
|
||||
/* Tool failed state */
|
||||
.agent-tool-step.tool-failed .tool-name { color: #f87171; }
|
||||
|
||||
/* Config form controls */
|
||||
#view-config input[type="text"],
|
||||
#view-config input[type="number"],
|
||||
#view-config input[type="password"] {
|
||||
height: 40px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
#view-config input:focus {
|
||||
border-color: #4ABE6E;
|
||||
box-shadow: 0 0 0 3px rgba(74, 190, 110, 0.12);
|
||||
}
|
||||
#view-config input[type="text"]:hover,
|
||||
#view-config input[type="number"]:hover,
|
||||
#view-config input[type="password"]:hover {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
.dark #view-config input[type="text"]:hover,
|
||||
.dark #view-config input[type="number"]:hover,
|
||||
.dark #view-config input[type="password"]:hover {
|
||||
border-color: #64748b;
|
||||
}
|
||||
|
||||
/* Custom dropdown */
|
||||
.cfg-dropdown {
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
.cfg-dropdown-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
font-size: 0.875rem;
|
||||
color: #1e293b;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
.dark .cfg-dropdown-selected {
|
||||
border-color: #475569;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
.cfg-dropdown-selected:hover { border-color: #94a3b8; }
|
||||
.dark .cfg-dropdown-selected:hover { border-color: #64748b; }
|
||||
.cfg-dropdown.open .cfg-dropdown-selected,
|
||||
.cfg-dropdown:focus .cfg-dropdown-selected {
|
||||
border-color: #4ABE6E;
|
||||
box-shadow: 0 0 0 3px rgba(74, 190, 110, 0.12);
|
||||
}
|
||||
.cfg-dropdown-arrow {
|
||||
font-size: 0.625rem;
|
||||
color: #94a3b8;
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.cfg-dropdown.open .cfg-dropdown-arrow { transform: rotate(180deg); }
|
||||
.cfg-dropdown-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
padding: 4px;
|
||||
}
|
||||
.dark .cfg-dropdown-menu {
|
||||
border-color: #334155;
|
||||
background: #1e1e1e;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.cfg-dropdown.open .cfg-dropdown-menu { display: block; }
|
||||
.cfg-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
color: #334155;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.dark .cfg-dropdown-item { color: #cbd5e1; }
|
||||
.cfg-dropdown-item:hover { background: #f1f5f9; }
|
||||
.dark .cfg-dropdown-item:hover { background: rgba(255, 255, 255, 0.08); }
|
||||
.cfg-dropdown-item.active {
|
||||
background: rgba(74, 190, 110, 0.1);
|
||||
color: #228547;
|
||||
font-weight: 500;
|
||||
}
|
||||
.dark .cfg-dropdown-item.active {
|
||||
background: rgba(74, 190, 110, 0.15);
|
||||
color: #74E9A4;
|
||||
}
|
||||
|
||||
/* API Key masking via CSS (avoids browser password prompts) */
|
||||
.cfg-key-masked {
|
||||
-webkit-text-security: disc;
|
||||
text-security: disc;
|
||||
}
|
||||
|
||||
/* Chat Input */
|
||||
#chat-input {
|
||||
resize: none; height: 42px; max-height: 180px;
|
||||
|
||||
@@ -28,7 +28,11 @@ const I18N = {
|
||||
config_agent_enabled: 'Agent 模式', config_max_tokens: '最大 Token',
|
||||
config_max_turns: '最大轮次', config_max_steps: '最大步数',
|
||||
config_channel_type: '通道类型',
|
||||
config_coming_soon: '完整编辑功能即将推出,当前为只读展示。',
|
||||
config_provider: '模型厂商', config_model_name: '模型',
|
||||
config_custom_model_hint: '输入自定义模型名称',
|
||||
config_save: '保存', config_saved: '已保存',
|
||||
config_save_error: '保存失败',
|
||||
config_custom_option: '自定义...',
|
||||
skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 技能',
|
||||
skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处',
|
||||
memory_title: '记忆管理', memory_desc: '查看 Agent 记忆文件和内容',
|
||||
@@ -60,7 +64,11 @@ const I18N = {
|
||||
config_agent_enabled: 'Agent Mode', config_max_tokens: 'Max Tokens',
|
||||
config_max_turns: 'Max Turns', config_max_steps: 'Max Steps',
|
||||
config_channel_type: 'Channel Type',
|
||||
config_coming_soon: 'Full editing capability coming soon. Currently displaying read-only configuration.',
|
||||
config_provider: 'Provider', config_model_name: 'Model',
|
||||
config_custom_model_hint: 'Enter custom model name',
|
||||
config_save: 'Save', config_saved: 'Saved',
|
||||
config_save_error: 'Save failed',
|
||||
config_custom_option: 'Custom...',
|
||||
skills_title: 'Skills', skills_desc: 'View, enable, or disable agent skills',
|
||||
skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading',
|
||||
memory_title: 'Memory', memory_desc: 'View agent memory files and contents',
|
||||
@@ -236,7 +244,7 @@ let isPolling = false;
|
||||
let loadingContainers = {};
|
||||
let activeStreams = {}; // request_id -> EventSource
|
||||
let isComposing = false;
|
||||
let appConfig = { use_agent: false, title: 'CowAgent', subtitle: '' };
|
||||
let appConfig = { use_agent: false, title: 'CowAgent', subtitle: '', providers: {}, api_bases: {} };
|
||||
|
||||
const SESSION_ID_KEY = 'cow_session_id';
|
||||
|
||||
@@ -268,14 +276,8 @@ fetch('/config').then(r => r.json()).then(data => {
|
||||
appConfig = data;
|
||||
const title = data.title || 'CowAgent';
|
||||
document.getElementById('welcome-title').textContent = title;
|
||||
document.getElementById('cfg-model').textContent = data.model || '--';
|
||||
document.getElementById('cfg-agent').textContent = data.use_agent ? 'Enabled' : 'Disabled';
|
||||
document.getElementById('cfg-max-tokens').textContent = data.agent_max_context_tokens || '--';
|
||||
document.getElementById('cfg-max-turns').textContent = data.agent_max_context_turns || '--';
|
||||
document.getElementById('cfg-max-steps').textContent = data.agent_max_steps || '--';
|
||||
document.getElementById('cfg-channel').textContent = data.channel_type || '--';
|
||||
initConfigView(data);
|
||||
}
|
||||
// Load conversation history after config is ready
|
||||
loadHistory(1);
|
||||
}).catch(() => { loadHistory(1); });
|
||||
|
||||
@@ -820,15 +822,312 @@ function applyHighlighting(container) {
|
||||
// =====================================================================
|
||||
// Config View
|
||||
// =====================================================================
|
||||
let configProviders = {};
|
||||
let configApiBases = {};
|
||||
let configApiKeys = {};
|
||||
let configCurrentModel = '';
|
||||
let cfgProviderValue = '';
|
||||
let cfgModelValue = '';
|
||||
|
||||
// --- Custom dropdown helper ---
|
||||
function initDropdown(el, options, selectedValue, onChange) {
|
||||
const textEl = el.querySelector('.cfg-dropdown-text');
|
||||
const menuEl = el.querySelector('.cfg-dropdown-menu');
|
||||
const selEl = el.querySelector('.cfg-dropdown-selected');
|
||||
|
||||
el._ddValue = selectedValue || '';
|
||||
el._ddOnChange = onChange;
|
||||
|
||||
function render() {
|
||||
menuEl.innerHTML = '';
|
||||
options.forEach(opt => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'cfg-dropdown-item' + (opt.value === el._ddValue ? ' active' : '');
|
||||
item.textContent = opt.label;
|
||||
item.dataset.value = opt.value;
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
el._ddValue = opt.value;
|
||||
textEl.textContent = opt.label;
|
||||
menuEl.querySelectorAll('.cfg-dropdown-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
el.classList.remove('open');
|
||||
if (el._ddOnChange) el._ddOnChange(opt.value);
|
||||
});
|
||||
menuEl.appendChild(item);
|
||||
});
|
||||
const sel = options.find(o => o.value === el._ddValue);
|
||||
textEl.textContent = sel ? sel.label : (options[0] ? options[0].label : '--');
|
||||
if (!sel && options[0]) el._ddValue = options[0].value;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
if (!el._ddBound) {
|
||||
selEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.cfg-dropdown.open').forEach(d => { if (d !== el) d.classList.remove('open'); });
|
||||
el.classList.toggle('open');
|
||||
});
|
||||
el._ddBound = true;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.cfg-dropdown.open').forEach(d => d.classList.remove('open'));
|
||||
});
|
||||
|
||||
function getDropdownValue(el) { return el._ddValue || ''; }
|
||||
|
||||
// --- Config init ---
|
||||
function initConfigView(data) {
|
||||
configProviders = data.providers || {};
|
||||
configApiBases = data.api_bases || {};
|
||||
configApiKeys = data.api_keys || {};
|
||||
configCurrentModel = data.model || '';
|
||||
|
||||
const providerEl = document.getElementById('cfg-provider');
|
||||
const providerOpts = Object.entries(configProviders).map(([pid, p]) => ({ value: pid, label: p.label }));
|
||||
|
||||
const detected = detectProvider(configCurrentModel);
|
||||
cfgProviderValue = detected || (providerOpts[0] ? providerOpts[0].value : '');
|
||||
|
||||
initDropdown(providerEl, providerOpts, cfgProviderValue, onProviderChange);
|
||||
|
||||
onProviderChange(cfgProviderValue);
|
||||
syncModelSelection(configCurrentModel);
|
||||
|
||||
document.getElementById('cfg-max-tokens').value = data.agent_max_context_tokens || 50000;
|
||||
document.getElementById('cfg-max-turns').value = data.agent_max_context_turns || 30;
|
||||
document.getElementById('cfg-max-steps').value = data.agent_max_steps || 15;
|
||||
}
|
||||
|
||||
function detectProvider(model) {
|
||||
if (!model) return Object.keys(configProviders)[0] || '';
|
||||
for (const [pid, p] of Object.entries(configProviders)) {
|
||||
if (pid === 'linkai') continue;
|
||||
if (p.models && p.models.includes(model)) return pid;
|
||||
}
|
||||
return Object.keys(configProviders)[0] || '';
|
||||
}
|
||||
|
||||
function onProviderChange(pid) {
|
||||
cfgProviderValue = pid || getDropdownValue(document.getElementById('cfg-provider'));
|
||||
const p = configProviders[cfgProviderValue];
|
||||
if (!p) return;
|
||||
|
||||
const modelEl = document.getElementById('cfg-model-select');
|
||||
const modelOpts = (p.models || []).map(m => ({ value: m, label: m }));
|
||||
modelOpts.push({ value: '__custom__', label: t('config_custom_option') });
|
||||
|
||||
initDropdown(modelEl, modelOpts, modelOpts[0] ? modelOpts[0].value : '', onModelSelectChange);
|
||||
|
||||
// API Key
|
||||
const keyField = p.api_key_field;
|
||||
const keyWrap = document.getElementById('cfg-api-key-wrap');
|
||||
const keyInput = document.getElementById('cfg-api-key');
|
||||
if (keyField) {
|
||||
keyWrap.classList.remove('hidden');
|
||||
keyInput.classList.add('cfg-key-masked');
|
||||
const maskedVal = configApiKeys[keyField] || '';
|
||||
keyInput.value = maskedVal;
|
||||
keyInput.dataset.field = keyField;
|
||||
keyInput.dataset.masked = maskedVal ? '1' : '';
|
||||
keyInput.dataset.maskedVal = maskedVal;
|
||||
const toggleIcon = document.querySelector('#cfg-api-key-toggle i');
|
||||
if (toggleIcon) toggleIcon.className = 'fas fa-eye text-xs';
|
||||
|
||||
if (!keyInput._cfgBound) {
|
||||
keyInput.addEventListener('focus', function() {
|
||||
if (this.dataset.masked === '1') {
|
||||
this.value = '';
|
||||
this.dataset.masked = '';
|
||||
this.classList.remove('cfg-key-masked');
|
||||
}
|
||||
});
|
||||
keyInput.addEventListener('blur', function() {
|
||||
if (!this.value.trim() && this.dataset.maskedVal) {
|
||||
this.value = this.dataset.maskedVal;
|
||||
this.dataset.masked = '1';
|
||||
this.classList.add('cfg-key-masked');
|
||||
}
|
||||
});
|
||||
keyInput.addEventListener('input', function() {
|
||||
this.dataset.masked = '';
|
||||
});
|
||||
keyInput._cfgBound = true;
|
||||
}
|
||||
} else {
|
||||
keyWrap.classList.add('hidden');
|
||||
keyInput.value = '';
|
||||
keyInput.dataset.field = '';
|
||||
}
|
||||
|
||||
// API Base
|
||||
if (p.api_base_key) {
|
||||
document.getElementById('cfg-api-base-wrap').classList.remove('hidden');
|
||||
document.getElementById('cfg-api-base').value = configApiBases[p.api_base_key] || p.api_base_default || '';
|
||||
} else {
|
||||
document.getElementById('cfg-api-base-wrap').classList.add('hidden');
|
||||
document.getElementById('cfg-api-base').value = '';
|
||||
}
|
||||
|
||||
onModelSelectChange(modelOpts[0] ? modelOpts[0].value : '');
|
||||
}
|
||||
|
||||
function onModelSelectChange(val) {
|
||||
cfgModelValue = val || getDropdownValue(document.getElementById('cfg-model-select'));
|
||||
const customWrap = document.getElementById('cfg-model-custom-wrap');
|
||||
if (cfgModelValue === '__custom__') {
|
||||
customWrap.classList.remove('hidden');
|
||||
document.getElementById('cfg-model-custom').focus();
|
||||
} else {
|
||||
customWrap.classList.add('hidden');
|
||||
document.getElementById('cfg-model-custom').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function syncModelSelection(model) {
|
||||
const p = configProviders[cfgProviderValue];
|
||||
if (!p) return;
|
||||
|
||||
const modelEl = document.getElementById('cfg-model-select');
|
||||
if (p.models && p.models.includes(model)) {
|
||||
const modelOpts = (p.models || []).map(m => ({ value: m, label: m }));
|
||||
modelOpts.push({ value: '__custom__', label: t('config_custom_option') });
|
||||
initDropdown(modelEl, modelOpts, model, onModelSelectChange);
|
||||
cfgModelValue = model;
|
||||
document.getElementById('cfg-model-custom-wrap').classList.add('hidden');
|
||||
} else {
|
||||
cfgModelValue = '__custom__';
|
||||
const modelOpts = (p.models || []).map(m => ({ value: m, label: m }));
|
||||
modelOpts.push({ value: '__custom__', label: t('config_custom_option') });
|
||||
initDropdown(modelEl, modelOpts, '__custom__', onModelSelectChange);
|
||||
document.getElementById('cfg-model-custom-wrap').classList.remove('hidden');
|
||||
document.getElementById('cfg-model-custom').value = model;
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedModel() {
|
||||
if (cfgModelValue === '__custom__') {
|
||||
return document.getElementById('cfg-model-custom').value.trim();
|
||||
}
|
||||
return cfgModelValue;
|
||||
}
|
||||
|
||||
function toggleApiKeyVisibility() {
|
||||
const input = document.getElementById('cfg-api-key');
|
||||
const icon = document.querySelector('#cfg-api-key-toggle i');
|
||||
if (input.classList.contains('cfg-key-masked')) {
|
||||
input.classList.remove('cfg-key-masked');
|
||||
icon.className = 'fas fa-eye-slash text-xs';
|
||||
} else {
|
||||
input.classList.add('cfg-key-masked');
|
||||
icon.className = 'fas fa-eye text-xs';
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(elId, msgKey, isError) {
|
||||
const el = document.getElementById(elId);
|
||||
el.textContent = t(msgKey);
|
||||
el.classList.toggle('text-red-500', !!isError);
|
||||
el.classList.toggle('text-primary-500', !isError);
|
||||
el.classList.remove('opacity-0');
|
||||
setTimeout(() => el.classList.add('opacity-0'), 2500);
|
||||
}
|
||||
|
||||
function saveModelConfig() {
|
||||
const model = getSelectedModel();
|
||||
if (!model) return;
|
||||
|
||||
const updates = { model: model };
|
||||
const p = configProviders[cfgProviderValue];
|
||||
updates.use_linkai = (cfgProviderValue === 'linkai');
|
||||
if (p && p.api_base_key) {
|
||||
const base = document.getElementById('cfg-api-base').value.trim();
|
||||
if (base) updates[p.api_base_key] = base;
|
||||
}
|
||||
if (p && p.api_key_field) {
|
||||
const keyInput = document.getElementById('cfg-api-key');
|
||||
const rawVal = keyInput.value.trim();
|
||||
if (rawVal && keyInput.dataset.masked !== '1') {
|
||||
updates[p.api_key_field] = rawVal;
|
||||
}
|
||||
}
|
||||
|
||||
const btn = document.getElementById('cfg-model-save');
|
||||
btn.disabled = true;
|
||||
fetch('/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ updates })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
configCurrentModel = model;
|
||||
if (data.applied) {
|
||||
const keyInput = document.getElementById('cfg-api-key');
|
||||
Object.entries(data.applied).forEach(([k, v]) => {
|
||||
if (k === 'model') return;
|
||||
if (k.includes('api_key')) {
|
||||
const masked = v.length > 8
|
||||
? v.substring(0, 4) + '*'.repeat(v.length - 8) + v.substring(v.length - 4)
|
||||
: v;
|
||||
configApiKeys[k] = masked;
|
||||
if (keyInput.dataset.field === k) {
|
||||
keyInput.value = masked;
|
||||
keyInput.dataset.masked = '1';
|
||||
keyInput.dataset.maskedVal = masked;
|
||||
keyInput.classList.add('cfg-key-masked');
|
||||
const toggleIcon = document.querySelector('#cfg-api-key-toggle i');
|
||||
if (toggleIcon) toggleIcon.className = 'fas fa-eye text-xs';
|
||||
}
|
||||
} else {
|
||||
configApiBases[k] = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
showStatus('cfg-model-status', 'config_saved', false);
|
||||
} else {
|
||||
showStatus('cfg-model-status', 'config_save_error', true);
|
||||
}
|
||||
})
|
||||
.catch(() => showStatus('cfg-model-status', 'config_save_error', true))
|
||||
.finally(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function saveAgentConfig() {
|
||||
const updates = {
|
||||
agent_max_context_tokens: parseInt(document.getElementById('cfg-max-tokens').value) || 50000,
|
||||
agent_max_context_turns: parseInt(document.getElementById('cfg-max-turns').value) || 30,
|
||||
agent_max_steps: parseInt(document.getElementById('cfg-max-steps').value) || 15,
|
||||
};
|
||||
|
||||
const btn = document.getElementById('cfg-agent-save');
|
||||
btn.disabled = true;
|
||||
fetch('/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ updates })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showStatus('cfg-agent-status', 'config_saved', false);
|
||||
} else {
|
||||
showStatus('cfg-agent-status', 'config_save_error', true);
|
||||
}
|
||||
})
|
||||
.catch(() => showStatus('cfg-agent-status', 'config_save_error', true))
|
||||
.finally(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
function loadConfigView() {
|
||||
fetch('/config').then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
document.getElementById('cfg-model').textContent = data.model || '--';
|
||||
document.getElementById('cfg-agent').textContent = data.use_agent ? 'Enabled' : 'Disabled';
|
||||
document.getElementById('cfg-max-tokens').textContent = data.agent_max_context_tokens || '--';
|
||||
document.getElementById('cfg-max-turns').textContent = data.agent_max_context_turns || '--';
|
||||
document.getElementById('cfg-max-steps').textContent = data.agent_max_steps || '--';
|
||||
document.getElementById('cfg-channel').textContent = data.channel_type || '--';
|
||||
appConfig = data;
|
||||
initConfigView(data);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1089,7 +1388,8 @@ navigateTo = function(viewId) {
|
||||
_origNavigateTo(viewId);
|
||||
|
||||
// Lazy-load view data
|
||||
if (viewId === 'skills') loadSkillsView();
|
||||
if (viewId === 'config') loadConfigView();
|
||||
else if (viewId === 'skills') loadSkillsView();
|
||||
else if (viewId === 'memory') {
|
||||
// Always start from the list panel when navigating to memory
|
||||
document.getElementById('memory-panel-viewer').classList.add('hidden');
|
||||
|
||||
@@ -14,6 +14,8 @@ from bridge.context import *
|
||||
from bridge.reply import Reply, ReplyType
|
||||
from channel.chat_channel import ChatChannel, check_prefix
|
||||
from channel.chat_message import ChatMessage
|
||||
from collections import OrderedDict
|
||||
from common import const
|
||||
from common.log import logger
|
||||
from common.singleton import singleton
|
||||
from config import conf
|
||||
@@ -379,16 +381,137 @@ class ChatHandler:
|
||||
|
||||
|
||||
class ConfigHandler:
|
||||
|
||||
_RECOMMENDED_MODELS = [
|
||||
const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING,
|
||||
const.GLM_5, const.GLM_4_7,
|
||||
const.QWEN3_MAX, const.QWEN35_PLUS,
|
||||
const.KIMI_K2_5, const.KIMI_K2,
|
||||
const.DOUBAO_SEED_2_PRO, const.DOUBAO_SEED_2_CODE,
|
||||
const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS, const.CLAUDE_4_5_SONNET,
|
||||
const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE,
|
||||
const.GPT_5, const.GPT_41, const.GPT_4o,
|
||||
const.DEEPSEEK_CHAT, const.DEEPSEEK_REASONER,
|
||||
]
|
||||
|
||||
PROVIDER_MODELS = OrderedDict([
|
||||
("minimax", {
|
||||
"label": "MiniMax",
|
||||
"api_key_field": "minimax_api_key",
|
||||
"api_base_key": None,
|
||||
"api_base_default": None,
|
||||
"models": [const.MINIMAX_M2_5, const.MINIMAX_M2_1, const.MINIMAX_M2_1_LIGHTNING],
|
||||
}),
|
||||
("glm-4", {
|
||||
"label": "智谱AI",
|
||||
"api_key_field": "zhipu_ai_api_key",
|
||||
"api_base_key": "zhipu_ai_api_base",
|
||||
"api_base_default": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"models": [const.GLM_5, const.GLM_4_7],
|
||||
}),
|
||||
("dashscope", {
|
||||
"label": "通义千问",
|
||||
"api_key_field": "dashscope_api_key",
|
||||
"api_base_key": None,
|
||||
"api_base_default": None,
|
||||
"models": [const.QWEN3_MAX, const.QWEN35_PLUS],
|
||||
}),
|
||||
("moonshot", {
|
||||
"label": "Kimi",
|
||||
"api_key_field": "moonshot_api_key",
|
||||
"api_base_key": "moonshot_base_url",
|
||||
"api_base_default": "https://api.moonshot.cn/v1",
|
||||
"models": [const.KIMI_K2_5, const.KIMI_K2],
|
||||
}),
|
||||
("doubao", {
|
||||
"label": "豆包",
|
||||
"api_key_field": "ark_api_key",
|
||||
"api_base_key": "ark_base_url",
|
||||
"api_base_default": "https://ark.cn-beijing.volces.com/api/v3",
|
||||
"models": [const.DOUBAO_SEED_2_PRO, const.DOUBAO_SEED_2_CODE],
|
||||
}),
|
||||
("claudeAPI", {
|
||||
"label": "Claude",
|
||||
"api_key_field": "claude_api_key",
|
||||
"api_base_key": "claude_api_base",
|
||||
"api_base_default": "https://api.anthropic.com/v1",
|
||||
"models": [const.CLAUDE_4_6_SONNET, const.CLAUDE_4_6_OPUS, const.CLAUDE_4_5_SONNET],
|
||||
}),
|
||||
("gemini", {
|
||||
"label": "Gemini",
|
||||
"api_key_field": "gemini_api_key",
|
||||
"api_base_key": "gemini_api_base",
|
||||
"api_base_default": "https://generativelanguage.googleapis.com",
|
||||
"models": [const.GEMINI_31_PRO_PRE, const.GEMINI_3_FLASH_PRE],
|
||||
}),
|
||||
("openAI", {
|
||||
"label": "OpenAI",
|
||||
"api_key_field": "open_ai_api_key",
|
||||
"api_base_key": "open_ai_api_base",
|
||||
"api_base_default": "https://api.openai.com/v1",
|
||||
"models": [const.GPT_5, const.GPT_41, const.GPT_4o],
|
||||
}),
|
||||
("deepseek", {
|
||||
"label": "DeepSeek",
|
||||
"api_key_field": "open_ai_api_key",
|
||||
"api_base_key": None,
|
||||
"api_base_default": None,
|
||||
"models": [const.DEEPSEEK_CHAT, const.DEEPSEEK_REASONER],
|
||||
}),
|
||||
("linkai", {
|
||||
"label": "LinkAI",
|
||||
"api_key_field": "linkai_api_key",
|
||||
"api_base_key": None,
|
||||
"api_base_default": None,
|
||||
"models": _RECOMMENDED_MODELS,
|
||||
}),
|
||||
])
|
||||
|
||||
EDITABLE_KEYS = {
|
||||
"model", "use_linkai",
|
||||
"open_ai_api_base", "claude_api_base", "gemini_api_base",
|
||||
"zhipu_ai_api_base", "moonshot_base_url", "ark_base_url",
|
||||
"open_ai_api_key", "claude_api_key", "gemini_api_key",
|
||||
"zhipu_ai_api_key", "dashscope_api_key", "moonshot_api_key",
|
||||
"ark_api_key", "minimax_api_key", "linkai_api_key",
|
||||
"agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _mask_key(value: str) -> str:
|
||||
"""Mask the middle part of an API key for display."""
|
||||
if not value or len(value) <= 8:
|
||||
return value
|
||||
return value[:4] + "*" * (len(value) - 8) + value[-4:]
|
||||
|
||||
def GET(self):
|
||||
"""Return configuration info for the web console."""
|
||||
"""Return configuration info and provider/model metadata."""
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
local_config = conf()
|
||||
use_agent = local_config.get("agent", False)
|
||||
title = "CowAgent" if use_agent else "AI Assistant"
|
||||
|
||||
if use_agent:
|
||||
title = "CowAgent"
|
||||
else:
|
||||
title = "AI Assistant"
|
||||
api_bases = {}
|
||||
api_keys_masked = {}
|
||||
for pid, pinfo in self.PROVIDER_MODELS.items():
|
||||
base_key = pinfo.get("api_base_key")
|
||||
if base_key:
|
||||
api_bases[base_key] = local_config.get(base_key, pinfo["api_base_default"])
|
||||
key_field = pinfo.get("api_key_field")
|
||||
if key_field and key_field not in api_keys_masked:
|
||||
raw = local_config.get(key_field, "")
|
||||
api_keys_masked[key_field] = self._mask_key(raw) if raw else ""
|
||||
|
||||
providers = {}
|
||||
for pid, p in self.PROVIDER_MODELS.items():
|
||||
providers[pid] = {
|
||||
"label": p["label"],
|
||||
"models": p["models"],
|
||||
"api_base_key": p["api_base_key"],
|
||||
"api_base_default": p["api_base_default"],
|
||||
"api_key_field": p.get("api_key_field"),
|
||||
}
|
||||
|
||||
return json.dumps({
|
||||
"status": "success",
|
||||
@@ -396,14 +519,58 @@ class ConfigHandler:
|
||||
"title": title,
|
||||
"model": local_config.get("model", ""),
|
||||
"channel_type": local_config.get("channel_type", ""),
|
||||
"agent_max_context_tokens": local_config.get("agent_max_context_tokens", ""),
|
||||
"agent_max_context_turns": local_config.get("agent_max_context_turns", ""),
|
||||
"agent_max_steps": local_config.get("agent_max_steps", ""),
|
||||
})
|
||||
"agent_max_context_tokens": local_config.get("agent_max_context_tokens", 50000),
|
||||
"agent_max_context_turns": local_config.get("agent_max_context_turns", 30),
|
||||
"agent_max_steps": local_config.get("agent_max_steps", 15),
|
||||
"api_bases": api_bases,
|
||||
"api_keys": api_keys_masked,
|
||||
"providers": providers,
|
||||
}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting config: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
"""Update configuration values in memory and persist to config.json."""
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
data = json.loads(web.data())
|
||||
updates = data.get("updates", {})
|
||||
if not updates:
|
||||
return json.dumps({"status": "error", "message": "no updates provided"})
|
||||
|
||||
local_config = conf()
|
||||
applied = {}
|
||||
for key, value in updates.items():
|
||||
if key not in self.EDITABLE_KEYS:
|
||||
continue
|
||||
if key in ("agent_max_context_tokens", "agent_max_context_turns", "agent_max_steps"):
|
||||
value = int(value)
|
||||
if key == "use_linkai":
|
||||
value = bool(value)
|
||||
local_config[key] = value
|
||||
applied[key] = value
|
||||
|
||||
if not applied:
|
||||
return json.dumps({"status": "error", "message": "no valid keys to update"})
|
||||
|
||||
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
|
||||
os.path.abspath(__file__)))), "config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
file_cfg = json.load(f)
|
||||
else:
|
||||
file_cfg = {}
|
||||
file_cfg.update(applied)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(file_cfg, f, indent=4, ensure_ascii=False)
|
||||
|
||||
logger.info(f"[WebChannel] Config updated: {list(applied.keys())}")
|
||||
return json.dumps({"status": "success", "applied": applied}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating config: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
def _get_workspace_root():
|
||||
"""Resolve the agent workspace directory."""
|
||||
@@ -411,6 +578,19 @@ def _get_workspace_root():
|
||||
return expand_path(conf().get("agent_workspace", "~/cow"))
|
||||
|
||||
|
||||
class ToolsHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.tools.tool_manager import ToolManager
|
||||
tm = ToolManager()
|
||||
loaded = list(tm.tool_classes.keys())
|
||||
return json.dumps({"status": "success", "tools": loaded}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Tools API error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
class SkillsHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
@@ -426,6 +606,30 @@ class SkillsHandler:
|
||||
logger.error(f"[WebChannel] Skills API error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
def POST(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.skills.service import SkillService
|
||||
from agent.skills.manager import SkillManager
|
||||
body = json.loads(web.data())
|
||||
action = body.get("action")
|
||||
name = body.get("name")
|
||||
if not action or not name:
|
||||
return json.dumps({"status": "error", "message": "action and name are required"})
|
||||
workspace_root = _get_workspace_root()
|
||||
manager = SkillManager(custom_dir=os.path.join(workspace_root, "skills"))
|
||||
service = SkillService(manager)
|
||||
if action == "open":
|
||||
service.open({"name": name})
|
||||
elif action == "close":
|
||||
service.close({"name": name})
|
||||
else:
|
||||
return json.dumps({"status": "error", "message": f"unknown action: {action}"})
|
||||
return json.dumps({"status": "success"}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Skills POST error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
class MemoryHandler:
|
||||
def GET(self):
|
||||
|
||||
Reference in New Issue
Block a user