mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat(i18n): add global language resolution and localize user-facing text
This commit is contained in:
@@ -12,6 +12,7 @@ from agent.protocol.models import LLMRequest, LLMModel
|
|||||||
from agent.protocol.message_utils import sanitize_claude_messages, compress_turn_to_text_only
|
from agent.protocol.message_utils import sanitize_claude_messages, compress_turn_to_text_only
|
||||||
from agent.tools.base_tool import BaseTool, ToolResult
|
from agent.tools.base_tool import BaseTool, ToolResult
|
||||||
from common.log import logger
|
from common.log import logger
|
||||||
|
from common.i18n import t as _t
|
||||||
|
|
||||||
# Optional: repair malformed JSON args from non-strict providers (e.g. unescaped quotes in long content).
|
# Optional: repair malformed JSON args from non-strict providers (e.g. unescaped quotes in long content).
|
||||||
try:
|
try:
|
||||||
@@ -317,7 +318,10 @@ class AgentStreamExecutor:
|
|||||||
|
|
||||||
# Hard stop at 8 failures - abort with critical message
|
# Hard stop at 8 failures - abort with critical message
|
||||||
if same_tool_failures >= 8:
|
if same_tool_failures >= 8:
|
||||||
return True, f"抱歉,我没能完成这个任务。可能是我理解有误或者当前方法不太合适。\n\n建议你:\n• 换个方式描述需求试试\n• 把任务拆分成更小的步骤\n• 或者换个思路来解决", True
|
return True, _t(
|
||||||
|
"抱歉,我没能完成这个任务。可能是我理解有误或者当前方法不太合适。\n\n建议你:\n• 换个方式描述需求试试\n• 把任务拆分成更小的步骤\n• 或者换个思路来解决",
|
||||||
|
"Sorry, I couldn't complete this task. I may have misunderstood, or my current approach isn't quite right.\n\nYou could try:\n• Rephrasing your request\n• Breaking the task into smaller steps\n• Taking a different approach",
|
||||||
|
), True
|
||||||
|
|
||||||
# Warning at 6 failures
|
# Warning at 6 failures
|
||||||
if same_tool_failures >= 6:
|
if same_tool_failures >= 6:
|
||||||
@@ -436,14 +440,16 @@ class AgentStreamExecutor:
|
|||||||
elif not assistant_msg:
|
elif not assistant_msg:
|
||||||
# Still empty (no text and no tool_calls): use fallback
|
# Still empty (no text and no tool_calls): use fallback
|
||||||
logger.warning(f"[Agent] Still empty after explicit request")
|
logger.warning(f"[Agent] Still empty after explicit request")
|
||||||
final_response = (
|
final_response = _t(
|
||||||
"抱歉,我暂时无法生成回复。请尝试换一种方式描述你的需求,或稍后再试。"
|
"抱歉,我暂时无法生成回复。请尝试换一种方式描述你的需求,或稍后再试。",
|
||||||
|
"Sorry, I can't generate a reply right now. Please try rephrasing your request, or try again later.",
|
||||||
)
|
)
|
||||||
logger.info(f"Generated fallback response for empty LLM output")
|
logger.info(f"Generated fallback response for empty LLM output")
|
||||||
else:
|
else:
|
||||||
# 第一轮就空回复,直接 fallback
|
# First-turn empty reply, fall back directly
|
||||||
final_response = (
|
final_response = _t(
|
||||||
"抱歉,我暂时无法生成回复。请尝试换一种方式描述你的需求,或稍后再试。"
|
"抱歉,我暂时无法生成回复。请尝试换一种方式描述你的需求,或稍后再试。",
|
||||||
|
"Sorry, I can't generate a reply right now. Please try rephrasing your request, or try again later.",
|
||||||
)
|
)
|
||||||
logger.info(f"Generated fallback response for empty LLM output")
|
logger.info(f"Generated fallback response for empty LLM output")
|
||||||
else:
|
else:
|
||||||
@@ -514,7 +520,7 @@ class AgentStreamExecutor:
|
|||||||
# Check for critical error - abort entire conversation
|
# Check for critical error - abort entire conversation
|
||||||
if result.get("status") == "critical_error":
|
if result.get("status") == "critical_error":
|
||||||
logger.error(f"💥 检测到严重错误,终止对话")
|
logger.error(f"💥 检测到严重错误,终止对话")
|
||||||
final_response = result.get('result', '任务执行失败')
|
final_response = result.get('result') or _t("任务执行失败", "Task execution failed")
|
||||||
return final_response
|
return final_response
|
||||||
|
|
||||||
# Log tool result in compact format
|
# Log tool result in compact format
|
||||||
@@ -650,15 +656,15 @@ class AgentStreamExecutor:
|
|||||||
logger.info(f"💭 Summary: {summary_response[:150]}{'...' if len(summary_response) > 150 else ''}")
|
logger.info(f"💭 Summary: {summary_response[:150]}{'...' if len(summary_response) > 150 else ''}")
|
||||||
else:
|
else:
|
||||||
# Fallback if model still doesn't respond
|
# Fallback if model still doesn't respond
|
||||||
final_response = (
|
final_response = _t(
|
||||||
f"我已经执行了{turn}个决策步骤,达到了单次运行的步数上限。"
|
f"我已经执行了{turn}个决策步骤,达到了单次运行的步数上限。任务可能还未完全完成,建议你将任务拆分成更小的步骤,或者换一种方式描述需求。",
|
||||||
"任务可能还未完全完成,建议你将任务拆分成更小的步骤,或者换一种方式描述需求。"
|
f"I've taken {turn} decision steps and reached the per-run limit. The task may not be fully complete — try breaking it into smaller steps, or describe your request differently.",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to get summary from LLM: {e}")
|
logger.warning(f"Failed to get summary from LLM: {e}")
|
||||||
final_response = (
|
final_response = _t(
|
||||||
f"我已经执行了{turn}个决策步骤,达到了单次运行的步数上限。"
|
f"我已经执行了{turn}个决策步骤,达到了单次运行的步数上限。任务可能还未完全完成,建议你将任务拆分成更小的步骤,或者换一种方式描述需求。",
|
||||||
"任务可能还未完全完成,建议你将任务拆分成更小的步骤,或者换一种方式描述需求。"
|
f"I've taken {turn} decision steps and reached the per-run limit. The task may not be fully complete — try breaking it into smaller steps, or describe your request differently.",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
# Remove the injected user prompt from history to avoid polluting
|
# Remove the injected user prompt from history to avoid polluting
|
||||||
@@ -953,13 +959,15 @@ class AgentStreamExecutor:
|
|||||||
self.messages.clear()
|
self.messages.clear()
|
||||||
self._clear_session_db()
|
self._clear_session_db()
|
||||||
if is_context_overflow:
|
if is_context_overflow:
|
||||||
raise Exception(
|
raise Exception(_t(
|
||||||
"抱歉,对话历史过长导致上下文溢出。我已清空历史记录,请重新描述你的需求。"
|
"抱歉,对话历史过长导致上下文溢出。我已清空历史记录,请重新描述你的需求。",
|
||||||
)
|
"Sorry, the conversation history got too long and overflowed the context. I've cleared the history — please describe your request again.",
|
||||||
|
))
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
raise Exception(_t(
|
||||||
"抱歉,之前的对话出现了问题。我已清空历史记录,请重新发送你的消息。"
|
"抱歉,之前的对话出现了问题。我已清空历史记录,请重新发送你的消息。",
|
||||||
)
|
"Sorry, something went wrong with the earlier conversation. I've cleared the history — please send your message again.",
|
||||||
|
))
|
||||||
|
|
||||||
# Check if error is rate limit (429)
|
# Check if error is rate limit (429)
|
||||||
is_rate_limit = '429' in error_str_lower or 'rate limit' in error_str_lower
|
is_rate_limit = '429' in error_str_lower or 'rate limit' in error_str_lower
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from bridge.reply import *
|
|||||||
from channel.channel import Channel
|
from channel.channel import Channel
|
||||||
from common.dequeue import Dequeue
|
from common.dequeue import Dequeue
|
||||||
from common import memory
|
from common import memory
|
||||||
|
from common.i18n import t as _t
|
||||||
from plugins import *
|
from plugins import *
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -265,7 +266,7 @@ class ChatChannel(Channel):
|
|||||||
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
if reply.type in self.NOT_SUPPORT_REPLYTYPE:
|
||||||
logger.error("[chat_channel]reply type not support: " + str(reply.type))
|
logger.error("[chat_channel]reply type not support: " + str(reply.type))
|
||||||
reply.type = ReplyType.ERROR
|
reply.type = ReplyType.ERROR
|
||||||
reply.content = "不支持发送的消息类型: " + str(reply.type)
|
reply.content = _t("不支持发送的消息类型: ", "Unsupported message type: ") + str(reply.type)
|
||||||
|
|
||||||
if reply.type == ReplyType.TEXT:
|
if reply.type == ReplyType.TEXT:
|
||||||
reply_text = reply.content
|
reply_text = reply.content
|
||||||
@@ -476,9 +477,9 @@ class ChatChannel(Channel):
|
|||||||
|
|
||||||
cancelled = get_cancel_registry().cancel_session(session_id)
|
cancelled = get_cancel_registry().cancel_session(session_id)
|
||||||
text = (
|
text = (
|
||||||
"🛑 已中止"
|
_t("🛑 已中止", "🛑 Cancelled")
|
||||||
if cancelled > 0
|
if cancelled > 0
|
||||||
else "当前没有可中止的任务。"
|
else _t("当前没有可中止的任务。", "Nothing to cancel.")
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[chat_channel] /cancel fast-path: session={session_id}, cancelled={cancelled}"
|
f"[chat_channel] /cancel fast-path: session={session_id}, cancelled={cancelled}"
|
||||||
|
|||||||
@@ -47,11 +47,30 @@
|
|||||||
This runs synchronously in <head> so the correct class is on <html>
|
This runs synchronously in <head> so the correct class is on <html>
|
||||||
before any CSS or body rendering occurs. -->
|
before any CSS or body rendering occurs. -->
|
||||||
<script>
|
<script>
|
||||||
|
// Map an arbitrary locale string (zh-CN, en-US, fr ...) to 'zh' / 'en',
|
||||||
|
// or '' when unrecognized so callers can fall through to the next source.
|
||||||
|
window.__cowNormalizeLang__ = function(raw) {
|
||||||
|
if (!raw) return '';
|
||||||
|
var v = String(raw).trim().toLowerCase();
|
||||||
|
if (v === 'auto') return '';
|
||||||
|
if (v.indexOf('zh') === 0) return 'zh';
|
||||||
|
if (v.indexOf('en') === 0) return 'en';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
// Resolve the console language by priority:
|
||||||
|
// user choice (localStorage) -> backend-detected -> browser -> 'zh'.
|
||||||
|
window.__cowResolveLang__ = function() {
|
||||||
|
return window.__cowNormalizeLang__(localStorage.getItem('cow_lang'))
|
||||||
|
|| window.__cowNormalizeLang__(window.__COW_DEFAULT_LANG__)
|
||||||
|
|| window.__cowNormalizeLang__(navigator.language || (navigator.languages && navigator.languages[0]))
|
||||||
|
|| 'zh';
|
||||||
|
};
|
||||||
(function() {
|
(function() {
|
||||||
|
// Backend-resolved default language (from cow_lang config / auto-detect).
|
||||||
|
window.__COW_DEFAULT_LANG__ = '{{COW_DEFAULT_LANG}}';
|
||||||
var theme = localStorage.getItem('cow_theme') || 'dark';
|
var theme = localStorage.getItem('cow_theme') || 'dark';
|
||||||
if (theme === 'dark') document.documentElement.classList.add('dark');
|
if (theme === 'dark') document.documentElement.classList.add('dark');
|
||||||
var lang = localStorage.getItem('cow_lang') || 'zh';
|
document.documentElement.setAttribute('lang', window.__cowResolveLang__());
|
||||||
document.documentElement.setAttribute('lang', lang);
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -91,6 +91,27 @@ const I18N = {
|
|||||||
example_knowledge_title: '知识库', example_knowledge_text: '查看知识库当前文档情况',
|
example_knowledge_title: '知识库', example_knowledge_text: '查看知识库当前文档情况',
|
||||||
example_skill_title: '技能系统', example_skill_text: '查看所有支持的工具和技能',
|
example_skill_title: '技能系统', example_skill_text: '查看所有支持的工具和技能',
|
||||||
example_web_title: '指令中心', example_web_text: '查看全部命令',
|
example_web_title: '指令中心', example_web_text: '查看全部命令',
|
||||||
|
slash_help: '显示命令帮助',
|
||||||
|
slash_status: '查看运行状态',
|
||||||
|
slash_context: '查看对话上下文',
|
||||||
|
slash_context_clear: '清除对话上下文',
|
||||||
|
slash_skill_list: '查看已安装技能',
|
||||||
|
slash_skill_list_remote: '浏览技能广场',
|
||||||
|
slash_skill_search: '搜索技能',
|
||||||
|
slash_skill_install: '安装技能 (名称或 GitHub URL)',
|
||||||
|
slash_skill_uninstall: '卸载技能',
|
||||||
|
slash_skill_info: '查看技能详情',
|
||||||
|
slash_skill_enable: '启用技能',
|
||||||
|
slash_skill_disable: '禁用技能',
|
||||||
|
slash_memory_dream: '手动触发记忆蒸馏 (可指定天数, 默认3)',
|
||||||
|
slash_knowledge: '查看知识库统计',
|
||||||
|
slash_knowledge_list: '查看知识库文件树',
|
||||||
|
slash_knowledge_on: '开启知识库',
|
||||||
|
slash_knowledge_off: '关闭知识库',
|
||||||
|
slash_config: '查看当前配置',
|
||||||
|
slash_cancel: '中止当前正在运行的 Agent 任务',
|
||||||
|
slash_logs: '查看最近日志',
|
||||||
|
slash_version: '查看版本',
|
||||||
input_placeholder: '输入消息,或输入 / 使用指令',
|
input_placeholder: '输入消息,或输入 / 使用指令',
|
||||||
config_title: '配置管理', config_desc: '管理模型和 Agent 配置',
|
config_title: '配置管理', config_desc: '管理模型和 Agent 配置',
|
||||||
config_model: '模型配置', config_agent: 'Agent 配置',
|
config_model: '模型配置', config_agent: 'Agent 配置',
|
||||||
@@ -265,6 +286,27 @@ const I18N = {
|
|||||||
example_knowledge_title: 'Knowledge', example_knowledge_text: 'Show me the current knowledge base',
|
example_knowledge_title: 'Knowledge', example_knowledge_text: 'Show me the current knowledge base',
|
||||||
example_skill_title: 'Skills', example_skill_text: 'Show current tools and skills',
|
example_skill_title: 'Skills', example_skill_text: 'Show current tools and skills',
|
||||||
example_web_title: 'Commands', example_web_text: 'Show all commands',
|
example_web_title: 'Commands', example_web_text: 'Show all commands',
|
||||||
|
slash_help: 'Show this help',
|
||||||
|
slash_status: 'Show running status',
|
||||||
|
slash_context: 'Show conversation context',
|
||||||
|
slash_context_clear: 'Clear conversation context',
|
||||||
|
slash_skill_list: 'List installed skills',
|
||||||
|
slash_skill_list_remote: 'Browse Skill Hub',
|
||||||
|
slash_skill_search: 'Search skills',
|
||||||
|
slash_skill_install: 'Install a skill (name or GitHub URL)',
|
||||||
|
slash_skill_uninstall: 'Uninstall a skill',
|
||||||
|
slash_skill_info: 'Show skill details',
|
||||||
|
slash_skill_enable: 'Enable a skill',
|
||||||
|
slash_skill_disable: 'Disable a skill',
|
||||||
|
slash_memory_dream: 'Trigger memory distillation (optional days, default 3)',
|
||||||
|
slash_knowledge: 'Show knowledge base stats',
|
||||||
|
slash_knowledge_list: 'Show knowledge base file tree',
|
||||||
|
slash_knowledge_on: 'Enable knowledge base',
|
||||||
|
slash_knowledge_off: 'Disable knowledge base',
|
||||||
|
slash_config: 'Show current config',
|
||||||
|
slash_cancel: 'Abort the running Agent task',
|
||||||
|
slash_logs: 'Show recent logs',
|
||||||
|
slash_version: 'Show version',
|
||||||
input_placeholder: 'Type a message, or press / for commands',
|
input_placeholder: 'Type a message, or press / for commands',
|
||||||
config_title: 'Configuration', config_desc: 'Manage model and agent settings',
|
config_title: 'Configuration', config_desc: 'Manage model and agent settings',
|
||||||
config_model: 'Model Configuration', config_agent: 'Agent Configuration',
|
config_model: 'Model Configuration', config_agent: 'Agent Configuration',
|
||||||
@@ -361,7 +403,25 @@ const I18N = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentLang = localStorage.getItem('cow_lang') || 'zh';
|
// Resolve language by priority: user choice (localStorage) -> backend-detected
|
||||||
|
// (cow_lang) -> browser language -> 'zh'. Shares __cowResolveLang__ defined in
|
||||||
|
// chat.html; falls back to a local resolver if loaded standalone.
|
||||||
|
let currentLang = (typeof window.__cowResolveLang__ === 'function')
|
||||||
|
? window.__cowResolveLang__()
|
||||||
|
: (function () {
|
||||||
|
const norm = (raw) => {
|
||||||
|
if (!raw) return '';
|
||||||
|
const v = String(raw).trim().toLowerCase();
|
||||||
|
if (v === 'auto') return '';
|
||||||
|
if (v.indexOf('zh') === 0) return 'zh';
|
||||||
|
if (v.indexOf('en') === 0) return 'en';
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
return norm(localStorage.getItem('cow_lang'))
|
||||||
|
|| norm(window.__COW_DEFAULT_LANG__)
|
||||||
|
|| norm(navigator.language)
|
||||||
|
|| 'zh';
|
||||||
|
})();
|
||||||
|
|
||||||
function t(key) {
|
function t(key) {
|
||||||
return (I18N[currentLang] && I18N[currentLang][key]) || (I18N.en[key]) || key;
|
return (I18N[currentLang] && I18N[currentLang][key]) || (I18N.en[key]) || key;
|
||||||
@@ -1298,28 +1358,30 @@ chatInput.addEventListener('compositionstart', () => { isComposing = true; });
|
|||||||
chatInput.addEventListener('compositionend', () => { setTimeout(() => { isComposing = false; }, 100); });
|
chatInput.addEventListener('compositionend', () => { setTimeout(() => { isComposing = false; }, 100); });
|
||||||
|
|
||||||
// ── Slash Command Menu ───────────────────────────────────────
|
// ── Slash Command Menu ───────────────────────────────────────
|
||||||
|
// desc holds an i18n key, resolved via t() at render time so the menu follows
|
||||||
|
// the current UI language.
|
||||||
const SLASH_COMMANDS = [
|
const SLASH_COMMANDS = [
|
||||||
{ cmd: '/help', desc: '显示命令帮助' },
|
{ cmd: '/help', desc: 'slash_help' },
|
||||||
{ cmd: '/status', desc: '查看运行状态' },
|
{ cmd: '/status', desc: 'slash_status' },
|
||||||
{ cmd: '/context', desc: '查看对话上下文' },
|
{ cmd: '/context', desc: 'slash_context' },
|
||||||
{ cmd: '/context clear', desc: '清除对话上下文' },
|
{ cmd: '/context clear', desc: 'slash_context_clear' },
|
||||||
{ cmd: '/skill list', desc: '查看已安装技能' },
|
{ cmd: '/skill list', desc: 'slash_skill_list' },
|
||||||
{ cmd: '/skill list --remote', desc: '浏览技能广场' },
|
{ cmd: '/skill list --remote', desc: 'slash_skill_list_remote' },
|
||||||
{ cmd: '/skill search ', desc: '搜索技能' },
|
{ cmd: '/skill search ', desc: 'slash_skill_search' },
|
||||||
{ cmd: '/skill install ', desc: '安装技能 (名称或 GitHub URL)' },
|
{ cmd: '/skill install ', desc: 'slash_skill_install' },
|
||||||
{ cmd: '/skill uninstall ', desc: '卸载技能' },
|
{ cmd: '/skill uninstall ', desc: 'slash_skill_uninstall' },
|
||||||
{ cmd: '/skill info ', desc: '查看技能详情' },
|
{ cmd: '/skill info ', desc: 'slash_skill_info' },
|
||||||
{ cmd: '/skill enable ', desc: '启用技能' },
|
{ cmd: '/skill enable ', desc: 'slash_skill_enable' },
|
||||||
{ cmd: '/skill disable ', desc: '禁用技能' },
|
{ cmd: '/skill disable ', desc: 'slash_skill_disable' },
|
||||||
{ cmd: '/memory dream ', desc: '手动触发记忆蒸馏 (可指定天数, 默认3)' },
|
{ cmd: '/memory dream ', desc: 'slash_memory_dream' },
|
||||||
{ cmd: '/knowledge', desc: '查看知识库统计' },
|
{ cmd: '/knowledge', desc: 'slash_knowledge' },
|
||||||
{ cmd: '/knowledge list', desc: '查看知识库文件树' },
|
{ cmd: '/knowledge list', desc: 'slash_knowledge_list' },
|
||||||
{ cmd: '/knowledge on', desc: '开启知识库' },
|
{ cmd: '/knowledge on', desc: 'slash_knowledge_on' },
|
||||||
{ cmd: '/knowledge off', desc: '关闭知识库' },
|
{ cmd: '/knowledge off', desc: 'slash_knowledge_off' },
|
||||||
{ cmd: '/config', desc: '查看当前配置' },
|
{ cmd: '/config', desc: 'slash_config' },
|
||||||
{ cmd: '/cancel', desc: '中止当前正在运行的 Agent 任务' },
|
{ cmd: '/cancel', desc: 'slash_cancel' },
|
||||||
{ cmd: '/logs', desc: '查看最近日志' },
|
{ cmd: '/logs', desc: 'slash_logs' },
|
||||||
{ cmd: '/version', desc: '查看版本' },
|
{ cmd: '/version', desc: 'slash_version' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const slashMenu = document.getElementById('slash-menu');
|
const slashMenu = document.getElementById('slash-menu');
|
||||||
@@ -1373,7 +1435,7 @@ function renderSlashItems() {
|
|||||||
slashFiltered.map((c, i) =>
|
slashFiltered.map((c, i) =>
|
||||||
`<div class="slash-menu-item${i === slashActiveIdx ? ' active' : ''}" data-idx="${i}">` +
|
`<div class="slash-menu-item${i === slashActiveIdx ? ' active' : ''}" data-idx="${i}">` +
|
||||||
`<span class="cmd">${escapeHtml(c.cmd)}</span>` +
|
`<span class="cmd">${escapeHtml(c.cmd)}</span>` +
|
||||||
`<span class="desc">${escapeHtml(c.desc)}</span></div>`
|
`<span class="desc">${escapeHtml(t(c.desc))}</span></div>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const activeEl = slashMenu.querySelector('.slash-menu-item.active');
|
const activeEl = slashMenu.querySelector('.slash-menu-item.active');
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from channel.chat_channel import ChatChannel, check_prefix
|
|||||||
from channel.chat_message import ChatMessage
|
from channel.chat_message import ChatMessage
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from common import const
|
from common import const
|
||||||
|
from common import i18n
|
||||||
from common.log import logger
|
from common.log import logger
|
||||||
from common.singleton import singleton
|
from common.singleton import singleton
|
||||||
from config import conf
|
from config import conf
|
||||||
@@ -98,7 +99,7 @@ def _require_auth():
|
|||||||
def _cancel_reply_text(cancelled: int, lang: str) -> str:
|
def _cancel_reply_text(cancelled: int, lang: str) -> str:
|
||||||
en = lang.startswith("en")
|
en = lang.startswith("en")
|
||||||
if cancelled > 0:
|
if cancelled > 0:
|
||||||
return "🛑 Cancelled." if en else "🛑 已中止"
|
return "🛑 Cancelled" if en else "🛑 已中止"
|
||||||
return "Nothing to cancel." if en else "当前没有可中止的任务。"
|
return "Nothing to cancel." if en else "当前没有可中止的任务。"
|
||||||
|
|
||||||
|
|
||||||
@@ -477,7 +478,10 @@ class WebChannel(ChatChannel):
|
|||||||
)
|
)
|
||||||
q.put({
|
q.put({
|
||||||
"type": "done",
|
"type": "done",
|
||||||
"content": "(模型未返回任何内容,请重试或换一种方式描述你的需求)",
|
"content": i18n.t(
|
||||||
|
"(模型未返回任何内容,请重试或换一种方式描述你的需求)",
|
||||||
|
"(The model returned no content. Please retry or rephrase your request.)",
|
||||||
|
),
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
})
|
})
|
||||||
@@ -805,13 +809,13 @@ class WebChannel(ChatChannel):
|
|||||||
if not fpath:
|
if not fpath:
|
||||||
continue
|
continue
|
||||||
if ftype == "image":
|
if ftype == "image":
|
||||||
file_refs.append(f"[图片: {fpath}]")
|
file_refs.append(f"[{i18n.t('图片', 'Image')}: {fpath}]")
|
||||||
elif ftype == "video":
|
elif ftype == "video":
|
||||||
file_refs.append(f"[视频: {fpath}]")
|
file_refs.append(f"[{i18n.t('视频', 'Video')}: {fpath}]")
|
||||||
elif ftype == "directory":
|
elif ftype == "directory":
|
||||||
file_refs.append(f"[目录: {fpath}]")
|
file_refs.append(f"[{i18n.t('目录', 'Directory')}: {fpath}]")
|
||||||
else:
|
else:
|
||||||
file_refs.append(f"[文件: {fpath}]")
|
file_refs.append(f"[{i18n.t('文件', 'File')}: {fpath}]")
|
||||||
if file_refs:
|
if file_refs:
|
||||||
prompt = prompt + "\n" + "\n".join(file_refs)
|
prompt = prompt + "\n" + "\n".join(file_refs)
|
||||||
logger.info(f"[WebChannel] Attached {len(file_refs)} file(s) to message")
|
logger.info(f"[WebChannel] Attached {len(file_refs)} file(s) to message")
|
||||||
@@ -952,7 +956,7 @@ class WebChannel(ChatChannel):
|
|||||||
if request_id and request_id in self.sse_queues:
|
if request_id and request_id in self.sse_queues:
|
||||||
self.sse_queues[request_id].put({
|
self.sse_queues[request_id].put({
|
||||||
"type": "cancelled",
|
"type": "cancelled",
|
||||||
"content": "Cancelled" if lang.startswith("en") else "已中止",
|
"content": "🛑 Cancelled" if lang.startswith("en") else "🛑 已中止",
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
})
|
})
|
||||||
@@ -1008,7 +1012,10 @@ class WebChannel(ChatChannel):
|
|||||||
"""Serve the chat HTML page."""
|
"""Serve the chat HTML page."""
|
||||||
file_path = os.path.join(os.path.dirname(__file__), 'chat.html') # 使用绝对路径
|
file_path = os.path.join(os.path.dirname(__file__), 'chat.html') # 使用绝对路径
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
return f.read()
|
html = f.read()
|
||||||
|
# Inject the backend-resolved default language so the console can use
|
||||||
|
# it on first load (when the user has no saved cow_lang preference).
|
||||||
|
return html.replace("{{COW_DEFAULT_LANG}}", i18n.get_language())
|
||||||
|
|
||||||
def startup(self):
|
def startup(self):
|
||||||
configured_host = conf().get("web_host", "")
|
configured_host = conf().get("web_host", "")
|
||||||
@@ -1388,6 +1395,8 @@ class ChatHandler:
|
|||||||
cache_bust = str(int(time.time()))
|
cache_bust = str(int(time.time()))
|
||||||
html = html.replace('assets/js/console.js', f'assets/js/console.js?v={cache_bust}')
|
html = html.replace('assets/js/console.js', f'assets/js/console.js?v={cache_bust}')
|
||||||
html = html.replace('assets/css/console.css', f'assets/css/console.css?v={cache_bust}')
|
html = html.replace('assets/css/console.css', f'assets/css/console.css?v={cache_bust}')
|
||||||
|
# Inject the backend-resolved default language for first-load fallback.
|
||||||
|
html = html.replace("{{COW_DEFAULT_LANG}}", i18n.get_language())
|
||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ CHINA_MIRROR = "https://registry.npmmirror.com/-/binary/playwright"
|
|||||||
|
|
||||||
# stream(msg, fg=None) — fg is "yellow" | "green" | "red" | None
|
# stream(msg, fg=None) — fg is "yellow" | "green" | "red" | None
|
||||||
StreamFn = Callable[[str, Optional[str]], None]
|
StreamFn = Callable[[str, Optional[str]], None]
|
||||||
# on_phase(msg) — coarse-grained progress for chat channels (Chinese)
|
# on_phase(msg) — coarse-grained progress for chat channels (localized via i18n)
|
||||||
PhaseFn = Callable[[str], None]
|
PhaseFn = Callable[[str], None]
|
||||||
|
|
||||||
|
|
||||||
@@ -112,16 +112,25 @@ def run_install_browser(
|
|||||||
stream: Optional callback ``(message, fg)`` for each line. ``fg`` is
|
stream: Optional callback ``(message, fg)`` for each line. ``fg`` is
|
||||||
``yellow`` / ``green`` / ``red`` or None. Defaults to colored click output.
|
``yellow`` / ``green`` / ``red`` or None. Defaults to colored click output.
|
||||||
on_phase: Optional callback for coarse progress (e.g. push to chat);
|
on_phase: Optional callback for coarse progress (e.g. push to chat);
|
||||||
messages are short Chinese status lines.
|
messages are short status lines localized via i18n.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
0 on success, 1 on fatal failure (pip or chromium install failed).
|
0 on success, 1 on fatal failure (pip or chromium install failed).
|
||||||
"""
|
"""
|
||||||
|
from cli.utils import get_cli_language
|
||||||
|
from common import i18n
|
||||||
|
|
||||||
|
get_cli_language() # resolve cow_lang so i18n.t reflects config
|
||||||
|
_t = i18n.t
|
||||||
|
|
||||||
stream = stream or _default_stream
|
stream = stream or _default_stream
|
||||||
python = sys.executable
|
python = sys.executable
|
||||||
legacy_mode = False
|
legacy_mode = False
|
||||||
|
|
||||||
_phase(on_phase, "🔧 开始安装浏览器工具依赖(约几分钟,请耐心等待)…")
|
_phase(on_phase, _t(
|
||||||
|
"🔧 开始安装浏览器工具依赖(约几分钟,请耐心等待)…",
|
||||||
|
"🔧 Installing browser tool dependencies (a few minutes, please wait)…",
|
||||||
|
))
|
||||||
|
|
||||||
glibc = _get_glibc_version()
|
glibc = _get_glibc_version()
|
||||||
if glibc and glibc < GLIBC_THRESHOLD:
|
if glibc and glibc < GLIBC_THRESHOLD:
|
||||||
@@ -136,27 +145,36 @@ def run_install_browser(
|
|||||||
stream("")
|
stream("")
|
||||||
_phase(
|
_phase(
|
||||||
on_phase,
|
on_phase,
|
||||||
|
_t(
|
||||||
f"ℹ️ 检测到 glibc {glibc_str}(较旧),将安装兼容版 Playwright {PLAYWRIGHT_LEGACY_VERSION}。",
|
f"ℹ️ 检测到 glibc {glibc_str}(较旧),将安装兼容版 Playwright {PLAYWRIGHT_LEGACY_VERSION}。",
|
||||||
|
f"ℹ️ Detected glibc {glibc_str} (older); installing compatible Playwright {PLAYWRIGHT_LEGACY_VERSION}.",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
target_version = PLAYWRIGHT_LEGACY_VERSION if legacy_mode else PLAYWRIGHT_VERSION
|
target_version = PLAYWRIGHT_LEGACY_VERSION if legacy_mode else PLAYWRIGHT_VERSION
|
||||||
|
|
||||||
_phase(on_phase, "📦 [1/3] 正在安装 Playwright Python 包…")
|
_phase(on_phase, _t("📦 [1/3] 正在安装 Playwright Python 包…", "📦 [1/3] Installing Playwright Python package…"))
|
||||||
stream("[1/3] Installing playwright Python package...", "yellow")
|
stream("[1/3] Installing playwright Python package...", "yellow")
|
||||||
ret = _pip_install(f"playwright=={target_version}", stream)
|
ret = _pip_install(f"playwright=={target_version}", stream)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
stream("Failed to install playwright package.", "red")
|
stream("Failed to install playwright package.", "red")
|
||||||
_phase(on_phase, "❌ [1/3] Playwright Python 包安装失败。")
|
_phase(on_phase, _t("❌ [1/3] Playwright Python 包安装失败。", "❌ [1/3] Failed to install Playwright Python package."))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
installed = _get_installed_version()
|
installed = _get_installed_version()
|
||||||
if installed:
|
if installed:
|
||||||
stream(f" playwright {installed} installed.", "green")
|
stream(f" playwright {installed} installed.", "green")
|
||||||
stream("")
|
stream("")
|
||||||
_phase(on_phase, f"✅ [1/3] Playwright 包已安装({installed or target_version})。")
|
_phase(on_phase, _t(
|
||||||
|
f"✅ [1/3] Playwright 包已安装({installed or target_version})。",
|
||||||
|
f"✅ [1/3] Playwright package installed ({installed or target_version}).",
|
||||||
|
))
|
||||||
|
|
||||||
if sys.platform == "linux":
|
if sys.platform == "linux":
|
||||||
_phase(on_phase, "🔧 [2/3] 正在安装 Linux 系统依赖与轻量中文字体(文泉驿正黑,部分步骤可能需要 sudo)…")
|
_phase(on_phase, _t(
|
||||||
|
"🔧 [2/3] 正在安装 Linux 系统依赖与轻量中文字体(文泉驿正黑,部分步骤可能需要 sudo)…",
|
||||||
|
"🔧 [2/3] Installing Linux system deps and a lightweight CJK font (WenQuanYi Zen Hei; some steps may need sudo)…",
|
||||||
|
))
|
||||||
stream("[2/3] Installing system dependencies (Linux)...", "yellow")
|
stream("[2/3] Installing system dependencies (Linux)...", "yellow")
|
||||||
ret = subprocess.call([python, "-m", "playwright", "install-deps", "chromium"])
|
ret = subprocess.call([python, "-m", "playwright", "install-deps", "chromium"])
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
@@ -183,14 +201,23 @@ def run_install_browser(
|
|||||||
stream(" CJK font (wqy-zenhei) installed.", "green")
|
stream(" CJK font (wqy-zenhei) installed.", "green")
|
||||||
_phase(
|
_phase(
|
||||||
on_phase,
|
on_phase,
|
||||||
|
_t(
|
||||||
"✅ [2/3] Linux 依赖与字体步骤已执行(若有权限问题请查看服务器日志或手动执行提示命令)。",
|
"✅ [2/3] Linux 依赖与字体步骤已执行(若有权限问题请查看服务器日志或手动执行提示命令)。",
|
||||||
|
"✅ [2/3] Linux deps and font steps executed (on permission issues, check the server log or run the suggested commands manually).",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
stream(f"[2/3] Skipping system deps (not needed on {sys.platform}).", "yellow")
|
stream(f"[2/3] Skipping system deps (not needed on {sys.platform}).", "yellow")
|
||||||
_phase(on_phase, f"ℹ️ [2/3] 当前系统({sys.platform})跳过 Linux 专用依赖。")
|
_phase(on_phase, _t(
|
||||||
|
f"ℹ️ [2/3] 当前系统({sys.platform})跳过 Linux 专用依赖。",
|
||||||
|
f"ℹ️ [2/3] Skipping Linux-specific deps on this platform ({sys.platform}).",
|
||||||
|
))
|
||||||
stream("")
|
stream("")
|
||||||
|
|
||||||
_phase(on_phase, "🌐 [3/3] 正在下载并安装 Chromium(体积较大,请耐心等待)…")
|
_phase(on_phase, _t(
|
||||||
|
"🌐 [3/3] 正在下载并安装 Chromium(体积较大,请耐心等待)…",
|
||||||
|
"🌐 [3/3] Downloading and installing Chromium (large download, please wait)…",
|
||||||
|
))
|
||||||
stream("[3/3] Installing Chromium browser...", "yellow")
|
stream("[3/3] Installing Chromium browser...", "yellow")
|
||||||
cmd = [python, "-m", "playwright", "install", "chromium"]
|
cmd = [python, "-m", "playwright", "install", "chromium"]
|
||||||
|
|
||||||
@@ -209,27 +236,33 @@ def run_install_browser(
|
|||||||
if use_mirror:
|
if use_mirror:
|
||||||
env["PLAYWRIGHT_DOWNLOAD_HOST"] = CHINA_MIRROR
|
env["PLAYWRIGHT_DOWNLOAD_HOST"] = CHINA_MIRROR
|
||||||
stream(f" (using China mirror: {CHINA_MIRROR})", None)
|
stream(f" (using China mirror: {CHINA_MIRROR})", None)
|
||||||
_phase(on_phase, "📡 检测到国内 pip 源配置,Chromium 将优先走国内镜像下载。")
|
_phase(on_phase, _t(
|
||||||
|
"📡 检测到国内 pip 源配置,Chromium 将优先走国内镜像下载。",
|
||||||
|
"📡 Detected a China pip mirror; Chromium will be downloaded from the China mirror first.",
|
||||||
|
))
|
||||||
|
|
||||||
ret = subprocess.call(cmd, env=env)
|
ret = subprocess.call(cmd, env=env)
|
||||||
|
|
||||||
if ret != 0 and use_mirror:
|
if ret != 0 and use_mirror:
|
||||||
stream(" Mirror download failed, retrying with official CDN...", "yellow")
|
stream(" Mirror download failed, retrying with official CDN...", "yellow")
|
||||||
_phase(on_phase, "⚠️ 镜像下载失败,正在改用官方源重试…")
|
_phase(on_phase, _t(
|
||||||
|
"⚠️ 镜像下载失败,正在改用官方源重试…",
|
||||||
|
"⚠️ Mirror download failed; retrying with the official CDN…",
|
||||||
|
))
|
||||||
env_no_mirror = os.environ.copy()
|
env_no_mirror = os.environ.copy()
|
||||||
env_no_mirror.pop("PLAYWRIGHT_DOWNLOAD_HOST", None)
|
env_no_mirror.pop("PLAYWRIGHT_DOWNLOAD_HOST", None)
|
||||||
ret = subprocess.call(cmd, env=env_no_mirror)
|
ret = subprocess.call(cmd, env=env_no_mirror)
|
||||||
|
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
stream("Failed to install Chromium.", "red")
|
stream("Failed to install Chromium.", "red")
|
||||||
_phase(on_phase, "❌ [3/3] Chromium 安装失败。")
|
_phase(on_phase, _t("❌ [3/3] Chromium 安装失败。", "❌ [3/3] Failed to install Chromium."))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
stream("")
|
stream("")
|
||||||
_phase(on_phase, "✅ [3/3] Chromium 已安装。")
|
_phase(on_phase, _t("✅ [3/3] Chromium 已安装。", "✅ [3/3] Chromium installed."))
|
||||||
|
|
||||||
stream("Verifying browser installation...", None)
|
stream("Verifying browser installation...", None)
|
||||||
_phase(on_phase, "🔍 正在验证 Playwright 能否正常加载…")
|
_phase(on_phase, _t("🔍 正在验证 Playwright 能否正常加载…", "🔍 Verifying that Playwright loads correctly…"))
|
||||||
ret = subprocess.call(
|
ret = subprocess.call(
|
||||||
[python, "-c", "from playwright.sync_api import sync_playwright; print('OK')"],
|
[python, "-c", "from playwright.sync_api import sync_playwright; print('OK')"],
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
@@ -240,14 +273,20 @@ def run_install_browser(
|
|||||||
" Consider upgrading your OS or using Docker.",
|
" Consider upgrading your OS or using Docker.",
|
||||||
"yellow",
|
"yellow",
|
||||||
)
|
)
|
||||||
_phase(on_phase, "⚠️ 验证未完全通过:本机可能仍无法使用浏览器工具,请查看日志或升级系统。")
|
_phase(on_phase, _t(
|
||||||
|
"⚠️ 验证未完全通过:本机可能仍无法使用浏览器工具,请查看日志或升级系统。",
|
||||||
|
"⚠️ Verification did not fully pass: the browser tool may still not work here; check the log or upgrade your system.",
|
||||||
|
))
|
||||||
else:
|
else:
|
||||||
stream(" Verification passed.", "green")
|
stream(" Verification passed.", "green")
|
||||||
_phase(on_phase, "✅ 验证通过。")
|
_phase(on_phase, _t("✅ 验证通过。", "✅ Verification passed."))
|
||||||
|
|
||||||
stream("")
|
stream("")
|
||||||
stream("Browser tool ready! Restart CowAgent to enable it.", "green")
|
stream("Browser tool ready! Restart CowAgent to enable it.", "green")
|
||||||
_phase(on_phase, "🎉 全部步骤结束。请重启 CowAgent 后使用 browser 工具。")
|
_phase(on_phase, _t(
|
||||||
|
"🎉 全部步骤结束。请重启 CowAgent 后使用 browser 工具。",
|
||||||
|
"🎉 All steps finished. Restart CowAgent to use the browser tool.",
|
||||||
|
))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,11 @@ def update(ctx):
|
|||||||
def status():
|
def status():
|
||||||
"""Show CowAgent running status."""
|
"""Show CowAgent running status."""
|
||||||
from cli import __version__
|
from cli import __version__
|
||||||
from cli.utils import load_config_json
|
from cli.utils import load_config_json, get_cli_language
|
||||||
|
from common import i18n
|
||||||
|
|
||||||
|
get_cli_language() # resolve cow_lang so i18n.t reflects config
|
||||||
|
_t = i18n.t
|
||||||
|
|
||||||
pid = _read_pid()
|
pid = _read_pid()
|
||||||
if pid:
|
if pid:
|
||||||
@@ -283,17 +287,17 @@ def status():
|
|||||||
else:
|
else:
|
||||||
click.echo(click.style("● CowAgent is not running", fg="red"))
|
click.echo(click.style("● CowAgent is not running", fg="red"))
|
||||||
|
|
||||||
click.echo(f" 版本: v{__version__}")
|
click.echo(_t(f" 版本: v{__version__}", f" Version: v{__version__}"))
|
||||||
|
|
||||||
cfg = load_config_json()
|
cfg = load_config_json()
|
||||||
if cfg:
|
if cfg:
|
||||||
channel = cfg.get("channel_type", "unknown")
|
channel = cfg.get("channel_type", "unknown")
|
||||||
if isinstance(channel, list):
|
if isinstance(channel, list):
|
||||||
channel = ", ".join(channel)
|
channel = ", ".join(channel)
|
||||||
click.echo(f" 通道: {channel}")
|
click.echo(_t(f" 通道: {channel}", f" Channel: {channel}"))
|
||||||
click.echo(f" 模型: {cfg.get('model', 'unknown')}")
|
click.echo(_t(f" 模型: {cfg.get('model', 'unknown')}", f" Model: {cfg.get('model', 'unknown')}"))
|
||||||
mode = "Chat" if cfg.get("agent") is False else "Agent"
|
mode = "Chat" if cfg.get("agent") is False else "Agent"
|
||||||
click.echo(f" 模式: {mode}")
|
click.echo(_t(f" 模式: {mode}", f" Mode: {mode}"))
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
|||||||
@@ -517,18 +517,24 @@ def _install_targz_bytes(content: bytes, name: str, skills_dir: str, result: Ins
|
|||||||
|
|
||||||
def _print_install_success(name: str, source: str):
|
def _print_install_success(name: str, source: str):
|
||||||
"""Print a unified install success message with description and source."""
|
"""Print a unified install success message with description and source."""
|
||||||
|
from cli.utils import get_cli_language
|
||||||
|
from common import i18n
|
||||||
|
|
||||||
|
get_cli_language() # resolve cow_lang so i18n.t reflects config
|
||||||
|
_t = i18n.t
|
||||||
|
|
||||||
skills_dir = get_skills_dir()
|
skills_dir = get_skills_dir()
|
||||||
config = load_skills_config()
|
config = load_skills_config()
|
||||||
display = config.get(name, {}).get("display_name", "")
|
display = config.get(name, {}).get("display_name", "")
|
||||||
desc = _read_skill_description(os.path.join(skills_dir, name))
|
desc = _read_skill_description(os.path.join(skills_dir, name))
|
||||||
click.echo(click.style(f"✓ {name}", fg="green"))
|
click.echo(click.style(f"✓ {name}", fg="green"))
|
||||||
if display and display != name:
|
if display and display != name:
|
||||||
click.echo(f" 名称: {display}")
|
click.echo(_t(f" 名称: {display}", f" Name: {display}"))
|
||||||
if desc:
|
if desc:
|
||||||
if len(desc) > 60:
|
if len(desc) > 60:
|
||||||
desc = desc[:57] + "…"
|
desc = desc[:57] + "…"
|
||||||
click.echo(f" 描述: {desc}")
|
click.echo(_t(f" 描述: {desc}", f" Description: {desc}"))
|
||||||
click.echo(f" 来源: {source}")
|
click.echo(_t(f" 来源: {source}", f" Source: {source}"))
|
||||||
|
|
||||||
|
|
||||||
def _validate_skill_name(name: str):
|
def _validate_skill_name(name: str):
|
||||||
|
|||||||
16
cli/utils.py
16
cli/utils.py
@@ -40,6 +40,22 @@ def load_config_json() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cli_language() -> str:
|
||||||
|
"""Resolve the CLI UI language using the shared i18n detector.
|
||||||
|
|
||||||
|
Reads the `cow_lang` field from config.json (defaults to "auto") and runs
|
||||||
|
the same detection used by the running app, so CLI output matches.
|
||||||
|
"""
|
||||||
|
ensure_sys_path()
|
||||||
|
try:
|
||||||
|
from common import i18n
|
||||||
|
|
||||||
|
configured = load_config_json().get("cow_lang", "auto")
|
||||||
|
return i18n.resolve_language(configured)
|
||||||
|
except Exception:
|
||||||
|
return "en"
|
||||||
|
|
||||||
|
|
||||||
def load_skills_config() -> dict:
|
def load_skills_config() -> dict:
|
||||||
"""Load skills_config.json from the custom skills directory."""
|
"""Load skills_config.json from the custom skills directory."""
|
||||||
path = os.path.join(get_skills_dir(), "skills_config.json")
|
path = os.path.join(get_skills_dir(), "skills_config.json")
|
||||||
|
|||||||
177
common/i18n.py
Normal file
177
common/i18n.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# encoding:utf-8
|
||||||
|
|
||||||
|
"""Lightweight global language detection and resolution.
|
||||||
|
|
||||||
|
This module is the single source of truth for the runtime UI language used
|
||||||
|
across the CLI, startup logs, error messages, agent prompts and channel
|
||||||
|
replies. It must NOT import project config (to avoid circular imports) and
|
||||||
|
must stay dependency-free so it can run at the earliest startup phase.
|
||||||
|
|
||||||
|
Resolution priority (highest first):
|
||||||
|
1. Explicit `cow_lang` from config.json — also covers Docker/CI, since any
|
||||||
|
config key is overridable via its uppercase env var (e.g. COW_LANG=zh),
|
||||||
|
handled by config.load_config() before resolution. COW_LANG is a private
|
||||||
|
name to avoid clashing with the gettext-standard LANGUAGE variable.
|
||||||
|
2. macOS `defaults read -g AppleLocale` (system-level preference; a Chinese
|
||||||
|
system locale is a strong signal that beats a shell-default LANG)
|
||||||
|
3. Standard locale env vars: LC_ALL > LC_MESSAGES > LANG
|
||||||
|
4. Python locale module
|
||||||
|
5. Default -> English
|
||||||
|
|
||||||
|
A value of "auto" (the default) triggers detection (steps 2-5). Explicitly
|
||||||
|
setting "zh" or "en" locks the language and skips detection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Supported language codes
|
||||||
|
ZH = "zh"
|
||||||
|
EN = "en"
|
||||||
|
SUPPORTED = (ZH, EN)
|
||||||
|
DEFAULT_LANG = EN
|
||||||
|
|
||||||
|
# Resolved language cache; None until first resolution.
|
||||||
|
_resolved_lang = None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize(raw):
|
||||||
|
"""Map an arbitrary locale-ish string to a supported code, or None.
|
||||||
|
|
||||||
|
Only Chinese is detected explicitly; everything else (including unknown
|
||||||
|
or empty values) yields None so the caller can fall through to the next
|
||||||
|
detection source.
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
return None
|
||||||
|
value = str(raw).strip().lower().replace("_", "-")
|
||||||
|
if value in ("auto", ""):
|
||||||
|
return None
|
||||||
|
# Chinese variants: zh, zh-cn, zh-hans, zh-hans-cn, zh-tw, zh-hk ...
|
||||||
|
if value.startswith("zh") or value.startswith("chinese"):
|
||||||
|
return ZH
|
||||||
|
if value.startswith("en") or value.startswith("english"):
|
||||||
|
return EN
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_from_env():
|
||||||
|
"""Detect language from standard locale environment variables.
|
||||||
|
|
||||||
|
Note: on macOS, `LANG` is often a shell default (e.g. en_US.UTF-8 set by
|
||||||
|
.zshrc) that does not reflect the user's real preference, so AppleLocale
|
||||||
|
is checked first (see detect_language). On Linux these vars are the
|
||||||
|
primary signal.
|
||||||
|
|
||||||
|
The cow_lang env override (COW_LANG=zh) is intentionally NOT read here:
|
||||||
|
it sets config["cow_lang"] and is handled via the explicit config path,
|
||||||
|
not auto-detection.
|
||||||
|
"""
|
||||||
|
for key in ("LC_ALL", "LC_MESSAGES", "LANG"):
|
||||||
|
lang = _normalize(os.environ.get(key))
|
||||||
|
if lang:
|
||||||
|
return lang
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_from_macos():
|
||||||
|
"""macOS fallback: read the system-wide AppleLocale preference.
|
||||||
|
|
||||||
|
On macOS the terminal often does NOT export LANG, yet the system locale
|
||||||
|
is still meaningful (e.g. a Chinese Mac reports zh_CN). This recovers
|
||||||
|
that signal so Chinese users are not misdetected as English.
|
||||||
|
"""
|
||||||
|
if sys.platform != "darwin":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["defaults", "read", "-g", "AppleLocale"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
if out.returncode == 0:
|
||||||
|
return _normalize(out.stdout)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_from_python_locale():
|
||||||
|
"""Last-resort detection via Python's locale module."""
|
||||||
|
try:
|
||||||
|
import locale
|
||||||
|
|
||||||
|
for value in locale.getlocale():
|
||||||
|
lang = _normalize(value)
|
||||||
|
if lang:
|
||||||
|
return lang
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def detect_language():
|
||||||
|
"""Run full auto-detection and return a supported language code.
|
||||||
|
|
||||||
|
Order (auto-detection only; explicit config["cow_lang"] is resolved
|
||||||
|
before this is reached):
|
||||||
|
1. macOS AppleLocale (system-level preference; a Chinese system locale
|
||||||
|
is a strong, low-false-positive signal that beats a shell-default
|
||||||
|
LANG like en_US.UTF-8)
|
||||||
|
2. locale env vars LC_ALL / LC_MESSAGES / LANG (primary signal on Linux)
|
||||||
|
3. Python locale module
|
||||||
|
4. default English
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
_detect_from_macos()
|
||||||
|
or _detect_from_env()
|
||||||
|
or _detect_from_python_locale()
|
||||||
|
or DEFAULT_LANG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_language(configured=None):
|
||||||
|
"""Resolve the effective language from a configured value.
|
||||||
|
|
||||||
|
`configured` is the raw `cow_lang` value from config.json (may be None,
|
||||||
|
"auto", "zh" or "en"). An explicit "zh"/"en" locks the result; "auto"
|
||||||
|
or empty triggers detection. The result is cached globally.
|
||||||
|
"""
|
||||||
|
global _resolved_lang
|
||||||
|
explicit = _normalize(configured)
|
||||||
|
if explicit:
|
||||||
|
_resolved_lang = explicit
|
||||||
|
else:
|
||||||
|
_resolved_lang = detect_language()
|
||||||
|
return _resolved_lang
|
||||||
|
|
||||||
|
|
||||||
|
def set_language(lang):
|
||||||
|
"""Force the resolved language (used by tests or per-request overrides)."""
|
||||||
|
global _resolved_lang
|
||||||
|
normalized = _normalize(lang)
|
||||||
|
_resolved_lang = normalized or DEFAULT_LANG
|
||||||
|
return _resolved_lang
|
||||||
|
|
||||||
|
|
||||||
|
def get_language():
|
||||||
|
"""Return the currently resolved language, detecting lazily if needed."""
|
||||||
|
global _resolved_lang
|
||||||
|
if _resolved_lang is None:
|
||||||
|
_resolved_lang = detect_language()
|
||||||
|
return _resolved_lang
|
||||||
|
|
||||||
|
|
||||||
|
def is_zh():
|
||||||
|
return get_language() == ZH
|
||||||
|
|
||||||
|
|
||||||
|
def t(zh_text, en_text):
|
||||||
|
"""Pick a string by the current language. Tiny inline-translation helper.
|
||||||
|
|
||||||
|
Intended for one-off strings where a full message catalog is overkill:
|
||||||
|
t("已中止", "Cancelled")
|
||||||
|
"""
|
||||||
|
return zh_text if get_language() == ZH else en_text
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"cow_lang": "auto",
|
||||||
"channel_type": "weixin",
|
"channel_type": "weixin",
|
||||||
"model": "deepseek-v4-flash",
|
"model": "deepseek-v4-flash",
|
||||||
"deepseek_api_key": "",
|
"deepseek_api_key": "",
|
||||||
|
|||||||
11
config.py
11
config.py
@@ -7,11 +7,17 @@ import os
|
|||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
from common.log import logger
|
from common.log import logger
|
||||||
|
from common import i18n
|
||||||
|
|
||||||
# All available config keys are listed in this dict (use lowercase keys).
|
# All available config keys are listed in this dict (use lowercase keys).
|
||||||
# The values here are placeholders only; the program does NOT read them.
|
# The values here are placeholders only; the program does NOT read them.
|
||||||
# They merely document the expected format — put real values in config.json.
|
# They merely document the expected format — put real values in config.json.
|
||||||
available_setting = {
|
available_setting = {
|
||||||
|
# global UI language for CLI, startup logs, error messages, agent prompts
|
||||||
|
# and channel replies. Options: "auto" (detect from system locale, default),
|
||||||
|
# "zh" (Chinese) or "en" (English). An explicit value locks the language.
|
||||||
|
# value: auto/en/zh
|
||||||
|
"cow_lang": "auto",
|
||||||
# openai api config
|
# openai api config
|
||||||
"open_ai_api_key": "", # openai api key
|
"open_ai_api_key": "", # openai api key
|
||||||
# openai api base; when use_azure_chatgpt is true, set the matching api base
|
# openai api base; when use_azure_chatgpt is true, set the matching api base
|
||||||
@@ -390,12 +396,17 @@ def load_config():
|
|||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
logger.debug("[INIT] set log level to DEBUG")
|
logger.debug("[INIT] set log level to DEBUG")
|
||||||
|
|
||||||
|
# Resolve the global UI language as early as possible so that every
|
||||||
|
# downstream layer (logs, CLI, agent prompts, channel replies) shares it.
|
||||||
|
resolved_lang = i18n.resolve_language(config.get("cow_lang", "auto"))
|
||||||
|
|
||||||
logger.info("[INIT] load config: {}".format(drag_sensitive(config)))
|
logger.info("[INIT] load config: {}".format(drag_sensitive(config)))
|
||||||
|
|
||||||
# print system initialization info
|
# print system initialization info
|
||||||
logger.info("[INIT] ========================================")
|
logger.info("[INIT] ========================================")
|
||||||
logger.info("[INIT] System Initialization")
|
logger.info("[INIT] System Initialization")
|
||||||
logger.info("[INIT] ========================================")
|
logger.info("[INIT] ========================================")
|
||||||
|
logger.info("[INIT] Language: {}".format(resolved_lang))
|
||||||
logger.info("[INIT] Channel: {}".format(config.get("channel_type", "unknown")))
|
logger.info("[INIT] Channel: {}".format(config.get("channel_type", "unknown")))
|
||||||
logger.info("[INIT] Model: {}".format(config.get("model", "unknown")))
|
logger.info("[INIT] Model: {}".format(config.get("model", "unknown")))
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "9899:9899"
|
- "9899:9899"
|
||||||
environment:
|
environment:
|
||||||
|
COW_LANG: 'auto'
|
||||||
CHANNEL_TYPE: 'weixin'
|
CHANNEL_TYPE: 'weixin'
|
||||||
MODEL: 'deepseek-v4-flash'
|
MODEL: 'deepseek-v4-flash'
|
||||||
DEEPSEEK_API_KEY: ''
|
DEEPSEEK_API_KEY: ''
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from models.openai.openai_compat import (
|
|||||||
from models.openai.openai_http_client import OpenAIHTTPClient, OpenAIHTTPError
|
from models.openai.openai_http_client import OpenAIHTTPClient, OpenAIHTTPError
|
||||||
import requests
|
import requests
|
||||||
from common import const
|
from common import const
|
||||||
|
from common.i18n import t as _t
|
||||||
from models.bot import Bot
|
from models.bot import Bot
|
||||||
from models.openai_compatible_bot import OpenAICompatibleBot
|
from models.openai_compatible_bot import OpenAICompatibleBot
|
||||||
from models.chatgpt.chat_gpt_session import ChatGPTSession
|
from models.chatgpt.chat_gpt_session import ChatGPTSession
|
||||||
@@ -94,13 +95,13 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
|||||||
clear_memory_commands = conf().get("clear_memory_commands", ["#清除记忆"])
|
clear_memory_commands = conf().get("clear_memory_commands", ["#清除记忆"])
|
||||||
if query in clear_memory_commands:
|
if query in clear_memory_commands:
|
||||||
self.sessions.clear_session(session_id)
|
self.sessions.clear_session(session_id)
|
||||||
reply = Reply(ReplyType.INFO, "记忆已清除")
|
reply = Reply(ReplyType.INFO, _t("记忆已清除", "Memory cleared"))
|
||||||
elif query == "#清除所有":
|
elif query == "#清除所有":
|
||||||
self.sessions.clear_all_session()
|
self.sessions.clear_all_session()
|
||||||
reply = Reply(ReplyType.INFO, "所有人记忆已清除")
|
reply = Reply(ReplyType.INFO, _t("所有人记忆已清除", "All memories cleared"))
|
||||||
elif query == "#更新配置":
|
elif query == "#更新配置":
|
||||||
load_config()
|
load_config()
|
||||||
reply = Reply(ReplyType.INFO, "配置已更新")
|
reply = Reply(ReplyType.INFO, _t("配置已更新", "Config updated"))
|
||||||
if reply:
|
if reply:
|
||||||
return reply
|
return reply
|
||||||
session = self.sessions.session_query(query, session_id)
|
session = self.sessions.session_query(query, session_id)
|
||||||
@@ -148,7 +149,7 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
|||||||
reply = self.reply_image(context)
|
reply = self.reply_image(context)
|
||||||
return reply
|
return reply
|
||||||
else:
|
else:
|
||||||
reply = Reply(ReplyType.ERROR, "Bot不支持处理{}类型的消息".format(context.type))
|
reply = Reply(ReplyType.ERROR, _t("Bot不支持处理{}类型的消息", "Bot does not support message type {}").format(context.type))
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
def reply_image(self, context):
|
def reply_image(self, context):
|
||||||
@@ -165,7 +166,7 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
|||||||
# Check if file exists
|
# Check if file exists
|
||||||
if not os.path.exists(image_path):
|
if not os.path.exists(image_path):
|
||||||
logger.error(f"[CHATGPT] Image file not found: {image_path}")
|
logger.error(f"[CHATGPT] Image file not found: {image_path}")
|
||||||
return Reply(ReplyType.ERROR, "图片文件不存在")
|
return Reply(ReplyType.ERROR, _t("图片文件不存在", "Image file not found"))
|
||||||
|
|
||||||
# Read and encode image
|
# Read and encode image
|
||||||
with open(image_path, "rb") as f:
|
with open(image_path, "rb") as f:
|
||||||
@@ -232,7 +233,7 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
|||||||
logger.error(f"[CHATGPT] Image processing error: {e}")
|
logger.error(f"[CHATGPT] Image processing error: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return Reply(ReplyType.ERROR, f"图片识别失败: {str(e)}")
|
return Reply(ReplyType.ERROR, _t("图片识别失败: ", "Image recognition failed: ") + str(e))
|
||||||
|
|
||||||
def reply_text(self, session: ChatGPTSession, api_key=None, args=None, retry_count=0) -> dict:
|
def reply_text(self, session: ChatGPTSession, api_key=None, args=None, retry_count=0) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -277,25 +278,25 @@ class ChatGPTBot(Bot, OpenAIImage, OpenAICompatibleBot):
|
|||||||
def _handle_reply_error(self, e, session, api_key, args, retry_count):
|
def _handle_reply_error(self, e, session, api_key, args, retry_count):
|
||||||
"""Map exception to user-facing reply with retry/backoff (mirrors SDK behavior)."""
|
"""Map exception to user-facing reply with retry/backoff (mirrors SDK behavior)."""
|
||||||
need_retry = retry_count < 2
|
need_retry = retry_count < 2
|
||||||
result = {"completion_tokens": 0, "content": "我现在有点累了,等会再来吧"}
|
result = {"completion_tokens": 0, "content": _t("我现在有点累了,等会再来吧", "I'm a bit tired right now. Please try again later.")}
|
||||||
if isinstance(e, RateLimitError):
|
if isinstance(e, RateLimitError):
|
||||||
logger.warn("[CHATGPT] RateLimitError: {}".format(e))
|
logger.warn("[CHATGPT] RateLimitError: {}".format(e))
|
||||||
result["content"] = "提问太快啦,请休息一下再问我吧"
|
result["content"] = _t("提问太快啦,请休息一下再问我吧", "You're asking too fast. Please take a short break and try again.")
|
||||||
if need_retry:
|
if need_retry:
|
||||||
time.sleep(20)
|
time.sleep(20)
|
||||||
elif isinstance(e, Timeout):
|
elif isinstance(e, Timeout):
|
||||||
logger.warn("[CHATGPT] Timeout: {}".format(e))
|
logger.warn("[CHATGPT] Timeout: {}".format(e))
|
||||||
result["content"] = "我没有收到你的消息"
|
result["content"] = _t("我没有收到你的消息", "I didn't receive your message")
|
||||||
if need_retry:
|
if need_retry:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
elif isinstance(e, APIConnectionError):
|
elif isinstance(e, APIConnectionError):
|
||||||
logger.warn("[CHATGPT] APIConnectionError: {}".format(e))
|
logger.warn("[CHATGPT] APIConnectionError: {}".format(e))
|
||||||
result["content"] = "我连接不到你的网络"
|
result["content"] = _t("我连接不到你的网络", "I can't reach your network")
|
||||||
if need_retry:
|
if need_retry:
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
elif isinstance(e, APIError):
|
elif isinstance(e, APIError):
|
||||||
logger.warn("[CHATGPT] Bad Gateway: {}".format(e))
|
logger.warn("[CHATGPT] Bad Gateway: {}".format(e))
|
||||||
result["content"] = "请再问我一次"
|
result["content"] = _t("请再问我一次", "Please ask me again")
|
||||||
if need_retry:
|
if need_retry:
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
else:
|
else:
|
||||||
@@ -358,7 +359,7 @@ class AzureChatGPTBot(ChatGPTBot):
|
|||||||
status = ""
|
status = ""
|
||||||
while (status != "succeeded"):
|
while (status != "succeeded"):
|
||||||
if retry_count > 3:
|
if retry_count > 3:
|
||||||
return False, "图片生成失败"
|
return False, _t("图片生成失败", "Image generation failed")
|
||||||
response = requests.get(operation_location, headers=headers)
|
response = requests.get(operation_location, headers=headers)
|
||||||
status = response.json()['status']
|
status = response.json()['status']
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
@@ -366,7 +367,7 @@ class AzureChatGPTBot(ChatGPTBot):
|
|||||||
return True, image_url
|
return True, image_url
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("create image error: {}".format(e))
|
logger.error("create image error: {}".format(e))
|
||||||
return False, "图片生成失败"
|
return False, _t("图片生成失败", "Image generation failed")
|
||||||
elif text_to_image_model == "dall-e-3":
|
elif text_to_image_model == "dall-e-3":
|
||||||
api_version = conf().get("azure_api_version", "2024-02-15-preview")
|
api_version = conf().get("azure_api_version", "2024-02-15-preview")
|
||||||
endpoint = conf().get("azure_openai_dalle_api_base","open_ai_api_base")
|
endpoint = conf().get("azure_openai_dalle_api_base","open_ai_api_base")
|
||||||
@@ -389,7 +390,7 @@ class AzureChatGPTBot(ChatGPTBot):
|
|||||||
else:
|
else:
|
||||||
error_message = "响应中没有图像 URL"
|
error_message = "响应中没有图像 URL"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
return False, "图片生成失败"
|
return False, _t("图片生成失败", "Image generation failed")
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
# 捕获所有请求相关的异常
|
# 捕获所有请求相关的异常
|
||||||
@@ -405,9 +406,9 @@ class AzureChatGPTBot(ChatGPTBot):
|
|||||||
# 捕获所有其他异常
|
# 捕获所有其他异常
|
||||||
error_message = f"生成图像时发生错误: {e}"
|
error_message = f"生成图像时发生错误: {e}"
|
||||||
logger.error(error_message)
|
logger.error(error_message)
|
||||||
return False, "图片生成失败"
|
return False, _t("图片生成失败", "Image generation failed")
|
||||||
else:
|
else:
|
||||||
return False, "图片生成失败,未配置text_to_image参数"
|
return False, _t("图片生成失败,未配置text_to_image参数", "Image generation failed: text_to_image is not configured")
|
||||||
|
|
||||||
|
|
||||||
class _AzureChatHTTPClient(OpenAIHTTPClient):
|
class _AzureChatHTTPClient(OpenAIHTTPClient):
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from plugins import Plugin, Event, EventContext, EventAction
|
|||||||
from bridge.context import ContextType
|
from bridge.context import ContextType
|
||||||
from bridge.reply import Reply, ReplyType
|
from bridge.reply import Reply, ReplyType
|
||||||
from common.log import logger
|
from common.log import logger
|
||||||
|
from common.i18n import t as _t
|
||||||
from config import conf
|
from config import conf
|
||||||
from cli import __version__
|
from cli import __version__
|
||||||
|
|
||||||
@@ -280,19 +281,18 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _typo_hint(token: str, suggestion) -> str:
|
def _typo_hint(token: str, suggestion) -> str:
|
||||||
hint = f"未知命令: /{token}"
|
hint = _t(f"未知命令: /{token}", f"Unknown command: /{token}")
|
||||||
if suggestion:
|
if suggestion:
|
||||||
hint += f"\n你是不是想输入 /{suggestion} ?"
|
hint += _t(f"\n你是不是想输入 /{suggestion} ?", f"\nDid you mean /{suggestion} ?")
|
||||||
hint += "\n发送 /help 查看全部命令。"
|
hint += _t("\n发送 /help 查看全部命令。", "\nSend /help to see all commands.")
|
||||||
return hint
|
return hint
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _ambiguous_hint(token: str, candidates) -> str:
|
def _ambiguous_hint(token: str, candidates) -> str:
|
||||||
options = " ".join(f"/{c}" for c in candidates)
|
options = " ".join(f"/{c}" for c in candidates)
|
||||||
return (
|
return _t(
|
||||||
f"命令不明确: /{token}\n"
|
f"命令不明确: /{token}\n可能想输入: {options}\n发送 /help 查看全部命令。",
|
||||||
f"可能想输入: {options}\n"
|
f"Ambiguous command: /{token}\nDid you mean: {options}\nSend /help to see all commands.",
|
||||||
"发送 /help 查看全部命令。"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -324,7 +324,10 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
def _dispatch(self, cmd: str, args: str, e_context: EventContext, session_id: str = "") -> str:
|
def _dispatch(self, cmd: str, args: str, e_context: EventContext, session_id: str = "") -> str:
|
||||||
if cmd in CLI_ONLY_COMMANDS:
|
if cmd in CLI_ONLY_COMMANDS:
|
||||||
return f"⚠️ `cow {cmd}` 只能在命令行终端中执行。\n请在终端运行: cow {cmd}"
|
return _t(
|
||||||
|
f"⚠️ `cow {cmd}` 只能在命令行终端中执行。\n请在终端运行: cow {cmd}",
|
||||||
|
f"⚠️ `cow {cmd}` can only run in a terminal.\nRun it in your shell: cow {cmd}",
|
||||||
|
)
|
||||||
|
|
||||||
handler_attr = "_cmd_" + cmd.replace("-", "_")
|
handler_attr = "_cmd_" + cmd.replace("-", "_")
|
||||||
handler = getattr(self, handler_attr, None)
|
handler = getattr(self, handler_attr, None)
|
||||||
@@ -333,39 +336,68 @@ class CowCliPlugin(Plugin):
|
|||||||
return handler(args, e_context, session_id=session_id)
|
return handler(args, e_context, session_id=session_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[CowCli] command '{cmd}' failed: {e}")
|
logger.error(f"[CowCli] command '{cmd}' failed: {e}")
|
||||||
return f"命令执行失败: {e}"
|
return _t(f"命令执行失败: {e}", f"Command failed: {e}")
|
||||||
|
|
||||||
return f"未知命令: {cmd}"
|
return _t(f"未知命令: {cmd}", f"Unknown command: {cmd}")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# help / version
|
# help / version
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _cmd_help(self, args: str, e_context, **_) -> str:
|
def _cmd_help(self, args: str, e_context, **_) -> str:
|
||||||
|
if _t("zh", "en") == "en":
|
||||||
|
lines = [
|
||||||
|
"📋 CowAgent Commands",
|
||||||
|
"",
|
||||||
|
"/help: Show this help",
|
||||||
|
"/version: Show version",
|
||||||
|
"/status: Show running status",
|
||||||
|
"/cancel: Abort the running Agent task",
|
||||||
|
"/logs [N]: Show the last N log lines (default 20)",
|
||||||
|
"/context: Show current conversation context",
|
||||||
|
"/context clear: Clear current conversation context",
|
||||||
|
"/skill list: List installed skills",
|
||||||
|
"/skill list --remote: Browse Skill Hub",
|
||||||
|
"/skill search <keyword>: Search skills",
|
||||||
|
"/skill install <name>: Install a skill",
|
||||||
|
"/skill info <name>: Show skill details",
|
||||||
|
"/config: Show current config",
|
||||||
|
"/config <key>: Show a config item",
|
||||||
|
"/config <key> <val>: Update a config item",
|
||||||
|
"/memory status: Show memory index status",
|
||||||
|
"/memory rebuild-index: Rebuild the vector index (required after switching embedding model)",
|
||||||
|
"/memory dream [N]: Trigger memory distillation (last N days, default 3, max 30)",
|
||||||
|
"/knowledge: Show knowledge base stats",
|
||||||
|
"/knowledge list: Show knowledge base file tree",
|
||||||
|
"/knowledge on|off: Enable/disable knowledge base",
|
||||||
|
"",
|
||||||
|
"💡 You can also use cow <command> instead of /<command>",
|
||||||
|
]
|
||||||
|
else:
|
||||||
lines = [
|
lines = [
|
||||||
"📋 CowAgent 命令列表",
|
"📋 CowAgent 命令列表",
|
||||||
"",
|
"",
|
||||||
" /help 显示此帮助",
|
"/help: 显示此帮助",
|
||||||
" /version 查看版本",
|
"/version: 查看版本",
|
||||||
" /status 查看运行状态",
|
"/status: 查看运行状态",
|
||||||
" /cancel 中止当前正在运行的 Agent 任务",
|
"/cancel: 中止当前正在运行的 Agent 任务",
|
||||||
" /logs [N] 查看最近N条日志 (默认20)",
|
"/logs [N]: 查看最近N条日志 (默认20)",
|
||||||
" /context 查看当前对话上下文信息",
|
"/context: 查看当前对话上下文信息",
|
||||||
" /context clear 清除当前对话上下文",
|
"/context clear: 清除当前对话上下文",
|
||||||
" /skill list 查看已安装的技能",
|
"/skill list: 查看已安装的技能",
|
||||||
" /skill list --remote 浏览技能广场",
|
"/skill list --remote: 浏览技能广场",
|
||||||
" /skill search <关键词> 搜索技能",
|
"/skill search <关键词>: 搜索技能",
|
||||||
" /skill install <名称> 安装技能",
|
"/skill install <名称>: 安装技能",
|
||||||
" /skill info <名称> 查看技能详情",
|
"/skill info <名称>: 查看技能详情",
|
||||||
" /config 查看当前配置",
|
"/config: 查看当前配置",
|
||||||
" /config <key> 查看某项配置",
|
"/config <key>: 查看某项配置",
|
||||||
" /config <key> <val> 修改配置",
|
"/config <key> <val>: 修改配置",
|
||||||
" /memory status 查看记忆索引状态",
|
"/memory status: 查看记忆索引状态",
|
||||||
" /memory rebuild-index 清空并重建向量索引 (切换 embedding 模型后必须执行)",
|
"/memory rebuild-index: 清空并重建向量索引 (切换 embedding 模型后必须执行)",
|
||||||
" /memory dream [N] 手动触发记忆蒸馏 (整理近N天, 默认3, 最多30)",
|
"/memory dream [N]: 手动触发记忆蒸馏 (整理近N天, 默认3, 最多30)",
|
||||||
" /knowledge 查看知识库统计",
|
"/knowledge: 查看知识库统计",
|
||||||
" /knowledge list 查看知识库文件树",
|
"/knowledge list: 查看知识库文件树",
|
||||||
" /knowledge on|off 开启/关闭知识库",
|
"/knowledge on|off: 开启/关闭知识库",
|
||||||
"",
|
"",
|
||||||
"💡 也可以用 cow <command> 代替 /<command>",
|
"💡 也可以用 cow <command> 代替 /<command>",
|
||||||
]
|
]
|
||||||
@@ -405,9 +437,9 @@ class CowCliPlugin(Plugin):
|
|||||||
cancelled = registry.cancel_session(target_session)
|
cancelled = registry.cancel_session(target_session)
|
||||||
|
|
||||||
if cancelled <= 0:
|
if cancelled <= 0:
|
||||||
return "当前没有可中止的任务。"
|
return _t("当前没有可中止的任务。", "Nothing to cancel.")
|
||||||
|
|
||||||
return "🛑 已中止"
|
return _t("🛑 已中止", "🛑 Cancelled")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# status
|
# status
|
||||||
@@ -417,21 +449,21 @@ class CowCliPlugin(Plugin):
|
|||||||
from config import conf
|
from config import conf
|
||||||
|
|
||||||
cfg = conf()
|
cfg = conf()
|
||||||
lines = ["📊 CowAgent 运行状态", ""]
|
lines = [_t("📊 CowAgent 运行状态", "📊 CowAgent Status"), ""]
|
||||||
|
|
||||||
lines.append(f" 版本: v{__version__}")
|
lines.append(_t(f" 版本: v{__version__}", f" Version: v{__version__}"))
|
||||||
lines.append(f" 进程: PID {os.getpid()}")
|
lines.append(_t(f" 进程: PID {os.getpid()}", f" Process: PID {os.getpid()}"))
|
||||||
|
|
||||||
channel = cfg.get("channel_type", "unknown")
|
channel = cfg.get("channel_type", "unknown")
|
||||||
if isinstance(channel, list):
|
if isinstance(channel, list):
|
||||||
channel = ", ".join(channel)
|
channel = ", ".join(channel)
|
||||||
lines.append(f" 通道: {channel}")
|
lines.append(_t(f" 通道: {channel}", f" Channel: {channel}"))
|
||||||
|
|
||||||
model_name = cfg.get("model", "unknown")
|
model_name = cfg.get("model", "unknown")
|
||||||
lines.append(f" 模型: {model_name}")
|
lines.append(_t(f" 模型: {model_name}", f" Model: {model_name}"))
|
||||||
|
|
||||||
mode = "Chat" if cfg.get("agent") is False else "Agent"
|
mode = "Chat" if cfg.get("agent") is False else "Agent"
|
||||||
lines.append(f" 模式: {mode}")
|
lines.append(_t(f" 模式: {mode}", f" Mode: {mode}"))
|
||||||
|
|
||||||
session_id = self._get_session_id(e_context, fallback=session_id)
|
session_id = self._get_session_id(e_context, fallback=session_id)
|
||||||
agent = self._get_agent(session_id)
|
agent = self._get_agent(session_id)
|
||||||
@@ -439,7 +471,7 @@ class CowCliPlugin(Plugin):
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
with agent.messages_lock:
|
with agent.messages_lock:
|
||||||
msg_count = len(agent.messages)
|
msg_count = len(agent.messages)
|
||||||
lines.append(f" 会话消息数: {msg_count}")
|
lines.append(_t(f" 会话消息数: {msg_count}", f" Session messages: {msg_count}"))
|
||||||
|
|
||||||
if agent.skill_manager:
|
if agent.skill_manager:
|
||||||
total = len(agent.skill_manager.skills)
|
total = len(agent.skill_manager.skills)
|
||||||
@@ -447,10 +479,10 @@ class CowCliPlugin(Plugin):
|
|||||||
1 for v in agent.skill_manager.skills_config.values()
|
1 for v in agent.skill_manager.skills_config.values()
|
||||||
if v.get("enabled", True)
|
if v.get("enabled", True)
|
||||||
)
|
)
|
||||||
lines.append(f" 已加载技能: {enabled}/{total}")
|
lines.append(_t(f" 已加载技能: {enabled}/{total}", f" Loaded skills: {enabled}/{total}"))
|
||||||
else:
|
else:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f" Agent: 未初始化 (首次对话后自动创建)")
|
lines.append(_t(" Agent: 未初始化 (首次对话后自动创建)", " Agent: not initialized (created on first chat)"))
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -465,7 +497,7 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
log_file = self._find_log_file()
|
log_file = self._find_log_file()
|
||||||
if not log_file:
|
if not log_file:
|
||||||
return "未找到日志文件"
|
return _t("未找到日志文件", "No log file found")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
|
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
|
||||||
@@ -473,10 +505,10 @@ class CowCliPlugin(Plugin):
|
|||||||
tail = all_lines[-num_lines:]
|
tail = all_lines[-num_lines:]
|
||||||
content = "".join(tail).strip()
|
content = "".join(tail).strip()
|
||||||
if not content:
|
if not content:
|
||||||
return "日志为空"
|
return _t("日志为空", "Log is empty")
|
||||||
return f"📄 最近 {len(tail)} 条日志:\n\n{content}"
|
return _t(f"📄 最近 {len(tail)} 条日志:\n\n{content}", f"📄 Last {len(tail)} log lines:\n\n{content}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"读取日志失败: {e}"
|
return _t(f"读取日志失败: {e}", f"Failed to read log: {e}")
|
||||||
|
|
||||||
def _find_log_file(self) -> str:
|
def _find_log_file(self) -> str:
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
@@ -507,13 +539,13 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
def _context_info(self, agent, session_id: str) -> str:
|
def _context_info(self, agent, session_id: str) -> str:
|
||||||
if not agent:
|
if not agent:
|
||||||
return "⚠️ Agent 未初始化,暂无上下文信息"
|
return _t("⚠️ Agent 未初始化,暂无上下文信息", "⚠️ Agent not initialized, no context yet")
|
||||||
|
|
||||||
with agent.messages_lock:
|
with agent.messages_lock:
|
||||||
messages = agent.messages.copy()
|
messages = agent.messages.copy()
|
||||||
|
|
||||||
if not messages:
|
if not messages:
|
||||||
return "当前对话上下文为空"
|
return _t("当前对话上下文为空", "Current conversation context is empty")
|
||||||
|
|
||||||
user_msgs = sum(1 for m in messages if m.get("role") == "user")
|
user_msgs = sum(1 for m in messages if m.get("role") == "user")
|
||||||
assistant_msgs = sum(1 for m in messages if m.get("role") == "assistant")
|
assistant_msgs = sum(1 for m in messages if m.get("role") == "assistant")
|
||||||
@@ -521,6 +553,20 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
total_chars = sum(len(str(m.get("content", ""))) for m in messages)
|
total_chars = sum(len(str(m.get("content", ""))) for m in messages)
|
||||||
|
|
||||||
|
if _t("zh", "en") == "en":
|
||||||
|
lines = [
|
||||||
|
"💬 Current Conversation Context",
|
||||||
|
"",
|
||||||
|
f" Session: {session_id or 'default'}",
|
||||||
|
f" Total messages: {len(messages)}",
|
||||||
|
f" User messages: {user_msgs}",
|
||||||
|
f" Assistant replies: {assistant_msgs}",
|
||||||
|
f" Tool calls: {tool_msgs}",
|
||||||
|
f" Total content length: ~{total_chars} chars",
|
||||||
|
"",
|
||||||
|
" Send /context clear to clear the conversation context",
|
||||||
|
]
|
||||||
|
else:
|
||||||
lines = [
|
lines = [
|
||||||
"💬 当前对话上下文",
|
"💬 当前对话上下文",
|
||||||
"",
|
"",
|
||||||
@@ -537,13 +583,13 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
def _context_clear(self, agent, session_id: str) -> str:
|
def _context_clear(self, agent, session_id: str) -> str:
|
||||||
if not agent:
|
if not agent:
|
||||||
return "⚠️ Agent 未初始化"
|
return _t("⚠️ Agent 未初始化", "⚠️ Agent not initialized")
|
||||||
|
|
||||||
with agent.messages_lock:
|
with agent.messages_lock:
|
||||||
count = len(agent.messages)
|
count = len(agent.messages)
|
||||||
agent.messages.clear()
|
agent.messages.clear()
|
||||||
|
|
||||||
return f"✅ 已清除当前对话上下文 ({count} 条消息)"
|
return _t(f"✅ 已清除当前对话上下文 ({count} 条消息)", f"✅ Conversation context cleared ({count} messages)")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# config
|
# config
|
||||||
@@ -578,21 +624,24 @@ class CowCliPlugin(Plugin):
|
|||||||
def _config_show_all(self) -> str:
|
def _config_show_all(self) -> str:
|
||||||
from config import conf
|
from config import conf
|
||||||
cfg = conf()
|
cfg = conf()
|
||||||
lines = ["⚙️ 当前配置", ""]
|
lines = [_t("⚙️ 当前配置", "⚙️ Current Config"), ""]
|
||||||
for key in sorted(self._CONFIG_READABLE):
|
for key in sorted(self._CONFIG_READABLE):
|
||||||
val = cfg.get(key, "")
|
val = cfg.get(key, "")
|
||||||
lines.append(f" {key}: {val}")
|
lines.append(f" {key}: {val}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
lines.append("💡 /config <key> 查看配置")
|
lines.append(_t("💡 /config <key>: 查看配置", "💡 /config <key>: Show a config item"))
|
||||||
lines.append("💡 /config <key> <val> 修改配置")
|
lines.append(_t("💡 /config <key> <val>: 修改配置", "💡 /config <key> <val>: Update a config item"))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _config_get(self, key: str) -> str:
|
def _config_get(self, key: str) -> str:
|
||||||
from config import conf
|
from config import conf
|
||||||
if key not in self._CONFIG_READABLE:
|
if key not in self._CONFIG_READABLE:
|
||||||
available = ", ".join(sorted(self._CONFIG_READABLE))
|
available = ", ".join(sorted(self._CONFIG_READABLE))
|
||||||
return f"不支持查看 '{key}'\n\n可查看的配置项: {available}"
|
return _t(
|
||||||
|
f"不支持查看 '{key}'\n\n可查看的配置项: {available}",
|
||||||
|
f"Cannot show '{key}'\n\nReadable config items: {available}",
|
||||||
|
)
|
||||||
val = conf().get(key, "")
|
val = conf().get(key, "")
|
||||||
return f"⚙️ {key}: {val}"
|
return f"⚙️ {key}: {val}"
|
||||||
|
|
||||||
@@ -602,9 +651,12 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
if key not in self._CONFIG_WRITABLE:
|
if key not in self._CONFIG_WRITABLE:
|
||||||
if key in self._CONFIG_READABLE:
|
if key in self._CONFIG_READABLE:
|
||||||
return f"⚠️ '{key}' 为只读配置,不支持修改"
|
return _t(f"⚠️ '{key}' 为只读配置,不支持修改", f"⚠️ '{key}' is read-only and cannot be modified")
|
||||||
available = ", ".join(sorted(self._CONFIG_WRITABLE))
|
available = ", ".join(sorted(self._CONFIG_WRITABLE))
|
||||||
return f"不支持修改 '{key}'\n\n可修改的配置项: {available}"
|
return _t(
|
||||||
|
f"不支持修改 '{key}'\n\n可修改的配置项: {available}",
|
||||||
|
f"Cannot modify '{key}'\n\nWritable config items: {available}",
|
||||||
|
)
|
||||||
|
|
||||||
old_val = conf().get(key, "")
|
old_val = conf().get(key, "")
|
||||||
|
|
||||||
@@ -637,7 +689,7 @@ class CowCliPlugin(Plugin):
|
|||||||
with open(config_path, "w", encoding="utf-8") as f:
|
with open(config_path, "w", encoding="utf-8") as f:
|
||||||
_json.dump(file_config, f, indent=4, ensure_ascii=False)
|
_json.dump(file_config, f, indent=4, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"写入 config.json 失败: {e}"
|
return _t(f"写入 config.json 失败: {e}", f"Failed to write config.json: {e}")
|
||||||
|
|
||||||
# Sync updated values to environment variables so that load_config()
|
# Sync updated values to environment variables so that load_config()
|
||||||
# won't overwrite the new value with a stale env var (common in Docker).
|
# won't overwrite the new value with a stale env var (common in Docker).
|
||||||
@@ -660,7 +712,7 @@ class CowCliPlugin(Plugin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[CowCli] config reload warning: {e}")
|
logger.warning(f"[CowCli] config reload warning: {e}")
|
||||||
|
|
||||||
result = f"✅ 配置已更新\n\n {key}: {old_val} → {new_val}"
|
result = _t(f"✅ 配置已更新\n\n {key}: {old_val} → {new_val}", f"✅ Config updated\n\n {key}: {old_val} → {new_val}")
|
||||||
if "bot_type" in updates and updates["bot_type"] != old_bot_type:
|
if "bot_type" in updates and updates["bot_type"] != old_bot_type:
|
||||||
result += f"\n bot_type: {old_bot_type} → {updates['bot_type']}"
|
result += f"\n bot_type: {old_bot_type} → {updates['bot_type']}"
|
||||||
return result
|
return result
|
||||||
@@ -725,10 +777,13 @@ class CowCliPlugin(Plugin):
|
|||||||
from cli.commands.install import run_install_browser
|
from cli.commands.install import run_install_browser
|
||||||
|
|
||||||
if args.strip():
|
if args.strip():
|
||||||
return (
|
return _t(
|
||||||
"用法: /install-browser\n\n"
|
"用法: /install-browser\n\n"
|
||||||
"无需参数,等同于终端执行 `cow install-browser`。\n"
|
"无需参数,等同于终端执行 `cow install-browser`。\n"
|
||||||
"安装过程可能持续数分钟;进度会以多条消息推送,pip 详细输出见服务日志。"
|
"安装过程可能持续数分钟;进度会以多条消息推送,pip 详细输出见服务日志。",
|
||||||
|
"Usage: /install-browser\n\n"
|
||||||
|
"No arguments needed; equivalent to running `cow install-browser` in a terminal.\n"
|
||||||
|
"Installation may take a few minutes; progress is pushed as multiple messages, and detailed pip output goes to the service log.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Suppress detailed stream in chat; phases go through channel.send
|
# Suppress detailed stream in chat; phases go through channel.send
|
||||||
@@ -740,11 +795,16 @@ class CowCliPlugin(Plugin):
|
|||||||
on_phase=lambda m: self._send_install_progress(e_context, m),
|
on_phase=lambda m: self._send_install_progress(e_context, m),
|
||||||
)
|
)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
return (
|
return _t(
|
||||||
"❌ 安装未成功结束,请查看上方分段提示或服务器日志;"
|
"❌ 安装未成功结束,请查看上方分段提示或服务器日志;"
|
||||||
"也可在终端执行 `cow install-browser`。"
|
"也可在终端执行 `cow install-browser`。",
|
||||||
|
"❌ Installation did not finish successfully. Check the messages above or the server log; "
|
||||||
|
"you can also run `cow install-browser` in a terminal.",
|
||||||
|
)
|
||||||
|
return _t(
|
||||||
|
"✅ 安装流程已结束。请重启 CowAgent 后使用 browser 工具(进度见上方消息)。",
|
||||||
|
"✅ Installation finished. Restart CowAgent to use the browser tool (see messages above for progress).",
|
||||||
)
|
)
|
||||||
return "✅ 安装流程已结束。请重启 CowAgent 后使用 browser 工具(进度见上方消息)。"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# skill
|
# skill
|
||||||
@@ -770,16 +830,25 @@ class CowCliPlugin(Plugin):
|
|||||||
elif sub == "disable":
|
elif sub == "disable":
|
||||||
return self._skill_set_enabled(sub_args, False)
|
return self._skill_set_enabled(sub_args, False)
|
||||||
else:
|
else:
|
||||||
return (
|
return _t(
|
||||||
"用法: /skill <子命令>\n\n"
|
"用法: /skill <子命令>\n\n"
|
||||||
"子命令:\n"
|
"子命令:\n"
|
||||||
" list [--remote] 查看技能列表\n"
|
"list [--remote]: 查看技能列表\n"
|
||||||
" search <关键词> 搜索技能\n"
|
"search <关键词>: 搜索技能\n"
|
||||||
" install <名称> 安装技能\n"
|
"install <名称>: 安装技能\n"
|
||||||
" uninstall <名称> 卸载技能\n"
|
"uninstall <名称>: 卸载技能\n"
|
||||||
" info <名称> 查看技能详情\n"
|
"info <名称>: 查看技能详情\n"
|
||||||
" enable <名称> 启用技能\n"
|
"enable <名称>: 启用技能\n"
|
||||||
" disable <名称> 禁用技能"
|
"disable <名称>: 禁用技能",
|
||||||
|
"Usage: /skill <subcommand>\n\n"
|
||||||
|
"Subcommands:\n"
|
||||||
|
"list [--remote]: List skills\n"
|
||||||
|
"search <keyword>: Search skills\n"
|
||||||
|
"install <name>: Install a skill\n"
|
||||||
|
"uninstall <name>: Uninstall a skill\n"
|
||||||
|
"info <name>: Show skill details\n"
|
||||||
|
"enable <name>: Enable a skill\n"
|
||||||
|
"disable <name>: Disable a skill",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _refresh_skill_manager(self):
|
def _refresh_skill_manager(self):
|
||||||
@@ -813,13 +882,16 @@ class CowCliPlugin(Plugin):
|
|||||||
if os.path.exists(os.path.join(skill_path, "SKILL.md")):
|
if os.path.exists(os.path.join(skill_path, "SKILL.md")):
|
||||||
entries.append({"name": name, "source": source, "enabled": True})
|
entries.append({"name": name, "source": source, "enabled": True})
|
||||||
if not entries:
|
if not entries:
|
||||||
return "暂无已安装的技能\n\n💡 /skill list --remote 浏览技能广场"
|
return _t(
|
||||||
|
"暂无已安装的技能\n\n💡 /skill list --remote: 浏览技能广场",
|
||||||
|
"No skills installed yet\n\n💡 /skill list --remote: Browse Skill Hub",
|
||||||
|
)
|
||||||
config = {e["name"]: e for e in entries}
|
config = {e["name"]: e for e in entries}
|
||||||
|
|
||||||
sorted_entries = sorted(config.values(), key=lambda e: e.get("name", ""))
|
sorted_entries = sorted(config.values(), key=lambda e: e.get("name", ""))
|
||||||
enabled_count = sum(1 for e in sorted_entries if e.get("enabled", True))
|
enabled_count = sum(1 for e in sorted_entries if e.get("enabled", True))
|
||||||
|
|
||||||
lines = [f"📦 已安装的技能 ({enabled_count}/{len(sorted_entries)})", ""]
|
lines = [_t(f"📦 已安装的技能 ({enabled_count}/{len(sorted_entries)})", f"📦 Installed Skills ({enabled_count}/{len(sorted_entries)})"), ""]
|
||||||
for entry in sorted_entries:
|
for entry in sorted_entries:
|
||||||
name = entry.get("name", "")
|
name = entry.get("name", "")
|
||||||
enabled = entry.get("enabled", True)
|
enabled = entry.get("enabled", True)
|
||||||
@@ -835,13 +907,13 @@ class CowCliPlugin(Plugin):
|
|||||||
if desc:
|
if desc:
|
||||||
line += f"\n {desc}"
|
line += f"\n {desc}"
|
||||||
if source:
|
if source:
|
||||||
line += f"\n 来源: {source}"
|
line += _t(f"\n 来源: {source}", f"\n Source: {source}")
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
lines.append("💡 /skill list --remote 浏览技能广场")
|
lines.append(_t("💡 /skill list --remote: 浏览技能广场", "💡 /skill list --remote: Browse Skill Hub"))
|
||||||
lines.append("💡 /skill info <名称> 查看详情")
|
lines.append(_t("💡 /skill info <名称>: 查看详情", "💡 /skill info <name>: Show details"))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _skill_list(self, args: str) -> str:
|
def _skill_list(self, args: str) -> str:
|
||||||
@@ -871,43 +943,43 @@ class CowCliPlugin(Plugin):
|
|||||||
skills = data.get("skills", [])
|
skills = data.get("skills", [])
|
||||||
total = data.get("total", len(skills))
|
total = data.get("total", len(skills))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"获取技能广场失败: {e}"
|
return _t(f"获取技能广场失败: {e}", f"Failed to fetch Skill Hub: {e}")
|
||||||
|
|
||||||
if not skills and page == 1:
|
if not skills and page == 1:
|
||||||
return "技能广场暂无可用技能"
|
return _t("技能广场暂无可用技能", "No skills available on Skill Hub")
|
||||||
|
|
||||||
total_pages = max(1, (total + page_size - 1) // page_size)
|
total_pages = max(1, (total + page_size - 1) // page_size)
|
||||||
page = min(page, total_pages)
|
page = min(page, total_pages)
|
||||||
installed = set(load_skills_config().keys())
|
installed = set(load_skills_config().keys())
|
||||||
|
|
||||||
lines = ["🌐 技能广场", ""]
|
lines = [_t("🌐 技能广场", "🌐 Skill Hub"), ""]
|
||||||
for s in skills:
|
for s in skills:
|
||||||
name = s.get("name", "")
|
name = s.get("name", "")
|
||||||
display = s.get("display_name", "") or name
|
display = s.get("display_name", "") or name
|
||||||
desc = s.get("description", "")
|
desc = s.get("description", "")
|
||||||
if len(desc) > 50:
|
if len(desc) > 50:
|
||||||
desc = desc[:47] + "…"
|
desc = desc[:47] + "…"
|
||||||
badge = " [已安装]" if name in installed else ""
|
badge = _t(" [已安装]", " [installed]") if name in installed else ""
|
||||||
lines.append(f"📌 {display}{badge}")
|
lines.append(f"📌 {display}{badge}")
|
||||||
lines.append(f" 名称: {name}")
|
lines.append(_t(f" 名称: {name}", f" Name: {name}"))
|
||||||
if desc:
|
if desc:
|
||||||
lines.append(f" {desc}")
|
lines.append(f" {desc}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
lines.append(f"📄 第 {page}/{total_pages} 页")
|
lines.append(_t(f"📄 第 {page}/{total_pages} 页", f"📄 Page {page}/{total_pages}"))
|
||||||
if page < total_pages:
|
if page < total_pages:
|
||||||
lines.append(f"💡 /skill list --remote --page {page + 1} 下一页")
|
lines.append(_t(f"💡 /skill list --remote --page {page + 1}: 下一页", f"💡 /skill list --remote --page {page + 1}: Next page"))
|
||||||
if page > 1:
|
if page > 1:
|
||||||
lines.append(f"💡 /skill list --remote --page {page - 1} 上一页")
|
lines.append(_t(f"💡 /skill list --remote --page {page - 1}: 上一页", f"💡 /skill list --remote --page {page - 1}: Previous page"))
|
||||||
lines.append("💡 /skill install <名称> 安装技能")
|
lines.append(_t("💡 /skill install <名称>: 安装技能", "💡 /skill install <name>: Install a skill"))
|
||||||
lines.append("💡 /skill search <关键词> 搜索技能")
|
lines.append(_t("💡 /skill search <关键词>: 搜索技能", "💡 /skill search <keyword>: Search skills"))
|
||||||
lines.append("🌐 https://skills.cowagent.ai 在线浏览全部技能")
|
lines.append(_t("🌐 https://skills.cowagent.ai 在线浏览全部技能", "🌐 https://skills.cowagent.ai Browse all skills online"))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _skill_search(self, query: str) -> str:
|
def _skill_search(self, query: str) -> str:
|
||||||
if not query:
|
if not query:
|
||||||
return "请指定搜索关键词: /skill search <关键词>"
|
return _t("请指定搜索关键词: /skill search <关键词>", "Please specify a search keyword: /skill search <keyword>")
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from cli.utils import SKILL_HUB_API, load_skills_config
|
from cli.utils import SKILL_HUB_API, load_skills_config
|
||||||
@@ -916,35 +988,35 @@ class CowCliPlugin(Plugin):
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
skills = resp.json().get("skills", [])
|
skills = resp.json().get("skills", [])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"搜索失败: {e}"
|
return _t(f"搜索失败: {e}", f"Search failed: {e}")
|
||||||
|
|
||||||
if not skills:
|
if not skills:
|
||||||
return f"未找到与「{query}」相关的技能"
|
return _t(f"未找到与「{query}」相关的技能", f"No skills found for \"{query}\"")
|
||||||
|
|
||||||
installed = set(load_skills_config().keys())
|
installed = set(load_skills_config().keys())
|
||||||
lines = [f"🔍 搜索「{query}」({len(skills)} 个结果)", ""]
|
lines = [_t(f"🔍 搜索「{query}」({len(skills)} 个结果)", f"🔍 Search \"{query}\" ({len(skills)} results)"), ""]
|
||||||
for s in skills:
|
for s in skills:
|
||||||
name = s.get("name", "")
|
name = s.get("name", "")
|
||||||
display = s.get("display_name", "") or name
|
display = s.get("display_name", "") or name
|
||||||
desc = s.get("description", "")
|
desc = s.get("description", "")
|
||||||
if len(desc) > 50:
|
if len(desc) > 50:
|
||||||
desc = desc[:47] + "…"
|
desc = desc[:47] + "…"
|
||||||
badge = " [已安装]" if name in installed else ""
|
badge = _t(" [已安装]", " [installed]") if name in installed else ""
|
||||||
lines.append(f"📌 {display}{badge}")
|
lines.append(f"📌 {display}{badge}")
|
||||||
lines.append(f" 名称: {name}")
|
lines.append(_t(f" 名称: {name}", f" Name: {name}"))
|
||||||
if desc:
|
if desc:
|
||||||
lines.append(f" {desc}")
|
lines.append(f" {desc}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
lines.append("💡 /skill install <名称> 安装技能")
|
lines.append(_t("💡 /skill install <名称>: 安装技能", "💡 /skill install <name>: Install a skill"))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
_INSTALL_TIMEOUT = 60
|
_INSTALL_TIMEOUT = 60
|
||||||
|
|
||||||
def _skill_install(self, name: str, e_context: EventContext) -> str:
|
def _skill_install(self, name: str, e_context: EventContext) -> str:
|
||||||
if not name:
|
if not name:
|
||||||
return "请指定要安装的技能: /skill install <名称>"
|
return _t("请指定要安装的技能: /skill install <名称>", "Please specify a skill to install: /skill install <name>")
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
|
||||||
from cli.commands.skill import install_skill
|
from cli.commands.skill import install_skill
|
||||||
@@ -955,16 +1027,16 @@ class CowCliPlugin(Plugin):
|
|||||||
result = future.result(timeout=self._INSTALL_TIMEOUT)
|
result = future.result(timeout=self._INSTALL_TIMEOUT)
|
||||||
|
|
||||||
if result.error:
|
if result.error:
|
||||||
return f"安装失败: {result.error}"
|
return _t(f"安装失败: {result.error}", f"Install failed: {result.error}")
|
||||||
|
|
||||||
if not result.installed:
|
if not result.installed:
|
||||||
return "\n".join(result.messages) if result.messages else "未找到可安装的技能"
|
return "\n".join(result.messages) if result.messages else _t("未找到可安装的技能", "No installable skill found")
|
||||||
|
|
||||||
return self._format_install_result(result)
|
return self._format_install_result(result)
|
||||||
except FuturesTimeout:
|
except FuturesTimeout:
|
||||||
return "安装超时,请稍后重试或检查网络连接"
|
return _t("安装超时,请稍后重试或检查网络连接", "Install timed out. Please retry later or check your network connection.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"安装失败: {e}"
|
return _t(f"安装失败: {e}", f"Install failed: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_install_result(result) -> str:
|
def _format_install_result(result) -> str:
|
||||||
@@ -978,20 +1050,20 @@ class CowCliPlugin(Plugin):
|
|||||||
for skill_name in result.installed:
|
for skill_name in result.installed:
|
||||||
desc = _read_skill_description(os.path.join(skills_dir, skill_name))
|
desc = _read_skill_description(os.path.join(skills_dir, skill_name))
|
||||||
display = config.get(skill_name, {}).get("display_name", "")
|
display = config.get(skill_name, {}).get("display_name", "")
|
||||||
lines.append(f"✅ 技能安装成功:{skill_name}")
|
lines.append(_t(f"✅ 技能安装成功:{skill_name}", f"✅ Skill installed: {skill_name}"))
|
||||||
if display and display != skill_name:
|
if display and display != skill_name:
|
||||||
lines.append(f" 名称:{display}")
|
lines.append(_t(f" 名称:{display}", f" Name: {display}"))
|
||||||
if desc:
|
if desc:
|
||||||
lines.append(f" 描述:{desc}")
|
lines.append(_t(f" 描述:{desc}", f" Description: {desc}"))
|
||||||
|
|
||||||
if len(result.installed) > 1:
|
if len(result.installed) > 1:
|
||||||
lines.append(f"\n共安装 {len(result.installed)} 个技能")
|
lines.append(_t(f"\n共安装 {len(result.installed)} 个技能", f"\nInstalled {len(result.installed)} skills"))
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _skill_uninstall(self, name: str) -> str:
|
def _skill_uninstall(self, name: str) -> str:
|
||||||
if not name:
|
if not name:
|
||||||
return "请指定要卸载的技能: /skill uninstall <名称>"
|
return _t("请指定要卸载的技能: /skill uninstall <名称>", "Please specify a skill to uninstall: /skill uninstall <name>")
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
@@ -1004,7 +1076,7 @@ class CowCliPlugin(Plugin):
|
|||||||
skill_dir = self._resolve_skill_dir(name, skills_dir)
|
skill_dir = self._resolve_skill_dir(name, skills_dir)
|
||||||
|
|
||||||
if not skill_dir:
|
if not skill_dir:
|
||||||
return f"技能 '{name}' 未安装"
|
return _t(f"技能 '{name}' 未安装", f"Skill '{name}' is not installed")
|
||||||
|
|
||||||
shutil.rmtree(skill_dir)
|
shutil.rmtree(skill_dir)
|
||||||
|
|
||||||
@@ -1019,7 +1091,7 @@ class CowCliPlugin(Plugin):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return f"✅ 技能 '{name}' 已卸载"
|
return _t(f"✅ 技能 '{name}' 已卸载", f"✅ Skill '{name}' uninstalled")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_skill_dir(name: str, skills_dir: str):
|
def _resolve_skill_dir(name: str, skills_dir: str):
|
||||||
@@ -1055,7 +1127,7 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
def _skill_info(self, name: str) -> str:
|
def _skill_info(self, name: str) -> str:
|
||||||
if not name:
|
if not name:
|
||||||
return "请指定技能名称: /skill info <名称>"
|
return _t("请指定技能名称: /skill info <名称>", "Please specify a skill name: /skill info <name>")
|
||||||
|
|
||||||
from cli.utils import get_skills_dir, get_builtin_skills_dir
|
from cli.utils import get_skills_dir, get_builtin_skills_dir
|
||||||
|
|
||||||
@@ -1078,18 +1150,18 @@ class CowCliPlugin(Plugin):
|
|||||||
source = "custom"
|
source = "custom"
|
||||||
|
|
||||||
if not skill_dir:
|
if not skill_dir:
|
||||||
return f"技能 '{name}' 未找到"
|
return _t(f"技能 '{name}' 未找到", f"Skill '{name}' not found")
|
||||||
|
|
||||||
skill_md = os.path.join(skill_dir, "SKILL.md")
|
skill_md = os.path.join(skill_dir, "SKILL.md")
|
||||||
if not os.path.exists(skill_md):
|
if not os.path.exists(skill_md):
|
||||||
return f"技能 '{name}' 没有 SKILL.md 文件"
|
return _t(f"技能 '{name}' 没有 SKILL.md 文件", f"Skill '{name}' has no SKILL.md file")
|
||||||
|
|
||||||
with open(skill_md, "r", encoding="utf-8") as f:
|
with open(skill_md, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
meta, body = self._strip_frontmatter(content)
|
meta, body = self._strip_frontmatter(content)
|
||||||
|
|
||||||
header_lines = [f"📖 技能: {name} [{source}]", ""]
|
header_lines = [_t(f"📖 技能: {name} [{source}]", f"📖 Skill: {name} [{source}]"), ""]
|
||||||
desc = meta.get("description", "")
|
desc = meta.get("description", "")
|
||||||
if desc:
|
if desc:
|
||||||
header_lines.append(f" {desc}")
|
header_lines.append(f" {desc}")
|
||||||
@@ -1104,8 +1176,10 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
def _skill_set_enabled(self, name: str, enabled: bool) -> str:
|
def _skill_set_enabled(self, name: str, enabled: bool) -> str:
|
||||||
if not name:
|
if not name:
|
||||||
action = "启用" if enabled else "禁用"
|
return _t(
|
||||||
return f"请指定技能名称: /skill {'enable' if enabled else 'disable'} <名称>"
|
f"请指定技能名称: /skill {'enable' if enabled else 'disable'} <名称>",
|
||||||
|
f"Please specify a skill name: /skill {'enable' if enabled else 'disable'} <name>",
|
||||||
|
)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from cli.utils import get_skills_dir
|
from cli.utils import get_skills_dir
|
||||||
@@ -1114,24 +1188,25 @@ class CowCliPlugin(Plugin):
|
|||||||
config_path = os.path.join(skills_dir, "skills_config.json")
|
config_path = os.path.join(skills_dir, "skills_config.json")
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
return "技能配置文件不存在"
|
return _t("技能配置文件不存在", "Skills config file not found")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_path, "r", encoding="utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"读取配置失败: {e}"
|
return _t(f"读取配置失败: {e}", f"Failed to read config: {e}")
|
||||||
|
|
||||||
if name not in config:
|
if name not in config:
|
||||||
return f"技能 '{name}' 未在配置中找到"
|
return _t(f"技能 '{name}' 未在配置中找到", f"Skill '{name}' not found in config")
|
||||||
|
|
||||||
config[name]["enabled"] = enabled
|
config[name]["enabled"] = enabled
|
||||||
with open(config_path, "w", encoding="utf-8") as f:
|
with open(config_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
action = "启用" if enabled else "禁用"
|
|
||||||
icon = "✅" if enabled else "⬚"
|
icon = "✅" if enabled else "⬚"
|
||||||
return f"{icon} 技能 '{name}' 已{action}"
|
if enabled:
|
||||||
|
return _t(f"{icon} 技能 '{name}' 已启用", f"{icon} Skill '{name}' enabled")
|
||||||
|
return _t(f"{icon} 技能 '{name}' 已禁用", f"{icon} Skill '{name}' disabled")
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# memory
|
# memory
|
||||||
@@ -1157,13 +1232,19 @@ class CowCliPlugin(Plugin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _memory_help() -> str:
|
def _memory_help() -> str:
|
||||||
return (
|
return _t(
|
||||||
"🧠 记忆管理\n\n"
|
"🧠 记忆管理\n\n"
|
||||||
"用法: /memory <子命令>\n\n"
|
"用法: /memory <子命令>\n\n"
|
||||||
"子命令:\n"
|
"子命令:\n"
|
||||||
" status 查看索引状态 (provider / model / dim / chunks)\n"
|
"status: 查看索引状态 (provider / model / dim / chunks)\n"
|
||||||
" rebuild-index 清空并重建向量索引 (切换 embedding 模型后必须执行)\n"
|
"rebuild-index: 清空并重建向量索引 (切换 embedding 模型后必须执行)\n"
|
||||||
" dream [N] 手动触发记忆蒸馏 (整理近N天, 默认3, 最多30)"
|
"dream [N]: 手动触发记忆蒸馏 (整理近N天, 默认3, 最多30)",
|
||||||
|
"🧠 Memory Management\n\n"
|
||||||
|
"Usage: /memory <subcommand>\n\n"
|
||||||
|
"Subcommands:\n"
|
||||||
|
"status: Show index status (provider / model / dim / chunks)\n"
|
||||||
|
"rebuild-index: Rebuild the vector index (required after switching embedding model)\n"
|
||||||
|
"dream [N]: Trigger memory distillation (last N days, default 3, max 30)",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _memory_dream(self, days: int, e_context, session_id: str) -> str:
|
def _memory_dream(self, days: int, e_context, session_id: str) -> str:
|
||||||
@@ -1178,10 +1259,10 @@ class CowCliPlugin(Plugin):
|
|||||||
try:
|
try:
|
||||||
flush_mgr = self._create_standalone_flush_manager()
|
flush_mgr = self._create_standalone_flush_manager()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"⚠️ 无法初始化记忆蒸馏: {e}"
|
return _t(f"⚠️ 无法初始化记忆蒸馏: {e}", f"⚠️ Failed to initialize memory distillation: {e}")
|
||||||
|
|
||||||
if not flush_mgr.llm_model:
|
if not flush_mgr.llm_model:
|
||||||
return "⚠️ 未配置 LLM 模型,无法执行记忆蒸馏"
|
return _t("⚠️ 未配置 LLM 模型,无法执行记忆蒸馏", "⚠️ No LLM model configured, cannot run memory distillation")
|
||||||
|
|
||||||
# SaaS (e_context is None): run synchronously, return full result
|
# SaaS (e_context is None): run synchronously, return full result
|
||||||
if e_context is None:
|
if e_context is None:
|
||||||
@@ -1196,13 +1277,16 @@ class CowCliPlugin(Plugin):
|
|||||||
if result:
|
if result:
|
||||||
self._notify(e_context, self._build_dream_result(flush_mgr, is_web))
|
self._notify(e_context, self._build_dream_result(flush_mgr, is_web))
|
||||||
else:
|
else:
|
||||||
self._notify(e_context, "💤 记忆蒸馏跳过 — 没有新的记忆内容需要整理")
|
self._notify(e_context, _t("💤 记忆蒸馏跳过 — 没有新的记忆内容需要整理", "💤 Memory distillation skipped — no new memories to process"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[CowCli] /memory dream failed: {e}")
|
logger.warning(f"[CowCli] /memory dream failed: {e}")
|
||||||
self._notify(e_context, f"❌ 记忆蒸馏失败: {e}")
|
self._notify(e_context, _t(f"❌ 记忆蒸馏失败: {e}", f"❌ Memory distillation failed: {e}"))
|
||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
return f"🌙 记忆蒸馏已启动 (整理近 {days} 天的记忆)\n\n整理在后台执行,完成后会通知你。"
|
return _t(
|
||||||
|
f"🌙 记忆蒸馏已启动 (整理近 {days} 天的记忆)\n\n整理在后台执行,完成后会通知你。",
|
||||||
|
f"🌙 Memory distillation started (processing the last {days} days)\n\nRunning in the background; you'll be notified when it's done.",
|
||||||
|
)
|
||||||
|
|
||||||
def _memory_dream_sync(self, flush_mgr, days: int) -> str:
|
def _memory_dream_sync(self, flush_mgr, days: int) -> str:
|
||||||
"""Run deep dream synchronously and return the full result."""
|
"""Run deep dream synchronously and return the full result."""
|
||||||
@@ -1210,10 +1294,10 @@ class CowCliPlugin(Plugin):
|
|||||||
result = flush_mgr.deep_dream(lookback_days=days, force=True)
|
result = flush_mgr.deep_dream(lookback_days=days, force=True)
|
||||||
if result:
|
if result:
|
||||||
return self._build_dream_result(flush_mgr, is_web=True)
|
return self._build_dream_result(flush_mgr, is_web=True)
|
||||||
return "💤 记忆蒸馏跳过 — 没有新的记忆内容需要整理"
|
return _t("💤 记忆蒸馏跳过 — 没有新的记忆内容需要整理", "💤 Memory distillation skipped — no new memories to process")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[CowCli] /memory dream sync failed: {e}")
|
logger.warning(f"[CowCli] /memory dream sync failed: {e}")
|
||||||
return f"❌ 记忆蒸馏失败: {e}"
|
return _t(f"❌ 记忆蒸馏失败: {e}", f"❌ Memory distillation failed: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_active_embedding():
|
def _resolve_active_embedding():
|
||||||
@@ -1255,9 +1339,9 @@ class CowCliPlugin(Plugin):
|
|||||||
agent = self._get_agent("")
|
agent = self._get_agent("")
|
||||||
memory_manager = agent.memory_manager if agent else None
|
memory_manager = agent.memory_manager if agent else None
|
||||||
|
|
||||||
lines = ["🧠 记忆索引状态", ""]
|
lines = [_t("🧠 记忆索引状态", "🧠 Memory Index Status"), ""]
|
||||||
if not memory_manager:
|
if not memory_manager:
|
||||||
lines.append(" ⚠️ Agent 尚未初始化,先发一条普通消息再试")
|
lines.append(_t(" ⚠️ Agent 尚未初始化,先发一条普通消息再试", " ⚠️ Agent not initialized yet, send a normal message first"))
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
stats = memory_manager.storage.get_stats()
|
stats = memory_manager.storage.get_stats()
|
||||||
@@ -1278,7 +1362,7 @@ class CowCliPlugin(Plugin):
|
|||||||
lines.append(f" Model : {cfg_model}")
|
lines.append(f" Model : {cfg_model}")
|
||||||
lines.append(f" Dim : {cfg_dim if cfg_dim else '?'}")
|
lines.append(f" Dim : {cfg_dim if cfg_dim else '?'}")
|
||||||
else:
|
else:
|
||||||
lines.append(" Provider : (未初始化, keyword-only)")
|
lines.append(_t(" Provider : (未初始化, keyword-only)", " Provider : (not initialized, keyword-only)"))
|
||||||
|
|
||||||
# Health hints — only shown when the user has explicitly opted into
|
# Health hints — only shown when the user has explicitly opted into
|
||||||
# vector search via `embedding_provider`. Legacy users (no explicit
|
# vector search via `embedding_provider`. Legacy users (no explicit
|
||||||
@@ -1289,17 +1373,17 @@ class CowCliPlugin(Plugin):
|
|||||||
if explicitly_opted_in and provider_obj is not None:
|
if explicitly_opted_in and provider_obj is not None:
|
||||||
if chunks > 0 and embedded < chunks:
|
if chunks > 0 and embedded < chunks:
|
||||||
missing = chunks - embedded
|
missing = chunks - embedded
|
||||||
warnings.append(
|
warnings.append(_t(
|
||||||
f" ⚠️ {missing}/{chunks} 个 chunk 没有向量;"
|
f" ⚠️ {missing}/{chunks} 个 chunk 没有向量;运行 /memory rebuild-index 后所有记忆才会被向量化检索",
|
||||||
f"运行 /memory rebuild-index 后所有记忆才会被向量化检索"
|
f" ⚠️ {missing}/{chunks} chunks have no vectors; run /memory rebuild-index to enable vector search for all memories",
|
||||||
)
|
))
|
||||||
|
|
||||||
index_dim = detect_index_dim(memory_manager.storage)
|
index_dim = detect_index_dim(memory_manager.storage)
|
||||||
if index_dim is not None and cfg_dim and index_dim != cfg_dim:
|
if index_dim is not None and cfg_dim and index_dim != cfg_dim:
|
||||||
warnings.append(
|
warnings.append(_t(
|
||||||
f" ⚠️ 索引中存量向量为 {index_dim} 维,与当前配置 {cfg_dim} 维不一致;"
|
f" ⚠️ 索引中存量向量为 {index_dim} 维,与当前配置 {cfg_dim} 维不一致;运行 /memory rebuild-index 重建后向量检索才会生效",
|
||||||
f"运行 /memory rebuild-index 重建后向量检索才会生效"
|
f" ⚠️ Existing vectors are {index_dim}-dim, mismatching the current {cfg_dim}-dim config; run /memory rebuild-index to make vector search work",
|
||||||
)
|
))
|
||||||
|
|
||||||
if warnings:
|
if warnings:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -1312,9 +1396,11 @@ class CowCliPlugin(Plugin):
|
|||||||
session_id = self._get_session_id(e_context, fallback=session_id)
|
session_id = self._get_session_id(e_context, fallback=session_id)
|
||||||
agent = self._get_agent(session_id)
|
agent = self._get_agent(session_id)
|
||||||
if not agent or not agent.memory_manager:
|
if not agent or not agent.memory_manager:
|
||||||
return (
|
return _t(
|
||||||
"⚠️ Agent 尚未初始化,无法重建索引。\n"
|
"⚠️ Agent 尚未初始化,无法重建索引。\n"
|
||||||
"请先发送一条普通消息触发 Agent 启动后再试。"
|
"请先发送一条普通消息触发 Agent 启动后再试。",
|
||||||
|
"⚠️ Agent not initialized, cannot rebuild the index.\n"
|
||||||
|
"Send a normal message first to start the Agent, then try again.",
|
||||||
)
|
)
|
||||||
|
|
||||||
memory_manager = agent.memory_manager
|
memory_manager = agent.memory_manager
|
||||||
@@ -1328,12 +1414,14 @@ class CowCliPlugin(Plugin):
|
|||||||
._init_embedding_provider(memory_manager.config, session_id=session_id)
|
._init_embedding_provider(memory_manager.config, session_id=session_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("[CowCli] /memory rebuild-index: build provider failed")
|
logger.exception("[CowCli] /memory rebuild-index: build provider failed")
|
||||||
return f"⚠️ 无法根据当前配置构造 embedding provider: {e}"
|
return _t(f"⚠️ 无法根据当前配置构造 embedding provider: {e}", f"⚠️ Failed to build embedding provider from current config: {e}")
|
||||||
|
|
||||||
if fresh_provider is None:
|
if fresh_provider is None:
|
||||||
return (
|
return _t(
|
||||||
"⚠️ 当前没有可用的 embedding provider。\n"
|
"⚠️ 当前没有可用的 embedding provider。\n"
|
||||||
"请检查 config.json 中的 embedding 相关配置 (provider / api key)。"
|
"请检查 config.json 中的 embedding 相关配置 (provider / api key)。",
|
||||||
|
"⚠️ No embedding provider available.\n"
|
||||||
|
"Check the embedding settings in config.json (provider / api key).",
|
||||||
)
|
)
|
||||||
memory_manager.embedding_provider = fresh_provider
|
memory_manager.embedding_provider = fresh_provider
|
||||||
|
|
||||||
@@ -1353,23 +1441,29 @@ class CowCliPlugin(Plugin):
|
|||||||
if result.ok:
|
if result.ok:
|
||||||
self._notify(
|
self._notify(
|
||||||
e_context,
|
e_context,
|
||||||
(
|
_t(
|
||||||
f"✅ 索引重建完成\n"
|
f"✅ 索引重建完成\n"
|
||||||
f" cleared : {result.removed}\n"
|
f" cleared : {result.removed}\n"
|
||||||
f" chunks : {result.chunks}\n"
|
f" chunks : {result.chunks}\n"
|
||||||
f" files : {result.files}"
|
f" files : {result.files}",
|
||||||
|
f"✅ Index rebuild complete\n"
|
||||||
|
f" cleared : {result.removed}\n"
|
||||||
|
f" chunks : {result.chunks}\n"
|
||||||
|
f" files : {result.files}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._notify(e_context, f"❌ 索引重建失败: {result.error}")
|
self._notify(e_context, _t(f"❌ 索引重建失败: {result.error}", f"❌ Index rebuild failed: {result.error}"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("[CowCli] /memory rebuild-index failed")
|
logger.exception("[CowCli] /memory rebuild-index failed")
|
||||||
self._notify(e_context, f"❌ 索引重建失败: {e}")
|
self._notify(e_context, _t(f"❌ 索引重建失败: {e}", f"❌ Index rebuild failed: {e}"))
|
||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
return (
|
return _t(
|
||||||
f"🔧 索引重建已启动 (model={model_label}, dim={dim_label})\n\n"
|
f"🔧 索引重建已启动 (model={model_label}, dim={dim_label})\n\n"
|
||||||
f"将重新向量化所有记忆和知识文件,完成后会通知你。"
|
f"将重新向量化所有记忆和知识文件,完成后会通知你。",
|
||||||
|
f"🔧 Index rebuild started (model={model_label}, dim={dim_label})\n\n"
|
||||||
|
f"Re-vectorizing all memory and knowledge files; you'll be notified when done.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1380,15 +1474,19 @@ class CowCliPlugin(Plugin):
|
|||||||
result = rebuild_in_process(memory_manager)
|
result = rebuild_in_process(memory_manager)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("[CowCli] /memory rebuild-index sync failed")
|
logger.exception("[CowCli] /memory rebuild-index sync failed")
|
||||||
return f"❌ 索引重建失败: {e}"
|
return _t(f"❌ 索引重建失败: {e}", f"❌ Index rebuild failed: {e}")
|
||||||
|
|
||||||
if not result.ok:
|
if not result.ok:
|
||||||
return f"❌ 索引重建失败: {result.error}"
|
return _t(f"❌ 索引重建失败: {result.error}", f"❌ Index rebuild failed: {result.error}")
|
||||||
return (
|
return _t(
|
||||||
f"✅ 索引重建完成 (model={model_label}, dim={dim_label})\n"
|
f"✅ 索引重建完成 (model={model_label}, dim={dim_label})\n"
|
||||||
f" cleared : {result.removed}\n"
|
f" cleared : {result.removed}\n"
|
||||||
f" chunks : {result.chunks}\n"
|
f" chunks : {result.chunks}\n"
|
||||||
f" files : {result.files}"
|
f" files : {result.files}",
|
||||||
|
f"✅ Index rebuild complete (model={model_label}, dim={dim_label})\n"
|
||||||
|
f" cleared : {result.removed}\n"
|
||||||
|
f" chunks : {result.chunks}\n"
|
||||||
|
f" files : {result.files}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1418,7 +1516,7 @@ class CowCliPlugin(Plugin):
|
|||||||
def _build_dream_result(flush_mgr, is_web: bool) -> str:
|
def _build_dream_result(flush_mgr, is_web: bool) -> str:
|
||||||
"""Build dream completion message with diary content."""
|
"""Build dream completion message with diary content."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
lines = ["✅ 记忆蒸馏完成"]
|
lines = [_t("✅ 记忆蒸馏完成", "✅ Memory distillation complete")]
|
||||||
|
|
||||||
# Read today's dream diary
|
# Read today's dream diary
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
today = datetime.now().strftime("%Y-%m-%d")
|
||||||
@@ -1433,9 +1531,9 @@ class CowCliPlugin(Plugin):
|
|||||||
lines.append(f"\n{diary}")
|
lines.append(f"\n{diary}")
|
||||||
|
|
||||||
if is_web:
|
if is_web:
|
||||||
lines.append("\n[MEMORY.md](/memory/MEMORY.md) | [梦境日记](/memory/dreams)")
|
lines.append(_t("\n[MEMORY.md](/memory/MEMORY.md) | [梦境日记](/memory/dreams)", "\n[MEMORY.md](/memory/MEMORY.md) | [Dream Diary](/memory/dreams)"))
|
||||||
else:
|
else:
|
||||||
lines.append("\nMEMORY.md 已更新")
|
lines.append(_t("\nMEMORY.md 已更新", "\nMEMORY.md updated"))
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -1485,11 +1583,17 @@ class CowCliPlugin(Plugin):
|
|||||||
with open(config_path, "w", encoding="utf-8") as f:
|
with open(config_path, "w", encoding="utf-8") as f:
|
||||||
_json.dump(file_config, f, indent=4, ensure_ascii=False)
|
_json.dump(file_config, f, indent=4, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"⚠️ 内存中已切换,但写入 config.json 失败: {e}"
|
return _t(f"⚠️ 内存中已切换,但写入 config.json 失败: {e}", f"⚠️ Switched in memory, but failed to write config.json: {e}")
|
||||||
|
|
||||||
status = "开启 ✅" if enabled else "关闭 ❌"
|
if enabled:
|
||||||
note = "知识库将在下次对话中生效" if enabled else "知识库系统已停用,不再注入提示词和索引知识文件"
|
return _t(
|
||||||
return f"📚 知识库已{status}\n\n{note}"
|
"📚 知识库已开启 ✅\n\n知识库将在下次对话中生效",
|
||||||
|
"📚 Knowledge base enabled ✅\n\nIt will take effect in the next conversation",
|
||||||
|
)
|
||||||
|
return _t(
|
||||||
|
"📚 知识库已关闭 ❌\n\n知识库系统已停用,不再注入提示词和索引知识文件",
|
||||||
|
"📚 Knowledge base disabled ❌\n\nThe knowledge system is off; no prompt injection or file indexing",
|
||||||
|
)
|
||||||
|
|
||||||
def _knowledge_stats(self) -> str:
|
def _knowledge_stats(self) -> str:
|
||||||
from config import conf
|
from config import conf
|
||||||
@@ -1499,7 +1603,7 @@ class CowCliPlugin(Plugin):
|
|||||||
"knowledge"
|
"knowledge"
|
||||||
)
|
)
|
||||||
if not os.path.isdir(knowledge_dir):
|
if not os.path.isdir(knowledge_dir):
|
||||||
return "📚 知识库目录不存在\n\n💡 开启知识库: /knowledge on"
|
return _t("📚 知识库目录不存在\n\n💡 开启知识库: /knowledge on", "📚 Knowledge base directory not found\n\n💡 Enable it: /knowledge on")
|
||||||
|
|
||||||
enabled = conf().get("knowledge", True)
|
enabled = conf().get("knowledge", True)
|
||||||
total_files = 0
|
total_files = 0
|
||||||
@@ -1516,13 +1620,13 @@ class CowCliPlugin(Plugin):
|
|||||||
total_bytes += os.path.getsize(os.path.join(root, f))
|
total_bytes += os.path.getsize(os.path.join(root, f))
|
||||||
cat_count[category] = cat_count.get(category, 0) + 1
|
cat_count[category] = cat_count.get(category, 0) + 1
|
||||||
|
|
||||||
status = "✅ 已开启" if enabled else "❌ 已关闭"
|
status = _t("✅ 已开启", "✅ Enabled") if enabled else _t("❌ 已关闭", "❌ Disabled")
|
||||||
lines = [
|
lines = [
|
||||||
"📚 知识库统计",
|
_t("📚 知识库统计", "📚 Knowledge Base Stats"),
|
||||||
"",
|
"",
|
||||||
f"状态: {status}",
|
_t(f"状态: {status}", f"Status: {status}"),
|
||||||
f"页面: {total_files} 篇",
|
_t(f"页面: {total_files} 篇", f"Pages: {total_files}"),
|
||||||
f"大小: {total_bytes / 1024:.1f} KB",
|
_t(f"大小: {total_bytes / 1024:.1f} KB", f"Size: {total_bytes / 1024:.1f} KB"),
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
if cat_count:
|
if cat_count:
|
||||||
@@ -1530,12 +1634,12 @@ class CowCliPlugin(Plugin):
|
|||||||
lines.append(f"- {cat}/ ({cat_count[cat]} pages)")
|
lines.append(f"- {cat}/ ({cat_count[cat]} pages)")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append(f"路径: {knowledge_dir}")
|
lines.append(_t(f"路径: {knowledge_dir}", f"Path: {knowledge_dir}"))
|
||||||
lines.extend([
|
lines.extend([
|
||||||
"",
|
"",
|
||||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
||||||
"💡 /knowledge list 查看文件树",
|
_t("💡 /knowledge list: 查看文件树", "💡 /knowledge list: Show file tree"),
|
||||||
"💡 /knowledge on|off 开关知识库",
|
_t("💡 /knowledge on|off: 开关知识库", "💡 /knowledge on|off: Toggle knowledge base"),
|
||||||
])
|
])
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@@ -1547,7 +1651,7 @@ class CowCliPlugin(Plugin):
|
|||||||
"knowledge"
|
"knowledge"
|
||||||
)
|
)
|
||||||
if not os.path.isdir(knowledge_dir):
|
if not os.path.isdir(knowledge_dir):
|
||||||
return "📚 知识库目录不存在\n\n💡 开启知识库: /knowledge on"
|
return _t("📚 知识库目录不存在\n\n💡 开启知识库: /knowledge on", "📚 Knowledge base directory not found\n\n💡 Enable it: /knowledge on")
|
||||||
|
|
||||||
tree = ["knowledge/"]
|
tree = ["knowledge/"]
|
||||||
|
|
||||||
@@ -1577,7 +1681,7 @@ class CowCliPlugin(Plugin):
|
|||||||
tree.append(f"{child_prefix}└── ... +{len(md_files) - max_show} more")
|
tree.append(f"{child_prefix}└── ... +{len(md_files) - max_show} more")
|
||||||
|
|
||||||
if not subdirs:
|
if not subdirs:
|
||||||
tree.append("(空)")
|
tree.append(_t("(空)", "(empty)"))
|
||||||
|
|
||||||
return "```\n" + "\n".join(tree) + "\n```"
|
return "```\n" + "\n".join(tree) + "\n```"
|
||||||
|
|
||||||
@@ -1602,4 +1706,4 @@ class CowCliPlugin(Plugin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_help_text(self, **kwargs):
|
def get_help_text(self, **kwargs):
|
||||||
return "在对话中使用 /help 或 cow help 查看可用命令"
|
return _t("在对话中使用 /help 或 cow help 查看可用命令", "Use /help or cow help in chat to see available commands")
|
||||||
|
|||||||
Reference in New Issue
Block a user