kailashhh commited on
Commit
2112adc
·
1 Parent(s): e91b865
Files changed (7) hide show
  1. config.json +0 -0
  2. docker-compose.yml +13 -0
  3. dockerfile +18 -0
  4. main.py +150 -0
  5. requirements.txt +6 -0
  6. server.py +41 -0
  7. utils.py +13 -0
config.json ADDED
File without changes
docker-compose.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3'
2
+
3
+ services:
4
+ reverse-kimi:
5
+ container_name: reverse-kimi
6
+ image: yunpengtai/reverse-kimi:latest
7
+ restart: always
8
+ ports:
9
+ - "6867:6867"
10
+ volumes:
11
+ - ./config.json:/app/config.json
12
+ environment:
13
+ - TZ=Asia/Shanghai
dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方 Python 运行时作为父镜像
2
+ FROM python:3.9-slim
3
+
4
+ # 设置工作目录为 /app
5
+ WORKDIR /app
6
+
7
+ # 将当前目录内容复制到位于 /app 的容器中
8
+ COPY . /app
9
+
10
+ # 设置环境变量
11
+ ENV PYTHONUNBUFFERED=1
12
+
13
+ RUN pip3 install --upgrade pip
14
+
15
+ RUN pip3 install --no-cache-dir --upgrade -r requirements.txt
16
+
17
+ # 在容器启动时先运行 main.py,然后运行 server.py
18
+ CMD ["sh", "-c", "python3 main.py & python3 server.py"]
main.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ import re
4
+
5
+ import requests
6
+
7
+ from utils import load_config, write_tokens
8
+
9
+
10
+ def get_chat_url(
11
+ chat_id): return f'https://kimi.moonshot.cn/api/chat/{chat_id}/completion/stream'
12
+
13
+
14
+ REFRESH_INTERVAL = 5 * 60
15
+ REFRESH_URL = 'https://kimi.moonshot.cn/api/auth/token/refresh'
16
+ LIST_URL = 'https://kimi.moonshot.cn/api/chat/list'
17
+ NEW_CHAT_URL = 'https://kimi.moonshot.cn/api/chat'
18
+
19
+ HEADERS = {
20
+ 'user-agent':
21
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36',
22
+ 'Content-Type': 'application/json',
23
+ 'Referer': 'https://kimi.moonshot.cn',
24
+ 'Origin': 'https://kimi.moonshot.cn',
25
+ }
26
+
27
+
28
+ # list 和 create 头部都是 auth_token 来鉴权
29
+ def list_conversations():
30
+ response = requests.post(LIST_URL, headers=HEADERS, json={}).json()
31
+ return response['items']
32
+
33
+
34
+ def create_conversation(name):
35
+ response = requests.post(NEW_CHAT_URL,
36
+ headers=HEADERS,
37
+ json={'name': name}).json()
38
+ return response['id']
39
+
40
+
41
+ def format_url(text):
42
+ url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
43
+ urls = re.findall(url_pattern, text)
44
+ for url in urls:
45
+ new_url = f'<url id="" type="url" status="" title="" wc="">{url}</url>'
46
+ text = text.replace(url, new_url)
47
+ return text
48
+
49
+
50
+ def process_msg(messages):
51
+ concat_content = ''
52
+ for msg in messages:
53
+ role, content = msg['role'], msg['content']
54
+ single_msg = f'{role}: {content}\n\n'
55
+ concat_content += single_msg
56
+ concat_content = format_url(concat_content)
57
+ return [{'content': concat_content, 'role': 'user'}]
58
+
59
+
60
+ async def get_reply(messages):
61
+ HEADERS['Authorization'] = load_config()['auth_token']
62
+ chat_id = create_conversation('新的聊天')
63
+ chat_url = get_chat_url(chat_id)
64
+ # 添加文件,需要在 Kimi 官方上传好
65
+ # messages.append({'content': 'https://prod-chat-kimi.tos-s3-cn-beijing.volces.com/prod-chat-kimi/ckiuir33aesg978thq90/2024-03-03/cnhtnlsudu6ec6vquo9g/3e4545519d5a8a56256980d9fda4f2de_720w_thumbnail.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKLTYTJlNjgwMjY2ZDBkNDFiYmI5YWNiZDBlZmFmYjIzZTA%2F20240303%2Fcn-beijing%2Fs3%2Faws4_request&X-Amz-Date=20240303T020928Z&X-Amz-Expires=518400&X-Amz-SignedHeaders=host&X-Amz-Signature=f9c5dc63e55256871a54f453bc23efde8a9a78078cd7e778cf4dad86987e0a4b', 'type': 'file'})
66
+ concat_msg = process_msg(messages)
67
+ response = requests.post(chat_url,
68
+ headers=HEADERS,
69
+ json={'messages': concat_msg})
70
+ status_code = response.status_code
71
+ if status_code != 200:
72
+ print(status_code)
73
+ print(response.content.decode('utf-8'))
74
+ else:
75
+ search_content = '\n\n参考:\n'
76
+ url = None
77
+ link_count = 1
78
+ for r in response.iter_lines():
79
+ if r:
80
+ decoded = r.decode('utf-8')
81
+ if decoded.startswith('data:'):
82
+ json_data = json.loads(decoded[len('data: '):])
83
+ if json_data['event'] == 'cmpl':
84
+ answer = json_data['text']
85
+ yield json.dumps(
86
+ {
87
+ 'object': 'chat.completion.chunk',
88
+ 'model': 'moonshot-v1',
89
+ 'choices': [
90
+ {
91
+ 'delta': {'content': answer},
92
+ 'index': 0,
93
+ 'finish_reason': None,
94
+ }
95
+ ],
96
+ }
97
+ )
98
+
99
+ elif json_data['event'] == 'search_plus':
100
+ msg = json_data['msg']
101
+ if 'url' in msg:
102
+ title, url = msg['title'], msg['url']
103
+ search_content += f'{link_count}. [{title}]({url})\n'
104
+ link_count += 1
105
+ if url is not None:
106
+ yield json.dumps(
107
+ {
108
+ 'object': 'chat.completion.chunk',
109
+ 'model': 'moonshot-v1',
110
+ 'choices': [
111
+ {
112
+ 'delta': {'content': search_content},
113
+ 'index': 0,
114
+ 'finish_reason': 'search',
115
+ }
116
+ ],
117
+ }
118
+ )
119
+
120
+ yield json.dumps(
121
+ {
122
+ 'object': 'chat.completion.chunk',
123
+ 'model': 'moonshot-v1',
124
+ 'choices': [
125
+ {
126
+ 'delta': {},
127
+ 'index': 0,
128
+ 'finish_reason': 'stop',
129
+ }
130
+ ],
131
+ }
132
+ )
133
+
134
+
135
+ def refresh():
136
+ global refresh_token
137
+ refresh_token = load_config()['refresh_token']
138
+ HEADERS['Authorization'] = refresh_token
139
+ response = requests.get(REFRESH_URL, headers=HEADERS).json()
140
+ auth_token, refresh_token = list(response.values())
141
+ refresh_token = f'Bearer {refresh_token}'
142
+ auth_token = f'Bearer {auth_token}'
143
+ write_tokens(auth_token, refresh_token)
144
+
145
+
146
+ if __name__ == '__main__':
147
+ while True:
148
+ refresh()
149
+ print('Refresh......')
150
+ time.sleep(REFRESH_INTERVAL)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.110.1
2
+ pydantic==2.7.0
3
+ Requests==2.31.0
4
+ retry==0.9.2
5
+ sse_starlette==2.1.0
6
+ uvicorn==0.29.0
server.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Depends, FastAPI, HTTPException, status
2
+ from fastapi.security import OAuth2PasswordBearer
3
+ from pydantic import BaseModel
4
+ from sse_starlette.sse import EventSourceResponse
5
+
6
+ from main import get_reply
7
+ from utils import load_config
8
+
9
+ app = FastAPI()
10
+ TOKEN = load_config()['token']
11
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token')
12
+
13
+
14
+ class Request(BaseModel):
15
+ messages: list
16
+ model: str = None
17
+ stream: bool = None
18
+ temperature: float = None
19
+ presence_penalty: float = None
20
+ frequency_penalty: float = None
21
+ top_p: int = None
22
+ max_tokens: int = None
23
+
24
+
25
+ @app.post('/v1/chat/completions')
26
+ async def answer(request: Request, token: str = Depends(oauth2_scheme)):
27
+ if token != TOKEN:
28
+ print(f'请求的 token: {token}')
29
+ print(f'设置的 token: {TOKEN}')
30
+ raise HTTPException(
31
+ status_code=status.HTTP_401_UNAUTHORIZED,
32
+ detail='Invalid authentication credentials',
33
+ headers={'WWW-Authenticate': 'Bearer'},
34
+ )
35
+ # .model_dump() 相当于 .dict()
36
+ messages = request.model_dump()['messages']
37
+ return EventSourceResponse(get_reply(messages))
38
+
39
+ if __name__ == '__main__':
40
+ import uvicorn
41
+ uvicorn.run(app, host='0.0.0.0', port=6867)
utils.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+
4
+ def load_config():
5
+ return json.load(open('config.json', encoding='utf-8'))
6
+
7
+
8
+ def write_tokens(auth_token, refresh_token):
9
+ cfg = load_config()
10
+ cfg['auth_token'] = auth_token
11
+ cfg['refresh_token'] = refresh_token
12
+ with open('config.json', 'w', encoding='utf-8') as w:
13
+ w.write(json.dumps(cfg))