@lynn123411/dsh-chat-translate 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +35 -0
- package/cordis.patch.yml +6 -0
- package/dsh.plugin.json +13 -0
- package/lib/client.js +1565 -0
- package/lib/client.js.map +7 -0
- package/lib/index.js +1069 -0
- package/lib/index.js.map +7 -0
- package/lib/types/client/index.d.ts +21 -0
- package/lib/types/client/settings/store.d.ts +53 -0
- package/lib/types/client/settings/styles.d.ts +1 -0
- package/lib/types/client/settings/ui.d.ts +3 -0
- package/lib/types/client/translate/api.d.ts +24 -0
- package/lib/types/client/translate/client-cache.d.ts +14 -0
- package/lib/types/client/translate/lazy.d.ts +12 -0
- package/lib/types/client/translate/mount.d.ts +40 -0
- package/lib/types/client/translate/observer.d.ts +25 -0
- package/lib/types/client/translate/viewport-observer.d.ts +36 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/server/adapters/base.d.ts +1 -0
- package/lib/types/server/adapters/bing.d.ts +15 -0
- package/lib/types/server/adapters/openai.d.ts +10 -0
- package/lib/types/server/cache.d.ts +14 -0
- package/lib/types/server/config.d.ts +24 -0
- package/lib/types/server/credentials.d.ts +39 -0
- package/lib/types/server/dispatcher.d.ts +40 -0
- package/lib/types/server/pipeline/masking.d.ts +7 -0
- package/lib/types/server/router.d.ts +5 -0
- package/lib/types/server/types.d.ts +40 -0
- package/package.json +79 -0
package/lib/index.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/server/config.ts", "../src/server/credentials.ts", "../src/server/cache.ts", "../src/server/adapters/bing.ts", "../src/server/adapters/openai.ts", "../src/server/pipeline/masking.ts", "../src/server/dispatcher.ts", "../src/server/router.ts", "../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as os from 'node:os';\nimport type { PluginConfig, MaskedPluginConfig } from './types.ts';\nimport { CredentialsReader, TRANSLATE_API_KEY_REF } from './credentials.ts';\n\n/** Hard cap for the translation concurrency pool. */\nexport const MAX_CONCURRENCY = 100;\n\n/** Bounds for the AI channel request timeout. */\nexport const AI_TIMEOUT_MIN = 500;\nexport const AI_TIMEOUT_MAX = 120000;\n\nconst DEFAULT_CONFIG: PluginConfig = {\n enabled: true,\n concurrency: 3,\n timeoutMs: 2000,\n aiTimeoutMs: 30000,\n aiEnabled: true,\n bingEnabled: true,\n baseUrl: '',\n model: '',\n targetLang: 'zh-Hans',\n};\n\nexport class ConfigManager {\n private config: PluginConfig = { ...DEFAULT_CONFIG };\n private configPath: string;\n private credentials: CredentialsReader;\n private listeners = new Set<(config: PluginConfig) => void>();\n\n constructor(credentials?: CredentialsReader) {\n this.credentials = credentials ?? new CredentialsReader();\n const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');\n this.configPath = path.join(dshHome, 'dsh-chat-translate-config.json');\n }\n\n async init(): Promise<void> {\n try {\n const data = await fs.readFile(this.configPath, 'utf-8');\n const parsed = JSON.parse(data);\n this.config = {\n ...DEFAULT_CONFIG,\n ...parsed,\n };\n } catch {\n this.config = { ...DEFAULT_CONFIG };\n }\n\n // Sanitize values\n if (!Number.isFinite(this.config.concurrency) || this.config.concurrency < 1) {\n this.config.concurrency = DEFAULT_CONFIG.concurrency;\n } else {\n this.config.concurrency = Math.min(Math.max(Math.round(this.config.concurrency), 1), MAX_CONCURRENCY);\n }\n\n if (!Number.isFinite(this.config.timeoutMs) || this.config.timeoutMs < 500) {\n this.config.timeoutMs = DEFAULT_CONFIG.timeoutMs;\n } else {\n this.config.timeoutMs = Math.min(Math.max(Math.round(this.config.timeoutMs), 500), 10000);\n }\n\n if (!Number.isFinite(this.config.aiTimeoutMs) || this.config.aiTimeoutMs < AI_TIMEOUT_MIN) {\n this.config.aiTimeoutMs = DEFAULT_CONFIG.aiTimeoutMs;\n } else {\n this.config.aiTimeoutMs = Math.min(\n Math.max(Math.round(this.config.aiTimeoutMs), AI_TIMEOUT_MIN),\n AI_TIMEOUT_MAX\n );\n }\n\n if (typeof this.config.enabled !== 'boolean') this.config.enabled = DEFAULT_CONFIG.enabled;\n if (typeof this.config.aiEnabled !== 'boolean') this.config.aiEnabled = DEFAULT_CONFIG.aiEnabled;\n if (typeof this.config.bingEnabled !== 'boolean') this.config.bingEnabled = DEFAULT_CONFIG.bingEnabled;\n if (typeof this.config.baseUrl !== 'string') this.config.baseUrl = DEFAULT_CONFIG.baseUrl;\n if (typeof this.config.model !== 'string') this.config.model = DEFAULT_CONFIG.model;\n if (!this.config.targetLang || typeof this.config.targetLang !== 'string') {\n this.config.targetLang = DEFAULT_CONFIG.targetLang;\n }\n\n // Drop the retired `channels` field carried over from pre-1.1 configs so\n // it is not re-persisted on the next save.\n delete (this.config as Partial<PluginConfig> & { channels?: unknown }).channels;\n }\n\n getConfig(): PluginConfig {\n return { ...this.config };\n }\n\n /** Whether the AI channel has every required piece: baseUrl, model and key. */\n isAiConfigured(): boolean {\n return Boolean(\n this.config.baseUrl.trim() &&\n this.config.model.trim() &&\n this.credentials.getApiKey()\n );\n }\n\n getMaskedConfig(): MaskedPluginConfig {\n return {\n enabled: this.config.enabled,\n concurrency: this.config.concurrency,\n timeoutMs: this.config.timeoutMs,\n aiTimeoutMs: this.config.aiTimeoutMs,\n aiEnabled: this.config.aiEnabled,\n bingEnabled: this.config.bingEnabled,\n baseUrl: this.config.baseUrl,\n model: this.config.model,\n targetLang: this.config.targetLang || 'zh-Hans',\n aiConfigured: this.isAiConfigured(),\n };\n }\n\n onConfigChange(listener: (config: PluginConfig) => void): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private notifyListeners(): void {\n const snapshot = this.getConfig();\n for (const listener of this.listeners) {\n try {\n listener(snapshot);\n } catch (err) {\n console.warn('[dsh-chat-translate] Config listener error:', err);\n }\n }\n }\n\n async updateConfig(partial: Partial<PluginConfig>): Promise<PluginConfig> {\n const next: PluginConfig = {\n ...this.config,\n ...partial,\n };\n // Never let retired/unknown keys (e.g. the pre-1.1 `channels`) be\n // re-persisted through an update path.\n delete (next as Partial<PluginConfig> & { channels?: unknown }).channels;\n\n // Bounds check with Number.isFinite\n if (typeof partial.concurrency === 'number' && Number.isFinite(partial.concurrency)) {\n next.concurrency = Math.min(Math.max(Math.round(partial.concurrency), 1), MAX_CONCURRENCY);\n } else {\n next.concurrency = this.config.concurrency;\n }\n\n if (typeof partial.timeoutMs === 'number' && Number.isFinite(partial.timeoutMs)) {\n next.timeoutMs = Math.min(Math.max(Math.round(partial.timeoutMs), 500), 10000);\n } else {\n next.timeoutMs = this.config.timeoutMs;\n }\n\n if (typeof partial.aiTimeoutMs === 'number' && Number.isFinite(partial.aiTimeoutMs)) {\n next.aiTimeoutMs = Math.min(\n Math.max(Math.round(partial.aiTimeoutMs), AI_TIMEOUT_MIN),\n AI_TIMEOUT_MAX\n );\n } else {\n next.aiTimeoutMs = this.config.aiTimeoutMs;\n }\n\n if (typeof partial.enabled === 'boolean') next.enabled = partial.enabled;\n if (typeof partial.aiEnabled === 'boolean') next.aiEnabled = partial.aiEnabled;\n if (typeof partial.bingEnabled === 'boolean') next.bingEnabled = partial.bingEnabled;\n if (typeof partial.baseUrl === 'string') next.baseUrl = partial.baseUrl.trim();\n if (typeof partial.model === 'string') next.model = partial.model.trim();\n if (typeof partial.targetLang === 'string' && partial.targetLang.trim()) {\n next.targetLang = partial.targetLang.trim();\n }\n\n this.config = next;\n await this.save();\n this.notifyListeners();\n return this.getConfig();\n }\n\n private async save(): Promise<void> {\n const tmpPath = `${this.configPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;\n try {\n await fs.mkdir(path.dirname(this.configPath), { recursive: true });\n await fs.writeFile(tmpPath, JSON.stringify(this.config, null, 2), 'utf-8');\n await fs.rename(tmpPath, this.configPath);\n } catch (err) {\n console.warn('[dsh-chat-translate] Failed to save config file atomically:', err);\n try {\n await fs.unlink(tmpPath);\n } catch {}\n }\n }\n}\n\nexport { TRANSLATE_API_KEY_REF };\n", "import * as fs from 'node:fs';\nimport * as fsp from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as os from 'node:os';\n\n/**\n * Reads API keys from the shared DSH credentials file (~/.dsh/.credentials.yaml).\n *\n * The file is a tiny flat YAML document shaped like:\n *\n * version: 1\n * refs:\n * DEEPSEEK_API_KEY: sk-...\n * TRANSLATE_API_KEY: sk-...\n * records:\n * ...\n *\n * We only need the `refs` section (flat `KEY: value` pairs), so instead of\n * pulling in a YAML dependency we parse it with a small tolerant scanner.\n */\n\n/** Refs key that holds the translation API key. */\nexport const TRANSLATE_API_KEY_REF = 'TRANSLATE_API_KEY';\n\n/**\n * Parse the `refs` section of the credentials file into a flat map.\n * Returns an empty map on any malformed input \u2014 callers treat a missing key\n * as \"not configured\", never as an error.\n */\nexport function parseRefs(yaml: string): Record<string, string> {\n const refs: Record<string, string> = {};\n let inRefs = false;\n\n for (const line of yaml.split(/\\r?\\n/)) {\n const indent = line.match(/^\\s*/)?.[0].length ?? 0;\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n if (indent === 0) {\n // Top-level key \u2014 only `refs:` opens the section we care about.\n inRefs = trimmed === 'refs:' || trimmed.startsWith('refs:');\n continue;\n }\n if (!inRefs) continue;\n\n const m = /^([A-Za-z0-9_.\\-]+):\\s*(.*)$/.exec(trimmed);\n if (!m) continue;\n\n let value = m[2].trim();\n // Quote-aware handling: if the value starts with a quote, find the closing\n // quote first so a ` #` INSIDE the quoted value is not treated as a\n // comment. Anything after the closing quote (e.g. ` # note`) is dropped.\n if (value.startsWith('\"') || value.startsWith(\"'\")) {\n const q = value[0];\n let close = -1;\n for (let i = 1; i < value.length; i++) {\n if (q === '\"' && value[i] === '\\\\') {\n i++; // skip escaped character\n continue;\n }\n if (value[i] === q) {\n close = i;\n break;\n }\n }\n if (close > 0) {\n let inner = value.slice(1, close);\n if (q === '\"') inner = inner.replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\');\n value = inner;\n } else {\n value = value.slice(1); // unterminated quote \u2014 take the rest verbatim\n }\n } else {\n // Unquoted value: strip a trailing inline comment (e.g. `key: sk-x # note`)\n const hashIdx = value.indexOf(' #');\n if (hashIdx >= 0) value = value.slice(0, hashIdx).trim();\n }\n\n if (value) refs[m[1]] = value;\n }\n return refs;\n}\n\n/** How long a read key is considered fresh; keeps batches off the disk. */\nconst KEY_CACHE_TTL_MS = 1000;\n\nexport class CredentialsReader {\n private filePath: string;\n private cachedKey = '';\n private cachedAt = 0;\n\n constructor() {\n const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');\n this.filePath = path.join(dshHome, '.credentials.yaml');\n }\n\n /** Read the file fresh on every call so a key added at runtime takes effect immediately. */\n readRefs(): Record<string, string> {\n try {\n const content = fs.readFileSync(this.filePath, 'utf-8');\n return parseRefs(content);\n } catch {\n return {};\n }\n }\n\n getApiKey(): string {\n const now = Date.now();\n if (now - this.cachedAt < KEY_CACHE_TTL_MS) {\n return this.cachedKey;\n }\n this.cachedKey = (this.readRefs()[TRANSLATE_API_KEY_REF] || '').trim();\n this.cachedAt = now;\n return this.cachedKey;\n }\n\n /**\n * Write (or clear) the TRANSLATE_API_KEY ref, preserving every other line of\n * the file (other refs, records section, comments). The file stays the single\n * source of truth for credentials and keeps 0600 permissions. An empty key\n * removes the ref entirely.\n */\n async setApiKey(apiKey: string): Promise<void> {\n const normalized = apiKey.trim();\n let lines: string[];\n try {\n lines = fs.readFileSync(this.filePath, 'utf-8').split(/\\r?\\n/);\n } catch {\n // File missing -> create the minimal document shape.\n lines = ['version: 1', 'refs:', 'records: {}'];\n }\n\n const refsStart = lines.findIndex((l) => l.trim() === 'refs:' || l.trim().startsWith('refs:'));\n let replaced = false;\n if (refsStart >= 0) {\n for (let i = refsStart + 1; i < lines.length; i++) {\n const indent = lines[i].match(/^\\s*/)?.[0].length ?? 0;\n if (indent === 0) break; // left the refs section\n if (/^TRANSLATE_API_KEY\\s*:/.test(lines[i].trim())) {\n if (normalized) {\n lines[i] = ` TRANSLATE_API_KEY: \"${escapeYaml(normalized)}\"`;\n } else {\n lines.splice(i, 1); // clear -> remove the ref line\n }\n replaced = true;\n break;\n }\n }\n }\n\n if (!replaced && normalized) {\n if (refsStart >= 0) {\n // Insert at the end of the refs section (before the next top-level key).\n let insertAt = lines.length;\n for (let i = refsStart + 1; i < lines.length; i++) {\n const indent = lines[i].match(/^\\s*/)?.[0].length ?? 0;\n if (indent === 0 && lines[i].trim()) {\n insertAt = i;\n break;\n }\n }\n lines.splice(insertAt, 0, ` TRANSLATE_API_KEY: \"${escapeYaml(normalized)}\"`);\n } else {\n // No refs section yet -> create one right after the version line.\n lines.splice(1, 0, 'refs:', ` TRANSLATE_API_KEY: \"${escapeYaml(normalized)}\"`);\n }\n }\n\n await fsp.mkdir(path.dirname(this.filePath), { recursive: true });\n // Atomic write: temp file + rename, so a crash mid-write can never leave\n // the credentials file truncated. Keep 0600 permissions throughout.\n const tmpPath = `${this.filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;\n await fsp.writeFile(tmpPath, lines.join('\\n'), 'utf-8');\n await fsp.chmod(tmpPath, 0o600);\n await fsp.rename(tmpPath, this.filePath);\n\n // Invalidate the short cache so the new key is picked up immediately.\n this.cachedKey = '';\n this.cachedAt = 0;\n }\n}\n\n/** Escape a value for a double-quoted YAML scalar. */\nfunction escapeYaml(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n", "import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as os from 'node:os';\n\n/** Entries older than this are treated as expired. */\nconst TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\n\ninterface CacheEntry {\n t: number; // epoch ms; 0 = legacy entry without timestamp (never expires on its own)\n v: string;\n}\n\nexport class LruDiskCache {\n private cache = new Map<string, CacheEntry>();\n private maxEntries: number;\n private filePath: string;\n private saveTimer: NodeJS.Timeout | null = null;\n private dirty = false;\n\n constructor(maxEntries = 1000) {\n this.maxEntries = maxEntries;\n const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');\n this.filePath = path.join(dshHome, 'dsh-chat-translate-cache.json');\n }\n\n async init(): Promise<void> {\n try {\n const content = await fs.readFile(this.filePath, 'utf-8');\n const obj = JSON.parse(content);\n if (obj && typeof obj === 'object') {\n for (const [k, raw] of Object.entries(obj)) {\n if (typeof raw === 'string') {\n // Legacy entry from an older release \u2014 keep it, no known timestamp.\n this.cache.set(k, { t: 0, v: raw });\n } else if (raw && typeof raw === 'object' && typeof (raw as CacheEntry).v === 'string') {\n const entry = raw as CacheEntry;\n if (typeof entry.t === 'number' && Number.isFinite(entry.t)) {\n this.cache.set(k, entry);\n }\n }\n }\n }\n } catch {\n // Ignore missing or corrupt cache file\n }\n }\n\n get(key: string): string | undefined {\n const entry = this.cache.get(key);\n if (entry === undefined) return undefined;\n if (entry.t > 0 && Date.now() - entry.t > TTL_MS) {\n this.cache.delete(key);\n return undefined;\n }\n // Refresh key in LRU order (re-insert at the end)\n this.cache.delete(key);\n this.cache.set(key, entry);\n return entry.v;\n }\n\n set(key: string, value: string): void {\n if (this.cache.has(key)) {\n this.cache.delete(key);\n } else if (this.cache.size >= this.maxEntries) {\n // Remove least recently used entry (first in Map iterator)\n const oldestKey = this.cache.keys().next().value;\n if (oldestKey !== undefined) {\n this.cache.delete(oldestKey);\n }\n }\n this.cache.set(key, { t: Date.now(), v: value });\n this.dirty = true;\n this.scheduleSave();\n }\n\n private scheduleSave(): void {\n if (this.saveTimer) return;\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n if (this.dirty) {\n this.dirty = false;\n this.flush().catch((err) => {\n console.warn('[dsh-chat-translate] Failed to flush cache to disk:', err);\n });\n }\n }, 5000);\n }\n\n async flush(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n this.dirty = false;\n\n const tmpPath = `${this.filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;\n try {\n const obj: Record<string, CacheEntry> = {};\n for (const [k, v] of this.cache.entries()) {\n obj[k] = v;\n }\n await fs.mkdir(path.dirname(this.filePath), { recursive: true });\n await fs.writeFile(tmpPath, JSON.stringify(obj, null, 2), 'utf-8');\n await fs.rename(tmpPath, this.filePath);\n } catch (err) {\n console.warn('[dsh-chat-translate] Failed to write cache file atomically:', err);\n try {\n await fs.unlink(tmpPath);\n } catch {}\n }\n }\n\n async dispose(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n if (this.dirty) {\n await this.flush();\n }\n }\n}\n", "import type { ITranslationAdapter, PluginConfig } from './base.ts';\n\n/**\n * Built-in Bing Web translator channel \u2014 no key, no gateway jar, mainland\n * networks reach cn.bing.com directly. This is the same flow DeepLX and\n * Translate_Api_Free use: fetch the translator page for the IG token and the\n * abuse-prevention key/token, then POST to ttranslatev3.\n */\n\nconst TRANSLATOR_URL = 'https://cn.bing.com/translator';\nconst TRANSLATE_URL = 'https://cn.bing.com/ttranslatev3?isVertical=1&&IG={IG}&IID=translator.5025.1';\nconst UA =\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';\n\nconst IG_RES = [\n /_IG=\"([a-zA-Z0-9]+)\"/,\n /,IG:\"([a-zA-Z0-9]+)\"/,\n /IG:\"([a-zA-Z0-9]+)\"/,\n /\"IG\":\"([a-zA-Z0-9]+)\"/,\n];\nconst ABUSE_RES = [\n /params_AbusePreventionHelper\\s*=\\s*\\[\\s*(\\d+)\\s*,\\s*\"([^\"]+)\"/,\n /var\\s+params_AbusePreventionHelper\\s*=\\s*\\[\\s*(\\d+)\\s*,\\s*\"([^\"]+)\"/,\n];\n\ninterface BingTokens {\n ig: string;\n key: string;\n token: string;\n}\n\nlet cachedTokens: BingTokens | null = null;\nlet tokensFetchedAt = 0;\nconst TOKEN_TTL_MS = 15 * 60 * 1000; // 15 minutes\nlet inFlightTokenPromise: Promise<BingTokens> | null = null;\n\nfunction parseTokens(html: string): BingTokens {\n let ig: string | undefined;\n for (const re of IG_RES) {\n const m = re.exec(html);\n if (m && m[1]) {\n ig = m[1];\n break;\n }\n }\n\n let key: string | undefined;\n let token: string | undefined;\n for (const re of ABUSE_RES) {\n const m = re.exec(html);\n if (m && m[1] && m[2]) {\n key = m[1];\n token = m[2];\n break;\n }\n }\n\n if (!ig || !key || !token) {\n throw new Error(`Bing translator page: missing tokens (ig: ${!!ig}, key: ${!!key}, token: ${!!token})`);\n }\n\n return { ig, key, token };\n}\n\nexport async function fetchTokens(signal: AbortSignal, forceRefresh = false): Promise<BingTokens> {\n if (!forceRefresh && cachedTokens && Date.now() - tokensFetchedAt < TOKEN_TTL_MS) {\n return cachedTokens;\n }\n\n if (inFlightTokenPromise) {\n return inFlightTokenPromise;\n }\n\n inFlightTokenPromise = (async () => {\n try {\n const response = await fetch(TRANSLATOR_URL, {\n headers: {\n 'User-Agent': UA,\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n },\n signal,\n });\n if (!response.ok) {\n throw new Error(`Bing translator page responded with status ${response.status}`);\n }\n const html = await response.text();\n const tokens = parseTokens(html);\n cachedTokens = tokens;\n tokensFetchedAt = Date.now();\n return tokens;\n } finally {\n inFlightTokenPromise = null;\n }\n })();\n\n return inFlightTokenPromise;\n}\n\nexport class BingWebAdapter implements ITranslationAdapter {\n readonly id = 'bing';\n readonly name = '\u5FAE\u8F6F Bing \u7F51\u9875\u7FFB\u8BD1 (\u514DKey\u76F4\u8FDE)';\n\n isAvailable(_config: PluginConfig): boolean {\n return true; // No key, no gateway URL required\n }\n\n async translate(text: string, signal: AbortSignal, config: PluginConfig): Promise<string> {\n const targetLang = config.targetLang || 'zh-Hans';\n return this.executeTranslate(text, signal, targetLang, false);\n }\n\n private async executeTranslate(\n text: string,\n signal: AbortSignal,\n targetLang: string,\n isRetry: boolean\n ): Promise<string> {\n const tokens = await fetchTokens(signal, isRetry);\n\n const body = new URLSearchParams({\n fromLang: 'auto-detect',\n text,\n to: targetLang,\n key: tokens.key,\n token: tokens.token,\n tryFetchingGenderDebiasedTranslations: 'true',\n });\n\n const response = await fetch(TRANSLATE_URL.replace('{IG}', tokens.ig), {\n method: 'POST',\n headers: {\n 'User-Agent': UA,\n Referer: 'https://cn.bing.com/translator/',\n Origin: 'https://cn.bing.com',\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body,\n signal,\n });\n\n if (!response.ok) {\n cachedTokens = null;\n // Auto retry once with fresh tokens if not already retrying\n if (!isRetry && (response.status === 400 || response.status === 401 || response.status === 403)) {\n return this.executeTranslate(text, signal, targetLang, true);\n }\n throw new Error(`Bing translate responded with status ${response.status}`);\n }\n\n const json = (await response.json()) as Array<{ translations?: Array<{ text?: string }> }>;\n const translated = json?.[0]?.translations?.[0]?.text?.trim();\n if (!translated) {\n cachedTokens = null;\n if (!isRetry) {\n return this.executeTranslate(text, signal, targetLang, true);\n }\n throw new Error('Bing translate returned an empty result');\n }\n return translated;\n }\n}\n", "import type { ITranslationAdapter, PluginConfig } from './base.ts';\nimport type { CredentialsReader } from '../credentials.ts';\n\n/**\n * OpenAI-compatible Chat Completions translation channel.\n *\n * Talks to `POST {baseUrl}/chat/completions` with a Bearer token read from\n * ~/.dsh/.credentials.yaml (refs.TRANSLATE_API_KEY). Works with OpenAI,\n * DeepSeek, Qwen, Ollama and any other service exposing the standard endpoint.\n */\n\n/** Map Bing-style targetLang codes to a natural language name for the prompt. */\nconst LANG_HINTS: Record<string, string> = {\n 'zh-hans': 'Simplified Chinese',\n 'zh-cn': 'Simplified Chinese',\n 'zh': 'Simplified Chinese',\n 'zh-tw': 'Traditional Chinese',\n 'zh-hant': 'Traditional Chinese',\n en: 'English',\n ja: 'Japanese',\n ko: 'Korean',\n fr: 'French',\n de: 'German',\n es: 'Spanish',\n ru: 'Russian',\n pt: 'Portuguese',\n it: 'Italian',\n};\n\nexport class OpenAiCompatibleAdapter implements ITranslationAdapter {\n readonly id = 'openai';\n readonly name = 'OpenAI \u517C\u5BB9 (Chat Completions)';\n\n private credentials: CredentialsReader;\n\n constructor(credentials: CredentialsReader) {\n this.credentials = credentials;\n }\n\n isAvailable(config: PluginConfig): boolean {\n return Boolean(\n config.aiEnabled && config.baseUrl?.trim() && config.model?.trim() && this.credentials.getApiKey()\n );\n }\n\n async translate(text: string, signal: AbortSignal, config: PluginConfig): Promise<string> {\n const apiKey = this.credentials.getApiKey();\n if (!apiKey) {\n throw new Error(`TRANSLATE_API_KEY is not configured in ~/.dsh/.credentials.yaml`);\n }\n const baseUrl = (config.baseUrl || '').trim().replace(/\\/+$/, '');\n const model = (config.model || '').trim();\n if (!baseUrl || !model) {\n throw new Error('OpenAI channel: baseUrl or model is not configured');\n }\n\n const langName = LANG_HINTS[(config.targetLang || 'zh-Hans').toLowerCase()] || config.targetLang || 'Simplified Chinese';\n\n const response = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n temperature: 0,\n messages: [\n {\n role: 'system',\n content:\n `You are a professional translator. Translate the user's message into ${langName}. ` +\n `Output ONLY the translated text \u2014 no explanations, no quotation marks, no extra words. ` +\n `Preserve every placeholder like __DSH_MASK_0__ exactly as-is.`,\n },\n { role: 'user', content: text },\n ],\n }),\n signal,\n });\n\n if (!response.ok) {\n let detail = '';\n try {\n const errBody = (await response.json()) as { error?: { message?: string }; message?: string };\n detail = errBody?.error?.message || errBody?.message || '';\n } catch {\n // ignore body parse errors\n }\n throw new Error(`OpenAI-compatible API responded with ${response.status}${detail ? `: ${detail}` : ''}`);\n }\n\n const data = (await response.json()) as { choices?: Array<{ message?: { content?: string } }> };\n const content = data?.choices?.[0]?.message?.content;\n const translated = typeof content === 'string' ? content.trim() : '';\n if (!translated) {\n throw new Error('OpenAI-compatible API returned empty content');\n }\n return translated;\n }\n}\n", "export interface MaskResult {\n maskedText: string;\n unmask: (translatedText: string) => string;\n}\n\nexport class ContentMaskingPipeline {\n mask(text: string): MaskResult {\n if (!text || typeof text !== 'string') {\n return {\n maskedText: text,\n unmask: (t: string) => t,\n };\n }\n\n const masks: string[] = [];\n const addMask = (match: string): string => {\n const idx = masks.length;\n masks.push(match);\n return `__DSH_MASK_${idx}__`;\n };\n\n let processed = text;\n\n // 1. Multi-line code blocks (```...``` or ~~~...~~~)\n processed = processed.replace(/(?:```|~~~)[\\s\\S]*?(?:```|~~~)/g, (m) => addMask(m));\n\n // 2. Inline code (`...`)\n processed = processed.replace(/`[^`\\n]+`/g, (m) => addMask(m));\n\n // 3. URLs\n processed = processed.replace(/https?:\\/\\/[^\\s)\\];,;\"'<>]+/g, (m) => addMask(m));\n\n // 4. File paths and filenames with known extensions\n processed = processed.replace(\n /(?:(?:\\/|[a-zA-Z]:[\\\\\\/]|\\.\\.?[\\\\\\/])[\\w.\\-\\\\\\/]+|\\b(?:[\\w.\\-]+\\/)+[\\w.\\-]+\\.[a-zA-Z0-9]+\\b|\\b[\\w.\\-]+\\.(?:ts|tsx|js|jsx|json|ya?ml|md|py|go|rs|c|cpp|h|hpp|css|scss|html|sh|bash|mjs|cjs|toml|lock|log|env|svg|png|jpe?g|gif|tar|gz|zip|xml|sql)\\b)/g,\n (m) => addMask(m)\n );\n\n // 5. CLI flags / options (--flag, --flag=value, -f)\n processed = processed.replace(\n /(?<=^|[\\s(\\[{\"'])((?:--[a-zA-Z0-9_\\-]+(?:=[^\\s\"'<>]+)?)|(?:-[a-zA-Z0-9]+))(?=[\\s)\\]}\",:;!?]|$)/g,\n (m) => addMask(m)\n );\n\n const unmask = (translatedText: string): string => {\n if (!translatedText || masks.length === 0) {\n return translatedText;\n }\n // Tolerate whitespace variations and casing introduced by MT engines\n // e.g. \"__ DSH_MASK_0 __\", \"__dsh_mask_0__\", \"__DSH _ MASK _ 0__\"\n return translatedText.replace(\n /__\\s*DSH\\s*_\\s*MASK\\s*_\\s*(\\d+)\\s*__/gi,\n (_fullMatch, indexStr) => {\n const idx = parseInt(indexStr, 10);\n if (!Number.isNaN(idx) && idx >= 0 && idx < masks.length) {\n return masks[idx];\n }\n return _fullMatch;\n }\n );\n };\n\n return {\n maskedText: processed,\n unmask,\n };\n }\n}\n", "import type { ITranslationAdapter, PluginConfig, TranslateItemResult } from './types.ts';\nimport type { ConfigManager } from './config.ts';\nimport { MAX_CONCURRENCY } from './config.ts';\nimport type { LruDiskCache } from './cache.ts';\nimport type { CredentialsReader } from './credentials.ts';\nimport { BingWebAdapter } from './adapters/bing.ts';\nimport { OpenAiCompatibleAdapter } from './adapters/openai.ts';\nimport { ContentMaskingPipeline } from './pipeline/masking.ts';\n\ntype CircuitStateEnum = 'closed' | 'open' | 'half-open';\n\ninterface CircuitState {\n state: CircuitStateEnum;\n failureCount: number;\n openUntil: number;\n probeInFlight: boolean; // single-flight guard for half-open probes\n}\n\nexport class TranslationDispatcher {\n private configManager: ConfigManager;\n private cache: LruDiskCache;\n private credentials: CredentialsReader;\n private masking = new ContentMaskingPipeline();\n private adapters = new Map<string, ITranslationAdapter>();\n private circuitStates = new Map<string, CircuitState>();\n private inFlightMap = new Map<string, Promise<TranslateItemResult>>();\n private activeCount = 0;\n private queue: Array<() => void> = [];\n\n constructor(configManager: ConfigManager, cache: LruDiskCache, credentials?: CredentialsReader) {\n this.configManager = configManager;\n this.cache = cache;\n this.credentials = credentials ?? (configManager as any).credentials ?? { getApiKey: () => '' };\n\n // AI channel first (primary), Bing second (fallback) \u2014 map iteration order\n // follows registration order, so computeChannels() yields ['openai', 'bing'].\n this.registerAdapter(new OpenAiCompatibleAdapter(this.credentials));\n this.registerAdapter(new BingWebAdapter());\n\n // Listen to config changes to wake up queue on concurrency increase\n this.configManager.onConfigChange(() => {\n this.processNext();\n });\n }\n\n private registerAdapter(adapter: ITranslationAdapter): void {\n this.adapters.set(adapter.id, adapter);\n }\n\n /**\n * Decide which channels are active for the current config, in priority order.\n *\n * Truth table (user contract):\n * - AI on + configured + Bing on -> [openai, bing] (AI first, Bing fallback)\n * - AI on + NOT configured + Bing on -> [bing]\n * - AI on + NOT configured + Bing off -> [] (no translation)\n * - AI off + Bing on -> [bing]\n * - AI off + Bing off -> [] (no translation)\n */\n private computeChannels(config: PluginConfig): string[] {\n const channels: string[] = [];\n for (const [id, adapter] of this.adapters) {\n if (id === 'openai') {\n if (\n config.aiEnabled &&\n config.baseUrl?.trim() &&\n config.model?.trim() &&\n this.credentials.getApiKey()\n ) {\n channels.push(id);\n }\n continue;\n }\n if (id === 'bing') {\n if (config.bingEnabled) channels.push(id);\n continue;\n }\n // Custom/test adapters honor their own availability.\n if (adapter.isAvailable(config)) channels.push(id);\n }\n return channels;\n }\n\n async translateBatch(\n texts: string[],\n forceRefresh = false\n ): Promise<TranslateItemResult[]> {\n return Promise.all(texts.map((t) => this.translateOne(t, forceRefresh)));\n }\n\n async translateOne(\n rawText: string,\n forceRefresh = false\n ): Promise<TranslateItemResult> {\n const text = rawText.trim();\n if (!text) {\n return { original: rawText, translated: rawText, channel: 'none', cached: true };\n }\n\n const config = this.configManager.getConfig();\n if (!config.enabled) {\n return { original: rawText, translated: rawText, channel: 'disabled', cached: true };\n }\n\n const cacheKey = text.toLowerCase();\n\n // 1. Check L1/L2 Cache\n if (!forceRefresh) {\n const cached = this.cache.get(cacheKey);\n if (cached) {\n return { original: rawText, translated: cached, channel: 'cache', cached: true };\n }\n }\n\n // 2. In-flight Promise deduplication (only when not forceRefresh)\n if (!forceRefresh) {\n const inFlight = this.inFlightMap.get(cacheKey);\n if (inFlight) {\n return inFlight;\n }\n }\n\n // Mask code blocks, inline code, paths, urls, flags\n const { maskedText, unmask } = this.masking.mask(text);\n\n // 3. Queue task with concurrency limit\n const taskPromise = this.enqueueTask(async () => {\n const currentConfig = this.configManager.getConfig();\n const channels = this.computeChannels(currentConfig);\n\n for (const chId of channels) {\n const adapter = this.adapters.get(chId);\n if (!adapter || !adapter.isAvailable(currentConfig) || this.isCircuitOpen(chId)) {\n continue;\n }\n\n try {\n const timeout = chId === 'openai'\n ? currentConfig.aiTimeoutMs || 30000\n : currentConfig.timeoutMs || 2000;\n const abortCtrl = new AbortController();\n const timer = setTimeout(() => abortCtrl.abort(), timeout);\n\n let translatedMasked = '';\n try {\n translatedMasked = await adapter.translate(maskedText, abortCtrl.signal, currentConfig);\n } finally {\n clearTimeout(timer);\n }\n\n const cleaned = translatedMasked?.trim();\n if (cleaned && cleaned.length > 0) {\n const finalTranslated = unmask(cleaned);\n this.recordSuccess(chId);\n this.cache.set(cacheKey, finalTranslated);\n return {\n original: rawText,\n translated: finalTranslated,\n channel: chId,\n cached: false,\n };\n }\n // Empty result counts as a failure: it releases a half-open probe\n // flag (which would otherwise leak and permanently bypass the\n // channel) and feeds the circuit-breaker failure counter.\n this.recordFailure(chId);\n console.warn(\n `[dsh-chat-translate] channel ${chId} returned an empty translation | text: ${text.slice(0, 60)}`\n );\n } catch (err: any) {\n this.recordFailure(chId);\n console.warn(\n `[dsh-chat-translate] channel ${chId} failed: ${err?.message || String(err)} | text: ${text.slice(0, 60)}`\n );\n // Continue to next channel\n }\n }\n\n return { original: rawText, translated: rawText, channel: 'fallback', cached: false };\n });\n\n if (!forceRefresh) {\n this.inFlightMap.set(cacheKey, taskPromise);\n }\n\n try {\n return await taskPromise;\n } finally {\n if (!forceRefresh) {\n this.inFlightMap.delete(cacheKey);\n }\n }\n }\n\n async testChannel(channelId: string): Promise<{ ok: boolean; latencyMs: number; error?: string }> {\n const adapter = this.adapters.get(channelId);\n const config = this.configManager.getConfig();\n if (!adapter) {\n return { ok: false, latencyMs: 0, error: `Channel ${channelId} not found` };\n }\n if (!adapter.isAvailable(config)) {\n return { ok: false, latencyMs: 0, error: `Channel ${channelId} is not configured or disabled` };\n }\n\n const testText = 'List files in current directory';\n const start = Date.now();\n try {\n const timeout = channelId === 'openai' ? Math.min(config.aiTimeoutMs || 30000, 30000) : 4000;\n const abortCtrl = new AbortController();\n const timer = setTimeout(() => abortCtrl.abort(), timeout);\n let res = '';\n try {\n res = await adapter.translate(testText, abortCtrl.signal, config);\n } finally {\n clearTimeout(timer);\n }\n\n const latencyMs = Date.now() - start;\n if (res && res.trim()) {\n return { ok: true, latencyMs };\n }\n return { ok: false, latencyMs, error: 'Empty translation returned' };\n } catch (err: any) {\n return { ok: false, latencyMs: Date.now() - start, error: err?.message || String(err) };\n }\n }\n\n private enqueueTask<T>(task: () => Promise<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const exec = async () => {\n this.activeCount++;\n try {\n const result = await task();\n resolve(result);\n } catch (err) {\n reject(err);\n } finally {\n this.activeCount--;\n this.processNext();\n }\n };\n\n const maxConcurrency = Math.min(\n Math.max(this.configManager.getConfig().concurrency || 3, 1),\n MAX_CONCURRENCY\n );\n\n if (this.activeCount < maxConcurrency) {\n exec();\n } else {\n this.queue.push(exec);\n }\n });\n }\n\n private processNext(): void {\n const maxConcurrency = Math.min(\n Math.max(this.configManager.getConfig().concurrency || 3, 1),\n MAX_CONCURRENCY\n );\n\n while (this.queue.length > 0 && this.activeCount < maxConcurrency) {\n const next = this.queue.shift();\n if (next) {\n next();\n }\n }\n }\n\n private isCircuitOpen(channelId: string): boolean {\n let state = this.circuitStates.get(channelId);\n if (!state) return false;\n\n if (state.state === 'open') {\n if (Date.now() >= state.openUntil) {\n // Timeout elapsed -> transition to half-open; the first caller becomes\n // the single in-flight probe.\n state.state = 'half-open';\n state.probeInFlight = true;\n return false;\n }\n return true;\n }\n\n if (state.state === 'half-open') {\n // Single-flight: exactly one probe may run at a time, all others wait.\n if (state.probeInFlight) return true;\n state.probeInFlight = true;\n return false;\n }\n\n return false;\n }\n\n private recordSuccess(channelId: string): void {\n const state = this.circuitStates.get(channelId);\n if (state) {\n state.state = 'closed';\n state.failureCount = 0;\n state.openUntil = 0;\n state.probeInFlight = false;\n }\n }\n\n private recordFailure(channelId: string): void {\n let state = this.circuitStates.get(channelId);\n if (!state) {\n state = { state: 'closed', failureCount: 0, openUntil: 0, probeInFlight: false };\n this.circuitStates.set(channelId, state);\n }\n\n if (state.state === 'half-open') {\n // Probe failed -> trip back to open for 30s\n state.state = 'open';\n state.failureCount = 3;\n state.openUntil = Date.now() + 30000;\n state.probeInFlight = false;\n return;\n }\n\n state.failureCount++;\n if (state.failureCount >= 3) {\n state.state = 'open';\n state.openUntil = Date.now() + 30000; // Open circuit for 30 seconds\n }\n }\n}\n", "import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { ConfigManager } from './config.ts';\nimport type { TranslationDispatcher } from './dispatcher.ts';\nimport type { CredentialsReader } from './credentials.ts';\n\nconst MAX_BODY_BYTES = 1024 * 1024; // 1MB body limit to prevent DoS\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body);\n res.writeHead(status, {\n 'Content-Type': 'application/json; charset=utf-8',\n 'Content-Length': Buffer.byteLength(json),\n });\n res.end(json);\n}\n\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let totalLength = 0;\n\n req.on('data', (chunk) => {\n const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n totalLength += buf.length;\n if (totalLength > MAX_BODY_BYTES) {\n if (typeof req.destroy === 'function') {\n req.destroy();\n }\n reject(new Error('Request body exceeded maximum allowed size (1MB)'));\n return;\n }\n chunks.push(buf);\n });\n\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nexport function createHttpHandler(\n configManager: ConfigManager,\n dispatcher: TranslationDispatcher,\n credentials?: CredentialsReader\n) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const url = new URL(req.url || '/', 'http://localhost');\n const pathParts = url.pathname.split('/').filter(Boolean);\n // pathParts will start with ['api', 'dsh-chat-translate', ...]\n const endpoint = pathParts[2] || '';\n\n try {\n if (endpoint === 'translate' && req.method === 'POST') {\n const raw = await readBody(req);\n let parsed: any;\n try {\n parsed = JSON.parse(raw || '{}');\n } catch {\n sendJson(res, 400, { ok: false, error: 'Invalid JSON body' });\n return;\n }\n const rawTexts: unknown = parsed.texts !== undefined ? parsed.texts : parsed.text;\n\n let texts: string[] = [];\n if (Array.isArray(rawTexts)) {\n texts = rawTexts.filter((t): t is string => typeof t === 'string');\n } else if (typeof rawTexts === 'string') {\n texts = [rawTexts];\n }\n\n const forceRefresh = Boolean(parsed.forceRefresh);\n\n if (texts.length === 0) {\n sendJson(res, 200, { ok: true, results: [] });\n return;\n }\n\n const results = await dispatcher.translateBatch(texts, forceRefresh);\n sendJson(res, 200, { ok: true, results });\n return;\n }\n\n if (endpoint === 'config') {\n if (req.method === 'GET') {\n sendJson(res, 200, { ok: true, config: configManager.getMaskedConfig() });\n return;\n }\n if (req.method === 'POST') {\n const raw = await readBody(req);\n let updates: any;\n try {\n updates = JSON.parse(raw || '{}');\n } catch {\n sendJson(res, 400, { ok: false, error: 'Invalid JSON body' });\n return;\n }\n if (typeof updates !== 'object' || updates === null || Array.isArray(updates)) {\n sendJson(res, 400, { ok: false, error: 'Invalid config payload' });\n return;\n }\n await configManager.updateConfig(updates);\n sendJson(res, 200, { ok: true, config: configManager.getMaskedConfig() });\n return;\n }\n }\n\n if (endpoint === 'credentials' && req.method === 'POST') {\n if (!credentials) {\n sendJson(res, 500, { ok: false, error: 'Credentials reader unavailable' });\n return;\n }\n const raw = await readBody(req);\n let parsed: any;\n try {\n parsed = JSON.parse(raw || '{}');\n } catch {\n sendJson(res, 400, { ok: false, error: 'Invalid JSON body' });\n return;\n }\n const apiKey = typeof parsed.apiKey === 'string' ? parsed.apiKey : '';\n try {\n await credentials.setApiKey(apiKey);\n sendJson(res, 200, { ok: true, configured: Boolean(credentials.getApiKey()) });\n } catch (err: any) {\n sendJson(res, 500, { ok: false, error: err?.message || String(err) });\n }\n return;\n }\n\n if (endpoint === 'test-channel' && req.method === 'POST') {\n const raw = await readBody(req);\n let parsed: any;\n try {\n parsed = JSON.parse(raw || '{}');\n } catch {\n sendJson(res, 400, { ok: false, error: 'Invalid JSON body' });\n return;\n }\n const channelId = typeof parsed.channel === 'string' ? parsed.channel : '';\n const result = await dispatcher.testChannel(channelId);\n sendJson(res, 200, result);\n return;\n }\n\n sendJson(res, 404, { ok: false, error: 'Endpoint not found' });\n } catch (err: any) {\n const status = err?.message?.includes('exceeded maximum allowed size') ? 413 : 500;\n sendJson(res, status, { ok: false, error: err?.message || String(err) });\n }\n };\n}", "import { ConfigManager } from './server/config.ts';\nimport { CredentialsReader } from './server/credentials.ts';\nimport { LruDiskCache } from './server/cache.ts';\nimport { TranslationDispatcher } from './server/dispatcher.ts';\nimport { createHttpHandler } from './server/router.ts';\n\n/** Stable Cordis loader name. */\nexport const name = 'dsh-chat-translate';\n\n/** Inject webServer to serve the translation API */\nexport const inject = ['webServer'];\n\ninterface HostContext {\n webServer?: {\n register(route: {\n kind: 'prefix' | 'exact';\n path: string;\n handler: (req: any, res: any) => Promise<void> | void;\n }): () => void;\n };\n effect(factory: () => void | (() => void), label: string): void;\n get?(serviceName: string): any;\n}\n\n/** Mount the host half; provides translation proxy and settings persistence */\nexport function apply(ctx: HostContext): void {\n const credentials = new CredentialsReader();\n const configManager = new ConfigManager(credentials);\n const cache = new LruDiskCache(1000);\n const dispatcher = new TranslationDispatcher(configManager, cache, credentials);\n\n // Initialize async resources\n const initPromise = Promise.all([configManager.init(), cache.init()]).catch((err) => {\n console.warn('[dsh-chat-translate] Initialization error:', err);\n });\n\n const webServer = ctx.webServer || (ctx.get ? ctx.get('webServer') : null);\n if (webServer && typeof webServer.register === 'function') {\n const rawHandler = createHttpHandler(configManager, dispatcher, credentials);\n const handler = async (req: any, res: any) => {\n await initPromise;\n return rawHandler(req, res);\n };\n\n ctx.effect(\n () => {\n const unregister = webServer.register({\n kind: 'prefix',\n path: '/api/dsh-chat-translate',\n handler,\n });\n return () => {\n if (typeof unregister === 'function') {\n unregister();\n }\n cache.dispose().catch((err) => {\n console.warn('[dsh-chat-translate] Dispose cache error:', err);\n });\n };\n },\n 'dsh-chat-translate: translation API routes'\n );\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["fs", "path", "os", "fs", "path", "os"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Client plugin name, shared with the browser bundle id. */
|
|
2
|
+
export declare const name = "dsh-chat-translate";
|
|
3
|
+
/** Declare dependency on slots for settings section */
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
interface ClientContext {
|
|
6
|
+
effect(factory: () => void | (() => void), label: string): void;
|
|
7
|
+
get?(serviceName: string): any;
|
|
8
|
+
slots?: any;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Mount the tool-call / think-summary translation observer and the settings UI.
|
|
12
|
+
* @param ctx - DSH browser client context.
|
|
13
|
+
*/
|
|
14
|
+
export declare function apply(ctx: ClientContext): void;
|
|
15
|
+
export { chatTranslateObserver } from './translate/observer.ts';
|
|
16
|
+
export { setupSettingsUi } from './settings/ui.tsx';
|
|
17
|
+
export { NonDestructiveTranslationMount } from './translate/mount.ts';
|
|
18
|
+
export { StreamDebounceViewportObserver } from './translate/viewport-observer.ts';
|
|
19
|
+
export { clientCache } from './translate/client-cache.ts';
|
|
20
|
+
export { lazyQueue } from './translate/lazy.ts';
|
|
21
|
+
export { settingsStore } from './settings/store.ts';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface ClientSettingsState {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
concurrency: number;
|
|
4
|
+
aiEnabled: boolean;
|
|
5
|
+
bingEnabled: boolean;
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
model: string;
|
|
8
|
+
aiConfigured: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const LS_ENABLED = "dsh-chat-translate:enabled";
|
|
11
|
+
export declare const LS_CONCURRENCY = "dsh-chat-translate:concurrency";
|
|
12
|
+
export declare const LS_AI_ENABLED = "dsh-chat-translate:aiEnabled";
|
|
13
|
+
export declare const LS_BING_ENABLED = "dsh-chat-translate:bingEnabled";
|
|
14
|
+
export declare const LS_BASE_URL = "dsh-chat-translate:baseUrl";
|
|
15
|
+
export declare const LS_MODEL = "dsh-chat-translate:model";
|
|
16
|
+
declare class SettingsStore {
|
|
17
|
+
private state;
|
|
18
|
+
private listeners;
|
|
19
|
+
private storageListener;
|
|
20
|
+
private pushTimer;
|
|
21
|
+
private pushSeq;
|
|
22
|
+
private userTouched;
|
|
23
|
+
constructor();
|
|
24
|
+
private loadFromLocalStorage;
|
|
25
|
+
private initStorageListener;
|
|
26
|
+
private syncFromServer;
|
|
27
|
+
getState(): ClientSettingsState;
|
|
28
|
+
subscribe(listener: () => void): () => void;
|
|
29
|
+
private notify;
|
|
30
|
+
/**
|
|
31
|
+
* Debounced server push: fast typing in the baseUrl/model/concurrency inputs
|
|
32
|
+
* collapses into a single POST (300ms trailing). A monotonic sequence number
|
|
33
|
+
* ensures an out-of-order older response never overwrites newer state.
|
|
34
|
+
*/
|
|
35
|
+
private schedulePush;
|
|
36
|
+
private pushToServer;
|
|
37
|
+
update(partial: Partial<ClientSettingsState>): Promise<void>;
|
|
38
|
+
testChannel(channel: string): Promise<{
|
|
39
|
+
ok: boolean;
|
|
40
|
+
latencyMs: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
}>;
|
|
43
|
+
/** Persist the API key via the host and refresh the server-derived status. */
|
|
44
|
+
saveApiKey(apiKey: string): Promise<{
|
|
45
|
+
ok: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
}>;
|
|
48
|
+
/** Re-pull the server config (e.g. after the API key changed). */
|
|
49
|
+
refreshFromServer(): Promise<void>;
|
|
50
|
+
dispose(): void;
|
|
51
|
+
}
|
|
52
|
+
export declare const settingsStore: SettingsStore;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SETTINGS_CSS: string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface TranslateItemResult {
|
|
2
|
+
original: string;
|
|
3
|
+
translated: string;
|
|
4
|
+
channel: string;
|
|
5
|
+
cached: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface TranslateBatchOptions {
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function requestTranslateBatch(texts: string[], options?: TranslateBatchOptions): Promise<TranslateItemResult[]>;
|
|
12
|
+
export declare function fetchServerConfig(): Promise<any>;
|
|
13
|
+
export declare function updateServerConfig(updates: any): Promise<any>;
|
|
14
|
+
export declare function testServerChannel(channel: string): Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
latencyMs: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** Persist the translation API key into ~/.dsh/.credentials.yaml via the host. */
|
|
20
|
+
export declare function saveCredentials(apiKey: string): Promise<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
configured?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class ClientCache {
|
|
2
|
+
private memCache;
|
|
3
|
+
private dirty;
|
|
4
|
+
private saveTimer;
|
|
5
|
+
constructor();
|
|
6
|
+
private load;
|
|
7
|
+
get(text: string): string | undefined;
|
|
8
|
+
set(text: string, translated: string): void;
|
|
9
|
+
private scheduleSave;
|
|
10
|
+
flushSync(): void;
|
|
11
|
+
clear(): void;
|
|
12
|
+
size(): number;
|
|
13
|
+
}
|
|
14
|
+
export declare const clientCache: ClientCache;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare class LazyTranslationQueue {
|
|
2
|
+
private enabled;
|
|
3
|
+
private viewportObserver;
|
|
4
|
+
constructor();
|
|
5
|
+
setEnabled(enabled: boolean): void;
|
|
6
|
+
observe(element: HTMLElement, text: string, immediate?: boolean, isThink?: boolean): void;
|
|
7
|
+
private handleVisibleBatch;
|
|
8
|
+
private applyTranslation;
|
|
9
|
+
disconnect(): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const lazyQueue: LazyTranslationQueue;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-destructive translation DOM mount mechanism.
|
|
3
|
+
*
|
|
4
|
+
* Preserves the original React Fiber tree, event listeners, and nested DOM nodes
|
|
5
|
+
* by wrapping them in a hidden container (<span class="dsh-tidy-original-hidden">)
|
|
6
|
+
* rather than destroying them with `element.textContent = translated`.
|
|
7
|
+
*
|
|
8
|
+
* Displays the translated text in a companion container (<span class="dsh-tidy-translated-block">).
|
|
9
|
+
* Clicking the translation toggles between original and translated text in-place.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
|
|
12
|
+
export declare const CLASS_ORIGINAL_SHOWN = "dsh-tidy-original-shown";
|
|
13
|
+
export declare const CLASS_TRANSLATED_BLOCK = "dsh-tidy-translated-block";
|
|
14
|
+
export interface MountOptions {
|
|
15
|
+
originalText?: string;
|
|
16
|
+
isThink?: boolean;
|
|
17
|
+
interactive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class NonDestructiveTranslationMount {
|
|
20
|
+
/**
|
|
21
|
+
* Mounts a translated string onto the target element non-destructively.
|
|
22
|
+
*/
|
|
23
|
+
static mount(element: HTMLElement, translated: string, options?: MountOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Unmounts translation and restores original DOM nodes completely.
|
|
26
|
+
*/
|
|
27
|
+
static unmount(element: HTMLElement): void;
|
|
28
|
+
/**
|
|
29
|
+
* Checks if an element has non-destructive translation mounted.
|
|
30
|
+
*/
|
|
31
|
+
static isMounted(element: HTMLElement): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Gets the original text recorded on the element or contained in origWrapper.
|
|
34
|
+
*/
|
|
35
|
+
static getOriginal(element: HTMLElement): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Extracts text content excluding our own translation wrappers.
|
|
38
|
+
*/
|
|
39
|
+
static extractVisibleText(element: HTMLElement): string;
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ChatTranslateObserver {
|
|
2
|
+
private observer;
|
|
3
|
+
private rootElement;
|
|
4
|
+
private rootCheckTimer;
|
|
5
|
+
private isEnabled;
|
|
6
|
+
constructor();
|
|
7
|
+
setEnabled(enabled: boolean): void;
|
|
8
|
+
private restoreOriginals;
|
|
9
|
+
/**
|
|
10
|
+
* The active session's scroll container. Only the currently-viewed session
|
|
11
|
+
* is translated; switching sessions replaces this subtree and the observer
|
|
12
|
+
* naturally follows the new content.
|
|
13
|
+
*/
|
|
14
|
+
private isVisible;
|
|
15
|
+
private findRoot;
|
|
16
|
+
start(documentRef?: Document): () => void;
|
|
17
|
+
private scheduleRootCheck;
|
|
18
|
+
private restart;
|
|
19
|
+
private handleMutations;
|
|
20
|
+
private scanContainer;
|
|
21
|
+
private scanNode;
|
|
22
|
+
private processSpan;
|
|
23
|
+
disconnect(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare const chatTranslateObserver: ChatTranslateObserver;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamDebounceViewportObserver
|
|
3
|
+
*
|
|
4
|
+
* Combines:
|
|
5
|
+
* 1. True IntersectionObserver for viewport visibility detection (rootMargin: '150px 0px').
|
|
6
|
+
* 2. Typing/streaming debounce (default 400ms) to avoid translating partial streaming sentences.
|
|
7
|
+
* 3. Batch queuing to group multiple visible elements into efficient batch requests.
|
|
8
|
+
*/
|
|
9
|
+
export interface ViewportObserverOptions {
|
|
10
|
+
rootMargin?: string;
|
|
11
|
+
debounceMs?: number;
|
|
12
|
+
onVisibleBatch: (items: Array<{
|
|
13
|
+
element: HTMLElement;
|
|
14
|
+
text: string;
|
|
15
|
+
isThink?: boolean;
|
|
16
|
+
}>) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare class StreamDebounceViewportObserver {
|
|
19
|
+
private intersectionObserver;
|
|
20
|
+
private streamingTimers;
|
|
21
|
+
private pendingQueue;
|
|
22
|
+
private batchFlushTimer;
|
|
23
|
+
private options;
|
|
24
|
+
constructor(options: ViewportObserverOptions);
|
|
25
|
+
private initIntersectionObserver;
|
|
26
|
+
/**
|
|
27
|
+
* Observe an element with streaming debounce.
|
|
28
|
+
* If streaming updates characterData repeatedly within debounceMs, the timer resets.
|
|
29
|
+
*/
|
|
30
|
+
observeWithDebounce(element: HTMLElement, text: string, immediate?: boolean, isThink?: boolean): void;
|
|
31
|
+
private registerForViewport;
|
|
32
|
+
private enqueueBatch;
|
|
33
|
+
private flushQueue;
|
|
34
|
+
unobserve(element: HTMLElement): void;
|
|
35
|
+
disconnect(): void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Stable Cordis loader name. */
|
|
2
|
+
export declare const name = "dsh-chat-translate";
|
|
3
|
+
/** Inject webServer to serve the translation API */
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
interface HostContext {
|
|
6
|
+
webServer?: {
|
|
7
|
+
register(route: {
|
|
8
|
+
kind: 'prefix' | 'exact';
|
|
9
|
+
path: string;
|
|
10
|
+
handler: (req: any, res: any) => Promise<void> | void;
|
|
11
|
+
}): () => void;
|
|
12
|
+
};
|
|
13
|
+
effect(factory: () => void | (() => void), label: string): void;
|
|
14
|
+
get?(serviceName: string): any;
|
|
15
|
+
}
|
|
16
|
+
/** Mount the host half; provides translation proxy and settings persistence */
|
|
17
|
+
export declare function apply(ctx: HostContext): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { ITranslationAdapter, PluginConfig } from '../types.ts';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ITranslationAdapter, PluginConfig } from './base.ts';
|
|
2
|
+
interface BingTokens {
|
|
3
|
+
ig: string;
|
|
4
|
+
key: string;
|
|
5
|
+
token: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function fetchTokens(signal: AbortSignal, forceRefresh?: boolean): Promise<BingTokens>;
|
|
8
|
+
export declare class BingWebAdapter implements ITranslationAdapter {
|
|
9
|
+
readonly id = "bing";
|
|
10
|
+
readonly name = "\u5FAE\u8F6F Bing \u7F51\u9875\u7FFB\u8BD1 (\u514DKey\u76F4\u8FDE)";
|
|
11
|
+
isAvailable(_config: PluginConfig): boolean;
|
|
12
|
+
translate(text: string, signal: AbortSignal, config: PluginConfig): Promise<string>;
|
|
13
|
+
private executeTranslate;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ITranslationAdapter, PluginConfig } from './base.ts';
|
|
2
|
+
import type { CredentialsReader } from '../credentials.ts';
|
|
3
|
+
export declare class OpenAiCompatibleAdapter implements ITranslationAdapter {
|
|
4
|
+
readonly id = "openai";
|
|
5
|
+
readonly name = "OpenAI \u517C\u5BB9 (Chat Completions)";
|
|
6
|
+
private credentials;
|
|
7
|
+
constructor(credentials: CredentialsReader);
|
|
8
|
+
isAvailable(config: PluginConfig): boolean;
|
|
9
|
+
translate(text: string, signal: AbortSignal, config: PluginConfig): Promise<string>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class LruDiskCache {
|
|
2
|
+
private cache;
|
|
3
|
+
private maxEntries;
|
|
4
|
+
private filePath;
|
|
5
|
+
private saveTimer;
|
|
6
|
+
private dirty;
|
|
7
|
+
constructor(maxEntries?: number);
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
get(key: string): string | undefined;
|
|
10
|
+
set(key: string, value: string): void;
|
|
11
|
+
private scheduleSave;
|
|
12
|
+
flush(): Promise<void>;
|
|
13
|
+
dispose(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PluginConfig, MaskedPluginConfig } from './types.ts';
|
|
2
|
+
import { CredentialsReader, TRANSLATE_API_KEY_REF } from './credentials.ts';
|
|
3
|
+
/** Hard cap for the translation concurrency pool. */
|
|
4
|
+
export declare const MAX_CONCURRENCY = 100;
|
|
5
|
+
/** Bounds for the AI channel request timeout. */
|
|
6
|
+
export declare const AI_TIMEOUT_MIN = 500;
|
|
7
|
+
export declare const AI_TIMEOUT_MAX = 120000;
|
|
8
|
+
export declare class ConfigManager {
|
|
9
|
+
private config;
|
|
10
|
+
private configPath;
|
|
11
|
+
private credentials;
|
|
12
|
+
private listeners;
|
|
13
|
+
constructor(credentials?: CredentialsReader);
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
getConfig(): PluginConfig;
|
|
16
|
+
/** Whether the AI channel has every required piece: baseUrl, model and key. */
|
|
17
|
+
isAiConfigured(): boolean;
|
|
18
|
+
getMaskedConfig(): MaskedPluginConfig;
|
|
19
|
+
onConfigChange(listener: (config: PluginConfig) => void): () => void;
|
|
20
|
+
private notifyListeners;
|
|
21
|
+
updateConfig(partial: Partial<PluginConfig>): Promise<PluginConfig>;
|
|
22
|
+
private save;
|
|
23
|
+
}
|
|
24
|
+
export { TRANSLATE_API_KEY_REF };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads API keys from the shared DSH credentials file (~/.dsh/.credentials.yaml).
|
|
3
|
+
*
|
|
4
|
+
* The file is a tiny flat YAML document shaped like:
|
|
5
|
+
*
|
|
6
|
+
* version: 1
|
|
7
|
+
* refs:
|
|
8
|
+
* DEEPSEEK_API_KEY: sk-...
|
|
9
|
+
* TRANSLATE_API_KEY: sk-...
|
|
10
|
+
* records:
|
|
11
|
+
* ...
|
|
12
|
+
*
|
|
13
|
+
* We only need the `refs` section (flat `KEY: value` pairs), so instead of
|
|
14
|
+
* pulling in a YAML dependency we parse it with a small tolerant scanner.
|
|
15
|
+
*/
|
|
16
|
+
/** Refs key that holds the translation API key. */
|
|
17
|
+
export declare const TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
|
|
18
|
+
/**
|
|
19
|
+
* Parse the `refs` section of the credentials file into a flat map.
|
|
20
|
+
* Returns an empty map on any malformed input — callers treat a missing key
|
|
21
|
+
* as "not configured", never as an error.
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseRefs(yaml: string): Record<string, string>;
|
|
24
|
+
export declare class CredentialsReader {
|
|
25
|
+
private filePath;
|
|
26
|
+
private cachedKey;
|
|
27
|
+
private cachedAt;
|
|
28
|
+
constructor();
|
|
29
|
+
/** Read the file fresh on every call so a key added at runtime takes effect immediately. */
|
|
30
|
+
readRefs(): Record<string, string>;
|
|
31
|
+
getApiKey(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Write (or clear) the TRANSLATE_API_KEY ref, preserving every other line of
|
|
34
|
+
* the file (other refs, records section, comments). The file stays the single
|
|
35
|
+
* source of truth for credentials and keeps 0600 permissions. An empty key
|
|
36
|
+
* removes the ref entirely.
|
|
37
|
+
*/
|
|
38
|
+
setApiKey(apiKey: string): Promise<void>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TranslateItemResult } from './types.ts';
|
|
2
|
+
import type { ConfigManager } from './config.ts';
|
|
3
|
+
import type { LruDiskCache } from './cache.ts';
|
|
4
|
+
import type { CredentialsReader } from './credentials.ts';
|
|
5
|
+
export declare class TranslationDispatcher {
|
|
6
|
+
private configManager;
|
|
7
|
+
private cache;
|
|
8
|
+
private credentials;
|
|
9
|
+
private masking;
|
|
10
|
+
private adapters;
|
|
11
|
+
private circuitStates;
|
|
12
|
+
private inFlightMap;
|
|
13
|
+
private activeCount;
|
|
14
|
+
private queue;
|
|
15
|
+
constructor(configManager: ConfigManager, cache: LruDiskCache, credentials?: CredentialsReader);
|
|
16
|
+
private registerAdapter;
|
|
17
|
+
/**
|
|
18
|
+
* Decide which channels are active for the current config, in priority order.
|
|
19
|
+
*
|
|
20
|
+
* Truth table (user contract):
|
|
21
|
+
* - AI on + configured + Bing on -> [openai, bing] (AI first, Bing fallback)
|
|
22
|
+
* - AI on + NOT configured + Bing on -> [bing]
|
|
23
|
+
* - AI on + NOT configured + Bing off -> [] (no translation)
|
|
24
|
+
* - AI off + Bing on -> [bing]
|
|
25
|
+
* - AI off + Bing off -> [] (no translation)
|
|
26
|
+
*/
|
|
27
|
+
private computeChannels;
|
|
28
|
+
translateBatch(texts: string[], forceRefresh?: boolean): Promise<TranslateItemResult[]>;
|
|
29
|
+
translateOne(rawText: string, forceRefresh?: boolean): Promise<TranslateItemResult>;
|
|
30
|
+
testChannel(channelId: string): Promise<{
|
|
31
|
+
ok: boolean;
|
|
32
|
+
latencyMs: number;
|
|
33
|
+
error?: string;
|
|
34
|
+
}>;
|
|
35
|
+
private enqueueTask;
|
|
36
|
+
private processNext;
|
|
37
|
+
private isCircuitOpen;
|
|
38
|
+
private recordSuccess;
|
|
39
|
+
private recordFailure;
|
|
40
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { ConfigManager } from './config.ts';
|
|
3
|
+
import type { TranslationDispatcher } from './dispatcher.ts';
|
|
4
|
+
import type { CredentialsReader } from './credentials.ts';
|
|
5
|
+
export declare function createHttpHandler(configManager: ConfigManager, dispatcher: TranslationDispatcher, credentials?: CredentialsReader): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|