samlax12 commited on
Commit
20f7a0a
·
verified ·
1 Parent(s): 62b3377

Upload 25 files

Browse files
agents/agent_354996b8_1742743575.json CHANGED
@@ -77,7 +77,7 @@
77
  "created_at": 1742743611,
78
  "token": "db1779829c2b4ce5920be2181574655c",
79
  "expires_at": 0,
80
- "usage_count": 0
81
  },
82
  {
83
  "id": "dist_eaa6a1",
@@ -95,7 +95,7 @@
95
  }
96
  ],
97
  "stats": {
98
- "usage_count": 2,
99
- "last_used": 1742956769
100
  }
101
  }
 
77
  "created_at": 1742743611,
78
  "token": "db1779829c2b4ce5920be2181574655c",
79
  "expires_at": 0,
80
+ "usage_count": 5
81
  },
82
  {
83
  "id": "dist_eaa6a1",
 
95
  }
96
  ],
97
  "stats": {
98
+ "usage_count": 7,
99
+ "last_used": 1743320740
100
  }
101
  }
agents/agent_bff4886f_1742724690.json CHANGED
@@ -19,7 +19,7 @@
19
  "created_at": 1742732156,
20
  "token": "4781dc4214e041b38b1935eb4bb2e592",
21
  "expires_at": 0,
22
- "usage_count": 4
23
  },
24
  {
25
  "id": "dist_123907",
@@ -44,7 +44,7 @@
44
  }
45
  ],
46
  "stats": {
47
- "usage_count": 10,
48
- "last_used": 1742956846
49
  }
50
  }
 
19
  "created_at": 1742732156,
20
  "token": "4781dc4214e041b38b1935eb4bb2e592",
21
  "expires_at": 0,
22
+ "usage_count": 5
23
  },
24
  {
25
  "id": "dist_123907",
 
44
  }
45
  ],
46
  "stats": {
47
+ "usage_count": 11,
48
+ "last_used": 1743320208
49
  }
50
  }
app.py CHANGED
@@ -1,517 +1,939 @@
1
- from flask import Flask, request, jsonify, send_from_directory, render_template, redirect, url_for
2
- from flask_cors import CORS
3
- import os
4
- import time
5
- import traceback
6
- import json
7
- import re
8
- import sys
9
- import io
10
- import threading
11
- import queue
12
- import contextlib
13
- import signal
14
- import psutil
15
- from dotenv import load_dotenv
16
-
17
- # 导入模块路由
18
- from modules.knowledge_base.routes import knowledge_bp
19
- from modules.code_executor.routes import code_executor_bp
20
- from modules.visualization.routes import visualization_bp
21
- from modules.agent_builder.routes import agent_builder_bp
22
-
23
- # 加载环境变量
24
- load_dotenv()
25
-
26
- app = Flask(__name__)
27
- CORS(app)
28
-
29
- # 注册蓝图
30
- app.register_blueprint(knowledge_bp, url_prefix='/api/knowledge')
31
- app.register_blueprint(code_executor_bp, url_prefix='/api/code')
32
- app.register_blueprint(visualization_bp, url_prefix='/api/visualization')
33
- app.register_blueprint(agent_builder_bp, url_prefix='/api/agent')
34
-
35
- # 确保目录存在
36
- os.makedirs('static', exist_ok=True)
37
- os.makedirs('uploads', exist_ok=True)
38
- os.makedirs('agents', exist_ok=True)
39
-
40
- # 用于代码执行的上下文
41
- execution_contexts = {}
42
-
43
- def get_memory_usage():
44
- """获取当前进程的内存使用情况"""
45
- process = psutil.Process(os.getpid())
46
- return f"{process.memory_info().rss / 1024 / 1024:.1f} MB"
47
-
48
- class CustomStdin:
49
- def __init__(self, input_queue):
50
- self.input_queue = input_queue
51
- self.buffer = ""
52
-
53
- def readline(self):
54
- if not self.buffer:
55
- self.buffer = self.input_queue.get() + "\n"
56
-
57
- result = self.buffer
58
- self.buffer = ""
59
- return result
60
-
61
- class InteractiveExecution:
62
- """管理Python代码的交互式执行"""
63
- def __init__(self, code):
64
- self.code = code
65
- self.context_id = str(time.time())
66
- self.is_complete = False
67
- self.is_waiting_for_input = False
68
- self.stdout_buffer = io.StringIO()
69
- self.last_read_position = 0
70
- self.input_queue = queue.Queue()
71
- self.error = None
72
- self.thread = None
73
- self.should_terminate = False
74
-
75
- def run(self):
76
- """在单独的线程中启动执行"""
77
- self.thread = threading.Thread(target=self._execute)
78
- self.thread.daemon = True
79
- self.thread.start()
80
-
81
- # 给执行一点时间开始
82
- time.sleep(0.1)
83
- return self.context_id
84
-
85
- def _execute(self):
86
- """执行代码,处理标准输入输出"""
87
- try:
88
- # 保存原始的stdin/stdout
89
- orig_stdin = sys.stdin
90
- orig_stdout = sys.stdout
91
-
92
- # 创建自定义stdin
93
- custom_stdin = CustomStdin(self.input_queue)
94
-
95
- # 重定向stdin和stdout
96
- sys.stdin = custom_stdin
97
- sys.stdout = self.stdout_buffer
98
-
99
- try:
100
- # 检查终止的函数
101
- self._last_check_time = 0
102
-
103
- def check_termination():
104
- if self.should_terminate:
105
- raise KeyboardInterrupt("Execution terminated by user")
106
-
107
- # 设置一个模拟__main__模块的命名空间
108
- shared_namespace = {
109
- "__builtins__": __builtins__,
110
- "_check_termination": check_termination,
111
- "time": time,
112
- "__name__": "__main__"
113
- }
114
-
115
- # 在这个命名空间中执行用户代码
116
- try:
117
- exec(self.code, shared_namespace)
118
- except KeyboardInterrupt:
119
- print("\nExecution terminated by user")
120
-
121
- except Exception as e:
122
- self.error = {
123
- "error": str(e),
124
- "traceback": traceback.format_exc()
125
- }
126
-
127
- finally:
128
- # 恢复原始stdin/stdout
129
- sys.stdin = orig_stdin
130
- sys.stdout = orig_stdout
131
-
132
- # 标记执行完成
133
- self.is_complete = True
134
-
135
- except Exception as e:
136
- self.error = {
137
- "error": str(e),
138
- "traceback": traceback.format_exc()
139
- }
140
- self.is_complete = True
141
-
142
- def terminate(self):
143
- """终止执行"""
144
- self.should_terminate = True
145
-
146
- # 如果在等待输入,放入一些内容以解除阻塞
147
- if self.is_waiting_for_input:
148
- self.input_queue.put("\n")
149
-
150
- # 给执行一点时间终止
151
- time.sleep(0.2)
152
-
153
- # 标记为完成
154
- self.is_complete = True
155
-
156
- return True
157
-
158
- def provide_input(self, user_input):
159
- """为运行的代码提供输入"""
160
- self.input_queue.put(user_input)
161
- self.is_waiting_for_input = False
162
- return True
163
-
164
- def get_output(self):
165
- """获取stdout缓冲区的当前内容"""
166
- output = self.stdout_buffer.getvalue()
167
- return output
168
-
169
- def get_new_output(self):
170
- """只获取自上次读取以来的新输出"""
171
- current_value = self.stdout_buffer.getvalue()
172
- if self.last_read_position < len(current_value):
173
- new_output = current_value[self.last_read_position:]
174
- self.last_read_position = len(current_value)
175
- return new_output
176
- return ""
177
-
178
- @app.route('/')
179
- def index():
180
- """主界面"""
181
- return render_template('index.html')
182
- @app.route('/code_execution.html')
183
- def index2():
184
- """主界面"""
185
- return render_template('code_execution.html')
186
- @app.route('/api/progress/<task_id>', methods=['GET'])
187
- def get_progress(task_id):
188
- """获取文档处理进度"""
189
- try:
190
- # 从知识库模块访问处理任务
191
- from modules.knowledge_base.routes import processing_tasks
192
-
193
- progress_data = processing_tasks.get(task_id, {
194
- 'progress': 0,
195
- 'status': '未找到任务',
196
- 'error': True
197
- })
198
-
199
- return jsonify({"success": True, "data": progress_data})
200
- except Exception as e:
201
- traceback.print_exc()
202
- return jsonify({"success": False, "message": str(e)}), 500
203
-
204
- @app.route('/student/<agent_id>')
205
- def student_view(agent_id):
206
- """学生访问Agent界面"""
207
- token = request.args.get('token', '')
208
-
209
- # 验证Agent存在
210
- agent_path = os.path.join('agents', f"{agent_id}.json")
211
- if not os.path.exists(agent_path):
212
- return render_template('error.html',
213
- message="找不到指定的Agent",
214
- error_code=404)
215
-
216
- # 加载Agent配置
217
- with open(agent_path, 'r', encoding='utf-8') as f:
218
- try:
219
- agent_config = json.load(f)
220
- except:
221
- return render_template('error.html',
222
- message="Agent配置无效",
223
- error_code=500)
224
-
225
- # 验证访问令牌
226
- if token:
227
- valid_token = False
228
- if "distributions" in agent_config:
229
- for dist in agent_config["distributions"]:
230
- if dist.get("token") == token:
231
- valid_token = True
232
- break
233
-
234
- if not valid_token:
235
- return render_template('error.html',
236
- message="访问令牌无效",
237
- error_code=403)
238
-
239
- # 渲染学生页面
240
- return render_template('student.html',
241
- agent_id=agent_id,
242
- agent_name=agent_config.get('name', 'AI学习助手'),
243
- agent_description=agent_config.get('description', ''),
244
- token=token)
245
-
246
- @app.route('/code_execution.html')
247
- def code_execution_page():
248
- """代码执行页面"""
249
- return send_from_directory(os.path.dirname(os.path.abspath(__file__)), 'code_execution.html')
250
-
251
- @app.route('/api/student/chat/<agent_id>', methods=['POST'])
252
- def student_chat(agent_id):
253
- """学生与Agent聊天的API"""
254
- try:
255
- data = request.json
256
- message = data.get('message', '')
257
- token = data.get('token', '')
258
-
259
- if not message:
260
- return jsonify({"success": False, "message": "消息不能为空"}), 400
261
-
262
- # 验证Agent和令牌
263
- agent_path = os.path.join('agents', f"{agent_id}.json")
264
- if not os.path.exists(agent_path):
265
- return jsonify({"success": False, "message": "Agent不存在"}), 404
266
-
267
- with open(agent_path, 'r', encoding='utf-8') as f:
268
- agent_config = json.load(f)
269
-
270
- # 验证令牌(如果提供)
271
- if token and "distributions" in agent_config:
272
- valid_token = False
273
- for dist in agent_config["distributions"]:
274
- if dist.get("token") == token:
275
- valid_token = True
276
-
277
- # 更新使用计数
278
- dist["usage_count"] = dist.get("usage_count", 0) + 1
279
- break
280
-
281
- if not valid_token:
282
- return jsonify({"success": False, "message": "访问令牌无效"}), 403
283
-
284
- # 更新Agent使用统计
285
- if "stats" not in agent_config:
286
- agent_config["stats"] = {}
287
-
288
- agent_config["stats"]["usage_count"] = agent_config["stats"].get("usage_count", 0) + 1
289
- agent_config["stats"]["last_used"] = int(time.time())
290
-
291
- # 保存更新后的Agent配置
292
- with open(agent_path, 'w', encoding='utf-8') as f:
293
- json.dump(agent_config, f, ensure_ascii=False, indent=2)
294
-
295
- # 获取Agent关联的知识库和插件
296
- knowledge_bases = agent_config.get('knowledge_bases', [])
297
- plugins = agent_config.get('plugins', [])
298
-
299
- # 获取学科和指导者信息
300
- subject = agent_config.get('subject', agent_config.get('name', '通用学科'))
301
- instructor = agent_config.get('instructor', '教师')
302
-
303
- # 创建Generator实例,传入学科和指导者信息
304
- from modules.knowledge_base.generator import Generator
305
- generator = Generator(subject=subject, instructor=instructor)
306
-
307
- # 检测需要使用的插件
308
- suggested_plugins = []
309
-
310
- # 检测是否需要代码执行插件
311
- if 'code' in plugins and ('代码' in message or 'python' in message.lower() or '编程' in message or 'code' in message.lower() or 'program' in message.lower()):
312
- suggested_plugins.append('code')
313
-
314
- # 检测是否需要3D可视化插件
315
- if 'visualization' in plugins and ('3d' in message.lower() or '可视化' in message or '图形' in message):
316
- suggested_plugins.append('visualization')
317
-
318
- # 检测是否需要思维导图插件
319
- if 'mindmap' in plugins and ('思维导图' in message or 'mindmap' in message.lower()):
320
- suggested_plugins.append('mindmap')
321
-
322
- # 检查是否有配置知识库
323
- if not knowledge_bases:
324
- # 没有知识库,直接使用模型进行回答
325
- print(f"\n=== 处理查询: {message} (无知识库) ===")
326
-
327
- # 使用空的文档列表调用生成器进行回答
328
- final_response = ""
329
- for chunk in generator.generate_stream(message, []):
330
- if isinstance(chunk, dict):
331
- continue # 跳过处理数据
332
- final_response += chunk
333
-
334
- # 返回生成的回答
335
- return jsonify({
336
- "success": True,
337
- "message": final_response,
338
- "tools": suggested_plugins
339
- })
340
-
341
- # 有知识库配置,执行知识库查询流程
342
- try:
343
- # 导入RAG系统组件
344
- from modules.knowledge_base.retriever import Retriever
345
- from modules.knowledge_base.reranker import Reranker
346
-
347
- retriever = Retriever()
348
- reranker = Reranker()
349
-
350
- # 构建工具定义 - 将所有知识库作为工具
351
- tools = []
352
-
353
- # 创建工具名称到索引的映射
354
- tool_to_index = {}
355
-
356
- for i, index in enumerate(knowledge_bases):
357
- display_name = index[4:] if index.startswith('rag_') else index
358
-
359
- # 判断是否是视频知识库
360
- is_video = "视频" in display_name or "video" in display_name.lower()
361
-
362
- # 根据内容类型生成适当的工具名称
363
- if is_video:
364
- tool_name = f"video_knowledge_base_{i+1}"
365
- description = f"在'{display_name}'视频知识库中搜索,返回带时间戳的视频链接。适用于需要视频讲解的问题。"
366
- else:
367
- tool_name = f"knowledge_base_{i+1}"
368
- description = f"在'{display_name}'知识库中搜索专业知识、概念和原理。适用于需要文本说明的问题。"
369
-
370
- # 添加工具名到索引的映射
371
- tool_to_index[tool_name] = index
372
-
373
- tools.append({
374
- "type": "function",
375
- "function": {
376
- "name": tool_name,
377
- "description": description,
378
- "parameters": {
379
- "type": "object",
380
- "properties": {
381
- "keywords": {
382
- "type": "array",
383
- "items": {"type": "string"},
384
- "description": "搜索的关键词列表"
385
- }
386
- },
387
- "required": ["keywords"],
388
- "additionalProperties": False
389
- },
390
- "strict": True
391
- }
392
- })
393
-
394
- # 第一阶段:工具选择决策
395
- print(f"\n=== 处理查询: {message} ===")
396
- tool_calls = generator.extract_keywords_with_tools(message, tools)
397
-
398
- # 如果不需要调用工具,直接回答
399
- if not tool_calls:
400
- print("未检测到需要使用知识库,直接回答")
401
- final_response = ""
402
- for chunk in generator.generate_stream(message, []):
403
- if isinstance(chunk, dict):
404
- continue # 跳过处理数据
405
- final_response += chunk
406
-
407
- return jsonify({
408
- "success": True,
409
- "message": final_response,
410
- "tools": suggested_plugins
411
- })
412
-
413
- # 收集来自工具执行的所有文档
414
- all_docs = []
415
-
416
- # 执行每个工具调用
417
- for tool_call in tool_calls:
418
- try:
419
- tool_name = tool_call["function"]["name"]
420
- actual_index = tool_to_index.get(tool_name)
421
-
422
- if not actual_index:
423
- print(f"找不到工具名称 '{tool_name}' 对应的索引")
424
- continue
425
-
426
- print(f"\n执行工具 '{tool_name}' -> 使用索引 '{actual_index}'")
427
-
428
- arguments = json.loads(tool_call["function"]["arguments"])
429
- keywords = " ".join(arguments.get("keywords", []))
430
-
431
- if not keywords:
432
- print("没有提供关键词,跳过检索")
433
- continue
434
-
435
- print(f"检索关键词: {keywords}")
436
-
437
- # 执行检索
438
- retrieved_docs, _ = retriever.retrieve(keywords, specific_index=actual_index)
439
- print(f"检索到 {len(retrieved_docs)} 个文档")
440
-
441
- # 重排序文档
442
- reranked_docs = reranker.rerank(message, retrieved_docs, actual_index)
443
- print(f"重排序完成,排序后有 {len(reranked_docs)} 个文档")
444
-
445
- # 添加结果
446
- all_docs.extend(reranked_docs)
447
-
448
- except Exception as e:
449
- print(f"执行工具 '{tool_call.get('function', {}).get('name', '未知')}' 调用时出错: {str(e)}")
450
- import traceback
451
- traceback.print_exc()
452
-
453
- # 如果没有检索到任何文档,直接回答
454
- if not all_docs:
455
- print("未检索到任何相关文档,直接回答")
456
- final_response = ""
457
- for chunk in generator.generate_stream(message, []):
458
- if isinstance(chunk, dict):
459
- continue # 跳过处理数据
460
- final_response += chunk
461
-
462
- return jsonify({
463
- "success": True,
464
- "message": final_response,
465
- "tools": suggested_plugins
466
- })
467
-
468
- # 按相关性排序
469
- all_docs.sort(key=lambda x: x.get('rerank_score', 0), reverse=True)
470
- print(f"\n最终收集到 {len(all_docs)} 个文档用于生成回答")
471
-
472
- # 提取参考信息
473
- references = []
474
- for i, doc in enumerate(all_docs[:3], 1): # 只展示前3个参考来源
475
- file_name = doc['metadata'].get('file_name', '未知文件')
476
- content = doc['content']
477
-
478
- # 提取大约前100字符作为摘要
479
- summary = content[:100] + ('...' if len(content) > 100 else '')
480
-
481
- references.append({
482
- 'index': i,
483
- 'file_name': file_name,
484
- 'content': content,
485
- 'summary': summary
486
- })
487
-
488
- # 第二阶段:生成最终答案
489
- final_response = ""
490
- for chunk in generator.generate_stream(message, all_docs):
491
- if isinstance(chunk, dict):
492
- continue # 跳过处理数据
493
- final_response += chunk
494
-
495
- # 构建回复
496
- return jsonify({
497
- "success": True,
498
- "message": final_response,
499
- "tools": suggested_plugins,
500
- "references": references
501
- })
502
-
503
- except Exception as e:
504
- import traceback
505
- traceback.print_exc()
506
- return jsonify({
507
- "success": False,
508
- "message": f"处理查询时出错: {str(e)}"
509
- }), 500
510
-
511
- except Exception as e:
512
- import traceback
513
- traceback.print_exc()
514
- return jsonify({"success": False, "message": str(e)}), 500
515
-
516
- if __name__ == '__main__':
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  app.run(debug=True, host='0.0.0.0', port=7860)
 
