Add files via upload

This commit is contained in:
公明
2026-07-10 16:37:14 +08:00
committed by GitHub
parent 46f891a114
commit 04efc50161
12 changed files with 5208 additions and 81 deletions
+2354 -5
View File
File diff suppressed because it is too large Load Diff
+366 -4
View File
@@ -37,6 +37,15 @@
"language": "Interface language",
"backToDashboard": "Back to dashboard",
"userMenu": "User menu",
"userMenuFor": "User menu: {{name}}",
"currentUser": "Current user",
"unknownUser": "Unknown user",
"scopeAll": "All resources",
"scopeAssigned": "Assigned resources",
"scopeOwn": "Own resources",
"scopeUnknown": "Unknown scope",
"rolesCount": "{{count}} roles",
"permissionsCount": "{{count}} permissions",
"version": "Current version",
"toggleSidebar": "Collapse/expand sidebar"
},
@@ -59,7 +68,9 @@
"login": {
"title": "Sign in to CyberStrikeAI",
"titlePrefix": "Sign in to",
"subtitle": "Enter the access password from config",
"subtitle": "Sign in with a platform account. The default administrator is admin.",
"usernameLabel": "Username",
"usernamePlaceholder": "admin",
"passwordLabel": "Password",
"passwordPlaceholder": "Enter password",
"submit": "Sign in"
@@ -95,7 +106,8 @@
"c2Tasks": "Tasks",
"c2Payloads": "Payload",
"c2Events": "Events",
"c2Profiles": "Traffic profiles"
"c2Profiles": "Traffic profiles",
"platformRbac": "Platform Access"
},
"dashboard": {
"title": "Dashboard",
@@ -812,6 +824,7 @@
"running": "Running...",
"completed": "Completed",
"execFailed": "Execution failed",
"resultMissing": "Result record missing",
"toolSucceeded": "Succeeded",
"toolFailed": "Failed"
},
@@ -1236,6 +1249,7 @@
"robots": "Bots",
"terminal": "Terminal",
"security": "Security",
"rbac": "Platform permissions",
"audit": "Audit logs",
"infocollect": "Recon"
},
@@ -1404,7 +1418,7 @@
"sessionExpired": "Session expired, please sign in again",
"unauthorized": "Unauthorized",
"enterPassword": "Please enter password",
"loginFailedCheck": "Sign-in failed, please check the password",
"loginFailedCheck": "Sign-in failed, please check the username or password",
"loginFailedRetry": "Sign-in failed, please try again later",
"loggedOut": "Signed out"
},
@@ -3466,6 +3480,7 @@
"paginationLast": "Last"
},
"payloads": {
"386": "386",
"title": "Payload Generator",
"oneliner": "Oneliner Payload",
"build": "Build Beacon",
@@ -3485,7 +3500,6 @@
"darwin": "macOS",
"amd64": "AMD64",
"arm64": "ARM64",
"386": "386",
"arm": "ARM",
"onelinerDesc": "Generate a one-line reverse shell for the target (Bash / Python / PowerShell / Curl).",
"buildDesc": "Cross-compile a full Beacon binary for Linux / Windows / macOS.",
@@ -3568,5 +3582,353 @@
"confirmDelete": "Delete this profile?",
"submitCreate": "Create"
}
},
"rbac": {
"title": "Platform Access",
"pageSubtitle": "Manage members, roles, and accessible data in one place.",
"createUser": "Add Member",
"memberManagement": "Member Management",
"memberManagementHint": "Assign roles and data scope to members",
"roleManagement": "Role Management",
"roleManagementHint": "Define permission sets and default visibility",
"users": "Users",
"usersHint": "Select an account to manage roles and resource scope.",
"searchUsers": "Search username or display name",
"metricUsers": "Accounts",
"metricEnabled": "Enabled",
"metricRoles": "Roles",
"metricAssignments": "Resource Grants",
"metricUsersPill": "accounts",
"metricEnabledPill": "enabled accounts",
"metricRolesPill": "roles",
"metricAssignmentsPill": "grants",
"selectUser": "Select User",
"selectUserHint": "Select a platform user to edit roles, status, and resource grants.",
"createRole": "New Role",
"tabRoles": "Roles & Permissions",
"tabAssignments": "Resource Grants",
"tabAudit": "Change History",
"roleMembership": "Role Membership",
"roleMembershipHint": "Roles control allowed actions; resource scope controls visible data.",
"unsavedChanges": "Unsaved changes",
"saveRoleChanges": "Save Role Changes",
"resourceType": "Resource Type",
"resourceId": "Resource ID",
"resourceIdPlaceholder": "Enter resource IDs; separate multiple IDs with commas",
"searchResource": "Search Real Resources",
"searchResourcePlaceholder": "Search by name or ID",
"assignSelected": "Grant Selected Resources",
"advancedAssignment": "Advanced: Enter Resource IDs",
"advancedAssignmentHint": "For known IDs copied from logs or APIs. Resource existence is still validated on submit.",
"availableResources": "Available Resources",
"availableResourcesHint": "Select on this page; selections persist across pages",
"currentAssignments": "Current Explicit Grants",
"allResourceTypes": "All Types",
"searchAssignments": "Search granted resource names or IDs",
"items": "items",
"assignmentHelp": "Only Assigned Resources roles need explicit grants. Project grants also cover conversations and vulnerabilities in that project.",
"assignResource": "Grant",
"roleCatalog": "Role Catalog",
"roleCatalogHint": "Define permissions and resource scope first, then assign the role to members.",
"searchRoles": "Search roles or permissions",
"filterRoles": "Filter Roles",
"userModalTitle": "Platform User",
"username": "Username",
"displayName": "Display Name",
"password": "Password",
"passwordPlaceholder": "Leave blank when editing to keep unchanged",
"enabled": "Enable account",
"roles": "Roles",
"roleModalTitle": "Platform Role",
"roleName": "Role Name",
"description": "Description",
"scope": "Resource Scope",
"permissions": "Permissions",
"searchPermissions": "Search permissions, e.g. project:read",
"selectVisible": "Select visible",
"clearVisible": "Clear visible",
"deleteUserConfirm": "Delete this platform user? This will revoke the user's active sessions immediately.",
"deleteBuiltinUser": "The built-in admin user cannot be deleted",
"deleteRoleConfirm": "Delete this platform role?",
"scopeHint": "Resource scope and operation permissions jointly determine effective access.",
"builtinAccount": "Built-in account",
"statusEnabled": "Enabled",
"statusDisabled": "Disabled",
"noRoleAssigned": "No role assigned",
"noExplicitGrant": "No explicit grants",
"notConfigured": "Not configured",
"noRoleDescription": "No role description",
"noDescription": "No description",
"systemBuiltin": "System role",
"customRole": "Custom",
"operationPermissions": "Operation permissions",
"assignedMembers": "Assigned members",
"viewPermissions": "View permissions",
"effectivePermissions": "Effective permissions",
"effectiveScope": "Effective resource scope",
"revoke": "Revoke",
"retry": "Retry",
"alreadyAssigned": "Assigned",
"loadingResources": "Loading real resources…",
"viewSystemRole": "View System Role",
"editRole": "Edit Role",
"resourceCount": "{{count}} resources",
"permissionCount": "{{count}} permissions",
"userCount": "{{count}} users",
"confirmDiscardSwitchUser": "Role changes are not saved. Discard them and switch members?",
"confirmDiscardEditUser": "Role changes are not saved. Discard them and edit the member profile?",
"confirmDiscardSwitchTab": "Role changes are not saved. Discard them and switch tabs?",
"confirmDiscardResourceType": "Switching resource type clears the current selection. Continue?",
"pendingGrants": "Pending Grants",
"clearSelection": "Clear Selection",
"confirmGrant": "Confirm Grant",
"removePending": "Remove",
"selectedCount": "{{count}} selected",
"noRoleNoticeTitle": "No role assigned",
"noRoleNoticeHint": "Assign a role before configuring resource grants. Without a role, only self-created data is accessible and most features are unavailable.",
"assignRolesNow": "Assign Roles",
"viewPermissionsDetail": "View details",
"hidePermissions": "Hide details",
"effectivePermissionsDetail": "Effective permission details",
"editUserRolesHint": "Manage roles on the member detail page under Roles & Permissions.",
"auditTitle": "Permission change history",
"auditHint": "Audit events related to this member's platform access.",
"unknownActor": "Unknown actor",
"metricUserContext": "Selected member: {{name}} · {{roles}} roles · {{grants}} grants",
"deleteUserConfirmDetailed": "Delete member {{name}} (@{{username}})?\n\nThis will revoke active sessions and remove {{roles}} role bindings and {{grants}} resource grants.",
"auditActions": {
"create_user": "Create platform user",
"update_user": "Update platform user",
"delete_user": "Delete platform user",
"create_role": "Create platform role",
"update_role": "Update platform role",
"delete_role": "Delete platform role",
"assign_resource": "Grant resource access",
"delete_resource_assignment": "Revoke resource grant"
},
"copyId": "Copy ID",
"copiedId": "Copied",
"resourceIdLabel": "ID",
"resourceDetailProject": "Project {{id}}",
"pagination": {
"previous": "Previous",
"next": "Next",
"resourcePage": "Page {{page}}",
"pageSummary": "Page {{page}} / {{pages}} · {{total}} items",
"emptySummary": "0 items"
},
"resourceTypes": {
"project": "Project",
"conversation": "Conversation",
"vulnerability": "Vulnerability",
"webshell": "WebShell Connection",
"batch_task": "Batch Task",
"c2_listener": "C2 Listener"
},
"scopes": {
"all": {
"label": "All Resources",
"hint": "Can access all platform data",
"option": "All Resources — Access all platform data"
},
"assigned": {
"label": "Assigned Resources",
"hint": "Only data explicitly granted by an administrator",
"option": "Assigned Resources — Only administrator-granted data"
},
"own": {
"label": "Created by Me",
"hint": "Only data created by the member",
"option": "Created by Me — Only self-created data"
}
},
"systemRoles": {
"admin": { "name": "Administrator", "description": "Full platform administration access" },
"operator": { "name": "Operator", "description": "Run daily security workflows without account or core configuration management" },
"auditor": { "name": "Auditor", "description": "Read-only access to audits, monitoring, and assets" },
"viewer": { "name": "Read-only User", "description": "Read-only access to explicitly granted resources" }
},
"empty": {
"noMatchingUsers": "No matching members",
"noUsers": "No platform members yet",
"tryAnotherKeyword": "Try another keyword",
"addFirstUser": "Use “Add Member” to create the first member",
"noMatchingRoles": "No matching roles",
"adjustRoleSearch": "Adjust the search or create a custom role",
"noRoles": "No roles available",
"selectUserFirst": "Select a member first",
"noAssignments": "No resource grants",
"addAssignmentHint": "Select resources on the left, then use Confirm Grant above",
"noMatchingAssignments": "No matching grants",
"adjustAssignmentFilter": "Adjust the resource type or search",
"noMatchingResources": "No matching real resources",
"noMatchingPermissions": "No matching permissions",
"noEffectivePermissions": "No effective permissions",
"assignRoleFirst": "Assign a role to view permission details",
"noAuditLogs": "No change history",
"noAuditLogsHint": "Role and grant changes for this member will appear here"
},
"messages": {
"roleChangesSaved": "Role changes saved and effective immediately",
"grantPartial": "Added {{created}} grants; {{skipped}} already existed or were duplicated",
"grantSuccess": "Granted {{count}} {{resource}} resources"
},
"errors": {
"loadFailed": "Load failed",
"loadDataFailed": "Unable to load access-control data",
"loadResourcesFailed": "Unable to load resources",
"saveFailed": "Save failed",
"saveUserFailed": "Unable to save member",
"saveRoleFailed": "Unable to save role",
"deleteFailed": "Delete failed",
"deleteUserFailed": "Unable to delete member",
"deleteRoleFailed": "Unable to delete role",
"enterResourceId": "Enter at least one resource ID",
"tooManyResources": "A maximum of 100 resources can be granted at once",
"batchGrantFailed": "Batch grant failed",
"grantFailed": "Grant failed",
"revokeFailed": "Revoke failed",
"loadAuditFailed": "Unable to load audit history"
},
"permissionModules": {
"agent": "Agent Execution",
"agents": "Agent Definitions",
"attackchain": "Attack Chains",
"audit": "Audit",
"auth": "Account Security",
"c2": "C2",
"chat": "Conversations",
"config": "System Configuration",
"dashboard": "Dashboard",
"files": "Files",
"fofa": "FOFA",
"group": "Conversation Groups",
"hitl": "Human-in-the-loop",
"knowledge": "Knowledge Base",
"mcp": "MCP",
"monitor": "Execution Monitor",
"notification": "Notifications",
"openapi": "OpenAPI",
"project": "Projects",
"rbac": "Platform Access",
"robot": "Robots",
"roles": "Testing Roles",
"skills": "Skills",
"tasks": "Tasks",
"terminal": "Terminal",
"vulnerability": "Vulnerabilities",
"webshell": "WebShell",
"workflow": "Workflows"
},
"permissionDescriptions": {
"auth": { "self": "Manage own session and password" },
"dashboard": { "read": "View dashboard summaries" },
"chat": {
"read": "View conversations",
"write": "Create and update conversations",
"delete": "Delete conversations and turns"
},
"agent": { "execute": "Run AI agents and workflows" },
"hitl": {
"read": "View human-in-the-loop queues and logs",
"write": "Approve, dismiss, and configure human-in-the-loop requests"
},
"tasks": {
"read": "View task queues",
"write": "Create and run task queues",
"delete": "Delete task queues"
},
"project": {
"read": "View projects and project facts",
"write": "Create and update projects and project facts",
"delete": "Delete projects and project facts"
},
"vulnerability": {
"read": "View vulnerabilities",
"write": "Create and update vulnerabilities",
"delete": "Delete vulnerabilities"
},
"webshell": {
"read": "View WebShell connections",
"write": "Manage and use WebShell connections",
"delete": "Delete WebShell connections"
},
"c2": {
"read": "View C2 listeners, sessions, tasks, events, and profiles",
"write": "Operate C2 listeners, sessions, tasks, payloads, files, and profiles",
"delete": "Delete C2 objects"
},
"mcp": {
"read": "View MCP status and external MCP configuration",
"write": "Manage external MCP servers and invoke the MCP endpoint"
},
"knowledge": {
"read": "View the knowledge base and retrieval logs",
"write": "Create, update, index, and scan the knowledge base",
"delete": "Delete knowledge items and retrieval logs"
},
"skills": {
"read": "View Skills and statistics",
"write": "Create and update Skills",
"delete": "Delete Skills and statistics"
},
"agents": {
"read": "View Markdown agents",
"write": "Create and update Markdown agents",
"delete": "Delete Markdown agents"
},
"roles": {
"read": "View AI testing roles",
"write": "Create and update AI testing roles",
"delete": "Delete AI testing roles"
},
"workflow": {
"read": "View workflow definitions and runs",
"write": "Create, update, validate, run, and resume workflows",
"delete": "Delete workflows"
},
"config": {
"read": "View system configuration",
"write": "Update and apply system configuration"
},
"terminal": { "execute": "Execute terminal commands" },
"audit": {
"read": "View and export audit logs",
"delete": "Delete audit logs"
},
"rbac": {
"read": "View users, platform roles, permissions, and grants",
"write": "Manage users, platform roles, permissions, and grants"
},
"notification": {
"read": "View notifications",
"write": "Mark notifications as read"
},
"robot": {
"read": "View robot binding status",
"write": "Manage robot bindings and test callbacks"
},
"files": {
"read": "View chat uploads",
"write": "Upload, edit, and rename chat files",
"delete": "Delete chat files"
},
"attackchain": {
"read": "View attack chains",
"write": "Regenerate attack chains"
},
"fofa": { "execute": "Run FOFA searches and parse queries" },
"openapi": { "read": "Read OpenAPI aggregation results" },
"group": {
"read": "View conversation groups",
"write": "Create and update conversation groups",
"delete": "Delete conversation groups"
},
"monitor": {
"read": "View the execution monitor",
"write": "Cancel monitored executions",
"delete": "Delete execution-monitor records"
}
}
}
}
+366 -4
View File
@@ -37,6 +37,15 @@
"language": "界面语言",
"backToDashboard": "返回仪表盘",
"userMenu": "用户菜单",
"userMenuFor": "用户菜单:{{name}}",
"currentUser": "当前用户",
"unknownUser": "未知用户",
"scopeAll": "全部资源",
"scopeAssigned": "指定资源",
"scopeOwn": "自己的资源",
"scopeUnknown": "资源范围未知",
"rolesCount": "{{count}} 个角色",
"permissionsCount": "{{count}} 项权限",
"version": "当前版本",
"toggleSidebar": "折叠/展开侧边栏"
},
@@ -59,7 +68,9 @@
"login": {
"title": "登录 CyberStrikeAI",
"titlePrefix": "登录",
"subtitle": "请输入配置中的访问密码",
"subtitle": "使用平台账号登录,默认管理员为 admin。",
"usernameLabel": "用户名",
"usernamePlaceholder": "admin",
"passwordLabel": "密码",
"passwordPlaceholder": "输入登录密码",
"submit": "登录"
@@ -95,7 +106,8 @@
"c2Tasks": "任务",
"c2Payloads": "载荷",
"c2Events": "事件",
"c2Profiles": "流量伪装"
"c2Profiles": "流量伪装",
"platformRbac": "平台权限"
},
"dashboard": {
"title": "仪表盘",
@@ -800,6 +812,7 @@
"running": "执行中...",
"completed": "已完成",
"execFailed": "执行失败",
"resultMissing": "结果记录缺失",
"toolSucceeded": "成功",
"toolFailed": "失败"
},
@@ -1224,6 +1237,7 @@
"robots": "机器人设置",
"terminal": "终端",
"security": "安全设置",
"rbac": "平台权限",
"audit": "日志审计",
"infocollect": "信息收集"
},
@@ -1392,7 +1406,7 @@
"sessionExpired": "认证已过期,请重新登录",
"unauthorized": "未授权访问",
"enterPassword": "请输入密码",
"loginFailedCheck": "登录失败,请检查密码",
"loginFailedCheck": "登录失败,请检查用户名或密码",
"loginFailedRetry": "登录失败,请稍后重试",
"loggedOut": "已退出登录"
},
@@ -3454,6 +3468,7 @@
"paginationLast": "末页"
},
"payloads": {
"386": "386",
"title": "Payload 生成",
"oneliner": "单行 Payload",
"build": "编译 Beacon",
@@ -3473,7 +3488,6 @@
"darwin": "macOS",
"amd64": "AMD64",
"arm64": "ARM64",
"386": "386",
"arm": "ARM",
"onelinerDesc": "快速生成可在目标机直接执行的反弹命令,支持 Bash / Python / PowerShell / Curl",
"buildDesc": "交叉编译多平台完整 Beacon 可执行文件,支持 Linux / Windows / macOS",
@@ -3556,5 +3570,353 @@
"confirmDelete": "确定删除此 Profile",
"submitCreate": "创建"
}
},
"rbac": {
"title": "平台权限",
"pageSubtitle": "统一管理成员、角色和可访问的数据范围。",
"createUser": "添加成员",
"memberManagement": "成员管理",
"memberManagementHint": "为成员分配角色与数据范围",
"roleManagement": "角色管理",
"roleManagementHint": "定义权限集合与默认可见范围",
"users": "用户",
"usersHint": "按账号选择后管理角色与资源范围。",
"searchUsers": "搜索用户名或显示名称",
"metricUsers": "账号",
"metricEnabled": "启用",
"metricRoles": "角色",
"metricAssignments": "资源授权",
"metricUsersPill": "个账号",
"metricEnabledPill": "个启用账号",
"metricRolesPill": "个角色",
"metricAssignmentsPill": "项授权",
"selectUser": "选择用户",
"selectUserHint": "选择一个平台用户后编辑角色、状态与资源授权。",
"createRole": "新建角色",
"tabRoles": "角色权限",
"tabAssignments": "资源授权",
"tabAudit": "变更记录",
"roleMembership": "角色成员关系",
"roleMembershipHint": "角色决定能做什么,资源范围决定能看到哪些数据。",
"unsavedChanges": "有未保存的变更",
"saveRoleChanges": "保存角色变更",
"resourceType": "资源类型",
"resourceId": "资源 ID",
"resourceIdPlaceholder": "输入资源 ID;多个 ID 用逗号分隔",
"searchResource": "搜索真实资源",
"searchResourcePlaceholder": "按名称或 ID 搜索",
"assignSelected": "授权所选资源",
"advancedAssignment": "高级:直接输入资源 ID",
"advancedAssignmentHint": "适用于从日志或 API 复制的已知 ID,提交时仍会验证资源是否存在。",
"availableResources": "可授权资源",
"availableResourcesHint": "勾选当前页资源,可跨页累计选择",
"currentAssignments": "当前单独授权",
"allResourceTypes": "全部类型",
"searchAssignments": "搜索已授权资源名称或 ID",
"items": "项",
"assignmentHelp": "仅“指定资源”范围的角色需要单独授权。项目授权会覆盖该项目下的对话和漏洞。",
"assignResource": "授权",
"roleCatalog": "角色目录",
"roleCatalogHint": "先定义角色的操作权限与资源范围,再分配给成员。",
"searchRoles": "搜索角色或权限",
"filterRoles": "筛选角色",
"userModalTitle": "平台用户",
"username": "用户名",
"displayName": "显示名称",
"password": "密码",
"passwordPlaceholder": "编辑时留空表示不修改",
"enabled": "启用账号",
"roles": "角色",
"roleModalTitle": "平台角色",
"roleName": "角色名称",
"description": "描述",
"scope": "资源范围",
"permissions": "权限",
"searchPermissions": "搜索权限,例如 project:read",
"selectVisible": "选择可见",
"clearVisible": "清空可见",
"deleteUserConfirm": "确认删除该平台用户?该操作会立即吊销该用户会话。",
"deleteBuiltinUser": "内置管理员不能删除",
"deleteRoleConfirm": "确认删除该平台角色?",
"scopeHint": "资源范围与操作权限共同决定最终访问能力。",
"builtinAccount": "内置账号",
"statusEnabled": "启用",
"statusDisabled": "停用",
"noRoleAssigned": "未分配角色",
"noExplicitGrant": "无单独授权",
"notConfigured": "未设置",
"noRoleDescription": "暂无角色说明",
"noDescription": "无描述",
"systemBuiltin": "系统内置",
"customRole": "自定义",
"operationPermissions": "操作权限",
"assignedMembers": "已分配成员",
"viewPermissions": "查看权限",
"effectivePermissions": "有效权限",
"effectiveScope": "有效资源范围",
"revoke": "撤销",
"retry": "重试",
"alreadyAssigned": "已授权",
"loadingResources": "正在加载真实资源…",
"viewSystemRole": "查看系统角色",
"editRole": "编辑角色",
"resourceCount": "{{count}} 项资源",
"permissionCount": "{{count}} 项权限",
"userCount": "{{count}} 个用户",
"confirmDiscardSwitchUser": "当前角色变更尚未保存,确定放弃并切换成员吗?",
"confirmDiscardEditUser": "当前角色变更尚未保存,确定放弃并编辑成员资料吗?",
"confirmDiscardSwitchTab": "当前角色变更尚未保存,确定放弃并切换标签吗?",
"confirmDiscardResourceType": "切换资源类型将清空当前选择,确定继续吗?",
"pendingGrants": "待授权",
"clearSelection": "清空选择",
"confirmGrant": "确认授权",
"removePending": "移除",
"selectedCount": "已选 {{count}} 项",
"noRoleNoticeTitle": "尚未分配角色",
"noRoleNoticeHint": "请先分配角色,再配置资源授权。无角色时仅可访问本人创建的数据,且大部分功能不可用。",
"assignRolesNow": "去分配角色",
"viewPermissionsDetail": "查看明细",
"hidePermissions": "收起明细",
"effectivePermissionsDetail": "有效权限明细",
"editUserRolesHint": "角色请在成员详情页的「角色权限」标签中统一管理。",
"auditTitle": "权限变更记录",
"auditHint": "展示该成员相关的平台权限操作审计。",
"unknownActor": "未知操作者",
"metricUserContext": "当前成员:{{name}} · {{roles}} 个角色 · {{grants}} 项授权",
"deleteUserConfirmDetailed": "确认删除成员 {{name}}@{{username}})?\n\n将立即吊销其会话,并清除 {{roles}} 个角色绑定与 {{grants}} 项资源授权。",
"auditActions": {
"create_user": "创建平台用户",
"update_user": "更新平台用户",
"delete_user": "删除平台用户",
"create_role": "创建平台角色",
"update_role": "更新平台角色",
"delete_role": "删除平台角色",
"assign_resource": "授权资源访问",
"delete_resource_assignment": "撤销资源授权"
},
"copyId": "复制 ID",
"copiedId": "已复制",
"resourceIdLabel": "ID",
"resourceDetailProject": "所属项目 {{id}}",
"pagination": {
"previous": "上一页",
"next": "下一页",
"resourcePage": "第 {{page}} 页",
"pageSummary": "第 {{page}} / {{pages}} 页 · 共 {{total}} 项",
"emptySummary": "共 0 项"
},
"resourceTypes": {
"project": "项目",
"conversation": "对话",
"vulnerability": "漏洞",
"webshell": "WebShell 连接",
"batch_task": "批量任务",
"c2_listener": "C2 监听器"
},
"scopes": {
"all": {
"label": "全部资源",
"hint": "可访问平台内全部数据",
"option": "全部资源 — 可访问平台内所有数据"
},
"assigned": {
"label": "指定资源",
"hint": "仅访问管理员明确授权的数据",
"option": "指定资源 — 仅访问管理员授权的数据"
},
"own": {
"label": "本人创建",
"hint": "仅访问自己创建的数据",
"option": "本人创建 — 仅访问自己创建的数据"
}
},
"systemRoles": {
"admin": { "name": "管理员", "description": "全局管理权限" },
"operator": { "name": "操作员", "description": "可执行日常安全工作流,不能管理账号与核心配置" },
"auditor": { "name": "审计员", "description": "只读查看审计、监控与资产" },
"viewer": { "name": "只读用户", "description": "只读查看被授权资源" }
},
"empty": {
"noMatchingUsers": "没有匹配的成员",
"noUsers": "还没有平台成员",
"tryAnotherKeyword": "试试其他关键词",
"addFirstUser": "点击右上角“添加成员”开始配置",
"noMatchingRoles": "没有匹配的角色",
"adjustRoleSearch": "调整搜索条件或新建自定义角色",
"noRoles": "暂无角色",
"selectUserFirst": "请先选择用户",
"noAssignments": "暂无资源授权",
"addAssignmentHint": "在左侧勾选资源,或使用上方「确认授权」按钮",
"noMatchingAssignments": "没有匹配的授权",
"adjustAssignmentFilter": "调整资源类型或搜索条件",
"noMatchingResources": "没有匹配的真实资源",
"noMatchingPermissions": "没有匹配的权限",
"noEffectivePermissions": "暂无有效权限",
"assignRoleFirst": "分配角色后即可查看权限明细",
"noAuditLogs": "暂无变更记录",
"noAuditLogsHint": "该成员的角色与授权变更会显示在这里"
},
"messages": {
"roleChangesSaved": "角色变更已保存,权限立即生效",
"grantPartial": "新增 {{created}} 项授权,{{skipped}} 项已存在或重复",
"grantSuccess": "已授权 {{count}} 项{{resource}}"
},
"errors": {
"loadFailed": "加载失败",
"loadDataFailed": "无法加载权限数据",
"loadResourcesFailed": "加载资源失败",
"saveFailed": "保存失败",
"saveUserFailed": "保存用户失败",
"saveRoleFailed": "保存角色失败",
"deleteFailed": "删除失败",
"deleteUserFailed": "删除用户失败",
"deleteRoleFailed": "删除角色失败",
"enterResourceId": "请输入至少一个资源 ID",
"tooManyResources": "一次最多授权 100 个资源",
"batchGrantFailed": "批量授权失败",
"grantFailed": "授权失败",
"revokeFailed": "撤销失败",
"loadAuditFailed": "加载审计记录失败"
},
"permissionModules": {
"agent": "智能体",
"agents": "智能体定义",
"attackchain": "攻击链",
"audit": "审计",
"auth": "账户安全",
"c2": "C2",
"chat": "对话",
"config": "系统配置",
"dashboard": "仪表盘",
"files": "文件",
"fofa": "FOFA",
"group": "对话分组",
"hitl": "人机协同",
"knowledge": "知识库",
"mcp": "MCP",
"monitor": "执行监控",
"notification": "通知",
"openapi": "OpenAPI",
"project": "项目",
"rbac": "平台权限",
"robot": "机器人",
"roles": "测试角色",
"skills": "Skills",
"tasks": "任务",
"terminal": "终端",
"vulnerability": "漏洞",
"webshell": "WebShell",
"workflow": "工作流"
},
"permissionDescriptions": {
"auth": { "self": "管理自己的会话和密码" },
"dashboard": { "read": "查看仪表盘汇总" },
"chat": {
"read": "查看对话",
"write": "创建和更新对话",
"delete": "删除对话和消息轮次"
},
"agent": { "execute": "运行 AI 智能体和工作流" },
"hitl": {
"read": "查看人机协同队列和日志",
"write": "审批、驳回和配置人机协同"
},
"tasks": {
"read": "查看任务队列",
"write": "创建和运行任务队列",
"delete": "删除任务队列"
},
"project": {
"read": "查看项目和项目事实",
"write": "创建和更新项目及项目事实",
"delete": "删除项目和项目事实"
},
"vulnerability": {
"read": "查看漏洞",
"write": "创建和更新漏洞",
"delete": "删除漏洞"
},
"webshell": {
"read": "查看 WebShell 连接",
"write": "管理和使用 WebShell 连接",
"delete": "删除 WebShell 连接"
},
"c2": {
"read": "查看 C2 监听器、会话、任务、事件和配置",
"write": "操作 C2 监听器、会话、任务、载荷、文件和配置",
"delete": "删除 C2 对象"
},
"mcp": {
"read": "查看 MCP 状态和外部 MCP 配置",
"write": "管理外部 MCP 服务并调用 MCP 端点"
},
"knowledge": {
"read": "查看知识库和检索日志",
"write": "创建、更新、索引和扫描知识库",
"delete": "删除知识条目和检索日志"
},
"skills": {
"read": "查看 Skills 和统计信息",
"write": "创建和更新 Skills",
"delete": "删除 Skills 和统计信息"
},
"agents": {
"read": "查看 Markdown 智能体",
"write": "创建和更新 Markdown 智能体",
"delete": "删除 Markdown 智能体"
},
"roles": {
"read": "查看 AI 测试角色",
"write": "创建和更新 AI 测试角色",
"delete": "删除 AI 测试角色"
},
"workflow": {
"read": "查看工作流定义和运行记录",
"write": "创建、更新、验证、运行和恢复工作流",
"delete": "删除工作流"
},
"config": {
"read": "查看系统配置",
"write": "更新并应用系统配置"
},
"terminal": { "execute": "执行终端命令" },
"audit": {
"read": "查看和导出审计日志",
"delete": "删除审计日志"
},
"rbac": {
"read": "查看用户、平台角色、权限和授权关系",
"write": "管理用户、平台角色、权限和授权关系"
},
"notification": {
"read": "查看通知",
"write": "将通知标记为已读"
},
"robot": {
"read": "查看机器人绑定状态",
"write": "管理机器人绑定并测试回调"
},
"files": {
"read": "查看对话上传文件",
"write": "上传、编辑和重命名对话文件",
"delete": "删除对话文件"
},
"attackchain": {
"read": "查看攻击链",
"write": "重新生成攻击链"
},
"fofa": { "execute": "执行 FOFA 搜索和查询解析" },
"openapi": { "read": "读取 OpenAPI 聚合结果" },
"group": {
"read": "查看对话分组",
"write": "创建和更新对话分组",
"delete": "删除对话分组"
},
"monitor": {
"read": "查看执行监控",
"write": "取消监控中的执行",
"delete": "删除执行监控记录"
}
}
}
}
+207 -10
View File
@@ -1,6 +1,10 @@
const AUTH_STORAGE_KEY = 'cyberstrike-auth';
let authToken = null;
let authTokenExpiry = null;
let authUser = null;
let authRoles = [];
let authPermissions = new Set();
let authScope = '';
let authPromise = null;
let authPromiseResolvers = [];
let isAppInitialized = false;
@@ -9,28 +13,42 @@ function isTokenValid() {
return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now();
}
function saveAuth(token, expiresAt) {
function saveAuth(token, expiresAt, meta = {}) {
const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt);
authToken = token;
authTokenExpiry = expiry;
authUser = meta.user || null;
authRoles = Array.isArray(meta.roles) ? meta.roles : [];
authPermissions = new Set(Array.isArray(meta.permissions) ? meta.permissions : []);
authScope = meta.scope || '';
try {
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({
token,
expiresAt: expiry.toISOString(),
user: authUser,
roles: authRoles,
permissions: Array.from(authPermissions),
scope: authScope,
}));
} catch (error) {
console.warn('无法持久化认证信息:', error);
}
renderUserMenuProfile();
}
function clearAuthStorage() {
authToken = null;
authTokenExpiry = null;
authUser = null;
authRoles = [];
authPermissions = new Set();
authScope = '';
try {
localStorage.removeItem(AUTH_STORAGE_KEY);
} catch (error) {
console.warn('无法清除认证信息:', error);
}
renderUserMenuProfile();
}
function loadAuthFromStorage() {
@@ -51,6 +69,10 @@ function loadAuthFromStorage() {
}
authToken = stored.token;
authTokenExpiry = expiry;
authUser = stored.user || null;
authRoles = Array.isArray(stored.roles) ? stored.roles : [];
authPermissions = new Set(Array.isArray(stored.permissions) ? stored.permissions : []);
authScope = stored.scope || '';
return isTokenValid();
} catch (error) {
console.error('读取认证信息失败:', error);
@@ -68,6 +90,7 @@ function resolveAuthPromises(success) {
function showLoginOverlay(message = '') {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
if (!overlay) {
return;
@@ -83,7 +106,9 @@ function showLoginOverlay(message = '') {
}
}
setTimeout(function () {
if (passwordInput) {
if (usernameInput && !usernameInput.value) {
usernameInput.focus();
} else if (passwordInput) {
passwordInput.focus();
}
}, 100);
@@ -92,6 +117,7 @@ function showLoginOverlay(message = '') {
function hideLoginOverlay() {
const overlay = document.getElementById('login-overlay');
const errorBox = document.getElementById('login-error');
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
closeAppModal('login-overlay');
if (errorBox) {
@@ -101,6 +127,9 @@ function hideLoginOverlay() {
if (passwordInput) {
passwordInput.value = '';
}
if (usernameInput && !authUser) {
usernameInput.value = '';
}
}
function ensureAuthPromise() {
@@ -158,6 +187,13 @@ async function apiFetch(url, options = {}) {
: '未授权访问';
throw new Error(msg);
}
if (response.status === 403) {
const result = await response.clone().json().catch(() => ({}));
const msg = result.error || (typeof window !== 'undefined' && typeof window.t === 'function'
? window.t('auth.forbidden')
: '权限不足');
throw new Error(msg);
}
return response;
}
@@ -211,6 +247,7 @@ async function apiUploadWithProgress(url, formData, options = {}) {
async function submitLogin(event) {
event.preventDefault();
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
const errorBox = document.getElementById('login-error');
const submitBtn = document.querySelector('.login-submit');
@@ -219,6 +256,7 @@ async function submitLogin(event) {
return;
}
const username = usernameInput ? usernameInput.value.trim() : '';
const password = passwordInput.value.trim();
if (!password) {
if (errorBox) {
@@ -241,7 +279,7 @@ async function submitLogin(event) {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
body: JSON.stringify({ username, password }),
});
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.token) {
@@ -255,8 +293,14 @@ async function submitLogin(event) {
return;
}
saveAuth(result.token, result.expires_at);
saveAuth(result.token, result.expires_at, {
user: result.user,
roles: result.roles,
permissions: result.permissions,
scope: result.scope,
});
hideLoginOverlay();
applyRBACToUI();
resolveAuthPromises(true);
if (!isAppInitialized) {
await bootstrapApp();
@@ -306,9 +350,151 @@ async function bootstrapApp() {
initializeChatUI();
isAppInitialized = true;
}
applyRBACToUI();
await refreshAppData();
}
const PAGE_PERMISSION_MAP = {
dashboard: 'dashboard:read',
chat: 'chat:read',
hitl: 'hitl:read',
'info-collect': 'fofa:execute',
tasks: 'tasks:read',
workflows: 'workflow:read',
projects: 'project:read',
vulnerabilities: 'vulnerability:read',
'chat-files': 'files:read',
webshell: 'webshell:read',
c2: 'c2:read',
'c2-listeners': 'c2:read',
'c2-sessions': 'c2:read',
'c2-tasks': 'c2:read',
'c2-payloads': 'c2:read',
'c2-events': 'c2:read',
'c2-profiles': 'c2:read',
mcp: 'mcp:read',
'mcp-monitor': 'monitor:read',
'mcp-management': 'mcp:read',
knowledge: 'knowledge:read',
'knowledge-retrieval-logs': 'knowledge:read',
'knowledge-management': 'knowledge:read',
skills: 'skills:read',
'skills-monitor': 'skills:read',
'skills-management': 'skills:read',
agents: 'agents:read',
'agents-management': 'agents:read',
roles: 'roles:read',
'roles-management': 'roles:read',
'platform-rbac': 'rbac:read',
settings: 'config:read',
};
function hasPermission(permission) {
return !permission || authPermissions.has(permission);
}
function applyRBACToUI() {
document.querySelectorAll('[data-page]').forEach((el) => {
const page = el.getAttribute('data-page');
const permission = PAGE_PERMISSION_MAP[page];
if (!permission) return;
const allowed = hasPermission(permission);
el.hidden = !allowed;
el.setAttribute('aria-hidden', allowed ? 'false' : 'true');
});
const userAvatar = document.querySelector('.user-avatar-btn');
if (userAvatar && authUser && authUser.username) {
const displayName = getAuthDisplayName();
userAvatar.setAttribute('title', displayName);
userAvatar.setAttribute('aria-label', authT('header.userMenuFor', '用户菜单:{{name}}', { name: displayName }));
}
renderUserMenuProfile();
}
function authT(key, fallback, opts = {}) {
if (typeof window !== 'undefined' && typeof window.t === 'function') {
const translated = window.t(key, opts);
if (translated && translated !== key) {
return translated;
}
}
return fallback.replace(/\{\{\s*(\w+)\s*\}\}/g, function (_, name) {
return Object.prototype.hasOwnProperty.call(opts, name) ? String(opts[name]) : '';
});
}
function getAuthDisplayName() {
if (!authUser) {
return authT('header.unknownUser', '未知用户');
}
return String(authUser.display_name || authUser.displayName || authUser.username || '').trim()
|| authT('header.unknownUser', '未知用户');
}
function getAuthUsername() {
if (!authUser) {
return '-';
}
const username = String(authUser.username || '').trim();
return username ? `@${username}` : '-';
}
function getScopeLabel(scope) {
const normalized = String(scope || '').trim().toLowerCase();
const keyMap = {
all: 'header.scopeAll',
assigned: 'header.scopeAssigned',
own: 'header.scopeOwn',
};
const fallbackMap = {
all: '全部资源',
assigned: '指定资源',
own: '自己的资源',
};
const key = keyMap[normalized] || 'header.scopeUnknown';
const fallback = fallbackMap[normalized] || '资源范围未知';
return authT(key, fallback);
}
function renderUserMenuProfile() {
const displayNameEl = document.getElementById('user-menu-display-name');
const usernameEl = document.getElementById('user-menu-username');
const scopeEl = document.getElementById('user-menu-scope');
const rolesEl = document.getElementById('user-menu-roles');
const permissionsEl = document.getElementById('user-menu-permissions');
const avatarBtn = document.getElementById('user-avatar-btn') || document.querySelector('.user-avatar-btn');
const displayName = getAuthDisplayName();
const roleCount = Array.isArray(authRoles) ? authRoles.length : 0;
const permissionCount = authPermissions instanceof Set ? authPermissions.size : 0;
if (displayNameEl) displayNameEl.textContent = displayName;
if (usernameEl) usernameEl.textContent = getAuthUsername();
if (scopeEl) scopeEl.textContent = getScopeLabel(authScope);
if (rolesEl) rolesEl.textContent = authT('header.rolesCount', '{{count}} 个角色', { count: roleCount });
if (permissionsEl) permissionsEl.textContent = authT('header.permissionsCount', '{{count}} 项权限', { count: permissionCount });
if (avatarBtn && authUser) {
avatarBtn.setAttribute('title', displayName);
avatarBtn.setAttribute('aria-label', authT('header.userMenuFor', '用户菜单:{{name}}', { name: displayName }));
} else if (avatarBtn) {
avatarBtn.setAttribute('aria-label', authT('header.userMenu', '用户菜单'));
}
}
function setUserMenuOpen(open) {
const dropdown = document.getElementById('user-menu-dropdown');
const avatarBtn = document.getElementById('user-avatar-btn') || document.querySelector('.user-avatar-btn');
if (!dropdown) return;
dropdown.style.display = open ? 'block' : 'none';
if (avatarBtn) {
avatarBtn.classList.toggle('active', open);
avatarBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
}
if (open) {
renderUserMenuProfile();
}
}
// 通用工具函数
function getStatusText(status) {
const s = (status && String(status).toLowerCase()) || '';
@@ -366,7 +552,15 @@ async function initializeApp() {
method: 'GET',
});
if (response.ok) {
const result = await response.json().catch(() => ({}));
saveAuth(result.token || authToken, result.expires_at || authTokenExpiry, {
user: result.user || authUser,
roles: result.roles || authRoles,
permissions: result.permissions || Array.from(authPermissions),
scope: result.scope || authScope,
});
hideLoginOverlay();
applyRBACToUI();
resolveAuthPromises(true);
await bootstrapApp();
return;
@@ -386,7 +580,7 @@ function toggleUserMenu() {
if (!dropdown) return;
const isVisible = dropdown.style.display !== 'none';
dropdown.style.display = isVisible ? 'none' : 'block';
setUserMenuOpen(!isVisible);
}
// 点击页面其他地方时关闭下拉菜单
@@ -397,17 +591,18 @@ document.addEventListener('click', function(event) {
if (dropdown && avatarBtn &&
!dropdown.contains(event.target) &&
!avatarBtn.contains(event.target)) {
dropdown.style.display = 'none';
setUserMenuOpen(false);
}
});
document.addEventListener('languagechange', function () {
renderUserMenuProfile();
});
// 退出登录
async function logout() {
// 关闭下拉菜单
const dropdown = document.getElementById('user-menu-dropdown');
if (dropdown) {
dropdown.style.display = 'none';
}
setUserMenuOpen(false);
try {
// 先尝试调用退出API(如果token有效)
@@ -434,5 +629,7 @@ async function logout() {
// 导出函数供HTML使用
window.toggleUserMenu = toggleUserMenu;
window.logout = logout;
window.hasPermission = hasPermission;
window.applyRBACToUI = applyRBACToUI;
document.addEventListener('DOMContentLoaded', initializeApp);
+118 -8
View File
@@ -2458,6 +2458,32 @@ function syncProcessDetailButtonLabels(messageId, expanded) {
});
}
/** 懒加载占位提示可点击,与工具栏「展开详情」行为一致 */
function bindProcessDetailsLazyHint(hostEl, messageId) {
if (!hostEl || !messageId) return;
const emptyEl = hostEl.classList && hostEl.classList.contains('progress-timeline-empty')
? hostEl
: hostEl.querySelector('.progress-timeline-empty');
if (!emptyEl || emptyEl.dataset.lazyHintBound === '1') return;
emptyEl.dataset.lazyHintBound = '1';
emptyEl.classList.add('progress-timeline-lazy-clickable');
emptyEl.setAttribute('role', 'button');
emptyEl.setAttribute('tabindex', '0');
const activate = () => {
if (typeof toggleProcessDetails === 'function') {
toggleProcessDetails(null, messageId);
}
};
emptyEl.addEventListener('click', activate);
emptyEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activate();
}
});
}
window.bindProcessDetailsLazyHint = bindProcessDetailsLazyHint;
// 渲染过程详情
// options.append=true 时分页追加;options.markLoaded=false 时保留 lazy 标记(分页加载中)
function renderProcessDetails(messageId, processDetails, options) {
@@ -2465,6 +2491,13 @@ function renderProcessDetails(messageId, processDetails, options) {
const appendMode = !!renderOpts.append;
const prependMode = !!renderOpts.prepend;
const markLoaded = renderOpts.markLoaded !== false;
const toolStatusByProcessDetailId = new Map();
if (Array.isArray(renderOpts.toolExecutions)) {
renderOpts.toolExecutions.forEach((execution) => {
if (!execution || !execution.processDetailId) return;
toolStatusByProcessDetailId.set(String(execution.processDetailId), String(execution.status || '').toLowerCase());
});
}
const messageElement = document.getElementById(messageId);
if (!messageElement) {
return;
@@ -2529,6 +2562,7 @@ function renderProcessDetails(messageId, processDetails, options) {
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
let lazyHint = expandLabel + '(点击后加载迭代详情)';
timeline.innerHTML = '<div class="progress-timeline-empty">' + lazyHint + '</div>';
bindProcessDetailsLazyHint(timeline, messageId);
timeline.classList.remove('expanded');
prefetchProcessDetailsSummaryHint(messageId, messageElement);
return;
@@ -2725,6 +2759,9 @@ function renderProcessDetails(messageId, processDetails, options) {
processDetailId: detail.id || '',
createdAt: detail.createdAt
};
if (eventType === 'tool_call' && detail.id && toolStatusByProcessDetailId.has(String(detail.id))) {
timelineOpts.toolStatus = toolStatusByProcessDetailId.get(String(detail.id));
}
if (eventType === 'tool_call' && data._mergedResult) {
timelineOpts.mergedResult = data._mergedResult;
}
@@ -2781,6 +2818,7 @@ function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoa
lazyHint.textContent = (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') +
'(点击后加载完整过程详情)';
timeline.appendChild(lazyHint);
bindProcessDetailsLazyHint(lazyHint, messageElement.id);
}
const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt');
@@ -2841,6 +2879,7 @@ function prefetchProcessDetailsSummaryHint(messageId, messageElement) {
const empty = timeline.querySelector('.progress-timeline-empty');
if (empty) {
empty.textContent = hint;
bindProcessDetailsLazyHint(timeline, messageId);
}
})
.catch(() => {});
@@ -2999,16 +3038,42 @@ function normalizeToolExecutionSummaryForButton(raw) {
};
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
function cacheToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return [];
const normalized = summaries
.map(normalizeToolExecutionSummaryForButton)
.filter((item) => item.toolName || item.executionId || item.toolCallId);
if (normalized.length > 0) {
messageElement.dataset.toolExecutionSummaries = JSON.stringify(normalized);
}
return normalized;
}
function getCachedToolExecutionSummaries(messageElement) {
if (!messageElement || !messageElement.dataset || !messageElement.dataset.toolExecutionSummaries) return [];
try {
const parsed = JSON.parse(messageElement.dataset.toolExecutionSummaries);
return Array.isArray(parsed) ? parsed.map(normalizeToolExecutionSummaryForButton) : [];
} catch (e) {
delete messageElement.dataset.toolExecutionSummaries;
return [];
}
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
const normalized = cacheToolExecutionSummaries(messageElement, summaries);
if (normalized.length > 0) {
messageElement.dataset.pendingToolExecutionSummaries = JSON.stringify(normalized);
} else {
delete messageElement.dataset.pendingToolExecutionSummaries;
}
const renderedToolList = messageElement.querySelector('.mcp-tool-list');
if (normalized.length > 0 && renderedToolList && renderedToolList.querySelector('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]')) {
appendMcpCallSummaryButtons(messageElement, normalized);
delete messageElement.dataset.pendingToolExecutionSummaries;
delete messageElement.dataset.pendingMcpExecutionIds;
}
if (typeof syncMcpToolsToggleButton === 'function') {
syncMcpToolsToggleButton(messageElement);
}
@@ -3202,6 +3267,34 @@ async function focusToolExecutionInProcessDetails(messageElement, summary, index
}, 2200);
}
async function resolveToolExecutionSummaryForFocus(messageElement, executionId, index) {
const wantedExecutionId = executionId == null ? '' : String(executionId).trim();
let summaries = getCachedToolExecutionSummaries(messageElement);
let item = wantedExecutionId
? summaries.find((summary) => summary.executionId === wantedExecutionId)
: summaries[index];
if (item && (item.processDetailId || item.toolCallId)) return item;
const backendId = messageElement && messageElement.dataset
? String(messageElement.dataset.backendMessageId || '').trim()
: '';
if (!backendId || typeof apiFetch !== 'function') return item || null;
try {
const res = await apiFetch('/api/messages/' + encodeURIComponent(backendId) + '/process-details?summary=1');
const payload = await res.json().catch(() => ({}));
if (!res.ok || !payload.summary || !Array.isArray(payload.summary.toolExecutions)) {
return item || null;
}
summaries = cacheToolExecutionSummaries(messageElement, payload.summary.toolExecutions);
item = wantedExecutionId
? summaries.find((summary) => summary.executionId === wantedExecutionId)
: summaries[index];
return item || null;
} catch (e) {
return item || null;
}
}
function toggleMcpToolList(assistantMessageId) {
const messageEl = document.getElementById(assistantMessageId);
if (!messageEl) return;
@@ -3255,7 +3348,14 @@ function appendMcpCallButtons(messageElement, executionIds) {
detailBtn.dataset.execId = execId;
detailBtn.dataset.execIndex = String(index + 1);
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
detailBtn.onclick = () => showMCPDetail(execId);
detailBtn.onclick = async () => {
const summary = await resolveToolExecutionSummaryForFocus(messageElement, execId, index);
if (summary && (summary.processDetailId || summary.toolCallId)) {
await focusToolExecutionInProcessDetails(messageElement, summary, index);
return;
}
showMCPDetail(execId);
};
toolList.appendChild(detailBtn);
});
batchUpdateButtonToolNames(toolList, executionIds);
@@ -3273,10 +3373,16 @@ function appendMcpCallSummaryButtons(messageElement, summaries) {
const item = normalizeToolExecutionSummaryForButton(raw);
const key = item.executionId || item.toolCallId || `${item.toolName || 'tool'}-${index + 1}`;
const selector = '.mcp-detail-btn[data-tool-summary="' + CSS.escape(String(key)) + '"]';
if (toolList.querySelector(selector) || (item.executionId && toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]'))) {
const existingSummaryBtn = toolList.querySelector(selector);
const existingExecBtn = item.executionId
? toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]')
: null;
if (existingSummaryBtn) {
return;
}
const btn = document.createElement('button');
// 历史会话可能先按 executionId 渲染旧按钮,随后摘要才异步到达。
// 复用并升级该按钮,避免它永久保留“只看执行详情、不定位上下文”的旧处理器。
const btn = existingExecBtn || document.createElement('button');
btn.className = 'mcp-detail-btn';
btn.dataset.toolSummary = key;
if (item.executionId) {
@@ -3294,7 +3400,9 @@ function appendMcpCallSummaryButtons(messageElement, summaries) {
};
}
renderToolExecutionButtonContent(btn, item.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'), String(index + 1), item.status);
toolList.appendChild(btn);
if (!existingExecBtn) {
toolList.appendChild(btn);
}
});
syncMcpToolsToggleButton(messageElement);
}
@@ -3363,7 +3471,8 @@ function getToolExecutionStatusLabel(status) {
failed: 'mcpMonitor.statusFailed',
running: 'mcpMonitor.statusRunning',
cancelled: 'mcpMonitor.statusCancelled',
pending: 'mcpMonitor.statusPending'
pending: 'mcpMonitor.statusPending',
result_missing: 'timeline.resultMissing'
};
const key = keyMap[normalized];
if (key) {
@@ -3376,7 +3485,8 @@ function getToolExecutionStatusLabel(status) {
failed: '失败',
running: '运行中',
cancelled: '已取消',
pending: '等待中'
pending: '等待中',
result_missing: '结果记录缺失'
};
return fallback[normalized] || '';
}
+109 -41
View File
@@ -1357,6 +1357,10 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut
</div>
</div>
`;
if (typeof window.bindProcessDetailsLazyHint === 'function') {
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) window.bindProcessDetailsLazyHint(tl, assistantMessageId);
}
}
const expandLabel = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
@@ -1492,11 +1496,13 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId,
throw new Error((j && j.error) ? j.error : String(res.status));
}
const details = (j && Array.isArray(j.processDetails)) ? j.processDetails : [];
const toolExecutions = (j && Array.isArray(j.toolExecutions)) ? j.toolExecutions : [];
const hasMore = !!(j && j.hasMore);
renderProcessDetails(assistantMessageId, details, {
append: !isFirst || opts.append,
prepend: prepend,
markLoaded: autoLoadAll ? !hasMore : true
markLoaded: autoLoadAll ? !hasMore : true,
toolExecutions: toolExecutions
});
const responseOffset = j && typeof j.offset === 'number' ? j.offset : offset;
const total = j && typeof j.total === 'number' ? j.total : responseOffset + details.length;
@@ -1525,6 +1531,81 @@ async function loadProcessDetailsPaginated(assistantMessageId, backendMessageId,
window.loadProcessDetailsPaginated = loadProcessDetailsPaginated;
function resolveEventBackendMessageId(eventData) {
if (!eventData || typeof eventData !== 'object') return '';
const raw = eventData.messageId != null ? eventData.messageId : eventData.assistantMessageId;
return raw != null ? String(raw).trim() : '';
}
function triggerLazyProcessDetailsLoad(assistantMessageId, backendMessageId, detailsContainer) {
if (!assistantMessageId || !backendMessageId || !detailsContainer) return false;
if (detailsContainer.dataset.loading === '1') return false;
const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
detailsContainer.dataset.loading = '1';
const timeline = detailsContainer.querySelector('.progress-timeline');
if (timeline) {
timeline.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '</div>';
}
loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { autoLoadAll: false })
.catch((e) => {
console.error('加载过程详情失败:', e);
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('chat.noProcessDetail') : '暂无过程详情(加载失败)') + '</div>';
if (typeof window.bindProcessDetailsLazyHint === 'function') {
window.bindProcessDetailsLazyHint(tl, assistantMessageId);
}
}
detailsContainer.dataset.lazyNotLoaded = '1';
detailsContainer.dataset.loaded = '0';
})
.finally(() => {
detailsContainer.dataset.loading = '0';
if (detailsContainer.dataset.userExpanded === '1') {
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.classList.add('expanded');
}
if (typeof syncProcessDetailButtonLabels === 'function') {
syncProcessDetailButtonLabels(assistantMessageId, true);
} else {
document.querySelectorAll('#' + assistantMessageId + ' .process-detail-btn').forEach((btn) => {
btn.innerHTML = '<span>' + collapseT + '</span>';
});
}
}
});
return true;
}
function maybeReloadLazyProcessDetails(assistantMessageId) {
const detailsContainer = document.getElementById('process-details-' + assistantMessageId);
if (!detailsContainer) return;
const isLazy = detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1';
if (!isLazy) return;
const timeline = detailsContainer.querySelector('.progress-timeline');
const wantsExpanded = detailsContainer.dataset.userExpanded === '1' ||
!!(timeline && timeline.classList.contains('expanded'));
if (!wantsExpanded) return;
const messageEl = document.getElementById(assistantMessageId);
const backendId = messageEl && messageEl.dataset ? String(messageEl.dataset.backendMessageId || '').trim() : '';
if (!backendId) return;
triggerLazyProcessDetailsLoad(assistantMessageId, backendId, detailsContainer);
}
function scheduleProcessDetailsLoadWhenReady(assistantMessageId, detailsContainer, attempt) {
if (!assistantMessageId || !detailsContainer) return;
const tries = typeof attempt === 'number' ? attempt : 0;
if (tries > 25) return;
const messageEl = document.getElementById(assistantMessageId);
const backendId = messageEl && messageEl.dataset ? String(messageEl.dataset.backendMessageId || '').trim() : '';
if (backendId) {
triggerLazyProcessDetailsLoad(assistantMessageId, backendId, detailsContainer);
return;
}
setTimeout(() => scheduleProcessDetailsLoadWhenReady(assistantMessageId, detailsContainer, tries + 1), 200);
}
// 切换过程详情显示
function toggleProcessDetails(progressId, assistantMessageId) {
const detailsId = 'process-details-' + assistantMessageId;
@@ -1539,42 +1620,10 @@ function toggleProcessDetails(progressId, assistantMessageId) {
if (maybeLazy) {
const messageEl = document.getElementById(assistantMessageId);
const backendMessageId = messageEl && messageEl.dataset ? messageEl.dataset.backendMessageId : '';
if (backendMessageId && typeof apiFetch === 'function' && typeof renderProcessDetails === 'function') {
if (detailsContainer.dataset.loading === '1') {
// 正在加载中,避免重复请求
} else {
detailsContainer.dataset.loading = '1';
const timeline = detailsContainer.querySelector('.progress-timeline');
if (timeline) {
timeline.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('common.loading') : '加载中…') + '</div>';
}
loadProcessDetailsPaginated(assistantMessageId, backendMessageId, { autoLoadAll: false })
.catch((e) => {
console.error('加载过程详情失败:', e);
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.innerHTML = '<div class="progress-timeline-empty">' + ((typeof window.t === 'function') ? window.t('chat.noProcessDetail') : '暂无过程详情(加载失败)') + '</div>';
}
detailsContainer.dataset.lazyNotLoaded = '1';
detailsContainer.dataset.loaded = '0';
})
.finally(() => {
detailsContainer.dataset.loading = '0';
if (detailsContainer.dataset.userExpanded === '1') {
const tl = detailsContainer.querySelector('.progress-timeline');
if (tl) {
tl.classList.add('expanded');
}
if (typeof syncProcessDetailButtonLabels === 'function') {
syncProcessDetailButtonLabels(assistantMessageId, true);
} else {
document.querySelectorAll('#' + assistantMessageId + ' .process-detail-btn').forEach((btn) => {
btn.innerHTML = '<span>' + collapseT + '</span>';
});
}
}
});
}
if (backendMessageId) {
triggerLazyProcessDetailsLoad(assistantMessageId, backendMessageId, detailsContainer);
} else {
scheduleProcessDetailsLoadWhenReady(assistantMessageId, detailsContainer, 0);
}
}
@@ -1647,6 +1696,7 @@ function applyBackendMessageIdToAssistantDom(domAssistantId, backendMessageId) {
if (typeof attachDeleteTurnButton === 'function') {
attachDeleteTurnButton(el);
}
maybeReloadLazyProcessDetails(domAssistantId);
}
/** 将后端用户消息 ID 绑定到最后一条尚未绑定 backendMessageId 的用户气泡 */
@@ -2480,7 +2530,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 复用已有助手消息(若有),避免终态事件重复插入消息
{
const preferredMessageId = event.data && event.data.messageId ? event.data.messageId : null;
const preferredMessageId = resolveEventBackendMessageId(event.data) || null;
const { assistantId, assistantElement } = upsertTerminalAssistantMessage(event.message, preferredMessageId);
if (assistantId && preferredMessageId) {
applyBackendMessageIdToAssistantDom(assistantId, preferredMessageId);
@@ -2489,6 +2539,8 @@ function handleStreamEvent(event, progressElement, progressId,
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
integrateProgressToMCPSection(progressId, assistantId, typeof getMcpIds === 'function' ? (getMcpIds() || []) : []);
} else if (preferredMessageId) {
maybeReloadLazyProcessDetails(assistantId);
}
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
@@ -2501,7 +2553,7 @@ function handleStreamEvent(event, progressElement, progressId,
// Close any remaining running tool calls for this progress.
finalizeOutstandingToolCallsForProgress(progressId, 'failed');
break;
case 'response_start': {
const responseTaskState = progressTaskState.get(progressId);
const responseOriginalConversationId = responseTaskState?.conversationId;
@@ -2721,7 +2773,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 复用已有助手消息(若有),避免终态事件重复插入消息
{
const preferredMessageId = event.data && event.data.messageId ? event.data.messageId : null;
const preferredMessageId = resolveEventBackendMessageId(event.data) || null;
const { assistantId, assistantElement } = upsertTerminalAssistantMessage(event.message, preferredMessageId);
if (assistantId && preferredMessageId) {
applyBackendMessageIdToAssistantDom(assistantId, preferredMessageId);
@@ -2730,6 +2782,8 @@ function handleStreamEvent(event, progressElement, progressId,
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
integrateProgressToMCPSection(progressId, assistantId, typeof getMcpIds === 'function' ? (getMcpIds() || []) : []);
} else if (preferredMessageId) {
maybeReloadLazyProcessDetails(assistantId);
}
setTimeout(() => {
collapseAllProgressDetails(assistantId, progressId);
@@ -3961,6 +4015,7 @@ function addTimelineItem(timeline, type, options) {
item.dataset.toolCallId = String(d.toolCallId).trim();
}
const merged = options.mergedResult || d._mergedResult;
const terminalStatus = String(options.toolStatus || '').toLowerCase();
if (merged) {
item.dataset.toolResultMerged = '1';
item.dataset.toolSuccess = merged.success !== false ? '1' : '0';
@@ -3968,6 +4023,12 @@ function addTimelineItem(timeline, type, options) {
if (d._mergedResultDetailId) {
item.dataset.toolResultDetailId = String(d._mergedResultDetailId);
}
} else if (terminalStatus === 'completed' || terminalStatus === 'failed') {
item.dataset.toolSuccess = terminalStatus === 'completed' ? '1' : '0';
item.classList.add(terminalStatus === 'completed' ? 'tool-call-completed' : 'tool-call-failed');
} else if (terminalStatus === 'result_missing') {
item.classList.add('tool-call-incomplete');
item.title = typeof window.t === 'function' ? window.t('timeline.resultMissing') : '结果记录缺失';
}
}
if (type === 'hitl_interrupt' && options.data && options.data.interruptId != null && String(options.data.interruptId).trim() !== '') {
@@ -4041,19 +4102,26 @@ function addTimelineItem(timeline, type, options) {
const data = options.data;
const args = parseToolCallArgsFromData(data);
const merged = options.mergedResult || data._mergedResult;
const terminalStatus = String(options.toolStatus || '').toLowerCase();
const hasTerminalStatus = terminalStatus === 'completed' || terminalStatus === 'failed';
const hasHistoricalStatus = hasTerminalStatus || terminalStatus === 'result_missing';
if (merged) {
if (merged.success !== false) {
item.classList.add('tool-call-completed');
} else {
item.classList.add('tool-call-failed');
}
} else if (hasTerminalStatus) {
item.classList.add(terminalStatus === 'completed' ? 'tool-call-completed' : 'tool-call-failed');
} else if (terminalStatus === 'result_missing') {
item.classList.add('tool-call-incomplete');
} else if (!options.skipPendingResult) {
item.classList.add('tool-call-running');
}
setToolCallDetailState(item, {
args: args,
resultData: merged || null,
pending: !merged && !options.skipPendingResult,
pending: !merged && !hasHistoricalStatus && !options.skipPendingResult,
processDetailId: options.processDetailId || '',
resultDetailId: data._mergedResultDetailId || '',
payloadDeferred: data._payloadDeferred === true || (merged && merged._payloadDeferred === true),
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -81,7 +81,7 @@ function initRouter() {
const hashParts = hash.split('?');
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(500);
@@ -472,6 +472,11 @@ async function initPage(pageId) {
});
}
break;
case 'platform-rbac':
if (typeof initPlatformRbacPage === 'function') {
initPlatformRbacPage();
}
break;
case 'workflows':
if (typeof refreshWorkflows === 'function') {
refreshWorkflows();
@@ -538,7 +543,7 @@ document.addEventListener('DOMContentLoaded', function() {
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
switchPage(pageId);
if (pageId === 'chat') {
scheduleChatConversationFromHash(200);
+10
View File
@@ -509,6 +509,13 @@ function syncC2NavFromConfig(cfg) {
// 切换设置分类
function switchSettingsSection(section) {
if (section === 'rbac') {
if (typeof switchPage === 'function') {
switchPage('platform-rbac');
}
return;
}
// 更新导航项状态
document.querySelectorAll('.settings-nav-item').forEach(item => {
item.classList.remove('active');
@@ -3660,3 +3667,6 @@ document.addEventListener('languagechange', function () {
console.warn('languagechange MCP refresh failed', e);
}
});
window.initSettingsCustomSelects = initSettingsCustomSelects;
window.refreshSettingsCustomSelects = refreshSettingsCustomSelects;
+2
View File
@@ -1048,6 +1048,7 @@ function probeWebshellConnection(conn) {
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: buildWebshellProbeCommand(probeToken)
})
})
@@ -3854,6 +3855,7 @@ function execWebshellCommand(conn, command) {
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: command
})
}).then(function (r) { return r.json(); })