cc-viewer 1.6.293 → 1.6.295
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/cli.js +7 -2
- package/dist/assets/App-Br-u2TKk.js +2 -0
- package/dist/assets/App-eFrjLzF_.css +1 -0
- package/dist/assets/{MdxEditorPanel-Cf01KF6Z.js → MdxEditorPanel-Cy4egsQx.js} +1 -1
- package/dist/assets/{Mobile-BJlGkvAP.js → Mobile-ZHF74GQs.js} +1 -1
- package/dist/assets/{_baseUniq-CPUnJ5bQ.js → _baseUniq-r3p3rodd.js} +1 -1
- package/dist/assets/{arc-WhuJ-oY5.js → arc-CjTV5gxc.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-CWx77Yhd.js → architectureDiagram-Q4EWVU46-BqzjXpCq.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-D7AQLCoj.js → blockDiagram-DXYQGD6D-CLyFfeHh.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-BoPHNqCF.js → c4Diagram-AHTNJAMY-BaO-0tuc.js} +1 -1
- package/dist/assets/{channel-B9Ja6Xkc.js → channel-yOyhvOLV.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-B-b0RYab.js → chunk-4BX2VUAB-CMTnvZkS.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-BK_V34yf.js → chunk-4TB4RGXK-QI41m9WP.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-D-kMbu-2.js → chunk-55IACEB6-C4ZO8bM3.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-CEtSkZzd.js → chunk-EDXVE4YY-Bo8P4o65.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BXa_7Pn3.js → chunk-FMBD7UC4-CTHLGcHh.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-tvM_OApS.js → chunk-OYMX7WX6-D0OHxKGd.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-DrEmcVHf.js → chunk-QZHKN3VN-CoYnjUpS.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-D2M9T_R5.js → chunk-YZCP3GAM-BY71mTXM.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-C9o5ip5q.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-C9o5ip5q.js +1 -0
- package/dist/assets/clone-GDqN3kwT.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-H7bkwu5F.js → cose-bilkent-S5V4N54A-DUNsA_MT.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-DKXEGN18.js → dagre-KV5264BT-BzlT2Exr.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DZFhwpI3.js → diagram-5BDNPKRD-CiqQK3Ci.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Crg9GlIk.js → diagram-G4DWMVQ6-BciK18tQ.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-B8Qn1fKP.js → diagram-MMDJMWI5-C1WH1vfU.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BHE1LjtY.js → diagram-TYMM5635-CR5RzJ6u.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-BaEqFWLd.js → erDiagram-SMLLAGMA-NJQKXu51.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-b2ukTawV.js → flowDiagram-DWJPFMVM-Cjx5t_1H.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-D5quyFgK.js → ganttDiagram-T4ZO3ILL-YFTDBBiU.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-BE1H5_fN.js → gitGraphDiagram-UUTBAWPF-C2muKahz.js} +1 -1
- package/dist/assets/{graph-D_JLoOax.js → graph-I1olozIg.js} +1 -1
- package/dist/assets/{index-Cx8bk0Tp.js → index-7vxIrUNA.js} +1 -1
- package/dist/assets/{index-BDUs32pN.css → index-Be9T-kDq.css} +1 -1
- package/dist/assets/{index-CtrY6gFZ.js → index-C1RNAzAB.js} +1 -1
- package/dist/assets/{index-CQrdpZQb.js → index-Cf4FBg-V.js} +1 -1
- package/dist/assets/{index-B8UmlA4F.js → index-D-HPuqxB.js} +1 -1
- package/dist/assets/{index-k0AH8cvI.js → index-D2QUxu18.js} +1 -1
- package/dist/assets/index-DMuCrfTo.js +2 -0
- package/dist/assets/{index-DiZ9CErG.js → index-DhzoJ5wE.js} +1 -1
- package/dist/assets/{index-CWjqMDrs.js → index-fhI0i2p3.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-DQKlrVkw.js → infoDiagram-42DDH7IO-C9bza97c.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-BchFlpPc.js → ishikawaDiagram-UXIWVN3A-BtZGipfW.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-Dg1mt4df.js → journeyDiagram-VCZTEJTY-CKTp590c.js} +1 -1
- package/dist/assets/{jszip.min-LIb2SFoK.js → jszip.min-DDU-_oA-.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-226va2PS.js → kanban-definition-6JOO6SKY-BHLNWfr5.js} +1 -1
- package/dist/assets/{layout-rSa8rcPi.js → layout-DBmqcl9N.js} +1 -1
- package/dist/assets/{linear-BeARi8nH.js → linear-Br9n7mCI.js} +1 -1
- package/dist/assets/{mermaid.core-CDgdx9l7.js → mermaid.core-BV3ugHFm.js} +2 -2
- package/dist/assets/{min-B9yebCuj.js → min-D-YA3MGY.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-C3apVbdg.js → mindmap-definition-QFDTVHPH-CzrYj3cB.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-xjOQoQeL.js → pieDiagram-DEJITSTG-BAvtfiT3.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-Dq8x_VN2.js → quadrantDiagram-34T5L4WZ-i4zhnBJq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CLmO1Gai.js → requirementDiagram-MS252O5E-Cb2wX9Sk.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-BuUP1Eqq.js → sankeyDiagram-XADWPNL6-CcpbP6z5.js} +1 -1
- package/dist/assets/seqResourceLoaders-C7X23dCJ.js +2 -0
- package/dist/assets/{seqResourceLoaders-DWKAvGtj.css → seqResourceLoaders-De_-fYhE.css} +2 -2
- package/dist/assets/{sequenceDiagram-FGHM5R23-B18koU20.js → sequenceDiagram-FGHM5R23-BcbUxMmI.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-Cj57OCcO.js → stateDiagram-FHFEXIEX-CpIa1qoO.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-C01a2p--.js → stateDiagram-v2-QKLJ7IA2-d3GoyW9S.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-cOlsEN_F.js → timeline-definition-GMOUNBTQ-BfQPSOuT.js} +1 -1
- package/dist/assets/{vendor-antd-DqFS7Zj9.js → vendor-antd-Bur5ZxWE.js} +1 -1
- package/dist/assets/{vendor-codemirror-B_pF4DrA.js → vendor-codemirror-Si44UqBp.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-B_IrHcWH.js → vendor-mdxeditor-Cco3AQJS.js} +2 -2
- package/dist/assets/{vendor-qrcode-C4PneAS5.js → vendor-qrcode-Dn3GYC4l.js} +1 -1
- package/dist/assets/{vendor-virtuoso-CEGeJyDP.js → vendor-virtuoso-CW9EqKMt.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-BCjdwiDk.js → vennDiagram-DHZGUBPP-hTgiYDQL.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-CRmLlBwn.js → wardley-RL74JXVD-ByDpAPp1.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-BJYVDJ4F.js → wardleyDiagram-NUSXRM2D-D7LJTuWq.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-el5C4S1Z.js → xychartDiagram-5P7HB3ND-MW_KOomO.js} +1 -1
- package/dist/index.html +5 -5
- package/findcc.js +3 -3
- package/package.json +1 -1
- package/server/i18n.js +224 -8
- package/server/interceptor.js +21 -18
- package/server/lib/adapters/dingtalk-adapter.js +69 -0
- package/server/lib/adapters/discord-adapter.js +44 -1
- package/server/lib/adapters/feishu-adapter.js +56 -0
- package/server/lib/adapters/wecom-adapter.js +4 -0
- package/server/lib/ask-store.js +19 -90
- package/server/lib/async-file-lock.js +123 -0
- package/server/lib/async-write-queue.js +131 -0
- package/server/lib/git-diff.js +4 -1
- package/server/lib/im-bridge-core.js +178 -21
- package/server/lib/im-claude-md.js +37 -1
- package/server/lib/im-config.js +11 -6
- package/server/lib/im-process-manager.js +1 -1
- package/server/lib/im-senders.js +73 -0
- package/server/lib/jsonl-archive.js +0 -1
- package/server/lib/log-watcher.js +224 -177
- package/server/lib/plugin-manager.js +1 -1
- package/server/lib/updater.js +4 -2
- package/server/pty-manager.js +1 -1
- package/server/routes/ask-perm.js +2 -2
- package/server/routes/dingtalk.js +2 -0
- package/server/routes/files-fs.js +4 -4
- package/server/routes/im.js +117 -3
- package/server/routes/project-meta.js +18 -1
- package/server/routes/skills.js +180 -165
- package/server/routes/workspaces.js +7 -10
- package/server/server.js +23 -20
- package/server/workspace-registry.js +9 -53
- package/dist/assets/App-DRvRd96X.css +0 -1
- package/dist/assets/App-OM2oqZRW.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CCwGJXEA.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-CCwGJXEA.js +0 -1
- package/dist/assets/clone-BuQbTPQO.js +0 -1
- package/dist/assets/index-CnWSVlWW.js +0 -2
- package/dist/assets/seqResourceLoaders-BZ6M3Jb-.js +0 -2
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
|
|
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": "
|
|
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
|
|
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": "
|
|
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
|
|
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": "
|
|
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
|
|
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": "
|
|
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",
|
package/server/interceptor.js
CHANGED
|
@@ -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();
|
|
@@ -405,12 +409,13 @@ export function resetWorkspace() {
|
|
|
405
409
|
|
|
406
410
|
const MAX_LOG_SIZE = 300 * 1024 * 1024; // 300MB
|
|
407
411
|
|
|
408
|
-
function checkAndRotateLogFile() {
|
|
412
|
+
async function checkAndRotateLogFile() {
|
|
409
413
|
// Teammate 不做日志轮转,由 leader 负责
|
|
410
414
|
if (_isTeammate) return;
|
|
411
415
|
try {
|
|
412
416
|
if (!existsSync(LOG_FILE) || statSync(LOG_FILE).size < MAX_LOG_SIZE) return;
|
|
413
417
|
} catch { return; }
|
|
418
|
+
await _writeQueue.flush();
|
|
414
419
|
const { filePath } = generateNewLogFilePath();
|
|
415
420
|
const result = rotateLogFile(LOG_FILE, filePath, MAX_LOG_SIZE);
|
|
416
421
|
if (result.rotated) {
|
|
@@ -507,15 +512,15 @@ export function setupInterceptor() {
|
|
|
507
512
|
};
|
|
508
513
|
|
|
509
514
|
process.on('SIGINT', () => {
|
|
510
|
-
cleanupViewer().finally(() => process.exit(0));
|
|
515
|
+
_writeQueue.close().then(() => cleanupViewer()).finally(() => process.exit(0));
|
|
511
516
|
});
|
|
512
517
|
|
|
513
518
|
process.on('SIGTERM', () => {
|
|
514
|
-
cleanupViewer().finally(() => process.exit(0));
|
|
519
|
+
_writeQueue.close().then(() => cleanupViewer()).finally(() => process.exit(0));
|
|
515
520
|
});
|
|
516
521
|
|
|
517
522
|
process.on('beforeExit', () => {
|
|
518
|
-
cleanupViewer();
|
|
523
|
+
_writeQueue.close().then(() => cleanupViewer());
|
|
519
524
|
});
|
|
520
525
|
|
|
521
526
|
const _originalFetch = globalThis.fetch;
|
|
@@ -627,7 +632,7 @@ export function setupInterceptor() {
|
|
|
627
632
|
|
|
628
633
|
// 用户新指令边界:检查日志文件大小,超过 250MB 则切换新文件
|
|
629
634
|
if (requestEntry?.mainAgent) {
|
|
630
|
-
checkAndRotateLogFile();
|
|
635
|
+
await checkAndRotateLogFile();
|
|
631
636
|
// 仅 mainAgent 请求时缓存模型名,避免 SubAgent 覆盖
|
|
632
637
|
if (requestEntry.body?.model && typeof requestEntry.body.model === 'string') {
|
|
633
638
|
_cachedModel = requestEntry.body.model;
|
|
@@ -719,9 +724,7 @@ export function setupInterceptor() {
|
|
|
719
724
|
if (requestEntry) {
|
|
720
725
|
const willLiveStream = !!_livePort && requestEntry.mainAgent && !_isTeammate;
|
|
721
726
|
if (!willLiveStream) {
|
|
722
|
-
|
|
723
|
-
appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
|
|
724
|
-
} catch { }
|
|
727
|
+
_writeQueue.append(JSON.stringify(requestEntry) + '\n---\n');
|
|
725
728
|
}
|
|
726
729
|
}
|
|
727
730
|
|
|
@@ -924,8 +927,8 @@ export function setupInterceptor() {
|
|
|
924
927
|
// 移除在途请求标记,保持原始报文
|
|
925
928
|
delete requestEntry.inProgress;
|
|
926
929
|
delete requestEntry.requestId;
|
|
927
|
-
|
|
928
|
-
|
|
930
|
+
{ const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
|
|
931
|
+
_writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
|
|
929
932
|
// Release memory: clear large objects after disk write
|
|
930
933
|
streamedChunks = [];
|
|
931
934
|
streamedContentLen = 0;
|
|
@@ -935,8 +938,8 @@ export function setupInterceptor() {
|
|
|
935
938
|
requestEntry.response.body = fullContent.slice(0, 1000);
|
|
936
939
|
delete requestEntry.inProgress;
|
|
937
940
|
delete requestEntry.requestId;
|
|
938
|
-
|
|
939
|
-
|
|
941
|
+
{ const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
|
|
942
|
+
_writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
|
|
940
943
|
streamedChunks = [];
|
|
941
944
|
streamedContentLen = 0;
|
|
942
945
|
requestEntry.response = null;
|
|
@@ -1005,8 +1008,8 @@ export function setupInterceptor() {
|
|
|
1005
1008
|
};
|
|
1006
1009
|
delete requestEntry.inProgress;
|
|
1007
1010
|
delete requestEntry.requestId;
|
|
1008
|
-
|
|
1009
|
-
|
|
1011
|
+
{ const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
|
|
1012
|
+
_writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
|
|
1010
1013
|
resetStreamingState();
|
|
1011
1014
|
}
|
|
1012
1015
|
} else {
|
|
@@ -1033,13 +1036,13 @@ export function setupInterceptor() {
|
|
|
1033
1036
|
delete requestEntry.inProgress;
|
|
1034
1037
|
delete requestEntry.requestId;
|
|
1035
1038
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1039
|
+
{ const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
|
|
1040
|
+
_writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
|
|
1038
1041
|
} catch (err) {
|
|
1039
1042
|
delete requestEntry.inProgress;
|
|
1040
1043
|
delete requestEntry.requestId;
|
|
1041
|
-
|
|
1042
|
-
|
|
1044
|
+
{ const _dl = _deltaOriginalMessagesLength, _tf = _deltaOriginalTailFp;
|
|
1045
|
+
_writeQueue.append(JSON.stringify(requestEntry) + '\n---\n', () => _commitDeltaState(_dl, _tf)); }
|
|
1043
1046
|
}
|
|
1044
1047
|
}
|
|
1045
1048
|
}
|
|
@@ -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;
|
|
@@ -44,6 +47,7 @@ function normalizeInbound(res) {
|
|
|
44
47
|
conversationId,
|
|
45
48
|
isGroup: String(conversationType) === '2',
|
|
46
49
|
senderId: senderStaffId,
|
|
50
|
+
senderName: msg.senderNick || null, // 昵称回调里免费提供;头像不取(见下方 resolveSender 注释)
|
|
47
51
|
msgId: res?.headers?.messageId,
|
|
48
52
|
// opaque platform extras carried back to sendOne
|
|
49
53
|
target: { conversationId, conversationType, robotCode, senderStaffId },
|
|
@@ -113,6 +117,71 @@ const dingtalkAdapter = {
|
|
|
113
117
|
}
|
|
114
118
|
},
|
|
115
119
|
|
|
120
|
+
// 发送者姓名:回调里的 senderNick 已免费带出(见 normalizeInbound),「对话记录」直接用它。
|
|
121
|
+
// 故意不实现 resolveSender —— 头像需查通讯录(topapi/v2/user/get),而机器人应用的 appKey/appSecret
|
|
122
|
+
// 没有通讯录读取权限,调用必失败(errcode 60121/88);且这些未授权调用会打到与消息发送同一个 appKey,
|
|
123
|
+
// 触发钉钉风控/限流,反过来阻断模型回复的下发。故 DingTalk 发送者头像在「对话记录」里降级为默认头像
|
|
124
|
+
// (名字仍是真实昵称,不报错、不抢占发送配额)。后续如引入具备通讯录 scope 的凭证再补 resolveSender。
|
|
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
|
+
|
|
116
185
|
async testConnection(cfg, ctx) {
|
|
117
186
|
try {
|
|
118
187
|
ctx.store.tokenCache = null; // force a fresh fetch
|
|
@@ -30,15 +30,23 @@ const DISCORD_MAX = 2_000; // hard per-message limit
|
|
|
30
30
|
* event handler (before this). */
|
|
31
31
|
function normalizeInbound(message) {
|
|
32
32
|
const isGroup = typeof message.inGuild === 'function' ? message.inGuild() : !!message.guild;
|
|
33
|
-
const
|
|
33
|
+
const author = message.author || {};
|
|
34
|
+
const senderId = author.id || '';
|
|
34
35
|
const channelId = message.channelId;
|
|
35
36
|
// Strip a leading bot mention (<@123> / <@!123>) so "@bot do X" injects as "do X".
|
|
36
37
|
const text = String(message.content ?? '').replace(/^<@!?\d+>\s*/, '');
|
|
38
|
+
// 姓名 + 头像在事件里免费可得(无需 API):优先 global_name,头像用 discord.js 的 displayAvatarURL(),
|
|
39
|
+
// 退化为按 avatar hash 拼 CDN URL(无头像则 null,由前端回落默认头像)。
|
|
40
|
+
const senderAvatar = typeof author.displayAvatarURL === 'function'
|
|
41
|
+
? author.displayAvatarURL()
|
|
42
|
+
: (author.avatar ? `https://cdn.discordapp.com/avatars/${author.id}/${author.avatar}.png` : null);
|
|
37
43
|
return {
|
|
38
44
|
text,
|
|
39
45
|
conversationId: channelId,
|
|
40
46
|
isGroup,
|
|
41
47
|
senderId,
|
|
48
|
+
senderName: author.global_name || author.username || null,
|
|
49
|
+
senderAvatar,
|
|
42
50
|
msgId: message.id,
|
|
43
51
|
// For a DM, channels.fetch(channelId) is broken on a partial DM channel (CHANNEL_RECIPIENT_REQUIRED),
|
|
44
52
|
// so carry the user id and reply via users.fetch().createDM(). Guild channels fetch reliably by id.
|
|
@@ -121,6 +129,41 @@ const discordAdapter = {
|
|
|
121
129
|
}
|
|
122
130
|
},
|
|
123
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
|
+
|
|
124
167
|
async testConnection(cfg, ctx) {
|
|
125
168
|
// Validate the token via REST without opening a gateway (and without the privileged intent).
|
|
126
169
|
try {
|