From cbd8073beb285b155f91bde478cce40c26d19840 Mon Sep 17 00:00:00 2001 From: Sycun <165354365+Sycun@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:21:27 +0800 Subject: [PATCH] feat: move the one-click update into the system settings menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update console stops being a page of its own and becomes the last section of 系统设置 (data-section="update", gated by update:read): the sidebar entry and page-system-update are gone, the console markup lives in settings-section-update, and the settings menu is where an operator now finds it. #system-update keeps working as an alias - both hash parsers normalize it to settings + the update section - so existing deep links and docs keep landing. Polling follows the new home: leaving the settings page (router) or switching to another section (settings.js) stops it, selecting the section loads the console, and returning to the settings page with the section still selected re-reads the install tree and re-arms the poller (a frozen progress read is the "nothing is happening" picture an operator must never be shown). The guard suite follows the move: the old nav/page assertions become "must not exist", the new ones pin the section, the alias, the poller handoff and the retired nav.systemUpdate key (settings.nav.update replaces it in zh/EN/RU). README and both developer/deployment guides say 系统设置 → 一键更新. --- README.md | 2 +- README_CN.md | 2 +- docs/en-US/deployment.md | 2 +- docs/en-US/developer-guide.md | 2 +- docs/zh-CN/deployment.md | 2 +- docs/zh-CN/developer-guide.md | 2 +- web/static/css/style.css | 11 +++- web/static/i18n/en-US.json | 2 +- web/static/i18n/ru-RU.json | 2 +- web/static/i18n/zh-CN.json | 2 +- web/static/js/auth.js | 1 - web/static/js/router.js | 30 ++++++--- web/static/js/settings.js | 8 +++ web/static/js/update-ui.test.cjs | 103 +++++++++++++++++++++---------- web/static/js/update.js | 7 ++- web/templates/index.html | 35 ++++------- 16 files changed, 140 insertions(+), 73 deletions(-) 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, /