mistpe commited on
Commit
4a7b3a0
·
verified ·
1 Parent(s): 8e12f48

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +572 -0
  2. templates/index.html +1705 -0
  3. wechat_service.py +612 -0
app.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, jsonify, request
2
+ import json
3
+ import os
4
+ import threading
5
+
6
+ # 导入原有的微信服务模块
7
+ from wechat_service import (
8
+ TOKEN, session_manager, executor, client, API_KEY,
9
+ verify_signature, verify_msg_signature, parse_xml_message,
10
+ generate_response_xml, handle_async_task, generate_initial_response,
11
+ append_status_message, split_message, AsyncResponse, cleanup_sessions
12
+ )
13
+ import wechat_service
14
+ import uuid
15
+ import xml.etree.ElementTree as ET
16
+ import requests
17
+
18
+ app = Flask(__name__)
19
+
20
+ CONFIG_FILE = 'config.json'
21
+
22
+ def load_config():
23
+ """加载配置文件"""
24
+ if os.path.exists(CONFIG_FILE):
25
+ with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
26
+ return json.load(f)
27
+ else:
28
+ # 默认配置
29
+ default_config = {
30
+ "models": {
31
+ "text_model": "gpt-4.1-mini",
32
+ "image_model": "gpt-4.1-mini",
33
+ "image_generation_model": "dall-e-3"
34
+ },
35
+ "system_prompt": "你是HXIAO公众号的智能助手,这一个用来分享与学习人工智能的公众号,我们的目标是专注AI应用的简单研究与实践。致力于分享切实可行的技术方案,希望让复杂的技术变得简单易懂。也喜欢用通俗的语言来解释专业概念,让技术真正服务于每个学习者",
36
+ "functions": [
37
+ {
38
+ "name": "generate_image",
39
+ "description": "Generate an image based on text description",
40
+ "parameters": {
41
+ "type": "object",
42
+ "properties": {
43
+ "prompt": {
44
+ "type": "string",
45
+ "description": "The description of the image to generate"
46
+ }
47
+ },
48
+ "required": ["prompt"]
49
+ },
50
+ "http_config": {
51
+ "method": "POST",
52
+ "url": "https://api1.oaipro.com/v1/images/generations",
53
+ "headers": {
54
+ "Content-Type": "application/json",
55
+ "Authorization": f"Bearer {API_KEY}"
56
+ },
57
+ "body_template": {
58
+ "model": "dall-e-3",
59
+ "prompt": "{prompt}",
60
+ "n": 1,
61
+ "size": "1024x1024"
62
+ },
63
+ "response_type": "image"
64
+ }
65
+ }
66
+ ],
67
+ "settings": {
68
+ "max_workers": 10,
69
+ "request_timeout": 60,
70
+ "max_message_length": 500,
71
+ "session_timeout": 3600
72
+ }
73
+ }
74
+ save_config(default_config)
75
+ return default_config
76
+
77
+ def save_config(config):
78
+ """保存配置文件"""
79
+ with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
80
+ json.dump(config, f, ensure_ascii=False, indent=2)
81
+
82
+ # 全局配置
83
+ config = load_config()
84
+
85
+ def build_dynamic_tools():
86
+ """动态构建Function工具列表"""
87
+ tools = []
88
+ for func_config in config['functions']:
89
+ tool = {
90
+ "type": "function",
91
+ "function": {
92
+ "name": func_config['name'],
93
+ "description": func_config['description'],
94
+ "parameters": func_config['parameters']
95
+ }
96
+ }
97
+ tools.append(tool)
98
+ return tools
99
+
100
+ def execute_dynamic_function(function_name, arguments):
101
+ """执行动态Function"""
102
+ func_config = None
103
+ for func in config['functions']:
104
+ if func['name'] == function_name:
105
+ func_config = func
106
+ break
107
+
108
+ if not func_config:
109
+ raise Exception(f"Function {function_name} not found")
110
+
111
+ http_config = func_config['http_config']
112
+
113
+ # 构建请求
114
+ method = http_config['method']
115
+ url = http_config['url']
116
+ headers = http_config.get('headers', {})
117
+
118
+ # 处理URL参数替换
119
+ for key, value in arguments.items():
120
+ url = url.replace(f"{{{key}}}", str(value))
121
+
122
+ # 处理请求体
123
+ if method.upper() in ['POST', 'PUT', 'PATCH'] and 'body_template' in http_config:
124
+ body_template = http_config['body_template']
125
+ body = {}
126
+ for key, value in body_template.items():
127
+ if isinstance(value, str) and value.startswith('{') and value.endswith('}'):
128
+ param_name = value[1:-1]
129
+ if param_name in arguments:
130
+ body[key] = arguments[param_name]
131
+ else:
132
+ body[key] = value
133
+ else:
134
+ body[key] = value
135
+
136
+ response = requests.request(
137
+ method=method,
138
+ url=url,
139
+ headers=headers,
140
+ json=body,
141
+ timeout=config['settings']['request_timeout']
142
+ )
143
+ else:
144
+ # GET请求或其他无body请求
145
+ params = {}
146
+ for key, value in arguments.items():
147
+ if f"{{{key}}}" not in url: # 如果不在URL中,则作为查询参数
148
+ params[key] = value
149
+
150
+ response = requests.request(
151
+ method=method,
152
+ url=url,
153
+ headers=headers,
154
+ params=params,
155
+ timeout=config['settings']['request_timeout']
156
+ )
157
+
158
+ response.raise_for_status()
159
+
160
+ # 处理响应
161
+ if http_config.get('response_type') == 'image':
162
+ # 图片生成的特殊处理
163
+ result = response.json()
164
+ if 'data' in result and result['data']:
165
+ image_url = result['data'][0]['url']
166
+ img_response = requests.get(image_url, timeout=30)
167
+ img_response.raise_for_status()
168
+ return {
169
+ "type": "image",
170
+ "data": img_response.content
171
+ }
172
+
173
+ return {
174
+ "type": "text",
175
+ "data": response.json() if response.headers.get('content-type', '').startswith('application/json') else response.text
176
+ }
177
+
178
+ def update_wechat_service_config():
179
+ """更新wechat_service模块中的配置"""
180
+ if not session_manager:
181
+ return
182
+
183
+ # 更新现有会话的系统提示词
184
+ for session in session_manager.sessions.values():
185
+ if session.messages and session.messages[0]['role'] == 'system':
186
+ session.messages[0]['content'] = config['system_prompt']
187
+
188
+ def enhanced_process_long_running_task(messages, message_type='text', image_data=None):
189
+ """增强版的长时间任务处理,支持动态Function"""
190
+ try:
191
+ if message_type == 'image':
192
+ # 图片处理保持原有逻辑
193
+ if 'process_long_running_task' in globals():
194
+ return process_long_running_task(messages, message_type, image_data)
195
+ else:
196
+ raise Exception("wechat_service module not available")
197
+ else:
198
+ # 文本处理使用动态工具
199
+ tools = build_dynamic_tools()
200
+
201
+ response = client.chat.completions.create(
202
+ model=config['models']['text_model'],
203
+ messages=messages,
204
+ tools=tools,
205
+ tool_choice="auto",
206
+ timeout=config['settings']['request_timeout']
207
+ )
208
+
209
+ if response.choices[0].message.tool_calls:
210
+ tool_call = response.choices[0].message.tool_calls[0]
211
+ function_name = tool_call.function.name
212
+ arguments = json.loads(tool_call.function.arguments)
213
+
214
+ result = execute_dynamic_function(function_name, arguments)
215
+
216
+ if result["type"] == "image":
217
+ if 'upload_image_to_wechat' in globals():
218
+ media_id = upload_image_to_wechat(result["data"])
219
+ return {
220
+ "type": "image",
221
+ "media_id": media_id
222
+ }
223
+ else:
224
+ return {
225
+ "type": "text",
226
+ "content": "图片处理功能暂不可用"
227
+ }
228
+ else:
229
+ return {
230
+ "type": "text",
231
+ "content": str(result["data"])
232
+ }
233
+
234
+ return {
235
+ "type": "text",
236
+ "content": response.choices[0].message.content
237
+ }
238
+ except Exception as e:
239
+ print(f"增强任务处理失败: {str(e)}")
240
+ raise
241
+
242
+ def enhanced_handle_async_task(session, task_id, messages=None, message_type='text', message_data=None):
243
+ """增强版异步任务处理"""
244
+ try:
245
+ if task_id not in session.response_queue:
246
+ return
247
+
248
+ if message_type == 'image':
249
+ result = enhanced_process_long_running_task(None, 'image', message_data)
250
+ else:
251
+ result = enhanced_process_long_running_task(messages)
252
+
253
+ if task_id in session.response_queue and not session.response_queue[task_id].is_expired():
254
+ session.response_queue[task_id].status = "completed"
255
+ session.response_queue[task_id].response_type = result.get("type", "text")
256
+
257
+ if result["type"] == "image":
258
+ session.response_queue[task_id].media_id = result["media_id"]
259
+ session.response_queue[task_id].result = None
260
+ else:
261
+ session.response_queue[task_id].result = result["content"]
262
+
263
+ if messages and result["type"] == "text":
264
+ messages.append({"role": "assistant", "content": result["content"]})
265
+
266
+ except Exception as e:
267
+ print(f"增强异步任务处理失败: {str(e)}")
268
+ if task_id in session.response_queue:
269
+ session.response_queue[task_id].status = "failed"
270
+ session.response_queue[task_id].error = str(e)
271
+
272
+ # Web管理界面路由
273
+ @app.route('/')
274
+ def index():
275
+ return render_template('index.html')
276
+
277
+ @app.route('/api/config', methods=['GET'])
278
+ def get_config():
279
+ return jsonify(config)
280
+
281
+ @app.route('/api/config', methods=['POST'])
282
+ def update_config():
283
+ global config
284
+ try:
285
+ new_config = request.json
286
+ config.update(new_config)
287
+ save_config(config)
288
+
289
+ # 更新wechat_service模块的配置
290
+ update_wechat_service_config()
291
+
292
+ # 重新加载系统提示词到现有会话
293
+ for session in session_manager.sessions.values():
294
+ if session.messages and session.messages[0]['role'] == 'system':
295
+ session.messages[0]['content'] = config['system_prompt']
296
+
297
+ return jsonify({"success": True, "message": "配置已更新"})
298
+ except Exception as e:
299
+ return jsonify({"success": False, "message": str(e)}), 500
300
+
301
+ @app.route('/api/sessions', methods=['GET'])
302
+ def get_sessions():
303
+ try:
304
+ if not session_manager:
305
+ return jsonify([])
306
+
307
+ sessions_info = []
308
+ for user_id, session in session_manager.sessions.items():
309
+ sessions_info.append({
310
+ "user_id": user_id,
311
+ "last_active": session.last_active,
312
+ "message_count": len(session.messages),
313
+ "has_pending": len(session.pending_parts) > 0,
314
+ "current_task": session.current_task is not None
315
+ })
316
+ return jsonify(sessions_info)
317
+ except Exception as e:
318
+ print(f"获取会话列表失败: {e}")
319
+ return jsonify([])
320
+
321
+ @app.route('/api/sessions/<user_id>/clear', methods=['POST'])
322
+ def clear_user_session(user_id):
323
+ try:
324
+ if session_manager:
325
+ session_manager.clear_session(user_id)
326
+ return jsonify({"success": True, "message": f"用户 {user_id} 的会话已清理"})
327
+ else:
328
+ return jsonify({"success": False, "message": "会话管理器不可用"})
329
+ except Exception as e:
330
+ return jsonify({"success": False, "message": str(e)}), 500
331
+
332
+ @app.route('/api/logs', methods=['GET'])
333
+ def get_logs():
334
+ try:
335
+ lines = int(request.args.get('lines', 100))
336
+ with open('wechat_service.log', 'r', encoding='utf-8') as f:
337
+ log_lines = f.readlines()[-lines:]
338
+ return jsonify({"logs": log_lines})
339
+ except Exception as e:
340
+ return jsonify({"error": str(e)}), 500
341
+
342
+ @app.route('/api/test-function', methods=['POST'])
343
+ def test_function():
344
+ try:
345
+ data = request.json
346
+ function_name = data['function_name']
347
+ arguments = data['arguments']
348
+
349
+ result = execute_dynamic_function(function_name, arguments)
350
+ return jsonify({"success": True, "result": result})
351
+ except Exception as e:
352
+ return jsonify({"success": False, "error": str(e)}), 500
353
+
354
+ # 微信服务路由(使用原有逻辑但增强Function支持)
355
+ @app.route('/api/wx', methods=['GET', 'POST'])
356
+ def wechatai():
357
+ if request.method == 'GET':
358
+ signature = request.args.get('signature')
359
+ timestamp = request.args.get('timestamp')
360
+ nonce = request.args.get('nonce')
361
+ echostr = request.args.get('echostr')
362
+
363
+ if verify_signature(signature, timestamp, nonce, TOKEN):
364
+ return echostr
365
+ return 'error', 403
366
+
367
+ try:
368
+ encrypt_type = request.args.get('encrypt_type', '')
369
+
370
+ if encrypt_type == 'aes':
371
+ msg_signature = request.args.get('msg_signature')
372
+ timestamp = request.args.get('timestamp')
373
+ nonce = request.args.get('nonce')
374
+
375
+ xml_tree = ET.fromstring(request.data)
376
+ encrypted_text = xml_tree.find('Encrypt').text
377
+
378
+ if not verify_msg_signature(msg_signature, timestamp, nonce, TOKEN, encrypted_text):
379
+ return 'Invalid signature', 403
380
+
381
+ try:
382
+ decrypted_xml = session_manager.crypto.decrypt(encrypted_text)
383
+ message_data = parse_xml_message(decrypted_xml)
384
+ except Exception as e:
385
+ return 'Decryption failed', 403
386
+ else:
387
+ try:
388
+ message_data = parse_xml_message(request.data)
389
+ except Exception as e:
390
+ return 'Invalid XML', 400
391
+
392
+ from_user = message_data['from_user']
393
+ to_user = message_data['to_user']
394
+ msg_type = message_data['msg_type']
395
+
396
+ try:
397
+ session = session_manager.get_session(from_user)
398
+ except Exception as e:
399
+ return generate_response_xml(
400
+ from_user,
401
+ to_user,
402
+ append_status_message('系统错误,请稍后重试。'),
403
+ encrypt_type=encrypt_type
404
+ )
405
+
406
+ if msg_type == 'image':
407
+ try:
408
+ task_id = str(uuid.uuid4())
409
+ session.current_task = task_id
410
+ session.response_queue[task_id] = AsyncResponse()
411
+
412
+ executor.submit(
413
+ enhanced_handle_async_task,
414
+ session,
415
+ task_id,
416
+ None,
417
+ 'image',
418
+ message_data
419
+ )
420
+
421
+ return generate_response_xml(
422
+ from_user,
423
+ to_user,
424
+ append_status_message("正在分析图片,请稍候...\n回复'查询'获取分析结果", is_processing=True),
425
+ encrypt_type=encrypt_type
426
+ )
427
+ except Exception as e:
428
+ return generate_response_xml(
429
+ from_user,
430
+ to_user,
431
+ append_status_message('处理图片时出现错误,请稍后重试。'),
432
+ encrypt_type=encrypt_type
433
+ )
434
+ else:
435
+ try:
436
+ user_content = message_data['content'].strip()
437
+
438
+ if user_content == '新对话':
439
+ session_manager.clear_session(from_user)
440
+ return generate_response_xml(
441
+ from_user,
442
+ to_user,
443
+ append_status_message('已开始新的对话。请描述您的问题。'),
444
+ encrypt_type=encrypt_type
445
+ )
446
+
447
+ if user_content == '继续':
448
+ if session.pending_parts:
449
+ next_part = session.pending_parts.pop(0)
450
+ has_more = bool(session.pending_parts)
451
+ return generate_response_xml(
452
+ from_user,
453
+ to_user,
454
+ append_status_message(next_part, has_more),
455
+ encrypt_type=encrypt_type
456
+ )
457
+ return generate_response_xml(
458
+ from_user,
459
+ to_user,
460
+ append_status_message('没有更多内容了。请继续您的问题。'),
461
+ encrypt_type=encrypt_type
462
+ )
463
+
464
+ if user_content == '查询':
465
+ if session.current_task:
466
+ task_response = session.response_queue.get(session.current_task)
467
+ if task_response:
468
+ if task_response.is_expired():
469
+ del session.response_queue[session.current_task]
470
+ session.current_task = None
471
+ return generate_response_xml(
472
+ from_user,
473
+ to_user,
474
+ append_status_message('请求已过期,请重新提问。'),
475
+ encrypt_type=encrypt_type
476
+ )
477
+
478
+ if task_response.status == "completed":
479
+ if task_response.response_type == "image":
480
+ del session.response_queue[session.current_task]
481
+ session.current_task = None
482
+ return generate_response_xml(
483
+ from_user,
484
+ to_user,
485
+ "",
486
+ response_type="image",
487
+ media_id=task_response.media_id,
488
+ encrypt_type=encrypt_type
489
+ )
490
+ else:
491
+ response = task_response.result
492
+ del session.response_queue[session.current_task]
493
+ session.current_task = None
494
+
495
+ max_length = config['settings']['max_message_length']
496
+ if len(response) > max_length:
497
+ parts = split_message(response, max_length)
498
+ first_part = parts.pop(0)
499
+ session.pending_parts = parts
500
+ return generate_response_xml(
501
+ from_user,
502
+ to_user,
503
+ append_status_message(first_part, True),
504
+ encrypt_type=encrypt_type
505
+ )
506
+ return generate_response_xml(
507
+ from_user,
508
+ to_user,
509
+ append_status_message(response),
510
+ encrypt_type=encrypt_type
511
+ )
512
+ elif task_response.status == "failed":
513
+ error_message = '处理过程中出现错误,请重新提问。'
514
+ del session.response_queue[session.current_task]
515
+ session.current_task = None
516
+ return generate_response_xml(
517
+ from_user,
518
+ to_user,
519
+ append_status_message(error_message),
520
+ encrypt_type=encrypt_type
521
+ )
522
+ else:
523
+ return generate_response_xml(
524
+ from_user,
525
+ to_user,
526
+ append_status_message('正在处理中,请稍后再次查询。', is_processing=True),
527
+ encrypt_type=encrypt_type
528
+ )
529
+ return generate_response_xml(
530
+ from_user,
531
+ to_user,
532
+ append_status_message('没有正在处理的请求。'),
533
+ encrypt_type=encrypt_type
534
+ )
535
+
536
+ session.messages.append({"role": "user", "content": user_content})
537
+
538
+ task_id = str(uuid.uuid4())
539
+ session.current_task = task_id
540
+ session.response_queue[task_id] = AsyncResponse()
541
+
542
+ executor.submit(enhanced_handle_async_task, session, task_id, session.messages.copy())
543
+
544
+ return generate_response_xml(
545
+ from_user,
546
+ to_user,
547
+ append_status_message(generate_initial_response(), is_processing=True),
548
+ encrypt_type=encrypt_type
549
+ )
550
+
551
+ except Exception as e:
552
+ return generate_response_xml(
553
+ from_user,
554
+ to_user,
555
+ append_status_message('处理消息时出现错误,请稍后重试。'),
556
+ encrypt_type=encrypt_type
557
+ )
558
+
559
+ except Exception as e:
560
+ return generate_response_xml(
561
+ message_data['from_user'] if 'message_data' in locals() else 'unknown',
562
+ message_data['to_user'] if 'message_data' in locals() else 'unknown',
563
+ append_status_message('抱歉,系统暂时出现问题,请稍后重试。'),
564
+ encrypt_type if 'encrypt_type' in locals() else ''
565
+ )
566
+
567
+ if __name__ == '__main__':
568
+ # 启动会话清理线程
569
+ cleanup_thread = threading.Thread(target=cleanup_sessions, daemon=True)
570
+ cleanup_thread.start()
571
+
572
+ app.run(host='0.0.0.0', port=7860, debug=True)
templates/index.html ADDED
@@ -0,0 +1,1705 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>微信AI助手管理后台</title>
7
+ <style>
8
+ /* 基础样式重置 */
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
17
+ background-color: #ffffff;
18
+ color: #333333;
19
+ line-height: 1.6;
20
+ }
21
+
22
+ /* 应用容器 */
23
+ .app-container {
24
+ display: flex;
25
+ min-height: 100vh;
26
+ }
27
+
28
+ /* PC端侧边栏 */
29
+ .sidebar {
30
+ width: 260px;
31
+ background-color: #ffffff;
32
+ border-right: 1px solid #e8f5e8;
33
+ padding: 20px 0;
34
+ position: fixed;
35
+ height: 100vh;
36
+ overflow-y: auto;
37
+ z-index: 100;
38
+ }
39
+
40
+ .sidebar-header {
41
+ padding: 0 20px 30px;
42
+ border-bottom: 1px solid #e8f5e8;
43
+ margin-bottom: 20px;
44
+ }
45
+
46
+ .logo {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 12px;
50
+ }
51
+
52
+ .logo span {
53
+ font-size: 18px;
54
+ font-weight: 600;
55
+ color: #2e7d32;
56
+ }
57
+
58
+ .nav-menu {
59
+ list-style: none;
60
+ padding: 0 10px;
61
+ }
62
+
63
+ .nav-item {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 12px;
67
+ padding: 12px 16px;
68
+ margin-bottom: 4px;
69
+ border-radius: 8px;
70
+ cursor: pointer;
71
+ transition: all 0.2s ease;
72
+ color: #666666;
73
+ }
74
+
75
+ .nav-item:hover {
76
+ background-color: #f0f8f0;
77
+ color: #2e7d32;
78
+ }
79
+
80
+ .nav-item.active {
81
+ background-color: #e8f5e8;
82
+ color: #2e7d32;
83
+ border: 1px solid #c8e6c8;
84
+ }
85
+
86
+ .nav-item svg {
87
+ width: 20px;
88
+ height: 20px;
89
+ }
90
+
91
+ .nav-item span {
92
+ font-size: 14px;
93
+ font-weight: 500;
94
+ }
95
+
96
+ /* 移动端底部导航 */
97
+ .bottom-nav {
98
+ display: none;
99
+ position: fixed;
100
+ bottom: 0;
101
+ left: 0;
102
+ right: 0;
103
+ background-color: #ffffff;
104
+ border-top: 1px solid #e8f5e8;
105
+ padding: 8px 0;
106
+ z-index: 100;
107
+ }
108
+
109
+ .bottom-nav .nav-item {
110
+ flex: 1;
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: center;
114
+ gap: 4px;
115
+ padding: 8px 4px;
116
+ cursor: pointer;
117
+ transition: all 0.2s ease;
118
+ color: #666666;
119
+ }
120
+
121
+ .bottom-nav .nav-item:hover,
122
+ .bottom-nav .nav-item.active {
123
+ color: #2e7d32;
124
+ }
125
+
126
+ .bottom-nav .nav-item svg {
127
+ width: 20px;
128
+ height: 20px;
129
+ }
130
+
131
+ .bottom-nav .nav-item span {
132
+ font-size: 11px;
133
+ font-weight: 500;
134
+ }
135
+
136
+ /* 主内容区域 */
137
+ .main-content {
138
+ flex: 1;
139
+ margin-left: 260px;
140
+ background-color: #fafafa;
141
+ min-height: 100vh;
142
+ }
143
+
144
+ /* 页面样式 */
145
+ .page {
146
+ display: none;
147
+ padding: 30px;
148
+ }
149
+
150
+ .page.active {
151
+ display: block;
152
+ }
153
+
154
+ .page-header {
155
+ margin-bottom: 30px;
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: flex-start;
159
+ flex-wrap: wrap;
160
+ gap: 16px;
161
+ }
162
+
163
+ .page-header h1 {
164
+ font-size: 28px;
165
+ font-weight: 700;
166
+ color: #2e7d32;
167
+ margin-bottom: 4px;
168
+ }
169
+
170
+ .page-header p {
171
+ color: #666666;
172
+ font-size: 16px;
173
+ }
174
+
175
+ /* 按钮样式 */
176
+ .btn {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ gap: 8px;
180
+ padding: 10px 20px;
181
+ border: none;
182
+ border-radius: 8px;
183
+ font-size: 14px;
184
+ font-weight: 500;
185
+ cursor: pointer;
186
+ transition: all 0.2s ease;
187
+ text-decoration: none;
188
+ }
189
+
190
+ .btn-primary {
191
+ background-color: #90ee90;
192
+ color: #2e7d32;
193
+ border: 1px solid #81c784;
194
+ }
195
+
196
+ .btn-primary:hover {
197
+ background-color: #81c784;
198
+ color: #ffffff;
199
+ }
200
+
201
+ .btn-secondary {
202
+ background-color: #ffffff;
203
+ color: #666666;
204
+ border: 1px solid #e0e0e0;
205
+ }
206
+
207
+ .btn-secondary:hover {
208
+ background-color: #f5f5f5;
209
+ color: #333333;
210
+ }
211
+
212
+ .btn-danger {
213
+ background-color: #ffffff;
214
+ color: #d32f2f;
215
+ border: 1px solid #ffcdd2;
216
+ }
217
+
218
+ .btn-danger:hover {
219
+ background-color: #ffebee;
220
+ }
221
+
222
+ /* 统计卡片网格 */
223
+ .stats-grid {
224
+ display: grid;
225
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
226
+ gap: 20px;
227
+ margin-bottom: 30px;
228
+ }
229
+
230
+ .stat-card {
231
+ background-color: #ffffff;
232
+ border: 1px solid #e8f5e8;
233
+ border-radius: 12px;
234
+ padding: 24px;
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 16px;
238
+ }
239
+
240
+ .stat-icon {
241
+ width: 48px;
242
+ height: 48px;
243
+ background-color: #f0f8f0;
244
+ border-radius: 12px;
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ color: #2e7d32;
249
+ }
250
+
251
+ .stat-content h3 {
252
+ font-size: 14px;
253
+ color: #666666;
254
+ font-weight: 500;
255
+ margin-bottom: 4px;
256
+ }
257
+
258
+ .stat-value {
259
+ font-size: 24px;
260
+ font-weight: 700;
261
+ color: #2e7d32;
262
+ }
263
+
264
+ /* 卡片样式 */
265
+ .card {
266
+ background-color: #ffffff;
267
+ border: 1px solid #e8f5e8;
268
+ border-radius: 12px;
269
+ padding: 24px;
270
+ margin-bottom: 20px;
271
+ }
272
+
273
+ .card h3 {
274
+ font-size: 18px;
275
+ font-weight: 600;
276
+ color: #2e7d32;
277
+ margin-bottom: 20px;
278
+ }
279
+
280
+ /* 表单样式 */
281
+ .form-group {
282
+ margin-bottom: 20px;
283
+ }
284
+
285
+ .form-row {
286
+ display: grid;
287
+ grid-template-columns: 1fr 1fr;
288
+ gap: 20px;
289
+ }
290
+
291
+ .form-group label {
292
+ display: block;
293
+ font-size: 14px;
294
+ font-weight: 500;
295
+ color: #333333;
296
+ margin-bottom: 8px;
297
+ }
298
+
299
+ .form-group input,
300
+ .form-group textarea,
301
+ .form-group select {
302
+ width: 100%;
303
+ padding: 12px;
304
+ border: 1px solid #e0e0e0;
305
+ border-radius: 8px;
306
+ font-size: 14px;
307
+ transition: all 0.2s ease;
308
+ background-color: #ffffff;
309
+ }
310
+
311
+ .form-group input:focus,
312
+ .form-group textarea:focus,
313
+ .form-group select:focus {
314
+ outline: none;
315
+ border-color: #81c784;
316
+ box-shadow: 0 0 0 3px rgba(129, 199, 132, 0.1);
317
+ }
318
+
319
+ .form-actions {
320
+ display: flex;
321
+ gap: 12px;
322
+ margin-top: 30px;
323
+ }
324
+
325
+ /* Functions网格 */
326
+ .functions-grid {
327
+ display: grid;
328
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
329
+ gap: 20px;
330
+ }
331
+
332
+ .function-card {
333
+ background-color: #ffffff;
334
+ border: 1px solid #e8f5e8;
335
+ border-radius: 12px;
336
+ padding: 20px;
337
+ position: relative;
338
+ }
339
+
340
+ .function-card-header {
341
+ display: flex;
342
+ justify-content: space-between;
343
+ align-items: flex-start;
344
+ margin-bottom: 12px;
345
+ }
346
+
347
+ .function-card h4 {
348
+ font-size: 16px;
349
+ font-weight: 600;
350
+ color: #2e7d32;
351
+ margin-bottom: 4px;
352
+ }
353
+
354
+ .function-card p {
355
+ color: #666666;
356
+ font-size: 14px;
357
+ margin-bottom: 16px;
358
+ }
359
+
360
+ .function-method {
361
+ display: inline-block;
362
+ padding: 4px 8px;
363
+ border-radius: 4px;
364
+ font-size: 12px;
365
+ font-weight: 600;
366
+ text-transform: uppercase;
367
+ }
368
+
369
+ .method-get {
370
+ background-color: #e8f5e8;
371
+ color: #2e7d32;
372
+ }
373
+
374
+ .method-post {
375
+ background-color: #fff3e0;
376
+ color: #f57c00;
377
+ }
378
+
379
+ .method-put {
380
+ background-color: #e3f2fd;
381
+ color: #1976d2;
382
+ }
383
+
384
+ .method-delete {
385
+ background-color: #ffebee;
386
+ color: #d32f2f;
387
+ }
388
+
389
+ .function-actions {
390
+ display: flex;
391
+ gap: 8px;
392
+ margin-top: 16px;
393
+ }
394
+
395
+ .function-actions .btn {
396
+ padding: 6px 12px;
397
+ font-size: 12px;
398
+ }
399
+
400
+ .function-url {
401
+ margin-bottom: 16px;
402
+ }
403
+
404
+ .function-url code {
405
+ background-color: #f5f5f5;
406
+ padding: 4px 8px;
407
+ border-radius: 4px;
408
+ font-size: 12px;
409
+ color: #666666;
410
+ }
411
+
412
+ /* 表格样式 */
413
+ .sessions-table {
414
+ background-color: #ffffff;
415
+ border: 1px solid #e8f5e8;
416
+ border-radius: 12px;
417
+ overflow: hidden;
418
+ }
419
+
420
+ .sessions-table table {
421
+ width: 100%;
422
+ border-collapse: collapse;
423
+ }
424
+
425
+ .sessions-table th,
426
+ .sessions-table td {
427
+ padding: 12px 16px;
428
+ text-align: left;
429
+ border-bottom: 1px solid #f0f0f0;
430
+ }
431
+
432
+ .sessions-table th {
433
+ background-color: #f8f9fa;
434
+ font-weight: 600;
435
+ color: #333333;
436
+ font-size: 14px;
437
+ }
438
+
439
+ .sessions-table td {
440
+ font-size: 14px;
441
+ color: #666666;
442
+ }
443
+
444
+ .session-status {
445
+ display: inline-block;
446
+ padding: 4px 8px;
447
+ border-radius: 4px;
448
+ font-size: 12px;
449
+ font-weight: 500;
450
+ }
451
+
452
+ .status-active {
453
+ background-color: #e8f5e8;
454
+ color: #2e7d32;
455
+ }
456
+
457
+ .status-pending {
458
+ background-color: #fff3e0;
459
+ color: #f57c00;
460
+ }
461
+
462
+ .status-idle {
463
+ background-color: #f5f5f5;
464
+ color: #666666;
465
+ }
466
+
467
+ /* 日志容器 */
468
+ .logs-container {
469
+ background-color: #ffffff;
470
+ border: 1px solid #e8f5e8;
471
+ border-radius: 12px;
472
+ padding: 20px;
473
+ height: 600px;
474
+ overflow-y: auto;
475
+ }
476
+
477
+ #logs-content {
478
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
479
+ font-size: 12px;
480
+ line-height: 1.4;
481
+ color: #333333;
482
+ white-space: pre-wrap;
483
+ word-wrap: break-word;
484
+ }
485
+
486
+ /* 模态框样式 */
487
+ .modal {
488
+ display: none;
489
+ position: fixed;
490
+ top: 0;
491
+ left: 0;
492
+ width: 100%;
493
+ height: 100%;
494
+ background-color: rgba(0, 0, 0, 0.5);
495
+ z-index: 1000;
496
+ }
497
+
498
+ .modal.active {
499
+ display: flex;
500
+ align-items: center;
501
+ justify-content: center;
502
+ padding: 20px;
503
+ }
504
+
505
+ .modal-content {
506
+ background-color: #ffffff;
507
+ border-radius: 12px;
508
+ width: 100%;
509
+ max-width: 600px;
510
+ max-height: 90vh;
511
+ overflow-y: auto;
512
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
513
+ }
514
+
515
+ .modal-header {
516
+ display: flex;
517
+ justify-content: space-between;
518
+ align-items: center;
519
+ padding: 20px 24px;
520
+ border-bottom: 1px solid #f0f0f0;
521
+ }
522
+
523
+ .modal-header h3 {
524
+ font-size: 18px;
525
+ font-weight: 600;
526
+ color: #2e7d32;
527
+ }
528
+
529
+ .modal-close {
530
+ background: none;
531
+ border: none;
532
+ color: #666666;
533
+ cursor: pointer;
534
+ padding: 4px;
535
+ border-radius: 4px;
536
+ transition: all 0.2s ease;
537
+ }
538
+
539
+ .modal-close:hover {
540
+ background-color: #f5f5f5;
541
+ color: #333333;
542
+ }
543
+
544
+ .modal-body {
545
+ padding: 24px;
546
+ }
547
+
548
+ .modal-footer {
549
+ display: flex;
550
+ justify-content: flex-end;
551
+ gap: 12px;
552
+ padding: 20px 24px;
553
+ border-top: 1px solid #f0f0f0;
554
+ }
555
+
556
+ /* 通知样式 */
557
+ .notification {
558
+ position: fixed;
559
+ top: 20px;
560
+ right: 20px;
561
+ padding: 16px 20px;
562
+ border-radius: 8px;
563
+ color: #ffffff;
564
+ font-weight: 500;
565
+ z-index: 1100;
566
+ transform: translateX(400px);
567
+ transition: transform 0.3s ease;
568
+ }
569
+
570
+ .notification.show {
571
+ transform: translateX(0);
572
+ }
573
+
574
+ .notification.success {
575
+ background-color: #4caf50;
576
+ }
577
+
578
+ .notification.error {
579
+ background-color: #f44336;
580
+ }
581
+
582
+ .notification.warning {
583
+ background-color: #ff9800;
584
+ }
585
+
586
+ .notification.info {
587
+ background-color: #2196f3;
588
+ }
589
+
590
+ /* 代码块样式 */
591
+ pre {
592
+ background-color: #f8f9fa;
593
+ border: 1px solid #e9ecef;
594
+ border-radius: 8px;
595
+ padding: 16px;
596
+ overflow-x: auto;
597
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
598
+ font-size: 13px;
599
+ line-height: 1.4;
600
+ }
601
+
602
+ /* 空状态 */
603
+ .empty-state {
604
+ text-align: center;
605
+ padding: 60px 20px;
606
+ color: #666666;
607
+ }
608
+
609
+ .empty-state svg {
610
+ width: 48px;
611
+ height: 48px;
612
+ margin-bottom: 16px;
613
+ opacity: 0.5;
614
+ }
615
+
616
+ .empty-state h3 {
617
+ font-size: 18px;
618
+ margin-bottom: 8px;
619
+ color: #333333;
620
+ }
621
+
622
+ .empty-state p {
623
+ font-size: 14px;
624
+ margin-bottom: 20px;
625
+ }
626
+
627
+ /* 响应式设计 */
628
+ @media (max-width: 768px) {
629
+ .sidebar {
630
+ display: none;
631
+ }
632
+
633
+ .bottom-nav {
634
+ display: flex;
635
+ }
636
+
637
+ .main-content {
638
+ margin-left: 0;
639
+ padding-bottom: 70px;
640
+ }
641
+
642
+ .page {
643
+ padding: 20px;
644
+ }
645
+
646
+ .page-header {
647
+ flex-direction: column;
648
+ align-items: flex-start;
649
+ gap: 12px;
650
+ }
651
+
652
+ .page-header h1 {
653
+ font-size: 24px;
654
+ }
655
+
656
+ .stats-grid {
657
+ grid-template-columns: 1fr;
658
+ gap: 16px;
659
+ }
660
+
661
+ .form-row {
662
+ grid-template-columns: 1fr;
663
+ gap: 16px;
664
+ }
665
+
666
+ .functions-grid {
667
+ grid-template-columns: 1fr;
668
+ gap: 16px;
669
+ }
670
+
671
+ .form-actions {
672
+ flex-direction: column;
673
+ }
674
+
675
+ .btn {
676
+ justify-content: center;
677
+ }
678
+
679
+ .modal-content {
680
+ margin: 10px;
681
+ max-width: none;
682
+ }
683
+
684
+ .modal-footer {
685
+ flex-direction: column;
686
+ }
687
+
688
+ .sessions-table {
689
+ overflow-x: auto;
690
+ }
691
+
692
+ .sessions-table table {
693
+ min-width: 600px;
694
+ }
695
+ }
696
+
697
+ @media (max-width: 480px) {
698
+ .page {
699
+ padding: 16px;
700
+ }
701
+
702
+ .card {
703
+ padding: 16px;
704
+ }
705
+
706
+ .stat-card {
707
+ padding: 16px;
708
+ }
709
+
710
+ .modal-body,
711
+ .modal-header,
712
+ .modal-footer {
713
+ padding: 16px;
714
+ }
715
+ }
716
+ </style>
717
+ </head>
718
+ <body>
719
+ <div class="app-container">
720
+ <!-- PC端侧边栏 -->
721
+ <nav class="sidebar">
722
+ <div class="sidebar-header">
723
+ <div class="logo">
724
+ <svg width="32" height="32" viewBox="0 0 32 32">
725
+ <rect x="4" y="4" width="24" height="24" rx="4" fill="#90EE90" stroke="#4CAF50" stroke-width="2"/>
726
+ <circle cx="12" cy="12" r="2" fill="#fff"/>
727
+ <circle cx="20" cy="12" r="2" fill="#fff"/>
728
+ <path d="M10 20 Q16 24 22 20" stroke="#fff" stroke-width="2" fill="none"/>
729
+ </svg>
730
+ <span>AI助手管理</span>
731
+ </div>
732
+ </div>
733
+ <ul class="nav-menu">
734
+ <li class="nav-item active" data-page="dashboard">
735
+ <svg width="20" height="20" viewBox="0 0 20 20">
736
+ <rect x="2" y="2" width="7" height="7" rx="1" fill="currentColor"/>
737
+ <rect x="11" y="2" width="7" height="7" rx="1" fill="currentColor"/>
738
+ <rect x="2" y="11" width="7" height="7" rx="1" fill="currentColor"/>
739
+ <rect x="11" y="11" width="7" height="7" rx="1" fill="currentColor"/>
740
+ </svg>
741
+ <span>仪表盘</span>
742
+ </li>
743
+ <li class="nav-item" data-page="config">
744
+ <svg width="20" height="20" viewBox="0 0 20 20">
745
+ <circle cx="10" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
746
+ <path d="M10 1v2m0 14v2M18.66 5.34l-1.41 1.41M4.75 14.25l-1.41 1.41M1 10h2m14 0h2M18.66 14.66l-1.41-1.41M4.75 5.75l-1.41-1.41" stroke="currentColor" stroke-width="2"/>
747
+ </svg>
748
+ <span>系统配置</span>
749
+ </li>
750
+ <li class="nav-item" data-page="functions">
751
+ <svg width="20" height="20" viewBox="0 0 20 20">
752
+ <rect x="2" y="6" width="16" height="8" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
753
+ <path d="M6 10h8m-4-2v4" stroke="currentColor" stroke-width="2"/>
754
+ </svg>
755
+ <span>Function管理</span>
756
+ </li>
757
+ <li class="nav-item" data-page="sessions">
758
+ <svg width="20" height="20" viewBox="0 0 20 20">
759
+ <circle cx="6" cy="6" r="4" fill="none" stroke="currentColor" stroke-width="2"/>
760
+ <path d="M2 18v-2a4 4 0 0 1 8 0v2" stroke="currentColor" stroke-width="2"/>
761
+ <circle cx="16" cy="7" r="2" fill="currentColor"/>
762
+ <path d="M13 18v-1a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v1" stroke="currentColor" stroke-width="2"/>
763
+ </svg>
764
+ <span>用户会话</span>
765
+ </li>
766
+ <li class="nav-item" data-page="logs">
767
+ <svg width="20" height="20" viewBox="0 0 20 20">
768
+ <rect x="3" y="2" width="14" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
769
+ <path d="M7 6h6m-6 4h6m-6 4h4" stroke="currentColor" stroke-width="2"/>
770
+ </svg>
771
+ <span>系统日志</span>
772
+ </li>
773
+ </ul>
774
+ </nav>
775
+
776
+ <!-- 移动端底部导航 -->
777
+ <nav class="bottom-nav">
778
+ <div class="nav-item active" data-page="dashboard">
779
+ <svg width="20" height="20" viewBox="0 0 20 20">
780
+ <rect x="2" y="2" width="7" height="7" rx="1" fill="currentColor"/>
781
+ <rect x="11" y="2" width="7" height="7" rx="1" fill="currentColor"/>
782
+ <rect x="2" y="11" width="7" height="7" rx="1" fill="currentColor"/>
783
+ <rect x="11" y="11" width="7" height="7" rx="1" fill="currentColor"/>
784
+ </svg>
785
+ <span>仪表盘</span>
786
+ </div>
787
+ <div class="nav-item" data-page="config">
788
+ <svg width="20" height="20" viewBox="0 0 20 20">
789
+ <circle cx="10" cy="10" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
790
+ <path d="M10 1v2m0 14v2M18.66 5.34l-1.41 1.41M4.75 14.25l-1.41 1.41M1 10h2m14 0h2M18.66 14.66l-1.41-1.41M4.75 5.75l-1.41-1.41" stroke="currentColor" stroke-width="2"/>
791
+ </svg>
792
+ <span>配置</span>
793
+ </div>
794
+ <div class="nav-item" data-page="functions">
795
+ <svg width="20" height="20" viewBox="0 0 20 20">
796
+ <rect x="2" y="6" width="16" height="8" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
797
+ <path d="M6 10h8m-4-2v4" stroke="currentColor" stroke-width="2"/>
798
+ </svg>
799
+ <span>Functions</span>
800
+ </div>
801
+ <div class="nav-item" data-page="sessions">
802
+ <svg width="20" height="20" viewBox="0 0 20 20">
803
+ <circle cx="6" cy="6" r="4" fill="none" stroke="currentColor" stroke-width="2"/>
804
+ <path d="M2 18v-2a4 4 0 0 1 8 0v2" stroke="currentColor" stroke-width="2"/>
805
+ <circle cx="16" cy="7" r="2" fill="currentColor"/>
806
+ <path d="M13 18v-1a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v1" stroke="currentColor" stroke-width="2"/>
807
+ </svg>
808
+ <span>会话</span>
809
+ </div>
810
+ <div class="nav-item" data-page="logs">
811
+ <svg width="20" height="20" viewBox="0 0 20 20">
812
+ <rect x="3" y="2" width="14" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
813
+ <path d="M7 6h6m-6 4h6m-6 4h4" stroke="currentColor" stroke-width="2"/>
814
+ </svg>
815
+ <span>日志</span>
816
+ </div>
817
+ </nav>
818
+
819
+ <!-- 主内容区域 -->
820
+ <main class="main-content">
821
+ <!-- 仪表盘页面 -->
822
+ <div class="page active" id="dashboard">
823
+ <div class="page-header">
824
+ <div>
825
+ <h1>系统仪表盘</h1>
826
+ <p>微信AI助手运行状态监控</p>
827
+ </div>
828
+ </div>
829
+ <div class="stats-grid">
830
+ <div class="stat-card">
831
+ <div class="stat-icon">
832
+ <svg width="24" height="24" viewBox="0 0 24 24">
833
+ <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/>
834
+ <path d="M8 12l2 2 4-4" stroke="currentColor" stroke-width="2"/>
835
+ </svg>
836
+ </div>
837
+ <div class="stat-content">
838
+ <h3>服务状态</h3>
839
+ <p class="stat-value" id="service-status">运行中</p>
840
+ </div>
841
+ </div>
842
+ <div class="stat-card">
843
+ <div class="stat-icon">
844
+ <svg width="24" height="24" viewBox="0 0 24 24">
845
+ <circle cx="6" cy="6" r="4" fill="none" stroke="currentColor" stroke-width="2"/>
846
+ <path d="M2 18v-2a4 4 0 0 1 8 0v2" stroke="currentColor" stroke-width="2"/>
847
+ <circle cx="18" cy="9" r="3" fill="currentColor"/>
848
+ </svg>
849
+ </div>
850
+ <div class="stat-content">
851
+ <h3>活跃用户</h3>
852
+ <p class="stat-value" id="active-users">0</p>
853
+ </div>
854
+ </div>
855
+ <div class="stat-card">
856
+ <div class="stat-icon">
857
+ <svg width="24" height="24" viewBox="0 0 24 24">
858
+ <rect x="2" y="6" width="20" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
859
+ <path d="M6 10h12" stroke="currentColor" stroke-width="2"/>
860
+ </svg>
861
+ </div>
862
+ <div class="stat-content">
863
+ <h3>处理中任务</h3>
864
+ <p class="stat-value" id="pending-tasks">0</p>
865
+ </div>
866
+ </div>
867
+ <div class="stat-card">
868
+ <div class="stat-icon">
869
+ <svg width="24" height="24" viewBox="0 0 24 24">
870
+ <rect x="2" y="6" width="16" height="8" rx="2" fill="none" stroke="currentColor" stroke-width="2"/>
871
+ <path d="M6 10h8" stroke="currentColor" stroke-width="2"/>
872
+ </svg>
873
+ </div>
874
+ <div class="stat-content">
875
+ <h3>可用Functions</h3>
876
+ <p class="stat-value" id="function-count">0</p>
877
+ </div>
878
+ </div>
879
+ </div>
880
+ </div>
881
+
882
+ <!-- 系统配置页面 -->
883
+ <div class="page" id="config">
884
+ <div class="page-header">
885
+ <div>
886
+ <h1>系统配置</h1>
887
+ <p>管理AI模型和系统参数</p>
888
+ </div>
889
+ </div>
890
+ <div class="config-section">
891
+ <div class="card">
892
+ <h3>AI模型配置</h3>
893
+ <div class="form-group">
894
+ <label for="text-model">文本对话模型</label>
895
+ <input type="text" id="text-model" placeholder="gpt-4.1-mini">
896
+ </div>
897
+ <div class="form-group">
898
+ <label for="image-model">图像识别模型</label>
899
+ <input type="text" id="image-model" placeholder="gpt-4.1-mini">
900
+ </div>
901
+ <div class="form-group">
902
+ <label for="image-gen-model">图像生成模型</label>
903
+ <input type="text" id="image-gen-model" placeholder="dall-e-3">
904
+ </div>
905
+ </div>
906
+ <div class="card">
907
+ <h3>系统提示词</h3>
908
+ <div class="form-group">
909
+ <label for="system-prompt">AI助手角色设定</label>
910
+ <textarea id="system-prompt" rows="5" placeholder="输入系统提示词..."></textarea>
911
+ </div>
912
+ </div>
913
+ <div class="card">
914
+ <h3>系统参数</h3>
915
+ <div class="form-row">
916
+ <div class="form-group">
917
+ <label for="max-workers">最大并发数</label>
918
+ <input type="number" id="max-workers" min="1" max="50">
919
+ </div>
920
+ <div class="form-group">
921
+ <label for="request-timeout">请求超时时间(秒)</label>
922
+ <input type="number" id="request-timeout" min="10" max="300">
923
+ </div>
924
+ </div>
925
+ <div class="form-row">
926
+ <div class="form-group">
927
+ <label for="max-message-length">消息最大长度</label>
928
+ <input type="number" id="max-message-length" min="100" max="2000">
929
+ </div>
930
+ <div class="form-group">
931
+ <label for="session-timeout">会话超时时间(秒)</label>
932
+ <input type="number" id="session-timeout" min="300" max="7200">
933
+ </div>
934
+ </div>
935
+ </div>
936
+ <div class="form-actions">
937
+ <button class="btn btn-primary" onclick="saveConfig()">保存配置</button>
938
+ <button class="btn btn-secondary" onclick="loadConfig()">重新加载</button>
939
+ </div>
940
+ </div>
941
+ </div>
942
+
943
+ <!-- Function管理页面 -->
944
+ <div class="page" id="functions">
945
+ <div class="page-header">
946
+ <div>
947
+ <h1>Function管理</h1>
948
+ <p>添加和管理自定义Function</p>
949
+ </div>
950
+ <button class="btn btn-primary" onclick="showAddFunctionModal()">
951
+ <svg width="16" height="16" viewBox="0 0 16 16">
952
+ <path d="M8 1v14m-7-7h14" stroke="currentColor" stroke-width="2"/>
953
+ </svg>
954
+ 添加Function
955
+ </button>
956
+ </div>
957
+ <div class="functions-grid" id="functions-list">
958
+ <!-- Functions列表将在这里动态生成 -->
959
+ </div>
960
+ </div>
961
+
962
+ <!-- 用户会话页面 -->
963
+ <div class="page" id="sessions">
964
+ <div class="page-header">
965
+ <div>
966
+ <h1>用户会话</h1>
967
+ <p>查看和管理用户对话会话</p>
968
+ </div>
969
+ <button class="btn btn-secondary" onclick="refreshSessions()">
970
+ <svg width="16" height="16" viewBox="0 0 16 16">
971
+ <path d="M1 4v6h6m8-6v6h-6M15 4a8 8 0 0 0-8-8" stroke="currentColor" stroke-width="2" fill="none"/>
972
+ </svg>
973
+ 刷新
974
+ </button>
975
+ </div>
976
+ <div class="sessions-table">
977
+ <table>
978
+ <thead>
979
+ <tr>
980
+ <th>用户ID</th>
981
+ <th>最后活跃时间</th>
982
+ <th>消息数量</th>
983
+ <th>状态</th>
984
+ <th>操作</th>
985
+ </tr>
986
+ </thead>
987
+ <tbody id="sessions-tbody">
988
+ <!-- 会话列表将在这里动态生成 -->
989
+ </tbody>
990
+ </table>
991
+ </div>
992
+ </div>
993
+
994
+ <!-- 系统日志页面 -->
995
+ <div class="page" id="logs">
996
+ <div class="page-header">
997
+ <div>
998
+ <h1>系统日志</h1>
999
+ <p>查看系统运行日志</p>
1000
+ </div>
1001
+ <button class="btn btn-secondary" onclick="refreshLogs()">
1002
+ <svg width="16" height="16" viewBox="0 0 16 16">
1003
+ <path d="M1 4v6h6m8-6v6h-6M15 4a8 8 0 0 0-8-8" stroke="currentColor" stroke-width="2" fill="none"/>
1004
+ </svg>
1005
+ 刷新
1006
+ </button>
1007
+ </div>
1008
+ <div class="logs-container">
1009
+ <pre id="logs-content">加载中...</pre>
1010
+ </div>
1011
+ </div>
1012
+ </main>
1013
+ </div>
1014
+
1015
+ <!-- Function添加/编辑模态框 -->
1016
+ <div class="modal" id="function-modal">
1017
+ <div class="modal-content">
1018
+ <div class="modal-header">
1019
+ <h3 id="function-modal-title">添加Function</h3>
1020
+ <button class="modal-close" onclick="closeFunctionModal()">
1021
+ <svg width="20" height="20" viewBox="0 0 20 20">
1022
+ <path d="M15 5L5 15m0-10l10 10" stroke="currentColor" stroke-width="2"/>
1023
+ </svg>
1024
+ </button>
1025
+ </div>
1026
+ <div class="modal-body">
1027
+ <form id="function-form">
1028
+ <div class="form-group">
1029
+ <label for="func-name">Function名称</label>
1030
+ <input type="text" id="func-name" required>
1031
+ </div>
1032
+ <div class="form-group">
1033
+ <label for="func-description">描述</label>
1034
+ <textarea id="func-description" rows="2" required></textarea>
1035
+ </div>
1036
+ <div class="form-group">
1037
+ <label for="func-method">HTTP方法</label>
1038
+ <select id="func-method">
1039
+ <option value="GET">GET</option>
1040
+ <option value="POST">POST</option>
1041
+ <option value="PUT">PUT</option>
1042
+ <option value="DELETE">DELETE</option>
1043
+ </select>
1044
+ </div>
1045
+ <div class="form-group">
1046
+ <label for="func-url">请求URL</label>
1047
+ <input type="url" id="func-url" placeholder="https://api.example.com/endpoint" required>
1048
+ </div>
1049
+ <div class="form-group">
1050
+ <label for="func-headers">请求头(JSON格式)</label>
1051
+ <textarea id="func-headers" rows="3" placeholder='{"Content-Type": "application/json"}'></textarea>
1052
+ </div>
1053
+ <div class="form-group">
1054
+ <label for="func-body">请求体模板(JSON格式)</label>
1055
+ <textarea id="func-body" rows="4" placeholder='{"param1": "{value1}", "param2": "{value2}"}'></textarea>
1056
+ </div>
1057
+ <div class="form-group">
1058
+ <label for="func-parameters">参数定义(JSON Schema)</label>
1059
+ <textarea id="func-parameters" rows="6" placeholder='{"type": "object", "properties": {"value1": {"type": "string", "description": "参数描述"}}, "required": ["value1"]}'></textarea>
1060
+ </div>
1061
+ <div class="form-group">
1062
+ <label for="func-response-type">响应类型</label>
1063
+ <select id="func-response-type">
1064
+ <option value="text">文本</option>
1065
+ <option value="image">图片</option>
1066
+ </select>
1067
+ </div>
1068
+ </form>
1069
+ </div>
1070
+ <div class="modal-footer">
1071
+ <button type="button" class="btn btn-secondary" onclick="closeFunctionModal()">取消</button>
1072
+ <button type="button" class="btn btn-primary" onclick="testFunction()">测试</button>
1073
+ <button type="button" class="btn btn-primary" onclick="saveFunction()">保存</button>
1074
+ </div>
1075
+ </div>
1076
+ </div>
1077
+
1078
+ <!-- 测试结果模态框 -->
1079
+ <div class="modal" id="test-modal">
1080
+ <div class="modal-content">
1081
+ <div class="modal-header">
1082
+ <h3>测试结果</h3>
1083
+ <button class="modal-close" onclick="closeTestModal()">
1084
+ <svg width="20" height="20" viewBox="0 0 20 20">
1085
+ <path d="M15 5L5 15m0-10l10 10" stroke="currentColor" stroke-width="2"/>
1086
+ </svg>
1087
+ </button>
1088
+ </div>
1089
+ <div class="modal-body">
1090
+ <div class="form-group">
1091
+ <label>测试参数(JSON格式)</label>
1092
+ <textarea id="test-params" rows="4" placeholder='{"param1": "value1"}'></textarea>
1093
+ </div>
1094
+ <div class="form-group">
1095
+ <label>测试结果</label>
1096
+ <pre id="test-result">等待测试...</pre>
1097
+ </div>
1098
+ </div>
1099
+ <div class="modal-footer">
1100
+ <button type="button" class="btn btn-secondary" onclick="closeTestModal()">关闭</button>
1101
+ <button type="button" class="btn btn-primary" onclick="runTest()">执行测试</button>
1102
+ </div>
1103
+ </div>
1104
+ </div>
1105
+
1106
+ <script>
1107
+ // 全局状态
1108
+ let currentConfig = {};
1109
+ let currentEditingFunction = null;
1110
+
1111
+ // 页面初始化
1112
+ document.addEventListener('DOMContentLoaded', function() {
1113
+ initNavigation();
1114
+ loadConfig();
1115
+ loadDashboard();
1116
+
1117
+ // 定期刷新仪表盘数据
1118
+ setInterval(loadDashboard, 30000);
1119
+ });
1120
+
1121
+ // 导航功能
1122
+ function initNavigation() {
1123
+ // PC端导航
1124
+ const sidebarItems = document.querySelectorAll('.sidebar .nav-item');
1125
+ sidebarItems.forEach(item => {
1126
+ item.addEventListener('click', () => {
1127
+ const page = item.dataset.page;
1128
+ switchPage(page);
1129
+ updateActiveNav(item, '.sidebar .nav-item');
1130
+ });
1131
+ });
1132
+
1133
+ // 移动端导航
1134
+ const bottomNavItems = document.querySelectorAll('.bottom-nav .nav-item');
1135
+ bottomNavItems.forEach(item => {
1136
+ item.addEventListener('click', () => {
1137
+ const page = item.dataset.page;
1138
+ switchPage(page);
1139
+ updateActiveNav(item, '.bottom-nav .nav-item');
1140
+ });
1141
+ });
1142
+ }
1143
+
1144
+ function switchPage(pageId) {
1145
+ // 隐藏所有页面
1146
+ document.querySelectorAll('.page').forEach(page => {
1147
+ page.classList.remove('active');
1148
+ });
1149
+
1150
+ // 显示目标页面
1151
+ const targetPage = document.getElementById(pageId);
1152
+ if (targetPage) {
1153
+ targetPage.classList.add('active');
1154
+
1155
+ // 加载页面数据
1156
+ switch(pageId) {
1157
+ case 'dashboard':
1158
+ loadDashboard();
1159
+ break;
1160
+ case 'config':
1161
+ loadConfig();
1162
+ break;
1163
+ case 'functions':
1164
+ loadFunctions();
1165
+ break;
1166
+ case 'sessions':
1167
+ loadSessions();
1168
+ break;
1169
+ case 'logs':
1170
+ loadLogs();
1171
+ break;
1172
+ }
1173
+ }
1174
+ }
1175
+
1176
+ function updateActiveNav(activeItem, selector) {
1177
+ document.querySelectorAll(selector).forEach(item => {
1178
+ item.classList.remove('active');
1179
+ });
1180
+ activeItem.classList.add('active');
1181
+ }
1182
+
1183
+ // 仪表盘功能
1184
+ async function loadDashboard() {
1185
+ try {
1186
+ // 加载会话数据
1187
+ const sessionsResponse = await fetch('/api/sessions');
1188
+ const sessions = await sessionsResponse.json();
1189
+
1190
+ // 加载配置数据
1191
+ const configResponse = await fetch('/api/config');
1192
+ const config = await configResponse.json();
1193
+
1194
+ // 更新统计数据
1195
+ document.getElementById('service-status').textContent = '运行中';
1196
+ document.getElementById('active-users').textContent = sessions.length;
1197
+ document.getElementById('pending-tasks').textContent = sessions.filter(s => s.current_task).length;
1198
+ document.getElementById('function-count').textContent = config.functions ? config.functions.length : 0;
1199
+
1200
+ } catch (error) {
1201
+ console.error('加载仪表盘数据失败:', error);
1202
+ showNotification('加载仪表盘数据失败', 'error');
1203
+ }
1204
+ }
1205
+
1206
+ // 配置管理功能
1207
+ async function loadConfig() {
1208
+ try {
1209
+ const response = await fetch('/api/config');
1210
+ const config = await response.json();
1211
+ currentConfig = config;
1212
+
1213
+ // 填充表单
1214
+ document.getElementById('text-model').value = config.models.text_model || '';
1215
+ document.getElementById('image-model').value = config.models.image_model || '';
1216
+ document.getElementById('image-gen-model').value = config.models.image_generation_model || '';
1217
+ document.getElementById('system-prompt').value = config.system_prompt || '';
1218
+ document.getElementById('max-workers').value = config.settings.max_workers || 10;
1219
+ document.getElementById('request-timeout').value = config.settings.request_timeout || 60;
1220
+ document.getElementById('max-message-length').value = config.settings.max_message_length || 500;
1221
+ document.getElementById('session-timeout').value = config.settings.session_timeout || 3600;
1222
+
1223
+ } catch (error) {
1224
+ console.error('加载配置失败:', error);
1225
+ showNotification('加载配置失败', 'error');
1226
+ }
1227
+ }
1228
+
1229
+ async function saveConfig() {
1230
+ try {
1231
+ const config = {
1232
+ models: {
1233
+ text_model: document.getElementById('text-model').value,
1234
+ image_model: document.getElementById('image-model').value,
1235
+ image_generation_model: document.getElementById('image-gen-model').value
1236
+ },
1237
+ system_prompt: document.getElementById('system-prompt').value,
1238
+ settings: {
1239
+ max_workers: parseInt(document.getElementById('max-workers').value),
1240
+ request_timeout: parseInt(document.getElementById('request-timeout').value),
1241
+ max_message_length: parseInt(document.getElementById('max-message-length').value),
1242
+ session_timeout: parseInt(document.getElementById('session-timeout').value)
1243
+ },
1244
+ functions: currentConfig.functions || []
1245
+ };
1246
+
1247
+ const response = await fetch('/api/config', {
1248
+ method: 'POST',
1249
+ headers: {
1250
+ 'Content-Type': 'application/json'
1251
+ },
1252
+ body: JSON.stringify(config)
1253
+ });
1254
+
1255
+ const result = await response.json();
1256
+ if (result.success) {
1257
+ showNotification('配置保存成功', 'success');
1258
+ currentConfig = config;
1259
+ } else {
1260
+ showNotification(result.message, 'error');
1261
+ }
1262
+
1263
+ } catch (error) {
1264
+ console.error('保存配置失败:', error);
1265
+ showNotification('保存配置失败', 'error');
1266
+ }
1267
+ }
1268
+
1269
+ // Function管理功能
1270
+ async function loadFunctions() {
1271
+ try {
1272
+ const response = await fetch('/api/config');
1273
+ const config = await response.json();
1274
+ const functions = config.functions || [];
1275
+
1276
+ const container = document.getElementById('functions-list');
1277
+
1278
+ if (functions.length === 0) {
1279
+ container.innerHTML = `
1280
+ <div class="empty-state">
1281
+ <svg viewBox="0 0 48 48">
1282
+ <rect x="8" y="16" width="32" height="16" rx="4" fill="none" stroke="currentColor" stroke-width="2"/>
1283
+ <path d="M16 24h16m-8-4v8" stroke="currentColor" stroke-width="2"/>
1284
+ </svg>
1285
+ <h3>暂无Function</h3>
1286
+ <p>点击右上角按钮添加您的第一个Function</p>
1287
+ </div>
1288
+ `;
1289
+ return;
1290
+ }
1291
+
1292
+ container.innerHTML = functions.map(func => `
1293
+ <div class="function-card">
1294
+ <div class="function-card-header">
1295
+ <div>
1296
+ <h4>${func.name}</h4>
1297
+ <p>${func.description}</p>
1298
+ </div>
1299
+ <span class="function-method method-${func.http_config.method.toLowerCase()}">
1300
+ ${func.http_config.method}
1301
+ </span>
1302
+ </div>
1303
+ <div class="function-url">
1304
+ <code>${func.http_config.url}</code>
1305
+ </div>
1306
+ <div class="function-actions">
1307
+ <button class="btn btn-secondary" onclick="editFunction('${func.name}')">编辑</button>
1308
+ <button class="btn btn-secondary" onclick="testFunctionDialog('${func.name}')">测试</button>
1309
+ <button class="btn btn-danger" onclick="deleteFunction('${func.name}')">删除</button>
1310
+ </div>
1311
+ </div>
1312
+ `).join('');
1313
+
1314
+ } catch (error) {
1315
+ console.error('加载Functions失败:', error);
1316
+ showNotification('加载Functions失败', 'error');
1317
+ }
1318
+ }
1319
+
1320
+ function showAddFunctionModal() {
1321
+ currentEditingFunction = null;
1322
+ document.getElementById('function-modal-title').textContent = '添加Function';
1323
+ document.getElementById('function-form').reset();
1324
+
1325
+ // 设置默认值
1326
+ document.getElementById('func-headers').value = '{"Content-Type": "application/json"}';
1327
+ document.getElementById('func-parameters').value = JSON.stringify({
1328
+ "type": "object",
1329
+ "properties": {
1330
+ "param1": {
1331
+ "type": "string",
1332
+ "description": "参数描述"
1333
+ }
1334
+ },
1335
+ "required": ["param1"]
1336
+ }, null, 2);
1337
+
1338
+ document.getElementById('function-modal').classList.add('active');
1339
+ }
1340
+
1341
+ function editFunction(functionName) {
1342
+ const func = currentConfig.functions.find(f => f.name === functionName);
1343
+ if (!func) return;
1344
+
1345
+ currentEditingFunction = functionName;
1346
+ document.getElementById('function-modal-title').textContent = '编辑Function';
1347
+
1348
+ // 填充表单
1349
+ document.getElementById('func-name').value = func.name;
1350
+ document.getElementById('func-description').value = func.description;
1351
+ document.getElementById('func-method').value = func.http_config.method;
1352
+ document.getElementById('func-url').value = func.http_config.url;
1353
+ document.getElementById('func-headers').value = JSON.stringify(func.http_config.headers || {}, null, 2);
1354
+ document.getElementById('func-body').value = JSON.stringify(func.http_config.body_template || {}, null, 2);
1355
+ document.getElementById('func-parameters').value = JSON.stringify(func.parameters, null, 2);
1356
+ document.getElementById('func-response-type').value = func.http_config.response_type || 'text';
1357
+
1358
+ document.getElementById('function-modal').classList.add('active');
1359
+ }
1360
+
1361
+ function closeFunctionModal() {
1362
+ document.getElementById('function-modal').classList.remove('active');
1363
+ currentEditingFunction = null;
1364
+ }
1365
+
1366
+ async function saveFunction() {
1367
+ try {
1368
+ const formData = {
1369
+ name: document.getElementById('func-name').value,
1370
+ description: document.getElementById('func-description').value,
1371
+ parameters: JSON.parse(document.getElementById('func-parameters').value),
1372
+ http_config: {
1373
+ method: document.getElementById('func-method').value,
1374
+ url: document.getElementById('func-url').value,
1375
+ headers: JSON.parse(document.getElementById('func-headers').value || '{}'),
1376
+ body_template: JSON.parse(document.getElementById('func-body').value || '{}'),
1377
+ response_type: document.getElementById('func-response-type').value
1378
+ }
1379
+ };
1380
+
1381
+ // 验证必填字段
1382
+ if (!formData.name || !formData.description || !formData.http_config.url) {
1383
+ showNotification('请填写所有必填字段', 'warning');
1384
+ return;
1385
+ }
1386
+
1387
+ // 更新配置
1388
+ let functions = [...(currentConfig.functions || [])];
1389
+
1390
+ if (currentEditingFunction) {
1391
+ // 编辑现有Function
1392
+ const index = functions.findIndex(f => f.name === currentEditingFunction);
1393
+ if (index !== -1) {
1394
+ functions[index] = formData;
1395
+ }
1396
+ } else {
1397
+ // 添加新Function
1398
+ if (functions.find(f => f.name === formData.name)) {
1399
+ showNotification('Function名称已存在', 'warning');
1400
+ return;
1401
+ }
1402
+ functions.push(formData);
1403
+ }
1404
+
1405
+ const newConfig = {
1406
+ ...currentConfig,
1407
+ functions: functions
1408
+ };
1409
+
1410
+ const response = await fetch('/api/config', {
1411
+ method: 'POST',
1412
+ headers: {
1413
+ 'Content-Type': 'application/json'
1414
+ },
1415
+ body: JSON.stringify(newConfig)
1416
+ });
1417
+
1418
+ const result = await response.json();
1419
+ if (result.success) {
1420
+ showNotification('Function保存成功', 'success');
1421
+ currentConfig = newConfig;
1422
+ closeFunctionModal();
1423
+ loadFunctions();
1424
+ } else {
1425
+ showNotification(result.message, 'error');
1426
+ }
1427
+
1428
+ } catch (error) {
1429
+ console.error('保存Function失败:', error);
1430
+ showNotification('保存Function失败,请检查JSON格式', 'error');
1431
+ }
1432
+ }
1433
+
1434
+ async function deleteFunction(functionName) {
1435
+ if (!confirm('确定要删除这个Function吗?')) return;
1436
+
1437
+ try {
1438
+ const functions = currentConfig.functions.filter(f => f.name !== functionName);
1439
+ const newConfig = {
1440
+ ...currentConfig,
1441
+ functions: functions
1442
+ };
1443
+
1444
+ const response = await fetch('/api/config', {
1445
+ method: 'POST',
1446
+ headers: {
1447
+ 'Content-Type': 'application/json'
1448
+ },
1449
+ body: JSON.stringify(newConfig)
1450
+ });
1451
+
1452
+ const result = await response.json();
1453
+ if (result.success) {
1454
+ showNotification('Function删除成功', 'success');
1455
+ currentConfig = newConfig;
1456
+ loadFunctions();
1457
+ } else {
1458
+ showNotification(result.message, 'error');
1459
+ }
1460
+
1461
+ } catch (error) {
1462
+ console.error('删除Function失败:', error);
1463
+ showNotification('删除Function失败', 'error');
1464
+ }
1465
+ }
1466
+
1467
+ // Function测试功能
1468
+ function testFunctionDialog(functionName) {
1469
+ const func = currentConfig.functions.find(f => f.name === functionName);
1470
+ if (!func) return;
1471
+
1472
+ currentEditingFunction = functionName;
1473
+
1474
+ // 生成示例参数
1475
+ const exampleParams = {};
1476
+ if (func.parameters && func.parameters.properties) {
1477
+ Object.keys(func.parameters.properties).forEach(key => {
1478
+ const prop = func.parameters.properties[key];
1479
+ switch(prop.type) {
1480
+ case 'string':
1481
+ exampleParams[key] = '示例文本';
1482
+ break;
1483
+ case 'number':
1484
+ exampleParams[key] = 123;
1485
+ break;
1486
+ case 'boolean':
1487
+ exampleParams[key] = true;
1488
+ break;
1489
+ default:
1490
+ exampleParams[key] = '示例值';
1491
+ }
1492
+ });
1493
+ }
1494
+
1495
+ document.getElementById('test-params').value = JSON.stringify(exampleParams, null, 2);
1496
+ document.getElementById('test-result').textContent = '等待测试...';
1497
+ document.getElementById('test-modal').classList.add('active');
1498
+ }
1499
+
1500
+ function closeTestModal() {
1501
+ document.getElementById('test-modal').classList.remove('active');
1502
+ }
1503
+
1504
+ async function runTest() {
1505
+ try {
1506
+ const params = JSON.parse(document.getElementById('test-params').value);
1507
+
1508
+ document.getElementById('test-result').textContent = '测试中...';
1509
+
1510
+ const response = await fetch('/api/test-function', {
1511
+ method: 'POST',
1512
+ headers: {
1513
+ 'Content-Type': 'application/json'
1514
+ },
1515
+ body: JSON.stringify({
1516
+ function_name: currentEditingFunction,
1517
+ arguments: params
1518
+ })
1519
+ });
1520
+
1521
+ const result = await response.json();
1522
+
1523
+ if (result.success) {
1524
+ document.getElementById('test-result').textContent = JSON.stringify(result.result, null, 2);
1525
+ showNotification('测试成功', 'success');
1526
+ } else {
1527
+ document.getElementById('test-result').textContent = `错误: ${result.error}`;
1528
+ showNotification('测试失败', 'error');
1529
+ }
1530
+
1531
+ } catch (error) {
1532
+ console.error('Function测试失败:', error);
1533
+ document.getElementById('test-result').textContent = `错误: ${error.message}`;
1534
+ showNotification('测试失败,请检查参数格式', 'error');
1535
+ }
1536
+ }
1537
+
1538
+ function testFunction() {
1539
+ // 先保存Function,然后测试
1540
+ saveFunction().then(() => {
1541
+ if (currentEditingFunction) {
1542
+ closeFunctionModal();
1543
+ setTimeout(() => {
1544
+ testFunctionDialog(currentEditingFunction);
1545
+ }, 100);
1546
+ }
1547
+ });
1548
+ }
1549
+
1550
+ // 会话管理功能
1551
+ async function loadSessions() {
1552
+ try {
1553
+ const response = await fetch('/api/sessions');
1554
+ const sessions = await response.json();
1555
+
1556
+ const tbody = document.getElementById('sessions-tbody');
1557
+
1558
+ if (sessions.length === 0) {
1559
+ tbody.innerHTML = `
1560
+ <tr>
1561
+ <td colspan="5" style="text-align: center; padding: 40px; color: #666;">
1562
+ 暂无活跃会话
1563
+ </td>
1564
+ </tr>
1565
+ `;
1566
+ return;
1567
+ }
1568
+
1569
+ tbody.innerHTML = sessions.map(session => {
1570
+ const lastActive = new Date(session.last_active * 1000).toLocaleString();
1571
+ let status = 'idle';
1572
+ let statusText = '空闲';
1573
+
1574
+ if (session.current_task) {
1575
+ status = 'pending';
1576
+ statusText = '处理中';
1577
+ } else if (session.has_pending) {
1578
+ status = 'active';
1579
+ statusText = '活跃';
1580
+ }
1581
+
1582
+ return `
1583
+ <tr>
1584
+ <td>${session.user_id}</td>
1585
+ <td>${lastActive}</td>
1586
+ <td>${session.message_count}</td>
1587
+ <td><span class="session-status status-${status}">${statusText}</span></td>
1588
+ <td>
1589
+ <button class="btn btn-secondary" onclick="clearSession('${session.user_id}')">
1590
+ 清理会话
1591
+ </button>
1592
+ </td>
1593
+ </tr>
1594
+ `;
1595
+ }).join('');
1596
+
1597
+ } catch (error) {
1598
+ console.error('加载会话数据失败:', error);
1599
+ showNotification('加载会话数据失败', 'error');
1600
+ }
1601
+ }
1602
+
1603
+ async function clearSession(userId) {
1604
+ if (!confirm(`确定要清理用户 ${userId} 的会话吗?`)) return;
1605
+
1606
+ try {
1607
+ const response = await fetch(`/api/sessions/${userId}/clear`, {
1608
+ method: 'POST'
1609
+ });
1610
+
1611
+ const result = await response.json();
1612
+ if (result.success) {
1613
+ showNotification('会话清理成功', 'success');
1614
+ loadSessions();
1615
+ } else {
1616
+ showNotification(result.message, 'error');
1617
+ }
1618
+
1619
+ } catch (error) {
1620
+ console.error('清理会话失败:', error);
1621
+ showNotification('清理会话失败', 'error');
1622
+ }
1623
+ }
1624
+
1625
+ function refreshSessions() {
1626
+ loadSessions();
1627
+ }
1628
+
1629
+ // 日志管理功能
1630
+ async function loadLogs() {
1631
+ try {
1632
+ const response = await fetch('/api/logs?lines=200');
1633
+ const result = await response.json();
1634
+
1635
+ if (result.logs) {
1636
+ document.getElementById('logs-content').textContent = result.logs.join('');
1637
+
1638
+ // 滚动到底部
1639
+ const container = document.querySelector('.logs-container');
1640
+ container.scrollTop = container.scrollHeight;
1641
+ } else {
1642
+ document.getElementById('logs-content').textContent = result.error || '加载日志失败';
1643
+ }
1644
+
1645
+ } catch (error) {
1646
+ console.error('加载日志失败:', error);
1647
+ document.getElementById('logs-content').textContent = '加载日志失败';
1648
+ showNotification('加载日志失败', 'error');
1649
+ }
1650
+ }
1651
+
1652
+ function refreshLogs() {
1653
+ loadLogs();
1654
+ }
1655
+
1656
+ // 通知功能
1657
+ function showNotification(message, type = 'info') {
1658
+ const notification = document.createElement('div');
1659
+ notification.className = `notification ${type}`;
1660
+ notification.textContent = message;
1661
+
1662
+ document.body.appendChild(notification);
1663
+
1664
+ // 显示动画
1665
+ setTimeout(() => {
1666
+ notification.classList.add('show');
1667
+ }, 100);
1668
+
1669
+ // 自动隐藏
1670
+ setTimeout(() => {
1671
+ notification.classList.remove('show');
1672
+ setTimeout(() => {
1673
+ document.body.removeChild(notification);
1674
+ }, 300);
1675
+ }, 3000);
1676
+ }
1677
+
1678
+ // 键盘快捷键
1679
+ document.addEventListener('keydown', function(e) {
1680
+ // ESC关闭模态框
1681
+ if (e.key === 'Escape') {
1682
+ document.querySelectorAll('.modal.active').forEach(modal => {
1683
+ modal.classList.remove('active');
1684
+ });
1685
+ }
1686
+
1687
+ // Ctrl+S保存配置
1688
+ if (e.ctrlKey && e.key === 's') {
1689
+ e.preventDefault();
1690
+ const activePage = document.querySelector('.page.active');
1691
+ if (activePage && activePage.id === 'config') {
1692
+ saveConfig();
1693
+ }
1694
+ }
1695
+ });
1696
+
1697
+ // 点击模态框背景关闭
1698
+ document.addEventListener('click', function(e) {
1699
+ if (e.target.classList.contains('modal')) {
1700
+ e.target.classList.remove('active');
1701
+ }
1702
+ });
1703
+ </script>
1704
+ </body>
1705
+ </html>
wechat_service.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, make_response
2
+ import hashlib
3
+ import time
4
+ import xml.etree.ElementTree as ET
5
+ import os
6
+ import json
7
+ from openai import OpenAI
8
+ from dotenv import load_dotenv
9
+ from markdown import markdown
10
+ import re
11
+ import threading
12
+ import logging
13
+ from datetime import datetime
14
+ import asyncio
15
+ from concurrent.futures import ThreadPoolExecutor
16
+ import queue
17
+ import uuid
18
+ import base64
19
+ from Crypto.Cipher import AES
20
+ import struct
21
+ import random
22
+ import string
23
+ import requests
24
+
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format='%(asctime)s - %(levelname)s - %(message)s',
28
+ handlers=[
29
+ logging.FileHandler('wechat_service.log'),
30
+ logging.StreamHandler()
31
+ ]
32
+ )
33
+
34
+ load_dotenv()
35
+
36
+ # Configuration
37
+ TOKEN = os.getenv('TOKEN')
38
+ ENCODING_AES_KEY = os.getenv('ENCODING_AES_KEY')
39
+ APPID = os.getenv('APPID')
40
+ APPSECRET = os.getenv('APPSECRET')
41
+ API_KEY = os.getenv("API_KEY")
42
+ BASE_URL = os.getenv("OPENAI_BASE_URL")
43
+ IMAGE_MODEL_URL = os.getenv("IMAGE_MODEL_URL")
44
+ IMAGE_MODEL_KEY = os.getenv("IMAGE_MODEL_KEY")
45
+
46
+ client = OpenAI(api_key=API_KEY, base_url=BASE_URL)
47
+ executor = ThreadPoolExecutor(max_workers=10)
48
+
49
+ # Define tools for image generation
50
+ TOOLS = [
51
+ {
52
+ "type": "function",
53
+ "function": {
54
+ "name": "generate_image",
55
+ "description": "Generate an image based on text description",
56
+ "parameters": {
57
+ "type": "object",
58
+ "properties": {
59
+ "prompt": {
60
+ "type": "string",
61
+ "description": "The description of the image to generate"
62
+ }
63
+ },
64
+ "required": ["prompt"]
65
+ }
66
+ }
67
+ }
68
+ ]
69
+
70
+ class AccessTokenManager:
71
+ def __init__(self):
72
+ self._access_token = None
73
+ self._expires_at = 0
74
+ self._lock = threading.Lock()
75
+
76
+ def get_token(self):
77
+ with self._lock:
78
+ now = time.time()
79
+ # 提前5分钟刷新token,确保在调用时token都是有效的
80
+ if self._access_token and now < (self._expires_at - 300):
81
+ return self._access_token
82
+
83
+ try:
84
+ url = "https://api.weixin.qq.com/cgi-bin/token"
85
+ params = {
86
+ "grant_type": "client_credential",
87
+ "appid": APPID,
88
+ "secret": APPSECRET
89
+ }
90
+
91
+ logging.info("开始获取新的access_token")
92
+ response = requests.get(url, params=params)
93
+ response.raise_for_status()
94
+ result = response.json()
95
+
96
+ if "access_token" not in result:
97
+ error_msg = f"获取access_token失败: {result}"
98
+ logging.error(error_msg)
99
+ raise ValueError(error_msg)
100
+
101
+ self._access_token = result["access_token"]
102
+ self._expires_at = now + result["expires_in"]
103
+ logging.info("成功获取新的access_token")
104
+
105
+ return self._access_token
106
+
107
+ except Exception as e:
108
+ error_msg = f"获取access_token时发生错误: {str(e)}"
109
+ logging.error(error_msg)
110
+ raise
111
+
112
+ def refresh_token(self):
113
+ with self._lock:
114
+ self._access_token = None
115
+ return self.get_token()
116
+
117
+ class WeChatCrypto:
118
+ def __init__(self, key, app_id):
119
+ self.key = base64.b64decode(key + '=')
120
+ self.app_id = app_id
121
+
122
+ def encrypt(self, text):
123
+ random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
124
+ text_bytes = text.encode('utf-8')
125
+ msg_len = struct.pack('>I', len(text_bytes))
126
+ message = random_str.encode('utf-8') + msg_len + text_bytes + self.app_id.encode('utf-8')
127
+ pad_len = 32 - (len(message) % 32)
128
+ message += chr(pad_len).encode('utf-8') * pad_len
129
+ cipher = AES.new(self.key, AES.MODE_CBC, self.key[:16])
130
+ encrypted = cipher.encrypt(message)
131
+ return base64.b64encode(encrypted).decode('utf-8')
132
+
133
+ def decrypt(self, encrypted_text):
134
+ encrypted_data = base64.b64decode(encrypted_text)
135
+ cipher = AES.new(self.key, AES.MODE_CBC, self.key[:16])
136
+ decrypted = cipher.decrypt(encrypted_data)
137
+ pad_len = decrypted[-1]
138
+ if not isinstance(pad_len, int):
139
+ pad_len = ord(pad_len)
140
+ content = decrypted[16:-pad_len]
141
+ msg_len = struct.unpack('>I', content[:4])[0]
142
+ xml_content = content[4:msg_len + 4].decode('utf-8')
143
+ app_id = content[msg_len + 4:].decode('utf-8')
144
+ if app_id != self.app_id:
145
+ raise ValueError('Invalid AppID')
146
+ return xml_content
147
+
148
+ class AsyncResponse:
149
+ def __init__(self):
150
+ self.status = "processing"
151
+ self.result = None
152
+ self.error = None
153
+ self.create_time = time.time()
154
+ self.timeout = 3600
155
+ self.response_type = "text" # Can be "text" or "image"
156
+ self.media_id = None # For image responses
157
+
158
+ def is_expired(self):
159
+ return time.time() - self.create_time > self.timeout
160
+
161
+ class UserSession:
162
+ def __init__(self):
163
+ self.messages = [{"role": "system", "content": "你是HXIAO公众号的智能助手,这一个用来分享与学习人工智能的公众号,我们的目标是专注AI应用的简单研究与实践。致力于分享切实可行的技术方案,希望让复杂的技术变得简单易懂。也喜欢用通俗的语言来解释专业概念,让技术真正服务于每个学习者"}]
164
+ self.pending_parts = []
165
+ self.last_active = time.time()
166
+ self.current_task = None
167
+ self.response_queue = {}
168
+ self.session_timeout = 3600
169
+
170
+ def is_expired(self):
171
+ return time.time() - self.last_active > self.session_timeout
172
+
173
+ def cleanup_expired_tasks(self):
174
+ expired_tasks = [
175
+ task_id for task_id, response in self.response_queue.items()
176
+ if response.is_expired()
177
+ ]
178
+ for task_id in expired_tasks:
179
+ del self.response_queue[task_id]
180
+ if self.current_task == task_id:
181
+ self.current_task = None
182
+
183
+ class SessionManager:
184
+ def __init__(self):
185
+ self.sessions = {}
186
+ self._lock = threading.Lock()
187
+ self.crypto = WeChatCrypto(ENCODING_AES_KEY, APPID)
188
+
189
+ def get_session(self, user_id):
190
+ with self._lock:
191
+ current_time = time.time()
192
+ if user_id in self.sessions:
193
+ session = self.sessions[user_id]
194
+ if session.is_expired():
195
+ session = UserSession()
196
+ else:
197
+ session.cleanup_expired_tasks()
198
+ else:
199
+ session = UserSession()
200
+ session.last_active = current_time
201
+ self.sessions[user_id] = session
202
+ return session
203
+
204
+ def clear_session(self, user_id):
205
+ with self._lock:
206
+ if user_id in self.sessions:
207
+ self.sessions[user_id] = UserSession()
208
+
209
+ def cleanup_expired_sessions(self):
210
+ with self._lock:
211
+ current_time = time.time()
212
+ expired_users = [
213
+ user_id for user_id, session in self.sessions.items()
214
+ if session.is_expired()
215
+ ]
216
+ for user_id in expired_users:
217
+ del self.sessions[user_id]
218
+ logging.info(f"已清理过期会话: {user_id}")
219
+
220
+ def convert_markdown_to_wechat(md_text):
221
+ if not md_text:
222
+ return md_text
223
+
224
+ md_text = re.sub(r'^# (.*?)$', r'【标题】\1', md_text, flags=re.MULTILINE)
225
+ md_text = re.sub(r'^## (.*?)$', r'【子标题】\1', md_text, flags=re.MULTILINE)
226
+ md_text = re.sub(r'^### (.*?)$', r'【小标题】\1', md_text, flags=re.MULTILINE)
227
+ md_text = re.sub(r'\*\*(.*?)\*\*', r'『\1』', md_text)
228
+ md_text = re.sub(r'\*(.*?)\*', r'「\1」', md_text)
229
+ md_text = re.sub(r'`(.*?)`', r'「\1」', md_text)
230
+ md_text = re.sub(r'^\- ', '• ', md_text, flags=re.MULTILINE)
231
+ md_text = re.sub(r'^\d\. ', '○ ', md_text, flags=re.MULTILINE)
232
+ md_text = re.sub(r'```[\w]*\n(.*?)```', r'【代码开始】\n\1\n【代码结束】', md_text, flags=re.DOTALL)
233
+ md_text = re.sub(r'^> (.*?)$', r'▎\1', md_text, flags=re.MULTILINE)
234
+ md_text = re.sub(r'^-{3,}$', r'—————————', md_text, flags=re.MULTILINE)
235
+ md_text = re.sub(r'\[(.*?)\]\((.*?)\)', r'\1(\2)', md_text)
236
+ md_text = re.sub(r'\n{3,}', '\n\n', md_text)
237
+
238
+ return md_text
239
+
240
+ def verify_signature(signature, timestamp, nonce, token):
241
+ items = [token, timestamp, nonce]
242
+ items.sort()
243
+ temp_str = ''.join(items)
244
+ hash_sha1 = hashlib.sha1(temp_str.encode('utf-8')).hexdigest()
245
+ return hash_sha1 == signature
246
+
247
+ def verify_msg_signature(msg_signature, timestamp, nonce, token, encrypt_msg):
248
+ """
249
+ 验证消息签名
250
+ Args:
251
+ msg_signature: 消息签名
252
+ timestamp: 时间戳
253
+ nonce: 随机数
254
+ token: 验证令牌
255
+ encrypt_msg: 加密的消息内容
256
+ Returns:
257
+ bool: 签名是否有效
258
+ """
259
+ items = [token, timestamp, nonce, encrypt_msg]
260
+ items.sort()
261
+ temp_str = ''.join(items)
262
+ hash_sha1 = hashlib.sha1(temp_str.encode('utf-8')).hexdigest()
263
+ return hash_sha1 == msg_signature
264
+
265
+
266
+ def parse_xml_message(xml_content):
267
+ """
268
+ 解析微信XML消息,支持文本和图片消息类型
269
+ """
270
+ root = ET.fromstring(xml_content)
271
+ message = {
272
+ 'from_user': root.find('FromUserName').text,
273
+ 'to_user': root.find('ToUserName').text,
274
+ 'create_time': root.find('CreateTime').text,
275
+ 'msg_type': root.find('MsgType').text,
276
+ 'msg_id': root.find('MsgId').text if root.find('MsgId') is not None else '',
277
+ 'msg_data_id': root.find('MsgDataId').text if root.find('MsgDataId') is not None else '',
278
+ 'idx': root.find('Idx').text if root.find('Idx') is not None else ''
279
+ }
280
+
281
+ if message['msg_type'] == 'text':
282
+ message['content'] = root.find('Content').text if root.find('Content') is not None else ''
283
+ elif message['msg_type'] == 'image':
284
+ message['pic_url'] = root.find('PicUrl').text
285
+ message['media_id'] = root.find('MediaId').text
286
+
287
+ return message
288
+
289
+ def get_image_content(media_id):
290
+ """
291
+ 通过微信接口获取图片内容
292
+ """
293
+ try:
294
+ access_token = token_manager.get_token()
295
+ url = f'https://api.weixin.qq.com/cgi-bin/media/get?access_token={access_token}&media_id={media_id}'
296
+
297
+ logging.info(f"开始下载图片,media_id: {media_id}")
298
+ response = requests.get(url)
299
+
300
+ if response.headers.get('Content-Type') == 'text/plain':
301
+ # 如果返回JSON错误信息
302
+ error_info = response.json()
303
+ if error_info.get('errcode') == 40001:
304
+ # access_token过期,刷新后重试
305
+ logging.info("access_token已过期,正在刷新并重试")
306
+ access_token = token_manager.refresh_token()
307
+ url = f'https://api.weixin.qq.com/cgi-bin/media/get?access_token={access_token}&media_id={media_id}'
308
+ response = requests.get(url)
309
+
310
+ response.raise_for_status()
311
+ return response.content
312
+
313
+ except Exception as e:
314
+ logging.error(f"获取图片内容失败: {str(e)}")
315
+ raise
316
+
317
+
318
+ def generate_response_xml(to_user, from_user, content, response_type='text', media_id=None, encrypt_type='aes'):
319
+ timestamp = str(int(time.time()))
320
+ nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
321
+
322
+ if response_type == 'image' and media_id:
323
+ xml_content = f'''
324
+ <xml>
325
+ <ToUserName><![CDATA[{to_user}]]></ToUserName>
326
+ <FromUserName><![CDATA[{from_user}]]></FromUserName>
327
+ <CreateTime>{timestamp}</CreateTime>
328
+ <MsgType><![CDATA[image]]></MsgType>
329
+ <Image>
330
+ <MediaId><![CDATA[{media_id}]]></MediaId>
331
+ </Image>
332
+ </xml>
333
+ '''
334
+ else:
335
+ formatted_content = convert_markdown_to_wechat(content)
336
+ xml_content = f'''
337
+ <xml>
338
+ <ToUserName><![CDATA[{to_user}]]></ToUserName>
339
+ <FromUserName><![CDATA[{from_user}]]></FromUserName>
340
+ <CreateTime>{timestamp}</CreateTime>
341
+ <MsgType><![CDATA[text]]></MsgType>
342
+ <Content><![CDATA[{formatted_content}]]></Content>
343
+ </xml>
344
+ '''
345
+
346
+ if encrypt_type == 'aes':
347
+ encrypted = session_manager.crypto.encrypt(xml_content)
348
+ signature_list = [TOKEN, timestamp, nonce, encrypted]
349
+ signature_list.sort()
350
+ msg_signature = hashlib.sha1(''.join(signature_list).encode('utf-8')).hexdigest()
351
+
352
+ response_xml = f'''
353
+ <xml>
354
+ <Encrypt><![CDATA[{encrypted}]]></Encrypt>
355
+ <MsgSignature><![CDATA[{msg_signature}]]></MsgSignature>
356
+ <TimeStamp>{timestamp}</TimeStamp>
357
+ <Nonce><![CDATA[{nonce}]]></Nonce>
358
+ </xml>
359
+ '''
360
+ else:
361
+ response_xml = xml_content
362
+
363
+ response = make_response(response_xml)
364
+ response.content_type = 'application/xml'
365
+ return response
366
+
367
+ # 创建全局的token管理器实例
368
+ token_manager = AccessTokenManager()
369
+
370
+ def upload_image_to_wechat(image_data):
371
+ """上传图片到微信服务器并获取media_id"""
372
+ try:
373
+ access_token = token_manager.get_token()
374
+ upload_url = f'https://api.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image'
375
+ files = {'media': ('image.jpg', image_data, 'image/jpeg')}
376
+
377
+ logging.info("开始上传图片到微信服务器")
378
+ response = requests.post(upload_url, files=files)
379
+ response.raise_for_status()
380
+ result = response.json()
381
+
382
+ if 'media_id' not in result:
383
+ if 'errcode' in result and result['errcode'] == 40001:
384
+ # access_token 可能过期,尝试刷新并重试
385
+ logging.info("access_token已过期,正在刷新并重试")
386
+ access_token = token_manager.refresh_token()
387
+ upload_url = f'https://api.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type=image'
388
+ response = requests.post(upload_url, files=files)
389
+ response.raise_for_status()
390
+ result = response.json()
391
+
392
+ if 'media_id' not in result:
393
+ error_msg = f"上传图片失败: {result}"
394
+ logging.error(error_msg)
395
+ raise ValueError(error_msg)
396
+
397
+ logging.info(f"图片上传成功,获取到media_id")
398
+ return result['media_id']
399
+
400
+ except Exception as e:
401
+ error_msg = f"上传图片过程中发生错误: {str(e)}"
402
+ logging.error(error_msg)
403
+ raise
404
+
405
+ def process_long_running_task(messages, message_type='text', image_data=None):
406
+ """
407
+ 处理长时间运行的任务,支持文本对话和图片识别
408
+ """
409
+ try:
410
+ logging.info(f"开始调用AI服务,消息类型: {message_type}")
411
+
412
+ if message_type == 'image':
413
+ # 图片识别逻辑保持不变
414
+ try:
415
+ image_content = get_image_content(image_data['media_id'])
416
+ image_base64 = base64.b64encode(image_content).decode('utf-8')
417
+
418
+ image_messages = [
419
+ {
420
+ "role": "user",
421
+ "content": [
422
+ {"type": "text", "text": "请详细描述这张图片中的内容,包括主要对象、场景、活动等关键信息"},
423
+ {
424
+ "type": "image_url",
425
+ "image_url": {
426
+ "url": f"data:image/jpeg;base64,{image_base64}"
427
+ }
428
+ }
429
+ ]
430
+ }
431
+ ]
432
+
433
+ logging.info("开始调用图像识别模型")
434
+ image_response = client.chat.completions.create(
435
+ model="gpt-4.1-mini",
436
+ messages=image_messages,
437
+ max_tokens=300,
438
+ timeout=60
439
+ )
440
+ logging.info("图像识别完成")
441
+
442
+ if not image_response.choices:
443
+ raise Exception("图像识别服务未返回有效结果")
444
+
445
+ return {
446
+ "type": "text",
447
+ "content": image_response.choices[0].message.content
448
+ }
449
+
450
+ except Exception as e:
451
+ logging.error(f"图像识别过程中发生错误: {str(e)}")
452
+ raise
453
+
454
+ else:
455
+ # 处理文本消息
456
+ try:
457
+ logging.info("开始处理文本消息")
458
+ response = client.chat.completions.create(
459
+ model="gpt-4.1-mini",
460
+ messages=messages,
461
+ tools=TOOLS,
462
+ tool_choice="auto",
463
+ timeout=60
464
+ )
465
+
466
+ # 检查是否需要生成图片
467
+ if response.choices[0].message.tool_calls:
468
+ tool_call = response.choices[0].message.tool_calls[0]
469
+ if tool_call.function.name == "generate_image":
470
+ try:
471
+ logging.info("检测到图片生成请求")
472
+ args = json.loads(tool_call.function.arguments)
473
+
474
+ # 使用新的DALL-E 3 API进行图片生成
475
+ image_generation_response = requests.post(
476
+ "https://api1.oaipro.com/v1/images/generations",
477
+ headers={
478
+ 'Content-Type': 'application/json',
479
+ 'Authorization': f'Bearer {API_KEY}'
480
+ },
481
+ json={
482
+ "model": "dall-e-3",
483
+ "prompt": args['prompt'],
484
+ "n": 1,
485
+ "size": "1024x1024"
486
+ },
487
+ timeout=60
488
+ )
489
+ image_generation_response.raise_for_status()
490
+ generation_result = image_generation_response.json()
491
+
492
+ if 'data' not in generation_result or not generation_result['data']:
493
+ raise Exception("图片生成服务未返回有效结果")
494
+
495
+ # 获取生成的图片URL
496
+ image_url = generation_result['data'][0]['url']
497
+
498
+ # 下载生成的图片
499
+ img_response = requests.get(image_url, timeout=30)
500
+ img_response.raise_for_status()
501
+
502
+ # 上传图片到微信服务器
503
+ media_id = upload_image_to_wechat(img_response.content)
504
+
505
+ return {
506
+ "type": "image",
507
+ "media_id": media_id
508
+ }
509
+
510
+ except requests.exceptions.RequestException as e:
511
+ logging.error(f"图片生成过程中发生网络错误: {str(e)}")
512
+ raise
513
+ except Exception as e:
514
+ logging.error(f"图片生成过程中发生错误: {str(e)}")
515
+ raise
516
+
517
+ # 返回文本响应
518
+ return {
519
+ "type": "text",
520
+ "content": response.choices[0].message.content
521
+ }
522
+
523
+ except requests.exceptions.RequestException as e:
524
+ logging.error(f"处理文本消息时发生网络错误: {str(e)}")
525
+ raise
526
+ except Exception as e:
527
+ logging.error(f"处理文本消息时发生错误: {str(e)}")
528
+ raise
529
+
530
+ except Exception as e:
531
+ logging.error(f"API调用错误: {str(e)}")
532
+ raise
533
+
534
+ def handle_async_task(session, task_id, messages=None, message_type='text', message_data=None):
535
+ """
536
+ 处理异步任务,支持文本对话和图片识别
537
+ """
538
+ try:
539
+ logging.info(f"开始处理异步任务: {task_id}, 类型: {message_type}")
540
+
541
+ if task_id not in session.response_queue:
542
+ return
543
+
544
+ if message_type == 'image':
545
+ result = process_long_running_task(None, 'image', message_data)
546
+ else:
547
+ result = process_long_running_task(messages)
548
+
549
+ if task_id in session.response_queue and not session.response_queue[task_id].is_expired():
550
+ session.response_queue[task_id].status = "completed"
551
+ session.response_queue[task_id].response_type = result.get("type", "text")
552
+
553
+ if result["type"] == "image":
554
+ session.response_queue[task_id].media_id = result["media_id"]
555
+ session.response_queue[task_id].result = None
556
+ else:
557
+ session.response_queue[task_id].result = result["content"]
558
+
559
+ if messages and result["type"] == "text":
560
+ messages.append({"role": "assistant", "content": result["content"]})
561
+
562
+ except Exception as e:
563
+ logging.error(f"异步任务处理失败: {str(e)}")
564
+ if task_id in session.response_queue:
565
+ session.response_queue[task_id].status = "failed"
566
+ session.response_queue[task_id].error = str(e)
567
+
568
+
569
+ def generate_initial_response():
570
+ return "您的请求正在处理中,请回复'查询'获取结果(生图需要时间)"
571
+
572
+ def split_message(message, max_length=500):
573
+ """
574
+ 将长消息分割成多个部分
575
+ Args:
576
+ message: 需要分割的消息
577
+ max_length: 每部分的最大长度
578
+ Returns:
579
+ list: 分割后的消息部分列表
580
+ """
581
+ return [message[i:i+max_length] for i in range(0, len(message), max_length)]
582
+
583
+ def append_status_message(content, has_pending_parts=False, is_processing=False):
584
+ """
585
+ 添加状态消息到响应内容
586
+ Args:
587
+ content: 原始内容
588
+ has_pending_parts: 是否有待发送的部分
589
+ is_processing: 是否正在处理中
590
+ Returns:
591
+ str: 添加了状态信息的内容
592
+ """
593
+ if "您的请求正在处理中" in content:
594
+ return content + "\n\n-------------------\n发送'新对话'开始新的对话"
595
+
596
+ status_message = "\n\n-------------------"
597
+ if is_processing:
598
+ status_message += "\n请回复'查询'获取结果"
599
+ elif has_pending_parts:
600
+ status_message += "\n当前消息已截断,发送'继续'查看后续内容"
601
+ status_message += "\n发送'新对话'开始新的对话"
602
+ return content + status_message
603
+
604
+ session_manager = SessionManager()
605
+
606
+ def cleanup_sessions():
607
+ while True:
608
+ time.sleep(3600) # 每小时清理一次
609
+ try:
610
+ session_manager.cleanup_expired_sessions()
611
+ except Exception as e:
612
+ logging.error(f"清理会话时出错: {str(e)}")