├── .env.example ├── .gitattributes ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── README.md ├── clean_benchmark_results.py ├── config.toml ├── docker-compose.yml ├── frontend ├── .gitignore ├── README.md ├── app │ ├── chat │ │ └── page.tsx │ ├── docs │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── bun.lockb ├── components.json ├── components │ ├── chat.tsx │ ├── settings.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chat-input.tsx │ │ ├── collapsible.tsx │ │ ├── copy-button.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── markdown-editor.tsx │ │ ├── select.tsx │ │ ├── sheet.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── eslint.config.mjs ├── hooks │ └── use-feature-flags.ts ├── lib │ └── utils.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── providers │ └── posthog.tsx ├── public │ ├── asterisk.png │ ├── deepclaude.ico │ ├── deepclaude.png │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── r1-plus-sonnet-benchmarks.png │ ├── vercel.svg │ └── window.svg ├── tailwind.config.ts └── tsconfig.json ├── picture ├── chatbox.png ├── cherrystudio.png ├── mode.png └── setting.png ├── src ├── clients │ ├── anthropic.rs │ ├── deepseek.rs │ └── mod.rs ├── config.rs ├── error.rs ├── handlers.rs ├── main.rs ├── models │ ├── mod.rs │ ├── request.rs │ └── response.rs └── utils.rs ├── test_api.py └── test_claude_api.py /.env.example: -------------------------------------------------------------------------------- 1 | # api密钥,自己取的 2 | API_KEY=xyh110 3 | # deepseek的密钥 4 | DEEPSEEK_API_KEY= 5 | # claude模型的密钥 6 | ANTHROPIC_API_KEY= 7 | # 服务的端口 8 | PORT=1337 9 | # 选择模式,包括full和normal,full是包括r1的结果且进行了专门的优化适合于编程,normal是只包含思考内容,所以full模型下,获取calude结果时间更长 10 | MODE=normal 11 | # API URL配置 12 | # DeeepSeek的密钥 13 | # 如果使用deepseek格式的api就填DEEPSEEK_OPENAI_TYPE_API_URL 14 | DEEPSEEK_OPENAI_TYPE_API_URL=https://ark.cn-beijing.volces.com/api/v3/chat/completions 15 | # Claude的密钥,底下两种2选1填 16 | # 如果使用claude格式的api就填ANTHROPIC_API_URL,比如https://xxxx/v1/messages 17 | ANTHROPIC_API_URL= 18 | # 如果使用openai格式的api就填CLAUDE_OPENAI_TYPE_API_URL,比如https://xxxx/v1/chat/completions 19 | CLAUDE_OPENAI_TYPE_API_URL=https://api.gptsapi.net/v1/chat/completions 20 | # 模型配置 21 | CLAUDE_DEFAULT_MODEL=claude-3-7-sonnet-20250219 22 | #DEEPSEEK_DEFAULT_MODEL=deepseek-r1-250120 23 | DEEPSEEK_DEFAULT_MODEL=deepseek-r1-250120 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.rs linguist-vendored=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea/ 18 | 19 | # Ignore .DS_Store files 20 | .DS_Store -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome Contributors 2 | 3 | We welcome contributions to enhance DeepClaude's capabilities and improve its performance. To report bugs, create a [GitHub issue](https://github.com/getasterisk/deepclaude/issues). 4 | 5 | > Before contributing, read through the existing issues and pull requests to see if someone else is already working on something similar. That way you can avoid duplicating efforts. 6 | 7 | To contribute, please follow these steps: 8 | 9 | 1. Fork the DeepClaude repository on GitHub. 10 | 2. Create a new branch for your feature or bug fix. 11 | 3. Make your changes and ensure that the code passes all tests. 12 | 4. Submit a pull request describing your changes and their benefits. 13 | 14 | ### Pull Request Guidelines 15 | When submitting a pull request, please follow these guidelines: 16 | 17 | 1. **Title**: please include following prefixes: 18 | - `Feature:` for new features 19 | - `Fix:` for bug fixes 20 | - `Docs:` for documentation changes 21 | - `Refactor:` for code refactoring 22 | - `Improve:` for performance improvements 23 | - `Other:` for other changes 24 | 25 | for example: 26 | - `Feature: added new feature to the code` 27 | - `Fix: fixed the bug in the code` 28 | 29 | 2. **Description**: Provide a clear and detailed description of your changes in the pull request. Explain the problem you are solving, the approach you took, and any potential side effects or limitations of your changes. 30 | 3. **Documentation**: Update the relevant documentation to reflect your changes. This includes the README file, code comments, and any other relevant documentation. 31 | 4. **Dependencies**: If your changes require new dependencies, ensure that they are properly documented and added to the `Cargo.toml` or `package.json` files. 32 | 5. if the pull request does not meet the above guidelines, it may be closed without merging. 33 | 34 | 35 | **Note**: Please ensure that you have the latest version of the code before creating a pull request. If you have an existing fork, just sync your fork with the latest version of the DeepClaude repository. 36 | 37 | Please adhere to the coding conventions, maintain clear documentation, and provide thorough testing for your contributions. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deepclaude" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "A high-performance LLM inference API and Chat UI that integrates DeepSeek R1's CoT reasoning traces with Anthropic Claude models." 6 | authors = ["Mufeed VH "] 7 | 8 | [dependencies] 9 | # Web framework 10 | axum = { version = "0.8", features = ["json", "macros"] } 11 | tower = "0.5" 12 | tower-http = { version = "0.6", features = ["trace", "cors"] } 13 | 14 | dotenv = "0.15" 15 | uuid = { version = "1.0", features = ["v4"] } 16 | 17 | # Async runtime 18 | tokio = { version = "1.4", features = ["full"] } 19 | tokio-stream = "0.1" 20 | futures = "0.3" 21 | async-stream = "0.3" 22 | 23 | # Serialization 24 | serde = { version = "1.0", features = ["derive"] } 25 | serde_json = "1.0" 26 | 27 | # HTTP client 28 | reqwest = { version = "0.12", features = ["json", "stream"] } 29 | 30 | # Error handling 31 | anyhow = "1.0" 32 | thiserror = "2.0" 33 | 34 | # Logging 35 | tracing = "0.1" 36 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 37 | 38 | # Configuration 39 | config = { version = "0.15", features = ["toml"] } 40 | 41 | # Time 42 | chrono = { version = "0.4", features = ["serde"] } 43 | 44 | # Utilities 45 | once_cell = "1.20" 46 | 47 | # OpenSSL (vendored) 48 | openssl = { version = "0.10", features = ["vendored"] } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder stage 2 | FROM rust:latest as builder 3 | 4 | WORKDIR /usr/src/deepclaude 5 | COPY . . 6 | 7 | # Install build dependencies 8 | RUN apt-get update && \ 9 | apt-get install -y pkg-config libssl-dev && \ 10 | rm -rf /var/lib/apt/lists/* 11 | 12 | # Build the application 13 | RUN cargo build --release 14 | 15 | # Runtime stage 16 | FROM debian:bookworm-slim 17 | 18 | WORKDIR /usr/local/bin 19 | 20 | # Install runtime dependencies 21 | RUN apt-get update && \ 22 | apt-get install -y libssl3 ca-certificates && \ 23 | rm -rf /var/lib/apt/lists/* 24 | 25 | # Copy the built binary 26 | COPY --from=builder /usr/src/deepclaude/target/release/deepclaude . 27 | COPY --from=builder /usr/src/deepclaude/config.toml . 28 | 29 | # Set the host and port in config 30 | ENV DEEPCLAUDE_HOST=0.0.0.0 31 | ENV DEEPCLAUDE_PORT=1337 32 | 33 | # Expose port 1337 34 | EXPOSE 1337 35 | 36 | # Run the binary 37 | CMD ["./deepclaude"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

DeepClaude_Pro(OpenAI Compatible) 🐬🧠

