ftjc2021 commited on
Commit
9302a78
·
verified ·
1 Parent(s): f120938

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +42 -0
  2. README.md +5 -6
  3. entrypoint.sh +8 -0
  4. main.py +773 -0
  5. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image
2
+ FROM python:3.9-slim
3
+
4
+ # Set up a new user named "user" with user ID 1000
5
+ RUN useradd -m -u 1000 user
6
+
7
+ # Install required system dependencies
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ build-essential \
10
+ && apt-get clean \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Switch to the "user" user
14
+ USER user
15
+
16
+ # Set home to the user's home directory
17
+ ENV HOME=/home/user \
18
+ PATH=/home/user/.local/bin:$PATH
19
+
20
+ # Set the working directory
21
+ WORKDIR $HOME/app
22
+
23
+ # Copy requirements file
24
+ COPY --chown=user requirements.txt .
25
+
26
+ # Install dependencies
27
+ RUN pip install --no-cache-dir --upgrade pip && \
28
+ pip install --no-cache-dir -r requirements.txt
29
+
30
+ # Copy the application code
31
+ COPY --chown=user . .
32
+
33
+ # Make entrypoint script executable
34
+ USER root
35
+ RUN chmod +x entrypoint.sh
36
+ USER user
37
+
38
+ # Expose the port the app runs on
39
+ EXPOSE 7860
40
+
41
+ # Command to run the application
42
+ CMD ["./entrypoint.sh"]
README.md CHANGED
@@ -1,11 +1,10 @@
1
  ---
2
- title: Dify
3
- emoji: 🚀
4
- colorFrom: gray
5
- colorTo: pink
6
  sdk: docker
 
7
  pinned: false
8
- short_description: dify
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: OpenDify
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
9
  ---
