codeharbor 0.1.21 → 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.
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,169 +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">Local whisper command</span>
371
- <input id="global-cli-audio-local-command" type="text" placeholder='python3 /opt/whisper/transcribe.py --input {input}' />
379
+ <span class="field-label" data-i18n="global.audioMaxRetries">\u97F3\u9891\u8F6C\u5199\u6700\u5927\u91CD\u8BD5\u6B21\u6570</span>
380
+ <input id="global-cli-audio-max-retries" type="number" min="0" max="10" />
381
+ </label>
382
+ <label class="field">
383
+ <span class="field-label" data-i18n="global.audioRetryDelay">\u97F3\u9891\u8F6C\u5199\u91CD\u8BD5\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09</span>
384
+ <input id="global-cli-audio-retry-delay" type="number" min="0" />
372
385
  </label>
373
386
  <label class="field">
374
- <span class="field-label">Local whisper timeout (ms)</span>
387
+ <span class="field-label" data-i18n="global.audioMaxBytes">\u97F3\u9891\u6700\u5927\u5B57\u8282\u6570</span>
388
+ <input id="global-cli-audio-max-bytes" type="number" min="1" />
389
+ </label>
390
+ <label class="field">
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" />
393
+ </label>
394
+ <label class="field">
395
+ <span class="field-label" data-i18n="global.audioLocalTimeout">\u672C\u5730 Whisper \u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09</span>
375
396
  <input id="global-cli-audio-local-timeout" type="number" min="1" />
376
397
  </label>
377
- <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>
378
399
  <label class="field">
379
- <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>
380
401
  <input id="global-agent-repair-rounds" type="number" min="0" max="10" />
381
402
  </label>
382
403
  </div>
383
404
  <div class="actions">
384
- <button id="global-save-btn" type="button">Save Global Config</button>
385
- <button id="global-reload-btn" type="button" class="secondary">Reload</button>
386
- <button id="global-restart-main-btn" type="button" class="secondary">Restart Main Service</button>
387
- <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>
388
409
  </div>
389
- <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>
390
411
  </section>
391
412
 
392
413
  <section class="panel" data-view="settings-rooms" hidden>
393
- <h2 class="panel-title">Room Config</h2>
414
+ <h2 class="panel-title" data-i18n="rooms.title">\u623F\u95F4\u914D\u7F6E</h2>
394
415
  <div class="grid">
395
416
  <label class="field">
396
- <span class="field-label">Room ID</span>
397
- <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" />
398
419
  </label>
399
420
  <label class="field">
400
- <span class="field-label">Audit Summary (optional)</span>
401
- <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" />
402
423
  </label>
403
424
  <label class="field full">
404
- <span class="field-label">Workdir</span>
425
+ <span class="field-label" data-i18n="rooms.workdir">\u5DE5\u4F5C\u76EE\u5F55</span>
405
426
  <input id="room-workdir" type="text" />
406
427
  </label>
407
- <label class="checkbox"><input id="room-enabled" type="checkbox" /><span>Enabled</span></label>
408
- <label class="checkbox"><input id="room-mention" type="checkbox" /><span>Allow mention trigger</span></label>
409
- <label class="checkbox"><input id="room-reply" type="checkbox" /><span>Allow reply trigger</span></label>
410
- <label class="checkbox"><input id="room-window" type="checkbox" /><span>Allow active-window trigger</span></label>
411
- <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>
412
433
  </div>
413
434
  <div class="actions">
414
- <button id="room-load-btn" type="button" class="secondary">Load Room</button>
415
- <button id="room-save-btn" type="button">Save Room</button>
416
- <button id="room-delete-btn" type="button" class="danger">Delete Room</button>
417
- <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>
418
439
  </div>
419
440
  <div class="table-wrap">
420
441
  <table>
421
442
  <thead>
422
443
  <tr>
423
- <th>Room ID</th>
424
- <th>Enabled</th>
425
- <th>Workdir</th>
426
- <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>
427
448
  </tr>
428
449
  </thead>
429
450
  <tbody id="room-list-body"></tbody>
@@ -432,17 +453,17 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
432
453
  </section>
433
454
 
434
455
  <section class="panel" data-view="health" hidden>
435
- <h2 class="panel-title">Health Check</h2>
456
+ <h2 class="panel-title" data-i18n="health.title">\u5065\u5EB7\u68C0\u67E5</h2>
436
457
  <div class="actions">
437
- <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>
438
459
  </div>
439
460
  <div class="table-wrap">
440
461
  <table>
441
462
  <thead>
442
463
  <tr>
443
- <th>Component</th>
444
- <th>Status</th>
445
- <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>
446
467
  </tr>
447
468
  </thead>
448
469
  <tbody id="health-body"></tbody>
@@ -451,23 +472,23 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
451
472
  </section>
452
473
 
453
474
  <section class="panel" data-view="audit" hidden>
