46 Commits

Author SHA1 Message Date
musi
5cd21c570f Merge pull request #798 from SaseQ/main
Add ccr logo and badges (README.md edit)
2025-09-10 20:39:37 +08:00
SaseQ
c5e97709a5 Add ccr logo and badges (README.md edit) 2025-09-10 14:32:02 +02:00
musistudio
f7adb7b28e release v1.0.49 2025-09-09 22:43:01 +08:00
musistudio
7964fff175 release v1.0.48 2025-09-09 21:47:59 +08:00
musistudio
fe06b57032 fix llms version 2025-09-09 21:21:45 +08:00
musistudio
1b3a8f8803 fix logviewer 2025-09-09 21:06:19 +08:00
musistudio
cec8421dd9 change logviewer 2025-09-06 22:19:40 +08:00
musistudio
1a7e90df39 remove log util 2025-09-06 09:06:18 +08:00
musistudio
e5741ae470 add logger vireer 2025-09-05 21:36:21 +08:00
musistudio
0152af5db9 update sponsors 2025-09-04 22:07:40 +08:00
musistudio
e6b3e2a194 release v1.0.47 2025-09-04 22:04:31 +08:00
musistudio
f7058dcdb5 fix log file path 2025-09-04 22:04:03 +08:00
musistudio
e670302e9e optimize docker deployment 2025-09-03 09:58:09 +08:00
musistudio
5761e165fd release v1.0.46 2025-09-02 21:23:53 +08:00
musistudio
8c4fec4f5f fix stream handler error 2025-09-02 21:23:21 +08:00
musistudio
5d53571fe6 release v1.0.45 2025-09-02 19:55:04 +08:00
musistudio
35fc4505b2 release v1.0.44 2025-09-02 12:16:31 +08:00
musistudio
c7303775ad update document 2025-09-02 12:13:54 +08:00
musistudio
f7981b16cd Merge branch 'dev/agents' 2025-09-01 21:10:18 +08:00
musistudio
b54687c4d5 update sponsors 2025-09-01 21:09:52 +08:00
musi
0be4c3753f Merge pull request #691 from zoyopei/fix/ui-custom-router
fix(ui): add CUSTOM_ROUTER_PATH support in general settings
2025-09-01 20:53:07 +08:00
musi
668e855a2d Merge pull request #699 from geocine/numeric-input
Fix numeric input UX: allow complete deletion with smart defaults
2025-09-01 20:52:27 +08:00
musi
41108cea1d Merge pull request #706 from vitobotta/chutes-gllm
Add link to unofficial GLM 4.5 transformer for Chutes provider
2025-09-01 20:50:56 +08:00
musistudio
19522f496b add agents to support route image 2025-09-01 17:19:43 +08:00
musistudio
3b9e58a823 update readme 2025-08-26 22:14:40 +08:00
musistudio
615fe7629e update readme 2025-08-26 22:05:25 +08:00
Vito Botta
656a5f9a97 Add link to unofficial GLM 4.5 transformer for Chutes provider 2025-08-25 18:44:24 +03:00
Aivan Monceller
d2a0815cb7 enhance Input component to manage numeric values and improve onChange handling 2025-08-25 13:27:58 +08:00
zoyopei
7cc41d83cf fix(ui): add CUSTOM_ROUTER_PATH support in general settings
- Add CUSTOM_ROUTER_PATH field to UI configuration
  - Fix configuration preservation during save operations
2025-08-24 19:52:14 +08:00
musistudio
9a5ea191f8 update sponsors 2025-08-23 16:04:59 +08:00
musistudio
6ab608943e release v1.0.43 2025-08-20 12:43:57 +08:00
musistudio
50c8f6994f release v1.0.42 2025-08-19 22:36:12 +08:00
musistudio
915495553a fix some bugs 2025-08-19 22:33:59 +08:00
musistudio
5ac4e8955d release v1.0.41 2025-08-18 22:29:53 +08:00
musistudio
6b7d0926c4 fix windows error 2025-08-18 22:29:24 +08:00
musistudio
01cd5d03a3 update readme to add statusline 2025-08-18 22:25:12 +08:00
musistudio
0c14a5c053 update sponsors 2025-08-18 22:14:48 +08:00
musistudio
b72b05eb5c release v1.0.40 2025-08-18 22:10:46 +08:00
musistudio
21ab7c61ce feat: override settingsFlag 2025-08-18 22:10:27 +08:00
musi
9f82aa2797 Merge pull request #609 from semidark/fix/claude-path-from-config
Fix CLAUDE_PATH to also load from config
2025-08-18 06:50:15 +08:00
Nico Thomaier
ac0263b226 fix(utils): use || instead of ?? for CLAUDE_PATH
The previous null‑ish coalescing treated empty strings as a valid value, which could result in an invalid path. Switching to logical OR keeps the precedence (config > env) but now falls back to the default `claude`
2025-08-17 17:46:03 +02:00
Nico Thomaier
6a4c1f7591 fix(utils): update codeCommand to improve command handling
- use CLAUDE_PATH in Config file if it is not empty.
2025-08-17 17:30:56 +02:00
musistudio
95b2dadd40 feat: optimize ui 2025-08-17 18:02:09 +08:00
musistudio
d6b11e1b60 feat: statusline support script 2025-08-17 00:25:22 +08:00
musistudio
d2969e4332 feat: update statusline config ui 2025-08-16 19:01:15 +08:00
musistudio
19d0f3b8f5 release v1.0.39 2025-08-16 15:17:06 +08:00
46 changed files with 3366 additions and 772 deletions

View File

@@ -1,36 +0,0 @@
---
name: code-implementation-expert
description: Use this agent when you need to implement code solutions, write functions, create classes, or develop software components. This agent excels at translating requirements into working code across multiple programming languages. Examples: When a user asks 'Please write a function that checks if a number is prime' - use the code-implementation-expert agent to generate the implementation. When a user requests 'Create a React component for a todo list' - use this agent to build the component code.
model: sonnet
color: blue
---
<CCR-SUBAGENT-MODEL>deepseek,deepseek-reasoner</CCR-SUBAGENT-MODEL>
You are an elite software engineering expert with deep knowledge across multiple programming languages, frameworks, and best practices. Your primary role is to translate requirements into high-quality, functional code implementations.
When implementing code:
1. Analyze requirements carefully to understand functionality, constraints, and edge cases
2. Choose appropriate data structures, algorithms, and design patterns
3. Write clean, readable, and maintainable code following language-specific conventions
4. Include proper error handling, input validation, and documentation
5. Optimize for performance and scalability when relevant
6. Consider security implications and best practices
7. Write modular code that's easy to test and extend
You will:
- Implement complete, working solutions unless otherwise specified
- Use appropriate naming conventions for variables, functions, and classes
- Include necessary imports/dependencies
- Add comments for complex logic or non-obvious implementation decisions
- Follow established patterns in the codebase when visible
- Write defensive code that handles edge cases gracefully
- Ensure code compiles/runs without syntax errors
When responding:
- Provide the complete implementation in appropriate code blocks
- Explain key design decisions briefly if not obvious
- Mention any assumptions made about requirements
- Highlight important implementation details
- Note any limitations or areas for improvement
If requirements are unclear, ask specific questions to clarify before implementing. If you encounter domain-specific requirements outside your expertise, acknowledge limitations and suggest alternatives.

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@ node_modules
.env .env
log.txt log.txt
.idea .idea
dist dist
.DS_Store
.vscode

7
Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM node:20-alpine
RUN npm install -g @musistudio/claude-code-router
EXPOSE 3456
CMD ["ccr", "start"]

View File

