mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-10 14:28:43 +02:00
Add files via upload
This commit is contained in:
+2
-1
@@ -11,7 +11,7 @@ Optional integrations that connect CyberStrikeAI with other tools.
|
|||||||
### Browser (Chrome / Edge)
|
### Browser (Chrome / Edge)
|
||||||
|
|
||||||
- **Path**: `plugins/browser-extension/cyberstrikeai-browser-extension/`
|
- **Path**: `plugins/browser-extension/cyberstrikeai-browser-extension/`
|
||||||
- **Version**: **0.3.5**
|
- **Version**: **0.3.8**
|
||||||
- **Install**: `chrome://extensions/` → Load unpacked → F12 → **CyberStrikeAI** tab
|
- **Install**: `chrome://extensions/` → Load unpacked → F12 → **CyberStrikeAI** tab
|
||||||
- **Package**: `bash package.sh` → `dist/cyberstrikeai-browser-extension.zip`
|
- **Package**: `bash package.sh` → `dist/cyberstrikeai-browser-extension.zip`
|
||||||
- **Docs**: `README.zh-CN.md` (full) / `README.md` (summary)
|
- **Docs**: `README.zh-CN.md` (full) / `README.md` (summary)
|
||||||
@@ -20,6 +20,7 @@ Optional integrations that connect CyberStrikeAI with other tools.
|
|||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
|
| Token expiry | Remaining time + 30s validate probe; detects server restart / unreachable |
|
||||||
| Capture pause | **● Capturing** / **○ Paused** — stop recording without closing DevTools |
|
| 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) |
|
| 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 |
|
| Collapsible conn bar | Host/Port/Validate collapses after success |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## CyberStrikeAI Browser Extension
|
## CyberStrikeAI Browser Extension
|
||||||
|
|
||||||
**Version 0.3.5** — Full docs: **README.zh-CN.md**
|
**Version 0.3.8** — 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.
|
Chromium DevTools extension: capture Network traffic and send it to CyberStrikeAI for AI-assisted security testing. Aligned with the Burp Suite plugin.
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Chromium DevTools extension: capture Network traffic and send it to CyberStrikeA
|
|||||||
- **Deferred Markdown** — plain text while streaming; render after done; skip above 100KB
|
- **Deferred Markdown** — plain text while streaming; render after done; skip above 100KB
|
||||||
- **Stop** — abort local stream + server cancel via `conversationId`
|
- **Stop** — abort local stream + server cancel via `conversationId`
|
||||||
- **Latest XHR**, **Copy**, project/role/agent send dialog
|
- **Latest XHR**, **Copy**, project/role/agent send dialog
|
||||||
- Session token + optional host permissions
|
- Session token with **expires_at** tracking, 30s server probe, restart/unreachable detection
|
||||||
|
|
||||||
### Data limits (no unbounded growth)
|
### Data limits (no unbounded growth)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
## CyberStrikeAI 浏览器扩展
|
## CyberStrikeAI 浏览器扩展
|
||||||
|
|
||||||
**当前版本:0.3.5**
|
**当前版本:0.3.8**(UI 为英文;中文说明见下文)
|
||||||
|
|
||||||
Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Network** 流量,发送到 CyberStrikeAI 进行 AI 辅助安全测试。能力与 Burp Suite 插件对齐,并按生产场景做了性能与体验优化。
|
Chrome / Edge(Chromium)DevTools 扩展:在开发者工具中捕获 **Network** 流量,发送到 CyberStrikeAI 进行 AI 辅助安全测试。能力与 Burp Suite 插件对齐,并按生产场景做了性能与体验优化。
|
||||||
|
|
||||||
@@ -81,8 +81,13 @@ Cookie: ...
|
|||||||
#### 安全与权限
|
#### 安全与权限
|
||||||
|
|
||||||
- Token 存 **chrome.storage.session**(关浏览器失效)
|
- Token 存 **chrome.storage.session**(关浏览器失效)
|
||||||
- **optional_host_permissions**:Validate 时按需授权 CyberStrikeAI 地址
|
- 登录后保存 **`expires_at`**,状态栏显示 **剩余时间**(如 `OK · 剩余 11h 30m`)
|
||||||
- Markdown iframe 使用 `sandbox`
|
- **不会自动续期**:过期后需重新 Validate(需 Password)
|
||||||
|
- 本地过期检测(30s)+ 服务端 `/api/auth/validate` 探测(同周期;切回面板时立即探测)
|
||||||
|
- 服务不可达时显示 **无法连接服务**;重启后 Token 失效显示 **服务已重启或 Token 已失效**
|
||||||
|
- **401/403** 时自动清空 Token 并展开连接栏
|
||||||
|
- Send 前主动校验 Token 有效性
|
||||||
|
- **optional_host_permissions**:Validate 时按需授权
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -141,6 +146,12 @@ Cookie: ...
|
|||||||
**扩展更新后报错 `chrome.runtime.connect` undefined?**
|
**扩展更新后报错 `chrome.runtime.connect` undefined?**
|
||||||
扩展重载后旧 DevTools 面板上下文失效。请:**关闭 DevTools → 重新加载扩展 → 再开 F12**。
|
扩展重载后旧 DevTools 面板上下文失效。请:**关闭 DevTools → 重新加载扩展 → 再开 F12**。
|
||||||
|
|
||||||
|
**Token 过期会自动刷新吗?**
|
||||||
|
**不会自动续期**(无 refresh token)。插件会保存 `expires_at`、显示剩余时间;每 30s 向服务端校验,切回 DevTools 时立即校验。服务重启后 session 清空,会提示重新 Validate。
|
||||||
|
|
||||||
|
**重启服务后状态还显示 OK?**
|
||||||
|
v0.3.7 起每 30s 探测 `/api/auth/validate`;不可达显示黄色警告,Token 失效则清空并展开连接栏。重载扩展后请关闭 DevTools 再开 F12。
|
||||||
|
|
||||||
**Request 里为什么曾经有 `:authority`、`:method`?**
|
**Request 里为什么曾经有 `:authority`、`:method`?**
|
||||||
HTTP/2 伪首部。展示与 AI Prompt 已归一化为 HTTP/1.1;原始 HAR 仍保存在内存 entry 中。
|
HTTP/2 伪首部。展示与 AI Prompt 已归一化为 HTTP/1.1;原始 HAR 仍保存在内存 entry 中。
|
||||||
|
|
||||||
@@ -205,8 +216,9 @@ panel/
|
|||||||
popup/
|
popup/
|
||||||
popup.html / popup.js / popup.css # 只读状态
|
popup.html / popup.js / popup.css # 只读状态
|
||||||
lib/
|
lib/
|
||||||
|
auth-session.js # Token 过期检测与剩余时间提示
|
||||||
api.js # 登录、SSE、项目/角色 API
|
api.js # 登录、SSE、项目/角色 API
|
||||||
storage.js # 配置与 session token
|
storage.js # 配置 + session token + expires_at
|
||||||
capture.js # HAR 摘要、静态过滤
|
capture.js # HAR 摘要、静态过滤
|
||||||
http-normalize.js # HTTP/2 → HTTP/1.1 展示/Prompt
|
http-normalize.js # HTTP/2 → HTTP/1.1 展示/Prompt
|
||||||
formatter.js # toPrompt 组装
|
formatter.js # toPrompt 组装
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -10,7 +10,14 @@ function agentModeById(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function apiFetch(baseUrl, path, options = {}) {
|
async function apiFetch(baseUrl, path, options = {}) {
|
||||||
const res = await fetch(baseUrl + path, options);
|
let res;
|
||||||
|
try {
|
||||||
|
res = await fetch(baseUrl + path, options);
|
||||||
|
} catch (err) {
|
||||||
|
const e = err instanceof Error ? err : new Error(String(err));
|
||||||
|
e.network = true;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
let json = null;
|
let json = null;
|
||||||
try {
|
try {
|
||||||
@@ -19,8 +26,9 @@ async function apiFetch(baseUrl, path, options = {}) {
|
|||||||
json = null;
|
json = null;
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = (json && json.error) || text || `HTTP ${res.status}`;
|
const err = new Error((json && json.error) || text || `HTTP ${res.status}`);
|
||||||
throw new Error(err);
|
err.status = res.status;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
@@ -34,12 +42,19 @@ async function loginAndValidate(baseUrl, password, signal) {
|
|||||||
});
|
});
|
||||||
const token = login && login.token;
|
const token = login && login.token;
|
||||||
if (!token) throw new Error('Login response missing token');
|
if (!token) throw new Error('Login response missing token');
|
||||||
|
await validateTokenSession(baseUrl, token, signal);
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
expiresAt: (login && login.expires_at) || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateTokenSession(baseUrl, token, signal) {
|
||||||
await apiFetch(baseUrl, '/api/auth/validate', {
|
await apiFetch(baseUrl, '/api/auth/validate', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
return token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchProjects(baseUrl, token, signal) {
|
async function fetchProjects(baseUrl, token, signal) {
|
||||||
@@ -48,11 +63,11 @@ async function fetchProjects(baseUrl, token, signal) {
|
|||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
const list = (data && data.projects) || [];
|
const list = (data && data.projects) || [];
|
||||||
const out = [{ id: '', label: '(无)' }];
|
const out = [{ id: '', label: '(none)' }];
|
||||||
for (const p of list) {
|
for (const p of list) {
|
||||||
if (!p.id) continue;
|
if (!p.id) continue;
|
||||||
let label = p.name || p.id;
|
let label = p.name || p.id;
|
||||||
if (p.status === 'archived') label += ' [已归档]';
|
if (p.status === 'archived') label += ' [archived]';
|
||||||
out.push({ id: p.id, label });
|
out.push({ id: p.id, label });
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -64,7 +79,7 @@ async function fetchRoles(baseUrl, token, signal) {
|
|||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
const list = (data && data.roles) || [];
|
const list = (data && data.roles) || [];
|
||||||
const out = [{ id: '', label: '默认' }];
|
const out = [{ id: '', label: 'Default' }];
|
||||||
for (const r of list) {
|
for (const r of list) {
|
||||||
if (r.enabled === false) continue;
|
if (r.enabled === false) continue;
|
||||||
if (!r.name) continue;
|
if (!r.name) continue;
|
||||||
@@ -106,7 +121,9 @@ async function streamTest(baseUrl, token, options, handlers) {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const t = await res.text();
|
const t = await res.text();
|
||||||
throw new Error(t || `HTTP ${res.status}`);
|
const err = new Error(t || `HTTP ${res.status}`);
|
||||||
|
err.status = res.status;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/** Session token expiry helpers (aligned with web auth.js patterns). */
|
||||||
|
|
||||||
|
const SESSION_TOKEN_EXPIRY_KEY = 'csai_token_expires_at';
|
||||||
|
|
||||||
|
/** Warn when remaining session time is below this (ms). */
|
||||||
|
const TOKEN_WARN_BEFORE_MS = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
function parseExpiresAt(iso) {
|
||||||
|
if (!iso) return null;
|
||||||
|
const d = new Date(iso);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTokenExpiredByTime(expiresAtIso) {
|
||||||
|
const d = parseExpiresAt(expiresAtIso);
|
||||||
|
if (!d) return false;
|
||||||
|
return d.getTime() <= Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenExpiresWithin(expiresAtIso, withinMs) {
|
||||||
|
const d = parseExpiresAt(expiresAtIso);
|
||||||
|
if (!d) return false;
|
||||||
|
return d.getTime() - Date.now() <= withinMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTokenExpiryHint(expiresAtIso) {
|
||||||
|
const d = parseExpiresAt(expiresAtIso);
|
||||||
|
if (!d) return 'OK (token saved)';
|
||||||
|
const ms = d.getTime() - Date.now();
|
||||||
|
if (ms <= 0) return 'Session expired';
|
||||||
|
const h = Math.floor(ms / 3600000);
|
||||||
|
const m = Math.floor((ms % 3600000) / 60000);
|
||||||
|
if (h >= 1) return `OK · ${h}h ${m}m left`;
|
||||||
|
if (m >= 1) return `OK · ${m}m left`;
|
||||||
|
return 'OK · expiring soon';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthHttpStatus(status) {
|
||||||
|
return status === 401 || status === 403;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** fetch() failed before HTTP response (server down, connection refused, etc.). */
|
||||||
|
function isNetworkFetchError(err) {
|
||||||
|
if (!err) return false;
|
||||||
|
if (err.network === true) return true;
|
||||||
|
if (err.name === 'AbortError') return false;
|
||||||
|
const msg = String(err.message || err);
|
||||||
|
return err.name === 'TypeError' && /fetch|network|Failed to fetch/i.test(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachHttpStatus(err, status) {
|
||||||
|
if (err && typeof err === 'object') err.status = status;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
@@ -14,4 +14,4 @@ const CSAI_LIMITS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CSAI_DEFAULT_INSTRUCTION =
|
const CSAI_DEFAULT_INSTRUCTION =
|
||||||
'针对该流量做web渗透测试,并输出测试结果,要求:只针对该接口流量做测试,切勿拓展其他接口';
|
'Perform web penetration testing on this traffic and output results. Test only this endpoint; do not expand to other APIs.';
|
||||||
|
|||||||
@@ -27,42 +27,118 @@ const DEFAULTS = {
|
|||||||
showDebugEvents: false,
|
showDebugEvents: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function extensionContextAlive() {
|
||||||
|
try {
|
||||||
|
return !!(typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionContextError() {
|
||||||
|
const err = new Error('Extension context invalidated');
|
||||||
|
err.name = 'ExtensionContextError';
|
||||||
|
err.contextInvalidated = true;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExtensionContextError(err) {
|
||||||
|
if (!err) return false;
|
||||||
|
if (err.contextInvalidated || err.name === 'ExtensionContextError') return true;
|
||||||
|
return /extension context invalidated/i.test(String(err.message || err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeStorageError(err) {
|
||||||
|
if (isExtensionContextError(err)) return extensionContextError();
|
||||||
|
return err instanceof Error ? err : new Error(String(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectLastError(reject) {
|
||||||
|
const msg = chrome.runtime.lastError && chrome.runtime.lastError.message;
|
||||||
|
if (!msg) return false;
|
||||||
|
reject(/invalidated/i.test(msg) ? extensionContextError() : new Error(msg));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function localGet(keys) {
|
function localGet(keys) {
|
||||||
return new Promise((resolve) => chrome.storage.local.get(keys, resolve));
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!extensionContextAlive()) {
|
||||||
|
reject(extensionContextError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.storage.local.get(keys, (data) => {
|
||||||
|
if (rejectLastError(reject)) return;
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(normalizeStorageError(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function localSet(obj) {
|
function localSet(obj) {
|
||||||
return new Promise((resolve) => chrome.storage.local.set(obj, resolve));
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
if (!extensionContextAlive()) {
|
||||||
|
reject(extensionContextError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.storage.local.set(obj, () => {
|
||||||
|
if (rejectLastError(reject)) return;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(normalizeStorageError(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionGet(keys) {
|
function sessionGet(keys) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!chrome.storage.session) {
|
try {
|
||||||
chrome.storage.local.get(keys, resolve);
|
if (!extensionContextAlive()) {
|
||||||
return;
|
reject(extensionContextError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const store = chrome.storage.session || chrome.storage.local;
|
||||||
|
store.get(keys, (data) => {
|
||||||
|
if (rejectLastError(reject)) return;
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(normalizeStorageError(err));
|
||||||
}
|
}
|
||||||
chrome.storage.session.get(keys, resolve);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionSet(obj) {
|
function sessionSet(obj) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!chrome.storage.session) {
|
try {
|
||||||
chrome.storage.local.set(obj, resolve);
|
if (!extensionContextAlive()) {
|
||||||
return;
|
reject(extensionContextError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const store = chrome.storage.session || chrome.storage.local;
|
||||||
|
store.set(obj, () => {
|
||||||
|
if (rejectLastError(reject)) return;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
reject(normalizeStorageError(err));
|
||||||
}
|
}
|
||||||
chrome.storage.session.set(obj, resolve);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
const data = await localGet(Object.values(STORAGE_KEYS));
|
const data = await localGet(Object.values(STORAGE_KEYS));
|
||||||
const sess = await sessionGet([SESSION_TOKEN_KEY]);
|
const sess = await sessionGet([SESSION_TOKEN_KEY, SESSION_TOKEN_EXPIRY_KEY]);
|
||||||
return {
|
return {
|
||||||
host: data[STORAGE_KEYS.host] || DEFAULTS.host,
|
host: data[STORAGE_KEYS.host] || DEFAULTS.host,
|
||||||
port: data[STORAGE_KEYS.port] || DEFAULTS.port,
|
port: data[STORAGE_KEYS.port] || DEFAULTS.port,
|
||||||
https: data[STORAGE_KEYS.https] !== false,
|
https: data[STORAGE_KEYS.https] !== false,
|
||||||
token: sess[SESSION_TOKEN_KEY] || '',
|
token: sess[SESSION_TOKEN_KEY] || '',
|
||||||
|
tokenExpiresAt: sess[SESSION_TOKEN_EXPIRY_KEY] || '',
|
||||||
lastProjectId: data[STORAGE_KEYS.lastProjectId] || '',
|
lastProjectId: data[STORAGE_KEYS.lastProjectId] || '',
|
||||||
lastRole: data[STORAGE_KEYS.lastRole] || '',
|
lastRole: data[STORAGE_KEYS.lastRole] || '',
|
||||||
lastAgentMode: data[STORAGE_KEYS.lastAgentMode] || 'eino_single',
|
lastAgentMode: data[STORAGE_KEYS.lastAgentMode] || 'eino_single',
|
||||||
@@ -88,7 +164,10 @@ async function saveConfig(partial) {
|
|||||||
if (partial.renderMarkdown != null) localMap[STORAGE_KEYS.renderMarkdown] = partial.renderMarkdown;
|
if (partial.renderMarkdown != null) localMap[STORAGE_KEYS.renderMarkdown] = partial.renderMarkdown;
|
||||||
if (partial.showDebugEvents != null) localMap[STORAGE_KEYS.showDebugEvents] = partial.showDebugEvents;
|
if (partial.showDebugEvents != null) localMap[STORAGE_KEYS.showDebugEvents] = partial.showDebugEvents;
|
||||||
if (Object.keys(localMap).length) await localSet(localMap);
|
if (Object.keys(localMap).length) await localSet(localMap);
|
||||||
if (partial.token != null) await sessionSet({ [SESSION_TOKEN_KEY]: partial.token });
|
const sessionMap = {};
|
||||||
|
if (partial.token != null) sessionMap[SESSION_TOKEN_KEY] = partial.token;
|
||||||
|
if (partial.tokenExpiresAt != null) sessionMap[SESSION_TOKEN_EXPIRY_KEY] = partial.tokenExpiresAt;
|
||||||
|
if (Object.keys(sessionMap).length) await sessionSet(sessionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
function baseUrlFrom(cfg) {
|
function baseUrlFrom(cfg) {
|
||||||
@@ -98,6 +177,7 @@ function baseUrlFrom(cfg) {
|
|||||||
|
|
||||||
/** Request optional host permission for the configured CyberStrikeAI origin. */
|
/** Request optional host permission for the configured CyberStrikeAI origin. */
|
||||||
async function ensureHostPermission(baseUrl) {
|
async function ensureHostPermission(baseUrl) {
|
||||||
|
if (!extensionContextAlive()) throw extensionContextError();
|
||||||
if (!chrome.permissions || !chrome.permissions.request) return;
|
if (!chrome.permissions || !chrome.permissions.request) return;
|
||||||
let origin;
|
let origin;
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +189,6 @@ async function ensureHostPermission(baseUrl) {
|
|||||||
if (has) return;
|
if (has) return;
|
||||||
const granted = await chrome.permissions.request({ origins: [origin] });
|
const granted = await chrome.permissions.request({ origins: [origin] });
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
throw new Error('需要授权访问 CyberStrikeAI 服务器地址');
|
throw new Error('Permission required to access the CyberStrikeAI server');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "CyberStrikeAI",
|
"name": "CyberStrikeAI",
|
||||||
"version": "0.3.5",
|
"version": "0.3.9",
|
||||||
"description": "Capture browser HTTP traffic and send to CyberStrikeAI for AI-assisted security testing.",
|
"description": "Capture browser HTTP traffic and send to CyberStrikeAI for AI-assisted security testing.",
|
||||||
"devtools_page": "devtools.html",
|
"devtools_page": "devtools.html",
|
||||||
"permissions": ["storage"],
|
"permissions": ["storage"],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="ctx-banner" class="ctx-banner hidden" role="alert">
|
<div id="ctx-banner" class="ctx-banner hidden" role="alert">
|
||||||
扩展已更新或上下文失效,请关闭 DevTools 后重新打开(F12 → CyberStrikeAI 面板)。
|
Extension updated or context invalidated. Close DevTools and reopen (F12 → CyberStrikeAI panel).
|
||||||
</div>
|
</div>
|
||||||
<header class="toolbar" id="toolbar">
|
<header class="toolbar" id="toolbar">
|
||||||
<div class="toolbar-row toolbar-row--conn">
|
<div class="toolbar-row toolbar-row--conn">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<span class="brand-name">CyberStrikeAI</span>
|
<span class="brand-name">CyberStrikeAI</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="conn-summary" class="conn-summary"></span>
|
<span id="conn-summary" class="conn-summary"></span>
|
||||||
<button id="btn-conn-toggle" type="button" class="btn btn--ghost" aria-expanded="false">连接设置</button>
|
<button id="btn-conn-toggle" type="button" class="btn btn--ghost" aria-expanded="false">Connection</button>
|
||||||
<span id="status" class="status">Not validated</span>
|
<span id="status" class="status">Not validated</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="conn-details" class="conn-details">
|
<div id="conn-details" class="conn-details">
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<button id="btn-copy" type="button" class="btn btn--ghost">Copy</button>
|
<button id="btn-copy" type="button" class="btn btn--ghost">Copy</button>
|
||||||
<button id="btn-clear" type="button" class="btn btn--ghost">Clear Output</button>
|
<button id="btn-clear" type="button" class="btn btn--ghost">Clear Output</button>
|
||||||
<span class="action-divider"></span>
|
<span class="action-divider"></span>
|
||||||
<button id="btn-capture-toggle" type="button" class="btn btn--capture-on" aria-pressed="true">● 捕获中</button>
|
<button id="btn-capture-toggle" type="button" class="btn btn--capture-on" aria-pressed="true">● Capturing</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-group">
|
<div class="option-group">
|
||||||
<label class="toggle-pill"><input id="filter-api" type="checkbox" checked><span>XHR/Fetch only</span></label>
|
<label class="toggle-pill"><input id="filter-api" type="checkbox" checked><span>XHR/Fetch only</span></label>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<div class="sidebar-section grow">
|
<div class="sidebar-section grow">
|
||||||
<div class="sidebar-head">
|
<div class="sidebar-head">
|
||||||
<strong>Captured Requests</strong>
|
<strong>Captured Requests</strong>
|
||||||
<span id="capture-paused-hint" class="capture-paused-hint hidden">已暂停</span>
|
<span id="capture-paused-hint" class="capture-paused-hint hidden">Paused</span>
|
||||||
<button id="btn-clear-captures" type="button" class="link-btn">Clear</button>
|
<button id="btn-clear-captures" type="button" class="link-btn">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-wrap">
|
<div class="search-wrap">
|
||||||
@@ -114,24 +114,25 @@
|
|||||||
<form method="dialog" id="send-form">
|
<form method="dialog" id="send-form">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
<img src="../icons/icon48.png" alt="" width="32" height="32" class="dialog-logo">
|
<img src="../icons/icon48.png" alt="" width="32" height="32" class="dialog-logo">
|
||||||
<h3>发送到 CyberStrikeAI</h3>
|
<h3>Send to CyberStrikeAI</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="send-row">
|
<div class="send-row">
|
||||||
<label class="field block">项目 <select id="dlg-project"></select></label>
|
<label class="field block">Project <select id="dlg-project"></select></label>
|
||||||
<label class="field block">角色 <select id="dlg-role"></select></label>
|
<label class="field block">Role <select id="dlg-role"></select></label>
|
||||||
<label class="field block">对话 <select id="dlg-agent"></select></label>
|
<label class="field block">Agent <select id="dlg-agent"></select></label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field block">测试指令(可针对当前流量修改)
|
<label class="field block">Test instruction (editable for this request)
|
||||||
<textarea id="dlg-instruction" rows="6"></textarea>
|
<textarea id="dlg-instruction" rows="6"></textarea>
|
||||||
</label>
|
</label>
|
||||||
<div class="dialog-actions">
|
<div class="dialog-actions">
|
||||||
<button type="button" id="dlg-cancel" class="btn btn--ghost">取消</button>
|
<button type="button" id="dlg-cancel" class="btn btn--ghost">Cancel</button>
|
||||||
<button type="submit" value="ok" class="btn btn--primary">确定</button>
|
<button type="submit" value="ok" class="btn btn--primary">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script src="../lib/constants.js"></script>
|
<script src="../lib/constants.js"></script>
|
||||||
|
<script src="../lib/auth-session.js"></script>
|
||||||
<script src="../lib/storage.js"></script>
|
<script src="../lib/storage.js"></script>
|
||||||
<script src="../lib/http-normalize.js"></script>
|
<script src="../lib/http-normalize.js"></script>
|
||||||
<script src="../lib/formatter.js"></script>
|
<script src="../lib/formatter.js"></script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* global chrome, loadConfig, saveConfig, baseUrlFrom, ensureHostPermission,
|
/* global chrome, loadConfig, saveConfig, baseUrlFrom, ensureHostPermission,
|
||||||
loginAndValidate, fetchCatalogCached, invalidateCatalogCache, streamTest,
|
loginAndValidate, validateTokenSession, fetchCatalogCached, invalidateCatalogCache, streamTest,
|
||||||
cancelByConversationId, extractConversationId, AGENT_MODES, toPrompt,
|
cancelByConversationId, extractConversationId, AGENT_MODES, toPrompt,
|
||||||
defaultInstruction, markdownToHtml, CSAI_LIMITS */
|
defaultInstruction, markdownToHtml, CSAI_LIMITS, isExtensionContextError */
|
||||||
|
|
||||||
let config = {};
|
let config = {};
|
||||||
let entries = [];
|
let entries = [];
|
||||||
@@ -10,6 +10,7 @@ let selectedEntryId = null;
|
|||||||
let selectedRunId = null;
|
let selectedRunId = null;
|
||||||
let inspectedTabId = null;
|
let inspectedTabId = null;
|
||||||
let token = '';
|
let token = '';
|
||||||
|
let tokenExpiresAt = '';
|
||||||
let bgPort = null;
|
let bgPort = null;
|
||||||
let activeAbort = null;
|
let activeAbort = null;
|
||||||
let activeRunId = null;
|
let activeRunId = null;
|
||||||
@@ -18,6 +19,12 @@ let validating = false;
|
|||||||
let captureEnabled = true;
|
let captureEnabled = true;
|
||||||
let searchDebounceTimer = null;
|
let searchDebounceTimer = null;
|
||||||
let connectRetryTimer = null;
|
let connectRetryTimer = null;
|
||||||
|
let tokenCheckTimer = null;
|
||||||
|
let probeInFlight = null;
|
||||||
|
let lastProbeAt = 0;
|
||||||
|
|
||||||
|
const TOKEN_TICK_INTERVAL_MS = 30 * 1000;
|
||||||
|
const PROBE_MIN_GAP_MS = 5000;
|
||||||
let outputFlushScheduled = false;
|
let outputFlushScheduled = false;
|
||||||
let pendingProgressRunId = null;
|
let pendingProgressRunId = null;
|
||||||
let pendingFinalRunId = null;
|
let pendingFinalRunId = null;
|
||||||
@@ -34,6 +41,25 @@ function extensionAlive() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopOnContextLoss() {
|
||||||
|
if (tokenCheckTimer) {
|
||||||
|
clearInterval(tokenCheckTimer);
|
||||||
|
tokenCheckTimer = null;
|
||||||
|
}
|
||||||
|
if (connectRetryTimer) {
|
||||||
|
clearTimeout(connectRetryTimer);
|
||||||
|
connectRetryTimer = null;
|
||||||
|
}
|
||||||
|
bgPort = null;
|
||||||
|
showContextBanner(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onContextLoss(err) {
|
||||||
|
if (!isExtensionContextError(err)) return false;
|
||||||
|
stopOnContextLoss();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function showContextBanner(show) {
|
function showContextBanner(show) {
|
||||||
const el = $('ctx-banner');
|
const el = $('ctx-banner');
|
||||||
if (el) el.classList.toggle('hidden', !show);
|
if (el) el.classList.toggle('hidden', !show);
|
||||||
@@ -56,7 +82,7 @@ function setStatus(text, kind) {
|
|||||||
const el = $('status');
|
const el = $('status');
|
||||||
el.textContent = text;
|
el.textContent = text;
|
||||||
el.className = 'status' + (kind ? ' status-' + kind : '');
|
el.className = 'status' + (kind ? ' status-' + kind : '');
|
||||||
updateConnSummary();
|
if (kind !== 'error') updateConnSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionEndpointLabel() {
|
function connectionEndpointLabel() {
|
||||||
@@ -73,7 +99,7 @@ function setConnExpanded(expanded) {
|
|||||||
const btn = $('btn-conn-toggle');
|
const btn = $('btn-conn-toggle');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||||
btn.textContent = expanded ? '收起' : '连接设置';
|
btn.textContent = expanded ? 'Collapse' : 'Connection';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +107,129 @@ function updateConnSummary() {
|
|||||||
const summary = $('conn-summary');
|
const summary = $('conn-summary');
|
||||||
if (!summary) return;
|
if (!summary) return;
|
||||||
summary.textContent = connectionEndpointLabel();
|
summary.textContent = connectionEndpointLabel();
|
||||||
summary.classList.toggle('conn-summary--ok', !!token);
|
summary.classList.toggle('conn-summary--ok', !!token && !isTokenExpiredByTime(tokenExpiresAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshAuthStatus() {
|
||||||
|
if (!token) {
|
||||||
|
setStatus('Not validated', '');
|
||||||
|
updateConnSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isTokenExpiredByTime(tokenExpiresAt)) {
|
||||||
|
setStatus('Session expired — please Validate again', 'error');
|
||||||
|
updateConnSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hint = formatTokenExpiryHint(tokenExpiresAt);
|
||||||
|
const kind = tokenExpiresWithin(tokenExpiresAt, TOKEN_WARN_BEFORE_MS) ? 'warn' : 'ok';
|
||||||
|
setStatus(hint, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAuthSession(message) {
|
||||||
|
token = '';
|
||||||
|
tokenExpiresAt = '';
|
||||||
|
try {
|
||||||
|
await saveConfig({ token: '', tokenExpiresAt: '' });
|
||||||
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
invalidateCatalogCache();
|
||||||
|
setStatus(message || 'Not validated', message ? 'error' : '');
|
||||||
|
setConnExpanded(true);
|
||||||
|
updateConnSummary();
|
||||||
|
updateSendButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAuthFailure(err, fallbackMessage) {
|
||||||
|
if (err && isAuthHttpStatus(err.status)) {
|
||||||
|
await clearAuthSession(fallbackMessage || 'Token invalid or expired — please Validate again');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureAuthReady() {
|
||||||
|
if (!token) {
|
||||||
|
setStatus('Validate first', 'warn');
|
||||||
|
setConnExpanded(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isTokenExpiredByTime(tokenExpiresAt)) {
|
||||||
|
await clearAuthSession('Session expired — please Validate again');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await probeTokenOnServer(true);
|
||||||
|
return !!token;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTokenWatch() {
|
||||||
|
if (tokenCheckTimer) clearInterval(tokenCheckTimer);
|
||||||
|
tokenCheckTimer = setInterval(async () => {
|
||||||
|
if (!extensionAlive()) {
|
||||||
|
stopOnContextLoss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!token) return;
|
||||||
|
if (isTokenExpiredByTime(tokenExpiresAt)) {
|
||||||
|
await clearAuthSession('Session expired — please Validate again');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await probeTokenOnServer();
|
||||||
|
}, TOKEN_TICK_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function probeTokenOnServer(force) {
|
||||||
|
if (!extensionAlive()) {
|
||||||
|
stopOnContextLoss();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!token || isTokenExpiredByTime(tokenExpiresAt)) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (!force && now - lastProbeAt < PROBE_MIN_GAP_MS) return;
|
||||||
|
if (probeInFlight) return probeInFlight;
|
||||||
|
|
||||||
|
probeInFlight = (async () => {
|
||||||
|
try {
|
||||||
|
const baseUrl = baseUrlFrom(await loadConfig());
|
||||||
|
await validateTokenSession(baseUrl, token);
|
||||||
|
lastProbeAt = Date.now();
|
||||||
|
refreshAuthStatus();
|
||||||
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
|
lastProbeAt = Date.now();
|
||||||
|
if (await handleAuthFailure(err, 'Server restarted or token invalid — please Validate again')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isNetworkFetchError(err)) {
|
||||||
|
setStatus('Cannot reach server — confirm CyberStrikeAI is running', 'warn');
|
||||||
|
updateConnSummary();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus('Validation failed: ' + err.message, 'warn');
|
||||||
|
updateConnSummary();
|
||||||
|
} finally {
|
||||||
|
probeInFlight = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return probeInFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAuthProbeHooks() {
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible' && token) {
|
||||||
|
probeTokenOnServer(true).catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('focus', () => {
|
||||||
|
if (token) probeTokenOnServer(true).catch(() => {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConnToggle() {
|
function onConnToggle() {
|
||||||
@@ -96,7 +244,7 @@ function updateCaptureToggleUi() {
|
|||||||
btn.classList.toggle('btn--capture-on', captureEnabled);
|
btn.classList.toggle('btn--capture-on', captureEnabled);
|
||||||
btn.classList.toggle('btn--capture-off', !captureEnabled);
|
btn.classList.toggle('btn--capture-off', !captureEnabled);
|
||||||
btn.setAttribute('aria-pressed', captureEnabled ? 'true' : 'false');
|
btn.setAttribute('aria-pressed', captureEnabled ? 'true' : 'false');
|
||||||
btn.textContent = captureEnabled ? '● 捕获中' : '○ 已暂停';
|
btn.textContent = captureEnabled ? '● Capturing' : '○ Paused';
|
||||||
}
|
}
|
||||||
const sidebar = $('sidebar');
|
const sidebar = $('sidebar');
|
||||||
if (sidebar) sidebar.classList.toggle('capture-paused', !captureEnabled);
|
if (sidebar) sidebar.classList.toggle('capture-paused', !captureEnabled);
|
||||||
@@ -105,7 +253,12 @@ function updateCaptureToggleUi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function syncCaptureEnabled() {
|
async function syncCaptureEnabled() {
|
||||||
await saveConfig({ captureEnabled });
|
try {
|
||||||
|
await saveConfig({ captureEnabled });
|
||||||
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await runtimeSendMessage({ type: 'set-capture-enabled', enabled: captureEnabled });
|
await runtimeSendMessage({ type: 'set-capture-enabled', enabled: captureEnabled });
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -491,8 +644,14 @@ function setRunConversationId(runId, cid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initConfig() {
|
async function initConfig() {
|
||||||
config = await loadConfig();
|
try {
|
||||||
|
config = await loadConfig();
|
||||||
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
token = config.token || '';
|
token = config.token || '';
|
||||||
|
tokenExpiresAt = config.tokenExpiresAt || '';
|
||||||
$('host').value = config.host;
|
$('host').value = config.host;
|
||||||
$('port').value = config.port;
|
$('port').value = config.port;
|
||||||
$('https').checked = config.https;
|
$('https').checked = config.https;
|
||||||
@@ -500,23 +659,36 @@ async function initConfig() {
|
|||||||
captureEnabled = config.captureEnabled !== false;
|
captureEnabled = config.captureEnabled !== false;
|
||||||
$('render-md').checked = config.renderMarkdown;
|
$('render-md').checked = config.renderMarkdown;
|
||||||
$('debug-events').checked = config.showDebugEvents;
|
$('debug-events').checked = config.showDebugEvents;
|
||||||
setStatus(token ? 'OK (token saved, session)' : 'Not validated', token ? 'ok' : '');
|
|
||||||
|
if (token && isTokenExpiredByTime(tokenExpiresAt)) {
|
||||||
|
await clearAuthSession('Session expired — please Validate again');
|
||||||
|
} else {
|
||||||
|
refreshAuthStatus();
|
||||||
|
if (token) {
|
||||||
|
startTokenWatch();
|
||||||
|
probeTokenOnServer().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setConnExpanded(!token);
|
setConnExpanded(!token);
|
||||||
updateConnSummary();
|
|
||||||
updateCaptureToggleUi();
|
updateCaptureToggleUi();
|
||||||
syncCaptureEnabled().catch(() => {});
|
syncCaptureEnabled().catch(() => {});
|
||||||
updateSendButtons();
|
updateSendButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function persistConnection() {
|
async function persistConnection() {
|
||||||
await saveConfig({
|
try {
|
||||||
host: $('host').value.trim(),
|
await saveConfig({
|
||||||
port: $('port').value.trim(),
|
host: $('host').value.trim(),
|
||||||
https: $('https').checked,
|
port: $('port').value.trim(),
|
||||||
filterApiOnly: $('filter-api').checked,
|
https: $('https').checked,
|
||||||
renderMarkdown: $('render-md').checked,
|
filterApiOnly: $('filter-api').checked,
|
||||||
showDebugEvents: $('debug-events').checked,
|
renderMarkdown: $('render-md').checked,
|
||||||
});
|
showDebugEvents: $('debug-events').checked,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onContextLoss(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onValidate() {
|
async function onValidate() {
|
||||||
@@ -532,23 +704,37 @@ async function onValidate() {
|
|||||||
$('btn-validate').textContent = 'Cancel';
|
$('btn-validate').textContent = 'Cancel';
|
||||||
setStatus('Validating...', 'pending');
|
setStatus('Validating...', 'pending');
|
||||||
await persistConnection();
|
await persistConnection();
|
||||||
config = await loadConfig();
|
try {
|
||||||
|
config = await loadConfig();
|
||||||
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
const baseUrl = baseUrlFrom(config);
|
const baseUrl = baseUrlFrom(config);
|
||||||
const password = $('password').value;
|
const password = $('password').value;
|
||||||
try {
|
try {
|
||||||
await ensureHostPermission(baseUrl);
|
await ensureHostPermission(baseUrl);
|
||||||
token = await loginAndValidate(baseUrl, password, validateAbort.signal);
|
const auth = await loginAndValidate(baseUrl, password, validateAbort.signal);
|
||||||
await saveConfig({ token });
|
token = auth.token;
|
||||||
|
tokenExpiresAt = auth.expiresAt || '';
|
||||||
|
await saveConfig({ token, tokenExpiresAt });
|
||||||
invalidateCatalogCache();
|
invalidateCatalogCache();
|
||||||
setStatus('OK (token saved, session)', 'ok');
|
refreshAuthStatus();
|
||||||
|
startTokenWatch();
|
||||||
setConnExpanded(false);
|
setConnExpanded(false);
|
||||||
updateSendButtons();
|
updateSendButtons();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
if (err.name === 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
setStatus('Cancelled', 'warn');
|
setStatus('Cancelled', 'warn');
|
||||||
} else {
|
} else {
|
||||||
token = '';
|
token = '';
|
||||||
await saveConfig({ token: '' });
|
tokenExpiresAt = '';
|
||||||
|
try {
|
||||||
|
await saveConfig({ token: '', tokenExpiresAt: '' });
|
||||||
|
} catch (saveErr) {
|
||||||
|
onContextLoss(saveErr);
|
||||||
|
}
|
||||||
setStatus('Failed: ' + err.message, 'error');
|
setStatus('Failed: ' + err.message, 'error');
|
||||||
setConnExpanded(true);
|
setConnExpanded(true);
|
||||||
}
|
}
|
||||||
@@ -585,15 +771,16 @@ function fillSelect(sel, items, value) {
|
|||||||
|
|
||||||
async function openSendDialog(entryOverride) {
|
async function openSendDialog(entryOverride) {
|
||||||
const e = entryOverride || selectedEntry();
|
const e = entryOverride || selectedEntry();
|
||||||
if (!e || !token) return;
|
if (!e) return;
|
||||||
|
if (!(await ensureAuthReady())) return;
|
||||||
const dlg = $('send-dialog');
|
const dlg = $('send-dialog');
|
||||||
config = await loadConfig();
|
config = await loadConfig();
|
||||||
const baseUrl = baseUrlFrom(config);
|
const baseUrl = baseUrlFrom(config);
|
||||||
|
|
||||||
$('dlg-instruction').value = config.lastInstruction || defaultInstruction();
|
$('dlg-instruction').value = config.lastInstruction || defaultInstruction();
|
||||||
fillAgentSelect($('dlg-agent'), config.lastAgentMode);
|
fillAgentSelect($('dlg-agent'), config.lastAgentMode);
|
||||||
$('dlg-project').innerHTML = '<option>加载中...</option>';
|
$('dlg-project').innerHTML = '<option>Loading…</option>';
|
||||||
$('dlg-role').innerHTML = '<option>加载中...</option>';
|
$('dlg-role').innerHTML = '<option>Loading…</option>';
|
||||||
dlg.showModal();
|
dlg.showModal();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -601,8 +788,12 @@ async function openSendDialog(entryOverride) {
|
|||||||
fillSelect($('dlg-project'), projects, config.lastProjectId);
|
fillSelect($('dlg-project'), projects, config.lastProjectId);
|
||||||
fillSelect($('dlg-role'), roles, config.lastRole);
|
fillSelect($('dlg-role'), roles, config.lastRole);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fillSelect($('dlg-project'), [{ id: '', label: '(加载失败)' }], '');
|
if (await handleAuthFailure(err, 'Token invalid or expired — please Validate again')) {
|
||||||
fillSelect($('dlg-role'), [{ id: '', label: '默认' }], '');
|
dlg.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fillSelect($('dlg-project'), [{ id: '', label: '(load failed)' }], '');
|
||||||
|
fillSelect($('dlg-role'), [{ id: '', label: 'Default' }], '');
|
||||||
}
|
}
|
||||||
|
|
||||||
dlg.dataset.entryId = e.id;
|
dlg.dataset.entryId = e.id;
|
||||||
@@ -611,22 +802,27 @@ async function openSendDialog(entryOverride) {
|
|||||||
async function onSendConfirmed() {
|
async function onSendConfirmed() {
|
||||||
const entryId = $('send-dialog').dataset.entryId;
|
const entryId = $('send-dialog').dataset.entryId;
|
||||||
const e = entries.find((x) => x.id === entryId) || selectedEntry();
|
const e = entries.find((x) => x.id === entryId) || selectedEntry();
|
||||||
if (!e || !token) return;
|
if (!e || !(await ensureAuthReady())) return;
|
||||||
|
|
||||||
const instruction = $('dlg-instruction').value.trim() || defaultInstruction();
|
const instruction = $('dlg-instruction').value.trim() || defaultInstruction();
|
||||||
const projectId = $('dlg-project').value || '';
|
const projectId = $('dlg-project').value || '';
|
||||||
const role = $('dlg-role').value || '';
|
const role = $('dlg-role').value || '';
|
||||||
const agentMode = $('dlg-agent').value || 'eino_single';
|
const agentMode = $('dlg-agent').value || 'eino_single';
|
||||||
|
|
||||||
await saveConfig({
|
try {
|
||||||
lastInstruction: instruction,
|
await saveConfig({
|
||||||
lastProjectId: projectId,
|
lastInstruction: instruction,
|
||||||
lastRole: role,
|
lastProjectId: projectId,
|
||||||
lastAgentMode: agentMode,
|
lastRole: role,
|
||||||
});
|
lastAgentMode: agentMode,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (onContextLoss(err)) return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const modeLabel = (AGENT_MODES.find((m) => m.id === agentMode) || {}).label || agentMode;
|
const modeLabel = (AGENT_MODES.find((m) => m.id === agentMode) || {}).label || agentMode;
|
||||||
const roleLabel = role || '默认';
|
const roleLabel = role || 'Default';
|
||||||
const run = createRun(e, modeLabel + ' · ' + roleLabel);
|
const run = createRun(e, modeLabel + ' · ' + roleLabel);
|
||||||
activeRunId = run.id;
|
activeRunId = run.id;
|
||||||
prependRunItem(run);
|
prependRunItem(run);
|
||||||
@@ -665,6 +861,7 @@ async function onSendConfirmed() {
|
|||||||
appendToRun(run.id, 'progress', '\n[info] Local stream stopped.\n');
|
appendToRun(run.id, 'progress', '\n[info] Local stream stopped.\n');
|
||||||
setRunStatus(run.id, 'cancelled');
|
setRunStatus(run.id, 'cancelled');
|
||||||
} else {
|
} else {
|
||||||
|
await handleAuthFailure(err, 'Token invalid or expired — please Validate again');
|
||||||
appendToRun(run.id, 'progress', '\n[error] ' + err.message + '\n');
|
appendToRun(run.id, 'progress', '\n[error] ' + err.message + '\n');
|
||||||
setRunStatus(run.id, 'error');
|
setRunStatus(run.id, 'error');
|
||||||
}
|
}
|
||||||
@@ -677,7 +874,7 @@ function handleStreamEvent(runId, type, message, ev) {
|
|||||||
const debug = $('debug-events').checked;
|
const debug = $('debug-events').checked;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'response_start':
|
case 'response_start':
|
||||||
appendToRun(runId, 'progress', '\n\n[主回复]\n');
|
appendToRun(runId, 'progress', '\n\n[Main reply]\n');
|
||||||
break;
|
break;
|
||||||
case 'response_delta':
|
case 'response_delta':
|
||||||
if (message) appendToRun(runId, 'final', message);
|
if (message) appendToRun(runId, 'final', message);
|
||||||
@@ -686,7 +883,7 @@ function handleStreamEvent(runId, type, message, ev) {
|
|||||||
if (message) appendToRun(runId, 'final', message);
|
if (message) appendToRun(runId, 'final', message);
|
||||||
break;
|
break;
|
||||||
case 'eino_agent_reply_stream_start':
|
case 'eino_agent_reply_stream_start':
|
||||||
appendToRun(runId, 'progress', '\n\n[子代理回复]\n');
|
appendToRun(runId, 'progress', '\n\n[Sub-agent reply]\n');
|
||||||
break;
|
break;
|
||||||
case 'eino_agent_reply_stream_delta':
|
case 'eino_agent_reply_stream_delta':
|
||||||
if (message) appendToRun(runId, 'progress', message);
|
if (message) appendToRun(runId, 'progress', message);
|
||||||
@@ -869,7 +1066,8 @@ async function clearCaptures() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendLatestXhr() {
|
async function sendLatestXhr() {
|
||||||
if (inspectedTabId == null || !token) return;
|
if (inspectedTabId == null) return;
|
||||||
|
if (!(await ensureAuthReady())) return;
|
||||||
const resp = await runtimeSendMessage({
|
const resp = await runtimeSendMessage({
|
||||||
type: 'get-latest-api',
|
type: 'get-latest-api',
|
||||||
tabId: inspectedTabId,
|
tabId: inspectedTabId,
|
||||||
@@ -941,6 +1139,7 @@ $('send-form').addEventListener('submit', (ev) => {
|
|||||||
$('dlg-cancel').addEventListener('click', () => $('send-dialog').close());
|
$('dlg-cancel').addEventListener('click', () => $('send-dialog').close());
|
||||||
|
|
||||||
setupTabs();
|
setupTabs();
|
||||||
|
setupAuthProbeHooks();
|
||||||
initConfig().then(() => {
|
initConfig().then(() => {
|
||||||
if (extensionAlive()) connectBackground();
|
if (extensionAlive()) connectBackground();
|
||||||
else showContextBanner(true);
|
else showContextBanner(true);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="popup.css">
|
<link rel="stylesheet" href="popup.css">
|
||||||
@@ -13,19 +13,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="conn-status" class="conn-status conn-status--idle">加载中…</div>
|
<div id="conn-status" class="conn-status conn-status--idle">Loading…</div>
|
||||||
|
|
||||||
<p class="hint">主工作区在 DevTools 面板:按 <strong>F12</strong> → <strong>CyberStrikeAI</strong></p>
|
<p class="hint">Main workspace is in DevTools: press <strong>F12</strong> → <strong>CyberStrikeAI</strong></p>
|
||||||
|
|
||||||
<ul class="tips">
|
<ul class="tips">
|
||||||
<li>连接配置与 Validate 请在 DevTools 面板完成</li>
|
<li>Connection and Validate are in the DevTools panel</li>
|
||||||
<li>默认只捕获 XHR/Fetch</li>
|
<li>Captures XHR/Fetch by default</li>
|
||||||
<li>Test History 可回看多次发送</li>
|
<li>Test History stores recent sends</li>
|
||||||
<li><strong>Latest XHR</strong> 快速发送最近 API 请求</li>
|
<li><strong>Latest XHR</strong> sends the most recent API request</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<script src="../lib/constants.js"></script>
|
<script src="../lib/constants.js"></script>
|
||||||
|
<script src="../lib/auth-session.js"></script>
|
||||||
<script src="../lib/storage.js"></script>
|
<script src="../lib/storage.js"></script>
|
||||||
|
<script src="../lib/api.js"></script>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,17 +12,37 @@ async function renderPopup() {
|
|||||||
try {
|
try {
|
||||||
const cfg = await loadConfig();
|
const cfg = await loadConfig();
|
||||||
const endpoint = baseUrlFrom(cfg);
|
const endpoint = baseUrlFrom(cfg);
|
||||||
if (cfg.token) {
|
if (!cfg.token) {
|
||||||
statusEl.className = 'conn-status conn-status--ok';
|
|
||||||
statusEl.textContent = `已连接 ${endpoint}`;
|
|
||||||
} else {
|
|
||||||
statusEl.className = 'conn-status conn-status--idle';
|
statusEl.className = 'conn-status conn-status--idle';
|
||||||
statusEl.textContent = `未验证 · 目标 ${endpoint}`;
|
statusEl.textContent = `Not validated · ${endpoint}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isTokenExpiredByTime(cfg.tokenExpiresAt)) {
|
||||||
|
statusEl.className = 'conn-status conn-status--idle';
|
||||||
|
statusEl.textContent = `Session expired · ${endpoint}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await validateTokenSession(endpoint, cfg.token);
|
||||||
|
statusEl.className = 'conn-status conn-status--ok';
|
||||||
|
statusEl.textContent = `${formatTokenExpiryHint(cfg.tokenExpiresAt)} · ${endpoint}`;
|
||||||
|
} catch (err) {
|
||||||
|
if (isAuthHttpStatus(err.status)) {
|
||||||
|
statusEl.className = 'conn-status conn-status--idle';
|
||||||
|
statusEl.textContent = `Server restarted or token invalid · ${endpoint}`;
|
||||||
|
} else if (isNetworkFetchError(err)) {
|
||||||
|
statusEl.className = 'conn-status conn-status--idle';
|
||||||
|
statusEl.textContent = `Cannot reach server · ${endpoint}`;
|
||||||
|
} else {
|
||||||
|
statusEl.className = 'conn-status conn-status--idle';
|
||||||
|
statusEl.textContent = `Validation failed · ${endpoint}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
statusEl.className = 'conn-status conn-status--idle';
|
statusEl.className = 'conn-status conn-status--idle';
|
||||||
statusEl.textContent = '无法读取配置';
|
statusEl.textContent = 'Cannot read config';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPopup();
|
renderPopup().catch(() => {});
|
||||||
|
|||||||
Reference in New Issue
Block a user