454
- <h2 class="panel-title">Config Audit</h2>
475
+ <h2 class="panel-title" data-i18n="audit.title">\u914D\u7F6E\u5BA1\u8BA1</h2>
455
476
  <div class="actions">
456
477
  <label class="field" style="max-width: 120px;">
457
- <span class="field-label">Limit</span>
478
+ <span class="field-label" data-i18n="audit.limit">\u6761\u6570</span>
458
479
  <input id="audit-limit" type="number" min="1" max="200" value="30" />
459
480
  </label>
460
- <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>
461
482
  </div>
462
483
  <div class="table-wrap">
463
484
  <table>
464
485
  <thead>
465
486
  <tr>
466
- <th>ID</th>
467
- <th>Time</th>
468
- <th>Actor</th>
469
- <th>Summary</th>
470
- <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>
471
492
  </tr>
472
493
  </thead>
473
494
  <tbody id="audit-body"></tbody>
@@ -494,6 +515,264 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
494
515
  };
495
516
  var storageTokenKey = "codeharbor.admin.token";
496
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
+ }
497
776
  var loaded = {
498
777
  "settings-global": false,
499
778
  "settings-rooms": false,
@@ -503,6 +782,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
503
782
 
504
783
  var tokenInput = document.getElementById("auth-token");
505
784
  var actorInput = document.getElementById("auth-actor");
785
+ var langSelect = document.getElementById("lang-select");
506
786
  var noticeNode = document.getElementById("notice");
507
787
  var authRoleNode = document.getElementById("auth-role");
508
788
  var roomListBody = document.getElementById("room-list-body");
@@ -511,11 +791,19 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
511
791
 
512
792
  tokenInput.value = localStorage.getItem(storageTokenKey) || "";
513
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
+ });
514
802
 
515
803
  document.getElementById("auth-save-btn").addEventListener("click", function () {
516
804
  localStorage.setItem(storageTokenKey, tokenInput.value.trim());
517
805
  localStorage.setItem(storageActorKey, actorInput.value.trim());
518
- showNotice("ok", "Auth settings saved to localStorage.");
806
+ showNotice("ok", t("notice.authSaved"));
519
807
  void refreshAuthStatus();
520
808
  });
521
809
 
@@ -524,7 +812,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
524
812
  actorInput.value = "";
525
813
  localStorage.removeItem(storageTokenKey);
526
814
  localStorage.removeItem(storageActorKey);
527
- showNotice("warn", "Auth settings cleared.");
815
+ showNotice("warn", t("notice.authCleared"));
528
816
  void refreshAuthStatus();
529
817
  });
530
818
 
@@ -550,6 +838,8 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
550
838
  } else {
551
839
  handleRoute();
552
840
  }
841
+ applyLanguage();
842
+ showNotice("ok", t("notice.ready"));
553
843
  void refreshAuthStatus();
554
844
 
555
845
  function getCurrentView() {
@@ -636,6 +926,42 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
636
926
  return document.getElementById(inputId).value.trim();
637
927
  }
638
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
+
639
965
  function showNotice(type, message) {
640
966
  noticeNode.className = "notice " + type;
641
967
  noticeNode.textContent = message;
@@ -646,21 +972,25 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
646
972
  var response = await apiRequest("/api/admin/auth/status", "GET");
647
973
  var data = response.data || {};
648
974
  if (!data.role) {
649
- authRoleNode.textContent = "Permission: unauthenticated";
975
+ authRoleNode.textContent = t("auth.permission.unauth");
650
976
  return;
651
977
  }
652
978
 
653
979
  var role = String(data.role).toUpperCase();
654
980
  var source = data.source ? " (" + String(data.source) + ")" : "";
655
- var actor = data.actor ? " as " + String(data.actor) : "";
656
- 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
+ });
657
987
  } catch (error) {
658
988
  var message = error && error.message ? String(error.message) : "";
659
989
  if (/Unauthorized/i.test(message)) {
660
- authRoleNode.textContent = "Permission: unauthenticated";
990
+ authRoleNode.textContent = t("auth.permission.unauth");
661
991
  return;
662
992
  }
663
- authRoleNode.textContent = "Permission: unknown";
993
+ authRoleNode.textContent = t("auth.permission.unknown");
664
994
  }
665
995
  }
666
996
 
@@ -712,6 +1042,11 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
712
1042
  document.getElementById("global-cli-audio-model").value = cliCompat.audioTranscribeModel || "gpt-4o-mini-transcribe";
713
1043
  document.getElementById("global-cli-audio-timeout").value = String(cliCompat.audioTranscribeTimeoutMs || 120000);
714
1044
  document.getElementById("global-cli-audio-max-chars").value = String(cliCompat.audioTranscribeMaxChars || 6000);
