feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection

- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru
- Auto-detect system language via navigator.languages; fallback to English
- Add BCP47 locale mapping for date formatting and sorting
- Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each)
- Update language selector with native language names
- Extend locale key parity test to cover all 7 languages (70 assertions)
- Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features
- Add credential encryption, host metrics, session backup crypto, and zmodem bridge
- Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
Logan.Li committed 2026-08-12 00:07:48 +08:00
1 parent fea6d8db04
commit b0d0e3a5f3
301 files changed
+28002 -2705

No files matched your search

+15 -5
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { analyzeAgentOutput } from '../../src/utils/agentOutput'
import {
agentCommandAllowFingerprint,
agentCommandNeedsConfirmation,
assessAgentCommandRisk,
} from '../../src/utils/agentCommandRisk'
@@ -21,13 +22,22 @@ describe('Agent command guardrail', () => {
expect(assessAgentCommandRisk(command)).toBe(expected)
})
it('uses strict, balanced and permissive confirmation thresholds', () => {
it('uses strict and relaxed confirmation thresholds', () => {
expect(agentCommandNeedsConfirmation('safe', 'strict')).toBe(false)
expect(agentCommandNeedsConfirmation('medium', 'strict')).toBe(true)
expect(agentCommandNeedsConfirmation('medium', 'balanced')).toBe(false)
expect(agentCommandNeedsConfirmation('high', 'balanced')).toBe(true)
expect(agentCommandNeedsConfirmation('high', 'permissive')).toBe(false)
expect(agentCommandNeedsConfirmation('critical', 'permissive')).toBe(false)
expect(agentCommandNeedsConfirmation('medium', 'relaxed')).toBe(false)
expect(agentCommandNeedsConfirmation('high', 'relaxed')).toBe(true)
expect(agentCommandNeedsConfirmation('critical', 'relaxed')).toBe(false)
expect(agentCommandNeedsConfirmation('critical', 'strict')).toBe(false)
})
it('fingerprints similar temp-path commands for session Always', () => {
expect(agentCommandAllowFingerprint('rm /tmp/tmp.3RUZRHTwk2', 'high')).toBe(
agentCommandAllowFingerprint('rm /tmp/tmp.ABCDEFG123', 'high')
)
expect(agentCommandAllowFingerprint('rm /tmp/tmp.3RUZRHTwk2', 'high')).not.toBe(
agentCommandAllowFingerprint('rm -rf /var/log/nginx', 'high')
)
})
})
+66
View File
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest'
import { normalizeDistroId, parseOsPrettyName, parseOsReleaseOrUname, resolveSessionDistro } from '../../src/utils/hostDistro'
describe('hostDistro', () => {
it('normalizes common distro ids', () => {
expect(normalizeDistroId('ubuntu')).toBe('ubuntu')
expect(normalizeDistroId('deepin')).toBe('deepin')
expect(normalizeDistroId('uos')).toBe('deepin')
expect(normalizeDistroId('rocky')).toBe('rocky')
expect(normalizeDistroId('rhel')).toBe('redhat')
expect(normalizeDistroId('openEuler')).toBe('openeuler')
expect(normalizeDistroId('anolis')).toBe('anolis')
expect(normalizeDistroId('alinux')).toBe('alinux')
expect(normalizeDistroId('amzn2')).toBe('amazon')
expect(normalizeDistroId('Amazon Linux')).toBe('amazon')
expect(normalizeDistroId('Alibaba Cloud Linux')).toBe('alinux')
expect(normalizeDistroId('Linux')).toBe('linux')
})
it('parses cloud linux from os-release NAME when needed', () => {
expect(
parseOsReleaseOrUname(`NAME="Alibaba Cloud Linux"
ID=alinux
VERSION_ID="3"`)
).toBe('alinux')
expect(
parseOsReleaseOrUname(`NAME="Amazon Linux"
ID=amzn
VERSION_ID="2023"`)
).toBe('amazon')
})
it('parses os-release ID', () => {
const sample = `NAME="Ubuntu"
VERSION="22.04"
ID=ubuntu
ID_LIKE=debian
`
expect(parseOsReleaseOrUname(sample)).toBe('ubuntu')
})
it('maps TencentOS to centos icon family', () => {
expect(normalizeDistroId('tencentos')).toBe('centos')
expect(
parseOsReleaseOrUname(`NAME="TencentOS Server"
ID="tencentos"
VERSION_ID="4"`)
).toBe('centos')
})
it('parses PRETTY_NAME', () => {
expect(
parseOsPrettyName(`NAME="Ubuntu"
PRETTY_NAME="Ubuntu 24.04.4 LTS"
ID=ubuntu`)
).toBe('Ubuntu 24.04.4 LTS')
})
it('resolves protocol defaults for desktop sessions', () => {
expect(resolveSessionDistro({ protocol: 'rdp' })).toBe('windows')
expect(resolveSessionDistro({ protocol: 'vnc' })).toBe('vnc')
expect(resolveSessionDistro({ protocol: 'ssh', distro: 'debian' })).toBe('debian')
expect(resolveSessionDistro({ protocol: 'ssh' })).toBe('linux')
expect(resolveSessionDistro({})).toBe('linux')
})
})
+1 -1
View File
@@ -49,6 +49,6 @@ describe('platformShells', () => {
it('uses CRLF enter for windows shells', () => {
expect(localShellEnterKey('cmd')).toBe('\r\n')
expect(localShellEnterKey('pwsh')).toBe('\r\n')
expect(localShellEnterKey('bash')).toBe('\n')
expect(localShellEnterKey('bash')).toBe('\r')
})
})
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import {
decryptSessionBackup,
encryptSessionBackup,
isEncryptedSessionBackup,
} from '../../src/utils/sessionBackupCrypto'
describe('sessionBackupCrypto', () => {
it('encrypts and decrypts UTF-8 session payload', async () => {
const plaintext = JSON.stringify({
version: 1,
sessions: [{ name: '生产', host: 'ops.example.com', privateKey: '-----BEGIN KEY-----\n密\n' }],
})
const encrypted = await encryptSessionBackup(plaintext, 's3cret!')
const envelope = JSON.parse(encrypted)
expect(isEncryptedSessionBackup(envelope)).toBe(true)
expect(encrypted).not.toContain('ops.example.com')
expect(encrypted).not.toContain('BEGIN KEY')
const restored = await decryptSessionBackup(envelope, 's3cret!')
expect(restored).toBe(plaintext)
})
it('rejects wrong password', async () => {
const encrypted = await encryptSessionBackup('{"sessions":[]}', 'right-password')
const envelope = JSON.parse(encrypted)
await expect(decryptSessionBackup(envelope, 'wrong-password')).rejects.toThrow(
'BACKUP_PASSWORD_INVALID'
)
})
})
+88
View File
@@ -1,6 +1,11 @@
import { describe, it, expect } from 'vitest'
import { expandSnippetVariables } from '../../src/utils/snippetVariables'
import { applyKeywordHighlights, DEFAULT_KEYWORD_HIGHLIGHT_RULES } from '../../src/utils/keywordHighlight'
import {
StreamingKeywordHighlight,
trailingIncompleteEscape,
} from '../../src/utils/streamingKeywordHighlight'
import { localShellEnterKey, normalizeTerminalInject } from '../../src/utils/platformShells'
describe('expandSnippetVariables', () => {
it('replaces known placeholders', () => {
@@ -35,4 +40,87 @@ describe('applyKeywordHighlights', () => {
const text = 'no highlight'
expect(applyKeywordHighlights(text, rules)).toBe(text)
})
it('does not inject SGR inside OSC window-title sequences (SSH title leak)', () => {
const rules = [
{
id: 'ipv4',
label: 'IPv4',
pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b',
isRegex: true,
color: '#8be9fd',
enabled: true,
},
]
const osc = '\x1b]0;ssh root@165.154.155.48\x07'
const out = applyKeywordHighlights(`${osc}Welcome to Ubuntu`, rules)
expect(out).toContain(osc)
expect(out).not.toMatch(/\x1b\]0;.*\x1b\[38/)
expect(out).toContain('Welcome to Ubuntu')
})
it('still highlights IPs in plain text when the ipv4 rule is enabled', () => {
const rules = [
{
id: 'ipv4',
label: 'IPv4',
pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b',
isRegex: true,
color: '#8be9fd',
enabled: true,
},
]
const out = applyKeywordHighlights('peer 10.0.0.1 online', rules)
expect(out).toContain('\x1b[38;2;')
expect(out).toContain('10.0.0.1')
expect(out).toContain('\x1b[39m')
})
it('leaves existing CSI color sequences intact', () => {
const colored = '\x1b[01;32mroot\x1b[00m@host'
const out = applyKeywordHighlights(colored, DEFAULT_KEYWORD_HIGHLIGHT_RULES)
expect(out).toContain('\x1b[01;32mroot\x1b[00m')
})
})
describe('StreamingKeywordHighlight', () => {
const ipv4Rules = [
{
id: 'ipv4',
label: 'IPv4',
pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b',
isRegex: true,
color: '#8be9fd',
enabled: true,
},
]
it('holds incomplete OSC across chunks', () => {
expect(trailingIncompleteEscape('\x1b]0;ssh root@1.2.3.4')).toBe('\x1b]0;ssh root@1.2.3.4')
const stream = new StreamingKeywordHighlight()
expect(stream.push('\x1b]0;ssh root@1.2.3.4', ipv4Rules)).toBe('')
const done = stream.push('\x07Welcome', ipv4Rules)
expect(done).toContain('\x1b]0;ssh root@1.2.3.4\x07')
expect(done).toContain('Welcome')
expect(done).not.toMatch(/\x1b\]0;.*\x1b\[38/)
})
it('holds incomplete CSI across chunks', () => {
const stream = new StreamingKeywordHighlight()
expect(stream.push('\x1b[38;2;139', ipv4Rules)).toBe('')
const out = stream.push(';233;253m10.0.0.1 done', ipv4Rules)
expect(out.startsWith('\x1b[38;2;139;233;253m')).toBe(true)
})
})
describe('normalizeTerminalInject', () => {
it('converts LF to CR like xterm paste', () => {
expect(normalizeTerminalInject('echo hi\n', '')).toBe('echo hi\r')
expect(normalizeTerminalInject('echo hi', '\r')).toBe('echo hi\r')
})
it('uses CR enter for unix local shells', () => {
expect(localShellEnterKey('zsh')).toBe('\r')
expect(localShellEnterKey('pwsh')).toBe('\r\n')
})
})
+3 -3
View File
@@ -27,10 +27,10 @@ describe('terminal pane input routing', () => {
expect(resolveTerminalInputTargets(panes, 'missing', false)).toEqual([])
})
it('never broadcasts input originating from a local terminal', () => {
expect(shouldBroadcastTerminalInput(true, true, 'local')).toBe(false)
expect(shouldBroadcastTerminalInput(true, true, 'ssh-connection')).toBe(false)
it('broadcasts from both SSH and local terminals when enabled', () => {
expect(shouldBroadcastTerminalInput(true, true, 'local')).toBe(true)
expect(shouldBroadcastTerminalInput(true, false, 'ssh-connection')).toBe(true)
expect(shouldBroadcastTerminalInput(true, false, null)).toBe(false)
expect(shouldBroadcastTerminalInput(false, false, 'ssh-connection')).toBe(false)
})
})
+15
View File
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { decodeBase64ToText, encodeTextToBase64 } from '../../src/utils/textBase64'
describe('textBase64', () => {
it('round-trips UTF-8 including CJK', () => {
const sample = '{"name":"运维主机","host":"ops.example.com"}'
expect(decodeBase64ToText(encodeTextToBase64(sample))).toBe(sample)
})
it('produces standard base64 without raw JSON', () => {
const encoded = encodeTextToBase64('{"version":1}')
expect(encoded).not.toContain('{')
expect(encoded).toMatch(/^[A-Za-z0-9+/=]+$/)
})
})
+24
View File
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { parseSzFiles } from '../../src/utils/zmodemSftpBridge'
describe('parseSzFiles', () => {
it('parses simple sz command', () => {
expect(parseSzFiles('sz app.log\r')).toEqual(['app.log'])
})
it('parses multiple files and skips flags', () => {
expect(parseSzFiles('sz -y a.txt b.bin\n')).toEqual(['a.txt', 'b.bin'])
})
it('uses the latest command line', () => {
expect(parseSzFiles('sz old\rsz newer.tar.gz\r')).toEqual(['newer.tar.gz'])
})
it('returns empty when no sz command', () => {
expect(parseSzFiles('ls\rrz\r')).toEqual([])
})
it('strips bracketed-paste markers', () => {
expect(parseSzFiles('\x1b[200~sz report.txt\x1b[201~\r')).toEqual(['report.txt'])
})
})