10
 
 
entrypoint.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 输出环境信息
4
+ echo "Starting OpenDify on Hugging Face Spaces"
5
+ echo "Python version: $(python --version)"
6
+
7
+ # 运行应用
8
+ exec python main.py
main.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import asyncio
4
+ from flask import Flask, request, Response, stream_with_context, jsonify
5
+ import httpx
6
+ import time
7
+ from dotenv import load_dotenv
8
+ import os
9
+ import ast
10
+
11
+ # 配置日志
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(levelname)s - %(message)s'
15
+ )
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # 设置httpx的日志级别为WARNING,减少不必要的输出
19
+ logging.getLogger("httpx").setLevel(logging.WARNING)
20
+
21
+ # 加载环境变量
22
+ load_dotenv()
23
+
24
+ # 从环境变量读取有效的API密钥(逗号分隔)
25
+ VALID_API_KEYS = [key.strip() for key in os.getenv("VALID_API_KEYS", "").split(",") if key]
26
+
27
+ # 获取会话记忆功能模式配置
28
+ # 1: 构造history_message附加到消息中的模式(默认)
29
+ # 2: 零宽字符模式
30
+ CONVERSATION_MEMORY_MODE = int(os.getenv('CONVERSATION_MEMORY_MODE', '1'))
31
+
32
+ class DifyModelManager:
33
+ def __init__(self):
34
+ self.api_keys = []
35
+ self.name_to_api_key = {} # 应用名称到API Key的映射
36
+ self.api_key_to_name = {} # API Key到应用名称的映射
37
+ self.load_api_keys()
38
+
39
+ def load_api_keys(self):
40
+ """从环境变量加载API Keys"""
41
+ api_keys_str = os.getenv('DIFY_API_KEYS', '')
42
+ if api_keys_str:
43
+ self.api_keys = [key.strip() for key in api_keys_str.split(',') if key.strip()]
44
+ logger.info(f"Loaded {len(self.api_keys)} API keys")
45
+
46
+ async def fetch_app_info(self, api_key):
47
+ """获取Dify应用信息"""
48
+ try:
49
+ async with httpx.AsyncClient() as client:
50
+ headers = {
51
+ "Authorization": f"Bearer {api_key}",
52
+ "Content-Type": "application/json"
53
+ }
54
+ response = await client.get(
55
+ f"{DIFY_API_BASE}/info",
56
+ headers=headers,
57
+ params={"user": "default_user"}
58
+ )
59
+
60
+ if response.status_code == 200:
61
+ app_info = response.json()
62
+ return app_info.get("name", "Unknown App")
63
+ else:
64
+ logger.error(f"Failed to fetch app info for API key: {api_key[:8]}...")
65
+ return None
66
+ except Exception as e:
67
+ logger.error(f"Error fetching app info: {str(e)}")
68
+ return None
69
+
70
+ async def refresh_model_info(self):
71
+ """刷新所有应用信息"""
72
+ self.name_to_api_key.clear()
73
+ self.api_key_to_name.clear()
74
+
75
+ for api_key in self.api_keys:
76
+ app_name = await self.fetch_app_info(api_key)
77
+ if app_name:
78
+ self.name_to_api_key[app_name] = api_key
79
+ self.api_key_to_name[api_key] = app_name
80
+ logger.info(f"Mapped app '{app_name}' to API key: {api_key[:8]}...")
81
+
82
+ def get_api_key(self, model_name):
83
+ """根据模型名称获取API Key"""
84
+ return self.name_to_api_key.get(model_name)
85
+
86
+ def get_available_models(self):
87
+ """获取可用模型列表"""
88
+ return [
89
+ {
90
+ "id": name,
91
+ "object": "model",
92
+ "created": int(time.time()),
93
+ "owned_by": "dify"
94
+ }
95
+ for name in self.name_to_api_key.keys()
96
+ ]
97
+
98
+ # 创建模型管理器实例
99
+ model_manager = DifyModelManager()
100
+
101
+ # 从环境变量获取API基础URL
102
+ DIFY_API_BASE = os.getenv("DIFY_API_BASE", "")
103
+
104
+ app = Flask(__name__)
105
+
106
+ def get_api_key(model_name):
107
+ """根据模型名称获取对应的API密钥"""
108
+ api_key = model_manager.get_api_key(model_name)
109
+ if not api_key:
110
+ logger.warning(f"No API key found for model: {model_name}")
111
+ return api_key
112
+
113
+ def transform_openai_to_dify(openai_request, endpoint):
114
+ """将OpenAI格式的请求转换为Dify格式"""
115
+
116
+ if endpoint == "/chat/completions":
117
+ messages = openai_request.get("messages", [])
118
+ stream = openai_request.get("stream", False)
119
+
120
+ # 尝试从历史消息中提取conversation_id
121
+ conversation_id = None
122
+
123
+ # 提取system消息内容
124
+ system_content = ""
125
+ system_messages = [msg for msg in messages if msg.get("role") == "system"]
126
+ if system_messages:
127
+ system_content = system_messages[0].get("content", "")
128
+ # 记录找到的system消息
129
+ logger.info(f"Found system message: {system_content[:100]}{'...' if len(system_content) > 100 else ''}")
130
+
131
+ if CONVERSATION_MEMORY_MODE == 2: # 零宽字符模式
132
+ if len(messages) > 1:
133
+ # 遍历历史消息,找到最近的assistant消息
134
+ for msg in reversed(messages[:-1]): # 除了最后一条消息
135
+ if msg.get("role") == "assistant":
136
+ content = msg.get("content", "")
137
+ # 尝试解码conversation_id
138
+ conversation_id = decode_conversation_id(content)
139
+ if conversation_id:
140
+ break
141
+
142
+ # 获取最后一条用户消息
143
+ user_query = messages[-1]["content"] if messages and messages[-1].get("role") != "system" else ""
144
+
145
+ # 如果有system消息且是首次对话(没有conversation_id),则将system内容添加到用户查询前
146
+ if system_content and not conversation_id:
147
+ user_query = f"系统指令: {system_content}\n\n用户问题: {user_query}"
148
+ logger.info(f"[零宽字符模式] 首次对话,添加system内容到查询前")
149
+
150
+ dify_request = {
151
+ "inputs": {},
152
+ "query": user_query,
153
+ "response_mode": "streaming" if stream else "blocking",
154
+ "conversation_id": conversation_id,
155
+ "user": openai_request.get("user", "default_user")
156
+ }
157
+ else: # history_message模式(默认)
158
+ # 获取最后一条用户消息
159
+ user_query = messages[-1]["content"] if messages and messages[-1].get("role") != "system" else ""
160
+
161
+ # 构造历史消息
162
+ if len(messages) > 1:
163
+ history_messages = []
164
+ has_system_in_history = False
165
+
166
+ # 检查历史消息中是否已经包含system消息
167
+ for msg in messages[:-1]: # 除了最后一条消息
168
+ role = msg.get("role", "")
169
+ content = msg.get("content", "")
170
+ if role and content:
171
+ if role == "system":
172
+ has_system_in_history = True
173
+ history_messages.append(f"{role}: {content}")
174
+
175
+ # 如果历史中没有system消息但现在有system消息,则添加到历史的最前面
176
+ if system_content and not has_system_in_history:
177
+ history_messages.insert(0, f"system: {system_content}")
178
+ logger.info(f"[history_message模式] 添加system内容到历史消息前")
179
+
180
+ # 将历史消息添加到查询中
181
+ if history_messages:
182
+ history_context = "\n\n".join(history_messages)
183
+ user_query = f"<history>\n{history_context}\n</history>\n\n用户当前问题: {user_query}"
184
+ elif system_content: # 没有历史消息但有system消息
185
+ user_query = f"系统指令: {system_content}\n\n用户问题: {user_query}"
186
+ logger.info(f"[history_message模式] 首次对话,添加system内容到查询前")
187
+
188
+ dify_request = {
189
+ "inputs": {},
190
+ "query": user_query,
191
+ "response_mode": "streaming" if stream else "blocking",
192
+ "user": openai_request.get("user", "default_user")
193
+ }
194
+
195
+ return dify_request
196
+
197
+ return None
198
+
199
+ def transform_dify_to_openai(dify_response, model="claude-3-5-sonnet-v2", stream=False):
200
+ """将Dify格式的响应转换为OpenAI格式"""
201
+
202
+ if not stream:
203
+ # 首先获取回答内容,支持不同的响应模式
204
+ answer = ""
205
+ mode = dify_response.get("mode", "")
206
+
207
+ # 普通聊天模式
208
+ if "answer" in dify_response:
209
+ answer = dify_response.get("answer", "")
210
+
211
+ # 如果是Agent模式,需要从agent_thoughts中提取回答
212
+ elif "agent_thoughts" in dify_response:
213
+ # Agent模式下通常最后一个thought包含最终答案
214
+ agent_thoughts = dify_response.get("agent_thoughts", [])
215
+ if agent_thoughts:
216
+ for thought in agent_thoughts:
217
+ if thought.get("thought"):
218
+ answer = thought.get("thought", "")
219
+
220
+ # 只在零宽字符会话记忆模式时处理conversation_id
221
+ if CONVERSATION_MEMORY_MODE == 2:
222
+ conversation_id = dify_response.get("conversation_id", "")
223
+ history = dify_response.get("conversation_history", [])
224
+
225
+ # 检查历史消息中是否已经有会话ID
226
+ has_conversation_id = False
227
+ if history:
228
+ for msg in history:
229
+ if msg.get("role") == "assistant":
230
+ content = msg.get("content", "")
231
+ if decode_conversation_id(content) is not None:
232
+ has_conversation_id = True
233
+ break
234
+
235
+ # 只在新会话且历史消息中没有会话ID时插入
236
+ if conversation_id and not has_conversation_id:
237
+ logger.info(f"[Debug] Inserting conversation_id: {conversation_id}, history_length: {len(history)}")
238
+ encoded = encode_conversation_id(conversation_id)
239
+ answer = answer + encoded
240
+ logger.info(f"[Debug] Response content after insertion: {repr(answer)}")
241
+
242
+ return {
243
+ "id": dify_response.get("message_id", ""),
244
+ "object": "chat.completion",
245
+ "created": dify_response.get("created", int(time.time())),
246
+ "model": model,
247
+ "choices": [{
248
+ "index": 0,
249
+ "message": {
250
+ "role": "assistant",
251
+ "content": answer
252
+ },
253
+ "finish_reason": "stop"
254
+ }]
255
+ }
256
+ else:
257
+ # 流式响应的转换在stream_response函数中处理
258
+ return dify_response
259
+
260
+ def create_openai_stream_response(content, message_id, model="claude-3-5-sonnet-v2"):
261
+ """创建OpenAI格式的流式响应"""
262
+ return {
263
+ "id": message_id,
264
+ "object": "chat.completion.chunk",
265
+ "created": int(time.time()),
266
+ "model": model,
267
+ "choices": [{
268
+ "index": 0,
269
+ "delta": {
270
+ "content": content
271
+ },
272
+ "finish_reason": None
273
+ }]
274
+ }
275
+
276
+ def encode_conversation_id(conversation_id):
277
+ """将conversation_id编码为不可见的字符序列"""
278
+ if not conversation_id:
279
+ return ""
280
+
281
+ # 使用Base64编码减少长度
282
+ import base64
283
+ encoded = base64.b64encode(conversation_id.encode()).decode()
284
+
285
+ # 使用8种不同的零宽字符表示3位数字
286
+ # 这样可以将编码长度进一步减少
287
+ char_map = {
288
+ '0': '\u200b', # 零宽空格
289
+ '1': '\u200c', # 零宽非连接符
290
+ '2': '\u200d', # 零宽连接符
291
+ '3': '\ufeff', # 零宽非断空格
292
+ '4': '\u2060', # 词组连接符
293
+ '5': '\u180e', # 蒙古语元音分隔符
294
+ '6': '\u2061', # 函数应用
295
+ '7': '\u2062', # 不可见乘号
296
+ }
297
+
298
+ # 将Base64字符串转换为八进制数字
299
+ result = []
300
+ for c in encoded:
301
+ # 将每个字符转换为8进制数字(0-7)
302
+ if c.isalpha():
303
+ if c.isupper():
304
+ val = ord(c) - ord('A')
305
+ else:
306
+ val = ord(c) - ord('a') + 26
307
+ elif c.isdigit():
308
+ val = int(c) + 52
309
+ elif c == '+':
310
+ val = 62
311
+ elif c == '/':
312
+ val = 63
313
+ else: # '='
314
+ val = 0
315
+
316
+ # 每个Base64字符可以产生2个3位数字
317
+ first = (val >> 3) & 0x7
318
+ second = val & 0x7
319
+ result.append(char_map[str(first)])
320
+ if c != '=': # 不编码填充字符的后半部分
321
+ result.append(char_map[str(second)])
322
+
323
+ return ''.join(result)
324
+
325
+ def decode_conversation_id(content):
326
+ """从消息内容中解码conversation_id"""
327
+ try:
328
+ # 零宽字符到3位数字的映射
329
+ char_to_val = {
330
+ '\u200b': '0', # 零宽空格
331
+ '\u200c': '1', # 零宽非连接符
332
+ '\u200d': '2', # 零宽连接符
333
+ '\ufeff': '3', # 零宽非断空格
334
+ '\u2060': '4', # 词组连接符
335
+ '\u180e': '5', # 蒙古语元音分隔符
336
+ '\u2061': '6', # 函数应用
337
+ '\u2062': '7', # 不可见乘号
338
+ }
339
+
340
+ # 提取最后一段零宽字符序列
341
+ space_chars = []
342
+ for c in reversed(content):
343
+ if c not in char_to_val:
344
+ break
345
+ space_chars.append(c)
346
+
347
+ if not space_chars:
348
+ return None
349
+
350
+ # 将零宽字符转换回Base64字符串
351
+ space_chars.reverse()
352
+ base64_chars = []
353
+ for i in range(0, len(space_chars), 2):
354
+ first = int(char_to_val[space_chars[i]], 8)
355
+ if i + 1 < len(space_chars):
356
+ second = int(char_to_val[space_chars[i + 1]], 8)
357
+ val = (first << 3) | second
358
+ else:
359
+ val = first << 3
360
+
361
+ # 转换回Base64字符
362
+ if val < 26:
363
+ base64_chars.append(chr(val + ord('A')))
364
+ elif val < 52:
365
+ base64_chars.append(chr(val - 26 + ord('a')))
366
+ elif val < 62:
367
+ base64_chars.append(str(val - 52))
368
+ elif val == 62:
369
+ base64_chars.append('+')
370
+ else:
371
+ base64_chars.append('/')
372
+
373
+ # 添加Base64填充
374
+ padding = len(base64_chars) % 4
375
+ if padding:
376
+ base64_chars.extend(['='] * (4 - padding))
377
+
378
+ # 解码Base64字符串
379
+ import base64
380
+ base64_str = ''.join(base64_chars)
381
+ return base64.b64decode(base64_str).decode()
382
+
383
+ except Exception as e:
384
+ logger.debug(f"Failed to decode conversation_id: {e}")
385
+ return None
386
+
387
+ @app.route('/v1/chat/completions', methods=['POST'])
388
+ def chat_completions():
389
+ try:
390
+ # 新增:验证API密钥
391
+ auth_header = request.headers.get('Authorization')
392
+ if not auth_header:
393
+ return jsonify({
394
+ "error": {
395
+ "message": "Missing Authorization header",
396
+ "type": "invalid_request_error",
397
+ "param": None,
398
+ "code": "invalid_api_key"
399
+ }
400
+ }), 401
401
+
402
+ parts = auth_header.split()
403
+ if len(parts) != 2 or parts[0].lower() != 'bearer':
404
+ return jsonify({
405
+ "error": {
406
+ "message": "Invalid Authorization header format. Expected: Bearer <API_KEY>",
407
+ "type": "invalid_request_error",
408
+ "param": None,
409
+ "code": "invalid_api_key"
410
+ }
411
+ }), 401
412
+
413
+ provided_api_key = parts[1]
414
+ if provided_api_key not in VALID_API_KEYS:
415
+ return jsonify({
416
+ "error": {
417
+ "message": "Invalid API key",
418
+ "type": "invalid_request_error",
419
+ "param": None,
420
+ "code": "invalid_api_key"
421
+ }
422
+ }), 401
423
+
424
+ # 继续处理原始逻辑
425
+ openai_request = request.get_json()
426
+ logger.info(f"Received request: {json.dumps(openai_request, ensure_ascii=False)}")
427
+
428
+ model = openai_request.get("model", "claude-3-5-sonnet-v2")
429
+
430
+ # 验证模型是否支持
431
+ api_key = get_api_key(model)
432
+ if not api_key:
433
+ error_msg = f"Model {model} is not supported. Available models: {', '.join(model_manager.name_to_api_key.keys())}"
434
+ logger.error(error_msg)
435
+ return {
436
+ "error": {
437
+ "message": error_msg,
438
+ "type": "invalid_request_error",
439
+ "code": "model_not_found"
440
+ }
441
+ }, 404
442
+
443
+ dify_request = transform_openai_to_dify(openai_request, "/chat/completions")
444
+
445
+ if not dify_request:
446
+ logger.error("Failed to transform request")
447
+ return {
448
+ "error": {
449
+ "message": "Invalid request format",
450
+ "type": "invalid_request_error",
451
+ }
452
+ }, 400
453
+
454
+ headers = {
455
+ "Authorization": f"Bearer {api_key}",
456
+ "Content-Type": "application/json"
457
+ }
458
+
459
+ stream = openai_request.get("stream", False)
460
+ dify_endpoint = f"{DIFY_API_BASE}/chat-messages"
461
+ logger.info(f"Sending request to Dify endpoint: {dify_endpoint}, stream={stream}")
462
+
463
+ if stream:
464
+ def generate():
465
+ client = httpx.Client(timeout=None)
466
+
467
+ def flush_chunk(chunk_data):
468
+ """Helper function to flush chunks immediately"""
469
+ return chunk_data.encode('utf-8')
470
+
471
+ def calculate_delay(buffer_size):
472
+ """
473
+ 根据缓冲区大小动态计算延迟
474
+ buffer_size: 缓冲区中剩余的字符数量
475
+ """
476
+ if buffer_size > 30: # 缓冲区内容较多,快速输出
477
+ return 0.001 # 5ms延迟
478
+ elif buffer_size > 20: # 中等数量,适中速度
479
+ return 0.002 # 10ms延迟
480
+ elif buffer_size > 10: # 较少内容,稍慢速度
481
+ return 0.01 # 20ms延迟
482
+ else: # 内容很少,使用较慢的速度
483
+ return 0.02 # 30ms延迟
484
+
485
+ def send_char(char, message_id):
486
+ """Helper function to send single character"""
487
+ openai_chunk = {
488
+ "id": message_id,
489
+ "object": "chat.completion.chunk",
490
+ "created": int(time.time()),
491
+ "model": model,
492
+ "choices": [{
493
+ "index": 0,
494
+ "delta": {
495
+ "content": char
496
+ },
497
+ "finish_reason": None
498
+ }]
499
+ }
500
+ chunk_data = f"data: {json.dumps(openai_chunk)}\n\n"
501
+ return flush_chunk(chunk_data)
502
+
503
+ # 初始化缓冲区
504
+ output_buffer = []
505
+
506
+ try:
507
+ with client.stream(
508
+ 'POST',
509
+ dify_endpoint,
510
+ json=dify_request,
511
+ headers={
512
+ **headers,
513
+ 'Accept': 'text/event-stream',
514
+ 'Cache-Control': 'no-cache',
515
+ 'Connection': 'keep-alive'
516
+ }
517
+ ) as response:
518
+ generate.message_id = None
519
+ buffer = ""
520
+
521
+ for raw_bytes in response.iter_raw():
522
+ if not raw_bytes:
523
+ continue
524
+
525
+ try:
526
+ buffer += raw_bytes.decode('utf-8')
527
+
528
+ while '\n' in buffer:
529
+ line, buffer = buffer.split('\n', 1)
530
+ line = line.strip()
531
+
532
+ if not line or not line.startswith('data: '):
533
+ continue
534
+
535
+ try:
536
+ json_str = line[6:]
537
+ dify_chunk = json.loads(json_str)
538
+
539
+ if dify_chunk.get("event") == "message" and "answer" in dify_chunk:
540
+ current_answer = dify_chunk["answer"]
541
+ if not current_answer:
542
+ continue
543
+
544
+ message_id = dify_chunk.get("message_id", "")
545
+ if not generate.message_id:
546
+ generate.message_id = message_id
547
+
548
+ # 将当前批次的字符添加到输出缓冲区
549
+ for char in current_answer:
550
+ output_buffer.append((char, generate.message_id))
551
+
552
+ # 根据缓冲区大小动态调整输出速度
553
+ while output_buffer:
554
+ char, msg_id = output_buffer.pop(0)
555
+ yield send_char(char, msg_id)
556
+ # 根据剩余缓冲区大小计算延迟
557
+ delay = calculate_delay(len(output_buffer))
558
+ time.sleep(delay)
559
+
560
+ # 立即继续处理下一个请求
561
+ continue
562
+
563
+ # 处理Agent模式的消息事件
564
+ elif dify_chunk.get("event") == "agent_message" and "answer" in dify_chunk:
565
+ current_answer = dify_chunk["answer"]
566
+ if not current_answer:
567
+ continue
568
+
569
+ message_id = dify_chunk.get("message_id", "")
570
+ if not generate.message_id:
571
+ generate.message_id = message_id
572
+
573
+ # 将当前批次的字符添加到输出缓冲区
574
+ for char in current_answer:
575
+ output_buffer.append((char, generate.message_id))
576
+
577
+ # 根据缓冲区大小动态调整输出速度
578
+ while output_buffer:
579
+ char, msg_id = output_buffer.pop(0)
580
+ yield send_char(char, msg_id)
581
+ # 根据剩余缓冲区大小计算延迟
582
+ delay = calculate_delay(len(output_buffer))
583
+ time.sleep(delay)
584
+
585
+ # 立即继续处理下一个请求
586
+ continue
587
+
588
+ # 处理Agent的思考过程,记录日志但不输出给用户
589
+ elif dify_chunk.get("event") == "agent_thought":
590
+ thought_id = dify_chunk.get("id", "")
591
+ thought = dify_chunk.get("thought", "")
592
+ tool = dify_chunk.get("tool", "")
593
+ tool_input = dify_chunk.get("tool_input", "")
594
+ observation = dify_chunk.get("observation", "")
595
+
596
+ logger.info(f"[Agent Thought] ID: {thought_id}, Tool: {tool}")
597
+ if thought:
598
+ logger.info(f"[Agent Thought] Thought: {thought}")
599
+ if tool_input:
600
+ logger.info(f"[Agent Thought] Tool Input: {tool_input}")
601
+ if observation:
602
+ logger.info(f"[Agent Thought] Observation: {observation}")
603
+
604
+ # 获取message_id以关联思考和最终输出
605
+ message_id = dify_chunk.get("message_id", "")
606
+ if not generate.message_id and message_id:
607
+ generate.message_id = message_id
608
+
609
+ continue
610
+
611
+ # 处理消息中的文件(如图片),记录日志但不直接输出给用户
612
+ elif dify_chunk.get("event") == "message_file":
613
+ file_id = dify_chunk.get("id", "")
614
+ file_type = dify_chunk.get("type", "")
615
+ file_url = dify_chunk.get("url", "")
616
+
617
+ logger.info(f"[Message File] ID: {file_id}, Type: {file_type}, URL: {file_url}")
618
+ continue
619
+
620
+ elif dify_chunk.get("event") == "message_end":
621
+ # 快速输出剩余内容
622
+ while output_buffer:
623
+ char, msg_id = output_buffer.pop(0)
624
+ yield send_char(char, msg_id)
625
+ time.sleep(0.001) # 固定使用最小延迟快速输出剩余内容
626
+
627
+ # 只在零宽字符会话记忆模式时处理conversation_id
628
+ if CONVERSATION_MEMORY_MODE == 2:
629
+ conversation_id = dify_chunk.get("conversation_id")
630
+ history = dify_chunk.get("conversation_history", [])
631
+
632
+ has_conversation_id = False
633
+ if history:
634
+ for msg in history:
635
+ if msg.get("role") == "assistant":
636
+ content = msg.get("content", "")
637
+ if decode_conversation_id(content) is not None:
638
+ has_conversation_id = True
639
+ break
640
+
641
+ # 只在新会话且历史消息中没有会话ID时插入
642
+ if conversation_id and not has_conversation_id:
643
+ logger.info(f"[Debug] Inserting conversation_id in stream: {conversation_id}")
644
+ encoded = encode_conversation_id(conversation_id)
645
+ logger.info(f"[Debug] Stream encoded content: {repr(encoded)}")
646
+ for char in encoded:
647
+ yield send_char(char, generate.message_id)
648
+
649
+ final_chunk = {
650
+ "id": generate.message_id,
651
+ "object": "chat.completion.chunk",
652
+ "created": int(time.time()),
653
+ "model": model,
654
+ "choices": [{
655
+ "index": 0,
656
+ "delta": {},
657
+ "finish_reason": "stop"
658
+ }]
659
+ }
660
+ yield flush_chunk(f"data: {json.dumps(final_chunk)}\n\n")
661
+ yield flush_chunk("data: [DONE]\n\n")
662
+
663
+ except json.JSONDecodeError as e:
664
+ logger.error(f"JSON decode error: {str(e)}")
665
+ continue
666
+
667
+ except Exception as e:
668
+ logger.error(f"Error processing chunk: {str(e)}")
669
+ continue
670
+
671
+ finally:
672
+ client.close()
673
+
674
+ return Response(
675
+ stream_with_context(generate()),
676
+ content_type='text/event-stream',
677
+ headers={
678
+ 'Cache-Control': 'no-cache, no-transform',
679
+ 'Connection': 'keep-alive',
680
+ 'Transfer-Encoding': 'chunked',
681
+ 'X-Accel-Buffering': 'no',
682
+ 'Content-Encoding': 'none'
683
+ },
684
+ direct_passthrough=True
685
+ )
686
+ else:
687
+ async def sync_response():
688
+ try:
689
+ async with httpx.AsyncClient() as client:
690
+ response = await client.post(
691
+ dify_endpoint,
692
+ json=dify_request,
693
+ headers=headers
694
+ )
695
+
696
+ if response.status_code != 200:
697
+ error_msg = f"Dify API error: {response.text}"
698
+ logger.error(f"Request failed: {error_msg}")
699
+ return {
700
+ "error": {
701
+ "message": error_msg,
702
+ "type": "api_error",
703
+ "code": response.status_code
704
+ }
705
+ }, response.status_code
706
+
707
+ dify_response = response.json()
708
+ logger.info(f"Received response from Dify: {json.dumps(dify_response, ensure_ascii=False)}")
709
+ logger.info(f"[Debug] Response content: {repr(dify_response.get('answer', ''))}")
710
+ openai_response = transform_dify_to_openai(dify_response, model=model)
711
+ conversation_id = dify_response.get("conversation_id")
712
+ if conversation_id:
713
+ # 在响应头中传递conversation_id
714
+ return Response(
715
+ json.dumps(openai_response),
716
+ content_type='application/json',
717
+ headers={
718
+ 'Conversation-Id': conversation_id
719
+ }
720
+ )
721
+ else:
722
+ return openai_response
723
+ except httpx.RequestError as e:
724
+ error_msg = f"Failed to connect to Dify: {str(e)}"
725
+ logger.error(error_msg)
726
+ return {
727
+ "error": {
728
+ "message": error_msg,
729
+ "type": "api_error",
730
+ "code": "connection_error"
731
+ }
732
+ }, 503
733
+
734
+ return asyncio.run(sync_response())
735
+
736
+ except Exception as e:
737
+ logger.exception("Unexpected error occurred")
738
+ return {
739
+ "error": {
740
+ "message": str(e),
741
+ "type": "internal_error",
742
+ }
743
+ }, 500
744
+
745
+ @app.route('/v1/models', methods=['GET'])
746
+ def list_models():
747
+ """返回可用的模型列表"""
748
+ logger.info("Listing available models")
749
+
750
+ # 刷新模型信息
751
+ asyncio.run(model_manager.refresh_model_info())
752
+
753
+ # 获取可用模型列表
754
+ available_models = model_manager.get_available_models()
755
+
756
+ response = {
757
+ "object": "list",
758
+ "data": available_models
759
+ }
760
+ logger.info(f"Available models: {json.dumps(response, ensure_ascii=False)}")
761
+ return response
762
+
763
+ # 在main.py的最后初始化时添加环境变量检查:
764
+ if __name__ == "__main__":
765
+ # 刷新模型信息
766
+ asyncio.run(model_manager.refresh_model_info())
767
+
768
+ # 从环境变量获取服务器配置
769
+ host = os.getenv("SERVER_HOST", "0.0.0.0") # 在Docker中使用0.0.0.0
770
+ port = int(os.getenv("SERVER_PORT", 7860)) # Hugging Face Spaces默认使用7860端口
771
+
772
+ logger.info(f"Starting server on {host}:{port}")
773
+ app.run(host=host, port=port)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ flask
2
+ httpx
3
+ python-dotenv