1
+ from flask import Flask, request, jsonify, send_from_directory, render_template, redirect, url_for, session
2
+ from flask_cors import CORS
3
+ import os
4
+ import time
5
+ import traceback
6
+ import json
7
+ import re
8
+ import sys
9
+ import io
10
+ import threading
11
+ import queue
12
+ import contextlib
13
+ import signal
14
+ import psutil
15
+ from dotenv import load_dotenv
16
+
17
+ # 导入模块路由
18
+ from modules.knowledge_base.routes import knowledge_bp
19
+ from modules.code_executor.routes import code_executor_bp
20
+ from modules.visualization.routes import visualization_bp
21
+ from modules.agent_builder.routes import agent_builder_bp
22
+
23
+ # 加载环境变量
24
+ load_dotenv()
25
+
26
+ app = Flask(__name__)
27
+ CORS(app)
28
+
29
+ # 设置session密钥
30
+ app.secret_key = os.getenv("SECRET_KEY", "your_secret_key_here")
31
+
32
+ # 注册蓝图
33
+ app.register_blueprint(knowledge_bp, url_prefix='/api/knowledge')
34
+ app.register_blueprint(code_executor_bp, url_prefix='/api/code')
35
+ app.register_blueprint(visualization_bp, url_prefix='/api/visualization')
36
+ app.register_blueprint(agent_builder_bp, url_prefix='/api/agent')
37
+
38
+ # 确保目录存在
39
+ os.makedirs('static', exist_ok=True)
40
+ os.makedirs('uploads', exist_ok=True)
41
+ os.makedirs('agents', exist_ok=True)
42
+
43
+ # 用于代码执行的上下文
44
+ execution_contexts = {}
45
+
46
+ # 硬编码用户(仅用于演示)
47
+ users = {
48
+ "teachers": [
49
+ {"username": "teacher", "password": "123456", "name": "李志刚"},
50
+ {"username": "admin", "password": "admin123", "name": "管理员"}
51
+ ],
52
+ "students": [
53
+ {"username": "student1", "password": "123456", "name": "张三"},
54
+ {"username": "student2", "password": "123456", "name": "李四"}
55
+ ]
56
+ }
57
+
58
+ # 学生活动记录存储(实际应用中应使用数据库)
59
+ student_activities = {}
60
+
61
+ def get_memory_usage():
62
+ """获取当前进程的内存使用情况"""
63
+ process = psutil.Process(os.getpid())
64
+ return f"{process.memory_info().rss / 1024 / 1024:.1f} MB"
65
+
66
+ class CustomStdin:
67
+ def __init__(self, input_queue):
68
+ self.input_queue = input_queue
69
+ self.buffer = ""
70
+
71
+ def readline(self):
72
+ if not self.buffer:
73
+ self.buffer = self.input_queue.get() + "\n"
74
+
75
+ result = self.buffer
76
+ self.buffer = ""
77
+ return result
78
+
79
+ class InteractiveExecution:
80
+ """管理Python代码的交互式执行"""
81
+ def __init__(self, code):
82
+ self.code = code
83
+ self.context_id = str(time.time())
84
+ self.is_complete = False
85
+ self.is_waiting_for_input = False
86
+ self.stdout_buffer = io.StringIO()
87
+ self.last_read_position = 0
88
+ self.input_queue = queue.Queue()
89
+ self.error = None
90
+ self.thread = None
91
+ self.should_terminate = False
92
+
93
+ def run(self):
94
+ """在单独的线程中启动执行"""
95
+ self.thread = threading.Thread(target=self._execute)
96
+ self.thread.daemon = True
97
+ self.thread.start()
98
+
99
+ # 给执行一点时间开始
100
+ time.sleep(0.1)
101
+ return self.context_id
102
+
103
+ def _execute(self):
104
+ """执行代码,处理标准输入输出"""
105
+ try:
106
+ # 保存原始的stdin/stdout
107
+ orig_stdin = sys.stdin
108
+ orig_stdout = sys.stdout
109
+
110
+ # 创建自定义stdin
111
+ custom_stdin = CustomStdin(self.input_queue)
112
+
113
+ # 重定向stdin和stdout
114
+ sys.stdin = custom_stdin
115
+ sys.stdout = self.stdout_buffer
116
+
117
+ try:
118
+ # 检查终止的函数
119
+ self._last_check_time = 0
120
+
121
+ def check_termination():
122
+ if self.should_terminate:
123
+ raise KeyboardInterrupt("Execution terminated by user")
124
+
125
+ # 设置一个模拟__main__模块的命名空间
126
+ shared_namespace = {
127
+ "__builtins__": __builtins__,
128
+ "_check_termination": check_termination,
129
+ "time": time,
130
+ "__name__": "__main__"
131
+ }
132
+
133
+ # 在这个命名空间中执行用户代码
134
+ try:
135
+ exec(self.code, shared_namespace)
136
+ except KeyboardInterrupt:
137
+ print("\nExecution terminated by user")
138
+
139
+ except Exception as e:
140
+ self.error = {
141
+ "error": str(e),
142
+ "traceback": traceback.format_exc()
143
+ }
144
+
145
+ finally:
146
+ # 恢复原始stdin/stdout
147
+ sys.stdin = orig_stdin
148
+ sys.stdout = orig_stdout
149
+
150
+ # 标记执行完成
151
+ self.is_complete = True
152
+
153
+ except Exception as e:
154
+ self.error = {
155
+ "error": str(e),
156
+ "traceback": traceback.format_exc()
157
+ }
158
+ self.is_complete = True
159
+
160
+ def terminate(self):
161
+ """终止执行"""
162
+ self.should_terminate = True
163
+
164
+ # 如果在等待输入,放入一些内容以解除阻塞
165
+ if self.is_waiting_for_input:
166
+ self.input_queue.put("\n")
167
+
168
+ # 给执行一点时间终止
169
+ time.sleep(0.2)
170
+
171
+ # 标记为完成
172
+ self.is_complete = True
173
+
174
+ return True
175
+
176
+ def provide_input(self, user_input):
177
+ """为运行的代码提供输入"""
178
+ self.input_queue.put(user_input)
179
+ self.is_waiting_for_input = False
180
+ return True
181
+
182
+ def get_output(self):
183
+ """获取stdout缓冲区的当前内容"""
184
+ output = self.stdout_buffer.getvalue()
185
+ return output
186
+
187
+ def get_new_output(self):
188
+ """只获取自上次读取以来的新输出"""
189
+ current_value = self.stdout_buffer.getvalue()
190
+ if self.last_read_position < len(current_value):
191
+ new_output = current_value[self.last_read_position:]
192
+ self.last_read_position = len(current_value)
193
+ return new_output
194
+ return ""
195
+
196
+ # 记录活动函数(可在各个操作点调用)
197
+ def record_student_activity(username, activity_type, title, agent_id=None, agent_name=None):
198
+ """记录学生活动"""
199
+ if username not in student_activities:
200
+ student_activities[username] = []
201
+
202
+ # 创建活动记录
203
+ activity = {
204
+ "type": activity_type, # 'chat', 'code', 'viz', 'mindmap'
205
+ "title": title,
206
+ "timestamp": int(time.time()),
207
+ "agent_id": agent_id,
208
+ "agent_name": agent_name
209
+ }
210
+
211
+ # 添加到用户活动列表(最多保存20条记录)
212
+ student_activities[username].insert(0, activity)
213
+ if len(student_activities[username]) > 20:
214
+ student_activities[username] = student_activities[username][:20]
215
+
216
+ return activity
217
+
218
+ # 登录相关路由
219
+ @app.route('/login.html')
220
+ def login_page():
221
+ """登录页面"""
222
+ return render_template('login.html')
223
+
224
+ @app.route('/api/auth/login', methods=['POST'])
225
+ def login():
226
+ """处理登录请求"""
227
+ data = request.json
228
+ username = data.get('username')
229
+ password = data.get('password')
230
+ user_type = data.get('type', 'teacher') # 默认为教师
231
+
232
+ if user_type == 'teacher':
233
+ user_list = users['teachers']
234
+ else:
235
+ user_list = users['students']
236
+
237
+ for user in user_list:
238
+ if user['username'] == username and user['password'] == password:
239
+ # 设置session
240
+ session['logged_in'] = True
241
+ session['username'] = username
242
+ session['user_type'] = user_type
243
+ session['user_name'] = user['name']
244
+
245
+ return jsonify({
246
+ 'success': True,
247
+ 'user': {
248
+ 'name': user['name'],
249
+ 'type': user_type
250
+ }
251
+ })
252
+
253
+ return jsonify({
254
+ 'success': False,
255
+ 'message': '用户名或密码错误'
256
+ }), 401
257
+
258
+ @app.route('/api/auth/logout', methods=['POST'])
259
+ def logout():
260
+ """处理登出请求"""
261
+ session.clear()
262
+ return jsonify({
263
+ 'success': True
264
+ })
265
+
266
+ @app.route('/api/auth/check', methods=['GET'])
267
+ def check_auth():
268
+ """检查用户是否已登录"""
269
+ if session.get('logged_in'):
270
+ return jsonify({
271
+ 'success': True,
272
+ 'user': {
273
+ 'name': session.get('user_name'),
274
+ 'type': session.get('user_type')
275
+ }
276
+ })
277
+
278
+ return jsonify({
279
+ 'success': False
280
+ }), 401
281
+
282
+ # 登录验证装饰器
283
+ def login_required(f):
284
+ def decorated_function(*args, **kwargs):
285
+ if not session.get('logged_in'):
286
+ return redirect(url_for('login_page'))
287
+ return f(*args, **kwargs)
288
+ decorated_function.__name__ = f.__name__
289
+ return decorated_function
290
+
291
+ # 教师角色验证装饰器
292
+ def teacher_required(f):
293
+ def decorated_function(*args, **kwargs):
294
+ if not session.get('logged_in') or session.get('user_type') != 'teacher':
295
+ return jsonify({
296
+ 'success': False,
297
+ 'message': '需要教师权限'
298
+ }), 403
299
+ return f(*args, **kwargs)
300
+ decorated_function.__name__ = f.__name__
301
+ return decorated_function
302
+
303
+ # 学生角色验证装饰器
304
+ def student_required(f):
305
+ def decorated_function(*args, **kwargs):
306
+ if not session.get('logged_in') or session.get('user_type') != 'student':
307
+ return jsonify({
308
+ 'success': False,
309
+ 'message': '需要学生权限'
310
+ }), 403
311
+ return f(*args, **kwargs)
312
+ decorated_function.__name__ = f.__name__
313
+ return decorated_function
314
+
315
+ # 首页路由
316
+ @app.route('/')
317
+ def root():
318
+ """重定向到登录页面或主界面"""
319
+ if session.get('logged_in'):
320
+ if session.get('user_type') == 'teacher':
321
+ return redirect('/index.html')
322
+ else:
323
+ return redirect('/student_portal.html')
324
+ return redirect('/login.html')
325
+
326
+ @app.route('/index.html')
327
+ @login_required
328
+ def index():
329
+ """教师端主界面"""
330
+ if session.get('user_type') != 'teacher':
331
+ return redirect('/student_portal.html')
332
+ return render_template('index.html')
333
+
334
+ @app.route('/student_portal.html')
335
+ @login_required
336
+ def student_portal():
337
+ """学生端门户"""
338
+ if session.get('user_type') != 'student':
339
+ return redirect('/index.html')
340
+ return render_template('student_portal.html')
341
+
342
+ @app.route('/code_execution.html')
343
+ def code_execution_page():
344
+ """代码执行页面"""
345
+ return send_from_directory(os.path.dirname(os.path.abspath(__file__)), 'templates/code_execution.html')
346
+
347
+ @app.route('/verify_token.html')
348
+ def verify_token_page():
349
+ """令牌验证页面"""
350
+ return render_template('token_verification.html')
351
+
352
+ @app.route('/api/progress/<task_id>', methods=['GET'])
353
+ def get_progress(task_id):
354
+ """获取文档处理进度"""
355
+ try:
356
+ # 从知识库模块访问处理任务
357
+ from modules.knowledge_base.routes import processing_tasks
358
+
359
+ progress_data = processing_tasks.get(task_id, {
360
+ 'progress': 0,
361
+ 'status': '未找到任务',
362
+ 'error': True
363
+ })
364
+
365
+ return jsonify({"success": True, "data": progress_data})
366
+ except Exception as e:
367
+ traceback.print_exc()
368
+ return jsonify({"success": False, "message": str(e)}), 500
369
+
370
+ @app.route('/student/<agent_id>')
371
+ def student_view(agent_id):
372
+ """学生访问Agent界面"""
373
+ token = request.args.get('token', '')
374
+
375
+ # 验证Agent存在
376
+ agent_path = os.path.join('agents', f"{agent_id}.json")
377
+ if not os.path.exists(agent_path):
378
+ return render_template('error.html',
379
+ message="找不到指定的Agent",
380
+ error_code=404)
381
+
382
+ # 加载Agent配置
383
+ with open(agent_path, 'r', encoding='utf-8') as f:
384
+ try:
385
+ agent_config = json.load(f)
386
+ except:
387
+ return render_template('error.html',
388
+ message="Agent配置无效",
389
+ error_code=500)
390
+
391
+ # 验证访问令牌
392
+ if token:
393
+ valid_token = False
394
+ if "distributions" in agent_config:
395
+ for dist in agent_config["distributions"]:
396
+ if dist.get("token") == token:
397
+ valid_token = True
398
+ break
399
+
400
+ if not valid_token:
401
+ return render_template('token_verification.html',
402
+ message="访问令牌无效",
403
+ error_code=403)
404
+
405
+ # 更新使用统计
406
+ if "distributions" in agent_config:
407
+ for dist in agent_config["distributions"]:
408
+ if dist.get("token") == token:
409
+ # 更新分发使用次数
410
+ dist["usage_count"] = dist.get("usage_count", 0) + 1
411
+
412
+ # 更新Agent使用统计
413
+ if "stats" not in agent_config:
414
+ agent_config["stats"] = {}
415
+
416
+ agent_config["stats"]["usage_count"] = agent_config["stats"].get("usage_count", 0) + 1
417
+ agent_config["stats"]["last_used"] = int(time.time())
418
+
419
+ # 保存更新后的Agent配置
420
+ with open(agent_path, 'w', encoding='utf-8') as f:
421
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
422
+
423
+ break
424
+
425
+ # 渲染学生页面
426
+ return render_template('student.html',
427
+ agent_id=agent_id,
428
+ agent_name=agent_config.get('name', 'AI学习助手'),
429
+ agent_description=agent_config.get('description', ''),
430
+ token=token)
431
+
432
+ @app.route('/api/student/chat/<agent_id>', methods=['POST'])
433
+ def student_chat(agent_id):
434
+ """学生与Agent聊天的API"""
435
+ try:
436
+ data = request.json
437
+ message = data.get('message', '')
438
+ token = data.get('token', '')
439
+
440
+ if not message:
441
+ return jsonify({"success": False, "message": "消息不能为空"}), 400
442
+
443
+ # 验证Agent和令牌
444
+ agent_path = os.path.join('agents', f"{agent_id}.json")
445
+ if not os.path.exists(agent_path):
446
+ return jsonify({"success": False, "message": "Agent不存在"}), 404
447
+
448
+ with open(agent_path, 'r', encoding='utf-8') as f:
449
+ agent_config = json.load(f)
450
+
451
+ # 验证令牌(如果提供)
452
+ if token and "distributions" in agent_config:
453
+ valid_token = False
454
+ for dist in agent_config["distributions"]:
455
+ if dist.get("token") == token:
456
+ valid_token = True
457
+
458
+ # 更新使用计数
459
+ dist["usage_count"] = dist.get("usage_count", 0) + 1
460
+ break
461
+
462
+ if not valid_token:
463
+ return jsonify({"success": False, "message": "访问令牌无效"}), 403
464
+
465
+ # 更新Agent使用统计
466
+ if "stats" not in agent_config:
467
+ agent_config["stats"] = {}
468
+
469
+ agent_config["stats"]["usage_count"] = agent_config["stats"].get("usage_count", 0) + 1
470
+ agent_config["stats"]["last_used"] = int(time.time())
471
+
472
+ # 保存更新后的Agent配置
473
+ with open(agent_path, 'w', encoding='utf-8') as f:
474
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
475
+
476
+ # 获取Agent关联的知识库和插件
477
+ knowledge_bases = agent_config.get('knowledge_bases', [])
478
+ plugins = agent_config.get('plugins', [])
479
+
480
+ # 获取学科和指导者信息
481
+ subject = agent_config.get('subject', agent_config.get('name', '通用学科'))
482
+ instructor = agent_config.get('instructor', '教师')
483
+
484
+ # 创建Generator实例,传入学科和指导者信息
485
+ from modules.knowledge_base.generator import Generator
486
+ generator = Generator(subject=subject, instructor=instructor)
487
+
488
+ # 检测需要使用的插件
489
+ suggested_plugins = []
490
+
491
+ # 检测是否需要代码执行插件
492
+ if 'code' in plugins and ('代码' in message or 'python' in message.lower() or '编程' in message or 'code' in message.lower() or 'program' in message.lower()):
493
+ suggested_plugins.append('code')
494
+
495
+ # 检测是否需要3D可视化插件
496
+ if 'visualization' in plugins and ('3d' in message.lower() or '可视化' in message or '图形' in message):
497
+ suggested_plugins.append('visualization')
498
+
499
+ # 检测是否需要思维导图插件
500
+ if 'mindmap' in plugins and ('思维导图' in message or 'mindmap' in message.lower()):
501
+ suggested_plugins.append('mindmap')
502
+
503
+ # 记录活动(添加此部分代码)
504
+ if session.get('logged_in'):
505
+ username = session.get('username')
506
+ # 记录对话活动
507
+ record_student_activity(
508
+ username=username,
509
+ activity_type='chat',
510
+ title=f'与 {agent_config.get("name", "AI助手")} 进行了对话',
511
+ agent_id=agent_id,
512
+ agent_name=agent_config.get('name')
513
+ )
514
+
515
+ # 如果使用了插件,记录相应的插件活动
516
+ if 'code' in suggested_plugins:
517
+ record_student_activity(
518
+ username=username,
519
+ activity_type='code',
520
+ title='执行了Python代码',
521
+ agent_id=agent_id,
522
+ agent_name=agent_config.get('name')
523
+ )
524
+
525
+ if 'visualization' in suggested_plugins:
526
+ record_student_activity(
527
+ username=username,
528
+ activity_type='viz',
529
+ title='查看了3D可视化图形',
530
+ agent_id=agent_id,
531
+ agent_name=agent_config.get('name')
532
+ )
533
+
534
+ if 'mindmap' in suggested_plugins:
535
+ record_student_activity(
536
+ username=username,
537
+ activity_type='mindmap',
538
+ title='生成了思维导图',
539
+ agent_id=agent_id,
540
+ agent_name=agent_config.get('name')
541
+ )
542
+
543
+ # 检查是否有配置知识库
544
+ if not knowledge_bases:
545
+ # 没有知识库,直接使用模型进行回答
546
+ print(f"\n=== 处理查询: {message} (无知识库) ===")
547
+
548
+ # 使用空的文档列表调用生成器进行回答
549
+ final_response = ""
550
+ for chunk in generator.generate_stream(message, []):
551
+ if isinstance(chunk, dict):
552
+ continue # 跳过处理数据
553
+ final_response += chunk
554
+
555
+ # 返回生成的回答
556
+ return jsonify({
557
+ "success": True,
558
+ "message": final_response,
559
+ "tools": suggested_plugins
560
+ })
561
+
562
+ # 有知识库配置,执行知识库查询流程
563
+ try:
564
+ # 导入RAG系统组件
565
+ from modules.knowledge_base.retriever import Retriever
566
+ from modules.knowledge_base.reranker import Reranker
567
+
568
+ retriever = Retriever()
569
+ reranker = Reranker()
570
+
571
+ # 构建工具定义 - 将所有知识库作为工具
572
+ tools = []
573
+
574
+ # 创建工具名称到索引的映射
575
+ tool_to_index = {}
576
+
577
+ for i, index in enumerate(knowledge_bases):
578
+ display_name = index[4:] if index.startswith('rag_') else index
579
+
580
+ # 判断是否是视频知识库
581
+ is_video = "视频" in display_name or "video" in display_name.lower()
582
+
583
+ # 根据内容类型生成适当的工具名称
584
+ if is_video:
585
+ tool_name = f"video_knowledge_base_{i+1}"
586
+ description = f"在'{display_name}'视频知识库中搜索,返回带时间戳的视频链接。适用于需要视频讲解的问题。"
587
+ else:
588
+ tool_name = f"knowledge_base_{i+1}"
589
+ description = f"在'{display_name}'知识库中搜索专业知识、概念和原理。适用于需要文本说明的问题。"
590
+
591
+ # 添加工具名到索引的映射
592
+ tool_to_index[tool_name] = index
593
+
594
+ tools.append({
595
+ "type": "function",
596
+ "function": {
597
+ "name": tool_name,
598
+ "description": description,
599
+ "parameters": {
600
+ "type": "object",
601
+ "properties": {
602
+ "keywords": {
603
+ "type": "array",
604
+ "items": {"type": "string"},
605
+ "description": "搜索的关键词列表"
606
+ }
607
+ },
608
+ "required": ["keywords"],
609
+ "additionalProperties": False
610
+ },
611
+ "strict": True
612
+ }
613
+ })
614
+
615
+ # 第一阶段:工具选择决策
616
+ print(f"\n=== 处理查询: {message} ===")
617
+ tool_calls = generator.extract_keywords_with_tools(message, tools)
618
+
619
+ # 如果不需要调用工具,直接回答
620
+ if not tool_calls:
621
+ print("未检测到需要使用知识库,直接回答")
622
+ final_response = ""
623
+ for chunk in generator.generate_stream(message, []):
624
+ if isinstance(chunk, dict):
625
+ continue # 跳过处理数据
626
+ final_response += chunk
627
+
628
+ return jsonify({
629
+ "success": True,
630
+ "message": final_response,
631
+ "tools": suggested_plugins
632
+ })
633
+
634
+ # 收集来自工具执行的所有文档
635
+ all_docs = []
636
+
637
+ # 执行每个工具调用
638
+ for tool_call in tool_calls:
639
+ try:
640
+ tool_name = tool_call["function"]["name"]
641
+ actual_index = tool_to_index.get(tool_name)
642
+
643
+ if not actual_index:
644
+ print(f"找不到工具名称 '{tool_name}' 对应的索引")
645
+ continue
646
+
647
+ print(f"\n执行工具 '{tool_name}' -> 使用索引 '{actual_index}'")
648
+
649
+ arguments = json.loads(tool_call["function"]["arguments"])
650
+ keywords = " ".join(arguments.get("keywords", []))
651
+
652
+ if not keywords:
653
+ print("没有提供关键词,跳过检索")
654
+ continue
655
+
656
+ print(f"检索关键词: {keywords}")
657
+
658
+ # 执行检索
659
+ retrieved_docs, _ = retriever.retrieve(keywords, specific_index=actual_index)
660
+ print(f"检索到 {len(retrieved_docs)} 个文档")
661
+
662
+ # 重排序文档
663
+ reranked_docs = reranker.rerank(message, retrieved_docs, actual_index)
664
+ print(f"重排序完成,排序后有 {len(reranked_docs)} 个文档")
665
+
666
+ # 添加结果
667
+ all_docs.extend(reranked_docs)
668
+
669
+ except Exception as e:
670
+ print(f"执行工具 '{tool_call.get('function', {}).get('name', '未知')}' 调用时出错: {str(e)}")
671
+ import traceback
672
+ traceback.print_exc()
673
+
674
+ # 如果没有检索到任何文档,直接回答
675
+ if not all_docs:
676
+ print("未检索到任何相关文档,直接回答")
677
+ final_response = ""
678
+ for chunk in generator.generate_stream(message, []):
679
+ if isinstance(chunk, dict):
680
+ continue # 跳过处理数据
681
+ final_response += chunk
682
+
683
+ return jsonify({
684
+ "success": True,
685
+ "message": final_response,
686
+ "tools": suggested_plugins
687
+ })
688
+
689
+ # 按相关性排序
690
+ all_docs.sort(key=lambda x: x.get('rerank_score', 0), reverse=True)
691
+ print(f"\n最终收集到 {len(all_docs)} 个文档用于生成回答")
692
+
693
+ # 提取参考信息
694
+ references = []
695
+ for i, doc in enumerate(all_docs[:3], 1): # 只展示前3个参考来源
696
+ file_name = doc['metadata'].get('file_name', '未知文件')
697
+ content = doc['content']
698
+
699
+ # 提取大约前100字符作为摘要
700
+ summary = content[:100] + ('...' if len(content) > 100 else '')
701
+
702
+ references.append({
703
+ 'index': i,
704
+ 'file_name': file_name,
705
+ 'content': content,
706
+ 'summary': summary
707
+ })
708
+
709
+ # 第二阶段:生成最终答案
710
+ final_response = ""
711
+ for chunk in generator.generate_stream(message, all_docs):
712
+ if isinstance(chunk, dict):
713
+ continue # 跳过处理数据
714
+ final_response += chunk
715
+
716
+ # 构建回复
717
+ return jsonify({
718
+ "success": True,
719
+ "message": final_response,
720
+ "tools": suggested_plugins,
721
+ "references": references
722
+ })
723
+
724
+ except Exception as e:
725
+ import traceback
726
+ traceback.print_exc()
727
+ return jsonify({
728
+ "success": False,
729
+ "message": f"处理查询时出错: {str(e)}"
730
+ }), 500
731
+
732
+ except Exception as e:
733
+ import traceback
734
+ traceback.print_exc()
735
+ return jsonify({"success": False, "message": str(e)}), 500
736
+
737
+ # API端点:获取学生活动记录
738
+ @app.route('/api/student/activities', methods=['GET'])
739
+ @student_required
740
+ def get_student_activities():
741
+ """获取学生活动记录"""
742
+ try:
743
+ username = session.get('username')
744
+
745
+ # 获取该学生的活动记录
746
+ activities = student_activities.get(username, [])
747
+
748
+ # 格式化输出
749
+ formatted_activities = []
750
+ for activity in activities:
751
+ # 格式化时间显示
752
+ timestamp = activity['timestamp']
753
+ current_time = int(time.time())
754
+
755
+ if current_time - timestamp < 86400: # 24小时内
756
+ if current_time - timestamp < 3600: # 1小时内
757
+ time_text = f"{(current_time - timestamp) // 60}分钟前"
758
+ else:
759
+ time_text = f"今天 {time.strftime('%H:%M', time.localtime(timestamp))}"
760
+ elif current_time - timestamp < 172800: # 48小时内
761
+ time_text = f"昨天 {time.strftime('%H:%M', time.localtime(timestamp))}"
762
+ else:
763
+ time_text = time.strftime('%m月%d日 %H:%M', time.localtime(timestamp))
764
+
765
+ formatted_activities.append({
766
+ "type": activity['type'],
767
+ "title": activity['title'],
768
+ "time": time_text,
769
+ "agent_id": activity.get('agent_id'),
770
+ "agent_name": activity.get('agent_name')
771
+ })
772
+
773
+ return jsonify({
774
+ "success": True,
775
+ "activities": formatted_activities
776
+ })
777
+
778
+ except Exception as e:
779
+ import traceback
780
+ traceback.print_exc()
781
+ return jsonify({
782
+ "success": False,
783
+ "message": str(e)
784
+ }), 500
785
+
786
+ # API端点:验证访问令牌
787
+ @app.route('/api/verify_token', methods=['POST'])
788
+ def verify_token():
789
+ """验证访问令牌有效性"""
790
+ try:
791
+ data = request.json
792
+ token = data.get('token', '')
793
+ agent_id = data.get('agent_id', '')
794
+
795
+ if not token:
796
+ return jsonify({
797
+ "success": False,
798
+ "message": "未提供访问令牌"
799
+ }), 400
800
+
801
+ # 如果提供了agent_id,验证特定Agent的令牌
802
+ if agent_id:
803
+ agent_path = os.path.join('agents', f"{agent_id}.json")
804
+ if not os.path.exists(agent_path):
805
+ return jsonify({
806
+ "success": False,
807
+ "message": "Agent不存在"
808
+ }), 404
809
+
810
+ with open(agent_path, 'r', encoding='utf-8') as f:
811
+ agent_config = json.load(f)
812
+
813
+ # 验证令牌
814
+ if "distributions" in agent_config:
815
+ for dist in agent_config["distributions"]:
816
+ if dist.get("token") == token:
817
+ # 检查是否过期
818
+ if dist.get("expires_at", 0) > 0 and dist.get("expires_at", 0) < time.time():
819
+ return jsonify({
820
+ "success": False,
821
+ "message": "访问令牌已过期"
822
+ })
823
+
824
+ return jsonify({
825
+ "success": True,
826
+ "agent": {
827
+ "id": agent_id,
828
+ "name": agent_config.get('name', 'AI学习助手'),
829
+ "description": agent_config.get('description', ''),
830
+ "subject": agent_config.get('subject', ''),
831
+ "instructor": agent_config.get('instructor', '教师')
832
+ }
833
+ })
834
+
835
+ return jsonify({
836
+ "success": False,
837
+ "message": "访问令牌无效"
838
+ })
839
+
840
+ # 如果没有提供agent_id,搜索所有Agent
841
+ valid_agent = None
842
+
843
+ for filename in os.listdir('agents'):
844
+ if filename.endswith('.json'):
845
+ agent_path = os.path.join('agents', filename)
846
+ with open(agent_path, 'r', encoding='utf-8') as f:
847
+ agent_config = json.load(f)
848
+
849
+ # 验证令牌
850
+ if "distributions" in agent_config:
851
+ for dist in agent_config["distributions"]:
852
+ if dist.get("token") == token:
853
+ # 检查是否过期
854
+ if dist.get("expires_at", 0) > 0 and dist.get("expires_at", 0) < time.time():
855
+ continue
856
+
857
+ valid_agent = {
858
+ "id": agent_config.get('id'),
859
+ "name": agent_config.get('name', 'AI学习助手'),
860
+ "description": agent_config.get('description', ''),
861
+ "subject": agent_config.get('subject', ''),
862
+ "instructor": agent_config.get('instructor', '教师')
863
+ }
864
+ break
865
+
866
+ if valid_agent:
867
+ break
868
+
869
+ if valid_agent:
870
+ return jsonify({
871
+ "success": True,
872
+ "agent": valid_agent
873
+ })
874
+
875
+ return jsonify({
876
+ "success": False,
877
+ "message": "未找到匹配的访问令牌"
878
+ })
879
+
880
+ except Exception as e:
881
+ import traceback
882
+ traceback.print_exc()
883
+ return jsonify({
884
+ "success": False,
885
+ "message": f"验证访问令牌时出错: {str(e)}"
886
+ }), 500
887
+
888
+ # API端点:获取学生的Agent列表
889
+ @app.route('/api/student/agents', methods=['GET'])
890
+ @student_required
891
+ def get_student_agents():
892
+ """获取学生可访问的Agent列表"""
893
+ try:
894
+ # 实际应用中应根据学生ID过滤
895
+ # 这里简化为获取所有Agent
896
+ agents = []
897
+
898
+ for filename in os.listdir('agents'):
899
+ if filename.endswith('.json'):
900
+ agent_path = os.path.join('agents', filename)
901
+ with open(agent_path, 'r', encoding='utf-8') as f:
902
+ agent_config = json.load(f)
903
+
904
+ # 简化信息
905
+ agent_info = {
906
+ "id": agent_config.get('id'),
907
+ "name": agent_config.get('name', 'AI学习助手'),
908
+ "description": agent_config.get('description', ''),
909
+ "subject": agent_config.get('subject', ''),
910
+ "instructor": agent_config.get('instructor', '教师'),
911
+ "plugins": agent_config.get('plugins', []),
912
+ "last_used": agent_config.get('stats', {}).get('last_used')
913
+ }
914
+
915
+ # 添加TOKEN(实际应用中应严格控制令牌访问)
916
+ if "distributions" in agent_config and agent_config["distributions"]:
917
+ # 仅添加第一个分发的令牌
918
+ agent_info["token"] = agent_config["distributions"][0].get("token")
919
+
920
+ agents.append(agent_info)
921
+
922
+ # 按最后使用时间排序
923
+ agents.sort(key=lambda x: x.get('last_used', 0) or 0, reverse=True)
924
+
925
+ return jsonify({
926
+ "success": True,
927
+ "agents": agents
928
+ })
929
+
930
+ except Exception as e:
931
+ import traceback
932
+ traceback.print_exc()
933
+ return jsonify({
934
+ "success": False,
935
+ "message": str(e)
936
+ }), 500
937
+
938
+ if __name__ == '__main__':
939
  app.run(debug=True, host='0.0.0.0', port=7860)
modules/agent_builder/routes.py CHANGED
@@ -1,504 +1,504 @@
1
- # modules/agent_builder/routes.py
2
- from flask import Blueprint, request, jsonify
3
- import os
4
- import json
5
- import time
6
- import uuid
7
- import requests
8
- from config import STREAM_API_KEY, STREAM_BASE_URL, DEFAULT_MODEL
9
-
10
- agent_builder_bp = Blueprint('agent_builder', __name__)
11
-
12
- # 确保Agent存储目录存在
13
- AGENTS_DIR = 'agents'
14
- os.makedirs(AGENTS_DIR, exist_ok=True)
15
-
16
- @agent_builder_bp.route('/create', methods=['POST'])
17
- def create_agent():
18
- """创建新的Agent"""
19
- try:
20
- data = request.json
21
- name = data.get('name')
22
- description = data.get('description', '')
23
- subject = data.get('subject', name) # 默认使用名称作为学科
24
- instructor = data.get('instructor', '教师') # 默认使用教师作为指导者
25
- plugins = data.get('plugins', [])
26
- knowledge_bases = data.get('knowledge_bases', [])
27
- workflow = data.get('workflow')
28
-
29
- if not name:
30
- return jsonify({
31
- "success": False,
32
- "message": "Agent名称不能为空"
33
- }), 400
34
-
35
- # 创建Agent ID
36
- agent_id = f"agent_{uuid.uuid4().hex[:8]}_{int(time.time())}"
37
-
38
- # 构建Agent配置
39
- agent_config = {
40
- "id": agent_id,
41
- "name": name,
42
- "description": description,
43
- "subject": subject, # 添加学科
44
- "instructor": instructor, # 添加指导者
45
- "created_at": int(time.time()),
46
- "plugins": plugins,
47
- "knowledge_bases": knowledge_bases,
48
- "workflow": workflow or {
49
- "nodes": [],
50
- "edges": []
51
- },
52
- "distributions": [],
53
- "stats": {
54
- "usage_count": 0,
55
- "last_used": None
56
- }
57
- }
58
-
59
- # 保存Agent配置
60
- with open(os.path.join(AGENTS_DIR, f"{agent_id}.json"), 'w', encoding='utf-8') as f:
61
- json.dump(agent_config, f, ensure_ascii=False, indent=2)
62
-
63
- return jsonify({
64
- "success": True,
65
- "agent_id": agent_id,
66
- "message": f"Agent '{name}' 创建成功"
67
- })
68
-
69
- except Exception as e:
70
- import traceback
71
- traceback.print_exc()
72
- return jsonify({
73
- "success": False,
74
- "message": str(e)
75
- }), 500
76
-
77
- @agent_builder_bp.route('/list', methods=['GET'])
78
- def list_agents():
79
- """获取所有Agent列表"""
80
- try:
81
- agents = []
82
-
83
- for filename in os.listdir(AGENTS_DIR):
84
- if filename.endswith('.json'):
85
- with open(os.path.join(AGENTS_DIR, filename), 'r', encoding='utf-8') as f:
86
- agent_config = json.load(f)
87
-
88
- # 简化版本,只返回关键信息
89
- agents.append({
90
- "id": agent_config.get("id"),
91
- "name": agent_config.get("name"),
92
- "description": agent_config.get("description"),
93
- "subject": agent_config.get("subject", agent_config.get("name")),
94
- "instructor": agent_config.get("instructor", "教师"),
95
- "created_at": agent_config.get("created_at"),
96
- "plugins": agent_config.get("plugins", []),
97
- "knowledge_bases": agent_config.get("knowledge_bases", []),
98
- "usage_count": agent_config.get("stats", {}).get("usage_count", 0),
99
- "distribution_count": len(agent_config.get("distributions", []))
100
- })
101
-
102
- # 按创建时间排序
103
- agents.sort(key=lambda x: x.get("created_at", 0), reverse=True)
104
-
105
- return jsonify({
106
- "success": True,
107
- "agents": agents
108
- })
109
-
110
- except Exception as e:
111
- import traceback
112
- traceback.print_exc()
113
- return jsonify({
114
- "success": False,
115
- "message": str(e)
116
- }), 500
117
-
118
- @agent_builder_bp.route('/<agent_id>', methods=['GET'])
119
- def get_agent(agent_id):
120
- """获取特定Agent的配置"""
121
- try:
122
- agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
123
-
124
- if not os.path.exists(agent_path):
125
- return jsonify({
126
- "success": False,
127
- "message": "Agent不存在"
128
- }), 404
129
-
130
- with open(agent_path, 'r', encoding='utf-8') as f:
131
- agent_config = json.load(f)
132
-
133
- return jsonify({
134
- "success": True,
135
- "agent": agent_config
136
- })
137
-
138
- except Exception as e:
139
- import traceback
140
- traceback.print_exc()
141
- return jsonify({
142
- "success": False,
143
- "message": str(e)
144
- }), 500
145
-
146
- @agent_builder_bp.route('/<agent_id>', methods=['PUT'])
147
- def update_agent(agent_id):
148
- """更新Agent配置"""
149
- try:
150
- data = request.json
151
- agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
152
-
153
- if not os.path.exists(agent_path):
154
- return jsonify({
155
- "success": False,
156
- "message": "Agent不存在"
157
- }), 404
158
-
159
- # 读取现有配置
160
- with open(agent_path, 'r', encoding='utf-8') as f:
161
- agent_config = json.load(f)
162
-
163
- # 更新允许的字段
164
- if 'name' in data:
165
- agent_config['name'] = data['name']
166
-
167
- if 'description' in data:
168
- agent_config['description'] = data['description']
169
-
170
- if 'plugins' in data:
171
- agent_config['plugins'] = data['plugins']
172
-
173
- if 'knowledge_bases' in data:
174
- agent_config['knowledge_bases'] = data['knowledge_bases']
175
-
176
- if 'workflow' in data:
177
- agent_config['workflow'] = data['workflow']
178
-
179
- # 添加更新时间
180
- agent_config['updated_at'] = int(time.time())
181
-
182
- # 保存更新后的配置
183
- with open(agent_path, 'w', encoding='utf-8') as f:
184
- json.dump(agent_config, f, ensure_ascii=False, indent=2)
185
-
186
- return jsonify({
187
- "success": True,
188
- "message": "Agent更新成功"
189
- })
190
-
191
- except Exception as e:
192
- import traceback
193
- traceback.print_exc()
194
- return jsonify({
195
- "success": False,
196
- "message": str(e)
197
- }), 500
198
-
199
- @agent_builder_bp.route('/<agent_id>', methods=['DELETE'])
200
- def delete_agent(agent_id):
201
- """删除Agent"""
202
- try:
203
- agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
204
-
205
- if not os.path.exists(agent_path):
206
- return jsonify({
207
- "success": False,
208
- "message": "Agent不存在"
209
- }), 404
210
-
211
- # 删除Agent配置文件
212
- os.remove(agent_path)
213
-
214
- return jsonify({
215
- "success": True,
216
- "message": "Agent删除成功"
217
- })
218
-
219
- except Exception as e:
220
- import traceback
221
- traceback.print_exc()
222
- return jsonify({
223
- "success": False,
224
- "message": str(e)
225
- }), 500
226
-
227
- @agent_builder_bp.route('/<agent_id>/distribute', methods=['POST'])
228
- def distribute_agent(agent_id):
229
- """为Agent创建分发链接"""
230
- try:
231
- data = request.json
232
- expires_in = data.get('expires_in', 0) # 0表示永不过期
233
-
234
- agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
235
-
236
- if not os.path.exists(agent_path):
237
- return jsonify({
238
- "success": False,
239
- "message": "Agent不存在"
240
- }), 404
241
-
242
- # 读取Agent配置
243
- with open(agent_path, 'r', encoding='utf-8') as f:
244
- agent_config = json.load(f)
245
-
246
- # 创建访问令牌
247
- token = uuid.uuid4().hex
248
-
249
- # 计算过期时间
250
- expiry = int(time.time() + expires_in) if expires_in > 0 else 0
251
-
252
- # 创建分发记录
253
- distribution = {
254
- "id": f"dist_{uuid.uuid4().hex[:6]}",
255
- "created_at": int(time.time()),
256
- "token": token,
257
- "expires_at": expiry,
258
- "usage_count": 0
259
- }
260
-
261
- # 更新Agent配置
262
- if "distributions" not in agent_config:
263
- agent_config["distributions"] = []
264
-
265
- agent_config["distributions"].append(distribution)
266
-
267
- # 保存更新后的配置
268
- with open(agent_path, 'w', encoding='utf-8') as f:
269
- json.dump(agent_config, f, ensure_ascii=False, indent=2)
270
-
271
- # 构建访问链接
272
- access_link = f"/student/{agent_id}?token={token}"
273
-
274
- return jsonify({
275
- "success": True,
276
- "distribution": {
277
- "id": distribution["id"],
278
- "link": access_link,
279
- "token": token,
280
- "expires_at": expiry
281
- },
282
- "message": "分发链接创建成功"
283
- })
284
-
285
- except Exception as e:
286
- import traceback
287
- traceback.print_exc()
288
- return jsonify({
289
- "success": False,
290
- "message": str(e)
291
- }), 500
292
-
293
- @agent_builder_bp.route('/ai-assist', methods=['POST'])
294
- def ai_assisted_workflow():
295
- """使用AI辅助Agent工作流编排"""
296
- try:
297
- data = request.json
298
- description = data.get('description', '')
299
- subject = data.get('subject', '通用学科')
300
- knowledge_bases = data.get('knowledge_bases', [])
301
- plugins = data.get('plugins', [])
302
-
303
- if not description:
304
- return jsonify({
305
- "success": False,
306
- "message": "请提供Agent描述"
307
- }), 400
308
-
309
- # 获取所有可用的知识库
310
- available_knowledge_bases = []
311
- try:
312
- # ���取知识库列表的API调用
313
- kb_response = requests.get("https://samlax12-agent.hf.space/api/knowledge")
314
- if kb_response.status_code == 200:
315
- kb_data = kb_response.json()
316
- if kb_data.get("success"):
317
- for kb in kb_data.get("data", []):
318
- available_knowledge_bases.append(kb["id"])
319
- except:
320
- # 如果API调用失败,使用默认值
321
- pass
322
-
323
- # 可用的插件
324
- available_plugins = ["code", "visualization", "mindmap"]
325
-
326
- # 构建提示
327
- system_prompt = """你是一个专业的AI工作流设计师。你需要根据用户描述,设计一个适合教育场景的Agent工作流。
328
- 你不仅要设计工作流结构,还要推荐合适的知识库和插件。请确保工作流逻辑合理,能够满足用户的需求。
329
-
330
- 工作流应包含以下类型的节点:
331
- 1. 意图识别:识别用户输入的意图
332
- 2. 知识库查询:从指定知识库中检索信息
333
- 3. 插件调用:调用特定插件(如代码执行、3D可视化或思维导图)
334
- 4. 回复生成:生成最终回复
335
-
336
- 用户当前选择的知识库:{knowledge_bases}
337
- 系统中可用的知识库有:{available_knowledge_bases}
338
-
339
- 用户当前选择的插件:{plugins}
340
- 系统中可用的插件有:{available_plugins}
341
-
342
- 这个Agent的主题领域是:{subject}
343
-
344
- 重要:根据Agent描述,推荐最合适的知识库和插件。在推荐时,只能使用系统中真实可用的知识库和插件。
345
-
346
- 请返回三部分内容:
347
- 1. 推荐的知识库列表(只能从可用知识库中选择)
348
- 2. 推荐的插件列表(只能从可用插件中选择)
349
- 3. 完整的工作流JSON结构
350
-
351
- JSON格式示例:
352
- {{
353
- "recommended_knowledge_bases": ["rag_knowledge1", "rag_knowledge2"],
354
- "recommended_plugins": ["code", "visualization"],
355
- "workflow": {{
356
- "nodes": [
357
- {{ "id": "node1", "type": "intent_recognition", "data": {{ "name": "意图识别" }} }},
358
- {{ "id": "node2", "type": "knowledge_query", "data": {{ "name": "知识库查询", "knowledge_base_id": "rag_knowledge1" }} }},
359
- {{ "id": "node3", "type": "plugin_call", "data": {{ "name": "调用代码执行插件", "plugin_id": "code" }} }},
360
- {{ "id": "node4", "type": "generate_response", "data": {{ "name": "生成回复" }} }}
361
- ],
362
- "edges": [
363
- {{ "id": "edge1", "source": "node1", "target": "node2", "condition": "需要知识" }},
364
- {{ "id": "edge2", "source": "node1", "target": "node3", "condition": "需要代码执行" }},
365
- {{ "id": "edge3", "source": "node2", "target": "node4" }},
366
- {{ "id": "edge4", "source": "node3", "target": "node4" }}
367
- ]
368
- }}
369
- }}
370
- """
371
-
372
- system_prompt = system_prompt.format(
373
- knowledge_bases=", ".join(knowledge_bases) if knowledge_bases else "无",
374
- available_knowledge_bases=", ".join(available_knowledge_bases) if available_knowledge_bases else "无可用知识库",
375
- plugins=", ".join(plugins) if plugins else "无",
376
- available_plugins=", ".join(available_plugins),
377
- subject=subject
378
- )
379
-
380
- # 使用流式API
381
- try:
382
- headers = {
383
- "Authorization": f"Bearer {STREAM_API_KEY}",
384
- "Content-Type": "application/json"
385
- }
386
-
387
- response = requests.post(
388
- f"{STREAM_BASE_URL}/chat/completions",
389
- headers=headers,
390
- json={
391
- "model": DEFAULT_MODEL,
392
- "messages": [
393
- {"role": "system", "content": system_prompt},
394
- {"role": "user", "content": f"请为以下描述的教育Agent设计工作流并推荐知识库和插件:\n\n{description}"}
395
- ]
396
- }
397
- )
398
-
399
- if response.status_code != 200:
400
- return jsonify({
401
- "success": False,
402
- "message": f"Error code: {response.status_code} - {response.text}",
403
- "workflow": create_default_workflow(),
404
- "recommended_knowledge_bases": [],
405
- "recommended_plugins": []
406
- }), 200 # 返回200但包含错误信息和备用工作流
407
-
408
- result = response.json()
409
- content = result['choices'][0]['message']['content']
410
-
411
- except Exception as api_error:
412
- return jsonify({
413
- "success": False,
414
- "message": f"无法连接到AI模型服务: {str(api_error)}",
415
- "workflow": create_default_workflow(),
416
- "recommended_knowledge_bases": [],
417
- "recommended_plugins": []
418
- }), 200
419
-
420
- # 查找JSON部分
421
- import re
422
- json_match = re.search(r'```json\n([\s\S]*?)\n```', content)
423
-
424
- if json_match:
425
- workflow_json = json_match.group(1)
426
- else:
427
- # 尝试直接解析整个内容
428
- workflow_json = content
429
-
430
- # 解析JSON
431
- try:
432
- result_data = json.loads(workflow_json)
433
- except:
434
- # 如果解析失败,使用正则表达式清理
435
- workflow_json = re.sub(r'```json\n|\n```', '', content)
436
- try:
437
- result_data = json.loads(workflow_json)
438
- except:
439
- # 如果仍然解析失败,提取标准的JSON结构 { ... }
440
- import re
441
- json_patterns = re.findall(r'\{[\s\S]*?\}', content)
442
- if json_patterns:
443
- try:
444
- # 尝试解析最长的JSON结构
445
- longest_json = max(json_patterns, key=len)
446
- result_data = json.loads(longest_json)
447
- except:
448
- # 仍然失败,返回默认值
449
- return jsonify({
450
- "success": True,
451
- "message": "使用默认工作流(AI生成的JSON无效)",
452
- "workflow": create_default_workflow(),
453
- "recommended_knowledge_bases": [],
454
- "recommended_plugins": []
455
- })
456
- else:
457
- # 没有找到有效的JSON结构
458
- return jsonify({
459
- "success": True,
460
- "message": "使用默认工作流(未找到JSON结构)",
461
- "workflow": create_default_workflow(),
462
- "recommended_knowledge_bases": [],
463
- "recommended_plugins": []
464
- })
465
-
466
- # 提取推荐的知识库和插件
467
- recommended_knowledge_bases = result_data.get("recommended_knowledge_bases", [])
468
- recommended_plugins = result_data.get("recommended_plugins", [])
469
- workflow = result_data.get("workflow", create_default_workflow())
470
-
471
- # 验证推荐的知识库都存在
472
- valid_knowledge_bases = []
473
- for kb in recommended_knowledge_bases:
474
- if kb in available_knowledge_bases:
475
- valid_knowledge_bases.append(kb)
476
-
477
- # 验证推荐的插件都存在
478
- valid_plugins = []
479
- for plugin in recommended_plugins:
480
- if plugin in available_plugins:
481
- valid_plugins.append(plugin)
482
-
483
- return jsonify({
484
- "success": True,
485
- "workflow": workflow,
486
- "recommended_knowledge_bases": valid_knowledge_bases,
487
- "recommended_plugins": valid_plugins,
488
- "message": "已成功创建工作流"
489
- })
490
-
491
- except Exception as e:
492
- import traceback
493
- traceback.print_exc()
494
- return jsonify({
495
- "success": False,
496
- "message": str(e)
497
- }), 500
498
-
499
- def create_default_workflow():
500
- """创建一个默认的空工作流"""
501
- return {
502
- "nodes": [],
503
- "edges": []
504
  }
 
