From a5d26db5c0679832a00836c5b05c997517787a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:51:05 +0800 Subject: [PATCH] Add files via upload --- plugins/README.md | 28 +- .../cyberstrikeai-browser-extension/README.md | 72 ++ .../README.zh-CN.md | 231 +++++ .../background/service-worker.js | 143 +++ .../devtools.html | 9 + .../devtools.js | 69 ++ .../dist/cyberstrikeai-browser-extension.zip | Bin 0 -> 55551 bytes .../icons/icon128.png | Bin 0 -> 21109 bytes .../icons/icon16.png | Bin 0 -> 633 bytes .../icons/icon48.png | Bin 0 -> 3597 bytes .../lib/api.js | 153 +++ .../lib/capture.js | 103 ++ .../lib/catalog-cache.js | 39 + .../lib/constants.js | 17 + .../lib/formatter.js | 47 + .../lib/http-normalize.js | 104 ++ .../lib/markdown.js | 135 +++ .../lib/storage.js | 114 +++ .../manifest.json | 21 + .../package.sh | 9 + .../panel/panel.css | 867 ++++++++++++++++ .../panel/panel.html | 143 +++ .../panel/panel.js | 947 ++++++++++++++++++ .../popup/popup.css | 94 ++ .../popup/popup.html | 31 + .../popup/popup.js | 28 + 26 files changed, 3398 insertions(+), 6 deletions(-) create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/README.md create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/background/service-worker.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/devtools.html create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/devtools.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon128.png create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon16.png create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon48.png create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/api.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/capture.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/catalog-cache.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/formatter.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/http-normalize.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/markdown.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/package.sh create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.css create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.js create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.css create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.html create mode 100644 plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.js diff --git a/plugins/README.md b/plugins/README.md index a33e6f2a..c3896a05 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -1,12 +1,28 @@ ## Plugins -This directory contains optional plugins/extensions that integrate CyberStrikeAI with other tools. +Optional integrations that connect CyberStrikeAI with other tools. -- `burp-suite/`: Burp Suite extensions - -### Burp Suite Extension +### Burp Suite - **Path**: `plugins/burp-suite/cyberstrikeai-burp-extension/` -- **Build output**: `plugins/burp-suite/cyberstrikeai-burp-extension/dist/cyberstrikeai-burp-extension.jar` -- **Docs**: see the plugin folder `README.md` / `README.zh-CN.md` +- **Build**: `bash build-mvn.sh` → `dist/cyberstrikeai-burp-extension.jar` +- **Docs**: `README.md` / `README.zh-CN.md` +### Browser (Chrome / Edge) + +- **Path**: `plugins/browser-extension/cyberstrikeai-browser-extension/` +- **Version**: **0.3.5** +- **Install**: `chrome://extensions/` → Load unpacked → F12 → **CyberStrikeAI** tab +- **Package**: `bash package.sh` → `dist/cyberstrikeai-browser-extension.zip` +- **Docs**: `README.zh-CN.md` (full) / `README.md` (summary) + +#### Highlights (v0.3.x) + +| Feature | Description | +|---------|-------------| +| Capture pause | **● Capturing** / **○ Paused** — stop recording without closing DevTools | +| HTTP/1.1 display | Raw HAR in memory; UI + AI prompt normalized (no `:method` pseudo-headers) | +| Collapsible conn bar | Host/Port/Validate collapses after success | +| Popup | Read-only endpoint + connection status | +| Performance | XHR-only filter, no body read for static assets, rAF-throttled stream UI | +| Data caps | 200 captures/tab, 50 test runs, 512KB progress — all in-memory | diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/README.md b/plugins/browser-extension/cyberstrikeai-browser-extension/README.md new file mode 100644 index 00000000..33b93a0e --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/README.md @@ -0,0 +1,72 @@ +## CyberStrikeAI Browser Extension + +**Version 0.3.5** — Full docs: **README.zh-CN.md** + +Chromium DevTools extension: capture Network traffic and send it to CyberStrikeAI for AI-assisted security testing. Aligned with the Burp Suite plugin. + +### Quick install + +1. `chrome://extensions/` → Developer mode → **Load unpacked** +2. Select `plugins/browser-extension/cyberstrikeai-browser-extension/` +3. Open target page → **F12** → **CyberStrikeAI** tab → **Validate** +4. Select a captured request → **Send** → view **Output** + +### Popup vs DevTools panel + +| Location | Purpose | +|----------|---------| +| **DevTools panel** | Connection, Validate, capture, Send, Output (primary UI) | +| **Extension popup** | Read-only connection status + version + guide | + +### Key features + +- **Capture toggle**: **● Capturing** / **○ Paused** — pause stops `getContent` and list updates; Send still works on existing entries +- **Collapsible connection bar** — collapses to `https://host:port` after Validate +- **HTTP/1.1 normalization** — raw HAR stored; display and AI prompt strip HTTP/2 pseudo-headers (`:method`, etc.) +- **Test History** (50 runs) + **Captured Requests** (200/tab, XHR/Fetch filter) +- **SSE streaming** — Progress capped at 512KB; Final uncapped for active run +- **Deferred Markdown** — plain text while streaming; render after done; skip above 100KB +- **Stop** — abort local stream + server cancel via `conversationId` +- **Latest XHR**, **Copy**, project/role/agent send dialog +- Session token + optional host permissions + +### Data limits (no unbounded growth) + +| Data | Limit | Storage | +|------|-------|---------| +| Captures | 200 / tab | In-memory | +| Tabs tracked | 20 | In-memory | +| Test runs | 50 | Panel memory | +| Config / token | Small | `chrome.storage` | + +Closing DevTools clears panel data. Closing the browser invalidates the session token. + +### Performance + +- **DevTools closed** → zero impact on page load +- **Capture paused** → near-zero overhead +- **Capturing + XHR only** → light overhead on matching requests only + +### Troubleshooting + +After reloading the extension, close DevTools completely and reopen (F12) if you see `chrome.runtime.connect` errors — the old panel context is invalidated. + +### Package + +```bash +bash package.sh +# → dist/cyberstrikeai-browser-extension.zip +``` + +### Layout + +```text +manifest.json +background/service-worker.js +devtools.js +panel/ # main UI +popup/ # read-only status +lib/ # api, storage, capture, http-normalize, markdown, … +icons/ +package.sh +``` diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md b/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md new file mode 100644 index 00000000..be6a652b --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/README.zh-CN.md @@ -0,0 +1,231 @@ +## CyberStrikeAI 浏览器扩展 + +**当前版本:0.3.5** + +Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Network** 流量,发送到 CyberStrikeAI 进行 AI 辅助安全测试。能力与 Burp Suite 插件对齐,并按生产场景做了性能与体验优化。 + +--- + +### 快速开始 + +1. `chrome://extensions/` → 开发者模式 → **加载已解压的扩展程序** +2. 选择目录:`plugins/browser-extension/cyberstrikeai-browser-extension/` +3. 打开目标页面 → **F12** → 顶部 Tab **CyberStrikeAI** +4. 填写 Host / Port / Password → **Validate**(首次会请求访问服务器地址权限) +5. 左侧选中捕获请求 → **Send** → 在 **Output** 查看 AI 结果 + +点击浏览器工具栏图标可查看 **只读连接状态**;完整配置与操作均在 DevTools 面板内完成。 + +--- + +### 界面说明 + +``` +┌─ 连接栏(Validate 成功后可收起)────────────────────────────┐ +│ Logo │ https://host:port │ 连接设置 │ ● OK │ +├─ 操作栏 ────────────────────────────────────────────────────┤ +│ Send │ Latest XHR │ Stop │ Copy │ Clear │ ●捕获中/○已暂停 │ +│ XHR/Fetch only │ Debug │ Markdown │ +├──────────────┬──────────────────────────────────────────────┤ +│ Test History │ Output │ Request │ Response │ +│ Captured Req │ Progress + Final Response │ +└──────────────┴──────────────────────────────────────────────┘ +``` + +| 区域 | 说明 | +|------|------| +| **连接栏** | Host、Port、HTTPS、Password、Validate;成功后收起为 `https://host:port` 摘要 | +| **Test History** | 最多 50 次 Send 记录,可回看 Progress / Final | +| **Captured Requests** | 当前 Tab 捕获列表,最多 200 条,支持搜索 | +| **Output** | 默认 Tab:流式 Progress + Final Response | +| **Request / Response** | 查看选中流量的 HTTP/1.1 格式原文 | + +--- + +### 功能一览 + +#### 捕获 + +- **Background 中枢**:`devtools.js` 监听 Network → `service-worker` 队列 → Panel 订阅 +- 默认 **XHR/Fetch only**(可关闭以捕获更多类型) +- 静态资源 URL / MIME **预过滤**,命中前不读响应体 +- **● 捕获中 / ○ 已暂停**:暂停后零开销,已有列表仍可 Send +- 单条截断:请求体 **64KB**、响应 **4KB** + +#### HTTP 展示与 AI Prompt + +- **存储**:内存中保留原始 HAR(含 HTTP/2 伪首部 `:method` 等) +- **展示 / Prompt**:归一化为 **HTTP/1.1**(与 Burp 插件一致) + +```http +GET /api/foo HTTP/1.1 +Host: example.com +Cookie: ... +``` + +#### 发送到 CyberStrikeAI + +- 弹窗选择:**项目 / 角色 / 对话模式**(动态 API)+ 测试指令 +- 支持 **Eino Single**、**Deep**、**Plan-Execute**、**Supervisor** +- **Latest XHR**:一键选中最近 API 请求并打开发送弹窗 +- **Stop**:中止本地 SSE + 调用服务端 `/api/agent-loop/cancel` + +#### 流式输出 + +- Progress 日志上限 **512KB**(超出截断) +- **Final Response 不截断**(当前进行中的测试) +- 历史 run 切换后 Final 软截断 **100KB** +- **Markdown**:流式阶段纯文本;结束后 `requestIdleCallback` 渲染;超 **100KB** 降级纯文本 +- **Copy**:复制当前 Request / Response / Final + +#### 安全与权限 + +- Token 存 **chrome.storage.session**(关浏览器失效) +- **optional_host_permissions**:Validate 时按需授权 CyberStrikeAI 地址 +- Markdown iframe 使用 `sandbox` + +--- + +### 按钮与选项 + +| 控件 | 作用 | +|------|------| +| **Validate** | 登录并校验 Token;进行中再次点击为 Cancel | +| **连接设置 / 收起** | 展开或折叠 Host/Port/Password 表单 | +| **Send** | 对选中捕获发起到 CyberStrikeAI | +| **Latest XHR** | 选中最近 XHR/Fetch 并 Send | +| **Stop** | 停止当前 AI 流(本地 + 服务端) | +| **Clear Output** | 清空当前 run 的 Progress / Final | +| **● 捕获中 / ○ 已暂停** | 启用或暂停 Network 捕获 | +| **XHR/Fetch only** | 只捕获 API 类请求 | +| **Debug events** | 在 Progress 显示更多 SSE 事件 | +| **Markdown** | Final 完成后渲染富文本 | +| **Clear All** | 清空 Test History | +| **Clear** | 清空当前 Tab 捕获列表 | + +--- + +### 数据与内存(不会无限增长) + +| 数据 | 上限 | 位置 | 清理时机 | +|------|------|------|----------| +| 捕获请求 | 200 条 / Tab | Background + Panel 内存 | 超出丢弃最旧;可手动 Clear | +| Tab 捕获槽 | 20 个 Tab | Background 内存 | 超出丢弃非当前 Tab | +| 测试历史 | 50 条 | Panel 内存 | 超出丢弃最旧;Clear All | +| Progress | 512KB / run | Panel 内存 | 超出截断 | +| Final(进行中) | 无硬上限 | Panel 内存 | — | +| Final(历史) | 100KB 软截断 | Panel 内存 | 切换到其他 run 时 | +| 配置 + Token | 极小 | chrome.storage | 手动改配置 | + +- 关闭 **DevTools** → Panel 内存清空 +- 关闭 **浏览器** → Session Token 失效 +- Service Worker 被回收 → Background 捕获队列清空 + +--- + +### 性能说明 + +| 场景 | 影响 | +|------|------| +| 未开 DevTools | **无影响**(不监听 Network) | +| DevTools 开 + 捕获暂停 | **几乎无影响** | +| DevTools 开 + 捕获中 + XHR only | 仅匹配请求有轻微开销 | +| 高流量 SPA | 建议保持 **XHR/Fetch only**,不需要时点 **已暂停** | + +已做优化:过滤器内存缓存、静态资源不读 body、列表增量插入、搜索防抖、rAF 节流流式 UI。 + +--- + +### 常见问题 + +**扩展更新后报错 `chrome.runtime.connect` undefined?** +扩展重载后旧 DevTools 面板上下文失效。请:**关闭 DevTools → 重新加载扩展 → 再开 F12**。 + +**Request 里为什么曾经有 `:authority`、`:method`?** +HTTP/2 伪首部。展示与 AI Prompt 已归一化为 HTTP/1.1;原始 HAR 仍保存在内存 entry 中。 + +**Console 里 localhost CORS 报错是插件造成的吗?** +不是。那是页面自身请求本机服务被浏览器拦截,与扩展无关。 + +**Test History 很多会挡住 Captured Requests 吗?** +不会。历史区最高占侧边栏 **42%**,超出部分区域内滚动;捕获区占剩余空间。 + +**会拖慢网页吗?** +日常浏览(不开 DevTools)无影响。调试时可用 **已暂停** 完全停止捕获。 + +--- + +### Popup 与 DevTools 分工 + +| 位置 | 用途 | +|------|------| +| **DevTools 面板** | 连接、Validate、捕获、Send、Output(主工作区) | +| **扩展 Popup** | 只读连接状态 + 版本号 + 打开 DevTools 引导 | + +不在 Popup 中重复完整配置表单,避免与主流程脱节。 + +--- + +### 打包发布 + +```bash +bash plugins/browser-extension/cyberstrikeai-browser-extension/package.sh +# → dist/cyberstrikeai-browser-extension.zip +``` + +图标从项目根 `images/logo.png` 生成: + +```bash +LOGO="images/logo.png" +ICONS="plugins/browser-extension/cyberstrikeai-browser-extension/icons" +for size in 16 48 128; do + sips -z $size $size "$LOGO" --out "$ICONS/icon${size}.png" +done +``` + +--- + +### 限制 + +- Chrome **不提供** Network 面板右键菜单 API → 使用 **Latest XHR** + 自建列表 +- Firefox 需 `about:debugging` 临时加载;`storage.session` 不可用时 Token 回退 `local` +- 无法一键从 Popup 跳转到 DevTools 指定面板(Chrome API 限制) + +--- + +### 目录结构 + +```text +manifest.json # MV3 清单 +background/service-worker.js # 捕获队列、Panel Port、全局开关 +devtools.js # Network 监听(最早过滤) +devtools.html +panel/ + panel.html / panel.js / panel.css # 主 UI +popup/ + popup.html / popup.js / popup.css # 只读状态 +lib/ + api.js # 登录、SSE、项目/角色 API + storage.js # 配置与 session token + capture.js # HAR 摘要、静态过滤 + http-normalize.js # HTTP/2 → HTTP/1.1 展示/Prompt + formatter.js # toPrompt 组装 + markdown.js # Final Markdown 渲染 + catalog-cache.js # 项目/角色 5 分钟缓存 + constants.js # 上限常量 +icons/ # 16 / 48 / 128 +package.sh +``` + +--- + +### 与 Burp 插件对比 + +| 能力 | Burp 插件 | 浏览器扩展 | +|------|-----------|------------| +| 流量来源 | Proxy 历史 | DevTools Network | +| 连接配置 | Tab 内 | Tab 内(可折叠) | +| HTTP 格式 | HTTP/1.1 | 展示/Prompt 归一化为 HTTP/1.1 | +| 项目/角色/模式 | Send 弹窗 | Send 弹窗 | +| SSE 输出 | Progress + Final | Progress + Final | +| 捕获开关 | — | ● 捕获中 / ○ 已暂停 | diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/background/service-worker.js b/plugins/browser-extension/cyberstrikeai-browser-extension/background/service-worker.js new file mode 100644 index 00000000..3d39cb2b --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/background/service-worker.js @@ -0,0 +1,143 @@ +/* global chrome, CSAI_LIMITS, shouldCaptureEntry */ + +importScripts('../lib/constants.js', '../lib/capture.js'); + +/** Background hub: per-tab capture queue + panel subscriptions. */ + +const capturesByTab = new Map(); +const portsByTab = new Map(); +const filterApiByTab = new Map(); +let captureEnabled = true; + +function getCaptures(tabId) { + if (!capturesByTab.has(tabId)) capturesByTab.set(tabId, []); + return capturesByTab.get(tabId); +} + +function trimCaptures(list) { + if (list.length > CSAI_LIMITS.MAX_CAPTURED) { + list.length = CSAI_LIMITS.MAX_CAPTURED; + } +} + +function broadcastTab(tabId, message) { + const ports = portsByTab.get(tabId); + if (!ports) return; + for (const port of ports) { + try { + port.postMessage(message); + } catch (_) { + ports.delete(port); + } + } +} + +function cleanupOldTabs(activeTabId) { + if (capturesByTab.size <= CSAI_LIMITS.MAX_TAB_CAPTURES) return; + for (const tabId of capturesByTab.keys()) { + if (tabId !== activeTabId) { + capturesByTab.delete(tabId); + portsByTab.delete(tabId); + filterApiByTab.delete(tabId); + } + if (capturesByTab.size <= CSAI_LIMITS.MAX_TAB_CAPTURES) break; + } +} + +chrome.runtime.onConnect.addListener((port) => { + if (port.name !== 'cyberstrike-panel') return; + + port.onDisconnect.addListener(() => { + for (const [tabId, set] of portsByTab.entries()) { + set.delete(port); + if (set.size === 0) portsByTab.delete(tabId); + } + }); + + port.onMessage.addListener((msg) => { + if (!msg || msg.type !== 'subscribe') return; + const tabId = msg.tabId; + if (tabId == null) return; + + if (!portsByTab.has(tabId)) portsByTab.set(tabId, new Set()); + portsByTab.get(tabId).add(port); + + if (typeof msg.filterApiOnly === 'boolean') { + filterApiByTab.set(tabId, msg.filterApiOnly); + } + + port.postMessage({ type: 'list', entries: getCaptures(tabId) }); + cleanupOldTabs(tabId); + }); +}); + +chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { + if (!msg || !msg.type) return false; + + if (msg.type === 'capture-entry') { + const tabId = msg.tabId; + if (tabId == null) return false; + if (!captureEnabled) { + sendResponse({ ok: true, skipped: true }); + return false; + } + const filterApi = filterApiByTab.has(tabId) ? filterApiByTab.get(tabId) : true; + const entry = msg.entry; + if (!shouldCaptureEntry(entry, filterApi)) { + sendResponse({ ok: true, skipped: true }); + return false; + } + const list = getCaptures(tabId); + list.unshift(entry); + trimCaptures(list); + broadcastTab(tabId, { type: 'entry', entry }); + sendResponse({ ok: true }); + return false; + } + + if (msg.type === 'clear-captures') { + const tabId = msg.tabId; + if (tabId != null) { + capturesByTab.set(tabId, []); + broadcastTab(tabId, { type: 'list', entries: [] }); + } + sendResponse({ ok: true }); + return false; + } + + if (msg.type === 'set-filter-api') { + const tabId = msg.tabId; + if (tabId != null && typeof msg.filterApiOnly === 'boolean') { + filterApiByTab.set(tabId, msg.filterApiOnly); + } + sendResponse({ ok: true }); + return false; + } + + if (msg.type === 'set-capture-enabled') { + if (typeof msg.enabled === 'boolean') { + captureEnabled = msg.enabled; + } + sendResponse({ ok: true }); + return false; + } + + if (msg.type === 'get-latest-api') { + const tabId = msg.tabId; + const list = getCaptures(tabId); + const latest = list.find((e) => { + const rt = (e.resourceType || '').toLowerCase(); + return rt === 'xhr' || rt === 'fetch'; + }); + sendResponse({ entry: latest || null }); + return false; + } + + return false; +}); + +chrome.runtime.onInstalled.addListener(() => { + if (chrome.contextMenus && chrome.contextMenus.removeAll) { + chrome.contextMenus.removeAll(); + } +}); diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/devtools.html b/plugins/browser-extension/cyberstrikeai-browser-extension/devtools.html new file mode 100644 index 00000000..ad7821fb --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/devtools.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/devtools.js b/plugins/browser-extension/cyberstrikeai-browser-extension/devtools.js new file mode 100644 index 00000000..9a5f909c --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/devtools.js @@ -0,0 +1,69 @@ +/* global chrome, summarizeHarEntry, inferResourceType, mightCaptureRequest */ + +const FILTER_STORAGE_KEY = 'csai_filter_api_only'; +const CAPTURE_ENABLED_KEY = 'csai_capture_enabled'; + +/** In-memory flags — avoids storage read on every network request. */ +let filterApiOnly = true; +let captureEnabled = true; + +function syncFromStorage(data) { + filterApiOnly = (data && data[FILTER_STORAGE_KEY]) !== false; + captureEnabled = (data && data[CAPTURE_ENABLED_KEY]) !== false; +} + +chrome.storage.local.get([FILTER_STORAGE_KEY, CAPTURE_ENABLED_KEY], (data) => { + syncFromStorage(data); +}); + +chrome.storage.onChanged.addListener((changes, area) => { + if (area !== 'local') return; + if (changes[FILTER_STORAGE_KEY]) { + filterApiOnly = changes[FILTER_STORAGE_KEY].newValue !== false; + } + if (changes[CAPTURE_ENABLED_KEY]) { + captureEnabled = changes[CAPTURE_ENABLED_KEY].newValue !== false; + } +}); + +chrome.runtime.onMessage.addListener((msg) => { + if (!msg || !msg.type) return; + if (msg.type === 'set-filter-api' && typeof msg.filterApiOnly === 'boolean') { + filterApiOnly = msg.filterApiOnly; + } + if (msg.type === 'set-capture-enabled' && typeof msg.enabled === 'boolean') { + captureEnabled = msg.enabled; + } +}); + +chrome.devtools.network.onRequestFinished.addListener((request) => { + if (!captureEnabled) return; + + const tabId = chrome.devtools.inspectedWindow.tabId; + const req = request.request || {}; + const res = request.response || {}; + const url = req.url || ''; + const resourceType = + (request._resourceType || request.resourceType || inferResourceType(url, res.headers) || 'other') + .toLowerCase(); + + if (!mightCaptureRequest(url, resourceType, filterApiOnly)) { + return; + } + + request.getContent((body) => { + const entry = summarizeHarEntry(request, body, resourceType); + chrome.runtime.sendMessage({ + type: 'capture-entry', + tabId, + entry, + }); + }); +}); + +chrome.devtools.panels.create( + 'CyberStrikeAI', + 'icons/icon48.png', + 'panel/panel.html', + () => {} +); diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip b/plugins/browser-extension/cyberstrikeai-browser-extension/dist/cyberstrikeai-browser-extension.zip new file mode 100644 index 0000000000000000000000000000000000000000..5c708c99143423f7310f6422b90e3baa933da468 GIT binary patch literal 55551 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 literal 0 HcmV?d00001 diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon128.png b/plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..ab13d087506fbf0d1a0d4e62a608f28178339b61 GIT binary patch literal 21109 zcmV)dK&QWnP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91fS>~a1ONa40RR91fB*mh07#AmcK`rD07*naRCob>eFu14SDCh?8I7b- zZ>t|F%TH&+!Y=TH085rYmX;-};loxoE2HET78FV&ST`E#JQ>owy1j)b4J(?J7LdjW$h1qPZpr8m8GV!Mbz&oVD#7VOjbX0O(&_y&p1xd+5TMdE* z9koo@^L%_C5dWx$HXxUA*Qn=qy2 z32MmEMsQ4Elw!#fv!1A`#6raa1i0!=Dxt_cdnkbDvrnkBRM@}$Yh0r$^%vC z;<%A)HVgoiO@}HHA;lnlL*JMd5h*Bol-A1F40CI+$$sR}3&x^T=9Hqp{YfV2$^ zf-*BM70pFq7$7{NTQ0#ZRN#bh2_jbD3ra$=DR6g|ELjYW(E-AkNAiLsv0c((jTGt} z5iNC8(J3QDSp;U0c=S5*SSAH#yeK#`;N3`%s&r1^1mV18ZQ@Z40zXU_RTv0FWYyae zRpL#_WFr8|=cbU6Be!j@AraimeE3Hf(VhJgBHc4CM@^IBw_`HI1aN!v&h;~3Gm|5Ar<$)OrjGt$fXbPOX~TM zrs_2jz_bJzjat+uM_g}^kXHyzSobAAVo*_tsUb#B_H#~0!b-RIASMN~L8n5Pc~pXRx`(&TI-hl1R4gAPAj4a7AO7lc4ifhg0j-M|M}8pbCaiW?NX>4xxVz|yIW ziK9kZQKGJ!Qy&2GEHDEU`mzIADJlXJav;uwya8~06IP(mv;ws}A3_v*z0hk~R(n&?n(8;B9l8Q!EBqN&VmMTG5lWLJR8pJd&Lj(PzJfMWj zaF$!w2ob1=#)_IQ$&F2_fFeA0$Wce0L}WJ>`hRx|KaL&%mkXfgDMuDYazvklvMwrZ zTO0y_L(rgWR@BJ#$S}4<)RQ2)7EIJ7GNm=Wly|Ts<|sBb&A1$;Du{GlUPG7)0#QpH z*)g1)P2)&b+VTfP$&qaOBwJB_qFx|kONRV5)6O}10}uT)G{T@=GZ&EvCqzq4upUCz zys%5ND0kA0df0!XHo?%*?tzLKqi`OdwvKRAVu2<>RyHJP$q&O&%z3(+GwcTsWLo$P z6UzK(1ifwEP{Uo!;nOd&sRwB*wlpyWy5W8sH#cE}@ZZ~J|Cc?0vS!hZ$toQH)zaxk zg=%DYK-<9)cHAX+ExCxGE~8N}hAU`_nJ`2+Od*L8f(d{F3IvE`gP|jmiP16{IRPP2 zQsNAXKGR(m`R`5di3K_(CC;s3N`qbNd!0uxBqK;A5W+Sh7Ums<15>WUmXgT?Fa^xH zX<^LeE|;9(zpGk0C~UJTAm$=91p`t|mEaAS>31NYQ z$&X`+#Xh9%6QO}6iebZZ$`G1mCYwPgRumJ(iHr&|RxGhH2AmMMO9I+jxlu%4;CT=M z3Y=V%t=tlHq_m9F7|}Tfbjaqu02C!v+`1x+pvh2*ND+$UqLA2#!jn&;xQbw-{BV2$ zhCjLht&q4p0GV-gY13)5jhg!`I}rllL?C81-Q3W6(#o@A*^<8D!-dg|3;?bwI4DI? zF)4prD3MP^?0+M61R>lFg*hKUSLeP;GzD&;hOF7(S!ZG>PtiPe%NR-I`4|Rmkn)=B zd6!VuB$RoXOE||KbTM3z!U)gx(P6Q^^wK;WpPWe zZ^9llEz**E={q~^;-7oMjoX4p6mf=xpiHLz7(v7oO=m&mnE~ok7!_TVZB*!MJd7z( zRA9px#0X0z7C|XKwAi7elofzT>`_Bna*PJQhVqT5i%N=N ziYm`5=hx3i*Z<1MCB~jtha!JG9)7V!myD5+cFi&xf7OKGOf4?ayexB2fOH7 zn^NjZ95jxQcp^lVMebd6;4DHA2H@Oz7ZNpn>beWgJ^89%-+$w-{VzkWBwiVh7e{z# zn2%9BVNNT4N1{m1PZBlxwWW1pFh7)00UD!IMkz&N*1wQ3Fw44xts;cP3@yje@OYfm zm0&bFZZIcKe_RYiw&j+RZ628l6HK-zkN}zrs3`y?Qxsp`I)O}MNh}JQMuQTY*_ncZ z%)2l9)WX)4|8eUV`vyA;3yZVaRCXp7D=3A46&p0R&8?`#up_F%AST%=)^uN~Y}t1K zZCEjEt^p$CeIv#g+@e&$mpD}d!wjKE>)bFEER=}c%Nt`uU`E-Z7VWx{S)-~AJzGIU zsH*6xvTJ(85s+k|;+a?6LJ%6~J^gcW>SR-8CFP%e&-YR@sc-(|!_%qBl0;Q`S#|%= zLEM#36qm`Gp<5E!wn<_z=AG?`Y%>nwQ%o5J0fqj0Iy@)vkqpZ^RV09#2Yv5TQFfr? zY2x}qHpn@T2*kqhh{F$CrXzm{Ba?2|B%q;TfP%CHEj8060PP7vLKK83iTFpBCzNfn zC~R{8mL#Xznczn-$o{PQx!?QDodZL?|NdY9Je``36_qsBww9JuKn!Q1#c?*GlBY;p z#8wGAA+(h!II* zH>aLWK$3I5(4bOh!h|x4jh}c1$T(Tpi}tcSn#fe9kFcj;1RkVGk}Zj*7SCV(<1an< z?5ltM;cvgFd%#8YHFHZ6<>o+oCS6>V+XLh?^m4^a!hhNP*j};dc#pPf&Gn39_QicN_`B|9uxrMoZ;>xrB?Vo@3 z%X_ZB_5SbU(g*yiiiV10Eyk`0*Ixw%Q|XCVEPnvZx|}lyMQ|z&f`U*x=|uPk;O2?%n&kw;$-*J~-Bs$)u6}b56SA>mU62fByboe*M4? z@DvDa7RO8Ks&Uqjf?EMR5ue5Y6qSe)RUt-Z8HKLHZ%ce2h-L$kOE2SNCOr<#T#Hyy zvl)2Mk9*X#5T$^)W>Q<)W|4X0Sd$5q7H#=HQc#j8yXB6X&fW0lUw!j04?TD1ZTJ49 zqvwEzffLsJl%gOteDp_7qn??`6fpG#jdgR@F5Ym;>I+sZII*$1Whyn@-GAsn*X|wr zUh3>UG%(ydJ~@(}Nim_ZlCnfqZB=7^#oQ&WC!f6J%Cbb|p@G-F_s0)ic-q^q`@qfL zzwOJGede$*TU*^6E5b<3ut%Q?GTEt;yn`@3Q@Rl!@U_cox^m)%Y$zWRBn9yYmL`^P zrHdg`wiqA;5vwbaiUBZfgN-m~R;Z8xlqm%oSP&#f2n+xO>!4W@FE2|ZZ~pzax9)iE zlh=IZjCGej{Omoye(*=#eb^gt=cC9Slu4T<)Mdeh%~m99PF{KLMH}96!m^DB>-GIF z-2c>VFYb71f5*;2E^A>SqSgPc#A+*=AbIJ=cU|_@u=bocpY@No ze(SHd|M}M6JaE(SXkQk)9fa|r8_=@bVKcL8=U#rs`>xpd{`$(6mv%jI{jWdu^eYeb z3><=piruz?!>~dG^~x3$iLhYMEJP7=^|l$N;qAmRRhf7KX-9(xj;2nuZ~Y1^`+S zfSI>i;L5D)%j1B=t<4sijtq=XjMh}t>IfMd9UFak)1O}2_FUWS#n)c`h0D%(*R2oy z@Xm*SIWaj}P*4oy>#aTpWMkxm)gh{AW84-~ttpIF{l9mCh+P zOCt@_K~4lK77%D8U{89>xgY%cwKv`I=&x?L?Xv_?!ZH1;%Igz}G8!cb;+j%if~Je( ziK4>T4E90^^D@G7;xVcwN|_RJ;el8!#Sm6$+mSHaHX17NHGaHL6j zBvsOx2un8|3InBNn9SgCe@o+>(nRIl=0&^qzligb-hu9({;ua=d;G!;Z~4HLU%GU| z|M{=GzwyYHyJlvl6UC)(KK1?AUh*$hRn@Nh&Kw< z+{97$vW@Tk>U)0l(DV0y<7XerWHQm2SS(gtRndr(Rv-b+5@`C_!b~E8B}it3$oxZ4 zY^IOFl8-_@hak$a2t*`TGX)!8IC4C+A_U5^1EDYg)Me3FMUu1uK~uCbL>t6_#;wO- zLInl=L*1(uo-m!BZmesaoE|^W{yGo!5_<6I-#zxieQ!DQBcHhHKQ24vqrZRnhKo;n z_c6(eDai3w(n3<_eHpb&6I-`}AAh{<{F`J0hR43>3kM1eNt{L&kli^@2%w;k1Goh5o zwIE6mDg&Hp3i*McMpQdQHu^pwKNf1LABfm8fy`ncQASiF!JvBN$VE5T^(w$1Dw(fc2(HHM*tzGe!4PULUZ27NyuKVF#pBxWLYwmn)0b31!VvD-hK&uIL^(WMgS)_`apSwl$Xj z2ng|b(ls>Nn@rT6yy(MA=3X>9(b+wW852bs!V7^TpOItCiJ3-CdJqy3OyJv&kmGULr>2tTe+;|l6b82K;PDMWXA3gL#ii?( zpK{iT7p+{dA)QI@eB;%xe&CAQ^5#qmCyq1YlM@|%dzQ^UslIyFr~mWn*Y`f3pM3!q zRhHMIA(wHrnBf|LU=O(86f+v&=9(jW3CJ|A< zwD~Urdo;v~Ty8_#QfGA6WFl-;m5m6MDGy^nIH=kpVE6*&80a6dT$6(`)6sKiLCdn& z_HS*@wm~3KT(W5X>g{`;$H3sqQ{!qH#mMG^FMt2V{r6w7{+sVT`S!K5Z-3ymuXPUL zU=X9pf>d=?{pDxB!+Vj&5Tr)Heh8NpB#tP(nPYhyzw6|{OF2> z%f9}Lk3Y8M&ZDvqbGQjwQd~~Yi(=ywgRq?}t)d%+xRAzz221fwniCGUm8Mdo3mVS3 z{P>^T_u|KgC-5a@vCS=Hflm7&$Z~6Q)Y8id!z4+FklrN0q}5Q9GAs~6(J?20Aj?du z0gy;9S(79o63}2_NG@&WNyL{;7oPNvxlIfI`@tL5FFPB2#Dy7$Gx|L|xpZBMhJxiA z%{6nbx_sUB$&!Y@ZoB^RT{ot)!*74nweP#?;{(Hezq#*cf8F#y{X<6^d!fR4IHn1J<$nz=mx!O z)9;*~ndWUJ+*_KNi4~UijBLN}rH>4aAIKk3Lg!ZYJa?j|(v_RLq|?u6KunNb{mU&u z5Hmpva}4m2xsZMhMr4O06XoATG)lC$>(HyGuD&Rhn#5TUW+)=XvA4FWxv62evuAJq z+UnkL5@fd?zWt562rzxL2gnHyC}Y6kyx~88BqEl;%*N4+pG7iSKQgD1bu;e@qppoXt!5fxdilqDgBnfFOlIi4qi=K;y-C z#3P{og0M6=9wZ=Q{KLmw7p%;RZ9=GV-hHrpcWJ!5wz6?_e2}{foWeYu$u>7Es4TBT zQyNEA+!seCy6=AF6Hj%2@$xIzKEL(Jw|wf12cG<6CIizvK|&lM`^>XBhm)>T*If9G zUwmkCYSf2TE&?6SK&G?=UpJOuYIpbV!+b9*sRW04q{ms@%fluSE5~nD&{p>0=?wOQ zsnl3q`Qo#ef2*Vz>lJTLDGAI1TahdBgJ?ii5wbPSP4ei94IOqMGMPmb|AB^_VIKwh z{Gc*H+c1sFB*FAY%*YGjdr}IgQ)9g&hgKYOeD6TJNdQeA7q&DmjK>qulL^^7qRO_- zUUJnXZ~EL#mtKF%=f}oz?u694LDxc`6gUUS^wI_EpMCwI-3MQa77E%DChKfLd1*Bk z`mVlx8X?@Ms)jmlq2e1c3|Kld1*y_R4J0x7x%%)xzz-VJ>9LmT^`|fXGA@#3F{;dh zyUuJeC9F{_QQibiWNtB)l|z@41f~^Bga_xO&louMO%}Bzm^B{>QkV$|SPexJ2jGi4 zpE_>InL{Jp8J^EE`{_&y^Q)|+qNQOW2RAY)2k1i<*Ge_-y69s!{P&k0dG=nL^?`}4 zY=`6qe`3k9(ubbABPVd z%DEsZ?-?E+u$zLEol1|kHJrC*-g^)M!AYeRCW1Aq{7^zl0zHqH*CZDO4sZ!_7`0r; z6Uhz#L=CCOxPV6cxlnzX${}su_Q<@3#U+X4$k-sXF`J+@G}1dZK3rYVR8u*NvrHDF z3WIX`+6({i(0{$M>$%)?rg=K5fAR!s+JQjFv2@42SH^i~NpjH!4J8OCmq_&UD1GD0kZBN!(-V zoY4=D4B-Ns{>ew13AWF{@9ESO&U~U-2`dnU{N(fqmKW?6;DXRl-Igs#!yTm`COF>8 zG4hHrzyYA}gkwHln!u%8?f@o(a!T>r5abO6Stw4IgSn7Bbes{`)j5+K0a8u9K;dcw zB*tY7_(yrA!N-A0Ak37Olz#W4cf;z|9nYM;@{)(Q+}=<#2h$I>F%6R?m6(GgV|}=` zQt(paT#n&(*ul@^?vNMroq#hTBQ`6(P zA(%|mZ&>)z!om`My~82k;v-e<2JFCcrYzBbZ_N((KN;QKGkcK_Q3Gz|5>2b-q`a3K z495Jy667P{8*@=g5k*;s7x0OLsXwT5o1;htRW@_=*`HXw@T5cCyPn_vm$tb}8fuz* z2M!{?2n-)AN^@o{!P(yE#Gvx!iiov$pvO;R2n=ZYXgSs82F8yD@Cn|lr^>^DJQT>b zT|_Ph9?2l6Ac;6HN@u1)YOY?LEUnKL;(mr@oU@F_0t(v-fnaeAn_Ap_S#$MqcGW@{ z&M7m;o?N8^V5km;$wbk7H3!@dNcxJ3Jp4tYVtv8*P!gLVxftMWibMg41#?LZRLx)5vhtbN|AYrX5HN0gb@lDV63pNfpY-T-UQbpO7sId*BGR(M z&}3aMOWWc`6aFy{76d4H%A)v&P1*`l*m96BSbg1#`)|XEPDnKJbu4KfmG_s82a?qVS{`i0LIQ7xj-GeEyA}j!yRC zieX3Bo$2&=5aBMX+zMc(Af1_Ps#-s=-`&^PhN5A zwYc2ZqaX2#vN~MQ+M!H-m@+NMS2N`XMWsJ1W(;jM&{wQzhhQi}y6>B6si#~pA_+)? zw%~AW`TY9I`47MTuiz9Erk>hy{r1Cm)>h6z-ublw{G*m>utvddy?pl7u_8RjXk4n2 z2Xq>+${L(VoCbj|5fW8=qYJSdlvtW9?x@V5O>wE9vM$Gg*+&lrXWS)kJM;711ATWr z{iA5<#1i-WNB`r5rRU9UJoZr6E=;Gvk*wSFN_0R6Pv88qe9uVTS7~N6fG_x}w zP;iBus;gKsyXJU|A2>24F`AHuR|>`>G9asASrW`1Dcl1zi$(QBgX75t74l5F^VJp8 zWx2sHq>nrH{B_5k_Tc7Ubr0;%J5MMY9_zX1nV)_5%5RU1^>p_gz_%hrcrKyUpMlea{MFqkpk6JXUqk-#An|y3@*rKjU;hOjG(CfH7v^hC6eh6?a zwd-)_L&eY$w@!+PNIkn&Sk!bW7En*PUgKGWU5r*H584#mxl zEr=vCXem&-Y*xX77q*l;LyaN=9c`Bj;`CQVEVxM5BTBBJDTTQVu9*f43600f=F}cP zJw2)5crmPF0OG~RyHH8bOgB`ntSVbzH;6*E2+vHauUO1rUd`Z?OVHd^dszol<%JHj zXO3G0^4CaVaHZhJ;K#z*>(?K1&a>P8ysr~qMfr_Gj~_E7TX5qYpS^JXyG~yAruOcg z4b}AxRW10+DcF?XcF>_+V7mw2(ZR-jVr1lU2SD=BQAFsOFflDgS0=#<2q*|i`l^j# z^lW-o)v}t>V{z>!2xXrNN*a&Gwk}}@$rQ)R=hdANmZm(E&_tdTM*iF;#X_SC@&**{ z0SqP-j)mrh{1gaS?z@BnEohK^=9+(u6~_Pi;;nf;X{?Ec-7elec<{!1KKGfo{G_dE z)$vO=_7C@l;{+sFm`WnELZMe`sAaChPtwA07&}Np9Xj^GM&7e)t!PLjk?T2zZ&uE~ zd@S8NIXxK1V~Q}t)GO)O_+bIaAZKs^(Oh#v5uawH4KMPYE-Dzv9T5)$awZ+HKEC>bIqVQ%?+31?B*B&4Lr3i*U;1?3t{Qp z!95cyCUtp9b6d@u9((;e_rCJpot<|9nBSX1gT)U6NWCu5P^GHMT5FQN3K5?^&>K3C z^GxVudxK&;4wwuD!<8P$RD&e89+o!dxfIYe4ti<{DYLBg)TYV>TX#P^GM>AXb(@ga zv{kq0j;DS!XZHNUY%%Uv%MgJg-_2Z}eL3x}%MGh&oU)8XBJ6>tj4Cz_fdoujqhe?v z&})2h!H_evcK*A{OJ^O|_Thys=e}^@rpI@Fb#Q#QPB$UR2f1VoL&2Q^ce-#}qN!?~ zyI|+y?u`s_N>k^EVtyV$vH_6moHIPN)ZP&MfxUS?C^8d~QQ4D;NRJL^M+{`DbAn4&ZTgOesn`^vp8
  • 4CY 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 M07*qoM6N<$f~Kt`f&c&j literal 0 HcmV?d00001 diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon16.png b/plugins/browser-extension/cyberstrikeai-browser-extension/icons/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..db614b679d48cf52d8c14fa3c578cf28046afc63 GIT binary patch literal 633 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 m.id === id) || AGENT_MODES[0]; +} + +async function apiFetch(baseUrl, path, options = {}) { + const res = await fetch(baseUrl + path, options); + const text = await res.text(); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch (_) { + json = null; + } + if (!res.ok) { + const err = (json && json.error) || text || `HTTP ${res.status}`; + throw new Error(err); + } + return json; +} + +async function loginAndValidate(baseUrl, password, signal) { + const login = await apiFetch(baseUrl, '/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ password }), + signal, + }); + const token = login && login.token; + if (!token) throw new Error('Login response missing token'); + await apiFetch(baseUrl, '/api/auth/validate', { + method: 'GET', + headers: { Authorization: `Bearer ${token}` }, + signal, + }); + return token; +} + +async function fetchProjects(baseUrl, token, signal) { + const data = await apiFetch(baseUrl, '/api/projects?limit=500', { + headers: { Authorization: `Bearer ${token}` }, + signal, + }); + const list = (data && data.projects) || []; + const out = [{ id: '', label: '(无)' }]; + for (const p of list) { + if (!p.id) continue; + let label = p.name || p.id; + if (p.status === 'archived') label += ' [已归档]'; + out.push({ id: p.id, label }); + } + return out; +} + +async function fetchRoles(baseUrl, token, signal) { + const data = await apiFetch(baseUrl, '/api/roles', { + headers: { Authorization: `Bearer ${token}` }, + signal, + }); + const list = (data && data.roles) || []; + const out = [{ id: '', label: '默认' }]; + for (const r of list) { + if (r.enabled === false) continue; + if (!r.name) continue; + out.push({ id: r.name, label: r.name }); + } + return out; +} + +function extractConversationId(ev) { + if (!ev || typeof ev !== 'object') return ''; + if (ev.conversationId) return String(ev.conversationId).trim(); + if (ev.data && ev.data.conversationId) return String(ev.data.conversationId).trim(); + return ''; +} + +async function streamTest(baseUrl, token, options, handlers) { + const mode = agentModeById(options.agentMode); + const body = { + message: options.message, + conversationId: '', + role: options.role || '', + }; + if (options.projectId) body.projectId = options.projectId; + if (mode.orchestration) body.orchestration = mode.orchestration; + + const controller = new AbortController(); + if (handlers.setAbortController) handlers.setAbortController(controller); + + const res = await fetch(baseUrl + mode.path, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(body), + signal: controller.signal, + }); + + if (!res.ok) { + const t = await res.text(); + throw new Error(t || `HTTP ${res.status}`); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + if (!line.startsWith('data:')) continue; + const json = line.slice(5).trim(); + if (!json) continue; + let ev; + try { + ev = JSON.parse(json); + } catch (_) { + continue; + } + const type = ev.type || ''; + const message = ev.message || ''; + if (handlers.onEvent) handlers.onEvent(type, message, ev); + if (type === 'done') { + if (handlers.onDone) handlers.onDone(); + return; + } + } + } + if (handlers.onDone) handlers.onDone(); +} + +async function cancelByConversationId(baseUrl, token, conversationId) { + await apiFetch(baseUrl, '/api/agent-loop/cancel', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ conversationId }), + }); +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/capture.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/capture.js new file mode 100644 index 00000000..156e0984 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/capture.js @@ -0,0 +1,103 @@ +/** Capture filtering and HAR entry normalization. */ + +const STATIC_EXT = + /\.(js|css|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|eot|map|wasm)(\?|$)/i; + +const STATIC_MIME_PREFIXES = [ + 'image/', + 'font/', + 'audio/', + 'video/', + 'text/css', +]; + +function inferResourceType(url, responseHeaders) { + if (STATIC_EXT.test(url || '')) return 'static'; + const ct = headerValue(responseHeaders, 'content-type').toLowerCase(); + for (const p of STATIC_MIME_PREFIXES) { + if (ct.startsWith(p)) return 'static'; + } + return 'other'; +} + +function headerValue(headers, name) { + if (!headers || !headers.length) return ''; + const lower = name.toLowerCase(); + for (const h of headers) { + if ((h.name || '').toLowerCase() === lower) return h.value || ''; + } + return ''; +} + +function headerLines(headers) { + if (!headers || !headers.length) return ''; + return headers.map((h) => `${h.name}: ${h.value}`).join('\n'); +} + +function truncate(str, max) { + if (!str || str.length <= max) return str || ''; + return str.slice(0, max) + '\n… [truncated]'; +} + +function shouldCaptureEntry(entry, filterApiOnly) { + if (!entry || !entry.url) return false; + return mightCaptureRequest(entry.url, entry.resourceType, filterApiOnly); +} + +/** Fast pre-filter before reading response body in devtools. */ +function mightCaptureRequest(url, resourceType, filterApiOnly) { + const rt = (resourceType || inferResourceType(url, null) || 'other').toLowerCase(); + if (filterApiOnly) { + return rt === 'xhr' || rt === 'fetch' || rt === 'websocket'; + } + if (rt === 'static') return false; + if (STATIC_EXT.test(url || '')) return false; + return true; +} + +function summarizeHarEntry(harEntry, responseBody, resourceType) { + const req = harEntry.request || {}; + const res = harEntry.response || {}; + const url = req.url || ''; + let path = '/'; + try { + const u = new URL(url); + path = u.pathname + (u.search || ''); + } catch (_) { + path = url; + } + const shortPath = path.length > 80 ? path.slice(0, 77) + '...' : path; + const title = `${req.method || 'GET'} ${shortPath}`; + + return { + id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + title, + method: req.method || 'GET', + url, + resourceType: resourceType || 'other', + requestHeaders: headerLines(req.headers), + requestBody: truncate((req.postData && req.postData.text) || '', CSAI_LIMITS.MAX_REQUEST_BODY), + responseStatus: res.status, + responseHeaders: headerLines(res.headers), + responseBody: truncate(responseBody || '', CSAI_LIMITS.MAX_RESPONSE_BODY), + capturedAt: Date.now(), + }; +} + +function summarizePageContext(page) { + return { + id: `page_${Date.now()}`, + title: `PAGE ${page.title || page.url || ''}`.slice(0, 80), + method: 'PAGE', + url: page.url || '', + resourceType: 'document', + requestHeaders: '', + requestBody: '', + responseStatus: 0, + responseHeaders: '', + responseBody: '', + pageTitle: page.title || '', + isPageContext: true, + capturedAt: Date.now(), + }; +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/catalog-cache.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/catalog-cache.js new file mode 100644 index 00000000..649e06be --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/catalog-cache.js @@ -0,0 +1,39 @@ +/** In-memory cache for projects / roles lists (per baseUrl + token). */ + +const CATALOG_CACHE_TTL_MS = 5 * 60 * 1000; + +const catalogCache = { + baseUrl: '', + token: '', + projects: null, + roles: null, + fetchedAt: 0, +}; + +function catalogCacheValid(baseUrl, token) { + if (!catalogCache.fetchedAt) return false; + if (catalogCache.baseUrl !== baseUrl || catalogCache.token !== token) return false; + return Date.now() - catalogCache.fetchedAt < CATALOG_CACHE_TTL_MS; +} + +function invalidateCatalogCache() { + catalogCache.projects = null; + catalogCache.roles = null; + catalogCache.fetchedAt = 0; +} + +async function fetchCatalogCached(baseUrl, token, signal) { + if (catalogCacheValid(baseUrl, token) && catalogCache.projects && catalogCache.roles) { + return { projects: catalogCache.projects, roles: catalogCache.roles }; + } + const [projects, roles] = await Promise.all([ + fetchProjects(baseUrl, token, signal), + fetchRoles(baseUrl, token, signal), + ]); + catalogCache.baseUrl = baseUrl; + catalogCache.token = token; + catalogCache.projects = projects; + catalogCache.roles = roles; + catalogCache.fetchedAt = Date.now(); + return { projects, roles }; +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js new file mode 100644 index 00000000..70b1ffed --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/constants.js @@ -0,0 +1,17 @@ +/** Shared limits and defaults for the browser extension. */ +const CSAI_LIMITS = { + MAX_CAPTURED: 200, + MAX_RUNS: 50, + MAX_REQUEST_BODY: 65536, + MAX_RESPONSE_BODY: 4096, + /** Progress log only; active Final Response is not truncated. */ + MAX_PROGRESS_CHARS: 524288, + MAX_TAB_CAPTURES: 20, + /** Markdown render skipped above this size (plain text only). */ + MAX_MARKDOWN_CHARS: 100000, + /** Non-selected completed runs: soft-trim final to limit memory. */ + MAX_FINAL_ARCHIVE_CHARS: 100000, +}; + +const CSAI_DEFAULT_INSTRUCTION = + '针对该流量做web渗透测试,并输出测试结果,要求:只针对该接口流量做测试,切勿拓展其他接口'; diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/formatter.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/formatter.js new file mode 100644 index 00000000..25894b50 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/formatter.js @@ -0,0 +1,47 @@ +const DEFAULT_INSTRUCTION = CSAI_DEFAULT_INSTRUCTION; + +function defaultInstruction() { + return DEFAULT_INSTRUCTION; +} + +function toPrompt(entry, instruction) { + if (entry && entry.isPageContext) { + const prefix = (instruction && instruction.trim()) ? instruction.trim() : DEFAULT_INSTRUCTION; + return ( + prefix + + '\n\n[Target]\n' + + 'PAGE ' + + (entry.url || '') + + '\n\n[Page]\n' + + 'Title: ' + + (entry.pageTitle || '') + + '\nURL: ' + + (entry.url || '') + ); + } + const prefix = (instruction && instruction.trim()) ? instruction.trim() : DEFAULT_INSTRUCTION; + const method = entry.method || 'GET'; + const url = entry.url || '(unknown)'; + const reqHeaders = normalizeRequestBlock(entry); + const reqBody = entry.requestBody || ''; + let respSnippet = ''; + if (entry.responseHeaders || entry.responseBody) { + respSnippet = + '\n\n[Optional: Response (truncated)]\n' + + normalizeResponseBlock(entry) + + '\n\n' + + (entry.responseBody || ''); + } + return ( + prefix + + '\n\n[Target]\n' + + method + + ' ' + + url + + '\n\n[Request]\n' + + reqHeaders + + '\n\n' + + reqBody + + respSnippet + ); +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/http-normalize.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/http-normalize.js new file mode 100644 index 00000000..ae81176f --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/http-normalize.js @@ -0,0 +1,104 @@ +/** HTTP/2 pseudo-header → HTTP/1.1 normalization for display and AI prompts. + * Raw HAR headers in storage are never modified. */ + +function parseHeaderLines(headerText) { + const headers = []; + for (const line of String(headerText || '').split(/\r?\n/)) { + if (!line.trim()) continue; + const idx = line.indexOf(':'); + if (idx <= 0) continue; + headers.push({ + name: line.slice(0, idx).trim(), + value: line.slice(idx + 1).trim(), + }); + } + return headers; +} + +function urlParts(url) { + try { + const u = new URL(url || ''); + return { + host: u.host, + path: (u.pathname || '/') + (u.search || ''), + }; + } catch (_) { + return { host: '', path: '/' }; + } +} + +/** Request line + headers (HTTP/1.1), no body. */ +function normalizeRequestBlock(entry) { + if (!entry || entry.isPageContext) return entry?.requestHeaders || ''; + + const headers = parseHeaderLines(entry.requestHeaders); + const fromUrl = urlParts(entry.url); + let method = entry.method || 'GET'; + let path = fromUrl.path || '/'; + let host = fromUrl.host; + + const regular = []; + let hasHost = false; + + for (const h of headers) { + const name = h.name; + const lower = name.toLowerCase(); + if (name.startsWith(':')) { + if (lower === ':method') method = h.value || method; + else if (lower === ':path') path = h.value || path; + else if (lower === ':authority') host = h.value || host; + continue; + } + if (lower === 'host') hasHost = true; + regular.push(`${name}: ${h.value}`); + } + + if (host && !hasHost) { + regular.unshift(`Host: ${host}`); + } + + if (!path.startsWith('/')) path = '/' + path; + return `${method} ${path} HTTP/1.1\n${regular.join('\n')}`; +} + +/** Status line + headers (HTTP/1.1), no body. */ +function normalizeResponseBlock(entry) { + if (!entry) return ''; + + const headers = parseHeaderLines(entry.responseHeaders); + let status = entry.responseStatus || 0; + const regular = []; + + for (const h of headers) { + const name = h.name; + const lower = name.toLowerCase(); + if (name.startsWith(':')) { + if (lower === ':status') { + const n = parseInt(h.value, 10); + if (!Number.isNaN(n)) status = n; + } + continue; + } + regular.push(`${name}: ${h.value}`); + } + + const statusLine = `HTTP/1.1 ${status || '?'}`; + return regular.length ? `${statusLine}\n${regular.join('\n')}` : statusLine; +} + +function formatRequestDisplay(entry) { + if (!entry) return ''; + if (entry.isPageContext) { + return `PAGE ${entry.url || ''}\n\nTitle: ${entry.pageTitle || ''}`; + } + const block = normalizeRequestBlock(entry); + const body = entry.requestBody || ''; + return body ? `${block}\n\n${body}` : block; +} + +function formatResponseDisplay(entry) { + if (!entry) return ''; + const block = normalizeResponseBlock(entry); + const body = entry.responseBody || ''; + return body ? `${block}\n\n${body}` : block; +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/markdown.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/markdown.js new file mode 100644 index 00000000..a07ec9af --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/markdown.js @@ -0,0 +1,135 @@ +/** Minimal Markdown → HTML (aligned with Burp plugin renderer). */ + +function mdEscapeHtml(s) { + return String(s || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function mdHeadingLevel(s) { + let i = 0; + while (i < s.length && s[i] === '#') i++; + if (i >= 1 && i <= 6 && i < s.length && /\s/.test(s[i])) return i; + return 0; +} + +function mdReplaceInlineCode(s) { + let out = ''; + let inCode = false; + let buf = ''; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === '`') { + if (!inCode) { + inCode = true; + buf = ''; + } else { + out += `${buf}`; + inCode = false; + } + continue; + } + if (inCode) buf += c; + else out += c; + } + if (inCode) out += '`' + buf; + return out; +} + +function mdReplaceBold(s) { + let out = ''; + let i = 0; + while (i < s.length) { + const start = s.indexOf('**', i); + if (start < 0) { + out += s.slice(i); + break; + } + const end = s.indexOf('**', start + 2); + if (end < 0) { + out += s.slice(i); + break; + } + out += s.slice(i, start) + '' + s.slice(start + 2, end) + ''; + i = end + 2; + } + return out; +} + +function mdInlineFormat(text) { + let escaped = mdEscapeHtml(text); + escaped = mdReplaceInlineCode(escaped); + escaped = mdReplaceBold(escaped); + return escaped; +} + +function markdownToHtml(markdown) { + const lines = String(markdown || '').split(/\r?\n/); + const css = + 'body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;line-height:1.45;margin:10px;color:#111;}' + + 'code,pre{font-family:ui-monospace,Menlo,Consolas,monospace;}' + + 'code{font-size:0.95em;background:#f6f8fa;border:1px solid #e5e7eb;border-radius:4px;padding:0 4px;}' + + 'pre{font-size:0.95em;background:#f6f8fa;border:1px solid #e5e7eb;border-radius:6px;padding:10px;overflow:auto;}' + + 'pre code{background:transparent;border:none;padding:0;}' + + 'p{margin:0.55em 0;}ul{margin:0.4em 0 0.6em 1.2em;padding:0;}'; + + let out = ``; + let inCode = false; + let inList = false; + let codeBuf = ''; + + for (const raw of lines) { + const line = raw == null ? '' : raw; + if (line.trim().startsWith('```')) { + if (!inCode) { + inCode = true; + codeBuf = ''; + } else { + out += `
    ${mdEscapeHtml(codeBuf)}
    `; + inCode = false; + } + continue; + } + if (inCode) { + codeBuf += line + '\n'; + continue; + } + const trimmed = line.trim(); + if (!trimmed) { + if (inList) { + out += ''; + inList = false; + } + out += "
    "; + continue; + } + const h = mdHeadingLevel(trimmed); + if (h > 0) { + if (inList) { + out += ''; + inList = false; + } + out += `${mdInlineFormat(trimmed.slice(h).trim())}`; + continue; + } + if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) { + if (!inList) { + out += '
      '; + inList = true; + } + out += `
    • ${mdInlineFormat(trimmed.slice(2).trim())}
    • `; + continue; + } + if (inList) { + out += '
    '; + inList = false; + } + out += `

    ${mdInlineFormat(trimmed)}

    `; + } + if (inCode) out += `
    ${mdEscapeHtml(codeBuf)}
    `; + if (inList) out += ''; + out += ''; + return out; +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js new file mode 100644 index 00000000..2ba1cdcc --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/lib/storage.js @@ -0,0 +1,114 @@ +/* global chrome */ + +const STORAGE_KEYS = { + host: 'csai_host', + port: 'csai_port', + https: 'csai_https', + lastProjectId: 'csai_last_project', + lastRole: 'csai_last_role', + lastAgentMode: 'csai_last_agent_mode', + lastInstruction: 'csai_last_instruction', + filterApiOnly: 'csai_filter_api_only', + captureEnabled: 'csai_capture_enabled', + renderMarkdown: 'csai_render_markdown', + showDebugEvents: 'csai_show_debug', +}; + +const SESSION_TOKEN_KEY = 'csai_token'; + +const DEFAULTS = { + host: '127.0.0.1', + port: '8080', + https: true, + lastInstruction: CSAI_DEFAULT_INSTRUCTION, + filterApiOnly: true, + captureEnabled: true, + renderMarkdown: true, + showDebugEvents: false, +}; + +function localGet(keys) { + return new Promise((resolve) => chrome.storage.local.get(keys, resolve)); +} + +function localSet(obj) { + return new Promise((resolve) => chrome.storage.local.set(obj, resolve)); +} + +function sessionGet(keys) { + return new Promise((resolve) => { + if (!chrome.storage.session) { + chrome.storage.local.get(keys, resolve); + return; + } + chrome.storage.session.get(keys, resolve); + }); +} + +function sessionSet(obj) { + return new Promise((resolve) => { + if (!chrome.storage.session) { + chrome.storage.local.set(obj, resolve); + return; + } + chrome.storage.session.set(obj, resolve); + }); +} + +async function loadConfig() { + const data = await localGet(Object.values(STORAGE_KEYS)); + const sess = await sessionGet([SESSION_TOKEN_KEY]); + return { + host: data[STORAGE_KEYS.host] || DEFAULTS.host, + port: data[STORAGE_KEYS.port] || DEFAULTS.port, + https: data[STORAGE_KEYS.https] !== false, + token: sess[SESSION_TOKEN_KEY] || '', + lastProjectId: data[STORAGE_KEYS.lastProjectId] || '', + lastRole: data[STORAGE_KEYS.lastRole] || '', + lastAgentMode: data[STORAGE_KEYS.lastAgentMode] || 'eino_single', + lastInstruction: data[STORAGE_KEYS.lastInstruction] || DEFAULTS.lastInstruction, + filterApiOnly: data[STORAGE_KEYS.filterApiOnly] !== false, + captureEnabled: data[STORAGE_KEYS.captureEnabled] !== false, + renderMarkdown: data[STORAGE_KEYS.renderMarkdown] !== false, + showDebugEvents: data[STORAGE_KEYS.showDebugEvents] === true, + }; +} + +async function saveConfig(partial) { + const localMap = {}; + if (partial.host != null) localMap[STORAGE_KEYS.host] = partial.host; + if (partial.port != null) localMap[STORAGE_KEYS.port] = partial.port; + if (partial.https != null) localMap[STORAGE_KEYS.https] = partial.https; + if (partial.lastProjectId != null) localMap[STORAGE_KEYS.lastProjectId] = partial.lastProjectId; + if (partial.lastRole != null) localMap[STORAGE_KEYS.lastRole] = partial.lastRole; + if (partial.lastAgentMode != null) localMap[STORAGE_KEYS.lastAgentMode] = partial.lastAgentMode; + if (partial.lastInstruction != null) localMap[STORAGE_KEYS.lastInstruction] = partial.lastInstruction; + if (partial.filterApiOnly != null) localMap[STORAGE_KEYS.filterApiOnly] = partial.filterApiOnly; + if (partial.captureEnabled != null) localMap[STORAGE_KEYS.captureEnabled] = partial.captureEnabled; + if (partial.renderMarkdown != null) localMap[STORAGE_KEYS.renderMarkdown] = partial.renderMarkdown; + if (partial.showDebugEvents != null) localMap[STORAGE_KEYS.showDebugEvents] = partial.showDebugEvents; + if (Object.keys(localMap).length) await localSet(localMap); + if (partial.token != null) await sessionSet({ [SESSION_TOKEN_KEY]: partial.token }); +} + +function baseUrlFrom(cfg) { + const scheme = cfg.https ? 'https' : 'http'; + return `${scheme}://${cfg.host}:${cfg.port}`; +} + +/** Request optional host permission for the configured CyberStrikeAI origin. */ +async function ensureHostPermission(baseUrl) { + if (!chrome.permissions || !chrome.permissions.request) return; + let origin; + try { + origin = new URL(baseUrl).origin + '/*'; + } catch (_) { + throw new Error('Invalid Host/Port'); + } + const has = await chrome.permissions.contains({ origins: [origin] }); + if (has) return; + const granted = await chrome.permissions.request({ origins: [origin] }); + if (!granted) { + throw new Error('需要授权访问 CyberStrikeAI 服务器地址'); + } +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json b/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json new file mode 100644 index 00000000..fbe2bfa3 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/manifest.json @@ -0,0 +1,21 @@ +{ + "manifest_version": 3, + "name": "CyberStrikeAI", + "version": "0.3.5", + "description": "Capture browser HTTP traffic and send to CyberStrikeAI for AI-assisted security testing.", + "devtools_page": "devtools.html", + "permissions": ["storage"], + "optional_host_permissions": ["http://*/*", "https://*/*"], + "background": { + "service_worker": "background/service-worker.js" + }, + "action": { + "default_popup": "popup/popup.html", + "default_title": "CyberStrikeAI" + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/package.sh b/plugins/browser-extension/cyberstrikeai-browser-extension/package.sh new file mode 100644 index 00000000..6282a4b3 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/package.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +OUT="$ROOT/dist/cyberstrikeai-browser-extension.zip" +mkdir -p "$ROOT/dist" +rm -f "$OUT" +(cd "$ROOT" && zip -r "$OUT" . \ + -x './dist/*' -x './package.sh' -x '*/.DS_Store') +echo "[+] $OUT" diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.css b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.css new file mode 100644 index 00000000..5b2d332a --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.css @@ -0,0 +1,867 @@ +/* CyberStrikeAI DevTools panel — light default, dark via prefers-color-scheme */ + +:root { + --csai-accent: #ea580c; + --csai-accent-hover: #c2410c; + --csai-accent-soft: rgba(234, 88, 12, 0.12); + --csai-accent-ring: rgba(234, 88, 12, 0.28); + + --csai-bg: #f4f5f7; + --csai-surface: #ffffff; + --csai-surface-2: #f8f9fb; + --csai-surface-3: #eef0f4; + --csai-border: #e2e5eb; + --csai-border-strong: #cdd2dc; + + --csai-text: #1a1d24; + --csai-text-muted: #5c6470; + --csai-text-faint: #8b939e; + + --csai-success: #16a34a; + --csai-success-bg: rgba(22, 163, 74, 0.1); + --csai-danger: #dc2626; + --csai-danger-bg: rgba(220, 38, 38, 0.1); + --csai-info: #2563eb; + --csai-warn: #d97706; + + --csai-radius: 8px; + --csai-radius-sm: 6px; + --csai-shadow: 0 1px 2px rgba(16, 24, 40, 0.06); + --csai-shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08); + + --csai-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --csai-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; + + --csai-control-h: 30px; + --csai-bar-h: 40px; +} + +@media (prefers-color-scheme: dark) { + :root { + --csai-bg: #1a1d23; + --csai-surface: #22262e; + --csai-surface-2: #2a2f38; + --csai-surface-3: #323844; + --csai-border: #3a404c; + --csai-border-strong: #4a5160; + + --csai-text: #e8eaed; + --csai-text-muted: #9aa3af; + --csai-text-faint: #6b7280; + + --csai-accent-soft: rgba(251, 146, 60, 0.15); + --csai-accent-ring: rgba(251, 146, 60, 0.35); + --csai-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + --csai-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.35); + } +} + +* { box-sizing: border-box; } + +.ctx-banner { + padding: 8px 12px; + background: #fef3c7; + border-bottom: 1px solid #f59e0b; + color: #92400e; + font-size: 12px; + font-weight: 500; + text-align: center; + flex-shrink: 0; +} + +.ctx-banner.hidden { display: none; } + +@media (prefers-color-scheme: dark) { + .ctx-banner { + background: rgba(245, 158, 11, 0.15); + color: #fbbf24; + border-color: rgba(245, 158, 11, 0.35); + } +} + +body { + margin: 0; + font: 12px/1.45 var(--csai-font); + color: var(--csai-text); + background: var(--csai-bg); + height: 100vh; + display: flex; + flex-direction: column; +} + +/* ── Toolbar ── */ +.toolbar { + background: var(--csai-surface); + border-bottom: 1px solid var(--csai-border); + padding: 8px 12px; + display: flex; + flex-direction: column; + gap: 8px; + box-shadow: var(--csai-shadow); +} + +.toolbar-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + min-height: var(--csai-control-h); +} + +.toolbar-row--conn { + gap: 10px; + flex: 1; + min-width: 0; +} + +.toolbar--conn-collapsed .conn-details { + display: none; +} + +.toolbar:not(.toolbar--conn-collapsed) .conn-summary { + display: none; +} + +.conn-summary { + flex: 1; + min-width: 0; + padding: 0 10px; + height: var(--csai-control-h); + display: inline-flex; + align-items: center; + font-size: 11px; + font-weight: 600; + font-family: var(--csai-mono); + color: var(--csai-text-muted); + background: var(--csai-surface-2); + border: 1px solid var(--csai-border); + border-radius: var(--csai-radius-sm); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conn-summary--ok { + color: var(--csai-success); + background: var(--csai-success-bg); + border-color: rgba(22, 163, 74, 0.25); +} + +.conn-details { + padding-bottom: 2px; +} + +.conn-details .conn-group { + flex: 1; +} + +#btn-conn-toggle { + flex-shrink: 0; +} + +.toolbar-row--top { gap: 12px; } + +.toolbar-row--bottom { + justify-content: space-between; + align-items: center; +} + +.brand { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + height: var(--csai-control-h); + padding-right: 12px; + border-right: 1px solid var(--csai-border); +} + +.brand-logo { + width: 24px; + height: 24px; + border-radius: var(--csai-radius-sm); + box-shadow: var(--csai-shadow); +} + +.brand-name { + font-size: 13px; + font-weight: 700; + letter-spacing: -0.02em; + color: var(--csai-text); + line-height: 1; +} + +.conn-group, +.action-group, +.option-group { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.conn-group { flex: 1; min-width: 0; } + +.conn-actions { + display: inline-flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.action-divider { + width: 1px; + height: 18px; + background: var(--csai-border); + flex-shrink: 0; +} + +/* Toolbar: inline label + control on one baseline */ +.toolbar .field { + flex-direction: row; + align-items: center; + gap: 6px; +} + +.toolbar .field-label { + font-size: 11px; + font-weight: 600; + text-transform: none; + letter-spacing: 0; + color: var(--csai-text-muted); + line-height: 1; + white-space: nowrap; +} + +/* ── Form fields ── */ +.field { + display: inline-flex; + flex-direction: column; + gap: 2px; +} + +.field.block { + display: flex; + width: 100%; +} + +.field-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--csai-text-faint); +} + +.field input[type="text"], +.field input[type="password"], +.field select, +.field textarea, +#search { + height: var(--csai-control-h); + padding: 0 10px; + border: 1px solid var(--csai-border); + border-radius: var(--csai-radius-sm); + background: var(--csai-surface-2); + color: var(--csai-text); + font: inherit; + line-height: normal; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.field textarea { + height: auto; + min-height: 120px; + padding: 8px 10px; +} + +.field input:focus, +.field select:focus, +.field textarea:focus, +#search:focus { + outline: none; + border-color: var(--csai-accent); + box-shadow: 0 0 0 3px var(--csai-accent-ring); +} + +.field--sm input { width: 64px; } + +#host { width: 120px; } +#password { width: 100px; } + +.search-wrap { + padding: 8px 10px 4px; +} + +#search { + width: 100%; + background: var(--csai-surface-2); +} + +#search::placeholder { color: var(--csai-text-faint); } + +/* ── Toggle pills ── */ +.toggle-pill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: var(--csai-control-h); + padding: 0 12px; + border-radius: var(--csai-radius-sm); + border: 1px solid var(--csai-border); + background: var(--csai-surface-2); + cursor: pointer; + font-size: 11px; + line-height: 1; + color: var(--csai-text-muted); + user-select: none; + transition: background 0.15s, border-color 0.15s, color 0.15s; + flex-shrink: 0; +} + +.toggle-pill:hover { + border-color: var(--csai-border-strong); + color: var(--csai-text); +} + +.toggle-pill:has(input:checked) { + background: var(--csai-accent-soft); + border-color: var(--csai-accent); + color: var(--csai-accent); + font-weight: 600; +} + +.toggle-pill input { margin: 0; accent-color: var(--csai-accent); } + +/* ── Buttons ── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--csai-control-h); + padding: 0 14px; + border-radius: var(--csai-radius-sm); + border: 1px solid transparent; + font: inherit; + font-size: 12px; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, transform 0.1s, opacity 0.15s; + white-space: nowrap; + flex-shrink: 0; +} + +.btn:active:not(:disabled) { transform: scale(0.98); } +.btn:disabled { opacity: 0.45; cursor: not-allowed; } + +.btn--primary { + background: var(--csai-accent); + color: #fff; + border-color: var(--csai-accent); +} + +.btn--primary:hover:not(:disabled) { + background: var(--csai-accent-hover); + border-color: var(--csai-accent-hover); +} + +.btn--secondary { + background: var(--csai-surface-2); + color: var(--csai-text); + border-color: var(--csai-border); +} + +.btn--secondary:hover:not(:disabled) { + background: var(--csai-surface-3); + border-color: var(--csai-border-strong); +} + +.btn--danger { + background: var(--csai-danger-bg); + color: var(--csai-danger); + border-color: rgba(220, 38, 38, 0.25); +} + +.btn--danger:hover:not(:disabled) { + background: rgba(220, 38, 38, 0.18); +} + +.btn--ghost { + background: transparent; + color: var(--csai-text-muted); + border-color: var(--csai-border); +} + +.btn--ghost:hover:not(:disabled) { + background: var(--csai-surface-2); + color: var(--csai-text); +} + +.btn--capture-on { + background: var(--csai-success-bg); + color: var(--csai-success); + border-color: rgba(22, 163, 74, 0.3); +} + +.btn--capture-on:hover:not(:disabled) { + background: rgba(22, 163, 74, 0.18); +} + +.btn--capture-off { + background: var(--csai-surface-2); + color: var(--csai-text-muted); + border-color: var(--csai-border); +} + +.btn--capture-off:hover:not(:disabled) { + background: var(--csai-surface-3); + color: var(--csai-text); +} + +.link-btn { + border: none; + background: none; + color: var(--csai-accent); + padding: 2px 6px; + font-size: 11px; + font-weight: 600; + cursor: pointer; + border-radius: var(--csai-radius-sm); +} + +.link-btn:hover { + background: var(--csai-accent-soft); +} + +/* ── Status badge ── */ +.status { + display: inline-flex; + align-items: center; + gap: 6px; + height: var(--csai-control-h); + padding: 0 12px; + border-radius: var(--csai-radius-sm); + font-size: 11px; + font-weight: 500; + line-height: 1; + color: var(--csai-text-muted); + background: var(--csai-surface-2); + border: 1px solid var(--csai-border); + flex-shrink: 0; + white-space: nowrap; +} + +.status::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--csai-text-faint); + flex-shrink: 0; +} + +.status-ok { + color: var(--csai-success); + background: var(--csai-success-bg); + border-color: rgba(22, 163, 74, 0.25); +} + +.status-ok::before { background: var(--csai-success); } + +.status-error { + color: var(--csai-danger); + background: var(--csai-danger-bg); + border-color: rgba(220, 38, 38, 0.25); +} + +.status-error::before { background: var(--csai-danger); } + +.status-pending { + color: var(--csai-info); + background: rgba(37, 99, 235, 0.1); + border-color: rgba(37, 99, 235, 0.25); +} + +.status-pending::before { + background: var(--csai-info); + animation: pulse 1.2s ease-in-out infinite; +} + +.status-warn { + color: var(--csai-warn); + background: rgba(217, 119, 6, 0.1); + border-color: rgba(217, 119, 6, 0.25); +} + +.status-warn::before { background: var(--csai-warn); } + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +/* ── Layout ── */ +.layout { + flex: 1; + display: flex; + min-height: 0; + padding: 10px; + gap: 10px; + align-items: stretch; +} + +.sidebar { + width: 300px; + flex-shrink: 0; + background: var(--csai-surface); + border: 1px solid var(--csai-border); + border-radius: var(--csai-radius); + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; + box-shadow: var(--csai-shadow); +} + +.sidebar-section { + display: flex; + flex-direction: column; + min-height: 0; + border-bottom: 1px solid var(--csai-border); + max-height: 42%; +} + +.sidebar-section.grow { + flex: 1; + max-height: none; + border-bottom: none; +} + +.sidebar-head { + display: flex; + justify-content: space-between; + align-items: center; + height: var(--csai-bar-h); + min-height: var(--csai-bar-h); + padding: 0 12px; + background: var(--csai-surface-2); + border-bottom: 1px solid var(--csai-border); + flex-shrink: 0; +} + +.sidebar-head strong { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--csai-text-muted); +} + +.capture-paused-hint { + font-size: 10px; + font-weight: 600; + color: var(--csai-warn); +} + +.sidebar-head .link-btn { + margin-left: auto; +} + +.sidebar.capture-paused .request-list { + opacity: 0.55; +} + +/* ── Lists ── */ +.run-list, +.request-list { + list-style: none; + margin: 0; + padding: 4px; + overflow-x: hidden; + overflow-y: auto; + flex: 1; + min-width: 0; +} + +.run-list li, +.request-list li { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 8px 10px; + margin-bottom: 2px; + border-radius: var(--csai-radius-sm); + cursor: pointer; + font-size: 11px; + line-height: 1.4; + border: 1px solid transparent; + transition: background 0.12s, border-color 0.12s; +} + +.run-list li:hover, +.request-list li:hover { + background: var(--csai-surface-2); +} + +.run-list li.selected, +.request-list li.selected { + background: var(--csai-accent-soft); + border-color: rgba(234, 88, 12, 0.35); +} + +.run-body { min-width: 0; flex: 1; } + +.run-title { + font-weight: 600; + color: var(--csai-text); + word-break: break-all; +} + +.run-list .meta { + color: var(--csai-text-faint); + font-size: 10px; + margin-top: 3px; +} + +.dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-top: 4px; + flex-shrink: 0; + box-shadow: 0 0 0 2px var(--csai-surface); +} + +.dot-running { + background: var(--csai-info); + box-shadow: 0 0 0 2px var(--csai-surface), 0 0 6px rgba(37, 99, 235, 0.5); +} + +.dot-done { background: var(--csai-success); } +.dot-error { background: var(--csai-danger); } +.dot-cancelled { background: var(--csai-warn); } + +.request-list .method { + font-weight: 700; + font-size: 10px; + padding: 1px 5px; + border-radius: 4px; + background: var(--csai-accent-soft); + color: var(--csai-accent); + margin-right: 6px; + flex-shrink: 0; +} + +.request-list .status-ok { + font-weight: 700; + font-size: 10px; + color: var(--csai-success); + margin-right: 4px; + flex-shrink: 0; + margin-top: 1px; +} + +.request-list .status-err { + font-weight: 700; + font-size: 10px; + color: var(--csai-danger); + margin-right: 4px; + flex-shrink: 0; + margin-top: 1px; +} + +.request-list li { + align-items: flex-start; + min-width: 0; +} + +.request-list .req-path { + flex: 1; + min-width: 0; + word-break: break-all; + overflow-wrap: anywhere; + line-height: 1.4; +} + +/* ── Main panel ── */ +.main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + background: var(--csai-surface); + border: 1px solid var(--csai-border); + border-radius: var(--csai-radius); + overflow: hidden; + box-shadow: var(--csai-shadow); +} + +.tabs { + display: flex; + align-items: flex-end; + gap: 2px; + height: var(--csai-bar-h); + min-height: var(--csai-bar-h); + padding: 0 10px; + background: var(--csai-surface-2); + border-bottom: 1px solid var(--csai-border); + flex-shrink: 0; + box-sizing: border-box; +} + +.tab { + border: none; + border-radius: var(--csai-radius-sm) var(--csai-radius-sm) 0 0; + background: transparent; + height: calc(var(--csai-bar-h) - 1px); + padding: 0 16px; + display: inline-flex; + align-items: center; + font: inherit; + font-size: 12px; + font-weight: 600; + line-height: 1; + color: var(--csai-text-muted); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: color 0.15s, background 0.15s; +} + +.tab:hover { + color: var(--csai-text); + background: var(--csai-surface); +} + +.tab.active { + color: var(--csai-accent); + background: var(--csai-surface); + border-bottom-color: var(--csai-accent); +} + +.tab-panel { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--csai-surface); +} + +.tab-panel.hidden { display: none; } + +.split { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.pane { + border-bottom: 1px solid var(--csai-border); + display: flex; + flex-direction: column; + min-height: 72px; + max-height: 38%; +} + +.pane.grow { + flex: 1; + max-height: none; + border-bottom: none; + position: relative; +} + +.pane-title { + padding: 6px 12px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--csai-text-faint); + background: var(--csai-surface-2); + border-bottom: 1px solid var(--csai-border); +} + +.log { + margin: 0; + padding: 12px; + overflow: auto; + flex: 1; + white-space: pre-wrap; + word-break: break-word; + font: 11px/1.55 var(--csai-mono); + color: var(--csai-text); + background: var(--csai-surface); +} + +.log.full { height: 100%; } + +.md-frame { + border: none; + width: 100%; + flex: 1; + background: var(--csai-surface); +} + +.hidden { display: none !important; } + +/* ── Dialog ── */ +#send-dialog { + border: 1px solid var(--csai-border); + border-radius: 12px; + padding: 0; + width: min(720px, 92vw); + background: var(--csai-surface); + color: var(--csai-text); + box-shadow: var(--csai-shadow-md); +} + +#send-dialog::backdrop { + background: rgba(0, 0, 0, 0.45); + backdrop-filter: blur(2px); +} + +#send-dialog form { padding: 20px; margin: 0; } + +.dialog-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.dialog-logo { + border-radius: var(--csai-radius-sm); + box-shadow: var(--csai-shadow); +} + +#send-dialog h3 { + margin: 0; + font-size: 16px; + font-weight: 700; + letter-spacing: -0.02em; +} + +.send-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 12px; + margin-bottom: 14px; +} + +#dlg-instruction { + resize: vertical; + font-family: var(--csai-font); + min-height: 120px; +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid var(--csai-border); +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html new file mode 100644 index 00000000..936d92c2 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.html @@ -0,0 +1,143 @@ + + + + + + CyberStrikeAI + + + + +
    +
    +
    + + CyberStrikeAI +
    + + + Not validated +
    +
    +
    + + + + + +
    +
    +
    +
    + + + + + + + + +
    +
    + + + +
    +
    +
    + +
    + + +
    +
    + + + +
    +
    +
    +
    +
    Progress
    +
    
    +          
    +
    +
    Final Response
    +
    
    +            
    +          
    +
    +
    + + +
    +
    + + +
    +
    + +

    发送到 CyberStrikeAI

    +
    +
    + + + +
    + +
    + + +
    +
    +
    + + + + + + + + + + + diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.js b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.js new file mode 100644 index 00000000..4ff7cb52 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/panel/panel.js @@ -0,0 +1,947 @@ +/* global chrome, loadConfig, saveConfig, baseUrlFrom, ensureHostPermission, + loginAndValidate, fetchCatalogCached, invalidateCatalogCache, streamTest, + cancelByConversationId, extractConversationId, AGENT_MODES, toPrompt, + defaultInstruction, markdownToHtml, CSAI_LIMITS */ + +let config = {}; +let entries = []; +let runs = []; +let selectedEntryId = null; +let selectedRunId = null; +let inspectedTabId = null; +let token = ''; +let bgPort = null; +let activeAbort = null; +let activeRunId = null; +let validateAbort = null; +let validating = false; +let captureEnabled = true; +let searchDebounceTimer = null; +let connectRetryTimer = null; +let outputFlushScheduled = false; +let pendingProgressRunId = null; +let pendingFinalRunId = null; +let finalRenderedLen = 0; +let markdownRenderToken = 0; + +const $ = (id) => document.getElementById(id); + +function extensionAlive() { + try { + return !!(typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id); + } catch (_) { + return false; + } +} + +function showContextBanner(show) { + const el = $('ctx-banner'); + if (el) el.classList.toggle('hidden', !show); +} + +function scheduleConnect(delayMs) { + if (connectRetryTimer) clearTimeout(connectRetryTimer); + connectRetryTimer = setTimeout(connectBackground, delayMs); +} + +function runtimeSendMessage(msg) { + if (!extensionAlive()) { + showContextBanner(true); + return Promise.reject(new Error('Extension context lost')); + } + return chrome.runtime.sendMessage(msg); +} + +function setStatus(text, kind) { + const el = $('status'); + el.textContent = text; + el.className = 'status' + (kind ? ' status-' + kind : ''); + updateConnSummary(); +} + +function connectionEndpointLabel() { + const scheme = $('https').checked ? 'https' : 'http'; + const host = ($('host').value || '').trim() || '127.0.0.1'; + const port = ($('port').value || '').trim() || '8080'; + return `${scheme}://${host}:${port}`; +} + +function setConnExpanded(expanded) { + const toolbar = $('toolbar'); + if (!toolbar) return; + toolbar.classList.toggle('toolbar--conn-collapsed', !expanded); + const btn = $('btn-conn-toggle'); + if (btn) { + btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'); + btn.textContent = expanded ? '收起' : '连接设置'; + } +} + +function updateConnSummary() { + const summary = $('conn-summary'); + if (!summary) return; + summary.textContent = connectionEndpointLabel(); + summary.classList.toggle('conn-summary--ok', !!token); +} + +function onConnToggle() { + const toolbar = $('toolbar'); + const collapsed = toolbar && toolbar.classList.contains('toolbar--conn-collapsed'); + setConnExpanded(!!collapsed); +} + +function updateCaptureToggleUi() { + const btn = $('btn-capture-toggle'); + if (btn) { + btn.classList.toggle('btn--capture-on', captureEnabled); + btn.classList.toggle('btn--capture-off', !captureEnabled); + btn.setAttribute('aria-pressed', captureEnabled ? 'true' : 'false'); + btn.textContent = captureEnabled ? '● 捕获中' : '○ 已暂停'; + } + const sidebar = $('sidebar'); + if (sidebar) sidebar.classList.toggle('capture-paused', !captureEnabled); + const hint = $('capture-paused-hint'); + if (hint) hint.classList.toggle('hidden', captureEnabled); +} + +async function syncCaptureEnabled() { + await saveConfig({ captureEnabled }); + try { + await runtimeSendMessage({ type: 'set-capture-enabled', enabled: captureEnabled }); + } catch (_) {} + updateCaptureToggleUi(); +} + +async function onCaptureToggle() { + captureEnabled = !captureEnabled; + await syncCaptureEnabled(); +} + +function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function truncateOutput(s, max) { + if (!s || s.length <= max) return s || ''; + return s.slice(0, max) + '\n\n… [output truncated at ' + max + ' chars]'; +} + +function selectedEntry() { + return entries.find((e) => e.id === selectedEntryId) || null; +} + +function selectedRun() { + return runs.find((r) => r.id === selectedRunId) || null; +} + +function statusDotClass(status) { + switch (status) { + case 'running': return 'dot-running'; + case 'done': return 'dot-done'; + case 'error': return 'dot-error'; + case 'cancelled': + case 'cancelling': return 'dot-cancelled'; + default: return ''; + } +} + +function createRun(entry, meta) { + const run = { + id: 'run_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), + title: entry ? entry.title : 'Unknown', + status: 'running', + meta: meta || '', + progress: '', + final: '', + entry: entry ? { ...entry } : null, + conversationId: '', + }; + runs.unshift(run); + if (runs.length > CSAI_LIMITS.MAX_RUNS) runs.length = CSAI_LIMITS.MAX_RUNS; + selectedRunId = run.id; + return run; +} + +function buildRunLi(r) { + const li = document.createElement('li'); + li.dataset.id = r.id; + if (r.id === selectedRunId) li.classList.add('selected'); + li.innerHTML = + `` + + `
    ` + + `
    ${escapeHtml(r.title)}
    ` + + `
    ${escapeHtml(r.meta)} · ${escapeHtml(r.status)}
    ` + + `
    `; + li.addEventListener('click', () => selectRun(r.id)); + return li; +} + +function renderRunList() { + const ul = $('run-list'); + ul.innerHTML = ''; + for (const r of runs) { + ul.appendChild(buildRunLi(r)); + } +} + +function prependRunItem(run) { + const ul = $('run-list'); + ul.insertBefore(buildRunLi(run), ul.firstChild); + while (ul.children.length > runs.length) { + ul.removeChild(ul.lastChild); + } +} + +function updateRunListItem(runId) { + const r = runs.find((x) => x.id === runId); + const ul = $('run-list'); + const li = ul.querySelector(`li[data-id="${CSS.escape(runId)}"]`); + if (!r || !li) { + renderRunList(); + return; + } + const selected = r.id === selectedRunId; + li.className = selected ? 'selected' : ''; + li.innerHTML = + `` + + `
    ` + + `
    ${escapeHtml(r.title)}
    ` + + `
    ${escapeHtml(r.meta)} · ${escapeHtml(r.status)}
    ` + + `
    `; + li.onclick = () => selectRun(r.id); +} + +function currentSearchQuery() { + return ($('search').value || '').trim().toLowerCase(); +} + +function entryMatchesSearch(e, q) { + return !q || (e.url || '').toLowerCase().includes(q) || (e.title || '').toLowerCase().includes(q); +} + +function buildRequestLi(e) { + const li = document.createElement('li'); + li.dataset.id = e.id; + if (e.id === selectedEntryId) li.classList.add('selected'); + const st = e.responseStatus || 0; + const stClass = st >= 200 && st < 400 ? 'status-ok' : 'status-err'; + li.innerHTML = + `${escapeHtml(e.method)}` + + `${st || '—'}` + + `${escapeHtml(e.title || e.url)}`; + li.addEventListener('click', () => selectEntry(e.id)); + return li; +} + +function renderRequestList() { + const q = currentSearchQuery(); + const ul = $('request-list'); + ul.innerHTML = ''; + const filtered = entries.filter((e) => entryMatchesSearch(e, q)); + for (const e of filtered) { + ul.appendChild(buildRequestLi(e)); + } + updateSendButtons(); +} + +function prependRequestEntry(entry) { + const q = currentSearchQuery(); + if (!entryMatchesSearch(entry, q)) return; + const ul = $('request-list'); + ul.insertBefore(buildRequestLi(entry), ul.firstChild); + while (ul.children.length > CSAI_LIMITS.MAX_CAPTURED) { + ul.removeChild(ul.lastChild); + } + updateSendButtons(); +} + +function updateRequestSelection() { + $('request-list').querySelectorAll('li').forEach((li) => { + li.classList.toggle('selected', li.dataset.id === selectedEntryId); + }); +} + +function updateSendButtons() { + const ok = !!token; + $('btn-send').disabled = !selectedEntryId || !ok; + $('btn-latest-xhr').disabled = !ok; +} + +function selectEntry(id) { + selectedEntryId = id; + const e = selectedEntry(); + updateRequestSelection(); + if (!e) return; + $('view-request').textContent = formatRawRequest(e); + $('view-response').textContent = formatRawResponse(e); +} + +function updateRunSelection(prevId, nextId) { + const ul = $('run-list'); + if (prevId && prevId !== nextId) { + const prevLi = ul.querySelector(`li[data-id="${CSS.escape(prevId)}"]`); + if (prevLi) prevLi.classList.remove('selected'); + } + if (nextId) { + const nextLi = ul.querySelector(`li[data-id="${CSS.escape(nextId)}"]`); + if (nextLi) nextLi.classList.add('selected'); + } +} + +function trimInactiveRunFinals(activeId) { + const max = CSAI_LIMITS.MAX_FINAL_ARCHIVE_CHARS; + for (const r of runs) { + if (r.id === activeId || r.status === 'running') continue; + if (r.final && r.final.length > max) { + r.final = r.final.slice(0, max) + '\n\n… [inactive run truncated at ' + max + ' chars]'; + } + } +} + +function selectRun(id) { + const prevId = selectedRunId; + selectedRunId = id; + if (prevId !== id) { + updateRunSelection(prevId, id); + trimInactiveRunFinals(id); + } + const r = selectedRun(); + if (!r) return; + flushOutputViewNow(); + refreshFinalView(r.final, { streaming: r.status === 'running' && activeRunId === r.id }); + if (r.entry) { + $('view-request').textContent = formatRawRequest(r.entry); + $('view-response').textContent = formatRawResponse(r.entry); + } +} + +function formatRawRequest(e) { + return formatRequestDisplay(e); +} + +function formatRawResponse(e) { + return formatResponseDisplay(e); +} + +function resetFinalDom(finalText) { + const raw = $('final-raw'); + raw.textContent = finalText || ''; + finalRenderedLen = (finalText || '').length; +} + +function appendFinalDom(delta) { + if (!delta) return; + const raw = $('final-raw'); + raw.append(document.createTextNode(delta)); + finalRenderedLen += delta.length; + raw.scrollTop = raw.scrollHeight; +} + +function refreshFinalView(finalText, options) { + const opts = options || {}; + const streaming = !!opts.streaming; + const text = finalText || ''; + const raw = $('final-raw'); + const md = $('final-md'); + const wantMd = + $('render-md').checked && + text.trim() && + text.length <= CSAI_LIMITS.MAX_MARKDOWN_CHARS; + + if (streaming) { + md.classList.add('hidden'); + raw.classList.remove('hidden'); + if (text.length < finalRenderedLen) { + resetFinalDom(text); + } else if (text.length > finalRenderedLen) { + appendFinalDom(text.slice(finalRenderedLen)); + } + return; + } + + resetFinalDom(text); + markdownRenderToken += 1; + const token = markdownRenderToken; + + if (wantMd) { + const renderMd = () => { + if (token !== markdownRenderToken) return; + md.classList.remove('hidden'); + raw.classList.add('hidden'); + md.srcdoc = markdownToHtml(text); + }; + if (typeof requestIdleCallback === 'function') { + requestIdleCallback(renderMd, { timeout: 500 }); + } else { + setTimeout(renderMd, 0); + } + return; + } + + md.classList.add('hidden'); + raw.classList.remove('hidden'); + if (text.length > CSAI_LIMITS.MAX_MARKDOWN_CHARS && $('render-md').checked) { + raw.textContent = + text + '\n\n… [Markdown disabled: content exceeds ' + CSAI_LIMITS.MAX_MARKDOWN_CHARS + ' chars]'; + } +} + +function finalizeFinalView(finalText) { + flushOutputViewNow(); + refreshFinalView(finalText, { streaming: false }); +} + +function scheduleOutputFlush() { + if (outputFlushScheduled) return; + outputFlushScheduled = true; + requestAnimationFrame(flushOutputView); +} + +function flushOutputView() { + outputFlushScheduled = false; + if (pendingProgressRunId != null) { + const rid = pendingProgressRunId; + pendingProgressRunId = null; + if (rid === selectedRunId) { + const r = runs.find((x) => x.id === rid); + if (r) { + const el = $('progress'); + el.textContent = r.progress; + el.scrollTop = el.scrollHeight; + } + } + } + if (pendingFinalRunId != null) { + const rid = pendingFinalRunId; + pendingFinalRunId = null; + if (rid === selectedRunId) { + const r = runs.find((x) => x.id === rid); + if (r) { + refreshFinalView(r.final, { streaming: isRunStreaming(rid) }); + } + } + } +} + +function flushOutputViewNow() { + outputFlushScheduled = false; + if (pendingProgressRunId != null || pendingFinalRunId != null) { + const p = pendingProgressRunId; + const f = pendingFinalRunId; + pendingProgressRunId = null; + pendingFinalRunId = null; + if (p === selectedRunId) { + const r = runs.find((x) => x.id === p); + if (r) { + $('progress').textContent = r.progress; + $('progress').scrollTop = $('progress').scrollHeight; + } + } + if (f === selectedRunId) { + const r = runs.find((x) => x.id === f); + if (r) { + refreshFinalView(r.final, { streaming: isRunStreaming(f) }); + } + } + } +} + +function isRunStreaming(runId) { + if (runId !== activeRunId) return false; + const r = runs.find((x) => x.id === runId); + return !!(r && r.status === 'running'); +} + +function appendToRun(runId, field, text) { + const r = runs.find((x) => x.id === runId); + if (!r || !text) return; + if (field === 'progress') { + r.progress = truncateOutput(r.progress + text, CSAI_LIMITS.MAX_PROGRESS_CHARS); + } else { + r[field] = (r[field] || '') + text; + } + if (runId === selectedRunId) { + if (field === 'progress') { + pendingProgressRunId = runId; + scheduleOutputFlush(); + } else if (field === 'final') { + pendingFinalRunId = runId; + scheduleOutputFlush(); + } + } +} + +function setRunStatus(runId, status) { + const r = runs.find((x) => x.id === runId); + if (!r) return; + if (r.status === status) return; + r.status = status; + updateRunListItem(runId); + if (runId === selectedRunId && status !== 'running') { + finalizeFinalView(r ? r.final : ''); + } +} + +function setRunConversationId(runId, cid) { + const r = runs.find((x) => x.id === runId); + if (r && cid) r.conversationId = cid; +} + +async function initConfig() { + config = await loadConfig(); + token = config.token || ''; + $('host').value = config.host; + $('port').value = config.port; + $('https').checked = config.https; + $('filter-api').checked = config.filterApiOnly; + captureEnabled = config.captureEnabled !== false; + $('render-md').checked = config.renderMarkdown; + $('debug-events').checked = config.showDebugEvents; + setStatus(token ? 'OK (token saved, session)' : 'Not validated', token ? 'ok' : ''); + setConnExpanded(!token); + updateConnSummary(); + updateCaptureToggleUi(); + syncCaptureEnabled().catch(() => {}); + updateSendButtons(); +} + +async function persistConnection() { + await saveConfig({ + host: $('host').value.trim(), + port: $('port').value.trim(), + https: $('https').checked, + filterApiOnly: $('filter-api').checked, + renderMarkdown: $('render-md').checked, + showDebugEvents: $('debug-events').checked, + }); +} + +async function onValidate() { + if (validating) { + validateAbort?.abort(); + validating = false; + $('btn-validate').textContent = 'Validate'; + setStatus('Cancelled', 'warn'); + return; + } + validating = true; + validateAbort = new AbortController(); + $('btn-validate').textContent = 'Cancel'; + setStatus('Validating...', 'pending'); + await persistConnection(); + config = await loadConfig(); + const baseUrl = baseUrlFrom(config); + const password = $('password').value; + try { + await ensureHostPermission(baseUrl); + token = await loginAndValidate(baseUrl, password, validateAbort.signal); + await saveConfig({ token }); + invalidateCatalogCache(); + setStatus('OK (token saved, session)', 'ok'); + setConnExpanded(false); + updateSendButtons(); + } catch (err) { + if (err.name === 'AbortError') { + setStatus('Cancelled', 'warn'); + } else { + token = ''; + await saveConfig({ token: '' }); + setStatus('Failed: ' + err.message, 'error'); + setConnExpanded(true); + } + updateSendButtons(); + } finally { + validating = false; + validateAbort = null; + $('btn-validate').textContent = 'Validate'; + } +} + +function fillAgentSelect(sel, selected) { + sel.innerHTML = ''; + for (const m of AGENT_MODES) { + const opt = document.createElement('option'); + opt.value = m.id; + opt.textContent = m.label; + sel.appendChild(opt); + } + sel.value = selected || 'eino_single'; +} + +function fillSelect(sel, items, value) { + sel.innerHTML = ''; + for (const item of items) { + const opt = document.createElement('option'); + opt.value = item.id; + opt.textContent = item.label; + sel.appendChild(opt); + } + const has = items.some((i) => i.id === value); + sel.value = has ? value : (items[0] && items[0].id) || ''; +} + +async function openSendDialog(entryOverride) { + const e = entryOverride || selectedEntry(); + if (!e || !token) return; + const dlg = $('send-dialog'); + config = await loadConfig(); + const baseUrl = baseUrlFrom(config); + + $('dlg-instruction').value = config.lastInstruction || defaultInstruction(); + fillAgentSelect($('dlg-agent'), config.lastAgentMode); + $('dlg-project').innerHTML = ''; + $('dlg-role').innerHTML = ''; + dlg.showModal(); + + try { + const { projects, roles } = await fetchCatalogCached(baseUrl, token); + fillSelect($('dlg-project'), projects, config.lastProjectId); + fillSelect($('dlg-role'), roles, config.lastRole); + } catch (err) { + fillSelect($('dlg-project'), [{ id: '', label: '(加载失败)' }], ''); + fillSelect($('dlg-role'), [{ id: '', label: '默认' }], ''); + } + + dlg.dataset.entryId = e.id; +} + +async function onSendConfirmed() { + const entryId = $('send-dialog').dataset.entryId; + const e = entries.find((x) => x.id === entryId) || selectedEntry(); + if (!e || !token) return; + + const instruction = $('dlg-instruction').value.trim() || defaultInstruction(); + const projectId = $('dlg-project').value || ''; + const role = $('dlg-role').value || ''; + const agentMode = $('dlg-agent').value || 'eino_single'; + + await saveConfig({ + lastInstruction: instruction, + lastProjectId: projectId, + lastRole: role, + lastAgentMode: agentMode, + }); + + const modeLabel = (AGENT_MODES.find((m) => m.id === agentMode) || {}).label || agentMode; + const roleLabel = role || '默认'; + const run = createRun(e, modeLabel + ' · ' + roleLabel); + activeRunId = run.id; + prependRunItem(run); + finalRenderedLen = 0; + markdownRenderToken += 1; + selectRun(run.id); + + const baseUrl = baseUrlFrom(await loadConfig()); + const prompt = toPrompt(e, instruction); + appendToRun(run.id, 'progress', `[*] ${e.title}\n[server] ${baseUrl}\n[mode] ${modeLabel}\n`); + if (projectId) appendToRun(run.id, 'progress', `[project] ${projectId}\n`); + appendToRun(run.id, 'progress', '\n'); + + try { + await streamTest( + baseUrl, + token, + { message: prompt, role, projectId, agentMode }, + { + setAbortController(ctrl) { + activeAbort = ctrl; + }, + onEvent(type, message, ev) { + handleStreamEvent(run.id, type, message, ev); + }, + onDone() { + appendToRun(run.id, 'progress', '\n[done]\n'); + setRunStatus(run.id, 'done'); + activeAbort = null; + activeRunId = null; + }, + } + ); + } catch (err) { + if (err.name === 'AbortError') { + appendToRun(run.id, 'progress', '\n[info] Local stream stopped.\n'); + setRunStatus(run.id, 'cancelled'); + } else { + appendToRun(run.id, 'progress', '\n[error] ' + err.message + '\n'); + setRunStatus(run.id, 'error'); + } + activeAbort = null; + activeRunId = null; + } +} + +function handleStreamEvent(runId, type, message, ev) { + const debug = $('debug-events').checked; + switch (type) { + case 'response_start': + appendToRun(runId, 'progress', '\n\n[主回复]\n'); + break; + case 'response_delta': + if (message) appendToRun(runId, 'final', message); + break; + case 'response': + if (message) appendToRun(runId, 'final', message); + break; + case 'eino_agent_reply_stream_start': + appendToRun(runId, 'progress', '\n\n[子代理回复]\n'); + break; + case 'eino_agent_reply_stream_delta': + if (message) appendToRun(runId, 'progress', message); + break; + case 'progress': + appendToRun(runId, 'progress', '\n[progress] ' + message + '\n'); + setRunStatus(runId, 'running'); + break; + case 'error': + appendToRun(runId, 'progress', '\n[error] ' + message + '\n'); + setRunStatus(runId, 'error'); + break; + case 'cancelled': + appendToRun(runId, 'progress', '\n[cancelled] ' + message + '\n'); + setRunStatus(runId, 'cancelled'); + break; + case 'conversation': + case 'message_saved': { + const cid = extractConversationId(ev); + if (cid) setRunConversationId(runId, cid); + if (type === 'conversation' && debug && message) { + appendToRun(runId, 'progress', '\n[conversation] ' + message + '\n'); + } + break; + } + case 'tool_call': + case 'tool_result': + case 'tool_result_delta': + if (debug && message) appendToRun(runId, 'progress', '\n[' + type + '] ' + message + '\n'); + break; + default: + if (debug && message && !type.endsWith('_stream_delta') && !type.endsWith('_stream_start')) { + appendToRun(runId, 'progress', '\n[' + type + '] ' + message + '\n'); + } + break; + } +} + +async function onStop() { + const runId = activeRunId || selectedRunId; + const run = runs.find((x) => x.id === runId); + if (!run) return; + + if (activeAbort) { + activeAbort.abort(); + activeAbort = null; + } + + setRunStatus(run.id, 'cancelling'); + appendToRun(run.id, 'progress', '\n[info] Stop requested (local stream + server cancel).\n'); + + const cid = run.conversationId; + if (!cid || !token) { + if (!cid) { + appendToRun(run.id, 'progress', '[info] conversationId not ready yet; only local stream was stopped.\n'); + } + setRunStatus(run.id, 'cancelled'); + activeRunId = null; + return; + } + + const baseUrl = baseUrlFrom(await loadConfig()); + try { + await cancelByConversationId(baseUrl, token, cid); + appendToRun(run.id, 'progress', '[info] Server cancel acknowledged.\n'); + setRunStatus(run.id, 'cancelled'); + } catch (err) { + appendToRun(run.id, 'progress', '[error] Server cancel failed: ' + err.message + '\n'); + setRunStatus(run.id, 'cancelled'); + } + activeRunId = null; +} + +function clearCurrentRunOutput() { + pendingProgressRunId = null; + pendingFinalRunId = null; + outputFlushScheduled = false; + finalRenderedLen = 0; + markdownRenderToken += 1; + const r = selectedRun(); + if (r) { + r.progress = ''; + r.final = ''; + $('progress').textContent = ''; + refreshFinalView(''); + } else { + $('progress').textContent = ''; + refreshFinalView(''); + } +} + +function clearAllRuns() { + runs = []; + selectedRunId = null; + pendingProgressRunId = null; + pendingFinalRunId = null; + outputFlushScheduled = false; + finalRenderedLen = 0; + markdownRenderToken += 1; + renderRunList(); + $('progress').textContent = ''; + refreshFinalView(''); +} + +function connectBackground() { + connectRetryTimer = null; + + if (!extensionAlive()) { + showContextBanner(true); + bgPort = null; + return; + } + + showContextBanner(false); + + try { + if (!chrome.devtools || !chrome.devtools.inspectedWindow) { + scheduleConnect(1000); + return; + } + inspectedTabId = chrome.devtools.inspectedWindow.tabId; + bgPort = chrome.runtime.connect({ name: 'cyberstrike-panel' }); + } catch (_) { + if (!extensionAlive()) { + showContextBanner(true); + bgPort = null; + return; + } + scheduleConnect(1000); + return; + } + + bgPort.postMessage({ + type: 'subscribe', + tabId: inspectedTabId, + filterApiOnly: $('filter-api').checked, + }); + bgPort.onMessage.addListener((msg) => { + if (msg.type === 'list') { + entries = msg.entries || []; + renderRequestList(); + } else if (msg.type === 'entry') { + entries.unshift(msg.entry); + if (entries.length > CSAI_LIMITS.MAX_CAPTURED) entries.length = CSAI_LIMITS.MAX_CAPTURED; + prependRequestEntry(msg.entry); + } + }); + bgPort.onDisconnect.addListener(() => { + bgPort = null; + if (!extensionAlive()) { + showContextBanner(true); + return; + } + scheduleConnect(1000); + }); +} + +async function onFilterApiChange() { + await persistConnection(); + if (bgPort && inspectedTabId != null) { + bgPort.postMessage({ + type: 'subscribe', + tabId: inspectedTabId, + filterApiOnly: $('filter-api').checked, + }); + runtimeSendMessage({ + type: 'set-filter-api', + tabId: inspectedTabId, + filterApiOnly: $('filter-api').checked, + }).catch(() => {}); + } +} + +async function clearCaptures() { + if (inspectedTabId == null) return; + await runtimeSendMessage({ type: 'clear-captures', tabId: inspectedTabId }); + entries = []; + selectedEntryId = null; + renderRequestList(); +} + +async function sendLatestXhr() { + if (inspectedTabId == null || !token) return; + const resp = await runtimeSendMessage({ + type: 'get-latest-api', + tabId: inspectedTabId, + }); + if (resp && resp.entry) { + selectedEntryId = resp.entry.id; + selectEntry(resp.entry.id); + openSendDialog(resp.entry); + } +} + +function onSearchInput() { + clearTimeout(searchDebounceTimer); + searchDebounceTimer = setTimeout(renderRequestList, 150); +} + +function onCopyOutput() { + const activeTab = document.querySelector('.tab.active'); + const tabName = activeTab ? activeTab.dataset.tab : 'output'; + let text = ''; + if (tabName === 'request') text = $('view-request').textContent; + else if (tabName === 'response') text = $('view-response').textContent; + else { + const r = selectedRun(); + text = (r && r.final) ? r.final : $('final-raw').textContent; + } + navigator.clipboard.writeText(text || '').catch(() => {}); +} + +function setupTabs() { + document.querySelectorAll('.tab').forEach((btn) => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tab').forEach((b) => b.classList.remove('active')); + document.querySelectorAll('.tab-panel').forEach((p) => p.classList.add('hidden')); + btn.classList.add('active'); + $('tab-' + btn.dataset.tab).classList.remove('hidden'); + }); + }); +} + +$('btn-validate').addEventListener('click', onValidate); +$('btn-conn-toggle').addEventListener('click', onConnToggle); +$('host').addEventListener('input', updateConnSummary); +$('port').addEventListener('input', updateConnSummary); +$('https').addEventListener('change', updateConnSummary); +$('btn-send').addEventListener('click', () => openSendDialog()); +$('btn-latest-xhr').addEventListener('click', sendLatestXhr); +$('btn-stop').addEventListener('click', onStop); +$('btn-copy').addEventListener('click', onCopyOutput); +$('btn-clear').addEventListener('click', clearCurrentRunOutput); +$('btn-clear-runs').addEventListener('click', clearAllRuns); +$('btn-clear-captures').addEventListener('click', clearCaptures); +$('search').addEventListener('input', onSearchInput); +$('btn-capture-toggle').addEventListener('click', onCaptureToggle); +$('filter-api').addEventListener('change', onFilterApiChange); +$('render-md').addEventListener('change', () => { + const r = selectedRun(); + const text = r ? r.final : $('final-raw').textContent; + const streaming = r && r.status === 'running' && activeRunId === r.id; + refreshFinalView(text, { streaming }); + persistConnection(); +}); +$('debug-events').addEventListener('change', persistConnection); +$('send-form').addEventListener('submit', (ev) => { + ev.preventDefault(); + $('send-dialog').close(); + onSendConfirmed(); +}); +$('dlg-cancel').addEventListener('click', () => $('send-dialog').close()); + +setupTabs(); +initConfig().then(() => { + if (extensionAlive()) connectBackground(); + else showContextBanner(true); +}); diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.css b/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.css new file mode 100644 index 00000000..f35f4de1 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.css @@ -0,0 +1,94 @@ +body { + width: 300px; + margin: 0; + padding: 14px 16px; + font: 12px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + color: #1a1d24; + background: #fff; +} + +.popup-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.popup-header img { + border-radius: 8px; + flex-shrink: 0; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); +} + +h1 { + font-size: 14px; + font-weight: 700; + margin: 0; + line-height: 1.2; +} + +.version { + margin: 2px 0 0; + color: #6b7280; + font-size: 11px; +} + +.conn-status { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-radius: 8px; + font-size: 11px; + font-weight: 600; + margin-bottom: 10px; + border: 1px solid #e2e5eb; + background: #f8f9fb; + color: #5c6470; + word-break: break-all; +} + +.conn-status::before { + content: ''; + width: 7px; + height: 7px; + border-radius: 50%; + background: #8b939e; + flex-shrink: 0; +} + +.conn-status--ok { + color: #16a34a; + background: rgba(22, 163, 74, 0.1); + border-color: rgba(22, 163, 74, 0.25); +} + +.conn-status--ok::before { background: #16a34a; } + +.conn-status--idle::before { background: #d97706; } + +.hint { + margin: 0 0 10px; + color: #374151; + font-size: 12px; +} + +.tips { + margin: 0; + padding-left: 18px; + color: #4b5563; + font-size: 11px; +} + +.tips li { margin-bottom: 4px; } + +@media (prefers-color-scheme: dark) { + body { background: #22262e; color: #e8eaed; } + .version, .tips { color: #9aa3af; } + .hint { color: #d1d5db; } + .conn-status { + background: #2a2f38; + border-color: #3a404c; + color: #9aa3af; + } +} diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.html b/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.html new file mode 100644 index 00000000..77167ce0 --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.html @@ -0,0 +1,31 @@ + + + + + + + + + +
    加载中…
    + +

    主工作区在 DevTools 面板:按 F12CyberStrikeAI

    + +
      +
    • 连接配置与 Validate 请在 DevTools 面板完成
    • +
    • 默认只捕获 XHR/Fetch
    • +
    • Test History 可回看多次发送
    • +
    • Latest XHR 快速发送最近 API 请求
    • +
    + + + + + + diff --git a/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.js b/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.js new file mode 100644 index 00000000..28b76cee --- /dev/null +++ b/plugins/browser-extension/cyberstrikeai-browser-extension/popup/popup.js @@ -0,0 +1,28 @@ +chrome.action.setBadgeText({ text: '' }); + +async function renderPopup() { + const verEl = document.getElementById('version'); + if (verEl && chrome.runtime && chrome.runtime.getManifest) { + verEl.textContent = 'v' + chrome.runtime.getManifest().version; + } + + const statusEl = document.getElementById('conn-status'); + if (!statusEl || typeof loadConfig !== 'function') return; + + try { + const cfg = await loadConfig(); + const endpoint = baseUrlFrom(cfg); + if (cfg.token) { + statusEl.className = 'conn-status conn-status--ok'; + statusEl.textContent = `已连接 ${endpoint}`; + } else { + statusEl.className = 'conn-status conn-status--idle'; + statusEl.textContent = `未验证 · 目标 ${endpoint}`; + } + } catch (_) { + statusEl.className = 'conn-status conn-status--idle'; + statusEl.textContent = '无法读取配置'; + } +} + +renderPopup();