Compare commits
28 Commits
dev/agents
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd21c570f | ||
|
|
c5e97709a5 | ||
|
|
f7adb7b28e | ||
|
|
7964fff175 | ||
|
|
fe06b57032 | ||
|
|
1b3a8f8803 | ||
|
|
cec8421dd9 | ||
|
|
1a7e90df39 | ||
|
|
e5741ae470 | ||
|
|
0152af5db9 | ||
|
|
e6b3e2a194 | ||
|
|
f7058dcdb5 | ||
|
|
e670302e9e | ||
|
|
5761e165fd | ||
|
|
8c4fec4f5f | ||
|
|
5d53571fe6 | ||
|
|
35fc4505b2 | ||
|
|
c7303775ad | ||
|
|
f7981b16cd | ||
|
|
b54687c4d5 | ||
|
|
0be4c3753f | ||
|
|
668e855a2d | ||
|
|
41108cea1d | ||
|
|
3b9e58a823 | ||
|
|
615fe7629e | ||
|
|
656a5f9a97 | ||
|
|
d2a0815cb7 | ||
|
|
7cc41d83cf |
7
Dockerfile
Normal file
7
Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
RUN npm install -g @musistudio/claude-code-router
|
||||||
|
|
||||||
|
EXPOSE 3456
|
||||||
|
|
||||||
|
CMD ["ccr", "start"]
|
||||||
37
README.md
37
README.md
@@ -1,14 +1,23 @@
|
|||||||
# Claude Code Router
|