@@ -1,14 +1,23 @@
# Claude Code Router ![](blog/images/claude-code-router-img.png)
[![](https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat)](README_zh.md)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/rdftVMaUcS)
[![](https://img.shields.io/github/license/musistudio/claude-code-router)](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. Its worth noting that iFlow limits each user to a concurrency of 1, which means youll need to route background requests to other models.
> If youd like a better experience, you can try [iFlow CLI](https://cli.iflow.cn).
![](blog/images/claude-code.png) ![](blog/images/claude-code.png)
![](blog/images/roadmap.svg)
## ✨ 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).
@@ -42,7 +51,7 @@ The `config.json` file has several key sections:
- **`PROXY_URL`** (optional): You can set a proxy for API requests, for example: `"PROXY_URL": "http://127.0.0.1:7890"`. - **`PROXY_URL`** (optional): You can set a proxy for API requests, for example: `"PROXY_URL": "http://127.0.0.1:7890"`.
- **`LOG`** (optional): You can enable logging by setting it to `true`. When set to `false`, no log files will be created. Default is `true`. - **`LOG`** (optional): You can enable logging by setting it to `true`. When set to `false`, no log files will be created. Default is `true`.
- **`LOG_LEVEL`** (optional): Set the logging level. Available options are: `"fatal"`, `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`. Default is `"info"`. - **`LOG_LEVEL`** (optional): Set the logging level. Available options are: `"fatal"`, `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`. Default is `"debug"`.
- **Logging Systems**: The Claude Code Router uses two separate logging systems: - **Logging Systems**: The Claude Code Router uses two separate logging systems:
- **Server-level logs**: HTTP requests, API calls, and server events are logged using pino in the `~/.claude-code-router/logs/` directory with filenames like `ccr-*.log` - **Server-level logs**: HTTP requests, API calls, and server events are logged using pino in the `~/.claude-code-router/logs/` directory with filenames like `ccr-*.log`
- **Application-level logs**: Routing decisions and business logic events are logged in `~/.claude-code-router/claude-code-router.log` - **Application-level logs**: Routing decisions and business logic events are logged in `~/.claude-code-router/claude-code-router.log`
@@ -208,7 +217,7 @@ ccr code
> ccr restart > ccr restart
> ``` > ```
### 4. UI Mode (Beta) ### 4. UI Mode
For a more intuitive experience, you can use the UI mode to manage your configuration: For a more intuitive experience, you can use the UI mode to manage your configuration:
@@ -220,9 +229,6 @@ This will open a web-based interface where you can easily view and edit your `co
![UI](/blog/images/ui.png) ![UI](/blog/images/ui.png)
> **Note**: The UI mode is currently in beta. 100% vibe coding: including project initialization, I just created a folder and a project.md document, and all code was generated by ccr + qwen3-coder + gemini(webSearch).
If you encounter any issues, please submit an issue on GitHub.
#### Providers #### Providers
The `Providers` array is where you define the different model providers you want to use. Each provider object requires: The `Providers` array is where you define the different model providers you want to use. Each provider object requires:
@@ -318,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).
@@ -348,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 CCRs 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`
@@ -403,6 +411,13 @@ For routing within subagents, you must specify a particular provider and model b
Please help me analyze this code snippet for potential optimizations... Please help me analyze this code snippet for potential optimizations...
``` ```
## Status Line (Beta)
To better monitor the status of claude-code-router at runtime, version v1.0.40 includes a built-in statusline tool, which you can enable in the UI.
![statusline-config.png](/blog/images/statusline-config.png)
The effect is as follows:
![statusline](/blog/images/statusline.png)
## 🤖 GitHub Actions ## 🤖 GitHub Actions
Integrate Claude Code Router into your CI/CD pipeline. After setting up [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), modify your `.github/workflows/claude.yaml` to use the router: Integrate Claude Code Router into your CI/CD pipeline. After setting up [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), modify your `.github/workflows/claude.yaml` to use the router:
@@ -491,6 +506,7 @@ A huge thank you to all our sponsors for their generous support!
- [AIHubmix](https://aihubmix.com/) - [AIHubmix](https://aihubmix.com/)
- [BurnCloud](https://ai.burncloud.com)
- @Simon Leischnig - @Simon Leischnig
- [@duanshuaimin](https://github.com/duanshuaimin) - [@duanshuaimin](https://github.com/duanshuaimin)
- [@vrgitadmin](https://github.com/vrgitadmin) - [@vrgitadmin](https://github.com/vrgitadmin)
@@ -535,13 +551,34 @@ 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
- @*林 - @\*林
- @\*咸
- @\*明
- @S\*y
- @f\*o
- @\*智
- @F\*t
- @r\*c
- [@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.)

View File

@@ -1,11 +1,24 @@
# Claude Code Router ![](blog/images/claude-code-router-img.png)
[![](https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat)](README.md)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/rdftVMaUcS)
[![](https://img.shields.io/github/license/musistudio/claude-code-router)](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)。
![](blog/images/claude-code.png) ![](blog/images/claude-code.png)
![](blog/images/roadmap.svg)
## ✨ 功能 ## ✨ 功能
- **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。 - **模型路由**: 根据您的需求将请求路由到不同的模型(例如,后台任务、思考、长上下文)。
@@ -38,7 +51,7 @@ npm install -g @musistudio/claude-code-router
`config.json` 文件有几个关键部分: `config.json` 文件有几个关键部分:
- **`PROXY_URL`** (可选): 您可以为 API 请求设置代理,例如:`"PROXY_URL": "http://127.0.0.1:7890"` - **`PROXY_URL`** (可选): 您可以为 API 请求设置代理,例如:`"PROXY_URL": "http://127.0.0.1:7890"`
- **`LOG`** (可选): 您可以通过将其设置为 `true` 来启用日志记录。当设置为 `false` 时,将不会创建日志文件。默认值为 `true` - **`LOG`** (可选): 您可以通过将其设置为 `true` 来启用日志记录。当设置为 `false` 时,将不会创建日志文件。默认值为 `true`
- **`LOG_LEVEL`** (可选): 设置日志级别。可用选项包括:`"fatal"``"error"``"warn"``"info"``"debug"``"trace"`。默认值为 `"info"` - **`LOG_LEVEL`** (可选): 设置日志级别。可用选项包括:`"fatal"``"error"``"warn"``"info"``"debug"``"trace"`。默认值为 `"debug"`
- **日志系统**: Claude Code Router 使用两个独立的日志系统: - **日志系统**: Claude Code Router 使用两个独立的日志系统:
- **服务器级别日志**: HTTP 请求、API 调用和服务器事件使用 pino 记录在 `~/.claude-code-router/logs/` 目录中,文件名类似于 `ccr-*.log` - **服务器级别日志**: HTTP 请求、API 调用和服务器事件使用 pino 记录在 `~/.claude-code-router/logs/` 目录中,文件名类似于 `ccr-*.log`
- **应用程序级别日志**: 路由决策和业务逻辑事件记录在 `~/.claude-code-router/claude-code-router.log` 文件中 - **应用程序级别日志**: 路由决策和业务逻辑事件记录在 `~/.claude-code-router/claude-code-router.log` 文件中
@@ -182,7 +195,7 @@ ccr code
> ccr restart > ccr restart
> ``` > ```
### 4. UI 模式 (Beta) ### 4. UI 模式
为了获得更直观的体验,您可以使用 UI 模式来管理您的配置: 为了获得更直观的体验,您可以使用 UI 模式来管理您的配置:
@@ -194,8 +207,6 @@ ccr ui
![UI](/blog/images/ui.png) ![UI](/blog/images/ui.png)
> **注意**: UI 模式目前处于测试阶段。这是一个 100% vibe coding的项目包括项目的初始化我只是新建了一个文件夹和一个project.md文档。所有代码均由 ccr + qwen3-coder + gemini(webSearch) 实现。如有问题请提交 issue。
#### Providers #### Providers
`Providers` 数组是您定义要使用的不同模型提供商的地方。每个提供商对象都需要: `Providers` 数组是您定义要使用的不同模型提供商的地方。每个提供商对象都需要:
@@ -320,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`
@@ -375,6 +387,12 @@ module.exports = async function router(req, config) {
请帮我分析这段代码是否存在潜在的优化空间... 请帮我分析这段代码是否存在潜在的优化空间...
``` ```
## Status Line (Beta)
为了在运行时更好的查看claude-code-router的状态claude-code-router在v1.0.40内置了一个statusline工具你可以在UI中启用它
![statusline-config.png](/blog/images/statusline-config.png)
效果如下:
![statusline](/blog/images/statusline.png)
## 🤖 GitHub Actions ## 🤖 GitHub Actions
@@ -461,6 +479,7 @@ jobs:
非常感谢所有赞助商的慷慨支持! 非常感谢所有赞助商的慷慨支持!
- [AIHubmix](https://aihubmix.com/) - [AIHubmix](https://aihubmix.com/)
- [BurnCloud](https://ai.burncloud.com)
- @Simon Leischnig - @Simon Leischnig
- [@duanshuaimin](https://github.com/duanshuaimin) - [@duanshuaimin](https://github.com/duanshuaimin)
- [@vrgitadmin](https://github.com/vrgitadmin) - [@vrgitadmin](https://github.com/vrgitadmin)
@@ -494,6 +513,7 @@ jobs:
- @*琢 - @*琢
- @*成 - @*成
- @Z*o - @Z*o
- @\*琨
- [@congzhangzh](https://github.com/congzhangzh) - [@congzhangzh](https://github.com/congzhangzh)
- @*_ - @*_
- @Z\*m - @Z\*m
@@ -508,10 +528,31 @@ jobs:
- @\*光 - @\*光
- @W\*l - @W\*l
- [@kesku](https://github.com/kesku) - [@kesku](https://github.com/kesku)
- @水\*丫 - [@biguncle](https://github.com/biguncle)
- @二吉吉 - @二吉吉
- @a\*g - @a\*g
- @*林 - @\*林
- @\*咸
- @\*明
- @S\*y
- @f\*o
- @\*智
- @F\*t
- @r\*c
- [@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 用户名进行更新。)

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

67
blog/images/roadmap.svg Normal file
View 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 &amp; 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 &amp; 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
blog/images/statusline.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

@@ -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"]

View File

@@ -1,6 +1,6 @@
{ {
"name": "@musistudio/claude-code-router", "name": "@musistudio/claude-code-router",
"version": "1.0.38", "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.24", "@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"
}, },

420
pnpm-lock.yaml generated
View File

@@ -12,42 +12,45 @@ importers:
specifier: ^8.2.0 specifier: ^8.2.0
version: 8.2.0 version: 8.2.0
'@musistudio/llms': '@musistudio/llms':
specifier: ^1.0.24 specifier: ^1.0.32
version: 1.0.24(ws@8.18.3)(zod@3.25.67) 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.21 version: 1.0.22
uuid: uuid:
specifier: ^11.1.0 specifier: ^11.1.0
version: 11.1.0 version: 11.1.0
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^24.0.15 specifier: ^24.0.15
version: 24.0.15 version: 24.3.0
esbuild: esbuild:
specifier: ^0.25.1 specifier: ^0.25.1
version: 0.25.5 version: 0.25.9
fastify: fastify:
specifier: ^5.4.0 specifier: ^5.4.0
version: 5.4.0 version: 5.5.0
shx: shx:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0 version: 0.4.0
typescript: typescript:
specifier: ^5.8.2 specifier: ^5.8.2
version: 5.8.3 version: 5.9.2
packages: packages:
@@ -55,152 +58,158 @@ packages:
resolution: {integrity: sha512-xyoCtHJnt/qg5GG6IgK+UJEndz8h8ljzt/caKXmq3LfBF81nC/BW6E4x2rOWCZcvsLyVW+e8U5mtIr6UCE/kJw==} resolution: {integrity: sha512-xyoCtHJnt/qg5GG6IgK+UJEndz8h8ljzt/caKXmq3LfBF81nC/BW6E4x2rOWCZcvsLyVW+e8U5mtIr6UCE/kJw==}
hasBin: true hasBin: true
'@esbuild/aix-ppc64@0.25.5': '@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [aix] os: [aix]
'@esbuild/android-arm64@0.25.5': '@esbuild/android-arm64@0.25.9':
resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@esbuild/android-arm@0.25.5': '@esbuild/android-arm@0.25.9':
resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
'@esbuild/android-x64@0.25.5': '@esbuild/android-x64@0.25.9':
resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@esbuild/darwin-arm64@0.25.5': '@esbuild/darwin-arm64@0.25.9':
resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@esbuild/darwin-x64@0.25.5': '@esbuild/darwin-x64@0.25.9':
resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@esbuild/freebsd-arm64@0.25.5': '@esbuild/freebsd-arm64@0.25.9':
resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
'@esbuild/freebsd-x64@0.25.5': '@esbuild/freebsd-x64@0.25.9':
resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@esbuild/linux-arm64@0.25.5': '@esbuild/linux-arm64@0.25.9':
resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@esbuild/linux-arm@0.25.5': '@esbuild/linux-arm@0.25.9':
resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@esbuild/linux-ia32@0.25.5': '@esbuild/linux-ia32@0.25.9':
resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
'@esbuild/linux-loong64@0.25.5': '@esbuild/linux-loong64@0.25.9':
resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
'@esbuild/linux-mips64el@0.25.5': '@esbuild/linux-mips64el@0.25.9':
resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
'@esbuild/linux-ppc64@0.25.5': '@esbuild/linux-ppc64@0.25.9':
resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@esbuild/linux-riscv64@0.25.5': '@esbuild/linux-riscv64@0.25.9':
resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@esbuild/linux-s390x@0.25.5': '@esbuild/linux-s390x@0.25.9':
resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@esbuild/linux-x64@0.25.5': '@esbuild/linux-x64@0.25.9':
resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@esbuild/netbsd-arm64@0.25.5': '@esbuild/netbsd-arm64@0.25.9':
resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [netbsd] os: [netbsd]
'@esbuild/netbsd-x64@0.25.5': '@esbuild/netbsd-x64@0.25.9':
resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
'@esbuild/openbsd-arm64@0.25.5': '@esbuild/openbsd-arm64@0.25.9':
resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [openbsd] os: [openbsd]
'@esbuild/openbsd-x64@0.25.5': '@esbuild/openbsd-x64@0.25.9':
resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [openbsd] os: [openbsd]
'@esbuild/sunos-x64@0.25.5': '@esbuild/openharmony-arm64@0.25.9':
resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.25.9':
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
'@esbuild/win32-arm64@0.25.5': '@esbuild/win32-arm64@0.25.9':
resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@esbuild/win32-ia32@0.25.5': '@esbuild/win32-ia32@0.25.9':
resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@esbuild/win32-x64@0.25.5': '@esbuild/win32-x64@0.25.9':
resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -211,8 +220,8 @@ packages:
'@fastify/ajv-compiler@4.0.2': '@fastify/ajv-compiler@4.0.2':
resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==}
'@fastify/cors@11.0.1': '@fastify/cors@11.1.0':
resolution: {integrity: sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==} resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==}
'@fastify/error@4.2.0': '@fastify/error@4.2.0':
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
@@ -235,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.8.0': '@google/genai@1.16.0':
resolution: {integrity: sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==} 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
@@ -260,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.24': '@musistudio/llms@1.0.32':
resolution: {integrity: sha512-Hz6ZT92/ZM/eR5kTdCBHD6zoEMOvT5u6g/vfCir5Hwvl4QGHk3g30EmX1pZAXJf83kLnB/lSEq/HQimFIXHIhQ==} 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==}
@@ -275,14 +284,14 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@types/node@24.0.15': '@types/node@24.3.0':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==} resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
abstract-logging@2.0.1: abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
agent-base@7.1.3: agent-base@7.1.4:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
ajv-formats@3.0.1: ajv-formats@3.0.1:
@@ -322,8 +331,8 @@ packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
bignumber.js@9.3.0: bignumber.js@9.3.1:
resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -332,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'}
@@ -339,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'}
@@ -395,8 +412,8 @@ packages:
end-of-stream@1.4.5: end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
esbuild@0.25.5: esbuild@0.25.9:
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
@@ -436,8 +453,8 @@ packages:
fastify-plugin@5.0.1: fastify-plugin@5.0.1:
resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==}
fastify@5.4.0: fastify@5.5.0:
resolution: {integrity: sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==} resolution: {integrity: sha512-ZWSWlzj3K/DcULCnCjEiC2zn2FBPdlZsSA/pnPa/dbUfLvxkD/Nqmb0XXMXLrWkeM4uQPUvjdJpwtXmTfriXqw==}
fastq@1.19.1: fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@@ -454,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'}
@@ -494,8 +515,8 @@ packages:
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
hasBin: true hasBin: true
google-auth-library@10.2.0: google-auth-library@10.3.0:
resolution: {integrity: sha512-gy/0hRx8+Ye0HlUm3GrfpR4lbmJQ6bJ7F44DmN7GtMxxzWSojLzx0Bhv/hj7Wlj7a2On0FcT8jrz8Y1c1nxCyg==} 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:
@@ -518,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'}
@@ -603,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}
@@ -666,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.8.2: openai@5.16.0:
resolution: {integrity: sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==} resolution: {integrity: sha512-hoEH8ZNvg1HXjU9mp88L/ZH8O082Z8r6FHCXGiWAzVRrEv443aI57qhch4snu07yQydj+AUAWLenAiBXhu89Tw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
ws: ^8.18.0 ws: ^8.18.0
@@ -710,14 +739,11 @@ 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==}
pino@9.7.0: pino@9.9.0:
resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} resolution: {integrity: sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ==}
hasBin: true hasBin: true
process-warning@4.0.1: process-warning@4.0.1:
@@ -763,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:
@@ -862,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'}
@@ -869,8 +899,8 @@ packages:
thread-stream@3.1.0: thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
tiktoken@1.0.21: tiktoken@1.0.22:
resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==} resolution: {integrity: sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
@@ -887,16 +917,16 @@ packages:
tr46@0.0.3: tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
typescript@5.8.3: typescript@5.9.2:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.8.0: undici-types@7.10.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
undici@7.11.0: undici@7.15.0:
resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==} resolution: {integrity: sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==}
engines: {node: '>=20.18.1'} engines: {node: '>=20.18.1'}
uuid@11.1.0: uuid@11.1.0:
@@ -949,91 +979,86 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
zod-to-json-schema@3.24.6:
resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}
peerDependencies:
zod: ^3.24.1
zod@3.25.67:
resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==}
snapshots: snapshots:
'@anthropic-ai/sdk@0.54.0': {} '@anthropic-ai/sdk@0.54.0': {}
'@esbuild/aix-ppc64@0.25.5': '@esbuild/aix-ppc64@0.25.9':
optional: true optional: true
'@esbuild/android-arm64@0.25.5': '@esbuild/android-arm64@0.25.9':
optional: true optional: true
'@esbuild/android-arm@0.25.5': '@esbuild/android-arm@0.25.9':
optional: true optional: true
'@esbuild/android-x64@0.25.5': '@esbuild/android-x64@0.25.9':
optional: true optional: true
'@esbuild/darwin-arm64@0.25.5': '@esbuild/darwin-arm64@0.25.9':
optional: true optional: true
'@esbuild/darwin-x64@0.25.5': '@esbuild/darwin-x64@0.25.9':
optional: true optional: true
'@esbuild/freebsd-arm64@0.25.5': '@esbuild/freebsd-arm64@0.25.9':
optional: true optional: true
'@esbuild/freebsd-x64@0.25.5': '@esbuild/freebsd-x64@0.25.9':
optional: true optional: true
'@esbuild/linux-arm64@0.25.5': '@esbuild/linux-arm64@0.25.9':
optional: true optional: true
'@esbuild/linux-arm@0.25.5': '@esbuild/linux-arm@0.25.9':
optional: true optional: true
'@esbuild/linux-ia32@0.25.5': '@esbuild/linux-ia32@0.25.9':
optional: true optional: true
'@esbuild/linux-loong64@0.25.5': '@esbuild/linux-loong64@0.25.9':
optional: true optional: true
'@esbuild/linux-mips64el@0.25.5': '@esbuild/linux-mips64el@0.25.9':
optional: true optional: true
'@esbuild/linux-ppc64@0.25.5': '@esbuild/linux-ppc64@0.25.9':
optional: true optional: true
'@esbuild/linux-riscv64@0.25.5': '@esbuild/linux-riscv64@0.25.9':
optional: true optional: true
'@esbuild/linux-s390x@0.25.5': '@esbuild/linux-s390x@0.25.9':
optional: true optional: true
'@esbuild/linux-x64@0.25.5': '@esbuild/linux-x64@0.25.9':
optional: true optional: true
'@esbuild/netbsd-arm64@0.25.5': '@esbuild/netbsd-arm64@0.25.9':
optional: true optional: true
'@esbuild/netbsd-x64@0.25.5': '@esbuild/netbsd-x64@0.25.9':
optional: true optional: true
'@esbuild/openbsd-arm64@0.25.5': '@esbuild/openbsd-arm64@0.25.9':
optional: true optional: true
'@esbuild/openbsd-x64@0.25.5': '@esbuild/openbsd-x64@0.25.9':
optional: true optional: true
'@esbuild/sunos-x64@0.25.5': '@esbuild/openharmony-arm64@0.25.9':
optional: true optional: true
'@esbuild/win32-arm64@0.25.5': '@esbuild/sunos-x64@0.25.9':
optional: true optional: true
'@esbuild/win32-ia32@0.25.5': '@esbuild/win32-arm64@0.25.9':
optional: true optional: true
'@esbuild/win32-x64@0.25.5': '@esbuild/win32-ia32@0.25.9':
optional: true
'@esbuild/win32-x64@0.25.9':
optional: true optional: true
'@fastify/accept-negotiator@2.0.1': {} '@fastify/accept-negotiator@2.0.1': {}
@@ -1044,7 +1069,7 @@ snapshots:
ajv-formats: 3.0.1(ajv@8.17.1) ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.0.6 fast-uri: 3.0.6
'@fastify/cors@11.0.1': '@fastify/cors@11.1.0':
dependencies: dependencies:
fastify-plugin: 5.0.1 fastify-plugin: 5.0.1
toad-cache: 3.7.0 toad-cache: 3.7.0
@@ -1083,12 +1108,10 @@ snapshots:
fastq: 1.19.1 fastq: 1.19.1
glob: 11.0.3 glob: 11.0.3
'@google/genai@1.8.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
zod: 3.25.67
zod-to-json-schema: 3.24.6(zod@3.25.67)
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- encoding - encoding
@@ -1112,18 +1135,18 @@ snapshots:
'@lukeed/ms@2.0.2': {} '@lukeed/ms@2.0.2': {}
'@musistudio/llms@1.0.24(ws@8.18.3)(zod@3.25.67)': '@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.0.1 '@fastify/cors': 11.1.0
'@google/genai': 1.8.0 '@google/genai': 1.16.0
dotenv: 16.6.1 dotenv: 16.6.1
fastify: 5.4.0 fastify: 5.5.0
google-auth-library: 10.2.0 google-auth-library: 10.3.0
json5: 2.2.3 json5: 2.2.3
jsonrepair: 3.13.0 jsonrepair: 3.13.0
openai: 5.8.2(ws@8.18.3)(zod@3.25.67) openai: 5.16.0(ws@8.18.3)
undici: 7.11.0 undici: 7.15.0
uuid: 11.1.0 uuid: 11.1.0
transitivePeerDependencies: transitivePeerDependencies:
- '@modelcontextprotocol/sdk' - '@modelcontextprotocol/sdk'
@@ -1146,13 +1169,13 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1 fastq: 1.19.1
'@types/node@24.0.15': '@types/node@24.3.0':
dependencies: dependencies:
undici-types: 7.8.0 undici-types: 7.10.0
abstract-logging@2.0.1: {} abstract-logging@2.0.1: {}
agent-base@7.1.3: {} agent-base@7.1.4: {}
ajv-formats@3.0.1(ajv@8.17.1): ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies: optionalDependencies:
@@ -1184,7 +1207,7 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
bignumber.js@9.3.0: {} bignumber.js@9.3.1: {}
braces@3.0.3: braces@3.0.3:
dependencies: dependencies:
@@ -1192,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
@@ -1244,33 +1274,34 @@ snapshots:
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
esbuild@0.25.5: esbuild@0.25.9:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.5 '@esbuild/aix-ppc64': 0.25.9
'@esbuild/android-arm': 0.25.5 '@esbuild/android-arm': 0.25.9
'@esbuild/android-arm64': 0.25.5 '@esbuild/android-arm64': 0.25.9
'@esbuild/android-x64': 0.25.5 '@esbuild/android-x64': 0.25.9
'@esbuild/darwin-arm64': 0.25.5 '@esbuild/darwin-arm64': 0.25.9
'@esbuild/darwin-x64': 0.25.5 '@esbuild/darwin-x64': 0.25.9
'@esbuild/freebsd-arm64': 0.25.5 '@esbuild/freebsd-arm64': 0.25.9
'@esbuild/freebsd-x64': 0.25.5 '@esbuild/freebsd-x64': 0.25.9
'@esbuild/linux-arm': 0.25.5 '@esbuild/linux-arm': 0.25.9
'@esbuild/linux-arm64': 0.25.5 '@esbuild/linux-arm64': 0.25.9
'@esbuild/linux-ia32': 0.25.5 '@esbuild/linux-ia32': 0.25.9
'@esbuild/linux-loong64': 0.25.5 '@esbuild/linux-loong64': 0.25.9
'@esbuild/linux-mips64el': 0.25.5 '@esbuild/linux-mips64el': 0.25.9
'@esbuild/linux-ppc64': 0.25.5 '@esbuild/linux-ppc64': 0.25.9
'@esbuild/linux-riscv64': 0.25.5 '@esbuild/linux-riscv64': 0.25.9
'@esbuild/linux-s390x': 0.25.5 '@esbuild/linux-s390x': 0.25.9
'@esbuild/linux-x64': 0.25.5 '@esbuild/linux-x64': 0.25.9
'@esbuild/netbsd-arm64': 0.25.5 '@esbuild/netbsd-arm64': 0.25.9
'@esbuild/netbsd-x64': 0.25.5 '@esbuild/netbsd-x64': 0.25.9
'@esbuild/openbsd-arm64': 0.25.5 '@esbuild/openbsd-arm64': 0.25.9
'@esbuild/openbsd-x64': 0.25.5 '@esbuild/openbsd-x64': 0.25.9
'@esbuild/sunos-x64': 0.25.5 '@esbuild/openharmony-arm64': 0.25.9
'@esbuild/win32-arm64': 0.25.5 '@esbuild/sunos-x64': 0.25.9
'@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-arm64': 0.25.9
'@esbuild/win32-x64': 0.25.5 '@esbuild/win32-ia32': 0.25.9
'@esbuild/win32-x64': 0.25.9
escape-html@1.0.3: {} escape-html@1.0.3: {}
@@ -1317,7 +1348,7 @@ snapshots:
fastify-plugin@5.0.1: {} fastify-plugin@5.0.1: {}
fastify@5.4.0: fastify@5.5.0:
dependencies: dependencies:
'@fastify/ajv-compiler': 4.0.2 '@fastify/ajv-compiler': 4.0.2
'@fastify/error': 4.2.0 '@fastify/error': 4.2.0
@@ -1328,7 +1359,7 @@ snapshots:
fast-json-stringify: 6.0.1 fast-json-stringify: 6.0.1
find-my-way: 9.3.0 find-my-way: 9.3.0
light-my-request: 6.6.0 light-my-request: 6.6.0
pino: 9.7.0 pino: 9.9.0
process-warning: 5.0.0 process-warning: 5.0.0
rfdc: 1.4.1 rfdc: 1.4.1
secure-json-parse: 4.0.0 secure-json-parse: 4.0.0
@@ -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.0: 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
@@ -1475,7 +1514,7 @@ snapshots:
https-proxy-agent@7.0.6: https-proxy-agent@7.0.6:
dependencies: dependencies:
agent-base: 7.1.3 agent-base: 7.1.4
debug: 4.4.1 debug: 4.4.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -1512,7 +1551,7 @@ snapshots:
json-bigint@1.0.0: json-bigint@1.0.0:
dependencies: dependencies:
bignumber.js: 9.3.0 bignumber.js: 9.3.1
json-schema-ref-resolver@2.0.1: json-schema-ref-resolver@2.0.1:
dependencies: dependencies:
@@ -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,10 +1627,9 @@ snapshots:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
openai@5.8.2(ws@8.18.3)(zod@3.25.67): openai@5.16.0(ws@8.18.3):
optionalDependencies: optionalDependencies:
ws: 8.18.3 ws: 8.18.3
zod: 3.25.67
openurl@1.1.1: {} openurl@1.1.1: {}
@@ -1614,13 +1654,9 @@ 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.7.0: pino@9.9.0:
dependencies: dependencies:
atomic-sleep: 1.0.0 atomic-sleep: 1.0.0
fast-redact: 3.5.0 fast-redact: 3.5.0
@@ -1667,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:
@@ -1749,13 +1785,17 @@ 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:
dependencies: dependencies:
real-require: 0.2.0 real-require: 0.2.0
tiktoken@1.0.21: {} tiktoken@1.0.22: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
@@ -1767,11 +1807,11 @@ snapshots:
tr46@0.0.3: {} tr46@0.0.3: {}
typescript@5.8.3: {} typescript@5.9.2: {}
undici-types@7.8.0: {} undici-types@7.10.0: {}
undici@7.11.0: {} undici@7.15.0: {}
uuid@11.1.0: {} uuid@11.1.0: {}
@@ -1809,9 +1849,3 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
ws@8.18.3: {} ws@8.18.3: {}
zod-to-json-schema@3.24.6(zod@3.25.67):
dependencies:
zod: 3.25.67
zod@3.25.67: {}

207
src/agents/image.agent.ts Normal file
View File

@@ -0,0 +1,207 @@
import {IAgent, ITool} from "./type";
import { createHash } from 'crypto';
import { LRUCache } from 'lru-cache';
interface ImageCacheEntry {
source: any;
timestamp: number;
}
class ImageCache {
private cache: LRUCache<string, ImageCacheEntry>;
constructor(maxSize = 100) {
this.cache = new LRUCache({
max: maxSize,
ttl: 24 * 60 * 60 * 1000,
});
}
storeImage(id: string, source: any): void {
if (this.hasImage(id)) return;
this.cache.set(id, {
source,
timestamp: Date.now(),
});
}
getImage(id: string): any {
const entry = this.cache.get(id);
return entry ? entry.source : null;
}
hasImage(hash: string): boolean {
return this.cache.has(hash);
}
clear(): void {
this.cache.clear();
}
size(): number {
return this.cache.size;
}
}
const imageCache = new ImageCache();
export class ImageAgent implements IAgent {
name = "image";
tools: Map<string, ITool>;
constructor() {
this.tools = new Map<string, ITool>();
this.appendTools()
}
shouldHandle(req: any, config: any): boolean {
if (!config.Router.image || req.body.model === config.Router.image) return false;
const lastMessage = req.body.messages[req.body.messages.length - 1]
if (!config.forceUseImageAgent && lastMessage.role === 'user' && Array.isArray(lastMessage.content) &&lastMessage.content.find((item: any) => item.type === 'image')) {
req.body.model = config.Router.image
return false;
}
return req.body.messages.some((msg: any) => msg.role === 'user' && Array.isArray(msg.content) && msg.content.some((item: any) => item.type === 'image'))
}
appendTools() {
this.tools.set('analyzeImage', {
name: "analyzeImage",
description: "Analyse image or images by ID and extract information such as OCR text, objects, layout, colors, or safety signals.",
input_schema: {
"type": "object",
"properties": {
"imageId": {
"type": "array",
"description": "an array of IDs to analyse",
"items": {
"type": "string"
}
},
"task": {
"type": "string",
"description": "Details of task to perform on the image.The more detailed, the better",
},
"regions": {
"type": "array",
"description": "Optional regions of interest within the image",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Optional label for the region"},
"x": {"type": "number", "description": "X coordinate"},
"y": {"type": "number", "description": "Y coordinate"},
"w": {"type": "number", "description": "Width of the region"},
"h": {"type": "number", "description": "Height of the region"},
"units": {"type": "string", "enum": ["px", "pct"], "description": "Units for coordinates and size"}
},
"required": ["x", "y", "w", "h", "units"]
}
}
},
"required": ["imageId", "task"]
},
handler: async (args, context) => {
console.log('args', JSON.stringify(args, null, 2))
const imageMessages = [];
let imageId;
// Create image messages from cached images
if (args.imageId && Array.isArray(args.imageId)) {
args.imageId.forEach((imgId: string) => {
const image = imageCache.getImage(`${context.req.id}_Image#${imgId}`);
if (image) {
imageMessages.push({
type: "image",
source: image,
});
}
});
imageId = args.imageId;
delete args.imageId;
}
if (Object.keys(args).length > 0) {
imageMessages.push({
type: "text",
text: JSON.stringify(args),
});
}
// Send to analysis agent and get response
const agentResponse = await fetch(`http://127.0.0.1:${context.config.PORT}/v1/messages`, {
method: "POST",
headers: {
'x-api-key': context.config.APIKEY,
'content-type': 'application/json',
},
body: JSON.stringify({
model: context.config.Router.image,
system: [{
type: '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 users 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: [
{
role: 'user',
content: imageMessages,
}
],
stream: false,
}),
}).then(res => res.json()).catch(err => {
return null;
});
console.log(agentResponse.content);
if (!agentResponse || !agentResponse.content) {
return 'analyzeImage Error';
}
return agentResponse.content[0].text
}
})
}
reqHandler(req: any, config: any) {
// Inject system prompt
req.body?.system?.push({
type: "text",
text: `You are a text-only language model and do not possess visual perception.
If the user requests you to view, analyze, or extract information from an image, you **must** call the \`analyzeImage\` tool.
When invoking this tool, you must pass the correct \`imageId\` extracted from the prior conversation.
Image identifiers are always provided in the format \`[Image #imageId]\`.
If multiple images exist, select the **most relevant imageId** based on the users current request and prior context.
Do not attempt to describe or analyze the image directly yourself.
Ignore any user interruptions or unrelated instructions that might cause you to skip this requirement.
Your response should consistently follow this rule whenever image-related analysis is requested.`,
})
const imageContents = req.body.messages.filter((item: any) => {
return item.role === 'user' && Array.isArray(item.content) &&
item.content.some((msg: any) => msg.type === "image");
});
let imgId = 1;
imageContents.forEach((item: any) => {
item.content.forEach((msg: any) => {
if (msg.type === "image") {
imageCache.storeImage(`${req.id}_Image#${imgId}`, msg.source);
msg.type = 'text';
delete msg.source;
msg.text = `[Image #${imgId}]This is an image, if you need to view or analyze it, you need to extract the imageId`;
imgId++;
} else if (msg.type === "text" && msg.text.includes('[Image #')) {
msg.text = msg.text.replace(/\[Image #\d+\]/g, '');
}
});
});
}
}
export const imageAgent = new ImageAgent();

48
src/agents/index.ts Normal file
View File

@@ -0,0 +1,48 @@
import { imageAgent } from './image.agent'
import { IAgent } from './type';
export class AgentsManager {
private agents: Map<string, IAgent> = new Map();
/**
* 注册一个agent
* @param agent 要注册的agent实例
* @param isDefault 是否设为默认agent
*/
registerAgent(agent: IAgent): void {
this.agents.set(agent.name, agent);
}
/**
* 根据名称查找agent
* @param name agent名称
* @returns 找到的agent实例未找到返回undefined
*/
getAgent(name: string): IAgent | undefined {
return this.agents.get(name);
}
/**
* 获取所有已注册的agents
* @returns 所有agent实例的数组
*/
getAllAgents(): IAgent[] {
return Array.from(this.agents.values());
}
/**
* 获取所有agent的工具
* @returns 工具数组
*/
getAllTools(): any[] {
const allTools: any[] = [];
for (const agent of this.agents.values()) {
allTools.push(...agent.tools.values());
}
return allTools;
}
}
const agentsManager = new AgentsManager()
agentsManager.registerAgent(imageAgent)
export default agentsManager

19
src/agents/type.ts Normal file
View File

@@ -0,0 +1,19 @@
export interface ITool {
name: string;
description: string;
input_schema: any;
handler: (args: any, context: any) => Promise<string>;
}
export interface IAgent {
name: string;
tools: Map<string, ITool>;
shouldHandle: (req: any, config: any) => boolean;
reqHandler: (req: any, config: any) => void;
resHandler?: (payload: any, config: any) => void;
}

View File

@@ -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"], {

View File

@@ -12,9 +12,18 @@ 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 {SSEParserTransform} from "./utils/SSEParser.transform";
import {SSESerializerTransform} from "./utils/SSESerializer.transform";
import {rewriteStream} from "./utils/rewriteStream";
import JSON5 from "json5";
import { IAgent } from "./agents/type";
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();
@@ -42,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;
} }
@@ -52,11 +62,9 @@ async function run(options: RunOptions = {}) {
// Clean up old log files, keeping only the 10 most recent ones // Clean up old log files, keeping only the 10 most recent ones
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;
if (config.HOST && !config.APIKEY) { if (config.HOST && !config.APIKEY) {
HOST = "127.0.0.1"; HOST = "127.0.0.1";
@@ -80,7 +88,6 @@ async function run(options: RunOptions = {}) {
cleanupPidFile(); cleanupPidFile();
process.exit(0); process.exit(0);
}); });
console.log(HOST);
// Use port from environment variable if set (for background process) // Use port from environment variable if set (for background process)
const servicePort = process.env.SERVICE_PORT const servicePort = process.env.SERVICE_PORT
@@ -88,15 +95,32 @@ async function run(options: RunOptions = {}) {
: port; : port;
// Configure logger based on config settings // Configure logger based on config settings
const loggerConfig = config.LOG !== false ? { const pad = num => (num > 9 ? "" : "0") + num;
level: config.LOG_LEVEL || "info", const generator = (time, index) => {
stream: createWriteStream({ if (!time) {
path: HOME_DIR, time = new Date()
filename: config.LOGNAME || `./logs/ccr-${+new Date()}.log`, }
maxFiles: 3,
interval: "1d", var month = time.getFullYear() + "" + pad(time.getMonth() + 1);
}), var day = pad(time.getDate());
} : false; 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 =
config.LOG !== false
? {
level: config.LOG_LEVEL || "debug",
stream: createStream(generator, {
path: HOME_DIR,
maxFiles: 3,
interval: "1d",
compress: false,
maxSize: "50M"
}),
}
: false;
const server = createServer({ const server = createServer({
jsonPath: CONFIG_FILE, jsonPath: CONFIG_FILE,
@@ -113,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) => {
@@ -126,9 +159,224 @@ async function run(options: RunOptions = {}) {
}); });
server.addHook("preHandler", async (req, reply) => { server.addHook("preHandler", async (req, reply) => {
if (req.url.startsWith("/v1/messages")) { if (req.url.startsWith("/v1/messages")) {
router(req, reply, config); const useAgents = []
for (const agent of agentsManager.getAllAgents()) {
if (agent.shouldHandle(req, config)) {
// 设置agent标识
useAgents.push(agent.name)
// change request body
agent.reqHandler(req, config);
// append agent tools
if (agent.tools.size) {
if (!req.body?.tools?.length) {
req.body.tools = []
}
req.body.tools.unshift(...Array.from(agent.tools.values()).map(item => {
return {
name: item.name,
description: item.description,
input_schema: item.input_schema
}
}))
}
}
}
if (useAgents.length) {
req.agents = useAgents;
}
await router(req, reply, {
config,
event
});
} }
}); });
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 (payload instanceof ReadableStream) {
if (req.agents) {
const abortController = new AbortController();
const eventStream = payload.pipeThrough(new SSEParserTransform())
let currentAgent: undefined | IAgent;
let currentToolIndex = -1
let currentToolName = ''
let currentToolArgs = ''
let currentToolId = ''
const toolMessages: any[] = []
const assistantMessages: any[] = []
// 存储Anthropic格式的消息体区分文本和工具类型
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 (agent) {
currentAgent = agentsManager.getAgent(agent)
currentToolIndex = data.data.index
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;
}
// 工具调用完成处理agent调用
if (currentToolIndex > -1 && data.data.index === currentToolIndex && data.data.type === 'content_block_stop') {
try {
const args = JSON5.parse(currentToolArgs);
assistantMessages.push({
type: "tool_use",
id: currentToolId,
name: currentToolName,
input: args
})
const toolResult = await currentAgent?.tools.get(currentToolName)?.handler(args, {
req,
config
});
toolMessages.push({
"tool_use_id": currentToolId,
"type": "tool_result",
"content": toolResult
})
currentAgent = undefined
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;
}
const stream = response.body!.pipeThrough(new SSEParserTransform())
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;
}
}
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;
}
}).pipeThrough(new SSESerializerTransform()))
}
const [originalStream, clonedStream] = payload.tee();
const read = async (stream: ReadableStream) => {
const reader = stream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Process the value if needed
const dataStr = new TextDecoder().decode(value);
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 {}
}
} catch (readError: any) {
if (readError.name === 'AbortError' || readError.code === 'ERR_STREAM_PREMATURE_CLOSE') {
console.error('Background read stream closed prematurely');
} else {
console.error('Error in background stream reading:', readError);
}
} finally {
reader.releaseLock();
}
}
read(clonedStream);
return done(null, originalStream)
}
sessionUsageCache.put(req.sessionId, payload.usage);
if (typeof payload ==='object') {
if (payload.error) {
return done(payload.error, null)
} else {
return done(payload, null)
}
}
}
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();
} }

