dsh-any-agent 0.6.2 → 0.6.3

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/src/live-view.mjs CHANGED
@@ -612,16 +612,101 @@ export function clipLine(s, cols) {
612
612
  return out + '…\x1b[0m';
613
613
  }
614
614
 
615
+ // ── 界面语言 ────────────────────────────────────────────────
616
+
617
+ /** watch / dashboard 命令行界面的两份文案。函数式条目负责插值。 */
618
+ export const TEXT = {
619
+ zh: {
620
+ header: (c, r) => `DSH live · ${c} 会话 · ${r} 运行中`,
621
+ queued: (n) => `${n} 排队`,
622
+ pending: (n) => `${n} 待审批`,
623
+ escalations: (n) => `⚠ ${n} 次提权`,
624
+ tokens: (i, o) => `token 入 ${i} / 出 ${o}`,
625
+ unknownClient: '未知客户端',
626
+ exited: ' (已退出)',
627
+ running: '运行中',
628
+ queuedState: '排队',
629
+ closed: '已释放',
630
+ idle: '空闲',
631
+ total: (i, o) => ` · 累计 ${i}/${o}`,
632
+ context: (p) => ` · 上下文 ${p}%`,
633
+ untitled: '(无标题)',
634
+ tools: (n) => `${n} 工具`,
635
+ runEscalations: (n) => `⚠${n} 提权`,
636
+ earlier: (n) => `…前面还有 ${n} 条`,
637
+ noMatch: (q) => `没有匹配 ${q} 的会话。`,
638
+ empty: '还没有任何事件。派一次活(dsh_send)之后这里就会出现。',
639
+ approval: '审批',
640
+ waiting: ' → 等待裁决',
641
+ interject: '插话',
642
+ interrupt: '■ 中断',
643
+ hiddenRows: (n) => `…还有 ${n} 行没显示(给会话 id 前缀、或用 --runs / --items 收窄)`,
644
+ footer: (time, dir) => `更新于 ${time} · live 目录: ${dir} · Ctrl+C 退出`,
645
+ },
646
+ en: {
647
+ header: (c, r) => `DSH live · ${c} conversation${c === 1 ? '' : 's'} · ${r} running`,
648
+ queued: (n) => `${n} queued`,
649
+ pending: (n) => `${n} awaiting approval`,
650
+ escalations: (n) => `⚠ ${n} escalation${n === 1 ? '' : 's'}`,
651
+ tokens: (i, o) => `tokens in ${i} / out ${o}`,
652
+ unknownClient: 'unknown client',
653
+ exited: ' (exited)',
654
+ running: 'running',
655
+ queuedState: 'queued',
656
+ closed: 'released',
657
+ idle: 'idle',
658
+ total: (i, o) => ` · total ${i}/${o}`,
659
+ context: (p) => ` · context ${p}%`,
660
+ untitled: '(untitled)',
661
+ tools: (n) => `${n} tool${n === 1 ? '' : 's'}`,
662
+ runEscalations: (n) => `⚠${n} escalation${n === 1 ? '' : 's'}`,
663
+ earlier: (n) => `…${n} earlier`,
664
+ noMatch: (q) => `No conversation matches ${q}.`,
665
+ empty: 'No events yet. Dispatch something (dsh_send) and it shows up here.',
666
+ approval: 'approval',
667
+ waiting: ' → awaiting decision',
668
+ interject: 'interject',
669
+ interrupt: '■ interrupted',
670
+ hiddenRows: (n) => `…${n} more line${n === 1 ? '' : 's'} hidden (pass a conversation id prefix, or narrow with --runs / --items)`,
671
+ footer: (time, dir) => `updated ${time} · live dir: ${dir} · Ctrl+C to quit`,
672
+ },
673
+ };
674
+
675
+ /**
676
+ * 界面语言:显式 `zh` / `en` 优先;否则看 DSH_MCP_LANG,再看 LC_ALL / LC_MESSAGES / LANG,
677
+ * 最后看系统区域设置(Windows 上通常没有 LANG)。以 zh 开头 → 中文,其余 → 英文。
678
+ */
679
+ export function detectLang(explicit = null, env = process.env, locale = null) {
680
+ const pick = (v) => (typeof v === 'string' && v.trim() ? v.trim().toLowerCase() : null);
681
+ const norm = (v) => (v.startsWith('zh') ? 'zh' : 'en');
682
+ const direct = pick(explicit) ?? pick(env.DSH_MCP_LANG);
683
+ if (direct === 'zh' || direct === 'en') return direct;
684
+ if (direct) return norm(direct);
685
+ const fromEnv = pick(env.LC_ALL) ?? pick(env.LC_MESSAGES) ?? pick(env.LANG);
686
+ if (fromEnv && fromEnv !== 'c' && fromEnv !== 'posix') return norm(fromEnv);
687
+ let sys = locale;
688
+ if (!sys) {
689
+ try {
690
+ sys = Intl.DateTimeFormat().resolvedOptions().locale;
691
+ } catch {
692
+ sys = 'en';
693
+ }
694
+ }
695
+ return norm(String(sys).toLowerCase());
696
+ }
697
+
698
+ const T = (lang) => TEXT[lang] ?? TEXT.zh;
699
+
615
700
  /**
616
701
  * 把一帧文本排成"适合这个终端"的行:截宽、截高(超出的行数在最后一行说明)。
617
702
  * 纯函数,方便测试。
618
703
  */
