choging commited on
Commit
611c243
·
verified ·
1 Parent(s): 0ac29d7

Create pipes/hix_ai_pipe.py

Browse files
Files changed (1) hide show
  1. pipes/hix_ai_pipe.py +351 -0
pipes/hix_ai_pipe.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ title: DeepSeek R1 From Hix
3
+ author: Jason
4
+ description: Hix AI Chat Pipe with multi-turn conversation, storing chatId->botId in a local file.
5
+ version: 1.0.0
6
+ licence: MIT
7
+ """
8
+ import datetime
9
+ import os
10
+ import json
11
+ import random
12
+ import uuid
13
+ import hashlib
14
+ import re
15
+ import asyncio
16
+ from typing import AsyncGenerator, Callable, Awaitable, Optional
17
+
18
+ import cloudscraper
19
+ from pydantic import BaseModel, Field
20
+
21
+ # 用于从 assistant 消息中提取 chatId
22
+ CHAT_ID_REGEX = re.compile(r"Chat ID:\s*([a-zA-Z0-9_-]+)")
23
+
24
+
25
+ class Pipe:
26
+ class Valves(BaseModel):
27
+ API_DOMAIN: str = Field(default="https://hix.ai", description="API Domain")
28
+
29
+ def __init__(self):
30
+ # 1) 配置 & 常量
31
+ self.valves = self.Valves()
32
+ self.f = "nvKTEonFAll-in-One AI Writing CopilotwNLf2plwvtlcCxam"
33
+ self.salt = "xJ7fTJBgQ55/9r|"
34
+ self.sse_prefix = "data: "
35
+
36
+ # 2) 会话属性 (同进程多轮)
37
+ self.scraper = None
38
+ self.device_id = None
39
+ self.device_number = None
40
+ self.csrf_token = None
41
+ self.logged_in = False # 是否已匿名登录
42
+ # 英文/中文词汇示例,可根据需要自行扩充
43
+ self.ENGLISH_WORDS = [
44
+ "Alpha", "Beta", "Gamma", "Deep", "Seek", "Magic", "Galaxy",
45
+ "Hello", "Future", "Project", "Vision", "Global", "Spark",
46
+ "Harmony", "Creative", "Insight", "Innovate", "Design", "Space",
47
+ ]
48
+
49
+ self.CHINESE_WORDS = [
50
+ "世界", "探索", "未来", "灵感", "空间", "科技", "思考",
51
+ "聚合", "演示", "测试", "案例", "交互", "创造", "变革",
52
+ "创新", "设计", "实验", "方案", "方向", "策略", "策划"
53
+ ]
54
+ # 3) 多子模型列表
55
+ self.sub_pipes = [
56
+ {"id": "73", "bot_id": 73, "name": "HIX Chat"},
57
+ {"id": "85426", "bot_id": 85426, "name": "DeepSeek-R1"},
58
+ # {"id": "85427", "bot_id": 85427, "name": "DeepSeek-V3"},
59
+ # {"id": "85422", "bot_id": 85422, "name": "Claude 3.5 Haiku"},
60
+ # {"id": "1181", "bot_id": 1181, "name": "OpenAI o1"},
61
+ # {"id": "85424", "bot_id": 85424, "name": "OpenAI o3-mini"},
62
+ # {"id": "1182", "bot_id": 1182, "name": "OpenAI o1-mini"},
63
+ # {"id": "85428", "bot_id": 85428, "name": "Grok-2"},
64
+ # {"id": "5", "bot_id": 5, "name": "GPT-4o"},
65
+ # {"id": "6", "bot_id": 6, "name": "GPT-4o 128K"},
66
+ # {"id": "86", "bot_id": 86, "name": "GPT-4o mini"},
67
+ # {"id": "8", "bot_id": 8, "name": "GPT-4 Turbo"},
68
+ # {"id": "9", "bot_id": 9, "name": "GPT-4 Turbo 128K"},
69
+ # {"id": "10", "bot_id": 10, "name": "GPT-4"},
70
+ # {"id": "42", "bot_id": 42, "name": "Claude"},
71
+ # {"id": "50", "bot_id": 50, "name": "Claude 3.5 Sonnet v2"},
72
+ # {"id": "52", "bot_id": 52, "name": "Claude 3 Haiku"},
73
+ # {"id": "53", "bot_id": 53, "name": "Claude 3 Opus"},
74
+ # {"id": "85423", "bot_id": 85423, "name": "Claude 3.5 Haiku 200K"},
75
+ # {"id": "54", "bot_id": 54, "name": "Claude 3.5 Sonnet v2 200K"},
76
+ # {"id": "55", "bot_id": 55, "name": "Claude 3 Sonnet 200K"},
77
+ # {"id": "56", "bot_id": 56, "name": "Claude 3 Haiku 200K"},
78
+ # {"id": "57", "bot_id": 57, "name": "Claude 3 Opus 200K"},
79
+ # {"id": "59", "bot_id": 59, "name": "Gemini 1.5 Flash"},
80
+ # {"id": "60", "bot_id": 60, "name": "Gemini 1.5 Pro"},
81
+ # {"id": "62", "bot_id": 62, "name": "Gemini 1.5 Flash 128K"},
82
+ # {"id": "63", "bot_id": 63, "name": "Gemini 1.5 Pro 128K"},
83
+ # {"id": "65", "bot_id": 65, "name": "Gemini 1.5 Flash 1M"},
84
+ # {"id": "67", "bot_id": 67, "name": "Gemini 1.5 Pro 1M"},
85
+ # {"id": "2", "bot_id": 2, "name": "ChatGPT"},
86
+ # {"id": "3", "bot_id": 3, "name": "GPT-3.5 Turbo"},
87
+ # {"id": "4", "bot_id": 4, "name": "GPT-3.5 Turbo 16K"},
88
+ # {"id": "44", "bot_id": 44, "name": "Claude Instant 100K"},
89
+ # {"id": "45", "bot_id": 45, "name": "Claude 2"},
90
+ # {"id": "47", "bot_id": 47, "name": "Claude 2 100K"},
91
+ # {"id": "49", "bot_id": 49, "name": "Claude 2.1 200K"},
92
+ # {"id": "51", "bot_id": 51, "name": "Claude 3 Sonnet"},
93
+ # {"id": "83", "bot_id": 83, "name": "Gemini"},
94
+ {"id": "58", "bot_id": 58, "name": "Gemini 1.0 Pro"},
95
+ ]
96
+
97
+ # 4) chatId->botId 映射:持久化到文件
98
+ self.map_file = "/tmp/chat_map.json"
99
+ self.chat_map = {}
100
+ if os.path.exists(self.map_file):
101
+ try:
102
+ with open(self.map_file, "r", encoding="utf-8") as f:
103
+ self.chat_map = json.load(f) # {chat_id: bot_id}
104
+ except:
105
+ self.chat_map = {}
106
+
107
+ def pipes(self):
108
+ """
109
+ 返回子模型列表到 OpenWebUI,如:
110
+ [
111
+ {"id": "73", "name": "HIX Chat (botId=73)"},
112
+ {"id": "85427", "name": "DeepSeek-V3 (botId=85427)"}
113
+ ...
114
+ ]
115
+ """
116
+ results = []
117
+ for sp in self.sub_pipes:
118
+ results.append({"id": sp["id"], "name": f"{sp['name']} (botId={sp['id']})"})
119
+ return results
120
+
121
+ async def pipe(
122
+ self, body: dict, __event_emitter__: Callable[[dict], Awaitable[None]] = None
123
+ ) -> AsyncGenerator[str, None]:
124
+ """
125
+ 主接口:
126
+ 1) 解析 body,获取 model => sub_id
127
+ 2) 从 messages 中提取 user问题 & chatId
128
+ 3) 在后台执行 _exec_chat_flow => yield SSE
129
+ """
130
+ chat_id = None
131
+ question = ""
132
+ sub_id = None
133
+
134
+ # A) 解析 "model": e.g. "hix-r1.85427"
135
+ if "model" in body:
136
+ model_id = body["model"]
137
+ if "." in model_id:
138
+ _, sub_id = model_id.split(".", 1) # "85427"
139
+ else:
140
+ sub_id = model_id
141
+ else:
142
+ # 若没给 => 默认
143
+ if self.sub_pipes:
144
+ sub_id = self.sub_pipes[0]["id"]
145
+ else:
146
+ sub_id = "85427" # fallback
147
+
148
+ # B) 解析 user问题 + chatId
149
+ if "messages" in body and isinstance(body["messages"], list):
150
+ for msg in body["messages"]:
151
+ if msg["role"] == "assistant":
152
+ match = CHAT_ID_REGEX.search(msg.get("content", ""))
153
+ if match:
154
+ chat_id = match.group(1)
155
+ elif msg["role"] == "user":
156
+ question = msg.get("content", "")
157
+
158
+ if not question:
159
+ yield json.dumps({"error": "No user question found"}, ensure_ascii=False)
160
+ return
161
+
162
+ if __event_emitter__:
163
+ await __event_emitter__(
164
+ {
165
+ "type": "status",
166
+ "data": {"description": f"Asking: {question[:30]}", "done": False},
167
+ }
168
+ )
169
+
170
+ try:
171
+ # 后台线程 => 同步逻辑
172
+ for token in await asyncio.to_thread(
173
+ self._exec_chat_flow, question, chat_id, sub_id
174
+ ):
175
+ yield token
176
+ except Exception as e:
177
+ yield json.dumps({"error": str(e)}, ensure_ascii=False)
178
+
179
+ if __event_emitter__:
180
+ await __event_emitter__(
181
+ {
182
+ "type": "status",
183
+ "data": {"description": "Answer complete", "done": True},
184
+ }
185
+ )
186
+
187
+ # ========== 核心同步逻辑 ==========
188
+
189
+ def _exec_chat_flow(self, question: str, chat_id: Optional[str], sub_id: str):
190
+ """
191
+ 1) 若 chat_id 在 chat_map => 直接用该 botId, 忽略 sub_id
192
+ 2) 若没有 => sub_id => botId => create_chat => 存映射
193
+ 3) init + login => SSE => yield => 末尾 ***\n Chat ID
194
+ """
195
+ # 1) 查找 chatId 对应 botId
196
+ if chat_id and (chat_id in self.chat_map):
197
+ # 已记录 => 用 map 里的 botId
198
+ chosen_bot_id = self.chat_map[chat_id]
199
+ else:
200
+ # 未记录 => 第一次 => sub_id => botId
201
+ chosen_bot_id = 85427 # fallback
202
+ sub_pipe = next((sp for sp in self.sub_pipes if sp["id"] == sub_id), None)
203
+ if sub_pipe:
204
+ chosen_bot_id = sub_pipe["bot_id"]
205
+
206
+ # 2) 初始化 scraper if needed
207
+ if not self.scraper:
208
+ self.scraper = cloudscraper.create_scraper(
209
+ browser={"browser": "chrome", "platform": "windows", "mobile": False}
210
+ )
211
+ if not self.device_id or not self.device_number:
212
+ self.device_id, self.device_number = self._gen_device_info()
213
+
214
+ # 若还没登录 => anonymous_login
215
+ if not self.logged_in:
216
+ self.csrf_token = self._get_csrf_token(self.scraper)
217
+ self._anonymous_login(
218
+ self.scraper, self.device_id, self.device_number, self.csrf_token
219
+ )
220
+ self.logged_in = True
221
+
222
+ # 3) 如果没有 chat_id => create 并记录
223
+ if not chat_id:
224
+ chat_id = self._create_chat(chosen_bot_id)
225
+ self.chat_map[chat_id] = chosen_bot_id
226
+ self._save_map()
227
+ else:
228
+ # 若 chat_id 不在 map => 说明是进程重启or首次 => 也要记录
229
+ if chat_id not in self.chat_map:
230
+ self.chat_map[chat_id] = chosen_bot_id
231
+ self._save_map()
232
+
233
+ # 4) SSE
234
+ yield from self._sse_chat(chat_id, question)
235
+
236
+ # 5) 末尾
237
+ yield "\n\n***\n"
238
+ yield f"Chat ID: {chat_id}\n"
239
+
240
+ # ============ 具体函数 ============
241
+ def _gen_device_info(self):
242
+ random_uuid = uuid.uuid4().hex.encode("utf-8")
243
+ device_id = hashlib.md5(random_uuid).hexdigest()
244
+
245
+ f_md5 = hashlib.md5(self.f.encode("utf-8")).hexdigest()
246
+ e_str = f_md5 + device_id + self.salt
247
+ device_number = hashlib.sha256(e_str.encode("utf-8")).hexdigest()
248
+ return device_id, device_number
249
+
250
+ def _get_csrf_token(self, scraper):
251
+ url = f"{self.valves.API_DOMAIN}/api/auth/csrf"
252
+ resp = scraper.get(url, allow_redirects=True)
253
+ resp.raise_for_status()
254
+ return resp.json().get("csrfToken")
255
+
256
+ def _anonymous_login(self, scraper, device_id, device_number, csrf_token):
257
+ url = f"{self.valves.API_DOMAIN}/api/auth/callback/anonymous-user"
258
+ form_data = {
259
+ "redirect": "false",
260
+ "version": "v1",
261
+ "deviceId": device_id,
262
+ "deviceNumber": device_number,
263
+ "csrfToken": csrf_token,
264
+ "callbackUrl": "https://hix.ai",
265
+ "json": True,
266
+ }
267
+ headers = {
268
+ "Content-Type": "application/x-www-form-urlencoded",
269
+ "Referer": "https://hix.ai/",
270
+ }
271
+ resp = scraper.post(url, data=form_data, headers=headers)
272
+ resp.raise_for_status()
273
+
274
+
275
+
276
+ def generate_random_title(self):
277
+ """生成一个随机的中英文混合标题,并附带当前时间(YYYY-MM-DD HH:MM)。"""
278
+
279
+ # 1) 随机选取英文单词 (1~2个,示例随机)
280
+ english_part_count = random.randint(1, 2)
281
+ english_part = " ".join(random.choice(self.ENGLISH_WORDS) for _ in range(english_part_count))
282
+
283
+ # 2) 随机选取中文词汇 (1~2个,拼接成一句话)
284
+ chinese_part_count = random.randint(1, 2)
285
+ chinese_part = "".join(random.choice(self.CHINESE_WORDS) for _ in range(chinese_part_count))
286
+
287
+ # 3) 当前时间
288
+ now = datetime.datetime.now()
289
+ time_str = now.strftime("%Y-%m-%d %H:%M")
290
+
291
+ # 4) 组合标题
292
+ # 例如: "Alpha Hello 世界未来 - 2023-08-08 14:25"
293
+ # 你也可以调整连接符、空格等
294
+ title = f"{english_part} {chinese_part} - {time_str}"
295
+
296
+ return title
297
+
298
+ def _create_chat(self, bot_id: int):
299
+ url = f"{self.valves.API_DOMAIN}/api/trpc/hixChat.createChat?batch=1"
300
+ new_title = self.generate_random_title()
301
+ payload = {"0": {"json": {"title": f"{new_title} for hixAI TestTeam.", "botId": bot_id}}}
302
+ r = self.scraper.post(url, json=payload)
303
+ r.raise_for_status()
304
+ data = r.json()
305
+ chat_id = data[0]["result"]["data"]["json"]["id"]
306
+ return chat_id
307
+
308
+ def _sse_chat(self, chat_id: str, question: str):
309
+ url = f"{self.valves.API_DOMAIN}/api/hix/chat"
310
+ payload = {"chatId": chat_id, "question": question, "fileUrl": ""}
311
+ resp = self.scraper.post(url, json=payload, stream=True)
312
+ resp.raise_for_status()
313
+
314
+ for line_bytes in resp.iter_lines():
315
+ if not line_bytes:
316
+ continue
317
+ line = line_bytes.decode("utf-8", errors="ignore").strip()
318
+ if not line.startswith(self.sse_prefix):
319
+ continue
320
+
321
+ json_str = line[len(self.sse_prefix) :].strip()
322
+ if not json_str:
323
+ continue
324
+
325
+ try:
326
+ data = json.loads(json_str)
327
+ except json.JSONDecodeError:
328
+ continue
329
+
330
+ if isinstance(data, list):
331
+ for item in data:
332
+ if isinstance(item, str):
333
+ yield item
334
+ elif isinstance(item, dict) and "content" in item:
335
+ yield item["content"]
336
+ else:
337
+ yield str(item)
338
+ elif isinstance(data, dict):
339
+ content = data.get("content")
340
+ if content:
341
+ yield content
342
+ elif isinstance(data, str):
343
+ yield data
344
+ else:
345
+ yield str(data)
346
+
347
+ # ---------- 持久化 chat_map 到文件 ----------
348
+ def _save_map(self):
349
+ map_file = "/tmp/chat_map.json" # 同 self.map_file
350
+ with open(map_file, "w", encoding="utf-8") as f:
351
+ json.dump(self.chat_map, f, ensure_ascii=False)