3 | 4 | 5 |
6 | This project is upgraded from the official Rust version of deepclaude. It supports the return results in OpenAI format and can be used in chatbox and cherrystudio. At the same time, it allows for relatively free replacement of third-party APIs of Claude or DeepSeek to achieve other model combinations such as deepclaude or deepgeminipro. 7 | 8 | With the help of the API, this project can combine the reasoning ability of DeepSeek R1 with the creativity and code generation ability of Claude. As for the effectiveness, you can check the evaluation results of my other project, deepclaude's benchmark. 9 | 10 | In the future, I will further explore diverse model combinations and prompt engineering to optimize this project. The feature of this project is that if there are code modifications related to process or engineering optimization, the benchmark will be tested synchronously to ensure that everyone can use an API with a real effectiveness improvement. 11 | 12 | 该项目是基于deepclaude rust官方版本升级而来,支持了OpenAI格式的返回结果,可以用于chatbox和cherrystudio,同时可以比较自由的替换claude 或者deepseek的第三方api来实现deepclaude或者deepgeminipro等其他模型组合。 13 | 14 | 借助API,该项目可以结合DeepSeek R1的推理能力以及Claude的创造力和代码生成能力。至于效果,可以看我另一个项目的评测结果deepclaude的benchmark。 15 | 16 | 后续我将进一步尝试模型多样化组合和提示词工程去优化这个项目,这个项目特点是如果有流程或者工程优化相关的代码修改,会同步的测试benchmark,确保大家可以用上真实有效果提升的api。 17 |
18 | 19 | [![Rust](https://img.shields.io/badge/rust-v1.75%2B-orange)](https://www.rust-lang.org/) 20 | [![API Status](https://img.shields.io/badge/API-Stable-green)](https://deepclaude.asterisk.so) 21 | 22 |
23 | 24 |
25 | 更新日志: 26 |
27 | 2025-04-12: 更新 1.6版本, 28 |
  • 支持免费使用gemini2.5pro的专属升级模式
  • 29 |
  • 支持升级pro+账户来使用gemini2.5pro和deepseekv3的组合模型
  • 30 |
    31 |
    32 | 2025-04-05: 更新 1.5版本, 33 |
  • 正式版本发布,支持线上使用
  • 34 |
  • 支持用户注册
  • 35 |
  • 支持注册用户免费使用额度
  • 36 |
  • 支持pro账户升级
  • 37 |
  • 支持查看更新记录
  • 38 |
    39 |
    40 | 2025-03-20: 更新 1.3.1版本,前端密钥支持隐藏显示,后端修复claude的openai格式返回错误问题 41 |
    42 |
    43 | 2025-03-16: 更新 1.3版本,完整模式大更新,参照aider架构师编辑师模式,提升完整模式效果,benchmark的效果测试已完成 44 |
    45 |
    46 | 2025-03-15: 更新 1.2版本,后端大版本更新,新增完整模式,前端界面支持配置完整或者普通模式,benchmark的效果测试已完成 47 |
    48 |
    49 | 2025-03-14: 更新 1.1版本,支持前端界面配置环境变量,前端直接支持对话 50 |
    51 |
    52 | 2025-03-13: 更新 1.0.2版本,支持在.env文件中配置api路径和模型id 53 |
    54 |
    55 | 2025-03-11: 更新 1.0.1版本,修复cherrystudio输出问题 56 |
    57 |
    58 | 2025-03-09: 更新 1.0 版本,支持chatbox和cherrystudio 59 |
    60 |
    61 |
    62 | 介绍视频: 63 |
    64 | 1.6版本deepclaude pro新增gemini2.5pro专属优化模式,提供免费试用额度 65 |
    66 |
    67 | 1.5版本deepclaude pro支持在线使用 68 |
    69 |
    70 | 1.3.1版本deepclaude pro普通模式和架构师模式生成塞尔达版本超级马里奥对比 71 |
    72 |
    73 | 1.3完整模式更新,包括deepclaude pro连接cursor教程 74 |
    75 |
    76 | 1.2后端大版本更新介绍,增加了完整模式 77 |
    78 |
    79 | 1.1前端大版本更新介绍 80 |
    81 |
    82 | 83 | 84 | 85 | 86 | 87 | 88 | ## 概述 89 | 90 | DeepClaude是一个高性能的大语言模型(LLM)推理API,它将深度求索R1的思维链(CoT)推理能力与人工智能公司Anthropic的Claude模型在创造力和代码生成方面的优势相结合。它提供了一个统一的接口,让你在完全掌控自己的API密钥和数据的同时,充分利用这两个模型的优势。 91 | 92 | ## 在线访问地址 93 | 94 | ``` 95 | https://deepclaudepro.com/ 96 | ``` 97 | 98 | ## 功能特性 99 | 🚀 **零延迟** - 由高性能的Rust API驱动,先由R1的思维链提供即时响应,随后在单个流中呈现Claude的回复 100 | 🔒 **私密且安全** - 采用端到端的安全措施,进行本地API密钥管理。你的数据将保持私密 101 | ⚙️ **高度可配置** - 可自定义API和接口的各个方面,以满足你的需求 102 | 🌟 **开源** - 免费的开源代码库。你可以根据自己的意愿进行贡献、修改和部署 103 | 🤖 **双人工智能能力** - 将深度求索R1的推理能力与Claude的创造力和代码生成能力相结合 104 | 🔑 **自带密钥管理的API** - 在我们的托管基础设施中使用你自己的API密钥,实现完全掌控 105 | 106 | ## 为什么选择R1和Claude? 107 | 深度求索R1的思维链轨迹展示了深度推理能力,达到了大语言模型能够进行“元认知”的程度——能够自我纠正、思考边缘情况,并以自然语言进行准蒙特卡洛树搜索。 108 | 109 | 然而,R1在代码生成、创造力和对话技巧方面有所欠缺。claude 3.5 sonnet版本在这些领域表现出色,是完美的补充。DeepClaude结合了这两个模型,以提供: 110 | - R1卓越的推理和问题解决能力 111 | - Claude出色的代码生成和创造力 112 | - 单次API调用即可实现快速的流式响应 113 | - 使用你自己的API密钥实现完全掌控 114 | 115 | ## 快速入门 116 | ### 先决条件 117 | - Rust 1.75或更高版本 118 | - 深度求索API密钥 119 | - Anthropic API密钥 120 | 121 | ### 安装步骤 122 | 1. 克隆存储库: 123 | ```bash 124 | git clone https://github.com/getasterisk/deepclaude.git 125 | cd deepclaude 126 | ``` 127 | 2. 构建项目: 128 | ```bash 129 | cargo build --release 130 | ``` 131 | 132 | 3. 运行后端环境 133 | 134 | ``` 135 | UST_LOG=debug cargo run --release 136 | ``` 137 | 138 | 4. 运行前端环境 139 | 140 | windows中 141 | 142 | ``` 143 | cd frontend & npm run dev 144 | ``` 145 | 146 | macos中 147 | 148 | ``` 149 | cd frontend && npm run dev 150 | ``` 151 | 152 | 5. 前端访问地址 153 | 154 | ``` 155 | http://localhost:3000/chat 156 | ``` 157 | 158 | ### 模式切换 159 | 测试结果在:deepclaude的benchmark项目中, full模式是参照aider官方的架构师编辑师模式实现,需要等更长时间,有更好的效果。 160 | 161 | **方法一:** 162 | 163 | 在前端界面直接设置,在底下编辑完成后,可以直接保存环境变量到.env文件中 164 | 165 | 166 | 167 | **方法二:** 168 | 169 | 在项目根目录中编辑`.env`文件: 170 | 171 | mode变量可以编辑为full或者normal 172 | 173 | ### 配置方法 174 | 175 | 第一步执行环境文件的模版迁移,会将 `.env.example` 文件复制为 `.env` 文件 176 | 177 | mac os中 178 | 179 | ``` 180 | cp .env.example .env 181 | ``` 182 | 183 | windows中 184 | 185 | ``` 186 | copy .env.example .env 187 | ``` 188 | 189 | 第二步就是配置.env文件的内容 190 | 191 | **方法一:** 192 | 193 | 在前端界面直接设置,在底下编辑完成后,可以直接保存环境变量到.env文件中 194 | 195 | 196 | 197 | **方法二:** 198 | 199 | 在项目根目录中编辑`.env`文件: 200 | 201 | ```toml 202 | # api密钥,自己取的 203 | API_KEY=xyh110 204 | # deepseek的密钥 205 | DEEPSEEK_API_KEY= 206 | # claude模型的密钥 207 | ANTHROPIC_API_KEY= 208 | # 服务的端口 209 | PORT=1337 210 | # 选择模式,包括full和normal,full是包括r1的结果且进行了专门的优化适合于编程,normal是只包含思考内容,所以full模型下,获取calude结果时间更长 211 | MODE=normal 212 | # API URL配置 213 | # DeeepSeek的密钥 214 | # 如果使用deepseek格式的api就填DEEPSEEK_OPENAI_TYPE_API_URL 215 | DEEPSEEK_OPENAI_TYPE_API_URL=https://ark.cn-beijing.volces.com/api/v3/chat/completions 216 | # Claude的密钥,底下两种2选1填 217 | # 如果使用claude格式的api就填ANTHROPIC_API_URL,比如https://xxxx/v1/messages 218 | ANTHROPIC_API_URL= 219 | # 如果使用openai格式的api就填CLAUDE_OPENAI_TYPE_API_URL,比如https://xxxx/v1/chat/completions 220 | CLAUDE_OPENAI_TYPE_API_URL=https://api.gptsapi.net/v1/chat/completions 221 | # 模型配置 222 | CLAUDE_DEFAULT_MODEL=claude-3-7-sonnet-20250219 223 | #DEEPSEEK_DEFAULT_MODEL=deepseek-r1-250120 224 | DEEPSEEK_DEFAULT_MODEL=deepseek-r1-250120 225 | ``` 226 | 227 | ## API使用方法 228 | 229 | 请参阅[API文档](https://deepclaude.chat) 230 | 231 | ### 非流式输出示例 232 | 233 | ```python 234 | curl -X POST "http://127.0.0.1:1337/v1/chat/completions" \ 235 | -H "Authorization: Bearer xyh110" \ 236 | -H "Content-Type: application/json" \ 237 | -d '{ 238 | "model": "deepclaude", 239 | "messages": [ 240 | {"role": "user", "content": "你是谁"} 241 | ] 242 | }' 243 | ``` 244 | 245 | ### 流式传输示例 246 | ```python 247 | curl -X POST "http://127.0.0.1:1337/v1/chat/completions" \ 248 | -H "Authorization: Bearer xyh110" \ 249 | -H "Content-Type: application/json" \ 250 | -d '{ 251 | "model": "deepclaude", 252 | "messages": [ 253 | {"role": "user", "content": "你是谁"} 254 | ], 255 | "stream": true 256 | }' 257 | ``` 258 | 259 | ## 配置选项 260 | API支持通过请求体进行广泛的配置: 261 | ```json 262 | { 263 | "stream": false, 264 | "verbose": false, 265 | "system": "可选的系统提示", 266 | "messages": [...], 267 | "deepseek_config": { 268 | "headers": {}, 269 | "body": {} 270 | }, 271 | "anthropic_config": { 272 | "headers": {}, 273 | "body": {} 274 | } 275 | } 276 | ``` 277 | 278 | ## 配置chatbox和cherrystudio 279 | 280 | 密钥都是前面.env中配置的API_KEY=xxx,那么这里就填xxx 281 | 282 | **chatbox** 283 | 284 | 285 | 286 | **cherrystudio** 287 | 288 | 289 | 290 | ## 自主托管 291 | 292 | DeepClaude可以在你自己的基础设施上进行自主托管。请按照以下步骤操作: 293 | 1. 配置环境变量或`config.toml`文件 294 | 2. 构建Docker镜像或从源代码编译 295 | 3. 部署到你首选的托管平台 296 | 297 | ## 安全性 298 | - 不存储或记录数据 299 | - 采用自带密钥(BYOK)架构 300 | - 定期进行安全审计和更新 301 | 302 | # 星星记录 303 | 304 | [![Star History Chart](https://api.star-history.com/svg?repos=yuanhang110/DeepClaude_Pro&type=Date)](https://star-history.com/#yuanhang110/DeepClaude_Pro&Date) 305 | 306 | ## 贡献代码 307 | 我们欢迎贡献!请参阅我们的[贡献指南](CONTRIBUTING.md),了解有关以下方面的详细信息: 308 | - 行为准则 309 | - 开发流程 310 | - 提交拉取请求 311 | - 报告问题 312 | -------------------------------------------------------------------------------- /clean_benchmark_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import json 6 | import argparse 7 | from pathlib import Path 8 | 9 | 10 | def clean_benchmark_results(base_dir="tmp.benchmarks"): 11 | """ 12 | 遍历指定目录下的所有文件夹,查找并删除符合条件的.aider.results.json文件 13 | 14 | 参数: 15 | base_dir: 基础目录路径,默认为"tmp.benchmarks" 16 | """ 17 | base_path = Path(base_dir) 18 | 19 | if not base_path.exists() or not base_path.is_dir(): 20 | print(f"错误: 目录 {base_dir} 不存在或不是一个文件夹") 21 | return 22 | 23 | # 遍历基础目录下的所有子目录 24 | for subdir in base_path.iterdir(): 25 | if not subdir.is_dir(): 26 | continue 27 | 28 | print(f"正在检查目录: {subdir}") 29 | process_directory(subdir) 30 | 31 | 32 | def process_directory(directory): 33 | """ 34 | 处理指定目录及其子目录中的.aider.results.json文件 35 | 36 | 参数: 37 | directory: 要处理的目录路径 38 | """ 39 | # 递归遍历目录 40 | found_files = False 41 | for path in directory.glob("**/*.aider.results.json"): 42 | found_files = True 43 | try: 44 | print(f"发现文件: {path}") 45 | with open(path, "r", encoding="utf-8") as f: 46 | data = json.load(f) 47 | 48 | # 打印num_user_asks和test_timeouts的值以便调试 49 | num_asks = data.get("num_user_asks", "未找到") 50 | test_timeouts = data.get("test_timeouts", "未找到") 51 | print(f"num_user_asks值: {num_asks}") 52 | print(f"test_timeouts值: {test_timeouts}") 53 | 54 | # 检查num_user_asks或test_timeouts字段,确保值不为0 55 | if ("num_user_asks" in data and data["num_user_asks"] != 0) or ( 56 | "test_timeouts" in data and data["test_timeouts"] != 0 57 | ): 58 | # 构建对应的.aider.chat.history.md文件路径 59 | history_file = path.parent / ".aider.chat.history.md" 60 | 61 | # 询问用户是否确认删除 62 | print(f"\n发现需要删除的文件:") 63 | print(f" - {path}") 64 | if history_file.exists(): 65 | print(f" - {history_file}") 66 | 67 | confirm = input(f"确认删除以上文件? (y/n): ").strip().lower() 68 | if confirm == "y": 69 | # 删除.aider.results.json文件 70 | os.remove(path) 71 | print(f"已删除文件: {path}") 72 | 73 | # 如果存在对应的.aider.chat.history.md文件,也删除它 74 | if history_file.exists(): 75 | os.remove(history_file) 76 | print(f"已删除文件: {history_file}") 77 | 78 | print(f"已从目录 {path.parent} 中删除文件") 79 | else: 80 | print("已跳过删除") 81 | else: 82 | print( 83 | f"跳过文件 {path} (num_user_asks = {num_asks}, test_timeouts = {test_timeouts})" 84 | ) 85 | except json.JSONDecodeError: 86 | print(f"警告: 无法解析JSON文件: {path}") 87 | except Exception as e: 88 | print(f"处理文件 {path} 时出错: {str(e)}") 89 | 90 | if not found_files: 91 | print(f"在目录 {directory} 及其子目录中未找到任何 .aider.results.json 文件") 92 | 93 | 94 | def main(): 95 | parser = argparse.ArgumentParser(description="清理benchmark结果文件") 96 | parser.add_argument("--dir", default="tmp.benchmarks", help="要处理的基础目录") 97 | parser.add_argument( 98 | "--subdir", help="只处理指定的子目录(例如: 2025-02-27-deepclaude37-rust)" 99 | ) 100 | parser.add_argument( 101 | "--path", 102 | help="指定完整的基础目录路径(例如: /Users/xiaoyuanhang/Desktop/aider/aider/tmp.benchmarks)", 103 | ) 104 | 105 | args = parser.parse_args() 106 | 107 | # 如果提供了完整路径,则使用它 108 | if args.path: 109 | base_dir = Path(args.path) 110 | else: 111 | # 获取当前工作目录 112 | current_dir = Path.cwd() 113 | base_dir = Path(args.dir) 114 | 115 | # 如果base_dir不是绝对路径,则将其视为相对于当前工作目录的路径 116 | if not base_dir.is_absolute(): 117 | base_dir = current_dir / base_dir 118 | 119 | print(f"使用基础目录: {base_dir}") 120 | 121 | if args.subdir: 122 | # 只处理指定的子目录 123 | target_dir = base_dir / args.subdir 124 | if not target_dir.exists(): 125 | print(f"错误: 目录 {target_dir} 不存在") 126 | return 127 | 128 | if not target_dir.is_dir(): 129 | print(f"错误: {target_dir} 不是一个文件夹") 130 | return 131 | 132 | print(f"正在检查指定目录: {target_dir}") 133 | process_directory(target_dir) 134 | else: 135 | # 处理所有子目录 136 | if not base_dir.exists(): 137 | print(f"错误: 目录 {base_dir} 不存在") 138 | return 139 | 140 | if not base_dir.is_dir(): 141 | print(f"错误: {base_dir} 不是一个文件夹") 142 | return 143 | 144 | clean_benchmark_results(base_dir) 145 | 146 | print("处理完成") 147 | 148 | 149 | if __name__ == "__main__": 150 | main() 151 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | [server] 3 | host = "127.0.0.1" 4 | port = 1337 5 | 6 | # Pricing Configuration (per million tokens) 7 | [pricing] 8 | [pricing.deepseek] 9 | input_cache_hit_price = 0.14 10 | input_cache_miss_price = 0.55 11 | output_price = 2.19 12 | 13 | [pricing.anthropic] 14 | [pricing.anthropic.claude_3_sonnet] 15 | input_price = 3.0 16 | output_price = 15.0 17 | cache_write_price = 3.75 18 | cache_read_price = 0.30 19 | 20 | [pricing.anthropic.claude_3_haiku] 21 | input_price = 0.80 22 | output_price = 4.0 23 | cache_write_price = 1.0 24 | cache_read_price = 0.08 25 | 26 | [pricing.anthropic.claude_3_opus] 27 | input_price = 15.0 28 | output_price = 75.0 29 | cache_write_price = 18.75 30 | cache_read_price = 1.50 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | build: . 4 | container_name: deepclaude_api 5 | restart: unless-stopped 6 | ports: 7 | - "127.0.0.1:1337:1337" 8 | volumes: 9 | - ./config.toml:/usr/local/bin/config.toml 10 | networks: 11 | - deepclaude_network 12 | 13 | networks: 14 | deepclaude_network: 15 | name: deepclaude_network -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | /dist 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env* 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /frontend/app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { Chat } from "../../components/chat" 5 | import { Settings } from "../../components/settings" 6 | 7 | export default function ChatPage() { 8 | const [selectedModel, setSelectedModel] = useState("claude-3-5-sonnet-20241022") 9 | const [apiTokens, setApiTokens] = useState({ 10 | deepseekApiToken: "", 11 | anthropicApiToken: "" 12 | }) 13 | 14 | return ( 15 |
    16 | 19 | 24 |
    25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyoumo/DeepClaude_Pro/001b7ebb4196f7960e39e9be34086c1a007154a8/frontend/app/favicon.ico -------------------------------------------------------------------------------- /frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 8 | 9 | --background: 0 0% 100%; 10 | --foreground: 240 10% 3.9%; 11 | --card: 0 0% 100%; 12 | --card-foreground: 240 10% 3.9%; 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | --primary: 240 5.9% 10%; 16 | --primary-foreground: 0 0% 98%; 17 | --secondary: 240 4.8% 95.9%; 18 | --secondary-foreground: 240 5.9% 10%; 19 | --muted: 240 4.8% 95.9%; 20 | --muted-foreground: 240 3.8% 46.1%; 21 | --accent: 240 4.8% 95.9%; 22 | --accent-foreground: 240 5.9% 10%; 23 | --destructive: 0 84.2% 60.2%; 24 | --destructive-foreground: 0 0% 98%; 25 | --border: 240 5.9% 90%; 26 | --input: 240 5.9% 90%; 27 | --ring: 240 5.9% 10%; 28 | --radius: 0.5rem; 29 | } 30 | 31 | .dark { 32 | --background: 240 10% 3.9%; 33 | --foreground: 0 0% 98%; 34 | --card: 240 10% 3.9%; 35 | --card-foreground: 0 0% 98%; 36 | --popover: 240 10% 3.9%; 37 | --popover-foreground: 0 0% 98%; 38 | --primary: 0 0% 98%; 39 | --primary-foreground: 240 5.9% 10%; 40 | --secondary: 240 3.7% 15.9%; 41 | --secondary-foreground: 0 0% 98%; 42 | --muted: 240 3.7% 15.9%; 43 | --muted-foreground: 240 5% 64.9%; 44 | --accent: 240 3.7% 15.9%; 45 | --accent-foreground: 0 0% 98%; 46 | --destructive: 0 62.8% 30.6%; 47 | --destructive-foreground: 0 0% 98%; 48 | --border: 240 3.7% 15.9%; 49 | --input: 240 3.7% 15.9%; 50 | --ring: 240 4.9% 83.9%; 51 | } 52 | } 53 | 54 | @layer base { 55 | * { 56 | @apply border-border; 57 | } 58 | body { 59 | @apply bg-background text-foreground; 60 | } 61 | } 62 | 63 | @layer utilities { 64 | /* Base grid pattern */ 65 | .bg-dots-base { 66 | background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h40v40H0z' fill='none'/%3E%3Cpath d='M0 0h1v1H0z' fill='rgb(255 255 255 / 0.15)'/%3E%3C/svg%3E"); 67 | } 68 | 69 | /* Diagonal lines pattern */ 70 | .bg-lines { 71 | background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0l60 60M30 0l30 60M0 30l30 30' stroke='rgb(255 255 255 / 0.1)' stroke-width='1' fill='none'/%3E%3C/svg%3E"); 72 | } 73 | 74 | /* Small dots pattern */ 75 | .bg-dots-small { 76 | background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='10' cy='10' r='1' fill='rgb(255 255 255 / 0.12)'/%3E%3C/svg%3E"); 77 | transform: rotate(30deg); 78 | } 79 | 80 | /* Combined pattern class */ 81 | .bg-pattern-combined { 82 | @apply bg-dots-base bg-lines bg-dots-small; 83 | background-size: 40px 40px, 60px 60px, 20px 20px; 84 | } 85 | 86 | .animate-float { 87 | animation: float 6s ease-in-out infinite; 88 | } 89 | } 90 | 91 | @keyframes float { 92 | 0% { 93 | transform: translateY(0px); 94 | } 95 | 50% { 96 | transform: translateY(-20px); 97 | } 98 | 100% { 99 | transform: translateY(0px); 100 | } 101 | } 102 | 103 | /* Markdown Styles */ 104 | .prose pre { 105 | @apply bg-secondary text-secondary-foreground p-4 rounded-lg overflow-x-auto; 106 | } 107 | 108 | .prose code { 109 | @apply bg-secondary text-secondary-foreground px-1.5 py-0.5 rounded; 110 | } 111 | 112 | .prose pre code { 113 | @apply bg-transparent p-0 text-sm; 114 | } 115 | 116 | .prose img { 117 | @apply rounded-lg; 118 | } 119 | 120 | .prose a { 121 | @apply text-primary underline-offset-4 hover:text-primary/80; 122 | } 123 | 124 | .prose blockquote { 125 | @apply border-l-4 border-primary/20 pl-4 italic; 126 | } 127 | 128 | .prose ul { 129 | @apply list-disc list-outside; 130 | } 131 | 132 | .prose ol { 133 | @apply list-decimal list-outside; 134 | } 135 | 136 | .prose h1, .prose h2, .prose h3, .prose h4 { 137 | @apply font-semibold text-foreground scroll-m-20; 138 | } 139 | 140 | .prose h1 { 141 | @apply text-3xl lg:text-4xl; 142 | } 143 | 144 | .prose h2 { 145 | @apply text-2xl lg:text-3xl; 146 | } 147 | 148 | .prose h3 { 149 | @apply text-xl lg:text-2xl; 150 | } 151 | 152 | .prose h4 { 153 | @apply text-lg lg:text-xl; 154 | } 155 | 156 | /* Streaming text animation */ 157 | @keyframes fadeIn { 158 | from { 159 | opacity: 0; 160 | transform: translateY(1px); 161 | } 162 | to { 163 | opacity: 1; 164 | transform: translateY(0); 165 | } 166 | } 167 | 168 | .animate-stream { 169 | animation: fadeIn 0.15s ease-out forwards; 170 | } 171 | 172 | /* Sidebar Transitions */ 173 | @media (max-width: 1024px) { 174 | .sidebar-open { 175 | overflow: hidden; 176 | } 177 | } 178 | 179 | /* Custom scrollbar for dark mode */ 180 | /* Custom scrollbar for dark mode */ 181 | .dark ::-webkit-scrollbar { 182 | width: 6px; 183 | height: 6px; 184 | } 185 | 186 | .dark ::-webkit-scrollbar-track { 187 | @apply bg-background; 188 | } 189 | 190 | .dark ::-webkit-scrollbar-thumb { 191 | @apply bg-muted rounded-full; 192 | } 193 | 194 | .dark ::-webkit-scrollbar-thumb:hover { 195 | @apply bg-muted-foreground; 196 | } 197 | 198 | /* Sidebar scrollbar styling */ 199 | .dark .sidebar-scroll::-webkit-scrollbar { 200 | width: 2px; 201 | } 202 | 203 | .dark .sidebar-scroll::-webkit-scrollbar-track { 204 | @apply bg-background; 205 | } 206 | 207 | .dark .sidebar-scroll::-webkit-scrollbar-thumb { 208 | @apply bg-muted/30 rounded-full hover:bg-muted/50 transition-colors; 209 | } 210 | 211 | /* Smooth transitions */ 212 | .message-transition { 213 | transition: opacity 0.2s ease-in-out, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 214 | opacity: 0; 215 | transform: translateY(4px); 216 | } 217 | 218 | .message-transition[data-loaded="true"] { 219 | opacity: 1; 220 | transform: translateY(0); 221 | } 222 | 223 | .virtual-item-transition { 224 | transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 225 | will-change: transform; 226 | } 227 | 228 | /* Content fade transitions */ 229 | .content-fade { 230 | opacity: 0; 231 | transition: opacity 0.15s ease-in-out; 232 | } 233 | 234 | .content-fade[data-streaming="true"] { 235 | opacity: 1; 236 | } 237 | 238 | /* Initial state for all content */ 239 | .message-content { 240 | opacity: 1; 241 | } 242 | 243 | /* Add this to your existing globals.css */ 244 | @keyframes shimmer { 245 | 0% { 246 | background-position: -1000px 0; 247 | } 248 | 100% { 249 | background-position: 1000px 0; 250 | } 251 | } 252 | 253 | .shimmer { 254 | background: linear-gradient( 255 | 90deg, 256 | rgba(var(--primary) / 0.1) 0%, 257 | rgba(var(--primary) / 0.15) 50%, 258 | rgba(var(--primary) / 0.1) 100% 259 | ); 260 | background-size: 1000px 100%; 261 | animation: shimmer 8s linear infinite; 262 | } 263 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Toaster } from "../components/ui/toaster" 3 | import { PostHogProvider } from "../providers/posthog" 4 | import "./globals.css" 5 | 6 | export const metadata: Metadata = { 7 | title: "DeepClaude Pro", 8 | description: "DeepClaude Pro - 高级AI助手", 9 | } 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode 15 | }>) { 16 | return ( 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image" 4 | import Link from "next/link" 5 | import { usePostHog } from "../providers/posthog" 6 | import { Button } from "../components/ui/button" 7 | import { Card } from "../components/ui/card" 8 | import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../components/ui/collapsible" 9 | import { ArrowRight, Zap, Lock, Settings2, Code2, Sparkles, Github, Megaphone, ChevronDown } from "lucide-react" 10 | 11 | export default function LandingPage() { 12 | const posthog = usePostHog() 13 | return ( 14 |
    15 | {/* Hero Section */} 16 |
    17 | {/* Background Pattern */} 18 |
    19 | 20 | {/* Gradient Overlay */} 21 |
    22 | 23 | {/* Announcement Banner */} 24 |
    25 |
    26 |
    27 | 28 | 29 | 升级版本DeepClaude,支持OpenAI格式,可以自由配置OpenAI格式的DeepSeek R1和Claude 3.5 Sonnet的API 30 | 31 |
    32 |
    33 | 34 | {/* Content */} 35 |
    36 |
    37 | DeepClaude Logo 45 |
    46 | 47 |

    48 | DeepClaude 49 |

    50 | 51 |

    52 | 通过统一的应用程序编程接口(API)和聊天界面,利用 DeepSeek R1 的推理能力以及 Claude 的创造力和代码生成能力。 53 |

    54 | 55 |
    56 | { 57 | posthog.capture('cta_click', { 58 | location: 'hero', 59 | target: 'chat', 60 | timestamp: new Date().toISOString() 61 | }) 62 | }}> 63 | 67 | 68 | { 69 | posthog.capture('cta_click', { 70 | location: 'hero', 71 | target: 'docs', 72 | timestamp: new Date().toISOString() 73 | }) 74 | }}> 75 | 79 | 80 | { 85 | posthog.capture('cta_click', { 86 | location: 'hero', 87 | target: 'github', 88 | timestamp: new Date().toISOString() 89 | }) 90 | }} 91 | > 92 | 96 | 97 |
    98 | 99 |
    100 | 免费开源的项目 101 |
    102 |
    103 | 104 | {/* Scroll Indicator */} 105 | 116 |
    117 | 118 |
    119 | {/* Features Grid */} 120 |
    121 | {/* Background Pattern */} 122 |
    123 |
    124 |

    125 | 项目特点 126 |

    127 | 128 |
    129 | {/* Performance */} 130 | { 133 | posthog.capture('feature_view', { 134 | feature: 'zero_latency', 135 | timestamp: new Date().toISOString() 136 | }) 137 | }} 138 | > 139 |
    140 |
    141 | 142 |
    143 |

    0 延迟

    144 |

    145 | 由用 Rust 语言编写的高性能流式应用程序编程接口(API)提供支持,以单一流的形式实现 R1 的思维链CoT即时回复,随后紧跟 Claude 的回复。 146 |

    147 |
    148 |
    149 | 150 | {/* Security */} 151 | 152 |
    153 |
    154 | 155 |
    156 |

    隐私 & 安全

    157 |

    158 | 您的数据在端到端的安全性和本地API密钥管理下保持私密。 159 |

    160 |
    161 |
    162 | 163 | {/* Configuration */} 164 | 165 |
    166 |
    167 | 168 |
    169 |

    高度可配置

    170 |

    171 | 自定义API和界面以满足您的需求。 172 |

    173 |
    174 |
    175 | 176 | {/* Open Source */} 177 | 178 |
    179 |
    180 | 181 |
    182 |

    开源

    183 |

    184 | 免费且开源的代码库。贡献、修改和部署,随您所愿。 185 |

    186 |
    187 |
    188 | 189 | {/* AI Integration */} 190 | 191 |
    192 |
    193 | 194 |
    195 |

    双AI力量

    196 |

    197 | 结合DeepSeek R1的推理能力和Claude的创造力和代码生成能力。 198 |

    199 |
    200 |
    201 | 202 | {/* Managed BYOK API */} 203 | 204 |
    205 |
    206 | 215 | 216 | 217 |
    218 |

    托管BYOK API

    219 |

    220 | 使用您的API密钥和我们的托管基础设施,实现完全控制和灵活性。 221 |

    222 |
    223 |
    224 |
    225 |
    226 |
    227 | 228 | {/* FAQ Section */} 229 |
    230 |
    231 | {/* Background Pattern */} 232 |
    233 |
    234 |
    235 |
    236 |

    237 | 常见问题 238 |

    239 | 240 |
    241 | {/* Why R1 + Claude? */} 242 | 243 | 244 | { 247 | posthog.capture('faq_interaction', { 248 | question: 'why_r1_claude', 249 | timestamp: new Date().toISOString() 250 | }) 251 | }} 252 | > 253 |
    254 |

    为什么 R1 + Claude?

    255 | 256 |
    257 |
    258 | 259 |

    260 | DeepSeek R1的思维链(CoT)追踪展示了深度推理能力,达到了让大语言模型(LLM)出现“元认知”的程度——自我修正、思考极端情况等等。这是一种自然语言形式的准蒙特卡洛树搜索(MCTS) 。 261 |

    262 |

    263 | 但R1在代码生成、创造力以及对话技巧方面有所欠缺。在这三个方面都表现出色的模型是来自Anthropic公司的Claude 3.5 Sonnet New。那么,我们把它们两者结合起来怎么样呢?兼取两者之长?于是就有了DeepClaude! 264 |

    265 |

    266 | 使用DeepClaude,您可以在单个API调用中获得快速流式R1 CoT + Claude模型,使用您自己的API密钥。 267 |

    268 |
    269 |
    270 |
    271 | 272 | {/* Is it free? */} 273 | 274 | 275 | 276 |
    277 |

    托管API是免费的吗?

    278 | 279 |
    280 |
    281 | 282 |

    283 | 是的,100%免费,您可以使用自己的密钥。API将DeepSeek和Anthropic的流式API包装在一起。您还可以获得一些便利功能,例如计算组合使用情况和价格以供您使用。我们不保留任何日志,它是完全开源的——您可以自行托管、修改、重新分发,等等。 284 |

    285 |

    286 | 请随意在规模上使用它,我们已经在Asterisk生产中使用它,每天为数百万个令牌提供服务,它还没有让我们失望。像所有美好的事物一样,不要滥用它。 287 |

    288 |
    289 |
    290 |
    291 |
    292 |
    293 |
    294 | 295 | {/* CTA Section */} 296 |
    297 |
    298 | {/* Background Pattern */} 299 |
    300 |
    301 |
    302 |
    303 |

    304 | 开始阅读一些AI内部独白? 305 |

    306 |

    307 | 无需注册。无需信用卡。无需存储数据。 308 |

    309 |
    310 | { 311 | posthog.capture('cta_click', { 312 | location: 'footer', 313 | target: 'chat', 314 | timestamp: new Date().toISOString() 315 | }) 316 | }}> 317 | 321 | 322 | { 323 | posthog.capture('cta_click', { 324 | location: 'footer', 325 | target: 'docs', 326 | timestamp: new Date().toISOString() 327 | }) 328 | }}> 329 | 333 | 334 |
    335 |
    336 |
    337 |
    338 | 339 | {/* Footer */} 340 |
    341 |
    342 |
    343 | 一个“好玩”的项目由 344 | 350 | Asterisk Logo 359 | 360 |
    361 |
    362 |
    363 |
    364 | ) 365 | } 366 | -------------------------------------------------------------------------------- /frontend/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyoumo/DeepClaude_Pro/001b7ebb4196f7960e39e9be34086c1a007154a8/frontend/bun.lockb -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /frontend/components/settings.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect, useCallback, useMemo, memo } from "react" 4 | import { usePostHog } from "../providers/posthog" 5 | import debounce from "lodash/debounce" 6 | import { Settings2, RotateCcw, Save, Download, Eye, EyeOff } from "lucide-react" 7 | import { useToast } from "./ui/use-toast" 8 | import { Button } from "./ui/button" 9 | import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet" 10 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" 11 | import { Form, FormControl, FormField, FormItem, FormLabel } from "./ui/form" 12 | import { Textarea } from "./ui/textarea" 13 | import { Input } from "./ui/input" 14 | import { useForm } from "react-hook-form" 15 | 16 | // 添加后端API基础URL 17 | const API_BASE_URL = 'http://127.0.0.1:1337'; 18 | 19 | interface SettingsFormValues { 20 | model: string 21 | systemPrompt: string 22 | apiKey: string 23 | port: string 24 | deepseekApiKey: string 25 | anthropicApiKey: string 26 | deepseekApiUrl: string 27 | anthropicApiUrl: string 28 | claudeOpenaiTypeApiUrl: string 29 | claudeDefaultModel: string 30 | deepseekDefaultModel: string 31 | deepseekHeaders: { key: string; value: string }[] 32 | deepseekBody: { key: string; value: string }[] 33 | anthropicHeaders: { key: string; value: string }[] 34 | anthropicBody: { key: string; value: string }[] 35 | mode: string 36 | } 37 | 38 | interface SettingsProps { 39 | onSettingsChange: (settings: { deepseekApiToken: string; anthropicApiToken: string }) => void 40 | } 41 | 42 | // 使用memo优化KeyValuePairFields组件 43 | const KeyValuePairFields = memo(({ 44 | name, 45 | label, 46 | initialValue, 47 | onChange 48 | }: { 49 | name: "deepseekHeaders" | "deepseekBody" | "anthropicHeaders" | "anthropicBody" 50 | label: string 51 | initialValue: Array<{key: string, value: string}> 52 | onChange: (pairs: Array<{key: string, value: string}>) => void 53 | }) => { 54 | // 使用完全独立的状态管理键值对 55 | const [pairs, setPairs] = useState>(() => { 56 | return initialValue && initialValue.length > 0 57 | ? initialValue.map(pair => ({ key: pair.key || "", value: pair.value || "" })) 58 | : [{ key: "", value: "" }]; 59 | }); 60 | 61 | // 当初始值变化时更新本地状态 62 | useEffect(() => { 63 | if (initialValue && initialValue.length > 0) { 64 | setPairs(initialValue.map(pair => ({ key: pair.key || "", value: pair.value || "" }))); 65 | } 66 | }, [initialValue]); 67 | 68 | // 所有状态更改都通过这个函数处理,确保通知父组件 69 | const updatePairs = useCallback((newPairs: Array<{key: string, value: string}>) => { 70 | setPairs(newPairs); 71 | onChange(newPairs); 72 | }, [onChange]); 73 | 74 | // 处理键的变化 75 | const handleKeyChange = useCallback((index: number, newKey: string) => { 76 | const newPairs = [...pairs]; 77 | newPairs[index] = { ...newPairs[index], key: newKey }; 78 | updatePairs(newPairs); 79 | }, [pairs, updatePairs]); 80 | 81 | // 处理值的变化 82 | const handleValueChange = useCallback((index: number, newValue: string) => { 83 | const newPairs = [...pairs]; 84 | newPairs[index] = { ...newPairs[index], value: newValue }; 85 | updatePairs(newPairs); 86 | }, [pairs, updatePairs]); 87 | 88 | // 删除一对 89 | const handleRemove = useCallback((index: number) => { 90 | if (pairs.length > 1) { 91 | const newPairs = [...pairs]; 92 | newPairs.splice(index, 1); 93 | updatePairs(newPairs); 94 | } 95 | }, [pairs, updatePairs]); 96 | 97 | // 添加一对 98 | const handleAdd = useCallback(() => { 99 | updatePairs([...pairs, { key: "", value: "" }]); 100 | }, [pairs, updatePairs]); 101 | 102 | return ( 103 |
    104 |
    {label}
    105 |
    106 | {pairs.map((pair, index) => ( 107 |
    108 | handleKeyChange(index, e.target.value)} 112 | /> 113 | handleValueChange(index, e.target.value)} 117 | /> 118 | 126 |
    127 | ))} 128 | 135 |
    136 |
    137 | ); 138 | }); 139 | 140 | // 为了避免React开发模式下的警告,添加displayName 141 | KeyValuePairFields.displayName = "KeyValuePairFields"; 142 | 143 | // 处理API密钥显示的函数,只显示前6个字符,其余用*替代 144 | const maskApiKey = (key: string): string => { 145 | if (!key) return ''; 146 | return key.length > 6 ? key.substring(0, 6) + '*'.repeat(key.length - 6) : key; 147 | }; 148 | 149 | export function Settings({ onSettingsChange }: SettingsProps) { 150 | const [open, setOpen] = useState(false) 151 | const { toast } = useToast() 152 | const posthog = usePostHog() 153 | const [showDeepseekApiKey, setShowDeepseekApiKey] = useState(false); 154 | const [showAnthropicApiKey, setShowAnthropicApiKey] = useState(false); 155 | const [showApiKey, setShowApiKey] = useState(false); 156 | 157 | const form = useForm({ 158 | defaultValues: { 159 | model: "", 160 | systemPrompt: "You are a helpful AI assistant who excels at reasoning and responds in Markdown format. For code snippets, you wrap them in Markdown codeblocks with it's language specified.", 161 | apiKey: "", 162 | port: "1337", 163 | deepseekApiKey: "", 164 | anthropicApiKey: "", 165 | deepseekApiUrl: "", 166 | anthropicApiUrl: "", 167 | claudeOpenaiTypeApiUrl: "", 168 | claudeDefaultModel: "", 169 | deepseekDefaultModel: "", 170 | deepseekHeaders: [{ key: "", value: "" }], 171 | deepseekBody: [{ key: "", value: "" }], 172 | anthropicHeaders: [{ key: "anthropic-version", value: "2023-06-01" }], 173 | anthropicBody: [{ key: "", value: "" }], 174 | mode: "normal" 175 | } 176 | }) 177 | 178 | // 添加独立的键值对状态 179 | const [deepseekHeaders, setDeepseekHeaders] = useState>([ 180 | { key: "", value: "" } 181 | ]); 182 | const [deepseekBody, setDeepseekBody] = useState>([ 183 | { key: "", value: "" } 184 | ]); 185 | const [anthropicHeaders, setAnthropicHeaders] = useState>([ 186 | { key: "anthropic-version", value: "2023-06-01" } 187 | ]); 188 | const [anthropicBody, setAnthropicBody] = useState>([ 189 | { key: "", value: "" } 190 | ]); 191 | 192 | // 更新useEffect以加载本地存储的设置到独立状态 193 | useEffect(() => { 194 | const savedSettings = localStorage.getItem('deepclaude-settings') 195 | if (savedSettings) { 196 | const settings = JSON.parse(savedSettings) 197 | form.reset(settings) 198 | 199 | // 更新独立的键值对状态 200 | if (settings.deepseekHeaders) setDeepseekHeaders(settings.deepseekHeaders); 201 | if (settings.deepseekBody) setDeepseekBody(settings.deepseekBody); 202 | if (settings.anthropicHeaders) setAnthropicHeaders(settings.anthropicHeaders); 203 | if (settings.anthropicBody) setAnthropicBody(settings.anthropicBody); 204 | 205 | onSettingsChange({ 206 | deepseekApiToken: settings.deepseekApiKey, 207 | anthropicApiToken: settings.anthropicApiKey 208 | }) 209 | } 210 | }, [form, onSettingsChange]) 211 | 212 | // 创建一个居中的toast函数 213 | const centerToast = useCallback((props: any) => { 214 | toast({ 215 | ...props, 216 | className: "fixed top-4 left-1/2 transform -translate-x-1/2 z-[200] max-w-[25%] w-fit", // 限制宽度为屏幕的1/4 217 | }); 218 | }, [toast]); 219 | 220 | // 修改保存设置函数,将独立状态合并到表单数据中 221 | const saveSettings = async (values: Omit) => { 222 | try { 223 | // 合并表单值和独立状态 224 | const completeValues = { 225 | ...values, 226 | deepseekHeaders, 227 | deepseekBody, 228 | anthropicHeaders, 229 | anthropicBody 230 | }; 231 | 232 | const response = await fetch(`${API_BASE_URL}/v1/env/update`, { 233 | method: 'POST', 234 | headers: { 235 | 'Content-Type': 'application/json', 236 | }, 237 | body: JSON.stringify({ 238 | variables: { 239 | API_KEY: values.apiKey, 240 | PORT: values.port, 241 | DEEPSEEK_API_KEY: values.deepseekApiKey, 242 | ANTHROPIC_API_KEY: values.anthropicApiKey, 243 | DEEPSEEK_OPENAI_TYPE_API_URL: values.deepseekApiUrl, 244 | ANTHROPIC_API_URL: values.anthropicApiUrl, 245 | CLAUDE_OPENAI_TYPE_API_URL: values.claudeOpenaiTypeApiUrl, 246 | CLAUDE_DEFAULT_MODEL: values.claudeDefaultModel, 247 | DEEPSEEK_DEFAULT_MODEL: values.deepseekDefaultModel, 248 | MODE: values.mode, 249 | } 250 | }) 251 | }); 252 | 253 | if (!response.ok) { 254 | throw new Error('保存设置失败'); 255 | } 256 | 257 | centerToast({ 258 | title: "设置已保存", 259 | description: "所有环境变量已成功更新", 260 | }) 261 | 262 | // 保存到localStorage 263 | localStorage.setItem('deepclaude-settings', JSON.stringify(completeValues)) 264 | 265 | // 通知父组件设置已更改 266 | onSettingsChange({ 267 | deepseekApiToken: values.deepseekApiKey, 268 | anthropicApiToken: values.anthropicApiKey 269 | }) 270 | } catch (error) { 271 | console.error('保存设置失败:', error) 272 | centerToast({ 273 | title: "错误", 274 | description: "保存设置失败,请重试", 275 | variant: "destructive" 276 | }) 277 | } 278 | } 279 | 280 | // 更新重置函数以重置独立状态 281 | const handleReset = () => { 282 | form.reset({ 283 | systemPrompt: "You are a helpful AI assistant who excels at reasoning and responds in Markdown format. For code snippets, you wrap them in Markdown codeblocks with it's language specified.", 284 | apiKey: "", 285 | port: "1337", 286 | deepseekApiKey: "", 287 | anthropicApiKey: "", 288 | deepseekApiUrl: "", 289 | anthropicApiUrl: "", 290 | claudeOpenaiTypeApiUrl: "", 291 | claudeDefaultModel: "", 292 | deepseekDefaultModel: "", 293 | deepseekHeaders: [], 294 | deepseekBody: [], 295 | anthropicHeaders: [], 296 | anthropicBody: [], 297 | mode: "normal" 298 | }) 299 | 300 | // 重置独立状态 301 | setDeepseekHeaders([{ key: "", value: "" }]); 302 | setDeepseekBody([{ key: "", value: "" }]); 303 | setAnthropicHeaders([{ key: "anthropic-version", value: "2023-06-01" }]); 304 | setAnthropicBody([{ key: "", value: "" }]); 305 | 306 | localStorage.removeItem('deepclaude-settings') 307 | onSettingsChange({ 308 | deepseekApiToken: "", 309 | anthropicApiToken: "" 310 | }) 311 | 312 | // Track settings reset 313 | posthog.capture('settings_reset', { 314 | timestamp: new Date().toISOString() 315 | }) 316 | 317 | centerToast({ 318 | description: "Settings reset to defaults", 319 | duration: 2000, 320 | }) 321 | } 322 | 323 | // 加载环境变量并填充到表单中 324 | const loadEnvVariables = async () => { 325 | try { 326 | centerToast({ 327 | title: "正在获取环境变量", 328 | description: "请稍候...", 329 | duration: 2000, 330 | }); 331 | 332 | const response = await fetch(`${API_BASE_URL}/v1/env/variables`); 333 | if (!response.ok) { 334 | throw new Error('获取环境变量失败'); 335 | } 336 | 337 | const data = await response.json(); 338 | console.log('获取到的环境变量:', data); 339 | 340 | if (data.status === 'success' && data.variables) { 341 | // 将环境变量填充到对应的表单字段中 342 | const variables = data.variables; 343 | 344 | // 创建一个新的表单值对象 345 | const newFormValues: Partial = { 346 | apiKey: variables.API_KEY || '', 347 | port: variables.PORT || '1337', 348 | deepseekApiKey: variables.DEEPSEEK_API_KEY || '', 349 | anthropicApiKey: variables.ANTHROPIC_API_KEY || '', 350 | deepseekApiUrl: variables.DEEPSEEK_OPENAI_TYPE_API_URL || '', 351 | anthropicApiUrl: variables.ANTHROPIC_API_URL || '', 352 | claudeOpenaiTypeApiUrl: variables.CLAUDE_OPENAI_TYPE_API_URL || '', 353 | claudeDefaultModel: variables.CLAUDE_DEFAULT_MODEL || '', 354 | deepseekDefaultModel: variables.DEEPSEEK_DEFAULT_MODEL || '', 355 | mode: variables.MODE || 'normal', 356 | }; 357 | 358 | console.log('准备设置表单值:', newFormValues); 359 | 360 | // 使用reset方法一次性更新所有表单值 361 | form.reset(newFormValues); 362 | 363 | // 更新独立的键值对状态 364 | // 这里可以根据需要进行扩展 365 | 366 | centerToast({ 367 | title: "环境变量已加载", 368 | description: "已将环境变量填充到表单中", 369 | duration: 3000, 370 | }); 371 | 372 | // 通知父组件API密钥已更改 373 | if (newFormValues.deepseekApiKey || newFormValues.anthropicApiKey) { 374 | onSettingsChange({ 375 | deepseekApiToken: newFormValues.deepseekApiKey || '', 376 | anthropicApiToken: newFormValues.anthropicApiKey || '' 377 | }); 378 | } 379 | } else { 380 | centerToast({ 381 | title: "警告", 382 | description: "获取到的环境变量格式不正确", 383 | variant: "destructive", 384 | duration: 3000, 385 | }); 386 | } 387 | } catch (error) { 388 | console.error('加载环境变量失败:', error); 389 | centerToast({ 390 | title: "错误", 391 | description: "加载环境变量失败,请重试", 392 | variant: "destructive" 393 | }); 394 | } 395 | }; 396 | 397 | // 移除错误处理器 - 在加载配置按钮上 398 | useEffect(() => { 399 | // 捕获并抑制可能的React错误 400 | const originalConsoleError = console.error; 401 | console.error = (...args) => { 402 | const message = args[0] || ''; 403 | if (typeof message === 'string' && message.includes('changing an uncontrolled input')) { 404 | // 忽略控制组件相关的错误 405 | return; 406 | } 407 | originalConsoleError.apply(console, args); 408 | }; 409 | 410 | return () => { 411 | console.error = originalConsoleError; 412 | }; 413 | }, []); 414 | 415 | return ( 416 | 417 | 418 |
    419 | 426 | {!form.getValues("deepseekApiKey") || !form.getValues("anthropicApiKey") ? ( 427 |
    428 | 配置API密钥以开始 429 |
    430 | ) : null} 431 |
    432 |
    433 | 434 | 435 |
    {/* Spacer for close button */} 436 |
    437 | 设置 438 |
    439 | 448 | 469 |
    470 |
    471 | 472 |
    473 | 474 | 475 |
    476 |

    环境变量

    477 |
    478 | 487 | 494 | 509 |
    510 | ( 514 | 515 | API密钥 516 | 517 |
    518 | field.onChange(e.target.value)} 523 | /> 524 | 533 |
    534 |
    535 |
    536 | )} 537 | /> 538 | 539 | ( 543 | 544 | 端口 545 | 546 | 547 | 548 | 549 | )} 550 | /> 551 | 552 | ( 556 | 557 | 模式 558 | 559 |
    560 | 572 |
    573 |
    574 |
    575 | 普通模式: 仅将DeepSeek的思考内容传递给Claude
    576 | 完整模式: 将DeepSeek不包括思考内容的最终结果传给Claude 577 |
    578 |
    579 | )} 580 | /> 581 |
    582 | 583 | ( 587 | 588 | DeepSeek API密钥 589 | 590 |
    591 | field.onChange(e.target.value)} 596 | /> 597 | 606 |
    607 |
    608 |
    609 | )} 610 | /> 611 | 612 | ( 616 | 617 | Anthropic API密钥 618 | 619 |
    620 | field.onChange(e.target.value)} 625 | /> 626 | 635 |
    636 |
    637 |
    638 | )} 639 | /> 640 | 641 | ( 645 | 646 | DeepSeek API URL 647 | 648 | 649 | 650 | 651 | )} 652 | /> 653 | 654 | ( 658 | 659 | Anthropic API URL 660 | 661 | 662 | 663 | 664 | )} 665 | /> 666 | 667 | ( 671 | 672 | Claude OpenAI格式 API URL 673 | 674 | 675 | 676 | 677 | )} 678 | /> 679 | 680 | ( 684 | 685 | Claude默认模型 686 | 687 | 688 | 689 | 690 | )} 691 | /> 692 | 693 | ( 697 | 698 | DeepSeek默认模型 699 | 700 | 701 | 702 | 703 | )} 704 | /> 705 | 706 |
    707 |
    708 |

    DeepSeek 配置

    709 | 715 | 721 |
    722 | 723 |
    724 |

    Anthropic 配置

    725 | 731 | 737 |
    738 |
    739 | 740 | 741 | 742 | 743 | ) 744 | } 745 | -------------------------------------------------------------------------------- /frontend/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /frontend/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
    41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
    53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
    61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
    73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /frontend/components/ui/chat-input.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { useRef, useEffect } from "react" 5 | import { Textarea } from "./textarea" 6 | import { Button } from "./button" 7 | 8 | interface ChatInputProps { 9 | value: string 10 | onChange: (value: string) => void 11 | onSubmit: () => void 12 | placeholder?: string 13 | } 14 | 15 | export function ChatInput({ 16 | value, 17 | onChange, 18 | onSubmit, 19 | placeholder 20 | }: ChatInputProps) { 21 | const textareaRef = useRef(null) 22 | 23 | // 自动调整高度 24 | useEffect(() => { 25 | const textarea = textareaRef.current 26 | if (textarea) { 27 | textarea.style.height = 'auto' 28 | textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px` 29 | } 30 | }, [value]) 31 | 32 | // 处理特殊按键 33 | const handleKeyDown = (e: React.KeyboardEvent) => { 34 | const textarea = e.currentTarget 35 | const { selectionStart, selectionEnd, value } = textarea 36 | 37 | // 按下 Enter 发送消息 38 | if (e.key === "Enter" && !e.shiftKey) { 39 | e.preventDefault() 40 | onSubmit() 41 | return 42 | } 43 | 44 | // 按下 Enter + Shift 换行 45 | if (e.key === "Enter" && e.shiftKey) { 46 | e.preventDefault() 47 | const before = value.slice(0, selectionStart) 48 | const after = value.slice(selectionEnd) 49 | onChange(before + "\n" + after) 50 | // 将光标移动到新行 51 | setTimeout(() => { 52 | textarea.setSelectionRange( 53 | selectionStart + 1, 54 | selectionStart + 1 55 | ) 56 | }, 0) 57 | return 58 | } 59 | 60 | // 处理代码块自动补全 61 | if (e.key === "`") { 62 | const beforeCursor = value.slice(0, selectionStart) 63 | const afterCursor = value.slice(selectionEnd) 64 | 65 | // 三个反引号 66 | if (beforeCursor.endsWith("``")) { 67 | e.preventDefault() 68 | const insertion = "```\n\n```" 69 | onChange( 70 | value.slice(0, selectionStart - 2) + insertion + afterCursor 71 | ) 72 | // 将光标放在代码块中间 73 | setTimeout(() => { 74 | textarea.setSelectionRange( 75 | selectionStart + 2, 76 | selectionStart + 2 77 | ) 78 | }, 0) 79 | return 80 | } 81 | 82 | // 单个反引号 83 | if (!beforeCursor.endsWith("`")) { 84 | e.preventDefault() 85 | const insertion = "``" 86 | onChange( 87 | value.slice(0, selectionStart) + insertion + afterCursor 88 | ) 89 | // 将光标放在反引号中间 90 | setTimeout(() => { 91 | textarea.setSelectionRange( 92 | selectionStart + 1, 93 | selectionStart + 1 94 | ) 95 | }, 0) 96 | } 97 | } 98 | } 99 | 100 | return ( 101 |
    102 |