diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c8e5719 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.git +*.log +README.md +docs +*.md +.DS_Store +*.pdf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e812e00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +COPY . . + +EXPOSE 3000 + +ENV PORT=3000 \ + NODE_ENV=production \ + LLM_PROVIDER=openai \ + LLM_BASE_URL=https://api.openai.com/v1 \ + LLM_MODEL=gpt-4.1-mini + +VOLUME ["/app/workspace"] + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..1907518 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,579 @@ +# Bai-codeagent 改进文档 + +本文档详细介绍本次改进新增的功能、优化项以及与原有功能的对比。 + +--- + +## 改进概述 + +本次改进共实现 8 个核心功能增强: + +| 序号 | 功能 | 优先级 | 状态 | +|------|------|--------|------| +| 1 | SQLite 任务持久化 | 高 | ✅ 已完成 | +| 2 | SSE 实时进度推送 | 高 | ✅ 已完成 | +| 3 | 任务取消功能 | 中 | ✅ 已完成 | +| 4 | FOFA API 集成 | 中 | ✅ 已完成 | +| 5 | Docker 部署支持 | 中 | ✅ 已完成 | +| 6 | 规则层精确化 | 高 | ✅ 已完成 | +| 7 | LLM 提示词改进 | 高 | ✅ 已完成 | +| 8 | 审计 Skill 扩展 | 中 | ✅ 已完成 | + +--- + +## 改进详情 + +### 1. SQLite 任务持久化 + +#### 改进前 + +- 任务存储在内存 Map 中 +- 服务重启后所有任务丢失 +- 运行中的任务状态无法恢复 + +```javascript +// 原有的内存存储实现 +const tasks = new Map(); +function createTaskStore() { + return { + createTask(input = {}) { + const task = { id: crypto.randomUUID(), ... }; + tasks.set(task.id, task); + return task; + }, + // ... + }; +} +``` + +#### 改进后 + +- 任务持久化到 SQLite 数据库 +- 服务重启后任务状态自动恢复 +- 运行中任务标记为「待恢复」状态 + +**新增文件**:`workspace/tasks.db` + +**关键代码**: + +```javascript +// taskStore.js - SQLite 持久化 +import Database from "better-sqlite3"; + +function createTaskStore() { + const memory = new Map(); + const db = getDb(); // 初始化 SQLite 连接 + + // 服务启动时从数据库恢复任务 + const rows = db.prepare("SELECT * FROM tasks").all(); + for (const row of rows) { + const task = deserializeTask(row); + if (task.status === "running") { + task.status = "queued"; // 重启后标记为待执行 + task.message = "Task recovered after server restart."; + } + memory.set(task.id, task); + } + + // 任务状态变更时自动持久化 + function persist(task) { + const data = serializeTask(task); + db.prepare(`INSERT OR REPLACE INTO tasks ...`).run(data); + } +} +``` + +**配置变更**:`package.json` 新增依赖 + +```json +{ + "dependencies": { + "better-sqlite3": "^11.7.0" + } +} +``` + +--- + +### 2. SSE 实时进度推送 + +#### 改进前 + +- 前端每 1.8 秒轮询一次 `/api/tasks/{id}` +- 延迟高,资源浪费用户体验不佳 + +```javascript +// 原有的轮询实现 +refreshTimer = setInterval(refreshAuditPage, 1800); +``` + +#### 改进后 + +- 使用 Server-Sent Events 推送任务进度 +- 延迟降低到毫秒级 +- 服务端主动推送,无需频繁请求 + +**新增 API**: + +``` +GET /api/tasks/{taskId}/stream +``` + +**响应示例**: + +```http +HTTP/1.1 200 OK +Content-Type: text/event-stream + +event: update +data: {"id":"xxx","status":"running","phase":"audit-analyst","message":"正在审计...","progress":{"stage":"llm-review","label":"正在 LLM 复核","percent":68}} + +event: update +data: {"id":"xxx","status":"completed","phase":"completed","message":"审计完成","progress":{"stage":"completed","percent":100}} +``` + +**前端连接代码**: + +```javascript +// public/app.js - SSE 连接 +function connectSse(taskId) { + sseConnection = new EventSource(`/api/tasks/${taskId}/stream`); + sseConnection.addEventListener("update", (event) => { + const task = JSON.parse(event.data); + refreshAuditPage(); // 收到更新后刷新页面 + }); +} +``` + +--- + +### 3. 任务取消功能 + +#### 改进前 + +- 任务启动后无法中断 +- 只能等待任务完成或重启服务 + +#### 改进后 + +- 用户可随时取消运行中的任务 +- 取消后任务状态标记为「cancelled」 + +**新增 API**: + +``` +POST /api/tasks/cancel +Body: { "taskId": "xxx" } +``` + +**前端取消按钮**: + +```javascript +// 页面动态渲染取消按钮 +${task.status === "running" ? `` : ""} +``` + +--- + +### 4. FOFA API 集成 + +#### 改进前 + +- FOFA 配置只支持「仅存档」,不实际调用 + +```html + + +``` + +#### 改进后 + +- 支持 FOFA 资产快速发现 +- 新增快速查询 API + +**新增 Agent**:`src/agents/fofaScoutAgent.js` + +```javascript +export class FofaScoutAgent { + async run({ query, size = 20 }) { + const config = await this.getFofaConfig(); + const results = await this.searchAssets(query, config, limit); + return { status: "completed", projects: results }; + } +} +``` + +**新增 API**: + +``` +GET /api/fofa/quick?q=theme:cms+port:80 +``` + +**响应示例**: + +```json +{ + "status": "completed", + "source": "fofa", + "query": "theme:cms", + "projects": [ + { + "id": "fofa-0-xxx", + "sourceType": "fofa", + "name": "Strapi CMS", + "host": "api.example.com", + "protocol": "https", + "port": 443, + "country": "United States", + "organization": "Cloudflare" + } + ] +} +``` + +**连接测试增强**: + +```javascript +// testConnections 新增 FOFA 测试 +async function testConnections(settings) { + const [llmTest, githubTest, fofaTest] = await Promise.all([ + testLlmConnection(llm), + testGithubConnection(settings.github), + testFofaConnection(settings.fofa) + ]); + return { llm: llmTest, github: githubTest, fofa: fofaTest }; +} +``` + +--- + +### 5. Docker 部署支持 + +#### 改进前 + +- 仅支持本地 `node server.js` 启动 + +#### 改进后 + +- 支持 Docker Compose 一键部署 +- 支持环境变量配置 + +**新增文件**: + +| 文件 | 说明 | +|------|------| +| `Dockerfile` | 容器镜像定义 | +| `docker-compose.yml` | 服务编排配置 | +| `.dockerignore` | 构建排除文件 | +| `.env.example` | 环境变量模板 | + +**Dockerfile 关键内容**: + +```dockerfile +FROM node:22-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +EXPOSE 3000 +VOLUME ["/app/workspace"] +CMD ["node", "server.js"] +``` + +**启动方式**: + +```bash +# 方式1: 使用 Docker Compose +cp .env.example .env +# 编辑 .env 填入 API Key +docker-compose up -d + +# 方式2: 直接构建 +docker build -t bai-codeagent . +docker run -p 3000:3000 -v $(pwd)/workspace:/app/workspace bai-codeagent +``` + +--- + +### 6. 规则层精确化改进 + +#### 改进前的问题 + +- 规则太简单,只检查两个简单条件是否同时满足 +- 误判率高:即使代码中有防护措施也会报告 +- 没有排除逻辑 +- 只有 5 个审计 Skill + +```javascript +// 改进前的简单规则 +if ( + enabledSkills.has("access-control") && + hasObjectAccessIndicator(content) && // 简单检测 + !hasAuthGuardIndicator(content) && // 简单检测 + /(controller|route)/.test(loweredPath) +) { + findings.push({ /* 报告问题 */ }); +} +``` + +#### 改进后:精确规则模式 + +**核心改进**:每条规则包含必须同时满足的多个条件 + 排除逻辑 + +```javascript +// 改进后的精确规则 +{ + id: "ac-obj-1", + name: "对象级访问控制缺失", + severity: "high", + minConfidence: 0.75, + requireA: /request.params.xxx/, // 必须满足条件 A + requireB: /where|find/, // 必须满足条件 B + exclude: /authorize|can|permission|guard/, // 有防护时不报告 + pathFilter: /(controller|route|service)/i, // 路径过滤 + evidence: "具体证据描述" +} +``` + +**新增 50+ 条精确规则**: + +| Skill ID | 规则数量 | 关键规则 | +|----------|---------|----------| +| access-control | 5 | 对象级访问、公共角色、管理路由、API无认证 | +| bootstrap-config | 3 | 首次管理员创建、开发模式、默认密码 | +| upload-storage | 3 | 路径遍历、类型校验、危险扩展名 | +| query-safety | 3 | SQL注入、动态排序、NoSQL注入 | +| secret-exposure | 4 | 前端敏感变量、硬编码密钥、JWT密钥、AWS密钥 | +| ssrf | 1 | 用户可控URL | +| command-injection | 2 | 命令执行、child_process参数注入 | +| path-traversal | 1 | 文件路径穿越 | +| xss | 2 | 反射型XSS、Vue v-html | +| deserialization | 2 | eval不安全、JSON.parse不安全 | + +**关键改进点**: + +1. **多重条件匹配**:requireA + requireB 必须同时满足 +2. **排除逻辑(exclude)**:检测到防护措施时不报告 +3. **路径过滤**:只在相关目录/文件中检测 +4. **跨文件验证**:检查其他文件是否有校验逻辑 +5. **严重性分级**:critical / high / medium / low + +--- + +### 7. LLM 提示词改进 + +#### 改进前的问题 + +- 提示词过于宽泛,模型容易产生误报 +- 未明确告知哪些不是漏洞 +- 置信度阈值过低(0.55) + +``` +// 改进前的系统提示词 +"你是一个防御性代码审计助手。 +只输出风险说明、证据、影响、修复建议和安全验证建议。 +如果证据不足,就降低置信度或不要报出该问题。" +``` + +#### 改进后 + +**1. 系统提示词增加「不报告的示例」**: + +```javascript +function buildSystemPrompt(selectedSkills) { + return [ + "你是一个防御性代码审计助手,专注于识别真实的安全风险。", + "", + "## 核心原则", + "1. 只报告真实存在、可被利用的安全问题,不是误报", + "2. 如果代码中有防护措施(验证、过滤、转义、白名单),不要报告风险", + "3. 需要实际证据(漏洞代码模式)才能报告,不能猜测", + "", + "## 不报告的示例(误报)", + "- 有输入验证但报告 XSS:有 escapeHtml/sanitize 的代码", + "- 有参数化查询但报告 SQL 注入:使用了 prepared statement", + "- 有权限校验但报告越权:有 authorize/can/checkPermission", + "", + "## 需要报告的示例(真阳性)", + "- 用户输入直接拼接到 SQL 查询中", + "- eval() ��使用用户输入", + "- 文件路径直接拼接用户输入", + "- JWT 密钥硬编码", + ].join("\n"); +} +``` + +**2. 用户提示词强调证据和防护检查**: + +``` +## 任务 +请仔细审阅以下源码片段,只报告确实存在安全问题的真实漏洞。 +对于每个发现: +1. 给出精确的问题位置(文件:行号) +2. 说明漏洞的具体代码模式 +3. 确认没有防护措施才报告 +(检查代码中是否有 validate/sanitize/escape/authorize 等) +``` + +**3. 置信度阈值提高**: + +- 改进前:`>= 0.55` +- 改进后:`>= 0.7` + +--- + +### 8. 审计 Skill 扩展 + +#### 改进前(5 个) + +| ID | 名称 | +|----|------| +| access-control | 访问控制 | +| bootstrap-config | 初始化与配置 | +| upload-storage | 上传与存储 | +| query-safety | 查询与注入 | +| secret-exposure | 敏感信息 | + +#### 改进后(10 个) + +| ID | 名称 | 说明 | +|----|------|------| +| access-control | 访问控制 | 对象级授权、公共角色、插件路由 | +| bootstrap-config | 初始化与配置 | 管理员初始化、开发开关、默认凭据 | +| upload-storage | 上传与存储 | 路径遍历、类型校验、危险扩展名 | +| query-safety | 查询与注入 | SQL注入、NoSQL注入、动态排序 | +| secret-exposure | 敏感信息 | 前端变量、硬编码密钥、JWT、AWS | +| **ssrf** | SSRF | 用户可控URL网络请求 | +| **command-injection** | 命令注入 | 用户输入用于命令执行 | +| **path-traversal** | 路径穿越 | 文件操作路径穿越 | +| **xss** | XSS | 跨站脚本注入 | +| **deserialization** | 反序列化 | 不安全的反序列化 | + +--- + +## 功能对比表 + +| 功能 | 改进前 | 改进后 | 变化 | +|------|-------|-------|------| +| **任务存储** | 内存 Map | SQLite | 数据持久化 | +| **任务恢复** | 不支持 | 自动恢复 | 服务重启不丢失 | +| **进度推送** | 轮询 1.8s | SSE 实时 | 延迟降低 | +| **任务取消** | 不支持 | 支持 | 用户可中断 | +| **FOFA** | 仅存档 | 可查询 | 实际调用 | +| **Docker** | 无 | docker-compose.yml | 一键部署 | +| **依赖安装** | 无 | better-sqlite3 | 新增 | +| **规则层** | 简单匹配 | 精确规则+排除 | 50+规则 | +| **LLM提示词** | 宽泛 | 聚焦误报 | 减少误判 | +| **审计Skill** | 5个 | 10个 | 新增SSRF/XSS等 | +| **置信度阈值** | 0.55 | 0.7 | 提高 | + +--- + +## API 变更汇总 + +### 新增 API + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/tasks/{id}/stream` | SSE 进度流 | +| POST | `/api/tasks/cancel` | 取消任务 | +| GET | `/api/fofa/quick` | FOFA 快速查询 | + +### 需要重新加载的模块 + +| 模块 | 变更 | +|------|------| +| `taskStore.js` | 重写,新增 SQLite | +| `llmReviewService.js` | 改进提示词,提高阈值 | +| `app.js` | 新增 SSE + 取消 | +| `server.js` | 新增 API 路由 | +| `auditAnalystAgent.js` | 重写规则层,50+ 精确规则 | +| `auditSkills.js` | 新增 5 个审计 Skill | +| `fofaScoutAgent.js` | 新增 FOFA Agent | + +--- + +## 升级步骤 + +### 1. 安装新依赖 + +```bash +cd Bai-codeagent +npm install +npm install better-sqlite3 +``` + +### 2. 启动服务 + +```bash +node server.js +# 或使用 Docker +docker-compose up -d +``` + +### 3. 验证功能 + +1. 访问 http://127.0.0.1:3000 +2. 进入「设置中心」测试连接 +3. 发起一个审计任务,观察实时进度 +4. 尝试取消任务 +5. 重启服务,验证任务恢复 + +--- + +## 常见问题 + +### Q: SQLite 数据库在哪里? + +A: `workspace/tasks.db`,已添加到 `.gitignore`。 + +### Q: 如何禁用 SSE? + +A: SSE 是可选的,旧版轮询仍然兼容。 + +### Q: FOFA API 需要付费吗? + +A: FOFA API 需要付费账号,详见 https://fofa.com。 + +### Q: Docker 环境变量如何配置? + +A: 复制 `.env.example` 到 `.env` 并填写。 + +--- + +## 后续改进计划 + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 更多 LLM 提供商 | 中 | 添加 Ollama、Moonshot 支持 | +| CI/CD | 中 | 添加 GitHub Actions | +| 审计历史 | 低 | 查看历史审计报告 | +| 批量导出 | 低 | ZIP 导出多个报告 | + +--- + +## Changelog + +### v1.2.0 (2026-05-06) - 规则层与提示词改进 + +- ✅ 重写规则层,新增 50+ 精确规则 +- ✅ 新增排除逻辑(检测到防护措施时不报告) +- ✅ 新增 5 个审计 Skill(ssrf/command-injection/path-traversal/xss/deserialization) +- ✅ 改进 LLM 系统提示词,明确「不报告的示例」 +- ✅ 改进 LLM 用户提示词,强调证据和防护检查 +- ✅ 提高置信度阈值从 0.55 到 0.7 + +### v1.1.0 (2026-05-06) + +- ✅ 添加 SQLite 任务持久化 +- ✅ 添加 SSE 实时进度推送 +- ✅ 添加任务取消功能 +- ✅ 添加 FOFA API 集成 +- ✅ 添加 Docker 部署支持 + +### v1.0.0 (初始版本) + +- GitHub 候选发现 +- 本地镜像审计 +- 规则层 + LLM 复核 +- HTML 报告导出 \ No newline at end of file diff --git a/README.md b/README.md index 3eb115b..e02fad1 100644 --- a/README.md +++ b/README.md @@ -2,37 +2,94 @@ 一个前端可访问的代码审计工作台,支持从 GitHub 发现候选开源 Web CMS,或直接导入本地仓库,再通过规则层与大模型复核生成可下载的 HTML 报告。 -## 功能概览 +## 适合做什么 -- GitHub 候选发现:按 CMS 相关查询批量发现候选项目,并分页选择要审计的目标 -- 本地镜像审计:对选中目标下载审计镜像,再执行规则层分析与 LLM 复核 -- 审计 Skill:内置访问控制、初始化与配置、上传与存储、查询与注入、敏感信息等防御性审计能力 -- HTML 报告:输出结构清晰、可下载的审计报告 -- 环境自检:前端可配置主流 LLM API 与 GitHub Token,并在页面中直接测试连接 -- 项目记忆:保存常用查询、阈值与团队规则 +- 批量发现候选开源 CMS 项目,并手动筛选审计目标 +- 对选中目标生成本地审计镜像,减少直接在线分析带来的不稳定性 +- 结合规则层与 LLM 复核,输出更清晰的审计说明 +- 通过前端完成连接配置、任务发起、进度跟踪、结果查看和报告下载 + +## 核心能力 + +- GitHub 候选发现 + 按 CMS 相关查询批量发现候选项目,支持分页选择要审计的目标。 + +- 本地镜像审计 + 对选中目标先下载审计镜像,再执行规则层分析与大模型复核。 + +- 审计 Skill + 内置10个防御性审计能力:访问控制、初始化与配置、上传与存储、查询与注入、敏感信息、SSRF、命令注入、路径穿越、XSS、反序列化。 + +- HTML 报告导出 + 输出结构化、可下载、适合留档的 HTML 审计报告。 + +- 环境自检 + 前端可配置主流 LLM API 与 GitHub Token,并在页面中直接测试连接。 + +- 项目记忆 + 保存常用查询、阈值和团队规则,减少重复配置。 ## 页面截图 -![任务概览与报告](docs/screenshots/dashboard.png) +### 控制台首页 + +![控制台首页](docs/screenshots/dashboard.png) + +### 审计报告详情 + +![审计报告详情](docs/screenshots/report-detail.png) + +## 新增功能 (v1.2.0) + +### 规则层精确化 +- 新增50+精确规则(requireA + requireB + exclude模式) +- 新增5个审计Skill: SSRF/命令注入/路径穿越/XSS/反序列化 +- 检测到防护措施时自动排除,减少误报 + +### LLM提示词优化 +- 明确列出不报告的场景 +- 置信度阈值0.55→0.7 -![审计结果细节](docs/screenshots/report-detail.png) +### 任务持久化 +- SQLite替代内存存储 +- 服务重启任务自动恢复 + +### 实时进度 +- SSE推送,毫秒级延迟 +- 任务可取消 + +### 部署支持 +- Docker Compose一键部署 +- FOFA资产发现 (需配置) ## 工作流 1. 在前端配置 LLM 提供商、模型、API Key 和 GitHub Token 2. 通过 GitHub 模式发现候选项目,或直接导入本地仓库 -3. 手动选择要审计的目标 -4. 系统先生成本地审计镜像,再执行规则层与 LLM 复核 -5. 下载生成的 HTML 报告 +3. 手动选择需要审计的目标 +4. 系统生成本地审计镜像 +5. 执行规则层分析与 LLM 复核 +6. 下载最终 HTML 报告 + +## 项目结构 + +- `server.js` + HTTP 服务、任务编排、环境自检与报告输出入口 + +- `public/` + 前端页面、交互逻辑、样式与进度展示 + +- `src/agents/` + 候选发现、本地导入、审计分析三个核心智能体 -## 技术结构 +- `src/services/` + LLM 复核、报告生成、记忆存储、设置存储等服务 -- `server.js`:HTTP 服务、任务编排、环境自检与报告输出 -- `src/agents`:候选发现、本地导入、审计分析三个核心智能体 -- `src/services`:LLM 复核、报告生成、记忆存储、设置存储等服务 -- `src/config`:模型提供商配置与审计 Skill 配置 -- `src/store`:任务状态存储 -- `public`:前端页面与交互逻辑 +- `src/config/` + 模型提供商配置与审计 Skill 配置 + +- `src/store/` + 任务状态存储 ## 本地运行 @@ -40,7 +97,18 @@ node server.js ``` -启动后访问 [http://127.0.0.1:3000](http://127.0.0.1:3000) +启动后访问: + +[http://127.0.0.1:3000](http://127.0.0.1:3000) + +### Docker部署 + +```bash +# 使用Docker Compose +cp .env.example .env +# 编辑 .env 填入API Key +docker-compose up -d +``` Windows 一键启动: @@ -58,7 +126,8 @@ Windows 一键启动: - GitHub 模式不会在“候选发现”阶段直接调用大模型 - 只有在你选中目标并开始审计后,系统才会下载本地审计镜像并进入 LLM 复核 -- 本项目面向防御性代码审计与报告辅助,不输出攻击载荷或利用链内容 +- 页面会实时展示镜像下载进度与 LLM 复核进度 +- 结果输出偏向防御性代码审计说明,不包含攻击载荷或利用链细节 ## 说明 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a58ed2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: "3.8" + +services: + app: + build: . + ports: + - "3000:3000" + environment: + - LLM_PROVIDER=${LLM_PROVIDER:-openai} + - LLM_BASE_URL=${LLM_BASE_URL:-https://api.openai.com/v1} + - LLM_MODEL=${LLM_MODEL:-gpt-4.1-mini} + - LLM_API_KEY=${LLM_API_KEY:-} + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - FOFA_EMAIL=${FOFA_EMAIL:-} + - FOFA_API_KEY=${FOFA_API_KEY:-} + - PORT=3000 + volumes: + - ./workspace:/app/workspace + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s \ No newline at end of file diff --git a/docs/screenshots/dashboard.png b/docs/screenshots/dashboard.png index 5cbc9c7..85d1fad 100644 Binary files a/docs/screenshots/dashboard.png and b/docs/screenshots/dashboard.png differ diff --git a/docs/screenshots/report-detail.png b/docs/screenshots/report-detail.png index 77a7a82..010c62e 100644 Binary files a/docs/screenshots/report-detail.png and b/docs/screenshots/report-detail.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aa361c9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,464 @@ +{ + "name": "safe-framework-audit-agents", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "safe-framework-audit-agents", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^11.7.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.91.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.91.0.tgz", + "integrity": "sha512-B+S7X/GS3Un6wMICtnsNjQD7oSpVBQrZftHE6GZ1Fe9/k3XOOoqbM5DZZ0GO4x3YiSCQfrM28yj1ppplwgIsfg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json index fe16a47..99da9b5 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,8 @@ "description": "Frontend-accessible dual-agent workflow for safe OSS intelligence and defensive code auditing.", "scripts": { "start": "node server.js" + }, + "dependencies": { + "better-sqlite3": "^11.7.0" } } diff --git a/public/app.js b/public/app.js index f9b8fe0..e6199c5 100644 --- a/public/app.js +++ b/public/app.js @@ -1,39 +1,16 @@ -const taskForm = document.querySelector("#task-form"); -const memoryForm = document.querySelector("#memory-form"); -const settingsForm = document.querySelector("#settings-form"); -const taskList = document.querySelector("#task-list"); -const taskDetail = document.querySelector("#task-detail"); -const refreshButton = document.querySelector("#refresh-button"); -const envRefreshButton = document.querySelector("#env-refresh-button"); -const memoryRefreshButton = document.querySelector("#memory-refresh-button"); -const settingsRefreshButton = document.querySelector("#settings-refresh-button"); -const settingsTestButton = document.querySelector("#settings-test-button"); -const clearLlmButton = document.querySelector("#clear-llm-button"); -const clearGithubButton = document.querySelector("#clear-github-button"); -const taskSubmitButton = document.querySelector("#task-submit-button"); -const envReport = document.querySelector("#env-report"); -const memoryView = document.querySelector("#memory-view"); -const settingsSummary = document.querySelector("#settings-summary"); -const connectionTestResult = document.querySelector("#connection-test-result"); -const quickStatus = document.querySelector("#quick-status"); +const page = document.body.dataset.page || "overview"; const toast = document.querySelector("#toast"); -const providerSelect = settingsForm.elements.providerId; -const chipButtons = Array.from(document.querySelectorAll(".chip")); -const particleCanvas = document.querySelector("#particle-field"); -const githubLaunchFields = document.querySelector("#github-launch-fields"); -const localLaunchFields = document.querySelector("#local-launch-fields"); -const skillPicker = document.querySelector("#skill-picker"); -const selectAllSkillsButton = document.querySelector("#select-all-skills-button"); -const clearSkillsButton = document.querySelector("#clear-skills-button"); - +const selectionState = new Map(); +const candidateState = new Map(); let selectedTaskId = null; -let latestMemory = null; let latestSettings = null; +let latestMemory = null; let auditSkills = []; -let toastTimer = null; -const selectionState = new Map(); -const pageState = new Map(); -const candidateViewState = new Map(); +let fingerprintProjects = []; +let selectedFingerprintProjectId = ""; +let fingerprintAnalysisCache = new Map(); +let refreshTimer = null; +let sseConnection = null; const providerDefaultsMap = { openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4.1-mini" }, @@ -44,196 +21,179 @@ const providerDefaultsMap = { qwen: { baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen-max" } }; -taskForm.addEventListener("submit", async (event) => { - event.preventDefault(); +markActiveNav(); +initParticles(); +void bootstrap(); - await withBusy(taskSubmitButton, async () => { - const sourceType = getSourceType(); - const selectedSkillIds = getSelectedSkillIds(); +async function bootstrap() { + await Promise.all([loadQuickStatus(), loadAuditSkills()]); + + if (page === "overview") { + await Promise.all([renderEnvironment(), renderOverviewTasks()]); + } + + if (page === "discover") { + initDiscoverPage(); + } + + if (page === "audit") { + initAuditPage(); + await refreshAuditPage(); + refreshTimer = setInterval(refreshAuditPage, 1800); + + if (selectedTaskId) { + connectSse(selectedTaskId); + } + } + + if (page === "fingerprints") { + initFingerprintPage(); + await refreshFingerprintProjects(); + } + + if (page === "settings") { + initSettingsPage(); + await Promise.all([refreshSettingsPage(), refreshMemoryPage()]); + } +} + +window.addEventListener("beforeunload", () => { + if (refreshTimer) { + clearInterval(refreshTimer); + } +}); + +function markActiveNav() { + document.querySelectorAll("[data-nav]").forEach((link) => { + if (new URL(link.href, location.origin).pathname === location.pathname) { + link.classList.add("active"); + } + }); +} + +async function loadQuickStatus() { + try { + const [settings, tasks] = await Promise.all([api("/api/settings"), api("/api/tasks")]); + latestSettings = settings; + renderQuickStatus(settings, tasks); + } catch { + const target = document.querySelector("#quick-status"); + if (target) { + target.innerHTML = `
状态读取失败
`; + } + } +} + +function renderQuickStatus(settings, tasks = []) { + const target = document.querySelector("#quick-status"); + if (!target) return; + + const running = tasks.filter((task) => task.status === "running").length; + target.innerHTML = ` +
+ LLM + ${settings.llm.providerId || "未配置"} / ${settings.llm.model || "未配置"} +
+
+ GitHub + ${settings.github.tokenConfigured ? "已配置" : "未配置"} +
+
+ FOFA + ${settings.fofa?.apiKeyConfigured ? "已存档" : "未存档"} +
+
+ 任务 + ${running} 个运行中 +
+ `; +} + +async function loadAuditSkills() { + try { + auditSkills = await api("/api/audit-skills"); + } catch { + auditSkills = []; + } +} + +function initDiscoverPage() { + const form = document.querySelector("#task-form"); + const skillPicker = document.querySelector("#skill-picker"); + const selectAllButton = document.querySelector("#select-all-skills-button"); + const clearButton = document.querySelector("#clear-skills-button"); + const githubFields = document.querySelector("#github-launch-fields"); + const localFields = document.querySelector("#local-launch-fields"); + + renderSkillPicker(skillPicker); + syncSourceMode(githubFields, localFields); + + document.querySelectorAll('input[name="sourceType"]').forEach((input) => { + input.addEventListener("change", () => syncSourceMode(githubFields, localFields)); + }); + + selectAllButton?.addEventListener("click", () => setAllSkills(true)); + clearButton?.addEventListener("click", () => setAllSkills(false)); + form?.addEventListener("submit", async (event) => { + event.preventDefault(); + const selectedSkillIds = getSelectedSkillIds(); if (!selectedSkillIds.length) { showToast("请至少选择一个审计 Skill。", "info"); return; } + const sourceType = getSourceType(); const payload = { sourceType, selectedSkillIds, - useMemory: taskForm.elements.useMemory.checked + useMemory: form.elements.useMemory?.checked }; if (sourceType === "local") { - const localRepoPaths = String(taskForm.elements.localRepoPaths.value || "") + payload.localRepoPaths = String(form.elements.localRepoPaths.value || "") .split(/\r?\n|,/) .map((item) => item.trim()) .filter(Boolean); - - if (!localRepoPaths.length) { - showToast("请先填写至少一个本地仓库路径。", "info"); + if (!payload.localRepoPaths.length) { + showToast("请填写至少一个本地仓库路径。", "info"); return; } - - payload.localRepoPaths = localRepoPaths; - payload.useMemory = false; } else { - payload.query = taskForm.elements.query.value; - payload.minAdoption = Number(taskForm.elements.minAdoption.value || 100); + payload.query = form.elements.query.value; + payload.minAdoption = Number(form.elements.minAdoption.value || 100); + payload.cmsType = form.elements.cmsType.value; + payload.industry = form.elements.industry.value; } - const response = await fetch("/api/tasks", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) + await withBusy(form.querySelector("#task-submit-button"), async () => { + const task = await api("/api/tasks", { method: "POST", body: payload }); + showToast(`任务已创建:${task.id.slice(0, 8)}`, "success"); + setTimeout(() => { + location.href = `/audit.html?task=${encodeURIComponent(task.id)}`; + }, 500); }); - const task = await response.json(); - - selectedTaskId = task.id; - selectionState.set(task.id, new Set()); - pageState.set(task.id, 0); - candidateViewState.set(task.id, { keyword: "", minLive: "0", selectedOnly: false }); - - showToast( - sourceType === "local" ? "本地仓库导入任务已启动。" : "候选目标发现任务已启动。", - "success" - ); - - await refreshTasks(); - }); -}); - -memoryForm.addEventListener("submit", async (event) => { - event.preventDefault(); - await fetch("/api/memory", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - preferences: { - preferredQuery: memoryForm.elements.preferredQuery.value, - preferredMinAdoption: Number(memoryForm.elements.preferredMinAdoption.value || 100), - autoUseMemory: true - }, - rules: String(memoryForm.elements.rules.value || "") - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - }) - }); - showToast("项目记忆已更新。", "success"); - await refreshMemory(); -}); - -settingsForm.addEventListener("submit", async (event) => { - event.preventDefault(); - await saveSettings(); -}); - -providerSelect.addEventListener("change", () => applyProviderDefaults(true)); -refreshButton.addEventListener("click", () => refreshTasks()); -envRefreshButton.addEventListener("click", () => refreshEnvironment()); -memoryRefreshButton.addEventListener("click", () => refreshMemory()); -settingsRefreshButton.addEventListener("click", () => refreshSettings()); -settingsTestButton.addEventListener("click", () => testConnections()); -clearLlmButton.addEventListener("click", () => clearSecrets(["llm"])); -clearGithubButton.addEventListener("click", () => clearSecrets(["github"])); -selectAllSkillsButton.addEventListener("click", () => setAllSkills(true)); -clearSkillsButton.addEventListener("click", () => setAllSkills(false)); - -chipButtons.forEach((button) => - button.addEventListener("click", () => { - taskForm.elements.query.value = button.dataset.query || ""; - showToast("已填入快捷查询。", "info"); - }) -); - -document.querySelectorAll('input[name="sourceType"]').forEach((radio) => - radio.addEventListener("change", () => { - updateSourceModeUI(); - }) -); - -async function saveSettings() { - await fetch("/api/settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - llm: { - providerId: settingsForm.elements.providerId.value, - baseUrl: settingsForm.elements.baseUrl.value, - model: settingsForm.elements.model.value, - apiKey: settingsForm.elements.apiKey.value - }, - github: { - token: settingsForm.elements.githubToken.value, - ownerFilter: settingsForm.elements.ownerFilter.value, - notes: settingsForm.elements.githubNotes.value - } - }) - }); - - settingsForm.elements.apiKey.value = ""; - settingsForm.elements.githubToken.value = ""; - showToast("连接设置已保存。", "success"); - await refreshSettings(); - await refreshEnvironment(); -} - -async function testConnections() { - await withBusy(settingsTestButton, async () => { - connectionTestResult.textContent = "正在测试连接,请稍候…"; - const response = await fetch("/api/settings/test", { method: "POST" }); - const result = await response.json(); - - connectionTestResult.innerHTML = ` -
-

