diff --git a/README.md b/README.md index 0f04575..43b941d 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,3 @@ If you find this project useful, please consider [buying me a cup of coffee](htt 您的电脑上必须安装有 Google Chrome 浏览器才能够使用当前分支。旧版已不再维护。 Google Chrome must be installed on your system in order to use this proxy. Previous versions are no longer maintained. - -## Limitations 限制 - -Only Anthropic API format is supported. Change the model name to use different models (default claude_3_opus) - -只支持A社的API格式,默认模型为 claude_3_opus。 diff --git a/index.mjs b/index.mjs index 1fb0525..834ae67 100644 --- a/index.mjs +++ b/index.mjs @@ -28,6 +28,9 @@ const modelMappping = { "claude-3-haiku-20240307": "claude_3_haiku", "claude-2.1": "claude_2", "claude-2.0": "claude_2", + "gpt-4": "gpt_4", + "gpt-4o": "gpt_4o", + "gpt-4-turbo": "gpt_4_turbo", }; // import config.mjs @@ -50,7 +53,7 @@ app.options("/v1/messages", (req, res) => { res.status(200).end(); }); // openai format model request -app.get("/v1/models", apiKeyAuth, (req, res) => { +app.get("/v1/models", OpenAIApiKeyAuth, (req, res) => { res.setHeader("Content-Type", "application/json"); res.setHeader("Access-Control-Allow-Origin", "*"); let models = availableModels.map((model, index) => { @@ -64,7 +67,8 @@ app.get("/v1/models", apiKeyAuth, (req, res) => { }); res.json({ object: "list", data: models }); }); -app.post("/v1/messages", apiKeyAuth, (req, res) => { +// handle openai format model request +app.post("/v1/chat/completions", OpenAIApiKeyAuth, (req, res) => { req.rawBody = ""; req.setEncoding("utf8"); @@ -73,6 +77,188 @@ app.post("/v1/messages", apiKeyAuth, (req, res) => { }); req.on("end", async () => { + console.log("Handling request of OpenAI format"); + res.setHeader("Content-Type", "text/event-stream;charset=utf-8"); + res.setHeader("Access-Control-Allow-Origin", "*"); + let jsonBody = JSON.parse(req.rawBody); + + console.log("message length:" + jsonBody.messages.length); + // decide which session to use randomly + var randomSession = Object.keys(provider.sessions)[Math.floor(Math.random() * Object.keys(provider.sessions).length)]; + console.log("Using session " + randomSession); + // call provider to get completion + + // try to map model + if (jsonBody.model && modelMappping[jsonBody.model]) { + jsonBody.model = modelMappping[jsonBody.model]; + } + if (jsonBody.model && !availableModels.includes(jsonBody.model)) { + res.json({ error: { code: 404, message: "Invalid Model" } }); + return; + } + console.log("Using model " + jsonBody.model); + + // call provider to get completion + await provider + .getCompletion(randomSession, jsonBody.messages, jsonBody.stream ? true : false, jsonBody.model, process.env.USE_CUSTOM_MODE == "true" ? true : false) + .then(({ completion, cancel }) => { + completion.on("start", (id) => { + if (jsonBody.stream) { + // send message start + res.write(createEvent(":", "queue heartbeat 114514")); + res.write( + createEvent("data", { + id: msgid, + object: "chat.completion.chunk", + created: Math.floor(new Date().getTime() / 1000), + model: jsonBody.model, + system_fingerprint: "114514", + choices: [{ index: 0, delta: { role: "assistant", content: "" }, logprobs: null, finish_reason: null }], + }) + ); + } + }); + + completion.on("completion", (id, text) => { + if (jsonBody.stream) { + // send message delta + if (jsonBody.stream) { + res.write( + createEvent("data", { + choices: [ + { + content_filter_results: { + hate: { filtered: false, severity: "safe" }, + self_harm: { filtered: false, severity: "safe" }, + sexual: { filtered: false, severity: "safe" }, + violence: { filtered: false, severity: "safe" }, + }, + delta: { content: text }, + finish_reason: null, + index: 0, + }, + ], + created: Math.floor(new Date().getTime() / 1000), + id: id, + model: jsonBody.model, + object: "chat.completion.chunk", + system_fingerprint: "114514", + }) + ); + } + } else { + // 只会发一次,发送final response + res.write( + JSON.stringify({ + id: id, + object: "chat.completion", + created: Math.floor(new Date().getTime() / 1000), + model: jsonBody.model, + system_fingerprint: "114514", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: text, + }, + logprobs: null, + finish_reason: "stop", + }, + ], + usage: { + prompt_tokens: 1, + completion_tokens: 1, + total_tokens: 1, + }, + }) + ); + res.end(); + } + }); + + completion.on("end", () => { + if (jsonBody.stream) { + res.write(createEvent("data", "[DONE]")); + res.end(); + } + }); + + res.on("close", () => { + console.log(" > [Client closed]"); + completion.removeAllListeners(); + cancel(); + }); + }) + .catch((error) => { + console.error(error); + if (jsonBody.stream) { + res.write( + createEvent("data", { + choices: [ + { + content_filter_results: { + hate: { filtered: false, severity: "safe" }, + self_harm: { filtered: false, severity: "safe" }, + sexual: { filtered: false, severity: "safe" }, + violence: { filtered: false, severity: "safe" }, + }, + delta: { content: "Error occurred, please check the log.\n\n出现错误,请检查日志:
" + error.stack || error + "" }, + finish_reason: null, + index: 0, + }, + ], + created: Math.floor(new Date().getTime() / 1000), + id: uuidv4(), + model: jsonBody.model, + object: "chat.completion.chunk", + system_fingerprint: "114514", + }) + ); + res.end(); + } else { + res.write( + JSON.stringify({ + id: uuidv4(), + object: "chat.completion", + created: Math.floor(new Date().getTime() / 1000), + model: jsonBody.model, + system_fingerprint: "114514", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: "Error occurred, please check the log.\n\n出现错误,请检查日志:
" + error.stack || error + "", + }, + logprobs: null, + finish_reason: "stop", + }, + ], + usage: { + prompt_tokens: 1, + completion_tokens: 1, + total_tokens: 1, + }, + }) + ); + res.end(); + } + return; + }); + }); +}); +// handle anthropic format model request +app.post("/v1/messages", AnthropicApiKeyAuth, (req, res) => { + req.rawBody = ""; + req.setEncoding("utf8"); + + req.on("data", function (chunk) { + req.rawBody += chunk; + }); + + req.on("end", async () => { + console.log("Handling request of Anthropic format"); res.setHeader("Content-Type", "text/event-stream;charset=utf-8"); res.setHeader("Access-Control-Allow-Origin", "*"); let jsonBody = JSON.parse(req.rawBody); @@ -99,7 +285,7 @@ app.post("/v1/messages", apiKeyAuth, (req, res) => { // call provider to get completion await provider .getCompletion(randomSession, jsonBody.messages, jsonBody.stream ? true : false, proxyModel, process.env.USE_CUSTOM_MODE == "true" ? true : false) - .then(({completion, cancel}) => { + .then(({ completion, cancel }) => { completion.on("start", (id) => { if (jsonBody.stream) { // send message start @@ -191,17 +377,20 @@ app.post("/v1/messages", apiKeyAuth, (req, res) => { createEvent("content_block_delta", { type: "content_block_delta", index: 0, - delta: { type: "text_delta", text: "出现错误,请检查日志:
" + error + ""}, + delta: { + type: "text_delta", + text: "Error occurred, please check the log.\n\n出现错误,请检查日志:
" + error.stack || error + "", + }, }) ); res.end(); } else { res.write( JSON.stringify({ - id: id, + id: uuidv4(), content: [ { - text: "出现错误,请检查日志:
" + error + "" + text: "Error occurred, please check the log.\n\n出现错误,请检查日志:
" + error.stack || error + "", }, { id: "string", @@ -235,10 +424,10 @@ app.listen(port, () => { if (!validApiKey) { console.log(`Proxy is currently running with no authentication`); } - console.log(`API Format: Anthropic; Custom mode: ${process.env.USE_CUSTOM_MODE == "true" ? "enabled" : "disabled"}`); + console.log(`Custom mode: ${process.env.USE_CUSTOM_MODE == "true" ? "enabled" : "disabled"}`); }); -function apiKeyAuth(req, res, next) { +function AnthropicApiKeyAuth(req, res, next) { const reqApiKey = req.header("x-api-key"); if (validApiKey && reqApiKey !== validApiKey) { @@ -250,3 +439,16 @@ function apiKeyAuth(req, res, next) { next(); } + +function OpenAIApiKeyAuth(req, res, next) { + const reqApiKey = req.header("Authorization"); + + if (validApiKey && reqApiKey !== "Bearer " + validApiKey) { + // If Environment variable PASSWORD is set AND Authorization header is not equal to it, return 401 + const clientIpAddress = req.headers["x-forwarded-for"] || req.ip; + console.log(`Receviced Request from IP ${clientIpAddress} but got invalid password.`); + return res.status(401).json({ error: { code: 403, message: "Invalid Password" } }); + } + + next(); +} diff --git a/usage.md b/usage.md index 15a8687..3178c49 100644 --- a/usage.md +++ b/usage.md @@ -1,12 +1,17 @@ + # 使用方法 -0. 安装 Node.JS +0. 安装 Node.JS 和 Git 1. 获得一个 YOU.COM 账户并且订阅,登录。 -2. 打开 F12(DevTools),找到 “Network(网络)”、刷新一下页面,找到“you.com”这个项目(或者“instrumentation”这个项目) +2. 打开 F12(DevTools),找到 Console -3. 点进去,往下滑找到 "Cookie",完整的复制后面的内容。 +3. 在 > 后面粘贴以下代码并回车。然后复制所有内容(Cookie)。 + +```javascript +prompt("请Ctrl+C复制以下所有内容(Cookie)",document.cookie) +``` 4. 下载或Clone本项目代码,解压 @@ -30,29 +35,31 @@ export const config = { 7. (可选)如果需要,您可以仿照第6步在`start.bat`中设定一个名为 "PASSWORD" 的环境变量,并将其用作密码。如果没有定义该环境变量,程序将接受所有传入的请求,而不进行任何身份验证 - (可选)如果需要,可以设置代理。 - - (可选)如果需要,可以修改使用的模型。但是仍然建议使用opus,因为其他未经测试。 + (可选)如果需要,可以设置代理,请参考下文。 (可选)如果需要,可以启用自定义会话模式(`USE_CUSTOM_MODE`设置为`true`)。可以缩短原系统消息长度、禁用联网、减缓等待时间,可能有助于破限。但有可能导致更容易出现 unusual query volume。 8. 启动 start.bat -9. 酒馆中选择 Claude,反向代理地址填 http://127.0.0.1:8080/v1 **反代密码必须填, 同时打开流式传输**,随便什么都可以(除非你在第7步设置了PASSWORD)。 +9. 酒馆中选择 **Custom (OpenAI-compatible)**,反向代理地址填 http://127.0.0.1:8080/v1 **反代密码必须填**,随便什么都可以(除非你在第7步设置了PASSWORD)。 10. 开始使用。如果失败了/没有结果/403/Warning 就多重试几次。 # 使用代理 -可以使用本地的socks5或http(s)代理。只需在 start.bat 中设置 `https_proxy` 环境变量。 +如果用户在中国大陆,可能会遇到访问不稳定的情况。从npm安装依赖和从 git 拉取代码也需要稳定的网络连接。这些操作遇到问题的时候应该首先考虑挂全局代理。 + +对于项目本身,可以使用本地的socks5或http(s)代理。只需在 start.bat 中设置 `https_proxy` 环境变量。 比如,如要使用 Clash 的默认本地代理,则应设置为 `set https_proxy=http://127.0.0.1:7890` 欲知详细代理格式,请查阅 https://www.npmjs.com/package/proxy-from-env -# VPS 部署 +# 操作系统部署 -请使用项目中的 Dockerfile,不建议使用其他方式部署。 +对于Linux,请使用项目中的 Dockerfile,但总的来说还不是很稳定。如果你会自己配置 X11、桌面环境,那就最好,否则建议还是用本地Windows部署。 + +对于 Android 下的Termux环境,暂时没有适配Android Chrome WebDriver的计划所以不支持。当用户询问手机使用相关问题时,请告诉他们用puppeteer的新版本不支持,如果是旧版本,可以尽力为他们提供帮助但是需要说明旧版本已经不再更新。 # 修改模型 @@ -76,9 +83,33 @@ command_r_plus zephyr ``` -## 注意事项 +# 注意事项 -如果浏览器弹出人机验证窗口,请在30秒内完成。 +## 不同分支的问题 +项目目前有三个分支,可以用git checkout <分支> 的指令相互切换: +- bypass-cf 分支,最新版本(维护中),API格式为任意格式(两种格式都支持)。使用puppeteer模拟浏览器访问。 +- anthropic 分支,旧版本(不再维护),API格式为Anthropic格式。直接请求API,容易被CloudFlare识别为机器人而拦截。 +- openai 分支,旧版本(不再维护),API格式为OpenAI格式。直接请求API,容易被CloudFlare识别为机器人而拦截。 + +在诊断用户问题的时候,注意观察或者询问用户用的是哪个分支,不同API格式不能通用。酒馆提示 Not Found,可能就是因为选错了对应的API格式。 + +## 关于403问题的原理(基本只存在于旧版本) + +cloudflare有一个风控分数。这个和你的TLS指纹、浏览器指纹、IP地址声誉等等有关系 +我们这个项目一直用的TLS指纹和浏览器指纹就非常可疑(都是自动化库和Node内置TLS),分数直接拉满 +相当于已经预先有了30+30分数,剩下就看IP地址声誉(40分)你拿了几分 +(具体分数不详,只是举个例子) +那如果你IP确实白,拿了0分,那你总共分数就是60。 +假设you那边设置了分数高于80的要跳验证码,那现在就没事 +如果你IP黑,拿了超过20分,那你就是>80分,你就要跳验证码,结果就是403 +然后最近you觉得被薅狠了,或者别的啥原因,把这个分数设置成60以上的就要跳验证码 +结果就我IP有点黑,不管怎么搞都过不去了。 +但是同样的IP,你用正常的Google Chrome访问,就没问题,因为它的指纹非常干净,所以前面的指纹分数就很低 +就算加上IP声誉分他也没到那条线 +总之以上是一个简化的版本,CF抗bot还有很多指标、很多策略 +这个问题基本只存在于旧版本,新版本由于使用了浏览器模拟访问,已经不容易被拦截。 + +新版本如果弹出人机验证提示,用户只需要在30秒内点击完成CloudFlare的人机验证,并且等待程序继续处理即可。 # Usage @@ -115,13 +146,11 @@ export const config = { (Optional) You can set the proxy in start.bat. See below. - (Optional) You may change the model to use, but only claude_3_opus is tested. - (Optional) You may turn on the custom chat mode by setting `USE_CUSTOM_MODE` env to `true` 8. Start start.bat -9. Select Claude in the Tavern and put http://127.0.0.1:8080/v1 as the address of the reverse proxy. **Use any random string for password, also turn on Streaming** (unless you set PASSWORD in step 7). +9. Select **Custom (OpenAI-compatible)** in the SillyTavern and ues http://127.0.0.1:8080/v1 as the endpoint of the reverse proxy. **Use any random string for password** (unless you set PASSWORD in step 7). 10. Enjoy it. If it fails/no result/403/Warning, try again. @@ -133,30 +162,6 @@ Use the `https_proxy` env to set custom proxy. Refer to https://www.npmjs.com/pa Docker is highly recommended, please use the Dockerfile. -# Change model - -Change `AI_MODEL` env to switch between models. - -Supported model names (refer to you.com website for latest models): - -``` -gpt_4o -gpt_4_turbo -gpt_4 -claude_3_5_sonnet -claude_3_opus -claude_3_sonnet -claude_3_haiku -claude_2 -llama3 -gemini_pro -gemini_1_5_pro -databricks_dbrx_instruct -command_r -command_r_plus -zephyr -``` - ## Caution If you get a CloudFlare Challenge, solve it in 30 seconds.