619
- export function layoutFrame(text, { cols = 120, rows = 40 } = {}) {
704
+ export function layoutFrame(text, { cols = 120, rows = 40, lang = 'zh' } = {}) {
620
705
  let lines = String(text).split('\n').map((l) => clipLine(l, cols));
621
706
  if (rows > 2 && lines.length > rows) {
622
707
  const hidden = lines.length - (rows - 1);
623
708
  lines = lines.slice(0, rows - 1);
624
- lines.push(clipLine(`\x1b[2m…还有 ${hidden} 行没显示(给会话 id 前缀、或用 --runs / --items 收窄)\x1b[0m`, cols));
709
+ lines.push(clipLine(`\x1b[2m${T(lang).hiddenRows(hidden)}\x1b[0m`, cols));
625
710
  }
626
711
  return lines;
627
712
  }
@@ -677,8 +762,10 @@ function clip(s, n) {
677
762
  * @param {number} [opts.runs] 每个会话显示最近几轮(默认 3)
678
763
  * @param {number} [opts.items] 每轮显示最后几条工具/审批(默认 12)
679
764
  * @param {boolean} [opts.color] ANSI 颜色
765
+ * @param {'zh'|'en'} [opts.lang] 界面语言(默认中文)
680
766
  */
681
- export function renderText(view, { conv = null, runs = 3, items = 12, color = false } = {}) {
767
+ export function renderText(view, { conv = null, runs = 3, items = 12, color = false, lang = 'zh' } = {}) {
768
+ const L = T(lang);
682
769
  const c = (code, s) => (color ? `\x1b[${code}m${s}\x1b[0m` : s);
683
770
  const dim = (s) => c('2', s);
684
771
  const warn = (s) => c('33', s);
@@ -687,27 +774,27 @@ export function renderText(view, { conv = null, runs = 3, items = 12, color = fa
687
774
  const out = [];
688
775
  const t = view.totals;
689
776
  out.push(
690
- `DSH live · ${t.conversations} 会话 · ${t.running} 运行中` +
691
- (t.queued ? ` · ${t.queued} 排队` : '') +
692
- (t.pending_approvals ? ` · ${warn(`${t.pending_approvals} 待审批`)}` : '') +
693
- (t.escalations ? ` · ${warn(`⚠ ${t.escalations} 次提权`)}` : '') +
694
- (t.tokens ? ` · token 入 ${fmtTokens(tokenInputTotal(t.tokens))} / 出 ${fmtTokens(t.tokens.output)}` : ''),
777
+ L.header(t.conversations, t.running) +
778
+ (t.queued ? ` · ${L.queued(t.queued)}` : '') +
779
+ (t.pending_approvals ? ` · ${warn(L.pending(t.pending_approvals))}` : '') +
780
+ (t.escalations ? ` · ${warn(L.escalations(t.escalations))}` : '') +
781
+ (t.tokens ? ` · ${L.tokens(fmtTokens(tokenInputTotal(t.tokens)), fmtTokens(t.tokens.output))}` : ''),
695
782
  );
696
783
  let shown = 0;
697
784
  for (const g of view.groups) {
698
785
  const convs = conv ? g.conversations.filter((x) => x.id.startsWith(conv)) : g.conversations;
699
786
  if (!convs.length) continue;
700
787
  out.push('');
701
- out.push(`${g.client ?? '未知客户端'} ${dim(`pid ${g.pid ?? '?'}${g.alive ? '' : ' (已退出)'}`)}`);
788
+ out.push(`${g.client ?? L.unknownClient} ${dim(`pid ${g.pid ?? '?'}${g.alive ? '' : L.exited}`)}`);
702
789
  convs.forEach((cv, ci) => {
703
790
  shown++;
704
791
  const lastConv = ci === convs.length - 1;
705
792
  const p1 = lastConv ? '└─ ' : '├─ ';
706
793
  const p1c = lastConv ? ' ' : '│ ';
707
- const stateTxt = cv.state === 'running' ? good('运行中') : cv.state === 'queued' ? warn('排队') : dim(cv.state === 'closed' ? '已释放' : '空闲');
708
- const ctx = cv.context && cv.context.window ? ` · 上下文 ${Math.round((cv.context.used / cv.context.window) * 100)}%` : '';
709
- const tok = cv.tokens ? ` · 累计 ${fmtTokens(tokenInputTotal(cv.tokens))}/${fmtTokens(cv.tokens.output)}` : '';
710
- out.push(`${p1}${shortId(cv.id)} ${clip(cv.title ?? '(无标题)', 40)} ${stateTxt}${dim(tok + ctx)} ${dim(`${cv.permission ?? ''} · ${clip(cv.cwd ?? '', 50)}`)}`);
794
+ const stateTxt = cv.state === 'running' ? good(L.running) : cv.state === 'queued' ? warn(L.queuedState) : dim(cv.state === 'closed' ? L.closed : L.idle);
795
+ const ctx = cv.context && cv.context.window ? L.context(Math.round((cv.context.used / cv.context.window) * 100)) : '';
796
+ const tok = cv.tokens ? L.total(fmtTokens(tokenInputTotal(cv.tokens)), fmtTokens(cv.tokens.output)) : '';
797
+ out.push(`${p1}${shortId(cv.id)} ${clip(cv.title ?? L.untitled, 40)} ${stateTxt}${dim(tok + ctx)} ${dim(`${cv.permission ?? ''} · ${clip(cv.cwd ?? '', 50)}`)}`);
711
798
  const rs = cv.runs.slice(-runs);
712
799
  rs.forEach((r, ri) => {
713
800
  const lastRun = ri === rs.length - 1;
@@ -719,20 +806,20 @@ export function renderText(view, { conv = null, runs = 3, items = 12, color = fa
719
806
  r.via ? `[${r.via}]` : null,
720
807
  r.status,
721
808
  r.duration_ms != null ? fmtMs(r.duration_ms) : null,
722
- r.tool_count != null ? `${r.tool_count} 工具` : null,
809
+ r.tool_count != null ? L.tools(r.tool_count) : null,
723
810
  r.tokens ? `${fmtTokens(tokenInputTotal(r.tokens))}↑ ${fmtTokens(r.tokens.output)}↓ tok` : null,
724
- r.escalations ? warn(`⚠${r.escalations} 提权`) : null,
725
- r.pending_approvals ? warn(`${r.pending_approvals} 待审批`) : null,
811
+ r.escalations ? warn(L.runEscalations(r.escalations)) : null,
812
+ r.pending_approvals ? warn(L.pending(r.pending_approvals)) : null,
726
813
  ].filter(Boolean);
727
814
  out.push(`${p2}${markC} ${r.run} ${meta.join(' · ')}${r.prompt_preview ? ` ${dim(`“${clip(r.prompt_preview, 60)}”`)}` : ''}`);
728
815
  if (r.error) out.push(`${p2c} ${bad(clip(r.error, 100))}`);
729
816
  const its = r.items.slice(-items);
730
- if (r.items.length > its.length) out.push(`${p2c} ${dim(`…前面还有 ${r.items.length - its.length} 条`)}`);
731
- for (const it of its) out.push(p2c + ' ' + renderItem(it, { warn, bad, good, dim }, cv.cwd));
817
+ if (r.items.length > its.length) out.push(`${p2c} ${dim(L.earlier(r.items.length - its.length))}`);
818
+ for (const it of its) out.push(p2c + ' ' + renderItem(it, { warn, bad, good, dim }, cv.cwd, L));
732
819
  });
733
820
  });
734
821
  }
735
- if (!shown) out.push('', dim(conv ? `没有匹配 ${conv} 的会话。` : '还没有任何事件。派一次活(dsh_send)之后这里就会出现。'));
822
+ if (!shown) out.push('', dim(conv ? L.noMatch(conv) : L.empty));
736
823
  return out.join('\n');
737
824
  }
738
825
 
@@ -745,18 +832,18 @@ export function relToCwd(text, cwd) {
745
832
  return out.split(root).join('.');
746
833
  }
747
834
 
748
- function renderItem(it, { warn, bad, good, dim }, cwd) {
835
+ function renderItem(it, { warn, bad, good, dim }, cwd, L = TEXT.zh) {
749
836
  if (it.kind === 'tool') {
750
837
  const st = it.status === 'completed' ? good('✓') : it.status === 'failed' ? bad('✗') : it.status === 'unfinished' ? dim('·') : warn('▶');
751
838
  const what = relToCwd(it.input_preview ?? (it.title !== it.name ? it.title : '') ?? '', cwd);
752
839
  return `${st} ${it.name ?? it.title ?? it.id ?? 'tool'}${what ? ` ${dim(clip(what, 70))}` : ''}${it.elapsed_ms != null ? ` ${dim(fmtMs(it.elapsed_ms))}` : ''}`;
753
840
  }
754
841
  if (it.kind === 'approval') {
755
- const decided = it.decided ? ` → ${it.decided.decision ?? it.decided.effective ?? ''}${it.decided.by ? `(${it.decided.by})` : ''}` : it.mode === 'ask' ? warn(' → 等待裁决') : '';
842
+ const decided = it.decided ? ` → ${it.decided.decision ?? it.decided.effective ?? ''}${it.decided.by ? `(${it.decided.by})` : ''}` : it.mode === 'ask' ? warn(L.waiting) : '';
756
843
  const esc = it.escalation ? warn(` ⚠ ${it.escalation}`) : '';
757
- return `${warn('◆')} 审批 ${it.mode ?? ''}${esc}${decided}${it.command_preview ? ` ${dim(clip(relToCwd(it.command_preview, cwd), 60))}` : ''}`;
844
+ return `${warn('◆')} ${L.approval} ${it.mode ?? ''}${esc}${decided}${it.command_preview ? ` ${dim(clip(relToCwd(it.command_preview, cwd), 60))}` : ''}`;
758
845
  }
759
- if (it.kind === 'interject') return `${warn('↪')} 插话(${it.mode ?? ''})${it.text_preview ? ` ${dim(clip(it.text_preview, 60))}` : ''}`;
760
- if (it.kind === 'interrupt') return bad('■ 中断');
846
+ if (it.kind === 'interject') return `${warn('↪')} ${L.interject}(${it.mode ?? ''})${it.text_preview ? ` ${dim(clip(it.text_preview, 60))}` : ''}`;
847
+ if (it.kind === 'interrupt') return bad(L.interrupt);
761
848
  return dim(it.kind);
762
849
  }