You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
929 lines
32 KiB
929 lines
32 KiB
3 weeks ago
|
// 全局变量,用于保存当前选中的节点数据
|
||
|
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; // 待执行指令当前页
|
||
|
const pageSize = 10; // 每页固定显示 10 行
|
||
|
|
||
|
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);
|