Merge pull request #2935 from anomixer/master

feat(i18n): 新增繁體中文 (zh-tw) 支援,並新增 Web 登出按鈕與動態顯示
This commit is contained in:
zhayujie
2026-07-08 10:42:05 +08:00
committed by GitHub
8 changed files with 843 additions and 33 deletions

View File

@@ -12,7 +12,7 @@
</p> </p>
<p align="center"> <p align="center">
[English] | [<a href="docs/zh/README.md">中文</a>] | [<a href="docs/ja/README.md">日本語</a>] [English] | [<a href="docs/zh/README.md">中文</a>] | [<a href="docs/zh/README-Hant.md">繁體中文</a>] | [<a href="docs/ja/README.md">日本語</a>]
</p> </p>
**CowAgent** is an open-source super AI assistant that proactively plans tasks, controls your computer and external services, creates and runs Skills, builds a personal knowledge base and long-term memory, and grows alongside you through self-evolution — a reference implementation of Agent Harness engineering. **CowAgent** is an open-source super AI assistant that proactively plans tasks, controls your computer and external services, creates and runs Skills, builds a personal knowledge base and long-term memory, and grows alongside you through self-evolution — a reference implementation of Agent Harness engineering.

View File

@@ -47,12 +47,15 @@
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', // Map an arbitrary locale string (zh-CN, en-US, fr ...) to 'zh' / 'zh-Hant' / 'en',
// or '' when unrecognized so callers can fall through to the next source. // or '' when unrecognized so callers can fall through to the next source.
window.__cowNormalizeLang__ = function(raw) { window.__cowNormalizeLang__ = function(raw) {
if (!raw) return ''; if (!raw) return '';
var v = String(raw).trim().toLowerCase(); var v = String(raw).trim().toLowerCase().replace('_', '-');
if (v === 'auto') return ''; if (v === 'auto') return '';
// Handle Traditional Chinese variants first (more specific)
if (v === 'zh-hant' || v.indexOf('zh-hant-') === 0 || v === 'zh-tw' || v === 'zh-hk') return 'zh-Hant';
// Then Simplified Chinese
if (v.indexOf('zh') === 0) return 'zh'; if (v.indexOf('zh') === 0) return 'zh';
if (v.indexOf('en') === 0) return 'en'; if (v.indexOf('en') === 0) return 'en';
return ''; return '';
@@ -304,6 +307,14 @@
cursor-pointer transition-colors duration-150" title="GitHub"> cursor-pointer transition-colors duration-150" title="GitHub">
<i class="fab fa-github text-lg"></i> <i class="fab fa-github text-lg"></i>
</a> </a>
<!-- Logout Button (hidden by default) -->
<button id="logout-btn-header" class="p-2 rounded-lg text-slate-500 dark:text-slate-400
hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 dark:hover:text-red-400
cursor-pointer transition-colors duration-150 hidden"
onclick="handleLogout()" title="Logout" data-i18n-title="logout">
<i class="fas fa-sign-out-alt text-base"></i>
</button>
</header> </header>
<!-- Content Area --> <!-- Content Area -->

View File

@@ -147,8 +147,9 @@ const I18N = {
config_custom_tip: '接口需遵循 OpenAI API 协议', config_custom_tip: '接口需遵循 OpenAI API 协议',
config_security: '安全设置', config_password: '访问密码', config_security: '安全设置', config_password: '访问密码',
config_password_hint: '留空则不启用密码保护', config_password_hint: '留空则不启用密码保护',
config_password_changed: '密码已更新,请重新登录', config_password_changed: '密码已更新',
config_password_cleared: '密码已清除', config_password_cleared: '密码已清除',
config_password_security_warning: '⚠️ 警告:目前密码为空且对外连接埠开放,建议重启服务,或检查是否调整监听位址绑定。',
skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能广场', skills_title: '技能管理', skills_desc: '查看、启用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能广场',
skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处', skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处',
tools_section_title: '内置工具', tools_loading: '加载工具中...', tools_section_title: '内置工具', tools_loading: '加载工具中...',
@@ -254,6 +255,254 @@ const I18N = {
regenerate_response: '重新生成', regenerate_response: '重新生成',
edit_save: '保存并发送', edit_save: '保存并发送',
edit_cancel: '取消', edit_cancel: '取消',
logout: '退出',
},
'zh-Hant': {
console: '控制台',
nav_chat: '對話', nav_manage: '管理', nav_monitor: '監控',
menu_chat: '對話', menu_config: '設定', menu_models: '模型', menu_skills: '技能',
menu_memory: '記憶', menu_knowledge: '知識', menu_channels: '管道', menu_tasks: '定時',
menu_logs: '日誌',
models_title: '模型管理',
models_desc: '統一管理對話、影像、語音、向量、搜尋能力',
models_section_vendors: '廠商憑據',
models_section_vendors_desc: '一處設定,多個模型能力共享',
models_section_capabilities: '模型能力',
models_add_vendor: '新增廠商',
models_provider: '廠商',
models_model: '模型',
models_voice: '音色',
models_configured: '已設定',
models_not_configured: '未設定',
models_pick_to_configure: '選擇以設定',
models_clear_credential: '清除憑據',
models_base_default_hint: '留空將使用官方預設地址',
models_base_default: '預設',
models_custom_vendor_label: '自定義',
models_custom_name: '名稱',
models_custom_delete: '刪除',
models_custom_delete_confirm_title: '刪除自定義廠商',
models_custom_delete_confirm_msg: '確定刪除該自定義廠商嗎?此操作無法撤銷。',
models_custom_name_required: '請填寫名稱',
models_custom_base_required: '請填寫 API Base',
models_custom_edit_title: '編輯自定義廠商',
models_custom_add_title: '新增自定義廠商',
models_capability_chat: '主模型',
models_capability_chat_desc: '用於基礎對話和 Agent 推理',
models_capability_vision: '影像理解',
models_capability_vision_desc: '識別圖片內容,用於影像識別工具',
models_capability_image: '影像生成',
models_capability_image_desc: '生成圖片,用於影像生成技能',
models_auto_using: '當前優先使用',
models_capability_asr: '語音識別',
models_capability_asr_desc: '語音轉文字',
models_capability_tts: '語音合成',
models_capability_tts_desc: '文字轉語音',
models_capability_embedding: '向量',
models_capability_embedding_desc: '用於記憶與知識的向量化檢索',
models_capability_search: '聯網搜尋',
models_capability_search_desc: '實時網頁檢索能力,用於搜尋工具',
models_strategy_auto: '自動',
models_search_strategy_label: '策略',
models_search_strategy_fixed: '指定',
models_search_strategy_auto_hint: '從已設定廠商中自動選擇',
models_search_strategy_fixed_hint: '指定使用搜尋廠商',
models_pending_config: '待設定',
models_search_available_label: '可用搜尋廠商:',
models_search_none_configured: '暫未啟用任何搜尋廠商,點選新增',
models_search_add_provider: '新增廠商',
models_search_add_desc: '選擇一個搜尋廠商進行設定',
models_search_bocha_title: '設定博查 API Key',
models_search_bocha_desc: '前往博查開放平臺建立 API Key',
models_search_edit_hint: '點選修改設定',
models_unavailable: '不可用',
models_set_via_env: '透過環境變數啟用',
models_dim_label: '維度',
models_save_success: '已儲存',
models_save_failed: '儲存失敗',
models_cleared: '已清除',
models_clear_failed: '清除失敗',
models_embedding_change_title: '更改向量模型',
models_embedding_change_msg: '切換向量模型後,已有索引將失效,需要重建。是否繼續?',
models_embedding_saved_title: '向量模型已更新',
models_embedding_saved_msg: '請在聊天框輸入 /memory rebuild-index 重建索引。',
models_embedding_saved_ok: '去執行',
models_pick_provider: '待選擇',
models_clear_confirm_title: '清除廠商憑據',
models_clear_confirm_msg: '確認清除該廠商的 API Key 與 Base URL 嗎?相關能力將不再可用。',
cancel: '取消',
save: '儲存',
ok: '確定',
knowledge_title: '知識庫', knowledge_desc: '瀏覽和探索你的知識庫',
knowledge_tab_docs: '檔案', knowledge_tab_graph: '圖譜',
knowledge_loading: '載入知識庫中...', knowledge_loading_desc: '知識頁面將顯示在這裡',
knowledge_select_hint: '選擇一個檔案檢視', knowledge_empty_hint: '暫無知識頁面',
knowledge_empty_guide: '在對話中傳送檔案、連結或主題給 Agent它會自動整理到你的知識庫中。',
knowledge_go_chat: '開始對話',
knowledge_new: '新建',
knowledge_new_category: '新建分類',
knowledge_new_document: '新建檔案',
knowledge_import_documents: '匯入檔案',
welcome_subtitle: '我可以幫你解答問題、管理電腦、創造和執行技能,並透過<br>長期記憶和知識庫不斷成長',
example_sys_title: '系統管理', example_sys_text: '檢視工作空間裡有哪些檔案',
example_task_title: '定時任務', example_task_text: '1分鐘後提醒我檢查伺服器',
example_code_title: '程式設計助手', example_code_text: '搜尋AI資訊並生成視覺化網頁報告',
example_knowledge_title: '知識庫', example_knowledge_text: '檢視知識庫當前檔案情況',
example_skill_title: '技能系統', example_skill_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: '輸入訊息,或輸入 / 使用指令',
config_title: '設定管理', config_desc: '管理模型和 Agent 設定',
config_model: '模型設定', config_agent: 'Agent 設定',
config_language: '語言', config_language_hint: '介面展示、命令文案、系統提示詞等使用的語言(與右上角切換同步)',
config_model_advanced: '高階設定',
config_channel: '管道設定',
config_agent_enabled: 'Agent 模式',
config_max_tokens: '最大上下文 Token', config_max_tokens_hint: '對話中 Agent 能輸入的最大 Token 長度,超過後會智慧壓縮處理',
config_max_turns: '最大記憶輪次', config_max_turns_hint: '一問一答為一輪,超過後會智慧壓縮處理',
config_max_steps: '最大執行步數', config_max_steps_hint: '單次對話中 Agent 最多呼叫工具的次數',
config_enable_thinking: '深度思考', config_enable_thinking_hint: '是否啟用深度思考模式',
config_self_evolution: '自主進化', config_self_evolution_hint: '會話空閒後自動覆盤,沉澱記憶、最佳化技能、處理未完成事項',
evolution_badge: '自主學習',
config_channel_type: '管道型別',
config_provider: '模型廠商', config_model_name: '模型',
config_custom_model_hint: '輸入自定義模型名稱',
config_save: '儲存', config_saved: '已儲存',
config_save_error: '儲存失敗',
config_custom_option: '自定義',
config_custom_tip: '介面需遵循 OpenAI API 協議',
config_security: '安全設定', config_password: '訪問密碼',
config_password_hint: '留空則不啟用密碼保護',
config_password_changed: '密碼已更新',
config_password_cleared: '密碼已清除',
config_password_security_warning: '⚠️ 警告:目前密碼為空且對外連接埠開放,建議重啟服務,或檢查是否調整監聽位址綁定。',
skills_title: '技能管理', skills_desc: '檢視、啟用或禁用 Agent 工具和技能', skills_hub_btn: '探索技能廣場',
skills_loading: '載入技能中...', skills_loading_desc: '技能載入後將顯示在此處',
tools_section_title: '內建工具', tools_loading: '載入工具中...',
skills_section_title: '技能', skill_enable: '啟用', skill_disable: '禁用',
skill_toggle_error: '操作失敗,請稍後再試',
memory_title: '記憶管理', memory_desc: '檢視 Agent 記憶檔案和內容',
memory_tab_files: '記憶檔案', memory_tab_dreams: '自主進化',
memory_loading: '載入記憶檔案中...', memory_loading_desc: '記憶檔案將顯示在此處',
memory_back: '返回列表',
memory_col_name: '檔名', memory_col_type: '型別', memory_col_size: '大小', memory_col_updated: '更新時間',
channels_title: '管道管理', channels_desc: '管理已接入的訊息管道',
channels_add: '接入管道', channels_disconnect: '斷開',
channels_save: '儲存設定', channels_saved: '已儲存', channels_save_error: '儲存失敗',
channels_restarted: '已儲存並重啟',
channels_connect_btn: '接入', channels_cancel: '取消',
channels_select_placeholder: '選擇要接入的管道...',
channels_empty: '暫未接入任何管道', channels_empty_desc: '點選右上角「接入管道」按鈕開始設定',
channels_disconnect_confirm: '確認斷開該管道?設定將保留但管道會停止執行。',
channels_connected: '已接入', channels_connecting: '接入中...',
weixin_scan_title: '微信掃碼登入', weixin_scan_desc: '請使用微信掃描下方二維碼',
weixin_scan_loading: '正在獲取二維碼...', weixin_scan_waiting: '等待掃碼...',
weixin_scan_scanned: '已掃碼,請在手機上確認', weixin_scan_expired: '二維碼已過期,正在重新整理...',
weixin_scan_success: '登入成功,正在啟動管道...', weixin_scan_fail: '獲取二維碼失敗',
weixin_qr_tip: '二維碼約2分鐘後過期',
wecom_scan_btn: '掃碼建立企微機器人', wecom_scan_desc: '使用企業微信掃碼,一鍵建立智慧機器人',
wecom_scan_success: '建立成功,正在啟動管道...',
wecom_scan_fail: '建立失敗',
wecom_mode_scan: '掃碼接入', wecom_mode_manual: '手動填寫',
feishu_scan_btn: '一鍵建立飛書應用',
feishu_scan_desc: '使用飛書 App 掃碼,自動建立應用並預置全部許可權與事件訂閱',
feishu_scan_replace_desc: '使用飛書 App 掃碼建立新機器人,將覆蓋當前的 App ID / Secret',
feishu_scan_loading: '正在向飛書申請二維碼...',
feishu_scan_waiting: '等待掃碼...',
feishu_scan_tip: '二維碼 10 分鐘內有效,僅供一次掃描',
feishu_scan_open_link: '或點選此處在瀏覽器中開啟',
feishu_scan_success: '應用建立成功,正在啟動管道...',
feishu_scan_expired: '二維碼已過期,請重試',
feishu_scan_denied: '已取消授權',
feishu_scan_fail: '建立失敗',
feishu_scan_retry: '重試',
feishu_mode_scan: '掃碼建立', feishu_mode_manual: '手動填寫',
tasks_title: '定時任務', tasks_desc: '檢視和管理定時任務',
tasks_coming: '即將推出', tasks_coming_desc: '定時任務管理功能即將在此提供',
task_add_btn: '新增任務',
task_edit_title: '編輯定時任務',
task_add_title: '新增定時任務',
task_name: '任務名稱',
task_enabled: '啟用任務',
task_schedule_type: '排程型別',
task_schedule_cron: 'Cron 表示式',
task_schedule_interval: '固定間隔',
task_schedule_once: '一次性任務',
task_cron_expression: 'Cron 表示式',
task_cron_hint: '格式: 分 時 日 月 周,例如 "0 9 * * *" 表示每天 9:00',
task_interval_seconds: '間隔秒數',
task_interval_hint: '最小 60 秒,例如 3600 表示每小時執行一次',
task_once_time: '執行時間',
task_action_type: '動作型別',
task_action_send_message: '傳送訊息',
task_action_agent_task: 'AI 任務',
task_channel_type: '管道型別',
task_channel_hint: '選擇定時訊息傳送的管道',
task_message_content: '訊息內容',
task_task_description: '任務描述',
task_delete_btn: '刪除任務',
task_delete_confirm_title: '刪除定時任務',
task_delete_confirm_msg: '確定刪除該定時任務嗎?此操作無法撤銷。',
logs_title: '日誌', logs_desc: '實時日誌輸出 (run.log)',
logs_live: '實時', logs_coming_msg: '日誌流即將在此提供。將連線 run.log 實現類似 tail -f 的實時輸出。',
new_chat: '新對話',
session_history: '歷史會話',
today: '今天', yesterday: '昨天', earlier: '更早',
delete_session_confirm: '確認刪除該會話?所有訊息將被清除。',
delete_session_title: '刪除會話',
rename_session: '重新命名',
delete_message_confirm: '確認刪除這條訊息?',
delete_message_title: '刪除訊息',
edit_disabled_reply_active: '正在生成回覆,暫時無法編輯。',
delete_disabled_reply_active: '正在生成回覆,暫時無法刪除。',
untitled_session: '新對話',
context_cleared: '— 以上內容已從上下文中移除 —',
tip_new_chat: '新建對話',
tip_clear_context: '清除上下文',
tip_attach: '新增附件',
attach_menu_file: '上傳檔案',
mic_idle_title: '點選錄音 / 再按一次結束',
mic_recording_title: '錄音中,再次點選結束',
mic_busy_title: '識別中…',
mic_permission_denied: '無法訪問麥克風,請檢查瀏覽器許可權',
mic_too_short: '錄音太短,請重試',
mic_error: '語音識別失敗',
speak_msg: '朗讀這段回覆',
voice_reply_mode_label: '語音回覆策略',
voice_reply_off: '關閉',
voice_reply_if_voice: '僅語音問/語音答',
voice_reply_always: '總是語音回覆',
attach_menu_folder: '上傳資料夾',
confirm_yes: '確認',
confirm_cancel: '取消',
error_send: '傳送失敗,請稍後再試。', error_timeout: '請求超時,請再試一次。',
thinking_in_progress: '思考中...', thinking_done: '已深度思考', thinking_duration: '耗時',
edit_message: '編輯訊息',
regenerate_response: '重新生成',
edit_save: '儲存併傳送',
edit_cancel: '取消',
logout: '登出',
}, },
en: { en: {
console: 'Console', console: 'Console',
@@ -391,8 +640,9 @@ const I18N = {
config_custom_tip: 'API must follow OpenAI protocol.', config_custom_tip: 'API must follow OpenAI protocol.',
config_security: 'Security', config_password: 'Password', config_security: 'Security', config_password: 'Password',
config_password_hint: 'Leave empty to disable password protection', config_password_hint: 'Leave empty to disable password protection',
config_password_changed: 'Password updated, please re-login', config_password_changed: 'Password updated',
config_password_cleared: 'Password cleared', config_password_cleared: 'Password cleared',
config_password_security_warning: '⚠️ Warning: Password is now empty and the port is exposed. Consider restarting the service or adjusting the listening address binding.',
skills_title: 'Skills', skills_desc: 'View, enable, or disable agent tools and skills', skills_hub_btn: 'Skill Hub', skills_title: 'Skills', skills_desc: 'View, enable, or disable agent tools and skills', skills_hub_btn: 'Skill Hub',
skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading', skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading',
tools_section_title: 'Built-in Tools', tools_loading: 'Loading tools...', tools_section_title: 'Built-in Tools', tools_loading: 'Loading tools...',
@@ -498,6 +748,7 @@ const I18N = {
regenerate_response: 'Regenerate', regenerate_response: 'Regenerate',
edit_save: 'Save and send', edit_save: 'Save and send',
edit_cancel: 'Cancel', edit_cancel: 'Cancel',
logout: 'Logout',
} }
}; };
@@ -511,6 +762,9 @@ let currentLang = (typeof window.__cowResolveLang__ === 'function')
if (!raw) return ''; if (!raw) return '';
const v = String(raw).trim().toLowerCase(); const v = String(raw).trim().toLowerCase();
if (v === 'auto') return ''; if (v === 'auto') return '';
// Handle Traditional Chinese variants first (more specific)
if (v === 'zh-hant' || v.startsWith('zh-hant-') || v === 'zh-tw' || v === 'zh-hk') return 'zh-Hant';
// Then Simplified Chinese
if (v.indexOf('zh') === 0) return 'zh'; if (v.indexOf('zh') === 0) return 'zh';
if (v.indexOf('en') === 0) return 'en'; if (v.indexOf('en') === 0) return 'en';
return ''; return '';
@@ -544,12 +798,25 @@ function applyI18n() {
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => { document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.dataset['i18nPlaceholder']); el.placeholder = t(el.dataset['i18nPlaceholder']);
}); });
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.dataset['i18nTitle']);
});
document.querySelectorAll('[data-tip-key]').forEach(el => { document.querySelectorAll('[data-tip-key]').forEach(el => {
el.setAttribute('data-tooltip', t(el.dataset.tipKey)); el.setAttribute('data-tooltip', t(el.dataset.tipKey));
}); });
installCfgTipPortal(); installCfgTipPortal();
// Clear any status messages when language changes
document.querySelectorAll('[id$="-status"]').forEach(el => {
el.classList.add('opacity-0');
});
const langLabel = document.getElementById('lang-label'); const langLabel = document.getElementById('lang-label');
if (langLabel) langLabel.textContent = currentLang === 'zh' ? '中文' : 'EN'; if (langLabel) {
if (currentLang === 'zh-Hant') langLabel.textContent = '繁中';
else if (currentLang === 'zh') langLabel.textContent = '簡中';
else langLabel.textContent = 'EN';
}
// Point the docs link to the locale-specific documentation site. // Point the docs link to the locale-specific documentation site.
const docsLink = document.getElementById('docs-link'); const docsLink = document.getElementById('docs-link');
if (docsLink) docsLink.href = currentLang === 'zh' ? 'https://docs.cowagent.ai/zh' : 'https://docs.cowagent.ai'; if (docsLink) docsLink.href = currentLang === 'zh' ? 'https://docs.cowagent.ai/zh' : 'https://docs.cowagent.ai';
@@ -559,7 +826,7 @@ function applyI18n() {
// persists the user choice locally, re-renders the UI, and binds the choice to // persists the user choice locally, re-renders the UI, and binds the choice to
// the backend `cow_lang` config so logs / agent replies / CLI follow suit. // the backend `cow_lang` config so logs / agent replies / CLI follow suit.
function setLanguage(lang) { function setLanguage(lang) {
const next = (lang === 'en') ? 'en' : 'zh'; const next = (lang === 'en' || lang === 'zh' || lang === 'zh-Hant') ? lang : 'zh';
if (next === currentLang) { if (next === currentLang) {
// Still persist + sync in case storage/backend drifted from the UI. // Still persist + sync in case storage/backend drifted from the UI.
syncLanguageToBackend(next); syncLanguageToBackend(next);
@@ -569,38 +836,52 @@ function setLanguage(lang) {
localStorage.setItem('cow_lang', currentLang); localStorage.setItem('cow_lang', currentLang);
applyI18n(); applyI18n();
_applyInputTooltips(); _applyInputTooltips();
// Re-render views whose DOM is built in JS (data-i18n alone does not
// cover strings interpolated via t() into innerHTML).
try { rerenderDynamicViews(); } catch (e) {}
// Keep the language switch button and config selector visually in sync. // Keep the language switch button and config selector visually in sync.
try { updateLangControls(); } catch (e) {} try { updateLangControls(); } catch (e) {}
syncLanguageToBackend(currentLang);
// Sync language choice to backend first, then trigger dynamic views reload
// to avoid race conditions on API endpoints.
syncLanguageToBackend(currentLang, () => {
try { rerenderDynamicViews(); } catch (e) {}
});
} }
// Persist the language to the backend `cow_lang` config (best-effort; the UI // Persist the language to the backend `cow_lang` config (best-effort; the UI
// has already switched locally, so a network failure is non-blocking). // has already switched locally, so a network failure is non-blocking).
function syncLanguageToBackend(lang) { function syncLanguageToBackend(lang, callback) {
try { try {
fetch('/config', { fetch('/config', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updates: { cow_lang: lang } }) body: JSON.stringify({ updates: { cow_lang: lang } })
}).catch(() => {}); })
} catch (e) {} .then(() => { if (callback) callback(); })
.catch(() => { if (callback) callback(); });
} catch (e) {
if (callback) callback();
}
} }
// Reflect the current language on both the top-right toggle and the config // Reflect the current language on both the top-right toggle and the config
// selector (if present), so the two entry points stay synchronized. // selector (if present), so the two entry points stay synchronized.
function updateLangControls() { function updateLangControls() {
const langLabel = document.getElementById('lang-label'); const langLabel = document.getElementById('lang-label');
if (langLabel) langLabel.textContent = currentLang === 'zh' ? '中文' : 'EN'; if (langLabel) {
if (currentLang === 'zh-Hant') langLabel.textContent = '繁體';
else if (currentLang === 'zh') langLabel.textContent = '簡體';
else langLabel.textContent = 'EN';
}
// The config language picker is the custom .cfg-dropdown component. Only // The config language picker is the custom .cfg-dropdown component. Only
// sync it once it has been initialized (i.e. the config panel was opened). // sync it once it has been initialized (i.e. the config panel was opened).
const sel = document.getElementById('cfg-lang-select'); const sel = document.getElementById('cfg-lang-select');
if (sel && sel._ddValue !== undefined && sel._ddValue !== currentLang) { if (sel && sel._ddValue !== undefined && sel._ddValue !== currentLang) {
sel._ddValue = currentLang; sel._ddValue = currentLang;
const textEl = sel.querySelector('.cfg-dropdown-text'); const textEl = sel.querySelector('.cfg-dropdown-text');
if (textEl) textEl.textContent = currentLang === 'zh' ? '中文' : 'English'; if (textEl) {
if (currentLang === 'zh-Hant') textEl.textContent = '繁體中文';
else if (currentLang === 'zh') textEl.textContent = '简体中文';
else textEl.textContent = 'English';
}
sel.querySelectorAll('.cfg-dropdown-item').forEach(i => { sel.querySelectorAll('.cfg-dropdown-item').forEach(i => {
i.classList.toggle('active', i.dataset.value === currentLang); i.classList.toggle('active', i.dataset.value === currentLang);
}); });
@@ -608,7 +889,13 @@ function updateLangControls() {
} }
function toggleLanguage() { function toggleLanguage() {
setLanguage(currentLang === 'zh' ? 'en' : 'zh'); if (currentLang === 'zh') {
setLanguage('zh-Hant');
} else if (currentLang === 'zh-Hant') {
setLanguage('en');
} else {
setLanguage('zh');
}
} }
// Refresh JS-rendered views after a language switch. Each branch uses the // Refresh JS-rendered views after a language switch. Each branch uses the
@@ -623,6 +910,19 @@ function rerenderDynamicViews() {
tasksLoaded = false; tasksLoaded = false;
loadTasksView(); loadTasksView();
} }
// Reload skills and tools after language switch
if (currentView === 'skills') {
toolsLoaded = false;
loadSkillsView();
}
// Reload channels after language switch
if (currentView === 'channels') {
loadChannelsView();
}
// Reload config after language switch
if (currentView === 'config') {
loadConfigView();
}
} }
// Floating tooltip portal for [data-tip-key] elements. Tooltip nodes are // Floating tooltip portal for [data-tip-key] elements. Tooltip nodes are
@@ -730,6 +1030,12 @@ function navigateTo(viewId) {
document.getElementById('breadcrumb-page').textContent = t(meta.page); document.getElementById('breadcrumb-page').textContent = t(meta.page);
document.getElementById('breadcrumb-page').dataset.i18n = meta.page; document.getElementById('breadcrumb-page').dataset.i18n = meta.page;
currentView = viewId; currentView = viewId;
// Clear status messages when navigating away
document.querySelectorAll('[id$="-status"]').forEach(el => {
el.classList.add('opacity-0');
});
if (window.innerWidth < 1024) closeSidebar(); if (window.innerWidth < 1024) closeSidebar();
} }
@@ -4266,7 +4572,7 @@ function initConfigView(data) {
if (langSel) { if (langSel) {
initDropdown( initDropdown(
langSel, langSel,
[{ value: 'zh', label: '中文' }, { value: 'en', label: 'English' }], [{ value: 'zh', label: '简体中文' }, { value: 'zh-Hant', label: '繁體中文' }, { value: 'en', label: 'English' }],
currentLang, currentLang,
(val) => setLanguage(val) (val) => setLanguage(val)
); );
@@ -4439,7 +4745,10 @@ function showStatus(elId, msgKey, isError) {
el.classList.toggle('text-red-500', !!isError); el.classList.toggle('text-red-500', !!isError);
el.classList.toggle('text-primary-500', !isError); el.classList.toggle('text-primary-500', !isError);
el.classList.remove('opacity-0'); el.classList.remove('opacity-0');
// Warning messages (errors) should stay visible, success messages auto-hide
if (!isError) {
setTimeout(() => el.classList.add('opacity-0'), 2500); setTimeout(() => el.classList.add('opacity-0'), 2500);
}
} }
function saveModelConfig() { function saveModelConfig() {
@@ -4552,16 +4861,34 @@ function savePasswordConfig() {
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
console.log('[Password Config] Response:', data); // Debug
if (data.status === 'success') { if (data.status === 'success') {
if (newPwd) { if (newPwd) {
showStatus('cfg-password-status', 'config_password_changed', false); showStatus('cfg-password-status', 'config_password_changed', false);
setTimeout(() => { window.location.reload(); }, 1500); // Mark as masked so user needs to re-enter to change again
input.dataset.masked = '1';
input.dataset.maskedVal = newPwd;
input.value = '••••••••';
input.classList.add('cfg-key-masked');
// Show logout button since password is now enabled
const logoutBtn = document.getElementById('logout-btn-header');
if (logoutBtn) logoutBtn.classList.remove('hidden');
} else { } else {
input.dataset.masked = ''; input.dataset.masked = '';
input.dataset.maskedVal = ''; input.dataset.maskedVal = '';
input.classList.remove('cfg-key-masked'); input.classList.remove('cfg-key-masked');
// Show security warning if password was cleared with public host
if (data.warning === 'password_cleared_with_public_host') {
showStatus('cfg-password-status', 'config_password_security_warning', true);
} else {
showStatus('cfg-password-status', 'config_password_cleared', false); showStatus('cfg-password-status', 'config_password_cleared', false);
} }
const logoutBtn = document.getElementById('logout-btn-header');
if (logoutBtn) logoutBtn.classList.add('hidden');
}
} else { } else {
showStatus('cfg-password-status', 'config_save_error', true); showStatus('cfg-password-status', 'config_save_error', true);
} }
@@ -8728,6 +9055,9 @@ function showLoginScreen() {
if (currentLang === 'en') { if (currentLang === 'en') {
subtitle.textContent = 'Enter password to access the console'; subtitle.textContent = 'Enter password to access the console';
loginBtn.textContent = 'Login'; loginBtn.textContent = 'Login';
} else if (currentLang === 'zh-Hant') {
subtitle.textContent = '請輸入密碼以存取控制台';
loginBtn.textContent = '登入';
} else { } else {
subtitle.textContent = '请输入密码以访问控制台'; subtitle.textContent = '请输入密码以访问控制台';
loginBtn.textContent = '登录'; loginBtn.textContent = '登录';
@@ -8754,16 +9084,30 @@ function showLoginScreen() {
if (data.status === 'success') { if (data.status === 'success') {
overlay.classList.add('hidden'); overlay.classList.add('hidden');
document.getElementById('app').classList.remove('hidden'); document.getElementById('app').classList.remove('hidden');
const logoutBtn = document.getElementById('logout-btn-header');
if (logoutBtn) logoutBtn.classList.remove('hidden');
initApp(); initApp();
} else { } else {
errEl.textContent = currentLang === 'zh' ? '密码错误' : 'Wrong password'; if (currentLang === 'zh-Hant') {
errEl.textContent = '密碼錯誤';
} else if (currentLang === 'zh') {
errEl.textContent = '密码错误';
} else {
errEl.textContent = 'Wrong password';
}
errEl.classList.remove('hidden'); errEl.classList.remove('hidden');
pwdInput.value = ''; pwdInput.value = '';
pwdInput.focus(); pwdInput.focus();
} }
btn.disabled = false; btn.disabled = false;
}).catch(() => { }).catch(() => {
errEl.textContent = currentLang === 'zh' ? '网络错误,请重试' : 'Network error, please retry'; if (currentLang === 'zh-Hant') {
errEl.textContent = '網路錯誤,請重試';
} else if (currentLang === 'zh') {
errEl.textContent = '网络错误,请重试';
} else {
errEl.textContent = 'Network error, please retry';
}
errEl.classList.remove('hidden'); errEl.classList.remove('hidden');
btn.disabled = false; btn.disabled = false;
}); });
@@ -8771,6 +9115,19 @@ function showLoginScreen() {
}; };
} }
function handleLogout() {
fetch('/auth/logout', {
method: 'POST'
}).then(r => r.json()).then(data => {
if (data.status === 'success') {
window.location.reload();
}
}).catch(() => {
window.location.reload();
});
}
window.handleLogout = handleLogout;
// Intercept 401 responses globally to show login screen on session expiry // Intercept 401 responses globally to show login screen on session expiry
const _originalFetch = window.fetch; const _originalFetch = window.fetch;
window.fetch = function(...args) { window.fetch = function(...args) {
@@ -8816,6 +9173,10 @@ fetch('/auth/check').then(r => r.json()).then(data => {
if (data.auth_required && !data.authenticated) { if (data.auth_required && !data.authenticated) {
showLoginScreen(); showLoginScreen();
} else { } else {
if (data.auth_required) {
const logoutBtn = document.getElementById('logout-btn-header');
if (logoutBtn) logoutBtn.classList.remove('hidden');
}
initApp(); initApp();
} }
}).catch(() => { }).catch(() => {

View File

@@ -1854,9 +1854,13 @@ class ConfigHandler:
return json.dumps({"status": "error", "message": "no valid keys to update"}) return json.dumps({"status": "error", "message": "no valid keys to update"})
config_path = os.path.join(get_data_root(), "config.json") config_path = os.path.join(get_data_root(), "config.json")
old_password = "" # Store old password before update
if os.path.exists(config_path): if os.path.exists(config_path):
with open(config_path, "r", encoding="utf-8") as f: with open(config_path, "r", encoding="utf-8") as f:
file_cfg = json.load(f) file_cfg = json.load(f)
# Capture old password before updating
if "web_password" in applied:
old_password = file_cfg.get("web_password", "")
else: else:
file_cfg = {} file_cfg = {}
file_cfg.update(applied) file_cfg.update(applied)
@@ -1874,6 +1878,26 @@ class ConfigHandler:
except Exception as lang_err: except Exception as lang_err:
logger.warning(f"[WebChannel] Failed to apply language: {lang_err}") logger.warning(f"[WebChannel] Failed to apply language: {lang_err}")
# Check if password was cleared: if there was a password before clearing,
# the service is likely bound to 0.0.0.0 (public), so warn the user.
password_warning = None
if "web_password" in applied:
new_password = applied["web_password"]
configured_host = file_cfg.get("web_host", "")
# If password was cleared and there was a password before
if not new_password and old_password:
# If web_host is not explicitly set, the service auto-binds based on password
# With password → 0.0.0.0 (public), without password → 127.0.0.1 (local)
# So clearing password when it was previously set means going from public to local
if not configured_host or configured_host == "0.0.0.0":
password_warning = "password_cleared_with_public_host"
logger.warning(
"[WebChannel] Password cleared while service is likely bound to 0.0.0.0. "
"Consider restarting the service to rebind to 127.0.0.1 "
"or explicitly set web_host in config to prevent unauthorized access."
)
# Reset Bridge so that bot routing reflects the new config. # Reset Bridge so that bot routing reflects the new config.
# Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot) # Without this, Bridge keeps its cached bot instance (e.g. LinkAIBot)
# even after the user switches bot_type / use_linkai / model in UI. # even after the user switches bot_type / use_linkai / model in UI.
@@ -1886,7 +1910,7 @@ class ConfigHandler:
except Exception as reset_err: except Exception as reset_err:
logger.warning(f"[WebChannel] Failed to reset bridge: {reset_err}") logger.warning(f"[WebChannel] Failed to reset bridge: {reset_err}")
return json.dumps({"status": "success", "applied": applied}, ensure_ascii=False) return json.dumps({"status": "success", "applied": applied, "warning": password_warning}, ensure_ascii=False)
except Exception as e: except Exception as e:
logger.error(f"Error updating config: {e}") logger.error(f"Error updating config: {e}")
return json.dumps({"status": "error", "message": str(e)}) return json.dumps({"status": "error", "message": str(e)})
@@ -3667,11 +3691,13 @@ class ChannelsHandler:
_require_auth() _require_auth()
web.header('Content-Type', 'application/json; charset=utf-8') web.header('Content-Type', 'application/json; charset=utf-8')
try: try:
from common import i18n
local_config = conf() local_config = conf()
active_channels = self._active_channel_set() active_channels = self._active_channel_set()
# Desktop build ships without lark-oapi, so hide Feishu from the list. # Desktop build ships without lark-oapi, so hide Feishu from the list.
desktop_mode = os.environ.get("COW_DESKTOP") == "1" desktop_mode = os.environ.get("COW_DESKTOP") == "1"
channels = [] channels = []
is_hant = i18n.get_language() == i18n.ZH_HANT
for ch_name, ch_def in self.CHANNEL_DEFS.items(): for ch_name, ch_def in self.CHANNEL_DEFS.items():
if desktop_mode and ch_name == "feishu": if desktop_mode and ch_name == "feishu":
continue continue
@@ -3682,16 +3708,32 @@ class ChannelsHandler:
display_val = self._mask_secret(str(raw_val)) display_val = self._mask_secret(str(raw_val))
else: else:
display_val = raw_val display_val = raw_val
label_val = f["label"]
if is_hant and isinstance(label_val, str):
label_val = i18n.to_traditional(label_val)
elif is_hant and isinstance(label_val, dict):
label_val = label_val.copy()
label_val["zh-Hant"] = i18n.to_traditional(label_val.get("zh", ""))
fields_out.append({ fields_out.append({
"key": f["key"], "key": f["key"],
"label": f["label"], "label": label_val,
"type": f["type"], "type": f["type"],
"value": display_val, "value": display_val,
"default": f.get("default", ""), "default": f.get("default", ""),
}) })
label_val = ch_def["label"]
if is_hant and isinstance(label_val, str):
label_val = i18n.to_traditional(label_val)
elif is_hant and isinstance(label_val, dict):
label_val = label_val.copy()
label_val["zh-Hant"] = i18n.to_traditional(label_val.get("zh", ""))
ch_info = { ch_info = {
"name": ch_name, "name": ch_name,
"label": ch_def["label"], "label": label_val,
"icon": ch_def["icon"], "icon": ch_def["icon"],
"color": ch_def["color"], "color": ch_def["color"],
"active": ch_name in active_channels, "active": ch_name in active_channels,
@@ -4248,16 +4290,26 @@ class ToolsHandler:
web.header('Content-Type', 'application/json; charset=utf-8') web.header('Content-Type', 'application/json; charset=utf-8')
try: try:
from agent.tools.tool_manager import ToolManager from agent.tools.tool_manager import ToolManager
from common import i18n
tm = ToolManager() tm = ToolManager()
if not tm.tool_classes: if not tm.tool_classes:
tm.load_tools() tm.load_tools()
tools = [] tools = []
lang = i18n.get_language()
for name, cls in tm.tool_classes.items(): for name, cls in tm.tool_classes.items():
try: try:
instance = cls() instance = cls()
desc = instance.description
if lang == i18n.ZH_HANT and desc:
desc = i18n.to_traditional(desc)
elif lang == "en" and name == "scheduler":
desc = (
"Create, query and manage scheduled tasks (reminders, periodic tasks, etc.).\n\n"
"⚠️ IMPORTANT: Only use this tool when delayed or periodic execution is needed."
)
tools.append({ tools.append({
"name": name, "name": name,
"description": instance.description, "description": desc,
}) })
except Exception: except Exception:
tools.append({"name": name, "description": ""}) tools.append({"name": name, "description": ""})
@@ -4274,10 +4326,17 @@ class SkillsHandler:
try: try:
from agent.skills.service import SkillService from agent.skills.service import SkillService
from agent.skills.manager import SkillManager from agent.skills.manager import SkillManager
from common import i18n
workspace_root = _get_workspace_root() workspace_root = _get_workspace_root()
manager = SkillManager(custom_dir=os.path.join(workspace_root, "skills")) manager = SkillManager(custom_dir=os.path.join(workspace_root, "skills"))
service = SkillService(manager) service = SkillService(manager)
skills = service.query() skills = service.query()
if i18n.get_language() == i18n.ZH_HANT:
for skill in skills:
if isinstance(skill, dict):
for k, v in list(skill.items()):
if k in ("name", "description", "display_name") and isinstance(v, str):
skill[k] = i18n.to_traditional(v)
return json.dumps({"status": "success", "skills": skills}, ensure_ascii=False) return json.dumps({"status": "success", "skills": skills}, ensure_ascii=False)
except Exception as e: except Exception as e:
logger.error(f"[WebChannel] Skills API error: {e}") logger.error(f"[WebChannel] Skills API error: {e}")

View File

@@ -7,6 +7,11 @@ across the CLI, startup logs, error messages, agent prompts and channel
replies. It must NOT import project config (to avoid circular imports) and 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. must stay dependency-free so it can run at the earliest startup phase.
Supported language codes (BCP 47 compliant):
- "zh" (Simplified Chinese)
- "zh-Hant" (Traditional Chinese, script-based tag per Unicode CLDR)
- "en" (English)
Resolution priority (highest first): Resolution priority (highest first):
1. Explicit `cow_lang` from config.json — also covers Docker/CI, since any 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), config key is overridable via its uppercase env var (e.g. COW_LANG=zh),
@@ -19,7 +24,10 @@ Resolution priority (highest first):
5. Default -> English 5. Default -> English
A value of "auto" (the default) triggers detection (steps 2-5). Explicitly A value of "auto" (the default) triggers detection (steps 2-5). Explicitly
setting "zh" or "en" locks the language and skips detection. setting "zh", "zh-Hant", or "en" locks the language and skips detection.
Note: For backwards compatibility, zh-tw, zh-hk, and other regional variants
are automatically normalized to zh-Hant during detection.
""" """
import os import os
@@ -28,10 +36,85 @@ import sys
# Supported language codes # Supported language codes
ZH = "zh" ZH = "zh"
ZH_HANT = "zh-Hant"
EN = "en" EN = "en"
SUPPORTED = (ZH, EN) SUPPORTED = (ZH, ZH_HANT, EN)
DEFAULT_LANG = EN DEFAULT_LANG = EN
# Mapped Simplified to Traditional characters in this codebase
_SIMPLIFIED = "与专业东丢两严个丰临为举么义乐乔习书乱于云产亲仅从仓们价众优伙会伟传体余侧倾储儿关兴兽内册写冲决况准减几凭凯击划则刚创删别剥办务动区华协单占厂历压参双发变叙台号后吗听启员响嚣团国图场坏块声处备复够头夹妩姗娇娱婶学宝实宠审宽对导将尝尽层属币师带帧帮幂干并广庆库应开异弃张弹强归当录彦彻征径忆态总悦惯戏战户执扩扫抛抢护报拟拥拦择换据数断无旧时昵显晓暂术机杂权条来杰极构枪标树样档桦梦检欢残毕气汇汉汤没泄泼洁测浏润涩淀渊温游湾湿滞满滤灵灿炀点炼烦热爱爷状独猪环现瑶电画监盖盘着睁码础确离种积称稳竞笔签简类粤紧纠红约级纪纯纳线组细织终绍经结绘给络绝统继绪续维综缀缓编缩网罗羁职联聪脑脚脱腾舰艺节苍苏范荐获萝蔼虑补装见观规视览觉触计订认讨让训议讯记讲许论设访证识诉诊词译试诚话询该详语误说请读谁调谢谨谱贝负责贤败账货质费资赋赖赘轩转轮软轻载较辑输边达过运还这进远违连迟适选递逻遥邮邻采释里鉴针钉钟钥钮钱铁链销锁错锤键镜长闭问闲间闺闻闽阅队阳际陆陕险随隐难静韩页项顺须顾预领频题额风飞饭饰馆馈馏马驻驿验骤鱼鸡麦齐"
_TRADITIONAL = "與專業東丟兩嚴個豐臨為舉麼義樂喬習書亂於雲產親僅從倉們價眾優夥會偉傳體餘側傾儲兒關興獸內冊寫沖決況準減幾憑凱擊劃則剛創刪別剝辦務動區華協單佔廠歷壓參雙發變敘臺號後嗎聽啟員響囂團國圖場壞塊聲處備復夠頭夾嫵姍嬌娛嬸學寶實寵審寬對導將嘗盡層屬幣師帶幀幫冪幹並廣慶庫應開異棄張彈強歸當錄彥徹徵徑憶態總悅慣戲戰戶執擴掃拋搶護報擬擁攔擇換據數斷無舊時暱顯曉暫術機雜權條來傑極構槍標樹樣檔樺夢檢歡殘畢氣匯漢湯沒洩潑潔測瀏潤澀澱淵溫遊灣溼滯滿濾靈燦煬點煉煩熱愛爺狀獨豬環現瑤電畫監蓋盤著睜碼礎確離種積稱穩競筆籤簡類粵緊糾紅約級紀純納線組細織終紹經結繪給絡絕統繼緒續維綜綴緩編縮網羅羈職聯聰腦腳脫騰艦藝節蒼蘇範薦獲蘿藹慮補裝見觀規視覽覺觸計訂認討讓訓議訊記講許論設訪證識訴診詞譯試誠話詢該詳語誤說請讀誰調謝謹譜貝負責賢敗帳貨質費資檔案影片圖片連結資料資訊支援排程執行帳號密碼憑證埠服務啟用管道終端機控制台"
_CHAR_MAP = None
_PHRASE_MAP = {
"默认": "預設",
"内存": "記憶體",
"配置": "設定",
"进程": "處理程序",
"目录": "目錄",
"文件夹": "資料夾",
"文件": "檔案",
"视频": "影片",
"图片": "圖片",
"影象": "影像",
"图像": "影像",
"链接": "連結",
"数据": "資料",
"信息": "資訊",
"支持": "支援",
"定时": "排程",
"运行": "執行",
"账号": "帳號",
"密码": "密碼",
"凭据": "憑證",
"端口": "",
"服务": "服務",
"激活": "啟用",
"通道": "管道",
"终端": "終端機",
"主控台": "控制台",
"创建": "建立",
"计算机": "電腦",
}
def to_traditional(text):
"""Convert Simplified Chinese text to Traditional Chinese.
Uses a two-tier approach:
1. Phrase-level mapping for project-specific terms (e.g., "配置""設定")
2. OpenCC library (opencc-python-reimplemented) if available for high-quality
context-aware conversion, with fallback to built-in character mapping
This function is designed to work without external dependencies. If OpenCC
is not installed, it falls back to a curated 450-character mapping table
plus 30+ technical term mappings that cover common project vocabulary.
For production use with zh-Hant language, installing OpenCC is recommended:
pip install opencc-python-reimplemented
"""
if not text:
return text
# Replace phrases first
for s, t_phrase in _PHRASE_MAP.items():
text = text.replace(s, t_phrase)
try:
from opencc import OpenCC
cc = OpenCC('s2twp')
return cc.convert(text)
except Exception:
pass
global _CHAR_MAP
if _CHAR_MAP is None:
_CHAR_MAP = dict(zip(_SIMPLIFIED, _TRADITIONAL))
# Replace characters
return "".join(_CHAR_MAP.get(c, c) for c in text)
# Resolved language cache; None until first resolution. # Resolved language cache; None until first resolution.
_resolved_lang = None _resolved_lang = None
@@ -48,7 +131,10 @@ def _normalize(raw):
value = str(raw).strip().lower().replace("_", "-") value = str(raw).strip().lower().replace("_", "-")
if value in ("auto", ""): if value in ("auto", ""):
return None return None
# Chinese variants: zh, zh-cn, zh-hans, zh-hans-cn, zh-tw, zh-hk ... # Traditional Chinese variants: zh-tw, zh-hk, zh-hant, zh-hant-tw, zh-hant-hk...
if value.startswith("zh-tw") or value.startswith("zh-hk") or "hant" in value:
return ZH_HANT
# General or Simplified Chinese variants: zh, zh-cn, zh-hans...
if value.startswith("zh") or value.startswith("chinese"): if value.startswith("zh") or value.startswith("chinese"):
return ZH return ZH
if value.startswith("en") or value.startswith("english"): if value.startswith("en") or value.startswith("english"):
@@ -167,7 +253,7 @@ def get_language():
def is_zh(): def is_zh():
return get_language() == ZH return get_language() in (ZH, ZH_HANT)
def t(zh_text, en_text): def t(zh_text, en_text):
@@ -176,4 +262,7 @@ def t(zh_text, en_text):
Intended for one-off strings where a full message catalog is overkill: Intended for one-off strings where a full message catalog is overkill:
t("已中止", "Cancelled") t("已中止", "Cancelled")
""" """
return zh_text if get_language() == ZH else en_text lang = get_language()
if lang == ZH_HANT:
return to_traditional(zh_text)
return zh_text if lang == ZH else en_text

286
docs/zh/README-Hant.md Normal file
View File

@@ -0,0 +1,286 @@
<p align="center"><img src= "https://github.com/user-attachments/assets/eca9a9ec-8534-4615-9e0f-96c5ac1d10a3" alt="CowAgent" width="420" /></p>
<p align="center">
<a href="https://github.com/zhayujie/CowAgent/releases/latest"><img src="https://img.shields.io/github/v/release/zhayujie/CowAgent?cacheSeconds=3600" alt="Latest release"></a>
<a href="https://github.com/zhayujie/CowAgent/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
<a href="https://github.com/zhayujie/CowAgent"><img src="https://img.shields.io/github/stars/zhayujie/CowAgent?style=flat-square&cacheSeconds=3600" alt="Stars"></a>
<a href="https://docs.cowagent.ai/zh"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-cowagent.ai-blue?style=flat&logo=readthedocs&logoColor=white" alt="文件"></a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/25763" target="_blank"><img src="https://trendshift.io/api/badge/repositories/25763" alt="zhayujie%2FCowAgent | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
<p align="center">
[<a href="../../README.md">English</a>] | [<a href="README.md">中文</a>] | [繁體中文] | [<a href="../ja/README.md">日本語</a>]
</p>
**CowAgent** 是一個開源的超級 AI 助理,能夠主動思考和規劃任務、操作電腦和外部資源、創造和執行 Skills、構建知識庫與長期記憶、透過自主進化與你一同成長是 Agent Harness 工程的最佳實踐之一。
CowAgent 輕量、易部署、可擴充自由接入主流大模型覆蓋微信、飛書、釘釘、企微、QQ、Telegram、Slack、網頁等多渠道7×24 執行於個人電腦或伺服器中。
<p align="center">
<a href="https://cowagent.ai/?lang=zh">🌐 官網</a> &nbsp;·&nbsp;
<a href="https://docs.cowagent.ai/zh/">📖 文件中心</a> &nbsp;·&nbsp;
<a href="https://docs.cowagent.ai/zh/guide/quick-start">🚀 快速開始</a> &nbsp;·&nbsp;
<a href="https://skills.cowagent.ai/">🧩 技能廣場</a> &nbsp;·&nbsp;
<a href="https://link-ai.tech/cowagent/create">☁️ 線上體驗</a>
</p>
<br/>
## 🌟 核心能力
| 能力 | 說明 |
| :--- | :--- |
| [任務規劃](https://docs.cowagent.ai/zh/intro/architecture) | 理解複雜任務並自主分解執行,迴圈呼叫工具直到完成目標 |
| [長期記憶](https://docs.cowagent.ai/zh/memory) | 三層記憶架構(上下文 → 天級 → 核心),夢境蒸餾自動整理,支援關鍵詞與向量混合檢索 |
| [知識庫](https://docs.cowagent.ai/zh/knowledge) | 自動整理結構化知識為 Markdown Wiki構建持續增長的知識圖譜視覺化瀏覽 |
| [自主進化](https://docs.cowagent.ai/zh/memory/self-evolution) | 自動覆盤對話,最佳化技能、處理未完成事項、沉澱記憶與知識,在使用中持續成長 |
| [技能](https://docs.cowagent.ai/zh/skills) | 從 [Skill Hub](https://skills.cowagent.ai/)、GitHub、ClawHub 等一鍵安裝;也可透過對話創造自定義技能 |
| [工具](https://docs.cowagent.ai/zh/tools) | 內建檔案讀寫、終端、瀏覽器、定時任務、記憶檢索、聯網搜尋等 10+ 工具,支援 MCP 協議 |
| [通道](https://docs.cowagent.ai/zh/channels) | 一個 Agent 同時接入 Web、微信、飛書、釘釘、企微、QQ、公眾號、Telegram、Slack 等多個渠道 |
| 多模態 | 文字、圖片、語音、檔案全訊息型別支援,覆蓋識別、生成、收發 |
| [模型](https://docs.cowagent.ai/zh/models) | DeepSeek、Claude、Gemini、GPT、GLM、Qwen、Kimi、MiniMax、Doubao 等主流廠商,設定一行切換 |
| [部署](https://docs.cowagent.ai/zh/guide/quick-start) | 一鍵指令碼安裝Web 控制台統一管理本地、Docker、伺服器多種部署方式 |
<br/>
## 🏗️ 架構總覽
<img src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/architecture/zh/architecture.jpg" alt="CowAgent Architecture" width="750"/>
CowAgent 是一個完整的 **Agent Harness**:訊息從各類**通道**進入,**Agent Core** 結合記憶、知識庫與可用工具/技能進行任務規劃與決策,呼叫**模型**生成結果,再回傳至原通道。各模組解耦清晰,按需擴充。
詳見 [專案架構](https://docs.cowagent.ai/zh/intro/architecture)。
<br/>
## 🚀 快速開始
專案提供一鍵安裝指令碼,自動完成依賴安裝、設定和啟動:
**Linux / macOS**
```bash
bash <(curl -fsSL https://cdn.link-ai.tech/code/cow/run.sh)
```
**WindowsPowerShell**
```powershell
irm https://cdn.link-ai.tech/code/cow/run.ps1 | iex
```
**Docker**
```bash
curl -O https://cdn.link-ai.tech/code/cow/docker-compose.yml
docker compose up -d
```
啟動成功後開啟 `http://localhost:9899` 進入 **Web 控制台**,在控制台內即可完成模型設定、渠道接入、技能安裝等全部操作。
> 伺服器部署且需要從外部網路存取控制台時,請在 `config.json` 中將 `web_host` 設為 `0.0.0.0`(同時強烈建議設定 `web_password` 啟用身分驗證),然後訪問 `http://<server-ip>:9899`,並確保防火牆/安全組放行 `9899` 埠。
> 📖 詳細安裝指南:[快速開始](https://docs.cowagent.ai/zh/guide/quick-start) · [原始碼安裝](https://docs.cowagent.ai/zh/guide/manual-install) · [升級](https://docs.cowagent.ai/zh/guide/upgrade)
安裝後可使用 `cow` [CLI 命令](https://docs.cowagent.ai/zh/cli) 管理服務:
```bash
cow start | stop | restart # 服務管理
cow status | logs # 狀態和日誌
cow update # 拉取最新程式碼並重啟
cow skill install <名稱> # 安裝技能
cow install-browser # 安裝瀏覽器工具
```
<br/>
## 🤖 模型支援
CowAgent 支援國內外主流廠商的大語言模型。**文字對話、影像理解、影像生成、語音識別/合成、向量** 等能力均可獨立設定廠商。
| 廠商 | 代表模型 | 文字 | 影像理解 | 影像生成 | 語音識別 | 語音合成 | 向量 |
| --- | --- | :-: | :-: | :-: | :-: | :-: | :-: |
| [DeepSeek](https://docs.cowagent.ai/zh/models/deepseek) | deepseek-v4-flash / pro | ✅ | | | | | |
| [MiniMax](https://docs.cowagent.ai/zh/models/minimax) | MiniMax-M3 | ✅ | ✅ | ✅ | | ✅ | |
| [Claude](https://docs.cowagent.ai/zh/models/claude) | claude-sonnet-5 | ✅ | ✅ | | | | |
| [Gemini](https://docs.cowagent.ai/zh/models/gemini) | gemini-3.5-flash | ✅ | ✅ | ✅ | | | |
| [OpenAI](https://docs.cowagent.ai/zh/models/openai) | gpt-5.5、o 系列 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [智譜 GLM](https://docs.cowagent.ai/zh/models/glm) | glm-5.2、glm-5v-turbo | ✅ | ✅ | | ✅ | | ✅ |
| [通義千問](https://docs.cowagent.ai/zh/models/qwen) | qwen3.7-plus | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [豆包 Doubao](https://docs.cowagent.ai/zh/models/doubao) | doubao-seed-2.1 系列 | ✅ | ✅ | ✅ | | | ✅ |
| [Kimi](https://docs.cowagent.ai/zh/models/kimi) | kimi-k2.7-code | ✅ | ✅ | | | | |
| [百度ERNIE](https://docs.cowagent.ai/zh/models/qianfan) | ernie-5.1 | ✅ | ✅ | | | | |
| [小米 MiMo](https://docs.cowagent.ai/zh/models/mimo) | mimo-v2.5-pro / v2.5 | ✅ | ✅ | | | ✅ | |
| [LinkAI](https://docs.cowagent.ai/zh/models/linkai) | 一個 Key 接入 100+ 模型 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| [自定義](https://docs.cowagent.ai/zh/models/custom) | 本地模型 / 三方代理 | ✅ | | | | | |
> 推薦透過 Web 控制台線上設定,無需手動編輯檔案。手動設定請參考各廠商文件,詳見 [模型概覽](https://docs.cowagent.ai/zh/models)。
<br/>
## 💬 通道接入
一個 Agent 例項可同時接入多個渠道,啟動時透過 `channel_type` 切換或並行執行。
| 通道 | 文字 | 圖片 | 檔案 | 語音 | 群聊 |
| --- | :-: | :-: | :-: | :-: | :-: |
| [Web 控制台](https://docs.cowagent.ai/zh/channels/web)(預設) | ✅ | ✅ | ✅ | ✅ | |
| [微信](https://docs.cowagent.ai/zh/channels/weixin) | ✅ | ✅ | ✅ | ✅ | |
| [飛書](https://docs.cowagent.ai/zh/channels/feishu) | ✅ | ✅ | ✅ | ✅ | ✅ |
| [釘釘](https://docs.cowagent.ai/zh/channels/dingtalk) | ✅ | ✅ | ✅ | ✅ | ✅ |
| [企微智慧機器人](https://docs.cowagent.ai/zh/channels/wecom-bot) | ✅ | ✅ | ✅ | ✅ | ✅ |
| [QQ](https://docs.cowagent.ai/zh/channels/qq) | ✅ | ✅ | ✅ | | ✅ |
| [企業微信應用](https://docs.cowagent.ai/zh/channels/wecom) | ✅ | ✅ | ✅ | ✅ | |
| [微信客服](https://docs.cowagent.ai/zh/channels/wechat-kf) | ✅ | ✅ | ✅ | ✅ | |
| [微信公眾號](https://docs.cowagent.ai/zh/channels/wechatmp) | ✅ | ✅ | | ✅ | |
| [Telegram](https://docs.cowagent.ai/zh/channels/telegram) | ✅ | ✅ | ✅ | ✅ | ✅ |
| [Slack](https://docs.cowagent.ai/zh/channels/slack) | ✅ | ✅ | ✅ | | ✅ |
| [Discord](https://docs.cowagent.ai/zh/channels/discord) | ✅ | ✅ | ✅ | | ✅ |
> 飛書、企微智慧機器人支援在 Web 控制台內**掃碼一鍵接入**,無需公有 IP。詳見 [通道概覽](https://docs.cowagent.ai/zh/channels)。
<img src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/zh/web-console-chat.png" alt="CowAgent Web 控制台" width="800"/>
*Web 控制台是預設通道,也是統一的 Agent 設定和管理入口*
<br/>
## 🧠 記憶與知識庫
**長期記憶**採用三層架構:對話上下文(短期)→ 天級記憶(中期)→ MEMORY.md長期。每日自動執行**夢境蒸餾Deep Dream**,將分散記憶整合為精煉的長期記憶並生成敘事日記。詳見 [長期記憶](https://docs.cowagent.ai/zh/memory) · [夢境蒸餾](https://docs.cowagent.ai/zh/memory/deep-dream)。
**個人知識庫** 與按時間記錄的記憶不同,以**主題為維度**組織結構化知識。Agent 在對話中自動整理有價值資訊維護交叉引用與索引Web 控制台可視覺化瀏覽知識圖譜。詳見 [個人知識庫](https://docs.cowagent.ai/zh/knowledge)。
<table>
<tr>
<td width="50%">
<img src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/zh/web-console-memory.png" alt="長期記憶" />
<p align="center"><em>長期記憶 · 三層記憶 + 夢境蒸餾</em></p>
</td>
<td width="50%">
<img src="https://cdn.jsdelivr.net/gh/zhayujie/cowagent-assets@main/screenshots/zh/web-console-knowledge.png" alt="個人知識庫" />
<p align="center"><em>個人知識庫 · 自動整理的 Markdown Wiki</em></p>
</td>
</tr>
</table>
<br/>
## 🔧 工具與技能
**工具Tools** 是 Agent 作業系統資源的原子能力,**技能Skills** 是基於說明檔案的高階工作流,可組合多個工具完成複雜任務。
### 工具系統
**內建工具** 涵蓋檔案讀寫(`read` / `write` / `edit` / `ls`)、終端(`bash`)、檔案傳送(`send`)、記憶檢索(`memory`)、環境變數(`env_config`)、網頁獲取(`web_fetch`)、定時任務(`scheduler`)、聯網搜尋(`web_search`)、影像識別(`vision`)、瀏覽器自動化(`browser`)等常用能力。
**MCP 協議** 透過 [Model Context Protocol](https://modelcontextprotocol.io) 接入開放生態中的各種 MCP 服務,設定一次 `mcp.json` 即用即得,支援 stdio / SSE 協議、熱更新、零程式碼接入。
詳見 [工具概覽](https://docs.cowagent.ai/zh/tools) · [MCP 整合](https://docs.cowagent.ai/zh/tools/mcp)。
### 技能系統
- **[Skill Hub](https://skills.cowagent.ai/)** — 開源的技能廣場,瀏覽、搜尋、一鍵安裝
- **GitHub / ClawHub / URL 等** — 任意來源一鍵安裝
- **對話創造** — 透過 `skill-creator` 用對話快速生成自定義技能,可將工作流程或第三方介面直接固化為技能
```bash
/skill list # 檢視當前技能
/skill search <關鍵詞> # 在技能廣場搜尋
/skill install <名稱> # 一鍵安裝
```
詳見 [技能概覽](https://docs.cowagent.ai/zh/skills) · [建立技能](https://docs.cowagent.ai/zh/skills/create)。
<br/>
## 🏷 更新日誌
> **2026.06.18** [v2.1.2](https://github.com/zhayujie/CowAgent/releases/tag/2.1.2) — Web 控制台升級定時任務管理、知識庫分類、多模型自定義廠商、自主進化最佳化、新模型接入kimi-k2.7-code、glm-5.2)、安全加固和體驗最佳化
> **2026.06.09** [v2.1.1](https://github.com/zhayujie/CowAgent/releases/tag/2.1.1) — 自進化能力、Web 控制台升級訊息管理、多會話並行、新模型接入MiniMax-M3、qwen3.7-plus、Python 3.13 支援
> **2026.06.01** [v2.1.0](https://github.com/zhayujie/CowAgent/releases/tag/2.1.0) — 國際化支援、新增通道Telegram、Discord、Slack、微信客服、命令列互動升級、一鍵安裝指令碼最佳化、MCP Streamable HTTP 支援、新模型接入claude-opus-4-8、MiMo
> **2026.05.22** [v2.0.9](https://github.com/zhayujie/CowAgent/releases/tag/2.0.9) — 模型管理、MCP 協議支援、瀏覽器登入態持久化、新模型接入gpt-5.5、gemini-3.5-flash、qwen3.7-max、部署安全加固
> **2026.05.06** [v2.0.8](https://github.com/zhayujie/CowAgent/releases/tag/2.0.8) — 飛書渠道全面升級語音、流式輸出、掃碼接入、新模型支援DeepSeek V4、百度千帆、定時任務工具增強
> **2026.04.22** [v2.0.7](https://github.com/zhayujie/CowAgent/releases/tag/2.0.7) — 影像生成內建技能GPT Image 2、Nano Banana、新模型支援Kimi K2.6、Claude Opus 4.7、GLM 5.1)、知識庫和記憶增強
> **2026.04.14** [v2.0.6](https://github.com/zhayujie/CowAgent/releases/tag/2.0.6) — 知識庫系統、夢境記憶模組、上下文智慧壓縮、Web 控制台多會話
> **2026.04.01** [v2.0.5](https://github.com/zhayujie/CowAgent/releases/tag/2.0.5) — Cow CLI 命令系統、Skill Hub 開源、瀏覽器工具、企微掃碼建立
> **2026.03.22** [v2.0.4](https://github.com/zhayujie/CowAgent/releases/tag/2.0.4) — 新增個人微信通道,支援文字/圖片/檔案/語音訊息
> **2026.02.03** [v2.0.0](https://github.com/zhayujie/CowAgent/releases/tag/2.0.0) — 正式升級為超級 Agent 助理支援多輪任務決策、長期記憶、Skills 框架
完整更新歷史:[Release Notes](https://docs.cowagent.ai/zh/releases)
<br/>
## 🤝 社群與支援
掃碼加入微信開源交流群:
<img width="130" src="https://img-1317903499.cos.ap-guangzhou.myqcloud.com/docs/open-community.png">
也可透過以下方式獲取支援:
- 🐛 [提交 Issue](https://github.com/zhayujie/CowAgent/issues)
- 🤖 線上 AI 助手:[專案小助手](https://link-ai.tech/app/Kv2fXJcH)(基於專案知識庫)
<br/>
## 🔗 相關專案
- **[Cow Skill Hub](https://github.com/zhayujie/cow-skill-hub)** — 開源的 AI Agent 技能廣場,支援 CowAgent、OpenClaw、Claude Code 等多種 Agent
- **[bot-on-anything](https://github.com/zhayujie/bot-on-anything)** — 輕量大模型應用框架,支援 Slack、Telegram、Discord、Gmail 等海外平臺
- **[AgentMesh](https://github.com/MinimalFuture/AgentMesh)** — 開源多智慧體Multi-Agent框架透過團隊協同解決複雜問題
<br/>
## 🏢 企業服務
<a href="https://link-ai.tech" target="_blank"><img width="650" src="https://cdn.link-ai.tech/image/link-ai-intro.jpg"></a>
> [LinkAI](https://link-ai.tech/) 是面向企業和個人的一站式 AI 智慧體平臺,為 CowAgent 提供雲端託管和企業級支援:
>
> - **🚀 免部署線上執行**:無需伺服器即可建立 [CowAgent 線上助理](https://link-ai.tech/cowagent/create)1 分鐘擁有專屬 Agent
> - **🧠 Agent 基礎設施**:聚合主流大模型、知識庫、資料庫、技能、工作流,提供開箱即用的 Agent 能力擴充
> - **🏢 企業級協作**:提供團隊協作、許可權分級、審計日誌、私有化部署等能力,讓 Agent 安全落地企業場景
**產品諮詢和企業服務** 可聯絡產品客服:
<img width="130" src="https://cdn.link-ai.tech/portal/linkai-customer-service.png">
<br/>
## 🛠️ 開發與貢獻
歡迎各種形式的貢獻新功能、Bug 修復、效能最佳化、文件完善,或向 [Skill Hub](https://skills.cowagent.ai/submit) 分享你的技能。請先閱讀 [CONTRIBUTING.md](/CONTRIBUTING.md) 瞭解如何開始,然後提交 Issue 討論或直接發起 PR。
歡迎 ⭐ Star 支援專案,並透過 Watch → Custom → Releases 訂閱新版本通知。也歡迎提交 PR、Issue 進行反饋。
## 🌟 貢獻者
![cow contributors](https://contrib.rocks/image?repo=zhayujie/CowAgent&max=1000)
<br/>
## ⚠️ 宣告
1. 本專案遵循 [MIT 開源協議](/LICENSE),主要用於技術研究和學習。使用時請遵守所在地法律法規及相關政策,因使用本專案所產生的一切後果由使用者自行承擔。
2. **成本與安全:** Agent 模式 Token 消耗顯著高於普通對話請根據效果與成本權衡選擇模型Agent 具備訪問本地作業系統的能力,請謹慎選擇部署環境。
3. CowAgent 專案專注於開源技術開發,不會參與、授權或發行任何加密貨幣。
<br/>
## 📌 專案更名說明
本專案原名 `chatgpt-on-wechat`,於 2026.04.13 正式更名為 **CowAgent**。原 GitHub 地址已自動重定向,老使用者可選擇執行 `git remote set-url origin https://github.com/zhayujie/CowAgent.git` 更新本地遠端地址。

View File

@@ -12,7 +12,7 @@
</p> </p>
<p align="center"> <p align="center">
[<a href="../../README.md">English</a>] | [中文] | [<a href="../ja/README.md">日本語</a>] [<a href="../../README.md">English</a>] | [中文] | [<a href="README-Hant.md">繁體中文</a>] | [<a href="../ja/README.md">日本語</a>]
</p> </p>
**CowAgent** 是一个开源的超级 AI 助理,能够主动思考和规划任务、操作计算机和外部资源、创造和执行 Skills、构建知识库与长期记忆、通过自主进化与你一同成长是 Agent Harness 工程的最佳实践之一。 **CowAgent** 是一个开源的超级 AI 助理,能够主动思考和规划任务、操作计算机和外部资源、创造和执行 Skills、构建知识库与长期记忆、通过自主进化与你一同成长是 Agent Harness 工程的最佳实践之一。

View File

@@ -20,3 +20,7 @@ pypdf
python-docx python-docx
openpyxl openpyxl
python-pptx python-pptx
# i18n: Traditional Chinese (zh-Hant) high-quality conversion
# Built-in fallback provides basic conversion without this dependency
opencc-python-reimplemented