1045
+ document.getElementById("global-cli-audio-max-retries").value = String(
1046
+ typeof cliCompat.audioTranscribeMaxRetries === "number" ? cliCompat.audioTranscribeMaxRetries : 1
1047
+ );
1048
+ document.getElementById("global-cli-audio-retry-delay").value = String(cliCompat.audioTranscribeRetryDelayMs || 800);
1049
+ document.getElementById("global-cli-audio-max-bytes").value = String(cliCompat.audioTranscribeMaxBytes || 26214400);
715
1050
  document.getElementById("global-cli-audio-local-command").value = cliCompat.audioLocalWhisperCommand || "";
716
1051
  document.getElementById("global-cli-audio-local-timeout").value = String(cliCompat.audioLocalWhisperTimeoutMs || 180000);
717
1052
  document.getElementById("global-agent-enabled").checked = Boolean(agentWorkflow.enabled);
@@ -719,9 +1054,9 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
719
1054
  typeof agentWorkflow.autoRepairMaxRounds === "number" ? agentWorkflow.autoRepairMaxRounds : 1
720
1055
  );
721
1056
 
722
- showNotice("ok", "Global config loaded.");
1057
+ showNotice("ok", t("notice.globalLoaded"));
723
1058
  } catch (error) {
724
- showNotice("error", "Failed to load global config: " + error.message);
1059
+ showNotice("error", t("notice.globalLoadFailed", { error: error.message }));
725
1060
  }
726
1061
  }
727
1062
 
@@ -760,6 +1095,9 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
760
1095
  audioTranscribeModel: asText("global-cli-audio-model") || "gpt-4o-mini-transcribe",
761
1096
  audioTranscribeTimeoutMs: asNumber("global-cli-audio-timeout", 120000),
762
1097
  audioTranscribeMaxChars: asNumber("global-cli-audio-max-chars", 6000),
1098
+ audioTranscribeMaxRetries: asNumber("global-cli-audio-max-retries", 1),
1099
+ audioTranscribeRetryDelayMs: asNumber("global-cli-audio-retry-delay", 800),
1100
+ audioTranscribeMaxBytes: asNumber("global-cli-audio-max-bytes", 26214400),
763
1101
  audioLocalWhisperCommand: asText("global-cli-audio-local-command"),
764
1102
  audioLocalWhisperTimeoutMs: asNumber("global-cli-audio-local-timeout", 180000)
765
1103
  },
@@ -770,10 +1108,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
770
1108
  };
771
1109
  var response = await apiRequest("/api/admin/config/global", "PUT", body);
772
1110
  var keys = Array.isArray(response.updatedKeys) ? response.updatedKeys.join(", ") : "global config";
773
- showNotice("warn", "Saved: " + keys + ". Restart is required.");
1111
+ showNotice("warn", t("notice.globalSaved", { keys: keys }));
774
1112
  await loadAudit();
775
1113
  } catch (error) {
776
- showNotice("error", "Failed to save global config: " + error.message);
1114
+ showNotice("error", t("notice.globalSaveFailed", { error: error.message }));
777
1115
  }
778
1116
  }
779
1117
 
@@ -783,10 +1121,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
783
1121
  withAdmin: Boolean(withAdmin)
784
1122
  });
785
1123
  var restarted = Array.isArray(response.restarted) ? response.restarted.join(", ") : "codeharbor";
786
- var suffix = withAdmin ? " Admin page may reconnect during restart." : "";
787
- showNotice("warn", "Restart requested: " + restarted + "." + suffix);
1124
+ var suffix = withAdmin ? t("notice.restartSuffixAll") : "";
1125
+ showNotice("warn", t("notice.restartRequested", { services: restarted, suffix: suffix }));
788
1126
  } catch (error) {
789
- showNotice("error", "Failed to restart service(s): " + error.message);
1127
+ showNotice("error", t("notice.restartFailed", { error: error.message }));
790
1128
  }
791
1129
  }
792
1130
 
@@ -796,7 +1134,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
796
1134
  var items = Array.isArray(response.data) ? response.data : [];
797
1135
  roomListBody.innerHTML = "";
798
1136
  if (items.length === 0) {
799
- renderEmptyRow(roomListBody, 4, "No room settings.");
1137
+ renderEmptyRow(roomListBody, 4, t("notice.roomsEmpty"));
800
1138
  return;
801
1139
  }
802
1140
  for (var i = 0; i < items.length; i += 1) {
@@ -808,10 +1146,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
808
1146
  appendCell(row, item.updatedAt ? new Date(item.updatedAt).toISOString() : "-");
809
1147
  roomListBody.appendChild(row);
810
1148
  }
811
- showNotice("ok", "Loaded " + items.length + " room setting(s).");
1149
+ showNotice("ok", t("notice.roomsLoaded", { count: items.length }));
812
1150
  } catch (error) {
813
- showNotice("error", "Failed to load room list: " + error.message);
814
- renderEmptyRow(roomListBody, 4, "Failed to load room settings.");
1151
+ showNotice("error", t("notice.roomsLoadFailed", { error: error.message }));
1152
+ renderEmptyRow(roomListBody, 4, t("table.loadFailed"));
815
1153
  }