|
||||||
|
|
||||||
|
[](README_zh.md)
|
||||||
|
[](https://discord.gg/rdftVMaUcS)
|
||||||
|
[](https://github.com/musistudio/claude-code-router/blob/main/LICENSE)
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
I am seeking funding support for this project to better sustain its development. If you have any ideas, feel free to reach out to me: [m@musiiot.top](mailto:m@musiiot.top)
|
I am seeking funding support for this project to better sustain its development. If you have any ideas, feel free to reach out to me: [m@musiiot.top](mailto:m@musiiot.top)
|
||||||
|
|
||||||
|
|
||||||
[中文版](README_zh.md)
|
|
||||||
|
|
||||||
> A powerful tool to route Claude Code requests to different models and customize any request.
|
> A powerful tool to route Claude Code requests to different models and customize any request.
|
||||||
|
|
||||||
|
> Now you can use models such as `GLM-4.5`, `Kimi-K2`, `Qwen3-Coder-480B-A35B`, and `DeepSeek v3.1` for free through the [iFlow Platform](https://platform.iflow.cn/docs/api-mode).
|
||||||
|
> You can use the `ccr ui` command to directly import the `iflow` template in the UI. It’s worth noting that iFlow limits each user to a concurrency of 1, which means you’ll need to route background requests to other models.
|
||||||
|
> If you’d like a better experience, you can try [iFlow CLI](https://cli.iflow.cn).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- **Model Routing**: Route requests to different models based on your needs (e.g., background tasks, thinking, long context).
|
- **Model Routing**: Route requests to different models based on your needs (e.g., background tasks, thinking, long context).
|
||||||
@@ -315,6 +324,7 @@ Transformers allow you to modify the request and response payloads to ensure com
|
|||||||
- `enhancetool`: Adds a layer of error tolerance to the tool call parameters returned by the LLM (this will cause the tool call information to no longer be streamed).
|
- `enhancetool`: Adds a layer of error tolerance to the tool call parameters returned by the LLM (this will cause the tool call information to no longer be streamed).
|
||||||
- `cleancache`: Clears the `cache_control` field from requests.
|
- `cleancache`: Clears the `cache_control` field from requests.
|
||||||
- `vertex-gemini`: Handles the Gemini API using Vertex authentication.
|
- `vertex-gemini`: Handles the Gemini API using Vertex authentication.
|
||||||
|
- `chutes-glm` Unofficial support for GLM 4.5 model via Chutes [chutes-glm-transformer.js](https://gist.github.com/vitobotta/2be3f33722e05e8d4f9d2b0138b8c863).
|
||||||
- `qwen-cli` (experimental): Unofficial support for qwen3-coder-plus model via Qwen CLI [qwen-cli.js](https://gist.github.com/musistudio/f5a67841ced39912fd99e42200d5ca8b).
|
- `qwen-cli` (experimental): Unofficial support for qwen3-coder-plus model via Qwen CLI [qwen-cli.js](https://gist.github.com/musistudio/f5a67841ced39912fd99e42200d5ca8b).
|
||||||
- `rovo-cli` (experimental): Unofficial support for gpt-5 via Atlassian Rovo Dev CLI [rovo-cli.js](https://gist.github.com/SaseQ/c2a20a38b11276537ec5332d1f7a5e53).
|
- `rovo-cli` (experimental): Unofficial support for gpt-5 via Atlassian Rovo Dev CLI [rovo-cli.js](https://gist.github.com/SaseQ/c2a20a38b11276537ec5332d1f7a5e53).
|
||||||
|
|
||||||
@@ -345,8 +355,9 @@ The `Router` object defines which model to use for different scenarios:
|
|||||||
- `longContext`: A model for handling long contexts (e.g., > 60K tokens).
|
- `longContext`: A model for handling long contexts (e.g., > 60K tokens).
|
||||||
- `longContextThreshold` (optional): The token count threshold for triggering the long context model. Defaults to 60000 if not specified.
|
- `longContextThreshold` (optional): The token count threshold for triggering the long context model. Defaults to 60000 if not specified.
|
||||||
- `webSearch`: Used for handling web search tasks and this requires the model itself to support the feature. If you're using openrouter, you need to add the `:online` suffix after the model name.
|
- `webSearch`: Used for handling web search tasks and this requires the model itself to support the feature. If you're using openrouter, you need to add the `:online` suffix after the model name.
|
||||||
|
- `image` (beta): Used for handling image-related tasks (supported by CCR’s built-in agent). If the model does not support tool calling, you need to set the `config.forceUseImageAgent` property to `true`.
|
||||||
|
|
||||||
You can also switch models dynamically in Claude Code with the `/model` command:
|
- You can also switch models dynamically in Claude Code with the `/model` command:
|
||||||
`/model provider_name,model_name`
|
`/model provider_name,model_name`
|
||||||
Example: `/model openrouter,anthropic/claude-3.5-sonnet`
|
Example: `/model openrouter,anthropic/claude-3.5-sonnet`
|
||||||
|
|
||||||
@@ -540,11 +551,11 @@ A huge thank you to all our sponsors for their generous support!
|
|||||||
- @b\*g
|
- @b\*g
|
||||||
- @\*亿
|
- @\*亿
|
||||||
- @\*辉
|
- @\*辉
|
||||||
- @JACK
|
- @JACK
|
||||||
- @\*光
|
- @\*光
|
||||||
- @W\*l
|
- @W\*l
|
||||||
- [@kesku](https://github.com/kesku)
|
- [@kesku](https://github.com/kesku)
|
||||||
- @水\*丫
|
- [@biguncle](https://github.com/biguncle)
|
||||||
- @二吉吉
|
- @二吉吉
|
||||||
- @a\*g
|
- @a\*g
|
||||||
- @\*林
|
- @\*林
|
||||||
@@ -557,5 +568,17 @@ A huge thank you to all our sponsors for their generous support!
|
|||||||
- @r\*c
|
- @r\*c
|
||||||
- [@qierkang](http://github.com/qierkang)
|
- [@qierkang](http://github.com/qierkang)
|
||||||
- @\*军
|
- @\*军
|
||||||
|
- [@snrise-z](http://github.com/snrise-z)
|
||||||
|
- @\*王
|
||||||
|
- [@greatheart1000](http://github.com/greatheart1000)
|
||||||
|
- @\*王
|
||||||
|
- @zcutlip
|
||||||
|
- [@Peng-YM](http://github.com/Peng-YM)
|
||||||
|
- @\*更
|
||||||
|
- @\*.
|
||||||
|
- @F\*t
|
||||||
|
- @\*政
|
||||||
|
- @\*铭
|
||||||
|
- @\*叶
|
||||||
|
|
||||||
(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)
|
(If your name is masked, please contact me via my homepage email to update it with your GitHub username.)
|
||||||
|
|||||||
31
README_zh.md
31
README_zh.md
@@ -1,11 +1,24 @@
|
|||||||
# Claude Code Router
|

|
||||||
|
|
||||||
|
[](README.md)
|
||||||
|
[](https://discord.gg/rdftVMaUcS)
|
||||||
|
[](https://github.com/musistudio/claude-code-router/blob/main/LICENSE)
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
我正在为该项目寻求资金支持,以更好地维持其发展。如果您有任何想法,请随时与我联系: [m@musiiot.top](mailto:m@musiiot.top)
|
我正在为该项目寻求资金支持,以更好地维持其发展。如果您有任何想法,请随时与我联系: [m@musiiot.top](mailto:m@musiiot.top)
|
||||||
|
|
||||||
> 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。
|
> 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。
|
||||||
|
|
||||||
|
> 现在你可以通过[心流平台](https://platform.iflow.cn/docs/api-mode)免费使用`GLM-4.5`、`Kimi-K2`、`Qwen3-Coder-480B-A35B`、`DeepSeek v3.1`等模型。
|
||||||
|
> 你可以使用`ccr ui`命令在UI中直接导入`iflow`模板,值得注意的是心流限制每位用户的并发数为1,意味着你需要将`background`路由到其他模型。
|
||||||
|
> 如果你想获得更好的体验,可以尝试[iFlow CLI](https://cli.iflow.cn)。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## ✨ 功能
|
## ✨ 功能
|
||||||
|
|
||||||
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
|
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
|
||||||
@@ -318,6 +331,7 @@ Transformers 允许您修改请求和响应负载,以确保与不同提供商
|
|||||||
- `longContext`: 用于处理长上下文(例如,> 60K 令牌)的模型。
|
- `longContext`: 用于处理长上下文(例如,> 60K 令牌)的模型。
|
||||||
- `longContextThreshold` (可选): 触发长上下文模型的令牌数阈值。如果未指定,默认为 60000。
|
- `longContextThreshold` (可选): 触发长上下文模型的令牌数阈值。如果未指定,默认为 60000。
|
||||||
- `webSearch`: 用于处理网络搜索任务,需要模型本身支持。如果使用`openrouter`需要在模型后面加上`:online`后缀。
|
- `webSearch`: 用于处理网络搜索任务,需要模型本身支持。如果使用`openrouter`需要在模型后面加上`:online`后缀。
|
||||||
|
- `image`(测试版): 用于处理图片类任务(采用CCR内置的agent支持),如果该模型不支持工具调用,需要将`config.forceUseImageAgent`属性设置为`true`。
|
||||||
|
|
||||||
您还可以使用 `/model` 命令在 Claude Code 中动态切换模型:
|
您还可以使用 `/model` 命令在 Claude Code 中动态切换模型:
|
||||||
`/model provider_name,model_name`
|
`/model provider_name,model_name`
|
||||||
@@ -499,6 +513,7 @@ jobs:
|
|||||||
- @*琢
|
- @*琢
|
||||||
- @*成
|
- @*成
|
||||||
- @Z*o
|
- @Z*o
|
||||||
|
- @\*琨
|
||||||
- [@congzhangzh](https://github.com/congzhangzh)
|
- [@congzhangzh](https://github.com/congzhangzh)
|
||||||
- @*_
|
- @*_
|
||||||
- @Z\*m
|
- @Z\*m
|
||||||
@@ -513,7 +528,7 @@ jobs:
|
|||||||
- @\*光
|
- @\*光
|
||||||
- @W\*l
|
- @W\*l
|
||||||
- [@kesku](https://github.com/kesku)
|
- [@kesku](https://github.com/kesku)
|
||||||
- @水\*丫
|
- [@biguncle](https://github.com/biguncle)
|
||||||
- @二吉吉
|
- @二吉吉
|
||||||
- @a\*g
|
- @a\*g
|
||||||
- @\*林
|
- @\*林
|
||||||
@@ -526,6 +541,18 @@ jobs:
|
|||||||
- @r\*c
|
- @r\*c
|
||||||
- [@qierkang](http://github.com/qierkang)
|
- [@qierkang](http://github.com/qierkang)
|
||||||
- @\*军
|
- @\*军
|
||||||
|
- [@snrise-z](http://github.com/snrise-z)
|
||||||
|
- @\*王
|
||||||
|
- [@greatheart1000](http://github.com/greatheart1000)
|
||||||
|
- @\*王
|
||||||
|
- @zcutlip
|
||||||
|
- [@Peng-YM](http://github.com/Peng-YM)
|
||||||
|
- @\*更
|
||||||
|
- @\*.
|
||||||
|
- @F\*t
|
||||||
|
- @\*政
|
||||||
|
- @\*铭
|
||||||
|
- @\*叶
|
||||||
|
|
||||||
(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。)
|
(如果您的名字被屏蔽,请通过我的主页电子邮件与我联系,以便使用您的 GitHub 用户名进行更新。)
|
||||||
|
|
||||||
|
|||||||
BIN
blog/images/claude-code-router-img.png
Normal file
BIN
blog/images/claude-code-router-img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
67
blog/images/roadmap.svg
Normal file
67
blog/images/roadmap.svg
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<svg viewBox="0 0 1200 420" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.road { stroke: #7aa2ff; stroke-width: 6; fill: none; filter: drop-shadow(0 6px 18px rgba(122,162,255,0.25)); }
|
||||||
|
.dash { stroke: rgba(122,162,255,0.25); stroke-width: 6; fill: none; stroke-dasharray: 2 18; }
|
||||||
|
.node { filter: drop-shadow(0 3px 10px rgba(126,240,193,0.35)); }
|
||||||
|
.node-circle { fill: #7ef0c1; }
|
||||||
|
.node-core { fill: #181b22; stroke: white; stroke-width: 1.5; }
|
||||||
|
.label-bg { fill: rgba(24,27,34,0.8); stroke: rgba(255,255,255,0.12); rx: 12; }
|
||||||
|
.label-text { fill: #e8ecf1; font-weight: 700; font-size: 14px; font-family: Arial, sans-serif; }
|
||||||
|
.label-sub { fill: #9aa6b2; font-weight: 500; font-size: 12px; font-family: Arial, sans-serif; }
|
||||||
|
.spark { fill: none; stroke: #ffd36e; stroke-width: 1.6; stroke-linecap: round; }
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background road with dash -->
|
||||||
|
<path class="dash" d="M60,330 C320,260 460,100 720,160 C930,205 990,260 1140,260"/>
|
||||||
|
|
||||||
|
<!-- Main road -->
|
||||||
|
<path class="road" d="M60,330 C320,260 460,100 720,160 C930,205 990,260 1140,260"/>
|
||||||
|
|
||||||
|
<!-- New Documentation Node -->
|
||||||
|
<g class="node" transform="translate(200,280)">
|
||||||
|
<circle class="node-circle" r="10"/>
|
||||||
|
<circle class="node-core" r="6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- New Documentation Label -->
|
||||||
|
<g transform="translate(80,120)">
|
||||||
|
<rect class="label-bg" width="260" height="92"/>
|
||||||
|
<text class="label-text" x="16" y="34">New Documentation</text>
|
||||||
|
<text class="label-sub" x="16" y="58">Clear structure, examples & best practices</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Plugin Marketplace Node -->
|
||||||
|
<g class="node" transform="translate(640,150)">
|
||||||
|
<circle class="node-circle" r="10"/>
|
||||||
|
<circle class="node-core" r="6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Plugin Marketplace Label -->
|
||||||
|
<g transform="translate(560,20)">
|
||||||
|
<rect class="label-bg" width="320" height="100"/>
|
||||||
|
<text class="label-text" x="16" y="34">Plugin Marketplace</text>
|
||||||
|
<text class="label-sub" x="16" y="58">Community submissions, ratings & version constraints</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- One More Thing Node -->
|
||||||
|
<g class="node" transform="translate(1080,255)">
|
||||||
|
<circle class="node-circle" r="10"/>
|
||||||
|
<circle class="node-core" r="6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- One More Thing Label -->
|
||||||
|
<g transform="translate(940,300)">
|
||||||
|
<rect class="label-bg" width="250" height="86"/>
|
||||||
|
<text class="label-text" x="16" y="34">One More Thing</text>
|
||||||
|
<text class="label-sub" x="16" y="58">🚀 Confidential project · Revealing soon</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Spark decorations -->
|
||||||
|
<g transform="translate(1125,290)">
|
||||||
|
<path class="spark" d="M0 0 L8 0 M4 -4 L4 4"/>
|
||||||
|
<path class="spark" d="M14 -2 L22 -2 M18 -6 L18 2"/>
|
||||||
|
<path class="spark" d="M-10 6 L-2 6 M-6 2 L-6 10"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
24
dockerfile
24
dockerfile
@@ -1,24 +0,0 @@
|
|||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy all files
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Install pnpm globally
|
|
||||||
RUN npm install -g pnpm
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# Fix rollup optional dependencies issue
|
|
||||||
RUN cd ui && npm install
|
|
||||||
|
|
||||||
# Build the entire project including UI
|
|
||||||
RUN pnpm run build
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 3456
|
|
||||||
|
|
||||||
# Start the router service
|
|
||||||
CMD ["node", "dist/cli.js", "start"]
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@musistudio/claude-code-router",
|
"name": "@musistudio/claude-code-router",
|
||||||
"version": "1.0.43",
|
"version": "1.0.49",
|
||||||
"description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
|
"description": "Use Claude Code without an Anthropics account and route it to another LLM provider",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ccr": "./dist/cli.js"
|
"ccr": "./dist/cli.js"
|
||||||
@@ -20,11 +20,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@musistudio/llms": "^1.0.28",
|
"@musistudio/llms": "^1.0.32",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"find-process": "^2.0.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"openurl": "^1.1.1",
|
"openurl": "^1.1.1",
|
||||||
"pino-rotating-file-stream": "^0.0.2",
|
"rotating-file-stream": "^3.2.7",
|
||||||
"tiktoken": "^1.0.21",
|
"tiktoken": "^1.0.21",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
111
pnpm-lock.yaml
generated
111
pnpm-lock.yaml
generated
@@ -12,20 +12,23 @@ importers:
|
|||||||
specifier: ^8.2.0
|
specifier: ^8.2.0
|
||||||
version: 8.2.0
|
version: 8.2.0
|
||||||
'@musistudio/llms':
|
'@musistudio/llms':
|
||||||
specifier: ^1.0.28
|
specifier: ^1.0.32
|
||||||
version: 1.0.28(ws@8.18.3)
|
version: 1.0.32(ws@8.18.3)
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
|
find-process:
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.0.0
|
||||||
json5:
|
json5:
|
||||||
specifier: ^2.2.3
|
specifier: ^2.2.3
|
||||||
version: 2.2.3
|
version: 2.2.3
|
||||||
openurl:
|
openurl:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
pino-rotating-file-stream:
|
rotating-file-stream:
|
||||||
specifier: ^0.0.2
|
specifier: ^3.2.7
|
||||||
version: 0.0.2
|
version: 3.2.7
|
||||||
tiktoken:
|
tiktoken:
|
||||||
specifier: ^1.0.21
|
specifier: ^1.0.21
|
||||||
version: 1.0.22
|
version: 1.0.22
|
||||||
@@ -241,11 +244,11 @@ packages:
|
|||||||
'@fastify/static@8.2.0':
|
'@fastify/static@8.2.0':
|
||||||
resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==}
|
resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==}
|
||||||
|
|
||||||
'@google/genai@1.14.0':
|
'@google/genai@1.16.0':
|
||||||
resolution: {integrity: sha512-jirYprAAJU1svjwSDVCzyVq+FrJpJd5CSxR/g2Ga/gZ0ZYZpcWjMS75KJl9y71K1mDN+tcx6s21CzCbB2R840g==}
|
resolution: {integrity: sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@modelcontextprotocol/sdk': ^1.11.0
|
'@modelcontextprotocol/sdk': ^1.11.4
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -266,8 +269,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
'@musistudio/llms@1.0.28':
|
'@musistudio/llms@1.0.32':
|
||||||
resolution: {integrity: sha512-rHvcJTtrFsRC7ayxz7ZXVoC7lZUwLtAHubdouUj+LYkv35Hr8S6K3lpOMXKYyqcKCtMvxbpjvM9MiwjCaleGEA==}
|
resolution: {integrity: sha512-i+dB7x4qxZ8oOM3TLijjJ2rwIOje6/ovyHdU8A5h6d2wcTKOd0JUpNixUgBO3dPJp2dYVXz0SSfhw7gzmt1Kkg==}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
@@ -338,6 +341,10 @@ packages:
|
|||||||
buffer-equal-constant-time@1.0.1:
|
buffer-equal-constant-time@1.0.1:
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
engines: {node: '>=7.0.0'}
|
engines: {node: '>=7.0.0'}
|
||||||
@@ -345,6 +352,10 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
commander@12.1.0:
|
||||||
|
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -460,6 +471,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==}
|
resolution: {integrity: sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
find-process@2.0.0:
|
||||||
|
resolution: {integrity: sha512-YUBQnteWGASJoEVVsOXy6XtKAY2O1FCsWnnvQ8y0YwgY1rZiKeVptnFvMu6RSELZAJOGklqseTnUGGs5D0bKmg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -500,8 +515,8 @@ packages:
|
|||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
google-auth-library@10.2.1:
|
google-auth-library@10.3.0:
|
||||||
resolution: {integrity: sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A==}
|
resolution: {integrity: sha512-ylSE3RlCRZfZB56PFJSfUCuiuPq83Fx8hqu1KPWGK8FVdSaxlp/qkeMMX/DT/18xkwXIHvXEXkZsljRwfrdEfQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
google-auth-library@9.15.1:
|
google-auth-library@9.15.1:
|
||||||
@@ -524,6 +539,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
has-flag@4.0.0:
|
||||||
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -609,6 +628,10 @@ packages:
|
|||||||
light-my-request@6.6.0:
|
light-my-request@6.6.0:
|
||||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||||
|
|
||||||
|
loglevel@1.9.2:
|
||||||
|
resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
lru-cache@11.1.0:
|
lru-cache@11.1.0:
|
||||||
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -672,8 +695,8 @@ packages:
|
|||||||
once@1.4.0:
|
once@1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
openai@5.12.2:
|
openai@5.16.0:
|
||||||
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==}
|
resolution: {integrity: sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
ws: ^8.18.0
|
ws: ^8.18.0
|
||||||
@@ -716,9 +739,6 @@ packages:
|
|||||||
pino-abstract-transport@2.0.0:
|
pino-abstract-transport@2.0.0:
|
||||||
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
|
||||||
|
|
||||||
pino-rotating-file-stream@0.0.2:
|
|
||||||
resolution: {integrity: sha512-knF+ReDBMQMB7gzBfuFpUmCrXpRen6YYh5Q9Ymmj//dDHeH4QEMwAV7VoGEEM+30s7VHqfbabazs9wxkMO2BIQ==}
|
|
||||||
|
|
||||||
pino-std-serializers@7.0.0:
|
pino-std-serializers@7.0.0:
|
||||||
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
|
||||||
|
|
||||||
@@ -769,8 +789,8 @@ packages:
|
|||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
rotating-file-stream@3.2.6:
|
rotating-file-stream@3.2.7:
|
||||||
resolution: {integrity: sha512-r8yShzMWUvWXkRzbOXDM1fEaMpc3qo2PzK7bBH/0p0Nl/uz8Mud/Y+0XTQxe3kbSnDF7qBH2tSe83WDKA7o3ww==}
|
resolution: {integrity: sha512-SVquhBEVvRFY+nWLUc791Y0MIlyZrEClRZwZFLLRgJKldHyV1z4e2e/dp9LPqCS3AM//uq/c3PnOFgjqnm5P+A==}
|
||||||
engines: {node: '>=14.0'}
|
engines: {node: '>=14.0'}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
@@ -868,6 +888,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0:
|
supports-preserve-symlinks-flag@1.0.0:
|
||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -901,8 +925,8 @@ packages:
|
|||||||
undici-types@7.10.0:
|
undici-types@7.10.0:
|
||||||
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
|
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
|
||||||
|
|
||||||
undici@7.13.0:
|
undici@7.15.0:
|
||||||
resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==}
|
resolution: {integrity: sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
uuid@11.1.0:
|
uuid@11.1.0:
|
||||||
@@ -1084,7 +1108,7 @@ snapshots:
|
|||||||
fastq: 1.19.1
|
fastq: 1.19.1
|
||||||
glob: 11.0.3
|
glob: 11.0.3
|
||||||
|
|
||||||
'@google/genai@1.14.0':
|
'@google/genai@1.16.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
google-auth-library: 9.15.1
|
google-auth-library: 9.15.1
|
||||||
ws: 8.18.3
|
ws: 8.18.3
|
||||||
@@ -1111,18 +1135,18 @@ snapshots:
|
|||||||
|
|
||||||
'@lukeed/ms@2.0.2': {}
|
'@lukeed/ms@2.0.2': {}
|
||||||
|
|
||||||
'@musistudio/llms@1.0.28(ws@8.18.3)':
|
'@musistudio/llms@1.0.32(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.54.0
|
'@anthropic-ai/sdk': 0.54.0
|
||||||
'@fastify/cors': 11.1.0
|
'@fastify/cors': 11.1.0
|
||||||
'@google/genai': 1.14.0
|
'@google/genai': 1.16.0
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
fastify: 5.5.0
|
fastify: 5.5.0
|
||||||
google-auth-library: 10.2.1
|
google-auth-library: 10.3.0
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
jsonrepair: 3.13.0
|
jsonrepair: 3.13.0
|
||||||
openai: 5.12.2(ws@8.18.3)
|
openai: 5.16.0(ws@8.18.3)
|
||||||
undici: 7.13.0
|
undici: 7.15.0
|
||||||
uuid: 11.1.0
|
uuid: 11.1.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@@ -1191,12 +1215,19 @@ snapshots:
|
|||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
|
chalk@4.1.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 4.3.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
|
||||||
color-convert@2.0.1:
|
color-convert@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
color-name: 1.1.4
|
color-name: 1.1.4
|
||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
commander@12.1.0: {}
|
||||||
|
|
||||||
content-disposition@0.5.4:
|
content-disposition@0.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -1354,6 +1385,12 @@ snapshots:
|
|||||||
fast-querystring: 1.1.2
|
fast-querystring: 1.1.2
|
||||||
safe-regex2: 5.0.0
|
safe-regex2: 5.0.0
|
||||||
|
|
||||||
|
find-process@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
chalk: 4.1.2
|
||||||
|
commander: 12.1.0
|
||||||
|
loglevel: 1.9.2
|
||||||
|
|
||||||
foreground-child@3.3.1:
|
foreground-child@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -1418,7 +1455,7 @@ snapshots:
|
|||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 2.0.0
|
path-scurry: 2.0.0
|
||||||
|
|
||||||
google-auth-library@10.2.1:
|
google-auth-library@10.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ecdsa-sig-formatter: 1.0.11
|
ecdsa-sig-formatter: 1.0.11
|
||||||
@@ -1461,6 +1498,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -1541,6 +1580,8 @@ snapshots:
|
|||||||
process-warning: 4.0.1
|
process-warning: 4.0.1
|
||||||
set-cookie-parser: 2.7.1
|
set-cookie-parser: 2.7.1
|
||||||
|
|
||||||
|
loglevel@1.9.2: {}
|
||||||
|
|
||||||
lru-cache@11.1.0: {}
|
lru-cache@11.1.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
@@ -1586,7 +1627,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
openai@5.12.2(ws@8.18.3):
|
openai@5.16.0(ws@8.18.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ws: 8.18.3
|
ws: 8.18.3
|
||||||
|
|
||||||
@@ -1613,10 +1654,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
split2: 4.2.0
|
split2: 4.2.0
|
||||||
|
|
||||||
pino-rotating-file-stream@0.0.2:
|
|
||||||
dependencies:
|
|
||||||
rotating-file-stream: 3.2.6
|
|
||||||
|
|
||||||
pino-std-serializers@7.0.0: {}
|
pino-std-serializers@7.0.0: {}
|
||||||
|
|
||||||
pino@9.9.0:
|
pino@9.9.0:
|
||||||
@@ -1666,7 +1703,7 @@ snapshots:
|
|||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rotating-file-stream@3.2.6: {}
|
rotating-file-stream@3.2.7: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1748,6 +1785,10 @@ snapshots:
|
|||||||
|
|
||||||
strip-eof@1.0.0: {}
|
strip-eof@1.0.0: {}
|
||||||
|
|
||||||
|
supports-color@7.2.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 4.0.0
|
||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
thread-stream@3.1.0:
|
thread-stream@3.1.0:
|
||||||
@@ -1770,7 +1811,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.10.0: {}
|
undici-types@7.10.0: {}
|
||||||
|
|
||||||
undici@7.13.0: {}
|
undici@7.15.0: {}
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,8 @@ class ImageCache {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateHash(base64Image: string): string {
|
|
||||||
const hash = createHash('sha256');
|
|
||||||
hash.update(base64Image);
|
|
||||||
return hash.digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
storeImage(id: string, source: any): void {
|
storeImage(id: string, source: any): void {
|
||||||
if (this.hasImage(id)) return;
|
if (this.hasImage(id)) return;
|
||||||
const base64Image = source.data
|
|
||||||
this.cache.set(id, {
|
this.cache.set(id, {
|
||||||
source,
|
source,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -62,12 +55,10 @@ export class ImageAgent implements IAgent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shouldHandle(req: any, config: any): boolean {
|
shouldHandle(req: any, config: any): boolean {
|
||||||
if (!config.Router.image) return false;
|
if (!config.Router.image || req.body.model === config.Router.image) return false;
|
||||||
const lastMessage = req.body.messages[req.body.messages.length - 1]
|
const lastMessage = req.body.messages[req.body.messages.length - 1]
|
||||||
if (lastMessage.role === 'user' && Array.isArray(lastMessage.content) &&lastMessage.content.find((item: any) => item.type === 'image')) {
|
if (!config.forceUseImageAgent && lastMessage.role === 'user' && Array.isArray(lastMessage.content) &&lastMessage.content.find((item: any) => item.type === 'image')) {
|
||||||
if (config.Router.image) {
|
req.body.model = config.Router.image
|
||||||
req.body.model = config.Router.image
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return req.body.messages.some((msg: any) => msg.role === 'user' && Array.isArray(msg.content) && msg.content.some((item: any) => item.type === 'image'))
|
return req.body.messages.some((msg: any) => msg.role === 'user' && Array.isArray(msg.content) && msg.content.some((item: any) => item.type === 'image'))
|
||||||
@@ -111,6 +102,7 @@ export class ImageAgent implements IAgent {
|
|||||||
"required": ["imageId", "task"]
|
"required": ["imageId", "task"]
|
||||||
},
|
},
|
||||||
handler: async (args, context) => {
|
handler: async (args, context) => {
|
||||||
|
console.log('args', JSON.stringify(args, null, 2))
|
||||||
const imageMessages = [];
|
const imageMessages = [];
|
||||||
let imageId;
|
let imageId;
|
||||||
|
|
||||||
@@ -129,7 +121,6 @@ export class ImageAgent implements IAgent {
|
|||||||
delete args.imageId;
|
delete args.imageId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add text message with the response
|
|
||||||
if (Object.keys(args).length > 0) {
|
if (Object.keys(args).length > 0) {
|
||||||
imageMessages.push({
|
imageMessages.push({
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -148,7 +139,10 @@ export class ImageAgent implements IAgent {
|
|||||||
model: context.config.Router.image,
|
model: context.config.Router.image,
|
||||||
system: [{
|
system: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: `你需要按照任务去解析图片`
|
text: `You must interpret and analyze images strictly according to the assigned task.
|
||||||
|
When an image placeholder is provided, your role is to parse the image content only within the scope of the user’s instructions.
|
||||||
|
Do not ignore or deviate from the task.
|
||||||
|
Always ensure that your response reflects a clear, accurate interpretation of the image aligned with the given objective.`
|
||||||
}],
|
}],
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
@@ -161,6 +155,7 @@ export class ImageAgent implements IAgent {
|
|||||||
}).then(res => res.json()).catch(err => {
|
}).then(res => res.json()).catch(err => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
console.log(agentResponse.content);
|
||||||
if (!agentResponse || !agentResponse.content) {
|
if (!agentResponse || !agentResponse.content) {
|
||||||
return 'analyzeImage Error';
|
return 'analyzeImage Error';
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/cli.ts
10
src/cli.ts
@@ -45,7 +45,8 @@ async function waitForService(
|
|||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
while (Date.now() - startTime < timeout) {
|
while (Date.now() - startTime < timeout) {
|
||||||
if (isServiceRunning()) {
|
const isRunning = await isServiceRunning()
|
||||||
|
if (isRunning) {
|
||||||
// Wait for an additional short period to ensure service is fully ready
|
// Wait for an additional short period to ensure service is fully ready
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
return true;
|
return true;
|
||||||
@@ -56,6 +57,7 @@ async function waitForService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
const isRunning = await isServiceRunning()
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "start":
|
case "start":
|
||||||
run();
|
run();
|
||||||
@@ -95,7 +97,7 @@ async function main() {
|
|||||||
inputData += chunk;
|
inputData += chunk;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.stdin.on("end", async () => {
|
process.stdin.on("end", async () => {
|
||||||
try {
|
try {
|
||||||
const input: StatusLineInput = JSON.parse(inputData);
|
const input: StatusLineInput = JSON.parse(inputData);
|
||||||
@@ -108,7 +110,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "code":
|
case "code":
|
||||||
if (!isServiceRunning()) {
|
if (!isRunning) {
|
||||||
console.log("Service not running, starting service...");
|
console.log("Service not running, starting service...");
|
||||||
const cliPath = join(__dirname, "cli.js");
|
const cliPath = join(__dirname, "cli.js");
|
||||||
const startProcess = spawn("node", [cliPath, "start"], {
|
const startProcess = spawn("node", [cliPath, "start"], {
|
||||||
@@ -153,7 +155,7 @@ async function main() {
|
|||||||
break;
|
break;
|
||||||
case "ui":
|
case "ui":
|
||||||
// Check if service is running
|
// Check if service is running
|
||||||
if (!isServiceRunning()) {
|
if (!isRunning) {
|
||||||
console.log("Service not running, starting service...");
|
console.log("Service not running, starting service...");
|
||||||
const cliPath = join(__dirname, "cli.js");
|
const cliPath = join(__dirname, "cli.js");
|
||||||
const startProcess = spawn("node", [cliPath, "start"], {
|
const startProcess = spawn("node", [cliPath, "start"], {
|
||||||
|
|||||||
284
src/index.ts
284
src/index.ts
@@ -12,9 +12,8 @@ import {
|
|||||||
savePid,
|
savePid,
|
||||||
} from "./utils/processCheck";
|
} from "./utils/processCheck";
|
||||||
import { CONFIG_FILE } from "./constants";
|
import { CONFIG_FILE } from "./constants";
|
||||||
import createWriteStream from "pino-rotating-file-stream";
|
import { createStream } from 'rotating-file-stream';
|
||||||
import { HOME_DIR } from "./constants";
|
import { HOME_DIR } from "./constants";
|
||||||
import { configureLogging } from "./utils/log";
|
|
||||||
import { sessionUsageCache } from "./utils/cache";
|
import { sessionUsageCache } from "./utils/cache";
|
||||||
import {SSEParserTransform} from "./utils/SSEParser.transform";
|
import {SSEParserTransform} from "./utils/SSEParser.transform";
|
||||||
import {SSESerializerTransform} from "./utils/SSESerializer.transform";
|
import {SSESerializerTransform} from "./utils/SSESerializer.transform";
|
||||||
@@ -22,7 +21,9 @@ import {rewriteStream} from "./utils/rewriteStream";
|
|||||||
import JSON5 from "json5";
|
import JSON5 from "json5";
|
||||||
import { IAgent } from "./agents/type";
|
import { IAgent } from "./agents/type";
|
||||||
import agentsManager from "./agents";
|
import agentsManager from "./agents";
|
||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
|
||||||
|
const event = new EventEmitter()
|
||||||
|
|
||||||
async function initializeClaudeConfig() {
|
async function initializeClaudeConfig() {
|
||||||
const homeDir = homedir();
|
const homeDir = homedir();
|
||||||
@@ -50,7 +51,8 @@ interface RunOptions {
|
|||||||
|
|
||||||
async function run(options: RunOptions = {}) {
|
async function run(options: RunOptions = {}) {
|
||||||
// Check if service is already running
|
// Check if service is already running
|
||||||
if (isServiceRunning()) {
|
const isRunning = await isServiceRunning()
|
||||||
|
if (isRunning) {
|
||||||
console.log("✅ Service is already running in the background.");
|
console.log("✅ Service is already running in the background.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -61,8 +63,6 @@ async function run(options: RunOptions = {}) {
|
|||||||
await cleanupLogFiles();
|
await cleanupLogFiles();
|
||||||
const config = await initConfig();
|
const config = await initConfig();
|
||||||
|
|
||||||
// Configure logging based on config
|
|
||||||
configureLogging(config);
|
|
||||||
|
|
||||||
let HOST = config.HOST || "127.0.0.1";
|
let HOST = config.HOST || "127.0.0.1";
|
||||||
|
|
||||||
@@ -95,15 +95,29 @@ async function run(options: RunOptions = {}) {
|
|||||||
: port;
|
: port;
|
||||||
|
|
||||||
// Configure logger based on config settings
|
// Configure logger based on config settings
|
||||||
|
const pad = num => (num > 9 ? "" : "0") + num;
|
||||||
|
const generator = (time, index) => {
|
||||||
|
if (!time) {
|
||||||
|
time = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
var month = time.getFullYear() + "" + pad(time.getMonth() + 1);
|
||||||
|
var day = pad(time.getDate());
|
||||||
|
var hour = pad(time.getHours());
|
||||||
|
var minute = pad(time.getMinutes());
|
||||||
|
|
||||||
|
return `./logs/ccr-${month}${day}${hour}${minute}${pad(time.getSeconds())}${index ? `_${index}` : ''}.log`;
|
||||||
|
};
|
||||||
const loggerConfig =
|
const loggerConfig =
|
||||||
config.LOG !== false
|
config.LOG !== false
|
||||||
? {
|
? {
|
||||||
level: config.LOG_LEVEL || "debug",
|
level: config.LOG_LEVEL || "debug",
|
||||||
stream: createWriteStream({
|
stream: createStream(generator, {
|
||||||
path: HOME_DIR,
|
path: HOME_DIR,
|
||||||
filename: config.LOGNAME || `./logs/ccr-${+new Date()}.log`,
|
|
||||||
maxFiles: 3,
|
maxFiles: 3,
|
||||||
interval: "1d",
|
interval: "1d",
|
||||||
|
compress: false,
|
||||||
|
maxSize: "50M"
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: false;
|
: false;
|
||||||
@@ -123,6 +137,15 @@ async function run(options: RunOptions = {}) {
|
|||||||
},
|
},
|
||||||
logger: loggerConfig,
|
logger: loggerConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add global error handlers to prevent the service from crashing
|
||||||
|
process.on("uncaughtException", (err) => {
|
||||||
|
server.log.error("Uncaught exception:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason, promise) => {
|
||||||
|
server.log.error("Unhandled rejection at:", promise, "reason:", reason);
|
||||||
|
});
|
||||||
// Add async preHandler hook for authentication
|
// Add async preHandler hook for authentication
|
||||||
server.addHook("preHandler", async (req, reply) => {
|
server.addHook("preHandler", async (req, reply) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -148,6 +171,9 @@ async function run(options: RunOptions = {}) {
|
|||||||
|
|
||||||
// append agent tools
|
// append agent tools
|
||||||
if (agent.tools.size) {
|
if (agent.tools.size) {
|
||||||
|
if (!req.body?.tools?.length) {
|
||||||
|
req.body.tools = []
|
||||||
|
}
|
||||||
req.body.tools.unshift(...Array.from(agent.tools.values()).map(item => {
|
req.body.tools.unshift(...Array.from(agent.tools.values()).map(item => {
|
||||||
return {
|
return {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -162,13 +188,20 @@ async function run(options: RunOptions = {}) {
|
|||||||
if (useAgents.length) {
|
if (useAgents.length) {
|
||||||
req.agents = useAgents;
|
req.agents = useAgents;
|
||||||
}
|
}
|
||||||
await router(req, reply, config);
|
await router(req, reply, {
|
||||||
|
config,
|
||||||
|
event
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
server.addHook("onSend", async (req, reply, payload) => {
|
server.addHook("onError", async (request, reply, error) => {
|
||||||
|
event.emit('onError', request, reply, error);
|
||||||
|
})
|
||||||
|
server.addHook("onSend", (req, reply, payload, done) => {
|
||||||
if (req.sessionId && req.url.startsWith("/v1/messages")) {
|
if (req.sessionId && req.url.startsWith("/v1/messages")) {
|
||||||
if (payload instanceof ReadableStream) {
|
if (payload instanceof ReadableStream) {
|
||||||
if (req.agents) {
|
if (req.agents) {
|
||||||
|
const abortController = new AbortController();
|
||||||
const eventStream = payload.pipeThrough(new SSEParserTransform())
|
const eventStream = payload.pipeThrough(new SSEParserTransform())
|
||||||
let currentAgent: undefined | IAgent;
|
let currentAgent: undefined | IAgent;
|
||||||
let currentToolIndex = -1
|
let currentToolIndex = -1
|
||||||
@@ -178,118 +211,171 @@ async function run(options: RunOptions = {}) {
|
|||||||
const toolMessages: any[] = []
|
const toolMessages: any[] = []
|
||||||
const assistantMessages: any[] = []
|
const assistantMessages: any[] = []
|
||||||
// 存储Anthropic格式的消息体,区分文本和工具类型
|
// 存储Anthropic格式的消息体,区分文本和工具类型
|
||||||
return rewriteStream(eventStream, async (data, controller) => {
|
return done(null, rewriteStream(eventStream, async (data, controller) => {
|
||||||
// 检测工具调用开始
|
try {
|
||||||
if (data.event === 'content_block_start' && data?.data?.content_block?.name) {
|
// 检测工具调用开始
|
||||||
const agent = req.agents.find((name: string) => agentsManager.getAgent(name)?.tools.get(data.data.content_block.name))
|
if (data.event === 'content_block_start' && data?.data?.content_block?.name) {
|
||||||
if (agent) {
|
const agent = req.agents.find((name: string) => agentsManager.getAgent(name)?.tools.get(data.data.content_block.name))
|
||||||
currentAgent = agentsManager.getAgent(agent)
|
if (agent) {
|
||||||
currentToolIndex = data.data.index
|
currentAgent = agentsManager.getAgent(agent)
|
||||||
currentToolName = data.data.content_block.name
|
currentToolIndex = data.data.index
|
||||||
currentToolId = data.data.content_block.id
|
currentToolName = data.data.content_block.name
|
||||||
|
currentToolId = data.data.content_block.id
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集工具参数
|
||||||
|
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data?.delta?.type === 'input_json_delta') {
|
||||||
|
currentToolArgs += data.data?.delta?.partial_json;
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 收集工具参数
|
// 工具调用完成,处理agent调用
|
||||||
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data?.delta?.type === 'input_json_delta') {
|
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') {
|
||||||
currentToolArgs += data.data?.delta?.partial_json;
|
try {
|
||||||
return undefined;
|
const args = JSON5.parse(currentToolArgs);
|
||||||
}
|
assistantMessages.push({
|
||||||
|
type: "tool_use",
|
||||||
// 工具调用完成,处理agent调用
|
id: currentToolId,
|
||||||
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') {
|
name: currentToolName,
|
||||||
try {
|
input: args
|
||||||
const args = JSON5.parse(currentToolArgs);
|
})
|
||||||
assistantMessages.push({
|
const toolResult = await currentAgent?.tools.get(currentToolName)?.handler(args, {
|
||||||
type: "tool_use",
|
req,
|
||||||
id: currentToolId,
|
config
|
||||||
name: currentToolName,
|
});
|
||||||
input: args
|
toolMessages.push({
|
||||||
})
|
"tool_use_id": currentToolId,
|
||||||
const toolResult = await currentAgent?.tools.get(currentToolName)?.handler(args, {
|
"type": "tool_result",
|
||||||
req,
|
"content": toolResult
|
||||||
config
|
})
|
||||||
});
|
currentAgent = undefined
|
||||||
toolMessages.push({
|
currentToolIndex = -1
|
||||||
"tool_use_id": currentToolId,
|
currentToolName = ''
|
||||||
"type": "tool_result",
|
currentToolArgs = ''
|
||||||
"content": toolResult
|
currentToolId = ''
|
||||||
})
|
} catch (e) {
|
||||||
currentAgent = undefined
|
console.log(e);
|
||||||
currentToolIndex = -1
|
}
|
||||||
currentToolName = ''
|
|
||||||
currentToolArgs = ''
|
|
||||||
currentToolId = ''
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.event === 'message_delta' && toolMessages.length) {
|
|
||||||
req.body.messages.push({
|
|
||||||
role: 'assistant',
|
|
||||||
content: assistantMessages
|
|
||||||
})
|
|
||||||
req.body.messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: toolMessages
|
|
||||||
})
|
|
||||||
const response = await fetch(`http://127.0.0.1:${config.PORT}/v1/messages`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'x-api-key': config.APIKEY,
|
|
||||||
'content-type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(req.body),
|
|
||||||
})
|
|
||||||
if (!response.ok) {
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const stream = response.body!.pipeThrough(new SSEParserTransform())
|
|
||||||
const reader = stream.getReader()
|
if (data.event === 'message_delta' && toolMessages.length) {
|
||||||
while (true) {
|
req.body.messages.push({
|
||||||
const {value, done} = await reader.read();
|
role: 'assistant',
|
||||||
if (done) {
|
content: assistantMessages
|
||||||
break;
|
})
|
||||||
|
req.body.messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: toolMessages
|
||||||
|
})
|
||||||
|
const response = await fetch(`http://127.0.0.1:${config.PORT}/v1/messages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'x-api-key': config.APIKEY,
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(req.body),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
if (['message_start', 'message_stop'].includes(value.event)) {
|
const stream = response.body!.pipeThrough(new SSEParserTransform())
|
||||||
continue
|
const reader = stream.getReader()
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const {value, done} = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (['message_start', 'message_stop'].includes(value.event)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查流是否仍然可写
|
||||||
|
if (!controller.desiredSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.enqueue(value)
|
||||||
|
}catch (readError: any) {
|
||||||
|
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||||
|
abortController.abort(); // 中止所有相关操作
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw readError;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
controller.enqueue(value)
|
return undefined
|
||||||
}
|
}
|
||||||
return undefined
|
return data
|
||||||
|
}catch (error: any) {
|
||||||
|
console.error('Unexpected error in stream processing:', error);
|
||||||
|
|
||||||
|
// 处理流提前关闭的错误
|
||||||
|
if (error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||||
|
abortController.abort();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他错误仍然抛出
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return data
|
}).pipeThrough(new SSESerializerTransform()))
|
||||||
}).pipeThrough(new SSESerializerTransform())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [originalStream, clonedStream] = payload.tee();
|
const [originalStream, clonedStream] = payload.tee();
|
||||||
const read = async (stream: ReadableStream) => {
|
const read = async (stream: ReadableStream) => {
|
||||||
const reader = stream.getReader();
|
const reader = stream.getReader();
|
||||||
while (true) {
|
try {
|
||||||
const { done, value } = await reader.read();
|
while (true) {
|
||||||
if (done) break;
|
const { done, value } = await reader.read();
|
||||||
// Process the value if needed
|
if (done) break;
|
||||||
const dataStr = new TextDecoder().decode(value);
|
// Process the value if needed
|
||||||
if (!dataStr.startsWith("event: message_delta")) {
|
const dataStr = new TextDecoder().decode(value);
|
||||||
continue;
|
if (!dataStr.startsWith("event: message_delta")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const str = dataStr.slice(27);
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(str);
|
||||||
|
sessionUsageCache.put(req.sessionId, message.usage);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
const str = dataStr.slice(27);
|
} catch (readError: any) {
|
||||||
try {
|
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
|
||||||
const message = JSON.parse(str);
|
console.error('Background read stream closed prematurely');
|
||||||
sessionUsageCache.put(req.sessionId, message.usage);
|
} else {
|
||||||
} catch {}
|
console.error('Error in background stream reading:', readError);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read(clonedStream);
|
read(clonedStream);
|
||||||
return originalStream
|
return done(null, originalStream)
|
||||||
}
|
}
|
||||||
sessionUsageCache.put(req.sessionId, payload.usage);
|
sessionUsageCache.put(req.sessionId, payload.usage);
|
||||||
|
if (typeof payload ==='object') {
|
||||||
|
if (payload.error) {
|
||||||
|
return done(payload.error, null)
|
||||||
|
} else {
|
||||||
|
return done(payload, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return payload;
|
if (typeof payload ==='object' && payload.error) {
|
||||||
|
return done(payload.error, null)
|
||||||
|
}
|
||||||
|
done(null, payload)
|
||||||
});
|
});
|
||||||
|
server.addHook("onSend", async (req, reply, payload) => {
|
||||||
|
event.emit('onSend', req, reply, payload);
|
||||||
|
return payload;
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
server.start();
|
server.start();
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/server.ts
103
src/server.ts
@@ -3,6 +3,8 @@ import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils";
|
|||||||
import { checkForUpdates, performUpdate } from "./utils";
|
import { checkForUpdates, performUpdate } from "./utils";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import fastifyStatic from "@fastify/static";
|
import fastifyStatic from "@fastify/static";
|
||||||
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs";
|
||||||
|
import { homedir } from "os";
|
||||||
|
|
||||||
export const createServer = (config: any): Server => {
|
export const createServer = (config: any): Server => {
|
||||||
const server = new Server(config);
|
const server = new Server(config);
|
||||||
@@ -63,16 +65,16 @@ export const createServer = (config: any): Server => {
|
|||||||
server.app.get("/ui", async (_, reply) => {
|
server.app.get("/ui", async (_, reply) => {
|
||||||
return reply.redirect("/ui/");
|
return reply.redirect("/ui/");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 版本检查端点
|
// 版本检查端点
|
||||||
server.app.get("/api/update/check", async (req, reply) => {
|
server.app.get("/api/update/check", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
// 获取当前版本
|
// 获取当前版本
|
||||||
const currentVersion = require("../package.json").version;
|
const currentVersion = require("../package.json").version;
|
||||||
const { hasUpdate, latestVersion, changelog } = await checkForUpdates(currentVersion);
|
const { hasUpdate, latestVersion, changelog } = await checkForUpdates(currentVersion);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasUpdate,
|
hasUpdate,
|
||||||
latestVersion: hasUpdate ? latestVersion : undefined,
|
latestVersion: hasUpdate ? latestVersion : undefined,
|
||||||
changelog: hasUpdate ? changelog : undefined
|
changelog: hasUpdate ? changelog : undefined
|
||||||
};
|
};
|
||||||
@@ -81,7 +83,7 @@ export const createServer = (config: any): Server => {
|
|||||||
reply.status(500).send({ error: "Failed to check for updates" });
|
reply.status(500).send({ error: "Failed to check for updates" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 执行更新端点
|
// 执行更新端点
|
||||||
server.app.post("/api/update/perform", async (req, reply) => {
|
server.app.post("/api/update/perform", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
@@ -91,10 +93,10 @@ export const createServer = (config: any): Server => {
|
|||||||
reply.status(403).send("Full access required to perform updates");
|
reply.status(403).send("Full access required to perform updates");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行更新逻辑
|
// 执行更新逻辑
|
||||||
const result = await performUpdate();
|
const result = await performUpdate();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to perform update:", error);
|
console.error("Failed to perform update:", error);
|
||||||
@@ -102,5 +104,92 @@ export const createServer = (config: any): Server => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取日志文件列表端点
|
||||||
|
server.app.get("/api/logs/files", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const logDir = join(homedir(), ".claude-code-router", "logs");
|
||||||
|
const logFiles: Array<{ name: string; path: string; size: number; lastModified: string }> = [];
|
||||||
|
|
||||||
|
if (existsSync(logDir)) {
|
||||||
|
const files = readdirSync(logDir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.log')) {
|
||||||
|
const filePath = join(logDir, file);
|
||||||
|
const stats = statSync(filePath);
|
||||||
|
|
||||||
|
logFiles.push({
|
||||||
|
name: file,
|
||||||
|
path: filePath,
|
||||||
|
size: stats.size,
|
||||||
|
lastModified: stats.mtime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按修改时间倒序排列
|
||||||
|
logFiles.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
return logFiles;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get log files:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to get log files" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取日志内容端点
|
||||||
|
server.app.get("/api/logs", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const filePath = (req.query as any).file as string;
|
||||||
|
let logFilePath: string;
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// 如果指定了文件路径,使用指定的路径
|
||||||
|
logFilePath = filePath;
|
||||||
|
} else {
|
||||||
|
// 如果没有指定文件路径,使用默认的日志文件路径
|
||||||
|
logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(logFilePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const logContent = readFileSync(logFilePath, 'utf8');
|
||||||
|
const logLines = logContent.split('\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
return logLines;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get logs:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to get logs" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除日志内容端点
|
||||||
|
server.app.delete("/api/logs", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const filePath = (req.query as any).file as string;
|
||||||
|
let logFilePath: string;
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// 如果指定了文件路径,使用指定的路径
|
||||||
|
logFilePath = filePath;
|
||||||
|
} else {
|
||||||
|
// 如果没有指定文件路径,使用默认的日志文件路径
|
||||||
|
logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(logFilePath)) {
|
||||||
|
writeFileSync(logFilePath, '', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: "Logs cleared successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to clear logs:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to clear logs" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { join } from 'path';
|
|||||||
|
|
||||||
export async function closeService() {
|
export async function closeService() {
|
||||||
const PID_FILE = join(HOME_DIR, '.claude-code-router.pid');
|
const PID_FILE = join(HOME_DIR, '.claude-code-router.pid');
|
||||||
|
const isRunning = await isServiceRunning()
|
||||||
if (!isServiceRunning()) {
|
|
||||||
|
if (!isRunning) {
|
||||||
console.log("No service is currently running.");
|
console.log("No service is currently running.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ import {join} from "path";
|
|||||||
export async function executeCodeCommand(args: string[] = []) {
|
export async function executeCodeCommand(args: string[] = []) {
|
||||||
// Set environment variables
|
// Set environment variables
|
||||||
const config = await readConfigFile();
|
const config = await readConfigFile();
|
||||||
|
const port = config.PORT || 3456;
|
||||||
const env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
...process.env,
|
...process.env,
|
||||||
ANTHROPIC_AUTH_TOKEN: config?.APIKEY || "test",
|
ANTHROPIC_AUTH_TOKEN: config?.APIKEY || "test",
|
||||||
ANTHROPIC_API_KEY: '',
|
ANTHROPIC_API_KEY: '',
|
||||||
ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.PORT || 3456}`,
|
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
||||||
|
NO_PROXY: `127.0.0.1`,
|
||||||
|
DISABLE_TELEMETRY: 'true',
|
||||||
|
DISABLE_COST_WARNINGS: 'true',
|
||||||
API_TIMEOUT_MS: String(config.API_TIMEOUT_MS ?? 600000), // Default to 10 minutes if not set
|
API_TIMEOUT_MS: String(config.API_TIMEOUT_MS ?? 600000), // Default to 10 minutes if not set
|
||||||
};
|
};
|
||||||
let settingsFlag: Record<string, any> | undefined;
|
let settingsFlag: Record<string, any> | undefined;
|
||||||
@@ -65,7 +69,6 @@ export async function executeCodeCommand(args: string[] = []) {
|
|||||||
const stdioConfig: StdioOptions = config.NON_INTERACTIVE_MODE
|
const stdioConfig: StdioOptions = config.NON_INTERACTIVE_MODE
|
||||||
? ["pipe", "inherit", "inherit"] // Pipe stdin for non-interactive
|
? ["pipe", "inherit", "inherit"] // Pipe stdin for non-interactive
|
||||||
: "inherit"; // Default inherited behavior
|
: "inherit"; // Default inherited behavior
|
||||||
console.log(joinedArgs)
|
|
||||||
const claudeProcess = spawn(
|
const claudeProcess = spawn(
|
||||||
claudePath + (joinedArgs ? ` ${joinedArgs}` : ""),
|
claudePath + (joinedArgs ? ` ${joinedArgs}` : ""),
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -83,25 +83,38 @@ export const readConfigFile = async () => {
|
|||||||
} catch (readError: any) {
|
} catch (readError: any) {
|
||||||
if (readError.code === "ENOENT") {
|
if (readError.code === "ENOENT") {
|
||||||
// Config file doesn't exist, prompt user for initial setup
|
// Config file doesn't exist, prompt user for initial setup
|
||||||
const name = await question("Enter Provider Name: ");
|
try {
|
||||||
const APIKEY = await question("Enter Provider API KEY: ");
|
// Initialize directories
|
||||||
const baseUrl = await question("Enter Provider URL: ");
|
await initDir();
|
||||||
const model = await question("Enter MODEL Name: ");
|
|
||||||
const config = Object.assign({}, DEFAULT_CONFIG, {
|
// Backup existing config file if it exists
|
||||||
Providers: [
|
const backupPath = await backupConfigFile();
|
||||||
{
|
if (backupPath) {
|
||||||
name,
|
console.log(
|
||||||
api_base_url: baseUrl,
|
`Backed up existing configuration file to ${backupPath}`
|
||||||
api_key: APIKEY,
|
);
|
||||||
models: [model],
|
}
|
||||||
},
|
const config = {
|
||||||
],
|
PORT: 3456,
|
||||||
Router: {
|
Providers: [],
|
||||||
default: `${name},${model}`,
|
Router: {},
|
||||||
},
|
}
|
||||||
});
|
// Create a minimal default config file
|
||||||
await writeConfigFile(config);
|
await writeConfigFile(config);
|
||||||
return config;
|
console.log(
|
||||||
|
"Created minimal default configuration file at ~/.claude-code-router/config.json"
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"Please edit this file with your actual configuration."
|
||||||
|
);
|
||||||
|
return config
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(
|
||||||
|
"Failed to create default configuration:",
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`Failed to read config file at ${CONFIG_FILE}`);
|
console.error(`Failed to read config file at ${CONFIG_FILE}`);
|
||||||
console.error("Error details:", readError.message);
|
console.error("Error details:", readError.message);
|
||||||
@@ -116,19 +129,19 @@ export const backupConfigFile = async () => {
|
|||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;
|
const backupPath = `${CONFIG_FILE}.${timestamp}.bak`;
|
||||||
await fs.copyFile(CONFIG_FILE, backupPath);
|
await fs.copyFile(CONFIG_FILE, backupPath);
|
||||||
|
|
||||||
// Clean up old backups, keeping only the 3 most recent
|
// Clean up old backups, keeping only the 3 most recent
|
||||||
try {
|
try {
|
||||||
const configDir = path.dirname(CONFIG_FILE);
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
const configFileName = path.basename(CONFIG_FILE);
|
const configFileName = path.basename(CONFIG_FILE);
|
||||||
const files = await fs.readdir(configDir);
|
const files = await fs.readdir(configDir);
|
||||||
|
|
||||||
// Find all backup files for this config
|
// Find all backup files for this config
|
||||||
const backupFiles = files
|
const backupFiles = files
|
||||||
.filter(file => file.startsWith(configFileName) && file.endsWith('.bak'))
|
.filter(file => file.startsWith(configFileName) && file.endsWith('.bak'))
|
||||||
.sort()
|
.sort()
|
||||||
.reverse(); // Sort in descending order (newest first)
|
.reverse(); // Sort in descending order (newest first)
|
||||||
|
|
||||||
// Delete all but the 3 most recent backups
|
// Delete all but the 3 most recent backups
|
||||||
if (backupFiles.length > 3) {
|
if (backupFiles.length > 3) {
|
||||||
for (let i = 3; i < backupFiles.length; i++) {
|
for (let i = 3; i < backupFiles.length; i++) {
|
||||||
@@ -139,7 +152,7 @@ export const backupConfigFile = async () => {
|
|||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
console.warn("Failed to clean up old backups:", cleanupError);
|
console.warn("Failed to clean up old backups:", cleanupError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return backupPath;
|
return backupPath;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { HOME_DIR } from "../constants";
|
|
||||||
|
|
||||||
const LOG_FILE = path.join(HOME_DIR, "claude-code-router.log");
|
|
||||||
|
|
||||||
// Ensure log directory exists
|
|
||||||
if (!fs.existsSync(HOME_DIR)) {
|
|
||||||
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global variable to store the logging configuration
|
|
||||||
let isLogEnabled: boolean | null = null;
|
|
||||||
let logLevel: string = "info";
|
|
||||||
|
|
||||||
// Function to configure logging
|
|
||||||
export function configureLogging(config: { LOG?: boolean; LOG_LEVEL?: string }) {
|
|
||||||
isLogEnabled = config.LOG !== false; // Default to true if not explicitly set to false
|
|
||||||
logLevel = config.LOG_LEVEL || "debug";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function log(...args: any[]) {
|
|
||||||
// If logging configuration hasn't been set, default to enabled
|
|
||||||
if (isLogEnabled === null) {
|
|
||||||
isLogEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLogEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const logMessage = `[${timestamp}] ${
|
|
||||||
Array.isArray(args)
|
|
||||||
? args
|
|
||||||
.map((arg) =>
|
|
||||||
typeof arg === "object" ? JSON.stringify(arg) : String(arg)
|
|
||||||
)
|
|
||||||
.join(" ")
|
|
||||||
: ""
|
|
||||||
}\n`;
|
|
||||||
|
|
||||||
// Append to log file
|
|
||||||
fs.appendFileSync(LOG_FILE, logMessage, "utf8");
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants';
|
import { PID_FILE, REFERENCE_COUNT_FILE } from '../constants';
|
||||||
import { readConfigFile } from '.';
|
import { readConfigFile } from '.';
|
||||||
|
import find from 'find-process';
|
||||||
|
|
||||||
|
export async function isProcessRunning(pid: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const processes = await find('pid', pid);
|
||||||
|
return processes.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function incrementReferenceCount() {
|
export function incrementReferenceCount() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -27,15 +37,14 @@ export function getReferenceCount(): number {
|
|||||||
return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;
|
return parseInt(readFileSync(REFERENCE_COUNT_FILE, 'utf-8')) || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isServiceRunning(): boolean {
|
export async function isServiceRunning(): Promise<boolean> {
|
||||||
if (!existsSync(PID_FILE)) {
|
if (!existsSync(PID_FILE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
||||||
process.kill(pid, 0);
|
return await isProcessRunning(pid);
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Process not running, clean up pid file
|
// Process not running, clean up pid file
|
||||||
cleanupPidFile();
|
cleanupPidFile();
|
||||||
@@ -62,7 +71,7 @@ export function getServicePid(): number | null {
|
|||||||
if (!existsSync(PID_FILE)) {
|
if (!existsSync(PID_FILE)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
const pid = parseInt(readFileSync(PID_FILE, 'utf-8'));
|
||||||
return isNaN(pid) ? null : pid;
|
return isNaN(pid) ? null : pid;
|
||||||
@@ -73,10 +82,10 @@ export function getServicePid(): number | null {
|
|||||||
|
|
||||||
export async function getServiceInfo() {
|
export async function getServiceInfo() {
|
||||||
const pid = getServicePid();
|
const pid = getServicePid();
|
||||||
const running = isServiceRunning();
|
const running = await isServiceRunning();
|
||||||
const config = await readConfigFile();
|
const config = await readConfigFile();
|
||||||
const port = config.PORT || 3456;
|
const port = config.PORT || 3456;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
running,
|
running,
|
||||||
pid,
|
pid,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
Tool,
|
Tool,
|
||||||
} from "@anthropic-ai/sdk/resources/messages";
|
} from "@anthropic-ai/sdk/resources/messages";
|
||||||
import { get_encoding } from "tiktoken";
|
import { get_encoding } from "tiktoken";
|
||||||
import { log } from "./log";
|
|
||||||
import { sessionUsageCache, Usage } from "./cache";
|
import { sessionUsageCache, Usage } from "./cache";
|
||||||
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
const enc = get_encoding("cl100k_base");
|
const enc = get_encoding("cl100k_base");
|
||||||
|
|
||||||
@@ -94,11 +94,8 @@ const getUseModel = async (
|
|||||||
(lastUsageThreshold || tokenCountThreshold) &&
|
(lastUsageThreshold || tokenCountThreshold) &&
|
||||||
config.Router.longContext
|
config.Router.longContext
|
||||||
) {
|
) {
|
||||||
log(
|
req.log.info(
|
||||||
"Using long context model due to token count:",
|
`Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}`
|
||||||
tokenCount,
|
|
||||||
"threshold:",
|
|
||||||
longContextThreshold
|
|
||||||
);
|
);
|
||||||
return config.Router.longContext;
|
return config.Router.longContext;
|
||||||
}
|
}
|
||||||
@@ -122,12 +119,12 @@ const getUseModel = async (
|
|||||||
req.body.model?.startsWith("claude-3-5-haiku") &&
|
req.body.model?.startsWith("claude-3-5-haiku") &&
|
||||||
config.Router.background
|
config.Router.background
|
||||||
) {
|
) {
|
||||||
log("Using background model for ", req.body.model);
|
req.log.info(`Using background model for ${req.body.model}`);
|
||||||
return config.Router.background;
|
return config.Router.background;
|
||||||
}
|
}
|
||||||
// if exits thinking, use the think model
|
// if exits thinking, use the think model
|
||||||
if (req.body.thinking && config.Router.think) {
|
if (req.body.thinking && config.Router.think) {
|
||||||
log("Using think model for ", req.body.thinking);
|
req.log.info(`Using think model for ${req.body.thinking}`);
|
||||||
return config.Router.think;
|
return config.Router.think;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -140,7 +137,8 @@ const getUseModel = async (
|
|||||||
return config.Router!.default;
|
return config.Router!.default;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const router = async (req: any, _res: any, config: any) => {
|
export const router = async (req: any, _res: any, context: any) => {
|
||||||
|
const { config, event } = context;
|
||||||
// Parse sessionId from metadata.user_id
|
// Parse sessionId from metadata.user_id
|
||||||
if (req.body.metadata?.user_id) {
|
if (req.body.metadata?.user_id) {
|
||||||
const parts = req.body.metadata.user_id.split("_session_");
|
const parts = req.body.metadata.user_id.split("_session_");
|
||||||
@@ -150,6 +148,11 @@ export const router = async (req: any, _res: any, config: any) => {
|
|||||||
}
|
}
|
||||||
const lastMessageUsage = sessionUsageCache.get(req.sessionId);
|
const lastMessageUsage = sessionUsageCache.get(req.sessionId);
|
||||||
const { messages, system = [], tools }: MessageCreateParamsBase = req.body;
|
const { messages, system = [], tools }: MessageCreateParamsBase = req.body;
|
||||||
|
if (config.REWRITE_SYSTEM_PROMPT && system.length > 1 && system[1]?.text?.includes('<env>')) {
|
||||||
|
const prompt = await readFile(config.REWRITE_SYSTEM_PROMPT, 'utf-8');
|
||||||
|
system[1].text = `${prompt}<env>${system[1].text.split('<env>').pop()}`
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenCount = calculateTokenCount(
|
const tokenCount = calculateTokenCount(
|
||||||
messages as MessageParam[],
|
messages as MessageParam[],
|
||||||
@@ -162,9 +165,11 @@ export const router = async (req: any, _res: any, config: any) => {
|
|||||||
try {
|
try {
|
||||||
const customRouter = require(config.CUSTOM_ROUTER_PATH);
|
const customRouter = require(config.CUSTOM_ROUTER_PATH);
|
||||||
req.tokenCount = tokenCount; // Pass token count to custom router
|
req.tokenCount = tokenCount; // Pass token count to custom router
|
||||||
model = await customRouter(req, config);
|
model = await customRouter(req, config, {
|
||||||
|
event
|
||||||
|
});
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
log("failed to load custom router", e.message);
|
req.log.error(`failed to load custom router: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@@ -172,7 +177,7 @@ export const router = async (req: any, _res: any, config: any) => {
|
|||||||
}
|
}
|
||||||
req.body.model = model;
|
req.body.model = model;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
log("Error in router middleware:", error.message);
|
req.log.error(`Error in router middleware: ${error.message}`);
|
||||||
req.body.model = config.Router!.default;
|
req.body.model = config.Router!.default;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Transformers } from "@/components/Transformers";
|
|||||||
import { Providers } from "@/components/Providers";
|
import { Providers } from "@/components/Providers";
|
||||||
import { Router } from "@/components/Router";
|
import { Router } from "@/components/Router";
|
||||||
import { JsonEditor } from "@/components/JsonEditor";
|
import { JsonEditor } from "@/components/JsonEditor";
|
||||||
|
import { LogViewer } from "@/components/LogViewer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useConfig } from "@/components/ConfigProvider";
|
import { useConfig } from "@/components/ConfigProvider";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp } from "lucide-react";
|
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -32,6 +33,7 @@ function App() {
|
|||||||
const { config, error } = useConfig();
|
const { config, error } = useConfig();
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
||||||
|
const [isLogViewerOpen, setIsLogViewerOpen] = useState(false);
|
||||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||||
// 版本检查状态
|
// 版本检查状态
|
||||||
@@ -276,6 +278,9 @@ function App() {
|
|||||||
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
<FileJson className="h-5 w-5" />
|
<FileJson className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setIsLogViewerOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
||||||
@@ -350,6 +355,11 @@ function App() {
|
|||||||
onOpenChange={setIsJsonEditorOpen}
|
onOpenChange={setIsJsonEditorOpen}
|
||||||
showToast={(message, type) => setToast({ message, type })}
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
/>
|
/>
|
||||||
|
<LogViewer
|
||||||
|
open={isLogViewerOpen}
|
||||||
|
onOpenChange={setIsLogViewerOpen}
|
||||||
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
|
/>
|
||||||
{/* 版本更新对话框 */}
|
{/* 版本更新对话框 */}
|
||||||
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
|
|||||||
@@ -95,15 +95,18 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
|||||||
think: typeof data.Router.think === 'string' ? data.Router.think : '',
|
think: typeof data.Router.think === 'string' ? data.Router.think : '',
|
||||||
longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',
|
longContext: typeof data.Router.longContext === 'string' ? data.Router.longContext : '',
|
||||||
longContextThreshold: typeof data.Router.longContextThreshold === 'number' ? data.Router.longContextThreshold : 60000,
|
longContextThreshold: typeof data.Router.longContextThreshold === 'number' ? data.Router.longContextThreshold : 60000,
|
||||||
webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : ''
|
webSearch: typeof data.Router.webSearch === 'string' ? data.Router.webSearch : '',
|
||||||
|
image: typeof data.Router.image === 'string' ? data.Router.image : ''
|
||||||
} : {
|
} : {
|
||||||
default: '',
|
default: '',
|
||||||
background: '',
|
background: '',
|
||||||
think: '',
|
think: '',
|
||||||
longContext: '',
|
longContext: '',
|
||||||
longContextThreshold: 60000,
|
longContextThreshold: 60000,
|
||||||
webSearch: ''
|
webSearch: '',
|
||||||
}
|
image: ''
|
||||||
|
},
|
||||||
|
CUSTOM_ROUTER_PATH: typeof data.CUSTOM_ROUTER_PATH === 'string' ? data.CUSTOM_ROUTER_PATH : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
setConfig(validConfig);
|
setConfig(validConfig);
|
||||||
@@ -131,8 +134,10 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
|
|||||||
think: '',
|
think: '',
|
||||||
longContext: '',
|
longContext: '',
|
||||||
longContextThreshold: 60000,
|
longContextThreshold: 60000,
|
||||||
webSearch: ''
|
webSearch: '',
|
||||||
}
|
image: ''
|
||||||
|
},
|
||||||
|
CUSTOM_ROUTER_PATH: ''
|
||||||
});
|
});
|
||||||
setError(err as Error);
|
setError(err as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
726
ui/src/components/LogViewer.tsx
Normal file
726
ui/src/components/LogViewer.tsx
Normal file
@@ -0,0 +1,726 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import Editor from '@monaco-editor/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers } from 'lucide-react';
|
||||||
|
|
||||||
|
interface LogViewerProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
showToast?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
message: string; // 现在这个字段直接包含原始JSON字符串
|
||||||
|
source?: string;
|
||||||
|
reqId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedLogs {
|
||||||
|
[reqId: string]: LogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogGroupSummary {
|
||||||
|
reqId: string;
|
||||||
|
logCount: number;
|
||||||
|
firstLog: string;
|
||||||
|
lastLog: string;
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedLogsResponse {
|
||||||
|
grouped: boolean;
|
||||||
|
groups: { [reqId: string]: LogEntry[] };
|
||||||
|
summary: {
|
||||||
|
totalRequests: number;
|
||||||
|
totalLogs: number;
|
||||||
|
requests: LogGroupSummary[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
|
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
|
const [groupByReqId, setGroupByReqId] = useState(false);
|
||||||
|
const [groupedLogs, setGroupedLogs] = useState<GroupedLogsResponse | null>(null);
|
||||||
|
const [selectedReqId, setSelectedReqId] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const workerRef = useRef<Worker | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadLogFiles();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// 创建内联 Web Worker
|
||||||
|
const createInlineWorker = (): Worker => {
|
||||||
|
const workerCode = `
|
||||||
|
// 日志聚合Web Worker
|
||||||
|
self.onmessage = function(event) {
|
||||||
|
const { type, data } = event.data;
|
||||||
|
|
||||||
|
if (type === 'groupLogsByReqId') {
|
||||||
|
try {
|
||||||
|
const { logs } = data;
|
||||||
|
|
||||||
|
// 按reqId聚合日志
|
||||||
|
const groupedLogs = {};
|
||||||
|
|
||||||
|
logs.forEach((log, index) => {
|
||||||
|
log = JSON.parse(log);
|
||||||
|
let reqId = log.reqId || 'no-req-id';
|
||||||
|
|
||||||
|
if (!groupedLogs[reqId]) {
|
||||||
|
groupedLogs[reqId] = [];
|
||||||
|
}
|
||||||
|
groupedLogs[reqId].push(log);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按时间戳排序每个组的日志
|
||||||
|
Object.keys(groupedLogs).forEach(reqId => {
|
||||||
|
groupedLogs[reqId].sort((a, b) => a.time - b.time);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提取model信息
|
||||||
|
const extractModelInfo = (reqId) => {
|
||||||
|
const logGroup = groupedLogs[reqId];
|
||||||
|
for (const log of logGroup) {
|
||||||
|
try {
|
||||||
|
// 尝试从message字段解析JSON
|
||||||
|
if (log.type === 'request body' && log.data && log.data.model) {
|
||||||
|
return log.data.model;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,继续尝试下一条日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成摘要信息
|
||||||
|
const summary = {
|
||||||
|
totalRequests: Object.keys(groupedLogs).length,
|
||||||
|
totalLogs: logs.length,
|
||||||
|
requests: Object.keys(groupedLogs).map(reqId => ({
|
||||||
|
reqId,
|
||||||
|
logCount: groupedLogs[reqId].length,
|
||||||
|
firstLog: groupedLogs[reqId][0]?.time,
|
||||||
|
lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.time,
|
||||||
|
model: extractModelInfo(reqId)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
grouped: true,
|
||||||
|
groups: groupedLogs,
|
||||||
|
summary
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送结果回主线程
|
||||||
|
self.postMessage({
|
||||||
|
type: 'groupLogsResult',
|
||||||
|
data: response
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 发送错误回主线程
|
||||||
|
self.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||||
|
const workerUrl = URL.createObjectURL(blob);
|
||||||
|
return new Worker(workerUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化Web Worker
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
try {
|
||||||
|
// 创建内联Web Worker
|
||||||
|
workerRef.current = createInlineWorker();
|
||||||
|
|
||||||
|
// 监听Worker消息
|
||||||
|
workerRef.current.onmessage = (event) => {
|
||||||
|
const { type, data, error } = event.data;
|
||||||
|
|
||||||
|
if (type === 'groupLogsResult') {
|
||||||
|
setGroupedLogs(data);
|
||||||
|
} else if (type === 'error') {
|
||||||
|
console.error('Worker error:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.worker_error') + ': ' + error, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听Worker错误
|
||||||
|
workerRef.current.onerror = (error) => {
|
||||||
|
console.error('Worker error:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.worker_init_failed'), 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create worker:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.worker_init_failed'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理Worker
|
||||||
|
return () => {
|
||||||
|
if (workerRef.current) {
|
||||||
|
workerRef.current.terminate();
|
||||||
|
workerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [showToast, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoRefresh && open && selectedFile) {
|
||||||
|
refreshInterval.current = setInterval(() => {
|
||||||
|
loadLogs();
|
||||||
|
}, 5000); // Refresh every 5 seconds
|
||||||
|
} else if (refreshInterval.current) {
|
||||||
|
clearInterval(refreshInterval.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (refreshInterval.current) {
|
||||||
|
clearInterval(refreshInterval.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [autoRefresh, open, selectedFile]);
|
||||||
|
|
||||||
|
// Load logs when selected file changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFile && open) {
|
||||||
|
setLogs([]); // Clear existing logs
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
}, [selectedFile, open]);
|
||||||
|
|
||||||
|
// Handle open/close animations
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setIsVisible(true);
|
||||||
|
// Trigger the animation after a small delay to ensure the element is rendered
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setIsAnimating(false);
|
||||||
|
// Wait for the animation to complete before hiding
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const loadLogFiles = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await api.getLogFiles();
|
||||||
|
|
||||||
|
if (response && Array.isArray(response)) {
|
||||||
|
setLogFiles(response);
|
||||||
|
setSelectedFile(null);
|
||||||
|
setLogs([]);
|
||||||
|
} else {
|
||||||
|
setLogFiles([]);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.no_log_files_available'), 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load log files:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.load_files_failed') + ': ' + (error as Error).message, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLogs = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
|
||||||
|
// 始终加载原始日志数据
|
||||||
|
const response = await api.getLogs(selectedFile.path);
|
||||||
|
|
||||||
|
if (response && Array.isArray(response)) {
|
||||||
|
// 现在接口返回的是原始日志字符串数组,直接存储
|
||||||
|
setLogs(response);
|
||||||
|
|
||||||
|
// 如果启用了分组,使用Web Worker进行聚合(需要转换为LogEntry格式供Worker使用)
|
||||||
|
if (groupByReqId && workerRef.current) {
|
||||||
|
// const workerLogs: LogEntry[] = response.map((logLine, index) => ({
|
||||||
|
// timestamp: new Date().toISOString(),
|
||||||
|
// level: 'info',
|
||||||
|
// message: logLine,
|
||||||
|
// source: undefined,
|
||||||
|
// reqId: undefined
|
||||||
|
// }));
|
||||||
|
|
||||||
|
workerRef.current.postMessage({
|
||||||
|
type: 'groupLogsByReqId',
|
||||||
|
data: { logs: response }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setGroupedLogs(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLogs([]);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.no_logs_available'), 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.load_failed') + ': ' + (error as Error).message, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLogs = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.clearLogs(selectedFile.path);
|
||||||
|
setLogs([]);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.logs_cleared'), 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear logs:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.clear_failed') + ': ' + (error as Error).message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFile = (file: LogFile) => {
|
||||||
|
setSelectedFile(file);
|
||||||
|
setAutoRefresh(false); // Reset auto refresh when changing files
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const toggleGroupByReqId = () => {
|
||||||
|
const newValue = !groupByReqId;
|
||||||
|
setGroupByReqId(newValue);
|
||||||
|
|
||||||
|
if (newValue && selectedFile && logs.length > 0) {
|
||||||
|
// 启用聚合时,如果已有日志,则使用Worker进行聚合
|
||||||
|
if (workerRef.current) {
|
||||||
|
workerRef.current.postMessage({
|
||||||
|
type: 'groupLogsByReqId',
|
||||||
|
data: { logs }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (!newValue) {
|
||||||
|
// 禁用聚合时,清除聚合结果
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectReqId = (reqId: string) => {
|
||||||
|
setSelectedReqId(reqId);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getDisplayLogs = () => {
|
||||||
|
if (groupByReqId && groupedLogs) {
|
||||||
|
if (selectedReqId && groupedLogs.groups[selectedReqId]) {
|
||||||
|
return groupedLogs.groups[selectedReqId];
|
||||||
|
}
|
||||||
|
// 当在分组模式但没有选中具体请求时,显示原始日志字符串数组
|
||||||
|
return logs.map(logLine => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: 'info',
|
||||||
|
message: logLine,
|
||||||
|
source: undefined,
|
||||||
|
reqId: undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// 当不在分组模式时,显示原始日志字符串数组
|
||||||
|
return logs.map(logLine => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: 'info',
|
||||||
|
message: logLine,
|
||||||
|
source: undefined,
|
||||||
|
reqId: undefined
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadLogs = () => {
|
||||||
|
if (!selectedFile || logs.length === 0) return;
|
||||||
|
|
||||||
|
// 直接下载原始日志字符串,每行一个日志
|
||||||
|
const logText = logs.join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([logText], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${selectedFile.name}-${new Date().toISOString().split('T')[0]}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.logs_downloaded'), 'success');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 面包屑导航项类型
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取面包屑导航项
|
||||||
|
const getBreadcrumbs = (): BreadcrumbItem[] => {
|
||||||
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
id: 'root',
|
||||||
|
label: t('log_viewer.title'),
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setAutoRefresh(false);
|
||||||
|
setLogs([]);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (selectedFile) {
|
||||||
|
breadcrumbs.push({
|
||||||
|
id: 'file',
|
||||||
|
label: selectedFile.name,
|
||||||
|
onClick: () => {
|
||||||
|
if (groupByReqId) {
|
||||||
|
// 如果在分组模式下,点击文件层级应该返回到分组列表
|
||||||
|
setSelectedReqId(null);
|
||||||
|
} else {
|
||||||
|
// 如果不在分组模式下,点击文件层级关闭分组功能
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedReqId) {
|
||||||
|
breadcrumbs.push({
|
||||||
|
id: 'req',
|
||||||
|
label: `${t('log_viewer.request')} ${selectedReqId}`,
|
||||||
|
onClick: () => {
|
||||||
|
// 点击当前层级时不做任何操作
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取返回按钮的处理函数
|
||||||
|
const getBackAction = (): (() => void) | null => {
|
||||||
|
if (selectedReqId) {
|
||||||
|
return () => {
|
||||||
|
setSelectedReqId(null);
|
||||||
|
};
|
||||||
|
} else if (selectedFile) {
|
||||||
|
return () => {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setAutoRefresh(false);
|
||||||
|
setLogs([]);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLogsForEditor = () => {
|
||||||
|
// 如果在分组模式且选中了具体请求,显示该请求的日志
|
||||||
|
if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {
|
||||||
|
const requestLogs = groupedLogs.groups[selectedReqId];
|
||||||
|
console.log(requestLogs)
|
||||||
|
// 提取原始JSON字符串并每行一个
|
||||||
|
return requestLogs.map(log => JSON.stringify(log)).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他情况,直接显示原始日志字符串数组,每行一个
|
||||||
|
return logs.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isVisible && !open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(isVisible || open) && (
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-50 transition-all duration-300 ease-out ${
|
||||||
|
isAnimating && open ? 'bg-black/50 opacity-100' : 'bg-black/0 opacity-0 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-white shadow-2xl transition-all duration-300 ease-out transform ${
|
||||||
|
isAnimating && open ? 'translate-y-0' : 'translate-y-full'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
maxHeight: '100vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getBackAction() && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={getBackAction()!}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.back')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
<nav className="flex items-center space-x-1 text-sm">
|
||||||
|
{getBreadcrumbs().map((breadcrumb, index) => (
|
||||||
|
<React.Fragment key={breadcrumb.id}>
|
||||||
|
{index > 0 && (
|
||||||
|
<span className="text-gray-400 mx-1">/</span>
|
||||||
|
)}
|
||||||
|
{index === getBreadcrumbs().length - 1 ? (
|
||||||
|
<span className="text-gray-900 font-medium">
|
||||||
|
{breadcrumb.label}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={breadcrumb.onClick}
|
||||||
|
className="text-blue-600 hover:text-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
{breadcrumb.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{selectedFile && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleGroupByReqId}
|
||||||
|
className={groupByReqId ? 'bg-blue-100 text-blue-700' : ''}
|
||||||
|
>
|
||||||
|
<Layers className="h-4 w-4 mr-2" />
|
||||||
|
{groupByReqId ? t('log_viewer.grouped_on') : t('log_viewer.group_by_req_id')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||||
|
className={autoRefresh ? 'bg-blue-100 text-blue-700' : ''}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 mr-2 ${autoRefresh ? 'animate-spin' : ''}`} />
|
||||||
|
{autoRefresh ? t('log_viewer.auto_refresh_on') : t('log_viewer.auto_refresh_off')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={downloadLogs}
|
||||||
|
disabled={logs.length === 0}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.download')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearLogs}
|
||||||
|
disabled={logs.length === 0}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.clear')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.close')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 bg-gray-50">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
) : selectedFile ? (
|
||||||
|
<>
|
||||||
|
{groupByReqId && groupedLogs && !selectedReqId ? (
|
||||||
|
// 显示日志组列表
|
||||||
|
<div className="flex flex-col h-full p-6">
|
||||||
|
<div className="mb-4 flex-shrink-0">
|
||||||
|
<h3 className="text-lg font-medium mb-2">{t('log_viewer.request_groups')}</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{t('log_viewer.total_requests')}: {groupedLogs.summary.totalRequests} |
|
||||||
|
{t('log_viewer.total_logs')}: {groupedLogs.summary.totalLogs}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto space-y-3">
|
||||||
|
{groupedLogs.summary.requests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.reqId}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
onClick={() => selectReqId(request.reqId)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<File className="h-5 w-5 text-blue-600" />
|
||||||
|
<span className="font-medium text-sm">{request.reqId}</span>
|
||||||
|
{request.model && (
|
||||||
|
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
|
||||||
|
{request.model}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
|
{request.logCount} {t('log_viewer.logs')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
|
<div>{t('log_viewer.first_log')}: {formatDate(request.firstLog)}</div>
|
||||||
|
<div>{t('log_viewer.last_log')}: {formatDate(request.lastLog)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 显示日志内容
|
||||||
|
<Editor
|
||||||
|
height="100%"
|
||||||
|
defaultLanguage="json"
|
||||||
|
value={formatLogsForEditor()}
|
||||||
|
theme="vs"
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: true },
|
||||||
|
fontSize: 14,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
wordWrap: 'on',
|
||||||
|
readOnly: true,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
folding: true,
|
||||||
|
renderWhitespace: 'all',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">{t('log_viewer.select_file')}</h3>
|
||||||
|
{logFiles.length === 0 ? (
|
||||||
|
<div className="text-gray-500 text-center py-8">
|
||||||
|
<File className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p>{t('log_viewer.no_log_files_available')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{logFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
onClick={() => selectFile(file)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<File className="h-5 w-5 text-blue-600" />
|
||||||
|
<span className="font-medium text-sm">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
|
<div>{formatFileSize(file.size)}</div>
|
||||||
|
<div>{formatDate(file.lastModified)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,7 +30,8 @@ export function Router() {
|
|||||||
think: "",
|
think: "",
|
||||||
longContext: "",
|
longContext: "",
|
||||||
longContextThreshold: 60000,
|
longContextThreshold: 60000,
|
||||||
webSearch: ""
|
webSearch: "",
|
||||||
|
image: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRouterChange = (field: string, value: string | number) => {
|
const handleRouterChange = (field: string, value: string | number) => {
|
||||||
@@ -40,6 +41,10 @@ export function Router() {
|
|||||||
setConfig({ ...config, Router: newRouter });
|
setConfig({ ...config, Router: newRouter });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleForceUseImageAgentChange = (value: boolean) => {
|
||||||
|
setConfig({ ...config, forceUseImageAgent: value });
|
||||||
|
};
|
||||||
|
|
||||||
// Handle case where config.Providers might be null or undefined
|
// Handle case where config.Providers might be null or undefined
|
||||||
const providers = Array.isArray(config.Providers) ? config.Providers : [];
|
const providers = Array.isArray(config.Providers) ? config.Providers : [];
|
||||||
|
|
||||||
@@ -133,6 +138,33 @@ export function Router() {
|
|||||||
emptyPlaceholder={t("router.noModelFound")}
|
emptyPlaceholder={t("router.noModelFound")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label>{t("router.image")} (beta)</Label>
|
||||||
|
<Combobox
|
||||||
|
options={modelOptions}
|
||||||
|
value={routerConfig.image || ""}
|
||||||
|
onChange={(value) => handleRouterChange("image", value)}
|
||||||
|
placeholder={t("router.selectModel")}
|
||||||
|
searchPlaceholder={t("router.searchModel")}
|
||||||
|
emptyPlaceholder={t("router.noModelFound")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-48">
|
||||||
|
<Label htmlFor="forceUseImageAgent">{t("router.forceUseImageAgent")}</Label>
|
||||||
|
<select
|
||||||
|
id="forceUseImageAgent"
|
||||||
|
value={config.forceUseImageAgent ? "true" : "false"}
|
||||||
|
onChange={(e) => handleForceUseImageAgentChange(e.target.value === "true")}
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="false">{t("common.no")}</option>
|
||||||
|
<option value="true">{t("common.yes")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -212,6 +212,21 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
|
|||||||
className="transition-all-ease focus:scale-[1.01]"
|
className="transition-all-ease focus:scale-[1.01]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="custom-router-path"
|
||||||
|
className="transition-all-ease hover:scale-[1.01] cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("toplevel.custom_router_path")}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="custom-router-path"
|
||||||
|
value={config.CUSTOM_ROUTER_PATH || ""}
|
||||||
|
onChange={(e) => setConfig({ ...config, CUSTOM_ROUTER_PATH: e.target.value })}
|
||||||
|
placeholder={t("toplevel.custom_router_path_placeholder")}
|
||||||
|
className="transition-all-ease focus:scale-[1.01]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter className="p-4 pt-0">
|
<DialogFooter className="p-4 pt-0">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -4,15 +4,72 @@ import { cn } from "@/lib/utils"
|
|||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
|
const isNumeric = type === "number";
|
||||||
|
const [tempValue, setTempValue] = React.useState(props.value?.toString() || '');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (props.value !== undefined) {
|
||||||
|
setTempValue(props.value.toString());
|
||||||
|
}
|
||||||
|
}, [props.value]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
|
||||||
|
if (isNumeric) {
|
||||||
|
// Only allow empty string or numbers for numeric input
|
||||||
|
if (newValue === '' || /^\d+$/.test(newValue)) {
|
||||||
|
setTempValue(newValue);
|
||||||
|
// Only call onChange if the value is not empty
|
||||||
|
if (props.onChange && newValue !== '') {
|
||||||
|
props.onChange(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setTempValue(newValue);
|
||||||
|
if (props.onChange) {
|
||||||
|
props.onChange(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (isNumeric && tempValue === '') {
|
||||||
|
const defaultValue = props.placeholder || "1";
|
||||||
|
setTempValue(defaultValue);
|
||||||
|
|
||||||
|
// Create a synthetic event for the corrected value
|
||||||
|
if (props.onChange) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
...e,
|
||||||
|
target: { ...e.target, value: defaultValue }
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
|
||||||
|
props.onChange(syntheticEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.onBlur) {
|
||||||
|
props.onBlur(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// For numeric inputs, use text type and manage value internally
|
||||||
|
const inputType = isNumeric ? "text" : type;
|
||||||
|
const inputValue = isNumeric ? tempValue : props.value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
{...props}
|
||||||
|
type={inputType}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
import type { Config, Provider, Transformer } from '@/types';
|
import type { Config, Provider, Transformer } from '@/types';
|
||||||
|
|
||||||
|
// 日志聚合响应类型
|
||||||
|
interface GroupedLogsResponse {
|
||||||
|
grouped: boolean;
|
||||||
|
groups: { [reqId: string]: Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }> };
|
||||||
|
summary: {
|
||||||
|
totalRequests: number;
|
||||||
|
totalLogs: number;
|
||||||
|
requests: Array<{
|
||||||
|
reqId: string;
|
||||||
|
logCount: number;
|
||||||
|
firstLog: string;
|
||||||
|
lastLog: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// API Client Class for handling requests with baseUrl and apikey authentication
|
// API Client Class for handling requests with baseUrl and apikey authentication
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
@@ -204,6 +220,21 @@ class ApiClient {
|
|||||||
async performUpdate(): Promise<{ success: boolean; message: string }> {
|
async performUpdate(): Promise<{ success: boolean; message: string }> {
|
||||||
return this.post<{ success: boolean; message: string }>('/api/update/perform', {});
|
return this.post<{ success: boolean; message: string }>('/api/update/perform', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get log files list
|
||||||
|
async getLogFiles(): Promise<Array<{ name: string; path: string; size: number; lastModified: string }>> {
|
||||||
|
return this.get<Array<{ name: string; path: string; size: number; lastModified: string }>>('/logs/files');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs from specific file
|
||||||
|
async getLogs(filePath: string): Promise<string[]> {
|
||||||
|
return this.get<string[]>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear logs from specific file
|
||||||
|
async clearLogs(filePath: string): Promise<void> {
|
||||||
|
return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a default instance of the API client
|
// Create a default instance of the API client
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code Router",
|
"title": "Claude Code Router",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -42,7 +46,9 @@
|
|||||||
"port": "Port",
|
"port": "Port",
|
||||||
"apikey": "API Key",
|
"apikey": "API Key",
|
||||||
"timeout": "API Timeout (ms)",
|
"timeout": "API Timeout (ms)",
|
||||||
"proxy_url": "Proxy URL"
|
"proxy_url": "Proxy URL",
|
||||||
|
"custom_router_path": "Custom Router Script Path",
|
||||||
|
"custom_router_path_placeholder": "Enter absolute path to custom router script file"
|
||||||
},
|
},
|
||||||
"transformers": {
|
"transformers": {
|
||||||
"title": "Custom Transformers",
|
"title": "Custom Transformers",
|
||||||
@@ -105,6 +111,8 @@
|
|||||||
"longContext": "Long Context",
|
"longContext": "Long Context",
|
||||||
"longContextThreshold": "Context Threshold",
|
"longContextThreshold": "Context Threshold",
|
||||||
"webSearch": "Web Search",
|
"webSearch": "Web Search",
|
||||||
|
"image": "Image",
|
||||||
|
"forceUseImageAgent": "Force Use Image Agent",
|
||||||
"selectModel": "Select a model...",
|
"selectModel": "Select a model...",
|
||||||
"searchModel": "Search model...",
|
"searchModel": "Search model...",
|
||||||
"noModelFound": "No model found."
|
"noModelFound": "No model found."
|
||||||
@@ -185,5 +193,36 @@
|
|||||||
"template_download_success": "Template downloaded successfully",
|
"template_download_success": "Template downloaded successfully",
|
||||||
"template_download_success_desc": "Configuration template has been downloaded to your device",
|
"template_download_success_desc": "Configuration template has been downloaded to your device",
|
||||||
"template_download_failed": "Failed to download template"
|
"template_download_failed": "Failed to download template"
|
||||||
|
},
|
||||||
|
"log_viewer": {
|
||||||
|
"title": "Log Viewer",
|
||||||
|
"close": "Close",
|
||||||
|
"download": "Download",
|
||||||
|
"clear": "Clear",
|
||||||
|
"auto_refresh_on": "Auto Refresh On",
|
||||||
|
"auto_refresh_off": "Auto Refresh Off",
|
||||||
|
"load_failed": "Failed to load logs",
|
||||||
|
"no_logs_available": "No logs available",
|
||||||
|
"logs_cleared": "Logs cleared successfully",
|
||||||
|
"clear_failed": "Failed to clear logs",
|
||||||
|
"logs_downloaded": "Logs downloaded successfully",
|
||||||
|
"back_to_files": "Back to Files",
|
||||||
|
"select_file": "Select a log file to view",
|
||||||
|
"no_log_files_available": "No log files available",
|
||||||
|
"load_files_failed": "Failed to load log files",
|
||||||
|
"group_by_req_id": "Group by Request ID",
|
||||||
|
"grouped_on": "Grouped",
|
||||||
|
"request_groups": "Request Groups",
|
||||||
|
"total_requests": "Total Requests",
|
||||||
|
"total_logs": "Total Logs",
|
||||||
|
"request": "Request",
|
||||||
|
"logs": "logs",
|
||||||
|
"first_log": "First Log",
|
||||||
|
"last_log": "Last Log",
|
||||||
|
"back_to_all_logs": "Back to All Logs",
|
||||||
|
"worker_error": "Worker error",
|
||||||
|
"worker_init_failed": "Failed to initialize worker",
|
||||||
|
"grouping_not_supported": "Log grouping not supported by server",
|
||||||
|
"back": "Back"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"common": {
|
||||||
|
"yes": "是",
|
||||||
|
"no": "否"
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Claude Code Router",
|
"title": "Claude Code Router",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
@@ -42,7 +46,9 @@
|
|||||||
"port": "端口",
|
"port": "端口",
|
||||||
"apikey": "API 密钥",
|
"apikey": "API 密钥",
|
||||||
"timeout": "API 超时时间 (毫秒)",
|
"timeout": "API 超时时间 (毫秒)",
|
||||||
"proxy_url": "代理地址"
|
"proxy_url": "代理地址",
|
||||||
|
"custom_router_path": "自定义路由脚本路径",
|
||||||
|
"custom_router_path_placeholder": "输入自定义路由脚本文件的绝对路径"
|
||||||
},
|
},
|
||||||
"transformers": {
|
"transformers": {
|
||||||
"title": "自定义转换器",
|
"title": "自定义转换器",
|
||||||
@@ -105,6 +111,8 @@
|
|||||||
"longContext": "长上下文",
|
"longContext": "长上下文",
|
||||||
"longContextThreshold": "上下文阈值",
|
"longContextThreshold": "上下文阈值",
|
||||||
"webSearch": "网络搜索",
|
"webSearch": "网络搜索",
|
||||||
|
"image": "图像",
|
||||||
|
"forceUseImageAgent": "强制使用图像代理",
|
||||||
"selectModel": "选择一个模型...",
|
"selectModel": "选择一个模型...",
|
||||||
"searchModel": "搜索模型...",
|
"searchModel": "搜索模型...",
|
||||||
"noModelFound": "未找到模型."
|
"noModelFound": "未找到模型."
|
||||||
@@ -185,5 +193,36 @@
|
|||||||
"template_download_success": "模板下载成功",
|
"template_download_success": "模板下载成功",
|
||||||
"template_download_success_desc": "配置模板已下载到您的设备",
|
"template_download_success_desc": "配置模板已下载到您的设备",
|
||||||
"template_download_failed": "模板下载失败"
|
"template_download_failed": "模板下载失败"
|
||||||
|
},
|
||||||
|
"log_viewer": {
|
||||||
|
"title": "日志查看器",
|
||||||
|
"close": "关闭",
|
||||||
|
"download": "下载",
|
||||||
|
"clear": "清除",
|
||||||
|
"auto_refresh_on": "自动刷新开启",
|
||||||
|
"auto_refresh_off": "自动刷新关闭",
|
||||||
|
"load_failed": "加载日志失败",
|
||||||
|
"no_logs_available": "暂无日志",
|
||||||
|
"logs_cleared": "日志清除成功",
|
||||||
|
"clear_failed": "清除日志失败",
|
||||||
|
"logs_downloaded": "日志下载成功",
|
||||||
|
"back_to_files": "返回文件列表",
|
||||||
|
"select_file": "选择要查看的日志文件",
|
||||||
|
"no_log_files_available": "暂无日志文件",
|
||||||
|
"load_files_failed": "加载日志文件失败",
|
||||||
|
"group_by_req_id": "按请求ID分组",
|
||||||
|
"grouped_on": "已分组",
|
||||||
|
"request_groups": "请求组",
|
||||||
|
"total_requests": "总请求数",
|
||||||
|
"total_logs": "总日志数",
|
||||||
|
"request": "请求",
|
||||||
|
"logs": "条日志",
|
||||||
|
"first_log": "首条日志",
|
||||||
|
"last_log": "末条日志",
|
||||||
|
"back_to_all_logs": "返回所有日志",
|
||||||
|
"worker_error": "Worker错误",
|
||||||
|
"worker_init_failed": "Worker初始化失败",
|
||||||
|
"grouping_not_supported": "服务器不支持日志分组",
|
||||||
|
"back": "返回"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface RouterConfig {
|
|||||||
longContext: string;
|
longContext: string;
|
||||||
longContextThreshold: number;
|
longContextThreshold: number;
|
||||||
webSearch: string;
|
webSearch: string;
|
||||||
|
image: string;
|
||||||
custom?: any;
|
custom?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export interface Config {
|
|||||||
Router: RouterConfig;
|
Router: RouterConfig;
|
||||||
transformers: Transformer[];
|
transformers: Transformer[];
|
||||||
StatusLine?: StatusLineConfig;
|
StatusLine?: StatusLineConfig;
|
||||||
|
forceUseImageAgent?: boolean;
|
||||||
// Top-level settings
|
// Top-level settings
|
||||||
LOG: boolean;
|
LOG: boolean;
|
||||||
LOG_LEVEL: string;
|
LOG_LEVEL: string;
|
||||||
@@ -62,6 +64,7 @@ export interface Config {
|
|||||||
APIKEY: string;
|
APIKEY: string;
|
||||||
API_TIMEOUT_MS: string;
|
API_TIMEOUT_MS: string;
|
||||||
PROXY_URL: string;
|
PROXY_URL: string;
|
||||||
|
CUSTOM_ROUTER_PATH?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccessLevel = 'restricted' | 'full';
|
export type AccessLevel = 'restricted' | 'full';
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user