1
+ # modules/agent_builder/routes.py
2
+ from flask import Blueprint, request, jsonify
3
+ import os
4
+ import json
5
+ import time
6
+ import uuid
7
+ import requests
8
+ from config import STREAM_API_KEY, STREAM_BASE_URL, DEFAULT_MODEL
9
+
10
+ agent_builder_bp = Blueprint('agent_builder', __name__)
11
+
12
+ # 确保Agent存储目录存在
13
+ AGENTS_DIR = 'agents'
14
+ os.makedirs(AGENTS_DIR, exist_ok=True)
15
+
16
+ @agent_builder_bp.route('/create', methods=['POST'])
17
+ def create_agent():
18
+ """创建新的Agent"""
19
+ try:
20
+ data = request.json
21
+ name = data.get('name')
22
+ description = data.get('description', '')
23
+ subject = data.get('subject', name) # 默认使用名称作为学科
24
+ instructor = data.get('instructor', '教师') # 默认使用教师作为指导者
25
+ plugins = data.get('plugins', [])
26
+ knowledge_bases = data.get('knowledge_bases', [])
27
+ workflow = data.get('workflow')
28
+
29
+ if not name:
30
+ return jsonify({
31
+ "success": False,
32
+ "message": "Agent名称不能为空"
33
+ }), 400
34
+
35
+ # 创建Agent ID
36
+ agent_id = f"agent_{uuid.uuid4().hex[:8]}_{int(time.time())}"
37
+
38
+ # 构建Agent配置
39
+ agent_config = {
40
+ "id": agent_id,
41
+ "name": name,
42
+ "description": description,
43
+ "subject": subject, # 添加学科
44
+ "instructor": instructor, # 添加指导者
45
+ "created_at": int(time.time()),
46
+ "plugins": plugins,
47
+ "knowledge_bases": knowledge_bases,
48
+ "workflow": workflow or {
49
+ "nodes": [],
50
+ "edges": []
51
+ },
52
+ "distributions": [],
53
+ "stats": {
54
+ "usage_count": 0,
55
+ "last_used": None
56
+ }
57
+ }
58
+
59
+ # 保存Agent配置
60
+ with open(os.path.join(AGENTS_DIR, f"{agent_id}.json"), 'w', encoding='utf-8') as f:
61
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
62
+
63
+ return jsonify({
64
+ "success": True,
65
+ "agent_id": agent_id,
66
+ "message": f"Agent '{name}' 创建成功"
67
+ })
68
+
69
+ except Exception as e:
70
+ import traceback
71
+ traceback.print_exc()
72
+ return jsonify({
73
+ "success": False,
74
+ "message": str(e)
75
+ }), 500
76
+
77
+ @agent_builder_bp.route('/list', methods=['GET'])
78
+ def list_agents():
79
+ """获取所有Agent列表"""
80
+ try:
81
+ agents = []
82
+
83
+ for filename in os.listdir(AGENTS_DIR):
84
+ if filename.endswith('.json'):
85
+ with open(os.path.join(AGENTS_DIR, filename), 'r', encoding='utf-8') as f:
86
+ agent_config = json.load(f)
87
+
88
+ # 简化版本,只返回关键信息
89
+ agents.append({
90
+ "id": agent_config.get("id"),
91
+ "name": agent_config.get("name"),
92
+ "description": agent_config.get("description"),
93
+ "subject": agent_config.get("subject", agent_config.get("name")),
94
+ "instructor": agent_config.get("instructor", "教师"),
95
+ "created_at": agent_config.get("created_at"),
96
+ "plugins": agent_config.get("plugins", []),
97
+ "knowledge_bases": agent_config.get("knowledge_bases", []),
98
+ "usage_count": agent_config.get("stats", {}).get("usage_count", 0),
99
+ "distribution_count": len(agent_config.get("distributions", []))
100
+ })
101
+
102
+ # 按创建时间排序
103
+ agents.sort(key=lambda x: x.get("created_at", 0), reverse=True)
104
+
105
+ return jsonify({
106
+ "success": True,
107
+ "agents": agents
108
+ })
109
+
110
+ except Exception as e:
111
+ import traceback
112
+ traceback.print_exc()
113
+ return jsonify({
114
+ "success": False,
115
+ "message": str(e)
116
+ }), 500
117
+
118
+ @agent_builder_bp.route('/<agent_id>', methods=['GET'])
119
+ def get_agent(agent_id):
120
+ """获取特定Agent的配置"""
121
+ try:
122
+ agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
123
+
124
+ if not os.path.exists(agent_path):
125
+ return jsonify({
126
+ "success": False,
127
+ "message": "Agent不存在"
128
+ }), 404
129
+
130
+ with open(agent_path, 'r', encoding='utf-8') as f:
131
+ agent_config = json.load(f)
132
+
133
+ return jsonify({
134
+ "success": True,
135
+ "agent": agent_config
136
+ })
137
+
138
+ except Exception as e:
139
+ import traceback
140
+ traceback.print_exc()
141
+ return jsonify({
142
+ "success": False,
143
+ "message": str(e)
144
+ }), 500
145
+
146
+ @agent_builder_bp.route('/<agent_id>', methods=['PUT'])
147
+ def update_agent(agent_id):
148
+ """更新Agent配置"""
149
+ try:
150
+ data = request.json
151
+ agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
152
+
153
+ if not os.path.exists(agent_path):
154
+ return jsonify({
155
+ "success": False,
156
+ "message": "Agent不存在"
157
+ }), 404
158
+
159
+ # 读取现有配置
160
+ with open(agent_path, 'r', encoding='utf-8') as f:
161
+ agent_config = json.load(f)
162
+
163
+ # 更新允许的字段
164
+ if 'name' in data:
165
+ agent_config['name'] = data['name']
166
+
167
+ if 'description' in data:
168
+ agent_config['description'] = data['description']
169
+
170
+ if 'plugins' in data:
171
+ agent_config['plugins'] = data['plugins']
172
+
173
+ if 'knowledge_bases' in data:
174
+ agent_config['knowledge_bases'] = data['knowledge_bases']
175
+
176
+ if 'workflow' in data:
177
+ agent_config['workflow'] = data['workflow']
178
+
179
+ # 添加更新时间
180
+ agent_config['updated_at'] = int(time.time())
181
+
182
+ # 保存更新后的配置
183
+ with open(agent_path, 'w', encoding='utf-8') as f:
184
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
185
+
186
+ return jsonify({
187
+ "success": True,
188
+ "message": "Agent更新成功"
189
+ })
190
+
191
+ except Exception as e:
192
+ import traceback
193
+ traceback.print_exc()
194
+ return jsonify({
195
+ "success": False,
196
+ "message": str(e)
197
+ }), 500
198
+
199
+ @agent_builder_bp.route('/<agent_id>', methods=['DELETE'])
200
+ def delete_agent(agent_id):
201
+ """删除Agent"""
202
+ try:
203
+ agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
204
+
205
+ if not os.path.exists(agent_path):
206
+ return jsonify({
207
+ "success": False,
208
+ "message": "Agent不存在"
209
+ }), 404
210
+
211
+ # 删除Agent配置文件
212
+ os.remove(agent_path)
213
+
214
+ return jsonify({
215
+ "success": True,
216
+ "message": "Agent删除成功"
217
+ })
218
+
219
+ except Exception as e:
220
+ import traceback
221
+ traceback.print_exc()
222
+ return jsonify({
223
+ "success": False,
224
+ "message": str(e)
225
+ }), 500
226
+
227
+ @agent_builder_bp.route('/<agent_id>/distribute', methods=['POST'])
228
+ def distribute_agent(agent_id):
229
+ """为Agent创建分发链接"""
230
+ try:
231
+ data = request.json
232
+ expires_in = data.get('expires_in', 0) # 0表示永不过期
233
+
234
+ agent_path = os.path.join(AGENTS_DIR, f"{agent_id}.json")
235
+
236
+ if not os.path.exists(agent_path):
237
+ return jsonify({
238
+ "success": False,
239
+ "message": "Agent不存在"
240
+ }), 404
241
+
242
+ # 读取Agent配置
243
+ with open(agent_path, 'r', encoding='utf-8') as f:
244
+ agent_config = json.load(f)
245
+
246
+ # 创建访问令牌
247
+ token = uuid.uuid4().hex
248
+
249
+ # 计算过期时间
250
+ expiry = int(time.time() + expires_in) if expires_in > 0 else 0
251
+
252
+ # 创建分发记录
253
+ distribution = {
254
+ "id": f"dist_{uuid.uuid4().hex[:6]}",
255
+ "created_at": int(time.time()),
256
+ "token": token,
257
+ "expires_at": expiry,
258
+ "usage_count": 0
259
+ }
260
+
261
+ # 更新Agent配置
262
+ if "distributions" not in agent_config:
263
+ agent_config["distributions"] = []
264
+
265
+ agent_config["distributions"].append(distribution)
266
+
267
+ # 保存更新后的配置
268
+ with open(agent_path, 'w', encoding='utf-8') as f:
269
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
270
+
271
+ # 构建访问链接
272
+ access_link = f"/student/{agent_id}?token={token}"
273
+
274
+ return jsonify({
275
+ "success": True,
276
+ "distribution": {
277
+ "id": distribution["id"],
278
+ "link": access_link,
279
+ "token": token,
280
+ "expires_at": expiry
281
+ },
282
+ "message": "分发链接创建成功"
283
+ })
284
+
285
+ except Exception as e:
286
+ import traceback
287
+ traceback.print_exc()
288
+ return jsonify({
289
+ "success": False,
290
+ "message": str(e)
291
+ }), 500
292
+
293
+ @agent_builder_bp.route('/ai-assist', methods=['POST'])
294
+ def ai_assisted_workflow():
295
+ """使用AI辅助Agent工作流编排"""
296
+ try:
297
+ data = request.json
298
+ description = data.get('description', '')
299
+ subject = data.get('subject', '通用学科')
300
+ knowledge_bases = data.get('knowledge_bases', [])
301
+ plugins = data.get('plugins', [])
302
+
303
+ if not description:
304
+ return jsonify({
305
+ "success": False,
306
+ "message": "请提供Agent描述"
307
+ }), 400
308
+
309
+ # 获取所有可用的知识库
310
+ available_knowledge_bases = []
311
+ try:
312
+ # 获取知识库列表的API调用
313
+ kb_response = requests.get("https://samlax12-agent.hf.space/api/knowledge")
314
+ if kb_response.status_code == 200:
315
+ kb_data = kb_response.json()
316
+ if kb_data.get("success"):
317
+ for kb in kb_data.get("data", []):
318
+ available_knowledge_bases.append(kb["id"])
319
+ except:
320
+ # 如果API调用失败,使用默认值
321
+ pass
322
+
323
+ # 可用的插件
324
+ available_plugins = ["code", "visualization", "mindmap"]
325
+
326
+ # 构建提示
327
+ system_prompt = """你是一个专业的AI工作流设计师。你需要根据用户描述,设计一个适合教育场景的Agent工作流。
328
+ 你不仅要设计工作流结构,还要推荐合适的知识库和插件。请确保工作流逻辑合理,能够满足用户的需求。
329
+
330
+ 工作流应包含以下类型的节点:
331
+ 1. 意图识别:识别用户输入的意图
332
+ 2. 知识库查询:从指定知识库中检索信息
333
+ 3. 插件调用:调用特定插件(如代码执行、3D可视化或思维导图)
334
+ 4. 回复生成:生成最终回复
335
+
336
+ 用户当前选择的知识库:{knowledge_bases}
337
+ 系统中可用的知识库有:{available_knowledge_bases}
338
+
339
+ 用户当前选择的插件:{plugins}
340
+ 系统中可用的插件有:{available_plugins}
341
+
342
+ 这个Agent的主题领域是:{subject}
343
+
344
+ 重要:根据Agent描述,推荐最合适的知识库和插件。在推荐时,只能使用系统中真实可用的知识库和插件。
345
+
346
+ 请返回三部分内容:
347
+ 1. 推荐的知识库列表(只能从可用知识库中选择)
348
+ 2. 推荐的插件列表(只能从可用插件中选择)
349
+ 3. 完整的工作流JSON结构
350
+
351
+ JSON格式示例:
352
+ {{
353
+ "recommended_knowledge_bases": ["rag_knowledge1", "rag_knowledge2"],
354
+ "recommended_plugins": ["code", "visualization"],
355
+ "workflow": {{
356
+ "nodes": [
357
+ {{ "id": "node1", "type": "intent_recognition", "data": {{ "name": "意图识别" }} }},
358
+ {{ "id": "node2", "type": "knowledge_query", "data": {{ "name": "知识库查询", "knowledge_base_id": "rag_knowledge1" }} }},
359
+ {{ "id": "node3", "type": "plugin_call", "data": {{ "name": "调用代码执行插件", "plugin_id": "code" }} }},
360
+ {{ "id": "node4", "type": "generate_response", "data": {{ "name": "生成回复" }} }}
361
+ ],
362
+ "edges": [
363
+ {{ "id": "edge1", "source": "node1", "target": "node2", "condition": "需要知识" }},
364
+ {{ "id": "edge2", "source": "node1", "target": "node3", "condition": "需要代码执行" }},
365
+ {{ "id": "edge3", "source": "node2", "target": "node4" }},
366
+ {{ "id": "edge4", "source": "node3", "target": "node4" }}
367
+ ]
368
+ }}
369
+ }}
370
+ """
371
+
372
+ system_prompt = system_prompt.format(
373
+ knowledge_bases=", ".join(knowledge_bases) if knowledge_bases else "无",
374
+ available_knowledge_bases=", ".join(available_knowledge_bases) if available_knowledge_bases else "无可用知识库",
375
+ plugins=", ".join(plugins) if plugins else "无",
376
+ available_plugins=", ".join(available_plugins),
377
+ subject=subject
378
+ )
379
+
380
+ # 使用流式API
381
+ try:
382
+ headers = {
383
+ "Authorization": f"Bearer {STREAM_API_KEY}",
384
+ "Content-Type": "application/json"
385
+ }
386
+
387
+ response = requests.post(
388
+ f"{STREAM_BASE_URL}/chat/completions",
389
+ headers=headers,
390
+ json={
391
+ "model": DEFAULT_MODEL,
392
+ "messages": [
393
+ {"role": "system", "content": system_prompt},
394
+ {"role": "user", "content": f"请为以下描述的教育Agent设计工作流并推荐知识库和插件:\n\n{description}"}
395
+ ]
396
+ }
397
+ )
398
+
399
+ if response.status_code != 200:
400
+ return jsonify({
401
+ "success": False,
402
+ "message": f"Error code: {response.status_code} - {response.text}",
403
+ "workflow": create_default_workflow(),
404
+ "recommended_knowledge_bases": [],
405
+ "recommended_plugins": []
406
+ }), 200 # 返回200但包含错误信息和备用工作流
407
+
408
+ result = response.json()
409
+ content = result['choices'][0]['message']['content']
410
+
411
+ except Exception as api_error:
412
+ return jsonify({
413
+ "success": False,
414
+ "message": f"无法连接到AI模型服务: {str(api_error)}",
415
+ "workflow": create_default_workflow(),
416
+ "recommended_knowledge_bases": [],
417
+ "recommended_plugins": []
418
+ }), 200
419
+
420
+ # 查找JSON部分
421
+ import re
422
+ json_match = re.search(r'```json\n([\s\S]*?)\n```', content)
423
+
424
+ if json_match:
425
+ workflow_json = json_match.group(1)
426
+ else:
427
+ # 尝试直接解析整个内容
428
+ workflow_json = content
429
+
430
+ # 解析JSON
431
+ try:
432
+ result_data = json.loads(workflow_json)
433
+ except:
434
+ # 如果解析失败,使用正则表达式清理
435
+ workflow_json = re.sub(r'```json\n|\n```', '', content)
436
+ try:
437
+ result_data = json.loads(workflow_json)
438
+ except:
439
+ # 如果仍然解析失败,提取标准的JSON结构 { ... }
440
+ import re
441
+ json_patterns = re.findall(r'\{[\s\S]*?\}', content)
442
+ if json_patterns:
443
+ try:
444
+ # 尝试解析最长的JSON结构
445
+ longest_json = max(json_patterns, key=len)
446
+ result_data = json.loads(longest_json)
447
+ except:
448
+ # 仍然失败,返回默认值
449
+ return jsonify({
450
+ "success": True,
451
+ "message": "使用默认工作流(AI生成的JSON无效)",
452
+ "workflow": create_default_workflow(),
453
+ "recommended_knowledge_bases": [],
454
+ "recommended_plugins": []
455
+ })
456
+ else:
457
+ # 没有找到有效的JSON结构
458
+ return jsonify({
459
+ "success": True,
460
+ "message": "使用默认工作流(未找到JSON结构)",
461
+ "workflow": create_default_workflow(),
462
+ "recommended_knowledge_bases": [],
463
+ "recommended_plugins": []
464
+ })
465
+
466
+ # 提取推荐的知识库和插件
467
+ recommended_knowledge_bases = result_data.get("recommended_knowledge_bases", [])
468
+ recommended_plugins = result_data.get("recommended_plugins", [])
469
+ workflow = result_data.get("workflow", create_default_workflow())
470
+
471
+ # 验证推荐的知识库都存在
472
+ valid_knowledge_bases = []
473
+ for kb in recommended_knowledge_bases:
474
+ if kb in available_knowledge_bases:
475
+ valid_knowledge_bases.append(kb)
476
+
477
+ # 验证推荐的插件都存在
478
+ valid_plugins = []
479
+ for plugin in recommended_plugins:
480
+ if plugin in available_plugins:
481
+ valid_plugins.append(plugin)
482
+
483
+ return jsonify({
484
+ "success": True,
485
+ "workflow": workflow,
486
+ "recommended_knowledge_bases": valid_knowledge_bases,
487
+ "recommended_plugins": valid_plugins,
488
+ "message": "已成功创建工作流"
489
+ })
490
+
491
+ except Exception as e:
492
+ import traceback
493
+ traceback.print_exc()
494
+ return jsonify({
495
+ "success": False,
496
+ "message": str(e)
497
+ }), 500
498
+
499
+ def create_default_workflow():
500
+ """创建一个默认的空工作流"""
501
+ return {
502
+ "nodes": [],
503
+ "edges": []
504
  }
modules/knowledge_base/generator.py CHANGED
@@ -1,396 +1,396 @@
1
- # modules/knowledge_base/generator.py
2
- from typing import List, Dict, Generator, Union, Optional, Any
3
- import requests
4
- import os
5
- import json
6
- import time
7
- import re
8
- from dotenv import load_dotenv
9
-
10
- load_dotenv()
11
-
12
- class Generator:
13
- def __init__(self, subject="", instructor=""):
14
- # Set defaults if not provided
15
- self.subject = subject or "通用学科"
16
- self.instructor = instructor or "教师"
17
-
18
- # 基础API配置
19
- self.api_key = os.getenv("API_KEY")
20
- self.api_base = os.getenv("BASE_URL")
21
-
22
- # 流式文本问答配置
23
- self.stream_api_key = os.getenv("STREAM_API_KEY")
24
- self.stream_api_base = os.getenv("STREAM_BASE_URL")
25
- self.stream_model = os.getenv("STREAM_MODEL")
26
-
27
- # 通用提示词模板 - 填充主题和讲师信息
28
- self.system_prompt = self.get_system_prompt_template().format(
29
- subject=self.subject,
30
- instructor=self.instructor
31
- )
32
-
33
- def get_system_prompt_template(self):
34
- """返回可定制的系统提示词模板"""
35
- return """<system>
36
- 你是一位{subject}课程的智能助教,由{instructor}指导开发。你的目标是帮助学生理解和掌握{subject}课程的关键概念、原理和方法。
37
-
38
- <knowledge_base>
39
- 你拥有《{subject}》课程的专业知识库,包含教材内容、课件、习题解析等材料。当回答问题时,你应该优先使用知识库中检索到的相关内容,而不是依赖你的通用知识。
40
- </knowledge_base>
41
-
42
- <role_definition>
43
- 作为{subject}助教,你应该:
44
- 1. 用专业且易于理解的方式解释复杂概念
45
- 2. 提供准确的技术信息和计算示例
46
- 3. 在适当时使用比喻或类比帮助理解
47
- 4. 引导学生思考而不是直接给出所有答案
48
- 5. ⁠提供进一步学习的建议和资源
49
- </role_definition>
50
-
51
- <answering_guidelines>
52
- 当回答问题时,请遵循以下原则:
53
- 1. 先从知识库中检索与问题最相关的内容
54
- 2. 将检索结果整合成连贯、清晰的回答
55
- 3. 保持学术严谨性,确保概念解释和计算过程准确无误
56
- 4. 使用专业术语的同时,确保解释足够通俗易懂
57
- 5. 回答问题时注明知识来源,例如"根据教材第X章..."
58
- 6. 当遇到计算题时,展示完整的计算步骤和思路
59
- 7. 当知识库中没有直接相关内容时,明确告知学生并提供基于可靠原理的解答
60
- 8. 对于概念性问题,先给出简短定义,再补充详细解释和例子
61
- </answering_guidelines>
62
-
63
- <response_format>
64
- 对于不同类型的问题,采用不同的回答格式:
65
- 1. 概念解释类问题:
66
- - 先给出简明定义
67
- - 提供详细解释
68
- - 举例说明
69
- - 补充相关知识点连接
70
- 2. 计算类问题:
71
- - 明确列出已知条件和所求内容
72
- - 说明解题思路和所用公式
73
- - 展示详细计算步骤
74
- - 给出最终答案并解释其含义
75
- 3. 综合分析类问题:
76
- - 分点阐述相关知识点
77
- - 提供分析框架
78
- - 给出结论和建议
79
- </response_format>
80
- <video_content_guidelines>
81
- 当检索到视频内容时,你应该:
82
- 1. 明确告知用户你找到了相关视频资源
83
- 2. 提供视频链接并确保包含时间戳
84
- 3. 简要描述视频内容和主要学习点
85
- 4. 建议用户观看视频以获得可视化理解
86
- 5. 在回答结束时,再次强调视频资源的价值
87
-
88
- 对于所有包含视频链接的回答,必须以下述格式呈现视频资源:
89
-
90
- 推荐学习资源:
91
- [视频标题] - [视频链接]
92
- </video_content_guidelines>
93
- </system>"""
94
-
95
- def get_tool_selection_template(self):
96
- """返回工具选择提示词模板"""
97
- return """<system>
98
- 你是{subject}课程智能助教系统的决策组件。你的唯一任务是判断用户问题类型并决定是否调用知识库工具,以及提取精准的搜索关键词。
99
-
100
- <decision_guidelines>
101
- 对于所有涉及{subject}专业知识的问题,必须调用至少一个知识库工具。系统依赖这些知识库提供准确信息,而不是依赖模型的通用知识。
102
-
103
- 判断标准:
104
- 1. 所有涉及课程概念、原理、计算方法的问题 → 必须调用相关知识库
105
- 2. 所有需要专业解释或例子的问题 → 必须调用相关知识库
106
- 3. 所有学习指导或复习相关的问题 → 必须调用相关知识库
107
- 4. 仅对于纯粹的问候语或与课程无关的闲聊 → 不调用任何工具
108
-
109
- 当决定调用知识库时,提取的关键词必须满足以下条件:
110
- 1. 准确反映问题的核心主题
111
- 2. 包含{subject}相关的专业术语或技术名词
112
- 3. 去除无关紧要的修饰词
113
- 4. 每个关键词尽量简洁,优先使用专业术语
114
- 5. 提供2-5个关键词,确保覆盖问题的核心概念
115
- </decision_guidelines>
116
-
117
- <tool_selection_examples>
118
- 例1:问题 - "请详细解释相关概念的表示方法和计算过程。"
119
- 判断:需要专业知识解释
120
- 工具选择:教材知识库(包含基础概念和详细解释)
121
- 关键词:["表示方法", "计算过程"]
122
-
123
- 例2:问题 - "这个概念的工作原理是什么?能给我一个直观的例子吗?"
124
- 判断:需要专业知识解释,且需要直观演示
125
- 工具选择:教材知识库(基础概念)和视频知识库(直观演示)
126
- 关键词:["工作原理", "例子"]
127
-
128
- 例3:问题 - "你好,今天天气怎么样?"
129
- 判断:与课程无关的闲聊
130
- 工具选择:不调用任何工具
131
- 关键词:[]
132
- </tool_selection_examples>
133
-
134
- <video_knowledge_criteria>
135
- 以下情况应优先选择视频知识库工具:
136
- 1. 用户明确要求视频讲解或视频资料
137
- 2. 问题涉及复杂的步骤或流程,可能需要可视化展示
138
- 3. 问题关于动态过程的理解
139
- 4. 问题涉及图形或结构的理解
140
- 5. 问题包含"演示"、"展示"、"直观"、"可视化"等类似于需求的词语
141
- </video_knowledge_criteria>
142
-
143
- <quiz_knowledge_criteria>
144
- 以下情况应选择习题知识库工具:
145
- 1. 用户明确询问习题、例题或解题方法
146
- 2. 问题是关于如何解决特定类型的问题
147
- 3. 用户寻求考试或作业的帮助
148
- 4. 用户提出的问题形式类似于典型习题
149
- </quiz_knowledge_criteria>
150
-
151
- <tool_instruction>
152
- 你不需要回答用户问题,只需决定调用哪些工具以及提供精准的关键词数组。
153
- </tool_instruction>
154
- </system>"""
155
-
156
- def get_code_execution_prompt_template(self):
157
- """返回代码执行插件的提示词模板"""
158
- return """<code_execution>
159
- 只要当用户询问编程、代码或特别是Python相关的问题时,你必须在回答中整合代码执行插件的使用。
160
-
161
- 使用代码执行插件的指南:
162
- 1. 创建Python代码示例时,请使用正确的Markdown语法,用```python和```作为代码块的分隔符。
163
- 2. 确保你的代码示例完整、可运行,并附有适当的注释。
164
- 3. 在代码前后提供解释,帮助用户理解代码的功能和原理。
165
- 4. 当代码与用户问题相关时,明确告知用户可以使用代码执行环境运行这段代码。
166
- 5. 对于教学场景,考虑创建循序渐进的代码示例,让用户可以逐步学习和理解。
167
-
168
- 示例回答格式:
169
- "这是一个[描述]的Python程序:
170
-
171
- ```python
172
- # 你的完整、可运行的代码
173
- print('Hello, world!')
174
- ```
175
-
176
- 你可以通过点击'运行'按钮在代码执行环境中运行这段代码。
177
- 如果你想修改代码,只需在编辑器中编辑并再次运行即可。"
178
- </code_execution>"""
179
-
180
- def get_visualization_prompt_template(self):
181
- """返回可视化插件的提示词模板"""
182
- return """<visualization>
183
- 当用户询问有关数学图形、函数、几何或需要3D可视化的内容时,你应该在回答中提供一个完整的Python函数来生成3D图形。
184
-
185
- 使用3D可视化插件的指南:
186
- 1. 你必须提供一个名为create_3d_plot的Python函数,该函数不接受任何参数。
187
- 2. 这个函数应该导入必要的库(主要是numpy as np)。
188
- 3. 函数需要返回一个包含以下结构的字典:
189
- {
190
- 'x': x_data,
191
- 'y': y_data,
192
- 'z': z_data,
193
- 'type': 'surface' 或 'scatter3d' (取决于数据类型)
194
- }
195
- 4. 确保你的代码可以直接运行,无需额外修改。
196
-
197
- 示例回答格式:
198
- "下面是[数学概念]的3D可视化函数:
199
-
200
- ```python
201
- import numpy as np
202
-
203
- def create_3d_plot():
204
- # 生成数据
205
- x = np.linspace(-5, 5, 100)
206
- y = np.linspace(-5, 5, 100)
207
- X, Y = np.meshgrid(x, y)
208
- Z = np.sin(np.sqrt(X**2 + Y**2))
209
-
210
- return {
211
- 'x': X.tolist(),
212
- 'y': Y.tolist(),
213
- 'z': Z.tolist(),
214
- 'type': 'surface'
215
- }
216
- ```
217
-
218
- 这个函数创建了[概念描述]的3D图形,你可以观察[关键特征]。"
219
- </visualization>"""
220
-
221
- def get_mindmap_prompt_template(self):
222
- """返回思维导图插件的提示词模板"""
223
- return """<mindmap>
224
- 当用户需要组织和梳理知识结构、概念关系或学习规划时,你应该在回答中整合思维导图。
225
-
226
- 使用思维导图的指南:
227
- 1. 提供一个完整的思维导图结构,使用PlantUML格式。
228
- 2. 使用@startmindmap和@endmindmap标记包裹内容。
229
- 3. 使用星号(*)表示层级:*为中央主题,**为主要主题,***为子主题,****为叶子节点。
230
- 4. 确保思维导图结构清晰、逻辑合理,能够帮助用户理解知识体系。
231
-
232
- 示例格式:
233
- @startmindmap
234
- * 中心主题
235
- ** 主要分支1
236
- *** 子主题1.1
237
- **** 叶子节点1.1.1
238
- *** 子主题1.2
239
- ** 主要分支2
240
- *** 子主题2.1
241
- @endmindmap
242
-
243
- 确保思维导图涵盖主题的关键概念和它们之间的关系,帮助用户建立完整的知识体系。
244
- </mindmap>"""
245
-
246
- def extract_keywords_with_tools(self, question: str, tools: List[Dict]) -> List[Dict]:
247
- """使用工具化架构分析问题,决定使用哪些知识库以及提取关键词"""
248
- system_prompt = self.get_tool_selection_template().format(subject=self.subject)
249
-
250
- headers = {
251
- "Authorization": f"Bearer {self.stream_api_key}",
252
- "Content-Type": "application/json"
253
- }
254
-
255
- response = requests.post(
256
- f"{self.stream_api_base}/chat/completions",
257
- headers=headers,
258
- json={
259
- "model": self.stream_model,
260
- "messages": [
261
- {"role": "system", "content": system_prompt},
262
- {"role": "user", "content": question}
263
- ],
264
- "tools": tools,
265
- "tool_choice": "auto"
266
- }
267
- )
268
-
269
- if response.status_code != 200:
270
- raise Exception(f"工具调用出错: {response.text}")
271
-
272
- response_data = response.json()
273
- message = response_data["choices"][0]["message"]
274
-
275
- # 如果模型决定调用工具
276
- if "tool_calls" in message and message["tool_calls"]:
277
- return message["tool_calls"]
278
- else:
279
- # 模型没有调用工具,返回空列表
280
- return []
281
-
282
- def generate_stream(self, query: str, context_docs: List[Dict], process_data: Optional[Dict] = None) -> Generator[Union[str, Dict], None, None]:
283
- """流式生成回答 - 用于所有类型的问答"""
284
- start_time = time.time()
285
-
286
- # 构建带有引用标记的上下文
287
- context_with_refs = []
288
-
289
- # 检查是否有图片URL
290
- has_images = any(doc['metadata'].get('img_url', '') for doc in context_docs)
291
-
292
- # 检查查询是否与特定插件相关
293
- is_code_related = any(kw in query.lower() for kw in ['code', 'python', 'program', '代码', '编程', 'coding', 'script'])
294
- is_visualization_related = any(kw in query.lower() for kw in ['3d', 'graph', 'plot', 'function', 'visualization', '可视化', '图形', '函数'])
295
- is_mindmap_related = any(kw in query.lower() for kw in ['mindmap', 'mind map', 'concept map', '思维导图', '概念图', '知识图'])
296
-
297
- # 处理每个文档
298
- for i, doc in enumerate(context_docs, 1):
299
- # 直接使用文件名作为来源
300
- file_name = doc['metadata'].get('file_name', '未知文件')
301
- img_url = doc['metadata'].get('img_url', '')
302
-
303
- # 构建文档内容,如果有图片URL则包含在内
304
- content = doc['content']
305
- if img_url:
306
- content += f"\n[图片地址: {img_url}]"
307
-
308
- context_with_refs.append(f"[{i}] {content}\n来源:{file_name}")
309
-
310
- context = "\n\n".join(context_with_refs)
311
-
312
- # 增强系统提示词,根据查询内容添加插件特定提示
313
- enhanced_system_prompt = self.system_prompt
314
-
315
- # 如果包含图片,添加图片处理相关指令
316
- if has_images:
317
- enhanced_system_prompt += """
318
- 此外,如果参考内容中包含图片地址,请在回答中适当引用这些图片信息,并在回答的最后列出所有参考的图片来源。
319
- """
320
-
321
- # 添加插件特定提示
322
- if is_code_related:
323
- enhanced_system_prompt += "\n\n" + self.get_code_execution_prompt_template()
324
-
325
- if is_visualization_related:
326
- enhanced_system_prompt += "\n\n" + self.get_visualization_prompt_template()
327
-
328
- if is_mindmap_related:
329
- enhanced_system_prompt += "\n\n" + self.get_mindmap_prompt_template()
330
-
331
- # 流式API
332
- headers = {
333
- "Authorization": f"Bearer {self.stream_api_key}",
334
- "Content-Type": "application/json"
335
- }
336
-
337
- try:
338
- response = requests.post(
339
- f"{self.stream_api_base}/chat/completions",
340
- headers=headers,
341
- json={
342
- "model": self.stream_model,
343
- "messages": [
344
- {"role": "system", "content": enhanced_system_prompt},
345
- {"role": "user", "content": f"""
346
- 参考内容:
347
- {context}
348
-
349
- 问题:{query}
350
-
351
- 请按照要求回答问题,包括引用标注和来源列表。
352
-
353
- 如果在参考内容中找到视频资源,请使用以下格式标记视频链接:
354
- <video_link>视频链接</video_link>
355
-
356
- 在回答结束时,请使用以下固定格式列出所有获取的参考内容作为参考来源:
357
-
358
- ===参考来源开始===
359
- [1] 摘要内容,"文件名"
360
- [2] 摘要内容,"文件名"
361
- ===参考来源结束===
362
-
363
- 如果没有参考内容,则无需包含上述部分。
364
- """}
365
- ],
366
- "stream": True
367
- }
368
- )
369
-
370
- if response.status_code != 200:
371
- yield f"生成回答时出错: {response.text}"
372
- return
373
-
374
- # 返回流式响应
375
- for line in response.iter_lines():
376
- if not line:
377
- continue
378
-
379
- line_text = line.decode('utf-8')
380
- if line_text.startswith('data: ') and line_text != 'data: [DONE]':
381
- try:
382
- json_str = line_text[6:] # 移除 "data: " 前缀
383
- data = json.loads(json_str)
384
- content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
385
- if content:
386
- yield content
387
- except Exception as e:
388
- yield f"解析响应出错: {str(e)}"
389
-
390
- # 如果需要返回处理数据
391
- if process_data:
392
- process_data["generation"]["time"] = round(time.time() - start_time, 3)
393
- yield {"process_data": process_data}
394
-
395
- except Exception as e:
396
- yield f"连接错误: {str(e)}"
 