816
1154
  }
817
1155
 
@@ -824,15 +1162,15 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
824
1162
  async function loadRoom() {
825
1163
  var roomId = asText("room-id");
826
1164
  if (!roomId) {
827
- showNotice("warn", "Room ID is required.");
1165
+ showNotice("warn", t("notice.roomIdRequired"));
828
1166
  return;
829
1167
  }
830
1168
  try {
831
1169
  var response = await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "GET");
832
1170
  fillRoomForm(response.data || {});
833
- showNotice("ok", "Room config loaded for " + roomId + ".");
1171
+ showNotice("ok", t("notice.roomLoaded", { roomId: roomId }));
834
1172
  } catch (error) {
835
- showNotice("error", "Failed to load room config: " + error.message);
1173
+ showNotice("error", t("notice.roomLoadFailed", { error: error.message }));
836
1174
  }
837
1175
  }
838
1176
 
@@ -848,7 +1186,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
848
1186
  async function saveRoom() {
849
1187
  var roomId = asText("room-id");
850
1188
  if (!roomId) {
851
- showNotice("warn", "Room ID is required.");
1189
+ showNotice("warn", t("notice.roomIdRequired"));
852
1190
  return;
853
1191
  }
854
1192
  try {
@@ -862,30 +1200,30 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
862
1200
  summary: asText("room-summary")
863
1201
  };
864
1202
  await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "PUT", body);
865
- showNotice("ok", "Room config saved for " + roomId + ".");
1203
+ showNotice("ok", t("notice.roomSaved", { roomId: roomId }));
866
1204
  await refreshRoomList();
867
1205
  await loadAudit();
868
1206
  } catch (error) {
869
- showNotice("error", "Failed to save room config: " + error.message);
1207
+ showNotice("error", t("notice.roomSaveFailed", { error: error.message }));
870
1208
  }
871
1209
  }
872
1210
 
873
1211
  async function deleteRoom() {
874
1212
  var roomId = asText("room-id");
875
1213
  if (!roomId) {
876
- showNotice("warn", "Room ID is required.");
1214
+ showNotice("warn", t("notice.roomIdRequired"));
877
1215
  return;
878
1216
  }
879
- if (!window.confirm("Delete room config for " + roomId + "?")) {
1217
+ if (!window.confirm(t("confirm.roomDelete", { roomId: roomId }))) {
880
1218
  return;
881
1219
  }
882
1220
  try {
883
1221
  await apiRequest("/api/admin/config/rooms/" + encodeURIComponent(roomId), "DELETE");
884
- showNotice("ok", "Room config deleted for " + roomId + ".");
1222
+ showNotice("ok", t("notice.roomDeleted", { roomId: roomId }));
885
1223
  await refreshRoomList();
886
1224
  await loadAudit();
887
1225
  } catch (error) {
888
- showNotice("error", "Failed to delete room config: " + error.message);
1226
+ showNotice("error", t("notice.roomDeleteFailed", { error: error.message }));
889
1227
  }
890
1228
  }
891
1229
 
@@ -897,24 +1235,30 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
897
1235
  var codex = response.codex || {};
898
1236
  var matrix = response.matrix || {};
899
1237
 
900
- appendHealthRow("Codex", Boolean(codex.ok), codex.ok ? (codex.version || "ok") : (codex.error || "failed"));
901
1238
  appendHealthRow(
902
- "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"),
903
1245
  Boolean(matrix.ok),
904
- 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"))
905
1249
  );
906
- appendHealthRow("Overall", Boolean(response.ok), response.timestamp || "");
907
- showNotice("ok", "Health check completed.");
1250
+ appendHealthRow(t("health.component.overall"), Boolean(response.ok), response.timestamp || "");
1251
+ showNotice("ok", t("notice.healthDone"));
908
1252
  } catch (error) {
909
- showNotice("error", "Health check failed: " + error.message);
910
- renderEmptyRow(healthBody, 3, "Failed to run health check.");
1253
+ showNotice("error", t("notice.healthFailed", { error: error.message }));
1254
+ renderEmptyRow(healthBody, 3, t("notice.healthEmptyFailed"));
911
1255
  }
912
1256
  }
913
1257
 
914
1258
  function appendHealthRow(component, ok, detail) {
915
1259
  var row = document.createElement("tr");
916
1260
  appendCell(row, component);
917
- appendCell(row, ok ? "OK" : "FAIL");
1261
+ appendCell(row, ok ? t("health.status.ok") : t("health.status.fail"));
918
1262
  appendCell(row, detail);
919
1263
  healthBody.appendChild(row);
920
1264
  }
