diff --git a/README.en.md b/README.en.md
index 2050afcc..816e8cc2 100644
--- a/README.en.md
+++ b/README.en.md
@@ -25,21 +25,26 @@ Turns [Windsurf](https://windsurf.com) (formerly Codeium)'s AI models into **two
## What is it doing?
-```
- ┌─────────────┐ /v1/chat/completions ┌────────────┐
- │ OpenAI SDK │ ──────────────────────→ │ │
- │ curl / Frontend │ ←────────────────────── │ │
- └─────────────┘ OpenAI JSON + SSE │ WindsurfAPI│
- │ Node.js │ ┌──────────────┐ ┌─────────────────┐
- ┌─────────────┐ /v1/messages │ (This Service) │ gRPC │ Language │ HTTPS │ Windsurf Cloud │
- │ Claude Code │ ──────────────────────→ │ │ ───→ │ Server (LS) │ ────→ │ server.self- │
- │ Cline │ ←────────────────────── │ │ ←─── │ (Windsurf │ ←─── │ serve.windsurf │
- │ Cursor │ Anthropic SSE │ │ │ binary) │ │ .com │
- └─────────────┘ └────────────┘ └──────────────┘ └─────────────────┘
- ↑
- Account Pool Round-Robin
- Rate Limit Isolation
- Failover
+```mermaid
+flowchart LR
+ subgraph Clients
+ A[OpenAI SDK
curl / Frontend]
+ B[Claude Code
Cline
Cursor]
+ end
+
+ subgraph WindsurfAPI["WindsurfAPI (Node.js)"]
+ C[HTTP Service
Port 3003]
+ D[Account Pool
Round-Robin
Rate Limit
Failover]
+ end
+
+ E["Language Server
(Windsurf binary)"]
+ F[Windsurf Cloud
server.self-serve.windsurf.com]
+
+ A -->|"/v1/chat/completions"
OpenAI JSON + SSE| C
+ B -->|"/v1/messages"
Anthropic SSE| C
+ C <-->|gRPC| E
+ E <-->|HTTPS| F
+ D -.-> C
```
**What it does**:
@@ -52,29 +57,26 @@ Turns [Windsurf](https://windsurf.com) (formerly Codeium)'s AI models into **two
The model itself does **not** operate on files — file operations are executed locally by the IDE Agent client (Claude Code, Cline, etc.):
-```
- You "Help me fix a bug" Claude Code WindsurfAPI Windsurf Cloud
- │ │ │ │
- │────────────────────────────→ │ │ │
- │ │ POST /v1/messages │ │
- │ │ messages + tools + system │ │
- │ │ ─────────────────────────────→│ Package into Cascade request │
- │ │ │ ──────────────────────→ │
- │ │ │ │
- │ │ │ Model thinks → returns
- │ │ │ tool_use(edit_file)
- │ │ │ ←────────────────────── │
- │ │ ←── Anthropic SSE ────────────│ │
- │ │ content_block=tool_use │ │
- │ │ │ │
- │ │ Execute edit_file() locally │ │
- │ │ (Read/write local files) │ │
- │ │ │ │
- │ │ Send another turn with tool_result │ │
- │ │ ─────────────────────────────→│ ──────────────────────→ │
- │ │ ... (loop) ...
- │ │ │ │
- │ ← Final answer │ │ │
+```mermaid
+sequenceDiagram
+ actor U as You
+ participant CC as Claude Code
+ participant WA as WindsurfAPI
+ participant WC as Windsurf Cloud
+
+ U->>CC: "Help me fix a bug"
+ CC->>WA: POST /v1/messages
messages + tools + system
+ WA->>WC: Package into Cascade request
+ WC-->>WA: Model thinks → returns
tool_use(edit_file)
+ WA-->>CC: Anthropic SSE
content_block=tool_use
+ CC->>CC: Execute edit_file() locally
(Read/write local files)
+ CC->>WA: Send tool_result
+ WA->>WC: Continue conversation...
+ loop Conversation Loop
+ WC-->>WA: Response
+ WA-->>CC: SSE stream
+ end
+ CC-->>U: Final answer
```
**Key Point**: WindsurfAPI is only responsible for **passing** `tool_use` / `tool_result`. The client CLI is what actually modifies the files.
@@ -165,6 +167,10 @@ LS_PORT=42100
DASHBOARD_PASSWORD=
EOF
+# Note: Inline comments are supported in .env for unquoted values:
+# PORT=3003 # Service port
+# Quoted values preserve everything inside the quotes.
+
node src/index.js
```
@@ -181,16 +187,16 @@ Open `http://YOUR_IP:3003/dashboard` → Login to get token → Click **Sign in
Go to [windsurf.com/show-auth-token](https://windsurf.com/show-auth-token) to copy your token:
```bash
-curl -X POST http://localhost:3003/auth/login
- -H "Content-Type: application/json"
+curl -X POST http://localhost:3003/auth/login
+ -H "Content-Type: application/json"
-d '{"token": "YOUR_TOKEN"}'
```
**Method 3: Batch**
```bash
-curl -X POST http://localhost:3003/auth/login
- -H "Content-Type: application/json"
+curl -X POST http://localhost:3003/auth/login
+ -H "Content-Type: application/json"
-d '{"accounts": [{"token": "t1"}, {"token": "t2"}]}'
```
@@ -218,9 +224,9 @@ claude # Use Claude Code as usual
```bash
# Raw curl test
-curl http://localhost:3003/v1/messages
- -H "Authorization: Bearer YOUR_KEY"
- -H "anthropic-version: 2023-06-01"
+curl http://localhost:3003/v1/messages
+ -H "Authorization: Bearer YOUR_KEY"
+ -H "anthropic-version: 2023-06-01"
-d '{"model":"claude-opus-4.6","max_tokens":100,"messages":[{"role":"user","content":"Hello"}]}'
```
@@ -250,12 +256,21 @@ In your client's settings for **Custom OpenAI Compatible**:
| `PORT` | `3003` | Service port |
| `API_KEY` | empty | API key required for requests. Leave empty to disable validation. |
| `DATA_DIR` | project root | Directory for persisted JSON state and `logs/`. Docker deployments should usually use `/data`. |
+| `CODEIUM_API_KEY` | empty | Direct API key from Windsurf (alternative to token-based auth). |
+| `CODEIUM_AUTH_TOKEN` | empty | Token from [windsurf.com/show-auth-token](https://windsurf.com/show-auth-token). |
+| `CODEIUM_EMAIL` | empty | Email for Windsurf account authentication. |
+| `CODEIUM_PASSWORD` | empty | Password for Windsurf account authentication. |
+| `CODEIUM_API_URL` | `https://server.self-serve.windsurf.com` | Windsurf cloud API endpoint. |
| `DEFAULT_MODEL` | `claude-4.5-sonnet-thinking` | The model to use if `model` is not specified. |
| `MAX_TOKENS` | `8192` | Default maximum number of response tokens. |
| `LOG_LEVEL` | `info` | debug / info / warn / error |
| `LS_BINARY_PATH` | `/opt/windsurf/language_server_linux_x64` | Path to the LS binary. |
| `LS_PORT` | `42100` | LS gRPC port. |
+| `LS_DATA_DIR` | `/opt/windsurf` | Per-proxy LS data directory root. |
| `DASHBOARD_PASSWORD` | empty | Dashboard password. Leave empty for no password. |
+| `CASCADE_REUSE_STRICT` | `0` | Set to `1` for strict conversation reuse mode (waits for same fingerprint). |
+| `CASCADE_REUSE_STRICT_RETRY_MS` | `60000` | Retry delay in ms for strict reuse mode. |
+| `CASCADE_REUSE_HASH_SYSTEM` | `0` | Set to `1` to include system messages in conversation reuse hash. |
## Dashboard Features
@@ -306,6 +321,10 @@ deepseek-v3 / v3-2 / r1 · grok-3 / mini / mini-thinking / code-fast-1 · qwen-3
> **Free accounts** can only use `gpt-4o-mini` and `gemini-2.5-flash`. Others require Windsurf Pro.
+### Language-Following for CJK Users
+
+The service automatically detects Chinese, Japanese, or Korean characters in your messages and injects a language-following hint to ensure the model responds in the same language. This fixes the issue where Claude Code's large English system prompt would override the communication language.
+
## Architecture Highlights
- **Zero npm dependencies** Everything uses `node:*` built-ins · Protobuf is handcrafted (`src/proto.js`) · Download and run.
diff --git a/docs/dashboard-i18n.md b/docs/dashboard-i18n.md
new file mode 100644
index 00000000..fc4879be
--- /dev/null
+++ b/docs/dashboard-i18n.md
@@ -0,0 +1,252 @@
+# Dashboard Internationalization (i18n) Guide
+
+This document describes the internationalization system used in the WindsurfAPI dashboard.
+
+## Overview
+
+The dashboard supports multiple languages through JSON locale bundles. Currently implemented:
+- English (`en`)
+- Simplified Chinese (`zh-CN`)
+
+## Architecture
+
+### Files
+
+| File | Purpose |
+|------|---------|
+| `src/dashboard/i18n/en.json` | English translations |
+| `src/dashboard/i18n/zh-CN.json` | Chinese translations |
+| `src/dashboard/index.html` | Main dashboard (contains I18n helper) |
+| `src/dashboard/check-i18n.js` | Regression protection script |
+
+### I18n Helper
+
+The `I18n` object in `index.html` provides:
+
+```javascript
+// Load locale bundles
+await I18n.init('en'); // or 'zh-CN'
+
+// Translate a key
+I18n.t('nav.overview'); // "Dashboard"
+I18n.t('card.activeAccounts.title'); // "Active Accounts"
+
+// Translate with placeholders
+I18n.t('card.activeAccounts.subtitle', { total: 5, error: 1 });
+// Result: "5 total · 1 abnormal"
+```
+
+## Adding New Translations
+
+### 1. Static HTML Content
+
+Add `data-i18n` attributes to translatable elements:
+
+```html
+
+
Active Accounts
+
+
+Active Accounts
+```
+
+Update both locale files:
+
+```json
+// en.json
+{
+ "section": {
+ "accounts": {
+ "title": "Active Accounts"
+ }
+ }
+}
+
+// zh-CN.json
+{
+ "section": {
+ "accounts": {
+ "title": "活跃账号"
+ }
+ }
+}
+```
+
+### 2. Dynamic JavaScript Content
+
+Use `I18n.t()` in JavaScript:
+
+```javascript
+// Before
+element.textContent = `成功 ${count} 个`;
+
+// After
+element.textContent = I18n.t('toast.successCount', { count });
+```
+
+Add to locale files:
+
+```json
+// en.json
+{ "toast": { "successCount": "Success: {{count}}" } }
+
+// zh-CN.json
+{ "toast": { "successCount": "成功 {{count}} 个" } }
+```
+
+### 3. Backend Error Codes
+
+Backend should return error codes, not raw messages:
+
+```javascript
+// Before
+res.json({ error: '邮箱或密码错误' });
+
+// After
+res.json({ error: 'ERR_INVALID_CREDENTIALS' });
+```
+
+Add the error code to locale files:
+
+```json
+// en.json
+{ "error": { "ERR_INVALID_CREDENTIALS": "Invalid email or password" } }
+
+// zh-CN.json
+{ "error": { "ERR_INVALID_CREDENTIALS": "邮箱或密码错误" } }
+```
+
+## Key Naming Conventions
+
+### Structure
+```
+..
+```
+
+Examples:
+- `nav.overview` - Navigation items
+- `page.accounts.title` - Page titles
+- `card.activeAccounts.title` - Card titles
+- `button.save` - Button labels
+- `toast.saved` - Toast messages
+- `error.ERR_*` - Error codes
+
+### Common Sections
+
+| Section | Usage |
+|---------|-------|
+| `brand` | Brand name and subtitle |
+| `nav` | Navigation items |
+| `page` | Page titles and subtitles |
+| `pageSubtitle` | Page descriptions |
+| `section` | Panel titles and descriptions |
+| `card` | Metric card titles |
+| `button` | Button labels |
+| `action` | Action labels |
+| `field` | Form field labels and placeholders |
+| `table` | Table headers |
+| `toast` | Toast notification messages |
+| `help` | Help text |
+| `status` | Status messages |
+| `confirm` | Confirmation dialog titles/descriptions |
+| `modal` | Modal dialog content |
+| `log` | Log level names |
+| `batch` | Batch import messages |
+| `error` | Error messages and codes |
+| `footer` | Footer text |
+| `experimental` | Experimental feature labels |
+
+## Placeholder Syntax
+
+Use `{{variableName}}` for dynamic values:
+
+```json
+{
+ "card": {
+ "activeAccounts": {
+ "subtitle": "{{total}} total · {{error}} abnormal"
+ }
+ }
+}
+```
+
+Usage:
+```javascript
+I18n.t('card.activeAccounts.subtitle', { total: 5, error: 1 });
+// "5 total · 1 abnormal"
+```
+
+## Regression Protection
+
+Run the check script before committing:
+
+```bash
+cd src/dashboard
+node check-i18n.js
+```
+
+This checks for:
+1. Hardcoded Chinese text in HTML/JS
+2. Missing translation keys
+3. Keys present in one locale but not the other
+4. `data-i18n` attributes referencing non-existent keys
+
+### CI Integration
+
+Add to your CI pipeline:
+
+```yaml
+# .github/workflows/i18n-check.yml
+name: I18n Check
+on: [push, pull_request]
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ - run: node src/dashboard/check-i18n.js
+```
+
+## Language Toggle
+
+The dashboard persists language preference in `localStorage`:
+
+```javascript
+// Toggle language
+I18n.toggle();
+
+// Get current locale
+const current = I18n.currentLocale; // 'en' or 'zh-CN'
+```
+
+## Best Practices
+
+1. **Always add both locales**: When adding a new key, add it to both `en.json` and `zh-CN.json`
+
+2. **Use descriptive keys**: Prefer `card.activeAccounts.title` over `card.title1`
+
+3. **Keep placeholders semantic**: Use `{{email}}` not `{{e}}`
+
+4. **Error codes**: Use `ERR_*` prefix for backend error codes
+
+5. **Test both languages**: Verify your changes in both EN and CN modes
+
+6. **Run regression check**: Use `check-i18n.js` before committing
+
+## Troubleshooting
+
+### Key not translating
+- Check that the key exists in both locale files
+- Verify the key path is correct (e.g., `section.accounts.title` vs `accounts.title`)
+- Check browser console for I18n errors
+
+### Locale not loading
+- Verify the locale file is valid JSON
+- Check browser Network tab for 404 errors on `/dashboard/i18n/*.json`
+- Ensure `I18n.init()` completed before using `I18n.t()`
+
+### Mixed languages showing
+- Some content may be cached; hard refresh (Ctrl+F5) the page
+- Check that all dynamic content uses `I18n.t()`
+- Verify no hardcoded strings remain in JavaScript
diff --git a/src/dashboard/api.js b/src/dashboard/api.js
index fe9994d7..928f38e2 100644
--- a/src/dashboard/api.js
+++ b/src/dashboard/api.js
@@ -52,8 +52,9 @@ function checkAuth(req) {
async function processWindsurfLogin({ email, password, loginProxy, autoAdd }) {
if (!email || !password) {
- const err = new Error('email 和 password 为必填');
+ const err = new Error('ERR_EMAIL_PASSWORD_REQUIRED');
err.statusCode = 400;
+ err.code = 'ERR_EMAIL_PASSWORD_REQUIRED';
throw err;
}
@@ -175,7 +176,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
// ─── Proxy test — try an HTTP CONNECT through the given proxy ──
if (subpath === '/test-proxy' && method === 'POST') {
const { host, port, username, password, type = 'http' } = body || {};
- if (!host || !port) return json(res, 400, { ok: false, error: '缺少 host 或 port' });
+ if (!host || !port) return json(res, 400, { ok: false, error: 'ERR_HOST_PORT_REQUIRED' });
const startTime = Date.now();
try {
const result = await testProxy({ host, port: Number(port), username, password, type });
@@ -210,7 +211,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
return json(res, 200, {
ok: false,
dirty: true,
- error: '工作区有未提交的修改(SFTP 部署或手动改过代码)。确定要覆盖本地修改用远程最新版本吗?',
+ error: 'ERR_UNCOMMITTED_CHANGES',
dirtyFiles: dirty.split('\n').slice(0, 20),
});
}
@@ -523,7 +524,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
try {
const { accounts, proxy: loginProxy, autoAdd } = body || {};
if (!Array.isArray(accounts) || !accounts.length) {
- return json(res, 400, { error: 'accounts 为必填数组' });
+ return json(res, 400, { error: 'ERR_ACCOUNTS_REQUIRED' });
}
const results = [];
@@ -563,9 +564,9 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
if (subpath === '/batch-import' && method === 'POST') {
try {
const { text, autoAdd = true } = body || {};
- if (!text || typeof text !== 'string') return json(res, 400, { error: '缺少 text 字段' });
+ if (!text || typeof text !== 'string') return json(res, 400, { error: 'ERR_TEXT_REQUIRED' });
const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
- if (!lines.length) return json(res, 400, { error: '没有有效行' });
+ if (!lines.length) return json(res, 400, { error: 'ERR_NO_VALID_LINES' });
const results = [];
for (const line of lines) {
@@ -579,7 +580,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
email = parts[0];
password = parts[1];
} else {
- results.push({ success: false, email: line.slice(0, 30), error: '格式错误 需要 [proxy] email password' });
+ results.push({ success: false, email: line.slice(0, 30), error: 'ERR_FORMAT_INVALID' });
continue;
}
try {
@@ -616,7 +617,7 @@ export async function handleDashboardApi(method, subpath, body, req, res) {
if (subpath === '/oauth-login' && method === 'POST') {
try {
const { idToken, refreshToken, email, provider, autoAdd } = body;
- if (!idToken) return json(res, 400, { error: '缺少 idToken' });
+ if (!idToken) return json(res, 400, { error: 'ERR_IDTOKEN_REQUIRED' });
const proxy = getProxyConfig().global;
const { apiKey, name } = await reRegisterWithCodeium(idToken, proxy);
@@ -731,7 +732,7 @@ async function gitStatus() {
const PRIVATE_PROXY_HOST = /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|0\.|localhost$|0\.0\.0\.0$)/i;
async function testProxy({ host, port, username, password, type }) {
- if (PRIVATE_PROXY_HOST.test(host)) throw new Error('代理地址不能指向内网/本机');
+ if (PRIVATE_PROXY_HOST.test(host)) throw new Error('ERR_PROXY_PRIVATE_IP');
const { isSocks, createSocksTunnel } = await import('../socks.js');
const tls = await import('node:tls');
const targetHost = 'api.ipify.org';
@@ -755,11 +756,11 @@ async function testProxy({ host, port, username, password, type }) {
timeout: 10000,
});
req.on('connect', (res, sock) => {
- if (res.statusCode !== 200) { sock.destroy(); return reject(new Error(`代理返回 HTTP ${res.statusCode}`)); }
+ if (res.statusCode !== 200) { sock.destroy(); return reject(new Error(`ERR_PROXY_HTTP_ERROR:${res.statusCode}`)); }
resolve(sock);
});
- req.on('error', (err) => reject(new Error(`连接失败: ${err.message}`)));
- req.on('timeout', () => { req.destroy(); reject(new Error('超时(10s)')); });
+ req.on('error', (err) => reject(new Error(`ERR_CONNECTION_FAILED:${err.message}`)));
+ req.on('timeout', () => { req.destroy(); reject(new Error('ERR_TIMEOUT')); });
req.end();
});
}
@@ -777,10 +778,10 @@ async function testProxy({ host, port, username, password, type }) {
const ip = match ? match[1].trim() : '';
tlsSock.destroy();
if (!ip || !/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
- return reject(new Error('TLS 隧道建立但返回内容异常'));
+ return reject(new Error('ERR_TLS_TUNNEL_ERROR'));
}
resolve({ egressIp: ip, type });
});
- tlsSock.on('error', (err) => reject(new Error(`TLS 失败: ${err.message}`)));
+ tlsSock.on('error', (err) => reject(new Error(`ERR_TLS_FAILED:${err.message}`)));
});
}
diff --git a/src/dashboard/check-i18n.js b/src/dashboard/check-i18n.js
new file mode 100644
index 00000000..09c05d4c
--- /dev/null
+++ b/src/dashboard/check-i18n.js
@@ -0,0 +1,262 @@
+#!/usr/bin/env node
+/**
+ * I18n Regression Protection Script
+ *
+ * This script checks for:
+ * 1. Hardcoded Chinese text in dashboard HTML/JS
+ * 2. Missing translation keys in locale files
+ * 3. Keys present in one locale but not the other
+ *
+ * Usage: node check-i18n.js
+ * Exit code: 0 if all checks pass, 1 if violations found
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const RED = '\x1b[31m';
+const GREEN = '\x1b[32m';
+const YELLOW = '\x1b[33m';
+const RESET = '\x1b[0m';
+
+let exitCode = 0;
+let violations = 0;
+
+function logError(msg) {
+ console.log(`${RED}✗${RESET} ${msg}`);
+ violations++;
+ exitCode = 1;
+}
+
+function logWarn(msg) {
+ console.log(`${YELLOW}⚠${RESET} ${msg}`);
+}
+
+function logOk(msg) {
+ console.log(`${GREEN}✓${RESET} ${msg}`);
+}
+
+// Chinese character regex
+const CHINESE_REGEX = /[\u4e00-\u9fff]/;
+
+// Patterns that are allowed to contain Chinese (whitelisted)
+const WHITELIST_PATTERNS = [
+ /data-i18n=/, // i18n attributes are ok
+ /\/\/.*/, // comments
+ /https?:\/\//, // URLs
+ /windsurf\.com/, // windsurf domain
+ /firebase/, // Firebase references
+];
+
+function isWhitelisted(line) {
+ return WHITELIST_PATTERNS.some(p => p.test(line));
+}
+
+function checkFileForChinese(filePath, content) {
+ const lines = content.split('\n');
+ let found = false;
+
+ lines.forEach((line, idx) => {
+ if (CHINESE_REGEX.test(line) && !isWhitelisted(line)) {
+ logError(`${filePath}:${idx + 1}: ${line.trim().slice(0, 80)}`);
+ found = true;
+ }
+ });
+
+ return found;
+}
+
+function extractKeys(obj, prefix = '') {
+ const keys = [];
+ for (const [k, v] of Object.entries(obj)) {
+ const fullKey = prefix ? `${prefix}.${k}` : k;
+ if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
+ keys.push(...extractKeys(v, fullKey));
+ } else {
+ keys.push(fullKey);
+ }
+ }
+ return keys;
+}
+
+function compareLocales(enKeys, zhKeys) {
+ const enSet = new Set(enKeys);
+ const zhSet = new Set(zhKeys);
+
+ const onlyInEn = enKeys.filter(k => !zhSet.has(k));
+ const onlyInZh = zhKeys.filter(k => !enSet.has(k));
+
+ return { onlyInEn, onlyInZh };
+}
+
+// Main checks
+console.log('\n📋 I18n Regression Check\n');
+
+// 1. Check HTML file for hardcoded Chinese
+const htmlPath = path.join(__dirname, 'index.html');
+const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
+console.log('Checking index.html for hardcoded Chinese...');
+const htmlHasChinese = checkFileForChinese('index.html', htmlContent);
+if (!htmlHasChinese) {
+ logOk('No hardcoded Chinese found in HTML');
+}
+
+// 2. Check API files for Chinese error messages
+const apiPath = path.join(__dirname, '../dashboard/api.js');
+if (fs.existsSync(apiPath)) {
+ const apiContent = fs.readFileSync(apiPath, 'utf-8');
+ console.log('\nChecking api.js for Chinese error messages...');
+
+ // Look for Chinese in error messages (not in comments)
+ const lines = apiContent.split('\n');
+ let foundApiChinese = false;
+ lines.forEach((line, idx) => {
+ // Skip comments
+ if (/^\s*\/\//.test(line)) return;
+ // Check for Chinese in error: or throw new Error(
+ if (/error.*:.*'[^']*[一-鿿]/.test(line) || /throw new Error\('[^']*[一-鿿]/.test(line)) {
+ if (!isWhitelisted(line)) {
+ logError(`api.js:${idx + 1}: ${line.trim().slice(0, 80)}`);
+ foundApiChinese = true;
+ }
+ }
+ });
+
+ if (!foundApiChinese) {
+ logOk('No Chinese error messages found in API');
+ }
+}
+
+// 3. Compare locale files
+console.log('\nComparing locale files...');
+const enPath = path.join(__dirname, 'i18n/en.json');
+const zhPath = path.join(__dirname, 'i18n/zh-CN.json');
+
+const enJson = JSON.parse(fs.readFileSync(enPath, 'utf-8'));
+const zhJson = JSON.parse(fs.readFileSync(zhPath, 'utf-8'));
+
+const enKeys = extractKeys(enJson).sort();
+const zhKeys = extractKeys(zhJson).sort();
+
+const { onlyInEn, onlyInZh } = compareLocales(enKeys, zhKeys);
+
+if (onlyInEn.length > 0) {
+ logError(`Keys in en.json but missing in zh-CN.json (${onlyInEn.length}):`);
+ onlyInEn.forEach(k => console.log(` - ${k}`));
+}
+
+if (onlyInZh.length > 0) {
+ logError(`Keys in zh-CN.json but missing in en.json (${onlyInZh.length}):`);
+ onlyInZh.forEach(k => console.log(` - ${k}`));
+}
+
+if (onlyInEn.length === 0 && onlyInZh.length === 0) {
+ logOk('Locale files are synchronized');
+}
+
+// 4. Check for data-i18n usage consistency
+console.log('\nChecking data-i18n attribute usage...');
+const i18nRegex = /data-i18n="([^"]+)"/g;
+const usedKeys = [];
+let match;
+while ((match = i18nRegex.exec(htmlContent)) !== null) {
+ usedKeys.push(match[1]);
+}
+
+// Check if all used keys exist in locales
+const missingInLocales = [];
+for (const key of usedKeys) {
+ // Navigate nested key path
+ const parts = key.split('.');
+ let enVal = enJson;
+ let zhVal = zhJson;
+ let exists = true;
+
+ for (const part of parts) {
+ enVal = enVal?.[part];
+ zhVal = zhVal?.[part];
+ if (enVal === undefined || zhVal === undefined) {
+ exists = false;
+ break;
+ }
+ }
+
+ if (!exists) {
+ missingInLocales.push(key);
+ }
+}
+
+if (missingInLocales.length > 0) {
+ logError(`data-i18n keys missing in locales (${missingInLocales.length}):`);
+ missingInLocales.forEach(k => console.log(` - ${k}`));
+} else {
+ logOk('All data-i18n keys exist in locales');
+}
+
+// 5. Check for I18n.t() calls in JavaScript code
+console.log('\nChecking I18n.t() calls in JavaScript code...');
+const i18nCallRegex = /I18n\.t\(['"`]([^'"`]+)['"`]/g;
+const jsKeys = [];
+while ((match = i18nCallRegex.exec(htmlContent)) !== null) {
+ jsKeys.push(match[1]);
+}
+
+// Also check for I18n.t() with variables (e.g., I18n.t(key))
+const i18nVarRegex = /I18n\.t\(\s*([^)]+)\s*\)/g;
+while ((match = i18nVarRegex.exec(htmlContent)) !== null) {
+ const keyExpr = match[1].trim();
+ // Skip if it's a variable expression (not a string literal)
+ if (!/^[`'"']/.test(keyExpr) && !/^\${/.test(keyExpr) && keyExpr !== 'key') {
+ jsKeys.push(keyExpr);
+ }
+}
+
+// Deduplicate
+const uniqueJsKeys = [...new Set(jsKeys)];
+
+// Check if all I18n.t() keys exist in locales
+const missingJsKeys = [];
+for (const key of uniqueJsKeys) {
+ // Skip template expressions and variables
+ if (key.includes('${') || key === 'key' || key.includes('?') || key.includes('vars')) continue;
+
+ // Navigate nested key path
+ const parts = key.split('.');
+ let enVal = enJson;
+ let zhVal = zhJson;
+ let exists = true;
+
+ for (const part of parts) {
+ enVal = enVal?.[part];
+ zhVal = zhVal?.[part];
+ if (enVal === undefined || zhVal === undefined) {
+ exists = false;
+ break;
+ }
+ }
+
+ if (!exists) {
+ missingJsKeys.push(key);
+ }
+}
+
+if (missingJsKeys.length > 0) {
+ logError(`I18n.t() keys missing in locales (${missingJsKeys.length}):`);
+ missingJsKeys.forEach(k => console.log(` - ${k}`));
+} else {
+ logOk('All I18n.t() keys exist in locales');
+}
+
+// Summary
+console.log(`\n${'='.repeat(50)}`);
+if (violations === 0) {
+ console.log(`${GREEN}✓ All i18n checks passed!${RESET}`);
+} else {
+ console.log(`${RED}✗ Found ${violations} violation(s)${RESET}`);
+}
+console.log(`${'='.repeat(50)}\n`);
+
+process.exit(exitCode);
diff --git a/src/dashboard/i18n/en.json b/src/dashboard/i18n/en.json
new file mode 100644
index 00000000..2f6a766a
--- /dev/null
+++ b/src/dashboard/i18n/en.json
@@ -0,0 +1,841 @@
+{
+ "_meta": {
+ "name": "English",
+ "code": "en"
+ },
+
+ "brand": {
+ "sub": "Control Panel"
+ },
+
+ "nav": {
+ "group": {
+ "overview": "Overview",
+ "account": "Accounts",
+ "system": "System",
+ "about": "About"
+ },
+ "overview": "Dashboard",
+ "stats": "Statistics",
+ "windsurf-login": "Account Login",
+ "accounts": "Account Pool",
+ "bans": "Health Check",
+ "models": "Model Control",
+ "proxy": "Proxy",
+ "logs": "Logs",
+ "experimental": "Experimental",
+ "credits": "Credits"
+ },
+
+ "page": {
+ "overview": "Dashboard",
+ "stats": "Statistics",
+ "windsurf-login": "Account Login",
+ "accounts": "Account Pool",
+ "bans": "Health Check",
+ "models": "Model Control",
+ "proxy": "Proxy Config",
+ "logs": "Runtime Logs",
+ "experimental": "Experimental Features",
+ "credits": "Credits"
+ },
+
+ "pageTitle": {
+ "main": "WindsurfAPI bydwgx1337 Control Panel"
+ },
+
+ "pageSubtitle": {
+ "overview": "System status and key metrics overview",
+ "windsurf-login": "Login to Windsurf via Google / GitHub / Email to get API Key",
+ "accounts": "Manage account pool, probe subscription tiers and available models",
+ "models": "Configure model access policies, limit available model scope",
+ "proxy": "Configure global or per-account exit proxies; each dedicated proxy spawns an independent Language Server instance",
+ "logs": "Receive server logs in real-time via SSE streaming",
+ "stats": "Request volume, success rate, latency percentiles and account/model dimension stats",
+ "bans": "Track error accounts and abnormal status",
+ "experimental": "Unstable optimizations; disable anytime if issues occur",
+ "credits": "Thanks to contributors who submitted PRs / audited code / reported issues"
+ },
+
+ "button": {
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "forceOverride": "Force Overwrite & Update",
+ "save": "Save",
+ "restart": "Restart",
+ "updateAndRestart": "Update and Restart"
+ },
+
+ "action": {
+ "checkUpdate": "Check Update",
+ "applyUpdate": "Update & Restart",
+ "restart": "Restart",
+ "reset": "Reset",
+ "save": "Save",
+ "clearPool": "Clear Pool",
+ "login": "Login",
+ "clear": "Clear",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "delete": "Delete",
+ "refresh": "Refresh",
+ "probe": "Probe",
+ "probeTitle": "Probe capabilities",
+ "enable": "Enable",
+ "disable": "Disable",
+ "copy": "Copy",
+ "copyTitle": "Click to copy",
+ "configure": "Configure",
+ "test": "Test",
+ "readClipboard": "Read Clipboard",
+ "batchImport": "Batch Import & Login",
+ "paste": "Paste",
+ "add": "Add",
+ "close": "Close",
+ "edit": "Edit",
+ "selectAll": "Select All",
+ "selectNone": "Select None",
+ "invert": "Invert",
+ "showPassword": "Show/Hide Password"
+ },
+
+ "status": {
+ "active": "Active",
+ "disabled": "Disabled",
+ "error": "Error",
+ "running": "Running",
+ "stopped": "Stopped",
+ "success": "Success",
+ "failed": "Failed",
+ "pending": "Pending",
+ "unknown": "Unknown",
+ "rateLimited": "Rate Limited",
+ "loading": "Loading...",
+ "saving": "Saving...",
+ "testing": "Testing...",
+ "updateAndRestartHint": "Update and restart the service",
+ "proxySuccess": "✓ Connected. Egress IP {{ip}} · {{latency}}ms",
+ "proxyFailed": "✗ Failed: {{error}} · {{latency}}ms",
+ "checkingUpdate": "Checking for updates...",
+ "updateAvailable": "Update available",
+ "currentVersion": "Current:",
+ "remoteVersion": "Remote:",
+ "upToDate": "✓ Up to date ({{commit}}) · {{message}}",
+ "pullingRestarting": "Pulling + restarting...",
+ "cancelled": "Cancelled",
+ "noUpdateNeeded": "Already up to date, no update needed",
+ "updateComplete": "Update complete {{before}} → {{after}}",
+ "restarting": "Service restarting in background, page will auto-refresh in ~8 seconds...",
+ "notGitRepo": "Not a git repository",
+ "notGitRepoHint": "Ensure .git directory is present for one-click updates",
+ "workingDirDirty": "Working directory has local changes",
+ "workingDirDirtyHint": "The following files have local changes:
{{files}}
Continue will overwrite with remote version.",
+ "notFetched": "Not fetched",
+ "fetchFailed": "Fetch failed",
+ "clickToEditTier": "Click to manually edit tier (use when probe fails)"
+ },
+
+ "tier": {
+ "pro": "PRO",
+ "free": "FREE",
+ "expired": "Expired",
+ "unknown": "Unknown",
+ "expiredLabel": "Expired"
+ },
+
+ "creditsBar": {
+ "daily": "D",
+ "weekly": "W",
+ "monthly": "M"
+ },
+
+ "section": {
+ "banStatus": {
+ "title": "Health Status"
+ },
+ "lsStatus": {
+ "title": "Language Server",
+ "desc": "Language Server instance status"
+ },
+ "oauthLogin": {
+ "title": "Quick Login (Recommended)",
+ "desc": "Login to Windsurf directly with your Google or GitHub account — no password needed"
+ },
+ "emailLogin": {
+ "title": "Email & Password Login",
+ "desc": "For accounts registered with email+password only. Third-party login please use buttons above. Supports single login or batch import in \"[proxy] email password\" format."
+ },
+ "loginProxy": {
+ "title": "Login Proxy (Optional)",
+ "desc": "Specify a proxy for this login session; leave empty to use global proxy. When proxy takes effect, subsequent chat requests will also egress through this proxy."
+ },
+ "loginHistory": {
+ "title": "Login History",
+ "desc": "Local record of last 50 login operations"
+ },
+ "addAccount": {
+ "title": "Add Account",
+ "desc": "Supports both API Key and Auth Token methods",
+ "hint": "Recommended: Use Token method. Visit {{link}} after logging in, copy the Token and paste below. Works with all login methods (Email/Google/GitHub)."
+ },
+ "accountList": {
+ "title": "Account List",
+ "desc": "Click probe button to update capability info for a specific account"
+ },
+ "modelPolicy": {
+ "title": "Access Policy",
+ "desc": "Choose \"Allow All\" for no restrictions; \"Allowlist\" only permits listed models; \"Blocklist\" blocks listed models"
+ },
+ "modelList": {
+ "title": "Model List",
+ "titleAllow": "Allowed Models",
+ "titleBlock": "Blocked Models",
+ "descAllow": "Only selected models are allowed; unselected models return 403 error",
+ "descBlock": "Selected models will be blocked; unselected models work normally",
+ "search": "Search models...",
+ "provider": "Provider"
+ },
+ "globalProxy": {
+ "title": "Global Proxy",
+ "desc": "Accounts without dedicated proxy will use this configuration"
+ },
+ "accountProxy": {
+ "title": "Per-Account Proxy",
+ "desc": "Set dedicated proxy for specific accounts; each dedicated proxy spawns an independent LS instance"
+ },
+ "identityInjection": {
+ "title": "Model Identity Injection",
+ "desc": "When enabled, injects a system prompt at the start of each request, overriding Cascade's built-in Windsurf identity; each provider's template below can be customized and takes effect immediately after saving."
+ },
+ "cascadeReuse": {
+ "title": "Cascade Conversation Reuse",
+ "desc": "Reuse the same cascade_id across multi-turn conversations, only sending the latest user message to Windsurf. Let the server maintain context cache. Significantly reduces TTFB and upload volume on cache hits; automatically falls back to new session on cache miss or account/LS change. Requires client to retain full history and append in order (e.g., new-api, OpenWebUI).",
+ "enable": "Enable Cascade Conversation Reuse",
+ "hint": "Disabled by default. When enabled, takes effect immediately on current conversation pool."
+ },
+ "identityPrompt": {
+ "title": "Model Identity Prompt",
+ "desc": "Inject model identity system prompt at the start of each request.",
+ "enable": "Enable Model Identity Injection",
+ "hint": "Enabled by default; templates below won't work when disabled.",
+ "templateHint": "Each provider's template is editable. {{model}} placeholder will be replaced with the requested model name."
+ },
+ "systemPrompts": {
+ "title": "System Prompts",
+ "desc": "Tool injection, conversation mode and other built-in prompt templates. Changes take effect immediately; click \"Reset\" to restore defaults."
+ },
+ "lsPool": {
+ "title": "Language Server Pool",
+ "desc": "{{count}} LS instances · one process per dedicated proxy",
+ "sectionTitle": "Language Server Pool",
+ "sectionDesc": "Dedicated Language Server instances per proxy",
+ "port": "Port",
+ "defaultInstance": "Default Instance",
+ "noProxy": "No proxy"
+ },
+ "modelStats": {
+ "title": "Model Usage Statistics",
+ "desc": "Aggregated by model: requests, success count, error count, success rate, avg duration and p50/p95 latency percentiles"
+ },
+ "accountStats": {
+ "title": "Account Dimension Statistics",
+ "desc": "Request volume and success rate per account ID (first 8 chars)"
+ },
+ "accountHealth": {
+ "title": "Account Health Status"
+ },
+ "requestChart": {
+ "title": "Request Volume Time Series"
+ }
+ },
+
+ "help": {
+ "token": "Recommended: Use Token method. Visit {{link}} after logging in, copy the Token and paste below. Works with all login methods (Email/Google/GitHub).",
+ "tokenGuide": "Recommended: Use Token method. Visit ",
+ "tokenGuideEnd": " after logging in, copy the Token and paste below. Works with all login methods (Email/Google/GitHub)."
+ },
+
+ "field": {
+ "email": {
+ "label": "Email",
+ "placeholder": "your-email@example.com"
+ },
+ "password": {
+ "label": "Password",
+ "placeholder": "••••••••"
+ },
+ "type": {
+ "label": "Type"
+ },
+ "tier": {
+ "label": "Tier"
+ },
+ "provider": {
+ "label": "Provider",
+ "all": "All Providers"
+ },
+ "searchModel": {
+ "label": "Search Model",
+ "placeholder": "Enter model name to filter..."
+ },
+ "proxyUser": {
+ "placeholder": "Proxy auth username"
+ },
+ "proxyPass": {
+ "placeholder": "Proxy auth password"
+ },
+ "searchProvider": {
+ "placeholder": "Search providers..."
+ },
+ "host": {
+ "label": "Host",
+ "placeholder": "proxy.example.com"
+ },
+ "port": {
+ "label": "Port",
+ "placeholder": "8080"
+ },
+ "username": {
+ "label": "Username",
+ "placeholder": "Optional"
+ },
+ "passwordProxy": {
+ "label": "Password",
+ "placeholder": "Optional"
+ },
+ "key": {
+ "label": "Key / Token",
+ "placeholder": "Paste Auth Token (from windsurf.com/show-auth-token)"
+ },
+ "label": {
+ "label": "Label",
+ "placeholder": "Optional"
+ },
+ "batchInput": {
+ "label": "Batch Import",
+ "hint": "One account per line. Format: [proxy] email password. Proxy is optional, supports http/socks5. Proxy will be automatically bound to corresponding account."
+ },
+ "proxyType": {
+ "label": "Type"
+ },
+ "proxyHost": {
+ "label": "Host",
+ "placeholder": "Leave empty = use global"
+ },
+ "providerFilter": {
+ "label": "Provider"
+ },
+ "logLevel": {
+ "label": "Log Level"
+ },
+ "identitySearch": {
+ "placeholder": "Search providers..."
+ },
+ "search": {
+ "label": "Search",
+ "placeholder": "Search..."
+ },
+ "autoScroll": "Auto-scroll",
+ "autoAdd": "Auto-add to pool after login"
+ },
+
+ "table": {
+ "header": {
+ "time": "Time",
+ "email": "Email",
+ "status": "Status",
+ "proxy": "Proxy",
+ "action": "Action",
+ "id": "ID",
+ "label": "Label",
+ "tier": "Tier",
+ "rpm": "RPM",
+ "credits": "Credits",
+ "models": "Available Models",
+ "error": "Error",
+ "lastUsed": "Last Used",
+ "key": "Key",
+ "accountId": "Account ID",
+ "account": "Account",
+ "requests": "Requests",
+ "success": "Success",
+ "errors": "Errors",
+ "successRate": "Success Rate",
+ "avg": "Avg",
+ "p50": "p50",
+ "p95": "p95",
+ "errorCount": "Error Count",
+ "model": "Model",
+ "note": "Note"
+ },
+ "empty": {
+ "loginHistory": "No login records",
+ "accounts": "No accounts yet, please add one first",
+ "accountsProxy": "No accounts",
+ "modelStats": "No model statistics available",
+ "accountStats": "No account statistics available",
+ "banList": "No abnormal accounts",
+ "models": "No matching models",
+ "credits": "Loading...",
+ "accountNotFound": "Account not found",
+ "noValidAccounts": "No valid accounts available for routing",
+ "probeFailed": "Probe failed",
+ "noRequestData": "No request data",
+ "noAccountRequestData": "No account-level request data",
+ "noData": "No data"
+ }
+ },
+
+ "card": {
+ "activeAccounts": {
+ "title": "Active Accounts",
+ "subtitle": "{{total}} total · {{error}} abnormal"
+ },
+ "totalRequests": {
+ "title": "Total Requests",
+ "subtitle": "{{rate}}% success rate"
+ },
+ "uptime": {
+ "title": "Uptime",
+ "subtitle": "Started at {{time}}"
+ },
+ "languageServer": {
+ "title": "Language Server",
+ "running": "Running",
+ "stopped": "Stopped",
+ "subtitle": "{{count}} instances · Port {{port}}"
+ },
+ "responseCache": {
+ "title": "Response Cache",
+ "subtitle": "{{hits}} hit / {{misses}} miss · {{size}}/{{max}} entries"
+ },
+ "banStats": {
+ "title": "Health Overview"
+ },
+ "abnormalAccounts": {
+ "title": "Abnormal Accounts",
+ "subtitle": "Total {{count}} accounts"
+ },
+ "disabled": {
+ "title": "Disabled",
+ "subtitle": "Auto-disabled due to errors"
+ },
+ "rateLimit": {
+ "title": "Rate Limited",
+ "badge": "Rate Limited",
+ "subtitle": "Temporarily excluded from scheduling"
+ },
+ "stats": {
+ "title": "Total Requests",
+ "subtitle": "Running {{time}}"
+ },
+ "success": {
+ "title": "Success",
+ "subtitle": "{{rate}}% success rate"
+ },
+ "errors": {
+ "title": "Errors",
+ "subtitle": "{{rate}}% failure rate"
+ },
+ "p95Latency": {
+ "title": "Avg p95 Latency",
+ "subtitle": "{{count}} models"
+ },
+ "hitRate": {
+ "title": "Hit Rate",
+ "subtitle": "{{hits}} hit / {{misses}} miss"
+ },
+ "poolSize": {
+ "title": "Pool Size",
+ "subtitle": "Max {{max}} · TTL {{ttl}} min"
+ },
+ "stores": {
+ "title": "Stores",
+ "subtitle": "Expired {{expired}} · Evicted {{evicted}}"
+ },
+ "defaultInstance": "Default Instance",
+ "noProxy": "No proxy",
+ "restarts": "restarts",
+ "custom": "Customized"
+ },
+
+ "login": {
+ "title": "Control Panel Login",
+ "subtitle": "Please enter the admin password to continue",
+ "passwordPlaceholder": "Dashboard Password",
+ "button": "Login"
+ },
+
+ "range": {
+ "6h": "Last 6h",
+ "24h": "Last 24h",
+ "72h": "Last 72h"
+ },
+
+ "credits": {
+ "contributors": "Core Contributors",
+ "cta": {
+ "title": "Want to join this list?",
+ "desc": "Open an issue or pull request on GitHub — bug fixes, new models, UI improvements, or security findings all get recognised here.",
+ "issue": "Open Issue",
+ "pr": "Open PR"
+ }
+ },
+
+ "experimental": {
+ "enable": "Enable",
+ "disable": "Disable",
+ "cascadeReuse": {
+ "title": "Enable Cascade Conversation Reuse",
+ "desc": "Disabled by default. When enabled, takes effect immediately on current conversation pool.",
+ "enable": "Enable Cascade Conversation Reuse",
+ "hint": "Disabled by default. When enabled, takes effect immediately on current conversation pool."
+ },
+ "identityPrompt": {
+ "title": "Enable Model Identity Injection",
+ "desc": "Enabled by default; templates below won't work when disabled."
+ },
+ "templateHint": "Each provider's template is editable. {{model}} placeholder will be replaced with the requested model name (e.g., claude-opus-4.6).",
+ "clearPool": "Clear Conversation Pool",
+ "refresh": "Refresh",
+ "noMatch": "No matches",
+ "restoreDefault": "Restore Default",
+ "save": "Save",
+ "default": "Default",
+ "toolReinforcement": "Tool Call Format Guide",
+ "communicationWithTools": "Communication with Tools",
+ "communicationNoTools": "Communication without Tools",
+ "promptEmpty": "Template cannot be empty",
+ "missingPlaceholder": "Missing {model} placeholder",
+ "missingPlaceholderDesc": "After saving, the provider's model name won't be replaced in the prompt. Continue?",
+ "restored": "restored"
+ },
+
+ "confirm": {
+ "restartTitle": "Restart Language Server",
+ "restartDesc": "In-progress requests will fail. Continue?",
+ "restoreDefaultDesc": "restore to default?",
+ "clearPoolDesc": "All active Cascade sessions will be cleared. Continue?"
+ },
+
+ "modal": {
+ "blockedModels": {
+ "title": "Edit Available Models — {{email}}",
+ "hint": "Checked = available; Uncheck = disabled for this account",
+ "enabled": "Enabled",
+ "saved": "Saved: enabled {{enabled}} / disabled {{disabled}}",
+ "saving": "Saving...",
+ "total": "Total"
+ },
+ "restartLS": {
+ "title": "Restart Language Server",
+ "desc": "In-progress requests will fail. Continue?"
+ },
+ "deleteAccount": {
+ "title": "Delete Account",
+ "desc": "This action cannot be undone. Delete this account?"
+ },
+ "resetStats": {
+ "title": "Reset Statistics",
+ "desc": "Clear all statistics data?"
+ },
+ "overrideTier": {
+ "title": "Manually Set Account Tier",
+ "desc": "Current: {{current}}. Windsurf Pro trial sometimes fails auto-detection (issue #8 side effect), manually change to pro to unlock all Pro models.",
+ "options": {
+ "pro": "Pro (unlock all models)",
+ "free": "Free (gpt-4o-mini and gemini-2.5-flash only)",
+ "unknown": "Unknown (let system re-evaluate)"
+ }
+ },
+ "applyUpdate": {
+ "title": "Update & Restart",
+ "desc": "Will execute git pull and restart PM2. Dashboard will be briefly unavailable during restart (about 5-10 seconds). Continue?"
+ },
+ "dirtyWorktree": {
+ "title": "Local Changes in Working Directory",
+ "desc": "The following files have been modified but not committed:
{{files}}
Continuing will overwrite local changes with remote version. For non-git deployments (SFTP / zip), click force overwrite.",
+ "forceUpdate": "Force Overwrite & Update"
+ },
+ "configureProxy": {
+ "title": "Configure Account Proxy",
+ "desc": "Set dedicated proxy for account {{label}}"
+ }
+ },
+
+ "oauth": {
+ "tryAnotherWay": "Try Another Way",
+ "google": "Google Login",
+ "github": "GitHub Login",
+ "copyAuthToken": "Copy Auth Token",
+ "pasteToken": "Paste Token in Account Management",
+ "status": {
+ "loading": "Logging in via {{provider}}...",
+ "success": "Login successful! {{email}} added to pool",
+ "codeium": "{{provider}} auth successful, registering with Codeium...",
+ "firebaseError": "Firebase SDK failed to load, please refresh and retry",
+ "loggingIn": "Logging in via {{label}}...",
+ "codeiumRegistering": "{{label}} auth success, registering Codeium...",
+ "loginSuccess": "Login successful {{email}} added to pool",
+ "loginFailed": "{{label}} login failed: {{error}}"
+ }
+ },
+
+ "update": {
+ "checking": "Checking for updates...",
+ "newVersion": "New version found",
+ "current": "Current",
+ "remote": "Remote",
+ "latest": "Already up to date",
+ "applying": "Pulling + restarting...",
+ "complete": "Update complete",
+ "restarting": "Service restarting in background, page will auto-refresh in ~8 seconds...",
+ "cancelled": "Cancelled",
+ "notGit": "Not a git deployment — one-click update unavailable",
+ "notGitDesc": "If you deployed via git clone, ensure .git directory is with WindsurfAPI source. For SFTP / zip uploads, manually replace files and pm2 restart.",
+ "upToDate": "Already up to date, no update needed"
+ },
+
+ "proxy": {
+ "test": {
+ "button": "Test Proxy",
+ "result": "Result"
+ },
+ "none": "None (use global)",
+ "direct": "Direct",
+ "global": "Global",
+ "current": "Current",
+ "notConfigured": "No global proxy configured",
+ "placeholderSaved": "Saved (leave empty to keep)",
+ "placeholderPassword": "Proxy auth password",
+ "auth": "Auth"
+ },
+
+ "account": {
+ "addSuccess": "Account added",
+ "deleteSuccess": "Account deleted",
+ "enableSuccess": "Account enabled",
+ "disableSuccess": "Account disabled",
+ "resetErrors": "Errors reset",
+ "tierSet": "Tier set to {{tier}}",
+ "editModels": "Edit available models",
+ "probe": {
+ "doing": "Probing, takes about 10-30 seconds...",
+ "complete": "Probe complete: {{result}}",
+ "allDoing": "Probing all accounts, takes 1-3 minutes depending on count...",
+ "allComplete": "Probe complete: {{summary}}",
+ "probeAllComplete": "Probe complete: {{summary}}",
+ "probeComplete": "Probe complete: {{tier}}"
+ },
+ "credits": {
+ "refreshTitle": "Refresh credits",
+ "refreshed": "Credits refreshed",
+ "refreshFailed": "Refresh failed",
+ "allRefreshed": "All account credits refreshed: {{ok}} success / {{fail}} failed",
+ "refreshing": "Refreshing all account credits...",
+ "refreshingAll": "Refreshing all account credits..."
+ },
+ "ls": {
+ "restarting": "Language Server restarting..."
+ }
+ },
+
+ "batch": {
+ "pasteFirst": "Please paste batch accounts first",
+ "importing": "Importing...",
+ "importingDesc": "{{count}} lines, please wait",
+ "complete": "Batch import complete",
+ "importFailed": "Batch import failed",
+ "importComplete": "Batch import complete: success {{success}} / failed {{fail}}",
+ "success": "Success",
+ "fail": "Fail",
+ "formatError": "Line {{line}} format error"
+ },
+
+ "toast": {
+ "copied": "Copied",
+ "saved": "Saved",
+ "cleared": "Cleared",
+ "refresh": "Refresh",
+ "error": "Error",
+ "loading": "Loading...",
+ "enterHostPort": "Please enter host and port first",
+ "experimentalEnabled": "Experimental feature enabled",
+ "experimentalDisabled": "Experimental feature disabled",
+ "toggleFailed": "Toggle failed: {{error}}",
+ "modeUpdated": "Mode updated",
+ "noTierModels": "No models available for this account tier",
+ "loginSuccess": "{{label}} login successful",
+ "loginFailed": "{{label}} login failed: {{error}}",
+ "loginSuccessAdded": "Login successful, account added to pool",
+ "clipboardRead": "Clipboard read",
+ "clipboardEmpty": "Clipboard is empty",
+ "clipboardUnsupported": "Current environment doesn't support auto clipboard read, please paste manually",
+ "clipboardError": "Failed to read clipboard: {{error}}",
+ "apiKeyCopied": "API Key copied",
+ "copyFailed": "Copy failed, please select manually",
+ "copyFailedError": "Copy failed: {{error}}",
+ "enterEmailPassword": "Please enter email and password",
+ "restarting": "Language Server restarting...",
+ "proxyConfigured": "Account proxy configured",
+ "proxyCleared": "Account proxy cleared",
+ "globalProxySaved": "Global proxy saved",
+ "globalProxyCleared": "Global proxy cleared",
+ "enterProxyHost": "Please enter proxy host",
+ "statsReset": "Statistics reset",
+ "enterKeyOrToken": "Please enter Key or Token",
+ "addFailed": "Add failed",
+ "probe": {
+ "doing": "Probing, takes about 10-30 seconds...",
+ "probeComplete": "Probe complete: {{tier}}",
+ "allDoing": "Probing all accounts, takes 1-3 minutes depending on count...",
+ "probeAllComplete": "Probe complete: {{summary}}"
+ },
+ "probeComplete": "Probe complete: {{tier}}",
+ "probeAllComplete": "Probe complete: {{summary}}",
+ "credits": {
+ "refreshed": "Credits refreshed",
+ "refreshFailed": "Failed to refresh credits",
+ "refreshing": "Refreshing credits...",
+ "allRefreshed": "All account credits refreshed: {{ok}} success / {{fail}} failed"
+ }
+ },
+
+ "log": {
+ "level": {
+ "all": "All Levels",
+ "debug": "Debug",
+ "info": "Info",
+ "warn": "Warn",
+ "error": "Error"
+ },
+ "searchPlaceholder": "Search log content...",
+ "clearView": "Clear View",
+ "viaSSE": "via SSE real-time streaming"
+ },
+
+ "model": {
+ "mode": {
+ "all": "Allow All",
+ "allowlist": "Allowlist",
+ "blocklist": "Blocklist"
+ },
+ "provider": {
+ "all": "All Providers",
+ "anthropic": "Anthropic",
+ "openai": "OpenAI",
+ "google": "Google",
+ "deepseek": "DeepSeek",
+ "xai": "xAI",
+ "alibaba": "Alibaba",
+ "moonshot": "Moonshot",
+ "windsurf": "Windsurf"
+ },
+ "list": {
+ "current": "Current List ({{count}})",
+ "empty": "List is empty"
+ }
+ },
+
+ "error": {
+ "generic": "An error occurred",
+ "unknown": "Unknown error",
+ "updateFailed": "Update failed",
+ "apiError": "API error",
+ "loadFailed": "Failed to load",
+ "probeFailed": "Probe failed",
+ "accountNotFound": "Account not found",
+ "openFailed": "Failed to open",
+ "saveFailed": "Save failed",
+ "deleteFailed": "Delete failed",
+ "networkError": "Network error",
+ "authRequired": "Please login first",
+ "clipboardNotSupported": "Current environment doesn't support clipboard auto-read, please paste manually",
+ "clipboardEmpty": "Clipboard is empty",
+ "clipboardReadFailed": "Failed to read clipboard",
+ "firebaseLoadFailed": "Firebase SDK failed to load, please refresh and retry",
+ "ERR_EMAIL_PASSWORD_REQUIRED": "Email and password are required",
+ "ERR_HOST_PORT_REQUIRED": "Host and port are required",
+ "ERR_UNCOMMITTED_CHANGES": "Working directory has uncommitted changes. Force overwrite?",
+ "ERR_ACCOUNTS_REQUIRED": "Accounts array is required",
+ "ERR_TEXT_REQUIRED": "Text field is required",
+ "ERR_NO_VALID_LINES": "No valid lines found",
+ "ERR_FORMAT_INVALID": "Invalid format. Expected: [proxy] email password",
+ "ERR_IDTOKEN_REQUIRED": "ID token is required",
+ "ERR_PROXY_PRIVATE_IP": "Proxy cannot point to private/local address",
+ "ERR_PROXY_HTTP_ERROR": "Proxy returned HTTP error",
+ "ERR_CONNECTION_FAILED": "Connection failed",
+ "ERR_TIMEOUT": "Timeout (10s)",
+ "ERR_TLS_TUNNEL_ERROR": "TLS tunnel established but returned abnormal content",
+ "ERR_TLS_FAILED": "TLS failed",
+ "ERR_LOGIN_FAILED": "Login failed",
+ "ERR_EMAIL_NOT_FOUND": "Email not registered for password login. If registered via Google/GitHub OAuth, use OAuth buttons or visit windsurf.com/show-auth-token to copy Auth Token",
+ "ERR_INVALID_PASSWORD": "Invalid password",
+ "ERR_INVALID_CREDENTIALS": "Invalid email or password",
+ "ERR_NO_PASSWORD_SET": "No password set for this account. Use Google/GitHub OAuth or visit windsurf.com/show-auth-token to copy Auth Token",
+ "ERR_USER_DISABLED": "Account has been disabled",
+ "ERR_TOO_MANY_ATTEMPTS": "Too many attempts. Please try again later",
+ "ERR_INVALID_EMAIL": "Invalid email format",
+ "ERR_CODEIUM_REGISTER_FAILED": "Codeium registration failed",
+ "ERR_AUTH1_TOKEN_MISSING": "Auth1 response missing token",
+ "ERR_POSTAUTH_FAILED": "Windsurf PostAuth failed",
+ "ERR_TOKEN_FETCH_FAILED": "Failed to fetch one-time auth token",
+ "ERR_FIREBASE_TOKEN_MISSING": "Firebase response missing idToken"
+ },
+
+ "footer": {
+ "version": "Version",
+ "langToggle": "Switch language"
+ },
+
+ "placeholder": {
+ "noProxy": "No proxy"
+ },
+
+ "loginResult": {
+ "success": "✓ Login Successful",
+ "apiKeyGenerated": "API Key generated{{autoAdd}}",
+ "fail": "✗ Login Failed",
+ "batchComplete": "Batch Import Complete",
+ "batchDesc": "Total {{total}} accounts, {{success}} success, {{fail}} fail{{autoAdd}}",
+ "tryOther": "Try Another Way",
+ "oauthHint": "If Google/GitHub OAuth was used to register, password login won't work. Use the Google/GitHub buttons above, or visit {{link}} to copy Auth Token then manually add in Account Pool page.",
+ "fields": {
+ "email": "Email",
+ "name": "Name",
+ "apiKey": "API Key",
+ "accountId": "Account ID"
+ },
+ "successTitle": "Login Success",
+ "addedToPool": "and added to account pool"
+ },
+
+ "timeRange": {
+ "minutesAgo": "{{count}}m ago",
+ "hoursAgo": "{{count}}h ago",
+ "daysAgo": "{{count}}d ago"
+ },
+
+ "time": {
+ "day": "d",
+ "hour": "h",
+ "minute": "m",
+ "second": "s"
+ },
+
+ "prompt": {
+ "editor": {
+ "reset": "Reset",
+ "search": "Search providers...",
+ "templateHint": "{{model}} placeholder will be replaced with the requested model name."
+ }
+ },
+
+ "ls": {
+ "defaultInstance": "Default Instance",
+ "noProxy": "No proxy",
+ "port": "Port",
+ "pid": "PID",
+ "restartCount": "Restarts"
+ }
+}
diff --git a/src/dashboard/i18n/zh-CN.json b/src/dashboard/i18n/zh-CN.json
new file mode 100644
index 00000000..1c169524
--- /dev/null
+++ b/src/dashboard/i18n/zh-CN.json
@@ -0,0 +1,841 @@
+{
+ "_meta": {
+ "name": "简体中文",
+ "code": "zh-CN"
+ },
+
+ "brand": {
+ "sub": "管理控制台"
+ },
+
+ "nav": {
+ "group": {
+ "overview": "概览",
+ "account": "账号",
+ "system": "系统",
+ "about": "关于"
+ },
+ "overview": "仪表盘",
+ "stats": "统计分析",
+ "windsurf-login": "登录取号",
+ "accounts": "账号管理",
+ "bans": "异常监测",
+ "models": "模型控制",
+ "proxy": "代理配置",
+ "logs": "运行日志",
+ "experimental": "实验性功能",
+ "credits": "致谢"
+ },
+
+ "pageTitle": {
+ "main": "WindsurfAPI bydwgx1337 控制台"
+ },
+
+ "page": {
+ "overview": "仪表盘",
+ "stats": "统计分析",
+ "windsurf-login": "登录取号",
+ "accounts": "账号管理",
+ "bans": "异常监测",
+ "models": "模型控制",
+ "proxy": "代理配置",
+ "logs": "运行日志",
+ "experimental": "实验性功能",
+ "credits": "致谢"
+ },
+
+ "pageSubtitle": {
+ "overview": "系统运行状态与关键指标概览",
+ "windsurf-login": "支持 Google / GitHub / 邮箱密码 三种方式登录 Windsurf 获取 API Key",
+ "accounts": "维护账号池,探测各账号订阅层级与可用模型",
+ "models": "配置模型访问策略,限制可用模型范围",
+ "proxy": "全局或按账号配置出口代理,独立代理将启动独立的语言服务器实例",
+ "logs": "通过 SSE 实时流式接收服务端日志",
+ "stats": "请求量、成功率、延迟分位数与账号/模型维度统计",
+ "bans": "追踪错误账号和异常状态",
+ "experimental": "尚未稳定的优化项;如果发现异常可以随时关闭",
+ "credits": "感谢以下朋友为项目提交 PR / 审计代码 / 修复问题"
+ },
+
+ "button": {
+ "cancel": "取消",
+ "confirm": "确认",
+ "forceOverride": "强制覆盖并更新",
+ "save": "保存",
+ "restart": "重启",
+ "updateAndRestart": "更新并重启"
+ },
+
+ "action": {
+ "checkUpdate": "检查更新",
+ "applyUpdate": "一键更新并重启",
+ "restart": "重启",
+ "login": "登录",
+ "save": "保存",
+ "clear": "清除",
+ "cancel": "取消",
+ "confirm": "确认",
+ "delete": "删除",
+ "reset": "重置",
+ "clearPool": "清空对话池",
+ "refresh": "刷新",
+ "probe": "探测",
+ "probeTitle": "探测能力",
+ "enable": "启用",
+ "disable": "停用",
+ "copy": "复制",
+ "copyTitle": "点击复制",
+ "configure": "配置",
+ "test": "测试",
+ "readClipboard": "读取剪贴板",
+ "batchImport": "批量导入并登录",
+ "paste": "粘贴",
+ "add": "添加",
+ "close": "关闭",
+ "edit": "编辑",
+ "selectAll": "全选",
+ "selectNone": "全不选",
+ "invert": "反选",
+ "showPassword": "显示/隐藏密码"
+ },
+
+ "status": {
+ "active": "正常",
+ "disabled": "已禁用",
+ "error": "错误",
+ "running": "运行中",
+ "stopped": "已停止",
+ "success": "成功",
+ "failed": "失败",
+ "pending": "待处理",
+ "unknown": "未知",
+ "rateLimited": "限速中",
+ "loading": "加载中…",
+ "testing": "测试中…",
+ "saving": "保存中…",
+ "updateAndRestartHint": "更新并重启服务",
+ "proxySuccess": "✓ 连通成功 出口 IP {{ip}} · 耗时 {{latency}}ms",
+ "proxyFailed": "✗ 失败: {{error}} · {{latency}}ms",
+ "checkingUpdate": "正在检查更新…",
+ "updateAvailable": "发现新版本",
+ "currentVersion": "当前版本",
+ "remoteVersion": "远程版本",
+ "upToDate": "✓ 已是最新 ({{commit}}) · {{message}}",
+ "pullingRestarting": "正在拉取 + 重启…",
+ "cancelled": "已取消",
+ "noUpdateNeeded": "已是最新,无需更新",
+ "updateComplete": "更新完成 {{before}} → {{after}}",
+ "restarting": "服务正在后台重启 约 8 秒后自动刷新页面…",
+ "notGitRepo": "非 git 部署 — 一键更新不可用",
+ "notGitRepoHint": "如果你是通过 git clone 部署的,请确保 .git 目录随 WindsurfAPI 源码一起上传",
+ "workingDirDirty": "工作区有本地修改",
+ "workingDirDirtyHint": "以下文件已被修改但未提交:
{{files}}
继续将用远程版本覆盖这些修改。",
+ "notFetched": "尚未获取",
+ "fetchFailed": "获取失败",
+ "clickToEditTier": "点击手动修改层级(探测异常时用)"
+ },
+
+ "tier": {
+ "pro": "PRO",
+ "free": "FREE",
+ "expired": "已过期",
+ "unknown": "未知",
+ "expiredLabel": "已过期"
+ },
+
+ "creditsBar": {
+ "daily": "日",
+ "weekly": "周",
+ "monthly": "月"
+ },
+
+ "section": {
+ "banStatus": {
+ "title": "健康状态"
+ },
+ "lsStatus": {
+ "title": "Language Server",
+ "desc": "语言服务器实例状态"
+ },
+ "oauthLogin": {
+ "title": "快捷登录(推荐)",
+ "desc": "用你的 Google 或 GitHub 账号直接登录 Windsurf 无需密码"
+ },
+ "emailLogin": {
+ "title": "邮箱密码登录",
+ "desc": "仅限邮箱+密码注册的账号 第三方登录请用上面的按钮;支持单个登录或按「邮箱 密码」格式批量导入"
+ },
+ "loginProxy": {
+ "title": "登录代理(可选)",
+ "desc": "为本次登录指定代理;留空则使用全局代理设置。代理生效后账号的后续聊天请求也会经此代理出站"
+ },
+ "loginHistory": {
+ "title": "登录历史",
+ "desc": "本地记录最近 50 条登录操作"
+ },
+ "addAccount": {
+ "title": "添加账号",
+ "desc": "支持 API Key 或 Auth Token 两种方式",
+ "hint": "推荐用 Token 方式添加账号 点 {{link}} 登录后复制 Token 粘贴到下面就行 所有登录方式(邮箱/Google/GitHub)都能用"
+ },
+ "accountList": {
+ "title": "账号列表",
+ "desc": "点击探测按钮可单独更新某个账号的能力信息"
+ },
+ "modelPolicy": {
+ "title": "访问策略",
+ "desc": "选择「全部允许」不限制;「白名单」只允许列出的模型;「黑名单」屏蔽列出的模型"
+ },
+ "modelList": {
+ "title": "模型清单",
+ "titleAllow": "允许的模型",
+ "titleBlock": "屏蔽的模型",
+ "descAllow": "只有选中的模型可以使用,未选中的模型将返回 403 错误",
+ "descBlock": "选中的模型将被屏蔽,未选中的模型可正常使用",
+ "search": "搜索模型...",
+ "provider": "供应商"
+ },
+ "globalProxy": {
+ "title": "全局代理",
+ "desc": "未配置独立代理的账号将使用此配置"
+ },
+ "accountProxy": {
+ "title": "账号独立代理",
+ "desc": "为特定账号设置专属代理;每个独立代理会启动独立 LS 实例"
+ },
+ "cascadeReuse": {
+ "title": "Cascade 对话复用",
+ "desc": "多轮对话时复用同一个 cascade_id,只把最新一条 user 消息发给 Windsurf,让服务端维持上下文缓存。命中时可显著降低 TTFB 与上传体积;未命中或账号/LS 变更会自动回退到新会话。需要客户端保留完整历史并按顺序追加(如 new-api、OpenWebUI)。",
+ "enable": "启用 Cascade 对话复用",
+ "hint": "默认关闭。开启后对当前对话池立即生效。"
+ },
+ "identityPrompt": {
+ "title": "模型身份提示词",
+ "desc": "在每个请求开头注入模型身份系统提示词。",
+ "enable": "启用模型身份注入",
+ "hint": "默认开启;关闭后下面的模板不起作用",
+ "templateHint": "每个厂商的模板可编辑,{{model}} 占位符会被替换成请求的模型名(例如 claude-opus-4.6)。"
+ },
+ "identityInjection": {
+ "title": "模型身份注入",
+ "desc": "开启后在每个请求最前面注入一条系统提示词,覆盖 Cascade 自带的 Windsurf 身份;下面每个厂商的模板可自定义,保存后立即生效"
+ },
+ "systemPrompts": {
+ "title": "系统提示词",
+ "desc": "工具注入、对话模式等内置提示词模板。修改后立即生效,点「重置」恢复默认。"
+ },
+ "lsPool": {
+ "title": "语言服务器池",
+ "desc": "{{count}} 个 LS 实例 · 每个独立代理对应一个进程",
+ "sectionTitle": "语言服务器池",
+ "sectionDesc": "{{count}} 个 LS 实例 · 每个独立代理对应一个进程",
+ "port": "端口",
+ "defaultInstance": "默认实例",
+ "noProxy": "无代理"
+ },
+ "modelStats": {
+ "title": "模型使用统计",
+ "desc": "按模型聚合:请求量、成功率、平均耗时与 p50 / p95 延迟分位数"
+ },
+ "accountStats": {
+ "title": "账号维度统计",
+ "desc": "每个账号 ID 前 8 位的请求量与成功率"
+ },
+ "accountHealth": {
+ "title": "账号健康状况"
+ },
+ "requestChart": {
+ "title": "请求量时间序列"
+ }
+ },
+
+ "help": {
+ "token": "推荐用 Token 方式添加账号 点 {{link}} 登录后复制 Token 粘贴到下面就行 所有登录方式(邮箱/Google/GitHub)都能用",
+ "tokenGuide": "推荐用 Token 方式添加账号 点 ",
+ "tokenGuideEnd": " 登录后复制 Token 粘贴到下面就行 所有登录方式(邮箱/Google/GitHub)都能用"
+ },
+
+ "field": {
+ "email": {
+ "label": "邮箱",
+ "placeholder": "your-email@example.com"
+ },
+ "password": {
+ "label": "密码",
+ "placeholder": "••••••••"
+ },
+ "type": {
+ "label": "类型"
+ },
+ "tier": {
+ "label": "层级"
+ },
+ "host": {
+ "label": "主机",
+ "placeholder": "proxy.example.com"
+ },
+ "port": {
+ "label": "端口",
+ "placeholder": "8080"
+ },
+ "username": {
+ "label": "用户名",
+ "placeholder": "可选"
+ },
+ "passwordProxy": {
+ "label": "密码",
+ "placeholder": "可选"
+ },
+ "key": {
+ "label": "Key / Token",
+ "placeholder": "粘贴 Auth Token(从 windsurf.com/show-auth-token 获取)"
+ },
+ "label": {
+ "label": "标签",
+ "placeholder": "可选"
+ },
+ "batchInput": {
+ "label": "批量导入",
+ "hint": "每行一个账号。格式:[代理] 邮箱 密码。代理可选,支持 http/socks5。会自动绑定代理到对应账号。"
+ },
+ "proxyType": {
+ "label": "类型"
+ },
+ "proxyHost": {
+ "label": "主机",
+ "placeholder": "留空=使用全局"
+ },
+ "provider": {
+ "label": "供应商",
+ "all": "全部供应商"
+ },
+ "proxyUser": {
+ "placeholder": "代理认证用户名"
+ },
+ "proxyPass": {
+ "placeholder": "代理认证密码"
+ },
+ "searchProvider": {
+ "placeholder": "搜索提供商..."
+ },
+ "search": {
+ "label": "搜索",
+ "placeholder": "搜索..."
+ },
+ "searchModel": {
+ "label": "搜索模型",
+ "placeholder": "输入模型名称筛选..."
+ },
+ "providerFilter": {
+ "label": "供应商"
+ },
+ "logLevel": {
+ "label": "日志级别"
+ },
+ "identitySearch": {
+ "placeholder": "搜索提供商..."
+ },
+ "autoScroll": "自动滚动",
+ "autoAdd": "登录成功自动加入账号池"
+ },
+
+ "table": {
+ "header": {
+ "time": "时间",
+ "email": "邮箱",
+ "status": "状态",
+ "proxy": "代理",
+ "action": "操作",
+ "id": "ID",
+ "label": "标签",
+ "tier": "层级",
+ "rpm": "RPM",
+ "credits": "余额",
+ "models": "可用模型",
+ "error": "错误",
+ "lastUsed": "最后使用",
+ "key": "Key",
+ "accountId": "账号 ID",
+ "account": "账号",
+ "requests": "请求",
+ "success": "成功",
+ "errors": "错误",
+ "successRate": "成功率",
+ "avg": "平均",
+ "p50": "p50",
+ "p95": "p95",
+ "errorCount": "错误数",
+ "model": "模型",
+ "note": "说明"
+ },
+ "empty": {
+ "loginHistory": "暂无登录记录",
+ "accounts": "暂无账号,请先添加",
+ "accountNotFound": "账号不存在",
+ "probeFailed": "探测失败",
+ "noRequestData": "暂无请求数据",
+ "noAccountRequestData": "暂无账号级请求数据",
+ "noData": "暂无数据",
+ "noValidAccounts": "暂无可用账号可用于路由",
+ "accountsProxy": "暂无账号",
+ "modelStats": "暂无模型统计数据",
+ "accountStats": "暂无账号统计数据",
+ "banList": "暂无异常账号",
+ "models": "没有符合的模型",
+ "credits": "加载中…"
+ }
+ },
+
+ "card": {
+ "activeAccounts": {
+ "title": "活跃账号",
+ "subtitle": "{{total}} 个总账号 · {{error}} 异常"
+ },
+ "totalRequests": {
+ "title": "总请求数",
+ "subtitle": "成功率 {{rate}}%"
+ },
+ "uptime": {
+ "title": "运行时间",
+ "subtitle": "启动于 {{time}}"
+ },
+ "languageServer": {
+ "title": "Language Server",
+ "running": "运行中",
+ "stopped": "已停止",
+ "subtitle": "{{count}} 个实例 · 端口 {{port}}"
+ },
+ "responseCache": {
+ "title": "响应缓存",
+ "subtitle": "{{hits}} 命中 / {{misses}} 未命中 · {{size}}/{{max}} 条"
+ },
+ "banStats": {
+ "title": "健康概览"
+ },
+ "abnormalAccounts": {
+ "title": "异常账号",
+ "subtitle": "共 {{count}} 个账号"
+ },
+ "disabled": {
+ "title": "已停用",
+ "subtitle": "自动错误停用"
+ },
+ "rateLimit": {
+ "title": "限流中",
+ "badge": "限流中",
+ "subtitle": "暂时不参与调度"
+ },
+ "stats": {
+ "title": "总请求",
+ "subtitle": "运行 {{time}}"
+ },
+ "success": {
+ "title": "成功",
+ "subtitle": "成功率 {{rate}}%"
+ },
+ "errors": {
+ "title": "错误",
+ "subtitle": "失败率 {{rate}}%"
+ },
+ "p95Latency": {
+ "title": "平均 p95 延迟",
+ "subtitle": "{{count}} 个模型"
+ },
+ "hitRate": {
+ "title": "命中率",
+ "subtitle": "{{hits}} 命中 / {{misses}} 未命中"
+ },
+ "poolSize": {
+ "title": "对话池",
+ "subtitle": "上限 {{max}} · TTL {{ttl}} 分钟"
+ },
+ "stores": {
+ "title": "存储次数",
+ "subtitle": "过期 {{expired}} · 淘汰 {{evicted}}"
+ },
+ "defaultInstance": "默认实例",
+ "noProxy": "无代理",
+ "restarts": "重启",
+ "custom": "已自定义"
+ },
+
+ "login": {
+ "title": "控制台登录",
+ "subtitle": "请输入管理密码以继续",
+ "passwordPlaceholder": "Dashboard 密码",
+ "button": "登 录"
+ },
+
+ "range": {
+ "6h": "近 6小时",
+ "24h": "近 24小时",
+ "72h": "近 72小时"
+ },
+
+ "credits": {
+ "contributors": "核心贡献者",
+ "cta": {
+ "title": "想加入这份名单?",
+ "desc": "欢迎到 GitHub 提 issue 或 pull request — 不管是修 bug、加模型、改 UI 还是发现安全问题,都会被记录在这里。",
+ "issue": "提 issue",
+ "pr": "提 PR"
+ }
+ },
+
+ "experimental": {
+ "enable": "启用",
+ "disable": "禁用",
+ "cascadeReuse": {
+ "title": "Cascade 对话复用",
+ "desc": "多轮对话时复用同一个 cascade_id,只把最新一条 user 消息发给 Windsurf,让服务端维持上下文缓存。命中时可显著降低 TTFB 与上传体积;未命中或账号/LS 变更会自动回退到新会话。需要客户端保留完整历史并按顺序追加(如 new-api、OpenWebUI)。",
+ "enable": "启用 Cascade 对话复用",
+ "hint": "默认关闭。开启后对当前对话池立即生效。"
+ },
+ "identityPrompt": {
+ "title": "启用模型身份注入",
+ "desc": "默认开启;关闭后下面的模板不起作用。"
+ },
+ "templateHint": "每个厂商的模板可编辑,{{model}} 占位符会被替换成请求的模型名(例如 claude-opus-4.6)。",
+ "clearPool": "清空对话池",
+ "refresh": "刷新",
+ "noMatch": "无匹配",
+ "restoreDefault": "恢复默认",
+ "save": "保存",
+ "default": "默认",
+ "toolReinforcement": "工具调用格式指引",
+ "communicationWithTools": "有工具时的通信指令",
+ "communicationNoTools": "无工具时的通信指令",
+ "promptEmpty": "模板不能为空",
+ "missingPlaceholder": "缺少 {model} 占位符",
+ "missingPlaceholderDesc": "保存后该厂商的模型名将无法被替换进提示词。确定继续?",
+ "restored": "已恢复"
+ },
+
+ "confirm": {
+ "restartTitle": "重启 Language Server",
+ "restartDesc": "进行中的请求将会失败,确定要继续吗?",
+ "restoreDefaultDesc": "恢复到默认?",
+ "clearPoolDesc": "当前所有进行中的 Cascade 会话都将被清除,确定继续吗?"
+ },
+
+ "modal": {
+ "blockedModels": {
+ "title": "编辑可用模型 — {{email}}",
+ "hint": "勾选 = 可用;取消 = 对该账号禁用",
+ "enabled": "已启用",
+ "saved": "已保存:启用 {{enabled}} / 禁用 {{disabled}}",
+ "saving": "保存中...",
+ "total": "总计"
+ },
+ "restartLS": {
+ "title": "重启 Language Server",
+ "desc": "进行中的请求将会失败,确定要继续吗?"
+ },
+ "deleteAccount": {
+ "title": "删除账号",
+ "desc": "此操作不可撤销,确定要删除该账号吗?"
+ },
+ "resetStats": {
+ "title": "重置统计",
+ "desc": "清空所有统计数据?"
+ },
+ "overrideTier": {
+ "title": "手动设置账号层级",
+ "desc": "当前:{{current}}。Windsurf Pro 试用有时自动探测失败(issue #8 的连锁影响),手动改成 pro 即可解锁所有 Pro 模型。",
+ "options": {
+ "pro": "Pro(解锁所有模型)",
+ "free": "Free(仅 gpt-4o-mini 和 gemini-2.5-flash)",
+ "unknown": "Unknown(让系统重新判定)"
+ }
+ },
+ "applyUpdate": {
+ "title": "一键更新并重启",
+ "desc": "将执行 git pull 并重启 PM2。重启期间 dashboard 会短暂不可用(约 5-10 秒)。确定继续?"
+ },
+ "dirtyWorktree": {
+ "title": "工作区有本地修改",
+ "desc": "以下文件已被修改但未提交:
{{files}}
继续将用远程版本覆盖这些修改。非 git 部署(SFTP / 压缩包)请点强制覆盖。",
+ "forceUpdate": "强制覆盖并更新"
+ },
+ "configureProxy": {
+ "title": "配置账号代理",
+ "desc": "为账号 {{label}} 设置独立代理"
+ }
+ },
+
+ "oauth": {
+ "tryAnotherWay": "换个方式试试",
+ "google": "Google 登录",
+ "github": "GitHub 登录",
+ "copyAuthToken": "复制 Auth Token",
+ "pasteToken": "去账号管理粘贴 Token",
+ "status": {
+ "loading": "正在通过 {{provider}} 登录...",
+ "success": "登录成功 {{email}} 已加入账号池",
+ "codeium": "{{provider}} 认证成功 正在注册 Codeium...",
+ "firebaseError": "Firebase SDK 加载失败 请刷新页面重试",
+ "loggingIn": "正在通过 {{label}} 登录...",
+ "codeiumRegistering": "{{label}} 认证成功 正在注册 Codeium...",
+ "loginSuccess": "登录成功 {{email}} 已加入账号池",
+ "loginFailed": "{{label}} 登录失败: {{error}}"
+ }
+ },
+
+ "update": {
+ "checking": "正在检查更新...",
+ "newVersion": "发现新版本",
+ "current": "当前",
+ "remote": "远程",
+ "latest": "已是最新",
+ "applying": "正在拉取 + 重启...",
+ "complete": "更新完成",
+ "restarting": "服务正在后台重启 约 8 秒后自动刷新页面...",
+ "cancelled": "已取消",
+ "notGit": "此实例不是 git 部署 一键更新不可用",
+ "notGitDesc": "如果你用 git clone 部署的 需要确保 .git 目录和 WindsurfAPI 源代码目录在一起。SFTP / 压缩包上传的部署请手动替换文件后 pm2 重启",
+ "upToDate": "已是最新,无需更新"
+ },
+
+ "proxy": {
+ "test": {
+ "button": "测试代理",
+ "result": "结果"
+ },
+ "none": "无(使用全局)",
+ "direct": "直连",
+ "global": "全局",
+ "current": "当前",
+ "notConfigured": "未配置全局代理",
+ "placeholderSaved": "已保存(留空保持原值)",
+ "placeholderPassword": "代理认证密码",
+ "auth": "认证"
+ },
+
+ "account": {
+ "addSuccess": "账号已添加",
+ "deleteSuccess": "账号已删除",
+ "enableSuccess": "账号已启用",
+ "disableSuccess": "账号已停用",
+ "resetErrors": "错误已重置",
+ "tierSet": "层级已设为 {{tier}}",
+ "editModels": "编辑可用模型",
+ "probe": {
+ "doing": "探测中,约需 10-30 秒...",
+ "complete": "探测完成:{{result}}",
+ "allDoing": "全部探测中,依账号数量约需 1-3 分钟...",
+ "allComplete": "探测完成:{{summary}}",
+ "probeAllComplete": "探测完成:{{summary}}",
+ "probeComplete": "探测完成:{{tier}}"
+ },
+ "credits": {
+ "refreshTitle": "刷新余额",
+ "refreshed": "余额已刷新",
+ "refreshFailed": "刷新失败",
+ "allRefreshed": "余额刷新完成:{{ok}} 成功 / {{fail}} 失败",
+ "refreshing": "正在刷新所有账号余额...",
+ "refreshingAll": "正在刷新所有账号余额..."
+ },
+ "ls": {
+ "restarting": "Language Server 重启中..."
+ }
+ },
+
+ "batch": {
+ "pasteFirst": "请先粘贴批量账号",
+ "importing": "导入中...",
+ "importingDesc": "{{count}} 行,请稍候",
+ "complete": "批量导入完成",
+ "importFailed": "批量导入失败",
+ "importComplete": "批量导入完成:成功 {{success}} / 失败 {{fail}}",
+ "success": "成功",
+ "fail": "失败",
+ "formatError": "第 {{line}} 行格式错误"
+ },
+
+ "toast": {
+ "copied": "已复制",
+ "saved": "已保存",
+ "cleared": "已清空",
+ "refresh": "已刷新",
+ "error": "错误",
+ "loading": "加载中...",
+ "enterHostPort": "请先填主机和端口",
+ "experimentalEnabled": "已启用实验性功能",
+ "experimentalDisabled": "已关闭实验性功能",
+ "toggleFailed": "切换失败:{{error}}",
+ "modeUpdated": "模式已更新",
+ "noTierModels": "该账号层级无可用模型",
+ "loginSuccess": "{{label}} 登录成功",
+ "loginFailed": "{{label}} 登录失败:{{error}}",
+ "loginSuccessAdded": "登录成功,账号已加入账号池",
+ "clipboardRead": "已读取剪贴板",
+ "clipboardEmpty": "剪贴板为空",
+ "clipboardUnsupported": "当前环境不支持自动读取剪贴板,请手动粘贴",
+ "clipboardError": "读取剪贴板失败:{{error}}",
+ "apiKeyCopied": "API Key 已复制",
+ "copyFailed": "复制失败,请手动选择",
+ "copyFailedError": "复制失败:{{error}}",
+ "enterEmailPassword": "请输入邮箱和密码",
+ "restarting": "Language Server 重启中...",
+ "proxyConfigured": "账号代理已配置",
+ "proxyCleared": "账号代理已清除",
+ "globalProxySaved": "全局代理已保存",
+ "globalProxyCleared": "全局代理已清除",
+ "enterProxyHost": "请输入代理主机",
+ "statsReset": "统计已重置",
+ "enterKeyOrToken": "请输入 Key 或 Token",
+ "addFailed": "添加失败",
+ "probe": {
+ "doing": "探测中,约需 10-30 秒...",
+ "probeComplete": "探测完成:{{tier}}",
+ "allDoing": "探测所有账号中,约需 1-3 分钟(视数量而定)...",
+ "probeAllComplete": "探测完成:{{summary}}"
+ },
+ "probeComplete": "探测完成:{{tier}}",
+ "probeAllComplete": "探测完成:{{summary}}",
+ "credits": {
+ "refreshed": "余额已刷新",
+ "refreshFailed": "刷新余额失败",
+ "refreshing": "正在刷新余额...",
+ "allRefreshed": "余额刷新完成:{{ok}} 成功 / {{fail}} 失败"
+ }
+ },
+
+ "log": {
+ "level": {
+ "all": "所有级别",
+ "debug": "Debug",
+ "info": "Info",
+ "warn": "Warn",
+ "error": "Error"
+ },
+ "searchPlaceholder": "搜索日志内容...",
+ "clearView": "清空视图",
+ "viaSSE": "通过 SSE 实时流式接收"
+ },
+
+ "model": {
+ "mode": {
+ "all": "全部允许",
+ "allowlist": "白名单",
+ "blocklist": "黑名单"
+ },
+ "provider": {
+ "all": "全部供应商",
+ "anthropic": "Anthropic",
+ "openai": "OpenAI",
+ "google": "Google",
+ "deepseek": "DeepSeek",
+ "xai": "xAI",
+ "alibaba": "Alibaba",
+ "moonshot": "Moonshot",
+ "windsurf": "Windsurf"
+ },
+ "list": {
+ "current": "当前清单 ({{count}})",
+ "empty": "清单为空"
+ }
+ },
+
+ "error": {
+ "generic": "发生错误",
+ "unknown": "未知错误",
+ "updateFailed": "更新失败",
+ "apiError": "API 错误",
+ "loadFailed": "加载失败",
+ "probeFailed": "探测失败",
+ "accountNotFound": "账号未找到",
+ "openFailed": "打开失败",
+ "saveFailed": "保存失败",
+ "deleteFailed": "删除失败",
+ "networkError": "网络错误",
+ "authRequired": "请先登录",
+ "clipboardNotSupported": "当前环境不支持自动读取剪贴板,请手动粘贴",
+ "clipboardEmpty": "剪贴板为空",
+ "clipboardReadFailed": "读取剪贴板失败",
+ "firebaseLoadFailed": "Firebase SDK 加载失败 请刷新页面重试",
+ "ERR_EMAIL_PASSWORD_REQUIRED": "email 和 password 为必填",
+ "ERR_HOST_PORT_REQUIRED": "缺少 host 或 port",
+ "ERR_UNCOMMITTED_CHANGES": "工作区有未提交的修改(SFTP 部署或手动改过代码)。确定要覆盖本地修改用远程最新版本吗?",
+ "ERR_ACCOUNTS_REQUIRED": "accounts 为必填数组",
+ "ERR_TEXT_REQUIRED": "缺少 text 字段",
+ "ERR_NO_VALID_LINES": "没有有效行",
+ "ERR_FORMAT_INVALID": "格式错误 需要 [proxy] email password",
+ "ERR_IDTOKEN_REQUIRED": "缺少 idToken",
+ "ERR_PROXY_PRIVATE_IP": "代理地址不能指向内网/本机",
+ "ERR_PROXY_HTTP_ERROR": "代理返回 HTTP 错误",
+ "ERR_CONNECTION_FAILED": "连接失败",
+ "ERR_TIMEOUT": "超时(10s)",
+ "ERR_TLS_TUNNEL_ERROR": "TLS 隧道建立但返回内容异常",
+ "ERR_TLS_FAILED": "TLS 失败",
+ "ERR_LOGIN_FAILED": "登录失败",
+ "ERR_EMAIL_NOT_FOUND": "该邮箱未注册邮箱密码登录方式。若通过 Google/GitHub OAuth 注册,请使用 OAuth 按钮或访问 windsurf.com/show-auth-token 复制 Auth Token",
+ "ERR_INVALID_PASSWORD": "密码错误",
+ "ERR_INVALID_CREDENTIALS": "邮箱或密码错误",
+ "ERR_NO_PASSWORD_SET": "该账号未设置密码登录方式。使用 Google/GitHub OAuth 或访问 windsurf.com/show-auth-token 复制 Auth Token",
+ "ERR_USER_DISABLED": "账号已被停用",
+ "ERR_TOO_MANY_ATTEMPTS": "尝试太多次 请稍后再试",
+ "ERR_INVALID_EMAIL": "邮箱格式错误",
+ "ERR_CODEIUM_REGISTER_FAILED": "Codeium 注册失败",
+ "ERR_AUTH1_TOKEN_MISSING": "Auth1 回应缺少 token",
+ "ERR_POSTAUTH_FAILED": "Windsurf PostAuth 失败",
+ "ERR_TOKEN_FETCH_FAILED": "获取一次性 Auth Token 失败",
+ "ERR_FIREBASE_TOKEN_MISSING": "Firebase 回应缺少 idToken"
+ },
+
+ "footer": {
+ "version": "版本",
+ "langToggle": "切换语言"
+ },
+
+ "placeholder": {
+ "noProxy": "无代理"
+ },
+
+ "loginResult": {
+ "success": "✓ 登录成功",
+ "apiKeyGenerated": "API Key 已生成{{autoAdd}}",
+ "fail": "✗ 登录失败",
+ "batchComplete": "批量导入完成",
+ "batchDesc": "共 {{total}} 个账号,成功 {{success}} 个,失败 {{fail}} 个{{autoAdd}}",
+ "tryOther": "换个方式试试",
+ "oauthHint": "若你用 Google/GitHub 注册的 Windsurf 账号 此处密码登录不适用 请用页面顶部的 Google / GitHub 登录按钮 或访问 {{link}} 复制 Auth Token 后在「账号管理」页手动添加",
+ "fields": {
+ "email": "邮箱",
+ "name": "姓名",
+ "apiKey": "API Key",
+ "accountId": "账号 ID"
+ },
+ "successTitle": "登录成功",
+ "addedToPool": "并加入账号池"
+ },
+
+ "timeRange": {
+ "minutesAgo": "{{count}}分钟前",
+ "hoursAgo": "{{count}}小时前",
+ "daysAgo": "{{count}}天前"
+ },
+
+ "time": {
+ "day": "天",
+ "hour": "时",
+ "minute": "分",
+ "second": "秒"
+ },
+
+ "prompt": {
+ "editor": {
+ "reset": "重置",
+ "search": "搜索提供商...",
+ "templateHint": "{{model}} 占位符会被替换成请求的模型名。"
+ }
+ },
+
+ "ls": {
+ "defaultInstance": "默认实例",
+ "noProxy": "无代理",
+ "port": "端口",
+ "pid": "PID",
+ "restartCount": "重启次数"
+ }
+}
diff --git a/src/dashboard/index.html b/src/dashboard/index.html
index a4fe3fc0..5b339136 100644
--- a/src/dashboard/index.html
+++ b/src/dashboard/index.html
@@ -3,7 +3,7 @@
-WindsurfAPI bydwgx1337 控制台
+WindsurfAPI bydwgx1337 控制台