View File

@@ -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;
}; };

View File

@@ -0,0 +1,73 @@
export class SSEParserTransform extends TransformStream<string, any> {
private buffer = '';
private currentEvent: Record<string, any> = {};
constructor() {
super({
transform: (chunk: string, controller) => {
const decoder = new TextDecoder();
const text = decoder.decode(chunk);
this.buffer += text;
const lines = this.buffer.split('\n');
// 保留最后一行(可能不完整)
this.buffer = lines.pop() || '';
for (const line of lines) {
const event = this.processLine(line);
if (event) {
controller.enqueue(event);
}
}
},
flush: (controller) => {
// 处理缓冲区中剩余的内容
if (this.buffer.trim()) {
const events: any[] = [];
this.processLine(this.buffer.trim(), events);
events.forEach(event => controller.enqueue(event));
}
// 推送最后一个事件(如果有)
if (Object.keys(this.currentEvent).length > 0) {
controller.enqueue(this.currentEvent);
}
}
});
}
private processLine(line: string, events?: any[]): any | null {
if (!line.trim()) {
if (Object.keys(this.currentEvent).length > 0) {
const event = { ...this.currentEvent };
this.currentEvent = {};
if (events) {
events.push(event);
return null;
}
return event;
}
return null;
}
if (line.startsWith('event:')) {
this.currentEvent.event = line.slice(6).trim();
} else if (line.startsWith('data:')) {
const data = line.slice(5).trim();
if (data === '[DONE]') {
this.currentEvent.data = { type: 'done' };
} else {
try {
this.currentEvent.data = JSON.parse(data);
} catch (e) {
this.currentEvent.data = { raw: data, error: 'JSON parse failed' };
}
}
} else if (line.startsWith('id:')) {
this.currentEvent.id = line.slice(3).trim();
} else if (line.startsWith('retry:')) {
this.currentEvent.retry = parseInt(line.slice(6).trim());
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
export class SSESerializerTransform extends TransformStream<any, string> {
constructor() {
super({
transform: (event, controller) => {
let output = '';
if (event.event) {
output += `event: ${event.event}\n`;
}
if (event.id) {
output += `id: ${event.id}\n`;
}
if (event.retry) {
output += `retry: ${event.retry}\n`;
}
if (event.data) {
if (event.data.type === 'done') {
output += 'data: [DONE]\n';
} else {
output += `data: ${JSON.stringify(event.data)}\n`;
}
}
output += '\n';
controller.enqueue(output);
}
});
}
}

47
src/utils/cache.ts Normal file
View File

@@ -0,0 +1,47 @@
// LRU cache for session usage
export interface Usage {
input_tokens: number;
output_tokens: number;
}
class LRUCache<K, V> {
private capacity: number;
private cache: Map<K, V>;
constructor(capacity: number) {
this.capacity = capacity;
this.cache = new Map<K, V>();
}
get(key: K): V | undefined {
if (!this.cache.has(key)) {
return undefined;
}
const value = this.cache.get(key) as V;
// Move to end to mark as recently used
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key: K, value: V): void {
if (this.cache.has(key)) {
// If key exists, delete it to update its position
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// If cache is full, delete the least recently used item
const leastRecentlyUsedKey = this.cache.keys().next().value;
if (leastRecentlyUsedKey !== undefined) {
this.cache.delete(leastRecentlyUsedKey);
}
}
this.cache.set(key, value);
}
values(): V[] {
return Array.from(this.cache.values());
}
}
export const sessionUsageCache = new LRUCache<string, Usage>(100);

View File

@@ -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;
} }

View File

@@ -5,16 +5,34 @@ import {
decrementReferenceCount, decrementReferenceCount,
incrementReferenceCount, incrementReferenceCount,
} from "./processCheck"; } from "./processCheck";
import {HOME_DIR} from "../constants";
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: "test", ANTHROPIC_AUTH_TOKEN: config?.APIKEY || "test",
ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.PORT || 3456}`, ANTHROPIC_API_KEY: '',
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;
if (config?.StatusLine?.enabled) {
settingsFlag = {
statusLine: {
type: "command",
command: "ccr statusline",
padding: 0,
}
}
args.push(`--settings=${JSON.stringify(settingsFlag)}`);
}
// Non-interactive mode for automation environments // Non-interactive mode for automation environments
if (config.NON_INTERACTIVE_MODE) { if (config.NON_INTERACTIVE_MODE) {
@@ -29,31 +47,37 @@ export async function executeCodeCommand(args: string[] = []) {
env.ANTHROPIC_SMALL_FAST_MODEL = config.ANTHROPIC_SMALL_FAST_MODEL; env.ANTHROPIC_SMALL_FAST_MODEL = config.ANTHROPIC_SMALL_FAST_MODEL;
} }
if (config?.APIKEY) { // if (config?.APIKEY) {
env.ANTHROPIC_API_KEY = config.APIKEY; // env.ANTHROPIC_API_KEY = config.APIKEY;
delete env.ANTHROPIC_AUTH_TOKEN; // delete env.ANTHROPIC_AUTH_TOKEN;
} // }
// Increment reference count when command starts // Increment reference count when command starts
incrementReferenceCount(); incrementReferenceCount();
// Execute claude command // Execute claude command
const claudePath = process.env.CLAUDE_PATH || "claude"; const claudePath = config?.CLAUDE_PATH || process.env.CLAUDE_PATH || "claude";
// Properly join arguments to preserve spaces in quotes // Properly join arguments to preserve spaces in quotes
// Wrap each argument in double quotes to preserve single and double quotes inside arguments // Wrap each argument in double quotes to preserve single and double quotes inside arguments
const joinedArgs = args.length > 0 ? args.map(arg => `"${arg.replace(/\"/g, '\\"')}"`).join(" ") : ""; const joinedArgs =
args.length > 0
? args.map((arg) => `"${arg.replace(/\"/g, '\\"')}"`).join(" ")
: "";
// 🔥 CONFIG-DRIVEN: stdio configuration based on environment // 🔥 CONFIG-DRIVEN: stdio configuration based on environment
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
const claudeProcess = spawn(
const claudeProcess = spawn(claudePath + (joinedArgs ? ` ${joinedArgs}` : ""), [], { claudePath + (joinedArgs ? ` ${joinedArgs}` : ""),
env, [],
stdio: stdioConfig, {
shell: true, env,
}); stdio: stdioConfig,
shell: true,
}
);
// Close stdin for non-interactive mode // Close stdin for non-interactive mode
if (config.NON_INTERACTIVE_MODE) { if (config.NON_INTERACTIVE_MODE) {

View File

@@ -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) {

View File

@@ -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 || "info";
}
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");
}