@@ -932,7 +1276,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
932
1276
  var items = Array.isArray(response.data) ? response.data : [];
933
1277
  auditBody.innerHTML = "";
934
1278
  if (items.length === 0) {
935
- renderEmptyRow(auditBody, 5, "No audit records.");
1279
+ renderEmptyRow(auditBody, 5, t("notice.auditEmpty"));
936
1280
  return;
937
1281
  }
938
1282
  for (var i = 0; i < items.length; i += 1) {
@@ -949,10 +1293,10 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
949
1293
  row.appendChild(payloadCell);
950
1294
  auditBody.appendChild(row);
951
1295
  }
952
- showNotice("ok", "Audit loaded: " + items.length + " record(s).");
1296
+ showNotice("ok", t("notice.auditLoaded", { count: items.length }));
953
1297
  } catch (error) {
954
- showNotice("error", "Failed to load audit: " + error.message);
955
- renderEmptyRow(auditBody, 5, "Failed to load audit records.");
1298
+ showNotice("error", t("notice.auditLoadFailed", { error: error.message }));
1299
+ renderEmptyRow(auditBody, 5, t("table.loadFailed"));
956
1300
  }
957
1301
  }
958
1302
 
@@ -2037,6 +2381,37 @@ var AdminServer = class {
2037
2381
  envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS = String(value);
2038
2382
  updatedKeys.push("cliCompat.audioTranscribeMaxChars");
2039
2383
  }
