From 0e3d532b9c99364af8100d0cd60df5a91dae6e0d Mon Sep 17 00:00:00 2001 From: Hsukqi Lee Date: Tue, 10 Nov 2026 19:09:05 +0800 Subject: [PATCH 1/4] fix: reject cancelled starts and non-finite agent timeouts Allow cancellation before approval and prevent start from clearing an accepted cancellation while cleanup is pending. Refs #2, #3. --- server.py | 9 ++++++--- tests/test_fastlab.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/server.py b/server.py index 8547075..ca6b299 100755 --- a/server.py +++ b/server.py @@ -3,6 +3,7 @@ import argparse import json +import math import mimetypes import os import re @@ -221,8 +222,8 @@ def parse_agent_timeout(value): timeout = float(raw) except (TypeError, ValueError) as exc: raise ValueError("FASTLAB_AGENT_TIMEOUT 必须是大于等于 0 的秒数。") from exc - if timeout < 0: - raise ValueError("FASTLAB_AGENT_TIMEOUT 必须是大于等于 0 的秒数。") + if not math.isfinite(timeout) or timeout < 0: + raise ValueError("FASTLAB_AGENT_TIMEOUT 必须是有限且大于等于 0 的秒数。") return timeout @@ -2068,6 +2069,8 @@ def start_task(self, task_id): def _start_task_locked(self, task_id): task = self.require_task(task_id) + if task["cancel_requested"]: + raise ValueError("任务已请求取消。") if task["status"] != "awaiting_approval": raise ValueError("任务尚未准备好或已经开始。") for subtask in task["subtasks"]: @@ -2986,7 +2989,7 @@ def cancel_task(self, task_id): def _cancel_task_locked(self, task_id): task = self.require_task(task_id) - if task["status"] not in {"planning", "running", "verifying"}: + if task["status"] not in {"planning", "awaiting_approval", "running", "verifying"}: raise ValueError("当前任务不在运行。") self.store.update_task(task_id, cancel_requested=1) self.agent_gate.wake_all() diff --git a/tests/test_fastlab.py b/tests/test_fastlab.py index 8745206..01b572f 100644 --- a/tests/test_fastlab.py +++ b/tests/test_fastlab.py @@ -770,6 +770,35 @@ def test_cancel_stops_running_workers(self): self.assertTrue(branch_exists(self.repo, "user-keep")) self.assertFalse((self.data / "worktrees" / task["id"]).exists()) + def test_cancel_awaiting_approval_does_not_start_work(self): + task = self.prepare_task() + finish_entered = threading.Event() + release_finish = threading.Event() + original_finish = self.app._finish_cancel + + def blocked_finish(snapshot): + finish_entered.set() + self.assertTrue(release_finish.wait(5)) + return original_finish(snapshot) + + with patch.object(self.app, "_finish_cancel", side_effect=blocked_finish): + returned = self.app.cancel_task(task["id"]) + self.assertEqual(returned["status"], "awaiting_approval") + self.assertTrue(returned["cancel_requested"]) + self.assertTrue(finish_entered.wait(5)) + with self.assertRaises(ValueError): + self.app.start_task(task["id"]) + release_finish.set() + + worker_calls = [ + call for call in self.adapter.calls + if call["key"].startswith("subtask-%s-" % task["id"]) + ] + self.assertEqual(worker_calls, []) + self.assertFalse((self.data / "worktrees" / task["id"]).exists()) + cancelled = self.wait_for_status(task["id"], "cancelled") + self.assertTrue(all(item["status"] == "cancelled" for item in cancelled["subtasks"])) + def test_failure_conflict_and_retry_preserve_main(self): task = self.prepare_task("CONFLICT") base = git(self.repo, "rev-parse", "main") @@ -1267,6 +1296,9 @@ def test_command_helpers_support_windows(self): self.assertEqual(parse_agent_timeout("12.5"), 12.5) with self.assertRaisesRegex(ValueError, "大于等于 0"): parse_agent_timeout("-1") + for raw in ("nan", "NaN", "inf", "Infinity", "1e9999"): + with self.subTest(raw=raw), self.assertRaisesRegex(ValueError, "有限"): + parse_agent_timeout(raw) def test_cycle_and_unknown_executor_are_rejected(self): cycle = { From d193fab3562e8e0c5db31e660e03a22a47dd1046 Mon Sep 17 00:00:00 2001 From: Hsukqi Lee Date: Wed, 11 Nov 2026 06:05:57 +0800 Subject: [PATCH 2/4] refactor: consume versioned shared UI assets Use immutable public release artifacts and retain product-specific behavior. Independent review and real release installation recorded in the sharing handoff. --- .gitattributes | 2 + web/FASTUI.md | 5 ++ web/app.js | 10 ++-- web/fastui/LICENSE | 21 ++++++++ web/fastui/NOTICE.md | 23 +++++++++ web/fastui/color-mode.js | 75 +++++++++++++++++++++++++++++ web/fastui/controls.css | 61 ++++++++++++++++++++++++ web/fastui/provenance.json | 12 +++++ web/fastui/tokens.css | 98 ++++++++++++++++++++++++++++++++++++++ web/index.html | 5 +- web/styles.css | 89 +++++++++++++++++----------------- web/supply_fastui.py | 42 ++++++++++++++++ 12 files changed, 392 insertions(+), 51 deletions(-) create mode 100644 .gitattributes create mode 100644 web/FASTUI.md create mode 100644 web/fastui/LICENSE create mode 100644 web/fastui/NOTICE.md create mode 100644 web/fastui/color-mode.js create mode 100644 web/fastui/controls.css create mode 100644 web/fastui/provenance.json create mode 100644 web/fastui/tokens.css create mode 100644 web/supply_fastui.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0684f7b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Preserve supplier bytes/CRLF metadata while retaining real whitespace checks. +web/fastui/** -text whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --git a/web/FASTUI.md b/web/FASTUI.md new file mode 100644 index 0000000..a0b0c2e --- /dev/null +++ b/web/FASTUI.md @@ -0,0 +1,5 @@ +# FastUI 0.1.0 + +部署前运行 `python web/supply_fastui.py`。离线供应:`python web/supply_fastui.py --artifact /path/to/fastrd-ui-foundation-0.1.0.tgz`。脚本从固定 release URL 下载或读取同字节 tarball,校验 SHA-256 后生成 `web/fastui/`,含版本、包摘要、逐文件摘要和许可证;供应目录不手工编辑。 + +提交/部署生成目录后,服务运行只读取本地文件,不需要 npm/CDN。颜色模式初始化在 HTML 内容之前执行,`fastlabs-theme` 未配置时跟随系统;共享控件通过显式 `fastui-*` 类 opt-in,宿主业务布局使用共享 token。任务表单字段、事件委托和调度状态不变。 diff --git a/web/app.js b/web/app.js index 027f41a..e3bc426 100644 --- a/web/app.js +++ b/web/app.js @@ -200,14 +200,14 @@ function renderCreate() { main.innerHTML = `