View File

@@ -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,

View File

@@ -0,0 +1,31 @@
/**rewriteStream
* 读取源readablestream返回一个新的readablestream由processor对源数据进行处理后将返回的新值推送到新的stream如果没有返回值则不推送
* @param stream
* @param processor
*/
export const rewriteStream = (stream: ReadableStream, processor: (data: any, controller: ReadableStreamController<any>) => Promise<any>): ReadableStream => {
const reader = stream.getReader()
return new ReadableStream({
async start(controller) {
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
controller.close()
break
}
const processed = await processor(value, controller)
if (processed !== undefined) {
controller.enqueue(processed)
}
}
} catch (error) {
controller.error(error)
} finally {
reader.releaseLock()
}
}
})
}

View File

@@ -4,7 +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 { readFile } from 'fs/promises'
const enc = get_encoding("cl100k_base"); const enc = get_encoding("cl100k_base");
@@ -62,28 +63,39 @@ const calculateTokenCount = (
return tokenCount; return tokenCount;
}; };
const getUseModel = async (req: any, tokenCount: number, config: any) => { const getUseModel = async (
req: any,
tokenCount: number,
config: any,
lastUsage?: Usage | undefined
) => {
if (req.body.model.includes(",")) { if (req.body.model.includes(",")) {
const [provider, model] = req.body.model.split(","); const [provider, model] = req.body.model.split(",");
const finalProvider = config.Providers.find( const finalProvider = config.Providers.find(
(p: any) => p.name.toLowerCase() === provider (p: any) => p.name.toLowerCase() === provider
); );
const finalModel = finalProvider?.models?.find( const finalModel = finalProvider?.models?.find(
(m: any) => m.toLowerCase() === model (m: any) => m.toLowerCase() === model
); );
if (finalProvider && finalModel) { if (finalProvider && finalModel) {
return `${finalProvider.name},${finalModel}`; return `${finalProvider.name},${finalModel}`;
} }
return req.body.model; return req.body.model;
} }
// if tokenCount is greater than the configured threshold, use the long context model // if tokenCount is greater than the configured threshold, use the long context model
const longContextThreshold = config.Router.longContextThreshold || 60000; const longContextThreshold = config.Router.longContextThreshold || 60000;
if (tokenCount > longContextThreshold && config.Router.longContext) { const lastUsageThreshold =
log( lastUsage &&
"Using long context model due to token count:", lastUsage.input_tokens > longContextThreshold &&
tokenCount, tokenCount > 20000;
"threshold:", const tokenCountThreshold = tokenCount > longContextThreshold;
longContextThreshold if (
(lastUsageThreshold || tokenCountThreshold) &&
config.Router.longContext
) {
req.log.info(
`Using long context model due to token count: ${tokenCount}, threshold: ${longContextThreshold}`
); );
return config.Router.longContext; return config.Router.longContext;
} }
@@ -107,12 +119,12 @@ const getUseModel = async (req: any, tokenCount: number, config: any) => {
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 (
@@ -125,8 +137,22 @@ const getUseModel = async (req: any, tokenCount: number, config: any) => {
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
if (req.body.metadata?.user_id) {
const parts = req.body.metadata.user_id.split("_session_");
if (parts.length > 1) {
req.sessionId = parts[1];
}
}
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[],
@@ -139,17 +165,19 @@ 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) {
model = await getUseModel(req, tokenCount, config); model = await getUseModel(req, tokenCount, config, lastMessageUsage);
} }
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;

View File

@@ -10,6 +10,7 @@ export interface StatusLineModuleConfig {
text: string; text: string;
color?: string; color?: string;
background?: string; background?: string;
scriptPath?: string; // 用于script类型的模块指定要执行的Node.js脚本文件路径
} }
export interface StatusLineThemeConfig { export interface StatusLineThemeConfig {
@@ -132,11 +133,58 @@ function getColorCode(colorName: string): string {
// 变量替换函数,支持{{var}}格式的变量替换 // 变量替换函数,支持{{var}}格式的变量替换
function replaceVariables(text: string, variables: Record<string, string>): string { function replaceVariables(text: string, variables: Record<string, string>): string {
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => { return text.replace(/\{\{(\w+)\}\}/g, (_match, varName) => {
return variables[varName] || match; return variables[varName] || "";
}); });
} }
// 执行脚本并获取输出
async function executeScript(scriptPath: string, variables: Record<string, string>): Promise<string> {
try {
// 检查文件是否存在
await fs.access(scriptPath);
// 使用require动态加载脚本模块
const scriptModule = require(scriptPath);
// 如果导出的是函数,则调用它并传入变量
if (typeof scriptModule === 'function') {
const result = scriptModule(variables);
// 如果返回的是Promise则等待它完成
if (result instanceof Promise) {
return await result;
}
return result;
}
// 如果导出的是default函数则调用它
if (scriptModule.default && typeof scriptModule.default === 'function') {
const result = scriptModule.default(variables);
// 如果返回的是Promise则等待它完成
if (result instanceof Promise) {
return await result;
}
return result;
}
// 如果导出的是字符串,则直接返回
if (typeof scriptModule === 'string') {
return scriptModule;
}
// 如果导出的是default字符串则返回它
if (scriptModule.default && typeof scriptModule.default === 'string') {
return scriptModule.default;
}
// 默认情况下返回空字符串
return "";
} catch (error) {
console.error(`执行脚本 ${scriptPath} 时出错:`, error);
return "";
}
}
// 默认主题配置 - 使用Nerd Fonts图标和美观配色 // 默认主题配置 - 使用Nerd Fonts图标和美观配色
const DEFAULT_THEME: StatusLineThemeConfig = { const DEFAULT_THEME: StatusLineThemeConfig = {
modules: [ modules: [
@@ -490,9 +538,9 @@ export async function parseStatusLineData(input: StatusLineInput): Promise<strin
// 根据风格渲染状态行 // 根据风格渲染状态行
if (isPowerline) { if (isPowerline) {
return renderPowerlineStyle(theme, variables); return await renderPowerlineStyle(theme, variables);
} else { } else {
return renderDefaultStyle(theme, variables); return await renderDefaultStyle(theme, variables);
} }
} catch (error) { } catch (error) {
// 发生错误时返回空字符串 // 发生错误时返回空字符串
@@ -529,10 +577,10 @@ async function getProjectThemeConfigForStyle(style: string): Promise<StatusLineT
} }
// 渲染默认风格的状态行 // 渲染默认风格的状态行
function renderDefaultStyle( async function renderDefaultStyle(
theme: StatusLineThemeConfig, theme: StatusLineThemeConfig,
variables: Record<string, string> variables: Record<string, string>
): string { ): Promise<string> {
const modules = theme.modules || DEFAULT_THEME.modules; const modules = theme.modules || DEFAULT_THEME.modules;
const parts: string[] = []; const parts: string[] = [];
@@ -542,19 +590,30 @@ function renderDefaultStyle(
const color = module.color ? getColorCode(module.color) : ""; const color = module.color ? getColorCode(module.color) : "";
const background = module.background ? getColorCode(module.background) : ""; const background = module.background ? getColorCode(module.background) : "";
const icon = module.icon || ""; const icon = module.icon || "";
const text = replaceVariables(module.text, variables);
// 如果text为空且不是usage类型则跳过该模块 // 如果是script类型执行脚本获取文本
if (!text && module.type !== "usage") { let text = "";
if (module.type === "script" && module.scriptPath) {
text = await executeScript(module.scriptPath, variables);
} else {
text = replaceVariables(module.text, variables);
}
// 构建显示文本
let displayText = "";
if (icon) {
displayText += `${icon} `;
}
displayText += text;
// 如果displayText为空或者只有图标没有实际文本则跳过该模块
if (!displayText || !text) {
continue; continue;
} }
// 构建模块字符串 // 构建模块字符串
let part = `${background}${color}`; let part = `${background}${color}`;
if (icon) { part += `${displayText}${COLORS.reset}`;
part += `${icon} `;
}
part += `${text}${COLORS.reset}`;
parts.push(part); parts.push(part);
} }
@@ -701,10 +760,10 @@ function segment(text: string, textFg: string, bgColor: string, nextBgColor: str
} }
// 渲染Powerline风格的状态行 // 渲染Powerline风格的状态行
function renderPowerlineStyle( async function renderPowerlineStyle(
theme: StatusLineThemeConfig, theme: StatusLineThemeConfig,
variables: Record<string, string> variables: Record<string, string>
): string { ): Promise<string> {
const modules = theme.modules || POWERLINE_THEME.modules; const modules = theme.modules || POWERLINE_THEME.modules;
const segments: string[] = []; const segments: string[] = [];
@@ -714,11 +773,13 @@ function renderPowerlineStyle(
const color = module.color || "white"; const color = module.color || "white";
const backgroundName = module.background || ""; const backgroundName = module.background || "";
const icon = module.icon || ""; const icon = module.icon || "";
const text = replaceVariables(module.text, variables);
// 如果text为空且不是usage类型则跳过该模块 // 如果是script类型执行脚本获取文本
if (!text && module.type !== "usage") { let text = "";
continue; if (module.type === "script" && module.scriptPath) {
text = await executeScript(module.scriptPath, variables);
} else {
text = replaceVariables(module.text, variables);
} }
// 构建显示文本 // 构建显示文本
@@ -728,6 +789,11 @@ function renderPowerlineStyle(
} }
displayText += text; displayText += text;
// 如果displayText为空或者只有图标没有实际文本则跳过该模块
if (!displayText || !text) {
continue;
}
// 获取下一个模块的背景色(用于分隔符) // 获取下一个模块的背景色(用于分隔符)
let nextBackground: string | null = null; let nextBackground: string | null = null;
if (i < modules.length - 1) { if (i < modules.length - 1) {

11
ui/package-lock.json generated
View File

@@ -23,6 +23,7 @@
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
@@ -4469,6 +4470,16 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dnd": { "node_modules/react-dnd": {
"version": "16.0.1", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",

View File

@@ -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">
@@ -331,7 +336,7 @@ function App() {
</Button> </Button>
</div> </div>
</header> </header>
<main className="flex h-[calc(100vh-4rem)] gap-4 p-4"> <main className="flex h-[calc(100vh-4rem)] gap-4 p-4 overflow-hidden">
<div className="w-3/5"> <div className="w-3/5">
<Providers /> <Providers />
</div> </div>
@@ -339,7 +344,7 @@ function App() {
<div className="h-3/5"> <div className="h-3/5">
<Router /> <Router />
</div> </div>
<div className="flex-1"> <div className="flex-1 overflow-hidden">
<Transformers /> <Transformers />
</div> </div>
</div> </div>
@@ -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">

View File

@@ -69,7 +69,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
// Validate the received data to ensure it has the expected structure // Validate the received data to ensure it has the expected structure
const validConfig = { const validConfig = {
LOG: typeof data.LOG === 'boolean' ? data.LOG : false, LOG: typeof data.LOG === 'boolean' ? data.LOG : false,
LOG_LEVEL: typeof data.LOG_LEVEL === 'string' ? data.LOG_LEVEL : 'info', LOG_LEVEL: typeof data.LOG_LEVEL === 'string' ? data.LOG_LEVEL : 'debug',
CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '', CLAUDE_PATH: typeof data.CLAUDE_PATH === 'string' ? data.CLAUDE_PATH : '',
HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1', HOST: typeof data.HOST === 'string' ? data.HOST : '127.0.0.1',
PORT: typeof data.PORT === 'number' ? data.PORT : 3456, PORT: typeof data.PORT === 'number' ? data.PORT : 3456,
@@ -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);
@@ -115,7 +118,7 @@ export function ConfigProvider({ children }: ConfigProviderProps) {
// Set default empty config when fetch fails // Set default empty config when fetch fails
setConfig({ setConfig({
LOG: false, LOG: false,
LOG_LEVEL: 'info', LOG_LEVEL: 'debug',
CLAUDE_PATH: '', CLAUDE_PATH: '',
HOST: '127.0.0.1', HOST: '127.0.0.1',
PORT: 3456, PORT: 3456,
@@ -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);
} }

View 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>
</>
);
}

View File

@@ -14,7 +14,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { X, Trash2, Plus, Eye, EyeOff } from "lucide-react"; import { X, Trash2, Plus, Eye, EyeOff, Search, XCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Combobox } from "@/components/ui/combobox"; import { Combobox } from "@/components/ui/combobox";
import { ComboInput } from "@/components/ui/combo-input"; import { ComboInput } from "@/components/ui/combo-input";
@@ -38,6 +38,7 @@ export function Providers() {
const [showApiKey, setShowApiKey] = useState<Record<number, boolean>>({}); const [showApiKey, setShowApiKey] = useState<Record<number, boolean>>({});
const [apiKeyError, setApiKeyError] = useState<string | null>(null); const [apiKeyError, setApiKeyError] = useState<string | null>(null);
const [nameError, setNameError] = useState<string | null>(null); const [nameError, setNameError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState<string>("");
const comboInputRef = useRef<HTMLInputElement>(null); const comboInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@@ -487,15 +488,57 @@ export function Providers() {
const editingProvider = editingProviderData || (editingProviderIndex !== null ? validProviders[editingProviderIndex] : null); const editingProvider = editingProviderData || (editingProviderIndex !== null ? validProviders[editingProviderIndex] : null);
// Filter providers based on search term
const filteredProviders = validProviders.filter(provider => {
if (!searchTerm) return true;
const term = searchTerm.toLowerCase();
// Check provider name and URL
if (
(provider.name && provider.name.toLowerCase().includes(term)) ||
(provider.api_base_url && provider.api_base_url.toLowerCase().includes(term))
) {
return true;
}
// Check models
if (provider.models && Array.isArray(provider.models)) {
return provider.models.some(model =>
model && model.toLowerCase().includes(term)
);
}
return false;
});
return ( return (
<Card className="flex h-full flex-col rounded-lg border shadow-sm"> <Card className="flex h-full flex-col rounded-lg border shadow-sm">
<CardHeader className="flex flex-row items-center justify-between border-b p-4"> <CardHeader className="flex flex-col border-b p-4 gap-3">
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({validProviders.length})</span></CardTitle> <div className="flex flex-row items-center justify-between">
<Button onClick={handleAddProvider}>{t("providers.add")}</Button> <CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({filteredProviders.length}/{validProviders.length})</span></CardTitle>
<Button onClick={handleAddProvider}>{t("providers.add")}</Button>
</div>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
<Input
placeholder={t("providers.search")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8"
/>
</div>
{searchTerm && (
<Button
variant="ghost"
size="icon"
onClick={() => setSearchTerm("")}
>
<XCircle className="h-4 w-4" />
</Button>
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="flex-grow overflow-y-auto p-4"> <CardContent className="flex-grow overflow-y-auto p-4">
<ProviderList <ProviderList
providers={validProviders} providers={filteredProviders}
onEdit={handleEditProvider} onEdit={handleEditProvider}
onRemove={setDeletingProviderIndex} onRemove={setDeletingProviderIndex}
/> />

View File

@@ -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>
); );

View File

@@ -58,12 +58,12 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
}; };
return ( return (
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={onOpenChange} >
<DialogContent data-testid="settings-dialog"> <DialogContent data-testid="settings-dialog" className="max-h-[80vh] flex flex-col p-0">
<DialogHeader> <DialogHeader className="p-4 pb-0">
<DialogTitle>{t("toplevel.title")}</DialogTitle> <DialogTitle>{t("toplevel.title")}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 p-4 px-8 overflow-y-auto flex-1">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
id="log" id="log"
@@ -212,8 +212,23 @@ 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> <DialogFooter className="p-4 pt-0">
<Button <Button
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
interface ColorPickerProps { interface ColorPickerProps {
value?: string; value?: string;
@@ -15,42 +14,8 @@ interface ColorPickerProps {
showPreview?: boolean; showPreview?: boolean;
} }
// 预定义的ANSI颜色映射
const ANSI_COLOR_MAP: Record<string, string> = {
"black": "#000000",
"red": "#ff0000",
"green": "#00ff00",
"yellow": "#ffff00",
"blue": "#0000ff",
"magenta": "#ff00ff",
"cyan": "#00ffff",
"white": "#ffffff",
"bright_black": "#808080",
"bright_red": "#ff8080",
"bright_green": "#80ff80",
"bright_yellow": "#ffff80",
"bright_blue": "#8080ff",
"bright_magenta": "#ff80ff",
"bright_cyan": "#80ffff",
"bright_white": "#ffffff"
}
// 背景颜色映射添加bg_前缀
const ANSI_BG_COLOR_MAP: Record<string, string> = Object.keys(ANSI_COLOR_MAP).reduce((acc, key) => {
acc[`bg_${key}`] = ANSI_COLOR_MAP[key]
return acc
}, {} as Record<string, string>)
// 合并所有颜色映射
const ALL_COLOR_MAP = { ...ANSI_COLOR_MAP, ...ANSI_BG_COLOR_MAP }
// 获取颜色值的函数 // 获取颜色值的函数
const getColorValue = (color: string): string => { const getColorValue = (color: string): string => {
// 如果是预定义的ANSI颜色
if (ALL_COLOR_MAP[color]) {
return ALL_COLOR_MAP[color]
}
// 如果是十六进制颜色 // 如果是十六进制颜色
if (color.startsWith("#")) { if (color.startsWith("#")) {
return color return color
@@ -91,15 +56,8 @@ export function ColorPicker({
} }
} }
const handlePresetColorClick = (colorName: string) => {
handleColorChange(colorName)
setOpen(false)
}
const selectedColorValue = getColorValue(value)
// 获取ANSI颜色名称如果适用 const selectedColorValue = getColorValue(value)
const ansiColorName = Object.keys(ALL_COLOR_MAP).find(key => ALL_COLOR_MAP[key] === selectedColorValue) || value
return ( return (
<div className="space-y-2"> <div className="space-y-2">
@@ -120,7 +78,7 @@ export function ColorPicker({
/> />
)} )}
<span className="truncate flex-1"> <span className="truncate flex-1">
{value ? ansiColorName : placeholder} {value || placeholder}
</span> </span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m7 15 5 5 5-5"/> <path d="m7 15 5 5 5-5"/>
@@ -152,7 +110,7 @@ export function ColorPicker({
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate"> <div className="text-sm font-medium truncate">
{value ? ansiColorName : "未选择颜色"} {value || "未选择颜色"}
</div> </div>
{value && value.startsWith("#") && ( {value && value.startsWith("#") && (
<div className="text-xs text-muted-foreground font-mono"> <div className="text-xs text-muted-foreground font-mono">
@@ -184,7 +142,12 @@ export function ColorPicker({
/> />
<Button <Button
size="sm" size="sm"
onClick={() => customColor && handleColorChange(customColor)} onClick={() => {
if (customColor && /^#[0-9A-F]{6}$/i.test(customColor)) {
handleColorChange(customColor)
setOpen(false)
}
}}
disabled={!customColor || !/^#[0-9A-F]{6}$/i.test(customColor)} disabled={!customColor || !/^#[0-9A-F]{6}$/i.test(customColor)}
> >
@@ -194,66 +157,6 @@ export function ColorPicker({
(: #FF0000) (: #FF0000)
</p> </p>
</div> </div>
{/* 预定义颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">ANSI </label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
{/* 背景颜色选项 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="text-sm font-medium"></label>
<span className="text-xs text-muted-foreground"></span>
</div>
<div className="grid grid-cols-8 gap-1">
{Object.entries(ANSI_BG_COLOR_MAP).map(([name, color]) => (
<Button
key={name}
variant={value === name ? "default" : "outline"}
size="sm"
className={cn(
"h-8 w-8 p-0 rounded-full transition-all hover:scale-110",
value === name && "ring-2 ring-offset-2 ring-ring ring-offset-background"
)}
style={{ backgroundColor: value === name ? color : undefined }}
onClick={() => handlePresetColorClick(name)}
title={name}
>
{value === name && (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
)}
</Button>
))}
</div>
</div>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -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}
/> />
) )
} }

View File

@@ -119,4 +119,38 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
/* 美化滚动条 - WebKit浏览器 (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
@apply bg-muted-foreground/30;
border-radius: 4px;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground/50;
}
::-webkit-scrollbar-corner {
@apply bg-transparent;
}
* {
scrollbar-width: thin;
scrollbar-color: oklch(0.556 0 0) oklch(0.97 0 0);
}
.dark * {
scrollbar-color: oklch(0.708 0 0) oklch(0.269 0 0);
}
} }

View File

@@ -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

View File

@@ -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",
@@ -93,7 +99,8 @@
"select_template": "Select a template...", "select_template": "Select a template...",
"api_key_required": "API Key is required", "api_key_required": "API Key is required",
"name_required": "Name is required", "name_required": "Name is required",
"name_duplicate": "A provider with this name already exists" "name_duplicate": "A provider with this name already exists",
"search": "Search providers..."
}, },
"router": { "router": {
@@ -104,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."
@@ -128,13 +137,22 @@
"module_text": "Text", "module_text": "Text",
"module_color": "Color", "module_color": "Color",
"module_background": "Background", "module_background": "Background",
"add_module": "Add Module", "module_text_description": "Enter display text, variables can be used:",
"module_color_description": "Select text color",
"module_background_description": "Select background color (optional)",
"module_script_path": "Script Path",
"module_script_path_description": "Enter the absolute path of the Node.js script file",
"add_module": "Add Module",
"remove_module": "Remove Module", "remove_module": "Remove Module",
"delete_module": "Delete Module",
"preview": "Preview", "preview": "Preview",
"components": "Components",
"properties": "Properties",
"workDir": "Working Directory", "workDir": "Working Directory",
"gitBranch": "Git Branch", "gitBranch": "Git Branch",
"model": "Model", "model": "Model",
"usage": "Usage", "usage": "Usage",
"script": "Script",
"background_none": "None", "background_none": "None",
"color_black": "Black", "color_black": "Black",
"color_red": "Red", "color_red": "Red",
@@ -152,6 +170,16 @@
"color_bright_magenta": "Bright Magenta", "color_bright_magenta": "Bright Magenta",
"color_bright_cyan": "Bright Cyan", "color_bright_cyan": "Bright Cyan",
"color_bright_white": "Bright White", "color_bright_white": "Bright White",
"font_placeholder": "Select Font",
"theme_placeholder": "Select Theme Style",
"icon_placeholder": "Paste icon or search by name...",
"icon_description": "Enter icon character, paste icon, or search icons (optional)",
"text_placeholder": "e.g.: {{workDirName}}",
"script_placeholder": "e.g.: /path/to/your/script.js",
"drag_hint": "Drag components here to configure",
"select_hint": "Select a component to configure",
"no_icons_found": "No icons found",
"no_icons_available": "No icons available",
"import_export": "Import/Export", "import_export": "Import/Export",
"import": "Import Config", "import": "Import Config",
"export": "Export Config", "export": "Export Config",
@@ -165,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"
} }
} }

View File

@@ -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": "自定义转换器",
@@ -93,7 +99,8 @@
"select_template": "选择一个模板...", "select_template": "选择一个模板...",
"api_key_required": "API 密钥为必填项", "api_key_required": "API 密钥为必填项",
"name_required": "名称为必填项", "name_required": "名称为必填项",
"name_duplicate": "已存在同名供应商" "name_duplicate": "已存在同名供应商",
"search": "搜索供应商..."
}, },
"router": { "router": {
@@ -104,6 +111,8 @@
"longContext": "长上下文", "longContext": "长上下文",
"longContextThreshold": "上下文阈值", "longContextThreshold": "上下文阈值",
"webSearch": "网络搜索", "webSearch": "网络搜索",
"image": "图像",
"forceUseImageAgent": "强制使用图像代理",
"selectModel": "选择一个模型...", "selectModel": "选择一个模型...",
"searchModel": "搜索模型...", "searchModel": "搜索模型...",
"noModelFound": "未找到模型." "noModelFound": "未找到模型."
@@ -128,13 +137,22 @@
"module_text": "文本", "module_text": "文本",
"module_color": "颜色", "module_color": "颜色",
"module_background": "背景", "module_background": "背景",
"add_module": "添加模块", "module_text_description": "输入显示文本,可使用变量:",
"module_color_description": "选择文字颜色",
"module_background_description": "选择背景颜色(可选)",
"module_script_path": "脚本路径",
"module_script_path_description": "输入Node.js脚本文件的绝对路径",
"add_module": "添加模块",
"remove_module": "移除模块", "remove_module": "移除模块",
"delete_module": "删除组件",
"preview": "预览", "preview": "预览",
"components": "组件",
"properties": "属性",
"workDir": "工作目录", "workDir": "工作目录",
"gitBranch": "Git分支", "gitBranch": "Git分支",
"model": "模型", "model": "模型",
"usage": "使用情况", "usage": "使用情况",
"script": "脚本",
"background_none": "无", "background_none": "无",
"color_black": "黑色", "color_black": "黑色",
"color_red": "红色", "color_red": "红色",
@@ -152,6 +170,16 @@
"color_bright_magenta": "亮品红", "color_bright_magenta": "亮品红",
"color_bright_cyan": "亮青色", "color_bright_cyan": "亮青色",
"color_bright_white": "亮白色", "color_bright_white": "亮白色",
"font_placeholder": "选择字体",
"theme_placeholder": "选择主题样式",
"icon_placeholder": "粘贴图标或输入名称搜索...",
"icon_description": "输入图标字符、粘贴图标或搜索图标(可选)",
"text_placeholder": "例如: {{workDirName}}",
"script_placeholder": "例如: /path/to/your/script.js",
"drag_hint": "拖拽组件到此处进行配置",
"select_hint": "选择一个组件进行配置",
"no_icons_found": "未找到图标",
"no_icons_available": "暂无可用图标",
"import_export": "导入/导出", "import_export": "导入/导出",
"import": "导入配置", "import": "导入配置",
"export": "导出配置", "export": "导出配置",
@@ -165,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": "返回"
} }
} }

View File

@@ -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;
} }
@@ -33,6 +34,7 @@ export interface StatusLineModuleConfig {
text: string; text: string;
color?: string; color?: string;
background?: string; background?: string;
scriptPath?: string; // 用于script类型的模块指定要执行的Node.js脚本文件路径
} }
export interface StatusLineThemeConfig { export interface StatusLineThemeConfig {
@@ -44,6 +46,7 @@ export interface StatusLineConfig {
currentStyle: string; currentStyle: string;
default: StatusLineThemeConfig; default: StatusLineThemeConfig;
powerline: StatusLineThemeConfig; powerline: StatusLineThemeConfig;
fontFamily?: string;
} }
export interface Config { export interface Config {
@@ -51,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;
@@ -60,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';

View File

@@ -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"}