├── README.md ├── app.py ├── main.ts └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Partyrock2api 2 | 3 | ### 对你有用的话麻烦给个stars谢谢 4 | 5 | ## 支持模型 6 | - claude-3-5-haiku 7 | - claude-3-5-sonnet 8 | - nova-lite-v1-0 9 | - nova-pro-v1-0 10 | - llama3-1-7b 11 | - llama3-1-70b 12 | - mistral-small 13 | - mistral-large 14 | 15 | ## 请求格式 16 | 和openai的请求格式相同支非流和流 17 | 18 | ## Docker部署 19 | ### 拉取 20 | ```bash 21 | docker pull mtxyt/partyrock-api:1.3 22 | ``` 23 | ### 运行 24 | ```bash 25 | docker run -d -p 8803:8803 mtxyt/partyrock-api:1.3 26 | ``` 27 | ## 获取请求key获取方式 28 | ### 准备步骤 29 | 访问:[partyrock](https://partyrock.aws "https://partyrock.aws") 30 | 点击Generate app按钮创建app 31 | ![image](https://github.com/user-attachments/assets/847748e6-896f-471d-8048-de3379cdbf70) 32 | 创建app后按f12打开开发者工具点击网络随便发起提问 33 | ### 1.在标头里拿到anti-csrftoken-a2z和cookie 34 | 找到对应请求 35 | ![屏幕截图 2025-01-26 204042](https://github.com/user-attachments/assets/e8c27ce9-0a0d-468c-89aa-8c61e64b990e) 36 | ### 2.在负载里拿到appid 37 | ![屏幕截图 2025-01-26 204209](https://github.com/user-attachments/assets/37c6707f-ad98-4cad-af35-b37d6c4d1ef7) 38 | 39 | ## 注 40 | 不支持大陆地区好像 41 | key的格式为appid|||anti-csrftoken-a2z|||cookie组合 42 | 如果你的appid=abab1,anti-csrftoken-a2z=132hdwqo,cookie=sdakvfjdijvdiv 43 | 那你的key就是abab1|||132hdwqo|||sdakvfjdijvdiv 44 | 45 | ## 更多 46 | 有位朋友提供了ts版本 47 | 大家自己部署只需要部署单个版本即可docker部署的是py版本的 48 | 49 | ## 声明 50 | 本项目仅供学术交流使用,请勿用于商业用途。 51 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, Response, stream_with_context 2 | import requests 3 | import json 4 | import uuid 5 | import time 6 | import os 7 | 8 | app = Flask(__name__) 9 | 10 | MODELS = { 11 | 'claude-3-5-haiku': 'bedrock-anthropic.claude-3-5-haiku', 12 | 'claude-3-5-sonnet': 'bedrock-anthropic.claude-3-5-sonnet-v2-0', 13 | 'nova-lite-v1-0': 'bedrock-amazon.nova-lite-v1-0', 14 | 'nova-pro-v1-0': 'bedrock-amazon.nova-pro-v1-0', 15 | 'llama3-1-7b': 'bedrock-meta.llama3-1-8b-instruct-v1', 16 | 'llama3-1-70b': 'bedrock-meta.llama3-1-70b-instruct-v1', 17 | 'mistral-small': 'bedrock-mistral.mistral-small-2402-v1-0', 18 | 'mistral-large': 'bedrock-mistral.mistral-large-2407-v1-0' 19 | } 20 | 21 | def validate_key(key): 22 | try: 23 | parts = key.split('|||', 3) 24 | if len(parts) != 3: 25 | return None, None, None 26 | return parts[0], parts[2], parts[1] 27 | except: 28 | return None, None, None 29 | 30 | def transform_messages(messages): 31 | return [ 32 | { 33 | "role": "user", 34 | "content": [{"text": f"Here is the system prompt to use: {msg['content']}"}] 35 | } if msg["role"] == "system" else { 36 | "role": msg["role"], 37 | "content": [{"text": msg["content"]}] 38 | } 39 | for msg in messages 40 | ] 41 | 42 | def create_partyrock_request(openai_req, app_id): 43 | return { 44 | "messages": transform_messages(openai_req['messages']), 45 | "modelName": MODELS.get(openai_req.get('model', 'claude-3-5-haiku')), 46 | "context": {"type": "chat-widget", "appId": app_id}, 47 | "options": {"temperature": 0}, 48 | "apiVersion": 3 49 | } 50 | 51 | @app.route('/', methods=['GET']) 52 | def home(): 53 | return {"status": "PartyRock API Service Running", "port": 8803} 54 | 55 | @app.route('/v1/chat/completions', methods=['POST']) 56 | def chat(): 57 | try: 58 | api_key = request.headers.get('Authorization', '').replace('Bearer ', '') 59 | app_id, cookie, csrf_token = validate_key(api_key) 60 | 61 | print(f"App ID: {app_id}") 62 | print(f"Cookie length: {len(cookie) if cookie else 'None'}") 63 | print(f"CSRF Token: {csrf_token}") 64 | 65 | if not app_id or not cookie or not csrf_token: 66 | return Response("Invalid API key format. Use: appId|||csrf_token|||cookies", status=401) 67 | 68 | headers = { 69 | 'accept': 'text/event-stream', 70 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', 71 | 'anti-csrftoken-a2z': csrf_token, 72 | 'content-type': 'application/json', 73 | 'origin': 'https://partyrock.aws', 74 | 'referer': f'https://partyrock.aws/u/chatyt/{app_id}', 75 | 'cookie': cookie, 76 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 77 | } 78 | 79 | response = requests.post( 80 | 'https://partyrock.aws/stream/getCompletion', 81 | headers=headers, 82 | json=create_partyrock_request(request.json, app_id), 83 | stream=True 84 | ) 85 | 86 | if response.status_code != 200: 87 | return Response(f"PartyRock API error: {response.text}", status=response.status_code) 88 | 89 | if not request.json.get('stream', False): 90 | try: 91 | full_content = "" 92 | buffer = "" 93 | for chunk in response.iter_content(chunk_size=1024, decode_unicode=True): 94 | if chunk: 95 | buffer += chunk 96 | while '\n' in buffer: 97 | line, buffer = buffer.split('\n', 1) 98 | if line.startswith('data: '): 99 | try: 100 | data = json.loads(line[6:]) 101 | if data["type"] == "text": 102 | content = data["text"] 103 | if isinstance(content, str): 104 | content = content.encode('latin1').decode('utf-8') 105 | if content and content != " ": 106 | full_content += content 107 | except: 108 | continue 109 | 110 | return { 111 | "id": str(uuid.uuid4()), 112 | "object": "chat.completion", 113 | "created": int(time.time()), 114 | "model": request.json.get('model', 'claude-3-haiku'), 115 | "choices": [{ 116 | "message": { 117 | "role": "assistant", 118 | "content": full_content 119 | }, 120 | "finish_reason": "stop", 121 | "index": 0 122 | }] 123 | } 124 | except Exception as e: 125 | print(f"Error processing response: {str(e)}") 126 | return Response("Failed to process response", status=500) 127 | 128 | def generate(): 129 | try: 130 | buffer = "" 131 | for chunk in response.iter_content(chunk_size=1024, decode_unicode=True): 132 | if chunk: 133 | buffer += chunk 134 | while '\n' in buffer: 135 | line, buffer = buffer.split('\n', 1) 136 | if line.startswith('data: '): 137 | try: 138 | data = json.loads(line[6:]) 139 | if data["type"] == "text": 140 | content = data["text"] 141 | if isinstance(content, str): 142 | content = content.encode('latin1').decode('utf-8') 143 | chunk_resp = { 144 | "id": str(uuid.uuid4()), 145 | "object": "chat.completion.chunk", 146 | "created": int(time.time()), 147 | "model": request.json.get('model', 'claude-3-haiku'), 148 | "choices": [{ 149 | "delta": {"content": content}, 150 | "index": 0, 151 | "finish_reason": None 152 | }] 153 | } 154 | yield f"data: {json.dumps(chunk_resp, ensure_ascii=False)}\n\n" 155 | except: 156 | continue 157 | 158 | yield f"data: {json.dumps({'choices':[{'delta':{'content':''},'index':0,'finish_reason':'stop'}]})}\n\n" 159 | yield "data: [DONE]\n\n" 160 | except Exception as e: 161 | print(f"Error in generate: {str(e)}") 162 | return 163 | 164 | return Response( 165 | stream_with_context(generate()), 166 | content_type='text/event-stream' 167 | ) 168 | 169 | except Exception as e: 170 | print(f"Error: {str(e)}") 171 | return Response(f"Internal server error: {str(e)}", status=500) 172 | 173 | if __name__ == '__main__': 174 | app.run(host='0.0.0.0', port=8803, debug=True) 175 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | // main.ts 2 | import { Hono } from 'https://deno.land/x/hono@v3.4.1/mod.ts' 3 | import { cors } from 'https://deno.land/x/hono@v3.4.1/middleware.ts' 4 | 5 | // ---------------------------------- 6 | // 1) SSE 辅助函数 streamSSE 的实现 7 | // ---------------------------------- 8 | /** 9 | * streamSSE(c, callback) 10 | * 11 | * 用来在 Hono 中处理 Server-Sent Events 的流式输出。 12 | * - `c` 是路由处理器中传入的上下文(Context) 13 | * - `callback` 是一个异步函数,拿到 `stream` 对象后,可以多次调用 `stream.writeSSE({ data, event, id })` 14 | * 来发送 SSE 消息(行首加上 "data:", "event:", "id:", 结尾多一个空行)。 15 | * - 最后 callback 结束时,会自动关闭可读流,客户端收到断流。 16 | */ 17 | type SSEWriter = { 18 | writeSSE: (msg: { data: string; event?: string; id?: string }) => Promise 19 | } 20 | 21 | async function streamSSE( 22 | c: any, // Hono 的 Context 类型,如果需要更严格可自行改成 c: Context 23 | callback: (stream: SSEWriter) => Promise 24 | ) { 25 | const textEncoder = new TextEncoder() 26 | 27 | // 创建一个可读流,用于往里 enqueue SSE 文本 28 | const body = new ReadableStream({ 29 | async start(controller) { 30 | const writer: SSEWriter = { 31 | // 向客户端推送一条 SSE 数据 32 | async writeSSE({ data, event, id }) { 33 | // 组装 SSE 消息。SSE 协议中,"data:" 行必需,"event:" 和 "id:" 可选 34 | let sseMessage = '' 35 | if (id) sseMessage += `id: ${id}\n` 36 | if (event) sseMessage += `event: ${event}\n` 37 | sseMessage += `data: ${data}\n\n` 38 | 39 | // 写入流 40 | controller.enqueue(textEncoder.encode(sseMessage)) 41 | }, 42 | } 43 | 44 | // 让调用方往 SSE 流里写数据 45 | await callback(writer) 46 | 47 | // 写完后关闭流 48 | controller.close() 49 | } 50 | }) 51 | 52 | // 返回一个新的响应,指定 content-type 为 text/event-stream 53 | return c.newResponse(body, { 54 | status: 200, 55 | headers: { 56 | 'Content-Type': 'text/event-stream; charset=utf-8', 57 | 'Cache-Control': 'no-cache', 58 | 'Connection': 'keep-alive', 59 | } 60 | }) 61 | } 62 | 63 | 64 | // ---------------------------------- 65 | // 2) 业务相关类型和函数 66 | // ---------------------------------- 67 | 68 | // 类型定义 69 | interface OpenAIMessage { 70 | role: string 71 | content: string 72 | } 73 | 74 | interface OpenAIRequest { 75 | messages: OpenAIMessage[] 76 | model?: string 77 | stream?: boolean 78 | } 79 | 80 | interface PartyRockMessage { 81 | role: string 82 | content: { text: string }[] 83 | } 84 | 85 | interface PartyRockRequest { 86 | messages: PartyRockMessage[] 87 | modelName: string 88 | context: { 89 | type: string 90 | appId: string 91 | } 92 | options: { 93 | temperature: number 94 | } 95 | apiVersion: number 96 | } 97 | 98 | // 模型映射表 99 | const MODELS: Record = { 100 | 'claude-3-5-haiku': 'bedrock-anthropic.claude-3-5-haiku', 101 | 'claude-3-5-sonnet': 'bedrock-anthropic.claude-3-5-sonnet-v2-0', 102 | 'nova-lite-v1-0': 'bedrock-amazon.nova-lite-v1-0', 103 | 'nova-pro-v1-0': 'bedrock-amazon.nova-pro-v1-0', 104 | 'llama3-1-7b': 'bedrock-meta.llama3-1-8b-instruct-v1', 105 | 'llama3-1-70b': 'bedrock-meta.llama3-1-70b-instruct-v1', 106 | 'mistral-small': 'bedrock-mistral.mistral-small-2402-v1-0', 107 | 'mistral-large': 'bedrock-mistral.mistral-large-2407-v1-0' 108 | } 109 | 110 | 111 | // ---------------------------------- 112 | // 3) Base64 Key 解析 113 | // ---------------------------------- 114 | /** 115 | * 接收形如 "appId|||csrfToken|||cookie" 的字符串的 Base64 编码。 116 | * 解码后 split('|||') 得到 3 段,再返回 [appId, cookie, csrfToken]。 117 | */ 118 | function validateBase64Key(encodedKey: string): [string | null, string | null, string | null] { 119 | try { 120 | const decoded = atob(encodedKey) // Deno 环境自带 atob 121 | // decoded 应是 "appId|||csrfToken|||cookie" 122 | const parts = decoded.split('|||', 3) 123 | if (parts.length !== 3) { 124 | return [null, null, null] 125 | } 126 | // 这里与原先代码对应: [appId, cookie, csrfToken] 127 | return [parts[0], parts[2], parts[1]] 128 | } catch { 129 | return [null, null, null] 130 | } 131 | } 132 | 133 | 134 | // ---------------------------------- 135 | // 4) 生成 PartyRock 的请求体 136 | // ---------------------------------- 137 | function createPartyRockRequest(openaiReq: OpenAIRequest, appId: string): PartyRockRequest { 138 | return { 139 | messages: openaiReq.messages.map(msg => ({ 140 | role: msg.role, 141 | content: [{ text: msg.content }] 142 | })), 143 | modelName: MODELS[openaiReq.model || 'claude-3-5-haiku'], 144 | context: { type: 'chat-widget', appId }, 145 | options: { temperature: 0 }, 146 | apiVersion: 3 147 | } 148 | } 149 | 150 | 151 | // ---------------------------------- 152 | // 5) Hono 应用 153 | // ---------------------------------- 154 | const app = new Hono() 155 | 156 | // 使用 CORS 中间件 157 | app.use('/*', cors()) 158 | 159 | // 测试用 GET 路由 160 | app.get('/', (c) => { 161 | return c.json({ 162 | status: 'PartyRock API Service Running', 163 | port: 8803 164 | }) 165 | }) 166 | 167 | 168 | // 主要的 POST 路由 169 | app.post('/v1/chat/completions', async (c) => { 170 | try { 171 | // 读取 Header: Authorization: Bearer 172 | const authorization = c.req.header('Authorization') || '' 173 | const token = authorization.replace('Bearer ', '') 174 | 175 | // 解码 + 拆分 176 | const [appId, cookie, csrfToken] = validateBase64Key(token) 177 | 178 | console.log(`App ID: ${appId}`) 179 | console.log(`Cookie length: ${cookie ? cookie.length : 'None'}`) 180 | console.log(`CSRF Token: ${csrfToken}`) 181 | 182 | // 如果没正确解析出三段 183 | if (!appId || !cookie || !csrfToken) { 184 | return c.text( 185 | 'Invalid Base64 or key format. Expecting "appId|||csrfToken|||cookie".', 186 | 401 187 | ) 188 | } 189 | 190 | // 拿到请求体 191 | const body = await c.req.json() 192 | 193 | // 组装 fetch PartyRock 时需要的请求头 194 | const headers = { 195 | 'accept': 'text/event-stream', 196 | 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8', 197 | 'anti-csrftoken-a2z': csrfToken, 198 | 'content-type': 'application/json', 199 | 'origin': 'https://partyrock.aws', 200 | 'referer': `https://partyrock.aws/u/chatyt/${appId}`, 201 | 'cookie': cookie, 202 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' 203 | } 204 | 205 | // 请求 PartyRock 206 | const response = await fetch('https://partyrock.aws/stream/getCompletion', { 207 | method: 'POST', 208 | headers, 209 | body: JSON.stringify(createPartyRockRequest(body, appId)) 210 | }) 211 | 212 | // PartyRock 不成功 213 | if (!response.ok) { 214 | const errorMsg = await response.text() 215 | return c.text(`PartyRock API error: ${errorMsg}`, response.status) 216 | } 217 | 218 | // 如果不需要流式输出,就把服务器返回的 SSE 内容读取完后合并成一次性响应 219 | if (!body.stream) { 220 | let fullContent = '' 221 | const reader = response.body?.getReader() 222 | if (!reader) { 223 | return c.text('No response body from PartyRock API.', 500) 224 | } 225 | 226 | const decoder = new TextDecoder() 227 | while (true) { 228 | const { done, value } = await reader.read() 229 | if (done) break 230 | const chunk = decoder.decode(value) 231 | const lines = chunk.split('\n') 232 | for (const line of lines) { 233 | if (line.startsWith('data: ')) { 234 | try { 235 | const data = JSON.parse(line.slice(6)) 236 | if (data.type === 'text') { 237 | fullContent += data.text 238 | } 239 | } catch { 240 | continue 241 | } 242 | } 243 | } 244 | } 245 | 246 | return c.json({ 247 | id: crypto.randomUUID(), 248 | object: 'chat.completion', 249 | created: Date.now(), 250 | model: body.model || 'claude-3-5-haiku', 251 | choices: [{ 252 | message: { role: 'assistant', content: fullContent }, 253 | finish_reason: 'stop', 254 | index: 0 255 | }] 256 | }) 257 | } 258 | 259 | // ------------------------- 260 | // 需要流式输出 (SSE) 261 | // ------------------------- 262 | return streamSSE(c, async (stream) => { 263 | const reader = response.body?.getReader() 264 | if (!reader) { 265 | await stream.writeSSE({ data: 'No response body from PartyRock API.' }) 266 | return 267 | } 268 | 269 | const decoder = new TextDecoder() 270 | 271 | try { 272 | // 循环读取 PartyRock 返回的 SSE 数据 273 | while (true) { 274 | const { done, value } = await reader.read() 275 | if (done) break 276 | 277 | const chunk = decoder.decode(value) 278 | const lines = chunk.split('\n') 279 | 280 | for (const line of lines) { 281 | if (line.startsWith('data: ')) { 282 | try { 283 | const data = JSON.parse(line.slice(6)) 284 | if (data.type === 'text') { 285 | // 构造 OpenAI 兼容的 SSE chunk 286 | const chunkResp = { 287 | id: crypto.randomUUID(), 288 | object: 'chat.completion.chunk', 289 | created: Date.now(), 290 | model: body.model || 'claude-3-5-haiku', 291 | choices: [{ 292 | delta: { content: data.text }, 293 | index: 0, 294 | finish_reason: null 295 | }] 296 | } 297 | // 写到 SSE 流 298 | await stream.writeSSE({ 299 | data: JSON.stringify(chunkResp) 300 | }) 301 | } 302 | } catch { 303 | continue 304 | } 305 | } 306 | } 307 | } 308 | 309 | // 最后写一次空的数据表示结束 310 | await stream.writeSSE({ 311 | data: JSON.stringify({ 312 | choices: [{ 313 | delta: { content: '' }, 314 | index: 0, 315 | finish_reason: 'stop' 316 | }] 317 | }) 318 | }) 319 | await stream.writeSSE({ data: '[DONE]' }) 320 | 321 | } catch (e) { 322 | console.error('Error in SSE stream:', e) 323 | } 324 | }) 325 | 326 | } catch (e) { 327 | console.error(`Error: ${e}`) 328 | return c.text(`Internal server error: ${e}`, 500) 329 | } 330 | }) 331 | 332 | // 启动 Deno 服务 333 | Deno.serve({ port: 8803 }, app.fetch) 334 | console.log('Server running on http://localhost:8803') 335 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | requests --------------------------------------------------------------------------------