codeharbor 0.1.22 → 0.1.23

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.
Files changed (2) hide show
  1. package/dist/cli.js +454 -130
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -42,11 +42,11 @@ var import_node_util2 = require("util");
42
42
 
43
43
  // src/admin-console-html.ts
44
44
  var ADMIN_CONSOLE_HTML = `<!doctype html>
45
- <html lang="en">
45
+ <html lang="zh-CN">
46
46
  <head>
47
47
  <meta charset="utf-8" />
48
48
  <meta name="viewport" content="width=device-width, initial-scale=1" />
49
- <title>CodeHarbor Admin Console</title>
49
+ <title>CodeHarbor \u7BA1\u7406\u540E\u53F0 / Admin Console</title>
50
50
  <style>
51
51
  :root {
52
52
  --bg-start: #0f172a;
@@ -129,13 +129,15 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
129
129
  color: var(--muted);
130
130
  }
131
131
  input,
132
+ select,
132
133
  button,
133
134
  textarea {
134
135
  font: inherit;
135
136
  }
136
137
  input[type="text"],
137
138
  input[type="password"],
138
- input[type="number"] {
139
+ input[type="number"],
140
+ select {
139
141
  border: 1px solid var(--panel-border);
140
142
  background: #0f172acc;
141
143
  color: var(--text);
@@ -261,181 +263,188 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
261
263
  <body>
262
264
  <main class="shell">
263
265
  <section class="header">
264
- <h1 class="title">CodeHarbor Admin Console</h1>
265
- <p class="subtitle">Manage global settings, room policies, health checks, and config audit records.</p>
266
+ <h1 class="title" data-i18n="header.title">CodeHarbor \u7BA1\u7406\u540E\u53F0</h1>
267
+ <p class="subtitle" data-i18n="header.subtitle">\u7BA1\u7406\u5168\u5C40\u914D\u7F6E\u3001\u623F\u95F4\u7B56\u7565\u3001\u5065\u5EB7\u68C0\u67E5\u4E0E\u914D\u7F6E\u5BA1\u8BA1\u8BB0\u5F55\u3002</p>
266
268
  <nav class="tabs">
267
- <a class="tab" data-page="settings-global" href="#/settings/global">Global</a>
268
- <a class="tab" data-page="settings-rooms" href="#/settings/rooms">Rooms</a>
269
- <a class="tab" data-page="health" href="#/health">Health</a>
270
- <a class="tab" data-page="audit" href="#/audit">Audit</a>
269
+ <a class="tab" data-page="settings-global" href="#/settings/global" data-i18n="tab.global">\u5168\u5C40</a>
270
+ <a class="tab" data-page="settings-rooms" href="#/settings/rooms" data-i18n="tab.rooms">\u623F\u95F4</a>
271
+ <a class="tab" data-page="health" href="#/health" data-i18n="tab.health">\u5065\u5EB7</a>
272
+ <a class="tab" data-page="audit" href="#/audit" data-i18n="tab.audit">\u5BA1\u8BA1</a>
271
273
  </nav>
272
274
  <div class="auth-row">
273
275
  <label class="field">
274
- <span class="field-label">Admin Token (optional)</span>
275
- <input id="auth-token" type="password" placeholder="ADMIN_TOKEN" />
276
+ <span class="field-label" data-i18n="auth.token.label">\u7BA1\u7406\u5458\u4EE4\u724C\uFF08\u53EF\u9009\uFF09</span>
277
+ <input id="auth-token" type="password" placeholder="ADMIN_TOKEN" data-i18n-placeholder="auth.token.placeholder" />
276
278
  </label>
277
279
  <label class="field">
278
- <span class="field-label">Actor (for audit logs)</span>
279
- <input id="auth-actor" type="text" placeholder="your-name" />
280
+ <span class="field-label" data-i18n="auth.actor.label">\u64CD\u4F5C\u8005\uFF08\u7528\u4E8E\u5BA1\u8BA1\u65E5\u5FD7\uFF09</span>
281
+ <input id="auth-actor" type="text" placeholder="\u4F60\u7684\u540D\u5B57" data-i18n-placeholder="auth.actor.placeholder" />
280
282
  </label>
281
- <button id="auth-save-btn" type="button" class="secondary">Save Auth</button>
282
- <button id="auth-clear-btn" type="button" class="secondary">Clear Auth</button>
283
+ <label class="field">
284
+ <span class="field-label" data-i18n="auth.language.label">\u754C\u9762\u8BED\u8A00</span>
285
+ <select id="lang-select">
286
+ <option value="zh">\u4E2D\u6587</option>
287
+ <option value="en">English</option>
288
+ </select>
289
+ </label>
290
+ <button id="auth-save-btn" type="button" class="secondary" data-i18n="auth.save">\u4FDD\u5B58\u8BA4\u8BC1</button>
291
+ <button id="auth-clear-btn" type="button" class="secondary" data-i18n="auth.clear">\u6E05\u9664\u8BA4\u8BC1</button>
283
292
  </div>
284
- <div id="notice" class="notice">Ready.</div>
285
- <p id="auth-role" class="muted">Permission: unknown</p>
293
+ <div id="notice" class="notice" data-i18n="notice.ready">\u5C31\u7EEA\u3002</div>
294
+ <p id="auth-role" class="muted" data-i18n="auth.permission.unknown">\u6743\u9650\uFF1A\u672A\u77E5</p>
286
295
  </section>
287
296
 
288
297
  <section class="panel" data-view="settings-global">
289
- <h2 class="panel-title">Global Config</h2>
298
+ <h2 class="panel-title" data-i18n="global.title">\u5168\u5C40\u914D\u7F6E</h2>
290
299
  <div class="grid">
291
300
  <label class="field">
292
- <span class="field-label">Command Prefix</span>
301
+ <span class="field-label" data-i18n="global.commandPrefix">\u547D\u4EE4\u524D\u7F00</span>
293
302
  <input id="global-matrix-prefix" type="text" />
294
303
  </label>
295
304
  <label class="field">
296
- <span class="field-label">Default Workdir</span>
305
+ <span class="field-label" data-i18n="global.defaultWorkdir">\u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55</span>
297
306
  <input id="global-workdir" type="text" />
298
307
  </label>
299
308
  <label class="field">
300
- <span class="field-label">Progress Interval (ms)</span>
309
+ <span class="field-label" data-i18n="global.progressInterval">\u8FDB\u5EA6\u66F4\u65B0\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09</span>
301
310
  <input id="global-progress-interval" type="number" min="1" />
302
311
  </label>
303
312
  <label class="field">
304
- <span class="field-label">Typing Timeout (ms)</span>
313
+ <span class="field-label" data-i18n="global.typingTimeout">\u8F93\u5165\u72B6\u6001\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09</span>
305
314
  <input id="global-typing-timeout" type="number" min="1" />
306
315
  </label>
307
316
  <label class="field">
308
- <span class="field-label">Session Active Window (minutes)</span>
317
+ <span class="field-label" data-i18n="global.sessionWindow">\u4F1A\u8BDD\u6D3B\u8DC3\u7A97\u53E3\uFF08\u5206\u949F\uFF09</span>
309
318
  <input id="global-active-window" type="number" min="1" />
310
319
  </label>
311
320
  <label class="checkbox">
312
321
  <input id="global-progress-enabled" type="checkbox" />
313
- <span>Enable progress updates</span>
322
+ <span data-i18n="global.progressEnabled">\u542F\u7528\u8FDB\u5EA6\u66F4\u65B0</span>
314
323
  </label>
315
324
 
316
325
  <label class="field">
317
- <span class="field-label">Rate Window (ms)</span>
326
+ <span class="field-label" data-i18n="global.rateWindow">\u9650\u6D41\u7A97\u53E3\uFF08\u6BEB\u79D2\uFF09</span>
318
327
  <input id="global-rate-window" type="number" min="1" />
319
328
  </label>
320
329
  <label class="field">
321
- <span class="field-label">Rate Max Requests / User</span>
330
+ <span class="field-label" data-i18n="global.rateUser">\u5355\u7528\u6237\u7A97\u53E3\u6700\u5927\u8BF7\u6C42\u6570</span>
322
331
  <input id="global-rate-user" type="number" min="0" />
323
332
  </label>
324
333
  <label class="field">
325
- <span class="field-label">Rate Max Requests / Room</span>
334
+ <span class="field-label" data-i18n="global.rateRoom">\u5355\u623F\u95F4\u7A97\u53E3\u6700\u5927\u8BF7\u6C42\u6570</span>
326
335
  <input id="global-rate-room" type="number" min="0" />
327
336
  </label>
328
337
  <label class="field">
329
- <span class="field-label">Max Concurrent Global</span>
338
+ <span class="field-label" data-i18n="global.concurrentGlobal">\u5168\u5C40\u6700\u5927\u5E76\u53D1</span>
330
339
  <input id="global-concurrency-global" type="number" min="0" />
331
340
  </label>
332
341
  <label class="field">
333
- <span class="field-label">Max Concurrent / User</span>
342
+ <span class="field-label" data-i18n="global.concurrentUser">\u5355\u7528\u6237\u6700\u5927\u5E76\u53D1</span>
334
343
  <input id="global-concurrency-user" type="number" min="0" />
335
344
  </label>
336
345
  <label class="field">
337
- <span class="field-label">Max Concurrent / Room</span>
346
+ <span class="field-label" data-i18n="global.concurrentRoom">\u5355\u623F\u95F4\u6700\u5927\u5E76\u53D1</span>
338
347
  <input id="global-concurrency-room" type="number" min="0" />
339
348
  </label>
340
349
 
341
- <label class="checkbox"><input id="global-direct-mode" type="checkbox" /><span>Group direct mode (no trigger required)</span></label>
342
- <label class="checkbox"><input id="global-trigger-mention" type="checkbox" /><span>Trigger: mention</span></label>
343
- <label class="checkbox"><input id="global-trigger-reply" type="checkbox" /><span>Trigger: reply</span></label>
344
- <label class="checkbox"><input id="global-trigger-window" type="checkbox" /><span>Trigger: active window</span></label>
345
- <label class="checkbox"><input id="global-trigger-prefix" type="checkbox" /><span>Trigger: prefix</span></label>
350
+ <label class="checkbox"><input id="global-direct-mode" type="checkbox" /><span data-i18n="global.groupDirect">\u7FA4\u804A\u76F4\u901A\u6A21\u5F0F\uFF08\u65E0\u9700\u89E6\u53D1\uFF09</span></label>
351
+ <label class="checkbox"><input id="global-trigger-mention" type="checkbox" /><span data-i18n="global.triggerMention">\u89E6\u53D1\uFF1A\u63D0\u53CA\u673A\u5668\u4EBA</span></label>
352
+ <label class="checkbox"><input id="global-trigger-reply" type="checkbox" /><span data-i18n="global.triggerReply">\u89E6\u53D1\uFF1A\u56DE\u590D\u673A\u5668\u4EBA</span></label>
353
+ <label class="checkbox"><input id="global-trigger-window" type="checkbox" /><span data-i18n="global.triggerWindow">\u89E6\u53D1\uFF1A\u6D3B\u8DC3\u7A97\u53E3</span></label>
354
+ <label class="checkbox"><input id="global-trigger-prefix" type="checkbox" /><span data-i18n="global.triggerPrefix">\u89E6\u53D1\uFF1A\u547D\u4EE4\u524D\u7F00</span></label>
346
355
 
347
- <label class="checkbox"><input id="global-cli-enabled" type="checkbox" /><span>CLI compat mode</span></label>
348
- <label class="checkbox"><input id="global-cli-pass" type="checkbox" /><span>CLI passthrough events</span></label>
349
- <label class="checkbox"><input id="global-cli-whitespace" type="checkbox" /><span>Preserve whitespace</span></label>
350
- <label class="checkbox"><input id="global-cli-disable-split" type="checkbox" /><span>Disable reply split</span></label>
356
+ <label class="checkbox"><input id="global-cli-enabled" type="checkbox" /><span data-i18n="global.cliEnabled">CLI \u517C\u5BB9\u6A21\u5F0F</span></label>
357
+ <label class="checkbox"><input id="global-cli-pass" type="checkbox" /><span data-i18n="global.cliPass">CLI \u900F\u4F20\u4E8B\u4EF6</span></label>
358
+ <label class="checkbox"><input id="global-cli-whitespace" type="checkbox" /><span data-i18n="global.cliWhitespace">\u4FDD\u7559\u7A7A\u767D\u7B26</span></label>
359
+ <label class="checkbox"><input id="global-cli-disable-split" type="checkbox" /><span data-i18n="global.cliDisableSplit">\u7981\u7528\u56DE\u590D\u5206\u7247</span></label>
351
360
  <label class="field">
352
- <span class="field-label">CLI progress throttle (ms)</span>
361
+ <span class="field-label" data-i18n="global.cliThrottle">CLI \u8FDB\u5EA6\u8282\u6D41\uFF08\u6BEB\u79D2\uFF09</span>
353
362
  <input id="global-cli-throttle" type="number" min="0" />
354
363
  </label>
355
- <label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span>Fetch media attachments</span></label>
356
- <label class="checkbox"><input id="global-cli-transcribe-audio" type="checkbox" /><span>Transcribe audio attachments</span></label>
364
+ <label class="checkbox"><input id="global-cli-fetch-media" type="checkbox" /><span data-i18n="global.cliFetchMedia">\u4E0B\u8F7D\u5A92\u4F53\u9644\u4EF6</span></label>
365
+ <label class="checkbox"><input id="global-cli-transcribe-audio" type="checkbox" /><span data-i18n="global.cliTranscribeAudio">\u8F6C\u5199\u97F3\u9891\u9644\u4EF6</span></label>
357
366
  <label class="field">
358
- <span class="field-label">Audio transcribe model</span>
367
+ <span class="field-label" data-i18n="global.audioModel">\u97F3\u9891\u8F6C\u5199\u6A21\u578B</span>
359
368
  <input id="global-cli-audio-model" type="text" />
360
369
  </label>
361
370
  <label class="field">
362
- <span class="field-label">Audio transcribe timeout (ms)</span>
371
+ <span class="field-label" data-i18n="global.audioTimeout">\u97F3\u9891\u8F6C\u5199\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09</span>
363
372
  <input id="global-cli-audio-timeout" type="number" min="1" />
364
373
  </label>
365
374
  <label class="field">
366
- <span class="field-label">Audio transcript max chars</span>
375
+ <span class="field-label" data-i18n="global.audioMaxChars">\u97F3\u9891\u8F6C\u5199\u6700\u5927\u5B57\u7B26\u6570</span>
367
376
  <input id="global-cli-audio-max-chars" type="number" min="1" />
368
377
  </label>
369
378
  <label class="field">
370
- <span class="field-label">Audio transcribe max retries</span>
379
+ <span class="field-label" data-i18n="global.audioMaxRetries">\u97F3\u9891\u8F6C\u5199\u6700\u5927\u91CD\u8BD5\u6B21\u6570</span>
371
380
  <input id="global-cli-audio-max-retries" type="number" min="0" max="10" />
372
381
  </label>
373
382
  <label class="field">
374
- <span class="field-label">Audio transcribe retry delay (ms)</span>
383
+ <span class="field-label" data-i18n="global.audioRetryDelay">\u97F3\u9891\u8F6C\u5199\u91CD\u8BD5\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09</span>
375
384
  <input id="global-cli-audio-retry-delay" type="number" min="0" />
376
385
  </label>
377
386
  <label class="field">
378
- <span class="field-label">Audio max bytes</span>
387
+ <span class="field-label" data-i18n="global.audioMaxBytes">\u97F3\u9891\u6700\u5927\u5B57\u8282\u6570</span>
379
388
  <input id="global-cli-audio-max-bytes" type="number" min="1" />
380
389
  </label>
381
390
  <label class="field">
382
- <span class="field-label">Local whisper command</span>
383
- <input id="global-cli-audio-local-command" type="text" placeholder='python3 /opt/whisper/transcribe.py --input {input}' />
391
+ <span class="field-label" data-i18n="global.audioLocalCommand">\u672C\u5730 Whisper \u547D\u4EE4</span>
392
+ <input id="global-cli-audio-local-command" type="text" placeholder='python3 /opt/whisper/transcribe.py --input {input}' data-i18n-placeholder="global.audioLocalCommandPlaceholder" />
384
393
  </label>
385
394
  <label class="field">
386
- <span class="field-label">Local whisper timeout (ms)</span>
395
+ <span class="field-label" data-i18n="global.audioLocalTimeout">\u672C\u5730 Whisper \u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09</span>
387
396
  <input id="global-cli-audio-local-timeout" type="number" min="1" />
388
397
  </label>
389
- <label class="checkbox"><input id="global-agent-enabled" type="checkbox" /><span>Enable multi-agent workflow</span></label>
398
+ <label class="checkbox"><input id="global-agent-enabled" type="checkbox" /><span data-i18n="global.agentEnabled">\u542F\u7528\u591A\u667A\u80FD\u4F53\u5DE5\u4F5C\u6D41</span></label>
390
399
  <label class="field">
391
- <span class="field-label">Workflow auto-repair rounds</span>
400
+ <span class="field-label" data-i18n="global.agentRounds">\u5DE5\u4F5C\u6D41\u81EA\u52A8\u4FEE\u590D\u8F6E\u6B21</span>
392
401
  <input id="global-agent-repair-rounds" type="number" min="0" max="10" />
393
402
  </label>
394
403
  </div>
395
404
  <div class="actions">
396
- <button id="global-save-btn" type="button">Save Global Config</button>
397
- <button id="global-reload-btn" type="button" class="secondary">Reload</button>
398
- <button id="global-restart-main-btn" type="button" class="secondary">Restart Main Service</button>
399
- <button id="global-restart-all-btn" type="button" class="secondary">Restart Main + Admin</button>
405
+ <button id="global-save-btn" type="button" data-i18n="global.save">\u4FDD\u5B58\u5168\u5C40\u914D\u7F6E</button>
406
+ <button id="global-reload-btn" type="button" class="secondary" data-i18n="global.reload">\u91CD\u65B0\u52A0\u8F7D</button>
407
+ <button id="global-restart-main-btn" type="button" class="secondary" data-i18n="global.restartMain">\u91CD\u542F\u4E3B\u670D\u52A1</button>
408
+ <button id="global-restart-all-btn" type="button" class="secondary" data-i18n="global.restartAll">\u91CD\u542F\u4E3B\u670D\u52A1+\u7BA1\u7406\u540E\u53F0</button>
400
409
  </div>
401
- <p class="muted">Saving global config updates .env and requires restart to fully take effect.</p>
410
+ <p class="muted" data-i18n="global.restartHint">\u4FDD\u5B58\u5168\u5C40\u914D\u7F6E\u4F1A\u66F4\u65B0 .env\uFF0C\u5E76\u9700\u8981\u91CD\u542F\u540E\u5B8C\u5168\u751F\u6548\u3002</p>
402
411
  </section>
403
412
 
404
413
  <section class="panel" data-view="settings-rooms" hidden>
405
- <h2 class="panel-title">Room Config</h2>
414
+ <h2 class="panel-title" data-i18n="rooms.title">\u623F\u95F4\u914D\u7F6E</h2>
406
415
  <div class="grid">
407
416
  <label class="field">
408
- <span class="field-label">Room ID</span>
409
- <input id="room-id" type="text" placeholder="!room:example.com" />
417
+ <span class="field-label" data-i18n="rooms.roomId">\u623F\u95F4 ID</span>
418
+ <input id="room-id" type="text" placeholder="!room:example.com" data-i18n-placeholder="rooms.roomIdPlaceholder" />
410
419
  </label>
411
420
  <label class="field">
412
- <span class="field-label">Audit Summary (optional)</span>
413
- <input id="room-summary" type="text" placeholder="bind room to project A" />
421
+ <span class="field-label" data-i18n="rooms.summary">\u5BA1\u8BA1\u6458\u8981\uFF08\u53EF\u9009\uFF09</span>
422
+ <input id="room-summary" type="text" placeholder="\u7ED1\u5B9A\u623F\u95F4\u5230\u9879\u76EE A" data-i18n-placeholder="rooms.summaryPlaceholder" />
414
423
  </label>
415
424
  <label class="field full">
416
- <span class="field-label">Workdir</span>
425
+ <span class="field-label" data-i18n="rooms.workdir">\u5DE5\u4F5C\u76EE\u5F55</span>
417
426
  <input id="room-workdir" type="text" />
418
427
  </label>
419
- <label class="checkbox"><input id="room-enabled" type="checkbox" /><span>Enabled</span></label>
420
- <label class="checkbox"><input id="room-mention" type="checkbox" /><span>Allow mention trigger</span></label>
421
- <label class="checkbox"><input id="room-reply" type="checkbox" /><span>Allow reply trigger</span></label>
422
- <label class="checkbox"><input id="room-window" type="checkbox" /><span>Allow active-window trigger</span></label>
423
- <label class="checkbox"><input id="room-prefix" type="checkbox" /><span>Allow prefix trigger</span></label>
428
+ <label class="checkbox"><input id="room-enabled" type="checkbox" /><span data-i18n="rooms.enabled">\u542F\u7528</span></label>
429
+ <label class="checkbox"><input id="room-mention" type="checkbox" /><span data-i18n="rooms.allowMention">\u5141\u8BB8\u63D0\u53CA\u89E6\u53D1</span></label>
430
+ <label class="checkbox"><input id="room-reply" type="checkbox" /><span data-i18n="rooms.allowReply">\u5141\u8BB8\u56DE\u590D\u89E6\u53D1</span></label>
431
+ <label class="checkbox"><input id="room-window" type="checkbox" /><span data-i18n="rooms.allowWindow">\u5141\u8BB8\u6D3B\u8DC3\u7A97\u53E3\u89E6\u53D1</span></label>
432
+ <label class="checkbox"><input id="room-prefix" type="checkbox" /><span data-i18n="rooms.allowPrefix">\u5141\u8BB8\u524D\u7F00\u89E6\u53D1</span></label>
424
433
  </div>
425
434
  <div class="actions">
426
- <button id="room-load-btn" type="button" class="secondary">Load Room</button>
427
- <button id="room-save-btn" type="button">Save Room</button>
428
- <button id="room-delete-btn" type="button" class="danger">Delete Room</button>
429
- <button id="room-refresh-btn" type="button" class="secondary">Refresh List</button>
435
+ <button id="room-load-btn" type="button" class="secondary" data-i18n="rooms.load">\u52A0\u8F7D\u623F\u95F4</button>
436
+ <button id="room-save-btn" type="button" data-i18n="rooms.save">\u4FDD\u5B58\u623F\u95F4</button>
437
+ <button id="room-delete-btn" type="button" class="danger" data-i18n="rooms.delete">\u5220\u9664\u623F\u95F4</button>
438
+ <button id="room-refresh-btn" type="button" class="secondary" data-i18n="rooms.refresh">\u5237\u65B0\u5217\u8868</button>
430
439
  </div>
431
440
  <div class="table-wrap">
432
441
  <table>
433
442
  <thead>
434
443
  <tr>
435
- <th>Room ID</th>
436
- <th>Enabled</th>
437
- <th>Workdir</th>
438
- <th>Updated At</th>
444
+ <th data-i18n="rooms.table.roomId">\u623F\u95F4 ID</th>
445
+ <th data-i18n="rooms.table.enabled">\u542F\u7528</th>
446
+ <th data-i18n="rooms.table.workdir">\u5DE5\u4F5C\u76EE\u5F55</th>
447
+ <th data-i18n="rooms.table.updatedAt">\u66F4\u65B0\u65F6\u95F4</th>
439
448
  </tr>
440
449
  </thead>
441
450
  <tbody id="room-list-body"></tbody>
@@ -444,17 +453,17 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
444
453
  </section>
445
454
 
446
455
  <section class="panel" data-view="health" hidden>
447
- <h2 class="panel-title">Health Check</h2>
456
+ <h2 class="panel-title" data-i18n="health.title">\u5065\u5EB7\u68C0\u67E5</h2>
448
457
  <div class="actions">
449
- <button id="health-refresh-btn" type="button">Run Health Check</button>
458
+ <button id="health-refresh-btn" type="button" data-i18n="health.run">\u6267\u884C\u5065\u5EB7\u68C0\u67E5</button>
450
459
  </div>
451
460
  <div class="table-wrap">
452
461
  <table>
453
462
  <thead>
454
463
  <tr>
455
- <th>Component</th>
456
- <th>Status</th>
457
- <th>Details</th>
464
+ <th data-i18n="health.table.component">\u7EC4\u4EF6</th>
465
+ <th data-i18n="health.table.status">\u72B6\u6001</th>
466
+ <th data-i18n="health.table.details">\u8BE6\u60C5</th>
458
467
  </tr>
459
468
  </thead>
460
469
  <tbody id="health-body"></tbody>
@@ -463,23 +472,23 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
463
472
  </section>
464
473
 
465
474
  <section class="panel" data-view="audit" hidden>
466
- <h2 class="panel-title">Config Audit</h2>
475
+ <h2 class="panel-title" data-i18n="audit.title">\u914D\u7F6E\u5BA1\u8BA1</h2>
467
476
  <div class="actions">
468
477
  <label class="field" style="max-width: 120px;">
469
- <span class="field-label">Limit</span>
478
+ <span class="field-label" data-i18n="audit.limit">\u6761\u6570</span>
470
479
  <input id="audit-limit" type="number" min="1" max="200" value="30" />
471
480
  </label>
472
- <button id="audit-refresh-btn" type="button">Refresh Audit</button>
481
+ <button id="audit-refresh-btn" type="button" data-i18n="audit.refresh">\u5237\u65B0\u5BA1\u8BA1</button>
473
482
  </div>
474
483
  <div class="table-wrap">
475
484
  <table>
476
485
  <thead>
477
486
  <tr>
478
- <th>ID</th>
479
- <th>Time</th>
480
- <th>Actor</th>
481
- <th>Summary</th>
482
- <th>Payload</th>
487
+ <th data-i18n="audit.table.id">ID</th>
488
+ <th data-i18n="audit.table.time">\u65F6\u95F4</th>
489
+ <th data-i18n="audit.table.actor">\u64CD\u4F5C\u8005</th>
490
+ <th data-i18n="audit.table.summary">\u6458\u8981</th>
491
+ <th data-i18n="audit.table.payload">\u8F7D\u8377</th>
483
492
  </tr>
484
493
  </thead>
485
494
  <tbody id="audit-body"></tbody>
@@ -506,6 +515,264 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
506
515
  };
507
516
  var storageTokenKey = "codeharbor.admin.token";
508
517
  var storageActorKey = "codeharbor.admin.actor";
518
+ var storageLangKey = "codeharbor.admin.lang";
519
+ var defaultLang = "zh";
520
+ var i18n = {
521
+ zh: {
522
+ "header.title": "CodeHarbor \u7BA1\u7406\u540E\u53F0",
523
+ "header.subtitle": "\u7BA1\u7406\u5168\u5C40\u914D\u7F6E\u3001\u623F\u95F4\u7B56\u7565\u3001\u5065\u5EB7\u68C0\u67E5\u4E0E\u914D\u7F6E\u5BA1\u8BA1\u8BB0\u5F55\u3002",
524
+ "tab.global": "\u5168\u5C40",
525
+ "tab.rooms": "\u623F\u95F4",
526
+ "tab.health": "\u5065\u5EB7",
527
+ "tab.audit": "\u5BA1\u8BA1",
528
+ "auth.token.label": "\u7BA1\u7406\u5458\u4EE4\u724C\uFF08\u53EF\u9009\uFF09",
529
+ "auth.token.placeholder": "ADMIN_TOKEN",
530
+ "auth.actor.label": "\u64CD\u4F5C\u8005\uFF08\u7528\u4E8E\u5BA1\u8BA1\u65E5\u5FD7\uFF09",
531
+ "auth.actor.placeholder": "\u4F60\u7684\u540D\u5B57",
532
+ "auth.language.label": "\u754C\u9762\u8BED\u8A00",
533
+ "auth.save": "\u4FDD\u5B58\u8BA4\u8BC1",
534
+ "auth.clear": "\u6E05\u9664\u8BA4\u8BC1",
535
+ "auth.permission.unknown": "\u6743\u9650\uFF1A\u672A\u77E5",
536
+ "auth.permission.unauth": "\u6743\u9650\uFF1A\u672A\u8BA4\u8BC1",
537
+ "auth.permission.prefix": "\u6743\u9650\uFF1A{role}{source}{actor}",
538
+ "auth.permission.actorSuffix": "\uFF08\u7528\u6237\uFF1A{actor}\uFF09",
539
+ "notice.ready": "\u5C31\u7EEA\u3002",
540
+ "notice.authSaved": "\u8BA4\u8BC1\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u5230 localStorage\u3002",
541
+ "notice.authCleared": "\u8BA4\u8BC1\u8BBE\u7F6E\u5DF2\u6E05\u9664\u3002",
542
+ "global.title": "\u5168\u5C40\u914D\u7F6E",
543
+ "global.commandPrefix": "\u547D\u4EE4\u524D\u7F00",
544
+ "global.defaultWorkdir": "\u9ED8\u8BA4\u5DE5\u4F5C\u76EE\u5F55",
545
+ "global.progressInterval": "\u8FDB\u5EA6\u66F4\u65B0\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09",
546
+ "global.typingTimeout": "\u8F93\u5165\u72B6\u6001\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
547
+ "global.sessionWindow": "\u4F1A\u8BDD\u6D3B\u8DC3\u7A97\u53E3\uFF08\u5206\u949F\uFF09",
548
+ "global.progressEnabled": "\u542F\u7528\u8FDB\u5EA6\u66F4\u65B0",
549
+ "global.rateWindow": "\u9650\u6D41\u7A97\u53E3\uFF08\u6BEB\u79D2\uFF09",
550
+ "global.rateUser": "\u5355\u7528\u6237\u7A97\u53E3\u6700\u5927\u8BF7\u6C42\u6570",
551
+ "global.rateRoom": "\u5355\u623F\u95F4\u7A97\u53E3\u6700\u5927\u8BF7\u6C42\u6570",
552
+ "global.concurrentGlobal": "\u5168\u5C40\u6700\u5927\u5E76\u53D1",
553
+ "global.concurrentUser": "\u5355\u7528\u6237\u6700\u5927\u5E76\u53D1",
554
+ "global.concurrentRoom": "\u5355\u623F\u95F4\u6700\u5927\u5E76\u53D1",
555
+ "global.groupDirect": "\u7FA4\u804A\u76F4\u901A\u6A21\u5F0F\uFF08\u65E0\u9700\u89E6\u53D1\uFF09",
556
+ "global.triggerMention": "\u89E6\u53D1\uFF1A\u63D0\u53CA\u673A\u5668\u4EBA",
557
+ "global.triggerReply": "\u89E6\u53D1\uFF1A\u56DE\u590D\u673A\u5668\u4EBA",
558
+ "global.triggerWindow": "\u89E6\u53D1\uFF1A\u6D3B\u8DC3\u7A97\u53E3",
559
+ "global.triggerPrefix": "\u89E6\u53D1\uFF1A\u547D\u4EE4\u524D\u7F00",
560
+ "global.cliEnabled": "CLI \u517C\u5BB9\u6A21\u5F0F",
561
+ "global.cliPass": "CLI \u900F\u4F20\u4E8B\u4EF6",
562
+ "global.cliWhitespace": "\u4FDD\u7559\u7A7A\u767D\u7B26",
563
+ "global.cliDisableSplit": "\u7981\u7528\u56DE\u590D\u5206\u7247",
564
+ "global.cliThrottle": "CLI \u8FDB\u5EA6\u8282\u6D41\uFF08\u6BEB\u79D2\uFF09",
565
+ "global.cliFetchMedia": "\u4E0B\u8F7D\u5A92\u4F53\u9644\u4EF6",
566
+ "global.cliTranscribeAudio": "\u8F6C\u5199\u97F3\u9891\u9644\u4EF6",
567
+ "global.audioModel": "\u97F3\u9891\u8F6C\u5199\u6A21\u578B",
568
+ "global.audioTimeout": "\u97F3\u9891\u8F6C\u5199\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
569
+ "global.audioMaxChars": "\u97F3\u9891\u8F6C\u5199\u6700\u5927\u5B57\u7B26\u6570",
570
+ "global.audioMaxRetries": "\u97F3\u9891\u8F6C\u5199\u6700\u5927\u91CD\u8BD5\u6B21\u6570",
571
+ "global.audioRetryDelay": "\u97F3\u9891\u8F6C\u5199\u91CD\u8BD5\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09",
572
+ "global.audioMaxBytes": "\u97F3\u9891\u6700\u5927\u5B57\u8282\u6570",
573
+ "global.audioLocalCommand": "\u672C\u5730 Whisper \u547D\u4EE4",
574
+ "global.audioLocalCommandPlaceholder": "python3 /opt/whisper/transcribe.py --input {input}",
575
+ "global.audioLocalTimeout": "\u672C\u5730 Whisper \u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09",
576
+ "global.agentEnabled": "\u542F\u7528\u591A\u667A\u80FD\u4F53\u5DE5\u4F5C\u6D41",
577
+ "global.agentRounds": "\u5DE5\u4F5C\u6D41\u81EA\u52A8\u4FEE\u590D\u8F6E\u6B21",
578
+ "global.save": "\u4FDD\u5B58\u5168\u5C40\u914D\u7F6E",
579
+ "global.reload": "\u91CD\u65B0\u52A0\u8F7D",
580
+ "global.restartMain": "\u91CD\u542F\u4E3B\u670D\u52A1",
581
+ "global.restartAll": "\u91CD\u542F\u4E3B\u670D\u52A1+\u7BA1\u7406\u540E\u53F0",
582
+ "global.restartHint": "\u4FDD\u5B58\u5168\u5C40\u914D\u7F6E\u4F1A\u66F4\u65B0 .env\uFF0C\u5E76\u9700\u8981\u91CD\u542F\u540E\u5B8C\u5168\u751F\u6548\u3002",
583
+ "notice.globalLoaded": "\u5168\u5C40\u914D\u7F6E\u5DF2\u52A0\u8F7D\u3002",
584
+ "notice.globalLoadFailed": "\u52A0\u8F7D\u5168\u5C40\u914D\u7F6E\u5931\u8D25\uFF1A{error}",
585
+ "notice.globalSaved": "\u4FDD\u5B58\u6210\u529F\uFF1A{keys}\u3002\u9700\u8981\u91CD\u542F\u540E\u751F\u6548\u3002",
586
+ "notice.globalSaveFailed": "\u4FDD\u5B58\u5168\u5C40\u914D\u7F6E\u5931\u8D25\uFF1A{error}",
587
+ "notice.restartRequested": "\u5DF2\u8BF7\u6C42\u91CD\u542F\uFF1A{services}\u3002{suffix}",
588
+ "notice.restartFailed": "\u91CD\u542F\u670D\u52A1\u5931\u8D25\uFF1A{error}",
589
+ "notice.restartSuffixAll": "\u7BA1\u7406\u540E\u53F0\u9875\u9762\u53EF\u80FD\u5728\u91CD\u542F\u671F\u95F4\u77ED\u6682\u65AD\u8FDE\u3002",
590
+ "rooms.title": "\u623F\u95F4\u914D\u7F6E",
591
+ "rooms.roomId": "\u623F\u95F4 ID",
592
+ "rooms.roomIdPlaceholder": "!room:example.com",
593
+ "rooms.summary": "\u5BA1\u8BA1\u6458\u8981\uFF08\u53EF\u9009\uFF09",
594
+ "rooms.summaryPlaceholder": "\u7ED1\u5B9A\u623F\u95F4\u5230\u9879\u76EE A",
595
+ "rooms.workdir": "\u5DE5\u4F5C\u76EE\u5F55",
596
+ "rooms.enabled": "\u542F\u7528",
597
+ "rooms.allowMention": "\u5141\u8BB8\u63D0\u53CA\u89E6\u53D1",
598
+ "rooms.allowReply": "\u5141\u8BB8\u56DE\u590D\u89E6\u53D1",
599
+ "rooms.allowWindow": "\u5141\u8BB8\u6D3B\u8DC3\u7A97\u53E3\u89E6\u53D1",
600
+ "rooms.allowPrefix": "\u5141\u8BB8\u524D\u7F00\u89E6\u53D1",
601
+ "rooms.load": "\u52A0\u8F7D\u623F\u95F4",
602
+ "rooms.save": "\u4FDD\u5B58\u623F\u95F4",
603
+ "rooms.delete": "\u5220\u9664\u623F\u95F4",
604
+ "rooms.refresh": "\u5237\u65B0\u5217\u8868",
605
+ "rooms.table.roomId": "\u623F\u95F4 ID",
606
+ "rooms.table.enabled": "\u542F\u7528",
607
+ "rooms.table.workdir": "\u5DE5\u4F5C\u76EE\u5F55",
608
+ "rooms.table.updatedAt": "\u66F4\u65B0\u65F6\u95F4",
609
+ "notice.roomsEmpty": "\u6682\u65E0\u623F\u95F4\u914D\u7F6E\u3002",
610
+ "notice.roomsLoaded": "\u5DF2\u52A0\u8F7D {count} \u6761\u623F\u95F4\u914D\u7F6E\u3002",
611
+ "notice.roomsLoadFailed": "\u52A0\u8F7D\u623F\u95F4\u5217\u8868\u5931\u8D25\uFF1A{error}",
612
+ "notice.roomIdRequired": "\u623F\u95F4 ID \u4E0D\u80FD\u4E3A\u7A7A\u3002",
613
+ "notice.roomLoaded": "\u623F\u95F4\u914D\u7F6E\u5DF2\u52A0\u8F7D\uFF1A{roomId}\u3002",
614
+ "notice.roomLoadFailed": "\u52A0\u8F7D\u623F\u95F4\u914D\u7F6E\u5931\u8D25\uFF1A{error}",
615
+ "notice.roomSaved": "\u623F\u95F4\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF1A{roomId}\u3002",
616
+ "notice.roomSaveFailed": "\u4FDD\u5B58\u623F\u95F4\u914D\u7F6E\u5931\u8D25\uFF1A{error}",
617
+ "notice.roomDeleted": "\u623F\u95F4\u914D\u7F6E\u5DF2\u5220\u9664\uFF1A{roomId}\u3002",
618
+ "notice.roomDeleteFailed": "\u5220\u9664\u623F\u95F4\u914D\u7F6E\u5931\u8D25\uFF1A{error}",
619
+ "confirm.roomDelete": "\u786E\u8BA4\u5220\u9664\u623F\u95F4\u914D\u7F6E\uFF1A{roomId}\uFF1F",
620
+ "health.title": "\u5065\u5EB7\u68C0\u67E5",
621
+ "health.run": "\u6267\u884C\u5065\u5EB7\u68C0\u67E5",
622
+ "health.table.component": "\u7EC4\u4EF6",
623
+ "health.table.status": "\u72B6\u6001",
624
+ "health.table.details": "\u8BE6\u60C5",
625
+ "health.component.codex": "Codex",
626
+ "health.component.matrix": "Matrix",
627
+ "health.component.overall": "\u6574\u4F53",
628
+ "health.status.ok": "\u6B63\u5E38",
629
+ "health.status.fail": "\u5931\u8D25",
630
+ "notice.healthDone": "\u5065\u5EB7\u68C0\u67E5\u5B8C\u6210\u3002",
631
+ "notice.healthFailed": "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF1A{error}",
632
+ "notice.healthEmptyFailed": "\u5065\u5EB7\u68C0\u67E5\u6267\u884C\u5931\u8D25\u3002",
633
+ "audit.title": "\u914D\u7F6E\u5BA1\u8BA1",
634
+ "audit.limit": "\u6761\u6570",
635
+ "audit.refresh": "\u5237\u65B0\u5BA1\u8BA1",
636
+ "audit.table.id": "ID",
637
+ "audit.table.time": "\u65F6\u95F4",
638
+ "audit.table.actor": "\u64CD\u4F5C\u8005",
639
+ "audit.table.summary": "\u6458\u8981",
640
+ "audit.table.payload": "\u8F7D\u8377",
641
+ "notice.auditEmpty": "\u6682\u65E0\u5BA1\u8BA1\u8BB0\u5F55\u3002",
642
+ "notice.auditLoaded": "\u5BA1\u8BA1\u8BB0\u5F55\u5DF2\u52A0\u8F7D\uFF1A{count} \u6761\u3002",
643
+ "notice.auditLoadFailed": "\u52A0\u8F7D\u5BA1\u8BA1\u8BB0\u5F55\u5931\u8D25\uFF1A{error}",
644
+ "table.loadFailed": "\u52A0\u8F7D\u5931\u8D25\u3002"
645
+ },
646
+ en: {
647
+ "header.title": "CodeHarbor Admin Console",
648
+ "header.subtitle": "Manage global settings, room policies, health checks, and config audit records.",
649
+ "tab.global": "Global",
650
+ "tab.rooms": "Rooms",
651
+ "tab.health": "Health",
652
+ "tab.audit": "Audit",
653
+ "auth.token.label": "Admin Token (optional)",
654
+ "auth.token.placeholder": "ADMIN_TOKEN",
655
+ "auth.actor.label": "Actor (for audit logs)",
656
+ "auth.actor.placeholder": "your-name",
657
+ "auth.language.label": "Language",
658
+ "auth.save": "Save Auth",
659
+ "auth.clear": "Clear Auth",
660
+ "auth.permission.unknown": "Permission: unknown",
661
+ "auth.permission.unauth": "Permission: unauthenticated",
662
+ "auth.permission.prefix": "Permission: {role}{source}{actor}",
663
+ "auth.permission.actorSuffix": " as {actor}",
664
+ "notice.ready": "Ready.",
665
+ "notice.authSaved": "Auth settings saved to localStorage.",
666
+ "notice.authCleared": "Auth settings cleared.",
667
+ "global.title": "Global Config",
668
+ "global.commandPrefix": "Command Prefix",
669
+ "global.defaultWorkdir": "Default Workdir",
670
+ "global.progressInterval": "Progress Interval (ms)",
671
+ "global.typingTimeout": "Typing Timeout (ms)",
672
+ "global.sessionWindow": "Session Active Window (minutes)",
673
+ "global.progressEnabled": "Enable progress updates",
674
+ "global.rateWindow": "Rate Window (ms)",
675
+ "global.rateUser": "Rate Max Requests / User",
676
+ "global.rateRoom": "Rate Max Requests / Room",
677
+ "global.concurrentGlobal": "Max Concurrent Global",
678
+ "global.concurrentUser": "Max Concurrent / User",
679
+ "global.concurrentRoom": "Max Concurrent / Room",
680
+ "global.groupDirect": "Group direct mode (no trigger required)",
681
+ "global.triggerMention": "Trigger: mention",
682
+ "global.triggerReply": "Trigger: reply",
683
+ "global.triggerWindow": "Trigger: active window",
684
+ "global.triggerPrefix": "Trigger: prefix",
685
+ "global.cliEnabled": "CLI compat mode",
686
+ "global.cliPass": "CLI passthrough events",
687
+ "global.cliWhitespace": "Preserve whitespace",
688
+ "global.cliDisableSplit": "Disable reply split",
689
+ "global.cliThrottle": "CLI progress throttle (ms)",
690
+ "global.cliFetchMedia": "Fetch media attachments",
691
+ "global.cliTranscribeAudio": "Transcribe audio attachments",
692
+ "global.audioModel": "Audio transcribe model",
693
+ "global.audioTimeout": "Audio transcribe timeout (ms)",
694
+ "global.audioMaxChars": "Audio transcript max chars",
695
+ "global.audioMaxRetries": "Audio transcribe max retries",
696
+ "global.audioRetryDelay": "Audio transcribe retry delay (ms)",
697
+ "global.audioMaxBytes": "Audio max bytes",
698
+ "global.audioLocalCommand": "Local whisper command",
699
+ "global.audioLocalCommandPlaceholder": "python3 /opt/whisper/transcribe.py --input {input}",
700
+ "global.audioLocalTimeout": "Local whisper timeout (ms)",
701
+ "global.agentEnabled": "Enable multi-agent workflow",
702
+ "global.agentRounds": "Workflow auto-repair rounds",
703
+ "global.save": "Save Global Config",
704
+ "global.reload": "Reload",
705
+ "global.restartMain": "Restart Main Service",
706
+ "global.restartAll": "Restart Main + Admin",
707
+ "global.restartHint": "Saving global config updates .env and requires restart to fully take effect.",
708
+ "notice.globalLoaded": "Global config loaded.",
709
+ "notice.globalLoadFailed": "Failed to load global config: {error}",
710
+ "notice.globalSaved": "Saved: {keys}. Restart is required.",
711
+ "notice.globalSaveFailed": "Failed to save global config: {error}",
712
+ "notice.restartRequested": "Restart requested: {services}. {suffix}",
713
+ "notice.restartFailed": "Failed to restart service(s): {error}",
714
+ "notice.restartSuffixAll": "Admin page may reconnect during restart.",
715
+ "rooms.title": "Room Config",
716
+ "rooms.roomId": "Room ID",
717
+ "rooms.roomIdPlaceholder": "!room:example.com",
718
+ "rooms.summary": "Audit Summary (optional)",
719
+ "rooms.summaryPlaceholder": "bind room to project A",
720
+ "rooms.workdir": "Workdir",
721
+ "rooms.enabled": "Enabled",
722
+ "rooms.allowMention": "Allow mention trigger",
723
+ "rooms.allowReply": "Allow reply trigger",
724
+ "rooms.allowWindow": "Allow active-window trigger",
725
+ "rooms.allowPrefix": "Allow prefix trigger",
726
+ "rooms.load": "Load Room",
727
+ "rooms.save": "Save Room",
728
+ "rooms.delete": "Delete Room",
729
+ "rooms.refresh": "Refresh List",
730
+ "rooms.table.roomId": "Room ID",
731
+ "rooms.table.enabled": "Enabled",
732
+ "rooms.table.workdir": "Workdir",
733
+ "rooms.table.updatedAt": "Updated At",
734
+ "notice.roomsEmpty": "No room settings.",
735
+ "notice.roomsLoaded": "Loaded {count} room setting(s).",
736
+ "notice.roomsLoadFailed": "Failed to load room list: {error}",
737
+ "notice.roomIdRequired": "Room ID is required.",
738
+ "notice.roomLoaded": "Room config loaded for {roomId}.",
739
+ "notice.roomLoadFailed": "Failed to load room config: {error}",
740
+ "notice.roomSaved": "Room config saved for {roomId}.",
741
+ "notice.roomSaveFailed": "Failed to save room config: {error}",
742
+ "notice.roomDeleted": "Room config deleted for {roomId}.",
743
+ "notice.roomDeleteFailed": "Failed to delete room config: {error}",
744
+ "confirm.roomDelete": "Delete room config for {roomId}?",
745
+ "health.title": "Health Check",
746
+ "health.run": "Run Health Check",
747
+ "health.table.component": "Component",
748
+ "health.table.status": "Status",
749
+ "health.table.details": "Details",
750
+ "health.component.codex": "Codex",
751
+ "health.component.matrix": "Matrix",
752
+ "health.component.overall": "Overall",
753
+ "health.status.ok": "OK",
754
+ "health.status.fail": "FAIL",
755
+ "notice.healthDone": "Health check completed.",
756
+ "notice.healthFailed": "Health check failed: {error}",
757
+ "notice.healthEmptyFailed": "Failed to run health check.",
758
+ "audit.title": "Config Audit",
759
+ "audit.limit": "Limit",
760
+ "audit.refresh": "Refresh Audit",
761
+ "audit.table.id": "ID",
762
+ "audit.table.time": "Time",
763
+ "audit.table.actor": "Actor",
764
+ "audit.table.summary": "Summary",
765
+ "audit.table.payload": "Payload",
766
+ "notice.auditEmpty": "No audit records.",
767
+ "notice.auditLoaded": "Audit loaded: {count} record(s).",
768
+ "notice.auditLoadFailed": "Failed to load audit: {error}",
769
+ "table.loadFailed": "Failed to load."
770
+ }
771
+ };
772
+ var currentLang = localStorage.getItem(storageLangKey);
773
+ if (currentLang !== "en" && currentLang !== "zh") {
774
+ currentLang = defaultLang;
775
+ }
509
776
  var loaded = {
510
777
  "settings-global": false,
511
778
  "settings-rooms": false,
@@ -515,6 +782,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
515
782
 
516
783
  var tokenInput = document.getElementById("auth-token");
517
784
  var actorInput = document.getElementById("auth-actor");
785
+ var langSelect = document.getElementById("lang-select");
518
786
  var noticeNode = document.getElementById("notice");
519
787
  var authRoleNode = document.getElementById("auth-role");
520
788
  var roomListBody = document.getElementById("room-list-body");
@@ -523,11 +791,19 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
523
791
 
524
792
  tokenInput.value = localStorage.getItem(storageTokenKey) || "";
525
793
  actorInput.value = localStorage.getItem(storageActorKey) || "";
794
+ langSelect.value = currentLang;
795
+
796
+ langSelect.addEventListener("change", function () {
797
+ currentLang = langSelect.value === "en" ? "en" : "zh";
798
+ localStorage.setItem(storageLangKey, currentLang);
799
+ applyLanguage();
800
+ void refreshAuthStatus();
801
+ });
526
802
 
527
803
  document.getElementById("auth-save-btn").addEventListener("click", function () {
528
804
  localStorage.setItem(storageTokenKey, tokenInput.value.trim());
529
805
  localStorage.setItem(storageActorKey, actorInput.value.trim());
530
- showNotice("ok", "Auth settings saved to localStorage.");
806
+ showNotice("ok", t("notice.authSaved"));
531
807
  void refreshAuthStatus();
532
808
  });
533
809
 
@@ -536,7 +812,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
536
812
  actorInput.value = "";
537
813
  localStorage.removeItem(storageTokenKey);
538
814
  localStorage.removeItem(storageActorKey);
539
- showNotice("warn", "Auth settings cleared.");
815
+ showNotice("warn", t("notice.authCleared"));
540
816
  void refreshAuthStatus();
541
817
  });
542
818
 
@@ -562,6 +838,8 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
562
838
  } else {
563
839
  handleRoute();
564
840
  }
841
+ applyLanguage();
842
+ showNotice("ok", t("notice.ready"));
565
843
  void refreshAuthStatus();
566
844
 
567
845
  function getCurrentView() {
@@ -648,6 +926,42 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
648
926
  return document.getElementById(inputId).value.trim();
649
927
  }
650
928
 
929
+ function t(key, vars) {
930
+ var dict = i18n[currentLang] || i18n[defaultLang];
931
+ var template = dict[key] || key;
932
+ if (!vars) {
933
+ return template;
934
+ }
935
+ return template.replace(/{([a-zA-Z0-9_]+)}/g, function (_all, name) {
936
+ return vars[name] === undefined || vars[name] === null ? "" : String(vars[name]);
937
+ });
938
+ }
939
+
940
+ function applyLanguage() {
941
+ var nodes = document.querySelectorAll("[data-i18n]");
942
+ for (var i = 0; i < nodes.length; i += 1) {
943
+ var node = nodes[i];
944
+ var key = node.getAttribute("data-i18n");
945
+ if (!key) {
946
+ continue;
947
+ }
948
+ node.textContent = t(key);
949
+ }
950
+ var placeholderNodes = document.querySelectorAll("[data-i18n-placeholder]");
951
+ for (var j = 0; j < placeholderNodes.length; j += 1) {
952
+ var input = placeholderNodes[j];
953
+ var placeholderKey = input.getAttribute("data-i18n-placeholder");
954
+ if (!placeholderKey) {
955
+ continue;
956
+ }
957
+ input.setAttribute("placeholder", t(placeholderKey));
958
+ }
959
+ document.documentElement.lang = currentLang === "en" ? "en" : "zh-CN";
960
+ if (langSelect.value !== currentLang) {
961
+ langSelect.value = currentLang;
962
+ }
963
+ }
964
+
651
965
  function showNotice(type, message) {
652
966
  noticeNode.className = "notice " + type;
653
967
  noticeNode.textContent = message;
@@ -658,21 +972,25 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
658
972
  var response = await apiRequest("/api/admin/auth/status", "GET");
659
973
  var data = response.data || {};
660
974
  if (!data.role) {
661
- authRoleNode.textContent = "Permission: unauthenticated";
975
+ authRoleNode.textContent = t("auth.permission.unauth");
662
976
  return;
663
977
  }
664
978
 
665
979
  var role = String(data.role).toUpperCase();
666
980
  var source = data.source ? " (" + String(data.source) + ")" : "";
667
- var actor = data.actor ? " as " + String(data.actor) : "";
668
- authRoleNode.textContent = "Permission: " + role + source + actor;
981
+ var actor = data.actor ? t("auth.permission.actorSuffix", { actor: String(data.actor) }) : "";
982
+ authRoleNode.textContent = t("auth.permission.prefix", {
983
+ role: role,
984
+ source: source,
985
+ actor: actor
986
+ });
669
987
  } catch (error) {
670
988
  var message = error && error.message ? String(error.message) : "";
671
989
  if (/Unauthorized/i.test(message)) {
672
- authRoleNode.textContent = "Permission: unauthenticated";
990
+ authRoleNode.textContent = t("auth.permission.unauth");
673
991
  return;
674
992
  }
675
- authRoleNode.textContent = "Permission: unknown";
993
+ authRoleNode.textContent = t("auth.permission.unknown");
676
994
  }
677
995
  }
678
996
 
@@ -736,9 +1054,9 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
736
1054
  typeof agentWorkflow.autoRepairMaxRounds === "number" ? agentWorkflow.autoRepairMaxRounds : 1
737
1055
  );
738
1056
 
739
- showNotice("ok", "Global config loaded.");
1057
+ showNotice("ok", t("notice.globalLoaded"));
740
1058
  } catch (error) {
741
- showNotice("error", "Failed to load global config: " + error.message);
1059
+ showNotice("error", t("notice.globalLoadFailed", { error: error.message }));
742
1060
  }
743
1061
  }
744
1062
 
@@ -790,10 +1108,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
790
1108
  };
791
1109
  var response = await apiRequest("/api/admin/config/global", "PUT", body);
792
1110
  var keys = Array.isArray(response.updatedKeys) ? response.updatedKeys.join(", ") : "global config";
793
- showNotice("warn", "Saved: " + keys + ". Restart is required.");
1111
+ showNotice("warn", t("notice.globalSaved", { keys: keys }));
794
1112
  await loadAudit();
795
1113
  } catch (error) {
796
- showNotice("error", "Failed to save global config: " + error.message);
1114
+ showNotice("error", t("notice.globalSaveFailed", { error: error.message }));
797
1115
  }
798
1116
  }
799
1117
 
@@ -803,10 +1121,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
803
1121
  withAdmin: Boolean(withAdmin)
804
1122
  });
805
1123
  var restarted = Array.isArray(response.restarted) ? response.restarted.join(", ") : "codeharbor";
806
- var suffix = withAdmin ? " Admin page may reconnect during restart." : "";
807
- showNotice("warn", "Restart requested: " + restarted + "." + suffix);
1124
+ var suffix = withAdmin ? t("notice.restartSuffixAll") : "";
1125
+ showNotice("warn", t("notice.restartRequested", { services: restarted, suffix: suffix }));
808
1126
  } catch (error) {
809
- showNotice("error", "Failed to restart service(s): " + error.message);
1127
+ showNotice("error", t("notice.restartFailed", { error: error.message }));
810
1128
  }
811
1129
  }
812
1130
 
@@ -816,7 +1134,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
816
1134
  var items = Array.isArray(response.data) ? response.data : [];
817
1135
  roomListBody.innerHTML = "";
818
1136
  if (items.length === 0) {
819
- renderEmptyRow(roomListBody, 4, "No room settings.");
1137
+ renderEmptyRow(roomListBody, 4, t("notice.roomsEmpty"));
820
1138
  return;
821
1139
  }
822
1140
  for (var i = 0; i < items.length; i += 1) {
@@ -828,10 +1146,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
828
1146
  appendCell(row, item.updatedAt ? new Date(item.updatedAt).toISOString() : "-");
829
1147
  roomListBody.appendChild(row);
830
1148
  }
831
- showNotice("ok", "Loaded " + items.length + " room setting(s).");
1149
+ showNotice("ok", t("notice.roomsLoaded", { count: items.length }));
832
1150
  } catch (error) {
833
- showNotice("error", "Failed to load room list: " + error.message);
834
- renderEmptyRow(roomListBody, 4, "Failed to load room settings.");
1151
+ showNotice("error", t("notice.roomsLoadFailed", { error: error.message }));
1152
+ renderEmptyRow(roomListBody, 4, t("table.loadFailed"));
835
1153
  }
836
1154
  }
837
1155
 
@@ -844,15 +1162,15 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
844
1162
  async function loadRoom() {
845
1163
  var roomId = asText("room-id");
846
1164
  if (!roomId) {
847
- showNotice("warn", "Room ID is required.");
1165
+ showNotice("warn", t("notice.roomIdRequired"));
848
1166
  return;
849
1167
  }
850
1168
  try {
851
1169
  var response = await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "GET");
852
1170
  fillRoomForm(response.data || {});
853
- showNotice("ok", "Room config loaded for " + roomId + ".");
1171
+ showNotice("ok", t("notice.roomLoaded", { roomId: roomId }));
854
1172
  } catch (error) {
855
- showNotice("error", "Failed to load room config: " + error.message);
1173
+ showNotice("error", t("notice.roomLoadFailed", { error: error.message }));
856
1174
  }
857
1175
  }
858
1176
 
@@ -868,7 +1186,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
868
1186
  async function saveRoom() {
869
1187
  var roomId = asText("room-id");
870
1188
  if (!roomId) {
871
- showNotice("warn", "Room ID is required.");
1189
+ showNotice("warn", t("notice.roomIdRequired"));
872
1190
  return;
873
1191
  }
874
1192
  try {
@@ -882,30 +1200,30 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
882
1200
  summary: asText("room-summary")
883
1201
  };
884
1202
  await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "PUT", body);
885
- showNotice("ok", "Room config saved for " + roomId + ".");
1203
+ showNotice("ok", t("notice.roomSaved", { roomId: roomId }));
886
1204
  await refreshRoomList();
887
1205
  await loadAudit();
888
1206
  } catch (error) {
889
- showNotice("error", "Failed to save room config: " + error.message);
1207
+ showNotice("error", t("notice.roomSaveFailed", { error: error.message }));
890
1208
  }
891
1209
  }
892
1210
 
893
1211
  async function deleteRoom() {
894
1212
  var roomId = asText("room-id");
895
1213
  if (!roomId) {
896
- showNotice("warn", "Room ID is required.");
1214
+ showNotice("warn", t("notice.roomIdRequired"));
897
1215
  return;
898
1216
  }
899
- if (!window.confirm("Delete room config for " + roomId + "?")) {
1217
+ if (!window.confirm(t("confirm.roomDelete", { roomId: roomId }))) {
900
1218
  return;
901
1219
  }
902
1220
  try {
903
1221
  await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "DELETE");
904
- showNotice("ok", "Room config deleted for " + roomId + ".");
1222
+ showNotice("ok", t("notice.roomDeleted", { roomId: roomId }));
905
1223
  await refreshRoomList();
906
1224
  await loadAudit();
907
1225
  } catch (error) {
908
- showNotice("error", "Failed to delete room config: " + error.message);
1226
+ showNotice("error", t("notice.roomDeleteFailed", { error: error.message }));
909
1227
  }
910
1228
  }
911
1229
 
@@ -917,24 +1235,30 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
917
1235
  var codex = response.codex || {};
918
1236
  var matrix = response.matrix || {};
919
1237
 
920
- appendHealthRow("Codex", Boolean(codex.ok), codex.ok ? (codex.version || "ok") : (codex.error || "failed"));
921
1238
  appendHealthRow(
922
- "Matrix",
1239
+ t("health.component.codex"),
1240
+ Boolean(codex.ok),
1241
+ codex.ok ? (codex.version || t("health.status.ok")) : (codex.error || t("health.status.fail"))
1242
+ );
1243
+ appendHealthRow(
1244
+ t("health.component.matrix"),
923
1245
  Boolean(matrix.ok),
924
- matrix.ok ? "HTTP " + matrix.status + " " + JSON.stringify(matrix.versions || []) : (matrix.error || "failed")
1246
+ matrix.ok
1247
+ ? "HTTP " + matrix.status + " " + JSON.stringify(matrix.versions || [])
1248
+ : (matrix.error || t("health.status.fail"))
925
1249
  );
926
- appendHealthRow("Overall", Boolean(response.ok), response.timestamp || "");
927
- showNotice("ok", "Health check completed.");
1250
+ appendHealthRow(t("health.component.overall"), Boolean(response.ok), response.timestamp || "");
1251
+ showNotice("ok", t("notice.healthDone"));
928
1252
  } catch (error) {
929
- showNotice("error", "Health check failed: " + error.message);
930
- renderEmptyRow(healthBody, 3, "Failed to run health check.");
1253
+ showNotice("error", t("notice.healthFailed", { error: error.message }));
1254
+ renderEmptyRow(healthBody, 3, t("notice.healthEmptyFailed"));
931
1255
  }
932
1256
  }
933
1257
 
934
1258
  function appendHealthRow(component, ok, detail) {
935
1259
  var row = document.createElement("tr");
936
1260
  appendCell(row, component);
937
- appendCell(row, ok ? "OK" : "FAIL");
1261
+ appendCell(row, ok ? t("health.status.ok") : t("health.status.fail"));
938
1262
  appendCell(row, detail);
939
1263
  healthBody.appendChild(row);
940
1264
  }
@@ -952,7 +1276,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
952
1276
  var items = Array.isArray(response.data) ? response.data : [];
953
1277
  auditBody.innerHTML = "";
954
1278
  if (items.length === 0) {
955
- renderEmptyRow(auditBody, 5, "No audit records.");
1279
+ renderEmptyRow(auditBody, 5, t("notice.auditEmpty"));
956
1280
  return;
957
1281
  }
958
1282
  for (var i = 0; i < items.length; i += 1) {
@@ -969,10 +1293,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
969
1293
  row.appendChild(payloadCell);
970
1294
  auditBody.appendChild(row);
971
1295
  }
972
- showNotice("ok", "Audit loaded: " + items.length + " record(s).");
1296
+ showNotice("ok", t("notice.auditLoaded", { count: items.length }));
973
1297
  } catch (error) {
974
- showNotice("error", "Failed to load audit: " + error.message);
975
- renderEmptyRow(auditBody, 5, "Failed to load audit records.");
1298
+ showNotice("error", t("notice.auditLoadFailed", { error: error.message }));
1299
+ renderEmptyRow(auditBody, 5, t("table.loadFailed"));
976
1300
  }
977
1301
  }
978
1302
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeharbor",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Instant-messaging bridge for Codex CLI sessions",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli.js",