cc-viewer 1.2.3 → 1.2.7
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/README.md +55 -58
- package/cli.js +237 -37
- package/dist/assets/{index-CSuPDSgh.js → index-CN78DISW.js} +121 -85
- package/dist/assets/index-DhZ8St4J.css +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +4 -3
- package/interceptor.js +45 -24
- package/locales/i18n.json +23 -18
- package/package.json +2 -1
- package/proxy.js +215 -0
- package/server.js +15 -15
- package/dist/assets/index-BXa9W5oy.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
body{margin:0;background-color:#0d0d0d}*{scrollbar-width:thin;scrollbar-color:#3a3a3a #0d0d0d}*::-webkit-scrollbar{width:6px;height:6px}*::-webkit-scrollbar-track{background:#0d0d0d}*::-webkit-scrollbar-thumb{background:#3a3a3a;border-radius:3px}*::-webkit-scrollbar-thumb:hover{background:#555}.diff-view{background:#1a1a2e;border:1px solid #2a2a3e;border-radius:8px;padding:8px 12px}.diff-line-del{background:#ef444426;color:#fca5a5;padding:0 4px}.diff-line-add{background:#22c55e26;color:#86efac;padding:0 4px}.code-highlight{color:#e6edf3}.hl-keyword{color:#ff7b72}.hl-string{color:#a5d6ff}.hl-comment{color:#8b949e;font-style:italic}.hl-number{color:#79c0ff}.hl-linenum{color:#484f58;-webkit-user-select:none;user-select:none}.chat-md pre{background:#0d1117;border:1px solid #2a2a2a;border-radius:6px;padding:12px;overflow-x:auto;font-size:13px;line-height:1.5}.chat-md code{background:#1a1a2e;padding:2px 6px;border-radius:4px;font-size:13px;color:#e5e7eb}.chat-md pre code{background:none;padding:0}.chat-md p{margin:6px 0}.chat-md ul,.chat-md ol{padding-left:20px;margin:6px 0}.chat-md li{margin:2px 0}.chat-md h1,.chat-md h2,.chat-md h3{margin:12px 0 6px;color:#fff}.chat-md h1{font-size:1.3em}.chat-md h2{font-size:1.15em}.chat-md h3{font-size:1.05em}.chat-md blockquote{border-left:3px solid #3b82f6;margin:8px 0;padding:4px 12px;color:#9ca3af}.chat-md table{border-collapse:collapse;margin:8px 0;font-size:13px}.chat-md th,.chat-md td{border:1px solid #2a2a2a;padding:6px 10px}.chat-md th{background:#1a1a1a;color:#fff}.chat-md a{color:#60a5fa}.chat-md hr{border:none;border-top:1px solid #2a2a2a;margin:12px 0}.chat-md strong{color:#f1f5f9}.chat-md em{color:#cbd5e1}._helpBtn_1gxlm_1{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:#1a1a1a;border:1px solid #444;color:#aaa;font-size:11px;line-height:1;cursor:pointer;margin-left:4px;vertical-align:middle;transition:background .2s;-webkit-user-select:none;user-select:none}._helpBtn_1gxlm_1:hover{background:#333}._modalBody_1gxlm_24{max-height:60vh;overflow-y:auto;line-height:1.7}._modalBody_1gxlm_24 h1,._modalBody_1gxlm_24 h2,._modalBody_1gxlm_24 h3{margin-top:.8em}._modalBody_1gxlm_24 p{margin:.5em 0}._modalBody_1gxlm_24 code{background:#2a2a2a;padding:1px 4px;border-radius:3px;font-size:.9em}._modalBody_1gxlm_24 pre{background:#1a1a1a;padding:12px;border-radius:6px;overflow-x:auto}._spinWrap_1gxlm_54{display:flex;justify-content:center;padding:40px 0}._headerBar_2bgmo_2{display:flex;align-items:center;justify-content:space-between;width:100%;height:100%}._titleText_2bgmo_11{color:#fff;font-size:18px;cursor:pointer}._logoImage_2bgmo_17{height:24px;width:24px;margin-right:6px;border-radius:3px;vertical-align:text-bottom}._titleArrow_2bgmo_25{font-size:12px;margin-left:4px}._tokenStatsTag_2bgmo_31{border-radius:12px;cursor:pointer;background:#2a2a2a;border:1px solid #3a3a3a;color:#ccc}._tokenStatsIcon_2bgmo_39{margin-right:4px}._liveTag_2bgmo_44{border-radius:12px}._liveTag_2bgmo_44 .ant-badge-status-processing{animation:_breathe_2bgmo_1 2.5s ease-in-out infinite}@keyframes _breathe_2bgmo_1{0%,to{opacity:.4;transform:scale(.85)}50%{opacity:1;transform:scale(1.15)}}._liveTagHistory_2bgmo_57{background:#2a2a2a;border-color:#424242;color:#d1d5db}._liveTagText_2bgmo_63{margin-left:4px}._countdownStrong_2bgmo_68{font-variant-numeric:tabular-nums}._langSelector_2bgmo_73,._settingsBtn_2bgmo_84{color:#888;font-size:12px;cursor:pointer;-webkit-user-select:none;user-select:none;border:1px solid #555;border-radius:4px;padding:2px 8px}._settingsBtn_2bgmo_84:hover{color:#bbb;border-color:#777}._settingsItem_2bgmo_100{display:flex;justify-content:space-between;align-items:center;padding:12px 0}._settingsLabel_2bgmo_107{font-size:14px}._tokenStatsEmpty_2bgmo_112{padding:8px 4px;color:#999;font-size:13px}._tokenStatsContainer_2bgmo_119{display:flex;gap:12px;align-items:flex-start}._tokenStatsColumn_2bgmo_125{min-width:240px}._toolStatsColumn_2bgmo_129{min-width:180px}._modelCard_2bgmo_134{border:1px solid #333;border-radius:6px;padding:8px 10px;background:#111}._modelCardSpaced_2bgmo_141{margin-bottom:10px}._modelName_2bgmo_147{font-size:13px;font-weight:600;color:#e5e5e5;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid #333}._statsTable_2bgmo_157{width:100%;border-collapse:collapse}._th_2bgmo_162{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:#888;font-weight:400;text-align:right}._td_2bgmo_172{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:#e5e5e5;text-align:right}._label_2bgmo_181{padding:2px 12px;font-size:12px;font-family:monospace;white-space:nowrap;color:#aaa;font-weight:400;text-align:left}._rowBorder_2bgmo_191{border-bottom:1px solid #2a2a2a}._rebuildCard_2bgmo_195{border:1px solid #333;border-radius:6px;padding:8px 10px;margin-top:10px;background:#111}._rebuildTotalRow_2bgmo_203{border-top:1px solid #444}._rebuildTotalRow_2bgmo_203 td{font-weight:600}._promptExportBar_2bgmo_212{margin-bottom:12px}._promptScrollArea_2bgmo_216{max-height:500px;overflow:auto}._promptEmpty_2bgmo_221{color:#999;padding:12px}._promptTimestamp_2bgmo_227{color:#666;font-size:12px;margin:12px 0 4px;padding-bottom:6px}._textPromptCard_2bgmo_235{margin:4px 0;background:#141414;border-radius:6px;border:1px solid #303030;padding:10px 14px}._preText_2bgmo_244{white-space:pre-wrap;word-break:break-word;font-size:13px;line-height:1.6;color:#d9d9d9;margin:4px 0}._systemCollapse_2bgmo_254{margin:4px 0;background:#1a1a1a;border:1px solid #303030;border-radius:6px}._systemLabel_2bgmo_261{color:#888;font-size:12px}._preSys_2bgmo_266{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.5;color:#999;margin:0}._promptTextarea_2bgmo_276{background:#000;width:100%;min-height:400px;color:#d9d9d9;font-family:monospace;font-size:13px;line-height:1.6;border:none;resize:vertical;padding:10px 14px;outline:none}._centerEmpty_ckz8l_1{display:flex;align-items:center;justify-content:center;height:100%}._scrollContainer_ckz8l_8{overflow:auto;height:100%}._listItem_ckz8l_13{cursor:pointer;padding:8px 12px;border-left:3px solid transparent;border-bottom:1px solid #1f1f1f;transition:background .15s}._listItem_ckz8l_13:hover{background:#151515}._listItemActive_ckz8l_25{background:#1a2332;border-left-color:#3b82f6}._listItemActive_ckz8l_25:hover{background:#1a2332}._itemContent_ckz8l_34{width:100%;min-width:0}._itemHeader_ckz8l_39{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px}._tagNoMargin_ckz8l_47{margin:0;font-size:12px}._modelName_ckz8l_52{font-size:12px}._time_ckz8l_56{font-size:12px;color:#6b7280;margin-left:auto}._detailRow_ckz8l_62{display:flex;gap:8px;font-size:12px;align-items:center}._urlText_ckz8l_69{color:#555;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}._duration_ckz8l_78{color:#6b7280;flex-shrink:0}._statusOk_ckz8l_83{color:#52c41a;opacity:.5;flex-shrink:0}._statusErr_ckz8l_89{color:#ef4444;flex-shrink:0}._statusDefault_ckz8l_94{color:#9ca3af;flex-shrink:0}._usageBox_ckz8l_99{background:#111;border-radius:4px;padding:3px 6px;margin-top:4px;font-size:12px;color:#6b7280;line-height:1.6}._cacheDot_ckz8l_109{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 3px;vertical-align:middle}._cacheDotLoss_ckz8l_118{background-color:#8b1a1a;cursor:help}._cacheDotNormal_ckz8l_123{background-color:#3a3a3a}._GzYRV{line-height:1.2;white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word}._3eOF8{margin-right:5px;font-weight:700}._3eOF8+._3eOF8{margin-left:-5px}._1MFti{cursor:pointer}._f10Tu{font-size:1.2em;margin-right:5px;-webkit-user-select:none;-moz-user-select:none;user-select:none}._1UmXx:after{content:"▸"}._1LId0:after{content:"▾"}._1pNG9{margin-right:5px}._1pNG9:after{content:"...";font-size:.8em}._2IvMF{background:#eee}._2bkNM{margin:0;padding:0 10px}._1BXBN{margin:0;padding:0}._1MGIk{font-weight:600;margin-right:5px;color:#000}._3uHL6{color:#000}._2T6PJ,._1Gho6{color:#df113a}._vGjyY{color:#2a3f3c}._1bQdo{color:#0b75f5}._3zQKs{color:#469038}._1xvuR{color:#43413d}._oLqym,._2AXVT,._2KJWg{color:#000}._11RoI{background:#002b36}._17H2C,._3QHg2,._3fDAz{color:#fdf6e3}._2bSDX{font-weight:bolder;margin-right:5px;color:#fdf6e3}._gsbQL{color:#fdf6e3}._LaAZe,._GTKgm{color:#81b5ac}._Chy1W{color:#cb4b16}._2bveF{color:#d33682}._2vRm-{color:#ae81ff}._1prJR{color:#268bd2}._container_1h2lr_1{background:#0d1117;border-radius:6px;border:1px solid #2a2a2a;padding:12px;font-size:13px;font-family:monospace;overflow:auto}._container_y3z3z_1{height:100%;overflow:auto;padding:0 16px}._emptyState_y3z3z_7{display:flex;align-items:center;justify-content:center;height:100%}._urlSection_y3z3z_14{padding:12px 0;border-bottom:1px solid #1f1f1f;display:flex;align-items:flex-start}._urlLeft_y3z3z_21{flex:1;min-width:0}._tokenStatsBox_y3z3z_26{flex-shrink:0;padding-left:12px;display:flex;align-items:center}._tokenGrid_y3z3z_33{display:flex;border:1px solid #303030;border-radius:6px;overflow:hidden;min-width:360px;font-size:11px;line-height:1.6}._tokenRows_y3z3z_43{flex:1}._tokenRow_y3z3z_43{display:flex}._tokenRowBorder_y3z3z_51{border-top:1px solid #303030}._tokenLabel_y3z3z_55{color:#888;padding:4px 8px;white-space:nowrap;font-weight:600}._tokenTd_y3z3z_62{flex:1;color:#d1d5db;text-align:right;padding:4px 8px;font-family:monospace;white-space:nowrap}._tokenHitRate_y3z3z_71{display:flex;flex-direction:column;align-items:center;justify-content:center;color:#d1d5db;padding:4px 8px;font-family:monospace;white-space:nowrap;border-left:1px solid #303030;min-width:100px}._tokenHitRateLabel_y3z3z_84{color:#888;font-size:10px;font-family:sans-serif}._tokenRowBorder_y3z3z_51 td{border-top:1px solid #303030}._urlText_y3z3z_94{color:#d1d5db;font-size:13px;margin-bottom:8px;word-break:break-all}._metaText_y3z3z_101,._headersContainer_y3z3z_105{font-size:12px}._headerRow_y3z3z_109{display:flex;padding:4px 0;border-bottom:1px solid #1f1f1f}._headerKey_y3z3z_115{min-width:200px;flex-shrink:0}._headerValue_y3z3z_120{word-break:break-all;margin-left:8px}._streamingBox_y3z3z_125{padding:20px;background:#1a1a1a;border-radius:6px;border:1px solid #2a2a2a}._bodyToolbar_y3z3z_132{display:flex;gap:8px;margin-bottom:8px}._rawTextPre_y3z3z_138{background:#0d1117;border:1px solid #2a2a2a;border-radius:6px;padding:12px;font-size:12px;color:#e5e7eb;overflow:auto;max-height:600px;white-space:pre-wrap;word-break:break-all}._tabContent_y3z3z_151{padding:12px 0}._collapseSpacing_y3z3z_155{margin-bottom:16px}._bodyLabel_y3z3z_159{margin:0}._bodyHeader_y3z3z_163{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}._diffSection_y3z3z_170{margin-bottom:16px}._diffToggle_y3z3z_174{display:inline-block;margin-bottom:8px;cursor:pointer}._diffIcon_y3z3z_180{font-size:12px;margin-left:4px}._viewInChatBtn_y3z3z_185{display:inline-flex;align-items:center;height:26px;border-radius:13px;border:1px solid #424242;background:transparent;color:#888;cursor:pointer;font-size:12px;transition:all .2s;padding:0 10px;white-space:nowrap}._viewInChatBtn_y3z3z_185:hover{border-color:#666;color:#d1d5db;background:#ffffff0f}._reminderSelect_y3z3z_206{min-width:140px;font-size:12px}._reminderSelect_y3z3z_206 .ant-select-selector{border-radius:2px!important;border-color:#424242!important;background:transparent!important;min-height:26px!important;height:auto!important;padding:0 8px!important;font-family:monospace}._reminderSelect_y3z3z_206 .ant-select-selection-placeholder{font-size:11px}._wrapper_1o255_1{margin:6px 0}._header_1o255_5{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}._filePath_1o255_12{color:#a78bfa;font-size:12px;font-weight:600}._toggle_1o255_18{color:#6b7280;font-size:11px;cursor:pointer;-webkit-user-select:none;user-select:none}._code_1o255_25{margin:0;font-size:12px;line-height:1.5;overflow:auto}._plainResult_1uh60_1{background:#111;border-radius:6px;font-size:12px}._plainTitle_1uh60_7{font-size:11px}._plainPre_1uh60_11{color:#d1d5db;margin:0;white-space:pre-wrap;word-break:break-all;font-size:12px;padding:8px 12px}._codeResult_1uh60_20{background:#0d1117;border-radius:6px;border:1px solid #1e2a3a;overflow:hidden}._codeHeader_1uh60_27{display:flex;justify-content:space-between;align-items:center;padding:4px 10px;background:#161b22;border-bottom:1px solid #1e2a3a}._codeTitle_1uh60_36{font-size:11px;color:#8b949e}._codeToggle_1uh60_41{font-size:11px;color:#484f58;cursor:pointer;-webkit-user-select:none;user-select:none}._codePre_1uh60_48{margin:0;padding:8px 12px;font-size:12px;line-height:1.5;overflow:auto;max-height:500px}._markdownBody_1uh60_57{padding:8px 12px;font-size:13px;line-height:1.6;overflow:auto;max-height:500px}._tag_17wfp_1{display:inline-block;font-size:10px;color:#6b7280;background:#ffffff0f;border:1px solid rgba(255,255,255,.1);border-radius:3px;padding:0 4px;margin-left:6px;cursor:pointer;line-height:18px;vertical-align:middle;transition:color .2s,border-color .2s;-webkit-user-select:none;user-select:none}._tag_17wfp_1:hover{color:#93c5fd;border-color:#93c5fd4d}._tagActive_17wfp_22{color:#93c5fd;border-color:#93c5fd40}._tagLoading_17wfp_27{cursor:wait;opacity:.7}._spinner_17wfp_32{display:inline-block;width:10px;height:10px;border:1.5px solid rgba(147,197,253,.3);border-top-color:#93c5fd;border-radius:50%;animation:_spin_17wfp_32 .6s linear infinite;margin-right:3px;vertical-align:middle}@keyframes _spin_17wfp_32{to{transform:rotate(360deg)}}._avatar_1od17_3{width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0}._avatarImg_1od17_13{width:32px;height:32px;border-radius:50%;flex-shrink:0;object-fit:cover}._bubble_1od17_21{border-radius:8px;padding:10px 14px;max-width:100%;font-size:14px;line-height:1.6;word-break:break-word}._messageRow_1od17_32{display:flex;gap:10px;padding:8px 0}._messageRowEnd_1od17_38{display:flex;gap:10px;padding:8px 0;justify-content:flex-end}._contentCol_1od17_47{min-width:0;flex:1}._contentColLimited_1od17_52{min-width:0;max-width:80%;width:fit-content}._labelRow_1od17_60{display:flex;justify-content:space-between;align-items:center;margin-bottom:2px}._labelRight_1od17_67{display:flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}._labelText_1od17_75{font-size:11px}._timeText_1od17_79,._timeTextNoMargin_1od17_85{font-size:10px;color:#6b7280;flex-shrink:0}._labelTextRight_1od17_91{font-size:11px;margin-left:auto}._bubbleUser_1od17_98{background:#1668dc;color:#e5e7eb}._bubbleAssistant_1od17_104{background:#141414;color:#e5e7eb;transition:box-shadow 0s;position:relative}._bubbleHighlight_1od17_112{box-shadow:0 0 10px #1668dc99}._bubbleHighlightFading_1od17_116{box-shadow:0 0 10px #1668dc00;transition:box-shadow 5s ease-out}._borderSvg_1od17_121{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none;overflow:visible}._borderSvgFading_1od17_130{opacity:0;transition:opacity 5s ease-out}._borderRect_1od17_135{animation:_dashRotate_1od17_1 4s linear infinite}@keyframes _dashRotate_1od17_1{0%{stroke-dashoffset:0}to{stroke-dashoffset:-100}}._bubblePlan_1od17_144{background:#141428;border:5px solid #1668dc;font-size:12px;color:#e5e7eb}._bubbleSubAgent_1od17_152{background:#1a1a2e;color:#e5e7eb}._bubbleSelection_1od17_158{background:#1a3a1a;color:#e5e7eb}._systemTagLabel_1od17_166{font-size:12px}._skillLabel_1od17_170{font-size:12px;color:#a78bfa}._systemTagPre_1od17_175{font-size:12px;color:#9ca3af;white-space:pre-wrap;word-break:break-all;margin:0}._collapseMargin_1od17_183{margin:4px 0}._collapseNoMargin_1od17_187{margin:0}._thinkingLabel_1od17_193{font-size:12px}._toolBox_1od17_199{background:#1a1a2e;border:1px solid #2a2a3e;border-radius:8px;padding:8px 12px;margin:6px 0;font-size:12px}._toolLabel_1od17_208{color:#a78bfa}._codePre_1od17_214{font-size:12px;margin:4px 0 0;white-space:pre-wrap;word-break:break-all;background:#0d1117;border-radius:4px;padding:6px 8px}._pathTag_1od17_226{color:#7dd3fc;font-size:11px}._descSpan_1od17_233{color:#6b7280;font-weight:400}._secondarySpan_1od17_238{color:#6b7280}._patternSpan_1od17_242{color:#fbbf24}._kvContainer_1od17_248{margin-top:4px;font-size:11px}._kvItem_1od17_253{margin:2px 0}._kvKey_1od17_257{color:#7dd3fc}._kvValue_1od17_261{color:#9ca3af}._toolResult_1od17_267{background:#111827;border:1px solid #1e293b;border-radius:6px;padding:6px 10px;margin:2px 0 6px;font-size:11px}._toolResultLabel_1od17_276{font-size:11px}._compactLabel_1od17_282{font-size:12px;color:#93c5fd}._compactPre_1od17_287{font-size:12px;color:#d1d5db;white-space:pre-wrap;word-break:break-all;margin:0}._viewRequestBtn_1od17_297{font-size:10px;color:#555;cursor:pointer;margin-left:6px;flex-shrink:0}._viewRequestBtn_1od17_297:hover{color:#93c5fd}._optionList_1od17_311{padding-left:8px}._optionDesc_1od17_315{color:#555;margin-left:6px;font-weight:400}._questionText_1od17_323{font-size:13px;color:#ccc;margin-bottom:4px}._questionSpacing_1od17_329{margin-bottom:10px}._option_1od17_311{font-size:12px;padding:1px 0}._centerEmpty_1j3v3_1{display:flex;align-items:center;justify-content:center;height:100%}._container_1j3v3_8{height:100%;overflow:auto;padding:16px 24px;display:flex;flex-direction:column}._sessionDividerText_1j3v3_16{font-size:11px;color:#555}._lastResponseLabel_1j3v3_21{font-size:11px}._resizer_yamj2_1{width:6px;cursor:col-resize;background:#1f1f1f;flex-shrink:0;transition:background .2s}._resizer_yamj2_1:hover{background:#3b82f6}._layout_11bka_1{height:100vh;overflow:hidden}._header_11bka_6{background:#111;border-bottom:1px solid #1f1f1f;padding:0 24px;height:60px;line-height:60px}._content_11bka_14{flex:1;overflow:hidden}._mainContainer_11bka_19{display:flex;height:100%}._leftPanel_11bka_24{flex-shrink:0;border-right:1px solid #1f1f1f;display:flex;flex-direction:column;background:#0a0a0a}._leftPanelHeader_11bka_32{padding:10px 16px;border-bottom:1px solid #1f1f1f;font-size:13px;color:#9ca3af;font-weight:500;display:flex;justify-content:space-between;align-items:center}._leftPanelCount_11bka_43{font-size:12px;color:#555;font-weight:400}._leftPanelBody_11bka_49{flex:1;overflow:hidden}._rightPanel_11bka_54{flex:1;overflow:hidden;background:#0d0d0d}._modalActions_11bka_60{margin-bottom:12px}._spinCenter_11bka_64{text-align:center;padding:40px}._emptyCenter_11bka_69{text-align:center;color:#999;padding:40px}._logCheckbox_11bka_75{margin-right:8px}._logListItem_11bka_79{cursor:pointer;padding:8px 12px}._logItemRow_11bka_84{display:flex;align-items:center;width:100%;justify-content:space-between;flex-wrap:nowrap;gap:12px}._logItemRow_11bka_84>span:first-child{display:flex;align-items:center;flex:1;min-width:0;overflow:hidden}._logFileName_11bka_101{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}._logItemRow_11bka_84>span:last-child{display:flex;align-items:center;flex-shrink:0;gap:8px}._logFileIcon_11bka_114{margin-right:8px;color:#3b82f6;flex-shrink:0}._folderIcon_11bka_120{margin-right:8px}._logTag_11bka_124{margin-left:8px}._loadingOverlay_11bka_128{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#000000bf;display:flex;align-items:center;justify-content:center;z-index:9999}._loadingText_11bka_141{color:#fff;font-size:16px;font-weight:700}._footer_11bka_147{height:18px;background:#000;display:flex;align-items:center;justify-content:flex-end;padding:0 12px;flex-shrink:0}._footerRight_11bka_157{display:flex;align-items:center;gap:6px;font-size:11px;color:#555}._footerLink_11bka_165{display:inline-flex;align-items:center;gap:3px;color:#555;text-decoration:none}._footerLink_11bka_165:hover{color:#888}._footerIcon_11bka_177{width:12px;height:12px}._footerDivider_11bka_182{color:#333}._footerText_11bka_186{color:#555}
|
package/dist/favicon.ico
ADDED
|
Binary file
|
package/dist/index.html
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Claude Code Viewer</title>
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
<
|
|
7
|
+
<link rel="icon" href="/favicon.ico?v=1">
|
|
8
|
+
<link rel="shortcut icon" href="/favicon.ico?v=1">
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CN78DISW.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DhZ8St4J.css">
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
13
|
<div id="root"></div>
|
package/interceptor.js
CHANGED
|
@@ -31,7 +31,7 @@ function generateNewLogFilePath() {
|
|
|
31
31
|
try { cwd = process.cwd(); } catch { cwd = homedir(); }
|
|
32
32
|
const projectName = basename(cwd).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
|
|
33
33
|
const dir = join(homedir(), '.claude', 'cc-viewer', projectName);
|
|
34
|
-
try { mkdirSync(dir, { recursive: true }); } catch {}
|
|
34
|
+
try { mkdirSync(dir, { recursive: true }); } catch { }
|
|
35
35
|
return { filePath: join(dir, `${projectName}_${ts}.jsonl`), dir, projectName };
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -44,7 +44,7 @@ function findRecentLog(dir, projectName) {
|
|
|
44
44
|
.reverse();
|
|
45
45
|
if (files.length === 0) return null;
|
|
46
46
|
return join(dir, files[0]);
|
|
47
|
-
} catch {}
|
|
47
|
+
} catch { }
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -67,9 +67,9 @@ function cleanupTempFiles(dir, projectName) {
|
|
|
67
67
|
} else {
|
|
68
68
|
renameSync(tempPath, newPath);
|
|
69
69
|
}
|
|
70
|
-
} catch {}
|
|
70
|
+
} catch { }
|
|
71
71
|
}
|
|
72
|
-
} catch {}
|
|
72
|
+
} catch { }
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
// Resume 状态(供 server.js 使用)
|
|
@@ -119,16 +119,24 @@ const _initPromise = (async () => {
|
|
|
119
119
|
try {
|
|
120
120
|
const recentLog = findRecentLog(_logDir, _projectName);
|
|
121
121
|
if (recentLog) {
|
|
122
|
-
//
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
// Check if file is modified within 1 hour
|
|
123
|
+
const stats = statSync(recentLog);
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const diff = now - stats.mtime;
|
|
126
|
+
const oneHour = 60 * 60 * 1000;
|
|
127
|
+
|
|
128
|
+
if (diff < oneHour) {
|
|
129
|
+
// 设置临时文件,不阻塞
|
|
130
|
+
const tempFile = _newLogFile.replace('.jsonl', '_temp.jsonl');
|
|
131
|
+
LOG_FILE = tempFile;
|
|
132
|
+
_resumeState = {
|
|
133
|
+
recentFile: recentLog,
|
|
134
|
+
recentFileName: basename(recentLog),
|
|
135
|
+
tempFile,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
130
138
|
}
|
|
131
|
-
} catch {}
|
|
139
|
+
} catch { }
|
|
132
140
|
})();
|
|
133
141
|
|
|
134
142
|
export { LOG_FILE, _initPromise, _resumeState, _choicePromise, resolveResumeChoice, _projectName };
|
|
@@ -167,7 +175,7 @@ function migrateConversationContext(oldFile, newFile) {
|
|
|
167
175
|
break;
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
|
-
} catch {}
|
|
178
|
+
} catch { }
|
|
171
179
|
}
|
|
172
180
|
|
|
173
181
|
if (originIndex < 0) return; // 找不到起点,不迁移
|
|
@@ -180,7 +188,7 @@ function migrateConversationContext(oldFile, newFile) {
|
|
|
180
188
|
if (isPreflightEntry(prev)) {
|
|
181
189
|
migrationStart = originIndex - 1;
|
|
182
190
|
}
|
|
183
|
-
} catch {}
|
|
191
|
+
} catch { }
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
// 迁移条目写入新文件
|
|
@@ -194,7 +202,7 @@ function migrateConversationContext(oldFile, newFile) {
|
|
|
194
202
|
} else {
|
|
195
203
|
writeFileSync(oldFile, '');
|
|
196
204
|
}
|
|
197
|
-
} catch {}
|
|
205
|
+
} catch { }
|
|
198
206
|
}
|
|
199
207
|
|
|
200
208
|
function checkAndRotateLogFile() {
|
|
@@ -207,7 +215,7 @@ function checkAndRotateLogFile() {
|
|
|
207
215
|
LOG_FILE = filePath;
|
|
208
216
|
migrateConversationContext(oldFile, filePath);
|
|
209
217
|
}
|
|
210
|
-
} catch {}
|
|
218
|
+
} catch { }
|
|
211
219
|
}
|
|
212
220
|
|
|
213
221
|
// 从环境变量 ANTHROPIC_BASE_URL 提取域名用于请求匹配
|
|
@@ -217,7 +225,7 @@ function getBaseUrlHost() {
|
|
|
217
225
|
if (baseUrl) {
|
|
218
226
|
return new URL(baseUrl).hostname;
|
|
219
227
|
}
|
|
220
|
-
} catch {}
|
|
228
|
+
} catch { }
|
|
221
229
|
return null;
|
|
222
230
|
}
|
|
223
231
|
const CUSTOM_API_HOST = getBaseUrlHost();
|
|
@@ -227,7 +235,7 @@ function isAnthropicApiPath(urlStr) {
|
|
|
227
235
|
try {
|
|
228
236
|
const pathname = new URL(urlStr).pathname;
|
|
229
237
|
return /^\/v1\/messages(\/count_tokens|\/batches(\/.*)?)?$/.test(pathname)
|
|
230
|
-
|
|
238
|
+
|| /^\/api\/eval\/sdk-/.test(pathname);
|
|
231
239
|
} catch {
|
|
232
240
|
return /\/v1\/messages/.test(urlStr);
|
|
233
241
|
}
|
|
@@ -374,7 +382,7 @@ export function setupInterceptor() {
|
|
|
374
382
|
|
|
375
383
|
const _originalFetch = globalThis.fetch;
|
|
376
384
|
|
|
377
|
-
globalThis.fetch = async function(url, options) {
|
|
385
|
+
globalThis.fetch = async function (url, options) {
|
|
378
386
|
// cc-viewer 内部请求(翻译等)直接透传,不拦截
|
|
379
387
|
const internalHeader = options?.headers?.['x-cc-viewer-internal']
|
|
380
388
|
|| (options?.headers instanceof Headers && options.headers.get('x-cc-viewer-internal'));
|
|
@@ -387,7 +395,17 @@ export function setupInterceptor() {
|
|
|
387
395
|
|
|
388
396
|
try {
|
|
389
397
|
const urlStr = typeof url === 'string' ? url : url?.url || String(url);
|
|
390
|
-
|
|
398
|
+
// 检查 headers 中是否包含 x-cc-viewer-trace 标记
|
|
399
|
+
const headers = options?.headers || {};
|
|
400
|
+
const isProxyTrace = headers['x-cc-viewer-trace'] === 'true' || headers['x-cc-viewer-trace'] === true;
|
|
401
|
+
|
|
402
|
+
// 如果是 proxy 转发的,或者符合 URL 规则
|
|
403
|
+
if (isProxyTrace || urlStr.includes('anthropic') || urlStr.includes('claude') || (CUSTOM_API_HOST && urlStr.includes(CUSTOM_API_HOST)) || isAnthropicApiPath(urlStr)) {
|
|
404
|
+
// 如果是 proxy 转发的,需要清理掉标记 header 避免发给上游
|
|
405
|
+
if (isProxyTrace && options?.headers) {
|
|
406
|
+
delete options.headers['x-cc-viewer-trace'];
|
|
407
|
+
}
|
|
408
|
+
|
|
391
409
|
const timestamp = new Date().toISOString();
|
|
392
410
|
let body = null;
|
|
393
411
|
if (options?.body) {
|
|
@@ -463,7 +481,7 @@ export function setupInterceptor() {
|
|
|
463
481
|
})()
|
|
464
482
|
};
|
|
465
483
|
}
|
|
466
|
-
} catch {}
|
|
484
|
+
} catch { }
|
|
467
485
|
|
|
468
486
|
// 用户新指令边界:检查日志文件大小,超过 500MB 则切换新文件
|
|
469
487
|
if (requestEntry?.mainAgent) {
|
|
@@ -534,7 +552,8 @@ export function setupInterceptor() {
|
|
|
534
552
|
const assembledMessage = assembleStreamMessage(events);
|
|
535
553
|
|
|
536
554
|
// 直接使用组装后的 message 对象作为 response.body
|
|
537
|
-
|
|
555
|
+
// 如果组装失败(例如非标准 SSE),则使用原始流内容
|
|
556
|
+
requestEntry.response.body = assembledMessage || streamedContent;
|
|
538
557
|
appendFileSync(LOG_FILE, JSON.stringify(requestEntry, null, 2) + '\n---\n');
|
|
539
558
|
} catch (err) {
|
|
540
559
|
requestEntry.response.body = streamedContent.slice(0, 1000);
|
|
@@ -603,4 +622,6 @@ export function setupInterceptor() {
|
|
|
603
622
|
setupInterceptor();
|
|
604
623
|
|
|
605
624
|
// 等待日志文件初始化完成后启动 Web Viewer 服务
|
|
606
|
-
_initPromise.then(() => import('
|
|
625
|
+
_initPromise.then(() => import('./server.js')).catch((err) => {
|
|
626
|
+
console.error('[CC-Viewer] Failed to start viewer server:', err);
|
|
627
|
+
});
|
package/locales/i18n.json
CHANGED
|
@@ -179,6 +179,11 @@
|
|
|
179
179
|
"tr": "\nKaldırmak için çalıştırın: ccv --uninstall",
|
|
180
180
|
"uk": "\nДля видалення виконайте: ccv --uninstall"
|
|
181
181
|
},
|
|
182
|
+
"cli.help": {
|
|
183
|
+
"zh": "CC Viewer CLI\n\n用法:\n ccv [options]\n ccv run -- <command> [args...]\n\n选项:\n -h, --help 显示帮助\n -v, --version 显示版本\n --uninstall 移除 CC Viewer 集成\n\n说明:\n 直接运行 ccv 将安装/修复 Claude Code 的集成 Hook。",
|
|
184
|
+
"en": "CC Viewer CLI\n\nUsage:\n ccv [options]\n ccv run -- <command> [args...]\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --uninstall Remove CC Viewer integration\n\nNotes:\n Running ccv without arguments installs/repairs the Claude Code hook.",
|
|
185
|
+
"zh-TW": "CC Viewer CLI\n\n用法:\n ccv [options]\n ccv run -- <command> [args...]\n\n選項:\n -h, --help 顯示說明\n -v, --version 顯示版本\n --uninstall 移除 CC Viewer 整合\n\n說明:\n 直接執行 ccv 會安裝/修復 Claude Code 的整合 Hook。"
|
|
186
|
+
},
|
|
182
187
|
"cli.usage.uninstallHint": {
|
|
183
188
|
"zh": "",
|
|
184
189
|
"en": "",
|
|
@@ -320,24 +325,24 @@
|
|
|
320
325
|
"uk": "❌ Не вдалося очистити shell hook: {error}"
|
|
321
326
|
},
|
|
322
327
|
"cli.uninstall.done": {
|
|
323
|
-
"zh": "\n🗑️ CC Viewer
|
|
324
|
-
"en": "\n🗑️ CC Viewer
|
|
325
|
-
"zh-TW": "\n🗑️ CC Viewer
|
|
326
|
-
"ko": "\n🗑️ CC Viewer
|
|
327
|
-
"ja": "\n🗑️ CC Viewer
|
|
328
|
-
"de": "\n🗑️ CC Viewer
|
|
329
|
-
"es": "\n🗑️ CC Viewer
|
|
330
|
-
"fr": "\n🗑️ CC Viewer
|
|
331
|
-
"it": "\n🗑️ CC Viewer
|
|
332
|
-
"da": "\n🗑️ CC Viewer
|
|
333
|
-
"pl": "\n🗑️ CC Viewer
|
|
334
|
-
"ru": "\n🗑️ CC Viewer
|
|
335
|
-
"ar": "\n🗑️
|
|
336
|
-
"no": "\n🗑️ CC Viewer
|
|
337
|
-
"pt-BR": "\n🗑️ CC Viewer
|
|
338
|
-
"th": "\n🗑️ CC Viewer
|
|
339
|
-
"tr": "\n🗑️ CC Viewer
|
|
340
|
-
"uk": "\n🗑️ CC Viewer
|
|
328
|
+
"zh": "\n🗑️ CC Viewer 集成已移除 (运行 'npm uninstall -g cc-viewer' 以彻底删除命令)",
|
|
329
|
+
"en": "\n🗑️ CC Viewer integration removed (Run 'npm uninstall -g cc-viewer' to remove command)",
|
|
330
|
+
"zh-TW": "\n🗑️ CC Viewer 集成已移除 (運行 'npm uninstall -g cc-viewer' 以徹底刪除命令)",
|
|
331
|
+
"ko": "\n🗑️ CC Viewer 통합 제거됨 ('npm uninstall -g cc-viewer'를 실행하여 명령 삭제)",
|
|
332
|
+
"ja": "\n🗑️ CC Viewer 統合を削除しました (コマンドを完全に削除するには 'npm uninstall -g cc-viewer' を実行)",
|
|
333
|
+
"de": "\n🗑️ CC Viewer-Integration entfernt (Führen Sie 'npm uninstall -g cc-viewer' aus, um den Befehl zu entfernen)",
|
|
334
|
+
"es": "\n🗑️ Integración de CC Viewer eliminada (Ejecute 'npm uninstall -g cc-viewer' para eliminar el comando)",
|
|
335
|
+
"fr": "\n🗑️ Intégration CC Viewer supprimée (Exécutez 'npm uninstall -g cc-viewer' pour supprimer la commande)",
|
|
336
|
+
"it": "\n🗑️ Integrazione CC Viewer rimossa (Esegui 'npm uninstall -g cc-viewer' per rimuovere il comando)",
|
|
337
|
+
"da": "\n🗑️ CC Viewer integration fjernet (Kør 'npm uninstall -g cc-viewer' for at fjerne kommandoen)",
|
|
338
|
+
"pl": "\n🗑️ Integracja CC Viewer usunięta (Uruchom 'npm uninstall -g cc-viewer', aby usunąć polecenie)",
|
|
339
|
+
"ru": "\n🗑️ Интеграция CC Viewer удалена (Запустите 'npm uninstall -g cc-viewer' для удаления команды)",
|
|
340
|
+
"ar": "\n🗑️ تمت إزالة تكامل CC Viewer (قم بتشغيل 'npm uninstall -g cc-viewer' لإزالة الأمر)",
|
|
341
|
+
"no": "\n🗑️ CC Viewer-integrasjon fjernet (Kjør 'npm uninstall -g cc-viewer' for å fjerne kommandoen)",
|
|
342
|
+
"pt-BR": "\n🗑️ Integração do CC Viewer removida (Execute 'npm uninstall -g cc-viewer' para remover o comando)",
|
|
343
|
+
"th": "\n🗑️ การรวม CC Viewer ถูกลบออก (เรียกใช้ 'npm uninstall -g cc-viewer' เพื่อลบคำสั่ง)",
|
|
344
|
+
"tr": "\n🗑️ CC Viewer entegrasyonu kaldırıldı (Komutu kaldırmak için 'npm uninstall -g cc-viewer' çalıştırın)",
|
|
345
|
+
"uk": "\n🗑️ Інтеграцію CC Viewer видалено (Запустіть 'npm uninstall -g cc-viewer', щоб видалити команду)"
|
|
341
346
|
},
|
|
342
347
|
"server.started": {
|
|
343
348
|
"zh": "\nClaude 请求监控服务已启动: http://{host}:{port}\n",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Claude Code Logger visualization management tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dist/",
|
|
45
45
|
"server.js",
|
|
46
46
|
"cli.js",
|
|
47
|
+
"proxy.js",
|
|
47
48
|
"interceptor.js",
|
|
48
49
|
"i18n.js",
|
|
49
50
|
"locales/",
|
package/proxy.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { setupInterceptor } from './interceptor.js';
|
|
7
|
+
|
|
8
|
+
// Setup interceptor to patch fetch
|
|
9
|
+
setupInterceptor();
|
|
10
|
+
|
|
11
|
+
function getOriginalBaseUrl() {
|
|
12
|
+
// 1. Check settings.json (Priority 1)
|
|
13
|
+
try {
|
|
14
|
+
const settingsPath = join(homedir(), '.claude', 'settings.json');
|
|
15
|
+
if (existsSync(settingsPath)) {
|
|
16
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
17
|
+
if (settings.env && settings.env.ANTHROPIC_BASE_URL) {
|
|
18
|
+
return settings.env.ANTHROPIC_BASE_URL;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 2. Check env var (Priority 2)
|
|
26
|
+
if (process.env.ANTHROPIC_BASE_URL) {
|
|
27
|
+
return process.env.ANTHROPIC_BASE_URL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 3. Default
|
|
31
|
+
return 'https://api.anthropic.com';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function startProxy() {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const server = createServer(async (req, res) => {
|
|
37
|
+
// [Debug] Log incoming request
|
|
38
|
+
// console.error(`[CC-Viewer Proxy] Received request: ${req.method} ${req.url}`);
|
|
39
|
+
|
|
40
|
+
// Handle CORS preflight if needed (though claude cli probably doesn't send OPTIONS)
|
|
41
|
+
|
|
42
|
+
const originalBaseUrl = getOriginalBaseUrl();
|
|
43
|
+
const targetUrl = new URL(req.url, originalBaseUrl);
|
|
44
|
+
|
|
45
|
+
// Use the patched fetch (which logs to cc-viewer)
|
|
46
|
+
try {
|
|
47
|
+
// Convert incoming headers
|
|
48
|
+
const headers = { ...req.headers };
|
|
49
|
+
delete headers.host; // Let fetch set the host
|
|
50
|
+
|
|
51
|
+
// [Fix] Handle compressed body
|
|
52
|
+
// If content-encoding is set (e.g. gzip), and we read the raw stream into a buffer,
|
|
53
|
+
// we are passing the compressed buffer as body.
|
|
54
|
+
// fetch will automatically add content-length, but might not handle content-encoding correctly if we just pass the buffer?
|
|
55
|
+
// Actually, fetch should handle it fine if we pass the headers.
|
|
56
|
+
|
|
57
|
+
// However, if we are reading the body here to pass it to fetch, we are buffering it.
|
|
58
|
+
// For large uploads this might be bad, but for text prompts it's fine.
|
|
59
|
+
|
|
60
|
+
// We need to read the body if any
|
|
61
|
+
const buffers = [];
|
|
62
|
+
for await (const chunk of req) {
|
|
63
|
+
buffers.push(chunk);
|
|
64
|
+
}
|
|
65
|
+
const body = Buffer.concat(buffers);
|
|
66
|
+
|
|
67
|
+
// [Debug] Log body size
|
|
68
|
+
// console.error(`[CC-Viewer Proxy] Request body size: ${body.length}`);
|
|
69
|
+
|
|
70
|
+
const fetchOptions = {
|
|
71
|
+
method: req.method,
|
|
72
|
+
headers: headers,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// 标记此请求为 CC-Viewer 代理转发的 Claude API 请求
|
|
76
|
+
// 拦截器识别到此 Header 会强制记录,忽略 URL 匹配规则
|
|
77
|
+
fetchOptions.headers['x-cc-viewer-trace'] = 'true';
|
|
78
|
+
|
|
79
|
+
if (body.length > 0) {
|
|
80
|
+
fetchOptions.body = body;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// [Crucial Fix]
|
|
84
|
+
// If originalBaseUrl is also a proxy or special endpoint, make sure we construct the full URL correctly.
|
|
85
|
+
// originalBaseUrl might end with /v1 or not.
|
|
86
|
+
// req.url from proxy server is usually just the path (e.g. /v1/messages) if client is configured with base_url=http://localhost:port
|
|
87
|
+
// So new URL(req.url, originalBaseUrl) should work.
|
|
88
|
+
|
|
89
|
+
// However, if originalBaseUrl already contains a path (e.g. /api/anthropic), and req.url is /v1/messages,
|
|
90
|
+
// new URL(req.url, originalBaseUrl) might treat req.url as absolute path if it starts with /, replacing the path in originalBaseUrl?
|
|
91
|
+
// Let's test: new URL('/v1/messages', 'https://example.com/api').toString() -> 'https://example.com/v1/messages' (path replaced!)
|
|
92
|
+
|
|
93
|
+
// This is why we get 404! The user's base URL is https://antchat.alipay.com/api/anthropic
|
|
94
|
+
// But our proxy constructs: https://antchat.alipay.com/v1/messages
|
|
95
|
+
// It lost the /api/anthropic part!
|
|
96
|
+
|
|
97
|
+
// We need to append req.url to the pathname of originalBaseUrl, carefully avoiding double slashes.
|
|
98
|
+
|
|
99
|
+
const originalUrlObj = new URL(originalBaseUrl);
|
|
100
|
+
// Ensure original pathname doesn't end with slash if we append
|
|
101
|
+
let basePath = originalUrlObj.pathname;
|
|
102
|
+
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
|
|
103
|
+
|
|
104
|
+
// req.url starts with / usually
|
|
105
|
+
const reqPath = req.url.startsWith('/') ? req.url : '/' + req.url;
|
|
106
|
+
|
|
107
|
+
// Check if we should join them
|
|
108
|
+
// If req.url is /v1/messages, and base is /api/anthropic, we want /api/anthropic/v1/messages
|
|
109
|
+
originalUrlObj.pathname = basePath + reqPath;
|
|
110
|
+
// Search params are already in req.url? Yes, req.url includes query string in Node http server.
|
|
111
|
+
// Wait, new URL(req.url, base) parses query string correctly.
|
|
112
|
+
// But if we manually concat pathname, we need to handle query string separately.
|
|
113
|
+
|
|
114
|
+
// Let's do it simpler: use string concatenation for the full URL
|
|
115
|
+
// But we need to handle the origin correctly.
|
|
116
|
+
|
|
117
|
+
// Better approach:
|
|
118
|
+
// 1. Remove trailing slash from originalBaseUrl
|
|
119
|
+
const cleanBase = originalBaseUrl.endsWith('/') ? originalBaseUrl.slice(0, -1) : originalBaseUrl;
|
|
120
|
+
// 2. Remove leading slash from req.url
|
|
121
|
+
const cleanReq = req.url.startsWith('/') ? req.url.slice(1) : req.url;
|
|
122
|
+
// 3. Join
|
|
123
|
+
const fullUrl = `${cleanBase}/${cleanReq}`;
|
|
124
|
+
|
|
125
|
+
// [Debug] Proxying to
|
|
126
|
+
// console.error(`[CC-Viewer Proxy] Forwarding to: ${fullUrl}`);
|
|
127
|
+
|
|
128
|
+
const response = await fetch(fullUrl, fetchOptions);
|
|
129
|
+
|
|
130
|
+
// [Crucial Fix]
|
|
131
|
+
// Handle decompression manually if needed.
|
|
132
|
+
// Node's fetch automatically decompresses if 'compress: true' is default?
|
|
133
|
+
// Actually, fetch handles gzip/deflate by default.
|
|
134
|
+
// But if we pipe the response body to the client response (res), we need to be careful.
|
|
135
|
+
// The issue is likely that we are trying to read the body as text/json for logging,
|
|
136
|
+
// but it might be compressed or binary.
|
|
137
|
+
|
|
138
|
+
// Let's modify how we handle the response body.
|
|
139
|
+
// We need to:
|
|
140
|
+
// 1. Pipe the response to the client (res) so Claude Code gets the data.
|
|
141
|
+
// 2. Clone the response to read it for logging? fetch response.clone() might not work with streaming body easily.
|
|
142
|
+
|
|
143
|
+
// Better approach: intercept the stream.
|
|
144
|
+
// Or simply: don't log the response body for now to avoid breaking the stream.
|
|
145
|
+
// User just wants to see the request in the viewer.
|
|
146
|
+
// If we want to log response, we need to handle it carefully.
|
|
147
|
+
|
|
148
|
+
// Let's check where ZlibError comes from. It likely comes from `response.text()` or `response.json()`
|
|
149
|
+
// if the content-encoding header is set but fetch didn't decompress it automatically?
|
|
150
|
+
// Or maybe we are double decompressing?
|
|
151
|
+
|
|
152
|
+
// Wait, if we use `response.body.pipe(res)`, that's fine.
|
|
153
|
+
// But do we read the body elsewhere?
|
|
154
|
+
|
|
155
|
+
// Let's look at how we log the response.
|
|
156
|
+
// We are not logging response body in this proxy.js currently.
|
|
157
|
+
// Wait, line 105: `response.body.pipe(res);`
|
|
158
|
+
|
|
159
|
+
// If the error happens, it might be because `fetch` failed to decompress?
|
|
160
|
+
// Or maybe `response.body` is already decompressed stream, but we are piping it to `res` which expects raw?
|
|
161
|
+
// No, `res` (http.ServerResponse) expects raw data.
|
|
162
|
+
|
|
163
|
+
// If `fetch` decompresses automatically, then `response.body` yields decompressed chunks.
|
|
164
|
+
// But `res` writes those chunks to the client.
|
|
165
|
+
// The client (Claude Code) expects compressed data if it sent `Accept-Encoding: gzip`.
|
|
166
|
+
// If we send decompressed data but keep `Content-Encoding: gzip` header, client will try to decompress again -> ZlibError!
|
|
167
|
+
|
|
168
|
+
// FIX: Remove content-encoding header from response headers before piping to client.
|
|
169
|
+
// This tells the client "the data I'm sending you is NOT compressed" (because fetch already decompressed it).
|
|
170
|
+
|
|
171
|
+
const responseHeaders = {};
|
|
172
|
+
for (const [key, value] of response.headers.entries()) {
|
|
173
|
+
// Skip Content-Encoding and Transfer-Encoding to let Node/Client handle it
|
|
174
|
+
if (key.toLowerCase() !== 'content-encoding' && key.toLowerCase() !== 'transfer-encoding' && key.toLowerCase() !== 'content-length') {
|
|
175
|
+
responseHeaders[key] = value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
res.writeHead(response.status, responseHeaders);
|
|
180
|
+
|
|
181
|
+
// Also log that we are piping
|
|
182
|
+
// console.error(`[CC-Viewer Proxy] Response status: ${response.status}`);
|
|
183
|
+
|
|
184
|
+
if (response.body) {
|
|
185
|
+
// We need to convert Web Stream (response.body) to Node Stream for piping to res
|
|
186
|
+
// Node 18+ fetch returns a Web ReadableStream.
|
|
187
|
+
// We can use Readable.fromWeb(response.body)
|
|
188
|
+
const { Readable } = await import('node:stream');
|
|
189
|
+
// @ts-ignore
|
|
190
|
+
const nodeStream = Readable.fromWeb(response.body);
|
|
191
|
+
nodeStream.pipe(res);
|
|
192
|
+
|
|
193
|
+
// Optional: Log response body for debugging (careful with streams)
|
|
194
|
+
// For now, let's just ensure reliability.
|
|
195
|
+
} else {
|
|
196
|
+
res.end();
|
|
197
|
+
}
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error('[CC-Viewer Proxy] Error:', err);
|
|
200
|
+
res.statusCode = 502;
|
|
201
|
+
res.end('Proxy Error');
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Start on random port
|
|
206
|
+
server.listen(0, '127.0.0.1', () => {
|
|
207
|
+
const address = server.address();
|
|
208
|
+
resolve(address.port);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
server.on('error', (err) => {
|
|
212
|
+
reject(err);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
package/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, watchFile, unwatchFile, statSync, readdirSync, renameSync, unlinkSync, openSync, readSync, closeSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join, extname, basename } from 'node:path';
|
|
5
5
|
import { homedir, userInfo, platform } from 'node:os';
|
|
@@ -25,14 +25,14 @@ function getUserProfile() {
|
|
|
25
25
|
const rn = execSync(`dscl . -read /Users/${name} RealName`, { encoding: 'utf-8', timeout: 3000 });
|
|
26
26
|
const match = rn.match(/RealName:\n?\s*(.+)/);
|
|
27
27
|
if (match && match[1].trim()) displayName = match[1].trim();
|
|
28
|
-
} catch {}
|
|
28
|
+
} catch { }
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
31
|
const buf = execSync(`dscl . -read /Users/${name} JPEGPhoto | tail -1 | xxd -r -p`, { timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
32
32
|
if (buf && buf.length > 100) {
|
|
33
33
|
avatarBase64 = `data:image/jpeg;base64,${buf.toString('base64')}`;
|
|
34
34
|
}
|
|
35
|
-
} catch {}
|
|
35
|
+
} catch { }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
_userProfile = { name: displayName, avatar: avatarBase64 };
|
|
@@ -123,7 +123,7 @@ function watchLogFile(logFile) {
|
|
|
123
123
|
clients.forEach(client => {
|
|
124
124
|
try {
|
|
125
125
|
client.write(`event: full_reload\ndata: ${JSON.stringify(newEntries)}\n\n`);
|
|
126
|
-
} catch {}
|
|
126
|
+
} catch { }
|
|
127
127
|
});
|
|
128
128
|
watchLogFile(LOG_FILE);
|
|
129
129
|
}
|
|
@@ -154,7 +154,7 @@ function handleRequest(req, res) {
|
|
|
154
154
|
// User preferences API
|
|
155
155
|
if (url === '/api/preferences' && method === 'GET') {
|
|
156
156
|
let prefs = {};
|
|
157
|
-
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch {}
|
|
157
|
+
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch { }
|
|
158
158
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
159
159
|
res.end(JSON.stringify(prefs));
|
|
160
160
|
return;
|
|
@@ -167,7 +167,7 @@ function handleRequest(req, res) {
|
|
|
167
167
|
try {
|
|
168
168
|
const incoming = JSON.parse(body);
|
|
169
169
|
let prefs = {};
|
|
170
|
-
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch {}
|
|
170
|
+
try { if (existsSync(PREFS_FILE)) prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8')); } catch { }
|
|
171
171
|
Object.assign(prefs, incoming);
|
|
172
172
|
writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
|
|
173
173
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -228,14 +228,14 @@ function handleRequest(req, res) {
|
|
|
228
228
|
clients.forEach(client => {
|
|
229
229
|
try {
|
|
230
230
|
client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
|
|
231
|
-
} catch {}
|
|
231
|
+
} catch { }
|
|
232
232
|
});
|
|
233
233
|
// 发送 full_reload 让客户端重新加载数据
|
|
234
234
|
const entries = readLogFile();
|
|
235
235
|
clients.forEach(client => {
|
|
236
236
|
try {
|
|
237
237
|
client.write(`event: full_reload\ndata: ${JSON.stringify(entries)}\n\n`);
|
|
238
|
-
} catch {}
|
|
238
|
+
} catch { }
|
|
239
239
|
});
|
|
240
240
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
241
241
|
res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
|
|
@@ -268,7 +268,7 @@ function handleRequest(req, res) {
|
|
|
268
268
|
const prefs = JSON.parse(readFileSync(PREFS_FILE, 'utf-8'));
|
|
269
269
|
if (prefs.lang) targetLang = prefs.lang;
|
|
270
270
|
}
|
|
271
|
-
} catch {}
|
|
271
|
+
} catch { }
|
|
272
272
|
if (!targetLang) targetLang = detectLanguage();
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -309,10 +309,10 @@ function handleRequest(req, res) {
|
|
|
309
309
|
body: JSON.stringify({
|
|
310
310
|
model: _cachedHaikuModel || 'claude-haiku-4-5-20251001',
|
|
311
311
|
max_tokens: 32000,
|
|
312
|
-
tools:[],
|
|
312
|
+
tools: [],
|
|
313
313
|
system: [{
|
|
314
|
-
type:"text",
|
|
315
|
-
text
|
|
314
|
+
type: "text",
|
|
315
|
+
text: `You are a translator. Translate the following text from ${from} to ${targetLang}. Output only the translated text, nothing else.`
|
|
316
316
|
}],
|
|
317
317
|
messages: [{ role: 'user', content: inputText }],
|
|
318
318
|
stream: false,
|
|
@@ -619,7 +619,7 @@ export async function startViewer() {
|
|
|
619
619
|
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
620
620
|
execSync(`${cmd} ${url}`, { stdio: 'ignore', timeout: 5000 });
|
|
621
621
|
}
|
|
622
|
-
} catch {}
|
|
622
|
+
} catch { }
|
|
623
623
|
startWatching();
|
|
624
624
|
resolve(server);
|
|
625
625
|
});
|
|
@@ -646,7 +646,7 @@ export function stopViewer() {
|
|
|
646
646
|
const newPath = tempFile.replace('_temp.jsonl', '.jsonl');
|
|
647
647
|
renameSync(tempFile, newPath);
|
|
648
648
|
}
|
|
649
|
-
} catch {}
|
|
649
|
+
} catch { }
|
|
650
650
|
}
|
|
651
651
|
for (const logFile of watchedFiles.keys()) {
|
|
652
652
|
unwatchFile(logFile);
|
|
@@ -674,7 +674,7 @@ function handleExit() {
|
|
|
674
674
|
const newPath = _resumeState.tempFile.replace('_temp.jsonl', '.jsonl');
|
|
675
675
|
renameSync(_resumeState.tempFile, newPath);
|
|
676
676
|
}
|
|
677
|
-
} catch {}
|
|
677
|
+
} catch { }
|
|
678
678
|
}
|
|
679
679
|
}
|
|
680
680
|
process.on('exit', handleExit);
|