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
+3 -3
View File
@@ -203,9 +203,9 @@ multi_agent:
tool_search_always_visible_tools: [read_file, glob, grep, analyze_image, write_file, edit_file, execute, task, transfer_to_agent, exit, write_todos, skill, tool_search, TaskCreate, TaskGet, TaskUpdate, TaskList, record_vulnerability, list_vulnerabilities, get_vulnerability, list_knowledge_risk_types, search_knowledge_base, webshell_exec, webshell_file_list, webshell_file_read, webshell_file_write, manage_webshell_list, manage_webshell_add, manage_webshell_update, manage_webshell_delete, manage_webshell_test, batch_task_list, batch_task_get, batch_task_start, batch_task_rerun, batch_task_pause, batch_task_update_metadata, batch_task_update_schedule, batch_task_schedule_enabled, batch_task_update_task, batch_task_remove_task, batch_task_delete, batch_task_create, batch_task_add_task, http-framework-test, exec] # 后端内置常驻工具白名单(优先于 always_visible 数量策略)
plantask_enable: true # P0:主代理挂载 TaskCreate/Get/Update/List 结构化任务板;需 eino_skills 可用且 skills_dir 存在
plantask_rel_dir: .eino/plantask # 任务文件相对 skills_dir,按会话分子目录:skills/.eino/plantask/<conversationId>/
reduction_enable: true # true:大工具输出截断/落盘以控上下文;依赖与 plantask 相同的 eino local 写盘后端,无后端时不挂载
reduction_enable: true # true:大工具输出截断/落盘以控上下文;后端会独立创建,不依赖 eino_skills 是否启用
reduction_max_length_for_trunc: 50000 # 单条工具结果超过该字符数(bytes)时截断并落盘(由 reduction 中间件处理)
reduction_max_tokens_for_clear: 160000 # 历史工具结果清理阈值(tokens),超阈值时在模型调用前清理旧结果
reduction_max_tokens_for_clear: 60000 # 历史工具结果清理阈值(tokens),应低于 max_total_tokens * summarization_trigger_ratio
reduction_root_dir: "" # 非空:截断/清理内容落盘根路径;空:使用系统临时目录下按会话隔离的默认路径
reduction_clear_exclude: [] # 不参与「清理阶段」的工具名额外列表(会与 task/transfer/exit 等内置排除项合并);需要时用 YAML 列表填写
reduction_sub_agents: true # true:子代理也挂 reductionfalse:仅编排主代理使用 reduction
@@ -417,4 +417,4 @@ project:
# 事实关系速览段预算(从索引总预算中预留)
fact_index_path_max_runes: 10000
fact_summary_max_runes: 24000
default_inject_deprecated: false
default_inject_deprecated: false
+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(); })
+337 -4
View File
@@ -39,9 +39,13 @@
<span class="brand-wordmark__core">CyberStrike</span><span class="brand-wordmark__ai">AI</span>
</span>
</h2>
<p class="login-subtitle" data-i18n="login.subtitle">请输入配置中的访问密码</p>
<p class="login-subtitle" data-i18n="login.subtitle">使用平台账号登录,默认管理员为 admin。</p>
</div>
<form id="login-form" class="login-form">
<div class="form-group">
<label for="login-username" data-i18n="login.usernameLabel">用户名</label>
<input type="text" id="login-username" placeholder="admin" autocomplete="username" data-i18n="login.usernamePlaceholder" data-i18n-attr="placeholder" />
</div>
<div class="form-group">
<label for="login-password" data-i18n="login.passwordLabel">密码</label>
<input type="password" id="login-password" placeholder="输入登录密码" required autocomplete="current-password" data-i18n="login.passwordPlaceholder" data-i18n-attr="placeholder" />
@@ -123,14 +127,29 @@
</div>
</div>
<div class="user-menu-container">
<button class="user-avatar-btn" onclick="toggleUserMenu()" data-i18n="header.userMenu" data-i18n-attr="title" data-i18n-skip-text="true" title="用户菜单">
<button id="user-avatar-btn" class="user-avatar-btn" onclick="toggleUserMenu()" data-i18n="header.userMenu" data-i18n-attr="title" data-i18n-skip-text="true" title="用户菜单" aria-haspopup="menu" aria-expanded="false" aria-controls="user-menu-dropdown">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="user-menu-dropdown" class="user-menu-dropdown" style="display: none;">
<div class="user-menu-item" onclick="logout()">
<div id="user-menu-dropdown" class="user-menu-dropdown" role="menu" style="display: none;">
<div class="user-menu-profile" role="presentation">
<div class="user-menu-profile-row">
<div class="user-menu-profile-main">
<div id="user-menu-current-label" class="user-menu-profile-label" data-i18n="header.currentUser">当前用户</div>
<div id="user-menu-display-name" class="user-menu-profile-name">-</div>
<div id="user-menu-username" class="user-menu-profile-username">-</div>
</div>
</div>
<div class="user-menu-profile-meta" aria-labelledby="user-menu-current-label">
<span id="user-menu-scope" class="user-menu-profile-pill">-</span>
<span id="user-menu-roles" class="user-menu-profile-pill">-</span>
<span id="user-menu-permissions" class="user-menu-profile-pill">-</span>
</div>
</div>
<div class="user-menu-divider" role="separator"></div>
<div class="user-menu-item" role="menuitem" tabindex="0" onclick="logout()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();logout();}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="16 17 21 12 16 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -370,6 +389,21 @@
</div>
</div>
</div>
<div class="nav-item" data-page="platform-rbac">
<div class="nav-item-content" data-title="平台权限" onclick="switchPage('platform-rbac')" data-i18n="nav.platformRbac" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="7.5" cy="14.5" r="3.5"></circle>
<path d="M10.3 12.3 21 1.6"></path>
<path d="m15 7 2 2"></path>
<path d="m18 4 2 2"></path>
<path d="M4 4h4"></path>
<path d="M6 2v4"></path>
<path d="M18 18h4"></path>
<path d="M20 16v4"></path>
</svg>
<span data-i18n="nav.platformRbac">平台权限</span>
</div>
</div>
<div class="nav-item" data-page="settings">
<div class="nav-item-content" data-title="系统设置" onclick="switchPage('settings')" data-i18n="nav.settings" data-i18n-attr="data-title" data-i18n-skip-text="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -4109,9 +4143,220 @@
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 平台权限页面 -->
<div id="page-platform-rbac" class="page platform-rbac-page">
<div class="page-header">
<div>
<h2 data-i18n="rbac.title">平台权限</h2>
</div>
<div class="page-header-actions">
<button class="btn-secondary" type="button" onclick="loadPlatformRbac()" data-i18n="common.refresh">刷新</button>
<button class="btn-secondary" type="button" onclick="openRbacRoleModal()" data-i18n="rbac.createRole">新建角色</button>
<button class="btn-primary" type="button" onclick="openRbacUserModal()" data-i18n="rbac.createUser">添加成员</button>
</div>
</div>
<div class="page-content">
<div class="rbac-topbar">
<div class="rbac-overview" id="rbac-overview">
<div class="rbac-metric rbac-metric-users">
<strong id="rbac-metric-users">0</strong>
<span data-i18n="rbac.metricUsersPill">个账号</span>
</div>
<div class="rbac-metric rbac-metric-enabled">
<strong id="rbac-metric-enabled">0</strong>
<span data-i18n="rbac.metricEnabledPill">个启用账号</span>
</div>
<div class="rbac-metric rbac-metric-roles">
<strong id="rbac-metric-roles">0</strong>
<span data-i18n="rbac.metricRolesPill">个角色</span>
</div>
<div class="rbac-metric rbac-metric-assignments">
<strong id="rbac-metric-assignments">0</strong>
<span data-i18n="rbac.metricAssignmentsPill">项授权</span>
</div>
</div>
<p id="rbac-metric-context" class="rbac-metric-context" hidden></p>
<div class="rbac-workspace-nav" role="tablist" aria-label="权限管理视图">
<button type="button" class="rbac-workspace-tab is-active" role="tab" aria-selected="true" data-rbac-view="users" onclick="switchRbacView('users')">
<span data-i18n="rbac.memberManagement">成员管理</span>
<small data-i18n="rbac.memberManagementHint">为成员分配角色与数据范围</small>
</button>
<button type="button" class="rbac-workspace-tab" role="tab" aria-selected="false" data-rbac-view="roles" onclick="switchRbacView('roles')">
<span data-i18n="rbac.roleManagement">角色管理</span>
<small data-i18n="rbac.roleManagementHint">定义权限集合与默认可见范围</small>
</button>
</div>
</div>
<div id="rbac-view-users" class="rbac-workspace-view rbac-layout">
<section class="rbac-panel rbac-panel-users">
<div class="rbac-panel-head">
<div>
<h3 data-i18n="rbac.users">用户</h3>
<p class="rbac-muted" data-i18n="rbac.usersHint">按账号选择后管理角色与资源范围。</p>
</div>
<span id="rbac-users-count" class="rbac-count">0</span>
</div>
<div class="rbac-search">
<input id="rbac-user-search" class="form-input" type="search" placeholder="搜索用户名或显示名称" data-i18n="rbac.searchUsers" data-i18n-attr="placeholder" oninput="setRbacUserSearch(this.value)">
</div>
<div id="rbac-users-list" class="rbac-list">
<div class="loading-spinner" data-i18n="knowledgePage.loading">加载中...</div>
</div>
</section>
<section class="rbac-panel rbac-panel-detail">
<div class="rbac-detail-head">
<div>
<h3 id="rbac-selected-title" data-i18n="rbac.selectUser">选择用户</h3>
<p id="rbac-selected-subtitle" class="rbac-muted" data-i18n="rbac.selectUserHint">选择一个平台用户后编辑角色、状态与资源授权。</p>
</div>
<div class="rbac-detail-actions">
<button class="btn-secondary btn-small btn-delete" type="button" id="rbac-delete-user-btn" onclick="deleteSelectedRbacUser()" disabled data-i18n="common.delete">删除</button>
<button class="btn-primary btn-small" type="button" id="rbac-edit-user-btn" onclick="openRbacUserModalForSelected()" disabled data-i18n="common.edit">编辑</button>
</div>
</div>
<div id="rbac-selected-summary" class="rbac-selected-summary"></div>
<div id="rbac-effective-permissions" class="rbac-effective-permissions" hidden></div>
<div id="rbac-no-role-notice" class="rbac-no-role-notice" hidden></div>
<div class="rbac-tabs" role="tablist">
<button type="button" class="rbac-tab is-active" data-rbac-tab="roles" onclick="switchRbacTab('roles')" data-i18n="rbac.tabRoles">角色权限</button>
<button type="button" class="rbac-tab" data-rbac-tab="assignments" onclick="switchRbacTab('assignments')" data-i18n="rbac.tabAssignments">资源授权</button>
<button type="button" class="rbac-tab" data-rbac-tab="audit" onclick="switchRbacTab('audit')" data-i18n="rbac.tabAudit">变更记录</button>
</div>
<div id="rbac-tab-roles" class="rbac-tab-panel">
<div class="rbac-section-toolbar">
<div>
<strong data-i18n="rbac.roleMembership">角色成员关系</strong>
<p class="rbac-muted" data-i18n="rbac.roleMembershipHint">角色决定能做什么,资源范围决定能看到哪些数据。</p>
</div>
<div class="rbac-role-save-actions">
<span id="rbac-role-unsaved" class="rbac-unsaved-indicator" hidden data-i18n="rbac.unsavedChanges">有未保存的变更</span>
<button id="rbac-role-reset-btn" class="btn-secondary btn-small" type="button" onclick="resetSelectedUserRoles()" disabled data-i18n="common.cancel">取消</button>
<button id="rbac-role-apply-btn" class="btn-primary btn-small" type="button" onclick="saveSelectedUserRoles()" disabled data-i18n="rbac.saveRoleChanges">保存角色变更</button>
</div>
</div>
<div id="rbac-roles-list" class="rbac-role-grid"></div>
</div>
<div id="rbac-tab-assignments" class="rbac-tab-panel" hidden>
<div class="rbac-assignment-controls">
<div class="rbac-assignment-toolbar">
<div class="rbac-assignment-toolbar-main">
<div class="rbac-assignment-field">
<label for="rbac-assignment-type" data-i18n="rbac.resourceType">资源类型</label>
<select id="rbac-assignment-type" class="form-select rbac-form-select" onchange="changeRbacResourceType()">
<option value="conversation" data-i18n="rbac.resourceTypes.conversation">对话</option>
<option value="project" data-i18n="rbac.resourceTypes.project">项目</option>
<option value="batch_task" data-i18n="rbac.resourceTypes.batch_task">批量任务</option>
<option value="vulnerability" data-i18n="rbac.resourceTypes.vulnerability">漏洞</option>
<option value="webshell" data-i18n="rbac.resourceTypes.webshell">WebShell 连接</option>
<option value="c2_listener" data-i18n="rbac.resourceTypes.c2_listener">C2 监听器</option>
</select>
</div>
<div class="rbac-assignment-field rbac-assignment-field--search">
<label for="rbac-resource-search" data-i18n="rbac.searchResource">搜索真实资源</label>
<input id="rbac-resource-search" class="form-input" type="search" placeholder="按名称或 ID 搜索" data-i18n="rbac.searchResourcePlaceholder" data-i18n-attr="placeholder" oninput="queueRbacResourceSearch(this.value)">
</div>
</div>
<details class="rbac-advanced-assignment rbac-advanced-assignment--inline">
<summary data-i18n="rbac.advancedAssignment">高级:直接输入资源 ID</summary>
<div class="rbac-advanced-assignment-body">
<input id="rbac-assignment-id" class="form-input" type="text" placeholder="输入资源 ID;多个 ID 用逗号分隔" data-i18n="rbac.resourceIdPlaceholder" data-i18n-attr="placeholder" oninput="syncRbacAssignmentSubmit()">
<span data-i18n="rbac.advancedAssignmentHint">适用于从日志或 API 复制的已知 ID,提交时仍会验证资源是否存在。</span>
</div>
</details>
</div>
<p class="rbac-assignment-help" data-i18n="rbac.assignmentHelp">仅“指定资源”范围的角色需要单独授权。项目授权会覆盖该项目下的对话和漏洞。</p>
</div>
<div id="rbac-pending-bar" class="rbac-pending-bar" hidden>
<div class="rbac-pending-bar-head">
<strong id="rbac-pending-title" data-i18n="rbac.pendingGrants">待授权</strong>
<span id="rbac-pending-count" class="rbac-pill is-info">0</span>
</div>
<div id="rbac-pending-list" class="rbac-pending-list"></div>
<div class="rbac-pending-bar-actions">
<button type="button" class="btn-secondary btn-small" onclick="clearRbacResourceSelection()" data-i18n="rbac.clearSelection">清空选择</button>
<button type="button" class="btn-primary btn-small" onclick="createRbacAssignment()" data-i18n="rbac.confirmGrant">确认授权</button>
</div>
</div>
<div class="rbac-assignment-columns">
<section class="rbac-resource-section">
<div class="rbac-resource-section-head">
<div>
<strong data-i18n="rbac.availableResources">可授权资源</strong>
<span data-i18n="rbac.availableResourcesHint">勾选当前页资源,可跨页累计选择</span>
</div>
</div>
<div id="rbac-resource-picker" class="rbac-resource-picker"></div>
<div id="rbac-resource-pagination" class="rbac-pagination">
<button type="button" class="btn-secondary btn-small" data-rbac-page-previous onclick="changeRbacResourcePage(-1)" data-i18n="rbac.pagination.previous">上一页</button>
<span data-rbac-page-info>第 1 页</span>
<button type="button" class="btn-secondary btn-small" data-rbac-page-next onclick="changeRbacResourcePage(1)" data-i18n="rbac.pagination.next">下一页</button>
</div>
</section>
<section class="rbac-resource-section">
<div class="rbac-resource-section-head">
<div>
<strong data-i18n="rbac.currentAssignments">当前单独授权</strong>
<span><span id="rbac-current-assignment-count">0</span> <span data-i18n="rbac.items"></span></span>
</div>
<div class="rbac-assignment-filters">
<select id="rbac-assignment-filter-type" class="form-select rbac-form-select" onchange="changeRbacAssignmentFilter(this.value)" aria-label="筛选资源类型">
<option value="all" data-i18n="rbac.allResourceTypes">全部类型</option>
<option value="conversation" data-i18n="rbac.resourceTypes.conversation">对话</option>
<option value="project" data-i18n="rbac.resourceTypes.project">项目</option>
<option value="batch_task" data-i18n="rbac.resourceTypes.batch_task">批量任务</option>
<option value="vulnerability" data-i18n="rbac.resourceTypes.vulnerability">漏洞</option>
<option value="webshell" data-i18n="rbac.resourceTypes.webshell">WebShell 连接</option>
<option value="c2_listener" data-i18n="rbac.resourceTypes.c2_listener">C2 监听器</option>
</select>
<input id="rbac-assignment-search" class="form-input" type="search" placeholder="搜索已授权资源名称或 ID" data-i18n="rbac.searchAssignments" data-i18n-attr="placeholder" oninput="filterRbacAssignments(this.value)">
</div>
</div>
<div id="rbac-assignments-list" class="rbac-assignments-list"></div>
<div id="rbac-assignment-pagination" class="rbac-pagination">
<button type="button" class="btn-secondary btn-small" data-rbac-page-previous onclick="changeRbacAssignmentPage(-1)" data-i18n="rbac.pagination.previous">上一页</button>
<span data-rbac-page-info></span>
<button type="button" class="btn-secondary btn-small" data-rbac-page-next onclick="changeRbacAssignmentPage(1)" data-i18n="rbac.pagination.next">下一页</button>
</div>
</section>
</div>
</div>
<div id="rbac-tab-audit" class="rbac-tab-panel" hidden>
<div class="rbac-section-toolbar">
<div>
<strong data-i18n="rbac.auditTitle">权限变更记录</strong>
<p class="rbac-muted" data-i18n="rbac.auditHint">展示该成员相关的平台权限操作审计。</p>
</div>
<button type="button" class="btn-secondary btn-small" onclick="loadRbacUserAuditLogs()" data-i18n="common.refresh">刷新</button>
</div>
<div id="rbac-audit-list" class="rbac-audit-list">
<div class="rbac-empty"><strong data-i18n="rbac.empty.selectUserFirst">请先选择用户</strong></div>
</div>
</div>
</section>
</div>
<div id="rbac-view-roles" class="rbac-workspace-view rbac-role-management" hidden>
<div class="rbac-role-management-head">
<div>
<h3 data-i18n="rbac.roleCatalog">角色目录</h3>
<p class="rbac-muted" data-i18n="rbac.roleCatalogHint">先定义角色的操作权限与资源范围,再分配给成员。</p>
</div>
<div class="rbac-role-management-actions">
<div class="rbac-toolbar-search">
<label for="rbac-role-search" data-i18n="rbac.filterRoles">筛选角色</label>
<input id="rbac-role-search" class="form-input" type="search" placeholder="搜索角色或权限" data-i18n="rbac.searchRoles" data-i18n-attr="placeholder" oninput="setRbacRoleSearch(this.value)">
</div>
<button class="btn-primary btn-small" type="button" onclick="openRbacRoleModal()" data-i18n="rbac.createRole">新建角色</button>
</div>
</div>
<div id="rbac-role-catalog" class="rbac-role-catalog"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -5231,6 +5476,93 @@
</div>
</div>
<div id="rbac-user-modal" class="modal">
<div class="modal-content" style="max-width: 640px;">
<div class="modal-header">
<h2 id="rbac-user-modal-title" data-i18n="rbac.userModalTitle">平台用户</h2>
<span class="modal-close" onclick="closeRbacUserModal()">&times;</span>
</div>
<div class="modal-body">
<input type="hidden" id="rbac-user-id">
<div class="form-group">
<label for="rbac-username" data-i18n="rbac.username">用户名</label>
<input type="text" id="rbac-username" class="form-input" autocomplete="off">
</div>
<div class="form-group">
<label for="rbac-display-name" data-i18n="rbac.displayName">显示名称</label>
<input type="text" id="rbac-display-name" class="form-input" autocomplete="off">
</div>
<div class="form-group">
<label for="rbac-password" data-i18n="rbac.password">密码</label>
<input type="password" id="rbac-password" class="form-input" autocomplete="new-password" placeholder="编辑时留空表示不修改" data-i18n="rbac.passwordPlaceholder" data-i18n-attr="placeholder">
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="rbac-user-enabled" class="modern-checkbox" checked>
<span class="checkbox-custom"></span>
<span class="checkbox-text" data-i18n="rbac.enabled">启用账号</span>
</label>
</div>
<div class="form-group" id="rbac-user-roles-create-section">
<label data-i18n="rbac.roles">角色</label>
<div id="rbac-user-role-checkboxes" class="rbac-checkbox-grid"></div>
</div>
<div class="form-group" id="rbac-user-roles-edit-section" hidden>
<label data-i18n="rbac.roles">角色</label>
<div id="rbac-user-roles-readonly" class="rbac-user-roles-readonly"></div>
<p class="rbac-field-hint" data-i18n="rbac.editUserRolesHint">角色请在成员详情页的「角色权限」标签中统一管理。</p>
<button type="button" class="btn-secondary btn-small" onclick="openRbacRolesFromUserModal()" data-i18n="rbac.assignRolesNow">去分配角色</button>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" type="button" onclick="closeRbacUserModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" type="button" onclick="saveRbacUser()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
<div id="rbac-role-modal" class="modal">
<div class="modal-content" style="max-width: 840px;">
<div class="modal-header">
<h2 id="rbac-role-modal-title" data-i18n="rbac.roleModalTitle">平台角色</h2>
<span class="modal-close" onclick="closeRbacRoleModal()">&times;</span>
</div>
<div class="modal-body">
<input type="hidden" id="rbac-role-id">
<div class="form-group">
<label for="rbac-role-name" data-i18n="rbac.roleName">角色名称</label>
<input type="text" id="rbac-role-name" class="form-input" autocomplete="off">
</div>
<div class="form-group">
<label for="rbac-role-description" data-i18n="rbac.description">描述</label>
<input type="text" id="rbac-role-description" class="form-input" autocomplete="off">
</div>
<div class="form-group">
<label for="rbac-role-scope" data-i18n="rbac.scope">资源范围</label>
<select id="rbac-role-scope" class="form-select rbac-form-select">
<option value="all" data-i18n="rbac.scopes.all.option">全部资源 — 可访问平台内所有数据</option>
<option value="assigned" data-i18n="rbac.scopes.assigned.option">指定资源 — 仅访问管理员授权的数据</option>
<option value="own" data-i18n="rbac.scopes.own.option">本人创建 — 仅访问自己创建的数据</option>
</select>
<p id="rbac-role-scope-hint" class="rbac-field-hint" data-i18n="rbac.scopeHint">资源范围与操作权限共同决定最终访问能力。</p>
</div>
<div class="form-group">
<label data-i18n="rbac.permissions">权限</label>
<div class="rbac-permission-tools">
<input id="rbac-permission-search" class="form-input" type="search" placeholder="搜索权限,例如 project:read" data-i18n="rbac.searchPermissions" data-i18n-attr="placeholder" oninput="filterRbacPermissions(this.value)">
<button class="btn-secondary btn-small" type="button" onclick="selectVisibleRbacPermissions(true)" data-i18n="rbac.selectVisible">选择可见</button>
<button class="btn-secondary btn-small" type="button" onclick="selectVisibleRbacPermissions(false)" data-i18n="rbac.clearVisible">清空可见</button>
</div>
<div id="rbac-permission-checkboxes" class="rbac-permission-groups"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" type="button" onclick="closeRbacRoleModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" id="rbac-role-save-btn" type="button" onclick="saveRbacRole()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
<!-- 项目管理弹窗(挂 body 下,避免被 .page overflow 裁剪) -->
<div id="project-modal" class="modal-overlay projects-modal-overlay" style="display:none;" role="dialog" aria-modal="true" aria-labelledby="project-modal-title" onclick="if(event.target===this)closeProjectModal()">
<div class="projects-modal-dialog" onclick="event.stopPropagation()">
@@ -5396,6 +5728,7 @@
<script src="/static/js/tasks.js"></script>
<script src="/static/js/workflows.js"></script>
<script src="/static/js/roles.js"></script>
<script src="/static/js/rbac.js"></script>
<script src="/static/js/c2.js"></script>
</body>
</html>