1
+ # modules/knowledge_base/generator.py
2
+ from typing import List, Dict, Generator, Union, Optional, Any
3
+ import requests
4
+ import os
5
+ import json
6
+ import time
7
+ import re
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ class Generator:
13
+ def __init__(self, subject="", instructor=""):
14
+ # Set defaults if not provided
15
+ self.subject = subject or "通用学科"
16
+ self.instructor = instructor or "教师"
17
+
18
+ # 基础API配置
19
+ self.api_key = os.getenv("API_KEY")
20
+ self.api_base = os.getenv("BASE_URL")
21
+
22
+ # 流式文本问答配置
23
+ self.stream_api_key = os.getenv("STREAM_API_KEY")
24
+ self.stream_api_base = os.getenv("STREAM_BASE_URL")
25
+ self.stream_model = os.getenv("STREAM_MODEL")
26
+
27
+ # 通用提示词模板 - 填充主题和讲师信息
28
+ self.system_prompt = self.get_system_prompt_template().format(
29
+ subject=self.subject,
30
+ instructor=self.instructor
31
+ )
32
+
33
+ def get_system_prompt_template(self):
34
+ """返回可定制的系统提示词模板"""
35
+ return """<system>
36
+ 你是一位{subject}课程的智能助教,由{instructor}指导开发。你的目标是帮助学生理解和掌握{subject}课程的关键概念、原理和方法。
37
+
38
+ <knowledge_base>
39
+ 你拥有《{subject}》课程的专业知识库,包含教材内容、课件、习题解析等材料。当回答问题时,你应该优先使用知识库中检索到的相关内容,而不是依赖你的通用知识。
40
+ </knowledge_base>
41
+
42
+ <role_definition>
43
+ 作为{subject}助教,你应该:
44
+ 1. 用专业且易于理解的方式解释复杂概念
45
+ 2. 提供准确的技术信息和计算示例
46
+ 3. 在适当时使用比喻或类比帮助理解
47
+ 4. 引导学生思考而不是直接给出所有答案
48
+ 5. ⁠提供进一步学习的建议和资源
49
+ </role_definition>
50
+
51
+ <answering_guidelines>
52
+ 当回答问题时,请遵循以下原则:
53
+ 1. 先从知识库中检索与问题最相关的内容
54
+ 2. 将检索结果整合成连贯、清晰的回答
55
+ 3. 保持学术严谨性,确保概念解释和计算过程准确无误
56
+ 4. 使用专业术语的同时,确保解释足够通俗易懂
57
+ 5. 回答问题时注明知识来源,例如"根据教材第X章..."
58
+ 6. 当遇到计算题时,展示完整的计算步骤和思路
59
+ 7. 当知识库中没有直接相关内容时,明确告知学生并提供基于可靠原理的解答
60
+ 8. 对于概念性问题,先给出简短定义,再补充详细解释和例子
61
+ </answering_guidelines>
62
+
63
+ <response_format>
64
+ 对于不同类型的问题,采用不同的回答格式:
65
+ 1. 概念解释类问题:
66
+ - 先给出简明定义
67
+ - 提供详细解释
68
+ - 举例说明
69
+ - 补充相关知识点连接
70
+ 2. 计算类问题:
71
+ - 明确列出已知条件和所求内容
72
+ - 说明解题思路和所用公式
73
+ - 展示详细计算步骤
74
+ - 给出最终答案并解释其含义
75
+ 3. 综合分析类问题:
76
+ - 分点阐述相关知识点
77
+ - 提供分析框架
78
+ - 给出结论和建议
79
+ </response_format>
80
+ <video_content_guidelines>
81
+ 当检索到视频内容时,你应该:
82
+ 1. 明确告知用户你找到了相关视频资源
83
+ 2. 提供视频链接并确保包含时间戳
84
+ 3. 简要描述视频内容和主要学习点
85
+ 4. 建议用户观看视频以获得可视化理解
86
+ 5. 在回答结束时,再次强调视频资源的价值
87
+
88
+ 对于所有包含视频链接的回答,必须以下述格式呈现视频资源:
89
+
90
+ 推荐学习资源:
91
+ [视频标题] - [视频链接]
92
+ </video_content_guidelines>
93
+ </system>"""
94
+
95
+ def get_tool_selection_template(self):
96
+ """返回工具选择提示词模板"""
97
+ return """<system>
98
+ 你是{subject}课程智能助教系统的决策组件。你的唯一任务是判断用户问题类型并决定是否调用知识库工具,以及提取精准的搜索关键词。
99
+
100
+ <decision_guidelines>
101
+ 对于所有涉及{subject}专业知识的问题,必须调用至少一个知识库工具。系统依赖这些知识库提供准确信息,而不是依赖模型的通用知识。
102
+
103
+ 判断标准:
104
+ 1. 所有涉及课程概念、原理、计算方法的问题 → 必须调用相关知识库
105
+ 2. 所有需要专业解释或例子的问题 → 必须调用相关知识库
106
+ 3. 所有学习指导或复习相关的问题 → 必须调用相关知识库
107
+ 4. 仅对于纯粹的问候语或与课程无关的闲聊 → 不调用任何工具
108
+
109
+ 当决定调用知识库时,提取的关键词必须满足以下条件:
110
+ 1. 准确反映问题的核心主题
111
+ 2. 包含{subject}相关的专业术语或技术名词
112
+ 3. 去除无关紧要的修饰词
113
+ 4. 每个关键词尽量简洁,优先使用专业术语
114
+ 5. 提供2-5个关键词,确保覆盖问题的核心概念
115
+ </decision_guidelines>
116
+
117
+ <tool_selection_examples>
118
+ 例1:问题 - "请详细解释相关概念的表示方法和计算过程。"
119
+ 判断:需要专业知识解释
120
+ 工具选择:教材知识库(包含基础概念和详细解释)
121
+ 关键词:["表示方法", "计算过程"]
122
+
123
+ 例2:问题 - "这个概念的工作原理是什么?能给我一个直观的例子吗?"
124
+ 判断:需要专业知识解释,且需要直观演示
125
+ 工具选择:教材知识库(基础概念)和视频知识库(直观演示)
126
+ 关键词:["工作原理", "例子"]
127
+
128
+ 例3:问题 - "你好,今天天气怎么样?"
129
+ 判断:与课程无关的闲聊
130
+ 工具选择:不调用任何工具
131
+ 关键词:[]
132
+ </tool_selection_examples>
133
+
134
+ <video_knowledge_criteria>
135
+ 以下情况应优先选择视频知识库工具:
136
+ 1. 用户明确要求视频讲解或视频资料
137
+ 2. 问题涉及复杂的步骤或流程,可能需要可视化展示
138
+ 3. 问题关于动态过程的理解
139
+ 4. 问题涉及图形或结构的理解
140
+ 5. 问题包含"演示"、"展示"、"直观"、"可视化"等类似于需求的词语
141
+ </video_knowledge_criteria>
142
+
143
+ <quiz_knowledge_criteria>
144
+ 以下情况应选择习题知识库工具:
145
+ 1. 用户明确询问习题、例题或解题方法
146
+ 2. 问题是关于如何解决特定类型的问题
147
+ 3. 用户寻求考试或作业的帮助
148
+ 4. 用户提出的问题形式类似于典型习题
149
+ </quiz_knowledge_criteria>
150
+
151
+ <tool_instruction>
152
+ 你不需要回答用户问题,只需决定调用哪些工具以及提供精准的关键词数组。
153
+ </tool_instruction>
154
+ </system>"""
155
+
156
+ def get_code_execution_prompt_template(self):
157
+ """返回代码执行插件的提示词模板"""
158
+ return """<code_execution>
159
+ 只要当用户询问编程、代码或特别是Python相关的问题时,你必须在回答中整合代码执行插件的使用。
160
+
161
+ 使用代码执行插件的指南:
162
+ 1. 创建Python代码示例时,请使用正确的Markdown语法,用```python和```作为代码块的分隔符。
163
+ 2. 确保你的代码示例完整、可运行,并附有适当的注释。
164
+ 3. 在代码前后提供解释,帮助用户理解代码的功能和原理。
165
+ 4. 当代码与用户问题相关时,明确告知用户可以使用代码执行环境运行这段代码。
166
+ 5. 对于教学场景,考虑创建循序渐进的代码示例,让用户可以逐步学习和理解。
167
+
168
+ 示例回答格式:
169
+ "这是一个[描述]的Python程序:
170
+
171
+ ```python
172
+ # 你的完整、可运行的代码
173
+ print('Hello, world!')
174
+ ```
175
+
176
+ 你可以通过点击'运行'按钮在代码执行环境中运行这段代码。
177
+ 如果你想修改代码,只需在编辑器中编辑并再次运行即可。"
178
+ </code_execution>"""
179
+
180
+ def get_visualization_prompt_template(self):
181
+ """返回可视化插件的提示词模板"""
182
+ return """<visualization>
183
+ 当用户询问有关数学图形、函数、几何或需要3D可视化的内容时,你应该在回答中提供一个完整的Python函数来生成3D图形。
184
+
185
+ 使用3D可视化插件的指南:
186
+ 1. 你必须提供一个名为create_3d_plot的Python函数,该函数不接受任何参数。
187
+ 2. 这个函数应该导入必要的库(主要是numpy as np)。
188
+ 3. 函数需要返回一个包含以下结构的字典:
189
+ {
190
+ 'x': x_data,
191
+ 'y': y_data,
192
+ 'z': z_data,
193
+ 'type': 'surface' 或 'scatter3d' (取决于数据类型)
194
+ }
195
+ 4. 确保你的代码可以直接运行,无需额外修改。
196
+
197
+ 示例回答格式:
198
+ "下面是[数学概念]的3D可视化函数:
199
+
200
+ ```python
201
+ import numpy as np
202
+
203
+ def create_3d_plot():
204
+ # 生成数据
205
+ x = np.linspace(-5, 5, 100)
206
+ y = np.linspace(-5, 5, 100)
207
+ X, Y = np.meshgrid(x, y)
208
+ Z = np.sin(np.sqrt(X**2 + Y**2))
209
+
210
+ return {
211
+ 'x': X.tolist(),
212
+ 'y': Y.tolist(),
213
+ 'z': Z.tolist(),
214
+ 'type': 'surface'
215
+ }
216
+ ```
217
+
218
+ 这个函数创建了[概念描述]的3D图形,你可以观察[关键特征]。"
219
+ </visualization>"""
220
+
221
+ def get_mindmap_prompt_template(self):
222
+ """返回思维导图插件的提示词模板"""
223
+ return """<mindmap>
224
+ 当用户需要组织和梳理知识结构、概念关系或学习规划时,你应该在回答中整合思维导图。
225
+
226
+ 使用思维导图的指南:
227
+ 1. 提供一个完整的思维导图结构,使用PlantUML格式。
228
+ 2. 使用@startmindmap和@endmindmap标记包裹内容。
229
+ 3. 使用星号(*)表示层级:*为中央主题,**为主要主题,***为子主题,****为叶子节点。
230
+ 4. 确保思维导图结构清晰、逻辑合理,能够帮助用户理解知识体系。
231
+
232
+ 示例格式:
233
+ @startmindmap
234
+ * 中心主题
235
+ ** 主要分支1
236
+ *** 子主题1.1
237
+ **** 叶子节点1.1.1
238
+ *** 子主题1.2
239
+ ** 主要分支2
240
+ *** 子主题2.1
241
+ @endmindmap
242
+
243
+ 确保思维导图涵盖主题的关键概念和它们之间的关系,帮助用户建立完整的知识体系。
244
+ </mindmap>"""
245
+
246
+ def extract_keywords_with_tools(self, question: str, tools: List[Dict]) -> List[Dict]:
247
+ """使用工具化架构分析问题,决定使用哪些知识库以及提取关键词"""
248
+ system_prompt = self.get_tool_selection_template().format(subject=self.subject)
249
+
250
+ headers = {
251
+ "Authorization": f"Bearer {self.stream_api_key}",
252
+ "Content-Type": "application/json"
253
+ }
254
+
255
+ response = requests.post(
256
+ f"{self.stream_api_base}/chat/completions",
257
+ headers=headers,
258
+ json={
259
+ "model": self.stream_model,
260
+ "messages": [
261
+ {"role": "system", "content": system_prompt},
262
+ {"role": "user", "content": question}
263
+ ],
264
+ "tools": tools,
265
+ "tool_choice": "auto"
266
+ }
267
+ )
268
+
269
+ if response.status_code != 200:
270
+ raise Exception(f"工具调用出错: {response.text}")
271
+
272
+ response_data = response.json()
273
+ message = response_data["choices"][0]["message"]
274
+
275
+ # 如果模型决定调用工具
276
+ if "tool_calls" in message and message["tool_calls"]:
277
+ return message["tool_calls"]
278
+ else:
279
+ # 模型没有调用工具,返回空列表
280
+ return []
281
+
282
+ def generate_stream(self, query: str, context_docs: List[Dict], process_data: Optional[Dict] = None) -> Generator[Union[str, Dict], None, None]:
283
+ """流式生成回答 - 用于所有类型的问答"""
284
+ start_time = time.time()
285
+
286
+ # 构建带有引用标记的上下文
287
+ context_with_refs = []
288
+
289
+ # 检查是否有图片URL
290
+ has_images = any(doc['metadata'].get('img_url', '') for doc in context_docs)
291
+
292
+ # 检查查询是否与特定插件相关
293
+ is_code_related = any(kw in query.lower() for kw in ['code', 'python', 'program', '代码', '编程', 'coding', 'script'])
294
+ is_visualization_related = any(kw in query.lower() for kw in ['3d', 'graph', 'plot', 'function', 'visualization', '可视化', '图形', '函数'])
295
+ is_mindmap_related = any(kw in query.lower() for kw in ['mindmap', 'mind map', 'concept map', '思维导图', '概念图', '知识图'])
296
+
297
+ # 处理每个文档
298
+ for i, doc in enumerate(context_docs, 1):
299
+ # 直接使用文件名作为来源
300
+ file_name = doc['metadata'].get('file_name', '未知文件')
301
+ img_url = doc['metadata'].get('img_url', '')
302
+
303
+ # 构建文档内容,如果有图片URL则包含在内
304
+ content = doc['content']
305
+ if img_url:
306
+ content += f"\n[图片地址: {img_url}]"
307
+
308
+ context_with_refs.append(f"[{i}] {content}\n来源:{file_name}")
309
+
310
+ context = "\n\n".join(context_with_refs)
311
+
312
+ # 增强系统提示词,根据查询内容添加插件特定提示
313
+ enhanced_system_prompt = self.system_prompt
314
+
315
+ # 如果包含图片,添加图片处理相关指令
316
+ if has_images:
317
+ enhanced_system_prompt += """
318
+ 此外,如果参考内容中包含图片地址,请在回答中适当引用这些图片信息,并在回答的最后列出所有参考的图片来源。
319
+ """
320
+
321
+ # 添加插件特定提示
322
+ if is_code_related:
323
+ enhanced_system_prompt += "\n\n" + self.get_code_execution_prompt_template()
324
+
325
+ if is_visualization_related:
326
+ enhanced_system_prompt += "\n\n" + self.get_visualization_prompt_template()
327
+
328
+ if is_mindmap_related:
329
+ enhanced_system_prompt += "\n\n" + self.get_mindmap_prompt_template()
330
+
331
+ # 流式API
332
+ headers = {
333
+ "Authorization": f"Bearer {self.stream_api_key}",
334
+ "Content-Type": "application/json"
335
+ }
336
+
337
+ try:
338
+ response = requests.post(
339
+ f"{self.stream_api_base}/chat/completions",
340
+ headers=headers,
341
+ json={
342
+ "model": self.stream_model,
343
+ "messages": [
344
+ {"role": "system", "content": enhanced_system_prompt},
345
+ {"role": "user", "content": f"""
346
+ 参考内容:
347
+ {context}
348
+
349
+ 问题:{query}
350
+
351
+ 请按照要求回答问题,包括引用标注和来源列表。
352
+
353
+ 如果在参考内容中找到视频资源,请使用以下格式标记视频链接:
354
+ <video_link>视频链接</video_link>
355
+
356
+ 在回答结束时,请使用以下固定格式列出所有获取的参考内容作为参考来源:
357
+
358
+ ===参考来源开始===
359
+ [1] 摘要内容,"文件名"
360
+ [2] 摘要内容,"文件名"
361
+ ===参考来源结束===
362
+
363
+ 如果没有参考内容,则无需包含上述部分。
364
+ """}
365
+ ],
366
+ "stream": True
367
+ }
368
+ )
369
+
370
+ if response.status_code != 200:
371
+ yield f"生成回答时出错: {response.text}"
372
+ return
373
+
374
+ # 返回流式响应
375
+ for line in response.iter_lines():
376
+ if not line:
377
+ continue
378
+
379
+ line_text = line.decode('utf-8')
380
+ if line_text.startswith('data: ') and line_text != 'data: [DONE]':
381
+ try:
382
+ json_str = line_text[6:] # 移除 "data: " 前缀
383
+ data = json.loads(json_str)
384
+ content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
385
+ if content:
386
+ yield content
387
+ except Exception as e:
388
+ yield f"解析响应出错: {str(e)}"
389
+
390
+ # 如果需要返回处理数据
391
+ if process_data:
392
+ process_data["generation"]["time"] = round(time.time() - start_time, 3)
393
+ yield {"process_data": process_data}
394
+
395
+ except Exception as e:
396
+ yield f"连接错误: {str(e)}"
modules/knowledge_base/processor.py CHANGED
@@ -1,201 +1,201 @@
1
- from typing import List, Dict, Callable, Optional
2
- from langchain.text_splitter import RecursiveCharacterTextSplitter
3
- from langchain_community.document_loaders import (
4
- DirectoryLoader,
5
- UnstructuredMarkdownLoader,
6
- PyPDFLoader,
7
- TextLoader
8
- )
9
- import os
10
- import requests
11
- import base64
12
- from PIL import Image
13
- import io
14
-
15
- class DocumentLoader:
16
- """通用文档加载器"""
17
- def __init__(self, file_path: str):
18
- self.file_path = file_path
19
- self.extension = os.path.splitext(file_path)[1].lower()
20
- self.api_key = os.getenv("API_KEY")
21
- self.api_base = os.getenv("BASE_URL")
22
-
23
- def process_image(self, image_path: str) -> str:
24
- """使用 SiliconFlow VLM 模型处理图片"""
25
- try:
26
- # 读取图片并转换为base64
27
- with open(image_path, 'rb') as image_file:
28
- image_data = image_file.read()
29
- base64_image = base64.b64encode(image_data).decode('utf-8')
30
-
31
- # 调用 SiliconFlow API
32
- headers = {
33
- "Authorization": f"Bearer {self.api_key}",
34
- "Content-Type": "application/json"
35
- }
36
-
37
- response = requests.post(
38
- f"{self.api_base}/chat/completions",
39
- headers=headers,
40
- json={
41
- "model": "Qwen/Qwen2.5-VL-72B-Instruct",
42
- "messages": [
43
- {
44
- "role": "user",
45
- "content": [
46
- {
47
- "type": "image_url",
48
- "image_url": {
49
- "url": f"data:image/jpeg;base64,{base64_image}",
50
- "detail": "high"
51
- }
52
- },
53
- {
54
- "type": "text",
55
- "text": "请详细描述这张图片的内容,包括主要对象、场景、活动、颜色、布局等关键信息。"
56
- }
57
- ]
58
- }
59
- ],
60
- "temperature": 0.7,
61
- "max_tokens": 500
62
- }
63
- )
64
-
65
- if response.status_code != 200:
66
- raise Exception(f"图片处理API调用失败: {response.text}")
67
-
68
- description = response.json()["choices"][0]["message"]["content"]
69
- return description
70
-
71
- except Exception as e:
72
- print(f"处理图片时出错: {str(e)}")
73
- return "图片处理失败"
74
-
75
- def load(self):
76
- try:
77
- if self.extension == '.md':
78
- loader = UnstructuredMarkdownLoader(self.file_path, encoding='utf-8')
79
- return loader.load()
80
- elif self.extension == '.pdf':
81
- loader = PyPDFLoader(self.file_path)
82
- return loader.load()
83
- elif self.extension == '.txt':
84
- loader = TextLoader(self.file_path, encoding='utf-8')
85
- return loader.load()
86
- elif self.extension in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
87
- # 处理图片
88
- description = self.process_image(self.file_path)
89
- # 创建一个包含图片描述的文档
90
- from langchain.schema import Document
91
- doc = Document(
92
- page_content=description,
93
- metadata={
94
- 'source': self.file_path,
95
- 'img_url': os.path.abspath(self.file_path) # 存储图片的绝对路径
96
- }
97
- )
98
- return [doc]
99
- else:
100
- raise ValueError(f"不支持的文件格式: {self.extension}")
101
-
102
- except UnicodeDecodeError:
103
- # 如果 utf-8 失败,尝试 gbk
104
- if self.extension in ['.md', '.txt']:
105
- loader = TextLoader(self.file_path, encoding='gbk')
106
- return loader.load()
107
- raise
108
-
109
- class DocumentProcessor:
110
- def __init__(self):
111
- self.text_splitter = RecursiveCharacterTextSplitter(
112
- chunk_size=1000,
113
- chunk_overlap=200,
114
- length_function=len,
115
- )
116
-
117
- def get_index_name(self, path: str) -> str:
118
- """根据文件路径生成索引名称"""
119
- if os.path.isdir(path):
120
- # 如果是目录,使用目录名
121
- return f"rag_{os.path.basename(path).lower()}"
122
- else:
123
- # 如果是文件,使用文件名(不含扩展名)
124
- return f"rag_{os.path.splitext(os.path.basename(path))[0].lower()}"
125
-
126
- def process(self, path: str, progress_callback: Optional[Callable] = None) -> List[Dict]:
127
- """
128
- 加载并处理文档,支持目录或单个文件
129
- 参数:
130
- path: 文档路径
131
- progress_callback: 进度回调函数,用于报告处理进度
132
- 返回:处理后的文档列表
133
- """
134
- if os.path.isdir(path):
135
- documents = []
136
- total_files = sum([len(files) for _, _, files in os.walk(path)])
137
- processed_files = 0
138
- processed_size = 0
139
-
140
- for root, _, files in os.walk(path):
141
- for file in files:
142
- file_path = os.path.join(root, file)
143
- try:
144
- # 更新处理进度
145
- if progress_callback:
146
- file_size = os.path.getsize(file_path)
147
- processed_size += file_size
148
- processed_files += 1
149
- progress_callback(processed_size, f"处理文件 {processed_files}/{total_files}: {file}")
150
-
151
- loader = DocumentLoader(file_path)
152
- docs = loader.load()
153
- # 添加文件名到metadata
154
- for doc in docs:
155
- doc.metadata['file_name'] = os.path.basename(file_path)
156
- documents.extend(docs)
157
- except Exception as e:
158
- print(f"警告:加载文件 {file_path} 时出错: {str(e)}")
159
- continue
160
- else:
161
- try:
162
- if progress_callback:
163
- file_size = os.path.getsize(path)
164
- progress_callback(file_size * 0.3, f"加载文件: {os.path.basename(path)}")
165
-
166
- loader = DocumentLoader(path)
167
- documents = loader.load()
168
-
169
- # 更新进度
170
- if progress_callback:
171
- progress_callback(file_size * 0.6, f"处理文件内容...")
172
-
173
- # 添加文件名到metadata
174
- file_name = os.path.basename(path)
175
- for doc in documents:
176
- doc.metadata['file_name'] = file_name
177
- except Exception as e:
178
- print(f"加载文件时出错: {str(e)}")
179
- raise
180
-
181
- # 分块
182
- chunks = self.text_splitter.split_documents(documents)
183
-
184
- # 更新进度
185
- if progress_callback:
186
- if os.path.isdir(path):
187
- progress_callback(processed_size, f"文档分块完成,共{len(chunks)}个文档片段")
188
- else:
189
- file_size = os.path.getsize(path)
190
- progress_callback(file_size * 0.9, f"文档分块完成,共{len(chunks)}个文档片段")
191
-
192
- # 处理成统一格式
193
- processed_docs = []
194
- for i, chunk in enumerate(chunks):
195
- processed_docs.append({
196
- 'id': f'doc_{i}',
197
- 'content': chunk.page_content,
198
- 'metadata': chunk.metadata
199
- })
200
-
201
  return processed_docs
 
