diff --git a/README.md b/README.md index c1fc2cfc..ac70abac 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ If server logs show `client sent an HTTP request to an HTTPS server`, a client i ### Upgrade and Compatibility -**Prefer the platform's own one-click update** (console Platform management -> One-click update, or +**Prefer the platform's own one-click update** (console System settings -> One-click update, or `./cyberstrike-ai -update` / `-check-update` / `-update-rollback`): it pulls **the remote this installation directory already tracks**, fast-forwards, rebuilds and swaps the binary atomically, keeps your `roles/skills/tools/agents/data/config.yaml` untouched (the result names every file it diff --git a/README_CN.md b/README_CN.md index 5ede15d5..796bacb4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -268,7 +268,7 @@ go build -o cyberstrike-ai ./cmd/server ### 版本升级与兼容性 -**优先用平台自带的「一键更新」**(控制台「平台管理 → 一键更新」,或 `./cyberstrike-ai -update` / +**优先用平台自带的「一键更新」**(控制台「系统设置 → 一键更新」,或 `./cyberstrike-ai -update` / `-check-update` / `-update-rollback`):它拉的是**本安装目录自己跟踪的远端**,快进后重编译并原子换二进制, 你的 `roles/skills/tools/agents/data/config.yaml` 会被原样保留(结果里逐个点名保留了什么), 本地源码有改动或分支已分叉时它会拒绝而不是覆盖。详见 [部署指南](docs/zh-CN/deployment.md)。 diff --git a/docs/en-US/deployment.md b/docs/en-US/deployment.md index 7f16fd46..0d41ba4c 100644 --- a/docs/en-US/deployment.md +++ b/docs/en-US/deployment.md @@ -77,7 +77,7 @@ proxy_set_header Connection "upgrade"; ## Upgrading -**Prefer the platform's own one-click update** (console Platform management -> One-click update, +**Prefer the platform's own one-click update** (console System settings -> One-click update, `POST /api/system/update/apply`, or `./cyberstrike-ai -update` at a keyboard). It pulls **the remote this install directory already tracks**: fetch, fast-forward, `go build`, atomic binary swap (the old binary is kept as `cyberstrike-ai.prev`), while operator content - `roles/ skills/ tools/ agents/ diff --git a/docs/en-US/developer-guide.md b/docs/en-US/developer-guide.md index 0adf87d2..0dcf92af 100644 --- a/docs/en-US/developer-guide.md +++ b/docs/en-US/developer-guide.md @@ -48,7 +48,7 @@ disagree about why an update was refused. | entry | how it is used | |---|---| -| console | Platform management -> One-click update (`#system-update`). Opening the page reads the local tree only and does not go online; **Check** is what performs the fetch; **Update** starts a job the page polls; the page also carries the "exit after updating" option and a rollback button | +| console | System settings -> One-click update (`#system-update`, the old deep link still works). Opening this section reads the local tree only and does not go online; **Check** is what performs the fetch; **Update** starts a job the page polls; the page also carries the "exit after updating" option and a rollback button | | REST | `GET /api/system/update` (state on disk, no network), `POST /api/system/update/check` (fetch, then report the gap), `POST /api/system/update/apply` (`202` with `job_id`, polled through `GET /api/system/update/job`), `POST /api/system/update/rollback` | | CLI | `./cyberstrike-ai -check-update`, `./cyberstrike-ai -update`, `./cyberstrike-ai -update-rollback`. The install root is the directory holding `--config`, or the current directory when `--config` was not given | diff --git a/docs/zh-CN/deployment.md b/docs/zh-CN/deployment.md index d227b9bd..424e22ef 100644 --- a/docs/zh-CN/deployment.md +++ b/docs/zh-CN/deployment.md @@ -179,7 +179,7 @@ SQLite 热备份时最好先停止服务,或至少复制 `*.db`、`*.db-wal` ## 升级 -**优先用平台自带的「一键更新」**(控制台「平台管理 → 一键更新」、`POST /api/system/update/apply`、 +**优先用平台自带的「一键更新」**(控制台「系统设置 → 一键更新」、`POST /api/system/update/apply`、 或键盘上的 `./cyberstrike-ai -update`)。它拉的是**这个安装目录自己跟踪的远端**:fetch → 快进合并 → `go build` → 原子换二进制(旧的留作 `cyberstrike-ai.prev`),并把 `roles/ skills/ tools/ agents/ knowledge_base/ data/ config.yaml` 等运维者内容先暂存再放回,结果里点名保留了哪些文件。 diff --git a/docs/zh-CN/developer-guide.md b/docs/zh-CN/developer-guide.md index 2ba69d4c..21670098 100644 --- a/docs/zh-CN/developer-guide.md +++ b/docs/zh-CN/developer-guide.md @@ -54,7 +54,7 @@ go run ./cmd/server --config config.yaml | 入口 | 怎么用 | |---|---| -| 控制台 | 「平台管理 → 一键更新」(`#system-update`)。打开页面只读本机状态、不联网;点「检查更新」才去 fetch;「一键更新」发起任务并轮询进度;页面另有"更新完成后退出进程"勾选与回滚按钮 | +| 控制台 | 「系统设置 → 一键更新」(`#system-update` 老深链仍可用)。打开这一区只读本机状态、不联网;点「检查更新」才去 fetch;「一键更新」发起任务并轮询进度;页面另有"更新完成后退出进程"勾选与回滚按钮 | | REST | `GET /api/system/update`(磁盘现状,不联网)、`POST /api/system/update/check`(fetch 后报告差集)、`POST /api/system/update/apply`(`202` 返回 `job_id`,用 `GET /api/system/update/job` 轮询)、`POST /api/system/update/rollback` | | CLI | `./cyberstrike-ai -check-update`、`./cyberstrike-ai -update`、`./cyberstrike-ai -update-rollback`。安装目录 = `--config` 所在目录,未给 `--config` 时是当前目录 | diff --git a/web/static/css/style.css b/web/static/css/style.css index f78c8ee1..b821f954 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -46718,7 +46718,7 @@ html[data-theme="dark"] { --tool-blocked-color: #f4bc62; } /* --------------------------------------------------------------------------- - 一键更新控制台(system-update):这里说的每一句状态都要能被眼睛分区—— + 一键更新控制台(系统设置页的 settings-section-update 分区):这里说的每一句状态都要能被眼睛分区—— 本机事实、远端比较、能不能点、正在跑的进度、以及跑完的结论。 ok/warn/danger 三色是给"说谎"留的对照组:查不了必须是 warn/danger,不能长得像 ok。 --------------------------------------------------------------------------- */ @@ -46728,6 +46728,15 @@ html[data-theme="dark"] { --tool-blocked-color: #f4bc62; } gap: 20px; } +/* 分区标题行:标题在左、刷新在右;窄屏放不下就换行,别把标题压扁 */ +.update-section-head { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; +} + .update-intro { color: var(--text-secondary); margin: 0 0 12px; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 4e6af6ad..e83b9bfa 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -202,7 +202,6 @@ "rollbackItems": "Files that were not rolled back" }, "nav": { - "systemUpdate": "One-Click Update", "security": "Security", "toolGuard": "Call blocking", "dashboard": "Dashboard", @@ -1920,6 +1919,7 @@ "rbac": "Platform permissions", "audit": "Audit logs", "storage": "Storage cleanup", + "update": "One-Click Update", "infocollect": "Asset management" }, "infocollect": { diff --git a/web/static/i18n/ru-RU.json b/web/static/i18n/ru-RU.json index dfcba373..ab009e58 100644 --- a/web/static/i18n/ru-RU.json +++ b/web/static/i18n/ru-RU.json @@ -202,7 +202,6 @@ "rollbackItems": "Файлы, которые не были откачены" }, "nav": { - "systemUpdate": "Обновление в один клик", "security": "Безопасность", "toolGuard": "Блокировка вызовов", "dashboard": "Обзор", @@ -1933,6 +1932,7 @@ "rbac": "Права платформы", "audit": "Журнал аудита", "storage": "Очистка хранилища", + "update": "Обновление в один клик", "infocollect": "Управление активами" }, "infocollect": { diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 59712d2e..f821746a 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -202,7 +202,6 @@ "rollbackItems": "未能回滚的文件" }, "nav": { - "systemUpdate": "一键更新", "security": "安全防护", "toolGuard": "调用拦截", "dashboard": "仪表盘", @@ -1908,6 +1907,7 @@ "rbac": "平台权限", "audit": "日志审计", "storage": "存储清理", + "update": "一键更新", "infocollect": "资产管理" }, "infocollect": { diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 229a8b6e..6ab6942e 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -443,7 +443,6 @@ const PAGE_PERMISSION_MAP = { roles: 'roles:read', 'roles-management': 'roles:read', 'platform-rbac': 'rbac:read', - 'system-update': 'update:read', settings: 'config:read', }; diff --git a/web/static/js/router.js b/web/static/js/router.js index 3aeb92aa..6ac61a77 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -110,8 +110,14 @@ function initRouter() { const hashParts = hash.split('?'); let pageId = hashParts[0].replace(/^#?\//, ''); if (pageId === 'c2') pageId = 'c2-listeners'; - if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'system-update', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { + // 一键更新已并进系统设置页的分区菜单:#system-update 这条老深链落到设置页的那一区。 + const settingsSection = pageId === 'system-update' ? 'update' : ''; + if (settingsSection) pageId = 'settings'; + if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { switchPage(pageId); + if (settingsSection && typeof switchSettingsSection === 'function') { + switchSettingsSection(settingsSection); + } if (pageId === 'chat') { scheduleChatConversationFromHash(0); } @@ -524,14 +530,17 @@ async function initPage(pageId) { initChatFilesPage(); } break; - case 'system-update': - if (typeof loadUpdateConsole === 'function') loadUpdateConsole(); - break; case 'settings': // 初始化设置页面(不需要加载工具列表) if (typeof loadConfig === 'function') { loadConfig(false); } + // 回到设置页时若停在「一键更新」分区,重新读一遍安装状态并把进度轮询接上—— + // 离开设置页时轮询已被停掉,不接的话画面上会留一份不再动的进度。 + const updateSection = document.getElementById('settings-section-update'); + if (updateSection && updateSection.classList.contains('active') && typeof loadUpdateConsole === 'function') { + loadUpdateConsole(); + } break; case 'roles-management': // 初始化角色管理页面 @@ -608,8 +617,9 @@ async function initPage(pageId) { if (pageId !== 'tasks' && typeof cleanupTasksPage === 'function') { cleanupTasksPage(); } - // 一键更新的进度轮询同理:离开那一页就没必要每 1.5 秒读一次任务了。 - if (pageId !== 'system-update' && typeof stopUpdatePolling === 'function') { + // 一键更新的进度轮询同理:它现在住在系统设置页里,离开设置页就没必要每 1.5 秒读一次任务了 + // (同一页内切到别的分区由 settings.js 的 switchSettingsSection 负责停)。 + if (pageId !== 'settings' && typeof stopUpdatePolling === 'function') { stopUpdatePolling(); } } @@ -628,8 +638,14 @@ document.addEventListener('DOMContentLoaded', function() { let pageId = hashParts[0].replace(/^#?\//, ''); if (pageId === 'c2') pageId = 'c2-listeners'; - if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'system-update', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { + // 与首屏解析同一套别名:#system-update 落到系统设置页的「一键更新」分区。 + const settingsSection = pageId === 'system-update' ? 'update' : ''; + if (settingsSection) pageId = 'settings'; + if (pageId && ['dashboard', 'chat', 'hitl', 'tool-guard', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { switchPage(pageId); + if (settingsSection && typeof switchSettingsSection === 'function') { + switchSettingsSection(settingsSection); + } if (pageId === 'chat') { scheduleChatConversationFromHash(0); } diff --git a/web/static/js/settings.js b/web/static/js/settings.js index f7699ed6..9e371efd 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -691,6 +691,14 @@ function switchSettingsSection(section) { if (section === 'storage' && typeof initStorageSection === 'function') { setTimeout(initStorageSection, 0); } + if (section === 'update') { + if (typeof loadUpdateConsole === 'function') { + loadUpdateConsole(); + } + } else if (typeof stopUpdatePolling === 'function') { + // 离开更新分区就停掉进度轮询;服务端的任务照跑,回来时 loadUpdateConsole 会接着读。 + stopUpdatePolling(); + } } // 打开设置 diff --git a/web/static/js/update-ui.test.cjs b/web/static/js/update-ui.test.cjs index e0d9013e..1a300871 100644 --- a/web/static/js/update-ui.test.cjs +++ b/web/static/js/update-ui.test.cjs @@ -6,6 +6,7 @@ const assert = require('node:assert/strict'); const source = fs.readFileSync('web/static/js/update.js', 'utf8'); const template = fs.readFileSync('web/templates/index.html', 'utf8'); const router = fs.readFileSync('web/static/js/router.js', 'utf8'); +const settings = fs.readFileSync('web/static/js/settings.js', 'utf8'); const sheet = fs.readFileSync('web/static/css/style.css', 'utf8'); const zh = JSON.parse(fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8')); const en = JSON.parse(fs.readFileSync('web/static/i18n/en-US.json', 'utf8')); @@ -160,8 +161,12 @@ function harness(options) { checked: false, insertAdjacentHTML(_pos, html) { this.innerHTML = html + this.innerHTML; }, }; + // 控制台住在系统设置页的「一键更新」分区里:页面和分区都得是 active,它才算在前台。 + let seeded = ['page']; + if (id === 'page-settings') seeded = ['page'].concat(active ? ['active'] : []); + if (id === 'settings-section-update') seeded = ['settings-section-content'].concat(active ? ['active'] : []); el.classList = { - _set: new Set(id === 'page-system-update' && active ? ['page', 'active'] : ['page']), + _set: new Set(seeded), contains(c) { return this._set.has(c); }, add(c) { this._set.add(c); }, remove(c) { this._set.delete(c); }, @@ -325,11 +330,15 @@ function harness(options) { if (!text) throw new Error('handler ' + name + ' is not present in the rendered markup'); return runHandler(text, thisArg); }, - // Flipping the router's page-active class is all that leaving a page means for this console. + // Flipping the router's active classes (the settings page and its update section) is all + // that leaving means for this console. setActive(v) { active = v; - const node = nodes.get('page-system-update'); - if (v) node.classList.add('active'); else node.classList.remove('active'); + ['page-settings', 'settings-section-update'].forEach(id => { + const node = nodes.get(id); + if (!node) return; + if (v) node.classList.add('active'); else node.classList.remove('active'); + }); }, setConfirm(v) { confirmAnswer = v; }, liveTimers() { return timers.filter(t => !t.cleared && !t.deferred); }, @@ -343,38 +352,64 @@ function harness(options) { return Promise.resolve(sandbox.api.loadUpdateConsole()).then(flush).then(() => h); } -test('the page is wired in everywhere a page has to be registered', () => { - assert.match(template, /