Compare commits
3 Commits
e4504daca0
...
ad1711029a
Author | SHA1 | Date |
---|---|---|
|
ad1711029a | 1 week ago |
|
f44fa1d565 | 3 weeks ago |
|
d075ede8b3 | 3 weeks ago |
78 changed files with 6928 additions and 3689 deletions
@ -1,321 +0,0 @@ |
|||||
''' |
|
||||
渗透测试任务管理类 一次任务的闭合性要检查2025-3-10 一次任务后要清理LLM和InstrM的数据 |
|
||||
''' |
|
||||
from mycode.TargetManager import TargetManager # 从模块导入类 |
|
||||
#from LLMManager import LLMManager # 同理修正其他导入 |
|
||||
from mycode.ControlCenter import ControlCenter #控制中心替代LLM--控制中心要实现一定的基础逻辑和渗透测试树的维护。 |
|
||||
from myutils.FileManager import FileManager |
|
||||
from mycode.InstructionManager import InstructionManager |
|
||||
from mycode.DBManager import DBManager |
|
||||
from myutils.MyTime import get_local_timestr |
|
||||
from myutils.MyLogger_logger import LogHandler |
|
||||
import pickle |
|
||||
import queue |
|
||||
import time |
|
||||
import os |
|
||||
import threading |
|
||||
|
|
||||
class TaskManager: |
|
||||
|
|
||||
def __init__(self): |
|
||||
self.TargetM = TargetManager() |
|
||||
self.logger = LogHandler().get_logger("TaskManager") |
|
||||
# 生成功能对象 |
|
||||
self.DBM = DBManager() #主进程一个DBM |
|
||||
if not self.DBM.connect(): |
|
||||
self.logger.error("数据库连接失败!终止工作!") |
|
||||
return |
|
||||
self.CCM = ControlCenter(self.DBM,self) |
|
||||
self.InstrM = InstructionManager(self) # 类对象渗透,要约束只读取信息 |
|
||||
# 控制最大并发指令数量 |
|
||||
self.max_thread_num = 2 |
|
||||
self.task_id = 0 #任务id -- |
|
||||
self.workth_list = [] #线程句柄list |
|
||||
# self.long_instr_num = 0 #耗时指令数量 |
|
||||
# self.long_time_instr = ['nikto'] #耗时操作不计入批量执行的数量,不加不减 |
|
||||
self.node_queue = queue.Queue() |
|
||||
|
|
||||
self.lock = threading.Lock() #线程锁 |
|
||||
self.node_num = 0 #在处理Node线程的处理 |
|
||||
self.brun = True |
|
||||
self.cookie = "" #cookie参数 |
|
||||
|
|
||||
def res_in_quere(self,bres,instr,reslut,start_time,end_time,th_DBM,source_result,ext_params,work_node): |
|
||||
''' |
|
||||
执行结果入队列 |
|
||||
:param bres: |
|
||||
:param instr: |
|
||||
:param reslut: |
|
||||
:return: |
|
||||
''' |
|
||||
#入数据库 -- bres True和False 都入数据库2025-3-10---加node_path(2025-3-18)#? |
|
||||
if th_DBM.ok: |
|
||||
work_node.do_sn += 1 # 指令的执行序列是一个任务共用,要线程锁,错误问题也不大 |
|
||||
th_DBM.insetr_result(self.task_id, instr, reslut, work_node.do_sn, start_time, end_time, source_result, |
|
||||
ext_params, work_node.path) |
|
||||
else: |
|
||||
self.logger.error("数据库连接失败!!") |
|
||||
|
|
||||
#结果入队列---2025-3-18所有的指令均需返回给LLM便于节点状态的更新,所以bres作用要调整。 |
|
||||
res = {'执行指令':instr,'结果':reslut} |
|
||||
str_res = json.dumps(res,ensure_ascii=False) #直接字符串组合也可以-待验证 |
|
||||
work_node.llm_type = 1 |
|
||||
work_node.add_res(str_res) #入节点结果队列 |
|
||||
|
|
||||
def do_worker_th(self): |
|
||||
#线程的dbm需要一个线程一个 |
|
||||
th_DBM = DBManager() |
|
||||
th_DBM.connect() |
|
||||
while self.brun: |
|
||||
try: |
|
||||
work_node = self.node_queue.get(block=False) |
|
||||
# 测试时使用 |
|
||||
with self.lock: |
|
||||
self.node_num += 1 |
|
||||
# 开始执行指令 |
|
||||
while work_node.instr_queue: |
|
||||
#for instruction in work_node.instr_queue: #这里要重新调整#? |
|
||||
instruction = work_node.instr_queue.pop(0) |
|
||||
start_time = get_local_timestr() # 指令执行开始时间 |
|
||||
bres, instr, reslut, source_result, ext_params = self.InstrM.execute_instruction(instruction) |
|
||||
end_time = get_local_timestr() # 指令执行结束时间 |
|
||||
self.res_in_quere(bres, instr, reslut, start_time, end_time, th_DBM, source_result, |
|
||||
ext_params, work_node) # 执行结果入队列 |
|
||||
|
|
||||
# #针对一个节点的指令执行完成后,提交LLM规划下一步操作 |
|
||||
#self.CCM.llm_quere.put(work_node) |
|
||||
|
|
||||
# 保存记录--测试时使用--后期增加人为停止测试时可以使用 |
|
||||
with self.lock: |
|
||||
self.node_num -= 1 |
|
||||
if self.node_num == 0 and self.node_queue.empty(): # |
|
||||
self.logger.debug("此批次指令执行完成!") |
|
||||
with open("attack_tree", 'wb') as f: |
|
||||
pickle.dump(self.CCM.attack_tree, f) |
|
||||
|
|
||||
except queue.Empty: |
|
||||
time.sleep(20) |
|
||||
|
|
||||
def start_task(self,target_name,target_in): |
|
||||
''' |
|
||||
|
|
||||
:param target_name: 任务目标名字 |
|
||||
:param target_in: 任务目标访问地址 |
|
||||
:return: |
|
||||
''' |
|
||||
#判断目标合法性 |
|
||||
bok,target,type = self.TargetM.validate_and_extract(target_in) |
|
||||
if bok: |
|
||||
self.target = target |
|
||||
self.type = type #1-IP,2-domain |
|
||||
self.task_id = self.DBM.start_task(target_name,target_in) #数据库新增任务记录 |
|
||||
#获取基本信息: 读取数据库或预生成指令,获取基本的已知信息 |
|
||||
know_info = "无" #? |
|
||||
#启动--初始化指令 |
|
||||
self.CCM.start_do(target,self.task_id) |
|
||||
|
|
||||
#创建工作线程----2025-3-18调整为一个节点一个线程, |
|
||||
for i in range(self.max_thread_num): |
|
||||
w_th = threading.Thread(target=self.do_worker_th) |
|
||||
w_th.start() |
|
||||
self.workth_list.append(w_th) |
|
||||
|
|
||||
#等待线程结束--执行生成报告 |
|
||||
for t in self.workth_list: |
|
||||
t.join() |
|
||||
#生成报告 |
|
||||
pass |
|
||||
else: |
|
||||
return False,"{target}检测目标不合规,请检查!" |
|
||||
|
|
||||
def stop_task(self): |
|
||||
self.brun = False |
|
||||
self.CCM.stop_do() #清空一些全局变量 |
|
||||
self.InstrM.init_data() |
|
||||
#结束任务需要收尾处理#? |
|
||||
|
|
||||
|
|
||||
if __name__ == "__main__": |
|
||||
import json |
|
||||
TM = TaskManager() |
|
||||
FM = FileManager() |
|
||||
current_path = os.path.dirname(os.path.realpath(__file__)) |
|
||||
strMsg = FM.read_file("test",1) |
|
||||
|
|
||||
test_type = 2 |
|
||||
instr_index = 19 |
|
||||
iput_index = -1 # 0是根节点 |
|
||||
indexs = [] |
|
||||
if test_type == 0: #新目标测试 |
|
||||
# 启动--初始化指令 |
|
||||
node_list = TM.CCM.start_do("58.216.217.70", 1) |
|
||||
#异步处理,需要等待线程结束了 |
|
||||
for th in TM.CCM.llmth_list: |
|
||||
th.join() |
|
||||
elif test_type == 1: |
|
||||
#测试执行指令 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
# 遍历node,查看有instr的ndoe |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
if indexs: |
|
||||
for index in indexs: |
|
||||
node = nodes[index] |
|
||||
if node.instr_queue: # list |
|
||||
TM.node_queue.put(node) |
|
||||
else: |
|
||||
for node in nodes: |
|
||||
if node.instr_queue: |
|
||||
TM.node_queue.put(node) |
|
||||
|
|
||||
#创建线程执行指令 |
|
||||
for i in range(TM.max_thread_num): |
|
||||
w_th = threading.Thread(target=TM.do_worker_th) |
|
||||
w_th.start() |
|
||||
TM.workth_list.append(w_th) |
|
||||
# 等待线程结束--执行生成报告 |
|
||||
for t in TM.workth_list: |
|
||||
t.join() |
|
||||
elif test_type ==2: |
|
||||
#测试LLM返回下一步指令 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
#遍历node,查看有res的数据 |
|
||||
iput_max_num = 0 |
|
||||
iput_num = 0 |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
if indexs: |
|
||||
for index in indexs: |
|
||||
node = nodes[index] |
|
||||
if node.res_quere: |
|
||||
TM.CCM.llm_quere.put(node) |
|
||||
else: |
|
||||
for node in nodes: |
|
||||
if node.res_quere: |
|
||||
TM.CCM.llm_quere.put(node) |
|
||||
|
|
||||
#创建llm工作线程 |
|
||||
TM.CCM.brun = True |
|
||||
for i in range(TM.CCM.max_thread_num): |
|
||||
l_th = threading.Thread(target=TM.CCM.th_llm_worker()) |
|
||||
l_th.start() |
|
||||
TM.CCM.llmth_list.append(l_th) |
|
||||
# 等待线程结束 |
|
||||
for t in TM.CCM.llmth_list: |
|
||||
t.join() |
|
||||
elif test_type ==3: #执行指定指令 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
# 遍历node,查看有instr的ndoe |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
instrlist = nodes[instr_index].instr_queue |
|
||||
instrlist = [''' |
|
||||
mkdir -p /tmp/nfs_test && mount -t nfs -o nolock 192.168.204.137:/ /tmp/nfs_test && ls /tmp/nfs_test |
|
||||
'''] |
|
||||
for instr in instrlist: |
|
||||
start_time = get_local_timestr() # 指令执行开始时间 |
|
||||
bres, instr, reslut, source_result, ext_params = TM.InstrM.execute_instruction(instr) |
|
||||
end_time = get_local_timestr() # 指令执行结束时间 |
|
||||
|
|
||||
res = {'执行结果': reslut} |
|
||||
str_res = json.dumps(res,ensure_ascii=False) # 直接字符串组合也可以-待验证 |
|
||||
print(str_res) |
|
||||
elif test_type == 4: #修改Message |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
#创建一个新的节点 |
|
||||
from mycode.AttackMap import TreeNode |
|
||||
testnode = TreeNode("test",0,0) |
|
||||
TM.CCM.LLM.build_initial_prompt(testnode)#新的Message |
|
||||
systems = testnode.messages[0]["content"] |
|
||||
#print(systems) |
|
||||
# 遍历node,查看有instr的ndoe |
|
||||
nodes = TM.CCM.attack_tree.traverse_bfs() |
|
||||
for node in nodes: |
|
||||
node.messages[0]["content"] = systems |
|
||||
with open("attack_tree", 'wb') as f: |
|
||||
pickle.dump(TM.CCM.attack_tree, f) |
|
||||
elif test_type ==5: #显示指令和结果list |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
if iput_index == -1: |
|
||||
for node in nodes: |
|
||||
print(f"----{node.path}-{node.status}----\n****instr_quere") |
|
||||
print(f"{','.join(node.instr_queue)}\n****res_quere") |
|
||||
try: |
|
||||
print(f"{','.join(node.res_quere)}") |
|
||||
except: |
|
||||
print(f"{json.dumps(node.res_quere)}") |
|
||||
elif iput_index == -2:#只输出有instruction的数据 |
|
||||
index = 0 |
|
||||
for node in nodes: |
|
||||
if node.instr_queue: |
|
||||
print(f"----{index}--{node.path}--{node.status}----") |
|
||||
print(f"{','.join(node.instr_queue)}") |
|
||||
index += 1 |
|
||||
else: |
|
||||
print(f"********\n{','.join(nodes[iput_index].instr_queue)}\n********") |
|
||||
print(f"&&&&&&&&\n{','.join(nodes[iput_index].res_quere)}\n&&&&&&&&") |
|
||||
elif test_type == 6: #给指定节点添加测试指令 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
str_instr = "nmap -sV -p- 192.168.204.137 -T4 -oN nmap_full_scan.txt" |
|
||||
index = 9 |
|
||||
nodes[index].instr_queue.append(str_instr) |
|
||||
nodes[index].res_quere = [] |
|
||||
with open("attack_tree", 'wb') as f: |
|
||||
pickle.dump(TM.CCM.attack_tree, f) |
|
||||
elif test_type == 7: #给指定节点修改指令的执行结果 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
str_instr = "psql -h 192.168.204.137 -U postgres -c '\l'" |
|
||||
start_time = get_local_timestr() # 指令执行开始时间 |
|
||||
bres, instr, reslut, source_result, ext_params = TM.InstrM.execute_instruction(str_instr) |
|
||||
end_time = get_local_timestr() # 指令执行结束时间 |
|
||||
# 入数据库 -- bres True和False 都入数据库2025-3-10---加node_path(2025-3-18)#? |
|
||||
if TM.DBM.ok: |
|
||||
TM.DBM.insetr_result(0, instr, reslut, 0, start_time, end_time, source_result, |
|
||||
ext_params, "独立命令执行") |
|
||||
index = 9 |
|
||||
nodes[index].res_quere.clear() |
|
||||
nodes[index].res_quere.append(reslut) |
|
||||
|
|
||||
with open("attack_tree", 'wb') as f: |
|
||||
pickle.dump(TM.CCM.attack_tree, f) |
|
||||
elif test_type ==8: #显示有漏洞信息的数据 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
if nodes: |
|
||||
for node in nodes: |
|
||||
if node.vul_type != "未发现": |
|
||||
print(f"{node.path}----{node.vul_type}") |
|
||||
elif test_type == 9: #处理自定义llm回复内容 |
|
||||
with open("attack_tree", "rb") as f: |
|
||||
TM.CCM.attack_tree = pickle.load(f) |
|
||||
nodes = TM.CCM.attack_tree.traverse_dfs() |
|
||||
node = nodes[5] |
|
||||
strconent = ''' |
|
||||
{'role': 'assistant', 'content': '{"action":"update_status", "node": "25端口", "status": "已完成", "vulnerability": {"name":"SMTP用户枚举漏洞","risk":"中危","info":"VRFY命令可验证有效用户"}}\n\n```bash-[目标系统->192.168.204.137->25端口]\nsmtp-user-enum -M VRFY -U /usr/share/wordlists/metasploit/unix_users.txt -t 192.168.204.137\n```\n\n```bash-[目标系统->192.168.204.137->25端口]\nnc -nv 192.168.204.137 25 << EOF\nEXPN root\nMAIL FROM: attacker@example.com\nRCPT TO: external@example.com\nDATA\nTest open relay\n.\nQUIT\nEOF\n```'} |
|
||||
''' |
|
||||
strjson = json.loads(strconent) |
|
||||
node_cmds,commands = TM.CCM.LLM.fetch_instruction(strjson["content"]) |
|
||||
TM.CCM.tree_manager(node_cmds) |
|
||||
|
|
||||
# 更新tree |
|
||||
bok, new_commands = TM.CCM.tree_manager(node_cmds, node, commands, TM.DBM) |
|
||||
# 分析指令入对应节点 |
|
||||
if bok: # 节点指令若存在错误,测试指令都不处理,需要LLM重新生成 |
|
||||
node_list = TM.CCM.instr_in_node(new_commands, node) |
|
||||
|
|
||||
#报不保存待定-- |
|
||||
with open("attack_tree", 'wb') as f: |
|
||||
pickle.dump(TM.CCM.attack_tree, f) |
|
||||
|
|
||||
|
|
||||
else: |
|
||||
#完整过程测试---要设定终止条件 |
|
||||
pass |
|
@ -0,0 +1,4 @@ |
|||||
|
class ClientSocket: |
||||
|
def __init__(self): |
||||
|
self.user_id = -1 |
||||
|
self.th_read = None |
@ -0,0 +1,97 @@ |
|||||
|
#对llm返回的指令进行校验 |
||||
|
import re |
||||
|
class CommandVerify: |
||||
|
def __init__(self): |
||||
|
pass |
||||
|
|
||||
|
#验证节点指令的结构完整性--主要是判断JSON元素是否完整 |
||||
|
def verify_node_cmds(self,node_cmds): |
||||
|
''' |
||||
|
- 新增节点:{\"action\":\"add_node\", \"parent\": \"父节点\", \"nodes\": \"节点1,节点2\"}; |
||||
|
- 未生成指令节点列表:{\"action\": \"no_instruction\", \"nodes\": \"节点1,节点2\"}; |
||||
|
- 漏洞验证成功:{\"action\": \"find_vul\", \"node\": \"节点\",\"vulnerability\": {\"name\":\"漏洞名称\",\"risk\":\"风险等级(低危/中危/高危)\",\"info\":\"补充信息(没有可为空)\"}}; |
||||
|
- 完成测试:{\"action\": \"end_work\", \"node\": \"节点\"}; |
||||
|
''' |
||||
|
strerror = "" |
||||
|
for node_json in node_cmds: |
||||
|
if "action" not in node_json: |
||||
|
self.logger.error(f"缺少action节点:{node_json}") |
||||
|
strerror = {"节点指令错误":f"{node_json}缺少action节点,不符合格式要求!"} |
||||
|
break |
||||
|
|
||||
|
action = node_json["action"] |
||||
|
if action == "add_node": |
||||
|
if "parent" not in node_json or "nodes" not in node_json: |
||||
|
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
||||
|
break |
||||
|
elif action == "end_work": |
||||
|
if "node" not in node_json: |
||||
|
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
||||
|
break |
||||
|
elif action =="no_instruction": |
||||
|
if "nodes" not in node_json: |
||||
|
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
||||
|
break |
||||
|
elif action =="find_vul": |
||||
|
if "node" not in node_json or "vulnerability" not in node_json: |
||||
|
strerror = {"节点指令错误": f"{node_json}不符合格式要求,缺少节点!"} |
||||
|
break |
||||
|
else: |
||||
|
strerror = {"节点指令错误": f"{node_json}不可识别的action值!"} |
||||
|
break |
||||
|
if not strerror: |
||||
|
return True,strerror |
||||
|
return False,strerror |
||||
|
|
||||
|
# 验证节点数据的合规性 |
||||
|
def verify_node_data(self,node_cmds): |
||||
|
add_nodes = [] |
||||
|
no_instr_nodes = [] |
||||
|
for node_cmd in node_cmds: |
||||
|
do_type = node_cmd["action"] |
||||
|
if do_type == "add_node": |
||||
|
nodes = node_cmd["nodes"].split(",") |
||||
|
add_nodes.extend(nodes) |
||||
|
elif do_type == "no_instruction": |
||||
|
nodes = node_cmd["nodes"].split(",") |
||||
|
no_instr_nodes.extend(nodes) |
||||
|
else:# 其他类型暂时不验证 |
||||
|
pass |
||||
|
#核对指令是否有缺失 |
||||
|
had_inst_nodes = self._difference_a_simple(add_nodes,no_instr_nodes) #在新增节点,但不在没有指令列表,就是应该要有指令的节点数据 |
||||
|
no_add_nodes = self._difference_a_simple(no_instr_nodes,add_nodes) #在未新增指令的节点,但不在新增节点,就是没有add的节点,需要新增 |
||||
|
return had_inst_nodes,no_add_nodes |
||||
|
|
||||
|
#--------------辅助函数----------------- |
||||
|
def get_path_from_command(self,command): |
||||
|
pass |
||||
|
|
||||
|
def _difference_a(self,list_a: list, list_b: list) -> list: |
||||
|
"""获取 list_a 中存在但 list_b 中不存在的元素(去重版)""" |
||||
|
set_b = set(list_b) |
||||
|
return [x for x in list_a if x not in set_b] |
||||
|
|
||||
|
def _difference_b(self,list_a: list, list_b: list) -> list: |
||||
|
"""获取 list_b 中存在但 list_a 中不存在的元素(去重版)""" |
||||
|
set_a = set(list_a) |
||||
|
return [x for x in list_b if x not in set_a] |
||||
|
|
||||
|
def _difference_a_keep_duplicates(self,list_a: list, list_b: list) -> list: |
||||
|
"""获取 list_a 中存在但 list_b 中不存在的元素(保留所有重复项和顺序)""" |
||||
|
set_b = set(list_b) |
||||
|
return [x for x in list_a if x not in set_b] |
||||
|
|
||||
|
def _difference_b_keep_duplicates(self,list_a: list, list_b: list) -> list: |
||||
|
"""获取 list_b 中存在但 list_a 中不存在的元素(保留所有重复项和顺序)""" |
||||
|
set_a = set(list_a) |
||||
|
return [x for x in list_b if x not in set_a] |
||||
|
|
||||
|
def _difference_a_simple(self,list_a: list, list_b: list) -> list: |
||||
|
"""集合差集:list_a - list_b""" |
||||
|
return list(set(list_a) - set(list_b)) |
||||
|
|
||||
|
def _difference_b_simple(self,list_a: list, list_b: list) -> list: |
||||
|
"""集合差集:list_b - list_a""" |
||||
|
return list(set(list_b) - set(list_a)) |
||||
|
|
||||
|
g_CV = CommandVerify() |
@ -1,8 +0,0 @@ |
|||||
import openai |
|
||||
from openai import OpenAI |
|
||||
#LLM的基类 |
|
||||
|
|
||||
class LLMBase: |
|
||||
def __init__(self): |
|
||||
pass |
|
||||
|
|
@ -0,0 +1,38 @@ |
|||||
|
import queue |
||||
|
import threading |
||||
|
import time |
||||
|
from mycode.PythoncodeTool import PythoncodeTool |
||||
|
|
||||
|
|
||||
|
class PythonTManager: |
||||
|
def __init__(self,maxnum): |
||||
|
self.brun = True |
||||
|
self.cur_num = 0 |
||||
|
self.put_lock = threading.Lock() |
||||
|
# 构建进程池--Python 代码的执行在子进程完成-3个子进程 |
||||
|
self.maxnum = maxnum |
||||
|
self.python_tool = PythoncodeTool(maxnum) #python工具实例 |
||||
|
|
||||
|
def __del__(self): |
||||
|
self.python_tool.shutdown() |
||||
|
|
||||
|
def execute_instruction(self,instruction): |
||||
|
bwork = False |
||||
|
while self.brun: |
||||
|
with self.put_lock: |
||||
|
if self.cur_num < self.maxnum: |
||||
|
self.cur_num += 1 |
||||
|
bwork = True |
||||
|
if bwork:#还有空的子进程 |
||||
|
#提交给进程池执行 |
||||
|
_,instruction,analysis,_,ext_params = self.python_tool.execute_instruction(instruction) |
||||
|
#执行完成后,数量减一 |
||||
|
with self.put_lock: |
||||
|
self.cur_num -= 1 |
||||
|
#返回结果 |
||||
|
return instruction,analysis,analysis,ext_params |
||||
|
else: #如果没获取的许可,则等待N秒后再尝试---存在问题:多线程间没有先来先到的机制了,有可能第一个来排队的一直等到最后 |
||||
|
time.sleep(20) #休眠20秒 |
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,230 @@ |
|||||
|
#python代码动态执行 |
||||
|
import queue |
||||
|
import ast |
||||
|
import subprocess |
||||
|
import json |
||||
|
import builtins |
||||
|
import re |
||||
|
import paramiko |
||||
|
import impacket |
||||
|
import psycopg2 |
||||
|
import socket |
||||
|
import struct |
||||
|
import sys |
||||
|
import requests |
||||
|
import ssl |
||||
|
import mysql.connector |
||||
|
import telnetlib |
||||
|
import time |
||||
|
import uuid |
||||
|
import multiprocessing |
||||
|
import textwrap |
||||
|
from mycode.Result_merge import my_merge |
||||
|
from ftplib import FTP |
||||
|
from requests.auth import HTTPBasicAuth |
||||
|
|
||||
|
from myutils.ReturnParams import ReturnParams |
||||
|
from concurrent.futures import ProcessPoolExecutor, TimeoutError |
||||
|
|
||||
|
# -------------------------------------------- |
||||
|
# 1) 全局 helper:放在模块顶层,才能被子进程 picklable 调用 |
||||
|
# -------------------------------------------- |
||||
|
def _execute_dynamic(instruction_str): |
||||
|
""" |
||||
|
在子进程中执行 instruction_str 所描述的 dynamic_fun, |
||||
|
并返回 (status: bool, output: str)。 |
||||
|
""" |
||||
|
# 允许的内置函数白名单 |
||||
|
allowed_builtins = { |
||||
|
'__name__': __name__, |
||||
|
'__import__': builtins.__import__, |
||||
|
'abs': abs, 'all': all, 'any': any, 'bool': bool, |
||||
|
'chr': chr, 'dict': dict, 'enumerate': enumerate, |
||||
|
'float': float, 'int': int, 'len': len, 'list': list, |
||||
|
'max': max, 'min': min, 'print': print, 'range': range, |
||||
|
'set': set, 'str': str, 'sum': sum, 'type': type, |
||||
|
'open': open, 'Exception': Exception, 'locals': locals |
||||
|
} |
||||
|
# 构造安全的 globals |
||||
|
safe_globals = { |
||||
|
'__builtins__': allowed_builtins, |
||||
|
'subprocess': subprocess, |
||||
|
'json': json, |
||||
|
're': re, |
||||
|
'paramiko': paramiko, |
||||
|
'impacket': impacket, |
||||
|
'psycopg2': psycopg2, |
||||
|
'socket': socket, |
||||
|
'mysql': mysql, |
||||
|
'mysql.connector': mysql.connector, |
||||
|
'struct': struct, |
||||
|
'sys': sys, |
||||
|
'requests': requests, |
||||
|
'ssl': ssl, |
||||
|
'FTP': FTP, |
||||
|
'HTTPBasicAuth': HTTPBasicAuth, |
||||
|
'telnetlib': telnetlib, |
||||
|
'time': time, |
||||
|
'uuid':uuid, |
||||
|
} |
||||
|
safe_locals = {} |
||||
|
try: |
||||
|
# 编译并执行用户提供的 code 字符串 |
||||
|
compiled = compile(instruction_str, '<dynamic>', 'exec') |
||||
|
exec(compiled, safe_globals, safe_locals) |
||||
|
|
||||
|
# dynamic_fun 必须存在 |
||||
|
if 'dynamic_fun' not in safe_locals: |
||||
|
return False, "Function dynamic_fun() 未定义" |
||||
|
|
||||
|
# 调用它并返回结果 |
||||
|
res = safe_locals['dynamic_fun']() |
||||
|
if not (isinstance(res, tuple) and len(res) == 2 and isinstance(res[0], bool)): |
||||
|
return False, "dynamic_fun 返回值格式不对" |
||||
|
return res |
||||
|
except MemoryError: |
||||
|
return False, "内存溢出" |
||||
|
except RecursionError: |
||||
|
return False, "递归深度过深" |
||||
|
except Exception as e: |
||||
|
return False, f"子进程执行出错: {e}" |
||||
|
|
||||
|
class PythoncodeTool(): |
||||
|
def __init__(self,max_num): |
||||
|
self.proc_pool = ProcessPoolExecutor(max_workers=max_num) |
||||
|
|
||||
|
def preprocess(self,code: str) -> str: |
||||
|
# 去掉最外层空行 |
||||
|
code = code.strip('\n') |
||||
|
# 去除多余缩进 |
||||
|
return textwrap.dedent(code) |
||||
|
|
||||
|
def is_safe_code(self,code): |
||||
|
# List of high-risk functions to block (can be adjusted based on requirements) |
||||
|
# 只屏蔽这些“完整”函数调用 |
||||
|
HIGH_RISK = { |
||||
|
'eval', # eval(...) |
||||
|
'exec', # exec(...) |
||||
|
'os.system', # os.system(...) |
||||
|
'subprocess.call', # subprocess.call(...) |
||||
|
'subprocess.Popen' # subprocess.Popen(...) |
||||
|
} |
||||
|
try: |
||||
|
tree = ast.parse(code) |
||||
|
for node in ast.walk(tree): |
||||
|
if isinstance(node, ast.Call): |
||||
|
fn = node.func |
||||
|
# 1) 裸 exec/eval |
||||
|
if isinstance(fn, ast.Name): |
||||
|
if fn.id in ('exec', 'eval'): |
||||
|
return False,"有高风险函数,暂不执行!" |
||||
|
|
||||
|
# 2) 模块级别的 os.system、subprocess.call、subprocess.Popen |
||||
|
elif isinstance(fn, ast.Attribute): |
||||
|
# value 必须是 Name,才算"模块.方法" |
||||
|
if isinstance(fn.value, ast.Name): |
||||
|
fullname = f"{fn.value.id}.{fn.attr}" |
||||
|
if fullname in HIGH_RISK: |
||||
|
return False,"有高风险函数,暂不执行!" |
||||
|
|
||||
|
return True,"" |
||||
|
|
||||
|
except SyntaxError as se: |
||||
|
# 语法都不通过,也算不安全 |
||||
|
print("解析失败!", se, "第", se.lineno, "行") |
||||
|
print("出错的那行是:", code.splitlines()[se.lineno - 1]) |
||||
|
return False,str(se) |
||||
|
|
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 60*10 |
||||
|
instr = instruction.replace("python_code ","") |
||||
|
instr = instr.replace("python-code ", "") |
||||
|
instr = self.preprocess(instr) |
||||
|
# Safety check |
||||
|
bsafe,error = self.is_safe_code((instr)) |
||||
|
if not bsafe: |
||||
|
return "", timeout,error |
||||
|
return instr,timeout,"" |
||||
|
|
||||
|
def safe_import(self,name,*args,**kwargs): |
||||
|
ALLOWED_MODULES = ['subprocess', 'json','re'] |
||||
|
if name not in ALLOWED_MODULES: |
||||
|
raise ImportError(f"Import of '{name}' is not allowed") |
||||
|
return builtins.__import__(name, *args, **kwargs) |
||||
|
|
||||
|
def _run_dynamic(self, safe_locals, q): |
||||
|
"""子进程执行 dynamic_fun 并把结果放入队列""" |
||||
|
try: |
||||
|
fn = safe_locals['dynamic_fun'] |
||||
|
res = fn() |
||||
|
q.put(res) |
||||
|
except Exception as e: |
||||
|
q.put((False, f"执行出错: {e}")) |
||||
|
|
||||
|
def execute_instruction(self, instruction_old): |
||||
|
''' |
||||
|
执行指令:验证合法性 -> 执行 -> 分析结果 |
||||
|
:param instruction_old: |
||||
|
:return: |
||||
|
bool:true-正常返回给大模型,false-结果不返回给大模型 |
||||
|
str:执行的指令 |
||||
|
str:执行指令的结果 |
||||
|
''' |
||||
|
ext_params = ReturnParams() |
||||
|
ext_params["is_user"] = False # 是否要提交用户确认 -- 默认False |
||||
|
ext_params["is_vulnerability"] = False # 是否是脆弱点 |
||||
|
|
||||
|
# 第一步:验证指令合法性 |
||||
|
instruction,time_out,error = self.validate_instruction(instruction_old) |
||||
|
if not instruction: |
||||
|
return False, instruction_old, error,"",ext_params |
||||
|
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
||||
|
|
||||
|
# 第二步:执行指令 |
||||
|
future = self.proc_pool.submit(_execute_dynamic, instruction) |
||||
|
try: |
||||
|
# 在主进程中等待结果,超时则抛 TimeoutError |
||||
|
status, tmpout = future.result(timeout=time_out) #这里是阻塞的 |
||||
|
except TimeoutError: |
||||
|
# 超时处理 |
||||
|
future.cancel() |
||||
|
status, tmpout = False, f"执行超时({time_out} 秒)" |
||||
|
except Exception as e: |
||||
|
# 其他异常 |
||||
|
status, tmpout = False, f"提交子进程运行出错: {e}" |
||||
|
output = f"status:{status},output:{tmpout}" |
||||
|
|
||||
|
# 第三步:分析执行结果 |
||||
|
analysis = self.analyze_result(output, instruction,"","") |
||||
|
# 指令和结果入数据库 |
||||
|
# ? |
||||
|
if not analysis: # analysis为“” 不提交LLM |
||||
|
return False, instruction, analysis,"",ext_params |
||||
|
return True, instruction, analysis,"",ext_params |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 --- 要不要限定一个max_len? |
||||
|
if "enum4linux " in instruction: #存在指令包装成Python代码返回的情况 |
||||
|
result = my_merge("enum4linux",result) |
||||
|
else: |
||||
|
if len(result) > 3000: #超过2000长度时,尝试去重重复行 |
||||
|
lines = result.splitlines() |
||||
|
seen = set() |
||||
|
unique_lines = [] |
||||
|
for line in lines: |
||||
|
if line not in seen: |
||||
|
seen.add(line) |
||||
|
unique_lines.append(line) |
||||
|
return "\n".join(unique_lines) |
||||
|
return result |
||||
|
|
||||
|
#关闭进程池 |
||||
|
def shutdown(self): |
||||
|
self.proc_pool.shutdown(wait=True) |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
llm_code = """ |
||||
|
def run_test(): |
||||
|
return 'Penetration test executed successfully!' |
||||
|
""" |
@ -0,0 +1,5 @@ |
|||||
|
|
||||
|
class RsikManager: |
||||
|
def __init__(self): |
||||
|
pass |
||||
|
|
@ -0,0 +1,233 @@ |
|||||
|
from myutils.ConfigManager import myCongif |
||||
|
from mycode.TaskObject import TaskObject |
||||
|
from mycode.DBManager import app_DBM |
||||
|
from myutils.PickleManager import g_PKM |
||||
|
|
||||
|
import threading |
||||
|
|
||||
|
class TaskManager: |
||||
|
def __init__(self): |
||||
|
self.tasks = {} # 执行中的任务,test_id为key |
||||
|
self.num_threads = myCongif.get_data("Task_max_threads") |
||||
|
#获取系统信息 -- 用户可修改的都放在DB中,不修改的放config |
||||
|
data = app_DBM.get_system_info() |
||||
|
self.local_ip = data[0] |
||||
|
self.version = data[1] |
||||
|
self.tasks_lock = threading.Lock() #加个线程锁?不使用1,quart要使用的异步锁,2.限制只允许一个用户登录,3.遍历到删除的问题不大 |
||||
|
self.web_cur_task = 0 #web端当前显示的 |
||||
|
|
||||
|
|
||||
|
#判断目标是不是在当前执行任务中,---没加锁,最多跟删除有冲突,问题应该不大 |
||||
|
def is_target_in_tasks(self,task_target): |
||||
|
for task in self.tasks.values(): |
||||
|
if task_target == task.target: |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
#程序启动后,加载未完成的测试任务 |
||||
|
def load_tasks(self): |
||||
|
'''程序启动时,加载未执行完成的任务''' |
||||
|
datas = app_DBM.get_run_tasks() |
||||
|
for data in datas: |
||||
|
task_id = data[0] |
||||
|
task_target = data[1] |
||||
|
task_status = data[2] |
||||
|
work_type = data[3] |
||||
|
cookie_info = data[4] |
||||
|
llm_type = data[5] |
||||
|
# 创建任务对象 |
||||
|
task = TaskObject(task_target, cookie_info, work_type, llm_type, self.num_threads, self.local_ip,self) |
||||
|
#读取attact_tree |
||||
|
attack_tree = g_PKM.ReadData(str(task_id)) |
||||
|
#开始任务 ---会根据task_status来判断是否需要启动工作线程 |
||||
|
task.start_task(task_id,task_status,attack_tree) |
||||
|
# 保留task对象 |
||||
|
self.tasks[task_id] = task |
||||
|
|
||||
|
#新建测试任务 |
||||
|
def create_task(self, test_target,cookie_info,llm_type,work_type): |
||||
|
"""创建新任务--create和load复用?-- |
||||
|
1.创建和初始化task_object; |
||||
|
2.创建task_id |
||||
|
3.启动工作线程 |
||||
|
return T/F |
||||
|
""" |
||||
|
if self.is_target_in_tasks(test_target): |
||||
|
raise ValueError(f"Task {test_target} already exists") |
||||
|
#创建任务对象 |
||||
|
task = TaskObject(test_target,cookie_info,work_type,llm_type,self.num_threads,self.local_ip,self) |
||||
|
#获取task_id -- test_target,cookie_info,work_type,llm_type 入数据库 |
||||
|
task_id = app_DBM.start_task(test_target,cookie_info,work_type,llm_type) |
||||
|
if task_id >0: |
||||
|
#创建后启动工作--同时赋值task_id |
||||
|
task.start_task(task_id) |
||||
|
#保留task对象 |
||||
|
self.tasks[task_id] = task |
||||
|
return True |
||||
|
else: |
||||
|
return False |
||||
|
|
||||
|
def over_task(self,task_id): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
task.brun = False |
||||
|
#修改数据库数据 |
||||
|
bsuccess = app_DBM.over_task(task_id) |
||||
|
if bsuccess: |
||||
|
del self.tasks[task_id] #删除缓存 |
||||
|
return bsuccess,"" |
||||
|
else: |
||||
|
return False,"没有找到对应的任务" |
||||
|
|
||||
|
def del_task(self,task_id): |
||||
|
if g_PKM.DelData(str(task_id)): |
||||
|
bsuccess = app_DBM.del_task(task_id) |
||||
|
return bsuccess,"" |
||||
|
else: |
||||
|
return False,"删除对应文件失败" |
||||
|
|
||||
|
#控制task启停----线程不停 |
||||
|
def control_taks(self,task_id): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
if task.task_status == 0: # 0-暂停,1-执行中,2-已完成 |
||||
|
task.task_status = 1 |
||||
|
elif task.task_status == 1: |
||||
|
task.task_status = 0 |
||||
|
else: |
||||
|
return False,"当前任务状态不允许修改,请联系管理员!",task.task_status |
||||
|
else: |
||||
|
return False,"没有找到对应的任务",None |
||||
|
return True,"",task.task_status |
||||
|
|
||||
|
# 获取任务list |
||||
|
def get_task_list(self): |
||||
|
tasks = [] |
||||
|
for task in self.tasks.values(): |
||||
|
one_json = {"taskID": task.task_id, "testTarget": task.target, "taskStatus": task.task_status, "safeRank": task.safe_rank, |
||||
|
"workType": task.work_type} |
||||
|
tasks.append(one_json) |
||||
|
rejson = {"tasks": tasks} |
||||
|
return rejson |
||||
|
|
||||
|
#获取节点树 |
||||
|
def get_node_tree(self,task_id): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
self.web_cur_task = task_id |
||||
|
tree_dict = task.attack_tree.get_node_dict() |
||||
|
return tree_dict |
||||
|
return None |
||||
|
|
||||
|
#获取历史节点树数据 |
||||
|
def get_his_node_tree(self,task_id): |
||||
|
attack_tree = g_PKM.ReadData(str(task_id)) |
||||
|
if attack_tree: |
||||
|
tree_dict = attack_tree.get_node_dict() |
||||
|
return tree_dict |
||||
|
return None |
||||
|
|
||||
|
#修改任务的工作模式,只有在暂停状态才能修改 |
||||
|
def update_task_work_type(self,task_id,new_work_type): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
if task.task_status == 0: |
||||
|
task.work_type = new_work_type |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
#控制节点的工作状态 |
||||
|
def node_bwork_control(self,task_id,node_path): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
bsuccess,new_bwork = task.attack_tree.update_node_bwork(node_path) |
||||
|
if bsuccess: |
||||
|
pass #是否要更新IO数据?----待验证是否有只修改部分数据的方案 |
||||
|
return bsuccess,new_bwork |
||||
|
return False,False |
||||
|
|
||||
|
#节点单步--只允許web端调用 |
||||
|
async def node_one_step(self,task_id,node_path): |
||||
|
task = self.tasks[task_id] |
||||
|
node = task.attack_tree.find_node_by_nodepath(node_path) |
||||
|
#web端触发的任务,需要判断当前的执行状态 |
||||
|
bsuccess,error = await task.put_one_node(node) |
||||
|
return bsuccess,error |
||||
|
|
||||
|
#任务单点--只允许web端调用 |
||||
|
async def task_one_step(self,task_id): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
bsuccess,error = await task.put_one_task() |
||||
|
return bsuccess,error |
||||
|
else: |
||||
|
return False,"task_id值存在问题!" |
||||
|
|
||||
|
#获取节点待执行任务 |
||||
|
def get_task_node_todo_instr(self,task_id,nodepath): |
||||
|
todoinstr = [] |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
node = task.attack_tree.find_node_by_nodepath(nodepath) |
||||
|
if node: |
||||
|
todoinstr = node.get_instr_user() |
||||
|
return todoinstr |
||||
|
|
||||
|
#获取节点的MSG信息 |
||||
|
def get_task_node_MSG(self,task_id,nodepath): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
node = task.attack_tree.find_node_by_nodepath(nodepath) |
||||
|
if node: |
||||
|
tmpMsg = node.get_res_user() |
||||
|
if tmpMsg: |
||||
|
return node.messages,tmpMsg[0] #待提交消息正常应该只有一条 |
||||
|
else: |
||||
|
return node.messages,{} |
||||
|
return [],{} |
||||
|
|
||||
|
def update_node_MSG(self,task_id,nodepath,newtype,newcontent): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
node = task.attack_tree.find_node_by_nodepath(nodepath) |
||||
|
if node: |
||||
|
work_status = node.get_work_status() |
||||
|
if work_status == 0 or work_status == 3: |
||||
|
bsuccess,error = node.updatemsg(newtype,newcontent,0) #取的第一条,也就修改第一条 |
||||
|
return bsuccess,error |
||||
|
else: |
||||
|
return False,"当前节点的工作状态不允许修改MSG!" |
||||
|
return False,"找不到对应节点!" |
||||
|
|
||||
|
def del_node_instr(self,task_id,nodepath,instr): |
||||
|
task = self.tasks[task_id] |
||||
|
if task: |
||||
|
node = task.attack_tree.find_node_by_nodepath(nodepath) |
||||
|
if node: |
||||
|
return node.del_instr(instr) |
||||
|
return False,"找不到对应节点!" |
||||
|
|
||||
|
def get_his_tasks(self,target_name,safe_rank,llm_type,start_time,end_time): |
||||
|
tasks = app_DBM.get_his_tasks(target_name,safe_rank,llm_type,start_time,end_time) |
||||
|
return tasks |
||||
|
|
||||
|
|
||||
|
#------------以下函数还未验证处理----------- |
||||
|
|
||||
|
def start_task(self, task_id): |
||||
|
"""启动指定任务""" |
||||
|
task = self.tasks.get(task_id) |
||||
|
if task: |
||||
|
task.start(self.num_threads) |
||||
|
else: |
||||
|
print(f"Task {task_id} not found") |
||||
|
|
||||
|
def stop_task(self, task_id): |
||||
|
"""停止指定任务""" |
||||
|
task = self.tasks.get(task_id) |
||||
|
if task: |
||||
|
task.stop() |
||||
|
else: |
||||
|
print(f"Task {task_id} not found") |
||||
|
|
||||
|
g_TaskM = TaskManager() #单一实例 |
@ -0,0 +1,670 @@ |
|||||
|
''' |
||||
|
渗透测试任务管理类 一次任务的闭合性要检查2025-3-10 一次任务后要清理LLM和InstrM的数据 |
||||
|
''' |
||||
|
from mycode.TargetManager import TargetManager # 从模块导入类 |
||||
|
#from LLMManager import LLMManager # 同理修正其他导入 |
||||
|
from mycode.ControlCenter import ControlCenter #控制中心替代LLM--控制中心要实现一定的基础逻辑和渗透测试树的维护。 |
||||
|
from mycode.InstructionManager import g_instrM |
||||
|
from mycode.DBManager import DBManager,app_DBM |
||||
|
from mycode.LLMManager import LLMManager |
||||
|
from mycode.AttackMap import AttackTree,TreeNode |
||||
|
from myutils.MyTime import get_local_timestr |
||||
|
from myutils.MyLogger_logger import LogHandler |
||||
|
from myutils.PickleManager import g_PKM |
||||
|
from myutils.ConfigManager import myCongif |
||||
|
from mycode.WebSocketManager import g_WSM |
||||
|
from mycode.CommandVerify import g_CV |
||||
|
from mycode.PythonTManager import PythonTManager |
||||
|
import asyncio |
||||
|
import queue |
||||
|
import time |
||||
|
import os |
||||
|
import re |
||||
|
import threading |
||||
|
import json |
||||
|
|
||||
|
class TaskObject: |
||||
|
|
||||
|
def __init__(self,test_target,cookie_info,work_type,llm_type,num_threads,local_ip,taskM): |
||||
|
#功能类相关 |
||||
|
self.taskM = taskM |
||||
|
self.logger = LogHandler().get_logger("TaskObject") |
||||
|
self.InstrM = g_instrM # 类对象渗透,要约束只读取信息,且不允许有类全局对象--持续检查 |
||||
|
self.PythonM = PythonTManager(myCongif.get_data("Python_max_procs")) |
||||
|
self.CCM = ControlCenter() #一个任务一个CCM |
||||
|
self.LLM = LLMManager(llm_type) # LLM对象调整为一个任务一个对象,这样可以为不同的任务选择不同的LLM |
||||
|
#全局变量 |
||||
|
self.app_work_type = myCongif.get_data("App_Work_type") #app工作为0时,只允许单步模式工作,是附加规则,不影响正常逻辑处理 |
||||
|
self.brun = False #任务的停止可以用该变量来控制 |
||||
|
self.sleep_time = myCongif.get_data("sleep_time") |
||||
|
self.target = test_target |
||||
|
self.cookie = cookie_info |
||||
|
self.work_type = work_type #工作模式 0-人工,1-自动 |
||||
|
self.task_id = None |
||||
|
self.task_status = 0 #0-暂停,1-执行中,2-已完成 |
||||
|
self.local_ip = local_ip |
||||
|
self.attack_tree = None #任务节点树 |
||||
|
self.safe_rank = 0 #安全级别 0-9 #?暂时还没实现更新逻辑 |
||||
|
|
||||
|
#指令执行相关------- |
||||
|
self.max_thread_num = num_threads #指令执行线程数量 |
||||
|
self.workth_list = [None] * num_threads #线程句柄list |
||||
|
self.doing_instr_list= [""] * num_threads |
||||
|
self.instr_node_queue = queue.Queue() #待执行指令的节点队列 |
||||
|
self.node_num = 0 #在处理Node线程的处理 |
||||
|
#llm执行相关-------- |
||||
|
self.llm_max_nums = myCongif.get_data("LLM_max_threads") # 控制最大并发指令数量 --- 多线程的话节点树需要加锁 |
||||
|
self.llmth_list = [None] * self.llm_max_nums # llm线程list |
||||
|
self.doing_llm_list = [""] * self.llm_max_nums |
||||
|
self.llm_node_queue = queue.Queue() #待提交LLM的节点队列 |
||||
|
#自检线程-------- |
||||
|
self.check_th = None #自检线程句柄 |
||||
|
#-----四队列----- |
||||
|
self.run_instr_lock = threading.Lock() # 线程锁 |
||||
|
self.runing_instr = {} #执行中指令记录 |
||||
|
|
||||
|
#---------------三个线程------------ |
||||
|
#测试指令执行线程 |
||||
|
def mill_instr_preprocess(self,instructions,str_split): |
||||
|
new_instr = [] |
||||
|
instrs = instructions.split(str_split) |
||||
|
index = 0 |
||||
|
for instr in instrs: |
||||
|
if instr.strip().startswith("curl"): |
||||
|
if " --max-time " not in instr: |
||||
|
out_time = g_instrM.get_tool_out_time("curl") |
||||
|
instr = instr.strip() + f" --max-time {str(out_time)}" |
||||
|
#instr = instr.strip() + " --max-time 10" |
||||
|
new_instr.append(instr) |
||||
|
index += 1 |
||||
|
new_star_instr = f"{str_split}".join(new_instr) |
||||
|
print(new_star_instr) |
||||
|
return new_star_instr |
||||
|
|
||||
|
def do_worker_th(self,index): |
||||
|
#线程的dbm需要一个线程一个 |
||||
|
th_DBM = DBManager() |
||||
|
th_DBM.connect() |
||||
|
th_index = index |
||||
|
while self.brun: |
||||
|
if self.task_status == 1: |
||||
|
try: |
||||
|
work_node = self.instr_node_queue.get(block=False)#正常一个队列中一个节点只会出现一次,进而也就只有一个线程在处理 |
||||
|
# 开始执行指令 |
||||
|
results = [] |
||||
|
while True: |
||||
|
instruction = work_node.get_instr() |
||||
|
if not instruction: |
||||
|
break |
||||
|
instruction = instruction.strip() |
||||
|
#对多shell指令的情况进行处理--也有风险 |
||||
|
if "python-code" not in instruction: |
||||
|
if "&&" in instruction: |
||||
|
instruction = self.mill_instr_preprocess(instruction, "&&") |
||||
|
elif "||" in instruction: |
||||
|
instruction = self.mill_instr_preprocess(instruction, "||") |
||||
|
|
||||
|
start_time = get_local_timestr() # 指令执行开始时间 |
||||
|
self.doing_instr_list[th_index] = instruction |
||||
|
if instruction.startswith("python-code"):#python代码--超过子进程数会阻塞等待,但不开始超时记时 |
||||
|
instr, reslut, source_result, ext_params = self.PythonM.execute_instruction(instruction) |
||||
|
else:#shell指令 |
||||
|
instr, reslut, source_result, ext_params = self.InstrM.execute_instruction(instruction) |
||||
|
self.doing_instr_list[th_index] = "" |
||||
|
end_time = get_local_timestr() # 指令执行结束时间 |
||||
|
|
||||
|
# 入数据库 -- bres True和False 都入数据库2025-3-10---加node_path(2025-3-18)#? |
||||
|
if th_DBM.ok: |
||||
|
work_node.do_sn += 1 |
||||
|
th_DBM.insetr_result(self.task_id, instr, reslut, work_node.do_sn, start_time, end_time, |
||||
|
source_result, |
||||
|
ext_params, work_node.path) |
||||
|
else: |
||||
|
self.logger.error("数据库连接失败!!") |
||||
|
#暂存结果 |
||||
|
oneres = {'执行指令': instr, '结果': reslut} |
||||
|
results.append(oneres) |
||||
|
#指令都执行结束后,入节点待提交队列 |
||||
|
str_res = json.dumps(results, ensure_ascii=False) |
||||
|
# 提交llm待处理任务 --更新节点work_status |
||||
|
self.put_node_reslist(work_node, str_res, 1) |
||||
|
# 保存记录 |
||||
|
g_PKM.WriteData(self.attack_tree,str(self.task_id)) |
||||
|
except queue.Empty: |
||||
|
time.sleep(self.sleep_time) |
||||
|
else:#不是工作状态则休眠 |
||||
|
time.sleep(self.sleep_time) |
||||
|
|
||||
|
#llm请求提交线程 |
||||
|
def th_llm_worker(self,index): |
||||
|
''' |
||||
|
几个规则--TM的work线程同 |
||||
|
1.线程获取一个节点后,其他线程不能再获取这个节点(遇到被执行的节点,直接放弃执行)--- 加了没办法保存中间结果进行测试 |
||||
|
2.llm返回的指令,只可能是该节点,或者是子节点的,不符合这条规则的都不处理,避免llm处理混乱。 |
||||
|
:return: |
||||
|
''' |
||||
|
# 线程的dbm需要一个线程一个 |
||||
|
th_DBM = DBManager() |
||||
|
th_DBM.connect() |
||||
|
th_index = index |
||||
|
while self.brun: |
||||
|
if self.task_status == 1: |
||||
|
try: |
||||
|
llm_node = self.llm_node_queue.get(block=False) #获取一个待处理节点 |
||||
|
#开始处理 |
||||
|
tmp_commands = [] |
||||
|
# {llm_node.status} --- 暂时固化为未完成 |
||||
|
user_Prompt = f''' |
||||
|
当前分支路径:{llm_node.path} |
||||
|
当前节点信息: |
||||
|
- 节点名称:{llm_node.name} |
||||
|
- 节点状态:未完成 |
||||
|
- 漏洞类型:{llm_node.vul_type} |
||||
|
''' |
||||
|
while True: |
||||
|
llm_data = llm_node.get_res() |
||||
|
if llm_data is None: |
||||
|
break |
||||
|
llm_type = llm_data["llm_type"] |
||||
|
str_res = llm_data["result"] |
||||
|
#获取提示词 |
||||
|
prompt = self.get_llm_prompt(llm_type,str_res,user_Prompt) |
||||
|
self.doing_llm_list[th_index] = prompt |
||||
|
# 提交llm请求返回数据--并对返回数据进行处理,节点指令直接执行,测试指令根据工作模式执行 |
||||
|
node_cmds, commands,reasoning_content, content, post_time = self.LLM.get_llm_instruction(prompt,llm_node) # message要更新 |
||||
|
self.doing_llm_list[th_index] = "" |
||||
|
# LLM记录存数据库 |
||||
|
if th_DBM.ok: |
||||
|
llm_node.llm_sn += 1 |
||||
|
bres = th_DBM.insert_llm(self.task_id, prompt, reasoning_content, content, post_time, llm_node.llm_sn,llm_node.path) |
||||
|
if not bres: |
||||
|
self.logger.error(f"{llm_node.name}-llm入库失败!") |
||||
|
else: |
||||
|
self.logger.error("数据库连接失败!") |
||||
|
''' |
||||
|
对于LLM返回的错误处理机制 |
||||
|
1.验证节点是否都有测试指令返回 |
||||
|
2.LLM的回复开始反复时(有点难判断) |
||||
|
''' |
||||
|
# 更新tree |
||||
|
bok, new_commands,iadd_node = self.tree_manager(node_cmds, llm_node, commands, th_DBM) |
||||
|
# 分析指令入对应节点 |
||||
|
if bok: # 节点指令若存在错误,测试指令都不处理,需要LLM重新生成 |
||||
|
tmp_commands.extend(new_commands) |
||||
|
#测试指令入节点待处理队列 --同时修改节点的work_status |
||||
|
self.put_node_instrlist(tmp_commands, llm_node,iadd_node) |
||||
|
#一个节点完成,节点树持久化---待验证是否有局部更新持久化的方案 |
||||
|
g_PKM.WriteData(self.attack_tree,str(self.task_id)) |
||||
|
except queue.Empty: |
||||
|
#self.logger.debug("llm队列中暂时无新的提交任务!") |
||||
|
time.sleep(self.sleep_time) |
||||
|
else: |
||||
|
time.sleep(self.sleep_time) |
||||
|
|
||||
|
#自检线程 --1.输出执行状态。2.需要自检和修复 |
||||
|
def th_check(self): |
||||
|
while self.brun: |
||||
|
try: |
||||
|
cur_time = get_local_timestr() |
||||
|
print(f"-----------当前时间程序运行情况:{cur_time}") |
||||
|
# #待执行instr-node |
||||
|
# instr_node_list = list(self.instr_node_queue.queue) #待执行指令的node--线程不安全 |
||||
|
# print(f"**当前待执行指令node的数量为:{len(instr_node_list)}") |
||||
|
#执行中instr-node |
||||
|
index = 0 |
||||
|
for w_th in self.workth_list: |
||||
|
if not w_th.is_alive():#线程 |
||||
|
print(f"线程-{index}已处于异常状态,需要重新创建一个工作线程") |
||||
|
else: |
||||
|
print(f"线程-{index}在执行指令:{self.doing_instr_list[index]}") |
||||
|
index += 1 |
||||
|
|
||||
|
index = 0 |
||||
|
for l_th in self.llmth_list: |
||||
|
if not l_th.is_alive(): |
||||
|
print(f"LLM线程-{index}已处于异常状态,需要重新创建一个LLM线程") |
||||
|
else: |
||||
|
print(f"LLM线程-{index}在执行指令:{self.doing_llm_list[index]}") |
||||
|
index += 1 |
||||
|
#待提交llm-node |
||||
|
# llm_node_list = list(self.llm_node_queue.queue) #待提交llm的node--线程不安全 |
||||
|
# print(f"**当前待提交llm的node的数量为:{len(llm_node_list)}") |
||||
|
#休眠60 |
||||
|
time.sleep(60) |
||||
|
except Exception as e: |
||||
|
print(f"*********自检线程异常退出:{str(e)}") |
||||
|
break |
||||
|
|
||||
|
#------------入两个nodeMQ-禁止直接调用入队列----------- |
||||
|
def put_instr_mq(self,node): |
||||
|
#这里不做状态的判断,调用前处理 |
||||
|
self.instr_node_queue.put(node) |
||||
|
self.update_node_work_status(node,2) #在执行--1.work_status不影响整个任务的执行,错了问题不大,2--attack_tree持久化需要出去lock信息。 |
||||
|
|
||||
|
def put_llm_mq(self,node): |
||||
|
#同instr_mq |
||||
|
self.llm_node_queue.put(node) |
||||
|
self.update_node_work_status(node,4) #提交中 |
||||
|
|
||||
|
async def put_instr_mq_async(self,node): |
||||
|
#这里不做状态的判断,调用前处理 |
||||
|
self.instr_node_queue.put(node) |
||||
|
await self.update_node_work_status_async(node,2) #在执行--1.work_status不影响整个任务的执行,错了问题不大,2--attack_tree持久化需要出去lock信息。 |
||||
|
|
||||
|
async def put_llm_mq_async(self,node): |
||||
|
#同instr_mq |
||||
|
self.llm_node_queue.put(node) |
||||
|
await self.update_node_work_status_async(node,4) #提交中 |
||||
|
|
||||
|
async def update_node_work_status_async(self,node,work_status): |
||||
|
#更新状态 |
||||
|
bchange = node.update_work_status(work_status) |
||||
|
#基于websocket推送到前端 |
||||
|
if bchange: |
||||
|
#判断是否是web端最新获取数据的task |
||||
|
if self.taskM.web_cur_task == self.task_id: |
||||
|
idatatype = 1 |
||||
|
strdata = {"node_path":node.path,"node_workstatus":work_status} |
||||
|
await g_WSM.send_data(idatatype,strdata) |
||||
|
|
||||
|
#------------入Node的两个list--禁止直接调用入list------- |
||||
|
def put_node_reslist(self, node, str_res, llm_type): |
||||
|
# 推送llm提交任务到节点的待处理任务中,并根据工作模式判断是否如llm_node_quere |
||||
|
one_llm = {'llm_type': llm_type, 'result': str_res} |
||||
|
node.add_res(one_llm) # 入节点结果队列 |
||||
|
self.update_node_work_status(node,3) #待提交llm |
||||
|
# 如果是自动执行的模式则入队列交给llm线程处理 |
||||
|
if self.app_work_type == 1: |
||||
|
if self.work_type == 1 and node.bwork: |
||||
|
self.put_llm_mq(node) #变4 |
||||
|
|
||||
|
#递归找节点 |
||||
|
def find_node_by_child_node_name(self,cur_node,node_name): |
||||
|
find_node = None |
||||
|
if cur_node.children: |
||||
|
for child_node in cur_node.children: |
||||
|
if child_node.name == node_name: |
||||
|
find_node = child_node |
||||
|
break |
||||
|
else: |
||||
|
find_node = self.find_node_by_child_node_name(child_node,node_name) |
||||
|
if find_node: |
||||
|
break |
||||
|
return find_node |
||||
|
|
||||
|
def put_node_instrlist(self, commands, node,iadd_node): #如果当前节点没有进一般指令返回,需要修改节点执行状态 |
||||
|
node_list = [] #一次返回的测试指令 |
||||
|
for command in commands: |
||||
|
# 使用正则匹配方括号中的node_path(非贪婪模式) |
||||
|
match = re.search(r'\[(.*?)\]', command) |
||||
|
if match: |
||||
|
node_path = match.group(1) |
||||
|
instruction = re.sub(r'\[.*?\]', "", command, count=1, flags=re.DOTALL) |
||||
|
#'''强制约束,不是本节点或者是子节点的指令不处理''' |
||||
|
find_node = self.attack_tree.find_node_by_nodepath_parent(node_path,node,iadd_node,commands) |
||||
|
if not find_node:#对于没有基于节点路径找到对应节点--增加通过节点名称匹配的机制 2025-4-13日添加 |
||||
|
node_name = node_path.split("->")[-1] |
||||
|
find_node = self.find_node_by_child_node_name(node,node_name) |
||||
|
|
||||
|
if find_node: |
||||
|
find_node.add_instr(instruction) |
||||
|
#DS-llm存在返回指令还会修改节点状态为已完成的问题,需要修正 |
||||
|
find_node.status = "未完成" |
||||
|
if find_node not in node_list: |
||||
|
node_list.append(find_node) |
||||
|
self.update_node_work_status(find_node,1) #待执行 |
||||
|
else:#如果还没找到就暂时放弃 |
||||
|
self.logger.error(f"基于节点路径没有找到对应的节点{node_path},父节点都没找到!当前节点{node.path}")#丢弃该指令 |
||||
|
else: |
||||
|
self.logger.error(f"得到的指令格式不符合规范:{command}")#丢弃该指令--- |
||||
|
#这里对于丢弃指令,有几种方案: |
||||
|
# 1.直接丢弃不处理,但需要考虑会不会产生节点缺失指令的问题,需要有机制验证节点;------ 需要有个独立线程,节点要加锁--首选待改进方案 |
||||
|
# 2.入当前节点的res_queue,但有可能当前节点没有其他指令,不会触发提交,另外就算提交,会不会产生预设范围外的返回,不确定; |
||||
|
# 3.独立队列处理 |
||||
|
#判断当前节点是否有指令 |
||||
|
if node not in node_list: |
||||
|
#修改该节点状态为0--无待执行任务 |
||||
|
self.update_node_work_status(node,0) |
||||
|
#入instr队列 |
||||
|
if self.app_work_type == 1: |
||||
|
if self.work_type == 1: #是自动执行模式 |
||||
|
for node in node_list: |
||||
|
if node.bwork: |
||||
|
self.put_instr_mq(node) #2-执行中 |
||||
|
|
||||
|
def put_work_node(self): |
||||
|
'''遍历节点需要处理的任务,提交mq''' |
||||
|
nodes = self.attack_tree.traverse_bfs() |
||||
|
for node in nodes: |
||||
|
if not node.is_instr_empty(): #待执行指令有值 |
||||
|
if not node.is_llm_empty(): |
||||
|
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
||||
|
else: |
||||
|
if node.bwork: |
||||
|
self.put_instr_mq(node) #提交执行 |
||||
|
elif not node.is_llm_empty(): #待提交llm有值 |
||||
|
if not node.is_instr_empty(): |
||||
|
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
||||
|
else: |
||||
|
if node.bwork: |
||||
|
self.put_llm_mq(node) #提交执行 |
||||
|
|
||||
|
#web端提交单步任务--节点单步 |
||||
|
async def put_one_node(self,node): |
||||
|
#提交某个节点的代表任务 |
||||
|
if self.task_status ==1 and self.work_type==0 and node.bwork: |
||||
|
if not node.is_instr_empty(): #待执行指令有值 |
||||
|
if not node.is_llm_empty(): |
||||
|
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
||||
|
return False,"该节点的待执行任务数据不正确,请联系管理员!" |
||||
|
else: |
||||
|
if node.bwork: |
||||
|
await self.put_instr_mq_async(node) #提交执行 |
||||
|
elif not node.is_llm_empty(): #待提交llm有值 |
||||
|
if not node.is_instr_empty(): |
||||
|
self.logger.error(f"{node.path}即存在待执行指令,还存在待提交的llm,需要人工介入!!") |
||||
|
return False, "该节点的待执行任务数据不正确,请联系管理员!" |
||||
|
else: |
||||
|
if node.bwork: |
||||
|
await self.put_llm_mq_async(node) #提交执行 |
||||
|
else: |
||||
|
await self.update_node_work_status_async(node,0) #只是修补措施,保障状态的一致性 |
||||
|
return False,"当前节点没有待执行任务!" |
||||
|
return True,"已提交单步任务" |
||||
|
else: |
||||
|
return False,"当前的任务或节点状态不允许执行单步,请检查!" |
||||
|
|
||||
|
#web端提交任务单步--任务单步 |
||||
|
async def put_one_task(self): |
||||
|
if self.task_status == 1 and self.work_type == 0: |
||||
|
nodes = self.attack_tree.traverse_bfs() |
||||
|
for node in nodes: |
||||
|
await self.put_one_node(node) |
||||
|
return True,"已提交单步任务" |
||||
|
else: |
||||
|
return False,"当前的任务状态不允许执行单步,请检查!" |
||||
|
|
||||
|
#修改节点的执行状态,并需要基于websocket推送到前端显示 同步线程调用 |
||||
|
def update_node_work_status(self,node,work_status): |
||||
|
#更新状态 |
||||
|
bchange = node.update_work_status(work_status) |
||||
|
#基于websocket推送到前端 |
||||
|
if bchange: |
||||
|
#判断是否是web端最新获取数据的task |
||||
|
if self.taskM.web_cur_task == self.task_id: |
||||
|
idatatype = 1 |
||||
|
strdata = {"node_path":node.path,"node_workstatus":work_status} |
||||
|
asyncio.run(g_WSM.send_data(idatatype,strdata)) |
||||
|
|
||||
|
#获取本次的提交提示词 |
||||
|
def get_llm_prompt(self,llm_type,str_res,user_Prompt): |
||||
|
if llm_type == 0: |
||||
|
ext_Prompt = f''' |
||||
|
初始信息:{str_res} |
||||
|
任务:请开始对该目标的渗透测试工作。 |
||||
|
''' |
||||
|
elif llm_type == 1: # 提交指令执行结果 --- 正常提交 |
||||
|
# 构造本次提交的prompt |
||||
|
ext_Prompt = f''' |
||||
|
上一步结果:{str_res} |
||||
|
任务:生成下一步渗透测试指令或判断是否完成该节点测试。 |
||||
|
''' |
||||
|
elif llm_type == 2: # llm返回的指令存在问题,需要再次请求返回 |
||||
|
ext_Prompt = f''' |
||||
|
反馈类型:节点指令格式错误 |
||||
|
错误信息:{str_res} |
||||
|
任务:请按格式要求重新生成该节点上一次返回中生成的所有指令。 |
||||
|
''' |
||||
|
elif llm_type ==3: #对节点没有指令的,请求指令 |
||||
|
ext_Prompt = f''' |
||||
|
任务:{str_res}。 |
||||
|
''' |
||||
|
elif llm_type == 5: |
||||
|
ext_Prompt = f''' |
||||
|
反馈类型:测试指令格式错误 |
||||
|
错误信息:{str_res} |
||||
|
任务:请根据格式要求,重新生成该测试指令。 |
||||
|
''' |
||||
|
else: |
||||
|
self.logger.debug("意外的类型参数") |
||||
|
return "" |
||||
|
|
||||
|
user_Prompt = user_Prompt + ext_Prompt |
||||
|
return user_Prompt |
||||
|
|
||||
|
#添加子节点 |
||||
|
def add_children_node(self,parent_node,children_names,cur_node,status="未完成"): |
||||
|
for child_name in children_names: |
||||
|
bfind = False |
||||
|
for node_child in parent_node.children: |
||||
|
if node_child.name == child_name: |
||||
|
bfind = True #有重复的了 |
||||
|
break |
||||
|
if not bfind: |
||||
|
# 添加节点 |
||||
|
new_node = TreeNode(child_name, parent_node.task_id, status) |
||||
|
parent_node.add_child(new_node,cur_node.messages) # message的传递待验证 |
||||
|
|
||||
|
#处理节点指令 |
||||
|
def tree_manager(self,node_cmds,node,commands,DBM): |
||||
|
'''更新渗透测试树 |
||||
|
node_cmds是json-list |
||||
|
2025-03-22添加commands参数,用于处理LLM对同一个节点返回了测试指令,但还返回了no_instruction节点指令 |
||||
|
''' |
||||
|
if not node_cmds: # or len(node_cmds)==0: 正常not判断就可以有没有节点指令 |
||||
|
return True,commands,0 |
||||
|
|
||||
|
#对节点指令进行校验 |
||||
|
bok,strerror = g_CV.verify_node_cmds(node_cmds) |
||||
|
if not bok: #节点指令存在问题,则不进行后续处理,提交一个错误反馈任务 |
||||
|
# 提交llm待处理任务 |
||||
|
self.put_node_reslist(node, strerror, 2) |
||||
|
return False,commands,0 |
||||
|
|
||||
|
residue_node_cmds = [] |
||||
|
no_instr_nodes = [] |
||||
|
#如果有on_instruction,先补全指令保障信息链的完整 |
||||
|
for node_cmd in node_cmds: |
||||
|
action = node_cmd["action"] |
||||
|
if action == "no_instruction": |
||||
|
node_names = node_cmd["nodes"].split(',') |
||||
|
for node_name in node_names: |
||||
|
# 先判断是否在测试指令中,若在则不提交llm任务,只能接受在一次返回中同一节点有多条测试指令,不允许分次返回 |
||||
|
bcommand = False |
||||
|
for com in commands: |
||||
|
if node_name in com: |
||||
|
bcommand = True |
||||
|
break |
||||
|
if bcommand: # 如果存在测试指令,则不把该节点放入补充信息llm任务---尝试不对比是否有返回指令,DS会一直返回指令,还返回on_instruction |
||||
|
continue |
||||
|
no_instr_nodes.append(node_name) |
||||
|
else:#剩余的节点指令 |
||||
|
residue_node_cmds.append(node_cmd) |
||||
|
|
||||
|
if no_instr_nodes: # 阻塞式,在当前节点提交补充信息,完善节点指令 -- 优势是省token |
||||
|
new_commands = self.get_other_instruction(no_instr_nodes, DBM, node) |
||||
|
commands.extend(new_commands) |
||||
|
|
||||
|
# #对节点数据进行初步验证 |
||||
|
# ad_instr_nodes, no_add_nodes = g_CV.verify_node_data(node_cmds) |
||||
|
# if no_add_nodes:#如果有没有添加的节点,默认在当前节点下添加 -- 一般不会有,还没遇到 |
||||
|
# self.add_children_node(node,no_add_nodes,node) |
||||
|
# #ad_instr_nodes --- 还没处理 |
||||
|
|
||||
|
#先执行add_node操作---2025-4-12-调整:message取当前节点,节点允许为子节点添加子节点 |
||||
|
iadd_node = 0 |
||||
|
residue_cmd_sno_add_= [] |
||||
|
for node_json in residue_node_cmds: |
||||
|
action = node_json["action"] |
||||
|
if action == "add_node": # 新增节点 |
||||
|
parent_node_name = node_json["parent"] |
||||
|
status = "未完成" #2025-4-11修改MGS-节点指令格式,取消了status |
||||
|
node_names = node_json["nodes"].split(',') |
||||
|
# 新增节点原则上应该都是当前节点增加子节点 |
||||
|
if node.name == parent_node_name or parent_node_name.endswith(node.name): #2233ai,节点名称字段会返回整个路径 |
||||
|
#添加当前节点的子节点 -- 这是标准情况 |
||||
|
self.add_children_node(node, node_names,node) |
||||
|
iadd_node += len(node_names) # 添加节点的数量---当前只记录给当前节点添加的子节点的数量 |
||||
|
elif node.parent.name == parent_node_name or parent_node_name.endswith(node.parent.name):#添加当前节点的平级节点 |
||||
|
#是添加当前节点的平级节点(当前节点的父节点下添加子节点) --使用2233ai-o3时遇到的情况 |
||||
|
self.add_children_node(node.parent,node_names,node) |
||||
|
else: |
||||
|
badd = False |
||||
|
for child_node in node.children:#给子节点添加子节点 |
||||
|
if parent_node_name == child_node.name or parent_node_name.endswith(child_node.name): |
||||
|
badd = True |
||||
|
self.add_children_node(child_node,node_names,node) |
||||
|
break |
||||
|
if not badd: |
||||
|
self.logger.error(f"添加子节点时,遇到父节点名称没有找到的,需要介入!!{node_json}") # 丢弃该节点 |
||||
|
else:#未处理的节点指令添加到list |
||||
|
residue_cmd_sno_add_.append(node_json) |
||||
|
|
||||
|
if iadd_node and self.taskM.web_cur_task == self.task_id: #如果新增了节点,且该节点树是当前查看的数据,需要通知前端更新数据 |
||||
|
idatatype = 2 |
||||
|
strdata = "update accack_tree!" |
||||
|
asyncio.run(g_WSM.send_data(idatatype, strdata)) |
||||
|
#先取消当前task,已经通知前端重新获取,这样可以避免后端不必要的数据推送 |
||||
|
self.taskM.web_cur_task = 0 |
||||
|
|
||||
|
#执行剩余的节点指令--不分先后 |
||||
|
for node_json in residue_cmd_sno_add_:#2025-4-11重新调整了节点指令格式定义 |
||||
|
action = node_json["action"] |
||||
|
if action == "find_vul": |
||||
|
node_name = node_json["node"] |
||||
|
vul_node = None |
||||
|
if node.name == node_name or node_name.endswith(node.name): #正常应该是当前节点漏洞信息--暂时只考虑只会有一个漏洞 |
||||
|
vul_node = node |
||||
|
else: #匹配子节点 |
||||
|
for child in node.children: |
||||
|
if child.name == node_name or node_name.endswith(child.name): |
||||
|
vul_node = node |
||||
|
break |
||||
|
if vul_node: #找到对应了漏洞节点 |
||||
|
try: |
||||
|
vul_node.vul_type = node_json["vulnerability"]["name"] |
||||
|
vul_node.vul_grade = node_json["vulnerability"]["risk"] |
||||
|
vul_node.vul_info = node_json["vulnerability"]["info"] |
||||
|
#保存到数据库 |
||||
|
DBM.insert_taks_vul(self.task_id,vul_node.name,vul_node.path,vul_node.vul_type,vul_node.vul_grade, |
||||
|
vul_node.vul_info) |
||||
|
except: |
||||
|
self.logger.error("漏洞信息错误") |
||||
|
continue |
||||
|
else: |
||||
|
str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}" |
||||
|
self.logger.error(str_user) |
||||
|
elif action == "end_work": |
||||
|
node_name = node_json["node"] |
||||
|
if node.name == node_name or node_name.endswith(node_name): # 正常应该是当前节点 |
||||
|
node.status = "已完成" |
||||
|
else: |
||||
|
str_user = f"遇到不是修改本节点状态的,需要介入!!{node_json}" |
||||
|
self.logger.error(str_user) |
||||
|
elif action == "no_create": #提交人工确认 |
||||
|
nodes = node_json["nodes"] |
||||
|
if nodes: |
||||
|
str_add = {"未新增的节点": nodes} |
||||
|
self.logger.debug(str_add) |
||||
|
# 提交一个继续反馈任务--继续后续工作 2025-3-25不自动处理 |
||||
|
# self.put_one_llm_work(str_add, node, 4) |
||||
|
# self.logger.debug(f"未新增的节点有:{nodes}") |
||||
|
else: |
||||
|
self.logger.error("****不应该执行到这!程序逻辑存在问题!") |
||||
|
return True,commands,iadd_node |
||||
|
|
||||
|
#阻塞轮询补充指令 |
||||
|
def get_other_instruction(self,nodes,DBM,cur_node): |
||||
|
res_str = ','.join(nodes) |
||||
|
new_commands = [] |
||||
|
while res_str: |
||||
|
self.logger.debug(f"开始针对f{res_str}这些节点请求测试指令") |
||||
|
user_Prompt = f''' |
||||
|
当前分支路径:{cur_node.path} |
||||
|
当前节点信息: |
||||
|
- 节点名称:{cur_node.name} |
||||
|
- 节点状态:{cur_node.status} |
||||
|
- 漏洞类型:{cur_node.vul_type} |
||||
|
反馈类型:需要补充以下子节点的测试指令:{res_str} |
||||
|
任务: |
||||
|
1.请生成这些子节点的测试指令,注意不要生成重复的测试指令; |
||||
|
2.这些节点的父节点为当前节点,请正确生成这些节点的节点路径; |
||||
|
3.只有当还有节点未能生成测试指令或不完整时,才返回未生成指令的节点列表。 |
||||
|
''' |
||||
|
res_str = "" |
||||
|
#node_cmds, commands = self.LLM.get_llm_instruction(user_Prompt, DBM, cur_node) # message要更新 |
||||
|
node_cmds, commands, reasoning_content, content, post_time = self.LLM.get_llm_instruction(user_Prompt, |
||||
|
cur_node) # message要更新 |
||||
|
# LLM记录存数据库 |
||||
|
cur_node.llm_sn += 1 |
||||
|
bres = DBM.insert_llm(self.task_id, user_Prompt, reasoning_content, content, post_time, cur_node.llm_sn,cur_node.path) |
||||
|
if not bres: |
||||
|
self.logger.error(f"{cur_node.name}-llm入库失败!") |
||||
|
#把返回的测试指令进行追加 |
||||
|
new_commands.extend(commands) |
||||
|
#判断是否还有未添加指令的节点 |
||||
|
for node_json in node_cmds: #正常应该只有一条no_instruction --暂时只处理 |
||||
|
if "no_instruction" in node_json and "nodes" in node_json: |
||||
|
tmp_nodes = [] |
||||
|
node_names = node_json["nodes"].split(',') |
||||
|
for node_name in node_names: |
||||
|
if node_name in nodes: |
||||
|
tmp_nodes.append(node_name) |
||||
|
res_str = ','.join(tmp_nodes) |
||||
|
break |
||||
|
self.logger.debug("未添加指令的节点,都已完成指令的添加!") |
||||
|
return new_commands |
||||
|
|
||||
|
def start_task(self,task_id,task_status=1,attack_tree=None): |
||||
|
self.task_id = task_id |
||||
|
''' |
||||
|
启动该测试任务 |
||||
|
''' |
||||
|
#判断目标合法性 |
||||
|
# bok,target,type = self.TargetM.validate_and_extract(self.target) #是否还需要判断待定? |
||||
|
# if not bok: |
||||
|
# return False, "{target}检测目标不合规,请检查!" |
||||
|
#初始化节点树 |
||||
|
if attack_tree:#有值的情况是load |
||||
|
self.attack_tree =attack_tree |
||||
|
#加载未完成的任务 |
||||
|
if self.work_type ==1:#自动模式 |
||||
|
#提交到mq,待线程执行 |
||||
|
self.put_work_node() |
||||
|
else: #无值的情况是new_create |
||||
|
root_node = TreeNode(self.target, self.task_id) #根节点 |
||||
|
self.attack_tree = AttackTree(root_node) #创建测试树,同时更新根节点相关内容 |
||||
|
self.LLM.build_initial_prompt(root_node) #对根节点初始化system-msg |
||||
|
#插入一个user消息 |
||||
|
# 提交第一个llm任务,开始工作 |
||||
|
know_info = f"本测试主机的IP地址为:{self.local_ip}" |
||||
|
if self.cookie: |
||||
|
know_info = know_info + f",本站点的cookie值为{self.cookie}" |
||||
|
self.put_node_reslist(root_node,know_info,0) #入待提交list |
||||
|
#初始保存个attack_tree文件 |
||||
|
g_PKM.WriteData(self.attack_tree,str(self.task_id)) |
||||
|
#启动工作线程 |
||||
|
self.task_status = task_status |
||||
|
self.brun = True #线程正常启动 |
||||
|
#启动指令工作线程 |
||||
|
for i in range(self.max_thread_num): |
||||
|
w_th = threading.Thread(target=self.do_worker_th,args=(i,)) |
||||
|
w_th.start() |
||||
|
self.workth_list[i] = w_th |
||||
|
#启动llm提交线程--llm暂时单线程,多线程处理时attack_tree需要加锁 |
||||
|
for j in range(self.llm_max_nums): |
||||
|
l_th = threading.Thread(target=self.th_llm_worker,args=(j,)) |
||||
|
l_th.start() |
||||
|
self.llmth_list[j]=l_th |
||||
|
#启动自检线程 |
||||
|
self.check_th = threading.Thread(target=self.th_check) |
||||
|
self.check_th.start() |
||||
|
|
||||
|
def stop_task(self): #还未处理 |
||||
|
self.brun = False |
||||
|
self.InstrM.init_data() |
||||
|
#结束任务需要收尾处理#? |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
pass |
@ -0,0 +1,49 @@ |
|||||
|
import json |
||||
|
import struct |
||||
|
from quart import websocket |
||||
|
|
||||
|
class WebSocketManager: |
||||
|
def __init__(self): |
||||
|
self.ws_clients={} |
||||
|
|
||||
|
async def register(self, user_id, ws_proxy): |
||||
|
ws = ws_proxy._get_current_object() # 获取代理背后的真实对象 |
||||
|
self.ws_clients[user_id] = ws |
||||
|
|
||||
|
async def unregister(self, user_id=0): |
||||
|
if user_id in self.ws_clients: |
||||
|
del self.ws_clients[user_id] |
||||
|
print(f"{user_id}-socket退出") |
||||
|
|
||||
|
def create_binary_error_message(self,error_text, idatatype=0): |
||||
|
# 将错误信息包装成 JSON |
||||
|
body = json.dumps({"error": error_text}).encode('utf-8') |
||||
|
idata_len = len(body) |
||||
|
# 构造数据头:固定字符串 "TFTF",后面 8 字节分别为 idatatype 和 idata_len (使用大端字节序) |
||||
|
header = b"TFTF" + struct.pack("!II", idatatype, idata_len) |
||||
|
return header + body |
||||
|
|
||||
|
async def send_data(self,idatatype,data,user_id=0): |
||||
|
""" |
||||
|
发送数据格式: |
||||
|
- 固定 4 字节:"TFTF"(ASCII) |
||||
|
- 接下来 8 字节为数据头,包含两个无符号整数(每个 4 字节),分别为 idatatype 与 idata_len |
||||
|
- 最后 idata_len 字节为数据体(JSON字符串),数据体中包含 node_path 和 node_workstatus |
||||
|
""" |
||||
|
if user_id not in self.ws_clients: |
||||
|
return |
||||
|
ws = self.ws_clients[user_id] |
||||
|
|
||||
|
# 将 data 转换为 JSON 字符串并转换为字节 |
||||
|
body = json.dumps(data).encode('utf-8') |
||||
|
idata_len = len(body) |
||||
|
# 使用 struct.pack 构造数据头(大端字节序) |
||||
|
header = b"TFTF" + struct.pack("!II", idatatype, idata_len) |
||||
|
message = header + body |
||||
|
try: |
||||
|
await ws.send(message) |
||||
|
except Exception as e: |
||||
|
print(f"发送失败: {e}") |
||||
|
await self.unregister(user_id) # 异常时自动注销连接 |
||||
|
|
||||
|
g_WSM = WebSocketManager() |
@ -0,0 +1,53 @@ |
|||||
|
import pickle |
||||
|
import threading |
||||
|
import os |
||||
|
from myutils.ConfigManager import myCongif |
||||
|
|
||||
|
class PickleManager: |
||||
|
def __init__(self): |
||||
|
self.lock = threading.Lock() # 线程锁 |
||||
|
self.tree_file = myCongif.get_data("TreeFile") |
||||
|
|
||||
|
def getfile_path(self,filename=""): |
||||
|
filepath = self.tree_file |
||||
|
if filename: |
||||
|
filepath = "tree_data/"+filename |
||||
|
return filepath |
||||
|
|
||||
|
def WriteData(self,attack_tree,filename=""): |
||||
|
filepath = self.getfile_path(filename) |
||||
|
with self.lock: |
||||
|
with open(filepath, 'wb') as f: |
||||
|
pickle.dump(attack_tree, f) |
||||
|
|
||||
|
def ReadData(self,filename=""): |
||||
|
attack_tree = None |
||||
|
filepath = self.getfile_path(filename) |
||||
|
with self.lock: |
||||
|
with open(filepath, "rb") as f: |
||||
|
attack_tree = pickle.load(f) |
||||
|
return attack_tree |
||||
|
|
||||
|
def DelData(self,filename=""): |
||||
|
filepath = self.getfile_path(filename) |
||||
|
#删除文件 |
||||
|
try: |
||||
|
os.remove(filepath) |
||||
|
return True |
||||
|
except FileNotFoundError: |
||||
|
# 文件不存在 |
||||
|
return True |
||||
|
except PermissionError: |
||||
|
# 没有删除权限/文件被占用 |
||||
|
return False |
||||
|
except IsADirectoryError: |
||||
|
# 路径指向的是目录而非文件 |
||||
|
return False |
||||
|
except Exception as e: |
||||
|
# 其他未知错误(如路径非法、存储介质故障等) |
||||
|
print(f"删除文件时发生意外错误: {str(e)}") |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
|
||||
|
g_PKM = PickleManager() |
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class ArpingTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,13 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class DirSearchTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
if "-o " not in instruction or "--output=" not in instruction: |
||||
|
instruction += " -o ds_result.txt" |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class GitdumperTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 60*2 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class MedusaTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,76 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
import os |
||||
|
import shlex |
||||
|
import subprocess |
||||
|
import tempfile |
||||
|
|
||||
|
class MsfvenomTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def do_worker_script(self,str_instruction,timeout,ext_params): |
||||
|
# 创建临时文件保存输出 |
||||
|
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
||||
|
output_file = tmpfile.name |
||||
|
|
||||
|
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
||||
|
safe_instr = shlex.quote(str_instruction.strip()) |
||||
|
# 构建 script 命令 |
||||
|
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
||||
|
script_cmd = f"script -q -c {safe_instr} {output_file}" |
||||
|
# 选项 -q 表示静默(quiet),减少不必要的输出 |
||||
|
|
||||
|
# # 构建并执行 script 命令 |
||||
|
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
||||
|
try: |
||||
|
if timeout ==0: |
||||
|
result = subprocess.run(script_cmd, shell=True, text=True) |
||||
|
else: |
||||
|
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
||||
|
# 读取输出文件内容 |
||||
|
with open(output_file, 'r') as f: |
||||
|
output = f.read() |
||||
|
lines = output.splitlines() |
||||
|
# 跳过第一行(Script started)和最后一行(Script done) |
||||
|
ftp_output = lines[1:-1] |
||||
|
output = '\n'.join(ftp_output) |
||||
|
except subprocess.TimeoutExpired: |
||||
|
output = "命令超时返回" |
||||
|
try: |
||||
|
with open(output_file, 'r') as f: |
||||
|
partial_output = f.read() |
||||
|
if partial_output: |
||||
|
output += f"\n部分输出:\n{partial_output}" |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
except subprocess.CalledProcessError as e: |
||||
|
output = f"错误: {e}" |
||||
|
finally: |
||||
|
# 删除临时文件 |
||||
|
try: |
||||
|
os.remove(output_file) |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
return output |
||||
|
|
||||
|
def execute_instruction(self, instruction_old): |
||||
|
ext_params = self.create_extparams() |
||||
|
# 第一步:验证指令合法性 |
||||
|
instruction,time_out = self.validate_instruction(instruction_old) |
||||
|
if not instruction: |
||||
|
return False, instruction_old, "该指令暂不执行!","",ext_params |
||||
|
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
||||
|
|
||||
|
# 第二步:执行指令---需要对ftp指令进行区分判断 |
||||
|
output = self.do_worker_script(instruction, time_out, ext_params) |
||||
|
|
||||
|
# 第三步:分析执行结果 |
||||
|
analysis = self.analyze_result(output,instruction,"","") |
||||
|
|
||||
|
return True, instruction, analysis,output,ext_params |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,75 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
import re |
||||
|
import os |
||||
|
import shlex |
||||
|
import subprocess |
||||
|
import tempfile |
||||
|
|
||||
|
|
||||
|
class OtherTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 60*2 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def do_worker_script(self,str_instruction,timeout,ext_params): |
||||
|
# 创建临时文件保存输出 |
||||
|
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
||||
|
output_file = tmpfile.name |
||||
|
|
||||
|
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
||||
|
safe_instr = shlex.quote(str_instruction.strip()) |
||||
|
# 构建 script 命令 |
||||
|
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
||||
|
script_cmd = f"script -q -c {safe_instr} {output_file}" |
||||
|
# 选项 -q 表示静默(quiet),减少不必要的输出 |
||||
|
|
||||
|
# # 构建并执行 script 命令 |
||||
|
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
||||
|
try: |
||||
|
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
||||
|
# 读取输出文件内容 |
||||
|
with open(output_file, 'r') as f: |
||||
|
output = f.read() |
||||
|
lines = output.splitlines() |
||||
|
# 跳过第一行(Script started)和最后一行(Script done) |
||||
|
ftp_output = lines[1:-1] |
||||
|
output = '\n'.join(ftp_output) |
||||
|
except subprocess.TimeoutExpired: |
||||
|
output = "命令超时返回" |
||||
|
try: |
||||
|
with open(output_file, 'r') as f: |
||||
|
partial_output = f.read() |
||||
|
if partial_output: |
||||
|
output += f"\n部分输出:\n{partial_output}" |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
except subprocess.CalledProcessError as e: |
||||
|
output = f"错误: {e}" |
||||
|
finally: |
||||
|
# 删除临时文件 |
||||
|
try: |
||||
|
os.remove(output_file) |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
return output |
||||
|
|
||||
|
def execute_instruction(self, instruction_old): |
||||
|
ext_params = self.create_extparams() |
||||
|
# 第一步:验证指令合法性 |
||||
|
instruction,time_out = self.validate_instruction(instruction_old) |
||||
|
if not instruction: |
||||
|
return False, instruction_old, "该指令暂不执行!","",ext_params |
||||
|
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
||||
|
|
||||
|
# 第二步:执行指令---需要对ftp指令进行区分判断 |
||||
|
output = self.do_worker_script(instruction, time_out, ext_params) |
||||
|
|
||||
|
# 第三步:分析执行结果 |
||||
|
analysis = self.analyze_result(output,instruction,"","") |
||||
|
|
||||
|
return True, instruction, analysis,output,ext_params |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class PingTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -1,153 +0,0 @@ |
|||||
#python代码动态执行 |
|
||||
import ast |
|
||||
import subprocess |
|
||||
import json |
|
||||
import builtins |
|
||||
import re |
|
||||
import paramiko |
|
||||
import impacket |
|
||||
import psycopg2 |
|
||||
import socket |
|
||||
import struct |
|
||||
import sys |
|
||||
import requests |
|
||||
import ssl |
|
||||
import mysql.connector |
|
||||
from tools.ToolBase import ToolBase |
|
||||
from mycode.Result_merge import my_merge |
|
||||
|
|
||||
class PythoncodeTool(ToolBase): |
|
||||
|
|
||||
def is_safe_code(self,code): |
|
||||
# List of high-risk functions to block (can be adjusted based on requirements) |
|
||||
HIGH_RISK_FUNCTIONS = ['eval', 'exec', 'os.system', 'subprocess.call', 'subprocess.Popen'] |
|
||||
|
|
||||
"""Check if the code contains high-risk function calls.""" |
|
||||
try: |
|
||||
tree = ast.parse(code) |
|
||||
for node in ast.walk(tree): |
|
||||
if isinstance(node, ast.Call): |
|
||||
if isinstance(node.func, ast.Name) and node.func.id in HIGH_RISK_FUNCTIONS: |
|
||||
return False |
|
||||
elif isinstance(node.func, ast.Attribute) and node.func.attr in HIGH_RISK_FUNCTIONS: |
|
||||
return False |
|
||||
return True |
|
||||
except SyntaxError: |
|
||||
return False |
|
||||
|
|
||||
def validate_instruction(self, instruction): |
|
||||
#指令过滤 |
|
||||
timeout = 0 |
|
||||
instr = instruction.replace("python_code ","") |
|
||||
instr = instruction.replace("python-code ", "") |
|
||||
# Safety check |
|
||||
if not self.is_safe_code(instr): |
|
||||
return "", timeout |
|
||||
return instr,timeout |
|
||||
|
|
||||
def safe_import(self,name,*args,**kwargs): |
|
||||
ALLOWED_MODULES = ['subprocess', 'json','re'] |
|
||||
if name not in ALLOWED_MODULES: |
|
||||
raise ImportError(f"Import of '{name}' is not allowed") |
|
||||
return builtins.__import__(name, *args, **kwargs) |
|
||||
|
|
||||
def execute_instruction(self, instruction_old): |
|
||||
''' |
|
||||
执行指令:验证合法性 -> 执行 -> 分析结果 |
|
||||
:param instruction_old: |
|
||||
:return: |
|
||||
bool:true-正常返回给大模型,false-结果不返回给大模型 |
|
||||
str:执行的指令 |
|
||||
str:执行指令的结果 |
|
||||
''' |
|
||||
ext_params = self.create_extparams() |
|
||||
|
|
||||
# 定义允许的内置函数集合 --白名单 |
|
||||
allowed_builtins = { |
|
||||
'__name__': __name__, |
|
||||
'__import__': builtins.__import__, |
|
||||
"abs": abs, |
|
||||
"all": all, |
|
||||
"any": any, |
|
||||
"bool": bool, |
|
||||
"chr": chr, |
|
||||
"dict": dict, |
|
||||
"float": float, |
|
||||
"int": int, |
|
||||
"len": len, |
|
||||
"list": list, |
|
||||
"max": max, |
|
||||
"min": min, |
|
||||
"print": print, |
|
||||
"range": range, |
|
||||
"set": set, |
|
||||
"str": str, |
|
||||
"sum": sum, |
|
||||
"type": type, |
|
||||
'open':open, |
|
||||
'Exception':Exception, |
|
||||
# 根据需要可以添加其他安全的内置函数 |
|
||||
} |
|
||||
# 第一步:验证指令合法性 |
|
||||
instruction,time_out = self.validate_instruction(instruction_old) |
|
||||
if not instruction: |
|
||||
return False, instruction_old, "该指令暂不执行!","",ext_params |
|
||||
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
|
||||
|
|
||||
# 第二步:执行指令 |
|
||||
output = "" |
|
||||
try: |
|
||||
# 构造安全的全局命名空间,只包含我们允许的 __builtins__ |
|
||||
# 虽然动态代码中包含了import subprocess,但是还是需要在全局命名空间中添加subprocess这些库 |
|
||||
# 正常情况应该是不需要的,后续再研究 |
|
||||
safe_globals = {"__builtins__": allowed_builtins, |
|
||||
'subprocess':subprocess, |
|
||||
'json':json, |
|
||||
're':re, |
|
||||
'paramiko':paramiko, |
|
||||
'impacket':impacket, |
|
||||
'psycopg2':psycopg2, |
|
||||
'socket':socket, |
|
||||
'mysql':mysql, |
|
||||
'mysql.connector':mysql.connector, |
|
||||
'struct':struct, |
|
||||
'sys':sys, |
|
||||
'requests':requests, |
|
||||
'ssl':ssl} |
|
||||
safe_locals = {} #不需要预设局部参数 |
|
||||
# 在限制环境中执行代码 |
|
||||
exec(instruction, safe_globals,safe_locals) |
|
||||
# Check if check_samba_vuln is defined |
|
||||
if 'dynamic_fun' not in safe_locals: |
|
||||
analysis = "Function dynamic_fun() is not defined" |
|
||||
ext_params['is_use'] = True |
|
||||
return True,instruction,analysis,analysis,ext_params |
|
||||
# Get the function and call it |
|
||||
dynamic_fun = safe_locals['dynamic_fun'] |
|
||||
status, tmpout = dynamic_fun() #LLM存在status定义错误的情况(执行成功,却返回的是False) #重点要处理 |
|
||||
output = f"status:{status},output:{tmpout}" |
|
||||
except Exception as e: |
|
||||
analysis = f"执行动态代码时出错: {str(e)}" |
|
||||
ext_params['is_use'] = True |
|
||||
return True,instruction,analysis,analysis,ext_params |
|
||||
|
|
||||
|
|
||||
# 第三步:分析执行结果 |
|
||||
analysis = self.analyze_result(output, instruction,"","") |
|
||||
# 指令和结果入数据库 |
|
||||
# ? |
|
||||
if not analysis: # analysis为“” 不提交LLM |
|
||||
return False, instruction, analysis,"",ext_params |
|
||||
return True, instruction, analysis,"",ext_params |
|
||||
|
|
||||
def analyze_result(self, result,instruction,stderr,stdout): |
|
||||
#指令结果分析 --- 要不要限定一个max_len? |
|
||||
if "enum4linux " in instruction: #存在指令包装成Python代码返回的情况 |
|
||||
result = my_merge("enum4linux",result) |
|
||||
return result |
|
||||
|
|
||||
if __name__ == "__main__": |
|
||||
llm_code = """ |
|
||||
def run_test(): |
|
||||
return 'Penetration test executed successfully!' |
|
||||
""" |
|
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class RpcclientTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class RpcinfoTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,13 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class SmbmapTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
if " grep " not in instruction: |
||||
|
instruction =instruction.strip() + " | grep -E 'READ|WRITE|Disk|path'" |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,77 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
import os |
||||
|
import shlex |
||||
|
import subprocess |
||||
|
import tempfile |
||||
|
|
||||
|
|
||||
|
class SshpassTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def do_worker_script(self,str_instruction,timeout,ext_params): |
||||
|
# 创建临时文件保存输出 |
||||
|
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
||||
|
output_file = tmpfile.name |
||||
|
|
||||
|
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
||||
|
safe_instr = shlex.quote(str_instruction.strip()) |
||||
|
# 构建 script 命令 |
||||
|
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
||||
|
script_cmd = f"script -q -c {safe_instr} {output_file}" |
||||
|
# 选项 -q 表示静默(quiet),减少不必要的输出 |
||||
|
|
||||
|
# # 构建并执行 script 命令 |
||||
|
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
||||
|
try: |
||||
|
if timeout ==0: |
||||
|
result = subprocess.run(script_cmd, shell=True, text=True) |
||||
|
else: |
||||
|
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
||||
|
# 读取输出文件内容 |
||||
|
with open(output_file, 'r') as f: |
||||
|
output = f.read() |
||||
|
lines = output.splitlines() |
||||
|
# 跳过第一行(Script started)和最后一行(Script done) |
||||
|
ftp_output = lines[1:-1] |
||||
|
output = '\n'.join(ftp_output) |
||||
|
except subprocess.TimeoutExpired: |
||||
|
output = "命令超时返回" |
||||
|
try: |
||||
|
with open(output_file, 'r') as f: |
||||
|
partial_output = f.read() |
||||
|
if partial_output: |
||||
|
output += f"\n部分输出:\n{partial_output}" |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
except subprocess.CalledProcessError as e: |
||||
|
output = f"错误: {e}" |
||||
|
finally: |
||||
|
# 删除临时文件 |
||||
|
try: |
||||
|
os.remove(output_file) |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
return output |
||||
|
|
||||
|
def execute_instruction(self, instruction_old): |
||||
|
ext_params = self.create_extparams() |
||||
|
# 第一步:验证指令合法性 |
||||
|
instruction,time_out = self.validate_instruction(instruction_old) |
||||
|
if not instruction: |
||||
|
return False, instruction_old, "该指令暂不执行!","",ext_params |
||||
|
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
||||
|
|
||||
|
# 第二步:执行指令---需要对ftp指令进行区分判断 |
||||
|
output = self.do_worker_script(instruction, time_out, ext_params) |
||||
|
|
||||
|
# 第三步:分析执行结果 |
||||
|
analysis = self.analyze_result(output,instruction,"","") |
||||
|
|
||||
|
return True, instruction, analysis,output,ext_params |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,73 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
import os |
||||
|
import shlex |
||||
|
import subprocess |
||||
|
import tempfile |
||||
|
|
||||
|
class TcpdumpTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 60*2 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def do_worker_script(self,str_instruction,timeout,ext_params): |
||||
|
# 创建临时文件保存输出 |
||||
|
with tempfile.NamedTemporaryFile(delete=False) as tmpfile: |
||||
|
output_file = tmpfile.name |
||||
|
|
||||
|
# 使用 shlex.quote 对 str_instruction 进行安全包装,确保整个命令作为一个参数传递 |
||||
|
safe_instr = shlex.quote(str_instruction.strip()) |
||||
|
# 构建 script 命令 |
||||
|
# 注意:此时 safe_instr 包含单引号,确保整个 -c 参数不被拆分 |
||||
|
script_cmd = f"script -q -c {safe_instr} {output_file}" |
||||
|
# 选项 -q 表示静默(quiet),减少不必要的输出 |
||||
|
|
||||
|
# # 构建并执行 script 命令 |
||||
|
# script_cmd = f"script -c '{str_instruction}' {output_file}" |
||||
|
try: |
||||
|
result = subprocess.run(script_cmd, shell=True, text=True,timeout=timeout) |
||||
|
# 读取输出文件内容 |
||||
|
with open(output_file, 'r') as f: |
||||
|
output = f.read() |
||||
|
lines = output.splitlines() |
||||
|
# 跳过第一行(Script started)和最后一行(Script done) |
||||
|
ftp_output = lines[1:-1] |
||||
|
output = '\n'.join(ftp_output) |
||||
|
except subprocess.TimeoutExpired: |
||||
|
output = "命令超时返回" |
||||
|
try: |
||||
|
with open(output_file, 'r') as f: |
||||
|
partial_output = f.read() |
||||
|
if partial_output: |
||||
|
output += f"\n部分输出:\n{partial_output}" |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
except subprocess.CalledProcessError as e: |
||||
|
output = f"错误: {e}" |
||||
|
finally: |
||||
|
# 删除临时文件 |
||||
|
try: |
||||
|
os.remove(output_file) |
||||
|
except FileNotFoundError: |
||||
|
pass # 文件可能未创建 |
||||
|
return output |
||||
|
|
||||
|
def execute_instruction1(self, instruction_old): |
||||
|
ext_params = self.create_extparams() |
||||
|
# 第一步:验证指令合法性 |
||||
|
instruction,time_out = self.validate_instruction(instruction_old) |
||||
|
if not instruction: |
||||
|
return False, instruction_old, "该指令暂不执行!","",ext_params |
||||
|
# 过滤修改后的指令是否需要判重?同样指令再执行结果一致?待定---#? |
||||
|
|
||||
|
# 第二步:执行指令---需要对ftp指令进行区分判断 |
||||
|
output = self.do_worker_script(instruction, time_out, ext_params) |
||||
|
|
||||
|
# 第三步:分析执行结果 |
||||
|
analysis = self.analyze_result(output,instruction,"","") |
||||
|
|
||||
|
return True, instruction, analysis,output,ext_params |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -0,0 +1,11 @@ |
|||||
|
from tools.ToolBase import ToolBase |
||||
|
|
||||
|
class XvfbrunTool(ToolBase): |
||||
|
def validate_instruction(self, instruction): |
||||
|
#指令过滤 |
||||
|
timeout = 0 |
||||
|
return instruction,timeout |
||||
|
|
||||
|
def analyze_result(self, result,instruction,stderr,stdout): |
||||
|
#指令结果分析 |
||||
|
return result |
@ -1,4 +1,4 @@ |
|||||
from quart import Blueprint |
from quart import Blueprint |
||||
#定义模块 |
#定义模块 |
||||
api = Blueprint('api',__name__) |
api = Blueprint('api',__name__) |
||||
from . import user |
from . import user,task,wsm,system |
||||
|
@ -0,0 +1,23 @@ |
|||||
|
from . import api |
||||
|
from mycode.DBManager import app_DBM |
||||
|
from myutils.ReManager import mReM |
||||
|
from quart import Quart, render_template, redirect, url_for, request,jsonify |
||||
|
|
||||
|
@api.route('/system/getinfo',methods=['GET']) |
||||
|
async def get_system_info(): |
||||
|
data = app_DBM.getsystem_info() |
||||
|
return jsonify({"local_ip":data[0],"version":data[1]}) |
||||
|
|
||||
|
@api.route('/system/updateip',methods=['POST']) |
||||
|
async def update_local_ip(): |
||||
|
data = await request.get_json() |
||||
|
local_ip = data.get("local_ip") |
||||
|
if mReM.is_valid_ip(local_ip): |
||||
|
bsuccess = app_DBM.update_localip(local_ip) |
||||
|
error = "" |
||||
|
if not bsuccess: |
||||
|
error = "修改IP地址失败!" |
||||
|
else: |
||||
|
bsuccess = False |
||||
|
error = "IP不合法" |
||||
|
return jsonify({"bsuccess":bsuccess,"error":error}) |
@ -0,0 +1,245 @@ |
|||||
|
from . import api |
||||
|
from quart import Quart, render_template, redirect, url_for, request,jsonify |
||||
|
from mycode.TargetManager import g_TM |
||||
|
from mycode.DBManager import app_DBM |
||||
|
from mycode.TaskManager import g_TaskM |
||||
|
|
||||
|
|
||||
|
def is_valid_target(test_target: str) -> bool: |
||||
|
""" |
||||
|
验证 test_target 的逻辑(这里用简单示例代替已有逻辑) |
||||
|
例如:测试目标不能为空且长度大于3 |
||||
|
""" |
||||
|
if test_target: |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
@api.route('/task/start',methods=['POST']) |
||||
|
async def start_task(): #开始任务 |
||||
|
data = await request.get_json() |
||||
|
test_target = data.get("testTarget") |
||||
|
cookie_info = data.get("cookieInfo") |
||||
|
llm_type = data.get("curmodel") # //0-腾讯云,1-DS,2-2233.ai,3-GPT 目前只有1-2,2025-4-4 |
||||
|
work_type = data.get("workType") #0-人工,1-自动 |
||||
|
#新增任务处理 |
||||
|
bok,_,_ = g_TM.validate_and_extract(test_target) |
||||
|
if not bok: |
||||
|
# 返回错误信息,状态码 400 表示请求错误 |
||||
|
return jsonify({"error": "测试目标验证失败,请检查输入内容!"}), 400 |
||||
|
#开始任务 |
||||
|
try: |
||||
|
b_success = g_TaskM.create_task(test_target,cookie_info,llm_type,work_type) |
||||
|
#再启动 |
||||
|
if not b_success: |
||||
|
return jsonify({"error": "检测任务创建失败,请联系管理员!"}), 500 |
||||
|
except: |
||||
|
return jsonify({"error": "该目标已经在测试中,请检查!"}), 400 |
||||
|
#跳转到任务管理页面 |
||||
|
return redirect(url_for('main.get_html', html='task_manager.html')) |
||||
|
|
||||
|
@api.route('/task/taskover',methods=['POST']) |
||||
|
async def over_task(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("cur_task_id") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
bsuccess,error = g_TaskM.over_task(task_id) |
||||
|
return jsonify({"bsuccess": bsuccess, "error": error}) |
||||
|
|
||||
|
@api.route('/task/deltask',methods=['POST']) |
||||
|
async def del_task(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
bsuccess,error = g_TaskM.del_task(task_id) |
||||
|
return jsonify({"bsuccess": bsuccess, "error": error}) |
||||
|
|
||||
|
|
||||
|
@api.route('/task/getlist',methods=['GET']) |
||||
|
async def get_task_list(): |
||||
|
#task_list = app_DBM.get_task_list() #从内存取--2025-4-6 |
||||
|
task_list = g_TaskM.get_task_list() |
||||
|
if task_list: |
||||
|
return jsonify(task_list) |
||||
|
else: |
||||
|
return jsonify({"error":"查询任务数据出错!"}),500 |
||||
|
|
||||
|
@api.route('/task/getinstr',methods=['POST']) |
||||
|
async def get_instr(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("cur_task_id") |
||||
|
node_name = data.get("nodeName") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
instrs = app_DBM.get_task_instrs(task_id,node_name) |
||||
|
return jsonify({"instrs":instrs}) |
||||
|
|
||||
|
@api.route('/task/getvul',methods=['POST']) |
||||
|
async def get_vul(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("cur_task_id") |
||||
|
node_name = data.get("nodeName") |
||||
|
vul_type = data.get("vulType") |
||||
|
vul_level = data.get("vulLevel") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
vuls = app_DBM.get_task_vul(task_id,node_name,vul_type,vul_level) |
||||
|
return jsonify({"vuls":vuls}) |
||||
|
|
||||
|
@api.route('/task/gettree',methods=['POST']) |
||||
|
async def get_tree(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
tree_dict = g_TaskM.get_node_tree(task_id) |
||||
|
return jsonify({"tree":tree_dict}) |
||||
|
|
||||
|
@api.route('/task/gethistree',methods=['POST']) |
||||
|
async def get_his_tree(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
tree_dict = g_TaskM.get_his_node_tree(task_id) |
||||
|
return jsonify({"tree":tree_dict}) |
||||
|
|
||||
|
@api.route('/task/taskcontrol',methods=['POST']) |
||||
|
async def task_status_control(): |
||||
|
'''控制任务状态 |
||||
|
1.对于执行时间长的指令,如何处理?强制停止的话,要有个执行中指令的缓存,强制停止该指令返回到待执行,执行完成,该指令到执行完成; |
||||
|
''' |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("cur_task_id") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
#只做暂停和继续间的切换,以服务器端的状态为准 |
||||
|
bsuccess,strerror,new_task_status = g_TaskM.control_taks(task_id) |
||||
|
if bsuccess: |
||||
|
return jsonify({'newstatus':new_task_status}) |
||||
|
return jsonify({'error': strerror}), 400 |
||||
|
|
||||
|
@api.route('/task/taskstep',methods=['POST']) |
||||
|
async def task_one_step(): |
||||
|
'''单步推进任务--也就是待处理node 返回bsuccess,error |
||||
|
1.执行单步的前提条件是,工作线程都要在工作; |
||||
|
2.遍历节点把需要处理的节点进入待处理queue,instr和llm只能一个有数据(强制约束) |
||||
|
''' |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("cur_task_id") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
bsuccess,error = await g_TaskM.task_one_step(task_id) |
||||
|
return jsonify({"bsuccess":bsuccess,"error":error}) |
||||
|
|
||||
|
@api.route('/task/nodestep',methods=['POST']) |
||||
|
async def node_one_step(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
node_path = data.get("node_path") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
bsuccess,error = await g_TaskM.node_one_step(task_id,node_path) |
||||
|
return jsonify({"bsuccess":bsuccess,"error":error}) |
||||
|
|
||||
|
@api.route('/task/taskworktype',methods=['POST']) |
||||
|
async def task_work_type_control(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("cur_task_id") |
||||
|
newwork_type = data.get("mode") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id or newwork_type'}), 400 |
||||
|
bsuccess = g_TaskM.update_task_work_type(task_id,newwork_type) |
||||
|
return jsonify({"bsuccess": bsuccess}) |
||||
|
|
||||
|
@api.route('/task/nodecontrol',methods=['POST']) |
||||
|
async def node_work_status_control(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
nodepath = data.get("node_path") |
||||
|
if not task_id or not nodepath: |
||||
|
return jsonify({'error': 'Missing task_id or node_path'}), 400 |
||||
|
#修改节点的工作状态 |
||||
|
bsuccess,newbwork = g_TaskM.node_bwork_control(task_id,nodepath) |
||||
|
if not bsuccess: |
||||
|
return jsonify({'error': 'node_path not bfind'}), 400 |
||||
|
return jsonify({"newbwork":newbwork}) |
||||
|
|
||||
|
@api.route('/task/nodegetinstr',methods=['POST']) |
||||
|
async def node_get_instr(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
nodepath = data.get("node_path") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
#返回 { doneInstrs: [...], todoInstrs: [...] } |
||||
|
doneInstrs = app_DBM.get_task_node_done_instr(task_id,nodepath) |
||||
|
todoInstrs = g_TaskM.get_task_node_todo_instr(task_id,nodepath) |
||||
|
return jsonify({"doneInstrs":doneInstrs,"todoInstrs":todoInstrs}) |
||||
|
|
||||
|
@api.route('/task/hisnodegetinstr',methods=['POST']) |
||||
|
async def his_node_get_instr(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
nodepath = data.get("node_path") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
#返回 { doneInstrs: [...], todoInstrs: [...] } |
||||
|
doneInstrs = app_DBM.get_task_node_done_instr(task_id,nodepath) |
||||
|
#todoInstrs = g_TaskM.get_task_node_todo_instr(task_id,nodepath) |
||||
|
return jsonify({"doneInstrs":doneInstrs}) |
||||
|
|
||||
|
@api.route('/task/nodegetmsg',methods=['POST']) |
||||
|
async def node_get_msg(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
nodepath = data.get("node_path") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
submitted,pending = g_TaskM.get_task_node_MSG(task_id,nodepath) |
||||
|
return jsonify({"submitted": submitted, "pending": pending}) |
||||
|
|
||||
|
@api.route('/task/nodeupdatemsg',methods=['POST']) |
||||
|
async def node_update_msg(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
nodepath = data.get("node_path") |
||||
|
newllm_type = data.get("llmtype") |
||||
|
newcontent = data.get("content") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
bsuccess,error =g_TaskM.update_node_MSG(task_id,nodepath,newllm_type,newcontent) |
||||
|
return jsonify({"bsuccess":bsuccess,"error":error}) |
||||
|
|
||||
|
@api.route('/task/delnodeinstr',methods=['POST']) |
||||
|
async def node_del_instr(): |
||||
|
data = await request.get_json() |
||||
|
task_id = data.get("task_id") |
||||
|
nodepath = data.get("node_path") |
||||
|
instr = data.get("item") |
||||
|
if not task_id: |
||||
|
return jsonify({'error': 'Missing task_id'}), 400 |
||||
|
bsuccess,error = g_TaskM.del_node_instr(task_id,nodepath,instr) |
||||
|
return jsonify({"bsuccess": bsuccess, "error": error}) |
||||
|
|
||||
|
|
||||
|
@api.route('/task/histasks',methods=['POST']) |
||||
|
async def get_his_task(): |
||||
|
data = await request.get_json() |
||||
|
target_name = data.get("target_name") |
||||
|
safe_rank = data.get("safe_rank") |
||||
|
llm_type = data.get("llm_type") |
||||
|
start_time= data.get("start_time") |
||||
|
end_time= data.get("end_time") |
||||
|
his_tasks = g_TaskM.get_his_tasks(target_name,safe_rank,llm_type,start_time,end_time) |
||||
|
return jsonify({"his_tasks":his_tasks}) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,43 @@ |
|||||
|
import json |
||||
|
from . import api |
||||
|
from quart import Quart, websocket, jsonify |
||||
|
from mycode.WebSocketManager import g_WSM |
||||
|
|
||||
|
# WebSocket 路由,端口默认与 HTTP 同端口,例如 5000(开发时) |
||||
|
@api.websocket("/ws") |
||||
|
async def ws(): |
||||
|
""" |
||||
|
WebSocket 连接入口: |
||||
|
1. 客户端连接成功后,首先应发送登录数据包,例如 {"user_id": 1} |
||||
|
2. 后端解析登录数据包,将 user_id 与 websocket 绑定(注册) |
||||
|
3. 后续进入消息接收循环,根据数据协议(TFTF+头+体格式)处理数据 |
||||
|
""" |
||||
|
# 接收登录数据包(假设为纯 JSON 包,非二进制格式) |
||||
|
login_msg = await websocket.receive() |
||||
|
try: |
||||
|
login_data = json.loads(login_msg) |
||||
|
user_id = login_data.get("user_id") |
||||
|
if user_id is None: |
||||
|
error_msg = g_WSM.create_binary_error_message("没有提供user_id") |
||||
|
await websocket.send(error_msg) |
||||
|
return |
||||
|
# 注册当前连接 |
||||
|
await g_WSM.register(user_id, websocket) |
||||
|
except Exception as e: |
||||
|
errof_msg = g_WSM.create_binary_error_message(str(e)) |
||||
|
await websocket.send(errof_msg) |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
# 主循环:接收后续消息并处理;此处可以根据协议解析消息 ---暂时无web发送数据的业务 |
||||
|
while True: |
||||
|
message = await websocket.receive() |
||||
|
# 这里可以添加对接收到的消息解析处理,根据协议分离 TFTF 等 |
||||
|
print("收到消息:", message) |
||||
|
# 例如,直接回显消息;实际项目中应解析消息格式并调用相应的业务逻辑 |
||||
|
# await websocket.send(message) |
||||
|
except Exception as e: |
||||
|
print("WebSocket 出现异常:", e) |
||||
|
finally: |
||||
|
# 注销当前连接 |
||||
|
await g_WSM.unregister(user_id) |
@ -0,0 +1,134 @@ |
|||||
|
/* 左侧树容器:固定高度,允许滚动,根节点居中 */ |
||||
|
.node-tree-area { |
||||
|
width: 100%; |
||||
|
height: 100%; /* 示例高度,根据需求调整 */ |
||||
|
border: 1px solid #ddd; |
||||
|
overflow: hidden; /* 超出时出现滚动条 */ |
||||
|
background-color: #f8f9fa; |
||||
|
text-align: center; /* 内部 inline-block 居中 */ |
||||
|
position: relative; |
||||
|
} |
||||
|
|
||||
|
/* 树节点内容区域,不包含刷新按钮 */ |
||||
|
.tree-content { |
||||
|
height: 100%; |
||||
|
overflow: auto; |
||||
|
padding-top: 5px; /* 留出顶部刷新按钮位置 */ |
||||
|
} |
||||
|
|
||||
|
/* 顶部刷新按钮 */ |
||||
|
.refresh-container { |
||||
|
position: absolute; |
||||
|
top: 5px; |
||||
|
left: 5px; |
||||
|
z-index: 100; |
||||
|
} |
||||
|
|
||||
|
.tree-refresh { |
||||
|
position: absolute; |
||||
|
top: 5px; |
||||
|
left: 5px; |
||||
|
z-index: 100; |
||||
|
cursor: pointer; |
||||
|
border: none; |
||||
|
background-color: #007bff; /* 蓝色背景 */ |
||||
|
color: #fff; /* 白色文字 */ |
||||
|
font-size: 20px; |
||||
|
padding: 5px 10px; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
.tree-refresh:hover { |
||||
|
background-color: #0056b3; /* 悬停时深蓝 */ |
||||
|
} |
||||
|
|
||||
|
/* 让树的 ul 使用 inline-block 显示,便于根节点居中 */ |
||||
|
.tree-root-ul { |
||||
|
display: inline-block; |
||||
|
text-align: left; /* 子节点左对齐 */ |
||||
|
margin: 20px; |
||||
|
padding:0; |
||||
|
} |
||||
|
.tree-root-ul ul { |
||||
|
margin-left: 20px; /* 子节点缩进 */ |
||||
|
padding-left: 20px; |
||||
|
border-left: 1px dashed #ccc; /* 增加分隔线效果 */ |
||||
|
} |
||||
|
/* 节点容器:增加立体效果 */ |
||||
|
.node-container { |
||||
|
display: inline-flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
/* 切换图标 */ |
||||
|
.toggle-icon { |
||||
|
margin-right: 5px; |
||||
|
font-weight: bold; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
/* 每个节点样式 */ |
||||
|
.tree-node { |
||||
|
display: inline-block; |
||||
|
padding: 6px 10px; |
||||
|
margin: 3px 0; |
||||
|
border: 1px solid transparent; |
||||
|
border-radius: 4px; |
||||
|
background-color: #fff; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.2s ease; |
||||
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
.tree-node:hover { |
||||
|
background-color: #e6f7ff; |
||||
|
transform: translateY(-1px); |
||||
|
} |
||||
|
.tree-node.selected { |
||||
|
border-color: #1890ff; |
||||
|
background-color: #bae7ff; |
||||
|
} |
||||
|
/* 针对漏洞级别的样式 */ |
||||
|
.tree-node.vul-low { |
||||
|
border-color: #87d068; |
||||
|
background-color: #f6ffed; |
||||
|
} |
||||
|
.tree-node.vul-medium { |
||||
|
border-color: #faad14; |
||||
|
background-color: #fff7e6; |
||||
|
} |
||||
|
.tree-node.vul-high { |
||||
|
border-color: #ff4d4f; |
||||
|
background-color: #fff1f0; |
||||
|
} |
||||
|
.tree-node.no-work { |
||||
|
border-color: #151515; |
||||
|
background-color: #c4c4c4; |
||||
|
} |
||||
|
/* 收缩/展开时隐藏子节点 ul */ |
||||
|
.collapsed { |
||||
|
display: none; |
||||
|
} |
||||
|
/* 右侧信息区域 */ |
||||
|
.node-info-area { |
||||
|
border: 1px solid #ddd; |
||||
|
padding: 10px; |
||||
|
min-height: 200px; |
||||
|
} |
||||
|
/* 按钮区域 */ |
||||
|
.node-actions button { |
||||
|
margin-right: 10px; |
||||
|
margin-bottom: 10px; |
||||
|
} |
||||
|
|
||||
|
/* 限制模态框对话框的最大高度(比如距离屏幕上下各留 20px) */ |
||||
|
.modal-dialog { |
||||
|
max-height: calc(100vh - 60px); |
||||
|
/* 如果模态框尺寸需要自适应宽度,可以考虑设置 width: auto; */ |
||||
|
} |
||||
|
|
||||
|
/* 限制模态框内容区域的最大高度并启用垂直滚动 */ |
||||
|
.modal-content { |
||||
|
max-height: calc(100vh - 60px); |
||||
|
} |
||||
|
|
||||
|
/* 模态框主体区域启用滚动 */ |
||||
|
.modal-body { |
||||
|
overflow-y: auto; |
||||
|
} |
@ -1,563 +0,0 @@ |
|||||
let video_list = {}; //element_id -- socket
|
|
||||
let run_list = {}; //element_id -- runtag
|
|
||||
let berror_state_list = {}; //element_id -- 错误信息显示
|
|
||||
let m_count = 0; |
|
||||
let connection_version = {}; // 保存每个 element_id 的版本号
|
|
||||
let channel_list = null; |
|
||||
const fourViewButton = document.getElementById('fourView'); |
|
||||
const nineViewButton = document.getElementById('nineView'); |
|
||||
|
|
||||
//页面即将卸载时执行
|
|
||||
window.addEventListener('beforeunload', function (event) { |
|
||||
// 关闭所有 WebSocket 连接或执行其他清理操作
|
|
||||
for(let key in video_list){ |
|
||||
const videoFrame = document.getElementById(`video-${key}`); |
|
||||
const event = new Event('closeVideo'); |
|
||||
videoFrame.dispatchEvent(event); |
|
||||
|
|
||||
delete video_list[key]; |
|
||||
berror_state_list[key] = false; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
//页面加载时执行
|
|
||||
document.addEventListener('DOMContentLoaded', async function() { |
|
||||
console.log('DOM fully loaded and parsed'); |
|
||||
// 发送请求获取额外数据 --- 这个接口功能有点大了---暂时只是更新通道树2024-7-29
|
|
||||
try { |
|
||||
let response = await fetch('/api/channel/list'); |
|
||||
if (!response.ok) { |
|
||||
throw new Error('Network response was not ok'); |
|
||||
} |
|
||||
channel_list = await response.json(); |
|
||||
// 遍历输出每个元素的信息
|
|
||||
let area_name = "" |
|
||||
let html = '<ul class="list-group">'; |
|
||||
channel_list.forEach(channel => { |
|
||||
// console.log(`Area Name: ${channel.area_name}`);
|
|
||||
// console.log(`ID: ${channel.ID}`);
|
|
||||
// console.log(`Channel Name: ${channel.channel_name}`);
|
|
||||
// console.log(`URL: ${channel.url}`);
|
|
||||
// console.log(`Type: ${channel.type}`);
|
|
||||
// console.log(`Status: ${channel.status}`);
|
|
||||
// console.log(`Element ID: ${channel.element_id}`);
|
|
||||
if(area_name !== `${channel.area_name}`){ |
|
||||
if(area_name !== ""){ |
|
||||
html += '</ul>'; |
|
||||
html += '</li>'; |
|
||||
} |
|
||||
area_name = `${channel.area_name}`; |
|
||||
html += `<li class="list-group-item"><strong>${area_name}</strong>`; |
|
||||
html += '<ul class="list-group">'; |
|
||||
} |
|
||||
//html += `<li class="list-group-item">${channel.channel_name}</li>`;
|
|
||||
html += `<li class="list-group-item" draggable="true" ondragstart="drag(event)"
|
|
||||
data-node-id="${channel.ID}" data-node-name="${area_name}--${channel.channel_name}"> |
|
||||
<svg class="bi" width="16" height="16"><use xlink:href="#view"/></svg> |
|
||||
${channel.channel_name} |
|
||||
</li>`; |
|
||||
}); |
|
||||
if(area_name !== ""){ |
|
||||
html += '</ul>'; |
|
||||
html += '</li>'; |
|
||||
} |
|
||||
html += '</ul>'; |
|
||||
const treeView = document.getElementById('treeView'); |
|
||||
treeView.innerHTML = html |
|
||||
generateVideoNodes(4); |
|
||||
} catch (error) { |
|
||||
console.error('Failed to fetch data:', error); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
document.addEventListener('click', function() { |
|
||||
console.log("第一次页面点击,开始显示视频--已注释",m_count); |
|
||||
// if(m_count != 0){
|
|
||||
// count = m_count
|
|
||||
// //获取视频接口
|
|
||||
// const url = `/api/viewlist?count=${count}`;
|
|
||||
// fetch(url)
|
|
||||
// .then(response => response.json())
|
|
||||
// .then(data => {
|
|
||||
// console.log('Success:', data);
|
|
||||
// clist = data.clist;
|
|
||||
// elist = data.elist;
|
|
||||
// nlist = data.nlist;
|
|
||||
// for(let i=0;i<clist.length;i++){
|
|
||||
// if(parseInt(elist[i]) < count){
|
|
||||
// console.log("切换窗口时进行连接",clist[i])
|
|
||||
// connectToStream(elist[i],clist[i],nlist[i])
|
|
||||
// //startFLVStream(elist[i],clist[i],nlist[i]);
|
|
||||
// }
|
|
||||
// }
|
|
||||
// })
|
|
||||
// .catch(error => {
|
|
||||
// console.error('Error:', error);
|
|
||||
// });
|
|
||||
// }
|
|
||||
}, { once: true }); |
|
||||
|
|
||||
//视频窗口
|
|
||||
document.getElementById('fourView').addEventListener('click', function() { |
|
||||
if (fourViewButton.classList.contains('btn-primary')) { |
|
||||
return; // 如果按钮已经是选中状态,直接返回
|
|
||||
} |
|
||||
const videoGrid = document.getElementById('videoGrid'); |
|
||||
videoGrid.classList.remove('nine'); |
|
||||
videoGrid.classList.add('four'); |
|
||||
generateVideoNodes(4); |
|
||||
//更新按钮点击状态
|
|
||||
fourViewButton.classList.remove('btn-secondary'); |
|
||||
fourViewButton.classList.add('btn-primary'); |
|
||||
nineViewButton.classList.remove('btn-primary'); |
|
||||
nineViewButton.classList.add('btn-secondary'); |
|
||||
}); |
|
||||
|
|
||||
document.getElementById('nineView').addEventListener('click', function() { |
|
||||
if (nineViewButton.classList.contains('btn-primary')) { |
|
||||
return; // 如果按钮已经是选中状态,直接返回
|
|
||||
} |
|
||||
const videoGrid = document.getElementById('videoGrid'); |
|
||||
videoGrid.classList.remove('four'); |
|
||||
videoGrid.classList.add('nine'); |
|
||||
generateVideoNodes(9); |
|
||||
//更新按钮点击状态
|
|
||||
fourViewButton.classList.remove('btn-primary'); |
|
||||
fourViewButton.classList.add('btn-secondary'); |
|
||||
nineViewButton.classList.remove('btn-secondary'); |
|
||||
nineViewButton.classList.add('btn-primary'); |
|
||||
}); |
|
||||
|
|
||||
function generateVideoNodes(count) { //在这里显示视频-初始化 ---这里使用重置逻辑
|
|
||||
//结束在播放的socket
|
|
||||
for(let key in video_list){ |
|
||||
//flv使用
|
|
||||
// const videoFrame = document.getElementById(`video-${key}`);
|
|
||||
// const event = new Event('closeVideo');
|
|
||||
// videoFrame.dispatchEvent(event);
|
|
||||
|
|
||||
//通用关闭
|
|
||||
run_list[key] = false; |
|
||||
video_list[key].close(); |
|
||||
berror_state_list[key] = false; |
|
||||
delete video_list[key]; |
|
||||
} |
|
||||
//切换窗口布局
|
|
||||
const videoGrid = document.getElementById('videoGrid'); |
|
||||
let html = ''; |
|
||||
for (let i = 0; i < count; i++) { |
|
||||
let frameWidth = count === 4 ? 'calc(50% - 10px)' : 'calc(33.33% - 10px)'; |
|
||||
html += ` |
|
||||
<div class="video-frame" data-frame-id="${i}" style="width: ${frameWidth};" |
|
||||
ondrop="drop(event)" ondragover="allowDrop(event)"> |
|
||||
<div class="video-header"> |
|
||||
<div class="video-title">Video Stream ${i+1}</div> |
|
||||
<div class="video-buttons"> |
|
||||
<button onclick="toggleFullScreen(${i})">🔲</button> |
|
||||
<button onclick="closeVideo(${i})">❌</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="video-area"><canvas id="video-${i}"></canvas></div> |
|
||||
</div>`; |
|
||||
} |
|
||||
videoGrid.innerHTML = html; |
|
||||
|
|
||||
//开始还原视频,获取视频接口
|
|
||||
// if(m_count != 0){
|
|
||||
const url = `/api/viewlist?count=${count}`; |
|
||||
fetch(url) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
console.log('Success:', data); |
|
||||
clist = data.clist; |
|
||||
elist = data.elist; |
|
||||
nlist = data.nlist; |
|
||||
for(let i=0;i<clist.length;i++){ |
|
||||
if(parseInt(elist[i]) < count){ |
|
||||
connectToStream(elist[i],clist[i],nlist[i]) |
|
||||
//startFLVStream(elist[i],clist[i],nlist[i]);
|
|
||||
} |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('Error:', error); |
|
||||
}); |
|
||||
// }
|
|
||||
//m_count = count
|
|
||||
} |
|
||||
|
|
||||
function toggleFullScreen(id) { |
|
||||
console.log('toggleFullScreen'); |
|
||||
const videoFrame = document.querySelector(`[data-frame-id="${id}"]`); |
|
||||
if (!document.fullscreenElement) { |
|
||||
videoFrame.requestFullscreen().catch(err => { |
|
||||
alert(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); |
|
||||
}); |
|
||||
} else { |
|
||||
document.exitFullscreen(); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
function allowDrop(event) { |
|
||||
event.preventDefault(); |
|
||||
} |
|
||||
|
|
||||
function drag(event) { |
|
||||
event.dataTransfer.setData("text", event.target.dataset.nodeId); |
|
||||
event.dataTransfer.setData("name", event.target.dataset.nodeName); |
|
||||
} |
|
||||
|
|
||||
function drop(event) { |
|
||||
event.preventDefault(); |
|
||||
const nodeId = event.dataTransfer.getData("text"); |
|
||||
const nodeName = event.dataTransfer.getData("name"); |
|
||||
const frameId = event.currentTarget.dataset.frameId; |
|
||||
|
|
||||
//需要判断下当前窗口是否已经在播放视频
|
|
||||
const imgElement = document.getElementById(`video-${frameId}`); |
|
||||
const titleElement = document.querySelector(`[data-frame-id="${frameId}"] .video-title`); |
|
||||
|
|
||||
if (titleElement.textContent !== `Video Stream ${Number(frameId)+1}`) { |
|
||||
showModal('请先关闭当前窗口视频,然后再播放新的视频。'); |
|
||||
return; |
|
||||
}; |
|
||||
//发送视频链接接口
|
|
||||
const url = '/api/start_stream'; |
|
||||
const data = {"channel_id":nodeId,"element_id":frameId}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
if(istatus === 0){ |
|
||||
showModal(data.msg); // 使用 Modal 显示消息
|
|
||||
return; |
|
||||
} |
|
||||
else{ |
|
||||
//获取视频流
|
|
||||
console.log("drop触发") |
|
||||
connectToStream(frameId,nodeId,nodeName); |
|
||||
//startFLVStream(frameId,nodeId,nodeName); #基于FLV的开发程度:后端直接用RTSP流转发是有画面的,但CPU占用太高,用不了。2024-8-30
|
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|
||||
return; |
|
||||
}); |
|
||||
//console.log('retrun 只是把fetch结束,这里的代码还是会执行');
|
|
||||
} |
|
||||
|
|
||||
function connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanvas,streamUrl) { |
|
||||
//判断是否有重复socket,进行删除
|
|
||||
if(element_id in video_list) { |
|
||||
run_list[element_id] = false; |
|
||||
video_list[element_id].close(); |
|
||||
delete video_list[element_id]; |
|
||||
console.log("有历史数据未删干净!!---",element_id) //要不要等待待定
|
|
||||
} |
|
||||
// 每次连接时增加版本号
|
|
||||
const current_version = (connection_version[element_id] || 0) + 1; |
|
||||
connection_version[element_id] = current_version; |
|
||||
const socket = new WebSocket(streamUrl); |
|
||||
socket.binaryType = 'arraybuffer'; // 设置为二进制数据接收
|
|
||||
socket.customData = { channel_id: channel_id, element_id: element_id, |
|
||||
imgcanvas:imgcanvas,ctx:ctx,offscreenCtx:offscreenCtx,offscreenCanvas:offscreenCanvas, |
|
||||
version_id: current_version,streamUrl:streamUrl}; // 自定义属性 -- JS异步事件只能等到当前同步任务(代码块)完成之后才有可能被触发。
|
|
||||
//新的连接
|
|
||||
video_list[element_id] = socket; |
|
||||
run_list[element_id] = true; |
|
||||
berror_state_list[element_id] = false; |
|
||||
imgcanvas.style.display = 'block'; |
|
||||
|
|
||||
// 处理连接打开事件
|
|
||||
socket.onopen = function(){ |
|
||||
console.log('WebSocket connection established--',socket.customData.channel_id); |
|
||||
}; |
|
||||
|
|
||||
socket.onmessage = function(event) { |
|
||||
let el_id = socket.customData.element_id |
|
||||
let cl_id = socket.customData.channel_id |
|
||||
let imgcanvas = socket.customData.imgcanvas |
|
||||
let ctx = socket.customData.ctx |
|
||||
let offctx = socket.customData.offscreenCtx |
|
||||
let offscreenCanvas = socket.customData.offscreenCanvas |
|
||||
|
|
||||
// 转换为字符串来检查前缀
|
|
||||
let message = new TextDecoder().decode(event.data.slice(0, 6)); // 取前6个字节
|
|
||||
if (message.startsWith('frame:')){ |
|
||||
//如有错误信息显示 -- 清除错误信息
|
|
||||
if(berror_state_list[el_id]){ |
|
||||
console.log("清除错误信息!"); |
|
||||
removeErrorMessage(imgcanvas); |
|
||||
berror_state_list[el_id] = false; |
|
||||
} |
|
||||
// 接收到 JPG 图像数据,转换为 Blob
|
|
||||
let img = new Image(); |
|
||||
let blob = new Blob([event.data.slice(6)], { type: 'image/jpeg' }); |
|
||||
// 将 Blob 转换为可用的图像 URL
|
|
||||
img.src = URL.createObjectURL(blob); |
|
||||
//定义图片加载函数
|
|
||||
img.onload = function() { |
|
||||
imgcanvas.width = offscreenCanvas.width = img.width; |
|
||||
imgcanvas.height = offscreenCanvas.height = img.height; |
|
||||
|
|
||||
// 在 OffscreenCanvas 上绘制
|
|
||||
offctx.clearRect(0, 0, imgcanvas.width, imgcanvas.height); |
|
||||
offctx.drawImage(img, 0, 0, imgcanvas.width, imgcanvas.height); |
|
||||
// 将 OffscreenCanvas 的内容复制到主 canvas
|
|
||||
ctx.drawImage(offscreenCanvas, 0, 0); |
|
||||
|
|
||||
// 用完就释放
|
|
||||
URL.revokeObjectURL(img.src); |
|
||||
// blob = null
|
|
||||
// img = null
|
|
||||
// message = null
|
|
||||
// event.data = null
|
|
||||
// event = null
|
|
||||
}; |
|
||||
}else if(message.startsWith('error:')){ |
|
||||
const errorText = new TextDecoder().decode(event.data.slice(6)); // 截掉前缀 'error:'
|
|
||||
//目前只处理一个错误信息,暂不区分
|
|
||||
displayErrorMessage(imgcanvas, "该视频源未获取到画面,请检查后刷新重试,默认两分钟后重连"); |
|
||||
berror_state_list[el_id] = true; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
socket.onclose = function() { |
|
||||
let el_id = socket.customData.element_id; |
|
||||
let cl_id = socket.customData.channel_id; |
|
||||
//判断是不是要重连
|
|
||||
if(socket.customData.version_id === connection_version[el_id]){ |
|
||||
if(run_list[el_id]){ |
|
||||
console.log(`尝试重新连接... Channel ID: ${cl_id}`); |
|
||||
setTimeout(() => connect(cl_id, el_id, socket.customData.imgcanvas, |
|
||||
socket.customData.ctx,socket.customData.streamUrl), 1000*10); // 尝试在10秒后重新连接
|
|
||||
} |
|
||||
delete video_list[el_id]; |
|
||||
delete connection_version[el_id]; |
|
||||
delete run_list[el_id]; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
socket.onerror = function() { |
|
||||
console.log(`WebSocket错误,Channel ID: ${socket.customData.channel_id}`); |
|
||||
socket.close(1000, "Normal Closure"); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
function connectToStream(element_id,channel_id,channel_name) { |
|
||||
console.log("开始连接视频",element_id,channel_id); |
|
||||
//更新控件状态--设置视频区域的标题
|
|
||||
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`); |
|
||||
titleElement.textContent = channel_name; |
|
||||
//视频控件
|
|
||||
//const imgElement = document.getElementById(`video-${element_id}`);
|
|
||||
//imgElement.alt = `Stream ${channel_name}`;
|
|
||||
const imgcanvas = document.getElementById(`video-${element_id}`); |
|
||||
const ctx = imgcanvas.getContext('2d') |
|
||||
// 创建 OffscreenCanvas
|
|
||||
const offscreenCanvas = new OffscreenCanvas(imgcanvas.width, imgcanvas.height); |
|
||||
const offscreenCtx = offscreenCanvas.getContext('2d'); |
|
||||
|
|
||||
const streamUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`; |
|
||||
|
|
||||
//创建websocket连接,并接收和显示图片
|
|
||||
connect(channel_id,element_id,imgcanvas,ctx,offscreenCtx,offscreenCanvas,streamUrl); //执行websocket连接 -- 异步的应该会直接返回
|
|
||||
} |
|
||||
|
|
||||
function closeVideo(id) { |
|
||||
if(id in video_list) { |
|
||||
const imgcanvas = document.getElementById(`video-${id}`); |
|
||||
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`); |
|
||||
//断socket
|
|
||||
run_list[id] = false; |
|
||||
video_list[id].close(); |
|
||||
delete video_list[id]; |
|
||||
//清空控件状态
|
|
||||
imgcanvas.style.display = 'none'; // 停止播放时隐藏元素
|
|
||||
titleElement.textContent = `Video Stream ${id+1}`; |
|
||||
removeErrorMessage(imgcanvas); |
|
||||
berror_state_list[id] = false; |
|
||||
//删记录
|
|
||||
const url = '/api/close_stream'; |
|
||||
const data = {"element_id":id}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
console.log('Success:', data); |
|
||||
const istatus = data.status; |
|
||||
if(istatus == 0){ |
|
||||
showModal(data.msg); // 使用 Modal 显示消息
|
|
||||
return; |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|
||||
return; |
|
||||
}); |
|
||||
} |
|
||||
else{ |
|
||||
showModal('当前视频窗口未播放视频。'); |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function startFLVStream(element_id,channel_id,channel_name) { |
|
||||
|
|
||||
// 设置视频区域的标题
|
|
||||
const titleElement = document.querySelector(`[data-frame-id="${element_id}"] .video-title`); |
|
||||
titleElement.textContent = channel_name; |
|
||||
//获取视频
|
|
||||
// const imgElement = document.getElementById(`video-${element_id}`);
|
|
||||
// imgElement.alt = `Stream ${channel_name}`;
|
|
||||
const videoElement = document.getElementById(`video-${element_id}`); |
|
||||
let reconnectAttempts = 0; |
|
||||
const maxReconnectAttempts = 3; |
|
||||
const flvUrl = `ws://${window.location.host}/api/ws/video_feed/${channel_id}`; |
|
||||
|
|
||||
function initFLVPlayer() { |
|
||||
if (flvjs.isSupported()) { |
|
||||
//要避免重复播放
|
|
||||
if(element_id in video_list) { |
|
||||
closeFLVStream(element_id) |
|
||||
}else{ |
|
||||
video_list[element_id] = element_id; |
|
||||
berror_state_list[element_id] = true; |
|
||||
} |
|
||||
|
|
||||
flvPlayer = flvjs.createPlayer({ |
|
||||
type: 'flv', |
|
||||
url: flvUrl, |
|
||||
}); |
|
||||
|
|
||||
flvPlayer.attachMediaElement(videoElement); |
|
||||
flvPlayer.load(); |
|
||||
flvPlayer.play(); |
|
||||
|
|
||||
// 设定超时时间,例如10秒
|
|
||||
timeoutId = setTimeout(() => { |
|
||||
console.error('No video data received. Closing connection.'); |
|
||||
flvPlayer.destroy(); // 停止视频
|
|
||||
// 显示错误信息或提示
|
|
||||
displayErrorMessage(videoElement, "该视频源获取画面超时,请检查后刷新重试,默认两分钟后重连"); |
|
||||
berror_state_list[element_id] = true; |
|
||||
}, 130000); // 130秒
|
|
||||
|
|
||||
// 错误处理
|
|
||||
flvPlayer.on(flvjs.Events.ERROR, (errorType, errorDetail) => { |
|
||||
console.error(`FLV Error: ${errorType} - ${errorDetail}`); |
|
||||
if (reconnectAttempts < maxReconnectAttempts) { |
|
||||
reconnectAttempts++; |
|
||||
console.log("开始重连") |
|
||||
setTimeout(initFLVPlayer, 30000); // 尝试重连
|
|
||||
} else { |
|
||||
displayErrorMessage(videoElement, "重连超时,请检查后重试,可联系技术支持!"); |
|
||||
berror_state_list[element_id] = true; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 监听播放事件,如果播放成功则清除超时计时器
|
|
||||
flvPlayer.on(flvjs.Events.STATISTICS_INFO, () => { |
|
||||
clearTimeout(timeoutId); |
|
||||
timeoutId = null; |
|
||||
removeErrorMessage(videoElement); |
|
||||
berror_state_list[element_id] = false; |
|
||||
}); |
|
||||
|
|
||||
// 关闭视频流时销毁播放器
|
|
||||
videoElement.addEventListener('closeVideo', () => { |
|
||||
if(flvPlayer){ |
|
||||
flvPlayer.destroy(); |
|
||||
videoElement.removeEventListener('closeVideo', onCloseVideo); |
|
||||
flvPlayer.off(flvjs.Events.ERROR); |
|
||||
flvPlayer.off(flvjs.Events.STATISTICS_INFO); |
|
||||
delete flvPlayer |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
} else { |
|
||||
console.error('FLV is not supported in this browser.'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
initFLVPlayer(); |
|
||||
} |
|
||||
|
|
||||
// 主动关闭视频的函数
|
|
||||
function closeFLVStream(id) { |
|
||||
const titleElement = document.querySelector(`[data-frame-id="${id}"] .video-title`); |
|
||||
if (titleElement.textContent === `Video Stream ${Number(id)+1}`) { |
|
||||
showModal('当前视频窗口未播放视频。'); |
|
||||
return; |
|
||||
}; |
|
||||
|
|
||||
//发送视频链接接口
|
|
||||
const url = '/api/close_stream'; |
|
||||
const data = {"element_id":id}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
console.log('Success:', data); |
|
||||
const istatus = data.status; |
|
||||
if(istatus == 0){ |
|
||||
showModal(data.msg); // 使用 Modal 显示消息
|
|
||||
return; |
|
||||
} |
|
||||
else{ |
|
||||
const videoFrame = document.getElementById(`video-${element_id}`); |
|
||||
const event = new Event('closeVideo'); |
|
||||
videoFrame.dispatchEvent(event); |
|
||||
|
|
||||
//videoFrame.style.display = 'none'; // 停止播放时隐藏 img 元素
|
|
||||
titleElement.textContent = `Video Stream ${id+1}`; |
|
||||
removeErrorMessage(videoFrame); |
|
||||
berror_state_list[key] = false; |
|
||||
delete video_list[id]; |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
showModal(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|
||||
return; |
|
||||
}); |
|
||||
|
|
||||
} |
|
||||
|
|
||||
|
|
||||
function displayErrorMessage(imgElement, message) { |
|
||||
removeErrorMessage(imgElement) |
|
||||
imgElement.style.display = 'none'; // 隐藏图片
|
|
||||
const errorElement = document.createElement('div'); |
|
||||
errorElement.textContent = message; |
|
||||
errorElement.classList.add('error-message'); |
|
||||
imgElement.parentNode.appendChild(errorElement); |
|
||||
} |
|
||||
|
|
||||
function removeErrorMessage(imgElement) { |
|
||||
const errorElement = imgElement.parentNode.querySelector('.error-message'); |
|
||||
if (errorElement) { |
|
||||
imgElement.parentNode.removeChild(errorElement); |
|
||||
imgElement.style.display = 'block'; |
|
||||
} |
|
||||
} |
|
||||
|
|
@ -1,735 +0,0 @@ |
|||||
const apiEndpoint = '/api/channel/list'; |
|
||||
const rowsPerPage = 10; |
|
||||
//算法配置窗口部分控件
|
|
||||
const searchEndpoint = '/api/channel/select'; |
|
||||
const canvas = document.getElementById('myCanvas'); |
|
||||
const ctx = canvas.getContext('2d'); |
|
||||
const backgroundCanvas = document.getElementById('backgroundCanvas'); |
|
||||
const backgroundCtx = backgroundCanvas.getContext('2d'); |
|
||||
const img = new Image(); |
|
||||
const tbody = document.getElementById('schedule-body');//布防计划
|
|
||||
|
|
||||
let currentPage = 1; |
|
||||
let channelData = []; |
|
||||
let channelData_bak = []; |
|
||||
let areaData = ["请选择"]; |
|
||||
let currentEditingRow = null; |
|
||||
let cid_schedule = "-1"; |
|
||||
let m_polygon = ""; |
|
||||
let check_area = 0; |
|
||||
let draw_status = false; //是否是绘制状态,处于绘制状态才能开始绘制
|
|
||||
let b_img = false; //有没有加载图片成功,如果没有初始化的时候就不绘制线条了。
|
|
||||
let points = []; //检测区域的点坐标数组
|
|
||||
//布防计划
|
|
||||
|
|
||||
|
|
||||
document.addEventListener('DOMContentLoaded', function () { |
|
||||
fetchChannelData(); //初始化通道管理页面元素数据
|
|
||||
|
|
||||
document.getElementById('searchButton').addEventListener('click', function () { |
|
||||
performSearch(); |
|
||||
}); |
|
||||
//新增通道模块--保存按钮
|
|
||||
document.getElementById('saveButton').addEventListener('click', function () { |
|
||||
addChannel(1); |
|
||||
}); |
|
||||
//修改通道模块--保存按钮
|
|
||||
document.getElementById('saveButton_cc').addEventListener('click', function () { |
|
||||
addChannel(2); |
|
||||
}); |
|
||||
//算法配置模块--取消按钮
|
|
||||
document.getElementById('cancelButton_mx').addEventListener('click', function () { |
|
||||
close_mx_model(); |
|
||||
}); |
|
||||
//保存算法配置--保存按钮
|
|
||||
document.getElementById('saveButton_mx').addEventListener('click', function () { |
|
||||
save_mx_model(); |
|
||||
}); |
|
||||
//开始绘制区域按钮
|
|
||||
document.getElementById('but_hzqy').addEventListener('click', function () { |
|
||||
startDraw(); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
//添加和修改通道 1--新增,2--修改
|
|
||||
function addChannel(itype) { |
|
||||
let area; |
|
||||
let cName; |
|
||||
let Rtsp; |
|
||||
let cid; |
|
||||
const spinnerOverlay = document.getElementById("spinnerOverlay"); |
|
||||
let saveButton = null; |
|
||||
let CNameInput = null; |
|
||||
let RTSPInput = null; |
|
||||
|
|
||||
if(itype ==1){ |
|
||||
saveButton = document.getElementById('saveButton'); |
|
||||
CNameInput = document.getElementById('CNameInput'); |
|
||||
RTSPInput = document.getElementById('RTSPInput'); |
|
||||
area = document.getElementById('areaSelect_M').value; |
|
||||
cid = -1 |
|
||||
} |
|
||||
else if(itype ==2){ |
|
||||
saveButton = document.getElementById('saveButton_cc'); |
|
||||
CNameInput = document.getElementById('CNameInput_cc'); |
|
||||
RTSPInput = document.getElementById('RTSPInput_cc'); |
|
||||
area = document.getElementById('areaSelect_CC').value; |
|
||||
cid = currentEditingRow.cells[0].innerText; |
|
||||
} |
|
||||
console.log("点击了保存按钮"); |
|
||||
cName = CNameInput.value.trim(); |
|
||||
Rtsp = RTSPInput.value.trim(); |
|
||||
if(area === "请选择"){ |
|
||||
alert('请选择所属区域'); |
|
||||
} |
|
||||
else{ |
|
||||
if (cName && Rtsp) { |
|
||||
saveButton.disabled = true; |
|
||||
//发送视频链接接口
|
|
||||
const url = '/api/channel/add'; |
|
||||
const data = {"area":area,"cName":cName,"Rtsp":Rtsp,"cid":cid}; |
|
||||
// 显示 Spinners
|
|
||||
spinnerOverlay.style.display = "flex"; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
saveButton.disabled = false; |
|
||||
alert(data.msg); // 使用 Modal 显示消息
|
|
||||
if(istatus == 1){ |
|
||||
//刷新列表
|
|
||||
fetchChannelData(); |
|
||||
if(itype ==1){ |
|
||||
//添加通道成功
|
|
||||
$('#channelModal').modal('hide'); |
|
||||
} |
|
||||
else if(itype==2){ |
|
||||
//修改通道成功
|
|
||||
currentEditingRow = null; |
|
||||
$('#ChangeC').modal('hide'); |
|
||||
} |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|
||||
// 启用保存按钮
|
|
||||
saveButton.disabled = false; |
|
||||
return; |
|
||||
}) |
|
||||
.finally(()=>{ |
|
||||
// 隐藏 Spinners
|
|
||||
spinnerOverlay.style.display = "none"; |
|
||||
}); |
|
||||
} else { |
|
||||
alert('通道名称和RTSP地址不能为空'); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//初始化通道管理页面元素数据
|
|
||||
async function fetchChannelData() { //获取通道相关信息(/api/channel/list),刷新通道表格控件数据
|
|
||||
try { |
|
||||
const response = await fetch(apiEndpoint); |
|
||||
channelData = await response.json(); |
|
||||
channelData_bak = channelData; |
|
||||
|
|
||||
url = "/api/channel/area/list" |
|
||||
area_response = await fetch(url); |
|
||||
areaDatas = await area_response.json(); |
|
||||
areaData = ["请选择"]; //清空下
|
|
||||
areaDatas.forEach((area) => { |
|
||||
areaData.push(area.area_name) |
|
||||
}); |
|
||||
|
|
||||
renderTable(); //刷新表格
|
|
||||
renderPagination(); //刷新分页元素
|
|
||||
renderAreaOptions(); //所属区域下来框
|
|
||||
} catch (error) { |
|
||||
console.error('Error fetching channel data:', error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//刷新表单页面数据
|
|
||||
function renderTable() { |
|
||||
const tableBody = document.getElementById('table-body'); |
|
||||
tableBody.innerHTML = ''; |
|
||||
|
|
||||
const start = (currentPage - 1) * rowsPerPage; |
|
||||
const end = start + rowsPerPage; |
|
||||
const pageData = channelData.slice(start, end); |
|
||||
const surplus_count = rowsPerPage - pageData.length; |
|
||||
|
|
||||
pageData.forEach((channel) => { |
|
||||
// if(area_name!==channel.area_name){ //这里要求区域名称一样的要在一起
|
|
||||
// area_name = channel.area_name;
|
|
||||
// areaData.push(area_name);
|
|
||||
// }
|
|
||||
const row = document.createElement('tr'); |
|
||||
row.innerHTML = ` |
|
||||
<td>${channel.ID}</td> |
|
||||
<td>${channel.area_name}</td> |
|
||||
<td>${channel.channel_name}</td> |
|
||||
<td>${channel.ulr}</td> |
|
||||
<td>${channel.model_name}</td> |
|
||||
<td> |
|
||||
<button class="btn btn-primary btn-sm modify-btn">修改</button> |
|
||||
<button class="btn btn-secondary btn-sm algorithm-btn">算法</button> |
|
||||
<button class="btn btn-danger btn-sm delete-btn">删除</button> |
|
||||
</td> |
|
||||
`;
|
|
||||
tableBody.appendChild(row); |
|
||||
row.querySelector('.modify-btn').addEventListener('click', () => modifyChannel(row)); |
|
||||
row.querySelector('.algorithm-btn').addEventListener('click', () => configureAlgorithm(row)); |
|
||||
row.querySelector('.delete-btn').addEventListener('click', () => deleteChannel(row)); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
//关键字查询数据
|
|
||||
async function performSearch() { |
|
||||
try { |
|
||||
const area = document.getElementById('areaSelect').value; |
|
||||
const channelName = document.getElementById('channelNameInput').value; |
|
||||
if(area === "请选择" && channelName===""){ |
|
||||
channelData = channelData_bak; |
|
||||
} |
|
||||
else if(area === "请选择"){ |
|
||||
channelData = []; |
|
||||
channelData_bak.forEach((channel) => { |
|
||||
if(channelName === channel.channel_name){ |
|
||||
channelData.push(channel); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
else if(channelName === ""){ |
|
||||
channelData = []; |
|
||||
channelData_bak.forEach((channel) => { |
|
||||
if(area === channel.area_name){ |
|
||||
channelData.push(channel); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
else{ |
|
||||
channelData = []; |
|
||||
channelData_bak.forEach((channel) => { |
|
||||
if(area === channel.area_name && channelName === channel.channel_name){ |
|
||||
channelData.push(channel); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
// 渲染表格和分页控件
|
|
||||
currentPage = 1; // 重置当前页为第一页
|
|
||||
renderTable(); |
|
||||
renderPagination(); |
|
||||
} catch (error) { |
|
||||
console.error('Error performing search:', error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//点击修改按钮,显示修改通道信息模块 --只是显示
|
|
||||
function modifyChannel(row) { |
|
||||
// const cid = row.cells[0].innerText;
|
|
||||
const areaName = row.cells[1].innerText; |
|
||||
const channelName = row.cells[2].innerText; |
|
||||
const url = row.cells[3].innerText; |
|
||||
|
|
||||
const area = document.getElementById('areaSelect_CC'); |
|
||||
const CName = document.getElementById('CNameInput_cc'); |
|
||||
const RTSP = document.getElementById('RTSPInput_cc'); |
|
||||
|
|
||||
for(let i=0;i< area.options.length;i++){ |
|
||||
if(area.options[i].value === areaName){ |
|
||||
area.options[i].selected = true; |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
CName.value = channelName; |
|
||||
RTSP.value = url; |
|
||||
|
|
||||
currentEditingRow = row; |
|
||||
$('#ChangeC').modal('show'); |
|
||||
} |
|
||||
|
|
||||
//点击算法按钮,显示算法配置模块 --只是显示
|
|
||||
function configureAlgorithm(row) { |
|
||||
//获取当前行信息
|
|
||||
currentEditingRow = row; |
|
||||
const cid = row.cells[0].innerText; |
|
||||
//清除数据,若需要的话
|
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); //清除左侧绘画和画线信息
|
|
||||
tbody.innerHTML = ''; //清空布防控件数据
|
|
||||
points = []; //清空绘制检测区域
|
|
||||
draw_status = false; |
|
||||
b_img = false; |
|
||||
document.getElementById('but_hzqy').textContent = "绘制区域"; |
|
||||
//开始初始化算法管理模块
|
|
||||
show_channel_model_schedule(cid); //获取并显示结构化数据
|
|
||||
show_channel_img(cid); //获取并显示一帧图片 -- 获取不到图片就是黑画面 --并要绘制检测区域
|
|
||||
|
|
||||
//显示窗口
|
|
||||
$('#MX_M').modal('show'); |
|
||||
} |
|
||||
|
|
||||
//获取一帧图片
|
|
||||
function show_channel_img(cid){ |
|
||||
const data = {"cid":cid}; |
|
||||
fetch('/api/channel/img', { |
|
||||
method: 'POST', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' |
|
||||
}, |
|
||||
body: JSON.stringify(data) |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.image) { |
|
||||
b_img = true; |
|
||||
img.src = 'data:image/jpeg;base64,' + data.image; |
|
||||
} else { |
|
||||
console.error('Error:', data.error); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => console.error('Error:', error)); |
|
||||
} |
|
||||
|
|
||||
//图片加载事项
|
|
||||
img.onload = () => { //清除、画图和画线应该分开
|
|
||||
// 设置画布宽高
|
|
||||
backgroundCanvas.width = canvas.width = img.width; |
|
||||
backgroundCanvas.height = canvas.height = img.height; |
|
||||
// 将图片绘制到背景画布上
|
|
||||
backgroundCtx.drawImage(img, 0, 0, img.width, img.height); |
|
||||
drawLines(); |
|
||||
// 将背景画布的内容复制到前台画布上
|
|
||||
//ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); //绘制画面
|
|
||||
}; |
|
||||
|
|
||||
//开始和重新绘制
|
|
||||
function startDraw(){ |
|
||||
if(!document.getElementById('zdjc').checked){ |
|
||||
alert("请先选择指定区域!"); |
|
||||
return; |
|
||||
} |
|
||||
let but = document.getElementById('but_hzqy'); |
|
||||
if(!draw_status){//开始绘制
|
|
||||
if(points.length >0){ |
|
||||
if (confirm('开始绘制将清除未提交保存的绘制数据,是否继续?')) { |
|
||||
draw_status = true; |
|
||||
points = []; |
|
||||
//按钮文字调整为结束绘制
|
|
||||
but.textContent = '结 束 绘 制'; |
|
||||
// 清除前台画布
|
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
// 将背景画布的内容复制到前台画布上
|
|
||||
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); |
|
||||
} |
|
||||
} |
|
||||
else{ |
|
||||
draw_status = true; |
|
||||
but.textContent = '结束绘制'; |
|
||||
} |
|
||||
} |
|
||||
else{//结束绘制
|
|
||||
draw_status = false; |
|
||||
but.textContent = '绘制区域'; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//单选按钮点击事件处理
|
|
||||
function handleRadioClick(event) { |
|
||||
const selectedRadio = event.target; |
|
||||
console.log('Selected Radio:', selectedRadio.id); |
|
||||
// 根据选中的单选按钮执行相应操作
|
|
||||
if (selectedRadio.id === 'qjjc') { |
|
||||
if(draw_status){ |
|
||||
alert("请先结束绘制后,再切换检测方案!"); |
|
||||
document.getElementById('zdjc').checked = true; |
|
||||
} |
|
||||
else{ |
|
||||
// 处理全画面生效的逻辑
|
|
||||
if(points.length>0){ |
|
||||
if (!confirm('切换到全画面生效,将清除已绘制的区域信息,是否切换?')) { |
|
||||
document.getElementById('zdjc').checked = true; |
|
||||
}else{ |
|
||||
points = []; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
//console.log('全画面生效');
|
|
||||
} else if (selectedRadio.id === 'zdjc') { |
|
||||
// 处理指定区域的逻辑
|
|
||||
console.log('指定区域'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 鼠标点击事件处理--动态绘图
|
|
||||
canvas.addEventListener('click', (event) => { |
|
||||
if(draw_status){ |
|
||||
const rect = canvas.getBoundingClientRect(); |
|
||||
const scaleX = canvas.width / rect.width; |
|
||||
const scaleY = canvas.height / rect.height; |
|
||||
|
|
||||
// 获取鼠标相对于canvas的位置
|
|
||||
const x = (event.clientX - rect.left) * scaleX; |
|
||||
const y = (event.clientY - rect.top) * scaleY; |
|
||||
points.push({ x, y }); |
|
||||
console.log(points); |
|
||||
//绘制线条
|
|
||||
drawLines(); |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
// 绘制区域,各点连接
|
|
||||
function drawLines() { |
|
||||
if(b_img){ |
|
||||
// 清除前台画布
|
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height); |
|
||||
// 将背景画布的内容复制到前台画布上
|
|
||||
ctx.drawImage(backgroundCanvas, 0, 0, canvas.width, canvas.height); |
|
||||
|
|
||||
// 绘制点和线
|
|
||||
ctx.strokeStyle = 'red'; |
|
||||
ctx.lineWidth = 2; |
|
||||
|
|
||||
if (points.length > 0) { |
|
||||
ctx.beginPath(); |
|
||||
ctx.moveTo(points[0].x, points[0].y); |
|
||||
|
|
||||
for (let i = 1; i < points.length; i++) { |
|
||||
ctx.lineTo(points[i].x, points[i].y); |
|
||||
} |
|
||||
|
|
||||
// 连接最后一个点到起点
|
|
||||
ctx.lineTo(points[0].x, points[0].y); |
|
||||
ctx.stroke(); |
|
||||
} |
|
||||
|
|
||||
points.forEach(point => { |
|
||||
ctx.beginPath(); |
|
||||
ctx.arc(point.x, point.y, 5, 0, Math.PI * 2); |
|
||||
ctx.fillStyle = 'red'; |
|
||||
ctx.fill(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//获取并显示该通道相关算法的结构化数据 --- 这里用GET会更加贴切一些
|
|
||||
function show_channel_model_schedule(cid){ |
|
||||
//发送视频链接接口
|
|
||||
const url = '/api/channel/C2M'; |
|
||||
const data = {"cid":cid}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const m_datas = data.m_datas; //算法清单
|
|
||||
const c2m_data = data.c2m_data; //该通道管理算法的相关数据,会有空的情况
|
|
||||
const schedule = data.schedule; //布防计划
|
|
||||
//console.log("m_datas--",m_datas);
|
|
||||
//console.log("c2m_data--",c2m_data);
|
|
||||
//console.log("schedule--",schedule);
|
|
||||
//配置算法下拉清单
|
|
||||
select_datas = ["请选择"]; |
|
||||
m_datas.forEach(option => { |
|
||||
select_datas.push(option.model_name); |
|
||||
}); |
|
||||
set_select_data("model_select",select_datas); |
|
||||
select_str = currentEditingRow.cells[4].innerText; |
|
||||
//检测区域
|
|
||||
if(c2m_data.length >0){ |
|
||||
model_id = c2m_data[0].model_id; |
|
||||
model_name = currentEditingRow.cells[4].innerText; |
|
||||
set_select_selct("model_select",model_name); |
|
||||
check_area = c2m_data[0].check_area |
|
||||
if( check_area == 0){ //全画面生效
|
|
||||
document.getElementById('qjjc').checked = true; |
|
||||
m_polygon = ""; |
|
||||
} |
|
||||
else{//指定区域
|
|
||||
document.getElementById('zdjc').checked = true; |
|
||||
m_polygon = c2m_data[0].polygon; |
|
||||
console.log("m_polygon--",m_polygon); |
|
||||
if(m_polygon !== ""){ //指定区域了,一般是会有数据的。
|
|
||||
const coords = parseCoordStr(m_polygon); |
|
||||
points = coords; |
|
||||
} |
|
||||
} |
|
||||
//阈值
|
|
||||
document.getElementById('zxyz').value = c2m_data[0].conf_thres |
|
||||
document.getElementById('iouyz').value = c2m_data[0].iou_thres |
|
||||
} |
|
||||
|
|
||||
//布防计划
|
|
||||
const days = ['一', '二', '三', '四', '五', '六','日']; |
|
||||
const num_days=['0','1','2','3','4','5','6'] |
|
||||
days.forEach((day, dayIndex) => { |
|
||||
const row = document.createElement('tr'); |
|
||||
const dayCell = document.createElement('th'); |
|
||||
dayCell.textContent = day; |
|
||||
row.appendChild(dayCell); |
|
||||
num_day = num_days[dayIndex] |
|
||||
for (let hour = 0; hour < 24; hour++) { |
|
||||
const cell = document.createElement('td'); |
|
||||
if(schedule.length >0){ |
|
||||
const status = schedule.find(item => item.day === num_day && item.hour === hour); |
|
||||
if (status && status.status === 1) { |
|
||||
cell.classList.add('blocked'); |
|
||||
} else { |
|
||||
cell.classList.add('allowed'); |
|
||||
} |
|
||||
} |
|
||||
else{ |
|
||||
cell.classList.add('blocked'); |
|
||||
} |
|
||||
row.appendChild(cell); |
|
||||
|
|
||||
cell.addEventListener('click', () => { |
|
||||
if (cell.classList.contains('blocked')) { |
|
||||
cell.classList.remove('blocked'); |
|
||||
cell.classList.add('allowed'); |
|
||||
// Update status in the database
|
|
||||
//updateStatus(day, hour, 0);
|
|
||||
} else { |
|
||||
cell.classList.remove('allowed'); |
|
||||
cell.classList.add('blocked'); |
|
||||
// Update status in the database
|
|
||||
//updateStatus(day, hour, 1);
|
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
tbody.appendChild(row); |
|
||||
}); |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|
||||
return; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// 将字符串转换为数组
|
|
||||
function parseCoordStr(str) { |
|
||||
return str.match(/\(([^)]+)\)/g).map(pair => { |
|
||||
const [x, y] = pair.replace(/[()]/g, '').split(',').map(Number); |
|
||||
return { x, y }; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
//关闭算法配置窗口
|
|
||||
function close_mx_model(){ |
|
||||
if (confirm('确定退出窗口吗?未保存的修改将丢失!')) { |
|
||||
$('#MX_M').modal('hide'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//保存算法配置窗口数据
|
|
||||
function save_mx_model(){ |
|
||||
let model_name; //算法名称
|
|
||||
let check_area; //检测区域标识 0-全局,1-指定范围
|
|
||||
let polygon_str; //具体的检测区域
|
|
||||
let conf_thres; //置信阈值
|
|
||||
let iou_thres; //iou阈值
|
|
||||
let schedule; //布防计划
|
|
||||
const saveButton = document.getElementById('saveButton_mx'); |
|
||||
saveButton.disabled = true; //不可点击状态
|
|
||||
//配置算法
|
|
||||
model_name = document.getElementById("model_select").value; |
|
||||
//检测区域
|
|
||||
if(document.getElementById('zdjc').checked){ |
|
||||
check_area = 1; |
|
||||
console.log("points--",points); |
|
||||
if (points.length > 0){ |
|
||||
const formattedArray = points.map(point => `(${point.x},${point.y})`); |
|
||||
polygon_str = `[${formattedArray.join(',')}]`; |
|
||||
}else{ |
|
||||
polygon_str = ""; |
|
||||
} |
|
||||
|
|
||||
}else{ |
|
||||
check_area = 0; |
|
||||
polygon_str = ""; |
|
||||
} |
|
||||
//置信阈值和IOU阈值
|
|
||||
conf_thres = getInputValueAsFloat('zxyz'); |
|
||||
iou_thres = getInputValueAsFloat('iouyz'); |
|
||||
//验证数据
|
|
||||
if(model_name !== "请选择"){ |
|
||||
console.log(model_name); |
|
||||
if(conf_thres <= 0 || conf_thres>=1 || iou_thres <= 0 || iou_thres>=1){ |
|
||||
alert("阈值的有效范围是大于0,小于1;请输入正确的阈值(默认可0.5)。"); |
|
||||
saveButton.disabled = false; //不可点击状态
|
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
//布防计划
|
|
||||
// 定义一个对象来存储数据
|
|
||||
const scheduleData = { |
|
||||
'0': Array(24).fill(0), |
|
||||
'1': Array(24).fill(0), |
|
||||
'2': Array(24).fill(0), |
|
||||
'3': Array(24).fill(0), |
|
||||
'4': Array(24).fill(0), |
|
||||
'5': Array(24).fill(0), |
|
||||
'6': Array(24).fill(0) |
|
||||
}; |
|
||||
// 遍历 tbody 的每一行
|
|
||||
[...tbody.children].forEach((row, dayIndex) => { |
|
||||
// 获取当前行的所有单元格
|
|
||||
const cells = row.getElementsByTagName('td'); |
|
||||
|
|
||||
// 遍历每一个单元格
|
|
||||
for (let hour = 0; hour < cells.length; hour++) { |
|
||||
// 检查单元格的 class 是否包含 'blocked'
|
|
||||
if (cells[hour].classList.contains('blocked')) { |
|
||||
// 将对应的 scheduleData 位置设置为 1
|
|
||||
scheduleData[dayIndex][hour] = 1; |
|
||||
} else { |
|
||||
// 将对应的 scheduleData 位置设置为 0
|
|
||||
scheduleData[dayIndex][hour] = 0; |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
// 将 scheduleData 对象转换为 JSON 字符串
|
|
||||
const scheduleData_json = JSON.stringify(scheduleData); |
|
||||
//提交到服务器
|
|
||||
// console.log("model_name--",model_name);
|
|
||||
// console.log("check_area--",check_area);
|
|
||||
// console.log("polygon_str--",polygon_str);
|
|
||||
// console.log("iou_thres--",iou_thres);
|
|
||||
// console.log("conf_thres--",conf_thres);
|
|
||||
// console.log("schedule-- ",scheduleData_json);
|
|
||||
cid = currentEditingRow.cells[0].innerText; |
|
||||
const url = '/api/channel/chanegeC2M'; |
|
||||
const data = {"model_name":model_name,"check_area":check_area,"polygon_str":polygon_str,"iou_thres":iou_thres, |
|
||||
"conf_thres":conf_thres,"schedule":scheduleData_json,"cid":cid}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
if(istatus === 0){ |
|
||||
alert(data.msg); // 使用 Modal 显示消息
|
|
||||
// 启用保存按钮
|
|
||||
saveButton.disabled = false; |
|
||||
return; |
|
||||
} |
|
||||
else{ |
|
||||
// 启用保存按钮
|
|
||||
saveButton.disabled = false; |
|
||||
//刷新列表
|
|
||||
fetchChannelData(); |
|
||||
//$('#MX_M').modal('hide');
|
|
||||
alert("修改成功!"); |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
alert(`Error: ${error.message}`); |
|
||||
// 启用保存按钮
|
|
||||
saveButton.disabled = false; |
|
||||
return; |
|
||||
}); |
|
||||
|
|
||||
} |
|
||||
|
|
||||
//删除通道
|
|
||||
function deleteChannel(row) { |
|
||||
if (confirm('确定删除此通道吗?')) { |
|
||||
cid = row.cells[0].innerText; |
|
||||
//发送视频链接接口
|
|
||||
const url = '/api/channel/del'; |
|
||||
const data = {"cid":cid}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
if(istatus === 0){ |
|
||||
alert(data.msg); // 使用 Modal 显示消息
|
|
||||
return; |
|
||||
} |
|
||||
else{ |
|
||||
//刷新列表
|
|
||||
row.remove(); |
|
||||
alert("删除通道成功!"); |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
alert(`Error: ${error.message}`); // 使用 Modal 显示错误信息
|
|
||||
return; |
|
||||
}); |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//刷新分页标签
|
|
||||
function renderPagination() { |
|
||||
const pagination = document.getElementById('pagination'); |
|
||||
pagination.innerHTML = ''; |
|
||||
|
|
||||
const totalPages = Math.ceil(channelData.length / rowsPerPage); |
|
||||
for (let i = 1; i <= totalPages; i++) { |
|
||||
const pageItem = document.createElement('li'); |
|
||||
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); |
|
||||
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; |
|
||||
pageItem.addEventListener('click', (event) => { |
|
||||
event.preventDefault(); |
|
||||
currentPage = i; |
|
||||
renderTable(); |
|
||||
renderPagination(); |
|
||||
}); |
|
||||
pagination.appendChild(pageItem); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//刷新区域下拉控件
|
|
||||
function renderAreaOptions() { |
|
||||
const areaSelect = document.getElementById('areaSelect'); |
|
||||
const areaSelect_M = document.getElementById('areaSelect_M') |
|
||||
const areaSelect_CC = document.getElementById('areaSelect_CC') |
|
||||
//先清空
|
|
||||
areaSelect.innerHTML = ''; |
|
||||
areaSelect_M.innerHTML = ''; |
|
||||
areaSelect_CC.innerHTML = ''; |
|
||||
//再添加
|
|
||||
areaData.forEach(option => { |
|
||||
const optionElement = document.createElement('option'); |
|
||||
optionElement.textContent = option; |
|
||||
areaSelect.appendChild(optionElement); |
|
||||
|
|
||||
const optionElement_m = document.createElement('option'); |
|
||||
optionElement_m.textContent = option; |
|
||||
areaSelect_M.appendChild(optionElement_m); |
|
||||
|
|
||||
const optionElement_cc = document.createElement('option'); |
|
||||
optionElement_cc.textContent = option; |
|
||||
areaSelect_CC.appendChild(optionElement_cc); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
@ -1,303 +0,0 @@ |
|||||
let currentPage = 1; |
|
||||
const rowsPerPage = 10; |
|
||||
let modelData = []; |
|
||||
let modelData_bak = []; //用于关键字检索
|
|
||||
let currentEditingRow = null; |
|
||||
|
|
||||
|
|
||||
|
|
||||
//页面加载初始化
|
|
||||
document.addEventListener('DOMContentLoaded', function () { |
|
||||
fetchModelData(); //初始化通道管理页面元素数据
|
|
||||
//新增算法模态框---保存按钮
|
|
||||
document.getElementById('saveButton_model').addEventListener('click',function () { |
|
||||
addModel(); |
|
||||
}); |
|
||||
|
|
||||
//配置算法模态框--保存按钮
|
|
||||
document.getElementById('saveButton_config_model').addEventListener('click',function () { |
|
||||
post_configureModel(); |
|
||||
}); |
|
||||
|
|
||||
//升级算法模态框--保存按钮
|
|
||||
document.getElementById('saveButton_upmodel').addEventListener('click',function () { |
|
||||
post_modifyModel(); |
|
||||
}); |
|
||||
|
|
||||
//查询按钮
|
|
||||
document.getElementById('searchMButton').addEventListener('click',function () { |
|
||||
searchModel(); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
//获取算法列表数据,并更新页面
|
|
||||
async function fetchModelData() { |
|
||||
try{ |
|
||||
let response = await fetch('/api/model/list'); |
|
||||
if (!response.ok) { |
|
||||
throw new Error('Network response was not ok'); |
|
||||
} |
|
||||
modelData = await response.json(); |
|
||||
modelData_bak = modelData; |
|
||||
currentPage = 1; // 重置当前页为第一页
|
|
||||
renderTable(); //刷新表格
|
|
||||
renderPagination(); |
|
||||
}catch (error) { |
|
||||
console.error('Error fetching model data:', error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//刷新表单页面数据
|
|
||||
function renderTable() { |
|
||||
const tableBody = document.getElementById('table-body-model'); |
|
||||
tableBody.innerHTML = ''; //清空
|
|
||||
|
|
||||
const start = (currentPage - 1) * rowsPerPage; |
|
||||
const end = start + rowsPerPage; |
|
||||
const pageData = modelData.slice(start, end); |
|
||||
const surplus_count = rowsPerPage - pageData.length; |
|
||||
|
|
||||
|
|
||||
pageData.forEach((model) => { |
|
||||
const row = document.createElement('tr'); |
|
||||
row.innerHTML = ` |
|
||||
<td>${model.ID}</td> |
|
||||
<td>${model.name}</td> |
|
||||
<td>${model.version}</td> |
|
||||
<td>${model.duration_time}</td> |
|
||||
<td>${model.proportion}</td> |
|
||||
<td> |
|
||||
<button class="btn btn-primary btn-sm modify-btn">升级</button> |
|
||||
<button class="btn btn-secondary btn-sm algorithm-btn">配置</button> |
|
||||
<button class="btn btn-danger btn-sm delete-btn">删除</button> |
|
||||
</td> |
|
||||
`;
|
|
||||
tableBody.appendChild(row); |
|
||||
row.querySelector('.modify-btn').addEventListener('click', () => modifyModel(row)); |
|
||||
row.querySelector('.algorithm-btn').addEventListener('click', () => configureModel(row)); |
|
||||
row.querySelector('.delete-btn').addEventListener('click', () => deleteModel(row)); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
//刷新分页标签
|
|
||||
function renderPagination() { |
|
||||
const pagination = document.getElementById('pagination-model'); |
|
||||
pagination.innerHTML = ''; |
|
||||
|
|
||||
const totalPages = Math.ceil(modelData.length / rowsPerPage); |
|
||||
for (let i = 1; i <= totalPages; i++) { |
|
||||
const pageItem = document.createElement('li'); |
|
||||
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); |
|
||||
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; |
|
||||
pageItem.addEventListener('click', (event) => { |
|
||||
event.preventDefault(); |
|
||||
currentPage = i; |
|
||||
renderTable(); |
|
||||
renderPagination(); |
|
||||
}); |
|
||||
pagination.appendChild(pageItem); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//显示升级算法模态框
|
|
||||
function modifyModel(row){ |
|
||||
currentEditingRow = row; |
|
||||
model_name = row.cells[1].innerText; |
|
||||
version_name = row.cells[2].innerText; |
|
||||
|
|
||||
document.getElementById('update_mname_label').innerText = `算法名称: ${model_name}`; |
|
||||
document.getElementById('update_mversion_label').innerText = `当前版本: ${version_name}`; |
|
||||
$('#updateMM').modal('show'); |
|
||||
} |
|
||||
|
|
||||
//升级算法模态框--点击保存按钮
|
|
||||
function post_modifyModel(){ |
|
||||
mid = currentEditingRow.cells[0].innerText; |
|
||||
const btn = document.getElementById('saveButton_upmodel'); |
|
||||
const fileInput = document.getElementById('updateModelFile'); |
|
||||
const file = fileInput.files[0]; |
|
||||
if(file){ |
|
||||
btn.disabled = true; //不可点击
|
|
||||
const formData = new FormData(); |
|
||||
formData.append('file', file); |
|
||||
formData.append('mid', mid); |
|
||||
|
|
||||
fetch('/api/model/upgrade', { |
|
||||
method: 'POST', |
|
||||
body: formData, |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
alert(data.msg); |
|
||||
btn.disabled = false; |
|
||||
if(istatus == 1 ){ |
|
||||
fetchModelData(); |
|
||||
$('#updateMM').modal('hide'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('Error:', error); |
|
||||
alert('升级失败,请重试。'); |
|
||||
btn.disabled = false; |
|
||||
}); |
|
||||
} |
|
||||
else{ |
|
||||
alert('请选择升级包进行上传。'); |
|
||||
btn.disabled = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//显示配置算法模态框
|
|
||||
function configureModel(row){ |
|
||||
currentEditingRow = row; |
|
||||
model_name = row.cells[1].innerText; |
|
||||
duration_time = row.cells[3].innerText; |
|
||||
proportion = row.cells[4].innerText; |
|
||||
//设置模态框控件遍历
|
|
||||
document.getElementById('config_mname_label').innerText = `算法名称: ${model_name}`; |
|
||||
document.getElementById('duration_timeInput').value = duration_time; |
|
||||
document.getElementById('proportionInput').value = proportion; |
|
||||
$('#configMM').modal('show'); |
|
||||
} |
|
||||
|
|
||||
//配置算法模态框--点击保存按钮
|
|
||||
function post_configureModel(){ |
|
||||
mid = currentEditingRow.cells[0].innerText; |
|
||||
duration_time = parseInt(document.getElementById('duration_timeInput').value); |
|
||||
proportion = parseFloat(document.getElementById('proportionInput').value); |
|
||||
if(isNaN(duration_time) || isNaN(proportion) ){ |
|
||||
alert("请输入数字!"); |
|
||||
return; |
|
||||
} |
|
||||
if(proportion<=0 || proportion>=1){ |
|
||||
alert("占比阈值需要大于0,且小于1"); |
|
||||
return; |
|
||||
} |
|
||||
//提交数据
|
|
||||
const url = '/api/model/changecnf'; |
|
||||
const data = {"mid":mid,"duration_time":duration_time,"proportion":proportion}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
if(istatus === 0){ |
|
||||
alert(data.msg); |
|
||||
return; |
|
||||
} |
|
||||
else{ |
|
||||
//刷新列表
|
|
||||
fetchModelData(); |
|
||||
alert(data.msg); |
|
||||
$('#configMM').modal('hide'); |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
alert(`Error: ${error.message}`); |
|
||||
return; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
//删除算法记录
|
|
||||
function deleteModel(row){ |
|
||||
if (confirm('确定删除此算法吗?')) { |
|
||||
mid = row.cells[0].innerText; |
|
||||
const url = '/api/model/del'; |
|
||||
const data = {"mid":mid}; |
|
||||
// 发送 POST 请求
|
|
||||
fetch(url, { |
|
||||
method: 'POST', // 指定请求方法为 POST
|
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' // 设置请求头,告诉服务器请求体的数据类型为 JSON
|
|
||||
}, |
|
||||
body: JSON.stringify(data) // 将 JavaScript 对象转换为 JSON 字符串
|
|
||||
}) |
|
||||
.then(response => response.json()) // 将响应解析为 JSON
|
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
if(istatus === 0){ |
|
||||
alert(data.msg); |
|
||||
return; |
|
||||
} |
|
||||
else{ |
|
||||
//刷新列表
|
|
||||
row.remove(); |
|
||||
alert("删除算法成功!"); |
|
||||
} |
|
||||
}) |
|
||||
.catch((error) => { |
|
||||
alert(`Error: ${error.message}`); |
|
||||
return; |
|
||||
}); |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//新增算法--保存按钮
|
|
||||
function addModel(){ |
|
||||
const btn = document.getElementById('saveButton_model'); |
|
||||
const fileInput = document.getElementById('uploadModelFile'); |
|
||||
const file = fileInput.files[0]; |
|
||||
const mName = document.getElementById('MNameInput').value; |
|
||||
|
|
||||
if (file && mName) { |
|
||||
btn.disabled = true; //不可点击
|
|
||||
const formData = new FormData(); |
|
||||
formData.append('file', file); |
|
||||
formData.append('mName', mName); |
|
||||
|
|
||||
fetch('/api/model/add', { |
|
||||
method: 'POST', |
|
||||
body: formData, |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
const istatus = data.status; |
|
||||
alert(data.msg); |
|
||||
btn.disabled = false; |
|
||||
if(istatus == 1 ){ |
|
||||
fetchModelData(); |
|
||||
$('#addMM').modal('hide'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('Error:', error); |
|
||||
alert('上传失败,请重试。'); |
|
||||
btn.disabled = false; |
|
||||
}); |
|
||||
} else { |
|
||||
alert('请填写算法名称并选择一个升级包进行上传。'); |
|
||||
btn.disabled = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//关键字检索
|
|
||||
function searchModel(){ |
|
||||
try { |
|
||||
const modelName = document.getElementById('modelNameInput').value; |
|
||||
if(modelName===""){ |
|
||||
modelData = modelData_bak; |
|
||||
} |
|
||||
else{ |
|
||||
modelData = []; |
|
||||
modelData_bak.forEach((model) => { |
|
||||
if(model.name.includes(modelName)){ |
|
||||
modelData.push(model); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
// 渲染表格和分页控件
|
|
||||
currentPage = 1; // 重置当前页为第一页
|
|
||||
renderTable(); |
|
||||
renderPagination(); |
|
||||
} catch (error) { |
|
||||
console.error('Error performing search:', error); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,82 @@ |
|||||
|
function getWsUrl() { |
||||
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws"; |
||||
|
const host = window.location.host; // 包含主机和端口(如 localhost:5000)
|
||||
|
const path = "/api/ws"; // 你的 WebSocket 路由
|
||||
|
return `${protocol}://${host}${path}`; |
||||
|
} |
||||
|
|
||||
|
function initWebSocket() { |
||||
|
// 替换成你实际的 WebSocket 地址,例如 ws://localhost:5000/ws
|
||||
|
wsUrl = getWsUrl(); |
||||
|
const ws = new WebSocket(wsUrl); |
||||
|
// 设置接收二进制数据为 ArrayBuffer
|
||||
|
ws.binaryType = "arraybuffer"; |
||||
|
|
||||
|
ws.onopen = function() { |
||||
|
console.log("WebSocket 已连接"); |
||||
|
// 发送登录数据包,假设格式为 JSON(后端可以根据需要解析此包) --暂时默认0只有一个
|
||||
|
ws.send(JSON.stringify({ user_id: 0 })); |
||||
|
}; |
||||
|
|
||||
|
ws.onmessage = function(event) { |
||||
|
// 解析收到的二进制数据
|
||||
|
const buffer = event.data; |
||||
|
const dataView = new DataView(buffer); |
||||
|
// 读取前四个字节,验证魔数 "TFTF"
|
||||
|
let magic = ""; |
||||
|
for (let i = 0; i < 4; i++) { |
||||
|
magic += String.fromCharCode(dataView.getUint8(i)); |
||||
|
} |
||||
|
if (magic !== "TFTF") { |
||||
|
console.error("收到非法格式数据:", magic); |
||||
|
return; |
||||
|
} |
||||
|
// 读取数据头:接下来的 8 字节分别为 idata_type 和 idata_len(均为 32 位无符号整数,假设大端)
|
||||
|
const idata_type = dataView.getUint32(4, false); |
||||
|
const idata_len = dataView.getUint32(8, false); |
||||
|
|
||||
|
// 数据体从偏移量 12 开始,长度为 idata_len
|
||||
|
const bodyBytes = new Uint8Array(buffer, 12, idata_len); |
||||
|
const decoder = new TextDecoder("utf-8"); |
||||
|
const bodyText = decoder.decode(bodyBytes); |
||||
|
// 假设数据体为 JSON 字符串
|
||||
|
let bodyData; |
||||
|
try { |
||||
|
bodyData = JSON.parse(bodyText); |
||||
|
} catch (error) { |
||||
|
console.error("解析数据体出错:", error); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 针对 idata_type 为 1 的处理:更新节点
|
||||
|
if(idata_type === 0){ |
||||
|
console.log(bodyData); |
||||
|
} |
||||
|
else if (idata_type === 1) {//type-1为更新节点的node_workstatus
|
||||
|
// bodyData 应该包含 node_path 和 node_workstatus 两个字段
|
||||
|
updateTreeNode(bodyData.node_path, bodyData.node_workstatus);//node_tree.js
|
||||
|
} |
||||
|
else if(idata_type === 2){ |
||||
|
if(cur_task_id === 0){return;} |
||||
|
// 清空选中状态
|
||||
|
selectedNodeData = null; |
||||
|
//刷新界面内容
|
||||
|
update_select_node_data_show("-","-","-","-","-",false) |
||||
|
// 重新加载节点树数据
|
||||
|
loadNodeTree(cur_task_id); |
||||
|
} |
||||
|
else { |
||||
|
console.error("未知的数据类型"); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
ws.onerror = function(error) { |
||||
|
console.error("WebSocket 错误:", error); |
||||
|
}; |
||||
|
|
||||
|
ws.onclose = function() { |
||||
|
console.log("WebSocket 连接关闭"); |
||||
|
}; |
||||
|
|
||||
|
return ws; |
||||
|
} |
@ -0,0 +1,930 @@ |
|||||
|
// 全局变量,用于保存当前选中的节点数据
|
||||
|
let selectedNodeData = null; |
||||
|
|
||||
|
/** |
||||
|
* 根据节点数据递归生成树形结构(返回 <li> 元素) |
||||
|
* 假设节点数据格式: |
||||
|
* { |
||||
|
* "node_name":node.name, |
||||
|
* "node_path":node.path, |
||||
|
* "node_status":node.status, |
||||
|
* "node_bwork":node.bwork, |
||||
|
* "node_vultype":node.vul_type, |
||||
|
* "node_vulgrade":node.vul_grade, |
||||
|
* children: [ { ... }, { ... } ] |
||||
|
* } |
||||
|
*/ |
||||
|
function generateTreeHTML(nodeData) { |
||||
|
const li = document.createElement("li"); |
||||
|
const nodeSpan = document.createElement("span"); |
||||
|
nodeSpan.className = "tree-node"; |
||||
|
//设置data属性
|
||||
|
nodeSpan.setAttribute("data-node_name", nodeData.node_name); |
||||
|
nodeSpan.setAttribute("data-node_path", nodeData.node_path); |
||||
|
nodeSpan.setAttribute("data-node_status", nodeData.node_status); |
||||
|
nodeSpan.setAttribute("data-node_bwork", nodeData.node_bwork); |
||||
|
nodeSpan.setAttribute("data-node_vultype", nodeData.node_vultype); |
||||
|
nodeSpan.setAttribute("data-node_vulgrade", nodeData.node_vulgrade || ""); |
||||
|
nodeSpan.setAttribute("data-node_workstatus",nodeData.node_workstatus); |
||||
|
if(nodeData.node_workstatus ===0){ |
||||
|
nodeSpan.classList.add("no-work"); |
||||
|
}else { |
||||
|
nodeSpan.classList.remove("no-work"); |
||||
|
} |
||||
|
// 根据漏洞级别添加样式
|
||||
|
if (nodeData.node_vulgrade) { |
||||
|
nodeSpan.classList.remove("no-work"); |
||||
|
if (nodeData.node_vulgrade === "低危") { |
||||
|
nodeSpan.classList.add("vul-low"); |
||||
|
} else if (nodeData.node_vulgrade === "中危") { |
||||
|
nodeSpan.classList.add("vul-medium"); |
||||
|
} else if (nodeData.node_vulgrade === "高危") { |
||||
|
nodeSpan.classList.add("vul-high"); |
||||
|
} |
||||
|
} |
||||
|
// 创建容器用于存放切换图标与文本
|
||||
|
const container = document.createElement("div"); |
||||
|
container.className = "node-container"; |
||||
|
// 如果有子节点,则添加切换图标
|
||||
|
if (nodeData.children && nodeData.children.length > 0) { |
||||
|
const toggleIcon = document.createElement("span"); |
||||
|
toggleIcon.className = "toggle-icon"; |
||||
|
toggleIcon.textContent = "-"; // 默认展开时显示“-”
|
||||
|
container.appendChild(toggleIcon); |
||||
|
} |
||||
|
//节点文本
|
||||
|
const textSpan = document.createElement("span"); |
||||
|
textSpan.className = "node-text"; |
||||
|
textSpan.textContent = nodeData.node_name; |
||||
|
container.appendChild(textSpan); |
||||
|
nodeSpan.appendChild(container); |
||||
|
li.appendChild(nodeSpan); |
||||
|
//如果存在子节点,递归生成子节点列表
|
||||
|
if (nodeData.children && nodeData.children.length > 0) { |
||||
|
const ul = document.createElement("ul"); |
||||
|
nodeData.children.forEach((child) => { |
||||
|
ul.appendChild(generateTreeHTML(child)); |
||||
|
}); |
||||
|
li.appendChild(ul); |
||||
|
} |
||||
|
return li; |
||||
|
} |
||||
|
|
||||
|
// 绑定所有节点的点击事件
|
||||
|
function bindTreeNodeEvents() { |
||||
|
document.querySelectorAll(".tree-node").forEach((el) => { |
||||
|
el.addEventListener("click", (event) => { |
||||
|
// 阻止事件冒泡,避免点击时展开折叠影响
|
||||
|
event.stopPropagation(); |
||||
|
// 清除之前选中的节点样式
|
||||
|
document |
||||
|
.querySelectorAll(".tree-node.selected") |
||||
|
.forEach((node) => node.classList.remove("selected")); |
||||
|
// 当前节点标记为选中
|
||||
|
el.classList.add("selected"); |
||||
|
// 读取 data 属性更新右侧显示
|
||||
|
const nodeName = el.getAttribute("data-node_name"); |
||||
|
const status = el.getAttribute("data-node_status"); |
||||
|
const nodepath = el.getAttribute("data-node_path"); |
||||
|
const nodebwork = el.getAttribute("data-node_bwork"); |
||||
|
const vulType = el.getAttribute("data-node_vultype"); |
||||
|
const vulLevel = el.getAttribute("data-node_vulgrade"); |
||||
|
const workstatus = el.getAttribute("data-node_workstatus"); |
||||
|
//selectedNodeData = { nodeName, status, vulType, vulLevel,nodepath,nodebwork };
|
||||
|
// 示例中默认填充
|
||||
|
selectedNodeData = { |
||||
|
node_name: nodeName, |
||||
|
node_path: nodepath, |
||||
|
status: status, |
||||
|
node_bwork: nodebwork, |
||||
|
vul_type: vulType, |
||||
|
vul_grade: vulLevel || "-", |
||||
|
workstatus: workstatus |
||||
|
}; |
||||
|
//刷新界面内容
|
||||
|
update_select_node_data_show(nodeName,status,vulType,vulLevel,workstatus,nodebwork) |
||||
|
}); |
||||
|
// 双击事件:展开/收缩子节点区域
|
||||
|
el.addEventListener("dblclick", (event) => { |
||||
|
event.stopPropagation(); |
||||
|
// 找到该节点下的 <ul> 子节点列表
|
||||
|
const parentLi = el.parentElement; |
||||
|
const childUl = parentLi.querySelector("ul"); |
||||
|
if (childUl) { |
||||
|
// 切换 collapsed 类,控制 display
|
||||
|
childUl.classList.toggle("collapsed"); |
||||
|
// 更新切换图标
|
||||
|
const toggleIcon = el.querySelector(".toggle-icon"); |
||||
|
if (toggleIcon) { |
||||
|
toggleIcon.textContent = childUl.classList.contains("collapsed") |
||||
|
? "+" |
||||
|
: "-"; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 动态加载节点树数据
|
||||
|
async function loadNodeTree(task_id) { |
||||
|
// 清空选中状态
|
||||
|
selectedNodeData = null; |
||||
|
//刷新界面内容
|
||||
|
update_select_node_data_show("-","-","-","-","-",false) |
||||
|
try { |
||||
|
const res = await fetch("/api/task/gettree", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ task_id }), //task_id:task_id
|
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
const treeData = data.tree; |
||||
|
if (!treeData) { |
||||
|
document.getElementById("treeContent").innerHTML = |
||||
|
"<p>无节点数据</p>"; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 创建一个 <ul> 作为树的根容器
|
||||
|
const ul = document.createElement("ul"); |
||||
|
ul.className = "tree-root-ul"; |
||||
|
ul.appendChild(generateTreeHTML(treeData)); |
||||
|
// 替换节点树容器的内容
|
||||
|
const container = document.getElementById("treeContent"); |
||||
|
container.innerHTML = ""; |
||||
|
container.appendChild(ul); |
||||
|
// 绑定节点点击事件
|
||||
|
bindTreeNodeEvents(); |
||||
|
} catch (error) { |
||||
|
console.error("加载节点树失败:", error); |
||||
|
document.getElementById("treeContent").innerHTML = "<p>加载节点树失败</p>"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getWorkStatus_Str(workstatus){ |
||||
|
strworkstatus = "" |
||||
|
switch (workstatus){ |
||||
|
case 0: |
||||
|
strworkstatus = "无待执行任务"; |
||||
|
break; |
||||
|
case 1: |
||||
|
strworkstatus = "待执行指令中"; |
||||
|
break; |
||||
|
case 2: |
||||
|
strworkstatus = "指令执行中"; |
||||
|
break; |
||||
|
case 3: |
||||
|
strworkstatus = "待提交llm中"; |
||||
|
break; |
||||
|
case 4: |
||||
|
strworkstatus = "提交llm中"; |
||||
|
break; |
||||
|
default: |
||||
|
strworkstatus = "-" |
||||
|
} |
||||
|
return strworkstatus |
||||
|
} |
||||
|
|
||||
|
//根据web端过来的数据,更新节点的工作状态
|
||||
|
function updateTreeNode(node_path, node_workstatus) { |
||||
|
// 根据 node_path 查找对应节点(假设每个 .tree-node 上设置了 data-node_path 属性)
|
||||
|
const nodeEl = document.querySelector(`.tree-node[data-node_path="${node_path}"]`); |
||||
|
if (nodeEl) { |
||||
|
// 更新 DOM 属性(属性值均为字符串)
|
||||
|
nodeEl.setAttribute("data-node_workstatus", node_workstatus); |
||||
|
//判断是否需要更新界面
|
||||
|
if(selectedNodeData){ |
||||
|
if(node_path === selectedNodeData.node_path){ //只有是当前选中节点才更新数据
|
||||
|
selectedNodeData.workstatus = node_workstatus; |
||||
|
strnew = getWorkStatus_Str(node_workstatus); |
||||
|
document.getElementById("node_workstatus").textContent = strnew; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
console.warn(`未找到节点 ${node_path}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//刷新节点的数据显示
|
||||
|
function update_select_node_data_show(nodeName,testStatus,vulType,vulLevel,workStatus,nodebwork){ |
||||
|
document.getElementById("nodeName").textContent = nodeName; |
||||
|
document.getElementById("testStatus").textContent = testStatus; |
||||
|
document.getElementById("node_vulType").textContent = vulType; |
||||
|
document.getElementById("node_vulLevel").textContent = vulLevel; |
||||
|
str_workStatus = getWorkStatus_Str(Number(workStatus)); |
||||
|
document.getElementById("node_workstatus").textContent = str_workStatus; |
||||
|
if(nodebwork==="true"){ |
||||
|
document.getElementById("node_bwork").textContent = "执行中"; |
||||
|
document.getElementById("btnToggleStatus").textContent = "暂停"; |
||||
|
}else { |
||||
|
document.getElementById("node_bwork").textContent = "暂停中"; |
||||
|
document.getElementById("btnToggleStatus").textContent = "继续"; |
||||
|
} |
||||
|
setNodeBtnStatus(); |
||||
|
} |
||||
|
|
||||
|
//节点按钮的状态控制
|
||||
|
function setNodeBtnStatus(){ |
||||
|
const btn_TS = document.getElementById("btnToggleStatus"); |
||||
|
const btn_NodeStep = document.getElementById("btnNodeStep"); |
||||
|
const btn_VI = document.getElementById("btnViewInstr"); |
||||
|
const btn_VM = document.getElementById("btnViewMsg"); |
||||
|
const btn_AI = document.getElementById("btnAddInfo"); |
||||
|
const btn_AC = document.getElementById("btnAddChild"); |
||||
|
if(!selectedNodeData){ |
||||
|
//没有选择node,按钮全部置不可用
|
||||
|
btn_TS.disabled = true; |
||||
|
btn_TS.classList.add("disabled-btn"); |
||||
|
btn_NodeStep.disabled = true; |
||||
|
btn_NodeStep.classList.add("disabled-btn"); |
||||
|
btn_VI.disabled = true; |
||||
|
btn_VI.classList.add("disabled-btn"); |
||||
|
btn_VM.disabled = true; |
||||
|
btn_VM.classList.add("disabled-btn"); |
||||
|
btn_AI.disabled = true; |
||||
|
btn_AI.classList.add("disabled-btn"); |
||||
|
btn_AC.disabled = true; |
||||
|
btn_AC.classList.add("disabled-btn"); |
||||
|
} |
||||
|
else{ |
||||
|
//5个可用
|
||||
|
btn_TS.disabled = false; |
||||
|
btn_TS.classList.remove("disabled-btn"); |
||||
|
btn_VI.disabled = false; |
||||
|
btn_VI.classList.remove("disabled-btn"); |
||||
|
btn_VM.disabled = false; |
||||
|
btn_VM.classList.remove("disabled-btn"); |
||||
|
btn_AI.disabled = false; |
||||
|
btn_AI.classList.remove("disabled-btn"); |
||||
|
btn_AC.disabled = false; |
||||
|
btn_AC.classList.remove("disabled-btn"); |
||||
|
if(cur_task.taskStatus === 1 && cur_task.workType === 0 && selectedNodeData.node_bwork==="true"){ |
||||
|
btn_NodeStep.disabled = false; |
||||
|
btn_NodeStep.classList.remove("disabled-btn"); |
||||
|
} |
||||
|
else{ |
||||
|
btn_NodeStep.disabled = true; |
||||
|
btn_NodeStep.classList.add("disabled-btn"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 刷新按钮事件绑定
|
||||
|
document.getElementById("btnRefresh").addEventListener("click", () => { |
||||
|
// 重新加载节点树数据
|
||||
|
loadNodeTree(cur_task_id); |
||||
|
}); |
||||
|
|
||||
|
// 按钮事件:当未选中节点时提示
|
||||
|
function checkSelectedNode() { |
||||
|
if (!selectedNodeData) { |
||||
|
alert("请先选择节点"); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
//节点-暂停/继续,bwork控制
|
||||
|
document.getElementById("btnToggleStatus").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
// bwork的状态已后端状态为准,只做切换
|
||||
|
update_node_bwork(cur_task_id,selectedNodeData.node_path); |
||||
|
}); |
||||
|
async function update_node_bwork(task_id,node_path){ |
||||
|
try { |
||||
|
const res = await fetch("/api/task/nodecontrol", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ task_id,node_path }), //task_id:task_id
|
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
//修改成功
|
||||
|
const data = await res.json(); |
||||
|
const newbwork = data.newbwork; |
||||
|
//更新数据
|
||||
|
const selectedEl = document.querySelector(".tree-node.selected"); |
||||
|
if (selectedEl) { |
||||
|
selectedEl.setAttribute("data-node_bwork", newbwork); |
||||
|
selectedNodeData.node_bwork = newbwork; |
||||
|
} |
||||
|
//刷新界面
|
||||
|
const btn_NodeStep = document.getElementById("btnNodeStep"); |
||||
|
if(newbwork){ |
||||
|
document.getElementById("node_bwork").textContent ="执行中"; |
||||
|
document.getElementById("btnToggleStatus").textContent = "暂停"; |
||||
|
if(cur_task.taskStatus === 1 && cur_task.workType === 0){ |
||||
|
btn_NodeStep.disabled = false; |
||||
|
btn_NodeStep.classList.remove("disabled-btn"); |
||||
|
} |
||||
|
}else { |
||||
|
document.getElementById("node_bwork").textContent = "暂停中"; |
||||
|
document.getElementById("btnToggleStatus").textContent = "继续"; |
||||
|
btn_NodeStep.disabled = true; |
||||
|
btn_NodeStep.classList.add("disabled-btn"); |
||||
|
} |
||||
|
}catch (error) { |
||||
|
alert("修改节点的bwork失败:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//节点-单步工作
|
||||
|
document.getElementById("btnNodeStep").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
node_one_step(cur_task_id,selectedNodeData.node_path); |
||||
|
}); |
||||
|
async function node_one_step(task_id,node_path){ |
||||
|
try { |
||||
|
const res = await fetch("/api/task/nodestep", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ task_id,node_path }), //task_id:task_id
|
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
//修改成功
|
||||
|
const data = await res.json(); |
||||
|
const bsuccess = data.bsuccess; |
||||
|
if(bsuccess){ |
||||
|
alert("该节点任务已提交,请稍候查看执行结果!") |
||||
|
} |
||||
|
else{ |
||||
|
error = data.erroe; |
||||
|
alert("该节点单步失败!",error) |
||||
|
} |
||||
|
|
||||
|
}catch (error) { |
||||
|
alert("该节点单步失败,请联系管理员!", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//----------------------查看指令modal----------------------------
|
||||
|
let doneInstrs = []; // 已执行指令的所有数据
|
||||
|
let todoInstrs = []; // 待执行指令的所有数据
|
||||
|
let donePage = 1; // 已执行指令当前页
|
||||
|
let todoPage = 1; // 待执行指令当前页
|
||||
|
|
||||
|
document.getElementById("msgModal").addEventListener("hidden.bs.modal", () => { |
||||
|
document.activeElement.blur(); // 清除当前焦点
|
||||
|
}); |
||||
|
|
||||
|
document.getElementById("btnViewInstr").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
openInstrModal() |
||||
|
}); |
||||
|
// 打开对话框函数
|
||||
|
function openInstrModal() { |
||||
|
const modalEl = document.getElementById("instrModal"); |
||||
|
// 假设用 Bootstrap 5 的 Modal 组件
|
||||
|
const instrModal = new bootstrap.Modal(modalEl, {keyboard: false}); |
||||
|
// 在打开 modal 时,先更新提示内容,将 loadingMsg 显示“请稍后,数据获取中…”
|
||||
|
const loadingMsg = document.getElementById("loadingMsg"); |
||||
|
if (loadingMsg) { |
||||
|
loadingMsg.textContent = "请稍后,数据获取中..."; |
||||
|
} |
||||
|
// 显示对话框
|
||||
|
instrModal.show(); |
||||
|
// 加载指令数据
|
||||
|
loadInstrData(); |
||||
|
} |
||||
|
|
||||
|
// 调用后端接口,获取指令数据
|
||||
|
async function loadInstrData() { |
||||
|
task_id = cur_task_id; |
||||
|
node_path = selectedNodeData.node_path; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/nodegetinstr", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({task_id,node_path}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
// 数据获取成功后,清除加载提示
|
||||
|
const loadingMsg = document.getElementById("loadingMsg"); |
||||
|
if (loadingMsg) { |
||||
|
loadingMsg.style.display = "none"; // 或者清空其 innerHTML
|
||||
|
} |
||||
|
doneInstrs = data.doneInstrs || []; |
||||
|
todoInstrs = data.todoInstrs || []; |
||||
|
donePage = 1; |
||||
|
todoPage = 1; |
||||
|
renderDoneInstrTable(donePage); |
||||
|
renderTodoInstrTable(todoPage); |
||||
|
} catch (error) { |
||||
|
console.error("加载指令数据异常:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 渲染已执行指令表格
|
||||
|
function renderDoneInstrTable(page) { |
||||
|
const tbody = document.getElementById("doneInstrTbody"); |
||||
|
// 计算起始索引
|
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = doneInstrs.slice(startIndex, endIndex); |
||||
|
//select instruction,start_time,result from task_result where task_id=%s and node_path=%s;
|
||||
|
tbody.innerHTML = ""; |
||||
|
// 插入行
|
||||
|
pageData.forEach((item, i) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
|
||||
|
// 第一列:序号
|
||||
|
const tdIndex = document.createElement("td"); |
||||
|
tdIndex.textContent = startIndex + i + 1; |
||||
|
tr.appendChild(tdIndex); |
||||
|
|
||||
|
// 第二列:指令内容
|
||||
|
const tdInstr = document.createElement("td"); |
||||
|
tdInstr.textContent = item[0]; |
||||
|
tr.appendChild(tdInstr); |
||||
|
|
||||
|
// 第三列:开始时间(如果没有则显示空字符串)
|
||||
|
const tdStartTime = document.createElement("td"); |
||||
|
tdStartTime.textContent = item[1] || ""; |
||||
|
tr.appendChild(tdStartTime); |
||||
|
|
||||
|
// 第四列:执行结果
|
||||
|
const tdResult = document.createElement("td"); |
||||
|
tdResult.textContent = item[2] || ""; |
||||
|
tr.appendChild(tdResult); |
||||
|
|
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
|
||||
|
// 若不足 10 行,补空行
|
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
tr.innerHTML = ` |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
`;
|
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 渲染待执行指令表格
|
||||
|
function renderTodoInstrTable(page) { |
||||
|
const tbody = document.getElementById("todoInstrTbody"); |
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = todoInstrs.slice(startIndex, endIndex); |
||||
|
|
||||
|
tbody.innerHTML = ""; |
||||
|
pageData.forEach((item, i) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
const idx = startIndex + i + 1; |
||||
|
// 第一列:序号
|
||||
|
const tdIndex = document.createElement("td"); |
||||
|
tdIndex.textContent = idx; |
||||
|
tr.appendChild(tdIndex); |
||||
|
|
||||
|
// 第二列:指令文本内容(直接使用 textContent)
|
||||
|
const tdItem = document.createElement("td"); |
||||
|
tdItem.textContent = item; // 使用 textContent 避免 HTML 解析
|
||||
|
tr.appendChild(tdItem); |
||||
|
|
||||
|
// 第三列:复制和删除按钮
|
||||
|
const tdAction = document.createElement("td"); |
||||
|
// const btn_cp = document.createElement("button");
|
||||
|
// btn_cp.className = "btn btn-primary btn-sm";
|
||||
|
// btn_cp.textContent = "复制";
|
||||
|
// btn_cp.style.marginRight = "2px"; // 设置间隔
|
||||
|
// btn_cp.onclick = () => confirmCopyTodoInstr(idx - 1);
|
||||
|
// tdAction.appendChild(btn_cp);
|
||||
|
const btn = document.createElement("button"); |
||||
|
btn.className = "btn btn-danger btn-sm"; |
||||
|
btn.textContent = "删除"; |
||||
|
btn.onclick = () => confirmDeleteTodoInstr(idx - 1); |
||||
|
tdAction.appendChild(btn); |
||||
|
tr.appendChild(tdAction); |
||||
|
|
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
|
||||
|
// 补空行
|
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
tr.innerHTML = ` |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
`;
|
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function confirmCopyTodoInstr(idx) { |
||||
|
// 从全局数组 todoInstrs 中获取指令文本
|
||||
|
const instruction = todoInstrs[idx]; |
||||
|
|
||||
|
if (navigator.clipboard && navigator.clipboard.writeText) { |
||||
|
navigator.clipboard.writeText(instruction) |
||||
|
.then(() => { |
||||
|
alert("已复制: " + instruction); |
||||
|
}) |
||||
|
.catch((err) => { |
||||
|
console.error("使用 Clipboard API 复制失败:", err); |
||||
|
fallbackCopyTextToClipboard(instruction); |
||||
|
}); |
||||
|
} else { |
||||
|
// 如果 clipboard API 不可用,则回退使用 execCommand 方法
|
||||
|
fallbackCopyTextToClipboard(instruction); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function fallbackCopyTextToClipboard(text) { |
||||
|
// 创建 textarea 元素
|
||||
|
const textArea = document.createElement("textarea"); |
||||
|
textArea.value = text; |
||||
|
|
||||
|
// 使用 CSS 样式使其不可见,同时保证能够获得焦点
|
||||
|
textArea.style.position = "fixed"; // 避免页面滚动
|
||||
|
textArea.style.top = "0"; |
||||
|
textArea.style.left = "0"; |
||||
|
textArea.style.width = "2em"; |
||||
|
textArea.style.height = "2em"; |
||||
|
textArea.style.padding = "0"; |
||||
|
textArea.style.border = "none"; |
||||
|
textArea.style.outline = "none"; |
||||
|
textArea.style.boxShadow = "none"; |
||||
|
textArea.style.background = "transparent"; |
||||
|
|
||||
|
document.body.appendChild(textArea); |
||||
|
textArea.focus(); |
||||
|
textArea.select(); |
||||
|
|
||||
|
try { |
||||
|
const successful = document.execCommand('copy'); |
||||
|
if (successful) { |
||||
|
alert("已复制: " + text); |
||||
|
} else { |
||||
|
alert("复制失败,请手动复制!"); |
||||
|
} |
||||
|
} catch (err) { |
||||
|
console.error("Fallback: 无法复制", err); |
||||
|
alert("复制失败,请手动复制!"); |
||||
|
} |
||||
|
document.body.removeChild(textArea); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// 删除待执行指令,先确认
|
||||
|
function confirmDeleteTodoInstr(arrIndex) { |
||||
|
if (!confirm("确认删除该条待执行指令?")) return; |
||||
|
// arrIndex 在当前分页中的索引
|
||||
|
// 先算出全局索引
|
||||
|
const realIndex = (todoPage - 1) * pageSize + arrIndex; |
||||
|
const item = todoInstrs[realIndex]; |
||||
|
// 调用后端删除接口
|
||||
|
deleteTodoInstr(item); |
||||
|
} |
||||
|
|
||||
|
// 调用后端接口删除
|
||||
|
async function deleteTodoInstr(item) { |
||||
|
task_id = cur_task_id; |
||||
|
node_path = selectedNodeData.node_path; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/delnodeinstr", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ task_id,node_path,item }) |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
alert(data.error || "删除失败"); |
||||
|
return; |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
if(data.bsuccess){ |
||||
|
// 删除成功,更新本地数据
|
||||
|
const idx = todoInstrs.findIndex(x => x.id === item.id); |
||||
|
if (idx !== -1) { |
||||
|
todoInstrs.splice(idx, 1); |
||||
|
} |
||||
|
// 重新渲染
|
||||
|
renderTodoInstrTable(todoPage); |
||||
|
//0需要更新work_status为无待执行任务的状态
|
||||
|
if(todoInstrs.length === 0){ |
||||
|
updateTreeNode(node_path, 0); |
||||
|
} |
||||
|
} |
||||
|
else{ |
||||
|
alert("指令删除失败",data.error) |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("删除指令异常:", error); |
||||
|
alert("删除指令异常,请联系管理员!"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 分页事件
|
||||
|
document.getElementById("doneInstrPrev").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
if (donePage > 1) { |
||||
|
donePage--; |
||||
|
renderDoneInstrTable(donePage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("doneInstrNext").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
const maxPage = Math.ceil(doneInstrs.length / pageSize); |
||||
|
if (donePage < maxPage) { |
||||
|
donePage++; |
||||
|
renderDoneInstrTable(donePage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("todoInstrPrev").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
if (todoPage > 1) { |
||||
|
todoPage--; |
||||
|
renderTodoInstrTable(todoPage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("todoInstrNext").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
const maxPage = Math.ceil(todoInstrs.length / pageSize); |
||||
|
if (todoPage < maxPage) { |
||||
|
todoPage++; |
||||
|
renderTodoInstrTable(todoPage); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 导出当前页数据
|
||||
|
document.getElementById("btnExport").addEventListener("click", () => { |
||||
|
// 判断当前是哪个 Tab
|
||||
|
const activeTab = document.querySelector("#instrTab button.nav-link.active"); |
||||
|
if (activeTab.id === "doneInstrTab") { |
||||
|
exportCurrentPage(doneInstrs, donePage, ["序号", "执行指令", "执行时间", "执行结果"]); |
||||
|
} else { |
||||
|
exportCurrentPage(todoInstrs, todoPage, ["序号", "待执行指令"]); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
function exportCurrentPage(dataArr, page, headerArr) { |
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = dataArr.slice(startIndex, endIndex); |
||||
|
|
||||
|
// 在 CSV 的开头加上 BOM,用于 Excel 识别 UTF-8 编码
|
||||
|
let csvContent = "\uFEFF" + headerArr.join(",") + "\n"; |
||||
|
pageData.forEach((item, i) => { |
||||
|
const rowIndex = startIndex + i + 1; |
||||
|
if (headerArr.length === 4) { |
||||
|
// 已执行:序号,执行指令,执行时间,执行结果
|
||||
|
csvContent += rowIndex + "," + |
||||
|
(item.command || "") + "," + |
||||
|
(item.execTime || "") + "," + |
||||
|
(item.result || "") + "\n"; |
||||
|
} else { |
||||
|
// 待执行:序号,待执行指令
|
||||
|
csvContent += rowIndex + "," + (item.command || "") + "\n"; |
||||
|
} |
||||
|
}); |
||||
|
// 如果不足 pageSize 行,补足空行(根据列数进行适当补全)
|
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
// 根据 headerArr.length 来设置空行的格式
|
||||
|
if (headerArr.length === 4) { |
||||
|
csvContent += ",,,\n"; |
||||
|
} else { |
||||
|
csvContent += ",\n"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
const link = document.createElement("a"); |
||||
|
link.href = url; |
||||
|
link.download = "指令导出.csv"; |
||||
|
link.click(); |
||||
|
URL.revokeObjectURL(url); |
||||
|
} |
||||
|
|
||||
|
//---------------------查看MSGmodal------------------------------
|
||||
|
document.getElementById("msgModal").addEventListener("hidden.bs.modal", () => { |
||||
|
document.activeElement.blur(); // 清除当前焦点
|
||||
|
}); |
||||
|
|
||||
|
document.getElementById("btnViewMsg").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
openMsgModal(); |
||||
|
}); |
||||
|
|
||||
|
let submittedMsgs = []; // 存储已提交的 MSG 数据数组
|
||||
|
let pendingMsg = {}; // 存储待提交的 MSG 数据
|
||||
|
let submittedPage = 1; |
||||
|
|
||||
|
function openMsgModal(){ |
||||
|
// 显示 Modal(使用 Bootstrap 5 Modal)
|
||||
|
const msgModal = new bootstrap.Modal(document.getElementById("msgModal"), { keyboard: false }); |
||||
|
msgModal.show(); |
||||
|
|
||||
|
// 加载数据:调用后端接口 /api/task/getnodeinstr 或其他接口获取数据
|
||||
|
// 这里仅作示例使用模拟数据
|
||||
|
loadMsgData(); |
||||
|
} |
||||
|
|
||||
|
async function loadMsgData(){ |
||||
|
task_id = cur_task_id; |
||||
|
node_path = selectedNodeData.node_path; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/nodegetmsg", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({task_id,node_path}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
submittedMsgs = data.submitted || []; |
||||
|
pendingMsg = data.pending || {}; //one_llm = {'llm_type': llm_type, 'result': str_res}
|
||||
|
|
||||
|
submittedPage = 1; |
||||
|
renderSubmittedTable(submittedPage); |
||||
|
// 填充待提交区域 ---
|
||||
|
document.getElementById("llmtype").value = pendingMsg.llm_type || "0"; |
||||
|
document.getElementById("pendingContent").value = pendingMsg.result || ""; |
||||
|
} catch (error) { |
||||
|
console.error("加载Msg数据异常:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function renderSubmittedTable(page){ |
||||
|
const tbody = document.getElementById("submittedTbody"); |
||||
|
const start = (page - 1) * pageSize; |
||||
|
const end = start + pageSize; |
||||
|
const pageData = submittedMsgs.slice(start, end); |
||||
|
tbody.innerHTML = ""; |
||||
|
pageData.forEach((item, index) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
|
||||
|
// 第一列:序号
|
||||
|
const tdIndex = document.createElement("td"); |
||||
|
tdIndex.textContent = start + index + 1; |
||||
|
tr.appendChild(tdIndex); |
||||
|
|
||||
|
// 第二列:角色
|
||||
|
const tdRole = document.createElement("td"); |
||||
|
tdRole.textContent = item.role; |
||||
|
tr.appendChild(tdRole); |
||||
|
|
||||
|
// 第三列:内容
|
||||
|
const tdContent = document.createElement("td"); |
||||
|
tdContent.textContent = item.content; |
||||
|
tr.appendChild(tdContent); |
||||
|
|
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
// 不足 10 行时,补空行
|
||||
|
for(let i = pageData.length; i < pageSize; i++){ |
||||
|
const tr = document.createElement("tr"); |
||||
|
tr.innerHTML = `<td> </td><td> </td><td> </td>`; |
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 分页按钮事件
|
||||
|
document.getElementById("submittedPrev").addEventListener("click", function(e){ |
||||
|
e.preventDefault(); |
||||
|
if(submittedPage > 1){ |
||||
|
submittedPage--; |
||||
|
renderSubmittedTable(submittedPage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("submittedNext").addEventListener("click", function(e){ |
||||
|
e.preventDefault(); |
||||
|
const maxPage = Math.ceil(submittedMsgs.length / pageSize); |
||||
|
if(submittedPage < maxPage){ |
||||
|
submittedPage++; |
||||
|
renderSubmittedTable(submittedPage); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 导出功能:导出当前页已提交数据到 CSV
|
||||
|
document.getElementById("btnExportSubmitted").addEventListener("click", function(){ |
||||
|
exportSubmittedCurrentPage(); |
||||
|
}); |
||||
|
function exportSubmittedCurrentPage(){ |
||||
|
const start = (submittedPage - 1) * pageSize; |
||||
|
const end = start + pageSize; |
||||
|
const pageData = submittedMsgs.slice(start, end); |
||||
|
let csv = "\uFEFF" + "序号,角色,内容\n"; // 添加 BOM 防乱码
|
||||
|
pageData.forEach((item, i) => { |
||||
|
csv += `${start + i + 1},${item.role},${item.content}\n`; |
||||
|
}); |
||||
|
// 补空行
|
||||
|
for(let i = pageData.length; i < pageSize; i++){ |
||||
|
csv += ",,\n"; |
||||
|
} |
||||
|
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
const a = document.createElement("a"); |
||||
|
a.href = url; |
||||
|
a.download = "已提交消息.csv"; |
||||
|
a.click(); |
||||
|
URL.revokeObjectURL(url); |
||||
|
} |
||||
|
|
||||
|
//新增请求指令的msg
|
||||
|
document.getElementById("btnNeedInstr").addEventListener("click",function(){ |
||||
|
if(selectedNodeData.workstatus === "0"){ |
||||
|
if (!confirm("是否确认要为该节点新增请求指令的信息?")) return; |
||||
|
// 获取用户在待提交区输入的新值
|
||||
|
newType = 3; |
||||
|
newContent = "请针对该节点信息,生成下一步渗透测试指令。"; |
||||
|
//提交到后端更新
|
||||
|
bsuccess = putnewmsg(newType,newContent); |
||||
|
if(bsuccess){ |
||||
|
//更新缓存
|
||||
|
selectedNodeData.workstatus = "3"; |
||||
|
const nodeEl = document.querySelector(`.tree-node[data-node_path="${selectedNodeData.node_path}"]`); |
||||
|
if (nodeEl) { |
||||
|
// 更新 DOM 属性(属性值均为字符串)
|
||||
|
nodeEl.setAttribute("data-node_workstatus", "3"); |
||||
|
nodeEl.classList.remove("no-work"); |
||||
|
} |
||||
|
//更新界面
|
||||
|
strnew = getWorkStatus_Str(3); |
||||
|
document.getElementById("node_workstatus").textContent = strnew; |
||||
|
document.getElementById("llmtype").value = newType; |
||||
|
document.getElementById("pendingContent").value = newContent; |
||||
|
} |
||||
|
}else { |
||||
|
alert("只允许在-无待执行任务状态下新增请求指令的msg!") |
||||
|
} |
||||
|
}); |
||||
|
// 保存待提交内容修改
|
||||
|
document.getElementById("btnSavePending").addEventListener("click", function(){ |
||||
|
if(selectedNodeData.workstatus === "3"){ |
||||
|
if (!confirm("是否确认要保存对该节点待提交信息的修改?")) return; |
||||
|
// 获取用户在待提交区输入的新值
|
||||
|
const newType = document.getElementById("llmtype").value; |
||||
|
const newContent = document.getElementById("pendingContent").value; |
||||
|
//提交到后端更新
|
||||
|
putnewmsg(newType,newContent); |
||||
|
}else { |
||||
|
alert("只允许在-待提交llm状态下修改或新增msg!") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
async function putnewmsg(llmtype,content){ |
||||
|
task_id = cur_task_id; |
||||
|
node_path = selectedNodeData.node_path; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/nodeupdatemsg", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({task_id,node_path,llmtype,content}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
bsuccess = data.bsuccess; |
||||
|
if(bsuccess){ |
||||
|
alert("修改成功") |
||||
|
pendingMsg.llmtype = llmtype; |
||||
|
pendingMsg.content = content; |
||||
|
return true; |
||||
|
} |
||||
|
else{ |
||||
|
alert("修改失败:",data.error) |
||||
|
return false; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("加载Msg数据异常:", error); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//---------------------添加信息modal------------------------------
|
||||
|
document.getElementById("btnAddInfo").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
alert("该功能实现中..."); |
||||
|
}); |
||||
|
document.getElementById("btnAddChild").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
alert("该功能实现中..."); |
||||
|
}); |
||||
|
|
||||
|
// 页面加载完成后,加载节点树
|
||||
|
//document.addEventListener("DOMContentLoaded", loadNodeTree);
|
@ -0,0 +1,534 @@ |
|||||
|
let task_list = [] |
||||
|
let cur_task = null //当前选择的task--用于修改缓存时使用
|
||||
|
let cur_task_id = 0 //当前选择的cur_task_id
|
||||
|
let ws = null |
||||
|
|
||||
|
// 页面卸载时断开连接
|
||||
|
window.addEventListener("beforeunload", function() { |
||||
|
if (ws) { |
||||
|
ws.close(); |
||||
|
ws =null; |
||||
|
} |
||||
|
task_list = [] |
||||
|
cur_task = null; |
||||
|
cur_task_id = 0; |
||||
|
}); |
||||
|
|
||||
|
// 页面加载完成后调用接口获取任务列表数据
|
||||
|
document.addEventListener("DOMContentLoaded", async () => { |
||||
|
//建立wbsocket
|
||||
|
initWebSocket() |
||||
|
//获取左侧任务列表
|
||||
|
getTasklist(); |
||||
|
//当前选中的数据要清空
|
||||
|
cur_task = null; |
||||
|
cur_task_id = 0; |
||||
|
//任务基本信息界面初始化
|
||||
|
document.getElementById("detailTestTarget").textContent = "-"; |
||||
|
document.getElementById("detailTestStatus").textContent = "-"; |
||||
|
document.getElementById("detailSafeStatus").textContent = '-'; |
||||
|
//单选按钮
|
||||
|
set_radio_selection('testMode', 'auto'); |
||||
|
setSetpBtnStatus(); |
||||
|
//节点树界面初始化
|
||||
|
update_select_node_data_show("-","-","-","-","-",false) |
||||
|
//单选按钮点击事件 ------ 是不是更规范的编程方式,应该把控件事件的添加都放页面加载完成后处理
|
||||
|
const autoRadio = document.getElementById("autoMode"); |
||||
|
const manualRadio = document.getElementById("manualMode"); |
||||
|
autoRadio.addEventListener("click", () => updateTestMode(1)); |
||||
|
manualRadio.addEventListener("click", () => updateTestMode(0)); |
||||
|
//tab页点击刷新数据
|
||||
|
document.getElementById("testInstructionsTab").addEventListener("click",()=>searchInstructions()); |
||||
|
document.getElementById("vulnerabilitiesTab").addEventListener("click",()=>searchVulnerabilities()); |
||||
|
//指令和漏洞数据的导出按钮点击事件
|
||||
|
document.getElementById("instrExportBtn").addEventListener("click",()=>ExportInstructions()); |
||||
|
document.getElementById("vulExportBtn").addEventListener("click",()=>ExportVuls()); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
//----------------------左侧任务列表-----------------------
|
||||
|
function getstrsafeR(safeRank){ |
||||
|
if(safeRank === 0){ |
||||
|
safeR = "安全"; |
||||
|
} |
||||
|
else{ |
||||
|
safeR = "存在风险"; |
||||
|
} |
||||
|
return safeR; |
||||
|
} |
||||
|
function getstrTaskS(taskStatus){ |
||||
|
if(taskStatus === 0){ |
||||
|
taskS = "暂停中"; |
||||
|
}else if(taskStatus === 1){ |
||||
|
taskS = "执行中"; |
||||
|
}else { |
||||
|
taskS = "已完成"; |
||||
|
} |
||||
|
return taskS; |
||||
|
} |
||||
|
|
||||
|
async function getTasklist(){ |
||||
|
try { |
||||
|
const res = await fetch("/api/task/getlist"); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
task_list = data.tasks |
||||
|
const taskList = document.getElementById("taskList"); |
||||
|
taskList.innerHTML = ""; // 清空“加载中”提示
|
||||
|
// 遍历任务数组,生成任务项
|
||||
|
task_list.forEach((task) => { |
||||
|
const taskItem = document.createElement("div"); |
||||
|
taskItem.dataset.taskID =task.taskID //在taskItem添加数据属性,这是关联数据的第二种方法,selectedEl.dataset.taskId; 感觉比cur_task更好
|
||||
|
taskItem.className = "task-item"; |
||||
|
|
||||
|
// 第一行:测试目标
|
||||
|
const targetDiv = document.createElement("div"); |
||||
|
targetDiv.className = "task-target"; |
||||
|
targetDiv.textContent = task.testTarget; |
||||
|
taskItem.appendChild(targetDiv); |
||||
|
|
||||
|
// 第二行:测试状态,带缩进
|
||||
|
const statusDiv = document.createElement("div"); |
||||
|
statusDiv.className = "task-status"; |
||||
|
let safeR = getstrsafeR(task.safeRank); |
||||
|
let taskS = getstrTaskS(task.taskStatus); |
||||
|
statusDiv.textContent = `${taskS}-${safeR}`; |
||||
|
taskItem.appendChild(statusDiv); |
||||
|
|
||||
|
// 可绑定点击事件:点击任务项更新右侧详情
|
||||
|
taskItem.addEventListener("click", () => { |
||||
|
// 取消所有任务项的选中状态
|
||||
|
document.querySelectorAll(".task-item.selected").forEach(item => { |
||||
|
item.classList.remove("selected"); |
||||
|
}); |
||||
|
// 给当前点击的任务项添加选中状态
|
||||
|
taskItem.classList.add("selected"); |
||||
|
//执行业务代码 --参数当前选中task的数据
|
||||
|
cur_task = task; |
||||
|
selected_task_item() |
||||
|
}); |
||||
|
taskList.appendChild(taskItem); |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
console.error("加载任务列表出错:", error); |
||||
|
document.getElementById("taskList").innerHTML = "<p>加载任务列表失败!</p>"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//选中tasklist--更新界面数据
|
||||
|
function selected_task_item(){ |
||||
|
if(cur_task_id === cur_task.taskID) return; |
||||
|
cur_task_id = cur_task.taskID; |
||||
|
//按钮状态更新
|
||||
|
actionButton = document.getElementById("actionButton"); |
||||
|
if(cur_task.taskStatus === 0){ |
||||
|
actionButton.textContent = "继续"; |
||||
|
}else if(cur_task.taskStatus === 1){ |
||||
|
actionButton.textContent = "暂停"; |
||||
|
}else { |
||||
|
} |
||||
|
//基本信息
|
||||
|
let safeR = getstrsafeR(cur_task.safeRank); |
||||
|
let taskS = getstrTaskS(cur_task.taskStatus); |
||||
|
document.getElementById("detailTestTarget").textContent = cur_task.testTarget; |
||||
|
document.getElementById("detailTestStatus").textContent = taskS; |
||||
|
document.getElementById("detailSafeStatus").textContent = safeR; |
||||
|
//单选按钮
|
||||
|
if(cur_task.workType === 0){ //人工
|
||||
|
set_radio_selection('testMode', 'manual'); |
||||
|
}else { //1-自动
|
||||
|
set_radio_selection('testMode', 'auto'); |
||||
|
} |
||||
|
//更新单步按钮
|
||||
|
setSetpBtnStatus() |
||||
|
//加载任务其他信息--node_tree.js
|
||||
|
loadNodeTree(cur_task_id); |
||||
|
//加载测试指令和漏洞数据
|
||||
|
searchInstructions(); |
||||
|
searchVulnerabilities(); |
||||
|
// renderTableRows(document.querySelector("#instrTable tbody"), []);
|
||||
|
// renderTableRows(document.querySelector("#vulTable tbody"), []);
|
||||
|
} |
||||
|
|
||||
|
//--------------------任务基本信息区域--------------------
|
||||
|
//单选按钮--测试模式修改
|
||||
|
async function updateTestMode(mode){ //0-人工,1-自动
|
||||
|
if(cur_task){ |
||||
|
if(cur_task.workType !== mode){ |
||||
|
if(cur_task.taskStatus === 1){ |
||||
|
alert("执行状态,不允许修改测试模式!"); |
||||
|
if( cur_task.workType === 0){ //人工
|
||||
|
set_radio_selection('testMode', 'manual'); |
||||
|
}else { //1-自动
|
||||
|
set_radio_selection('testMode', 'auto'); |
||||
|
} |
||||
|
} |
||||
|
else {//不是执行状态,可以修改测试模式
|
||||
|
try { |
||||
|
const res = await fetch("/api/task/taskworktype", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ cur_task_id,mode }), //task_id:task_id
|
||||
|
}); |
||||
|
// 新增状态码校验
|
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
bsuccess = data.bsuccess; |
||||
|
if(bsuccess){ |
||||
|
cur_task.workType = mode; //更新前端缓存
|
||||
|
//更新单步按钮状态
|
||||
|
setSetpBtnStatus() |
||||
|
}else { |
||||
|
alert("修改测试模失败!") |
||||
|
if( cur_task.workType === 0){ //人工 修改失败还原单选按钮点击状态
|
||||
|
set_radio_selection('testMode', 'manual'); |
||||
|
}else { //1-自动
|
||||
|
set_radio_selection('testMode', 'auto'); |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("控制任务状态异常:", error); |
||||
|
alert("控制任务状态异常:",error); |
||||
|
if( cur_task.workType === 0){ //人工 修改失败还原单选按钮点击状态
|
||||
|
set_radio_selection('testMode', 'manual'); |
||||
|
}else { //1-自动
|
||||
|
set_radio_selection('testMode', 'auto'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
//点击暂停/继续按钮
|
||||
|
document.getElementById("actionButton").addEventListener("click",() => { |
||||
|
controlTask(); |
||||
|
}); |
||||
|
async function controlTask(){ |
||||
|
if(cur_task_id === 0){ |
||||
|
alert("请先选择一个任务!") |
||||
|
return |
||||
|
} |
||||
|
try { |
||||
|
const res = await fetch("/api/task/taskcontrol", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ cur_task_id }), //task_id:task_id
|
||||
|
}); |
||||
|
// 新增状态码校验
|
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
newstatus = data.newstatus; |
||||
|
//更新页面
|
||||
|
if(newstatus === 0){ |
||||
|
document.getElementById("detailTestStatus").textContent = "暂停中"; |
||||
|
actionButton.textContent = "继续"; |
||||
|
}else if(newstatus === 1){ |
||||
|
document.getElementById("detailTestStatus").textContent = "执行中"; |
||||
|
actionButton.textContent = "暂停"; |
||||
|
}else { |
||||
|
document.getElementById("detailTestStatus").textContent = "已完成"; |
||||
|
actionButton.textContent = "已完成"; |
||||
|
} |
||||
|
cur_task.taskStatus = newstatus; //光有个cur_taks也可以
|
||||
|
setSetpBtnStatus(); |
||||
|
//更新task_list的显示
|
||||
|
updateTaskList(); |
||||
|
} catch (error) { |
||||
|
console.error("控制任务状态异常:", error); |
||||
|
alert("控制任务状态异常:",error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//结束任务-brun-false
|
||||
|
document.getElementById("btnTaskOver").addEventListener("click",()=>{ |
||||
|
overTask(); |
||||
|
}) |
||||
|
async function overTask(){ |
||||
|
if(cur_task_id === 0){ |
||||
|
alert("请先选择一个任务!") |
||||
|
return |
||||
|
} |
||||
|
try { |
||||
|
if (confirm('确定要结束此任务吗?')){ |
||||
|
const res = await fetch("/api/task/taskover", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ cur_task_id }), //task_id:task_id
|
||||
|
}); |
||||
|
// 新增状态码校验
|
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
bsuccess = data.bsuccess; |
||||
|
error = data.error; |
||||
|
if(bsuccess){ |
||||
|
//更新页面
|
||||
|
task_list = [] |
||||
|
cur_task = null //当前选择的task--用于修改缓存时使用
|
||||
|
cur_task_id = 0 //当前选择的cur_task_id
|
||||
|
//重新获取任务list
|
||||
|
getTasklist(); |
||||
|
}else { |
||||
|
alert("结束任务失败:",error); |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
alert("结束任务失败:",error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//修改了涉及到tasklist的展示内容,修改tasklist显示
|
||||
|
function updateTaskList(){ |
||||
|
//更新数据
|
||||
|
const selectedEl = document.querySelector(".task-item.selected"); |
||||
|
if (selectedEl) { |
||||
|
let safeR = getstrsafeR(cur_task.safeRank); |
||||
|
let taskS = getstrTaskS(cur_task.taskStatus); |
||||
|
const statusDiv = selectedEl.querySelector(".task-status"); |
||||
|
statusDiv.textContent = `${taskS}-${safeR}`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//设置单步按钮的可点击状态状态
|
||||
|
function setSetpBtnStatus(){ |
||||
|
if(cur_task){ |
||||
|
task_status = cur_task.taskStatus; |
||||
|
work_type = cur_task.workType; |
||||
|
}else { |
||||
|
task_status = 0; |
||||
|
work_type = 0; |
||||
|
} |
||||
|
btn_TaskStep = document.getElementById("one_step"); |
||||
|
btn_NodeStep = document.getElementById("btnNodeStep"); |
||||
|
if(task_status===1 && work_type===0){ //执行中且是人工模式
|
||||
|
btn_TaskStep.disabled= false; |
||||
|
btn_TaskStep.classList.remove("disabled-btn"); |
||||
|
//node-step
|
||||
|
if (selectedNodeData) { |
||||
|
if(selectedNodeData.node_bwork){ //有选中node,且节点为工作状态
|
||||
|
btn_NodeStep.disabled= false; |
||||
|
btn_NodeStep.classList.remove("disabled-btn"); |
||||
|
} |
||||
|
else { |
||||
|
btn_NodeStep.disabled = true; |
||||
|
btn_NodeStep.classList.add("disabled-btn"); //css会去重
|
||||
|
} |
||||
|
} |
||||
|
}else{ //其他情况都是不可用状态
|
||||
|
btn_TaskStep.disabled = true; // 添加 disabled 属性
|
||||
|
btn_TaskStep.classList.add("disabled-btn"); // 添加自定义样式
|
||||
|
if (selectedNodeData) { |
||||
|
btn_NodeStep.disabled = true; |
||||
|
btn_NodeStep.classList.add("disabled-btn"); //css会去重
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//单步按钮--任务单步
|
||||
|
document.getElementById("one_step").addEventListener("click",() => { |
||||
|
if(cur_task_id===0){ |
||||
|
return |
||||
|
} |
||||
|
one_step_task(); |
||||
|
}); |
||||
|
async function one_step_task(){ |
||||
|
try { |
||||
|
const res = await fetch("/api/task/taskstep", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ cur_task_id }), //task_id:task_id
|
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
//修改成功
|
||||
|
const data = await res.json(); |
||||
|
const bsuccess = data.bsuccess; |
||||
|
if(bsuccess){ |
||||
|
alert("该任务已提交单步工作,请稍候查看执行结果!") |
||||
|
} |
||||
|
else{ |
||||
|
error = data.erroe; |
||||
|
alert("该任务单步失败!",error) |
||||
|
} |
||||
|
}catch (error) { |
||||
|
alert("该节点单步失败,请联系管理员!", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//------------------测试数据和漏洞数据tab-------------------
|
||||
|
const pageSize = 10; |
||||
|
// 复用:根据返回的数据数组渲染表格 tbody,保证固定 10 行
|
||||
|
function renderTableRows(tbody, rowsData) { |
||||
|
tbody.innerHTML = ""; |
||||
|
// 遍历数据行,生成 <tr>
|
||||
|
rowsData.forEach((row, index) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
// 这里假设 row 为对象,包含各个字段;下标从1开始显示序号
|
||||
|
for (const cellData of Object.values(row)) { |
||||
|
const td = document.createElement("td"); |
||||
|
td.textContent = cellData; |
||||
|
tr.appendChild(td); |
||||
|
} |
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
// 补足空行
|
||||
|
const rowCount = rowsData.length; |
||||
|
for (let i = rowCount; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
for (let j = 0; j < tbody.parentElement.querySelectorAll("th").length; j++) { |
||||
|
const td = document.createElement("td"); |
||||
|
td.innerHTML = " "; |
||||
|
tr.appendChild(td); |
||||
|
} |
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//--------------------------测试指令-------------------------------
|
||||
|
let allInstrs = []; |
||||
|
let currentInstrPage = 1; |
||||
|
function renderInstrPage(page) { |
||||
|
currentInstrPage = page; |
||||
|
const start = (page - 1) * pageSize; |
||||
|
const end = start + pageSize; |
||||
|
const pageData = allInstrs.slice(start, end); |
||||
|
|
||||
|
const tbody = document.querySelector("#instrTable tbody"); |
||||
|
renderTableRows(tbody, pageData); |
||||
|
|
||||
|
// 更新分页按钮
|
||||
|
document.getElementById("instrPrev").dataset.page = page > 1 ? page - 1 : 1; |
||||
|
document.getElementById("instrNext").dataset.page = (end < allInstrs.length) ? page + 1 : page; |
||||
|
} |
||||
|
|
||||
|
// 查询测试指令
|
||||
|
async function searchInstructions(page = 1) { |
||||
|
if(cur_task_id === 0){ |
||||
|
return; |
||||
|
} |
||||
|
const nodeName = document.getElementById("instrNodeName").value.trim(); |
||||
|
try { |
||||
|
const res = await fetch("/api/task/getinstr", { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
cur_task_id, |
||||
|
nodeName |
||||
|
}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
allInstrs = data.instrs; |
||||
|
renderInstrPage(1); //显示第一页数据
|
||||
|
} catch (error) { |
||||
|
console.error("获取测试指令失败:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//导出测试指令数据
|
||||
|
async function ExportInstructions(){ |
||||
|
alert("导出指令功能实现中。。。"); |
||||
|
} |
||||
|
|
||||
|
// 绑定测试指令查询按钮事件
|
||||
|
document.getElementById("instrSearchBtn").addEventListener("click", () => { |
||||
|
searchInstructions(); |
||||
|
}); |
||||
|
// 绑定测试指令分页点击事件
|
||||
|
document.getElementById("instrPrev").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderInstrPage(page); |
||||
|
}); |
||||
|
document.getElementById("instrNext").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderInstrPage(page); |
||||
|
}); |
||||
|
|
||||
|
//------------------漏洞数据---------------------------------
|
||||
|
let allVuls = []; |
||||
|
let currentVulPage = 1; |
||||
|
function renderVulPage(page) { |
||||
|
currentVulPage = page; |
||||
|
const start = (page - 1) * pageSize; |
||||
|
const end = start + pageSize; |
||||
|
const pageData = allVuls.slice(start, end); |
||||
|
|
||||
|
const tbody = document.querySelector("#vulTable tbody"); |
||||
|
renderTableRows(tbody, pageData); |
||||
|
|
||||
|
// 更新分页按钮
|
||||
|
document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1; |
||||
|
document.getElementById("vulNext").dataset.page = (end < allVuls.length) ? page + 1 : page; |
||||
|
} |
||||
|
|
||||
|
// 查询漏洞数据
|
||||
|
async function searchVulnerabilities(page = 1) { |
||||
|
if(cur_task_id === 0){return;} |
||||
|
const nodeName = document.getElementById("vulNodeName").value.trim(); |
||||
|
const vulType = document.getElementById("vulType").value.trim(); |
||||
|
const vulLevel = document.getElementById("vulLevel").value; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/getvul", { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
cur_task_id, |
||||
|
nodeName, |
||||
|
vulType, |
||||
|
vulLevel |
||||
|
}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
allVuls = data.vuls; |
||||
|
renderVulPage(1) |
||||
|
} catch (error) { |
||||
|
console.error("获取漏洞数据失败:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//导出漏洞数据
|
||||
|
async function ExportVuls(){ |
||||
|
alert("导出漏洞功能实现中。。。"); |
||||
|
} |
||||
|
|
||||
|
// 绑定漏洞数据查询按钮事件
|
||||
|
document.getElementById("vulSearchBtn").addEventListener("click", () => { |
||||
|
searchVulnerabilities(); |
||||
|
}); |
||||
|
// 绑定漏洞数据分页点击事件
|
||||
|
document.getElementById("vulPrev").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderVulPage(page); |
||||
|
}); |
||||
|
document.getElementById("vulNext").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderVulPage(page); |
||||
|
}); |
@ -0,0 +1,659 @@ |
|||||
|
// 全局变量,用于保存当前选中的节点数据
|
||||
|
let selectedNodeData = null; |
||||
|
|
||||
|
/** |
||||
|
* 根据节点数据递归生成树形结构(返回 <li> 元素) |
||||
|
* 假设节点数据格式: |
||||
|
* { |
||||
|
* "node_name":node.name, |
||||
|
* "node_path":node.path, |
||||
|
* "node_status":node.status, |
||||
|
* "node_bwork":node.bwork, |
||||
|
* "node_vultype":node.vul_type, |
||||
|
* "node_vulgrade":node.vul_grade, |
||||
|
* children: [ { ... }, { ... } ] |
||||
|
* } |
||||
|
*/ |
||||
|
function generateTreeHTML(nodeData) { |
||||
|
const li = document.createElement("li"); |
||||
|
const nodeSpan = document.createElement("span"); |
||||
|
nodeSpan.className = "tree-node"; |
||||
|
//设置data属性
|
||||
|
nodeSpan.setAttribute("data-node_name", nodeData.node_name); |
||||
|
nodeSpan.setAttribute("data-node_path", nodeData.node_path); |
||||
|
nodeSpan.setAttribute("data-node_status", nodeData.node_status); |
||||
|
nodeSpan.setAttribute("data-node_bwork", nodeData.node_bwork); |
||||
|
nodeSpan.setAttribute("data-node_vultype", nodeData.node_vultype); |
||||
|
nodeSpan.setAttribute("data-node_vulgrade", nodeData.node_vulgrade || ""); |
||||
|
nodeSpan.setAttribute("data-node_workstatus",nodeData.node_workstatus); |
||||
|
if(nodeData.node_workstatus ===0){ |
||||
|
nodeSpan.classList.add("no-work"); |
||||
|
}else { |
||||
|
nodeSpan.classList.remove("no-work"); |
||||
|
} |
||||
|
// 根据漏洞级别添加样式
|
||||
|
if (nodeData.node_vulgrade) { |
||||
|
nodeSpan.classList.remove("no-work"); |
||||
|
if (nodeData.node_vulgrade === "低危") { |
||||
|
nodeSpan.classList.add("vul-low"); |
||||
|
} else if (nodeData.node_vulgrade === "中危") { |
||||
|
nodeSpan.classList.add("vul-medium"); |
||||
|
} else if (nodeData.node_vulgrade === "高危") { |
||||
|
nodeSpan.classList.add("vul-high"); |
||||
|
} |
||||
|
} |
||||
|
// 创建容器用于存放切换图标与文本
|
||||
|
const container = document.createElement("div"); |
||||
|
container.className = "node-container"; |
||||
|
// 如果有子节点,则添加切换图标
|
||||
|
if (nodeData.children && nodeData.children.length > 0) { |
||||
|
const toggleIcon = document.createElement("span"); |
||||
|
toggleIcon.className = "toggle-icon"; |
||||
|
toggleIcon.textContent = "-"; // 默认展开时显示“-”
|
||||
|
container.appendChild(toggleIcon); |
||||
|
} |
||||
|
//节点文本
|
||||
|
const textSpan = document.createElement("span"); |
||||
|
textSpan.className = "node-text"; |
||||
|
textSpan.textContent = nodeData.node_name; |
||||
|
container.appendChild(textSpan); |
||||
|
nodeSpan.appendChild(container); |
||||
|
li.appendChild(nodeSpan); |
||||
|
//如果存在子节点,递归生成子节点列表
|
||||
|
if (nodeData.children && nodeData.children.length > 0) { |
||||
|
const ul = document.createElement("ul"); |
||||
|
nodeData.children.forEach((child) => { |
||||
|
ul.appendChild(generateTreeHTML(child)); |
||||
|
}); |
||||
|
li.appendChild(ul); |
||||
|
} |
||||
|
return li; |
||||
|
} |
||||
|
|
||||
|
// 绑定所有节点的点击事件
|
||||
|
function bindTreeNodeEvents() { |
||||
|
document.querySelectorAll(".tree-node").forEach((el) => { |
||||
|
el.addEventListener("click", (event) => { |
||||
|
// 阻止事件冒泡,避免点击时展开折叠影响
|
||||
|
event.stopPropagation(); |
||||
|
// 清除之前选中的节点样式
|
||||
|
document |
||||
|
.querySelectorAll(".tree-node.selected") |
||||
|
.forEach((node) => node.classList.remove("selected")); |
||||
|
// 当前节点标记为选中
|
||||
|
el.classList.add("selected"); |
||||
|
// 读取 data 属性更新右侧显示
|
||||
|
const nodeName = el.getAttribute("data-node_name"); |
||||
|
const status = el.getAttribute("data-node_status"); |
||||
|
const nodepath = el.getAttribute("data-node_path"); |
||||
|
const nodebwork = el.getAttribute("data-node_bwork"); |
||||
|
const vulType = el.getAttribute("data-node_vultype"); |
||||
|
const vulLevel = el.getAttribute("data-node_vulgrade"); |
||||
|
const workstatus = el.getAttribute("data-node_workstatus"); |
||||
|
//selectedNodeData = { nodeName, status, vulType, vulLevel,nodepath,nodebwork };
|
||||
|
// 示例中默认填充
|
||||
|
selectedNodeData = { |
||||
|
node_name: nodeName, |
||||
|
node_path: nodepath, |
||||
|
status: status, |
||||
|
node_bwork: nodebwork, |
||||
|
vul_type: vulType, |
||||
|
vul_grade: vulLevel || "-", |
||||
|
workstatus: workstatus |
||||
|
}; |
||||
|
//刷新界面内容
|
||||
|
update_select_node_data_show(nodeName,status,vulType,vulLevel,workstatus,nodebwork) |
||||
|
}); |
||||
|
// 双击事件:展开/收缩子节点区域
|
||||
|
el.addEventListener("dblclick", (event) => { |
||||
|
event.stopPropagation(); |
||||
|
// 找到该节点下的 <ul> 子节点列表
|
||||
|
const parentLi = el.parentElement; |
||||
|
const childUl = parentLi.querySelector("ul"); |
||||
|
if (childUl) { |
||||
|
// 切换 collapsed 类,控制 display
|
||||
|
childUl.classList.toggle("collapsed"); |
||||
|
// 更新切换图标
|
||||
|
const toggleIcon = el.querySelector(".toggle-icon"); |
||||
|
if (toggleIcon) { |
||||
|
toggleIcon.textContent = childUl.classList.contains("collapsed") |
||||
|
? "+" |
||||
|
: "-"; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 动态加载节点树数据
|
||||
|
async function loadNodeTree(task_id) { |
||||
|
// 清空选中状态
|
||||
|
selectedNodeData = null; |
||||
|
//刷新界面内容
|
||||
|
update_select_node_data_show("-","-","-","-","-",false) |
||||
|
try { |
||||
|
const res = await fetch("/api/task/gethistree", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({ task_id }), //task_id:task_id
|
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
const treeData = data.tree; |
||||
|
if (!treeData) { |
||||
|
document.getElementById("treeContent").innerHTML = |
||||
|
"<p>无节点数据</p>"; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 创建一个 <ul> 作为树的根容器
|
||||
|
const ul = document.createElement("ul"); |
||||
|
ul.className = "tree-root-ul"; |
||||
|
ul.appendChild(generateTreeHTML(treeData)); |
||||
|
// 替换节点树容器的内容
|
||||
|
const container = document.getElementById("treeContent"); |
||||
|
container.innerHTML = ""; |
||||
|
container.appendChild(ul); |
||||
|
// 绑定节点点击事件
|
||||
|
bindTreeNodeEvents(); |
||||
|
} catch (error) { |
||||
|
console.error("加载节点树失败:", error); |
||||
|
document.getElementById("treeContent").innerHTML = "<p>加载节点树失败</p>"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getWorkStatus_Str(workstatus){ |
||||
|
strworkstatus = "" |
||||
|
switch (workstatus){ |
||||
|
case 0: |
||||
|
strworkstatus = "无待执行任务"; |
||||
|
break; |
||||
|
case 1: |
||||
|
strworkstatus = "待执行指令中"; |
||||
|
break; |
||||
|
case 2: |
||||
|
strworkstatus = "指令执行中"; |
||||
|
break; |
||||
|
case 3: |
||||
|
strworkstatus = "待提交llm中"; |
||||
|
break; |
||||
|
case 4: |
||||
|
strworkstatus = "提交llm中"; |
||||
|
break; |
||||
|
default: |
||||
|
strworkstatus = "-" |
||||
|
} |
||||
|
return strworkstatus |
||||
|
} |
||||
|
|
||||
|
//根据web端过来的数据,更新节点的工作状态
|
||||
|
function updateTreeNode(node_path, node_workstatus) { |
||||
|
// 根据 node_path 查找对应节点(假设每个 .tree-node 上设置了 data-node_path 属性)
|
||||
|
const nodeEl = document.querySelector(`.tree-node[data-node_path="${node_path}"]`); |
||||
|
if (nodeEl) { |
||||
|
// 更新 DOM 属性(属性值均为字符串)
|
||||
|
nodeEl.setAttribute("data-node_workstatus", node_workstatus); |
||||
|
//判断是否需要更新界面
|
||||
|
if(selectedNodeData){ |
||||
|
if(node_path === selectedNodeData.node_path){ //只有是当前选中节点才更新数据
|
||||
|
selectedNodeData.workstatus = node_workstatus; |
||||
|
strnew = getWorkStatus_Str(node_workstatus); |
||||
|
document.getElementById("node_workstatus").textContent = strnew; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
console.warn(`未找到节点 ${node_path}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//刷新节点的数据显示
|
||||
|
function update_select_node_data_show(nodeName,testStatus,vulType,vulLevel,workStatus,nodebwork){ |
||||
|
document.getElementById("nodeName").textContent = nodeName; |
||||
|
document.getElementById("testStatus").textContent = testStatus; |
||||
|
document.getElementById("node_vulType").textContent = vulType; |
||||
|
document.getElementById("node_vulLevel").textContent = vulLevel; |
||||
|
str_workStatus = getWorkStatus_Str(Number(workStatus)); |
||||
|
document.getElementById("node_workstatus").textContent = str_workStatus; |
||||
|
if(nodebwork==="true"){ |
||||
|
document.getElementById("node_bwork").textContent = "执行中"; |
||||
|
}else { |
||||
|
document.getElementById("node_bwork").textContent = "暂停中"; |
||||
|
} |
||||
|
setNodeBtnStatus(); |
||||
|
} |
||||
|
|
||||
|
//节点按钮的状态控制
|
||||
|
function setNodeBtnStatus(){ |
||||
|
const btn_VI = document.getElementById("btnViewInstr"); |
||||
|
if(!selectedNodeData){ |
||||
|
//没有选择node,按钮全部置不可用
|
||||
|
btn_VI.disabled = true; |
||||
|
btn_VI.classList.add("disabled-btn"); |
||||
|
} |
||||
|
else{ |
||||
|
btn_VI.disabled = false; |
||||
|
btn_VI.classList.remove("disabled-btn"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// // 刷新按钮事件绑定
|
||||
|
// document.getElementById("btnRefresh").addEventListener("click", () => {
|
||||
|
// // 重新加载节点树数据
|
||||
|
// loadNodeTree(cur_task_id);
|
||||
|
// });
|
||||
|
|
||||
|
// 按钮事件:当未选中节点时提示
|
||||
|
function checkSelectedNode() { |
||||
|
if (!selectedNodeData) { |
||||
|
alert("请先选择节点"); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
//----------------------查看节点--指令modal----------------------------
|
||||
|
let doneInstrs = []; // 已执行指令的所有数据
|
||||
|
let todoInstrs = []; // 待执行指令的所有数据
|
||||
|
let donePage = 1; // 已执行指令当前页
|
||||
|
let todoPage = 1; // 待执行指令当前页
|
||||
|
|
||||
|
document.getElementById("btnViewInstr").addEventListener("click", () => { |
||||
|
if (!checkSelectedNode()) return; |
||||
|
openInstrModal() |
||||
|
}); |
||||
|
// 打开对话框函数
|
||||
|
function openInstrModal() { |
||||
|
const instrCanvas = new bootstrap.Offcanvas(document.getElementById('instrCanvas')); |
||||
|
instrCanvas.show(); |
||||
|
|
||||
|
// const modalEl = document.getElementById("instrModal");
|
||||
|
// // 假设用 Bootstrap 5 的 Modal 组件
|
||||
|
// const instrModal = new bootstrap.Modal(modalEl, {keyboard: false});
|
||||
|
// 显示对话框
|
||||
|
//instrModal.show();
|
||||
|
|
||||
|
// 在打开 modal 时,先更新提示内容,将 loadingMsg 显示“请稍后,数据获取中…”
|
||||
|
const loadingMsg = document.getElementById("loadingMsg"); |
||||
|
if (loadingMsg) { |
||||
|
loadingMsg.textContent = "请稍后,数据获取中..."; |
||||
|
} |
||||
|
|
||||
|
// 加载指令数据
|
||||
|
loadInstrData(); |
||||
|
} |
||||
|
|
||||
|
// 调用后端接口,获取指令数据
|
||||
|
async function loadInstrData() { |
||||
|
task_id = cur_task_id; |
||||
|
node_path = selectedNodeData.node_path; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/hisnodegetinstr", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({task_id,node_path}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
// 数据获取成功后,清除加载提示
|
||||
|
const loadingMsg = document.getElementById("loadingMsg"); |
||||
|
if (loadingMsg) { |
||||
|
loadingMsg.style.display = "none"; // 或者清空其 innerHTML
|
||||
|
} |
||||
|
doneInstrs = data.doneInstrs || []; |
||||
|
//todoInstrs = data.todoInstrs || [];
|
||||
|
donePage = 1; |
||||
|
todoPage = 1; |
||||
|
renderDoneInstrTable(donePage); |
||||
|
//renderTodoInstrTable(todoPage);
|
||||
|
} catch (error) { |
||||
|
console.error("加载指令数据异常:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 渲染已执行指令表格
|
||||
|
function renderDoneInstrTable(page) { |
||||
|
const tbody = document.getElementById("doneInstrTbody"); |
||||
|
// 计算起始索引
|
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = doneInstrs.slice(startIndex, endIndex); |
||||
|
//select instruction,start_time,result from task_result where task_id=%s and node_path=%s;
|
||||
|
tbody.innerHTML = ""; |
||||
|
// 插入行
|
||||
|
pageData.forEach((item, i) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
|
||||
|
// 第一列:序号
|
||||
|
const tdIndex = document.createElement("td"); |
||||
|
tdIndex.textContent = startIndex + i + 1; |
||||
|
tr.appendChild(tdIndex); |
||||
|
|
||||
|
// 第二列:指令内容
|
||||
|
const tdInstr = document.createElement("td"); |
||||
|
tdInstr.textContent = item[0]; |
||||
|
tr.appendChild(tdInstr); |
||||
|
|
||||
|
// 第三列:开始时间(如果没有则显示空字符串)
|
||||
|
const tdStartTime = document.createElement("td"); |
||||
|
tdStartTime.textContent = item[1] || ""; |
||||
|
tr.appendChild(tdStartTime); |
||||
|
|
||||
|
// 第四列:执行结果
|
||||
|
const tdResult = document.createElement("td"); |
||||
|
tdResult.textContent = item[2] || ""; |
||||
|
tr.appendChild(tdResult); |
||||
|
|
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
|
||||
|
// 若不足 10 行,补空行
|
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
tr.innerHTML = ` |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
`;
|
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 渲染待执行指令表格
|
||||
|
function renderTodoInstrTable(page) { |
||||
|
const tbody = document.getElementById("todoInstrTbody"); |
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = todoInstrs.slice(startIndex, endIndex); |
||||
|
|
||||
|
tbody.innerHTML = ""; |
||||
|
pageData.forEach((item, i) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
const idx = startIndex + i + 1; |
||||
|
// 第一列:序号
|
||||
|
const tdIndex = document.createElement("td"); |
||||
|
tdIndex.textContent = idx; |
||||
|
tr.appendChild(tdIndex); |
||||
|
|
||||
|
// 第二列:指令文本内容(直接使用 textContent)
|
||||
|
const tdItem = document.createElement("td"); |
||||
|
tdItem.textContent = item; // 使用 textContent 避免 HTML 解析
|
||||
|
tr.appendChild(tdItem); |
||||
|
|
||||
|
// 第三列:复制和删除按钮
|
||||
|
const tdAction = document.createElement("td"); |
||||
|
// const btn_cp = document.createElement("button");
|
||||
|
// btn_cp.className = "btn btn-primary btn-sm";
|
||||
|
// btn_cp.textContent = "复制";
|
||||
|
// btn_cp.style.marginRight = "2px"; // 设置间隔
|
||||
|
// btn_cp.onclick = () => confirmCopyTodoInstr(idx - 1);
|
||||
|
// tdAction.appendChild(btn_cp);
|
||||
|
const btn = document.createElement("button"); |
||||
|
btn.className = "btn btn-danger btn-sm"; |
||||
|
btn.textContent = "删除"; |
||||
|
btn.onclick = () => confirmDeleteTodoInstr(idx - 1); |
||||
|
tdAction.appendChild(btn); |
||||
|
tr.appendChild(tdAction); |
||||
|
|
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
|
||||
|
// 补空行
|
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
tr.innerHTML = ` |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
<td> </td> |
||||
|
`;
|
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 分页事件
|
||||
|
document.getElementById("doneInstrPrev").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
if (donePage > 1) { |
||||
|
donePage--; |
||||
|
renderDoneInstrTable(donePage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("doneInstrNext").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
const maxPage = Math.ceil(doneInstrs.length / pageSize); |
||||
|
if (donePage < maxPage) { |
||||
|
donePage++; |
||||
|
renderDoneInstrTable(donePage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("todoInstrPrev").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
if (todoPage > 1) { |
||||
|
todoPage--; |
||||
|
renderTodoInstrTable(todoPage); |
||||
|
} |
||||
|
}); |
||||
|
document.getElementById("todoInstrNext").addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
const maxPage = Math.ceil(todoInstrs.length / pageSize); |
||||
|
if (todoPage < maxPage) { |
||||
|
todoPage++; |
||||
|
renderTodoInstrTable(todoPage); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 导出当前页数据
|
||||
|
document.getElementById("btnExport").addEventListener("click", () => { |
||||
|
// 判断当前是哪个 Tab
|
||||
|
const activeTab = document.querySelector("#instrTab button.nav-link.active"); |
||||
|
if (activeTab.id === "doneInstrTab") { |
||||
|
exportCurrentPage(doneInstrs, donePage, ["序号", "执行指令", "执行时间", "执行结果"]); |
||||
|
} else { |
||||
|
exportCurrentPage(todoInstrs, todoPage, ["序号", "待执行指令"]); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
function exportCurrentPage(dataArr, page, headerArr) { |
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = dataArr.slice(startIndex, endIndex); |
||||
|
|
||||
|
// 在 CSV 的开头加上 BOM,用于 Excel 识别 UTF-8 编码
|
||||
|
let csvContent = "\uFEFF" + headerArr.join(",") + "\n"; |
||||
|
pageData.forEach((item, i) => { |
||||
|
const rowIndex = startIndex + i + 1; |
||||
|
if (headerArr.length === 4) { |
||||
|
// 已执行:序号,执行指令,执行时间,执行结果
|
||||
|
csvContent += rowIndex + "," + |
||||
|
(item.command || "") + "," + |
||||
|
(item.execTime || "") + "," + |
||||
|
(item.result || "") + "\n"; |
||||
|
} else { |
||||
|
// 待执行:序号,待执行指令
|
||||
|
csvContent += rowIndex + "," + (item.command || "") + "\n"; |
||||
|
} |
||||
|
}); |
||||
|
// 如果不足 pageSize 行,补足空行(根据列数进行适当补全)
|
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
// 根据 headerArr.length 来设置空行的格式
|
||||
|
if (headerArr.length === 4) { |
||||
|
csvContent += ",,,\n"; |
||||
|
} else { |
||||
|
csvContent += ",\n"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
const link = document.createElement("a"); |
||||
|
link.href = url; |
||||
|
link.download = "指令导出.csv"; |
||||
|
link.click(); |
||||
|
URL.revokeObjectURL(url); |
||||
|
} |
||||
|
|
||||
|
//------------------测试数据和漏洞数据tab-------------------
|
||||
|
// 复用:根据返回的数据数组渲染表格 tbody,保证固定 10 行
|
||||
|
function renderTableRows(tbody, rowsData) { |
||||
|
tbody.innerHTML = ""; |
||||
|
// 遍历数据行,生成 <tr>
|
||||
|
rowsData.forEach((row, index) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
// 这里假设 row 为对象,包含各个字段;下标从1开始显示序号
|
||||
|
for (const cellData of Object.values(row)) { |
||||
|
const td = document.createElement("td"); |
||||
|
td.textContent = cellData; |
||||
|
tr.appendChild(td); |
||||
|
} |
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
// 补足空行
|
||||
|
const rowCount = rowsData.length; |
||||
|
for (let i = rowCount; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
for (let j = 0; j < tbody.parentElement.querySelectorAll("th").length; j++) { |
||||
|
const td = document.createElement("td"); |
||||
|
td.innerHTML = " "; |
||||
|
tr.appendChild(td); |
||||
|
} |
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//--------------------------测试指令-------------------------------
|
||||
|
let allInstrs = []; |
||||
|
let currentInstrPage = 1; |
||||
|
function renderInstrPage(page) { |
||||
|
currentInstrPage = page; |
||||
|
const start = (page - 1) * pageSize; |
||||
|
const end = start + pageSize; |
||||
|
const pageData = allInstrs.slice(start, end); |
||||
|
|
||||
|
const tbody = document.querySelector("#instrTable tbody"); |
||||
|
renderTableRows(tbody, pageData); |
||||
|
|
||||
|
// 更新分页按钮
|
||||
|
document.getElementById("instrPrev").dataset.page = page > 1 ? page - 1 : 1; |
||||
|
document.getElementById("instrNext").dataset.page = (end < allInstrs.length) ? page + 1 : page; |
||||
|
} |
||||
|
|
||||
|
// 查询测试指令
|
||||
|
async function searchInstructions(page = 1) { |
||||
|
if(cur_task_id === 0){ |
||||
|
return; |
||||
|
} |
||||
|
const nodeName = document.getElementById("instrNodeName").value.trim(); |
||||
|
try { |
||||
|
const res = await fetch("/api/task/getinstr", { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
cur_task_id, |
||||
|
nodeName |
||||
|
}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
allInstrs = data.instrs; |
||||
|
renderInstrPage(1); //显示第一页数据
|
||||
|
} catch (error) { |
||||
|
console.error("获取测试指令失败:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//导出测试指令数据
|
||||
|
async function ExportInstructions(){ |
||||
|
alert("导出指令功能实现中。。。") |
||||
|
} |
||||
|
|
||||
|
// 绑定测试指令查询按钮事件
|
||||
|
document.getElementById("instrSearchBtn").addEventListener("click", () => { |
||||
|
searchInstructions(); |
||||
|
}); |
||||
|
// 绑定测试指令分页点击事件
|
||||
|
document.getElementById("instrPrev").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderInstrPage(page); |
||||
|
}); |
||||
|
document.getElementById("instrNext").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderInstrPage(page);; |
||||
|
}); |
||||
|
|
||||
|
//------------------漏洞数据---------------------------------
|
||||
|
let allVuls = []; |
||||
|
let currentVulPage = 1; |
||||
|
function renderVulPage(page) { |
||||
|
currentVulPage = page; |
||||
|
const start = (page - 1) * pageSize; |
||||
|
const end = start + pageSize; |
||||
|
const pageData = allVuls.slice(start, end); |
||||
|
|
||||
|
const tbody = document.querySelector("#vulTable tbody"); |
||||
|
renderTableRows(tbody, pageData); |
||||
|
|
||||
|
// 更新分页按钮
|
||||
|
document.getElementById("vulPrev").dataset.page = page > 1 ? page - 1 : 1; |
||||
|
document.getElementById("vulNext").dataset.page = (end < allVuls.length) ? page + 1 : page; |
||||
|
} |
||||
|
|
||||
|
// 查询漏洞数据
|
||||
|
async function searchVulnerabilities(page = 1) { |
||||
|
if(cur_task_id === 0){return;} |
||||
|
const nodeName = document.getElementById("vulNodeName").value.trim(); |
||||
|
const vulType = document.getElementById("vulType").value.trim(); |
||||
|
const vulLevel = document.getElementById("vulLevel").value; |
||||
|
try { |
||||
|
const res = await fetch("/api/task/getvul", { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
cur_task_id, |
||||
|
nodeName, |
||||
|
vulType, |
||||
|
vulLevel |
||||
|
}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
allVuls = data.vuls; |
||||
|
renderVulPage(1) |
||||
|
} catch (error) { |
||||
|
console.error("获取漏洞数据失败:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//导出漏洞数据
|
||||
|
async function ExportVuls(){ |
||||
|
alert("导出漏洞功能实现中。。。") |
||||
|
} |
||||
|
|
||||
|
// 绑定漏洞数据查询按钮事件
|
||||
|
document.getElementById("vulSearchBtn").addEventListener("click", () => { |
||||
|
searchVulnerabilities(); |
||||
|
}); |
||||
|
// 绑定漏洞数据分页点击事件
|
||||
|
document.getElementById("vulPrev").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderVulPage(page); |
||||
|
}); |
||||
|
document.getElementById("vulNext").addEventListener("click", (e) => { |
||||
|
const page = parseInt(e.target.dataset.page, 10); |
||||
|
renderVulPage(page); |
||||
|
}); |
@ -1,246 +0,0 @@ |
|||||
let modelMap = []; //model_name model_id
|
|
||||
let channelMap = []; //channel_name channel_id
|
|
||||
let warn_data = []; //查询到的报警数据
|
|
||||
let page_data = []; |
|
||||
let currentEditingRow = null; |
|
||||
let currentPage = 1; |
|
||||
const rowsPerPage = 30; |
|
||||
//页面加载初始化
|
|
||||
document.addEventListener('DOMContentLoaded', function () { |
|
||||
perWarnHtml() |
|
||||
|
|
||||
document.getElementById('delPageButton').addEventListener('click', function () { |
|
||||
delpageWarn(); |
|
||||
}); |
|
||||
}); |
|
||||
|
|
||||
//搜索按钮点击
|
|
||||
document.getElementById('searchMButton').addEventListener('click', function() { |
|
||||
shearchWarn(); |
|
||||
}); |
|
||||
|
|
||||
//查询数据
|
|
||||
async function shearchWarn(){ |
|
||||
//查询告警数据
|
|
||||
let modelName = document.getElementById('modelSelect').value; |
|
||||
let channelName = document.getElementById('channelSelect').value; |
|
||||
const startTime = document.getElementById('startTime').value; |
|
||||
const endTime = document.getElementById('endTime').value; |
|
||||
const sCount = 0; // 起始记录数从0开始
|
|
||||
const eCount = 5000; // 每页显示10条记录
|
|
||||
|
|
||||
if(modelName == "请选择"){ |
|
||||
modelName = ""; |
|
||||
} |
|
||||
if(channelName == "请选择"){ |
|
||||
channelName = ""; |
|
||||
} |
|
||||
|
|
||||
// 构造请求体
|
|
||||
const requestData = { |
|
||||
model_name: modelName || "", // 如果为空,则传空字符串
|
|
||||
channel_name: channelName || "", |
|
||||
start_time: startTime || "", |
|
||||
end_time: endTime || "", |
|
||||
s_count: sCount, |
|
||||
e_count: eCount |
|
||||
}; |
|
||||
try{ |
|
||||
// 发送POST请求到后端
|
|
||||
const response = await fetch('/api/warn/search_warn', { |
|
||||
method: 'POST', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' |
|
||||
}, |
|
||||
body: JSON.stringify(requestData) // 将数据转为JSON字符串
|
|
||||
}); |
|
||||
|
|
||||
// 检查响应是否成功
|
|
||||
if (response.ok) { |
|
||||
warn_data = await response.json(); |
|
||||
// 在这里处理查询结果,比如更新表格显示数据
|
|
||||
currentPage = 1; // 重置当前页为第一页
|
|
||||
renderTable(); //刷新表格
|
|
||||
renderPagination(); |
|
||||
} else { |
|
||||
console.error('查询失败:', response.status); |
|
||||
} |
|
||||
} catch (error) { |
|
||||
console.error('请求出错:', error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async function perWarnHtml() { |
|
||||
//获取算法和通道列表,在下拉框显示
|
|
||||
try{ |
|
||||
//算法名称下拉框
|
|
||||
let response = await fetch('/api/model/list'); |
|
||||
if (!response.ok) { |
|
||||
throw new Error('Network response was not ok'); |
|
||||
} |
|
||||
model_datas = await response.json(); |
|
||||
model_select_datas = ["请选择"]; |
|
||||
model_datas.forEach(option => { |
|
||||
model_select_datas.push(option.name); |
|
||||
modelMap[option.name] = option.ID; |
|
||||
}); |
|
||||
set_select_data("modelSelect",model_select_datas); |
|
||||
|
|
||||
//视频通道下拉框
|
|
||||
response = await fetch('/api/channel/tree'); |
|
||||
if (!response.ok) { |
|
||||
throw new Error('Network response was not ok'); |
|
||||
} |
|
||||
channel_datas = await response.json(); |
|
||||
channel_select_datas = ["请选择"]; |
|
||||
channel_datas.forEach(option => { |
|
||||
channel_select_datas.push(option.channel_name); |
|
||||
channelMap[option.channel_name] = option.ID; |
|
||||
}); |
|
||||
set_select_data("channelSelect",channel_select_datas); |
|
||||
//查询数据
|
|
||||
shearchWarn() |
|
||||
}catch (error) { |
|
||||
console.error('Error fetching model data:', error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//刷新表单页面数据
|
|
||||
function renderTable() { |
|
||||
const tableBody = document.getElementById('table-body-warn'); |
|
||||
tableBody.innerHTML = ''; //清空
|
|
||||
|
|
||||
const start = (currentPage - 1) * rowsPerPage; |
|
||||
const end = start + rowsPerPage; |
|
||||
pageData = warn_data.slice(start, end); |
|
||||
const surplus_count = rowsPerPage - pageData.length; |
|
||||
|
|
||||
|
|
||||
pageData.forEach((warn) => { |
|
||||
const row = document.createElement('tr'); |
|
||||
row.innerHTML = ` |
|
||||
<td>${warn.ID}</td> |
|
||||
<td>${warn.model_name}</td> |
|
||||
<td>${warn.channel_name}</td> |
|
||||
<td>${warn.creat_time}</td> |
|
||||
<td> |
|
||||
<button class="btn btn-primary btn-sm warn-show-btn">查看</button> |
|
||||
<button class="btn btn-secondary btn-sm warn-video-btn">视频</button> |
|
||||
<button class="btn btn-danger btn-sm warn-delete-btn">删除</button> |
|
||||
</td> |
|
||||
`;
|
|
||||
tableBody.appendChild(row); |
|
||||
row.querySelector('.warn-show-btn').addEventListener('click', () => showWarn(row)); |
|
||||
row.querySelector('.warn-video-btn').addEventListener('click', () => videoWarn(row)); |
|
||||
row.querySelector('.warn-delete-btn').addEventListener('click', () => deleteWarn(row)); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
//刷新分页标签
|
|
||||
function renderPagination() { |
|
||||
const pagination = document.getElementById('pagination-warn'); |
|
||||
pagination.innerHTML = ''; |
|
||||
|
|
||||
const totalPages = Math.ceil(warn_data.length / rowsPerPage); |
|
||||
for (let i = 1; i <= totalPages; i++) { |
|
||||
const pageItem = document.createElement('li'); |
|
||||
pageItem.className = 'page-item' + (i === currentPage ? ' active' : ''); |
|
||||
pageItem.innerHTML = `<a class="page-link" href="#">${i}</a>`; |
|
||||
pageItem.addEventListener('click', (event) => { |
|
||||
event.preventDefault(); |
|
||||
currentPage = i; |
|
||||
renderTable(); |
|
||||
renderPagination(); |
|
||||
}); |
|
||||
pagination.appendChild(pageItem); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//显示报警信息详情
|
|
||||
function showWarn(row){ |
|
||||
currentEditingRow = row; |
|
||||
model_name = row.cells[1].innerText; |
|
||||
channel_name = row.cells[2].innerText; |
|
||||
create_time = row.cells[3].innerText; |
|
||||
img_path = pageData[row.rowIndex-1].img_path; |
|
||||
|
|
||||
document.getElementById('modelName').innerText = `${model_name}`; |
|
||||
document.getElementById('channleName').innerText = `${channel_name}`; |
|
||||
document.getElementById('warnTime').innerText = `${create_time}`; |
|
||||
document.getElementById('warnImg').src = `/api/warn/warn_img?path=${img_path}`; // 设置图片的 src
|
|
||||
|
|
||||
$('#showWarn').modal('show'); |
|
||||
} |
|
||||
|
|
||||
//下载视频按钮
|
|
||||
function videoWarn(row){ |
|
||||
video_path = pageData[row.rowIndex-1].video_path; |
|
||||
// const videoPlayer = document.getElementById('videoPlayer');
|
|
||||
// videoPlayer.src = `/api/warn/warn_video?path=${encodeURIComponent(video_path)}`;
|
|
||||
// videoPlayer.load(); // 确保重新加载视频
|
|
||||
// $('#showVideo').modal('show');
|
|
||||
// 创建下载链接
|
|
||||
const downloadUrl = `/api/warn/warn_video?path=${encodeURIComponent(video_path)}`; |
|
||||
const a = document.createElement('a'); |
|
||||
a.href = downloadUrl; |
|
||||
document.body.appendChild(a); |
|
||||
a.click(); |
|
||||
document.body.removeChild(a); |
|
||||
} |
|
||||
|
|
||||
//删除一条报警数据
|
|
||||
function deleteWarn(row){ |
|
||||
if (confirm('确定删除此报警吗?')) { |
|
||||
let alarmIds=[]; |
|
||||
warn_id = row.cells[0].innerText; |
|
||||
alarmIds.push(warn_id); |
|
||||
// 发送POST请求到后端
|
|
||||
fetch('/api/warn/warn_del', { |
|
||||
method: 'POST', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' |
|
||||
}, |
|
||||
body: JSON.stringify({ alarmIds: alarmIds }) |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
alert('删除成功'); |
|
||||
} else { |
|
||||
alert('删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('Error:', error); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//删除当前页的报警数据
|
|
||||
function delpageWarn(){ |
|
||||
if (confirm('确定删除此页报警吗?')) { |
|
||||
let alarmIds=[]; |
|
||||
pageData.forEach((warn) => { |
|
||||
alarmIds.push(warn.ID) |
|
||||
}); |
|
||||
// 发送POST请求到后端
|
|
||||
fetch('/api/warn/warn_del', { |
|
||||
method: 'POST', |
|
||||
headers: { |
|
||||
'Content-Type': 'application/json' |
|
||||
}, |
|
||||
body: JSON.stringify({ alarmIds: alarmIds }) |
|
||||
}) |
|
||||
.then(response => response.json()) |
|
||||
.then(data => { |
|
||||
if (data.success) { |
|
||||
alert('删除成功'); |
|
||||
} else { |
|
||||
alert('删除失败'); |
|
||||
} |
|
||||
}) |
|
||||
.catch(error => { |
|
||||
console.error('Error:', error); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,16 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
|
||||
|
{% block title %}ZFSAFE{% endblock %} |
||||
|
|
||||
|
<!-- 页面样式块 --> |
||||
|
{% block style %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面内容块 --> |
||||
|
{% block content %} |
||||
|
<h3 style="text-align: center;padding: 10px"> 功能建设中,在二期实现。。。</h3> |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面脚本块 --> |
||||
|
{% block script %} |
||||
|
{% endblock %} |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -0,0 +1,661 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
|
||||
|
{% block title %}ZFSAFE{% endblock %} |
||||
|
|
||||
|
<!-- 在此处可添加样式文件 --> |
||||
|
{% block style_link %} |
||||
|
<link href="{{ url_for('main.static', filename='css/node_tree.css') }}" rel="stylesheet"> |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面样式块 --> |
||||
|
{% block style %} |
||||
|
|
||||
|
/* 查询条件区域:使用 row 分布,输入框占满所在列 */ |
||||
|
.search-section .form-control, |
||||
|
.search-section .form-select { |
||||
|
width: 100%; |
||||
|
} |
||||
|
/* 查询条件区域,每个条件统一高度且左右间隔均等 */ |
||||
|
.search-section .col { |
||||
|
padding: 0 5px; |
||||
|
} |
||||
|
|
||||
|
/* 表格样式:统一垂直居中 */ |
||||
|
.table thead th, .table tbody td { |
||||
|
vertical-align: middle; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
/* 分页区域右对齐 */ |
||||
|
.pagination-section { |
||||
|
text-align: right; |
||||
|
padding-right: 15px; |
||||
|
} |
||||
|
|
||||
|
/* 固定行高,比如 45px,每页 10 行 */ |
||||
|
.fixed-row-height { |
||||
|
height: 45px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
/* 模态框内部最大高度,超出部分滚动 */ |
||||
|
.modal-dialog { |
||||
|
max-height: calc(100vh+20px); |
||||
|
} |
||||
|
.modal-content { |
||||
|
max-height: calc(100vh+20px); |
||||
|
} |
||||
|
.modal-body { |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
/* 这里设置页码按钮样式(可根据需要调整) */ |
||||
|
.pagination { |
||||
|
margin: 0; |
||||
|
} |
||||
|
.disabled-btn { |
||||
|
/* 禁用状态样式 */ |
||||
|
background-color: #cccccc; /* 灰色背景 */ |
||||
|
color: #666666; /* 文字颜色变浅 */ |
||||
|
cursor: not-allowed; /* 鼠标显示禁用图标 */ |
||||
|
opacity: 0.7; /* 可选:降低透明度 */ |
||||
|
|
||||
|
/* 禁用点击事件(通过 disabled 属性已实现,此样式仅增强视觉效果) */ |
||||
|
pointer-events: none; /* 可选:彻底阻止鼠标事件 */ |
||||
|
} |
||||
|
|
||||
|
.offcanvas-backdrop.show { |
||||
|
z-index: 1055; |
||||
|
} |
||||
|
|
||||
|
/* 再把 offcanvas 本身提到更高,超过 modal(modal 是 1055) */ |
||||
|
.offcanvas.show { |
||||
|
z-index: 1060; |
||||
|
} |
||||
|
/* 让所有右侧 offcanvas-end 都变成 60% 宽 */ |
||||
|
.offcanvas.offcanvas-end { |
||||
|
width: 60% !important; |
||||
|
max-width: none; /* 取消默认 max-width */ |
||||
|
} |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面内容块 --> |
||||
|
{% block content %} |
||||
|
|
||||
|
<div class="container"> |
||||
|
<!-- 查询条件区域 --> |
||||
|
<div class="search-section mb-3"> |
||||
|
<form class="row g-3 align-items-center"> |
||||
|
<!-- 每个输入框直接使用 placeholder 显示标题,水平分布 --> |
||||
|
<div class="col-3"> |
||||
|
<input type="text" class="form-control" id="testTarget" name="target" placeholder="检测目标"> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<select class="form-select" id="riskLevel" name="risk_level"> |
||||
|
<option value="">风险级别</option> |
||||
|
<option value="0">0</option> |
||||
|
<option value="1">1</option> |
||||
|
<option value="2">2</option> |
||||
|
<option value="3">3</option> |
||||
|
<option value="4">4</option> |
||||
|
<option value="5">5</option> |
||||
|
<option value="6">6</option> |
||||
|
<option value="7">7</option> |
||||
|
<option value="8">8</option> |
||||
|
<option value="9">9</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<select class="form-select" id="useModel" name="model"> |
||||
|
<option value="">使用模型</option> |
||||
|
<option value="1">DeepSeek</option> |
||||
|
<option value="2">GPT-O3</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="col-2" > |
||||
|
<input type="date" id="startTime" class="form-control" name="start_time" placeholder="开始时间"> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<input type="date" id="endTime" class="form-control" name="end_time" placeholder="结束时间"> |
||||
|
</div> |
||||
|
<div class="col-auto"> |
||||
|
<button type="button" class="btn btn-primary" id="btnQuery">查询</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 表格区域 --> |
||||
|
<div class="table-section mb-3"> |
||||
|
<table class="table table-bordered table-hover" id="histasksTable" style="width: 100%; table-layout: fixed;"> |
||||
|
<thead class="table-light"> |
||||
|
<tr> |
||||
|
<th style="width: 60px;">ID</th> |
||||
|
<th style="width: 20%;">检测目标</th> |
||||
|
<th style="width: 15%;">开始时间</th> |
||||
|
<th style="width: 15%;">结束时间</th> |
||||
|
<th style="width: 15%;">风险等级</th> |
||||
|
<th style="width: 15%;">使用模型</th> |
||||
|
<th style="width: 100px;">操作</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="histasksTbody"> |
||||
|
<!-- 数据由JS动态填充,固定10行一页 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 分页控件区域 --> |
||||
|
<div class="pagination-section mb-3"> |
||||
|
<nav> |
||||
|
<ul class="pagination pagination-sm justify-content-end" id="histasksPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="prevPage">上一页</a> |
||||
|
</li> |
||||
|
<!-- 页码动态生成 --> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="nextPage">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 模态框:显示 task_manager.html 中的中间 tab 页内容 --> |
||||
|
<!-- 这里只显示节点树、测试指令、漏洞数据三个 tab 页 --> |
||||
|
<div class="modal fade" id="viewModal" tabindex="-1" aria-labelledby="viewModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog modal-xl"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<h5 class="modal-title" id="viewModalLabel">任务详情</h5> |
||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button> |
||||
|
</div> |
||||
|
<div class="modal-body p-0"> |
||||
|
<!-- 这里仅嵌入中间 tab 页部分 --> |
||||
|
<div class="tab-wrapper"> |
||||
|
<ul class="nav nav-tabs" id="myTab" role="tablist"> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button class="nav-link active" id="nodeTreeTab" data-bs-toggle="tab" data-bs-target="#nodeTree" type="button" role="tab" aria-controls="nodeTree" aria-selected="true"> |
||||
|
节点树 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button class="nav-link" id="testInstructionsTab" data-bs-toggle="tab" data-bs-target="#testInstructions" type="button" role="tab" aria-controls="testInstructions" aria-selected="false"> |
||||
|
测试指令 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button class="nav-link" id="vulnerabilitiesTab" data-bs-toggle="tab" data-bs-target="#vulnerabilities" type="button" role="tab" aria-controls="vulnerabilities" aria-selected="false"> |
||||
|
漏洞数据 |
||||
|
</button> |
||||
|
</li> |
||||
|
</ul> |
||||
|
<div class="tab-content" id="myTabContent"> |
||||
|
<!-- 节点树 --> |
||||
|
<div class="tab-pane fade show active p-3 h-100" id="nodeTree" role="tabpanel" aria-labelledby="nodeTreeTab"> |
||||
|
<div class="row h-100"> |
||||
|
<!-- 左侧:节点树区域 --> |
||||
|
<div class="col-8 h-100"> |
||||
|
<div class="node-tree-area" id="nodeTreeContainer" style="height: 100%; overflow-y: auto; position: relative; background-color: #f8f9fa;"> |
||||
|
<!-- 固定刷新按钮 --> |
||||
|
<!-- <div class="refresh-container" style="position: absolute; top: 5px; left: 5px; z-index: 100;">--> |
||||
|
<!-- <button class="tree-refresh btn btn-primary btn-sm" id="btnRefresh" title="刷新节点树">↻</button>--> |
||||
|
<!-- </div>--> |
||||
|
<!-- 节点树内容 --> |
||||
|
<div id="treeContent" class="tree-content" style="padding-top: 40px;"> |
||||
|
<p id="treeLoadingMsg" style="text-align:center;">加载中...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 右侧:节点信息与操作 --> |
||||
|
<div class="col-4 h-100"> |
||||
|
<div class="node-info-area mb-3" style="padding: 10px;"> |
||||
|
<h5>节点信息</h5> |
||||
|
<p><strong>节点名称:</strong> <span id="nodeName">-</span></p> |
||||
|
<p><strong>测试状态:</strong> <span id="testStatus">-</span></p> |
||||
|
<p><strong>漏洞类型:</strong> <span id="node_vulType">-</span></p> |
||||
|
<p><strong>漏洞级别:</strong> <span id="node_vulLevel">-</span></p> |
||||
|
<p><strong>工作状态:</strong> <span id="node_bwork">-</span></p> |
||||
|
<p><strong>执行状态:</strong> <span id="node_workstatus">-</span></p> |
||||
|
</div> |
||||
|
<div class="node-actions" style="padding: 0 10px 10px;"> |
||||
|
<div class="row mb-2"> |
||||
|
<div class="col-12"> |
||||
|
<button class="btn btn-primary w-100" id="btnViewInstr">查看指令</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 测试指令 --> |
||||
|
<div class="tab-pane fade p-3" id="testInstructions" role="tabpanel" aria-labelledby="testInstructionsTab"> |
||||
|
<div class="row search-area mb-2"> |
||||
|
<div class="col-4"> |
||||
|
<input type="text" class="form-control" id="instrNodeName" placeholder="节点名称"> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<button class="btn btn-primary" id="instrSearchBtn">查询</button> |
||||
|
<button class="btn btn-primary" id="instrExportBtn">导出</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<table class="table table-bordered table-hover" id="instrTable" style="width: 100%; table-layout: fixed;"> |
||||
|
<colgroup> |
||||
|
<col style="width: 5%;"> |
||||
|
<col style="width: 15%;"> |
||||
|
<col style="width: 5%;"> |
||||
|
<col style="width: 30%;" class="wrap-cell"> |
||||
|
<col style="width: auto;"> |
||||
|
</colgroup> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>序号</th> |
||||
|
<th>节点路径</th> |
||||
|
<th>指序</th> |
||||
|
<th>执行指令</th> |
||||
|
<th>执行结果</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<!-- 默认显示10行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination" id="instrPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="instrPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="instrNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
<!-- 漏洞数据 --> |
||||
|
<div class="tab-pane fade p-3" id="vulnerabilities" role="tabpanel" aria-labelledby="vulnerabilitiesTab"> |
||||
|
<div class="row search-area mb-2"> |
||||
|
<div class="col-3"> |
||||
|
<input type="text" class="form-control" id="vulNodeName" placeholder="节点名称"> |
||||
|
</div> |
||||
|
<div class="col-3"> |
||||
|
<input type="text" class="form-control" id="vulType" placeholder="漏洞类型"> |
||||
|
</div> |
||||
|
<div class="col-3"> |
||||
|
<select class="form-select" id="vulLevel"> |
||||
|
<option value="">漏洞级别</option> |
||||
|
<option value="低危">低危</option> |
||||
|
<option value="中危">中危</option> |
||||
|
<option value="高危">高危</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<button class="btn btn-primary" id="vulSearchBtn">查询</button> |
||||
|
<button class="btn btn-primary" id="vulExportBtn">导出</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<table class="table table-bordered table-hover" id="vulTable"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th class="seq-col">序号</th> |
||||
|
<th>节点路径</th> |
||||
|
<th>漏洞类型</th> |
||||
|
<th>漏洞级别</th> |
||||
|
<th>漏洞说明</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<!-- 默认显示10行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination" id="vulPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="vulPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="vulNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 模态框 footer --> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 查看节点执行指令offcanvas --modal 改---> |
||||
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="instrCanvas" aria-labelledby="instrCanvasLabel"> |
||||
|
<div class="offcanvas-header"> |
||||
|
<!-- 返回按钮 --> |
||||
|
<!-- <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas">--> |
||||
|
<!-- ←--> |
||||
|
<!-- </button>--> |
||||
|
<h5 class="offcanvas-title" id="instrOffcanvasLabel">测试指令</h5> |
||||
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button> |
||||
|
</div> |
||||
|
<div class="offcanvas-body"> |
||||
|
<!-- 返回按钮 --> |
||||
|
<div class="mb-3"> |
||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="offcanvas"> |
||||
|
← 返回 |
||||
|
</button> |
||||
|
</div> |
||||
|
<!-- 新增一个提示容器 --> |
||||
|
<!-- <div id="loadingMsg" style="text-align: center; padding: 10px;">请稍后,数据获取中...</div>--> |
||||
|
<div id="loadingMsg" class="text-center mb-3">请稍后,数据获取中...</div> |
||||
|
<!-- 页签(已执行、待执行) --> |
||||
|
<ul class="nav nav-tabs" id="instrTab" role="tablist"> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link active" |
||||
|
id="doneInstrTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#doneInstr" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="doneInstr" |
||||
|
aria-selected="true" |
||||
|
> |
||||
|
已执行 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link" |
||||
|
id="todoInstrTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#todoInstr" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="todoInstr" |
||||
|
aria-selected="false" |
||||
|
> |
||||
|
待执行 |
||||
|
</button> |
||||
|
</li> |
||||
|
</ul> |
||||
|
<div class="tab-content pt-3" id="instrTabContent"> |
||||
|
<!-- 已执行指令表格 --> |
||||
|
<div |
||||
|
class="tab-pane fade show active" |
||||
|
id="doneInstr" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="doneInstrTab" |
||||
|
> |
||||
|
<table class="table table-bordered table-hover"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th style="width: 50px;">序号</th> |
||||
|
<th>执行指令</th> |
||||
|
<th>执行时间</th> |
||||
|
<th>执行结果</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="doneInstrTbody"> |
||||
|
<!-- 动态生成,固定 10 行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination justify-content-end" id="doneInstrPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="doneInstrPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="doneInstrNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
<!-- 待执行指令表格 --> |
||||
|
<div |
||||
|
class="tab-pane fade" |
||||
|
id="todoInstr" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="todoInstrTab" |
||||
|
> |
||||
|
<table class="table table-bordered table-hover"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th style="width: 50px;">序号</th> |
||||
|
<th>待执行指令</th> |
||||
|
<th style="width: 80px;">操作</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="todoInstrTbody"> |
||||
|
<!-- 动态生成,固定 10 行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination justify-content-end" id="todoInstrPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="todoInstrPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="todoInstrNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 操作按钮 --> |
||||
|
<div class="mt-4 d-flex justify-content-end"> |
||||
|
<button type="button" class="btn btn-primary" id="btnExport">导出</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面脚本块 --> |
||||
|
{% block script %} |
||||
|
<script src="{{ url_for('main.static', filename='scripts/task_modal.js') }}"></script> |
||||
|
<script> |
||||
|
// 全局变量 |
||||
|
let cur_task_id = 0; |
||||
|
let allHistasks = []; |
||||
|
let currentPage = 1; |
||||
|
const pageSize = 10; |
||||
|
|
||||
|
// 分页渲染函数 |
||||
|
function renderHistasksTable(page) { |
||||
|
currentPage = page; |
||||
|
const tbody = document.getElementById("histasksTbody"); |
||||
|
const startIndex = (page - 1) * pageSize; |
||||
|
const endIndex = startIndex + pageSize; |
||||
|
const pageData = allHistasks.slice(startIndex, endIndex); |
||||
|
//select ID,task_target,safe_rank,llm_type,start_time,end_time from task |
||||
|
tbody.innerHTML = ""; |
||||
|
pageData.forEach((task, i) => { |
||||
|
const tr = document.createElement("tr"); |
||||
|
// 每个单元格创建时使用 textContent,确保固定行高,如有需要也可增加 class "fixed-row-height" |
||||
|
const tdId = document.createElement("td"); |
||||
|
tdId.textContent = task[0]; |
||||
|
tr.appendChild(tdId); |
||||
|
|
||||
|
const tdTarget = document.createElement("td"); |
||||
|
tdTarget.textContent = task[1]; |
||||
|
tr.appendChild(tdTarget); |
||||
|
|
||||
|
const tdStart = document.createElement("td"); |
||||
|
tdStart.textContent = task[4] || ""; |
||||
|
tr.appendChild(tdStart); |
||||
|
|
||||
|
const tdEnd = document.createElement("td"); |
||||
|
tdEnd.textContent = task[5] || ""; |
||||
|
tr.appendChild(tdEnd); |
||||
|
|
||||
|
const tdRisk = document.createElement("td"); |
||||
|
tdRisk.textContent = (task[2] === 0) ? "安全" : "存在风险"; |
||||
|
tr.appendChild(tdRisk); |
||||
|
|
||||
|
const tdModel = document.createElement("td"); |
||||
|
model_test = "" |
||||
|
if(task[3]===1){ |
||||
|
model_test="DeepSeek"; |
||||
|
} |
||||
|
else if(task[3]===2){ |
||||
|
model_test="GPT-O3"; |
||||
|
} |
||||
|
else{ |
||||
|
model_test="其他模型"; |
||||
|
} |
||||
|
tdModel.textContent = model_test; |
||||
|
tr.appendChild(tdModel); |
||||
|
|
||||
|
const tdAction = document.createElement("td"); |
||||
|
// 查看按钮(点击后弹出 modal) |
||||
|
const btnView = document.createElement("button"); |
||||
|
btnView.className = "btn btn-outline-info btn-sm"; |
||||
|
btnView.textContent = "查看"; |
||||
|
btnView.onclick = () => openViewModal(task[0]); |
||||
|
tdAction.appendChild(btnView); |
||||
|
// 删除按钮(示例) |
||||
|
const btnDel = document.createElement("button"); |
||||
|
btnDel.className = "btn btn-outline-danger btn-sm ms-1"; |
||||
|
btnDel.textContent = "删除"; |
||||
|
btnDel.onclick = () => confirmDeleteTask(task[0]); |
||||
|
tdAction.appendChild(btnDel); |
||||
|
|
||||
|
tr.appendChild(tdAction); |
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
|
||||
|
// 补空行 |
||||
|
for (let i = pageData.length; i < pageSize; i++) { |
||||
|
const tr = document.createElement("tr"); |
||||
|
for (let j = 0; j < 7; j++) { |
||||
|
const td = document.createElement("td"); |
||||
|
td.textContent = "\u00A0"; |
||||
|
tr.appendChild(td); |
||||
|
} |
||||
|
tbody.appendChild(tr); |
||||
|
} |
||||
|
updatePagination(); |
||||
|
} |
||||
|
|
||||
|
// 更新分页按钮 |
||||
|
function updatePagination() { |
||||
|
const totalPages = Math.ceil(allHistasks.length / pageSize); |
||||
|
document.getElementById("prevPage").dataset.page = currentPage > 1 ? currentPage - 1 : 1; |
||||
|
document.getElementById("nextPage").dataset.page = currentPage < totalPages ? currentPage + 1 : totalPages; |
||||
|
} |
||||
|
|
||||
|
// 分页按钮点击事件 |
||||
|
document.getElementById("prevPage").addEventListener("click", function(e) { |
||||
|
e.preventDefault(); |
||||
|
const page = parseInt(this.dataset.page, 10); |
||||
|
renderHistasksTable(page); |
||||
|
}); |
||||
|
document.getElementById("nextPage").addEventListener("click", function(e) { |
||||
|
e.preventDefault(); |
||||
|
const page = parseInt(this.dataset.page, 10); |
||||
|
renderHistasksTable(page); |
||||
|
}); |
||||
|
|
||||
|
// 查询按钮事件,调用 /api/task/histasks 接口获取数据(示例) |
||||
|
document.getElementById("btnQuery").addEventListener("click", async function() { |
||||
|
// 此处可拼接获取表单数据条件,示例直接调用接口 |
||||
|
/* |
||||
|
target_name = data.get("target_name") |
||||
|
safe_rank = data.get("safe_rank") |
||||
|
llm_type = data.get("llm_type") |
||||
|
start_time= data.get("start_time") |
||||
|
end_time= data.get("end_time") |
||||
|
* */ |
||||
|
const target_name = document.getElementById("testTarget").value.trim(); |
||||
|
const safe_rank = document.getElementById("riskLevel").value; |
||||
|
const llm_type = document.getElementById("useModel").value; |
||||
|
const start_time = document.getElementById("startTime").value; |
||||
|
const end_time = document.getElementById("endTime").value; |
||||
|
|
||||
|
try { |
||||
|
const res = await fetch("/api/task/histasks", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({target_name,safe_rank,llm_type,start_time,end_time}) |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
allHistasks = data.his_tasks || []; |
||||
|
renderHistasksTable(1); |
||||
|
} catch (error) { |
||||
|
console.error("查询任务记录出错:", error); |
||||
|
alert("查询失败!"); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// “查看详情”按钮事件(统一使用模态框显示 task_manager.html) |
||||
|
async function openViewModal(task_id) { |
||||
|
cur_task_id = task_id; |
||||
|
const viewModal = new bootstrap.Modal(document.getElementById("viewModal"), { keyboard: false }); |
||||
|
viewModal.show(); |
||||
|
//查询节点树数据 |
||||
|
loadNodeTree(task_id); |
||||
|
//查询指令数据 |
||||
|
searchInstructions(1); |
||||
|
//查询漏洞数据 |
||||
|
searchVulnerabilities(1); |
||||
|
} |
||||
|
|
||||
|
// 删除任务的示例函数 |
||||
|
async function confirmDeleteTask(task_id) { |
||||
|
if (confirm("确认删除任务 " + task_id + " 吗?")) { |
||||
|
// 发送删除请求... |
||||
|
try { |
||||
|
const res = await fetch("/api/task/deltask", { |
||||
|
method: "POST", |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
body: JSON.stringify({task_id}), |
||||
|
}); |
||||
|
if (!res.ok) { |
||||
|
const errorData = await res.json(); |
||||
|
throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
} |
||||
|
const data = await res.json(); |
||||
|
bsuccess = data.bsuccess; |
||||
|
if(bsuccess){ |
||||
|
// 1. 从前端缓存里删除这条任务 |
||||
|
allHistasks = allHistasks.filter(t => t[0] !== task_id); |
||||
|
|
||||
|
// 2. 重新渲染当前页 |
||||
|
// 注意:如果删除后当前页已经没有任何数据了,可以让 currentPage-- |
||||
|
const totalPages = Math.ceil(allHistasks.length / pageSize) || 1; |
||||
|
if (currentPage > totalPages) { |
||||
|
currentPage = totalPages; |
||||
|
} |
||||
|
renderHistasksTable(currentPage); |
||||
|
|
||||
|
// (可选)如果你想做局部删除,而不重画整表,也可以直接: |
||||
|
// btnEl.closest("tr").remove(); |
||||
|
alert("删除成功") |
||||
|
} |
||||
|
else{ |
||||
|
alert("删除失败:",data.error) |
||||
|
return false; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("删除任务数据异常:", error); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 页面加载时可以自动调用查询接口加载数据 |
||||
|
document.addEventListener("DOMContentLoaded", () => { |
||||
|
// 可自动加载数据,或者等待用户点击查询 |
||||
|
document.getElementById("btnQuery").click(); |
||||
|
//renderHistasksTable(1); |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
{% endblock %} |
@ -0,0 +1,172 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
|
||||
|
{% block title %}ZFSAFE{% endblock %} |
||||
|
|
||||
|
<!-- 页面样式块 --> |
||||
|
{% block style %} |
||||
|
#cookieInfo { |
||||
|
font-size: 0.9rem; |
||||
|
} |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面内容块 --> |
||||
|
{% block content %} |
||||
|
<div class="container mt-4"> |
||||
|
<!-- 测试目标输入框 --> |
||||
|
<div class="mb-3"> |
||||
|
<label for="testTarget" class="form-label"> |
||||
|
测试目标: <span class="text-danger">*</span> |
||||
|
</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
id="testTarget" |
||||
|
placeholder="输入测试目标" |
||||
|
required |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- cookie 信息输入框,左缩进,非必填 --> |
||||
|
<div class="mb-3"> |
||||
|
<div style="margin-left: 20px;margin-bottom: 10px"> |
||||
|
<label class="fw-bold" style="font-size:0.9rem">cookie信息 (非必填):</label> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
id="cookieInfo" |
||||
|
placeholder="输入cookie信息" |
||||
|
/> |
||||
|
</div> |
||||
|
<!-- 模型选择 --> |
||||
|
<div style="margin-left: 20px; margin-bottom: 10px"> |
||||
|
<label class="fw-bold" style="font-size:0.9rem">模型选择:</label> |
||||
|
<select class="form-select" id="modelSelect" style="font-size:0.9rem"> |
||||
|
<option value="DeepSeek">DeepSeek</option> |
||||
|
<option value="GPT-O3">GPT-O3</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 测试模式:全自动,半自动 --> |
||||
|
<div style="margin-left: 20px"> |
||||
|
<label class="fw-bold" style="font-size:0.9rem">测试模式: </label> |
||||
|
<div class="form-check form-check-inline"> |
||||
|
<input |
||||
|
class="form-check-input" |
||||
|
type="radio" |
||||
|
name="testMode" |
||||
|
id="autoMode" |
||||
|
value="auto" |
||||
|
/> |
||||
|
<label class="form-check-label" for="autoMode" |
||||
|
>自动执行</label |
||||
|
> |
||||
|
</div> |
||||
|
<div class="form-check form-check-inline"> |
||||
|
<input |
||||
|
class="form-check-input" |
||||
|
type="radio" |
||||
|
name="testMode" |
||||
|
id="manualMode" |
||||
|
value="manual" |
||||
|
checked |
||||
|
/> |
||||
|
<label class="form-check-label" for="manualMode" |
||||
|
>人工确认</label |
||||
|
> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 开始按钮,右对齐 --> |
||||
|
<div class="mb-3 text-end"> |
||||
|
<button id="startButton" class="btn btn-primary">开始</button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 使用说明 --> |
||||
|
<div class="mt-4"> |
||||
|
<label for="usage" class="form-label">使用说明:</label> |
||||
|
<textarea class="form-control" id="usage" rows="10"> |
||||
|
1.测试模式分为两种:自动执行和人工确认(单步模式),模式的切换只允许在暂停情况下调整; |
||||
|
2.暂停不停止正在执行指令,指令执行后会根据当前参数的设定执行下一步工作; |
||||
|
3.单步的作用是将节点中:待执行的指令进行执行,待提交LLM的数据提交LLM; |
||||
|
4.顶部的单步是针对整个任务的单步执行,若节点执行状态不一致,会存在某些节点执行测试指令,某些节点提交llm任务的情况,节点树区域的控制是针对该节点的控制; |
||||
|
5.由于LLM的不一致性,会存在无执行任务,但没有标记完成的任务节点,可作为已完成论; |
||||
|
6.在单步模式下,若某指令执行的结果错误,可以在查看MSG功能里,修改待提交的执行结果,来保障测试的顺利推进; |
||||
|
7.对于已经验证漏洞存在的节点,若LLM返回了测试指令,但没有必要继续验证的话,可以点击该节点的暂停按钮,暂停该节点的测试推进; |
||||
|
8.本工具仅限于在得到授权的前提下使用,若目标重要性很高,请使用单步模式,确认测试指令的影响后执行。 |
||||
|
</textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面脚本块 --> |
||||
|
{% block script %} |
||||
|
<script> |
||||
|
let curmodel = 1 //0-腾讯云,1-DS,2-2233.ai,3-GPT |
||||
|
// 为模型选择下拉框绑定选择事件 |
||||
|
document.getElementById("modelSelect").addEventListener("change", function() { |
||||
|
const selectedModel = this.value; |
||||
|
console.log("选择的模型为:" + selectedModel); |
||||
|
// 可根据需要进一步处理选中模型 |
||||
|
if(selectedModel === "DeepSeek"){ |
||||
|
curmodel = 1 |
||||
|
}else if(selectedModel === "GPT-O3"){ |
||||
|
curmodel = 2 //暂时用2233.ai接口代替o3 |
||||
|
} |
||||
|
else { |
||||
|
alert("模型参数存在问题,请联系管理员!!"); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
document.getElementById("startButton").addEventListener("click", async () => { |
||||
|
//取值 |
||||
|
const testTarget = document.getElementById("testTarget").value; |
||||
|
const cookieInfo = document.getElementById("cookieInfo").value; |
||||
|
let workType = 0; //0-人工,1-自动 |
||||
|
const selected = document.getElementById('manualMode').checked; |
||||
|
if(selected){ |
||||
|
workType = 0; |
||||
|
}else { |
||||
|
workType = 1; |
||||
|
} |
||||
|
// 测试目标不能为空 |
||||
|
if (!testTarget) { |
||||
|
alert("测试目标不能为空!"); |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
const response = await fetch("/api/task/start", { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
}, |
||||
|
body: JSON.stringify({ |
||||
|
testTarget, |
||||
|
cookieInfo, |
||||
|
workType, |
||||
|
curmodel, |
||||
|
}), |
||||
|
}); |
||||
|
|
||||
|
// // 状态码校验 |
||||
|
// if (!response.ok) { |
||||
|
// const errorData = await res.json(); |
||||
|
// throw new Error(errorData.error || `HTTP错误 ${res.status}`); |
||||
|
// } |
||||
|
|
||||
|
// 如果后端返回了重定向,则前端自动跳转 |
||||
|
if (response.redirected) { |
||||
|
window.location.href = response.url; |
||||
|
} else { //除了跳转,都是返回的错误信息 |
||||
|
const data = await response.json(); |
||||
|
if (data.error) { |
||||
|
alert(data.error); |
||||
|
} |
||||
|
} |
||||
|
} catch (error) { |
||||
|
console.error("Error:", error); |
||||
|
alert("请求出错,请稍后再试!"); |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
{% endblock %} |
@ -1,176 +0,0 @@ |
|||||
<!DOCTYPE html> |
|
||||
<html lang="en"> |
|
||||
<head> |
|
||||
<meta charset="UTF-8"> |
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
||||
<title>ZFBOX</title> |
|
||||
<link rel="stylesheet" href="{{ url_for('main.static', filename='css/bootstrap.min.css') }}"> |
|
||||
<link href="../static/resources/css/bootstrap.min.css" rel="stylesheet"> |
|
||||
<style> |
|
||||
body { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
height: 100vh; |
|
||||
} |
|
||||
|
|
||||
header { |
|
||||
background-color: #007bff; |
|
||||
color: white; |
|
||||
padding: 10px 0; |
|
||||
} |
|
||||
|
|
||||
.navbar-nav .nav-link { |
|
||||
color: white; |
|
||||
} |
|
||||
|
|
||||
main { |
|
||||
display: flex; |
|
||||
flex: 1; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.tree-view { |
|
||||
border-right: 1px solid #ddd; |
|
||||
padding: 10px; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
|
|
||||
.video-content { |
|
||||
padding: 10px; |
|
||||
overflow-y: auto; |
|
||||
flex: 1; |
|
||||
} |
|
||||
|
|
||||
footer { |
|
||||
background-color: #f8f9fa; |
|
||||
text-align: center; |
|
||||
padding: 10px 0; |
|
||||
} |
|
||||
|
|
||||
.video-frame { |
|
||||
background-color: #f8f9fa; |
|
||||
border: 1px solid #ddd; |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
|
|
||||
.video-frame img { |
|
||||
width: 100%; |
|
||||
height: auto; |
|
||||
} |
|
||||
|
|
||||
.video-grid { |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(2, 1fr); |
|
||||
gap: 10px; |
|
||||
} |
|
||||
|
|
||||
.video-grid.eight { |
|
||||
grid-template-columns: repeat(4, 1fr); |
|
||||
} |
|
||||
|
|
||||
.toggle-buttons { |
|
||||
margin-bottom: 10px; |
|
||||
} |
|
||||
</style> |
|
||||
</head> |
|
||||
<body> |
|
||||
<header> |
|
||||
<nav class="navbar navbar-expand-lg navbar-light"> |
|
||||
<div class="container-fluid"> |
|
||||
<a class="navbar-brand" href="#">智凡BOX</a> |
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> |
|
||||
<span class="navbar-toggler-icon"></span> |
|
||||
</button> |
|
||||
<div class="collapse navbar-collapse" id="navbarNav"> |
|
||||
<ul class="navbar-nav"> |
|
||||
<li class="nav-item"> |
|
||||
<a class="nav-link" href="#">实时预览</a> |
|
||||
</li> |
|
||||
<li class="nav-item"> |
|
||||
<a class="nav-link" href="#">通道管理</a> |
|
||||
</li> |
|
||||
<li class="nav-item"> |
|
||||
<a class="nav-link" href="#">算法管理</a> |
|
||||
</li> |
|
||||
<li class="nav-item"> |
|
||||
<a class="nav-link" href="#">系统管理</a> |
|
||||
</li> |
|
||||
<li class="nav-item"> |
|
||||
<a class="nav-link" href="#">用户管理</a> |
|
||||
</li> |
|
||||
</ul> |
|
||||
<ul class="navbar-nav ms-auto"> |
|
||||
<li class="nav-item"> |
|
||||
<a class="nav-link" href="#">张三 退出</a> |
|
||||
</li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
</div> |
|
||||
</nav> |
|
||||
</header> |
|
||||
<main> |
|
||||
<div class="tree-view col-md-3"> |
|
||||
<ul class="list-group"> |
|
||||
<li class="list-group-item">一区 |
|
||||
<ul class="list-group"> |
|
||||
<li class="list-group-item">北门通道一</li> |
|
||||
<li class="list-group-item">南门通道二</li> |
|
||||
<li class="list-group-item">通道三</li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
<li class="list-group-item">二区域 |
|
||||
<ul class="list-group"> |
|
||||
<li class="list-group-item">通道一</li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
<div class="video-content col-md-9"> |
|
||||
<div class="toggle-buttons"> |
|
||||
<button id="fourView" class="btn btn-primary">四画面</button> |
|
||||
<button id="eightView" class="btn btn-secondary">八画面</button> |
|
||||
</div> |
|
||||
<div id="videoGrid" class="video-grid"> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</main> |
|
||||
<footer> |
|
||||
© 2024 ZFKJ All Rights Reserved |
|
||||
</footer> |
|
||||
|
|
||||
<script src="{{ url_for('main.static', filename='js/bootstrap.bundle.min.js') }}"></script> |
|
||||
<script> |
|
||||
document.getElementById('fourView').addEventListener('click', function() { |
|
||||
const videoGrid = document.getElementById('videoGrid'); |
|
||||
videoGrid.classList.remove('eight'); |
|
||||
videoGrid.classList.add('four'); |
|
||||
videoGrid.innerHTML = ` |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
`; |
|
||||
}); |
|
||||
|
|
||||
document.getElementById('eightView').addEventListener('click', function() { |
|
||||
const videoGrid = document.getElementById('videoGrid'); |
|
||||
videoGrid.classList.remove('four'); |
|
||||
videoGrid.classList.add('eight'); |
|
||||
videoGrid.innerHTML = ` |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
<div class="video-frame"><img src="../static/resources/images/video_placeholder.png" alt="Video Stream"></div> |
|
||||
`; |
|
||||
}); |
|
||||
</script> |
|
||||
</body> |
|
||||
</html> |
|
@ -0,0 +1,444 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
|
||||
|
{% block title %}ZFSAFE{% endblock %} |
||||
|
|
||||
|
<!-- 在此处可添加样式文件 --> |
||||
|
{% block style_link %} |
||||
|
<link href="{{ url_for('main.static', filename='css/node_tree.css') }}" rel="stylesheet"> |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面样式块 --> |
||||
|
{% block style %} |
||||
|
/* 搜索条件区域居中 */ |
||||
|
.search-area { |
||||
|
text-align: center; |
||||
|
margin-bottom: 15px; |
||||
|
} |
||||
|
/* 表格样式 */ |
||||
|
.table thead th { |
||||
|
text-align: center; /* 表头居中 */ |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
.table tbody td { |
||||
|
vertical-align: middle; |
||||
|
padding: 0.3rem 0.5rem; /* 缩小单元格上下内边距 */ |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
/* 序号列宽度较小 */ |
||||
|
.table thead th.seq-col, |
||||
|
.table tbody td.seq-col { |
||||
|
width: 50px; |
||||
|
text-align: center; |
||||
|
} |
||||
|
/* 分页控件右对齐 */ |
||||
|
.pagination { |
||||
|
justify-content: end; |
||||
|
} |
||||
|
/* 左侧任务列表整体样式 */ |
||||
|
.task-list { |
||||
|
height: calc(100vh - 210px); |
||||
|
overflow-y: auto; |
||||
|
background-color: #fff; |
||||
|
padding: 5px; |
||||
|
border-right: 1px solid #ddd; |
||||
|
} |
||||
|
/* 每个任务项样式 */ |
||||
|
.task-item { |
||||
|
padding: 5px; |
||||
|
margin-bottom: 5px; |
||||
|
background-color: #f9f9f9; |
||||
|
border: 1px solid #eee; |
||||
|
border-radius: 4px; |
||||
|
cursor: pointer; |
||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease; |
||||
|
} |
||||
|
/* 任务项选中状态样式 */ |
||||
|
.task-item.selected { |
||||
|
background-color: #e6f7ff; |
||||
|
border: 1px solid #1890ff; |
||||
|
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.2); |
||||
|
} |
||||
|
.task-item:hover { |
||||
|
background-color: #e6f7ff; |
||||
|
} |
||||
|
.task-item.selected { |
||||
|
border-color: #1890ff; |
||||
|
background-color: #bae7ff; |
||||
|
} |
||||
|
/* 任务目标样式,第一行 */ |
||||
|
.task-target { |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
/* 任务状态,第二行,有缩进 */ |
||||
|
.task-status { |
||||
|
margin-left: 10px; |
||||
|
color: #666; |
||||
|
font-size: 0.8rem; |
||||
|
} |
||||
|
|
||||
|
/* 定义一个全高容器,与左侧任务列表高度保持一致 */ |
||||
|
.full-height { |
||||
|
height: calc(100vh - 210px); |
||||
|
} |
||||
|
|
||||
|
/* 右侧区域采用 flex 布局,垂直排列 */ |
||||
|
.right-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
/* 基本信息区域,保持固定高度 */ |
||||
|
.basic-info { |
||||
|
/* 根据需要设定高度,或保持内容自适应 */ |
||||
|
flex: 0 0 auto; |
||||
|
} |
||||
|
|
||||
|
/* Tab 内容区域占满剩余空间 */ |
||||
|
.tab-wrapper { |
||||
|
flex: 1 1 auto; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
overflow: hidden; /* 防止内部溢出 */ |
||||
|
} |
||||
|
|
||||
|
/* 导航部分根据内容高度 */ |
||||
|
#myTab { |
||||
|
flex: 0 0 auto; |
||||
|
} |
||||
|
|
||||
|
.tab-content{ |
||||
|
flex: 1 1 auto; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.disabled-btn { |
||||
|
/* 禁用状态样式 */ |
||||
|
background-color: #cccccc; /* 灰色背景 */ |
||||
|
color: #666666; /* 文字颜色变浅 */ |
||||
|
cursor: not-allowed; /* 鼠标显示禁用图标 */ |
||||
|
opacity: 0.7; /* 可选:降低透明度 */ |
||||
|
|
||||
|
/* 禁用点击事件(通过 disabled 属性已实现,此样式仅增强视觉效果) */ |
||||
|
pointer-events: none; /* 可选:彻底阻止鼠标事件 */ |
||||
|
} |
||||
|
|
||||
|
{% endblock %} |
||||
|
{% block modal %} |
||||
|
{% include 'task_manager_modal.html' %} |
||||
|
{% endblock %} |
||||
|
<!-- 页面内容块 --> |
||||
|
{% block content %} |
||||
|
<div class="container mt-4"> |
||||
|
<div class="container-fluid"> |
||||
|
<div class="row"> |
||||
|
<!-- 左侧:任务列表 --> |
||||
|
<div class="col-2 task-list" id="taskList"> |
||||
|
<!-- 动态生成的任务项将插入此处 --> |
||||
|
<p>加载中...</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧:任务详情 --> |
||||
|
<div class="col-10 full-height right-container"> |
||||
|
<!-- 上方:基本信息 --> |
||||
|
<div class="row basic-info"> |
||||
|
<div class="col-9"> |
||||
|
<div class="mb-2"> |
||||
|
<label class="fw-bold">测试目标: </label> |
||||
|
<span id="detailTestTarget">192.168.1.110</span> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-3"> |
||||
|
<label class="fw-bold">工作状态: </label> |
||||
|
<span id="detailTestStatus">执行中</span> |
||||
|
</div> |
||||
|
<div class="col-3"> |
||||
|
<label class="fw-bold">安全情况: </label> |
||||
|
<span id="detailSafeStatus">安全</span> |
||||
|
</div> |
||||
|
<div class="col-6"> |
||||
|
<label class="fw-bold">测试模式: </label> |
||||
|
<div class="form-check form-check-inline"> |
||||
|
<input |
||||
|
class="form-check-input" |
||||
|
type="radio" |
||||
|
name="testMode" |
||||
|
id="autoMode" |
||||
|
value="auto" |
||||
|
checked |
||||
|
/> |
||||
|
<label class="form-check-label" for="autoMode" |
||||
|
>自动执行</label |
||||
|
> |
||||
|
</div> |
||||
|
<div class="form-check form-check-inline"> |
||||
|
<input |
||||
|
class="form-check-input" |
||||
|
type="radio" |
||||
|
name="testMode" |
||||
|
id="manualMode" |
||||
|
value="manual" |
||||
|
/> |
||||
|
<label class="form-check-label" for="manualMode" |
||||
|
>人工确认</label |
||||
|
> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- <div class="col-2" style="display: flex; justify-content: center; align-items: center"> --> |
||||
|
<div class="col-3 d-flex justify-content-center align-items-center"> |
||||
|
<!-- 按钮 (联动测试状态示例: 执行中->暂停, 暂停中->继续, 已结束->重启) --> |
||||
|
<button class="btn btn-primary btn-block" id="actionButton">暂停</button> |
||||
|
<button class="btn btn-primary btn-block m-2" id="one_step">单步</button> |
||||
|
<button class="btn btn-danger btn-block m-2" id="btnTaskOver">结束</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 下方:Tab 页 --> |
||||
|
<div class="tab-wrapper"> |
||||
|
<ul class="nav nav-tabs" id="myTab" role="tablist"> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link active" |
||||
|
id="nodeTreeTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#nodeTree" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="nodeTree" |
||||
|
aria-selected="true" |
||||
|
> |
||||
|
节点树 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link" |
||||
|
id="testInstructionsTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#testInstructions" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="testInstructions" |
||||
|
aria-selected="false" |
||||
|
> |
||||
|
测试指令 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link" |
||||
|
id="vulnerabilitiesTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#vulnerabilities" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="vulnerabilities" |
||||
|
aria-selected="false" |
||||
|
> |
||||
|
漏洞数据 |
||||
|
</button> |
||||
|
</li> |
||||
|
</ul> |
||||
|
<div class="tab-content " id="myTabContent"> |
||||
|
<!-- 节点树 --> |
||||
|
<div |
||||
|
class="tab-pane fade show active p-3 h-100" |
||||
|
id="nodeTree" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="nodeTreeTab" |
||||
|
> |
||||
|
<div class="row h-100"> |
||||
|
<!-- 左侧:节点树区域 --> |
||||
|
<div class="col-8 h-100"> |
||||
|
<div class="node-tree-area" id="nodeTreeContainer"> |
||||
|
<!-- 顶部刷新按钮 --> |
||||
|
<div class="refresh-container"> |
||||
|
<button class="tree-refresh" id="btnRefresh" title="刷新节点树"> |
||||
|
↻ |
||||
|
</button> |
||||
|
</div> |
||||
|
<!-- 节点树内容区域 --> |
||||
|
<div id="treeContent" class="tree-content"> |
||||
|
<p id="treeLoadingMsg" style="text-align:center;">加载中...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 右侧:节点信息与操作 --> |
||||
|
<div class="col-4 h-100"> |
||||
|
<div class="node-info-area mb-3"> |
||||
|
<h5>节点信息</h5> |
||||
|
<p><strong>节点名称:</strong> <span id="nodeName">-</span></p> |
||||
|
<p><strong>测试状态:</strong> <span id="testStatus">-</span></p> |
||||
|
<p><strong>漏洞类型:</strong> <span id="node_vulType">-</span></p> |
||||
|
<p><strong>漏洞级别:</strong> <span id="node_vulLevel">-</span></p> |
||||
|
<p><strong>工作状态:</strong> <span id="node_bwork">-</span></p> |
||||
|
<p><strong>执行状态:</strong> <span id="node_workstatus">-</span></p> |
||||
|
</div> |
||||
|
<div class="node-actions"> |
||||
|
<div class="row"> |
||||
|
<div class="col-6"><button class="btn btn-primary w-100" id="btnToggleStatus">暂停</button></div> |
||||
|
<div class="col-6"><button class="btn btn-primary w-100" id="btnNodeStep">单步</button></div> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-6"><button class="btn btn-primary w-100" id="btnViewInstr">查看指令</button></div> |
||||
|
<div class="col-6"><button class="btn btn-primary w-100" id="btnViewMsg">查看MSG</button></div> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-6"><button class="btn btn-primary w-100" id="btnAddInfo">添加信息</button></div> |
||||
|
<div class="col-6"><button class="btn btn-primary w-100" id="btnAddChild">添加子节点</button></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 测试指令 --> |
||||
|
<div |
||||
|
class="tab-pane fade p-3" |
||||
|
id="testInstructions" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="testInstructionsTab" |
||||
|
> |
||||
|
<!-- 搜索区域 --> |
||||
|
<div class="row search-area"> |
||||
|
<div class="col-4"> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
id="instrNodeName" |
||||
|
placeholder="节点名称" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<button class="btn btn-primary" id="instrSearchBtn"> |
||||
|
查询 |
||||
|
</button> |
||||
|
<button class="btn btn-primary" id="instrExportBtn"> |
||||
|
导出 |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<table class="table table-bordered table-hover" id="instrTable" style="width: 100%; table-layout: fixed;"> |
||||
|
<colgroup> |
||||
|
<!-- 第一列:序号,固定宽度或百分比 --> |
||||
|
<col style="width: 5%;"> |
||||
|
<!-- 第二列:节点路径,例如 25% --> |
||||
|
<col style="width: 15%;"> |
||||
|
<!-- 第三列:指令序号,固定宽度 --> |
||||
|
<col style="width: 5%;"> |
||||
|
<!-- 第四列:执行指令,设置为固定宽度或者百分比 --> |
||||
|
<col style="width: 30%;" class="wrap-cell"> |
||||
|
<!-- 第五列:执行结果,占用剩余所有宽度 --> |
||||
|
<col style="width: auto;"> |
||||
|
</colgroup> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>序号</th> |
||||
|
<th>节点路径</th> |
||||
|
<th>指序</th> |
||||
|
<th>执行指令</th> |
||||
|
<th>执行结果</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<!-- 默认显示 10 行(后续用 JS 渲染数据,补空行) --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination" id="instrPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="instrPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="instrNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 漏洞数据 --> |
||||
|
<div |
||||
|
class="tab-pane fade p-3" |
||||
|
id="vulnerabilities" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="vulnerabilitiesTab" |
||||
|
> |
||||
|
<!-- 搜索区域 --> |
||||
|
<div class="row search-area"> |
||||
|
<div class="col-3"> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
id="vulNodeName" |
||||
|
placeholder="节点名称" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="col-3"> |
||||
|
<input |
||||
|
type="text" |
||||
|
class="form-control" |
||||
|
id="vulType" |
||||
|
placeholder="漏洞类型" |
||||
|
/> |
||||
|
</div> |
||||
|
<div class="col-3"> |
||||
|
<select class="form-select" id="vulLevel"> |
||||
|
<option value="">漏洞级别</option> |
||||
|
<option value="低危">低</option> |
||||
|
<option value="中危">中</option> |
||||
|
<option value="高危">高</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
<div class="col-2"> |
||||
|
<button class="btn btn-primary" id="vulSearchBtn"> |
||||
|
查询 |
||||
|
</button> |
||||
|
<button class="btn btn-primary" id="vulExportBtn"> |
||||
|
导出 |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<table class="table table-bordered table-hover" id="vulTable"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th class="seq-col">序号</th> |
||||
|
<th>节点路径</th> |
||||
|
<th>漏洞类型</th> |
||||
|
<th>漏洞级别</th> |
||||
|
<th>漏洞说明</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<!-- 默认显示 10 行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination" id="vulPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="vulPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="vulNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面脚本块 --> |
||||
|
{% block script %} |
||||
|
<!-- 在此处可添加与后端交互的脚本 --> |
||||
|
<script src="{{ url_for('main.static', filename='scripts/my_web_socket.js') }}"></script> |
||||
|
<script src="{{ url_for('main.static', filename='scripts/task_manager.js') }}"></script> |
||||
|
<script src="{{ url_for('main.static', filename='scripts/node_tree.js') }}"></script> |
||||
|
<script src="{{ url_for('main.static', filename='scripts/jquery-3.2.1.slim.min.js') }}"></script> |
||||
|
{% endblock %} |
@ -0,0 +1,214 @@ |
|||||
|
<!-- 指令对话框:Bootstrap Modal --> |
||||
|
<div |
||||
|
class="modal fade" |
||||
|
id="instrModal" |
||||
|
tabindex="-1" |
||||
|
aria-labelledby="instrModalLabel" |
||||
|
aria-hidden="true" |
||||
|
> |
||||
|
<div class="modal-dialog modal-xl"><!-- 宽一些,可根据需求调整 --> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<h5 class="modal-title" id="instrModalLabel">测试指令</h5> |
||||
|
<button |
||||
|
type="button" |
||||
|
class="btn-close" |
||||
|
data-bs-dismiss="modal" |
||||
|
aria-label="Close" |
||||
|
></button> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<!-- 新增一个提示容器 --> |
||||
|
<div id="loadingMsg" style="text-align: center; padding: 10px;">请稍后,数据获取中...</div> |
||||
|
<!-- 页签(已执行、待执行) --> |
||||
|
<ul class="nav nav-tabs" id="instrTab" role="tablist"> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link active" |
||||
|
id="doneInstrTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#doneInstr" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="doneInstr" |
||||
|
aria-selected="true" |
||||
|
> |
||||
|
已执行 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button |
||||
|
class="nav-link" |
||||
|
id="todoInstrTab" |
||||
|
data-bs-toggle="tab" |
||||
|
data-bs-target="#todoInstr" |
||||
|
type="button" |
||||
|
role="tab" |
||||
|
aria-controls="todoInstr" |
||||
|
aria-selected="false" |
||||
|
> |
||||
|
待执行 |
||||
|
</button> |
||||
|
</li> |
||||
|
</ul> |
||||
|
<div class="tab-content pt-3" id="instrTabContent"> |
||||
|
<!-- 已执行指令表格 --> |
||||
|
<div |
||||
|
class="tab-pane fade show active" |
||||
|
id="doneInstr" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="doneInstrTab" |
||||
|
> |
||||
|
<table class="table table-bordered table-hover"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th style="width: 50px;">序号</th> |
||||
|
<th>执行指令</th> |
||||
|
<th>执行时间</th> |
||||
|
<th>执行结果</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="doneInstrTbody"> |
||||
|
<!-- 动态生成,固定 10 行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination justify-content-end" id="doneInstrPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="doneInstrPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="doneInstrNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
<!-- 待执行指令表格 --> |
||||
|
<div |
||||
|
class="tab-pane fade" |
||||
|
id="todoInstr" |
||||
|
role="tabpanel" |
||||
|
aria-labelledby="todoInstrTab" |
||||
|
> |
||||
|
<table class="table table-bordered table-hover"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th style="width: 50px;">序号</th> |
||||
|
<th>待执行指令</th> |
||||
|
<th style="width: 80px;">操作</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="todoInstrTbody"> |
||||
|
<!-- 动态生成,固定 10 行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination justify-content-end" id="todoInstrPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="todoInstrPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="todoInstrNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 对话框底部:导出按钮居右 --> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> |
||||
|
关闭 |
||||
|
</button> |
||||
|
<button type="button" class="btn btn-primary" id="btnExport"> |
||||
|
导出 |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 查看MSG对话框:Bootstrap Modal --> |
||||
|
<div class="modal fade" id="msgModal" tabindex="-1" aria-labelledby="msgModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog modal-xl"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<h5 class="modal-title" id="msgModalLabel">查看MSG</h5> |
||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="关闭"></button> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<!-- Tab 导航 --> |
||||
|
<ul class="nav nav-tabs" id="msgTab" role="tablist"> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button class="nav-link active" id="submittedTab" data-bs-toggle="tab" data-bs-target="#submitted" type="button" role="tab" aria-controls="submitted" aria-selected="true"> |
||||
|
已提交 |
||||
|
</button> |
||||
|
</li> |
||||
|
<li class="nav-item" role="presentation"> |
||||
|
<button class="nav-link" id="pendingTab" data-bs-toggle="tab" data-bs-target="#pending" type="button" role="tab" aria-controls="pending" aria-selected="false"> |
||||
|
待提交 |
||||
|
</button> |
||||
|
</li> |
||||
|
</ul> |
||||
|
<div class="tab-content pt-3" id="msgTabContent"> |
||||
|
<!-- 已提交 Tab --> |
||||
|
<div class="tab-pane fade show active" id="submitted" role="tabpanel" aria-labelledby="submittedTab"> |
||||
|
<table class="table table-bordered table-hover"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th style="width:50px;">序号</th> |
||||
|
<th>角色</th> |
||||
|
<th>内容</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="submittedTbody"> |
||||
|
<!-- 动态生成 10 行;数据不足时补空行 --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<!-- 分页控件 --> |
||||
|
<nav> |
||||
|
<ul class="pagination justify-content-end" id="submittedPagination"> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="submittedPrev">上一页</a> |
||||
|
</li> |
||||
|
<li class="page-item"> |
||||
|
<a class="page-link" href="#" id="submittedNext">下一页</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</nav> |
||||
|
<!-- 导出按钮 --> |
||||
|
<div class="text-end"> |
||||
|
<button class="btn btn-primary" id="btnExportSubmitted">导出</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 待提交 Tab --> |
||||
|
<div class="tab-pane fade" id="pending" role="tabpanel" aria-labelledby="pendingTab"> |
||||
|
<form id="pendingForm"> |
||||
|
<div class="mb-3"> |
||||
|
<label for="llmtype" class="form-label fw-bold" style="font-size:0.9rem">llmtype:</label> |
||||
|
<input type="text" class="form-control" id="llmtype" placeholder="请输入 llmtype" disabled> |
||||
|
</div> |
||||
|
<div class="mb-3"> |
||||
|
<label for="pendingContent" class="form-label fw-bold" style="font-size:0.9rem">内容:</label> |
||||
|
<textarea class="form-control" id="pendingContent" rows="5" placeholder="请输入内容"></textarea> |
||||
|
</div> |
||||
|
<!-- 你可以在此处增加一个保存按钮,由用户提交待提交的内容修改 --> |
||||
|
<div class="text-end"> |
||||
|
<button type="button" class="btn btn-primary" id="btnSavePending">保存</button> |
||||
|
<button type="button" class="btn btn-primary" id="btnNeedInstr">请求指令</button> |
||||
|
<!-- <button type="button" class="btn btn-primary" id="btnRedoInstr">重新执行</button>--> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="modal-footer"> |
||||
|
<!-- 关闭按钮 --> |
||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -1,143 +0,0 @@ |
|||||
{% extends 'base.html' %} |
|
||||
|
|
||||
{% block title %}ZFBOX{% endblock %} |
|
||||
|
|
||||
{% block style %} |
|
||||
.nav-scroller { |
|
||||
position: relative; |
|
||||
z-index: 2; |
|
||||
height: 2.75rem; |
|
||||
overflow-y: hidden; |
|
||||
} |
|
||||
|
|
||||
.nav-scroller .nav { |
|
||||
display: flex; |
|
||||
flex-wrap: nowrap; |
|
||||
padding-bottom: 1rem; |
|
||||
margin-top: -1px; |
|
||||
overflow-x: auto; |
|
||||
text-align: center; |
|
||||
white-space: nowrap; |
|
||||
-webkit-overflow-scrolling: touch; |
|
||||
} |
|
||||
|
|
||||
.blue-svg { |
|
||||
color: blue; /* 这将影响所有使用currentColor的fill属性 */ |
|
||||
} |
|
||||
|
|
||||
main { |
|
||||
display: flex; |
|
||||
flex: 1; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.tree-view { |
|
||||
border-right: 1px solid #ddd; |
|
||||
padding: 10px; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
|
|
||||
.video-frame { |
|
||||
position: relative; |
|
||||
width: calc(50% - 10px); /* 默认4画面布局,每行2个视频框架 */ |
|
||||
margin: 5px; |
|
||||
float: left; |
|
||||
background-color: #ccc; /* 默认灰色填充 */ |
|
||||
border: 1px solid #000; /* 边框 */ |
|
||||
} |
|
||||
.video-header { |
|
||||
display: flex; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
background-color: #2c3e50; |
|
||||
color: #fff; |
|
||||
padding: 5px; |
|
||||
} |
|
||||
.video-area { |
|
||||
width: 100%; |
|
||||
padding-bottom: 75%; /* 4:3 比例 */ |
|
||||
background-color: #000; /* 默认灰色填充 */ |
|
||||
position: relative; |
|
||||
border: 1px solid #ddd; /* 视频区域边框 */ |
|
||||
} |
|
||||
|
|
||||
.video-area canvas { |
|
||||
display: none; |
|
||||
position: absolute; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
width: 100%; |
|
||||
height: 100%; |
|
||||
object-fit: contain; |
|
||||
} |
|
||||
|
|
||||
.video-buttons { |
|
||||
display: flex; |
|
||||
gap: 10px; |
|
||||
} |
|
||||
|
|
||||
.video-buttons button { |
|
||||
background: none; |
|
||||
border: none; |
|
||||
color: white; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.video-buttons button:hover { |
|
||||
color: #f39c12; |
|
||||
} |
|
||||
|
|
||||
.dropdown-menu { |
|
||||
min-width: 100px; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
#videoGrid.four .video-frame { |
|
||||
width: calc(50% - 10px); /* 每行4个视频框架 */ |
|
||||
} |
|
||||
#videoGrid.eight .video-frame { |
|
||||
width: calc(12.5% - 10px); /* 每行8个视频框架 */ |
|
||||
} |
|
||||
#videoGrid.nine .video-frame { |
|
||||
width: calc(33.33% - 10px); /* 每行3个视频框架,9画面布局 */ |
|
||||
} |
|
||||
.toggle-buttons { |
|
||||
margin-bottom: 5px; |
|
||||
} |
|
||||
.btn-small { |
|
||||
font-size: 0.75rem; |
|
||||
padding: 0.25rem 0.5rem; |
|
||||
} |
|
||||
.error-message { |
|
||||
position: absolute; |
|
||||
top: 50%; |
|
||||
left: 50%; |
|
||||
transform: translate(-50%, -50%); |
|
||||
color: red; |
|
||||
background-color: rgba(255, 255, 255, 0.8); /* 半透明背景 */ |
|
||||
padding: 10px; |
|
||||
border-radius: 5px; |
|
||||
} |
|
||||
{% endblock %} |
|
||||
|
|
||||
{% block content %} |
|
||||
<div class="container d-flex flex-wrap" > |
|
||||
<div id="treeView" class="tree-view col-md-3 "> |
|
||||
<!-- 动态树视图 --> |
|
||||
</div> |
|
||||
|
|
||||
<div class="video-content col-md-9"> |
|
||||
<div id="videoGrid" class="row four"> |
|
||||
<!-- 动态视频节点 --> |
|
||||
</div> |
|
||||
<div class="toggle-buttons"> |
|
||||
<button id="fourView" class="btn btn-primary btn-small">四画面</button> |
|
||||
<button id="nineView" class="btn btn-secondary btn-small">九画面</button> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
{% endblock %} |
|
||||
|
|
||||
{% block script %} |
|
||||
<script src="{{ url_for('main.static', filename='scripts/aiortc-client-new.js') }}"></script> |
|
||||
{% endblock %} |
|
@ -0,0 +1,16 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
|
||||
|
{% block title %}ZFSAFE{% endblock %} |
||||
|
|
||||
|
<!-- 页面样式块 --> |
||||
|
{% block style %} |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面内容块 --> |
||||
|
{% block content %} |
||||
|
<h3 style="text-align: center;padding: 10px"> 功能建设中,在二期实现。。。</h3> |
||||
|
{% endblock %} |
||||
|
|
||||
|
<!-- 页面脚本块 --> |
||||
|
{% block script %} |
||||
|
{% endblock %} |
Loading…
Reference in new issue