samlax12 commited on
Commit
1625bb7
·
verified ·
1 Parent(s): c9cb09f

Upload 23 files

Browse files
.env ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 基础API配置
2
+ API_KEY=sk-enngzpiipobubtkttbootznwkyamairlpxzcoyngeenpqldd
3
+ BASE_URL=https://api.siliconflow.cn/v1
4
+ EMBEDDING_MODEL=BAAI/bge-m3
5
+ RERANK_MODEL=BAAI/bge-reranker-v2-m3
6
+ VISION_MODEL=Qwen/Qwen2.5-VL-72B-Instruct
7
+
8
+ # Elasticsearch配置
9
+ PASSWORD=zhx123456
10
+
11
+ # 流式文本问答配置
12
+ STREAM_API_KEY=sk-TqiNvIAcOuYkfdjn7fAf0a29E32a4eF49f376b151f075770
13
+ STREAM_BASE_URL=https://api.oaipro.com/v1
14
+ STREAM_MODEL=gpt-4o-mini
15
+ DEFAULT_MODEL=gpt-4o-mini
agents/agent_10f4dc45_1742739896.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "agent_10f4dc45_1742739896",
3
+ "name": "Python助教",
4
+ "description": "可以编写和执行Python代码辅助学习编程",
5
+ "subject": "Python",
6
+ "instructor": "李志刚教授",
7
+ "created_at": 1742739896,
8
+ "plugins": [
9
+ "code",
10
+ "mindmap"
11
+ ],
12
+ "knowledge_bases": [],
13
+ "workflow": {
14
+ "nodes": [],
15
+ "edges": []
16
+ },
17
+ "distributions": [
18
+ {
19
+ "id": "dist_388637",
20
+ "created_at": 1742739902,
21
+ "token": "f733d0fce2394e58b8fd0a21a9e0e83e",
22
+ "expires_at": 0,
23
+ "usage_count": 3
24
+ }
25
+ ],
26
+ "stats": {
27
+ "usage_count": 3,
28
+ "last_used": 1742740060
29
+ }
30
+ }
agents/agent_2fb5faf5_1742706102.json ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "agent_2fb5faf5_1742706102",
3
+ "name": "计算机组成原理助教",
4
+ "description": "会借助所有与计算机组成原理相关的知识库辅助问答,不额外使用多余的插件",
5
+ "subject": "计算机组成原理",
6
+ "instructor": "李志刚教授",
7
+ "created_at": 1742706102,
8
+ "plugins": [],
9
+ "knowledge_bases": [
10
+ "rag_计算机组成原理课程视频",
11
+ "rag_计算机组成原理"
12
+ ],
13
+ "workflow": {
14
+ "edges": [
15
+ {
16
+ "condition": "优先查询视频知识库",
17
+ "id": "edge1",
18
+ "source": "node1",
19
+ "target": "node2"
20
+ },
21
+ {
22
+ "condition": "有结果",
23
+ "id": "edge2",
24
+ "source": "node2",
25
+ "target": "node4"
26
+ },
27
+ {
28
+ "condition": "视频知识库无结果",
29
+ "id": "edge3",
30
+ "source": "node1",
31
+ "target": "node3"
32
+ },
33
+ {
34
+ "id": "edge4",
35
+ "source": "node3",
36
+ "target": "node4"
37
+ }
38
+ ],
39
+ "nodes": [
40
+ {
41
+ "data": {
42
+ "name": "意图识别"
43
+ },
44
+ "id": "node1",
45
+ "type": "intent_recognition"
46
+ },
47
+ {
48
+ "data": {
49
+ "knowledge_base_id": "rag_计算机组成原理课程视频",
50
+ "name": "知识库查询"
51
+ },
52
+ "id": "node2",
53
+ "type": "knowledge_query"
54
+ },
55
+ {
56
+ "data": {
57
+ "knowledge_base_id": "rag_计算机组成原理",
58
+ "name": "知识库查询"
59
+ },
60
+ "id": "node3",
61
+ "type": "knowledge_query"
62
+ },
63
+ {
64
+ "data": {
65
+ "name": "生成回复"
66
+ },
67
+ "id": "node4",
68
+ "type": "generate_response"
69
+ }
70
+ ]
71
+ },
72
+ "distributions": [
73
+ {
74
+ "id": "dist_f273d6",
75
+ "created_at": 1742706113,
76
+ "token": "1fe9aafc609f413fb1de8c92e05e08e7",
77
+ "expires_at": 0,
78
+ "usage_count": 1
79
+ }
80
+ ],
81
+ "stats": {
82
+ "usage_count": 1,
83
+ "last_used": 1742706127
84
+ }
85
+ }
agents/agent_354996b8_1742743575.json ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "agent_354996b8_1742743575",
3
+ "name": "Python助教2",
4
+ "description": "可以主动的调用python插件运行代码",
5
+ "subject": "Python",
6
+ "instructor": "李志刚教授",
7
+ "created_at": 1742743575,
8
+ "plugins": [
9
+ "code"
10
+ ],
11
+ "knowledge_bases": [
12
+ "rag_计算机组成原理课程视频",
13
+ "rag_计算机组成原理",
14
+ "rag_高等数学视频"
15
+ ],
16
+ "workflow": {
17
+ "edges": [
18
+ {
19
+ "condition": "需要知识",
20
+ "id": "edge1",
21
+ "source": "node1",
22
+ "target": "node2"
23
+ },
24
+ {
25
+ "condition": "需要代码执行",
26
+ "id": "edge2",
27
+ "source": "node1",
28
+ "target": "node3"
29
+ },
30
+ {
31
+ "id": "edge3",
32
+ "source": "node2",
33
+ "target": "node4"
34
+ },
35
+ {
36
+ "id": "edge4",
37
+ "source": "node3",
38
+ "target": "node4"
39
+ }
40
+ ],
41
+ "nodes": [
42
+ {
43
+ "data": {
44
+ "name": "意图识别"
45
+ },
46
+ "id": "node1",
47
+ "type": "intent_recognition"
48
+ },
49
+ {
50
+ "data": {
51
+ "knowledge_base_id": "rag_计算机组成原理课程视频",
52
+ "name": "知识库查询"
53
+ },
54
+ "id": "node2",
55
+ "type": "knowledge_query"
56
+ },
57
+ {
58
+ "data": {
59
+ "name": "调用代码执行插件",
60
+ "plugin_id": "code"
61
+ },
62
+ "id": "node3",
63
+ "type": "plugin_call"
64
+ },
65
+ {
66
+ "data": {
67
+ "name": "生成回复"
68
+ },
69
+ "id": "node4",
70
+ "type": "generate_response"
71
+ }
72
+ ]
73
+ },
74
+ "distributions": [
75
+ {
76
+ "id": "dist_1749ca",
77
+ "created_at": 1742743611,
78
+ "token": "db1779829c2b4ce5920be2181574655c",
79
+ "expires_at": 0,
80
+ "usage_count": 0
81
+ },
82
+ {
83
+ "id": "dist_eaa6a1",
84
+ "created_at": 1742955236,
85
+ "token": "6468d351bdc34a30a6ebcfad4d559890",
86
+ "expires_at": 0,
87
+ "usage_count": 2
88
+ },
89
+ {
90
+ "id": "dist_acab19",
91
+ "created_at": 1742956891,
92
+ "token": "30c40751c905427ba3e5afb2f48a1889",
93
+ "expires_at": 0,
94
+ "usage_count": 0
95
+ }
96
+ ],
97
+ "stats": {
98
+ "usage_count": 2,
99
+ "last_used": 1742956769
100
+ }
101
+ }
agents/agent_6a80c7ea_1742729316.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "agent_6a80c7ea_1742729316",
3
+ "name": "高等数学2",
4
+ "description": "高等数学",
5
+ "subject": "高等数学",
6
+ "instructor": "李志刚教授",
7
+ "created_at": 1742729316,
8
+ "plugins": [
9
+ "mindmap"
10
+ ],
11
+ "knowledge_bases": [],
12
+ "workflow": {
13
+ "nodes": [],
14
+ "edges": []
15
+ },
16
+ "distributions": [
17
+ {
18
+ "id": "dist_7d8bdd",
19
+ "created_at": 1742729323,
20
+ "token": "29564d0449e347449e7366eb0dfb5564",
21
+ "expires_at": 0,
22
+ "usage_count": 5
23
+ },
24
+ {
25
+ "id": "dist_de5c84",
26
+ "created_at": 1742733494,
27
+ "token": "dab67c87cccd44c8afdd7de415912e8b",
28
+ "expires_at": 0,
29
+ "usage_count": 1
30
+ },
31
+ {
32
+ "id": "dist_98e524",
33
+ "created_at": 1742733958,
34
+ "token": "2cee854d84e844aa895b0941d71f0c2c",
35
+ "expires_at": 0,
36
+ "usage_count": 1
37
+ },
38
+ {
39
+ "id": "dist_c86df7",
40
+ "created_at": 1742955361,
41
+ "token": "645ff249fba0437c9148a78decae2f7b",
42
+ "expires_at": 0,
43
+ "usage_count": 2
44
+ }
45
+ ],
46
+ "stats": {
47
+ "usage_count": 9,
48
+ "last_used": 1742956513
49
+ }
50
+ }
agents/agent_bff4886f_1742724690.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "agent_bff4886f_1742724690",
3
+ "name": "高等数学",
4
+ "description": "基于本地知识库的高等数学课程助教",
5
+ "subject": "高等数学",
6
+ "instructor": "李志刚教授",
7
+ "created_at": 1742724690,
8
+ "plugins": [
9
+ "visualization"
10
+ ],
11
+ "knowledge_bases": [],
12
+ "workflow": {
13
+ "nodes": [],
14
+ "edges": []
15
+ },
16
+ "distributions": [
17
+ {
18
+ "id": "dist_627b13",
19
+ "created_at": 1742732156,
20
+ "token": "4781dc4214e041b38b1935eb4bb2e592",
21
+ "expires_at": 0,
22
+ "usage_count": 4
23
+ },
24
+ {
25
+ "id": "dist_123907",
26
+ "created_at": 1742733536,
27
+ "token": "84bc8e25700c48f9ab006a33b92a0040",
28
+ "expires_at": 0,
29
+ "usage_count": 1
30
+ },
31
+ {
32
+ "id": "dist_f65e32",
33
+ "created_at": 1742734008,
34
+ "token": "6df809fc2b194a73ac94446979752575",
35
+ "expires_at": 0,
36
+ "usage_count": 1
37
+ },
38
+ {
39
+ "id": "dist_9614d5",
40
+ "created_at": 1742955410,
41
+ "token": "b4f594893f87470680b9ee5a739699e1",
42
+ "expires_at": 0,
43
+ "usage_count": 4
44
+ }
45
+ ],
46
+ "stats": {
47
+ "usage_count": 10,
48
+ "last_used": 1742956846
49
+ }
50
+ }
agents/agent_d4b3238b_1742733873.json ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "id": "agent_d4b3238b_1742733873",
3
+ "name": "计算机组成原理助教2.0",
4
+ "description": "会借助所有与计算机组成原理相关的知识库辅助问答,不额外使用多余的插件",
5
+ "subject": "计算机组成原理",
6
+ "instructor": "李志刚教授",
7
+ "created_at": 1742733873,
8
+ "plugins": [],
9
+ "knowledge_bases": [
10
+ "rag_计算机组成原理课程视频",
11
+ "rag_计算机组成原理"
12
+ ],
13
+ "workflow": {
14
+ "edges": [
15
+ {
16
+ "condition": "需要知识",
17
+ "id": "edge1",
18
+ "source": "node1",
19
+ "target": "node2"
20
+ },
21
+ {
22
+ "id": "edge2",
23
+ "source": "node2",
24
+ "target": "node4"
25
+ },
26
+ {
27
+ "condition": "需要知识",
28
+ "id": "edge3",
29
+ "source": "node1",
30
+ "target": "node3"
31
+ },
32
+ {
33
+ "id": "edge4",
34
+ "source": "node3",
35
+ "target": "node4"
36
+ }
37
+ ],
38
+ "nodes": [
39
+ {
40
+ "data": {
41
+ "name": "意图识别"
42
+ },
43
+ "id": "node1",
44
+ "type": "intent_recognition"
45
+ },
46
+ {
47
+ "data": {
48
+ "knowledge_base_id": "rag_计算机组成原理课程视频",
49
+ "name": "知识库查询"
50
+ },
51
+ "id": "node2",
52
+ "type": "knowledge_query"
53
+ },
54
+ {
55
+ "data": {
56
+ "knowledge_base_id": "rag_计算机组成原理",
57
+ "name": "知识库查询"
58
+ },
59
+ "id": "node3",
60
+ "type": "knowledge_query"
61
+ },
62
+ {
63
+ "data": {
64
+ "name": "生成回复"
65
+ },
66
+ "id": "node4",
67
+ "type": "generate_response"
68
+ }
69
+ ]
70
+ },
71
+ "distributions": [
72
+ {
73
+ "id": "dist_832b92",
74
+ "created_at": 1742733904,
75
+ "token": "9c60e6d2343240018588c6044bfd0531",
76
+ "expires_at": 0,
77
+ "usage_count": 1
78
+ },
79
+ {
80
+ "id": "dist_688ac8",
81
+ "created_at": 1742955257,
82
+ "token": "c8d5092db34f4fe69d6a184eae06330e",
83
+ "expires_at": 0,
84
+ "usage_count": 1
85
+ }
86
+ ],
87
+ "stats": {
88
+ "usage_count": 2,
89
+ "last_used": 1742955313
90
+ }
91
+ }
app.py ADDED
@@ -0,0 +1,517 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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=5000)
config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # config.py
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ # API keys
8
+ API_KEY = os.getenv("API_KEY")
9
+ BASE_URL = os.getenv("BASE_URL")
10
+ STREAM_API_KEY = os.getenv("STREAM_API_KEY")
11
+ STREAM_BASE_URL = os.getenv("STREAM_BASE_URL")
12
+ STREAM_MODEL = os.getenv("STREAM_MODEL")
13
+ DEFAULT_MODEL = os.getenv("DEFAULT_MODEL")
14
+ OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
15
+
16
+ # Knowledge base configuration
17
+ EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3")
18
+ RERANK_MODEL = os.getenv("RERANK_MODEL", "BAAI/bge-reranker-v2-m3")
19
+
20
+ # System configuration
21
+ UPLOAD_FOLDER = "uploads"
22
+ AGENTS_FOLDER = "agents"
23
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
modules/agent_builder/routes.py ADDED
@@ -0,0 +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("http://localhost:5000/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/code_executor/routes.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/code_executor/routes.py
2
+ from flask import Blueprint, request, jsonify
3
+ import sys
4
+ import io
5
+ import threading
6
+ import queue
7
+ import time
8
+ import traceback
9
+ import os
10
+ import re
11
+ from openai import OpenAI
12
+ from config import API_KEY, BASE_URL, OPENAI_MODEL
13
+
14
+ code_executor_bp = Blueprint('code_executor', __name__)
15
+
16
+ # 创建OpenAI客户端
17
+ client = OpenAI(
18
+ api_key=API_KEY,
19
+ base_url=BASE_URL
20
+ )
21
+
22
+ # 执行上下文存储
23
+ execution_contexts = {}
24
+
25
+ class CustomStdin:
26
+ def __init__(self, input_queue):
27
+ self.input_queue = input_queue
28
+ self.buffer = ""
29
+
30
+ def readline(self):
31
+ if not self.buffer:
32
+ self.buffer = self.input_queue.get() + "\n"
33
+
34
+ result = self.buffer
35
+ self.buffer = ""
36
+ return result
37
+
38
+ class InteractiveExecution:
39
+ """管理Python代码的交互式执行"""
40
+ def __init__(self, code):
41
+ self.code = code
42
+ self.context_id = str(time.time())
43
+ self.is_complete = False
44
+ self.is_waiting_for_input = False
45
+ self.stdout_buffer = io.StringIO()
46
+ self.last_read_position = 0
47
+ self.input_queue = queue.Queue()
48
+ self.error = None
49
+ self.thread = None
50
+ self.should_terminate = False
51
+
52
+ def run(self):
53
+ """在单独的线程中启动执行"""
54
+ self.thread = threading.Thread(target=self._execute)
55
+ self.thread.daemon = True
56
+ self.thread.start()
57
+
58
+ # 给执行一点时间开始
59
+ time.sleep(0.1)
60
+ return self.context_id
61
+
62
+ def _execute(self):
63
+ """执行代码,处理标准输入输出"""
64
+ try:
65
+ # 保存原始的stdin/stdout
66
+ orig_stdin = sys.stdin
67
+ orig_stdout = sys.stdout
68
+
69
+ # 创建自定义stdin
70
+ custom_stdin = CustomStdin(self.input_queue)
71
+
72
+ # 重定向stdin和stdout
73
+ sys.stdin = custom_stdin
74
+ sys.stdout = self.stdout_buffer
75
+
76
+ try:
77
+ # 检查终止的函数
78
+ self._last_check_time = 0
79
+
80
+ def check_termination():
81
+ if self.should_terminate:
82
+ raise KeyboardInterrupt("Execution terminated by user")
83
+
84
+ # 设置一个模拟__main__模块的命名空间
85
+ shared_namespace = {
86
+ "__builtins__": __builtins__,
87
+ "_check_termination": check_termination,
88
+ "time": time,
89
+ "__name__": "__main__"
90
+ }
91
+
92
+ # 在这个命名空间中执行用户代码
93
+ try:
94
+ exec(self.code, shared_namespace)
95
+ except KeyboardInterrupt:
96
+ print("\nExecution terminated by user")
97
+
98
+ except Exception as e:
99
+ self.error = {
100
+ "error": str(e),
101
+ "traceback": traceback.format_exc()
102
+ }
103
+
104
+ finally:
105
+ # 恢复原始stdin/stdout
106
+ sys.stdin = orig_stdin
107
+ sys.stdout = orig_stdout
108
+
109
+ # 标记执行完成
110
+ self.is_complete = True
111
+
112
+ except Exception as e:
113
+ self.error = {
114
+ "error": str(e),
115
+ "traceback": traceback.format_exc()
116
+ }
117
+ self.is_complete = True
118
+
119
+ def terminate(self):
120
+ """终止执行"""
121
+ self.should_terminate = True
122
+
123
+ # 如果在等待输入,放入一些内容以解除阻塞
124
+ if self.is_waiting_for_input:
125
+ self.input_queue.put("\n")
126
+
127
+ # 给执行一点时间终止
128
+ time.sleep(0.2)
129
+
130
+ # 标记为完成
131
+ self.is_complete = True
132
+
133
+ return True
134
+
135
+ def provide_input(self, user_input):
136
+ """为运行的代码提供输入"""
137
+ self.input_queue.put(user_input)
138
+ self.is_waiting_for_input = False
139
+ return True
140
+
141
+ def get_output(self):
142
+ """获取stdout缓冲区的当前内容"""
143
+ output = self.stdout_buffer.getvalue()
144
+ return output
145
+
146
+ def get_new_output(self):
147
+ """只获取自上次读取以来的新输出"""
148
+ current_value = self.stdout_buffer.getvalue()
149
+ if self.last_read_position < len(current_value):
150
+ new_output = current_value[self.last_read_position:]
151
+ self.last_read_position = len(current_value)
152
+ return new_output
153
+ return ""
154
+
155
+ @code_executor_bp.route('/generate', methods=['POST'])
156
+ def generate_code():
157
+ """使用AI生成Python代码"""
158
+ try:
159
+ prompt = request.json.get('prompt')
160
+ if not prompt:
161
+ return jsonify({
162
+ "success": False,
163
+ "error": "No prompt provided"
164
+ })
165
+
166
+ # 构建带有适当上下文的提示
167
+ full_prompt = f"""You are a Python programming assistant. Generate Python code based on this requirement:
168
+ {prompt}
169
+
170
+ Provide only the Python code without any explanation or markdown formatting."""
171
+
172
+ # 调用OpenAI API
173
+ response = client.chat.completions.create(
174
+ model=OPENAI_MODEL,
175
+ messages=[{"role": "user", "content": full_prompt}]
176
+ )
177
+
178
+ # 提取和清理代码
179
+ code = response.choices[0].message.content.strip()
180
+
181
+ # 删除Markdown代码块标记(如果存在)
182
+ code = re.sub(r'```python\n', '', code)
183
+ code = re.sub(r'```', '', code)
184
+
185
+ return jsonify({
186
+ "success": True,
187
+ "code": code
188
+ })
189
+
190
+ except Exception as e:
191
+ return jsonify({
192
+ "success": False,
193
+ "error": str(e)
194
+ })
195
+
196
+ @code_executor_bp.route('/execute', methods=['POST'])
197
+ def execute_code():
198
+ """执行Python代码"""
199
+ try:
200
+ code = request.json.get('code')
201
+ if not code:
202
+ return jsonify({
203
+ "success": False,
204
+ "error": "No code provided"
205
+ })
206
+
207
+ # 创建并启动执行
208
+ execution = InteractiveExecution(code)
209
+ context_id = execution.run()
210
+
211
+ # 存储在全局上下文中
212
+ execution_contexts[context_id] = execution
213
+
214
+ # 检查初始状态
215
+ if execution.error:
216
+ # 执行立即失败
217
+ error_info = execution.error
218
+ del execution_contexts[context_id]
219
+
220
+ return jsonify({
221
+ "success": False,
222
+ "error": error_info["error"],
223
+ "traceback": error_info["traceback"]
224
+ })
225
+
226
+ # 获取初始输出
227
+ output = execution.get_output()
228
+ # 更新上次读取位置以标记此输出为已读
229
+ execution.last_read_position = len(output)
230
+
231
+ # 检查是否命中input()调用或完成执行
232
+ if execution.is_complete:
233
+ # 执行完成,不需要输入
234
+ del execution_contexts[context_id]
235
+
236
+ return jsonify({
237
+ "success": True,
238
+ "output": output,
239
+ "needsInput": False
240
+ })
241
+ else:
242
+ # 假设我们正在等待输入
243
+ execution.is_waiting_for_input = True
244
+
245
+ return jsonify({
246
+ "success": True,
247
+ "output": output,
248
+ "needsInput": True,
249
+ "context_id": context_id
250
+ })
251
+
252
+ except Exception as e:
253
+ return jsonify({
254
+ "success": False,
255
+ "error": str(e),
256
+ "traceback": traceback.format_exc()
257
+ })
258
+
259
+ @code_executor_bp.route('/input', methods=['POST'])
260
+ def provide_input():
261
+ """为正在执行的代码提供输入"""
262
+ try:
263
+ user_input = request.json.get('input', '')
264
+ context_id = request.json.get('context_id')
265
+
266
+ if not context_id or context_id not in execution_contexts:
267
+ return jsonify({
268
+ "success": False,
269
+ "error": "Invalid or expired execution context"
270
+ })
271
+
272
+ execution = execution_contexts[context_id]
273
+
274
+ # 提供输入
275
+ execution.provide_input(user_input)
276
+
277
+ # 给一点时间处理
278
+ time.sleep(0.1)
279
+
280
+ # 获取输入后的新输出
281
+ new_output = execution.get_new_output()
282
+
283
+ # 检查执行状态
284
+ if execution.is_complete:
285
+ # 执行已完成
286
+ if execution.error:
287
+ # 有错误
288
+ error_info = execution.error
289
+ del execution_contexts[context_id]
290
+
291
+ return jsonify({
292
+ "success": False,
293
+ "error": error_info["error"],
294
+ "traceback": error_info["traceback"]
295
+ })
296
+ else:
297
+ # 干净执行
298
+ del execution_contexts[context_id]
299
+
300
+ return jsonify({
301
+ "success": True,
302
+ "output": new_output,
303
+ "needsInput": False
304
+ })
305
+ else:
306
+ # 执行仍在运行,假设我们需要更多输入
307
+ execution.is_waiting_for_input = True
308
+
309
+ return jsonify({
310
+ "success": True,
311
+ "output": new_output,
312
+ "needsInput": True
313
+ })
314
+
315
+ except Exception as e:
316
+ return jsonify({
317
+ "success": False,
318
+ "error": str(e),
319
+ "traceback": traceback.format_exc()
320
+ })
321
+
322
+ @code_executor_bp.route('/stop', methods=['POST'])
323
+ def stop_execution():
324
+ """停止执行"""
325
+ try:
326
+ context_id = request.json.get('context_id')
327
+
328
+ if not context_id or context_id not in execution_contexts:
329
+ return jsonify({
330
+ "success": False,
331
+ "error": "Invalid or expired execution context"
332
+ })
333
+
334
+ execution = execution_contexts[context_id]
335
+
336
+ # 终止执行
337
+ execution.terminate()
338
+
339
+ # 获取最终输出
340
+ output = execution.get_output()
341
+
342
+ # 清理
343
+ del execution_contexts[context_id]
344
+
345
+ return jsonify({
346
+ "success": True,
347
+ "output": output,
348
+ "message": "Execution terminated"
349
+ })
350
+
351
+ except Exception as e:
352
+ return jsonify({
353
+ "success": False,
354
+ "error": str(e),
355
+ "traceback": traceback.format_exc()
356
+ })
modules/knowledge_base/generator.py ADDED
@@ -0,0 +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)}"
modules/knowledge_base/processor.py ADDED
@@ -0,0 +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
modules/knowledge_base/reranker.py ADDED
@@ -0,0 +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
modules/knowledge_base/retriever.py ADDED
@@ -0,0 +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
modules/knowledge_base/routes.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/knowledge_base/routes.py
2
+ from flask import Blueprint, request, jsonify
3
+ import os
4
+ import time
5
+ import threading
6
+ import uuid
7
+ from werkzeug.utils import secure_filename
8
+
9
+ # Import existing components
10
+ from modules.knowledge_base.processor import DocumentProcessor
11
+ from modules.knowledge_base.vector_store import VectorStore
12
+ from modules.knowledge_base.retriever import Retriever
13
+ from modules.knowledge_base.reranker import Reranker
14
+
15
+ knowledge_bp = Blueprint('knowledge', __name__)
16
+
17
+ # Initialize components
18
+ doc_processor = DocumentProcessor()
19
+ vector_store = VectorStore()
20
+ retriever = Retriever()
21
+ reranker = Reranker()
22
+
23
+ # Store progress information
24
+ processing_tasks = {}
25
+
26
+ # Upload folder configuration
27
+ UPLOAD_FOLDER = "uploads"
28
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
29
+
30
+ @knowledge_bp.route('/', methods=['GET'])
31
+ def get_all_knowledge():
32
+ """Get all knowledge base information"""
33
+ try:
34
+ indices = retriever.get_all_indices()
35
+ result = []
36
+
37
+ for index in indices:
38
+ display_name = index[4:] if index.startswith('rag_') else index
39
+ files = vector_store.get_files_in_index(index)
40
+ result.append({
41
+ "id": index,
42
+ "name": display_name,
43
+ "files": files,
44
+ "fileCount": len(files)
45
+ })
46
+
47
+ return jsonify({"success": True, "data": result})
48
+ except Exception as e:
49
+ import traceback
50
+ traceback.print_exc()
51
+ return jsonify({"success": False, "message": str(e)}), 500
52
+
53
+ @knowledge_bp.route('/', methods=['POST'])
54
+ def create_knowledge():
55
+ """Create a new knowledge base"""
56
+ try:
57
+ data = request.form
58
+ name = data.get('name')
59
+
60
+ if not name:
61
+ return jsonify({"success": False, "message": "Knowledge base name cannot be empty"}), 400
62
+
63
+ # Check if knowledge base already exists
64
+ indices = retriever.get_all_indices()
65
+ if f"rag_{name}" in indices:
66
+ return jsonify({"success": False, "message": f"Knowledge base '{name}' already exists"}), 400
67
+
68
+ # Process uploaded file
69
+ if 'file' not in request.files:
70
+ return jsonify({"success": False, "message": "No file uploaded"}), 400
71
+
72
+ file = request.files['file']
73
+ if file.filename == '':
74
+ return jsonify({"success": False, "message": "No file selected"}), 400
75
+
76
+ # Save file
77
+ filename = secure_filename(file.filename)
78
+ file_path = os.path.join(UPLOAD_FOLDER, filename)
79
+ file.save(file_path)
80
+
81
+ # Create task ID
82
+ task_id = f"task_{int(time.time())}_{name}"
83
+
84
+ # Initialize task status
85
+ processing_tasks[task_id] = {
86
+ "progress": 0,
87
+ "status": "Starting document processing...",
88
+ "index_name": name,
89
+ "file_path": file_path,
90
+ "error": False,
91
+ "docCount": 0
92
+ }
93
+
94
+ # Process documents in a separate thread
95
+ def process_in_thread():
96
+ try:
97
+ # Update task status
98
+ processing_tasks[task_id]["progress"] = 10
99
+ processing_tasks[task_id]["status"] = "Loading document..."
100
+
101
+ # Process document with progress tracking
102
+ def update_progress(progress, status):
103
+ processing_tasks[task_id]["progress"] = min(95, progress)
104
+ processing_tasks[task_id]["status"] = status
105
+
106
+ # Process the document
107
+ processed_docs = doc_processor.process(file_path, progress_callback=update_progress)
108
+
109
+ # Update task status
110
+ processing_tasks[task_id]["progress"] = 95
111
+ processing_tasks[task_id]["status"] = "Creating vector store..."
112
+ processing_tasks[task_id]["docCount"] = len(processed_docs)
113
+
114
+ # Store vectors
115
+ vector_store.store(processed_docs, f"rag_{name}")
116
+
117
+ # Complete task
118
+ processing_tasks[task_id]["progress"] = 100
119
+ processing_tasks[task_id]["status"] = "Processing complete"
120
+
121
+ except Exception as e:
122
+ # Record error
123
+ processing_tasks[task_id]["error"] = True
124
+ processing_tasks[task_id]["status"] = f"Processing failed: {str(e)}"
125
+ import traceback
126
+ traceback.print_exc()
127
+
128
+ threading.Thread(target=process_in_thread).start()
129
+
130
+ return jsonify({
131
+ "success": True,
132
+ "message": "Started processing document",
133
+ "task_id": task_id
134
+ }), 202
135
+
136
+ except Exception as e:
137
+ import traceback
138
+ traceback.print_exc()
139
+ return jsonify({"success": False, "message": str(e)}), 500
140
+
141
+ @knowledge_bp.route('/progress/<task_id>', methods=['GET'])
142
+ def get_progress(task_id):
143
+ """Get document processing progress"""
144
+ try:
145
+ task_data = processing_tasks.get(task_id, {
146
+ "progress": 0,
147
+ "status": "Task not found",
148
+ "error": True
149
+ })
150
+
151
+ return jsonify({"success": True, "data": task_data})
152
+ except Exception as e:
153
+ import traceback
154
+ traceback.print_exc()
155
+ return jsonify({"success": False, "message": str(e)}), 500
156
+
157
+ @knowledge_bp.route('/<index_id>/documents', methods=['POST'])
158
+ def add_documents(index_id):
159
+ """Add documents to a knowledge base"""
160
+ try:
161
+ # Check if knowledge base exists
162
+ indices = retriever.get_all_indices()
163
+ if index_id not in indices:
164
+ return jsonify({"success": False, "message": "Knowledge base does not exist"}), 404
165
+
166
+ # Process uploaded file
167
+ if 'file' not in request.files:
168
+ return jsonify({"success": False, "message": "No file uploaded"}), 400
169
+
170
+ file = request.files['file']
171
+ if file.filename == '':
172
+ return jsonify({"success": False, "message": "No file selected"}), 400
173
+
174
+ # Save file
175
+ filename = secure_filename(file.filename)
176
+ file_path = os.path.join(UPLOAD_FOLDER, filename)
177
+ file.save(file_path)
178
+
179
+ # Extract knowledge base name from index ID
180
+ kb_name = index_id[4:] if index_id.startswith('rag_') else index_id
181
+
182
+ # Create task ID
183
+ task_id = f"task_{int(time.time())}_{kb_name}_{filename}"
184
+
185
+ # Initialize task status
186
+ processing_tasks[task_id] = {
187
+ "progress": 0,
188
+ "status": "Starting document processing...",
189
+ "index_name": kb_name,
190
+ "file_path": file_path,
191
+ "error": False,
192
+ "docCount": 0
193
+ }
194
+
195
+ # Process documents in a separate thread
196
+ def process_in_thread():
197
+ try:
198
+ # Update task status
199
+ processing_tasks[task_id]["progress"] = 10
200
+ processing_tasks[task_id]["status"] = "Loading document..."
201
+
202
+ # Process document with progress tracking
203
+ def update_progress(progress, status):
204
+ processing_tasks[task_id]["progress"] = min(95, progress)
205
+ processing_tasks[task_id]["status"] = status
206
+
207
+ # Process the document
208
+ processed_docs = doc_processor.process(file_path, progress_callback=update_progress)
209
+
210
+ # Update task status
211
+ processing_tasks[task_id]["progress"] = 95
212
+ processing_tasks[task_id]["status"] = "Creating vector store..."
213
+ processing_tasks[task_id]["docCount"] = len(processed_docs)
214
+
215
+ # Store vectors
216
+ vector_store.store(processed_docs, index_id)
217
+
218
+ # Complete task
219
+ processing_tasks[task_id]["progress"] = 100
220
+ processing_tasks[task_id]["status"] = "Processing complete"
221
+
222
+ except Exception as e:
223
+ # Record error
224
+ processing_tasks[task_id]["error"] = True
225
+ processing_tasks[task_id]["status"] = f"Processing failed: {str(e)}"
226
+ import traceback
227
+ traceback.print_exc()
228
+
229
+ threading.Thread(target=process_in_thread).start()
230
+
231
+ return jsonify({
232
+ "success": True,
233
+ "message": "Started processing document",
234
+ "task_id": task_id
235
+ }), 202
236
+
237
+ except Exception as e:
238
+ import traceback
239
+ traceback.print_exc()
240
+ return jsonify({"success": False, "message": str(e)}), 500
241
+
242
+ @knowledge_bp.route('/<index_id>', methods=['DELETE'])
243
+ def delete_knowledge(index_id):
244
+ """Delete a knowledge base"""
245
+ try:
246
+ result = vector_store.delete_index(index_id)
247
+ if result:
248
+ return jsonify({"success": True, "message": "Knowledge base deleted successfully"})
249
+ else:
250
+ return jsonify({"success": False, "message": "Failed to delete knowledge base"})
251
+ except Exception as e:
252
+ import traceback
253
+ traceback.print_exc()
254
+ return jsonify({"success": False, "message": str(e)}), 500
255
+
256
+ @knowledge_bp.route('/<index_id>/documents/<path:file_name>', methods=['DELETE'])
257
+ def delete_document(index_id, file_name):
258
+ """Delete a document from a knowledge base"""
259
+ try:
260
+ result = vector_store.delete_document(index_id, file_name)
261
+ if result:
262
+ return jsonify({"success": True, "message": "Document deleted successfully"})
263
+ else:
264
+ return jsonify({"success": False, "message": "Failed to delete document"})
265
+ except Exception as e:
266
+ import traceback
267
+ traceback.print_exc()
268
+ return jsonify({"success": False, "message": str(e)}), 500
modules/knowledge_base/vector_store.py ADDED
@@ -0,0 +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
modules/visualization/routes.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/visualization/routes.py
2
+ from flask import Blueprint, request, jsonify, send_file
3
+ import os
4
+ import time
5
+ import re
6
+ import requests
7
+ import plotly.graph_objects as go
8
+ import numpy as np
9
+ from mpl_toolkits.mplot3d import Axes3D
10
+ import matplotlib.pyplot as plt
11
+ from io import BytesIO
12
+ import matplotlib
13
+ import json
14
+ matplotlib.use('Agg') # 非交互式后端
15
+
16
+ visualization_bp = Blueprint('visualization', __name__)
17
+
18
+ # 确保存储目录存在
19
+ os.makedirs('static', exist_ok=True)
20
+
21
+ @visualization_bp.route('/mindmap', methods=['POST'])
22
+ def generate_mindmap():
23
+ """生成思维导图"""
24
+ try:
25
+ data = request.json
26
+ content = data.get('content', '')
27
+
28
+ if not content:
29
+ return jsonify({
30
+ "success": False,
31
+ "message": "思维导图内容不能为空"
32
+ }), 400
33
+
34
+ # 提取@startmindmap和@endmindmap之间的内容
35
+ pattern = r'@startmindmap\n([\s\S]*?)@endmindmap'
36
+ match = re.search(pattern, content)
37
+
38
+ if match:
39
+ processed_content = f"@startmindmap\n{match.group(1)}\n@endmindmap"
40
+ else:
41
+ # 如果内容中没有正确格式,直接使用原始内容
42
+ processed_content = content
43
+
44
+ # 确保内容有开始和结束标记
45
+ if '@startmindmap' not in processed_content:
46
+ processed_content = "@startmindmap\n" + processed_content
47
+ if '@endmindmap' not in processed_content:
48
+ processed_content += "\n@endmindmap"
49
+
50
+ # 使用HuggingFace Space API生成图像
51
+ response = requests.post(
52
+ 'https://mistpe-flask.hf.space/v1/images/generations',
53
+ headers={
54
+ 'Authorization': 'Bearer sk-xxx', # 实际应用中应从配置中获取
55
+ 'Content-Type': 'application/json'
56
+ },
57
+ json={
58
+ 'model': 'dall-e-3',
59
+ 'prompt': processed_content,
60
+ 'n': 1,
61
+ 'size': '1024x1024'
62
+ }
63
+ )
64
+
65
+ if response.status_code != 200:
66
+ return jsonify({
67
+ "success": False,
68
+ "message": "生成思维导图图像失败",
69
+ "status_code": response.status_code
70
+ }), 500
71
+
72
+ response_data = response.json()
73
+
74
+ if not response_data.get('data') or not response_data['data'][0].get('url'):
75
+ return jsonify({
76
+ "success": False,
77
+ "message": "API返回的数据中没有图像URL"
78
+ }), 500
79
+
80
+ image_url = response_data['data'][0]['url']
81
+
82
+ # 下载图像并保存到本地
83
+ img_response = requests.get(image_url)
84
+ if img_response.status_code != 200:
85
+ return jsonify({
86
+ "success": False,
87
+ "message": "下载生成的图像失败"
88
+ }), 500
89
+
90
+ # 保存图像到本地
91
+ img_filename = f'mindmap_{int(time.time())}.png'
92
+ img_path = os.path.join('static', img_filename)
93
+
94
+ with open(img_path, 'wb') as f:
95
+ f.write(img_response.content)
96
+
97
+ # 构建URL
98
+ local_img_url = f'/static/{img_filename}'
99
+
100
+ return jsonify({
101
+ "success": True,
102
+ "url": local_img_url,
103
+ "original_url": image_url,
104
+ "message": "思维导图生成成功"
105
+ })
106
+
107
+ except Exception as e:
108
+ import traceback
109
+ traceback.print_exc()
110
+ return jsonify({
111
+ "success": False,
112
+ "message": str(e)
113
+ }), 500
114
+ @visualization_bp.route('/3d-surface', methods=['POST'])
115
+ def generate_3d_surface():
116
+ """生成3D表面图并返回嵌入式HTML"""
117
+ try:
118
+ data = request.json
119
+ code = data.get('code', '')
120
+
121
+ if not code:
122
+ return jsonify({
123
+ "success": False,
124
+ "message": "请提供函数代码"
125
+ }), 400
126
+
127
+ # 清理代码(移除可能的Markdown标记)
128
+ code = re.sub(r'```python\n', '', code)
129
+ code = re.sub(r'```', '', code)
130
+
131
+ # 在安全环境中执行代码
132
+ local_vars = {}
133
+ try:
134
+ # 仅提供numpy库
135
+ exec(code, {"__builtins__": __builtins__, "np": np, "numpy": np}, local_vars)
136
+
137
+ # 检查是否有create_3d_plot函数
138
+ if 'create_3d_plot' not in local_vars:
139
+ return jsonify({
140
+ "success": False,
141
+ "message": "提供的代码未包含create_3d_plot函数"
142
+ }), 500
143
+
144
+ # 执行函数获取数据
145
+ plot_data = local_vars['create_3d_plot']()
146
+
147
+ if not isinstance(plot_data, dict) or not all(k in plot_data for k in ['x', 'y', 'z']):
148
+ return jsonify({
149
+ "success": False,
150
+ "message": "函数未返回有效的3D数据"
151
+ }), 500
152
+
153
+ # 创建Plotly图形
154
+ fig = go.Figure()
155
+
156
+ # 根据类型添加跟踪
157
+ if plot_data.get('type') == 'scatter3d':
158
+ fig.add_trace(go.Scatter3d(
159
+ x=plot_data['x'],
160
+ y=plot_data['y'],
161
+ z=plot_data['z'],
162
+ mode='markers',
163
+ marker=dict(
164
+ size=4,
165
+ color=plot_data.get('color', plot_data['z']),
166
+ colorscale='Viridis',
167
+ opacity=0.8
168
+ )
169
+ ))
170
+ else: # 默认为surface
171
+ # 处理数据格式
172
+ try:
173
+ x = np.array(plot_data['x'])
174
+ y = np.array(plot_data['y'])
175
+ z = np.array(plot_data['z'])
176
+
177
+ if len(x.shape) == 1 and len(y.shape) == 1:
178
+ X, Y = np.meshgrid(x, y)
179
+ if len(z.shape) == 1:
180
+ Z = z.reshape(len(y), len(x))
181
+ else:
182
+ Z = z
183
+ else:
184
+ X = x
185
+ Y = y
186
+ Z = z
187
+
188
+ fig.add_trace(go.Surface(
189
+ z=Z,
190
+ x=X,
191
+ y=Y,
192
+ colorscale='Viridis'
193
+ ))
194
+ except Exception as data_error:
195
+ return jsonify({
196
+ "success": False,
197
+ "message": f"处理3D数据时出错: {str(data_error)}"
198
+ }), 500
199
+
200
+ # 设置图形布局
201
+ fig.update_layout(
202
+ title=plot_data.get('title', '3D 可视化'),
203
+ scene=dict(
204
+ xaxis_title='X',
205
+ yaxis_title='Y',
206
+ zaxis_title='Z'
207
+ ),
208
+ width=800,
209
+ height=600,
210
+ margin=dict(l=0, r=0, b=0, t=30) # 减小边距,使图形更紧凑
211
+ )
212
+
213
+ # 直接生成HTML
214
+ html_content = fig.to_html(include_plotlyjs=True, full_html=True)
215
+
216
+ # 保存为文件
217
+ html_filename = f'3d_plot_{int(time.time())}.html'
218
+ html_path = os.path.join('static', html_filename)
219
+
220
+ with open(html_path, 'w', encoding='utf-8') as f:
221
+ f.write(html_content)
222
+
223
+ return jsonify({
224
+ "success": True,
225
+ "html_url": f'/static/{html_filename}',
226
+ "message": "3D图形生成成功"
227
+ })
228
+
229
+ except Exception as e:
230
+ import traceback
231
+ error_traceback = traceback.format_exc()
232
+ print(f"3D可视化执行错误: {error_traceback}")
233
+ return jsonify({
234
+ "success": False,
235
+ "message": f"执行代码时出错: {str(e)}"
236
+ }), 500
237
+
238
+ except Exception as e:
239
+ import traceback
240
+ traceback.print_exc()
241
+ return jsonify({
242
+ "success": False,
243
+ "message": str(e)
244
+ }), 500
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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
templates/code_execution.html ADDED
@@ -0,0 +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>
templates/index.html ADDED
@@ -0,0 +1,2362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- templates/index.html -->
2
+ <!DOCTYPE html>
3
+ <html lang="zh-CN">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
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
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script>
12
+ <style>
13
+ :root {
14
+ --primary-color: #4361ee;
15
+ --secondary-color: #3f37c9;
16
+ --accent-color: #4cc9f0;
17
+ --success-color: #4caf50;
18
+ --warning-color: #ff9800;
19
+ --danger-color: #f44336;
20
+ --light-color: #f8f9fa;
21
+ --dark-color: #212529;
22
+ --border-color: #dee2e6;
23
+ --border-radius: 0.375rem;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
28
+ display: flex;
29
+ margin: 0;
30
+ padding: 0;
31
+ height: 100vh;
32
+ background-color: #f5f7fa;
33
+ color: #333;
34
+ }
35
+
36
+ .sidebar {
37
+ width: 280px;
38
+ background-color: #fff;
39
+ border-right: 1px solid var(--border-color);
40
+ box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
41
+ display: flex;
42
+ flex-direction: column;
43
+ transition: all 0.3s ease;
44
+ z-index: 1000;
45
+ }
46
+
47
+ .sidebar-header {
48
+ padding: 1.5rem;
49
+ border-bottom: 1px solid var(--border-color);
50
+ }
51
+
52
+ .sidebar-header h2 {
53
+ margin: 0;
54
+ font-size: 1.5rem;
55
+ font-weight: 600;
56
+ color: var(--primary-color);
57
+ }
58
+
59
+ .sidebar-menu {
60
+ list-style: none;
61
+ padding: 0;
62
+ margin: 0;
63
+ flex: 1;
64
+ overflow-y: auto;
65
+ }
66
+
67
+ .sidebar-menu li {
68
+ padding: 1rem 1.5rem;
69
+ cursor: pointer;
70
+ transition: background-color 0.2s ease;
71
+ display: flex;
72
+ align-items: center;
73
+ position: relative;
74
+ }
75
+
76
+ .sidebar-menu li i {
77
+ margin-right: 0.75rem;
78
+ font-size: 1.1rem;
79
+ }
80
+
81
+ .sidebar-menu li:hover {
82
+ background-color: rgba(67, 97, 238, 0.05);
83
+ color: var(--primary-color);
84
+ }
85
+
86
+ .sidebar-menu li.active {
87
+ background-color: rgba(67, 97, 238, 0.1);
88
+ color: var(--primary-color);
89
+ font-weight: 500;
90
+ }
91
+
92
+ .sidebar-menu li.active::before {
93
+ content: '';
94
+ position: absolute;
95
+ left: 0;
96
+ top: 0;
97
+ bottom: 0;
98
+ width: 4px;
99
+ background-color: var(--primary-color);
100
+ }
101
+
102
+ .main-content {
103
+ flex: 1;
104
+ padding: 2rem;
105
+ overflow-y: auto;
106
+ }
107
+
108
+ .content-header {
109
+ margin-bottom: 2rem;
110
+ }
111
+
112
+ .content-header h1 {
113
+ margin: 0;
114
+ font-size: 1.75rem;
115
+ font-weight: 600;
116
+ color: #333;
117
+ }
118
+
119
+ .content-section {
120
+ display: none;
121
+ }
122
+
123
+ .content-section.active {
124
+ display: block;
125
+ }
126
+
127
+ .card {
128
+ background-color: #fff;
129
+ border-radius: var(--border-radius);
130
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
131
+ margin-bottom: 1.5rem;
132
+ border: 1px solid var(--border-color);
133
+ overflow: hidden;
134
+ }
135
+
136
+ .card-header {
137
+ background-color: #fff;
138
+ border-bottom: 1px solid var(--border-color);
139
+ padding: 1rem 1.5rem;
140
+ display: flex;
141
+ justify-content: space-between;
142
+ align-items: center;
143
+ }
144
+
145
+ .card-header h3 {
146
+ margin: 0;
147
+ font-size: 1.2rem;
148
+ font-weight: 600;
149
+ }
150
+
151
+ .card-body {
152
+ padding: 1.5rem;
153
+ }
154
+
155
+ .form-group {
156
+ margin-bottom: 1.5rem;
157
+ }
158
+
159
+ .form-label {
160
+ font-weight: 500;
161
+ margin-bottom: 0.5rem;
162
+ display: block;
163
+ }
164
+
165
+ .form-control {
166
+ border-radius: var(--border-radius);
167
+ }
168
+
169
+ .btn {
170
+ border-radius: var(--border-radius);
171
+ font-weight: 500;
172
+ }
173
+
174
+ .btn-primary {
175
+ background-color: var(--primary-color);
176
+ border-color: var(--primary-color);
177
+ }
178
+
179
+ .btn-primary:hover {
180
+ background-color: var(--secondary-color);
181
+ border-color: var(--secondary-color);
182
+ }
183
+
184
+ .workflow-editor {
185
+ border: 1px solid var(--border-color);
186
+ border-radius: var(--border-radius);
187
+ min-height: 400px;
188
+ background-color: #f8f9fa;
189
+ position: relative;
190
+ }
191
+
192
+ .knowledge-item {
193
+ padding: 1rem;
194
+ margin-bottom: 1rem;
195
+ border: 1px solid var(--border-color);
196
+ border-radius: var(--border-radius);
197
+ background-color: #fff;
198
+ transition: all 0.2s ease;
199
+ }
200
+
201
+ .knowledge-item:hover {
202
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
203
+ }
204
+
205
+ .knowledge-item .header {
206
+ display: flex;
207
+ justify-content: space-between;
208
+ align-items: center;
209
+ margin-bottom: 0.5rem;
210
+ }
211
+
212
+ .agent-item {
213
+ padding: 1.25rem;
214
+ margin-bottom: 1.5rem;
215
+ border-radius: var(--border-radius);
216
+ background-color: #fff;
217
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
218
+ transition: all 0.2s ease;
219
+ border-left: 4px solid var(--primary-color);
220
+ }
221
+
222
+ .agent-item:hover {
223
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
224
+ transform: translateY(-2px);
225
+ }
226
+
227
+ .agent-item .header {
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: center;
231
+ margin-bottom: 1rem;
232
+ }
233
+
234
+ .agent-item .title {
235
+ font-size: 1.2rem;
236
+ font-weight: 600;
237
+ margin: 0;
238
+ color: #333;
239
+ }
240
+
241
+ .agent-item .description {
242
+ color: #666;
243
+ margin-bottom: 1rem;
244
+ }
245
+
246
+ .agent-item .meta {
247
+ display: flex;
248
+ font-size: 0.85rem;
249
+ color: #777;
250
+ margin-top: 1rem;
251
+ }
252
+
253
+ .agent-item .meta div {
254
+ margin-right: 1.5rem;
255
+ display: flex;
256
+ align-items: center;
257
+ }
258
+
259
+ .agent-item .meta i {
260
+ margin-right: 0.5rem;
261
+ }
262
+
263
+ .nav-tabs {
264
+ border-bottom: 1px solid var(--border-color);
265
+ margin-bottom: 1.5rem;
266
+ }
267
+
268
+ .nav-tabs .nav-link {
269
+ margin-bottom: -1px;
270
+ border: 1px solid transparent;
271
+ border-top-left-radius: 0.375rem;
272
+ border-top-right-radius: 0.375rem;
273
+ padding: 0.75rem 1.25rem;
274
+ color: #495057;
275
+ }
276
+
277
+ .nav-tabs .nav-link.active {
278
+ color: var(--primary-color);
279
+ background-color: #fff;
280
+ border-color: var(--border-color) var(--border-color) #fff;
281
+ font-weight: 500;
282
+ }
283
+
284
+ .nav-tabs .nav-link:hover {
285
+ border-color: #e9ecef #e9ecef var(--border-color);
286
+ }
287
+
288
+ .spinner-border {
289
+ width: 1.5rem;
290
+ height: 1.5rem;
291
+ }
292
+
293
+ .distribution-link {
294
+ padding: 0.75rem 1rem;
295
+ background-color: #f8f9fa;
296
+ border: 1px solid var(--border-color);
297
+ border-radius: var(--border-radius);
298
+ display: flex;
299
+ justify-content: space-between;
300
+ align-items: center;
301
+ margin-bottom: 1rem;
302
+ }
303
+
304
+ .badge {
305
+ display: inline-block;
306
+ padding: 0.35em 0.65em;
307
+ font-size: 0.75em;
308
+ font-weight: 500;
309
+ line-height: 1;
310
+ text-align: center;
311
+ white-space: nowrap;
312
+ vertical-align: baseline;
313
+ border-radius: 50rem;
314
+ margin-left: 0.5rem;
315
+ }
316
+
317
+ .badge-primary {
318
+ background-color: var(--primary-color);
319
+ color: #fff;
320
+ }
321
+
322
+ .badge-success {
323
+ background-color: var(--success-color);
324
+ color: #fff;
325
+ }
326
+
327
+ .badge-warning {
328
+ background-color: var(--warning-color);
329
+ color: #fff;
330
+ }
331
+
332
+ .plugin-option {
333
+ padding: 1rem;
334
+ margin-bottom: 1rem;
335
+ border: 1px solid var(--border-color);
336
+ border-radius: var(--border-radius);
337
+ display: flex;
338
+ align-items: center;
339
+ cursor: pointer;
340
+ transition: all 0.2s ease;
341
+ }
342
+
343
+ .plugin-option:hover {
344
+ background-color: rgba(67, 97, 238, 0.05);
345
+ border-color: var(--primary-color);
346
+ }
347
+
348
+ .plugin-option.selected {
349
+ background-color: rgba(67, 97, 238, 0.1);
350
+ border-color: var(--primary-color);
351
+ }
352
+
353
+ .plugin-option .icon {
354
+ font-size: 1.5rem;
355
+ margin-right: 1rem;
356
+ color: var(--primary-color);
357
+ }
358
+
359
+ .plugin-option .content {
360
+ flex: 1;
361
+ }
362
+
363
+ .plugin-option .title {
364
+ font-weight: 500;
365
+ margin-bottom: 0.25rem;
366
+ }
367
+
368
+ .plugin-option .description {
369
+ font-size: 0.9rem;
370
+ color: #666;
371
+ }
372
+
373
+ /* 工作流可视化相关样式 */
374
+ .workflow-canvas {
375
+ height: 400px;
376
+ background-color: #f9fbfd;
377
+ border: 1px solid var(--border-color);
378
+ border-radius: var(--border-radius);
379
+ position: relative;
380
+ }
381
+
382
+ /* 工作流节点样式 */
383
+ .workflow-node {
384
+ position: absolute;
385
+ width: 150px;
386
+ height: 50px;
387
+ background-color: white;
388
+ border-radius: 4px;
389
+ border: 1px solid #ccc;
390
+ box-shadow: 0 2px 6px rgba(0,0,0,0.1);
391
+ display: flex;
392
+ overflow: hidden;
393
+ user-select: none;
394
+ z-index: 10;
395
+ }
396
+
397
+ .node-icon {
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: center;
401
+ width: 30px;
402
+ height: 100%;
403
+ color: white;
404
+ font-weight: bold;
405
+ }
406
+
407
+ .node-content {
408
+ flex: 1;
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ padding: 0 10px;
413
+ font-size: 14px;
414
+ color: #333;
415
+ }
416
+
417
+ /* 节点类型样式 */
418
+ .node-type-intent_recognition .node-icon {
419
+ background-color: #673AB7;
420
+ }
421
+ .node-type-intent_recognition {
422
+ border-color: #673AB7;
423
+ background-color: #EDE7F6;
424
+ }
425
+
426
+ .node-type-knowledge_query .node-icon {
427
+ background-color: #009688;
428
+ }
429
+ .node-type-knowledge_query {
430
+ border-color: #009688;
431
+ background-color: #E0F2F1;
432
+ }
433
+
434
+ .node-type-plugin_call .node-icon {
435
+ background-color: #FF9800;
436
+ }
437
+ .node-type-plugin_call {
438
+ border-color: #FF9800;
439
+ background-color: #FFF3E0;
440
+ }
441
+
442
+ .node-type-generate_response .node-icon {
443
+ background-color: #2196F3;
444
+ }
445
+ .node-type-generate_response {
446
+ border-color: #2196F3;
447
+ background-color: #E3F2FD;
448
+ }
449
+
450
+ /* jsPlumb 连接线标签样式 */
451
+ .connection-label {
452
+ background-color: white;
453
+ padding: 3px 6px;
454
+ border-radius: 3px;
455
+ border: 1px solid #eee;
456
+ font-size: 12px;
457
+ color: #666;
458
+ }
459
+ </style>
460
+ </head>
461
+ <body>
462
+ <div class="sidebar">
463
+ <div class="sidebar-header">
464
+ <h2>AI助教开发平台</h2>
465
+ </div>
466
+ <ul class="sidebar-menu">
467
+ <li class="active" data-section="dashboard">
468
+ <i class="bi bi-grid-1x2"></i>
469
+ <span>仪表盘</span>
470
+ </li>
471
+ <li data-section="create-agent">
472
+ <i class="bi bi-plus-circle"></i>
473
+ <span>创建Agent</span>
474
+ </li>
475
+ <li data-section="knowledge-base">
476
+ <i class="bi bi-database"></i>
477
+ <span>知识库管理</span>
478
+ </li>
479
+ <li data-section="agent-list">
480
+ <i class="bi bi-list-ul"></i>
481
+ <span>Agent列表</span>
482
+ </li>
483
+ </ul>
484
+ </div>
485
+
486
+ <div class="main-content">
487
+ <!-- 仪表盘 -->
488
+ <section id="dashboard" class="content-section active">
489
+ <div class="content-header">
490
+ <h1>仪表盘</h1>
491
+ <p class="text-muted mt-2">欢迎使用教育AI助手开发平台</p>
492
+ </div>
493
+
494
+ <div class="row">
495
+ <div class="col-md-4">
496
+ <div class="card">
497
+ <div class="card-body text-center">
498
+ <div class="display-4 mb-3">
499
+ <i class="bi bi-robot text-primary"></i>
500
+ </div>
501
+ <h3 id="agent-count">0</h3>
502
+ <p class="text-muted">AI助手</p>
503
+ </div>
504
+ </div>
505
+ </div>
506
+ <div class="col-md-4">
507
+ <div class="card">
508
+ <div class="card-body text-center">
509
+ <div class="display-4 mb-3">
510
+ <i class="bi bi-database text-success"></i>
511
+ </div>
512
+ <h3 id="knowledge-count">0</h3>
513
+ <p class="text-muted">知识库</p>
514
+ </div>
515
+ </div>
516
+ </div>
517
+ <div class="col-md-4">
518
+ <div class="card">
519
+ <div class="card-body text-center">
520
+ <div class="display-4 mb-3">
521
+ <i class="bi bi-share text-warning"></i>
522
+ </div>
523
+ <h3 id="distribution-count">0</h3>
524
+ <p class="text-muted">分发链接</p>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ </div>
529
+
530
+ <div class="row mt-4">
531
+ <div class="col-md-6">
532
+ <div class="card">
533
+ <div class="card-header">
534
+ <h3>最近创建的Agent</h3>
535
+ </div>
536
+ <div class="card-body" id="recent-agents">
537
+ <div class="text-center py-4">
538
+ <div class="spinner-border text-primary" role="status">
539
+ <span class="visually-hidden">加载中...</span>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ </div>
544
+ </div>
545
+ <div class="col-md-6">
546
+ <div class="card">
547
+ <div class="card-header">
548
+ <h3>快速入门</h3>
549
+ </div>
550
+ <div class="card-body">
551
+ <div class="quick-start-item mb-3 p-3 border-bottom">
552
+ <h5><i class="bi bi-1-circle me-2"></i> 创建知识库</h5>
553
+ <p>上传PDF、文本等文档,构建AI助手的知识基础。</p>
554
+ <button class="btn btn-sm btn-outline-primary" onclick="switchSection('knowledge-base')">开始创建</button>
555
+ </div>
556
+ <div class="quick-start-item mb-3 p-3 border-bottom">
557
+ <h5><i class="bi bi-2-circle me-2"></i> 创建Agent</h5>
558
+ <p>设计您的AI助手,选择插件和知识库。</p>
559
+ <button class="btn btn-sm btn-outline-primary" onclick="switchSection('create-agent')">开始创建</button>
560
+ </div>
561
+ <div class="quick-start-item p-3">
562
+ <h5><i class="bi bi-3-circle me-2"></i> 分发给学生</h5>
563
+ <p>生成访问链接,分享给您的学生。</p>
564
+ <button class="btn btn-sm btn-outline-primary" onclick="switchSection('agent-list')">查看Agent</button>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ </div>
569
+ </div>
570
+ </section>
571
+
572
+ <!-- 创建Agent -->
573
+ <section id="create-agent" class="content-section">
574
+ <div class="content-header">
575
+ <h1>创建新Agent</h1>
576
+ <p class="text-muted mt-2">设计您的AI助教Agent</p>
577
+ </div>
578
+
579
+ <div class="card">
580
+ <div class="card-body">
581
+ <ul class="nav nav-tabs" id="agentCreationTabs" role="tablist">
582
+ <li class="nav-item" role="presentation">
583
+ <button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true">基本信息</button>
584
+ </li>
585
+ <li class="nav-item" role="presentation">
586
+ <button class="nav-link" id="plugins-tab" data-bs-toggle="tab" data-bs-target="#plugins" type="button" role="tab" aria-controls="plugins" aria-selected="false">插件选择</button>
587
+ </li>
588
+ <li class="nav-item" role="presentation">
589
+ <button class="nav-link" id="knowledge-tab" data-bs-toggle="tab" data-bs-target="#knowledge" type="button" role="tab" aria-controls="knowledge" aria-selected="false">知识库</button>
590
+ </li>
591
+ <li class="nav-item" role="presentation">
592
+ <button class="nav-link" id="workflow-tab" data-bs-toggle="tab" data-bs-target="#workflow" type="button" role="tab" aria-controls="workflow" aria-selected="false">工作流设计</button>
593
+ </li>
594
+ </ul>
595
+
596
+ <div class="tab-content" id="agentCreationTabContent">
597
+ <!-- 基本信息 -->
598
+ <div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab">
599
+ <div class="mt-4">
600
+ <div class="form-group">
601
+ <label for="agent-name" class="form-label">Agent名称</label>
602
+ <input type="text" class="form-control" id="agent-name" placeholder="例如:计算机组成原理助教">
603
+ </div>
604
+ <div class="form-group">
605
+ <label for="agent-description" class="form-label">描述</label>
606
+ <textarea class="form-control" id="agent-description" rows="3" placeholder="描述这个Agent的功能和用途..."></textarea>
607
+ </div>
608
+ <div class="form-group">
609
+ <label for="agent-subject" class="form-label">学科/课程名称</label>
610
+ <input type="text" class="form-control" id="agent-subject" placeholder="例如:计算机组成原理">
611
+ <div class="form-text">这将影响AI助教的知识领域定位</div>
612
+ </div>
613
+ <div class="form-group">
614
+ <label for="agent-instructor" class="form-label">指导教师</label>
615
+ <input type="text" class="form-control" id="agent-instructor" placeholder="例如:李志刚教授">
616
+ </div>
617
+ <div class="form-group">
618
+ <label for="agent-type" class="form-label">Agent类型</label>
619
+ <select class="form-select" id="agent-type">
620
+ <option value="educational">教育辅导</option>
621
+ <option value="programming">编程辅助</option>
622
+ <option value="math">数学辅导</option>
623
+ <option value="general">通用助手</option>
624
+ </select>
625
+ </div>
626
+ <button class="btn btn-primary" onclick="nextTab('plugins-tab')">下一步</button>
627
+ </div>
628
+ </div>
629
+
630
+ <!-- 插件选择 -->
631
+ <div class="tab-pane fade" id="plugins" role="tabpanel" aria-labelledby="plugins-tab">
632
+ <div class="mt-4">
633
+ <div class="mb-3">
634
+ <h5>选择Agent所需的插件</h5>
635
+ <p class="text-muted">插件可以扩展Agent的能力,使其更加强大</p>
636
+ </div>
637
+
638
+ <div class="plugin-option" data-plugin="code" onclick="togglePluginSelection(this)">
639
+ <div class="icon">
640
+ <i class="bi bi-code-square"></i>
641
+ </div>
642
+ <div class="content">
643
+ <div class="title">代码执行</div>
644
+ <div class="description">允许学生编写和执行Python代码,实时获取结果</div>
645
+ </div>
646
+ <div class="checkbox">
647
+ <input type="checkbox" class="form-check-input">
648
+ </div>
649
+ </div>
650
+
651
+ <div class="plugin-option" data-plugin="visualization" onclick="togglePluginSelection(this)">
652
+ <div class="icon">
653
+ <i class="bi bi-bar-chart"></i>
654
+ </div>
655
+ <div class="content">
656
+ <div class="title">3D可视化</div>
657
+ <div class="description">生成交互式3D图形,帮助学生理解数学概念</div>
658
+ </div>
659
+ <div class="checkbox">
660
+ <input type="checkbox" class="form-check-input">
661
+ </div>
662
+ </div>
663
+
664
+ <div class="plugin-option" data-plugin="mindmap" onclick="togglePluginSelection(this)">
665
+ <div class="icon">
666
+ <i class="bi bi-diagram-3"></i>
667
+ </div>
668
+ <div class="content">
669
+ <div class="title">思维导图</div>
670
+ <div class="description">创建结构化的思维导图,帮助学生梳理知识点</div>
671
+ </div>
672
+ <div class="checkbox">
673
+ <input type="checkbox" class="form-check-input">
674
+ </div>
675
+ </div>
676
+
677
+ <div class="d-flex justify-content-between mt-4">
678
+ <button class="btn btn-outline-secondary" onclick="nextTab('basic-tab')">上一步</button>
679
+ <button class="btn btn-primary" onclick="nextTab('knowledge-tab')">下一步</button>
680
+ </div>
681
+ </div>
682
+ </div>
683
+
684
+ <!-- 知识库 -->
685
+ <div class="tab-pane fade" id="knowledge" role="tabpanel" aria-labelledby="knowledge-tab">
686
+ <div class="mt-4">
687
+ <div class="mb-3">
688
+ <h5>选择Agent使用的知识库</h5>
689
+ <p class="text-muted">知识库提供Agent回答问题的专业知识</p>
690
+ </div>
691
+
692
+ <div class="mb-3">
693
+ <div class="input-group">
694
+ <input type="text" class="form-control" placeholder="搜索知识库..." id="knowledge-search">
695
+ <button class="btn btn-outline-secondary" type="button">
696
+ <i class="bi bi-search"></i>
697
+ </button>
698
+ </div>
699
+ </div>
700
+
701
+ <div id="knowledge-list" class="mb-4">
702
+ <div class="text-center py-4">
703
+ <div class="spinner-border text-primary" role="status">
704
+ <span class="visually-hidden">加载中...</span>
705
+ </div>
706
+ </div>
707
+ </div>
708
+
709
+ <div class="d-flex justify-content-between mt-4">
710
+ <button class="btn btn-outline-secondary" onclick="nextTab('plugins-tab')">上一步</button>
711
+ <button class="btn btn-primary" onclick="nextTab('workflow-tab')">下一步</button>
712
+ </div>
713
+ </div>
714
+ </div>
715
+
716
+ <!-- 工作流设计 -->
717
+ <div class="tab-pane fade" id="workflow" role="tabpanel" aria-labelledby="workflow-tab">
718
+ <div class="mt-4">
719
+ <div class="mb-3">
720
+ <h5>设计Agent工作流</h5>
721
+ <p class="text-muted">定义Agent如何处理用户的请求</p>
722
+ </div>
723
+
724
+ <div class="mb-4">
725
+ <button class="btn btn-primary" id="ai-workflow-btn">
726
+ <i class="bi bi-magic"></i> 使用AI自动设计工作流
727
+ </button>
728
+ </div>
729
+
730
+ <div class="card mb-4">
731
+ <div class="card-body p-0">
732
+ <ul class="nav nav-tabs" id="workflowViewTabs" role="tablist">
733
+ <li class="nav-item" role="presentation">
734
+ <button class="nav-link active" id="visual-tab" data-bs-toggle="tab" data-bs-target="#visual-view" type="button" role="tab">可视化视图</button>
735
+ </li>
736
+ <li class="nav-item" role="presentation">
737
+ <button class="nav-link" id="code-tab" data-bs-toggle="tab" data-bs-target="#code-view" type="button" role="tab">代码视图</button>
738
+ </li>
739
+ </ul>
740
+ <div class="tab-content" id="workflowViewContent">
741
+ <div class="tab-pane fade show active" id="visual-view" role="tabpanel">
742
+ <div id="workflow-canvas" class="workflow-canvas">
743
+ <div class="p-4 text-center text-muted">
744
+ 点击"使用AI自动设计工作流"按钮,或手动设计工作流
745
+ </div>
746
+ </div>
747
+ </div>
748
+ <div class="tab-pane fade" id="code-view" role="tabpanel">
749
+ <div class="p-4">
750
+ <pre id="workflow-code" class="mb-0 p-3 bg-light border rounded">{}</pre>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ </div>
755
+ </div>
756
+
757
+ <div class="d-flex justify-content-between">
758
+ <button class="btn btn-outline-secondary" onclick="nextTab('knowledge-tab')">上一步</button>
759
+ <button class="btn btn-success" id="create-agent-btn">创建Agent</button>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ </div>
764
+ </div>
765
+ </div>
766
+ </section>
767
+
768
+ <!-- 知识库管理 -->
769
+ <section id="knowledge-base" class="content-section">
770
+ <div class="content-header">
771
+ <h1>知识库管理</h1>
772
+ <p class="text-muted mt-2">上传和管理Agent的知识源</p>
773
+ </div>
774
+
775
+ <div class="row">
776
+ <div class="col-md-4">
777
+ <div class="card">
778
+ <div class="card-header">
779
+ <h3>创建知识库</h3>
780
+ </div>
781
+ <div class="card-body">
782
+ <form id="knowledge-form">
783
+ <div class="form-group">
784
+ <label for="knowledge-name" class="form-label">知识库名称</label>
785
+ <input type="text" class="form-control" id="knowledge-name" placeholder="例如:计算机组成原理" required>
786
+ </div>
787
+
788
+ <div class="form-group">
789
+ <label for="knowledge-file" class="form-label">上传文件</label>
790
+ <input type="file" class="form-control" id="knowledge-file" required>
791
+ <div class="form-text">支持PDF、TXT、MD等格式</div>
792
+ </div>
793
+
794
+ <div class="progress mt-3 mb-2" style="display: none;" id="upload-progress">
795
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
796
+ </div>
797
+
798
+ <button type="submit" class="btn btn-primary mt-3 w-100">创建知识库</button>
799
+ </form>
800
+ </div>
801
+ </div>
802
+ </div>
803
+
804
+ <div class="col-md-8">
805
+ <div class="card">
806
+ <div class="card-header">
807
+ <h3>现有知识库</h3>
808
+ </div>
809
+ <div class="card-body">
810
+ <div id="existing-knowledge-list">
811
+ <div class="text-center py-4">
812
+ <div class="spinner-border text-primary" role="status">
813
+ <span class="visually-hidden">加载中...</span>
814
+ </div>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ </div>
819
+ </div>
820
+ </div>
821
+ </section>
822
+
823
+ <!-- Agent列表 -->
824
+ <section id="agent-list" class="content-section">
825
+ <div class="content-header d-flex justify-content-between align-items-center">
826
+ <div>
827
+ <h1>Agent列表</h1>
828
+ <p class="text-muted mt-2">管理和分发您创建的AI助手</p>
829
+ </div>
830
+ <div>
831
+ <button class="btn btn-primary" onclick="switchSection('create-agent')">
832
+ <i class="bi bi-plus-lg"></i> 创建新Agent
833
+ </button>
834
+ </div>
835
+ </div>
836
+
837
+ <div class="row">
838
+ <div class="col-12">
839
+ <div class="card">
840
+ <div class="card-body">
841
+ <div id="agent-list-container">
842
+ <div class="text-center py-4">
843
+ <div class="spinner-border text-primary" role="status">
844
+ <span class="visually-hidden">加载中...</span>
845
+ </div>
846
+ </div>
847
+ </div>
848
+ </div>
849
+ </div>
850
+ </div>
851
+ </div>
852
+ </section>
853
+ </div>
854
+
855
+ <!-- 模态框:Agent详情 -->
856
+ <div class="modal fade" id="agentDetailModal" tabindex="-1" aria-hidden="true">
857
+ <div class="modal-dialog modal-lg">
858
+ <div class="modal-content">
859
+ <div class="modal-header">
860
+ <h5 class="modal-title" id="agent-detail-title">Agent详情</h5>
861
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
862
+ </div>
863
+ <div class="modal-body" id="agent-detail-body">
864
+ <!-- Agent详情将通过JavaScript填充 -->
865
+ </div>
866
+ <div class="modal-footer">
867
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
868
+ </div>
869
+ </div>
870
+ </div>
871
+ </div>
872
+
873
+ <!-- 模态框:创建分发链接 -->
874
+ <div class="modal fade" id="createDistributionModal" tabindex="-1" aria-hidden="true">
875
+ <div class="modal-dialog">
876
+ <div class="modal-content">
877
+ <div class="modal-header">
878
+ <h5 class="modal-title">创建分发链接</h5>
879
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
880
+ </div>
881
+ <div class="modal-body">
882
+ <form id="distribution-form">
883
+ <input type="hidden" id="distribution-agent-id">
884
+ <div class="form-group mb-3">
885
+ <label for="distribution-expires" class="form-label">链接有效期</label>
886
+ <select class="form-select" id="distribution-expires">
887
+ <option value="0">永不过期</option>
888
+ <option value="86400">1天</option>
889
+ <option value="604800">7天</option>
890
+ <option value="2592000">30天</option>
891
+ </select>
892
+ </div>
893
+ </form>
894
+ </div>
895
+ <div class="modal-footer">
896
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
897
+ <button type="button" class="btn btn-primary" id="create-distribution-btn">创建链接</button>
898
+ </div>
899
+ </div>
900
+ </div>
901
+ </div>
902
+
903
+ <!-- 模态框:显示分发链接 -->
904
+ <div class="modal fade" id="showDistributionModal" tabindex="-1" aria-hidden="true">
905
+ <div class="modal-dialog">
906
+ <div class="modal-content">
907
+ <div class="modal-header">
908
+ <h5 class="modal-title">分发链接</h5>
909
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
910
+ </div>
911
+ <div class="modal-body">
912
+ <div class="alert alert-success">
913
+ 链接创建成功!请复制以下链接分享给学生
914
+ </div>
915
+ <div class="input-group mb-3">
916
+ <input type="text" class="form-control" id="distribution-link" readonly>
917
+ <button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('distribution-link')">
918
+ <i class="bi bi-clipboard"></i>
919
+ </button>
920
+ </div>
921
+ </div>
922
+ <div class="modal-footer">
923
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
924
+ </div>
925
+ </div>
926
+ </div>
927
+ </div>
928
+
929
+ <script>
930
+ // 初始化变量
931
+ let selectedPlugins = [];
932
+ let selectedKnowledgeBases = [];
933
+ let currentWorkflow = { nodes: [], edges: [] };
934
+ let allKnowledgeBases = [];
935
+ let allAgents = [];
936
+ let jsPlumbWorkflowInstance = null; // 添加jsPlumb实例变量
937
+
938
+ // 页面加载完成后执行
939
+ document.addEventListener('DOMContentLoaded', function() {
940
+ // 初始化侧边栏菜单点击事件
941
+ document.querySelectorAll('.sidebar-menu li').forEach(item => {
942
+ item.addEventListener('click', function() {
943
+ const section = this.getAttribute('data-section');
944
+ switchSection(section);
945
+ });
946
+ });
947
+
948
+ // 初始化表单提交事件
949
+ document.getElementById('knowledge-form').addEventListener('submit', createKnowledgeBase);
950
+
951
+ // 初始化AI工作流按钮
952
+ document.getElementById('ai-workflow-btn').addEventListener('click', generateAIWorkflow);
953
+
954
+ // 初始化创建Agent按钮
955
+ document.getElementById('create-agent-btn').addEventListener('click', createAgent);
956
+
957
+ // 初始化创建分发链接按钮
958
+ document.getElementById('create-distribution-btn').addEventListener('click', createDistribution);
959
+
960
+ // 初始化工作流视图标签切换
961
+ initWorkflowTabs();
962
+
963
+ // 加载知识库列表
964
+ loadKnowledgeBases();
965
+
966
+ // 加载Agent列表
967
+ loadAgents();
968
+ });
969
+
970
+ // 初始化工作流标签切换
971
+ function initWorkflowTabs() {
972
+ // 为主工作流和详情工作流标签添加点击事件
973
+ document.querySelectorAll('#workflowViewTabs .nav-link, #detailWorkflowTabs .nav-link').forEach(tab => {
974
+ tab.addEventListener('click', function(event) {
975
+ event.preventDefault();
976
+
977
+ // 获取目标面板ID
978
+ const targetId = this.getAttribute('data-bs-target');
979
+ const targetPane = document.querySelector(targetId);
980
+
981
+ if (!targetPane) return;
982
+
983
+ // 获取所有同级标签并移除激活状态
984
+ const tabs = this.closest('.nav-tabs').querySelectorAll('.nav-link');
985
+ tabs.forEach(t => t.classList.remove('active'));
986
+
987
+ // 激活当前标签
988
+ this.classList.add('active');
989
+
990
+ // 获取所有同级面板并隐藏
991
+ const tabContent = targetPane.closest('.tab-content');
992
+ if (tabContent) {
993
+ tabContent.querySelectorAll('.tab-pane').forEach(pane => {
994
+ pane.classList.remove('show', 'active');
995
+ });
996
+ }
997
+
998
+ // 显示目标面板
999
+ targetPane.classList.add('show', 'active');
1000
+
1001
+ // 如果切换到可视化视图,重新渲染工作流
1002
+ if (targetId === '#visual-view' || targetId === '#detail-visual-view') {
1003
+ const canvasId = targetId === '#visual-view' ? 'workflow-canvas' : 'detail-workflow-canvas';
1004
+
1005
+ // 确定要使用的工作流数据
1006
+ let workflowData = currentWorkflow;
1007
+ if (targetId === '#detail-visual-view') {
1008
+ const modalBody = document.getElementById('agent-detail-body');
1009
+ if (modalBody && modalBody.dataset.agentWorkflow) {
1010
+ try {
1011
+ workflowData = JSON.parse(modalBody.dataset.agentWorkflow);
1012
+ } catch (e) {
1013
+ console.error('解析工作流数据出错:', e);
1014
+ }
1015
+ }
1016
+ }
1017
+
1018
+ // 延迟一小段时间确保DOM已经更新
1019
+ setTimeout(() => {
1020
+ renderWorkflowVisualization(workflowData, canvasId);
1021
+ }, 200);
1022
+ }
1023
+ });
1024
+ });
1025
+ }
1026
+
1027
+ // 切换内容区域
1028
+ function switchSection(section) {
1029
+ // 隐藏所有内容区域
1030
+ document.querySelectorAll('.content-section').forEach(item => {
1031
+ item.classList.remove('active');
1032
+ });
1033
+
1034
+ // 显示选中的内容区域
1035
+ document.getElementById(section).classList.add('active');
1036
+
1037
+ // 更新侧边栏选中状态
1038
+ document.querySelectorAll('.sidebar-menu li').forEach(item => {
1039
+ item.classList.remove('active');
1040
+ if (item.getAttribute('data-section') === section) {
1041
+ item.classList.add('active');
1042
+ }
1043
+ });
1044
+
1045
+ // 如果切换到了工作流视图,重新绘制连接
1046
+ if (section === 'create-agent') {
1047
+ // 获取当前激活的标签页
1048
+ const activeTab = document.querySelector('#agentCreationTabs .nav-link.active');
1049
+ if (activeTab && activeTab.id === 'workflow-tab') {
1050
+ // 检查当前哪个视图处于激活状态
1051
+ const activeView = document.querySelector('#workflowViewContent .tab-pane.active');
1052
+ if (activeView && activeView.id === 'visual-view') {
1053
+ // 延迟执行以确保DOM已更新
1054
+ setTimeout(() => {
1055
+ renderWorkflowVisualization(currentWorkflow, 'workflow-canvas');
1056
+ }, 200);
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+
1062
+ // 切换到下一个选项卡
1063
+ function nextTab(tabId) {
1064
+ const tab = document.getElementById(tabId);
1065
+ const bsTab = new bootstrap.Tab(tab);
1066
+ bsTab.show();
1067
+ }
1068
+
1069
+ // 切换插件选择状态
1070
+ function togglePluginSelection(element) {
1071
+ const pluginId = element.getAttribute('data-plugin');
1072
+ const checkbox = element.querySelector('input[type="checkbox"]');
1073
+
1074
+ if (element.classList.contains('selected')) {
1075
+ element.classList.remove('selected');
1076
+ checkbox.checked = false;
1077
+ selectedPlugins = selectedPlugins.filter(id => id !== pluginId);
1078
+ } else {
1079
+ element.classList.add('selected');
1080
+ checkbox.checked = true;
1081
+ selectedPlugins.push(pluginId);
1082
+ }
1083
+ }
1084
+
1085
+ // 切换知识库选择状态
1086
+ function toggleKnowledgeSelection(element) {
1087
+ const knowledgeId = element.getAttribute('data-id');
1088
+
1089
+ if (element.classList.contains('selected')) {
1090
+ element.classList.remove('selected');
1091
+ element.querySelector('input[type="checkbox"]').checked = false;
1092
+ selectedKnowledgeBases = selectedKnowledgeBases.filter(id => id !== knowledgeId);
1093
+ } else {
1094
+ element.classList.add('selected');
1095
+ element.querySelector('input[type="checkbox"]').checked = true;
1096
+ selectedKnowledgeBases.push(knowledgeId);
1097
+ }
1098
+ }
1099
+
1100
+ // 加载知识库列表
1101
+ async function loadKnowledgeBases() {
1102
+ try {
1103
+ const response = await fetch('/api/knowledge');
1104
+ const result = await response.json();
1105
+
1106
+ if (result.success) {
1107
+ allKnowledgeBases = result.data || [];
1108
+
1109
+ // 更新仪表盘计数
1110
+ document.getElementById('knowledge-count').textContent = allKnowledgeBases.length;
1111
+
1112
+ // 更新Agent创建页面的知识库列表
1113
+ const knowledgeListElement = document.getElementById('knowledge-list');
1114
+ if (knowledgeListElement) {
1115
+ if (allKnowledgeBases.length === 0) {
1116
+ knowledgeListElement.innerHTML = `
1117
+ <div class="alert alert-info">
1118
+ <i class="bi bi-info-circle me-2"></i>
1119
+ 暂无知识库,请先创建知识库
1120
+ </div>
1121
+ <div class="text-center">
1122
+ <button class="btn btn-primary" onclick="switchSection('knowledge-base')">
1123
+ 创建知识库
1124
+ </button>
1125
+ </div>
1126
+ `;
1127
+ } else {
1128
+ knowledgeListElement.innerHTML = '';
1129
+
1130
+ allKnowledgeBases.forEach(knowledge => {
1131
+ const item = document.createElement('div');
1132
+ item.className = 'plugin-option';
1133
+ item.setAttribute('data-id', knowledge.id);
1134
+ item.setAttribute('onclick', 'toggleKnowledgeSelection(this)');
1135
+
1136
+ item.innerHTML = `
1137
+ <div class="icon">
1138
+ <i class="bi bi-journal-text"></i>
1139
+ </div>
1140
+ <div class="content">
1141
+ <div class="title">${knowledge.name}</div>
1142
+ <div class="description">
1143
+ <small>${knowledge.fileCount}个文件</small>
1144
+ </div>
1145
+ </div>
1146
+ <div class="checkbox">
1147
+ <input type="checkbox" class="form-check-input">
1148
+ </div>
1149
+ `;
1150
+
1151
+ knowledgeListElement.appendChild(item);
1152
+ });
1153
+ }
1154
+ }
1155
+
1156
+ // 更新知识库管理页面的列表
1157
+ const existingKnowledgeList = document.getElementById('existing-knowledge-list');
1158
+ if (existingKnowledgeList) {
1159
+ if (allKnowledgeBases.length === 0) {
1160
+ existingKnowledgeList.innerHTML = `
1161
+ <div class="alert alert-info">
1162
+ <i class="bi bi-info-circle me-2"></i>
1163
+ 暂无知识库,请创建第一个知识库
1164
+ </div>
1165
+ `;
1166
+ } else {
1167
+ existingKnowledgeList.innerHTML = '';
1168
+
1169
+ allKnowledgeBases.forEach(knowledge => {
1170
+ const item = document.createElement('div');
1171
+ item.className = 'knowledge-item';
1172
+
1173
+ let fileListHtml = '';
1174
+ if (knowledge.files && knowledge.files.length > 0) {
1175
+ fileListHtml = '<div class="mt-3"><small class="text-muted">包含文件:</small><ul class="list-unstyled ms-3">';
1176
+ knowledge.files.forEach(file => {
1177
+ fileListHtml += `<li><small><i class="bi bi-file-text me-1"></i>${file}</small></li>`;
1178
+ });
1179
+ fileListHtml += '</ul></div>';
1180
+ }
1181
+
1182
+ item.innerHTML = `
1183
+ <div class="header">
1184
+ <h5>${knowledge.name}</h5>
1185
+ <div>
1186
+ <button class="btn btn-sm btn-outline-danger" onclick="deleteKnowledgeBase('${knowledge.id}')">
1187
+ <i class="bi bi-trash"></i>
1188
+ </button>
1189
+ </div>
1190
+ </div>
1191
+ <div>
1192
+ <span class="badge bg-primary">${knowledge.fileCount}个文件</span>
1193
+ </div>
1194
+ ${fileListHtml}
1195
+ <div class="mt-3">
1196
+ <form class="upload-form" data-id="${knowledge.id}">
1197
+ <div class="input-group">
1198
+ <input type="file" class="form-control form-control-sm" required>
1199
+ <button type="submit" class="btn btn-sm btn-outline-primary">添加文件</button>
1200
+ </div>
1201
+ </form>
1202
+ </div>
1203
+ `;
1204
+
1205
+ existingKnowledgeList.appendChild(item);
1206
+ });
1207
+
1208
+ // 添加上传表单事件监听
1209
+ document.querySelectorAll('.upload-form').forEach(form => {
1210
+ form.addEventListener('submit', function(event) {
1211
+ event.preventDefault();
1212
+ addFileToKnowledgeBase(form);
1213
+ });
1214
+ });
1215
+ }
1216
+ }
1217
+ } else {
1218
+ console.error('加载知识库失败:', result.message);
1219
+ }
1220
+ } catch (error) {
1221
+ console.error('加载知识库出错:', error);
1222
+ }
1223
+ }
1224
+
1225
+ // 创建知识库
1226
+ async function createKnowledgeBase(event) {
1227
+ event.preventDefault();
1228
+
1229
+ const nameInput = document.getElementById('knowledge-name');
1230
+ const fileInput = document.getElementById('knowledge-file');
1231
+ const progressBar = document.getElementById('upload-progress');
1232
+
1233
+ if (!nameInput.value || !fileInput.files[0]) {
1234
+ alert('请填写知识库名称并选择文件');
1235
+ return;
1236
+ }
1237
+
1238
+ // 显示进度条
1239
+ progressBar.style.display = 'block';
1240
+ progressBar.querySelector('.progress-bar').style.width = '0%';
1241
+
1242
+ const formData = new FormData();
1243
+ formData.append('name', nameInput.value);
1244
+ formData.append('file', fileInput.files[0]);
1245
+
1246
+ try {
1247
+ const response = await fetch('/api/knowledge', {
1248
+ method: 'POST',
1249
+ body: formData
1250
+ });
1251
+
1252
+ const result = await response.json();
1253
+
1254
+ if (result.success) {
1255
+ // 成功启动处理,轮询进度
1256
+ pollProgress(result.task_id, progressBar);
1257
+ } else {
1258
+ alert('创建知识库失败: ' + result.message);
1259
+ progressBar.style.display = 'none';
1260
+ }
1261
+ } catch (error) {
1262
+ console.error('创建知识库出错:', error);
1263
+ alert('创建知识库出错,请查看控制台日志');
1264
+ progressBar.style.display = 'none';
1265
+ }
1266
+ }
1267
+
1268
+ // 添加文件到知识库
1269
+ async function addFileToKnowledgeBase(form) {
1270
+ const knowledgeId = form.getAttribute('data-id');
1271
+ const fileInput = form.querySelector('input[type="file"]');
1272
+ const submitBtn = form.querySelector('button[type="submit"]');
1273
+
1274
+ if (!fileInput.files[0]) {
1275
+ alert('请选择文件');
1276
+ return;
1277
+ }
1278
+
1279
+ // 禁用提交按钮
1280
+ submitBtn.disabled = true;
1281
+ submitBtn.textContent = '上传中...';
1282
+
1283
+ const formData = new FormData();
1284
+ formData.append('file', fileInput.files[0]);
1285
+
1286
+ try {
1287
+ const response = await fetch(`/api/knowledge/${knowledgeId}/documents`, {
1288
+ method: 'POST',
1289
+ body: formData
1290
+ });
1291
+
1292
+ const result = await response.json();
1293
+
1294
+ if (result.success) {
1295
+ alert('文件上传成功,开始处理');
1296
+ // 轮询处理进度
1297
+ pollProgressSimple(result.task_id, submitBtn);
1298
+ } else {
1299
+ alert('添加文件失败: ' + result.message);
1300
+ submitBtn.disabled = false;
1301
+ submitBtn.textContent = '添加文件';
1302
+ }
1303
+ } catch (error) {
1304
+ console.error('添加文件出错:', error);
1305
+ alert('添加文件出错,请查看控制台日志');
1306
+ submitBtn.disabled = false;
1307
+ submitBtn.textContent = '添加文件';
1308
+ }
1309
+ }
1310
+
1311
+ // 删除知识库
1312
+ async function deleteKnowledgeBase(knowledgeId) {
1313
+ if (!confirm('确定要删除这个知识库吗?此操作不可恢复。')) {
1314
+ return;
1315
+ }
1316
+
1317
+ try {
1318
+ const response = await fetch(`/api/knowledge/${knowledgeId}`, {
1319
+ method: 'DELETE'
1320
+ });
1321
+
1322
+ const result = await response.json();
1323
+
1324
+ if (result.success) {
1325
+ alert('知识库删除成功!');
1326
+ loadKnowledgeBases(); // 重新加载知识库列表
1327
+ } else {
1328
+ alert('删除知识库失败: ' + result.message);
1329
+ }
1330
+ } catch (error) {
1331
+ console.error('删除知识库出错:', error);
1332
+ alert('删除知识库出错,请查看控制台日志');
1333
+ }
1334
+ }
1335
+
1336
+ // 轮询进度(带进度条显示)
1337
+ function pollProgress(taskId, progressElement) {
1338
+ const progressBar = progressElement.querySelector('.progress-bar');
1339
+
1340
+ const interval = setInterval(async () => {
1341
+ try {
1342
+ const response = await fetch(`/api/progress/${taskId}`);
1343
+ const data = await response.json();
1344
+
1345
+ if (data.success) {
1346
+ const progressData = data.data;
1347
+
1348
+ // 更新进度条
1349
+ progressBar.style.width = progressData.progress + '%';
1350
+
1351
+ // 处理完成或出错时
1352
+ if (progressData.progress >= 100 || progressData.error) {
1353
+ clearInterval(interval);
1354
+
1355
+ if (progressData.error) {
1356
+ alert('处理失败: ' + progressData.status);
1357
+ } else {
1358
+ alert(`知识库处理成功! 已处理${progressData.docCount}个文档片段`);
1359
+ }
1360
+
1361
+ // 隐藏进度条
1362
+ progressElement.style.display = 'none';
1363
+
1364
+ // 重置表单
1365
+ document.getElementById('knowledge-name').value = '';
1366
+ document.getElementById('knowledge-file').value = '';
1367
+
1368
+ // 重新加载知识库列表
1369
+ loadKnowledgeBases();
1370
+ }
1371
+ }
1372
+ } catch (error) {
1373
+ console.error('获取进度信息出错:', error);
1374
+ }
1375
+ }, 1000); // 每秒轮询一次
1376
+ }
1377
+
1378
+ // 简化版轮询进度(不显示进度条)
1379
+ function pollProgressSimple(taskId, buttonElement) {
1380
+ const originalText = buttonElement.textContent;
1381
+
1382
+ const interval = setInterval(async () => {
1383
+ try {
1384
+ const response = await fetch(`/api/progress/${taskId}`);
1385
+ const data = await response.json();
1386
+
1387
+ if (data.success) {
1388
+ const progressData = data.data;
1389
+
1390
+ // 处理完成或出错时
1391
+ if (progressData.progress >= 100 || progressData.error) {
1392
+ clearInterval(interval);
1393
+
1394
+ if (progressData.error) {
1395
+ alert('处理失败: ' + progressData.status);
1396
+ } else {
1397
+ alert(`处理成功! 已处理${progressData.docCount}个文档片段`);
1398
+ }
1399
+
1400
+ // 恢复按钮
1401
+ buttonElement.disabled = false;
1402
+ buttonElement.textContent = originalText;
1403
+
1404
+ // 重新加载知识库列表
1405
+ loadKnowledgeBases();
1406
+ }
1407
+ }
1408
+ } catch (error) {
1409
+ console.error('获取进度信息出错:', error);
1410
+ }
1411
+ }, 1000); // 每秒轮询一次
1412
+ }
1413
+
1414
+ // 使用AI生成工作流
1415
+ async function generateAIWorkflow() {
1416
+ const description = document.getElementById('agent-description').value;
1417
+ const subject = document.getElementById('agent-subject').value || document.getElementById('agent-name').value;
1418
+
1419
+ if (!description) {
1420
+ alert('请先填写Agent描述,以便AI生成工作流');
1421
+ nextTab('basic-tab');
1422
+ return;
1423
+ }
1424
+
1425
+ // 显示加载状态
1426
+ const workflowCanvas = document.getElementById('workflow-canvas');
1427
+ workflowCanvas.innerHTML = `
1428
+ <div class="text-center py-5">
1429
+ <div class="spinner-border text-primary" role="status">
1430
+ <span class="visually-hidden">生成中...</span>
1431
+ </div>
1432
+ <p class="mt-3">AI正在设计工作流,请稍候...</p>
1433
+ </div>
1434
+ `;
1435
+
1436
+ try {
1437
+ const response = await fetch('/api/agent/ai-assist', {
1438
+ method: 'POST',
1439
+ headers: { 'Content-Type': 'application/json' },
1440
+ body: JSON.stringify({
1441
+ description: description,
1442
+ subject: subject,
1443
+ plugins: selectedPlugins,
1444
+ knowledge_bases: selectedKnowledgeBases
1445
+ })
1446
+ });
1447
+
1448
+ const result = await response.json();
1449
+
1450
+ if (result.success) {
1451
+ // 保存工作流
1452
+ currentWorkflow = result.workflow;
1453
+
1454
+ // 更新代码视图
1455
+ updateWorkflowCodeView(currentWorkflow);
1456
+
1457
+ // 处理AI推荐的知识库和插件
1458
+ let recommendationHtml = '';
1459
+ let recommendationsApplied = false;
1460
+
1461
+ // 应用推荐的知识库
1462
+ if (result.recommended_knowledge_bases && result.recommended_knowledge_bases.length > 0) {
1463
+ // 更新选择的知识库
1464
+ selectedKnowledgeBases = result.recommended_knowledge_bases;
1465
+
1466
+ // 更新UI中的选择状态
1467
+ document.querySelectorAll('#knowledge-list .plugin-option').forEach(item => {
1468
+ const kbId = item.getAttribute('data-id');
1469
+ const selected = selectedKnowledgeBases.includes(kbId);
1470
+
1471
+ item.classList.toggle('selected', selected);
1472
+ const checkbox = item.querySelector('input[type="checkbox"]');
1473
+ if (checkbox) checkbox.checked = selected;
1474
+ });
1475
+
1476
+ recommendationsApplied = true;
1477
+ recommendationHtml += `
1478
+ <div class="mb-3">
1479
+ <h6>AI推荐的知识库:</h6>
1480
+ <ul>
1481
+ ${selectedKnowledgeBases.map(kb => `<li>${kb}</li>`).join('')}
1482
+ </ul>
1483
+ </div>
1484
+ `;
1485
+ }
1486
+
1487
+ // 应用推荐的插件
1488
+ if (result.recommended_plugins && result.recommended_plugins.length > 0) {
1489
+ // 更新选择的插件
1490
+ selectedPlugins = result.recommended_plugins;
1491
+
1492
+ // 更新UI中的选择状态
1493
+ document.querySelectorAll('[data-plugin]').forEach(item => {
1494
+ const pluginId = item.getAttribute('data-plugin');
1495
+ const selected = selectedPlugins.includes(pluginId);
1496
+
1497
+ item.classList.toggle('selected', selected);
1498
+ const checkbox = item.querySelector('input[type="checkbox"]');
1499
+ if (checkbox) checkbox.checked = selected;
1500
+ });
1501
+
1502
+ recommendationsApplied = true;
1503
+ recommendationHtml += `
1504
+ <div class="mb-3">
1505
+ <h6>AI推荐的插件:</h6>
1506
+ <ul>
1507
+ ${selectedPlugins.map(plugin => {
1508
+ let pluginName = plugin;
1509
+ if (plugin === 'code') pluginName = '代码执行';
1510
+ if (plugin === 'visualization') pluginName = '3D可视化';
1511
+ if (plugin === 'mindmap') pluginName = '思维导图';
1512
+ return `<li>${pluginName}</li>`;
1513
+ }).join('')}
1514
+ </ul>
1515
+ </div>
1516
+ `;
1517
+ }
1518
+
1519
+ // 显示推荐结果
1520
+ if (recommendationsApplied) {
1521
+ workflowCanvas.innerHTML = `
1522
+ <div class="alert alert-success mb-3">
1523
+ <i class="bi bi-check-circle-fill me-2"></i>
1524
+ AI已成功设计工作流并推荐了知识库和插件
1525
+ </div>
1526
+ ${recommendationHtml}
1527
+ <div id="workflow-visualization-container"></div>
1528
+ `;
1529
+ } else {
1530
+ workflowCanvas.innerHTML = `
1531
+ <div class="alert alert-info mb-3">
1532
+ <i class="bi bi-info-circle-fill me-2"></i>
1533
+ AI已设计工作流但未推荐额外的知识库或插件
1534
+ </div>
1535
+ <div id="workflow-visualization-container"></div>
1536
+ `;
1537
+ }
1538
+
1539
+ // 渲染工作流视图
1540
+ setTimeout(() => {
1541
+ renderWorkflowVisualization(currentWorkflow, 'workflow-canvas');
1542
+ }, 200);
1543
+
1544
+ } else {
1545
+ workflowCanvas.innerHTML = `
1546
+ <div class="alert alert-danger m-3">
1547
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1548
+ 生成工作流失败: ${result.message}
1549
+ </div>
1550
+ `;
1551
+ }
1552
+ } catch (error) {
1553
+ console.error('生成工作流出错:', error);
1554
+ workflowCanvas.innerHTML = `
1555
+ <div class="alert alert-danger m-3">
1556
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
1557
+ 生成工作流时发生错误,请重试
1558
+ </div>
1559
+ `;
1560
+ }
1561
+ }
1562
+
1563
+ // 更新工作流代码视图
1564
+ function updateWorkflowCodeView(workflow) {
1565
+ const workflowCodeElement = document.getElementById('workflow-code');
1566
+ if (workflowCodeElement) {
1567
+ workflowCodeElement.textContent = JSON.stringify(workflow, null, 2);
1568
+ }
1569
+ }
1570
+
1571
+ // 创建Agent
1572
+ async function createAgent() {
1573
+ const name = document.getElementById('agent-name').value;
1574
+ const description = document.getElementById('agent-description').value;
1575
+ const subject = document.getElementById('agent-subject').value || name; // 使用课程名称,如果未填写则使用Agent名称
1576
+ const instructor = document.getElementById('agent-instructor').value || '教师'; // 使用指导教师,如果未填写则使用默认值
1577
+ const type = document.getElementById('agent-type').value;
1578
+
1579
+ if (!name) {
1580
+ alert('请填写Agent名称');
1581
+ nextTab('basic-tab');
1582
+ return;
1583
+ }
1584
+
1585
+ // 准备Agent数据
1586
+ const agentData = {
1587
+ name: name,
1588
+ description: description,
1589
+ subject: subject,
1590
+ instructor: instructor,
1591
+ type: type,
1592
+ plugins: selectedPlugins,
1593
+ knowledge_bases: selectedKnowledgeBases,
1594
+ workflow: currentWorkflow
1595
+ };
1596
+
1597
+ try {
1598
+ const response = await fetch('/api/agent/create', {
1599
+ method: 'POST',
1600
+ headers: { 'Content-Type': 'application/json' },
1601
+ body: JSON.stringify(agentData)
1602
+ });
1603
+
1604
+ const result = await response.json();
1605
+
1606
+ if (result.success) {
1607
+ alert(`Agent '${name}' 创建成功!`);
1608
+
1609
+ // 重置表单
1610
+ document.getElementById('agent-name').value = '';
1611
+ document.getElementById('agent-description').value = '';
1612
+ document.getElementById('agent-subject').value = '';
1613
+ document.getElementById('agent-instructor').value = '';
1614
+ document.getElementById('agent-type').value = 'educational';
1615
+ selectedPlugins = [];
1616
+ selectedKnowledgeBases = [];
1617
+ currentWorkflow = { nodes: [], edges: [] };
1618
+
1619
+ // 重置插件选择
1620
+ document.querySelectorAll('.plugin-option').forEach(item => {
1621
+ item.classList.remove('selected');
1622
+ item.querySelector('input[type="checkbox"]').checked = false;
1623
+ });
1624
+
1625
+ // 重置工作流视图
1626
+ document.getElementById('workflow-canvas').innerHTML = `
1627
+ <div class="p-4 text-center text-muted">
1628
+ 点击"使用AI自动设计工作流"按钮,或手动设计工作流
1629
+ </div>
1630
+ `;
1631
+
1632
+ // 更新代码视图
1633
+ updateWorkflowCodeView(currentWorkflow);
1634
+
1635
+ // 切换到Agent列表页面
1636
+ switchSection('agent-list');
1637
+
1638
+ // 重新加载Agent列表
1639
+ loadAgents();
1640
+ } else {
1641
+ alert('创建Agent失败: ' + result.message);
1642
+ }
1643
+ } catch (error) {
1644
+ console.error('创建Agent出错:', error);
1645
+ alert('创建Agent出错,请查看控制台日志');
1646
+ }
1647
+ }
1648
+
1649
+ // 加载Agent列表
1650
+ async function loadAgents() {
1651
+ try {
1652
+ const response = await fetch('/api/agent/list');
1653
+ const result = await response.json();
1654
+
1655
+ if (result.success) {
1656
+ allAgents = result.agents || [];
1657
+
1658
+ // 更新仪表盘计数
1659
+ document.getElementById('agent-count').textContent = allAgents.length;
1660
+
1661
+ // 计算分发链接总数
1662
+ let distributionCount = 0;
1663
+ allAgents.forEach(agent => {
1664
+ distributionCount += agent.distribution_count || 0;
1665
+ });
1666
+ document.getElementById('distribution-count').textContent = distributionCount;
1667
+
1668
+ // 更新最近创建的Agent
1669
+ updateRecentAgents();
1670
+
1671
+ // 更新Agent列表页面
1672
+ updateAgentListPage();
1673
+ } else {
1674
+ console.error('加载Agent列表失败:', result.message);
1675
+ }
1676
+ } catch (error) {
1677
+ console.error('加载Agent列表出错:', error);
1678
+ }
1679
+ }
1680
+
1681
+ // 更新最近创建的Agent列表
1682
+ function updateRecentAgents() {
1683
+ const recentAgentsElement = document.getElementById('recent-agents');
1684
+
1685
+ if (recentAgentsElement) {
1686
+ if (allAgents.length === 0) {
1687
+ recentAgentsElement.innerHTML = `
1688
+ <div class="alert alert-info">
1689
+ <i class="bi bi-info-circle me-2"></i>
1690
+ 您还没有创建Agent
1691
+ </div>
1692
+ <div class="text-center">
1693
+ <button class="btn btn-primary" onclick="switchSection('create-agent')">
1694
+ 创建第一个Agent
1695
+ </button>
1696
+ </div>
1697
+ `;
1698
+ } else {
1699
+ // 只显示最近3个Agent
1700
+ const recentAgents = allAgents.slice(0, 3);
1701
+
1702
+ recentAgentsElement.innerHTML = '';
1703
+
1704
+ recentAgents.forEach(agent => {
1705
+ const item = document.createElement('div');
1706
+ item.className = 'agent-item';
1707
+
1708
+ const createdDate = new Date(agent.created_at * 1000);
1709
+ const formattedDate = createdDate.toLocaleDateString();
1710
+
1711
+ // 添加学科和指导教师信息
1712
+ const subjectInfo = agent.subject ? `<small class="text-muted me-3"><i class="bi bi-book me-1"></i>${agent.subject}</small>` : '';
1713
+ const instructorInfo = agent.instructor ? `<small class="text-muted"><i class="bi bi-person me-1"></i>${agent.instructor}</small>` : '';
1714
+
1715
+ item.innerHTML = `
1716
+ <div class="header">
1717
+ <h5 class="title">${agent.name}</h5>
1718
+ <span class="badge badge-primary">${agent.type || 'educational'}</span>
1719
+ </div>
1720
+ <div class="description">${agent.description || '暂无描述'}</div>
1721
+ <div class="mt-2">
1722
+ ${subjectInfo}
1723
+ ${instructorInfo}
1724
+ </div>
1725
+ <div class="meta">
1726
+ <div>
1727
+ <i class="bi bi-calendar3"></i>
1728
+ ${formattedDate}
1729
+ </div>
1730
+ <div>
1731
+ <i class="bi bi-share"></i>
1732
+ ${agent.distribution_count || 0}个分发
1733
+ </div>
1734
+ </div>
1735
+ `;
1736
+
1737
+ recentAgentsElement.appendChild(item);
1738
+ });
1739
+ }
1740
+ }
1741
+ }
1742
+
1743
+ // 更新Agent列表页面
1744
+ function updateAgentListPage() {
1745
+ const agentListContainer = document.getElementById('agent-list-container');
1746
+
1747
+ if (agentListContainer) {
1748
+ if (allAgents.length === 0) {
1749
+ agentListContainer.innerHTML = `
1750
+ <div class="alert alert-info">
1751
+ <i class="bi bi-info-circle me-2"></i>
1752
+ 您还没有创建Agent
1753
+ </div>
1754
+ <div class="text-center">
1755
+ <button class="btn btn-primary" onclick="switchSection('create-agent')">
1756
+ 创建第一个Agent
1757
+ </button>
1758
+ </div>
1759
+ `;
1760
+ } else {
1761
+ agentListContainer.innerHTML = '';
1762
+
1763
+ allAgents.forEach(agent => {
1764
+ const item = document.createElement('div');
1765
+ item.className = 'agent-item';
1766
+
1767
+ const createdDate = new Date(agent.created_at * 1000);
1768
+ const formattedDate = createdDate.toLocaleDateString();
1769
+
1770
+ // 构建插件和知识库标签
1771
+ let pluginsHtml = '';
1772
+ if (agent.plugins && agent.plugins.length > 0) {
1773
+ pluginsHtml = '<div class="mt-2"><small class="text-muted me-2">插件:</small>';
1774
+ agent.plugins.forEach(plugin => {
1775
+ let pluginName = '未知';
1776
+ let pluginIcon = 'puzzle';
1777
+
1778
+ if (plugin === 'code') {
1779
+ pluginName = '代码执行';
1780
+ pluginIcon = 'code-square';
1781
+ } else if (plugin === 'visualization') {
1782
+ pluginName = '3D可视化';
1783
+ pluginIcon = 'bar-chart';
1784
+ } else if (plugin === 'mindmap') {
1785
+ pluginName = '思维导图';
1786
+ pluginIcon = 'diagram-3';
1787
+ }
1788
+
1789
+ pluginsHtml += `
1790
+ <span class="badge bg-light text-dark me-1">
1791
+ <i class="bi bi-${pluginIcon} me-1"></i>
1792
+ ${pluginName}
1793
+ </span>
1794
+ `;
1795
+ });
1796
+ pluginsHtml += '</div>';
1797
+ }
1798
+
1799
+ let knowledgeHtml = '';
1800
+ if (agent.knowledge_bases && agent.knowledge_bases.length > 0) {
1801
+ knowledgeHtml = '<div class="mt-2"><small class="text-muted me-2">知识库:</small>';
1802
+ agent.knowledge_bases.forEach(kb => {
1803
+ // 查找知识库名称
1804
+ const knowledge = allKnowledgeBases.find(k => k.id === kb);
1805
+ const knowledgeName = knowledge ? knowledge.name : kb;
1806
+
1807
+ knowledgeHtml += `
1808
+ <span class="badge bg-light text-dark me-1">
1809
+ <i class="bi bi-journal-text me-1"></i>
1810
+ ${knowledgeName}
1811
+ </span>
1812
+ `;
1813
+ });
1814
+ knowledgeHtml += '</div>';
1815
+ }
1816
+
1817
+ // 添加学科和指导教师信息
1818
+ const subjectInfo = agent.subject ? `<small class="text-muted me-3"><i class="bi bi-book me-1"></i>${agent.subject}</small>` : '';
1819
+ const instructorInfo = agent.instructor ? `<small class="text-muted"><i class="bi bi-person me-1"></i>${agent.instructor}</small>` : '';
1820
+
1821
+ item.innerHTML = `
1822
+ <div class="header">
1823
+ <h5 class="title">${agent.name}</h5>
1824
+ <div>
1825
+ <button class="btn btn-sm btn-primary me-1" onclick="showCreateDistributionModal('${agent.id}')">
1826
+ <i class="bi bi-share"></i> 分发
1827
+ </button>
1828
+ <button class="btn btn-sm btn-outline-secondary me-1" onclick="showAgentDetail('${agent.id}')">
1829
+ <i class="bi bi-info-circle"></i> 详情
1830
+ </button>
1831
+ <button class="btn btn-sm btn-outline-danger" onclick="deleteAgent('${agent.id}')">
1832
+ <i class="bi bi-trash"></i>
1833
+ </button>
1834
+ </div>
1835
+ </div>
1836
+ <div class="description">${agent.description || '暂无描述'}</div>
1837
+ <div class="mt-2">
1838
+ ${subjectInfo}
1839
+ ${instructorInfo}
1840
+ </div>
1841
+ ${pluginsHtml}
1842
+ ${knowledgeHtml}
1843
+ <div class="meta">
1844
+ <div>
1845
+ <i class="bi bi-calendar3"></i>
1846
+ ${formattedDate}
1847
+ </div>
1848
+ <div>
1849
+ <i class="bi bi-share"></i>
1850
+ ${agent.distribution_count || 0}个分发
1851
+ </div>
1852
+ <div>
1853
+ <i class="bi bi-eye"></i>
1854
+ ${agent.usage_count || 0}次使用
1855
+ </div>
1856
+ </div>
1857
+ `;
1858
+
1859
+ agentListContainer.appendChild(item);
1860
+ });
1861
+ }
1862
+ }
1863
+ }
1864
+
1865
+ // 显示Agent详情
1866
+ async function showAgentDetail(agentId) {
1867
+ try {
1868
+ // 获取Agent详细信息
1869
+ const response = await fetch(`/api/agent/${agentId}`);
1870
+ const result = await response.json();
1871
+
1872
+ if (result.success) {
1873
+ const agent = result.agent;
1874
+
1875
+ // 更新模态框内容
1876
+ const modalTitle = document.getElementById('agent-detail-title');
1877
+ const modalBody = document.getElementById('agent-detail-body');
1878
+
1879
+ modalTitle.textContent = agent.name;
1880
+
1881
+ // 格式化创建时间
1882
+ const createdDate = new Date(agent.created_at * 1000);
1883
+ const formattedDate = createdDate.toLocaleString();
1884
+
1885
+ // 构建模态框内容
1886
+ modalBody.innerHTML = `
1887
+ <div class="mb-3">
1888
+ <h6>描述</h6>
1889
+ <p>${agent.description || '暂无描述'}</p>
1890
+ </div>
1891
+
1892
+ <div class="row mb-3">
1893
+ <div class="col-md-6">
1894
+ <h6>学科/课程</h6>
1895
+ <p>${agent.subject || agent.name}</p>
1896
+ </div>
1897
+ <div class="col-md-6">
1898
+ <h6>指导教师</h6>
1899
+ <p>${agent.instructor || '教师'}</p>
1900
+ </div>
1901
+ </div>
1902
+
1903
+ <div class="row mb-3">
1904
+ <div class="col-md-6">
1905
+ <h6>创建时间</h6>
1906
+ <p>${formattedDate}</p>
1907
+ </div>
1908
+ <div class="col-md-6">
1909
+ <h6>使用次数</h6>
1910
+ <p>${agent.stats ? agent.stats.usage_count || 0 : 0}次</p>
1911
+ </div>
1912
+ </div>
1913
+
1914
+ <div class="mb-3">
1915
+ <h6>插件</h6>
1916
+ ${getPluginsHtml(agent.plugins)}
1917
+ </div>
1918
+
1919
+ <div class="mb-3">
1920
+ <h6>知识库</h6>
1921
+ ${getKnowledgeBasesHtml(agent.knowledge_bases)}
1922
+ </div>
1923
+
1924
+ <div class="mb-3">
1925
+ <h6>分发链接</h6>
1926
+ ${getDistributionsHtml(agent.distributions, agent.id)}
1927
+ </div>
1928
+
1929
+ <div class="mb-4">
1930
+ <h6>工作流配置</h6>
1931
+ <ul class="nav nav-tabs mb-3" id="detailWorkflowTabs" role="tablist">
1932
+ <li class="nav-item" role="presentation">
1933
+ <button class="nav-link active" id="detail-visual-tab" data-bs-toggle="tab" data-bs-target="#detail-visual-view" type="button" role="tab">可视化视图</button>
1934
+ </li>
1935
+ <li class="nav-item" role="presentation">
1936
+ <button class="nav-link" id="detail-code-tab" data-bs-toggle="tab" data-bs-target="#detail-code-view" type="button" role="tab">代码视图</button>
1937
+ </li>
1938
+ </ul>
1939
+ <div class="tab-content" id="detailWorkflowContent">
1940
+ <div class="tab-pane fade show active" id="detail-visual-view" role="tabpanel">
1941
+ <div id="detail-workflow-canvas" class="workflow-canvas"></div>
1942
+ </div>
1943
+ <div class="tab-pane fade" id="detail-code-view" role="tabpanel">
1944
+ <div class="p-3 bg-light border rounded">
1945
+ <pre class="mb-0">${JSON.stringify(agent.workflow, null, 2)}</pre>
1946
+ </div>
1947
+ </div>
1948
+ </div>
1949
+ </div>
1950
+ `;
1951
+
1952
+ // 存储工作流数据
1953
+ modalBody.dataset.agentWorkflow = JSON.stringify(agent.workflow);
1954
+
1955
+ // 显示模态框
1956
+ const modal = new bootstrap.Modal(document.getElementById('agentDetailModal'));
1957
+ modal.show();
1958
+
1959
+ // 在模态框显示后初始化工作流标签事件
1960
+ document.getElementById('agentDetailModal').addEventListener('shown.bs.modal', function() {
1961
+ // 初始化详情模态框内的标签切换
1962
+ document.querySelectorAll('#detailWorkflowTabs .nav-link').forEach(tab => {
1963
+ tab.addEventListener('click', function(event) {
1964
+ event.preventDefault();
1965
+
1966
+ const targetId = this.getAttribute('data-bs-target');
1967
+ const targetPane = document.querySelector(targetId);
1968
+
1969
+ // 切换标签激活状态
1970
+ document.querySelectorAll('#detailWorkflowTabs .nav-link').forEach(t =>
1971
+ t.classList.remove('active'));
1972
+ this.classList.add('active');
1973
+
1974
+ // 切换内容区域显示状态
1975
+ document.querySelectorAll('#detailWorkflowContent .tab-pane').forEach(p =>
1976
+ p.classList.remove('show', 'active'));
1977
+ targetPane.classList.add('show', 'active');
1978
+
1979
+ // 如果切换到可视化视图,重新渲染工作流
1980
+ if (targetId === '#detail-visual-view') {
1981
+ setTimeout(() => {
1982
+ const workflow = JSON.parse(modalBody.dataset.agentWorkflow);
1983
+ renderWorkflowVisualization(workflow, 'detail-workflow-canvas');
1984
+ }, 200);
1985
+ }
1986
+ });
1987
+ });
1988
+
1989
+ // 立即渲染工作流可视化
1990
+ setTimeout(() => {
1991
+ const workflow = JSON.parse(modalBody.dataset.agentWorkflow);
1992
+ renderWorkflowVisualization(workflow, 'detail-workflow-canvas');
1993
+ }, 200);
1994
+ });
1995
+ } else {
1996
+ alert('获取Agent详情失败: ' + result.message);
1997
+ }
1998
+ } catch (error) {
1999
+ console.error('获取Agent详情出错:', error);
2000
+ alert('获取Agent详情出错,请查看控制台日志');
2001
+ }
2002
+ }
2003
+
2004
+ // 辅助函数,用于生成插件HTML
2005
+ function getPluginsHtml(plugins) {
2006
+ if (!plugins || plugins.length === 0) {
2007
+ return '<p>无</p>';
2008
+ }
2009
+
2010
+ let html = '<ul>';
2011
+ plugins.forEach(plugin => {
2012
+ let pluginName = '未知';
2013
+
2014
+ if (plugin === 'code') {
2015
+ pluginName = '代码执行';
2016
+ } else if (plugin === 'visualization') {
2017
+ pluginName = '3D可视化';
2018
+ } else if (plugin === 'mindmap') {
2019
+ pluginName = '思维导图';
2020
+ }
2021
+
2022
+ html += `<li>${pluginName}</li>`;
2023
+ });
2024
+ html += '</ul>';
2025
+
2026
+ return html;
2027
+ }
2028
+
2029
+ // 辅助函数,用于生成知识库HTML
2030
+ function getKnowledgeBasesHtml(knowledgeBases) {
2031
+ if (!knowledgeBases || knowledgeBases.length === 0) {
2032
+ return '<p>无</p>';
2033
+ }
2034
+
2035
+ let html = '<ul>';
2036
+ knowledgeBases.forEach(kb => {
2037
+ // 查找知识库名称
2038
+ const knowledge = allKnowledgeBases.find(k => k.id === kb);
2039
+ const knowledgeName = knowledge ? knowledge.name : kb;
2040
+
2041
+ html += `<li>${knowledgeName}</li>`;
2042
+ });
2043
+ html += '</ul>';
2044
+
2045
+ return html;
2046
+ }
2047
+
2048
+ // 辅助函数,用于生成分发链接HTML
2049
+ function getDistributionsHtml(distributions, agentId) {
2050
+ if (!distributions || distributions.length === 0) {
2051
+ return '<p>无</p>';
2052
+ }
2053
+
2054
+ let html = '<div class="list-group">';
2055
+ distributions.forEach(dist => {
2056
+ const expiryText = dist.expires_at > 0
2057
+ ? `过期时间: ${new Date(dist.expires_at * 1000).toLocaleString()}`
2058
+ : '永不过期';
2059
+
2060
+ const url = `/student/${agentId}?token=${dist.token}`;
2061
+
2062
+ html += `
2063
+ <div class="distribution-link">
2064
+ <div>
2065
+ <div class="mb-1">${url}</div>
2066
+ <small class="text-muted">${expiryText} | 使用次数: ${dist.usage_count || 0}</small>
2067
+ </div>
2068
+ <button class="btn btn-sm btn-outline-primary" onclick="copyToClipboard('${url}')">
2069
+ <i class="bi bi-clipboard"></i> 复制
2070
+ </button>
2071
+ </div>
2072
+ `;
2073
+ });
2074
+ html += '</div>';
2075
+
2076
+ return html;
2077
+ }
2078
+
2079
+ // 删除Agent
2080
+ async function deleteAgent(agentId) {
2081
+ if (!confirm('确定要删除这个Agent吗?此操作不可恢复。')) {
2082
+ return;
2083
+ }
2084
+
2085
+ try {
2086
+ const response = await fetch(`/api/agent/${agentId}`, {
2087
+ method: 'DELETE'
2088
+ });
2089
+
2090
+ const result = await response.json();
2091
+
2092
+ if (result.success) {
2093
+ alert('Agent删除成功!');
2094
+ loadAgents(); // 重新加载Agent列表
2095
+ } else {
2096
+ alert('删除Agent失败: ' + result.message);
2097
+ }
2098
+ } catch (error) {
2099
+ console.error('删除Agent出错:', error);
2100
+ alert('删除Agent出错,请查看控制台日志');
2101
+ }
2102
+ }
2103
+
2104
+ // 显示创建分发链接模态框
2105
+ function showCreateDistributionModal(agentId) {
2106
+ document.getElementById('distribution-agent-id').value = agentId;
2107
+
2108
+ // 显示模态框
2109
+ const modal = new bootstrap.Modal(document.getElementById('createDistributionModal'));
2110
+ modal.show();
2111
+ }
2112
+
2113
+ // 创建分发链接
2114
+ async function createDistribution() {
2115
+ const agentId = document.getElementById('distribution-agent-id').value;
2116
+ const expiresIn = parseInt(document.getElementById('distribution-expires').value);
2117
+
2118
+ if (!agentId) {
2119
+ alert('Agent ID不能为空');
2120
+ return;
2121
+ }
2122
+
2123
+ try {
2124
+ const response = await fetch(`/api/agent/${agentId}/distribute`, {
2125
+ method: 'POST',
2126
+ headers: { 'Content-Type': 'application/json' },
2127
+ body: JSON.stringify({ expires_in: expiresIn })
2128
+ });
2129
+
2130
+ const result = await response.json();
2131
+
2132
+ if (result.success) {
2133
+ // 隐藏创建模态框
2134
+ const createModal = bootstrap.Modal.getInstance(document.getElementById('createDistributionModal'));
2135
+ createModal.hide();
2136
+
2137
+ // 更新分发链接输入框
2138
+ const fullUrl = window.location.origin + result.distribution.link;
2139
+ document.getElementById('distribution-link').value = fullUrl;
2140
+
2141
+ // 显示分发链接模态框
2142
+ const showModal = new bootstrap.Modal(document.getElementById('showDistributionModal'));
2143
+ showModal.show();
2144
+
2145
+ // 重新加载Agent列表
2146
+ loadAgents();
2147
+ } else {
2148
+ alert('创建分发链接失败: ' + result.message);
2149
+ }
2150
+ } catch (error) {
2151
+ console.error('创建分发链接出错:', error);
2152
+ alert('创建分发链接出错,请查看控制台日志');
2153
+ }
2154
+ }
2155
+
2156
+ // 复制到剪贴板
2157
+ function copyToClipboard(textOrElementId) {
2158
+ let text;
2159
+
2160
+ if (textOrElementId.startsWith('/') || textOrElementId.startsWith('http')) {
2161
+ // 直接传入的文本
2162
+ text = textOrElementId;
2163
+ } else {
2164
+ // 传入的是元素ID
2165
+ const element = document.getElementById(textOrElementId);
2166
+ text = element.value || element.textContent;
2167
+ }
2168
+
2169
+ navigator.clipboard.writeText(text).then(() => {
2170
+ alert('已复制到剪贴板!');
2171
+ }).catch(err => {
2172
+ console.error('复制失败:', err);
2173
+ });
2174
+ }
2175
+
2176
+ // 根据节点类型获取图标
2177
+ function getNodeIcon(type) {
2178
+ switch (type) {
2179
+ case 'intent_recognition':
2180
+ return 'AI';
2181
+ case 'knowledge_query':
2182
+ return 'KB';
2183
+ case 'plugin_call':
2184
+ return 'API';
2185
+ case 'generate_response':
2186
+ return 'AI';
2187
+ default:
2188
+ return '';
2189
+ }
2190
+ }
2191
+
2192
+ // 渲染工作流可视化
2193
+ function renderWorkflowVisualization(workflow, containerId) {
2194
+ const container = document.getElementById(containerId);
2195
+ if (!container) return;
2196
+
2197
+ // 清空容器
2198
+ container.innerHTML = '';
2199
+
2200
+ // 如果没有工作流数据,显示提示
2201
+ if (!workflow || !workflow.nodes || workflow.nodes.length === 0) {
2202
+ container.innerHTML = `
2203
+ <div class="p-4 text-center text-muted">
2204
+ 暂无工作流数据
2205
+ </div>
2206
+ `;
2207
+ return;
2208
+ }
2209
+
2210
+ // 创建或重置jsPlumb实例
2211
+ if (jsPlumbWorkflowInstance) {
2212
+ jsPlumbWorkflowInstance.reset();
2213
+ }
2214
+
2215
+ jsPlumbWorkflowInstance = jsPlumb.getInstance({
2216
+ Endpoint: ["Dot", { radius: 4 }],
2217
+ Connector: ["Bezier", { curviness: 50 }],
2218
+ PaintStyle: { stroke: "#4361ee", strokeWidth: 2 },
2219
+ HoverPaintStyle: { stroke: "#3a56e4", strokeWidth: 3 },
2220
+ ConnectionOverlays: [
2221
+ ["Arrow", { location: 1, width: 10, length: 10, foldback: 0.7 }]
2222
+ ],
2223
+ Container: containerId
2224
+ });
2225
+
2226
+ // 创建节点
2227
+ workflow.nodes.forEach(node => {
2228
+ const nodeEl = document.createElement('div');
2229
+ nodeEl.id = node.id;
2230
+ nodeEl.className = `workflow-node node-type-${node.type}`;
2231
+ nodeEl.innerHTML = `
2232
+ <div class="node-icon">${getNodeIcon(node.type)}</div>
2233
+ <div class="node-content">${node.data.name}</div>
2234
+ `;
2235
+
2236
+ container.appendChild(nodeEl);
2237
+ });
2238
+
2239
+ // 计算节点依赖关系
2240
+ const dependencies = {};
2241
+ const dependents = {};
2242
+
2243
+ workflow.nodes.forEach(node => {
2244
+ dependencies[node.id] = [];
2245
+ dependents[node.id] = [];
2246
+ });
2247
+
2248
+ workflow.edges.forEach(edge => {
2249
+ dependencies[edge.target].push(edge.source);
2250
+ dependents[edge.source].push(edge.target);
2251
+ });
2252
+
2253
+ // 计算层级
2254
+ const nodeLevels = {};
2255
+ const queue = [];
2256
+
2257
+ // 找到没有依赖的节点(入度为0的节点)
2258
+ workflow.nodes.forEach(node => {
2259
+ if (dependencies[node.id].length === 0) {
2260
+ nodeLevels[node.id] = 0;
2261
+ queue.push(node.id);
2262
+ }
2263
+ });
2264
+
2265
+ // BFS计算层级
2266
+ while (queue.length > 0) {
2267
+ const currentId = queue.shift();
2268
+ const currentLevel = nodeLevels[currentId];
2269
+
2270
+ dependents[currentId].forEach(dependentId => {
2271
+ const allDependenciesProcessed = dependencies[dependentId].every(
2272
+ depId => nodeLevels[depId] !== undefined
2273
+ );
2274
+
2275
+ if (allDependenciesProcessed) {
2276
+ const maxDependencyLevel = Math.max(
2277
+ ...dependencies[dependentId].map(depId => nodeLevels[depId])
2278
+ );
2279
+ nodeLevels[dependentId] = maxDependencyLevel + 1;
2280
+ queue.push(dependentId);
2281
+ }
2282
+ });
2283
+ }
2284
+
2285
+ // 按层级对节点进行布局
2286
+ const levelGroups = {};
2287
+ Object.keys(nodeLevels).forEach(nodeId => {
2288
+ const level = nodeLevels[nodeId];
2289
+ if (!levelGroups[level]) {
2290
+ levelGroups[level] = [];
2291
+ }
2292
+ levelGroups[level].push(nodeId);
2293
+ });
2294
+
2295
+ const containerWidth = container.clientWidth;
2296
+ const containerHeight = container.clientHeight;
2297
+ const levelCount = Object.keys(levelGroups).length;
2298
+ const levelHeight = containerHeight / (levelCount > 1 ? levelCount : 2);
2299
+
2300
+ Object.entries(levelGroups).forEach(([level, nodeIds]) => {
2301
+ const levelIndex = parseInt(level);
2302
+ const nodeCount = nodeIds.length;
2303
+ const nodeWidth = containerWidth / (nodeCount + 1);
2304
+
2305
+ nodeIds.forEach((nodeId, index) => {
2306
+ const node = document.getElementById(nodeId);
2307
+ if (node) {
2308
+ node.style.top = `${levelIndex * levelHeight + 50}px`;
2309
+ node.style.left = `${(index + 1) * nodeWidth - (node.clientWidth / 2)}px`;
2310
+ }
2311
+ });
2312
+ });
2313
+
2314
+ // 为每个节点创建可拖动功能
2315
+ workflow.nodes.forEach(node => {
2316
+ const nodeEl = document.getElementById(node.id);
2317
+ if (nodeEl) {
2318
+ jsPlumbWorkflowInstance.draggable(nodeEl, {
2319
+ containment: container
2320
+ });
2321
+ }
2322
+ });
2323
+
2324
+ // 添加连接
2325
+ setTimeout(() => {
2326
+ workflow.edges.forEach(edge => {
2327
+ const sourceEl = document.getElementById(edge.source);
2328
+ const targetEl = document.getElementById(edge.target);
2329
+
2330
+ if (sourceEl && targetEl) {
2331
+ const connection = jsPlumbWorkflowInstance.connect({
2332
+ source: sourceEl,
2333
+ target: targetEl,
2334
+ anchors: ["Bottom", "Top"],
2335
+ connector: ["Bezier", { curviness: 50 }],
2336
+ paintStyle: { stroke: "#4361ee", strokeWidth: 2 },
2337
+ endpoint: "Blank",
2338
+ overlays: [
2339
+ ["Arrow", { location: 1, width: 10, length: 10, foldback: 0.7 }]
2340
+ ]
2341
+ });
2342
+
2343
+ // 添加连接标签
2344
+ if (edge.condition) {
2345
+ connection.addOverlay([
2346
+ "Label", {
2347
+ label: edge.condition,
2348
+ cssClass: "connection-label",
2349
+ location: 0.5
2350
+ }
2351
+ ]);
2352
+ }
2353
+ }
2354
+ });
2355
+
2356
+ // 刷新所有连接
2357
+ jsPlumbWorkflowInstance.repaintEverything();
2358
+ }, 100);
2359
+ }
2360
+ </script>
2361
+ </body>
2362
+ </html>
templates/student.html ADDED
@@ -0,0 +1,909 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI学习助手</title>
7
+ <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
+ <style>
10
+ :root {
11
+ --primary-color: #4361ee;
12
+ --secondary-color: #3f37c9;
13
+ --accent-color: #4cc9f0;
14
+ --success-color: #4caf50;
15
+ --warning-color: #ff9800;
16
+ --danger-color: #f44336;
17
+ --light-color: #f8f9fa;
18
+ --dark-color: #212529;
19
+ --border-color: #dee2e6;
20
+ --border-radius: 0.375rem;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
25
+ margin: 0;
26
+ padding: 0;
27
+ height: 100vh;
28
+ background-color: #f5f7fa;
29
+ color: #333;
30
+ display: flex;
31
+ flex-direction: column;
32
+ }
33
+
34
+ .header {
35
+ background-color: #fff;
36
+ border-bottom: 1px solid var(--border-color);
37
+ padding: 1rem;
38
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
39
+ }
40
+
41
+ .header-content {
42
+ max-width: 1600px;
43
+ margin: 0 auto;
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ }
48
+
49
+ .header h1 {
50
+ margin: 0;
51
+ font-size: 1.5rem;
52
+ color: var(--primary-color);
53
+ }
54
+
55
+ .main-container {
56
+ flex: 1;
57
+ display: flex;
58
+ max-width: 1600px;
59
+ margin: 0 auto;
60
+ padding: 1rem;
61
+ width: 100%;
62
+ box-sizing: border-box;
63
+ height: calc(100vh - 70px); /* Header height + padding */
64
+ overflow: hidden;
65
+ }
66
+
67
+ .chat-container {
68
+ flex: 1;
69
+ display: flex;
70
+ flex-direction: column;
71
+ background-color: #fff;
72
+ border-radius: var(--border-radius);
73
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
74
+ border: 1px solid var(--border-color);
75
+ overflow: hidden;
76
+ height: 100%;
77
+ max-width: 800px;
78
+ margin: 0 auto;
79
+ }
80
+
81
+ /* When plugins are active, adjust the chat container */
82
+ .main-container.with-plugin .chat-container {
83
+ margin: 0;
84
+ max-width: 350px;
85
+ }
86
+
87
+ .chat-messages {
88
+ flex: 1;
89
+ overflow-y: auto;
90
+ padding: 1rem;
91
+ display: flex;
92
+ flex-direction: column;
93
+ }
94
+
95
+ .message {
96
+ max-width: 90%;
97
+ margin-bottom: 1rem;
98
+ padding: 0.75rem 1rem;
99
+ border-radius: var(--border-radius);
100
+ position: relative;
101
+ }
102
+
103
+ .user-message {
104
+ background-color: #e3f2fd;
105
+ align-self: flex-end;
106
+ color: #0d47a1;
107
+ }
108
+
109
+ .bot-message {
110
+ background-color: #f5f5f5;
111
+ align-self: flex-start;
112
+ color: #333;
113
+ border-left: 3px solid var(--primary-color);
114
+ }
115
+
116
+ .input-container {
117
+ padding: 1rem;
118
+ border-top: 1px solid var(--border-color);
119
+ background-color: #f9f9f9;
120
+ }
121
+
122
+ .input-row {
123
+ display: flex;
124
+ gap: 0.5rem;
125
+ }
126
+
127
+ .input-field {
128
+ flex: 1;
129
+ padding: 0.75rem 1rem;
130
+ border: 1px solid var(--border-color);
131
+ border-radius: var(--border-radius);
132
+ resize: none;
133
+ font-size: 0.95rem;
134
+ }
135
+
136
+ .input-field:focus {
137
+ outline: none;
138
+ border-color: var(--primary-color);
139
+ box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.1);
140
+ }
141
+
142
+ .send-button {
143
+ padding: 0.75rem 1.5rem;
144
+ background-color: var(--primary-color);
145
+ color: white;
146
+ border: none;
147
+ border-radius: var(--border-radius);
148
+ cursor: pointer;
149
+ font-weight: 500;
150
+ transition: background-color 0.2s;
151
+ }
152
+
153
+ .send-button:hover {
154
+ background-color: var(--secondary-color);
155
+ }
156
+
157
+ .send-button:disabled {
158
+ background-color: #ccc;
159
+ cursor: not-allowed;
160
+ }
161
+
162
+ .plugin-container {
163
+ display: none;
164
+ flex-direction: column;
165
+ background-color: #fff;
166
+ border-radius: var(--border-radius);
167
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
168
+ border: 1px solid var(--border-color);
169
+ overflow: hidden;
170
+ height: 100%;
171
+ flex: 2;
172
+ margin-left: 1rem;
173
+ }
174
+
175
+ .plugin-header {
176
+ padding: 0.75rem 1rem;
177
+ border-bottom: 1px solid var(--border-color);
178
+ background-color: #f9f9f9;
179
+ display: flex;
180
+ justify-content: space-between;
181
+ align-items: center;
182
+ }
183
+
184
+ .plugin-title {
185
+ margin: 0;
186
+ font-size: 1rem;
187
+ font-weight: 500;
188
+ }
189
+
190
+ .plugin-close {
191
+ background: none;
192
+ border: none;
193
+ cursor: pointer;
194
+ font-size: 1rem;
195
+ color: #777;
196
+ }
197
+
198
+ .plugin-content {
199
+ flex: 1;
200
+ overflow-y: auto;
201
+ }
202
+
203
+ /* 代码执行插件样式 */
204
+ .code-plugin .plugin-content {
205
+ padding: 0;
206
+ }
207
+
208
+ /* 可视化插件样式 */
209
+ .visualization-plugin .visualization-form {
210
+ margin-bottom: 1rem;
211
+ padding: 1rem;
212
+ }
213
+
214
+ .visualization-plugin .visualization-result {
215
+ margin-top: 1rem;
216
+ text-align: center;
217
+ padding: 0 1rem;
218
+ }
219
+
220
+ .visualization-plugin .visualization-image {
221
+ max-width: 100%;
222
+ border-radius: var(--border-radius);
223
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
224
+ }
225
+
226
+ /* 思维导图插件样式 */
227
+ .mindmap-plugin .mindmap-form {
228
+ margin-bottom: 1rem;
229
+ padding: 1rem;
230
+ }
231
+
232
+ .mindmap-plugin .mindmap-result {
233
+ margin-top: 1rem;
234
+ text-align: center;
235
+ padding: 0 1rem;
236
+ }
237
+
238
+ .mindmap-plugin .mindmap-image {
239
+ max-width: 100%;
240
+ border-radius: var(--border-radius);
241
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
242
+ }
243
+
244
+ /* 参考资料样式 */
245
+ .reference-container {
246
+ margin-top: 10px;
247
+ border-top: 1px dashed #ddd;
248
+ padding-top: 10px;
249
+ }
250
+
251
+ .reference-toggle {
252
+ color: #666;
253
+ font-size: 0.9rem;
254
+ cursor: pointer;
255
+ display: flex;
256
+ align-items: center;
257
+ }
258
+
259
+ .reference-content {
260
+ display: none;
261
+ margin-top: 10px;
262
+ padding: 10px;
263
+ background-color: #f9f9f9;
264
+ border-radius: 5px;
265
+ font-size: 0.85rem;
266
+ }
267
+
268
+ /* 代码样式 */
269
+ pre {
270
+ background-color: #f8f9fa;
271
+ border-radius: 5px;
272
+ padding: 12px;
273
+ margin: 10px 0;
274
+ overflow-x: auto;
275
+ border: 1px solid #eee;
276
+ }
277
+
278
+ code {
279
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
280
+ font-size: 14px;
281
+ }
282
+
283
+ /* 响应式样式 */
284
+ @media (max-width: 1200px) {
285
+ .main-container.with-plugin .chat-container {
286
+ max-width: 300px;
287
+ }
288
+ }
289
+
290
+ @media (max-width: 992px) {
291
+ .main-container {
292
+ flex-direction: column;
293
+ }
294
+
295
+ .main-container.with-plugin .chat-container {
296
+ max-width: 100%;
297
+ margin-bottom: 1rem;
298
+ height: 40vh;
299
+ }
300
+
301
+ .plugin-container {
302
+ margin-left: 0;
303
+ height: calc(60vh - 32px);
304
+ }
305
+ }
306
+
307
+ @media (max-width: 768px) {
308
+ .main-container {
309
+ padding: 0.5rem;
310
+ }
311
+ }
312
+ </style>
313
+ </head>
314
+ <body>
315
+ <header class="header">
316
+ <div class="header-content">
317
+ <h1>{{ agent_name }}</h1>
318
+ <div>
319
+ <span class="badge bg-primary">AI学习助手</span>
320
+ </div>
321
+ </div>
322
+ </header>
323
+
324
+ <div class="main-container" id="main-container">
325
+ <div class="chat-container">
326
+ <div class="chat-messages" id="chat-messages">
327
+ <div class="message bot-message">
328
+ <p>你好!我是{{ agent_name }},有什么可以帮助你的吗?</p>
329
+ {% if agent_description %}
330
+ <p>{{ agent_description }}</p>
331
+ {% endif %}
332
+ </div>
333
+ </div>
334
+
335
+ <div class="input-container">
336
+ <div class="input-row">
337
+ <textarea class="input-field" id="user-input" placeholder="输入您的问题..." rows="2"></textarea>
338
+ <button class="send-button" id="send-button">
339
+ <i class="bi bi-send"></i>
340
+ </button>
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ <!-- 代码执行插件 -->
346
+ <div class="plugin-container code-plugin" id="code-plugin">
347
+ <div class="plugin-header">
348
+ <h3 class="plugin-title">Python代码执行</h3>
349
+ <button class="plugin-close" id="close-code-plugin">
350
+ <i class="bi bi-x-lg"></i>
351
+ </button>
352
+ </div>
353
+ <div class="plugin-content">
354
+ <iframe id="code-execution-frame" src="" style="width: 100%; height: 100%; border: none;"></iframe>
355
+ </div>
356
+ </div>
357
+
358
+ <!-- 3D可视化插件 -->
359
+ <div class="plugin-container visualization-plugin" id="visualization-plugin">
360
+ <div class="plugin-header">
361
+ <h3 class="plugin-title">3D可视化</h3>
362
+ <button class="plugin-close" id="close-visualization-plugin">
363
+ <i class="bi bi-x-lg"></i>
364
+ </button>
365
+ </div>
366
+ <div class="plugin-content">
367
+ <div class="visualization-result" id="visualization-result">
368
+ <div class="text-center py-4">
369
+ <div class="spinner-border text-primary" role="status">
370
+ <span class="visually-hidden">加载中...</span>
371
+ </div>
372
+ <p class="mt-3">正在准备3D可视化...</p>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- 思维导图插件 -->
379
+ <div class="plugin-container mindmap-plugin" id="mindmap-plugin">
380
+ <div class="plugin-header">
381
+ <h3 class="plugin-title">思维导图</h3>
382
+ <button class="plugin-close" id="close-mindmap-plugin">
383
+ <i class="bi bi-x-lg"></i>
384
+ </button>
385
+ </div>
386
+ <div class="plugin-content">
387
+ <div class="mindmap-result" id="mindmap-result">
388
+ <div class="text-center py-4">
389
+ <div class="spinner-border text-primary" role="status">
390
+ <span class="visually-hidden">加载中...</span>
391
+ </div>
392
+ <p class="mt-3">正在生成思维导图...</p>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ </div>
397
+ </div>
398
+
399
+ <script>
400
+ // 全局变量
401
+ const agentId = "{{ agent_id }}";
402
+ const token = "{{ token }}";
403
+ let executionContext = null;
404
+ const mainContainer = document.getElementById('main-container');
405
+
406
+ // DOM元素
407
+ const chatMessages = document.getElementById('chat-messages');
408
+ const userInput = document.getElementById('user-input');
409
+ const sendButton = document.getElementById('send-button');
410
+
411
+ // 代码执行插件元素
412
+ const codePlugin = document.getElementById('code-plugin');
413
+ const closeCodePlugin = document.getElementById('close-code-plugin');
414
+ const codeExecutionFrame = document.getElementById('code-execution-frame');
415
+
416
+ // 3D可视化插件元素
417
+ const visualizationPlugin = document.getElementById('visualization-plugin');
418
+ const closeVisualizationPlugin = document.getElementById('close-visualization-plugin');
419
+ const visualizationResult = document.getElementById('visualization-result');
420
+
421
+ // 思维导图插件元素
422
+ const mindmapPlugin = document.getElementById('mindmap-plugin');
423
+ const closeMindmapPlugin = document.getElementById('close-mindmap-plugin');
424
+ const mindmapResult = document.getElementById('mindmap-result');
425
+
426
+ // 页面加载完成后执行
427
+ document.addEventListener('DOMContentLoaded', function() {
428
+ // 初始化发送按钮事件
429
+ sendButton.addEventListener('click', sendMessage);
430
+
431
+ // 初始化输入框回车事件
432
+ userInput.addEventListener('keypress', function(e) {
433
+ if (e.key === 'Enter' && !e.shiftKey) {
434
+ e.preventDefault();
435
+ sendMessage();
436
+ }
437
+ });
438
+
439
+ // 初始化插件关闭事件
440
+ closeCodePlugin.addEventListener('click', () => {
441
+ codePlugin.style.display = 'none';
442
+ // 清空iframe源以停止任何运行中的代码
443
+ codeExecutionFrame.src = '';
444
+ updateMainContainerLayout();
445
+ });
446
+
447
+ closeVisualizationPlugin.addEventListener('click', () => {
448
+ visualizationPlugin.style.display = 'none';
449
+ updateMainContainerLayout();
450
+ });
451
+
452
+ closeMindmapPlugin.addEventListener('click', () => {
453
+ mindmapPlugin.style.display = 'none';
454
+ updateMainContainerLayout();
455
+ });
456
+ });
457
+
458
+ // 更新主容器布局
459
+ function updateMainContainerLayout() {
460
+ // 检查是否有任何插件处于显示状态
461
+ const isAnyPluginVisible =
462
+ codePlugin.style.display === 'flex' ||
463
+ visualizationPlugin.style.display === 'flex' ||
464
+ mindmapPlugin.style.display === 'flex';
465
+
466
+ // 更新主容器类名
467
+ if (isAnyPluginVisible) {
468
+ mainContainer.classList.add('with-plugin');
469
+ } else {
470
+ mainContainer.classList.remove('with-plugin');
471
+ }
472
+ }
473
+
474
+ // 发送消息
475
+ async function sendMessage() {
476
+ const message = userInput.value.trim();
477
+ if (!message) return;
478
+
479
+ // 添加用户消息
480
+ addMessage(message, true);
481
+
482
+ // 清空输入框
483
+ userInput.value = '';
484
+
485
+ // 禁用发送按钮
486
+ sendButton.disabled = true;
487
+
488
+ try {
489
+ // 发送请求
490
+ const response = await fetch(`/api/student/chat/${agentId}`, {
491
+ method: 'POST',
492
+ headers: {
493
+ 'Content-Type': 'application/json'
494
+ },
495
+ body: JSON.stringify({
496
+ message: message,
497
+ token: token
498
+ })
499
+ });
500
+
501
+ const data = await response.json();
502
+
503
+ if (data.success) {
504
+ // 处理回复内容,移除参考来源部分
505
+ const processedContent = processResponseContent(data.message);
506
+
507
+ // 添加机器人回复
508
+ const messageElement = addMessage(processedContent, false);
509
+
510
+ // 添加参考信息(如果有)
511
+ if (data.references && data.references.length > 0) {
512
+ addReferences(messageElement, data.references);
513
+ }
514
+
515
+ // 检查是否需要显示工具
516
+ if (data.tools && data.tools.length > 0) {
517
+ // 隐藏所有插件
518
+ hideAllPlugins();
519
+
520
+ // 使用新的插件激活函数
521
+ activatePlugins(data.message, data.tools);
522
+ }
523
+ } else {
524
+ // 添加错误消息
525
+ addMessage(`错误: ${data.message}`, false);
526
+ }
527
+ } catch (error) {
528
+ console.error('发送请求出错:', error);
529
+ addMessage('发送请求时出错,请重试', false);
530
+ } finally {
531
+ // 恢复发送按钮
532
+ sendButton.disabled = false;
533
+ }
534
+ }
535
+
536
+ // 处理回复内容,移除参考来源部分
537
+ function processResponseContent(content) {
538
+ // 移除 "===参考来源开始===" 到 "===参考来源结束===" 之间的内容
539
+ return content.replace(/===参考来源开始===[\s\S]*?===参考来源结束===/, '');
540
+ }
541
+
542
+ // 添加参考信息显示函数
543
+ function addReferences(messageElement, references) {
544
+ // 创建参考容器
545
+ const referenceContainer = document.createElement('div');
546
+ referenceContainer.className = 'reference-container';
547
+
548
+ // 创建参考切换按钮
549
+ const toggleButton = document.createElement('div');
550
+ toggleButton.className = 'reference-toggle';
551
+ toggleButton.textContent = '参考来源';
552
+ toggleButton.style.color = '#666';
553
+ toggleButton.style.fontSize = '0.9rem';
554
+ toggleButton.style.cursor = 'pointer';
555
+ toggleButton.style.display = 'flex';
556
+ toggleButton.style.alignItems = 'center';
557
+
558
+ // 创建参考内容
559
+ const referenceContent = document.createElement('div');
560
+ referenceContent.className = 'reference-content';
561
+
562
+ // 添加参考项
563
+ references.forEach(ref => {
564
+ const refItem = document.createElement('div');
565
+ refItem.style.marginBottom = '8px';
566
+ refItem.style.paddingBottom = '8px';
567
+ refItem.style.borderBottom = '1px solid #eee';
568
+
569
+ refItem.innerHTML = `
570
+ <div><strong>[${ref.index}]</strong> ${ref.summary}</div>
571
+ <div><small>来源: ${ref.file_name}</small></div>
572
+ `;
573
+
574
+ referenceContent.appendChild(refItem);
575
+ });
576
+
577
+ // 添加切换功能
578
+ toggleButton.addEventListener('click', () => {
579
+ if (referenceContent.style.display === 'none') {
580
+ referenceContent.style.display = 'block';
581
+ toggleButton.textContent = '折叠参考来源';
582
+ } else {
583
+ referenceContent.style.display = 'none';
584
+ toggleButton.textContent = '参考来源';
585
+ }
586
+ });
587
+
588
+ // 组装并添加到消息
589
+ referenceContainer.appendChild(toggleButton);
590
+ referenceContainer.appendChild(referenceContent);
591
+ messageElement.appendChild(referenceContainer);
592
+ }
593
+
594
+ // 添加消息函数
595
+ function addMessage(content, isUser) {
596
+ const messageDiv = document.createElement('div');
597
+ messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
598
+
599
+ // 处理内容中的换行符和代码块
600
+ const processedContent = processMessageContent(content);
601
+ messageDiv.innerHTML = processedContent;
602
+
603
+ chatMessages.appendChild(messageDiv);
604
+
605
+ // 滚动到底部
606
+ chatMessages.scrollTop = chatMessages.scrollHeight;
607
+
608
+ // 对代码块应用高亮
609
+ if (!isUser) {
610
+ applySyntaxHighlighting(messageDiv);
611
+ }
612
+
613
+ return messageDiv;
614
+ }
615
+
616
+ // 处理消息内容中的换行符和代码块
617
+ function processMessageContent(content) {
618
+ // 先替换代码块,保护它们不受其他替换的影响
619
+ const codeBlocks = [];
620
+ let processedContent = content.replace(/```(\w*)\n([\s\S]*?)\n```/g, function(match, language, code) {
621
+ const id = codeBlocks.length;
622
+ codeBlocks.push({ language, code });
623
+ return `__CODE_BLOCK_${id}__`;
624
+ });
625
+
626
+ // 替换普通换行符
627
+ processedContent = processedContent.replace(/\n/g, '<br>');
628
+
629
+ // 恢复代码块
630
+ processedContent = processedContent.replace(/__CODE_BLOCK_(\d+)__/g, function(match, id) {
631
+ const { language, code } = codeBlocks[id];
632
+ const langClass = language ? ` class="language-${language}"` : '';
633
+ return `<pre><code${langClass}>${escapeHtml(code)}</code></pre>`;
634
+ });
635
+
636
+ return processedContent;
637
+ }
638
+
639
+ // 应用语法高亮
640
+ function applySyntaxHighlighting(element) {
641
+ // 检查是否已加载highlight.js
642
+ if (typeof hljs !== 'undefined') {
643
+ // 找出所有代码块并应用高亮
644
+ element.querySelectorAll('pre code').forEach((block) => {
645
+ hljs.highlightBlock(block);
646
+ });
647
+ }
648
+ }
649
+
650
+ // 转义HTML字符
651
+ function escapeHtml(text) {
652
+ return text
653
+ .replace(/&/g, "&amp;")
654
+ .replace(/</g, "&lt;")
655
+ .replace(/>/g, "&gt;")
656
+ .replace(/"/g, "&quot;")
657
+ .replace(/'/g, "&#039;");
658
+ }
659
+
660
+ // 隐藏所有插件
661
+ function hideAllPlugins() {
662
+ codePlugin.style.display = 'none';
663
+ visualizationPlugin.style.display = 'none';
664
+ mindmapPlugin.style.display = 'none';
665
+ updateMainContainerLayout();
666
+ }
667
+
668
+ // 提取代码块
669
+ function extractCodeBlocks(message) {
670
+ const codeBlocks = [];
671
+ const codeRegex = /```python\n([\s\S]*?)\n```/g;
672
+
673
+ let match;
674
+ while ((match = codeRegex.exec(message)) !== null) {
675
+ codeBlocks.push(match[1]);
676
+ }
677
+
678
+ return codeBlocks;
679
+ }
680
+
681
+ // 提取3D可视化函数代码
682
+ function extract3DVisualizationCode(message) {
683
+ // 尝试提取函数代码块
684
+ const codeRegex = /```python\s*(import[\s\S]*?def create_3d_plot\(\):[\s\S]*?return[\s\S]*?})\s*```/i;
685
+ const match = codeRegex.exec(message);
686
+
687
+ if (match) {
688
+ return match[1].trim();
689
+ }
690
+
691
+ return null;
692
+ }
693
+
694
+ // 提取思维导图内容
695
+ function extractMindmapContent(message) {
696
+ // 尝试提取@startmindmap和@endmindmap之间的内容
697
+ const mindmapRegex = /@startmindmap\n([\s\S]*?)@endmindmap/;
698
+ const match = mindmapRegex.exec(message);
699
+
700
+ if (match) {
701
+ return `@startmindmap\n${match[1]}\n@endmindmap`;
702
+ }
703
+
704
+ return null;
705
+ }
706
+
707
+ // 激活代码执行插件
708
+ function activateCodePlugin(message) {
709
+ // 提取代码块
710
+ const codeBlocks = extractCodeBlocks(message);
711
+
712
+ if (codeBlocks.length > 0) {
713
+ const isAlreadyVisible = codePlugin.style.display === 'flex';
714
+
715
+ // 显示插件容器
716
+ codePlugin.style.display = 'flex';
717
+ updateMainContainerLayout();
718
+
719
+ // 获取iframe
720
+ const iframe = document.getElementById('code-execution-frame');
721
+
722
+ // 如果插件已经可见且iframe已加载
723
+ if (isAlreadyVisible && iframe.contentWindow) {
724
+ // 发送新代码到现有iframe
725
+ iframe.contentWindow.postMessage({
726
+ type: 'setCode',
727
+ code: codeBlocks[0]
728
+ }, '*');
729
+ } else {
730
+ // 设置iframe源
731
+ let src = '/code_execution.html';
732
+ if (codeBlocks.length > 0) {
733
+ src += `?code=${encodeURIComponent(codeBlocks[0])}`;
734
+ }
735
+
736
+ iframe.src = src;
737
+
738
+ // 设置iframe加载事件处理程序
739
+ iframe.onload = function() {
740
+ if (codeBlocks.length > 0) {
741
+ iframe.contentWindow.postMessage({
742
+ type: 'setCode',
743
+ code: codeBlocks[0]
744
+ }, '*');
745
+ }
746
+ };
747
+ }
748
+ }
749
+ }
750
+
751
+ // 激活3D可视化插件
752
+ // 激活3D可视化插件
753
+ function activate3DVisualization(message) {
754
+ const code = extract3DVisualizationCode(message);
755
+
756
+ if (code) {
757
+ // 显示插件容器
758
+ visualizationPlugin.style.display = 'flex';
759
+ updateMainContainerLayout();
760
+
761
+ // 显示加载状态
762
+ visualizationResult.innerHTML = `
763
+ <div class="text-center py-4">
764
+ <div class="spinner-border text-primary" role="status">
765
+ <span class="visually-hidden">生成中...</span>
766
+ </div>
767
+ <p class="mt-3">正在生成3D图形,请稍候...</p>
768
+ </div>
769
+ `;
770
+
771
+ // 自动生成可视化
772
+ fetch('/api/visualization/3d-surface', {
773
+ method: 'POST',
774
+ headers: {
775
+ 'Content-Type': 'application/json'
776
+ },
777
+ body: JSON.stringify({ code: code })
778
+ })
779
+ .then(response => response.json())
780
+ .then(data => {
781
+ if (data.success) {
782
+ // 直接嵌入HTML
783
+ visualizationResult.innerHTML = `
784
+ <div class="visualization-iframe-container" style="width:100%; height:500px;">
785
+ <iframe src="${data.html_url}" style="width:100%; height:100%; border:none;"></iframe>
786
+ </div>
787
+ <div class="text-center mt-3">
788
+ <p>3D图形生成成功</p>
789
+ <a href="${data.html_url}" class="btn btn-sm btn-outline-primary" target="_blank">
790
+ <i class="bi bi-arrows-fullscreen"></i> 全屏查看
791
+ </a>
792
+ </div>
793
+ `;
794
+ } else {
795
+ visualizationResult.innerHTML = `
796
+ <div class="alert alert-danger">
797
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
798
+ 生成失败: ${data.message}
799
+ </div>
800
+ `;
801
+ }
802
+ })
803
+ .catch(error => {
804
+ console.error('生成3D图形出错:', error);
805
+ visualizationResult.innerHTML = `
806
+ <div class="alert alert-danger">
807
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
808
+ 生成3D图形时发生错误,请重试
809
+ </div>
810
+ `;
811
+ });
812
+ }
813
+ }
814
+ // 激活思维导图插件
815
+ function activateMindmap(message) {
816
+ const content = extractMindmapContent(message);
817
+
818
+ if (content) {
819
+ // 显示插件容器
820
+ mindmapPlugin.style.display = 'flex';
821
+ updateMainContainerLayout();
822
+
823
+ // 显示加载状态
824
+ mindmapResult.innerHTML = `
825
+ <div class="text-center py-4">
826
+ <div class="spinner-border text-primary" role="status">
827
+ <span class="visually-hidden">生成中...</span>
828
+ </div>
829
+ <p class="mt-3">正在生成思维导图,请稍候...</p>
830
+ </div>
831
+ `;
832
+
833
+ // 自动生成思维导图
834
+ fetch('/api/visualization/mindmap', {
835
+ method: 'POST',
836
+ headers: {
837
+ 'Content-Type': 'application/json'
838
+ },
839
+ body: JSON.stringify({ content: content })
840
+ })
841
+ .then(response => response.json())
842
+ .then(data => {
843
+ if (data.success) {
844
+ mindmapResult.innerHTML = `
845
+ <div class="text-center">
846
+ <img src="${data.url}" class="mindmap-image" alt="思维导图">
847
+ <p class="mt-3">生成成功!</p>
848
+ </div>
849
+ `;
850
+ }
851
+ else {
852
+ mindmapResult.innerHTML = `
853
+ <div class="alert alert-danger">
854
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
855
+ 生成失败: ${data.message}
856
+ </div>
857
+ `;
858
+ }
859
+ })
860
+ .catch(error => {
861
+ console.error('生成思维导图出错:', error);
862
+ mindmapResult.innerHTML = `
863
+ <div class="alert alert-danger">
864
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
865
+ 生成思维导图时发生错误,请重试
866
+ </div>
867
+ `;
868
+ });
869
+ }
870
+ }
871
+
872
+ // 根据消息内容激活适当的插件
873
+ function activatePlugins(message, tools) {
874
+ // 检查并激活代码执行插件
875
+ if (tools.includes('code') && message.includes('```python')) {
876
+ activateCodePlugin(message);
877
+ }
878
+
879
+ // 检查并激活3D可视化插件
880
+ if (tools.includes('visualization') &&
881
+ (message.includes('def create_3d_plot') ||
882
+ message.includes('3D') || message.includes('可视化'))) {
883
+ activate3DVisualization(message);
884
+ }
885
+
886
+ // 检查并激活思维导图插件
887
+ if (tools.includes('mindmap') &&
888
+ (message.includes('@startmindmap') ||
889
+ message.includes('思维导图'))) {
890
+ activateMindmap(message);
891
+ }
892
+ }
893
+ </script>
894
+
895
+ <!-- 添加highlight.js用于代码高亮 -->
896
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
897
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
898
+ <script>
899
+ // 初始化代码高亮
900
+ document.addEventListener('DOMContentLoaded', function() {
901
+ if (typeof hljs !== 'undefined') {
902
+ hljs.configure({
903
+ languages: ['python']
904
+ });
905
+ }
906
+ });
907
+ </script>
908
+ </body>
909
+ </html>
utils/helpers.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils/helpers.py
2
+ import os
3
+ import json
4
+ import time
5
+ import re
6
+
7
+ def create_response(success, message="", data=None, status_code=200):
8
+ """创建标准化的API响应"""
9
+ response = {"success": success}
10
+
11
+ if message:
12
+ response["message"] = message
13
+
14
+ if data is not None:
15
+ response["data"] = data
16
+
17
+ return response, status_code
18
+
19
+ def validate_agent_access(agent_id, token=None):
20
+ """验证Agent访问权限"""
21
+ agent_path = os.path.join('agents', f"{agent_id}.json")
22
+
23
+ if not os.path.exists(agent_path):
24
+ return False, "Agent不存在"
25
+
26
+ try:
27
+ with open(agent_path, 'r', encoding='utf-8') as f:
28
+ agent_config = json.load(f)
29
+
30
+ # 如果没有提供令牌,则允许访问
31
+ if not token:
32
+ return True, agent_config
33
+
34
+ # 验证令牌
35
+ if "distributions" in agent_config:
36
+ for dist in agent_config["distributions"]:
37
+ if dist.get("token") == token:
38
+ # 检查是否过期
39
+ if dist.get("expires_at", 0) > 0 and dist.get("expires_at", 0) < time.time():
40
+ return False, "访问令牌已过期"
41
+
42
+ # 更新使用计数
43
+ dist["usage_count"] = dist.get("usage_count", 0) + 1
44
+
45
+ # 更新Agent使用统计
46
+ if "stats" not in agent_config:
47
+ agent_config["stats"] = {}
48
+
49
+ agent_config["stats"]["usage_count"] = agent_config["stats"].get("usage_count", 0) + 1
50
+ agent_config["stats"]["last_used"] = int(time.time())
51
+
52
+ # 保存更新后的配置
53
+ with open(agent_path, 'w', encoding='utf-8') as f:
54
+ json.dump(agent_config, f, ensure_ascii=False, indent=2)
55
+
56
+ return True, agent_config
57
+
58
+ return False, "访问令牌无效"
59
+
60
+ except Exception as e:
61
+ return False, f"验证过程出错: {str(e)}"
62
+
63
+ def extract_code_from_text(text):
64
+ """从文本中提取代码块"""
65
+ # 尝试找到Python代码块
66
+ pattern = r'```(?:python)?\n([\s\S]*?)\n```'
67
+ match = re.search(pattern, text)
68
+
69
+ if match:
70
+ return match.group(1)
71
+
72
+ # 如果没有找到带有语言标记的代码块,尝试无语言标记的
73
+ pattern = r'```\n([\s\S]*?)\n```'
74
+ match = re.search(pattern, text)
75
+
76
+ if match:
77
+ return match.group(1)
78
+
79
+ # 如果没有代码块标记,返回原始文本
80
+ return text