cc-viewer 1.6.294 → 1.6.296

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 (40) hide show
  1. package/cli.js +7 -2
  2. package/dist/assets/App-BeCGow-I.js +2 -0
  3. package/dist/assets/{MdxEditorPanel-B8xrlDZJ.js → MdxEditorPanel-D52b5qxi.js} +1 -1
  4. package/dist/assets/{Mobile-fsi8-Lpb.js → Mobile-8fflztx7.js} +1 -1
  5. package/dist/assets/index-DtpelJc4.js +2 -0
  6. package/dist/assets/seqResourceLoaders-DM-48tr-.js +2 -0
  7. package/dist/index.html +1 -1
  8. package/findcc.js +3 -3
  9. package/package.json +1 -1
  10. package/server/i18n.js +224 -8
  11. package/server/interceptor.js +23 -19
  12. package/server/lib/adapters/dingtalk-adapter.js +62 -0
  13. package/server/lib/adapters/discord-adapter.js +35 -0
  14. package/server/lib/adapters/feishu-adapter.js +37 -0
  15. package/server/lib/ask-store.js +19 -90
  16. package/server/lib/async-file-lock.js +123 -0
  17. package/server/lib/async-write-queue.js +131 -0
  18. package/server/lib/git-diff.js +4 -1
  19. package/server/lib/im-bridge-core.js +119 -14
  20. package/server/lib/im-config.js +11 -6
  21. package/server/lib/im-process-manager.js +1 -1
  22. package/server/lib/jsonl-archive.js +0 -1
  23. package/server/lib/log-management.js +46 -99
  24. package/server/lib/log-stream.js +102 -8
  25. package/server/lib/log-watcher.js +231 -178
  26. package/server/lib/plugin-manager.js +1 -1
  27. package/server/lib/updater.js +4 -2
  28. package/server/pty-manager.js +1 -1
  29. package/server/routes/ask-perm.js +2 -2
  30. package/server/routes/dingtalk.js +2 -0
  31. package/server/routes/events.js +3 -3
  32. package/server/routes/files-fs.js +4 -4
  33. package/server/routes/logs.js +5 -5
  34. package/server/routes/project-meta.js +18 -1
  35. package/server/routes/workspaces.js +10 -13
  36. package/server/server.js +33 -25
  37. package/server/workspace-registry.js +26 -72
  38. package/dist/assets/App-C66LoBEz.js +0 -2
  39. package/dist/assets/index-BTZqk5O5.js +0 -2
  40. package/dist/assets/seqResourceLoaders-6k4uXcNn.js +0 -2
package/dist/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  // 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
22
22
  // electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
23
23
  </script>
