Add files via upload

This commit is contained in:
公明
2026-05-27 11:40:10 +08:00
committed by GitHub
parent c0f0861b31
commit 3f9dbb4214
22 changed files with 988 additions and 153 deletions
+5 -1
View File
@@ -61,4 +61,8 @@ max_iterations: 0
5) Follow-up Verification Plan(后续验证建议)
- 对每个优先条目:建议由哪个阶段子代理接手、需要补测的最小证据集
输出后直接结束。遇到证据不足的条目标注为“需要补证据”。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
输出后直接结束。遇到证据不足的条目标注为“需要补证据”。
+5 -1
View File
@@ -51,4 +51,8 @@ max_iterations: 0
- 可能仍残留的风险类别与建议监控方式(只做高层建议)
4) Handoff to Reporting(交接给报告的要点)
- 报告里应包含哪些字段以证明“合规清理”。
- 报告里应包含哪些字段以证明“合规清理”。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+5 -1
View File
@@ -61,4 +61,8 @@ max_iterations: 0
5) Open Questions(待澄清问题)
- 不足以继续的关键问题(尽量少而关键)
当你完成以上输出时,直接停止;不要向协调主代理以外的人解释过多背景。将所有不确定性标注为“需要补证据/需要澄清”。
当你完成以上输出时,直接停止;不要向协调主代理以外的人解释过多背景。将所有不确定性标注为“需要补证据/需要澄清”。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+5 -1
View File
@@ -50,4 +50,8 @@ max_iterations: 0
- 你要求执行的最小化原则(如不导出明文敏感字段、不保留原始样本等,用描述性语言)
4) Recommended Next Agent(下一步建议)
- 建议交给 `reporting-remediation``cleanup-rollback` 的证据输入要点。
- 建议交给 `reporting-remediation``cleanup-rollback` 的证据输入要点。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+4
View File
@@ -32,3 +32,7 @@ max_iterations: 0
- 优先用工具拿可验证事实,标注信息来源与置信度;避免无依据推测。
- 输出结构化(目标、发现项、证据摘要、建议后续动作),便于协调者合并进总报告。
- 不执行未授权的入侵或社工骚扰;双用途技术仅用于甲方书面授权场景。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+4
View File
@@ -32,3 +32,7 @@ max_iterations: 0
- 聚焦:内网拓扑与关键资产推断、凭据与令牌利用、常见横向协议与服务、权限路径与域/云环境注意事项(在工具与可见数据范围内)。
- 每一步说明假设前提与证据;禁止对未授权网段、生产无关系统或真实用户数据进行操作。
- 输出结构化:当前据点能力、发现的主机/服务、建议的下一步(可交给其他子代理或主代理编排)、风险与回滚注意点。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+5 -1
View File
@@ -51,4 +51,8 @@ max_iterations: 0
- 建议记录哪些证据字段(时间戳、目标、请求摘要、响应摘要、变更清单、回滚确认)
4) Stop & Rollback Criteria(停止与回滚标准)
- 触发阈值/不可控情况(用描述性语言即可)
- 触发阈值/不可控情况(用描述性语言即可)
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+26 -2
View File
@@ -102,10 +102,34 @@ description: plan_execute 模式下的规划/重规划侧主代理:拆解目
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。
## 证据与漏洞
## 证据、黑板与漏洞
- 要求结论有证据支撑(请求/响应、命令输出、可复现步骤);禁止无依据的确定断言。
- 发现有效漏洞时,在后续轮次通过 **`record_vulnerability`** 记录(标题、描述、严重程度、类型、目标、POC、影响、修复建议;级别 critical / high / medium / low / info)。
## 项目黑板(事实)与漏洞记录(分离)
当前对话若已绑定项目,系统会自动注入「项目黑板索引」(仅 `fact_key` + 摘要)。**摘要不足时必须调用 `get_project_fact(fact_key)` 获取 body,禁止凭摘要臆造细节。**
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。委派/子任务返回新认知或漏洞时,由协调者及时写入,勿假定子代理已记。
- **环境/目标/认证等认知**(非正式漏洞):使用 **`upsert_project_fact`**`fact_key` 建议 `category/slug`(如 `target/primary_domain`),同 key 覆盖更新;body 记端口/版本/凭据特征与证据来源。
- **发现与利用上下文**(审计复现):`fact_key` 建议 `finding/``chain/``exploit/``poc/` 前缀;**body 必填**完整攻击链(入口 → 步骤 → 原始请求/响应或命令 → 现象 → 关联 `related_vulnerability_id`),**禁止仅写结论**;summary 写「什么 + 在哪 + 如何验证」一行要点。
- **可交付漏洞**:使用 **`record_vulnerability`**(标题、描述、严重程度、类型、目标、证明 POC、影响、修复建议)。严重程度 critical / high / medium / low / info。
- 同一发现可能需**各记一次**(事实记可复现攻击链,漏洞记正式 findings)。误报用 **`deprecate_project_fact`** 或漏洞状态 false_positive。
- 事实多时用 **`list_project_facts`** / **`search_project_facts`** 检索。
- **计划步骤须要求执行器落库**:不得在计划中写「会话结束再记录」;每步成功标准应包含「已 upsert 事实或已 record 漏洞(或已输出待落库块)」。
### 事实写入规范(审计复现 / 知识沉淀)
- **summary**:索引用一行,须含「什么 + 在哪 + 如何触发/验证」要点,禁止只写结论(如仅写「存在 SQLi」)。
- **body**:完整可复现上下文,写入 `upsert_project_fact` 的 body 字段;索引不含 body,后续会话须靠 `get_project_fact` 取回。
- **category / fact_key 建议**
- 环境认知:`target/``auth/``infra/``business/`body 用环境模板即可)
- 发现与利用:`finding/``chain/``exploit/``poc/`(**必须**用攻击链模板填满 body:入口、逐步攻击链、原始请求/响应或命令、证据、关联漏洞 ID)
- **与漏洞记录分工**`record_vulnerability` 记可交付 findings;事实记**复现所需的全部上下文**(含失败尝试、绕过、依赖会话),二者可各记一次。
- 更新同一发现时保持相同 `fact_key` 覆盖写入,勿散落多个 key 导致上下文丢失。
严重程度:critical / high / medium / low / info。证明须含足够证据(请求响应、截图、命令输出等)。
## 执行器对用户输出(重要)
+22 -2
View File
@@ -117,9 +117,29 @@ description: supervisor 模式下的协调者:通过 transfer 委派专家子
3. 期望交付物是否可验收(例如:可复现命令、截图要点、结论段落)?
4. 是否已明确写出 URL/IP:Port/域名路径与 in-scope 边界(而非“按上文继续”)?
## 漏洞
## 项目黑板(事实)与漏洞记录(分离)
有效漏洞应通过 **`record_vulnerability`** 记录(含 POC 与严重性)。
当前对话若已绑定项目,系统会自动注入「项目黑板索引」(仅 `fact_key` + 摘要)。**摘要不足时必须调用 `get_project_fact(fact_key)` 获取 body,禁止凭摘要臆造细节。**
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。委派/子任务返回新认知或漏洞时,由协调者及时写入,勿假定子代理已记。
- **环境/目标/认证等认知**(非正式漏洞):使用 **`upsert_project_fact`**`fact_key` 建议 `category/slug`(如 `target/primary_domain`),同 key 覆盖更新;body 记端口/版本/凭据特征与证据来源。
- **发现与利用上下文**(审计复现):`fact_key` 建议 `finding/``chain/``exploit/``poc/` 前缀;**body 必填**完整攻击链(入口 → 步骤 → 原始请求/响应或命令 → 现象 → 关联 `related_vulnerability_id`),**禁止仅写结论**;summary 写「什么 + 在哪 + 如何验证」一行要点。
- **可交付漏洞**:使用 **`record_vulnerability`**(标题、描述、严重程度、类型、目标、证明 POC、影响、修复建议)。严重程度 critical / high / medium / low / info。
- 同一发现可能需**各记一次**(事实记可复现攻击链,漏洞记正式 findings)。误报用 **`deprecate_project_fact`** 或漏洞状态 false_positive。
- 事实多时用 **`list_project_facts`** / **`search_project_facts`** 检索。
### 事实写入规范(审计复现 / 知识沉淀)
- **summary**:索引用一行,须含「什么 + 在哪 + 如何触发/验证」要点,禁止只写结论(如仅写「存在 SQLi」)。
- **body**:完整可复现上下文,写入 `upsert_project_fact` 的 body 字段;索引不含 body,后续会话须靠 `get_project_fact` 取回。
- **category / fact_key 建议**
- 环境认知:`target/``auth/``infra/``business/`body 用环境模板即可)
- 发现与利用:`finding/``chain/``exploit/``poc/`(**必须**用攻击链模板填满 body:入口、逐步攻击链、原始请求/响应或命令、证据、关联漏洞 ID)
- **与漏洞记录分工**`record_vulnerability` 记可交付 findings;事实记**复现所需的全部上下文**(含失败尝试、绕过、依赖会话),二者可各记一次。
- 更新同一发现时保持相同 `fact_key` 覆盖写入,勿散落多个 key 导致上下文丢失。
严重程度:critical / high / medium / low / info。证明须含足够证据(请求响应、截图、命令输出等)。
## 表达
+23 -6
View File
@@ -127,12 +127,29 @@ description: 多代理模式下的 Deep 编排者:在已授权安全场景中
## 工具与 MCP
- **工具调用失败时**:1) 仔细分析错误信息,理解失败的具体原因;2) 如果工具不存在或未启用,尝试使用其他替代工具完成相同目标;3) 如果参数错误,根据错误提示修正参数后重试;4) 如果工具执行失败但输出了有用信息,可以基于这些信息继续分析;5) 如果确实无法使用某个工具,向用户说明问题,并建议替代方案或手动操作;6) 不要因为单个工具失败就停止整个测试流程,尝试其他方法继续完成任务。工具返回的错误信息会包含在工具响应中,请仔细阅读并做出合理决策。
- **项目黑板(事实)与漏洞记录(分离)**:当前对话若已绑定项目,系统会自动注入「项目黑板索引」(仅 `fact_key` + 摘要)。**摘要不足时必须调用 `get_project_fact(fact_key)` 获取 body,禁止凭摘要臆造细节。**
- **环境/目标/认证等认知**(非正式漏洞):使用 **`upsert_project_fact`**`fact_key` 建议 `category/slug`(如 `target/primary_domain`),同 key 覆盖更新;body 记端口/版本/凭据特征与证据来源。
- **发现与利用上下文**(审计复现):`fact_key` 建议 `finding/``chain/``exploit/``poc/` 前缀;**body 必填**完整攻击链(入口 → 步骤 → 原始请求/响应或命令 → 现象 → 关联 `related_vulnerability_id`),**禁止仅写结论**;summary 写「什么 + 在哪 + 如何验证」一行要点。
- **可交付漏洞**:使用 **`record_vulnerability`**(标题、描述、严重程度、类型、目标、证明 POC、影响、修复建议)。严重程度 critical / high / medium / low / info。
- 同一发现可能需**各记一次**(事实记可复现攻击链,漏洞记正式 findings)。误报用 **`deprecate_project_fact`** 或漏洞状态 false_positive
- 事实多时用 **`list_project_facts`** / **`search_project_facts`** 检索。
## 项目黑板(事实)与漏洞记录(分离)
当前对话若已绑定项目,系统会自动注入「项目黑板索引」(仅 `fact_key` + 摘要)。**摘要不足时必须调用 `get_project_fact(fact_key)` 获取 body,禁止凭摘要臆造细节。**
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。委派/子任务返回新认知或漏洞时,由协调者及时写入,勿假定子代理已记
- **环境/目标/认证等认知**(非正式漏洞):使用 **`upsert_project_fact`**`fact_key` 建议 `category/slug`(如 `target/primary_domain`),同 key 覆盖更新;body 记端口/版本/凭据特征与证据来源。
- **发现与利用上下文**(审计复现):`fact_key` 建议 `finding/``chain/``exploit/``poc/` 前缀;**body 必填**完整攻击链(入口 → 步骤 → 原始请求/响应或命令 → 现象 → 关联 `related_vulnerability_id`),**禁止仅写结论**;summary 写「什么 + 在哪 + 如何验证」一行要点。
- **可交付漏洞**:使用 **`record_vulnerability`**(标题、描述、严重程度、类型、目标、证明 POC、影响、修复建议)。严重程度 critical / high / medium / low / info。
- 同一发现可能需**各记一次**(事实记可复现攻击链,漏洞记正式 findings)。误报用 **`deprecate_project_fact`** 或漏洞状态 false_positive。
- 事实多时用 **`list_project_facts`** / **`search_project_facts`** 检索。
### 事实写入规范(审计复现 / 知识沉淀)
- **summary**:索引用一行,须含「什么 + 在哪 + 如何触发/验证」要点,禁止只写结论(如仅写「存在 SQLi」)。
- **body**:完整可复现上下文,写入 `upsert_project_fact` 的 body 字段;索引不含 body,后续会话须靠 `get_project_fact` 取回。
- **category / fact_key 建议**
- 环境认知:`target/``auth/``infra/``business/`body 用环境模板即可)
- 发现与利用:`finding/``chain/``exploit/``poc/`(**必须**用攻击链模板填满 body:入口、逐步攻击链、原始请求/响应或命令、证据、关联漏洞 ID)
- **与漏洞记录分工**`record_vulnerability` 记可交付 findings;事实记**复现所需的全部上下文**(含失败尝试、绕过、依赖会话),二者可各记一次。
- 更新同一发现时保持相同 `fact_key` 覆盖写入,勿散落多个 key 导致上下文丢失。
严重程度:critical / high / medium / low / info。证明须含足够证据(请求响应、截图、命令输出等)。
- **编排进度(待办)**:当你的任务包含 3 个或以上步骤,或你准备委派多个子目标并行/串行推进时,优先使用 `write_todos` 来向用户展示“当前在做什么/接下来做什么”。维护约束:同一时刻最多一个条目处于 `in_progress`;完成后立刻标记 `completed`;遇到阻塞就保留为 `in_progress` 并继续推进。
- **强触发建议(提升多 agent 使用率)**:如果你将要进行任何“证据收集/枚举/扫描/验证/复现/整理报告”这类实质执行动作,且不只是单步查询,请优先在第一个工具调用前就用 `write_todos` 建立计划;随后用 `task` 委派至少一个子代理获取结构化证据,而不是自己把全部步骤做完。
- **技能库(Skills)与知识库**:技能包位于服务器 `skills/` 目录(各子目录 `SKILL.md`,遵循 agentskills.io);知识库用于向量检索片段,Skills 为可执行工作流指令。多代理本会话通过内置 **`skill`** 工具渐进加载;子代理同样挂载 skill + 可选本机文件工具时,可在委派说明中提示按需加载。若当前无 skill 工具,需要完整 Skill 工作流时请使用多代理模式或切换为 Eino 编排会话。
+5 -1
View File
@@ -31,5 +31,9 @@ max_iterations: 0
- 禁止自行猜测目标、替换为历史目标或擅自发起全量探索。
- 以证据为中心:请求/响应、Payload、命令输出、截图说明等,便于审计与复现。
- 先确认边界与禁止项(如拒绝 DoS、数据破坏);发现有效漏洞时按协调者要求使用 `record_vulnerability` 等流程(若你的工具集中包含)
- 先确认边界与禁止项(如拒绝 DoS、数据破坏)。
- 输出包含:攻击路径摘要、关键步骤、影响评估、修复与缓解建议;语言简洁,便于主代理汇总。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+5 -1
View File
@@ -51,4 +51,8 @@ max_iterations: 0
- 列出需要清理/验证的痕迹类型(配置、会话、日志、服务变更等层级描述即可)
4) Recommended Next Steps(下一步建议)
- 建议由哪个阶段子代理接手,以及需要哪些证据输入。
- 建议由哪个阶段子代理接手,以及需要哪些证据输入。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+5 -1
View File
@@ -53,4 +53,8 @@ max_iterations: 0
4) Recommended Next Agent(下一步建议)
- 明确建议由哪个子代理接手(例如 `lateral-movement` / `persistence-maintenance` / `impact-exfiltration` / `reporting-remediation`
输出后直接结束。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
输出后直接结束。
+4
View File
@@ -34,3 +34,7 @@ max_iterations: 0
-**`description` / 用户消息 / 上文交接包** 中已给出资产列表、枚举结论或明确写「跳过全量枚举 / 仅做增量 / 从端口扫描或验证开始」,则**不得**为走完整流程而重新执行等价的广域子域爆破或相同参数集的枚举;仅在交接包声明的**缺口**上补充侦察。
- 若子目标实为**漏洞验证、协议利用、权限提升**等而非攻击面扩展,应**极短说明**「当前角色为侦察;建议协调者改派专项代理」并仅提供与侦察相关的最小补充信息,避免擅自把任务扩写成新一轮全盘资产收集。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
+5 -1
View File
@@ -55,4 +55,8 @@ max_iterations: 0
5) Appendix(附录)
- 术语、假设、证据清单索引(按证据类型列出即可)
输出后直接结束。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
输出后直接结束。
+5 -1
View File
@@ -57,4 +57,8 @@ max_iterations: 0
4) Uncertainties & Missing Evidence(不确定性与缺口)
- 列出最关键的缺口(尽量少,但要关键)
输出后直接结束。
## 边渗透边记录
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
输出后直接结束。
+328 -61
View File
@@ -20996,6 +20996,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
}
#page-projects .page-content.projects-page-layout {
display: flex;
align-items: stretch;
gap: 16px;
min-height: calc(100vh - 128px);
padding: 16px 20px 24px;
@@ -21004,6 +21005,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-sidebar-card {
width: 260px;
flex-shrink: 0;
align-self: stretch;
display: flex;
flex-direction: column;
background: #ffffff;
@@ -21011,7 +21013,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
border-radius: 14px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
overflow: hidden;
max-height: calc(100vh - 160px);
min-height: 420px;
}
.projects-sidebar-head {
display: flex;
@@ -21118,6 +21120,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-detail {
flex: 1;
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
min-height: 420px;
@@ -21173,6 +21176,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
overflow: hidden;
min-height: 420px;
align-self: stretch;
}
.projects-detail-header {
display: flex;
@@ -21244,6 +21248,11 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 4px 10px;
border-radius: 999px;
}
.projects-stat-chip--warn {
color: #92400e;
background: #fef3c7;
border-color: #fcd34d;
}
.projects-detail-header-actions {
flex-shrink: 0;
display: flex;
@@ -21283,6 +21292,18 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding: 16px 24px 24px;
overflow: auto;
min-height: 0;
display: flex;
flex-direction: column;
}
.projects-panel--settings {
overflow: hidden;
padding: 0;
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-items: stretch;
min-height: 0;
background: #fff;
}
/* display:flex 会覆盖 [hidden] 默认 display:none,非激活 Tab 会叠在事实黑板下方 */
.projects-panel[hidden] {
@@ -21300,6 +21321,185 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
border: 1px solid #eef2f7;
border-radius: 10px;
}
/* —— 事实黑板:说明 + 筛选工具栏 —— */
.projects-fact-toolbar {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 14px;
padding: 12px 14px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.projects-fact-toolbar-hint {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 0;
padding: 8px 10px;
font-size: 0.8125rem;
line-height: 1.5;
color: #475569;
background: #f0f7ff;
border: 1px solid #dbeafe;
border-radius: 8px;
}
.projects-fact-toolbar-hint-icon {
flex-shrink: 0;
margin-top: 2px;
color: #3b82f6;
}
.projects-fact-toolbar-hint strong {
font-weight: 600;
color: #334155;
}
.projects-fact-toolbar-hint code {
padding: 1px 5px;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
color: #1d4ed8;
background: rgba(255, 255, 255, 0.85);
border: 1px solid #bfdbfe;
border-radius: 4px;
}
.projects-fact-toolbar-filters {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 10px;
}
.projects-fact-filter-field {
display: flex;
flex-direction: column;
gap: 4px;
margin: 0;
min-width: 0;
}
.projects-fact-filter-field--search {
flex: 1 1 200px;
min-width: 160px;
max-width: 360px;
position: relative;
}
.projects-fact-filter-label {
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: #94a3b8;
line-height: 1.2;
}
.projects-fact-filter-field input,
.projects-fact-filter-field select {
width: 100%;
min-width: 0;
height: 34px;
padding: 0 10px;
font-size: 0.8125rem;
color: #0f172a;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.projects-fact-filter-field--search input {
padding-left: 34px;
background: #fff;
}
.projects-fact-search-icon {
position: absolute;
left: 10px;
top: 9px;
color: #94a3b8;
pointer-events: none;
}
.projects-fact-filter-field input:focus,
.projects-fact-filter-field select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.12);
background: #fff;
}
.projects-fact-filter-field select {
min-width: 108px;
cursor: pointer;
}
.projects-fact-filter-toggles {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding-bottom: 1px;
}
.projects-fact-toggle {
display: inline-flex;
align-items: center;
margin: 0;
cursor: pointer;
user-select: none;
}
.projects-fact-toggle input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.projects-fact-toggle span {
display: inline-flex;
align-items: center;
height: 34px;
padding: 0 12px;
font-size: 0.8125rem;
font-weight: 500;
color: #64748b;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
.projects-fact-toggle:hover span {
border-color: #cbd5e1;
color: #475569;
}
.projects-fact-toggle input:focus-visible + span {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
.projects-fact-toggle input:checked + span {
color: #1d4ed8;
background: #eff6ff;
border-color: #93c5fd;
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.08);
}
@media (max-width: 720px) {
.projects-fact-filter-field--search {
flex: 1 1 100%;
max-width: none;
}
.projects-fact-filter-toggles {
width: 100%;
}
}
.projects-filter-check {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.8125rem;
color: #475569;
white-space: nowrap;
cursor: pointer;
user-select: none;
}
.projects-pin-toggle {
margin-top: 4px;
}
.projects-panel-hint {
font-size: 0.8125rem;
color: #64748b;
@@ -21374,33 +21574,28 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
color: #b91c1c;
background: #fef2f2;
}
.projects-panel--settings {
padding: 0;
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
display: flex;
flex-direction: column;
}
/* —— 项目设置:左右分栏 + 底部危险区,无内层滚动 —— */
.projects-settings-layout {
width: 100%;
max-width: none;
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 0;
flex: 1;
min-height: 0;
overflow: auto;
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
}
.projects-settings-intro {
padding: 18px 24px 14px;
flex-shrink: 0;
padding: 14px 20px 12px;
border-bottom: 1px solid rgba(226, 232, 240, 0.8);
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(8px);
}
.projects-settings-intro-title {
margin: 0;
font-size: 0.9375rem;
font-weight: 600;
color: #0f172a;
letter-spacing: -0.01em;
}
.projects-settings-intro-hint {
margin: 4px 0 0;
@@ -21410,35 +21605,37 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
}
.projects-settings-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
gap: 18px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
grid-template-rows: minmax(min-content, 1fr);
gap: 14px;
align-items: stretch;
padding: 20px 24px;
padding: 14px 20px;
flex: 1 1 auto;
min-height: min-content;
}
.projects-settings-card {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 14px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.05), 0 4px 12px rgba(15, 23, 42, 0.03);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.05);
overflow: hidden;
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.projects-settings-card:hover {
border-color: #cbd5e1;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.06), 0 8px 20px rgba(15, 23, 42, 0.04);
.projects-settings-card--basic {
min-height: min-content;
}
.projects-settings-card--scope {
min-height: 300px;
min-height: 0;
}
.projects-settings-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 16px 20px;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid #f1f5f9;
background: #fafbfc;
}
@@ -21476,16 +21673,14 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
margin: 3px 0 0;
}
.projects-settings-card-body {
padding: 18px 20px 20px;
flex: 1;
min-height: 0;
padding: 14px 16px 16px;
}
.projects-settings-card-body--fill {
flex: 1 1 auto;
display: flex;
flex-direction: column;
flex: 1;
min-height: 200px;
gap: 10px;
gap: 8px;
min-height: 0;
}
.projects-scope-toolbar {
display: flex;
@@ -21505,25 +21700,29 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
color: #0f172a;
}
.projects-scope-editor {
flex: 1;
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
min-height: 120px;
border-radius: 10px;
overflow: hidden;
border: 1px solid #1e293b;
background: #0f172a;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.projects-scope-textarea {
flex: 1;
min-height: 180px;
.projects-panel--settings .projects-scope-textarea {
flex: 1 1 auto;
display: block;
width: 100%;
box-sizing: border-box;
min-height: 132px;
max-height: none;
resize: vertical;
border: none !important;
border-radius: 0 !important;
background: #0f172a !important;
color: #e2e8f0 !important;
padding: 14px 16px !important;
padding: 12px 14px !important;
font-size: 0.8125rem !important;
line-height: 1.6 !important;
box-shadow: none !important;
@@ -21561,32 +21760,36 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
cursor: pointer;
}
.projects-settings-card--danger {
flex-shrink: 0;
flex-direction: row;
align-items: center;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin: 0 24px 16px;
padding: 16px 20px;
background: linear-gradient(135deg, #fffbfb 0%, #fff 50%);
height: auto;
margin: 0 20px 16px;
padding: 18px 20px;
background: linear-gradient(135deg, #fffbfb 0%, #fff 55%);
border-color: #fecaca;
box-shadow: 0 1px 2px rgba(220, 38, 38, 0.06);
}
.projects-settings-card--danger:hover {
border-color: #fca5a5;
}
.projects-settings-danger-main {
min-width: 0;
flex: 1;
display: flex;
align-items: flex-start;
gap: 12px;
gap: 14px;
}
.projects-settings-card--danger .projects-settings-icon--red {
margin-top: 1px;
}
.projects-settings-card--danger .projects-settings-card-title {
margin: 0 0 4px;
margin: 0 0 6px;
}
.projects-settings-card--danger .projects-settings-card-hint {
margin: 0;
max-width: 560px;
line-height: 1.6;
color: #64748b;
}
.projects-settings-card-title {
font-size: 0.9375rem;
@@ -21602,22 +21805,28 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
.projects-settings-danger-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
gap: 10px;
flex-shrink: 0;
align-self: center;
padding-top: 2px;
}
.projects-settings-danger-actions .btn-small {
min-height: 34px;
padding: 7px 14px;
}
.projects-settings-footer {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 24px;
margin-top: auto;
border-top: 1px solid #e2e8f0;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(10px);
position: sticky;
bottom: 0;
z-index: 2;
margin: 0;
border-top: 1px solid #eef2f7;
background: #fff;
border-radius: 0 0 14px 14px;
box-shadow: 0 -1px 0 rgba(15, 23, 42, 0.04);
}
.projects-settings-footer-hint {
font-size: 0.8125rem;
@@ -21651,33 +21860,35 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
@media (max-width: 960px) {
.projects-settings-grid {
grid-template-columns: 1fr;
padding: 16px;
grid-template-rows: auto;
padding: 12px 16px;
}
.projects-settings-card--scope {
min-height: 0;
}
.projects-form-row--2 {
grid-template-columns: 1fr;
.projects-settings-card {
height: auto;
}
.projects-settings-card--danger {
flex-direction: column;
align-items: stretch;
margin: 0 16px 12px;
margin: 0 16px 10px;
padding: 16px;
}
.projects-settings-danger-actions {
justify-content: flex-end;
}
.projects-form-row--2 {
grid-template-columns: 1fr;
}
.projects-settings-footer {
flex-direction: column;
align-items: stretch;
padding: 14px 16px;
padding: 12px 16px;
}
.projects-settings-footer .btn-primary {
justify-content: center;
width: 100%;
}
.projects-settings-intro {
padding: 14px 16px 12px;
padding: 12px 16px 10px;
}
}
.projects-form-field {
@@ -21969,6 +22180,56 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
body.projects-modal-open {
overflow: hidden;
}
.fact-detail-prev-wrap {
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 1px dashed #e2e8f0;
}
.fact-detail-prev-title,
.fact-detail-current-title {
margin: 0 0 8px;
font-size: 0.8125rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.fact-detail-body--muted {
opacity: 0.85;
max-height: 200px;
}
.projects-modal-footer--split {
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.projects-modal-footer-left,
.projects-modal-footer-right {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.vulnerability-related-facts {
margin-top: 12px;
padding: 10px 12px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.8125rem;
}
.vulnerability-related-facts ul {
margin: 8px 0 0;
padding-left: 18px;
}
.vulnerability-related-facts li {
margin: 4px 0;
}
.vulnerability-related-facts a {
color: #2563eb;
cursor: pointer;
text-decoration: underline;
}
.fact-detail-body {
max-height: 50vh;
overflow: auto;
@@ -22051,6 +22312,12 @@ body.projects-modal-open {
color: #64748b;
background: #f1f5f9;
}
.projects-fact-vuln-link {
display: block;
margin-top: 4px;
font-size: 0.6875rem;
color: #7c3aed;
}
.vulnerability-filter-field--project select {
min-width: 120px;
max-width: 160px;
+3
View File
@@ -2912,6 +2912,9 @@ async function startNewConversation() {
window.currentConversationId = '';
} catch (e) { /* ignore */ }
currentConversationGroupId = null; // 新对话不属于任何分组
if (typeof ensureDefaultActiveProjectForNewChat === 'function') {
ensureDefaultActiveProjectForNewChat().catch(() => {});
}
if (typeof refreshChatProjectSelector === 'function') {
refreshChatProjectSelector();
}
+43 -34
View File
@@ -2068,69 +2068,78 @@ function showToastNotification(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
// 根据类型设置颜色
const typeStyles = {
success: {
background: '#28a745',
color: '#fff',
icon: '✅'
background: '#f4fbf6',
border: '1px solid #cce8d4',
color: '#3d6654',
iconColor: '#52a06a',
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M5 8l2 2 4-4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>'
},
error: {
background: '#dc3545',
color: '#fff',
icon: '❌'
background: '#fef7f7',
border: '1px solid #f3d0d0',
color: '#8b4444',
iconColor: '#c96a6a',
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M6 6l4 4M10 6l-4 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>'
},
info: {
background: '#17a2b8',
color: '#fff',
icon: '️'
background: '#f5f9ff',
border: '1px solid #cfe0f5',
color: '#4a6078',
iconColor: '#6b8fbf',
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.2"/><path d="M8 7v4M8 5.5v.01" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>'
},
warning: {
background: '#ffc107',
color: '#000',
icon: '⚠️'
background: '#fffbf3',
border: '1px solid #f0dfc0',
color: '#7a6535',
iconColor: '#c4a04a',
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 2.5l6 10.5H2L8 2.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M8 7v2.5M8 11v.01" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>'
}
};
const style = typeStyles[type] || typeStyles.info;
toast.style.cssText = `
background: ${style.background};
border: ${style.border};
color: ${style.color};
padding: 14px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 300px;
max-width: 500px;
padding: 10px 14px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
min-width: 220px;
max-width: 420px;
pointer-events: auto;
animation: slideInRight 0.3s ease-out;
animation: slideInRight 0.25s ease-out;
display: flex;
align-items: center;
gap: 12px;
font-size: 0.9375rem;
line-height: 1.5;
gap: 10px;
font-size: 0.875rem;
line-height: 1.45;
word-wrap: break-word;
backdrop-filter: blur(8px);
`;
toast.innerHTML = `
<span style="font-size: 1.2em; flex-shrink: 0;">${style.icon}</span>
<span style="color: ${style.iconColor}; flex-shrink: 0; display: flex; align-items: center;">${style.icon}</span>
<span style="flex: 1;">${escapeHtml(message)}</span>
<button onclick="this.parentElement.remove()" style="
background: transparent;
border: none;
color: ${style.color};
cursor: pointer;
font-size: 1.2em;
font-size: 1rem;
padding: 0;
margin-left: 8px;
opacity: 0.7;
margin-left: 4px;
opacity: 0.45;
flex-shrink: 0;
width: 24px;
height: 24px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">×</button>
" onmouseover="this.style.opacity='0.75'" onmouseout="this.style.opacity='0.45'">×</button>
`;
container.appendChild(toast);
@@ -2156,7 +2165,7 @@ if (!document.getElementById('toast-notification-styles')) {
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
transform: translateX(16px);
opacity: 0;
}
to {
@@ -2170,7 +2179,7 @@ if (!document.getElementById('toast-notification-styles')) {
opacity: 1;
}
to {
transform: translateX(100%);
transform: translateX(16px);
opacity: 0;
}
}
+322 -24
View File
@@ -187,6 +187,25 @@ function prefetchProjectsForChat() {
ensureProjectsLoaded().catch(() => {});
}
/** 新对话时:保留有效 activeProjectId,否则默认选中第一个进行中的项目 */
async function ensureDefaultActiveProjectForNewChat() {
try {
await ensureProjectsLoaded();
const cur = getActiveProjectId();
if (cur && isActiveChatProjectId(cur)) return cur;
const first =
projectsCache.find((p) => p.pinned && p.status !== 'archived') ||
projectsCache.find((p) => p.status !== 'archived');
if (first) {
setActiveProjectId(first.id);
return first.id;
}
} catch (e) {
console.warn(e);
}
return '';
}
function getProjectName(id) {
return projectNameById[id] || id || '';
}
@@ -357,15 +376,39 @@ function updateProjectStatusPill(status) {
el.className = 'projects-status-pill ' + (archived ? 'projects-status-pill--archived' : 'projects-status-pill--active');
}
function updateProjectStats(factCount, vulnCount) {
function updateProjectStats(stats) {
const s = stats || {};
const f = document.getElementById('project-stat-facts');
const v = document.getElementById('project-stat-vulns');
if (f) f.textContent = `${factCount ?? 0} 条事实`;
if (v) v.textContent = `${vulnCount ?? 0} 个漏洞`;
const c = document.getElementById('project-stat-conversations');
const sparse = document.getElementById('project-stat-sparse');
const fc = s.fact_count ?? s.factCount ?? 0;
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 (sparse) {
if (sc > 0) {
sparse.hidden = false;
sparse.textContent = `${sc} 待补全`;
} else {
sparse.hidden = true;
}
}
}
async function selectProject(id) {
currentProjectId = id;
const searchEl = document.getElementById('project-facts-search');
const catEl = document.getElementById('project-facts-filter-category');
const confEl = document.getElementById('project-facts-filter-confidence');
const sparseEl = document.getElementById('project-facts-filter-sparse');
if (searchEl) searchEl.value = '';
if (catEl) catEl.value = '';
if (confEl) confEl.value = '';
if (sparseEl) sparseEl.checked = false;
renderProjectsSidebar();
updateProjectsDetailVisibility();
try {
@@ -379,6 +422,8 @@ async function selectProject(id) {
document.getElementById('project-edit-scope').value = p.scope_json || '';
const statusEl = document.getElementById('project-edit-status');
if (statusEl) statusEl.value = p.status || 'active';
const pinEl = document.getElementById('project-edit-pinned');
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)}`;
@@ -397,42 +442,78 @@ async function selectProject(id) {
} catch (e) {
console.warn(e);
}
refreshProjectHeaderStats();
await refreshProjectHeaderStats();
switchProjectTab(currentProjectTab);
}
function switchProjectTab(tab) {
currentProjectTab = tab;
['facts', 'vulns', 'settings'].forEach((t) => {
['facts', 'conversations', 'vulns', 'settings'].forEach((t) => {
const btn = document.getElementById(`project-tab-${t}`);
const panel = document.getElementById(`project-panel-${t}`);
if (btn) btn.classList.toggle('is-active', t === tab);
if (panel) panel.hidden = t !== tab;
});
if (tab === 'facts') loadProjectFacts();
if (tab === 'conversations') loadProjectConversations();
if (tab === 'vulns') loadProjectVulnerabilities();
}
function buildProjectFactsQueryParams() {
const params = new URLSearchParams();
params.set('limit', '200');
const search = document.getElementById('project-facts-search')?.value?.trim();
const category = document.getElementById('project-facts-filter-category')?.value?.trim();
const confidence = document.getElementById('project-facts-filter-confidence')?.value?.trim();
const sparseOnly = document.getElementById('project-facts-filter-sparse')?.checked;
const hideDeprecated = document.getElementById('project-facts-filter-hide-deprecated')?.checked;
if (search) params.set('search', search);
if (category) params.set('category', category);
if (confidence) params.set('confidence', confidence);
if (sparseOnly) params.set('sparse_only', 'true');
if (hideDeprecated) params.set('exclude_deprecated', 'true');
return params;
}
function debouncedLoadProjectFacts() {
if (_projectFactsFilterDebounce) clearTimeout(_projectFactsFilterDebounce);
_projectFactsFilterDebounce = setTimeout(() => {
_projectFactsFilterDebounce = null;
loadProjectFacts();
}, 280);
}
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>';
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?limit=200`);
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>';
return;
}
const facts = await res.json();
if (!facts.length) {
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="7">暂无事实,点击「添加事实」或由 Agent 自动写入</td></tr>';
const hasFilter =
document.getElementById('project-facts-search')?.value?.trim() ||
document.getElementById('project-facts-filter-category')?.value ||
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 自动写入'
}</td></tr>`;
refreshProjectHeaderStats();
return;
}
tbody.innerHTML = facts.map((f) => {
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>`
: '';
return `<tr>
<td><code>${keyEsc}</code></td>
<td><code>${keyEsc}</code>${vulnLink}</td>
<td>${formatCategoryBadge(f.category)}</td>
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
<td>${formatFactBodyBadge(f)}</td>
@@ -447,34 +528,85 @@ async function loadProjectFacts() {
async function refreshProjectHeaderStats() {
if (!currentProjectId) return;
try {
const [factsRes, vulnRes] = await Promise.all([
apiFetch(`/api/projects/${currentProjectId}/facts?limit=500`),
apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=100`),
]);
let fc = 0;
let vc = 0;
if (factsRes.ok) {
const f = await factsRes.json();
fc = Array.isArray(f) ? f.length : 0;
}
if (vulnRes.ok) {
const d = await vulnRes.json();
const items = d.Vulnerabilities || d.vulnerabilities || d.items || [];
vc = items.length;
}
updateProjectStats(fc, vc);
const res = await apiFetch(`/api/projects/${currentProjectId}/stats`);
if (!res.ok) return;
const stats = await res.json();
updateProjectStats(stats);
} catch (e) {
console.warn(e);
}
}
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>';
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>';
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>';
return;
}
tbody.innerHTML = items
.map((conv) => {
const id = conv.id;
const idEsc = escapeHtml(id);
const title = escapeHtml(conv.title || '未命名对话');
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>
</div>
</td>
</tr>`;
})
.join('');
}
function openProjectConversation(conversationId) {
if (!conversationId) return;
if (typeof switchPage === 'function') {
switchPage('chat');
}
setTimeout(() => {
if (typeof loadConversation === 'function') {
loadConversation(conversationId);
}
}, 200);
}
async function unbindConversationFromProject(conversationId) {
if (!conversationId || !confirm('解除该对话与当前项目的绑定?')) 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('解绑失败');
loadProjectConversations();
refreshProjectHeaderStats();
}
let _factDetailKey = null;
let _factDetailFact = null;
let _projectFactsFilterDebounce = null;
async function viewProjectFactBody(factKey) {
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`);
if (!res.ok) return alert('加载失败');
const f = await res.json();
_factDetailKey = f.fact_key;
_factDetailFact = f;
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
const metaParts = [
`分类: ${f.category}`,
@@ -483,6 +615,7 @@ async function viewProjectFactBody(factKey) {
];
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('含上一版本');
document.getElementById('fact-detail-meta').textContent = metaParts.join(' · ');
document.getElementById('fact-detail-body').textContent = f.body || '(无 body)';
const warnEl = document.getElementById('fact-detail-sparse-warn');
@@ -496,6 +629,30 @@ async function viewProjectFactBody(factKey) {
warnEl.textContent = '';
}
}
const prevWrap = document.getElementById('fact-detail-prev-wrap');
if (prevWrap) {
prevWrap.hidden = true;
if (f.id && f.supersedes_fact_id) {
try {
const prevRes = await apiFetch(
`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}/previous-version`,
);
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)';
}
} catch (e) {
console.warn(e);
}
}
}
const linkBtn = document.getElementById('fact-detail-link-vuln-btn');
const createBtn = document.getElementById('fact-detail-create-vuln-btn');
if (linkBtn) linkBtn.hidden = false;
if (createBtn) createBtn.hidden = false;
openProjectsOverlay('fact-detail-modal');
}
@@ -508,6 +665,99 @@ function editFactFromDetail() {
function closeFactDetailModal() {
closeProjectsOverlay('fact-detail-modal');
_factDetailKey = null;
_factDetailFact = null;
}
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('加载漏洞列表失败');
const data = await res.json();
const items = data.Vulnerabilities || data.vulnerabilities || data.items || [];
if (!items.length) return alert('本项目暂无漏洞,请先创建或让 Agent 记录漏洞');
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')}`);
if (pick == null || pick === '') return;
const idx = parseInt(pick, 10) - 1;
if (Number.isNaN(idx) || idx < 0 || idx >= items.length) return alert('序号无效');
const vulnId = items[idx].id;
const upd = await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fact_key: f.fact_key,
category: f.category,
summary: f.summary,
body: f.body || '',
confidence: f.confidence,
related_vulnerability_id: vulnId,
}),
});
if (!upd.ok) return alert('关联失败');
alert('已关联漏洞');
closeFactDetailModal();
loadProjectFacts();
}
async function createVulnerabilityFromCurrentFact() {
const f = _factDetailFact;
if (!f || !currentProjectId) return;
let convId =
(f.source_conversation_id || '').trim() ||
(typeof window.currentConversationId === 'string' ? window.currentConversationId.trim() : '');
if (!convId) {
convId = prompt('创建漏洞需要对话 ID(可与来源会话一致):', '')?.trim() || '';
}
if (!convId) return alert('已取消:未提供 conversation_id');
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} 生成`,
severity,
status: 'open',
type: f.category || 'finding',
target: '',
proof: f.body || '',
impact: '',
recommendation: '',
};
const res = await apiFetch('/api/vulnerabilities', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return alert(err.error || '创建漏洞失败');
}
const vuln = await res.json();
await apiFetch(`/api/projects/${currentProjectId}/facts/${encodeURIComponent(f.id)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fact_key: f.fact_key,
category: f.category,
summary: f.summary,
body: f.body || '',
confidence: f.confidence,
related_vulnerability_id: vuln.id,
}),
});
alert(`已创建漏洞并关联:${vuln.title || vuln.id}`);
closeFactDetailModal();
loadProjectFacts();
if (currentProjectTab === 'vulns') loadProjectVulnerabilities();
}
function inferSeverityFromFact(f) {
const c = (f.category || '').toLowerCase();
const key = (f.fact_key || '').toLowerCase();
if (c === 'exploit' || c === 'poc' || key.includes('rce') || key.includes('sqli')) return 'high';
if (c === 'finding' || c === 'chain') return 'medium';
return 'medium';
}
async function deprecateProjectFactByKey(factKey) {
@@ -573,6 +823,7 @@ async function loadProjectVulnerabilities() {
<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>
</div>
</td>
</tr>`;
@@ -587,6 +838,44 @@ function openVulnerabilityDetail(vulnId) {
}
}
async function viewFactsForVulnerability(vulnId) {
if (!currentProjectId) return;
switchProjectTab('facts');
const searchEl = document.getElementById('project-facts-search');
const catEl = document.getElementById('project-facts-filter-category');
const confEl = document.getElementById('project-facts-filter-confidence');
const sparseEl = document.getElementById('project-facts-filter-sparse');
const hideDepEl = document.getElementById('project-facts-filter-hide-deprecated');
if (searchEl) searchEl.value = '';
if (catEl) catEl.value = '';
if (confEl) confEl.value = '';
if (sparseEl) sparseEl.checked = false;
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('加载关联事实失败');
const facts = await res.json();
if (!facts.length) {
alert('该漏洞暂无关联事实,可在事实详情中「关联漏洞」或「生成漏洞草稿」建立链接');
loadProjectFacts();
return;
}
if (facts.length === 1) {
viewProjectFactBody(facts[0].fact_key);
return;
}
const pick = prompt(
`该漏洞关联 ${facts.length} 条事实,输入序号查看:\n${facts.map((f, i) => `${i + 1}. ${f.fact_key}`).join('\n')}`,
);
if (pick == null || pick === '') {
loadProjectFacts();
return;
}
const idx = parseInt(pick, 10) - 1;
if (facts[idx]) viewProjectFactBody(facts[idx].fact_key);
else loadProjectFacts();
}
function openProjectsOverlay(id) {
const el = document.getElementById(id);
if (!el) return;
@@ -683,6 +972,7 @@ async function saveProjectSettings() {
description: document.getElementById('project-edit-description').value.trim(),
scope_json: scopeRaw,
status: document.getElementById('project-edit-status')?.value || 'active',
pinned: !!document.getElementById('project-edit-pinned')?.checked,
};
const res = await apiFetch(`/api/projects/${currentProjectId}`, {
method: 'PUT',
@@ -1143,6 +1433,7 @@ window.toggleChatProjectPanel = toggleChatProjectPanel;
window.closeChatProjectPanel = closeChatProjectPanel;
window.selectChatProject = selectChatProject;
window.prefetchProjectsForChat = prefetchProjectsForChat;
window.ensureDefaultActiveProjectForNewChat = ensureDefaultActiveProjectForNewChat;
window.getActiveProjectId = getActiveProjectId;
window.getProjectName = getProjectName;
window.viewProjectFactBody = viewProjectFactBody;
@@ -1153,5 +1444,12 @@ window.restoreProjectFactByKey = restoreProjectFactByKey;
window.openVulnerabilitiesForProject = openVulnerabilitiesForProject;
window.openVulnerabilityDetail = openVulnerabilityDetail;
window.filterProjectsList = filterProjectsList;
window.debouncedLoadProjectFacts = debouncedLoadProjectFacts;
window.linkFactToExistingVulnerability = linkFactToExistingVulnerability;
window.createVulnerabilityFromCurrentFact = createVulnerabilityFromCurrentFact;
window.viewFactsForVulnerability = viewFactsForVulnerability;
window.openProjectConversation = openProjectConversation;
window.unbindConversationFromProject = unbindConversationFromProject;
window.loadProjectConversations = loadProjectConversations;
window.rebuildProjectNameMap = rebuildProjectNameMap;
window.projectNameById = projectNameById;
+65
View File
@@ -932,6 +932,7 @@ function renderVulnerabilities(vulnerabilities) {
${vuln.proof ? `<div class="vulnerability-proof"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailProof'))}:</strong><pre>${escapeHtml(vuln.proof)}</pre></div>` : ''}
${vuln.impact ? `<div class="vulnerability-impact"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailImpact'))}:</strong> ${escapeHtml(vuln.impact)}</div>` : ''}
${vuln.recommendation ? `<div class="vulnerability-recommendation"><strong>${escapeHtml(vulnT('vulnerabilityPage.detailRecommendation'))}:</strong> ${escapeHtml(vuln.recommendation)}</div>` : ''}
<div class="vulnerability-related-facts" id="vuln-related-facts-${vuln.id}" data-project-id="${escapeHtml(vuln.project_id || '')}" data-vuln-id="${escapeHtml(vuln.id)}" hidden></div>
</div>
</div>
`;
@@ -1295,12 +1296,76 @@ function toggleVulnerabilityDetails(id) {
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
loadVulnerabilityRelatedFacts(id).catch((e) => console.warn(e));
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
}
/** 展开漏洞详情时加载关联项目事实 */
async function loadVulnerabilityRelatedFacts(vulnId) {
const el = document.getElementById(`vuln-related-facts-${vulnId}`);
if (!el || el.dataset.loaded === '1') return;
const projectId = (el.dataset.projectId || '').trim();
if (!projectId) {
el.hidden = true;
el.dataset.loaded = '1';
return;
}
el.hidden = false;
el.innerHTML = '<span>加载关联事实…</span>';
try {
const res = await apiFetch(
`/api/projects/${encodeURIComponent(projectId)}/facts?related_vulnerability_id=${encodeURIComponent(vulnId)}&limit=20&exclude_deprecated=true`,
);
if (!res.ok) throw new Error('fetch failed');
const facts = await res.json();
if (!Array.isArray(facts) || !facts.length) {
el.innerHTML =
'<strong>关联事实</strong><p style="margin:6px 0 0;color:#64748b">暂无;可在「项目管理」事实详情中关联或生成漏洞草稿。</p>';
el.dataset.loaded = '1';
return;
}
const items = facts
.map((f) => {
const key = escapeHtml(f.fact_key);
const sum = escapeHtml((f.summary || '').slice(0, 120));
const pid = escapeHtml(projectId);
const rawKey = escapeHtml(f.fact_key);
return `<li><a role="button" href="#" data-project-id="${pid}" data-fact-key="${rawKey}" onclick="event.preventDefault();openProjectFactFromVulnerability(this.dataset.projectId,this.dataset.factKey)"><code>${key}</code></a> — ${sum}</li>`;
})
.join('');
el.innerHTML = `<strong>关联事实(${facts.length}</strong><ul>${items}</ul>`;
el.dataset.loaded = '1';
} catch (e) {
el.innerHTML = '<strong>关联事实</strong><p style="margin:6px 0 0;color:#b91c1c">加载失败</p>';
}
}
function openProjectFactFromVulnerability(projectId, factKey) {
if (!projectId || !factKey) return;
if (typeof switchPage === 'function') {
switchPage('projects');
}
setTimeout(async () => {
if (typeof window.initProjectsPage === 'function') {
await window.initProjectsPage();
}
if (typeof window.selectProject === 'function') {
await window.selectProject(projectId);
}
if (typeof window.switchProjectTab === 'function') {
window.switchProjectTab('facts');
}
if (typeof window.viewProjectFactBody === 'function') {
window.viewProjectFactBody(factKey);
}
}, 350);
}
window.openProjectFactFromVulnerability = openProjectFactFromVulnerability;
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
+94 -14
View File
@@ -1453,6 +1453,8 @@
<div class="projects-detail-stats" id="projects-detail-stats">
<span class="projects-stat-chip" id="project-stat-facts">0 条事实</span>
<span class="projects-stat-chip" id="project-stat-vulns">0 个漏洞</span>
<span class="projects-stat-chip" id="project-stat-conversations">0 个对话</span>
<span class="projects-stat-chip projects-stat-chip--warn" id="project-stat-sparse" hidden>0 待补全</span>
</div>
</div>
<div class="projects-detail-header-actions">
@@ -1462,13 +1464,63 @@
</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>
</nav>
<div id="project-panel-facts" class="projects-panel" role="tabpanel">
<div class="projects-panel-toolbar">
<span class="projects-panel-hint">索引仅含 key + 摘要(须含「什么+在哪+如何验证」);攻击链/POC 写在 bodyAgent 通过 get_project_fact 复现</span>
<button class="btn-primary btn-small" type="button" onclick="showAddFactModal()">+ 添加事实</button>
<div class="projects-fact-toolbar">
<p class="projects-fact-toolbar-hint" role="note">
<svg class="projects-fact-toolbar-hint-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
<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>
</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>
<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">
</label>
<label class="projects-fact-filter-field">
<span class="projects-fact-filter-label">分类</span>
<select id="project-facts-filter-category" onchange="loadProjectFacts()">
<option value="">全部</option>
<option value="target">target</option>
<option value="auth">auth</option>
<option value="infra">infra</option>
<option value="business">business</option>
<option value="finding">finding</option>
<option value="chain">chain</option>
<option value="exploit">exploit</option>
<option value="poc">poc</option>
<option value="note">note</option>
</select>
</label>
<label class="projects-fact-filter-field">
<span class="projects-fact-filter-label">置信度</span>
<select id="project-facts-filter-confidence" onchange="loadProjectFacts()">
<option value="">全部</option>
<option value="confirmed">已确认</option>
<option value="tentative">待确认</option>
<option value="deprecated">已废弃</option>
</select>
</label>
<div class="projects-fact-filter-toggles" role="group" aria-label="显示选项">
<label class="projects-fact-toggle">
<input type="checkbox" id="project-facts-filter-sparse" onchange="loadProjectFacts()">
<span>仅待补全</span>
</label>
<label class="projects-fact-toggle">
<input type="checkbox" id="project-facts-filter-hide-deprecated" checked onchange="loadProjectFacts()">
<span>隐藏废弃</span>
</label>
</div>
</div>
</div>
<div class="projects-table-wrap">
<table class="data-table data-table--projects">
@@ -1477,6 +1529,17 @@
</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>
</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>
<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>
@@ -1526,9 +1589,14 @@
</div>
</div>
</div>
<div class="projects-form-field">
<label class="projects-filter-check projects-pin-toggle">
<input type="checkbox" id="project-edit-pinned"> 置顶项目(列表优先显示)
</label>
</div>
<div class="projects-form-field">
<label for="project-edit-description">描述</label>
<textarea id="project-edit-description" class="form-input" rows="4" placeholder="测试目标、授权范围、联系人、注意事项…"></textarea>
<textarea id="project-edit-description" class="form-input" rows="3" placeholder="测试目标、授权范围、联系人、注意事项…"></textarea>
</div>
</div>
</section>
@@ -1572,14 +1640,14 @@
<button class="btn-secondary btn-small btn-danger-outline" type="button" onclick="deleteCurrentProject()">删除项目</button>
</div>
</section>
<footer class="projects-settings-footer">
<span class="projects-settings-footer-hint">修改后请点击保存以同步到服务器</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>
保存更改
</button>
</footer>
</div>
<footer class="projects-settings-footer">
<span class="projects-settings-footer-hint">修改后请点击保存以同步到服务器</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>
保存更改
</button>
</footer>
</div>
</div>
</main>
@@ -4046,11 +4114,23 @@
</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>
<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>
<pre id="fact-detail-body" class="fact-detail-body"></pre>
</div>
<div class="projects-modal-footer">
<button class="btn-secondary" type="button" onclick="closeFactDetailModal()">关闭</button>
<button class="btn-primary" type="button" id="fact-detail-edit-btn" onclick="editFactFromDetail()">编辑</button>
<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>
</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>
</div>
</div>
</div>
</div>