feat: optimize ui

This commit is contained in:
musistudio
2025-08-17 18:02:09 +08:00
parent d6b11e1b60
commit 95b2dadd40
16 changed files with 704 additions and 286 deletions

View File

@@ -42,7 +42,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"`.
- **`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:
- **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`

View File

@@ -38,7 +38,7 @@ npm install -g @musistudio/claude-code-router
`config.json` 文件有几个关键部分:
- **`PROXY_URL`** (可选): 您可以为 API 请求设置代理,例如:`"PROXY_URL": "http://127.0.0.1:7890"`
- **`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 使用两个独立的日志系统:
- **服务器级别日志**: HTTP 请求、API 调用和服务器事件使用 pino 记录在 `~/.claude-code-router/logs/` 目录中,文件名类似于 `ccr-*.log`
- **应用程序级别日志**: 路由决策和业务逻辑事件记录在 `~/.claude-code-router/claude-code-router.log` 文件中

345
pnpm-lock.yaml generated
View File

@@ -13,7 +13,7 @@ importers:
version: 8.2.0
'@musistudio/llms':
specifier: ^1.0.25
version: 1.0.25(ws@8.18.3)(zod@3.25.67)
version: 1.0.25(ws@8.18.3)
dotenv:
specifier: ^16.4.7
version: 16.6.1
@@ -28,26 +28,26 @@ importers:
version: 0.0.2
tiktoken:
specifier: ^1.0.21
version: 1.0.21
version: 1.0.22
uuid:
specifier: ^11.1.0
version: 11.1.0
devDependencies:
'@types/node':
specifier: ^24.0.15
version: 24.0.15
version: 24.3.0
esbuild:
specifier: ^0.25.1
version: 0.25.5
version: 0.25.9
fastify:
specifier: ^5.4.0
version: 5.4.0
version: 5.5.0
shx:
specifier: ^0.4.0
version: 0.4.0
typescript:
specifier: ^5.8.2
version: 5.8.3
version: 5.9.2
packages:
@@ -55,152 +55,158 @@ packages:
resolution: {integrity: sha512-xyoCtHJnt/qg5GG6IgK+UJEndz8h8ljzt/caKXmq3LfBF81nC/BW6E4x2rOWCZcvsLyVW+e8U5mtIr6UCE/kJw==}
hasBin: true
'@esbuild/aix-ppc64@0.25.5':
resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
'@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.25.5':
resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==}
'@esbuild/android-arm64@0.25.9':
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.25.5':
resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==}
'@esbuild/android-arm@0.25.9':
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.25.5':
resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==}
'@esbuild/android-x64@0.25.9':
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.25.5':
resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==}
'@esbuild/darwin-arm64@0.25.9':
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.25.5':
resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==}
'@esbuild/darwin-x64@0.25.9':
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.25.5':
resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==}
'@esbuild/freebsd-arm64@0.25.9':
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.5':
resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==}
'@esbuild/freebsd-x64@0.25.9':
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.25.5':
resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==}
'@esbuild/linux-arm64@0.25.9':
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.25.5':
resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==}
'@esbuild/linux-arm@0.25.9':
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.25.5':
resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==}
'@esbuild/linux-ia32@0.25.9':
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.25.5':
resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==}
'@esbuild/linux-loong64@0.25.9':
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.25.5':
resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==}
'@esbuild/linux-mips64el@0.25.9':
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.25.5':
resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==}
'@esbuild/linux-ppc64@0.25.9':
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.25.5':
resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==}
'@esbuild/linux-riscv64@0.25.9':
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.25.5':
resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==}
'@esbuild/linux-s390x@0.25.9':
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.25.5':
resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==}
'@esbuild/linux-x64@0.25.9':
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.25.5':
resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==}
'@esbuild/netbsd-arm64@0.25.9':
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.5':
resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==}
'@esbuild/netbsd-x64@0.25.9':
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.25.5':
resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==}
'@esbuild/openbsd-arm64@0.25.9':
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.5':
resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==}
'@esbuild/openbsd-x64@0.25.9':
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.25.5':
resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==}
'@esbuild/openharmony-arm64@0.25.9':
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'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.25.5':
resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==}
'@esbuild/win32-arm64@0.25.9':
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.25.5':
resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==}
'@esbuild/win32-ia32@0.25.9':
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.25.5':
resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==}
'@esbuild/win32-x64@0.25.9':
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -211,8 +217,8 @@ packages:
'@fastify/ajv-compiler@4.0.2':
resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==}
'@fastify/cors@11.0.1':
resolution: {integrity: sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==}
'@fastify/cors@11.1.0':
resolution: {integrity: sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==}
'@fastify/error@4.2.0':
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
@@ -235,8 +241,8 @@ packages:
'@fastify/static@8.2.0':
resolution: {integrity: sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==}
'@google/genai@1.8.0':
resolution: {integrity: sha512-n3KiMFesQCy2R9iSdBIuJ0JWYQ1HZBJJkmt4PPZMGZKvlgHhBAGw1kUMyX+vsAIzprN3lK45DI755lm70wPOOg==}
'@google/genai@1.14.0':
resolution: {integrity: sha512-jirYprAAJU1svjwSDVCzyVq+FrJpJd5CSxR/g2Ga/gZ0ZYZpcWjMS75KJl9y71K1mDN+tcx6s21CzCbB2R840g==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@modelcontextprotocol/sdk': ^1.11.0
@@ -275,14 +281,14 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@types/node@24.0.15':
resolution: {integrity: sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==}
'@types/node@24.3.0':
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
abstract-logging@2.0.1:
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
agent-base@7.1.3:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ajv-formats@3.0.1:
@@ -322,8 +328,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
bignumber.js@9.3.0:
resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==}
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -395,8 +401,8 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
esbuild@0.25.5:
resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
esbuild@0.25.9:
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
engines: {node: '>=18'}
hasBin: true
@@ -436,8 +442,8 @@ packages:
fastify-plugin@5.0.1:
resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==}
fastify@5.4.0:
resolution: {integrity: sha512-I4dVlUe+WNQAhKSyv15w+dwUh2EPiEl4X2lGYMmNSgF83WzTMAPKGdWEv5tPsCQOb+SOZwz8Vlta2vF+OeDgRw==}
fastify@5.5.0:
resolution: {integrity: sha512-ZWSWlzj3K/DcULCnCjEiC2zn2FBPdlZsSA/pnPa/dbUfLvxkD/Nqmb0XXMXLrWkeM4uQPUvjdJpwtXmTfriXqw==}
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@@ -494,8 +500,8 @@ packages:
engines: {node: 20 || >=22}
hasBin: true
google-auth-library@10.2.0:
resolution: {integrity: sha512-gy/0hRx8+Ye0HlUm3GrfpR4lbmJQ6bJ7F44DmN7GtMxxzWSojLzx0Bhv/hj7Wlj7a2On0FcT8jrz8Y1c1nxCyg==}
google-auth-library@10.2.1:
resolution: {integrity: sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A==}
engines: {node: '>=18'}
google-auth-library@9.15.1:
@@ -666,8 +672,8 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
openai@5.8.2:
resolution: {integrity: sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==}
openai@5.12.2:
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==}
hasBin: true
peerDependencies:
ws: ^8.18.0
@@ -716,8 +722,8 @@ packages:
pino-std-serializers@7.0.0:
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
pino@9.7.0:
resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==}
pino@9.9.0:
resolution: {integrity: sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ==}
hasBin: true
process-warning@4.0.1:
@@ -869,8 +875,8 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
tiktoken@1.0.21:
resolution: {integrity: sha512-/kqtlepLMptX0OgbYD9aMYbM7EFrMZCL7EoHM8Psmg2FuhXoo/bH64KqOiZGGwa6oS9TPdSEDKBnV2LuB8+5vQ==}
tiktoken@1.0.22:
resolution: {integrity: sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA==}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
@@ -887,16 +893,16 @@ packages:
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
typescript@5.9.2:
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici-types@7.10.0:
resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==}
undici@7.11.0:
resolution: {integrity: sha512-heTSIac3iLhsmZhUCjyS3JQEkZELateufzZuBaVM5RHXdSBMb1LPMQf5x+FH7qjsZYDP0ttAc3nnVpUB+wYbOg==}
undici@7.13.0:
resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==}
engines: {node: '>=20.18.1'}
uuid@11.1.0:
@@ -949,91 +955,86 @@ packages:
utf-8-validate:
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:
'@anthropic-ai/sdk@0.54.0': {}
'@esbuild/aix-ppc64@0.25.5':
'@esbuild/aix-ppc64@0.25.9':
optional: true
'@esbuild/android-arm64@0.25.5':
'@esbuild/android-arm64@0.25.9':
optional: true
'@esbuild/android-arm@0.25.5':
'@esbuild/android-arm@0.25.9':
optional: true
'@esbuild/android-x64@0.25.5':
'@esbuild/android-x64@0.25.9':
optional: true
'@esbuild/darwin-arm64@0.25.5':
'@esbuild/darwin-arm64@0.25.9':
optional: true
'@esbuild/darwin-x64@0.25.5':
'@esbuild/darwin-x64@0.25.9':
optional: true
'@esbuild/freebsd-arm64@0.25.5':
'@esbuild/freebsd-arm64@0.25.9':
optional: true
'@esbuild/freebsd-x64@0.25.5':
'@esbuild/freebsd-x64@0.25.9':
optional: true
'@esbuild/linux-arm64@0.25.5':
'@esbuild/linux-arm64@0.25.9':
optional: true
'@esbuild/linux-arm@0.25.5':
'@esbuild/linux-arm@0.25.9':
optional: true
'@esbuild/linux-ia32@0.25.5':
'@esbuild/linux-ia32@0.25.9':
optional: true
'@esbuild/linux-loong64@0.25.5':
'@esbuild/linux-loong64@0.25.9':
optional: true
'@esbuild/linux-mips64el@0.25.5':
'@esbuild/linux-mips64el@0.25.9':
optional: true
'@esbuild/linux-ppc64@0.25.5':
'@esbuild/linux-ppc64@0.25.9':
optional: true
'@esbuild/linux-riscv64@0.25.5':
'@esbuild/linux-riscv64@0.25.9':
optional: true
'@esbuild/linux-s390x@0.25.5':
'@esbuild/linux-s390x@0.25.9':
optional: true
'@esbuild/linux-x64@0.25.5':
'@esbuild/linux-x64@0.25.9':
optional: true
'@esbuild/netbsd-arm64@0.25.5':
'@esbuild/netbsd-arm64@0.25.9':
optional: true
'@esbuild/netbsd-x64@0.25.5':
'@esbuild/netbsd-x64@0.25.9':
optional: true
'@esbuild/openbsd-arm64@0.25.5':
'@esbuild/openbsd-arm64@0.25.9':
optional: true
'@esbuild/openbsd-x64@0.25.5':
'@esbuild/openbsd-x64@0.25.9':
optional: true
'@esbuild/sunos-x64@0.25.5':
'@esbuild/openharmony-arm64@0.25.9':
optional: true
'@esbuild/win32-arm64@0.25.5':
'@esbuild/sunos-x64@0.25.9':
optional: true
'@esbuild/win32-ia32@0.25.5':
'@esbuild/win32-arm64@0.25.9':
optional: true
'@esbuild/win32-x64@0.25.5':
'@esbuild/win32-ia32@0.25.9':
optional: true
'@esbuild/win32-x64@0.25.9':
optional: true
'@fastify/accept-negotiator@2.0.1': {}
@@ -1044,7 +1045,7 @@ snapshots:
ajv-formats: 3.0.1(ajv@8.17.1)
fast-uri: 3.0.6
'@fastify/cors@11.0.1':
'@fastify/cors@11.1.0':
dependencies:
fastify-plugin: 5.0.1
toad-cache: 3.7.0
@@ -1083,12 +1084,10 @@ snapshots:
fastq: 1.19.1
glob: 11.0.3
'@google/genai@1.8.0':
'@google/genai@1.14.0':
dependencies:
google-auth-library: 9.15.1
ws: 8.18.3
zod: 3.25.67
zod-to-json-schema: 3.24.6(zod@3.25.67)
transitivePeerDependencies:
- bufferutil
- encoding
@@ -1112,18 +1111,18 @@ snapshots:
'@lukeed/ms@2.0.2': {}
'@musistudio/llms@1.0.25(ws@8.18.3)(zod@3.25.67)':
'@musistudio/llms@1.0.25(ws@8.18.3)':
dependencies:
'@anthropic-ai/sdk': 0.54.0
'@fastify/cors': 11.0.1
'@google/genai': 1.8.0
'@fastify/cors': 11.1.0
'@google/genai': 1.14.0
dotenv: 16.6.1
fastify: 5.4.0
google-auth-library: 10.2.0
fastify: 5.5.0
google-auth-library: 10.2.1
json5: 2.2.3
jsonrepair: 3.13.0
openai: 5.8.2(ws@8.18.3)(zod@3.25.67)
undici: 7.11.0
openai: 5.12.2(ws@8.18.3)
undici: 7.13.0
uuid: 11.1.0
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
@@ -1146,13 +1145,13 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@types/node@24.0.15':
'@types/node@24.3.0':
dependencies:
undici-types: 7.8.0
undici-types: 7.10.0
abstract-logging@2.0.1: {}
agent-base@7.1.3: {}
agent-base@7.1.4: {}
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
@@ -1184,7 +1183,7 @@ snapshots:
base64-js@1.5.1: {}
bignumber.js@9.3.0: {}
bignumber.js@9.3.1: {}
braces@3.0.3:
dependencies:
@@ -1244,33 +1243,34 @@ snapshots:
dependencies:
once: 1.4.0
esbuild@0.25.5:
esbuild@0.25.9:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.5
'@esbuild/android-arm': 0.25.5
'@esbuild/android-arm64': 0.25.5
'@esbuild/android-x64': 0.25.5
'@esbuild/darwin-arm64': 0.25.5
'@esbuild/darwin-x64': 0.25.5
'@esbuild/freebsd-arm64': 0.25.5
'@esbuild/freebsd-x64': 0.25.5
'@esbuild/linux-arm': 0.25.5
'@esbuild/linux-arm64': 0.25.5
'@esbuild/linux-ia32': 0.25.5
'@esbuild/linux-loong64': 0.25.5
'@esbuild/linux-mips64el': 0.25.5
'@esbuild/linux-ppc64': 0.25.5
'@esbuild/linux-riscv64': 0.25.5
'@esbuild/linux-s390x': 0.25.5
'@esbuild/linux-x64': 0.25.5
'@esbuild/netbsd-arm64': 0.25.5
'@esbuild/netbsd-x64': 0.25.5
'@esbuild/openbsd-arm64': 0.25.5
'@esbuild/openbsd-x64': 0.25.5
'@esbuild/sunos-x64': 0.25.5
'@esbuild/win32-arm64': 0.25.5
'@esbuild/win32-ia32': 0.25.5
'@esbuild/win32-x64': 0.25.5
'@esbuild/aix-ppc64': 0.25.9
'@esbuild/android-arm': 0.25.9
'@esbuild/android-arm64': 0.25.9
'@esbuild/android-x64': 0.25.9
'@esbuild/darwin-arm64': 0.25.9
'@esbuild/darwin-x64': 0.25.9
'@esbuild/freebsd-arm64': 0.25.9
'@esbuild/freebsd-x64': 0.25.9
'@esbuild/linux-arm': 0.25.9
'@esbuild/linux-arm64': 0.25.9
'@esbuild/linux-ia32': 0.25.9
'@esbuild/linux-loong64': 0.25.9
'@esbuild/linux-mips64el': 0.25.9
'@esbuild/linux-ppc64': 0.25.9
'@esbuild/linux-riscv64': 0.25.9
'@esbuild/linux-s390x': 0.25.9
'@esbuild/linux-x64': 0.25.9
'@esbuild/netbsd-arm64': 0.25.9
'@esbuild/netbsd-x64': 0.25.9
'@esbuild/openbsd-arm64': 0.25.9
'@esbuild/openbsd-x64': 0.25.9
'@esbuild/openharmony-arm64': 0.25.9
'@esbuild/sunos-x64': 0.25.9
'@esbuild/win32-arm64': 0.25.9
'@esbuild/win32-ia32': 0.25.9
'@esbuild/win32-x64': 0.25.9
escape-html@1.0.3: {}
@@ -1317,7 +1317,7 @@ snapshots:
fastify-plugin@5.0.1: {}
fastify@5.4.0:
fastify@5.5.0:
dependencies:
'@fastify/ajv-compiler': 4.0.2
'@fastify/error': 4.2.0
@@ -1328,7 +1328,7 @@ snapshots:
fast-json-stringify: 6.0.1
find-my-way: 9.3.0
light-my-request: 6.6.0
pino: 9.7.0
pino: 9.9.0
process-warning: 5.0.0
rfdc: 1.4.1
secure-json-parse: 4.0.0
@@ -1418,7 +1418,7 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 2.0.0
google-auth-library@10.2.0:
google-auth-library@10.2.1:
dependencies:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
@@ -1475,7 +1475,7 @@ snapshots:
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.3
agent-base: 7.1.4
debug: 4.4.1
transitivePeerDependencies:
- supports-color
@@ -1512,7 +1512,7 @@ snapshots:
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.0
bignumber.js: 9.3.1
json-schema-ref-resolver@2.0.1:
dependencies:
@@ -1586,10 +1586,9 @@ snapshots:
dependencies:
wrappy: 1.0.2
openai@5.8.2(ws@8.18.3)(zod@3.25.67):
openai@5.12.2(ws@8.18.3):
optionalDependencies:
ws: 8.18.3
zod: 3.25.67
openurl@1.1.1: {}
@@ -1620,7 +1619,7 @@ snapshots:
pino-std-serializers@7.0.0: {}
pino@9.7.0:
pino@9.9.0:
dependencies:
atomic-sleep: 1.0.0
fast-redact: 3.5.0
@@ -1755,7 +1754,7 @@ snapshots:
dependencies:
real-require: 0.2.0
tiktoken@1.0.21: {}
tiktoken@1.0.22: {}
to-regex-range@5.0.1:
dependencies:
@@ -1767,11 +1766,11 @@ snapshots:
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.13.0: {}
uuid@11.1.0: {}
@@ -1809,9 +1808,3 @@ snapshots:
wrappy@1.0.2: {}
ws@8.18.3: {}
zod-to-json-schema@3.24.6(zod@3.25.67):
dependencies:
zod: 3.25.67
zod@3.25.67: {}

View File

@@ -15,6 +15,7 @@ import { CONFIG_FILE } from "./constants";
import createWriteStream from "pino-rotating-file-stream";
import { HOME_DIR } from "./constants";
import { configureLogging } from "./utils/log";
import { sessionUsageCache } from "./utils/cache";
async function initializeClaudeConfig() {
const homeDir = homedir();
@@ -88,7 +89,9 @@ async function run(options: RunOptions = {}) {
: port;
// Configure logger based on config settings
const loggerConfig = config.LOG !== false ? {
const loggerConfig =
config.LOG !== false
? {
level: config.LOG_LEVEL || "debug",
stream: createWriteStream({
path: HOME_DIR,
@@ -96,7 +99,8 @@ async function run(options: RunOptions = {}) {
maxFiles: 3,
interval: "1d",
}),
} : false;
}
: false;
const server = createServer({
jsonPath: CONFIG_FILE,
@@ -129,6 +133,33 @@ async function run(options: RunOptions = {}) {
router(req, reply, config);
}
});
server.addHook("onSend", async (req, reply, payload) => {
if (req.sessionId && req.url.startsWith("/v1/messages")) {
if (payload instanceof ReadableStream) {
const [originalStream, clonedStream] = payload.tee();
const reader1 = clonedStream.getReader();
while (true) {
const { done, value } = await reader1.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 {}
}
return originalStream;
} else {
sessionUsageCache.put(req.sessionId, payload.usage);
}
}
return payload;
});
server.start();
}

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

@@ -11,12 +11,14 @@ export async function executeCodeCommand(args: string[] = []) {
const config = await readConfigFile();
const env: Record<string, string> = {
...process.env,
ANTHROPIC_AUTH_TOKEN: "test",
ANTHROPIC_AUTH_TOKEN: config?.APIKEY || "test",
ANTHROPIC_BASE_URL: `http://127.0.0.1:${config.PORT || 3456}`,
API_TIMEOUT_MS: String(config.API_TIMEOUT_MS ?? 600000), // Default to 10 minutes if not set
};
const settingsFlag: Record<string, any> = {};
const settingsFlag: Record<string, any> = {
env,
};
if (config?.StatusLine?.enabled) {
settingsFlag.statusLine = {
type: "command",
@@ -41,10 +43,10 @@ export async function executeCodeCommand(args: string[] = []) {
env.ANTHROPIC_SMALL_FAST_MODEL = config.ANTHROPIC_SMALL_FAST_MODEL;
}
if (config?.APIKEY) {
env.ANTHROPIC_API_KEY = config.APIKEY;
delete env.ANTHROPIC_AUTH_TOKEN;
}
// if (config?.APIKEY) {
// env.ANTHROPIC_API_KEY = config.APIKEY;
// delete env.ANTHROPIC_AUTH_TOKEN;
// }
// Increment reference count when command starts
incrementReferenceCount();

View File

@@ -16,7 +16,7 @@ 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";
logLevel = config.LOG_LEVEL || "debug";
}
export function log(...args: any[]) {

View File

@@ -5,6 +5,7 @@ import {
} from "@anthropic-ai/sdk/resources/messages";
import { get_encoding } from "tiktoken";
import { log } from "./log";
import { sessionUsageCache, Usage } from "./cache";
const enc = get_encoding("cl100k_base");
@@ -62,7 +63,12 @@ const calculateTokenCount = (
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(",")) {
const [provider, model] = req.body.model.split(",");
const finalProvider = config.Providers.find(
@@ -78,7 +84,15 @@ const getUseModel = async (req: any, tokenCount: number, config: any) => {
}
// if tokenCount is greater than the configured threshold, use the long context model
const longContextThreshold = config.Router.longContextThreshold || 60000;
if (tokenCount > longContextThreshold && config.Router.longContext) {
const lastUsageThreshold =
lastUsage &&
lastUsage.input_tokens > longContextThreshold &&
tokenCount > 20000;
const tokenCountThreshold = tokenCount > longContextThreshold;
if (
(lastUsageThreshold || tokenCountThreshold) &&
config.Router.longContext
) {
log(
"Using long context model due to token count:",
tokenCount,
@@ -128,11 +142,12 @@ const getUseModel = async (req: any, tokenCount: number, config: any) => {
export const router = async (req: any, _res: any, config: any) => {
// Parse sessionId from metadata.user_id
if (req.body.metadata?.user_id) {
const parts = req.body.metadata.user_id.split('_session_');
const parts = req.body.metadata.user_id.split("_session_");
if (parts.length > 1) {
req.sessionId = parts[1];
}
}
const lastMessageUsage = sessionUsageCache.get(req.sessionId);
const { messages, system = [], tools }: MessageCreateParamsBase = req.body;
try {
const tokenCount = calculateTokenCount(
@@ -152,7 +167,7 @@ export const router = async (req: any, _res: any, config: any) => {
}
}
if (!model) {
model = await getUseModel(req, tokenCount, config);
model = await getUseModel(req, tokenCount, config, lastMessageUsage);
}
req.body.model = model;
} catch (error: any) {

View File

@@ -331,7 +331,7 @@ function App() {
</Button>
</div>
</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">
<Providers />
</div>
@@ -339,7 +339,7 @@ function App() {
<div className="h-3/5">
<Router />
</div>
<div className="flex-1">
<div className="flex-1 overflow-hidden">
<Transformers />
</div>
</div>

View File

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

View File

@@ -14,7 +14,7 @@ import {
DialogHeader,
DialogTitle,
} 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 { Combobox } from "@/components/ui/combobox";
import { ComboInput } from "@/components/ui/combo-input";
@@ -38,6 +38,7 @@ export function Providers() {
const [showApiKey, setShowApiKey] = useState<Record<number, boolean>>({});
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
const [nameError, setNameError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState<string>("");
const comboInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -487,15 +488,57 @@ export function Providers() {
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 (
<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">
<CardTitle className="text-lg">{t("providers.title")} <span className="text-sm font-normal text-gray-500">({validProviders.length})</span></CardTitle>
<CardHeader className="flex flex-col border-b p-4 gap-3">
<div className="flex flex-row items-center justify-between">
<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>
<CardContent className="flex-grow overflow-y-auto p-4">
<ProviderList
providers={validProviders}
providers={filteredProviders}
onEdit={handleEditProvider}
onRemove={setDeletingProviderIndex}
/>

View File

@@ -59,11 +59,11 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange} >
<DialogContent data-testid="settings-dialog">
<DialogHeader>
<DialogContent data-testid="settings-dialog" className="max-h-[80vh] flex flex-col p-0">
<DialogHeader className="p-4 pb-0">
<DialogTitle>{t("toplevel.title")}</DialogTitle>
</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">
<Switch
id="log"
@@ -213,7 +213,7 @@ export function SettingsDialog({ isOpen, onOpenChange }: SettingsDialogProps) {
/>
</div>
</div>
<DialogFooter>
<DialogFooter className="p-4 pt-0">
<Button
onClick={() => onOpenChange(false)}
className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]"

View File

@@ -1,5 +1,6 @@
import { useTranslation } from "react-i18next";
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import {
Dialog,
DialogContent,
@@ -96,6 +97,207 @@ const ANSI_COLORS: Record<string, string> = {
bg_bright_purple: "bg-purple-400",
};
// 图标搜索输入组件
interface IconData {
className: string;
unicode: string;
char: string;
}
interface IconSearchInputProps {
value: string;
onChange: (value: string) => void;
fontFamily: string;
t: (key: string) => string;
}
const IconSearchInput = React.memo(({ value, onChange, fontFamily, t }: IconSearchInputProps) => {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(value);
const [icons, setIcons] = useState<IconData[]>([]);
const [filteredIcons, setFilteredIcons] = useState<IconData[]>([]);
const [isLoading, setIsLoading] = useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
// 加载Nerdfonts图标数据
const loadIcons = useCallback(async () => {
if (icons.length > 0) return; // 已经加载过了
setIsLoading(true);
try {
const response = await fetch('https://www.nerdfonts.com/assets/css/combo.css');
const cssText = await response.text();
// 解析CSS中的图标类名和Unicode
const iconRegex = /\.nf-([a-zA-Z0-9_-]+):before\s*\{\s*content:\s*"\\([0-9a-fA-F]+)";?\s*\}/g;
const iconData: IconData[] = [];
let match;
while ((match = iconRegex.exec(cssText)) !== null) {
const className = `nf-${match[1]}`;
const unicode = match[2];
const char = String.fromCharCode(parseInt(unicode, 16));
iconData.push({ className, unicode, char });
}
setIcons(iconData);
setFilteredIcons(iconData.slice(0, 200));
} catch (error) {
console.error('Failed to load icons:', error);
setIcons([]);
setFilteredIcons([]);
} finally {
setIsLoading(false);
}
}, [icons.length]);
// 模糊搜索图标
useEffect(() => {
if (searchTerm.trim() === '') {
setFilteredIcons(icons.slice(0, 100)); // 显示前100个图标
return;
}
const term = searchTerm.toLowerCase();
let filtered = icons;
// 如果输入的是特殊字符(可能是粘贴的图标),则搜索对应图标
if (term.length === 1 || /[\u{2000}-\u{2FFFF}]/u.test(searchTerm)) {
const pastedIcon = icons.find(icon => icon.char === searchTerm);
if (pastedIcon) {
filtered = [pastedIcon];
} else {
// 搜索包含该字符的图标
filtered = icons.filter(icon => icon.char === searchTerm);
}
} else {
// 模糊搜索:类名、简化后的名称匹配
filtered = icons.filter(icon => {
const className = icon.className.toLowerCase();
const simpleClassName = className.replace(/[_-]/g, '');
const simpleTerm = term.replace(/[_-]/g, '');
return (
className.includes(term) ||
simpleClassName.includes(simpleTerm) ||
// 关键词匹配
term.split(' ').every(keyword =>
className.includes(keyword) || simpleClassName.includes(keyword)
)
);
});
}
setFilteredIcons(filtered.slice(0, 120)); // 显示更多结果
}, [searchTerm, icons]);
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setSearchTerm(newValue);
onChange(newValue);
// 始终打开下拉框,让用户搜索或确认粘贴的内容
setIsOpen(true);
if (icons.length === 0) {
loadIcons();
}
};
// 处理粘贴事件
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
const pastedText = e.clipboardData.getData('text');
// 如果是单个字符(可能是图标),直接接受并打开下拉框显示相应图标
if (pastedText && pastedText.length === 1) {
setTimeout(() => {
setIsOpen(true);
}, 10);
}
};
// 选择图标
const handleIconSelect = (iconChar: string) => {
setSearchTerm(iconChar);
onChange(iconChar);
setIsOpen(false);
inputRef.current?.focus();
};
// 处理焦点事件
const handleFocus = () => {
setIsOpen(true);
if (icons.length === 0) {
loadIcons();
}
};
// 处理失去焦点(延迟关闭以便点击图标)
const handleBlur = () => {
setTimeout(() => setIsOpen(false), 200);
};
return (
<div className="relative">
<div className="relative">
<Input
ref={inputRef}
value={searchTerm}
onChange={handleInputChange}
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handlePaste}
placeholder={t("statusline.icon_placeholder")}
style={{ fontFamily: fontFamily + ', monospace' }}
className="text-lg pr-2"
/>
</div>
{isOpen && (
<div className="absolute z-50 mt-1 w-full max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg">
{isLoading ? (
<div className="flex items-center justify-center p-8">
<svg className="animate-spin h-6 w-6 text-primary" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" opacity="0.1"/>
<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
</div>
) : (
<>
<div className="grid grid-cols-5 gap-2 p-2 max-h-72 overflow-y-auto">
{filteredIcons.map((icon) => (
<div
key={icon.className}
className="flex items-center justify-center p-3 text-2xl cursor-pointer hover:bg-secondary rounded transition-colors"
onClick={() => handleIconSelect(icon.char)}
onMouseDown={(e) => e.preventDefault()} // 防止失去焦点
title={`${icon.char} - ${icon.className}`}
style={{ fontFamily: fontFamily + ', monospace' }}
>
{icon.char}
</div>
))}
{filteredIcons.length === 0 && (
<div className="col-span-5 flex flex-col items-center justify-center p-8 text-muted-foreground">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="mb-2">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<div className="text-sm">
{searchTerm ? `${t("statusline.no_icons_found")} "${searchTerm}"` : t("statusline.no_icons_available")}
</div>
</div>
)}
</div>
</>
)}
</div>
)}
</div>
);
});
// 变量替换函数
function replaceVariables(
text: string,
@@ -500,9 +702,57 @@ export function StatusLineConfigDialog({
? currentModules[selectedModuleIndex]
: null;
// 删除选中模块的函数
const deleteSelectedModule = useCallback(() => {
if (selectedModuleIndex === null) return;
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [...((themeConfig as StatusLineThemeConfig).modules || [])]
: [];
if (selectedModuleIndex >= 0 && selectedModuleIndex < modules.length) {
modules.splice(selectedModuleIndex, 1);
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules },
}));
setSelectedModuleIndex(null);
}
}, [selectedModuleIndex, statusLineConfig]);
// 字体样式
const fontStyle = fontFamily ? { fontFamily } : {};
// 键盘事件监听器,支持删除选中的模块
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// 检查是否选中了模块
if (selectedModuleIndex === null) return;
// 检查是否按下了删除键 (Delete 或 Backspace)
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
deleteSelectedModule();
}
};
// 添加事件监听器
document.addEventListener('keydown', handleKeyDown);
// 清理函数
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedModuleIndex, deleteSelectedModule]);
// 当字体或主题变化时强制重新渲染
const fontKey = `${fontFamily}-${statusLineConfig.currentStyle}`;
@@ -558,30 +808,30 @@ export function StatusLineConfigDialog({
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="theme-style" className="text-sm font-medium">
{t("statusline.theme")}
</Label>
<Combobox
options={[
{ label: "默认", value: "default" },
{ label: "Powerline", value: "powerline" },
{ label: t("statusline.theme_default"), value: "default" },
{ label: t("statusline.theme_powerline"), value: "powerline" },
]}
value={statusLineConfig.currentStyle}
onChange={handleThemeChange}
data-testid="theme-selector"
placeholder="选择主题样式"
placeholder={t("statusline.theme_placeholder")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="font-family" className="text-sm font-medium">
{t("statusline.module_icon")}
</Label>
<Combobox
options={NERD_FONTS}
value={fontFamily}
onChange={(value) => setFontFamily(value)}
data-testid="font-family-selector"
placeholder="选择字体"
placeholder={t("statusline.font_placeholder")}
/>
</div>
</div>
@@ -590,9 +840,9 @@ export function StatusLineConfigDialog({
{/* 三栏布局:组件列表 | 预览区域 | 属性配置 */}
<div className="grid grid-cols-5 gap-6 overflow-hidden flex-1">
{/* 左侧:支持的组件 */}
<div className="border rounded-lg p-4 flex flex-col overflow-hidden col-span-1">
<h3 className="text-sm font-medium mb-3"></h3>
<div className="space-y-2 overflow-y-auto flex-1">
<div className="border rounded-lg flex flex-col overflow-hidden col-span-1">
<h3 className="text-sm font-medium p-4 pb-0 mb-3">{t("statusline.components")}</h3>
<div className="space-y-2 overflow-y-auto px-4 pb-4 flex-1">
{MODULE_TYPES_OPTIONS.map((moduleType) => (
<div
key={moduleType.value}
@@ -610,7 +860,7 @@ export function StatusLineConfigDialog({
{/* 中间:预览区域 */}
<div className="border rounded-lg p-4 flex flex-col col-span-3">
<h3 className="text-sm font-medium mb-3"></h3>
<h3 className="text-sm font-medium mb-3">{t("statusline.preview")}</h3>
<div
key={fontKey}
className={`rounded bg-black/90 text-white font-mono text-sm overflow-x-auto flex items-center border border-border p-3 py-5 shadow-inner overflow-hidden ${
@@ -793,7 +1043,7 @@ export function StatusLineConfigDialog({
<path d="M8 12h8" />
</svg>
<span className="text-gray-500 text-sm">
{t("statusline.drag_hint")}
</span>
</div>
)}
@@ -801,45 +1051,31 @@ export function StatusLineConfigDialog({
</div>
{/* 右侧:属性配置 */}
<div className="border rounded-lg p-4 flex flex-col overflow-hidden col-span-1">
<h3 className="text-sm font-medium mb-3"></h3>
<div className="overflow-y-auto flex-1">
<div className="border rounded-lg flex flex-col overflow-hidden col-span-1">
<h3 className="text-sm font-medium p-4 pb-0 mb-3">{t("statusline.properties")}</h3>
<div className="overflow-y-auto px-4 pb-4 flex-1">
{selectedModule && selectedModuleIndex !== null ? (
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("statusline.module_type")}</Label>
<Combobox
options={MODULE_TYPES_OPTIONS}
value={selectedModule.type}
onChange={(value) =>
handleModuleChange(selectedModuleIndex, "type", value)
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="space-y-2">
<Label htmlFor="module-icon">
{t("statusline.module_icon")}
</Label>
<Input
<IconSearchInput
key={fontKey}
id="module-icon"
value={selectedModule.icon || ""}
onChange={(e) =>
onChange={(value) =>
handleModuleChange(
selectedModuleIndex,
"icon",
e.target.value
value
)
}
placeholder="例如: 󰉋"
style={fontStyle}
fontFamily={fontFamily}
t={t}
/>
<p className="text-xs text-muted-foreground">
{t("statusline.icon_description")}
</p>
</div>
@@ -857,10 +1093,10 @@ export function StatusLineConfigDialog({
e.target.value
)
}
placeholder="例如: {{workDirName}}"
placeholder={t("statusline.text_placeholder")}
/>
<div className="text-xs text-muted-foreground">
<p>使:</p>
<p>{t("statusline.module_text_description")}</p>
<div className="flex flex-wrap gap-1 mt-1">
<Badge
variant="secondary"
@@ -909,7 +1145,7 @@ export function StatusLineConfigDialog({
}
/>
<p className="text-xs text-muted-foreground">
{t("statusline.module_color_description")}
</p>
</div>
@@ -926,7 +1162,7 @@ export function StatusLineConfigDialog({
}
/>
<p className="text-xs text-muted-foreground">
{t("statusline.module_background_description")}
</p>
</div>
@@ -934,7 +1170,7 @@ export function StatusLineConfigDialog({
{selectedModule.type === "script" && (
<div className="space-y-2">
<Label htmlFor="module-script-path">
{t("statusline.module_script_path")}
</Label>
<Input
id="module-script-path"
@@ -946,10 +1182,10 @@ export function StatusLineConfigDialog({
e.target.value
)
}
placeholder="例如: /path/to/your/script.js"
placeholder={t("statusline.script_placeholder")}
/>
<p className="text-xs text-muted-foreground">
Node.js脚本文件的绝对路径
{t("statusline.module_script_path_description")}
</p>
</div>
)}
@@ -958,36 +1194,15 @@ export function StatusLineConfigDialog({
<Button
variant="destructive"
size="sm"
onClick={() => {
const currentTheme =
statusLineConfig.currentStyle as keyof StatusLineConfig;
const themeConfig = statusLineConfig[currentTheme];
const modules =
themeConfig &&
typeof themeConfig === "object" &&
"modules" in themeConfig
? [
...((themeConfig as StatusLineThemeConfig)
.modules || []),
]
: [];
modules.splice(selectedModuleIndex, 1);
setStatusLineConfig((prev) => ({
...prev,
[currentTheme]: { modules },
}));
setSelectedModuleIndex(null);
}}
onClick={deleteSelectedModule}
>
{t("statusline.delete_module")}
</Button>
</div>
) : (
<div className="flex items-center justify-center h-full min-h-[200px]">
<p className="text-muted-foreground text-sm">
{t("statusline.select_hint")}
</p>
</div>
)}

View File

@@ -119,4 +119,38 @@
body {
@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

@@ -93,7 +93,8 @@
"select_template": "Select a template...",
"api_key_required": "API Key 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": {
@@ -128,9 +129,17 @@
"module_text": "Text",
"module_color": "Color",
"module_background": "Background",
"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",
"delete_module": "Delete Module",
"preview": "Preview",
"components": "Components",
"properties": "Properties",
"workDir": "Working Directory",
"gitBranch": "Git Branch",
"model": "Model",
@@ -153,6 +162,16 @@
"color_bright_magenta": "Bright Magenta",
"color_bright_cyan": "Bright Cyan",
"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": "Import Config",
"export": "Export Config",

View File

@@ -93,7 +93,8 @@
"select_template": "选择一个模板...",
"api_key_required": "API 密钥为必填项",
"name_required": "名称为必填项",
"name_duplicate": "已存在同名供应商"
"name_duplicate": "已存在同名供应商",
"search": "搜索供应商..."
},
"router": {
@@ -128,9 +129,17 @@
"module_text": "文本",
"module_color": "颜色",
"module_background": "背景",
"module_text_description": "输入显示文本,可使用变量:",
"module_color_description": "选择文字颜色",
"module_background_description": "选择背景颜色(可选)",
"module_script_path": "脚本路径",
"module_script_path_description": "输入Node.js脚本文件的绝对路径",
"add_module": "添加模块",
"remove_module": "移除模块",
"delete_module": "删除组件",
"preview": "预览",
"components": "组件",
"properties": "属性",
"workDir": "工作目录",
"gitBranch": "Git分支",
"model": "模型",
@@ -153,6 +162,16 @@
"color_bright_magenta": "亮品红",
"color_bright_cyan": "亮青色",
"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": "导出配置",