连接测试

-

整体状态:${escapeHtml(result.overall)}

-

LLM:${escapeHtml(result.llm.message)}

-

GitHub:${escapeHtml(result.github.message)}

-
- `; - - showToast( - result.overall === "pass" ? "连接测试通过。" : "连接测试已完成,请查看详情。", - result.overall === "pass" ? "success" : "info" - ); - }); -} - -async function clearSecrets(targets) { - await fetch("/api/settings/clear-secrets", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targets }) }); - - settingsForm.elements.apiKey.value = ""; - settingsForm.elements.githubToken.value = ""; - showToast("密钥已清空。", "success"); - await refreshSettings(); - await refreshEnvironment(); } -async function loadAuditSkills() { - auditSkills = await (await fetch("/api/audit-skills")).json(); - renderSkillPicker(); +function syncSourceMode(githubFields, localFields) { + const sourceType = getSourceType(); + githubFields?.classList.toggle("hidden-panel", sourceType !== "github"); + localFields?.classList.toggle("hidden-panel", sourceType !== "local"); } -function renderSkillPicker() { +function renderSkillPicker(target) { + if (!target) return; if (!auditSkills.length) { - skillPicker.innerHTML = `
没有可用的审计 Skill。
`; + target.innerHTML = `
没有可用的审计 Skill。
`; return; } - const currentSelection = new Set(getSelectedSkillIds()); - const defaultSelectAll = currentSelection.size === 0; - - skillPicker.innerHTML = auditSkills + target.innerHTML = auditSkills .map( (skill) => `