mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-01 12:01:46 +02:00
Add files via upload
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
"chat": "Chat",
|
||||
"infoCollect": "Recon",
|
||||
"tasks": "Tasks",
|
||||
"projects": "Projects",
|
||||
"vulnerabilities": "Vulnerabilities",
|
||||
"webshell": "WebShell Management",
|
||||
"chatFiles": "File Management",
|
||||
@@ -222,6 +223,179 @@
|
||||
"noVulnDesc": "This list shows recent records; new results appear here when detection completes in chat",
|
||||
"startScanBtn": "Go to chat to scan"
|
||||
},
|
||||
"projects": {
|
||||
"title": "Projects",
|
||||
"showArchived": "Show archived",
|
||||
"newProjectCta": "+ New project",
|
||||
"projectList": "Project list",
|
||||
"searchProjectsPlaceholder": "Search projects…",
|
||||
"selectOrCreateTitle": "Select or create a project",
|
||||
"selectOrCreateHint": "Projects share a cross-chat fact board; target, environment, auth and other facts are auto-injected in bound conversations.",
|
||||
"createFirstProject": "Create first project",
|
||||
"defaultProjectName": "Project",
|
||||
"statusActive": "Active",
|
||||
"statusArchived": "Archived",
|
||||
"vulnerabilityManagement": "Vulnerability management",
|
||||
"addFactCta": "+ Add fact",
|
||||
"tabFacts": "Fact board",
|
||||
"tabConversations": "Bound conversations",
|
||||
"tabVulns": "Related vulnerabilities",
|
||||
"tabSettings": "Settings",
|
||||
"factToolbarHint": "Index includes key and summary only (must include what + where + how to verify); put attack chain / POC in body, and reproduce via get_project_fact.",
|
||||
"searchFactsSr": "Search facts",
|
||||
"searchFactsPlaceholder": "Search key, summary, body…",
|
||||
"category": "Category",
|
||||
"all": "All",
|
||||
"confidence": "Confidence",
|
||||
"confidenceConfirmed": "Confirmed",
|
||||
"confidenceTentative": "Tentative",
|
||||
"confidenceDeprecated": "Deprecated",
|
||||
"displayOptions": "Display options",
|
||||
"sparseOnly": "Sparse only",
|
||||
"hideDeprecated": "Hide deprecated",
|
||||
"summary": "Summary",
|
||||
"updated": "Updated",
|
||||
"boundConversationsHint": "Conversations bound to this project; click to open",
|
||||
"titleLabel": "Title",
|
||||
"projectVulnSummaryHint": "Vulnerability summary under this project",
|
||||
"viewInVulnerabilityManagement": "View in vulnerability management",
|
||||
"severity": "Severity",
|
||||
"status": "Status",
|
||||
"modalNewTitle": "New project",
|
||||
"modalNewSubtitle": "After creation, bind conversations to share fact board across chats",
|
||||
"projectName": "Project name",
|
||||
"projectNamePlaceholder": "e.g. Client A Web pentest",
|
||||
"projectDescription": "Project description",
|
||||
"projectDescriptionPlaceholder": "Scope, authorization boundary, notes…",
|
||||
"createProject": "Create project",
|
||||
"newProject": "New project",
|
||||
"chatSelectorButton": "Share fact board across chats after binding a project",
|
||||
"selectProject": "Select project",
|
||||
"noProject": "No project",
|
||||
"factBodyEnvTitle": "Environment fact",
|
||||
"factBodyHasDetail": "Has details",
|
||||
"factBodySparseTitle": "Missing attack-chain/POC structure",
|
||||
"factBodySparse": "Incomplete",
|
||||
"factBodyReproducibleTitle": "Contains reproducible structure",
|
||||
"factBodyReproducible": "Reproducible",
|
||||
"factHintAttackSparse": "Attack-chain fact: fill complete body (steps, HTTP/command, response evidence); avoid conclusion-only notes. You can insert the attack-chain template.",
|
||||
"factHintAttackReady": "Attack-chain fact: body is used for audit reproduction, keep original request/response and step-by-step flow.",
|
||||
"factHintEnv": "Environment fact: body should include evidence source; for findings/exploitation use finding|chain|exploit|poc category.",
|
||||
"confirmOverwriteBodyTemplate": "Overwrite current body content with template?",
|
||||
"loadProjectsFailed": "Failed to load projects",
|
||||
"restoreTitle": "Restore as tentative and re-index into board",
|
||||
"restore": "Restore",
|
||||
"deprecateTitle": "Mark as deprecated",
|
||||
"deprecate": "Deprecate",
|
||||
"editTitle": "Edit fields",
|
||||
"viewBodyTitle": "View full body",
|
||||
"details": "Details",
|
||||
"deleteForeverTitle": "Delete permanently",
|
||||
"noProjects": "No projects",
|
||||
"noMatchingProjects": "No matching projects",
|
||||
"pinned": "Pinned",
|
||||
"archived": "Archived",
|
||||
"statsFacts": "{{count}} facts",
|
||||
"statsVulns": "{{count}} vulnerabilities",
|
||||
"statsConversations": "{{count}} conversations",
|
||||
"statsSparse": "{{count}} incomplete",
|
||||
"projectNotFound": "Project not found",
|
||||
"updatedPrefix": "Updated {{time}}",
|
||||
"noMatchingFacts": "No matching facts, try adjusting filters",
|
||||
"noFacts": "No facts yet. Click Add fact or let Agent write facts automatically",
|
||||
"relatedVulnIdTitle": "Related vulnerability ID",
|
||||
"noBoundConversations": "No bound conversations yet; select this project in chat to bind",
|
||||
"untitledConversation": "Untitled conversation",
|
||||
"open": "Open",
|
||||
"unbindProjectTitle": "Unbind project",
|
||||
"unbind": "Unbind",
|
||||
"confirmUnbindConversation": "Unbind this conversation from current project?",
|
||||
"unbindFailed": "Unbind failed",
|
||||
"factMetaCategory": "Category: {{value}}",
|
||||
"factMetaConfidence": "Confidence: {{value}}",
|
||||
"factMetaUpdated": "Updated: {{time}}",
|
||||
"factMetaRelatedVuln": "Related vulnerability: {{value}}",
|
||||
"factMetaSourceConversation": "Source conversation: {{value}}",
|
||||
"factMetaHasPrevious": "Has previous version",
|
||||
"emptyBody": "(empty body)",
|
||||
"factSparseWarn": "This fact belongs to attack-chain/exploit category, but body lacks reproducible structure (steps, HTTP/command, request/response, etc.). Edit and complete it for audit reproduction.",
|
||||
"factPreviousMeta": "Archived at {{time}} · Summary: {{summary}} · Confidence: {{confidence}}",
|
||||
"loadVulnerabilityListFailed": "Failed to load vulnerability list",
|
||||
"noVulnerabilitiesInProject": "No vulnerabilities in this project yet. Create one first or let Agent record it.",
|
||||
"promptLinkFactToVuln": "Enter index to link fact \"{{factKey}}\":\n\n{{lines}}",
|
||||
"invalidIndex": "Invalid index",
|
||||
"linkFailed": "Link failed",
|
||||
"linkSuccess": "Linked vulnerability",
|
||||
"promptConversationIdForVulnCreate": "Conversation ID is required to create vulnerability (can be source conversation):",
|
||||
"cancelledNoConversationId": "Cancelled: conversation_id not provided",
|
||||
"createVulnerabilityFailed": "Failed to create vulnerability",
|
||||
"createVulnerabilityAndLinkSuccess": "Created vulnerability and linked: {{value}}",
|
||||
"confirmDeprecateFact": "Mark fact {{factKey}} as deprecated?",
|
||||
"operationFailed": "Operation failed",
|
||||
"confirmRestoreFact": "Restore fact {{factKey}}? It will re-enter board index with tentative status.",
|
||||
"noVulnerabilityRecords": "No vulnerability records in this project",
|
||||
"viewRelatedFactsTitle": "View related facts",
|
||||
"facts": "Facts",
|
||||
"loadRelatedFactsFailed": "Failed to load related facts",
|
||||
"noFactsForVulnerability": "This vulnerability has no related facts yet. Link vulnerability or generate vulnerability draft from fact detail.",
|
||||
"promptChooseFactByIndex": "This vulnerability is linked to {{count}} facts. Enter index to view:\n{{lines}}",
|
||||
"enterProjectName": "Please enter project name",
|
||||
"saveFailed": "Save failed",
|
||||
"invalidJson": "Invalid JSON format",
|
||||
"scopeNoteAuthorizedWebOnly": "Authorized for Web application layer testing only",
|
||||
"invalidScopeJson": "Invalid scope JSON, please fix it first or click Format",
|
||||
"saved": "Saved",
|
||||
"confirmArchiveProject": "After archiving, this project is hidden from active list by default. Continue?",
|
||||
"confirmRestoreProjectActive": "Restore to active?",
|
||||
"confirmDeleteProject": "Delete this project? Facts will be deleted and conversations unbound.",
|
||||
"deleteFailed": "Delete failed",
|
||||
"addFact": "Add fact",
|
||||
"saveFact": "Save fact",
|
||||
"editFact": "Edit fact",
|
||||
"saveChanges": "Save changes",
|
||||
"customCategoryOption": "{{value}} (custom)",
|
||||
"selectProjectFirst": "Please select a project first",
|
||||
"loadFactFailed": "Failed to load fact",
|
||||
"factKeySummaryRequired": "fact_key and summary are required",
|
||||
"confirmSaveSparseFact": "This fact is attack-chain/exploit related, but body does not contain reproducible structure (steps, HTTP/command, request/response).\nSave anyway? It's recommended to insert attack-chain template and fill POC first.",
|
||||
"confirmDeleteFact": "Delete this fact?",
|
||||
"notUpdatedYet": "Not updated yet",
|
||||
"clearStaleProjectBindingFailed": "Failed to clear stale project binding",
|
||||
"noProjectDescription": "No project binding",
|
||||
"noProjectsClickCreate": "No projects yet, click New project below",
|
||||
"sharedFactBoard": "Shared fact board",
|
||||
"loadFailedRetry": "Load failed, please retry later",
|
||||
"projectBound": "Project bound",
|
||||
"projectUnbound": "Project unbound",
|
||||
"updateProjectBindingFailed": "Failed to update project binding",
|
||||
"basicInfoTitle": "Basic information",
|
||||
"basicInfoHint": "Name and description are shown in project details",
|
||||
"settingsIntroTitle": "Project settings",
|
||||
"settingsIntroHint": "Configure project metadata and Agent authorization boundary; takes effect immediately for bound conversations after saving.",
|
||||
"pinProject": "Pin project (show first in list)",
|
||||
"editDescriptionPlaceholder": "Targets, authorization scope, contacts, notes…",
|
||||
"scopeTitle": "Test scope",
|
||||
"scopeHint": "JSON format for Agent authorization boundary and target assets",
|
||||
"formatJson": "Format",
|
||||
"example": "Example",
|
||||
"scopeJsonLabel": "Scope JSON",
|
||||
"scopeFootnote": "Supports targets, exclude, notes and more. Empty means no scope limit.",
|
||||
"dangerZoneTitle": "Danger zone",
|
||||
"dangerZoneHint": "Archived projects are hidden unless 'Show archived' is enabled; deletion removes all facts permanently.",
|
||||
"archiveRestore": "Archive / Restore",
|
||||
"deleteProject": "Delete project",
|
||||
"saveChangesHint": "Click save to sync changes to server",
|
||||
"saveSettings": "Save changes",
|
||||
"factModalSubtitle": "Summary is indexed on board; body stores attack chain and POC for audit reproduction (separate from vulnerability records).",
|
||||
"relatedVulnIdLabel": "Related vulnerability ID",
|
||||
"optional": "Optional",
|
||||
"factDetails": "Fact details",
|
||||
"previousVersion": "Previous version",
|
||||
"currentVersion": "Current version",
|
||||
"linkVulnerability": "Link vulnerability",
|
||||
"createVulnerabilityDraft": "Create vulnerability draft",
|
||||
"generatedFromFact": "Generated from project fact {{factKey}}"
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "New chat",
|
||||
"toggleConversationPanel": "Collapse/expand conversation list",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"chat": "对话",
|
||||
"infoCollect": "信息收集",
|
||||
"tasks": "任务管理",
|
||||
"projects": "项目管理",
|
||||
"vulnerabilities": "漏洞管理",
|
||||
"webshell": "WebShell管理",
|
||||
"chatFiles": "文件管理",
|
||||
@@ -211,6 +212,179 @@
|
||||
"noVulnDesc": "此处展示近期漏洞记录;在对话中完成检测后,新结果会出现在这里",
|
||||
"startScanBtn": "前往对话发起扫描"
|
||||
},
|
||||
"projects": {
|
||||
"title": "项目管理",
|
||||
"showArchived": "显示已归档",
|
||||
"newProjectCta": "+ 新建项目",
|
||||
"projectList": "项目列表",
|
||||
"searchProjectsPlaceholder": "搜索项目…",
|
||||
"selectOrCreateTitle": "选择或创建项目",
|
||||
"selectOrCreateHint": "项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。",
|
||||
"createFirstProject": "创建第一个项目",
|
||||
"defaultProjectName": "项目",
|
||||
"statusActive": "进行中",
|
||||
"statusArchived": "已归档",
|
||||
"vulnerabilityManagement": "漏洞管理",
|
||||
"addFactCta": "+ 添加事实",
|
||||
"tabFacts": "事实黑板",
|
||||
"tabConversations": "关联对话",
|
||||
"tabVulns": "关联漏洞",
|
||||
"tabSettings": "设置",
|
||||
"factToolbarHint": "索引仅含 key 与摘要(须含「什么 + 在哪 + 如何验证」);攻击链 / POC 写在 body,Agent 通过 get_project_fact 复现",
|
||||
"searchFactsSr": "搜索事实",
|
||||
"searchFactsPlaceholder": "搜索 key、摘要、body…",
|
||||
"category": "分类",
|
||||
"all": "全部",
|
||||
"confidence": "置信度",
|
||||
"confidenceConfirmed": "已确认",
|
||||
"confidenceTentative": "待确认",
|
||||
"confidenceDeprecated": "已废弃",
|
||||
"displayOptions": "显示选项",
|
||||
"sparseOnly": "仅待补全",
|
||||
"hideDeprecated": "隐藏废弃",
|
||||
"summary": "摘要",
|
||||
"updated": "更新",
|
||||
"boundConversationsHint": "绑定到本项目的对话;点击可打开会话",
|
||||
"titleLabel": "标题",
|
||||
"projectVulnSummaryHint": "本项目下记录的漏洞汇总",
|
||||
"viewInVulnerabilityManagement": "在漏洞管理中查看",
|
||||
"severity": "严重度",
|
||||
"status": "状态",
|
||||
"modalNewTitle": "新建项目",
|
||||
"modalNewSubtitle": "创建后可绑定对话,跨会话共享事实黑板",
|
||||
"projectName": "项目名称",
|
||||
"projectNamePlaceholder": "例如:某客户 Web 渗透",
|
||||
"projectDescription": "项目描述",
|
||||
"projectDescriptionPlaceholder": "测试范围、授权边界、注意事项…",
|
||||
"createProject": "创建项目",
|
||||
"newProject": "新建项目",
|
||||
"chatSelectorButton": "绑定项目后共享事实黑板(跨对话)",
|
||||
"selectProject": "选择项目",
|
||||
"noProject": "无项目",
|
||||
"factBodyEnvTitle": "环境类事实",
|
||||
"factBodyHasDetail": "有详情",
|
||||
"factBodySparseTitle": "缺少攻击链/POC 结构",
|
||||
"factBodySparse": "待补全",
|
||||
"factBodyReproducibleTitle": "含可复现结构",
|
||||
"factBodyReproducible": "可复现",
|
||||
"factHintAttackSparse": "⚠ 攻击链类事实:请填写完整 body(步骤、HTTP/命令、响应现象),勿仅写结论。可点「插入攻击链模板」。",
|
||||
"factHintAttackReady": "攻击链类:body 将用于审计复现,请保留原始请求/响应与逐步步骤。",
|
||||
"factHintEnv": "环境认知类:body 建议记录来源证据;发现/利用请改用 finding|chain|exploit|poc 分类。",
|
||||
"confirmOverwriteBodyTemplate": "将覆盖当前 body 内容为模板,是否继续?",
|
||||
"loadProjectsFailed": "加载项目失败",
|
||||
"restoreTitle": "恢复为待确认并重新进入黑板索引",
|
||||
"restore": "恢复",
|
||||
"deprecateTitle": "标记为已废弃",
|
||||
"deprecate": "废弃",
|
||||
"editTitle": "编辑各字段",
|
||||
"viewBodyTitle": "查看完整 body",
|
||||
"details": "详情",
|
||||
"deleteForeverTitle": "永久删除",
|
||||
"noProjects": "暂无项目",
|
||||
"noMatchingProjects": "无匹配项目",
|
||||
"pinned": "置顶",
|
||||
"archived": "归档",
|
||||
"statsFacts": "{{count}} 条事实",
|
||||
"statsVulns": "{{count}} 个漏洞",
|
||||
"statsConversations": "{{count}} 个对话",
|
||||
"statsSparse": "{{count}} 待补全",
|
||||
"projectNotFound": "项目不存在",
|
||||
"updatedPrefix": "更新于 {{time}}",
|
||||
"noMatchingFacts": "无匹配事实,请调整筛选条件",
|
||||
"noFacts": "暂无事实,点击「添加事实」或由 Agent 自动写入",
|
||||
"relatedVulnIdTitle": "关联漏洞 ID",
|
||||
"noBoundConversations": "暂无绑定对话;在对话页选择本项目即可关联",
|
||||
"untitledConversation": "未命名对话",
|
||||
"open": "打开",
|
||||
"unbindProjectTitle": "解除项目绑定",
|
||||
"unbind": "解绑",
|
||||
"confirmUnbindConversation": "解除该对话与当前项目的绑定?",
|
||||
"unbindFailed": "解绑失败",
|
||||
"factMetaCategory": "分类: {{value}}",
|
||||
"factMetaConfidence": "置信度: {{value}}",
|
||||
"factMetaUpdated": "更新: {{time}}",
|
||||
"factMetaRelatedVuln": "关联漏洞: {{value}}",
|
||||
"factMetaSourceConversation": "来源对话: {{value}}",
|
||||
"factMetaHasPrevious": "含上一版本",
|
||||
"emptyBody": "(无 body)",
|
||||
"factSparseWarn": "⚠ 该事实属于攻击链/利用类,但 body 缺少可复现结构(攻击链步骤、HTTP/命令、请求响应等)。建议编辑后补全以便审计复现。",
|
||||
"factPreviousMeta": "归档于 {{time}} · 摘要: {{summary}} · 置信度: {{confidence}}",
|
||||
"loadVulnerabilityListFailed": "加载漏洞列表失败",
|
||||
"noVulnerabilitiesInProject": "本项目暂无漏洞,请先创建或让 Agent 记录漏洞",
|
||||
"promptLinkFactToVuln": "输入序号以关联事实「{{factKey}}」:\n\n{{lines}}",
|
||||
"invalidIndex": "序号无效",
|
||||
"linkFailed": "关联失败",
|
||||
"linkSuccess": "已关联漏洞",
|
||||
"promptConversationIdForVulnCreate": "创建漏洞需要对话 ID(可与来源会话一致):",
|
||||
"cancelledNoConversationId": "已取消:未提供 conversation_id",
|
||||
"createVulnerabilityFailed": "创建漏洞失败",
|
||||
"createVulnerabilityAndLinkSuccess": "已创建漏洞并关联:{{value}}",
|
||||
"confirmDeprecateFact": "将事实 {{factKey}} 标记为已废弃?",
|
||||
"operationFailed": "操作失败",
|
||||
"confirmRestoreFact": "恢复事实 {{factKey}}?将重新进入黑板索引(状态:待确认)。",
|
||||
"noVulnerabilityRecords": "本项目暂无漏洞记录",
|
||||
"viewRelatedFactsTitle": "查看关联事实",
|
||||
"facts": "事实",
|
||||
"loadRelatedFactsFailed": "加载关联事实失败",
|
||||
"noFactsForVulnerability": "该漏洞暂无关联事实,可在事实详情中「关联漏洞」或「生成漏洞草稿」建立链接",
|
||||
"promptChooseFactByIndex": "该漏洞关联 {{count}} 条事实,输入序号查看:\n{{lines}}",
|
||||
"enterProjectName": "请输入项目名称",
|
||||
"saveFailed": "保存失败",
|
||||
"invalidJson": "JSON 格式无效",
|
||||
"scopeNoteAuthorizedWebOnly": "仅授权 Web 应用层测试",
|
||||
"invalidScopeJson": "测试范围 JSON 无效,请先修正或点击「格式化」",
|
||||
"saved": "已保存",
|
||||
"confirmArchiveProject": "归档后默认不再出现在活跃列表,是否继续?",
|
||||
"confirmRestoreProjectActive": "恢复为 active?",
|
||||
"confirmDeleteProject": "确定删除该项目?事实将一并删除,对话将解除绑定。",
|
||||
"deleteFailed": "删除失败",
|
||||
"addFact": "添加事实",
|
||||
"saveFact": "保存事实",
|
||||
"editFact": "编辑事实",
|
||||
"saveChanges": "保存修改",
|
||||
"customCategoryOption": "{{value}}(自定义)",
|
||||
"selectProjectFirst": "请先选择项目",
|
||||
"loadFactFailed": "加载事实失败",
|
||||
"factKeySummaryRequired": "fact_key 与 summary 必填",
|
||||
"confirmSaveSparseFact": "该事实属于攻击链/利用类,但 body 尚未包含可复现结构(步骤、HTTP/命令、请求响应等)。\n仍要保存吗?建议先插入攻击链模板并填写 POC。",
|
||||
"confirmDeleteFact": "删除该事实?",
|
||||
"notUpdatedYet": "尚未更新",
|
||||
"clearStaleProjectBindingFailed": "清除失效的项目绑定失败",
|
||||
"noProjectDescription": "不绑定项目黑板",
|
||||
"noProjectsClickCreate": "暂无项目,点击下方「新建项目」",
|
||||
"sharedFactBoard": "共享事实黑板",
|
||||
"loadFailedRetry": "加载失败,请稍后重试",
|
||||
"projectBound": "已绑定项目",
|
||||
"projectUnbound": "已解除项目绑定",
|
||||
"updateProjectBindingFailed": "更新项目绑定失败",
|
||||
"basicInfoTitle": "基本信息",
|
||||
"basicInfoHint": "名称与描述会显示在项目详情中",
|
||||
"settingsIntroTitle": "项目设置",
|
||||
"settingsIntroHint": "配置项目元数据与 Agent 授权边界,保存后即时生效于绑定对话。",
|
||||
"pinProject": "置顶项目(列表优先显示)",
|
||||
"editDescriptionPlaceholder": "测试目标、授权范围、联系人、注意事项…",
|
||||
"scopeTitle": "测试范围",
|
||||
"scopeHint": "JSON 格式,供 Agent 理解授权边界与目标资产",
|
||||
"formatJson": "格式化",
|
||||
"example": "示例",
|
||||
"scopeJsonLabel": "范围 JSON",
|
||||
"scopeFootnote": "支持 targets、exclude、notes 等字段,留空表示不限制范围。",
|
||||
"dangerZoneTitle": "危险操作",
|
||||
"dangerZoneHint": "归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。",
|
||||
"archiveRestore": "归档 / 恢复",
|
||||
"deleteProject": "删除项目",
|
||||
"saveChangesHint": "修改后请点击保存以同步到服务器",
|
||||
"saveSettings": "保存更改",
|
||||
"factModalSubtitle": "摘要注入黑板索引;body 沉淀攻击链与 POC,供审计复现(与漏洞记录分工)",
|
||||
"relatedVulnIdLabel": "关联漏洞 ID",
|
||||
"optional": "可选",
|
||||
"factDetails": "事实详情",
|
||||
"previousVersion": "上一版本",
|
||||
"currentVersion": "当前版本",
|
||||
"linkVulnerability": "关联漏洞",
|
||||
"createVulnerabilityDraft": "生成漏洞草稿",
|
||||
"generatedFromFact": "由项目事实 {{factKey}} 生成"
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "新对话",
|
||||
"toggleConversationPanel": "折叠/展开对话列表",
|
||||
|
||||
+153
-118
@@ -11,6 +11,17 @@ let _projectsFetchPromise = null;
|
||||
|
||||
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
|
||||
|
||||
function tp(key, opts) {
|
||||
if (typeof window.t === 'function') return window.t(key, opts);
|
||||
return key;
|
||||
}
|
||||
|
||||
function tpFmt(key, fallback, opts) {
|
||||
const text = tp(key, opts);
|
||||
if (!text || text === key) return fallback;
|
||||
return text;
|
||||
}
|
||||
|
||||
/** 与后端 internal/project/fact_template.go 对齐 */
|
||||
const FACT_ATTACK_CHAIN_BODY_TEMPLATE = `## 结论(可验证,一句话)
|
||||
<勿仅写「存在漏洞」;写明类型 + 位置 + 触发条件>
|
||||
@@ -98,12 +109,12 @@ function isSparseFactBody(category, factKey, body) {
|
||||
function formatFactBodyBadge(f) {
|
||||
if (!requiresAttackChainFact(f.category, f.fact_key)) {
|
||||
const hasBody = !!(f.body || '').trim();
|
||||
return `<span class="projects-fact-badge projects-fact-badge--na" title="环境类事实">${hasBody ? '有详情' : '—'}</span>`;
|
||||
return `<span class="projects-fact-badge projects-fact-badge--na" title="${escapeHtml(tp('projects.factBodyEnvTitle'))}">${hasBody ? escapeHtml(tp('projects.factBodyHasDetail')) : '—'}</span>`;
|
||||
}
|
||||
if (isSparseFactBody(f.category, f.fact_key, f.body)) {
|
||||
return '<span class="projects-fact-badge projects-fact-badge--warn" title="缺少攻击链/POC 结构">待补全</span>';
|
||||
return `<span class="projects-fact-badge projects-fact-badge--warn" title="${escapeHtml(tp('projects.factBodySparseTitle'))}">${escapeHtml(tp('projects.factBodySparse'))}</span>`;
|
||||
}
|
||||
return '<span class="projects-fact-badge projects-fact-badge--ok" title="含可复现结构">可复现</span>';
|
||||
return `<span class="projects-fact-badge projects-fact-badge--ok" title="${escapeHtml(tp('projects.factBodyReproducibleTitle'))}">${escapeHtml(tp('projects.factBodyReproducible'))}</span>`;
|
||||
}
|
||||
|
||||
function updateFactFormHints() {
|
||||
@@ -115,11 +126,11 @@ function updateFactFormHints() {
|
||||
if (requiresAttackChainFact(cat, key)) {
|
||||
const sparse = isSparseFactBody(cat, key, body);
|
||||
hint.textContent = sparse
|
||||
? '⚠ 攻击链类事实:请填写完整 body(步骤、HTTP/命令、响应现象),勿仅写结论。可点「插入攻击链模板」。'
|
||||
: '攻击链类:body 将用于审计复现,请保留原始请求/响应与逐步步骤。';
|
||||
? tp('projects.factHintAttackSparse')
|
||||
: tp('projects.factHintAttackReady');
|
||||
hint.classList.toggle('projects-field-hint--warn', sparse);
|
||||
} else {
|
||||
hint.textContent = '环境认知类:body 建议记录来源证据;发现/利用请改用 finding|chain|exploit|poc 分类。';
|
||||
hint.textContent = tp('projects.factHintEnv');
|
||||
hint.classList.remove('projects-field-hint--warn');
|
||||
}
|
||||
}
|
||||
@@ -128,7 +139,7 @@ function insertFactBodyTemplate(kind) {
|
||||
const ta = document.getElementById('fact-modal-body');
|
||||
if (!ta) return;
|
||||
const tpl = kind === 'env' ? FACT_ENV_BODY_TEMPLATE : FACT_ATTACK_CHAIN_BODY_TEMPLATE;
|
||||
if (ta.value.trim() && !confirm('将覆盖当前 body 内容为模板,是否继续?')) return;
|
||||
if (ta.value.trim() && !confirm(tp('projects.confirmOverwriteBodyTemplate'))) return;
|
||||
ta.value = tpl;
|
||||
updateFactFormHints();
|
||||
ta.focus();
|
||||
@@ -160,7 +171,7 @@ async function fetchProjectsList(includeArchived) {
|
||||
const showArchived = includeArchived || document.getElementById('projects-show-archived')?.checked;
|
||||
const url = showArchived ? '/api/projects?limit=200' : '/api/projects?status=active&limit=200';
|
||||
const res = await apiFetch(url);
|
||||
if (!res.ok) throw new Error('加载项目失败');
|
||||
if (!res.ok) throw new Error(tp('projects.loadProjectsFailed'));
|
||||
const data = await res.json();
|
||||
projectsCache = Array.isArray(data) ? data : [];
|
||||
rebuildProjectNameMap(projectsCache);
|
||||
@@ -295,12 +306,12 @@ function formatConfidenceBadge(confidence) {
|
||||
let label = c || '—';
|
||||
if (c === 'confirmed') {
|
||||
cls = 'projects-confidence--confirmed';
|
||||
label = '已确认';
|
||||
label = tp('projects.confidenceConfirmed');
|
||||
} else if (c === 'deprecated') {
|
||||
cls = 'projects-confidence--deprecated';
|
||||
label = '已废弃';
|
||||
label = tp('projects.confidenceDeprecated');
|
||||
} else if (c === 'tentative') {
|
||||
label = '待确认';
|
||||
label = tp('projects.confidenceTentative');
|
||||
}
|
||||
return `<span class="projects-confidence ${cls}">${escapeHtml(label)}</span>`;
|
||||
}
|
||||
@@ -308,13 +319,13 @@ function formatConfidenceBadge(confidence) {
|
||||
function renderProjectFactActions(keyEsc, idEsc, confidence) {
|
||||
const isDeprecated = (confidence || '').toLowerCase() === 'deprecated';
|
||||
const toggleBtn = isDeprecated
|
||||
? `<button type="button" class="projects-action-btn projects-action-btn--restore" data-fact-key="${keyEsc}" onclick="restoreProjectFactByKey(this.dataset.factKey)" title="恢复为待确认并重新进入黑板索引">恢复</button>`
|
||||
: `<button type="button" class="projects-action-btn projects-action-btn--mute" data-fact-key="${keyEsc}" onclick="deprecateProjectFactByKey(this.dataset.factKey)" title="标记为已废弃">废弃</button>`;
|
||||
? `<button type="button" class="projects-action-btn projects-action-btn--restore" data-fact-key="${keyEsc}" onclick="restoreProjectFactByKey(this.dataset.factKey)" title="${escapeHtml(tp('projects.restoreTitle'))}">${escapeHtml(tp('projects.restore'))}</button>`
|
||||
: `<button type="button" class="projects-action-btn projects-action-btn--mute" data-fact-key="${keyEsc}" onclick="deprecateProjectFactByKey(this.dataset.factKey)" title="${escapeHtml(tp('projects.deprecateTitle'))}">${escapeHtml(tp('projects.deprecate'))}</button>`;
|
||||
return `<div class="projects-table-actions">
|
||||
<button type="button" class="projects-action-btn projects-action-btn--edit" data-fact-key="${keyEsc}" onclick="showEditFactModal(this.dataset.factKey)" title="编辑各字段">编辑</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-fact-key="${keyEsc}" onclick="viewProjectFactBody(this.dataset.factKey)" title="查看完整 body">详情</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--edit" data-fact-key="${keyEsc}" onclick="showEditFactModal(this.dataset.factKey)" title="${escapeHtml(tp('projects.editTitle'))}">${escapeHtml(tp('common.edit'))}</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-fact-key="${keyEsc}" onclick="viewProjectFactBody(this.dataset.factKey)" title="${escapeHtml(tp('projects.viewBodyTitle'))}">${escapeHtml(tp('projects.details'))}</button>
|
||||
${toggleBtn}
|
||||
<button type="button" class="projects-action-btn projects-action-btn--danger" data-fact-id="${idEsc}" onclick="deleteProjectFact(this.dataset.factId)" title="永久删除">删除</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--danger" data-fact-id="${idEsc}" onclick="deleteProjectFact(this.dataset.factId)" title="${escapeHtml(tp('projects.deleteForeverTitle'))}">${escapeHtml(tp('common.delete'))}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -342,12 +353,12 @@ function renderProjectsSidebar() {
|
||||
: projectsCache;
|
||||
if (!projectsCache.length) {
|
||||
el.innerHTML =
|
||||
'<div class="projects-empty">暂无项目<br><button type="button" class="btn-primary btn-small projects-empty-btn" onclick="showNewProjectModal()">新建项目</button></div>';
|
||||
`<div class="projects-empty">${escapeHtml(tp('projects.noProjects'))}<br><button type="button" class="btn-primary btn-small projects-empty-btn" onclick="showNewProjectModal()">${escapeHtml(tp('projects.newProject'))}</button></div>`;
|
||||
updateProjectsDetailVisibility();
|
||||
return;
|
||||
}
|
||||
if (!list.length) {
|
||||
el.innerHTML = '<div class="projects-empty">无匹配项目</div>';
|
||||
el.innerHTML = `<div class="projects-empty">${escapeHtml(tp('projects.noMatchingProjects'))}</div>`;
|
||||
updateProjectsDetailVisibility();
|
||||
return;
|
||||
}
|
||||
@@ -355,8 +366,8 @@ function renderProjectsSidebar() {
|
||||
const active = p.id === currentProjectId ? ' is-active' : '';
|
||||
const archived = p.status === 'archived' ? ' is-archived' : '';
|
||||
const badges = [
|
||||
p.pinned ? '<span class="projects-list-item-badge">置顶</span>' : '',
|
||||
p.status === 'archived' ? '<span class="projects-list-item-badge">归档</span>' : '',
|
||||
p.pinned ? `<span class="projects-list-item-badge">${escapeHtml(tp('projects.pinned'))}</span>` : '',
|
||||
p.status === 'archived' ? `<span class="projects-list-item-badge">${escapeHtml(tp('projects.archived'))}</span>` : '',
|
||||
].join('');
|
||||
return `<div class="projects-list-item${active}${archived}" data-id="${escapeHtml(p.id)}" onclick="selectProject('${escapeHtml(p.id)}')">
|
||||
<div class="projects-list-item-body">
|
||||
@@ -372,7 +383,7 @@ function updateProjectStatusPill(status) {
|
||||
const el = document.getElementById('projects-detail-status');
|
||||
if (!el) return;
|
||||
const archived = status === 'archived';
|
||||
el.textContent = archived ? '已归档' : '进行中';
|
||||
el.textContent = archived ? tp('projects.statusArchived') : tp('projects.statusActive');
|
||||
el.className = 'projects-status-pill ' + (archived ? 'projects-status-pill--archived' : 'projects-status-pill--active');
|
||||
}
|
||||
|
||||
@@ -386,13 +397,13 @@ function updateProjectStats(stats) {
|
||||
const vc = s.vuln_count ?? s.vulnCount ?? 0;
|
||||
const cc = s.conversation_count ?? s.conversationCount ?? 0;
|
||||
const sc = s.sparse_fact_count ?? s.sparseFactCount ?? 0;
|
||||
if (f) f.textContent = `${fc} 条事实`;
|
||||
if (v) v.textContent = `${vc} 个漏洞`;
|
||||
if (c) c.textContent = `${cc} 个对话`;
|
||||
if (f) f.textContent = tpFmt('projects.statsFacts', `${fc} facts`, { count: fc });
|
||||
if (v) v.textContent = tpFmt('projects.statsVulns', `${vc} vulnerabilities`, { count: vc });
|
||||
if (c) c.textContent = tpFmt('projects.statsConversations', `${cc} conversations`, { count: cc });
|
||||
if (sparse) {
|
||||
if (sc > 0) {
|
||||
sparse.hidden = false;
|
||||
sparse.textContent = `${sc} 待补全`;
|
||||
sparse.textContent = tpFmt('projects.statsSparse', `${sc} to complete`, { count: sc });
|
||||
} else {
|
||||
sparse.hidden = true;
|
||||
}
|
||||
@@ -413,10 +424,10 @@ async function selectProject(id) {
|
||||
updateProjectsDetailVisibility();
|
||||
try {
|
||||
const res = await apiFetch(`/api/projects/${id}`);
|
||||
if (!res.ok) throw new Error('项目不存在');
|
||||
if (!res.ok) throw new Error(tp('projects.projectNotFound'));
|
||||
const p = await res.json();
|
||||
const titleEl = document.getElementById('projects-detail-title');
|
||||
if (titleEl) titleEl.textContent = p.name || '项目';
|
||||
if (titleEl) titleEl.textContent = p.name || tp('projects.defaultProjectName');
|
||||
document.getElementById('project-edit-name').value = p.name || '';
|
||||
document.getElementById('project-edit-description').value = p.description || '';
|
||||
document.getElementById('project-edit-scope').value = p.scope_json || '';
|
||||
@@ -426,7 +437,7 @@ async function selectProject(id) {
|
||||
if (pinEl) pinEl.checked = !!p.pinned;
|
||||
updateProjectStatusPill(p.status || 'active');
|
||||
const metaEl = document.getElementById('projects-detail-meta');
|
||||
if (metaEl) metaEl.textContent = `更新于 ${formatProjectTime(p.updated_at)}`;
|
||||
if (metaEl) metaEl.textContent = tpFmt('projects.updatedPrefix', `Updated ${formatProjectTime(p.updated_at)}`, { time: formatProjectTime(p.updated_at) });
|
||||
const descEl = document.getElementById('projects-detail-desc');
|
||||
if (descEl) {
|
||||
const desc = (p.description || '').trim();
|
||||
@@ -486,11 +497,11 @@ function debouncedLoadProjectFacts() {
|
||||
async function loadProjectFacts() {
|
||||
const tbody = document.getElementById('project-facts-tbody');
|
||||
if (!tbody || !currentProjectId) return;
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="7">加载中…</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tp('common.loading'))}</td></tr>`;
|
||||
const qs = buildProjectFactsQueryParams().toString();
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?${qs}`);
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="7">加载失败</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tp('common.loadFailed'))}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const facts = await res.json();
|
||||
@@ -501,7 +512,7 @@ async function loadProjectFacts() {
|
||||
document.getElementById('project-facts-filter-confidence')?.value ||
|
||||
document.getElementById('project-facts-filter-sparse')?.checked;
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${
|
||||
hasFilter ? '无匹配事实,请调整筛选条件' : '暂无事实,点击「添加事实」或由 Agent 自动写入'
|
||||
hasFilter ? tp('projects.noMatchingFacts') : tp('projects.noFacts')
|
||||
}</td></tr>`;
|
||||
refreshProjectHeaderStats();
|
||||
return;
|
||||
@@ -510,7 +521,7 @@ async function loadProjectFacts() {
|
||||
const keyEsc = escapeHtml(f.fact_key);
|
||||
const idEsc = escapeHtml(f.id);
|
||||
const vulnLink = f.related_vulnerability_id
|
||||
? `<span class="projects-fact-vuln-link" title="关联漏洞 ID">${escapeHtml(f.related_vulnerability_id.slice(0, 8))}…</span>`
|
||||
? `<span class="projects-fact-vuln-link" title="${escapeHtml(tp('projects.relatedVulnIdTitle'))}">${escapeHtml(f.related_vulnerability_id.slice(0, 8))}…</span>`
|
||||
: '';
|
||||
return `<tr>
|
||||
<td><code>${keyEsc}</code>${vulnLink}</td>
|
||||
@@ -540,32 +551,31 @@ async function refreshProjectHeaderStats() {
|
||||
async function loadProjectConversations() {
|
||||
const tbody = document.getElementById('project-conversations-tbody');
|
||||
if (!tbody || !currentProjectId) return;
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="3">加载中…</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="3">${escapeHtml(tp('common.loading'))}</td></tr>`;
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/conversations?limit=100`);
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="3">加载失败</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="3">${escapeHtml(tp('common.loadFailed'))}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const items = data.conversations || [];
|
||||
if (!items.length) {
|
||||
tbody.innerHTML =
|
||||
'<tr class="is-empty-row"><td colspan="3">暂无绑定对话;在对话页选择本项目即可关联</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="3">${escapeHtml(tp('projects.noBoundConversations'))}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = items
|
||||
.map((conv) => {
|
||||
const id = conv.id;
|
||||
const idEsc = escapeHtml(id);
|
||||
const title = escapeHtml(conv.title || '未命名对话');
|
||||
const title = escapeHtml(conv.title || tp('projects.untitledConversation'));
|
||||
const updated = formatProjectTime(conv.updatedAt || conv.updated_at, conv.createdAt || conv.created_at);
|
||||
return `<tr>
|
||||
<td class="cell-summary" title="${title}">${title}</td>
|
||||
<td>${escapeHtml(updated)}</td>
|
||||
<td class="col-actions">
|
||||
<div class="projects-table-actions">
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-conv-id="${idEsc}" onclick="openProjectConversation(this.dataset.convId)">打开</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--mute" data-conv-id="${idEsc}" onclick="unbindConversationFromProject(this.dataset.convId)" title="解除项目绑定">解绑</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-conv-id="${idEsc}" onclick="openProjectConversation(this.dataset.convId)">${escapeHtml(tp('projects.open'))}</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--mute" data-conv-id="${idEsc}" onclick="unbindConversationFromProject(this.dataset.convId)" title="${escapeHtml(tp('projects.unbindProjectTitle'))}">${escapeHtml(tp('projects.unbind'))}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -586,13 +596,13 @@ function openProjectConversation(conversationId) {
|
||||
}
|
||||
|
||||
async function unbindConversationFromProject(conversationId) {
|
||||
if (!conversationId || !confirm('解除该对话与当前项目的绑定?')) return;
|
||||
if (!conversationId || !confirm(tp('projects.confirmUnbindConversation'))) return;
|
||||
const res = await apiFetch(`/api/conversations/${encodeURIComponent(conversationId)}/project`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectId: '' }),
|
||||
});
|
||||
if (!res.ok) return alert('解绑失败');
|
||||
if (!res.ok) return alert(tp('projects.unbindFailed'));
|
||||
loadProjectConversations();
|
||||
refreshProjectHeaderStats();
|
||||
}
|
||||
@@ -603,27 +613,28 @@ let _projectFactsFilterDebounce = null;
|
||||
|
||||
async function viewProjectFactBody(factKey) {
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`);
|
||||
if (!res.ok) return alert('加载失败');
|
||||
if (!res.ok) return alert(tp('common.loadFailed'));
|
||||
const f = await res.json();
|
||||
_factDetailKey = f.fact_key;
|
||||
_factDetailFact = f;
|
||||
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
|
||||
const metaParts = [
|
||||
`分类: ${f.category}`,
|
||||
`置信度: ${f.confidence}`,
|
||||
`更新: ${formatProjectTime(f.updated_at, f.created_at)}`,
|
||||
tpFmt('projects.factMetaCategory', `Category: ${f.category}`, { value: f.category }),
|
||||
tpFmt('projects.factMetaConfidence', `Confidence: ${f.confidence}`, { value: f.confidence }),
|
||||
tpFmt('projects.factMetaUpdated', `Updated: ${formatProjectTime(f.updated_at, f.created_at)}`, {
|
||||
time: formatProjectTime(f.updated_at, f.created_at),
|
||||
}),
|
||||
];
|
||||
if (f.related_vulnerability_id) metaParts.push(`关联漏洞: ${f.related_vulnerability_id}`);
|
||||
if (f.source_conversation_id) metaParts.push(`来源对话: ${f.source_conversation_id}`);
|
||||
if (f.supersedes_fact_id) metaParts.push('含上一版本');
|
||||
if (f.related_vulnerability_id) metaParts.push(tpFmt('projects.factMetaRelatedVuln', `Related vulnerability: ${f.related_vulnerability_id}`, { value: f.related_vulnerability_id }));
|
||||
if (f.source_conversation_id) metaParts.push(tpFmt('projects.factMetaSourceConversation', `Source conversation: ${f.source_conversation_id}`, { value: f.source_conversation_id }));
|
||||
if (f.supersedes_fact_id) metaParts.push(tp('projects.factMetaHasPrevious'));
|
||||
document.getElementById('fact-detail-meta').textContent = metaParts.join(' · ');
|
||||
document.getElementById('fact-detail-body').textContent = f.body || '(无 body)';
|
||||
document.getElementById('fact-detail-body').textContent = f.body || tp('projects.emptyBody');
|
||||
const warnEl = document.getElementById('fact-detail-sparse-warn');
|
||||
if (warnEl) {
|
||||
if (isSparseFactBody(f.category, f.fact_key, f.body)) {
|
||||
warnEl.hidden = false;
|
||||
warnEl.textContent =
|
||||
'⚠ 该事实属于攻击链/利用类,但 body 缺少可复现结构(攻击链步骤、HTTP/命令、请求响应等)。建议编辑后补全以便审计复现。';
|
||||
warnEl.textContent = tp('projects.factSparseWarn');
|
||||
} else {
|
||||
warnEl.hidden = true;
|
||||
warnEl.textContent = '';
|
||||
@@ -640,9 +651,16 @@ async function viewProjectFactBody(factKey) {
|
||||
if (prevRes.ok) {
|
||||
const prev = await prevRes.json();
|
||||
prevWrap.hidden = false;
|
||||
document.getElementById('fact-detail-prev-meta').textContent =
|
||||
`归档于 ${formatProjectTime(prev.archived_at)} · 摘要: ${prev.summary || '—'} · 置信度: ${prev.confidence || '—'}`;
|
||||
document.getElementById('fact-detail-prev-body').textContent = prev.body || '(无 body)';
|
||||
document.getElementById('fact-detail-prev-meta').textContent = tpFmt(
|
||||
'projects.factPreviousMeta',
|
||||
`Archived at ${formatProjectTime(prev.archived_at)} · Summary: ${prev.summary || '—'} · Confidence: ${prev.confidence || '—'}`,
|
||||
{
|
||||
time: formatProjectTime(prev.archived_at),
|
||||
summary: prev.summary || '—',
|
||||
confidence: prev.confidence || '—',
|
||||
},
|
||||
);
|
||||
document.getElementById('fact-detail-prev-body').textContent = prev.body || tp('projects.emptyBody');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
@@ -672,15 +690,15 @@ async function linkFactToExistingVulnerability() {
|
||||
const f = _factDetailFact;
|
||||
if (!f || !currentProjectId) return;
|
||||
const res = await apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=50`);
|
||||
if (!res.ok) return alert('加载漏洞列表失败');
|
||||
if (!res.ok) return alert(tp('projects.loadVulnerabilityListFailed'));
|
||||
const data = await res.json();
|
||||
const items = data.Vulnerabilities || data.vulnerabilities || data.items || [];
|
||||
if (!items.length) return alert('本项目暂无漏洞,请先创建或让 Agent 记录漏洞');
|
||||
if (!items.length) return alert(tp('projects.noVulnerabilitiesInProject'));
|
||||
const lines = items.map((v, i) => `${i + 1}. [${v.severity}] ${v.title} (${v.id})`);
|
||||
const pick = prompt(`输入序号以关联事实「${f.fact_key}」:\n\n${lines.join('\n')}`);
|
||||
const pick = prompt(tpFmt('projects.promptLinkFactToVuln', `Enter index to link fact "${f.fact_key}":\n\n${lines.join('\n')}`, { factKey: f.fact_key, lines: lines.join('\n') }));
|
||||
if (pick == null || pick === '') return;
|
||||
const idx = parseInt(pick, 10) - 1;
|
||||
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) return alert('序号无效');
|
||||
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) return alert(tp('projects.invalidIndex'));
|
||||
const vulnId = items[idx].id;
|
||||
const upd = await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
|
||||
method: 'PUT',
|
||||
@@ -694,8 +712,8 @@ async function linkFactToExistingVulnerability() {
|
||||
related_vulnerability_id: vulnId,
|
||||
}),
|
||||
});
|
||||
if (!upd.ok) return alert('关联失败');
|
||||
alert('已关联漏洞');
|
||||
if (!upd.ok) return alert(tp('projects.linkFailed'));
|
||||
alert(tp('projects.linkSuccess'));
|
||||
closeFactDetailModal();
|
||||
loadProjectFacts();
|
||||
}
|
||||
@@ -707,15 +725,15 @@ async function createVulnerabilityFromCurrentFact() {
|
||||
(f.source_conversation_id || '').trim() ||
|
||||
(typeof window.currentConversationId === 'string' ? window.currentConversationId.trim() : '');
|
||||
if (!convId) {
|
||||
convId = prompt('创建漏洞需要对话 ID(可与来源会话一致):', '')?.trim() || '';
|
||||
convId = prompt(tp('projects.promptConversationIdForVulnCreate'), '')?.trim() || '';
|
||||
}
|
||||
if (!convId) return alert('已取消:未提供 conversation_id');
|
||||
if (!convId) return alert(tp('projects.cancelledNoConversationId'));
|
||||
const severity = inferSeverityFromFact(f);
|
||||
const body = {
|
||||
conversation_id: convId,
|
||||
project_id: currentProjectId,
|
||||
title: (f.summary || f.fact_key).slice(0, 200),
|
||||
description: `由项目事实 ${f.fact_key} 生成`,
|
||||
description: tpFmt('projects.generatedFromFact', `Generated from project fact ${f.fact_key}`, { factKey: f.fact_key }),
|
||||
severity,
|
||||
status: 'open',
|
||||
type: f.category || 'finding',
|
||||
@@ -731,7 +749,7 @@ async function createVulnerabilityFromCurrentFact() {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
return alert(err.error || '创建漏洞失败');
|
||||
return alert(err.error || tp('projects.createVulnerabilityFailed'));
|
||||
}
|
||||
const vuln = await res.json();
|
||||
await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
|
||||
@@ -746,7 +764,7 @@ async function createVulnerabilityFromCurrentFact() {
|
||||
related_vulnerability_id: vuln.id,
|
||||
}),
|
||||
});
|
||||
alert(`已创建漏洞并关联:${vuln.title || vuln.id}`);
|
||||
alert(tpFmt('projects.createVulnerabilityAndLinkSuccess', `Created and linked vulnerability: ${vuln.title || vuln.id}`, { value: vuln.title || vuln.id }));
|
||||
closeFactDetailModal();
|
||||
loadProjectFacts();
|
||||
if (currentProjectTab === 'vulns') loadProjectVulnerabilities();
|
||||
@@ -761,18 +779,18 @@ function inferSeverityFromFact(f) {
|
||||
}
|
||||
|
||||
async function deprecateProjectFactByKey(factKey) {
|
||||
if (!confirm(`将事实 ${factKey} 标记为已废弃?`)) return;
|
||||
if (!confirm(tpFmt('projects.confirmDeprecateFact', `Deprecate fact ${factKey}?`, { factKey }))) return;
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts/deprecate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fact_key: factKey }),
|
||||
});
|
||||
if (!res.ok) return alert('操作失败');
|
||||
if (!res.ok) return alert(tp('projects.operationFailed'));
|
||||
loadProjectFacts();
|
||||
}
|
||||
|
||||
async function restoreProjectFactByKey(factKey) {
|
||||
if (!confirm(`恢复事实 ${factKey}?将重新进入黑板索引(状态:待确认)。`)) return;
|
||||
if (!confirm(tpFmt('projects.confirmRestoreFact', `Restore fact ${factKey}? It will re-enter the board index with tentative status.`, { factKey }))) return;
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts/restore`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -780,7 +798,7 @@ async function restoreProjectFactByKey(factKey) {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
return alert(err.error || '操作失败');
|
||||
return alert(err.error || tp('projects.operationFailed'));
|
||||
}
|
||||
loadProjectFacts();
|
||||
}
|
||||
@@ -801,16 +819,16 @@ function openVulnerabilitiesForProject(projectId) {
|
||||
async function loadProjectVulnerabilities() {
|
||||
const tbody = document.getElementById('project-vulns-tbody');
|
||||
if (!tbody || !currentProjectId) return;
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="4">加载中…</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('common.loading'))}</td></tr>`;
|
||||
const res = await apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=100`);
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="4">加载失败</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('common.loadFailed'))}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const items = data.Vulnerabilities || data.vulnerabilities || data.items || [];
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="4">本项目暂无漏洞记录</td></tr>';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="4">${escapeHtml(tp('projects.noVulnerabilityRecords'))}</td></tr>`;
|
||||
refreshProjectHeaderStats();
|
||||
return;
|
||||
}
|
||||
@@ -822,8 +840,8 @@ async function loadProjectVulnerabilities() {
|
||||
<td>${escapeHtml(v.status)}</td>
|
||||
<td class="col-actions">
|
||||
<div class="projects-table-actions">
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="openVulnerabilityDetail(this.dataset.vulnId)">查看</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="viewFactsForVulnerability(this.dataset.vulnId)" title="查看关联事实">事实</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="openVulnerabilityDetail(this.dataset.vulnId)">${escapeHtml(tp('common.view'))}</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="viewFactsForVulnerability(this.dataset.vulnId)" title="${escapeHtml(tp('projects.viewRelatedFactsTitle'))}">${escapeHtml(tp('projects.facts'))}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -853,10 +871,10 @@ async function viewFactsForVulnerability(vulnId) {
|
||||
if (hideDepEl) hideDepEl.checked = true;
|
||||
const params = new URLSearchParams({ limit: '50', related_vulnerability_id: vulnId });
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?${params}`);
|
||||
if (!res.ok) return alert('加载关联事实失败');
|
||||
if (!res.ok) return alert(tp('projects.loadRelatedFactsFailed'));
|
||||
const facts = await res.json();
|
||||
if (!facts.length) {
|
||||
alert('该漏洞暂无关联事实,可在事实详情中「关联漏洞」或「生成漏洞草稿」建立链接');
|
||||
alert(tp('projects.noFactsForVulnerability'));
|
||||
loadProjectFacts();
|
||||
return;
|
||||
}
|
||||
@@ -865,7 +883,11 @@ async function viewFactsForVulnerability(vulnId) {
|
||||
return;
|
||||
}
|
||||
const pick = prompt(
|
||||
`该漏洞关联 ${facts.length} 条事实,输入序号查看:\n${facts.map((f, i) => `${i + 1}. ${f.fact_key}`).join('\n')}`,
|
||||
tpFmt(
|
||||
'projects.promptChooseFactByIndex',
|
||||
`This vulnerability is linked to ${facts.length} facts. Enter index to view:\n${facts.map((f, i) => `${i + 1}. ${f.fact_key}`).join('\n')}`,
|
||||
{ count: facts.length, lines: facts.map((f, i) => `${i + 1}. ${f.fact_key}`).join('\n') },
|
||||
),
|
||||
);
|
||||
if (pick == null || pick === '') {
|
||||
loadProjectFacts();
|
||||
@@ -895,11 +917,11 @@ function closeProjectsOverlay(id) {
|
||||
}
|
||||
|
||||
function showNewProjectModal() {
|
||||
document.getElementById('project-modal-title').textContent = '新建项目';
|
||||
document.getElementById('project-modal-title').textContent = tp('projects.modalNewTitle');
|
||||
const sub = document.getElementById('project-modal-subtitle');
|
||||
if (sub) sub.textContent = '创建后可绑定对话,跨会话共享事实黑板';
|
||||
if (sub) sub.textContent = tp('projects.modalNewSubtitle');
|
||||
const submitBtn = document.getElementById('project-modal-submit-btn');
|
||||
if (submitBtn) submitBtn.textContent = '创建项目';
|
||||
if (submitBtn) submitBtn.textContent = tp('projects.createProject');
|
||||
document.getElementById('project-modal-name').value = '';
|
||||
document.getElementById('project-modal-description').value = '';
|
||||
window._projectModalEditId = null;
|
||||
@@ -915,7 +937,7 @@ function showNewProjectModalFromChat() {
|
||||
|
||||
async function saveProjectModal() {
|
||||
const name = document.getElementById('project-modal-name').value.trim();
|
||||
if (!name) return alert('请输入项目名称');
|
||||
if (!name) return alert(tp('projects.enterProjectName'));
|
||||
const body = {
|
||||
name,
|
||||
description: document.getElementById('project-modal-description').value.trim(),
|
||||
@@ -926,7 +948,7 @@ async function saveProjectModal() {
|
||||
: await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || '保存失败');
|
||||
alert(err.error || tp('projects.saveFailed'));
|
||||
return;
|
||||
}
|
||||
const fromChat = !!window._projectModalFromChat;
|
||||
@@ -956,7 +978,7 @@ function formatProjectScopeJson() {
|
||||
try {
|
||||
el.value = JSON.stringify(JSON.parse(raw), null, 2);
|
||||
} catch (e) {
|
||||
alert('JSON 格式无效:' + (e.message || String(e)));
|
||||
alert(tp('projects.invalidJson') + ': ' + (e.message || String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -966,7 +988,7 @@ function insertProjectScopeExample() {
|
||||
const example = {
|
||||
targets: ['https://example.com'],
|
||||
exclude: ['*.cdn.example.com'],
|
||||
notes: '仅授权 Web 应用层测试',
|
||||
notes: tp('projects.scopeNoteAuthorizedWebOnly'),
|
||||
};
|
||||
el.value = JSON.stringify(example, null, 2);
|
||||
el.focus();
|
||||
@@ -979,7 +1001,7 @@ async function saveProjectSettings() {
|
||||
try {
|
||||
JSON.parse(scopeRaw);
|
||||
} catch (e) {
|
||||
alert('测试范围 JSON 无效,请先修正或点击「格式化」:' + (e.message || String(e)));
|
||||
alert(tp('projects.invalidScopeJson') + ': ' + (e.message || String(e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -995,10 +1017,10 @@ async function saveProjectSettings() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) return alert('保存失败');
|
||||
if (!res.ok) return alert(tp('projects.saveFailed'));
|
||||
await loadProjectsList();
|
||||
await selectProject(currentProjectId);
|
||||
alert('已保存');
|
||||
alert(tp('projects.saved'));
|
||||
}
|
||||
|
||||
async function archiveCurrentProject() {
|
||||
@@ -1006,23 +1028,23 @@ async function archiveCurrentProject() {
|
||||
const statusEl = document.getElementById('project-edit-status');
|
||||
const cur = statusEl?.value || 'active';
|
||||
const next = cur === 'archived' ? 'active' : 'archived';
|
||||
if (!confirm(next === 'archived' ? '归档后默认不再出现在活跃列表,是否继续?' : '恢复为 active?')) return;
|
||||
if (!confirm(next === 'archived' ? tp('projects.confirmArchiveProject') : tp('projects.confirmRestoreProjectActive'))) return;
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: next }),
|
||||
});
|
||||
if (!res.ok) return alert('操作失败');
|
||||
if (!res.ok) return alert(tp('projects.operationFailed'));
|
||||
await loadProjectsList();
|
||||
await selectProject(currentProjectId);
|
||||
}
|
||||
|
||||
async function deleteCurrentProject() {
|
||||
if (!currentProjectId || !confirm('确定删除该项目?事实将一并删除,对话将解除绑定。')) return;
|
||||
if (!currentProjectId || !confirm(tp('projects.confirmDeleteProject'))) return;
|
||||
const deletedId = currentProjectId;
|
||||
const deletedIndex = projectsCache.findIndex((p) => p.id === deletedId);
|
||||
const res = await apiFetch(`/api/projects/${deletedId}`, { method: 'DELETE' });
|
||||
if (!res.ok) return alert('删除失败');
|
||||
if (!res.ok) return alert(tp('projects.deleteFailed'));
|
||||
if (getActiveProjectId() === deletedId) setActiveProjectId('');
|
||||
currentProjectId = null;
|
||||
await loadProjectsList();
|
||||
@@ -1038,8 +1060,8 @@ function resetFactModalForm() {
|
||||
window._factModalEditId = null;
|
||||
const keyEl = document.getElementById('fact-modal-key');
|
||||
if (keyEl) keyEl.disabled = false;
|
||||
document.getElementById('fact-modal-title').textContent = '添加事实';
|
||||
document.getElementById('fact-modal-submit-btn').textContent = '保存事实';
|
||||
document.getElementById('fact-modal-title').textContent = tp('projects.addFact');
|
||||
document.getElementById('fact-modal-submit-btn').textContent = tp('projects.saveFact');
|
||||
document.getElementById('fact-modal-key').value = '';
|
||||
document.getElementById('fact-modal-category').value = 'note';
|
||||
document.getElementById('fact-modal-summary').value = '';
|
||||
@@ -1052,8 +1074,8 @@ function resetFactModalForm() {
|
||||
|
||||
function fillFactModalForm(f) {
|
||||
window._factModalEditId = f.id;
|
||||
document.getElementById('fact-modal-title').textContent = '编辑事实';
|
||||
document.getElementById('fact-modal-submit-btn').textContent = '保存修改';
|
||||
document.getElementById('fact-modal-title').textContent = tp('projects.editFact');
|
||||
document.getElementById('fact-modal-submit-btn').textContent = tp('projects.saveChanges');
|
||||
document.getElementById('fact-modal-key').value = f.fact_key || '';
|
||||
const catEl = document.getElementById('fact-modal-category');
|
||||
const cat = (f.category || 'note').trim().toLowerCase();
|
||||
@@ -1063,7 +1085,7 @@ function fillFactModalForm(f) {
|
||||
else {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = f.category;
|
||||
opt.textContent = `${f.category}(自定义)`;
|
||||
opt.textContent = tpFmt('projects.customCategoryOption', `${f.category} (custom)`, { value: f.category });
|
||||
catEl.appendChild(opt);
|
||||
catEl.value = f.category;
|
||||
}
|
||||
@@ -1082,17 +1104,17 @@ function fillFactModalForm(f) {
|
||||
}
|
||||
|
||||
function showAddFactModal() {
|
||||
if (!currentProjectId) return alert('请先选择项目');
|
||||
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
|
||||
resetFactModalForm();
|
||||
openProjectsOverlay('fact-modal');
|
||||
}
|
||||
|
||||
async function showEditFactModal(factKey) {
|
||||
if (!currentProjectId) return alert('请先选择项目');
|
||||
if (!currentProjectId) return alert(tp('projects.selectProjectFirst'));
|
||||
const res = await apiFetch(
|
||||
`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`,
|
||||
);
|
||||
if (!res.ok) return alert('加载事实失败');
|
||||
if (!res.ok) return alert(tp('projects.loadFactFailed'));
|
||||
const f = await res.json();
|
||||
resetFactModalForm();
|
||||
fillFactModalForm(f);
|
||||
@@ -1109,10 +1131,10 @@ async function saveFactModal() {
|
||||
const summary = document.getElementById('fact-modal-summary').value.trim();
|
||||
const category = document.getElementById('fact-modal-category').value.trim() || 'note';
|
||||
const body = document.getElementById('fact-modal-body').value;
|
||||
if (!fact_key || !summary) return alert('fact_key 与 summary 必填');
|
||||
if (!fact_key || !summary) return alert(tp('projects.factKeySummaryRequired'));
|
||||
if (isSparseFactBody(category, fact_key, body)) {
|
||||
const ok = confirm(
|
||||
'该事实属于攻击链/利用类,但 body 尚未包含可复现结构(步骤、HTTP/命令、请求响应等)。\n仍要保存吗?建议先插入攻击链模板并填写 POC。',
|
||||
tp('projects.confirmSaveSparseFact'),
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
@@ -1138,14 +1160,14 @@ async function saveFactModal() {
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
return alert(err.error || '保存失败');
|
||||
return alert(err.error || tp('projects.saveFailed'));
|
||||
}
|
||||
closeFactModal();
|
||||
loadProjectFacts();
|
||||
}
|
||||
|
||||
async function deleteProjectFact(id) {
|
||||
if (!confirm('删除该事实?')) return;
|
||||
if (!confirm(tp('projects.confirmDeleteFact'))) return;
|
||||
await apiFetch(`/api/projects/${currentProjectId}/facts/${id}`, { method: 'DELETE' });
|
||||
loadProjectFacts();
|
||||
}
|
||||
@@ -1188,13 +1210,13 @@ function parseProjectDate(t) {
|
||||
|
||||
function formatProjectTime(t, fallback) {
|
||||
const d = parseProjectDate(t) || (fallback != null ? parseProjectDate(fallback) : null);
|
||||
if (!d) return '尚未更新';
|
||||
if (!d) return tp('projects.notUpdatedYet');
|
||||
const now = Date.now();
|
||||
const diff = now - d.getTime();
|
||||
if (diff < 60000) return '刚刚';
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`;
|
||||
if (diff < 60000) return tp('common.justNow');
|
||||
if (diff < 3600000) return tp('common.minutesAgo', { n: Math.floor(diff / 60000) });
|
||||
if (diff < 86400000) return tp('common.hoursAgo', { n: Math.floor(diff / 3600000) });
|
||||
if (diff < 604800000) return tp('common.daysAgo', { n: Math.floor(diff / 86400000) });
|
||||
return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
@@ -1249,7 +1271,7 @@ async function normalizeStaleChatProjectSelection() {
|
||||
body: JSON.stringify({ projectId: '' }),
|
||||
}
|
||||
);
|
||||
if (!res.ok) console.warn('清除失效的项目绑定失败');
|
||||
if (!res.ok) console.warn(tp('projects.clearStaleProjectBindingFailed'));
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
@@ -1265,7 +1287,7 @@ function updateChatProjectButtonLabel() {
|
||||
const textEl = document.getElementById('chat-project-text');
|
||||
if (!textEl) return;
|
||||
const id = resolveChatProjectSelection();
|
||||
textEl.textContent = id && projectNameById[id] ? projectNameById[id] : '无项目';
|
||||
textEl.textContent = id && projectNameById[id] ? projectNameById[id] : tp('projects.noProject');
|
||||
}
|
||||
|
||||
function renderChatProjectPanelList() {
|
||||
@@ -1273,9 +1295,9 @@ function renderChatProjectPanelList() {
|
||||
if (!list) return;
|
||||
const selected = resolveChatProjectSelection();
|
||||
const activeProjects = projectsCache.filter((p) => p.status !== 'archived');
|
||||
const items = [{ id: '', name: '无项目', description: '不绑定项目黑板' }, ...activeProjects];
|
||||
const items = [{ id: '', name: tp('projects.noProject'), description: tp('projects.noProjectDescription') }, ...activeProjects];
|
||||
if (!items.length) {
|
||||
list.innerHTML = '<div class="chat-project-panel-empty">暂无项目,点击下方「新建项目」</div>';
|
||||
list.innerHTML = `<div class="chat-project-panel-empty">${escapeHtml(tp('projects.noProjectsClickCreate'))}</div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '';
|
||||
@@ -1284,7 +1306,7 @@ function renderChatProjectPanelList() {
|
||||
const isSelected = isNone ? !selected : selected === p.id;
|
||||
const desc = isNone
|
||||
? (p.description || '')
|
||||
: (p.description || '').trim().slice(0, 80) || '共享事实黑板';
|
||||
: (p.description || '').trim().slice(0, 80) || tp('projects.sharedFactBoard');
|
||||
const projectId = p.id || '';
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
@@ -1296,7 +1318,7 @@ function renderChatProjectPanelList() {
|
||||
btn.innerHTML = `
|
||||
<div class="role-selection-item-icon-main">${isNone ? '—' : '📁'}</div>
|
||||
<div class="role-selection-item-content-main">
|
||||
<div class="role-selection-item-name-main">${escapeHtml(p.name || '未命名')}</div>
|
||||
<div class="role-selection-item-name-main">${escapeHtml(p.name || tp('common.untitled'))}</div>
|
||||
<div class="role-selection-item-description-main">${escapeHtml(desc)}</div>
|
||||
</div>
|
||||
${isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : ''}
|
||||
@@ -1308,12 +1330,12 @@ function renderChatProjectPanelList() {
|
||||
async function renderChatProjectPanel() {
|
||||
const list = document.getElementById('chat-project-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div class="chat-project-panel-loading">加载中…</div>';
|
||||
list.innerHTML = `<div class="chat-project-panel-loading">${escapeHtml(tp('common.loading'))}</div>`;
|
||||
try {
|
||||
await ensureProjectsLoaded();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
list.innerHTML = '<div class="chat-project-panel-empty">加载失败,请稍后重试</div>';
|
||||
list.innerHTML = `<div class="chat-project-panel-empty">${escapeHtml(tp('projects.loadFailedRetry'))}</div>`;
|
||||
return;
|
||||
}
|
||||
renderChatProjectPanelList();
|
||||
@@ -1373,11 +1395,11 @@ async function applyChatProjectSelection(projectId) {
|
||||
}
|
||||
window._loadedConversationProjectId = projectId;
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(projectId ? '已绑定项目' : '已解除项目绑定', 'success');
|
||||
showNotification(projectId ? tp('projects.projectBound') : tp('projects.projectUnbound'), 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('更新项目绑定失败: ' + (e.message || e));
|
||||
alert(tp('projects.updateProjectBindingFailed') + ': ' + (e.message || e));
|
||||
updateChatProjectButtonLabel();
|
||||
return;
|
||||
}
|
||||
@@ -1411,6 +1433,19 @@ async function onChatProjectChange() {
|
||||
function initChatProjectSelector() {
|
||||
if (window._chatProjectSelectorInited) return;
|
||||
window._chatProjectSelectorInited = true;
|
||||
if (!window._projectsLanguageListenerBound) {
|
||||
window._projectsLanguageListenerBound = true;
|
||||
document.addEventListener('languagechange', () => {
|
||||
renderProjectsSidebar();
|
||||
updateChatProjectButtonLabel();
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
if (panel && panel.style.display === 'flex') renderChatProjectPanelList();
|
||||
if (currentProjectId) {
|
||||
refreshProjectHeaderStats().catch(() => {});
|
||||
switchProjectTab(currentProjectTab || 'facts');
|
||||
}
|
||||
});
|
||||
}
|
||||
refreshChatProjectSelector().catch(() => {});
|
||||
document.addEventListener('click', (e) => {
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
|
||||
+92
-92
@@ -162,13 +162,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="projects">
|
||||
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')">
|
||||
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')" data-i18n="nav.projects" 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">
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
|
||||
<polyline points="2 17 12 22 22 17"></polyline>
|
||||
<polyline points="2 12 12 17 22 12"></polyline>
|
||||
</svg>
|
||||
<span>项目管理</span>
|
||||
<span data-i18n="nav.projects">项目管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="vulnerabilities">
|
||||
@@ -956,16 +956,16 @@
|
||||
<div class="chat-input-primary-row">
|
||||
<div class="chat-input-leading">
|
||||
<div class="role-selector-wrapper project-selector-wrapper">
|
||||
<button type="button" id="chat-project-btn" class="role-selector-btn" onclick="toggleChatProjectPanel()" aria-label="选择项目" aria-haspopup="listbox" aria-expanded="false" title="绑定项目后共享事实黑板(跨对话)">
|
||||
<button type="button" id="chat-project-btn" class="role-selector-btn" onclick="toggleChatProjectPanel()" aria-label="选择项目" aria-haspopup="listbox" aria-expanded="false" title="绑定项目后共享事实黑板(跨对话)" data-i18n="projects.chatSelectorButton" data-i18n-attr="aria-label,title">
|
||||
<span class="role-selector-icon" aria-hidden="true">📁</span>
|
||||
<span id="chat-project-text" class="role-selector-text">无项目</span>
|
||||
<span id="chat-project-text" class="role-selector-text" data-i18n="projects.noProject">无项目</span>
|
||||
<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="chat-project-panel" class="role-selection-panel chat-project-panel" style="display: none;" role="listbox" aria-labelledby="chat-project-panel-title">
|
||||
<div class="role-selection-panel-header">
|
||||
<h3 id="chat-project-panel-title" class="role-selection-panel-title">选择项目</h3>
|
||||
<h3 id="chat-project-panel-title" class="role-selection-panel-title" data-i18n="projects.selectProject">选择项目</h3>
|
||||
<button type="button" class="role-selection-panel-close" onclick="closeChatProjectPanel()" title="关闭" aria-label="关闭">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
@@ -977,7 +977,7 @@
|
||||
<div class="chat-project-panel-footer">
|
||||
<button type="button" class="role-selection-item-main chat-project-panel-create-btn" onclick="showNewProjectModalFromChat()">
|
||||
<span class="chat-project-panel-create-icon" aria-hidden="true">+</span>
|
||||
<span class="chat-project-panel-create-label">新建项目</span>
|
||||
<span class="chat-project-panel-create-label" data-i18n="projects.newProject">新建项目</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1426,36 +1426,36 @@
|
||||
<!-- 项目管理页面 -->
|
||||
<div id="page-projects" class="page projects-page">
|
||||
<div class="page-header">
|
||||
<h2>项目管理</h2>
|
||||
<h2 data-i18n="projects.title">项目管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<label class="projects-show-archived-label"><input type="checkbox" id="projects-show-archived" onchange="loadProjectsList()"> 显示已归档</label>
|
||||
<button class="btn-secondary" type="button" onclick="loadProjectsList()">刷新</button>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()">+ 新建项目</button>
|
||||
<label class="projects-show-archived-label"><input type="checkbox" id="projects-show-archived" onchange="loadProjectsList()"> <span data-i18n="projects.showArchived">显示已归档</span></label>
|
||||
<button class="btn-secondary" type="button" onclick="loadProjectsList()" data-i18n="common.refresh">刷新</button>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()" data-i18n="projects.newProjectCta">+ 新建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content projects-page-layout">
|
||||
<aside class="projects-sidebar-card">
|
||||
<div class="projects-sidebar-head">
|
||||
<span class="projects-sidebar-title">项目列表</span>
|
||||
<span class="projects-sidebar-title" data-i18n="projects.projectList">项目列表</span>
|
||||
<span class="projects-sidebar-count" id="projects-list-count">0</span>
|
||||
</div>
|
||||
<div class="projects-sidebar-search">
|
||||
<input type="search" id="projects-list-search" class="form-input" placeholder="搜索项目…" oninput="filterProjectsList()" autocomplete="off">
|
||||
<input type="search" id="projects-list-search" class="form-input" placeholder="搜索项目…" oninput="filterProjectsList()" autocomplete="off" data-i18n="projects.searchProjectsPlaceholder" data-i18n-attr="placeholder">
|
||||
</div>
|
||||
<div id="projects-list" class="projects-list"></div>
|
||||
</aside>
|
||||
<main class="projects-detail" id="projects-detail-main">
|
||||
<div class="projects-detail-placeholder" id="projects-detail-placeholder">
|
||||
<h3>选择或创建项目</h3>
|
||||
<p>项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。</p>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()">创建第一个项目</button>
|
||||
<h3 data-i18n="projects.selectOrCreateTitle">选择或创建项目</h3>
|
||||
<p data-i18n="projects.selectOrCreateHint">项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。</p>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()" data-i18n="projects.createFirstProject">创建第一个项目</button>
|
||||
</div>
|
||||
<div class="projects-detail-inner" id="projects-detail-inner" hidden>
|
||||
<header class="projects-detail-header">
|
||||
<div class="projects-detail-header-main">
|
||||
<div class="projects-detail-title-row">
|
||||
<h3 id="projects-detail-title" class="projects-detail-title">项目</h3>
|
||||
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active">进行中</span>
|
||||
<h3 id="projects-detail-title" class="projects-detail-title" data-i18n="projects.defaultProjectName">项目</h3>
|
||||
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active" data-i18n="projects.statusActive">进行中</span>
|
||||
</div>
|
||||
<p id="projects-detail-meta" class="projects-detail-meta"></p>
|
||||
<p id="projects-detail-desc" class="projects-detail-desc"></p>
|
||||
@@ -1467,15 +1467,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-detail-header-actions">
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()">漏洞管理</button>
|
||||
<button type="button" class="btn-primary btn-small" onclick="showAddFactModal()">+ 添加事实</button>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.vulnerabilityManagement">漏洞管理</button>
|
||||
<button type="button" class="btn-primary btn-small" onclick="showAddFactModal()" data-i18n="projects.addFactCta">+ 添加事实</button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="projects-tabs" role="tablist">
|
||||
<button type="button" id="project-tab-facts" class="projects-tab is-active" role="tab" onclick="switchProjectTab('facts')">事实黑板</button>
|
||||
<button type="button" id="project-tab-conversations" class="projects-tab" role="tab" onclick="switchProjectTab('conversations')">关联对话</button>
|
||||
<button type="button" id="project-tab-vulns" class="projects-tab" role="tab" onclick="switchProjectTab('vulns')">关联漏洞</button>
|
||||
<button type="button" id="project-tab-settings" class="projects-tab" role="tab" onclick="switchProjectTab('settings')">设置</button>
|
||||
<button type="button" id="project-tab-facts" class="projects-tab is-active" role="tab" onclick="switchProjectTab('facts')" data-i18n="projects.tabFacts">事实黑板</button>
|
||||
<button type="button" id="project-tab-conversations" class="projects-tab" role="tab" onclick="switchProjectTab('conversations')" data-i18n="projects.tabConversations">关联对话</button>
|
||||
<button type="button" id="project-tab-vulns" class="projects-tab" role="tab" onclick="switchProjectTab('vulns')" data-i18n="projects.tabVulns">关联漏洞</button>
|
||||
<button type="button" id="project-tab-settings" class="projects-tab" role="tab" onclick="switchProjectTab('settings')" data-i18n="projects.tabSettings">设置</button>
|
||||
</nav>
|
||||
<div id="project-panel-facts" class="projects-panel" role="tabpanel">
|
||||
<div class="projects-fact-toolbar">
|
||||
@@ -1484,21 +1484,21 @@
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 10v6M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>索引仅含 <strong>key</strong> 与 <strong>摘要</strong>(须含「什么 + 在哪 + 如何验证」);攻击链 / POC 写在 <strong>body</strong>,Agent 通过 <code>get_project_fact</code> 复现</span>
|
||||
<span data-i18n="projects.factToolbarHint">索引仅含 key 与摘要(须含「什么 + 在哪 + 如何验证」);攻击链 / POC 写在 body,Agent 通过 get_project_fact 复现</span>
|
||||
</p>
|
||||
<div class="projects-fact-toolbar-filters" role="search">
|
||||
<label class="projects-fact-filter-field projects-fact-filter-field--search">
|
||||
<span class="sr-only">搜索事实</span>
|
||||
<span class="sr-only" data-i18n="projects.searchFactsSr">搜索事实</span>
|
||||
<svg class="projects-fact-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M20 20L16 16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<input type="search" id="project-facts-search" placeholder="搜索 key、摘要、body…" oninput="debouncedLoadProjectFacts()" autocomplete="off">
|
||||
<input type="search" id="project-facts-search" placeholder="搜索 key、摘要、body…" oninput="debouncedLoadProjectFacts()" autocomplete="off" data-i18n="projects.searchFactsPlaceholder" data-i18n-attr="placeholder">
|
||||
</label>
|
||||
<label class="projects-fact-filter-field">
|
||||
<span class="projects-fact-filter-label">分类</span>
|
||||
<span class="projects-fact-filter-label" data-i18n="projects.category">分类</span>
|
||||
<select id="project-facts-filter-category" onchange="loadProjectFacts()">
|
||||
<option value="">全部</option>
|
||||
<option value="" data-i18n="projects.all">全部</option>
|
||||
<option value="target">target</option>
|
||||
<option value="auth">auth</option>
|
||||
<option value="infra">infra</option>
|
||||
@@ -1511,52 +1511,52 @@
|
||||
</select>
|
||||
</label>
|
||||
<label class="projects-fact-filter-field">
|
||||
<span class="projects-fact-filter-label">置信度</span>
|
||||
<span class="projects-fact-filter-label" data-i18n="projects.confidence">置信度</span>
|
||||
<select id="project-facts-filter-confidence" onchange="loadProjectFacts()">
|
||||
<option value="">全部</option>
|
||||
<option value="confirmed">已确认</option>
|
||||
<option value="tentative">待确认</option>
|
||||
<option value="deprecated">已废弃</option>
|
||||
<option value="" data-i18n="projects.all">全部</option>
|
||||
<option value="confirmed" data-i18n="projects.confidenceConfirmed">已确认</option>
|
||||
<option value="tentative" data-i18n="projects.confidenceTentative">待确认</option>
|
||||
<option value="deprecated" data-i18n="projects.confidenceDeprecated">已废弃</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="projects-fact-filter-toggles" role="group" aria-label="显示选项">
|
||||
<div class="projects-fact-filter-toggles" role="group" aria-label="显示选项" data-i18n="projects.displayOptions" data-i18n-attr="aria-label">
|
||||
<label class="projects-fact-toggle">
|
||||
<input type="checkbox" id="project-facts-filter-sparse" onchange="loadProjectFacts()">
|
||||
<span>仅待补全</span>
|
||||
<span data-i18n="projects.sparseOnly">仅待补全</span>
|
||||
</label>
|
||||
<label class="projects-fact-toggle">
|
||||
<input type="checkbox" id="project-facts-filter-hide-deprecated" checked onchange="loadProjectFacts()">
|
||||
<span>隐藏废弃</span>
|
||||
<span data-i18n="projects.hideDeprecated">隐藏废弃</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-table-wrap">
|
||||
<table class="data-table data-table--projects">
|
||||
<thead><tr><th>Key</th><th>分类</th><th>摘要</th><th>Body</th><th>置信度</th><th>更新</th><th class="col-actions">操作</th></tr></thead>
|
||||
<thead><tr><th>Key</th><th data-i18n="projects.category">分类</th><th data-i18n="projects.summary">摘要</th><th>Body</th><th data-i18n="projects.confidence">置信度</th><th data-i18n="projects.updated">更新</th><th class="col-actions" data-i18n="common.actions">操作</th></tr></thead>
|
||||
<tbody id="project-facts-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="project-panel-conversations" class="projects-panel" role="tabpanel" hidden>
|
||||
<div class="projects-panel-toolbar">
|
||||
<span class="projects-panel-hint">绑定到本项目的对话;点击可打开会话</span>
|
||||
<span class="projects-panel-hint" data-i18n="projects.boundConversationsHint">绑定到本项目的对话;点击可打开会话</span>
|
||||
</div>
|
||||
<div class="projects-table-wrap">
|
||||
<table class="data-table data-table--projects">
|
||||
<thead><tr><th>标题</th><th>更新</th><th class="col-actions">操作</th></tr></thead>
|
||||
<thead><tr><th data-i18n="projects.titleLabel">标题</th><th data-i18n="projects.updated">更新</th><th class="col-actions" data-i18n="common.actions">操作</th></tr></thead>
|
||||
<tbody id="project-conversations-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="project-panel-vulns" class="projects-panel" role="tabpanel" hidden>
|
||||
<div class="projects-panel-toolbar">
|
||||
<span class="projects-panel-hint">本项目下记录的漏洞汇总</span>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()">在漏洞管理中查看</button>
|
||||
<span class="projects-panel-hint" data-i18n="projects.projectVulnSummaryHint">本项目下记录的漏洞汇总</span>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()" data-i18n="projects.viewInVulnerabilityManagement">在漏洞管理中查看</button>
|
||||
</div>
|
||||
<div class="projects-table-wrap">
|
||||
<table class="data-table data-table--projects">
|
||||
<thead><tr><th>标题</th><th>严重度</th><th>状态</th><th class="col-actions">操作</th></tr></thead>
|
||||
<thead><tr><th data-i18n="projects.titleLabel">标题</th><th data-i18n="projects.severity">严重度</th><th data-i18n="projects.status">状态</th><th class="col-actions" data-i18n="common.actions">操作</th></tr></thead>
|
||||
<tbody id="project-vulns-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1565,8 +1565,8 @@
|
||||
<div class="projects-settings-layout">
|
||||
<header class="projects-settings-intro">
|
||||
<div class="projects-settings-intro-text">
|
||||
<h4 class="projects-settings-intro-title">项目设置</h4>
|
||||
<p class="projects-settings-intro-hint">配置项目元数据与 Agent 授权边界,保存后即时生效于绑定对话。</p>
|
||||
<h4 class="projects-settings-intro-title" data-i18n="projects.settingsIntroTitle">项目设置</h4>
|
||||
<p class="projects-settings-intro-hint" data-i18n="projects.settingsIntroHint">配置项目元数据与 Agent 授权边界,保存后即时生效于绑定对话。</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="projects-settings-grid">
|
||||
@@ -1577,35 +1577,35 @@
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="projects-settings-card-title">基本信息</h4>
|
||||
<p class="projects-settings-card-hint">名称与描述会显示在项目详情中</p>
|
||||
<h4 class="projects-settings-card-title" data-i18n="projects.basicInfoTitle">基本信息</h4>
|
||||
<p class="projects-settings-card-hint" data-i18n="projects.basicInfoHint">名称与描述会显示在项目详情中</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-card-body">
|
||||
<div class="projects-form-row projects-form-row--2">
|
||||
<div class="projects-form-field">
|
||||
<label for="project-edit-name">项目名称</label>
|
||||
<input type="text" id="project-edit-name" class="form-input" placeholder="例如:某客户 Web 渗透">
|
||||
<label for="project-edit-name" data-i18n="projects.projectName">项目名称</label>
|
||||
<input type="text" id="project-edit-name" class="form-input" placeholder="例如:某客户 Web 渗透" data-i18n="projects.projectNamePlaceholder" data-i18n-attr="placeholder">
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="project-edit-status">状态</label>
|
||||
<label for="project-edit-status" data-i18n="projects.status">状态</label>
|
||||
<div class="projects-status-select-wrap">
|
||||
<select id="project-edit-status" class="form-input projects-status-select">
|
||||
<option value="active">进行中</option>
|
||||
<option value="archived">已归档</option>
|
||||
<option value="active" data-i18n="projects.statusActive">进行中</option>
|
||||
<option value="archived" data-i18n="projects.statusArchived">已归档</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label class="projects-filter-check projects-pin-toggle">
|
||||
<input type="checkbox" id="project-edit-pinned"> 置顶项目(列表优先显示)
|
||||
<input type="checkbox" id="project-edit-pinned"> <span data-i18n="projects.pinProject">置顶项目(列表优先显示)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="project-edit-description">描述</label>
|
||||
<textarea id="project-edit-description" class="form-input" rows="3" placeholder="测试目标、授权范围、联系人、注意事项…"></textarea>
|
||||
<label for="project-edit-description" data-i18n="projects.projectDescription">描述</label>
|
||||
<textarea id="project-edit-description" class="form-input" rows="3" placeholder="测试目标、授权范围、联系人、注意事项…" data-i18n="projects.editDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1616,21 +1616,21 @@
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="projects-settings-card-title">测试范围</h4>
|
||||
<p class="projects-settings-card-hint">JSON 格式,供 Agent 理解授权边界与目标资产</p>
|
||||
<h4 class="projects-settings-card-title" data-i18n="projects.scopeTitle">测试范围</h4>
|
||||
<p class="projects-settings-card-hint" data-i18n="projects.scopeHint">JSON 格式,供 Agent 理解授权边界与目标资产</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-scope-toolbar">
|
||||
<button type="button" class="btn-ghost btn-small" onclick="formatProjectScopeJson()" title="格式化 JSON">格式化</button>
|
||||
<button type="button" class="btn-ghost btn-small" onclick="insertProjectScopeExample()" title="插入示例">示例</button>
|
||||
<button type="button" class="btn-ghost btn-small" onclick="formatProjectScopeJson()" title="格式化 JSON" data-i18n="projects.formatJson" data-i18n-attr="title">格式化</button>
|
||||
<button type="button" class="btn-ghost btn-small" onclick="insertProjectScopeExample()" title="插入示例" data-i18n="projects.example" data-i18n-attr="title">示例</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-card-body projects-settings-card-body--fill">
|
||||
<div class="projects-scope-editor">
|
||||
<label for="project-edit-scope" class="sr-only">范围 JSON</label>
|
||||
<label for="project-edit-scope" class="sr-only" data-i18n="projects.scopeJsonLabel">范围 JSON</label>
|
||||
<textarea id="project-edit-scope" class="form-input form-input--mono projects-scope-textarea" spellcheck="false" placeholder='{"targets":["https://example.com"],"exclude":["*.cdn.example.com"]}'></textarea>
|
||||
</div>
|
||||
<p class="projects-scope-footnote">支持 <code>targets</code>、<code>exclude</code>、<code>notes</code> 等字段,留空表示不限制范围。</p>
|
||||
<p class="projects-scope-footnote" data-i18n="projects.scopeFootnote">支持 targets、exclude、notes 等字段,留空表示不限制范围。</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -1640,21 +1640,21 @@
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="projects-settings-card-title">危险操作</h4>
|
||||
<p class="projects-settings-card-hint">归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。</p>
|
||||
<h4 class="projects-settings-card-title" data-i18n="projects.dangerZoneTitle">危险操作</h4>
|
||||
<p class="projects-settings-card-hint" data-i18n="projects.dangerZoneHint">归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-danger-actions">
|
||||
<button class="btn-secondary btn-small" type="button" onclick="archiveCurrentProject()">归档 / 恢复</button>
|
||||
<button class="btn-secondary btn-small btn-danger-outline" type="button" onclick="deleteCurrentProject()">删除项目</button>
|
||||
<button class="btn-secondary btn-small" type="button" onclick="archiveCurrentProject()" data-i18n="projects.archiveRestore">归档 / 恢复</button>
|
||||
<button class="btn-secondary btn-small btn-danger-outline" type="button" onclick="deleteCurrentProject()" data-i18n="projects.deleteProject">删除项目</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="projects-settings-footer">
|
||||
<span class="projects-settings-footer-hint">修改后请点击保存以同步到服务器</span>
|
||||
<span class="projects-settings-footer-hint" data-i18n="projects.saveChangesHint">修改后请点击保存以同步到服务器</span>
|
||||
<button class="btn-primary" type="button" onclick="saveProjectSettings()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||
保存更改
|
||||
<span data-i18n="projects.saveSettings">保存更改</span>
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -4021,25 +4021,25 @@
|
||||
<div class="projects-modal-header">
|
||||
<div class="projects-modal-header-text">
|
||||
<div>
|
||||
<h3 id="project-modal-title">新建项目</h3>
|
||||
<p id="project-modal-subtitle" class="projects-modal-subtitle">创建后可绑定对话,跨会话共享事实黑板</p>
|
||||
<h3 id="project-modal-title" data-i18n="projects.modalNewTitle">新建项目</h3>
|
||||
<p id="project-modal-subtitle" class="projects-modal-subtitle" data-i18n="projects.modalNewSubtitle">创建后可绑定对话,跨会话共享事实黑板</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="projects-modal-close" onclick="closeProjectModal()" aria-label="关闭">×</button>
|
||||
<button type="button" class="projects-modal-close" onclick="closeProjectModal()" aria-label="关闭" data-i18n="common.close" data-i18n-attr="aria-label" data-i18n-skip-text="true">×</button>
|
||||
</div>
|
||||
<div class="projects-modal-body">
|
||||
<div class="projects-form-field">
|
||||
<label for="project-modal-name">项目名称 <span class="required">*</span></label>
|
||||
<input type="text" id="project-modal-name" class="form-input" placeholder="例如:某客户 Web 渗透" autocomplete="off">
|
||||
<label for="project-modal-name" data-i18n="projects.projectName">项目名称 <span class="required">*</span></label>
|
||||
<input type="text" id="project-modal-name" class="form-input" placeholder="例如:某客户 Web 渗透" autocomplete="off" data-i18n="projects.projectNamePlaceholder" data-i18n-attr="placeholder">
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="project-modal-description">项目描述</label>
|
||||
<textarea id="project-modal-description" class="form-input" rows="4" placeholder="测试范围、授权边界、注意事项…"></textarea>
|
||||
<label for="project-modal-description" data-i18n="projects.projectDescription">项目描述</label>
|
||||
<textarea id="project-modal-description" class="form-input" rows="4" placeholder="测试范围、授权边界、注意事项…" data-i18n="projects.projectDescriptionPlaceholder" data-i18n-attr="placeholder"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeProjectModal()">取消</button>
|
||||
<button class="btn-primary" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()">创建项目</button>
|
||||
<button class="btn-secondary" type="button" onclick="closeProjectModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()" data-i18n="projects.createProject">创建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4048,11 +4048,11 @@
|
||||
<div class="projects-modal-header">
|
||||
<div class="projects-modal-header-text">
|
||||
<div>
|
||||
<h3 id="fact-modal-title">添加事实</h3>
|
||||
<p class="projects-modal-subtitle">摘要注入黑板索引;body 沉淀攻击链与 POC,供审计复现(与漏洞记录分工)</p>
|
||||
<h3 id="fact-modal-title" data-i18n="projects.addFact">添加事实</h3>
|
||||
<p class="projects-modal-subtitle" data-i18n="projects.factModalSubtitle">摘要注入黑板索引;body 沉淀攻击链与 POC,供审计复现(与漏洞记录分工)</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="projects-modal-close" onclick="closeFactModal()" aria-label="关闭">×</button>
|
||||
<button type="button" class="projects-modal-close" onclick="closeFactModal()" aria-label="关闭" data-i18n="common.close" data-i18n-attr="aria-label" data-i18n-skip-text="true">×</button>
|
||||
</div>
|
||||
<div class="projects-modal-body">
|
||||
<div class="projects-form-field">
|
||||
@@ -4062,7 +4062,7 @@
|
||||
</div>
|
||||
<div class="projects-form-row">
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-category">分类</label>
|
||||
<label for="fact-modal-category" data-i18n="projects.category">分类</label>
|
||||
<select id="fact-modal-category" class="form-input" onchange="updateFactFormHints()">
|
||||
<option value="target">target(目标)</option>
|
||||
<option value="auth">auth(认证)</option>
|
||||
@@ -4076,7 +4076,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-confidence">置信度</label>
|
||||
<label for="fact-modal-confidence" data-i18n="projects.confidence">置信度</label>
|
||||
<select id="fact-modal-confidence" class="form-input">
|
||||
<option value="tentative">待确认</option>
|
||||
<option value="confirmed">已确认</option>
|
||||
@@ -4100,13 +4100,13 @@
|
||||
<p id="fact-modal-body-hint" class="projects-field-hint" role="status"></p>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-related-vuln">关联漏洞 ID</label>
|
||||
<input type="text" id="fact-modal-related-vuln" class="form-input" placeholder="可选">
|
||||
<label for="fact-modal-related-vuln" data-i18n="projects.relatedVulnIdLabel">关联漏洞 ID</label>
|
||||
<input type="text" id="fact-modal-related-vuln" class="form-input" placeholder="可选" data-i18n="projects.optional" data-i18n-attr="placeholder">
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeFactModal()">取消</button>
|
||||
<button class="btn-primary" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()">保存事实</button>
|
||||
<button class="btn-secondary" type="button" onclick="closeFactModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()" data-i18n="projects.saveFact">保存事实</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4115,30 +4115,30 @@
|
||||
<div class="projects-modal-header">
|
||||
<div class="projects-modal-header-text">
|
||||
<div>
|
||||
<h3 id="fact-detail-title">事实详情</h3>
|
||||
<h3 id="fact-detail-title" data-i18n="projects.factDetails">事实详情</h3>
|
||||
<p id="fact-detail-meta" class="projects-modal-subtitle"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="projects-modal-close" onclick="closeFactDetailModal()" aria-label="关闭">×</button>
|
||||
<button type="button" class="projects-modal-close" onclick="closeFactDetailModal()" aria-label="关闭" data-i18n="common.close" data-i18n-attr="aria-label" data-i18n-skip-text="true">×</button>
|
||||
</div>
|
||||
<div class="projects-modal-body">
|
||||
<p id="fact-detail-sparse-warn" class="projects-fact-sparse-warn" hidden></p>
|
||||
<div id="fact-detail-prev-wrap" class="fact-detail-prev-wrap" hidden>
|
||||
<h4 class="fact-detail-prev-title">上一版本</h4>
|
||||
<h4 class="fact-detail-prev-title" data-i18n="projects.previousVersion">上一版本</h4>
|
||||
<p id="fact-detail-prev-meta" class="projects-modal-subtitle"></p>
|
||||
<pre id="fact-detail-prev-body" class="fact-detail-body fact-detail-body--muted"></pre>
|
||||
</div>
|
||||
<h4 class="fact-detail-current-title">当前版本</h4>
|
||||
<h4 class="fact-detail-current-title" data-i18n="projects.currentVersion">当前版本</h4>
|
||||
<pre id="fact-detail-body" class="fact-detail-body"></pre>
|
||||
</div>
|
||||
<div class="projects-modal-footer projects-modal-footer--split">
|
||||
<div class="projects-modal-footer-left">
|
||||
<button class="btn-secondary btn-small" type="button" id="fact-detail-link-vuln-btn" onclick="linkFactToExistingVulnerability()" hidden>关联漏洞</button>
|
||||
<button class="btn-secondary btn-small" type="button" id="fact-detail-create-vuln-btn" onclick="createVulnerabilityFromCurrentFact()" hidden>生成漏洞草稿</button>
|
||||
<button class="btn-secondary btn-small" type="button" id="fact-detail-link-vuln-btn" onclick="linkFactToExistingVulnerability()" hidden data-i18n="projects.linkVulnerability">关联漏洞</button>
|
||||
<button class="btn-secondary btn-small" type="button" id="fact-detail-create-vuln-btn" onclick="createVulnerabilityFromCurrentFact()" hidden data-i18n="projects.createVulnerabilityDraft">生成漏洞草稿</button>
|
||||
</div>
|
||||
<div class="projects-modal-footer-right">
|
||||
<button class="btn-secondary" type="button" onclick="closeFactDetailModal()">关闭</button>
|
||||
<button class="btn-primary" type="button" id="fact-detail-edit-btn" onclick="editFactFromDetail()">编辑</button>
|
||||
<button class="btn-secondary" type="button" onclick="closeFactDetailModal()" data-i18n="common.close">关闭</button>
|
||||
<button class="btn-primary" type="button" id="fact-detail-edit-btn" onclick="editFactFromDetail()" data-i18n="common.edit">编辑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user