mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
refactor(i18n): reply to upstream's #2935 feedback
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
[English] | [<a href="docs/zh/README.md">中文</a>] | [<a href="docs/zh/README-tw.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.
|
||||||
|
|||||||
@@ -47,13 +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' / 'zh-tw' / '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().replace('_', '-');
|
var v = String(raw).trim().toLowerCase().replace('_', '-');
|
||||||
if (v === 'auto') return '';
|
if (v === 'auto') return '';
|
||||||
if (v.indexOf('zh-tw') === 0 || v.indexOf('zh-hk') === 0 || v.indexOf('zh-hant') >= 0) return 'zh-tw';
|
// 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 '';
|
||||||
|
|||||||
@@ -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: '加载工具中...',
|
||||||
@@ -256,7 +257,7 @@ const I18N = {
|
|||||||
edit_cancel: '取消',
|
edit_cancel: '取消',
|
||||||
logout: '退出',
|
logout: '退出',
|
||||||
},
|
},
|
||||||
'zh-tw': {
|
'zh-Hant': {
|
||||||
|
|
||||||
console: '控制台',
|
console: '控制台',
|
||||||
nav_chat: '對話', nav_manage: '管理', nav_monitor: '監控',
|
nav_chat: '對話', nav_manage: '管理', nav_monitor: '監控',
|
||||||
@@ -393,8 +394,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: '載入工具中...',
|
||||||
@@ -638,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...',
|
||||||
@@ -759,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 '';
|
||||||
@@ -799,9 +805,15 @@ function applyI18n() {
|
|||||||
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) {
|
if (langLabel) {
|
||||||
if (currentLang === 'zh-tw') langLabel.textContent = '繁中';
|
if (currentLang === 'zh-Hant') langLabel.textContent = '繁中';
|
||||||
else if (currentLang === 'zh') langLabel.textContent = '簡中';
|
else if (currentLang === 'zh') langLabel.textContent = '簡中';
|
||||||
else langLabel.textContent = 'EN';
|
else langLabel.textContent = 'EN';
|
||||||
}
|
}
|
||||||
@@ -814,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' || lang === 'zh' || lang === 'zh-tw') ? lang : '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);
|
||||||
@@ -855,7 +867,7 @@ function syncLanguageToBackend(lang, callback) {
|
|||||||
function updateLangControls() {
|
function updateLangControls() {
|
||||||
const langLabel = document.getElementById('lang-label');
|
const langLabel = document.getElementById('lang-label');
|
||||||
if (langLabel) {
|
if (langLabel) {
|
||||||
if (currentLang === 'zh-tw') langLabel.textContent = '繁體';
|
if (currentLang === 'zh-Hant') langLabel.textContent = '繁體';
|
||||||
else if (currentLang === 'zh') langLabel.textContent = '簡體';
|
else if (currentLang === 'zh') langLabel.textContent = '簡體';
|
||||||
else langLabel.textContent = 'EN';
|
else langLabel.textContent = 'EN';
|
||||||
}
|
}
|
||||||
@@ -866,7 +878,7 @@ function updateLangControls() {
|
|||||||
sel._ddValue = currentLang;
|
sel._ddValue = currentLang;
|
||||||
const textEl = sel.querySelector('.cfg-dropdown-text');
|
const textEl = sel.querySelector('.cfg-dropdown-text');
|
||||||
if (textEl) {
|
if (textEl) {
|
||||||
if (currentLang === 'zh-tw') textEl.textContent = '繁體中文';
|
if (currentLang === 'zh-Hant') textEl.textContent = '繁體中文';
|
||||||
else if (currentLang === 'zh') textEl.textContent = '简体中文';
|
else if (currentLang === 'zh') textEl.textContent = '简体中文';
|
||||||
else textEl.textContent = 'English';
|
else textEl.textContent = 'English';
|
||||||
}
|
}
|
||||||
@@ -878,8 +890,8 @@ function updateLangControls() {
|
|||||||
|
|
||||||
function toggleLanguage() {
|
function toggleLanguage() {
|
||||||
if (currentLang === 'zh') {
|
if (currentLang === 'zh') {
|
||||||
setLanguage('zh-tw');
|
setLanguage('zh-Hant');
|
||||||
} else if (currentLang === 'zh-tw') {
|
} else if (currentLang === 'zh-Hant') {
|
||||||
setLanguage('en');
|
setLanguage('en');
|
||||||
} else {
|
} else {
|
||||||
setLanguage('zh');
|
setLanguage('zh');
|
||||||
@@ -1018,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4554,7 +4572,7 @@ function initConfigView(data) {
|
|||||||
if (langSel) {
|
if (langSel) {
|
||||||
initDropdown(
|
initDropdown(
|
||||||
langSel,
|
langSel,
|
||||||
[{ value: 'zh', label: '简体中文' }, { value: 'zh-tw', label: '繁體中文' }, { value: 'en', label: 'English' }],
|
[{ value: 'zh', label: '简体中文' }, { value: 'zh-Hant', label: '繁體中文' }, { value: 'en', label: 'English' }],
|
||||||
currentLang,
|
currentLang,
|
||||||
(val) => setLanguage(val)
|
(val) => setLanguage(val)
|
||||||
);
|
);
|
||||||
@@ -4727,8 +4745,11 @@ 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() {
|
||||||
const model = getSelectedModel();
|
const model = getSelectedModel();
|
||||||
@@ -4840,18 +4861,33 @@ 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');
|
const logoutBtn = document.getElementById('logout-btn-header');
|
||||||
if (logoutBtn) logoutBtn.classList.add('hidden');
|
if (logoutBtn) logoutBtn.classList.add('hidden');
|
||||||
setTimeout(() => { window.location.reload(); }, 1500);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showStatus('cfg-password-status', 'config_save_error', true);
|
showStatus('cfg-password-status', 'config_save_error', true);
|
||||||
@@ -9019,7 +9055,7 @@ 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-tw') {
|
} else if (currentLang === 'zh-Hant') {
|
||||||
subtitle.textContent = '請輸入密碼以存取控制台';
|
subtitle.textContent = '請輸入密碼以存取控制台';
|
||||||
loginBtn.textContent = '登入';
|
loginBtn.textContent = '登入';
|
||||||
} else {
|
} else {
|
||||||
@@ -9052,7 +9088,7 @@ function showLoginScreen() {
|
|||||||
if (logoutBtn) logoutBtn.classList.remove('hidden');
|
if (logoutBtn) logoutBtn.classList.remove('hidden');
|
||||||
initApp();
|
initApp();
|
||||||
} else {
|
} else {
|
||||||
if (currentLang === 'zh-tw') {
|
if (currentLang === 'zh-Hant') {
|
||||||
errEl.textContent = '密碼錯誤';
|
errEl.textContent = '密碼錯誤';
|
||||||
} else if (currentLang === 'zh') {
|
} else if (currentLang === 'zh') {
|
||||||
errEl.textContent = '密码错误';
|
errEl.textContent = '密码错误';
|
||||||
@@ -9065,7 +9101,7 @@ function showLoginScreen() {
|
|||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
if (currentLang === 'zh-tw') {
|
if (currentLang === 'zh-Hant') {
|
||||||
errEl.textContent = '網路錯誤,請重試';
|
errEl.textContent = '網路錯誤,請重試';
|
||||||
} else if (currentLang === 'zh') {
|
} else if (currentLang === 'zh') {
|
||||||
errEl.textContent = '网络错误,请重试';
|
errEl.textContent = '网络错误,请重试';
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1864,7 +1868,6 @@ class ConfigHandler:
|
|||||||
json.dump(file_cfg, f, indent=4, ensure_ascii=False)
|
json.dump(file_cfg, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
logger.info(f"[WebChannel] Config updated: {list(applied.keys())}")
|
logger.info(f"[WebChannel] Config updated: {list(applied.keys())}")
|
||||||
restarted = False
|
|
||||||
|
|
||||||
# Apply a language change immediately so backend logs, agent
|
# Apply a language change immediately so backend logs, agent
|
||||||
# replies and CLI output switch without a restart.
|
# replies and CLI output switch without a restart.
|
||||||
@@ -1875,25 +1878,25 @@ 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}")
|
||||||
|
|
||||||
# Trigger a dynamic restart of the web channel if web_password is changed,
|
# Check if password was cleared: if there was a password before clearing,
|
||||||
# so the socket re-binds to either 127.0.0.1 or 0.0.0.0 immediately.
|
# the service is likely bound to 0.0.0.0 (public), so warn the user.
|
||||||
|
password_warning = None
|
||||||
if "web_password" in applied:
|
if "web_password" in applied:
|
||||||
try:
|
new_password = applied["web_password"]
|
||||||
import sys
|
configured_host = file_cfg.get("web_host", "")
|
||||||
app_module = sys.modules.get('__main__') or sys.modules.get('app')
|
|
||||||
mgr = getattr(app_module, '_channel_mgr', None) if app_module else None
|
# If password was cleared and there was a password before
|
||||||
if mgr:
|
if not new_password and old_password:
|
||||||
def delay_restart():
|
# If web_host is not explicitly set, the service auto-binds based on password
|
||||||
time.sleep(0.5)
|
# With password → 0.0.0.0 (public), without password → 127.0.0.1 (local)
|
||||||
mgr.restart("web")
|
# So clearing password when it was previously set means going from public to local
|
||||||
threading.Thread(
|
if not configured_host or configured_host == "0.0.0.0":
|
||||||
target=delay_restart,
|
password_warning = "password_cleared_with_public_host"
|
||||||
daemon=True,
|
logger.warning(
|
||||||
).start()
|
"[WebChannel] Password cleared while service is likely bound to 0.0.0.0. "
|
||||||
restarted = True
|
"Consider restarting the service to rebind to 127.0.0.1 "
|
||||||
logger.info("[WebChannel] Web channel restart scheduled due to password change")
|
"or explicitly set web_host in config to prevent unauthorized access."
|
||||||
except Exception as restart_err:
|
)
|
||||||
logger.warning(f"[WebChannel] Failed to trigger web channel restart: {restart_err}")
|
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -1907,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, "restarted": restarted}, 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)})
|
||||||
@@ -3694,7 +3697,7 @@ class ChannelsHandler:
|
|||||||
# 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_tw = i18n.get_language() == i18n.ZH_TW
|
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
|
||||||
@@ -3707,11 +3710,11 @@ class ChannelsHandler:
|
|||||||
display_val = raw_val
|
display_val = raw_val
|
||||||
|
|
||||||
label_val = f["label"]
|
label_val = f["label"]
|
||||||
if is_tw and isinstance(label_val, str):
|
if is_hant and isinstance(label_val, str):
|
||||||
label_val = i18n.to_traditional(label_val)
|
label_val = i18n.to_traditional(label_val)
|
||||||
elif is_tw and isinstance(label_val, dict):
|
elif is_hant and isinstance(label_val, dict):
|
||||||
label_val = label_val.copy()
|
label_val = label_val.copy()
|
||||||
label_val["zh-tw"] = i18n.to_traditional(label_val.get("zh", ""))
|
label_val["zh-Hant"] = i18n.to_traditional(label_val.get("zh", ""))
|
||||||
|
|
||||||
fields_out.append({
|
fields_out.append({
|
||||||
"key": f["key"],
|
"key": f["key"],
|
||||||
@@ -3722,11 +3725,11 @@ class ChannelsHandler:
|
|||||||
})
|
})
|
||||||
|
|
||||||
label_val = ch_def["label"]
|
label_val = ch_def["label"]
|
||||||
if is_tw and isinstance(label_val, str):
|
if is_hant and isinstance(label_val, str):
|
||||||
label_val = i18n.to_traditional(label_val)
|
label_val = i18n.to_traditional(label_val)
|
||||||
elif is_tw and isinstance(label_val, dict):
|
elif is_hant and isinstance(label_val, dict):
|
||||||
label_val = label_val.copy()
|
label_val = label_val.copy()
|
||||||
label_val["zh-tw"] = i18n.to_traditional(label_val.get("zh", ""))
|
label_val["zh-Hant"] = i18n.to_traditional(label_val.get("zh", ""))
|
||||||
|
|
||||||
ch_info = {
|
ch_info = {
|
||||||
"name": ch_name,
|
"name": ch_name,
|
||||||
@@ -4297,7 +4300,7 @@ class ToolsHandler:
|
|||||||
try:
|
try:
|
||||||
instance = cls()
|
instance = cls()
|
||||||
desc = instance.description
|
desc = instance.description
|
||||||
if lang == i18n.ZH_TW and desc:
|
if lang == i18n.ZH_HANT and desc:
|
||||||
desc = i18n.to_traditional(desc)
|
desc = i18n.to_traditional(desc)
|
||||||
elif lang == "en" and name == "scheduler":
|
elif lang == "en" and name == "scheduler":
|
||||||
desc = (
|
desc = (
|
||||||
@@ -4328,7 +4331,7 @@ class SkillsHandler:
|
|||||||
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_TW:
|
if i18n.get_language() == i18n.ZH_HANT:
|
||||||
for skill in skills:
|
for skill in skills:
|
||||||
if isinstance(skill, dict):
|
if isinstance(skill, dict):
|
||||||
for k, v in list(skill.items()):
|
for k, v in list(skill.items()):
|
||||||
|
|||||||
@@ -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,9 +36,9 @@ import sys
|
|||||||
|
|
||||||
# Supported language codes
|
# Supported language codes
|
||||||
ZH = "zh"
|
ZH = "zh"
|
||||||
ZH_TW = "zh-tw"
|
ZH_HANT = "zh-Hant"
|
||||||
EN = "en"
|
EN = "en"
|
||||||
SUPPORTED = (ZH, ZH_TW, EN)
|
SUPPORTED = (ZH, ZH_HANT, EN)
|
||||||
DEFAULT_LANG = EN
|
DEFAULT_LANG = EN
|
||||||
|
|
||||||
# Mapped Simplified to Traditional characters in this codebase
|
# Mapped Simplified to Traditional characters in this codebase
|
||||||
@@ -71,6 +79,20 @@ _PHRASE_MAP = {
|
|||||||
|
|
||||||
|
|
||||||
def to_traditional(text):
|
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:
|
if not text:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
@@ -111,7 +133,7 @@ def _normalize(raw):
|
|||||||
return None
|
return None
|
||||||
# Traditional Chinese variants: zh-tw, zh-hk, zh-hant, zh-hant-tw, zh-hant-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:
|
if value.startswith("zh-tw") or value.startswith("zh-hk") or "hant" in value:
|
||||||
return ZH_TW
|
return ZH_HANT
|
||||||
# General or Simplified Chinese variants: zh, zh-cn, zh-hans...
|
# 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
|
||||||
@@ -231,7 +253,7 @@ def get_language():
|
|||||||
|
|
||||||
|
|
||||||
def is_zh():
|
def is_zh():
|
||||||
return get_language() in (ZH, ZH_TW)
|
return get_language() in (ZH, ZH_HANT)
|
||||||
|
|
||||||
|
|
||||||
def t(zh_text, en_text):
|
def t(zh_text, en_text):
|
||||||
@@ -241,6 +263,6 @@ def t(zh_text, en_text):
|
|||||||
t("已中止", "Cancelled")
|
t("已中止", "Cancelled")
|
||||||
"""
|
"""
|
||||||
lang = get_language()
|
lang = get_language()
|
||||||
if lang == ZH_TW:
|
if lang == ZH_HANT:
|
||||||
return to_traditional(zh_text)
|
return to_traditional(zh_text)
|
||||||
return zh_text if lang == ZH else en_text
|
return zh_text if lang == ZH else en_text
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
[<a href="../../README.md">English</a>] | [中文] | [<a href="README-tw.md">繁體中文</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 工程的最佳实践之一。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -36,7 +36,3 @@ discord.py
|
|||||||
web.py; python_version < "3.13"
|
web.py; python_version < "3.13"
|
||||||
web.py @ git+https://github.com/webpy/webpy.git ; python_version >= "3.13"
|
web.py @ git+https://github.com/webpy/webpy.git ; python_version >= "3.13"
|
||||||
legacy-cgi; python_version >= "3.13"
|
legacy-cgi; python_version >= "3.13"
|
||||||
|
|
||||||
# i18n translation mapping
|
|
||||||
opencc-python-reimplemented
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user