24
- <script type="module" crossorigin src="/assets/index-BTZqk5O5.js"></script>
24
+ <script type="module" crossorigin src="/assets/index-DtpelJc4.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="/assets/vendor-antd-Bur5ZxWE.js">
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-Si44UqBp.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-Cco3AQJS.js">
package/findcc.js CHANGED
@@ -105,7 +105,7 @@ export const LEGACY_INJECT_IMPORTS = [
105
105
 
106
106
  export function getGlobalNodeModulesDir() {
107
107
  try {
108
- return execSync('npm root -g', { encoding: 'utf-8' }).trim();
108
+ return execSync('npm root -g', { encoding: 'utf-8', windowsHide: true }).trim();
109
109
  } catch {
110
110
  return null;
111
111
  }
@@ -143,7 +143,7 @@ export function resolveNpmClaudePath() {
143
143
  for (const cmd of lookupCmds) {
144
144
  try {
145
145
  // Windows `where` 输出可能多行 CRLF,取第一行 trim 即可
146
- const rawOut = execSync(cmd, { encoding: 'utf-8', shell: true, env: process.env });
146
+ const rawOut = execSync(cmd, { encoding: 'utf-8', shell: true, env: process.env, windowsHide: true });
147
147
  const result = rawOut.split(/\r?\n/)[0].trim();
148
148
  if (result && existsSync(result)) {
149
149
  // 只接受 npm 安装的符号链接(解析后指向 node_modules)
@@ -200,7 +200,7 @@ export function resolveNativePath() {
200
200
  : [`which ${BINARY_NAME}`, `command -v ${BINARY_NAME}`];
201
201
  for (const cmd of lookupCmds) {
202
202
  try {
203
- const rawOut = execSync(cmd, { encoding: 'utf-8', shell: true, env: process.env });
203
+ const rawOut = execSync(cmd, { encoding: 'utf-8', shell: true, env: process.env, windowsHide: true });
204
204
  const result = rawOut.split(/\r?\n/)[0].trim();
205
205
  if (result && existsSync(result)) {
206
206
  // 只排除 .js 文件(老版本 npm 分发的 cli.js,需要 node 运行,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.294",
3
+ "version": "1.6.296",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
package/server/i18n.js CHANGED
@@ -634,11 +634,13 @@ const i18nData = {
634
634
  "server.dingtalk.noSession": { "zh": "当前没有活跃的 Claude 会话,无法处理。", "en": "No active Claude session — cannot process." },
635
635
  "server.dingtalk.notBound": { "zh": "该会话未绑定到本机,已忽略。", "en": "This conversation is not bound; ignored." },
636
636
  "server.dingtalk.notAuthorized": { "zh": "你不在允许的发送人白名单内。", "en": "You are not in the allowed sender list." },
637
- "server.dingtalk.busyQueued": { "zh": "Claude 正在忙,你的消息已排队。", "en": "Claude is busy; your message is queued." },
637
+ "server.dingtalk.busyQueued": { "zh": "Claude 正在忙,你的消息已排队(前方 {ahead} 条待处理,上限 {max})。", "en": "Claude is busy; your message is queued ({ahead} ahead, max {max})." },
638
638
  "server.dingtalk.skipPermWarning": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
639
639
  "server.dingtalk.skipPermBlocked": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
640
640
  "server.dingtalk.injectFailed": { "zh": "注入失败(会话已结束或不可用),请确认 Claude 会话仍在运行。", "en": "Injection failed (session ended or unavailable) — check the Claude session is still running." },
641
- "server.dingtalk.queueFull": { "zh": "消息队列已满,已丢弃该消息,请稍后再发。", "en": "Message queue is full; this message was dropped. Try again later." },
641
+ "server.dingtalk.queueFull": { "zh": "消息队列已满({max}),已丢弃该消息,请稍后再发。", "en": "Message queue is full ({max}); this message was dropped. Try again later." },
642
+ "server.dingtalk.ackProcessing": { "zh": "收到,正在思考…", "en": "Got it, thinking…" },
643
+ "server.dingtalk.ackTimeout": { "zh": "处理超时,请重试。", "en": "Processing timed out, please try again." },
642
644
  "server.feishu.replyChunkTitle": { "zh": "Claude", "en": "Claude" },
643
645
  "server.feishu.truncated": { "zh": "(回复过长已截断,完整内容请见 Web 界面)", "en": "(reply truncated — see the web UI for the full content)" },
644
646
  "server.feishu.noTextReply": { "zh": "(本轮无文本回复)", "en": "(no text reply this turn)" },
@@ -646,11 +648,13 @@ const i18nData = {
646
648
  "server.feishu.noSession": { "zh": "当前没有活跃的 Claude 会话,无法处理。", "en": "No active Claude session — cannot process." },
647
649
  "server.feishu.notBound": { "zh": "该会话未绑定到本机,已忽略。", "en": "This conversation is not bound; ignored." },
648
650
  "server.feishu.notAuthorized": { "zh": "你不在允许的发送人白名单内。", "en": "You are not in the allowed sender list." },
649
- "server.feishu.busyQueued": { "zh": "Claude 正在忙,你的消息已排队。", "en": "Claude is busy; your message is queued." },
651
+ "server.feishu.busyQueued": { "zh": "Claude 正在忙,你的消息已排队(前方 {ahead} 条待处理,上限 {max})。", "en": "Claude is busy; your message is queued ({ahead} ahead, max {max})." },
650
652
  "server.feishu.skipPermWarning": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
651
653
  "server.feishu.skipPermBlocked": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
652
654
  "server.feishu.injectFailed": { "zh": "注入失败(会话已结束或不可用),请确认 Claude 会话仍在运行。", "en": "Injection failed (session ended or unavailable) — check the Claude session is still running." },
653
- "server.feishu.queueFull": { "zh": "消息队列已满,已丢弃该消息,请稍后再发。", "en": "Message queue is full; this message was dropped. Try again later." },
655
+ "server.feishu.queueFull": { "zh": "消息队列已满({max}),已丢弃该消息,请稍后再发。", "en": "Message queue is full ({max}); this message was dropped. Try again later." },
656
+ "server.feishu.ackProcessing": { "zh": "收到,正在思考…", "en": "Got it, thinking…" },
657
+ "server.feishu.ackTimeout": { "zh": "处理超时,请重试。", "en": "Processing timed out, please try again." },
654
658
  "server.wecom.replyChunkTitle": { "zh": "Claude", "en": "Claude" },
655
659
  "server.wecom.truncated": { "zh": "(回复过长已截断,完整内容请见 Web 界面)", "en": "(reply truncated — see the web UI for the full content)" },
656
660
  "server.wecom.noTextReply": { "zh": "(本轮无文本回复)", "en": "(no text reply this turn)" },
@@ -658,11 +662,13 @@ const i18nData = {
658
662
  "server.wecom.noSession": { "zh": "当前没有活跃的 Claude 会话,无法处理。", "en": "No active Claude session — cannot process." },
659
663
  "server.wecom.notBound": { "zh": "该会话未绑定到本机,已忽略。", "en": "This conversation is not bound; ignored." },
660
664
  "server.wecom.notAuthorized": { "zh": "你不在允许的发送人白名单内。", "en": "You are not in the allowed sender list." },
661
- "server.wecom.busyQueued": { "zh": "Claude 正在忙,你的消息已排队。", "en": "Claude is busy; your message is queued." },
665
+ "server.wecom.busyQueued": { "zh": "Claude 正在忙,你的消息已排队(前方 {ahead} 条待处理,上限 {max})。", "en": "Claude is busy; your message is queued ({ahead} ahead, max {max})." },
662
666
  "server.wecom.skipPermWarning": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
663
667
  "server.wecom.skipPermBlocked": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
664
668
  "server.wecom.injectFailed": { "zh": "注入失败(会话已结束或不可用),请确认 Claude 会话仍在运行。", "en": "Injection failed (session ended or unavailable) — check the Claude session is still running." },
665
- "server.wecom.queueFull": { "zh": "消息队列已满,已丢弃该消息,请稍后再发。", "en": "Message queue is full; this message was dropped. Try again later." },
669
+ "server.wecom.queueFull": { "zh": "消息队列已满({max}),已丢弃该消息,请稍后再发。", "en": "Message queue is full ({max}); this message was dropped. Try again later." },
670
+ "server.wecom.ackProcessing": { "zh": "收到,正在思考…", "en": "Got it, thinking…" },
671
+ "server.wecom.ackTimeout": { "zh": "处理超时,请重试。", "en": "Processing timed out, please try again." },
666
672
  "server.discord.replyChunkTitle": { "zh": "Claude", "en": "Claude" },
667
673
  "server.discord.truncated": { "zh": "(回复过长已截断,完整内容请见 Web 界面)", "en": "(reply truncated — see the web UI for the full content)" },
668
674
  "server.discord.noTextReply": { "zh": "(本轮无文本回复)", "en": "(no text reply this turn)" },
@@ -670,11 +676,13 @@ const i18nData = {
670
676
  "server.discord.noSession": { "zh": "当前没有活跃的 Claude 会话,无法处理。", "en": "No active Claude session — cannot process." },
671
677
  "server.discord.notBound": { "zh": "该会话未绑定到本机,已忽略。", "en": "This conversation is not bound; ignored." },
672
678
  "server.discord.notAuthorized": { "zh": "你不在允许的发送人白名单内。", "en": "You are not in the allowed sender list." },
673
- "server.discord.busyQueued": { "zh": "Claude 正在忙,你的消息已排队。", "en": "Claude is busy; your message is queued." },
679
+ "server.discord.busyQueued": { "zh": "Claude 正在忙,你的消息已排队(前方 {ahead} 条待处理,上限 {max})。", "en": "Claude is busy; your message is queued ({ahead} ahead, max {max})." },
674
680
  "server.discord.skipPermWarning": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
675
681
  "server.discord.skipPermBlocked": { "zh": "收到,正在思考", "en": "Got it, thinking…" },
676
682
  "server.discord.injectFailed": { "zh": "注入失败(会话已结束或不可用),请确认 Claude 会话仍在运行。", "en": "Injection failed (session ended or unavailable) — check the Claude session is still running." },
677
- "server.discord.queueFull": { "zh": "消息队列已满,已丢弃该消息,请稍后再发。", "en": "Message queue is full; this message was dropped. Try again later." },
683
+ "server.discord.queueFull": { "zh": "消息队列已满({max}),已丢弃该消息,请稍后再发。", "en": "Message queue is full ({max}); this message was dropped. Try again later." },
684
+ "server.discord.ackProcessing": { "zh": "收到,正在思考…", "en": "Got it, thinking…" },
685
+ "server.discord.ackTimeout": { "zh": "处理超时,请重试。", "en": "Processing timed out, please try again." },
678
686
  "update.updating": {
679
687
  "zh": "正在更新到 v{version}...",
680
688
  "en": "Updating to v{version}...",
@@ -2639,6 +2647,214 @@ const i18nData = {
2639
2647
  "ar": "سؤال في {project}", "no": "Spørsmål i {project}", "pt-BR": "Pergunta em {project}",
2640
2648
  "th": "คำถามใน {project}", "tr": "{project} içinde soru", "uk": "Питання у {project}"
2641
2649
  },
2650
+ "electron.menu.file": {
2651
+ "zh": "文件", "en": "File", "zh-TW": "檔案",
2652
+ "ko": "파일", "ja": "ファイル", "de": "Datei",
2653
+ "es": "Archivo", "fr": "Fichier", "it": "File",
2654
+ "da": "Fil", "pl": "Plik", "ru": "Файл",
2655
+ "ar": "ملف", "no": "Fil", "pt-BR": "Arquivo",
2656
+ "th": "ไฟล์", "tr": "Dosya", "uk": "Файл"
2657
+ },
2658
+ "electron.menu.edit": {
2659
+ "zh": "编辑", "en": "Edit", "zh-TW": "編輯",
2660
+ "ko": "편집", "ja": "編集", "de": "Bearbeiten",
2661
+ "es": "Editar", "fr": "Édition", "it": "Modifica",
2662
+ "da": "Rediger", "pl": "Edycja", "ru": "Правка",
2663
+ "ar": "تحرير", "no": "Rediger", "pt-BR": "Editar",
2664
+ "th": "แก้ไข", "tr": "Düzen", "uk": "Редагувати"
2665
+ },
2666
+ "electron.menu.view": {
2667
+ "zh": "视图", "en": "View", "zh-TW": "檢視",
2668
+ "ko": "보기", "ja": "表示", "de": "Ansicht",
2669
+ "es": "Ver", "fr": "Affichage", "it": "Visualizza",
2670
+ "da": "Vis", "pl": "Widok", "ru": "Вид",
2671
+ "ar": "عرض", "no": "Vis", "pt-BR": "Exibir",
2672
+ "th": "มุมมอง", "tr": "Görünüm", "uk": "Вигляд"
2673
+ },
2674
+ "electron.menu.window": {
2675
+ "zh": "窗口", "en": "Window", "zh-TW": "視窗",
2676
+ "ko": "창", "ja": "ウィンドウ", "de": "Fenster",
2677
+ "es": "Ventana", "fr": "Fenêtre", "it": "Finestra",
2678
+ "da": "Vindue", "pl": "Okno", "ru": "Окно",
2679
+ "ar": "نافذة", "no": "Vindu", "pt-BR": "Janela",
2680
+ "th": "หน้าต่าง", "tr": "Pencere", "uk": "Вікно"
2681
+ },
2682
+ "electron.menu.newTab": {
2683
+ "zh": "新建标签页", "en": "New Tab", "zh-TW": "新增分頁",
2684
+ "ko": "새 탭", "ja": "新しいタブ", "de": "Neuer Tab",
2685
+ "es": "Nueva pestaña", "fr": "Nouvel onglet", "it": "Nuova scheda",
2686
+ "da": "Ny fane", "pl": "Nowa karta", "ru": "Новая вкладка",
2687
+ "ar": "علامة تبويب جديدة", "no": "Ny fane", "pt-BR": "Nova aba",
2688
+ "th": "แท็บใหม่", "tr": "Yeni Sekme", "uk": "Нова вкладка"
2689
+ },
2690
+ "electron.menu.closeTab": {
2691
+ "zh": "关闭标签页", "en": "Close Tab", "zh-TW": "關閉分頁",
2692
+ "ko": "탭 닫기", "ja": "タブを閉じる", "de": "Tab schließen",
2693
+ "es": "Cerrar pestaña", "fr": "Fermer l'onglet", "it": "Chiudi scheda",
2694
+ "da": "Luk fane", "pl": "Zamknij kartę", "ru": "Закрыть вкладку",
2695
+ "ar": "إغلاق علامة التبويب", "no": "Lukk fane", "pt-BR": "Fechar aba",
2696
+ "th": "ปิดแท็บ", "tr": "Sekmeyi Kapat", "uk": "Закрити вкладку"
2697
+ },
2698
+ "electron.menu.undo": {
2699
+ "zh": "撤销", "en": "Undo", "zh-TW": "復原",
2700
+ "ko": "실행 취소", "ja": "元に戻す", "de": "Rückgängig",
2701
+ "es": "Deshacer", "fr": "Annuler", "it": "Annulla",
2702
+ "da": "Fortryd", "pl": "Cofnij", "ru": "Отменить",
2703
+ "ar": "تراجع", "no": "Angre", "pt-BR": "Desfazer",
2704
+ "th": "เลิกทำ", "tr": "Geri Al", "uk": "Скасувати"
2705
+ },
2706
+ "electron.menu.redo": {
2707
+ "zh": "重做", "en": "Redo", "zh-TW": "重做",
2708
+ "ko": "다시 실행", "ja": "やり直す", "de": "Wiederholen",
2709
+ "es": "Rehacer", "fr": "Rétablir", "it": "Ripeti",
2710
+ "da": "Gentag", "pl": "Ponów", "ru": "Повторить",
2711
+ "ar": "إعادة", "no": "Gjør om", "pt-BR": "Refazer",
2712
+ "th": "ทำซ้ำ", "tr": "Yinele", "uk": "Повторити"
2713
+ },
2714
+ "electron.menu.cut": {
2715
+ "zh": "剪切", "en": "Cut", "zh-TW": "剪下",
2716
+ "ko": "잘라내기", "ja": "切り取り", "de": "Ausschneiden",
2717
+ "es": "Cortar", "fr": "Couper", "it": "Taglia",
2718
+ "da": "Klip", "pl": "Wytnij", "ru": "Вырезать",
2719
+ "ar": "قص", "no": "Klipp ut", "pt-BR": "Recortar",
2720
+ "th": "ตัด", "tr": "Kes", "uk": "Вирізати"
2721
+ },
2722
+ "electron.menu.copy": {
2723
+ "zh": "复制", "en": "Copy", "zh-TW": "複製",
2724
+ "ko": "복사", "ja": "コピー", "de": "Kopieren",
2725
+ "es": "Copiar", "fr": "Copier", "it": "Copia",
2726
+ "da": "Kopiér", "pl": "Kopiuj", "ru": "Копировать",
2727
+ "ar": "نسخ", "no": "Kopier", "pt-BR": "Copiar",
2728
+ "th": "คัดลอก", "tr": "Kopyala", "uk": "Копіювати"
2729
+ },
2730
+ "electron.menu.paste": {
2731
+ "zh": "粘贴", "en": "Paste", "zh-TW": "貼上",
2732
+ "ko": "붙여넣기", "ja": "貼り付け", "de": "Einfügen",
2733
+ "es": "Pegar", "fr": "Coller", "it": "Incolla",
2734
+ "da": "Sæt ind", "pl": "Wklej", "ru": "Вставить",
2735
+ "ar": "لصق", "no": "Lim inn", "pt-BR": "Colar",
2736
+ "th": "วาง", "tr": "Yapıştır", "uk": "Вставити"
2737
+ },
2738
+ "electron.menu.selectAll": {
2739
+ "zh": "全选", "en": "Select All", "zh-TW": "全選",
2740
+ "ko": "모두 선택", "ja": "すべて選択", "de": "Alles auswählen",
2741
+ "es": "Seleccionar todo", "fr": "Tout sélectionner", "it": "Seleziona tutto",
2742
+ "da": "Vælg alt", "pl": "Zaznacz wszystko", "ru": "Выделить всё",
2743
+ "ar": "تحديد الكل", "no": "Velg alt", "pt-BR": "Selecionar tudo",
2744
+ "th": "เลือกทั้งหมด", "tr": "Tümünü Seç", "uk": "Виділити все"
2745
+ },
2746
+ "electron.menu.reload": {
2747
+ "zh": "重新加载", "en": "Reload", "zh-TW": "重新載入",
2748
+ "ko": "새로 고침", "ja": "再読み込み", "de": "Neu laden",
2749
+ "es": "Recargar", "fr": "Recharger", "it": "Ricarica",
2750
+ "da": "Genindlæs", "pl": "Załaduj ponownie", "ru": "Перезагрузить",
2751
+ "ar": "إعادة تحميل", "no": "Last inn på nytt", "pt-BR": "Recarregar",
2752
+ "th": "โหลดใหม่", "tr": "Yeniden Yükle", "uk": "Перезавантажити"
2753
+ },
2754
+ "electron.menu.forceReload": {
2755
+ "zh": "强制重新加载", "en": "Force Reload", "zh-TW": "強制重新載入",
2756
+ "ko": "강제 새로 고침", "ja": "強制再読み込み", "de": "Erzwungenes Neuladen",
2757
+ "es": "Forzar recarga", "fr": "Forcer le rechargement", "it": "Forza ricarica",
2758
+ "da": "Tving genindlæsning", "pl": "Wymuś ponowne załadowanie", "ru": "Принудительно перезагрузить",
2759
+ "ar": "فرض إعادة التحميل", "no": "Tving omlasting", "pt-BR": "Forçar recarregamento",
2760
+ "th": "บังคับโหลดใหม่", "tr": "Zorla Yeniden Yükle", "uk": "Примусово перезавантажити"
2761
+ },
2762
+ "electron.menu.toggleDevTools": {
2763
+ "zh": "开发者工具", "en": "Developer Tools", "zh-TW": "開發人員工具",
2764
+ "ko": "개발자 도구", "ja": "開発者ツール", "de": "Entwicklertools",
2765
+ "es": "Herramientas de desarrollo", "fr": "Outils de développement", "it": "Strumenti di sviluppo",
2766
+ "da": "Udviklerværktøjer", "pl": "Narzędzia deweloperskie", "ru": "Инструменты разработчика",
2767
+ "ar": "أدوات المطور", "no": "Utviklerverktøy", "pt-BR": "Ferramentas do desenvolvedor",
2768
+ "th": "เครื่องมือนักพัฒนา", "tr": "Geliştirici Araçları", "uk": "Інструменти розробника"
2769
+ },
2770
+ "electron.menu.fullscreen": {
2771
+ "zh": "切换全屏", "en": "Toggle Full Screen", "zh-TW": "切換全螢幕",
2772
+ "ko": "전체 화면 전환", "ja": "フルスクリーン切替", "de": "Vollbild umschalten",
2773
+ "es": "Alternar pantalla completa", "fr": "Basculer en plein écran", "it": "Schermo intero",
2774
+ "da": "Skift fuld skærm", "pl": "Przełącz pełny ekran", "ru": "Полноэкранный режим",
2775
+ "ar": "ملء الشاشة", "no": "Veksle fullskjerm", "pt-BR": "Alternar tela cheia",
2776
+ "th": "สลับเต็มหน้าจอ", "tr": "Tam Ekran", "uk": "Повноекранний режим"
2777
+ },
2778
+ "electron.menu.minimize": {
2779
+ "zh": "最小化", "en": "Minimize", "zh-TW": "最小化",
2780
+ "ko": "최소화", "ja": "最小化", "de": "Minimieren",
2781
+ "es": "Minimizar", "fr": "Réduire", "it": "Riduci a icona",
2782
+ "da": "Minimér", "pl": "Minimalizuj", "ru": "Свернуть",
2783
+ "ar": "تصغير", "no": "Minimer", "pt-BR": "Minimizar",
2784
+ "th": "ย่อเล็กสุด", "tr": "Simge Durumuna Küçült", "uk": "Згорнути"
2785
+ },
2786
+ "electron.menu.maximize": {
2787
+ "zh": "最大化", "en": "Maximize", "zh-TW": "最大化",
2788
+ "ko": "최대화", "ja": "最大化", "de": "Maximieren",
2789
+ "es": "Maximizar", "fr": "Agrandir", "it": "Ingrandisci",
2790
+ "da": "Maksimér", "pl": "Maksymalizuj", "ru": "Развернуть",
2791
+ "ar": "تكبير", "no": "Maksimer", "pt-BR": "Maximizar",
2792
+ "th": "ขยายใหญ่สุด", "tr": "Ekranı Kapla", "uk": "Розгорнути"
2793
+ },
2794
+ "electron.menu.close": {
2795
+ "zh": "关闭窗口", "en": "Close Window", "zh-TW": "關閉視窗",
2796
+ "ko": "창 닫기", "ja": "ウィンドウを閉じる", "de": "Fenster schließen",
2797
+ "es": "Cerrar ventana", "fr": "Fermer la fenêtre", "it": "Chiudi finestra",
2798
+ "da": "Luk vindue", "pl": "Zamknij okno", "ru": "Закрыть окно",
2799
+ "ar": "إغلاق النافذة", "no": "Lukk vindu", "pt-BR": "Fechar janela",
2800
+ "th": "ปิดหน้าต่าง", "tr": "Pencereyi Kapat", "uk": "Закрити вікно"
2801
+ },
2802
+ "electron.menu.prevTab": {
2803
+ "zh": "上一个标签页", "en": "Previous Tab", "zh-TW": "上一個分頁",
2804
+ "ko": "이전 탭", "ja": "前のタブ", "de": "Vorheriger Tab",
2805
+ "es": "Pestaña anterior", "fr": "Onglet précédent", "it": "Scheda precedente",
2806
+ "da": "Forrige fane", "pl": "Poprzednia karta", "ru": "Предыдущая вкладка",
2807
+ "ar": "علامة التبويب السابقة", "no": "Forrige fane", "pt-BR": "Aba anterior",
2808
+ "th": "แท็บก่อนหน้า", "tr": "Önceki Sekme", "uk": "Попередня вкладка"
2809
+ },
2810
+ "electron.menu.nextTab": {
2811
+ "zh": "下一个标签页", "en": "Next Tab", "zh-TW": "下一個分頁",
2812
+ "ko": "다음 탭", "ja": "次のタブ", "de": "Nächster Tab",
2813
+ "es": "Pestaña siguiente", "fr": "Onglet suivant", "it": "Scheda successiva",
2814
+ "da": "Næste fane", "pl": "Następna karta", "ru": "Следующая вкладка",
2815
+ "ar": "علامة التبويب التالية", "no": "Neste fane", "pt-BR": "Próxima aba",
2816
+ "th": "แท็บถัดไป", "tr": "Sonraki Sekme", "uk": "Наступна вкладка"
2817
+ },
2818
+ "electron.menu.copyLink": {
2819
+ "zh": "复制链接地址", "en": "Copy Link Address", "zh-TW": "複製連結網址",
2820
+ "ko": "링크 주소 복사", "ja": "リンクアドレスをコピー", "de": "Linkadresse kopieren",
2821
+ "es": "Copiar dirección del enlace", "fr": "Copier l'adresse du lien", "it": "Copia indirizzo link",
2822
+ "da": "Kopiér linkadresse", "pl": "Kopiuj adres linku", "ru": "Копировать адрес ссылки",
2823
+ "ar": "نسخ عنوان الرابط", "no": "Kopier lenkeadresse", "pt-BR": "Copiar endereço do link",
2824
+ "th": "คัดลอกที่อยู่ลิงก์", "tr": "Bağlantı Adresini Kopyala", "uk": "Копіювати адресу посилання"
2825
+ },
2826
+ "electron.tabbar.newTab": {
2827
+ "zh": "新建项目标签页", "en": "New project tab", "zh-TW": "新增專案分頁",
2828
+ "ko": "새 프로젝트 탭", "ja": "新しいプロジェクトタブ", "de": "Neuer Projekt-Tab",
2829
+ "es": "Nueva pestaña de proyecto", "fr": "Nouvel onglet de projet", "it": "Nuova scheda progetto",
2830
+ "da": "Ny projektfane", "pl": "Nowa karta projektu", "ru": "Новая вкладка проекта",
2831
+ "ar": "علامة تبويب مشروع جديدة", "no": "Ny prosjektfane", "pt-BR": "Nova aba de projeto",
2832
+ "th": "แท็บโปรเจกต์ใหม่", "tr": "Yeni Proje Sekmesi", "uk": "Нова вкладка проєкту"
2833
+ },
2834
+ "electron.tabbar.toIpad": {
2835
+ "zh": "切换到 iPad 模式", "en": "Switch to iPad mode", "zh-TW": "切換到 iPad 模式",
2836
+ "ko": "iPad 모드로 전환", "ja": "iPad モードに切替", "de": "Zu iPad-Modus wechseln",
2837
+ "es": "Cambiar a modo iPad", "fr": "Passer en mode iPad", "it": "Passa alla modalità iPad",
2838
+ "da": "Skift til iPad-tilstand", "pl": "Przełącz na tryb iPad", "ru": "Переключиться в режим iPad",
2839
+ "ar": "التبديل إلى وضع iPad", "no": "Bytt til iPad-modus", "pt-BR": "Mudar para modo iPad",
2840
+ "th": "สลับเป็นโหมด iPad", "tr": "iPad moduna geç", "uk": "Перейти в режим iPad"
2841
+ },
2842
+ "electron.tabbar.toPc": {
2843
+ "zh": "切换到 PC 模式", "en": "Switch to PC mode", "zh-TW": "切換到 PC 模式",
2844
+ "ko": "PC 모드로 전환", "ja": "PC モードに切替", "de": "Zu PC-Modus wechseln",
2845
+ "es": "Cambiar a modo PC", "fr": "Passer en mode PC", "it": "Passa alla modalità PC",
2846
+ "da": "Skift til PC-tilstand", "pl": "Przełącz na tryb PC", "ru": "Переключиться в режим ПК",
2847
+ "ar": "التبديل إلى وضع PC", "no": "Bytt til PC-modus", "pt-BR": "Mudar para modo PC",
2848
+ "th": "สลับเป็นโหมด PC", "tr": "PC moduna geç", "uk": "Перейти в режим ПК"
2849
+ },
2850
+ "electron.tabbar.menu": {
2851
+ "zh": "菜单", "en": "Menu", "zh-TW": "選單",
2852
+ "ko": "메뉴", "ja": "メニュー", "de": "Menü",
2853
+ "es": "Menú", "fr": "Menu", "it": "Menu",
2854
+ "da": "Menu", "pl": "Menu", "ru": "Меню",
2855
+ "ar": "قائمة", "no": "Meny", "pt-BR": "Menu",
2856
+ "th": "เมนู", "tr": "Menü", "uk": "Меню"
2857
+ },
2642
2858
  "cli.userNameRequired": {
2643
2859
  "zh": "错误: --user-name 需要一个名称参数",
2644
2860
  "en": "Error: --user-name requires a name argument",
@@ -8,6 +8,7 @@ const _ccvSkip = _ccvSkipArgs.includes(process.argv[2]);
8
8
 
9
9
  import './lib/proxy-env.js';
10
10
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync, statSync, unlinkSync, existsSync, watchFile } from 'node:fs';
11
+ import { AsyncWriteQueue } from './lib/async-write-queue.js';
11
12
  import { renameSyncWithRetry } from './lib/file-api.js';
12
13
  import http from 'node:http';
13
14
  import https from 'node:https';
@@ -322,6 +323,9 @@ if (process.env.CCV_WORKSPACE_MODE === '1') {
322
323
  }
323
324
  let LOG_FILE = _newLogFile;
324
325
 
326
+ // 异步写入队列 — 替代 appendFileSync,避免阻塞事件循环(Windows NTFS 尤为严重)
327
+ const _writeQueue = new AsyncWriteQueue(() => LOG_FILE);
328
+
325
329
  // 现在 _projectName/_logDir 已初始化,可以安全加载 proxy profile(含 workspace override)
326
330
  // 并挂载 watchFile 同步列表变化。
327
331
  _loadProxyProfile();
@@ -403,14 +407,16 @@ export function resetWorkspace() {
403
407
  _loadProxyProfile(); // workspace 上下文消失,回落到 profile.json.active
404
408
  }
405
409
 
406
- const MAX_LOG_SIZE = 300 * 1024 * 1024; // 300MB
410
+ // Windows NTFS + Defender 下大文件 I/O 代价远高于 Mac/Linux,降低分割阈值减轻压力
411
+ const MAX_LOG_SIZE = (process.platform === 'win32' ? 150 : 300) * 1024 * 1024;
407
412
 
408
- function checkAndRotateLogFile() {
413
+ async function checkAndRotateLogFile() {
409
414
  // Teammate 不做日志轮转,由 leader 负责
410
415
  if (_isTeammate) return;
411
416
  try {
412
417
  if (!existsSync(LOG_FILE) || statSync(LOG_FILE).size < MAX_LOG_SIZE) return;
413
418
  } catch { return; }
419
+ await _writeQueue.flush();
414
420
  const { filePath } = generateNewLogFilePath();
415
421
  const result = rotateLogFile(LOG_FILE, filePath, MAX_LOG_SIZE);
416
422
  if (result.rotated) {
@@ -507,15 +513,15 @@ export function setupInterceptor() {
507
513
  };
508
514
 
509
515
  process.on('SIGINT', () => {
510
- cleanupViewer().finally(() => process.exit(0));
516
+ _writeQueue.close().then(() => cleanupViewer()).finally(() => process.exit(0));
511
517
  });
512
518
 
513
519
  process.on('SIGTERM', () => {
514
- cleanupViewer().finally(() => process.exit(0));
520
+ _writeQueue.close().then(() => cleanupViewer()).finally(() => process.exit(0));
515
521
  });
516
522
 
517
523
  process.on('beforeExit', () => {
518
- cleanupViewer();
524
+ _writeQueue.close().then(() => cleanupViewer());
519
525
  });
520
526
 
521
527
  const _originalFetch = globalThis.fetch;
@@ -627,7 +633,7 @@ export function setupInterceptor() {
627
633
 
628
634
  // 用户新指令边界:检查日志文件大小,超过 250MB 则切换新文件
629
635
  if (requestEntry?.mainAgent) {
630
- checkAndRotateLogFile();
636
+ await checkAndRotateLogFile();
631
637
  // 仅 mainAgent 请求时缓存模型名,避免 SubAgent 覆盖
632
638
  if (requestEntry.body?.model && typeof requestEntry.body.model === 'string') {
633
639
  _cachedModel = requestEntry.body.model;
@@ -719,9 +725,7 @@ export function setupInterceptor() {
719
725
  if (requestEntry) {
720
726
  const willLiveStream = !!_livePort && requestEntry.mainAgent && !_isTeammate;
721
727
  if (!willLiveStream) {
722
- try {
723
- appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
724
- } catch { }
728
+ _writeQueue.append(JSON.stringify(requestEntry) + '\n---\n');
725
729
  }
726
730
  }
727
731
 
@@ -924,8 +928,8 @@ export function setupInterceptor() {
924
928
  // 移除在途请求标记,保持原始报文
925
929
  delete requestEntry.inProgress;
926
930
  delete requestEntry.requestId;
927
- appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
928
- _commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
931
+ { const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
932
+ _writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
929
933
  // Release memory: clear large objects after disk write
930
934
  streamedChunks = [];
931
935
  streamedContentLen = 0;
@@ -935,8 +939,8 @@ export function setupInterceptor() {
935
939
  requestEntry.response.body = fullContent.slice(0, 1000);
936
940
  delete requestEntry.inProgress;
937
941
  delete requestEntry.requestId;
938
- appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
939
- _commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
942
+ { const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
943
+ _writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
940
944
  streamedChunks = [];
941
945
  streamedContentLen = 0;
942
946
  requestEntry.response = null;
@@ -1005,8 +1009,8 @@ export function setupInterceptor() {
1005
1009
  };
1006
1010
  delete requestEntry.inProgress;
1007
1011
  delete requestEntry.requestId;
1008
- appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
1009
- _commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
1012
+ { const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
1013
+ _writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
1010
1014
  resetStreamingState();
1011
1015
  }
1012
1016
  } else {
@@ -1033,13 +1037,13 @@ export function setupInterceptor() {
1033
1037
  delete requestEntry.inProgress;
1034
1038
  delete requestEntry.requestId;
1035
1039
 
1036
- appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
1037
- _commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
1040
+ { const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
1041
+ _writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
1038
1042
  } catch (err) {
1039
1043
  delete requestEntry.inProgress;
1040
1044
  delete requestEntry.requestId;
1041
- appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
1042
- _commitDeltaState(_deltaOriginalMessagesLength, _deltaOriginalTailFp);
1045
+ { const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
1046
+ _writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
1043
1047
  }
1044
1048
  }
1045
1049
  }
@@ -2,6 +2,7 @@
2
2
  // orchestration (dedup, access control, queue, inject, chunk, turn-end reply) lives in
3
3
  // server/lib/im-bridge-core.js; this module only knows DingTalk's Stream client, ACK protocol,
4
4
  // inbound payload shape, access-token fetch, and proactive App API send endpoints.
5
+ import { randomUUID } from 'node:crypto';
5
6
  import { registerAdapter } from '../im-bridge-core.js';
6
7
  import { t } from '../../i18n.js';
7
8
 
@@ -13,6 +14,8 @@ export function __setClientFactory(fn) { clientFactory = fn; }
13
14
  const TOKEN_URL = 'https://api.dingtalk.com/v1.0/oauth2/accessToken';
14
15
  const GROUP_SEND_URL = 'https://api.dingtalk.com/v1.0/robot/groupMessages/send';
15
16
  const OTO_SEND_URL = 'https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend';
17
+ const CARD_CREATE_URL = 'https://api.dingtalk.com/v1.0/card/instances';
18
+ const CARD_DELIVER_URL = 'https://api.dingtalk.com/v1.0/card/instances/deliver';
16
19
 
17
20
  async function getAccessToken(cfg, ctx) {
18
21
  const tc = ctx.store.tokenCache;
@@ -120,6 +123,65 @@ const dingtalkAdapter = {
120
123
  // 触发钉钉风控/限流,反过来阻断模型回复的下发。故 DingTalk 发送者头像在「对话记录」里降级为默认头像
121
124
  // (名字仍是真实昵称,不报错、不抢占发送配额)。后续如引入具备通讯录 scope 的凭证再补 resolveSender。
122
125
 
126
+ async sendAckCard(cfg, target, statusText, ctx) {
127
+ if (!cfg.cardTemplateId) return null;
128
+ const token = await getAccessToken(cfg, ctx);
129
+ const outTrackId = randomUUID();
130
+ const headers = { 'Content-Type': 'application/json', 'x-acs-dingtalk-access-token': token };
131
+
132
+ const cr = await ctx.fetch(CARD_CREATE_URL, {
133
+ method: 'POST',
134
+ headers,
135
+ body: JSON.stringify({
136
+ outTrackId,
137
+ cardTemplateId: cfg.cardTemplateId,
138
+ cardData: { cardParamMap: { status: statusText, content: '' } },
139
+ }),
140
+ });
141
+ if (!cr.ok) { const j = await cr.json().catch(() => ({})); throw new Error(`card create ${cr.status}: ${j.message || j.code || 'failed'}`); }
142
+
143
+ const isGroup = String(target.conversationType) === '2';
144
+ const deliverBody = isGroup
145
+ ? {
146
+ outTrackId,
147
+ userIdType: 1,
148
+ openSpaceId: 'dtv1.card//IM_GROUP.' + target.conversationId,
149
+ imGroupOpenDeliverModel: { robotCode: target.robotCode },
150
+ }
151
+ : {
152
+ outTrackId,
153
+ userIdType: 1,
154
+ openSpaceId: 'dtv1.card//IM_ROBOT.' + target.senderStaffId,
155
+ imRobotOpenDeliverModel: { spaceType: 'IM_ROBOT', robotCode: target.robotCode },
156
+ };
157
+
158
+ const dr = await ctx.fetch(CARD_DELIVER_URL, {
159
+ method: 'POST',
160
+ headers,
161
+ body: JSON.stringify(deliverBody),
162
+ });
163
+ if (!dr.ok) { const j = await dr.json().catch(() => ({})); throw new Error(`card deliver ${dr.status}: ${j.message || j.code || 'failed'}`); }
164
+
165
+ return { outTrackId };
166
+ },
167
+
168
+ async updateAckCard(cfg, target, handle, content, status, ctx) {
169
+ try {
170
+ const token = await getAccessToken(cfg, ctx);
171
+ const r = await ctx.fetch(CARD_CREATE_URL, {
172
+ method: 'PUT',
173
+ headers: { 'Content-Type': 'application/json', 'x-acs-dingtalk-access-token': token },
174
+ body: JSON.stringify({
175
+ outTrackId: handle.outTrackId,
176
+ cardData: { cardParamMap: { status: '', content } },
177
+ }),
178
+ });
179
+ return r.ok;
180
+ } catch {
181
+ return false;
182
+ }
183
+ },
184
+
123
185
  async testConnection(cfg, ctx) {
124
186
  try {
125
187
  ctx.store.tokenCache = null; // force a fresh fetch
@@ -129,6 +129,41 @@ const discordAdapter = {
129
129
  }
130
130
  },
131
131
 
132
+ async sendAckCard(cfg, target, statusText, ctx) {
133
+ const client = ctx.store.client;
134
+ if (!client) throw new Error('discord client not connected');
135
+ let channel;
136
+ if (target.userId) {
137
+ const user = await client.users.fetch(target.userId);
138
+ channel = await user.createDM();
139
+ } else {
140
+ channel = await client.channels.fetch(target.channelId);
141
+ }
142
+ if (!channel || typeof channel.send !== 'function') throw new Error(`channel not sendable: ${target.channelId}`);
143
+ const msg = await channel.send(statusText);
144
+ return { channelId: target.channelId, messageId: msg.id, userId: target.userId };
145
+ },
146
+
147
+ async updateAckCard(cfg, target, handle, content, status, ctx) {
148
+ try {
149
+ const client = ctx.store.client;
150
+ if (!client) return false;
151
+ let channel;
152
+ if (handle.userId) {
153
+ const user = await client.users.fetch(handle.userId);
154
+ channel = await user.createDM();
155
+ } else {
156
+ channel = await client.channels.fetch(handle.channelId);
157
+ }
158
+ if (!channel || typeof channel.messages?.fetch !== 'function') return false;
159
+ const msg = await channel.messages.fetch(handle.messageId);
160
+ await msg.edit(content.slice(0, DISCORD_MAX));
161
+ return true;
162
+ } catch {
163
+ return false;
164
+ }
165
+ },
166
+
132
167
  async testConnection(cfg, ctx) {
133
168
  // Validate the token via REST without opening a gateway (and without the privileged intent).
134
169
  try {