mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-28 02:02:27 +02:00
Add files via upload
This commit is contained in:
@@ -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 要点),供协调者**立即**写入。
|
||||
|
||||
输出后直接结束。遇到证据不足的条目标注为“需要补证据”。
|
||||
|
||||
@@ -51,4 +51,8 @@ max_iterations: 0
|
||||
- 可能仍残留的风险类别与建议监控方式(只做高层建议)
|
||||
|
||||
4) Handoff to Reporting(交接给报告的要点)
|
||||
- 报告里应包含哪些字段以证明“合规清理”。
|
||||
- 报告里应包含哪些字段以证明“合规清理”。
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -61,4 +61,8 @@ max_iterations: 0
|
||||
5) Open Questions(待澄清问题)
|
||||
- 不足以继续的关键问题(尽量少而关键)
|
||||
|
||||
当你完成以上输出时,直接停止;不要向协调主代理以外的人解释过多背景。将所有不确定性标注为“需要补证据/需要澄清”。
|
||||
当你完成以上输出时,直接停止;不要向协调主代理以外的人解释过多背景。将所有不确定性标注为“需要补证据/需要澄清”。
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -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 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -32,3 +32,7 @@ max_iterations: 0
|
||||
- 优先用工具拿可验证事实,标注信息来源与置信度;避免无依据推测。
|
||||
- 输出结构化(目标、发现项、证据摘要、建议后续动作),便于协调者合并进总报告。
|
||||
- 不执行未授权的入侵或社工骚扰;双用途技术仅用于甲方书面授权场景。
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -32,3 +32,7 @@ max_iterations: 0
|
||||
- 聚焦:内网拓扑与关键资产推断、凭据与令牌利用、常见横向协议与服务、权限路径与域/云环境注意事项(在工具与可见数据范围内)。
|
||||
- 每一步说明假设前提与证据;禁止对未授权网段、生产无关系统或真实用户数据进行操作。
|
||||
- 输出结构化:当前据点能力、发现的主机/服务、建议的下一步(可交给其他子代理或主代理编排)、风险与回滚注意点。
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -51,4 +51,8 @@ max_iterations: 0
|
||||
- 建议记录哪些证据字段(时间戳、目标、请求摘要、响应摘要、变更清单、回滚确认)
|
||||
|
||||
4) Stop & Rollback Criteria(停止与回滚标准)
|
||||
- 触发阈值/不可控情况(用描述性语言即可)
|
||||
- 触发阈值/不可控情况(用描述性语言即可)
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -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。证明须含足够证据(请求响应、截图、命令输出等)。
|
||||
|
||||
## 执行器对用户输出(重要)
|
||||
|
||||
|
||||
@@ -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
@@ -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 编排会话。
|
||||
|
||||
@@ -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 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -51,4 +51,8 @@ max_iterations: 0
|
||||
- 列出需要清理/验证的痕迹类型(配置、会话、日志、服务变更等层级描述即可)
|
||||
|
||||
4) Recommended Next Steps(下一步建议)
|
||||
- 建议由哪个阶段子代理接手,以及需要哪些证据输入。
|
||||
- 建议由哪个阶段子代理接手,以及需要哪些证据输入。
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -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 要点),供协调者**立即**写入。
|
||||
|
||||
输出后直接结束。
|
||||
|
||||
@@ -34,3 +34,7 @@ max_iterations: 0
|
||||
|
||||
- 若 **`description` / 用户消息 / 上文交接包** 中已给出资产列表、枚举结论或明确写「跳过全量枚举 / 仅做增量 / 从端口扫描或验证开始」,则**不得**为走完整流程而重新执行等价的广域子域爆破或相同参数集的枚举;仅在交接包声明的**缺口**上补充侦察。
|
||||
- 若子目标实为**漏洞验证、协议利用、权限提升**等而非攻击面扩展,应**极短说明**「当前角色为侦察;建议协调者改派专项代理」并仅提供与侦察相关的最小补充信息,避免擅自把任务扩写成新一轮全盘资产收集。
|
||||
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
@@ -55,4 +55,8 @@ max_iterations: 0
|
||||
5) Appendix(附录)
|
||||
- 术语、假设、证据清单索引(按证据类型列出即可)
|
||||
|
||||
输出后直接结束。
|
||||
## 边渗透边记录
|
||||
|
||||
- **边渗透边记录(强制节奏)**:勿等会话结束或收尾再批量写入。每**确认**一条新认知(开放端口/服务版本、入口路径、认证态或凭据特征、可利用点或攻击面变化)后,**立即**调用 `upsert_project_fact`(同 fact_key 覆盖更新)。每**验证**出一条可复现漏洞(含 POC/影响)后,**立即**调用 `record_vulnerability`;与事实可各记一次。继续下一步工作前优先落库,避免上下文压缩后细节丢失。未绑项目时说明无法写黑板,仍在本轮保留证据摘要。若工具集中无上述工具,须在交付物末尾给出「待落库」结构化条目(fact_key 建议、summary、body/POC 要点),供协调者**立即**写入。
|
||||
|
||||
输出后直接结束。
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 写在 body,Agent 通过 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>
|
||||
|
||||
Reference in New Issue
Block a user