2384
+ if ("audioTranscribeMaxRetries" in compat) {
2385
+ const value = normalizePositiveInt(
2386
+ compat.audioTranscribeMaxRetries,
2387
+ this.config.cliCompat.audioTranscribeMaxRetries,
2388
+ 0,
2389
+ 10
2390
+ );
2391
+ this.config.cliCompat.audioTranscribeMaxRetries = value;
2392
+ envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES = String(value);
2393
+ updatedKeys.push("cliCompat.audioTranscribeMaxRetries");
2394
+ }
2395
+ if ("audioTranscribeRetryDelayMs" in compat) {
2396
+ const value = normalizeNonNegativeInt(
2397
+ compat.audioTranscribeRetryDelayMs,
2398
+ this.config.cliCompat.audioTranscribeRetryDelayMs
2399
+ );
2400
+ this.config.cliCompat.audioTranscribeRetryDelayMs = value;
2401
+ envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS = String(value);
2402
+ updatedKeys.push("cliCompat.audioTranscribeRetryDelayMs");
2403
+ }
2404
+ if ("audioTranscribeMaxBytes" in compat) {
2405
+ const value = normalizePositiveInt(
2406
+ compat.audioTranscribeMaxBytes,
2407
+ this.config.cliCompat.audioTranscribeMaxBytes,
2408
+ 1,
2409
+ Number.MAX_SAFE_INTEGER
2410
+ );
2411
+ this.config.cliCompat.audioTranscribeMaxBytes = value;
2412
+ envUpdates.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES = String(value);
2413
+ updatedKeys.push("cliCompat.audioTranscribeMaxBytes");
2414
+ }
2040
2415
  if ("audioLocalWhisperCommand" in compat) {
2041
2416
  const value = normalizeString(
2042
2417
  compat.audioLocalWhisperCommand,
@@ -3753,12 +4128,15 @@ var import_promises3 = __toESM(require("fs/promises"));
3753
4128
  var import_node_path8 = __toESM(require("path"));
3754
4129
  var import_node_util3 = require("util");
3755
4130
  var execAsync = (0, import_node_util3.promisify)(import_node_child_process5.exec);
4131
+ var RETRYABLE_OPENAI_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
3756
4132
  var AudioTranscriber = class {
3757
4133
  enabled;
3758
4134
  apiKey;
3759
4135
  model;
3760
4136
  timeoutMs;
3761
4137
  maxChars;
4138
+ maxRetries;
4139
+ retryDelayMs;
3762
4140
  localWhisperCommand;
3763
4141
  localWhisperTimeoutMs;
3764
4142
  constructor(options) {
@@ -3767,6 +4145,8 @@ var AudioTranscriber = class {
3767
4145
  this.model = options.model;
3768
4146
  this.timeoutMs = options.timeoutMs;
3769
4147
  this.maxChars = options.maxChars;
4148
+ this.maxRetries = options.maxRetries;
4149
+ this.retryDelayMs = options.retryDelayMs;
3770
4150
  this.localWhisperCommand = options.localWhisperCommand;
3771
4151
  this.localWhisperTimeoutMs = options.localWhisperTimeoutMs;
3772
4152
  }
@@ -3808,7 +4188,7 @@ var AudioTranscriber = class {
3808
4188
  let localError = null;
3809
4189
  if (hasLocalWhisper) {
3810
4190
  try {
3811
- const localText = await this.transcribeOneWithLocalWhisper(attachment);
4191
+ const localText = await this.transcribeOneWithLocalWhisperWithRetry(attachment);
3812
4192
  if (localText) {
3813
4193
  return localText;
3814
4194
  }
@@ -3818,7 +4198,7 @@ var AudioTranscriber = class {
3818
4198
  }
3819
4199
  if (hasOpenAi) {
3820
4200
  try {
3821
- return await this.transcribeOneWithOpenAi(attachment);
4201
+ return await this.transcribeOneWithOpenAiWithRetry(attachment);
3822
4202
  } catch (error) {
3823
4203
  if (!localError) {
3824
4204
  throw error;
@@ -3834,6 +4214,34 @@ var AudioTranscriber = class {
3834
4214
  }
3835
4215
  return "";
3836
4216
  }
4217
+ async transcribeOneWithOpenAiWithRetry(attachment) {
4218
+ let attempt = 0;
4219
+ while (true) {
4220
+ try {
4221
+ return await this.transcribeOneWithOpenAi(attachment);
4222
+ } catch (error) {
4223
+ if (!isRetryableOpenAiError(error) || attempt >= this.maxRetries) {
4224
+ throw error;
4225
+ }
4226
+ attempt += 1;
4227
+ await sleep2(this.retryDelayMs * attempt);
4228
+ }
4229
+ }
4230
+ }
4231
+ async transcribeOneWithLocalWhisperWithRetry(attachment) {
4232
+ let attempt = 0;
4233
+ while (true) {
4234
+ try {
4235
+ return await this.transcribeOneWithLocalWhisper(attachment);
4236
+ } catch (error) {
4237
+ if (attempt >= this.maxRetries) {
4238
+ throw error;
4239
+ }
4240
+ attempt += 1;
4241
+ await sleep2(this.retryDelayMs * attempt);
4242
+ }
4243
+ }
4244
+ }
3837
4245
  async transcribeOneWithOpenAi(attachment) {
3838
4246
  if (!this.apiKey) {
3839
4247
  return "";
@@ -3866,7 +4274,7 @@ var AudioTranscriber = class {
3866
4274
  const payload = await response.json().catch(() => ({}));
3867
4275
  if (!response.ok) {
3868
4276
  const message = typeof payload?.error?.message === "string" ? payload.error.message : `HTTP ${response.status} ${response.statusText}`;
3869
- throw new Error(`Audio transcription failed for ${attachment.name}: ${message}`);
4277
+ throw new OpenAiTranscriptionHttpError(response.status, `Audio transcription failed for ${attachment.name}: ${message}`);
3870
4278
  }
3871
4279
  const text = typeof payload.text === "string" ? payload.text.trim() : "";
3872
4280
  return this.normalizeTranscriptText(text);
@@ -3911,12 +4319,38 @@ function buildLocalWhisperCommand(template, inputPath) {
3911
4319
  function shellEscape(value) {
3912
4320
  return `'${value.replace(/'/g, `'"'"'`)}'`;
3913
4321
  }
4322
+ function isRetryableOpenAiError(error) {
4323
+ if (error instanceof OpenAiTranscriptionHttpError) {
4324
+ return RETRYABLE_OPENAI_STATUS.has(error.status);
4325
+ }
4326
+ if (error instanceof Error && error.name === "AbortError") {
4327
+ return true;
4328
+ }
4329
+ return true;
4330
+ }
4331
+ async function sleep2(delayMs) {
4332
+ if (delayMs <= 0) {
4333
+ return;
4334
+ }
4335
+ await new Promise((resolve) => {
4336
+ const timer = setTimeout(resolve, delayMs);
4337
+ timer.unref?.();
4338
+ });
4339
+ }
3914
4340
  function formatError3(error) {
3915
4341
  if (error instanceof Error) {
3916
4342
  return error.message;
3917
4343
  }
3918
4344
  return String(error);
3919
4345
  }
4346
+ var OpenAiTranscriptionHttpError = class extends Error {
4347
+ status;
4348
+ constructor(status, message) {
4349
+ super(message);
4350
+ this.name = "OpenAiTranscriptionHttpError";
4351
+ this.status = status;
4352
+ }
4353
+ };
3920
4354
 
3921
4355
  // src/compat/cli-compat-recorder.ts
3922
4356
  var import_node_fs6 = __toESM(require("fs"));
@@ -4814,6 +5248,9 @@ var Orchestrator = class {
4814
5248
  audioTranscribeModel: "gpt-4o-mini-transcribe",
4815
5249
  audioTranscribeTimeoutMs: 12e4,
4816
5250
  audioTranscribeMaxChars: 6e3,
5251
+ audioTranscribeMaxRetries: 1,
5252
+ audioTranscribeRetryDelayMs: 800,
5253
+ audioTranscribeMaxBytes: 26214400,
4817
5254
  audioLocalWhisperCommand: null,
4818
5255
  audioLocalWhisperTimeoutMs: 18e4,
4819
5256
  recordPath: null
@@ -4825,6 +5262,8 @@ var Orchestrator = class {
4825
5262
  model: this.cliCompat.audioTranscribeModel,
4826
5263
  timeoutMs: this.cliCompat.audioTranscribeTimeoutMs,
4827
5264
  maxChars: this.cliCompat.audioTranscribeMaxChars,
5265
+ maxRetries: this.cliCompat.audioTranscribeMaxRetries,
5266
+ retryDelayMs: this.cliCompat.audioTranscribeRetryDelayMs,
4828
5267
  localWhisperCommand: this.cliCompat.audioLocalWhisperCommand,
4829
5268
  localWhisperTimeoutMs: this.cliCompat.audioLocalWhisperTimeoutMs
4830
5269
  });
@@ -5661,35 +6100,73 @@ var Orchestrator = class {
5661
6100
  if (!this.audioTranscriber.isEnabled()) {
5662
6101
  return [];
5663
6102
  }
5664
- const audioAttachments = message.attachments.filter((attachment) => attachment.kind === "audio" && Boolean(attachment.localPath)).map((attachment) => ({
5665
- name: attachment.name,
5666
- mimeType: attachment.mimeType,
5667
- localPath: attachment.localPath
5668
- }));
5669
- if (audioAttachments.length === 0) {
6103
+ const rawAudioAttachments = message.attachments.filter(
6104
+ (attachment) => attachment.kind === "audio" && Boolean(attachment.localPath)
6105
+ );
6106
+ if (rawAudioAttachments.length === 0) {
5670
6107
  return [];
5671
6108
  }
5672
- try {
5673
- const transcripts = await this.audioTranscriber.transcribeMany(audioAttachments);
5674
- if (transcripts.length > 0) {
5675
- this.logger.info("Audio transcription completed", {
6109
+ const maxBytes = this.cliCompat.audioTranscribeMaxBytes;
6110
+ const audioAttachments = [];
6111
+ let skippedTooLarge = 0;
6112
+ for (const attachment of rawAudioAttachments) {
6113
+ const localPath = attachment.localPath;
6114
+ const sizeBytes = await this.resolveAudioAttachmentSizeBytes(attachment.sizeBytes, localPath);
6115
+ if (sizeBytes !== null && sizeBytes > maxBytes) {
6116
+ skippedTooLarge += 1;
6117
+ this.logger.warn("Skip audio transcription for oversized attachment", {
5676
6118
  requestId,
5677
6119
  sessionKey,
5678
- attachmentCount: audioAttachments.length,
5679
- transcriptCount: transcripts.length
6120
+ name: attachment.name,
6121
+ sizeBytes,
6122
+ maxBytes
5680
6123
  });
6124
+ continue;
5681
6125
  }
6126
+ audioAttachments.push({
6127
+ name: attachment.name,
6128
+ mimeType: attachment.mimeType,
6129
+ localPath
6130
+ });
6131
+ }
6132
+ if (audioAttachments.length === 0) {
6133
+ return [];
6134
+ }
6135
+ const startedAt = Date.now();
6136
+ try {
6137
+ const transcripts = await this.audioTranscriber.transcribeMany(audioAttachments);
6138
+ this.logger.info("Audio transcription completed", {
6139
+ requestId,
6140
+ sessionKey,
6141
+ attachmentCount: audioAttachments.length,
6142
+ transcriptCount: transcripts.length,
6143
+ skippedTooLarge,
6144
+ durationMs: Date.now() - startedAt
6145
+ });
5682
6146
  return transcripts;
5683
6147
  } catch (error) {
5684
6148
  this.logger.warn("Audio transcription failed, continuing without transcripts", {
5685
6149
  requestId,
5686
6150
  sessionKey,
5687
6151
  attachmentCount: audioAttachments.length,
6152
+ skippedTooLarge,
6153
+ durationMs: Date.now() - startedAt,
5688
6154
  error: formatError4(error)
5689
6155
  });
5690
6156
  return [];
5691
6157
  }
5692
6158
  }
6159
+ async resolveAudioAttachmentSizeBytes(sizeBytes, localPath) {
6160
+ if (sizeBytes !== null) {
6161
+ return sizeBytes;
6162
+ }
6163
+ try {
6164
+ const stats = await import_promises5.default.stat(localPath);
6165
+ return stats.size;
6166
+ } catch {
6167
+ return null;
6168
+ }
6169
+ }
5693
6170
  buildExecutionPrompt(prompt, message, audioTranscripts) {
5694
6171
  if (message.attachments.length === 0 && audioTranscripts.length === 0) {
5695
6172
  return prompt;
@@ -6590,6 +7067,9 @@ var configSchema = import_zod.z.object({
6590
7067
  CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL: import_zod.z.string().default("gpt-4o-mini-transcribe"),
6591
7068
  CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS: import_zod.z.string().default("120000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
6592
7069
  CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS: import_zod.z.string().default("6000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
7070
+ CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES: import_zod.z.string().default("1").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(0).max(10)),
7071
+ CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS: import_zod.z.string().default("800").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().nonnegative()),
7072
+ CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES: import_zod.z.string().default("26214400").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
6593
7073
  CLI_COMPAT_AUDIO_LOCAL_WHISPER_COMMAND: import_zod.z.string().default(""),
6594
7074
  CLI_COMPAT_AUDIO_LOCAL_WHISPER_TIMEOUT_MS: import_zod.z.string().default("180000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
6595
7075
  CLI_COMPAT_RECORD_PATH: import_zod.z.string().default(""),
@@ -6656,6 +7136,9 @@ var configSchema = import_zod.z.object({
6656
7136
  audioTranscribeModel: v.CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL.trim() || "gpt-4o-mini-transcribe",
6657
7137
  audioTranscribeTimeoutMs: v.CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS,
6658
7138
  audioTranscribeMaxChars: v.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS,
7139
+ audioTranscribeMaxRetries: v.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES,
7140
+ audioTranscribeRetryDelayMs: v.CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS,
7141
+ audioTranscribeMaxBytes: v.CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES,
6659
7142
  audioLocalWhisperCommand: v.CLI_COMPAT_AUDIO_LOCAL_WHISPER_COMMAND.trim() ? v.CLI_COMPAT_AUDIO_LOCAL_WHISPER_COMMAND.trim() : null,
6660
7143
  audioLocalWhisperTimeoutMs: v.CLI_COMPAT_AUDIO_LOCAL_WHISPER_TIMEOUT_MS,
6661
7144
  recordPath: v.CLI_COMPAT_RECORD_PATH.trim() ? import_node_path12.default.resolve(v.CLI_COMPAT_RECORD_PATH) : null
@@ -6903,6 +7386,9 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
6903
7386
  "CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL",
6904
7387
  "CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS",
6905
7388
  "CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS",
7389
+ "CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES",
7390
+ "CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS",
7391
+ "CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES",
6906
7392
  "CLI_COMPAT_AUDIO_LOCAL_WHISPER_COMMAND",
6907
7393
  "CLI_COMPAT_AUDIO_LOCAL_WHISPER_TIMEOUT_MS",
6908
7394
  "CLI_COMPAT_RECORD_PATH",
@@ -6979,6 +7465,16 @@ var envSnapshotSchema = import_zod2.z.object({
6979
7465
  CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS: integerStringSchema("CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS", 1).default(
6980
7466
  "6000"
6981
7467
  ),
7468
+ CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES: integerStringSchema("CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES", 0, 10).default(
7469
+ "1"
7470
+ ),
7471
+ CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS: integerStringSchema(
7472
+ "CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS",
7473
+ 0
7474
+ ).default("800"),
7475
+ CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES: integerStringSchema("CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES", 1).default(
7476
+ "26214400"
7477
+ ),
6982
7478
  CLI_COMPAT_AUDIO_LOCAL_WHISPER_COMMAND: import_zod2.z.string().default(""),
6983
7479
  CLI_COMPAT_AUDIO_LOCAL_WHISPER_TIMEOUT_MS: integerStringSchema(
6984
7480
  "CLI_COMPAT_AUDIO_LOCAL_WHISPER_TIMEOUT_MS",
@@ -7178,6 +7674,9 @@ function buildSnapshotEnv(config) {
7178
7674
  CLI_COMPAT_AUDIO_TRANSCRIBE_MODEL: config.cliCompat.audioTranscribeModel,
7179
7675
  CLI_COMPAT_AUDIO_TRANSCRIBE_TIMEOUT_MS: String(config.cliCompat.audioTranscribeTimeoutMs),
7180
7676
  CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_CHARS: String(config.cliCompat.audioTranscribeMaxChars),
7677
+ CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_RETRIES: String(config.cliCompat.audioTranscribeMaxRetries),
7678
+ CLI_COMPAT_AUDIO_TRANSCRIBE_RETRY_DELAY_MS: String(config.cliCompat.audioTranscribeRetryDelayMs),
7679
+ CLI_COMPAT_AUDIO_TRANSCRIBE_MAX_BYTES: String(config.cliCompat.audioTranscribeMaxBytes),
7181
7680
  CLI_COMPAT_AUDIO_LOCAL_WHISPER_COMMAND: config.cliCompat.audioLocalWhisperCommand ?? "",
7182
7681
  CLI_COMPAT_AUDIO_LOCAL_WHISPER_TIMEOUT_MS: String(config.cliCompat.audioLocalWhisperTimeoutMs),
7183
7682
  CLI_COMPAT_RECORD_PATH: config.cliCompat.recordPath ?? "",