1
+ from typing import List, Dict, Callable, Optional
2
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
3
+ from langchain_community.document_loaders import (
4
+ DirectoryLoader,
5
+ UnstructuredMarkdownLoader,
6
+ PyPDFLoader,
7
+ TextLoader
8
+ )
9
+ import os
10
+ import requests
11
+ import base64
12
+ from PIL import Image
13
+ import io
14
+
15
+ class DocumentLoader:
16
+ """通用文档加载器"""
17
+ def __init__(self, file_path: str):
18
+ self.file_path = file_path
19
+ self.extension = os.path.splitext(file_path)[1].lower()
20
+ self.api_key = os.getenv("API_KEY")
21
+ self.api_base = os.getenv("BASE_URL")
22
+
23
+ def process_image(self, image_path: str) -> str:
24
+ """使用 SiliconFlow VLM 模型处理图片"""
25
+ try:
26
+ # 读取图片并转换为base64
27
+ with open(image_path, 'rb') as image_file:
28
+ image_data = image_file.read()
29
+ base64_image = base64.b64encode(image_data).decode('utf-8')
30
+
31
+ # 调用 SiliconFlow API
32
+ headers = {
33
+ "Authorization": f"Bearer {self.api_key}",
34
+ "Content-Type": "application/json"
35
+ }
36
+
37
+ response = requests.post(
38
+ f"{self.api_base}/chat/completions",
39
+ headers=headers,
40
+ json={
41
+ "model": "Qwen/Qwen2.5-VL-72B-Instruct",
42
+ "messages": [
43
+ {
44
+ "role": "user",
45
+ "content": [
46
+ {
47
+ "type": "image_url",
48
+ "image_url": {
49
+ "url": f"data:image/jpeg;base64,{base64_image}",
50
+ "detail": "high"
51
+ }
52
+ },
53
+ {
54
+ "type": "text",
55
+ "text": "请详细描述这张图片的内容,包括主要对象、场景、活动、颜色、布局等关键信息。"
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ "temperature": 0.7,
61
+ "max_tokens": 500
62
+ }
63
+ )
64
+
65
+ if response.status_code != 200:
66
+ raise Exception(f"图片处理API调用失败: {response.text}")
67
+
68
+ description = response.json()["choices"][0]["message"]["content"]
69
+ return description
70
+
71
+ except Exception as e:
72
+ print(f"处理图片时出错: {str(e)}")
73
+ return "图片处理失败"
74
+
75
+ def load(self):
76
+ try:
77
+ if self.extension == '.md':
78
+ loader = UnstructuredMarkdownLoader(self.file_path, encoding='utf-8')
79
+ return loader.load()
80
+ elif self.extension == '.pdf':
81
+ loader = PyPDFLoader(self.file_path)
82
+ return loader.load()
83
+ elif self.extension == '.txt':
84
+ loader = TextLoader(self.file_path, encoding='utf-8')
85
+ return loader.load()
86
+ elif self.extension in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
87
+ # 处理图片
88
+ description = self.process_image(self.file_path)
89
+ # 创建一个包含图片描述的文档
90
+ from langchain.schema import Document
91
+ doc = Document(
92
+ page_content=description,
93
+ metadata={
94
+ 'source': self.file_path,
95
+ 'img_url': os.path.abspath(self.file_path) # 存储图片的绝对路径
96
+ }
97
+ )
98
+ return [doc]
99
+ else:
100
+ raise ValueError(f"不支持的文件格式: {self.extension}")
101
+
102
+ except UnicodeDecodeError:
103
+ # 如果 utf-8 失败,尝试 gbk
104
+ if self.extension in ['.md', '.txt']:
105
+ loader = TextLoader(self.file_path, encoding='gbk')
106
+ return loader.load()
107
+ raise
108
+
109
+ class DocumentProcessor:
110
+ def __init__(self):
111
+ self.text_splitter = RecursiveCharacterTextSplitter(
112
+ chunk_size=1000,
113
+ chunk_overlap=200,
114
+ length_function=len,
115
+ )
116
+
117
+ def get_index_name(self, path: str) -> str:
118
+ """根据文件路径生成索引名称"""
119
+ if os.path.isdir(path):
120
+ # 如果是目录,使用目录名
121
+ return f"rag_{os.path.basename(path).lower()}"
122
+ else:
123
+ # 如果是文件,使用文件名(不含扩展名)
124
+ return f"rag_{os.path.splitext(os.path.basename(path))[0].lower()}"
125
+
126
+ def process(self, path: str, progress_callback: Optional[Callable] = None) -> List[Dict]:
127
+ """
128
+ 加载并处理文档,支持目录或单个文件
129
+ 参数:
130
+ path: 文档路径
131
+ progress_callback: 进度回调函数,用于报告处理进度
132
+ 返回:处理后的文档列表
133
+ """
134
+ if os.path.isdir(path):
135
+ documents = []
136
+ total_files = sum([len(files) for _, _, files in os.walk(path)])
137
+ processed_files = 0
138
+ processed_size = 0
139
+
140
+ for root, _, files in os.walk(path):
141
+ for file in files:
142
+ file_path = os.path.join(root, file)
143
+ try:
144
+ # 更新处理进度
145
+ if progress_callback:
146
+ file_size = os.path.getsize(file_path)
147
+ processed_size += file_size
148
+ processed_files += 1
149
+ progress_callback(processed_size, f"处理文件 {processed_files}/{total_files}: {file}")
150
+
151
+ loader = DocumentLoader(file_path)
152
+ docs = loader.load()
153
+ # 添加文件名到metadata
154
+ for doc in docs:
155
+ doc.metadata['file_name'] = os.path.basename(file_path)
156
+ documents.extend(docs)
157
+ except Exception as e:
158
+ print(f"警告:加载文件 {file_path} 时出错: {str(e)}")
159
+ continue
160
+ else:
161
+ try:
162
+ if progress_callback:
163
+ file_size = os.path.getsize(path)
164
+ progress_callback(file_size * 0.3, f"加载文件: {os.path.basename(path)}")
165
+
166
+ loader = DocumentLoader(path)
167
+ documents = loader.load()
168
+
169
+ # 更新进度
170
+ if progress_callback:
171
+ progress_callback(file_size * 0.6, f"处理文件内容...")
172
+
173
+ # 添加文件名到metadata
174
+ file_name = os.path.basename(path)
175
+ for doc in documents:
176
+ doc.metadata['file_name'] = file_name
177
+ except Exception as e:
178
+ print(f"加载文件时出错: {str(e)}")
179
+ raise
180
+
181
+ # 分块
182
+ chunks = self.text_splitter.split_documents(documents)
183
+
184
+ # 更新进度
185
+ if progress_callback:
186
+ if os.path.isdir(path):
187
+ progress_callback(processed_size, f"文档分块完成,共{len(chunks)}个文档片段")
188
+ else:
189
+ file_size = os.path.getsize(path)
190
+ progress_callback(file_size * 0.9, f"文档分块完成,共{len(chunks)}个文档片段")
191
+
192
+ # 处理成统一格式
193
+ processed_docs = []
194
+ for i, chunk in enumerate(chunks):
195
+ processed_docs.append({
196
+ 'id': f'doc_{i}',
197
+ 'content': chunk.page_content,
198
+ 'metadata': chunk.metadata
199
+ })
200
+
201
  return processed_docs
modules/knowledge_base/reranker.py CHANGED
@@ -1,49 +1,49 @@
1
- from typing import List, Dict
2
- import requests
3
- import time
4
- from dotenv import load_dotenv
5
- import os
6
-
7
- load_dotenv()
8
-
9
- class Reranker:
10
- def __init__(self):
11
- self.api_key = os.getenv("API_KEY")
12
- self.api_base = os.getenv("BASE_URL")
13
-
14
- def rerank(self, query: str, documents: List[Dict], index_name: str, top_k: int = 5) -> List[Dict]:
15
- """使用SiliconFlow的rerank API重排序文档"""
16
- headers = {
17
- "Authorization": f"Bearer {self.api_key}",
18
- "Content-Type": "application/json"
19
- }
20
-
21
- # 准备文档列表
22
- docs = [doc['content'] for doc in documents]
23
-
24
- response = requests.post(
25
- f"{self.api_base}/rerank",
26
- headers=headers,
27
- json={
28
- "model": "BAAI/bge-reranker-v2-m3",
29
- "query": query,
30
- "documents": docs,
31
- "top_n": top_k
32
- }
33
- )
34
-
35
- if response.status_code != 200:
36
- raise Exception(f"Error in reranking: {response.text}")
37
-
38
- # 处理结果
39
- results = response.json()["results"]
40
- reranked_docs = []
41
-
42
- for result in results:
43
- doc_index = result["index"]
44
- original_doc = documents[doc_index].copy()
45
- original_doc['rerank_score'] = result["relevance_score"]
46
- original_doc['index_name'] = index_name
47
- reranked_docs.append(original_doc)
48
-
49
  return reranked_docs
 
1
+ from typing import List, Dict
2
+ import requests
3
+ import time
4
+ from dotenv import load_dotenv
5
+ import os
6
+
7
+ load_dotenv()
8
+
9
+ class Reranker:
10
+ def __init__(self):
11
+ self.api_key = os.getenv("API_KEY")
12
+ self.api_base = os.getenv("BASE_URL")
13
+
14
+ def rerank(self, query: str, documents: List[Dict], index_name: str, top_k: int = 5) -> List[Dict]:
15
+ """使用SiliconFlow的rerank API重排序文档"""
16
+ headers = {
17
+ "Authorization": f"Bearer {self.api_key}",
18
+ "Content-Type": "application/json"
19
+ }
20
+
21
+ # 准备文档列表
22
+ docs = [doc['content'] for doc in documents]
23
+
24
+ response = requests.post(
25
+ f"{self.api_base}/rerank",
26
+ headers=headers,
27
+ json={
28
+ "model": "BAAI/bge-reranker-v2-m3",
29
+ "query": query,
30
+ "documents": docs,
31
+ "top_n": top_k
32
+ }
33
+ )
34
+
35
+ if response.status_code != 200:
36
+ raise Exception(f"Error in reranking: {response.text}")
37
+
38
+ # 处理结果
39
+ results = response.json()["results"]
40
+ reranked_docs = []
41
+
42
+ for result in results:
43
+ doc_index = result["index"]
44
+ original_doc = documents[doc_index].copy()
45
+ original_doc['rerank_score'] = result["relevance_score"]
46
+ original_doc['index_name'] = index_name
47
+ reranked_docs.append(original_doc)
48
+
49
  return reranked_docs
modules/knowledge_base/retriever.py CHANGED
@@ -1,109 +1,109 @@
1
- from typing import List, Dict, Tuple
2
- import requests
3
- from elasticsearch import Elasticsearch
4
- import os
5
- import time
6
- from dotenv import load_dotenv
7
-
8
- load_dotenv()
9
-
10
- class Retriever:
11
- def __init__(self):
12
- # 使用与 vector_store.py 相同的 ES 配置
13
- self.es = Elasticsearch(
14
- "https://samlax12-elastic.hf.space", # 注意是 https
15
- basic_auth=("elastic", os.getenv("PASSWORD")), # 使用相同的密码
16
- verify_certs=False # 开发环境可以禁用证书验证
17
- )
18
- self.api_key = os.getenv("API_KEY")
19
- self.api_base = os.getenv("BASE_URL")
20
-
21
- def get_embedding(self, text: str) -> List[float]:
22
- """调用SiliconFlow的embedding API获取向量"""
23
- headers = {
24
- "Authorization": f"Bearer {self.api_key}",
25
- "Content-Type": "application/json"
26
- }
27
-
28
- response = requests.post(
29
- f"{self.api_base}/embeddings",
30
- headers=headers,
31
- json={
32
- "model": "BAAI/bge-m3",
33
- "input": text
34
- }
35
- )
36
-
37
- if response.status_code == 200:
38
- return response.json()["data"][0]["embedding"]
39
- else:
40
- raise Exception(f"Error getting embedding: {response.text}")
41
-
42
- def get_all_indices(self) -> List[str]:
43
- """获取所有 RAG 相关的索引"""
44
- indices = self.es.indices.get_alias().keys()
45
- return [idx for idx in indices if idx.startswith('rag_')]
46
-
47
- def retrieve(self, query: str, top_k: int = 10, specific_index: str = None) -> Tuple[List[Dict], str]:
48
- """混合检索:结合 BM25 和向量检索,支持指定特定索引"""
49
- # 获取检索索引
50
- if specific_index:
51
- indices = [specific_index] if self.es.indices.exists(index=specific_index) else []
52
- else:
53
- indices = self.get_all_indices()
54
-
55
- if not indices:
56
- raise Exception("没有找到可用的文档索引!")
57
-
58
- # 计算查询向量
59
- query_vector = self.get_embedding(query)
60
-
61
- # 在所有索引中搜索
62
- all_results = []
63
- for index in indices:
64
- # 构建混合查询
65
- script_query = {
66
- "script_score": {
67
- "query": {
68
- "match": {
69
- "content": query # BM25
70
- }
71
- },
72
- "script": {
73
- "source": "cosineSimilarity(params.query_vector, 'vector') + 1.0",
74
- "params": {"query_vector": query_vector}
75
- }
76
- }
77
- }
78
-
79
- # 执行检索
80
- response = self.es.search(
81
- index=index,
82
- body={
83
- "query": script_query,
84
- "size": top_k
85
- }
86
- )
87
-
88
- # 处理结果
89
- for hit in response['hits']['hits']:
90
- result = {
91
- 'id': hit['_id'],
92
- 'content': hit['_source']['content'],
93
- 'score': hit['_score'],
94
- 'metadata': hit['_source']['metadata'],
95
- 'index': index
96
- }
97
- all_results.append(result)
98
-
99
- # 按分数排序并选择最相关的文档
100
- all_results.sort(key=lambda x: x['score'], reverse=True)
101
- top_results = all_results[:top_k]
102
-
103
- # 如果有结果,返回最相关文档所在的索引
104
- if top_results:
105
- most_relevant_index = top_results[0]['index']
106
- else:
107
- most_relevant_index = indices[0] if indices else ""
108
-
109
  return top_results, most_relevant_index
 
1
+ from typing import List, Dict, Tuple
2
+ import requests
3
+ from elasticsearch import Elasticsearch
4
+ import os
5
+ import time
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ class Retriever:
11
+ def __init__(self):
12
+ # 使用与 vector_store.py 相同的 ES 配置
13
+ self.es = Elasticsearch(
14
+ "https://samlax12-elastic.hf.space", # 注意是 https
15
+ basic_auth=("elastic", os.getenv("PASSWORD")), # 使用相同的密码
16
+ verify_certs=False # 开发环境可以禁用证书验证
17
+ )
18
+ self.api_key = os.getenv("API_KEY")
19
+ self.api_base = os.getenv("BASE_URL")
20
+
21
+ def get_embedding(self, text: str) -> List[float]:
22
+ """调用SiliconFlow的embedding API获取向量"""
23
+ headers = {
24
+ "Authorization": f"Bearer {self.api_key}",
25
+ "Content-Type": "application/json"
26
+ }
27
+
28
+ response = requests.post(
29
+ f"{self.api_base}/embeddings",
30
+ headers=headers,
31
+ json={
32
+ "model": "BAAI/bge-m3",
33
+ "input": text
34
+ }
35
+ )
36
+
37
+ if response.status_code == 200:
38
+ return response.json()["data"][0]["embedding"]
39
+ else:
40
+ raise Exception(f"Error getting embedding: {response.text}")
41
+
42
+ def get_all_indices(self) -> List[str]:
43
+ """获取所有 RAG 相关的索引"""
44
+ indices = self.es.indices.get_alias().keys()
45
+ return [idx for idx in indices if idx.startswith('rag_')]
46
+
47
+ def retrieve(self, query: str, top_k: int = 10, specific_index: str = None) -> Tuple[List[Dict], str]:
48
+ """混合检索:结合 BM25 和向量检索,支持指定特定索引"""
49
+ # 获取检索索引
50
+ if specific_index:
51
+ indices = [specific_index] if self.es.indices.exists(index=specific_index) else []
52
+ else:
53
+ indices = self.get_all_indices()
54
+
55
+ if not indices:
56
+ raise Exception("没有找到可用的文档索引!")
57
+
58
+ # 计算查询向量
59
+ query_vector = self.get_embedding(query)
60
+
61
+ # 在所有索引中搜索
62
+ all_results = []
63
+ for index in indices:
64
+ # 构建混合查询
65
+ script_query = {
66
+ "script_score": {
67
+ "query": {
68
+ "match": {
69
+ "content": query # BM25
70
+ }
71
+ },
72
+ "script": {
73
+ "source": "cosineSimilarity(params.query_vector, 'vector') + 1.0",
74
+ "params": {"query_vector": query_vector}
75
+ }
76
+ }
77
+ }
78
+
79
+ # 执行检索
80
+ response = self.es.search(
81
+ index=index,
82
+ body={
83
+ "query": script_query,
84
+ "size": top_k
85
+ }
86
+ )
87
+
88
+ # 处理结果
89
+ for hit in response['hits']['hits']:
90
+ result = {
91
+ 'id': hit['_id'],
92
+ 'content': hit['_source']['content'],
93
+ 'score': hit['_score'],
94
+ 'metadata': hit['_source']['metadata'],
95
+ 'index': index
96
+ }
97
+ all_results.append(result)
98
+
99
+ # 按分数排序并选择最相关的文档
100
+ all_results.sort(key=lambda x: x['score'], reverse=True)
101
+ top_results = all_results[:top_k]
102
+
103
+ # 如果有结果,返回最相关文档所在的索引
104
+ if top_results:
105
+ most_relevant_index = top_results[0]['index']
106
+ else:
107
+ most_relevant_index = indices[0] if indices else ""
108
+
109
  return top_results, most_relevant_index
modules/knowledge_base/vector_store.py CHANGED
@@ -1,187 +1,187 @@
1
- from typing import List, Dict
2
- import requests
3
- import numpy as np
4
- from elasticsearch import Elasticsearch
5
- import urllib3
6
- from dotenv import load_dotenv
7
- import os
8
-
9
- load_dotenv()
10
-
11
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
12
-
13
- class VectorStore:
14
- def __init__(self):
15
- # ES 8.x 的连接配置
16
- self.es = Elasticsearch(
17
- "https://samlax12-elastic.hf.space",
18
- basic_auth=("elastic", os.getenv("PASSWORD")),
19
- verify_certs=False,
20
- request_timeout=30,
21
- # 忽略系统索引警告
22
- headers={"accept": "application/vnd.elasticsearch+json; compatible-with=8"},
23
- )
24
- self.api_key = os.getenv("API_KEY")
25
- self.api_base = os.getenv("BASE_URL")
26
-
27
- def get_embedding(self, text: str) -> List[float]:
28
- """调用SiliconFlow的embedding API获取向量"""
29
- headers = {
30
- "Authorization": f"Bearer {self.api_key}",
31
- "Content-Type": "application/json"
32
- }
33
-
34
- response = requests.post(
35
- f"{self.api_base}/embeddings",
36
- headers=headers,
37
- json={
38
- "model": "BAAI/bge-m3",
39
- "input": text
40
- }
41
- )
42
-
43
- if response.status_code == 200:
44
- return response.json()["data"][0]["embedding"]
45
- else:
46
- raise Exception(f"Error getting embedding: {response.text}")
47
-
48
- def store(self, documents: List[Dict], index_name: str) -> None:
49
- """将文档存储到 Elasticsearch"""
50
- # 创建索引(如果不存在)
51
- if not self.es.indices.exists(index=index_name):
52
- self.create_index(index_name)
53
-
54
- # 获取当前索引中的文档数量
55
- try:
56
- response = self.es.count(index=index_name)
57
- last_id = response['count'] - 1 # 文档数量减1作为最后的ID
58
- if last_id < 0:
59
- last_id = -1
60
- except Exception as e:
61
- print(f"获取文档数量时出错,假设为-1: {str(e)}")
62
- last_id = -1
63
-
64
- # 批量索引文档
65
- bulk_data = []
66
- for i, doc in enumerate(documents, start=last_id + 1):
67
- # 获取文档向量
68
- vector = self.get_embedding(doc['content'])
69
-
70
- # 准备索引数据
71
- bulk_data.append({
72
- "index": {
73
- "_index": index_name,
74
- "_id": f"doc_{i}"
75
- }
76
- })
77
-
78
- # 构建文档数据,包含新的img_url字段
79
- doc_data = {
80
- "content": doc['content'],
81
- "vector": vector,
82
- "metadata": {
83
- "file_name": doc['metadata'].get('file_name', '未知文件'),
84
- "source": doc['metadata'].get('source', ''),
85
- "page": doc['metadata'].get('page', ''),
86
- "img_url": doc['metadata'].get('img_url', '') # 添加img_url字段
87
- }
88
- }
89
- bulk_data.append(doc_data)
90
-
91
- # 批量写入
92
- if bulk_data:
93
- response = self.es.bulk(operations=bulk_data, refresh=True)
94
- if response.get('errors'):
95
- print("批量写入时出现错误:", response)
96
-
97
- def get_files_in_index(self, index_name: str) -> List[str]:
98
- """获取索引中的所有文件名"""
99
- try:
100
- response = self.es.search(
101
- index=index_name,
102
- body={
103
- "size": 0,
104
- "aggs": {
105
- "unique_files": {
106
- "terms": {
107
- "field": "metadata.file_name",
108
- "size": 1000
109
- }
110
- }
111
- }
112
- }
113
- )
114
-
115
- files = [bucket['key'] for bucket in response['aggregations']['unique_files']['buckets']]
116
- return sorted(files)
117
- except Exception as e:
118
- print(f"获取文件列表时出错: {str(e)}")
119
- return []
120
-
121
- def create_index(self, index_name: str):
122
- """创建 Elasticsearch 索引"""
123
- settings = {
124
- "mappings": {
125
- "properties": {
126
- "content": {"type": "text"},
127
- "vector": {
128
- "type": "dense_vector",
129
- "dims": 1024
130
- },
131
- "metadata": {
132
- "properties": {
133
- "file_name": {
134
- "type": "keyword",
135
- "ignore_above": 256
136
- },
137
- "source": {
138
- "type": "keyword"
139
- },
140
- "page": {
141
- "type": "keyword"
142
- },
143
- "img_url": { # 新增图片URL字段
144
- "type": "keyword",
145
- "ignore_above": 2048
146
- }
147
- }
148
- }
149
- }
150
- }
151
- }
152
-
153
- # 如果索引已存在,先删除
154
- if self.es.indices.exists(index=index_name):
155
- self.es.indices.delete(index=index_name)
156
-
157
- self.es.indices.create(index=index_name, body=settings)
158
-
159
- def delete_index(self, index_id: str) -> bool:
160
- """删除一个索引"""
161
- try:
162
- if self.es.indices.exists(index=index_id):
163
- self.es.indices.delete(index=index_id)
164
- return True
165
- return False
166
- except Exception as e:
167
- print(f"删除索引时出错: {str(e)}")
168
- return False
169
-
170
- def delete_document(self, index_id: str, file_name: str) -> bool:
171
- """根据文件名删除文档"""
172
- try:
173
- response = self.es.delete_by_query(
174
- index=index_id,
175
- body={
176
- "query": {
177
- "term": {
178
- "metadata.file_name": file_name
179
- }
180
- }
181
- },
182
- refresh=True
183
- )
184
- return True
185
- except Exception as e:
186
- print(f"删除文档时出错: {str(e)}")
187
  return False
 
1
+ from typing import List, Dict
2
+ import requests
3
+ import numpy as np
4
+ from elasticsearch import Elasticsearch
5
+ import urllib3
6
+ from dotenv import load_dotenv
7
+ import os
8
+
9
+ load_dotenv()
10
+
11
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
12
+
13
+ class VectorStore:
14
+ def __init__(self):
15
+ # ES 8.x 的连接配置
16
+ self.es = Elasticsearch(
17
+ "https://samlax12-elastic.hf.space",
18
+ basic_auth=("elastic", os.getenv("PASSWORD")),
19
+ verify_certs=False,
20
+ request_timeout=30,
21
+ # 忽略系统索引警告
22
+ headers={"accept": "application/vnd.elasticsearch+json; compatible-with=8"},
23
+ )
24
+ self.api_key = os.getenv("API_KEY")
25
+ self.api_base = os.getenv("BASE_URL")
26
+
27
+ def get_embedding(self, text: str) -> List[float]:
28
+ """调用SiliconFlow的embedding API获取向量"""
29
+ headers = {
30
+ "Authorization": f"Bearer {self.api_key}",
31
+ "Content-Type": "application/json"
32
+ }
33
+
34
+ response = requests.post(
35
+ f"{self.api_base}/embeddings",
36
+ headers=headers,
37
+ json={
38
+ "model": "BAAI/bge-m3",
39
+ "input": text
40
+ }
41
+ )
42
+
43
+ if response.status_code == 200:
44
+ return response.json()["data"][0]["embedding"]
45
+ else:
46
+ raise Exception(f"Error getting embedding: {response.text}")
47
+
48
+ def store(self, documents: List[Dict], index_name: str) -> None:
49
+ """将文档存储到 Elasticsearch"""
50
+ # 创建索引(如果不存在)
51
+ if not self.es.indices.exists(index=index_name):
52
+ self.create_index(index_name)
53
+
54
+ # 获取当前索引中的文档数量
55
+ try:
56
+ response = self.es.count(index=index_name)
57
+ last_id = response['count'] - 1 # 文档数量减1作为最后的ID
58
+ if last_id < 0:
59
+ last_id = -1
60
+ except Exception as e:
61
+ print(f"获取文档数量时出错,假设为-1: {str(e)}")
62
+ last_id = -1
63
+
64
+ # 批量索引文档
65
+ bulk_data = []
66
+ for i, doc in enumerate(documents, start=last_id + 1):
67
+ # 获取文档向量
68
+ vector = self.get_embedding(doc['content'])
69
+
70
+ # 准备索引数据
71
+ bulk_data.append({
72
+ "index": {
73
+ "_index": index_name,
74
+ "_id": f"doc_{i}"
75
+ }
76
+ })
77
+
78
+ # 构建文档数据,包含新的img_url字段
79
+ doc_data = {
80
+ "content": doc['content'],
81
+ "vector": vector,
82
+ "metadata": {
83
+ "file_name": doc['metadata'].get('file_name', '未知文件'),
84
+ "source": doc['metadata'].get('source', ''),
85
+ "page": doc['metadata'].get('page', ''),
86
+ "img_url": doc['metadata'].get('img_url', '') # 添加img_url字段
87
+ }
88
+ }
89
+ bulk_data.append(doc_data)
90
+
91
+ # 批量写入
92
+ if bulk_data:
93
+ response = self.es.bulk(operations=bulk_data, refresh=True)
94
+ if response.get('errors'):
95
+ print("批量写入时出现错误:", response)
96
+
97
+ def get_files_in_index(self, index_name: str) -> List[str]:
98
+ """获取索引中的所有文件名"""
99
+ try:
100
+ response = self.es.search(
101
+ index=index_name,
102
+ body={
103
+ "size": 0,
104
+ "aggs": {
105
+ "unique_files": {
106
+ "terms": {
107
+ "field": "metadata.file_name",
108
+ "size": 1000
109
+ }
110
+ }
111
+ }
112
+ }
113
+ )
114
+
115
+ files = [bucket['key'] for bucket in response['aggregations']['unique_files']['buckets']]
116
+ return sorted(files)
117
+ except Exception as e:
118
+ print(f"获取文件列表时出错: {str(e)}")
119
+ return []
120
+
121
+ def create_index(self, index_name: str):
122
+ """创建 Elasticsearch 索引"""
123
+ settings = {
124
+ "mappings": {
125
+ "properties": {
126
+ "content": {"type": "text"},
127
+ "vector": {
128
+ "type": "dense_vector",
129
+ "dims": 1024
130
+ },
131
+ "metadata": {
132
+ "properties": {
133
+ "file_name": {
134
+ "type": "keyword",
135
+ "ignore_above": 256
136
+ },
137
+ "source": {
138
+ "type": "keyword"
139
+ },
140
+ "page": {
141
+ "type": "keyword"
142
+ },
143
+ "img_url": { # 新增图片URL字段
144
+ "type": "keyword",
145
+ "ignore_above": 2048
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ # 如果索引已存在,先删除
154
+ if self.es.indices.exists(index=index_name):
155
+ self.es.indices.delete(index=index_name)
156
+
157
+ self.es.indices.create(index=index_name, body=settings)
158
+
159
+ def delete_index(self, index_id: str) -> bool:
160
+ """删除一个索引"""
161
+ try:
162
+ if self.es.indices.exists(index=index_id):
163
+ self.es.indices.delete(index=index_id)
164
+ return True
165
+ return False
166
+ except Exception as e:
167
+ print(f"删除索引时出错: {str(e)}")
168
+ return False
169
+
170
+ def delete_document(self, index_id: str, file_name: str) -> bool:
171
+ """根据文件名删除文档"""
172
+ try:
173
+ response = self.es.delete_by_query(
174
+ index=index_id,
175
+ body={
176
+ "query": {
177
+ "term": {
178
+ "metadata.file_name": file_name
179
+ }
180
+ }
181
+ },
182
+ refresh=True
183
+ )
184
+ return True
185
+ except Exception as e:
186
+ print(f"删除文档时出错: {str(e)}")
187
  return False
requirements.txt CHANGED
@@ -1,14 +1,14 @@
1
- flask
2
- flask_cors
3
- python-dotenv
4
- requests
5
- openai
6
- elasticsearch
7
- psutil
8
- urllib3
9
- matplotlib
10
- plotly
11
- numpy
12
- pillow
13
- langchain
14
  langchain_community
 
1
+ flask
2
+ flask_cors
3
+ python-dotenv
4
+ requests
5
+ openai
6
+ elasticsearch
7
+ psutil
8
+ urllib3
9
+ matplotlib
10
+ plotly
11
+ numpy
12
+ pillow
13
+ langchain
14
  langchain_community
templates/code_execution.html CHANGED
@@ -1,718 +1,718 @@
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代码助手 - Python执行环境</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/vs2015.min.css">
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
11
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
12
- <style>
13
- /* Base styles */
14
- :root {
15
- --primary-color: #4361ee;
16
- --secondary-color: #3f37c9;
17
- --accent-color: #4cc9f0;
18
- --success-color: #4caf50;
19
- --warning-color: #ff9800;
20
- --danger-color: #f44336;
21
- --light-color: #f8f9fa;
22
- --dark-color: #212529;
23
- --border-color: #dee2e6;
24
- --border-radius: 0.375rem;
25
- }
26
-
27
- body {
28
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
29
- margin: 0;
30
- padding: 0;
31
- height: 100vh;
32
- background-color: #f5f7fa;
33
- color: #333;
34
- display: flex;
35
- flex-direction: column;
36
- }
37
-
38
- /* Layout structure */
39
- .workspace {
40
- display: grid;
41
- grid-template-columns: 1fr 1fr;
42
- gap: 16px;
43
- flex: 1;
44
- padding: 16px;
45
- }
46
-
47
- @media (max-width: 992px) {
48
- .workspace {
49
- grid-template-columns: 1fr;
50
- }
51
- }
52
-
53
- .section {
54
- background: #fff;
55
- border-radius: var(--border-radius);
56
- overflow: hidden;
57
- display: flex;
58
- flex-direction: column;
59
- border: 1px solid var(--border-color);
60
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
61
- }
62
-
63
- .section-header {
64
- background: #f8f9fa;
65
- padding: 12px 16px;
66
- border-bottom: 1px solid var(--border-color);
67
- display: flex;
68
- justify-content: space-between;
69
- align-items: center;
70
- }
71
-
72
- .section-title {
73
- display: flex;
74
- align-items: center;
75
- gap: 8px;
76
- font-size: 1rem;
77
- font-weight: 500;
78
- }
79
-
80
- /* Code editor styles */
81
- .editor-content {
82
- flex: 1;
83
- position: relative;
84
- overflow: hidden;
85
- }
86
-
87
- .code-area {
88
- position: absolute;
89
- left: 40px;
90
- right: 0;
91
- top: 0;
92
- bottom: 0;
93
- padding: 12px 16px;
94
- color: #333;
95
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
96
- font-size: 14px;
97
- line-height: 1.6;
98
- overflow: auto;
99
- }
100
-
101
- .code-area pre {
102
- margin: 0;
103
- padding: 0;
104
- background: none;
105
- border: none;
106
- }
107
-
108
- .code-area code {
109
- display: block;
110
- padding: 0;
111
- tab-size: 4;
112
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
113
- outline: none;
114
- position: relative;
115
- min-height: 100%;
116
- white-space: pre !important;
117
- word-wrap: normal !important;
118
- }
119
-
120
- .line-numbers {
121
- position: absolute;
122
- left: 0;
123
- top: 0;
124
- bottom: 0;
125
- width: 40px;
126
- padding: 12px 0;
127
- background: #f5f7fa;
128
- border-right: 1px solid #e9ecef;
129
- text-align: center;
130
- color: #6c757d;
131
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
132
- font-size: 14px;
133
- line-height: 1.6;
134
- user-select: none;
135
- }
136
-
137
- /* Terminal styles */
138
- .output-content {
139
- flex: 1;
140
- background: #f8f9fa;
141
- position: relative;
142
- overflow: hidden;
143
- display: flex;
144
- flex-direction: column;
145
- }
146
-
147
- .terminal-window {
148
- flex: 1;
149
- overflow-y: auto;
150
- background: #212529;
151
- color: #f8f9fa;
152
- }
153
-
154
- #output {
155
- color: #f8f9fa;
156
- margin: 0;
157
- padding: 12px 16px;
158
- background: transparent;
159
- border: none;
160
- white-space: pre-wrap;
161
- word-wrap: break-word;
162
- line-height: 1.6;
163
- font-size: 14px;
164
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
165
- }
166
-
167
- .console-input-container {
168
- display: flex;
169
- align-items: center;
170
- background: #343a40;
171
- border-top: 1px solid #495057;
172
- padding: 8px 12px;
173
- }
174
-
175
- .console-prompt {
176
- color: #4caf50;
177
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
178
- margin-right: 8px;
179
- font-size: 14px;
180
- user-select: none;
181
- }
182
-
183
- .console-input {
184
- flex: 1;
185
- background: transparent;
186
- border: none;
187
- color: #f8f9fa;
188
- font-family: 'JetBrains Mono', 'Fira Code', monospace;
189
- font-size: 14px;
190
- line-height: 1.5;
191
- padding: 4px 0;
192
- }
193
-
194
- .console-input:focus {
195
- outline: none;
196
- }
197
-
198
- /* Chat section */
199
- .chat-section {
200
- display: flex;
201
- flex-direction: column;
202
- height: 100%;
203
- }
204
-
205
- .chat-messages {
206
- flex: 1;
207
- overflow-y: auto;
208
- padding: 16px;
209
- }
210
-
211
- .message {
212
- margin-bottom: 16px;
213
- padding: 12px;
214
- border-radius: var(--border-radius);
215
- max-width: 85%;
216
- position: relative;
217
- }
218
-
219
- .message.user {
220
- background-color: #e3f2fd;
221
- color: #0d47a1;
222
- align-self: flex-end;
223
- margin-left: auto;
224
- }
225
-
226
- .message.bot {
227
- background-color: #f5f5f5;
228
- color: #333;
229
- align-self: flex-start;
230
- border-left: 3px solid var(--primary-color);
231
- }
232
-
233
- .chat-input-container {
234
- padding: 16px;
235
- border-top: 1px solid var(--border-color);
236
- background-color: #f9f9f9;
237
- }
238
-
239
- .input-row {
240
- display: flex;
241
- gap: 8px;
242
- }
243
-
244
- .chat-input {
245
- flex: 1;
246
- padding: 12px;
247
- border: 1px solid var(--border-color);
248
- border-radius: var(--border-radius);
249
- resize: none;
250
- font-size: 14px;
251
- height: 100px;
252
- }
253
-
254
- .chat-input:focus {
255
- outline: none;
256
- border-color: var(--primary-color);
257
- box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
258
- }
259
-
260
- /* Button styles */
261
- .btn {
262
- padding: 8px 16px;
263
- border: none;
264
- border-radius: var(--border-radius);
265
- cursor: pointer;
266
- font-weight: 500;
267
- font-size: 14px;
268
- display: flex;
269
- align-items: center;
270
- gap: 8px;
271
- transition: all 0.2s ease;
272
- }
273
-
274
- .btn-primary {
275
- background-color: var(--primary-color);
276
- color: white;
277
- }
278
-
279
- .btn-primary:hover {
280
- background-color: var(--secondary-color);
281
- }
282
-
283
- .btn-secondary {
284
- background-color: #6c757d;
285
- color: white;
286
- }
287
-
288
- .btn-secondary:hover {
289
- background-color: #5a6268;
290
- }
291
-
292
- .btn-danger {
293
- background-color: var(--danger-color);
294
- color: white;
295
- }
296
-
297
- .btn-danger:hover {
298
- background-color: #d32f2f;
299
- }
300
-
301
- /* Utilities */
302
- .loading {
303
- display: none;
304
- align-items: center;
305
- gap: 8px;
306
- color: #6c757d;
307
- font-size: 14px;
308
- }
309
-
310
- .loading.active {
311
- display: flex;
312
- }
313
-
314
- @keyframes spin {
315
- to { transform: rotate(360deg); }
316
- }
317
-
318
- .loading i {
319
- animation: spin 1s linear infinite;
320
- }
321
-
322
- /* Custom scrollbar */
323
- ::-webkit-scrollbar {
324
- width: 8px;
325
- height: 8px;
326
- }
327
-
328
- ::-webkit-scrollbar-track {
329
- background: #f1f1f1;
330
- }
331
-
332
- ::-webkit-scrollbar-thumb {
333
- background: #c1c1c1;
334
- border-radius: 4px;
335
- }
336
-
337
- ::-webkit-scrollbar-thumb:hover {
338
- background: #a8a8a8;
339
- }
340
-
341
- /* Terminal text styles */
342
- .term-input {
343
- color: #4caf50;
344
- }
345
-
346
- .term-output {
347
- color: #f8f9fa;
348
- }
349
-
350
- .term-error {
351
- color: #f44336;
352
- }
353
-
354
- .term-warning {
355
- color: #ff9800;
356
- }
357
-
358
- .term-system {
359
- color: #2196f3;
360
- }
361
-
362
- /* Header */
363
- .header {
364
- background-color: #fff;
365
- border-bottom: 1px solid var(--border-color);
366
- padding: 1rem;
367
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
368
- }
369
-
370
- .header-content {
371
- max-width: 1200px;
372
- margin: 0 auto;
373
- display: flex;
374
- justify-content: space-between;
375
- align-items: center;
376
- }
377
-
378
- .header h1 {
379
- margin: 0;
380
- font-size: 1.5rem;
381
- color: var(--primary-color);
382
- }
383
- </style>
384
- </head>
385
- <body>
386
- <header class="header">
387
- <div class="header-content">
388
- <h1>AI代码助手</h1>
389
- <div>
390
- <span class="badge bg-primary">Python编程环境</span>
391
- </div>
392
- </div>
393
- </header>
394
-
395
- <div class="workspace">
396
- <div class="section">
397
- <div class="section-header">
398
- <div class="section-title">
399
- <i class="bi bi-code-square"></i>
400
- 代码编辑器
401
- </div>
402
- <div style="display: flex; gap: 8px;">
403
- <button class="btn btn-primary" id="runCode">
404
- <i class="bi bi-play-fill"></i>
405
- 运行
406
- </button>
407
- <button class="btn btn-danger" id="stopCode" style="display: none;">
408
- <i class="bi bi-stop-fill"></i>
409
- 停止
410
- </button>
411
- <button class="btn btn-secondary" id="clearCode">
412
- <i class="bi bi-trash"></i>
413
- 清除
414
- </button>
415
- </div>
416
- </div>
417
- <div class="editor-content">
418
- <div class="line-numbers" id="lineNumbers">1</div>
419
- <div class="code-area" id="codeArea">
420
- <pre><code class="language-python" contenteditable="true" spellcheck="false" autocorrect="off" autocapitalize="off"># 您的代码将在这里显示</code></pre>
421
- </div>
422
- </div>
423
- </div>
424
-
425
- <div class="section">
426
- <div class="section-header">
427
- <div class="section-title">
428
- <i class="bi bi-terminal"></i>
429
- 终端输出
430
- </div>
431
- <div style="display: flex; gap: 8px;">
432
- <button class="btn btn-secondary" id="clearTerminal">
433
- <i class="bi bi-eraser"></i>
434
- 清除
435
- </button>
436
- </div>
437
- </div>
438
- <div class="output-content">
439
- <div class="terminal-window">
440
- <pre id="output"></pre>
441
- </div>
442
- <div class="console-input-container" id="consoleInputContainer" style="display: none;">
443
- <div class="console-prompt">>></div>
444
- <input type="text" id="consoleInput" class="console-input" autocomplete="off" spellcheck="false" />
445
- </div>
446
- </div>
447
- </div>
448
- </div>
449
-
450
- <script>
451
- document.addEventListener('DOMContentLoaded', () => {
452
- // Terminal elements
453
- const terminalOutput = document.getElementById('output');
454
- const consoleInputContainer = document.getElementById('consoleInputContainer');
455
- const consoleInput = document.getElementById('consoleInput');
456
- const clearTerminalBtn = document.getElementById('clearTerminal');
457
- const stopCodeBtn = document.getElementById('stopCode');
458
-
459
- // Editor elements
460
- const codeArea = document.querySelector('#codeArea code');
461
- const lineNumbers = document.getElementById('lineNumbers');
462
- const runButton = document.getElementById('runCode');
463
- const clearButton = document.getElementById('clearCode');
464
-
465
- // State variables
466
- let executionContext = null;
467
- let isExecuting = false;
468
- let executionStartTime = null;
469
-
470
- // Initialize terminal
471
- initializeTerminal();
472
-
473
- // Initialize editor
474
- updateLineNumbers();
475
-
476
- // Function to update line numbers
477
- function updateLineNumbers() {
478
- const lines = codeArea.textContent.split('\n').length;
479
- lineNumbers.innerHTML = Array.from({length: lines}, (_, i) => i + 1).join('<br>');
480
- }
481
-
482
- // Initialize terminal with welcome message
483
- function initializeTerminal() {
484
- clearTerminalOutput();
485
- appendToTerminal("欢迎使用Python交互式终端", "term-system");
486
- appendToTerminal("使用'运行'按钮执行您的代码", "term-system");
487
- appendToTerminal("", "term-system"); // 空行
488
- appendToTerminal(">>> 准备执行代码...", "term-system");
489
- }
490
-
491
- // Append text to terminal
492
- function appendToTerminal(text, type = null) {
493
- const lines = text.split('\n');
494
- let html = '';
495
-
496
- for (const line of lines) {
497
- if (type) {
498
- html += `<span class="${type}">${escapeHtml(line)}</span>\n`;
499
- } else {
500
- html += escapeHtml(line) + '\n';
501
- }
502
- }
503
-
504
- terminalOutput.innerHTML += html;
505
- terminalOutput.scrollTop = terminalOutput.scrollHeight;
506
- }
507
-
508
- // Clear terminal output
509
- function clearTerminalOutput() {
510
- terminalOutput.innerHTML = '';
511
- }
512
-
513
- // Escape HTML to prevent XSS
514
- function escapeHtml(text) {
515
- return text
516
- .replace(/&/g, "&amp;")
517
- .replace(/</g, "&lt;")
518
- .replace(/>/g, "&gt;")
519
- .replace(/"/g, "&quot;")
520
- .replace(/'/g, "&#039;");
521
- }
522
-
523
- // Run code
524
- runButton.addEventListener('click', async () => {
525
- if (isExecuting) return; // Prevent multiple executions
526
-
527
- const code = codeArea.textContent;
528
-
529
- // Clear terminal and show execution start
530
- clearTerminalOutput();
531
- appendToTerminal("开始执行Python代码...", "term-system");
532
-
533
- // Update UI state
534
- isExecuting = true;
535
- executionStartTime = performance.now();
536
- runButton.style.display = 'none';
537
- stopCodeBtn.style.display = 'flex';
538
-
539
- try {
540
- const response = await fetch('/api/code/execute', {
541
- method: 'POST',
542
- headers: { 'Content-Type': 'application/json' },
543
- body: JSON.stringify({ code })
544
- });
545
-
546
- const data = await response.json();
547
-
548
- if (data.success) {
549
- // Show output (if any)
550
- if (data.output && data.output.trim()) {
551
- appendToTerminal(data.output, "term-output");
552
- }
553
-
554
- if (data.needsInput) {
555
- // Code is waiting for input
556
- executionContext = data.context_id;
557
- consoleInputContainer.style.display = 'flex';
558
- consoleInput.focus();
559
- } else {
560
- // Code has completed, no input needed
561
- appendToTerminal("程序执行完成", "term-system");
562
- finishExecution();
563
- }
564
- } else {
565
- // Handle error
566
- appendToTerminal(`错误: ${data.error}`, "term-error");
567
- if (data.traceback) {
568
- appendToTerminal(data.traceback, "term-error");
569
- }
570
- appendToTerminal("执行失败", "term-system");
571
- finishExecution();
572
- }
573
- } catch (error) {
574
- appendToTerminal(`系统错误: ${error.message}`, "term-error");
575
- appendToTerminal("执行失败", "term-system");
576
- finishExecution();
577
- }
578
- });
579
-
580
- // Submit console input
581
- consoleInput.addEventListener('keydown', async (e) => {
582
- if (e.key === 'Enter' && executionContext) {
583
- const input = consoleInput.value;
584
- consoleInput.value = '';
585
-
586
- // Add input to terminal
587
- appendToTerminal(`>> ${input}`, "term-input");
588
-
589
- try {
590
- // Send input to backend
591
- const response = await fetch('/api/code/input', {
592
- method: 'POST',
593
- headers: { 'Content-Type': 'application/json' },
594
- body: JSON.stringify({
595
- input: input,
596
- context_id: executionContext
597
- })
598
- });
599
-
600
- const data = await response.json();
601
-
602
- if (data.success) {
603
- // Show new output
604
- if (data.output && data.output.trim()) {
605
- appendToTerminal(data.output, "term-output");
606
- }
607
-
608
- if (data.needsInput) {
609
- // Still waiting for more input
610
- consoleInput.focus();
611
- } else {
612
- // Execution completed
613
- appendToTerminal(">>> 程序执行完成", "term-system");
614
- finishExecution();
615
- }
616
- } else {
617
- // Handle error
618
- appendToTerminal(`错误: ${data.error}`, "term-error");
619
- if (data.traceback) {
620
- appendToTerminal(data.traceback, "term-error");
621
- }
622
- finishExecution();
623
- }
624
- } catch (error) {
625
- appendToTerminal(`系统错误: ${error.message}`, "term-error");
626
- finishExecution();
627
- }
628
- }
629
- });
630
-
631
- // Stop code execution
632
- stopCodeBtn.addEventListener('click', async () => {
633
- if (!executionContext) return;
634
-
635
- try {
636
- // Send stop request to server
637
- const response = await fetch('/api/code/stop', {
638
- method: 'POST',
639
- headers: { 'Content-Type': 'application/json' },
640
- body: JSON.stringify({ context_id: executionContext })
641
- });
642
-
643
- // Clean up UI regardless of response
644
- appendToTerminal("用户终止了执行", "term-warning");
645
- finishExecution();
646
- } catch (error) {
647
- console.error('停止执行时出错:', error);
648
- finishExecution();
649
- }
650
- });
651
-
652
- // Clear code
653
- clearButton.addEventListener('click', () => {
654
- codeArea.textContent = '# 您的代码将在这里显示';
655
- updateLineNumbers();
656
- clearTerminalOutput();
657
- initializeTerminal();
658
- consoleInputContainer.style.display = 'none';
659
- executionContext = null;
660
- isExecuting = false;
661
- runButton.style.display = 'flex';
662
- stopCodeBtn.style.display = 'none';
663
- });
664
-
665
- // Clear terminal
666
- clearTerminalBtn.addEventListener('click', () => {
667
- if (!isExecuting) {
668
- clearTerminalOutput();
669
- initializeTerminal();
670
- } else {
671
- // If execution is in progress, just add a separator
672
- appendToTerminal("\n--- 已清除终端 ---\n", "term-system");
673
- }
674
- });
675
-
676
- // Update line numbers on code changes
677
- codeArea.addEventListener('input', updateLineNumbers);
678
-
679
- // Handle tab key
680
- codeArea.addEventListener('keydown', (e) => {
681
- if (e.key === 'Tab') {
682
- e.preventDefault();
683
- document.execCommand('insertText', false, ' ');
684
- }
685
- });
686
-
687
- // Function to clean up after execution completes
688
- function finishExecution() {
689
- consoleInputContainer.style.display = 'none';
690
- executionContext = null;
691
- isExecuting = false;
692
- runButton.style.display = 'flex';
693
- stopCodeBtn.style.display = 'none';
694
- }
695
-
696
- // Check if code was provided via URL parameters
697
- const urlParams = new URLSearchParams(window.location.search);
698
- const initialCode = urlParams.get('code');
699
- if (initialCode) {
700
- try {
701
- codeArea.textContent = decodeURIComponent(initialCode);
702
- updateLineNumbers();
703
- } catch (e) {
704
- console.error('Failed to decode initial code:', e);
705
- }
706
- }
707
-
708
- // Check for messages from parent frame
709
- window.addEventListener('message', (event) => {
710
- if (event.data && event.data.type === 'setCode') {
711
- codeArea.textContent = event.data.code;
712
- updateLineNumbers();
713
- }
714
- });
715
- });
716
- </script>
717
- </body>
718
  </html>
 
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代码助手 - Python执行环境</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/vs2015.min.css">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
12
+ <style>
13
+ /* Base styles */
14
+ :root {
15
+ --primary-color: #4361ee;
16
+ --secondary-color: #3f37c9;
17
+ --accent-color: #4cc9f0;
18
+ --success-color: #4caf50;
19
+ --warning-color: #ff9800;
20
+ --danger-color: #f44336;
21
+ --light-color: #f8f9fa;
22
+ --dark-color: #212529;
23
+ --border-color: #dee2e6;
24
+ --border-radius: 0.375rem;
25
+ }
26
+
27
+ body {
28
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
29
+ margin: 0;
30
+ padding: 0;
31
+ height: 100vh;
32
+ background-color: #f5f7fa;
33
+ color: #333;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* Layout structure */
39
+ .workspace {
40
+ display: grid;
41
+ grid-template-columns: 1fr 1fr;
42
+ gap: 16px;
43
+ flex: 1;
44
+ padding: 16px;
45
+ }
46
+
47
+ @media (max-width: 992px) {
48
+ .workspace {
49
+ grid-template-columns: 1fr;
50
+ }
51
+ }
52
+
53
+ .section {
54
+ background: #fff;
55
+ border-radius: var(--border-radius);
56
+ overflow: hidden;
57
+ display: flex;
58
+ flex-direction: column;
59
+ border: 1px solid var(--border-color);
60
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
61
+ }
62
+
63
+ .section-header {
64
+ background: #f8f9fa;
65
+ padding: 12px 16px;
66
+ border-bottom: 1px solid var(--border-color);
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ }
71
+
72
+ .section-title {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 8px;
76
+ font-size: 1rem;
77
+ font-weight: 500;
78
+ }
79
+
80
+ /* Code editor styles */
81
+ .editor-content {
82
+ flex: 1;
83
+ position: relative;
84
+ overflow: hidden;
85
+ }
86
+
87
+ .code-area {
88
+ position: absolute;
89
+ left: 40px;
90
+ right: 0;
91
+ top: 0;
92
+ bottom: 0;
93
+ padding: 12px 16px;
94
+ color: #333;
95
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
96
+ font-size: 14px;
97
+ line-height: 1.6;
98
+ overflow: auto;
99
+ }
100
+
101
+ .code-area pre {
102
+ margin: 0;
103
+ padding: 0;
104
+ background: none;
105
+ border: none;
106
+ }
107
+
108
+ .code-area code {
109
+ display: block;
110
+ padding: 0;
111
+ tab-size: 4;
112
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
113
+ outline: none;
114
+ position: relative;
115
+ min-height: 100%;
116
+ white-space: pre !important;
117
+ word-wrap: normal !important;
118
+ }
119
+
120
+ .line-numbers {
121
+ position: absolute;
122
+ left: 0;
123
+ top: 0;
124
+ bottom: 0;
125
+ width: 40px;
126
+ padding: 12px 0;
127
+ background: #f5f7fa;
128
+ border-right: 1px solid #e9ecef;
129
+ text-align: center;
130
+ color: #6c757d;
131
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
132
+ font-size: 14px;
133
+ line-height: 1.6;
134
+ user-select: none;
135
+ }
136
+
137
+ /* Terminal styles */
138
+ .output-content {
139
+ flex: 1;
140
+ background: #f8f9fa;
141
+ position: relative;
142
+ overflow: hidden;
143
+ display: flex;
144
+ flex-direction: column;
145
+ }
146
+
147
+ .terminal-window {
148
+ flex: 1;
149
+ overflow-y: auto;
150
+ background: #212529;
151
+ color: #f8f9fa;
152
+ }
153
+
154
+ #output {
155
+ color: #f8f9fa;
156
+ margin: 0;
157
+ padding: 12px 16px;
158
+ background: transparent;
159
+ border: none;
160
+ white-space: pre-wrap;
161
+ word-wrap: break-word;
162
+ line-height: 1.6;
163
+ font-size: 14px;
164
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
165
+ }
166
+
167
+ .console-input-container {
168
+ display: flex;
169
+ align-items: center;
170
+ background: #343a40;
171
+ border-top: 1px solid #495057;
172
+ padding: 8px 12px;
173
+ }
174
+
175
+ .console-prompt {
176
+ color: #4caf50;
177
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
178
+ margin-right: 8px;
179
+ font-size: 14px;
180
+ user-select: none;
181
+ }
182
+
183
+ .console-input {
184
+ flex: 1;
185
+ background: transparent;
186
+ border: none;
187
+ color: #f8f9fa;
188
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
189
+ font-size: 14px;
190
+ line-height: 1.5;
191
+ padding: 4px 0;
192
+ }
193
+
194
+ .console-input:focus {
195
+ outline: none;
196
+ }
197
+
198
+ /* Chat section */
199
+ .chat-section {
200
+ display: flex;
201
+ flex-direction: column;
202
+ height: 100%;
203
+ }
204
+
205
+ .chat-messages {
206
+ flex: 1;
207
+ overflow-y: auto;
208
+ padding: 16px;
209
+ }
210
+
211
+ .message {
212
+ margin-bottom: 16px;
213
+ padding: 12px;
214
+ border-radius: var(--border-radius);
215
+ max-width: 85%;
216
+ position: relative;
217
+ }
218
+
219
+ .message.user {
220
+ background-color: #e3f2fd;
221
+ color: #0d47a1;
222
+ align-self: flex-end;
223
+ margin-left: auto;
224
+ }
225
+
226
+ .message.bot {
227
+ background-color: #f5f5f5;
228
+ color: #333;
229
+ align-self: flex-start;
230
+ border-left: 3px solid var(--primary-color);
231
+ }
232
+
233
+ .chat-input-container {
234
+ padding: 16px;
235
+ border-top: 1px solid var(--border-color);
236
+ background-color: #f9f9f9;
237
+ }
238
+
239
+ .input-row {
240
+ display: flex;
241
+ gap: 8px;
242
+ }
243
+
244
+ .chat-input {
245
+ flex: 1;
246
+ padding: 12px;
247
+ border: 1px solid var(--border-color);
248
+ border-radius: var(--border-radius);
249
+ resize: none;
250
+ font-size: 14px;
251
+ height: 100px;
252
+ }
253
+
254
+ .chat-input:focus {
255
+ outline: none;
256
+ border-color: var(--primary-color);
257
+ box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
258
+ }
259
+
260
+ /* Button styles */
261
+ .btn {
262
+ padding: 8px 16px;
263
+ border: none;
264
+ border-radius: var(--border-radius);
265
+ cursor: pointer;
266
+ font-weight: 500;
267
+ font-size: 14px;
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 8px;
271
+ transition: all 0.2s ease;
272
+ }
273
+
274
+ .btn-primary {
275
+ background-color: var(--primary-color);
276
+ color: white;
277
+ }
278
+
279
+ .btn-primary:hover {
280
+ background-color: var(--secondary-color);
281
+ }
282
+
283
+ .btn-secondary {
284
+ background-color: #6c757d;
285
+ color: white;
286
+ }
287
+
288
+ .btn-secondary:hover {
289
+ background-color: #5a6268;
290
+ }
291
+
292
+ .btn-danger {
293
+ background-color: var(--danger-color);
294
+ color: white;
295
+ }
296
+
297
+ .btn-danger:hover {
298
+ background-color: #d32f2f;
299
+ }
300
+
301
+ /* Utilities */
302
+ .loading {
303
+ display: none;
304
+ align-items: center;
305
+ gap: 8px;
306
+ color: #6c757d;
307
+ font-size: 14px;
308
+ }
309
+
310
+ .loading.active {
311
+ display: flex;
312
+ }
313
+
314
+ @keyframes spin {
315
+ to { transform: rotate(360deg); }
316
+ }
317
+
318
+ .loading i {
319
+ animation: spin 1s linear infinite;
320
+ }
321
+
322
+ /* Custom scrollbar */
323
+ ::-webkit-scrollbar {
324
+ width: 8px;
325
+ height: 8px;
326
+ }
327
+
328
+ ::-webkit-scrollbar-track {
329
+ background: #f1f1f1;
330
+ }
331
+
332
+ ::-webkit-scrollbar-thumb {
333
+ background: #c1c1c1;
334
+ border-radius: 4px;
335
+ }
336
+
337
+ ::-webkit-scrollbar-thumb:hover {
338
+ background: #a8a8a8;
339
+ }
340
+
341
+ /* Terminal text styles */
342
+ .term-input {
343
+ color: #4caf50;
344
+ }
345
+
346
+ .term-output {
347
+ color: #f8f9fa;
348
+ }
349
+
350
+ .term-error {
351
+ color: #f44336;
352
+ }
353
+
354
+ .term-warning {
355
+ color: #ff9800;
356
+ }
357
+
358
+ .term-system {
359
+ color: #2196f3;
360
+ }
361
+
362
+ /* Header */
363
+ .header {
364
+ background-color: #fff;
365
+ border-bottom: 1px solid var(--border-color);
366
+ padding: 1rem;
367
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
368
+ }
369
+
370
+ .header-content {
371
+ max-width: 1200px;
372
+ margin: 0 auto;
373
+ display: flex;
374
+ justify-content: space-between;
375
+ align-items: center;
376
+ }
377
+
378
+ .header h1 {
379
+ margin: 0;
380
+ font-size: 1.5rem;
381
+ color: var(--primary-color);
382
+ }
383
+ </style>
384
+ </head>
385
+ <body>
386
+ <header class="header">
387
+ <div class="header-content">
388
+ <h1>AI代码助手</h1>
389
+ <div>
390
+ <span class="badge bg-primary">Python编程环境</span>
391
+ </div>
392
+ </div>
393
+ </header>
394
+
395
+ <div class="workspace">
396
+ <div class="section">
397
+ <div class="section-header">
398
+ <div class="section-title">
399
+ <i class="bi bi-code-square"></i>
400
+ 代码编辑器
401
+ </div>
402
+ <div style="display: flex; gap: 8px;">
403
+ <button class="btn btn-primary" id="runCode">
404
+ <i class="bi bi-play-fill"></i>
405
+ 运行
406
+ </button>
407
+ <button class="btn btn-danger" id="stopCode" style="display: none;">
408
+ <i class="bi bi-stop-fill"></i>
409
+ 停止
410
+ </button>
411
+ <button class="btn btn-secondary" id="clearCode">
412
+ <i class="bi bi-trash"></i>
413
+ 清除
414
+ </button>
415
+ </div>
416
+ </div>
417
+ <div class="editor-content">
418
+ <div class="line-numbers" id="lineNumbers">1</div>
419
+ <div class="code-area" id="codeArea">
420
+ <pre><code class="language-python" contenteditable="true" spellcheck="false" autocorrect="off" autocapitalize="off"># 您的代码将在这里显示</code></pre>
421
+ </div>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="section">
426
+ <div class="section-header">
427
+ <div class="section-title">
428
+ <i class="bi bi-terminal"></i>
429
+ 终端输出
430
+ </div>
431
+ <div style="display: flex; gap: 8px;">
432
+ <button class="btn btn-secondary" id="clearTerminal">
433
+ <i class="bi bi-eraser"></i>
434
+ 清除
435
+ </button>
436
+ </div>
437
+ </div>
438
+ <div class="output-content">
439
+ <div class="terminal-window">
440
+ <pre id="output"></pre>
441
+ </div>
442
+ <div class="console-input-container" id="consoleInputContainer" style="display: none;">
443
+ <div class="console-prompt">>></div>
444
+ <input type="text" id="consoleInput" class="console-input" autocomplete="off" spellcheck="false" />
445
+ </div>
446
+ </div>
447
+ </div>
448
+ </div>
449
+
450
+ <script>
451
+ document.addEventListener('DOMContentLoaded', () => {
452
+ // Terminal elements
453
+ const terminalOutput = document.getElementById('output');
454
+ const consoleInputContainer = document.getElementById('consoleInputContainer');
455
+ const consoleInput = document.getElementById('consoleInput');
456
+ const clearTerminalBtn = document.getElementById('clearTerminal');
457
+ const stopCodeBtn = document.getElementById('stopCode');
458
+
459
+ // Editor elements
460
+ const codeArea = document.querySelector('#codeArea code');
461
+ const lineNumbers = document.getElementById('lineNumbers');
462
+ const runButton = document.getElementById('runCode');
463
+ const clearButton = document.getElementById('clearCode');
464
+
465
+ // State variables
466
+ let executionContext = null;
467
+ let isExecuting = false;
468
+ let executionStartTime = null;
469
+
470
+ // Initialize terminal
471
+ initializeTerminal();
472
+
473
+ // Initialize editor
474
+ updateLineNumbers();
475
+
476
+ // Function to update line numbers
477
+ function updateLineNumbers() {
478
+ const lines = codeArea.textContent.split('\n').length;
479
+ lineNumbers.innerHTML = Array.from({length: lines}, (_, i) => i + 1).join('<br>');
480
+ }
481
+
482
+ // Initialize terminal with welcome message
483
+ function initializeTerminal() {
484
+ clearTerminalOutput();
485
+ appendToTerminal("欢迎使用Python交互式终端", "term-system");
486
+ appendToTerminal("使用'运行'按钮执行您的代码", "term-system");
487
+ appendToTerminal("", "term-system"); // 空行
488
+ appendToTerminal(">>> 准备执行代码...", "term-system");
489
+ }
490
+
491
+ // Append text to terminal
492
+ function appendToTerminal(text, type = null) {
493
+ const lines = text.split('\n');
494
+ let html = '';
495
+
496
+ for (const line of lines) {
497
+ if (type) {
498
+ html += `<span class="${type}">${escapeHtml(line)}</span>\n`;
499
+ } else {
500
+ html += escapeHtml(line) + '\n';
501
+ }
502
+ }
503
+
504
+ terminalOutput.innerHTML += html;
505
+ terminalOutput.scrollTop = terminalOutput.scrollHeight;
506
+ }
507
+
508
+ // Clear terminal output
509
+ function clearTerminalOutput() {
510
+ terminalOutput.innerHTML = '';
511
+ }
512
+
513
+ // Escape HTML to prevent XSS
514
+ function escapeHtml(text) {
515
+ return text
516
+ .replace(/&/g, "&amp;")
517
+ .replace(/</g, "&lt;")
518
+ .replace(/>/g, "&gt;")
519
+ .replace(/"/g, "&quot;")
520
+ .replace(/'/g, "&#039;");
521
+ }
522
+
523
+ // Run code
524
+ runButton.addEventListener('click', async () => {
525
+ if (isExecuting) return; // Prevent multiple executions
526
+
527
+ const code = codeArea.textContent;
528
+
529
+ // Clear terminal and show execution start
530
+ clearTerminalOutput();
531
+ appendToTerminal("开始执行Python代码...", "term-system");
532
+
533
+ // Update UI state
534
+ isExecuting = true;
535
+ executionStartTime = performance.now();
536
+ runButton.style.display = 'none';
537
+ stopCodeBtn.style.display = 'flex';
538
+
539
+ try {
540
+ const response = await fetch('/api/code/execute', {
541
+ method: 'POST',
542
+ headers: { 'Content-Type': 'application/json' },
543
+ body: JSON.stringify({ code })
544
+ });
545
+
546
+ const data = await response.json();
547
+
548
+ if (data.success) {
549
+ // Show output (if any)
550
+ if (data.output && data.output.trim()) {
551
+ appendToTerminal(data.output, "term-output");
552
+ }
553
+
554
+ if (data.needsInput) {
555
+ // Code is waiting for input
556
+ executionContext = data.context_id;
557
+ consoleInputContainer.style.display = 'flex';
558
+ consoleInput.focus();
559
+ } else {
560
+ // Code has completed, no input needed
561
+ appendToTerminal("程序执行完成", "term-system");
562
+ finishExecution();
563
+ }
564
+ } else {
565
+ // Handle error
566
+ appendToTerminal(`错误: ${data.error}`, "term-error");
567
+ if (data.traceback) {
568
+ appendToTerminal(data.traceback, "term-error");
569
+ }
570
+ appendToTerminal("执行失败", "term-system");
571
+ finishExecution();
572
+ }
573
+ } catch (error) {
574
+ appendToTerminal(`系统错误: ${error.message}`, "term-error");
575
+ appendToTerminal("执行失败", "term-system");
576
+ finishExecution();
577
+ }
578
+ });
579
+
580
+ // Submit console input
581
+ consoleInput.addEventListener('keydown', async (e) => {
582
+ if (e.key === 'Enter' && executionContext) {
583
+ const input = consoleInput.value;
584
+ consoleInput.value = '';
585
+
586
+ // Add input to terminal
587
+ appendToTerminal(`>> ${input}`, "term-input");
588
+
589
+ try {
590
+ // Send input to backend
591
+ const response = await fetch('/api/code/input', {
592
+ method: 'POST',
593
+ headers: { 'Content-Type': 'application/json' },
594
+ body: JSON.stringify({
595
+ input: input,
596
+ context_id: executionContext
597
+ })
598
+ });
599
+
600
+ const data = await response.json();
601
+
602
+ if (data.success) {
603
+ // Show new output
604
+ if (data.output && data.output.trim()) {
605
+ appendToTerminal(data.output, "term-output");
606
+ }
607
+
608
+ if (data.needsInput) {
609
+ // Still waiting for more input
610
+ consoleInput.focus();
611
+ } else {
612
+ // Execution completed
613
+ appendToTerminal(">>> 程序执行完成", "term-system");
614
+ finishExecution();
615
+ }
616
+ } else {
617
+ // Handle error
618
+ appendToTerminal(`错误: ${data.error}`, "term-error");
619
+ if (data.traceback) {
620
+ appendToTerminal(data.traceback, "term-error");
621
+ }
622
+ finishExecution();
623
+ }
624
+ } catch (error) {
625
+ appendToTerminal(`系统错误: ${error.message}`, "term-error");
626
+ finishExecution();
627
+ }
628
+ }
629
+ });
630
+
631
+ // Stop code execution
632
+ stopCodeBtn.addEventListener('click', async () => {
633
+ if (!executionContext) return;
634
+
635
+ try {
636
+ // Send stop request to server
637
+ const response = await fetch('/api/code/stop', {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({ context_id: executionContext })
641
+ });
642
+
643
+ // Clean up UI regardless of response
644
+ appendToTerminal("用户终止了执行", "term-warning");
645
+ finishExecution();
646
+ } catch (error) {
647
+ console.error('停止执行时出错:', error);
648
+ finishExecution();
649
+ }
650
+ });
651
+
652
+ // Clear code
653
+ clearButton.addEventListener('click', () => {
654
+ codeArea.textContent = '# 您的代码将在这里显示';
655
+ updateLineNumbers();
656
+ clearTerminalOutput();
657
+ initializeTerminal();
658
+ consoleInputContainer.style.display = 'none';
659
+ executionContext = null;
660
+ isExecuting = false;
661
+ runButton.style.display = 'flex';
662
+ stopCodeBtn.style.display = 'none';
663
+ });
664
+
665
+ // Clear terminal
666
+ clearTerminalBtn.addEventListener('click', () => {
667
+ if (!isExecuting) {
668
+ clearTerminalOutput();
669
+ initializeTerminal();
670
+ } else {
671
+ // If execution is in progress, just add a separator
672
+ appendToTerminal("\n--- 已清除终端 ---\n", "term-system");
673
+ }
674
+ });
675
+
676
+ // Update line numbers on code changes
677
+ codeArea.addEventListener('input', updateLineNumbers);
678
+
679
+ // Handle tab key
680
+ codeArea.addEventListener('keydown', (e) => {
681
+ if (e.key === 'Tab') {
682
+ e.preventDefault();
683
+ document.execCommand('insertText', false, ' ');
684
+ }
685
+ });
686
+
687
+ // Function to clean up after execution completes
688
+ function finishExecution() {
689
+ consoleInputContainer.style.display = 'none';
690
+ executionContext = null;
691
+ isExecuting = false;
692
+ runButton.style.display = 'flex';
693
+ stopCodeBtn.style.display = 'none';
694
+ }
695
+
696
+ // Check if code was provided via URL parameters
697
+ const urlParams = new URLSearchParams(window.location.search);
698
+ const initialCode = urlParams.get('code');
699
+ if (initialCode) {
700
+ try {
701
+ codeArea.textContent = decodeURIComponent(initialCode);
702
+ updateLineNumbers();
703
+ } catch (e) {
704
+ console.error('Failed to decode initial code:', e);
705
+ }
706
+ }
707
+
708
+ // Check for messages from parent frame
709
+ window.addEventListener('message', (event) => {
710
+ if (event.data && event.data.type === 'setCode') {
711
+ codeArea.textContent = event.data.code;
712
+ updateLineNumbers();
713
+ }
714
+ });
715
+ });
716
+ </script>
717
+ </body>
718
  </html>
templates/index.html CHANGED
The diff for this file is too large to render. See raw diff
 
templates/login.html ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
7
+ <title>教育AI助手平台 - 登录</title>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
12
+
13
+ :root {
14
+ /* 优雅的配色方案 */
15
+ --primary-color: #0f2d49;
16
+ --primary-light: #234a70;
17
+ --secondary-color: #4a6cfd;
18
+ --secondary-light: #7b91ff;
19
+ --tertiary-color: #f7f9fe;
20
+ --success-color: #10b981;
21
+ --success-light: rgba(16, 185, 129, 0.1);
22
+ --warning-color: #f59e0b;
23
+ --warning-light: rgba(245, 158, 11, 0.1);
24
+ --info-color: #0ea5e9;
25
+ --info-light: rgba(14, 165, 233, 0.1);
26
+ --danger-color: #ef4444;
27
+ --danger-light: rgba(239, 68, 68, 0.1);
28
+ --neutral-50: #f9fafb;
29
+ --neutral-100: #f3f4f6;
30
+ --neutral-200: #e5e7eb;
31
+ --neutral-300: #d1d5db;
32
+ --neutral-400: #9ca3af;
33
+ --neutral-500: #6b7280;
34
+ --neutral-600: #4b5563;
35
+ --neutral-700: #374151;
36
+ --neutral-800: #1f2937;
37
+ --neutral-900: #111827;
38
+
39
+ /* 样式变量 */
40
+ --border-radius-sm: 0.25rem;
41
+ --border-radius: 0.375rem;
42
+ --border-radius-lg: 0.5rem;
43
+ --border-radius-xl: 0.75rem;
44
+ --border-radius-2xl: 1rem;
45
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
46
+ --card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1);
47
+ --card-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
48
+ --transition-base: all 0.2s ease-in-out;
49
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
50
+ --font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
51
+ }
52
+
53
+ /* 基础样式 */
54
+ body {
55
+ font-family: var(--font-family);
56
+ background-color: var(--neutral-50);
57
+ color: var(--neutral-800);
58
+ margin: 0;
59
+ padding: 0;
60
+ min-height: 100vh;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ -webkit-font-smoothing: antialiased;
65
+ -moz-osx-font-smoothing: grayscale;
66
+ }
67
+
68
+ h1, h2, h3, h4, h5, h6 {
69
+ font-weight: 600;
70
+ color: var(--neutral-900);
71
+ }
72
+
73
+ .text-gradient {
74
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
75
+ -webkit-background-clip: text;
76
+ -webkit-text-fill-color: transparent;
77
+ background-clip: text;
78
+ color: transparent;
79
+ }
80
+
81
+ /* 登录容器样式 */
82
+ .login-container {
83
+ width: 100%;
84
+ max-width: 450px;
85
+ padding: 2.5rem;
86
+ background-color: white;
87
+ border-radius: var(--border-radius-xl);
88
+ box-shadow: var(--card-shadow-lg);
89
+ transition: var(--transition-smooth);
90
+ position: relative;
91
+ overflow: hidden;
92
+ }
93
+
94
+ .login-container::before {
95
+ content: '';
96
+ position: absolute;
97
+ top: 0;
98
+ left: 0;
99
+ width: 100%;
100
+ height: 4px;
101
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
102
+ border-radius: 4px 4px 0 0;
103
+ }
104
+
105
+ .login-header {
106
+ text-align: center;
107
+ margin-bottom: 2.5rem;
108
+ }
109
+
110
+ .login-header h1 {
111
+ font-size: 1.75rem;
112
+ font-weight: 700;
113
+ margin-bottom: 0.5rem;
114
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
115
+ -webkit-background-clip: text;
116
+ -webkit-text-fill-color: transparent;
117
+ letter-spacing: -0.01em;
118
+ }
119
+
120
+ .login-header p {
121
+ color: var(--neutral-600);
122
+ margin-bottom: 0;
123
+ font-size: 0.95rem;
124
+ }
125
+
126
+ .user-type-switch {
127
+ margin-bottom: 2rem;
128
+ }
129
+
130
+ .user-type-switch .btn-group {
131
+ width: 100%;
132
+ box-shadow: var(--card-shadow);
133
+ border-radius: var(--border-radius-lg);
134
+ padding: 0.25rem;
135
+ background-color: var(--neutral-100);
136
+ }
137
+
138
+ .user-type-switch .btn {
139
+ flex: 1;
140
+ background: transparent;
141
+ border: none;
142
+ padding: 0.75rem 1rem;
143
+ font-weight: 500;
144
+ color: var(--neutral-700);
145
+ border-radius: var(--border-radius);
146
+ transition: var(--transition-base);
147
+ }
148
+
149
+ .user-type-switch .btn:hover {
150
+ color: var(--primary-color);
151
+ }
152
+
153
+ .user-type-switch .btn.active {
154
+ background: white;
155
+ color: var(--secondary-color);
156
+ box-shadow: var(--card-shadow);
157
+ }
158
+
159
+ /* 表单样式 */
160
+ .form-floating {
161
+ margin-bottom: 1.25rem;
162
+ }
163
+
164
+ .form-floating > .form-control {
165
+ padding: 1rem 1rem;
166
+ height: calc(3.5rem + 2px);
167
+ border-radius: var(--border-radius-lg);
168
+ border: 1px solid var(--neutral-200);
169
+ font-size: 0.95rem;
170
+ transition: var(--transition-base);
171
+ }
172
+
173
+ .form-floating > .form-control:focus {
174
+ border-color: var(--secondary-color);
175
+ box-shadow: 0 0 0 3px rgba(74, 108, 253, 0.1);
176
+ }
177
+
178
+ .form-floating > label {
179
+ padding: 1rem;
180
+ color: var(--neutral-500);
181
+ }
182
+
183
+ .form-check {
184
+ display: flex;
185
+ align-items: center;
186
+ margin-bottom: 1.5rem;
187
+ }
188
+
189
+ .form-check-input {
190
+ width: 1.25em;
191
+ height: 1.25em;
192
+ margin-right: 0.75rem;
193
+ background-color: white;
194
+ border: 1px solid var(--neutral-300);
195
+ border-radius: 0.25em;
196
+ transition: all 0.15s ease-in-out;
197
+ }
198
+
199
+ .form-check-input:checked {
200
+ background-color: var(--secondary-color);
201
+ border-color: var(--secondary-color);
202
+ }
203
+
204
+ .form-check-label {
205
+ color: var(--neutral-700);
206
+ font-size: 0.95rem;
207
+ }
208
+
209
+ /* 按钮样式 */
210
+ .btn-primary {
211
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
212
+ border: none;
213
+ width: 100%;
214
+ padding: 0.85rem;
215
+ font-weight: 500;
216
+ border-radius: var(--border-radius-lg);
217
+ transition: var(--transition-base);
218
+ }
219
+
220
+ .btn-primary:hover, .btn-primary:focus {
221
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
222
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.3);
223
+ transform: translateY(-1px);
224
+ }
225
+
226
+ .btn-outline-primary {
227
+ color: var(--secondary-color);
228
+ border-color: var(--secondary-color);
229
+ background-color: transparent;
230
+ }
231
+
232
+ .btn-outline-primary:hover, .btn-outline-primary:focus {
233
+ background-color: var(--secondary-color);
234
+ border-color: var(--secondary-color);
235
+ color: white;
236
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2);
237
+ }
238
+
239
+ /* 学生说明部分 */
240
+ .student-instructions {
241
+ margin-top: 1.5rem;
242
+ padding: 1.25rem;
243
+ background-color: var(--neutral-50);
244
+ border-radius: var(--border-radius-lg);
245
+ border: 1px solid var(--neutral-200);
246
+ font-size: 0.95rem;
247
+ color: var(--neutral-700);
248
+ transition: var(--transition-smooth);
249
+ }
250
+
251
+ .student-instructions p {
252
+ margin-bottom: 0.75rem;
253
+ }
254
+
255
+ .student-instructions p:last-child {
256
+ margin-bottom: 0;
257
+ }
258
+
259
+ .student-instructions strong {
260
+ color: var(--neutral-900);
261
+ }
262
+
263
+ /* 页脚样式 */
264
+ .login-footer {
265
+ text-align: center;
266
+ margin-top: 2rem;
267
+ color: var(--neutral-500);
268
+ font-size: 0.85rem;
269
+ }
270
+
271
+ /* 动画效果 */
272
+ @keyframes fadeIn {
273
+ from { opacity: 0; transform: translateY(8px); }
274
+ to { opacity: 1; transform: translateY(0); }
275
+ }
276
+
277
+ .fade-in {
278
+ opacity: 0;
279
+ animation: fadeIn 0.4s ease-out forwards;
280
+ }
281
+
282
+ /* 输入组样式 */
283
+ .input-group {
284
+ position: relative;
285
+ }
286
+
287
+ .input-group .form-control {
288
+ padding-right: 3.5rem;
289
+ border-radius: var(--border-radius-lg);
290
+ }
291
+
292
+ .input-group .btn {
293
+ position: absolute;
294
+ right: 0;
295
+ top: 0;
296
+ bottom: 0;
297
+ z-index: 5;
298
+ border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
299
+ color: var(--neutral-700);
300
+ background-color: var(--neutral-100);
301
+ border: 1px solid var(--neutral-200);
302
+ border-left: none;
303
+ transition: var(--transition-base);
304
+ }
305
+
306
+ .input-group .btn:hover, .input-group .btn:focus {
307
+ background-color: var(--neutral-200);
308
+ }
309
+ </style>
310
+ </head>
311
+ <body>
312
+ <div class="login-container fade-in">
313
+ <div class="login-header">
314
+ <h1>AI 助教平台</h1>
315
+ <p id="login-subtitle">教师端登录</p>
316
+ </div>
317
+
318
+ <div class="user-type-switch">
319
+ <div class="btn-group" role="group" id="user-type-buttons">
320
+ <button type="button" class="btn active" id="teacher-button">教师</button>
321
+ <button type="button" class="btn" id="student-button">学生</button>
322
+ </div>
323
+ </div>
324
+
325
+ <form id="login-form">
326
+ <div class="form-floating">
327
+ <input type="text" class="form-control" id="username" placeholder="用户名" required>
328
+ <label for="username">用户名</label>
329
+ </div>
330
+
331
+ <div class="form-floating">
332
+ <input type="password" class="form-control" id="password" placeholder="密码" required>
333
+ <label for="password">密码</label>
334
+ </div>
335
+
336
+ <div class="form-check">
337
+ <input class="form-check-input" type="checkbox" id="remember-me">
338
+ <label class="form-check-label" for="remember-me">
339
+ 记住我
340
+ </label>
341
+ </div>
342
+
343
+ <div id="student-instructions" class="student-instructions" style="display: none;">
344
+ <p><strong>学生登录说明:</strong></p>
345
+ <p>您也可以直接使用教师分享的链接访问AI助手,无需登录。</p>
346
+ <div class="input-group mt-3">
347
+ <input type="text" class="form-control" id="access-token" placeholder="输入访问令牌">
348
+ <button class="btn" type="button" id="access-button">
349
+ <i class="bi bi-arrow-right"></i>
350
+ </button>
351
+ </div>
352
+ </div>
353
+
354
+ <button type="submit" class="btn btn-primary mt-3">
355
+ <i class="bi bi-box-arrow-in-right me-2"></i>登录
356
+ </button>
357
+ </form>
358
+
359
+ <div class="login-footer">
360
+ © 2025 教育AI助手平台 版权所有
361
+ </div>
362
+ </div>
363
+
364
+ <script>
365
+ // 硬编码用户
366
+ const users = {
367
+ teachers: [
368
+ { username: 'teacher', password: '123456', name: '李志刚' },
369
+ { username: 'admin', password: 'admin123', name: '管理员' }
370
+ ],
371
+ students: [
372
+ { username: 'student1', password: '123456', name: '张三' },
373
+ { username: 'student2', password: '123456', name: '李四' }
374
+ ]
375
+ };
376
+
377
+ // DOM元素
378
+ const teacherButton = document.getElementById('teacher-button');
379
+ const studentButton = document.getElementById('student-button');
380
+ const loginSubtitle = document.getElementById('login-subtitle');
381
+ const studentInstructions = document.getElementById('student-instructions');
382
+ const loginForm = document.getElementById('login-form');
383
+ const accessButton = document.getElementById('access-button');
384
+
385
+ // 切换用户类型
386
+ teacherButton.addEventListener('click', function() {
387
+ teacherButton.classList.add('active');
388
+ studentButton.classList.remove('active');
389
+ loginSubtitle.textContent = '教师端登录';
390
+ studentInstructions.style.display = 'none';
391
+ });
392
+
393
+ studentButton.addEventListener('click', function() {
394
+ studentButton.classList.add('active');
395
+ teacherButton.classList.remove('active');
396
+ loginSubtitle.textContent = '学生端登录';
397
+ studentInstructions.style.display = 'block';
398
+ });
399
+
400
+ // 处理登录表单提交
401
+ // 处理登录表单提交
402
+ loginForm.addEventListener('submit', async function(e) {
403
+ e.preventDefault();
404
+
405
+ const username = document.getElementById('username').value;
406
+ const password = document.getElementById('password').value;
407
+ const isTeacher = teacherButton.classList.contains('active');
408
+
409
+ try {
410
+ const response = await fetch('/api/auth/login', {
411
+ method: 'POST',
412
+ headers: {
413
+ 'Content-Type': 'application/json'
414
+ },
415
+ body: JSON.stringify({
416
+ username: username,
417
+ password: password,
418
+ type: isTeacher ? 'teacher' : 'student'
419
+ })
420
+ });
421
+
422
+ const data = await response.json();
423
+
424
+ if (data.success) {
425
+ // 登录成功,重定向到相应页面
426
+ window.location.href = isTeacher ? '/index.html' : '/student_portal.html';
427
+ } else {
428
+ showAlert('用户名或密码错误', 'danger');
429
+ }
430
+ } catch (error) {
431
+ console.error('登录失败:', error);
432
+ showAlert('登录请求失败,请重试', 'danger');
433
+ }
434
+ });
435
+ // 处理访问令牌
436
+ accessButton.addEventListener('click', function() {
437
+ const token = document.getElementById('access-token').value.trim();
438
+
439
+ if (token) {
440
+ // 简单验证令牌格式
441
+ if (token.length >= 32) {
442
+ // 这里应该向服务器验证令牌有效性,这里简化处理
443
+ // 直接从URL中提取agent_id (假设格式为: [agent_id]?token=[token])
444
+ if (token.includes('?token=')) {
445
+ window.location.href = '/student/' + token;
446
+ } else {
447
+ // 假设这是一个纯token,需要输入Agent ID
448
+ const agentId = prompt('请输入Agent ID');
449
+ if (agentId) {
450
+ window.location.href = `/student/${agentId}?token=${token}`;
451
+ }
452
+ }
453
+ } else {
454
+ showAlert('无效的访问令牌格式', 'warning');
455
+ }
456
+ } else {
457
+ showAlert('请输入访问令牌', 'warning');
458
+ }
459
+ });
460
+
461
+ // 显示提示信息
462
+ function showAlert(message, type) {
463
+ // 移除现有的提示
464
+ const existingAlert = document.querySelector('.alert');
465
+ if (existingAlert) {
466
+ existingAlert.remove();
467
+ }
468
+
469
+ // 创建新提示
470
+ const alert = document.createElement('div');
471
+ alert.className = `alert alert-${type} fade-in`;
472
+ alert.role = 'alert';
473
+ alert.style.marginBottom = '1.5rem';
474
+ alert.innerHTML = `
475
+ <i class="bi ${type === 'danger' ? 'bi-exclamation-triangle' : 'bi-info-circle'} me-2"></i>
476
+ ${message}
477
+ `;
478
+
479
+ // 添加到表单上方
480
+ loginForm.insertAdjacentElement('beforebegin', alert);
481
+
482
+ // 3秒后自动移除
483
+ setTimeout(() => {
484
+ alert.style.opacity = '0';
485
+ setTimeout(() => alert.remove(), 300);
486
+ }, 3000);
487
+ }
488
+
489
+ // 页面加载时检查是否已经登录
490
+ // window.addEventListener('DOMContentLoaded', function() {
491
+ // const currentUser = localStorage.getItem('currentUser');
492
+
493
+ // if (currentUser) {
494
+ // const userData = JSON.parse(currentUser);
495
+
496
+ // if (userData.type === 'teacher') {
497
+ // window.location.href = '/index.html';
498
+ // } else if (userData.type === 'student') {
499
+ // window.location.href = '/student_portal.html';
500
+ // }
501
+ // }
502
+ // });
503
+ </script>
504
+ </body>
505
+ </html>
templates/student.html CHANGED
The diff for this file is too large to render. See raw diff
 
templates/student_portal.html ADDED
@@ -0,0 +1,1005 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
7
+ <title>学生端门户 - 教育AI助手平台</title>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
12
+
13
+ :root {
14
+ /* 优雅的配色方案 */
15
+ --primary-color: #0f2d49;
16
+ --primary-light: #234a70;
17
+ --secondary-color: #4a6cfd;
18
+ --secondary-light: #7b91ff;
19
+ --tertiary-color: #f7f9fe;
20
+ --success-color: #10b981;
21
+ --success-light: rgba(16, 185, 129, 0.1);
22
+ --warning-color: #f59e0b;
23
+ --warning-light: rgba(245, 158, 11, 0.1);
24
+ --info-color: #0ea5e9;
25
+ --info-light: rgba(14, 165, 233, 0.1);
26
+ --danger-color: #ef4444;
27
+ --danger-light: rgba(239, 68, 68, 0.1);
28
+ --neutral-50: #f9fafb;
29
+ --neutral-100: #f3f4f6;
30
+ --neutral-200: #e5e7eb;
31
+ --neutral-300: #d1d5db;
32
+ --neutral-400: #9ca3af;
33
+ --neutral-500: #6b7280;
34
+ --neutral-600: #4b5563;
35
+ --neutral-700: #374151;
36
+ --neutral-800: #1f2937;
37
+ --neutral-900: #111827;
38
+
39
+ /* 样式变量 */
40
+ --border-radius-sm: 0.25rem;
41
+ --border-radius: 0.375rem;
42
+ --border-radius-lg: 0.5rem;
43
+ --border-radius-xl: 0.75rem;
44
+ --border-radius-2xl: 1rem;
45
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
46
+ --card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1);
47
+ --card-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
48
+ --transition-base: all 0.2s ease-in-out;
49
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
50
+ --font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
51
+ }
52
+
53
+ /* 基础样式 */
54
+ body {
55
+ font-family: var(--font-family);
56
+ background-color: var(--neutral-50);
57
+ color: var(--neutral-800);
58
+ margin: 0;
59
+ padding: 0;
60
+ min-height: 100vh;
61
+ display: flex;
62
+ flex-direction: column;
63
+ -webkit-font-smoothing: antialiased;
64
+ -moz-osx-font-smoothing: grayscale;
65
+ }
66
+
67
+ h1, h2, h3, h4, h5, h6 {
68
+ font-weight: 600;
69
+ color: var(--neutral-900);
70
+ }
71
+
72
+ .text-gradient {
73
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
74
+ -webkit-background-clip: text;
75
+ -webkit-text-fill-color: transparent;
76
+ background-clip: text;
77
+ color: transparent;
78
+ }
79
+
80
+ /* 滚动条样式 */
81
+ ::-webkit-scrollbar {
82
+ width: 6px;
83
+ height: 6px;
84
+ }
85
+
86
+ ::-webkit-scrollbar-track {
87
+ background: var(--neutral-100);
88
+ border-radius: 10px;
89
+ }
90
+
91
+ ::-webkit-scrollbar-thumb {
92
+ background: var(--neutral-300);
93
+ border-radius: 10px;
94
+ }
95
+
96
+ ::-webkit-scrollbar-thumb:hover {
97
+ background: var(--neutral-400);
98
+ }
99
+
100
+ /* 头部样式 */
101
+ .header {
102
+ background-color: white;
103
+ border-bottom: 1px solid var(--neutral-200);
104
+ padding: 1rem 1.5rem;
105
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
106
+ position: sticky;
107
+ top: 0;
108
+ z-index: 100;
109
+ }
110
+
111
+ .header-content {
112
+ max-width: 1400px;
113
+ margin: 0 auto;
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: center;
117
+ }
118
+
119
+ .header h1 {
120
+ margin: 0;
121
+ font-size: 1.5rem;
122
+ font-weight: 700;
123
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
124
+ -webkit-background-clip: text;
125
+ -webkit-text-fill-color: transparent;
126
+ letter-spacing: -0.01em;
127
+ }
128
+
129
+ .user-info {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 0.75rem;
133
+ }
134
+
135
+ .user-info .badge {
136
+ font-size: 0.85rem;
137
+ padding: 0.4rem 0.75rem;
138
+ border-radius: var(--border-radius-lg);
139
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
140
+ color: white;
141
+ font-weight: 500;
142
+ }
143
+
144
+ .user-info button {
145
+ color: var(--neutral-600);
146
+ text-decoration: none;
147
+ font-size: 0.9rem;
148
+ transition: var(--transition-base);
149
+ }
150
+
151
+ .user-info button:hover {
152
+ color: var(--secondary-color);
153
+ }
154
+
155
+ /* 主容器样式 */
156
+ .main-container {
157
+ flex: 1;
158
+ max-width: 1400px;
159
+ margin: 2rem auto;
160
+ padding: 0 1.5rem;
161
+ width: 100%;
162
+ box-sizing: border-box;
163
+ }
164
+
165
+ /* 欢迎消息样式 */
166
+ .welcome-message {
167
+ background-color: white;
168
+ border-radius: var(--border-radius-xl);
169
+ box-shadow: var(--card-shadow);
170
+ padding: 2rem;
171
+ margin-bottom: 2.5rem;
172
+ position: relative;
173
+ overflow: hidden;
174
+ border: none;
175
+ transition: var(--transition-smooth);
176
+ }
177
+
178
+ .welcome-message:hover {
179
+ box-shadow: var(--card-shadow-hover);
180
+ transform: translateY(-3px);
181
+ }
182
+
183
+ .welcome-message::before {
184
+ content: '';
185
+ position: absolute;
186
+ top: 0;
187
+ bottom: 0;
188
+ left: 0;
189
+ width: 4px;
190
+ background: linear-gradient(to bottom, var(--info-color), var(--secondary-light));
191
+ border-radius: 4px 0 0 4px;
192
+ }
193
+
194
+ .welcome-message h2 {
195
+ margin-top: 0;
196
+ margin-bottom: 1rem;
197
+ font-size: 1.6rem;
198
+ font-weight: 700;
199
+ color: var(--primary-color);
200
+ }
201
+
202
+ .welcome-message p {
203
+ color: var(--neutral-600);
204
+ margin-bottom: 0.75rem;
205
+ font-size: 0.95rem;
206
+ line-height: 1.5;
207
+ }
208
+
209
+ .welcome-message p:last-child {
210
+ margin-bottom: 0;
211
+ }
212
+
213
+ /* 令牌输入样式 */
214
+ .token-input {
215
+ background-color: white;
216
+ border-radius: var(--border-radius-xl);
217
+ box-shadow: var(--card-shadow);
218
+ padding: 2rem;
219
+ margin-bottom: 2.5rem;
220
+ position: relative;
221
+ overflow: hidden;
222
+ transition: var(--transition-smooth);
223
+ }
224
+
225
+ .token-input:hover {
226
+ box-shadow: var(--card-shadow-hover);
227
+ transform: translateY(-3px);
228
+ }
229
+
230
+ .token-input::before {
231
+ content: '';
232
+ position: absolute;
233
+ top: 0;
234
+ bottom: 0;
235
+ left: 0;
236
+ width: 4px;
237
+ background: linear-gradient(to bottom, var(--warning-color), var(--secondary-light));
238
+ border-radius: 4px 0 0 4px;
239
+ }
240
+
241
+ .token-input h3 {
242
+ margin-top: 0;
243
+ margin-bottom: 1rem;
244
+ font-size: 1.35rem;
245
+ font-weight: 600;
246
+ color: var(--neutral-900);
247
+ }
248
+
249
+ .token-input p {
250
+ color: var(--neutral-600);
251
+ margin-bottom: 1.25rem;
252
+ }
253
+
254
+ .token-input .input-group {
255
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
256
+ border-radius: var(--border-radius-lg);
257
+ margin-bottom: 0;
258
+ }
259
+
260
+ .token-input .form-control {
261
+ border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
262
+ border: 1px solid var(--neutral-200);
263
+ padding: 0.75rem 1rem;
264
+ font-size: 0.95rem;
265
+ }
266
+
267
+ .token-input .form-control:focus {
268
+ box-shadow: none;
269
+ border-color: var(--secondary-color);
270
+ }
271
+
272
+ .token-input .btn {
273
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
274
+ color: white;
275
+ border: none;
276
+ border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
277
+ padding: 0.75rem 1.25rem;
278
+ transition: var(--transition-base);
279
+ }
280
+
281
+ .token-input .btn:hover {
282
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
283
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2);
284
+ }
285
+
286
+ /* 标题样式 */
287
+ .section-header {
288
+ margin-bottom: 1.5rem;
289
+ display: flex;
290
+ flex-direction: column;
291
+ }
292
+
293
+ .section-header h2 {
294
+ margin: 0;
295
+ font-size: 1.5rem;
296
+ font-weight: 700;
297
+ color: var(--primary-color);
298
+ margin-bottom: 0.5rem;
299
+ }
300
+
301
+ .section-header p {
302
+ color: var(--neutral-600);
303
+ margin: 0;
304
+ font-size: 0.95rem;
305
+ }
306
+
307
+ /* Agent卡片样式 */
308
+ .agent-card {
309
+ background-color: white;
310
+ border-radius: var(--border-radius-xl);
311
+ margin-bottom: 1.5rem;
312
+ box-shadow: var(--card-shadow);
313
+ overflow: hidden;
314
+ transition: var(--transition-smooth);
315
+ position: relative;
316
+ border: none;
317
+ }
318
+
319
+ .agent-card:hover {
320
+ transform: translateY(-5px);
321
+ box-shadow: var(--card-shadow-hover);
322
+ }
323
+
324
+ .agent-card::before {
325
+ content: '';
326
+ position: absolute;
327
+ top: 0;
328
+ bottom: 0;
329
+ left: 0;
330
+ width: 4px;
331
+ background: linear-gradient(to bottom, var(--secondary-color), var(--secondary-light));
332
+ border-radius: 4px 0 0 4px;
333
+ }
334
+
335
+ .agent-card-header {
336
+ padding: 1.5rem;
337
+ background-color: rgba(74, 108, 253, 0.03);
338
+ border-bottom: 1px solid var(--neutral-100);
339
+ }
340
+
341
+ .agent-card-title {
342
+ margin: 0;
343
+ font-size: 1.25rem;
344
+ font-weight: 600;
345
+ color: var(--primary-color);
346
+ display: flex;
347
+ align-items: center;
348
+ }
349
+
350
+ .agent-card-title i {
351
+ margin-right: 0.75rem;
352
+ font-size: 1.2rem;
353
+ color: var(--secondary-color);
354
+ }
355
+
356
+ .agent-card-content {
357
+ padding: 1.5rem;
358
+ }
359
+
360
+ .agent-card-description {
361
+ margin-bottom: 1.25rem;
362
+ color: var(--neutral-700);
363
+ font-size: 0.95rem;
364
+ line-height: 1.5;
365
+ }
366
+
367
+ .agent-meta {
368
+ display: flex;
369
+ flex-wrap: wrap;
370
+ margin-bottom: 1.25rem;
371
+ gap: 1.5rem;
372
+ }
373
+
374
+ .agent-meta-item {
375
+ display: flex;
376
+ align-items: center;
377
+ font-size: 0.9rem;
378
+ color: var(--neutral-600);
379
+ }
380
+
381
+ .agent-meta-item i {
382
+ margin-right: 0.5rem;
383
+ font-size: 0.95rem;
384
+ color: var(--secondary-color);
385
+ }
386
+
387
+ .agent-tags {
388
+ display: flex;
389
+ flex-wrap: wrap;
390
+ gap: 0.5rem;
391
+ margin-bottom: 1.5rem;
392
+ }
393
+
394
+ .agent-tag {
395
+ display: inline-flex;
396
+ align-items: center;
397
+ padding: 0.35em 0.75em;
398
+ background-color: rgba(74, 108, 253, 0.1);
399
+ color: var(--secondary-color);
400
+ border-radius: 20px;
401
+ font-size: 0.85rem;
402
+ font-weight: 500;
403
+ }
404
+
405
+ .agent-tag i {
406
+ margin-right: 0.4rem;
407
+ }
408
+
409
+ .agent-actions {
410
+ display: flex;
411
+ justify-content: flex-end;
412
+ }
413
+
414
+ .agent-actions .btn {
415
+ padding: 0.65em 1.25em;
416
+ font-weight: 500;
417
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
418
+ border: none;
419
+ color: white;
420
+ border-radius: var(--border-radius-lg);
421
+ transition: var(--transition-base);
422
+ }
423
+
424
+ .agent-actions .btn:hover {
425
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
426
+ transform: translateY(-2px);
427
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2);
428
+ }
429
+
430
+ .agent-actions .btn i {
431
+ margin-right: 0.5rem;
432
+ }
433
+
434
+ /* 活动记录样式 */
435
+ .recent-activity {
436
+ background-color: white;
437
+ border-radius: var(--border-radius-xl);
438
+ box-shadow: var(--card-shadow);
439
+ padding: 1.5rem;
440
+ margin-bottom: 2rem;
441
+ position: relative;
442
+ overflow: hidden;
443
+ transition: var(--transition-smooth);
444
+ }
445
+
446
+ .recent-activity:hover {
447
+ box-shadow: var(--card-shadow-hover);
448
+ transform: translateY(-3px);
449
+ }
450
+
451
+ .recent-activity::before {
452
+ content: '';
453
+ position: absolute;
454
+ top: 0;
455
+ bottom: 0;
456
+ left: 0;
457
+ width: 4px;
458
+ background: linear-gradient(to bottom, var(--success-color), var(--secondary-light));
459
+ border-radius: 4px 0 0 4px;
460
+ }
461
+
462
+ .recent-activity h3 {
463
+ margin-top: 0;
464
+ margin-bottom: 1.5rem;
465
+ font-size: 1.35rem;
466
+ font-weight: 600;
467
+ color: var(--neutral-900);
468
+ }
469
+
470
+ .activity-list {
471
+ list-style: none;
472
+ padding: 0;
473
+ margin: 0;
474
+ }
475
+
476
+ .activity-item {
477
+ display: flex;
478
+ padding: 1rem 0;
479
+ border-bottom: 1px solid var(--neutral-100);
480
+ align-items: center;
481
+ }
482
+
483
+ .activity-item:last-child {
484
+ border-bottom: none;
485
+ padding-bottom: 0;
486
+ }
487
+
488
+ .activity-icon {
489
+ width: 40px;
490
+ height: 40px;
491
+ border-radius: 50%;
492
+ background-color: rgba(74, 108, 253, 0.1);
493
+ display: flex;
494
+ align-items: center;
495
+ justify-content: center;
496
+ margin-right: 1rem;
497
+ flex-shrink: 0;
498
+ }
499
+
500
+ .activity-icon i {
501
+ color: var(--secondary-color);
502
+ font-size: 1.1rem;
503
+ }
504
+
505
+ .activity-content {
506
+ flex: 1;
507
+ }
508
+
509
+ .activity-title {
510
+ margin: 0;
511
+ margin-bottom: 0.35rem;
512
+ font-size: 0.95rem;
513
+ font-weight: 500;
514
+ color: var(--neutral-800);
515
+ }
516
+
517
+ .activity-time {
518
+ font-size: 0.85rem;
519
+ color: var(--neutral-500);
520
+ }
521
+
522
+ /* 动画效果 */
523
+ @keyframes fadeIn {
524
+ from { opacity: 0; transform: translateY(10px); }
525
+ to { opacity: 1; transform: translateY(0); }
526
+ }
527
+
528
+ .fade-in {
529
+ opacity: 0;
530
+ animation: fadeIn 0.4s ease-out forwards;
531
+ }
532
+
533
+ .animate-delay-1 { animation-delay: 0.1s; }
534
+ .animate-delay-2 { animation-delay: 0.2s; }
535
+ .animate-delay-3 { animation-delay: 0.3s; }
536
+ .animate-delay-4 { animation-delay: 0.4s; }
537
+ .animate-delay-5 { animation-delay: 0.5s; }
538
+
539
+ /* 响应式样式 */
540
+ @media (max-width: 992px) {
541
+ .main-container {
542
+ padding: 0 1rem;
543
+ }
544
+
545
+ .agent-meta {
546
+ flex-direction: column;
547
+ gap: 0.75rem;
548
+ }
549
+ }
550
+
551
+ @media (max-width: 768px) {
552
+ .header h1 {
553
+ font-size: 1.25rem;
554
+ }
555
+
556
+ .welcome-message h2 {
557
+ font-size: 1.35rem;
558
+ }
559
+
560
+ .agent-card-title {
561
+ font-size: 1.1rem;
562
+ }
563
+
564
+ .section-header h2 {
565
+ font-size: 1.3rem;
566
+ }
567
+
568
+ .recent-activity h3,
569
+ .token-input h3 {
570
+ font-size: 1.2rem;
571
+ }
572
+ }
573
+ </style>
574
+ </head>
575
+ <body>
576
+ <header class="header">
577
+ <div class="header-content">
578
+ <h1>教育AI助手平台 - 学生端</h1>
579
+ <div class="user-info">
580
+ <span class="badge" id="user-name">张三</span>
581
+ <button class="btn btn-sm btn-link text-decoration-none" id="logout-btn">登出</button>
582
+ </div>
583
+ </div>
584
+ </header>
585
+
586
+ <div class="main-container">
587
+ <div class="welcome-message fade-in">
588
+ <h2>欢迎回来,<span id="welcome-name">张三</span>!</h2>
589
+ <p>教育AI助手平台为您提供智能学习辅助工具,帮助您更高效地学习和解决问题。</p>
590
+ <p>下方是您的教师分享给您的AI助手,点击"开始对话"与它们互动。</p>
591
+ </div>
592
+
593
+ <div class="token-input fade-in animate-delay-1">
594
+ <h3>访问新的AI助手</h3>
595
+ <p>输入教师分享的访问令牌,即可使用新的AI助手。</p>
596
+ <div class="input-group">
597
+ <input type="text" class="form-control" id="access-token" placeholder="输入访问令牌">
598
+ <button class="btn" id="access-button">
599
+ <i class="bi bi-arrow-right-circle me-1"></i> 访问
600
+ </button>
601
+ </div>
602
+ </div>
603
+
604
+ <div class="section-header fade-in animate-delay-2">
605
+ <h2>我的AI助手</h2>
606
+ <p>教师分享给您的智能学习助手</p>
607
+ </div>
608
+
609
+ <div class="row" id="agents-container">
610
+ <!-- 代码将在JavaScript中动态生成 -->
611
+ </div>
612
+
613
+ <div class="recent-activity fade-in animate-delay-4">
614
+ <h3>最近活动</h3>
615
+ <ul class="activity-list" id="activity-list">
616
+ <!-- 代码将在JavaScript中动态生成 -->
617
+ </ul>
618
+ </div>
619
+ </div>
620
+
621
+ <script>
622
+ // DOM元素
623
+ const userNameElement = document.getElementById('user-name');
624
+ const welcomeNameElement = document.getElementById('welcome-name');
625
+ const logoutBtn = document.getElementById('logout-btn');
626
+ const accessButton = document.getElementById('access-button');
627
+ const agentsContainer = document.getElementById('agents-container');
628
+ const activityList = document.getElementById('activity-list');
629
+
630
+ // 页面加载函数
631
+ document.addEventListener('DOMContentLoaded', function() {
632
+ // 检查登录状态
633
+ checkAuthStatus();
634
+
635
+ // 加载用户的AI助手列表
636
+ loadAgents();
637
+
638
+ // 初始化登出按钮
639
+ logoutBtn.addEventListener('click', logout);
640
+
641
+ // 初始化访问令牌按钮
642
+ accessButton.addEventListener('click', accessWithToken);
643
+ });
644
+
645
+ // 检查认证状态
646
+ async function checkAuthStatus() {
647
+ try {
648
+ const response = await fetch('/api/auth/check');
649
+ const data = await response.json();
650
+
651
+ if (!data.success) {
652
+ // 未登录,跳转到登录页面
653
+ window.location.href = '/login.html';
654
+ return;
655
+ }
656
+
657
+ // 更新用户信息显示
658
+ userNameElement.textContent = data.user.name;
659
+ welcomeNameElement.textContent = data.user.name;
660
+
661
+ // 如果不是学生,跳转到教师端
662
+ if (data.user.type !== 'student') {
663
+ window.location.href = '/index.html';
664
+ }
665
+ } catch (error) {
666
+ console.error('验证登录状态出错:', error);
667
+ window.location.href = '/login.html';
668
+ }
669
+ }
670
+
671
+ // 登出
672
+ async function logout() {
673
+ try {
674
+ const response = await fetch('/api/auth/logout', {
675
+ method: 'POST'
676
+ });
677
+
678
+ const data = await response.json();
679
+
680
+ if (data.success) {
681
+ window.location.href = '/login.html';
682
+ }
683
+ } catch (error) {
684
+ console.error('登出出错:', error);
685
+ alert('登出失败,请重试');
686
+ }
687
+ }
688
+
689
+ // 加载Agent列表
690
+ async function loadAgents() {
691
+ try {
692
+ // 显示加载状态
693
+ agentsContainer.innerHTML = `
694
+ <div class="col-12 text-center py-4">
695
+ <div class="spinner-border text-primary" role="status">
696
+ <span class="visually-hidden">加载中...</span>
697
+ </div>
698
+ <p class="mt-3">正在加载AI助手列表...</p>
699
+ </div>
700
+ `;
701
+
702
+ // 获取Agent列表
703
+ const response = await fetch('/api/student/agents');
704
+ const result = await response.json();
705
+
706
+ if (result.success) {
707
+ const agents = result.agents || [];
708
+
709
+ if (agents.length === 0) {
710
+ agentsContainer.innerHTML = `
711
+ <div class="col-12">
712
+ <div class="alert alert-info">
713
+ <i class="bi bi-info-circle me-2"></i>
714
+ 您还没有可用的AI助手。请向您的教师获取访问令牌。
715
+ </div>
716
+ </div>
717
+ `;
718
+
719
+ // 无Agent时,清空活动列表
720
+ activityList.innerHTML = `
721
+ <li class="text-center py-3 text-muted">
722
+ 暂无活动记录
723
+ </li>
724
+ `;
725
+
726
+ return;
727
+ }
728
+
729
+ // 渲染Agent列表
730
+ agentsContainer.innerHTML = '';
731
+
732
+ agents.forEach(agent => {
733
+ const col = document.createElement('div');
734
+ col.className = 'col-lg-6 col-12 fade-in';
735
+
736
+ // 格式化最后使用时间
737
+ let lastUsedText = '从未使用';
738
+ if (agent.last_used) {
739
+ const lastUsedDate = new Date(agent.last_used * 1000);
740
+ lastUsedText = lastUsedDate.toLocaleString();
741
+ }
742
+
743
+ // 构建插件标签
744
+ let pluginsHtml = '';
745
+ if (agent.plugins && agent.plugins.length > 0) {
746
+ pluginsHtml = '<div class="agent-tags">';
747
+
748
+ agent.plugins.forEach(plugin => {
749
+ let pluginName = '未知插件';
750
+ let pluginIcon = 'puzzle';
751
+
752
+ if (plugin === 'code') {
753
+ pluginName = '代码执行';
754
+ pluginIcon = 'code-square';
755
+ } else if (plugin === 'visualization') {
756
+ pluginName = '3D可视化';
757
+ pluginIcon = 'bar-chart';
758
+ } else if (plugin === 'mindmap') {
759
+ pluginName = '思维导图';
760
+ pluginIcon = 'diagram-3';
761
+ }
762
+
763
+ pluginsHtml += `
764
+ <span class="agent-tag">
765
+ <i class="bi bi-${pluginIcon}"></i> ${pluginName}
766
+ </span>
767
+ `;
768
+ });
769
+
770
+ pluginsHtml += '</div>';
771
+ }
772
+
773
+ // 构建主题和教师标签
774
+ let metaHtml = '<div class="agent-meta">';
775
+
776
+ if (agent.subject) {
777
+ metaHtml += `
778
+ <div class="agent-meta-item">
779
+ <i class="bi bi-book"></i> ${agent.subject}
780
+ </div>
781
+ `;
782
+ }
783
+
784
+ if (agent.instructor) {
785
+ metaHtml += `
786
+ <div class="agent-meta-item">
787
+ <i class="bi bi-person"></i> ${agent.instructor}
788
+ </div>
789
+ `;
790
+ }
791
+
792
+ metaHtml += `
793
+ <div class="agent-meta-item">
794
+ <i class="bi bi-clock"></i> 最后使用: ${lastUsedText}
795
+ </div>
796
+ </div>`;
797
+
798
+ // 构建Agent卡片
799
+ col.innerHTML = `
800
+ <div class="agent-card">
801
+ <div class="agent-card-header">
802
+ <h3 class="agent-card-title">
803
+ <i class="bi bi-robot"></i> ${agent.name}
804
+ </h3>
805
+ </div>
806
+ <div class="agent-card-content">
807
+ <div class="agent-card-description">
808
+ ${agent.description || '暂无描述'}
809
+ </div>
810
+ ${metaHtml}
811
+ ${pluginsHtml}
812
+ <div class="agent-actions">
813
+ <a href="/student/${agent.id}?token=${agent.token}" class="btn">
814
+ <i class="bi bi-chat-dots me-1"></i> 开始对话
815
+ </a>
816
+ </div>
817
+ </div>
818
+ </div>
819
+ `;
820
+
821
+ agentsContainer.appendChild(col);
822
+ });
823
+
824
+ // 生成简单的活动记录(实际中应从API获取)
825
+ loadActivityRecords();
826
+ } else {
827
+ agentsContainer.innerHTML = `
828
+ <div class="col-12">
829
+ <div class="alert alert-danger">
830
+ <i class="bi bi-exclamation-triangle me-2"></i>
831
+ 加载Agent列表失败: ${result.message}
832
+ </div>
833
+ </div>
834
+ `;
835
+
836
+ // 加载失败时,清空活动列表
837
+ activityList.innerHTML = `
838
+ <li class="text-center py-3 text-muted">
839
+ 暂无活动记录
840
+ </li>
841
+ `;
842
+ }
843
+ } catch (error) {
844
+ console.error('加载Agent列表出错:', error);
845
+ agentsContainer.innerHTML = `
846
+ <div class="col-12">
847
+ <div class="alert alert-danger">
848
+ <i class="bi bi-exclamation-triangle me-2"></i>
849
+ 加载Agent列表时发生错误,请刷新页面重试
850
+ </div>
851
+ </div>
852
+ `;
853
+ }
854
+ }
855
+
856
+ // 加载活动记录(实际中应从API获取)
857
+ // 加载活动记录
858
+ async function loadActivityRecords() {
859
+ try {
860
+ // 从API获取活动记录
861
+ const response = await fetch('/api/student/activities');
862
+ const result = await response.json();
863
+
864
+ if (result.success) {
865
+ const activities = result.activities || [];
866
+
867
+ if (activities.length === 0) {
868
+ activityList.innerHTML = `
869
+ <li class="text-center py-3 text-muted">
870
+ 暂无活动记录
871
+ </li>
872
+ `;
873
+ return;
874
+ }
875
+
876
+ // 渲染活动记录
877
+ activityList.innerHTML = '';
878
+
879
+ activities.forEach(activity => {
880
+ let iconClass = 'bi-activity';
881
+
882
+ switch (activity.type) {
883
+ case 'chat':
884
+ iconClass = 'bi-chat-dots';
885
+ break;
886
+ case 'code':
887
+ iconClass = 'bi-code-square';
888
+ break;
889
+ case 'viz':
890
+ iconClass = 'bi-bar-chart';
891
+ break;
892
+ case 'mindmap':
893
+ iconClass = 'bi-diagram-3';
894
+ break;
895
+ }
896
+
897
+ const li = document.createElement('li');
898
+ li.className = 'activity-item';
899
+
900
+ // 如果有agent_id和token,添加链接
901
+ let titleHtml = `<h4 class="activity-title">${activity.title}</h4>`;
902
+ if (activity.agent_id) {
903
+ // 查找对应Agent的token
904
+ const agent = agents.find(a => a.id === activity.agent_id);
905
+ if (agent && agent.token) {
906
+ titleHtml = `
907
+ <h4 class="activity-title">
908
+ <a href="/student/${activity.agent_id}?token=${agent.token}">${activity.title}</a>
909
+ </h4>
910
+ `;
911
+ }
912
+ }
913
+
914
+ li.innerHTML = `
915
+ <div class="activity-icon">
916
+ <i class="bi ${iconClass}"></i>
917
+ </div>
918
+ <div class="activity-content">
919
+ ${titleHtml}
920
+ <div class="activity-time">${activity.time}</div>
921
+ </div>
922
+ `;
923
+
924
+ activityList.appendChild(li);
925
+ });
926
+ } else {
927
+ activityList.innerHTML = `
928
+ <li class="text-center py-3 text-muted">
929
+ <i class="bi bi-exclamation-circle me-2"></i>
930
+ 无法加载活动记录
931
+ </li>
932
+ `;
933
+ }
934
+ } catch (error) {
935
+ console.error('加载活动记录出错:', error);
936
+ activityList.innerHTML = `
937
+ <li class="text-center py-3 text-muted">
938
+ <i class="bi bi-exclamation-circle me-2"></i>
939
+ 加载活动记录时出错
940
+ </li>
941
+ `;
942
+ }
943
+ }
944
+ // 使用令牌访问AI助手
945
+ async function accessWithToken() {
946
+ const token = document.getElementById('access-token').value.trim();
947
+
948
+ if (!token) {
949
+ showMessage('请输入访问令牌', 'warning');
950
+ return;
951
+ }
952
+
953
+ try {
954
+ // 验证令牌有效性
955
+ const response = await fetch('/api/verify_token', {
956
+ method: 'POST',
957
+ headers: {
958
+ 'Content-Type': 'application/json'
959
+ },
960
+ body: JSON.stringify({ token })
961
+ });
962
+
963
+ const result = await response.json();
964
+
965
+ if (result.success) {
966
+ // 令牌有效,重定向到Agent页面
967
+ const agent = result.agent;
968
+ window.location.href = `/student/${agent.id}?token=${token}`;
969
+ } else {
970
+ showMessage(result.message || '无效的访问令牌', 'danger');
971
+ }
972
+ } catch (error) {
973
+ console.error('验证令牌出错:', error);
974
+ showMessage('验证令牌时发生错误,请重试', 'danger');
975
+ }
976
+ }
977
+
978
+ // 显示提示消息
979
+ function showMessage(message, type) {
980
+ // 移除现有的提示
981
+ const existingAlert = document.querySelector('.token-input .alert');
982
+ if (existingAlert) {
983
+ existingAlert.remove();
984
+ }
985
+
986
+ // 创建新提示
987
+ const alertDiv = document.createElement('div');
988
+ alertDiv.className = `alert alert-${type} mt-3`;
989
+ alertDiv.innerHTML = `
990
+ <i class="bi ${type === 'danger' ? 'bi-exclamation-triangle' : 'bi-info-circle'} me-2"></i>
991
+ ${message}
992
+ `;
993
+
994
+ // 添加到令牌输入区域
995
+ document.querySelector('.token-input .input-group').insertAdjacentElement('afterend', alertDiv);
996
+
997
+ // 3秒后自动移除
998
+ setTimeout(() => {
999
+ alertDiv.style.opacity = '0';
1000
+ setTimeout(() => alertDiv.remove(), 300);
1001
+ }, 3000);
1002
+ }
1003
+ </script>
1004
+ </body>
1005
+ </html>
templates/token_verification.html ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
7
+ <title>访问验证 - 教育AI助手平台</title>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
12
+
13
+ :root {
14
+ /* 优雅的配色方案 */
15
+ --primary-color: #0f2d49;
16
+ --primary-light: #234a70;
17
+ --secondary-color: #4a6cfd;
18
+ --secondary-light: #7b91ff;
19
+ --tertiary-color: #f7f9fe;
20
+ --success-color: #10b981;
21
+ --success-light: rgba(16, 185, 129, 0.1);
22
+ --warning-color: #f59e0b;
23
+ --warning-light: rgba(245, 158, 11, 0.1);
24
+ --info-color: #0ea5e9;
25
+ --info-light: rgba(14, 165, 233, 0.1);
26
+ --danger-color: #ef4444;
27
+ --danger-light: rgba(239, 68, 68, 0.1);
28
+ --neutral-50: #f9fafb;
29
+ --neutral-100: #f3f4f6;
30
+ --neutral-200: #e5e7eb;
31
+ --neutral-300: #d1d5db;
32
+ --neutral-400: #9ca3af;
33
+ --neutral-500: #6b7280;
34
+ --neutral-600: #4b5563;
35
+ --neutral-700: #374151;
36
+ --neutral-800: #1f2937;
37
+ --neutral-900: #111827;
38
+
39
+ /* 样式变量 */
40
+ --border-radius-sm: 0.25rem;
41
+ --border-radius: 0.375rem;
42
+ --border-radius-lg: 0.5rem;
43
+ --border-radius-xl: 0.75rem;
44
+ --border-radius-2xl: 1rem;
45
+ --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
46
+ --card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1);
47
+ --card-shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
48
+ --transition-base: all 0.2s ease-in-out;
49
+ --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
50
+ --font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif;
51
+ }
52
+
53
+ /* 基础样式 */
54
+ body {
55
+ font-family: var(--font-family);
56
+ background-color: var(--neutral-50);
57
+ color: var(--neutral-800);
58
+ margin: 0;
59
+ padding: 0;
60
+ min-height: 100vh;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ -webkit-font-smoothing: antialiased;
65
+ -moz-osx-font-smoothing: grayscale;
66
+ }
67
+
68
+ h1, h2, h3, h4, h5, h6 {
69
+ font-weight: 600;
70
+ color: var(--neutral-900);
71
+ }
72
+
73
+ .text-gradient {
74
+ background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light));
75
+ -webkit-background-clip: text;
76
+ -webkit-text-fill-color: transparent;
77
+ background-clip: text;
78
+ color: transparent;
79
+ }
80
+
81
+ /* 验证容器样式 */
82
+ .verification-container {
83
+ width: 100%;
84
+ max-width: 500px;
85
+ padding: 2.5rem;
86
+ background-color: white;
87
+ border-radius: var(--border-radius-xl);
88
+ box-shadow: var(--card-shadow-lg);
89
+ transition: var(--transition-smooth);
90
+ position: relative;
91
+ overflow: hidden;
92
+ }
93
+
94
+ .verification-container:hover {
95
+ box-shadow: var(--card-shadow-hover);
96
+ }
97
+
98
+ .verification-container::before {
99
+ content: '';
100
+ position: absolute;
101
+ top: 0;
102
+ left: 0;
103
+ width: 100%;
104
+ height: 4px;
105
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
106
+ border-radius: 4px 4px 0 0;
107
+ }
108
+
109
+ .verification-header {
110
+ text-align: center;
111
+ margin-bottom: 2rem;
112
+ }
113
+
114
+ .verification-header h1 {
115
+ font-size: 1.75rem;
116
+ font-weight: 700;
117
+ margin-bottom: 0.5rem;
118
+ background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));
119
+ -webkit-background-clip: text;
120
+ -webkit-text-fill-color: transparent;
121
+ letter-spacing: -0.01em;
122
+ }
123
+
124
+ .verification-header p {
125
+ color: var(--neutral-600);
126
+ margin-bottom: 0;
127
+ font-size: 0.95rem;
128
+ }
129
+
130
+ /* 加载状态 */
131
+ .loading-container {
132
+ text-align: center;
133
+ margin-bottom: 1.5rem;
134
+ }
135
+
136
+ .spinner-border {
137
+ width: 3rem;
138
+ height: 3rem;
139
+ color: var(--secondary-color);
140
+ margin-bottom: 1.5rem;
141
+ }
142
+
143
+ .loading-container h2 {
144
+ margin-bottom: 0.75rem;
145
+ font-size: 1.4rem;
146
+ color: var(--primary-color);
147
+ }
148
+
149
+ .loading-container p {
150
+ color: var(--neutral-600);
151
+ font-size: 0.95rem;
152
+ }
153
+
154
+ /* Agent信息卡片 */
155
+ .agent-info {
156
+ padding: 1.5rem;
157
+ background-color: var(--neutral-50);
158
+ border-radius: var(--border-radius-lg);
159
+ margin-bottom: 1.75rem;
160
+ border: 1px solid var(--neutral-200);
161
+ position: relative;
162
+ overflow: hidden;
163
+ transition: var(--transition-base);
164
+ }
165
+
166
+ .agent-info:hover {
167
+ border-color: var(--secondary-color);
168
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.1);
169
+ }
170
+
171
+ .agent-info::before {
172
+ content: '';
173
+ position: absolute;
174
+ top: 0;
175
+ bottom: 0;
176
+ left: 0;
177
+ width: 4px;
178
+ background: linear-gradient(to bottom, var(--secondary-color), var(--secondary-light));
179
+ border-radius: 4px 0 0 4px;
180
+ }
181
+
182
+ .agent-title {
183
+ font-size: 1.25rem;
184
+ font-weight: 600;
185
+ margin-bottom: 0.75rem;
186
+ color: var(--primary-color);
187
+ display: flex;
188
+ align-items: center;
189
+ }
190
+
191
+ .agent-title i {
192
+ margin-right: 0.75rem;
193
+ font-size: 1.1rem;
194
+ color: var(--secondary-color);
195
+ }
196
+
197
+ .agent-description {
198
+ color: var(--neutral-700);
199
+ margin-bottom: 1rem;
200
+ font-size: 0.95rem;
201
+ line-height: 1.5;
202
+ }
203
+
204
+ .agent-meta {
205
+ display: flex;
206
+ flex-wrap: wrap;
207
+ gap: 1rem;
208
+ font-size: 0.9rem;
209
+ color: var(--neutral-600);
210
+ }
211
+
212
+ .agent-meta-item {
213
+ display: flex;
214
+ align-items: center;
215
+ }
216
+
217
+ .agent-meta-item i {
218
+ margin-right: 0.5rem;
219
+ font-size: 0.95rem;
220
+ color: var(--secondary-color);
221
+ }
222
+
223
+ /* 验证操作按钮 */
224
+ .verification-actions {
225
+ margin-bottom: 1.5rem;
226
+ }
227
+
228
+ .btn {
229
+ font-weight: 500;
230
+ padding: 0.75rem 1.25rem;
231
+ border-radius: var(--border-radius-lg);
232
+ transition: var(--transition-base);
233
+ }
234
+
235
+ .btn-primary {
236
+ background: linear-gradient(to right, var(--secondary-color), var(--secondary-light));
237
+ border: none;
238
+ color: white;
239
+ }
240
+
241
+ .btn-primary:hover, .btn-primary:focus {
242
+ background: linear-gradient(to right, var(--secondary-light), var(--secondary-color));
243
+ box-shadow: 0 4px 10px rgba(74, 108, 253, 0.3);
244
+ transform: translateY(-2px);
245
+ }
246
+
247
+ .btn-outline-secondary {
248
+ color: var(--neutral-700);
249
+ border-color: var(--neutral-300);
250
+ background-color: white;
251
+ }
252
+
253
+ .btn-outline-secondary:hover, .btn-outline-secondary:focus {
254
+ background-color: var(--neutral-100);
255
+ border-color: var(--neutral-400);
256
+ color: var(--neutral-900);
257
+ }
258
+
259
+ /* 过期/错误状态 */
260
+ .expired-container {
261
+ text-align: center;
262
+ color: var(--danger-color);
263
+ margin-bottom: 1.5rem;
264
+ }
265
+
266
+ .expired-container i {
267
+ font-size: 3rem;
268
+ margin-bottom: 1rem;
269
+ display: block;
270
+ }
271
+
272
+ .expired-container h2 {
273
+ margin-bottom: 0.75rem;
274
+ font-size: 1.5rem;
275
+ color: var(--danger-color);
276
+ }
277
+
278
+ .expired-container p {
279
+ color: var(--neutral-700);
280
+ margin-bottom: 1.5rem;
281
+ }
282
+
283
+ /* 页脚样式 */
284
+ .verification-footer {
285
+ text-align: center;
286
+ margin-top: 2rem;
287
+ color: var(--neutral-500);
288
+ font-size: 0.85rem;
289
+ }
290
+
291
+ .verification-footer a {
292
+ color: var(--secondary-color);
293
+ text-decoration: none;
294
+ transition: var(--transition-base);
295
+ }
296
+
297
+ .verification-footer a:hover {
298
+ text-decoration: underline;
299
+ color: var(--secondary-light);
300
+ }
301
+
302
+ /* 动画效果 */
303
+ @keyframes fadeIn {
304
+ from { opacity: 0; transform: translateY(10px); }
305
+ to { opacity: 1; transform: translateY(0); }
306
+ }
307
+
308
+ .fade-in {
309
+ opacity: 0;
310
+ animation: fadeIn 0.4s ease-out forwards;
311
+ }
312
+
313
+ /* 响应式样式 */
314
+ @media (max-width: 576px) {
315
+ .verification-container {
316
+ padding: 1.5rem;
317
+ margin: 0 1rem;
318
+ }
319
+
320
+ .verification-header h1 {
321
+ font-size: 1.5rem;
322
+ }
323
+
324
+ .agent-title {
325
+ font-size: 1.15rem;
326
+ }
327
+
328
+ .agent-meta {
329
+ flex-direction: column;
330
+ gap: 0.5rem;
331
+ }
332
+ }
333
+ </style>
334
+ </head>
335
+ <body>
336
+ <div class="verification-container fade-in" id="main-container">
337
+ <div class="loading-container" id="loading-container">
338
+ <div class="spinner-border" role="status">
339
+ <span class="visually-hidden">验证中...</span>
340
+ </div>
341
+ <h2>正在验证访问权限</h2>
342
+ <p class="text-muted">请稍候,我们正在验证您的访问令牌...</p>
343
+ </div>
344
+
345
+ <div class="verification-header" id="verification-header" style="display: none;">
346
+ <h1>访问 AI 助手</h1>
347
+ <p>您正在使用访问令牌访问以下 AI 助手</p>
348
+ </div>
349
+
350
+ <div class="agent-info" id="agent-info" style="display: none;">
351
+ <!-- 将由JavaScript填充 -->
352
+ </div>
353
+
354
+ <div class="verification-actions" id="verification-actions" style="display: none;">
355
+ <div class="row g-3">
356
+ <div class="col-12">
357
+ <button class="btn btn-primary w-100" id="access-btn">
358
+ <i class="bi bi-robot me-2"></i>开始对话
359
+ </button>
360
+ </div>
361
+ <div class="col-12">
362
+ <button class="btn btn-outline-secondary w-100" id="back-btn">
363
+ <i class="bi bi-arrow-left me-2"></i>返回
364
+ </button>
365
+ </div>
366
+ </div>
367
+ </div>
368
+
369
+ <div class="expired-container" id="expired-container" style="display: none;">
370
+ <i class="bi bi-exclamation-triangle"></i>
371
+ <h2>访问令牌无效</h2>
372
+ <p>您使用的访问令牌无效或已过期,请联系教师获取新的访问令牌。</p>
373
+ <button class="btn btn-outline-secondary mt-3" id="back-btn-expired">
374
+ <i class="bi bi-arrow-left me-2"></i>返回
375
+ </button>
376
+ </div>
377
+
378
+ <div class="verification-footer" id="verification-footer" style="display: none;">
379
+ <p>教育 AI 助手平台 | <a href="/login.html">登录</a></p>
380
+ </div>
381
+ </div>
382
+
383
+ <script>
384
+ // 解析URL参数
385
+ function getUrlParams() {
386
+ const queryString = window.location.search;
387
+ const urlParams = new URLSearchParams(queryString);
388
+ const pathParts = window.location.pathname.split('/');
389
+
390
+ return {
391
+ agentId: pathParts[pathParts.length - 1] || '',
392
+ token: urlParams.get('token') || ''
393
+ };
394
+ }
395
+
396
+ // 页面加载函数
397
+ document.addEventListener('DOMContentLoaded', async function() {
398
+ const { agentId, token } = getUrlParams();
399
+
400
+ if (!token) {
401
+ showExpiredState('未提供访问令牌');
402
+ return;
403
+ }
404
+
405
+ try {
406
+ // 验证令牌
407
+ const response = await fetch('/api/verify_token', {
408
+ method: 'POST',
409
+ headers: {
410
+ 'Content-Type': 'application/json'
411
+ },
412
+ body: JSON.stringify({
413
+ token: token,
414
+ agent_id: agentId
415
+ })
416
+ });
417
+
418
+ const result = await response.json();
419
+
420
+ if (result.success) {
421
+ // 显示Agent信息
422
+ showAgentInfo(result.agent);
423
+ } else {
424
+ showExpiredState(result.message);
425
+ }
426
+ } catch (error) {
427
+ console.error('验证出错:', error);
428
+ showExpiredState('验证过程中出错');
429
+ }
430
+ });
431
+
432
+ // 显示Agent信息
433
+ function showAgentInfo(agent) {
434
+ // 隐藏加载区域
435
+ document.getElementById('loading-container').style.display = 'none';
436
+
437
+ // 显示��证内容
438
+ document.getElementById('verification-header').style.display = 'block';
439
+ document.getElementById('agent-info').style.display = 'block';
440
+ document.getElementById('verification-actions').style.display = 'block';
441
+ document.getElementById('verification-footer').style.display = 'block';
442
+
443
+ // 填充Agent信息
444
+ const agentInfoElement = document.getElementById('agent-info');
445
+
446
+ // 构建主题和教师信息
447
+ let metaHtml = '<div class="agent-meta">';
448
+
449
+ if (agent.subject) {
450
+ metaHtml += `
451
+ <div class="agent-meta-item">
452
+ <i class="bi bi-book"></i>
453
+ ${agent.subject}
454
+ </div>
455
+ `;
456
+ }
457
+
458
+ if (agent.instructor) {
459
+ metaHtml += `
460
+ <div class="agent-meta-item">
461
+ <i class="bi bi-person"></i>
462
+ ${agent.instructor}
463
+ </div>
464
+ `;
465
+ }
466
+
467
+ metaHtml += '</div>';
468
+
469
+ agentInfoElement.innerHTML = `
470
+ <div class="agent-title">
471
+ <i class="bi bi-robot"></i>${agent.name}
472
+ </div>
473
+ <div class="agent-description">
474
+ ${agent.description || '暂无描述'}
475
+ </div>
476
+ ${metaHtml}
477
+ `;
478
+
479
+ // 设置按钮链接
480
+ document.getElementById('access-btn').addEventListener('click', function() {
481
+ const { token } = getUrlParams();
482
+ window.location.href = `/student/${agent.id}?token=${token}`;
483
+ });
484
+
485
+ document.getElementById('back-btn').addEventListener('click', function() {
486
+ // 检查是否从学生门户进入
487
+ const hasHistory = document.referrer.includes('student_portal.html');
488
+ if (hasHistory) {
489
+ window.history.back();
490
+ } else {
491
+ window.location.href = '/student_portal.html';
492
+ }
493
+ });
494
+ }
495
+
496
+ // 显示过期状态
497
+ function showExpiredState(message) {
498
+ // 隐藏加载区域
499
+ document.getElementById('loading-container').style.display = 'none';
500
+
501
+ // 显示过期内容
502
+ document.getElementById('expired-container').style.display = 'block';
503
+ document.getElementById('verification-footer').style.display = 'block';
504
+
505
+ // 更新过期消息
506
+ const expiredContainer = document.getElementById('expired-container');
507
+ expiredContainer.querySelector('p').textContent = message || '您使用的访问令牌无效或已过期,请联系教师获取新的访问令牌。';
508
+
509
+ // 设置返回按钮
510
+ document.getElementById('back-btn-expired').addEventListener('click', function() {
511
+ // 检查是否从学生门户进入
512
+ const hasHistory = document.referrer.includes('student_portal.html');
513
+ if (hasHistory) {
514
+ window.history.back();
515
+ } else {
516
+ window.location.href = '/login.html';
517
+ }
518
+ });
519
+ }
520
+ </script>
521
+ </body>
522
+ </html>