创建任务

-
+
仓库内的相对目录
-
-
-
+
+
+
-
+
`; } diff --git a/web/fastui/LICENSE b/web/fastui/LICENSE new file mode 100644 index 0000000..47c4d66 --- /dev/null +++ b/web/fastui/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 FastR-D contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/fastui/NOTICE.md b/web/fastui/NOTICE.md new file mode 100644 index 0000000..59d1ef6 --- /dev/null +++ b/web/fastui/NOTICE.md @@ -0,0 +1,23 @@ +# Source and publication scope + +The owner has authorized publication of the generic UI portions under MIT. +Vue semantic primitives and v-model adapters derive from the common +`primitives.ts` (203 lines) in FastPPT and FastFigure. Modal's public props and +the focus-lifecycle purpose derive from their common `use-dialog-focus.ts` +(58 lines) and Modal component. FastUI delegates focus trapping, inertness, +nesting and focus restoration to the browser's native modal dialog rather +than maintaining the original hand-written trap. Select retains the shared +value/options/change contract and delegates interaction to the native select. + +The palette follows FastWrite's semantic colors and the approved FastTask +workbench design. Only generic UI code and semantic design values are included. +No private artwork, icons, logos, fonts, application data, product-specific +workflows or brand assets are distributed. Upstream product repositories +retain ownership of their application-specific code and assets. + +Source reference revisions: FastPPT `f6bf21f13445c2507719928fa68550552a8fcab2` +(`packages/components/src/`) and FastFigure +`13dcc4179fd0073da76f7626264af8605d5e8c70` (`src/components/ui/`). +At both revisions, `primitives.ts` has Git blob +`c7123af5f8316bc0486bce5c99cfbb2e9ab3dc73` and `use-dialog-focus.ts` has +Git blob `3ffbb6c6c7bd5e3c96f2b8c05c5e008dcdfc4cab`. diff --git a/web/fastui/color-mode.js b/web/fastui/color-mode.js new file mode 100644 index 0000000..02a627b --- /dev/null +++ b/web/fastui/color-mode.js @@ -0,0 +1,75 @@ +"use strict"; +(() => { + // packages/foundation/src/index.ts + var parse = (value) => value === "light" || value === "dark" ? value : "system"; + function initColorMode({ storageKey: storageKey2, target = document.documentElement }) { + const win = target.ownerDocument.defaultView; + const media = win.matchMedia("(prefers-color-scheme: dark)"); + let mode = "system"; + try { + mode = parse(win.localStorage.getItem(storageKey2)); + } catch { + } + let disposed = false; + const listeners = /* @__PURE__ */ new Set(); + const resolved = () => mode === "system" ? media.matches ? "dark" : "light" : mode; + const apply = () => { + target.dataset.fastuiTheme = resolved(); + listeners.forEach((listener) => listener({ mode, resolved: resolved() })); + }; + const systemChange = () => { + if (mode === "system") apply(); + }; + const storageChange = (event) => { + let storage; + try { + storage = win.localStorage; + } catch { + return; + } + if (event.storageArea !== storage || event.key !== storageKey2 && event.key !== null) return; + mode = parse(event.newValue); + apply(); + }; + media.addEventListener("change", systemChange); + win.addEventListener("storage", storageChange); + apply(); + return { + get mode() { + return mode; + }, + get resolved() { + return resolved(); + }, + setMode(value) { + if (disposed) return; + mode = parse(value); + try { + win.localStorage.setItem(storageKey2, mode); + } catch { + } + apply(); + }, + subscribe(listener) { + if (disposed) return () => { + }; + listeners.add(listener); + listener({ mode, resolved: resolved() }); + return () => { + listeners.delete(listener); + }; + }, + dispose() { + disposed = true; + media.removeEventListener("change", systemChange); + win.removeEventListener("storage", storageChange); + listeners.clear(); + } + }; + } + + // packages/foundation/src/bootstrap.ts + var storageKey = document.currentScript.dataset.storageKey; + if (!storageKey) throw new Error("FastUI color-mode.js requires data-storage-key on its script element."); + initColorMode({ storageKey }); +})(); diff --git a/web/fastui/controls.css b/web/fastui/controls.css new file mode 100644 index 0000000..fd85a23 --- /dev/null +++ b/web/fastui/controls.css @@ -0,0 +1,61 @@ +/* Every appearance rule is explicitly opted into by a fastui class. No reset. */ +.fastui-button, .fastui-input, .fastui-textarea, .fastui-select { + box-sizing: border-box; + font-family: var(--fastui-font); + font-size: 13px; + line-height: 1.5; + min-height: var(--fastui-height-md); + padding: 7px 10px; + border: 1px solid var(--fastui-border-strong); + border-radius: var(--fastui-radius-control); + background: var(--fastui-surface); + color: var(--fastui-text); + max-width: 100%; + min-width: 0; + color-scheme: var(--fastui-color-scheme); +} +.fastui-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; font-weight: 500; text-decoration: none; } +.fastui-button--sm, .fastui-input--sm, .fastui-select--sm { min-height: var(--fastui-height-sm); padding: 4px 8px; font-size: 12px; } +.fastui-button--xs, .fastui-input--xs, .fastui-select--xs { min-height: var(--fastui-height-xs); padding: 1px 6px; font-size: 12px; } +.fastui-button:hover:not(:disabled) { background: var(--fastui-selected); } +.fastui-button--primary { background: var(--fastui-primary); border-color: var(--fastui-primary); color: var(--fastui-on-primary); } +.fastui-button--primary:hover:not(:disabled) { background: var(--fastui-primary-hover); } +.fastui-button--danger { background: var(--fastui-danger-fill); border-color: var(--fastui-danger-fill); color: var(--fastui-on-primary); } +.fastui-button--danger:hover:not(:disabled) { filter: brightness(.9); background: var(--fastui-danger-fill); } +.fastui-button--quiet, .fastui-button--icon, .fastui-button--navigation { background: transparent; border-color: transparent; } +.fastui-button--icon { aspect-ratio: 1; padding: 5px; } +.fastui-button--navigation { border-radius: 0; } +.fastui-button[aria-pressed=true], .fastui-button[aria-selected=true] { box-shadow: inset 0 -2px var(--fastui-focus); } +.fastui-button:disabled, .fastui-input:disabled, .fastui-textarea:disabled, .fastui-select:disabled { opacity: .6; cursor: not-allowed; } +.fastui-button:focus-visible, .fastui-input:focus-visible, .fastui-textarea:focus-visible, .fastui-select:focus-visible, .fastui-link:focus-visible, .fastui-disclosure-trigger:focus-visible { outline: 2px solid var(--fastui-focus); outline-offset: 2px; } +.fastui-input::placeholder, .fastui-textarea::placeholder { color: var(--fastui-muted); } +.fastui-input[aria-invalid=true], .fastui-textarea[aria-invalid=true] { border-color: var(--fastui-danger); } +.fastui-input[type=checkbox], .fastui-input[type=radio], .fastui-input[type=range] { min-height: auto; padding: 0; accent-color: var(--fastui-primary); } +.fastui-textarea { resize: vertical; } +.fastui-field { display: grid; gap: 6px; color: var(--fastui-secondary); font: 13px/1.5 var(--fastui-font); } +.fastui-fieldset { border: 1px solid var(--fastui-border); border-radius: var(--fastui-radius-panel); min-width: 0; padding: 16px; } +.fastui-legend { color: var(--fastui-secondary); padding: 0 4px; } +.fastui-heading { color: var(--fastui-text); font-weight: 600; } +.fastui-link { color: var(--fastui-focus); text-underline-offset: 3px; } +.fastui-progress { accent-color: var(--fastui-primary); } +.fastui-table { border-collapse: collapse; text-align: left; } +.fastui-table :where(th, td) { padding: 12px; border-bottom: 1px solid var(--fastui-border); } +.fastui-table th { background: var(--fastui-raised); font-weight: 600; } +.fastui-disclosure-trigger { padding: 5px 8px; cursor: pointer; } +.fastui-dialog { box-sizing: border-box; width: min(620px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow: auto; padding: 0; border: 1px solid var(--fastui-border); border-radius: var(--fastui-radius-panel); background: var(--fastui-surface); color: var(--fastui-text); font: 14px/1.5 var(--fastui-font); box-shadow: 0 16px 48px #0004; } +.fastui-dialog--lg { width: min(880px, calc(100% - 32px)); } +.fastui-dialog--xl { width: min(1100px, calc(100% - 32px)); } +.fastui-dialog::backdrop { background: var(--fastui-overlay); } +.fastui-dialog > :where(header, footer), .fastui-modal-body { padding: 20px 24px; } +.fastui-dialog > header { border-bottom: 1px solid var(--fastui-border); } +.fastui-dialog > footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--fastui-border); } +.fastui-modal-body { display: grid; gap: 16px; } +.fastui-alert, .fastui-badge, .fastui-empty, .fastui-loading { font-family: var(--fastui-font); color: var(--fastui-text); } +.fastui-alert { border: 1px solid var(--fastui-border); border-left: 3px solid var(--fastui-tone, var(--fastui-focus)); border-radius: var(--fastui-radius-control); padding: 12px 16px; background: var(--fastui-surface); } +.fastui-badge { display: inline-block; border: 1px solid currentColor; color: var(--fastui-tone, var(--fastui-focus)); border-radius: 4px; padding: 1px 6px; font-size: 12px; } +.fastui-tone-success { --fastui-tone: var(--fastui-success); } +.fastui-tone-warning { --fastui-tone: var(--fastui-warning); } +.fastui-tone-danger { --fastui-tone: var(--fastui-danger); } +.fastui-empty { padding: 24px; text-align: center; } +.fastui-empty > h2 { font-size: 16px; } +.fastui-loading { display: inline-flex; align-items: center; gap: 8px; } diff --git a/web/fastui/provenance.json b/web/fastui/provenance.json new file mode 100644 index 0000000..b0228f6 --- /dev/null +++ b/web/fastui/provenance.json @@ -0,0 +1,12 @@ +{ + "package": "@fastrd/ui-foundation", + "version": "0.1.0", + "url": "https://github.com/FastR-D/FastUI/releases/download/v0.1.0/fastrd-ui-foundation-0.1.0.tgz", + "sha256": "5c87e8f60c2c349c98b17349396a4f6ac335de30f92d0d0b8ec2171b951ebd9e", + "files": { + "tokens.css": "fadf2cf99aa54150ff65776032c4f61b007ebe47a0051723f81afac0b397de38", + "controls.css": "f34fa4d48fbae9dcbf22fc7fcb1718e2b1a5cb33c36bc287805fd49b61cecbc4", + "color-mode.js": "596e123cf27bf280dec4cabafe98dfbcf780fbef546763c8c27a0c28e6c52f99" + }, + "generated_by": "python web/supply_fastui.py" +} diff --git a/web/fastui/tokens.css b/web/fastui/tokens.css new file mode 100644 index 0000000..7008b75 --- /dev/null +++ b/web/fastui/tokens.css @@ -0,0 +1,98 @@ +/* Generated from tokens.json. */ +:root { + --fastui-font: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans CJK SC', sans-serif; + --fastui-font-size: 14px; + --fastui-radius-control: 6px; + --fastui-radius-panel: 8px; + --fastui-space-1: 4px; + --fastui-space-2: 8px; + --fastui-space-3: 12px; + --fastui-space-4: 16px; + --fastui-space-6: 24px; + --fastui-space-8: 32px; + --fastui-height-xs: 24px; + --fastui-height-sm: 30px; + --fastui-height-md: 38px; + --fastui-on-primary: #ffffff; + --fastui-on-primary-rgb: 255, 255, 255; +} +:root, [data-fastui-theme="light"] { + --fastui-page: #f7f8fa; + --fastui-page-rgb: 247, 248, 250; + --fastui-workspace: #eef0f3; + --fastui-workspace-rgb: 238, 240, 243; + --fastui-surface: #ffffff; + --fastui-surface-rgb: 255, 255, 255; + --fastui-raised: #eef0f3; + --fastui-raised-rgb: 238, 240, 243; + --fastui-text: #151c28; + --fastui-text-rgb: 21, 28, 40; + --fastui-secondary: #4b5565; + --fastui-secondary-rgb: 75, 85, 101; + --fastui-muted: #687384; + --fastui-muted-rgb: 104, 115, 132; + --fastui-border: #dfe3e8; + --fastui-border-rgb: 223, 227, 232; + --fastui-border-strong: #a7b0bd; + --fastui-border-strong-rgb: 167, 176, 189; + --fastui-primary: #155fb8; + --fastui-primary-rgb: 21, 95, 184; + --fastui-primary-hover: #104a91; + --fastui-primary-hover-rgb: 16, 74, 145; + --fastui-focus: #155fb8; + --fastui-focus-rgb: 21, 95, 184; + --fastui-selected: #e5effb; + --fastui-selected-rgb: 229, 239, 251; + --fastui-disabled: #e8ebef; + --fastui-disabled-rgb: 232, 235, 239; + --fastui-success: #16834a; + --fastui-success-rgb: 22, 131, 74; + --fastui-warning: #8c5b09; + --fastui-warning-rgb: 140, 91, 9; + --fastui-danger: #c93636; + --fastui-danger-rgb: 201, 54, 54; + --fastui-danger-fill: #c93636; + --fastui-danger-fill-rgb: 201, 54, 54; + --fastui-overlay: #151c2880; + --fastui-color-scheme: light; +} +[data-fastui-theme="dark"] { + --fastui-page: #111820; + --fastui-page-rgb: 17, 24, 32; + --fastui-workspace: #111820; + --fastui-workspace-rgb: 17, 24, 32; + --fastui-surface: #171e27; + --fastui-surface-rgb: 23, 30, 39; + --fastui-raised: #273342; + --fastui-raised-rgb: 39, 51, 66; + --fastui-text: #f4f7fa; + --fastui-text-rgb: 244, 247, 250; + --fastui-secondary: #c5cfdb; + --fastui-secondary-rgb: 197, 207, 219; + --fastui-muted: #9aa8b9; + --fastui-muted-rgb: 154, 168, 185; + --fastui-border: #354252; + --fastui-border-rgb: 53, 66, 82; + --fastui-border-strong: #65758a; + --fastui-border-strong-rgb: 101, 117, 138; + --fastui-primary: #0e639c; + --fastui-primary-rgb: 14, 99, 156; + --fastui-primary-hover: #1177bb; + --fastui-primary-hover-rgb: 17, 119, 187; + --fastui-focus: #5ba3ea; + --fastui-focus-rgb: 91, 163, 234; + --fastui-selected: #094771; + --fastui-selected-rgb: 9, 71, 113; + --fastui-disabled: #273342; + --fastui-disabled-rgb: 39, 51, 66; + --fastui-success: #6acc96; + --fastui-success-rgb: 106, 204, 150; + --fastui-warning: #e6ba6a; + --fastui-warning-rgb: 230, 186, 106; + --fastui-danger: #ff848c; + --fastui-danger-rgb: 255, 132, 140; + --fastui-danger-fill: #b03030; + --fastui-danger-fill-rgb: 176, 48, 48; + --fastui-overlay: #00000099; + --fastui-color-scheme: dark; +} \ No newline at end of file diff --git a/web/index.html b/web/index.html index fb94955..78f62dc 100644 --- a/web/index.html +++ b/web/index.html @@ -6,6 +6,9 @@ FastLab + + + @@ -21,7 +24,7 @@

FastLab

- diff --git a/web/styles.css b/web/styles.css index 7550b37..2579ea2 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1,19 +1,19 @@ :root { - color-scheme: light; - --bg: #f5f5f7; - --surface: rgba(255, 255, 255, 0.86); - --surface-solid: #fff; - --text: #1d1d1f; - --muted: #6e6e73; - --faint: #86868b; - --line: rgba(29, 29, 31, 0.1); - --blue: #0071e3; - --blue-hover: #0077ed; - --green: #248a3d; - --orange: #c93400; - --red: #d70015; - --shadow: 0 18px 55px rgba(0, 0, 0, 0.07); - --small-shadow: 0 8px 24px rgba(0, 0, 0, 0.055); + color-scheme: var(--fastui-color-scheme); + --bg: var(--fastui-page); + --surface: var(--fastui-surface); + --surface-solid: var(--fastui-surface); + --text: var(--fastui-text); + --muted: var(--fastui-secondary); + --faint: var(--fastui-muted); + --line: var(--fastui-border); + --blue: var(--fastui-primary); + --blue-hover: var(--fastui-primary-hover); + --green: var(--fastui-success); + --orange: var(--fastui-warning); + --red: var(--fastui-danger); + --shadow: none; + --small-shadow: none; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "PingFang SC", "Helvetica Neue", Arial, sans-serif; font-synthesis: none; @@ -27,10 +27,7 @@ body { min-height: 100vh; margin: 0; color: var(--text); - background: - radial-gradient(circle at 78% -5%, rgba(114, 176, 255, 0.19), transparent 29rem), - radial-gradient(circle at 8% 95%, rgba(173, 139, 255, 0.1), transparent 28rem), - var(--bg); + background: var(--bg); -webkit-font-smoothing: antialiased; } @@ -42,6 +39,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus- } .ambient { + display: none; position: fixed; z-index: -1; width: 24rem; @@ -64,15 +62,15 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus- height: 100vh; padding: 24px 18px 20px; border-right: 1px solid var(--line); - background: rgba(250, 250, 252, 0.8); + background: var(--fastui-workspace); backdrop-filter: saturate(180%) blur(24px); } .brand { display: flex; align-items: center; gap: 12px; padding: 0 6px 25px; } .brand-mark { width: 42px; height: 42px; border: 0; border-radius: 13px; color: white; - background: linear-gradient(145deg, #232326, #050506); font-weight: 750; letter-spacing: -0.04em; - box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16); cursor: pointer; + background: var(--fastui-primary); font-weight: 750; letter-spacing: -0.04em; + box-shadow: none; cursor: pointer; } .brand h1 { margin: 0; font-size: 20px; letter-spacing: -0.03em; } @@ -82,10 +80,10 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus- .mobile-settings-button { display: none; } .new-task-button { display: flex; align-items: center; justify-content: center; gap: 8px; - padding: 11px 14px; border-radius: 12px; color: white; background: var(--text); + padding: 11px 14px; border-radius: 6px; color: var(--fastui-on-primary); background: var(--fastui-primary); font-size: 14px; font-weight: 650; box-shadow: var(--small-shadow); } -.new-task-button:hover { background: #000; } +.new-task-button:hover { background: var(--fastui-primary-hover); } .new-task-button span { font-size: 19px; font-weight: 300; line-height: 0; } .sidebar-section-heading { @@ -100,7 +98,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus- margin: 3px 0; padding: 10px 9px; border-radius: 12px; background: transparent; } .task-nav-item:hover { background: rgba(0, 0, 0, 0.035); } -.task-nav-item.active { background: white; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04), var(--small-shadow); } +.task-nav-item.active { background: var(--fastui-selected); box-shadow: none; } .task-nav-copy { min-width: 0; } .task-nav-copy strong, .task-nav-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .task-nav-copy strong { font-size: 13px; font-weight: 620; } @@ -130,18 +128,18 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus- .page-actions, .form-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } .panel, .metric { - border: 1px solid rgba(255, 255, 255, 0.75); background: var(--surface); + border: 1px solid var(--fastui-border); background: var(--surface); box-shadow: var(--shadow); backdrop-filter: saturate(160%) blur(22px); } .primary-button, .quiet-button, .danger-button { - min-height: 40px; padding: 9px 17px; border: 0; border-radius: 999px; + min-height: 40px; padding: 9px 17px; border: 1px solid var(--line); border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 620; transition: transform 0.16s, background 0.16s; } .primary-button { color: white; background: var(--blue); } .primary-button:hover { background: var(--blue-hover); transform: translateY(-1px); } -.quiet-button { border: 1px solid var(--line); background: rgba(255, 255, 255, 0.8); } -.quiet-button:hover { background: white; } +.quiet-button { border: 1px solid var(--line); background: var(--fastui-surface); } +.quiet-button:hover { background: var(--fastui-selected); } .danger-button { color: var(--red); background: rgba(215, 0, 21, 0.08); } .danger-button:hover { background: rgba(215, 0, 21, 0.13); } button:disabled { opacity: 0.42; cursor: not-allowed; transform: none !important; } @@ -152,7 +150,7 @@ button:disabled { opacity: 0.42; cursor: not-allowed; transform: none !important .page-heading { display: flex; justify-content: space-between; gap: 28px; align-items: end; margin-bottom: 28px; } .page-title { margin: 10px 0 7px; font-size: clamp(34px, 5vw, 54px); line-height: 1.02; letter-spacing: -0.055em; } .page-subtitle { max-width: 730px; margin: 0; color: var(--muted); line-height: 1.55; } -.panel { margin-bottom: 18px; padding: 24px; border-radius: 22px; } +.panel { margin-bottom: 18px; padding: 24px; border-radius: 8px; } .panel-header { display: flex; justify-content: space-between; gap: 20px; align-items: baseline; margin-bottom: 20px; } .panel-header h3 { margin: 0; font-size: 19px; letter-spacing: -0.025em; } .panel-header span { color: var(--faint); font-size: 12px; } @@ -162,23 +160,23 @@ button:disabled { opacity: 0.42; cursor: not-allowed; transform: none !important .field.full, .form-actions.full { grid-column: 1 / -1; } .field label { font-size: 13px; font-weight: 620; } .field-hint, .field-note { color: var(--faint); font-size: 11px; font-weight: 400; } -input, select, textarea { - width: 100%; border: 1px solid rgba(29, 29, 31, 0.13); border-radius: 12px; - color: var(--text); background: rgba(255, 255, 255, 0.9); padding: 11px 12px; +.app-shell :is(input, select, textarea) { + width: 100%; border: 1px solid var(--fastui-border-strong); border-radius: 5px; + color: var(--text); background: var(--fastui-surface); padding: 11px 12px; } textarea { resize: vertical; line-height: 1.5; } input:hover, select:hover, textarea:hover { border-color: rgba(29, 29, 31, 0.23); } input:focus, select:focus, textarea:focus { border-color: var(--blue); outline: 0; box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.12); } .metric-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; } -.metric { padding: 20px; border-radius: 18px; } +.metric { padding: 20px; border-radius: 8px; } .metric-label { display: block; margin-bottom: 8px; color: var(--faint); font-size: 11px; } .metric strong { display: block; overflow: hidden; font-size: 20px; letter-spacing: -0.025em; text-overflow: ellipsis; white-space: nowrap; } .alert { margin-bottom: 18px; padding: 13px 16px; border-radius: 13px; font-size: 13px; } .alert.error, .error-note { color: #9f0010; background: rgba(215, 0, 21, 0.07); } .subtask-list { display: grid; gap: 12px; } -.subtask-card { padding: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 17px; background: rgba(255, 255, 255, 0.68); } +.subtask-card { padding: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 8px; background: var(--fastui-surface); } .subtask-heading { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 13px; align-items: start; } .subtask-card > .subtask-heading { padding: 17px 19px; list-style: none; } .subtask-card > .subtask-heading::-webkit-details-marker { display: none; } @@ -199,8 +197,8 @@ input:focus, select:focus, textarea:focus { border-color: var(--blue); outline: .task-operation-note p { margin: 0; line-height: 1.5; } .subtask-action-panel { display: grid; gap: 15px; margin-top: 18px; padding: 17px; - border: 1px solid rgba(29, 29, 31, 0.09); border-radius: 15px; - background: rgba(245, 245, 247, 0.72); + border: 1px solid var(--fastui-border); border-radius: 6px; + background: var(--fastui-workspace); } .plan-editor { margin-top: 15px; } .restart-editor { background: rgba(0, 113, 227, 0.045); border-color: rgba(0, 113, 227, 0.13); } @@ -220,7 +218,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--blue); outline: .subtask-action-footer { display: flex; justify-content: space-between; gap: 14px; align-items: center; } .subtask-action-footer > span { color: var(--faint); font-size: 11px; line-height: 1.45; } .subtask-action-footer button { flex: 0 0 auto; } -.resume-editor { margin-top: 14px; border: 1px solid var(--line); border-radius: 14px; background: rgba(255, 255, 255, 0.72); overflow: hidden; } +.resume-editor { margin-top: 14px; border: 1px solid var(--line); border-radius: 6px; background: var(--fastui-surface); overflow: hidden; } .resume-editor > summary { padding: 13px 15px; color: var(--muted); font-size: 12px; cursor: pointer; font-weight: 620; } .resume-editor[open] > summary { border-bottom: 1px solid var(--line); } .resume-editor-body { display: grid; gap: 14px; padding: 15px; } @@ -240,7 +238,7 @@ input:focus, select:focus, textarea:focus { border-color: var(--blue); outline: .manual-verification p { margin: 0 0 12px; line-height: 1.5; } .settings-body { display: grid; gap: 14px; color: var(--muted); font-size: 13px; } .settings-body p { margin: 0; line-height: 1.5; } -code { padding: 2px 6px; border-radius: 6px; color: #3a3a3c; background: rgba(0, 0, 0, 0.045); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; } +code { padding: 2px 6px; border-radius: 5px; color: var(--fastui-text); background: var(--fastui-workspace); font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; } details.panel { padding: 0; overflow: hidden; } details summary { padding: 20px 24px; cursor: pointer; font-weight: 620; list-style-position: inside; } @@ -252,10 +250,10 @@ details[open] summary { border-bottom: 1px solid var(--line); } .event-list strong { font-weight: 620; } .event-list p { margin: 0; color: var(--muted); white-space: pre-wrap; } details.panel > h4 { margin: 20px 24px 8px; } -.document-view { max-height: 520px; margin: 0 24px 22px; overflow: auto; padding: 17px; border-radius: 13px; background: #f4f4f6; color: #343437; font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; } +.document-view { max-height: 520px; margin: 0 24px 22px; overflow: auto; padding: 17px; border-radius: 6px; background: var(--fastui-workspace); color: var(--fastui-text); font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; } .executor-list, .repository-list { display: grid; gap: 10px; } -.executor-row, .repository-row { display: flex; justify-content: space-between; gap: 20px; align-items: center; padding: 14px; border: 1px solid var(--line); border-radius: 14px; background: rgba(255, 255, 255, 0.58); } +.executor-row, .repository-row { display: flex; justify-content: space-between; gap: 20px; align-items: center; padding: 14px; border: 1px solid var(--line); border-radius: 6px; background: var(--fastui-surface); } .executor-copy strong, .executor-copy p, .repository-row strong, .repository-row p { display: block; margin: 0; } .executor-copy p, .repository-row p { margin-top: 4px; color: var(--faint); font-size: 11px; word-break: break-all; } .availability-badge, .default-badge { margin-left: 7px; padding: 4px 8px; color: #176b2a; background: rgba(36, 138, 61, 0.1); } @@ -284,12 +282,13 @@ details.panel > h4 { margin: 20px 24px 8px; } .app-shell { display: block; } .sidebar { position: relative; height: auto; padding: 15px 16px; border-right: 0; border-bottom: 1px solid var(--line); } .brand { padding: 0 0 13px; } + .brand h1 { font-size: 18px; } .brand-mark { width: 36px; height: 36px; border-radius: 11px; } .new-task-button { width: auto; position: absolute; top: 15px; right: 16px; padding: 8px 13px; } .mobile-settings-button { - display: block; position: absolute; top: 15px; right: 112px; min-height: 34px; - padding: 7px 13px; border: 1px solid var(--line); border-radius: 999px; - background: rgba(255, 255, 255, 0.82); font-size: 12px; font-weight: 620; cursor: pointer; + display: block; position: absolute; top: 15px; right: 136px; min-height: 34px; + padding: 7px 13px; border: 1px solid var(--line); border-radius: 6px; + background: var(--fastui-surface); font-size: 12px; font-weight: 620; cursor: pointer; } .sidebar-section-heading, .task-list, .settings-button, .local-status { display: none; } .main-content { padding: 38px 18px 70px; } @@ -297,7 +296,7 @@ details.panel > h4 { margin: 20px 24px 8px; } .page-actions { margin-top: 18px; } .content-grid.two-up, .task-form { grid-template-columns: 1fr; } .field.full, .form-actions.full { grid-column: auto; } - .panel { padding: 18px; border-radius: 18px; } + .panel { padding: 18px; border-radius: 8px; } .metric { padding: 16px; } .metric strong { font-size: 17px; } .subtask-heading { grid-template-columns: auto minmax(0, 1fr); } diff --git a/web/supply_fastui.py b/web/supply_fastui.py new file mode 100644 index 0000000..ab09d0f --- /dev/null +++ b/web/supply_fastui.py @@ -0,0 +1,42 @@ +"""Build local browser assets from FastUI's fixed release; no npm at runtime.""" +import argparse +import hashlib +import io +import json +from pathlib import Path +import tarfile +from urllib.request import urlopen + +VERSION = "0.1.0" +SHA256 = "5c87e8f60c2c349c98b17349396a4f6ac335de30f92d0d0b8ec2171b951ebd9e" +URL = f"https://github.com/FastR-D/FastUI/releases/download/v{VERSION}/fastrd-ui-foundation-{VERSION}.tgz" + + +def supply(artifact=None): + payload = Path(artifact).read_bytes() if artifact else urlopen(URL, timeout=60).read() + if hashlib.sha256(payload).hexdigest() != SHA256: + raise ValueError("FastUI foundation SHA-256 mismatch") + destination = Path(__file__).resolve().parent / "fastui" + destination.mkdir(parents=True, exist_ok=True) + files = {} + with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as archive: + for name in ("tokens.css", "controls.css", "color-mode.js"): + member = archive.getmember(f"package/dist/{name}") + if not member.isfile(): + raise ValueError(f"Not a regular package asset: {name}") + content = archive.extractfile(member).read() + (destination / name).write_bytes(content) + files[name] = hashlib.sha256(content).hexdigest() + for name in ("LICENSE", "NOTICE.md"): + (destination / name).write_bytes(archive.extractfile(f"package/{name}").read()) + (destination / "provenance.json").write_text(json.dumps({ + "package": "@fastrd/ui-foundation", "version": VERSION, + "url": URL, "sha256": SHA256, "files": files, + "generated_by": "python web/supply_fastui.py", + }, indent=2) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--artifact") + supply(parser.parse_args().artifact) From cbc5deeba9a393187554d55e76b678a81cd3753c Mon Sep 17 00:00:00 2001 From: Hsukqi Lee Date: Wed, 11 Nov 2026 14:44:07 +0800 Subject: [PATCH 3/4] chore: consume verified npm UI and transport releases --- web/FASTUI.md | 4 ++-- web/fastui/provenance.json | 6 +++--- web/supply_fastui.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/FASTUI.md b/web/FASTUI.md index a0b0c2e..f4730db 100644 --- a/web/FASTUI.md +++ b/web/FASTUI.md @@ -1,5 +1,5 @@ -# FastUI 0.1.0 +# FastUI 0.1.2 -部署前运行 `python web/supply_fastui.py`。离线供应:`python web/supply_fastui.py --artifact /path/to/fastrd-ui-foundation-0.1.0.tgz`。脚本从固定 release URL 下载或读取同字节 tarball,校验 SHA-256 后生成 `web/fastui/`,含版本、包摘要、逐文件摘要和许可证;供应目录不手工编辑。 +部署前运行 `python web/supply_fastui.py`。离线供应:`python web/supply_fastui.py --artifact /path/to/ui-foundation-0.1.2.tgz`。脚本从固定官方 npm tarball URL 下载或读取同字节 tarball,校验 SHA-256 后生成 `web/fastui/`,含版本、包摘要、逐文件摘要和许可证;供应目录不手工编辑。升级时从公开 registry metadata 确认实际 URL,并下载核准版本和完整摘要。 提交/部署生成目录后,服务运行只读取本地文件,不需要 npm/CDN。颜色模式初始化在 HTML 内容之前执行,`fastlabs-theme` 未配置时跟随系统;共享控件通过显式 `fastui-*` 类 opt-in,宿主业务布局使用共享 token。任务表单字段、事件委托和调度状态不变。 diff --git a/web/fastui/provenance.json b/web/fastui/provenance.json index b0228f6..3ae746f 100644 --- a/web/fastui/provenance.json +++ b/web/fastui/provenance.json @@ -1,8 +1,8 @@ { "package": "@fastrd/ui-foundation", - "version": "0.1.0", - "url": "https://github.com/FastR-D/FastUI/releases/download/v0.1.0/fastrd-ui-foundation-0.1.0.tgz", - "sha256": "5c87e8f60c2c349c98b17349396a4f6ac335de30f92d0d0b8ec2171b951ebd9e", + "version": "0.1.2", + "url": "https://registry.npmjs.org/@fastrd/ui-foundation/-/ui-foundation-0.1.2.tgz", + "sha256": "c3dabbad16980242f66db07619b161889d0ee2492daaa4aaddee1583bc7bcd0b", "files": { "tokens.css": "fadf2cf99aa54150ff65776032c4f61b007ebe47a0051723f81afac0b397de38", "controls.css": "f34fa4d48fbae9dcbf22fc7fcb1718e2b1a5cb33c36bc287805fd49b61cecbc4", diff --git a/web/supply_fastui.py b/web/supply_fastui.py index ab09d0f..a0ad795 100644 --- a/web/supply_fastui.py +++ b/web/supply_fastui.py @@ -7,9 +7,9 @@ import tarfile from urllib.request import urlopen -VERSION = "0.1.0" -SHA256 = "5c87e8f60c2c349c98b17349396a4f6ac335de30f92d0d0b8ec2171b951ebd9e" -URL = f"https://github.com/FastR-D/FastUI/releases/download/v{VERSION}/fastrd-ui-foundation-{VERSION}.tgz" +VERSION = "0.1.2" +SHA256 = "c3dabbad16980242f66db07619b161889d0ee2492daaa4aaddee1583bc7bcd0b" +URL = "https://registry.npmjs.org/@fastrd/ui-foundation/-/ui-foundation-0.1.2.tgz" def supply(artifact=None): From 9d141899d4378fbc9173a2e11cedf5731b404490 Mon Sep 17 00:00:00 2001 From: GuGuMur <2221533105@qq.com> Date: Wed, 11 Nov 2026 23:25:05 +0800 Subject: [PATCH 4/4] Prepare FastDeploy v2.0.2 production deployment --- .github/workflows/rollback.yml | 2 +- .github/workflows/tag-ci.yml | 4 ++-- docs/fastdeploy.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 9094ac2..28db2bf 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -14,7 +14,7 @@ permissions: jobs: rollback: if: github.ref == 'refs/heads/main' - uses: FastR-D/FastDeploy/.github/workflows/rollback.yml@cc223a0b83b0ef2389974dd70a4b74148cd1af87 + uses: FastR-D/FastDeploy/.github/workflows/rollback.yml@9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2 with: app: fastlabs expected-release: ${{ inputs.expected-release }} diff --git a/.github/workflows/tag-ci.yml b/.github/workflows/tag-ci.yml index 178dc59..cc5599e 100644 --- a/.github/workflows/tag-ci.yml +++ b/.github/workflows/tag-ci.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 with: repository: FastR-D/FastDeploy - ref: cc223a0b83b0ef2389974dd70a4b74148cd1af87 + ref: 9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2 path: .fastdeploy-tools persist-credentials: false - name: Package the tested runtime @@ -46,7 +46,7 @@ jobs: deploy: needs: validate if: startsWith(github.ref, 'refs/tags/v') - uses: FastR-D/FastDeploy/.github/workflows/deploy.yml@cc223a0b83b0ef2389974dd70a4b74148cd1af87 + uses: FastR-D/FastDeploy/.github/workflows/deploy.yml@9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2 with: app: fastlabs artifact-name: release-${{ github.sha }} diff --git a/docs/fastdeploy.md b/docs/fastdeploy.md index 937daeb..11b40ff 100644 --- a/docs/fastdeploy.md +++ b/docs/fastdeploy.md @@ -4,7 +4,7 @@ 普通运行参数直接写在该 workflow 的 `runtime-env`,使用字面 `KEY=value`,不要加 shell 引号。敏感值用该文件 `runtime-secrets` 中的同名仓库 Secrets,使用 `toJSON` 保留特殊字符。SSH 五项来自 FastR-D 组织的 `FASTRD_SSH_*` Secrets;请勿把凭证写入 YAML 或构建产物。 -构建 job 上传同一次运行的 `release.tar.gz` 和 `manifest.json`。FastDeploy 和打包器均固定到完整提交 `cc223a0b83b0ef2389974dd70a4b74148cd1af87`,服务器不拉源码或重新安装依赖。代码相对路径由 [FastDeploy 应用配置](https://github.com/FastR-D/FastDeploy/blob/cc223a0b83b0ef2389974dd70a4b74148cd1af87/config/apps.json) 注入,业务数据使用其中记录的原持久化路径。 +构建 job 上传同一次运行的 `release.tar.gz` 和 `manifest.json`。FastDeploy 和打包器均固定到完整提交 `9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2`,服务器不拉源码或重新安装依赖。代码相对路径由 [FastDeploy 应用配置](https://github.com/FastR-D/FastDeploy/blob/9a2c51dcd63f47a0e6c8f6bb07bd22ea589abbd2/config/apps.json) 注入,业务数据使用其中记录的原持久化路径。 发布示例(版本号每次必须唯一):