Compare commits

...
459 Commits
Author SHA1 Message Date
mesalogo 107dc974e3 merge: retain published Desktop 0.13.13 history
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Merge the published release preparation commit without rewriting either
branch. Keep the approved Desktop 0.13.14 and Agent 0.13.4 versions and
bilingual notes, and restore the historical release documentation.

The merged product source, tests, dependencies, and approved release notes
are identical to d23af99, which passed 5127 tests. Post-merge release-note
checks, nine focused tests, full typecheck, and lint passed.
2026-09-26 01:02:55 +08:00
mesalogo d23af99708 release: prepare Desktop 0.13.14 and Agent 0.13.4
Prepare the approved bilingual release notes for paged Supervisor reviews,
resizable to-dos, native direct-model searches, and remote recovery fixes.
Agent 0.13.4 requires Desktop 0.13.14; schema 47 requires a pre-upgrade
backup for rollback. Keep Desktop and Agent publication separate.

Include the existing validation fixture corrections and helper patches as
requested. Full validation passed 5127 tests with 74 skipped, typecheck,
lint, and release-note checks. Current-source Linux Host completion and
cleanup probes passed with two real model requests.

Release note: 改善监督回顾与续跑、待办布局和搜索;配套 Agent 修复远程完成状态并清理停用安装。请先升级桌面再更新 Host,升级前备份用户数据。
2026-09-26 00:59:27 +08:00
mesalogo ff746bdc1a fix: preserve native ripgrep capabilities for direct models
Replace the restricted workspace_rg parameter mapping with native args and optional cwd. Preserve rg output and exit codes, and reuse process cancellation and output_read pagination so large search results remain accessible. Ask remains read-only within the workspace; Execute uses current-account permissions.

Validation: focused regression 169 passed, 1 skipped; typecheck and lint passed. Real DeepSeek Ask and Execute pagination scenarios passed with six counted requests in the successful run; an earlier failed fixture run has no retained request count. Full-suite validation is not green: the recorded run had unrelated failures and the latest run timed out without a summary. The remote Agent does not use this local execution path.

Release note: 直连模型可使用 ripgrep 原生参数搜索文件并保留原生结果,大输出可通过 output_read 完整续读。Ask 保持工作区只读,Execute 使用当前账号权限。
2026-09-25 23:58:21 +08:00
mesalogo d4bc10c0f3 fix: use full width for supervisor recap and activity
Remove the extra reading and dashboard width caps so recap controls, results, and activity details fill the supervisor panel. Update layout assertions and UI documentation.

Release note: 修复监督者工作回顾与活动记录页面偏窄的问题,内容现在铺满页签下方的可用宽度。
2026-09-25 23:16:47 +08:00
mesalogo db5e43d7d5 fix: complete supervisor review coverage and execution workflow
Replace truncated review input with paged, resumable batches and retain extracted facts and source references through navigation summaries. Expose concurrency, timeouts, batch budgets and response capacity in supervisor settings.

Separate automatic supervision from review results, activity and settings; use a standard plan modal and show execution stages with saved batch details. Fix historical entity identity mapping, navigation output validation and arbitrary summary length failures.

Include focused regressions, isolated real-data validation scripts and updated design and verification records. Overall layout acceptance remains outstanding; this commit does not claim that all visual issues are resolved.

Release note: 监督回顾支持完整分页、分批保存和暂停后继续,可配置并发、超时及响应容量;修复实体引用、汇总校验和长摘要报错,并重新组织自动监督与活动记录入口。
2026-09-25 22:32:43 +08:00
mesalogo f68584b4e2 feat: resize todo panes and collapse note groups
Let users drag the to-do list/detail divider and remember its width. Note headings independently collapse task rows without clearing selected details. Use a 1px divider with a transparent drag target and compact spacing.

Validation: 115 focused tests including native Electron input; typecheck and lint passed.

Release note: Resize the Magic Notes to-do list with a thin draggable divider and collapse tasks by source note.
2026-09-24 16:17:16 +08:00
mesalogo 7adae97baa fix: restore remote conversation recovery and clean obsolete agents
Wait for model delivery acknowledgements before completing remote prompts and preserve completed outcomes when idle Runtime cleanup fails. Explicit messages after an unknown outcome now open a fresh binding with history instead of reusing the failed session; recovery remains attach-only.

Run obsolete payload cleanup through the current Agent after activation, retaining history and unresolved process ownership. Live legacy daemons and shared Runtime versions remain deferred. Includes focused regressions, local OpenCode simulation, and isolated Linux validation evidence. Typecheck and lint passed; the full test run timed out.

Release note: Remote conversations can continue after an uncertain result, and completed answers no longer fail solely because Runtime cleanup fails. Updates remove verified inactive Agent payloads and owned orphan processes while retaining active or uncertain installations and conversation history.
2026-09-24 10:39:28 +08:00
mesalogo 0ed1be1773 fix: widen project picker beyond the sidebar
Float the project picker at 380px with viewport bounds and preserve menu focus and dismissal. Align local package metadata and packaged release notes with Desktop 0.13.13.

Release note: 项目下拉列表可浮出侧栏,宽度调整为 380px,窄窗口自动收窄。
2026-09-24 00:34:08 +08:00
mesalogo d5f6dc6990 fix: hide queue attachment action for messages without attachments
Derive attachment presence from saved queue inputs and only show the preview action when attachments exist. Cover queue metadata and button visibility with regression tests.

Release note: Queued messages without attachments no longer show the View attachments button.
2026-09-23 23:44:08 +08:00
mesalogo c71c0adf9e fix: place supervision feedback below project overview
Order the task center as project overview, supervision feedback, then project tasks. Update the sidebar layout documentation to match.

Release note: 任务中心的监督反馈现位于项目概览下方、项目任务上方。
2026-09-23 23:11:33 +08:00
mesalogo 344008aaec fix: align note thumbnails with content edges
Remove extra top and left padding from the note record index so thumbnails align with the content area. Draw keyboard focus outlines inward to keep them visible within the scroll viewport, and cover alignment in the Electron layout regression test.

Release note: Note thumbnails now align with the content area, and keyboard focus outlines remain visible at the list edges.
2026-09-23 23:03:09 +08:00
mesalogo cebd513da8 fix: restore full workbar resize hit area
Keep the workbar divider's 9px hit target inside the sidebar so overflow clipping no longer makes it difficult to drag. Reserve matching space beside browser and terminal content, and remove it in browser fullscreen mode.

Validation: 68 sidebar tests, typecheck, lint, and Chromium hit tests passed. Full suite timed out after 10 minutes and reported three supervision test failures.

Release note: Make the divider between chat and the browser or terminal workbar easier to grab by restoring its full resize area.
2026-09-23 22:51:55 +08:00
mesalogo 2ca19ebf50 test: allow more time for large model bridge encoding
Give the 600 KiB request encoding round-trip test 15 seconds instead of the default 5 seconds after a CI timeout. Preserve the payload size, all assertions, global test timeout, and production behavior.

Validation: all nine model bridge contract tests and file-scoped ESLint passed. The full typecheck/lint command was cancelled before its completion could be confirmed.
2026-09-23 22:38:06 +08:00
mesalogo 7756231b3b release: prepare Desktop 0.13.13
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Prepare a Desktop-only patch for repeated activity-history reads during streaming and the supervision feedback sidebar layout. Keep Agent 0.13.3 and the database format unchanged; no Host update is required.

Update bilingual release notes, feature matrices, and validation evidence. Isolate this candidate from the original worktree and exclude its uncommitted supervision timeout tests. Validation passed 192 focused tests, an additional six-case Electron sidebar scenario, typecheck, lint, and release-note checks.

Release note: 减少流式活动历史的重复读取,修复监督反馈侧栏的按钮布局和目标名称显示;Agent 保持 0.13.3。
2026-09-23 22:02:55 +08:00
mesalogo b16fc900bb fix: restore supervision feedback sidebar layout
Replace unstyled full-width pin and refresh controls with shared icon buttons in the feedback header. Show conversation and task titles instead of raw IDs, with responsive spacing and keyboard focus coverage.

Release note: 修复任务中心监督反馈按钮样式和布局,显示会话或任务名称,并改善窄侧栏下的阅读与操作体验。
2026-09-23 21:52:36 +08:00
mesalogo bf8ff7f78e fix: avoid repeated activity history reads during streaming
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Reuse the last committed activity records when comparing streaming saves instead of reading the full history for every update. Invalidate comparisons on close, data clearing and commits from other SQLite connections; preserve rollback and retry behavior.

Validation: 3 related test files / 121 tests passed; typecheck and lint passed. Ten consecutive saves now perform at most one full-history scan while preserving the WAL write reduction. Installed-app physical disk throughput remains unverified.

Release note: Reduce repeated database reads while streaming activity updates in conversations with large activity histories.
2026-09-23 21:49:58 +08:00
mesalogo bb8c04af6c fix: use bundled OpenCode path in isolation tests
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Linux release CI could not start OpenCode because the isolation test assumed a platform-specific npm executable name. Reuse the production runtime path resolver, which matches the installed bin/opencode.exe on all platforms.

Validation: nine focused tests, typecheck, lint, and bilingual release-note verification passed. Product behavior, versions, and release notes remain unchanged; main-branch CI must pass before tagging.
2026-09-23 20:09:05 +08:00
lofyerandGitHub 073aa45a30 Merge pull request #7 from mesalogo/jojocoding-patch-6
Update README.zh-CN.md
2026-09-23 19:29:05 +08:00
lofyerandGitHub 1cf74d3d2c Merge pull request #6 from mesalogo/jojocoding-patch-5
Update README.md
2026-09-23 19:28:44 +08:00
mesalogo 163e245b00 fix: stabilize release CI regression tests
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Address Desktop CI 35849362694 failures with isolated OpenCode config dependencies, cleared IPC lookup counts, Supervisor default-off expectations, and polling-safe UI fixtures and timeouts.

Record the failed candidate CI and follow-up validation: 14 focused tests, typecheck, and lint passed. Product code, versions, and bilingual release notes are unchanged. The revised candidate requires approval and successful main-branch CI before tagging.
2026-09-23 19:15:08 +08:00
mesalogo bf8ece0590 release: prepare Desktop 0.13.12 and Agent 0.13.3
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Complete the pending Supervisor changes with opt-in execution, incremental automatic reviews, and activity history. Preserve saved preferences and plans, skip model calls for unchanged input, and keep full replay and cancellation limitations explicit.

Update both release versions, bilingual notes, feature matrices, migration fixtures, and validation evidence. Clear stale browser errors after a successful close retry. Focused validation passed 690 tests across 35 files, plus notes verification, typecheck, and lint; full CI and native release acceptance remain pending. No local production build or packaging was run.

Release note: Enable Supervisor to review work, trace story graphs, and inspect manual or automatic activity without repeating model calls for unchanged input. Desktop 0.13.12 and Agent 0.13.3 also prepare idle remote Runtime cleanup and retirement of replaced Agents; update Desktop before the Host environment. Database upgrades require a pre-upgrade backup for rollback.
2026-09-23 18:20:23 +08:00
mesalogo 83a48a4079 fix: reclaim idle remote runtimes and retire replaced agents
Remote tasks could leave idle Runtime processes behind, and replaced Agent
installations lacked a drain-and-exit lifecycle. Keep the current Agent
resident, let displaced Agents finish existing work and reconnect for its
results, then retire them without restarting obsolete installations.

Reclaim Runtime processes when their last active task finishes, preserve
pending questions and shared active tasks, and restore conversation history
when the next turn starts. Remove the Remote Projects technical preview label
from settings, errors, tests, and current documentation; the feature remains
disabled by default.

Validation recorded during implementation: 213 lifecycle tests and 28 focused
UI/IPC tests passed, as did SettingsPanel and lint. Linux x64 real-Host checks
used 8 provider requests. Broader tests and final typecheck encountered
unrelated concurrent changes; details and coverage limits are documented.

Release note: 远程任务结束后自动释放空闲运行进程,下一轮对话恢复历史;更新后的旧 Agent 等待已有任务完成再退出,当前 Agent 保持常驻。远程项目移除“技术预览”标识,仍默认关闭。
2026-09-23 17:37:14 +08:00
mesalogo cdd573778c fix: show expandable project and conversation usage totals
Group usage into project or conversation totals with collapsed Runtime/model children. Keep model grouping flat and preserve expanded rows when usage refreshes. Include aggregation and interaction tests plus matching documentation.

Validation: 25 focused tests, typecheck, and lint passed. Full suite timed out after 200 seconds with four failures reported in ipc.test.ts and ApplicationMenu.test.tsx.

Release note: Project and conversation usage now show totals first, with model details available by expanding each row.
2026-09-23 16:33:13 +08:00
mesalogo 0265cfb762 fix: group and label note page controls
Move the record index toggle beside the back button and show text labels for both index and AI comment controls. Preserve pane visibility, keyboard focus, and narrow-window behavior; update tests and documentation.

Validation: 112 workspace tests and Electron layout regression passed; typecheck and lint passed. Full suite hit unrelated test failures and timed out.
2026-09-23 15:47:54 +08:00
mesalogo fb4b9bdd46 fix: float Git branch picker above workspace content
Render the searchable branch picker in the shared floating portal so opening it no longer pushes down changes or commit history. Clamp it to the viewport and retain search focus, outside dismissal, and Escape focus restoration.

Update existing migration fixtures for activity history and supervision schema changes. Focused suites passed: 149 storage tests, 41 UI tests, and the Electron overlay test; typecheck and lint passed. The full suite did not complete cleanly and reported failures outside this change.

Release note: The Git branch picker now opens over the workspace without displacing the file list or commit history.
2026-09-23 15:27:33 +08:00
mesalogo 984f16f178 fix: reduce activity history disk writes during streaming
Save only changed activity records instead of rewriting the entire history snapshot. Migrate existing history to schema 44 while preserving order and duplicate IDs.

Run unit tests with up to four workers and keep the configured integration phase serial. Include activity-history I/O, storage-upgrade, settings, and OpenCode isolation regression coverage.

Validation: 4 related test files / 123 tests passed; typecheck and lint passed.

Release note: Reduce disk writes while agent activity updates stream in, preserving existing activity history.
2026-09-23 14:54:21 +08:00
mesalogo 7d487f9326 feat: add supervisor workspace and traceable story graph
Extend the existing heartbeat workspace with scoped work reviews, result-specific story graphs, source inspection, and pinned conversation or task feedback. Connect confirmed continuation and local knowledge entity previews through the desktop APIs, and preserve result history and user corrections in schema 43.

Include focused regression tests, an isolated visual fixture, and implementation evidence. Full event-by-event replay, Experiment targets, no-change handling, and cancellation remain incomplete; this commit does not claim complete feature acceptance. Existing automatic plans remain active when the application entry is disabled.

Release note: Review work progress in the Supervisor workspace, trace graph events to saved sources, and open a matching review from pinned conversation or task feedback.
2026-09-23 13:49:13 +08:00
mesalogo 785b1ee468 fix: keep Mermaid diagram edges reachable and add PNG export
Fix oversized diagram alignment in the large-diagram viewer and provide complete PNG downloads independent of zoom and pan. Cover the full window and shared native drag handling, with regression checks for both themes and narrow windows.

Validation: 58 focused tests passed, including 9 Electron scenarios and 10 PNG downloads; typecheck and lint passed. Full-suite runs did not complete.

Release note: Mermaid 大图放大后可浏览全部边缘,并可从弹窗导出完整 PNG 图片。
2026-09-23 11:53:09 +08:00
mesalogo be84534e77 fix: simplify notifications and shorten display time
Use compact neutral notification cards with vertically aligned icons, text, and close controls. Dismiss success and info messages after two seconds; keep errors visible until dismissed.

Release note: Notifications now use a cleaner compact layout, and success and info messages disappear after two seconds.
2026-09-23 11:51:17 +08:00
mesalogo f7541203e4 fix: restore cached reply duration when switching conversations
Keep execution statistics by conversation and project so returning to a viewed conversation immediately restores its reply duration while refreshing in the background. Preserve late-result isolation and clear failed scopes.

Validation: 15 focused tests, typecheck and lint passed. Full suite: 4878 passed, 9 navigation settings assertion failures and 1 Agent package timeout outside this change.

Release note: Switching back to a viewed conversation now immediately displays its cached cumulative Agent reply duration while refreshing the statistics.
2026-09-23 01:00:18 +08:00
jojokeeandGitHub 24bde0c2e3 Update README.zh-CN.md 2026-09-22 09:51:43 +08:00
jojokeeandGitHub 4366bdc716 Update README.md
更新社群码
2026-09-22 09:50:42 +08:00
mesalogo 598c68f955 release: prepare Desktop 0.13.11 2026-09-22 09:13:56 +08:00
mesalogo e6a25b779c ci: use OSS transfer acceleration for uploads 2026-09-22 09:09:05 +08:00
mesalogo a0b35d1192 release: prepare Desktop 0.13.10 2026-09-21 22:53:44 +08:00
mesalogo 6c4c054180 fix: improve document parsing settings spacing 2026-09-21 16:11:51 +08:00
mesalogo 38d0923d79 fix: stop repeated history migration after restart
Desktop 0.13.8 still treated SQLite freelist pages left by normal chat and note writes as evidence of pending migration when a file-backed note existed. Migration detection now checks only the schema version and legacy note payloads, while the startup worker reuses its confirmed pending decision during retries so actual migrations can still finish compaction.

Release note: 修复已有笔记用户正常聊天后重启仍重复显示历史迁移的问题;已完成迁移的数据继续按新格式保存,普通写入产生的空闲页不再触发迁移。
2026-09-21 12:31:14 +08:00
mesalogo 8dce1d7007 feat: improve model connections and settings workflow
Model settings can now duplicate saved connections with encrypted API-key copying, while settings navigation stays usable across long forms and preserves the separate Platform Features tabs. The assistant storage startup worker now gates historical migration and compaction on actual pending legacy data, so ordinary chat free pages do not reopen the migration screen. Desktop and Agent release metadata is prepared for 0.13.8 and 0.13.2.

Release note: 改进模型连接复制、长表单设置导航和历史数据迁移判断;普通聊天退出并重开后不再重复显示迁移页面。
2026-09-21 09:19:30 +08:00
mesalogo 6739e8e902 fix: simplify task center status selection and rendering 2026-09-20 22:03:27 +08:00
mesalogo fe093bc15f ci: allow full desktop validation to finish
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-09-20 14:15:27 +08:00
mesalogo 3cd6f44d7d ci: rehydrate Electron sandbox before validation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
2026-09-20 12:20:01 +08:00
mesalogo 9e8c98290e release: prepare Desktop 0.13.7 and Agent 0.13.1
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Prepare the approved bilingual Desktop 0.13.7 and Agent 0.13.1 release
notes, version locks, feature matrices, and Agent release notes. Package both
OpenCode and Continue in the compound Agent release and publish native
execution checklists for remote projects.

Fix Linux source validation by configuring Electron's chrome-sandbox helper
and running the full test suite under Xvfb without disabling sandboxing.

Release note: Desktop 0.13.7 adds paged canvas notes, HTTP OCR, managed app
navigation, remote Continue and execution checklists, while preserving
conversation history and improving remote/runtime recovery. Agent 0.13.1
ships OpenCode and Continue together with checklist delivery.

Validation: npm test passed; npm run typecheck passed; npm run lint passed;
npm run release:notes:verify passed; 22 release workflow tests passed. No
local production build or package was run. LoongArch preview is not included.
2026-09-20 11:54:58 +08:00
mesalogo 9836a4cc9d fix: keep question order and streamline contextual help
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Keep interactive questions and answered cards at their original position in
conversation history, including remote reception, recovery, and reopening.
Preserve legacy history placement without a database migration.

Move supplementary guidance into shared accessible contextual-help popovers
across pages and settings. Add keyboard, modal, accessibility, and Electron
regressions, plus bounded opt-in live canvas-analysis validation.

Remove unused Agent, Runtime, and in-memory graph-search helpers, obsolete
mocks and browser copy; reuse the existing package-size formatter. Keep
production execution paths, protocol schemas, and released data migrations.

Validation: targeted cleanup tests, typecheck, lint, shared Linux Host
attach/read/re-attach, and added help/Electron checks passed. Final full-suite
verification passed 4,858 tests with 67 skipped and zero failures (412 files
passed, 9 skipped). Focused help checks passed 54 tests, including Electron
style comparisons and Chromium accessibility descriptions. Details and
validation boundaries are recorded in feature docs.

Release note: 修复交互式问答在回答后排到后续输出末尾的问题,重新打开会话仍保留提问位置。页面和设置中的补充说明改为标题或字段旁的按需帮助,支持鼠标与键盘操作。
2026-09-20 01:26:36 +08:00
mesalogo bf1d34e897 feat: configure canvas pages sent for note analysis
Canvas analysis previously sent every page. Add a Magic Notes setting for
1-8 canvas pages, defaulting to 1 for new and historical settings. Select
the first N pages in current canvas order before capture and use the same
text/image range for saved entries, drafts and canvas-source todos.

Keep full 50-page saving/export, expose the same setting through config
tools, and use the shared settings select with an inline help icon.
Include pagination, persistence, production IPC and isolated Electron
regressions, including text-only input and scaled page boundaries.

Validation: focused feature tests and isolated Electron regressions passed;
the final compact UI passed 14 tests. Typecheck and lint were reported
passing by the coordinating agent. No full-suite pass is claimed because
the completed full test command exposed no summary. No live model calls
were made for this commit preparation.

Release note: 魔法笔记新增“发送画布页数”设置,可选 1~8 页,默认 1 页,按当前画布顺序发送前 N 页的文字和图片;标题旁问号可查看说明,保存和导出完整画布不受影响。
2026-09-20 00:07:07 +08:00
mesalogo ecf09f4e95 docs: consolidate product PR workflow and draft update article
Keep feature inventory, user-focused prioritization, draft confirmation,
article templates, screenshot guidance and publication checks in one guide.
Use separate new-feature, improvement and bug-fix sections, with explicit
image, replacement and verification markers in drafts.

Archive the unreleased update article and its evidence review, retaining
pending screenshots and release details. Remove the split guide files and
update release-runbook references.

Validation: documentation structure, draft markers, 59 local links and
anchors, and staged whitespace checks passed. No product code changed;
product tests and builds were not run.
2026-09-19 23:45:28 +08:00
mesalogo a3b7a27045 fix: preserve drafts, canvas edits, and remote Continue tools
Keep paragraph formatting and checklist todos when removing canvas pages,
and preserve accepted undo/redo history after oversized input is rejected.
Merge restored queued text into the latest conversation draft and retain
image delivery when OCR returns image references and generated warnings only.

Deliver session MCP servers to bridged remote Continue Execute while keeping
Ask read-only and preserving local model-profile isolation. Add regression
coverage and update feature documentation. Correct stale conversation-limit
and forced-color assertions and the measured Electron layout test deadline.

Validation: 4,826 tests passed, 67 skipped; full typecheck and lint passed.
Current-source Linux x64 Execute/Ask/Execute MCP delivery passed, using 10
real text-provider requests and no image-provider requests. Image generation
itself was not exercised. Staged whitespace checks passed before commit.

Release note: 修复恢复队列时覆盖新输入、画布删页丢失清单格式及超限输入清空撤销历史的问题;图片 OCR 无正文时保留原图发送方式,远程 Continue Execute 正确保留会话 MCP 工具。
2026-09-19 23:09:48 +08:00
mesalogo 375b2159c8 fix: remove conversation list and message history limits
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
List and search all active conversations instead of only the latest 100. Preserve message history across consecutive sends instead of truncating it to 500 messages. Existing detail loading and context compression remain unchanged.

Validation: 7 focused tests, typecheck and lint passed. Full test run timed out after reporting failures in App completion visibility and WorkspacePrimitives forced-color contracts.

Release note: 会话列表和搜索不再限制为最近 100 个会话;发送消息不再截断超过 500 条的历史记录。
2026-09-19 21:34:42 +08:00
mesalogo b5d8204986 fix: remove graph extraction budget and unify image download buttons
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Graph extraction could exhaust its fixed 8192-token budget before returning
structured content. OpenAI requests now omit output-token limits, while
Anthropic uses the model connection limit required by its protocol.
Provider limits, timeout, cancellation and bounded response handling remain.

Use the shared accessible icon button for image downloads in chat and the
image viewer. Add graph request regression coverage and document the limits.

Release note: 知识图谱抽取不再额外限制为 8192 tokens;OpenAI 由模型服务决定输出上限,Anthropic 沿用模型连接配置。聊天和图片预览中的下载按钮统一为带提示的图标按钮。
2026-09-19 21:02:43 +08:00
mesalogo c98eb69cbd feat: refine notes workspace and add canvas zoom
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Simplify sidebar selection and note record framing, compact the empty composer, and emphasize note titles. Replace redundant AI comment headings with an accessible panel toggle.

Add view-only canvas zoom and fit width, correct PDF import/export icons, and allow image files up to 20 MiB. Include regression coverage and updated design documentation.

Release note: Notes have clearer navigation and a more compact writing area. Canvases support zoom and fit width, and image imports accept files up to 20 MiB.
2026-09-19 20:56:03 +08:00
mesalogo df89d96803 fix: remove redundant topbar runtime status
Keep model selection and switching feedback in the composer. Report automatic runtime status failures through notifications and move the send button accessible description into the composer.

Release note: Remove the duplicate top-right model/runtime badge while retaining switching feedback and error notifications.
2026-09-19 20:24:00 +08:00
mesalogo ec08109a09 fix: keep menus and notifications usable above modals
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Parsing test menus and application notifications could appear behind modal overlays or become unreachable while the background was inert. Share a stable top-layer portal that belongs to the owning or active modal, preserving notification timers, keyboard navigation and focus restoration.

Add unit and Electron regression coverage for settings, nested parsing results, application center, inference monitoring and native dialogs, with matching UI guidance and validation evidence.

Validation recorded in the preceding implementation: 4,793 tests passed and 67 skipped; typecheck, lint, isolated production build and Electron UI checks passed. Staged diff whitespace checks passed before this commit.

Release note: Menus and notifications remain visible and usable while settings or other dialogs are open, including keyboard navigation and notification dismissal.
2026-09-19 18:11:39 +08:00
mesalogo 6f06fce039 docs: record attachment UI behavior and OCR validation
Document the compact import cancellation control and progress-card layout. Record the prior full-suite results, focused attachment and Electron checks, and production probe review with their validation boundaries.

Release note: 附件查看结果、更多操作、移除和导入取消采用紧凑图标按钮,并提供悬停提示和键盘焦点说明,减少窄窗口中的布局拥挤。
2026-09-19 17:26:46 +08:00
mesalogo b1eb5d00d5 test: update app regression coverage 2026-09-19 16:53:43 +08:00
mesalogo a9b59ceb1c chore: include document probe and attachment UI refinements 2026-09-19 16:53:21 +08:00
mesalogo 935a8f56bf feat: add persistent document handling and protect note drafts
Add HTTP PaddleOCR-VL settings and shared document previews for chat and knowledge imports. Preserve attachment originals and parsed assets across drafts, messages and queues; support explicit image OCR, selecting document images into a conversation draft, and restoring queued input for editing.

Protect unsaved note bodies and titles during navigation and asynchronous updates. Match canvas analysis to source revisions, invalidate affected comments, preserve annotations during pagination, and provide a resizable record index with saved width preferences.

Existing OCR settings retain local behavior. Historical attachments without saved originals are not reconstructed. Advanced OCR options depend on the configured service; image sending depends on the effective model and Runtime.

Validation: full typecheck and lint passed; 477 focused tests passed across 12 files; staged whitespace checks passed. The full repository suite was not rerun for this commit.

Release note: Keep note drafts safe while switching workspaces, resize the record index, and revisit saved document attachments with OCR previews and selected images in chat.
2026-09-19 16:51:10 +08:00
mesalogo 17186b0c11 feat: add file-backed canvas notes
Magic Notes previously supported rich-text records without a paged drawing or
PDF annotation workflow. Add the integrated PeopleLib Fabric and Quill editor
with flowing text, pen/highlighter, object transforms, images, paper templates,
PDF backgrounds with native text extraction, and raster PDF export. Saves are
manual; undo/redo remains scoped to an annotation page or Quill mode.

Analyze drafts, saved canvases and canvas-source todos with page images and
text when the default model supports images, or an explicitly labelled
text-only fallback. Keep successful saves when automatic analysis fails and
prevent MCP plain-text replacement of canvas records.

Move entry bodies and binary assets into local note files with schema 38
migration and verified readback. SQLite retains metadata, indexes, revisions,
todos and comments. Backups must coordinate SQLite and the adjacent notes
directory while writes are paused or the app is closed; no one-click backup
action is included.

Show a continuous record stream with independent collapsible record index and
AI pane, real first-page thumbnails, and scroll-only record navigation. Open a
blank text/canvas composer by default and reset it after creation. Protect
unsaved drafts and establish normalized editor baselines after saving so clean
records do not prompt again when leaving.

Validation: latest recorded focused run passed 149 tests across five files;
typecheck, lint and git diff --check passed. The prior user-reported full run
passed 4,713 tests before later corrections and UI changes; it is not a full
validation of this final snapshot. Tests were not rerun for this commit, as
requested; independent systematic review follows the commit.

Release note: Create paged notes with handwriting, flowing text and PDF
annotations, browse records through thumbnails, and request AI comments with
an explicit text-only fallback. Existing notes migrate to local file storage;
backups now need both the database and notes directory.
2026-09-19 11:54:23 +08:00
mesalogo c7a93803c9 feat: show runtime checklists and support remote Continue
Display native OpenCode and Continue checklists alongside scheduled tasks, preserving request ownership, explicit clears and saved progress. Bundle both remote runtimes and support managed Continue installation, model routing, cancellation and restart continuation.

Validation: real local and Linux remote model checks, full Electron remote Continue flows, typecheck and lint passed. Full suite: 4707 passed, 67 skipped, one OpenCode compaction failure passed targeted rerun.

Release note: Display native execution checklists above conversations and support managed remote Continue projects with saved progress and cancellation recovery.
2026-09-19 10:47:27 +08:00
mesalogo 164e89253a fix: reduce repeated rendering during subagent progress
Reuse unchanged task and activity state, preserve activity nodes across reordering, and isolate historical tool rendering. Add regression coverage for progress, terminal states, copying, and component reuse.

File-level snapshot also includes existing runtime checklist and remote Continue UI changes in the selected files. Their remaining dependencies are still uncommitted in the worktree; this commit is not a standalone validated build.

Validation on the combined worktree: typecheck and lint passed; full tests had one packaging timeout that passed separately. Real-model UI runs verified parallel child tasks, progress, details, and persistence with existing internal-network certificate trust unchanged.

Release note: Reduce unnecessary UI updates while subagents stream progress and preserve activity list state when records reorder.
2026-09-19 01:39:17 +08:00
mesalogo a8d1dbc11f fix: give notes and todos dedicated workspace layouts
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Replace the crowded note sidebar with a card overview and full note detail. Keep tasks in a stable split view with source content and AI comments, compact header navigation, and search-adjacent status filters. Preserve drafts and return context.

Validation: 93 focused tests, typecheck, lint, and isolated Electron renderer checks passed. Full suite was interrupted; renderer probes used in-memory APIs.

Release note: Notes now open from a card overview, while todos keep a stable task list beside their details. Compact view and filter controls leave more room for reading and editing.
2026-09-18 21:22:58 +08:00
mesalogo 5aad0cf5f7 fix: use a compact single-column application center 2026-09-18 16:31:13 +08:00
mesalogo 9bd0dae865 fix: prevent knowledge search icons from overlapping text
Preserve icon spacing in graph and document search fields against shared input styles. Update scheduled approval tests to expand the collapsed project task list before interacting with task approvals.

Release note: Knowledge search fields now keep the search icon clear of placeholder and entered text.
2026-09-18 16:03:10 +08:00
mesalogo daa14d0b7e fix: keep project tasks compact with visible running counts
Collapse the project task list by default while retaining create and filter actions. Move the project name into the conversation statistics card and keep the running task count visible independently of list filters.

Validation: 60 sidebar tests, typecheck, and lint passed. The earlier full test run encountered an offline dependency package failure and timed out.

Release note: 项目任务默认收起,标题旁显示运行中任务数量;项目名称整合到当前会话统计卡片中。
2026-09-18 15:48:26 +08:00
mesalogo ca3c919a34 feat: unify app ordering and simplify app settings access 2026-09-18 15:29:00 +08:00
mesalogo bbae893c45 feat: add app management, local inference controls, and task statistics 2026-09-18 08:20:45 +08:00
mesalogo 6dd6297414 fix: prevent subagent progress content from being clipped
Constrain nested subagent grids so long tool summaries cannot widen progress and final output beyond the card. Preserve text wrapping and local scrolling for code and tables, and extend the layout regression test.

Release note: Subagent progress and results now stay within the card width, keeping long text and tool status controls readable.
2026-09-17 13:06:28 +08:00
mesalogo 04d9ea9eba fix: use monochrome brand icons for messaging channels
Show distinct WeChat, WeCom, and DingTalk marks in channel settings and the project dropdown. Bundle the SVGs locally and inherit the interface foreground color.

Release note: 远程通道设置和项目下拉列表统一使用微信、企业微信、钉钉各自的黑白品牌图标。
2026-09-17 13:01:39 +08:00
mesalogo 94852acf82 fix: keep consecutive requests running and clear finished stop state
Wait for OpenCode session cancellation before reusing a conversation, and replace the session after unconfirmed cancellation. Preserve terminal message state when stale streaming snapshots arrive so finished conversations return to Send.

Validated with full production Electron UI and the configured real text model: queued requests complete, cancellation followed by immediate resend completes, and Stop clears. Focused tests: 362 passed; typecheck and targeted lint passed. Live dispatch counting limitations are recorded in the feature evidence.

Release note: Consecutive OpenCode requests no longer inherit a previous cancellation, and finished conversations no longer retain the Stop button after stale updates.
2026-09-17 10:11:31 +08:00
mesalogo 3639566e1b fix: align magic note actions with conversation list menus
Move pin and whole-note deletion into hover and focus menus on note rows. Preserve editor drafts when operating on notes and reuse destructive confirmation with keyboard navigation.

Commit the eight affected files in full, including existing application-settings integration and UI design edits in those files. Validation was performed against the shared worktree; unrelated changes remain uncommitted.

Release note: 魔法笔记的置顶和删除移至列表项的更多菜单,与会话列表保持一致,并保留未保存的编辑内容。
2026-09-17 00:15:23 +08:00
mesalogo 1698d9fd3e fix: keep valid older agent packages usable after desktop updates
Accept package-declared OpenCode, Node and koffi versions instead of desktop release pins. Preserve signature, hash, protocol and package-internal version consistency checks.

Validate both cached 0.11.18 packages and Linux x64 installation, bootstrap, health and stop. Related regression suite: 326 passed, 1 skipped; unrelated full-workspace check failures are recorded in the remote Host design.

Release note: Desktop updates no longer reject intact older Agent packages solely because their bundled runtime versions differ.
2026-09-16 19:04:54 +08:00
mesalogo 441f14b5f7 docs: organize PR articles by date and version tag 2026-09-16 16:35:44 +08:00
mesalogo cc9ea7c718 fix: prevent send queue flicker when the agent is idle
Claim idle conversation inputs before publishing queue changes so immediately dispatched messages do not briefly appear as pending. Preserve busy conversation queueing and add a regression assertion for notification ordering.

Validation: 369 focused tests, typecheck, and changed-file lint passed. Full-suite runs timed out with failures in other modules; repository lint reports errors in the existing story-graph demo.

Release note: Fix a brief pending-queue flash when sending a message while the agent is idle.
2026-09-16 16:35:13 +08:00
mesalogo 7e11d1099a docs: add product PR workflow and article templates 2026-09-16 15:17:16 +08:00
mesalogo 0a8439a855 fix: allow larger note searches and recoverable MCP validation errors
Raise note_search limit to 100 while preserving the default and output budget. Return scoped-tool argument validation failures as tool errors so models can correct and retry instead of receiving MCP internal errors.

Validation: 64 focused tests, typecheck and changed-file lint passed. Full tests timed out with two heartbeat migration failures; full lint reports unrelated story-graph demo errors.

Release note: 笔记搜索单次最多支持 100 条结果;内置 MCP 参数错误会返回明确提示,便于模型纠正后重试。
2026-09-16 15:02:11 +08:00
mesalogo da23810205 fix: show task approvals in their task cards
Task Center previously separated task approvals from their tasks and showed an empty approval section. Associated approvals now appear with allow-once and deny actions inside task cards, and those tasks appear under Active. Only unassociated approvals retain a separate section; no approval section or badge is shown when there are no pending requests.

Release note: Review and handle pending approvals directly in their Task Center cards. Tasks awaiting approval appear under Active, while requests without a matching task remain separately accessible.
2026-09-16 14:31:28 +08:00
mesalogo cb245a6402 fix: simplify task center scopes and status filters
Task Center previously split unbound tasks into a global scope and tasks needing attention into a separate filter. Keep Current project and All projects, label unbound tasks, and restore legacy global scope preferences as all-projects without changing task data.

Keep Active, Paused, and Finished filters. Active includes idle, queued, running, waiting_approval, failed, and interrupted tasks; task details and project-scoped approvals remain available.

Release note: Task Center now offers Current project or All projects and groups tasks needing attention under Active, while retaining project labels and approval actions.
2026-09-16 14:05:36 +08:00
mesalogo e84d7bd185 feat: retain completed project activity until viewed
Keep successful background conversations in project activity until their chat is visible and loaded. Track completion in memory for the current client session; initial history, failures and cancellations do not create completion notices.

Include whole-file companion changes already present in App and shared UI: preserve the workspace beneath Settings, show inline reply-copy feedback, and keep project settings inside the project menu. Include their component tests and documentation; navigation demo source changes remain unstaged.

Validation: 77 focused activity tests, typecheck and lint passed; Electron component fixtures passed 112 checks. Full suite: 4365 passed, 3 failed, 67 skipped (offline inventory timeout and two heartbeat migration failures). Commit-time staged whitespace check passed; tests were not rerun.

Release note: 后台会话完成后保留已完成提示,进入对应聊天后自动清除,避免错过任务结果。
2026-09-16 12:51:58 +08:00
mesalogo 6c045bfe2e fix: preserve remote task success after recoverable tool errors
Align remote ACP tool recovery with local runtime behavior when the model responds after a tool failure and completes normally. Preserve original errors and derive recovery state from committed remote events so reconnecting safely replays partial terminal events.

Validation: 461 focused tests, typecheck, lint, and shared Linux Host recovery scenarios passed. Full-suite validation has two pre-existing heartbeat migration fixture failures involving the pinned column.

Release note: 修复远端 Agent 在读取等工具失败后已正常回复,任务仍被误判为失败的问题,并修复断线重连时的工具状态恢复。
2026-09-16 12:49:17 +08:00
mesalogo eaf71ce16f fix: refine workbar tab overflow controls
Keep the add button after the last tab in the scrollable strip. Show transparent floating scroll arrows on hover without reducing tab space, preserve keyboard and touch access, and reveal activated tabs.

Update tab interaction documentation and regression coverage. Exclude documentation demos from ESLint.

Validation: 66 focused tests, typecheck and lint passed. Full tests reported Agent package and runtime reuse failures before timeout; desktop visual verification remains pending.

Release note: Workbar tabs now keep the add button beside the last tab and reveal floating scroll arrows when hovering over an overflowing tab bar.
2026-09-16 00:07:39 +08:00
mesalogo f1d7197480 fix: open browser popup links in managed tabs
Links requesting a new window were silently blocked. Open them in managed browser tabs with the existing session restrictions, clean up script-closed tabs, and follow click-created tabs in subsequent direct and MCP browser tools.

Validation: 232 targeted tests and Windows Electron E2E passed; typecheck and lint passed. The full suite retains six failures outside the popup path.

Release note: Browser links that open a new page now open in a managed tab, and AI browser actions follow the new page after a click.
2026-09-15 23:55:52 +08:00
mesalogo 625182c3dc feat: pin conversations and float their action menus
Persist conversation pins across restarts and show pinned conversations first, including older history. Anchor action menus above the list without shifting neighboring rows, with keyboard dismissal and sidebar-close cleanup.

Add schema 36 migration, focused regression coverage, and feature documentation. File-level staging also includes existing workbar styles and browser contract adjustments in shared files.

Validation: 145 backend tests and 237 App tests passed; typecheck and lint passed. Full test run timed out with Runtime and Agent package failures; native Electron visual validation remains pending.

Release note: 会话菜单新增置顶和取消置顶,重启后仍保留;操作菜单现在悬浮显示,不再挤动下方会话。
2026-09-15 23:39:06 +08:00
mesalogo b16b0d10e7 fix: update workbar tab titles when switching language
Derive application and default terminal titles from the current locale while preserving renamed and unmarked legacy terminal titles. Add locale-switch and layout-restoration regression coverage.

File-level scope also includes existing browser popup selection and close handling in the sidebar, its regression test, and workbar documentation updates.

Validation: 74 focused tests passed; typecheck and lint passed. Full test runs timed out with failures in settings navigation, Agent packaging, and database migration tests. Electron verification remains outstanding.

Release note: Workbar application and new default terminal titles now follow the interface language, including after restoring a layout; custom and legacy terminal names are preserved.
2026-09-15 23:21:51 +08:00
mesalogo a383b2d11f fix: reduce conversation history memory use
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Load lightweight conversation summaries and hydrate full history only when
needed, preserving search, copy, export, queued requests, and remote questions.
Keep unchanged image reconciliation messages stable so list refreshes no
longer resend the entire saved history.

Prepare Desktop 0.13.6 with the approved bilingual notes and aligned feature
matrices. Agent remains 0.13.0; no LoongArch preview or navigation demo edits
are included. Development validation passed 4301 tests with 67 skipped;
the user requested CI validation instead of repeating local release checks.

Release note: 历史会话按需加载,减少切回应用时的重复保存和内存占用,同时保留完整历史、搜索与续聊。
2026-09-15 20:33:47 +08:00
mesalogo f47049c40d Merge github/main README updates into release candidate
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Preserve the published English and Chinese README community QR-code updates. Desktop 0.13.5, Agent 0.13.0, approved bilingual release notes, and the validated product code remain unchanged.
2026-09-15 17:29:33 +08:00
mesalogo 4c18d8d9c6 chore: prepare Desktop 0.13.5 and Agent 0.13.0 releases
Record the approved bilingual release notes and align Desktop and Agent version metadata. Refresh both feature lists with image tools, PPTX OCR, Runtime process reuse, and the completed/to-implement status convention.

Agent 0.13.0 requires Desktop 0.13.5. Keep Node 24.19.0 and OpenCode 1.18.29 unchanged, preserve published release history, and exclude the LoongArch preview.

Validation: bilingual release notes verified; 4293 tests passed and 67 skipped; typecheck, lint, version consistency, and 63 documentation links passed. No new real model calls, Host reruns, local production build, or packaging. Full UI-driven image invocation and local/remote switching are not covered by the existing validation records.
2026-09-15 17:25:12 +08:00
mesalogo 8a63ac9ee8 fix: recognize PPTX images and release idle OCR memory
Image-only PPTX attachments failed with no indexable text even when a local
OCR model was configured. PPTX parsing now recognizes embedded PNG, JPEG
and WebP images through the existing local OCR path, preserves native text
and slide locators, and honors the selected parsing workflow.

OCR workers previously retained model sessions and WASM memory indefinitely
after successful parsing. Release the worker after 60 seconds with no active
or queued requests, reuse it for continuous work, and reload on demand.
Fast text/index modes still skip OCR. This does not render Office layouts,
recognize inherited master images, or change Agent execution permissions.

Validated four real PPTX attachments (40 pages), idle memory release and a
nine-page reimport after model reload on Windows x64 with PP-OCRv6 Tiny.
49 local OCR requests and zero text-model calls. All 4293 tests passed
with 67 skipped; typecheck, lint and the isolated Electron build passed.

Release note: 修复已配置本地 OCR 时图片型 PPTX 仍无法导入的问题,识别结果保留幻灯片定位。OCR 空闲 60 秒后自动释放模型运行内存,连续解析复用模型,下次使用时自动重新加载。
2026-09-15 16:58:28 +08:00
mesalogo 02e59427a1 docs: plan private app market and local model management
Record YAML application import/export, configurable market URLs, and future ShareServer distribution responsibilities. Define a local model management entry with managed-process and external-service boundaries; both remain planned capabilities.
2026-09-15 16:52:57 +08:00
mesalogo 34215c72a1 docs: define runtime checklist display scope
Specify read-only OpenCode and Continue checklists above conversations, including remote Agent delivery and validation requirements. Record native runtime test evidence and distinguish planned work from implemented behavior.
2026-09-15 15:59:13 +08:00
mesalogo f8d07419d2 feat: generate and edit conversation images across runtimes
Add model-controlled image invocation and read-only runtime assignments. Route image tools through Main so accepted operations retain their originating conversation when chat runtimes change, with saved references, cancellation, and manual recovery.

Keep Node hashing outside browser-shared contracts to restore production builds. Validation includes the production build, typecheck, lint, focused regressions, real Host transport checks, and two successful real image requests. Full UI-driven runtime switching acceptance remains documented as pending.

Release note: 开启图片模型的会话调用后,聊天助手可调用图片生成与编辑工具,Runtime 分配自动管理。图片成果保存在原会话,可继续修改,并独立显示生成状态和取消入口。
2026-09-15 15:51:52 +08:00
lofyerandGitHub d898f420fd Merge pull request #4 from mesalogo/jojocoding-patch-3
Update README.md
2026-09-15 08:20:55 +08:00
lofyerandGitHub c4492e4a92 Merge pull request #5 from mesalogo/jojocoding-patch-4
Update README.zh-CN.md
2026-09-15 08:20:34 +08:00
mesalogo e5aa9ed688 fix: reuse runtime processes across project sessions
Multiple projects could spawn duplicate heavyweight runtimes, while settings
changes and remote reconnection could cancel or strand native conversations.
Share compatible local OpenCode and DeepSeek Harness process owners and
Agent-owned ACP connections, retaining workspace routing, per-session modes,
model attribution, question ownership, targeted cancellation and idle cleanup.

Restore remote pending questions and terminal task projections across reconnect
and desktop restart, and preserve accepted ordinary requests during settings
replacement while cancelling expert-team parents with their replaced scheduler.
Extend regression coverage and Agent type checks. Record measured resource and
real-model validation results, including failures and limitations.

Also include the conversation media-generation design drafts (not an implemented
feature), documentation entry links and dedicated Linux test-directory guidance.
Desktop and Agent changes must be delivered together for shared process results.

Validation: typecheck and lint passed; staged content and documentation links
checked. Prior full regression and live-model evidence are recorded in progress
notes; no new full test suite or model calls were run for this commit.

Release note: 多项目会话现在复用兼容的 OpenCode 和 DeepSeek Harness 进程,减少重复启动并保持工作区、工具和取消隔离。修复远端问答在断线、重启及配置更新后的恢复与状态同步问题,Desktop 与 Agent 需配套更新。
2026-09-15 08:14:42 +08:00
mesalogo b755951ecd fix: offer recovery for blank desktop windows
React render failures now show a localized reload page, and renderer crashes offer native reload confirmation. Record renderer and load failures in existing diagnostics without automatic retry loops. Reload may lose unsaved input; the intermittent failure cause remains unconfirmed.

Release note: Recover from desktop rendering failures using a visible Reload action instead of being left without a recovery entry point.
2026-09-15 08:07:08 +08:00
mesalogo d4f1668c9b feat: paste local files into chat attachments
Copied local files previously required the attachment picker. The chat
composer now imports one or more supported files directly from paste,
sharing parsing progress, limits, and batch rollback with file selection.
Text and screenshot paste remain available, and failures preserve drafts.

Validation: prior 166 focused tests, typecheck, and lint passed; 28 targeted
tests passed again before commit. The prior full suite had 4214 passes,
67 skips, and 6 window-recovery failures; window tests passed 10/10 alone.
Native clipboard behavior was verified on Windows; macOS/Linux remain unverified.

Release note: 现在可在文件管理器中复制一个或多个支持的本地文件,直接粘贴到聊天输入框添加附件;可查看读取和解析进度,失败时保留草稿与已有附件。
2026-09-15 08:05:31 +08:00
mesalogo e1fe1cf10a fix: refresh magic notes after external updates
Subscribe the open Magic Notes workspace to committed database changes from desktop and Agent/MCP writes. Preserve selections and unsaved drafts during refresh and external deletion.

File-level commit includes existing runtime edits in shared files; their remaining dependencies are still in the working tree.

Release note: Magic Notes now updates automatically after Agent/MCP edits while preserving your selection and unsaved drafts.
2026-09-15 00:31:21 +08:00
mesalogo fbba2e3abf fix: place workbar toggle beside theme switch
Move the assistant workbar toggle from the floating content corner to the topbar immediately before the theme switch. Preserve toggle behavior and update layout tests and documentation.

Release note: The workbar toggle now stays beside the light/dark theme switch instead of floating over the content area.
2026-09-14 23:37:37 +08:00
mesalogo 511f50aa63 fix: keep remote recovery actions inside the project menu
Remove the duplicate recovery button beside New project and its extra layout column. Keep the collapsed failure status and the retry action on the affected project inside the dropdown.

Stage complete files as requested, including existing App snapshot regression coverage and remote runtime design updates in those files.

Validation: 29 ProjectSwitcher tests and the targeted App recovery test passed; typecheck and lint passed. Full test run timed out after 60 seconds.

Release note: Remote recovery retries now stay in the project dropdown, keeping the project header stable when a connection fails.
2026-09-14 22:16:13 +08:00
mesalogo 6c1e8105dd fix: keep project activity submenu height independent
Opening long conversation lists stretched and repositioned the project menu. Give the conversation submenu its own surface and positioning, aligned to the selected project row with independent viewport clamping and scrolling. Use compact navigation when neither side has room.

Validation: 13 component tests, typecheck and lint passed; 108 Electron fixture checks passed across 14 light/dark scenarios. Full test run timed out after 60 seconds.

Release note: Project activity menus now keep the project list stable when opening long conversation submenus.
2026-09-14 22:10:35 +08:00
jojokeeandGitHub 3b83c90d0f Update README.zh-CN.md
更新社群二维码
2026-09-14 16:43:20 +08:00
jojokeeandGitHub 327b06587b Update README.md
更新社群二维码
2026-09-14 16:42:30 +08:00
mesalogo de14a6157a fix: hide project activity summary when idle 2026-09-14 09:06:36 +08:00
mesalogo 81ae9e0324 chore: prepare Desktop 0.13.2 and Agent 0.11.25
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Prepare bilingual notes and aligned feature lists for the schema-35 history repair, retained tool summaries, Windows DeepSeek workspace paths, and compact project activity navigation.

Agent 0.11.25 is a maintenance release with unchanged protocol and runtime pins. Its compound packages require Desktop 0.13.2. Desktop targets the standard six platforms without a LoongArch preview.

Make activity interaction tests wait for the loaded conversation and enabled send button, reset response mocks between cases, and wait for completed answer rendering. Accept bilingual maintenance-only Agent notes without inventing features or fixes.

Validation: 4,155 tests passed, 66 skipped; typecheck, lint, and release notes verification passed. The affected 20 tests passed three consecutive runs; all 227 App and Agent release-note tests passed. No local production build or packaging was run.
2026-09-14 01:18:42 +08:00
mesalogo f49d6678a3 fix: refine project activity spacing and conversation rows
Add 8px below the activity summary to separate it from new conversation. Remove redundant conversation-row arrows while keeping project submenu arrows, and update the UI documentation.

Validation: 11 ProjectActivity tests and typecheck passed. Lint was interrupted before completion.

Release note: 调整项目活动摘要与新建会话的间距,移除会话列表末尾的多余箭头,保留项目子菜单指示。
2026-09-14 00:40:10 +08:00
mesalogo 0d7ba55a50 fix: preserve tool details and prevent repeated history growth
Show bounded operation summaries for Continue, DeepSeek Harness and direct-model tools, retaining input details across incremental updates. Use one canonical workspace path for DeepSeek Host startup and ACP sessions so equivalent Windows paths reuse a working Runtime.

Compare persisted subagent tool fields by value, release terminal task caches, and avoid rebuilding caches for duplicate remote events. Upgrade to schema 35 to recompact repeated schema-34 tool blocks while preserving events and provenance. Include regression coverage, validation records and temporary-resource test exclusions.

Validation: 4155 tests passed, 66 skipped; typecheck and lint passed. Full desktop real-model validation covered all three tool-summary paths and both Windows path spellings. Storage validation evidence includes local and remote tool workloads and real database upgrade checks.

Release note: Tool records now show paths, commands and search details across runtimes. Fix DeepSeek Windows workspace failures and repeated subagent history growth; schema 35 reclaims redundant history data while preserving execution records, and older clients cannot reopen the upgraded database.
2026-09-14 00:34:51 +08:00
mesalogo c71da4b6ae fix: browse project activity through cascading menus
Replace the production activity dialog with anchored project and conversation menus, preserving exact conversation navigation and adding keyboard and narrow-window coverage. Include the interactive demo and design documentation.

The demo hides idle activity and removes conversation arrows; production still retains those two elements. Validation: typecheck, lint and 11 ProjectActivity tests passed. The full suite timed out after 240 seconds with an agent-package offline inventory test failure.
2026-09-13 23:06:20 +08:00
mesalogo ba6f6b8f22 fix: show operation details in OpenCode tool records
Preserve OpenCode operation titles in main-session tool summaries, including permission waits, failures and interrupted calls. Keep title trimming and length limits aligned with subagent records.

Update tool display documentation and include the activity-menu wording in UI-DESIGN.md. Validation: 132 focused tests passed; main-process typecheck and changed-file lint passed. Full-suite checks were affected by concurrent activity-menu changes and a test timeout.

Release note: OpenCode main-session tool records now show file paths, commands or operation descriptions beside tool names, matching subagent records.
2026-09-13 22:55:12 +08:00
mesalogo 49377206cc fix: stop subagent history from exhausting disk space
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Repeated full subagent progress snapshots could grow the desktop database to
many gigabytes during ordinary long-running tasks. Local and managed SSH
writes now retain lossless progress changes while preserving conversation
snapshots, event identities, remote replay deduplication and transaction rollback.

Upgrade existing history in bounded worker-thread batches, show progress and
retry controls, and reclaim freed disk space after conversion. Schema 34 cannot
be opened by older clients; a pre-upgrade backup is required for client rollback.
Document reusable database migration guidance and the verified large-database,
portable, local Runtime and Linux Host evidence.

Prepare Desktop 0.13.1 with the approved bilingual notes and aligned feature
lists. Agent remains 0.11.24. Final candidate validation and production builds
will run on main CI, as requested, without additional local release testing.

Release note: 修复长任务和子任务造成的数据库异常膨胀;升级时自动整理旧记录并回收空间,保留聊天、执行详情和结果,并支持退出后继续处理。
2026-09-13 20:50:33 +08:00
mesalogo 1c22e1a06d fix: keep browser overlays and knowledge controls usable
Native browser pages could cover application dialogs and overlapping menus
or notifications. Suspend only the viewport while covered and restore the
same active page afterward, including nested dialogs and terminal prompts.

Keep knowledge source controls at their full height, constrain long library
cards and titles, and update automatic names when switching remote targets
without overwriting edited or saved names. Restore packaged release-note
loading now that the history exceeds its previous size limit.

Add focused regressions and real Electron UI evidence. Clean up temporary
Harness fixture directories at test completion instead of accumulating them.

Release note: 修复内嵌浏览器遮挡弹窗和浮层、知识库来源选项被压缩及长名称显示异常;切换远端知识库时自动名称正确更新,并恢复更新说明显示。
2026-09-13 19:46:37 +08:00
mesalogo a0456f687a chore: prepare Desktop 0.13.0 and Agent 0.11.24 releases
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
Record the approved bilingual release notes against Desktop v0.12.12 and
Agent agent-v0.11.23, rather than listing intermediate development fixes.
Desktop adds external knowledge connections and cross-project activity;
remote native questions remain classified as fixes in both release notes.

Align both feature lists, compatibility guidance and validation evidence.
Upgrade Desktop to 0.13.0 before Agent 0.11.24. Keep OpenCode at 1.18.29,
retain existing published notes, and exclude the LoongArch preview.

Allow fixes-only Agent notes while checking bilingual section consistency.
Validation: 4089 tests passed with 66 skipped; release-note verification,
typecheck, lint and documentation link checks passed. Full remote-provider
acceptance and candidate CI/native release validation remain outstanding.
No release tags or publication are part of this commit.
2026-09-13 17:29:37 +08:00
mesalogo 59a1624396 fix: close OpenCode event streams before ending requests
Completed parallel conversations could leave buffered event connections open.
Chat and native context compaction now cancel their own subscriptions before
ending SDK iteration, without cancelling another conversation or its Server.

Strengthen cleanup-order regressions and verify real OpenCode parallel runs,
cancellation and compaction. The remote Agent uses ACP stdio and transcript
polling rather than this SDK event stream and needs no corresponding change.

Validation: 99 focused tests passed; the final candidate passed 4089 tests
with 66 skipped, typecheck and lint. No external model calls were made.
2026-09-13 17:28:40 +08:00
mesalogo a44de1441a docs: record recurring defect types and the mechanism covering each
The titlebar drag-region defect was fixed three times in a row, each time
only for the dialogs someone reported, and the same batch introduced two more
instances. Repeated full-table lookups and production reuse of a verification
helper showed up in several places at once for the same reason.

Add docs/quality/recurring-defects.md describing each type that has actually
recurred here, the shared mechanism or automated test that now covers it, and
the history that justifies the entry. Require reading it from AGENTS.md before
touching overlays, store lookups, retrieval entry points, cancellation, or
render-time derived values, and require extending the shared mechanism rather
than patching one reported case.

Correct the external knowledge technical design where it described the removed
configuration-snapshot checks, the shared retrieval entry point and the
unreachable probe status, and record the refactor and its validation in the
knowledge base progress log.
2026-09-13 16:02:15 +08:00
mesalogo f7f0332143 chore: drop unreachable external knowledge states and helpers
Remove code with no production path: the probeStatus value 'healthy' that no
probe can produce, the external.transportConfirm string and the
remote-missing and config-invalid state strings that nothing renders,
KnowledgeService.searchHybridMany which lost its last caller when chat search
moved to retrieveMany, and the sourceUrl field that no provider parser
populates.

Keep sourceUrl in externalKnowledgeLocatorSchema because that schema parses
already-persisted citations. Point the affected fixtures at catalog-ready,
the status testInstance actually writes.

Document why ProjectActivity adjusts state during render: the guarded call is
the recommended pattern for resetting state on prop changes here, and the
repository lint rule rejects the useEffect alternative.
2026-09-13 16:02:01 +08:00
mesalogo 4222351f3c fix: keep every full-window overlay clickable over the titlebar
The drag-region fix applied to six dialogs left seven overlays uncovered,
including two added in the same batch, so their controls still stopped
responding to hover and clicks where they overlapped the native titlebar.
Three overlays used inset: 38px 0 0 to dodge the titlebar instead of
releasing the drag region, leaving an unmasked strip.

Replace the per-dialog declarations with one shared selector covering all
fifteen overlays and give each one inset: 0. Add a regression test that
discovers every fixed overlay from the stylesheet and fails when a new one
misses the shared rule, so this stops being fixed one dialog at a time.

Release note: 修复 MCP 编辑器、发布说明、远端目录选择、HTML 预览、设置、跨项目活动和外部
知识库实例等弹窗与标题栏重叠时,控件无法悬停和点击的问题。
2026-09-13 16:01:41 +08:00
mesalogo 8ad26d43bb fix: remove repeated full-table scans from knowledge and activity paths
Binding checks loaded and parsed every external knowledge binding to answer a
single-row question. requireLibrary did this on more than thirty call sites
including one inside a task reconciliation loop, and the knowledge snapshot
scanned the table twice per library across its seven callers.

Add keyed store lookups (getBinding, hasBinding, getBindingsForInstance) that
use the existing primary key, build the snapshot's bindings once into a Map,
and count bindings per instance in a single pass.

Memoize the conversation activity index in the renderer. It was rebuilt on
every render, including once per streaming token, which discarded half the
benefit of the memoized activity summary directly above it.

Release note: 知识库页面、对话预检索和跨项目活动列表在绑定较多时响应更快,流式回答期间的
界面开销降低。
2026-09-13 16:01:26 +08:00
mesalogo 74a2f187ba fix: stop external knowledge chat retrieval from running binding checks
Conversation retrieval called the binding verification helper, so every
question sent the user query as a test query and RAGFlow bindings with graph
or knowledge compilation enabled spent an extra detail request before
retrieving. A failed capability check could also surface
EXTERNAL_KB_CAPABILITY_UNAVAILABLE during a normal conversation.

Retrieval and verification are now separate service entry points sharing one
snippet-bounding helper. The capability pre-check stays in the verification
path and keeps using the binding's configured request timeout.

Replace the five configuration snapshot comparisons and the
EXTERNAL_KB_CONFIG_CHANGED code with the existing per-instance cancellation.
A connection test cancelled by a configuration change now rejects without
writing back a probe result, so disabling an instance no longer records it as
a failed probe. Duplicate binding safety relies on a re-check plus the
existing unique index.

Release note: 外部知识库对话检索不再执行仅用于保存校验的能力预检,RAGFlow 图谱绑定每次
提问少一次请求;停用实例时不再把它错误标记为连接失败。
2026-09-13 16:01:12 +08:00
mesalogo ca4a58c762 fix: keep full-window dialogs responsive to mouse input
Render feedback, parsing diagnostics, knowledge confirmations, WeChat binding and image preview overlays through body portals so native titlebar drag regions cannot intercept their controls. Preserve themed form styles and modal focus isolation.

Validation: 320 targeted tests, typecheck and lint passed. Native Windows hover and close checks passed for feedback and parsing diagnostics in light and dark themes at two sizes. Full suite timed out without a summary.

Release note: 修复反馈等弹窗与窗口标题栏重叠时,关闭按钮无法正常悬停和点击的问题。
2026-09-13 14:19:46 +08:00
mesalogo ea45533137 fix: allow follow-up messages after cancelling remote OpenCode questions
Cancelling a managed SSH prompt while answering a question could leave the conversation bound to its previous operation and reject the next message. Record the Agent cancellation outcome and reconcile and close the desktop binding before the next prompt.

Add two cancellation regressions and document real SSH/Electron question validation across eight theme and viewport combinations. Local queues and remote question routing are already included in 3b560c9.

Validation: prior focused checks passed 275 tests with 5 skipped; typecheck and lint passed. The final full run passed 4087 tests with 66 skipped and one packaging timeout; that packaging file separately passed 22 tests with 1 skipped. Staged diff whitespace check passed.

Release note: After cancelling a pending OpenCode question on a managed SSH task, you can send another message in the same conversation without an operation identity error.
2026-09-13 13:57:57 +08:00
mesalogo e9679fecf1 fix: keep project dialog close controls clickable
Render project creation and settings dialogs in a body portal so the Electron title-bar drag region does not intercept their close controls. Cover dialog placement, scrolling, and focus restoration, and document the layout rule.

Release note: Project creation and settings dialogs keep their close buttons clickable after scrolling and restore focus to their trigger when closed.
2026-09-13 13:31:42 +08:00
mesalogo 3b560c9a5e feat: add external knowledge retrieval and cross-project activity
Connect Dify, FastGPT and RAGFlow through read-only bindings, encrypted local credentials, shared retrieval and persistent citations. Configuration and removal affect GoodBuddy only.

Show active conversations across projects, retain older live work, and improve project dialog scrolling. Queue concurrent questions and route managed SSH OpenCode questions through the Agent. Preserve recoverable read/search errors and partial search results.

Document the Electron UI automation standard, real model and external knowledge validation, and remaining acceptance limits.

Validation: earlier full regression passed 4081 tests with 66 skipped; subsequent focused remote-question checks and real Linux evidence are recorded in feature progress. Precommit typecheck and lint passed; latest project dialog and shared UI suites passed 58 tests. The full suite was not rerun for the final dialog changes.

Release note: Use external knowledge in conversations, find active work across projects, and answer concurrent local or managed SSH OpenCode questions. Managed SSH questions require the updated Agent adapter.
2026-09-13 13:10:01 +08:00
mesalogo b84250f62a fix: allow closing browser tabs during active requests
Allow users to close leased browser tabs without cancelling the AI request or revoking its other tools. Closed-page operations report a recoverable error; explicit navigation creates a dedicated replacement tab for direct-model and MCP calls.

Add regression coverage and real Electron lifecycle checks, and update browser behavior documentation.

Release note: 浏览器标签页被 AI 请求占用时也可关闭,其他任务继续运行;再次导航会打开独立新页。
2026-09-13 11:21:37 +08:00
mesalogo 032ebc313f fix: simplify conversation composer options
Consolidate composer settings into a compact popover while preserving runtime, mode, sending, and queue controls. Place the settings icon after voice input, limit the panel to 300px, and show knowledge expansion state.

Include the navigation demo, design guidance, localized labels, and composer regression coverage.

Release note: Simplify the conversation input toolbar with compact settings, clearer knowledge expansion, and visible active selections.
2026-09-13 00:05:05 +08:00
mesalogo 719c9fd6cd fix: add inline clear action to conversation search
Show a clear button when the conversation search contains text. Clearing restores the scoped conversation list and returns focus to the input, with regression coverage for visibility and filtering.

Release note: Conversation search now includes an X button to clear keywords and return to the current project's conversations.
2026-09-12 22:10:25 +08:00
mesalogo d36a05f7ad fix: prevent blank browser reloads and clear stale errors
Require a committed page URL before enabling reload. Clear sidebar action errors when leaving or closing a panel, and ignore late failures from its previous scope.

Add regression coverage and document browser error lifecycle and validation results.

Release note: 修复空白浏览器页刷新时报错、关闭浏览器后错误提示仍残留的问题;切换或关闭面板后,旧操作的延迟失败不再重新显示。
2026-09-12 21:13:44 +08:00
mesalogo fc36433d76 fix: simplify tool execution records in conversations
Replace full-width tool cards with compact disclosure rows in conversations and subagent progress. Remove redundant runtime summaries, keep expansion under user control, and show successful calls with green checks.

Prioritize errors and results, fold inputs, and provide original-text copying through application notifications. Detail text wraps in bounded scroll areas with no extra display controls.

Validation: 3927 tests passed, 66 skipped; typecheck and lint passed.

Release note: Tool activity is now more compact, with clearer status icons and less repeated text. Expand a record to read and copy results, errors, or input parameters.
2026-09-11 14:54:11 +08:00
mesalogo f6287589a3 chore: prepare Desktop 0.12.12 release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Bump the desktop patch version and package matching Chinese and English notes
for image follow-up context, retained questions and answers, simpler Runtime
selection, and docked Git history with automatic project detection.

Review and align both feature matrices with the verified release range.
Agent stays at 0.11.23; no Agent release or LoongArch preview is included.
External knowledge probe tooling remains development-only, not a shipped
external knowledge-base feature.

Validation: 3911 tests passed, 66 skipped; typecheck, lint, and bilingual
release-note verification passed. Production builds use main-branch CI before
the immutable desktop tag; no local production package was built.
2026-09-11 01:21:50 +08:00
mesalogo b2c85b8ece fix: describe saved fixed project models accurately
Projects with a saved fixed Runtime model could incorrectly claim to follow
the global Runtime configuration. Restore the fixed-model description while
keeping the simplified menu and the ability to return to inherited settings.
Model resolution, permissions, and remote Agent behavior are unchanged.

Validation: 3911 tests passed, 66 skipped; typecheck, lint, and release-note
validation passed. Added bilingual regressions for all three Agent Runtimes.

Release note: 项目已固定 Runtime 模型时,现在明确显示所用固定模型,不再误报为跟随全局设置。
2026-09-11 01:21:24 +08:00
mesalogo 073eb0d2f5 fix: dock workspace history and detect Git on project switches
Keep commit history at the sidebar bottom, expand it to half the Git content area, and preserve independently scrolling panes when resizing. Use the shared refresh action and remove the redundant changed-files border.

Load Git status on project changes regardless of restored sidebar selection, and keep status and error fallback scoped to the selected project.

Release note: 提交历史默认贴底,展开占用一半内容区,支持拖拽调整并统一使用顶部刷新。切换项目后自动识别 Git 工作区,无需手动刷新。
2026-09-11 00:51:25 +08:00
mesalogo 3bf720a32c fix: retain submitted questions and answers in conversations
Preserve read-only question and answer reviews after successful submission or skipping, including multiple rounds and local conversation reloads. Keep failed submissions retryable and preserve newer questions and terminal states when responses arrive late.

Validation: typecheck and lint passed; full suite had 3900 passes, 66 skips and one Agent package timeout that passed on isolated rerun. Real OpenCode UI acceptance was not run. Previously discarded answers cannot be recovered.

Release note: 修复 OpenCode 提交回答后会话仅显示已提交的问题;现在保留原问题及对应答案,多轮问答和跳过记录在重新打开会话后仍可查看。
2026-09-10 23:47:44 +08:00
mesalogo 1274c762e8 fix: simplify runtime model choices in conversations and projects
Remove redundant provider and model combinations from conversation and project Runtime selectors. Runtime model configuration stays in system settings, while saved fixed project selections remain visible for compatibility.

Release note: Conversation and project Runtime menus are simpler; change Runtime models in system settings.
2026-09-10 23:00:01 +08:00
mesalogo c0ad27ab44 fix: preserve image context across follow-up requests
Image requests previously sent only the current prompt and attachments, so
follow-up edits could lose the previous image and conversation requirements.
Reuse the latest successful image from the existing artifact store together
with text history, including after reopening the app. Explicit attachments
take priority, and configured model IDs remain unchanged.

When image editing is explicitly unsupported, continue with text-based image
generation and retain a quiet message footer explaining the missing image
context. Keep genuine provider errors and cancellation visible. Add focused
regressions, persisted footer metadata, and aligned feature documentation.

Validated with four real sub-gpt-image-2 image requests and a simulated
unsupported-edit response. Image regressions, typecheck and lint passed;
the full working-tree run had two unrelated concurrent Runtime-menu failures.

Release note: 连续改图会自动带上上一张图片和文字上下文,重开对话后仍可继续修改。上游明确不支持编辑时仍可按文字生图,仅在回复底部轻提示未使用参考图片。
2026-09-10 22:39:41 +08:00
mesalogo 4483c92b57 fix: distinguish external knowledge probe failures from empty results
Validate Dify and RAGFlow outcomes, normalize FastGPT URLs, and report response envelopes, score structures and actual rerank state. Align integration documentation with official APIs and archive 33 redacted read-only requests with strict HTTPS verification.

Validation: 43 focused tests, typecheck and lint passed. Full test runs exceeded 60s and 240s without a summary; this limitation is recorded in the feature progress document.
2026-09-10 22:19:30 +08:00
mesalogo d9114c20bf test: make workspace history fixtures independent of Git identity
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Write fixed commit objects from the fixture index rather than invoking
Git commit with an implicit machine identity. Preserve real history,
rename diff, pagination and dirty branch-switch checks without changing
Git author or committer configuration or skipping any affected test.

Add fixture checks for valid objects, parent linkage, unchanged dirty
content and unchanged repository configuration. Record the CI regression
introduced when the prior test identity override was removed.

Validation: workspace tests passed 11 with 2 filesystem skips; release
notes verification, typecheck and lint passed. Full tests passed 3845 with
66 skipped and one unrelated ActivityPanel timeout; all 16 ActivityPanel
tests passed on isolated rerun. Product source and approved bilingual
Desktop 0.12.11 / Agent 0.11.23 release notes are unchanged.
2026-09-10 19:49:33 +08:00
mesalogo 877cf581f6 chore: prepare Desktop 0.12.11 and Agent 0.11.23 releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Record the approved bilingual release notes, classify long-task limits and
premature truncation as fixes, and align both feature matrices with the
verified workspace, browser, scheduling and Runtime behavior.

Advance the Agent lock to 0.11.23 for remote file and Git management, optional
model limits, larger bridge exchanges and the unlimited-duration connection
fix. Agent publication reads version-specific approved notes and remains
separate from Desktop Latest. Upgrade Desktop to 0.12.11 before updating Hosts.
No LoongArch preview is included.

Validation: release notes verification, all 3845 tests (66 skipped), typecheck
and lint passed. Current-source Linux x64 workspace and bridge checks passed;
managed ACP Ask and Execute completed two real model requests, both HTTP 200,
with delivered results and cleaned test resources. Native production packages
and publication remain subject to GitHub Actions after candidate approval.

Release note: 准备 Desktop 0.12.11 与 Agent 0.11.23 发布;远程文件和 Git 管理、长任务限制及连接超时修复需先升级桌面端,再更新 Host 上的 Agent。
2026-09-10 14:27:41 +08:00
mesalogo 0eb5205b1c fix: simplify workspace controls and branch search
Group the workspace view switch and refresh action in one header, show only
file or Git actions for the active view, and remove the isolated root icon.
Subdirectory breadcrumbs retain root navigation and restore keyboard focus.
Use the shared form style for branch search in a bounded, scrollable panel.

Confirm that fresh settings already default to automatic Execute tool
authorization, and add regression coverage without changing saved policies.
Update the workbar design and record isolated production Electron validation.

Validation: 354 focused tests, typecheck, lint and development bundle passed.
The full suite passed 3843 tests and skipped 66; an unchanged OpenCode stream
lifecycle timing test failed once and passed its isolated rerun.

Release note: 整理工作区文件与 Git 工具栏,移除孤立的根目录图标并保留子目录导航;分支搜索框、文字和按钮统一样式,宽窄工作栏下均保持紧凑布局。
2026-09-10 13:54:34 +08:00
mesalogo c037d7fb20 fix: complete runtime and workspace workflows
Restore Ask workspace reads and output paging without granting write access,
and let programming Subagents reuse their parent's browser page. Allocate
browser resources only when used, keep Agent pages aligned with workbar tabs,
recover released bindings, and remove unused automatic screenshot work.

Preserve inherited Runtime selections during remote usage persistence and
manual compression. Keep viable channel model fallbacks, display the actual
scheduled-task mode and Harness model source, and avoid rediscovery delays
when calling healthy MCP tools. Fix directory/history refresh, case-only
renames, rename-aware commit diffs, and FastGPT probe success reporting.

Add regression coverage and align feature documentation. The Agent-side
connection timeout fix requires deploying an updated Agent; existing Ask
read-only and Execute authorization boundaries remain unchanged.

Final validation: 3839 tests passed, 66 skipped; typecheck, lint, development
build, and diff checks passed. Isolated Electron UI/MCP workflows passed with
a loopback model fixture. Current-source Linux x64 Host checks passed with
three successful real model requests. Chromium loadURL rejection was not
reproduced in the UI; released-binding recovery has automated coverage.

Release note: 修复 Ask 读取项目文件、浏览器页面显示与恢复、文件管理及模型配置跟随中的不一致,并减少无用截图和 MCP 等待。定时任务显示实际工作模式,远程会话可正常保存继承 Runtime 的用量;远端连接超时修复需更新 Agent 后生效。
2026-09-10 12:48:30 +08:00
mesalogo 020a9ad0dc fix: retain conversation retrieval settings for scheduled messages
Persist the conversation knowledge retrieval mode across restarts so timed messages use the saved setting. Clarify shared conversation authorization in the scheduled-task documentation and correct the renderer test query type.

Validation: 374 focused tests passed; typecheck and lint passed. The full suite had two failures that passed on focused reruns. Real remote validation remains blocked by the temporary harness SDK module loading error; no model requests were made.

Release note: 修复会话知识检索模式在重启后丢失的问题,定时消息继续使用会话保存的检索设置,并同步任务授权说明。
2026-09-10 10:36:26 +08:00
mesalogo 444902d8ad fix: preserve conversation settings for models and scheduled messages
Preserve explicit Runtime model selections and Harness configuration rules without pinning resolved defaults. Expose fixed Runtime models in project settings and keep SSH conversations inheriting their project selection.

Dispatch scheduled instructions through ordinary conversation requests using current history, Runtime, work mode, and knowledge scope. Persist conversation work modes and update scheduling documentation and regression tests.

Validation checkpoint: typecheck, lint, and 104 focused tests passed. The SSH inheritance regression passed in isolation, but its process timed out on exit. Full-suite completion, remaining model documentation review, and real end-to-end validation are still pending.

Release note: Preserve fixed model choices and let scheduled messages use their conversation's current settings and history.
2026-09-10 10:17:45 +08:00
mesalogo fedafa21bd fix: pin workbar tab order and preserve browser titles 2026-09-10 08:46:17 +08:00
mesalogo c1848a9dc4 test: align runtime fixtures across platforms
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Account for Electron-as-Node when probing the current executable and update the Linux workspace registry scenario to validate the writable handle behavior shipped with remote file management.
2026-09-10 07:01:48 +08:00
mesalogo 21ac8330ea fix: preserve runtime configuration references
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Projects, channels, conversations, and Agent Runtimes now retain dynamic default references while explicit model selections remain pinned. Compatible fallback resolution stays aligned across settings validation, runtime creation, labels, context metrics, DeepSeek Harness browser routing, and remote project saves.

Prepare the bilingual Desktop 0.12.11 release metadata and keep Agent 0.11.22 as the published remote baseline.

Release note: 修复默认模型与 Runtime 配置变化后项目、通道和对话选择失去跟随关系的问题;显式选择仍保持固定,新请求使用当前兼容配置。
2026-09-10 06:27:48 +08:00
mesalogo b01a856bae fix: remove avoidable model runtime limits
Preserve long histories and parsed attachments, expose paged process and Subagent output, isolate MCP discovery failures, and expand browser snapshots and knowledge result selection. Remove fixed Harness output and updated remote Runtime duration limits.

Keep released Agent profile and v1 bridge compatibility while enabling larger framed exchanges and optional limits for updated peers. Document remote coverage requirements and real-model boundary validation.

Release note: Long model tasks can retain more context and continue reading complete tool output; remote execution preserves compatibility with released Agents. Updated remote limits require an updated Agent and Runtime bundle.
2026-09-10 03:01:21 +08:00
mesalogo 80b0b18712 feat: add persistent workbar browser tabs and scoped MCP routing 2026-09-10 01:26:56 +08:00
mesalogo 428b4c0d08 fix: synchronize scheduled task status and run actions
Reflect queued and active schedule runs in task status, refresh tasks on queue changes, and prevent duplicate Run Now actions until the current run finishes. Add lifecycle regression coverage and update scheduled task behavior documentation.

This file-level commit also includes concurrent conversation history, browser workbar, and runtime selection changes in the affected files. Focused schedule regressions passed; full workspace validation has unrelated failures as recorded during development.

Release note: Scheduled tasks now refresh their queued and running status and keep Run Now disabled while an occurrence is active.
2026-09-10 01:24:51 +08:00
mesalogo ec100f8176 feat: improve direct model workspace tools
Direct models now use bundled ripgrep for workspace discovery, read files through resumable pages, and edit multiple files with apply_patch. Ask can search and read while Execute retains write access, and fixed total input limits no longer constrain these tools.

Release note: 直连模型现可使用随包 ripgrep 搜索项目、分页读取任意大小的文本,并通过多文件补丁修改代码;设置中的相关能力统一显示为工作区文件和开发工具。
2026-09-10 00:45:03 +08:00
mesalogo c1f8ba80ee test: validate external knowledge provider contracts
Add read-only Dify, FastGPT, and RAGFlow probes, a redacted live capability baseline, and focused adapter tests. Document version-specific API and UI behavior, including Dify 0.x/1.x differences and RAGFlow GraphRAG evidence.
2026-09-10 00:33:09 +08:00
mesalogo f3b695214b fix: make workbar browser fullscreen 2026-09-09 21:15:28 +08:00
mesalogo abd3d5c6bb fix: keep workbar tab close background transparent 2026-09-09 20:01:03 +08:00
mesalogo f669e490b8 feat: add workspace file management and embedded browsing
Users previously had to leave the workbar to manage project files and inspect Git branches or commit history. Add file and directory creation, rename, move, delete and properties, plus searchable branches, branch creation and switching, explicit fetch, and commit file diffs.

Keep file previews in the right workspace pane with directory expansion, selection, scroll and focus preserved on return. Add compact action menus, directory pickers, preview refresh and source switching. Embed the shared browser session directly in the workbar with navigation, resizing and fullscreen controls instead of a separate interaction window.

Moves stay within the current workspace, deletion is permanent after confirmation, and Git switch reports conflicts without discarding or stashing changes. Typecheck, lint and staged whitespace checks pass; existing full-suite failures and the blocked real remote directory validation remain recorded in the workbar progress document.

Release note: 可在右侧工作区管理文件、切换 Git 分支并查看提交历史,返回预览后保留浏览位置;浏览器支持在工作栏内直接交互和全屏显示。
2026-09-09 15:47:24 +08:00
mesalogo b6d4633a33 docs: design ShareServer Office editing 2026-09-09 12:43:00 +08:00
mesalogo 5782667a40 fix: accept equivalent remote runtime lock formatting 2026-09-09 11:29:37 +08:00
mesalogo e35ea7692f test: avoid disk-bound history limit fixtures
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Use real in-memory SQLite for the remote-message ordering and active-task
retention query tests. Both exceeded the CI five-second limit while seeding
502 independently committed records on disk.

Keep the record counts, public database calls, and assertions unchanged.
Disk persistence, transactional migration, and reopen tests remain disk-backed.
Production database behavior is unchanged.
2026-09-09 03:33:25 +08:00
mesalogo 5a77dfdb93 fix: prevent managed runtime stalls during file changes
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Automatic native Git snapshot diffs could block concurrent OpenCode sessions
after broad rewrites or line-ending conversions. Managed desktop and Agent
profiles now skip snapshots that GoodBuddy does not use, while preserving
file tools, native subagents, workspace Git operations, and Ask/Execute rules.

Drain otherwise-unused Continue utility-host stdout to prevent console output
from filling the pipe and blocking requests. Correct the lifecycle regression's
body observer to watch the underlying reader rather than a buffered wrapper.

Prepare Desktop 0.12.10 and Agent 0.11.22 with aligned bilingual notes and
feature documentation. Existing runtimes require restart or Agent update.
Full desktop UI before/after and Linux Agent production-path checks passed;
3626 tests passed, with 61 existing platform/manual skips.

Release note: 修复批量文件改写或换行转换后的托管 OpenCode 并行停顿,以及 Continue 控制台输出阻塞;本机升级重启后生效,远程项目请配套更新 Agent。
2026-09-09 03:07:55 +08:00
mesalogo 177ab2140e fix: align chat messages and composer without horizontal overflow
Move message avatars into outer gutters and align message content with the composer while preserving existing colors and styling. Keep scrollbar space in the outer gutter to prevent horizontal scrolling.

Release note: Chat avatars now sit outside message content, with the composer aligned to message edges and no extra horizontal scrollbar.
2026-09-09 01:51:27 +08:00
mesalogo cbb6398851 fix: resolve npm for portable runtime packaging 2026-09-09 01:43:53 +08:00
mesalogo b1ba14f98e fix: move todo source entry to detail pane 2026-09-09 01:26:10 +08:00
mesalogo e95c7a60d4 chore: prepare 0.12.9 desktop release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Record the approved bilingual notes for todo source entries, accurate
request status, parallel OpenCode reliability, image editing, native
questions, longer Continue requests, reasoning formatting, and activity
history notices. Keep all previously published release entries intact.

Synchronize package and feature-matrix versions for the standard six-target
desktop release. Agent remains 0.11.21; no Agent release or LoongArch preview
is included. Release tags remain pending exact-commit approval and CI.

Validation: 3618 tests passed, 61 skipped; typecheck, lint, and bilingual
release-note verification passed. No local production package was built.
2026-09-08 23:01:14 +08:00
mesalogo c82d0161a9 fix: show accurate request status in message footers
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Requests could appear to keep working while OpenCode waited to retry, and
retry backoff was described as an active attempt. Report native OpenCode
retry timing, distinguish direct-model backoff from dispatch, and show
preparation, tool activity, pending input, and terminal states accurately.

Keep only the message-footer indicator static. Conversation-list activity
indicators, send/stop controls, retry limits, and command replay behavior
remain unchanged. Align the feature documentation with verified behavior.

Validation: 3618 tests passed, 61 skipped; typecheck and lint passed.
Isolated desktop validation used five real text-model calls across three
profiles, with controlled local fault injection for retry transitions.

Release note: 消息底部现在准确区分请求准备、等待重试、开始重试和需要回答的状态,不再用闪烁暗示仍有进展;会话列表闪点及发送、停止行为保持不变。
2026-09-08 22:47:06 +08:00
mesalogo 9b7cf730d0 fix: raise Continue host body limit 2026-09-08 21:58:51 +08:00
mesalogo 22a60497cb fix: preserve reasoning markdown boundaries 2026-09-08 21:55:47 +08:00
mesalogo 78418a56fa fix: remove native question limits 2026-09-08 21:52:10 +08:00
mesalogo fbc0f5e9f3 feat: show source entries in todo view 2026-09-08 21:38:33 +08:00
mesalogo c1f4fd0580 fix: prevent parallel OpenCode conversations from hanging
Starting another conversation could replace a busy local OpenCode server
after a brief health-check failure, leaving the existing conversation
reconnecting to a dead endpoint.

Keep active conversations and native Compact operations on their current
server. Propagate server exit and shutdown to bound requests and cancel
event-stream retry waits so failures are explicit instead of hanging.
Do not automatically replay commands.

Release note: 修复同时使用多个本机 OpenCode 会话时旧会话可能一直等待的问题。服务异常退出时会明确提示失败,不会自动重复执行命令。
2026-09-08 21:10:35 +08:00
mesalogo 28de0c45ba fix: restore image editing requests
Image editing requests included a generation-only response_format field that some compatible providers reject. Editing now sends only supported defaults while preserving explicitly configured custom request fields.\n\nRelease note: 修复部分图像服务商修改图片时报错的问题;参考图编辑现在可以正常提交。
2026-09-08 18:53:15 +08:00
mesalogo 024e6fb4b2 fix: remove legacy activity history warning 2026-09-08 15:56:45 +08:00
mesalogo f95acdc000 fix: restore packaged runtimes and ship macOS DMG only
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Desktop 0.12.7 omitted OpenCode's offline node_modules because the resource
copier excludes node_modules at its source root. Copy from the parent and
verify every prepared configuration file in the final package. Bundle DSH
session projection with the unpacked Host instead of leaving an ASAR-only
external import, and reject external DeepSeek imports in shipped chunks.

Run packaged OpenCode plugin import and the real DSH UtilityProcess handshake
in Windows x64 CI before removing unpacked output. Add regressions using the
actual electron-builder copier and Vite externalization configuration.

Prepare approved Desktop 0.12.8 notes and change macOS defaults and CI to DMG
only. Update aggregation, website downloads and update readers for 12
installers and 20 assets. Current readers retain historical DMG/ZIP support;
the user accepts that older macOS GitHub readers and all older mirror readers
need manual upgrades. Agent remains 0.11.21, with no new Agent or LoongArch tag.

Validation: 3608 tests passed, 0 failed, 61 skipped across 339 files;
typecheck, lint, release-note validation, website static checks and 18 website
tests passed. No local production build or packaged-app launch was performed
at the user's request; actual packaged startup validation runs in CI.

Release note: 修复安装后 OpenCode 离线依赖缺失和 DeepSeek Harness Host 启动退出;macOS 改为仅提供 DMG,受影响旧版用户需从下载页手动升级。
2026-09-08 14:17:38 +08:00
mesalogo 451b30d434 docs: design application and system tool navigation
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
2026-09-08 12:35:00 +08:00
mesalogo 25e897c22b chore: prepare GoodBuddy 0.12.7 and Agent 0.11.21 releases
Set the approved Desktop and Agent versions and package the approved Chinese
and English notes without changing published release history. Keep OpenCode
pinned to 1.18.29 and require Desktop 0.12.7 before the new Agent package.

Align both feature lists and affected design documents with reference-image
editing, inherited expert work modes, preview boundaries, and remaining
release acceptance. Application navigation remains design-only. No LoongArch
preview is planned for this release; Agent publication remains non-Latest.

The first candidate full run exposed a browser test clicking Send before it
was enabled. Wait for startup readiness using the existing test convention;
production code is unchanged by this release-preparation commit.

Validation: final full suite 3597 passed, 0 failed, 61 skipped across 338 files.
Typecheck, lint, bilingual release-note validation, version consistency,
unchanged historical notes, whitespace and relative-link checks passed.
No additional real model calls, local production builds, pushes or tags.
Concurrent navigation, workbar and roadmap design edits are not included.
2026-09-08 12:29:49 +08:00
mesalogo c8db340cc7 fix: prevent runtime permission stalls and Agent reconnect failures
Execute could stall on workspace-external access or native child-session
permissions, while structured questions could be lost or answered incorrectly.
Route owned OpenCode descendant interactions, correct Continue question and
custom-answer handling, and resolve DSH Execute ACP permissions without a
second approval. Ask remains read-only and unrelated sessions stay isolated.

Restore the required DSH session projection service. Reuse offline OpenCode
configuration by plugin version, remove unused npm shims from remote payloads,
normalize dependency modes, and accept real compound descriptors up to 4 MiB.
Fix intermittent Agent attach rejection and closed-handshake waits.

Add focused and real-Runtime regressions, document interaction boundaries and
validation evidence, and include the existing application/tool navigation
design drafts. Those navigation changes are design-only, not implemented UI.

Validation: full suite 3597 passed, 0 failed, 61 skipped across 338 files;
typecheck, lint, release-note verification, whitespace and relative-link checks
passed. Current-source Linux x64 installation, Ask/Execute, native subagent
external writes, reconnect and stop/bootstrap passed with 10 completed real
model calls, 13 native endpoint tests and 20 consecutive handshakes.
System sleep/wake and release CI packaging remain unverified.

Release note: 修复 Execute 访问工作区外目录或运行原生子代理时卡在权限等待的问题,并恢复 OpenCode 与 Continue 结构化问答的正确转交。修复远程 Agent 安装与重连失败;Ask 仍保持只读,任意终端密码或业务确认不会自动代答。
2026-09-08 11:34:45 +08:00
mesalogo 9aed926cda fix: stabilize OpenCode reuse and session switching
Check cached embedded servers before reuse, isolate concurrent cancellation, and serialize request MCP registration and cleanup. Preserve native controls across session switches and recover usable partial inventories.

Share the bundled offline OpenCode configuration with desktop and Agent launch paths, including model bridge environment forwarding.

Validation: focused tests and typecheck passed; lint passed before the final fixes. The full test run exceeded 120 seconds and reported a DS stdio MCP failure. Current-source Agent host and real suspend/resume validation remain pending.

Release note: 修复切换会话后 OpenCode 原生控件消失及并发取消相互影响的问题,并完善内嵌服务复用检查和桌面、Agent 的离线依赖配置。
2026-09-08 01:09:14 +08:00
mesalogo 86daae8be2 fix: remove terminal emulator padding 2026-09-07 18:08:52 +08:00
mesalogo 19086d656e fix: reuse bundled agent runtime resources 2026-09-07 16:57:59 +08:00
mesalogo d3947c79dd fix: avoid rerendering chat history while typing 2026-09-07 16:46:18 +08:00
mesalogo 6a1a671d3b fix: support reference image editing 2026-09-07 15:48:47 +08:00
mesalogo dbb7c2fc80 fix: align message content below avatars 2026-09-07 14:02:59 +08:00
mesalogo 237b8a8cee chore: prepare GoodBuddy 0.12.6 and Agent 0.11.20 releases
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Record approved bilingual notes for complete subagent progress, workspace diffs, knowledge selection, OpenCode stream cleanup, external command directories, and restored GitHub Agent updates. Remote progress and diff improvements require the new Agent package. No LoongArch preview is included.
2026-09-06 22:39:41 +08:00
mesalogo d08ba33c3e fix: complete subagent output and file workspace interactions
OpenCode child cards now show ordered text, reasoning and tool progress,
with the final result displayed separately. Local SDK and remote ACP paths
share child tracking, including progress retained across recovery and cancellation.
Remote live progress requires an Agent package containing the event plugin;
older Agent packages still expose final results without inventing missing history.

Knowledge-library rows remain clickable when native label activation changes
focus. File workspace changes use colored Git status letters and per-file
staged/unstaged diffs, including deleted, renamed and untracked files.
Refreshing reloads the selected diff and expanded directories, and more than
50 changes remain accessible through incremental loading.

Validated with 3552 passing tests, typecheck, lint, desktop build, current-source
Linux Host scenarios and isolated Windows desktop UI interactions.

Release note: 展开 OpenCode 子代理卡片可查看执行过程、工具详情与独立最终结果;知识库支持整行勾选。工作区支持单文件暂存与未暂存差异,刷新会更新当前差异,更多变更可继续加载。远端实时过程需要更新 GoodBuddy Agent。
2026-09-06 22:13:45 +08:00
mesalogo b03c25685c fix: clean up OpenCode streams and allow external working directories
Completed OpenCode requests left their event streams open across repeated and
parallel conversations. Each request now closes its own subscription on exit
without cancelling peer sessions, and cancellation preserves its original cause.

Direct-model Execute commands no longer restrict cwd to the workspace. Absolute
paths, parent-relative paths, and symlinks to external directories are accepted.
The workspace remains the default and relative-path base; Ask stays read-only,
and local execution binding, directory validation, and process cleanup remain.

Add real OpenCode lifecycle and real Shell tool-loop regressions, plus matching
feature documentation. Focused checks: 182 passed, 1 skipped; typecheck and lint
passed. Full shared-worktree suite: 3542 passed, 57 skipped, 1 unrelated sidebar
heading failure, reproduced separately and documented.

Release note: 修复 OpenCode 连续及并行对话结束后事件连接未释放的问题;直连模型 Execute 命令现在可直接指定工作区外的绝对路径、相对路径或符号链接目录,Ask 只读边界保持不变。
2026-09-06 19:53:42 +08:00
mesalogo 7003141e56 fix: restore GitHub Agent update checks with paginated discovery
The GitHub release history exceeded the catalog response limit, preventing users from checking Agent updates. Read ten releases per page and stop at the first eligible catalog while preserving the existing 100-release search window and signature checks.

Release note: 修复选择 GitHub 来源时 Agent 更新检查提示发布目录超出大小限制的问题;镜像更新和已有本地包保持不变。
2026-09-06 19:25:40 +08:00
mesalogo e692362414 chore: move ShareServer development to a standalone repository
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Remove the extracted ShareServer prototype and its server-specific documents
from Desktop. Link the shared network architecture to goodbuddy-shareserver,
while retaining Desktop ownership of the cross-product architecture.

Preserve local generated artifacts and ignore the old server build output.
Desktop runtime behavior and released user data are unchanged. Desktop tests,
typecheck, and lint passed; the standalone service was validated separately.
2026-09-06 19:02:41 +08:00
mesalogo ee804edcae fix: isolate desktop lint from ShareServer configuration
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Exclude the independent prototype from root lint and explicitly anchor TypeScript parser configuration to the Desktop repository. This fixes clean CI runners detecting both root and ShareServer configuration directories. Product behavior and approved release notes are unchanged.
2026-09-06 17:49:44 +08:00
mesalogo 85a4a374fa fix: keep ShareServer prototype tests outside desktop releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Exclude the unfinished standalone ShareServer prototype from the Desktop root test suite and remove its CI dependency installation. Preserve its independent test configuration and document the release boundary. Approved Desktop and Agent release notes remain unchanged.
2026-09-06 17:37:37 +08:00
mesalogo 89a81dd07c fix: install ShareServer test dependencies in release CI
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The root validation suite includes ShareServer tests, but clean CI runners only installed Desktop dependencies and could not resolve react-router-dom. Install the independently locked ShareServer dependencies before running validators. Release content and approved bilingual notes are unchanged.
2026-09-06 17:01:50 +08:00
mesalogo 0eb664c0b6 chore: prepare GoodBuddy 0.12.5 and Agent 0.11.19 releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Record approved bilingual release notes and align Desktop and Agent version metadata. Publish Desktop first so users can upgrade before the shared Agent catalog gains macOS arm64 entries. No LoongArch preview is included.
2026-09-06 16:44:46 +08:00
mesalogo 9c6d1611e2 feat: support macOS arm64 remote Agent hosts
Extend GoodBuddy Agent packaging, installation, SSH attachment, Runtime lifecycle, and Desktop package management to macOS arm64. Linux x64, Linux arm64, and Darwin arm64 share the existing signed catalog. Preserve Ask read-only behavior and Execute account permissions, and stop detached tool descendants during Mac cancellation.

Upgrade Desktop before publishing the first mixed-platform catalog because older Linux-only readers reject Darwin entries. Intel Macs are not supported; no launchd service or administrator access is required. This commit does not publish a release.

Validation: 3520 tests passed, 57 skipped; typecheck, lint, and development build passed. Real Mac installation, Ask/Execute, cancellation and reconnect passed; final Linux installation, Ask and reconnect passed. Nine real model requests were recorded.

Release note: 支持将 Apple Silicon Mac 用作远程项目 Host,可安装 macOS arm64 Agent 并使用 Ask、Execute 和断线重连;与 Linux 共用一个包目录,发布混合目录前需升级 Desktop。
2026-09-06 16:14:02 +08:00
mesalogo 1f01c26e04 fix: keep replies streaming while switching windows
Switching away from the desktop window and back could mark a reply that was
still being generated as "上次运行意外中断", freeze its output partway through,
and push the next message into the pending queue even though nothing had
actually failed. The conversation area also shifted on each switch because the
OpenCode runtime controls were unmounted and re-detected on every refresh.

Reading the conversation store no longer treats an in-progress reply as
interrupted; only startup recovery marks a run that an application exit truly
cut off. Runtime feature detection is now keyed on the resolved runtime scope,
so the OpenCode toolbar and manual Compact control stay in place across
refreshes. No request timeout or recovery behavior changed.

Release note: 修复来回切换桌面窗口时,正在生成的回复被误判为"上次运行意外中断"、输出中途停住、之后发送的消息只能进入待发送队列的问题。使用 OpenCode Runtime 时,会话区也不再因重复检测专属功能而抖动。
2026-09-05 16:28:45 +08:00
mesalogo dd8289fdef fix: make local knowledge bases ready for daily use
Knowledge selection was shared globally, document readiness overstated usable content, and important import and source-management paths exposed technical details or lacked safe recovery actions. Knowledge scope now persists per conversation, readiness reflects real processing channels, and the workspace guides users from creation and import into cited chat with controlled source actions.

Release note: 知识库现按对话保存使用范围,准确区分可用、处理中和失败文档,并提供更简洁的创建、检索测试、来源打开、删除确认和失败重试流程。
2026-09-05 15:51:07 +08:00
mesalogo d5fa0ec67c fix: keep long conversation content scrollable
Large messages containing reasoning, tool lists, or long replies could use an estimated off-screen height that left later content outside the reachable scroll range. Conversation messages now participate in layout with their actual height while the existing 80-message render window keeps long histories bounded.

Release note: 修复偶发的长会话滚动条提前到底问题;包含较长推理、工具记录或正文时,现在可以直接滚动查看全部已加载内容,无需切换会话刷新。
2026-09-05 14:51:21 +08:00
mesalogo 6eb73657e0 chore: ignore Python bytecode caches 2026-09-05 14:02:53 +08:00
mesalogo 4041edefa8 feat: add ShareServer console prototype
ShareServer previously existed only as design documents. The repository now includes a standalone Hono service and a responsive React management-console prototype covering administrator and member workflows, with local build, test, and container entry points.

The sharing design now includes organization-scoped digital agents and explicit local partitioned-memory selection for @ calls. Memory documentation consistently uses partitioned memory and treats time or internal events only as optional recall triggers.

This remains an interactive prototype: fixture actions do not persist, and production authentication, PostgreSQL isolation, device connections, routing, relay, and federation still require implementation.

Release note: 新增可独立运行的 ShareServer 管理控制台交互原型,并完善组织数字人与分区记忆共享设计;当前原型数据和操作不会写入生产系统。
2026-09-05 14:01:20 +08:00
mesalogo 482cd0c467 chore: prepare GoodBuddy 0.12.4 releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Bump Desktop to 0.12.4 and the separately published GoodBuddy Agent to 0.11.18. Add the approved bilingual release notes for HTML previews, long-run and queue reliability, remote terminal-state convergence, and faster OpenCode follow-up on Linux ARM.

This candidate uses the standard six-platform x64/arm64 matrix and does not request a LoongArch experimental preview.
2026-09-05 10:01:45 +08:00
mesalogo 5a1afb00f7 fix: reuse Runtime after native inventory load
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Native capability loading previously cold-started a temporary Runtime and disposed it immediately, so Linux ARM users paid the OpenCode startup cost again on their first request. Native inventory loading now uses the cached execution Runtime, preserving normal idle eviction and lifecycle cleanup.

Release note: 优化 Linux ARM 上切换 OpenCode 后的响应速度;原生能力清单与后续对话现在复用同一 Runtime,避免重复冷启动。
2026-09-05 09:45:23 +08:00
mesalogo a4b64a1641 feat: preview Agent HTML in conversations and unblock long replies
Agent replies that produced HTML could only be read as source, so users had
to copy the markup into an external file to see the result. Completed Agent
and Subagent output now renders complete HTML documents, single-root HTML
replies, and `html` / `htm` code blocks as an isolated static preview in
place. Each preview keeps a persistent notice that scripts and network are
disabled and offers the same icon actions as Mermaid diagrams: a source
toggle and a full-screen preview. The new "render HTML in conversations"
switch under Platform Features / General is on by default, applies to the
open conversation immediately, and falls back to HTML source when disabled.
Previews cannot run scripts, reach the network, submit forms, open windows,
or load remote media; author stylesheets and inline styles are preserved so
the intended layout survives, and remote resource loading is refused by the
preview CSP instead.

Direct-model replies no longer fail once they grow large. The byte caps on
streaming responses, single stream blocks, non-streaming replies, model tool
arguments, tool request context, aggregate tool results, and accumulated
answer text are removed, so users stop seeing "模型接口流式响应超过安全限制"
on long answers. Response length is now governed by the request timeout,
cancellation, and context compression. Image generation responses keep their
decodable-image validation.

A message sent after a failed or interrupted run stayed in the pending queue
instead of running. The conversation is now released whenever a run reaches
any terminal state, not only when the renderer reports readiness again, and a
dispatched queue item whose run never starts releases its reservation and
returns to pending. Later messages dispatch without extra user action.

Release note: Agent 回复中的 HTML 现在可直接在会话内预览,支持查看源码和全屏预览,默认开启并可在“平台功能 / 通用设置”关闭;直连模型的长回复不再因大小限制中断;上一次运行失败或被中断后,新发送的消息会立即执行而不再排队等待。
2026-09-05 09:10:44 +08:00
mesalogo 9588c4b901 fix: preserve long run activity and large previews
Long Runtime sessions could silently stop retaining tool and Subagent activity, truncate Run history details, and reject workspace previews over 256 KB. Run history now persists in Main SQLite without the former fixed record or detail caps, surfaces possible loss from legacy caches, and renders long histories in batches.

Local and remote workspace previews now load UTF-8-safe 256 KiB pages on demand instead of rejecting larger text files. The legacy migration, clear-data path, IPC contracts, tests, and feature documentation are updated with the resulting behavior.

Release note: 长时间 Runtime 运行现在会完整保留工具和 Subagent 活动;运行记录迁移到 SQLite,并提示旧版可能已丢失的内容。超过 256KB 的本地及远程文本文件可通过分页面预览继续加载。
2026-09-05 00:31:42 +08:00
mesalogo d180f63abd fix: converge remote run state on persisted terminal messages
After a managed SSH disconnect the renderer kept showing the remote
Agent as processing even though the recovered task had already been
persisted as completed. Local streaming copies shadowed the persisted
terminal state in mergePersistedConversations, and recovery completion
never asked the renderer to re-read conversations.

- Prefer the persisted terminal state over a local streaming message
- Settle active runs whose messages already reached a persisted terminal
  state and release the conversation queue once
- Refresh conversations on window focus and visibility changes
- Publish a conversation change before recovery reports completed/failed
2026-09-04 16:54:35 +08:00
mesalogo 670171ce5d merge: preserve GitHub README updates
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
2026-09-04 08:21:17 +08:00
mesalogo 4490e5fd15 fix: stabilize Runtime workflows for 0.12.3
OpenCode status checks could cold-start throwaway servers, unrelated
conversations could block each other, and model, Continue, or remote paths
could replay or duplicate work under failure and backlog conditions. Settings
could also lose edits or overwrite unrelated model fields.

This update makes status checks passive, keeps per-conversation ordering while
allowing concurrency, preserves native long-running work with bounded display
capture and backpressure, narrows retries to known pre-dispatch failures, and
aligns remote Ask permissions without reducing Execute. It also keeps
Embedding updates narrow, improves settings and browser guidance, removes the
unused legacy Main gateway state, and records the validated Desktop 0.12.3 and
Agent 0.11.17 release metadata.

Release note: 改善 OpenCode 切换、多会话长任务、模型重试、远程 Ask/Execute 与设置保存可靠性,并完成 Desktop 0.12.3 / Agent 0.11.17 发布准备。
2026-09-04 08:05:29 +08:00
mesalogo 374968d50b docs: add desktop pet feature design
Adds the full documentation set for the planned desktop pet feature under
docs/features/desktop-pet: README, PRD, user stories, logic design, UI
design, and technical design. The pet is scoped as a lightweight always-on
entry point for quick drafting, drag-and-drop staging, and task status at a
glance; it does not introduce a second Ask/Execute channel, task execution
path, or independent task-state store. Design only, no implementation yet.
2026-09-03 23:22:33 +08:00
mesalogo 4e60a4ca61 feat: customize model provider requests
Model connections can now save bounded JSON request headers and top-level body fields. Direct requests and Continue support both; local OpenCode and DeepSeek Harness use only native header support, while the managed remote OpenCode gateway applies both.

Runtime, protocol, and authentication fields keep precedence. Custom values are ordinary connection settings, not credential storage. This migrates Runtime settings from v20 to v21 and updates the DeepSeek control and managed Runtime ACP capabilities.

Release note: 模型连接详情新增自定义请求头和请求体;直连与 Continue 支持两者,本地 OpenCode 和 DeepSeek Harness 仅应用原生支持的请求头,受管远程 OpenCode 支持两者,界面会明确提示生效范围。
2026-09-03 22:34:01 +08:00
mesalogo 3765a937bd docs: clarify website product highlights
Reframe the feature section around the broader desktop product rather than only Agent Runtimes. Clarify official x64 and arm64 coverage, the separate LoongArch preview, and that compatibility claims are not vendor certification.
2026-09-03 22:31:49 +08:00
mesalogo feaebdf221 feat: share built-in browser across runtimes
The built-in browser was limited to direct-model tools, and its Agent switch also blocked manual Browser workbar actions. It is now a request-scoped GoodBuddy capability assignable to direct models, managed OpenCode, and Continue, while manual browser controls remain available when Agent access is off.

Existing browser enablement migrates to capability settings version 6. DeepSeek Harness remains unsupported.

Release note: 内置浏览器现在可分配给直连模型、OpenCode 和 Continue;关闭 Agent 浏览器能力不会影响用户在浏览器工作栏中的手动操作。
2026-09-03 19:36:06 +08:00
mesalogo 70ec2bf792 fix: restore rounded workbar tabs
The workbar tabs, close controls, and add button now use the shared rounded control styling again. The terminal panel and its toolbar remain square as requested.

Release note: 恢复工作栏页签、关闭按钮和加号入口的圆角外观;终端面板与终端工具栏继续保持直角。
2026-09-03 19:32:00 +08:00
mesalogo 8b9dbf55df fix: allow concurrent and unrestricted Runtime sessions
Local embedded OpenCode serialized otherwise independent conversations, and GoodBuddy could stop long Runtime tasks after fixed tool, activity, model-call, or accumulated display-output limits.

Different conversations can now run concurrently while requests in the same conversation stay ordered. Dynamic MCP tools remain isolated to the current request. OpenCode, Continue, managed ACP, and the Agent model gateway no longer stop native execution at GoodBuddy-defined call counts or prompt-wide output totals; display copies truncate and transport queues apply backpressure instead. Prompt-wide quota fields are removed from the current contracts.

Per-call model settings, provider request timeouts, cancellation, and same-conversation ordering remain unchanged.

Release note: 修复本机 OpenCode 不同会话互相等待的问题,并移除 GoodBuddy 对单次 Runtime 工具、活动和模型调用次数的人为上限;长输出现在只截断展示或施加传输背压,不再取消原生任务。
2026-09-03 17:17:09 +08:00
mesalogo b1e3101857 fix: restore terminal paste and square workbar chrome
Terminal paste previously relied on the Renderer browser Clipboard API, which could be denied by Electron and leave the toolbar action unusable. Text clipboard access now goes through the trusted Main/Preload bridge, and the workbar tabs plus terminal toolbar use straight corners for a continuous docked appearance.

Release note: 修复终端工具栏粘贴可能因剪贴板权限被拒绝而失败的问题,并将工作栏页签与终端工具栏调整为直角样式。
2026-09-03 17:12:27 +08:00
mesalogo 7369c13bcf docs: clarify remote runtime backpressure 2026-09-03 16:34:57 +08:00
mesalogo fba5e1ad30 docs: design device sharing network 2026-09-03 16:34:28 +08:00
mesalogo 468cf0483f docs: design external knowledge providers 2026-09-03 16:29:51 +08:00
mesalogo 6586192222 fix: enforce remote Ask at tool boundary
Managed SSH Ask previously required bubblewrap and applied a process-level filesystem sandbox, while Execute launched directly. Ask and Execute now share the same direct signed Runtime startup; Ask remains read-only through the Agent's existing tool permission mediation, which only permits native read requests and rejects other tool kinds.

Release note: 远程 Ask 与 Execute 现在都可直接启动签名 Runtime,无需 Host 安装 bubblewrap;Ask 仍在工具权限层仅允许读取操作。
2026-09-03 16:23:14 +08:00
mesalogo 987a08ad78 fix: allow remote Execute without bubblewrap
Remote Hosts without the Ask-only bubblewrap tool could report matching Agent and Runtime versions but fail project creation because the Agent hid the Runtime ACP capability entirely. The Agent now advertises a verified Runtime independently of bubblewrap, so Execute can start directly while Ask keeps its existing read-only bubblewrap boundary.

Release note: 修复远程 Host 缺少 bubblewrap 时 Execute 项目无法创建的问题;更新到 Agent 0.11.16 后,Execute 不再受 Ask 专用系统工具影响。
2026-09-03 15:59:05 +08:00
lofyerandGitHub 35a93e61cd Merge pull request #3 from mesalogo/jojocoding-patch-2
Update README.md
2026-09-03 14:25:23 +08:00
lofyerandGitHub 0cba3855ea Merge pull request #2 from mesalogo/jojocoding-patch-1
Update README.zh-CN.md
2026-09-03 14:24:28 +08:00
mesalogo 74c3ec19b4 fix: retry transient direct model failures
Direct model conversations previously stopped after the first transient network error or request timeout, including TLS setup ECONNRESET failures. Text requests now retry up to three times with cancellable backoff before visible output, while partial streamed responses are never replayed.

Release note: 直连模型遇到临时网络故障或请求超时时,会在尚未输出内容的情况下自动重试最多 3 次,并支持随时取消。
2026-09-03 14:24:09 +08:00
jojokeeandGitHub cdc8f6f57a Update README.md
更新英文README
2026-09-03 14:22:33 +08:00
jojokeeandGitHub b8c1d33e7b Update README.zh-CN.md
更新社群入群信息
2026-09-03 14:20:34 +08:00
mesalogo f18e4abb3c fix: simplify topbar Runtime status
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The topbar repeated a textual state after the Runtime or model name even
though it already displayed a compact status indicator. It now uses green,
yellow, and gray dots for ready, connecting, and unavailable states while
keeping detailed status available to assistive technology and tooltips.

Release note: 顶栏 Runtime 状态改用绿、黄、灰三色标记,不再重复显示“就绪”“连接中”或“不可用”文字。
2026-09-03 13:42:41 +08:00
mesalogo c52732441a chore: coordinate Agent 0.11.15 and Desktop 0.12.2
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
The published Agent 0.11.14 predates the registered-Runtime activation change included in the Desktop candidate. Bump the Agent lock to 0.11.15 and the unpublished Desktop recovery to 0.12.2 so the compound package minimum Desktop version, bilingual notes, and feature documentation describe one coordinated release.

Keep v0.12.0 and v0.12.1 immutable and unpublished, and document that Agent tags also require the exact main candidate workflow to pass before tagging.

Release note: 托管 Linux Host 使用 Agent 0.11.15,已登记 Runtime 激活时不再重复检查完整 OpenCode 二进制。
2026-09-03 11:51:30 +08:00
mesalogo ba000300d5 test: stabilize pre-tag main validation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
The Unix private-endpoint relay regression combines socket authentication, SQLite journaling, attach relay framing, and two protocol requests. Give only that end-to-end case a bounded 30-second allowance so a loaded serial CI runner does not fail its previous 5-second default.

Document the mandatory Desktop release order in BUILD.md: push the candidate to both main branches, wait for validators and the production bundle on that exact SHA, and only then create the annotated tag.
2026-09-03 10:56:42 +08:00
mesalogo 3b8d129693 fix: install managed Python on Linux filesystems
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Managed Python installation rejected valid case-distinct terminfo symlinks in the pinned Astral archives on Linux x64 and arm64. Archive uniqueness now follows the target filesystem: Linux preserves case-distinct paths, while every target still rejects exact duplicates and unsafe entries.

Move the unpublished 0.12.0 bilingual release notes to the immutable-tag recovery version 0.12.1 and record the failed tag evidence.

Release note: 修复 Linux x64/ARM64 上托管 Python 因官方归档包含大小写不同路径而无法安装的问题。
2026-09-03 10:31:14 +08:00
mesalogo fb5a9c6f43 test: allow bounded knowledge snapshot writes
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
The complete-snapshot boundary test performs 501 synchronous SQLite source and document writes. It passed functionally but could exceed its 10-second case timeout on the serial Linux CI runner, so give this I/O-heavy case a bounded 30-second allowance without changing product behavior or global test limits.
2026-09-03 06:00:16 +08:00
mesalogo 26b87d7ed5 test: use the installed OpenCode entrypoint
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The Linux routing test still referenced the obsolete bin/opencode name. With intentional path-only Runtime selection, that stale fixture was no longer masked by automatic discovery; use the locked opencode-ai 1.18.9 bin/opencode.exe entrypoint on every platform.

Keep the approved 0.12.0 notes and bilingual feature matrices explicit that local OpenCode paths are not prevalidated and fail only at actual launch.
2026-09-03 05:37:52 +08:00
mesalogo 93721d69b6 test: serialize full-suite validation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The Linux release validator still exhausted process and socket timing with two Vitest workers, causing unrelated OpenCode and private-endpoint tests to fail. Run test files with one worker so process-backed integration tests remain deterministic within the existing CI job budget.
2026-09-02 23:19:48 +08:00
mesalogo 8ecd87381f test: stabilize 0.12.0 candidate validation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Full-suite resource contention could make asynchronous renderer assertions and short runtime timing tests fail even though focused runs passed. Limit Vitest to two workers and give the affected UI operations the existing three-second async window.

Also include the approved Linux ARM OpenCode startup note in both feature matrices and release-note languages, and record the successful isolated Linux x64 Host validation of the current-source Runtime activation path.
2026-09-02 22:57:31 +08:00
mesalogo dc36e7a967 fix: skip repeated OpenCode binary validation
OpenCode startup on Linux ARM could run a --version probe before launching the configured Runtime and recheck registered remote binary metadata. Execution now trusts the selected path and registered manifest identity after setup-time verification, so switching and cold launch avoid duplicate binary work.

Release note: 优化 Linux ARM 上切换 OpenCode 的启动速度;已安装或已登记的 Runtime 在执行时不再重复启动或检查二进制。
2026-09-02 22:41:50 +08:00
mesalogo cdecb584d5 test: wait for settings before appearance assertions
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The Linux CI run could query the Appearance tab before the lazy Settings route finished loading. Appearance tests now wait for the Settings heading before interacting with its tabs, matching the stable navigation pattern used elsewhere.
2026-09-02 21:47:18 +08:00
mesalogo c4231c8f9c chore: prepare GoodBuddy 0.12.0 release
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Update the desktop version, approved bilingual release notes, and synchronized English and Chinese feature matrices for the 0.12.0 candidate.

Release preparation now runs local tests, type checks, and lint without a local production build, then requires the main-branch CI build before tagging. The package matrix uses a native Windows arm64 runner and validates managed Python on every target without adding extra license files to the desktop package.
2026-09-02 21:36:27 +08:00
mesalogo 8a14cf356a fix: hide unverified capability placeholders
The tool environment page always displayed an unverified capability-dependency state even though it had no Skill or MCP evidence behind it. The page now shows only actual interpreter and companion-tool diagnostics; capability dependency status remains reserved for capability details with real validation results.

Release note: 工具执行环境页不再显示没有实际验证依据的能力依赖占位状态。
2026-09-02 17:00:42 +08:00
mesalogo 19a3bafb63 docs: prevent dead-end selection flows
Document that options requiring an in-place prerequisite must remain operable, defer persistence until validation succeeds, avoid implicit first-candidate selection, preserve the previous value on cancellation, and show errors beside the action.
2026-09-02 16:47:24 +08:00
mesalogo 5fac1d3c41 fix: align DeepSeek Harness model selection
DeepSeek Harness previously required a separate Runtime model selection when no administrator-provided connection was available, even if the user had already selected a compatible GoodBuddy model. It now prefers the administrator connection, then follows the compatible default model or first compatible connection across conversations, projects, and messaging channels.

Release note: 修复 DeepSeek Harness 未自动使用已选兼容模型的问题;没有管理员预置连接时,现在会跟随当前默认或首个兼容模型,无需再到 Harness 页面重复指定。
2026-09-02 16:45:42 +08:00
mesalogo 42cbfa43f1 feat: add copy action to AI responses
Completed AI responses now expose a bottom copy action that copies their Markdown source without reasoning, tool logs, or citation metadata. Clipboard writes use a validated Preload/Main bridge so sandboxed Electron sessions no longer fail with a renderer clipboard permission error; full-conversation copy uses the same path.

Release note: AI 回复底部现在可以直接复制 Markdown 原文,并修复了沙箱环境下回复和完整会话可能无法写入剪贴板的问题。
2026-09-02 16:35:51 +08:00
mesalogo 4d9ab72778 fix: keep custom tool environments selectable
The custom Node.js and Python choices could appear unavailable until a candidate was already discovered, and selecting the source implicitly adopted the first candidate. The chooser now stays operable, waits for an explicit validated path, keeps cancellation quiet, and shows validation errors beside the affected runtime.

Candidate discovery now prioritizes active virtual environments and Conda before bounded user/system PATH and common Node/Python locations, while preserving canonical validation and deduplication.

Release note: 修复自定义 Node.js/Python 环境入口不可用的问题,并增加对 PATH、venv、Conda 和常见安装位置的解释器检测。
2026-09-02 16:29:55 +08:00
mesalogo be39665ca9 feat: add managed local tool environments
GoodBuddy can now provide managed Node.js and on-demand Python to local Skills and stdio MCP servers, or validate an explicitly selected custom interpreter. The Capabilities and tools settings page exposes source selection, diagnostics, installation progress, cancellation, and removal while new processes receive immutable PATH snapshots.

Application settings migrate to v10. Local terminal and remote Host environments remain unchanged. Windows x64 native-source Node and Python paths are validated; OSS mirror publication, redistributed license resources, and the remaining native platform probes are recorded as release blockers.

Release note: 新增工具执行环境,可为本机 Skills 和 stdio MCP 使用 GoodBuddy 托管的 Node.js、按需安装的 Python,或经过验证的自定义解释器。
2026-09-02 10:36:56 +08:00
mesalogo 1530fa116c fix: make product copy direct and factual
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
The website, README, and Smart Heartbeat interface used promotional superlatives, generic benefit labels, and personified growth language that obscured the actual product behavior. The English README is now the repository entry point with a separate Chinese version, and both website and in-app copy describe concrete capabilities, reports, suggestions, and execution boundaries.

Release note: 优化官网、README 与智能心跳界面的中英文文案,去除空泛宣传和拟人化表达,改为直接说明产品能力、运行记录与建议内容。
2026-09-02 01:14:16 +08:00
mesalogo b36d688900 feat: add direct model programming agents
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Direct model conversations can now execute bounded platform-native processes and delegate one level of programming work without duplicating tools in Agent Runtimes. Process and Subagent activity follows existing workspace, mode, approval, cancellation, and lifecycle boundaries, with chat attribution and complete feature documentation.

Remove the static Renderer bundle baseline gate and its build-only manifests; production builds continue to compile all processes and verify the control-plane installer. Also include the pending documentation link normalization.

Release note: 直连模型现在可在 Execute 模式运行本机命令,并可委派编程 Subagent 完成修改、测试和复核;构建流程不再因静态 Renderer 包体基线阻塞。
2026-09-02 00:50:54 +08:00
mesalogo d36889d9f6 fix: quiet resolved dependency reference logs
electron-builder listed shared, successfully resolved production dependencies as duplicate references during every package build. All desktop packaging entry points now lower only that non-actionable summary to debug while unresolved reference failures remain visible.

Move Renderer-bundled AntV G6 to development dependencies so its transitive graph is not copied into desktop packages.

Release note: 桌面打包不再显示无需处理的重复依赖引用摘要,也不再把已由 Renderer 打包的 AntV G6 依赖树重复复制到安装包中。
2026-09-02 00:45:15 +08:00
mesalogo 06c1123caa fix: focus built-in skill defaults
The Skills catalog exposed nine product-marketing recipes as independent
capabilities and enabled every marketing package for every Runtime by default.
It now keeps five top-level Skills, loads the nine recipes on demand through
the product-marketing package, and leaves the three marketing Skills disabled
until users enable them. Existing saved Skill settings remain authoritative.

Release note: 内置 Skills 已从 14 个顶层入口精简为 5 个;去 AI 味和长文工具默认启用,产品营销类默认停用,已保存的用户设置保持不变。
2026-09-02 00:44:44 +08:00
mesalogo 4f3e0bdcfd fix: unify capability settings navigation
Skills and MCP previously occupied separate top-level settings categories. They now share one Capabilities and tools category with accessible horizontal tabs, while the unimplemented tool environment remains hidden until its settings are usable.

Prevent nested PageTabs wrappers from collapsing to zero height in scrollable Flex layouts, add a CSS regression guard, and document the shared rule plus the planned local Node.js and Python environment.

Release note: 设置中心将 Skills 和 MCP 合并到“能力与工具”,并修复水平页签可能被压缩而不可见的问题;尚未实现的工具执行环境暂不显示。
2026-09-02 00:28:47 +08:00
mesalogo 2fca39ddb2 feat: expand deai writing checks
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The bundled deai-writing skill now detects additional template patterns,
unsupported attribution, role-slot prose, engineering metaphors, and absolute
claims. Its JSON report also summarizes findings by level and category while
preserving GoodBuddy's cross-platform Python guidance.

Release note: 中文“去 AI 味”审校新增分析套话、模糊归因、角色槽位、工程黑话和绝对化承诺等检查,并提供按分类汇总的扫描结果。
2026-09-01 20:48:58 +08:00
mesalogo 2f1881a414 docs: organize product design by feature
Define repository and feature-scoped documentation rules for PRDs, User Stories, logic, UI, technical design, and progress evidence.

Move feature-owned documents under docs/features, add one entry point per feature, remove the redundant docs index, and update all affected cross-references. Expand the local tool environment design with explicit product, story, logic, UI, technical, and progress documents.
2026-09-01 14:52:22 +08:00
mesalogo dba3380a06 fix: accept bare browser addresses
The Browser workbar required users to enter a complete HTTP(S) URL, so submitting a bare domain or host with a port failed URL validation. Enter and Go now prepend http:// before using the existing browser navigation path.

Explicit protocols remain unchanged so the Main browser URL policy continues to reject unsupported schemes with the correct error.

Release note: 浏览器地址栏现在支持直接输入域名或主机端口,回车或点击“前往”时会自动补全 http://。
2026-09-01 08:54:18 +08:00
mesalogo 77ce13aba3 fix: align the primary sidebar divider
The primary sidebar resize hit target still reserved four pixels in the flex row, leaving a visible gap between the divider and the content frame. Its opposing negative margins now keep the nine-pixel pointer target layout-neutral while preserving the aligned one-pixel divider.

Release note: 修复主侧栏拖拽分隔线与内容边缘未对齐的问题。
2026-09-01 08:37:05 +08:00
mesalogo 5127f7d045 feat: add browser workbar navigation controls
The Browser workbar previously showed the Agent-controlled page but did not let users navigate it directly. It now provides Back, Refresh or Stop Loading, an address field, and Go alongside the existing interaction and close actions.

User and Agent operations share the same per-conversation browser session and serialization path. Loading, committed URL, and history state keep the toolbar accurate, while Stop Loading interrupts page navigation without closing the session or releasing Runtime context.

Direct navigation continues to use the existing built-in-browser capability and URL policy. Closing the browser remains a distinct lifecycle action.

Release note: 浏览器工作栏新增返回、刷新/停止加载、地址栏和前往操作;用户与 Agent 共用同一浏览器会话,停止加载不会关闭会话。
2026-09-01 08:26:57 +08:00
mesalogo 27055693bf feat: add multi-terminal workbar and resizable sidebars
The assistant workbar now uses persistent application tabs with a permanent catalog entry point and supports multiple local or managed SSH terminals bound to the current project. Terminal sessions flow through the Main, preload, and renderer boundaries with bounded output, resize, lifecycle, and cleanup behavior.

The main sidebar and Magic Notes list now support pointer and keyboard resizing. Magic Notes can also hide and restore its list, remembers the chosen layout, and stacks its panes when the available width is narrow.

Closing a terminal tab ends its Shell session. Restored terminal tabs do not restart prior Shell processes and require an explicit reconnect.

Release note: 助手工作栏现可通过“+”打开多个本机或当前远程项目终端;主侧栏和魔法笔记列表支持拖拽调宽,魔法笔记列表可隐藏并记住布局。
2026-09-01 00:49:49 +08:00
mesalogo 73cc55bf75 fix: clarify local tools on remote hosts
The remote host settings page did not explain that locally configured Skills and MCP servers stay on the desktop. It now states that boundary in Chinese and English, with a focused UI regression test.

Document the planned local Node.js and Python environment for local Skills and stdio MCP servers, including managed and custom sources, compatibility migration, lifecycle, remote boundaries, and acceptance criteria.

Release note: 远程主机设置现在明确说明本机 Skills 和 MCP 不会作用于远程主机。
2026-08-31 23:28:41 +08:00
mesalogo 00a231da07 chore: prepare GoodBuddy 0.11.14 releases
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Set the Desktop package metadata and approved bilingual notes to 0.11.14,
aligned with the coordinated Agent 0.11.14 lock and release-status docs. This
candidate uses the standard x64/arm64 release matrices and does not include an
experimental LoongArch preview.

Release note: 准备 GoodBuddy Desktop 与 Agent 0.11.14 正式发布,包含托管 SSH 任务恢复、有界诊断和桌面反馈改进。
2026-08-31 16:02:53 +08:00
mesalogo 2833a27f83 feat: add bounded diagnostics and improve UX feedback
Ask and Execute guidance, Runtime state, live announcements, loading states,
retry paths, settings focus, and update failures now provide consistent and
accessible feedback across the desktop experience.

Add bounded redacted Desktop and Agent diagnostic logs, a read-only Agent
diagnostics command, and an explicit opt-in for attaching a small Desktop
diagnostic summary to in-app feedback. Diagnostics stay local by default and
the existing feedback v1 service contract remains compatible.

Release note: 新增可选桌面诊断反馈与 Agent 诊断导出,并统一 Ask/Execute 状态、错误恢复和无障碍反馈;诊断默认保留在本地,仅在用户明确勾选时附加桌面摘要。
2026-08-31 15:35:13 +08:00
mesalogo b1729125e1 fix: allow slower OpenCode startup on Linux ARM
OpenCode health polling restored a ten-second startup limit, causing Linux ARM systems and QEMU environments that became healthy just after that boundary to be reported unavailable. Managed local OpenCode now retains health-based readiness detection while allowing up to thirty seconds for startup.

Release note: 修复部分 Linux ARM 设备切换 OpenCode 时因启动超过 10 秒而误报超时的问题;健康检查现在最多等待 30 秒。
2026-08-31 13:25:49 +08:00
mesalogo 561de29f00 feat: keep managed SSH prompts running on Hosts
Managed SSH prompts previously depended on Desktop-owned provider and transcript state, so accepted work could stop or become uncertain when the Desktop, network, or power disappeared. The Agent now owns accepted OpenCode prompt execution, model and tool rounds, cancellation, and the durable semantic transcript, while Desktop remains authoritative for permanent projects, conversations, tasks, and messages.

Recovered work attaches to the original conversation without replay. The project switcher also groups managed SSH projects by saved Host and shows live Agent connectivity beside each group without opening a connection merely to read status. Remote project subtitles now show only the path, and the Agent lock advances to 0.11.14 with Runtime ACP v4.

Release note: 托管 SSH 任务在桌面端、网络或电源中断后可继续在主机上运行,并能回到原会话精确恢复;项目菜单现按主机分组并显示实时连接状态。
2026-08-31 11:04:41 +08:00
mesalogo f6f8d5e053 fix: release runtime status probes after checks
Chat startup and Runtime switching previously cached status-only Runtime instances, leaving managed OpenCode servers running without active conversations. Status checks now resolve the selected or configured default Runtime through a disposable probe and release it immediately.

Project execution Runtimes remain cached by model selection and execution space, preserving parallel work across different projects.

Release note: 修复启动或切换 Runtime 后空闲 OpenCode 服务持续驻留的问题;状态检查完成后会立即回收,多个项目仍可并行执行。
2026-08-31 01:35:04 +08:00
mesalogo 8c580a9174 fix: preserve remote environment checks across settings pages
The Hosts settings page discarded the last successful Agent and Runtime version check whenever users switched categories or restarted GoodBuddy, forcing them to refresh the same Host again. Validated results now persist locally and remain visible until users refresh, edit or delete the Host, or start a remote environment update.

Release note: 远程主机的 Agent 和 Runtime 版本检查结果现在会在设置页面切换和应用重启后保留,无需重复点击刷新版本。
2026-08-31 01:07:52 +08:00
mesalogo efd304fa1d feat: support native subagents and long-running prompts
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
OpenCode Task calls now appear as first-class subagents in local and managed SSH conversations, with stable status, persistence, terminal handling, and true event ordering instead of duplicate generic tool entries.

GoodBuddy no longer applies a fixed outer wall-clock total to OpenCode, Continue, DeepSeek Harness, or managed SSH Prompts. Runtime-owned limits, user cancellation, bounded control operations, output limits, and released Agent protocol compatibility remain intact. Desktop and Agent release metadata are synchronized at 0.11.13 with approved bilingual notes.

Release note: OpenCode 原生 Task 现在以独立子 Agent 展示;长任务不再被 GoodBuddy 固定总时限中止,并修复 OpenCode 启动识别、可配置 HTTP 服务地址与发布说明读取问题。
2026-08-30 17:18:33 +08:00
mesalogo 7c6f15d3c0 fix: allow HTTP for configurable service endpoints
DeepSeek Harness and Weixin binding configuration rejected non-loopback HTTP endpoints even when users explicitly selected those internal services. Configurable endpoints now accept valid HTTP and HTTPS URLs without host-location restrictions, while retaining the required protocol and API-key checks.

Release note: 模型连接、DeepSeek Harness 与微信绑定现在可直接使用局域网或公网 HTTP/HTTPS 服务地址,不再强制公网 HTTPS 或本机回环地址。
2026-08-30 16:49:47 +08:00
mesalogo 6dc219f3a3 fix: detect OpenCode readiness by health
GoodBuddy previously treated one exact OpenCode stdout line as the startup signal, so harmless log formatting differences could report a timeout even when the server was already listening. It now polls the authenticated health endpoint on the loopback port that GoodBuddy selected, with bounded attempts and the original ten-second startup limit.

Release note: 修复 OpenCode 已启动但因监听日志格式未被识别而误报超时的问题;启动检测现在直接使用本机健康接口。
2026-08-30 15:30:43 +08:00
mesalogo acc5a8e2d8 fix: allow slower OpenCode server startup
The bundled OpenCode server could be reported unavailable when slower Linux ARM systems needed more than ten seconds to initialize. GoodBuddy now allows thirty seconds for startup before terminating the server and reports the updated timeout.

Release note: 修复部分 Linux ARM 设备上 OpenCode 冷启动较慢时被误报为启动超时的问题。
2026-08-30 14:42:18 +08:00
mesalogo 9f0f231e3b chore: prepare GoodBuddy 0.11.12 release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
2026-08-30 11:08:55 +08:00
mesalogo 967796539f fix: recover remote conversations after uncertain prompts
An interrupted managed SSH prompt could leave its ACP binding as the permanent owner of the conversation, so every later user message failed with the same uncertain-recovery error. A later explicit prompt now retires that binding and starts a fresh ACP session without replaying the uncertain operation.

Ask-mode Runtime startup also waited on the wrong bubblewrap executable identity. The Agent now supervises the bounded transition to the signed Runtime or model-bridge executable, and the Agent version advances to 0.11.10.

Release note: 修复远程 ACP 请求中断后同一会话无法继续输入,以及 Ask 模式可能在模型调用前启动失败的问题。
2026-08-30 11:02:20 +08:00
mesalogo 8e08ab0bac feat: show Magic Notes update times
The Magic Notes list previously showed only each title, content preview, and
entry count, making similarly named or recently changed notes harder to scan.
Each note now shows its localized update time beside the entry count, with a
semantic timestamp and bounded single-line layout for the narrow list pane.

Release note: 魔法笔记左侧列表现在显示每篇笔记的本地化更新时间,便于快速找到最近修改的内容。
2026-08-30 10:09:39 +08:00
mesalogo edba31e873 chore: prepare GoodBuddy 0.11.11 release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Bump the desktop package metadata to 0.11.11 and package the approved Simplified Chinese and English release notes for complete remote conversation saves and discoverable Magic Notes list shortcuts.

Release note: GoodBuddy 0.11.11 修复远程项目长工具结果的会话持久化,并补充魔法笔记列表快捷输入提示。
2026-08-30 09:33:58 +08:00
mesalogo 0804f5ccd2 fix: preserve complete remote tool details
Managed SSH Runtime events could complete successfully and then fail local conversation persistence when tool details exceeded a second, smaller renderer-to-Main schema limit. Conversation persistence now stores the complete tool details already accepted from the Runtime, so long remote results no longer leave the final assistant message stuck in a streaming state.

Release note: 修复远程项目工具结果较长时会话持久化失败的问题;Runtime 已返回的工具详情现在会完整保存到本地会话。
2026-08-30 09:29:28 +08:00
mesalogo 29639ff516 fix: surface Magic Notes list shortcuts
Magic Notes already accepted Quill's native bullet, numbered, and checklist
autofill, but the editor did not make those shortcuts discoverable or protect
them with behavior coverage. The composer now explains the supported markers
in both languages, and tests verify they continue producing Delta list formats.

Release note: 魔法笔记编辑区现在提示项目符号、编号和待办清单快捷输入;输入标记后按空格即可创建对应列表。
2026-08-30 09:20:27 +08:00
mesalogo 22c80b2d94 fix: align Linux Agent verifier regression
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The Linux source validator still expected the full verifier's old payload mode
message after entrypoint metadata checks moved ahead of the payload loop. The
test now asserts the actual bounded entrypoint error returned by the optimized
verifier.

The unpublished desktop 0.11.9 notes move to 0.11.10. The Agent lock remains
on the successfully published Agent 0.11.9, and the recovery release still has
no LoongArch preview.

Release note: GoodBuddy 0.11.10 延续已批准的 Host 更新修复;未发布的桌面 0.11.9 由新版本替代。
2026-08-29 23:01:46 +08:00
mesalogo 85e14ca626 fix: make Host updates simple and fast
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Host repair could fail while adopting the published OpenCode Runtime, then
repeat package and payload verification and carry durable metadata snapshots
for recovery. Host preparation now verifies each acquired compound archive
once, adopts signed Agent and Runtime metadata without rereading payload trees,
and retries failed work by cleaning staging and preparing fresh.

The desktop and Agent versions are advanced to 0.11.9 with the approved
bilingual release notes. Remote Projects remain a disabled-by-default technical
preview, Agent packages remain separate Linux x64/arm64 releases, and this
release has no LoongArch preview.

Release note: 修复 Host 更新采用失败,移除重复远端包体校验,并将中断恢复简化为清理暂存后重新准备。
2026-08-29 22:31:39 +08:00
mesalogo a99b90cc02 fix: make Host environment updates fast and recoverable
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
Published Host updates could fail while the desktop reverified Runtime against
its bundled lock and repeatedly traversed remote payloads. Host preparation now
verifies and extracts once, adopts Agent and Runtime on one pinned connection,
and uses registered metadata for normal health, capability, and prompt paths.
Interrupted commits and adoption retain bounded recovery and rollback state.

Desktop and Agent versions are 0.11.8. Remote Projects remain a technical
preview; the Agent targets Linux x64 and arm64, with no LoongArch preview.

Release note: 修复正式环境 Host 更新失败并移除重复远端包体扫描;中断恢复与清理失败不再破坏已健康环境。
2026-08-29 17:20:25 +08:00
mesalogo ff4fde282f chore: prepare GoodBuddy 0.11.7 release notes
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Add the approved matching Chinese and English notes for the desktop 0.11.7
release and its separately published Agent 0.11.7 requirement. Remote Projects
remain a technical preview, and no LoongArch preview is included.
2026-08-29 14:03:52 +08:00
mesalogo f327341b00 fix: prevent duplicate queue replay and use cached Agents
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
An interrupted dispatch could leave the same user input both in conversation
history and in the pending queue, causing it to be sent again after restart.
Persisted user messages now retain their queue identity, startup recovery drops
only matching materialized inputs, and conversation branches discard that
ownership marker.

Explicit GoodBuddy transfer now leases a verified local Agent package when the
catalog is unavailable or the local version is at least as new, while a known
newer online package still wins. Prepare both the desktop and Agent locks for
0.11.7; the experimental LoongArch preview remains excluded.

Release note: 修复对话中断后同一条排队输入可能重复发送的问题;GoodBuddy 传输现在可复用已验证的本地 Agent 包,同时不会阻止已知的线上新版。
2026-08-29 13:31:56 +08:00
mesalogo 459cc21e3d fix: make managed SSH sessions fast and resilient
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Managed SSH project selection repeatedly activated the remote Host and could
leave idle ACP bindings rejected as stale-controller after a controller
generation change. Host environment updates also coupled project state to
component installation evidence and presented a crowded control surface.

Project selection is now local-only, one Host connection serves compatible
project and conversation callers, idle bindings are safely replaced only
after proven controller takeover, and active prompts keep exact resume/replay.
Remote environment setup now uses explicit signed compound package
preparation with bounded transfer, rollback, recovery, and clearer settings.

Managed Hosts must be updated to separately distributed Agent 0.11.6 before
the daemon-side controller recovery fix takes effect. Desktop packages do not
embed the Agent or remote Runtime, and existing project data is preserved.

Release note: 托管 SSH 项目切换不再重复连接,同一 Host 可顺畅承载多个项目和对话;同时修复重连后的 stale-controller,并优化远程环境安装、更新与状态布局。
2026-08-29 10:58:19 +08:00
lofyerandGitHub eb947213c8 Merge pull request #1 from mesalogo/jojocoding-patch-1
Update README.md
2026-08-27 21:43:15 +08:00
jojokeeandGitHub c2095037ee Update README.md
更新社群二维码
2026-08-27 21:33:33 +08:00
mesalogo 93bf3b908a fix: keep Chinese downloads on OSS loading state
Cross-platform packages / Validate source (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The Chinese download cards briefly exposed the GitHub fallback before OSS index validation completed. They now stay disabled while loading and only show GitHub if the OSS request or validation fails; English downloads remain unchanged.

Release note: 中文官网下载入口加载 OSS 索引期间不再短暂跳转 GitHub,仅在索引失败时回退。
2026-08-27 21:30:38 +08:00
mesalogo 38f6a23306 chore: prepare GoodBuddy 0.11.6 release notes
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Add the approved Simplified Chinese and English release notes for sidebar brand customization, independent Anthropic output limits, and restored composer and brand guidance.
2026-08-27 12:56:51 +08:00
mesalogo 12463ad593 feat: add sidebar brand customization
The primary sidebar brand was fixed to the built-in name, subtitle, and logo. Appearance settings now let users preview and save a custom brand name, localized Chinese and English subtitles, and a validated local PNG, JPEG, or WebP logo, with default-logo and full-reset actions.

Customization remains device-local and limited to the top-left brand area; installer, update, Agent, and protocol identities remain GoodBuddy. The desktop package version is now 0.11.6.

Release note: 新增“外观 → 品牌定制”,可修改左上角品牌名称、中英文副标题和本地 Logo,并支持实时预览、恢复默认及本机持久化。
2026-08-27 12:42:26 +08:00
mesalogo 618e505b8b fix: restore the workspace brand subtitle
The primary sidebar showed only the GoodBuddy name after its supporting brand line was removed. It now displays the localized “智能工作台” / “Intelligent Workspace” subtitle beneath the product name again.

Release note: 恢复左上角 GoodBuddy 标题下方的“智能工作台”副标题,使品牌区域的信息层级更加完整。
2026-08-27 10:30:15 +08:00
mesalogo 0a3bf6094a fix: restore composer guidance in input hint
The composer placeholder had been reduced to only the message prompt, hiding send, newline, and paste guidance unless users hovered the field. The full localized keyboard guidance now appears directly in the empty text area again for both chat and image generation.

Release note: 恢复聊天输入框中的发送、换行和粘贴快捷键提示,空输入框会再次直接显示完整操作说明。
2026-08-27 10:14:39 +08:00
mesalogo 957e2ecde7 fix: align homepage feature checklist
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Wrapped feature rows were centered independently, which shifted the lower-right item away from the column above it. The Chinese homepage now uses a shared two-column grid and collapses to one aligned column on narrow screens.

Release note: 官网首页特性列表现在按列对齐,桌面与移动端布局更整齐。
2026-08-27 08:04:36 +08:00
mesalogo 9316fed3e3 fix: streamline homepage introduction
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The homepage hero paragraph and page description now focus on GoodBuddy's desktop assistant and Agent Runtime capabilities. Download options and architecture support remain unchanged.

Release note: 官网首页产品介绍现更聚焦桌面助手与 Agent Runtime 能力。
2026-08-27 07:54:04 +08:00
mesalogo d6fea0c6c2 fix: simplify LoongArch download copy
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
The Linux download card now presents only the preview version, DEB format, size, and checksum. Preview index validation and fail-closed download behavior are unchanged.

Release note: 官网龙芯下载信息现仅展示版本、包格式、大小与校验值。
2026-08-27 07:45:03 +08:00
mesalogo d9a554b4b1 feat: add LoongArch preview download to the website
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
The Chinese Linux download card now includes a LoongArch processor option and fixes its package type to DEB. It reads a separate strictly validated OSS preview index, while the formal six-target release index and English GitHub downloads remain unchanged.

The page keeps three symmetric platform cards, disables only the LoongArch selection when its index is unavailable, and labels the package as unverified on real LoongArch hardware.

Release note: 官网 Linux 下载新增龙芯 LoongArch 处理器选项,选择后提供独立 DEB 预览包;正式下载与更新索引保持不变。
2026-08-26 23:23:27 +08:00
mesalogo b20c10f820 fix: separate Anthropic output and context limits
Direct Anthropic connections reused the configured context window as max_tokens, which could impose an unintended output value or send a provider-invalid request. Anthropic connections now expose an independent per-response maximum output setting, defaulting to 32K for the protocol-required field, while OpenAI direct requests remain without an added output token cap and direct tool rounds remain unmetered.

Release note: Anthropic 直连模型现在可单独配置单次最大输出,不再复用上下文窗口;OpenAI 直连请求和工具调用轮次不增加额外配额。
2026-08-26 22:53:49 +08:00
mesalogo 49700bc7cf fix: make LoongArch preview checksum portable
Generate SHA256SUMS from inside the timestamped artifact directory so it records only the DEB filename and remains verifiable after copying the preview output elsewhere.
2026-08-26 17:12:20 +08:00
mesalogo fcdcc8e081 fix: finalize 0.11.5 compatibility paths
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Legacy key-only environment deployments retain their historical model endpoint, name, and protocol while generic model variables continue to take precedence. Agent inventory scanning now overlaps the signed catalog check instead of waiting for the network first.

The separate LoongArch preview builder derives node-pty from the candidate lockfile and verifies the installed version before cross-compiling it, preventing stale preview-only pins.
2026-08-26 17:01:49 +08:00
mesalogo de64e9768a fix: make Agent updates reliable and visible
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Valid Agent packages could be rejected when packaged trusted-key JSON used CRLF, and Settings could not show whether a newer compatible Agent existed. Registry formatting is now parsed independently of cryptographic trust, and Settings reads only the signed catalog to report local and latest versions.

New installations use a credential-free local Ollama-compatible profile. The v20 migration changes only untouched historical defaults, while explicit profiles and legacy environment aliases remain compatible. Magic Notes count badges no longer shift navigation rows.

Release note: 修复 Agent 下载兼容性并在设置中显示更新状态;新安装默认连接本地模型,保留显式用户配置;魔法笔记待办数字不再改变侧栏菜单高度。
2026-08-26 15:43:41 +08:00
mesalogo f9137f0463 fix: restore cross-platform release packaging
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
The unpublished 0.11.3 candidate could not validate the DeepSeek Harness node-pty prebuilds on every native target, and catalog signing could misread large compound Agent payloads. Release packaging now verifies the rc.8 native layout and writes deterministic stored .gbagent archives with bounded central-directory, mode, CRC, SHA-256, and signature checks.

The 0.11.4 recovery also pins reviewed dependency lifecycle scripts, packages the omitted ONNX Runtime license, and carries the approved unpublished notes forward without changing the immutable failed tags or adding a LoongArch package.

Release note: 修复跨平台桌面包和大文件 Agent 包的发行校验失败,并将未发布的 0.11.3 内容顺延至 0.11.4。
2026-08-26 01:15:34 +08:00
mesalogo 798ac814a9 fix: update bundled DeepSeek Harness to rc.8
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
The desktop runtime still pinned the rc.6 Harness family. It now uses the coherent rc.8 package set, adapts the revised image-attachment limits and error contract, and derives release checks from the lockfile. The managed SSH Agent package remains unchanged.

Release note: 内置 DeepSeek Harness 已升级至 0.1.0-rc.8,并适配新版图片附件限制;托管 SSH Agent 包不受影响。
2026-08-25 23:26:43 +08:00
mesalogo 6913e9ce16 fix: stabilize extension startup deadline validation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The total-deadline test relied on a timer firing at an exact millisecond boundary, so a shared runner could legally begin the second extension before observing expiration. Use the existing finite synchronous-work fixture to exceed the total budget deterministically while preserving the same production behavior assertion.
2026-08-25 22:30:52 +08:00
mesalogo 9aca3b2627 docs: trim GoodBuddy 0.11.3 release notes
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Remove the Agent package verification and installation bullet from both language versions while keeping the remaining approved 0.11.3 notes aligned.
2026-08-25 22:19:53 +08:00
mesalogo 0cb7007429 fix: unify GoodBuddy release signing identity
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Use one generic GoodBuddy Ed25519 identity for the Agent bundle, bundled Runtime, outer package, and signed catalog while preserving each layer's existing signature domain.

Rename the production Environment contract to GOODBUDDY_SIGNING_*, register the matching production public key, and keep desktop and branch CI workflows isolated from the private key.
2026-08-25 22:02:47 +08:00
mesalogo bcd347e092 fix: align Unix endpoint tests with platform behavior
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Node and libuv remove a bound Unix socket path when the server closes. The removed test required protecting a same-user replacement inside the Agent-owned 0700 directory, which would need disproportionate custom socket publication machinery without crossing a real trust boundary.

Keep the practical live-endpoint and shutdown lifecycle coverage instead.
2026-08-25 21:31:47 +08:00
mesalogo 9c50de521e fix: stabilize Linux release validation
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Ubuntu validation exposed a missing Unix socket test import, a platform-dependent cancellation-message assertion, and a maximum-frame test that could exceed the default timeout under shared-runner load. The tests now exercise the intended behavior consistently without changing product code.
2026-08-25 21:21:58 +08:00
mesalogo da00a850ec fix: report verified Agent CI archives
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The native Linux CI builder completed both deterministic package assemblies but read an obsolete nested return field while producing its result JSON. It now reports the archive name from the actual assembleAgentPackage result and guards that shape in workflow tests.
2026-08-25 21:13:46 +08:00
mesalogo fcedaf578e fix: recover managed SSH Agent package release
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Linux .gbagent extraction inherited permissive directory modes, so compound packages failed secure Runtime verification on POSIX runners. Package roots and nested bundle directories now use private modes.

The production workflow now uses one GoodBuddy Agent signing identity for the Agent, bundled Runtime, outer package, and catalog. The unpublished 0.11.2 candidate is carried forward as desktop and Agent 0.11.3 without a LoongArch preview.

Release note: 修复 Linux 上 Agent 包解压后无法通过权限验证的问题,并将复合包各层统一为一组 GoodBuddy Agent 发布密钥。
2026-08-25 20:53:28 +08:00
mesalogo 53269f8b1a chore: release GoodBuddy 0.11.2
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build Linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build Linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build Linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Synchronize the desktop package and lockfile versions and package the approved
Simplified Chinese and English release notes for GoodBuddy 0.11.2.

The managed SSH package lock uses the formal Agent 0.11.2 release. Its
independent Agent tag and publication remain separate from the desktop tag,
and this release does not build the experimental LoongArch preview.
2026-08-25 19:30:43 +08:00
mesalogo 9388eb79d4 fix: restore vector model download feedback
Clicking the built-in embedding model download could appear to do nothing while
Main waited for the full transfer to finish. The vector model UI now follows
the speech model workflow with an immediate preparing state, verified source
and package metadata, live progress, cancellation, unavailable-source guidance,
and success notifications.

Use a bounded progress-only IPC instead of overlapping full snapshot polling,
and share model package size and progress presentation across embedding,
speech, and OCR model settings. Promote the GoodBuddy Agent lock from the e2e
prerelease identifier to the formal 0.11.2 version for production publication.

Release note: 修复点击内置向量模型下载后界面无响应的问题;现在会立即显示下载来源、进度与取消操作,并将 GoodBuddy Agent 版本标识转为正式的 0.11.2。
2026-08-25 19:22:51 +08:00
mesalogo 500f7d2735 feat: add experimental LoongArch desktop preview
GoodBuddy can now be cross-packaged as an isolated Debian loong64 preview with verified Electron, node-pty, and Koffi binaries. The preview remains separate from the standard release matrix and excludes Agent, Remote Runtime, OpenCode, and native Canvas until those dependencies support LoongArch.

The Chinese website identifies GoodBuddy as the first open-source desktop AI assistant with LoongArch support while clearly labeling the independently built package as a preview.

Release note: 新增龙芯 LoongArch 独立编译预览版,并提供完整构建文档;当前预览版不包含 Agent、Remote Runtime、OpenCode 和原生 Canvas。
2026-08-25 18:41:35 +08:00
mesalogo e5aebaa67d feat: add independent managed SSH agent packages
Managed SSH previously tied remote Agent and Runtime artifacts to desktop
packaging and Host activation. Settings now manages verified Linux x64 and
arm64 .gbagent packages explicitly, with source-aligned download and update
plus offline import and export. Each package contains the signed Agent, pinned
Node, and compatible OpenCode Runtime, and project activation never downloads
one automatically.

Dedicated native CI and a protected release workflow publish deterministic,
signed cumulative catalogs to GitHub and Beijing OSS without replacing the
desktop Latest release. Desktop packaging is metadata-only, remains independent
of Agent artifacts and signing credentials, and Linux desktop releases add RPM
installers.

The managed SSH path also keeps model credentials in Main, enforces Ask as
read-only, verifies signed Agent and Runtime inputs, preserves ACP recovery, and
reports Host update cancellation and failures accurately.

Release note: 远程项目现可在“设置”中手动下载、更新或离线导入签名 Agent 包,桌面安装包与远程组件发布相互独立;Linux 桌面版同时新增 RPM 安装包。
2026-08-25 18:28:29 +08:00
mesalogo 88658aceaa fix: stabilize knowledge graph extraction for long documents
Long document imports could send oversized graph prompts, surface empty model
responses without useful diagnostics, and fail the entire new document even
when its full-text index was usable. Graph extraction now uses bounded token
batches with retry and timeout handling, preserves successful partial results,
and reports graph failures independently from new-document indexing.

Chinese graph retrieval now recognizes bounded CJK query terms, while graph
search keeps one-hop relationship context visible around matched entities.

Release note: 修复长文档知识图谱抽取易因空响应或超时导致整篇文档导入失败的问题;现在图谱按 Token 分批重试,失败时仍保留新文档全文检索,并改进中文图谱问答和关系上下文展示。
2026-08-25 08:03:15 +08:00
mesalogo 098f21b47a feat: add in-app feedback submission
GoodBuddy previously had no direct way to send feedback from the desktop app. The About & Updates page now opens a bilingual, accessible feedback dialog for problems, feature requests, experience notes, and other feedback, with an optional email address and one screenshot.

Submissions use the narrow preload and trusted IPC boundary, a fixed production endpoint with strict TLS, bounded screenshot processing, an isolated resettable installation ID, preserved drafts, and idempotent manual retries. GoodBuddy does not automatically attach conversations, logs, files, settings, or credentials.

Release note: 现在可在“关于与更新”中直接提交问题、建议与体验反馈;邮箱和截图均可选,提交失败时会保留草稿供重试。
2026-08-25 08:01:42 +08:00
jojokeeandmesalogo efecf88af7 Update README.md
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize production Agent signing (push) Canceled after 0s
Cross-platform packages / Acquire Linux arm64 Agent runtime (push) Canceled after 0s
Cross-platform packages / Acquire Linux x64 Agent runtime (push) Canceled after 0s
Cross-platform packages / Linux arm64 signed Agent bundle (push) Canceled after 0s
Cross-platform packages / Linux x64 signed Agent bundle (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
增加官网信息和产品特色描述
2026-08-25 00:21:39 +08:00
mesalogo 6c74f057eb feat: gate remote projects as a technical preview
Remote Host and managed SSH project entry points were always exposed while the signed artifact workflow remains preview-stage. Remote Projects now defaults off behind its own Platform Features tab, preserves saved Hosts, projects, and credentials, and returns active remote work to a local project when disabled.

Main IPC enforces the same setting while still allowing in-flight cancellation. Application settings migrate to version 9 with the preview disabled.

Release note: 新增“远程项目(技术预览)”独立页签和开关,默认关闭;启用后显示 SSH Host 与远程项目功能,关闭不会删除已有配置。
2026-08-25 00:16:02 +08:00
mesalogo 607bb33249 feat: add managed SSH execution spaces
Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI.

Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts.

Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
2026-08-24 23:02:18 +08:00
mesalogo 3829a871f5 docs: design in-app feedback integration
Define the About-page feedback workflow, public API boundary, strict Main-process TLS path, screenshot handling, privacy rules, idempotency, and validation plan. The external feedback platform remains separately maintained and the client implementation remains gated on its final production HTTPS origin.
2026-08-24 17:50:47 +08:00
mesalogo 981a174e9f fix: keep Platform Features tabs visible
The Platform Features tab strip could shrink to zero height inside the scrollable Settings layout, leaving the Magic Notes tab present in the DOM but invisible and unusable. Prevent the strip from shrinking and add a CSS regression guard.

Release note: 修复设置中心“平台功能”中的“魔法笔记”页签不可见、无法点击的问题。
2026-08-24 08:30:51 +08:00
mesalogo 1154a6d8da fix: restore essential UI guidance
Settings navigation and several workspace surfaces had lost context that helps users scan, understand, and operate them. Restore concise bilingual descriptions, workflow cues, and visual hierarchy for the welcome message, composer, activity history, knowledge workflows, Magic Notes, assistant workbar, Runtime settings, and channel projects while keeping decorative or duplicate copy removed.

Release note: 恢复设置菜单副标题、新对话引导及知识库、运行记录、魔法笔记和助手工作栏中的必要说明,提升功能可发现性与页面信息层级。
2026-08-23 20:04:56 +08:00
mesalogo 076b3aaa7b feat: add managed Granite embedding models
GoodBuddy previously required an external OpenAI-compatible endpoint for vector retrieval. It can now install, verify, import, remove, and run the built-in Granite multilingual embedding model in an isolated local inference process, while retaining custom embedding connections and migrating existing settings.

The model connection UI now manages built-in and custom vector connections consistently, reports the actual retrieval path, preserves credentials across failed saves, and renders the vector tab synchronously. Related settings copy, switch styling, metadata typography, and renderer bundle guardrails are aligned with the resulting workflow.

Release note: 新增可管理的内置 Granite 多语言向量模型,支持本地下载、离线导入、连接切换与诊断;同时优化向量设置及相关界面的文案、开关和状态显示。
2026-08-23 11:26:44 +08:00
mesalogo 58d774ae0c fix: remove redundant context workbar tab
The assistant workbar duplicated attachment and knowledge-library controls already available in the composer, while its memory section exposed an unimplemented placeholder. The workbar now keeps Task Center, Files, Browser, and Results, with future task context and memory details assigned to Task Center.

Release note: 移除助手工作栏中重复且暂未完整实现的上下文页签;附件与知识库继续在会话输入区管理,工作栏保留任务中心、工作区、浏览器和成果。
2026-08-22 17:50:01 +08:00
mesalogo 7c34abdfbf fix: reveal conversation actions on hover
Conversation rows always reserved space for the more-actions button, and the active row kept it visible. The button now replaces the right-side timestamp only on hover, keyboard focus, or while its menu is open, leaving more room for conversation titles and timestamps by default.

Release note: 会话列表仅在悬停或键盘聚焦时显示更多操作按钮,更新时间默认更靠右,为会话标题留出更多空间。
2026-08-22 15:52:04 +08:00
mesalogo fb51793595 feat: show incomplete Magic Notes todo counts
Magic Notes can now show a default-on incomplete todo badge in the primary
navigation. The badge hides at zero, caps its visual label at 99+ while
retaining the exact accessible count, and refreshes after user or Agent/MCP
note changes.

Platform Features > Magic Notes includes a persistent switch that hides only
the count without changing notes or the Magic Notes entry. Existing settings
migrate with the count enabled.

Release note: 左侧“魔法笔记”入口现在可显示未完成待办数量;可在“平台功能 → 魔法笔记”中关闭,超过 99 项显示“99+”。
2026-08-22 08:32:57 +08:00
mesalogo fee4686750 fix: sync todo completion with source notes
Completing a Magic Notes todo updated the stored checklist but left the cached
source note stale until the workspace was remounted. Todo updates now return
the refreshed source note and update the active note cache immediately for
both completion and reopening.

Release note: 修复在待办视图勾选或取消待办后,源笔记必须切换页面才显示最新状态的问题;现在会即时同步。
2026-08-21 23:27:41 +08:00
mesalogo 63f0f7f0b0 fix: restore full-height primary sidebar
The full-width titlebar pushed the brand, project switcher, navigation, and account controls down from the window edge. The primary sidebar now spans the full window height while the titlebar belongs only to the right content frame, preserving the docked assistant workbar alignment.

Release notes now use the same full-window backdrop as other application modals, including over the titlebar.

Release note: 恢复左侧主导航的全高布局,并让版本更新说明的遮罩完整覆盖顶栏。
2026-08-21 17:20:34 +08:00
mesalogo 97419f3ef7 fix: align assistant workbar controls
Existing titlebar controls could be pushed into a second row, while task controls left uneven or redundant surfaces. The window titlebar is restored, the shared workbar toggle stays directly below the close control, empty conversation task rows disappear, and Task index owns creation plus full-width filters.

Release note: 修复助手工作栏顶部错位与任务入口重复问题;空会话不再显示任务栏,任务索引统一提供新建入口和全宽状态筛选。
2026-08-21 08:43:17 +08:00
mesalogo b29f2d7b0d fix: keep assistant workbar docked and resizable
The right assistant workbar could switch to an overlay, duplicate its close control, and let resizing crowd or clip the chat composer. It now uses one directional toggle below the window controls, remains a docked sibling, and preserves equal 300px minimums for the chat and workbar with symmetric composer gutters.

Release note: 修复右侧助手工作栏遮挡聊天区及拖动边界不一致的问题;工作栏现在使用单一方向按钮,并在左右区域达到相同最小宽度后停止拖动。
2026-08-20 23:58:48 +08:00
mesalogo a0977645a3 fix: enable verified ModelScope speech downloads
Selecting ModelScope could leave supported speech models unavailable because their fixed targets were incomplete and the official LFS redirect host was not allowed. Speech models now resolve complete per-source artifact fingerprints while preserving integrity checks, ZIP compatibility, and source isolation.

Release note: 修复选择 ModelScope 后部分语音模型仍无法下载的问题;六个语音模型现均可从已验证的固定 ModelScope 来源下载。
2026-08-20 23:57:38 +08:00
mesalogo aa339a73d6 feat: add independent conversation branching
Local conversations can now continue in a new branch after stable content is persisted. Each branch uses new conversation and message IDs, keeps its source visible, and remains independent without copying Tasks, queue items, runs, memories, or artifact ownership.

Conversation Task rows are also denser, align status markers at the right edge, and use an accessible checked success marker without redundant completion text.

Release note: 本地会话现在可从“在新会话中继续”创建独立分支并显示来源;Task 子项布局更紧凑,状态统一显示在右侧。
2026-08-20 17:13:57 +08:00
mesalogo 65c3ab9ab0 fix: improve magic notes placeholder contrast
The Magic Notes editor inherited Quill's dark placeholder color, making the hint difficult to read in dark mode. It now uses the shared muted text token in both themes, with regression coverage for the editor style.

Release note: 修复暗色模式下魔法笔记输入提示文字过暗的问题,现在提示内容清晰可读。
2026-08-20 14:31:45 +08:00
mesalogo 104ac2aec1 chore: release GoodBuddy 0.11.0
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Prepare the approved bilingual 0.11.0 release metadata and synchronize package versions. macOS packaging now signs and notarizes when all Apple credentials are available, emits unsigned packages when none are configured, and rejects partial credential sets so the six-platform release can proceed without silently misrepresenting signing status.
2026-08-20 13:20:50 +08:00
mesalogo d7cb7ba181 fix: verify model generation and retain credentials
Save and test could accept a successful endpoint response without proving that the selected model generated output, while changing a connection URL or temporarily disabling authentication could require the API Key again.

Model tests now issue bounded real text or image generation requests and validate their output. API Keys remain encrypted with their connection across URL and authentication changes until explicitly cleared or the connection is deleted, and the form places the key directly below authentication. Real tests may incur a small amount of provider usage.

Release note: 修复模型“保存并测试”仅验证连通性的问题;现在会执行真实文本或图片生成测试,并在修改地址或临时关闭认证时继续保留该连接的加密 API Key。
2026-08-20 11:48:16 +08:00
mesalogo f44a0dc907 feat: improve desktop reliability and customization
Address reliability and consistency gaps across Agent Runtimes, persistence, settings, Knowledge, Magic Notes, Smart Heartbeat, and the download site. Runtime processes now have bounded lifecycle cleanup and atomic configuration rollback, while model packages and persisted mutations recover safely.

Add a configurable global shortcut, protect unsaved work, improve modal and keyboard behavior, localize the built-in project without rewriting stored data, and lazy-load heavy renderer routes under enforced bundle budgets. Align project forms and disabled controls with shared typography and interaction states, and strengthen website release metadata validation and navigation accessibility.

Release note: 修复 Runtime、设置、知识库、魔法笔记与智能心跳中的可靠性和交互一致性问题;新增可配置全局快捷键,改进无障碍与加载性能,并强化官网下载校验。
2026-08-20 10:11:06 +08:00
mesalogo 20c15f74c6 feat: enrich project selector details
The project selector previously used a native list that showed only project names and inherited platform-specific styling. It now presents an accessible rich menu grouped into local projects and remote channels, with folder or channel context for each option.

Release note: 项目选择器现在按本地项目和远程通道分组,并显示目录或通道来源,切换项目时更容易辨认目标。
2026-08-19 22:19:47 +08:00
mesalogo 34033053be feat: add persistent conversation input queue
Messages and Scheduled Task occurrences previously could not share one ordered path while a Conversation was active. They now enter a durable FIFO queue with frozen execution settings and bounded attachments, recover safely after restart, and never write concurrently to the same timeline.

The compact queue above Composer supports removal and explicit interrupt-and-promote actions. SQLite schema v23 preserves pending schedule work, while Conversation Task children reuse the shared status-dot semantics for running, completed, failed, approval, paused, and cancelled states.

Release note: 回复生成期间仍可继续发送普通消息;消息与 Scheduled Task 会按 Conversation 顺序排队,并支持删除待发送项或立即中断后优先执行。
2026-08-19 21:46:43 +08:00
mesalogo 993c439228 feat: add stable scheduled tasks
Scheduled tasks previously created separate visible work for each run and lacked one conversation-backed product identity. Custom tasks now create or reuse one stable Task and Conversation, reuse that identity across triggers, and write text results back with Task provenance.

Tasks default to Execute while preserving the configured Runtime, tool authorization, and high-risk approval boundaries. Schema v22 backfills existing schedules to stable Task and Conversation links without deleting historical runs.

The conversation list, conversation Task strip, and Task Center now expose the same Task, with localized status metadata, overflow-aware titles, and shared schedule controls.

Release note: 现在可以创建关联当前或新会话的定制计划任务;重复执行会复用同一 Task 并将文本结果回写会话,左侧会话列表和 Task Center 可直接查看和管理。
2026-08-19 15:37:01 +08:00
mesalogo 43e1d162dc feat: add explicit managed model download sources
Managed speech and OCR downloads previously used catalog-specific source URLs without a global selection. Platform Features now lets users choose ModelScope by default or Hugging Face, while Main validates and freezes that source for each download.

Verified coverage remains explicit: downloads never mix artifacts or silently switch sources, and installed models plus ZIP imports stay source-independent.

Release note: 可在“设置 → 平台功能 → 通用设置”中选择 ModelScope 或 Hugging Face 作为后续语音输入与 OCR 模型下载源;缺少完整已验证文件或下载失败时不会静默换源。
2026-08-19 13:43:16 +08:00
mesalogo c41d7e8b96 fix: keep opened conversations pinned to bottom 2026-08-19 09:31:40 +08:00
mesalogo ccaab25d11 docs: organize product documentation by domain
Replace the flat features directory with document-type and functional-domain navigation, update repository-wide links, and add a top-level documentation index.

Define Task, Conversation, Job, Subjob, Run, Scheduled Task, Goal Task, and Task Center in one canonical document set. Keep Smart Heartbeat ownership separate and leave future partitioned memory explicitly undesigned.
2026-08-19 07:52:43 +08:00
mesalogo 3935f50017 feat: support scoped Smart Heartbeat plans
Smart Heartbeat plans were implicitly bound to one active project and duplicated across multiple configuration surfaces. Plans can now target Global or selected projects from one authoritative editor, aggregate bounded input across that scope, validate project-specific outputs, and preserve existing data through the schema migration.

Reports remain unscoped with frozen scope metadata, while duplicate Heartbeat forms are removed from Settings and Task Center.

Release note: 智能心跳计划现在可从统一入口选择 Global 或一个、多个 Project;旧配置和历史会自动保留,项目级记忆与后续任务会严格写入所选范围。
2026-08-19 07:51:52 +08:00
mesalogo 28b1590749 fix: show expert outputs before synthesis
Parallel expert runs previously exposed only child status, while the synthesized answer appeared above the expert cards. Each expert now retains and exposes its complete output, the cards can be expanded, and the final synthesis is rendered beneath them and persisted with the conversation.

Release note: 多专家并行分析现在可展开查看每位专家的完整输出,并在其下方显示总 Agent 的综合结果。
2026-08-19 01:32:19 +08:00
mesalogo 87dec9baf3 fix: let channels control reply length
ChannelService previously truncated every reply at 4,000 characters and discarded the remainder before the platform adapter could handle it. Complete executor output now reaches each channel driver, while error redaction and channel-specific platform limits remain intact.

Release note: 消息通道回复不再由公共服务统一截断;完整结果现在交由微信、企业微信或钉钉按各自平台能力处理。
2026-08-19 00:50:09 +08:00
mesalogo 0413807b8f fix: keep chat replies out of results
Completed local and channel replies were duplicated into Results as Markdown artifacts, making the panel a second conversation history. Replies now stay in their conversations, legacy duplicates are filtered without deleting data, and standalone images, scheduled jobs, headless delegations, heartbeats, and imports remain visible.

Release note: 普通本地与消息通道回复不再重复出现在成果栏;已有重复内容仅从列表隐藏,不会删除历史数据。
2026-08-18 13:14:13 +08:00
mesalogo e387c09f52 fix: let the assistant sidebar use available width
The resizable assistant sidebar stopped at 640px even on wide windows, leaving result and workspace previews unnecessarily cramped. It now expands up to the viewport-derived limit while preserving at least 520px for the main workspace.

Release note: 助手工作栏现在可在宽屏上拉伸到更大宽度,同时为主工作区保留至少 520 像素。
2026-08-18 12:39:29 +08:00
mesalogo 08884c127c fix: hide inactive browser profile settings
Managed browser profile controls suggested that login state and cookies would be reused, but named profiles do not yet affect browser execution. The settings UI now hides profile management while preserving the built-in browser switch, diagnostics, stored profile metadata, and compatibility APIs.

Release note: 暂时隐藏尚未实际生效的托管浏览器配置,避免误以为登录状态和 Cookie 会被复用;已有底层数据不会删除。
2026-08-18 12:36:18 +08:00
mesalogo 7a46422fc1 fix: move built-in browser to direct model tools
Browser control was grouped with client PC control even though it operates GoodBuddy's isolated browser. It now appears as Built-in Browser under direct-model tools with its existing master switch, profiles, and diagnostics, while Computer Control only lists capabilities that act on the client PC.

The existing capability ID and saved state are preserved. Enabling the browser lets direct models use it in Execute mode without per-action prompts.

Release note: 将内置浏览器移至直连模型工具,并明确它不会控制客户端已安装的浏览器;原有开关状态保持不变。
2026-08-18 11:24:09 +08:00
mesalogo 5dddccb282 docs: document verified release workflow
Record the end-to-end tagged release process, OSS OIDC publication contract, immutable-tag recovery rules, and public post-release checks verified by v0.10.4.

Align the build guide with the 12 installer and 20 GitHub asset contract, latest-index ordering, rerun behavior, and annotated release tags.
2026-08-18 09:12:51 +08:00
mesalogo 60119a4317 fix: align mirror publishing region
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The v0.10.3 release index was generated, but the pinned upload client rejected object uploads because V4 signing did not receive a region. The application and website also addressed the Beijing bucket through the previous Hangzhou endpoint.

Derive and pass the endpoint region to every upload, pin workflow configuration to the trusted production bucket, and align application and website URL validation with its actual endpoint. Advance recovery metadata to 0.10.4 and remove the unpublished 0.10.3 note.

Release note: 0.10.4 carries forward the approved mirror-node and conversation timestamp improvements from the unpublished candidates.
2026-08-17 22:17:43 +08:00
mesalogo 7ecb735db0 fix: restore release index publication
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
The v0.10.2 native packages completed, but the publication job rejected the valid --base-url option before any GitHub Release or mirror assets were published. Normalize CLI option names and cover the exact workflow arguments with a regression test.

Advance recovery metadata to 0.10.3 and move the approved user-facing notes forward, removing the unpublished 0.10.2 note to avoid a duplicate first-open display.

Release note: 0.10.3 carries forward the approved mirror-node and conversation timestamp improvements from the unpublished 0.10.2 candidate.
2026-08-17 21:21:07 +08:00
mesalogo 255df74d87 fix: order website support metrics logically
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The support overview previously listed its counts as 3, 2, 4, 1. It now presents Runtime, systems, architectures, and the entry point in a clear 4, 3, 2, 1 sequence.
2026-08-17 17:39:00 +08:00
mesalogo e8c1c1f3f3 docs: merge community README update 2026-08-17 17:37:57 +08:00
mesalogo abe017a465 chore: release GoodBuddy 0.10.2
Set package metadata to 0.10.2 and package the approved bilingual release notes for the mirror-node update source and clearer recent-conversation timestamps.
2026-08-17 17:34:57 +08:00
mesalogo dc8f86ff3e feat: add trusted mirror update source
Version checks and downloads previously depended on GitHub Release. About & Updates now offers GitHub or a validated mirror source, keeps the selector beneath the startup-check switch, and disables it when startup checks are off.

The website resolves platform downloads from a bounded OSS release index with a GitHub fallback. Tagged releases publish and verify immutable OSS assets through OIDC before switching the latest-version index; deployment requires the configured Alibaba Cloud environment variables and role.

Release note: “关于与更新”新增 GitHub 与镜像节点选择,启动检查、手动检查和下载页使用同一可信来源;官网下载也可按系统、架构和安装包类型直接选择。
2026-08-17 17:20:24 +08:00
mesalogo ce15e7021c fix: clarify conversation list timestamps
The recent-conversation list previously showed only a time, making older entries ambiguous. It now shows local time for conversations updated today, a local date for older entries, and includes the year when needed; the full timestamp remains available on hover.

Release note: 会话列表现在当天显示时间、较早会话显示日期,跨年记录同时显示年份,更容易快速判断最近更新时间。
2026-08-17 17:11:00 +08:00
jojokeeandGitHub fa9ead895f Update README.md
增加社区入群方式
2026-08-17 14:15:00 +08:00
mesalogo 9773f3ceb4 docs: complete 0.10.1 recovery notes
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
The cumulative recovery notes carried the failed 0.10.0 content forward but did not yet mention the user-visible Magic Notes loading-state correction. Include that behavior in both languages without adding another item or retaining a duplicate 0.10.0 entry.
2026-08-17 14:01:02 +08:00
mesalogo 47214ef87f fix: distinguish Magic Notes loading state
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
The AI comments pane reused its final empty-state title while the selected note detail was still loading. Slow CI runners could therefore satisfy the title wait before the final hint existed, causing the immutable v0.10.0 release validation to fail repeatedly.

The pane now presents a distinct loading state until note data is ready, and regression coverage controls the delayed detail response before asserting the final empty state. Recovery metadata moves the unpublished 0.10.0 notes forward to 0.10.1 so upgrading users see the cumulative release once.

Release note: 魔法笔记的 AI 评论区在笔记详情加载期间会明确显示加载状态,不再短暂显示空评论提示。
2026-08-17 13:54:12 +08:00
mesalogo 98fabe9e2e chore: reconcile GitHub Pages history
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
The GitHub Pages deployment commit existed only on github/main while the local branch had independently retained the same workflow and continued evolving the website. Record both histories in one merge without changing the already reviewed 0.10.0 candidate tree.
2026-08-17 13:19:44 +08:00
mesalogo cbbe30896e feat: expand multi-runtime workflows for 0.10.0
GoodBuddy previously exposed Runtime capabilities, MCP assignments, plugin controls, context compaction, usage reporting, and task-completion behavior through incomplete or inconsistent paths. This release unifies managed OpenCode, Continue, and DeepSeek Harness controls; adds DSH plugin and image workflows; strengthens MCP and Runtime lifecycle bounds; fixes Windows notification activation; validates cross-architecture packages; and presents the approved bilingual four-section release notes.

The DSH plugin marketplace remains a default-off preview whose trusted third-party code runs with current-user permissions. Ask remains read-only, Execute keeps approval controls, and context compaction may use the selected model without deleting GoodBuddy chat history.

Release note: GoodBuddy 0.10.0 重点完善多 Runtime 工作流,统一 OpenCode、Continue 与 DeepSeek Harness 的能力、MCP、插件和上下文管理,并提升长对话、多会话与任务通知的连贯性。
2026-08-17 12:57:12 +08:00
mesalogo f5ee9b2198 fix: refine Magic Notes editing layout
The Magic Notes editor had ambiguous font-size labels, cramped pane spacing, and a fixed short writing area. The toolbar now groups controls and uses numeric sizes, the pane and empty states are balanced, and the editor can be resized vertically from a taller default.

Release note: 优化魔法笔记编辑体验:字号改为数字、工具栏与分栏布局更清晰,输入区域支持纵向拖拽并默认显示更多内容。
2026-08-17 10:33:19 +08:00
mesalogo ddcc6dd1d9 fix: align website branding and spacing
The website used redrawn brand marks, generated icons retained opaque white edges, and adjacent download and Runtime sections accumulated excessive vertical space. It now uses synchronized official theme icons with transparent antialiased edges and a consistent responsive section rhythm.

Release note: 官网与应用图标现统一使用官方 GoodBuddy 标识,并优化下载区与 Agent Runtime 区之间的页面间距。
2026-08-17 01:30:53 +08:00
mesalogo e235522c0d feat: refresh product website experience
The website previously buried platform downloads and presented the desktop assistant, Agent Runtime, and broader work capabilities with competing priorities. It now leads with the desktop assistant and AI programming workbench positioning, surfaces cross-platform and domestic Linux compatibility, and uses a responsive interactive product preview with synchronized tilt and soft lighting.

Release note: GoodBuddy 官网现在更清晰地展示桌面助手、Agent Runtime、跨平台与国产化支持,并提供更直观的动态产品预览。
2026-08-17 01:05:46 +08:00
mesalogo 2b556e38bd fix: align settings page shell
Settings Center used different header and content gutters, omitted the standard title divider, and mirrored scrollbar space on the left of its category navigation. The title and two-column content now share responsive page gutters, restore the standard PageHeader boundary, and balance selected rows against the visible scrollport.

Release note: 统一设置中心与其他一级页面的标题分隔线和响应式边距,并修复左侧分类选中高亮在滚动时的视觉偏移。
2026-08-17 00:52:51 +08:00
mesalogo 2df8fb1364 fix: balance settings navigation highlights
The settings category list reserved scrollbar space only on its right edge, so selected rows appeared visibly offset. The navigation now reserves symmetric space and uses wider desktop and medium-width columns while preserving the horizontal narrow-window layout.

Release note: 修复设置中心左侧分类选中高亮左右不平衡的问题,并适当加宽分类导航以改善标题和说明的可读性。
2026-08-17 00:35:15 +08:00
mesalogo 792d80e67c feat: configure built-in MCP access
Built-in MCP servers were always granted to supported runtimes without per-server controls. They now have persistent enablement and runtime assignments for direct models, managed OpenCode, and Continue, while DeepSeek Harness remains visibly unsupported and cannot be assigned.

MCP settings are reorganized into Built-in MCP, Direct model, Custom MCP, and Computer control tabs. Direct-model web search now uses the same accessible collapsible inventory pattern as other tool groups.

Release note: 内置 MCP 现在可分别启停并分配给直连模型、OpenCode 和 Continue;MCP 设置分类与直连模型工具列表也更清晰,DeepSeek Harness 会明确显示为暂不支持。
2026-08-17 00:25:46 +08:00
mesalogo b751c70d75 feat: add model-aware DSH image input
DeepSeek Harness previously treated every selected model as text-only. It now carries the model profile's image capability through Main, the Utility Host, ACP, and Pi-AI, rejects unsupported images before model invocation, and validates supported JPEG/PNG data in a bounded process-local attachment store.

The Composer keeps universal controls on its first row, places OpenCode and Continue controls on a dedicated responsive row, and anchors context compaction without shifting the footer. Runtime supervision ownership for future Subagents and Jobs is documented for the right sidebar.

Release note: DeepSeek Harness 现可按所选模型连接的能力安全接收 JPEG/PNG 图片;Composer 同时将 Runtime 专属选择器移到独立一行,并固定上下文压缩入口。
2026-08-16 23:30:54 +08:00
mesalogo 18263a6b23 fix: keep local message IDs out of model requests
Direct-model conversation history began carrying GoodBuddy UUIDs after durable context compression was added. OpenAI Responses rejected follow-up messages because provider message IDs must start with msg_, and the same metadata could also reach other protocol payloads.

Model request serialization now strips local IDs across Anthropic Messages, Chat Completions, and Responses while retaining provider IDs during Responses tool rounds. Regression coverage includes compression, tools, and a gated real-model follow-up.

Release note: 修复默认直连模型在继续对话或重新编辑发送时可能因消息 ID 格式错误而失败的问题。
2026-08-16 22:23:03 +08:00
mesalogo 80526c57bf fix: streamline settings and context status
Settings Center navigation was hard to read and constrained form content,
while Runtime customization duplicated headings and save actions and could
lose drafts. Settings now uses readable navigation and wider content, saves
base and native Runtime settings together, protects drafts, and presents one
compact capabilities and defaults section.

Conversation context meters could retain display-only thresholds from old
Runtime settings. They now persist only measured usage, derive compression
lines from current Runtime and model settings, and normalize legacy snapshots
when loading them.

The static website now uses the project GitHub Pages canonical URL and includes
a validated Pages deployment workflow.

Release note: 优化设置中心和 Agent Runtime 配置流程,避免重复标题、重复保存和未保存定制丢失;压缩线会随当前设置即时更新,官网也可通过 GitHub Pages 自动部署。
2026-08-16 19:31:49 +08:00
mesalogo b56b0f8826 feat: add native runtime customization
OpenCode and Continue customization was previously planned but unavailable, and Runtime-native capabilities were not represented consistently across providers. GoodBuddy now provides secure Main-owned customization settings, truthful native inventories, OpenCode Agents and Commands, Continue Rules and Prompts, DSH Web Search/Fetch, MCP Prompt and Resource metadata, and manual compaction where supported.

Native capabilities are presented in eleven accessible tabs with Tools separated from Commands, LSP, and Formatters. Tool source and Ask/Execute availability are explicit, external OpenCode remains connection-only, Continue reports unsupported static tool discovery instead of advertising unreachable Skills, and disposable inventory probes avoid retaining background runtimes.

Ask remains read-only at the Runtime boundary, Execute keeps the existing authorization controls, and credentials remain confined to Main.

Release note: 新增 OpenCode、Continue 与 DeepSeek Harness 的 Runtime 原生定制与真实能力清单;工具来源、Ask/Execute 可用性、上下文压缩和 MCP 元数据现在可清晰查看,同时继续保持 Main 进程凭据保护与现有权限边界。
2026-08-16 17:08:46 +08:00
mesalogo 876f120a63 feat: publish website on GitHub Pages
The static product site did not have an automated public deployment. Changes under sites now validate and deploy from main to mesalogo.github.io/goodbuddy, and package metadata points to that address.
2026-08-16 14:17:49 +08:00
mesalogo ff61b5f81d feat: add DSH plugin marketplace and shared MCP
GoodBuddy could share Skills across runtimes, but custom MCP remained limited and DeepSeek Harness could not manage third-party extensions. The app now provides a default-off DSH npm marketplace with managed installation, configuration, failure isolation, and packaged npm support, while assigned custom MCP is available to managed OpenCode, Continue Agent, and DeepSeek Harness in Execute.

Third-party DSH install scripts, initialization, and tools run with the current user's permissions. Ask remains read-only at dispatch, and turning off the marketplace hides management without disabling installed plugins.

Release note: 新增默认关闭的 DSH 插件市场,并让自定义 MCP 可分配给 OpenCode、Continue 和 DeepSeek Harness;安装第三方插件前会明确提示当前用户权限边界。
2026-08-16 11:47:11 +08:00
mesalogo 9e6f664e06 fix: retain every direct-model usage call
Multiple model invocations in one task could share a call identifier when
a provider omitted or reused its response ID. Database upserts then replaced
earlier tool-round or summary usage, causing cumulative activity totals to
undercount successful calls.

Each completed direct-model invocation now receives a unique local call ID
while retaining the provider ID as diagnostic context. Tool rounds, final
responses, image calls, and repeated compression summaries are therefore
stored independently.

Release note: 修复连续工具调用或上下文摘要可能覆盖前序模型用量的问题;现在每次成功调用都会分别计入运行记录和累计统计。
2026-08-16 01:18:07 +08:00
mesalogo c98fe67f1a feat: add durable context compression
Long direct-model conversations mixed provider usage with local estimates,
and Agent tool rounds could remain above the configured compression target.
Compression state and status markers also did not reliably survive restarts
or bounded history rollover.

Direct-model calls now prefer provider-reported usage, compact complete
conversation turns and Agent tool rounds within reserved payload budgets,
and persist reusable summaries with scope-specific markers. The chat meter
separates latest-call usage from estimated compressed conversation size,
while failed or cancelled calls retain the last successful measurement.

Release note: 直连模型现可在长对话和多轮工具执行中自动压缩旧上下文,并分别显示本次调用用量与压缩后对话估算;摘要会自动保存并跨重启复用,无需手动操作。
2026-08-16 00:36:11 +08:00
mesalogo b3c753fac2 fix: label token usage by runtime and model
Token usage grouped and displayed provider identifiers, which exposed the internal goodbuddy Harness provider and made it appear comparable to OpenAI. Usage rows now follow the product's Runtime and model identity, including Direct model, OpenCode, Continue, and DeepSeek Harness, while provider data remains available only for cache accounting.

Release note: 修复 Token 用量中显示 goodbuddy 等内部标识的问题;统计现在按 Runtime 与模型名称清晰归类。
2026-08-15 22:38:48 +08:00
mesalogo 36b38ce2a8 feat: show normalized prompt cache hit rates
Token usage previously showed cache reads and writes without a comparable hit rate. Activity usage now calculates a weighted cache hit rate using OpenAI-compatible input totals and Anthropic's separately reported input, cache-read, and cache-write tokens, then shows it in totals and grouped details.

Release note: Token 用量统计新增缓存命中率,并针对 OpenAI 兼容接口与 Anthropic Messages 的不同上报口径进行归一化计算。
2026-08-15 22:01:49 +08:00
mesalogo 0b96400e51 fix: derive run status from final result 2026-08-15 21:17:17 +08:00
mesalogo 939c3ab1b1 fix: allow editing context compression limits 2026-08-15 21:11:02 +08:00
mesalogo 1cde0f3385 feat: show active conversations in sidebar 2026-08-15 20:57:46 +08:00
mesalogo c2b9ce4ff0 feat: preserve pages and compress context 2026-08-15 20:44:47 +08:00
mesalogo 1031ea618b fix: preserve per-conversation chat state 2026-08-15 19:20:17 +08:00
mesalogo 3f9defbad1 fix: show newest release notes first 2026-08-15 18:56:32 +08:00
mesalogo fd50d6555f chore: release 0.9.3 2026-08-15 18:19:25 +08:00
mesalogo faa4456661 fix: render preloaded routes synchronously 2026-08-15 18:06:04 +08:00
mesalogo 79d3d73038 fix: stream interactive model tool rounds 2026-08-15 18:06:04 +08:00
mesalogo afa7719ff2 feat: add direct model context compression 2026-08-15 18:06:04 +08:00
mesalogo 5f1ae7727a fix: use Latin activity node labels 2026-08-15 18:06:03 +08:00
mesalogo 67d30d9e21 feat: redesign run history views 2026-08-15 18:06:03 +08:00
mesalogo 9cd565fcc4 fix: emit DeepSeek Harness bundle manifest 2026-08-15 18:06:03 +08:00
mesalogo f27dd37f42 feat: improve streaming and startup responsiveness 2026-08-15 18:06:03 +08:00
mesalogo ed8d1791b1 feat: synchronize channel project settings 2026-08-15 18:06:03 +08:00
mesalogo 34bbfab2af feat: run agent tools with host permissions 2026-08-15 18:06:03 +08:00
mesalogo 79e2511f6f feat: improve application responsiveness 2026-08-15 18:06:02 +08:00
lofyer 1329251b5a chore: release 0.9.2
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
2026-08-14 18:28:22 +08:00
lofyer a5f1e31900 feat: preserve chat reading position 2026-08-14 18:27:55 +08:00
lofyer 57c57d232c chore: remove redundant Harness compatibility notice 2026-08-14 16:38:45 +08:00
lofyer a83f24a334 docs: clarify failed release recovery 2026-08-14 16:04:37 +08:00
lofyer 81f7e4f9e5 chore: release 0.9.1
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-14 14:42:49 +08:00
lofyer d070091350 fix: stage target runtime dependencies for packaging 2026-08-14 14:42:31 +08:00
lofyer 836f74a55d chore: release 0.9.0
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-14 13:25:16 +08:00
lofyer 6c879ab34a refactor: remove ineffective Harness window guard 2026-08-14 13:25:00 +08:00
lofyer de497b9553 docs: expand product planning 2026-08-14 13:24:42 +08:00
lofyer 45aeecb6dd feat: expand DeepSeek Harness compatibility 2026-08-14 12:38:01 +08:00
lofyer 36e05d45fa feat: simplify DeepSeek Harness model selection
- replace competing model-source choices with one DeepSeek connection picker
- keep administrator-provided environment settings compatible without exposing them as a selectable UI source
- update bilingual guidance, tests, and runtime design documentation
2026-08-14 10:30:53 +08:00
lofyer 8286e120a1 feat: add DeepSeek Harness runtime 2026-08-14 10:04:34 +08:00
lofyer fca9888f83 fix: keep streamed reasoning visible 2026-08-14 09:55:07 +08:00
lofyer 7a86c96623 feat: add natural language configuration tools 2026-08-14 01:23:32 +08:00
lofyer 445ebbc002 fix: open the selected channel settings 2026-08-14 01:12:39 +08:00
lofyer 4d487729e8 fix: improve Chinese typography 2026-08-14 00:41:56 +08:00
lofyer 11f84ec0cf fix: correct WeChat QR scan instructions 2026-08-14 00:00:06 +08:00
lofyer 0309ff8734 docs: add English project overview
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-13 17:10:50 +08:00
lofyer a52462aaf4 docs: open source under 0BSD
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-13 16:39:24 +08:00
lofyer 7e04382829 chore: release 0.8.20
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-13 15:55:53 +08:00
lofyer 48381cbb89 fix: remove chat message dividers 2026-08-13 15:45:54 +08:00
lofyer aab961226f fix: harden scoped tools and settings persistence 2026-08-13 14:56:53 +08:00
lofyer bf1ec5d2f1 fix: contain wide chat tables 2026-08-13 14:03:36 +08:00
lofyer 7a078c6ffe fix: publish knowledge rebuilds atomically 2026-08-13 06:35:07 +08:00
lofyer 0c46afba59 fix: preserve knowledge indexing state 2026-08-13 04:47:44 +08:00
lofyer e3b5702767 fix: bound document extraction 2026-08-13 04:46:30 +08:00
lofyer 5b579ae100 fix: bound model streaming 2026-08-13 03:18:55 +08:00
lofyer 980f3a0c8f fix: preserve channel message delivery 2026-08-13 02:09:09 +08:00
lofyer 67cb69f07d fix: recover interrupted schedules 2026-08-13 01:41:52 +08:00
lofyer 8cd23bada1 fix: serialize runtime cleanup 2026-08-13 01:36:00 +08:00
lofyer fd1ff92927 refactor: simplify scoped data tools 2026-08-13 01:32:43 +08:00
lofyer 04a260133a fix: preserve conversation and note data 2026-08-13 01:32:10 +08:00
lofyer 40696d9ac7 feat: render interactive Mermaid diagrams 2026-08-13 00:30:47 +08:00
lofyer 86b63406c2 fix: streamline document parsing settings 2026-08-13 00:08:15 +08:00
lofyer d33df979da feat: render Markdown formulas with KaTeX 2026-08-12 23:48:44 +08:00
lofyer 2e489d5bc3 fix: stream direct-model reasoning with tools 2026-08-12 23:29:37 +08:00
lofyer ca5b722571 fix: keep streamed reasoning expanded 2026-08-12 23:10:25 +08:00
lofyer d769f31492 fix: separate model credential status 2026-08-12 22:58:50 +08:00
lofyer c224da75fe feat: redesign knowledge workspace 2026-08-12 21:46:05 +08:00
lofyer 111f487e20 feat: enhance local knowledge retrieval 2026-08-12 21:45:47 +08:00
lofyer e0e5a8c1b3 docs: specify knowledge retrieval enhancements 2026-08-12 21:45:20 +08:00
lofyer 6a44335238 feat: support dynamic MCP tool loading 2026-08-11 23:49:18 +08:00
lofyer 98d7166ab3 feat: organize MCP settings into tabs 2026-08-11 23:04:41 +08:00
lofyer 2982f1ae33 chore: release 0.8.19
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-11 21:18:10 +08:00
lofyer e2d7837d91 fix: make speech path test cross-platform 2026-08-11 21:13:32 +08:00
lofyer 6c0defcf04 chore: release 0.8.18
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Canceled after 0s
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Canceled after 0s
Cross-platform packages / Publish GitHub Release (push) Canceled after 0s
2026-08-11 21:01:53 +08:00
lofyer 44d30b428d feat: add bilingual release notes 2026-08-11 20:59:26 +08:00
lofyer 6942bef567 fix: preserve recovered opencode results 2026-08-11 20:58:10 +08:00
lofyer d8f1badad6 fix: preserve shared switch dimensions 2026-08-11 20:14:16 +08:00
lofyer beb756bb2e feat: add system time to model prompt 2026-08-11 20:08:43 +08:00
lofyer 9bbaa2c53b docs: rename UI design guide and clarify switches 2026-08-11 20:07:58 +08:00
lofyer 184180e618 feat: expand model tools and document handling 2026-08-11 19:52:58 +08:00
lofyer 71a8662690 feat: add project default runtime 2026-08-11 17:52:55 +08:00
lofyer e0e7bc573c feat: add document OCR and offline model archives 2026-08-11 16:49:51 +08:00
lofyer 19a4469561 fix: streamline tool failure feedback 2026-08-11 16:39:17 +08:00
lofyer fde18c1568 fix: allow execute runtime tools by default 2026-08-11 13:07:36 +08:00
lofyer aff3b82998 feat: localize interface and enrich magic notes 2026-08-11 12:01:58 +08:00
lofyer c8050f4a9a feat: expand local speech models 2026-08-11 01:34:28 +08:00
lofyer f16ef993bc docs: refine compatibility and add notes demo
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions
2026-08-11 00:52:31 +08:00
lofyer 80c4ef5ed0 docs: make release tags optional 2026-08-11 00:05:44 +08:00
lofyer 1f44782b98 docs: simplify compatibility overview 2026-08-11 00:01:58 +08:00
lofyer 88e77cc5d4 chore: release 0.8.12 2026-08-10 23:50:25 +08:00
lofyer 90c4e9d8cc fix: align knowledge workspace controls 2026-08-10 23:23:54 +08:00
lofyer 7ce58da5f5 fix: propagate image input capability 2026-08-10 23:15:00 +08:00
lofyer 6fd41d2cfd feat: make magic todo status editable 2026-08-10 23:04:12 +08:00
lofyer 5cb99f3097 feat: expand magic notes MCP tools 2026-08-10 23:02:45 +08:00
lofyer cb0319c4d1 fix: use radial force for knowledge graph 2026-08-10 22:57:31 +08:00
lofyer 2cc76fc960 fix: unify settings center layout 2026-08-10 22:30:32 +08:00
lofyer 7d15e83153 feat: enhance magic notes AI comments 2026-08-10 22:27:50 +08:00
lofyer ad79659308 feat: expand multimodal and knowledge workflows 2026-08-10 21:25:47 +08:00
lofyer 0fab985f28 feat: enhance knowledge workflows and refresh interface 2026-08-10 16:27:40 +08:00
lofyer 2cb712e4ba fix: align WeChat channel modes 2026-08-10 11:47:03 +08:00
lofyer a9ae00a845 fix: register native OpenCode skills 2026-08-10 11:31:02 +08:00
lofyer be82caebc4 fix: deduplicate Continue system messages 2026-08-10 10:36:47 +08:00
lofyer 5ea022ad5c feat: globalize Magic Notes and improve runtime tools 2026-08-10 10:27:14 +08:00
lofyer 1a8e110866 fix: make skill budget test cross-platform
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
2026-08-10 00:40:52 +08:00
lofyer 5e40db4e51 fix: preserve fully truncated skill warnings
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
2026-08-10 00:33:37 +08:00
lofyer b249df116a feat: add configurable Magic Notes and polish UI
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
2026-08-10 00:24:19 +08:00
lofyer 1cc969317d fix: align channel runtime and settings UI 2026-08-09 21:54:25 +08:00
lofyer 66b098ae36 feat: add secure remote channel media 2026-08-09 20:33:35 +08:00
lofyer 6c891f3522 feat: add remote channels and richer notes 2026-08-09 15:48:52 +08:00
lofyer 417a9fccb6 feat: improve skill import and runtime delivery 2026-08-07 21:01:17 +08:00
lofyer 954b42ef55 fix: improve shutdown and project continuity 2026-08-07 18:46:26 +08:00
lofyer 53d18e2b06 feat: improve runtime visibility and browser interaction 2026-08-07 17:53:14 +08:00
lofyer 2c715e5e81 chore: prepare GoodBuddy 0.8.6
Cross-platform packages / Validate source (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Has been cancelled
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Has been cancelled
Cross-platform packages / Publish GitHub Release (push) Has been cancelled
2026-08-07 13:11:45 +08:00
lofyer 32aba176c8 docs: require release tags for GitHub pushes 2026-08-07 10:58:00 +08:00
lofyer 17e66a3369 chore: prepare GoodBuddy 0.8.5 2026-08-07 10:45:01 +08:00
1347 changed files with 483848 additions and 31690 deletions
+9
View File
@@ -0,0 +1,9 @@
from pathlib import Path
import difflib
files = ['src/main/assistant/assistant-database.test.ts', 'src/main/assistant/activity-history-io.test.ts', 'src/main/assistant/assistant-storage-upgrade.test.ts', 'src/main/assistant/heartbeat-database.test.ts', 'src/main/magic-notes/magic-note-storage.test.ts']
patch = ''
for name in files:
old = Path(name).read_text(encoding='utf-8')
new = old.replace('DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;', 'DROP VIEW IF EXISTS supervision_review_current;\n DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;\n DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;\n DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;')
patch += ''.join(difflib.unified_diff(old.splitlines(True), new.splitlines(True), fromfile='a/'+name, tofile='b/'+name))
Path('.validation-fixtures.patch').write_text(patch, encoding='utf-8')
+620
View File
@@ -0,0 +1,620 @@
name: GoodBuddy Agent Release
on:
push:
tags:
- 'agent-v*'
permissions:
contents: read
concurrency:
group: agent-release-${{ github.ref }}
cancel-in-progress: false
jobs:
authorize:
name: Authorize Agent release
if: >-
github.ref_type == 'tag' &&
startsWith(github.ref_name, 'agent-v')
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
authorized: ${{ steps.policy.outputs.authorized }}
version: ${{ steps.policy.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Verify immutable Agent release tag
id: policy
shell: bash
run: |
set -euo pipefail
version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
expected="agent-v$version"
test "$GITHUB_REF_NAME" = "$expected"
test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = "tag"
test "$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" = "$GITHUB_SHA"
git fetch --no-tags origin main
git merge-base --is-ancestor "$GITHUB_SHA" refs/remotes/origin/main
echo "authorized=true" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.platform }} ${{ matrix.arch }} compound package
needs: authorize
if: needs.authorize.outputs.authorized == 'true'
strategy:
fail-fast: false
matrix:
include:
- platform: linux
arch: x64
runner: ubuntu-24.04
- platform: linux
arch: arm64
runner: ubuntu-24.04-arm
- platform: darwin
arch: arm64
runner: macos-15
runs-on: ${{ matrix.runner }}
timeout-minutes: 25
environment:
name: agent-signing
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify native build target
run: node -e "if(process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Native Agent target mismatch')"
- name: Resolve locked native inputs
id: inputs
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { createHash } = require('node:crypto')
const agent = require('./agent-runtime-lock.json')
const remote = require('./remote-runtime-lock.json')
const node = agent.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
const runtime = remote.runtimes?.opencode
const runtimeKey = process.env.AGENT_PLATFORM === 'linux'
? process.env.AGENT_ARCH : `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
const opencode = runtime?.targets?.[runtimeKey]
if (
!node ||
!opencode ||
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(node.archive) ||
!/^[a-f0-9]{64}$/u.test(node.sha256) ||
!/^[a-z0-9][a-z0-9._-]*$/u.test(opencode.package) ||
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(opencode.integrity)
) {
throw new Error('Locked native Agent package inputs are invalid')
}
const nodeUrl = new URL(node.archive, agent.node.source)
if (
nodeUrl.protocol !== 'https:' ||
nodeUrl.hostname !== 'nodejs.org' ||
nodeUrl.username ||
nodeUrl.password ||
nodeUrl.search ||
nodeUrl.hash
) {
throw new Error('Agent Node runtime must use the official HTTPS origin')
}
const opencodeArchive =
`${opencode.package}-${runtime.version}.tgz`
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`node-archive=${node.archive}`,
`node-sha256=${node.sha256}`,
`node-url=${nodeUrl.href}`,
`opencode-package=${opencode.package}`,
`opencode-version=${runtime.version}`,
`opencode-integrity=${opencode.integrity}`,
`opencode-archive=${opencodeArchive}`,
`cache-key=${createHash('sha256')
.update(`${node.sha256}:${opencode.integrity}`)
.digest('hex')}`,
''
].join('\n')
)
NODE
- name: Cache verified native inputs
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-agent-release-inputs
key: goodbuddy-agent-release-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.inputs.outputs.cache-key }}
- name: Acquire and verify native inputs
shell: bash
env:
NODE_ARCHIVE: ${{ steps.inputs.outputs.node-archive }}
NODE_SHA256: ${{ steps.inputs.outputs.node-sha256 }}
NODE_URL: ${{ steps.inputs.outputs.node-url }}
OPENCODE_ARCHIVE: ${{ steps.inputs.outputs.opencode-archive }}
OPENCODE_INTEGRITY: ${{ steps.inputs.outputs.opencode-integrity }}
OPENCODE_PACKAGE: ${{ steps.inputs.outputs.opencode-package }}
OPENCODE_VERSION: ${{ steps.inputs.outputs.opencode-version }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-agent-release-inputs"
mkdir -p "$directory"
node_archive="$directory/$NODE_ARCHIVE"
if [[ ! -f "$node_archive" ]]; then
curl --proto '=https' --tlsv1.2 \
--fail --silent --show-error --location \
--retry 3 --retry-all-errors \
--connect-timeout 15 --max-time 300 \
--max-filesize 104857600 \
"$NODE_URL" --output "$node_archive.partial"
mv "$node_archive.partial" "$node_archive"
fi
node - "$node_archive" "$NODE_SHA256" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
throw new Error('Node runtime archive integrity mismatch')
}
NODE
opencode_archive="$directory/$OPENCODE_ARCHIVE"
if [[ ! -f "$opencode_archive" ]]; then
npm pack \
--ignore-scripts \
--registry https://registry.npmjs.org/ \
--pack-destination "$directory" \
"$OPENCODE_PACKAGE@$OPENCODE_VERSION"
fi
node - "$opencode_archive" "$OPENCODE_INTEGRITY" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync, statSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
const size = statSync(archive).size
if (size <= 0 || size > 536870912) {
throw new Error('OpenCode Runtime archive exceeds its safety limit')
}
const actual = `sha512-${createHash('sha512')
.update(readFileSync(archive))
.digest('base64')}`
if (actual !== expected) {
throw new Error('OpenCode Runtime archive integrity mismatch')
}
NODE
- name: Preflight production GoodBuddy signing identity
shell: bash
env:
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
run: |
set -euo pipefail
node build/agent-bundle.cjs preflight
- name: Build production compound Agent package
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
GOODBUDDY_SIGNING_PRIVATE_KEY: ${{ secrets.GOODBUDDY_SIGNING_PRIVATE_KEY }}
NODE_ARCHIVE: ${{ steps.inputs.outputs.node-archive }}
OPENCODE_ARCHIVE: ${{ steps.inputs.outputs.opencode-archive }}
run: |
set -euo pipefail
output="$RUNNER_TEMP/goodbuddy-agent-output"
mkdir -p "$output"
continue_archive="$(node build/acquire-continue-runtime.cjs "$RUNNER_TEMP/goodbuddy-continue-runtime" "$AGENT_ARCH" "$AGENT_PLATFORM")"
node build/agent-package.cjs build \
--platform "$AGENT_PLATFORM" \
--arch "$AGENT_ARCH" \
--minimum-desktop-version "$(node -p "require('./package.json').version")" \
--node-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$NODE_ARCHIVE" \
--runtime-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$OPENCODE_ARCHIVE" \
--continue-archive "$continue_archive" \
--output "$output/goodbuddy-agent-$AGENT_VERSION-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" \
> "$output/result.json"
node - "$output/result.json" "$AGENT_ARCH" "$AGENT_VERSION" <<'NODE'
const { readFileSync } = require('node:fs')
const [resultPath, architecture, version] = process.argv.slice(2)
const result = JSON.parse(readFileSync(resultPath, 'utf8'))
if (
result.descriptor?.architecture !== architecture ||
result.descriptor?.platform !== process.env.AGENT_PLATFORM ||
result.descriptor?.version !== version ||
result.archive !==
`goodbuddy-agent-${version}-${process.env.AGENT_PLATFORM}-${architecture}.gbagent` ||
!/^[a-f0-9]{64}$/u.test(result.sha256)
) {
throw new Error('Production Agent package result is invalid')
}
NODE
- name: Upload verified compound package
uses: actions/upload-artifact@v7
with:
name: goodbuddy-agent-package-${{ matrix.platform }}-${{ matrix.arch }}
path: ${{ runner.temp }}/goodbuddy-agent-output/goodbuddy-agent-*.gbagent
if-no-files-found: error
compression-level: 0
retention-days: 7
catalog:
name: Sign cumulative Agent catalog
needs:
- authorize
- build
if: needs.authorize.outputs.authorized == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 10
environment:
name: agent-signing
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
- name: Install dependencies
run: npm ci
- name: Download compound packages
uses: actions/download-artifact@v8
with:
pattern: goodbuddy-agent-package-*
path: dist/agent-release
merge-multiple: true
- name: Fetch previous signed catalog when available
id: previous
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
run: |
set -euo pipefail
root="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases"
current_catalog_status="$(
curl --silent --show-error \
--output dist/agent-release/previous.json \
--write-out '%{http_code}' \
"$root/v$AGENT_VERSION/agent-catalog.json"
)"
current_signature_status="$(
curl --silent --show-error \
--output dist/agent-release/previous.sig \
--write-out '%{http_code}' \
"$root/v$AGENT_VERSION/agent-catalog.sig"
)"
if [[ "$current_catalog_status" == 200 && "$current_signature_status" == 200 ]]; then
echo "arguments=--previous-catalog dist/agent-release/previous.json --previous-signature dist/agent-release/previous.sig" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$current_catalog_status" != 404 || "$current_signature_status" != 404 ]]; then
echo "Current Agent catalog objects are inconsistent: $current_catalog_status / $current_signature_status" >&2
exit 1
fi
rm -f dist/agent-release/previous.json dist/agent-release/previous.sig
pointer_status="$(
curl --silent --show-error \
--output dist/agent-release/previous-pointer.json \
--write-out '%{http_code}' \
"$root/latest.json"
)"
if [[ "$pointer_status" == 200 ]]; then
readarray -t paths < <(
node - dist/agent-release/previous-pointer.json <<'NODE'
const { readFileSync } = require('node:fs')
const pointer = JSON.parse(readFileSync(process.argv[2], 'utf8'))
if (
pointer?.formatVersion !== 1 ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(pointer.version) ||
pointer.catalog !== `v${pointer.version}/agent-catalog.json` ||
pointer.signature !== `v${pointer.version}/agent-catalog.sig`
) {
throw new Error('Previous Agent catalog pointer is invalid')
}
process.stdout.write(`${pointer.catalog}\n${pointer.signature}\n`)
NODE
)
curl --fail --silent --show-error \
"$root/${paths[0]}" \
--output dist/agent-release/previous.json
curl --fail --silent --show-error \
"$root/${paths[1]}" \
--output dist/agent-release/previous.sig
echo "arguments=--previous-catalog dist/agent-release/previous.json --previous-signature dist/agent-release/previous.sig" >> "$GITHUB_OUTPUT"
elif [[ "$pointer_status" == 404 ]]; then
rm -f dist/agent-release/previous-pointer.json
echo "arguments=" >> "$GITHUB_OUTPUT"
else
echo "Agent catalog pointer request failed: $pointer_status" >&2
exit 1
fi
- name: Sign cumulative Agent catalog
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
PREVIOUS_ARGUMENTS: ${{ steps.previous.outputs.arguments }}
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
GOODBUDDY_SIGNING_PRIVATE_KEY: ${{ secrets.GOODBUDDY_SIGNING_PRIVATE_KEY }}
run: |
set -euo pipefail
generated_at="$(git show -s --format=%cI "$GITHUB_SHA")"
read -r -a previous <<< "$PREVIOUS_ARGUMENTS"
node build/agent-catalog.cjs create \
--x64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-x64.gbagent" \
--arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-arm64.gbagent" \
--darwin-arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-darwin-arm64.gbagent" \
"${previous[@]}" \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig \
--generated-at "$generated_at"
node build/agent-catalog.cjs verify \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig
- name: Upload signed release set
uses: actions/upload-artifact@v7
with:
name: goodbuddy-agent-release-${{ needs.authorize.outputs.version }}
path: |
dist/agent-release/goodbuddy-agent-*.gbagent
dist/agent-release/agent-catalog.json
dist/agent-release/agent-catalog.sig
if-no-files-found: error
compression-level: 0
retention-days: 7
publish:
name: Publish Agent to GitHub and OSS
needs:
- authorize
- catalog
if: needs.authorize.outputs.authorized == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 25
environment:
name: aliyun-oss-release
permissions:
contents: write
actions: read
id-token: write
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
- name: Install verification dependencies
run: npm ci
- name: Download signed release set
uses: actions/download-artifact@v8
with:
name: goodbuddy-agent-release-${{ needs.authorize.outputs.version }}
path: dist/agent-release
- name: Verify signed release set
run: |
node build/agent-catalog.cjs verify \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig
- name: Verify OSS release configuration
shell: bash
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OIDC_PROVIDER_ARN: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }}
ROLE_ARN: ${{ vars.ALIYUN_ROLE_ARN }}
run: |
set -euo pipefail
test "$OSS_BUCKET" = "goodbuddy"
test "$OSS_ENDPOINT" = "https://oss-cn-beijing.aliyuncs.com"
case "$OIDC_PROVIDER_ARN" in
acs:ram::*:oidc-provider/*) ;;
*) echo "OIDC Provider ARN 无效" >&2; exit 1 ;;
esac
case "$ROLE_ARN" in
acs:ram::*:role/*) ;;
*) echo "RAM Role ARN 无效" >&2; exit 1 ;;
esac
- name: Authenticate to Alibaba Cloud
uses: aliyun/configure-aliyun-credentials-action@v1
with:
role-to-assume: ${{ vars.ALIYUN_ROLE_ARN }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }}
role-session-name: goodbuddy-agent-${{ github.run_id }}
role-session-expiration: 3600
audience: sts.aliyuncs.com
- name: Install ossutil
shell: bash
run: |
set -euo pipefail
version="2.3.0"
archive="$RUNNER_TEMP/ossutil.zip"
directory="$RUNNER_TEMP/ossutil"
curl --fail --silent --show-error --location \
"https://gosspublic.alicdn.com/ossutil/v2/$version/ossutil-$version-linux-amd64.zip" \
--output "$archive"
mkdir "$directory"
unzip -q "$archive" -d "$directory"
binary="$(find "$directory" -type f -name ossutil -print -quit)"
test -n "$binary"
chmod +x "$binary"
echo "$(dirname "$binary")" >> "$GITHUB_PATH"
- name: Upload immutable Agent release to OSS
id: oss
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_UPLOAD_ENDPOINT: https://oss-accelerate.aliyuncs.com
run: |
set -euo pipefail
export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN"
endpoint_host="${OSS_ENDPOINT#https://}"
region="${endpoint_host#oss-}"
region="${region%.aliyuncs.com}"
test "$region" = "cn-beijing"
for file in dist/agent-release/*; do
name="$(basename "$file")"
ossutil cp "$file" \
"oss://${OSS_BUCKET}/agent-releases/v${AGENT_VERSION}/${name}" \
--endpoint "$OSS_UPLOAD_ENDPOINT" \
--region "$region" \
--update
done
echo "region=$region" >> "$GITHUB_OUTPUT"
- name: Verify public immutable OSS packages
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
run: |
set -euo pipefail
root="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/v$AGENT_VERSION"
for target in linux-x64 linux-arm64 darwin-arm64; do
name="goodbuddy-agent-$AGENT_VERSION-$target.gbagent"
curl --fail --silent --show-error --location \
"$root/$name" --output "$RUNNER_TEMP/$name"
cmp "dist/agent-release/$name" "$RUNNER_TEMP/$name"
done
for name in agent-catalog.json agent-catalog.sig; do
curl --fail --silent --show-error --location \
"$root/$name" --output "$RUNNER_TEMP/immutable-$name"
cmp "dist/agent-release/$name" \
"$RUNNER_TEMP/immutable-$name"
done
- name: Publish non-Latest GitHub Agent release
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="agent-v$AGENT_VERSION"
title="GoodBuddy Agent $AGENT_VERSION"
notes="resources/agent-release-notes/$AGENT_VERSION.md"
test -s "$notes"
if gh release view "$tag" >/dev/null 2>&1; then
is_draft="$(gh release view "$tag" --json isDraft --jq .isDraft)"
if [[ "$is_draft" == true ]]; then
gh release edit "$tag" \
--title "$title" \
--notes-file "$notes" \
--latest=false
gh release upload "$tag" dist/agent-release/* --clobber
gh release edit "$tag" --draft=false --latest=false
else
verify="$RUNNER_TEMP/github-agent-release"
mkdir "$verify"
gh release download "$tag" --dir "$verify"
for file in dist/agent-release/*; do
cmp "$file" "$verify/$(basename "$file")"
done
fi
else
gh release create "$tag" \
--draft \
--verify-tag \
--title "$title" \
--notes-file "$notes" \
--latest=false
gh release upload "$tag" dist/agent-release/* --clobber
gh release edit "$tag" --draft=false --latest=false
fi
test "$(gh api repos/mesalogo/goodbuddy/releases/latest --jq .tag_name)" != "$tag"
- name: Point OSS Agent catalog to published release
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_REGION: ${{ steps.oss.outputs.region }}
run: |
set -euo pipefail
export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN"
node - "$AGENT_VERSION" > dist/agent-release/latest.json <<'NODE'
const version = process.argv[2]
process.stdout.write(`${JSON.stringify({
formatVersion: 1,
version,
catalog: `v${version}/agent-catalog.json`,
signature: `v${version}/agent-catalog.sig`
}, null, 2)}\n`)
NODE
ossutil cp dist/agent-release/latest.json \
"oss://${OSS_BUCKET}/agent-releases/latest.json" \
--endpoint "$OSS_ENDPOINT" \
--region "$OSS_REGION" \
--force
- name: Verify public Agent catalogs
shell: bash
env:
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
run: |
set -euo pipefail
mirror="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases"
github="https://github.com/mesalogo/goodbuddy/releases/download/agent-v$AGENT_VERSION"
curl --fail --silent --show-error --location \
"$mirror/latest.json" --output "$RUNNER_TEMP/mirror-pointer.json"
curl --fail --silent --show-error --location \
"$mirror/v$AGENT_VERSION/agent-catalog.json" \
--output "$RUNNER_TEMP/mirror.json"
curl --fail --silent --show-error --location \
"$mirror/v$AGENT_VERSION/agent-catalog.sig" \
--output "$RUNNER_TEMP/mirror.sig"
curl --fail --silent --show-error --location \
"$github/agent-catalog.json" --output "$RUNNER_TEMP/github.json"
curl --fail --silent --show-error --location \
"$github/agent-catalog.sig" --output "$RUNNER_TEMP/github.sig"
cmp dist/agent-release/latest.json "$RUNNER_TEMP/mirror-pointer.json"
cmp dist/agent-release/agent-catalog.json "$RUNNER_TEMP/mirror.json"
cmp dist/agent-release/agent-catalog.sig "$RUNNER_TEMP/mirror.sig"
cmp dist/agent-release/agent-catalog.json "$RUNNER_TEMP/github.json"
cmp dist/agent-release/agent-catalog.sig "$RUNNER_TEMP/github.sig"
+355
View File
@@ -0,0 +1,355 @@
name: GoodBuddy Agents
on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/agents.yml'
- 'agent-runtime-lock.json'
- 'build/agent-bundle.cjs'
- 'build/agent-package.cjs'
- 'build/agent-ci-bundle.cjs'
- 'build/agent-build-target.cjs'
- 'build/binary-architecture.cjs'
- 'build/file-hash.cjs'
- 'build/remote-runtime-bundle.cjs'
- 'build/signing-key-preflight.cjs'
- 'package-lock.json'
- 'package.json'
- 'resources/agent-release-keys.json'
- 'remote-runtime-lock.json'
- 'src/agent-daemon/**'
- 'src/shared/**'
- 'tests/agent-bundle.test.ts'
- 'tests/agent-darwin-build.test.ts'
- 'tests/agent-package.test.ts'
- 'tests/agents-workflow.test.ts'
- 'tests/remote-runtime-bundle.test.ts'
push:
branches:
- main
paths:
- '.github/workflows/agents.yml'
- 'agent-runtime-lock.json'
- 'build/agent-bundle.cjs'
- 'build/agent-package.cjs'
- 'build/agent-ci-bundle.cjs'
- 'build/agent-build-target.cjs'
- 'build/binary-architecture.cjs'
- 'build/file-hash.cjs'
- 'build/remote-runtime-bundle.cjs'
- 'build/signing-key-preflight.cjs'
- 'package-lock.json'
- 'package.json'
- 'resources/agent-release-keys.json'
- 'remote-runtime-lock.json'
- 'src/agent-daemon/**'
- 'src/shared/**'
- 'tests/agent-bundle.test.ts'
- 'tests/agent-darwin-build.test.ts'
- 'tests/agent-package.test.ts'
- 'tests/agents-workflow.test.ts'
- 'tests/remote-runtime-bundle.test.ts'
permissions:
contents: read
concurrency:
group: agents-${{ github.ref }}
cancel-in-progress: true
jobs:
validate:
name: Validate Agent source
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Validate Agent source and workflow
run: |
npm test -- \
tests/agent-bundle.test.ts \
tests/agent-darwin-build.test.ts \
tests/agent-package.test.ts \
tests/remote-runtime-bundle.test.ts \
tests/agents-workflow.test.ts
npm run typecheck
build:
name: Build ${{ matrix.platform }} ${{ matrix.arch }} Agent
needs: validate
strategy:
fail-fast: false
matrix:
include:
- platform: linux
arch: x64
runner: ubuntu-24.04
- platform: linux
arch: arm64
runner: ubuntu-24.04-arm
- platform: darwin
arch: arm64
runner: macos-15
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.19.0
cache: npm
- name: Install dependencies
run: npm ci
- name: Verify native runner target
run: node -e "if (process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Agent CI requires the native target')"
- name: Resolve locked official Node runtime
id: runtime
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { lock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
)
const target = lock.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
if (
!target ||
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(target.archive) ||
!/^[a-f0-9]{64}$/u.test(target.sha256)
) {
throw new Error('Locked Agent Node runtime metadata is invalid')
}
const url = new URL(target.archive, lock.node.source)
if (
url.protocol !== 'https:' ||
url.hostname !== 'nodejs.org' ||
url.username ||
url.password ||
url.search ||
url.hash
) {
throw new Error('Agent Node runtime must use the official HTTPS origin')
}
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`archive=${target.archive}`,
`sha256=${target.sha256}`,
`url=${url.href}`,
''
].join('\n')
)
NODE
- name: Cache verified Node runtime input
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-agent-node
key: goodbuddy-agent-ci-node-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
- name: Acquire and verify locked Node runtime
shell: bash
env:
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
NODE_SHA256: ${{ steps.runtime.outputs.sha256 }}
NODE_URL: ${{ steps.runtime.outputs.url }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-agent-node"
archive="$directory/$NODE_ARCHIVE"
mkdir -p "$directory"
if [[ ! -f "$archive" ]]; then
rm -f "$archive.partial"
curl --proto '=https' --tlsv1.2 \
--fail --silent --show-error --location \
--retry 3 --retry-all-errors \
--connect-timeout 15 --max-time 300 \
--max-filesize 104857600 \
"$NODE_URL" --output "$archive.partial"
mv "$archive.partial" "$archive"
fi
node - "$archive" "$NODE_SHA256" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
throw new Error('Node runtime archive integrity mismatch')
}
NODE
- name: Resolve locked OpenCode Runtime
id: opencode
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { createHash: hash } = require('node:crypto')
const { runtimeLock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
)
const runtime = runtimeLock.runtimes?.opencode
const key = process.env.AGENT_PLATFORM === 'linux'
? process.env.AGENT_ARCH
: `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
const target = runtime?.targets?.[key]
if (
!runtime ||
!target ||
!/^[a-z0-9][a-z0-9._-]*$/u.test(target.package) ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(runtime.version) ||
!/^sha512-[A-Za-z0-9+/]+={0,2}$/u.test(target.integrity)
) {
throw new Error('Locked OpenCode Runtime metadata is invalid')
}
const archive = `${target.package}-${runtime.version}.tgz`
appendFileSync(
process.env.GITHUB_OUTPUT,
[
`package=${target.package}`,
`version=${runtime.version}`,
`integrity=${target.integrity}`,
`archive=${archive}`,
`cache-key=${hash('sha256').update(target.integrity).digest('hex')}`,
''
].join('\n')
)
NODE
- name: Cache verified OpenCode Runtime input
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-opencode-runtime
key: goodbuddy-agent-ci-opencode-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
- name: Acquire and verify locked OpenCode Runtime
shell: bash
env:
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
OPENCODE_INTEGRITY: ${{ steps.opencode.outputs.integrity }}
OPENCODE_PACKAGE: ${{ steps.opencode.outputs.package }}
OPENCODE_VERSION: ${{ steps.opencode.outputs.version }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-opencode-runtime"
archive="$directory/$OPENCODE_ARCHIVE"
mkdir -p "$directory"
if [[ ! -f "$archive" ]]; then
npm pack \
--ignore-scripts \
--registry https://registry.npmjs.org/ \
--pack-destination "$directory" \
"$OPENCODE_PACKAGE@$OPENCODE_VERSION"
fi
node - "$archive" "$OPENCODE_INTEGRITY" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync, statSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
const size = statSync(archive).size
if (size <= 0 || size > 536870912) {
throw new Error('OpenCode Runtime archive exceeds its safety limit')
}
const actual = `sha512-${createHash('sha512')
.update(readFileSync(archive))
.digest('base64')}`
if (actual !== expected) {
throw new Error('OpenCode Runtime archive integrity mismatch')
}
NODE
- name: Run native Agent regressions
if: matrix.platform == 'linux'
run: |
npm test -- \
src/agent-daemon/model-bridge.test.ts \
src/agent-daemon/lifecycle.test.ts
- name: Run native macOS process regressions
if: matrix.platform == 'darwin'
run: npm test -- src/agent-daemon/darwin-process-identity.test.ts
- name: Build and verify ephemeral signed compound Agent package twice
id: agent
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
run: |
set -euo pipefail
directory="$RUNNER_TEMP/goodbuddy-agent-ci"
rm -rf "$directory"
mkdir -p "$directory"
agent_version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
continue_archive="$(node build/acquire-continue-runtime.cjs "$RUNNER_TEMP/goodbuddy-continue-runtime" "$AGENT_ARCH" "$AGENT_PLATFORM")"
node build/agent-ci-bundle.cjs \
--platform "$AGENT_PLATFORM" \
--arch "$AGENT_ARCH" \
--node-archive "$RUNNER_TEMP/goodbuddy-agent-node/$NODE_ARCHIVE" \
--opencode-archive "$RUNNER_TEMP/goodbuddy-opencode-runtime/$OPENCODE_ARCHIVE" \
--continue-archive "$continue_archive" \
--output-directory "$directory/bundle" \
--archive "$directory/goodbuddy-agent-$agent_version-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" |
tee "$directory/result.json"
node - <<'NODE'
const { appendFileSync, readFileSync } = require('node:fs')
const result = JSON.parse(
readFileSync(
`${process.env.RUNNER_TEMP}/goodbuddy-agent-ci/result.json`,
'utf8'
)
)
if (
result.architecture !== process.env.AGENT_ARCH ||
result.platform !== process.env.AGENT_PLATFORM ||
!/^[a-f0-9]{64}$/u.test(result.archiveSha256)
) {
throw new Error('Agent CI result is invalid')
}
appendFileSync(
process.env.GITHUB_OUTPUT,
`sha256=${result.archiveSha256}\n`
)
NODE
- name: Record verified Agent build
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
AGENT_SHA256: ${{ steps.agent.outputs.sha256 }}
run: |
{
echo "### $AGENT_PLATFORM $AGENT_ARCH Agent build"
echo
echo "Native compound Agent, Node, and OpenCode Runtime build, test-signature verification, launch smoke, and deterministic archive comparison passed."
echo
echo "Archive SHA-256: \`$AGENT_SHA256\`"
echo
echo "The ephemeral CI key and bundle are not published."
echo "Native build checks do not replace real Host installation, Attach, and Ask/Execute acceptance."
} >> "$GITHUB_STEP_SUMMARY"
+343 -18
View File
@@ -19,26 +19,43 @@ jobs:
validate:
name: Validate source
runs-on: ubuntu-24.04
timeout-minutes: 20
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
cache: npm
- name: Verify release tag version
if: github.ref_type == 'tag'
run: node -e "const p=require('./package.json'); const expected='v'+p.version; if(process.env.GITHUB_REF_NAME!==expected){throw new Error('Expected tag '+expected+', received '+process.env.GITHUB_REF_NAME)}"
- name: Verify bilingual release notes
if: github.ref_type == 'tag'
run: npm run release:notes:verify
- name: Install dependencies
run: npm ci
- name: Prepare sandboxed Electron tests
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends --yes xvfb xauth
sandbox="node_modules/electron/dist/chrome-sandbox"
if [[ ! -f "$sandbox" ]]; then
rm -rf node_modules/electron/dist
force_no_cache=true node node_modules/electron/install.js
fi
test -f "$sandbox"
sudo chown root:root "$sandbox"
sudo chmod 4755 "$sandbox"
- name: Run validators
run: |
npm test
xvfb-run --auto-servernum npm test
npm run typecheck
npm run lint
@@ -46,7 +63,9 @@ jobs:
run: npm run build:bundle
- name: Upload production bundle
if: github.event_name == 'workflow_dispatch' || github.ref_type == 'tag'
if: >-
github.ref_type == 'tag' ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
uses: actions/upload-artifact@v7
with:
name: goodbuddy-production-bundle
@@ -54,10 +73,67 @@ jobs:
if-no-files-found: error
retention-days: 1
authorize-release:
name: Authorize desktop packaging
if: >-
github.ref_type == 'tag' ||
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
authorized: ${{ steps.policy.outputs.authorized }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Verify immutable release ref policy
id: policy
shell: bash
run: |
set -euo pipefail
git fetch --no-tags origin \
"+refs/heads/main:refs/remotes/origin/main"
git merge-base --is-ancestor \
"$GITHUB_SHA" refs/remotes/origin/main
case "$GITHUB_EVENT_NAME" in
push)
test "$GITHUB_REF_TYPE" = "tag"
test "$(node -p "String(require(process.env.GITHUB_EVENT_PATH).created)")" = "true"
test "$(node -p "String(require(process.env.GITHUB_EVENT_PATH).forced)")" = "false"
test "$(node -p "String(require(process.env.GITHUB_EVENT_PATH).deleted)")" = "false"
expected="v$(node -p "require('./package.json').version")"
test "$GITHUB_REF" = "refs/tags/$expected"
test "$GITHUB_REF_NAME" = "$expected"
git fetch --no-tags origin \
"+refs/tags/${GITHUB_REF_NAME}:refs/tags/${GITHUB_REF_NAME}"
test "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" = "tag"
test "$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" = "$GITHUB_SHA"
;;
workflow_dispatch)
test "$GITHUB_REF" = "refs/heads/main"
test "$GITHUB_REF_TYPE" = "branch"
test "$GITHUB_SHA" = "$(git rev-parse refs/remotes/origin/main)"
;;
*)
echo "Desktop packaging is limited to immutable version-tag pushes or a manual run of refs/heads/main." >&2
exit 1
;;
esac
echo "authorized=true" >> "$GITHUB_OUTPUT"
package:
name: ${{ matrix.platform }} ${{ matrix.arch }}
if: github.event_name == 'workflow_dispatch' || github.ref_type == 'tag'
needs: validate
if: >-
needs.authorize-release.result == 'success' &&
needs.authorize-release.outputs.authorized == 'true'
needs:
- validate
- authorize-release
strategy:
fail-fast: false
matrix:
@@ -67,7 +143,7 @@ jobs:
runner: windows-2025
- platform: windows
arch: arm64
runner: windows-2025
runner: windows-11-vs2026-arm
- platform: macos
arch: x64
runner: macos-15-intel
@@ -88,7 +164,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
cache: npm
- name: Cache packaging toolsets
@@ -111,7 +187,121 @@ jobs:
name: goodbuddy-production-bundle
path: out
- name: Build and verify release packages
- name: Verify native runner architecture
run: node -e "if(process.arch !== '${{ matrix.arch }}'){throw new Error('Expected ${{ matrix.arch }} runner, received '+process.arch)}"
- name: Install and validate managed Python
run: npm exec vitest run tests/local-tool-environment-live.test.ts
env:
GOODBUDDY_RUN_LIVE_TOOL_ENVIRONMENT: '1'
- name: Install RPM packaging tools
if: matrix.platform == 'linux'
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install --no-install-recommends --yes rpm
rpmbuild --version
- name: Resolve macOS signing mode
id: macos-signing
if: matrix.platform == 'macos'
shell: bash
env:
MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
set -euo pipefail
names=(
MACOS_CERTIFICATE_BASE64
MACOS_CERTIFICATE_PASSWORD
APPLE_API_KEY_BASE64
APPLE_API_KEY_ID
APPLE_API_ISSUER
)
configured=0
missing=()
for name in "${names[@]}"; do
if [[ -n "${!name}" ]]; then
configured=$((configured + 1))
else
missing+=("$name")
fi
done
if [[ "$configured" -eq 0 ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning title=Unsigned macOS packages::Apple signing credentials are not configured. The macOS DMG and ZIP will be unsigned and unnotarized."
{
echo "### macOS signing"
echo
echo "Apple signing credentials are not configured. This target produces unsigned and unnotarized packages that Gatekeeper may block on first launch."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [[ "$configured" -ne "${#names[@]}" ]]; then
missing_names="$(IFS=,; echo "${missing[*]}")"
echo "::error title=Incomplete macOS signing credentials::Missing: $missing_names"
exit 1
fi
echo "enabled=true" >> "$GITHUB_OUTPUT"
printf '%s' "$MACOS_CERTIFICATE_BASE64" | base64 -D > "$RUNNER_TEMP/goodbuddy-developer-id.p12"
printf '%s' "$APPLE_API_KEY_BASE64" | base64 -D > "$RUNNER_TEMP/AuthKey.p8"
test -s "$RUNNER_TEMP/goodbuddy-developer-id.p12"
test -s "$RUNNER_TEMP/AuthKey.p8"
chmod 600 "$RUNNER_TEMP/goodbuddy-developer-id.p12" "$RUNNER_TEMP/AuthKey.p8"
{
echo "### macOS signing"
echo
echo "Complete Apple signing credentials were detected. This target will be signed, notarized, and verified."
} >> "$GITHUB_STEP_SUMMARY"
- name: Build, sign and notarize macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'true'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
CSC_LINK: ${{ runner.temp }}/goodbuddy-developer-id.p12
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: 'true'
APPLE_API_KEY: ${{ runner.temp }}/AuthKey.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
- name: Build unsigned macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'false'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build --unsigned
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- name: Verify macOS signature and notarization ticket
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'true'
shell: bash
run: |
set -euo pipefail
dmg="$(find "dist/release/macos-${{ matrix.arch }}" -maxdepth 1 -type f -name '*.dmg' -print -quit)"
test -n "$dmg"
mount_point="$RUNNER_TEMP/goodbuddy-dmg"
mkdir "$mount_point"
cleanup() {
hdiutil detach "$mount_point" -quiet || true
}
trap cleanup EXIT
hdiutil attach "$dmg" -nobrowse -readonly -mountpoint "$mount_point" -quiet
app="$(find "$mount_point" -maxdepth 1 -type d -name '*.app' -print -quit)"
test -n "$app"
codesign --verify --deep --strict --verbose=2 "$app"
spctl --assess --type execute --verbose=4 "$app"
xcrun stapler validate "$app"
- name: Build and verify non-macOS release packages
if: matrix.platform != 'macos'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
@@ -127,14 +317,22 @@ jobs:
retention-days: 30
release:
name: Publish GitHub Release
if: github.event_name == 'push' && github.ref_type == 'tag'
needs: package
name: Publish GitHub and OSS release
if: >-
github.event_name == 'push' &&
github.ref_type == 'tag' &&
needs.authorize-release.outputs.authorized == 'true'
needs:
- authorize-release
- package
runs-on: ubuntu-24.04
timeout-minutes: 20
timeout-minutes: 35
environment:
name: aliyun-oss-release
permissions:
contents: write
actions: read
id-token: write
steps:
- uses: actions/checkout@v7
@@ -143,7 +341,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
- name: Verify release tag
shell: bash
@@ -153,6 +351,9 @@ jobs:
test "$GITHUB_REF_NAME" = "$expected"
test "$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" = "$GITHUB_SHA"
- name: Prepare bilingual release notes
run: node build/release-notes.cjs --output release-notes.md
- name: Download Windows packages
uses: actions/download-artifact@v8
with:
@@ -174,18 +375,142 @@ jobs:
- name: Verify and aggregate release assets
run: node build/aggregate-release.cjs --input dist/release-downloads --output dist/release-upload
- name: Create or update draft release
- name: Verify OSS release configuration
shell: bash
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OIDC_PROVIDER_ARN: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }}
ROLE_ARN: ${{ vars.ALIYUN_ROLE_ARN }}
run: |
set -euo pipefail
test -n "$OSS_BUCKET"
test -n "$OSS_ENDPOINT"
test -n "$OIDC_PROVIDER_ARN"
test -n "$ROLE_ARN"
case "$OSS_BUCKET" in
goodbuddy) ;;
*) echo "OSS Bucket 必须与应用内置镜像地址一致" >&2; exit 1 ;;
esac
case "$OSS_ENDPOINT" in
https://oss-cn-beijing.aliyuncs.com) ;;
*) echo "OSS Endpoint 必须与应用内置镜像地址一致" >&2; exit 1 ;;
esac
case "$OIDC_PROVIDER_ARN" in
acs:ram::*:oidc-provider/*) ;;
*) echo "OIDC Provider ARN 无效" >&2; exit 1 ;;
esac
case "$ROLE_ARN" in
acs:ram::*:role/*) ;;
*) echo "RAM Role ARN 无效" >&2; exit 1 ;;
esac
- name: Authenticate to Alibaba Cloud
uses: aliyun/configure-aliyun-credentials-action@v1
with:
role-to-assume: ${{ vars.ALIYUN_ROLE_ARN }}
oidc-provider-arn: ${{ vars.ALIYUN_OIDC_PROVIDER_ARN }}
role-session-name: goodbuddy-release-${{ github.run_id }}
role-session-expiration: 3600
audience: sts.aliyuncs.com
- name: Install ossutil
shell: bash
run: |
set -euo pipefail
version="2.3.0"
archive="$RUNNER_TEMP/ossutil.zip"
directory="$RUNNER_TEMP/ossutil"
curl --fail --silent --show-error --location \
"https://gosspublic.alicdn.com/ossutil/v2/$version/ossutil-$version-linux-amd64.zip" \
--output "$archive"
mkdir "$directory"
unzip -q "$archive" -d "$directory"
binary="$(find "$directory" -type f -name ossutil -print -quit)"
test -n "$binary"
chmod +x "$binary"
echo "$(dirname "$binary")" >> "$GITHUB_PATH"
- name: Prepare OSS website release index
id: oss-release
shell: bash
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
run: |
set -euo pipefail
endpoint_host="${OSS_ENDPOINT#https://}"
oss_region="${endpoint_host#oss-}"
oss_region="${oss_region%.aliyuncs.com}"
case "$oss_region" in
*[!a-z0-9-]*|'') echo "无法从 OSS Endpoint 推导 Region" >&2; exit 1 ;;
esac
base_url="https://${OSS_BUCKET}.${endpoint_host}/releases/${GITHUB_REF_NAME}/"
node build/create-site-release.cjs \
--manifest dist/release-upload/release-manifest.json \
--base-url "$base_url" \
--output dist/site-release.json
echo "base-url=$base_url" >> "$GITHUB_OUTPUT"
echo "region=$oss_region" >> "$GITHUB_OUTPUT"
- name: Upload immutable release assets to OSS
shell: bash
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_UPLOAD_ENDPOINT: https://oss-accelerate.aliyuncs.com
OSS_REGION: ${{ steps.oss-release.outputs.region }}
run: |
set -euo pipefail
export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN"
test -n "$OSS_ACCESS_KEY_ID"
test -n "$OSS_ACCESS_KEY_SECRET"
test -n "$OSS_SESSION_TOKEN"
test -n "$OSS_REGION"
for file in dist/release-upload/* dist/site-release.json; do
name="$(basename "$file")"
ossutil cp "$file" \
"oss://${OSS_BUCKET}/releases/${GITHUB_REF_NAME}/${name}" \
--endpoint "$OSS_UPLOAD_ENDPOINT" \
--region "$OSS_REGION" \
--update
done
- name: Verify public OSS release assets
run: node build/verify-site-release.cjs --manifest dist/site-release.json
- name: Create or update draft GitHub release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="$GITHUB_REF_NAME"
version="$(node -p "require('./package.json').version")"
if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" --draft
gh release edit "$tag" --draft --title "GoodBuddy $version" --notes-file release-notes.md
else
version="$(node -p "require('./package.json').version")"
gh release create "$tag" --draft --verify-tag --generate-notes --title "GoodBuddy $version"
gh release create "$tag" --draft --verify-tag --title "GoodBuddy $version" --notes-file release-notes.md
fi
gh release upload "$tag" dist/release-upload/* --clobber
gh release edit "$tag" --draft=false --latest
- name: Point website to verified OSS release
shell: bash
env:
OSS_BUCKET: ${{ vars.ALIYUN_OSS_BUCKET }}
OSS_ENDPOINT: ${{ vars.ALIYUN_OSS_ENDPOINT }}
OSS_REGION: ${{ steps.oss-release.outputs.region }}
run: |
set -euo pipefail
export OSS_ACCESS_KEY_ID="$ALIBABA_CLOUD_ACCESS_KEY_ID"
export OSS_ACCESS_KEY_SECRET="$ALIBABA_CLOUD_ACCESS_KEY_SECRET"
export OSS_SESSION_TOKEN="$ALIBABA_CLOUD_SECURITY_TOKEN"
test -n "$OSS_REGION"
ossutil cp dist/site-release.json \
"oss://${OSS_BUCKET}/releases/latest.json" \
--endpoint "$OSS_ENDPOINT" \
--region "$OSS_REGION" \
--force
+47
View File
@@ -0,0 +1,47 @@
name: Deploy website to GitHub Pages
on:
workflow_dispatch:
push:
branches:
- main
paths:
- 'sites/**'
- '.github/workflows/pages.yml'
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
deploy:
name: Deploy static website
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Validate website
run: |
node sites/scripts/validate.mjs
node --check sites/app.js
- name: Configure GitHub Pages
uses: actions/configure-pages@v5
- name: Upload website artifact
uses: actions/upload-pages-artifact@v4
with:
path: sites
- name: Deploy website
id: deployment
uses: actions/deploy-pages@v4
+6
View File
@@ -1,11 +1,17 @@
node_modules/
out/
dist/
# Local build output left by the extracted ShareServer project.
shareserver/build-server/
coverage/
.vite/
.runtime-resources/
.remote-runtime-resources/
.agent-resources/
*.log
*.tsbuildinfo
__pycache__/
*.py[cod]
.env
.env.*
!.env.example
+258
View File
@@ -0,0 +1,258 @@
--- a/src/main/assistant/assistant-database.test.ts
+++ b/src/main/assistant/assistant-database.test.ts
@@ -174,7 +174,10 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 35;`)
legacy.close()
try {
@@ -325,7 +328,10 @@
DROP VIEW supervision_review_current;
DROP TABLE supervision_review_navigation; DROP TABLE supervision_review_batches;
DROP TABLE supervision_review_sources; DROP TABLE supervision_review_runs;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 41;`)
legacy.close()
try {
@@ -1088,6 +1094,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 3;
`)
@@ -1251,6 +1260,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 26;
`)
@@ -1367,6 +1379,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 30;
`)
@@ -1445,6 +1460,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 30;
`)
@@ -1542,6 +1560,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1589,6 +1610,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1637,6 +1661,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1681,6 +1708,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1714,6 +1744,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 24;
`)
@@ -1744,6 +1777,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 5;
`)
@@ -1871,6 +1907,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 21;
COMMIT;
@@ -1967,6 +2006,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 9;
`)
@@ -2025,6 +2067,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 16`)
legacy.close()
@@ -2977,6 +3022,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 23`)
legacy.close()
@@ -3141,6 +3189,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 18;
`)
@@ -3654,6 +3705,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 31;
`)
--- a/src/main/assistant/activity-history-io.test.ts
+++ b/src/main/assistant/activity-history-io.test.ts
@@ -27,6 +27,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version=43`)
const legacySave = raw.prepare('UPDATE activity_history SET records_json = ?, legacy_history_may_be_incomplete = 1')
--- a/src/main/assistant/assistant-storage-upgrade.test.ts
+++ b/src/main/assistant/assistant-storage-upgrade.test.ts
@@ -101,7 +101,10 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 37`)
} finally { legacy.close() }
expect(hasPendingAssistantStorageUpgrade(path)).toBe(true)
@@ -419,7 +422,10 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
- DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
+ DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = ${sourceVersion}; BEGIN`)
const insert = legacy.prepare(
`INSERT INTO task_events(
--- a/src/main/assistant/heartbeat-database.test.ts
+++ b/src/main/assistant/heartbeat-database.test.ts
@@ -82,6 +82,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 2`)
raw.close()
@@ -141,6 +144,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 20`)
raw.close()
--- a/src/main/magic-notes/magic-note-storage.test.ts
+++ b/src/main/magic-notes/magic-note-storage.test.ts
@@ -223,6 +223,9 @@
ALTER TABLE activity_history RENAME COLUMN record_order_json TO records_json;
DROP TRIGGER messages_review_insert; DROP TRIGGER messages_review_update;
DROP TRIGGER messages_review_delete; DROP TRIGGER tasks_review_delete;
+ DROP VIEW IF EXISTS supervision_review_current;
+ DROP TABLE IF EXISTS supervision_review_navigation; DROP TABLE IF EXISTS supervision_review_batches;
+ DROP TABLE IF EXISTS supervision_review_sources; DROP TABLE IF EXISTS supervision_review_runs;
DROP TABLE review_checkpoints; ALTER TABLE messages DROP COLUMN review_revision;
PRAGMA user_version = 37`)
rmSync(join(directory, 'notes'), { recursive: true })
+5
View File
@@ -0,0 +1,5 @@
--- a/src/renderer/src/App.test.tsx
+++ b/src/renderer/src/App.test.tsx
@@ -13972 +13972 @@
- supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 30, executionSeconds: 300 }
+ supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 30, executionSeconds: 300, responseKiB: 1024 }
+297 -47
View File
@@ -4,39 +4,115 @@
These instructions apply to the entire repository.
GoodBuddy is a secure, cross-platform Electron desktop assistant. It uses
Electron, React, TypeScript, Vite, Vitest, and SQLite. User-facing copy is
primarily Simplified Chinese.
GoodBuddy is a cross-platform Electron desktop assistant for internal
organizational networks. It uses Electron, React, TypeScript, Vite, Vitest, and
SQLite. User-facing copy is primarily Simplified Chinese.
## Product Priorities
GoodBuddy is primarily an internal-network product. Unless the user states
otherwise, assume a trusted organizational environment, not a hostile public,
multi-tenant, or zero-trust deployment.
Use this order when requirements or implementation choices compete:
1. Complete, working product functionality.
2. Measured performance and responsiveness.
3. Clear, low-friction user experience.
4. Simple, conventional, maintainable implementation.
5. Security controls only for a concrete risk in the actual deployment model.
Functionality and performance are the primary acceptance criteria. Do not
disable, delay, restrict, or complicate a working product path for speculative
security hardening. Preserve the existing Electron, credential, user-data, and
release-signing boundaries, but do not expand them without a concrete current
risk or an explicit user requirement.
## Design Simplicity
Follow KISS and YAGNI. Use the smallest conventional design that completes the
real user workflow. NO over-engineering or over-design.
- A demonstrated failure authorizes only the smallest proportional fix, not a
general recovery, compatibility, or security framework.
- Prefer deleting bounded staging and retrying, idempotent operations, and
reconciliation from one authoritative source of truth. Do not persist copies
of state that can be derived or safely retried.
- Do not add durable snapshots, journals, versioned internal receipts, rollback
engines, compatibility readers, multi-phase state machines, protocol phases,
cryptographic identities, trust tiers, consent ceremonies, or quotas when
cleanup, retry, or reconciliation is safe.
- Before introducing any of that machinery, stop before coding. Explain the
reproducible current failure, why the simple alternative is insufficient,
and obtain the user's explicit approval for the added complexity. A reviewer
suggestion, test-only scenario, hypothetical crash, or possible future need
is not sufficient evidence.
- Performance outranks simplicity only for a measured bottleneck. Security
complexity requires a concrete threat at an actual trust boundary.
- Ignore hypothetical threats that require a hostile public network,
untrusted tenants, or zero-trust deployment unless that environment is an
explicit product requirement. Do not turn such findings into implementation
work.
- A complete workflow means the normal production path plus demonstrated
failure cases. It does not require exhaustive recovery from every
hypothetical interruption point.
- Test Hosts are test environments. Do not turn their workflows into production
compliance exercises.
When the user asks to implement a feature, deliver the complete usable user
workflow unless the user explicitly requests a prototype, scaffold, staged
landing, or partial implementation. Internal contracts, managers, protocols,
disabled UI, unavailable catalog entries, placeholders, and tests around
injected fakes do not count as completion when the real product path still
cannot be used. If required metadata, dependencies, or integration details are
missing, investigate and resolve them as part of the implementation. Do not
turn missing implementation work into a product "security gate", permanent
unavailable state, or acceptance criterion. If a genuine external blocker
cannot be resolved, stop and ask the user rather than presenting the partial
work as the implemented feature.
## Architecture
- `src/main`: privileged Electron main process, runtimes, persistence, IPC,
knowledge, automation, and OS integration.
- `src/preload`: the narrow, typed bridge exposed to the renderer.
- `src/renderer`: React UI. It must not receive secrets or direct Node access.
- `src/shared`: schemas, contracts, presets, and IPC channel definitions shared
across process boundaries.
- `resources/skills`: bundled skills.
- `build`: packaging scripts and icons.
- `out` and `dist`: generated output. Change source files instead.
Keep Electron security boundaries intact:
Keep these existing Electron security boundaries intact. They do not authorize
additional hardening beyond the actual internal-network threat model:
- Preserve context isolation and sandboxing.
- Never enable renderer Node integration.
- Validate IPC input with shared Zod schemas and verify trusted senders.
- Expose only explicit preload methods. Do not pass raw Electron APIs.
- Keep API keys in the main process and encrypted settings store.
- Never log or return credentials, authorization headers, private documents, or
unredacted provider payloads.
## Runtime Behavior
- Ask and Plan modes must remain read-only at the runtime boundary.
- Execute mode may use tools only through the existing approval controls.
- Ask mode must remain read-only at the runtime boundary.
- Execute mode is the user's full authorization for the selected SSH account.
It may use all tools, processes, network access, and writable paths available
to that account. Do not add T2/T3 trust tiers, separate consent checklists,
per-tool approvals, or a second "controlled execution" concept.
- Preserve cancellation, timeout, bounded-output, and shutdown behavior.
- Treat OpenCode and Continue as untrusted child runtimes. Preserve environment
allowlists, sandbox checks, and per-tool approval enforcement.
- When fixing Runtime behavior or limits, also inspect the remote Runtime path
through GoodBuddy Agent (`gbagent`). Verify whether local and remote execution
actually share the affected control logic; do not assume that a desktop fix
covers the Agent, bridge, launcher, or bundled Runtime. Fix any affected
remote-specific implementation and validate it under the GoodBuddy Agent
development validation rules below. If no separate remote fix is needed,
record which shared implementation covers it or why the remote path is
unaffected.
- Keep model/provider credentials in Main, but do not otherwise reduce Execute
permissions with extra product policy gates. Keep child-process cleanup
straightforward and reliable.
- On test Hosts, GoodBuddy may install and start its own test resources and
create dedicated work directories. Never delete or overwrite unrelated Host
files.
- A successful image-model configuration check does not prove generation works.
Only an actual generation request verifies the provider path.
- Do not fetch provider-returned image URLs. Accept and validate bounded inline
@@ -44,7 +120,16 @@ Keep Electron security boundaries intact:
## Data and Compatibility
- Preserve existing user data and migrations.
- Before changing released database schemas or large-data migrations, read
[`docs/development/database-migrations.md`](./docs/development/database-migrations.md).
Keep feature-specific storage rules and validation evidence in the owning
feature documents.
- Preserve released user-owned data, migrations required by released data, and
supported external contracts.
- Compatibility does not apply to unshipped, branch-local, or safely disposable
internal operation records. Replace or discard those records instead of
adding migrations, legacy readers, versioned receipts, or recovery machinery.
- Do not weaken SQLite transaction, lifecycle, deduplication, or cleanup logic.
- Treat user workspaces and untracked files as user-owned.
- Keep Windows, macOS, Linux x64, and Linux arm64 behavior in mind.
@@ -55,45 +140,161 @@ Keep Electron security boundaries intact:
- Follow surrounding TypeScript and React patterns.
- Reuse installed libraries and shared contracts before adding dependencies.
- Keep changes focused. Do not add unrelated refactors or documentation.
- Reviewer findings do not expand task scope. Implement a finding only when it
identifies a reachable current failure, a released compatibility contract,
or reproducible user-visible harm. Record speculative concerns without
building machinery around them.
- Validate the real end-to-end path, not only schemas, mocks, injected test
doubles, or isolated infrastructure. A feature is not complete until its
normal UI action reaches the production implementation and produces the
intended result.
- Security work must remain proportional to a concrete risk and must not
replace missing product functionality. Use established platform and
repository mechanisms instead of inventing extra gates, ceremonies, or
disabled states.
- Add or update focused tests for behavioral changes and regressions.
- Read [`docs/quality/recurring-defects.md`](./docs/quality/recurring-defects.md)
before changing overlays, store lookups, retrieval entry points, cancellation
behavior, or render-time derived values. It records defect types that have
already recurred in this repository and the shared mechanism that covers each
one. When fixing an instance of a listed type, extend that shared mechanism
instead of patching the one reported case, and add an entry when a new defect
type recurs.
- After completing any functional change, inspect the affected product,
architecture, design, feature, setup, and operational documentation and
update every relevant document to match the implemented behavior. Treat the
final code and validated runtime behavior as the source of truth: correct
stale documentation rather than preserving outdated intent. Avoid
documentation churn only when the change has no documented impact.
- Avoid broad catches that erase HTTP status, cancellation, or provider error
context. Bound and redact any surfaced error details.
context.
- Keep UI accessible with labels, keyboard behavior, semantic roles, and visible
focus states.
## Release Packaging
## Product Design Documentation
- `.github/workflows/packages.yml` is the canonical cross-platform packaging
workflow. It validates and builds `out` once, then packages on six native
runners: Windows, macOS, and Linux, each for x64 and arm64.
- Run the unified packager with
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
`dist/release/<platform>-<arch>`.
- Default deliverables are NSIS and portable ZIP for Windows, DMG and ZIP for
macOS, and AppImage and DEB for Linux. Every target includes
`release-manifest.json` with SHA-256 hashes.
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
bundled Continue and OpenCode runtimes, executable architecture, and package
signatures before atomically replacing a release directory.
- Keep electron-builder invocations on `--publish never`. Main-branch builds
run validation and build the production bundle without running the native
package matrix. Manual builds upload 30-day GitHub Actions artifacts.
Version-tag builds verify and aggregate packages before publishing GitHub
Release assets. Signing and macOS notarization are not configured.
- Keep `ELECTRON_CACHE` and `ELECTRON_BUILDER_CACHE` under
`${{ runner.temp }}` in step-level workflow contexts. A cache beneath the
repository inherits the root `"type": "module"` and breaks electron-builder's
CommonJS macOS icon tool.
- Tag builds must use `v${package.version}`. The workflow also supports manual
dispatch and main-branch changes to release tooling.
- Verified baseline on 2026-08-04: commit `2f54938`, GitHub Actions run
`30893805567` succeeded for validation and all six package targets, producing
six release artifacts plus the shared production bundle.
- Organize feature documentation by product feature under
`docs/features/<feature-name>/`, not by document type.
- Treat each feature directory's `README.md` as its documentation entry point.
- Follow `docs/features/AGENTS.md` for required document responsibilities,
naming, traceability, progress evidence, and migration rules.
- Keep cross-feature architecture, quality protocols, development procedures,
and product-wide roadmaps outside feature directories only when they cannot
be owned by one feature.
- Maintain one authoritative definition for each requirement, state rule, UI
behavior, technical contract, and implementation fact. Link to it instead of
duplicating it across documents.
- Update the affected feature documents after functional changes. The
implemented and validated production behavior is the final source of truth.
## UI Consistency
- Treat `UI-DESIGN.md` as the canonical UI design system. Read and follow it
before changing renderer layout, shared controls, interaction feedback,
themes, responsive behavior, or accessibility semantics.
- Reuse the shared `PageTabs` and `SegmentedControl` primitives instead of
creating page-specific tab or toggle styles. A semantic tab set may use the
shared segmented visual variant, but it must retain `tablist`, `tab`,
`tabpanel`, `aria-selected`, roving focus, and arrow-key behavior.
- Use the shared sliding Switch pattern for persistent binary states and expose
`role="switch"` even when it is implemented with a checkbox input. Keep
Checkbox visuals and semantics for multi-select, assignment, and explicit
confirmation. Do not create page-specific Switch styling.
- Use the bundled `Inter Variable` and `Noto Sans SC Variable` UI fonts through
the shared typography tokens. Do not add remote font requests or page-local
font stacks. Keep redistributed font licenses in packaged resources and
retain system fallbacks for startup and unsupported glyphs.
- Route transient success and informational feedback, plus asynchronous errors
that are not tied to one field, through the application notification
viewport. Do not render page-local copies of the same notification pattern.
- Keep inline feedback only when it must remain attached to its context, such
as field validation, destructive confirmation, operation progress, a
blocking page state, or an error with an immediate local recovery action.
- Do not show the same event both inline and as an application notification.
Preserve user input and actionable error context when an operation fails.
## Commit Messages and Release Notes
Release notes are derived in part from commit history, so commits for
user-visible changes must record product intent rather than only the
implementation mechanism.
- Classify the commit by the user-visible behavior. Use `feat` only for a
capability users did not previously have. Use `fix` when restoring intended
behavior, removing inconsistency, or making two existing entry points reflect
the same underlying setting, even if the implementation adds new
synchronization logic.
- Keep the subject concise, then add a commit body for non-trivial user-visible
changes. State the previous user-facing problem, the resulting behavior, and
the affected surface or workflow. Include permissions, migration,
compatibility, cost, data, preview-status, or other usage caveats when
relevant.
- Describe the user outcome precisely. Do not promote an internal refactor,
synchronization mechanism, schema change, or newly added implementation code
to a product feature unless it creates a genuinely new user capability.
- When a change is release-note worthy, include a short `Release note:` line in
the commit body written in user-facing language. Prefer a concrete usage
scenario and benefit over technical implementation terminology.
- Treat commit messages as evidence, not as the sole source of truth. Before
drafting release notes, verify the diff and resulting behavior, correct any
inaccurate `feat` or `fix` classification, and include actionable usage
notes where the change affects defaults, synchronized settings, permissions,
resource usage, compatibility, or user data.
Example:
```text
fix: unify project settings across channel entry points
The top-left project settings and the project settings shown under messaging
channels could present or save inconsistent values. They now edit the same
project configuration for the project name, description, Runtime, and work
mode.
Release note: 修复左上角项目设置与消息通道项目设置不一致的问题;现在从任一入口修改后,另一处会同步显示相同配置。
```
## Releases
Detailed release operations are governed by
[`docs/development/release-runbook.md`](./docs/development/release-runbook.md).
Read that runbook only when preparing, publishing, recovering, or verifying a
desktop or GoodBuddy Agent release. It is an operator and CI procedure, not an
authorization to add product persistence or recovery machinery.
Repository-wide release invariants:
- Write each release-note feature item as two or three clear sentences that
identify the user situation, the available action, and the resulting
behavior or important boundary. Do not impose an arbitrary per-item
character limit; use the detail needed to make the user scenario clear.
- Before approving the exact release commit and bilingual release notes,
inspect the English `FEATURES.md` and Simplified Chinese
`FEATURES.zh-CN.md` against the verified changes since the previous stable
release. Keep both versions aligned, and update their provided capabilities,
roadmap status, current versions, limitations, and remaining release
acceptance work as needed. Include any required update in the release
candidate; if no change is needed, explicitly confirm that both feature lists
were reviewed and remain accurate.
- Never create or push a release tag before the user approves the exact release
commit and bilingual release notes.
- Never move, delete, recreate, or reuse a release tag.
- Keep desktop and Agent release paths, tags, artifacts, and Latest status
separate.
- GitHub Actions is the release build and cross-platform packaging authority.
During release preparation, run local tests, type checks, and lint, but do
not run `npm run build`, `npm run build:bundle`, local packaging, or
packaged-app probes. Push the approved candidate commit first and require its
main-branch CI validation and production build to pass before tagging it,
unless the user explicitly requests an additional local build.
- LoongArch is a separate experimental preview. Build or publish it only when
the user explicitly requests it for that release.
- Never commit or publish credentials. Production publication uses the existing
protected environments and short-lived credentials described in the runbook.
## Validation
Run all validators after source changes:
Run validators after source changes when function/fix done, not on every source change:
```text
npm test
@@ -101,15 +302,64 @@ npm run typecheck
npm run lint
```
Run `npm run build` for production build changes. Use `npm run portable` only
when a current Windows portable package is requested. Gated runtime tests may
make paid or external calls, so run them only with explicit authorization.
Run `npm run build` for production build changes during development, but not
solely as a release-preparation step; release candidates use the main-branch CI
production build before tagging. Use `npm run portable` only when a current
Windows portable package is requested. The user has granted standing
authorization in this repository for bounded real text-model calls needed to
validate changed product paths; do not ask again for each such call. Keep each
validation call minimal, disable tools and attachments where possible, and
report the exact call count. This standing authorization does not cover bulk,
high-cost, destructive, publishing, messaging, purchasing, or other
consequential external actions.
### GoodBuddy Agent development validation
Every source change that can alter the deployed GoodBuddy Agent or the
desktop-to-Agent production path must be validated on the shared Linux x64 test
Host while the change is being developed. This includes Agent daemon and
protocol changes, packaging and installation, attach/update behavior, Runtime
launch and process ownership, workspaces, model bridges, and lifecycle or
recovery behavior. Unit tests, mocks, fixtures, CI, and waiting for release
validation are not substitutes for this development-time check.
- Reach the shared Host at `192.168.0.23` on the LAN when available, otherwise
use `10.7.0.23` over VPN. These are two routes to the same physical machine,
not two independent test targets or architecture results.
- Exercise the current source or package under development, not only the
previously published Agent. Run the affected production path as soon as it is
runnable and repeat the relevant scenario after the final change.
- Select the real-Host scenario from the changed surface: attach/bootstrap or
update for installation changes; Ask/Execute and a bounded real model request
for Runtime or model-bridge changes; and the applicable disconnect, restart,
cancellation, or reconnection sequence for lifecycle and recovery changes.
- Use the existing pinned Host identity and GoodBuddy credential storage. Never
add credentials to source, documentation, commands, logs, or test output, and
never hard-code these test addresses into product behavior or defaults.
- Keep all artifacts in GoodBuddy-owned test directories. Before stopping or
changing a remote process, verify that it belongs to the test run; never
modify unrelated Host files or processes.
- During remote testing, prefer dedicated test directories under `/root/tmp`
for temporary files. Avoid creating files under `/tmp` where possible, since
it may be memory-backed and consume additional RAM. Set `TMPDIR` to the
dedicated test directory when needed so test tools use it as well.
- If neither route is reachable, report the real-Host validation as blocked and
do not present the Agent work as complete or defer its first real validation
to release time.
The detailed scenario rules are documented in
[`docs/features/remote-host/technical-design.md`](./docs/features/remote-host/technical-design.md).
Before committing or pushing, inspect `git status`, `git diff`, and
`git diff --cached`. Do not commit secrets, local databases, logs, generated
credentials, or private user artifacts.
Before a push that updates the `github` remote, ask whether the user wants a
release tag unless they already specified that choice. A branch-only push does
not require a version bump or tag.
This repository has two synchronized remotes, `origin` and `github`. Unless the
user explicitly names a remote, every requested push must update the current
branch on both remotes, plus any tags explicitly included in the request.
Verify both remote refs after pushing.
branch on both remotes. When the user requests a release tag, push the new tag
to every remote receiving the branch update. Verify all updated branch refs and
any applicable tag refs after pushing.
+445 -13
View File
@@ -31,6 +31,10 @@ npm start
## 质量验证
跨功能测试方法见[质量验证索引](./docs/quality/README.md)。桌面界面的测试分层、
原生键盘、截图与计算样式取证、隔离启动和脱敏报告遵循
[Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md)。
提交或打包前运行:
```bash
@@ -39,6 +43,31 @@ npm run typecheck
npm run lint
```
Vitest runs two ordered projects from `vitest.config.ts`. The `unit` project runs
first with up to four workers, bounded by Node's available CPU parallelism. Once
it finishes, the `integration` project runs with one worker. The serial group
contains all test files under `tests/` (including `*.electron.test.ts`),
`src/main/agent/**/*runtime*.test.ts`, and
`src/agent-daemon/private-endpoint.test.ts`. Other tests remain in the unit group.
Both projects inherit the React plugin, jsdom environment, renderer setup, and
existing exclusions; test-file isolation remains enabled.
This preserves serialization for the process/socket tests that previously timed
out on Linux with two workers (commit `93721d6`). `sequence.groupOrder` keeps the
two phases from overlapping. Existing file filters still work, for example
`npm test -- src/main/agent/opencode-runtime.test.ts`. To select a whole phase,
use `npm test -- --project unit` or `npm test -- --project integration`. To inspect
file selection without running tests, use `npx vitest list --filesOnly`.
For activity-history persistence, run
`npx vitest run src/main/assistant/activity-history-io.test.ts`. This checks both
WAL growth and repeated full-history reads: consecutive saves compare against
the last committed in-memory records, invalidated on close, data clearing or
another SQLite connection's commit. It also checks rollback and retry. The cache
does not change the database schema or the full-snapshot IPC contract. WAL size
and SQL scan counts do not measure physical disk throughput or conversation
message persistence; validate those separately when investigating live I/O.
监听模式:
```bash
@@ -51,7 +80,18 @@ npm run test:watch
GOODBUDDY_RUN_RUNTIME_E2E=1 npm test -- src/main/agent/runtime-e2e.manual.test.ts
```
该测试可能发起真实外部模型调用。测试不会输出 API Key,文件操作在临时工作区中执行。
OpenCode/Continue 用例默认读取 `dist/harness-package-probe/win-unpacked`;也可用
`GOODBUDDY_E2E_PACKAGED_ROOT` 指定其他已解包应用目录。该测试可能发起真实外部模型
调用。测试不会输出 API Key,文件操作在临时工作区中执行。文件包含经 Main 回环
broker 调用已分配自定义 MCP 的真实 OpenCode 和 Continue 用例。
不使用外部模型的 Runtime 原生交互回归位于
`src/main/agent/opencode-runtime-permissions.test.ts`、
`src/main/agent/continue-runtime-permissions.test.ts` 和
`src/main/agent/deepseek-harness-acp-e2e.test.ts`。它们用本地确定性模型驱动真实 Runtime
或固定 Host,覆盖工作区外访问、原生问答及 Ask 边界。OpenCode 用例默认使用开发缓存
二进制;可通过 `GOODBUDDY_TEST_OPENCODE_BINARY` 指定已经安装的候选二进制,避免把
仍在运行的旧缓存版本当作候选版本验证。这不替代当前源码 Agent 的 Linux Host 验收。
## 生产构建
@@ -63,6 +103,19 @@ npm run build
中间构建输出位于 `out`。该目录为生成内容,应修改源文件后重新构建,不要直接编辑。
## 图标生成
应用图标源文件位于 `icons`。修改源图或图标处理逻辑后运行:
```bash
npm run icons
```
脚本会精确裁出亮色和深色圆角卡片,清理圆角外侧背景并使用适合主题的
边缘颜色生成透明抗锯齿,避免缩放后出现白边。它会统一更新 `build` 中的
PNG / ICO、Renderer 图标,以及官网使用的亮色和深色品牌图标。任务栏和
托盘图标保持透明背景。
## 平台打包
### 当前平台默认包
@@ -111,26 +164,258 @@ npm run dist:linux:arm64
每个 Linux 架构生成:
- `deb`:适用于麒麟、统信 UOS 等 Debian 系桌面。
- `rpm`:适用于 Fedora、RHEL、Rocky Linux、openEuler 等 RPM 系桌面。
- AppImage:适用于免安装验证和便携运行。
打包产物位于 `dist`。
所有桌面打包入口通过同一个 `electron-builder` 启动脚本运行。正常日志会隐藏
`duplicate dependency references` 信息摘要,因为它只表示同一已解析依赖被多条
生产依赖路径共享,并不要求构建操作者处理;调试日志仍会保留该摘要。Renderer 中
已由 Vite 打包的 `@antv/g6` 归类为构建期依赖,不会再作为独立 Node 模块复制进桌面
安装包。
以上 `dist*` 与 `portable` 命令用于普通本地桌面打包。它们会携带
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
`agent-release-keys.json`,但所有桌面构建路径(包括本地 `portable`)都不会嵌入
可安装的远端 Agent 或远端 Runtime payload。没有使用托管 SSH 时,无需下载任何额外
组件,缺少远端包也不能阻塞普通桌面打包或发布。
“设置 > 平台功能 > 远程项目”列出用户数据目录中已经下载或导入的
Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
小型签名目录,显示本地版本、在线最新兼容版本以及 `有更新 / 已是最新 / 未下载 /
校验失败`;此检查不会下载 `.gbagent`。只有用户点击“下载”或“更新到”后才传输包,
用户也可导入或导出 `.gbagent` 离线包。
Main 在写入缓存前校验签名目录、目录签名、大小、SHA-256、桌面最低版本、Agent 协议、
内部 Agent/Runtime 签名和完整 payload。缓存位于 Electron `userData` 下的
`remote-components/agent-packages`,不暴露给 Renderer。
公钥 registry 的信任来自严格 schema、唯一 key ID、规范 Base64、有效 Ed25519 key、
production/test 环境和撤销列表,不来自 JSON 空白或换行。读取桌面资源、复合包和 Host
已有 registry 时接受等价 UTF-8 JSON 格式;向 Host 写入前统一重新序列化为确定性 LF
JSON。签名目录、包描述符、manifest、签名和 payload 的原始字节校验保持不变,包下载
继续流式计算 SHA-256。
“SSH 主机 > 远程运行环境”只有一个按版本事实显示“安装远程环境”“更新远程环境”或
“重新安装”的主按钮。次级 SegmentedControl 选择默认且不持久化的“自动”、“Host 下载”
或“GoodBuddy 传输”:
- “Host 下载”使用签名目录中的固定 GitHub/北京镜像 URL、大小、SHA-256、
有界重定向链和固定 SSH prepare channel。GoodBuddy 桌面控制面通过该通道发送有界
one-shot installer;Host 校验目录绑定的归档大小和 SHA-256 后,由包内固定 Node 解码
并运行 installer。
- 显式“GoodBuddy 传输”在在线目录不可用,或本地已验证包版本不低于在线候选时优先使用
本地包,包括版本更高的离线导入;线上存在更新且可用时仍取得线上新包。需要下载时,
同一次用户操作会校验 SHA-256 与签名、缓存并取得 lease,再以有界流式 SFTP 上传一个
compound archive 和归档内已验证的 bootstrap Node;约 294 MiB 的整包不会一次读入
Main `Buffer`,Host 仍再次校验。
- “自动”只在 operation/prepare 前执行 Host capability probe;直连明确可用才选择
Host 下载,否则选择 GoodBuddy 传输。显式选择保持有效,prepare、commit 或 adoption
失败后不跨 acquisition 自动 fallback。
两种 acquisition 都把同一签名 compound `.gbagent` 交付到固定 operation staging,随后
共用 control-plane `prepare → commit → Agent activate/health → Runtime activate →
finalize → cleanup`。Main 只持久化清理 operation staging 所需的 operation ID,不保存
Agent/Runtime metadata 副本。操作中断或 adoption 失败后,下一次更新尽力清理旧暂存并
重新 prepare,不恢复或续写旧操作。确认 adoption 后显式 cleanup。Host 在 prepare
解包时完成一次完整 payload 校验;commit 只复核签名 metadata
并原子发布,Agent/Runtime 注册后的 health、capabilities 和 prompt 启动不再遍历或哈希
完整 payload。cleanup 失败不回滚已健康环境,也不阻塞下一次 fresh prepare。
进入“主机与远程执行”设置页只读取本地 Host 列表,不逐台连接。用户点击某台 Host 的
“刷新版本”后才执行远端版本和直连准备能力探测。项目切换只更新本地选择,不建立 SSH
连接;首次实际使用远程 Workspace 或 Runtime 时才建立连接。当前进程会复用该 Host 已确认
的 Agent/Runtime identity 和 Agent 连接,Host 编辑或环境更新会定向清空它们。
直连安装本身只依赖 SSH,不依赖已有 Agent。显式选择 Host 下载时由实际安装流程重新探测
并报告工具、权限、空间或来源不可达等原因;本次操作不会自动改用 GoodBuddy 传输。
新建远程项目弹窗也只读本地 Host 验证记录;目录浏览或保存才连接所选 Host。Host 卡片
在版本相同时仍允许显式重装;损坏的 GoodBuddy-owned 同 digest 目录会先隔离、替换并在
失败时恢复。直连 bootstrap 使用固定短命令 `exec sh -s`,脚本和有界输入经 stdin 发送,
避免 SSH command 长度限制。
新增 Host 只保存并探测,不提供自动安装开关;只有用户明确点击主按钮后才传输完整包。现有
package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host 直连;归档无需携带
`agent/lib/package-installer.cjs`,签名目录不提供也不检查额外的 bootstrap 能力元数据。
首次 bootstrap 不依赖已安装 Agent daemon。目标契约见
[SSH Host 远程环境准备与直连下载设计](./docs/features/remote-host/environment-provisioning-technical-design.md)。
删除 Host 或远程项目属于纯本地管理操作。Host 确认框列出全部关联项目记录并级联清理
本地数据库,但不连接 Host、不检查远端目录,也不删除远端文件;Host 设置文件和项目事务
的普通失败会回滚已写入的一侧。
## Runtime 资源
发布包会携带经过版本与完整性校验的 OpenCode 和 Continue Runtime:
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
- DSH 插件安装使用精确锁定并从 `app.asar` 解包的 npm CLI,通过当前 Electron 的 Node 模式运行;最终用户不需要另装 Node.js 或 npm。
- DSH 图片输入使用精确锁定的 `@napi-rs/canvas` 完整解码 JPEG/PNG。通用包与目标平台、目标架构的 Skia 原生包必须从 `app.asar` 解包;当打包 Runner 的架构与目标架构不同时,发布脚本会根据 lockfile 的精确版本、下载地址和 integrity 临时暂存目标原生包,完成后清理。发布校验会检查版本、目标架构和 MIT 许可证。
- 打包钩子位于 `build/runtime-hooks.cjs`。
跨架构打包前,确认目标架构的 OpenCode 资源已经准备完成。不要用其他架构的二进制替代目标资源。
GoodBuddy 管理的本机 OpenCode 启动后会轮询已认证的 loopback health 接口,最多等待
30 秒。该窗口用于兼容 Linux ARM 等较慢环境;不要改回依赖 stdout 文案或 10 秒固定上限。
聊天中的 Runtime 切换只执行路径和模型凭据就绪检查,不为状态显示启动临时 Server。
设置中的显式连接测试、原生能力清单读取和首次实际请求仍会启动 OpenCode,并继续使用
上述完整健康检查。
## GoodBuddy Agent 工件
面向用户发布的是按远端 Host 架构区分的复合 `.gbagent`,每个包同时包含签名
Agent、固定 Node 和当前桌面源码维护并适配的签名 OpenCode Runtime。Runtime 不建立
独立的用户版本流。远程直连所需的有界 one-shot installer 由桌面控制面打包并通过固定
SSH prepare channel 发送,不是 `.gbagent` 的必需条目。Host 按签名目录校验完整归档
大小和 SHA-256 后,包内固定 Node 解码并运行 installer。两种 acquisition 都在固定
operation staging 中执行同一 prepare/commit;本地 pending record 只保存清理暂存所需的
operation ID。失败或中断后的下一次更新尽力清理旧暂存并重新 prepare,不保存或恢复
Agent/Runtime metadata 副本。生产构建命令为:
```bash
node build/agent-package.cjs build \
--arch <x64|arm64> \
--minimum-desktop-version <desktop-version> \
--node-archive <locked-node-runtime.tar.gz> \
--runtime-archive <locked-opencode-package.tgz> \
--output goodbuddy-agent-<agent-version>-linux-<arch>.gbagent
```
底层 `agent:build/import/verify` 与 `remote-runtime:build/import/verify` 命令继续用于
开发、签名和复合包组装验证;其 `.agent-resources` 与
`.remote-runtime-resources` 输出都是不提交的生成内容,也不会被任何桌面打包命令发现
或嵌入。
### Agent 原生 CI 策略
Agent 源码、共享协议、lock、bundle 工具和测试与桌面应用保持在同一仓库、同一 commit;
不维护长期分叉的 Agent 源码分支。`.github/workflows/agents.yml` 在相关 Pull Request、
`main` push 和手工触发时,分别使用 `ubuntu-24.04` 与 `ubuntu-24.04-arm` 原生构建
Linux x64/arm64 复合包,并使用 `macos-15` 原生构建 Darwin arm64
复合包。所有 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
地址与 digest,并从 `remote-runtime-lock.json` 解析固定 OpenCode npm 包与 integrity;
下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层
测试签名验证、Agent CLI 加载 smoke 和确定性 `.gbagent` 对比。所有目标还验证
Koffi 原生模块加载与 OpenCode `--version`;Linux job 继续运行 Agent 原生回归。
Darwin arm64 的 Node 和 OpenCode 固定输入分别由同一份
`agent-runtime-lock.json` 与 `remote-runtime-lock.json` 提供,不再维护 CI 专用副本。
组包器区分 ELF 与 Mach-O,Darwin CI 产物命名为
`goodbuddy-agent-<version>-darwin-arm64.gbagent`,不构建 Intel Mac 目标。
macOS Host 继续通过 SSH 按需启动 detached Agent,不注册 launchd 或开机服务。
进程身份使用 macOS 原生接口,Unix Socket 使用 `getpeereid`;Host 下载兼容系统自带
`shasum -a 256`,不要求安装 GNU coreutils。真实 Host 验证记录见
[远程主机技术设计](./docs/features/remote-host/technical-design.md)。
三个目标共用 `agent-catalog.json` 和现有签名格式,条目按平台与架构区分。旧版 Desktop
只接受 Linux catalog 条目;首次发布包含 Darwin 的累计 catalog 前必须先发布新版
Desktop,并在发布说明中要求用户升级 Desktop。旧版的在线 Agent 目录检查会失败,
本地已验证包不被删除。不得为此改写历史包或复用发布标签。
该验证 workflow 不读取 production signing secret,不修改公开 key registry,也不上传
可安装 Agent 工件。`.github/workflows/agent-release.yml` 是唯一 production Agent
发布路径:只接受指向受保护 `main` 历史的 annotated
`agent-v${agent-runtime-lock.agentVersion}` 标签,在 `agent-signing` Environment 中
原生构建 Linux x64、Linux arm64、macOS arm64 三个目标,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
与北京 OSS。该 Environment 只保护一组 GoodBuddy 通用发布变量与 Secret:
- `GOODBUDDY_SIGNING_KEY_ID` /
`GOODBUDDY_SIGNING_PRIVATE_KEY`
该 GoodBuddy 身份统一签署内部 Agent、固定 Runtime manifest、外层 `.gbagent`
描述符和累计目录,各层通过既有签名域区分用途,不再要求内部 Runtime 单独配置生产
身份。私钥只注入签名步骤;预检只读取 key ID 并确认公钥已在
`resources/agent-release-keys.json` 注册为 production 且未撤销。
累计目录使用 `agent-catalog.json` 与 `agent-catalog.sig`。每个条目绑定 Agent 版本、
最低桌面版本、Agent 协议、远端 OpenCode 版本/digest、架构、文件名、大小、SHA-256
以及固定 GitHub/OSS URL。同一版本和架构的字节不可改变;目录最多保留 200 个条目。
OSS 先写 `agent-releases/v<version>/` 不可变对象,GitHub Release 公开且验证后才更新
单一 `agent-releases/latest.json` 指针。该指针只引用同一不可变版本目录中的
`agent-catalog.json` 与 `agent-catalog.sig`,避免客户端观察到跨版本组合。Agent Release 必须使用
`--latest=false`,不得改变桌面 Release 的 Latest 标记。
### 远程 Runtime 工件
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.29 的 Linux x64
baseline 与 arm64 官方包 integrity、`bin/opencode` 入口和固定 `acp` 参数。它不同于
`agent-runtime-lock.json`,后者锁定 Agent 自带 Node 以及用于 Linux `SO_PEERCRED`
的 Koffi 版本。Agent bundle 将 Koffi loader 作为 external module,并只携带目标
Linux 架构的 glibc/musl 原生 binding;构建与导入都会校验其 ELF 架构和 MIT 许可证。
lock 同时固定 `@continuedev/cli@1.5.47` 的 npm integrity。CN 工件使用
`lib/continue/dist/cn.js`,由 Agent 中的 HTTP-to-ACP helper 启动,不能传入原生 `acp`
参数。`remote-runtime:build` 与 `build/agent-package.cjs build` 接受
`--runtime-id continue`;默认 Agent 组包通过 `--continue-archive` 同时携带 OC/CN。
CN 的 JS payload 通过文件摘要与签名校验,执行它的 Node 仍来自 Agent 固定包。
默认 workflow 已接入联合组包;开发包与完整桌面到 Linux 的实测见
[CN 部署记录](./docs/features/remote-host/runtime-checklist-validation.md#cn-部署与生命周期)。
远程 Runtime 工件由以下命令独立管理:
```bash
# 在对应 Linux 原生 Runner 上从 lock 指定的 npm 归档构建并签名
npm run remote-runtime:build -- --arch <x64|arm64> \
--runtime-archive <locked-opencode-package.tgz>
# 为复合 Agent 包组装导入并完整验证签名归档
npm run remote-runtime:import -- --arch x64 --archive <runtime-x64.tar>
npm run remote-runtime:import -- --arch arm64 --archive <runtime-arm64.tar>
```
默认生成目录为 `.remote-runtime-resources/linux-<arch>/opencode/<digest>`,
不提交到仓库。生产 workflow 从 `remote-runtime-lock.json` 解析固定包名、版本和
integrity,以 `--ignore-scripts` 从官方 npm registry 获取归档;bundle 工具在签名前
再次验证归档 SHA-512、包名、版本和 ELF 架构。
源码已经实现 Runtime bundle build/import/verify、Daemon 侧
manifest/Ed25519/payload/ELF/lock 校验、digest registry、ACP v3、直接进程 ownership 和
`runtime/model-bridge` v1。Agent 通过私有 Unix socket 按需 detached 启动,不依赖
systemd、D-Bus、Linger 或 bubblewrap。Ask 与 Execute 都直接启动签名 Runtime;两者的
差异由 OpenCode Ask 权限配置和 Agent 工具权限分发边界共同执行,Execute 不注入该
权限覆盖。
OpenCode 通过已签名 Agent helper 和每次 Prompt 的私有 Unix socket 使用 Main-only 模型
网关;Provider URL、API Key 和真实 Provider 认证头不进入远端。helper 的 loopback
HTTP 入口使用每个进程随机生成的路径 capability,Anthropic/OpenAI SDK 需要的固定非秘密
本地兼容标记由 helper 核对后丢弃。GoodBuddy 自己维护会话标题,所以生成的 OpenCode
配置必须禁用 title Agent;工具循环只使用 build Agent 的模型轮次。
Ask 模式的 ACP 权限中介只可为原生 `read` 选择 `allow_once`;其他工具种类和
`allow_always` 请求必须拒绝。Unix 模型桥
broker 必须按 socket 当前已缓冲字节增量读取长度帧,不能等待 `read(remaining)` 一次返回
完整大响应。Agent 传输或权限实现变化后,除了普通测试,还要在原生 Linux 上运行包含
至少 256 KiB 响应的模型桥回归。
`agent-runtime-lock.json` 维护独立于桌面应用的 Agent 版本,
`remote-runtime-lock.json` 维护 OpenCode Runtime 版本。Agent 内容变化时必须更新
`agentVersion`;Runtime 更新时使用 Runtime 自身版本。签名 manifest 或 bundle digest
唯一标识对应版本的精确工件。变更后必须重新构建、签名并导入 Linux x64/arm64 两套工件;
旧版本或旧 digest 会按设计校验失败。不要手工把本机 `.runtime-resources` 当作远程 bundle。
桌面应用更新后不会在项目切换时下载或安装 Agent。用户先在 SSH Host 的远程运行环境
卡片显式选择直连下载或 GoodBuddy 传输。托管 SSH 项目只保存 Host ID、远端路径、
Runtime 选择和默认模式;创建或保存时验证 Host 当前环境,普通项目切换只读取这些本地
配置。首次实际使用 Workspace 或 Runtime 时解析 Host current identity;同一进程内后续
项目复用已确认的 Agent/Runtime identity 和 Agent 连接。该流程不取得安装包、不重读完整
payload,也不发布组件。
两条手动准备路径都支持阶段进度和取消;成功后定向失效该 Host 的连接与 Runtime 缓存,
引用项目下一次打开自然使用新的 current 环境。验收应覆盖两种准备方式与项目当前环境
验证入口,并确认失败或取消不会删除 Host 配置、凭据、项目设置、Workspace 文件、全局
可信元数据或仍可用的旧安装。
## 跨平台 CI 与 GitHub Release
`.github/workflows/packages.yml` 是统一发布工作流。它先验证并生成一次
`out` 生产 bundle,再在六个原生 Runner 上分别打包 Windows、macOS 和
Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifact
供打包任务复用,不会上传到 GitHub Release。
供打包任务复用,不会上传到 GitHub Release。该工作流不构建、下载、签名或嵌入
远端 Agent/OpenCode 复合包,也不依赖 `agent-signing` Environment;远端包缺失或
Agent 发布失败不会阻塞任何桌面目标。
本地构建单个平台目标:
@@ -138,8 +423,21 @@ Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifac
npm run release:package -- --platform <windows|macos|linux> --arch <x64|arm64>
```
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直接
`release:package` 仅构建桌面产品。基础配置统一携带
`agent-release-keys.json`、`agent-runtime-lock.json` 与 `remote-runtime-lock.json`
供 Main 校验独立下载的复合包,但 release 配置不增加 `.agent-resources` 或
`.remote-runtime-resources`。发布校验继续检查桌面自身的 OpenCode、Continue 与
DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
在 macOS 上明确生成未签名、未公证的开发验证包:
```bash
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
```
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
`release-manifest.json`。
@@ -148,24 +446,109 @@ ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直
打包矩阵,避免随后推送版本标签时对同一提交重复完整打包。手动触发会运行
验证和六平台打包,并保留 30 天 Actions artifacts,但不会创建 Release。
### Desktop 发布顺序(强制)
Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段,不能在同一批操作中先后
推送 `main` 和标签:
1. 完成源码、版本、双语 Release Notes 与相关文档修改,运行 Release Notes 校验、
`npm test`、`npm run typecheck` 和 `npm run lint`。常规发布准备不在本机运行
production build、打包或安装包探针。
2. 提交候选,确认用户批准的准确 commit SHA 与双语 Release Notes,fetch 两个远端后,
将该候选分别推送到 `origin/main` 和 `github/main`。
3. 等待这个准确 SHA 的 `main` 分支 `Cross-platform packages` workflow 完成。只有
`Run validators` 和 `Build production bundle` 都成功,候选才具备打标签条件。
4. 如果 `main` workflow 失败,不得创建标签。修复必须形成新的 commit,重新完成本地
校验、双远端 `main` 推送,并等待新 SHA 的 `main` workflow 成功;旧 SHA 的成功结果
不能替代新 SHA 的验证。
5. `main` 成功后再次 fetch 两个远端,确认工作区干净、两个远端 `main` 都等于获批 SHA、
目标标签在本地和远端均未使用。此时才可在该 SHA 创建 annotated tag,并把标签分别
推送到两个远端,随后等待六平台打包和正式发布完成。
不可移动、删除、重建或复用失败的发布标签。完整审批、恢复和发布后检查以
[发布 Runbook](./docs/development/release-runbook.md) 为准。
推送 `v${package.version}` 标签时,工作流运行验证和六平台打包。只有在
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流创建或
更新 draft GitHub Release,上传全部资产成功后才发布。重跑会保留人工
编辑的 Release notes 和未知附件。
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流通过
GitHub OIDC 获取短期 STS 凭据,将发布资产和 `site-release.json` 上传到
北京 OSS 的不可变版本目录,并公开校验 12 个安装包。验证通过后才创建或
更新 draft GitHub Release、上传 20 个 Release 资产并正式发布,最后原子
切换官网 `latest.json`。任一步失败都不会提前切换官网最新版本。
同一标签重跑时,工作流会根据 `resources/release-notes.json` 重新生成并
覆盖 GitHub Release 正文,以 `--clobber` 更新已知发布资产,同时保留未知
附件。若源码与发布元数据未变化,应修正外部配置后重跑同一不可变标签;
只有必须修改代码或元数据时才递增版本并创建新标签。
Agent 发布使用另一套标签和 workflow:
```bash
agent_tag="agent-v$(node -p "require('./agent-runtime-lock.json').agentVersion")"
git tag -a "$agent_tag" -m "GoodBuddy Agent $(node -p "require('./agent-runtime-lock.json').agentVersion")"
```
实际创建或推送前同样必须获得用户确认。`.github/workflows/agent-release.yml`
原生构建两个 `.gbagent`、签名累计目录、上传
`agent-releases/v<agent-version>/`,创建 `--latest=false` GitHub Release,再切换
OSS 签名目录指针。生产发布需要 `agent-signing` 与 `aliyun-oss-release` 两个受保护
Environment;普通分支/PR 的 `agents.yml` 不使用其中任何 Secret。
Agent 标签也必须遵循与 Desktop 相同的 pre-tag 顺序。更新 Agent 生产源码或
`agentVersion` 后,先把候选提交推送到两个 `main`,等待该准确 SHA 的
`GoodBuddy Agents` main workflow 全部成功,再次 fetch 并确认两个远端 `main`、干净
工作区和未使用标签,最后才可创建和推送 annotated `agent-v<version>`。不得把 Agent
标签与候选 `main` 放在同一批推送中;任何修正 commit 都必须重新等待新 SHA 的 Agent
main workflow,旧 SHA 的成功结果不能复用。
中英文发布说明统一维护在 `resources/release-notes.json`。新版本按“本次
亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前
请留意 / Before You Start”四段组织,应用首次启动弹窗与 GitHub Release
正文共用该来源。旧版两段式记录会兼容读取,无需改写。提交发布候选前运行
`npm run release:notes:verify` 校验版本、双语条目数量并生成 Markdown。
发布标签必须与 `package.json` 版本完全一致。实际推送标签和触发发布前仍
需人工确认,例如当前版本应使用:
```bash
tag="v$(node -p "require('./package.json').version")"
git tag "$tag"
git tag -a "$tag" -m "GoodBuddy $(node -p "require('./package.json').version")"
git push origin "$tag"
git push github "$tag"
```
当前未配置 Windows/macOS 代码签名或 macOS notarization。对外分发前应按
目标平台配置签名凭据并重新验证安装、升级和系统安全提示。
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
任务失败,不能静默降级为未签名包。
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
- `MACOS_CERTIFICATE_PASSWORD`:导出 `.p12` 时设置的密码。
- `APPLE_API_KEY_BASE64`:App Store Connect API Key 的 `.p8` 文件经 Base64 编码后的内容。
- `APPLE_API_KEY_ID`:App Store Connect API Key 的 Key ID。
- `APPLE_API_ISSUER`:App Store Connect API Key 的 Issuer ID。
在 macOS 上生成适合 Secrets 的单行 Base64 内容:
```bash
base64 -i DeveloperIDApplication.p12 | tr -d '\n'
base64 -i AuthKey_XXXXXXXXXX.p8 | tr -d '\n'
```
证书必须是 Apple Developer 后台创建的 `Developer ID Application`,并在导出
`.p12` 的 Mac 钥匙串中同时包含对应私钥。API Key 建议使用团队级 App Store
Connect Key;`.p8` 只能下载一次。签名材料只放入 GitHub Secrets,不提交到仓库。
凭据完整时,macOS 打包完成后会挂载 DMG,并分别执行 `codesign`、Gatekeeper
`spctl` 和 `stapler` 校验;签名无效或 notarization ticket 不存在时,发布矩阵
会在上传产物前失败。完全没有凭据时,六平台矩阵和标签发布仍可完成,但 macOS
产物没有 Developer ID 签名或 Apple 公证,Gatekeeper 可能阻止首次打开。发布
工作流必须在 Actions 日志与摘要中明确这一限制;获得完整凭据后应恢复签名
发布并重新验证。
Windows 代码签名仍未配置。对外分发前还应配置 Windows 签名凭据,并重新验证
安装、升级和系统安全提示。
## 发布前冒烟测试
@@ -177,5 +560,54 @@ git push github "$tag"
4. 本地知识库导入、检索和知识图谱。
5. Ask、Execute 的权限边界与旧版 Plan 数据兼容。
6. OpenCode 与 Continue 的权限边界、取消和超时。
7. 智能心跳的创建、暂停、恢复和历史记录。
8. 应用退出后无残留 Runtime 子进程。
7. DeepSeek Harness Ask 拒绝写入和第三方插件工具,可调用 Main 管理的 Web Search/Fetch;Execute 可调用已启用插件工具。文本模型在网络调用前拒绝图片,声明图片能力的模型可以实际接收 JPEG/PNG。
8. OpenCode Agent/Command、原生上下文 Compact,以及 Continue Rules/Prompt 预设、结构化提问和 GoodBuddy 手动摘要压缩。
9. Runtime 原生清单把 Tools 与 Commands/LSP/Formatters 分开,显示来源及 Ask/Execute 可用性,不混入 GoodBuddy 分配的 Skills/MCP;外部 OpenCode 只报告连接状态,Continue 明确标记原生 Tools 静态发现不支持;内置 MCP 的启停与 Runtime 分配会持久化并限制后续请求,DeepSeek Harness 保持不可分配;MCP 测试只读取有界 Prompt/Resource 元数据,不读取 Resource 内容。
10. DSH 市场可安装、停用、重新启用和移除插件;启动失败插件不会阻止 Host,并显示为自动停用。
11. 智能心跳的创建、暂停、恢复和历史记录。
12. 应用退出后无残留 Runtime 子进程。
DeepSeek Harness 的 Electron Utility Host 可单独执行无模型、无凭据冒烟测试:
```bash
npm run smoke:deepseek-harness
```
该命令先生成 production bundle,再从 CommonJS Electron 主入口启动实际
`utilityProcess`,等待固定 Host 完成本地主机执行器初始化与内部 ready 握手。它不会发起
模型请求,也不会读取或传递 API Key。
Windows x64 完整打包后的 Utility Host 与内置 npm 冒烟测试:
```bash
npm run build
node node_modules/electron-builder/cli.js --win dir --x64 --publish never --config.directories.output=dist/harness-package-probe
npm run smoke:deepseek-harness:packaged
```
该测试启动打包后的 Host,并使用打包资源中的准确 npm 版本安装一个本地临时包,
确认 npm 依赖闭包、Electron Node 模式、`node` shim 和生命周期脚本都可用。它不访问
npm registry,也不运行模型请求。
真实 DSH 市场测试会访问公共 npm、运行第三方安装与插件代码,因此只在已明确授权时启用:
```bash
GOODBUDDY_DSH_MARKETPLACE_E2E=1 npm test -- src/main/agent/dsh-extension-marketplace.e2e.test.ts
```
该测试使用临时用户目录,经捆绑 npm 路径安装已审查的最小测试插件,并验证 Host
加载和真实工具调用;测试结束后删除临时目录。
要让真实模型同时验证插件的 Ask 拒绝与 Execute 调用,可显式提供兼容的
OpenAI Chat Completions 配置:
```bash
GOODBUDDY_DSH_MODEL_E2E=1 \
GOODBUDDY_DSH_API_KEY=... \
GOODBUDDY_DSH_BASE_URL=https://api.deepseek.com \
GOODBUDDY_DSH_MODEL=deepseek-chat \
npm test -- src/main/agent/deepseek-harness-acp-e2e.test.ts -t "rejects a real npm plugin"
```
如需覆盖发布包内置 npm 路径,再设置 `GOODBUDDY_DSH_NPM_CLI` 与
`GOODBUDDY_DSH_NODE_EXECUTABLE` 指向已解包应用中的 npm CLI 和应用主程序。
-505
View File
@@ -1,505 +0,0 @@
# GoodBuddy 统一界面设计系统
## 1. 目的与适用范围
本文定义 GoodBuddy 桌面端的统一界面规则,适用于聊天与最近对话、知识库、智能心跳、任务与活动记录,以及后续新增的一级页面。
设计系统解决两类问题:
1. 统一跨页面的视觉语言、信息层级和交互反馈。
2. 保留不同工作场景所需的布局差异,不强行把阅读、工作台、仪表盘和数据视图做成同一种页面。
所有新界面必须优先使用本文定义的语义令牌、页面壳层和共享组件。现有界面迁移时应保持功能与数据语义不变,不以视觉统一为理由隐藏范围、状态或风险。
## 2. 核心原则
### 2.1 一致的语义,不要求相同的布局
- 相同含义使用相同的颜色、间距、控件和文案结构。
- 不同任务使用明确命名的页面壳层,不再为单个页面随意设置宽度。
- 页签只用于同级页面导航,分段控件只用于当前视图内的单选切换,筛选控件只改变当前数据集合。
### 2.2 范围始终可见
知识、任务、自动化和活动记录可能属于全局或某个项目。当前范围必须在页面标题区或对象标题旁持续显示,不能只通过入口位置、默认筛选或颜色暗示。
### 2.3 主任务优先
- 每个页面只保留一个视觉上最突出的主操作。
- 页面标题、范围、说明、主操作和导航按固定层级排列。
- 次要设置、筛选和批量操作不得与主任务争夺注意力。
### 2.4 状态明确且可恢复
- 加载、空内容、无结果、失败、只读和权限不足是不同状态,必须分别呈现。
- 可撤销操作优先提供撤销,不可撤销操作必须在执行前说明影响范围。
- 不依赖颜色单独表达状态或风险。
### 2.5 默认支持浅色与深色主题
组件不得直接使用原始颜色值。主题差异只在令牌层定义,业务组件仅引用语义令牌。
## 3. 设计令牌
令牌以 CSS 自定义属性实现。`:root` 提供浅色值,`[data-theme="dark"]` 覆盖深色值。组件样式不得新增只服务于单个页面的颜色、阴影、圆角或间距常量。
### 3.1 颜色令牌
| 令牌 | 用途 |
| --- | --- |
| `--surface-canvas` | 应用和页面底色 |
| `--surface-raised` | 卡片、面板、输入框和弹窗 |
| `--surface-subtle` | 次级区域和控件底色 |
| `--surface-muted` | 进度轨道和更弱的分隔表面 |
| `--text-primary` | 标题与主要正文 |
| `--text-secondary` | 辅助正文 |
| `--text-muted` | 占位、时间和弱提示 |
| `--text-on-accent` | 强调色或危险色表面上的文字 |
| `--border-default` | 常规边框 |
| `--border-control` | 输入框、分段控件等必须清晰可辨的控件边界 |
| `--border-subtle` | 区块分隔线 |
| `--accent`、`--accent-hover` | 强调文字、图标和交互反馈 |
| `--accent-solid`、`--accent-solid-hover` | 主按钮等需要反白文字的强调背景 |
| `--accent-selected`、`--accent-subtle` | 选中边框、选中背景和信息强调 |
| `--success`、`--success-subtle` | 成功状态 |
| `--warning` | 警告状态 |
| `--danger`、`--danger-strong` | 危险文本和状态 |
| `--danger-solid` | 最终确认按钮等需要反白文字的危险背景 |
| `--danger-border`、`--danger-subtle` | 危险入口和失败状态背景 |
浅色与深色具体值只在 `styles.css` 的主题根节点维护。状态组件必须同时显示文字或图标,不能仅靠颜色区分。
### 3.2 间距令牌
采用 4 像素基准:
| 令牌 | 值 | 典型用途 |
| --- | --- | --- |
| `--space-0` | `0` | 显式取消间距 |
| `--space-1` | `4px` | 图标内部微调 |
| `--space-2` | `8px` | 紧凑控件、图标与文字 |
| `--space-3` | `12px` | 表单字段内部、紧凑列表 |
| `--space-4` | `16px` | 默认组件间距 |
| `--space-6` | `24px` | 区块间距、窄屏页面内边距 |
使用规则:
- 同一组件内部优先使用 `8px`、`12px`、`16px`。
- 同一区块内组件之间优先使用 `16px` 或 `24px`。
- 页面级区块之间优先使用 `32px` 或 `40px`。
- 不新增 `14px`、`18px`、`22px` 等非令牌间距。
### 3.3 字体令牌
界面字体使用系统无衬线字体栈,代码、标识符和原始日志使用等宽字体栈。
| 令牌 | 字号 / 行高 | 字重 | 用途 |
| --- | --- | --- | --- |
| `--font-caption` | `10px` | 时间、短标签和紧凑元数据 |
| `--font-body` | `12px` | 默认界面正文 |
| `--font-section-title` | `14px` | 卡片和区块标题 |
| `--font-page-title` | `24px` | 一级页面标题 |
连续阅读内容使用 `13px` 至 `14px`,持久辅助信息不得小于 `10px`。页面内不得通过同时放大字号、加粗和使用强调色制造多个同级主标题。
### 3.4 圆角、阴影与层级
| 令牌 | 值 | 用途 |
| --- | --- | --- |
| `--radius-control` | `8px` | 输入框、按钮、菜单项 |
| `--radius-card` | `12px` | 卡片和面板 |
| `--shadow-card` | 主题定义 | 卡片和选中分段控件 |
| `--shadow-dialog` | 主题定义 | 对话框和浮层 |
普通卡片通过表面色和边框区分,不默认添加阴影。阴影只表示真实的浮层关系。不允许页面自行创建高于 `--z-dialog` 的层级。
### 3.5 动效令牌
- `--motion-fast: 120ms`:悬停、按压、焦点反馈。
- `--motion-normal: 180ms`:菜单、折叠区域、轻量视图切换。
- `--motion-slow: 240ms`:对话框和抽屉。
- 默认缓动使用 `ease-out`,退出可使用 `ease-in`。
- 在 `prefers-reduced-motion: reduce` 下移除非必要位移和缩放,仅保留即时状态切换。
## 4. 页面壳层
所有一级页面使用 `PageShell`。壳层负责水平居中、最大宽度、响应式内边距、页面背景和滚动边界,业务页面不得再次设置独立的整体最大宽度。
| 变体 | 最大内容宽度 | 适用场景 | 页面映射 |
| --- | --- | --- | --- |
| `reading` | `820px` | 连续阅读、单列编辑、对话撰写 | 聊天正文与输入区 |
| `standard` | `960px` | 常规列表、设置、表单与任务管理 | 最近对话、任务 |
| `dashboard` | `1040px` | 指标、卡片网格、宽表格与审计数据 | 智能心跳、活动记录 |
| `master-detail` | 可用空间内流式铺开 | 左侧选择、右侧编辑或预览 | 知识库 |
共同规则:
- 宽度计算统一为 `min(100%, 变体最大宽度)`。
- 页面左右内边距在常规桌面窗口为 `32px`,中等窗口为 `24px`,窄窗口为 `16px`。
- 页面顶部和底部内边距默认为 `32px`。
- `master-detail` 的左栏建议为 `280px` 至 `360px`,右栏占剩余空间。分隔线和滚动容器属于壳层,不由内容卡片模拟。
- 聊天输入区可以粘附在 `reading` 壳层底部,但消息内容和输入区必须共享同一阅读宽度。
- 宽表格可在 `dashboard` 内容区内部横向滚动,不得撑宽整个应用窗口。
## 5. 信息层级
一级页面按以下顺序组织:
1. `PageHeader`
2. `PageTabs`,仅在存在同级子页面时出现
3. 页面级状态或重要提示
4. 筛选与批量操作工具栏
5. 主内容
6. 与主内容就近关联的分页或加载状态
一个页面只能有一个可见的一级标题。卡片标题不得重复页面标题。面包屑只在层级超过两级且返回关系无法通过侧栏或主从布局表达时使用。
### 5.1 PageHeader
`PageHeader` 统一接收以下内容:
- `title`:必填,简短名词或任务名称。
- `description`:可选,一行说明当前页面能做什么,不重复标题。
- `scope`:涉及范围的数据页面必填,使用 `ScopeBadge`。
- `primaryAction`:可选,页面唯一主操作。
- `secondaryActions`:可选,最多两个直接显示,其余进入更多菜单。
- `status`:可选,用于只读、同步中、连接异常等页面级状态。
布局规则:
- 标题和范围徽标在同一信息组中,范围不得放入更多菜单。
- 主操作位于标题区右侧,窄窗口下换行到标题下方并保持靠左。
- 不在内容卡片中再次渲染同名标题。
- 标题区默认不粘附。只有主内容长且页面级操作需要持续可用时才启用粘附。
## 6. 共享组件
### 6.1 PageTabs
用于同一一级页面内的同级内容面板,例如心跳的“成长概览”和“心跳计划”。
- 使用 `tablist`、`tab` 和 `tabpanel` 语义,当前项使用 `aria-selected="true"`。
- 一级页面之间的导航由应用主导航承担,不复用 `PageTabs`。
- 标签保持短名词,不显示句号,不用页签承载开关或过滤条件。
- 项目过多时优先重组信息架构,不把一级页签做成多行。
### 6.2 SegmentedControl
用于当前页面内互斥的视图或状态切换,例如“列表 / 网格”或少量单选状态。
- 选项数量为 2 至 4 个。
- 使用带 `aria-pressed` 的按钮组语义,支持方向键切换。
- 不能用于多选筛选、页面导航或执行即时命令。
- 控件宽度由内容决定,除移动窄屏外不默认等分整行。
### 6.3 筛选工具栏
筛选不创建第五种类似页签的视觉控件:
- 2 至 4 个互斥状态可使用 `SegmentedControl`。
- 多维条件使用统一尺寸的选择框、搜索框和复选菜单。
- 已生效条件以文字和值明确展示,并提供“清除筛选”。
- 无匹配结果使用“无结果”状态,不能显示为首次使用的空状态。
- 筛选条件改变数据范围时,结果数量或当前条件必须可见。
### 6.4 ScopeBadge
`ScopeBadge` 表达对象或页面数据的归属范围:
- 全局范围显示“全局”,并配合通用范围图标。
- 项目范围显示“项目:项目名称”,不得只显示项目名称。
- 未知或已失效范围显示“范围不可用”,并提供修复入口或只读说明。
- 徽标始终包含文字,不以颜色作为唯一差异。
- 可切换范围时,徽标作为范围选择器的触发器,并提供明确的展开状态和键盘操作。
- 不可切换范围时,徽标为只读状态,不显示下拉箭头。
创建新对象时必须在提交按钮附近再次显示目标范围。跨范围移动属于显式操作,需要说明原范围、目标范围和对关联数据的影响。
### 6.5 EmptyState
`EmptyState` 统一包含:
- 能说明状态的图标或简洁插图。
- 一句标题。
- 一至两句原因或下一步说明。
- 最多一个主操作和一个次操作。
必须区分:
| 状态 | 标题示例 | 操作原则 |
| --- | --- | --- |
| 首次为空 | “还没有知识条目” | 提供创建或导入 |
| 筛选无结果 | “没有符合条件的结果” | 提供清除筛选 |
| 搜索无结果 | “未找到相关内容” | 建议修改关键词 |
| 加载失败 | “内容加载失败” | 提供重试并保留错误上下文 |
| 无权限或只读 | “当前范围不可编辑” | 说明原因和可行路径 |
空状态不得使用孤立的短句或仅显示图标。加载中不得先闪现空状态。
### 6.6 危险操作
共享危险操作样式包含:
- `danger-ghost`:列表行、菜单和工具栏中的删除入口。
- `danger-solid`:确认对话框中的最终危险操作。
- `danger-zone`:设置页中集中展示的高影响操作区域。
普通页面不得用实心红色按钮与主操作并列。危险入口必须使用具体动词和对象,例如“删除知识条目”,避免只写“确定”。
### 6.7 数据表格与活动列表
- 表头、单元格、空值、状态和行操作使用统一对齐规则。
- 文本默认左对齐,数值右对齐,状态与短标签可居中。
- 时间显示使用一致格式,并在需要时通过工具提示提供完整时间和时区。
- 行操作默认放在行末。高频安全操作可直接显示,低频或危险操作进入更多菜单。
- 活动记录必须保留操作者、动作、对象、范围、结果和时间等审计语义,不用纯图标代替关键字段。
- 表格密度可以选择“默认”或“紧凑”,但同一页面不得混用。
### 6.8 应用顶栏与全局菜单
应用顶栏用于窗口级状态、侧栏开关和低频全局操作,不承担页面标题或主要导航。顶栏必须保持紧凑,不能与页面内容争夺注意力。
- 顶栏高度默认为 `58px`,图标按钮使用 `34px × 34px` 点击区域。
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。
- 全局菜单项使用 `--font-body`,图标为 `14px`,单项高度为 `32px`。
- 菜单标签使用短名称,例如“安全与 Runtime 设置”“使用帮助”,不得同时使用大字号、粗体和强调色。
- 全局菜单宽度由最长标签决定,建议为 `180px` 至 `200px`;说明性长文放入目标页面,不放在菜单项中。
- 顶栏只直接显示当前任务所需的高频操作。设置、帮助、关于和版本检查等低频操作进入同一个全局菜单。
- 窄窗口下优先压缩状态标签并保留图标按钮,不隐藏窗口控制、当前范围或进行中的风险状态。
- 菜单使用 `menu`、`menuitem` 语义,支持上下方向键、Home、End 和 Escape,关闭后焦点返回触发按钮。
## 7. 交互状态
所有可交互组件必须实现:
- 默认:文本、边框和背景层级清晰。
- 悬停:提供轻量背景或边框反馈,不改变布局。
- 按下:反馈比悬停更强,持续时间使用 `--motion-fast`。
- 键盘焦点:显示至少 `2px` 的高对比焦点环,不被容器裁切。
- 选中:同时使用背景、边框、图标或字重中的至少两种信号。
- 禁用:降低强调度,同时保留可读标签,并通过说明或工具提示解释原因。
- 加载:防止重复提交,保留原按钮宽度并显示进行中标签。
- 错误:就近显示可执行的错误说明,不只弹出短暂通知。
异步提交成功后更新内容并提供明确反馈。失败时保留用户输入和筛选上下文。
## 8. 范围与数据语义
### 8.1 页面范围
- 页面范围决定当前列表、搜索、创建和批量操作针对的数据集合。
- 页面标题区必须持续显示当前范围。
- 搜索框占位文案应反映范围,例如“搜索当前项目的知识”。
- 切换范围后清理不再有效的选择项,并明确提示数据集合已变化。
### 8.2 对象范围
- 详情页或主从布局的右侧面板显示所选对象自身的范围。
- 当页面范围与对象范围不一致时,必须显示解释,不得静默混合。
- 全局对象可被项目引用时,应分别表达“归属范围”和“当前引用关系”。
### 8.3 操作范围
批量操作、导入、删除、移动和自动化运行前,界面必须明确:
1. 将影响哪些对象。
2. 对象属于全局还是某个项目。
3. 操作是否可撤销。
4. 是否影响关联任务、知识引用或历史记录。
## 9. 破坏性操作政策
### 9.1 风险等级
| 等级 | 示例 | 要求 |
| --- | --- | --- |
| 低 | 移除可立即恢复的筛选、取消未保存草稿 | 通常无需确认,必要时提供撤销 |
| 中 | 删除单个可恢复对象、停止正在运行的任务 | 显式确认或操作后撤销,说明直接影响 |
| 高 | 永久删除、批量删除、清空历史、删除被引用对象 | 必须使用确认对话框,说明范围、数量和不可逆性 |
### 9.2 确认对话框
- 标题包含具体动作和对象,例如“永久删除 12 条活动记录?”
- 正文说明范围、关联影响和恢复可能性。
- 取消按钮在前,危险按钮在后。初始焦点放在取消按钮。
- 最终按钮使用 `danger-solid`,标签重复具体动作。
- 仅对高影响且不可恢复的操作要求输入对象名称或确认短语,避免把摩擦用于所有删除。
- 提交期间锁定重复操作。失败后保持对话框打开并显示可处理的错误信息。
### 9.3 撤销与反馈
- 可恢复删除优先在操作后显示带“撤销”的持久通知。
- 撤销窗口结束前,不把操作描述为“永久删除”。
- 删除成功后更新列表、选中项和计数。主从布局中被删除对象的详情面板应回到明确的未选择状态。
- 审计记录不得因普通对象删除而被静默移除。
## 10. 深色主题
- 深色主题通过语义令牌替换实现,不在组件中使用主题条件分支选择原始颜色。
- 表面层级主要依靠亮度和边框区分,避免大面积纯黑与高亮白形成刺眼对比。
- 输入框、代码块、表格悬停、选中行、弹窗遮罩和滚动条必须分别检查深色值。
- 图片、图表和状态色在深色背景下保持可读。图表系列不能只靠色相区分,还应使用形状、线型或标签。
- 焦点环、危险文本和弱文本在两种主题下都满足对比度要求。
- 原生控件和滚动条声明正确的 `color-scheme`。
- 主题切换不得导致布局、边框宽度或字号变化。
## 11. 无障碍
### 11.1 键盘与焦点
- 所有交互均可通过键盘完成。
- 焦点顺序与视觉顺序一致,打开弹窗后焦点进入弹窗,关闭后返回触发元素。
- 页面导航链接按正常 Tab 顺序操作。同页签组和单选组使用方向键,菜单使用上下方向键和 Escape。
- 不给不可交互容器添加焦点,不用 `div` 模拟按钮而缺少按钮语义。
### 11.2 语义与标签
- 图标按钮必须有可访问名称,并在视觉上提供工具提示。
- 表单控件有持久标签,不能只使用占位文案。
- 状态变化使用适当的实时区域,但避免对频繁日志逐条播报。
- 错误信息与对应字段建立程序化关联。
- 表格使用正确的表头关系,复杂审计记录在窄屏下仍保持字段标签。
### 11.3 对比度与触控目标
- 正文与背景对比度至少为 `4.5:1`,大号文本至少为 `3:1`。
- 控件边界、焦点和关键图形对比度至少为 `3:1`。
- 常规交互目标建议至少 `32px × 32px`。紧凑表格可使用 `28px` 高度,但相邻目标之间必须有足够间隔。
- 文字缩放至 `200%` 时,核心操作、范围信息和错误提示不能被裁切。
## 12. 响应式与窗口规则
GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证任务连续性,不简单隐藏信息。
### 12.1 宽窗口,`1200px` 及以上
- 使用各 `PageShell` 的完整最大宽度。
- `dashboard` 可使用 3 至 4 列卡片。
- `master-detail` 保持双栏,详情区获得主要空间。
### 12.2 中等窗口,`960px` 至 `1199px`
- 页面左右内边距为 `24px`。
- 仪表盘降为 2 列。
- 主从布局缩小左栏,但不得低于 `280px`。
- 表格优先收起低优先级列到详情或行展开区,不隐藏范围、状态和时间。
### 12.3 窄窗口,`720px` 至 `959px`
- `PageHeader` 的操作区换行。
- `master-detail` 变为单面板导航。进入详情后提供明确返回列表的按钮。
- 仪表盘使用单列或 2 列,取决于卡片最小宽度。
- `PageTabs` 可单行横向滚动,不换成下拉菜单。
- 宽表格在自身容器内横向滚动,并固定关键标识列时确保键盘可达。
### 12.4 极窄窗口,小于 `720px`
- 页面左右内边距为 `16px`。
- 标题、范围徽标和操作纵向排列,但范围不得隐藏。
- 主操作可以占满可用宽度,次操作进入更多菜单。
- 分段控件可等分整行,筛选工具栏改为可展开区域。
- 聊天输入区保持可见,并考虑窗口安全边距。
- 对话框使用接近全宽的布局,仍保留 `16px` 外边距。
## 13. 页面应用规范
### 13.1 聊天
- 使用 `reading` 壳层,消息流与输入区共享宽度。
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
- 模式、模型或工具权限属于上下文控制,不与页面导航页签混用。
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
### 13.2 最近对话
- 使用 `standard` 壳层和统一 `PageHeader`。
- 搜索、范围和时间筛选位于筛选工具栏。
- 行项目统一显示标题、范围、最近更新时间和必要状态。
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
### 13.3 知识库
- 使用 `master-detail` 壳层。
- 左侧负责范围、搜索、筛选和条目选择,右侧负责详情、编辑和预览。
- 两侧均只使用语义颜色令牌,禁止内联浅色背景或边框。
- 选中条目后持续显示对象范围。没有选中项与知识库为空必须使用不同状态。
- 窄窗口切换为单面板导航,不把双栏压缩到不可读。
### 13.4 智能心跳
- 使用 `dashboard` 壳层。
- 顶部先呈现运行状态、当前范围和主操作,再呈现指标和配置。
- 状态卡片使用统一状态令牌,不只依赖颜色。
- 运行历史与配置使用明确区块,不以多套相似页签混合导航、开关和筛选。
### 13.5 任务与活动
- 任务使用 `standard` 壳层,活动记录使用 `dashboard` 壳层。
- “任务 / 活动”作为同级页面时使用 `PageTabs`。
- 任务状态筛选使用 `SegmentedControl` 或筛选工具栏,不再模拟页签。
- 活动记录保留审计字段和范围,支持独立容器横向滚动。
- 批量停止、删除和清空历史遵循破坏性操作政策。
## 14. 文案规则
- 使用简体中文,动词直接、对象明确。
- 页面标题使用名词,例如“知识库”“智能心跳”“活动记录”。
- 按钮使用“动词 + 对象”,例如“新建任务”“导入知识”“停止运行”。
- 状态文案描述事实,例如“正在同步”“上次运行失败”,不使用含糊的“异常”。
- 错误提示包含发生了什么、用户可以做什么。保留必要的错误上下文,但不得暴露凭据、授权头、私人文档或未脱敏的提供商响应。
- 同一概念固定使用一个名称,不交替使用“项目空间”“工作区”“范围”等近义词。产品内统一使用“范围”表达全局与项目归属。
## 15. 迁移检查清单
### 15.1 基础层
- [ ] 建立浅色与深色语义颜色令牌,移除业务组件中的原始颜色值。
- [ ] 建立间距、字体、圆角、阴影、层级和动效令牌。
- [ ] 为主题切换、减少动态效果和原生控件设置全局规则。
- [ ] 建立组件交互状态和焦点环基线。
### 15.2 页面壳层与层级
- [ ] 实现 `PageShell` 的 `reading`、`standard`、`dashboard`、`master-detail` 变体。
- [ ] 按页面映射替换 `820px`、`900px`、`1040px` 和全宽等分散宽度声明。
- [ ] 每个一级页面只保留一个 `PageHeader` 和一个一级标题。
- [ ] 移除内容卡片中与页面标题重复的标题。
### 15.3 共享组件
- [ ] 实现并迁移 `PageHeader`。
- [ ] 使用 `PageTabs` 统一同级页面导航。
- [ ] 使用 `SegmentedControl` 统一少量互斥视图和状态切换。
- [ ] 建立统一筛选工具栏,移除以页签样式伪装的筛选。
- [ ] 实现 `ScopeBadge` 并覆盖全局、项目、失效和可切换状态。
- [ ] 实现 `EmptyState` 的首次为空、无结果、失败和只读变体。
- [ ] 实现 `danger-ghost`、`danger-solid` 和 `danger-zone`。
### 15.4 页面迁移
- [ ] 聊天迁移到 `reading`,统一消息流与输入区宽度。
- [ ] 最近对话迁移到 `standard`,统一搜索、范围、时间和删除行为。
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
- [ ] 智能心跳迁移到 `dashboard`,统一状态卡片、配置和运行历史层级。
- [ ] 任务迁移到 `standard`,活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
### 15.5 验收
- [ ] 在浅色与深色主题下检查所有页面、弹窗、菜单、输入框、表格和空状态。
- [ ] 在大于等于 `1200px`、`960px`、`720px` 和小于 `720px` 的窗口宽度检查布局。
- [ ] 仅使用键盘完成导航、筛选、创建、编辑、确认和取消。
- [ ] 验证焦点恢复、可访问名称、表单错误关联和实时状态播报。
- [ ] 验证页面范围、对象范围和操作范围在关键流程中始终可见。
- [ ] 验证删除、批量操作、停止运行和清空历史符合风险等级策略。
- [ ] 验证加载中、首次为空、筛选无结果、搜索无结果、失败和只读状态不会互相混用。
## 16. 完成标准
一个页面只有在满足以下条件时才视为完成迁移:
1. 使用明确的 `PageShell` 变体,没有页面专属整体宽度。
2. 使用统一标题层级、共享导航和筛选控件。
3. 所有颜色、间距、字体、圆角和阴影来自令牌。
4. 全局或项目范围在浏览、创建、编辑和危险操作中均可见。
5. 浅色、深色、键盘和各窗口宽度下均可完成核心任务。
6. 空状态、错误状态和危险操作符合本文规则。
+939 -83
View File
File diff suppressed because it is too large Load Diff
+172
View File
@@ -0,0 +1,172 @@
# GoodBuddy 功能矩阵与路线图
[English](./FEATURES.md) | **简体中文**
本文记录 GoodBuddy 的已提供能力和路线图。未完成项目不代表已包含在当前版本中。
## 状态说明
- [x] 已完成
- [ ] 待实现
状态表示功能实现情况,不等同于版本已发布或全部测试已执行。验证记录单独列出,不作为第三种功能状态。
## 功能总表
### 桌面基础、工作空间与上下文
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;仅在明确要求时构建。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
- [x] **跨项目会话活动**:统一入口汇总各项目正在运行和需要处理的对话,保留仍有活动的较早会话。锚定的“项目 → 会话”菜单支持键盘导航和精确打开对话,不再使用整块活动弹窗;空闲时隐藏摘要,长会话子菜单独立滚动,项目选择器继续显示活动数量。
后台会话成功完成后,在本次客户端运行期间保留完成提示,直到对应聊天可见且加载完成;提示不跨重启保留。
- [x] **会话置顶与完整历史**:常用会话可置顶,重启后保留。列表和搜索不再限于最近 100 个会话,连续发送不再截断超过 500 条的已保存历史;模型上下文限制仍有效,已丢失的历史无法恢复。
- [x] **按需上下文帮助**:页面和设置中的补充说明使用标题或字段旁的共享帮助入口,支持鼠标与键盘;关键操作后果和错误提示保持可见。
- [x] **紧凑会话控件**:输入选项集中在紧凑设置面板,Runtime、工作模式、发送和队列操作保持可用;会话搜索提供内嵌清除按钮,清除后恢复当前范围列表并把焦点返回输入框。
- [x] **会话历史按需加载**:列表只加载轻量摘要,不再一次读入全部会话的过程元数据;已打开和活动会话保留完整详情,搜索、复制、导出、续聊和远程待答问题保持可用。图片合并不会重新发送未变化消息,已确认保存的空闲详情随现有视图缓存释放。详见[历史读取与保留规则](./docs/features/assistant-workbar/execution-history-storage.md#会话列表读取与前端保留)。
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。聊天输入框支持 `Ctrl+V` 粘贴一个或多个本地支持文件并自动添加为附件,沿用附件按钮的解析进度与限制;普通文本和截图仍可直接粘贴。详见[聊天附件规则](./docs/features/document-processing/prd.md#321-聊天附件入口)。
- [x] **图片型 PPTX 文字识别**:配置本地 OCR 并选择启用 OCR 的解析方式后,可识别内嵌 PNG、JPEG 和 WebP 图片,保留原生文字与幻灯片定位。OCR 在没有活动或排队任务时空闲 60 秒自动释放模型内存,下次按需重载;快速文本/索引模式仍跳过 OCR。详见[解析行为与真实文件验证](./docs/features/document-processing/chat-attachments-technical-design.md)。
- [x] **HTTP OCR 与附件持久预览**:在“设置 / 文档解析”配置 HTTP PaddleOCR-VL,供聊天文档和知识库导入共用。保存原件与插图,提供正文/图片/详情预览、显式图片文字提取,以及选图加入指定会话草稿。普通图片默认走图片输入,发送与入队检查实际模型和 Runtime。Windows、直连视觉输入和共享 Linux Host 托管 OpenCode 已有真实验证;部署高级 OCR 模型及其他 Runtime 组合需分别测试。本机推理监控只管理本地服务,不新增应用中心入口,详见[范围与证据](./docs/features/document-processing/README.md)。
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异、当前浏览目录和已展开目录,超过 50 项变更可继续加载。
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。提交历史贴底,首次展开占 Git 内容区一半,支持调整比例、独立滚动和统一刷新;切换项目自动检测 Git,不沿用其他项目状态。移动限于工作区内,删除须确认,分支冲突不自动 stash 或丢弃改动;托管 SSH 操作需要 Agent `0.11.23`。
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Mermaid 大图放大后可滚动到各个边缘,并可导出完整 PNG,不受当前缩放和拖动位置影响。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
- [x] **助手工作栏、多终端与可调布局**:工作栏开关位于明暗主题按钮旁;右侧工作栏使用持久“+”能力目录和应用 Tab,任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目和所有项目,范围随布局保存;无项目任务在所有项目中标注“未绑定项目”。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记编辑器的 AI 评论栏支持鼠标与键盘调宽;AI 评论栏记住显示状态及宽度,窄窗口下移至编辑器底部。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
- [x] **应用中心与导航**:底部应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻筛选。点击应用行关闭菜单,本机推理监控打开保留底层工作区的独立 Modal,其余应用打开主内容区;底部“管理应用”打开可搜索卡片及设置详情,布局与交互见 [UI 设计](./docs/features/application-tool-navigation/ui-design.md)。四张应用卡片均支持上下移动和拖动排序,不受启用或常驻状态限制;卡片、菜单和经过过滤的侧栏共用一份保存顺序。知识库始终启用并常驻,提供“打开”和排序;监督者、魔法笔记和本机推理监控可设置启用与常驻。监督者沿用智能心跳身份,缺失启用设置时默认关闭,已明确保存的选择保留;关闭后保留计划和历史,拒绝新回顾并隐藏侧栏反馈,在途运行可完成。启用应用不会自动创建计划。旧应用顺序补齐默认项并保留已有相对顺序。设置持久化后的事件同步各入口,包括工具配置写入;重开中心刷新期间锁定修改,新快照覆盖迟到的旧读取。`local-inference` Modal 直接显示服务列表,不展示任务历史或外部连接;向量服务显示独立进程 CPU 与工作集内存,ASR/OCR 说明共享进程无法独立统计的原因。ASR 就绪状态未知,不从活动请求推断运行中。受支持的服务操作保留影响确认,失败后刷新列表并要求重新确认;TTS 显示不可用。取消中的推理在 Worker 确认或退出前仍计入活动任务及服务停止影响。实现已接入生产 App;验收记录分别列出完整 App、组件 fixture、真实本地引擎和剩余平台/安装包覆盖,不以历史失败代替当前全量结果,详见[验证进度](./docs/features/application-tool-navigation/progress.md)。
- [ ] **私有化应用市场**(规划中):在应用管理基础上提供组织自己的目录与应用分发。当前只管理内置应用,尚无市场浏览或安装能力,详见 [FR-15](./docs/features/application-tool-navigation/prd.md#fr-15-私有化市场与-yaml-交换后续)。
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入更完整的执行监督、统一 Runtime 监控、受管进程、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
- [ ] **ShareServer Office 协同编辑**(设计中):通过可选 ShareServer 集成 ONLYOFFICE Docs,在助手工作栏中以文件名打开多个文档 Tab,支持 DOCX、XLSX、PPTX 的人工编辑、保存、撤销、源文件冲突保护和后续 AI 选区修改。Office 正文会由所选 ShareServer 和编辑引擎处理;Desktop 不内置或启动 Document Server,未配置服务时不宣称离线编辑。详见[设计文档](./docs/features/office-document-editing/README.md)。
### Agent Runtime 与模型连接
- [x] **子任务历史去重与空间回收**:本机与托管 SSH 子任务事件只存变化,不再反复保存完整进度。首次升级自动转换旧记录并回收空间,显示进度且支持退出重试;保留聊天、执行详情、结果和远程事件去重。已随 0.13.2 发布的 schema 35 补修还会整理升级到 0.13.1 后重复写入的工具块,不删除事件;终态及时释放写入缓存,远程重复重放不重建已释放的缓存。旧客户端不能打开升级后的数据库,具体规则见[执行记录存储与升级回收](./docs/features/assistant-workbar/execution-history-storage.md)。
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。连续请求自动复用最近一次成功生成的图片和文字历史,已保存会话重开后仍可继续修改,本轮显式附图优先;上游明确不支持编辑时继续按文字生成,仅在回复底部提示未使用参考图。编辑使用 multipart 上传图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。详见[图片生成与连续修改](./docs/features/image-generation/README.md)。
- [x] **当前会话图片工具**:为图片模型开启会话调用后,支持工具调用的聊天模型可在 Execute 中生成图片、修改上传图片或历史成果,无需另开会话或手动切换图片模型。直连文本模型、本地 OpenCode、Continue、DeepSeek Harness 和托管远程 OpenCode 复用统一服务,Runtime 分配由模型设置派生;图片请求可能产生费用,编辑需服务商支持。详见[实现与验证记录](./docs/features/conversation-media-generation/progress.md)。
- [x] **跨项目 Runtime 进程复用**:兼容配置的本地 OpenCode 和 DeepSeek Harness 共享重型进程,托管 OpenCode 会话在同一 Host 上复用进程。项目适配器及会话的工作区、模型、工具、问答和取消仍独立路由;远程待答问题和任务状态可在重连或桌面重启后恢复,不重发已接受请求。新的远程行为需要 Desktop `0.13.5` 与 Agent `0.13.0` 配套更新,详见[设计与实测](./docs/features/assistant-workbar/runtime-process-reuse-technical-design.md)。
- [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 的原生参数发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并在 Execute 中应用多文件补丁。Ask 限于工作区只读搜索和读取,Execute 按当前账号权限搜索;用户无需另行安装 ripgrep。搜索保留原生输出与退出码,大结果通过 `output_read` 完整续读,可处理的参数错误允许模型调整后继续。
- [x] **长上下文与工具输出分页**:历史消息、长回复和文档解析文本不再采用旧的固定截断限制。直连模型可分页续读已保存的搜索、命令和 Subagent 输出;所选模型的上下文窗口及传输边界仍然有效。
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、启动与控制请求时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。托管的本机和远端 OpenCode 跳过界面未使用的自动 Git 快照,避免同步差异计算拖住会话,文件工具、子代理与工作区 Git 差异保留;Continue 持续排空不用展示的宿主 stdout,防止控制台输出填满管道。这些调整随新启动的 Runtime 生效,不改写外部 OpenCode Server 配置。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
- [x] **托管 SSH OpenCode 与 Continue**:该能力由“设置 > 平台功能”中的独立“远程项目”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。当前 Agent 可以常驻,更新替换的旧 Agent 排空活动任务后退出。任务完成后回收空闲 Runtime,共享进程仍有任务或待答问题时保留;下一轮重建 Runtime 并恢复对话历史。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node、OpenCode 和 Continue 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。Continue 需要更新后的联合包与配套 Desktop。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。历史 macOS OpenCode 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证,不代表 Continue 的同等实机覆盖或新版本已经发布。当前源码候选为 Agent `0.13.4`、Desktop `0.13.14`;详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以独立发布渠道为准。
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
- **SSH Host 环境准备验证记录**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。现有记录未覆盖 Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵,候选 CI/原生打包与系统休眠唤醒也尚未验证;开发记录不代表版本已经发布。
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
- [x] **远程原生问答与取消修复**:Agent `0.11.24` 修复远程原生问答转交和取消待答后同一会话续发,需要先升级 Desktop 至 `0.13.0`。
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。Windows Host 启动和 ACP 会话共用规范工作区路径,同目录不同写法可复用正常 Runtime,不再因路径不一致而创建会话失败。
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方代码不受 Ask 初始化隔离。
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权,包括工作区外路径及原生子代理工作。
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,本机与托管 SSH OpenCode 同时转交属于当前请求的子会话提问。托管 SSH OpenCode 问答需要 Desktop `0.13.0`、Agent `0.11.24` 和新启动的托管 Runtime,不扩展到任意 ACP 服务。回答或跳过成功后在原位置保留问题与答案,支持多轮记录和本地会话重载;旧版本已丢弃的答案不能恢复。并行问题按顺序等待回答,重复事件保留草稿,提交失败可重试;取消远程待答后可在同一会话继续发送。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
- [x] **原生执行清单**:OpenCode 和 Continue 在对话顶部更新只读执行清单,进度随会话保存。显式清空和远程重放保留请求归属,取消不把未完成条目标成完成;远程交付需要配套 Agent,详见[清单契约](./docs/features/assistant-workbar/runtime-checklist-technical-design.md)。
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求 Ask/Execute 模式,可使用已启用的本机直连模型工具,Ask 仍只读;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
- [x] **OpenCode 事件连接回收**:聊天与原生上下文压缩在结束事件迭代前关闭各自订阅,覆盖完成、失败、取消和消费方提前结束,不取消其他并行会话。
- [x] **准确的消息底部状态**:区分请求准备、等待重试、开始重试、工具活动、待回答和终态;本机 OpenCode 使用原生重试次数和计划时间,直连模型显示自身退避阶段。消息底部状态点保持静态,未接入的 Runtime 重试不编造提示,会话列表闪点及发送、停止行为不变。
- [x] **直连模型命令工作目录**:Execute 命令可通过绝对路径、相对路径或符号链接使用工作区外目录;工作区仍为默认目录和相对路径基准,Ask 保持只读。
- [x] **简化 Runtime 选择**:会话与项目菜单不再枚举每个 Runtime 与模型的组合,Runtime 模型在系统设置中配置;已保存的项目固定模型仍可使用,并明确显示其固定模型。
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
- [x] **模型请求定制**:每个 LLM 连接可填写有界 JSON 自定义请求头和顶层请求体。直连与 Continue 支持两者;本地 OpenCode 和 DeepSeek Harness 只使用各自原生支持的请求头,受管 SSH OpenCode 模型网关支持两者。Runtime、协议和认证字段始终优先,这些普通连接设置不能用于保存 API Key 或其他密钥。详见[模型连接请求定制](./docs/features/model-connections/README.md)。
- [x] **上下文用量与自动压缩**:直连模型按每次成功调用更新供应商用量,图片与工具轮次使用同一口径,供应商缺失 usage 时才回退估算;界面明确区分“本次模型调用”和“压缩后对话估算”,压缩线始终根据当前设置与所选模型窗口即时计算,不在每个对话中保存旧配置;压缩标识的前后值使用同一估算口径,运行记录仍保留各次模型调用的供应商 usage。对话与多轮工具 Agent 可在已完成调用越过阈值后自动重复压缩,规划时先为固定提示、工具定义和摘要预留预算;同一回复会分别保留 Agent 工具上下文与对话历史的压缩标识,并在应用重启或较早消息滚出本地历史窗口后继续复用摘要。
- [x] **持久凭据保护**:API Key 由 Main 使用系统安全存储加密且不暴露给 Renderer。托管 SSH accepted Prompt 只把当前 profile 与密钥放入 Agent 内存,不写入 SSH 参数、远端环境或磁盘;其他路径仍在 Main 内使用。密钥随对应模型连接保存,修改服务地址或临时切换为无需认证不会要求重新输入;只有用户显式清除凭据或删除连接时才移除。
- [x] **有界故障诊断**:Desktop 在用户数据目录中轮转保存启动、Runtime 和远程连接的固定阶段失败,最多 4 个 256 KiB 文件;GoodBuddy Agent 在各 installation 的私有 state 目录中轮转保存 daemon、连接、恢复和 Runtime 生命周期,最多 3 个 64 KiB 文件,并可通过固定 `diagnostics --installation-id` 命令读取。两端都只记录白名单阶段、稳定错误码/类型和固定短消息,不保存 Prompt、凭据、文件内容、路径、环境、SSH 参数或 Provider 原始响应;诊断写入失败不改变正常运行。
- [x] **OpenCode Runtime 定制**:GoodBuddy 管理的内置 OpenCode 可发现原生 Agents、Tools、Commands、LSP、Formatters、MCP、Skills、Prompts 与 Resources;Tools 单独显示读取、文件修改、命令、网络、Agent 编排等类型、来源及 Ask/Execute 可用性,并隐藏 OpenCode 内部 `invalid` 与 GoodBuddy 临时 MCP 工具。支持保存默认 Agent、每次请求覆盖 Agent、通过原生 SDK 执行 Command、显示上下文用量并调用有总时限的原生 Compact;并发外部 Server 对话的提问使用请求级公开 ID 映射,回答不会串到其他会话。外部 OpenCode Server 只报告连接状态,不宣称原生清单可读。任意插件安装、Session Share、自动 Worktree 和 OpenCode 原生会话持久化仍不开放。
- [x] **Continue Runtime 定制**:提供静态配置中的原生 Rules、Prompt 模板与 MCP 清单,以及可编辑的 GoodBuddy Rules/Prompt 配置预设;聊天可按请求选择预设和填入可继续编辑的 Prompt。当前 Continue Host 没有可信的静态原生 Tool 发现接口,且使用隔离的 `CONTINUE_GLOBAL_DIR`,因此界面明确标记 Tools 不支持静态发现,也不把 Host 实际不会加载的工作区或用户 Skills 冒充原生能力;GoodBuddy 分配的 Skills 仍按请求暂存执行。Continue 临时 Host 不复用原生会话压缩,手动压缩由 GoodBuddy 摘要模型完成并验证持久化摘要覆盖范围;Agent 交互提问转换为统一问答卡片。Resources、Hooks、后台 Job 和 Continue 原生会话管理继续暂缓。
- [x] **Runtime 原生清单语义**:原生能力以 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 个页签展示;清单状态独立于 Runtime 连通性,区分完整、部分、不可用、仅连接和不支持。DeepSeek Harness 通过 Host Registry 枚举有界的内置/插件 Tools 与 Skills,显示真实 Ask/Execute 边界,并排除 GoodBuddy 按请求分配的 Skills、Web/MCP 代理。
- [ ] **Runtime 监督栏目**(规划中):在应用级助手工作栏的固定 Runtime 栏目统一承载 OpenCode、Continue 和 DeepSeek Harness 的 Task 级委派、后台执行、Workflow/Hook、长任务与原生会话监督;用户只选择 Conversation 或 Task,Job/Run 保持内部,不形成树或独立操作对象。
- [ ] **Subagent 高级监督**(规划中):在工作栏固定 Runtime 栏目按 Task 聚合,提供可配置的嵌套、并行、预算和生命周期控制。基础专家模式继承与单层直连模型编程委派已经可用。
### Skills、MCP 与知识库
- [x] **Skills 按需接入**:可分配给直连模型、OpenCode、Continue 和 DeepSeek Harness,并使用有界资源和受控 Runtime 边界。
- [x] **本机工具执行环境源码链路**:在“能力与工具 > 工具执行环境”中为本机 Skills 与 stdio MCP 选择 GoodBuddy 托管 Node.js、按需安装的托管 Python,或经过真实验证的自定义解释器;提供独立的原生地址/OSS 镜像选择、诊断、安装进度、取消和删除。新的本机 Runtime 与 stdio MCP 获得不可变 PATH 快照,不修改普通终端、系统环境或远程 Host。Windows x64 托管 Node 与原生地址 Python 已通过真实安装验证;托管 Python 归档按目标文件系统验证,Linux 保留大小写不同的合法路径,所有目标仍拒绝完全重复路径和不安全条目。
- **本机工具执行环境验证记录**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv。不可变的 `v0.12.0` 尝试在发布前通过 Windows 与 macOS,但暴露了 Linux TAR 大小写处理问题;`v0.12.1` 在同步 Agent 发布时于原生打包前取消。已发布的 `v0.12.2` 随后通过全部六个原生打包任务及其托管 Python 安装探针。现有记录未覆盖真实 Skill/MCP、自定义解释器和运行中进程协调。
- [x] **内置 MCP 按需接入**:知识库、魔法笔记、GoodBuddy 配置与内置浏览器 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;DeepSeek Harness 在设置中明确显示为暂不支持。内置 MCP 仅通过当前请求的短期本机权限提供,Ask / Execute 读写边界不受用户配置放宽。
- [x] **MCP Tools**:显式启用的自定义 MCP 可按 Runtime 分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent Execute 和 DeepSeek Harness,并仅在 Execute 加载;Agent 子进程只获得按请求签发的本机回环权限,MCP 地址、命令和凭据保留在 Main,动态工具仍经过发现、执行记录与权限边界。直连模型与 Harness 的健康调用复用当前请求已发现的工具清单,不在每次调用前重复发现。
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
- [x] **外部知识库接入**:在知识库页面管理 Dify、FastGPT 和 RAGFlow 实例,通过目录或手工 ID 验证并绑定远端知识库,保留 Provider 专属检索配置和引用;对话支持按需检索及每次回答前检索。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
- **外部知识库验证记录**:三家 Provider 已通过真实服务、本机 HTTP MCP 和生产 IPC 短答案检查,Dify 另通过完整 App 输入区到回答及引用的链路。现有记录未覆盖 FastGPT/RAGFlow 桌面专属参数、复杂多库问答和远程回答前检索,详见[验证记录](./docs/features/knowledge-base/progress.md)。
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
模型抽取不再额外限制为 8192 tokens;OpenAI 使用服务商上限,Anthropic 使用模型连接的输出设置。
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
- [x] **混合检索测试台**:支持全文、中文词组、向量和图谱通道诊断,可调 Top K、阈值、权重、本地或学习型重排及上下文预算。
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论。笔记从自适应卡片总览打开;独立待办页提供搜索、状态筛选、分组列表和直接勾选,宽屏列表稳定在左侧,任务说明、来源记录及 AI 评论在右侧同一区域显示,窄屏按需显示详情并提供返回列表。紧凑页签放在页头新建按钮左侧,筛选放在搜索右侧。打开原笔记再返回可恢复任务上下文;切换任务不会混入上一项的来源内容,读取失败使用应用通知并提供就地重试,来源缺失有明确提示。左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。Agent / MCP 写入后,已打开的工作台自动更新,并保留当前选择与未保存草稿。
- 待办宽屏列表与详情之间可拖动分隔条并记住宽度;来源笔记分组可独立折叠,不清除当前选中任务的详情。
- [x] **连续记录工作区**:笔记详情连续展示全部记录,左侧索引默认 168px,可拖动右边界或用键盘在 140–320px 内调宽,并记住偏好;索引与右侧 AI 评论独立收起。每条画布记录显示真实首页缩略图、页数和时间,文字记录显示摘要;点击索引只滚动定位,保留当前编辑器和草稿。打开笔记就在标题下方显示空白输入区,保留文字/画布选择和保存操作;新增保存后清空,供下一条记录使用,旧记录通过编辑入口修改。空白输入区和保存后未再修改的记录返回时不提示;保存失败和真实修改仍受保护。窄屏索引使用固定宽度折叠抽屉,不显示调宽分隔条,AI 评论移至底部。
- [x] **分页画布笔记**:已集成 PeopleLib Fabric + Quill 编辑器,支持跨页正文、笔迹、高亮、对象选择及变换、浮动文字、图片和纸张模板。PDF 可作为分页底版导入并提取原生文字;正文和批注可导出为栅格 PDF,导出文件不保留可搜索文字层。用户手动保存后,记录正文与二进制资源存入本地笔记文件,SQLite 保留元数据、索引、修订、待办和评论。备份须协调保存 SQLite 与笔记目录,产品无一键备份入口。撤销/重做限当前批注页或 Quill 模式,不提供全局文档撤销、无限画布或 PNG 下载按钮。草稿、已保存记录和来源为画布的待办按默认模型图像输入能力使用页面图像加文字,或明确标注仅文字降级;“发送画布页数”可选 1~8,默认 1,按当前顺序发送前 N 页,文字和图片范围一致,保存与导出仍支持 50 页。纯视觉记录/草稿需要支持图像输入的模型。即时评论模式提供手动画布分析,保存后自动分析失败不阻断或回滚保存。提取文字不变时保留文字评论;布局变化使视觉评论失效,并在保存后自动评论模式下重新分析。MCP 禁止以纯文字覆写画布记录。实现已完成,最终验证状态见[功能进展](./docs/features/magic-notes/progress.md)。
编辑与只读画布均支持 25%–300% 视图缩放、100% 还原及随视口调整的“适应宽度”,不改变保存内容或导出尺寸。
首次升级将历史正文迁移到文件,并随附件变更把数据库升级至 schema 39;旧客户端不能再打开,回退需使用升级前备份。
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
### 工作管理、长期协作与工作流
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;用量按项目或会话分组时默认收起并显示汇总,展开后显示 Runtime / 模型子行,按模型分组时保持平铺;活动按会话分组并默认收起,避免长历史占满页面。
- [x] **紧凑会话工具记录**:逐项展开工具记录即可查看和复制结果、错误及输入参数;会话与子任务进度使用简洁行,减少重复 Runtime 摘要。OpenCode、Continue、DeepSeek Harness 和直连模型可显示已有文件路径、命令或搜索摘要,并在工具完成和会话重载后保留。
- [x] **Task 与定制任务体验**:每个产品级 Task 只关联一条 Conversation,一条 Conversation 可承载多个 Task;左侧会话列表通过行首展开按钮显示带共享状态点的 Task 子项,父会话行不重复任务标签,UI 只展示到 Task,不暴露 Job/Run 层级。新建定制任务可关联当前或新 Conversation,默认 Execute 并沿用 Runtime、工具和审批边界;重复触发复用同一 Task,文本结果回写 Conversation,独立文件和图片保留为成果。普通消息与到期 Scheduled Task 共用 Conversation 级持久发送队列,同一会话一次只执行一项;当前回复期间仍可继续发送,队列按顺序续跑,并允许删除或“立即中断并插入”。Task Center 继续作为完整索引,不建设独立 Automation Center。当前计划触发支持单次、每日和每周;高级时区、Cron、事件触发与重试治理仍按 PRD 逐步实现。详见 [Task Center PRD](./docs/features/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md)。
- [x] **记忆与智能心跳**:当前提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
- [x] **定时消息沿用会话**:定时消息使用会话当前历史、Runtime、工作模式和已保存的知识检索设置;任务详情显示实际运行模式,当前 occurrence 未结束时禁止重复“立即运行”。
任务中心通过状态数量和筛选定位运行中及待处理任务,审批操作保留在对应任务卡片中。
- [x] **自动监督设置与范围**:“监督者 > 自动监督”统一管理计划、自动监督报告和建议,支持创建和编辑 Global 或指定一个、多个 Project 的每日、每周计划;工作回顾阅读带日期的统一监督结果及历史。旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置中心不再复制心跳表单;按分区回顾、候选生成和唤起条件仍待设计。详见 [智能心跳 PRD](./docs/features/smart-heartbeat/prd.md)。
- [x] **监督者回顾、故事线图谱与活动**:按范围和时间回顾工作,查看保存结果的图谱及来源,确认或修订实体,预览写入本地知识库实体。侧栏可跟随或固定 Conversation、Task,并打开匹配结果。活动合并心跳报告与下游监督的阶段状态,保留失败和旧结果入口。自动阶段只处理新增、修改及未处理的来源片段,无变化时跳过模型;手动回顾重新整理有界历史。回顾只读且禁止工具,模型调用可能产生费用,详见[实施与验证记录](./docs/features/conversation-supervision/progress.md)。
- 手动回顾分页处理所选历史,并按批保存事实与来源;支持暂停和从保存进度继续。设置提供并发、超时、批次大小、执行预算和响应容量,活动记录显示实际阶段与已保存批次详情;工作回顾和活动记录使用面板可用全宽。
- [ ] **监督后续能力**(规划中):逐事件完整回放、Experiment 目标、人工修改时间序列审计、事件触发及更完整的执行观察仍未完成。当前自动回顾受输入预算和滚动窗口限制,删除报告不会重置处理进度或重建图谱。详见 [会话监督 PRD](./docs/features/conversation-supervision/prd.md)。
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
- [ ] **可视化受控工作流**(规划中):提供版本化 DAG、条件分支、审批、取消和恢复,执行节点继续经过 Main Runtime 边界。
- [ ] **统一 Run Graph 与回放**(规划中):关联任务、Subagent、模型、知识、工具审批、用量和成果,支持失败定位、重试和脱敏导出。
### 浏览器、通信、语音与应用维护
- [x] **Runtime 共享内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过内置 MCP 总开关和 Runtime 分配决定是否提供给 Execute,开启后不逐次询问。关闭 Agent 浏览器能力不影响用户继续在浏览器工作栏中手动前往、返回、刷新、停止加载、交互或关闭浏览器。直连模型、GoodBuddy 管理的 OpenCode 和 Continue 通过请求级权限共享按 Conversation 归属的会话和串行操作路径,用户导航会改变 Agent 下一步看到的页面。
- [x] **浏览器多页签与按需资源**:同一对话可打开多个共享登录状态、页面与导航独立的浏览器页签,模型请求固定使用开始时绑定的目标。未使用的请求预留不创建浏览器资源,已释放页面可重新打开;保留显式截图,普通操作不再触发无用的自动截图。
- [x] **浏览器关闭与刷新**:AI 使用中的页签也可关闭,不取消整个请求;后续显式导航会创建独立替代页。空白页不能刷新,切换或关闭面板后清除旧操作错误。
- [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。
- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
- [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。
- [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
- [x] **受管本地模型下载源**:在“平台功能 / 通用设置”中为后续语音输入与 OCR 模型下载全局选择 ModelScope(默认)或 Hugging Face;所选来源缺少完整已验证文件时明确不可用,不静默换源或混合文件。
- [x] **可选本地语音模型管理**:应用不内置模型权重;提供校验下载、进度与取消、来源链接、ZIP 或本地目录导入、切换和删除。
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
- [x] **窗口恢复入口**:渲染失败时显示本地化的重新加载页面,渲染进程崩溃时提供原生恢复确认;重新加载可能丢失未保存输入。详见[窗口恢复说明](./docs/development/window-recovery.md)。
- [x] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;默认不上传诊断,用户可主动附加有界的最近桌面诊断摘要。失败后保留草稿、诊断选择和同一请求编号;不会附加对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
- [x] **内网兼容**:允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书;反馈、微信凭据与媒体等固定平台服务仍保持严格证书校验。
### 开源、构建与发布
- 当前源码候选为 Desktop `0.13.14`、Agent `0.13.4`,OpenCode 固定为 `1.18.29`、Continue 为 `1.5.47`;正式发布状态以独立发布渠道为准。
- Desktop `0.13.14` 改善监督者分页回顾与续跑、ripgrep 原生搜索、待办布局和远程结果未知后的显式续聊。数据库升级至 schema 47,回退旧客户端需使用升级前完整备份。
- Agent `0.13.3` 回收空闲 Runtime,并使被替换的 Agent 等已有工作结束后退出;复合包要求 Desktop `0.13.12`,请先升级 Desktop,再更新 Host 环境。Node 保持 `24.19.0`。
- Agent `0.13.4` 在完成前等待模型交付确认,空闲 Runtime 回收失败不改变已完成结果,并在环境更新后清理已确认停用的旧程序及所属孤儿进程;保留历史、活动及状态不明的安装。独立包要求 Desktop `0.13.14`,请先升级 Desktop,再更新 Host。
- 发布准备要求本地全量测试、发布说明校验、类型检查及 lint;候选主分支 CI、生产构建、原生打包及公开发布核验为独立验收步骤。本轮不做本地生产构建、打包或龙芯预览,当前验证状态见[发布准备记录](./docs/development/release-preparation-0.13.14.md)。
- Desktop `0.13.12` 和 Agent `0.13.3` 已完成原生发布流程,但不替代开发期真实 Host 场景覆盖。
- 验证记录与功能状态分开。存储已有本地及 Linux x64 Host 真实工具任务、多项目多会话、取消和无损迁移验证;进程复用有 Windows 完整 App 与 Linux x64 Host 证据,PPTX OCR 与空闲内存释放有 Windows 真实文件证据。图片工具已有真实服务商生成/编辑及 Host 传输证据,这些记录未覆盖完整界面自然语言调用和本地/远程切换。候选仍需主分支 CI 及原生发布打包,详见[Runtime 验证记录](./docs/features/assistant-workbar/progress.md)与[图片工具验证记录](./docs/features/conversation-media-generation/progress.md)。
- 发布准备前,`9836a4c` 已通过本地 4,858 项测试(67 项跳过)、完整类型检查和 lint;真实服务商画布分析仍未验证。该提交的三个原生 Agent CI 构建通过;桌面 CI `35485936807` 为 4,875 项通过、8 项 Electron 测试失败,均因 Linux 沙箱辅助程序未配置,在页面断言前退出。候选配置该程序并在 Xvfb 下运行测试,不关闭沙箱;候选本地验证、主分支 CI 与原生标签打包分别记录,不在发布准备中运行本地生产构建或打包。
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
- [x] **桌面 Runtime 包校验**:OpenCode 离线依赖复制避开 electron-builder 对源根目录 node_modules 的排除,DSH 会话投影组件打入解包后的 Host。最终包逐文件核对离线依赖并拒绝外部 DeepSeek 导入;Windows x64 CI 还会导入包内 OpenCode 插件、验证真实 DSH UtilityProcess 握手,通过后才发布。
- [x] **DMG-only 更新清单**:当前 Desktop 和官网读取器同时接受仅 DMG 与历史 DMG/ZIP 清单。旧版 macOS 的 GitHub 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
### 开放接口、团队协作与远程执行
- [x] **远程 Continue 交付**:已接入 CN 1.5.47 受管包、Agent HTTP-to-ACP 适配、模型路由、项目选择和原生清单。默认包同时携带 OC/CN;Windows 桌面到 Linux x64 已通过安装、真实模型清单更新、长清单、取消及重启恢复验证。Execute 会话 MCP 已通过真实文本模型 Host 往返,图片服务使用替代接收端,不代表真实生图验证。候选 Agent `0.13.4` 需要 Desktop `0.13.14`;正式发布及原生平台验收与 Linux 开发证据分开,详见[验证记录](./docs/features/remote-host/runtime-checklist-validation.md)。
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
- **SSH 主机与远程执行空间验证记录**:主机 CRUD、Host Key、加密凭据、Project UI、Workspace、OpenCode ACP v5、Agent-owned Prompt/gateway/transcript、Ask 只读、Execute 完整账号权限、取消、detached Agent 精确重连和 release-only 双架构资源校验已经接线。Linux x64 当前源码已用真实模型与工具通过 detach、进程结束、relay 丢失、并发、取消、Provider 异常、Agent 重启和 Desktop SQLite 恢复矩阵;当前源码测试签名 Agent `0.11.17` 又通过两个并发 Ask 读取、一次 Execute 写入并读回,以及 6 次带自定义 Header 和 Body 的真实受管网关模型请求,未发现 Provider 请求重放,隔离 Agent state 中也没有 API Key 或自定义请求值。公开 signing key registry 已供应。控制面直连源码不等待新 installer-bearing 包;现有记录未覆盖当前 Linux x64/arm64 正式签名工件的完整公开核对及 GitHub/北京镜像、双架构和离线 GoodBuddy 传输矩阵,发布结果另行记录。
- [ ] **多云远程沙盒 Agent**(规划中):通过云厂商 API 和 SSH Agent 管理专用 Linux 沙盒;凭据留在 Main 进程,高风险控制面操作单独确认。
## 规划原则
规划中的工作流、Subagent、MCP、远程 API 和沙盒能力不得绕过现有 Main Runtime、Ask/Execute、权限、取消、超时和审计边界。
+12
View File
@@ -0,0 +1,12 @@
Copyright (C) 2026 MesaLogo
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
+149 -73
View File
@@ -1,102 +1,178 @@
<div align="center">
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
# GoodBuddy
面向专业工作与国产化环境的安全桌面智能助手。
**Desktop access to model chat, coding agents, knowledge, and scheduled work**
GoodBuddy 将模型连接、Agent Runtime、本地知识库、知识图谱、任务协作和持续成长能力组织在同一个桌面工作空间中。它不是简单的聊天窗口,而是一套可审计、可控制、可长期使用的个人智能工作环境。
**English** | [简体中文](./README.zh-CN.md)
![GoodBuddy 工作空间](docs/screenshots/workspace-overview.png)
[Website](https://mesalogo.github.io/goodbuddy/en.html) ·
[Download](https://github.com/mesalogo/goodbuddy/releases) ·
[Feature Matrix](./FEATURES.md) ·
[Build Guide](./BUILD.md)
## 为什么选择 GoodBuddy
[![Release](https://img.shields.io/github/v/release/mesalogo/goodbuddy?label=release)](https://github.com/mesalogo/goodbuddy/releases)
[![License: 0BSD](https://img.shields.io/badge/license-0BSD-blue.svg)](./LICENSE)
[![Windows](https://img.shields.io/badge/Windows-x64%20%7C%20arm64-0078D4)](https://github.com/mesalogo/goodbuddy/releases)
[![macOS](https://img.shields.io/badge/macOS-x64%20%7C%20arm64-000000)](https://github.com/mesalogo/goodbuddy/releases)
[![Linux](https://img.shields.io/badge/Linux-x64%20%7C%20arm64-FCC624)](https://github.com/mesalogo/goodbuddy/releases)
</div>
### 安全可控的 Agent 执行
![GoodBuddy workspace](./docs/screenshots/workspace-overview.png)
GoodBuddy 通过统一的 Agent Runtime 控制层接入直连模型、OpenCode 和 Continue。工具不会被直接暴露给界面,所有执行都受到工作模式、权限审批和运行边界约束。
GoodBuddy is a desktop application for model chat, coding agents, knowledge
retrieval, notes, and scheduled tasks. No GoodBuddy account is required.
Workspace data is stored locally by default. Model endpoints can run on the
same device, on a private network, or at a provider selected by the user.
- `Ask`:只读问答,不调用工具。
- `Execute`:选择该模式即授权当前交互运行使用已启用的受控工具。
- 可在设置中禁止直连模型执行所有工具;工具调用仍记录到活动。
- 统一处理取消、超时、输出边界、进程退出和异常恢复。
## Highlights
### 数据主权与本地优先
- **Local-first workspace**: No GoodBuddy account is required. Workspace data
stays local by default, and users choose where model services run.
- **Unified Agent Runtimes**: Use direct models, OpenCode, Continue, and the
preview DeepSeek Harness from one desktop interface.
- **Explicit execution boundaries**: `Ask` remains read-only, while `Execute`
uses the current account's permissions. Tool activity, cancellation,
timeouts, and token usage are recorded.
- **Broad platform coverage**: Official Linux releases cover `x64` and `arm64`,
including compatible distributions used in China's domestic computing
ecosystem. A separate
experimental `loong64` preview is available for LoongArch systems.
- **Knowledge and long-running work**: Knowledge bases, graphs, Magic Notes,
tasks, and Smart Heartbeat share one desktop workspace.
- 会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite。
- API Key 通过系统安全存储加密,不以明文写入配置。
- Electron Main、Preload、Renderer 严格分层,Renderer 仅能使用类型化 IPC。
- 子进程使用环境变量白名单,避免继承无关凭据。
- 默认不依赖 GoodBuddy 云端账户,也不代理用户的模型流量。
Platform coverage describes operating-system, instruction-set, and package
compatibility, not certification for every hardware vendor or distribution
release. LoongArch is outside the official release matrix; see the
[LoongArch preview notes](./docs/development/loongarch-preview-build.md) for its
current limitations.
### 面向国产化环境交付
## Capabilities
- 支持 Windows、macOS 与 Linux。
- 支持 Linux `x64` 和 `arm64`。
- 提供适用于麒麟、统信 UOS 等 Debian 系桌面的 `deb` 安装包。
- 提供 AppImage,便于免安装验证与便携分发。
- 支持 Anthropic Messages、OpenAI Chat Completions、OpenAI Images 与无认证本机模型。
- 可连接企业网关、私有模型服务和国产模型适配层。
| Area | Available in GoodBuddy |
| --- | --- |
| Agent Runtimes | Direct model connections, OpenCode, Continue, and the preview DeepSeek Harness |
| Data and execution | Local SQLite and note-file storage, read-only `Ask`, full-account `Execute`, and recorded tool activity |
| Knowledge | File, folder, and web imports with full-text, Chinese phrase, vector, and knowledge graph retrieval |
| Desktop releases | Official Windows, macOS, and Linux builds for `x64` and `arm64`, plus an experimental LoongArch preview |
| Integrations | Local or hosted model endpoints, custom MCP servers, WeChat ClawBot, WeCom, and DingTalk |
## 核心功能
## Product tour
### 一体化智能工作空间
### Knowledge bases and graphs
- Projects 与独立对话上下文。
- 专家角色和最多三个只读专家并行分析。
- 任务、活动、成果、记忆和自动化集中管理。
- 支持文件、桌面截图、应用窗口、剪贴板和语音上下文。
- 显示真实 Git 工作区变更。
- 支持远程任务委派与持久化结果发件箱。
- Skills 与 MCP 能力按需接入。
![GoodBuddy knowledge workspace](./docs/screenshots/knowledge-workspace.png)
### 本地知识库与知识图谱
![GoodBuddy knowledge graph](./docs/screenshots/knowledge-graph.png)
文件、目录和网页内容可以按知识库独立管理。GoodBuddy 会完成解析、索引、检索和图谱构建,并保留可追溯的来源与证据。
### Magic Notes and Smart Heartbeat
![GoodBuddy 知识工作区](docs/screenshots/knowledge-workspace.png)
Magic Notes displays a continuous record stream, with a narrow left index and
independent right AI comments. Both panes can collapse; thumbnails scroll to a
record while preserving drafts. It supports rich-text entries and paged canvases with flowing text,
handwriting, highlights, images and PDF backgrounds. Import PDF text and export
annotated pages as a raster PDF. Canvas saves are manual; AI comments use the
default model's image-input capability, with an explicit text-only fallback when
images are unsupported. See the [Magic Notes documentation](./docs/features/magic-notes/README.md)
for behavior and final validation status.
- SQLite FTS5 全文检索与有界上下文召回。
- 支持规则、模型和混合图谱抽取。
- 支持实体、关系、别名、证据与来源位置追溯。
- 图谱可搜索、筛选、缩放和拖动节点。
- 支持实体编辑、合并以及关系维护。
- 文档解析包含压缩包展开限制、路径校验和敏感字段过滤。
![GoodBuddy Magic Notes](./docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy 知识图谱](docs/screenshots/knowledge-graph.png)
![GoodBuddy Smart Heartbeat](./docs/screenshots/smart-heartbeat.png)
### 智能心跳
## Agent Runtimes
智能心跳让 GoodBuddy 不只响应当前问题,还能定期回顾近期工作,沉淀长期记忆,发现风险,并将洞察转化为可处理的建议。
![GoodBuddy 智能心跳](docs/screenshots/smart-heartbeat.png)
- 按项目或全局配置周期回顾计划。
- 展示心跳健康、记忆沉淀、洞察发现和行动转化。
- 提供成长趋势、最新报告和可审计的运行轨迹。
- 建议记忆可确认或忽略。
- 后续任务可带入 Ask 对话、标记完成或忽略。
- 支持手动运行、暂停、恢复和安全删除计划。
### 多 Runtime 与模型连接
| 能力 | 适用场景 | 控制方式 |
| Runtime | Functions | Configuration |
| --- | --- | --- |
| 直连模型 | 问答、知识总结、受控工具执行、图像生成 | Ask 只读;Execute 自动授权已启用的工作区、浏览器与 MCP 工具,可设置为全部禁止 |
| OpenCode | 完整编码与工作区任务 | Execute 不弹 GoodBuddy 审批,保留 Runtime 自身权限、取消和活动记录 |
| Continue | Agent 编码与工作区任务 | Execute 不弹 GoodBuddy 审批,使用独立宿主、取消和活动记录 |
| Direct models | Chat, retrieval, image generation, and built-in tools | GoodBuddy model connections |
| OpenCode | Coding agents, native commands, tools, and skills | GoodBuddy-managed or native OpenCode configuration |
| Continue | Rules, prompt presets, and coding tasks | GoodBuddy-managed or native Continue configuration |
| DeepSeek Harness (preview) | A fixed host with optional plugin capabilities | GoodBuddy-managed OpenAI-compatible connection |
## 功能矩阵与路线图
Each run reports cancellation, timeouts, token usage, tool calls, and history.
Runtime-specific capabilities and configuration remain separate.
以下为仓库首页的简要路线图;完整能力说明、状态和重大规划统一记录在 [FEATURES.md](FEATURES.md)。
## Download
- [x] [跨平台桌面工作空间与安全上下文](FEATURES.md#桌面基础工作空间与上下文)
- [x] [多 Runtime、模型连接、Skills 与 MCP](FEATURES.md#agent-runtime-与模型连接)
- [x] [本地知识库、向量检索与知识图谱](FEATURES.md#skillsmcp-与知识库)
- [x] [任务、成果、记忆与智能心跳](FEATURES.md#工作管理长期协作与工作流)
- [ ] [本地录音与离线转写](FEATURES.md#浏览器通信语音与应用维护)
- [ ] [魔法笔记 / Magic Notes](FEATURES.md#知识工作空间与魔法笔记):本地优先的结构化笔记、可追溯摘录与受控 AI 整理。
- [ ] [Agent 框架、受控工作流与团队协作](FEATURES.md#agent-框架与协作能力)
- [ ] [多云远程沙盒 Agent](FEATURES.md#多云远程沙盒-agent)
Download a build from
[GitHub Releases](https://github.com/mesalogo/goodbuddy/releases):
`[x]` 表示当前已提供,`[ ]` 表示开发中或规划中;未完成项目不代表已包含在当前发布版本中。
| Platform | Architectures | Formats |
| --- | --- | --- |
| Windows | `x64`, `arm64` | NSIS, portable ZIP |
| macOS | `x64`, `arm64` | DMG, ZIP |
| Linux | `x64`, `arm64` | AppImage, DEB, RPM |
## 隐私说明
The experimental LoongArch `loong64` build is distributed through a separate
preview channel on the [product website](https://mesalogo.github.io/goodbuddy/#download).
模型请求只会发送到用户选择的模型连接。本地数据保存在当前系统的应用数据目录中;远程委派仅在用户显式配置端点和令牌后启用。面向纯内网部署的“内网兼容模式”默认开启,允许 HTTP 并接受无效、自签名或过期的 HTTPS 证书;可在“安全与数据”中关闭并恢复严格校验。
Desktop installers do not contain remote Agent payloads. Local projects do not
need an Agent package; managed SSH users download or import one explicitly in
Settings.
Code signing and macOS notarization are not configured yet, so the operating
system may display a security warning.
## Run from source
Requires Node.js 24 and npm:
```bash
git clone https://github.com/mesalogo/goodbuddy.git
cd goodbuddy
npm ci
npm run dev
```
See [BUILD.md](./BUILD.md) for build and packaging instructions.
## Documentation
- [Feature matrix and roadmap](./FEATURES.md)
- [Product feature documentation](./docs/features/)
- [UI design system](./UI-DESIGN.md)
- [Build and packaging](./BUILD.md)
- [Release runbook](./docs/development/release-runbook.md)
## Privacy and execution boundaries
- Model requests are sent only to services selected by the user.
- Local data stays in the operating system's application data directory by
default. API keys are encrypted by secure system storage.
- The Renderer has no access to raw Electron APIs or model credentials.
- The DeepSeek Harness plugin marketplace is off by default. Third-party
install scripts, initialization code, and Execute tools run with the current
user's permissions.
- Remote delegation is enabled only after the user configures an endpoint and
token.
- Private-network compatibility permits in-app HTTP and non-standard HTTPS
certificates. WeChat credential and media endpoints remain strictly
validated.
## Contributing
Issues and pull requests are welcome. Read [AGENTS.md](./AGENTS.md) first, then
run:
```bash
npm test
npm run typecheck
npm run lint
```
## Community communication
Scan the QR code to join the GoodBuddy WeChat group:
<img width="1250" height="1743" alt="8d6cd5dc3f347a0f3f1cf3844071b4d3" src="https://github.com/user-attachments/assets/1c92c1b8-d1f7-4270-be2c-b9543beadc11" />
## License
Original GoodBuddy code is released under the [0BSD License](./LICENSE). You may
use, modify, distribute, and commercialize it freely. Third-party components
and resources retain their respective licenses.
+144
View File
@@ -0,0 +1,144 @@
<div align="center">
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
# GoodBuddy
**模型对话、编程 Agent、本地知识库和定时任务的桌面入口**
[English](./README.md) | **简体中文**
[产品官网](https://mesalogo.github.io/goodbuddy/) ·
[下载](https://github.com/mesalogo/goodbuddy/releases) ·
[功能矩阵](./FEATURES.zh-CN.md) ·
[构建说明](./BUILD.md)
[![Release](https://img.shields.io/github/v/release/mesalogo/goodbuddy?label=release)](https://github.com/mesalogo/goodbuddy/releases)
[![License: 0BSD](https://img.shields.io/badge/license-0BSD-blue.svg)](./LICENSE)
[![Windows](https://img.shields.io/badge/Windows-x64%20%7C%20arm64-0078D4)](https://github.com/mesalogo/goodbuddy/releases)
[![macOS](https://img.shields.io/badge/macOS-x64%20%7C%20arm64-000000)](https://github.com/mesalogo/goodbuddy/releases)
[![Linux](https://img.shields.io/badge/Linux-x64%20%7C%20arm64-FCC624)](https://github.com/mesalogo/goodbuddy/releases)
</div>
![GoodBuddy 工作空间](./docs/screenshots/workspace-overview.png)
GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时任务的桌面应用。使用 GoodBuddy 无需注册账号,工作数据默认保存在本机。模型服务可以运行在本机、组织内网或用户选择的云端服务。
## 产品亮点
- **本地优先**:无需注册 GoodBuddy 账号,工作数据默认保存在本机,模型服务由用户选择。
- **统一 Agent Runtime**:在一个桌面界面中使用直连模型、OpenCode、Continue 和预览版 DeepSeek Harness。
- **执行边界明确**:`Ask` 保持只读,`Execute` 使用当前账号权限,工具调用、取消、超时和 Token 用量均有记录。
- **支持信创软硬件**:正式 Linux 版本覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境;龙芯 LoongArch 提供独立 `loong64` 实验预览版。
- **知识与长期工作一体化**:知识库、知识图谱、魔法笔记、任务和智能心跳共享同一个桌面工作空间。
信创支持按操作系统、指令集和安装包兼容性描述,不代表已完成所有厂商、整机和系统版本的兼容认证。龙芯版本尚未进入正式发布矩阵,具体限制见[龙芯预览版说明](./docs/development/loongarch-preview-build.md)。
## 主要能力
| 范围 | GoodBuddy 提供的能力 |
| --- | --- |
| Agent Runtime | 直连模型、OpenCode、Continue 和预览版 DeepSeek Harness |
| 数据与执行 | 本地 SQLite 与笔记文件存储、只读 `Ask`、使用当前账号完整权限的 `Execute`、工具调用记录 |
| 知识库 | 导入文件、目录和网页,通过全文、中文词组、向量和知识图谱检索 |
| 桌面版本 | Windows、macOS、Linux 的 `x64` 与 `arm64` 正式版本,以及龙芯 LoongArch 实验预览版 |
| 外部连接 | 本机或内网模型服务、自定义 MCP、微信 ClawBot、企业微信和钉钉 |
## 产品界面
### 知识库与知识图谱
![GoodBuddy 知识工作区](./docs/screenshots/knowledge-workspace.png)
![GoodBuddy 知识图谱](./docs/screenshots/knowledge-graph.png)
### 魔法笔记与智能心跳
魔法笔记连续展示全部记录,左侧窄栏索引与右侧 AI 评论可独立收起,点击缩略图滚动定位并保留草稿。
支持富文本记录和分页画布,可混排跨页正文、手写笔迹、高亮、图片与 PDF 底版,
导入 PDF 时提取原生文字,并将批注页面导出为栅格 PDF。画布由用户手动保存;AI 评论按
默认模型的图像输入能力分析,不支持图像时明确提示仅文字降级。具体行为与最终验证状态见
[魔法笔记文档](./docs/features/magic-notes/README.md)。
![GoodBuddy 魔法笔记](./docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy 智能心跳](./docs/screenshots/smart-heartbeat.png)
## Agent Runtime
| Runtime | 适用场景 | 配置来源 |
| --- | --- | --- |
| 直连模型 | 对话、知识检索、图片生成和内置工具 | GoodBuddy 模型连接 |
| OpenCode | 编程 Agent、原生 Commands、Tools 和 Skills | GoodBuddy 管理或 OpenCode 自有配置 |
| Continue | Rules、Prompt 预设和编程任务 | GoodBuddy 管理或 Continue 自有配置 |
| DeepSeek Harness(预览) | 固定 Host 与可选插件能力 | GoodBuddy 管理的 OpenAI 兼容连接 |
每次运行都会记录取消、超时、Token 用量、工具调用和执行历史。各 Runtime 的原生能力与配置仍分别管理。
## 下载
从 [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases) 下载:
| 系统 | 架构 | 格式 |
| --- | --- | --- |
| Windows | `x64`、`arm64` | NSIS、便携 ZIP |
| macOS | `x64`、`arm64` | DMG、ZIP |
| Linux | `x64`、`arm64` | AppImage、DEB、RPM |
龙芯 LoongArch 使用独立的 `loong64` 实验预览通道,可在[产品官网](https://mesalogo.github.io/goodbuddy/#download)选择下载。
桌面安装包不包含远端 Agent payload。本地项目无需 Agent 包;托管 SSH 用户可在设置中单独下载或导入。
当前未配置代码签名和 macOS notarization,操作系统可能显示安全提示。
## 从源码运行
需要 Node.js 24 和 npm:
```bash
git clone https://github.com/mesalogo/goodbuddy.git
cd goodbuddy
npm ci
npm run dev
```
构建与打包说明见 [BUILD.md](./BUILD.md)。
## 文档
- [功能矩阵与路线图](./FEATURES.zh-CN.md)
- [产品功能文档](./docs/features/)
- [UI 设计系统](./UI-DESIGN.md)
- [构建与打包](./BUILD.md)
- [发布流程](./docs/development/release-runbook.md)
## 隐私与执行边界
- 模型请求只发送到用户选择的服务。
- 本地数据默认保存在系统应用数据目录,API Key 由系统安全存储加密。
- Renderer 不接触原始 Electron API 或模型凭据。
- DeepSeek Harness 插件市场默认关闭。第三方插件安装脚本、初始化代码和 Execute 工具以当前用户权限运行。
- 远程委派只在用户配置端点和令牌后启用。
- 内网兼容模式允许应用内 HTTP 和非标准 HTTPS 证书;微信凭据和媒体端点仍执行严格校验。
## 参与贡献
欢迎提交 Issue 和 Pull Request。请先阅读 [AGENTS.md](./AGENTS.md),提交前运行:
```bash
npm test
npm run typecheck
npm run lint
```
## 社区交流
扫描二维码加入 GoodBuddy 微信群:
<img width="1250" height="1743" alt="8d6cd5dc3f347a0f3f1cf3844071b4d3" src="https://github.com/user-attachments/assets/e2cd4874-0b6c-40d4-8867-0a9e843a7cb5" />
## 开源许可
GoodBuddy 原创代码采用 [0BSD License](./LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
+941
View File
@@ -0,0 +1,941 @@
# GoodBuddy 统一界面设计系统
## 1. 目的与适用范围
本文定义 GoodBuddy 桌面端的统一界面规则,适用于聊天与最近对话、任务中心、知识库、
智能心跳、运行记录,以及后续新增的一级页面。
设计系统解决两类问题:
1. 统一跨页面的视觉语言、信息层级和交互反馈。
2. 保留不同工作场景所需的布局差异,不强行把阅读、工作台、仪表盘和数据视图做成同一种页面。
所有新界面必须优先使用本文定义的语义令牌、页面壳层和共享组件。现有界面迁移时应保持功能与数据语义不变,不以视觉统一为理由隐藏范围、状态或风险。
## 2. 核心原则
### 2.1 一致的语义,不要求相同的布局
- 相同含义使用相同的颜色、间距、控件和文案结构。
- 不同任务使用明确命名的页面壳层,不再为单个页面随意设置宽度。
- 页签只用于同级页面导航,分段控件只用于当前视图内的单选切换,筛选控件只改变当前数据集合。
### 2.2 范围始终可见
知识、任务、自动化和活动记录可能属于全局或某个项目。当前范围必须在页面标题区或对象标题旁持续显示,不能只通过入口位置、默认筛选或颜色暗示。
### 2.3 主任务优先
- 每个页面只保留一个视觉上最突出的主操作。
- 页面标题、范围、说明、主操作和导航按固定层级排列。
- 次要设置、筛选和批量操作不得与主任务争夺注意力。
### 2.4 状态明确且可恢复
- 加载、空内容、无结果、失败、只读和权限不足是不同状态,必须分别呈现。
- 可撤销操作优先提供撤销,不可撤销操作必须在执行前说明影响范围。
- 不依赖颜色单独表达状态或风险。
- 当前模型或 Runtime 及切换进度集中显示在会话输入区,顶栏不重复显示可用性标识。切换失败、自动状态检查失败或返回不可用时,通过应用通知说明原因;发送按钮保留关联的无障碍状态说明。
### 2.5 默认支持浅色与深色主题
组件不得直接使用原始颜色值。主题差异只在令牌层定义,业务组件仅引用语义令牌。
两种主题必须保持相同的信息层级,但不要求机械地反转明暗。浅色主题以白色主内容画布、冰蓝灰侧栏和轻微着色的顶栏建立空间关系;深色主题使用深海军蓝与蓝灰表面逐层提亮,避免纯黑。蓝色承担主要选择和交互,青绿色主要承担成功与可用状态,二者不得混用语义。
## 3. 设计令牌
令牌以 CSS 自定义属性实现。`:root` 提供浅色值,`[data-theme="dark"]` 覆盖深色值。组件样式不得新增只服务于单个页面的颜色、阴影、圆角或间距常量。
### 3.1 颜色令牌
| 令牌 | 用途 |
| --- | --- |
| `--surface-canvas` | 应用和页面底色 |
| `--surface-raised` | 卡片、面板、输入框和弹窗 |
| `--surface-subtle` | 次级区域和控件底色 |
| `--surface-muted` | 进度轨道和更弱的分隔表面 |
| `--text-primary` | 标题与主要正文 |
| `--text-secondary` | 辅助正文 |
| `--text-muted` | 占位、时间和弱提示 |
| `--text-on-accent` | 强调色或危险色表面上的文字 |
| `--border-default` | 常规边框 |
| `--border-control` | 输入框、分段控件等必须清晰可辨的控件边界 |
| `--border-subtle` | 区块分隔线 |
| `--accent`、`--accent-hover` | 强调文字、图标和交互反馈 |
| `--accent-solid`、`--accent-solid-hover` | 主按钮等需要反白文字的强调背景 |
| `--accent-selected`、`--accent-subtle` | 选中边框、选中背景和信息强调 |
| `--success`、`--success-subtle` | 成功状态 |
| `--warning` | 警告状态 |
| `--danger`、`--danger-strong` | 危险文本和状态 |
| `--danger-solid` | 最终确认按钮等需要反白文字的危险背景 |
| `--danger-border`、`--danger-subtle` | 危险入口和失败状态背景 |
浅色与深色具体值只在 `styles.css` 的主题根节点维护。状态组件必须同时显示文字或图标,不能仅靠颜色区分。
表面与边框使用规则:
- 浅色主题的阅读、编辑和页面主内容使用白色或接近白色的 `--surface-raised`;主侧栏使用更深一阶的冰蓝灰 `--surface-canvas`,顶栏使用弱于侧栏的次级表面。相邻区域必须可辨,但不能形成高饱和色块。
- 深色主题从深海军蓝画布开始,以蓝灰表面逐层提亮。不同层级优先依靠表面亮度与语义边框区分,不使用纯黑底色或无边界的大面积同色区域。
- 浅色侧栏中,导航与最近会话、最近会话与账户区之间的结构分隔线使用 `--border-default`。列表行之间或卡片内部的弱分隔仍使用 `--border-subtle`,不得为了增强结构而给每一项加重边框。
- 控件边界、焦点环和选中边框必须达到至少 `3:1` 的非文本对比度;正文、状态色和弱文本分别遵守无障碍对比度要求。
- 业务组件不得通过主题条件分支写原始颜色;新增视觉层级时先确认能否复用现有表面、边框和状态令牌。
### 3.2 间距令牌
采用 4 像素基准:
| 令牌 | 值 | 典型用途 |
| --- | --- | --- |
| `--space-0` | `0` | 显式取消间距 |
| `--space-1` | `4px` | 图标内部微调 |
| `--space-2` | `8px` | 紧凑控件、图标与文字 |
| `--space-3` | `12px` | 表单字段内部、紧凑列表 |
| `--space-4` | `16px` | 默认组件间距 |
| `--space-6` | `24px` | 区块间距、窄屏页面内边距 |
使用规则:
- 同一组件内部优先使用 `8px`、`12px`、`16px`。
- 同一区块内组件之间优先使用 `16px` 或 `24px`。
- 页面级区块之间优先使用 `32px` 或 `40px`。
- 不新增 `14px`、`18px`、`22px` 等非令牌间距。
### 3.3 字体令牌
界面英文和数字优先使用随客户端本地打包的 `Inter Variable`。简体中文优先使用平台原生 UI 字体:macOS 使用 `PingFang SC`,Windows 使用 `Microsoft YaHei UI`,以获得与系统渲染匹配的小字号 hinting;其他平台及缺失字形使用本地打包的 `Noto Sans SC Variable`。代码、标识符和原始日志使用等宽字体栈。字体不得通过运行时网络请求加载。
| 令牌 | 字号 / 行高 | 字重 | 用途 |
| --- | --- | --- | --- |
| `--font-caption` | `11px` | 时间、短标签和紧凑元数据 |
| `--font-body` | `13px` | 默认界面正文 |
| `--font-section-title` | `14px` | 卡片和区块标题 |
| `--font-page-title` | `24px` | 一级页面标题 |
使用规则:
- 业务组件通过 `--font-family-ui` 与字体尺寸令牌继承字体,不创建页面专属字体栈。
- 表单按钮、输入框、选择框和文本域必须继承界面字体,避免回退为原生控件字体。
- 连续阅读内容使用 `14px`,持久辅助信息不得小于 `11px`。
- 本地兜底字体资源必须随生产包交付,并同时包含 Inter 与 Noto Sans SC 的 OFL 许可证。
- 页面内不得通过同时放大字号、加粗和使用强调色制造多个同级主标题。
### 3.4 圆角、阴影与层级
| 令牌 | 值 | 用途 |
| --- | --- | --- |
| `--radius-control` | `10px` | 输入框、按钮、菜单项 |
| `--radius-card` | `14px` | 卡片和面板 |
| `--shadow-card` | 主题定义 | 卡片和选中分段控件 |
| `--shadow-dialog` | 主题定义 | 对话框和浮层 |
整体使用适度圆角:控件和卡片保持清晰、克制的几何轮廓,不使用胶囊化的大圆角替代信息层级。普通卡片通过表面色和边框区分,不默认添加阴影;输入区等需要从内容流中明确浮起的持续操作面板可以使用克制的 `--shadow-card`。菜单和对话框使用对应层级阴影,阴影只表示真实的浮层关系。不允许页面自行创建高于 `--z-dialog` 的层级。
### 3.5 动效令牌
- `--motion-fast: 120ms`:悬停、按压、焦点反馈。
- `--motion-normal: 180ms`:菜单、折叠区域、轻量视图切换。
- `--motion-slow: 240ms`:对话框和抽屉。
- 默认缓动使用 `ease-out`,退出可使用 `ease-in`。
- 在 `prefers-reduced-motion: reduce` 下移除非必要位移和缩放,仅保留即时状态切换。
## 4. 页面壳层
所有一级页面使用 `PageShell`。壳层负责水平居中、最大宽度、响应式内边距、页面背景和滚动边界,业务页面不得再次设置独立的整体最大宽度。
| 变体 | 最大内容宽度 | 适用场景 | 页面映射 |
| --- | --- | --- | --- |
| `reading` | `820px` | 连续阅读、单列编辑、对话撰写 | 聊天正文与输入区 |
| `standard` | `960px` | 常规列表与表单 | 最近对话 |
| `dashboard` | `1040px` | 指标、卡片网格、宽表格与审计数据 | 活动记录 |
| `supervisor` | `1440px` | 选择列表、图谱画布、来源详情三栏工作台 | 监督者(含智能心跳设置) |
| `master-detail` | 可用空间内流式铺开 | 左侧选择、右侧编辑或预览 | 知识库 |
共同规则:
- 宽度计算统一为 `min(100%, 变体最大宽度)`。
- 页面左右内边距在常规桌面窗口为 `32px`,中等窗口为 `24px`,窄窗口为 `16px`。
- 页面顶部和底部内边距默认为 `32px`。
- `master-detail` 的左栏建议为 `280px` 至 `360px`,右栏占剩余空间。分隔线和滚动容器属于壳层,不由内容卡片模拟。
- 聊天输入区可以粘附在 `reading` 壳层底部,但消息内容和输入区必须共享同一阅读宽度。
- 宽表格可在 `dashboard` 内容区内部横向滚动,不得撑宽整个应用窗口。
## 5. 信息层级
一级页面按以下顺序组织:
1. `PageHeader`
2. `PageTabs`,仅在存在同级子页面时出现
3. 页面级状态或重要提示
4. 筛选与批量操作工具栏
5. 主内容
6. 与主内容就近关联的分页或加载状态
一个页面只能有一个可见的一级标题。卡片标题不得重复页面标题。面包屑只在层级超过两级且返回关系无法通过侧栏或主从布局表达时使用。
### 5.1 PageHeader
`PageHeader` 统一接收以下内容:
- `title`:必填,简短名词或任务名称。
- `description`:可选,需要常显的简短上下文,不重复标题或帮助内容。
- `help`:可选,补充用途和操作说明,通过标题同行的 `InlineHelp` 按需展开,规则见 6.14。
- `scope`:涉及范围的数据页面必填,使用 `ScopeBadge`。
- `primaryAction`:可选,页面唯一主操作。
- `secondaryActions`:可选,最多两个直接显示,其余进入更多菜单。
- `status`:可选,用于只读、同步中、连接异常等页面级状态。
布局规则:
- 标题和范围徽标在同一信息组中,范围不得放入更多菜单。
- 主操作位于标题区右侧,窄窗口下换行到标题下方并保持靠左。
- 不在内容卡片中再次渲染同名标题。
- 标题区默认不粘附。只有主内容长且页面级操作需要持续可用时才启用粘附。
## 6. 共享组件
### 6.1 PageTabs
用于同一一级页面内的同级内容面板,例如监督者的“工作回顾”和“故事线图谱”。
- 使用 `tablist`、`tab` 和 `tabpanel` 语义,当前项使用 `aria-selected="true"`。
- 一级页面之间的导航由应用主导航承担,不复用 `PageTabs`。
- 标签保持短名词,不显示句号,不用页签承载开关或过滤条件。
- 项目过多时优先重组信息架构,不把一级页签做成多行。
- `PageTabs` 可使用默认视觉或共享的 `segmented` 视觉变体。紧凑主从工作台中的 2 至 4 个同级面板可使用与模型设置一致的分段外观,但不得因此改用按钮组语义。
- 视觉变体不改变组件含义:分段外观的 `PageTabs` 仍使用页签语义、单一激活面板、游标焦点和方向键切换,不复制页面专属样式。
- 在独立滚动的 Flex 页面中,`PageTabs` 及其直接包装层必须保持 `flex: 0 0 auto`,不得被剩余内容压缩到零高度。新增页签容器时必须提供对应的 CSS 回归测试。
### 6.2 SegmentedControl
用于当前页面内互斥的视图或状态切换,例如“列表 / 网格”或少量单选状态。
- 选项数量为 2 至 4 个。
- 使用带 `aria-pressed` 的按钮组语义,支持方向键切换。
- 不能用于多选筛选、页面导航或执行即时命令。
- 与 `PageTabs` 一样,共享控件根节点必须 `flex: 0 0 auto`,不能被同级长表单或滚动区域
压缩高度。横向空间不足时保留控件自身滚动,不把标签裁成一条窄缝。
- 控件宽度由内容决定,除移动窄屏外不默认等分整行。
### 6.3 筛选工具栏
筛选不创建第五种类似页签的视觉控件:
- 2 至 4 个互斥状态可使用 `SegmentedControl`。
- 多维条件使用统一尺寸的选择框、搜索框和复选菜单。
- 已生效条件以文字和值明确展示,并提供“清除筛选”。
- 无匹配结果使用“无结果”状态,不能显示为首次使用的空状态。
- 筛选条件改变数据范围时,结果数量或当前条件必须可见。
### 6.4 ScopeBadge
`ScopeBadge` 表达对象或页面数据的归属范围:
- 全局范围显示“全局”,并配合通用范围图标。
- 项目范围显示“项目:项目名称”,不得只显示项目名称。
- 未知或已失效范围显示“范围不可用”,并提供修复入口或只读说明。
- 徽标始终包含文字,不以颜色作为唯一差异。
- 可切换范围时,徽标作为范围选择器的触发器,并提供明确的展开状态和键盘操作。
- 不可切换范围时,徽标为只读状态,不显示下拉箭头。
创建新对象时必须在提交按钮附近再次显示目标范围。跨范围移动属于显式操作,需要说明原范围、目标范围和对关联数据的影响。
### 6.5 EmptyState
`EmptyState` 统一包含:
- 能说明状态的图标或简洁插图。
- 一句标题。
- 一至两句原因或下一步说明。
- 最多一个主操作和一个次操作。
必须区分:
| 状态 | 标题示例 | 操作原则 |
| --- | --- | --- |
| 首次为空 | “还没有知识条目” | 提供创建或导入 |
| 筛选无结果 | “没有符合条件的结果” | 提供清除筛选 |
| 搜索无结果 | “未找到相关内容” | 建议修改关键词 |
| 加载失败 | “内容加载失败” | 提供重试并保留错误上下文 |
| 无权限或只读 | “当前范围不可编辑” | 说明原因和可行路径 |
空状态不得使用孤立的短句或仅显示图标。加载中不得先闪现空状态。
### 6.6 危险操作
共享危险操作样式包含:
- `danger-ghost`:列表行、菜单和工具栏中的删除入口。
- `danger-solid`:确认对话框中的最终危险操作。
- `danger-zone`:设置页中集中展示的高影响操作区域。
普通页面不得用实心红色按钮与主操作并列。危险入口必须使用具体动词和对象,例如“删除知识条目”,避免只写“确定”。
### 6.7 数据表格与活动列表
- 表头、单元格、空值、状态和行操作使用统一对齐规则。
- 文本默认左对齐,数值右对齐,状态与短标签可居中。
- 时间显示使用一致格式,并在需要时通过工具提示提供完整时间和时区。
- 行操作默认放在行末。高频安全操作可直接显示,低频或危险操作进入更多菜单。
- 活动记录必须保留操作者、动作、对象、范围、结果和时间等审计语义,不用纯图标代替关键字段。
- 表格密度可以选择“默认”或“紧凑”,但同一页面不得混用。
### 6.8 应用侧栏
主侧栏用于一级导航、最近会话和稳定的账户入口,必须通过表面、结构线和选中状态建立清楚但不过度装饰的层级。
- 浅色侧栏使用冰蓝灰表面,与白色主内容画布形成明确边界;深色侧栏使用比主画布略亮的蓝灰表面。
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
- 当前导航项和当前会话使用一致的淡蓝选中背景、强调色文字和中等字重,不额外描边或添加侧边标记;导航图标随选中状态使用强调色,悬停时保留选中状态。浅深主题遵循相同规则,强制颜色模式保留系统选中边框。
- 固定导航行高至少 `36px`、行间距 `4px`,图标统一为 `16px`;导航与最近会话分隔线上下各留 `16px`。普通待办和待处理建议数量使用中性色徽标,不使用危险红色;徽标高度不得改变导航行高。搜索框获得内部焦点时显示完整焦点环。
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
- 项目选择器顶行保留项目切换与新建按钮;项目设置统一从下拉列表对应行的管理按钮进入,关闭后焦点返回项目选择器。
- 项目下拉列表通过 `FloatingPortal` 浮在侧栏与会话区上方,默认宽度 `380px`,不受侧栏裁剪;距窗口边缘至少 `16px`,窄窗口自动收窄,长列表内部滚动。浮层锚定项目选择按钮,窗口、滚动和尺寸变化后重新定位。
- 项目选择器下方仅在有运行中、待处理或已完成未查看会话时显示跨项目活动汇总,无活动时隐藏;项目计数与项目、会话级联菜单遵守
[项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。
活动数量不改变项目菜单的稳定分组和顺序。
- 桌面停靠状态下,主侧栏宽度允许在 `220px` 至 `420px` 范围内通过指针或键盘调整,
并记住用户选择;分隔条使用 `separator` 语义,支持方向键、Home 和 End。小于
`900px` 时继续使用固定宽度抽屉,不显示拖拽分隔条。
- 关联 Task 的 Conversation 在行最左侧显示独立展开按钮,父会话行不重复任务标签或数量;
展开后的每个 Task 子项左侧显示名称,模式、计划和共享状态标记收在右侧,与父会话的
更新时间位置一致;展开层级只到 Task,不展示 Job、Subjob 或 Run。
- 会话标题保持单行且不挤压更新时间。更新时间默认靠近行右侧;低频的更多操作按钮不常驻
占位,只在悬停会话行时替换右侧时间显示。键盘焦点进入会话行或操作菜单展开时,按钮仍
保持可见。标题实际溢出时,悬停会话行才在标题的固定裁切区域内平滑横向滑动以展示完整
名称;移开后快速复位。未溢出标题不产生位移动效,`prefers-reduced-motion` 下禁用滑动
并保留完整标题提示。
- 分支会话在侧栏标题前和聊天顶栏标题旁显示同一个横向分叉徽标;徽标通过可访问名称和
工具提示说明来源会话,不能只靠图形表达。分支不改成树状会话列表,也不挤压更新时间。
- 新建 Task 后可以首次自动展开所属 Conversation;用户手动折叠后保持其选择,后台状态变化
只更新状态提示,不强制展开或抢占焦点。
- Task 子项右侧复用任务中心的共享状态标记:运行中使用脉冲强调色,已完成使用带勾成功点,
失败或中断使用危险色,等待审批使用警告色,暂停或取消使用禁用色;非完成态旁边同时
显示本地化状态文字,已完成文案保留给辅助技术而不在紧凑元数据中重复显示,不能只靠
颜色或动效表达状态。
- 左上角品牌区允许用户在“外观”设置中定制品牌名称、中英文副标题和单个 Logo。定制只
影响该品牌区,不替换安装包名称、更新协议、Agent 标识或应用正文中的 GoodBuddy 产品
身份。副标题按当前界面语言显示,留空时隐藏且不保留空行。
- 品牌设置必须提供同布局预览、显式保存、默认 Logo 和整组恢复默认操作。自定义 Logo 仅
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
单行截断,不能撑宽或挤压侧栏。
- 侧栏底部提供应用中心入口,设置按钮独立靠右。点击应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选;点击行关闭菜单,知识库、智能心跳和魔法笔记在主内容区打开页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。菜单底部“管理应用”打开既有整窗管理 Modal,共同应用设置沿用设置 Modal。
四个内置应用的管理列表、菜单和侧栏共用保存顺序,菜单按启用过滤,侧栏按启用且常驻过滤。知识库、智能心跳始终启用并常驻,可调整位置;管理 Modal 提供“打开”和排序,不提供启用、常驻开关或通用空设置,标签为“始终显示”(英文 `Always shown`)。原工作区配置及心跳单条计划 enabled 保持不变。只有魔法笔记、本机推理监控可修改启用和常驻,取消常驻不关闭功能。当前对话、运行记录和设置保持固定系统位置,不参与应用排序。
设置持久化后的事件同步各入口及工具配置写入;重开中心刷新期间锁定修改,新快照不被旧读取覆盖。文件、终端、浏览器和资源监控只通过右侧工作栏访问,不在左侧复制入口。
管理 Modal 始终使用紧凑单列横向列表,纵向对应保存顺序;左侧为图标、名称及简短用途,右侧为打开、可用的设置及上下移动操作,行间使用弱分隔线,不加卡片外框。窄窗口操作换行且不溢出,保留共享键盘焦点环。可选应用关闭时禁用“打开”。启用、常驻位于紧凑设置详情;四行均提供上下移动及拖动排序,不受启用或常驻影响,箭头按完整顺序判断边界。搜索时禁用拖动,箭头仍按完整顺序移动,不丢失未命中项。尺寸、返回与滚动重置规则见[应用中心 UI 设计](./docs/features/application-tool-navigation/ui-design.md)。已有稳定设置入口时,不在顶栏重复提供同一入口。
- 本机推理监控 Modal 复用整窗 Portal、背景隔离和焦点循环;停止、重启及释放使用其上方的紧凑影响确认。Escape 只关闭最上层,操作提交期间禁止关闭及重复提交;焦点恢复与尺寸见[本机推理监控 UI 规则](./docs/features/application-tool-navigation/ui-design.md#91-本机推理管理)。
### 6.9 助手工作栏
助手工作栏是应用级右侧工具容器,不归属于聊天页面,也不根据当前页面、项目或 Runtime
自动增删入口。产品契约见
[通用助手工作栏与执行空间 PRD](./docs/features/assistant-workbar/prd.md),
动态 Tab、“+”和终端细节见
[助手工作栏多终端页签 PRD](./docs/features/assistant-workbar/terminal-tabs-prd.md)。
- 升级后默认打开任务中心、工作区、一个浏览器实例和成果。任务中心与工作区是不可关闭的
单实例基础应用;浏览器是可关闭的多实例应用;成果保持可关闭的单实例应用。不可关闭不
等于锁定顺序。
- 资源监控作为本机单实例应用保留在能力目录,但不加入升级后的默认打开实例。面板只在可见
活动时采样,首期不持久化历史或提供后台负载告警。
- Tab 行只显示已打开实例,行末始终提供“+”普通按钮;点击后在内容区打开应用选择页。
“+”不使用 `tab` 语义,也不能因为终端或其他应用已经打开而消失。
- 应用注册表分别声明单实例或多实例、默认打开、必须存在、可关闭和排序策略。在工作栏“+”目录中,
单实例重复选择时聚焦已有 Tab;浏览器和终端每次选择创建独立实例。选择已有 Tab 只激活
该实例,保持其目标和历史;主侧栏不再提供按当前上下文匹配实例的重复工具入口。
- Task Center 是默认跟随当前项目的单实例索引,不使用多实例模式。用户可在同一 Tab 内切换
“当前项目”和“所有项目”;无项目任务在“所有项目”中标注“未绑定项目”。范围变化不得创建第二个 Task Center。每个 Task 只
关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,也
不得把 Job/Run 提升为可导航 UI 对象。
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。
- 用户可以主动排序栏目,并可关闭可关闭实例或恢复默认布局;任务中心和工作区不能隐藏或
关闭。应用不能用用户偏好机制实现自动能力裁剪。
- 已打开实例保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。不可关闭实例不渲染
关闭按钮,也不响应关闭快捷命令。
- 当前工作栏 Tab 必须同时使用连续内容表面、可见边框、顶部指示、图标或较高字重中的至少
两种信号;悬停反馈不得强于当前选中态。Tab 关闭按钮保留固定布局空间,默认仅在对应 Tab 悬停或
`focus-within` 时显示;无悬停或粗指针设备始终显示,键盘与辅助技术始终可访问。
- 工作栏 Tab、Tab 关闭按钮和“+”入口沿用共享控件圆角;终端面板外框及终端顶部工具栏按钮
使用直角,形成紧凑的停靠式终端界面。
- 选择“终端”后直接绑定当前项目,由项目执行空间自动决定本机或托管 SSH;不再显示第二层
目标选择器。终端面板内部不重复提供关闭会话按钮,关闭统一由 Tab 入口完成。
- 下一次请求使用的附件和知识库由会话输入区展示与管理,不在工作栏重复提供“上下文”栏目;
记忆功能实现前不展示占位内容,未来的记忆与历史执行上下文归入关联 Task 的任务中心详情。
- 徽标可以提示未解决意见、等待审批、失败或连接状态,但不能成为唯一状态信号,也不能无条件抢占当前栏目。
- 浏览器 Tab 的内容顶部固定使用一条紧凑工具栏,按“返回、刷新/停止加载、地址输入、
前往、交互、关闭浏览器”排列。“刷新”在当前页面加载时原位切换为“停止加载”;
“关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
尚未提交的文字改变 Agent 看到的页面。
- 浏览器工具栏操作的是该实例绑定的 Conversation 和 Browser Tab。同一 Conversation 的多个
Tab 共享隔离登录上下文,但页面、导航、操作队列和元素引用相互独立。模型请求开始时冻结
工具目标,用户切换工作栏 Tab 不改变进行中的 Agent 操作。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
不释放 Runtime 上下文。没有会话的活动 Conversation 可以直接在地址栏输入 URL 并“前往”
创建会话,但必须复用 Agent 浏览器相同的 URL 策略与能力路径。
- 所有窗口宽度下均停靠在主工作区右侧并参与水平布局回流;展开时压缩主工作区,不能覆盖、
遮罩或隔离其内容。分栏计算排除最左侧主导航,主工作区与工作栏采用相同的 `300px`
最小宽度;拖动保存两者在可用分栏宽度中的比例,任一侧到达最小值后都不得继续侵占。
所有可容纳两侧最小值的尺寸均支持指针和键盘调整,并须保留全部栏目入口。
- 工作栏分隔条须保留完整的 `9px` 拖拽命中区域,位于侧栏内部,不能被容器裁剪;栏目内容
和原生浏览器视口须避开该区域。浏览器全屏时隐藏分隔条并取消这段预留空间。
- 工作栏开关固定在顶栏 `.topbar__actions` 内、紧邻浅色/深色主题按钮之前,同一个按钮负责展开与收起并持续暴露状态;图标中的
箭头方向必须随当前动作切换,收起状态提示展开,展开状态提示收起。按钮保持普通图标按钮
视觉,不通过持续高亮表达展开状态。工作栏内部不再重复关闭按钮;当前会话没有 Task 时,
不得为了放置该开关而保留一条空上下文栏。
- 停靠工作栏不得获得对话框语义、焦点陷阱或背景遮罩。关闭后焦点返回同一个工作栏开关;
主工作区与工作栏在展开期间都保持可达。
- 对话消息流、输入区和输入区元信息必须在主工作区内部保留对称的左右安全边距。工作栏
分割线只能位于主工作区边界之外,不能覆盖、裁切或挤掉输入框边框、发送按钮或右侧边距。
- 终端、宽日志和大型成果可以由用户切换到底部停靠或独立窗口,应用不得因内容变化自动改变用户已选布局。
### 6.10 应用顶栏与全局操作
应用顶栏用于当前会话标题、范围、窗口级状态、侧栏开关和低频全局操作,不承担主要导航。
顶栏必须保持紧凑,不能与页面内容争夺注意力。
- 应用壳层在水平方向分为全高主侧栏和右侧内容框架。主侧栏从窗口顶部贯穿到底部,品牌区、
项目切换器、导航和账户区保持同一连续表面;不能用全宽标题栏把整条侧栏向下推移。
- 无边框窗口的紧凑标题栏只覆盖右侧内容框架,保留侧栏开关、当前会话标题、范围、Runtime
状态、主题开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
`34px × 34px` 点击区域。
- 发布说明 Modal 与其他应用级 Modal 一样使用整窗遮罩;遮罩必须覆盖主侧栏、右侧内容和
顶栏,不能保留一条未被遮罩的窗口控制色块。关闭操作由 Modal 自身提供。
- 整窗固定遮罩通过 `createPortal(..., document.body)` 挂载,避免页面层叠上下文影响;
保留 React Context、关闭与焦点管理,表单样式不得依赖已移出的页面祖先选择器。
- 共享锚定菜单 `AnchoredMenu`、同行帮助 `InlineHelp` 和应用通知共用 `FloatingPortal` 的非模态 `popover="manual"`,由浏览器 top layer
绘制,避免被 Modal 或滚动容器遮挡。菜单和帮助挂在触发按钮所属 Modal 内,通知跟随当前活动
Modal,无 Modal 时挂在 `body`;不能只提高 `z-index` 或解除背景的 `inert`。
- 原生浏览器 `WebContentsView` 不受网页 `z-index` 控制。应用级 Modal 打开时统一释放
浏览器 viewport;非模态对话框、菜单、局部确认或应用通知与浏览器区域相交时也释放。
浮层关闭后只恢复当前活动实例的 viewport,不关闭、重建或刷新网页。接线与测试见
[浏览器多实例技术设计](./docs/features/assistant-workbar/browser-tabs-technical-design.md#4-renderer-与-viewport)。
- 遮罩必须 `inset: 0` 铺满整窗,并由 `styles.css` 中统一的遮罩选择器声明
`-webkit-app-region: no-drag`,否则 `.brand` 与 `.topbar` 的原生拖动区域会截获指针,
使遮罩上的控件无法悬停和点击。新增整窗遮罩时扩展该共享选择器,不要在各自规则里复制
`no-drag`,也不要用 `inset: 38px 0 0` 之类的偏移绕开标题栏。
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
对齐。该栏不重复放置新建入口。
- “新建任务”只位于工作栏“任务索引”标题后方;任务状态筛选占满工作栏可用宽度,四个选项
等分,不能保留内容宽度的短控件。
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。顶栏 Runtime
状态由状态点和 Runtime/模型名称紧凑表达:就绪使用绿色,连接中使用黄色,不可用使用灰色,
不再重复显示状态文字;完整状态详情继续通过可访问说明和工具提示提供。
- 浅色与深色切换属于持续可用的窗口级操作,直接显示太阳或月亮图标,并通过可访问名称说明将切换到的主题。选择必须持久化,切换不得改变布局。
- 顶栏只直接显示当前任务所需的高频操作。已有侧栏账户设置入口时,不再重复显示 Runtime/设置入口;使用帮助优先放在相关操作附近,而不是为单个帮助项创建“更多”菜单。
- 助手工作栏开关固定在顶栏 `.topbar__actions` 内、紧邻浅色/深色主题按钮之前;展开后该开关保持原位并
负责收起,不在工作栏标题区域增加第二个关闭入口。
- 只有存在至少两个无法由稳定入口承载的低频全局操作时才增加全局菜单,不为了容纳一个冗余入口而显示省略号按钮。
- 窄窗口下优先压缩状态标签并保留图标按钮,不隐藏窗口控制、当前范围或进行中的风险状态。
- 使用全局菜单时,菜单项使用 `--font-body`、`14px` 图标和约 `32px` 单项高度;标签使用短名称。菜单保留 `menu`、`menuitem` 语义,支持上下方向键、Home、End 和 Escape,关闭后焦点返回触发按钮。
### 6.11 单选选择器
模型、专家角色、工作模式、Runtime Agent、Runtime 预设和 Runtime 快捷操作属于同一输入上下文,其选择器必须共享结构、尺寸和菜单视觉,不能出现一个精细菜单与多个风格不一致的原生下拉框。
- 只有选项能由单行短标签充分区分、不需要补充来源、位置、状态或说明时,才使用原生 `select`。
- 当用户必须在选择前比较来源、范围、位置、连接状态或其他辅助信息时,使用与输入区选择器同类的自定义富信息单选菜单,不用 CSS 修饰原生 `select` 冒充富信息菜单。
- 触发按钮复用统一的模型选择按钮样式,保持相同高度、圆角、边框、展开指示和焦点状态。
- 菜单使用 `menu` 与 `menuitemradio` 语义,当前项同时显示选中标记和 `aria-checked`。选项可以包含一行简短说明,但标签和说明不得被截断到无法区分。
- 支持上、下方向键、Home、End、Enter 或 Space、Escape;打开后焦点进入当前项,关闭后返回触发按钮。
- 点击或聚焦菜单外部时关闭;同一输入区内的模型、专家和模式菜单互斥展开。
- 不可用选项保持可读并说明原因,键盘导航不得停留在不可选择项上。
- 当某个选项需要用户在其展开区域内完成路径选择、授权或其他前置操作时,不得因为前置条件
尚未完成而禁用该入口。选择入口只展开流程,验证成功前不提交实际值;不得自动采用第一个
候选。取消保持原值,验证错误在对应操作附近显示。
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称,并按数据可用性补充目录、远程来源或其他能帮助辨认项目的信息。托管 SSH 项目在“远程项目”内按已保存 SSH Host 做一层轻量分组,Host 标题旁显示真实 Agent 连接状态;项目副标题只显示远端路径,不重复“托管 SSH”类型。
- 项目菜单区分“本地项目 / 远程项目 / 远程通道”,不把消息通道命名为远程项目。“远程项目”关闭时完整移除远程项目分组,而不是显示不可操作的残留选项;本地项目和远程通道继续可用。Agent 连接状态使用“未连接 / 连接中 / 就绪 / 异常”等文字标签,读取状态不得发起连接,且不能只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
### 6.12 应用通知与就地反馈
应用级通知统一进入全局通知视口,页面不得自行复制通知卡片或在内容流中长期堆放短期消息。
- 右上角通知使用紧凑的中性表面、弱边框和轻阴影,最大宽度为 `360px`,窄窗口保留两侧各 `16px`。状态色仅用于图标,不铺满卡片;正文直接呈现,通用状态标题仅供屏幕阅读器读取。图标、正文与关闭按钮统一垂直居中,不使用各自的上下偏移。
- 异步操作成功、无需立即处理的信息,以及不属于某个字段的异步失败,使用应用级 `success`、`info` 或 `error` 通知。
- 成功和信息通知默认在约 2 秒后自动消失;错误通知保持可见,直到用户关闭或同一去重键的更新替换它。
- 通知出现不抢焦点;Modal 打开时,通知关闭按钮纳入当前 Modal 的 Tab 循环。聚焦通知时
Escape 返回原控件或当前 Modal 的可用控件,不关闭 Modal;关闭通知后同样恢复焦点。
打开、关闭或切换 Modal 不重建通知,不重置自动消失计时。
- 同一语义和文案的重复通知应去重。通知正文必须有长度上限,不包含凭据、私人内容或未脱敏的提供商响应。
- 字段校验、破坏性确认、操作进度、阻塞整个页面的状态,以及需要就地重试或修正的错误保留在相关控件附近。
- 就地错误必须与对应字段或操作建立程序化关联;全局错误使用 `alert` 和 assertive 实时区域,成功与信息使用 `status` 和 polite 实时区域。
- 一个事件只能选择一种主要反馈位置,不得同时显示页内横幅和全局通知。失败时不得因通知切换而清空用户输入、筛选或未提交草稿。
### 6.13 Switch 与 Checkbox
Switch 用于在两个持久状态之间立即切换,例如启用能力、开启索引、允许群消息或显示平台入口。Checkbox 用于独立多选、范围分配或执行前确认,例如选择多个 Runtime、选择知识库、清除已保存密钥。两者不得只因底层都使用 `input[type="checkbox"]` 而混用视觉或语义。
- 二元启停必须使用共享滑动开关视觉,当前实现复用 `toggle-row`,不得显示为原生方形 Checkbox。
- Switch 底层可以使用 `input[type="checkbox"]`,但必须声明 `role="switch"`,通过原生 `checked` 状态暴露开关状态,并具有持久、明确的可访问名称。
- Checkbox 保留原生 Checkbox 语义和方形勾选视觉,不得添加 `role="switch"`。多项分配、列表选择、确认声明和“保存时清除密钥”等一次性选择均属于 Checkbox。
- 不创建页面专属 Switch 样式。需要紧凑布局时仍复用同一轨道、滑块、焦点环、禁用状态和动效,只调整共享组件支持的布局变体。
- Switch 支持 Tab 聚焦和 Space 切换,键盘焦点至少显示 `2px` 高对比焦点环。可见标签应描述被控制的能力,不能只显示“开 / 关”。
- 异步切换期间禁用重复操作并保留原状态。失败时恢复或保留最后确认状态,通过应用通知或就地可恢复错误说明原因。
- 涉及联网、上传、电脑控制或其他外部影响的 Switch,附近必须持续说明数据去向、权限范围或风险,不能只靠设置名称表达影响。
- 自动化测试应按 `switch` 角色查询二元开关,按 `checkbox` 角色查询多选或确认项,防止视觉迁移后语义回退。
### 6.14 InlineHelp 同行帮助
页面、区块和字段的补充用途、格式及操作说明统一收进标题或标签旁的 `InlineHelp` 问号按钮。
`PageHeader` 与 `SettingsSectionHeader` 通过 `help` 接收这类内容;`description` 保留需要常显的上下文,两处不重复说明。描述缺省或为空时不渲染段落,不保留空行。
当前范围、状态、字段错误、操作进度、风险、费用和影响用户决策的限制仍就地可见。混合文案先拆分,再将补充说明移入帮助。
- 标题或标签与问号使用共享 `inline-help-label` 同行排列,不为帮助另占说明行;字段标签通过 `htmlFor` 关联控件,帮助按钮放在 `label` 外,避免点击帮助触发输入或开关。
- 悬停或键盘聚焦按钮时展开;指针可移入浮层阅读。点击(含 Enter、Space 激活)固定展开,再次点击关闭;未固定时,指针和焦点均离开后关闭。
- Escape、点击外部或焦点移到外部关闭帮助。Escape 优先关闭帮助,不同时关闭所属 Modal;展开和关闭不主动转移焦点,不建立焦点陷阱。
- 按钮提供与标题或标签对应的可访问名称、`aria-expanded`,展开时通过 `aria-controls`、`aria-describedby` 关联 `role="tooltip"` 内容。提供稳定帮助 ID 时,关闭状态保留同 ID 的隐藏说明,展开时由浮层替换,确保字段的 `aria-describedby` 始终有唯一目标。持久标签与必要提示不得依赖浮层。
- SSH 密码字段常显认证用途和验证成功后加密保存的说明;Main 处理及不写入命令行、日志、普通设置的细节收进同行帮助。应用常驻说明与关闭后果使用独立翻译键,关闭后果保持常显,不按标点拆分翻译文本。
- 浮层复用 `FloatingPortal`,遵循 6.10 的 Modal 归属规则。宽度最多 `360px`,四周保留 `16px` 视口边距;空间不足时翻转或调整位置,长内容在浮层内滚动,窗口及滚动变化后重新定位。
- 问号使用共享 `28px × 28px` 点击区域、焦点环与主题令牌。不得另做原生 `title` 长说明或页面专属帮助浮层。
### 6.15 表单 Modal
- 复用 `styles.css` 中的 `custom-task-dialog__surface`、`custom-task-dialog__header`、`custom-task-dialog__content` 和 `custom-task-dialog__actions`,与定制任务、SSH Host 等现有弹窗使用同一结构;不为单个业务另建 Modal 框架。
- 标题放在 `header` 左侧,右侧只放共享 `icon-button` 的 X 关闭按钮,使用本地化 `aria-label` 和工具提示。取消不放在标题下方或表单顶部。
- `footer` 使用共享操作区,按钮整体右对齐,次要取消在前、主要保存在后。放弃修改确认沿用同一操作区,继续编辑在前、危险操作在后。
- header、滚动 body、footer 是 surface 的三个直接子区域。沿用共享 Grid 的 `auto minmax(0, 1fr) auto`,只有 body 滚动,短窗口仍可见标题、关闭和提交操作;窄窗口沿用共享外边距、内边距和主题令牌。
- X、取消、Escape 和遮罩关闭共用业务关闭检查,不绕过未保存确认或提交锁定。确认中的 Escape 或 X 返回编辑;继续复用 `activateModalFocus`、`trapTabFocus`,保留背景隔离、初始焦点和关闭后的焦点恢复。保存失败保留草稿及可恢复错误。
## 7. 交互状态
所有可交互组件必须实现:
- 默认:文本、边框和背景层级清晰。
- 悬停:提供轻量背景或边框反馈,不改变布局。
- 按下:反馈比悬停更强,持续时间使用 `--motion-fast`。
- 键盘焦点:显示至少 `2px` 的高对比焦点环,不被容器裁切。
- 选中:同时使用背景、边框、图标或字重中的至少两种信号。
- 禁用:降低强调度,同时保留可读标签,并通过说明或工具提示解释原因。
- 加载:防止重复提交,保留原按钮宽度并显示进行中标签。
- 错误:字段或局部操作错误就近显示可执行说明;非局部异步错误使用不会自动消失的应用级错误通知。
异步提交成功后更新内容并通过统一应用通知提供明确反馈。失败时保留用户输入和筛选上下文。
## 8. 范围与数据语义
### 8.1 页面范围
- 页面范围决定当前列表、搜索、创建和批量操作针对的数据集合。
- 页面标题区必须持续显示当前范围。
- 搜索框占位文案应反映范围,例如“搜索当前项目的知识”。
- 切换范围后清理不再有效的选择项,并明确提示数据集合已变化。
### 8.2 对象范围
- 详情页或主从布局的右侧面板显示所选对象自身的范围。
- 当页面范围与对象范围不一致时,必须显示解释,不得静默混合。
- 全局对象可被项目引用时,应分别表达“归属范围”和“当前引用关系”。
### 8.3 操作范围
批量操作、导入、删除、移动和自动化运行前,界面必须明确:
1. 将影响哪些对象。
2. 对象属于全局还是某个项目。
3. 操作是否可撤销。
4. 是否影响关联任务、知识引用或历史记录。
## 9. 破坏性操作政策
### 9.1 风险等级
| 等级 | 示例 | 要求 |
| --- | --- | --- |
| 低 | 移除可立即恢复的筛选、取消未保存草稿 | 通常无需确认,必要时提供撤销 |
| 中 | 删除单个可恢复对象、停止正在运行的任务 | 显式确认或操作后撤销,说明直接影响 |
| 高 | 永久删除、批量删除、清空历史、删除被引用对象 | 必须使用确认对话框,说明范围、数量和不可逆性 |
### 9.2 确认对话框
- 标题包含具体动作和对象,例如“永久删除 12 条活动记录?”
- 正文说明范围、关联影响和恢复可能性。
- 取消按钮在前,危险按钮在后。初始焦点放在取消按钮。
- 最终按钮使用 `danger-solid`,标签重复具体动作。
- 仅对高影响且不可恢复的操作要求输入对象名称或确认短语,避免把摩擦用于所有删除。
- 提交期间锁定重复操作。失败后保持对话框打开并显示可处理的错误信息。
### 9.3 撤销与反馈
- 可恢复删除优先在操作后显示带“撤销”的持久通知。
- 撤销窗口结束前,不把操作描述为“永久删除”。
- 删除成功后更新列表、选中项和计数。主从布局中被删除对象的详情面板应回到明确的未选择状态。
- 审计记录不得因普通对象删除而被静默移除。
## 10. 深色主题
- 深色主题通过语义令牌替换实现,不在组件中使用主题条件分支选择原始颜色。
- 主画布使用深海军蓝,侧栏、顶栏、输入区和浮层使用逐级提亮的蓝灰表面;表面层级主要依靠亮度和边框区分,避免大面积纯黑与高亮白形成刺眼对比。
- 深色强调色使用明亮但不荧光的蓝色,成功状态使用青绿色。用户消息等大面积强调表面使用更深的实心蓝,确保反白文字舒适可读。
- 输入框、代码块、表格悬停、选中行、弹窗遮罩和滚动条必须分别检查深色值。
- 图片、图表和状态色在深色背景下保持可读。图表系列不能只靠色相区分,还应使用形状、线型或标签。
- 焦点环、危险文本和弱文本在两种主题下都满足对比度要求。
- 原生控件和滚动条声明正确的 `color-scheme`。
- 主题切换不得导致布局、边框宽度或字号变化。
## 11. 无障碍
### 11.1 键盘与焦点
- 所有交互均可通过键盘完成。
- 焦点顺序与视觉顺序一致,打开弹窗后焦点进入弹窗,关闭后返回触发元素。
- 页面导航链接按正常 Tab 顺序操作。同页签组和单选组使用方向键,菜单使用上下方向键和 Escape。
- 不给不可交互容器添加焦点,不用 `div` 模拟按钮而缺少按钮语义。
### 11.2 语义与标签
- 图标按钮必须有可访问名称,并在视觉上提供工具提示。
- 表单控件有持久标签,不能只使用占位文案。
- 状态变化使用适当的实时区域,但避免对频繁日志逐条播报。
- 错误信息与对应字段建立程序化关联。
- 表格使用正确的表头关系,复杂审计记录在窄屏下仍保持字段标签。
### 11.3 对比度与触控目标
- 正文与背景对比度至少为 `4.5:1`,大号文本至少为 `3:1`。
- 控件边界、焦点和关键图形对比度至少为 `3:1`。
- 常规交互目标建议至少 `32px × 32px`。紧凑表格可使用 `28px` 高度,但相邻目标之间必须有足够间隔。
- 文字缩放至 `200%` 时,核心操作、范围信息和错误提示不能被裁切。
## 12. 响应式与窗口规则
GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证任务连续性,不简单隐藏信息。
### 12.1 宽窗口,`1200px` 及以上
- 使用各 `PageShell` 的完整最大宽度。
- `dashboard` 可使用 3 至 4 列卡片。
- `master-detail` 保持双栏,详情区获得主要空间。
### 12.2 中等窗口,`960px` 至 `1199px`
- 页面左右内边距为 `24px`。
- 仪表盘降为 2 列。
- 主从布局缩小左栏,但不得低于 `280px`。
- 表格优先收起低优先级列到详情或行展开区,不隐藏范围、状态和时间。
### 12.3 窄窗口,`720px` 至 `959px`
- `PageHeader` 的操作区换行。
- `master-detail` 变为单面板导航。进入详情后提供明确返回列表的按钮。
- 仪表盘使用单列或 2 列,取决于卡片最小宽度。
- `PageTabs` 可单行横向滚动,不换成下拉菜单。
- 宽表格在自身容器内横向滚动,并固定关键标识列时确保键盘可达。
### 12.4 极窄窗口,小于 `720px`
- 页面左右内边距为 `16px`。
- 标题、范围徽标和操作纵向排列,但范围不得隐藏。
- 主操作可以占满可用宽度,次操作进入更多菜单。
- 分段控件可等分整行,筛选工具栏改为可展开区域。
- 聊天输入区保持可见,并考虑窗口安全边距。
- 对话框使用接近全宽的布局,仍保留 `16px` 外边距。
## 13. 页面应用规范
### 13.1 聊天
- 使用 `reading` 壳层,消息流与输入区共享宽度。
- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐,
右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
原文,不包含推理过程、工具日志或引用元数据。复制成功后,当前按钮显示使用 `--success`
的绿色对号,可访问名称和工具提示同步表示已复制,3 秒后恢复复制图标;重复复制成功重新
计时,不显示成功通知。失败仍通过应用错误通知反馈。流式回复完成前不显示复制入口。
- 默认开启“会话 HTML 渲染”时,已完成 Agent 回复中的完整 HTML 与 `html` / `htm`
代码块在原位置显示隔离静态预览;用户消息、推理过程和流式中的回复不得执行 HTML。
每个预览持续标注脚本和网络已禁用,并在标题右侧复用与 Mermaid 图表相同的图标按钮组:
源码按钮使用代码图标并暴露 `aria-expanded`,全屏按钮使用放大图标打开统一 Modal。
全屏预览遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape 关闭和焦点恢复规则,
并沿用同一套清理与 Sandbox/CSP 边界。关闭平台功能后必须显示 HTML 源码,不能隐藏内容
或保留旧预览。
- Mermaid 大图支持缩放和拖动;图形超过画布时,四个边缘都必须能通过滚动到达,不能因居中而裁掉顶部或左侧。
弹窗工具栏提供“导出 PNG”,保存完整图形,不受当前缩放和滚动位置影响;背景使用当前主题的画布底色。
导出按原图四周留出 `16px`,以 2 倍分辨率绘制,任一边最多 `4096px`。导出期间禁用重复点击,失败在按钮旁提示重试。
- 本地会话的更多操作提供“在新会话中继续”。创建前必须先持久化当前稳定消息;有活动运行
或待发送内容时不可创建。新会话复制聊天内容与当前 Runtime/知识检索选择,重置压缩状态,
使用新的会话和消息 ID,且不复制 Task、队列、运行、记忆或成果归属。
- 模式、模型或工具权限属于上下文控制,不与页面导航页签混用。
- 模型、专家角色、工作模式、OpenCode Agent、Continue 预设和 Runtime 快捷操作使用统一的上下文单选菜单,并保持菜单互斥、键盘可达和选中状态明确。
- Runtime 选择菜单必须反映当前执行空间的真实能力。托管 SSH 项目当前只支持 OpenCode,因此输入区只渲染已配置的 OpenCode 选项和管理入口,不显示直连模型、Continue 或 DeepSeek Harness 的不可用入口;旧会话保存的其他 Runtime 选择在项目激活时恢复为当前 OpenCode 配置。
- 输入区使用一个完整容器,主工具栏保留附件、语音、Runtime、工作模式、会话选项及停止/发送。知识范围、专家角色、OpenCode Agent、Continue 预设和 Runtime 快捷操作收纳在会话选项浮层中;Runtime 专属选择在浮层内按提供方分组,无可选功能时不保留空分组。输入框上方以简短摘要显示生效的专家、知识范围、检索方式和 Runtime 专属选择。
- 会话选项浮层与主行菜单互斥,内部复用上下文选择菜单;子菜单就地展开,浮层统一限制宽高并滚动。Escape 先关闭子菜单并恢复其触发按钮焦点,再关闭会话选项并恢复入口焦点;点击外部、焦点离开、切换会话或开始/结束回复时关闭浮层。
- 会话选项入口紧随录音键,使用无常驻边框的调节图标,保留可访问名称、悬停说明和展开态反馈;浮层在工具栏左侧展开,最大宽度为 `300px`,窄窗口不超过输入区。浮层内选择器使用整行轻量列表样式,避免重复的按钮边框;知识库行右侧显示展开箭头,展开时朝上。
- OpenCode、Continue 和 DeepSeek Harness 后续的 Task 级委派与取消、后台执行进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止及原生会话监督统一进入应用级助手工作栏固定的“Runtime”栏目,不加入 Composer。栏目入口始终存在;只选择 Conversation 或 Task,并按 Task 聚合执行状态,不显示 Job/Run 树或独立操作菜单。内部可选区域按用户所选目标和 Runtime 的真实能力显示,不为未支持能力渲染空卡片或成排禁用按钮。切换跟随目标时必须清理上一归属的监督状态,固定目标则保持不变。
- 设置中心只管理持久 Runtime 配置、默认值和能力清单;右侧 Runtime 栏目管理用户当前跟随或固定目标的生命周期。两处不得复制同一实时操作,栏目中的高风险操作仍须就地确认并保留取消、权限、用量和活动审计。
- Runtime Prompt 快捷操作只把模板填入输入草稿,用户可以继续编辑;OpenCode Command 由 Runtime 原生 API 执行,输入框只承载可选参数,不以普通斜杠文本冒充执行。
- OpenCode 原生 Task 调用在本地项目和托管 SSH 项目中统一解析为子 Agent 状态卡,不显示为普通工具卡。状态卡持续显示 Agent 类型、任务说明、运行状态、完成输出或失败原因;身份徽标、Agent 名称和状态位于首层,任务说明作为主信息,Runtime 来源作为次级元数据,输出与错误在同一卡片内按需展开。父 Agent 的推理不冒充子 Agent 过程,必须与状态卡保持明确间距。状态不能只靠颜色表达。推理、子 Agent、工具和正文统一按事件首次出现顺序进入消息块;增量更新只更新原位置,Task 临时工具块重分类后必须原位替换,不能把子 Agent 卡统一提升到消息顶部。同一调用的增量事件按稳定调用 ID 更新,不能因 ACP 首帧尚无参数而永久保留重复工具卡或活动记录。一次请求内已观察到的子 Agent 均保留在消息与活动记录中;父请求取消、失败或意外中断时仍处于活动状态的卡片必须结算为对应终态,父请求成功结束但子 Agent 未报告终态时必须显示为失败而不是推断为完成。
- Agent 回复进行中锁定模型、专家角色、工作模式和 Runtime 定制选择器,并关闭已打开的上下文菜单;回复结束或停止后再恢复选择,避免界面状态与本次运行实际使用的上下文不一致。
- 普通工具在主会话和子 Agent 过程中使用同一轻量折叠记录:不显示外层“工具执行”标题、计数、背景和边框,保留无障碍区域名称。记录纵向左对齐,头部按内容收宽且不超过消息宽度;展开箭头在左,工具名作为主标题,有额外信息的单行摘要弱化显示。右侧以绿色对勾显示成功完成,以图标显示运行和待执行状态,提供悬停文字和无障碍名称;失败和可恢复状态保留文字。仅重复工具名和执行状态的已知 Runtime 模板摘要在头部与详情中均隐藏;刷新重试、审批等额外提示保留。默认折叠,状态和输出更新不改变用户的展开选择;折叠时露出一行错误,展开详情使用消息可用宽度、浅背景和左侧细线,按错误、结果、独立折叠的输入参数阅读。详情保留原始文本,完整 JSON 输入可缩进显示,复制仍使用收到的原文,并通过应用通知反馈。详情工具栏只保留复制按钮,文本默认自动换行,长内容在固定最大高度内滚动,不提供换行切换和展开全部按钮。
- OpenCode 主会话和子 Agent 的工具记录均保留 Runtime 提供的操作标题作为摘要,显示文件路径、命令或操作描述;本地 SDK 标题去除首尾空白后最多保留 240 字符,没有标题时沿用工具名回退规则。主会话工具失败或中断时保留已收到的标题;远端 ACP 继续在记录名称中显示其提供的标题。
- 工具取消后保留“已取消”文字,避免与失败图标混淆。
- Continue、DeepSeek Harness 和直连模型的工具名后显示单行操作摘要,内容提取、长度与完成态保留规则见[工具操作摘要](./docs/features/assistant-workbar/runtime-interactions.md#工具操作摘要)。
- Agent 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。
- 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。
- 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。
- 支持上下文状态的 Runtime 在输入区下方复用同一紧凑用量条;文案必须区分“本次模型调用”和“压缩后对话估算”。手动压缩仅在当前 Runtime 明确支持且没有活动回复时显示,作为元信息区左下角的浮动次操作,不参与输入区高度计算;元信息区始终预留稳定高度,切换 Runtime 不得让输入框上下位移。元信息区与窗口底部只保留紧凑安全留白,不形成额外空白区。进行中禁用重复操作,结果通过应用通知反馈。
- 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。
- 宽度大于 `700px` 时,主工具栏的添加内容、Runtime、模式、会话选项与发送保持同一行。窄输入区按容器宽度换行,保留发送及停止入口;选项浮层不得超出输入区宽度,不再常驻独立的 Runtime 专属功能行。
- 输入框原生支持 `Ctrl+V`:文本直接进入草稿,系统截图和从文件管理器复制的一个或多个支持文件加入本次消息附件。文件粘贴与上传按钮共用读取、解析进度和数量限制;读取期间阻止重复导入及发送,错误就地关联输入框和附件按钮。具体类型、大小及批次失败规则见[聊天附件要求](./docs/features/document-processing/prd.md#321-聊天附件入口)。默认工具栏不提供独立“读取剪贴板”“截取当前屏幕”和“选择应用窗口”入口。
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴文件、图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
- 草稿与消息中的附件操作使用紧凑图标按钮,并提供悬停和键盘焦点提示;文件名、解析状态与操作行的布局遵循[附件操作规则](./docs/features/document-processing/ui-design.md#12-附件操作菜单)。
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
排除在可滚动范围之外。
### 13.2 最近对话
- 使用 `standard` 壳层和统一 `PageHeader`。
- 搜索、范围和时间筛选位于筛选工具栏。
- 行项目统一显示标题、范围、最近更新时间和必要状态。
- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。
- 侧栏中有关联 Task 的 Conversation 显示行首展开按钮,父行不显示任务标签或数量;展开后
最多直接显示 3 个 Task,“查看全部 N 个任务”打开该 Conversation 的完整任务区。
- Task 子项左侧显示名称,右侧显示本地化的 Ask/Execute、计划摘要和共享状态标记,与父会话
的更新时间位置一致;已完成使用带勾成功点,不重复显示完成文字。点击子项打开同一
Conversation 并定位 Task,不继续展开 Job/Run。
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
### 13.3 知识库
- 使用 `master-detail` 壳层。
- 左侧负责范围、搜索、筛选和条目选择,右侧负责详情、编辑和预览。
- 两侧均只使用语义颜色令牌,禁止内联浅色背景或边框。
- 选中条目后持续显示对象范围。没有选中项与知识库为空必须使用不同状态。
- 窄窗口切换为单面板导航,不把双栏压缩到不可读。
### 13.4 智能心跳
- 监督者入口使用 `supervisor` 壳层;智能心跳设置位于同一壳层内。
- 顶层页签为“工作回顾 / 故事线图谱 / 自动监督 / 活动记录 / 设置”,使用一套共享 PageTabs,窄窗口单行横向滚动。页签下不重复同名标题,也不保留空标题容器;所选回顾使用“监督结果”标题,历史结果不得标为最新。
- 工作回顾只展示统一监督结果、历史选择和紧凑的新回顾控件;结果保留自身范围、覆盖区间和生成时间,图谱读取同一结果 ID。自动监督集中展示当前计划、运行概览、指标、趋势、自动监督报告、记忆与行动建议及心跳运行审计;活动记录只展示统一执行活动、进度和详情。每页保留所属数据的一处刷新入口,范围与时间选择仅作用于新回顾请求。详细布局与阅读规则见[监督者 UI 设计](docs/features/conversation-supervision/ui-design.md#应用入口)。
- 监督者保留统一页面壳层;阅读与活动子面板分别复用 `--content-reading` 和 `--content-dashboard`,在壳层内左对齐。各面板的工具栏、历史导航、状态和结果共享宽度,不在全宽卡片内部再次居中窄正文。活动筛选按内容紧邻排列,阶段以文字状态行呈现。
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
Checkbox,不能依靠进入页面时的当前项目推断。
- 状态卡片使用统一状态令牌,不只依赖颜色。
- 活动记录先显示运行状态、时间、范围及实际阶段,恢复操作紧邻进度。原始错误默认折叠,展开后在有界面板内滚动并可选择复制;配置归入次要详情。任务层级只展示可查询的记录,缺失阶段明确标注,不能用剩余来源为零推断发布成功。具体阶段与分页规则见[监督者活动设计](docs/features/conversation-supervision/ui-design.md#活动)。
- 自动监督的当前状态下显示计划列表,仅保留列表的一个创建按钮;无计划时使用列表的未配置说明,不叠加第二个空态或创建入口。创建与编辑复用同一份表单及共享 Modal 焦点、背景隔离和遮罩样式;未保存关闭须确认,保存失败保留草稿,编辑暂停计划不恢复运行。“执行记录”进入按该计划 ID 筛选的活动页,活动页不重复显示心跳审计列表。
- 设置只展示模型超时与并发、分页、分批和执行预算,不含计划、报告、审计或嵌套页签,不显示整页全局徽标或独立刷新行。
- 监督者的设置、帮助、空态及错误文案不得向终端用户展示内部路线图、交付进度或设计与实现缺项;只说明当前行为、适用范围和可执行的下一步。不得因此夸大为提供商全局限流或完整语义识别;真实的运行失败、超时、暂停、来源失效及恢复操作必须保留。
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
### 13.5 Task Center
- 保留现有助手工作栏入口,首期在窄栏内适度完善,不先扩张成新的独立一级页面。
- 只展示 Task;普通 Conversation、Job、Run、工具步骤、Subagent 和心跳事项不独立占行。
- 范围固定为当前项目,不提供全局或所有项目入口;切换会话不筛掉本项目其他会话的任务。
- 顶部紧凑显示当前会话消息总数和 Agent 累计回复时间,按每轮请求区间累加,包含轮内工具和
审批等待,不计轮间空闲。历史缺失显示不完整提示,远程缺少可靠证据时不虚构用时。
- 每项显示名称、关联 Conversation、所属项目、Ask/Execute、状态、最近进展、
最近真实活动时间、累计执行时间及需要关注信息。任务用时汇总历次执行请求,不按父任务生命周期计算。
- 点击列表项打开关联 Conversation 并定位该 Task,不显示第二份内容载体。
- 项目任务标题右侧依次为“+”新建和筛选图标;图标按钮提供可访问名称,筛选按钮提供展开状态。
状态筛选默认收起且为全部;展开后用共享 `SegmentedControl` 提供全部、进行中、暂停、已结束,
窄栏不足时单行滚动。收起保留条件,非全部时图标高亮并显示条件。状态归类遵循[任务中心 PRD](docs/features/task-and-job/task-center-prd.md#5-交互)。
- 完整消息留在 Conversation;长错误、工具、审批和成果按 Task 关联到 Runtime、活动记录和
成果查看器,不撑高列表,也不显示 Job/Run 树。
- “新建定制任务”使用共享 Modal,默认立即执行和当前 Conversation,可选择本项目其他可用
Conversation 或新建会话,选择必须持续可见。内容必填,名称可选并从内容生成;定时提供单次、每日和每周。
- 定时消息创建 Modal 使用关联 Conversation 的 Runtime 与 Ask/Execute 模式,不再提供独立
模式开关。未运行计划的详情显示会话当前模式,已运行任务显示该次执行记录的模式;
不得用旧计划字段表示下一次执行权限。不支持工具时不得静默降级。
### 13.6 运行记录
- 使用 `dashboard` 壳层,并通过 `PageTabs` 提供“任务与会话 / 活动时间线 / 用量统计”三个同级视图。
- 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠。
- 任务或会话的综合状态以最近一次顶层请求对应的最终 Agent 结果为准;最终结果尚未产生时使用该请求的当前状态。中间工具或子专家的失败、取消和中断保留在活动详情中,但不得覆盖最终成功状态。
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系。节点内使用 `U / G / S / T / A` 拉丁字母简称,节点下显示完整身份或名称;选择节点后显示所属会话、不可变范围快照和完整详情。
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度;按项目或会话分组时默认收起并显示汇总,展开后显示 Runtime / 模型子行,按模型分组时保持平铺。宽表格在独立容器内横向滚动。
- 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
### 13.7 魔法笔记
- 笔记总览与待办使用一个普通按钮切换,放在页头“新建笔记”按钮左边。按钮显示目标“切换到待办”或“切换到笔记”及对应图标,按内容宽度显示,不拉伸;窄窗口操作组可换行到标题下方,保留控件高度。
- 切换按钮不使用页签语义,切换后焦点回到同一个按钮;内容 section 持续提供当前列表的 `aria-label`。切换沿用草稿保护入口,待办状态仍使用独立的 `SegmentedControl`。
- 文字记录编辑器保留 Quill 原生列表快捷输入:输入 `-`、`*`、`1.`、`[ ]` 或 `[x]` 后按空格,分别转换为项目符号、编号或待办清单。快捷输入只生成现有 Delta 富文本,不引入 Markdown 源码模式、第二套文字记录持久化格式或第三方 Markdown 插件;新建文字记录的操作提示收进类型选择旁的 `InlineHelp`。
- 新记录可选择“文字 / 画布”;已保存画布提供分页阅读和“展开编辑画布”。画布使用 PeopleLib 的 Fabric 分页批注与 Quill 跨页正文,支持纸张模板、增删页、笔迹、高亮、整对象橡皮、选择及移动缩放旋转、浮动文字、图片和 PDF 底版。画布有固定页面边界;已保存记录、待办来源和编辑态默认容纳完整当前页及工具栏,高度随当前页实际显示尺寸变化,纵向由外层笔记流滚动。多页仍属于一条记录,通过页码翻页;窄窗口工具栏换行,纸张仅在自身视口内横向滚动,不撑宽外层页面。
- 画布通过“保存记录 / 保存修改”手动持久化,离开或切换记录类型前沿用未保存草稿确认。撤销/重做作用于当前批注页或 Quill 正文模式,增删页和 PDF 导入不属于全局撤销历史。删除非空批注页或 PDF 页需要在五秒内再次点击确认。
- 画布编辑与只读工具栏均提供缩小、当前百分比、放大和“适应宽度”。默认 100%,范围 25%–300%;加减每次调整 25 个百分点,点击百分比还原 100%。画布区域内 Ctrl/Command + 滚轮连续缩放,普通滚轮和 Shift 横滚保持原有方向与边界传递。适应宽度按实际视口计算,窗口或分栏调整后自动更新;翻页保留固定比例或适应宽度策略。缩放仅影响显示,整页高度与横向滚动范围随之变化,不新增内层纵向滚动,也不改变正文分页、保存内容、导出尺寸或未保存状态。控件沿用画布的主题、焦点环与提示,窄屏整组换行,适应宽度通过 `aria-pressed` 和选中样式表达状态。
- “导入 PDF”保留页面并提取原生文字供搜索与 AI 使用,扫描页不自动 OCR。“导出 PDF”将底版、正文和批注逐页合成为栅格 PDF,不保留可搜索文字层;当前 UI 无 PNG 下载按钮,页面 PNG 捕获仅作为程序接口提供。
- 画布新建与编辑态不显示常驻分析提示。操作放在编辑区右上角:新建与类型选择同排,顺序为“分析画布草稿、保存记录”;编辑放在记录头部,顺序为“分析画布草稿、取消、保存修改”。保存为最右侧主按钮,窄屏允许换行;底部不重复操作,新建不增加取消按钮。`immediate` 仍通过按钮手动分析,不随笔画自动请求;`after-save-auto` 在手动保存后分析。设置读取或页面捕获失败不阻断保存,保存成功后通过应用通知说明自动分析失败,可稍后重新分析;模型分析失败也不回滚已保存内容。
- 魔法笔记设置提供“发送画布页数”单选框,复用设置表单 `.field` 控件样式,范围 1~8,默认 1。标题同行的问号使用 `InlineHelp`,下拉框通过稳定的帮助 ID 关联展开的说明,交互遵循 6.14。说明包含当前页序前 N 页、不足时发送全部页、数量指单个画布页数而非记录数,以及完整保存与导出不受影响。修改后沿用设置保存反馈,下次分析生效。
- 草稿、已保存画布记录及来源为画布的待办,按默认模型的 `supportsImageInput` 使用所选前 N 页图像与文字,或只发送同一范围文字并标注“仅文字分析(未使用画布图像)”。所选页只有视觉内容的记录/草稿遇到文字模型时提示切换支持图像输入的默认模型,不声称已理解笔迹或布局。待办分析同时保留任务标题、说明和所选来源画布页的上下文。
- 画布文字评论在提取的 `plainText` 不变时保留;图像评论随画布内容或布局变化失效,在保存后自动评论模式下重新分析。草稿内容变化清除旧草稿分析,评论区显示实际使用的输入模式。存储和判定细节见[画布技术设计](./docs/features/magic-notes/technical-design.md#canvas-analysis-and-comments)。
- 默认进入笔记总览,不自动打开第一篇笔记。笔记以自适应卡片网格平铺,卡片显示标题、最多两行内容预览、记录数和本地化更新时间。点击笔记进入详情,详情不保留内部左列表;“返回总览”恢复搜索、滚动位置和入口焦点。新建入口在笔记总览与待办页可见,创建成功后进入笔记详情。
- 待办页是独立任务视图:搜索任务标题、说明或来源笔记,搜索框右侧同一工具栏放置“全部 / 未完成 / 已完成”筛选,显示匹配数与总数;任务按来源笔记分组。列表独立滚动,页头、搜索和筛选保持可见,窄屏工具栏空间不足时换行。直接勾选完成或恢复未完成,不自动选择任务;状态变化后按当前筛选更新列表。
- 待办内容容器宽于 `700px` 时,任务列表稳定在左侧,右侧显示当前任务说明、来源记录和 AI 评论;未选择时右侧提示选择任务。点击任务只更新右侧,不改变列表宽度、行位置或滚动位置,再次点击取消选择。AI 不增加第三栏。容器不超过 `700px` 时,按需详情替换同一内容区域,并提供“返回待办列表”;列表保持挂载,返回恢复滚动和任务行焦点,不将列表与详情上下堆叠。任务按钮提供 `aria-expanded` 和 `aria-controls`。
- 待办左右栏分割线支持拖拽,并记住列表宽度;键盘左右方向键每次调整 `16px`,Home/End 调整至边界。列表默认 `320px`、最小 `240px`,详情至少保留 `300px`;窄屏隐藏分割线,临时缩窄窗口不覆盖已保存宽度。每个来源笔记标题可独立收起或展开待办列表,保留标题与数量,通过 `aria-expanded` 和 `aria-controls` 表明状态;收起不清除右侧当前详情。
- 待办 AI 评论保留方向选择、流式输出、重新分析和已有结果;来源加载失败通过应用通知提示,并提供就地重试。从待办打开原笔记后,“返回待办”恢复任务选择、搜索、筛选和列表滚动;宽屏焦点返回任务行,窄屏返回待办详情中的返回按钮。
- 笔记详情主区按新到旧连续展示全部记录。打开笔记即在标题下、历史记录上显示空白输入区,保留文字/画布类型选择和保存操作,标题栏与索引均不提供“新增记录”按钮。新增保存成功后重建空白输入区,保留所选类型;自动分析结束也不转入旧记录编辑。旧记录通过显式编辑入口修改,保存使用同一 ID 和返回的修订号。点击索引或当前笔记内的 AI 来源只按 entry ID 滚动定位,不卸载编辑器、不清草稿、不询问离开。空白输入区离开不提示,返回总览、打开其他笔记或改为编辑另一条记录时仍保护未保存修改。
- 详情页的一级标题显示当前笔记已保存的标题,不显示模块说明;全局范围、返回导航和索引开关保持可见。原标题输入框使用正文大小,保留原有编辑、失焦保存与键盘操作,不形成第二个醒目标题。新建文字记录的输入区初始约四行,随内容自然增高,保留原有调整大小与滚动方式;此高度不作用于旧记录编辑器或画布。笔记内部列表、记录流、AI 区域和文字编辑区在精细指针设备上使用细原生滚动条,轨道透明;强制颜色与粗指针设备保留系统默认。说明、摘要和输入提示使用 `--font-body` 与 `--text-secondary`,时间和短标签保留 `--font-caption`。
- 左侧记录索引默认 `168px`,可拖动右边界在 `140–320px` 内调整栏宽,缩略图随列宽等比适配。开关独立位于详情标题左侧,提供 `aria-controls`、展开状态和动作提示;收起后完全移除索引列、分隔条、间隙、边框及轨道,不占宽度,隐藏内容不可聚焦。右侧 AI 评论独立显示、滚动和收起,不与索引共享页签。每条记录一个导航项:画布显示真实首页缩略图及总页数,文字显示摘要,两者均显示类型和时间。两侧分别记住桌面展开状态和宽度,共用原有布局偏好;宽布局分隔条共用样式及 `separator` 语义,方向键每次调整 `16px`,Home、End 跳到当前上下限。两侧宽度按可用空间协调限制,为主区保留至少 `300px`,窗口缩小时不覆盖保存的偏好。内容容器不超过 `800px` 时索引默认隐藏,由同一标题栏按钮开闭标题栏下方固定 `168px` 的抽屉,不显示调宽分隔条;标题栏自身保留控件高度,不保留轨道。选择记录或按 Escape 后关闭,Escape 将焦点返回开关。AI 移至底部,高度不超过可用布局的 40% 且最多 `280px`,不显示分隔条。
AI 评论的唯一显隐入口位于页面右上角,使用紧凑面板图标按钮;展开时应用共享选中样式,图标随显示或隐藏动作切换,并提供对应的工具提示、可访问名称、`aria-expanded` 和 `aria-controls`。评论面板内部不再显示重复的“AI 评论”标题或隐藏按钮行,保留区域的可访问名称、评论方向选择及各记录的分析操作。
- 笔记详情的内部记录索引使用透明底色,与主内容表面连续,仅保留细调宽分隔线。每条索引记录保留 `1px` 的 `--border-subtle` 淡边框,不加卡片底色或阴影,悬停使用轻微次级底色;选中记录使用淡蓝背景和左侧 `3px` 蓝色内侧标记,悬停时保留选中态。缩略图保留纸张边界,键盘焦点环与强制颜色模式选中边框保持可见。窄屏抽屉使用不透明的主内容表面,避免下层正文透出;深色主题沿用语义令牌。
- 笔记详情的标题、正文和记录流不再包裹额外的大外框,记录流容器不加内边距;缩略图列表顶部与正文区顶部对齐,左边界与索引栏左边界对齐,仅在右侧和底部保留 `8px` 留白,键盘焦点环向内绘制以避免滚动容器裁切。每条已保存记录使用一层 `1px` 的 `--border-subtle` 淡边框和 `--radius-control` 圆角,保留透明底色与记录间留白,不加阴影或头部下划线。记录编辑时保留输入区边框、页面安全边距及 AI 区域分隔。此规则仅作用于笔记详情,不改变卡片总览与待办独立布局。
- 画布新建、编辑和只读态统一保留 `12px` 内容间距;详情中已保存画布去掉预览外框和工具栏底色、下划线,保留灰色画布衬底与白色纸张,编辑态保留编辑器边框。记录头部操作按内容宽度排列,空间不足时自然换行。文字新建的保存区与编辑器属于同一卡片,空白编辑区使用最小高度,不随容器百分比高度拉长。文字工具栏图标、菜单和原生滚动条须跟随主题。
- 整篇笔记的置顶、取消置顶和删除放在卡片的“更多”菜单,标题编辑区不重复提供入口。按钮在悬停、键盘聚焦或菜单展开时显示,触屏设备常显;菜单沿用会话列表样式,通过 Portal 避免滚动区域裁切,支持方向键、Home、End 和 Escape。删除在菜单内二次确认并说明笔记标题及全部记录;打开菜单不进入详情。
- 左侧“魔法笔记”入口可显示未完成待办数量。数量为 0 时隐藏徽标,超过 99 时显示 `99+`,可访问名称仍说明精确数量;勾选、取消勾选以及 Agent / MCP 对笔记清单的写入都必须刷新数量。
- 未完成待办徽标的高度不得超过主导航图标;数量从 0 变为可见或再次隐藏时,主导航项行高和相邻菜单位置必须保持不变。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- Agent / MCP 或其他入口成功写入笔记后,已打开的工作台自动更新笔记列表、当前详情、待办及来源记录,不增加刷新按钮。保留仍存在的笔记与待办选择、搜索和筛选,以及未保存的标题、输入区和记录编辑草稿;普通后台刷新不重建编辑器。
- 外部删除当前笔记时,无草稿则返回总览;有草稿则保留编辑区并提示先复制草稿。外部删除正在编辑的记录时,同样保留该编辑器并明确提示记录已删除;保存失败不得清空草稿。
- 当前记录草稿非空或标题尚未保存时,返回总览必须先就地确认;编辑另一条记录等会丢弃正文草稿的操作沿用同一保护。继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 保存成功后,以该修订的编辑器初始化并完成 flush 后的内容作为修改比较基线;没有新编辑时返回不再提示。编辑器归一化不算用户修改,后续输入、画布变更和保存失败仍保留离开保护;同修订的后台刷新不替换已建立的基线。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
- 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。
### 13.8 设置中心
- 设置中心以整窗遮罩上的大 Modal 打开,保留底层工作区、会话草稿和助手工作栏,不切换一级页面。固定标题区和右上角关闭按钮,左侧分类与右侧内容各自滚动。关闭后返回原工作区并恢复入口焦点。
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
- Modal 最大尺寸为 `1200px × 900px`,常规外边距为 `24px`;窗口宽度不超过 `1020px` 或高度不超过 `720px` 时接近全屏,保留 `16px` 外边距。左侧分类宽度依次为 `220px`、`180px`(宽度不超过 `1020px`)、`120px`(不超过 `720px`);极窄窗口隐藏分类辅助说明,仍保留左右布局。内容区在可用空间内伸缩,最大宽度为 `960px`。
- 设置 Modal 通过 Portal 挂载到 `document.body`,复用共享背景隔离与焦点恢复机制。打开后聚焦关闭按钮,Tab 在弹窗内循环;Escape 先交由嵌套弹窗处理,设置自身的 Escape 与关闭按钮共用未保存确认,确认出现后再按 Escape 返回编辑。点击遮罩不关闭;保存、测试和清理数据期间禁止关闭。每次打开重新挂载设置,模型和消息通道等直达入口使用本次指定分类,普通入口默认打开 Agent Runtime。
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与说明来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
- 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。
- 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。
- 智能心跳的单条配置不在设置中心重复管理。设置中心如需呈现平台级说明,只提供
“打开智能心跳”导航,不复制创建、暂停、恢复或删除表单。
- 保存或测试成功统一进入应用通知视口,并按全局规则自动消失,不在分类页头或内容卡片中保留持久成功文案。加载、保存和测试错误显示在分类页头下方,并保留可处理的上下文。
- “模型连接”的认证字段按“认证方式 → API Key → 凭据状态”排列;只有选择 API Key 时显示输入框。已保存的 API Key 属于该连接,修改模型服务地址或临时选择“无需认证”不得自动清除,也不得要求重新输入;仅由明确的“清除凭据”或“删除连接”操作移除。
- “保存并测试模型”必须在保存后发送一次有界的真实模型请求。文本连接校验本次随机测试文本,图像连接校验实际返回的内联图片;仅收到成功 HTTP 状态、模型列表或健康检查响应不算测试成功。界面持续说明该操作可能产生少量服务商用量费用,成功反馈明确写为“真实生成测试通过”。
- 所有显式保存的设置草稿都参与离开保护:关闭、切换分类、主侧栏或工作区导航及托盘导航不得静默丢弃,统一通过设置中心的就地确认提供继续编辑与明确放弃入口,保存失败后保留输入。“平台功能 / 通用设置”承载全局快捷唤起的共享 Switch、可访问 accelerator 录制输入、恢复默认、保存及注册、停用或冲突状态,不新增分类或页签;注册或持久化失败时保留上一组可用快捷键和当前草稿,保存或停用成功后同步更新输入区的快捷键提示。
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
更新,保存失败时保留最后确认状态。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”和“远程项目”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、同行帮助和原生单选下拉框放在同一行,用途说明收进 `InlineHelp`,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
- Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。
- Agent Runtime 页面在低层程序与配置覆盖之外提供“能力与默认配置”区域。能力清单使用共享 `PageTabs`,按 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 类单行滚动展示,一次只呈现当前分类的 `tabpanel`;清单只显示 Runtime 自有能力,不混入 GoodBuddy 分配的 Skills、临时 MCP 或 Continue 预设。Tools 必须独立于 Commands、LSP 和 Formatters,显示工具类型、来源及 Ask/Execute 可用性;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。
- “能力与默认配置”只显示一个模块标题,刷新入口位于该标题右侧,能力状态压缩为一行并排在默认 Agent 或 Continue 预设编辑器之前;不得再复制“Runtime 原生能力”等同义标题、说明或状态结论。刷新只更新能力快照,不覆盖未保存的原生定制草稿。
- OpenCode 的默认 Agent 使用原生下拉选择;Continue 预设编辑器允许管理名称、说明、启用的 Rules 以及 Prompt 名称、说明和正文,并可展开查看原生 Rules 与启用预设 Rules 的最终合并顺序。持久启停仍使用共享 Switch,添加与删除使用明确按钮和可访问名称。
- 魔法笔记设置统一从应用中心对应行进入;笔记页和全局设置不再提供重复入口。“显示未完成待办数量”持久 Switch 只控制左侧数量徽标,不改变待办数据。旧入口开关绑定业务启用值,迁为“启用应用”并保留原值,常驻使用独立配置。默认开启仅适用于未配置布尔值,只有独立显隐偏好才映射到常驻。此迁移规则见[应用导航 PRD](./docs/features/application-tool-navigation/prd.md#fr-11-应用导航设置)。
- MCP 设置按“内置 MCP / 直连模型 / 自定义 MCP / 电脑控制”四个同级 `PageTabs` 组织。联网搜索保留在直连模型;内置浏览器归入内置 MCP,提供持久启停和 Runtime 分配,并明确说明其操作 GoodBuddy 隔离浏览器、不控制客户端已安装的浏览器,开启后可由已分配 Runtime 的 Execute 直接使用,不逐次询问。该开关只控制 Agent 工具访问,关闭时不得禁用浏览器工作栏中的用户手动导航、返回、刷新、停止加载、交互或关闭操作。尚未生效的命名浏览器配置不得显示在界面中,“电脑控制”只显示实际操作客户端电脑的能力。内置 MCP 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
- MCP Server 测试结果在同一展开卡片中分组显示 Tools、Prompts 和 Resources 的支持状态、数量与有界元数据;Prompt 参数标明必填项,Resource 只显示 URI、名称、类型和说明,不读取或渲染 Resource 内容。
- 内置 MCP 中的“图片生成与编辑”是自动管理例外:该行始终可见,标记“内置 · 自动管理”,不提供独立启停、分配保存、移除或凭据编辑。直连 Model、OpenCode、Continue、DeepSeek Harness 的分配只由已保存图片模型的“允许 AI 在会话中调用”设置推导;至少一个开启则全部勾选,没有开启模型则全部取消。使用禁用的原生 Checkbox、Runtime 标签及关联说明,保留可聚焦的“前往图片模型设置”导航;远程适用范围通过 GoodBuddy Agent 说明持续可见。Ask、离线、无工具能力或调用错误不改变勾选。图片模型默认关闭会话调用;默认生图模型独立于普通默认连接,关闭或删除后保留失效选择并提示改选,不自动回退。
### 13.9 文档解析设置
现有本地解析与本轮 HTTP 扩展的实现边界见[文档处理进度](./docs/features/document-processing/progress.md)。
跨场景入口、附件菜单、共享预览及选图返回规则由[文档处理 UI 设计](./docs/features/document-processing/ui-design.md)维护;
当前接线与存储行为见[生产实现说明](./docs/features/document-processing/implementation.md)。文档解析属于全局设置,不新增应用中心成员;本机推理监控只管理本地服务。
- 设置中心新增独立的“文档解析”分类,统一管理聊天附件、知识库导入以及后续文档审阅场景使用的提取、转换和 OCR 策略。OCR 不作为普通对话模型出现在“模型连接”中。
- 分类页头说明文档解析的跨场景作用,右侧依次显示“测试解析”和“保存设置”;保存位于最右侧。测试必须选择真实文件并执行实际解析,不能只检查模型文件或接口连通性。
- 页面首先显示原生解析、文档转换和 OCR 的运行状态,并明确当前可处理格式、回退能力与不可用原因。部分能力未配置时使用“部分可用”状态,不得把原生文本解析一并标记为失败。
- “使用场景”分别配置聊天附件和知识库导入。普通用户选择“自动解析”“快速文本”“完整索引”等预设;阈值、并发和超时放入默认折叠的高级设置。
- 本地 OCR 的全平台基线使用同一组 PP-OCRv6 ONNX 模型和 ONNX Runtime WebAssembly,在 Windows、macOS、Linux 的 x64 与 arm64 上保持相同功能。原生 ONNX、WebGPU、DirectML、CoreML 或 CUDA 只能作为可选加速,失败时必须回退到 WASM CPU。
- OCR 模型管理与语音模型保持一致:应用不内置权重,用户可按需从 ModelScope 下载,也可在联网设备导出 ZIP 并在离线或内网设备直接导入。语音和 OCR 模型的下载、取消、ZIP 导入、ZIP 导出、删除与打开受管目录使用同一交互语义;ZIP 操作不得隐式切换当前模型或保存解析设置。
- OCR 模型卡片必须持续显示来源、语言、运行时、体积、安装状态和许可。“打开 ModelScope”直接位于卡片右上角,不再使用“模型详情与手动导入”折叠区。窄窗口下仓库操作换行到模型摘要下方,仍须保持可访问名称和键盘操作。
- PP-OCRv6 提供三个已实现档位:Tiny 约 6 MiB,适合低资源设备;Small 约 30 MiB,官方支持 50 种语言并作为推荐档位;Medium 约 132 MiB,官方支持 50 种语言、质量更高但速度较慢,界面必须提示其更高的内存占用和延迟。
- 本地模型按受管目录和固定清单加载。ModelScope 下载地址必须固定不可变 revision、字节数和 SHA-256;下载先进入临时目录,全部校验成功后再原子安装。识别时不得从网络或可变分支临时加载模型。
- 模型 ZIP 使用版本化的 `goodbuddy-model.json` 清单,声明模型类型、内置目录 ID、文件角色、大小与 SHA-256。导出前重新校验已安装文件;导入时限制压缩包大小、条目数、单文件和总展开大小,拒绝路径穿越、重复、未知、缺失或嵌套条目,并以应用内置目录重新校验后原子安装。ZIP 内的自声明信息不能扩大受信任模型集合。
- PDF 先读取文本层。仅当页面无有效文本、乱码比例过高或用户选择“始终 OCR”时渲染该页并识别;不得因为单页需要 OCR 而丢弃其他页面已经提取的可靠文本。
- DOCX、XLSX、PPTX 优先保留段落、单元格、公式、备注等原生语义。转换为 PDF 用于补充版面、页码、图表和图片理解,不作为唯一中间格式。
- DOC、XLS、PPT 等旧格式通过受控转换 Provider 生成新式 Office 文档和 PDF。转换子进程必须禁用宏和网络,限制输入、输出、内存、超时与临时目录,并在关闭或取消时清理。
- OCR 来源使用“本地模型 / 远程服务”互斥选择,远程为 HTTP PaddleOCR-VL;MinerU 不在本轮范围。来源切换保留两套配置,未保存时禁止测试。来源配置、保存和测试规则见[文档解析设置设计](./docs/features/document-processing/ui-design.md#2-文档解析设置),不另加逐文件授权区。
- 用户配置并保存远程 OCR 服务即表示选择该处理路径,不再逐场景重复询问。界面仍须明确显示当前服务名称、处理范围和远程属性,API 密钥只保存在主进程加密设置中,未选中远程服务时不得上传文档。
- 解析结果使用统一文档结构,保留文档标题、来源格式、页码或工作表定位、正文块、处理方式和警告;置信度仅在解析器提供时显示,缺失不补造。聊天文档附件保留完整解析文本,超过暂存容量时报错;知识库使用完整结果分块和索引,具体要求见[文档处理 PRD](./docs/features/document-processing/prd.md#32-场景工作流)。普通图片默认走图片输入,显式 OCR、原图保存和文档选图遵循该功能的规划规则。
- 测试结果显示文件类型、页数、实际工作流、提取字数、OCR 页数、耗时和警告。测试文件不得自动进入聊天上下文或知识库。
## 14. 文案规则
- 使用简体中文,动词直接、对象明确。
- 页面标题使用名词,例如“知识库”“智能心跳”“活动记录”。
- 按钮使用“动词 + 对象”,例如“新建任务”“导入知识”“停止运行”。
- 状态文案描述事实,例如“正在同步”“上次运行失败”,不使用含糊的“异常”。
- 错误提示包含发生了什么、用户可以做什么。保留必要的错误上下文,但不得暴露凭据、授权头、私人文档或未脱敏的提供商响应。
- 同一概念固定使用一个名称,不交替使用“项目空间”“工作区”“范围”等近义词。产品内统一使用“范围”表达全局与项目归属。
## 15. 迁移检查清单
### 15.1 基础层
- [ ] 建立浅色与深色语义颜色令牌,移除业务组件中的原始颜色值。
- [ ] 建立白色浅色主画布、冰蓝灰侧栏与深海军蓝深色表面的稳定层级。
- [ ] 建立间距、字体、圆角、阴影、层级和动效令牌。
- [ ] 为主题切换、减少动态效果和原生控件设置全局规则。
- [ ] 建立组件交互状态和焦点环基线。
- [ ] 验证浅色侧栏结构分隔线与导航、会话选中状态清晰可辨。
### 15.2 页面壳层与层级
- [ ] 实现 `PageShell` 的 `reading`、`standard`、`dashboard`、`master-detail` 变体。
- [ ] 按页面映射替换 `820px`、`900px`、`1040px` 和全宽等分散宽度声明。
- [ ] 每个一级页面只保留一个 `PageHeader` 和一个一级标题。
- [ ] 移除内容卡片中与页面标题重复的标题。
### 15.3 共享组件
- [ ] 实现并迁移 `PageHeader`。
- [ ] 使用 `PageTabs` 统一同级页面导航。
- [ ] 使用 `SegmentedControl` 统一少量互斥视图和状态切换。
- [ ] 需要分段外观的同级面板使用 `PageTabs` 的共享 `segmented` 变体,不复制控件样式。
- [ ] 建立统一筛选工具栏,移除以页签样式伪装的筛选。
- [ ] 二元启停统一使用共享 Switch 视觉与 `role="switch"`,多选、范围分配和确认项保留 Checkbox。
- [ ] 将短期成功、信息和非局部异步错误接入应用通知视口,移除页面专属通知横幅。
- [ ] 实现 `ScopeBadge` 并覆盖全局、项目、失效和可切换状态。
- [ ] 实现 `EmptyState` 的首次为空、无结果、失败和只读变体。
- [ ] 实现 `danger-ghost`、`danger-solid` 和 `danger-zone`。
- [ ] 统一模型、专家角色和工作模式的单选菜单结构、视觉与键盘行为。
### 15.4 页面迁移
- [ ] 聊天迁移到 `reading`,统一消息流与输入区宽度。
- [ ] 将输入快捷键与附件提示置于空输入框内部,输入区下方保持单行说明。
- [ ] 最近对话迁移到 `standard`,统一搜索、范围、时间和删除行为。
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
- [x] 监督者使用 `supervisor`,工作回顾读取统一监督结果;自动监督包含 Global / 多 Project 计划、概览、自动监督报告、建议与审计;活动记录显示统一执行,设置只保留算法参数。
- [x] 在现有工作栏中完善任务中心,统一范围、状态、最近进展和筛选,不新建平行任务平台。
- [ ] 活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
- [ ] 设置中心使用共享分类定义与 `SettingsCategoryHeader`,将保存与测试操作统一放到分类页头右侧,并把成功反馈接入应用通知。
- [ ] 文档解析设置统一聊天附件与知识库的解析预设、OCR 状态、转换状态、隐私限制和真实文件测试。
### 15.5 验收
执行与取证方法见 [Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md),包括真实 Portal、原生键盘、计算样式与 PNG 审阅;视觉 fixture 与完整生产 App 的结果须分别标注。
- [ ] 在浅色与深色主题下检查所有页面、弹窗、菜单、输入框、表格和空状态。
- [ ] 在大于等于 `1200px`、`960px`、`720px` 和小于 `720px` 的窗口宽度检查布局。
- [ ] 仅使用键盘完成导航、筛选、创建、编辑、确认和取消。
- [ ] 验证焦点恢复、可访问名称、表单错误关联和实时状态播报。
- [ ] 验证页面范围、对象范围和操作范围在关键流程中始终可见。
- [ ] 验证删除、批量操作、停止运行和清空历史符合风险等级策略。
- [ ] 验证加载中、首次为空、筛选无结果、搜索无结果、失败和只读状态不会互相混用。
- [ ] 在 Windows、macOS、Linux 的 x64 与 arm64 上执行真实本地 OCR,并验证 WASM CPU 回退、取消、超时和离线运行。
- [ ] 在联网设备导出语音与 OCR 模型 ZIP,在离线设备导入后执行真实推理;验证错误模型 ID、篡改文件、路径穿越、未知条目和压缩炸弹均被拒绝。
## 16. 完成标准
一个页面只有在满足以下条件时才视为完成迁移:
1. 使用明确的 `PageShell` 变体,没有页面专属整体宽度。
2. 使用统一标题层级、共享导航和筛选控件。
3. 所有颜色、间距、字体、圆角和阴影来自令牌。
4. 全局或项目范围在浏览、创建、编辑和危险操作中均可见。
5. 浅色、深色、键盘和各窗口宽度下均可完成核心任务。
6. 空状态、错误状态和危险操作符合本文规则。
## 17. 一级页面加载性能
- 首次启动只可在低优先级空闲时预加载轻量一级页面;Knowledge、Magic Notes、
Settings、Activity 等较重页面应在对应导航控件获得指针意图或键盘焦点时预加载。
- 点击、快捷键和程序化导航不能依赖预加载完成,必须保留页面级 `Suspense` 加载
状态、错误边界和 KeepAlive 行为。
- Workspace 与 Conversation 的 KeepAlive 缓存必须在每次访问时立即执行容量上限
与 LRU 保护规则;定时清理只负责过期与数据失效兜底,不能作为容量门禁。
- 页面内大型可选视图应使用局部加载边界。知识图谱画布加载失败时,只替换画布
区域并提供可访问的重试操作,不得替换整个 Knowledge 页面或丢失其余页面状态。
- 加载状态使用 `role="status"`、`aria-live="polite"` 与 `aria-busy="true"`;
局部加载失败使用 `role="alert"`,并保留明确的恢复操作。
+35
View File
@@ -0,0 +1,35 @@
{
"formatVersion": 1,
"agentVersion": "0.13.4",
"protocol": {
"major": 2,
"minor": 0
},
"node": {
"version": "24.19.0",
"source": "https://nodejs.org/dist/v24.19.0/",
"targets": {
"linux-x64": {
"archive": "node-v24.19.0-linux-x64.tar.gz",
"sha256": "f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4",
"binaryPath": "node-v24.19.0-linux-x64/bin/node",
"licensePath": "node-v24.19.0-linux-x64/LICENSE"
},
"darwin-arm64": {
"archive": "node-v24.19.0-darwin-arm64.tar.gz",
"sha256": "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d",
"binaryPath": "node-v24.19.0-darwin-arm64/bin/node",
"licensePath": "node-v24.19.0-darwin-arm64/LICENSE"
},
"linux-arm64": {
"archive": "node-v24.19.0-linux-arm64.tar.gz",
"sha256": "d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f",
"binaryPath": "node-v24.19.0-linux-arm64/bin/node",
"licensePath": "node-v24.19.0-linux-arm64/LICENSE"
}
}
},
"koffi": {
"version": "3.1.4"
}
}
+18
View File
@@ -0,0 +1,18 @@
const { createHash } = require('node:crypto')
const { mkdirSync, readFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
const { execFileSync } = require('node:child_process')
const { readRemoteRuntimeLock, resolveLockedRuntimeInput } = require('./remote-runtime-bundle.cjs')
const [directory, architecture, platform = 'linux'] = process.argv.slice(2)
if (!directory) throw new Error('Continue archive destination is required')
const input = resolveLockedRuntimeInput(readRemoteRuntimeLock(), architecture, platform, 'continue')
const destination = resolve(directory)
mkdirSync(destination, { recursive: true })
execFileSync('npm', ['pack', `${input.packageName}@${input.version}`, '--ignore-scripts',
'--registry=https://registry.npmjs.org', '--pack-destination', destination], { stdio: 'ignore' })
const archive = join(destination, input.archive)
if (`sha512-${createHash('sha512').update(readFileSync(archive)).digest('base64')}` !== input.integrity) {
throw new Error('Continue archive integrity mismatch')
}
process.stdout.write(`${archive}\n`)
+33
View File
@@ -0,0 +1,33 @@
const { readFileSync } = require('node:fs')
const {
detectBinaryArchitecture,
detectElfArchitecture
} = require('./binary-architecture.cjs')
function targetName(architecture, platform = 'linux') {
if (
!(platform === 'linux' && ['x64', 'arm64'].includes(architecture)) &&
!(platform === 'darwin' && architecture === 'arm64')
) {
throw new Error(`Unsupported Agent build target: ${platform}-${architecture}`)
}
return `${platform}-${architecture}`
}
function assertTargetBinary(filePath, architecture, platform, label) {
targetName(architecture, platform)
const header = readFileSync(filePath).subarray(0, 64)
const isMachO = header.length >= 8 &&
(header.readUInt32LE(0) === 0xfeedfacf ||
header.readUInt32BE(0) === 0xfeedfacf)
const actual = platform === 'linux'
? detectElfArchitecture(header)
: isMachO ? detectBinaryArchitecture(header) : undefined
if (actual !== architecture) {
throw new Error(
`${label} architecture mismatch: expected ${platform}-${architecture}, received ${actual ?? 'unknown'}`
)
}
}
module.exports = { targetName, assertTargetBinary }
File diff suppressed because it is too large Load Diff
+960
View File
@@ -0,0 +1,960 @@
const {
createHash,
createPublicKey,
sign,
verify
} = require('node:crypto')
const {
closeSync,
fstatSync,
openSync,
readFileSync,
readSync,
writeFileSync
} = require('node:fs')
const { basename, resolve } = require('node:path')
const { crc32 } = require('node:zlib')
const {
productionSigningKey,
publicKeySpkiBase64,
readTrustedKeyRegistry,
redactSecrets
} = require('./agent-bundle.cjs')
const {
canonicalJson,
descriptorContentDigest,
signatureDomain: packageSignatureDomain
} = require('./agent-package.cjs')
const {
localFileHeaderSignature,
readExactZipBytes,
readZipCentralDirectory
} = require('./zip-central-directory.cjs')
const root = resolve(__dirname, '..')
function supportedTarget(platform, architecture) {
return (platform === 'linux' && ['x64', 'arm64'].includes(architecture)) ||
(platform === 'darwin' && architecture === 'arm64')
}
const catalogName = 'agent-catalog.json'
const catalogSignatureName = 'agent-catalog.sig'
const catalogSignatureDomain = Buffer.from(
'GoodBuddy Agent Package Catalog Signature v1\0',
'utf8'
)
const semanticVersionPattern =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u
const sha256Pattern = /^[a-f0-9]{64}$/u
const maximumPackageBytes = 512 * 1024 * 1024
const maximumPackageEntries = 50_002
const maximumPackageEntryBytes = 384 * 1024 * 1024
const maximumExpandedPackageBytes = 1024 * 1024 * 1024
const maximumMetadataBytes = 1024 * 1024
const maximumDescriptorBytes = 4 * 1024 * 1024
const maximumCentralDirectoryBytes = 32 * 1024 * 1024
const maximumCatalogEntries = 200
const windowsReservedNamePattern =
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu
function canonicalBytes(value) {
return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8')
}
function trustedProductionKey(registry, keyId) {
const key = registry.keys.find(
(candidate) =>
candidate.keyId === keyId &&
candidate.environment === 'production'
)
if (
!key ||
registry.revocations.some(
(revocation) => revocation.keyId === keyId
)
) {
throw new Error(`Agent signing key is not trusted: ${keyId}`)
}
return key
}
function verifyDetachedSignature(
bytes,
signatureText,
key,
domain,
label
) {
const signature = Buffer.from(
signatureText.toString('utf8').trim(),
'base64'
)
const publicKey = createPublicKey({
key: Buffer.from(key.publicKeySpkiBase64, 'base64'),
format: 'der',
type: 'spki'
})
if (
signature.length !== 64 ||
!verify(
null,
Buffer.concat([domain, bytes]),
publicKey,
signature
)
) {
throw new Error(`${label} signature verification failed`)
}
}
function parseCanonicalJson(bytes, label) {
const value = JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(bytes)
)
if (!canonicalBytes(value).equals(bytes)) {
throw new Error(`${label} is not canonical`)
}
return value
}
function assertProtocol(value, label) {
if (
typeof value !== 'object' ||
value === null ||
!Number.isSafeInteger(value.major) ||
value.major < 1 ||
!Number.isSafeInteger(value.minor) ||
value.minor < 0
) {
throw new Error(`${label} is invalid`)
}
}
function assertPackageDescriptor(descriptor) {
if (
typeof descriptor !== 'object' ||
descriptor === null ||
descriptor.format !== 'goodbuddy-agent-package' ||
descriptor.formatVersion !== 1 ||
descriptor.product !== 'GoodBuddy' ||
descriptor.component !== 'agent' ||
!semanticVersionPattern.test(descriptor.version ?? '') ||
!semanticVersionPattern.test(
descriptor.minimumDesktopVersion ?? ''
) ||
!supportedTarget(descriptor.platform, descriptor.architecture) ||
!['x64', 'arm64'].includes(descriptor.architecture) ||
typeof descriptor.signingKeyId !== 'string' ||
!/^sha256:[a-f0-9]{64}$/u.test(
descriptor.contentDigest ?? ''
) ||
!Array.isArray(descriptor.files) ||
descriptor.files.length === 0 ||
descriptor.files.length > 50_000 ||
typeof descriptor.remoteRuntime !== 'object' ||
descriptor.remoteRuntime === null ||
!['opencode', 'continue'].includes(descriptor.remoteRuntime.runtimeId) ||
descriptor.remoteRuntime.provider !== descriptor.remoteRuntime.runtimeId ||
!semanticVersionPattern.test(
descriptor.remoteRuntime.version ?? ''
) ||
!/^sha256:[a-f0-9]{64}$/u.test(
descriptor.remoteRuntime.bundleDigest ?? ''
)
) {
throw new Error('Agent package descriptor is invalid')
}
assertProtocol(descriptor.agentProtocol, 'Agent protocol')
assertAdditionalRuntimes(descriptor)
assertProtocol(
descriptor.remoteRuntime.protocol,
'Remote Runtime protocol'
)
const paths = new Set()
for (const file of descriptor.files) {
if (
typeof file !== 'object' ||
file === null ||
!safePackagePath(file.path) ||
!Number.isSafeInteger(file.size) ||
file.size < 0 ||
!sha256Pattern.test(file.sha256 ?? '') ||
!['0644', '0755'].includes(file.mode) ||
paths.has(file.path)
) {
throw new Error('Agent package file inventory is invalid')
}
paths.add(file.path)
}
if (
descriptor.contentDigest !==
descriptorContentDigest(descriptor)
) {
throw new Error('Agent package content identity is invalid')
}
}
function readPackageMetadata(archivePath, registry) {
const archive = resolve(archivePath)
const streamed = streamPackageMetadata(archive)
const descriptorBytes = streamed.descriptor
const signatureBytes = streamed.signature
if (!descriptorBytes || !signatureBytes) {
throw new Error('Agent package metadata is missing')
}
const descriptor = parseCanonicalJson(
Buffer.from(descriptorBytes),
'Agent package descriptor'
)
assertPackageDescriptor(descriptor)
verifyPackageFileInventory(descriptor, streamed.files)
const key = trustedProductionKey(
registry,
descriptor.signingKeyId
)
verifyDetachedSignature(
Buffer.from(descriptorBytes),
Buffer.from(signatureBytes),
key,
packageSignatureDomain,
'Agent package'
)
const expectedArchive =
`goodbuddy-agent-${descriptor.version}` +
`-${descriptor.platform}-${descriptor.architecture}.gbagent`
if (basename(archive) !== expectedArchive) {
throw new Error(
`Agent package archive must be named ${expectedArchive}`
)
}
return {
archive: expectedArchive,
descriptor,
size: streamed.size,
sha256: streamed.sha256
}
}
function safePackagePath(path) {
return (
typeof path === 'string' &&
path.length > 0 &&
path.length <= 512 &&
!path.startsWith('/') &&
!path.endsWith('/') &&
!path.includes('\\') &&
path.split('/').every(
(part) =>
part.length > 0 &&
part !== '.' &&
part !== '..' &&
!part.endsWith('.') &&
!part.endsWith(' ') &&
!part.includes(':') &&
[...part].every(
(character) => character.charCodeAt(0) > 0x1f
) &&
!windowsReservedNamePattern.test(part)
)
)
}
function verifyPackageFileInventory(descriptor, actualFiles) {
const expected = new Set([
...descriptor.files.map((file) => file.path),
'agent-package.json',
'agent-package.sig'
])
if (
actualFiles.size !== expected.size ||
[...actualFiles.keys()].some((path) => !expected.has(path))
) {
throw new Error(
'Agent package contains undeclared or missing files'
)
}
for (const file of descriptor.files) {
const actual = actualFiles.get(file.path)
if (
!actual ||
actual.size !== file.size ||
actual.sha256 !== file.sha256
) {
throw new Error(
`Agent package payload verification failed: ${file.path}`
)
}
}
}
function centralDirectoryEntries(handle, archiveSize) {
const parsed = readZipCentralDirectory(handle, archiveSize, {
label: 'Agent package',
minimumEntries: 1,
maximumEntries: maximumPackageEntries,
maximumCentralDirectoryBytes
})
const entries = []
const seen = new Set()
let expandedBytes = 0
for (const entry of parsed.entries) {
if (
entry.flags !== 0x0800 ||
entry.compression !== 0 ||
entry.compressedSize === 0xffffffff ||
entry.size === 0xffffffff ||
entry.compressedSize !== entry.size ||
entry.size > maximumPackageEntryBytes ||
entry.diskStart !== 0 ||
entry.localOffset === 0xffffffff
) {
throw new Error('Agent package ZIP entry is unsupported')
}
if (
!safePackagePath(entry.name) ||
seen.has(entry.name)
) {
throw new Error(
'Agent package contains an unsafe or duplicate path'
)
}
seen.add(entry.name)
expandedBytes += entry.size
if (expandedBytes > maximumExpandedPackageBytes) {
throw new Error(
'Agent package expanded payload exceeds its limit'
)
}
const local = readExactZipBytes(
handle,
30,
entry.localOffset,
'Agent package local file header'
)
const localNameLength = local.readUInt16LE(26)
const localExtraLength = local.readUInt16LE(28)
if (
local.readUInt32LE(0) !== localFileHeaderSignature ||
local.readUInt16LE(6) !== entry.flags ||
local.readUInt16LE(8) !== entry.compression ||
local.readUInt32LE(14) !== entry.checksum ||
local.readUInt32LE(18) !== entry.compressedSize ||
local.readUInt32LE(22) !== entry.size ||
localNameLength !== entry.nameBytes.length
) {
throw new Error('Agent package local file header is invalid')
}
const localName = readExactZipBytes(
handle,
localNameLength,
entry.localOffset + 30,
'Agent package local file name'
)
if (!localName.equals(entry.nameBytes)) {
throw new Error('Agent package local file name is invalid')
}
const dataOffset =
entry.localOffset +
30 +
localNameLength +
localExtraLength
const recordEnd = dataOffset + entry.compressedSize
if (recordEnd > parsed.centralOffset) {
throw new Error('Agent package entry exceeds its data region')
}
entries.push({
checksum: entry.checksum,
dataOffset,
name: entry.name,
recordStart: entry.localOffset,
recordEnd
})
}
entries.sort(
(left, right) => left.recordStart - right.recordStart
)
for (let index = 1; index < entries.length; index += 1) {
if (entries[index].recordStart < entries[index - 1].recordEnd) {
throw new Error('Agent package ZIP entries overlap')
}
}
return entries
}
function sameFileSnapshot(left, right) {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.size === right.size &&
left.mtimeMs === right.mtimeMs &&
left.ctimeMs === right.ctimeMs
)
}
function streamPackageMetadata(archivePath) {
const handle = openSync(archivePath, 'r')
const values = new Map()
const files = new Map()
try {
const initialStatus = fstatSync(handle)
if (
!initialStatus.isFile() ||
initialStatus.size <= 0 ||
initialStatus.size > maximumPackageBytes
) {
throw new Error(
'Agent package archive is not a bounded file'
)
}
const entries = centralDirectoryEntries(
handle,
initialStatus.size
)
const states = entries.map((entry) => {
const wanted =
entry.name === 'agent-package.json' ||
entry.name === 'agent-package.sig'
const size = entry.recordEnd - entry.dataOffset
const maximum = entry.name === 'agent-package.json'
? maximumDescriptorBytes
: maximumMetadataBytes
if (wanted && size > maximum) {
throw new Error('Agent package metadata exceeds its limit')
}
return {
...entry,
bytes: 0,
contents: wanted
? Buffer.allocUnsafe(size)
: undefined,
crc32: 0,
hash: createHash('sha256')
}
})
const archiveHash = createHash('sha256')
const buffer = Buffer.allocUnsafe(1024 * 1024)
let position = 0
let firstEntry = 0
while (position < initialStatus.size) {
const bytesRead = readSync(
handle,
buffer,
0,
Math.min(buffer.length, initialStatus.size - position),
position
)
if (bytesRead <= 0) {
throw new Error('Agent package archive is truncated')
}
const chunk = buffer.subarray(0, bytesRead)
const chunkEnd = position + bytesRead
archiveHash.update(chunk)
while (
firstEntry < states.length &&
states[firstEntry].recordEnd <= position
) {
firstEntry += 1
}
for (
let index = firstEntry;
index < states.length &&
states[index].dataOffset < chunkEnd;
index += 1
) {
const state = states[index]
const dataStart = Math.max(position, state.dataOffset)
const dataEnd = Math.min(chunkEnd, state.recordEnd)
if (dataStart >= dataEnd) {
continue
}
const data = chunk.subarray(
dataStart - position,
dataEnd - position
)
state.hash.update(data)
state.crc32 = crc32(data, state.crc32)
if (state.contents) {
data.copy(
state.contents,
dataStart - state.dataOffset
)
}
state.bytes += data.byteLength
}
position = chunkEnd
}
if (!sameFileSnapshot(initialStatus, fstatSync(handle))) {
throw new Error(
'Agent package archive changed during verification'
)
}
for (const state of states) {
const size = state.recordEnd - state.dataOffset
if (
state.bytes !== size ||
(state.crc32 >>> 0) !== state.checksum
) {
throw new Error(
`Agent package ZIP checksum verification failed: ${state.name}`
)
}
files.set(state.name, {
size,
sha256: state.hash.digest('hex')
})
if (state.contents) {
values.set(state.name, state.contents)
}
}
return {
descriptor: values.get('agent-package.json'),
signature: values.get('agent-package.sig'),
files,
sha256: archiveHash.digest('hex'),
size: initialStatus.size
}
} finally {
closeSync(handle)
}
}
function catalogEntry(metadata) {
const descriptor = metadata.descriptor
const {
contentDigest: _contentDigest,
files: _files,
signingKeyId: _signingKeyId,
...identity
} = descriptor
void _contentDigest
void _files
void _signingKeyId
return {
...identity,
archive: metadata.archive,
size: metadata.size,
sha256: metadata.sha256,
downloads: {
github: {
url:
'https://github.com/mesalogo/goodbuddy/releases/download/' +
`agent-v${descriptor.version}/${metadata.archive}`
},
mirror: {
url:
'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' +
`agent-releases/v${descriptor.version}/${metadata.archive}`
}
}
}
}
function assertCatalog(catalog) {
if (
typeof catalog !== 'object' ||
catalog === null ||
catalog.formatVersion !== 1 ||
catalog.product !== 'GoodBuddy' ||
catalog.component !== 'agent' ||
typeof catalog.signingKeyId !== 'string' ||
typeof catalog.generatedAt !== 'string' ||
!Number.isFinite(Date.parse(catalog.generatedAt)) ||
new Date(catalog.generatedAt).toISOString() !==
catalog.generatedAt ||
!Array.isArray(catalog.entries) ||
catalog.entries.length === 0 ||
catalog.entries.length > maximumCatalogEntries
) {
throw new Error('Agent catalog is invalid')
}
const identities = new Set()
for (const entry of catalog.entries) {
if (
typeof entry !== 'object' ||
entry === null ||
!semanticVersionPattern.test(entry.version ?? '') ||
!semanticVersionPattern.test(
entry.minimumDesktopVersion ?? ''
) ||
!supportedTarget(entry.platform, entry.architecture) ||
!['x64', 'arm64'].includes(entry.architecture) ||
entry.archive !==
`goodbuddy-agent-${entry.version}` +
`-${entry.platform}-${entry.architecture}.gbagent` ||
!Number.isSafeInteger(entry.size) ||
entry.size <= 0 ||
!sha256Pattern.test(entry.sha256 ?? '') ||
entry.downloads?.github?.url !==
'https://github.com/mesalogo/goodbuddy/releases/download/' +
`agent-v${entry.version}/${entry.archive}` ||
entry.downloads?.mirror?.url !==
'https://goodbuddy.oss-cn-beijing.aliyuncs.com/' +
`agent-releases/v${entry.version}/${entry.archive}` ||
entry.format !== 'goodbuddy-agent-package' ||
entry.formatVersion !== 1 ||
entry.product !== 'GoodBuddy' ||
entry.component !== 'agent' ||
!['opencode', 'continue'].includes(entry.remoteRuntime?.runtimeId) ||
entry.remoteRuntime?.provider !== entry.remoteRuntime?.runtimeId ||
!semanticVersionPattern.test(
entry.remoteRuntime?.version ?? ''
) ||
!/^sha256:[a-f0-9]{64}$/u.test(
entry.remoteRuntime?.bundleDigest ?? ''
)
) {
throw new Error('Agent catalog entry is invalid')
}
assertProtocol(entry.agentProtocol, 'Agent protocol')
assertAdditionalRuntimes(entry)
assertProtocol(
entry.remoteRuntime.protocol,
'Remote Runtime protocol'
)
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
if (identities.has(identity)) {
throw new Error('Agent catalog contains duplicate entries')
}
identities.add(identity)
}
}
function readVerifiedCatalog(catalogPath, signaturePath, registry) {
const bytes = readFileSync(resolve(catalogPath))
const catalog = parseCanonicalJson(bytes, 'Agent catalog')
assertCatalog(catalog)
const key = trustedProductionKey(
registry,
catalog.signingKeyId
)
verifyDetachedSignature(
bytes,
readFileSync(resolve(signaturePath)),
key,
catalogSignatureDomain,
'Agent catalog'
)
return catalog
}
function assertAdditionalRuntimes(value) {
if (value.additionalRuntimes === undefined) return
if (!Array.isArray(value.additionalRuntimes) || value.additionalRuntimes.length > 1) throw new Error('Invalid additional Runtimes')
for (const runtime of value.additionalRuntimes) {
if (!runtime || !['opencode', 'continue'].includes(runtime.runtimeId) || runtime.runtimeId === value.remoteRuntime.runtimeId ||
runtime.provider !== runtime.runtimeId || !semanticVersionPattern.test(runtime.version ?? '') || !/^sha256:[a-f0-9]{64}$/u.test(runtime.bundleDigest ?? '')) {
throw new Error('Invalid additional Runtime identity')
}
assertProtocol(runtime.protocol, 'Additional Runtime protocol')
}
}
function parseVersion(value) {
const match =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/u
.exec(value)
if (!match) {
throw new Error(`Invalid semantic version: ${value}`)
}
return [
BigInt(match[1]),
BigInt(match[2]),
BigInt(match[3]),
match[4]?.split('.')
]
}
function compareVersions(left, right) {
const leftParts = parseVersion(left)
const rightParts = parseVersion(right)
for (const index of [0, 1, 2]) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] > rightParts[index] ? 1 : -1
}
}
const leftPre = leftParts[3]
const rightPre = rightParts[3]
if (!leftPre && !rightPre) return 0
if (!leftPre) return 1
if (!rightPre) return -1
for (
let index = 0;
index < Math.max(leftPre.length, rightPre.length);
index += 1
) {
const leftValue = leftPre[index]
const rightValue = rightPre[index]
if (leftValue === undefined) return -1
if (rightValue === undefined) return 1
if (leftValue === rightValue) continue
const leftNumeric = /^\d+$/u.test(leftValue)
const rightNumeric = /^\d+$/u.test(rightValue)
if (leftNumeric && rightNumeric) {
return BigInt(leftValue) > BigInt(rightValue) ? 1 : -1
}
if (leftNumeric !== rightNumeric) {
return leftNumeric ? -1 : 1
}
return leftValue > rightValue ? 1 : -1
}
return 0
}
function createCatalog(options) {
const projectRoot = options.projectRoot ?? root
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
const signingIdentity =
options.signingIdentity ??
productionSigningKey(options.environment ?? process.env)
const signingRecord = trustedProductionKey(
registry,
signingIdentity.keyId
)
if (
publicKeySpkiBase64(signingIdentity.privateKey) !==
signingRecord.publicKeySpkiBase64
) {
throw new Error(
'Agent catalog signing key does not match the trusted registry'
)
}
const packages = [
readPackageMetadata(options.x64Package, registry),
readPackageMetadata(options.arm64Package, registry),
...(options.darwinArm64Package ? [readPackageMetadata(options.darwinArm64Package, registry)] : [])
]
const [first, second] = packages
const additionalIdentity = descriptor => canonicalJson((descriptor.additionalRuntimes ?? []).map(({ bundleDigest: _digest, ...identity }) => { void _digest; return identity }))
if (packages.some(value => additionalIdentity(value.descriptor) !== additionalIdentity(first.descriptor))) {
throw new Error('Agent package matrix has inconsistent additional Runtimes')
}
const {
bundleDigest: _firstRuntimeDigest,
...firstRuntimeIdentity
} = first.descriptor.remoteRuntime
const {
bundleDigest: _secondRuntimeDigest,
...secondRuntimeIdentity
} = second.descriptor.remoteRuntime
void _firstRuntimeDigest
void _secondRuntimeDigest
if (
first.descriptor.version !== second.descriptor.version ||
first.descriptor.minimumDesktopVersion !==
second.descriptor.minimumDesktopVersion ||
canonicalJson(first.descriptor.agentProtocol) !==
canonicalJson(second.descriptor.agentProtocol) ||
canonicalJson(firstRuntimeIdentity) !==
canonicalJson(secondRuntimeIdentity) ||
new Set(packages.map(
(metadata) => metadata.descriptor.architecture
).slice(0, 2)).size !== 2 ||
first.descriptor.platform !== 'linux' ||
second.descriptor.platform !== 'linux'
) {
throw new Error(
'Agent package matrix does not describe one coherent release'
)
}
if (packages[2]) {
const mac = packages[2].descriptor
const { bundleDigest: _digest, ...runtimeIdentity } = mac.remoteRuntime
void _digest
if (
mac.platform !== 'darwin' || mac.architecture !== 'arm64' ||
mac.version !== first.descriptor.version ||
mac.minimumDesktopVersion !== first.descriptor.minimumDesktopVersion ||
canonicalJson(mac.agentProtocol) !== canonicalJson(first.descriptor.agentProtocol) ||
canonicalJson(runtimeIdentity) !== canonicalJson(firstRuntimeIdentity)
) throw new Error('Agent package matrix does not describe one coherent release')
}
const previous =
options.previousCatalog && options.previousSignature
? readVerifiedCatalog(
options.previousCatalog,
options.previousSignature,
registry
)
: undefined
if (
Boolean(options.previousCatalog) !==
Boolean(options.previousSignature)
) {
throw new Error(
'Previous Agent catalog and signature must be provided together'
)
}
const entries = new Map()
for (const entry of previous?.entries ?? []) {
entries.set(`${entry.version}:${entry.platform}:${entry.architecture}`, entry)
}
for (const metadata of packages) {
const entry = catalogEntry(metadata)
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
const existing = entries.get(identity)
if (
existing &&
canonicalJson(existing) !== canonicalJson(entry)
) {
throw new Error(
`Agent release identity is immutable: ${identity}`
)
}
entries.set(identity, entry)
}
const sortedEntries = [...entries.values()].sort(
(left, right) =>
compareVersions(right.version, left.version) ||
left.platform.localeCompare(right.platform, 'en') ||
left.architecture.localeCompare(right.architecture, 'en')
)
if (sortedEntries.length > maximumCatalogEntries) {
throw new Error('Agent catalog exceeds its retention limit')
}
const catalog = {
formatVersion: 1,
product: 'GoodBuddy',
component: 'agent',
signingKeyId: signingIdentity.keyId,
generatedAt: new Date(options.generatedAt).toISOString(),
entries: sortedEntries
}
assertCatalog(catalog)
const bytes = canonicalBytes(catalog)
const signature = sign(
null,
Buffer.concat([catalogSignatureDomain, bytes]),
signingIdentity.privateKey
)
writeFileSync(resolve(options.outputCatalog), bytes)
writeFileSync(
resolve(options.outputSignature),
`${signature.toString('base64')}\n`,
'utf8'
)
return {
version: first.descriptor.version,
catalog: basename(options.outputCatalog),
signature: basename(options.outputSignature),
entries: sortedEntries.length
}
}
function parseArguments(argv) {
const command = argv[0]
if (!['create', 'verify'].includes(command)) {
throw new Error(
'Usage: agent-catalog.cjs <create|verify> [options]'
)
}
const options = {}
for (let index = 1; index < argv.length; index += 2) {
const flag = argv[index]
const value = argv[index + 1]
if (
![
'--x64-package',
'--arm64-package',
'--darwin-arm64-package',
'--previous-catalog',
'--previous-signature',
'--catalog',
'--signature',
'--generated-at'
].includes(flag) ||
!value ||
value.startsWith('--') ||
options[flag] !== undefined
) {
throw new Error('Invalid Agent catalog argument')
}
options[flag] = value
}
for (const flag of ['--catalog', '--signature']) {
if (!options[flag]) {
throw new Error(`Agent catalog argument is required: ${flag}`)
}
}
if (command === 'verify') {
return {
command,
catalog: resolve(options['--catalog']),
signature: resolve(options['--signature'])
}
}
for (const flag of [
'--x64-package',
'--arm64-package',
'--generated-at'
]) {
if (!options[flag]) {
throw new Error(`Agent catalog argument is required: ${flag}`)
}
}
return {
command,
x64Package: resolve(options['--x64-package']),
arm64Package: resolve(options['--arm64-package']),
...(options['--darwin-arm64-package'] ? {
darwinArm64Package: resolve(options['--darwin-arm64-package'])
} : {}),
...(options['--previous-catalog']
? {
previousCatalog: resolve(
options['--previous-catalog']
)
}
: {}),
...(options['--previous-signature']
? {
previousSignature: resolve(
options['--previous-signature']
)
}
: {}),
outputCatalog: resolve(options['--catalog']),
outputSignature: resolve(options['--signature']),
generatedAt: options['--generated-at']
}
}
function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const registry = readTrustedKeyRegistry(root)
const result =
options.command === 'create'
? createCatalog(options)
: {
entries: readVerifiedCatalog(
options.catalog,
options.signature,
registry
).entries.length
}
process.stdout.write(`${JSON.stringify(result)}\n`)
}
module.exports = {
catalogName,
catalogSignatureDomain,
catalogSignatureName,
canonicalBytes,
catalogEntry,
createCatalog,
parseArguments,
readPackageMetadata,
readVerifiedCatalog
}
if (require.main === module) {
try {
main()
} catch (error) {
process.stderr.write(
`${redactSecrets(
error instanceof Error ? error.message : String(error)
)}\n`
)
process.exitCode = 1
}
}
+274
View File
@@ -0,0 +1,274 @@
const { generateKeyPairSync } = require('node:crypto')
const {
mkdirSync,
rmSync
} = require('node:fs')
const {
basename,
dirname,
join,
resolve
} = require('node:path')
const { spawnSync } = require('node:child_process')
const {
buildAgentBundle,
publicKeySpkiBase64,
readRuntimeLock,
supportedArchitectures
} = require('./agent-bundle.cjs')
const {
buildRuntimeBundle,
readRemoteRuntimeLock
} = require('./remote-runtime-bundle.cjs')
const {
assembleAgentPackage
} = require('./agent-package.cjs')
const { sha256File } = require('./file-hash.cjs')
const { targetName } = require('./agent-build-target.cjs')
const ciSigningKeyId = 'goodbuddy-agent-ci-ephemeral'
function parseArguments(argv) {
const options = {}
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]
if (
![
'--arch',
'--platform',
'--node-archive',
'--opencode-archive',
'--continue-archive',
'--output-directory',
'--archive'
].includes(argument)
) {
throw new Error(`Unknown Agent CI bundle argument: ${argument}`)
}
const value = argv[index + 1]
if (!value || value.startsWith('--')) {
throw new Error(`${argument} requires a value`)
}
const key = argument.slice(2).replaceAll('-', '')
if (options[key] !== undefined) {
throw new Error(`${argument} may be provided only once`)
}
options[key] = value
index += 1
}
if (!supportedArchitectures.includes(options.arch)) {
throw new Error('--arch must be x64 or arm64')
}
const platform = options.platform ?? 'linux'
targetName(options.arch, platform)
for (const key of [
'nodearchive',
'opencodearchive',
'continuearchive',
'outputdirectory',
'archive'
]) {
if (!options[key]) {
throw new Error(
'Usage: agent-ci-bundle.cjs --arch <x64|arm64> --node-archive <path> --opencode-archive <path> --output-directory <path> --archive <path>'
)
}
}
return {
arch: options.arch,
platform,
nodeArchive: resolve(options.nodearchive),
opencodeArchive: resolve(options.opencodearchive),
continueArchive: resolve(options.continuearchive),
outputDirectory: resolve(options.outputdirectory),
archive: resolve(options.archive)
}
}
function readCiLocks(platform = 'linux', architecture = 'x64') {
const projectRoot = resolve(__dirname, '..')
targetName(architecture, platform)
const lock = readRuntimeLock(projectRoot)
const runtimeLock = readRemoteRuntimeLock(projectRoot)
return { lock, runtimeLock }
}
function ephemeralSigningIdentity() {
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
return {
privateKey,
registry: {
formatVersion: 1,
keys: [
{
keyId: ciSigningKeyId,
publicKeySpkiBase64: publicKeySpkiBase64(publicKey),
environment: 'test'
}
],
revocations: []
}
}
}
async function buildCiAgentBundle(options) {
const projectRoot = resolve(__dirname, '..')
const platform = options.platform ?? 'linux'
const target = targetName(options.arch, platform)
if (process.platform !== platform || process.arch !== options.arch) {
throw new Error(`Agent CI build requires a native ${target} host`)
}
const { lock, runtimeLock } = readCiLocks(platform, options.arch)
const signing = ephemeralSigningIdentity()
const firstArchiveRoot = resolve(
join(
dirname(options.archive),
`.agent-ci-first-${process.pid}`
)
)
mkdirSync(firstArchiveRoot, { recursive: true })
const firstArchive = join(
firstArchiveRoot,
basename(options.archive)
)
const runtimeRoot = resolve(
`${options.outputDirectory}-runtime`
)
try {
const identity = {
keyId: ciSigningKeyId,
privateKey: signing.privateKey
}
rmSync(options.outputDirectory, {
recursive: true,
force: true
})
buildAgentBundle({
platform,
lock,
projectRoot,
arch: options.arch,
runtimeArchive: options.nodeArchive,
outputDirectory: options.outputDirectory,
registry: signing.registry,
testSigningIdentity: identity
})
const runtime = buildRuntimeBundle({
platform,
lock: runtimeLock,
projectRoot,
architecture: options.arch,
runtimeArchive: options.opencodeArchive,
outputRoot: runtimeRoot,
registry: signing.registry,
testSigningIdentity: identity
})
const continueRuntime = buildRuntimeBundle({
platform, runtimeId: 'continue', lock: runtimeLock, projectRoot, architecture: options.arch,
runtimeArchive: options.continueArchive, outputRoot: runtimeRoot,
registry: signing.registry, testSigningIdentity: identity
})
const assemble = (archive) =>
assembleAgentPackage({
platform,
projectRoot,
architecture: options.arch,
minimumDesktopVersion:
require('../package.json').version,
output: archive,
agentBundle: options.outputDirectory,
runtimeBundle: runtime.bundleDirectory,
additionalRuntimeBundles: [continueRuntime.bundleDirectory],
agentLock: lock,
runtimeLock,
registry: signing.registry,
testSigningIdentity: identity
})
assemble(firstArchive)
const firstDigest = await sha256File(firstArchive)
const second = assemble(options.archive)
const archiveDigest = await sha256File(options.archive)
if (firstDigest !== archiveDigest) {
throw new Error('Agent CI archives are not deterministic')
}
const smoke = spawnSync(
resolve(options.outputDirectory, 'goodbuddy-agent'),
['ci-invalid-command'],
{
encoding: 'utf8',
timeout: 10_000,
windowsHide: true
}
)
if (
smoke.error ||
smoke.status !== 2 ||
!smoke.stderr.includes(
'Expected daemon, attach-or-bootstrap, doctor'
)
) {
throw new Error('Built Agent native launch smoke failed')
}
const nativeChecks = [
[
resolve(options.outputDirectory, 'node'),
['-e', `const k = require(${JSON.stringify(resolve(options.outputDirectory, 'lib/node_modules/koffi'))}); if (k.sizeof('int') !== 4) process.exit(1)`]
],
[join(runtime.bundleDirectory, 'bin', 'opencode'), ['--version']]
]
for (const [executable, args] of nativeChecks) {
const result = spawnSync(executable, args, {
encoding: 'utf8',
timeout: 30_000,
windowsHide: true
})
if (
result.error ||
result.status !== 0 ||
(args[0] === '--version' &&
result.stdout.trim() !== runtimeLock.runtimes.opencode.version)
) {
throw new Error(`Built native dependency smoke failed: ${basename(executable)}`)
}
}
return {
platform,
agentVersion: lock.agentVersion,
remoteRuntimeVersion:
runtimeLock.runtimes.opencode.version,
architecture: options.arch,
archive: second.archive,
archiveSha256: archiveDigest
}
} finally {
rmSync(runtimeRoot, { recursive: true, force: true })
rmSync(firstArchive, { force: true })
rmSync(firstArchiveRoot, { recursive: true, force: true })
}
}
async function main(argv = process.argv.slice(2)) {
const result = await buildCiAgentBundle(parseArguments(argv))
process.stdout.write(
`${JSON.stringify(result)}\n`
)
}
if (require.main === module) {
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
)
process.exitCode = 1
})
}
module.exports = {
buildCiAgentBundle,
parseArguments,
readCiLocks
}
+677
View File
@@ -0,0 +1,677 @@
const {
createHash,
sign
} = require('node:crypto')
const {
closeSync,
copyFileSync,
fsyncSync,
lstatSync,
mkdirSync,
mkdtempSync,
openSync,
readSync,
readdirSync,
rmSync,
statSync,
writeSync,
writeFileSync
} = require('node:fs')
const { basename, dirname, join, relative, resolve, sep } = require('node:path')
const { tmpdir } = require('node:os')
const { targetName } = require('./agent-build-target.cjs')
const { crc32 } = require('node:zlib')
const {
buildAgentBundle,
expectedManifestMode: expectedAgentManifestMode,
preflightProductionSigningKey,
productionSigningKey,
publicKeySpkiBase64,
readRuntimeLock,
readTrustedKeyRegistry,
redactSecrets,
verifyLockedBundle
} = require('./agent-bundle.cjs')
const {
buildRuntimeBundle,
canonicalJson,
readRemoteRuntimeLock,
verifyBundleDirectory: verifyRuntimeBundle
} = require('./remote-runtime-bundle.cjs')
const {
centralDirectoryHeaderSignature: zipCentralHeaderSignature,
endOfCentralDirectorySignature: zipEndSignature,
localFileHeaderSignature: zipLocalHeaderSignature
} = require('./zip-central-directory.cjs')
const root = join(__dirname, '..')
const packageDescriptorName = 'agent-package.json'
const packageSignatureName = 'agent-package.sig'
const signatureDomain = Buffer.from(
'GoodBuddy Agent Package Descriptor Signature v1\0',
'utf8'
)
const zipUtf8Flag = 0x0800
const zipVersion = 20
const zipMadeByUnix = (3 << 8) | zipVersion
const zipDosDate = (1 << 5) | 1
const semanticVersionPattern =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:(?:0|[1-9]\d*)|(?:\d*[A-Za-z-][0-9A-Za-z-]*)))*)?$/u
function descriptorBytes(descriptor) {
return Buffer.from(`${JSON.stringify(descriptor, null, 2)}\n`, 'utf8')
}
function descriptorContentDigest(descriptor) {
const { contentDigest: _contentDigest, ...content } = descriptor
void _contentDigest
return `sha256:${createHash('sha256')
.update(canonicalJson(content))
.digest('hex')}`
}
function listFiles(directory) {
const rootPath = resolve(directory)
const output = []
const visit = (current) => {
const currentStat = lstatSync(current)
if (currentStat.isSymbolicLink()) {
throw new Error(`Agent package source contains a symlink: ${current}`)
}
if (currentStat.isFile()) {
output.push(current)
return
}
if (!currentStat.isDirectory()) {
throw new Error(
`Agent package source contains an unsupported entry: ${current}`
)
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
visit(join(current, entry.name))
}
}
visit(rootPath)
return output.sort((left, right) =>
Buffer.from(relative(rootPath, left).split(sep).join('/')).compare(
Buffer.from(relative(rootPath, right).split(sep).join('/'))
)
)
}
function fileDigests(filePath) {
const hash = createHash('sha256')
const handle = openSync(filePath, 'r')
const buffer = Buffer.allocUnsafe(1024 * 1024)
let checksum = 0
try {
while (true) {
const bytesRead = readSync(
handle,
buffer,
0,
buffer.length,
null
)
if (bytesRead === 0) {
break
}
const chunk = buffer.subarray(0, bytesRead)
hash.update(chunk)
checksum = crc32(chunk, checksum)
}
} finally {
closeSync(handle)
}
return {
crc32: checksum >>> 0,
sha256: hash.digest('hex')
}
}
function copyTree(source, destination) {
for (const filePath of listFiles(source)) {
const name = relative(source, filePath).split(sep).join('/')
const target = join(destination, ...name.split('/'))
mkdirSync(dirname(target), { recursive: true })
copyFileSync(filePath, target)
}
}
function packageFileMode(path) {
if (path.startsWith('agent/')) {
return expectedAgentManifestMode(path.slice('agent/'.length))
}
return /(?:\/bin\/opencode|\/lib\/continue\/dist\/cn\.js)$/u.test(path) ? '0755' : '0644'
}
function stagePackagePayload(options) {
const staging = options.staging
copyTree(options.agentBundle, join(staging, 'agent'))
copyTree(
options.runtimeBundle,
join(
staging,
'runtime',
options.runtimeId ?? 'opencode',
basename(options.runtimeBundle)
)
)
for (const runtime of options.additionalRuntimes ?? []) {
copyTree(runtime.bundleDirectory, join(staging, 'runtime', runtime.manifest.runtimeId, basename(runtime.bundleDirectory)))
}
for (const [value, destination] of [
[
options.registry,
'agent-release-keys.json'
],
[
options.agentLock,
'agent-runtime-lock.json'
],
[
options.runtimeLock,
'remote-runtime-lock.json'
]
]) {
writeFileSync(
join(staging, destination),
`${JSON.stringify(value, null, 2)}\n`,
'utf8'
)
}
}
function payloadFiles(staging) {
const crc32ByPath = new Map()
const files = listFiles(staging)
.map((filePath) => {
const path = relative(staging, filePath).split(sep).join('/')
const size = statSync(filePath).size
const digests = fileDigests(filePath)
crc32ByPath.set(path, digests.crc32)
return {
path,
size,
sha256: digests.sha256,
mode: packageFileMode(path)
}
})
.filter(
(file) =>
file.path !== packageDescriptorName &&
file.path !== packageSignatureName
)
return { crc32ByPath, files }
}
function createPackageArchive(
staging,
output,
knownCrc32 = new Map()
) {
mkdirSync(dirname(output), { recursive: true })
const entries = listFiles(staging).map((filePath) => {
const name = relative(staging, filePath).split(sep).join('/')
const nameBytes = Buffer.from(name, 'utf8')
const size = statSync(filePath).size
if (
nameBytes.length === 0 ||
nameBytes.length > 0xffff ||
size > 0xffffffff
) {
throw new Error(`Agent package ZIP entry is too large: ${name}`)
}
return {
filePath,
name,
nameBytes,
size,
crc32:
knownCrc32.get(name) ?? fileDigests(filePath).crc32,
mode: packageFileMode(name),
offset: 0
}
})
if (entries.length === 0 || entries.length > 0xffff) {
throw new Error('Agent package ZIP entry count is invalid')
}
const outputHandle = openSync(output, 'w')
let outputOffset = 0
const archiveHash = createHash('sha256')
const writeBytes = (data) => {
let offset = 0
while (offset < data.byteLength) {
const written = writeSync(
outputHandle,
data,
offset,
data.byteLength - offset
)
if (written <= 0) {
throw new Error('Agent package archive write made no progress')
}
offset += written
}
archiveHash.update(data)
outputOffset += data.byteLength
}
const buffer = Buffer.allocUnsafe(1024 * 1024)
try {
for (const entry of entries) {
entry.offset = outputOffset
const header = Buffer.alloc(30)
header.writeUInt32LE(zipLocalHeaderSignature, 0)
header.writeUInt16LE(zipVersion, 4)
header.writeUInt16LE(zipUtf8Flag, 6)
header.writeUInt16LE(0, 8)
header.writeUInt16LE(0, 10)
header.writeUInt16LE(zipDosDate, 12)
header.writeUInt32LE(entry.crc32, 14)
header.writeUInt32LE(entry.size, 18)
header.writeUInt32LE(entry.size, 22)
header.writeUInt16LE(entry.nameBytes.length, 26)
header.writeUInt16LE(0, 28)
writeBytes(header)
writeBytes(entry.nameBytes)
const inputHandle = openSync(entry.filePath, 'r')
try {
let position = 0
while (position < entry.size) {
const bytesRead = readSync(
inputHandle,
buffer,
0,
Math.min(buffer.length, entry.size - position),
null
)
if (bytesRead <= 0) {
throw new Error(
`Agent package source read made no progress: ${entry.name}`
)
}
position += bytesRead
writeBytes(buffer.subarray(0, bytesRead))
}
} finally {
closeSync(inputHandle)
}
}
const centralOffset = outputOffset
for (const entry of entries) {
const record = Buffer.alloc(46)
record.writeUInt32LE(zipCentralHeaderSignature, 0)
record.writeUInt16LE(zipMadeByUnix, 4)
record.writeUInt16LE(zipVersion, 6)
record.writeUInt16LE(zipUtf8Flag, 8)
record.writeUInt16LE(0, 10)
record.writeUInt16LE(0, 12)
record.writeUInt16LE(zipDosDate, 14)
record.writeUInt32LE(entry.crc32, 16)
record.writeUInt32LE(entry.size, 20)
record.writeUInt32LE(entry.size, 24)
record.writeUInt16LE(entry.nameBytes.length, 28)
record.writeUInt16LE(0, 30)
record.writeUInt16LE(0, 32)
record.writeUInt16LE(0, 34)
record.writeUInt16LE(0, 36)
const unixMode =
0o100000 | Number.parseInt(entry.mode, 8)
record.writeUInt32LE((unixMode << 16) >>> 0, 38)
record.writeUInt32LE(entry.offset, 42)
writeBytes(record)
writeBytes(entry.nameBytes)
}
const centralSize = outputOffset - centralOffset
const end = Buffer.alloc(22)
end.writeUInt32LE(zipEndSignature, 0)
end.writeUInt16LE(0, 4)
end.writeUInt16LE(0, 6)
end.writeUInt16LE(entries.length, 8)
end.writeUInt16LE(entries.length, 10)
end.writeUInt32LE(centralSize, 12)
end.writeUInt32LE(centralOffset, 16)
end.writeUInt16LE(0, 20)
writeBytes(end)
fsyncSync(outputHandle)
} finally {
closeSync(outputHandle)
}
return {
size: outputOffset,
sha256: archiveHash.digest('hex')
}
}
function assembleAgentPackage(options) {
const projectRoot = options.projectRoot ?? root
const architecture = options.architecture
const platform = options.platform ?? 'linux'
const target = targetName(architecture, platform)
if (
!['x64', 'arm64'].includes(architecture) ||
!semanticVersionPattern.test(options.minimumDesktopVersion ?? '') ||
typeof options.output !== 'string'
) {
throw new Error('Agent package identity is invalid')
}
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
const agentLock =
options.agentLock ?? readRuntimeLock(projectRoot)
const runtimeLock =
options.runtimeLock ?? readRemoteRuntimeLock(projectRoot)
const signingIdentity =
options.testSigningIdentity ??
options.signingIdentity ??
productionSigningKey(options.environment ?? process.env)
const signingRecord = options.testSigningIdentity
? registry.keys.find(
(key) => key.keyId === signingIdentity.keyId
)
: preflightProductionSigningKey({
projectRoot,
registry,
keyId: signingIdentity.keyId
})
if (!signingRecord) {
throw new Error('Agent package signing key is not registered')
}
if (
publicKeySpkiBase64(signingIdentity.privateKey) !==
signingRecord.publicKeySpkiBase64
) {
throw new Error(
'Agent package signing private key does not match the trusted registry'
)
}
const agent = verifyLockedBundle(
options.agentBundle,
architecture,
{
platform,
projectRoot,
registry,
lock: agentLock,
verificationEnvironment: options.testSigningIdentity
? 'test'
: 'production'
}
)
const runtime = verifyRuntimeBundle(options.runtimeBundle, {
platform,
projectRoot,
architecture,
registry,
lock: runtimeLock,
verificationEnvironment: options.testSigningIdentity
? 'test'
: 'production'
})
const expectedArchiveName =
`goodbuddy-agent-${agent.manifest.agentVersion}` +
`-${target}.gbagent`
if (basename(options.output) !== expectedArchiveName) {
throw new Error(
`Agent package output must be named ${expectedArchiveName}`
)
}
const staging = mkdtempSync(
join(tmpdir(), `goodbuddy-agent-package-${target}-`)
)
try {
const additionalRuntimes = (options.additionalRuntimeBundles ?? []).map(directory => verifyRuntimeBundle(directory, {
platform, projectRoot, architecture, registry, lock: runtimeLock,
verificationEnvironment: options.testSigningIdentity ? 'test' : 'production'
}))
if (new Set([runtime, ...additionalRuntimes].map(value => value.manifest.runtimeId)).size !== additionalRuntimes.length + 1) {
throw new Error('Duplicate packaged Runtime')
}
stagePackagePayload({
staging,
registry,
agentLock,
runtimeLock,
agentBundle: resolve(options.agentBundle),
runtimeBundle: runtime.bundleDirectory,
runtimeId: runtime.manifest.runtimeId,
additionalRuntimes
})
const payload = payloadFiles(staging)
const initial = {
format: 'goodbuddy-agent-package',
formatVersion: 1,
product: 'GoodBuddy',
component: 'agent',
version: agent.manifest.agentVersion,
minimumDesktopVersion: options.minimumDesktopVersion,
platform,
architecture,
signingKeyId: signingIdentity.keyId,
agentProtocol: agent.manifest.protocol,
remoteRuntime: {
runtimeId: runtime.manifest.runtimeId,
provider: runtime.manifest.provider,
version: runtime.manifest.runtimeVersion,
bundleDigest: runtime.manifest.bundleDigest,
protocol: runtime.manifest.protocol
},
...(additionalRuntimes.length ? { additionalRuntimes: additionalRuntimes.map(({ manifest }) => ({
runtimeId: manifest.runtimeId, provider: manifest.provider, version: manifest.runtimeVersion,
bundleDigest: manifest.bundleDigest, protocol: manifest.protocol
})) } : {}),
contentDigest: `sha256:${'0'.repeat(64)}`,
files: payload.files
}
const descriptor = {
...initial,
contentDigest: descriptorContentDigest(initial)
}
const bytes = descriptorBytes(descriptor)
const signature = sign(
null,
Buffer.concat([signatureDomain, bytes]),
signingIdentity.privateKey
)
writeFileSync(join(staging, packageDescriptorName), bytes)
writeFileSync(
join(staging, packageSignatureName),
`${signature.toString('base64')}\n`,
'utf8'
)
const archive = createPackageArchive(
staging,
options.output,
payload.crc32ByPath
)
return {
descriptor,
archive: basename(options.output),
size: archive.size,
sha256: archive.sha256
}
} finally {
rmSync(staging, { recursive: true, force: true })
}
}
function buildAgentPackage(options) {
const projectRoot = options.projectRoot ?? root
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
const signingIdentity =
options.testSigningIdentity ??
options.signingIdentity ??
productionSigningKey(options.environment ?? process.env)
const agentRoot = mkdtempSync(
join(tmpdir(), `goodbuddy-agent-build-${options.architecture}-`)
)
const runtimeRoot = mkdtempSync(
join(tmpdir(), `goodbuddy-runtime-build-${options.architecture}-`)
)
try {
const agentBundle = buildAgentBundle({
platform: options.platform ?? 'linux',
projectRoot,
arch: options.architecture,
runtimeArchive: options.nodeArchive,
outputDirectory: join(agentRoot, 'agent'),
registry,
...(options.agentLock ? { lock: options.agentLock } : {}),
...(options.testSigningIdentity
? { testSigningIdentity: options.testSigningIdentity }
: { signingIdentity })
})
const runtime = buildRuntimeBundle({
runtimeId: options.runtimeId,
platform: options.platform ?? 'linux',
projectRoot,
architecture: options.architecture,
runtimeArchive: options.runtimeArchive,
outputRoot: runtimeRoot,
registry,
...(options.runtimeLock ? { lock: options.runtimeLock } : {}),
...(options.testSigningIdentity
? { testSigningIdentity: options.testSigningIdentity }
: { signingIdentity })
})
const additionalRuntimeBundles = []
if (!options.runtimeId || options.runtimeId === 'opencode') {
if (!options.continueArchive) throw new Error('Default Agent package requires the locked Continue archive')
additionalRuntimeBundles.push(buildRuntimeBundle({
runtimeId: 'continue', platform: options.platform ?? 'linux', projectRoot,
architecture: options.architecture, runtimeArchive: options.continueArchive, outputRoot: runtimeRoot,
registry, ...(options.runtimeLock ? { lock: options.runtimeLock } : {}),
...(options.testSigningIdentity ? { testSigningIdentity: options.testSigningIdentity } : { signingIdentity })
}).bundleDirectory)
}
return assembleAgentPackage({
...options,
projectRoot,
registry,
...(options.testSigningIdentity
? {}
: { signingIdentity }),
agentBundle,
runtimeBundle: runtime.bundleDirectory,
additionalRuntimeBundles
})
} finally {
rmSync(agentRoot, { recursive: true, force: true })
rmSync(runtimeRoot, { recursive: true, force: true })
}
}
function parseArguments(argv) {
const command = argv[0]
if (!['build', 'assemble'].includes(command)) {
throw new Error(
'Usage: agent-package.cjs <build|assemble> --arch <x64|arm64> --minimum-desktop-version <version> --output <archive>'
)
}
const options = {}
for (let index = 1; index < argv.length; index += 2) {
const flag = argv[index]
const value = argv[index + 1]
if (
![
'--arch',
'--platform',
'--minimum-desktop-version',
'--node-archive',
'--runtime-archive',
'--runtime-id',
'--continue-archive',
'--continue-bundle',
'--agent-bundle',
'--runtime-bundle',
'--output'
].includes(flag) ||
!value
) {
throw new Error('Invalid Agent package argument')
}
options[
flag.slice(2).replace(/-([a-z])/gu, (_match, valuePart) =>
valuePart.toUpperCase()
)
] = value
}
if (
!['x64', 'arm64'].includes(options.arch) ||
!semanticVersionPattern.test(
options.minimumDesktopVersion ?? ''
) ||
!options.output
) {
throw new Error('Agent package arguments are incomplete')
}
const common = {
runtimeId: options.runtimeId ?? 'opencode',
platform: options.platform ?? 'linux',
architecture: options.arch,
minimumDesktopVersion: options.minimumDesktopVersion,
output: resolve(options.output)
}
if (!['opencode', 'continue'].includes(common.runtimeId)) {
throw new Error('Unsupported Agent package Runtime')
}
targetName(common.architecture, common.platform)
if (command === 'build') {
if (!options.nodeArchive || !options.runtimeArchive) {
throw new Error('Agent package build inputs are incomplete')
}
return {
command,
...common,
nodeArchive: resolve(options.nodeArchive),
runtimeArchive: resolve(options.runtimeArchive),
continueArchive: options.continueArchive ? resolve(options.continueArchive) : undefined
}
}
if (!options.agentBundle || !options.runtimeBundle) {
throw new Error('Agent package assembly inputs are incomplete')
}
return {
command,
...common,
agentBundle: resolve(options.agentBundle),
runtimeBundle: resolve(options.runtimeBundle),
additionalRuntimeBundles: options.continueBundle ? [resolve(options.continueBundle)] : []
}
}
function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const result =
options.command === 'build'
? buildAgentPackage(options)
: assembleAgentPackage(options)
process.stdout.write(`${JSON.stringify(result)}\n`)
}
module.exports = {
assembleAgentPackage,
buildAgentPackage,
canonicalJson,
descriptorBytes,
descriptorContentDigest,
parseArguments,
signatureDomain
}
if (require.main === module) {
try {
main()
} catch (error) {
process.stderr.write(
`${redactSecrets(
error instanceof Error ? error.message : String(error)
)}\n`
)
process.exitCode = 1
}
}
+7 -5
View File
@@ -19,10 +19,10 @@ const manifestName = 'release-manifest.json'
const targetDefinitions = [
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb'] },
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb'] }
{ platform: 'macos', arch: 'x64', formats: ['dmg'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg'] },
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'] },
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb', 'rpm'] }
]
const allowedExtensions = {
nsis: '.exe',
@@ -30,7 +30,8 @@ const allowedExtensions = {
dmg: '.dmg',
zip: '.zip',
AppImage: '.AppImage',
deb: '.deb'
deb: '.deb',
rpm: '.rpm'
}
function parseArguments(argv) {
@@ -319,6 +320,7 @@ async function main(argv = process.argv.slice(2)) {
module.exports = {
aggregateRelease,
assertSafeName,
expectedFormatForFile,
parseArguments,
targetDefinitions
}
+71
View File
@@ -0,0 +1,71 @@
function detectElfArchitecture(buffer) {
if (
buffer.length < 20 ||
buffer[0] !== 0x7f ||
buffer.toString('ascii', 1, 4) !== 'ELF' ||
![1, 2].includes(buffer[5])
) {
return undefined
}
const machine =
buffer[5] === 2
? buffer.readUInt16BE(18)
: buffer.readUInt16LE(18)
if (machine === 62) {
return 'x64'
}
if (machine === 183) {
return 'arm64'
}
return undefined
}
function detectBinaryArchitecture(buffer) {
if (
buffer.length >= 64 &&
buffer[0] === 0x4d &&
buffer[1] === 0x5a
) {
const peOffset = buffer.readUInt32LE(0x3c)
if (
peOffset + 6 <= buffer.length &&
buffer.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0'
) {
const machine = buffer.readUInt16LE(peOffset + 4)
if (machine === 0x8664) {
return 'x64'
}
if (machine === 0xaa64) {
return 'arm64'
}
}
}
const elfArchitecture = detectElfArchitecture(buffer)
if (elfArchitecture) {
return elfArchitecture
}
if (buffer.length >= 8) {
const littleMagic = buffer.readUInt32LE(0)
const bigMagic = buffer.readUInt32BE(0)
const cpuType =
littleMagic === 0xfeedfacf
? buffer.readUInt32LE(4)
: bigMagic === 0xfeedfacf
? buffer.readUInt32BE(4)
: undefined
if (cpuType === 0x01000007) {
return 'x64'
}
if (cpuType === 0x0100000c) {
return 'arm64'
}
}
return undefined
}
module.exports = {
detectBinaryArchitecture,
detectElfArchitecture
}
+111 -86
View File
@@ -29,19 +29,6 @@ const portablePath = process.env.GOODBUDDY_OUT_DIR
const markerName = '.goodbuddy-portable.json'
const portableLocales = new Set(['zh-CN.pak', 'en-US.pak'])
if (process.platform !== 'win32' || process.arch !== 'x64') {
throw new Error('Portable 目录当前必须在 Windows x64 上构建')
}
if (
portablePath === parse(portablePath).root ||
portablePath === root ||
portablePath === outputRoot
) {
throw new Error(
`拒绝使用不安全的输出目录:${portablePath}`
)
}
function assertOutputUnlocked(directory) {
if (!existsSync(directory)) {
return
@@ -142,6 +129,9 @@ function portableRequiredPaths(directory) {
return [
join(directory, 'GoodBuddy.exe'),
join(directory, 'resources', 'app.asar'),
join(directory, 'resources', 'agent-runtime-lock.json'),
join(directory, 'resources', 'remote-runtime-lock.json'),
join(directory, 'resources', 'agent-release-keys.json'),
join(directory, 'resources', 'icon.ico'),
join(directory, 'resources', 'tray-icon.png'),
join(
@@ -151,6 +141,13 @@ function portableRequiredPaths(directory) {
'opencode',
'opencode.exe'
),
join(
directory,
'resources',
'runtimes',
'opencode',
'rg.exe'
),
join(
directory,
'resources',
@@ -219,7 +216,10 @@ function writePortableMarker(directory) {
)
}
function replacePortableOutput(source, destination) {
function replacePortableOutput(
source,
destination
) {
const replacement = `${destination}.replacement-${process.pid}`
const backup = `${destination}.previous-${process.pid}`
mkdirSync(dirname(destination), { recursive: true })
@@ -279,36 +279,76 @@ function replacePortableOutput(source, destination) {
}
}
const electronDist = ensureElectronRuntime()
mkdirSync(outputRoot, { recursive: true })
rmSync(stagingRoot, { recursive: true, force: true })
function main() {
if (process.platform !== 'win32' || process.arch !== 'x64') {
throw new Error('Portable 目录当前必须在 Windows x64 上构建')
}
if (
portablePath === parse(portablePath).root ||
portablePath === root ||
portablePath === outputRoot
) {
throw new Error(
`拒绝使用不安全的输出目录:${portablePath}`
)
}
const electronDist = ensureElectronRuntime()
mkdirSync(outputRoot, { recursive: true })
rmSync(stagingRoot, { recursive: true, force: true })
for (const [label, script, args] of [
[
'Node 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.node.json']
],
[
'Renderer 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.web.json']
],
[
'Production bundle',
join(
root,
'node_modules',
'electron-vite',
'bin',
'electron-vite.js'
),
['build']
]
]) {
const buildResult = spawnSync(
for (const [label, script, args] of [
[
'Node 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.node.json']
],
[
'Renderer 类型检查',
join(root, 'node_modules', 'typescript', 'bin', 'tsc'),
['--noEmit', '-p', 'tsconfig.web.json']
],
[
'Production bundle',
join(
root,
'node_modules',
'electron-vite',
'bin',
'electron-vite.js'
),
['build']
]
]) {
const buildResult = spawnSync(
process.execPath,
[script, ...args],
{
cwd: root,
env: process.env,
shell: false,
stdio: 'inherit',
windowsHide: true
}
)
if (buildResult.error) {
throw buildResult.error
}
if (buildResult.status !== 0) {
throw new Error(
`${label}失败(code ${buildResult.status ?? 1})`
)
}
}
const result = spawnSync(
process.execPath,
[script, ...args],
[
join(root, 'build', 'run-electron-builder.cjs'),
'--dir',
'--x64',
`--config.directories.output=${stagingRoot}`,
`--config.electronDist=${electronDist}`
],
{
cwd: root,
env: process.env,
@@ -317,55 +357,40 @@ for (const [label, script, args] of [
windowsHide: true
}
)
if (buildResult.error) {
throw buildResult.error
if (result.error) {
rmSync(stagingRoot, { recursive: true, force: true })
throw result.error
}
if (buildResult.status !== 0) {
if (result.status !== 0) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error(
`${label}失败(code ${buildResult.status ?? 1})`
`Electron Builder 构建失败(code ${result.status ?? 1})`
)
}
}
const result = spawnSync(
process.execPath,
[
join(root, 'node_modules', 'electron-builder', 'cli.js'),
'--dir',
'--x64',
`--config.directories.output=${stagingRoot}`,
`--config.electronDist=${electronDist}`
],
{
cwd: root,
env: process.env,
shell: false,
stdio: 'inherit',
windowsHide: true
if (!statSync(unpackedPath, {
throwIfNoEntry: false
})?.isDirectory()) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error('Electron Builder 未生成 portable 目录')
}
try {
pruneLocales(unpackedPath)
assertPortableOutput(unpackedPath)
replacePortableOutput(unpackedPath, portablePath)
} finally {
rmSync(stagingRoot, { recursive: true, force: true })
}
)
if (result.error) {
rmSync(stagingRoot, { recursive: true, force: true })
throw result.error
}
if (result.status !== 0) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error(
`Electron Builder 构建失败(code ${result.status ?? 1})`
)
}
if (!statSync(unpackedPath, { throwIfNoEntry: false })?.isDirectory()) {
rmSync(stagingRoot, { recursive: true, force: true })
throw new Error('Electron Builder 未生成 portable 目录')
}
try {
pruneLocales(unpackedPath)
assertPortableOutput(unpackedPath)
replacePortableOutput(unpackedPath, portablePath)
} finally {
rmSync(stagingRoot, { recursive: true, force: true })
console.log(`Portable 目录构建完成:${portablePath}`)
console.log(`启动文件:${join(portablePath, 'GoodBuddy.exe')}`)
}
console.log(`Portable 目录构建完成:${portablePath}`)
console.log(`启动文件:${join(portablePath, 'GoodBuddy.exe')}`)
module.exports = {
assertPortableOutput,
portableRequiredPaths
}
if (require.main === module) {
main()
}
+882 -156
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,74 @@
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const { resolve } = require('node:path')
const { spawnSync } = require('node:child_process')
const maximumInstallerBytes = 256 * 1024
function verifyControlPlanePackageInstaller(
filePath = resolve(
'out',
'main',
'remote-package-installer.mjs'
)
) {
const bytes = readFileSync(filePath)
if (
bytes.byteLength === 0 ||
bytes.byteLength > maximumInstallerBytes
) {
throw new Error(
'Control-plane package installer exceeds its transfer limit'
)
}
const source = new TextDecoder('utf-8', {
fatal: true,
ignoreBOM: true
}).decode(bytes)
if (
/(?:^|\n)\s*(?:import|export)\b[^\r\n]*\bfrom\s*['"]\.{1,2}\//u.test(
source
) ||
/(?:^|\n)\s*import\s*['"]\.{1,2}\//u.test(source) ||
/\bimport\s*\(\s*['"]\.{1,2}\//u.test(source)
) {
throw new Error(
'Control-plane package installer is not a standalone bundle'
)
}
const syntax = spawnSync(
process.execPath,
['--check', filePath],
{
encoding: 'utf8',
windowsHide: true
}
)
if (syntax.status !== 0) {
throw new Error(
`Control-plane package installer is not executable: ${
syntax.stderr.trim() || syntax.stdout.trim()
}`
)
}
return {
size: bytes.byteLength,
sha256: createHash('sha256').update(bytes).digest('hex')
}
}
if (require.main === module) {
const result = verifyControlPlanePackageInstaller(
process.argv[2]
? resolve(process.argv[2])
: undefined
)
process.stdout.write(
`Control-plane package installer: ${result.size} bytes, sha256 ${result.sha256}\n`
)
}
module.exports = {
maximumInstallerBytes,
verifyControlPlanePackageInstaller
}
+185
View File
@@ -0,0 +1,185 @@
const {
mkdirSync,
readFileSync,
writeFileSync
} = require('node:fs')
const { dirname, resolve } = require('node:path')
const {
expectedFormatForFile,
targetDefinitions
} = require('./aggregate-release.cjs')
const supportedTargets = new Map(
targetDefinitions.map((target) => [
`${target.platform}-${target.arch}`,
target
])
)
function parseArguments(argv) {
const options = {}
const optionNames = new Map([
['--manifest', 'manifest'],
['--base-url', 'baseUrl'],
['--output', 'output']
])
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]
const optionName = optionNames.get(argument)
if (optionName) {
const value = argv[index + 1]
if (!value) {
throw new Error(`${argument} 缺少值`)
}
options[optionName] = value
index += 1
} else {
throw new Error(`未知参数:${argument}`)
}
}
if (!options.manifest || !options.baseUrl || !options.output) {
throw new Error('必须指定 --manifest、--base-url 和 --output')
}
return {
manifest: resolve(options.manifest),
baseUrl: options.baseUrl,
output: resolve(options.output)
}
}
function validateBaseUrl(value) {
let url
try {
url = new URL(value)
} catch {
throw new Error(`OSS 基础地址无效:${value}`)
}
if (
url.protocol !== 'https:' ||
url.username ||
url.password ||
url.search ||
url.hash
) {
throw new Error('OSS 基础地址必须是无凭据、查询参数和片段的 HTTPS 地址')
}
if (!url.pathname.endsWith('/')) {
url.pathname += '/'
}
return url
}
function assertFile(file, targetKey) {
if (
!file ||
typeof file.name !== 'string' ||
!/^GoodBuddy-[A-Za-z0-9._-]+$/u.test(file.name) ||
!Number.isSafeInteger(file.size) ||
file.size < 1 ||
typeof file.sha256 !== 'string' ||
!/^[a-f0-9]{64}$/u.test(file.sha256)
) {
throw new Error(`发布文件元数据无效:${targetKey}`)
}
}
function createSiteRelease(manifest, baseUrlValue) {
if (
!manifest ||
manifest.formatVersion !== 1 ||
manifest.productName !== 'GoodBuddy' ||
typeof manifest.version !== 'string' ||
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(manifest.version) ||
!Array.isArray(manifest.targets)
) {
throw new Error('聚合发布 manifest 元数据无效')
}
const baseUrl = validateBaseUrl(baseUrlValue)
const seenTargets = new Set()
const targets = {}
for (const target of manifest.targets) {
const key = `${target?.platform}-${target?.arch}`
const expectedTarget = supportedTargets.get(key)
const expectedFormats = expectedTarget?.formats
if (
!expectedFormats ||
seenTargets.has(key) ||
!Array.isArray(target.formats) ||
!Array.isArray(target.files) ||
target.formats.length !== expectedFormats.length ||
!expectedFormats.every(
(format, index) => target.formats[index] === format
) ||
target.files.length !== expectedFormats.length
) {
throw new Error(`发布目标元数据无效:${key}`)
}
const files = {}
for (const file of target.files) {
assertFile(file, key)
const format = expectedFormatForFile(
file.name,
expectedTarget
)
if (!format || !expectedFormats.includes(format) || files[format]) {
throw new Error(`发布文件格式无效:${file.name}`)
}
files[format] = {
name: file.name,
size: file.size,
sha256: file.sha256,
url: new URL(encodeURIComponent(file.name), baseUrl).href
}
}
if (expectedFormats.some((format) => !files[format])) {
throw new Error(`发布目标文件不完整:${key}`)
}
targets[key] = {
platform: target.platform,
arch: target.arch,
files
}
seenTargets.add(key)
}
if (seenTargets.size !== supportedTargets.size) {
throw new Error(
`发布目标数量错误:期望 ${supportedTargets.size},实际 ${seenTargets.size}`
)
}
return {
formatVersion: 1,
productName: manifest.productName,
version: manifest.version,
targets,
checksumUrl: new URL('SHA256SUMS', baseUrl).href,
fallbackUrl: 'https://github.com/mesalogo/goodbuddy/releases/latest'
}
}
function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const manifest = JSON.parse(readFileSync(options.manifest, 'utf8'))
const siteRelease = createSiteRelease(manifest, options.baseUrl)
mkdirSync(dirname(options.output), { recursive: true })
writeFileSync(
options.output,
`${JSON.stringify(siteRelease, null, 2)}\n`,
'utf8'
)
console.log(`官网发布索引已生成:${options.output}`)
}
module.exports = {
createSiteRelease,
parseArguments,
validateBaseUrl
}
if (require.main === module) {
try {
main()
} catch (error) {
console.error(error)
process.exitCode = 1
}
}
+208
View File
@@ -0,0 +1,208 @@
'use strict'
const {
mkdirSync,
mkdtempSync
} = require('node:fs')
const {
rm,
writeFile
} = require('node:fs/promises')
const { tmpdir } = require('node:os')
const {
isAbsolute,
join,
resolve
} = require('node:path')
const { app, utilityProcess } = require('electron/main')
const protocol = 'goodbuddy.deepseek-harness.control'
const controlVersion = 3
const byteProtocol = 'goodbuddy.deepseek-harness.byte-stream'
const byteProtocolVersion = 1
const configuredHostPath =
process.env.GOODBUDDY_HARNESS_SMOKE_HOST
const hostPath = configuredHostPath
? isAbsolute(configuredHostPath)
? configuredHostPath
: resolve(configuredHostPath)
: resolve('out/main/deepseek-harness-host-bootstrap.js')
const workspace = mkdtempSync(
join(tmpdir(), 'goodbuddy-harness-electron-smoke-')
)
const dshHome = join(workspace, 'dsh-home')
mkdirSync(dshHome)
const configuredResultPath =
process.env.GOODBUDDY_HARNESS_SMOKE_RESULT
const resultPath =
configuredResultPath && isAbsolute(configuredResultPath)
? configuredResultPath
: join(
tmpdir(),
`goodbuddy-harness-utility-smoke-${process.pid}.json`
)
let child
let timeout
let stderr = ''
let settled = false
let transportProbed = false
void writeFile(
resultPath,
JSON.stringify({ status: 'checkpoint', stage: 'script-start' }),
'utf8'
)
async function checkpoint(stage, detail = '') {
await writeFile(
resultPath,
JSON.stringify({ status: 'checkpoint', stage, detail }),
'utf8'
)
}
function finish(status, detail = '') {
if (settled) {
return
}
settled = true
if (timeout) {
clearTimeout(timeout)
}
void writeFile(
resultPath,
JSON.stringify({
status,
detail: detail.slice(0, 4_096)
}),
'utf8'
)
.catch(() => undefined)
.finally(() => {
child?.kill()
void rm(workspace, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100
})
.catch(() => undefined)
.finally(() => {
if (!configuredResultPath) {
console.log(
`GoodBuddy packaged Harness smoke: ${status}`
)
}
app.exit(status === 'ready' ? 0 : 1)
})
})
}
async function run() {
await checkpoint('module-loaded')
await app.whenReady()
await checkpoint('app-ready')
child = utilityProcess.fork(hostPath, [], {
cwd: workspace,
env: {
PATH: process.env.PATH ?? '',
Path: process.env.Path ?? '',
PATHEXT: process.env.PATHEXT ?? '',
SystemRoot: process.env.SystemRoot ?? '',
COMSPEC: process.env.COMSPEC ?? '',
TEMP: process.env.TEMP ?? '',
TMP: process.env.TMP ?? '',
USERPROFILE: process.env.USERPROFILE ?? '',
APPDATA: process.env.APPDATA ?? '',
LOCALAPPDATA: process.env.LOCALAPPDATA ?? '',
DSH_HOME: dshHome,
DSH_TELEMETRY_DISABLED: '1',
OTEL_SDK_DISABLED: 'true'
},
serviceName: 'GoodBuddy DeepSeek Harness Smoke',
stdio: ['ignore', 'ignore', 'pipe'],
allowLoadingUnsignedLibraries: false,
disclaim: false
})
await checkpoint('utility-forked', String(child.pid ?? ''))
child.stderr?.on('data', (chunk) => {
stderr = (stderr + String(chunk)).slice(-4_096)
})
child.on('message', (message) => {
if (
message?.protocol === protocol &&
message.version === controlVersion &&
message.type === 'ready'
) {
child.postMessage({
protocol: byteProtocol,
version: byteProtocolVersion,
type: 'data',
stream: 'stdin',
seq: 0,
bytes: Buffer.from('{}\n')
})
return
}
if (
message?.protocol === byteProtocol &&
message.version === byteProtocolVersion &&
message.type === 'ack' &&
message.stream === 'stdin' &&
message.seq === 0
) {
transportProbed = true
finish('ready')
return
}
if (
message?.protocol === protocol &&
message.version === controlVersion &&
message.type === 'fatal'
) {
finish('fatal', String(message.code))
}
})
child.on('exit', (code) => {
finish(
'exit',
`${code}:${stderr.replaceAll(/\s+/gu, ' ').trim()}`
)
})
child.postMessage({
protocol,
version: controlVersion,
type: 'start',
config: {
workspace,
dshHome,
baseUrl: 'https://gateway.example/openai/v1',
api: 'openai-completions',
provider: 'goodbuddy',
model: 'qwen-plus',
supportsImageInput: false,
requestHeaders: {},
harnessVersion: '0.1.2-rc.1',
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
skillPackages: [],
extensionPackages: [],
maxFrameBytes: 8 * 1024 * 1024
}
})
timeout = setTimeout(() => {
finish(
'timeout',
`${transportProbed ? 'transport-probed ' : ''}${stderr.replaceAll(/\s+/gu, ' ').trim()}`
)
}, 20_000)
}
void run().catch((error) => {
finish(
'bootstrap-error',
error instanceof Error ? error.message : 'unknown error'
)
})
+1
View File
@@ -0,0 +1 @@
module.exports = require('../package.json').build
+181 -10
View File
@@ -17,6 +17,25 @@ const darkSourcePath = join(
'ChatGPT_qPkaIrLGsm.png'
)
const rendererAssetRoot = join(root, 'src', 'renderer', 'src', 'assets')
const websiteAssetRoot = join(root, 'sites', 'assets')
const lightTile = {
left: 40,
top: 32,
right: 704,
bottom: 696,
radius: 126,
edgeColor: [255, 255, 255]
}
const darkTile = {
left: 20,
top: 16,
right: 684,
bottom: 680,
radius: 126,
edgeColor: [15, 21, 31]
}
function cropSquare(source, size) {
const output = new PNG({ width: size, height: size })
@@ -24,6 +43,102 @@ function cropSquare(source, size) {
return output
}
function scaleTile(tile, sourceSize, targetSize) {
const scale = targetSize / sourceSize
return {
left: tile.left * scale,
top: tile.top * scale,
right: tile.right * scale,
bottom: tile.bottom * scale,
radius: tile.radius * scale,
edgeColor: tile.edgeColor
}
}
function cropTile(source, tile) {
const width = Math.round(tile.right - tile.left)
const height = Math.round(tile.bottom - tile.top)
if (width !== height || width <= 0) {
throw new Error('图标卡片裁剪区域必须是有效正方形')
}
const output = new PNG({ width, height })
PNG.bitblt(
source,
output,
Math.round(tile.left),
Math.round(tile.top),
width,
height,
0,
0
)
return {
image: output,
tile: {
left: 0,
top: 0,
right: width,
bottom: height,
radius: tile.radius,
edgeColor: tile.edgeColor
}
}
}
function roundedRectangleDistance(x, y, tile) {
const centerX = (tile.left + tile.right) / 2
const centerY = (tile.top + tile.bottom) / 2
const halfWidth = (tile.right - tile.left) / 2
const halfHeight = (tile.bottom - tile.top) / 2
const offsetX = Math.abs(x - centerX) - (halfWidth - tile.radius)
const offsetY = Math.abs(y - centerY) - (halfHeight - tile.radius)
return (
Math.hypot(Math.max(offsetX, 0), Math.max(offsetY, 0)) +
Math.min(Math.max(offsetX, offsetY), 0) -
tile.radius
)
}
function applyRoundedTransparency(image, tile) {
for (let y = 0; y < image.height; y += 1) {
for (let x = 0; x < image.width; x += 1) {
const offset = pixelOffset(image, x, y)
const distance = roundedRectangleDistance(x + 0.5, y + 0.5, tile)
const coverage = Math.min(Math.max(0.5 - distance, 0), 1)
if (coverage >= 1) {
if (distance > -2 && image.data[offset + 3] < 255) {
image.data[offset] = tile.edgeColor[0]
image.data[offset + 1] = tile.edgeColor[1]
image.data[offset + 2] = tile.edgeColor[2]
}
continue
}
image.data[offset] = tile.edgeColor[0]
image.data[offset + 1] = tile.edgeColor[1]
image.data[offset + 2] = tile.edgeColor[2]
image.data[offset + 3] = Math.round(
image.data[offset + 3] * coverage
)
}
}
}
function createRoundedIcon(source, tile, size) {
const cropped = cropTile(source, tile)
applyRoundedTransparency(cropped.image, cropped.tile)
const output = resize(
cropped.image,
size,
size,
'bicubicInterpolation'
)
applyRoundedTransparency(
output,
scaleTile(cropped.tile, cropped.image.width, size)
)
return output
}
function pixelOffset(image, x, y) {
return (y * image.width + x) * 4
}
@@ -119,6 +234,51 @@ function assertTaskbarIcon(image) {
}
}
function assertTransparentCorners(image, label) {
const corners = [
pixelOffset(image, 0, 0),
pixelOffset(image, image.width - 1, 0),
pixelOffset(image, 0, image.height - 1),
pixelOffset(image, image.width - 1, image.height - 1)
]
if (corners.some((offset) => image.data[offset + 3] !== 0)) {
throw new Error(`${label} 的圆角外侧必须透明`)
}
}
function assertDarkEdgeHasNoWhiteFringe(image) {
const edgeWidth = Math.max(4, Math.round(image.width * 0.08))
for (let y = 0; y < image.height; y += 1) {
for (let x = 0; x < image.width; x += 1) {
if (
x >= edgeWidth &&
x < image.width - edgeWidth &&
y >= edgeWidth &&
y < image.height - edgeWidth
) {
continue
}
const offset = pixelOffset(image, x, y)
const alpha = image.data[offset + 3]
if (
alpha > 0 &&
alpha < 255 &&
Math.max(
image.data[offset],
image.data[offset + 1],
image.data[offset + 2]
) > 96
) {
throw new Error(
`暗色图标的透明边缘仍包含白色像素:${x},${y} ` +
`rgba(${image.data[offset]},${image.data[offset + 1]},` +
`${image.data[offset + 2]},${alpha})`
)
}
}
}
}
function repairDarkCursor(dark, light) {
for (let y = 570; y <= 606; y += 1) {
const backgroundOffset = pixelOffset(dark, 640, y)
@@ -197,22 +357,31 @@ async function main() {
repairDarkCursor(darkSquare, lightSquare)
assertCursorRemoved(lightSquare, darkSquare)
const light = resize(lightSquare, 512, 512, 'bicubicInterpolation')
const dark = resize(darkSquare, 512, 512, 'bicubicInterpolation')
const taskbar = createTaskbarIcon(lightSquare)
const light = createRoundedIcon(lightSquare, lightTile, 512)
const dark = createRoundedIcon(darkSquare, darkTile, 512)
const rendererLight = createRoundedIcon(lightSquare, lightTile, 128)
const rendererDark = createRoundedIcon(darkSquare, darkTile, 128)
assertTaskbarIcon(taskbar)
assertTransparentCorners(light, '亮色图标')
assertTransparentCorners(dark, '暗色图标')
assertTransparentCorners(rendererLight, '亮色界面图标')
assertTransparentCorners(rendererDark, '暗色界面图标')
assertTransparentCorners(taskbar, '任务栏图标')
assertDarkEdgeHasNoWhiteFringe(dark)
assertDarkEdgeHasNoWhiteFringe(rendererDark)
const tray = resize(taskbar, 32, 32, 'bicubicInterpolation')
assertTransparentCorners(tray, '托盘图标')
const lightPng = PNG.sync.write(light)
const darkPng = PNG.sync.write(dark)
const taskbarPng = PNG.sync.write(taskbar)
const trayPng = PNG.sync.write(tray)
const rendererLightPng = PNG.sync.write(
resize(light, 128, 128, 'bicubicInterpolation')
)
const rendererDarkPng = PNG.sync.write(
resize(dark, 128, 128, 'bicubicInterpolation')
)
await mkdir(rendererAssetRoot, { recursive: true })
const rendererLightPng = PNG.sync.write(rendererLight)
const rendererDarkPng = PNG.sync.write(rendererDark)
await Promise.all([
mkdir(rendererAssetRoot, { recursive: true }),
mkdir(websiteAssetRoot, { recursive: true })
])
const outputs = [
[join(root, 'build', 'icon-light.png'), lightPng],
@@ -221,7 +390,9 @@ async function main() {
[join(root, 'build', 'icon-taskbar.png'), taskbarPng],
[join(root, 'build', 'icon-tray.png'), trayPng],
[join(rendererAssetRoot, 'goodbuddy-light.png'), rendererLightPng],
[join(rendererAssetRoot, 'goodbuddy-dark.png'), rendererDarkPng]
[join(rendererAssetRoot, 'goodbuddy-dark.png'), rendererDarkPng],
[join(websiteAssetRoot, 'goodbuddy-light.png'), rendererLightPng],
[join(websiteAssetRoot, 'goodbuddy-dark.png'), rendererDarkPng]
]
await Promise.all(outputs.map(([path, contents]) => writeFile(path, contents)))
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 287 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 287 KiB

+1
View File
@@ -0,0 +1 @@
results
+54
View File
@@ -0,0 +1,54 @@
FROM ubuntu:26.04
ARG DEBIAN_FRONTEND=noninteractive
ARG RUST_TOOLCHAIN=1.93.1
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
binutils-loongarch64-linux-gnu \
build-essential \
ca-certificates \
cmake \
dpkg-dev \
fakeroot \
file \
g++-loongarch64-linux-gnu \
gcc-loongarch64-linux-gnu \
libc6-dev-loong64-cross \
make \
ninja-build \
node-gyp \
nodejs \
npm \
pkg-config \
python3 \
qemu-user \
rustup \
tar \
xz-utils \
&& rm -rf /var/lib/apt/lists/*
RUN rustup set profile minimal \
&& rustup toolchain install "${RUST_TOOLCHAIN}" \
--target loongarch64-unknown-linux-gnu
ENV CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
AR=loongarch64-linux-gnu-ar \
RANLIB=loongarch64-linux-gnu-ranlib \
STRIP=loongarch64-linux-gnu-strip \
CARGO_BUILD_TARGET=loongarch64-unknown-linux-gnu \
CARGO_TARGET_LOONGARCH64_UNKNOWN_LINUX_GNU_LINKER=loongarch64-linux-gnu-gcc \
RUSTUP_TOOLCHAIN=${RUST_TOOLCHAIN}
COPY toolchain.cmake /opt/goodbuddy/loongarch64-toolchain.cmake
COPY probe.sh /usr/local/bin/goodbuddy-loongarch-cross-probe
COPY preview-deb.sh /usr/local/bin/goodbuddy-loongarch-preview-deb
RUN chmod 0755 \
/usr/local/bin/goodbuddy-loongarch-cross-probe \
/usr/local/bin/goodbuddy-loongarch-preview-deb
WORKDIR /work
ENTRYPOINT ["/usr/local/bin/goodbuddy-loongarch-cross-probe"]
+78
View File
@@ -0,0 +1,78 @@
# LoongArch cross-compilation probe
This directory defines an isolated Ubuntu 26.04 amd64 environment for probing
GoodBuddy's Linux `loong64` prerequisites. It does not build Electron or
OpenCode.
The image includes:
- GCC and G++ targeting `loongarch64-linux-gnu`
- A LoongArch glibc development sysroot
- QEMU user-mode execution
- CMake and Ninja
- Rust's `loongarch64-unknown-linux-gnu` target
- Node.js, npm, and node-gyp
The probe validates:
- C, C++, CMake, and Rust LoongArch ELF output
- C, C++, and Rust execution through QEMU
- A minimal NAPI-RS Rust addon cross-build
- A Debian package with `Architecture: loong64`
- Koffi 3.1.4's published LoongArch glibc binding and the absence of a musl
binding
- The current npm availability of an `@napi-rs/canvas` LoongArch package
- A source cross-build of node-pty 1.1.0
The node-pty probe uses its upstream node-gyp 11.4.2 dependency. Ubuntu's
distribution node-gyp unconditionally links Linux addons with `-lnode`, which
is appropriate for Ubuntu's shared Node.js package but not for Electron native
addons.
Build and run it from this directory on an amd64 Docker host:
```sh
docker build \
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
.
mkdir -p results
docker run --rm \
--volume "$PWD/results:/results" \
goodbuddy-loongarch-cross:ubuntu-26.04
```
Each run writes to a new UTC timestamped directory below `results`. A passing
run contains `summary.txt`, `SHA256SUMS`, the generated ELF files, and the
probe DEB.
QEMU only checks user-mode execution against the cross sysroot. Native Electron
loading, PTY behavior, desktop integration, GPU, audio, and packaging must
still be validated on a real Debian `loong64` host.
Koffi 3.1.4 currently publishes only `linux_loong64/koffi.node`, not a
`musl_loong64` binding. That is sufficient for a Debian desktop package but
does not satisfy GoodBuddy Agent's current dual-glibc/musl bundle contract.
The NAPI-RS probe confirms that a Rust Node addon can be cross-compiled. It
does not build Canvas itself, whose Skia dependency requires a separate port
and native validation.
## Experimental desktop preview
`prepare-input.cjs` creates the architecture-neutral GoodBuddy input archive.
`preview-deb.sh` combines it with the verified community Electron 42.3.0
LoongArch shell and produces an isolated `goodbuddy-loongarch-preview` DEB.
The preview:
- uses a separate application name and user-data directory;
- includes the direct model path and Continue runtime;
- cross-builds the input lockfile's node-pty version against Electron 42.3.0;
- includes Koffi's LoongArch glibc binding;
- does not include GoodBuddy Agent, Remote Runtime, OpenCode, or native Canvas;
- is not part of the production release matrix.
See
[`docs/development/loongarch-preview-build.md`](../../docs/development/loongarch-preview-build.md)
for the complete build and validation procedure.
+108
View File
@@ -0,0 +1,108 @@
const {
existsSync,
mkdirSync,
renameSync,
rmSync,
statSync
} = require('node:fs')
const { dirname, join, resolve } = require('node:path')
const tar = require('tar')
const root = join(__dirname, '..', '..')
const defaultOutput = join(
root,
'dist',
'loongarch-cross',
'goodbuddy-loongarch-cross-input.tgz'
)
const inputPaths = [
'LICENSE',
'agent-runtime-lock.json',
'package.json',
'package-lock.json',
'remote-runtime-lock.json',
'out',
'resources/agent-release-keys.json',
'resources/release-notes.json',
'resources/skills',
'build/icon.png',
'build/icon-tray.png',
'node_modules/@continuedev/cli/package.json',
'node_modules/@continuedev/cli/dist/cn.js',
'node_modules/@continuedev/cli/dist/index.js',
'node_modules/@continuedev/cli/dist/xhr-sync-worker.js',
'node_modules/npm'
]
function parseArguments(argv) {
const options = { output: defaultOutput }
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index]
if (argument !== '--output') {
throw new Error(`未知参数:${argument}`)
}
const value = argv[index + 1]
if (!value) {
throw new Error('--output 缺少值')
}
options.output = resolve(value)
index += 1
}
return options
}
function verifyInputs(projectRoot = root) {
for (const relativePath of inputPaths) {
if (!existsSync(join(projectRoot, relativePath))) {
throw new Error(`龙芯交叉构建输入缺失:${relativePath}`)
}
}
}
async function createInputArchive(outputPath, projectRoot = root) {
verifyInputs(projectRoot)
const resolvedOutput = resolve(outputPath)
const temporaryOutput = `${resolvedOutput}.partial-${process.pid}`
mkdirSync(dirname(resolvedOutput), { recursive: true })
rmSync(temporaryOutput, { force: true })
try {
await tar.c(
{
cwd: projectRoot,
file: temporaryOutput,
gzip: true,
portable: true,
noMtime: true
},
inputPaths
)
if (!statSync(temporaryOutput).isFile()) {
throw new Error('龙芯交叉构建输入归档未生成')
}
rmSync(resolvedOutput, { force: true })
renameSync(temporaryOutput, resolvedOutput)
} finally {
rmSync(temporaryOutput, { force: true })
}
return resolvedOutput
}
async function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const output = await createInputArchive(options.output)
console.log(`龙芯交叉构建输入已生成:${output}`)
}
module.exports = {
createInputArchive,
inputPaths,
parseArguments,
verifyInputs
}
if (require.main === module) {
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+328
View File
@@ -0,0 +1,328 @@
#!/usr/bin/env bash
set -euo pipefail
input_archive="${1:?missing GoodBuddy input archive}"
electron_archive="${2:?missing Electron loong64 archive}"
output_root="${3:-/preview-output}"
electron_version="42.3.0"
electron_sha256="92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e"
koffi_version="3.1.4"
node_gyp_version="11.4.2"
preview_iteration="1"
application_directory="GoodBuddy-LoongArch-Preview"
executable_name="goodbuddy-loongarch-preview"
assert_file() {
if ! test -f "$1"; then
echo "missing required file: $1" >&2
exit 1
fi
}
assert_loongarch_elf() {
local file_path="$1"
assert_file "${file_path}"
file "${file_path}"
readelf -h "${file_path}" | grep -Eq \
'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)'
}
assert_file "${input_archive}"
assert_file "${electron_archive}"
mkdir -p "${output_root}"
actual_electron_sha256="$(
sha256sum "${electron_archive}" | cut -d ' ' -f 1
)"
if test "${actual_electron_sha256}" != "${electron_sha256}"; then
echo "Electron loong64 archive digest mismatch" >&2
exit 1
fi
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
work_root="${output_root}/.work-${run_id}"
input_root="${work_root}/input"
package_root="${work_root}/package"
application_root="${package_root}/opt/${application_directory}"
resources_root="${application_root}/resources"
app_root="${resources_root}/app"
artifact_root="${output_root}/${run_id}"
if test -e "${work_root}" || test -e "${artifact_root}"; then
echo "refusing to reuse preview build directory" >&2
exit 1
fi
cleanup() {
rm -rf "${work_root}"
}
trap cleanup EXIT
mkdir -p "${input_root}" "${app_root}" "${artifact_root}"
tar -xzf "${input_archive}" -C "${input_root}"
project_version="$(
node -p \
"require('${input_root}/package.json').version"
)"
node_pty_version="$(
node -p \
"require('${input_root}/package-lock.json').packages['node_modules/node-pty'].version"
)"
preview_version="${project_version}-loong64-preview.${preview_iteration}"
deb_version="${project_version}~loong64preview${preview_iteration}"
artifact_name="GoodBuddy-${project_version}-linux-loong64-preview.deb"
echo "== extract verified Electron shell =="
python3 -m zipfile -e "${electron_archive}" "${application_root}"
assert_loongarch_elf "${application_root}/electron"
mv "${application_root}/electron" \
"${application_root}/${executable_name}"
chmod 0755 "${application_root}/${executable_name}"
rm -f "${resources_root}/default_app.asar"
if test -f "${application_root}/chrome-sandbox"; then
chmod 4755 "${application_root}/chrome-sandbox"
fi
echo "== install production JavaScript dependencies =="
cp "${input_root}/package.json" "${app_root}/package.json"
cp "${input_root}/package-lock.json" "${app_root}/package-lock.json"
(
cd "${app_root}"
npm ci \
--ignore-scripts \
--omit=dev \
--cpu=loong64 \
--os=linux
)
rm -rf \
"${app_root}/node_modules/@esbuild" \
"${app_root}/node_modules/@img" \
"${app_root}/node_modules/@rollup"
find "${app_root}/node_modules/@koromix" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-exec rm -rf -- {} + 2>/dev/null || true
find "${app_root}/node_modules/@napi-rs" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-name 'canvas-*' \
-exec rm -rf -- {} + 2>/dev/null || true
node - "${input_root}" "${app_root}/package.json" \
"${preview_version}" <<'NODE'
const { readFileSync, writeFileSync } = require('node:fs')
const [inputRoot, outputPath, version] = process.argv.slice(2)
const metadata = JSON.parse(
readFileSync(`${inputRoot}/package.json`, 'utf8')
)
metadata.name = 'goodbuddy-loongarch-preview'
metadata.productName = 'GoodBuddy LoongArch Preview'
metadata.desktopName = 'GoodBuddy LoongArch Preview'
metadata.version = version
metadata.main = './out/main/index.js'
delete metadata.build
delete metadata.devDependencies
delete metadata.optionalDependencies
writeFileSync(outputPath, `${JSON.stringify(metadata, null, 2)}\n`)
NODE
rm -f "${app_root}/package-lock.json"
echo "== stage target Koffi binding =="
koffi_stage="${work_root}/koffi"
mkdir -p "${koffi_stage}"
koffi_pack_json="$(
npm pack \
"@koromix/koffi-linux-loong64@${koffi_version}" \
--ignore-scripts \
--json \
--pack-destination "${koffi_stage}"
)"
koffi_archive="$(
node -e \
'const value=JSON.parse(process.argv[1]); process.stdout.write(value[0].filename)' \
"${koffi_pack_json}"
)"
koffi_integrity="$(
node -p \
"require('${input_root}/package-lock.json').packages['node_modules/@koromix/koffi-linux-loong64'].integrity"
)"
node - "${koffi_stage}/${koffi_archive}" \
"${koffi_integrity}" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [filePath, integrity] = process.argv.slice(2)
const [algorithm, expected] = integrity.split('-', 2)
const actual = createHash(algorithm)
.update(readFileSync(filePath))
.digest('base64')
if (actual !== expected) {
throw new Error('Koffi archive integrity mismatch')
}
NODE
koffi_target="${app_root}/node_modules/@koromix/koffi-linux-loong64"
mkdir -p "${koffi_target}"
tar \
-xzf "${koffi_stage}/${koffi_archive}" \
-C "${koffi_target}" \
--strip-components 1
koffi_binding="${koffi_target}/linux_loong64/koffi.node"
assert_loongarch_elf "${koffi_binding}"
echo "== cross-build node-pty ${node_pty_version} for Electron ${electron_version} =="
node_gyp_root="${work_root}/node-gyp"
npm install \
--ignore-scripts \
--prefix "${node_gyp_root}" \
"node-gyp@${node_gyp_version}"
node_pty_root="${app_root}/node_modules/node-pty"
test "$(
node -p "require('${node_pty_root}/package.json').version"
)" = "${node_pty_version}"
rm -rf "${node_pty_root}/build" "${node_pty_root}/prebuilds"
(
cd "${node_pty_root}"
CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
npm_config_arch=loong64 \
npm_config_target_arch=loong64 \
npm_config_runtime=electron \
npm_config_target="${electron_version}" \
npm_config_disturl=https://electronjs.org/headers \
"${node_gyp_root}/node_modules/.bin/node-gyp" rebuild \
--arch=loong64
)
node_pty_binding="${node_pty_root}/build/Release/pty.node"
assert_loongarch_elf "${node_pty_binding}"
echo "== stage GoodBuddy application and architecture-neutral runtimes =="
cp -a "${input_root}/out" "${app_root}/out"
mkdir -p \
"${resources_root}/licenses" \
"${resources_root}/runtimes" \
"${resources_root}/skills"
cp -a "${input_root}/resources/skills/." "${resources_root}/skills/"
cp "${input_root}/resources/release-notes.json" \
"${resources_root}/release-notes.json"
cp "${input_root}/resources/agent-release-keys.json" \
"${resources_root}/agent-release-keys.json"
cp "${input_root}/agent-runtime-lock.json" \
"${resources_root}/agent-runtime-lock.json"
cp "${input_root}/remote-runtime-lock.json" \
"${resources_root}/remote-runtime-lock.json"
cp "${input_root}/build/icon.png" "${resources_root}/icon.png"
cp "${input_root}/build/icon-tray.png" \
"${resources_root}/tray-icon.png"
cp "${input_root}/LICENSE" \
"${resources_root}/licenses/GoodBuddy-0BSD.txt"
mkdir -p "${resources_root}/runtimes/continue/dist"
cp "${input_root}/node_modules/@continuedev/cli/package.json" \
"${resources_root}/runtimes/continue/package.json"
cp "${input_root}/node_modules/@continuedev/cli/dist/cn.js" \
"${resources_root}/runtimes/continue/dist/cn.js"
cp "${input_root}/node_modules/@continuedev/cli/dist/index.js" \
"${resources_root}/runtimes/continue/dist/index.js"
cp "${input_root}/node_modules/@continuedev/cli/dist/xhr-sync-worker.js" \
"${resources_root}/runtimes/continue/dist/xhr-sync-worker.js"
cp -a "${input_root}/node_modules/npm" \
"${resources_root}/runtimes/npm"
echo "== reject foreign native addons =="
while IFS= read -r -d '' native_addon; do
assert_loongarch_elf "${native_addon}"
done < <(
find "${app_root}/node_modules" \
-type f \
-name '*.node' \
-print0
)
if test -e "${resources_root}/agents" ||
test -e "${resources_root}/remote-runtimes" ||
test -e "${resources_root}/runtimes/opencode"
then
echo "preview package unexpectedly contains Agent or OpenCode payload" >&2
exit 1
fi
echo "== build isolated preview DEB =="
mkdir -p \
"${package_root}/DEBIAN" \
"${package_root}/usr/bin" \
"${package_root}/usr/share/applications" \
"${package_root}/usr/share/icons/hicolor/512x512/apps"
ln -s \
"/opt/${application_directory}/${executable_name}" \
"${package_root}/usr/bin/${executable_name}"
cp "${input_root}/build/icon.png" \
"${package_root}/usr/share/icons/hicolor/512x512/apps/${executable_name}.png"
cat > "${package_root}/usr/share/applications/${executable_name}.desktop" <<EOF
[Desktop Entry]
Name=GoodBuddy LoongArch Preview
Comment=GoodBuddy LoongArch 主体预览版
Exec=/opt/${application_directory}/${executable_name} %U
Terminal=false
Type=Application
Icon=${executable_name}
Categories=Utility;
StartupWMClass=GoodBuddy LoongArch Preview
EOF
cat > "${package_root}/DEBIAN/control" <<EOF
Package: goodbuddy-loongarch-preview
Version: ${deb_version}
Architecture: loong64
Maintainer: MesaLogo
Section: utils
Priority: optional
Depends: libasound2t64, libatspi2.0-0t64, libgbm1, libgtk-3-0t64, libnotify4, libnss3, libpulse0, libsecret-1-0, libuuid1, libxss1, libxtst6, xdg-utils
Description: GoodBuddy LoongArch desktop preview
Experimental LoongArch desktop build without bundled Agent, Remote Runtime,
OpenCode, or native Canvas image decoding.
EOF
artifact="${artifact_root}/${artifact_name}"
dpkg-deb --root-owner-group --build "${package_root}" "${artifact}"
echo "== verify preview package =="
test "$(dpkg-deb --field "${artifact}" Architecture)" = "loong64"
verify_root="${work_root}/verify"
mkdir "${verify_root}"
dpkg-deb --extract "${artifact}" "${verify_root}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/${executable_name}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/resources/app/${node_pty_binding#${app_root}/}"
assert_loongarch_elf \
"${verify_root}/opt/${application_directory}/resources/app/${koffi_binding#${app_root}/}"
(
cd "${artifact_root}"
sha256sum "${artifact_name}" > SHA256SUMS
)
cat > "${artifact_root}/preview-manifest.json" <<EOF
{
"formatVersion": 1,
"product": "GoodBuddy LoongArch Preview",
"goodBuddyVersion": "${project_version}",
"previewVersion": "${preview_version}",
"electronVersion": "${electron_version}",
"architecture": "loong64",
"artifact": "${artifact_name}",
"bundledAgent": false,
"bundledRemoteRuntime": false,
"bundledOpenCode": false,
"nativeCanvas": false,
"continueRuntime": true,
"nodePty": true,
"koffiGlibc": true,
"nativeLaunchVerified": false
}
EOF
cat "${artifact_root}/preview-manifest.json"
echo "artifact=${artifact}"
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env bash
set -euo pipefail
results_root="${1:-/results}"
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
results="${results_root}/${run_id}"
mkdir -p "${results}"
log="${results}/probe.log"
exec > >(tee "${log}") 2>&1
assert_loongarch_elf() {
local file_path="$1"
file "${file_path}"
readelf -h "${file_path}" | grep -Eq \
'Machine:[[:space:]]+(LoongArch|Loongson Loongarch)'
}
echo "== tool versions =="
loongarch64-linux-gnu-gcc --version | head -n 1
loongarch64-linux-gnu-g++ --version | head -n 1
loongarch64-linux-gnu-ld --version | head -n 1
qemu-loongarch64 --version | head -n 1
node --version
npm --version
node-gyp --version
rustc --version
cargo --version
cmake --version | head -n 1
echo "== C probe =="
cat > "${results}/hello.c" <<'EOF'
#include <stdio.h>
int main(void) {
puts("goodbuddy-loongarch-c-ok");
return 0;
}
EOF
loongarch64-linux-gnu-gcc \
-O2 \
-Wall \
-Wextra \
-Werror \
"${results}/hello.c" \
-o "${results}/hello-c"
assert_loongarch_elf "${results}/hello-c"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${results}/hello-c"
)" = "goodbuddy-loongarch-c-ok"
echo "== C++ probe =="
cat > "${results}/hello.cpp" <<'EOF'
#include <iostream>
#include <string_view>
int main() {
constexpr std::string_view message{"goodbuddy-loongarch-cpp-ok"};
std::cout << message << '\n';
return 0;
}
EOF
loongarch64-linux-gnu-g++ \
-std=c++20 \
-O2 \
-Wall \
-Wextra \
-Werror \
"${results}/hello.cpp" \
-o "${results}/hello-cpp"
assert_loongarch_elf "${results}/hello-cpp"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${results}/hello-cpp"
)" = "goodbuddy-loongarch-cpp-ok"
echo "== CMake probe =="
mkdir "${results}/cmake-source"
cat > "${results}/cmake-source/CMakeLists.txt" <<'EOF'
cmake_minimum_required(VERSION 3.20)
project(goodbuddy_loongarch_probe LANGUAGES C)
add_executable(goodbuddy-cmake-probe main.c)
target_compile_options(
goodbuddy-cmake-probe
PRIVATE -Wall -Wextra -Werror
)
EOF
cp "${results}/hello.c" "${results}/cmake-source/main.c"
cmake \
-S "${results}/cmake-source" \
-B "${results}/cmake-build" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=/opt/goodbuddy/loongarch64-toolchain.cmake
cmake --build "${results}/cmake-build"
assert_loongarch_elf \
"${results}/cmake-build/goodbuddy-cmake-probe"
echo "== Rust probe =="
mkdir -p "${results}/rust-probe/src"
cat > "${results}/rust-probe/Cargo.toml" <<'EOF'
[package]
name = "goodbuddy-loongarch-rust-probe"
version = "0.1.0"
edition = "2024"
[dependencies]
EOF
cat > "${results}/rust-probe/src/main.rs" <<'EOF'
fn main() {
println!("goodbuddy-loongarch-rust-ok");
}
EOF
cargo build \
--release \
--manifest-path "${results}/rust-probe/Cargo.toml"
rust_binary="$(
find "${results}/rust-probe/target" \
-type f \
-name goodbuddy-loongarch-rust-probe \
-print \
-quit
)"
test -n "${rust_binary}"
assert_loongarch_elf "${rust_binary}"
test "$(
qemu-loongarch64 \
-L /usr/loongarch64-linux-gnu \
"${rust_binary}"
)" = "goodbuddy-loongarch-rust-ok"
echo "== NAPI-RS Rust addon probe =="
mkdir -p "${results}/napi-rs-probe/src"
cat > "${results}/napi-rs-probe/Cargo.toml" <<'EOF'
[package]
name = "goodbuddy-loongarch-napi-probe"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
napi = { version = "3", default-features = false, features = ["napi8"] }
napi-derive = "3"
[build-dependencies]
napi-build = "2"
EOF
cat > "${results}/napi-rs-probe/build.rs" <<'EOF'
fn main() {
napi_build::setup();
}
EOF
cat > "${results}/napi-rs-probe/src/lib.rs" <<'EOF'
use napi_derive::napi;
#[napi]
pub fn architecture_probe() -> &'static str {
"loong64"
}
EOF
cargo build \
--release \
--manifest-path "${results}/napi-rs-probe/Cargo.toml"
napi_binding="$(
find "${results}/napi-rs-probe/target" \
-type f \
-name 'libgoodbuddy_loongarch_napi_probe.so' \
-print \
-quit
)"
test -n "${napi_binding}"
assert_loongarch_elf "${napi_binding}"
echo "== Debian package probe =="
package_root="${results}/deb-root"
mkdir -p \
"${package_root}/DEBIAN" \
"${package_root}/usr/lib/goodbuddy-loongarch-probe"
cp "${results}/hello-c" \
"${package_root}/usr/lib/goodbuddy-loongarch-probe/hello"
cat > "${package_root}/DEBIAN/control" <<'EOF'
Package: goodbuddy-loongarch-cross-probe
Version: 1.0.0
Architecture: loong64
Maintainer: MesaLogo
Description: GoodBuddy LoongArch cross-compilation probe
EOF
dpkg-deb --build \
"${package_root}" \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb"
test "$(
dpkg-deb \
--field \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \
Architecture
)" = "loong64"
echo "== Koffi prebuilt probe =="
mkdir "${results}/koffi"
npm pack \
"@koromix/koffi-linux-loong64@3.1.4" \
--ignore-scripts \
--pack-destination "${results}/koffi"
koffi_archive="$(
find "${results}/koffi" \
-maxdepth 1 \
-type f \
-name '*.tgz' \
-print \
-quit
)"
test -n "${koffi_archive}"
mkdir "${results}/koffi/package"
tar \
-xzf "${koffi_archive}" \
-C "${results}/koffi/package" \
--strip-components 1
koffi_binding="${results}/koffi/package/linux_loong64/koffi.node"
assert_loongarch_elf "${koffi_binding}"
if test -e "${results}/koffi/package/musl_loong64/koffi.node"; then
echo "Unexpected Koffi musl binding is present"
exit 1
fi
echo "Koffi loong64 currently provides a glibc binding only"
echo "== Canvas package availability probe =="
canvas_package_status=unavailable
if npm view \
"@napi-rs/canvas-linux-loong64-gnu" \
version \
> "${results}/canvas-package-version.txt" \
2> "${results}/canvas-package-error.txt"
then
canvas_package_status=available
fi
echo "@napi-rs/canvas loong64 package: ${canvas_package_status}"
echo "== node-pty cross-build probe =="
mkdir "${results}/node-pty"
npm pack \
"node-pty@1.1.0" \
--ignore-scripts \
--pack-destination "${results}/node-pty"
node_pty_archive="$(
find "${results}/node-pty" \
-maxdepth 1 \
-type f \
-name '*.tgz' \
-print \
-quit
)"
test -n "${node_pty_archive}"
mkdir "${results}/node-pty/package"
tar \
-xzf "${node_pty_archive}" \
-C "${results}/node-pty/package" \
--strip-components 1
(
cd "${results}/node-pty/package"
npm install --ignore-scripts --omit=dev
npm install \
--ignore-scripts \
--no-save \
"node-gyp@11.4.2"
CC=loongarch64-linux-gnu-gcc \
CXX=loongarch64-linux-gnu-g++ \
npm_config_arch=loong64 \
npm_config_target_arch=loong64 \
npm_config_runtime=electron \
npm_config_target=43.2.0 \
npm_config_disturl=https://electronjs.org/headers \
./node_modules/.bin/node-gyp rebuild \
--arch=loong64
)
node_pty_binding="${results}/node-pty/package/build/Release/pty.node"
assert_loongarch_elf "${node_pty_binding}"
echo "== summary =="
sha256sum \
"${results}/hello-c" \
"${results}/hello-cpp" \
"${rust_binary}" \
"${napi_binding}" \
"${results}/goodbuddy-loongarch-cross-probe_1.0.0_loong64.deb" \
"${koffi_binding}" \
"${node_pty_binding}" \
> "${results}/SHA256SUMS"
cat > "${results}/summary.txt" <<EOF
result=pass
target=loong64
c=pass
cpp=pass
cmake=pass
rust=pass
napi_rs=pass
qemu=pass
deb=pass
koffi=pass
koffi_musl=unavailable
canvas_package=${canvas_package_status}
node_pty=pass
EOF
cat "${results}/summary.txt"
echo "results=${results}"
+14
View File
@@ -0,0 +1,14 @@
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR loongarch64)
set(CMAKE_C_COMPILER loongarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER loongarch64-linux-gnu-g++)
set(CMAKE_AR loongarch64-linux-gnu-ar)
set(CMAKE_RANLIB loongarch64-linux-gnu-ranlib)
set(CMAKE_STRIP loongarch64-linux-gnu-strip)
set(CMAKE_FIND_ROOT_PATH /usr/loongarch64-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+33
View File
@@ -0,0 +1,33 @@
const { existsSync } = require("node:fs");
const { join } = require("node:path");
function npmInvocation(projectDir, environment = process.env) {
const npmCli = environment.npm_execpath;
if (npmCli) {
return {
command: process.execPath,
prefixArgs: [npmCli],
};
}
const bundledNpmCli = join(
projectDir,
"node_modules",
"npm",
"bin",
"npm-cli.js",
);
if (existsSync(bundledNpmCli)) {
return {
command: process.execPath,
prefixArgs: [bundledNpmCli],
};
}
return {
command: process.platform === "win32" ? "npm.cmd" : "npm",
prefixArgs: [],
};
}
module.exports = { npmInvocation };
+110
View File
@@ -0,0 +1,110 @@
const { existsSync } = require("node:fs");
const {
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const { npmInvocation } = require("./npm-invocation.cjs");
const opencodeVersion = "1.18.29";
const opencodePluginPackage = "@opencode-ai/plugin";
function prepareBundledOpenCodeConfig(projectDir) {
const targetDirectory = join(
projectDir,
".runtime-resources",
"opencode-config",
);
try {
const installed = JSON.parse(
readFileSync(
join(
targetDirectory,
"node_modules",
"@opencode-ai",
"plugin",
"package.json",
),
"utf8",
),
);
if (installed.version === opencodeVersion) {
return targetDirectory;
}
} catch {
// Rebuild an incomplete or stale offline config template.
}
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
rmSync(stagingDirectory, { recursive: true, force: true });
mkdirSync(stagingDirectory, { recursive: true });
try {
writeFileSync(
join(stagingDirectory, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies: {
[opencodePluginPackage]: opencodeVersion,
},
},
null,
2,
)}\n`,
"utf8",
);
writeFileSync(join(stagingDirectory, ".gitignore"), "node_modules\n");
const npm = npmInvocation(projectDir);
const environment = { ...process.env };
delete environment.NODE_TLS_REJECT_UNAUTHORIZED;
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
"install",
"--ignore-scripts",
"--bin-links=false",
"--omit=dev",
"--omit=optional",
"--no-audit",
"--no-fund",
"--save-exact",
],
{
cwd: stagingDirectory,
encoding: "utf8",
env: environment,
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to prepare bundled OpenCode config: ${
result.error?.message ||
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`,
);
}
rmSync(targetDirectory, { recursive: true, force: true });
mkdirSync(join(targetDirectory, ".."), { recursive: true });
renameSync(stagingDirectory, targetDirectory);
return targetDirectory;
} finally {
if (existsSync(stagingDirectory)) {
rmSync(stagingDirectory, { recursive: true, force: true });
}
}
}
module.exports = {
opencodeVersion,
prepareBundledOpenCodeConfig,
};
+212
View File
@@ -0,0 +1,212 @@
const { readFileSync, writeFileSync } = require('node:fs')
const { join, resolve } = require('node:path')
const root = resolve(__dirname, '..')
const packageJson = JSON.parse(
readFileSync(join(root, 'package.json'), 'utf8')
)
const releaseNotesFile = JSON.parse(
readFileSync(join(root, 'resources', 'release-notes.json'), 'utf8')
)
function fail(message) {
throw new Error(`Release notes validation failed: ${message}`)
}
function hasExactKeys(value, keys) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value).length === keys.length &&
keys.every((key) => Object.hasOwn(value, key))
)
}
function validateItems(value, label) {
if (!Array.isArray(value) || value.length > 20) {
fail(`${label} must contain no more than 20 items`)
}
return value.map((item) => {
if (typeof item !== 'string') {
fail(`${label} contains a non-string item`)
}
const normalized = item.trim()
if (!normalized) {
fail(`${label} contains an empty item`)
}
return normalized
})
}
const releaseNoteSections = [
'highlights',
'features',
'fixes',
'notices'
]
const legacyReleaseNoteSections = ['features', 'fixes']
function validateRelease(value, index) {
const label = `releases[${index}]`
if (!hasExactKeys(value, ['version', 'releasedAt', 'notes'])) {
fail(`${label} has invalid fields`)
}
if (!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(
value.version
)) {
fail(`${label}.version must be a stable semantic version`)
}
const date = new Date(`${value.releasedAt}T00:00:00.000Z`)
if (
!/^\d{4}-\d{2}-\d{2}$/u.test(value.releasedAt) ||
Number.isNaN(date.getTime()) ||
date.toISOString().slice(0, 10) !== value.releasedAt
) {
fail(`${label}.releasedAt must be a real YYYY-MM-DD date`)
}
if (!hasExactKeys(value.notes, ['zh-CN', 'en-US'])) {
fail(`${label}.notes must contain zh-CN and en-US`)
}
const notes = Object.fromEntries(
['zh-CN', 'en-US'].map((locale) => {
const localized = value.notes[locale]
const isCurrentFormat = hasExactKeys(
localized,
releaseNoteSections
)
const isLegacyFormat = hasExactKeys(
localized,
legacyReleaseNoteSections
)
if (!isCurrentFormat && !isLegacyFormat) {
fail(`${label}.notes.${locale} has invalid fields`)
}
const normalized = Object.fromEntries(
releaseNoteSections.map((section) => [
section,
section in localized
? validateItems(
localized[section],
`${label}.notes.${locale}.${section}`
)
: []
])
)
if (normalized.highlights.length > 3) {
fail(
`${label}.notes.${locale}.highlights must contain no more than 3 items`
)
}
if (
releaseNoteSections.every(
(section) => normalized[section].length === 0
)
) {
fail(`${label}.notes.${locale} must not be empty`)
}
return [locale, normalized]
})
)
for (const section of releaseNoteSections) {
if (
notes['zh-CN'][section].length !==
notes['en-US'][section].length
) {
fail(`${label} localized ${section} counts do not match`)
}
}
return {
version: value.version,
releasedAt: value.releasedAt,
notes
}
}
if (
!hasExactKeys(releaseNotesFile, ['formatVersion', 'releases']) ||
releaseNotesFile.formatVersion !== 1 ||
!Array.isArray(releaseNotesFile.releases) ||
releaseNotesFile.releases.length < 1 ||
releaseNotesFile.releases.length > 100
) {
fail('unsupported file format')
}
const allReleases = releaseNotesFile.releases.map(validateRelease)
const uniqueVersionCount = new Set(
allReleases.map((release) => release.version)
).size
if (uniqueVersionCount !== allReleases.length) {
fail('release versions must be unique')
}
const releases = allReleases.filter(
(release) => release?.version === packageJson.version
)
if (releases.length !== 1) {
fail(
`expected exactly one entry for package version ${packageJson.version}`
)
}
const release = releases[0]
const localizedDefinitions = [
{
locale: 'zh-CN',
title: `GoodBuddy ${release.version} 更新内容`,
highlights: '本次亮点',
features: '功能更新',
fixes: '问题修复',
notices: '使用前请留意'
},
{
locale: 'en-US',
title: `What's New in GoodBuddy ${release.version}`,
highlights: 'Highlights',
features: 'Features',
fixes: 'Bug Fixes',
notices: 'Before You Start'
}
]
function markdownSection(title, items) {
if (items.length === 0) {
return []
}
return [`## ${title}`, '', ...items.map((item) => `- ${item}`), '']
}
const markdown = localizedDefinitions
.flatMap((definition, index) => {
const notes = release.notes[definition.locale]
return [
...(index === 0 ? [] : ['---', '']),
`# ${definition.title}`,
'',
...markdownSection(
definition.highlights,
notes.highlights
),
...markdownSection(definition.features, notes.features),
...markdownSection(definition.fixes, notes.fixes),
...markdownSection(definition.notices, notes.notices)
]
})
.join('\n')
.trimEnd()
.concat('\n')
const outputIndex = process.argv.indexOf('--output')
if (outputIndex >= 0) {
const outputPath = process.argv[outputIndex + 1]
if (!outputPath) {
fail('--output requires a path')
}
writeFileSync(resolve(root, outputPath), markdown, 'utf8')
} else {
process.stdout.write(
`Validated bilingual release notes for ${packageJson.version}\n`
)
}
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
const { spawnSync } = require('node:child_process')
const { rmSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join } = require('node:path')
const { buildSync } = require('esbuild')
const output = join(tmpdir(), `goodbuddy-browser-tabs-e2e-${process.pid}.cjs`)
try {
buildSync({
entryPoints: [join(process.cwd(), 'tests', 'browser-tabs-electron-e2e.ts')],
outfile: output,
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
external: ['electron']
})
const environment = { ...process.env }
delete environment.ELECTRON_RUN_AS_NODE
const result = spawnSync(require('electron'), [output], {
cwd: process.cwd(),
env: environment,
stdio: 'inherit',
timeout: 120_000
})
if (result.error) throw result.error
process.exitCode = result.status ?? 1
} finally {
rmSync(output, { force: true })
}
@@ -0,0 +1,149 @@
'use strict'
const { spawn } = require('node:child_process')
const {
readFile,
rm,
writeFile
} = require('node:fs/promises')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const electronPath = process.env.GOODBUDDY_HARNESS_SMOKE_ELECTRON
? resolve(process.env.GOODBUDDY_HARNESS_SMOKE_ELECTRON)
: require('electron')
const configuredAppPath =
process.env.GOODBUDDY_HARNESS_SMOKE_APP
const appPath = configuredAppPath
? resolve(configuredAppPath)
: resolve('build/smoke-app')
const temporaryAppPath =
configuredAppPath ||
process.env.GOODBUDDY_HARNESS_SMOKE_ELECTRON
? undefined
: join(
tmpdir(),
`goodbuddy-harness-smoke-app-${process.pid}`
)
const resultPath = join(
tmpdir(),
`goodbuddy-harness-utility-smoke-result-${process.pid}.json`
)
const profilePath = join(
tmpdir(),
`goodbuddy-harness-utility-smoke-profile-${process.pid}`
)
const environment = {
...process.env,
GOODBUDDY_HARNESS_SMOKE_RESULT: resultPath
}
delete environment.ELECTRON_RUN_AS_NODE
function runElectron(applicationPath) {
return new Promise((resolveRun, rejectRun) => {
const child = spawn(
electronPath,
[
applicationPath,
'--no-sandbox',
`--user-data-dir=${profilePath}`,
'--no-first-run'
],
{
cwd: resolve('.'),
env: environment,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
}
)
let output = ''
const capture = (chunk) => {
output = (output + String(chunk)).slice(-8_192)
}
child.stdout.on('data', capture)
child.stderr.on('data', capture)
const timeout = setTimeout(() => {
child.kill()
rejectRun(
new Error(
`DeepSeek Harness Electron smoke timed out: ${output.trim()}`
)
)
}, 30_000)
child.once('error', (error) => {
clearTimeout(timeout)
rejectRun(error)
})
child.once('exit', (code, signal) => {
clearTimeout(timeout)
resolveRun({ code, signal, output })
})
})
}
async function main() {
await rm(resultPath, { force: true })
await writeFile(
resolve('out/main/package.json'),
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version: '0.1.2-rc.1',
private: true,
type: 'module'
},
null,
2
)}\n`,
'utf8'
)
if (temporaryAppPath) {
await rm(temporaryAppPath, {
recursive: true,
force: true
})
const { cp, copyFile, mkdir } = require('node:fs/promises')
await mkdir(temporaryAppPath, { recursive: true })
await cp(resolve('build/smoke-app'), temporaryAppPath, {
recursive: true
})
await copyFile(
resolve('build/deepseek-harness-utility-smoke.cjs'),
join(temporaryAppPath, 'deepseek-harness-utility-smoke.cjs')
)
}
const execution = await runElectron(
temporaryAppPath ?? appPath
)
let result
try {
result = JSON.parse(await readFile(resultPath, 'utf8'))
} catch (error) {
throw new Error(
`DeepSeek Harness Electron smoke produced no valid result (code ${execution.code}, signal ${execution.signal ?? 'none'}): ${execution.output.trim()}`,
{ cause: error }
)
} finally {
await Promise.all([
rm(resultPath, { force: true }),
rm(profilePath, { recursive: true, force: true }),
temporaryAppPath
? rm(temporaryAppPath, {
recursive: true,
force: true
})
: Promise.resolve()
])
}
if (execution.code !== 0 || result.status !== 'ready') {
throw new Error(
`DeepSeek Harness Electron smoke failed (code ${execution.code}, status ${String(result.status)}): ${String(result.detail ?? execution.output).trim()}`
)
}
console.log('DeepSeek Harness Electron utility smoke: ready')
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
+32
View File
@@ -0,0 +1,32 @@
'use strict'
function configureElectronBuilderLogLevels(
moduleManager = require(
'app-builder-lib/out/node-module-collector/moduleManager'
)
) {
const duplicateReferenceMessage =
moduleManager.LogMessageByKey.PKG_DUPLICATE_REF
if (
!duplicateReferenceMessage ||
!moduleManager.logMessageLevelByKey[
duplicateReferenceMessage
]
) {
throw new Error(
'当前 electron-builder 不支持依赖引用日志级别配置'
)
}
moduleManager.logMessageLevelByKey[
duplicateReferenceMessage
] = 'debug'
}
if (require.main === module) {
configureElectronBuilderLogLevels()
require('electron-builder/cli')
}
module.exports = {
configureElectronBuilderLogLevels
}
@@ -0,0 +1,323 @@
'use strict'
const { spawn } = require('node:child_process')
const {
chmod,
copyFile,
mkdir,
mkdtemp,
readFile,
rm,
writeFile
} = require('node:fs/promises')
const { statSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { delimiter, join, resolve } = require('node:path')
const { pathToFileURL } = require('node:url')
const unpackedPath = process.argv[2]
? resolve(process.argv[2])
: resolve('dist/harness-package-probe/win-unpacked')
const executable = join(
unpackedPath,
process.platform === 'win32' ? 'GoodBuddy.exe' : 'goodbuddy'
)
const host = join(
unpackedPath,
'resources',
'app.asar.unpacked',
'out',
'main',
'deepseek-harness-host-bootstrap.js'
)
const npmRoot = join(unpackedPath, 'resources', 'runtimes', 'npm')
const npmCli = join(npmRoot, 'bin', 'npm-cli.js')
const npmManifestPath = join(npmRoot, 'package.json')
for (const [path, description] of [
[executable, 'packaged Electron executable'],
[host, 'packaged DeepSeek Harness host'],
[npmCli, 'packaged npm CLI'],
[npmManifestPath, 'packaged npm manifest']
]) {
if (!statSync(path, { throwIfNoEntry: false })?.isFile()) {
throw new Error(`${description} is missing: ${path}`)
}
}
function quotePosixShell(value) {
return `'${value.replaceAll("'", "'\\''")}'`
}
async function prepareNodeCommand(directory) {
await mkdir(directory, { recursive: true })
if (process.platform === 'win32') {
await writeFile(
join(directory, 'node.cmd'),
[
'@echo off',
'set "ELECTRON_RUN_AS_NODE=1"',
`"${executable.replaceAll('%', '%%')}" %*`,
''
].join('\r\n'),
'utf8'
)
return
}
const commandPath = join(directory, 'node')
await writeFile(
commandPath,
[
'#!/bin/sh',
`ELECTRON_RUN_AS_NODE=1 exec ${quotePosixShell(executable)} "$@"`,
''
].join('\n'),
'utf8'
)
await chmod(commandPath, 0o700)
}
function run(command, args, env, cwd = resolve('.')) {
return new Promise((resolveExit, rejectExit) => {
const child = spawn(command, args, {
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
})
let output = ''
const capture = (chunk) => {
output = (output + String(chunk)).slice(-8_192)
}
child.stdout.on('data', capture)
child.stderr.on('data', capture)
child.once('error', rejectExit)
child.once('exit', (exitCode, signal) => {
resolveExit({ exitCode, signal, output })
})
})
}
async function main() {
const root = await mkdtemp(
join(tmpdir(), 'goodbuddy-packaged-harness-smoke-')
)
try {
const project = join(root, 'app')
const profile = join(root, 'profile')
const resultPath = join(root, 'result.json')
const packageManagerBin = join(root, 'package-manager-bin')
const npmProject = join(root, 'npm-project')
const npmFixture = join(root, 'npm-fixture')
await mkdir(project, { recursive: true })
const pluginUrl = pathToFileURL(join(
unpackedPath, 'resources', 'runtimes', 'opencode-config',
'node_modules', '@opencode-ai', 'plugin', 'dist', 'index.js'
)).href
const importedPlugin = await run(
executable,
['--input-type=module', '--eval', `await import(${JSON.stringify(pluginUrl)})`],
{ ...process.env, ELECTRON_RUN_AS_NODE: '1' },
root
)
if (importedPlugin.exitCode !== 0 || importedPlugin.signal) {
throw new Error(`Packaged OpenCode plugin import failed: ${importedPlugin.output.trim()}`)
}
console.log('Packaged OpenCode offline plugin import: ready')
await copyFile(
resolve('build/deepseek-harness-utility-smoke.cjs'),
join(project, 'deepseek-harness-utility-smoke.cjs')
)
await writeFile(
join(project, 'package.json'),
`${JSON.stringify(
{
name: 'goodbuddy-packaged-harness-smoke',
version: '1.0.0',
private: true,
main: 'deepseek-harness-utility-smoke.cjs'
},
null,
2
)}\n`,
'utf8'
)
await writeFile(
join(project, 'electron-builder.yml'),
[
'appId: live.digiman.goodbuddy.harness-smoke',
'productName: GoodBuddyHarnessSmoke',
'electronVersion: "43.2.0"',
'asar: true',
'npmRebuild: false',
'files:',
' - package.json',
' - deepseek-harness-utility-smoke.cjs',
'win:',
' target:',
' - dir'
].join('\n'),
'utf8'
)
const packageArguments = [
resolve('build/run-electron-builder.cjs'),
'--projectDir',
project,
'--win',
'dir',
'--x64',
'--publish',
'never',
`--config.directories.output=${join(root, 'dist')}`
]
const electronDist = process.env.GOODBUDDY_ELECTRON_DIST
? resolve(process.env.GOODBUDDY_ELECTRON_DIST)
: resolve('node_modules/electron/dist')
if (
statSync(electronDist, {
throwIfNoEntry: false
})?.isDirectory()
) {
packageArguments.push(
`--config.electronDist=${electronDist}`
)
}
const packaged = await run(
process.execPath,
packageArguments,
process.env
)
if (packaged.exitCode !== 0 || packaged.signal) {
throw new Error(
`Unable to package Harness smoke app: ${packaged.output.trim()}`
)
}
const smokeEnvironment = {
...process.env,
GOODBUDDY_HARNESS_SMOKE_HOST: host,
GOODBUDDY_HARNESS_SMOKE_RESULT: resultPath
}
delete smokeEnvironment.ELECTRON_RUN_AS_NODE
const executed = await run(
join(root, 'dist', 'win-unpacked', 'GoodBuddyHarnessSmoke.exe'),
[`--user-data-dir=${profile}`, '--no-first-run'],
smokeEnvironment
)
const result = JSON.parse(await readFile(resultPath, 'utf8'))
if (
executed.exitCode !== 0 ||
executed.signal ||
result.status !== 'ready'
) {
throw new Error(
`Packaged DeepSeek Harness smoke failed (${executed.exitCode}, ${executed.signal ?? 'no signal'}): ${JSON.stringify(result)} ${executed.output.trim()}`
)
}
const npmManifest = JSON.parse(
await readFile(npmManifestPath, 'utf8')
)
await prepareNodeCommand(packageManagerBin)
await mkdir(npmProject, { recursive: true })
await mkdir(npmFixture, { recursive: true })
await writeFile(
join(npmProject, 'package.json'),
'{"name":"goodbuddy-packaged-npm-project","version":"1.0.0","private":true}\n',
'utf8'
)
await writeFile(
join(npmFixture, 'package.json'),
`${JSON.stringify({
name: 'goodbuddy-packaged-npm-smoke',
version: '1.0.0',
scripts: {
install: 'node install.cjs'
}
})}\n`,
'utf8'
)
await writeFile(
join(npmFixture, 'install.cjs'),
"require('node:fs').writeFileSync(require('node:path').join(__dirname, 'lifecycle-ran.txt'), 'ready\\n')\n",
'utf8'
)
const inheritedPath =
process.env.PATH ?? process.env.Path ?? ''
const npmEnvironment = {
...process.env,
PATH: inheritedPath
? `${packageManagerBin}${delimiter}${inheritedPath}`
: packageManagerBin,
Path: inheritedPath
? `${packageManagerBin}${delimiter}${inheritedPath}`
: packageManagerBin,
ELECTRON_RUN_AS_NODE: '1',
npm_execpath: npmCli,
npm_node_execpath: executable,
npm_config_audit: 'false',
npm_config_fund: 'false',
npm_config_update_notifier: 'false'
}
const npmVersion = await run(
executable,
[npmCli, '--version'],
npmEnvironment,
npmProject
)
if (
npmVersion.exitCode !== 0 ||
npmVersion.signal ||
npmVersion.output.trim() !== npmManifest.version
) {
throw new Error(
`Packaged npm version smoke failed: ${npmVersion.output.trim()}`
)
}
const installed = await run(
executable,
[
npmCli,
'install',
'--save-exact',
'--no-audit',
'--no-fund',
'--dangerously-allow-all-scripts',
'--loglevel=error',
npmFixture
],
npmEnvironment,
npmProject
)
if (installed.exitCode !== 0 || installed.signal) {
throw new Error(
`Packaged npm install smoke failed: ${installed.output.trim()}`
)
}
const lifecycleMarker = await readFile(
join(
npmProject,
'node_modules',
'goodbuddy-packaged-npm-smoke',
'lifecycle-ran.txt'
),
'utf8'
)
if (lifecycleMarker !== 'ready\n') {
throw new Error('Packaged npm lifecycle smoke failed')
}
console.log('Packaged DeepSeek Harness utility smoke: ready')
console.log(
`Packaged npm install smoke: ready (${npmManifest.version})`
)
} finally {
await rm(root, { recursive: true, force: true })
}
}
void main().catch((error) => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
+206 -128
View File
@@ -1,4 +1,4 @@
const { createHash } = require('node:crypto')
const { createHash } = require("node:crypto");
const {
chmod,
mkdir,
@@ -6,101 +6,84 @@ const {
rename,
rm,
stat,
writeFile
} = require('node:fs/promises')
const { existsSync } = require('node:fs')
const { join } = require('node:path')
const { spawnSync } = require('node:child_process')
const tar = require('tar')
const { sha256File } = require('./file-hash.cjs')
writeFile,
} = require("node:fs/promises");
const { existsSync } = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const tar = require("tar");
const { sha256File } = require("./file-hash.cjs");
const {
opencodeVersion,
prepareBundledOpenCodeConfig,
} = require("./opencode-config.cjs");
const { npmInvocation } = require("./npm-invocation.cjs");
const opencodeVersion = '1.18.9'
const architectureNames = {
1: 'x64',
3: 'arm64'
}
1: "x64",
3: "arm64",
};
const platformNames = {
darwin: 'darwin',
linux: 'linux',
win32: 'windows'
}
darwin: "darwin",
linux: "linux",
win32: "windows",
};
function sha512Integrity(contents) {
return `sha512-${createHash('sha512').update(contents).digest('base64')}`
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
}
async function lockedIntegrity(projectDir, packageName) {
async function lockedIntegrity(projectDir, packageName, version) {
const lock = JSON.parse(
await readFile(join(projectDir, 'package-lock.json'), 'utf8')
)
const entry = lock.packages?.[`node_modules/${packageName}`]
await readFile(join(projectDir, "package-lock.json"), "utf8"),
);
const entry = lock.packages?.[`node_modules/${packageName}`];
if (
entry?.version !== opencodeVersion ||
typeof entry.integrity !== 'string'
entry?.version !== version ||
typeof entry.integrity !== "string"
) {
throw new Error(
`Missing locked ${packageName}@${opencodeVersion} integrity`
)
`Missing locked ${packageName}@${version} integrity`,
);
}
return entry.integrity
return entry.integrity;
}
function npmInvocation() {
const npmCli = process.env.npm_execpath
if (npmCli) {
return {
command: process.execPath,
prefixArgs: [npmCli]
}
}
if (process.platform === 'win32') {
throw new Error('npm_execpath is required to prepare bundled runtimes')
}
return {
command: 'npm',
prefixArgs: []
}
}
async function downloadPackage(projectDir, packageName, integrity) {
const cacheDirectory = join(
projectDir,
'.runtime-resources',
'cache'
)
await mkdir(cacheDirectory, { recursive: true })
async function downloadPackage(projectDir, packageName, version, integrity) {
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
await mkdir(cacheDirectory, { recursive: true });
const archivePath = join(
cacheDirectory,
`${packageName}-${opencodeVersion}.tgz`
)
`${packageName.replaceAll("/", "-").replaceAll("@", "")}-${version}.tgz`,
);
if (existsSync(archivePath)) {
const cached = await readFile(archivePath)
const cached = await readFile(archivePath);
if (sha512Integrity(cached) === integrity) {
return archivePath
return archivePath;
}
await rm(archivePath, { force: true })
await rm(archivePath, { force: true });
}
const npm = npmInvocation()
const npm = npmInvocation(projectDir);
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
'pack',
`${packageName}@${opencodeVersion}`,
'--ignore-scripts',
'--json',
'--pack-destination',
cacheDirectory
"pack",
`${packageName}@${version}`,
"--ignore-scripts",
"--json",
"--pack-destination",
cacheDirectory,
],
{
cwd: projectDir,
encoding: 'utf8',
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true
}
)
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to fetch ${packageName}: ${
@@ -108,63 +91,150 @@ async function downloadPackage(projectDir, packageName, integrity) {
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`
)
}`,
);
}
const output = JSON.parse(result.stdout)
const downloadedPath = join(cacheDirectory, output[0].filename)
const contents = await readFile(downloadedPath)
const output = JSON.parse(result.stdout);
const downloadedPath = join(cacheDirectory, output[0].filename);
const contents = await readFile(downloadedPath);
if (sha512Integrity(contents) !== integrity) {
await rm(downloadedPath, { force: true })
throw new Error(`Integrity verification failed for ${packageName}`)
await rm(downloadedPath, { force: true });
throw new Error(`Integrity verification failed for ${packageName}`);
}
if (downloadedPath !== archivePath) {
await rm(archivePath, { force: true })
await rename(downloadedPath, archivePath)
await rm(archivePath, { force: true });
await rename(downloadedPath, archivePath);
}
return archivePath
return archivePath;
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName
const architecture = architectureNames[context.arch]
const packagePlatform = platformNames[platform]
if (!architecture || !packagePlatform) {
throw new Error(
`Bundled OpenCode does not support ${platform}/${context.arch}`
)
}
const suffix =
architecture === 'x64' ? `${architecture}-baseline` : architecture
const packageName = `opencode-${packagePlatform}-${suffix}`
const projectDir = context.packager.projectDir
const integrity = await lockedIntegrity(projectDir, packageName)
const targetDirectory = join(
projectDir,
'.runtime-resources',
architecture
)
const readyPath = join(targetDirectory, '.ready.json')
const executable = platform === 'win32' ? 'opencode.exe' : 'opencode'
const preparedPath = join(targetDirectory, executable)
const identity = {
packageName,
version: opencodeVersion,
integrity
}
async function prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
version,
) {
const packageName = `@vscode/ripgrep-${platform}-${architecture}`;
const integrity = await lockedIntegrity(projectDir, packageName, version);
const executable = platform === "win32" ? "rg.exe" : "rg";
const preparedPath = join(targetDirectory, executable);
const readyPath = join(targetDirectory, ".ripgrep-ready.json");
const identity = { packageName, version, integrity };
try {
const ready = JSON.parse(await readFile(readyPath, 'utf8'))
const ready = JSON.parse(await readFile(readyPath, "utf8"));
if (
ready.packageName === identity.packageName &&
ready.version === identity.version &&
ready.integrity === identity.integrity &&
typeof ready.executableSha256 === 'string' &&
(platform === 'win32' ||
typeof ready.executableSha256 === "string" &&
(platform === "win32" || ((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
return;
}
} catch {
// Rebuild an incomplete or stale ripgrep cache.
}
const archivePath = await downloadPackage(
projectDir,
packageName,
version,
integrity,
);
const stagingDirectory = `${targetDirectory}.ripgrep-staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
try {
await tar.x({
file: archivePath,
cwd: stagingDirectory,
strip: 1,
});
const sourcePath = join(stagingDirectory, "bin", executable);
const executableSha256 = await sha256File(sourcePath);
if (platform !== "win32") {
await chmod(sourcePath, 0o755);
}
await mkdir(targetDirectory, { recursive: true });
await rm(preparedPath, { force: true });
await rename(sourcePath, preparedPath);
await writeFile(
readyPath,
JSON.stringify({ ...identity, executableSha256 }),
"utf8",
);
} finally {
await rm(stagingDirectory, { recursive: true, force: true });
}
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName;
const architecture = architectureNames[context.arch];
const packagePlatform = platformNames[platform];
if (!architecture || !packagePlatform) {
throw new Error(
`Bundled OpenCode does not support ${platform}/${context.arch}`,
);
}
const suffix =
architecture === "x64" ? `${architecture}-baseline` : architecture;
const packageName = `opencode-${packagePlatform}-${suffix}`;
const projectDir = context.packager.projectDir;
await prepareBundledOpenCodeConfig(projectDir);
const projectPackage = JSON.parse(
await readFile(join(projectDir, "package.json"), "utf8"),
);
const ripgrepVersion = projectPackage.dependencies["@vscode/ripgrep"];
if (typeof ripgrepVersion !== "string" || !/^\d+\.\d+\.\d+$/u.test(ripgrepVersion)) {
throw new Error("Missing exact @vscode/ripgrep dependency version");
}
await writeFile(
join(projectDir, "out", "main", "package.json"),
`${JSON.stringify(
{
name: "@deepseek-ai/dsh-llm",
version: projectPackage.dependencies["@deepseek-ai/dsh-llm"],
private: true,
type: "module",
},
null,
2,
)}\n`,
"utf8",
);
const integrity = await lockedIntegrity(projectDir, packageName, opencodeVersion);
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
const readyPath = join(targetDirectory, ".ready.json");
const executable = platform === "win32" ? "opencode.exe" : "opencode";
const preparedPath = join(targetDirectory, executable);
const identity = {
packageName,
version: opencodeVersion,
integrity,
};
try {
const ready = JSON.parse(await readFile(readyPath, "utf8"));
if (
ready.packageName === identity.packageName &&
ready.version === identity.version &&
ready.integrity === identity.integrity &&
typeof ready.executableSha256 === "string" &&
(platform === "win32" ||
((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
return
await prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
ripgrepVersion,
);
return;
}
} catch {
// Rebuild an incomplete or stale runtime cache.
@@ -173,32 +243,40 @@ module.exports = async function prepareBundledRuntimes(context) {
const archivePath = await downloadPackage(
projectDir,
packageName,
integrity
)
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`
await rm(stagingDirectory, { recursive: true, force: true })
await mkdir(stagingDirectory, { recursive: true })
opencodeVersion,
integrity,
);
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
try {
await tar.x({
file: archivePath,
cwd: stagingDirectory,
strip: 1
})
const sourcePath = join(stagingDirectory, 'bin', executable)
const stagingExecutable = join(stagingDirectory, executable)
await rename(sourcePath, stagingExecutable)
if (platform !== 'win32') {
await chmod(stagingExecutable, 0o755)
strip: 1,
});
const sourcePath = join(stagingDirectory, "bin", executable);
const stagingExecutable = join(stagingDirectory, executable);
await rename(sourcePath, stagingExecutable);
if (platform !== "win32") {
await chmod(stagingExecutable, 0o755);
}
const executableSha256 = await sha256File(stagingExecutable)
const executableSha256 = await sha256File(stagingExecutable);
await writeFile(
join(stagingDirectory, '.ready.json'),
join(stagingDirectory, ".ready.json"),
JSON.stringify({ ...identity, executableSha256 }),
'utf8'
)
await rm(targetDirectory, { recursive: true, force: true })
await rename(stagingDirectory, targetDirectory)
"utf8",
);
await rm(targetDirectory, { recursive: true, force: true });
await rename(stagingDirectory, targetDirectory);
} finally {
await rm(stagingDirectory, { recursive: true, force: true })
await rm(stagingDirectory, { recursive: true, force: true });
}
}
await prepareRipgrep(
projectDir,
targetDirectory,
platform,
architecture,
ripgrepVersion,
);
};
+31
View File
@@ -0,0 +1,31 @@
function preflightRegisteredProductionKey(options) {
const keyId = options.keyId
if (!keyId) {
throw new Error(options.missingKeyIdMessage)
}
const signingRecord = options.registry.keys.find(
(key) => key.keyId === keyId
)
if (!signingRecord) {
throw new Error(
`Production ${options.component} public signing key ID "${keyId}" is absent from resources/agent-release-keys.json; provision the matching public key before building a release`
)
}
if (signingRecord.environment !== 'production') {
throw new Error(
`Production ${options.component} signing key ID "${keyId}" is not registered for production`
)
}
if (
options.registry.revocations.some(
(revocation) => revocation.keyId === keyId
)
) {
throw new Error(
`Production ${options.component} signing key ID "${keyId}" is revoked`
)
}
return signingRecord
}
module.exports = { preflightRegisteredProductionKey }
+5
View File
@@ -0,0 +1,5 @@
{
"name": "goodbuddy-harness-utility-smoke",
"private": true,
"main": "deepseek-harness-utility-smoke.cjs"
}
+39
View File
@@ -0,0 +1,39 @@
const { readdirSync, readFileSync, statSync } = require('node:fs')
const { join, relative } = require('node:path')
const { opencodeVersion } = require('./opencode-config.cjs')
function verifyOpenCodeConfig(resources, source) {
const destination = join(resources, 'runtimes', 'opencode-config')
const manifestPath = join(destination, 'node_modules', '@opencode-ai', 'plugin', 'package.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
if (manifest.name !== '@opencode-ai/plugin' || manifest.version !== opencodeVersion) {
throw new Error('Packaged OpenCode plugin version is invalid')
}
const pending = [source]
let files = 0
while (pending.length > 0) {
const directory = pending.pop()
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const file = join(directory, entry.name)
if (entry.isDirectory()) {
pending.push(file)
} else if (entry.isFile()) {
const target = join(destination, relative(source, file))
if (!statSync(target, { throwIfNoEntry: false })?.isFile() ||
!readFileSync(target).equals(readFileSync(file))) {
throw new Error(`Packaged OpenCode dependency is missing or changed: ${relative(source, file)}`)
}
files++
}
}
}
return files
}
function verifyHarnessBundleImports(source, name) {
if (/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']@deepseek-ai\//u.test(source)) {
throw new Error(`DeepSeek Harness bundle contains an external DeepSeek dependency: ${name}`)
}
}
module.exports = { verifyOpenCodeConfig, verifyHarnessBundleImports }
+71
View File
@@ -0,0 +1,71 @@
const { readFileSync } = require('node:fs')
const { resolve } = require('node:path')
const {
targetDefinitions
} = require('./aggregate-release.cjs')
const expectedInstallerCount = targetDefinitions.reduce(
(count, target) => count + target.formats.length,
0
)
function parseArguments(argv) {
if (argv.length !== 2 || argv[0] !== '--manifest' || !argv[1]) {
throw new Error('必须指定 --manifest')
}
return { manifest: resolve(argv[1]) }
}
async function verifySiteRelease(manifest, request = fetch) {
const files = Object.values(manifest?.targets ?? {}).flatMap(
(target) => Object.values(target?.files ?? {})
)
if (files.length !== expectedInstallerCount) {
throw new Error(`官网发布文件数量错误:${files.length}`)
}
const urls = new Set()
for (const file of files) {
if (
typeof file?.url !== 'string' ||
!Number.isSafeInteger(file.size) ||
file.size < 1 ||
urls.has(file.url)
) {
throw new Error('官网发布文件元数据无效')
}
urls.add(file.url)
const response = await request(file.url, {
method: 'HEAD',
redirect: 'error'
})
if (!response.ok) {
throw new Error(`OSS 文件不可访问:${file.url}(${response.status})`)
}
const contentLength = Number(response.headers.get('content-length'))
if (contentLength !== file.size) {
throw new Error(
`OSS 文件大小不匹配:${file.url},期望 ${file.size},实际 ${contentLength}`
)
}
}
return files.length
}
async function main(argv = process.argv.slice(2)) {
const options = parseArguments(argv)
const manifest = JSON.parse(readFileSync(options.manifest, 'utf8'))
const count = await verifySiteRelease(manifest)
console.log(`OSS 发布文件验证通过:${count} 个`)
}
module.exports = {
parseArguments,
verifySiteRelease
}
if (require.main === module) {
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
}
+141
View File
@@ -0,0 +1,141 @@
const { readSync } = require('node:fs')
const localFileHeaderSignature = 0x04034b50
const centralDirectoryHeaderSignature = 0x02014b50
const endOfCentralDirectorySignature = 0x06054b50
function readExactZipBytes(handle, length, position, label) {
const output = Buffer.allocUnsafe(length)
let offset = 0
while (offset < length) {
const bytesRead = readSync(
handle,
output,
offset,
length - offset,
position + offset
)
if (bytesRead <= 0) {
throw new Error(`${label} is truncated`)
}
offset += bytesRead
}
return output
}
function readZipCentralDirectory(handle, archiveSize, limits) {
if (archiveSize < 22) {
throw new Error(`${limits.label} central directory is missing`)
}
const endSize = Math.min(archiveSize, 65_557)
const endStart = archiveSize - endSize
const end = readExactZipBytes(
handle,
endSize,
endStart,
`${limits.label} end record`
)
let endOffset = -1
for (let index = end.length - 22; index >= 0; index -= 1) {
if (
end.readUInt32LE(index) === endOfCentralDirectorySignature &&
index + 22 + end.readUInt16LE(index + 20) === end.length
) {
endOffset = index
break
}
}
if (endOffset < 0) {
throw new Error(`${limits.label} central directory is missing`)
}
const diskNumber = end.readUInt16LE(endOffset + 4)
const centralDisk = end.readUInt16LE(endOffset + 6)
const diskEntryCount = end.readUInt16LE(endOffset + 8)
const entryCount = end.readUInt16LE(endOffset + 10)
const centralSize = end.readUInt32LE(endOffset + 12)
const centralOffset = end.readUInt32LE(endOffset + 16)
const absoluteEndOffset = endStart + endOffset
if (
diskNumber !== 0 ||
centralDisk !== 0 ||
diskEntryCount !== entryCount ||
entryCount === 0 ||
entryCount === 0xffff ||
entryCount < limits.minimumEntries ||
entryCount > limits.maximumEntries ||
centralSize === 0xffffffff ||
centralSize < 46 ||
centralSize > limits.maximumCentralDirectoryBytes ||
centralOffset === 0xffffffff ||
centralOffset + centralSize !== absoluteEndOffset
) {
throw new Error(`${limits.label} central directory is invalid`)
}
const central = readExactZipBytes(
handle,
centralSize,
centralOffset,
`${limits.label} central directory`
)
const entries = []
let offset = 0
for (let index = 0; index < entryCount; index += 1) {
if (
offset + 46 > central.length ||
central.readUInt32LE(offset) !==
centralDirectoryHeaderSignature
) {
throw new Error(
`${limits.label} central directory entry is invalid`
)
}
const nameLength = central.readUInt16LE(offset + 28)
const extraLength = central.readUInt16LE(offset + 30)
const commentLength = central.readUInt16LE(offset + 32)
const entryLength =
46 + nameLength + extraLength + commentLength
if (offset + entryLength > central.length) {
throw new Error(
`${limits.label} central directory entry exceeds its bounds`
)
}
const flags = central.readUInt16LE(offset + 8)
const nameBytes = central.subarray(
offset + 46,
offset + 46 + nameLength
)
const name = new TextDecoder(
flags & 0x0800 ? 'utf-8' : 'latin1',
{ fatal: true }
).decode(nameBytes)
entries.push({
name,
nameBytes: Buffer.from(nameBytes),
flags,
compression: central.readUInt16LE(offset + 10),
checksum: central.readUInt32LE(offset + 16),
compressedSize: central.readUInt32LE(offset + 20),
size: central.readUInt32LE(offset + 24),
diskStart: central.readUInt16LE(offset + 34),
localOffset: central.readUInt32LE(offset + 42)
})
offset += entryLength
}
if (offset !== central.length) {
throw new Error(
`${limits.label} central directory count is invalid`
)
}
return {
centralOffset,
entries
}
}
module.exports = {
centralDirectoryHeaderSignature,
endOfCentralDirectorySignature,
localFileHeaderSignature,
readExactZipBytes,
readZipCentralDirectory
}
-308
View File
@@ -1,308 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GoodBuddy 控制中心 · AI 运维仪表盘</title>
<meta name="description" content="GoodBuddy Control Center —— AI 代理运维监控与任务管理仪表盘" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<a class="skip-link" href="#main-content">跳转到主要内容</a>
<header class="site-header">
<div class="header-inner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">GB</span>
<span class="brand-name">GoodBuddy 控制中心</span>
</div>
<button
id="navToggleBtn"
class="nav-toggle"
type="button"
aria-expanded="false"
aria-controls="primaryNav"
>
<span class="visually-hidden">切换导航菜单</span>
<span class="nav-toggle-bar" aria-hidden="true"></span>
<span class="nav-toggle-bar" aria-hidden="true"></span>
<span class="nav-toggle-bar" aria-hidden="true"></span>
</button>
<nav id="primaryNav" class="primary-nav" aria-label="主导航">
<ul>
<li><a href="#overview">概览</a></li>
<li><a href="#agents">代理状态</a></li>
<li><a href="#tasks">任务看板</a></li>
<li><a href="#activity">活动日志</a></li>
</ul>
</nav>
<div class="header-actions">
<button id="themeToggleBtn" class="theme-toggle" type="button" aria-pressed="false">
<span class="theme-toggle-icon" aria-hidden="true">🌙</span>
<span class="theme-toggle-label">深色模式</span>
</button>
</div>
</div>
</header>
<main id="main-content">
<section id="overview" class="hero" aria-labelledby="overview-heading">
<div class="section-inner">
<h1 id="overview-heading">今日运维概览</h1>
<p class="hero-subtitle">实时掌握 AI 代理集群健康状况与任务执行进度</p>
<div class="metrics" role="list">
<div class="metric-card" role="listitem">
<p class="metric-label">总任务数</p>
<p class="metric-value">128</p>
<p class="metric-delta metric-delta--up">较昨日 +12</p>
</div>
<div class="metric-card" role="listitem">
<p class="metric-label">运行中代理</p>
<p class="metric-value">6</p>
<p class="metric-delta metric-delta--up">全部在线</p>
</div>
<div class="metric-card" role="listitem">
<p class="metric-label">已完成任务</p>
<p class="metric-value">96</p>
<p class="metric-delta metric-delta--up">完成率 75%</p>
</div>
<div class="metric-card" role="listitem">
<p class="metric-label">平均响应时间</p>
<p class="metric-value">340ms</p>
<p class="metric-delta metric-delta--down">较昨日 -18ms</p>
</div>
</div>
</div>
</section>
<section id="agents" class="agents" aria-labelledby="agents-heading">
<div class="section-inner">
<h2 id="agents-heading">代理状态</h2>
<ul class="agent-grid">
<li class="agent-card">
<div class="agent-card-top">
<span class="agent-avatar" aria-hidden="true">A1</span>
<span class="status-badge status-running">运行中</span>
</div>
<h3 class="agent-name">Agent Alpha</h3>
<p class="agent-meta">负责:数据同步 &amp; 报表生成</p>
<p class="agent-meta">最近活跃:2 分钟前</p>
</li>
<li class="agent-card">
<div class="agent-card-top">
<span class="agent-avatar" aria-hidden="true">B2</span>
<span class="status-badge status-idle">空闲</span>
</div>
<h3 class="agent-name">Agent Beta</h3>
<p class="agent-meta">负责:客户工单处理</p>
<p class="agent-meta">最近活跃:15 分钟前</p>
</li>
<li class="agent-card">
<div class="agent-card-top">
<span class="agent-avatar" aria-hidden="true">G3</span>
<span class="status-badge status-maintenance">维护中</span>
</div>
<h3 class="agent-name">Agent Gamma</h3>
<p class="agent-meta">负责:库存预警检测</p>
<p class="agent-meta">最近活跃:1 小时前</p>
</li>
<li class="agent-card">
<div class="agent-card-top">
<span class="agent-avatar" aria-hidden="true">D4</span>
<span class="status-badge status-running">运行中</span>
</div>
<h3 class="agent-name">Agent Delta</h3>
<p class="agent-meta">负责:日志汇总分析</p>
<p class="agent-meta">最近活跃:刚刚</p>
</li>
</ul>
</div>
</section>
<section id="tasks" class="tasks" aria-labelledby="tasks-heading">
<div class="section-inner">
<div class="tasks-header">
<h2 id="tasks-heading">任务看板</h2>
<button id="addTaskBtn" class="btn btn-primary" type="button">
<span aria-hidden="true">+</span> 添加任务
</button>
</div>
<div class="filter-group" role="group" aria-label="按状态筛选任务">
<button class="filter-btn is-active" type="button" data-filter="all" aria-pressed="true">
全部
</button>
<button class="filter-btn" type="button" data-filter="running" aria-pressed="false">
运行中
</button>
<button class="filter-btn" type="button" data-filter="completed" aria-pressed="false">
已完成
</button>
</div>
<p id="taskEmptyState" class="empty-state" hidden>没有符合条件的任务。</p>
<ul id="taskList" class="task-list">
<li class="task-card" data-status="running">
<div class="task-card-top">
<h3 class="task-title">数据同步任务</h3>
<span class="status-badge status-running">运行中</span>
</div>
<p class="task-meta">负责人:Agent Alpha</p>
<p class="task-meta">截止时间:今天 18:00</p>
</li>
<li class="task-card" data-status="completed">
<div class="task-card-top">
<h3 class="task-title">客户工单自动回复</h3>
<span class="status-badge status-completed">已完成</span>
</div>
<p class="task-meta">负责人:Agent Beta</p>
<p class="task-meta">完成时间:今天 09:24</p>
</li>
<li class="task-card" data-status="running">
<div class="task-card-top">
<h3 class="task-title">每日日志汇总</h3>
<span class="status-badge status-running">运行中</span>
</div>
<p class="task-meta">负责人:Agent Delta</p>
<p class="task-meta">截止时间:今天 23:00</p>
</li>
<li class="task-card" data-status="completed">
<div class="task-card-top">
<h3 class="task-title">库存预警检测</h3>
<span class="status-badge status-completed">已完成</span>
</div>
<p class="task-meta">负责人:Agent Gamma</p>
<p class="task-meta">完成时间:昨天 21:10</p>
</li>
<li class="task-card" data-status="running">
<div class="task-card-top">
<h3 class="task-title">周报生成</h3>
<span class="status-badge status-running">运行中</span>
</div>
<p class="task-meta">负责人:Agent Alpha</p>
<p class="task-meta">截止时间:本周五 12:00</p>
</li>
</ul>
</div>
</section>
<section id="activity" class="activity" aria-labelledby="activity-heading">
<div class="section-inner">
<h2 id="activity-heading">活动日志</h2>
<div class="search-field">
<label for="activitySearch" class="visually-hidden">搜索活动日志</label>
<input
type="search"
id="activitySearch"
class="search-input"
placeholder="搜索活动日志,例如“Agent Alpha”或“同步”"
autocomplete="off"
/>
</div>
<p id="activityEmptyState" class="empty-state" hidden>没有找到匹配的活动记录。</p>
<ul id="activityList" class="activity-list">
<li class="activity-item">
<span class="activity-time">10:42</span>
<span class="activity-text">Agent Alpha 完成了「数据同步任务」的第 3 阶段</span>
</li>
<li class="activity-item">
<span class="activity-time">10:31</span>
<span class="activity-text">Agent Beta 自动回复了 12 条客户工单</span>
</li>
<li class="activity-item">
<span class="activity-time">09:58</span>
<span class="activity-text">Agent Gamma 触发了库存预警检测维护流程</span>
</li>
<li class="activity-item">
<span class="activity-time">09:20</span>
<span class="activity-text">Agent Delta 生成了每日日志汇总报告</span>
</li>
<li class="activity-item">
<span class="activity-time">08:47</span>
<span class="activity-text">系统检测到 Agent Alpha 响应时间恢复正常</span>
</li>
<li class="activity-item">
<span class="activity-time">08:02</span>
<span class="activity-text">Agent Delta 启动,开始监听新任务队列</span>
</li>
</ul>
</div>
</section>
</main>
<footer class="site-footer">
<div class="section-inner">
<p>© 2026 GoodBuddy Control Center. 保留所有权利。</p>
<p class="footer-marker">CONTINUE_WEB_DEMO_OK</p>
</div>
</footer>
<div class="modal-overlay" id="taskModalOverlay" hidden></div>
<div
class="modal"
id="taskModal"
role="dialog"
aria-modal="true"
aria-labelledby="taskModalTitle"
hidden
>
<form id="taskForm" class="modal-form" novalidate>
<div class="modal-header">
<h2 id="taskModalTitle">添加新任务</h2>
<button type="button" id="modalCloseBtn" class="modal-close" aria-label="关闭对话框">
×
</button>
</div>
<div class="modal-body">
<div class="form-field">
<label for="taskTitleInput">任务名称</label>
<input
type="text"
id="taskTitleInput"
name="taskTitle"
required
maxlength="80"
placeholder="例如:生成月度运营报告"
/>
</div>
<div class="form-field">
<label for="taskAssigneeSelect">负责人</label>
<select id="taskAssigneeSelect" name="taskAssignee">
<option value="Agent Alpha">Agent Alpha</option>
<option value="Agent Beta">Agent Beta</option>
<option value="Agent Gamma">Agent Gamma</option>
<option value="Agent Delta">Agent Delta</option>
</select>
</div>
<div class="form-field">
<label for="taskStatusSelect">初始状态</label>
<select id="taskStatusSelect" name="taskStatus">
<option value="running">运行中</option>
<option value="completed">已完成</option>
</select>
</div>
<p id="taskFormError" class="form-error" role="alert" hidden>请填写任务名称。</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" id="modalCancelBtn">取消</button>
<button type="submit" class="btn btn-primary">保存任务</button>
</div>
</form>
</div>
<script src="script.js"></script>
</body>
</html>
-382
View File
@@ -1,382 +0,0 @@
/*!
* GoodBuddy Control Center — script.js
* Vanilla JS only. No external dependencies.
* CONTINUE_WEB_DEMO_OK
*/
/* global document, window */
(function () {
"use strict";
/* ---------------------------------------------------------
* Utilities
* ------------------------------------------------------- */
/**
* Escapes HTML-sensitive characters in a string so that it is
* safe to insert as text content inside markup.
* @param {string} value
* @returns {string}
*/
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
function qsa(selector, scope) {
return Array.prototype.slice.call((scope || document).querySelectorAll(selector));
}
/* ---------------------------------------------------------
* Theme toggle (persisted via localStorage)
* ------------------------------------------------------- */
var THEME_STORAGE_KEY = "goodbuddy-theme";
function initTheme() {
var themeToggleBtn = qs("#themeToggleBtn");
var iconEl = qs(".theme-toggle-icon", themeToggleBtn);
var labelEl = qs(".theme-toggle-label", themeToggleBtn);
function applyTheme(theme) {
var label;
if (theme === "dark") {
document.documentElement.setAttribute("data-theme", "dark");
themeToggleBtn.setAttribute("aria-pressed", "true");
if (iconEl) iconEl.textContent = "☀️";
label = "浅色模式";
} else {
document.documentElement.removeAttribute("data-theme");
themeToggleBtn.setAttribute("aria-pressed", "false");
if (iconEl) iconEl.textContent = "🌙";
label = "深色模式";
}
if (labelEl) labelEl.textContent = label;
// .theme-toggle-label is visually hidden on narrow viewports and the
// icon is aria-hidden, so without an explicit aria-label the button
// has no accessible name on mobile. Keep it in sync with the visible
// desktop label on every theme change.
themeToggleBtn.setAttribute("aria-label", label);
}
var stored = null;
try {
stored = window.localStorage.getItem(THEME_STORAGE_KEY);
} catch {
/* localStorage unavailable, use the default theme */
}
var prefersDark =
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
var initialTheme = stored || (prefersDark ? "dark" : "light");
applyTheme(initialTheme);
themeToggleBtn.addEventListener("click", function () {
var isDark = document.documentElement.getAttribute("data-theme") === "dark";
var nextTheme = isDark ? "light" : "dark";
applyTheme(nextTheme);
try {
window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
} catch {
/* localStorage unavailable — ignore silently */
}
});
}
/* ---------------------------------------------------------
* Mobile navigation toggle
* ------------------------------------------------------- */
function initNavToggle() {
var navToggleBtn = qs("#navToggleBtn");
var primaryNav = qs("#primaryNav");
if (!navToggleBtn || !primaryNav) return;
navToggleBtn.addEventListener("click", function () {
var isOpen = primaryNav.classList.toggle("is-open");
navToggleBtn.setAttribute("aria-expanded", String(isOpen));
});
qsa("a", primaryNav).forEach(function (link) {
link.addEventListener("click", function () {
primaryNav.classList.remove("is-open");
navToggleBtn.setAttribute("aria-expanded", "false");
});
});
}
/* ---------------------------------------------------------
* Task board: filtering + adding tasks
* ------------------------------------------------------- */
var STATUS_LABELS = {
running: "运行中",
completed: "已完成"
};
var STATUS_BADGE_CLASS = {
running: "status-running",
completed: "status-completed"
};
function initTaskBoard() {
var taskList = qs("#taskList");
var filterButtons = qsa(".filter-btn");
var emptyState = qs("#taskEmptyState");
function currentFilter() {
var activeBtn = qs(".filter-btn.is-active");
return activeBtn ? activeBtn.getAttribute("data-filter") : "all";
}
function applyFilter() {
var filter = currentFilter();
var cards = qsa(".task-card", taskList);
var visibleCount = 0;
cards.forEach(function (card) {
var status = card.getAttribute("data-status");
var matches = filter === "all" || status === filter;
card.hidden = !matches;
if (matches) visibleCount += 1;
});
if (emptyState) {
emptyState.hidden = visibleCount !== 0;
}
}
filterButtons.forEach(function (btn) {
btn.addEventListener("click", function () {
filterButtons.forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
applyFilter();
});
});
/**
* Builds a task card list item using safe DOM APIs so that
* user-provided text can never be interpreted as markup.
*/
function createTaskCard(title, assignee, status) {
var li = document.createElement("li");
li.className = "task-card";
li.setAttribute("data-status", status);
var top = document.createElement("div");
top.className = "task-card-top";
var heading = document.createElement("h3");
heading.className = "task-title";
// textContent assigns the raw string as plain text — the browser
// never parses it as markup, so no HTML-escaping is needed (or
// wanted) here. Escaping first would make textContent render the
// escaped entities literally (e.g. "&lt;img...") instead of the
// user's exact text.
heading.textContent = title;
var badge = document.createElement("span");
badge.className = "status-badge " + STATUS_BADGE_CLASS[status];
badge.textContent = STATUS_LABELS[status];
top.appendChild(heading);
top.appendChild(badge);
var meta = document.createElement("p");
meta.className = "task-meta";
meta.textContent = "负责人:" + assignee;
var metaTime = document.createElement("p");
metaTime.className = "task-meta";
var now = new Date();
var timeLabel =
status === "completed"
? "完成时间:刚刚"
: "截止时间:" +
String(now.getHours()).padStart(2, "0") +
":" +
String(now.getMinutes()).padStart(2, "0");
metaTime.textContent = timeLabel;
li.appendChild(top);
li.appendChild(meta);
li.appendChild(metaTime);
return li;
}
applyFilter();
return {
addTask: function (title, assignee, status) {
// Pass the trimmed, un-escaped user text straight through.
// createTaskCard assigns it via textContent (never innerHTML),
// so it is rendered as inert plain text and can never execute as
// markup/script regardless of its contents.
var card = createTaskCard(title.trim(), assignee, status);
taskList.insertBefore(card, taskList.firstChild);
applyFilter();
return card;
}
};
}
/* ---------------------------------------------------------
* Activity log search
* ------------------------------------------------------- */
function initActivitySearch() {
var searchInput = qs("#activitySearch");
var activityList = qs("#activityList");
var emptyState = qs("#activityEmptyState");
if (!searchInput || !activityList) return;
function runSearch() {
var query = searchInput.value.trim().toLowerCase();
var items = qsa(".activity-item", activityList);
var visibleCount = 0;
items.forEach(function (item) {
var text = item.textContent.toLowerCase();
var matches = query === "" || text.indexOf(query) !== -1;
item.hidden = !matches;
if (matches) visibleCount += 1;
});
if (emptyState) {
emptyState.hidden = visibleCount !== 0;
}
}
searchInput.addEventListener("input", runSearch);
}
/* ---------------------------------------------------------
* Add-task modal (accessible dialog)
* ------------------------------------------------------- */
function initTaskModal(taskBoard) {
var overlay = qs("#taskModalOverlay");
var modal = qs("#taskModal");
var openBtn = qs("#addTaskBtn");
var closeBtn = qs("#modalCloseBtn");
var cancelBtn = qs("#modalCancelBtn");
var form = qs("#taskForm");
var titleInput = qs("#taskTitleInput");
var assigneeSelect = qs("#taskAssigneeSelect");
var statusSelect = qs("#taskStatusSelect");
var errorMsg = qs("#taskFormError");
if (!overlay || !modal || !openBtn || !form) return;
var lastFocusedElement = null;
function getFocusableElements() {
return qsa(
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])',
modal
).filter(function (el) {
return el.offsetParent !== null;
});
}
function openModal() {
lastFocusedElement = document.activeElement;
overlay.hidden = false;
modal.hidden = false;
errorMsg.hidden = true;
form.reset();
document.body.style.overflow = "hidden";
titleInput.focus();
document.addEventListener("keydown", handleKeydown);
}
function closeModal() {
overlay.hidden = true;
modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", handleKeydown);
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
lastFocusedElement.focus();
}
}
function handleKeydown(event) {
if (event.key === "Escape") {
event.preventDefault();
closeModal();
return;
}
if (event.key === "Tab") {
var focusable = getFocusableElements();
if (focusable.length === 0) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
}
openBtn.addEventListener("click", openModal);
closeBtn.addEventListener("click", closeModal);
cancelBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", closeModal);
form.addEventListener("submit", function (event) {
event.preventDefault();
var title = titleInput.value.trim();
if (title === "") {
errorMsg.hidden = false;
titleInput.focus();
return;
}
errorMsg.hidden = true;
taskBoard.addTask(title, assigneeSelect.value, statusSelect.value);
closeModal();
});
}
/* ---------------------------------------------------------
* Bootstrap
* ------------------------------------------------------- */
function init() {
initTheme();
initNavToggle();
var taskBoard = initTaskBoard();
initActivitySearch();
initTaskModal(taskBoard);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
// Expose escapeHtml for testability / verification purposes only.
window.__goodbuddyEscapeHtml = escapeHtml;
})();
// CONTINUE_WEB_DEMO_OK
-782
View File
@@ -1,782 +0,0 @@
/* =========================================================
GoodBuddy Control Center — styles.css
========================================================= */
:root {
color-scheme: light;
--color-bg: #f4f6fb;
--color-bg-elevated: #ffffff;
--color-bg-header: rgba(255, 255, 255, 0.92);
--color-border: #dde3ee;
--color-text: #1c2333;
--color-text-muted: #5a6479;
--color-primary: #3a5bfd;
--color-primary-hover: #2a45d6;
--color-primary-contrast: #ffffff;
--color-success: #12a454;
--color-success-bg: #e4f8ec;
--color-warning: #b8860b;
--color-warning-bg: #fbf1d9;
--color-info: #2a6df4;
--color-info-bg: #e6edff;
--color-danger: #d43b3b;
--color-danger-bg: #fdeaea;
--shadow-sm: 0 1px 2px rgba(20, 26, 46, 0.06);
--shadow-md: 0 8px 24px rgba(20, 26, 46, 0.08);
--shadow-lg: 0 20px 48px rgba(20, 26, 46, 0.16);
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 20px;
--transition-fast: 150ms ease;
--transition-base: 220ms ease;
--focus-ring: 0 0 0 3px rgba(58, 91, 253, 0.35);
--max-width: 1180px;
}
[data-theme="dark"] {
color-scheme: dark;
--color-bg: #0f1220;
--color-bg-elevated: #1a1f33;
--color-bg-header: rgba(15, 18, 32, 0.9);
--color-border: #2b3150;
--color-text: #eef1fb;
--color-text-muted: #a3aac4;
--color-primary: #7b93ff;
--color-primary-hover: #93a7ff;
--color-primary-contrast: #0f1220;
--color-success: #35d17d;
--color-success-bg: rgba(53, 209, 125, 0.14);
--color-warning: #e8c15b;
--color-warning-bg: rgba(232, 193, 91, 0.14);
--color-info: #7ea2ff;
--color-info-bg: rgba(126, 162, 255, 0.14);
--color-danger: #f27272;
--color-danger-bg: rgba(242, 114, 114, 0.14);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 20px 48px rgba(0, 0, 0, 0.55);
--focus-ring: 0 0 0 3px rgba(123, 147, 255, 0.45);
}
/* ---------- Reset & base ---------- */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
background: var(--color-bg);
color: var(--color-text);
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans SC", system-ui,
-apple-system, sans-serif;
line-height: 1.6;
transition: background-color var(--transition-base), color var(--transition-base);
}
h1,
h2,
h3,
p {
margin: 0 0 0.5em;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
button {
font-family: inherit;
}
input,
select {
font-family: inherit;
}
.section-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 1.5rem;
}
/* ---------- Accessibility helpers ---------- */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link {
position: absolute;
top: -100px;
left: 1rem;
background: var(--color-primary);
color: var(--color-primary-contrast);
padding: 0.6rem 1rem;
border-radius: var(--radius-sm);
z-index: 1000;
transition: top var(--transition-fast);
}
.skip-link:focus {
top: 1rem;
}
:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
border-radius: var(--radius-sm);
}
a {
color: var(--color-primary);
}
/* ---------- Header ---------- */
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--color-bg-header);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--color-border);
}
.header-inner {
max-width: var(--max-width);
margin: 0 auto;
padding: 0.85rem 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
.brand {
display: flex;
align-items: center;
gap: 0.6rem;
font-weight: 700;
font-size: 1.05rem;
white-space: nowrap;
}
.brand-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, var(--color-primary), #7d5bfd);
color: #fff;
font-size: 0.85rem;
font-weight: 800;
}
.primary-nav {
flex: 1;
}
.primary-nav ul {
display: flex;
gap: 1.5rem;
}
.primary-nav a {
text-decoration: none;
color: var(--color-text-muted);
font-weight: 600;
padding: 0.4rem 0.2rem;
border-radius: var(--radius-sm);
transition: color var(--transition-fast);
}
.primary-nav a:hover {
color: var(--color-primary);
}
.header-actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.9rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-bg-elevated);
color: var(--color-text);
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.theme-toggle:hover {
transform: translateY(-1px);
}
.nav-toggle {
display: none;
flex-direction: column;
gap: 4px;
padding: 0.5rem;
background: transparent;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
cursor: pointer;
}
.nav-toggle-bar {
display: block;
width: 20px;
height: 2px;
background: var(--color-text);
border-radius: 2px;
}
/* ---------- Hero ---------- */
.hero {
padding: 3rem 0 2.5rem;
}
.hero-subtitle {
color: var(--color-text-muted);
font-size: 1.05rem;
max-width: 46ch;
}
.metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-top: 1.75rem;
}
.metric-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 1.25rem;
box-shadow: var(--shadow-sm);
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
}
.metric-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.metric-label {
color: var(--color-text-muted);
font-size: 0.85rem;
font-weight: 600;
}
.metric-value {
font-size: 2rem;
font-weight: 800;
}
.metric-delta {
font-size: 0.82rem;
font-weight: 700;
}
.metric-delta--up {
color: var(--color-success);
}
.metric-delta--down {
color: var(--color-danger);
}
/* ---------- Sections generic ---------- */
.agents,
.tasks,
.activity {
padding: 2.5rem 0;
}
.agents h2,
.tasks h2,
.activity h2 {
font-size: 1.5rem;
}
/* ---------- Agent grid ---------- */
.agent-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-top: 1.25rem;
}
.agent-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 1.25rem;
box-shadow: var(--shadow-sm);
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.agent-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.agent-card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.agent-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--color-info-bg);
color: var(--color-info);
font-weight: 800;
font-size: 0.85rem;
}
.agent-name {
font-size: 1.05rem;
}
.agent-meta {
color: var(--color-text-muted);
font-size: 0.88rem;
margin-bottom: 0.2rem;
}
/* ---------- Status badges ---------- */
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.28rem 0.7rem;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
}
.status-running {
background: var(--color-info-bg);
color: var(--color-info);
}
.status-completed {
background: var(--color-success-bg);
color: var(--color-success);
}
.status-idle {
background: var(--color-warning-bg);
color: var(--color-warning);
}
.status-maintenance {
background: var(--color-danger-bg);
color: var(--color-danger);
}
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.6rem 1.1rem;
border-radius: var(--radius-sm);
border: 1px solid transparent;
font-weight: 700;
font-size: 0.92rem;
cursor: pointer;
transition: background-color var(--transition-fast), transform var(--transition-fast),
box-shadow var(--transition-fast);
}
.btn:hover {
transform: translateY(-1px);
}
.btn-primary {
background: var(--color-primary);
color: var(--color-primary-contrast);
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
background: var(--color-primary-hover);
}
.btn-ghost {
background: transparent;
border-color: var(--color-border);
color: var(--color-text);
}
.btn-ghost:hover {
background: var(--color-bg);
}
/* ---------- Tasks ---------- */
.tasks-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 1rem;
}
.filter-group {
display: flex;
gap: 0.6rem;
margin: 1.25rem 0 1rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.5rem 1rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--color-bg-elevated);
color: var(--color-text-muted);
font-weight: 700;
font-size: 0.88rem;
cursor: pointer;
transition: all var(--transition-fast);
}
.filter-btn:hover {
color: var(--color-primary);
border-color: var(--color-primary);
}
.filter-btn.is-active {
background: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-primary-contrast);
}
.task-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.task-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 1.1rem 1.25rem;
box-shadow: var(--shadow-sm);
transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.task-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.4rem;
}
.task-title {
font-size: 1rem;
word-break: break-word;
}
.task-meta {
color: var(--color-text-muted);
font-size: 0.85rem;
margin-bottom: 0.15rem;
}
.empty-state {
color: var(--color-text-muted);
font-style: italic;
padding: 1rem 0;
}
/* ---------- Activity ---------- */
.search-field {
margin: 1.25rem 0;
}
.search-input {
width: 100%;
max-width: 420px;
padding: 0.65rem 1rem;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-bg-elevated);
color: var(--color-text);
font-size: 0.95rem;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.activity-item {
display: flex;
gap: 1rem;
align-items: baseline;
padding: 0.75rem 1rem;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
}
.activity-time {
flex-shrink: 0;
font-variant-numeric: tabular-nums;
color: var(--color-text-muted);
font-weight: 700;
font-size: 0.85rem;
min-width: 3.2rem;
}
.activity-text {
color: var(--color-text);
}
/* ---------- Footer ---------- */
.site-footer {
border-top: 1px solid var(--color-border);
padding: 1.5rem 0 2.5rem;
color: var(--color-text-muted);
font-size: 0.85rem;
}
.footer-marker {
opacity: 0.55;
font-size: 0.75rem;
letter-spacing: 0.02em;
}
/* ---------- Modal ---------- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(10, 12, 24, 0.5);
z-index: 200;
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(480px, calc(100vw - 2rem));
max-height: calc(100vh - 3rem);
overflow-y: auto;
background: var(--color-bg-elevated);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 201;
border: 1px solid var(--color-border);
}
.modal[hidden],
.modal-overlay[hidden] {
display: none;
}
.modal-form {
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.modal-header h2 {
font-size: 1.15rem;
margin: 0;
}
.modal-close {
background: transparent;
border: none;
font-size: 1.4rem;
line-height: 1;
color: var(--color-text-muted);
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
}
.modal-close:hover {
color: var(--color-danger);
}
.modal-body {
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.form-field label {
font-weight: 700;
font-size: 0.9rem;
}
.form-field input,
.form-field select {
padding: 0.6rem 0.75rem;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-bg);
color: var(--color-text);
font-size: 0.95rem;
}
.form-error {
color: var(--color-danger);
font-size: 0.85rem;
font-weight: 700;
margin: 0;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.1rem 1.5rem;
border-top: 1px solid var(--color-border);
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.metrics {
grid-template-columns: repeat(2, 1fr);
}
.agent-grid {
grid-template-columns: repeat(2, 1fr);
}
.task-list {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.nav-toggle {
display: inline-flex;
}
.primary-nav {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
display: none;
}
.primary-nav.is-open {
display: block;
}
.primary-nav ul {
flex-direction: column;
padding: 1rem 1.5rem;
gap: 0.25rem;
}
.primary-nav a {
display: block;
padding: 0.6rem 0.2rem;
}
.header-inner {
flex-wrap: wrap;
position: relative;
}
.theme-toggle-label {
display: none;
}
}
@media (max-width: 560px) {
.metrics {
grid-template-columns: 1fr;
}
.agent-grid {
grid-template-columns: 1fr;
}
.tasks-header {
flex-direction: column;
align-items: flex-start;
}
}
/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
html {
scroll-behavior: auto;
}
}
@@ -0,0 +1,588 @@
# 自动化、监督与记忆平台总体设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.3 |
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 文档角色 | Task/Job、调度、目标、并行实验、会话监督、分区记忆与持续学习的总纲 |
| 领域模型 | [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md) |
## 1. 背景
GoodBuddy 当前已经具备若干长期助手能力,但它们仍是彼此分离的功能:
1. 当前 Schedule 已支持单次、每日和每周触发,并在创建时绑定稳定产品级 Task 与真实
Conversation;重复触发复用同一身份,文本结果写回 Conversation,独立文件和图片保存为
Artifact。到期执行与用户在回复期间继续发送的普通消息共用 Conversation 级持久队列,
因而不会与当前回复并发写入同一时间线。IANA 时区、Cron、事件触发、租约、重试和完整
Job/Run 抽象仍待实现。
2. 当前智能心跳支持全局或项目范围的每日、每周回顾,读取有界会话、任务和已确认记忆,
生成摘要、记忆建议和后续任务。
3. 专家执行的 Job 支持有限并发和只读综合,但没有实验变量、重复运行、统一指标和结果晋升。
4. 记忆已有全局、项目、会话三种作用域,以及偏好、事实、摘要、流程四种类型,
但检索、来源、时态、冲突和运行级隔离仍不完整。
5. 魔法笔记已经提供“内容旁持续出现 AI 评论”的交互,可作为会话监督的体验参考,
但它只分析笔记或待办,不观察会话和任务运行。
智能心跳长期方向是面向未来的分区记忆,但该模型尚未设计。近期只改善现有心跳的权威入口
与 Global / 多 Project 范围,并保留 Task Center 作为 Task 索引。若继续把 Task、调度、
监督、学习和执行加入“智能心跳”,将无法解释一次后台行为为什么发生、读取了什么、是否
越权、产生了什么影响。
本设计将这些能力统一到一个平台模型中,同时保留不同产品的清晰边界。
## 2. 核心产品判断
### 2.1 智能心跳保持独立,分区记忆协同另行设计
当前智能心跳继续承担周期回顾、报告和建议,并支持 Global 或指定 Project 范围。它不是
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。按分区回顾、候选生成
以及时间/内部事件唤起需要独立设计,不能从当前心跳行为直接推导,也不创建平行记忆类型。
- Scheduled Task 解决“何时在一个 Task 中执行新的 Job”。
- Goal Task 解决“围绕结果在同一 Task 中持续规划和推进”。
- 并行实验解决“隔离多个候选并用相同标准比较”。
- 会话监督解决“独立观察并在必要时评论、告警或暂停”。
- 智能心跳当前解决“在什么范围周期回顾并提出报告和建议”。
- 记忆系统解决“哪些经验可以在什么范围内被未来运行读取”。
- 持续学习解决“候选经验如何经过评估后改变未来行为”。
这些能力可以共享调度、运行、证据、预算和审计基础,但不能共享一段不断膨胀的提示词。
### 2.2 增加会话监督,但不把它等同于第二个聊天 Agent
建议新增会话监督功能,并借鉴魔法笔记的右侧 AI 评论流:
- 默认只观察和评论,不替用户发言。
- 只依据可见消息、工具事件、任务状态、成果和目标进行判断。
- 不读取或展示模型隐藏推理。
- 评论必须引用具体消息、步骤或证据。
- 模型监督可以建议暂停,只有确定性安全规则或用户预先批准的门禁才能自动暂停。
- 监督器不能自动批准工具、扩大目录、跨项目读取记忆或修改安全策略。
### 2.3 先做分区和来源,再做知识图谱
GoodBuddy 当前最需要的不是立即引入重型图数据库,而是保证:
1. 运行只能读取明确允许的记忆分区。
2. 并行实验的各个 Run 不共享可变记忆。
3. 每条记忆知道来自哪次会话、任务、监督判断或实验结果。
4. 新事实与旧事实冲突时保留时态和证据,不静默覆盖。
5. 记忆进入模型上下文前经过范围、状态、敏感度和预算过滤。
SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现明确的关系追踪和跨实体查询
需求后,才考虑时间知识图谱。
### 2.4 学习必须有评估门和回滚
“生成一条总结并保存”不等于持续学习。只有当候选经验通过回放或实验验证,并能安全改变
未来行为时,才构成学习闭环。
初期自动学习只允许产生可审查候选,不允许自动修改:
- 工具权限和审批策略。
- Electron 安全边界。
- 项目根目录和数据访问范围。
- Runtime 当前用户执行权限与 Ask/Execute 边界。
- 系统级提示词。
- 远程消息发送或其他外部副作用策略。
## 3. 目标
### 3.1 用户目标
- 在现有 Task Center 中找到 Scheduled、Event 和 Goal Task,并直接打开关联 Conversation
和对应 Task。
- 清楚知道 Task 的触发原因、当前目标、聚合执行状态、预算和停止条件。
- 在一个工作台中观察多个候选运行,并追溯结论到原始证据。
- 为重要会话启用独立监督,及时发现偏题、遗漏、矛盾、证据不足和风险。
- 知道每条记忆属于哪个范围、从哪里产生、何时有效以及被哪些运行使用。
- 审查、批准、拒绝或回滚系统提出的记忆、模板和策略改进。
### 3.2 产品目标
- 复用现有 Project、Conversation、Task、Run、Artifact、Approval 和 Notification 能力。
- 保持每个 Task 只关联一条 Conversation,同时允许一条 Conversation 承载多个 Task,不为
同一项工作建立第二份内容载体。
- 为所有后台工作提供统一的幂等、租约、恢复、取消、预算和审计语义。
- 保持 Ask 只读,Execute 继续经过现有能力和审批控制。
- 保持本地优先,应用退出后不虚假承诺后台持续执行。
- 保证项目、会话、自动化和实验 Run 之间的记忆隔离。
- 先建立可观测和可评估能力,再允许任何形式的自动行为改变。
## 4. 非目标
本组设计不包含:
- 将 GoodBuddy 变为需要常驻服务器、Redis 或云端控制面的多租户平台。
- 在应用退出后依靠未安装的系统服务继续运行任务。
- 默认允许无人值守高风险 Execute。
- 让模型自行扩大工具、目录、知识库、记忆或网络访问范围。
- 允许多个实验 Run 并发修改同一个用户工作区。
- 记录键盘、持续录屏或静默监控其他应用。
- 把隐藏推理链作为监督、记忆或审计数据保存。
- 初期直接建设通用可视化工作流 DAG 编辑器。
- 将模型评分当作没有误差的客观真值。
## 5. 统一领域模型
以下模型是 Scheduled Task、Goal Task 和实验共享的技术基础,不要求新增独立
Automation Center。`AutomationPlan` 是 Task 的计划配置,`Job` 是 Task 内执行单位,
`Run` 是执行尝试。用户主要通过 Task Center、左侧会话 Task 列表和关联 Conversation
理解工作;当前 UI 不展示 Job/Run 层级。智能心跳
不属于此模型。
### 5.1 核心实体
```text
Conversation
└─ Task 0..N
├─ AutomationPlan(可选)
│ ├─ TriggerPolicy
│ ├─ ObjectiveSet
│ ├─ ExecutionProtocol
│ ├─ BudgetPolicy
│ ├─ ApprovalPolicy
│ ├─ SupervisorPolicy
│ └─ MemoryBinding
└─ Job
├─ Run
├─ Subjob
├─ Observation
├─ SupervisorDecision
├─ Artifact
├─ Metric
└─ MemoryCandidate
```
| 实体 | 职责 |
| --- | --- |
| `Task` | 用户可见工作单位,只关联一条 Conversation;Conversation 可以承载多个 Task |
| `Job` | Task 内部一次步骤、触发、并行分支或委派执行 |
| `Run` | Task/Job 的一次执行尝试和审计记录 |
| `AutomationPlan` | Task 的可编辑计划配置,描述做什么、为何做、何时做和允许做什么 |
| `TriggerPolicy` | 手动、时间、事件或条件触发,以及错过执行策略 |
| `ObjectiveSet` | 成功标准、优化指标、约束和停止条件 |
| `ExecutionProtocol` | 本次运行冻结的提示、步骤模板、变量、Runtime、工具和数据范围 |
| `BudgetPolicy` | 最大耗时、模型调用、Token、工具次数、Job/Subjob 数、成果大小和并发 |
| `ApprovalPolicy` | 哪些动作可自动执行、哪些等待批准、哪些禁止 |
| `SupervisorPolicy` | 观察维度、触发频率、干预级别和确定性门禁 |
| `MemoryBinding` | 运行可读取和可写入哪些记忆分区 |
| `Observation` | 对消息、步骤、工具、指标或系统状态的结构化观察 |
| `SupervisorDecision` | `continue`、`comment`、`warn`、`request_review`、`pause` 或 `stop` |
| `Metric` | 可复现的运行指标及其计算来源 |
| `MemoryCandidate` | 尚未进入未来上下文的候选经验 |
### 5.2 自动化类型
`AutomationPlan.kind` 第一阶段使用有限枚举,而不是任意工作流:
| 类型 | 说明 |
| --- | --- |
| `scheduled_task` | 到点运行一个固定任务 |
| `goal_loop` | 围绕目标重复执行“观察、计划、行动、评估” |
| `experiment` | 生成隔离候选 Run,按统一协议评估和比较 |
会话监督不是独立 Task。用户选择 Conversation、Task 或 Experiment 作为监督对象;
`SupervisorPolicy` 可以在内部观察所属 Job/Run 事件,但当前 UI 不把它们作为独立目标。
### 5.3 运行快照
每次启动必须冻结:
- Plan 版本。
- 项目和工作目录。
- Runtime 和模型配置引用。
- 工作模式。
- 提示和变量。
- 工具、Skills、MCP 和知识库范围。
- 可读、可写记忆分区。
- 监督策略和评估器版本。
- 预算和并发限制。
- 审批策略。
运行开始后的设置变化只影响下一次 Run。用户可以在 Task 执行记录中查看当次快照与最新
Plan 的差异,但 Run 不作为独立导航对象。
## 6. 统一状态模型
### 6.1 Plan 状态
```text
draft → active ↔ paused → archived
```
- `draft`:未通过配置校验,不能自动触发。
- `active`:可以被触发。
- `paused`:保留定义和历史,不产生新 Run。
- `archived`:只读保留,不能恢复运行,复制后可继续使用。
### 6.2 Run 状态
```text
queued
→ running
→ waiting_approval
→ paused
→ evaluating
→ completed
任意活动状态
→ failed | cancelled | interrupted | budget_exceeded | superseded
```
规则:
- `completed` 只表示协议成功结束,不自动表示目标达成。
- `goalStatus` 独立为 `met`、`not_met`、`inconclusive` 或 `not_applicable`。
- 应用退出时活动 Run 标记为 `interrupted`,不自动重放有副作用步骤。
- `waiting_approval` 不占用 LLM 并发配额。
- 预算耗尽必须使用 `budget_exceeded`,不能伪装为普通失败。
### 6.3 Supervisor 状态
```text
inactive → observing → attention_required → paused → resolved
```
监督状态不覆盖 Run 状态。Run 可以仍在运行但存在 `attention_required`,也可以因确定性门禁
进入 `paused`。
## 7. 统一运行循环
### 7.1 调度与执行分离
```text
Trigger
→ AutomationCoordinator 在所属 Task 内声明 Job
→ ExecutionQueue 按优先级和预算排队
→ AutomationExecutor 为 Job 创建或恢复 Run
→ Runtime 执行
→ Supervisor 观察
→ Evaluator 计算指标
→ 结果、证据和候选记忆入库
→ 用户审查或后续执行
```
`AutomationCoordinator` 只负责触发、声明和恢复,不直接调用模型。执行仍通过 Job 和
Runtime 边界完成,用户可见结果通过所属 Task 汇入关联 Conversation。
### 7.2 优先级
默认优先级从高到低:
1. 用户正在等待的前台对话。
2. 用户手动启动的 Task 执行。
3. 等待批准后恢复的 Task 执行。
4. 到期 Scheduled Task 的执行。
5. 目标循环和实验执行。
6. 心跳回顾、记忆巩固和维护。
后台任务必须可被背压延后。延后记录为 `deferred`,不得丢失,也不得在系统恢复空闲时一次性
释放全部积压。
### 7.3 Conversation 输入仲裁
当前实现以 Main 和 SQLite 中的 `conversation_queue_items` 作为每条 Conversation 的权威
输入队列,而不是在 Renderer 分别维护聊天草稿队列和 Scheduled Task 队列:
- 普通消息在发送时冻结 Runtime、工作模式、专家/团队、知识范围和附件上下文,再以
`source=user` 入队;附件内容使用有界序列化保存,应用重启后仍可恢复。
- 到期或手动启动的 Scheduled Task 先建立 `schedule_run`,再以 `source=schedule` 进入同一
队列,只派发任务要求和关联会话身份。执行时读取会话最新历史和配置,不保存任务上下文快照。
- Main 对每条 Conversation 只保留一个活动请求或 Renderer 派发保留位。默认按 FIFO 认领;
Scheduled Task 不另设执行器或并发计数。
- 两种来源均由 Main 派发给 Renderer,建立普通用户消息和流式助手消息后调用 `agent.run`。
Renderer 加载会话及设置后才声明队列可派发。SSH 项目同样通过该入口选择远程 Runtime,
使用普通请求的上下文、工具范围、取消和恢复流程。
- `schedule_run.id` 作为普通请求 ID;普通请求终态同步该次计划运行与稳定产品 Task 的状态。
尚未接受的派发可释放重试;已经接受的运行在重启后按普通 Task 状态结算或恢复,不重新发送。
- 当前执行到达终态后再认领下一项。删除只移除尚未执行的项;“立即中断并插入”取消当前
请求并把所选项设为下一项,不重排其他项。
- 每条 Conversation 最多保留 20 个用户可提交的待执行项。启动时将未完成的派发恢复为
`pending`,但应用退出期间不会实际执行任务。
Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收用户队列项;Main 在接受
`agent.run` 时校验队列项仍处于 `dispatching` 且属于同一 Conversation,防止 Renderer
绕过顺序仲裁。
### 7.4 幂等和租约
- 每次计划触发使用 `planId + scheduledFor + planVersion` 形成幂等键。
- 手动触发使用调用方提供的单次幂等键。
- Run 和长步骤使用租约,租约过期后才能恢复或重试。
- 有外部副作用的步骤还需要工具级幂等键,无法确认结果时进入
`outcome_unknown`,不得自动重试。
- 同一个 Plan 可以限制最大活动 Run 数,默认 1。
## 8. 触发模型
### 8.1 支持顺序
| 阶段 | 触发类型 |
| --- | --- |
| 第一阶段 | 手动、单次、每日、每周、每月、受限 Cron |
| 第二阶段 | 应用启动、会话完成、任务完成或失败、文件同步完成、变量变化 |
| 后续 | 用户定义的组合条件和外部受信任事件 |
事件触发必须来自 Main 进程内的持久事件,不允许 Renderer 临时事件直接启动高影响自动化。
### 8.2 错过执行策略
| 策略 | 行为 |
| --- | --- |
| `skip` | 记录跳过,不补跑 |
| `run_once` | 无论错过多少次,只补一次 |
| `catch_up_bounded` | 在数量和时间窗口上限内补跑 |
默认:
- 日常摘要使用 `run_once`。
- 高频事件使用 `skip` 或事件去重。
- 不允许无限补跑。
## 9. 目标、协议和实验的关系
```text
目标:想得到什么结果
协议:用什么固定方法尝试
运行:协议的一次执行
实验:同一问题下多个隔离协议或变量组合的运行集合
监督:运行过程中独立判断是否偏离目标、违反约束或需要人工介入
记忆:运行可读的历史经验,以及运行结束后提出的候选经验
```
关键规则:
- 没有可计算或可审查成功标准的目标,不允许宣称“已完成目标”。
- 实验的最佳结果只在成功 Run 中选择。
- 全部 Run 失败时,实验状态为失败,不生成伪最佳结果。
- 模型生成的实验协议必须先由用户审查,或在只读、低成本模板中明确启用自动接受。
- 实验结果不能直接修改生产自动化,只能创建候选版本。
## 10. 监督边界
监督分为两层:
### 10.1 确定性监督
由代码执行,适合:
- 权限、目录和工具白名单。
- Token、耗时、并发和输出大小预算。
- JSON Schema、状态机和幂等约束。
- 明确的停止条件和指标阈值。
- 数据分区和跨范围访问。
确定性监督可以阻止、暂停或终止运行。
### 10.2 模型监督
适合:
- 目标偏移。
- 计划遗漏。
- 结论与证据不一致。
- 多个候选之间的定性差异。
- 用户可能需要澄清的歧义。
- 质量、表达和风险评论。
模型监督默认只评论或请求关注。它不能替代确定性安全边界,也不能自动批准高风险动作。
## 11. 记忆边界
### 11.1 计划读取链
运行只读取显式绑定的分区。推荐优先级:
```text
当前 Run
→ 当前 Automation
→ 当前 Conversation(如有关联)
→ 当前 Project
→ Global
```
每一层都有独立结果数和字符预算。低层记忆不能通过同名内容自动覆盖高层记忆,
冲突必须被标记并交给上下文组装器处理。
### 11.2 写入规则
- Run 只能直接写入自己的运行分区和候选区。
- 向 Automation、Project 或 Global 晋升需要评估或用户确认。
- 实验 Run 不能直接互相读取运行记忆。
- Supervisor 的判断保存为观察或候选,不自动变成事实。
- 被拒绝的候选保留摘要指纹,避免重复建议,同时不进入模型上下文。
## 12. 信息架构
当前阶段保留任务中心并适度完善,不新增独立自动化中心。智能心跳使用自己的菜单入口,
并已在现有模型上实现 Global / 多 Project 范围与唯一配置入口:
```text
任务中心
├─ 进行中
├─ 暂停
└─ 已结束
└─ 打开任务自身
任务自身
├─ 消息时间线
├─ Run、步骤与审批活动
├─ 监督、指标与证据
└─ 独立成果
智能心跳
├─ 运行概览
├─ 待处理建议
├─ 报告与记录
└─ 心跳计划
└─ Global / 指定 Project
```
监督统一进入应用级助手工作栏中固定且始终可访问的“监督”栏目,不再保留“独立可折叠右栏”
和“动态新增页签”两种实现。栏目默认跟随当前上下文,用户可以固定到其他 Conversation、
Task 或实验对象;详细范围与交互契约见
[通用助手工作栏与执行空间 PRD](../features/assistant-workbar/prd.md)。
## 13. 安全与隐私
1. Ask 在 Runtime 边界保持只读,而不只是提示词要求只读。
2. Execute 继续通过现有审批、主机执行策略、工具和目录控制。
3. 无人值守只允许用户显式批准的能力集合;遇到未预授权动作时进入等待审批。
4. Supervisor、Evaluator 和 Heartbeat 都把消息、工具输出、记忆和成果视为不可信数据。
5. 监督器不能读取隐藏推理,只能读取产品允许持久化和展示的事件。
6. 所有跨分区读取由 Main 根据绑定关系决定,Renderer 不能提交任意分区 ID。
7. 记忆和监督证据不得包含密钥、认证头、Cookie、完整私有文件或未经限制的工具输出。
8. 自动化产生的通知默认隐藏私人内容。
9. 应用退出时停止调度和新执行,持久化中断状态,释放 Runtime 和租约。
10. 清除项目时按外键和显式事务清理其计划、运行、运行分区、监督记录和候选,
不影响 Global 或其他项目。
## 14. 可观测性
每次 Task 执行的内部 Job/Run 记录至少保存:
- 触发来源和计划版本。
- 计划目标和当前 `goalStatus`。
- Runtime、工作模式和工作目录。
- 实际读取的知识库与记忆分区。
- 实际调用的模型、Token、工具、耗时和成果大小。
- 当前预算和剩余预算。
- Task、Job 和 Subjob 状态。
- Supervisor 评论、证据、严重度和处理结果。
- 评估器版本、指标和证据。
- 产生的候选记忆或学习产物。
- 重试、延后、中断和恢复原因。
UI 在 Task 下呈现上述信息的有界摘要和活动,不提供 Job/Run 树或独立导航。不得只显示一个
模糊的“自动化成功率”而隐藏失败、跳过或无结论的 Task 执行。
## 15. 建议的数据模型增量
以下为设计建议,字段在实现前仍需共享 Zod Schema 和 SQLite 迁移细化:
```text
automation_plans
automation_plan_versions
automation_triggers
automation_runs
automation_run_events
automation_metrics
automation_observations
supervisor_sessions
supervisor_decisions
memory_namespaces
memory_candidates
learning_artifacts
evaluation_cases
evaluation_results
experiments
experiment_variants
experiment_runs
```
现有 `schedules`、`schedule_runs`、`heartbeat_configs`、`heartbeat_runs`、
`heartbeat_entries`、`tasks` 和 `runs` 不应一次性重写。Schedule 可渐进建立稳定 Task 与
Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心跳数据保持独立,不得
静默转成 `AutomationPlan` 或顶层 Task。分区记忆协同和唤起条件完成设计前,不新增迁移目标。
## 16. 分阶段实施
### 阶段 0:统一术语和可观测性
- 固定 Task、Conversation、Job、Subjob、Run、Plan、Goal、Protocol、Supervisor、
Observation、Memory Candidate 等概念。
- 明确 Task N:1 Conversation 关系、左侧行首展开按钮与 Task 子项图标,以及 Task Center
索引边界,不复制内容。
- 为现有 Scheduled Task 和专家执行建立按 Task 聚合的活动视图。
- 明确当前心跳保持独立,分区记忆协同与唤起条件尚待设计。
- 补充触发来源、运行版本、预算和读写范围展示。
### 阶段 1:调度与运行基础
- 统一 Run 声明、幂等、租约、恢复和错过执行策略。
- 增加月度和受限 Cron。
- 增加后台优先级与并发预算。
- 保持现有任务执行器不变。
### 阶段 2:会话监督与分区记忆
- 上线评论型会话监督。
- 智能心跳配置支持 Global 或指定一个、多个 Project。
- 增加 Automation 和 Run 记忆分区。
- 建立来源、证据、时态、冲突和晋升流程。
### 阶段 3:目标任务
- 增加目标、成功标准、约束、预算和停止条件。
- 支持有界的观察、计划、行动、评估循环。
- 默认 Ask 或需要逐步审批的 Execute。
### 阶段 4:并行实验
- 变量和运行隔离。
- 候选、重复、指标、证据、失败结算和最佳结果选择。
- 复用现有任务和受限子专家并发。
### 阶段 5:持续学习
- 先建立回放集和评估门。
- 再增加候选、Shadow、晋升、监控、衰减和回滚。
- 初期只晋升记忆和自动化模板,不自动改变安全策略。
## 17. 相关文档
- [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md)
- [Task Center PRD](../features/task-and-job/task-center-prd.md)
- [Scheduled Task PRD](../features/task-and-job/scheduled-task-prd.md)
- [Job 与 Subjob PRD](../features/task-and-job/job-and-subjob-prd.md)
- [智能心跳 PRD](../features/smart-heartbeat/prd.md)
- [并行实验工作台 PRD](../features/parallel-experiments/prd.md)
- [会话监督 PRD](../features/conversation-supervision/prd.md)
- [分区记忆 PRD](../features/memory/prd.md)
- [持续学习与评估门 PRD](../features/continuous-learning/prd.md)
- [GoodBuddy 长期助手功能规划](../roadmap/long-term-assistant-roadmap.md)
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
## 18. 总体验收标准
- [ ] 智能心跳保持独立,不作为 Task 类型;分区记忆协同与唤起条件尚未设计。
- [ ] Task Center 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以
关联多个 Task。
- [ ] 当前 UI 只展示到 Task,不提供 Job/Subjob/Run 树或独立导航。
- [ ] Scheduled Task 的重复触发和并行 Job 不创建新的顶层 Task。
- [ ] 每次自动执行的内部 Run 都记录触发原因、目标、范围、预算、状态和结果,并在所属
Task 下提供有界可观测信息。
- [ ] Ask 自动化无法调用写工具或产生外部副作用。
- [ ] Execute 自动化不能绕过现有审批、主机执行策略和能力控制。
- [ ] 会话监督默认只评论,不能替用户发言或批准工具。
- [ ] 并行 Run 的变量、会话、运行记忆、任务和成果相互隔离。
- [ ] 失败 Run 不参与最佳结果选择,全部失败不报告成功。
- [ ] 记忆跨分区读取必须显式授权并可审计。
- [ ] 候选经验在评估门和回滚能力完成前不能自动改变未来行为。
- [ ] 应用重启后状态可恢复,但不会自动重放结果未知的副作用步骤。
@@ -0,0 +1,101 @@
# 本机专用推理架构与任务控制评估
## 记录范围与状态
评估日期:2026-09-18。本文记录 ASR、OCR、内置 embedding 的执行边界、任务控制缺口和后续优化依据,供语音输入、文档解析、知识检索与索引共同使用。
现状依据本次工作区源码核对,包含尚未提交的变更,不等同于已发布版本或实机验收结论。本次仅整理文档,未运行引擎性能测试、复现故障或实施迁移。下文“建议”是评估方向,“待办”均未在本次实施,也没有约定交付日期。
产品 Task、Job、Run 的定义沿用 [Task 与 Job 领域模型](../features/task-and-job/task-and-job-model.md)。本文的“推理任务”指一次引擎请求及其执行记录,不自动等同于产品 Task,不据此新增产品对象、持久化日志或全局调度器。本机推理管理的现有界面和接入清单见[应用导航技术设计](../features/application-tool-navigation/technical-design.md#111-本机推理任务与执行服务);本文负责跨功能架构评估及优化待办。
## 当前执行边界
| 能力 | 源码现状 | 所有权与释放 | 评估建议 |
| --- | --- | --- | --- |
| ASR | 录音结果就绪后提交完整 PCM;Main 创建 `node:worker_threads` Worker,以 sherpa-onnx WASM 执行离线识别;服务一次只接受一个请求 | 每请求创建识别器;超时或 abort 调用并等待 `worker.terminate()`;正常结果路径的清理确认存在缺口,见 TC-4 | 保留请求级 Worker,适合间歇语音输入;常驻化需流式需求或测量支持 |
| OCR | Main 排队后发往主窗口 Renderer;Renderer 复用 Web Worker,运行 `ppu-paddle-ocr/web` 与 `onnxruntime-web` WASM;文档串行处理,PDF 逐页处理 | Renderer 模块持有 Worker;活动请求取消或单页超时会终止 Worker;无活动和排队请求时空闲 60 秒释放 | 当前保留 Web Worker;独立于 UI 的持续任务可评估专用后台 Renderer 加 Worker |
| 内置 embedding | Main 按模型目录共享引用计数 Broker;transport 使用 `utilityProcess.fork`,Granite tokenizer 和 ONNX session 首次请求懒加载并复用 | 多使用方共享进程;最后一个 provider 释放时 shutdown;也支持显式启停,停止等待 child exit | 保留共享 utilityProcess;优先核对取消后的容量回收与加载失败重试 |
### ASR 的证据与边界
[App.tsx](../../src/renderer/src/App.tsx) 在 `recording.result` 完成后调用 `speech.transcribe`;[录音模块](../../src/renderer/src/speech-recognition.ts)负责采样与重采样。[SpeechTranscriptionService](../../src/main/speech/speech-transcription-service.ts) 的 `active.size` 拒绝并发识别,`runSherpaTranscription` 为每次请求创建 Worker,转移音频缓冲区,使用 `createOfflineRecognizer` 同步 decode。Worker 创建后的识别超时为 120 秒,不包含此前模型解析的等待时间。
[package.json](../../package.json) 固定 `sherpa-onnx` 为 1.13.4;本次同时核对已安装包的 `index.js`,其入口加载 `sherpa-onnx-wasm-nodejs.js`。因此这里记录的是 Node Worker 内的 WASM 路径。Node Worker 与 Main 共享 OS 进程,线程隔离不能视为独立进程故障边界。
Worker 先 `postMessage`,再在 `finally` 中调用 `stream.free()` 和 `recognizer.free()`。Main 收到结果后以 `finish(..., false)` 结算、移除监听和超时,不等待 Worker exit;随后 `active` 删除请求,推理记录转为终态。这证明结果完成与资源清理确认之间存在记账间隙,但尚未证明实际泄漏或清理耗时。
### OCR 的浏览器依赖
[OCR Worker](../../src/renderer/src/document-ocr-worker.ts) 显式选择 WASM provider、`canvas-native` 图像处理,当前 `ort.env.wasm.numThreads = 1`。PDF 路径使用 `pdfjs-dist`、`OffscreenCanvas` 和 `convertToBlob`。本次核对的依赖链为 `ppu-paddle-ocr/web/platform.web.js` 到 `ppu-ocv/canvas-web`;后者的 `platform/web.js` 使用 `createImageBitmap` 解码图片。包依赖以 [package-lock.json](../../package-lock.json) 为准,依赖升级后需重新核对此结论。
这些能力与浏览器执行环境有关。直接迁到 Node utilityProcess,需要另行验证图像解码、Canvas、PDF 渲染及打包资源路径,不能只替换进程创建 API 就宣称等价。
[DocumentOcrBroker](../../src/main/document-ocr-broker.ts) 最多保留 4 个 pending 请求,一次派发一个;总超时从派发时开始,不覆盖 Main 排队等待。其预算为 `min(10 分钟, max(单页预算, 60 秒 + 单页预算 × 页数))`。[Renderer bridge](../../src/renderer/src/document-ocr-bridge.ts) 另有串行 Promise 队列,Worker 就绪超时为 120 秒,就绪后按单页预算计时,并在进度消息到达时重置单页计时器。Main 的 60 秒启动预算与 Renderer 的 120 秒就绪等待未对齐,短文档冷启动时 Main 有机会先判超时。
[Main 初始化](../../src/main/index.ts) 将 Broker 绑定到 `mainWindow`;[Renderer 入口](../../src/renderer/src/main.tsx) 安装 bridge。Broker 派发前仅检查窗口销毁等条件,没有等待 bridge 就绪的握手,也没有在该派发链路中处理 bridge 就绪丢失。Renderer 内的 Worker `ready` 只确认模型初始化,不能证明 Main 发请求时 Renderer 已安装 IPC 监听。窗口重载、Renderer 崩溃及启动时监听尚未安装的影响应按 TC-3 复现。
来源请求可以取消 OCR;已派发请求取消时 Main 发送取消消息并立即结算,然后派发下一项,未等待 Renderer 清理确认。Renderer 对当前活动请求的取消会终止共享 Worker。管理页因此没有将该操作作为安全的单任务取消能力;手动释放也会拒绝活动或排队请求。
### Embedding 的共享执行与取消
[Main provider 工厂](../../src/main/index.ts) 按模型目录维护 `sharedEmbeddingBrokers` 及使用计数;[transport](../../src/main/knowledge/embedding-utility-transport.ts) 以 spawn 确认进程启动。[Granite 引擎](../../src/main/knowledge/granite-embedding-engine.ts) 缓存 `loadTask`,在首次请求时创建 tokenizer 和 WASM ONNX session。进程已启动不等于模型已加载;当前没有为模型加载完成提供独立服务状态。
[Broker](../../src/main/knowledge/embedding-inference-broker.ts) 默认请求超时为 120 秒,计时在 transport 就绪后建立 pending 时开始。调用方取消或超时会先返回错误,同时发送 cancel,保留 pending 和推理执行记录,等待结果、`CANCELLED` 或进程退出。保留容量避免把仍在计算的任务当作已释放。
[执行端](../../src/main/knowledge/embedding-inference-worker.ts) 收到 cancel 后设置 AbortSignal;Granite 在加载、输入构造和 `session.run` 前后检查取消,但没有将可抢占的取消句柄传入 `session.run`。若该调用一直不返回,取消检查和确认就无法完成。Broker 的 `cancelPending` 会清除原计时器,当前没有针对该 pending 的后续有界清理升级,因此持续挂起可一直占用在途容量。此为源码可达的条件风险,真实引擎挂起尚未复现。
`loadTask ??= this.loadFiles(...)` 也会缓存被拒绝的加载 Promise;同一引擎实例后续请求复用同一个失败,不能通过再次请求重新加载。重建进程会建立新引擎;是否增加实例内重试及其清理方式,按 TC-2 验证后确定。
## 架构建议与触发条件
ASR 继续按录音请求创建 Worker。对低频、间歇输入,每次释放模型能避免长期占用;本次没有冷启动与常驻成本数据,不能量化收益。只有连续流式识别成为真实需求,或高频请求测出重复加载已影响目标延迟,才评估常驻 utilityProcess。届时需比较加载成本、暖请求延迟、空闲工作集和取消时延,并重新设计音频流与识别器复用;单纯常驻不会把当前离线 API 变成流式识别。
OCR 保留 Web Worker。如果文档任务需要在主 UI 重载或不可用时继续执行,优先评估由 Main 持有的专用后台 Renderer 加 Worker,以保留浏览器图像/PDF 能力。该方案仍是候选,需要完成以下设计与实机验证后才可决定迁移:
- 保持现有 sandbox、context isolation、禁用 Node integration 的边界;通过专用 preload 暴露必要方法,并沿用可信 sender 与输入校验。当前 IPC 若限定主窗口 sender,需要明确新增宿主的受信身份及响应归属。
- 分别确认后台 Renderer 的 IPC 监听就绪与 Worker 模型就绪;定义重载、崩溃、启动失败及应用退出时 pending、队列和模型的处置,防止静默丢请求或自动重放。
- Main 持有后台宿主生命周期;管理 UI 关闭不销毁执行宿主,服务释放和应用退出才按约定清理。验证隐藏 Renderer 的调度、计时器和 Worker 执行,不根据窗口不可见推断运行不受影响。
- 用实际 OS PID、Electron 进程指标和崩溃影响验证隔离程度。单独的 BrowserWindow/WebContents 不能直接作为独立 OS 进程或独立服务资源统计的证据;若与其他 Renderer 共享 PID,必须继续报告共享范围。
Embedding 保留共享 utilityProcess。查询与索引共享模型和进程的现有方式具备复用基础,优化先处理取消回收和失败重试。单请求取消继续尽量只影响该请求;进程终止属于服务级操作,必须说明其他使用方的结果。不得把每次请求超时直接等同于可以无条件杀死共享进程。
## 任务控制覆盖与缺口
[共享契约](../../src/shared/local-inference-contracts.ts)将服务状态与任务状态分开;[LocalInferenceService](../../src/main/local-inference-service.ts) 在 Main 内存登记请求,保存活动项及最近 100 条终态记录。任务的 `running` 当前包含排队、模型加载与执行,未提供统一细分阶段。该登记器是观测和控制入口,各引擎仍负责实际队列与清理。
| 控制维度 | 当前覆盖 | 后续评估和验收要求 |
| --- | --- | --- |
| 状态 | 任务有 `running/cancelling/completed/cancelled/failed`;服务另有启动、运行、停止、未知和错误状态;ASR 模型可用时服务仍为 unknown | 区分调用方已返回、执行仍活动、资源清理已确认;进程 spawn、模型 ready、请求完成不能互相替代。先增加必要证据,不预建完整阶段状态机 |
| 所有权 | ASR 请求持有 Worker;OCR Renderer 持有共享 Worker;embedding 按目录共享 Broker 并引用计数 | 控制记录注明真实来源与宿主;缺失项目、会话关联时不猜测归属。产品 Task 的取消不能凭名称去停止整个推理服务 |
| 取消 | ASR terminate;OCR 来源取消会终止当前 Worker;embedding 协作取消并保留执行记录 | 验证取消与自然完成竞态、重复取消及清理结果。单请求取消与共享服务停止分别定义影响,不用 UI 隐藏代替取消 |
| 超时与容量 | ASR Worker 120 秒;OCR 两端预算与排队语义不同;embedding 默认请求 120 秒且保留取消后容量 | 分别说明排队、启动、推理、清理的计时起点和上限;调用方超时后仍在执行的工作需要有界处置,不得提前释放容量制造超额并发 |
| 停止与清理 | 服务操作按 serviceId 串行,期间拒绝新增请求;停止校验当前活动 taskId 确认清单;向量停止等待 exit,重启不重放请求 | 清理失败保持可观察的活动/错误信息;窗口或宿主丢失需使受影响请求得到明确结果;ASR 和 OCR 的结果/清理间隙分别核对 |
| 可观测性 | 有来源、状态、时间、可选模型和错误;向量有 PID 资源数据;管理 Modal 不显示任务历史 | 测量排队、加载、执行、取消请求到执行结束、清理确认的耗时与在途数;日志不记录音频、图片或向量原文,未知指标不补零 |
本文的覆盖要求用于后续问题复现和最小修复,不构成引入统一常驻服务框架、持久化执行副本或自动恢复协议的决定。范围为桌面本机引擎;远程 Agent、外部向量端点和未接入的 TTS 不因此获得同样控制能力。
## 资源观测口径
[LocalInferencePage](../../src/renderer/src/LocalInferencePage.tsx) 在启用、文档可见且 Modal 内容未被 hidden/inert 隔离时每 5 秒读取快照,跳过重叠请求,关闭时清理轮询。该刷新间隔不能用于证明短请求的峰值或清理完成时刻。
[向量资源采样器](../../src/main/inference-process-resources.ts) 按 transport 当前 PID 匹配 `app.getAppMetrics()` 的 Utility 条目,用累计 CPU 秒差除以单调时间间隔。100% 表示一个逻辑核心,可超过 100%;首次采样、PID/创建时间变化、间隔超过 15 秒或读数异常时重建基线。工作集从 KiB 转为字节,UI 显示 MiB。
向量工作集包含运行时、模型、临时缓冲区及并发使用方,不能当作模型权重大小或单任务内存。ASR Node Worker 和 OCR Web Worker 没有当前可独立归属的 OS 进程工作集;宿主进程总量不能直接填入某个服务行。未来 OCR 后台宿主也必须先核实 PID 与共享情况,再决定指标范围。显存当前未采集。
## 优化待办与验收
优先级表示建议处理顺序:P1 为执行正确性和回收问题,P2 为测量及有条件的架构选型。全部待办待开展;源码核对已完成,故障复现、修复和实机验收分别记录,不以文档完成替代实现完成。
| ID | 优先级与事项 | 依赖及验收条件 |
| --- | --- | --- |
| TC-1 | P1:embedding 取消/超时后的有界回收 | 先覆盖 `session.run` 延迟返回及不返回,观察 caller、pending、执行记录与容量。定义取消后的清理等待上限及升级条件,数值依据测量确定。普通取消不终止其他共享任务;确需服务级终止时明确受影响使用方及失败结果,等实际退出再释放容量,且不重放请求。模拟用于稳定触发条件,最终以真实引擎取消和停止路径补验 |
| TC-2 | P1:embedding 加载失败重试 | 覆盖首次读模型或建 session 失败、随后恢复依赖的场景;选择清除失败缓存或显式重建引擎的最小方案。验收后续请求能够重新加载,并发请求不重复加载,失败中已创建的资源被清理,销毁后不再复活;不能把一次成功创建进程当作重试通过 |
| TC-3 | P1:OCR 宿主就绪、丢失与超时预算 | 保留当前 Web Worker 即可先验证,无须等待迁移。覆盖监听安装前派发、主 Renderer 重载/崩溃、冷启动超过 Main 启动预算及排队取消。明确 bridge 就绪与 Worker ready 的区别;受影响请求有明确结果,队列能继续使用,没有静默丢失或重放;两端预算、起算点和取消后下一项派发与清理顺序一致 |
| TC-4 | P1:ASR 结果与清理记账 | 覆盖结果先到、延迟退出、清理异常以及取消/超时与结果竞争。选择延后请求结算或单独保留清理记录的最小方案;验收资源仍活动时不会被报告为已释放,也不提前放开实际单请求容量。正常完成及异常路径均验证 Worker 最终退出 |
| TC-5 | P2:统一测量记录与资源归属核验 | 可与 P1 复现并行,为 TC-6/TC-7 提供依据。记录冷/暖加载时间、端到端延迟、排队时长、取消至执行结束/资源回收时长、空闲与峰值工作集、CPU 时间和样本数;按 PID 对照 OS 工具,分别报告引擎、宿主与应用总量。重启后不沿用旧 PID 基线,不以 5 秒 UI 轮询代替专门测量 |
| TC-6 | P2、条件待定:OCR 后台 Renderer 加 Worker | 依赖明确的 UI 独立执行需求、TC-3 生命周期结论及 TC-5 数据。候选验证需包含图片、多页 PDF、UI 重载时执行、后台宿主崩溃、空闲释放、应用退出、preload/IPC sender 边界和各支持平台打包资源。证实任务连续性及实际 PID 隔离后,再记录采用或保留现状的结论 |
| TC-7 | P2、条件待定:ASR 常驻 utilityProcess | 依赖流式/高频场景成立,或 TC-5 证明重复加载达到需优化程度,并完成 TC-4。对比请求级 Worker 与候选的冷启动、暖请求、空闲内存和取消成本;若为流式,验证连续音频、结果提交和停止语义。没有测量或实际需求时继续保留请求级 Worker |
测量记录应包含 OS/架构、Electron 与依赖版本、模型版本、硬件、输入时长/页数/token 与批量大小、冷暖条件、请求频率和并发使用方。使用固定可重复输入,报告样本数、分布及异常,不填入未经测量的性能收益。后续验收覆盖受影响的 Windows、macOS、Linux 构建;尚未测的平台明确保留为待验。
本次证据为源码和已安装依赖入口检查。后续执行待办时,在所属功能进度文档记录命令、环境、真实路径结果及遗留项,并引用 TC 编号;若迁移条件未成立,记录保留现状的原因即可。
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
# Runtime Storage And Background Reads
SQLite owns persisted task, event and conversation evidence. The desktop business
connection is long-lived. WAL permits readers alongside a writer; synchronous SQL
still blocks the thread that executes it. Memory caches are disposable derived data.
Startup storage upgrades run in `assistant-storage-worker` before the business
connection opens. Runtime readers must not invoke initialization, migrations or
recovery. `SubagentProgressStorage` caches event encoding state, not task statistics.
## Runtime Read Design
Execution statistics use one lazily started worker with a read-only connection.
The worker performs the existing evidence query inside a short read transaction.
Main supplies the current reply lease IDs and receives only scalar summaries.
There is no worker pool or duplicate persisted summary. The worker is terminated
when its owning database closes; errors reject reads, without a synchronous Main
fallback. In-memory test databases use the synchronous query because independent
connections cannot access the same private in-memory database.
Derived caches must specify scope, capacity, lifetime and invalidation. Statistics
keep at most eight scopes for 30 seconds. SQLite `data_version` detects other
connections' commits; `total_changes()` detects writes on the owning connection.
Reads inside an existing transaction clear and bypass the cache, so rolled-back
values cannot escape. Closing the connection clears its cache. Live leases bypass
the cache because elapsed time changes without a database write.
Read transactions end before replies are posted. Long-lived read transactions
would prevent WAL checkpoints from reclaiming old pages. Each reader has its own
SQLite page cache; adding workers increases memory usage and requires evidence of
parallel query demand. New consumers should reuse the same lifecycle and read-only
rules, without introducing a general query protocol before a second use case exists.
## Renderer Scheduling
Hidden task panels use React `Activity` to preserve state while scheduling hidden
render work below visible updates. Panel factories run inside that boundary.
Terminal and browser panels retain their existing mounted effects and sessions.
Statistics refresh only when the task center and document are visible. Results
must remain scoped and late responses from a previous selection must be discarded.
The [task statistics design](../features/task-and-job/technical-design.md) owns the
query contract and evidence rules. Tests and timings demonstrate only their fixture
conditions; they do not establish a latency guarantee for arbitrary retained history.
@@ -0,0 +1,497 @@
# GoodBuddy 设备共享与 ShareServer 联邦网络总体设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-09 |
| 适用范围 | GoodBuddy 桌面端、ShareServer、网关间联邦协议 |
| 桌面端设计 | [设备发现与共享](../features/device-sharing/README.md) |
| 服务端设计 | [ShareServer](../../../goodbuddy-shareserver/docs/features/share-server/README.md) |
## 1. 文档职责
本文是设备共享网络的跨端权威设计,负责定义拓扑、术语、身份、能力清单、授权计算、任务
路由、能力分发、协议边界和共同验收标准。桌面端的产品与界面行为由“设备发现与共享”功能
文档负责,独立 Web 服务由 ShareServer 功能文档负责。
本文描述完整目标系统,不以交付阶段删减领域模型,也不把尚未实现的设计表述为当前能力。
## 2. 核心决策
### 2.1 两个独立产品面
1. **GoodBuddy Desktop** 在设置中提供“设备发现与共享”,默认使用局域网发现与直连,也可
由用户额外连接一个或多个 ShareServer。
2. **ShareServer** 是独立部署、独立升级的 Web 服务,提供组织、设备、能力、权限、更新、
联邦、中继、Office 编辑和审计管理。它不运行在 Electron Main 中,也不由桌面端代为启动。
### 2.2 能力优先,来源可见
用户日常选择的是能力,不是网络连接。设备、ShareServer 和组织作为能力来源与诊断入口。
任何调用都必须持续显示提供者、执行位置和数据去向,不能为了统一体验隐藏跨设备或跨组织
边界。
### 2.3 局域网是默认连接方式,网关是显式附加连接
- 桌面端没有网关配置时,局域网发现、配对、调用和本机共享仍可完整工作。
- 添加、删除或断开 ShareServer 不改变局域网配对关系。
- ShareServer 不接管本机局域网发现,也不能静默把局域网设备注册到组织。
- 同一桌面端可以连接多个 ShareServer;能力来源和授权按连接分别计算。
### 2.4 发现、被发现和开放能力是三个独立动作
- **发现设备**:浏览同一局域网中主动广播的 GoodBuddy 服务。
- **允许被发现**:广播本设备存在及配对入口。
- **开放能力**:向指定设备、用户组或组织发布可查看、调用或拉取的能力。
开启其中一项不能推导另外两项已开启。新安装默认不开放能力。
### 2.5 ShareServer 优先作为控制面
ShareServer 负责身份、目录、策略、任务路由、更新和审计。数据面优先在两个已认证端点之间
直接建立;网络条件不允许时,可按组织策略使用 ShareServer 中继。是否允许中继、正文是否
端到端加密、是否允许跨组织,由本次任务冻结的策略共同决定。
### 2.6 Office 编辑是 ShareServer 托管服务
ShareServer 可以把 ONLYOFFICE Docs、工作副本存储、保存回调和编辑会话作为一个可选部署
能力提供给 Desktop。Desktop 只连接用户已明确配置的 ShareServer,不直接配置 Document
Server,也不在本机启动服务。Office 文档内容会由 ShareServer 和编辑引擎处理,因此不能沿用
“控制面默认不读取正文”的数据面假设;创建编辑会话前必须明确显示服务端、数据去向和保留期。
编辑后的本地或 SSH 源文件仍由 Desktop 或远程 GoodBuddy Agent 持有和原子回写。ShareServer
是工作副本及服务端保存检查点的权威,不是源文件最终写入成功的权威。完整设计见
[Office 协同编辑](../features/office-document-editing/README.md)。
## 3. 术语
| 术语 | 定义 |
| --- | --- |
| `Device` | 安装 GoodBuddy 并拥有稳定设备身份的终端 |
| `Peer` | 与当前设备存在局域网配对关系的另一台设备 |
| `ShareServer` | 提供组织控制面和可选数据中继的独立服务 |
| `Organization` | ShareServer 中隔离成员、设备、能力与策略的管理范围 |
| `GatewayConnection` | 某台设备与一个 ShareServer 上特定组织身份的连接 |
| `Capability` | 可被查看、调用或分发的稳定能力标识 |
| `CapabilityProvider` | 实际持有并执行某项能力的设备或服务节点 |
| `Publication` | 能力提供者向某一范围发布的可见性和使用声明 |
| `Grant` | 主体对能力执行某类动作的明确授权 |
| `Task` | 一次远程能力调用及其输入、执行、输出和终态 |
| `Package` | 可下载到本机安装的签名能力工件 |
| `FederationTrust` | 两个组织网关之间经管理员确认的双边信任关系 |
| `DocumentEditSession` | ShareServer 托管的单文档短期编辑会话、工作副本和保存检查点 |
“网关”是 ShareServer 的网络角色;产品名称为 `ShareServer`,独立仓库为
`goodbuddy-shareserver`。
## 4. 系统拓扑
```text
局域网:
GoodBuddy Desktop A <-- mDNS 发现 + 配对后 TLS 直连 --> GoodBuddy Desktop B
单组织:
GoodBuddy Desktop A -- 出站连接 --> ShareServer <-- 出站连接 -- Provider Device B
Office 编辑:
GoodBuddy Desktop --> ShareServer --> ONLYOFFICE Docs
└-------> 临时工作副本与保存检查点
跨组织:
Desktop A --> ShareServer A <== 联邦信任 ==> ShareServer B <-- Provider Device B
```
局域网和网关连接共享能力、授权和任务的领域语义,但不共享发现方式:
| 维度 | 局域网 | ShareServer |
| --- | --- | --- |
| 发现来源 | mDNS/DNS-SD 主动广播 | 已配置 URL 或邀请链接 |
| 初始身份 | 双端验证码确认设备指纹 | 用户登录、设备注册和组织成员身份 |
| 能力目录 | 已配对设备直接返回 | 服务端按组织策略过滤 |
| 连接方向 | 配对后直连 | 设备主动建立出站连接 |
| 管理范围 | 单个设备间授权 | 用户组、组织和联邦授权 |
| 审计权威 | 两端各自本地记录 | 组织网关记录元数据,两端保留本地记录 |
## 5. 组件边界
### 5.1 GoodBuddy Desktop
- 浏览和广播局域网服务。
- 创建、确认和撤销点对点配对。
- 连接 ShareServer,保存脱敏连接信息和本机受保护凭据。
- 发布用户明确选择的本机能力。
- 展示经当前身份过滤的能力目录。
- 在调用前冻结提供者、输入范围、授权和路由。
- 执行本机提供的任务,或消费远端任务结果。
- 校验并安装用户明确选择的签名能力包。
- 保存本机活动记录和可撤销授权。
### 5.2 ShareServer
- 认证用户、设备、服务节点和其他 ShareServer。
- 管理组织、成员、用户组和设备目录。
- 接收能力发布并生成权限过滤后的目录。
- 计算组织强制策略,签发短期任务授权。
- 路由任务、维护在线状态,并在允许时中继数据。
- 管理签名包、发布通道、更新策略和撤销。
- 可选管理 Office 编辑会话、ONLYOFFICE 接入、工作副本、保存回调、保留和清理。
- 建立双边联邦关系并限制跨组织可见性与调用。
- 保存有界审计元数据、用量和管理操作。
### 5.3 能力提供者
- 只执行已发布且本次授权允许的能力。
- 在执行边界重新验证任务令牌、输入 schema、大小、期限和调用方。
- 使用自身 Runtime、账号、工作区和数据权限。
- 返回有界进度、结果、成果或确定终态。
- 不因收到任务令牌而获得其他能力或任意系统接口。
## 6. 身份与信任
### 6.1 设备身份
每次 GoodBuddy 安装生成稳定设备密钥和自签设备证书,私钥保存在系统安全存储中。对外只
使用随机设备 ID、公钥指纹、用户设置的设备名称和必要协议版本。操作系统用户名、绝对路径、
软件资产和能力详情不进入未认证发现广播。
重装或主动重置设备身份会产生新设备。旧局域网配对和 ShareServer 设备注册不会自动迁移。
### 6.2 局域网配对
1. 发起端选择一台主动广播的设备。
2. 双方建立临时加密通道并计算共同短验证码。
3. 两端同时显示设备名、系统类型、公钥指纹和验证码。
4. 双方用户确认后保存对端公钥及配对 ID。
5. 后续使用双方设备证书认证;任一端可以撤销配对。
IP 地址不作为设备身份,也不单独构成授权依据。
### 6.3 ShareServer 注册
用户通过浏览器/OIDC 或服务端支持的登录方式认证后,为当前设备申请一次性注册挑战。设备
使用自身私钥签名挑战,ShareServer 建立 `user + organization + device` 绑定并签发可轮换的
设备凭据。设备凭据不等同于用户 Web 会话,不可用于管理控制台登录。
### 6.4 联邦身份
两个 ShareServer 由双方管理员交换组织标识、网关公钥、公开端点和指纹,并分别确认信任。
联邦关系只有在双方状态均为 `active` 时生效。单方暂停或撤销后立即停止新目录交换和新任务,
已执行任务保留审计终态。
## 7. 能力模型
### 7.1 能力清单
```json
{
"schemaVersion": "1.0",
"capabilityId": "document.convert.docx",
"name": "文档转 Word",
"version": "1.4.0",
"modes": ["remote", "package"],
"inputSchema": {},
"outputSchema": {},
"permissions": ["receive_text", "receive_files", "return_files"],
"limits": {
"maxInputBytes": 10485760,
"maxOutputBytes": 52428800,
"maxDurationSeconds": 900,
"maxConcurrentTasks": 2
},
"dataPolicy": {
"retention": "task_lifetime",
"networkAccess": false,
"delegation": "none"
}
}
```
清单只描述能力事实,不授予任何主体访问权。`inputSchema` 和 `outputSchema` 使用受限 JSON
Schema 子集;实现必须限制 schema 深度、属性数、枚举数和总字节,不能把清单当作可执行代码。
### 7.2 发布声明
`Publication` 将一个能力版本发布到以下一种范围:
- 指定局域网 Peer。
- 所有已配对 Peer。
- ShareServer 内指定用户、用户组或设备组。
- 指定联邦组织中的用户组。
发布声明同时定义允许动作:`discover`、`inspect`、`invoke`、`download`。`inspect` 允许查看
详细清单,`discover` 只允许看到名称和可用状态。
### 7.3 可调用与可分发
- `remote`:代码与受控数据留在提供方,调用方只发送输入并接收结果。
- `package`:签名工件下载到调用方,经校验和用户/组织策略批准后安装。
同一能力可以同时支持两种模式,但两种模式分别授权。远程调用授权不能推导下载权,下载权
也不能推导安装后自动启用。
## 8. 授权模型
### 8.1 授权输入
一次动作的最终决定取以下约束交集:
```text
调用方用户权限
∩ 调用方设备状态
∩ 调用方组织强制策略
∩ 发布范围
∩ 提供方授权
∩ 提供方组织强制策略
∩ 能力输入与数据策略
∩ 联邦关系限制(跨组织时)
= allow | require_approval | deny
```
`deny` 优先于 `require_approval`,`require_approval` 优先于 `allow`。客户端偏好只能在组织强制
策略允许的范围内收紧权限,不能放宽权限。
### 8.2 Grant
Grant 至少绑定:
- 主体:用户、用户组、设备、设备组或外部组织。
- 能力 ID 和允许的版本范围。
- 动作:查看、调用、下载、安装或发布。
- 输入:文本、文件、数据分类、单项和累计大小。
- 提供者和执行位置限制。
- 次数、并发、预算、时间段和有效期。
- 是否需要每次批准。
- 是否允许委托及明确的下游组织。
“始终允许”只创建与当前能力、提供者、输入类型和范围匹配的 Grant,不创建整台设备的任意
访问权。
### 8.3 转委托
默认 `delegation=none`。允许委托时,任务授权中必须冻结完整执行链和最大跳数。任一新组织、
新能力或新数据接收方加入执行链,都需要重新计算授权,不能沿用原任务令牌。
## 9. 任务协议
### 9.1 任务快照
创建任务时冻结:
- 调用方用户、设备和组织。
- 能力 ID、版本和提供者。
- 输入 schema 版本、数据类型、数据分类和字节数。
- 路由:局域网直连、网关直连、网关中继或联邦路径。
- 授权决定、批准记录和策略版本。
- 委托链、预算、期限和幂等键。
设置或策略后续变化不追改已接受任务;紧急撤销可以阻止尚未开始的任务,并按策略取消活动
任务。
### 9.2 状态
```text
created -> awaiting_approval -> queued -> connecting -> running
running -> completed | failed | cancelled | outcome_unknown
created/awaiting_approval/queued/connecting -> rejected | expired | cancelled
```
- `completed` 只在提供者提交完整终态且调用方确认接收后成立。
- 网络中断后无法确认副作用结果时使用 `outcome_unknown`,不得自动重放。
- 重试创建新 attempt,并复用仅在能力声明支持时才有效的幂等键。
- 用户取消必须传播到实际提供者;未确认取消时不能声称任务已停止。
### 9.3 数据传输
- 小型 JSON 输入使用任务通道传输。
- 文件使用内容 SHA-256、声明大小和一次性任务对象令牌传输。
- 接收端边读边校验大小和摘要,拒绝路径、符号链接和调用方文件名决定的落盘位置。
- 任务正文、文件和输出不进入普通网关审计日志。
- 中继模式下是否端到端加密由策略快照明确记录;界面必须显示网关能否读取正文。
## 10. 能力包与更新
每个 Package 必须提供不可变版本、目标平台和架构、内容大小、SHA-256、发布者签名、权限
声明、依赖和兼容范围。安装流程统一为:
```text
取得签名目录 -> 选择不可变目标 -> 下载暂存 -> 校验大小/摘要/签名
-> 展示权限差异 -> 批准 -> 安装到 GoodBuddy-owned 目录 -> 验证 -> 启用
```
要求:
- 下载开始后冻结来源,不在失败时静默切换 ShareServer 或镜像。
- 包不得包含凭据;配置只携带非敏感结构和密钥引用。
- 新版本扩大文件、进程、网络、数据或委托权限时,旧 Grant 不自动继承。
- 紧急撤销阻止新安装和新调用;已安装包的停用或删除遵循组织策略并向用户说明。
- 复用现有 GoodBuddy 签名工件原则,但不同用途使用独立签名域。
## 11. 协议分层
### 11.1 局域网发现层
- DNS-SD 服务类型:`_goodbuddy-share._tcp.local`。
- 广播仅包含协议 major、随机实例 ID、端口和配对状态。
- 设备名称、证书和详细版本通过临时加密握手取得。
- 不扫描任意端口,不枚举未主动广播的机器。
### 11.2 ShareServer 控制 API
使用 HTTPS 提供登录、设备注册、目录、管理和审计 API;使用 WebSocket 建立设备出站控制
通道,承载心跳、任务声明、取消和短期数据通道协商。普通管理 API 与设备通道使用不同凭据
和受众。
### 11.3 数据通道
数据通道可以是配对 TLS 直连、ShareServer WebSocket 中继或经网关协商的端到端加密流。
协议只保证有界帧、顺序、取消和终态,不自行实现另一套拥塞控制。
### 11.4 版本协商
- 每层独立声明 `major.minor`。
- major 不同拒绝连接;minor 使用双方共同支持的最低能力集合。
- 未识别字段在声明允许扩展的位置忽略,安全与授权枚举中的未知值一律拒绝。
- 联邦只交换双方共同支持的能力和策略表达式,不降低任一方策略。
### 11.5 Office 编辑协议
Office 编辑使用独立的 HTTPS 会话、文件和事件 API,不复用普通远程能力 Task 表达长生命周期
编辑器。Desktop 通过设备凭据创建会话和传输源文件;编辑器使用一次性启动授权;ONLYOFFICE
使用独立短期 JWT 获取工作副本并提交保存回调。三类凭据使用不同 audience,不能互换。
文档页签可以长期存在,但启动 URL、文件 URL、callback token 和编辑 Cookie 必须短期、单用途
并可撤销。跨端会话、保存和冲突规则见
[Office 协同编辑技术设计](../features/office-document-editing/technical-design.md)。
## 12. 数据与审计
### 12.1 最小审计事件
- 操作者、调用设备和组织。
- 动作、能力、版本和提供者。
- 路由与是否跨组织。
- 输入数据类别、分类、文件数和字节数。
- 授权结果、策略 ID 和批准者。
- 任务状态、耗时、输出大小和资源用量。
- 包下载、安装、更新、撤销和发布变更。
默认不保存提示正文、文件内容、模型输出、密钥、Cookie、认证头或完整私有路径。需要内容
审计的组织必须单独定义保留期限和访问角色,并在调用前向用户显示。
### 12.2 数据权威
- 桌面端是本地配对、本机共享偏好和本地活动记录的权威。
- ShareServer 是组织成员、设备注册、组织策略、发布目录和组织审计的权威。
- ShareServer 是 Office 工作副本、服务端保存检查点、编辑会话和保留清理的权威;Desktop 或
远程 GoodBuddy Agent 是源文件是否成功回写的权威。
- 能力提供者是运行中任务及结果是否产生的权威。
- 联邦两端各自保存本组织审计,不复制对方完整成员目录或内部策略。
## 13. 安全边界
ShareServer 可以部署到公网,因此公网连接、外部组织和服务端多租户是本设计中的实际信任
边界。共同要求如下:
1. 所有远端清单、名称、schema、错误、文件和模型输出都作为不可信数据处理。
2. Renderer 不接触设备私钥、网关令牌、任意 socket 或原始文件传输接口。
3. Main/Provider 在执行边界重新验证任务,不信任 Renderer 或 ShareServer 代替本机授权。
4. ShareServer 按组织隔离查询,搜索和计数接口不能泄露无权查看的设备或能力。
5. 设备只建立主动配置的网关出站连接,不接受 ShareServer 发起任意系统调用。
6. 局域网发现不等于信任,组织成员身份也不等于所有能力授权。
7. 远程能力不能扩大本机 Ask/Execute、Workspace、Runtime、Skill 或 MCP 现有权限。
8. 密钥只进入对应安全存储,诊断和审计使用固定字段与脱敏错误码。
9. 限制握手、清单、帧、文件、并发、速率、任务时长和审计查询大小。
10. 撤销设备、用户、Grant、Publication、Package 或 FederationTrust 后停止签发新令牌。
11. Office 编辑器使用固定 origin 和隔离会话,不获得 Electron、文件系统、Shell、Runtime 或
普通 ShareServer 管理 API;Document Server 默认禁止任意出站网络和未批准插件。
12. Office 文件正文由服务端实际处理,组织策略必须定义允许格式、大小、保留和内容访问角色,
Desktop 在上传前显示这些事实。
## 14. 建议领域实体
```text
DeviceIdentity
PeerPairing
GatewayConnection
Organization
Membership
Group
RegisteredDevice
Capability
CapabilityVersion
Publication
Grant
Policy
Approval
Task
TaskAttempt
ArtifactTransfer
Package
PackageRelease
FederationTrust
AuditEvent
Revocation
DocumentEditSession
DocumentCheckpoint
```
共享协议只定义跨端 DTO 和状态,不规定桌面 SQLite 与 ShareServer 数据库使用相同表结构。
## 15. 仓库边界
当前仓库边界(跨仓库文档链接约定两个仓库克隆到同一级目录):
```text
goodbuddy/
docs/architecture/share-network-architecture.md
docs/features/device-sharing/
src/ # GoodBuddy Desktop
goodbuddy-shareserver/
docs/features/share-server/
server/ # 独立 Hono Web 服务
shared/ # 当前原型 fixture,不是跨端协议
src/ # React 管理前端
```
ShareServer 源码和服务端文档已迁入独立仓库:
`ssh://git@git.digiman.live:11022/root/goodbuddy-shareserver.git`。
当前服务是交互原型,尚未提供真实 Desktop 集成。共享网络总体设计仍由本文统一维护。
ShareServer 不反向依赖桌面端 `src/shared`,构建和运行不要求桌面仓库存在。
真实跨端协议实现时双方依赖独立 `share-protocol` 包,具体仓库归属和发布方式届时确定,
不提前创建空包。协议包不得包含数据库模型、Electron API、服务实现或凭据。
## 16. 共同验收标准
- [ ] 不配置 ShareServer 时,两台桌面端可通过局域网发现、双端配对、查看获准能力并调用。
- [ ] 关闭“允许被发现”不会关闭主动发现;关闭主动发现不会撤销既有配对。
- [ ] 本机没有明确 Publication 时,任何 Peer 或组织成员都不能查看或调用本机能力。
- [ ] 桌面端可以连接多个 ShareServer,任一连接故障不影响局域网和其他连接。
- [ ] ShareServer 只展示当前用户、设备和组织获准查看的目录。
- [ ] 每次远程调用都能说明提供者、执行位置、发送数据、路由、授权依据和委托链。
- [ ] 网络中断且副作用结果不确定时进入 `outcome_unknown`,不会自动重放。
- [ ] 能力包经大小、SHA-256、签名、兼容和权限差异校验后才能安装。
- [ ] 跨组织调用同时满足两端策略;联邦撤销后不能创建新任务。
- [ ] 审计默认只保存必要元数据,不保存正文、文件、密钥或认证信息。
- [ ] ShareServer 可独立部署和升级,不由 Electron 启动,也不影响桌面端离线使用。
- [ ] 启用 Office 服务后,Desktop 可在多实例文档页签中编辑 DOCX、XLSX 和 PPTX;保存回调、
源文件冲突或网络故障均不会破坏原文件。
- [ ] Office 会话的启动、文件和 callback 凭据不可互换,过期或撤销后不能重用;临时工作副本
按策略清理,默认审计不保存文档正文或 AI 选区内容。
## 17. 相关设计
- [设备发现与共享 PRD](../features/device-sharing/prd.md)
- [设备发现与共享功能逻辑](../features/device-sharing/logic-design.md)
- [设备发现与共享 UI](../features/device-sharing/ui-design.md)
- [设备发现与共享技术设计](../features/device-sharing/technical-design.md)
- [ShareServer PRD](../../../goodbuddy-shareserver/docs/features/share-server/prd.md)
- [ShareServer 功能逻辑](../../../goodbuddy-shareserver/docs/features/share-server/logic-design.md)
- [ShareServer 管理界面](../../../goodbuddy-shareserver/docs/features/share-server/ui-design.md)
- [ShareServer 技术设计](../../../goodbuddy-shareserver/docs/features/share-server/technical-design.md)
- [Office 协同编辑](../features/office-document-editing/README.md)
- [SSH 远程主机与 Agent](../features/remote-host/README.md)
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
+75
View File
@@ -0,0 +1,75 @@
# 数据库迁移与大数据升级指南
本文约束已发布用户数据的数据库升级。具体字段、存储表示和功能行为由对应功能文档负责;
本文不建立通用迁移框架、回滚引擎或独立状态日志。
## 先确认问题和数据边界
1. 先只读统计文件大小、WAL、页数、空闲页、表与字段的字节数,不先删除或压缩数据库。
2. 区分有效内容、重复存储、索引和已释放但未回收的空间。空闲页少时,单独
`VACUUM` 不能消除重复记录。
3. 查清实际写入、读取、删除、重放与恢复入口。修改 Runtime 相关持久化时,同时检查
本机和 GoodBuddy Agent 的桌面路径,不假设一个入口覆盖另一个入口。
4. 明确用户必须保留的信息、稳定 ID、顺序、时间、引用与远程去重字段。
分支内未发布、可安全重建的内部记录不需要兼容机制。
## 实现原则
- 先修新增数据路径,再修历史数据。历史转换与新写入尽量使用同一套转换函数。
- 普通 schema 变更使用既有 `PRAGMA user_version` 和 SQLite 事务;
已升级 schema 不允许旧客户端强行打开,不通过回写版本号支持降级。
- 大数据转换不得同步阻塞 Electron Main 或 Renderer。放到工作线程,在业务连接
打开前处理,并使用应用的共享字体、主题与页面组件显示进度和可执行的失败提示。
- 根据实测设置有界读取批次与缓存,批次提交后再报告进度。保持 SQLite 事务与
checkpoint,不靠关闭同步落盘、取消约束或一次性读入整库提速。
- 从已提交数据直接重试和还原即可完成的迁移,不增加 durable snapshot、journal、
receipt 或多阶段恢复状态。退出及失败至少回滚未提交批次,保留已完成部分。
- 差量编码必须能精确还原原始事件。更新内容、删除或重排都要覆盖;涉及远程来源时,
去重、冲突判断、消息投影和恢复序号不得失效。
- 释放数据库页和归还文件系统空间是不同步骤。先减少实际存储内容,再执行需要的
空间回收;保留足够临时空间,磁盘不足时明确失败并允许重试。
- SQLite 同库写入是串行的。是否增加读取/转换并行,应由实际读取、解析、写入、
提交与 checkpoint 的耗时决定,不把增加写线程当作默认优化。
## 验证与备份
### 合成回归
覆盖正常转换、无变化记录、重复打开、内容修订、失败事务和退出续跑。数据库大小问题
必须同时验证新增写入的增长量、旧记录可还原性以及文件实际缩小,而不只检查 schema。
对象比较必须覆盖真实生产形状和 JSON 往返,尤其是可选字段值为 `undefined` 时与字段缺省
的差异。再次修复已发布迁移产生的数据时,提高目标 schema,并覆盖已升级用户的入口;
不能只处理尚未迁移的旧库。
### 真实旧库
1. 关闭占用源数据库的应用,确认 WAL 状态。没有 WAL 时可复制已关闭的数据库;
有活动 WAL 时使用 SQLite 一致性备份机制,不只复制主文件。
2. 备份到空间足够的位置,记录字节数并校验 SHA-256。备份和测试副本分离,
不直接在唯一原件或唯一备份上试跑。
3. 使用当前源码或用户要求的 portable 正常启动入口执行升级,不用手工 SQL 代替
产品升级过程。退出一次后重新启动,验证已提交数据可以继续处理。
4. 比较升级前后消息、任务、成果等用户表的内容;对转换事件逐条还原后比较原始内容、
ID、顺序、时间和去重来源,不只比较行数。日志只能包含统计和断言结果,不记录正文。
5. 运行数据库完整性检查,记录主文件和 WAL 的实际字节数、回收比例、耗时、平台、
源 schema 与目标 schema。确认原件和备份未被改写。
6. 运行 `npm test`、`npm run typecheck`、`npm run lint`;生产构建变化按开发验证规则
检查,发布准备则遵循[发布手册](./release-runbook.md),不重复本地打包。
7. 用真实模型模拟正常用户任务,允许主代理与子代理按需调用工具、修改独立测试项目、
运行测试和处理失败;只读纯文字模型调用不能证明工具事件增长正常。记录实际提供商
请求数与统计范围,不把 Runtime 用量上报数当成全部 HTTP 请求数。
## 发布与维护
发布说明应告知升级范围、可能耗时、退出重试、额外磁盘空间和降级边界。不得把一个测试库
的回收比例当作所有用户都能获得的保证。功能文档记录具体转换规则,进度文档记录实际
证据和未完成项;未经验证的阶段不能标为完成。
### 已有实例
- Desktop 0.13.1:子任务累计过程重复保存导致数据库膨胀,转换到 schema 34 的差量事件
表示,并回收空间。具体规则见
[执行记录存储与升级回收](../features/assistant-workbar/execution-history-storage.md),
数据保留与真实旧库验证见[工作栏进度](../features/assistant-workbar/progress.md)。
- 当前未发布源码:schema 35 同时整理旧快照和 schema 34 的重复工具块,工具字段按
JSON 可保存的值比较,不因 `undefined` 被省略而误判变化。
+252
View File
@@ -0,0 +1,252 @@
# GoodBuddy 龙芯(LoongArch)预览版构建
本文档是 GoodBuddy 龙芯预览版的权威构建与功能状态说明。当前目标是
Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支持矩阵。
## 当前状态
当前流程可以在 AMD64 Linux 主机上交叉构建一个独立的
`goodbuddy-loongarch-preview` DEB,并校验包内 Electron、node-pty 和 Koffi
均为 LoongArch ELF。
预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本
当前使用 Electron 43.2.0,因此该预览包只用于移植和真机验证,不能作为正式
发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。
已完成的实际构建结果:
| 项目 | 结果 |
| --- | --- |
| GoodBuddy 源版本 | `0.11.0` |
| 预览版本 | `0.11.0-loong64-preview.1` |
| Debian 架构 | `loong64` |
| Electron | 社区版 `42.3.0` |
| DEB 包名 | `goodbuddy-loongarch-preview` |
| 构建产物 | `GoodBuddy-0.11.0-linux-loong64-preview.deb` |
| 已验证构建时间(UTC) | `20260825T102937Z` |
| 已验证产物大小 | `187053208` bytes |
| 已验证产物 SHA-256 | `461ade49f10a20ef7b28163077a2ecd6afd2c5b1b0befc673c6d174fd58497b3` |
| DEB 解包、架构和动态库解析 | 已通过 |
| QEMU Electron 初始化 | 共享库解析通过;Chromium 多线程初始化在 QEMU 下异常退出 |
| 龙芯真机启动 | 尚未验证 |
版本号和产物名会跟随当前 `package.json` 版本变化,上表只记录首次成功构建。
## 功能列表
“已打包”只表示所需代码或组件进入 DEB,不等同于已在龙芯桌面完成运行验证。
| 功能 | 预览包状态 | 当前验证 |
| --- | --- | --- |
| GoodBuddy Electron 桌面主体 | 已打包 | Electron 可执行文件为 LoongArch ELF;尚未完成真机 GUI 启动 |
| React 界面和主进程业务代码 | 已打包 | 生产 bundle 构建、测试、类型检查和 lint 已通过 |
| 直接模型调用路径 | 已打包 | JavaScript 代码与生产依赖已包含;待龙芯真机端到端验证 |
| Continue Runtime | 已打包 | CLI JavaScript 入口及 npm Runtime 已包含;待真机执行验证 |
| 本地终端能力 | 已打包 | 输入锁文件固定的 node-pty(当前为 1.2.0-beta.15)已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 |
| Koffi glibc 原生绑定 | 已打包 | Koffi 3.1.4 LoongArch glibc binding 已校验为 LoongArch ELF |
| 内置 Skills | 已打包 | 静态资源已进入 DEB |
| GoodBuddy Agent | **不包含** | 当前 Agent 的正式构建和双 glibc/musl 产物契约不支持 LoongArch |
| Remote Runtime | **不包含** | 当前没有 LoongArch Remote Runtime 产物 |
| OpenCode Runtime | **不包含** | OpenCode 所需 Bun Runtime 当前不支持 LoongArch |
| 原生 Canvas 图像解码 | **不包含** | npm 当前没有 `@napi-rs/canvas-linux-loong64-gnu` 包 |
| Koffi musl binding | **不包含** | Koffi 3.1.4 只发布了 `linux_loong64` glibc binding |
| 正式自动更新和发布索引 | **不支持** | 预览包不加入正式 CI、安装包矩阵或更新索引 |
在真机验证完成前,不应把“已打包”功能描述为正式可用。尤其是窗口显示、GPU、
音频、系统托盘、桌面集成、PTY 交互和模型调用都必须在真实 LoongArch 桌面上
重新验证。
## 与正式 GoodBuddy 隔离
预览版有意使用单独身份,避免覆盖正式安装:
- Debian 包名:`goodbuddy-loongarch-preview`
- 产品名:`GoodBuddy LoongArch Preview`
- 可执行文件:`goodbuddy-loongarch-preview`
- 安装目录:`/opt/GoodBuddy-LoongArch-Preview`
- Electron 应用身份与正式 GoodBuddy 不同,因此使用单独的用户数据目录
该流程不会修改 `.github/workflows/packages.yml`,也不会把 `loong64` 加入
正式 release manifest 或下载索引。
## 构建前提
### 本地源码主机
- 已完成 `npm ci`
- Node.js 和 npm 版本满足仓库要求
- 可以成功执行 `npm run build:bundle`
### AMD64 交叉构建主机
- Linux AMD64
- Docker
- 建议至少 30 GiB 内存和 50 GiB 可用磁盘
- 可访问 Ubuntu、npm、Electron headers 和社区 Electron Release
### Electron LoongArch shell
从
[`darkyzhou/electron-loong64`](https://github.com/darkyzhou/electron-loong64)
的 Releases 下载:
```text
electron-v42.3.0-linux-loong64.zip
```
构建脚本固定校验以下 SHA-256,摘要不匹配时必须停止:
```text
92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e
```
这是社区产物,不是 Electron 官方发布的 Linux LoongArch 二进制。
## 第一步:生成架构无关输入
在 GoodBuddy 仓库根目录执行:
```sh
npm ci
npm run build:bundle
node build/loongarch-cross/prepare-input.cjs
```
默认输出:
```text
dist/loongarch-cross/goodbuddy-loongarch-cross-input.tgz
```
输入归档包含生产 `out`、锁文件、Skills、Continue Runtime、npm Runtime、
图标和发布元数据。它不包含 Agent、Remote Runtime 或 OpenCode 二进制。
可通过 `--output` 指定其他输出位置:
```sh
node build/loongarch-cross/prepare-input.cjs \
--output /path/to/goodbuddy-loongarch-cross-input.tgz
```
## 第二步:构建交叉工具链镜像
将 `build/loongarch-cross`、输入归档和 Electron zip 放到 AMD64 Docker
主机,然后执行:
```sh
docker build \
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
build/loongarch-cross
```
工具链镜像包括:
- `loongarch64-linux-gnu-gcc` 和 G++
- LoongArch glibc 开发 sysroot
- QEMU user mode
- CMake 和 Ninja
- Rust `loongarch64-unknown-linux-gnu` target
- Node.js、npm 和上游 node-gyp
可先运行基础探针:
```sh
mkdir -p build/loongarch-cross/results
docker run --rm \
--volume "$PWD/build/loongarch-cross/results:/results" \
goodbuddy-loongarch-cross:ubuntu-26.04
```
探针必须通过 C、C++、CMake、Rust、NAPI-RS、QEMU、DEB、Koffi 和
node-pty 检查。Canvas 和 Koffi musl 显示为 unavailable 是当前已知限制,
不是 Debian 桌面主体 DEB 的构建失败。
## 第三步:生成预览 DEB
假设交叉构建主机上的文件布局如下:
```text
input/goodbuddy-loongarch-cross-input.tgz
electron/electron-v42.3.0-linux-loong64.zip
preview-output/
```
运行:
```sh
docker run --rm \
--entrypoint /usr/local/bin/goodbuddy-loongarch-preview-deb \
--volume "$PWD/input:/input:ro" \
--volume "$PWD/electron:/electron:ro" \
--volume "$PWD/preview-output:/preview-output" \
goodbuddy-loongarch-cross:ubuntu-26.04 \
/input/goodbuddy-loongarch-cross-input.tgz \
/electron/electron-v42.3.0-linux-loong64.zip \
/preview-output
```
每次构建创建一个 UTC 时间戳目录,包含:
```text
GoodBuddy-<version>-linux-loong64-preview.deb
preview-manifest.json
SHA256SUMS
```
脚本会执行以下强制检查:
1. 校验 Electron zip 的固定 SHA-256。
2. 删除 npm 错误解析出的宿主架构可选原生包。
3. 按 lockfile integrity 校验并安装 Koffi LoongArch glibc binding。
4. 读取输入 lockfile 固定的 node-pty 版本,并使用上游 node-gyp 11.4.2
和 Electron 42.3.0 headers 交叉编译。
5. 拒绝 `node_modules` 中任何不是 LoongArch ELF 的 `.node` 文件。
6. 拒绝意外进入包内的 Agent、Remote Runtime 或 OpenCode 目录。
7. 生成 `Architecture: loong64` 的 DEB,解包后再次校验关键 ELF。
Ubuntu 发行版自带的 node-gyp 会在 Linux addon 链接阶段无条件添加
`-lnode`,不适合此 Electron 交叉编译;不要用它替换脚本固定的上游
node-gyp。
## 验证边界
### 已由交叉构建验证
- C、C++、CMake 和 Rust 生成 LoongArch ELF
- C、C++ 和 Rust 程序通过 QEMU user mode 执行
- 最小 NAPI-RS addon 交叉编译
- node-pty 针对 Electron headers 交叉编译
- Koffi LoongArch glibc binding 架构
- Electron、node-pty、Koffi 和 DEB `loong64` 元数据
- DEB 完整解包
- Debian Forky rootfs 中 Electron 全部共享库解析;该检查发现并补充了
`libpulse0` DEB 依赖
### 仍需真实龙芯桌面验证
1. 安装 DEB 并从桌面入口启动。
2. 验证窗口、渲染、GPU 回退、输入法、系统托盘和通知。
3. 配置模型并执行一次真实的直接模型对话。
4. 验证 Continue Runtime 启动、取消和退出清理。
5. 打开本地终端,验证 node-pty 输入、输出、窗口缩放和进程回收。
6. 验证 Koffi 使用路径。
7. 验证音频、文件选择器、剪贴板和外部链接。
8. 确认 Agent、Remote Runtime、OpenCode 和 Canvas 相关入口不会被误认为
预览版已支持。
QEMU 可以补充动态链接器和缺库检查,但不能替代上述 GUI、PTY、GPU、音频和
桌面集成真机测试。当前 Electron 在 QEMU user mode 下可以完成全部共享库
解析;继续进入 Chromium 多线程初始化后,QEMU 出现 target `SIGSEGV`,因此
不能用该结果判断真机 Electron 是否可以启动。
## 已知依赖问题
- npm 的 `--cpu=loong64 --os=linux` 仍可能安装宿主 x64 的 Koffi 可选包。
打包脚本会删除所有该作用域的已解析原生包,并按 lockfile integrity 手工
安装 LoongArch 包。
- `@napi-rs/canvas` 没有发布 LoongArch 原生包。基础 NAPI-RS 探针通过只说明
Rust addon 工具链可用,不代表 Skia/Canvas 已完成移植。
- Koffi 没有 LoongArch musl binding,因此当前不能满足 GoodBuddy Agent 的
双 glibc/musl 产物契约。
- 预览 Electron 比正式 GoodBuddy 低一个主版本。正式支持必须先获得与当前
Electron 基线一致、可重复构建并完成安全维护的 LoongArch shell。
@@ -0,0 +1,137 @@
# Desktop 0.13.12 and Agent 0.13.3 Preparation
Prepared on 2026-09-23 on `main`. This record describes a local candidate, not a
published release. Desktop notes are in `resources/release-notes.json`; Agent
notes are in `resources/agent-release-notes/0.13.3.md`. Both are bilingual drafts
awaiting approval with the exact candidate commit before tagging.
## Scope
- Desktop baseline: `v0.13.11`, including all nine subsequent commits through
`83a48a4` and the reviewed working-tree changes.
- Agent baseline: `agent-v0.13.2`. The Agent changes reclaim idle Runtimes and
drain displaced Agents; the Desktop Continue path restores conversation
history when reopening a Runtime.
- Desktop package and both root lockfile versions are `0.13.12`.
`agent-runtime-lock.json` is `0.13.3`. Agent package tooling derives the minimum
Desktop version from `package.json`. Node, OpenCode, and Continue locks are
unchanged at `24.19.0`, `1.18.29`, and `1.5.47`.
- Both feature matrices were reviewed and updated for versions, Supervisor
default-off behavior, implemented reviews/graphs/activity/incremental input,
remaining supervision work, and release acceptance limits.
## Local Validation
`npm run release:notes:verify`, `npm run typecheck`, and `npm run lint` passed
after the final source changes. Focused runs cover 35 distinct test files and
690 passing tests; 403 tests were excluded by the IPC/App name filter.
| Group | Files | Passing Tests |
| --- | ---: | ---: |
| Supervisor, settings, bridge, and release-note suites passing in the initial run | 14 | 157 |
| Database, migration, activity I/O, note storage, menu, and sidebar suites after fixes | 6 | 209 |
| IPC/App filtered by `supervision\|Supervisor\|heartbeat\|application enable\|graph navigation` | 2 | 16 |
| Agent CLI, daemon, Runtime backend, Desktop ACP, transport, and connection manager | 7 | 207 |
| Usage, duration, workspace, Markdown, and Supervisor Electron layout | 6 | 101 |
The initial focused run had 25 failures: 22 synthetic legacy database fixtures
retained schema-46 objects, two menu assertions assumed Supervisor was enabled,
and one browser close retry left its earlier error visible. Fixtures now remove
the new objects before simulating older versions, menu tests follow the saved
enabled preference, and a successful browser close clears its scoped error.
All affected suites passed on rerun. Production migration behavior was not
changed to accommodate synthetic fixtures.
The Electron layout test uses production components with isolated fixtures,
including 1440, 1024, and 390 pixel widths and light/dark themes. It is not a
packaged-app probe or a real-user database acceptance test.
No real model requests were made during this preparation. Existing development
evidence records eight requests on the shared Linux x64 Host for remote lifecycle
checks and three for incremental review. The remote evidence does not repeat
Continue real-Host validation, the complete signed installation flow, or the
native architecture matrix. See
[remote lifecycle evidence](../features/remote-host/technical-design.md#生命周期验证2026-09-23)
and [incremental review evidence](../features/conversation-supervision/progress.md#2026-09-23-自动增量验证).
## Pending Release Work
The user requested no further full local suite. Full main-branch CI validation
and its production build remain required before tagging; native Desktop and
Agent packaging and publication verification remain pending. No local production
build or package was run. No push, tag, remote synchronization, or publication
was performed during preparation. Approval must identify the exact candidate
commit and both bilingual note files before either release tag is created.
## Approved Candidate CI Attempt
The user approved `bf8ece05902509411454b27b742ebfec930005ad` and both bilingual
note files. On 2026-09-23 that exact commit was fast-forwarded to `main` on
`origin` and `github`; both remote refs were verified. GitHub SSH stalled, so
the GitHub push used HTTPS with the existing GitHub CLI keyring credentials.
No persistent Git configuration was changed. GitHub reported the account's
existing pull-request-rule bypass for this requested direct push.
- [Desktop main CI](https://github.com/mesalogo/goodbuddy/actions/runs/35849362694)
failed in the full suite: 4 files failed, 418 passed, 13 skipped; 10 tests
failed, 4,997 passed, 43 skipped. Type checks, lint, and the production build
did not execute after the test failure.
- [Agent main CI](https://github.com/mesalogo/goodbuddy/actions/runs/35849362799)
passed source validation and native Linux x64, Linux arm64, and Darwin arm64
deterministic compound-package verification with ephemeral test signatures.
- Neither release tag was created or pushed. No Desktop or Agent publication
workflow was started, and no production release assets were verified.
The follow-up changes are limited to tests. Two IPC tests clear the startup
settings-read count before measuring interactive request lookups. The settings
failure UI test now expects Supervisor to remain disabled. OpenCode isolation
tests prepare their real configuration dependencies in a dedicated temporary
directory instead of relying on a prior local package preparation; connection
failures now include their diagnostic detail. The large activity-list and
multi-page navigation and external knowledge binding tests have explicit
15-second budgets; the binding test also waits up to five seconds for its
resulting heading.
Its snapshot fixture now selects by library ID, and its instance list remains
available during polling instead of assuming a fixed number of asynchronous
reads. Both mocks reset between tests; the binding test waits for the
selected-library read.
These budgets address timeouts observed in the full CI run without removing
assertions or changing product behavior.
The approved source and bilingual notes remain unchanged. These test changes
need a revised candidate commit and approval before another release attempt;
the failed commit must not be tagged. No local production build, package,
external model request, or LoongArch build was performed.
Follow-up validation: the seven selected IPC/UI regression cases and all seven
OpenCode isolation cases passed locally. Type checks and lint passed. Full
remote CI and the production build still require a new approved commit; these
focused results do not satisfy that release gate.
## Linux Executable Path Follow-up
On 2026-09-23, local `main` was fast-forwarded from `163e245` to
`073aa45a30efe99fab7a92f8aee2a36087db9c2a` after inspecting the four intervening
commits. They update only the community QR codes in the two README files.
The user authorized retaining these changes, fixing CI, pushing both remotes,
and publishing Desktop `v0.13.12` and Agent `agent-v0.13.3` after successful
candidate CI without another approval round. No LoongArch preview is requested.
[Desktop CI 35854792610](https://github.com/mesalogo/goodbuddy/actions/runs/35854792610)
reported 5,004 passing tests, three failures, and 43 skipped tests. All three
failures were in the real OpenCode isolation test. It constructed
`opencode-ai/bin/opencode` on Linux, but OpenCode 1.18.29 installs its npm
executable as `bin/opencode.exe` on every platform. The test now reuses
`resolveBundledRuntimePaths`, the existing production resolver, instead of
constructing a platform-specific filename. Production Runtime code and both
release-note files are unchanged.
Local follow-up validation passed all nine tests in
`opencode-selection-isolation.test.ts` and `bundled-runtimes.test.ts`, including
the existing Linux development-path assertion, plus full type checks and lint.
Release-note and package/lockfile version validation passed. The isolation
tests made two requests to their local mock endpoint and zero real model
requests. No full local suite, production build, package, or real-Host check
was run for this test-only fix; it changes neither deployed Agent code nor
the Desktop-to-Agent production path. Successful Linux main-branch CI and
its production build remain required before tagging.
@@ -0,0 +1,37 @@
# Desktop 0.13.13 Preparation
Prepared on 2026-09-23 from `b16fc90`, relative to published Desktop `v0.13.12`.
This is a Desktop-only release; Agent remains at published `0.13.3`.
No LoongArch preview is requested.
## Scope and isolation
- `bf8ff7f` reduces repeated activity-history reads during streaming saves.
- `b16fc90` restores compact supervision feedback controls and target names.
- The candidate is prepared in a separate worktree. The user's uncommitted
supervision timeout tests in the original worktree are explicitly excluded.
- Desktop and both root lockfile versions are `0.13.13`. Both feature matrices
and bilingual release notes were reviewed against this range.
- Database schema and Agent/Runtime contracts are unchanged. The activity
comparison cache belongs to Desktop persistence; it does not modify the
deployed Agent, model bridge, or remote Runtime lifecycle.
## Validation
- Six focused files passed 192 tests: activity-history I/O, AssistantDatabase,
storage upgrades, sidebar interactions, SupervisionCard, and the Supervisor
Electron layout fixture.
- Re-running `tests/supervisor-layout.electron.test.ts` with
`GOODBUDDY_SUPERVISOR_SIDEBAR=1` passed its additional sidebar scenario:
480, 300, and 200 pixel widths in both themes, no horizontal overflow,
compact 34px buttons, visible keyboard focus, and no raw target ID in body text.
- `npm run release:notes:verify`, `npm run typecheck`, and `npm run lint` passed.
- Ten consecutive activity saves performed one full-history scan. WAL and SQL
counters are not installed-application physical disk throughput measurements.
- No real model requests, local full-suite rerun, production build, packaging,
or packaged-app probe were performed. UI validation uses production
components with isolated fixtures, not a real user database.
The candidate must pass complete main-branch CI and its production build before
an unused annotated `v0.13.13` tag is pushed to both remotes. Native six-platform
packaging and public GitHub/OSS verification remain subsequent release steps.
@@ -0,0 +1,59 @@
# Desktop 0.13.14 / Agent 0.13.4 发布准备
## 范围与审批
- 用户确认从 0.13.13 升至 0.13.14,现有七个测试修改及三个验证辅助文件一起提交。
- 不构建实验性 LoongArch 预览,不运行本地生产构建、打包或安装包启动探针。
- 准备起点为 `ff746bdc1add6a12227ccb61a7794f654aaaa8ba`。
- 最近可达稳定标签为 `v0.13.12`。已公开发布的 `v0.13.13` 指向
`7756231`,其发布准备提交不在当前 main 祖先链上;两项产品修复已包含,
0.13.13 的包内发布说明也已保留。需同时核对两个范围,不重复宣传已发布修复,
不移动标签或重写历史。
- 用户随后确认 Agent 也末位加一,目标为 0.13.4。已批准联合发布的双语说明,
以及验证通过后一起提交本地候选;最终候选 SHA 仍须另行确认。
- Agent 范围为 `agent-v0.13.3..HEAD`,生产改动来自 `7adae97`。
- 用户批准提交并推送两个远端、等待 CI;标签仍待精确提交确认。
- Fetch 确认两个远端 main 均为 `7756231b3bdfa148f1b2702e4ff3fc3bd0cb057e`。
用户批准以普通合并保留双方历史,保留本次版本和已批准说明,
补回历史发布文档;不强推、不改动已有标签。
- 本地候选提交为 `d23af99`。普通合并解决后,仅新增或修改文档;
产品源码、测试、依赖、版本文件及两份已批准说明与该候选完全一致。
合并后 `v0.13.13` 恢复为可达发布基线,先前祖先链差异仅记录准备起点状态。
## 交付与数据边界
- 本轮联合准备 Desktop 0.13.14 和独立 Agent 0.13.4,不覆盖历史 Agent 包。
- 桌面包含远程结果未知后显式发送新消息的恢复修复;Agent 完成确认、
空闲回收失败处理和旧安装清理随 0.13.4 交付。
- Agent 发布流程读取本次 package.json 作为最低 Desktop 版本,因此需先升级
Desktop 至 0.13.14,再下载并更新 Host。Node、OpenCode、Continue 锁定版本不变。
- 两个候选分别使用 `v0.13.14`、`agent-v0.13.4`,Agent 不替代 Desktop Latest。
- 数据库从 schema 46 升至 47。升级前需完整备份用户数据,包括数据库和
相邻 notes/ 目录;旧客户端不能打开升级后的数据库。
- 已检查中英文功能矩阵,更新当前版本、监督回顾、待办能力及独立 Agent
交付边界。其他平台和功能的既有未验收事项继续保留。
## 验证
- 首次 `npm test`:5,125 项通过、74 项跳过、2 项失败。失败均为版本已更新而
说明尚未获批写入导致的当前版本缺失,未修改断言规避。
- 联合说明写入后的 `npm test`:435 个文件通过、10 个文件跳过;
**5,127 项通过、74 项跳过,0 失败**,耗时 685.49 秒。
- `npm run typecheck`、`npm run lint`:首轮与联合候选复验均通过。
- `npm run release:notes:verify`:联合说明写入后通过;Desktop 三处版本一致,
当前说明仅一项,Agent lock 与独立双语标题一致。
- 合并后复验:发布说明校验、9 项发布说明测试、完整 typecheck、lint、
`git diff --check` 通过。因合并仅恢复文档,未重复运行完整测试和 Host 探针。
- 当前源码 Linux x64 完成/历史恢复探针通过:两轮 Ask 均完成,Runtime 重建后
保留会话历史,每轮结束及最终退出时所属进程数均为 0。
- 隔离旧安装清理探针通过:真实 CLI 回收所属孤儿进程、删除对应旧 payload,
保留存活旧 daemon、活动工作、历史及 current/candidate,重复清理无重复删除。
- 本轮新增真实模型调用 **2 次**,模拟模型调用 0 次;清理探针模型调用 0 次。
- 两个探针从当前源码重新生成临时测试脚本,仅操作共享 Linux x64 Host 的独立
`/root/tmp/gb-lifecycle-*` 与 `/root/tmp/gb-upgrade-*` 测试目录,不替换共享安装。
探针沿用历史测试身份版本,不是正式签名 0.13.4 包安装验收,也不代替
Linux arm64、macOS arm64 原生 CI 或完整 Host 更新界面验证。
- 复现命令沿用[远程 Host 技术设计](../features/remote-host/technical-design.md)
中 lifecycle-host 和 upgrade-cleanup 脚本;本轮生命周期参数为
`driver --completion-only`,使用真实模型而非 `--fake-model`。
- 候选主分支 CI、生产构建、六平台原生打包及公开发布验证:尚未执行。
+339
View File
@@ -0,0 +1,339 @@
# GoodBuddy Release Runbook
Read this runbook only when preparing, publishing, recovering, or verifying a
desktop or GoodBuddy Agent release.
This is an operator and CI procedure. It does not authorize adding application
persistence, snapshots, journals, rollback engines, recovery protocols, or
compatibility layers to automate release operations. Determine release recovery
from the authoritative GitHub, OSS, workflow, and Git state, then use the
smallest existing idempotent rerun or a new immutable version as specified
below.
## Optional PR Article
Product publicity is an optional companion to a release, not a release blocker
or a replacement for the approved bilingual release notes. When requested,
follow the [PR article workflow](../pr-articles/README.md) and choose either the
[featured article](../pr-articles/README.md#重点稿模板) or
[regular update](../pr-articles/README.md#常规与综合稿模板) template based on user value,
not version size or commit count.
Prepare the draft and real screenshots against the candidate commit using the
[isolated screenshot procedure](../pr-articles/README.md#截图执行步骤). This does
not waive the build, packaging, or launch restrictions below. Keep missing
materials in the article's review record without delaying the software release.
Before external publication, verify the final version, download links, upgrade
notes, and screenshot accuracy. Preparing materials does not authorize posting
to a public channel; follow the user's explicit publication instruction.
## Release Packaging
- Unless the user explicitly requests it, never run a local production build,
desktop package, or launch probe as part of release preparation. In
particular, do not run `npm run build`, `npm run build:bundle`,
`npm run portable`, `npm run release:package`, direct `electron-builder`
packaging, or launch a locally packaged app before publishing a release.
Local tests, type checks, and lint plus the main-branch and native tag GitHub
Actions jobs are the release authorities.
- After a release is published, confirm only that the expected Windows assets
exist in the public release metadata. Do not locally rebuild or launch the
Windows package, download or hash published installers, attach browser
automation to a packaged app, or perform exhaustive per-asset network probes
unless the user explicitly asks for that additional validation.
- `.github/workflows/packages.yml` is the canonical cross-platform packaging
workflow. It validates and builds `out` once, then packages on six native
runners: Windows, macOS, and Linux, each for x64 and arm64.
- Linux source validation configures the installed Electron `chrome-sandbox`
helper with root ownership and mode `4755`, rehydrates the Electron
distribution without cache when the helper is missing, then runs the full
test suite under Xvfb. Keep Electron sandboxing enabled; do not skip UI tests
or add `--no-sandbox` to work around runner setup failures.
- The experimental Linux LoongArch (`loong64`) preview is built separately
through `build/loongarch-cross` by following
`docs/development/loongarch-preview-build.md`. It is not part of
`.github/workflows/packages.yml`, the six-runner GitHub Actions matrix,
standard release manifests, GitHub Release asset expectations, or the
production update index. Do not add it to those paths unless the user
explicitly requests a future production-support migration.
- Run the unified packager with
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
`dist/release/<platform>-<arch>`.
- Default deliverables are NSIS and portable ZIP for Windows, DMG for
macOS, and AppImage, DEB, and RPM for Linux. Every target includes
`release-manifest.json` with SHA-256 hashes.
- Starting with Desktop 0.12.8, macOS releases contain DMG only. Older macOS
GitHub update readers and all older mirror readers require ZIP and cannot
validate this new matrix; affected users must upgrade manually from the
download page. The user approved this immediate format change. Current
readers accept both the historical DMG/ZIP matrix and the DMG-only matrix.
- Final Desktop package verification compares every bundled OpenCode
configuration file with the prepared dependency tree and rejects external
DeepSeek imports in the unpacked Host and chunks. Windows x64 CI also
imports the packaged OpenCode plugin and starts an isolated packaged
UtilityProcess before publishing; source-only tests do not replace it.
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
bundled Continue and OpenCode runtimes, executable architecture, and package
signatures before atomically replacing a release directory.
- All desktop build paths, including local `portable`, package the Agent and
remote Runtime version locks plus the public signing-key registry without
embedding installable Linux Agent or remote Runtime payloads. Managed SSH is
optional: users explicitly download a compatible compound `.gbagent` package
from Settings or import one offline before GoodBuddy can install or update a
Host.
- Each independently published Linux x64, Linux arm64, or macOS arm64 `.gbagent` contains the
Agent daemon, pinned Node, and the desktop-maintained compatible OpenCode
and Continue Runtimes. Build all three targets on native GitHub Actions runners from one
immutable `agent-v<agentVersion>` tag.
- Keep the GoodBuddy Agent source, shared protocol/contracts, runtime lock,
bundle tooling, and tests in this repository so a desktop commit identifies
the exact Agent source it expects. Do not create a separate Agent repository
or long-lived Agent release branch unless the Agent becomes an independently
released product serving multiple clients.
- `.github/workflows/agents.yml` is the branch and pull-request build
verification workflow for GoodBuddy Agent. It must build Linux x64 and
arm64 on native runners from the checked-out commit, acquire the locked
official Node archive and locked OpenCode and Continue npm archives, verify every locked
digest, use only an ephemeral in-memory test signing identity, and verify the
resulting compound package and deterministic archive. It must not read
production signing secrets, publish installable release artifacts, or modify
the checked-in public key registry. The Darwin arm64 target uses `macos-15`
and the same production locks. Native CI checks do not replace real Host
installation, Attach, Ask/Execute, and lifecycle validation during development.
- `.github/workflows/agent-release.yml` is the only production compound Agent
publication path. It requires an annotated immutable Agent tag, the protected
`agent-signing` Environment, native x64/arm64 builds, one production
GoodBuddy signing identity for every layer of the compound package and
catalog, a non-Latest GitHub Agent Release, and synchronized immutable
Beijing OSS objects plus the final signed latest-catalog pointer.
- `.github/workflows/packages.yml` and `release:package` build only desktop
products. Missing Agent packages, remote Runtime inputs, or GoodBuddy signing
credentials must never block an ordinary desktop package or release.
- Keep electron-builder invocations on `--publish never`. Main-branch builds
run validation and build the production bundle without running the native
package matrix. Manual builds upload 30-day GitHub Actions artifacts.
Version-tag builds verify and aggregate packages before publishing GitHub
Release assets. The macOS jobs sign, notarize, and verify packages when all
five Apple credentials are configured. With no Apple credentials they must
use the explicit `--unsigned` path, warn that Gatekeeper may block the
packages, and still complete; a partial credential set must fail rather than
silently downgrade.
- Keep `ELECTRON_CACHE` and `ELECTRON_BUILDER_CACHE` under
`${{ runner.temp }}` in step-level workflow contexts. A cache beneath the
repository inherits the root `"type": "module"` and breaks electron-builder's
CommonJS macOS icon tool.
- Tag builds must use `v${package.version}`. The workflow also supports manual
dispatch and main-branch changes to release tooling.
### Agent Release Process
- Agent releases use annotated `agent-v${agent-runtime-lock.agentVersion}` tags
and are separate from desktop `v${package.version}` releases. Confirm the
exact Agent release commit and tag with the user before creating or pushing
either.
- Store approved bilingual Agent release notes in
`resources/agent-release-notes/<agentVersion>.md`. The Agent publication job
reads that exact file instead of generating a generic release description;
keep it aligned with the approved version and source range.
- The tagged commit must be reachable from protected `main`. The three native
jobs build Linux x64, Linux arm64, and Darwin arm64 packages from the same source and locks;
the catalog job rejects missing architectures, changed bytes for an existing
version/platform/architecture identity, invalid prior signatures, and incompatible
matrix metadata.
- All three targets share the existing signed `agent-catalog.json`. Before the
first mixed-platform catalog is published, release the Desktop reader that
accepts Darwin entries and document the required Desktop upgrade. Older
Desktop readers reject the entire mixed catalog and cannot check online Agent
updates; preserve their locally verified packages and immutable historical
releases. Do not introduce a second catalog or silently weaken package
signature and integrity verification.
- Agent GitHub Releases are ordinary non-draft, non-prerelease releases but
must use `--latest=false`; they must never replace the desktop release marked
Latest. Publish immutable packages and the versioned catalog to
`agent-releases/v<agentVersion>/` in Beijing OSS, publish the GitHub Agent
Release, then atomically update the single `agent-releases/latest.json`
pointer to that version's immutable catalog and signature.
- Do not report an Agent release complete until both public package
architectures match the signed catalog by size and SHA-256, the GitHub and
OSS catalog bytes/signatures are identical, the application can read the
selected source, and GitHub's repository Latest tag remains the desktop tag.
### Tagged Release Process
Every version-tag release must follow this sequence. A branch-only push does
not require release notes.
1. Confirm that the user wants a release tag and identify the exact release
commit and the new `package.json` version. Separately ask whether this
release should also receive an experimental LoongArch preview build; never
infer that choice from an earlier release.
2. Find the latest stable version tag reachable before the release commit and
inspect the complete commit and file diff from that tag to the release
commit. For the first tagged release, inspect the relevant repository
history instead.
3. Draft concise, user-facing release notes in both Simplified Chinese and
English based only on verified changes in that range. Use the titles
`GoodBuddy <version> 更新内容` and
`What's New in GoodBuddy <version>`, with corresponding `功能更新` /
`Features` and `问题修复` / `Bug Fixes` sections when applicable. The two
language versions must describe the same changes. Do not expose
internal-only details, credentials, private content, or unverified claims.
4. Show the exact bilingual release-note draft to the user and wait for
explicit approval. If the release commit or either language version changes
after approval, inspect the updated tag range and request approval again.
5. Write the approved notes to the single entry for the release version in
`resources/release-notes.json`. A failed unpublished candidate whose content
is carried forward must not retain a duplicate packaged entry.
6. Verify that `package.json`, the root `package-lock.json` version, and
`package-lock.json.packages[""].version` all equal the release version. Run
`npm run release:notes:verify`, `npm test`, `npm run typecheck`, and
`npm run lint`. Do not run a local production build, packaging command, or
packaged-app launch probe unless the user explicitly requests one.
If the user requested a LoongArch preview in step 1, build it separately
from this exact candidate by following the LoongArch build document, record
its size and SHA-256, and keep it outside the standard GitHub/OSS release
asset set unless the user separately authorizes publishing it.
7. Commit the approved candidate and confirm the exact release commit with the
user. Fetch both remotes, inspect branch movement instead of overwriting or
silently merging it, then push that commit to `main` on `origin` and
`github`. Require the candidate commit's main-branch GitHub Actions source
validation and production build to succeed before tagging.
8. Fetch both remotes again immediately before tagging. Confirm the working
tree is clean, both remote `main` refs still equal the approved commit, the
candidate tag is unused locally and remotely, and the exact approved commit
has not changed. Only then create the annotated `v${package.version}` tag at
that commit, push the tag to both remotes, and verify each peeled tag SHA
(`refs/tags/<tag>^{}`) equals the release commit.
9. Keep both approved language versions as the single source for the GitHub
Release body and the packaged first-open release-notes modal. The modal
displays the release notes matching the current interface language and
contains no button linking to a full release page.
10. Observe the tag workflow through publication and complete the public
verification checklist below. A successful push is not a completed
release.
### OSS Publication Contract
The tagged release job publishes through the GitHub Environment selected by
`.github/workflows/packages.yml` (`aliyun-oss-release`) and reads the following
effective GitHub Actions variables. Before a release or same-tag rerun can
publish, verify that repository-, organization-, or environment-level
resolution exposes:
- `ALIYUN_OSS_BUCKET=goodbuddy`
- `ALIYUN_OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com`
- a non-empty `ALIYUN_OIDC_PROVIDER_ARN` matching
`acs:ram::*:oidc-provider/*`
- a non-empty `ALIYUN_ROLE_ARN` matching `acs:ram::*:role/*`
For environment-scoped values, use the exact Environment name from the
workflow; do not assume a similarly named UI environment such as `Production`
contains the active variables.
The Bucket and Endpoint are a deployment contract, not interchangeable
examples. They must stay aligned with the trusted URL checks in
`src/main/version-checker.ts`, `sites/app.js`, website validation, and related
tests. A host, Bucket, Region, or CDN migration must update and validate every
surface together before a new release.
- Use GitHub OIDC and the RAM Role to obtain short-lived STS credentials.
Never add long-lived AccessKeys to repository or environment secrets.
- Keep `ossutil` pinned. Its V4 signing requires the Region; derive it from the
canonical Endpoint, verify that the production value resolves to
`cn-beijing`, and pass `--region` to every `ossutil cp`, including the final
`latest.json` update.
- Grant the RAM Role only the actions and prefixes required by the workflow.
It must be able to write immutable version objects and the final latest
pointer without granting unrelated administration privileges.
- Upload release assets and `site-release.json` under the immutable
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
creating or publishing the GitHub Release. Update
`releases/latest.json` only after the GitHub Release is public and all prior
checks succeeded.
- The expected GitHub Release contains 20 assets: 12 installers (two formats
for each Windows target, DMG for each macOS target, and three formats for each Linux target), six
renamed target manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
`site-release.json` is an OSS publication artifact, not a GitHub Release
asset.
### Failed Tag Recovery
Classify a failed tag by the external side effects that completed before
choosing a recovery:
- If no source or release metadata must change, correct only the external
configuration and use **Re-run failed jobs** for the same immutable tag. Do
not change, move, delete, or recreate the tag.
- If immutable OSS objects were partially uploaded but their source bytes are
unchanged, a same-tag rerun may idempotently re-upload or verify them. Never
point `latest.json` at a partially verified prefix.
- If the GitHub Release is already public but the final latest-pointer step
failed, it is a published version. Keep its packaged notes and rerun the
failed release job for the same tag; do not classify it as an unpublished
candidate.
- If code or release metadata must change, keep the failed tag immutable,
increment the patch version, obtain approval for the revised exact release
commit and notes, and create a new tag. Do not reuse the failed version.
When recovering from a version tag whose workflow never published a public
GitHub Release:
- If a code or metadata change requires a higher version and a new tag, carry
the failed candidate's approved user-facing notes forward into the recovery
version, then remove the superseded failed version's entry from
`resources/release-notes.json`.
- The packaged first-open modal must show that carried-forward content only
once under the recovery version. Never retain both the failed version and
its cumulative recovery copy, because users upgrading across them would see
duplicate content.
- Never remove the packaged history for a version that successfully published
a GitHub Release. Verify the failed release state before treating an entry as
superseded.
### Post-Publication Verification
Do not report a release complete until all of the following are verified:
1. The tag workflow and all six native package jobs succeeded. Verify the
recorded macOS signing mode: signed builds must pass `codesign`, `spctl`,
and `stapler`; unsigned builds must record the Gatekeeper caveat in the
Actions log and job summary. In the final release job, explicitly verify
the OSS configuration, OIDC authentication, release-index generation,
immutable upload, public asset check, GitHub Release publication, and
latest-pointer steps.
2. The public GitHub Release is non-draft, non-prerelease, marked Latest, and
uses the expected tag and title. Its body must exactly match the Markdown
generated from the approved packaged bilingual notes.
3. GitHub release metadata contains exactly the expected 20 uploaded asset
names, including the expected Windows x64 and arm64 assets. Do not download,
hash, launch, or send individual network probes to published installers
unless the user explicitly requests that additional validation.
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
version, exact six targets and 12 installer entries, the trusted Beijing
URLs, and the GitHub fallback URL. It must match the immutable
`releases/<tag>/site-release.json`. Fetching and comparing these small JSON
metadata files is sufficient; rely on the successful publication job for
its per-asset public checks.
5. The live website successfully fetches the index and produces the 12 correct
platform/architecture/format links from that metadata.
6. Both remote `main` refs and both peeled tag refs still equal the approved
release commit, and the local working tree is clean.
Never create or push a release tag, and never push a previously created
release tag, before the release-note draft has received explicit approval.
- Before a push that updates the `github` remote, ask whether the user wants a
release tag unless they already specified that choice. A branch-only push
does not require a version bump or tag. When the user requests a release,
verify that `package.json` and `package-lock.json` contain the same release
version, create `v${package.version}` at the exact commit being pushed, and
push that tag so the native package matrix and GitHub Release run.
- Never move or reuse an existing release tag. If `v${package.version}` already
exists locally or on a remote at another commit, increment the package
version and create a new matching tag before the release push.
- Verified release baseline on 2026-08-18: commit
`60119a4317118fa3f077db0382664f15266a6682`, annotated tag `v0.10.4`, and
GitHub Actions run `32038633609` attempt 2 succeeded through all six native
packages, GitHub Release publication, Beijing OSS publication, and the final
`latest.json` switch.
+54
View File
@@ -0,0 +1,54 @@
# Desktop Window Recovery
This is recovery hardening for intermittent blank windows, not an established
diagnosis of their cause.
- `src/renderer/src/main.tsx` wraps the locale provider, App and storage-upgrade
page in `AppErrorBoundary`. A React render/lifecycle failure replaces the tree
with a blocking recovery page and an explicit Reload button. It uses the saved
UI language (including system preference resolution), shared theme tokens and
primary-button styling, without depending on the failed application providers.
- Reload refreshes the current document, retaining its URL and storage-upgrade
query. It can lose unsaved input. There are no automatic React retries.
- `src/main/window.ts` listens for `render-process-gone`. Abnormal exits open one
native dialog at a time, using the OS locale because renderer language storage
is unavailable to Main. Reload occurs only on explicit confirmation. Cancel
leaves the window unchanged; persistent failures require quitting and reopening
GoodBuddy. Clean exits, shutdown and destroyed windows do not trigger recovery.
- Main-frame load failures are logged; aborted navigation and subframe failures
are ignored. Initial load promises are caught, without automatic load retries
or a new load-failure UI.
- Native crashes and main-frame load failures use the existing bounded desktop
diagnostics store (`desktop.renderer.gone`, `desktop.renderer.load-failed`,
stage `renderer`). Persistent records retain fixed summaries rather than raw
errors. Crash reason/exit code and React errors/component stacks go to their
respective process consoles. There is no new diagnostic IPC.
The boundary does not catch module evaluation/startup failures before React
mounts, event-handler exceptions, asynchronous rejections, a hung renderer, or
main-process failure. Reload does not establish that a repeated underlying error
has been fixed. Agent runtimes and remote execution are unchanged.
Targeted regressions are in `AppErrorBoundary.test.tsx`, `window.test.ts` and
`desktop-diagnostics.test.ts`. Electron API mocks verify decision paths but do
not establish native dialog appearance or full application recovery.
Validation on Windows, 2026-09-15:
- Targeted Vitest run: 20 tests passed across the three files above.
- `npm test`: 359 files passed, 9 skipped; 4,225 tests passed, 67 skipped.
- `npm run typecheck`, `npm run lint` and `git diff --check` passed.
- An isolated Electron fixture bundled the current boundary, shared styles and
production window module. Both languages and themes at requested content sizes
1280x800, 960x720, 720x640 and 640x420 passed visibility, focus and horizontal
overflow checks. Windows scaling rounded actual widths down by one CSS pixel.
The 16-image overview and Chinese/light and English/dark short-window originals
were visually reviewed; no clipping or focus-style defect was found.
- A native Space key activated the recovery button and reloaded the document.
Two actual renderer crashes verified confirmed reload with URL retention and
cancellation without retry. Only native dialog responses were mocked; native
dialog appearance, full App business-state recovery and other OSes were not
verified. No model calls were made by this fixture.
- The first temporary Electron driver timed out during ESM startup. Removing its
top-level readiness await fixed the driver; the subsequent run passed. This
was a test-driver failure, not evidence of the reported application root cause.
+135
View File
@@ -0,0 +1,135 @@
# GoodBuddy Feature Documentation Guide
## Scope
These instructions apply to all files under `docs/features/`.
Each direct child directory represents one user-visible product feature or one coherent feature family. Organize documentation by feature ownership first, then by document responsibility.
## Standard Structure
Use the smallest applicable subset:
```text
docs/features/<feature-name>/
README.md
prd.md
user-stories.md
logic-design.md
ui-design.md
technical-design.md
progress.md
```
Additional documents are allowed when one standard file would mix independent responsibilities. List every additional document and its unique responsibility in the feature `README.md`.
Do not create empty placeholders. If a standard document is not needed yet, omit it and state the missing responsibility in `README.md` only when that absence matters to current work.
## Document Responsibilities
### `README.md`
- The sole entry point for the feature.
- Summarizes feature purpose and boundary.
- Lists all feature documents and their authoritative responsibilities.
- Defines feature-specific terminology.
- Points to cross-feature dependencies without copying them.
### `prd.md`
- Defines the user problem, goals, scope, non-goals, functional requirements, and product acceptance.
- Describes what the product must do, not module layout or implementation mechanics.
- Records explicit limitations when a workflow is intentionally unsupported.
### `user-stories.md`
- Defines user roles, scenarios, and Given/When/Then-style acceptance.
- Covers first use, normal use, failure recovery, compatibility, permissions, and boundary cases relevant to real users.
- Keeps implementation details out unless the detail is itself an observable contract.
### `logic-design.md`
- Defines invariants, state dimensions, state transitions, decision tables, priority rules, failure outcomes, and cross-story consistency.
- Resolves overlap between PRD requirements and User Stories before technical implementation.
- Includes a logic completeness assessment and identifies unresolved product decisions separately from technical selection work.
### `ui-design.md`
- Defines feature information architecture, shared component usage, states, copy, feedback, keyboard behavior, accessibility, and responsive behavior.
- Must follow the repository root `UI-DESIGN.md`.
- Does not create feature-local visual systems when shared tokens and primitives already exist.
### `technical-design.md`
- Defines process and module boundaries, shared contracts, persistence, IPC, lifecycle, packaging, compatibility, validation, and implementation order.
- Implements the PRD and logic design rather than redefining their behavior.
- Follows KISS and YAGNI. Do not add recovery or compatibility machinery without a demonstrated requirement.
### `progress.md`
- Records only verified implementation facts, remaining work, blockers, validation commands, and dated evidence.
- Never marks an item complete based only on a design, mock, schema, or injected fake when the production path remains incomplete.
- Does not duplicate future design. Link back to the authoritative document.
## Traceability
For non-trivial features:
1. PRD requirements use stable IDs such as `FR-1`.
2. User Stories use stable IDs such as `US-A1`.
3. Logic design maps rules and state transitions to PRD and User Story IDs.
4. UI and technical design link to the PRD and logic design.
5. Progress items map to implementation stages and record actual validation.
Do not renumber stable IDs merely for presentation after implementation begins.
## Source of Truth
- PRD owns product scope and requirements.
- User Stories own user scenarios and scenario acceptance.
- Logic design owns behavioral decisions and state rules.
- Root `UI-DESIGN.md` owns global UI consistency; feature UI design owns feature-specific application.
- Technical design owns implementation architecture.
- Progress owns current implementation and validation status.
- Final code and validated runtime behavior override stale documentation. Correct all affected documents when they diverge.
## Feature Boundaries
- Keep a document in a feature directory when one feature can own its behavior and acceptance.
- Keep a document in `docs/architecture`, `docs/quality`, `docs/development`, or `docs/roadmap` only when it truly governs multiple features and has no single feature owner.
- Reference cross-feature documents rather than copying their definitions.
- When two features share a global domain model, keep that model in the feature family that owns the domain or in cross-feature architecture if ownership is genuinely global.
## Managed External Artifacts
When a feature downloads external artifacts managed by GoodBuddy:
- Define the exact setting that selects the source and the setting scope.
- Use immutable artifact targets with expected byte size and SHA-256.
- Freeze the selected source when an operation starts.
- Fail explicitly when the selected source is unavailable or invalid. Never silently request another source.
- Keep download URLs and redirect allowlists out of Renderer-controlled input.
- State whether the setting affects models, application updates, Agent packages, managed tool runtimes, package-manager registries, or user-configured URLs. Do not imply a broader scope than implemented.
- When two targets are presented as an upstream/original address and its OSS mirror, require byte-identical content with one shared size and SHA-256. A mirror must not rebuild, recompress, or modify the upstream artifact.
## Migration Rules
When moving existing documentation:
1. Preserve content and Git history where practical.
2. Update every repository link to the new path.
3. Add or update the destination feature `README.md`.
4. Delete the old authoritative path; do not leave a duplicate compatibility copy.
5. Do not rewrite unrelated document content merely because the file moved.
6. Preserve uncommitted user edits while moving files.
7. Verify all relative Markdown links after migration.
## Review Checklist
- The feature has one clear entry point.
- Every document has one declared responsibility.
- Product requirements, stories, logic, UI, technical design, and progress do not contradict each other.
- Normal, failure, cancellation, compatibility, permission, and scope behavior are covered where applicable.
- UI behavior follows `UI-DESIGN.md`.
- Progress claims are backed by dated evidence.
- All relative links resolve.
@@ -0,0 +1,120 @@
# 应用导航与系统工具入口
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,四个内置应用共用可调整的保存顺序,知识库始终启用并常驻;
智能心跳、魔法笔记和本机推理监控可通过应用管理控制启用和常驻。底部应用中心向上展开轻量锚定菜单,列出已启用应用;底部“管理应用”打开既有管理 Modal。系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
商店入口、推荐位或安装流程。
当前分期见 [PRD 第 8 节](./prd.md#8-分期),实施步骤见[技术设计第 11 节](./technical-design.md#11-实施顺序)。
本轮 A–D 已接入生产源码:应用菜单、应用管理 Modal、共享应用设置、持久化偏好及本机推理监控。菜单不按打开历史或常驻状态筛选,无模态遮罩;点击应用行关闭菜单,知识库、智能心跳和魔法笔记打开主内容区页面。本机推理监控从菜单、常驻入口或管理 Modal 打开时均使用独立 Modal,保留底层工作区。管理列表、菜单和侧栏按同一保存顺序排列,侧栏按启用和常驻过滤。知识库在管理 Modal 中提供“打开”和排序,不提供启用、常驻开关或通用空设置;原工作区配置及心跳单条计划 enabled 保持原义。验证范围及集成缺口见[实施进度](./progress.md)。
新增计划包括 [私有化市场与 YAML 交换](./prd.md#fr-15-私有化市场与-yaml-交换后续):
通过市场 URL 获取应用,支持 `.yml` 导入导出,未来由 ShareServer 提供私有目录与分发。
[本机模型管理入口](./prd.md#fr-16-本机模型管理入口新增计划项)已独立于商店接入,作为
可常驻左侧的“本机推理监控”(英文 Local Inference Monitor)内置应用,直接查看语音、OCR、向量等本机服务状态与可用资源指标,不显示任务历史。向量服务显示独立进程的 CPU 和工作集内存;ASR、OCR 与宿主共享进程,逐行说明无法独立统计的原因。
本轮已接入 `local-inference` 管理 Modal、类型化 IPC、真实请求登记及引擎管理;实际支持清单见
[技术设计](./technical-design.md#111-本机推理任务与执行服务),市场仍为后续需求。
相关验收见 [User Stories](./user-stories.md#7-后续需求场景)。
项目活动汇总独立于上述应用中心计划,范围见 [FR-14](./prd.md#fr-14-项目活动汇总)。
计数规则由[活动逻辑](./logic-design.md#9-项目活动汇总)定义,入口与级联菜单见
[活动界面](./ui-design.md#10-项目活动汇总),数据加载与会话跳转见
[技术设计](./technical-design.md#12-项目活动汇总)。此处“活动”包括当前运行、待处理及本次客户端
会话内已完成但未查看的会话;完成提示在实际进入聊天后清除,不累计历史完成总数。
## 文档导航
现有 `heartbeat` 入口承载监督者工作回顾与故事线图谱,可管理启用、常驻和排序。应用默认关闭;完整执行边界见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)。未来市场复用同一应用身份,不增加平级重复入口,目标定义见[监督者产品设计](../conversation-supervision/supervisor-prd.md)。
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、产品边界、功能要求与产品验收 |
| [User Stories](./user-stories.md) | 用户场景及 Given/When/Then 验收 |
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
| [实施进度](./progress.md) | 应用中心、设置及项目活动级联的实现与验证记录 |
| [应用中心交互 Demo](./navigation-demo.html) | 对比六个候选方案;配套 CSS 和 JS 仅服务原型 |
Demo 可直接在支持原生 dialog、popover 的现代浏览器中打开,无需依赖或构建。
六个方案共用本次页面会话内的配置,刷新后全部恢复默认,不持久化或修改真实设置。
A–E 仅展示内置应用,没有商店入口、推荐或安装操作;F 单独演示未来私有市场。
模型服务、资源指标和工具内容均为演示数据,不执行真实安装、模型加载或进程操作。
窄屏上下排列侧栏与主内容,应用页面隐藏输入区,对话页面显示输入区。
Demo 的模型管理和 F 方案均使用模拟数据;正式本机推理监控 Modal 的交付范围以技术清单和实施进度为准,旧页面原型不作为 Modal 验收依据。
底部输入区共用一个简化候选布局:附件、语音、Runtime、模式和发送位于主操作行,专家、
知识范围及 Runtime 专属选项通过“选项”展开,非默认选择显示为可移除标签。可体验文件名
附件、模拟语音、模板填入、原生命令选择、8 秒模拟回复、发送排队、立即插入、删除、停止,
以及上下文详情和模拟压缩。文件只读取名称,语音使用固定示例文本,不录音、不上传、不调用 AI。
这是输入区设计讨论用的原型,尚未改变正式产品或替代现有 UI 规范。
左上角项目选择器下方仅在有运行中或待处理会话时显示活动摘要,全部空闲时隐藏。点击摘要打开项目
列表,悬停或点击项目向右展开会话子菜单,点击会话才切换项目与对话。子菜单与项目列表
相接,项目行保留子菜单箭头,会话行只显示名称与状态。支持上下方向键选择、右键进入、左键或 Esc 返回,顶层 Esc 关闭;窄屏在同一浮层内
进入会话层,并提供返回按钮。页面上方可切换“有待处理、仅运行中、仅当前项目运行、全部
空闲”四种场景。项目与会话均为演示数据;该级联交互已接入正式产品活动入口,范围与
验证记录见[实施进度](./progress.md)。
Demo 已移除左侧系统工具快捷区:文件、终端、浏览器和资源监控只在右侧
工作栏 Tab 中演示,不列入应用中心。旧原型仍直接打开整窗 Modal,其中使用可搜索应用列表。
Demo 待同步更正:本节仅描述旧原型,不是生产行为或验收依据。原型直接打开 Modal、知识库的启停设置,以及心跳默认启用和关闭仅隐藏入口的说明均已过时;原型的常驻排序也不代表生产中四个应用的共享顺序。生产规则见上述说明及 UI 设计。Demo 已同步“本机推理监控”名称及其默认不常驻状态。
六个方案共用“我的应用”区:通过 + 或“全部应用”选择常驻应用,编辑时可拖动或使用
上下箭头排序、移除入口。内置应用默认全部启用;魔法笔记、知识库、智能心跳默认常驻,计划预览“本机推理监控”默认不常驻。
取消常驻仅移除导航捷径,仍可从应用中心打开;关闭应用隐藏业务入口,应用中心保留设置与
重新开启入口,并记住常驻偏好。当前应用页关闭后显示不可用状态,重新启用从应用中心卡片设置进入。
知识库关闭时同步隐藏侧栏入口、会话选项中的知识范围和检索方式、已选知识标签;重新开启
恢复原来的选择。以上操作不删除数据,也不表示停止后台任务或模型服务。
“恢复默认应用”重置应用安装、启用、常驻和笔记设置,返回对话页;模型服务模拟状态保留至刷新。
通用应用设置统一从应用中心卡片进入,应用页面和全局设置不提供重复入口,分别控制
启用与常驻。魔法笔记面板另含显示未完成待办数量、AI 评论方式
(即时/保存后自动/保存后手动)、AI 评论形式(长评 + 要点/长评/要点)。现有“显示
魔法笔记入口”不在原型中重复提供。正式迁移保留其业务启用值,常驻使用独立配置。启用、常驻、待办数量显示默认开启;
评论方式默认即时,评论形式默认长评 + 要点。侧栏待办数为固定的 3 项演示数据。
修改即时生效于原型,不调用 AI 或写入真实应用设置。
此 Demo 中的任务、进度、使用方与服务内存为旧方案模拟内容;正式 Modal 已收窄为服务状态列表,见 [FR-16](./prd.md#fr-16-本机模型管理入口新增计划项)。
引擎和管理方式收进详情;受管进程模拟启动、停止、重启,进程内模型模拟加载与释放。
服务停止或重启后任务标为中断,重新启动不自动恢复任务;任务级取消仍待实际引擎接入。
影响使用方的操作先显示就地确认,取消保留原状态;隐藏或关闭管理应用不改变服务状态。
F 提供私有市场 URL、模拟获取目录、模拟不可达与重试,不发起网络请求或切换公共来源。
目录包含项目管理和阅读空间示例,可模拟安装或安装并常驻。YAML 文本支持导入预览和导出
预览,明确标为 mock schema,仅识别示例中的七个扁平字段,尚非通用 YAML 解析器。
导入列出依赖、外部资源和同标识冲突:缺少依赖或资源时不允许安装;内置应用不允许覆盖;
已安装扩展须显式确认更新定义,保留启用与常驻偏好。导入新应用在目录中以标识显示,原始名称
保留在定义及导出预览中。修改输入会使旧预览失效,取消不修改安装状态。
导出仅显示定义文本,不写文件,不包含凭据、会话、笔记、知识数据、程序包或模型权重。
| 方案 | 讨论重点 |
| --- | --- |
| A:紧凑入口 | 从底部向上展开应用列表 |
| B:应用宫格 | 在展开面板中展示应用图标、名称和常驻操作 |
| C:应用预览 | 用卡片说明应用用途 |
| D:搜索启动台 | 搜索打开能力,支持 Ctrl / Command K、方向键与 Enter |
| E:常驻管理 | 已常驻应用优先排列,可取消常驻或打开 |
| F:私有市场预览(未来) | URL 模拟获取、YAML 导入导出与显式模拟安装,不纳入首期 |
D 参考 [Raycast](https://www.raycast.com/) 的搜索启动与扩展发现方式。早期方案参考过
Notion 收藏和 VS Code Profiles,当前版本已移除场景工具包与快捷区。
原型支持 URL 的 `#launcher`、`#favorites`、
`#scenes` 等片段直接打开指定方案。
## 术语
| 术语 | 定义 |
| --- | --- |
| 系统入口 | GoodBuddy 工作流程或审计所必需的固定入口,用户不能卸载、隐藏或改变类型 |
| 应用 | 可显示在主导航区的功能;知识库、智能心跳和魔法笔记打开一级页面,本机推理监控打开保留底层工作区的 Modal |
| 系统工具 | GoodBuddy 自带且不可卸载的辅助能力,包括文件、终端、浏览器和资源监控,由右侧助手工作栏 Tab 承载 |
| 插件应用 | 未来由插件提供的一级页面入口 |
| 插件工具 | 未来由插件提供的辅助面板或命令入口 |
| 快捷入口 | 只负责打开或聚焦能力,不复制该能力的界面和业务状态 |
## 相关文档
- [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md)
- [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)
- [助手工作栏多终端页签 PRD](../assistant-workbar/terminal-tabs-prd.md)
- [智能心跳 PRD](../smart-heartbeat/prd.md)
- [知识库功能文档](../knowledge-base/README.md)
- [文档解析入口与职责](../document-processing/ui-design.md#11-入口与职责):全局 OCR 配置、附件与知识文档操作的目标分工;不新增应用中心成员,本机推理监控不管理 HTTP OCR 服务。
@@ -0,0 +1,269 @@
# 应用导航与系统工具入口功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 应用中心与本机推理监控已接入源码,整体验收待完成 |
| 版本 | 0.4 |
| 日期 | 2026-09-17 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 关联 User Stories | [User Stories](./user-stories.md) |
## 1. 不变量
1. “当前对话”“运行记录”和“设置”始终是系统入口。
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
4. 左下角提供应用中心和独立固定设置入口;文件、终端、浏览器、资源监控只通过右侧工作栏 Tab 和“+”访问,不进入应用中心。
5. 四个内置应用共享保存顺序。知识库始终启用并常驻,位置可调整;智能心跳、魔法笔记、本机推理监控可独立设置启用和常驻。取消常驻不关闭功能,关闭不删除数据或导航偏好。
6. 入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
7. 应用中心始终可达,点击向上展开无模态遮罩的锚定菜单,只列已启用应用,不使用打开历史或常驻过滤。菜单底部“管理应用”打开既有 Modal,关闭的可选应用在其中仍可搜索、配置和重新开启。
8. 所有入口和新请求的功能判断读取同一份保存成功的权威设置;保存中的目标值不提前生效。
## 2. 入口模型
```ts
type NavigationContribution =
| {
type: 'system-entry'
id: 'chat' | 'activity' | 'application-center' | 'settings'
placement: 'primary-start' | 'primary-end' | 'footer'
}
| {
type: 'application'
id: 'knowledge'
source: 'builtin'
alwaysShown: true
}
| {
type: 'application'
id: 'magic-notes' | 'heartbeat' | 'local-inference'
source: 'builtin'
enabled: boolean
pinned: boolean
}
| {
type: 'system-tool'
id: 'workspace' | 'terminal' | 'browser' | 'resources'
workbarAppId: 'workspace' | 'terminal' | 'browser' | 'resources'
}
```
该模型是首期的逻辑投影,启用值来自各应用的唯一业务配置,常驻及顺序来自导航偏好。
应用清单为魔法笔记、知识库、智能心跳、本机推理监控,打开无需安装。市场不加入本轮清单。
## 3. 主导航组合规则
主导航按以下顺序计算:
```text
当前对话
应用(按保存顺序;知识库始终显示,可选应用仅显示启用且常驻项)
运行记录
```
当前对话、运行记录和设置不参与应用排序。默认应用顺序为知识库、智能心跳、魔法笔记、本机推理监控;魔法笔记默认常驻,本机推理监控默认不常驻。过滤可选项不留下空白。
常驻、取消常驻和排序按设备保存,重启后恢复。排序只发生在应用区,不移动固定系统入口。
管理列表、应用菜单和主导航共用完整应用顺序,分别按搜索、启用、启用且常驻过滤;过滤不删除顺序记录,重新开启或常驻时按保存顺序恢复。
每个管理行均可排序,包括关闭或未常驻的应用。上下移动以完整保存顺序中的相邻应用为目标,拖动使用同一移动规则。搜索时禁用拖动,箭头仍按完整顺序移动,保留未命中应用及其相对顺序。
### 3.1 应用可见性与业务状态
下表适用于智能心跳、魔法笔记和本机推理监控。知识库始终可用,管理 Modal 中提供“打开”和排序,保留原工作区实际配置,不增加启用、常驻开关或通用空设置页。心跳应用的默认值与执行条件见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)。
| 常驻偏好 | 功能启用 | 结果 |
| --- | --- | --- |
| 是 | 是 | 主导航显示,可从主导航或应用中心打开 |
| 否 | 是 | 主导航隐藏,仍可从应用中心打开,后台和数据保持原状 |
| 是 | 否 | 主导航、应用菜单和业务入口隐藏,管理 Modal 保留设置,记住常驻及顺序 |
| 否 | 否 | 主导航、应用菜单和业务入口隐藏,管理 Modal 保留设置,记住未常驻状态 |
管理 Modal 浏览和搜索包含关闭的可选应用,搜索不修改启用或常驻。轻量菜单仅显示已启用项,点击行先关闭菜单;本机推理监控打开独立 Modal,其余应用请求主内容区导航。管理 Modal 的“打开”和常驻入口遵守同一规则。打开已启用应用不自动常驻;
取消当前应用的常驻不关闭当前页面或本机推理监控 Modal。关闭应用后不能从旧入口继续执行新的业务操作。
停用时仍允许管理常驻偏好,界面说明重新开启后才会显示入口。
### 3.2 关闭应用与恢复
关闭与恢复适用于可选应用,知识库页面、会话控件及关联不受影响。智能心跳关闭后的调度、手动请求、侧栏与在途运行按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)处理,保留单条计划 enabled。
| 对象 | 关闭后的结果 | 重新开启后的结果 |
| --- | --- | --- |
| 当前已打开应用页或本机推理监控 Modal | 原位置显示“应用已关闭”的不可用状态;保留内容状态及草稿,不强制跳转。重新启用从应用中心对应行的设置进入,监控 Modal 需先关闭 | 恢复可用性;仍打开时恢复内容,已关闭或离开则不自动打开 |
| 已在执行的请求及后台操作 | 沿用功能自身的取消、完成和清理生命周期;本次应用开关不新增取消策略 | 不自动恢复、中断后重跑或补发任务 |
| 数据、常驻及顺序 | 保留原值,业务入口按启用状态过滤 | 按保存的常驻及顺序恢复入口,不把未常驻应用自动常驻 |
知识请求及单个知识库实例停用、配置修改或删除继续遵守知识库自身规则,没有应用级禁用判断。
### 3.3 魔法笔记设置迁移
魔法笔记设置迁入应用中心对应行的设置页;笔记页面和全局设置均不提供重复入口,
编辑同一配置。启用、常驻、显示未完成待办数量分别控制功能、导航和徽标,不保留重复的
“显示魔法笔记入口”开关。
未配置的魔法笔记、本机推理监控启用及魔法笔记相关布尔项默认开启;心跳应用默认关闭。魔法笔记和心跳默认常驻,本机推理监控默认不常驻;已启用应用仍可从应用菜单及管理 Modal 打开。已有保存的 `true` 和 `false` 均保留。
AI 评论方式和评论形式继续为单选,沿用现有默认值“即时”和“长评 + 要点”,不能解释为
全部模式同时启用。迁移保留既有单选值及业务数据。关闭数量显示只隐藏徽标,不修改待办。
## 4. 系统工具打开规则
### 4.1 通用流程
```text
用户展开右侧工作栏
-> 选择已有 Tab,或从“+”目录选择工具
-> 已有 Tab 按实例 ID 激活;目录按单实例或多实例策略创建或激活
-> 把焦点移入面板标题或首个主要控件
```
若创建失败,工作栏保持展开并在对应能力位置显示错误;主页面和其他工作栏实例不被清空。
### 4.2 决策表
| 工具 | 选择已有 Tab | 从“+”目录选择 | 上下文不可用 |
| --- | --- | --- | --- |
| 文件 | 激活不可关闭单实例并保留跟随或固定状态 | 聚焦 `workspace`;仅迁移或布局修复时补回 | 跟随模式无有效项目目录或默认工作区时显示修复入口 |
| 终端 | 激活选中的确切实例,保留执行空间和历史状态 | 每次创建绑定当前执行空间的额外实例 | 新建时无项目使用本机 Home;SSH 失败保留远端目标与诊断 |
| 浏览器 | 激活选中的确切实例,保留绑定目标和页面 | 每次创建绑定当前 Conversation 的额外实例 | 新建时无 Conversation 创建待绑定空实例 |
| 资源监控 | 激活本机 `resources` 单实例 | 已存在则聚焦,否则创建 | 部分指标不可用时显示原因并保留其余指标 |
选择已有 Tab 不按当前项目或 Conversation 查找替代实例,也不重绑目标。文件的跟随模式
仍按工作栏自身规则更新上下文;固定目标失效时显示该目标的修复状态,不静默回退。
终端 `closing`、`exited`、`interrupted`、`failed` Tab 保留原状态,选择它们不自动重建。
终端和浏览器通过“+”新建时不按目标去重,不恢复旧快捷按钮的最近匹配实例复用行为。
### 4.3 选中与状态
工作栏按确切实例 ID 表达 Tab 选中状态,同类型其他实例不能同时显示为选中。“+”是普通
按钮,不是 Tab。主侧栏和应用中心不显示工具选中状态或工具徽标。
徽标优先级为:等待用户处理 > 错误 > 运行中 > 无徽标。同一 Tab 只显示一个视觉徽标,
可访问名称包含完整状态。
资源监控首期不在后台采样,因此不显示负载告警徽标。后台事件不自动展开工作栏或切换 Tab。
### 4.4 资源采样状态
```text
inactive -> warming -> loading -> live
-> partial
-> error
live/partial/error -> loading
warming/loading/live/partial/error -> inactive
```
- `inactive`:资源面板未处于可见活动状态,没有采样定时器。
- `warming`:首次进入或停止后恢复,建立 CPU 和磁盘 IO 区间基线;内存仍可立即显示。
- `loading`:正在请求后续样本;请求未结束时跳过后续时钟触发。
- `live`:所有首期指标可用,按 2 秒间隔追加样本。
- `partial`:至少一个指标可用;不可用项携带稳定错误码,其他指标继续更新。
- `error`:本次没有任何可用指标,保留已有历史;自动时钟或手动操作可以重试。
资源面板为活动实例、工作栏展开且 `document.visibilityState` 为 `visible` 时才离开 `inactive`。
任一条件失效即停止定时器并重置区间基线。历史队列只保留最后 150 个样本;时间间隔较长或
采样失败时保留真实时间缺口,不补零或伪造中间数据。
## 5. 设置保存规则
应用导航偏好与业务启停使用独立字段,通过同一应用设置保存链路提交。常驻、排序、启用和
魔法笔记设置更新串行执行;前一请求确认前,所有入口中会修改这组设置的控件暂不可操作。
每次只提交本次变更,Main 在最新已保存设置上合并并原子写入;顺序提交完整目标顺序。
持久化成功后由 Store 发布完整权威快照,应用中心、主导航、当前应用页、会话和所有设置入口同步采用,工具配置写入也走同一通知链路。每次重开中心先刷新并锁定修改;较新的设置事件使在途旧读取及旧保存响应失效,不能覆盖新快照。
保存失败时:
- 所有界面保留最后一次确认的配置,包括启用、常驻、顺序、徽标和会话入口;不提前运行关闭或恢复副作用。
- 发起操作的设置行或排序区域显示就地可重试错误,保留焦点及编辑上下文,不重复显示同一错误通知。
- 若响应中断导致保存结果未知,先重读权威设置再开放修改;不能把本地旧值当成回滚成功,也不自动重发变更。
## 6. 与需求的对应关系
| 逻辑 | PRD | User Stories |
| --- | --- | --- |
| 固定入口不变量 | FR-1、FR-2 | US-A1、US-A2 |
| 应用常驻和失败恢复 | FR-3、FR-4、FR-11、FR-12 | US-B1、US-B2、US-B4、US-B5 |
| 应用中心、常驻顺序 | FR-3、FR-4、FR-5、FR-11、FR-12;PRD 第 8 节 | US-B6、US-B7 |
| 可选应用启用恢复、知识库固定可用 | FR-3、FR-4、FR-12;PRD 第 8 节补充要求 | US-B8 |
| 魔法笔记迁移及共同配置 | FR-11、FR-12;PRD 第 8 节补充要求 | US-B9 |
| 工具打开决策表 | FR-6 至 FR-10 | US-C1 至 US-C7 |
| 资源采样状态 | FR-9 | US-C6 |
| 无障碍与窄窗口 | FR-13 | US-D1、US-D2 |
| 活动聚合、完成通知与查看清除 | FR-14 | US-E1、US-E2、US-E3 |
| 后续私有市场与 YAML | FR-15 | US-F1、US-F2 |
| 服务状态、资源可用性、服务生命周期 | FR-16 | US-F3、US-F5;US-F4 入口已移除 |
稳定 ID 保留原编号。US-B6 为应用中心,US-B7 为顺序,US-B8 为启用联动,US-B9 为笔记迁移;
US-F3 为服务概览,US-F4 为已移除的任务取消入口,US-F5 为服务生命周期。PRD 旧 FR-5 至 FR-10 和
User Stories 已同步上述入口规则,工具实例行为沿用右侧工作栏契约。
## 7. 逻辑完整性评估
首期覆盖应用中心访问、启用与常驻组合、顺序恢复、默认值与旧值迁移、共同设置、保存失败、
知识库固定入口、心跳应用启停、可选应用已打开页面恢复,以及右侧实例选择、本机 Home、托管 SSH、无会话、
目录失效、资源部分可用与采样停止。工具内部取消、超时、连接恢复及业务后台生命周期由
相应功能负责,本功能不复制状态机。底部入口打开轻量菜单,“管理应用”才进入与设置相同的整窗 Modal。实现与验证的剩余范围见[实施进度](./progress.md)。
## 8. 后续产品决策
后续市场 URL、YAML 交换、ShareServer 目录与分发、插件注册及安装不进入首期。其协议、
扩展身份与安装生命周期由市场功能另行确定;内置应用常驻及排序不依赖这些决策。
### 8.1 本机推理管理
FR-16 独立于市场排期,作为可常驻的管理 Modal。所有打开入口只改变 Modal 打开状态,不改变底层工作区、项目、会话或草稿;关闭时卸载 Modal,服务生命周期独立。US-F3 只有服务列表,展示名称、状态、引擎、模型及错误。共享服务只展示一次;资源按独立服务进程归属,CPU 预热与指标不可用分别给出原因,不填 `0`,不将宿主进程占用分摊给共享线程。采样与单位见[技术设计](./technical-design.md)。ASR 没有 Worker 就绪观测,显示按需运行且状态未知;活动请求不覆盖服务状态。OCR 状态来自 Renderer Worker,向量状态来自 Broker。
US-F4 的 Modal 单任务取消入口已移除。后端取消行为保留:Worker 确认或退出前仍计入活动任务和停止影响,即使调用方已收到取消或超时错误;任务先完成则保留真实完成结果,不通过终止共享服务模拟单任务取消。
| 服务管理方式 | 可提供操作 | 结果要求(US-F5) |
| --- | --- | --- |
| GoodBuddy 受管子进程 | 引擎已支持的启动、停止、重启 | 共用同一服务实例;操作成功由真实进程和服务就绪状态确认 |
| 外部服务 | 在模型与连接设置中管理 | 不列入本机服务,不推断本机归属 |
| 进程内模型 | 引擎实际支持的加载、释放 | 不显示虚构 PID 或进程启动、停止状态 |
停止、重启或释放均先打开紧凑的嵌套影响确认,列出任务和影响;无已登记活动任务时说明执行前再次核对。取消确认不改变运行状态。
确认存在时禁止关闭底层管理 Modal;提交期间两层均不能关闭,重复操作被拒绝。未提交时 Escape 只关闭确认,确认关闭后才可用 Escape 关闭管理 Modal。焦点规则由 [UI 设计](./ui-design.md#91-本机推理管理)定义。
实际操作失败关闭旧确认、显示错误并刷新快照;刷新期间或失败时禁用服务操作。再次操作重新取得当前任务清单并要求确认,避免重复提交旧清单。
再次启动不重放已中断任务;关闭管理 Modal、关闭管理应用或取消常驻不停止服务,卸载模型文件为独立操作。
当前向量服务允许确认影响后停止,并拒绝确认清单未包含的新任务;OCR 仅空闲时允许释放。
ASR 按请求加载与释放,支持独立取消;TTS 尚无执行服务。具体适配边界以
[技术清单](./technical-design.md#111-本机推理任务与执行服务)为准。活动请求仅在影响确认中展示,主列表不展示历史。应用退出清理沿用各引擎原有路径。
## 9. 项目活动汇总
对应 [FR-14](./prd.md#fr-14-项目活动汇总),按 Conversation ID 聚合,每条会话最多一行。
数据来源为活动会话 ID、会话中的流式消息及待回答/待审批请求,以及全部可见的运行中或
等待处理 Task,以及当前应用会话内的已完成未查看集合;不能只取当前项目、当前搜索结果或每条会话的最新 Task。
| 优先级(高到低) | 条件 | 汇总类别 |
| --- | --- | --- |
| 待回答 | 消息有待回答问题 | 待处理 |
| 待审批 | 消息有待审批请求 | 待处理 |
| Task 等待处理 | Task 为 `waiting_approval`,尚无更具体的消息状态 | 待处理 |
| 运行中 | 活动会话 ID、`streaming` 消息或运行中 Task | 运行中 |
| 已完成 | 会话在已完成未查看集合中,且无更高优先级实时状态 | 已完成 |
同一会话只采用最高优先级状态;Task 的 `waiting_approval` 也可能对应提问,不能直接
标成待审批。“待处理”“运行中”和“已完成”数量互斥,全局总数为三者之和;项目计数使用相同结果。
项目归属优先取会话元数据,缺少会话时取 Task 元数据;无项目归属的活动只计入全局汇总。
历史排队项、仅待调度的 Task、终态 Task 和普通未读标记本身不构成活动。完成通知规则如下:
- 本地运行收到成功 `done`,或当前活动运行从持久化快照确认成功完成时,记录会话完成通知。
- 后台顶层 Task 只有在本次应用会话中已观察到该任务,随后状态转为 `completed` 时才记录通知;
初次载入或首次出现的已完成历史任务只建立基线,子任务不单独产生通知。
- 失败、取消不产生完成通知。实时待处理或运行中状态优先;开始新运行时清除旧完成通知,
已观察 Task 转为 `running` 或 `waiting_approval` 时也清除对应会话的旧通知。
- 通知只保存在 Renderer 内存,不跨重载或重启恢复,也不从普通未读状态推导。
- 仅当目标会话实际成为当前聊天、设置未打开且文档未隐藏时清除通知;普通侧栏导航和活动菜单
导航采用同一查看条件。已在查看的会话完成后不保留通知,恢复可见聊天时也清除通知。
- 展开活动项目、查看菜单、设置覆盖下保留挂载的聊天、隐藏文档以及取消设置离开都不算查看。
没有实时状态且没有已完成未查看通知时才移除活动行。
级联菜单先按项目归属聚合活动行,沿用本地、SSH Host、通道的稳定项目顺序;缺少项目
元数据的归属组及“未归属项目”组放在已知项目之后。无归属组只用于访问这些会话,不增加
项目计数。每个项目内依次列待处理、运行中、已完成会话,同类别保留来源顺序。
展开项目只改变菜单层级,不导航。选中项目的活动行全部移除后返回项目列表;全局无活动行后
关闭菜单并隐藏摘要入口。后台更新保留仍存在的焦点项,删除焦点项时将焦点
移至当前层可用项。关闭菜单后,新的活动只恢复摘要,不会自动重新打开菜单。
@@ -0,0 +1,154 @@
:root {
color-scheme: light;
--surface-canvas: #e8f0f5; --surface-raised: #fff; --surface-subtle: #f4f8fb;
--text-primary: #17263a; --text-secondary: #4f6278; --border-default: #cedce7;
--accent: #086fc7; --accent-subtle: #e8f4ff; --accent-selected: #a7cfea;
--font-family-ui: "Inter Variable", "PingFang SC", "Microsoft YaHei UI", "Noto Sans SC Variable", system-ui, sans-serif;
--radius-control: 10px; --radius-card: 14px;
--warning: #865200;
}
[data-theme="dark"] { --warning: #f2bd66; }
.project-lab { max-width: 1440px; margin: 0 auto 12px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; color: var(--text-secondary); font-size: 12px; }
.project-lab select { font: inherit; color: var(--text-primary); background: var(--surface-raised); padding: 4px 8px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }
.project-entry { position: relative; margin-bottom: 12px; }
#project-trigger { width: 100%; display: flex; align-items: center; gap: 8px; text-align: left; border: 1px solid var(--border-default); }
#project-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-waiting { color: var(--warning); font-weight: 600; }
#activity-trigger { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: -8px 0 12px; padding: 4px 8px; color: var(--text-secondary); font-size: 11px; text-align: left; }
#project-activity { position: fixed; inset: auto; margin: 0; padding: 0; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); color: var(--text-primary); max-width: calc(100vw - 32px); }
#project-activity:popover-open { display: flex; align-items: stretch; }
#project-activity > section { width: 264px; max-width: calc(100vw - 34px); max-height: min(480px, calc(100dvh - 32px)); overflow: auto; padding: 8px; }
#project-activity h3 { margin: 8px; font-size: 11px; font-weight: 500; color: var(--text-secondary); overflow-wrap: anywhere; }
#activity-project-list > p { padding: 8px; }
#activity-project-list button { display: grid; grid-template-columns: minmax(0, 1fr) auto; width: 100%; gap: 4px 8px; text-align: left; }
#activity-project-list button > span:first-child { overflow-wrap: anywhere; }
#activity-project-list small { grid-column: 1; font-size: 11px; color: var(--text-secondary); }
#activity-project-list button > span:last-child { grid-column: 2; grid-row: 1 / 3; align-self: center; }
#activity-project-list [aria-expanded="true"] { background: var(--accent-subtle); color: var(--accent); }
#activity-sessions { border-left: 1px solid var(--border-default); }
#activity-back { display: none; font-size: 12px; color: var(--accent); }
#project-activity.activity-left { flex-direction: row-reverse; }
.activity-left #activity-sessions { border-left: 0; border-right: 1px solid var(--border-default); }
.activity-compact.has-submenu #activity-projects { display: none; }
.activity-compact #activity-back { display: block; }
.activity-compact #activity-sessions { border: 0; }
#project-menu { position: fixed; inset: auto; margin: 0; width: min(300px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); overflow: auto; padding: 8px; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); color: var(--text-primary); }
#project-list h3, #activity-list h3 { margin: 8px; font-size: 11px; font-weight: 500; color: var(--text-secondary); }
#project-list button { width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; text-align: left; }
.project-row-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
#project-list [aria-current="true"] { background: var(--accent-subtle); color: var(--accent); }
#project-list small { width: 100%; display: flex; gap: 12px; font-size: 11px; color: var(--text-secondary); }
#activity-list button { display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; text-align: left; padding: 12px; }
#activity-list strong { font-weight: 500; overflow-wrap: anywhere; }
#activity-list small { grid-column: 1; color: var(--text-secondary); overflow-wrap: anywhere; }
.history button small { display: block; font-size: 11px; color: var(--text-secondary); }
[data-theme="dark"] { color-scheme: dark; --surface-canvas: #07131f; --surface-raised: #102335; --surface-subtle: #162c40; --text-primary: #edf7ff; --text-secondary: #b5c6d5; --border-default: #29445a; --accent: #72c5ff; --accent-subtle: #123b5b; --accent-selected: #28668f; }
* { box-sizing: border-box; }
body { margin: 0; padding: 24px 32px; font: 13px/1.6 var(--font-family-ui); background: var(--surface-subtle); color: var(--text-primary); }
button, input, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: transparent; border-radius: var(--radius-control); padding: 8px 12px; }
button:hover { background: var(--accent-subtle); color: var(--accent); }
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
button:disabled { opacity: .45; cursor: default; }
svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; vertical-align: middle; flex-shrink: 0; }
p, h1, h2 { margin: 0; } h1 { font-size: 30px; font-weight: 550; margin: 12px 0; letter-spacing: -.8px; } h2 { font-size: 24px; }
.muted, .eyebrow { color: var(--text-secondary); }.eyebrow { font-size: 11px; letter-spacing: 1px; }
.lab-header, .design-note, .lab-footer { display: flex; align-items: center; justify-content: space-between; gap: 24px; max-width: 1440px; margin: auto; }
.lab-header strong { font-size: 16px; }.lab-header p { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
.variants { display: grid; grid-template-columns: repeat(3, 1fr); padding: 4px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }
.variants button[aria-pressed="true"] { background: var(--surface-raised); color: var(--accent); box-shadow: 0 1px 3px var(--border-default); }
.design-note { margin: 24px auto 12px; font-size: 12px; }.design-note #variant-label { color: var(--accent); font-weight: 600; white-space: nowrap; }.design-note p { flex: 1; }.design-note > span:last-child { color: var(--text-secondary); font-size: 11px; }
.app { display: flex; max-width: 1440px; height: min(780px, calc(100dvh - 204px)); min-height: 590px; margin: auto; border: 1px solid var(--border-default); border-radius: var(--radius-card); overflow: hidden; background: var(--surface-raised); }
.sidebar { width: 278px; flex: 0 0 278px; background: var(--surface-canvas); display: flex; flex-direction: column; padding: 20px 12px 8px; border-right: 1px solid var(--border-default); }
.brand { display: flex; gap: 8px; align-items: center; padding: 0 8px 24px; }.brand > .muted { margin-left: auto; font-size: 11px; }.brand-mark { display: grid; place-items: center; width: 32px; height: 32px; background: var(--text-primary); color: var(--surface-raised); border-radius: 10px; font-weight: 650; font-size: 12px; }
.new-chat { border: 1px solid var(--border-default); background: var(--surface-raised); text-align: left; margin-bottom: 16px; }.new-chat span { margin-right: 8px; }
.navigation { display: grid; gap: 3px; }.navigation button, .fixed-entry button { display: flex; align-items: center; gap: 10px; text-align: left; padding: 9px 10px; }.navigation .selected { background: var(--accent-subtle); color: var(--accent); }
.history { flex: 1; min-height: 72px; overflow: auto; padding: 24px 0 8px; }.history .eyebrow { display: block; padding: 0 10px 8px; }.history button { display: block; width: 100%; text-align: left; color: var(--text-secondary); font-size: 12px; }
.fixed-entry { border-top: 1px solid var(--border-default); padding: 8px 0; }.fixed-entry button { width: 100%; }
.shortcuts { border-top: 1px solid var(--border-default); padding-top: 12px; max-height: 52%; overflow: auto; flex-shrink: 0; }.section-label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; padding-left: 8px; font-size: 11px; color: var(--text-secondary); }.section-label button { font-size: 11px; padding: 4px 8px; }
.shortcut-list { display: grid; gap: 4px; }.shortcut-item { min-width: 0; }.shortcut-button { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; }.shortcut-button.active { color: var(--accent); background: var(--accent-subtle); }.shortcut-button .icon-tile { display: grid; place-items: center; }.shortcut-button .item-name { font-size: 11px; }
[data-variant="dock"] .shortcut-list { grid-template-columns: repeat(5, minmax(0, 1fr)); background: var(--surface-raised); padding: 4px; border: 1px solid var(--border-default); border-radius: var(--radius-card); }
[data-variant="dock"] .shortcut-button { padding: 10px 0; flex-direction: column; gap: 4px; }[data-variant="dock"] .shortcut-button .item-name { display: none; }[data-variant="dock"] .shortcut-list.editing { grid-template-columns: repeat(3, minmax(0, 1fr)); }[data-variant="dock"] .editing .item-name { display: block; }
.store-link { margin-top: 8px; padding: 8px; display: flex; align-items: center; width: 100%; gap: 8px; text-align: left; font-size: 12px; }.store-link span:last-child { margin-left: auto; color: var(--text-secondary); }
[data-variant="grid"] .shortcut-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }[data-variant="grid"] .shortcut-button { flex-direction: column; gap: 4px; padding: 8px 4px; }[data-variant="grid"] .icon-tile { width: 36px; height: 36px; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); }
.featured { border: 1px solid var(--border-default); border-radius: var(--radius-control); padding: 12px; margin-top: 12px; background: var(--surface-raised); position: relative; }.featured p { font-size: 11px; color: var(--text-secondary); margin: 4px 0; }.featured .dismiss { position: absolute; right: 4px; top: 4px; padding: 0 8px; }.featured .feature-open { color: var(--accent); padding: 4px 0 0; font-size: 11px; }
[data-variant="cards"] .shortcut-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }[data-variant="cards"] .shortcut-button { background: var(--surface-raised); border: 1px solid var(--border-default); padding: 12px 8px; flex-direction: column; align-items: flex-start; }[data-variant="cards"] .shortcut-button .item-name { font-size: 12px; }.card-detail { font-size: 11px; color: var(--text-secondary); text-align: left; }.mini-meter { display: flex; gap: 3px; align-items: end; height: 24px; }.mini-meter i { display: block; width: 5px; background: var(--accent-selected); border-radius: 2px; }.mini-meter i:nth-child(3n) { background: var(--accent); }
.edit-actions { display: flex; justify-content: center; }.edit-actions button { padding: 2px 6px; font-size: 12px; }.sidebar-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-left: 8px; font-size: 11px; }
.workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; }.workspace-header { display: flex; justify-content: space-between; padding: 16px 24px; border-bottom: 1px solid var(--border-default); font-size: 12px; }.conversation { flex: 1; padding: 64px 40px 24px; max-width: 740px; width: 100%; margin: auto; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }.suggestions { display: flex; gap: 12px; margin-top: 32px; flex-wrap: wrap; }.suggestions button { border: 1px solid var(--border-default); font-size: 12px; }.suggestions span { margin-left: 24px; }
.composer-area { margin: 16px 24px 8px; flex-shrink: 0; }
.composer { padding: 12px; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-subtle); }
.composer:focus-within { border-color: var(--accent-selected); }
.composer textarea { display: block; width: 100%; height: 76px; min-height: 76px; max-height: 180px; padding: 4px; border: 0; resize: none; background: transparent; line-height: 1.8; }
.composer textarea::placeholder { color: var(--text-secondary); font-size: 12px; }
.composer-toolbar { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; margin-top: 8px; }
.composer-toolbar button { padding: 6px 8px; font-size: 12px; white-space: nowrap; }
.composer-toolbar .composer-icon { width: 32px; height: 32px; padding: 4px; font-size: 20px; }
.composer-select select { max-width: 132px; padding: 6px 2px; border: 0; background: transparent; cursor: pointer; font: inherit; font-size: 12px; color: var(--text-primary); }
select option { background: var(--surface-raised); color: var(--text-primary); }
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.composer-actions { display: flex; gap: 4px; margin-left: auto; }
#send { width: 32px; height: 32px; padding: 0; font-size: 20px; background: var(--accent); color: var(--surface-raised); }
#voice[aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent); }
.composer-meta { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-height: 32px; color: var(--text-secondary); font-size: 10px; }
.composer-meta button { font-size: 10px; padding: 4px; }
#composer-tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px 8px; max-height: 96px; overflow: auto; }
#composer-tags button { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 8px; font-size: 11px; background: var(--accent-subtle); color: var(--accent); }
#composer-options, #context-details { position: fixed; inset: auto; margin: 0; width: min(320px, calc(100vw - 32px)); max-height: min(460px, 70dvh); overflow: auto; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); box-shadow: 0 8px 32px #07131f26; font-size: 12px; }
#composer-options header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.option-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; }
.option-row select { width: 166px; padding: 6px; border: 1px solid var(--border-default); border-radius: 6px; font: inherit; background: var(--surface-subtle); color: var(--text-primary); }
#composer-options fieldset { margin: 8px 0; padding: 12px 0; border: 0; border-top: 1px solid var(--border-default); }
#composer-options fieldset label { display: inline-flex; align-items: center; gap: 4px; margin-right: 12px; }
#composer-options input { accent-color: var(--accent); }
#composer-options p { margin-top: 12px; font-size: 11px; }
#context-details p { margin: 8px 0; color: var(--text-secondary); }
#context-details progress { width: 100%; accent-color: var(--accent); }
#compact { margin-top: 8px; color: var(--accent); background: var(--accent-subtle); }
#demo-queue { max-height: 96px; overflow: auto; margin-bottom: 8px; }
.queue-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
.queue-row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-row button { flex-shrink: 0; padding: 4px; font-size: 11px; }
.workbar { width: 280px; flex-shrink: 0; border-left: 1px solid var(--border-default); background: var(--surface-subtle); }.workbar header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border-default); }#tool-content { padding: 24px 16px; }.metric { margin: 20px 0; }.metric strong { float: right; }.metric progress { width: 100%; accent-color: var(--accent); }.file-row { padding: 12px 0; border-bottom: 1px solid var(--border-default); }pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }
.lab-footer { margin-top: 12px; font-size: 12px; color: var(--text-secondary); }
dialog { width: min(580px, calc(100% - 32px)); max-height: 85dvh; border: 1px solid var(--border-default); padding: 24px; border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); }dialog::backdrop { background: #07131f80; }dialog header { display: flex; justify-content: space-between; align-items: start; }dialog > p { margin: 12px 0 16px; }dialog input { width: 100%; padding: 10px 12px; background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-control); }.catalog-row { display: flex; align-items: center; gap: 12px; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.catalog-row > div { flex: 1; }.catalog-row p { font-size: 11px; color: var(--text-secondary); }.catalog-row button { color: var(--accent); background: var(--accent-subtle); font-size: 12px; }dialog footer { font-size: 11px; color: var(--text-secondary); margin-top: 16px; }
#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); padding: 10px 20px; border-radius: var(--radius-control); background: var(--text-primary); color: var(--surface-raised); pointer-events: none; opacity: 0; }#toast.visible { opacity: 1; }
[hidden] { display: none !important; }
#page-actions { margin-left: auto; }#page-actions button { padding: 4px 8px; font-size: 12px; }.note-count { margin-left: auto; padding: 0 6px; background: var(--accent-subtle); border-radius: var(--radius-control); color: var(--accent); }.note-setting-row { display: flex; gap: 16px; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.note-setting-row p, .note-setting-options p { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }.demo-switch { width: 36px; height: 20px; border-radius: 20px; border: 1px solid var(--text-secondary); background: var(--surface-canvas); padding: 2px; flex-shrink: 0; }.demo-switch::after { content: ''; display: block; width: 14px; height: 14px; border-radius: 50%; background: var(--text-secondary); }.demo-switch[aria-checked="true"] { background: var(--accent); border-color: var(--accent); }.demo-switch[aria-checked="true"]::after { margin-left: 16px; background: var(--surface-raised); }.note-setting-options { border: 0; padding: 16px 0; margin: 0; min-width: 0; }.note-setting-options legend { padding-top: 16px; font-weight: 550; }.note-segments { display: flex; padding: 4px; gap: 4px; background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-control); }.note-segments button { flex: 1; padding: 8px 4px; font-size: 12px; }.note-segments [aria-pressed="true"] { color: var(--accent); background: var(--surface-raised); box-shadow: 0 1px 3px var(--border-default); }.note-settings-preview { padding: 16px; background: var(--accent-subtle); border-radius: var(--radius-control); }.note-settings-preview small { color: var(--text-secondary); font-size: 11px; }#back-to-apps { margin-bottom: 12px; padding-left: 0; color: var(--accent); }#note-settings-summary { margin: 16px 0; }
.sidebar-footer { border-top: 1px solid var(--border-default); padding: 8px 0 0; gap: 4px; }#center-entry { flex: 1; min-width: 0; }#open-center { width: 100%; display: flex; align-items: center; gap: 10px; padding: 8px; text-align: left; }#open-center strong { font-size: 13px; font-weight: 550; }#open-center small { display: block; font-size: 11px; color: var(--text-secondary); }.entry-arrow { margin-left: auto; }.tool-tabs { display: flex; padding: 8px; gap: 2px; }.tool-tabs button { flex: 1; padding: 8px 4px; font-size: 12px; }.tool-tabs [aria-selected="true"] { background: var(--accent-subtle); color: var(--accent); }.center-switch { display: flex; gap: 4px; padding-bottom: 12px; }.center-switch [aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent); }.center-app { display: flex; gap: 12px; align-items: start; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.center-app > svg { margin-top: 4px; color: var(--accent); }.center-app-info { flex: 1; }.center-app p { color: var(--text-secondary); font-size: 11px; }.center-actions { display: flex; gap: 4px; flex-wrap: wrap; }.center-actions button { font-size: 11px; background: var(--accent-subtle); color: var(--accent); }.center-actions [aria-pressed="true"] { border: 1px solid var(--accent-selected); }
dialog[data-variant="dock"], dialog[data-variant="grid"], dialog[data-variant="favorites"] { position: fixed; left: 48px; bottom: 100px; top: auto; right: auto; margin: 0; width: min(520px, calc(100% - 32px)); max-height: 72dvh; }
dialog[data-variant="grid"] #catalog-items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }dialog[data-variant="grid"] .center-app { flex-direction: column; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }dialog[data-variant="cards"] .center-app { padding: 24px 0; }dialog[data-variant="cards"] .center-app > svg { width: 32px; height: 32px; }dialog[data-variant="cards"] .app-detail { margin-top: 8px; font-size: 13px; }dialog[data-variant="favorites"] .app-detail { display: none; }dialog[data-variant="scenes"] .center-app { background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-card); padding: 20px; margin-top: 16px; flex-wrap: wrap; }dialog[data-variant="scenes"] .center-actions { width: 100%; justify-content: flex-end; }
@media (max-width: 640px) { dialog[data-variant] { left: 16px; right: 16px; bottom: 16px; top: auto; margin: 0; max-height: 85dvh; width: calc(100% - 32px); padding: 16px; }.center-app { flex-wrap: wrap; }.center-actions { width: 100%; }.app { flex-direction: column; }.app .sidebar { flex: 1; min-height: 0; }.app .workspace { display: block; flex: 0 0 auto; }.app .workspace .conversation, .app .workspace .composer { display: none; }.app .workspace-header { padding: 8px 16px; align-items: center; }.app .workbar { z-index: 2; } }
.reference-note { max-width: 1440px; margin: 12px auto 0; display: flex; flex-wrap: wrap; gap: 8px 16px; color: var(--text-secondary); font-size: 11px; }.reference-note a { color: var(--accent); text-underline-offset: 3px; }
.sidebar > * { flex-shrink: 0; }.sidebar .history { flex: 1 1 80px; min-height: 0; }.sidebar .shortcuts { flex-shrink: 1; min-height: 80px; }
.sidebar .resident-apps { margin-top: 12px; min-height: 96px; flex-shrink: 1; display: flex; flex-direction: column; max-height: 240px; }.resident-apps .section-label { margin-bottom: 4px; flex-shrink: 0; }#resident-list { overflow: auto; min-height: 0; }.resident-row { display: flex; align-items: center; }.resident-row > button { display: flex; align-items: center; gap: 10px; padding: 8px 10px; flex: 1; min-width: 0; text-align: left; }.resident-row > button span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.resident-row [aria-current="page"] { background: var(--accent-subtle); color: var(--accent); }.resident-row[draggable="true"] { cursor: grab; }.resident-row .edit-actions { flex-shrink: 0; }.all-apps { display: flex; justify-content: space-between; padding: 6px 10px; font-size: 11px; color: var(--text-secondary); flex-shrink: 0; }.empty-apps { padding: 8px 10px; font-size: 11px; color: var(--text-secondary); }.app-placement { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }.app-placement button { font-size: 11px; padding: 6px 8px; }.app-placement button[aria-pressed="true"] { border: 1px solid var(--accent-selected); }.app-catalog-row { align-items: flex-start; }.app-catalog-row > svg { margin-top: 4px; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.workspace { min-height: 0; }
.workspace-header { flex: 0 0 auto; flex-wrap: wrap; gap: 8px; align-items: center; }
#page-label { overflow-wrap: anywhere; }
.conversation { min-height: 0; overflow: auto; }
.conversation.app-page { justify-content: flex-start; padding: 24px; max-width: 960px; gap: 12px; }
.app-page h1 { font-size: 24px; }
.model-list { width: 100%; }
.model-service { padding: 16px 0; border-bottom: 1px solid var(--border-default); }
.model-service p, .model-service details { color: var(--text-secondary); font-size: 12px; }
.model-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }
.model-actions button, .inline-confirm button { border: 1px solid var(--border-default); }
.inline-confirm { padding: 12px; margin: 8px 0; background: var(--accent-subtle); border: 1px solid var(--accent-selected); border-radius: var(--radius-control); }
#market-preview { padding: 12px 0; border-bottom: 1px solid var(--border-default); }
#market-preview label { display: block; margin: 8px 0; }
#market-preview textarea, #market-preview select { width: 100%; padding: 8px; font: inherit; background: var(--surface-subtle); color: var(--text-primary); border: 1px solid var(--border-default); border-radius: var(--radius-control); }
#market-preview textarea { resize: vertical; }
#market-preview p, #import-preview { font-size: 12px; overflow-wrap: anywhere; }
#market-preview summary, .model-service summary { cursor: pointer; }
.note-segments, .center-switch, .tool-tabs { flex: 0 0 auto; }
.center-app-info { min-width: 0; overflow-wrap: anywhere; }
dialog, dialog::backdrop { -webkit-app-region: no-drag; }
.launcher-trigger { display: flex; align-items: center; gap: 8px; width: 100%; padding: 12px 8px; border: 1px solid var(--border-default); background: var(--surface-raised); margin-bottom: 12px; font-size: 12px; }.launcher-trigger kbd { margin-left: auto; font: 10px var(--font-family-ui); color: var(--text-secondary); white-space: nowrap; }
[data-variant="launcher"] .shortcut-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }[data-variant="launcher"] .shortcut-button { flex-direction: column; padding: 8px 0; }[data-variant="launcher"] .icon-tile { height: 24px; }[data-variant="launcher"] .store-link { border-top: 1px solid var(--border-default); border-radius: 0; }
[data-variant="favorites"] .section-label { padding-left: 0; }[data-variant="favorites"] .shortcut-list { gap: 0; }[data-variant="favorites"] .shortcut-button { justify-content: flex-start; padding: 8px 12px; }[data-variant="favorites"] .item-name { font-size: 12px; }[data-variant="favorites"] .shortcut-item { display: flex; align-items: center; }[data-variant="favorites"] .edit-actions { margin-left: auto; }[data-variant="favorites"] .store-link { margin-top: 12px; border: 1px dashed var(--border-default); }.count { padding: 0 6px; color: var(--text-secondary); }
.scene-banner { display: flex; align-items: center; gap: 12px; margin: 0 4px 12px; padding: 12px; background: var(--accent-subtle); border-radius: var(--radius-control); }.scene-banner > svg { color: var(--accent); width: 24px; height: 24px; }.scene-banner .eyebrow { letter-spacing: 0; font-size: 10px; }.scene-count { margin-left: auto; font-size: 11px; color: var(--text-secondary); }.scene-label { display: block; }.scene-label select { font: inherit; font-weight: 600; color: var(--text-primary); background: transparent; border: 0; padding: 0; cursor: pointer; }.scene-label option { background: var(--surface-raised); }
[data-variant="scenes"] .shortcut-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }[data-variant="scenes"] .shortcut-button { border: 1px solid var(--border-default); background: var(--surface-raised); padding: 10px 8px; justify-content: flex-start; }.pack-preview { display: flex; gap: 8px; align-items: center; width: 100%; margin-top: 12px; padding: 12px 8px; text-align: left; background: var(--surface-raised); border: 1px solid var(--border-default); }.pack-preview strong { font-size: 11px; font-weight: 550; }.pack-preview small { display: block; color: var(--text-secondary); font-size: 10px; }.pack-preview > span:last-child { margin-left: auto; }.pack-symbol { color: var(--accent); }
.catalog-pack { padding: 20px 0; border-bottom: 1px solid var(--border-default); }.catalog-pack h3 { margin: 8px 0; font-size: 18px; font-weight: 550; }.pack-art { display: grid; place-items: center; float: right; width: 56px; height: 56px; border: 1px solid var(--accent-selected); background: var(--accent-subtle); color: var(--accent); border-radius: var(--radius-card); }.pack-art svg { width: 28px; height: 28px; }.pack-includes { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }.pack-includes span { padding: 4px 8px; font-size: 11px; background: var(--surface-subtle); border-radius: var(--radius-control); }.pack-includes svg { width: 14px; height: 14px; }.catalog-pack > button { border: 1px solid var(--accent-selected); color: var(--accent); background: var(--accent-subtle); }
dialog[data-mode="launch"] { width: min(640px, calc(100% - 32px)); }dialog[data-mode="launch"] .catalog-row:focus-within { background: var(--accent-subtle); }.catalog-row { padding-left: 8px; padding-right: 8px; }
@media (max-width: 1080px) { body { padding: 16px; }.app { position: relative; }.workbar { position: absolute; right: 0; top: 52px; bottom: 0; width: min(320px, 100%); box-shadow: -8px 0 24px var(--border-default); }.lab-header { flex-wrap: wrap; gap: 12px; }.design-note > span:last-child { display: none; }.conversation { padding: 32px 24px; } }
@media (max-width: 640px) { body { padding: 12px; }.lab-header > div:first-child { flex: 1; }.variants { order: 3; width: 100%; }.variants button { flex: 1; padding: 8px 4px; font-size: 12px; }.design-note { align-items: flex-start; gap: 12px; }.app { height: auto; min-height: 0; }.app .sidebar { width: 100%; flex: 0 0 auto; border: 0; padding-top: 12px; }.brand { padding-bottom: 12px; }.app .history { display: none; }.app .workspace { display: flex; min-height: 320px; border-top: 1px solid var(--border-default); }.app .workspace .conversation { display: flex; overflow: visible; padding: 16px; }.app .workspace .composer { display: block; }.composer-area { margin: 12px 12px 4px; margin-top: auto; }.composer-toolbar { gap: 2px; }.composer-select select { max-width: 108px; font-size: 11px; }.composer-toolbar button { font-size: 11px; padding: 6px; }.composer-toolbar .composer-icon { width: 28px; }.composer-meta { flex-wrap: wrap; gap: 0; }.shortcut-list { max-height: 300px; overflow: auto; }.lab-footer { align-items: flex-start; }.lab-footer button { white-space: nowrap; }.workbar { top: 0; width: 100%; }.design-note p { font-size: 11px; } }
@@ -0,0 +1,92 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoodBuddy · 应用中心设计实验</title>
<link rel="stylesheet" href="navigation-demo.css">
<script src="navigation-demo.js" defer></script>
</head>
<body>
<header class="lab-header">
<div><strong>GoodBuddy <span class="muted">/ 设计实验</span></strong><p>左下角应用中心 · 六个交互方案</p></div>
<div class="variants" role="group" aria-label="选择设计方案">
<button data-variant="dock" aria-pressed="true">A · 紧凑入口</button>
<button data-variant="grid" aria-pressed="false">B · 应用宫格</button>
<button data-variant="cards" aria-pressed="false">C · 应用预览</button>
<button data-variant="launcher" aria-pressed="false">D · 搜索启动台</button>
<button data-variant="favorites" aria-pressed="false">E · 常驻管理</button>
<button data-variant="scenes" aria-pressed="false">F · 私有市场预览</button>
</div>
<button id="theme">切换深色</button>
</header>
<div class="design-note"><span id="variant-label">A / 紧凑 Dock</span><p id="variant-description"></p><span>交互原型 · 全部状态仅在内存中,刷新重置</span></div>
<div class="project-lab"><label>项目活动演示 <select id="project-scenario"><option value="waiting">有待处理</option><option value="running">仅运行中</option><option value="current">仅当前项目运行</option><option value="idle">全部空闲</option></select></label><span>有活动时显示摘要,悬停或点击项目向右展开会话。</span></div>
<main class="app" data-variant="dock">
<aside class="sidebar" aria-label="主侧栏">
<div class="brand"><span class="brand-mark">GB</span><strong>GoodBuddy</strong><span class="muted">个人空间</span></div>
<div class="project-entry"><button id="project-trigger" popovertarget="project-menu" aria-expanded="false" aria-controls="project-menu"><span id="project-name">GoodBuddy</span><span class="muted" aria-hidden="true">⌄</span></button></div>
<button id="activity-trigger" popovertarget="project-activity" aria-expanded="false" aria-controls="project-activity"><span id="activity-summary"></span><span aria-hidden="true">⌄</span></button>
<div id="project-menu" popover aria-label="切换项目"><div id="project-list"></div></div>
<div id="project-activity" popover aria-label="所有项目活动"><section id="activity-projects"><h3>所有项目活动</h3><div id="activity-project-list"></div></section><section id="activity-sessions" hidden><button id="activity-back">‹ 返回项目列表</button><h3 id="activity-project-title"></h3><div id="activity-list"></div></section></div>
<button class="new-chat" data-page="当前对话"><span>+</span> 新对话</button>
<nav class="navigation" aria-label="应用导航">
<button class="selected" data-page="当前对话"><span data-icon="chat"></span>当前对话</button>
</nav>
<section class="resident-apps" aria-label="我的应用"><div class="section-label"><span>我的应用</span><div><button id="edit-apps">编辑</button><button data-catalog="apps" aria-label="管理常驻应用">+</button></div></div><nav id="resident-list" aria-label="常驻应用"></nav><button class="all-apps" data-catalog="apps">全部应用 <span>›</span></button></section>
<div class="history"><span class="eyebrow">最近对话</span><button data-page="整理产品设计思路">整理产品设计思路</button><button data-page="本周工作计划">本周工作计划</button><button data-page="项目文档检索">项目文档检索</button></div>
<div class="fixed-entry"><button data-page="运行记录"><span data-icon="history"></span>运行记录</button></div>
<div class="sidebar-footer"><div id="center-entry"></div><button data-page="设置" aria-label="设置" title="设置"><span data-icon="settings"></span></button></div>
</aside>
<section class="workspace">
<header class="workspace-header"><span id="page-label">当前对话</span><div id="page-actions"></div><button id="toggle-workbar" aria-expanded="true" aria-controls="workbar">助手工作栏</button></header>
<div class="conversation" id="page-content">
<span class="eyebrow">LET'S MAKE SOMETHING</span>
<h1>从一个想法开始。</h1>
<p class="muted">对话留在这里,常用能力随手打开。</p>
<div class="suggestions"><button data-page="项目文档检索">查找项目资料 <span>↗</span></button><button data-page="整理产品设计思路">梳理一个想法 <span>↗</span></button></div>
</div>
<section class="composer-area" aria-label="会话输入">
<div id="demo-queue" aria-label="待发送消息" hidden></div>
<div class="composer">
<div id="composer-tags" aria-label="当前附件与选项" hidden></div>
<textarea id="message" aria-label="演示消息输入" placeholder="向 GoodBuddy 提问…&#10;Enter 发送 · Shift+Enter 换行"></textarea>
<div class="composer-toolbar">
<button id="attach" class="composer-icon" aria-label="添加附件" title="添加附件">+</button>
<input id="attachment-input" type="file" multiple hidden>
<button id="voice" class="composer-icon" aria-label="模拟语音输入" aria-pressed="false" title="模拟语音输入"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 10v2a7 7 0 0 0 14 0v-2M12 19v3m-4 0h8"/></svg></button>
<label class="composer-select"><span class="sr-only">Runtime</span><select id="composer-runtime"><option>OpenCode</option><option>Continue</option><option>直连模型</option></select></label>
<label class="composer-select"><span class="sr-only">工作模式</span><select id="composer-mode"><option value="Execute">Execute</option><option value="Ask">Ask</option></select></label>
<button id="composer-options-trigger" popovertarget="composer-options" aria-expanded="false">选项 <span class="muted">⌃</span></button>
<div class="composer-actions"><button id="stop" hidden aria-label="停止演示回复">停止</button><button id="send" aria-label="发送演示消息" title="发送" disabled>↑</button></div>
</div>
<div id="composer-options" popover aria-label="会话选项">
<header><strong>会话选项</strong><button popovertarget="composer-options" popovertargetaction="hide" aria-label="关闭会话选项">×</button></header>
<label class="option-row">专家<select id="composer-expert"><option value="">默认助手</option><option>产品顾问</option><option>代码审查专家</option></select></label>
<fieldset data-knowledge-control><legend>知识范围</legend><label><input type="checkbox" data-knowledge="产品文档"> 产品文档</label><label><input type="checkbox" data-knowledge="团队知识"> 团队知识</label></fieldset>
<label class="option-row" data-knowledge-control>检索方式<select id="composer-retrieval"><option value="auto">自动检索</option><option value="always">每次检索</option></select></label>
<div id="runtime-options"><label class="option-row"><span id="agent-label">Agent</span><select id="composer-agent"><option value="">默认</option><option value="plan">Plan</option><option value="review">Review</option></select></label><label class="option-row">快捷操作<select id="composer-command"><option value="">无</option><option value="prompt">填入总结模板</option><option value="command">原生命令 /init</option></select></label></div>
<p class="muted">非默认选项会显示在输入框上方。</p>
</div>
</div>
<div class="composer-meta"><span id="composer-status" role="status">交互演示 · 不调用 AI</span><button id="context-trigger" popovertarget="context-details">上下文 <span id="context-value">42%</span></button></div>
<div id="context-details" popover aria-label="上下文用量"><strong>上下文用量 · 示例</strong><p id="context-description">本次模型调用:84k / 200k tokens</p><progress id="context-progress" value="42" max="100"></progress><button id="compact">压缩上下文</button></div>
</section>
</section>
<aside class="workbar" id="workbar" aria-label="助手工作栏"><header><strong>助手工作栏</strong><button id="close-tool" aria-label="关闭工作栏">×</button></header><div class="tool-tabs" role="tablist" aria-label="系统工具"><button role="tab" id="tab-files" data-tool="files" aria-selected="true" aria-controls="tool-content">文件</button><button role="tab" id="tab-terminal" data-tool="terminal" aria-selected="false" aria-controls="tool-content" tabindex="-1">终端</button><button role="tab" id="tab-browser" data-tool="browser" aria-selected="false" aria-controls="tool-content" tabindex="-1">浏览器</button><button role="tab" id="tab-resources" data-tool="resources" aria-selected="false" aria-controls="tool-content" tabindex="-1">资源</button></div><div id="tool-content" role="tabpanel" aria-labelledby="tab-files" tabindex="0"></div></aside>
</main>
<footer class="lab-footer"><span id="design-tradeoff"></span><button id="reset">恢复默认应用</button></footer>
<div class="reference-note">设计参考:<a href="https://www.raycast.com/" target="_blank" rel="noreferrer">Raycast · 搜索与扩展</a><a href="https://www.notion.com/help/intro-to-workspaces" target="_blank" rel="noreferrer">Notion · 收藏与折叠分区</a><a href="https://code.visualstudio.com/docs/configure/profiles" target="_blank" rel="noreferrer">VS Code · Profiles</a><span>借鉴交互思路,保留 GoodBuddy 的视觉与入口分类。</span></div>
<dialog id="catalog" aria-labelledby="catalog-title"><button id="back-to-apps" hidden>‹ 返回应用中心</button><header><div><span class="eyebrow">GOODBUDDY COLLECTION</span><h2 id="catalog-title">应用中心</h2></div><button id="close-catalog" aria-label="关闭">×</button></header><p class="muted" id="catalog-description"></p><div class="center-switch" role="group" aria-label="应用中心视图" hidden><button data-catalog="apps">我的应用</button><button data-catalog="store">未来私有市场</button></div><section id="market-preview" hidden>
<label>私有市场 URL<input id="market-url" type="url" value="https://market.example.invalid" aria-describedby="market-status"></label><button id="market-fetch">模拟获取目录</button><button id="market-fail">模拟不可达</button><p id="market-status" role="status">未来预览,不发送网络请求;来源仅用于市场。</p>
<details><summary>YAML 导入 / 导出预览(mock schema)</summary><p class="muted">仅支持下列扁平示例字段,非正式 Schema。支持 .yml / .yaml 的文本预览;不包含凭据或业务数据,不替代程序包或模型权重。</p><label>应用定义<textarea id="yaml-input" rows="9" spellcheck="false">schema: mock-v1
id: reading
name: 阅读空间
version: 1.0
entry: main
dependencies: builtin:knowledge
resources: bundled</textarea></label><button id="yaml-preview">预览导入</button><label>导出应用<select id="yaml-app"></select></label><button id="yaml-export">预览导出 YAML</button><pre id="yaml-output" tabindex="0" hidden></pre><div id="import-preview" hidden></div></details>
</section><input id="search" type="search" aria-label="搜索应用" placeholder="搜索应用…"><div id="catalog-items"></div><section id="app-settings" aria-label="应用设置" hidden></section><footer>取消常驻仅隐藏捷径;关闭应用隐藏业务入口,保留数据与后台状态。此处全部为模拟。</footer></dialog>
<div id="toast" role="status" aria-live="polite"></div>
</body>
</html>
@@ -0,0 +1,548 @@
/* global document, window, history, location, matchMedia, innerWidth, Event, setTimeout, clearTimeout */
const paths = {
note: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/>',
book: '<path d="M12 5v16M12 5C8 2 4 3 2 4v15c4-1 7-1 10 2 3-3 6-3 10-2V4c-2-1-6-2-10 1Z"/>',
chat: '<path d="M21 11a9 9 0 0 1-9 9H3l1-5A9 9 0 1 1 21 11Z"/>',
history: '<path d="M3 4v5h5M3 9a9 9 0 1 1 0 6m9-8v5l3 2"/>',
settings: '<circle cx="12" cy="12" r="4"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M5 5l2 2m10 10 2 2M5 19l2-2M17 7l2-2"/>',
activity: '<path d="M2 12h5l3-8 4 16 3-8h5"/>',
layers: '<path d="m12 3 10 5-10 5L2 8Zm-10 9 10 5 10-5M2 17l10 5 10-5"/>',
search: '<circle cx="10" cy="10" r="6"/><path d="m15 15 6 6"/>'
};
const icon = name => `<svg viewBox="0 0 24 24" aria-hidden="true">${paths[name] || paths.layers}</svg>`;
const items = [
{ id: 'notes', name: '魔法笔记', icon: 'note', detail: '记录想法、整理待办', summary: '把刚才的灵感留在这里', builtin: true },
{ id: 'knowledge', name: '知识库', icon: 'book', detail: '整理资料、检索知识', summary: '下一次提问,从已有知识开始', builtin: true },
{ id: 'heartbeat', name: '智能心跳', icon: 'activity', detail: '管理定时任务与主动提醒', summary: '让例行任务按计划发生', builtin: true },
{ id: 'models', name: '本机推理监控', icon: 'layers', detail: '语音、OCR、向量任务统一管理 · 计划预览', summary: '查看本机推理任务、使用方和资源占用', builtin: true },
{ id: 'projects', name: '项目管理', icon: 'layers', detail: '跟踪项目、任务与进度', summary: '把一个想法变成可跟进的项目', builtin: false },
{ id: 'reading', name: '阅读空间', icon: 'book', detail: '集中阅读、批注与摘录', summary: '给长文和深度阅读留一个空间', builtin: false }
];
const variants = {
dock: ['A / 紧凑入口', '最底部保留一个应用中心入口,向上展开紧凑列表。', '适合希望把侧栏空间留给对话的用户。'],
grid: ['B / 应用宫格', '底部入口展示应用数量,展开后用宫格浏览已安装应用。', '图标与名称同时可见,常驻位置由用户选择。'],
cards: ['C / 应用预览', '入口展示内置应用图标,展开后用卡片说明每个应用的用途。', '适合通过内容理解应用,卡片不代表后台持续运行。'],
launcher: ['D / 搜索启动台', '最底部就是搜索入口,也可以按 Ctrl / ⌘ K 打开。', '输入名称,用方向键与 Enter 打开已安装应用。'],
favorites: ['E / 常驻管理', '从底部管理哪些应用常驻菜单,未常驻应用也可直接打开。', '常驻应用优先排列;入口显隐不影响安装和运行状态。'],
scenes: ['F / 私有市场预览', '未来实验:私有市场 URL、模拟目录获取与 YAML 交换。', '不联网、不写入文件;mock schema 不代表正式协议。']
};
const $ = selector => document.querySelector(selector);
const demoProjects = [
{ id: 'goodbuddy', name: 'GoodBuddy', group: '本地', conversations: ['优化项目导航', '检查构建问题'] },
{ id: 'website', name: '官网改版', group: '本地', conversations: ['发布首页'] },
{ id: 'remote', name: '客户方案与交付文档工作空间', group: 'SSH', conversations: ['整理报价'] },
{ id: 'channel', name: '团队助手', group: '通道', conversations: ['本周工作计划'] }
];
let currentProject = 'goodbuddy';
let projectActivity = [];
let expandedActivityProject = '';
function renderProjects() {
const scenario = $('#project-scenario').value;
const activity = demoProjects.flatMap(project => project.conversations.map((name, index) => ({ project, name, index,
state: scenario === 'idle' || project.id === 'channel' || (scenario === 'current' && project.id !== currentProject) ? '' : scenario === 'waiting' && project.id === 'website' ? '等待审批' : '运行中'
}))).filter(row => row.state);
projectActivity = activity;
const waiting = activity.filter(row => row.state !== '运行中').length;
const running = activity.length - waiting;
const project = demoProjects.find(entry => entry.id === currentProject);
$('#project-name').textContent = project.name;
$('#project-trigger').setAttribute('aria-label', `切换项目,当前项目:${project.name}`);
$('#activity-trigger').hidden = !activity.length;
if (!activity.length && $('#project-activity').matches(':popover-open')) $('#project-activity').hidePopover();
$('#activity-summary').innerHTML = `${running ? `运行中 ${running}` : ''}${waiting && running ? ' · ' : ''}${waiting ? `<span class="project-waiting">待处理 ${waiting}</span>` : ''}`;
$('#activity-trigger').setAttribute('aria-label', `所有项目活动:${$('#activity-summary').textContent}`);
$('#project-list').innerHTML = ['本地', 'SSH', '通道'].map(group => `<section><h3>${group}</h3>${demoProjects.filter(entry => entry.group === group).map(entry => {
const rows = activity.filter(row => row.project.id === entry.id);
const pending = rows.filter(row => row.state !== '运行中').length;
const active = rows.length - pending;
return `<button data-project="${entry.id}" ${entry.id === currentProject ? 'aria-current="true"' : ''}><span class="project-row-name">${entry.name}</span>${entry.id === currentProject ? '<span aria-hidden="true">✓</span>' : ''}${rows.length ? `<small>${pending ? `<span class="project-waiting">待处理 ${pending}</span>` : ''}${active ? `<span>运行中 ${active}</span>` : ''}</small>` : ''}</button>`;
}).join('')}</section>`).join('');
$('#activity-project-list').innerHTML = demoProjects.map(entry => {
const rows = activity.filter(row => row.project.id === entry.id);
if (!rows.length) return '';
const pending = rows.filter(row => row.state !== '运行中').length;
return `<button data-activity-project="${entry.id}" aria-expanded="false" aria-controls="activity-sessions"><span>${entry.name}</span><small>${pending ? `<span class="project-waiting">待处理 ${pending}</span>` : `运行中 ${rows.length}`}</small><span aria-hidden="true">›</span></button>`;
}).join('') || '<p class="muted">暂无运行中或待处理会话。</p>';
expandActivityProject('');
$('.history').innerHTML = `<span class="eyebrow">当前项目对话</span>${project.conversations.map((name, index) => {
const row = activity.find(entry => entry.project.id === currentProject && entry.index === index);
return `<button data-conversation="${currentProject}" data-conversation-index="${index}">${name}${row ? `<small>${row.state}</small>` : ''}</button>`;
}).join('')}`;
}
$('#project-scenario').onchange = renderProjects;
const projectMenu = $('#project-menu');
function positionProjectMenu() {
const rect = $('#project-trigger').getBoundingClientRect();
projectMenu.style.left = `${Math.max(16, Math.min(rect.left, innerWidth - projectMenu.offsetWidth - 16))}px`;
projectMenu.style.top = `${Math.max(16, Math.min(rect.bottom + 8, window.innerHeight - projectMenu.offsetHeight - 16))}px`;
}
projectMenu.addEventListener('toggle', event => {
$('#project-trigger').setAttribute('aria-expanded', String(event.newState === 'open'));
if (event.newState === 'open') positionProjectMenu();
});
window.addEventListener('resize', () => { if (projectMenu.matches(':popover-open')) positionProjectMenu(); });
window.addEventListener('scroll', () => { if (projectMenu.matches(':popover-open')) positionProjectMenu(); }, true);
function positionActivityMenu() {
const panel = $('#project-activity');
const rect = $('#activity-trigger').getBoundingClientRect();
panel.classList.toggle('activity-compact', innerWidth < 620);
panel.classList.toggle('activity-left', innerWidth >= 620 && rect.left + panel.offsetWidth > innerWidth - 16 && rect.right >= panel.offsetWidth + 16);
const left = panel.classList.contains('activity-left') ? rect.right - panel.offsetWidth : rect.left;
panel.style.left = `${Math.max(16, Math.min(left, innerWidth - panel.offsetWidth - 16))}px`;
panel.style.top = `${Math.max(16, Math.min(rect.bottom + 4, window.innerHeight - panel.offsetHeight - 16))}px`;
}
function expandActivityProject(id, focus = false) {
expandedActivityProject = id;
$('#activity-sessions').hidden = !id;
$('#project-activity').classList.toggle('has-submenu', Boolean(id));
document.querySelectorAll('[data-activity-project]').forEach(button => button.setAttribute('aria-expanded', String(button.dataset.activityProject === id)));
if (id) {
$('#activity-project-title').textContent = demoProjects.find(project => project.id === id).name;
$('#activity-list').innerHTML = projectActivity.filter(row => row.project.id === id).sort((a, b) => Number(a.state === '运行中') - Number(b.state === '运行中')).map(row => `<button data-conversation="${id}" data-conversation-index="${row.index}"><strong>${row.name}</strong><small>${row.state}</small></button>`).join('');
}
if ($('#project-activity').matches(':popover-open')) positionActivityMenu();
if (focus) $('#activity-list button')?.focus();
}
$('#project-activity').addEventListener('toggle', event => {
$('#activity-trigger').setAttribute('aria-expanded', String(event.newState === 'open'));
if (event.newState === 'open') { expandActivityProject(''); positionActivityMenu(); }
});
$('#activity-project-list').addEventListener('pointerover', event => {
const button = event.target.closest('[data-activity-project]');
if (event.pointerType === 'mouse' && innerWidth >= 620 && button && button.dataset.activityProject !== expandedActivityProject) expandActivityProject(button.dataset.activityProject);
});
$('#activity-project-list').addEventListener('click', event => {
const button = event.target.closest('[data-activity-project]');
if (button) expandActivityProject(button.dataset.activityProject, true);
});
$('#activity-back').onclick = () => {
const id = expandedActivityProject;
expandActivityProject('');
$(`[data-activity-project="${id}"]`)?.focus();
};
$('#project-activity').addEventListener('keydown', event => {
const button = event.target.closest('[data-activity-project]');
if (event.key === 'ArrowRight' && button) { event.preventDefault(); expandActivityProject(button.dataset.activityProject, true); }
if ((event.key === 'ArrowLeft' || event.key === 'Escape') && expandedActivityProject) { event.preventDefault(); event.stopPropagation(); $('#activity-back').click(); }
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
const buttons = [...document.querySelectorAll(button ? '[data-activity-project]' : '#activity-list button')];
const index = buttons.indexOf(document.activeElement);
if (index >= 0) { event.preventDefault(); buttons[(index + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % buttons.length].focus(); }
}
});
window.addEventListener('resize', () => { if ($('#project-activity').matches(':popover-open')) positionActivityMenu(); });
window.addEventListener('scroll', () => { if ($('#project-activity').matches(':popover-open')) positionActivityMenu(); }, true);
renderProjects();
let variant = 'dock';
let catalogMode = 'apps';
let residentApps = items.filter(item => item.builtin && item.id !== 'models').map(item => item.id);
let enabledApps = new Set(items.filter(item => item.builtin).map(item => item.id));
let currentPage = '当前对话';
let settingsApp = 'notes';
let marketReady = false;
let importDraft = null;
const definitions = new Map();
const modelServices = [
{ id: 'ocr', name: 'OCR 文字识别', purpose: '文档入库 #12 · 正在识别第 8 / 24 页', engine: 'PaddleOCR worker', management: '受管进程', running: true, usage: '420 MB', users: '知识库 / 文档入库任务 #12' },
{ id: 'speech', name: '语音识别', purpose: '语音输入 #18 · 正在转写', engine: '本机语音识别引擎', management: '受管进程', running: true, usage: '310 MB', users: '当前会话 / 语音输入 #18' },
{ id: 'tts', name: '语音合成', purpose: '回复朗读 #19 · 正在合成', engine: '本机语音合成引擎', management: '进程内', running: true, usage: '180 MB', users: '当前会话 / 回复朗读 #19' },
{ id: 'embedding', name: '向量生成', purpose: '文档向量化 #7 · 已处理 32 / 80 个片段', engine: 'BGE-small / ONNX Runtime', management: '进程内', running: true, usage: '128 MB', users: '知识库 / 文档向量化任务 #7' }
];
let modelConfirmation = null;
let editingApps = false;
let draggedApp = '';
let installed = new Set(items.filter(item => item.builtin).map(item => item.id));
let toastTimer;
let settingsOpen = false;
const noteSettings = { showCount: true, mode: 'immediate', format: 'combined' };
const noteModes = { immediate: '即时', 'after-save-auto': '保存后自动', 'after-save-manual': '保存后手动' };
const noteFormats = { combined: '长评 + 要点', narrative: '长评', structured: '要点' };
function renderAppSettings() {
const item = items.find(entry => entry.id === settingsApp);
$('#app-settings').innerHTML = `<div class="note-setting-row"><div><strong>启用应用</strong><p>关闭后隐藏业务入口,保留常驻偏好、数据和后台状态。可在此重新开启。</p></div><button class="demo-switch" role="switch" aria-label="启用${item.name}" aria-checked="${enabledApps.has(item.id)}" data-app-toggle="enabled"></button></div>
<div class="note-setting-row"><div><strong>常驻左侧菜单</strong><p>仅控制主导航捷径;应用关闭时记住此偏好。</p></div><button class="demo-switch" role="switch" aria-label="常驻左侧菜单" aria-checked="${residentApps.includes(item.id)}" data-app-toggle="pinned"></button></div>`;
if (settingsApp === 'notes') $('#app-settings').innerHTML += `
<div class="note-setting-row"><div><strong>显示未完成待办数量</strong><p>在左侧入口显示数量,超过 99 项显示 99+。本 Demo 使用 3 项示例。</p></div><button class="demo-switch" role="switch" aria-label="显示未完成待办数量" aria-checked="${noteSettings.showCount}" data-note-toggle="showCount"></button></div>
<fieldset class="note-setting-options"><legend>AI 评论方式</legend><div class="note-segments" role="group" aria-label="AI 评论方式">${Object.entries(noteModes).map(([value, label]) => `<button data-note-mode="${value}" aria-pressed="${noteSettings.mode === value}">${label}</button>`).join('')}</div><p>即时:按回车并停止输入 5 秒后评论未保存草稿。保存后自动:保存后评论。保存后手动:点击 AI 分析后评论。</p></fieldset>
<fieldset class="note-setting-options"><legend>AI 评论形式</legend><div class="note-segments" role="group" aria-label="AI 评论形式">${Object.entries(noteFormats).map(([value, label]) => `<button data-note-format="${value}" aria-pressed="${noteSettings.format === value}">${label}</button>`).join('')}</div><p>默认同时生成流式长评和结构化要点,也可只保留其中一种。</p></fieldset>
<div class="note-settings-preview"><span class="eyebrow">当前配置预览</span><p>${noteModes[noteSettings.mode]} · ${noteFormats[noteSettings.format]}</p><small>选择后即时保留于本次 Demo 会话,不调用 AI、不修改真实设置。</small></div>`;
if ($('#note-settings-summary')) $('#note-settings-summary').textContent = `当前设置:${noteModes[noteSettings.mode]} · ${noteFormats[noteSettings.format]}。待办数量${noteSettings.showCount ? '显示' : '隐藏'}。`;
}
function showAppSettings(id = 'notes') {
settingsApp = id;
const item = items.find(entry => entry.id === id);
if (!$('#catalog').open) { catalogMode = 'apps'; $('#search').value = ''; }
settingsOpen = true;
$('#catalog').dataset.variant = variant;
$('#catalog-title').textContent = `${item.name}设置`;
$('#catalog-description').textContent = '应用中心、应用页面和全局设置共用本次演示配置。';
$('#back-to-apps').hidden = false; $('#app-settings').hidden = false;
for (const selector of ['.center-switch', '#search', '#catalog-items', '#market-preview']) $(selector).hidden = true;
renderAppSettings();
if (!$('#catalog').open) $('#catalog').showModal();
$('#back-to-apps').focus();
}
function notify(text) {
$('#toast').textContent = text; $('#toast').classList.add('visible');
clearTimeout(toastTimer); toastTimer = setTimeout(() => $('#toast').classList.remove('visible'), 2200);
}
function renderApps() {
$('#edit-apps').textContent = editingApps ? '完成' : '编辑';
$('#resident-list').innerHTML = residentApps.filter(id => enabledApps.has(id) && (variant === 'scenes' || items.find(item => item.id === id).builtin)).map(id => {
const index = residentApps.indexOf(id);
const item = items.find(entry => entry.id === id);
return `<div class="resident-row" data-resident="${id}" draggable="${editingApps}"><button data-open="${id}" ${$('#page-label').textContent === item.name ? 'aria-current="page"' : ''}>${icon(item.icon)}<span>${item.name}</span>${id === 'notes' && noteSettings.showCount ? '<small class="note-count" aria-label="3 项未完成待办(示例)">3</small>' : ''}</button>${editingApps ? `<div class="edit-actions"><button data-move="${id}" data-direction="-1" aria-label="上移${item.name}" ${index === 0 ? 'disabled' : ''}>↑</button><button data-pin="${id}" aria-label="移除${item.name}">×</button><button data-move="${id}" data-direction="1" aria-label="下移${item.name}" ${index === residentApps.length - 1 ? 'disabled' : ''}>↓</button></div>` : ''}</div>`;
}).join('') || '<p class="empty-apps">点击 +,选择常驻应用。</p>';
const subtitles = { dock: '', grid: '内置应用', cards: '笔记 · 知识 · 自动化 · 模型', launcher: 'Ctrl / ⌘ K', favorites: '管理常驻偏好', scenes: '未来私有市场 · 模拟' };
$('#center-entry').innerHTML = `<button id="open-center" aria-haspopup="dialog"><span class="brand-mark">${variant === 'launcher' ? icon('search') : 'GB'}</span><span><strong>${variant === 'launcher' ? '搜索应用…' : '应用中心'}</strong>${subtitles[variant] ? `<small>${subtitles[variant]}</small>` : ''}</span><span class="entry-arrow">${variant === 'scenes' ? '↗' : '⌃'}</span></button>`;
}
function renderCatalog() {
const query = $('#search').value.trim().toLowerCase();
const matches = items.filter(item => (catalogMode === 'store' ? marketReady && !item.builtin : installed.has(item.id) && (variant === 'scenes' || item.builtin)) && `${item.name}${item.detail}`.toLowerCase().includes(query));
if (variant === 'favorites') matches.sort((a, b) => Number(residentApps.includes(b.id)) - Number(residentApps.includes(a.id)));
$('#catalog-items').innerHTML = matches.map(item => {
const ready = installed.has(item.id); const pinned = residentApps.includes(item.id);
return `<article class="center-app">${icon(item.icon)}<div class="center-app-info"><strong>${item.name}</strong><p>${item.builtin ? '内置应用' : '私有市场 · 示例'}${item.id === 'models' ? ' · 计划预览' : ''}${ready ? enabledApps.has(item.id) ? ' · 已启用' : ' · 已关闭' : ''}</p><p class="app-detail">${variant === 'cards' ? item.summary : item.detail}</p></div><div class="center-actions">${ready ? `${enabledApps.has(item.id) ? `<button data-open="${item.id}">打开</button>` : ''}<button data-app-settings="${item.id}">${enabledApps.has(item.id) ? '设置' : '设置 / 重新开启'}</button><button data-pin="${item.id}" aria-pressed="${pinned}">${pinned ? '取消常驻' : '常驻左侧'}</button>` : `<button data-install="${item.id}">模拟安装</button><button data-install-pin="${item.id}">模拟安装并常驻</button>`}</div></article>`;
}).join('') || `<p class="empty-apps">${catalogMode === 'store' && !marketReady ? '请先模拟获取私有市场目录。' : '没有匹配的应用,请换个关键词。'}</p>`;
$('#yaml-app').innerHTML = items.filter(item => installed.has(item.id)).map(item => `<option value="${item.id}">${item.name}</option>`).join('');
document.querySelectorAll('.center-switch button').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.catalog === catalogMode)));
}
function openCatalog(mode = 'apps') {
importDraft = null; $('#import-preview').hidden = true;
if (variant !== 'scenes') mode = 'apps';
settingsOpen = false; $('#back-to-apps').hidden = true; $('#app-settings').hidden = true;
for (const selector of ['.center-switch', '#search', '#catalog-items']) $(selector).hidden = false;
$('.center-switch').hidden = variant !== 'scenes';
$('#market-preview').hidden = mode !== 'store';
catalogMode = mode; $('#catalog').dataset.variant = variant;
$('#catalog-title').textContent = mode === 'store' ? '私有市场 · 未来预览' : '应用中心';
$('#catalog-description').textContent = mode === 'store' ? '目录和安装均为模拟,未接入 ShareServer。导入不会启动模型或任务。' : '内置应用无需安装;启用与常驻分别设置。系统工具请使用右侧工作栏。';
$('#search').value = ''; renderCatalog();
if (!$('#catalog').open) $('#catalog').showModal();
$('#search').focus();
}
function openPage(name, keepSettings = false) {
currentPage = name;
modelConfirmation = null;
if ($('#catalog').open && !keepSettings) $('#catalog').close();
$('#page-label').textContent = name;
$('#page-content').replaceChildren();
const title = document.createElement('h1'); title.textContent = name;
const description = document.createElement('p'); description.className = 'muted'; description.textContent = '主内容区页面预览。右侧工作栏保持当前工具状态。';
$('#page-content').append(title, description);
const app = items.find(item => item.name === name);
$('.composer-area').hidden = Boolean(app) || name === '设置' || name === '运行记录';
$('#page-content').classList.toggle('app-page', $('.composer-area').hidden);
for (const panel of ['#composer-options', '#context-details']) if ($(panel).matches(':popover-open')) $(panel).hidePopover();
$('#page-actions').replaceChildren();
if (app && !enabledApps.has(app.id)) {
description.textContent = '此应用已关闭,当前页面不可用。已有数据和选择保留;此操作不停止后台任务或模型服务。';
renderApps(); return;
}
if (app?.id === 'models') renderModels();
if (name === '魔法笔记' || name === '设置') {
const summary = document.createElement('p'); summary.id = 'note-settings-summary'; summary.className = 'muted'; $('#page-content').append(summary);
renderAppSettings();
}
document.querySelectorAll('.navigation button').forEach(button => button.classList.toggle('selected', button.dataset.page === name));
renderApps(); if (!keepSettings) notify(`已打开${name}`);
}
function selectVariant(name) {
variant = name; $('.app').dataset.variant = name;
document.querySelectorAll('button[data-variant]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.variant === name)));
[$('#variant-label').textContent, $('#variant-description').textContent, $('#design-tradeoff').textContent] = variants[name];
history.replaceState(null, '', `#${name}`); renderApps();
if ($('#catalog').open) openCatalog();
const pageApp = items.find(item => item.name === currentPage);
if (name !== 'scenes' && pageApp && !pageApp.builtin) openPage('当前对话');
}
function selectTool(id) {
const content = {
files: '<span class="eyebrow">本机 / goodbuddy · 示例</span><div class="file-row">src /</div><div class="file-row">docs /</div><div class="file-row">resources /</div><div class="file-row">README.md</div>',
terminal: '<span class="eyebrow">当前项目 · 演示终端</span><pre>PS &gt; git status\nOn branch main\nworking tree clean\n\nPS &gt;</pre>',
browser: '<span class="eyebrow">浏览器 · 静态示例</span><div class="file-row">https://example.com</div><h2>Example Domain</h2>',
resources: '<span class="eyebrow">本机 · 静态演示数据</span><div class="metric">CPU <strong>18%</strong><progress value="18" max="100"></progress></div><div class="metric">内存 <strong>9.6 / 32 GB</strong><progress value="30" max="100"></progress></div><p>磁盘 IO:读 12 MB/s · 写 3 MB/s</p>'
};
$('#tool-content').innerHTML = content[id]; $('#tool-content').setAttribute('aria-labelledby', `tab-${id}`);
document.querySelectorAll('[data-tool]').forEach(button => { button.setAttribute('aria-selected', String(button.dataset.tool === id)); button.tabIndex = button.dataset.tool === id ? 0 : -1; });
}
function renderModels() {
$('#page-content').innerHTML = `<h1>本机推理监控</h1><p class="muted">统一查看语音、OCR、向量等本机推理任务与执行服务。任务进度、引擎及占用均为模拟数据。</p><h2>推理任务与服务</h2><p class="muted">下方操作作用于执行服务;启动或加载服务不会自动恢复已中断的任务。</p><div class="model-list">${modelServices.map(service => {
const external = service.management === '外部管理';
const active = service.management === '进程内' ? '已加载' : '运行中';
const actions = external ? ['config'] : service.management === '进程内' ? [service.running ? 'release' : 'load'] : service.running ? ['stop', 'restart'] : ['start'];
return `<article class="model-service"><strong>${service.name}</strong><p>${service.interrupted ? '任务已中断,需由原应用重新发起' : service.purpose}</p><p>执行服务:${service.running ? active : '未运行'} · 内存:${service.running ? service.usage : '已释放(模拟)'}</p><p>使用方:${service.interrupted ? '暂无活动使用方' : service.users}</p><div class="model-actions">${actions.map(action => `<button data-model="${service.id}" data-model-action="${action}">${modelActionLabels[action]}</button>`).join('')}</div>${modelConfirmation?.id === service.id ? `<div class="inline-confirm" role="group" aria-label="推理服务操作确认"><p>${modelActionLabels[modelConfirmation.action]} ${service.name} 将影响 ${service.interrupted ? '该执行服务,当前没有活动任务' : service.users}。仅模拟状态变化。</p><button data-model-cancel>取消</button><button data-model-confirm>确认${modelActionLabels[modelConfirmation.action]}</button></div>` : ''}<details><summary>引擎与错误详情</summary><p>${service.engine} · ${service.management}</p><p>最近错误:无(示例)。操作仅更新本页内存状态。</p></details></article>`;
}).join('')}</div>`;
}
const modelActionLabels = { start: '启动', stop: '停止', restart: '重启', load: '加载', release: '释放', config: '连接配置' };
function applyModelAction(id, action) {
const service = modelServices.find(entry => entry.id === id);
service.running = !['stop', 'release'].includes(action);
if (['stop', 'release', 'restart'].includes(action)) service.interrupted = true;
modelConfirmation = null;
renderModels();
$(`[data-model="${id}"]`)?.focus();
notify(`已模拟${modelActionLabels[action]} ${service.name}`);
}
function previewImport() {
importDraft = null;
const preview = $('#import-preview'); preview.hidden = false; preview.replaceChildren();
const values = {};
const fields = ['schema', 'id', 'name', 'version', 'entry', 'dependencies', 'resources'];
for (const line of $('#yaml-input').value.split(/\r?\n/).filter(line => line.trim() && !line.trim().startsWith('#'))) {
const match = line.match(/^([a-z]+):\s*(.+)$/);
if (!match || !fields.includes(match[1]) || Object.hasOwn(values, match[1])) { preview.textContent = '仅支持示例中的七个扁平字段,不接受重复或未知字段;请修正后重新预览。'; return; }
values[match[1]] = match[2].trim();
}
if (fields.some(field => !values[field]) || values.schema !== 'mock-v1' || !/^[a-z][a-z0-9-]*$/.test(values.id) || values.entry !== 'main') { preview.textContent = '请填写完整 mock-v1 示例,id 使用小写字母、数字或连字符,entry 为 main。'; return; }
const existing = items.find(item => item.id === values.id);
const dependencies = values.dependencies === 'none' ? [] : values.dependencies.split(',').map(value => value.trim());
const missing = dependencies.filter(value => !value.startsWith('builtin:') || !installed.has(value.slice(8)) || !enabledApps.has(value.slice(8)));
const blocked = existing?.builtin || missing.length || values.resources !== 'bundled';
const conflict = installed.has(values.id);
const text = document.createElement('p');
text.textContent = `${values.name} · ${values.id} · v${values.version} · 主内容区入口。依赖:${values.dependencies}。${missing.length ? `缺失或关闭:${missing.join('、')}。` : '依赖已具备(模拟)。'}资源:${values.resources}。${values.resources !== 'bundled' ? '需补充外部资源,不能安装为可用。' : ''}${existing?.builtin ? '与内置应用冲突,不允许覆盖。' : conflict ? '同标识冲突:需显式更新定义,保留启用与常驻偏好。' : '没有同标识冲突。'}`;
preview.append(text);
const cancel = document.createElement('button'); cancel.textContent = '取消'; cancel.onclick = () => { importDraft = null; preview.hidden = true; $('#yaml-preview').focus(); }; preview.append(cancel);
if (!blocked) {
importDraft = values;
const confirm = document.createElement('button'); confirm.id = 'import-confirm'; confirm.textContent = conflict ? '确认模拟更新定义' : '确认模拟安装';
confirm.onclick = () => {
if (!importDraft) return;
const draft = importDraft;
const item = items.find(entry => entry.id === draft.id);
if (!item) items.push({ id: draft.id, name: draft.id, icon: 'layers', detail: 'YAML 导入 · 未来示例', summary: '导入的应用定义预览', builtin: false });
// Imported text stays in textContent/textarea; catalog markup uses the validated identifier.
definitions.set(draft.id, { ...draft });
if (!installed.has(draft.id)) { installed.add(draft.id); enabledApps.add(draft.id); }
importDraft = null; preview.hidden = true; renderCatalog(); renderApps(); $('#yaml-preview').focus(); notify('已模拟安装 / 更新定义,未启动模型或任务');
};
preview.append(confirm);
}
}
$('#market-fetch').onclick = () => {
const valid = /^https?:\/\/[^\s/]+(?:\/[^\s]*)?$/.test($('#market-url').value.trim());
marketReady = valid;
$('#market-status').textContent = valid ? `已模拟获取目录:${$('#market-url').value.trim()},无网络请求。` : '请输入 http(s) 市场 URL 后重试。';
renderCatalog();
};
$('#market-fail').onclick = () => { marketReady = false; $('#market-status').textContent = '模拟来源不可达,请点击模拟获取目录重试。未切换公共市场,已安装应用仍可使用。'; renderCatalog(); };
$('#market-url').oninput = () => { marketReady = false; $('#market-status').textContent = '来源已修改,请重新模拟获取目录。'; renderCatalog(); };
$('#yaml-preview').onclick = previewImport;
$('#yaml-input').oninput = () => { importDraft = null; $('#import-preview').hidden = true; };
$('#yaml-export').onclick = () => {
const item = items.find(entry => entry.id === $('#yaml-app').value);
const definition = definitions.get(item.id) || { schema: 'mock-v1', id: item.id, name: item.name, version: '1.0', entry: 'main', dependencies: 'none', resources: 'bundled' };
$('#yaml-output').hidden = false;
$('#yaml-output').textContent = '# Mock schema only; no credentials or business data\n' + Object.entries(definition).map(([key, value]) => `${key}: ${value}`).join('\n');
};
document.addEventListener('click', event => {
const button = event.target.closest('button'); if (!button) return;
const data = button.dataset;
if (data.project || data.conversation) {
currentProject = data.project || data.conversation;
if (projectMenu.matches(':popover-open')) projectMenu.hidePopover();
if ($('#project-activity').matches(':popover-open')) $('#project-activity').hidePopover();
renderProjects();
const project = demoProjects.find(entry => entry.id === currentProject);
openPage(data.conversation ? project.conversations[Number(data.conversationIndex)] : '当前对话');
$('#page-label').textContent = `${project.name} / ${data.conversation ? project.conversations[Number(data.conversationIndex)] : '当前对话'}`;
$('#project-trigger').focus();
}
if (data.variant) selectVariant(data.variant);
if (data.catalog) openCatalog(data.catalog);
if (data.page) openPage(data.page);
if (data.open) openPage(items.find(item => item.id === data.open).name);
if (data.tool) selectTool(data.tool);
if (data.appSettings) showAppSettings(data.appSettings);
if (data.appToggle) {
const id = settingsApp;
if (data.appToggle === 'enabled') enabledApps.has(id) ? enabledApps.delete(id) : enabledApps.add(id);
else residentApps = residentApps.includes(id) ? residentApps.filter(entry => entry !== id) : [...residentApps, id];
renderApps(); renderComposer();
if (currentPage === items.find(item => item.id === id).name) {
openPage(currentPage, true);
}
renderAppSettings(); $(`[data-app-toggle="${data.appToggle}"]`).focus();
}
if (data.modelAction) {
if (data.modelAction === 'config') {
const row = button.closest('.model-service');
let config = row.querySelector('.model-config');
if (!config) { config = document.createElement('p'); config.className = 'model-config'; config.textContent = '连接配置预览:Ollama · http://localhost:11434 · 外部管理。仅展示示例,不连接或保存配置。'; row.append(config); }
} else {
const service = modelServices.find(entry => entry.id === data.model);
if (service.running && service.users) { modelConfirmation = { id: data.model, action: data.modelAction }; renderModels(); $('[data-model-cancel]').focus(); }
else applyModelAction(data.model, data.modelAction);
}
}
if (button.hasAttribute('data-model-cancel')) { const id = modelConfirmation.id; modelConfirmation = null; renderModels(); $(`[data-model="${id}"]`).focus(); }
if (button.hasAttribute('data-model-confirm') && modelConfirmation) applyModelAction(modelConfirmation.id, modelConfirmation.action);
if (data.noteToggle || data.noteMode || data.noteFormat) {
if (data.noteToggle === 'showCount') noteSettings.showCount = !noteSettings.showCount;
if (data.noteMode) noteSettings.mode = data.noteMode;
if (data.noteFormat) noteSettings.format = data.noteFormat;
renderApps(); renderAppSettings();
const selector = data.noteToggle ? `[data-note-toggle="${data.noteToggle}"]` : data.noteMode ? `[data-note-mode="${data.noteMode}"]` : `[data-note-format="${data.noteFormat}"]`;
$(selector).focus();
}
if (button.id === 'open-center') openCatalog(variant === 'scenes' ? 'store' : 'apps');
if (button.id === 'edit-apps') { editingApps = !editingApps; renderApps(); $('#edit-apps').focus(); }
if (data.pin || data.install || data.installPin) {
const id = data.pin || data.install || data.installPin;
if (data.install || data.installPin) { importDraft = null; $('#import-preview').hidden = true; installed.add(id); enabledApps.add(id); notify('已模拟安装,未启动任务或模型'); }
if (data.pin || data.installPin) residentApps = residentApps.includes(id) ? residentApps.filter(entry => entry !== id) : [...residentApps, id];
renderApps();
if ($('#catalog').open) { renderCatalog(); ($(`#catalog [data-pin="${id}"]`) || $('#search')).focus(); }
else $('#edit-apps').focus();
}
if (data.move) {
const from = residentApps.indexOf(data.move); const to = from + Number(data.direction);
if (to >= 0 && to < residentApps.length) [residentApps[from], residentApps[to]] = [residentApps[to], residentApps[from]];
renderApps(); $(`#resident-list [data-pin="${data.move}"]`).focus();
}
});
$('#resident-list').addEventListener('dragstart', event => { draggedApp = event.target.closest('[data-resident]')?.dataset.resident || ''; if (!editingApps || !draggedApp) { event.preventDefault(); return; } event.dataTransfer.setData('text/plain', draggedApp); });
$('#resident-list').addEventListener('dragover', event => { if (editingApps && draggedApp) event.preventDefault(); });
$('#resident-list').addEventListener('drop', event => {
event.preventDefault(); const target = event.target.closest('[data-resident]')?.dataset.resident;
if (!editingApps || !residentApps.includes(draggedApp) || !target || target === draggedApp) return;
const to = residentApps.indexOf(target); residentApps.splice(residentApps.indexOf(draggedApp), 1); residentApps.splice(to, 0, draggedApp); draggedApp = ''; renderApps(); $('#edit-apps').focus();
});
$('#resident-list').addEventListener('dragend', () => { draggedApp = ''; });
$('#search').addEventListener('input', renderCatalog);
$('#close-catalog').onclick = () => $('#catalog').close();
$('#back-to-apps').onclick = () => { const query = $('#search').value; openCatalog(catalogMode); $('#search').value = query; renderCatalog(); ($(`#catalog [data-app-settings="${settingsApp}"]`) || $('#search')).focus(); };
$('#app-settings').addEventListener('keydown', event => {
const group = event.target.closest('.note-segments');
if (!group || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const buttons = [...group.querySelectorAll('button')];
const index = buttons.indexOf(event.target);
buttons[event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length].click();
});
function toggleWorkbar(visible) { $('.workbar').hidden = !visible; $('#toggle-workbar').setAttribute('aria-expanded', String(visible)); }
$('#toggle-workbar').onclick = () => toggleWorkbar($('.workbar').hidden);
$('#close-tool').onclick = () => { toggleWorkbar(false); $('#toggle-workbar').focus(); };
$('.tool-tabs').addEventListener('keydown', event => {
const tabs = [...document.querySelectorAll('[data-tool]')]; const index = tabs.indexOf(event.target);
if (index < 0 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
selectTool(tabs[next].dataset.tool); tabs[next].focus();
});
document.addEventListener('keydown', event => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); openCatalog(); }
if (!$('#catalog').open || settingsOpen || variant !== 'launcher') return;
const actions = [...document.querySelectorAll('#catalog [data-open]')]; if (!actions.length) return;
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); const index = actions.indexOf(document.activeElement); const next = index < 0 ? (event.key === 'ArrowDown' ? 0 : actions.length - 1) : (index + (event.key === 'ArrowDown' ? 1 : -1) + actions.length) % actions.length; actions[next].focus(); }
if (event.key === 'Enter' && document.activeElement === $('#search')) { event.preventDefault(); actions[0].click(); }
});
$('#theme').onclick = () => { const dark = document.documentElement.dataset.theme !== 'dark'; document.documentElement.dataset.theme = dark ? 'dark' : 'light'; $('#theme').textContent = dark ? '切换浅色' : '切换深色'; };
$('#reset').onclick = () => { installed = new Set(items.filter(item => item.builtin).map(item => item.id)); residentApps = [...installed].filter(id => id !== 'models'); enabledApps = new Set(installed); Object.assign(noteSettings, { showCount: true, mode: 'immediate', format: 'combined' }); editingApps = false; definitions.clear(); importDraft = null; $('#import-preview').hidden = true; renderApps(); renderComposer(); openPage('当前对话'); notify('应用启用、常驻和笔记设置已恢复默认'); };
let demoRunning = false;
let demoRecording = false;
let demoAttachments = [];
let demoQueue = [];
let demoReplyTimer;
function renderComposer() {
const tags = $('#composer-tags'); tags.replaceChildren();
const addTag = (text, clear) => {
const button = document.createElement('button'); button.textContent = `${text} ×`; button.setAttribute('aria-label', `移除${text}`);
button.onclick = () => { clear(); renderComposer(); $('#message').focus(); }; tags.append(button);
};
demoAttachments.forEach((name, index) => addTag(name, () => demoAttachments.splice(index, 1)));
for (const id of ['composer-expert', 'composer-agent', 'composer-command']) {
const select = $(`#${id}`);
if (select.value) addTag(select.selectedOptions[0].textContent, () => { select.value = ''; });
}
document.querySelectorAll('[data-knowledge-control]').forEach(element => { element.hidden = !enabledApps.has('knowledge'); });
if (enabledApps.has('knowledge')) {
document.querySelectorAll('[data-knowledge]:checked').forEach(input => addTag(input.dataset.knowledge, () => { input.checked = false; }));
if ($('#composer-retrieval').value === 'always') addTag('每次检索', () => { $('#composer-retrieval').value = 'auto'; });
}
tags.hidden = !tags.childElementCount;
$('#stop').hidden = !demoRunning;
$('#send').disabled = !$('#message').value.trim() && $('#composer-command').value !== 'command';
$('#send').title = demoRunning ? '加入队列' : '发送';
$('#send').setAttribute('aria-label', demoRunning ? '加入演示队列' : '发送演示消息');
$('#composer-status').textContent = demoRecording ? '模拟录音中 · 再次点击结束' : demoRunning ? '模拟回复中 · 可继续发送加入队列' : '交互演示 · 不调用 AI';
for (const id of ['composer-runtime', 'composer-mode', 'composer-expert', 'composer-agent', 'composer-command']) $(`#${id}`).disabled = demoRunning;
$('#compact').disabled = demoRunning;
$('#runtime-options').hidden = $('#composer-runtime').value === '直连模型';
$('#compact').hidden = $('#composer-runtime').value === '直连模型';
$('#demo-queue').replaceChildren();
demoQueue.forEach((text, index) => {
const row = document.createElement('div'); row.className = 'queue-row';
const label = document.createElement('span'); label.textContent = text; label.title = text;
const insert = document.createElement('button'); insert.textContent = '立即插入'; insert.setAttribute('aria-label', `立即中断并插入:${text}`);
insert.onclick = () => { demoQueue.splice(index, 1); startDemoReply(text); };
const remove = document.createElement('button'); remove.textContent = '×'; remove.setAttribute('aria-label', `删除待发送消息:${text}`);
remove.onclick = () => { demoQueue.splice(index, 1); renderComposer(); };
row.append(label, insert, remove); $('#demo-queue').append(row);
});
$('#demo-queue').hidden = !demoQueue.length;
}
function startDemoReply(text) {
clearTimeout(demoReplyTimer); demoRunning = true;
if ($('#composer-options').matches(':popover-open')) $('#composer-options').hidePopover();
notify(`模拟处理:${text}`); renderComposer();
demoReplyTimer = setTimeout(() => {
demoRunning = false;
if (demoQueue.length) startDemoReply(demoQueue.shift());
else { renderComposer(); notify('演示回复已完成,未调用 AI'); }
}, 8000);
}
$('#send').onclick = () => {
if ($('#send').disabled) return;
const text = $('#composer-command').value === 'command' ? `/init ${$('#message').value.trim()}` : $('#message').value.trim();
$('#message').value = ''; $('#message').style.height = ''; demoAttachments = [];
if (demoRunning) { demoQueue.push(text); renderComposer(); }
else startDemoReply(text);
};
$('#stop').onclick = () => {
clearTimeout(demoReplyTimer); demoRunning = false;
if (demoQueue.length) startDemoReply(demoQueue.shift());
else { renderComposer(); notify('已停止演示回复'); }
};
$('#message').addEventListener('input', () => { $('#message').style.height = 'auto'; $('#message').style.height = `${Math.min(180, $('#message').scrollHeight)}px`; renderComposer(); });
$('#message').addEventListener('keydown', event => { if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); $('#send').click(); } });
$('#attach').onclick = () => $('#attachment-input').click();
$('#attachment-input').onchange = () => { demoAttachments.push(...Array.from($('#attachment-input').files, file => file.name)); $('#attachment-input').value = ''; renderComposer(); };
$('#voice').onclick = () => {
demoRecording = !demoRecording; $('#voice').setAttribute('aria-pressed', String(demoRecording));
if (!demoRecording) { $('#message').value += '请帮我整理一下今天的工作计划。'; $('#message').dispatchEvent(new Event('input')); }
renderComposer();
};
$('#composer-options').addEventListener('change', event => {
if (event.target.id === 'composer-command' && event.target.value === 'prompt') {
$('#message').value = '请总结当前讨论,列出关键结论和下一步行动。'; event.target.value = ''; $('#message').dispatchEvent(new Event('input'));
}
renderComposer();
});
$('#composer-runtime').onchange = () => {
$('#composer-agent').value = ''; $('#composer-command').value = '';
$('#agent-label').textContent = $('#composer-runtime').value === 'Continue' ? '预设' : 'Agent';
$('#composer-command').querySelector('[value="command"]').hidden = $('#composer-runtime').value !== 'OpenCode';
renderComposer();
};
$('#compact').onclick = () => {
$('#context-value').textContent = '18%'; $('#context-progress').value = 18;
$('#context-description').textContent = '压缩后对话估算:36k / 200k tokens';
$('#context-details').hidePopover(); notify('已模拟压缩上下文');
};
for (const [panelId, triggerId] of [['composer-options', 'composer-options-trigger'], ['context-details', 'context-trigger']]) {
const panel = $(`#${panelId}`); const trigger = $(`#${triggerId}`);
const position = () => {
const rect = trigger.getBoundingClientRect();
panel.style.left = `${Math.max(16, Math.min(rect.right - panel.offsetWidth, innerWidth - panel.offsetWidth - 16))}px`;
panel.style.top = `${Math.max(16, rect.top - panel.offsetHeight - 8)}px`;
};
panel.addEventListener('toggle', event => { trigger.setAttribute('aria-expanded', String(event.newState === 'open')); if (event.newState === 'open') position(); });
window.addEventListener('resize', () => { if (panel.matches(':popover-open')) position(); });
window.addEventListener('scroll', () => { if (panel.matches(':popover-open')) position(); }, true);
}
renderComposer();
document.querySelectorAll('[data-icon]').forEach(element => { element.innerHTML = icon(element.dataset.icon); });
window.addEventListener('hashchange', () => { if (Object.hasOwn(variants, location.hash.slice(1))) selectVariant(location.hash.slice(1)); });
selectVariant(Object.hasOwn(variants, location.hash.slice(1)) ? location.hash.slice(1) : 'dock'); selectTool('files');
if (matchMedia('(max-width: 1080px)').matches) toggleWorkbar(false);
@@ -0,0 +1,266 @@
# 应用导航与系统工具入口 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 应用中心与本机推理监控已接入源码,整体验收待完成 |
| 版本 | 0.4 |
| 日期 | 2026-09-17 |
| 适用产品 | GoodBuddy 桌面端 |
| 关联设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[助手工作栏 PRD](../assistant-workbar/prd.md) |
## 1. 背景
本版已同步应用中心方案,分期见[第 8 节](#8-分期)。左下角用于访问和管理应用,系统工具保留在右侧工作栏。
原主侧栏把“当前对话”“魔法笔记”“知识库”“智能心跳”和“运行记录”放在同一导航组,
底部显示“GB 本地工作区”和平台架构。当前已将底部改为应用中心及独立设置入口,文件、终端
和浏览器继续从右侧助手工作栏打开。
后续插件可能提供完整页面,也可能只提供辅助面板或命令。若不先区分系统入口、应用和工具,
插件接入后会同时争用主导航、侧栏底部和助手工作栏,用户也无法判断入口会切换页面还是打开
辅助面板。
## 2. 产品定义
### 2.1 入口分类
| 类型 | 首期成员 | 默认位置 | 用户控制范围 |
| --- | --- | --- | --- |
| 系统入口 | 当前对话、运行记录、设置 | 主导航固定位置或侧栏底部 | 不可隐藏、卸载或改变类型 |
| 始终显示的内置应用 | 知识库 | 主导航应用区及应用中心 | 始终启用并常驻,可排序,不提供启用或常驻开关;保留工作区实际配置 |
| 可选内置应用 | 智能心跳、魔法笔记、本机推理监控 | 应用中心及主导航常驻区 | 独立控制启用、常驻与排序;保留数据 |
| 系统工具 | 文件浏览器、终端、浏览器、资源监控 | 右侧助手工作栏 Tab 和“+”目录 | 自带且不可卸载,按工作栏规则管理实例 |
运行记录是系统审计页面,不属于工具。文件浏览器、终端、浏览器和资源监控是系统工具;它们
的入口与实际界面均由右侧助手工作栏承载,不列入应用中心。
### 2.2 后续插件类型
插件市场开放后,插件可以贡献“插件应用”或“插件工具”。插件不能注册、覆盖或替换系统入口,
也不能冒充系统工具。内置应用继续标记为“内置”,不因采用统一注册协议而显示成待安装商品。
## 3. 目标
- 用户从侧栏底部进入应用中心,查找、打开和配置内置应用。
- 主导航承载应用入口,本机推理监控打开 Modal,其余内置应用打开完整页面;辅助工具由工作栏承载。
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
- 四个内置应用均支持排序;智能心跳、魔法笔记和本机推理监控可独立设置启用与常驻,知识库始终显示,未常驻的已启用应用仍可从应用菜单打开。
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
## 4. 非目标
- 首期不提供应用商店页面、商店导航、推荐位或安装按钮,也不提供第三方插件搜索、下载、
安装、更新、签名验证或卸载。内置应用的浏览、搜索与打开属于应用中心,不依赖商店。
- 不把文件树、终端或浏览器重新实现到左侧栏。
- 不改变助手工作栏的面板实例、停靠、执行空间和安全边界。
- 不允许隐藏运行记录、当前对话或设置。
- 不因隐藏可选应用而删除业务数据。
- 不把“显示在侧边栏”解释为停止智能心跳计划或后台任务。
- 不提供任意本机程序启动器。
- 首期资源监控不采集远程主机、GPU、显存或网络吞吐,不提供后台告警和历史持久化。
## 5. 功能要求
### FR-1 入口分类
系统必须为每个可见入口声明稳定类型:`system-entry`、`application` 或 `system-tool`。界面位置、
设置项和点击结果均以该类型为准,不根据当前页面临时改变类型。
### FR-2 固定系统入口
“当前对话”固定在主导航首位;“运行记录”固定在应用区之后;“设置”固定在侧栏底部。
这些入口不出现在应用显隐、工具固定或插件管理设置中。
### FR-3 内置应用区
主导航在当前对话之后按共享保存顺序显示应用,知识库始终显示,可选应用仅显示已启用且常驻项;运行记录位于应用区之后。四个应用均可排序。本机推理监控入口打开独立 Modal,其余应用打开对应一级页面,保留
现有徽标、范围和页面状态。管理 Modal 列出全部内置应用,知识库只提供“打开”和排序。
### FR-4 隐藏应用
取消常驻只移除主导航捷径,不删除数据、不修改启用状态,也不终止正在进行的操作。
关闭可选应用则同步移除业务入口,保留常驻偏好;知识库不受这些偏好影响。心跳关闭后的执行边界见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)。
配置保存成功后各入口使用同一已确认状态,失败时保持原状态并允许重试。
### FR-5 应用中心入口
侧栏底部提供应用中心和独立设置入口。点击应用中心向上展开锚定按钮的轻量菜单,无模态遮罩。菜单列出已启用应用,不按是否打开过、历史记录或常驻状态筛选;点击应用行关闭菜单,本机推理监控打开 Modal,其余应用在主内容区打开页面。
菜单底部“管理应用”关闭菜单并打开既有管理 Modal,在其中浏览、搜索全部内置应用,为已关闭的可选应用提供设置与重新开启入口。无搜索结果时显示空状态并可清除搜索。
首期不展示商店、安装、推荐或系统工具清单。入口具有可访问名称,Escape 关闭后恢复触发位置的焦点。
### FR-6 文件浏览器入口
在右侧选择“工作区”Tab 时聚焦默认存在且不可关闭的单实例。该实例保持
“跟随当前上下文”或“固定到指定对象”状态;迁移或布局修复发现实例缺失时补回,并默认跟随
当前项目。没有项目时使用已配置的默认工作区。跟随上下文所需的路径不可用时,面板显示修复
入口;已有有效固定目标不受当前项目和默认工作区状态影响。固定目标失效时修复该目标,不静默回退到其他目录。
### FR-7 终端入口
选择已有终端 Tab 仅激活该实例,保持目标、会话和历史状态。通过工作栏“+”选择终端时
创建绑定当前执行空间的新实例,不按目标复用已有实例;无项目时使用本机 Home。
远程项目的新实例绑定其托管 SSH 执行空间和权威目录。创建失败保留当前主页面与其他实例。
### FR-8 浏览器入口
选择已有浏览器 Tab 保留其绑定目标、页面和导航状态;通过工作栏“+”选择浏览器时创建新实例,
有活动 Conversation 时绑定该会话,否则创建待绑定空实例。同一 Conversation 的实例共享隔离
登录上下文,页面与导航状态独立。入口不打开系统默认浏览器,也不创建旁路 WebView。
### FR-9 资源监控入口
通过右侧工作栏“+”打开或聚焦单实例的本机“资源监控”,已有面板直接选择其 Tab。面板显示
整机 CPU 使用率、内存使用量与总量、磁盘读写速率,以及 GoodBuddy 桌面进程的 CPU、内存
和进程数摘要。指标不可用时显示具体项目和原因,不能显示为 `0`。
资源面板处于可见活动状态、工作栏已展开且应用窗口未隐藏或最小化时,每 2 秒采样一次,在
Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止采样;首期不在后台持续监控,
也不据此产生侧栏告警。首次采样及停止后恢复的首次采样只建立 CPU 和磁盘 IO 速率基线,
不显示伪造的 `0` 值。
### FR-10 工作栏入口状态
工具面板已激活时,对应 Tab 显示选中状态;面板存在但未激活时保持普通状态。运行中、错误或
等待用户处理可以显示徽标,但必须有可访问文字,且不得自动抢占当前面板。
### FR-11 应用导航设置
管理 Modal 的四个应用行均提供上下移动和拖动排序,不受启用或常驻状态限制;管理列表、菜单和侧栏共用一份顺序。箭头边界按完整顺序计算,搜索过滤不丢失隐藏项。智能心跳、魔法笔记和本机推理监控另提供应用设置、常驻管理;知识库提供“打开”和排序,不添加通用空设置页或开关。原工作区配置入口及心跳单条计划 enabled 保持不变。设置中“启用应用”与“常驻左侧菜单”分别保存,
修改即时提交;请求期间串行处理并禁用会修改同一配置的控件,失败就地反馈。
魔法笔记设置统一从应用中心对应行进入,笔记页与全局设置不再提供重复入口。持久化成功后的设置变更同步覆盖工具配置写入;每次重开中心先刷新并锁定修改,较新的确认快照优先于迟到的读取结果。
### FR-12 偏好持久化
四个应用的顺序及可选应用启用、常驻按设备持久化,重启后恢复。默认顺序为知识库、智能心跳、魔法笔记、本机推理监控。心跳应用默认关闭,其余应用默认启用;心跳和魔法笔记默认常驻,本机推理监控默认不常驻;
魔法笔记待办数量默认显示,评论方式与形式沿用现有默认值。迁移保留已保存的启用、常驻和排序选择,
旧魔法笔记入口开关绑定业务启用值,迁为“启用应用”并保留原值;常驻使用独立配置,
只有已发布的独立显隐偏好才映射到常驻,不能由常驻推导业务启停。旧可选应用顺序补入知识库、智能心跳默认位置并保留已有相对顺序,具体补齐规则见[技术设计](./technical-design.md#43-持久化与-ipc)。`heartbeatEnabled` 的缺失值与保存值按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)处理;不读取未发布草案中的 `knowledgeEnabled`。
### FR-13 窄窗口行为
窄窗口下主侧栏仍按现有抽屉方式显示,应用中心和设置保持可操作且不重叠。应用列表可滚动,
搜索、设置及键盘排序不依赖拖拽;工具沿用右侧工作栏响应式规则,不搬到左侧。
### FR-14 项目活动汇总
用户在任一项目中都能查看跨项目的待处理、运行中与已完成未查看会话,并从活动列表直接进入所选会话。
汇总入口仅在有上述活动行时显示在项目选择器下方,无活动行时整行隐藏。点击后先显示有活动会话的项目,
悬停或点击项目展开会话子菜单,点击会话进入该条对话。项目菜单保留原有分组与顺序,并显示
各项目的活动数量;级联菜单内的项目顺序不随各类活动数量变化。
汇总不受当前项目、搜索条件或最近历史条数限制。计数与排除规则见
[活动逻辑](./logic-design.md#9-项目活动汇总),交互见[活动界面](./ui-design.md#10-项目活动汇总)。
验收覆盖跨项目后台运行、同会话多任务去重、提问与审批优先、较早的仍在运行或等待处理的
会话可见,以及从带搜索条件的页面或有未保存修改的设置页跳转。成功完成后保留“已完成”
通知,直到用户实际查看该会话;从活动菜单或普通侧栏进入均可清除。设置覆盖下的聊天及
隐藏文档不算查看。完成通知仅保留在当前应用会话内,初始任务历史不补发通知,失败和取消
不产生完成通知;普通未读标记本身不构成活动。
## 6. 权限与安全
- 快捷入口不授予新的文件、命令、网络或浏览器权限。
- 系统工具继续使用 Main、Preload、执行空间 Grant、Ask/Execute 和现有审批边界。
- Renderer 不因新增入口获得任意文件系统、PTY、进程或 Electron API。
- 资源指标由 Main 采集并以有界结构返回;Renderer 不接收进程命令行、环境变量或任意 PID。
- 资源样本只保存在当前 Renderer 内存中,不写入运行记录、SQLite 或遥测服务。
- 插件贡献类型只是界面与导航声明,不能代替权限清单、用户授权和运行隔离。
- 运行记录继续记录由系统工具触发的受审计操作。
## 7. 产品验收
- 主导航能明确区分固定系统入口和可配置内置应用。
- 运行记录在所有应用显隐组合下保持可见并可打开。
- 左下角不再用“本地工作区”与系统架构占据主要空间。
- 应用菜单只列已启用应用,点击行关闭菜单;本机推理监控打开保留底层工作区的 Modal,其余应用进入主内容区。“管理应用”打开可搜索的既有 Modal。四个应用均可排序,知识库始终可用,三个可选应用可管理启用与常驻,首期无商店入口。
- 系统工具仅从右侧 Tab 与“+”访问;选择 Tab 不创建或改绑实例,“+”可显式创建额外浏览器或终端。
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
最小化后停止请求新样本。
- CPU、内存、磁盘 IO 或 GoodBuddy 指标不可用时,页面区分部分可用与整体失败。
- 取消常驻不删除数据、不停止后台计划;关闭应用按统一规则联动业务入口,保存失败时各处保持原状。
- 魔法笔记设置迁移保留已有选择,应用中心与笔记页共用同一配置。
- 键盘、屏幕阅读器、浅色主题、深色主题和窄窗口均可完成相同操作。
## 8. 分期
### 首期
- 侧栏最底部应用中心打开向上锚定的轻量菜单,无模态遮罩;底部“管理应用”进入与设置相同的整窗 Modal 和可搜索应用列表,设置按钮保持独立。
- 应用中心提供魔法笔记、知识库、智能心跳及本轮追加的本机推理监控入口,不要求安装。本机推理监控 Modal 与服务单独实现和验证。
- 四个应用共享排序;支持智能心跳、魔法笔记、本机推理监控常驻主导航及取消常驻,知识库始终显示。已启用未常驻应用仍可从菜单打开,取消常驻不删除数据或停止后台任务。
- 通用应用设置统一从应用中心对应行进入;应用页面保留实际业务配置,本机推理监控保留“模型与连接设置”。
魔法笔记优先接入待办数量、AI 评论方式和评论形式,入口显示与常驻控制统一,避免重复开关。
- 文件、终端、浏览器保留在右侧工作栏 Tab,不在左下角复制入口,也不纳入应用中心。
- 原计划的本机资源监控仍作为系统工具交付项,在右侧增加单实例面板;与应用中心分别实现和验证。
- 保持运行记录为固定系统入口。
首期验收需覆盖:无商店入口时可从菜单访问已启用应用,并从管理 Modal 恢复关闭的可选应用;取消常驻后仍可重新打开和配置;
不同设置入口状态一致;常驻排序重启后保留;右侧工具切换不改变当前应用页面。
本节是范围调整,Demo 交互通过不代表正式功能已经实现。
### 补充要求:魔法笔记迁移与入口联动
- 将现有魔法笔记设置迁入应用中心对应行的设置界面,移除笔记页面内的重复入口。
魔法笔记可常驻主导航;移除原全局设置入口,保留已有配置值和业务数据。
- 魔法笔记相关布尔开关默认全部开启,包括入口显示(常驻)和未完成待办数量显示。
AI 评论方式、评论形式是单选项,沿用现有默认值,不把“全部开启”解释为同时选中多个模式。
新默认值适用于未配置状态,迁移时保留用户已明确保存的选择。
- 知识库侧栏入口始终存在且可调整位置;会话知识范围、检索方式、标签及请求行为继续遵守知识库自身规则。心跳入口按启用和常驻显示,单条计划 enabled 仍由心跳页面管理。
- 可选应用“取消常驻”只移除主导航捷径;“关闭应用”移除该应用在业务界面的入口。
应用中心保留管理和重新开启入口,重新开启沿用常驻偏好,不自动导航。
- 入口变化不删除已有数据。关闭后已打开的可选应用页显示不可用状态及恢复设置入口。
已发出的请求和后台任务沿用各功能的生命周期规则,关闭入口不新增强制取消或重放行为。
补充验收:关闭全部可选应用后,知识库侧栏入口仍可用,知识会话入口及心跳单条计划状态不变;管理 Modal 的知识库行提供“打开”和排序,不能启停或取消常驻。心跳执行边界按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)验收。验证魔法笔记未配置时的默认开启,以及
已有关闭选择在迁移后保留。设置保存失败时各入口保持原状态,不能出现互相矛盾的显示。
### 后续:应用商店与扩展
- 单独设计应用商店的目录、搜索、推荐、详情及安装管理,时间与具体范围另行确定。
- 插件应用和插件工具注册,以及安装、更新、卸载与权限说明。
- 插件应用可常驻主导航;插件工具按工作栏扩展设计接入,不恢复左下角重复系统工具条。
- 用户级快捷键与命令面板接入同一贡献命令。
现有 Demo 的商店页、项目管理和阅读空间安装流程仅保留为未来交互参考,不纳入首期验收,
也不作为已有商店能力的说明。首期实现不预建插件安装服务或远程目录协议。
### FR-15 私有化市场与 YAML 交换(后续)
市场面向组织私有化部署。用户配置市场 URL 后即可获取该来源的应用目录与详情,并按需获取
应用;需要认证的来源复用其登录流程。市场不可达时说明原因并允许重试,不影响已安装应用
和内置应用使用,也不自动切换到公共市场。市场 URL 只影响应用市场,不改变模型、桌面更新
或 Runtime 的下载源。市场 URL 按设备保存,重启后保留;认证失效时提示重新认证。
应用支持以 `.yml` 导入、导出,兼容 `.yaml` 扩展名,用于组织内部共享、备份和迁移。
导出内容为应用定义及可迁移配置,包含格式版本、应用标识、应用版本、入口声明和依赖说明;
凭据、会话、笔记及知识库业务数据不随应用定义导出。导入前展示应用摘要、缺失依赖和与已有
应用的冲突;同标识应用由用户选择更新或取消,不静默覆盖本机配置。成功后展示实际版本与可用状态;
获取、校验或更新失败保留原已安装版本与配置,并显示可重试原因。
YAML 描述文件不能代替模型权重或插件程序包。有外部依赖时应明确资源引用;离线导入仅在
所需资源已具备时可完成使用,否则列出需要补充的资源,不显示为可运行。导入应用不会自动
启动模型或执行任务。
未来 ShareServer 提供私有市场目录、应用定义的导入导出、发布和版本资源分发;桌面端负责
配置来源、获取目录、本地导入导出及应用安装管理。服务端职责归属见
[ShareServer 总体设计](../../architecture/share-network-architecture.md)。本节记录后续需求,
尚未确定 API、YAML Schema 或服务端交付版本,不纳入应用中心首期。
### FR-16 本机模型管理入口(新增计划项)
应用中心提供“本机推理监控”内置管理应用,可选择常驻左侧。从常驻入口、应用菜单或管理 Modal 的“打开”均进入同一独立管理 Modal;来源菜单或管理 Modal 关闭,底层工作区、当前项目、会话及草稿保持不变。该入口统一
查看本机语音识别、OCR、向量生成等服务的运行状态与可用资源指标。
Modal 只有服务列表,显示名称、简短状态、引擎、模型、资源和错误,不显示任务历史、任务页签、单任务取消或外部连接列表。独立向量进程显示 CPU 与工作集内存,并标明进程范围;CPU 预热、采样失败和共享进程 worker 无法独立统计时说明原因,不能用零或整个应用进程占用代替服务指标。活动请求可能仍在排队或加载,不能据此宣称服务正在运行。ASR 当前仅显示按需运行且状态未知;OCR 和向量服务使用各自执行模块的状态。
服务行保留实际支持的启动、停止、重启或模型加载、释放,工具栏保留刷新和模型设置入口。停止后重新启动不自动重放已中断任务。
停止、重启或释放前,在管理 Modal 上方打开紧凑影响确认,展示正在使用该服务的任务和影响;没有已登记活动任务时也说明执行前会再次核对。取消时保留运行状态,
失败时关闭旧确认、显示原因并刷新状态,再次操作须重新确认当前影响。多个应用共用同一模型服务时只展示同一个服务,不重复
启动进程。隐藏管理入口、关闭管理 Modal 或取消应用常驻均不停止模型;卸载模型文件是独立操作。
确认打开时不能关闭底层管理 Modal;Escape 只关闭最上层,提交期间禁止关闭及重复执行。关闭后恢复可用入口焦点,具体规则见 [UI 设计](./ui-design.md#91-本机推理管理)。
资源监控仍是只读系统工具,本机推理监控聚焦本地服务状态。该项独立于商店接入生产源码,支持 OCR 空闲加载与释放、内置向量进程启动/停止/重启;TTS 显示不可用。后端保留请求登记及原有取消能力,供执行路径和服务影响校验使用。各引擎边界见
[技术清单](./technical-design.md#111-本机推理任务与执行服务),验证范围见[实施进度](./progress.md)。
原首期资源监控的 GoodBuddy 桌面进程统计范围不因此扩大。
@@ -0,0 +1,375 @@
# 应用导航实施进度
2026-09-23 文档校正:当前 `heartbeat` 已作为可选应用管理,默认值与执行边界见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行),设置与导航契约见[技术设计](./technical-design.md#4-应用导航偏好)。下方日期记录保留当时实现与验证结果;其中“心跳始终显示”“无应用级开关”和旧字段删除说明不再代表当前行为。本次仅核对源码并更新文档,未重新执行功能测试。
## 2026-09-18:窄幅单列应用列表
对应 FR-11、US-B6、US-B7、US-D1。管理概览改为紧凑单列横向列表,概览与详情共用 `640px` 最大宽度;左侧显示图标、名称、状态与可换行用途,右侧放置打开、可用的设置及成组排序箭头。行间使用弱分隔线,窄窗口操作换行。保留四应用保存顺序、箭头和拖动规则、知识库与智能心跳始终显示、本机推理监控默认不常驻。现行尺寸与交互见 [UI 设计](./ui-design.md#4-应用中心与共同设置),下方历史记录保留当时布局。
验证:ApplicationCenter、ApplicationMenu、WorkspacePrimitives 共 54 项测试通过;`npm run typecheck`、`npm run lint`、`git diff --check` 通过。全量 `npm test` 超过 60 秒未取得汇总,不记为通过。
Windows Electron fixture 重新编译当前组件、生产 CSS 与中文翻译,使用真实 ApplicationSettingsStore 及临时 IPC;浅深主题分别检查 1280、960、720、640、375px 目标窗口,实际 CSS 视口因缩放约多 1px。计算布局均为单列且无横向溢出,桌面 Modal 宽度为 640px,四行共八个箭头且均可拖动。原生键盘排序、磁盘顺序读取、详情返回及关闭后的焦点恢复通过。证据为临时目录 `application-rows-Hl3X55/result.json`。截图捕获持续报 `UnknownVizError`,禁用硬件加速后仍失败,因此未完成截图审阅;此 fixture 不作为完整 App、安装包或原生指针拖放验收。
中文改动按 deai-writing 自查清单复核。未修改模型、OCR、任务中心或 Agent 执行逻辑,未调用模型,未创建提交。
## 2026-09-18:收拢通用应用设置入口
对应 FR-11、US-B9。移除魔法笔记页面及本机推理监控停用状态的通用“应用设置”按钮,启用、常驻和笔记评论配置统一从应用中心卡片进入。保留管理应用、设置详情返回导航、模型与连接设置及原业务控件。删除仅用于页面快捷入口的 Context、跳转回调和初始详情参数;停用时仍保留页面挂载状态。现行入口规则见 [UI 设计](./ui-design.md#41-共同应用设置页),Demo 已移除页面、停用恢复及全局设置中的重复快捷入口。
验证:App 入口与状态同步定向测试 14 项通过;ApplicationCenter、ApplicationMenu、LocalInferencePage、SettingsPanel 共 128 项通过。首次包含完整 App 文件的运行出现两项既有任务审批失败,以及新增笔记测试未启用 fixture 的失败;修正 fixture 后相关定向测试通过。两项审批失败与上次记录一致,本轮未修改。类型检查、全仓 lint、Demo 语法及 diff 检查通过。本轮未运行全量测试或 Electron 窗口验收,未调用模型,未创建提交;保留已有四应用排序、监控名称和默认不常驻改动。
## 2026-09-18:四应用共享顺序
对应 FR-3、FR-11、FR-12、US-B7。四张管理卡片均提供上下移动和拖动,包含关闭、未常驻以及始终显示的应用;卡片、菜单、侧栏共用 `applicationNavigation.order`。知识库和智能心跳保留无开关的始终启用、常驻行为,位置可调整,标签改为“始终显示”/`Always shown`。当前对话、运行记录、设置仍保持系统位置。搜索中箭头按完整顺序移动,拖动禁用。保留此前 `260px` 自适应网格、本机推理监控名称及默认不常驻。
权威设置返回及写入 schema 接受四个 ID 各一次,拒绝重复、未知和缺失项。仅存储读取补齐旧可选应用顺序及部分默认值,保留已有顺序、显式布尔值和无关设置;规则见[持久化与 IPC](./technical-design.md#43-持久化与-ipc)。没有新增存储版本、应用启用字段或第二份导航状态。
验证:`npm test -- src/main/application-settings-store.test.ts src/shared/goodbuddy-config-contracts.test.ts src/main/goodbuddy-config-service.test.ts src/renderer/src/ApplicationCenter.test.tsx src/renderer/src/ApplicationMenu.test.tsx src/renderer/src/LocalInferencePage.test.tsx src/renderer/src/WorkspacePrimitives.test.tsx` 共 127 项通过;App 排序保存、菜单与侧栏同步、外部事件、重开及入口行为的 9 项定向测试通过。包含完整 `App.test.tsx` 的八文件运行共 390 项通过、2 项失败,失败项是任务中心审批测试在折叠列表中找不到任务行(`App.test.tsx` 的 scheduled approvals 场景),本次没有修改该行为。`npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。全量 `npm test` 运行约七分钟仍未得到汇总,已停止本次测试进程;日志复现上述两项失败,不能宣称全量通过。
Windows Electron fixture 使用当前 ApplicationCenter、生产 CSS/中文翻译、真实 ApplicationSettingsStore 和临时 IPC,从旧两项顺序读取后,移动知识库以及关闭且未常驻的本机推理监控,并用新 Store 实例确认磁盘结果。浅深主题各检查 1280、960、640、375px 目标宽度,实际 CSS 视口因缩放多约 1px;均显示八个排序箭头、四个可拖动卡片,无横向溢出,列数分别为三、三、二、一。详情不含排序,返回搜索焦点和重开顺序通过。最终证据目录为临时 `all-app-order-t0hDya`;已查看桌面浅色及窄屏深色截图。此项是组件与存储验证,未验收完整生产 Main/Preload 启动或安装包,原生指针拖放由组件事件测试之外另行验收。
中文文档运行 deai-writing 扫描,无阻断项;对本次改动的复核候选人工检查,保留明确的产品状态与验收限制。未调用模型,未修改模型管理、GPU 指标或 Agent 执行路径,未创建提交。下方记录保留各次实施时的行为,旧“固定位置”“仅常驻项排序”规则已由本条及现行设计替代。
## 2026-09-18:卡片排序与自适应列数
对应 FR-11、US-B7、US-D1、US-D2。上下移动从设置详情移到常驻可选应用卡片,箭头名称及提示包含应用名;首尾、唯一常驻项和保存锁定时禁用对应操作。拖动也仅允许常驻可选应用,固定应用及未常驻项不参与排序。卡片改用 `auto-fit/minmax`,最小宽度 `260px`,Modal 最大宽度仍为 `960px`;启用、常驻及笔记设置继续使用原详情表单。保留本机推理监控名称及默认不常驻改动。
Windows Electron 组件 fixture 使用当前 ApplicationCenter、生产 CSS/中文 i18n、真实 ApplicationSettingsStore 及临时 IPC。浅深主题各检查 1280、960、640、375px 目标窗口宽度,实际 CSS 视口受缩放约多 1px;分别为三、三、二、一列,均无横向溢出。检查排序持久化、详情无排序及返回搜索焦点,并人工查看桌面浅色、窄屏深色截图。证据位于临时目录 `cards-order-fixture-tucKjv`。完整 App 配合已有 Main/Preload 的两次启动尝试未产生窗口,此次 fixture 不作为完整 App 或安装包验收。
`npm run typecheck`、`npm run lint`、`git diff --check` 通过;`npm test -- src/renderer/src/ApplicationCenter.test.tsx src/renderer/src/ApplicationMenu.test.tsx src/renderer/src/LocalInferencePage.test.tsx src/main/application-settings-store.test.ts` 共 66 项通过,覆盖固定/未常驻项、边界、保存锁定、搜索排序及设置入口。全量 `npm test` 在 60 秒后超时,未取得完整结果。中文改动按 deai-writing 自查清单人工复核。未调用模型、修改模型管理或 GPU 指标,未创建提交。
## 2026-09-18:本机推理监控默认不常驻
对应 FR-11、US-B 的默认值与迁移场景。共享导航默认值改为本机推理监控不常驻;新安装和旧设置缺失整个导航配置时采用此值,应用仍默认启用,可从应用菜单及管理 Modal 打开。默认可选顺序及知识库、智能心跳的固定入口规则保持不变。沿用存储版本 12,已有常驻 `true`/`false` 均保留;旧默认值与主动固定没有来源标记,无法区分,不强制取消已有常驻。Demo 初始状态和恢复默认同步。
验证:设置存储、应用中心、菜单、推理 Modal 共 62 项测试通过,App 入口及设置同步 8 项定向测试通过;`npm run typecheck`、`npm run lint`、Demo 的 `node --check` 和 `git diff --check` 通过。全量 `npm test` 在 60 秒后超时,未取得完整结果。中文改动按 deai-writing 自查清单人工复核。本次未启动 Electron 或调用模型,不涉及 GoodBuddy Agent 或远程执行路径;未创建提交,保留此前未提交的名称调整。
## 2026-09-18:统一名称为本机推理监控
对应 FR-16、US-F3。导航、应用中心、应用设置、Modal 标题及关闭按钮统一使用“本机推理监控”,英文应用名为 Local Inference Monitor。现行功能文档与 Demo 名称同步;`local-inference`、API、组件名及服务行为保持不变,下方历史记录保留当时称谓。
验证:应用菜单、应用中心、Modal 的 27 项测试及 App 入口、设置同步相关的 8 项测试通过;`npm run typecheck`、修改源码 ESLint、Demo 的 `node --check` 和 `git diff --check` 通过。Demo 被 ESLint 既有规则排除。中文改动按 deai-writing 自查清单人工复核,仅同步名称及本条验证事实。本次未运行全量测试或启动 Electron 验收窗口排版。
## 2026-09-18:独立推理进程资源采样
对应 FR-16、US-F3。向量 utility process 已按 PID 接入 Electron 真实 CPU 和工作集内存,服务行显示进程范围、PID、百分比及 MiB。ASR 为主进程线程,OCR 为渲染进程 Web Worker,分别显示不能独立统计的原因。采样口径及基线规则见[技术设计](./technical-design.md)。服务 Modal 继续保持单一列表。
本轮验证:资源采样器、Main 服务、向量 Broker、Modal 共 39 项定向测试通过;IPC 与 OCR Bridge 另有 130 项通过,共 169 项。类型检查、修改范围 ESLint、`git diff --check` 通过。全仓 lint 仅报告未修改的 story-graph Demo 的 22 个 `document` 错误;`npm test` 超过 60 秒未完成,不记为通过。中文功能文档 deai-writing 扫描阻断项为 0,复核项中的采样限制和验证边界按事实保留。
真实 Windows Electron 验证直接调用生产 `createEmbeddingUtilityTransport` 和采样器,使用临时 CPU 工作负载:PID 5892 的工作集从 57,769,984 字节变为 63,090,688 字节,区间 CPU 为 98.71%;退出后返回不可用,重启得到 PID 20532 并重新预热。进程刚 spawn 时指标可能尚未登记,返回等待数据。此验证没有加载推理模型,没有调用模型 API,也没有重新验收完整 App 窗口;macOS、Linux 尚未实机验证。改动仅涉及桌面本机服务监视,不影响 GoodBuddy Agent 或远程执行。
## 2026-09-18:本机推理服务监视简化
对应 FR-16、US-F3、US-F5;US-F4 的 Modal 单任务取消入口退役。`LocalInferencePage` 直接显示服务列表,移除任务页签、历史、外部连接和长说明,保留刷新、模型设置及受支持的服务操作。现行定义见 [UI 设计](./ui-design.md#91-本机推理管理)。
当时没有服务级 CPU/内存采样,统一显示“暂不可用”;这一限制已由上方资源采样实现修正。ASR 模型可用时返回 `unknown`,登记活动请求不再将状态覆盖为 `running`。OCR/向量仍使用各自执行模块的状态。操作失败关闭旧确认并刷新,重试时重新确认当前影响;后端任务清单校验和 OCR 忙碌保护保留。
验证结果:
- 定向运行 LocalInferencePage、Main 服务、向量 Broker、OCR Bridge 和 IPC,5 个文件共 165 项通过;App 的本机推理入口和停用恢复场景 4 项通过。
- `npm run typecheck` 和修改范围 ESLint 通过。`npm run lint` 仍报告无关 `docs/features/story-graph/demo.js` 的 22 个 `document` 错误。
- `npm test` 首次超过 120 秒,再次超过 600 秒,均未完成。第二次报告的 App 失败来自旧页签断言,已改为服务列表并定向复验通过;不将全量测试记为通过。
- 中文功能文档按 deai-writing 扫描,阻断项为 0;复核项中的状态限制和操作清单按产品事实保留。
本轮未启动真实 Electron 或调用模型,未重新验证原生窗口排版。改动仅影响桌面本机服务监视,不涉及 GoodBuddy Agent 或远程执行路径。未创建提交,下方历史记录保留当时的 UI 与验证边界。
## 2026-09-17:应用卡片与紧凑详情
对应 US-B6 至 US-B9、US-F3 至 US-F5。已核对 `ApplicationCenter`、`LocalInferencePage`
及其样式:管理改为桌面双列、窄窗口单列卡片,固定应用仅有打开;可选应用提供打开和设置,
关闭时禁用打开。启用、常驻及上下移动位于最大 `640px` 的详情,卡片拖动排序保留。
详情使用紧凑返回标题栏,进入及返回时滚动归顶。本机推理使用内容高度 Modal,列表状态与操作
分区并响应式排列。现行规则见 [UI 设计](./ui-design.md#4-应用中心与共同设置)。
本轮实现验证结果由实现方提供,本次仅更新文档,未重跑产品测试:
- 59 项组件测试及 8 项选定 App 测试通过;`npm run typecheck`、修改范围 ESLint 和
`git diff --check` 通过。
- 真实 Electron 使用当前 Renderer、已有 Main/Preload、真实 IPC 和隔离配置,四种尺寸、浅深
主题共 32 个视图无横向溢出;导航、焦点、Escape、背景隔离及详情滚动重置已检查。
- 实机任务列表为空态;有任务及长名称由组件测试覆盖。模型调用为 0,未重跑全量测试,
未创建提交;此前全量测试与全仓 lint 的失败记录仍保留,不将整体验收标为完成。
实机报告已核对:`C:/Users/jiang/AppData/Local/Temp/opencode/application-cards-review-cUiVAc/result.json`,
截图位于同目录。下方记录保留各轮当时的布局及验证范围,旧单列管理行和旧 Modal 尺寸不作为现行定义。
## 2026-09-17:本机推理改为独立 Modal
对应 FR-3、FR-5、FR-16、US-B1、US-B6、US-F3 至 US-F5。已核对当前 `App.tsx`、
`LocalInferencePage.tsx`、`local-inference.css` 和 `dialog-focus.ts`:常驻入口、应用菜单及管理
Modal 的“打开”均打开独立本机推理 Modal,保留底层工作区;其他内置应用仍打开主内容区页面。
停止、重启和释放使用嵌套紧凑影响确认,复用背景隔离、焦点循环与恢复,按当前层处理 Escape,
操作提交期间禁止关闭及重复执行。具体行为见 [UI 设计](./ui-design.md#91-本机推理管理)。
以下结果由实现代理提供,本次文档更新未重新运行代码测试:
- 180 项定向测试通过。
- `npm run typecheck` 与修改范围的 ESLint 通过。
- 完整 App 测试运行超时,未取得完整通过结果。
- 全仓 lint 仍有 `docs/features/story-graph/demo.js` 的既有 22 个 `document` 全局声明错误。
随后完成真实 Electron 验证:当前 Renderer 源码配合当日已有 Main/Preload 构建,使用隔离配置,
未模拟 IPC。三个入口均保留同一知识库工作区节点;背景隔离、Escape、焦点恢复及窄窗口侧栏
开关回退通过。使用已有本机资产完成 OCR 模型加载与确认释放,推理请求和模型下载均为零。
浅深主题及窄窗口未出现横向溢出,释放确认框约为 480 × 182 CSS 像素,内边距 16px、按钮高 36px。
修正窄窗口关闭按钮换行后再次通过实机检查和 43 项相关测试。证据位于临时目录
`inference-modal-ui-8KJFgQ/result.json` 及同目录截图;未验证重新打包后的应用。
`npm test` 在 240 秒边界停止,未取得全量通过结果;独立复跑心跳数据库测试为 6 项通过、
2 项失败,均为已有的 `duplicate column name: pinned`。下方旧页面截图不作为 Modal 验收证据。
本轮已同步源码与文档,未修改 Demo,未创建提交。
下方保留各轮当时的实现与验证记录,旧主内容区页面及无 props 描述不作为当前 Modal 的行为定义。
## 2026-09-17:生产界面密度与响应式复核
对应 US-B5 至 US-B9、US-F3。已审阅 `abcd-production-08rUQA` 中 A、B、D 的原生产截图。
应用管理从全宽纵向块改为最大 `960px` 的内容高度 Modal:名称旁标明固定/可选状态,桌面操作
同行,窄屏自然换行且排序箭头成组。共同设置统一标签、说明和开关对齐;本机推理改为弱分隔列表。
菜单只调整共享字体和图标层级。展示规则见 [UI 设计](./ui-design.md#4-应用中心与共同设置)。
最终源码验证:
- 应用中心、应用菜单、本机推理、设置面板和共享 UI 的 5 个定向文件共 148 项通过。
- `npm run typecheck`、改动 Renderer 文件的定向 ESLint、`git diff --check` 通过。
- `npm run build:bundle` 成功;最后一处布局修正后,`npx electron-vite build --logLevel error` 再次成功。
- 全仓 `npm run lint` 仍有 `docs/features/story-graph/demo.js` 的 22 个 `document` 全局声明错误,未改动该文件。
- 完整 `npm test` 用时 `648.80s`:4,454 项通过、67 项跳过、2 项失败。失败仍为心跳数据库
迁移的 `duplicate column name: pinned`,位于未改动的 `assistant-database.ts:9988`;全量测试
未通过。完整日志位于临时目录 `abcd-polish-tests-VxXZ34/test.log`。
真实 Electron 使用仓库 package 入口、当前生产 Main/Preload/App 和独立用户目录,未替换 IPC。
最终截图与测量位于临时目录 `abcd-production-m9I4Vy`:`1200×775` 下四行约 `75px` 高,管理
内容区 `scrollHeight` 与 `clientHeight` 均为 `420px`,操作同行;`520×740`、`375×667` 下
管理、共同设置和推理内容无横向溢出。已审阅桌面与窄屏浅深色截图、375px 管理和设置截图。
管理行按钮为 `34px`/`36px`,Switch 标签点击区高 `36px`,评论选项保留共享紧凑分段控件。
生产验证覆盖菜单 End/Escape、Modal 焦点保持、页签方向键,以及 Space 切换常驻后重载 Renderer
确认保存。运行报告为同目录 `result.json`,截图为 `A-menu*.png`、`B-manage*.png`、
`C-settings*.png`、`D-inference*.png` 和 `D-tasks.png`。
此次为界面调整,未改后端生命周期;隔离目录没有安装模型,模型请求为 0,仅检查真实服务快照与任务空态。
日志仍有既有字体 CSP 拦截,未捕获 JavaScript 异常。未验证模型启停、安装包或整个项目的功能验收。
工作区原有 Runtime 与 story-graph 修改保留,未创建提交。
## 2026-09-17:轻量应用菜单与两个固定应用
对应 FR-3、FR-5、FR-11、FR-12、US-B5 至 US-B8、US-D1/US-D2。当前生产源码的底部应用中心向上展开轻量锚定菜单,无模态遮罩,只列已启用应用,不按打开历史或常驻状态筛选。点击应用行关闭菜单并打开主内容区;底部“管理应用”打开既有管理 Modal。
[ApplicationMenu](../../../src/renderer/src/ApplicationMenu.tsx) 与 [AnchoredMenu](../../../src/renderer/src/AnchoredMenu.tsx) 承担菜单及键盘、定位;[App](../../../src/renderer/src/App.tsx) 固定依次显示知识库、智能心跳,再显示已启用且常驻的可选应用。[管理 Modal](../../../src/renderer/src/ApplicationCenter.tsx) 中两个固定应用仅提供“打开”,不提供启用、常驻、排序或通用空设置;原工作区配置及心跳单条计划 enabled 不变。
[设置契约](../../../src/shared/application-settings-contracts.ts) 的可选偏好仅包含 `magic-notes`、`local-inference`。未发布草案中的 `knowledgeEnabled`、`heartbeatEnabled` 已删除,不增加迁移或兼容读取。[Store](../../../src/main/application-settings-store.ts) 持久化后发布权威快照的链路保留,包含配置工具写入;重开刷新期间锁定修改,较新快照使迟到的旧读取和保存响应失效。
本节收录本轮提供的最新验证结果;此次文档编辑未重新运行产品测试或模型请求:
- `npm run typecheck` 通过,`npx eslint src` 通过。
- 完整 `npm test` 已结束:4,448 项通过、67 项跳过、2 项失败。两项仍为心跳数据库迁移的 `duplicate column name: pinned`,相关迁移未改动;全量测试未通过。
- 全仓 `npm run lint` 仍有 `docs/features/story-graph/demo.js` 的 22 个 `document` 全局声明错误,属于无关文档 Demo;全仓 lint 未通过。
- Electron fixture 共 8 个主题/尺寸场景通过,覆盖原生键盘、焦点、菜单几何、原生视图遮挡及实际 `ApplicationSettingsStore` 持久化。此证据验证组件 fixture 和真实设置存储,不是完整生产 App 的端到端验收,也不是安装包验收。
完整生产 App 补充验证:`npm run build:bundle` 成功后,以隔离用户目录启动仓库 package 入口,
使用实际 `out/main/index.js`、生产 Preload 和 App,未替换 IPC handlers。已验证上拉菜单、
“管理应用”Modal、两个固定应用仅有“打开”、笔记常驻切换,以及刷新 Renderer 和磁盘配置的
持久化结果;本机推理页通过真实注册 IPC 取得 ASR、OCR、向量和 TTS 状态。已审阅菜单与管理截图。
证据位于临时目录 `abcd-production-08rUQA` 的 `result.json`、`A-menu.png`、`B-manage.png`、
`D-inference.png` 和 `runtime.log`。生产启动日志有字体被 CSP 阻止的消息,未捕获 JavaScript 异常。
此轮未重启整个应用验证持久化;隔离目录没有安装模型,未执行模型启停或模型请求。
A–D 已接入源码并通过上述生产 App 冒烟检查;不将全量测试、安装包及整个计划标为通过。
D 的真实 ASR、OCR、向量引擎证据及最小 Main 接线限制保留在下方原记录中,本次状态读取不扩大
其模型操作验证范围。本机资源监控仍待实现。Demo 的直接 Modal 和知识库/心跳可选设置已过时,本轮未编辑 Demo 文件。
下方为历史记录,保留当时实现、测试结果及限制,旧应用级开关和旧入口描述不作为现行行为定义。
## 2026-09-16:知识库固定入口与设置同步更正
本节更正下方早期记录的现行行为,历史实现和验证记录原文保留,不作为最新修订已重新通过验证的证据。
A–D 的应用中心 Modal、可选应用共同设置、常驻排序及本机推理均已接入生产源码,整体验收边界仍见各次记录。
本次文档核对的源码证据:
- [共享设置契约](../../../src/shared/application-settings-contracts.ts) 将可编辑 ID 限为魔法笔记、智能心跳、本机推理;知识库不参与启用、常驻或排序。版本 12 为未发布分支格式,`knowledgeEnabled` 已移除,无兼容旧草案要求。
- [应用中心](../../../src/renderer/src/ApplicationCenter.tsx) 的知识库行只有“打开”;[App](../../../src/renderer/src/App.tsx) 在首个可选槽位后插入固定知识库,再过滤可选应用。知识库始终显示,原工作区实际配置保留;旧的禁用知识库请求守卫说明已从现行设计中删除。
- [设置 Store](../../../src/main/application-settings-store.ts) 持久化成功后发布 `onChanged`;[IPC](../../../src/main/ipc.ts) 转发完整快照,覆盖[配置工具服务](../../../src/main/goodbuddy-config-service.ts) 的写入。App 重开中心时刷新并锁定修改,较新的事件使在途旧读取和保存响应失效。
- [向量 Broker](../../../src/main/knowledge/embedding-inference-broker.ts) 区分调用方返回与 Worker 执行结束;取消或超时后,执行记录在 Worker 确认或退出前仍占用在途名额并计入活动任务和服务停止影响。
本次仅更新文档并核对源码、链接和文本一致性,未运行产品测试或实机验证,也未发起模型请求。
README 已标明 Demo 中知识库启停、常驻及通用设置描述待独立同步;本次未修改 Demo 文件。
## 2026-09-16:D 本机推理生产接入
对应 FR-16、US-F3 至 US-F5。`LocalInferencePage` 默认导出、无 props,已与中心代理的
`local-inference` 页面入口对接,未修改 App 生产入口或应用启用契约。真实支持范围见
[技术清单](./technical-design.md#111-本机推理任务与执行服务)。ASR、OCR、向量生产请求均登记
生命周期,服务操作和任务取消独立。内置向量 Broker 按模型目录共享;OCR 直接控制持有模型的
Renderer Worker。没有新增任务持久化、虚构进度、资源值或 PID。
验证证据:
- 原始 Vitest 配置运行 App、IPC、应用中心、设置存储、推理服务、ASR、OCR、向量 Broker、
Worker 和引擎,共 12 个文件、452 项通过。完整 App 测试使用真实页面模块,没有 resolve 替身。
- 收尾新增 OCR 加载失败恢复用例后,11 个定向文件 202 项通过;加上已通过的完整 App 251 项,
当前覆盖合计 453 项。最终 `npm run typecheck`、D 源码及测试定向 ESLint、`git diff --check`
均通过;diff 检查仅有工作区已有换行符提示。验证生成的临时 bundle 已清理。
- Windows Electron 隔离用户目录,加载当前页面与生产 CSS、生产 Preload、实际引擎模块,
通过临时最小 Main IPC 接线运行已安装的 SenseVoice、PaddleOCR、Granite 模型,未修改模型配置。
最终一轮:3 次向量请求(含完成/取消竞态)、1 次 ASR 静音采样请求、1 次 OCR 图片请求。
向量返回 384 维;UI 停止、启动后再次推理成功;隐藏窗口保持服务运行;OCR 识别 `HELLO`,
UI 加载与释放成功;1080px 和 520px 窗口无横向溢出。未调用外部付费模型。
- 实机取消竞态中执行方自然完成,管理页正确保留完成结果;取消确认和共享进程不被终止的分支
另由 Broker 回归测试验证。停止失败、启动失败重试、新任务影响清单变化由定向测试覆盖。
- 真实向量验证发现浏览器 ONNX `/wasm` 入口无法 fetch 本地资源;Main 改为 Node 条件入口后
实机推理通过,具体执行错误可回传任务列表。
限制与阻塞:本轮不是打包安装包验收,也未驱动完整生产 App 的所有入口与原生浏览器视图。
实机测试 Main 使用最小接线,完整生产 IPC 注册和 App 接入由上述原始测试及类型检查覆盖。
全量 `npm test` 在 200 秒预算内未结束;已复现心跳迁移两项 `duplicate column name: pinned`,
独立重跑仍失败,和应用设置 v12 期望值无关,未修改该数据库迁移。全仓 lint 仍被并行工作目录
`docs/features/story-graph/demo.js` 的 22 个 `document` 全局声明错误阻塞。不将全量测试或 lint
标为通过。D 不修改 GoodBuddy Agent 或远程执行链路,无远程主机验证要求;没有创建提交。
本节取代下方早期记录中“缺少 LocalInferencePage”的当前阻塞描述,早期验证范围仍保留供追溯。
## 2026-09-16:应用中心、共同设置与知识联动
对应 US-A2、US-B5 至 US-B9。当前源码接入左下角应用中心和独立设置按钮;中心使用设置 Modal
壳层,包含搜索、打开、常驻、拖动及上下移动。四个稳定 ID 为 `magic-notes`、`knowledge`、
`heartbeat`、`local-inference`。应用启用、常驻和顺序独立持久化,固定入口及右侧工具不参与排序。
`ApplicationSettingsStore` 升为版本 12,保留已有 `false` 和评论单选值,为缺失项补默认值。
笔记页与中心使用 `ApplicationSettingsView`;全局笔记入口已移除。保存期间共用锁,失败后读取
确认值,无法确认时锁定修改并允许重新读取。关闭当前应用保留页面挂载及草稿,显示恢复入口。
知识关闭后移除会话知识控件与摘要,保留各会话关联;Main 交互请求入口在授予知识能力及
检索前裁剪新请求范围,不取消已开始的请求。队列派发重新经过同一入口,本机和托管 SSH
分支消费同一个请求对象。未修改 Skills、MCP、专家或本机推理页面与后端实现。
验证记录:
- 合并定向运行 App、IPC、配置与 Renderer 共 10 个 suite,599 项全部通过;随后新增本机推理
主内容区入口用例,完整 App suite 251 项通过,合计覆盖 600 项;
App 仍使用下述临时页面契约替身,其余生产代码未替换。
- `ApplicationCenter.test.tsx`、设置、笔记、更新设置、共享界面与浏览器遮挡及配置契约的
定向回归共 227 项通过;随后增加的保存失败、缺失笔记配置回归与配置契约共 39 项通过。
- `ipc.test.ts` 和中心回归共 127 项通过,包含新请求禁用、在飞请求继续及重新开启的范围恢复。
- `App.test.tsx` 共 251 项通过;因独立负责的 `LocalInferencePage.tsx` 尚未到位,使用临时
Vitest resolve 插件提供无 props 空组件,仅用于验证 App 壳层,未替代或写入生产页面。
- Windows Electron 使用当前中心组件、生产 CSS、真实 `ApplicationSettingsStore` 与临时
最小 IPC 桥接验证常驻、排序、启用及重新读取持久化结果。浅深主题各验证 `1200 × 850` 和
`680 × 560` 窗口,无水平溢出;搜索初始焦点、Escape、关闭焦点恢复及背景隔离通过。
已审阅窄浅色和宽深色截图;证据位于本轮临时目录的 `application-center-*.png`。
- 全量 `npm test` 的 JSON 报告为 4,153 项通过、4 项失败、67 项跳过,另有 suite 加载失败。
其中应用配置契约旧快照已修正并定向通过;其余失败包括心跳旧库迁移两项、工作栏终端标题一项。
终端标题用例随后单独复跑通过,未修改其实现。
App suite 因缺少 `LocalInferencePage` 无法在原始配置下加载,不能将全量检查记为通过。
- `npm run typecheck` 当前仅报告缺少 `./LocalInferencePage`;`npm run lint` 当前仅报告
并行工作目录 `docs/features/story-graph/demo.js` 的 22 个 `document` 全局声明错误。
本轮修改的生产源文件定向 Lint 通过。
剩余集成验证:接入另一实现负责的默认导出 `LocalInferencePage` 后重跑原始 App 测试与类型
检查;完整应用的原生浏览器遮挡和共享 Linux Host 真实请求尚未实测。本轮真实模型调用为 0。
本节不宣称本机推理后端、管理页或整个计划已交付,也没有生成提交。
## 2026-09-16:设置改为大 Modal
对应 [US-A2](./user-stories.md#us-a2-从固定入口进入设置)。设置入口保留底层工作区,关闭后卸载
设置内容;布局、关闭与分类直达规则见 [设置中心 UI 规范](../../../UI-DESIGN.md#138-设置中心)。
`App.test.tsx`、`SettingsPanel.test.tsx`、`WorkspacePrimitives.test.tsx` 和
`browser-viewport-occlusion.test.ts` 定向回归共 389 项通过。`npm run lint` 通过。
全量 `npm test` 完成时为 4,354 项通过、9 项失败、67 项跳过;失败涉及本地 Runtime 复用超时、
心跳旧库迁移、远程恢复及 ProjectSwitcher。共享工作区存在并行修改,最终 `npm run typecheck`
报错位于非本次修改的 `use-unviewed-completions.test.tsx`,未将全量检查记为通过。
Windows Electron 使用临时用户目录、当前 Renderer 源码和已有 Main/Preload 输出,验证窗口
尺寸 `1440 × 960`、`1024 × 700`、`680 × 560`(系统缩放使实际 CSS 高宽略有偏差)。
浅色与深色检查通过:整窗背景隔离、左右分栏、无内容横向溢出、遮罩点击不关闭、原生 Tab 与
Escape、关闭后保留原知识库组件。截图保存在本轮临时验证目录;没有运行打包构建,也未单独
驱动原生 `WebContentsView`,其遮挡规则通过共享检测测试验证。本次未修改 GoodBuddy Agent
或桌面到 Agent 的执行路径,未额外发起真实模型请求。
## 2026-09-16:已完成保留至查看
对应 [FR-14](./prd.md#fr-14-项目活动汇总)、[US-E3](./user-stories.md#us-e3-完成后保留至查看)。
已核对当前工作区 `use-unviewed-completions.ts`、`conversation-activity.ts` 和 `App.tsx` 的
完成通知接入:Renderer 会话内集合、后台 Task 转换、本地及持久化活动运行成功标记、
三类计数,以及聊天、设置和文档可见性条件。行为定义见[活动逻辑](./logic-design.md#9-项目活动汇总)。
活动汇总、完成通知 Hook、项目活动组件、项目选择器及 App 活动集成定向回归共 77 项通过。
覆盖前台完成、后台完成、持久化完成补齐、失败和取消、普通会话入口及级联菜单进入、
设置覆盖聊天、窗口隐藏、历史完成不提醒、查看后刷新不重复提示和下一轮完成。
实际聊天详情加载完成后才清除提示。最终 `npm run typecheck` 与 `npm run lint` 通过。
全量 `npm test` 用 900 秒预算运行,762.20 秒结束:4,365 项通过、3 项失败、67 项跳过。
失败为 `agent-package.test.ts` 离线依赖清单安装内部 60 秒超时,以及
`heartbeat-database.test.ts` 两项迁移的 `duplicate column name: pinned`。
共享工作区存在并行修改,此项未改动这些文件,不将全量检查记为通过。
Windows Electron 43.2.0 / Chromium 150 的当前组件 fixture 验证浅深主题、1280×800 和
375×667、混合状态及仅完成状态,共 8 个场景、112 项检查通过,8 张 PNG 已审阅。
检查包含完成图标、三类计数、原生方向键与 Space、进入回调及移除后的空闲隐藏。
证据为临时目录 `activity-completion-0916-results.json` 和 `activity-completion-0916-review.md`。
此项使用受控活动数据,未验证完整 Main/Preload 真实模型端到端;模型及知识库请求为 0。
本次只修改 Renderer 展示与状态跟踪,本地和远程均消费已有事件/Task 更新,不改变 Agent 执行路径。
## 2026-09-14:菜单独立高度修正
生产菜单移除共用双栏表面,会话层独立定位到选中项目行。打开较长会话列表时,项目层的位置、
宽度与高度保持不变;会话层独立限制窗口边界和滚动。两侧均无法容纳子菜单时使用同层返回交互。
`npx vitest run src/renderer/src/ProjectActivity.test.tsx` 通过 13 项,类型检查与 Lint 通过。
`npm test` 运行 60 秒后超时,未取得完整结果。
最终源码的 Electron 43.2.0 / Chromium 150 组件 fixture 使用生产样式与中文 i18n、模拟活动数据,
深浅主题共 14 个场景、108 条检查通过,覆盖短/60 条会话、两层独立滚动、底部避让、向左展开、
375px 窄屏及 640px 两侧空间不足。桌面项目层打开前后矩形一致,短窗口会话层底部保留 16px。
此项验证实际组件排版和交互,不代表完整 Main/Preload 端到端验证;未修改远程执行路径。
## 2026-09-13:生产活动入口
对应 [FR-14](./prd.md#fr-14-项目活动汇总)、[US-E1 / US-E2](./user-stories.md#6-epic-e项目活动级联)。
本页只记录项目活动入口,不表示应用中心计划已交付。
`App` 已接入 `ProjectActivity` 常驻摘要和锚定级联菜单。会话和项目计数继续使用现有
`deriveConversationActivity` 结果,点击会话继续调用 `openActivityConversation`。未增加
持久化字段、IPC 或 Runtime 状态。项目选择器分组及计数保持原有行为。
组件与 App 定向回归通过 32 项,覆盖空闲入口、稳定项目及 SSH Host 顺序、待处理优先、
跨项目精确跳转、设置离开检查、实时状态清理、键盘返回、外部关闭和窄侧栏焦点。
Windows Electron 视觉 fixture 使用当前 `ProjectActivity`、生产样式和 i18n,活动数据为
受控样例。浅深主题分别验证 `1280×800`、`960×720`、`720×640`、`640×420`、`375×600`;
原生键盘验证 Enter、上下及右方向键、End、两级 Escape、Tab 退出,另测左侧回退和空闲入口。
此项证明组件的实际 Chromium 排版与交互,不代表完整生产 Main/Preload 的端到端验证。
浏览器遮挡复用共享菜单相交机制,组件测试检查相交和非相交结果;本轮未单独驱动原生
`WebContentsView` 可见性。远程 Agent 执行路径未改动,未发出模型或知识库请求。
提交前复核:`npm run typecheck`、`npm run lint` 通过;单独运行
`npx vitest run src/renderer/src/ProjectActivity.test.tsx`,11 项通过。
`npm test` 在 240 秒后超时,超时前 `tests/agent-package.test.ts` 的
`verifies and installs offline dependency inventories larger than one MiB` 用例失败,未取得完整结果。
Demo 已按最新讨论隐藏空闲摘要并移除会话行箭头;生产空闲摘要在下述非驻留修正中对齐。
后续界面修正已移除生产会话行箭头,并为活动摘要与下方新建会话增加 `8px` 间距。
## 2026-09-14:摘要非驻留修正
生产 `ProjectActivity` 在无活动时返回空内容,活动归零时关闭级联菜单;新活动只恢复摘要。
组件回归 11 项通过,覆盖初始空闲、活动归零关闭菜单及活动恢复后不自动展开。
App 的 `project activity integration` 定向回归 18 项通过,类型检查与 Lint 通过。
本轮 `npm test` 在 60 秒后超时,未取得全量结果;未重新执行 Electron 视觉验证。
## 2026-09-14:0.13.2 候选验证
正式发布准备时再次复现 App 问答用例的时序失败。活动摘要改为常驻后,等待该入口出现
不再意味着会话初始化和 Runtime 状态读取已经完成;测试有时点击仍被禁用的发送按钮,
留下未消费的一次性问答 mock,继而影响后续用例。
测试现等待已加载的会话和可用发送按钮,并在每个用例前重置问答及审批响应 mock。
答案重载用例等待提交后的界面状态,而不只等待 API 被调用。未修改生产交互、
未增加固定延时,也未跳过失败用例。相关 20 项连续三轮通过,完整 App 与 Agent
发布说明检查共 227 项通过。
最终候选 `npm test` 通过 4,155 项、跳过 66 项、失败 0 项;`npm run typecheck`、
`npm run lint` 和 `npm run release:notes:verify` 均通过。发布准备未执行本地生产构建、
打包或安装包启动探针,原生包验收由主分支与标签 CI 完成。
@@ -0,0 +1,502 @@
# 应用导航与系统工具入口技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 应用中心与本机推理监控已接入源码,整体验收待完成 |
| 版本 | 0.4 |
| 日期 | 2026-09-17 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| UI 设计 | [UI 设计](./ui-design.md) |
## 1. 现状
主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换;该类型也包含由独立状态打开 Modal 的入口:
```ts
type WorkspaceView =
| 'chat'
| 'magic-notes'
| 'knowledge'
| 'heartbeat'
| 'local-inference'
| 'activity'
| 'settings'
```
文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts`
承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程
实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控
面板。侧栏底部已替换为应用中心与独立设置按钮。
应用中心、四应用共享排序、可选应用常驻及启用联动、共同设置页和本机推理监控已接入生产源码;本机资源监控的
Main/Preload/Renderer 实现仍待交付。系统工具只使用右侧工作栏已有 Tab 和“+”链路。
行为由[功能逻辑](./logic-design.md)定义,验证范围见[实施进度](./progress.md)。
## 2. 模块边界
| 模块 | 责任 |
| --- | --- |
| `App.tsx` | 组合主导航、应用中心、固定设置,持有确认配置及页面离开检查 |
| `ApplicationMenu.tsx` / `AnchoredMenu.tsx` | 底部向上锚定的已启用应用菜单;关闭后导航,或打开既有管理 Modal;共享几何定位与键盘行为 |
| `ApplicationCenter.tsx` | 浏览、搜索内置清单,调用既有页面导航及共同设置入口,编辑常驻和顺序;复用设置 Modal 样式和焦点工具 |
| `ApplicationSettingsView` | 位于 `ApplicationCenter.tsx`,由应用中心对应行进入的应用配置表单 |
| `RightAssistantSidebar.tsx` | 保留右侧工具 Tab 和“+”目录调用链路 |
| `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 |
| `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 |
| `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 |
| `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 |
| `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 |
| `application-settings-contracts.ts` | 定义常驻、顺序及应用启用字段;保留笔记既有配置键 |
| `application-settings-store.ts` | 迁移默认值,串行合并更新并原子保存,返回完整确认快照 |
| `PlatformFeaturesSettingsSection` | 移除全局笔记设置页签、跳转入口及专用回调,保留通用设置和远程项目 |
应用中心只编排应用导航和配置,不读取文件、创建 PTY、操作浏览器或采集系统指标。
不增加左侧工具组件、左侧工具状态镜像或主侧栏到工作栏的专用命令通道。
## 3. Renderer 契约
### 3.1 内置应用清单
```ts
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat' | 'local-inference'
type BuiltInApplicationDefinition = {
id: BuiltInApplicationId
source: 'builtin'
titleKey: string
descriptionKey: string
view: BuiltInApplicationId
}
```
全部应用 ID 在 `application-settings-contracts.ts` 的 `BuiltInApplicationId` 定义;`EditableApplicationId` 包含 `magic-notes`、`heartbeat`、`local-inference`。Renderer 的 `applicationDefinitions` 映射图标、国际化键和可选应用启用键,知识库始终可用。
`App.tsx` 默认导入 `LocalInferencePage`,传入 `onClose`、`enabled` 和 `restoreFocus`。`local-inference` 保留为入口 ID,`setView` 在提交工作区切换前将其转为 `localInferenceOpen = true` 并返回,不更新 `view` 或 KeepAlive 缓存;组件按该独立状态挂载及卸载。
静态清单复用既有应用页面和国际化键。管理 Modal 的搜索只过滤该清单,包含未常驻和关闭的应用,不请求
远程目录。启用、常驻和排序从确认配置派生,不在清单中持久化第二份状态。
### 3.2 应用与设置导航
应用中心调用既有 `navigateFromSidebar` 或同一页面导航函数,沿用未保存离开检查和焦点
策略。打开前检查确认的启用值;未常驻但已启用的应用不附带常驻写入。本机推理监控从常驻入口、菜单及管理 Modal 均经 `setView('local-inference')` 打开独立 Modal,保留底层工作区与设置草稿;其他应用继续页面导航。
`ApplicationMenu` 按四个应用共用的持久化顺序派生清单,仅按启用过滤,不读取常驻或打开历史。应用行先关闭菜单再导航;底部“管理应用”关闭菜单、恢复锚点焦点并打开既有 `ApplicationCenter` Modal。
应用中心对应行的设置操作仅接受 `EditableApplicationId`;应用页和全局设置不提供重复入口。知识库的管理行提供“打开”和排序,原工作区实际配置保留,不添加通用空设置页。
共同页复用设置 Modal 容器、配置读取及更新方法,不建立独立配置存储或重复表单。
### 3.3 工作栏选择与创建
已有 Tab 仅按实例 ID 激活,保留目标和历史状态。不得按当前项目、执行空间或 Conversation
查找最近匹配实例替代用户选择,也不在选中时自动重建退出或失败终端。
“+”选择终端每次调用既有创建流程,解析当前 `ExecutionSpaceSelection`,绑定本机或托管
SSH 项目的权威目录;无项目时使用本机 Home。选择浏览器每次创建额外实例,绑定当前
Conversation,无 Conversation 时创建待绑定空实例。同一 Conversation 的浏览器页面独立,
登录上下文共享规则继续由工作栏负责。文件按单实例聚焦,缺失时仅由布局修复补回。
选择现有文件 Tab 保留跟随或固定模式,固定目标失效时不切换目标。
### 3.4 资源监控工作栏应用
在 `workbar-contracts.ts` 中增加 `resources`:
```ts
{
id: 'resources',
instancePolicy: 'single',
defaultContext: 'application'
}
```
该应用不加入升级后的默认打开实例,仅通过右侧已有 Tab 或“+”目录访问,使用既有单实例
规则创建或聚焦。左侧应用清单不包含 `resources`。
## 4. 应用导航偏好
### 4.1 设置结构
在 `ApplicationSettings` 增加独立字段:
```ts
type ApplicationNavigationSettings = {
order: BuiltInApplicationId[]
pinned: Record<EditableApplicationId, boolean>
}
```
`applicationNavigation` 保存四个应用的完整顺序和三个可选应用的常驻布尔值。默认顺序为
`knowledge`、`heartbeat`、`magic-notes`、`local-inference`;心跳和魔法笔记常驻默认 `true`,本机推理监控默认 `false`。
权威返回 schema 和更新 schema 均要求四个 ID 各出现一次,拒绝重复、未知和缺失 ID;`pinned` 包含三个可选应用,缺失 `heartbeat` 时补 `true`,显式布尔值保留。Renderer 直接读取该顺序,再按各入口的可见性过滤。知识库始终启用、常驻,无可写开关键。
按[排序规则](./logic-design.md#3-主导航组合规则)提交完整顺序;不持久化可推导的可见 ID 列表。
首期不预建插件字段,也不为未发布的旧隐藏 ID 草案增加迁移读取器。
### 4.2 与现有开关组合
可选应用侧栏入口可见条件如下,知识库按保存位置始终显示:
```ts
const visible = featureEnabled && applicationNavigation.pinned[id]
```
`magicNotesEnabled` 保留为笔记启用的唯一权威键,`localInferenceEnabled` 为本机推理监控应用启用键,默认 `true`,不在导航配置再复制 enabled map。`heartbeatEnabled` 控制心跳与监督者应用;Renderer 仅将 `settings?.heartbeatEnabled === true` 视为启用,设置未加载时也不开放入口。默认值及运行边界见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行),Main 与侧栏接入见[监督者运行路径](../conversation-supervision/technical-design.md#2-运行路径)。关闭可选应用时保留常驻控件和恢复入口。
笔记配置使用 `magicNotesShowIncompleteTodoCount`、`magicNoteCommentMode`、
`magicNoteCommentFormat` 和 `magicNoteCanvasPageCount`,应用中心与笔记页使用同一 schema 和更新链路。
画布发送页数默认 1、范围 1~8,具体选择规则见[画布分析契约](../magic-notes/technical-design.md#canvas-analysis-and-comments)。未配置的笔记
布尔值(含 `magicNotesEnabled`)默认 `true`;迁移必须用字段是否存在区分缺失与显式 `false`,
不能用 `value || true`。已有单选值保留,缺失时分别使用 `immediate` 和 `combined`。
若旧“显示入口”实际绑定 `magicNotesEnabled`,保留其启用含义和原值;独立常驻使用新字段,
不得直接把旧业务启用键改名为 pinned。只为确认已发布的独立显隐偏好增加对应迁移。
### 4.3 持久化与 IPC
沿用现有应用设置链路:
```text
Renderer
-> window.goodbuddy.updates.updateSettings
-> settings:application:update
-> application-settings-store
-> application-settings.json
```
Main 继续校验可信 sender、Zod 输入,复用 `ApplicationSettingsStore` 的更新队列,读取最新
确认值后合并本次 patch、原子写入并返回完整 `ApplicationSettings`。写入失败不发布目标值。
存储版本 12 补充可选应用导航和启用字段,为缺失字段补默认值;保留已有
`true`、`false`、单选值和无关设置,不迁移或清理笔记、知识及会话业务数据。
新安装和升级时缺失整个 `applicationNavigation` 均使用共享默认值,本机推理监控启用但不常驻;本次不增加存储版本。已保存的常驻值均保留,无法区分旧默认 `true` 与用户主动固定,不强制改为 `false`。
仅存储读取允许补齐旧数据:缺失的 `knowledge`、`heartbeat` 按此顺序前置,再保留已有 ID 的相对顺序,最后按默认顺序追加缺失的可选应用。旧 `[local-inference, magic-notes]` 因此变为 `[knowledge, heartbeat, local-inference, magic-notes]`;完整四项顺序原样保留。缺失或空顺序采用默认顺序,部分 `pinned` 映射仅补缺失键,显式 `false` 保留。重复或未知 ID 仍按现有损坏文件处理规则处理,不静默去重或丢弃。版本 11 迁移时写入归一化结果;版本 12 读取时归一化,下一次设置保存时写入完整结果。IPC 和配置工具写入不接受这些部分数据。
版本 12 属于未发布的分支格式,原草案 `knowledgeEnabled` 已移除,不增加兼容读取器。`heartbeatEnabled` 是当前应用启用字段,由 Store 归一化缺失值并保留显式保存值。
## 5. 侧栏底部重构
原 `user-card` 整行设置按钮已替换为容器和相邻独立按钮:
```tsx
<div className="sidebar-footer">
<button onClick={toggleApplicationMenu} aria-haspopup="menu" aria-expanded={applicationMenuOpen} ...>
{t('navigation.applicationCenter')}
</button>
<button aria-label={t('navigation.settings')} ...>
<Settings aria-hidden="true" />
</button>
</div>
```
应用中心和设置为相邻独立按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
保留设置预加载、焦点恢复和未保存离开确认。该调用只打开设置 Modal,不更新当前工作区或
KeepAlive 缓存;`settingsOpen` 独立管理打开状态。关闭后卸载设置内容,下次按入口指定的分类
和消息通道重新挂载,避免缓存上次分类覆盖本次直达目标。底层工作区继续挂载,普通关闭不跳回聊天。
应用菜单使用 `AnchoredMenu` 的 body Portal,向上锚定,无模态遮罩或背景隔离;`menu` 语义纳入共享浏览器 viewport 相交遮挡。管理 Modal 及共同设置沿用原 Portal、焦点隔离、恢复及 Escape 机制。
不在主侧栏复制工具入口。工作栏仍由窗口右上方固定开关展开和关闭,关闭后焦点返回该开关。
## 6. 应用可用性与知识请求
Renderer 从同一确认快照派生可选应用主导航、中心状态和页面可用性,依赖只包含相应
字段。keep-alive 页挂载不代表应用仍启用;禁用后在原位置渲染不可用状态,
保留页面草稿及业务数据,阻止新的页面操作。重新启用不触发导航或业务重放。
`ApplicationAvailability` 隐藏并设置业务内容为 inert,保留挂载的页面实例;重新启用从
应用中心对应行的设置进入。笔记页面直接消费确认的评论配置,
不因修改评论方式而重建编辑器。
知识库不经过 `ApplicationAvailability`;会话知识按钮、范围、检索方式和标签继续遵守原知识库规则。心跳页面受应用可用性控制,单条计划 enabled 保持不变。
请求构建没有应用级禁用知识库的裁剪或守卫;本机、托管 SSH 和排队请求沿用原知识上下文链路。
单个知识库实例的配置、停用、删除及取消清理仍由知识库功能负责。
## 7. 串行保存与确认快照
中心与应用页的启用、常驻、排序及笔记配置共用一个 Renderer 更新入口及 pending
状态。任一提交未完成时,所有入口中修改这组设置的控件暂时禁用;下一次操作基于最新确认
快照生成 patch,不提交整个过期 `ApplicationSettings` 覆盖无关字段。Main 队列仍负责串行合并。
`ApplicationSettingsStore.update` 在持久化成功后通过 `onChanged` 发布完整快照;IPC 转发
`applicationSettingsChanged`,Preload 提供 `updates.onSettingsChanged`。工具配置服务调用同一 Store,
因此配置工具写入也同步界面,保存失败不发事件。`App` 收到事件后推进读取修订号并替换快照,
迟到的旧读取及旧保存响应不能覆盖新状态。每次重开中心都刷新配置,刷新期间锁定修改,失败后
保留锁和重试入口;控件不先乐观生效。
明确保存失败时保留最后确认值,就地显示可重试错误与编辑上下文。若 IPC 响应中断而写入
结果未知,通过既有设置读取重新取得权威快照再解锁;读取失败继续显示错误并允许重试读取,
不自动重发变更或写回旧配置。无须引入持久化回滚日志或独立导航快照文件。
## 8. 插件兼容边界
首期不增加市场 URL、YAML 导入导出、ShareServer API、插件注册表或安装服务。FR-15 的
私有市场协议、YAML Schema、资源分发与扩展生命周期另行设计;系统 ID 不向插件开放。
不得直接复用 DeepSeek Harness 的 npm 插件市场。后续依赖见第 11 节,首期不预建占位界面。
## 9. 本机资源监控
### 9.1 采集范围
首期在 Main 中使用经过依赖审查的 `systeminformation` 获取整机 CPU、内存和磁盘 IO,使用
Electron `app.getAppMetrics()` 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收
Renderer 提供的路径、PID、设备名或命令。
首期指标定义:
- CPU:整机忙碌时间占比,规范化为 `0` 至 `100`。
- 内存:总字节数、可用字节数和 `总量 - 可用量` 得到的使用量。
- 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。
- GoodBuddy:`app.getAppMetrics()` 返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程
CPU 先相加,再按逻辑处理器数量归一化为整机容量占比 `0` 至 `100`;不能通过截断总和处理
多核数值。
受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回
不可用错误码,不能回退为 `0`。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链
检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。
### 9.2 快照契约
```ts
type ResourceMetric<T> =
| { status: 'available'; value: T }
| { status: 'warming-up' }
| { status: 'unavailable'; reason: ResourceMetricUnavailableReason }
type LocalResourceSnapshot = {
sampledAt: string
host: {
cpuPercent: ResourceMetric<number>
memory: ResourceMetric<{
usedBytes: number
totalBytes: number
}>
diskIo: ResourceMetric<{
readBytesPerSecond: number
writeBytesPerSecond: number
}>
}
goodbuddy: {
cpuPercent: ResourceMetric<number>
memoryBytes: ResourceMetric<number>
processCount: ResourceMetric<number>
}
}
```
所有数字必须有限且非负;百分比限制为 `0` 至 `100`,字节数设置跨 IPC 上限。`sampledAt` 由
Main 生成。不可用原因使用稳定枚举,例如 `unsupported-platform`、`permission-denied`、
`collector-timeout` 和 `collector-failed`,不向 Renderer 返回原始命令输出。`warming-up` 只用于
依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。
### 9.3 Preload 与 IPC
Preload 只暴露:
```ts
resourceMonitor.getLocalSnapshot(input: {
resetIntervalBaseline: boolean
}): Promise<LocalResourceSnapshot>
```
对应 IPC 为 `resource-monitor:local-snapshot:get`。Main 验证可信 sender;单次采集设置超时,
并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回
有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集
任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。
合并规则区分普通请求与基线重置请求:
- 普通请求在途时,新的普通请求复用它。
- 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能
用普通请求结果满足重置请求。
- 多个重置请求复用同一个在途或待执行重置。
- 重置请求在途时,普通请求复用其 `warming-up` 结果,下一次时钟再取得区间值。
因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列
长度。
面板每次从非活动状态恢复后的首次请求传入 `resetIntervalBaseline: true`。Main 消费本次
`systeminformation` 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 `warming-up`;
内存等瞬时指标正常返回。后续请求传入 `false` 并计算当前区间值,避免把面板停用期间计入
速率。
### 9.4 轮询与历史
`ResourceMonitorPanel` 仅在工作栏展开、`resources` 为活动实例且
`document.visibilityState === 'visible'` 时启动轮询:
1. 进入活动状态后立即请求一次,并设置 `resetIntervalBaseline: true`。
2. 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。
3. Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。
4. Renderer 环形队列最多保留 150 个带真实时间戳的样本。
5. 采集失败不追加伪造样本,也不持久化历史。
资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 `resources` 实例
及其活动状态。
## 10. 测试
### 10.1 Renderer 测试
- 固定入口不受应用启用、常驻和顺序组合影响,应用中心及设置始终可达;左侧没有重复系统工具。
- US-B6:底部菜单向上锚定且无遮罩,只列已启用应用,不受常驻或打开历史影响;点击行关闭菜单并导航,“管理应用”进入既有可搜索 Modal,包含关闭及未常驻应用。
- US-F3/US-F5:三个入口均打开本机推理监控 Modal 并保留底层工作区;覆盖嵌套确认的焦点循环、逐层 Escape、提交期间禁止关闭与重复执行、关闭后的入口及后备焦点恢复,以及停用内容后的键盘焦点。
- US-B7:常驻、取消常驻、键盘及拖动排序成功后同步,重新开启及重启保留原偏好和顺序。
- US-B8:知识库始终启用且常驻,对应行提供打开与排序,不提供启用或常驻开关;心跳应用启停按监督者逻辑验收,单条计划 enabled 不变;可选应用关闭后的不可用页保留草稿,重新开启不自动导航或重放。
- US-B9:应用中心对应行进入笔记表单,管理启用、常驻、待办数量、评论方式及形式;笔记页和全局设置无重复入口。
- US-B5:所有入口共享 pending,保存失败保留相同确认值;重开刷新期间锁定修改,较新事件使旧读取失效;结果未知时重读,不自动重发。
- 文件复用不可关闭单实例;选择任一已有终端或浏览器 Tab 保留确切目标,“+”每次创建额外实例。
- 已退出、失败的终端 Tab 不因选中而重建;工具切换不改变主页面,后台状态不抢占 Tab。
- 窄窗口、键盘排序、共享 Switch、Tab 语义及浮层焦点恢复正确;中心和设置不被原生浏览器遮挡。
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
### 10.2 Main 与契约测试
- 顺序包含四个应用 ID 各一次;公开写入拒绝重复、缺失、未知 ID 及无效常驻字段;旧存储顺序按迁移规则补齐,不为未发布的 `knowledgeEnabled` 添加迁移。
- 未配置的心跳应用默认关闭,其余应用启用及笔记布尔项默认开启,本机推理监控默认不常驻;已保存的启用、常驻值和评论单选值经迁移及重启保留。
- 更新常驻或顺序不修改启用、计划或业务数据;笔记既有配置键保持语义。
- 串行 patch 在 Main 最新快照上合并,原子写入失败不发布目标状态;响应丢失后重读实际结果。
- Store 持久化后才发布变更,工具配置写入使用同一通知链路;知识库不受可选应用偏好影响。
- 保存启用失败不会改变业务判断;会话关联和历史知识引用不因入口隐藏被清空。
- 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。
- 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。
- 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。
- 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。
- `resetIntervalBaseline: true` 时把 CPU 与磁盘 IO 标记为 `warming-up`。
- GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。
- GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在 `0` 至 `100`。
- GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。
### 10.3 回归范围
- 工作栏布局恢复和已关闭实例重建。
- 本地与托管 SSH 终端目标。
- 浏览器无会话、创建中和 Agent 操作中状态。
- 设置页未保存离开确认。
- 魔法笔记待办数和智能心跳建议数徽标。
- 浅色、深色、`220px` 与 `420px` 侧栏宽度。
- Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。
FR-16 单独覆盖 US-F3 概览去重、未知指标、US-F4 单任务取消与完成竞态、US-F5 服务
实际启动/停止/重启或加载/释放、使用方确认、外部管理限制及失败真实状态;用接入的真实引擎
验证共享服务不被单任务取消终止,重启不重放。设计文档检查或模拟服务不算生产路径验证。
## 11. 实施顺序
实施范围以 [PRD 分期](./prd.md#8-分期)及最新评审决定为准,逻辑映射见
[需求对应](./logic-design.md#6-与需求的对应关系)。既有 FR/US 编号保持稳定;新增故事使用
US-B6 应用中心、US-B7 顺序、US-B8 启用联动、US-B9 笔记迁移,以及 US-F3 概览、
US-F4 取消、US-F5 服务生命周期。PRD、User Stories、配套设计与根 UI 规范已同步入口及持久化规则。
以下为实施阶段划分,应用中心及本机推理监控已接入源码,验证状态以进度记录为准。
1. 底部入口使用向上锚定的轻量菜单,通过“管理应用”进入既有整窗 Modal;核对已发布配置的迁移来源。
2. 接入现有内置应用页面与最底部应用中心入口,保留独立设置和固定运行记录入口。
3. 实现常驻、完整顺序及启用契约,迁移时保留明确旧值,通过串行保存与确认快照同步所有界面。
4. 迁移魔法笔记共同设置,保留知识库始终可用及原工作区配置、心跳单条计划 enabled,验证可选应用关闭和重新开启。
5. 验证右侧现有文件、终端、浏览器 Tab 的确切目标与“+”额外实例行为,不实现左侧重复工具条。
6. 单独实现资源快照契约、Main 采集服务、Preload API 与右侧 `resources` 单实例面板。
7. 验证键盘、焦点、浅深主题、窄窗口、设置失败、重启持久化和跨平台行为。
首期使用内置应用清单,不依赖远程商店、插件注册或安装服务。商店目录、推荐、安装与
更新机制在后续独立设计中确定;Demo 中的模拟商店不接入首期产品界面。
本轮 `local-inference` 已接入管理 Modal、类型化 IPC 与真实引擎管理,支持范围见 11.1;
实机和自动化验证范围见[实施进度](./progress.md)。
后续设计输入见 [FR-15](./prd.md#fr-15-私有化市场与-yaml-交换后续) 与
[FR-16](./prd.md#fr-16-本机模型管理入口新增计划项):
- 私有市场的 YAML Schema 应由桌面本地导入与 ShareServer 发布共用,具体协议与字段在
市场功能设计中确定。市场 URL 按设备保存,认证信息沿用受保护凭据存储;导出仅包含
可迁移应用定义与非敏感配置。
- 程序包与模型资源单独分发。受管下载声明不可变版本、预期字节数和 SHA-256,下载开始后
固定来源,失败不静默切源;下载地址与重定向范围由 Main 验证,不接受 Renderer 任意覆盖。
市场来源配置不覆盖现有模型、应用更新或 Runtime 下载源设置。
- ShareServer 的市场发布与分发实现由其独立仓库承接,本仓库先记录依赖,不将桌面需求
记录标为服务端已实现。
### 11.1 本机推理任务与执行服务
ASR、OCR 与内置向量的跨功能执行边界、任务控制缺口及有条件的优化方向,见[本机专用推理架构与任务控制评估](../../architecture/local-inference-task-control.md)。该评估区分源码现状、建议与待办,未将常驻化或后台宿主迁移列为已采用实现。
`LocalInferencePage` 默认导出,接收必需的 `onClose` 及可选的 `enabled`、`restoreFocus`,通过 `createPortal(..., document.body)` 渲染管理 Modal,复用 `PageHeader` 和共享
`activateModalFocus`/`trapTabFocus`。`ImpactConfirmation` 使用独立 Portal 和紧凑确认样式;共享焦点工具隔离下层 Modal,并在关闭时恢复可用触发元素或指定后备焦点。`operation` ref 防止重复执行,`busy` 和 `control` 控制关闭与 Escape,交互细节见 [UI 设计](./ui-design.md#91-本机推理管理)。`local-inference-contracts.ts` 定义快照、操作与取消输入;Main 注册四个
`local-inference:*` 通道,复用可信 sender 校验和 Zod 输入校验。Preload 只暴露读取、服务操作、
任务取消及打开既有设置的方法,不接收 PID、命令或模型路径。
| 引擎 | 生产接入与可用操作 | 明确限制 |
| --- | --- | --- |
| ASR / sherpa-onnx | `SpeechTranscriptionService` 登记真实请求;执行路径保留独立 Worker 请求取消 | 每次请求加载、结束释放;未提供 Worker 就绪观测,模型可用时状态为 unknown,不从活动请求推断 running |
| OCR / PaddleOCR | `DocumentOcrBroker` 登记文档请求;`document-ocr-bridge` 提供实际 Worker 状态、加载及空闲释放 | 有活动或排队请求时拒绝释放;不把终止共享 Worker 暴露为单任务取消 |
| 内置向量 / Granite | 按模型目录复用引用计数 Broker;启动、停止、重启;取消消息等待 Worker 结果或取消确认 | 启动确认进程 spawn,模型在首个请求时加载;不把进程启动称为模型已加载 |
| TTS | 显示当前没有执行服务 | 不提供虚假任务或控制 |
| 外部向量连接 | 快照契约保留配置,Modal 不显示;通过既有模型设置管理 | 本页不主动探测,不推断本机归属,不控制外部进程 |
`LocalInferenceService` 在 Main 内存中登记真实 ASR、OCR、向量请求,保留活动请求和最近
100 条终态记录。只保存能力来源、时间、模型(执行路径有提供时)、状态和错误,不保存音频、
图片或向量输入文本。运行阶段包括排队、模型加载及执行,没有统一阶段进度;来源未提供项目或
会话对象时不补造对象关联。Modal 仅使用活动请求生成影响确认,不展示任务历史或单任务取消。
向量执行端通过 `utilityProcess.fork` 创建独立进程。每个 transport 持有自己的资源采样器,按当前 `child.pid` 从 `app.getAppMetrics()` 匹配 Utility 条目。服务快照的 `resources.scope` 为 `service-process`,携带 PID、采样时间及可用指标;无进程或无法采集时为 `unavailable` 并给出原因。Broker 清除退出的 transport,重启使用新采样器,不复用旧进程指标。
Modal 可见且启用时每 5 秒读取一次快照,跳过重叠请求。CPU 使用累计 CPU 秒数之差除以单调时钟间隔,100% 表示一个逻辑核心,可超过 100%;首次读取、PID 或创建时间变化、采样间隔超过 15 秒、计数回退及采样失败后重新建立基线。15 秒阈值为定时器延迟和跳过重叠请求留出余量。工作集由 Electron 的 KiB 转为契约中的字节,界面显示 MiB。缺少 CPU 时间或内存读数时只标记该指标不可用,不补零。数值为整个向量服务进程的占用,包含运行时和并发使用方,不是单任务或模型权重大小。
ASR 使用主进程中的 `node:worker_threads`;OCR 使用渲染进程中的 Web Worker,两者均没有独立进程工作集,逐行标明无法独立统计 CPU/内存。没有将主进程或整个 Renderer 的读数归属给服务。显存未采集,本机资源监控工作栏应用仍未实现。
服务操作按 serviceId 串行,操作期间拒绝新增该服务请求;停止前校验所有当前活动 taskId
都在用户确认清单内,新增使用方要求刷新后重新确认。向量停止等待实际 child exit;失败保留
可观测状态和错误。Renderer 操作失败时关闭旧确认并重新读取快照,刷新期间或失败时禁用服务操作;再次操作须重新确认当前活动请求。重启不重放中断请求。任务取消与服务停止分开,完成竞态保留执行方结果。
向量 Broker 将调用方 Promise 与 Worker 执行记录分开。调用方取消或超时可先返回错误,
执行记录在 Worker 确认、返回结果或进程退出前仍为活动项,保留在途名额并计入服务停止影响;
停止等待超时也不能提前移除记录。请求取消与自然完成竞争时采用执行方结果。
OCR 模型可用性由 Main 校验,Worker 运行状态由实际持有它的 Renderer 模块提供。手动加载
同样复用既有 Worker,加载超时恢复为可重试错误;空闲 60 秒自动释放。Modal 启用、文档可见且内容未被 hidden/inert 隔离时每 5 秒刷新,跳过未结束的请求;嵌套确认期间暂停新快照读取。
关闭 Modal 时卸载组件并清理轮询,窗口隐藏、关闭管理应用或取消常驻不会触发服务停止。退出整个进程继续
沿用原有应用清理路径。模型文件卸载仍在模型设置中独立进行。
向量 Main 引擎使用 `onnxruntime-web` 的 Node 条件入口,避免浏览器 `/wasm` 入口通过 fetch
加载本地文件失败。执行方的具体错误随任务返回;Renderer 不把失败展示为零指标或恢复成功。
## 12. 项目活动汇总
对应 [FR-14](./prd.md#fr-14-项目活动汇总),按[活动逻辑](./logic-design.md#9-项目活动汇总)
派生状态,界面行为见[活动界面](./ui-design.md#10-项目活动汇总)。
- `conversation-activity.ts` 从 `App.tsx` 的会话、`activeConversationIds`、全部可见
`assistantTasks`、`completedConversationIds` 和项目元数据派生会话行、全局及项目计数,不新增持久化活动副本。
`ProjectActivity` 在有活动时渲染汇总与项目、会话级联菜单,`ProjectSwitcher` 复用项目计数。
- `use-unviewed-completions.ts` 在 Renderer 内存维护完成集合和上一轮 Task 状态;已观察到的
顶层 Task 转为 `completed` 时加入集合,初始历史不生成通知。`App` 的本地 `done` 事件和
活动运行持久化快照的成功终态调用 `markConversationCompleted`,失败、取消不调用。
新运行及 Task 恢复运行或等待处理时清除旧通知。具体优先级和通知生命周期以活动逻辑为准。
- `App` 仅在 `view === 'chat' && !settingsOpen && !applicationCenterOpen` 时向 Hook 传入当前会话 ID;Hook 监听
`visibilitychange`,文档不为 `hidden` 时才按当前会话清除通知。这使普通侧栏、活动菜单
导航和可见性恢复共用查看规则,KeepAlive 挂载本身不代表查看。集合不写入 SQLite 或设置。
- 聚合状态新增 `completed`,全局和项目计数包含 `attention`、`running`、`completed`,
同一 Conversation ID 只按最高优先级计数;活动行为空才隐藏摘要。
- `App` 向 `ProjectActivity` 传入项目元数据、活动行、侧栏可见状态与既有精确跳转回调。
菜单按项目 ID 分组并记忆化派生集合;排序仅影响展示,不改聚合状态或持久化数据。
- 两层菜单在同一 body Portal 内相接,桌面会话层使用独立 fixed 定位,不参与项目层高度计算。
项目层锚定摘要,会话层锚定选中项目行并独立限制窗口边界。布局在打开、层级变化、实时数据更新、窗口缩放和
滚动时按锚点重算;窄窗口隐藏项目面板并保留返回操作。菜单不调用 Modal 焦点隔离,
捕获阶段消费 Escape,防止触发窄侧栏的文档级监听;Tab 关闭后交还正常焦点顺序。
菜单关闭与焦点恢复在导航回调之前同步完成。
- `role="menu"` 使浮层直接进入共享 `browser-viewport-occlusion.ts` 相交检测。未增加
浏览器实例的特殊隐藏条件,也不关闭或重建浏览器会话。
- 持久化快照刷新时,同一条仍在流式输出的消息保留本地待审批、待回答信息,持久化终态
则清除这些信息。响应成功后对应 Task 立即退出等待状态;响应期间到达的新问题、审批或
终态不能被旧响应覆盖。
- `AssistantDatabase.listTasks()` 保留最近历史窗口(默认 100 条),额外包含窗口外当前
运行或等待处理的可见 Task;`activeVisibleTaskSelect` 与 `toTask` 的有效状态保持一致,
待调度运行规范化为 `queued`,不因旧 Task 状态被误纳入实时集合。
- `listConversations()` 的范围与排序遵守
[会话列表读取与前端保留](../assistant-workbar/execution-history-storage.md#会话列表读取与前端保留),
包含上述 Task 关联及含 `streaming` 消息的较早活动会话,避免后台工作被最近记录挤出。
`listTasks()` 仍按原时间顺序返回;列表结果允许超过历史窗口大小。
- `openActivityConversation` 按确切 Conversation ID 导航;内存中缺失时刷新持久化会话
列表并合并,再查找同一 ID。刷新失败或会话已不存在时显示通知,不回退到项目最新会话。
设置离开检查通过后才提交项目、会话及界面清理状态。
- 远程恢复成功提示沿用独立的自动消失计时器;活动汇总更新不重置或替代该计时器。
完成通知的定向验证应覆盖本地成功、持久化活动运行成功、后台 Task 状态转换、初始历史、
失败和取消、实时状态优先、三类计数、普通侧栏查看、设置覆盖、隐藏文档与恢复可见、无活动行隐藏。
验证记录见[项目活动级联实施进度](./progress.md);源码核对不能替代真实 Electron 路径验证。
@@ -0,0 +1,308 @@
# 应用导航与系统工具入口 UI 设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 应用中心与本机推理监控已接入源码,整体验收待完成 |
| 版本 | 0.4 |
| 日期 | 2026-09-17 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
本文定义已接入的应用中心、共同设置和本机推理监控界面,以及待实现的右侧资源监控界面。
行为以[功能逻辑](./logic-design.md)为准,需求保留 FR-1 至 FR-16 与既有 US 编号。
根 UI 规范已同步应用中心与工具入口规则;共享控件、主题、设置 Modal 与工作栏布局遵守根规范。
## 1. 侧栏信息架构
```text
品牌
项目选择
项目活动汇总(仅有活动时显示)
新对话
搜索
当前对话
应用(按保存顺序;知识库始终显示,可选应用仅显示启用且常驻项)
运行记录
最近对话
...
────────────────────────
[应用中心] [设置]
```
“当前对话”和“运行记录”属于固定系统入口,常驻应用位于二者之间。首期应用清单为魔法笔记、
知识库、智能心跳及本轮追加的本机推理监控,应用中心与设置始终可见。侧栏底部不再显示“本地工作区”和 `platform · arch`;
排障信息移至关于或诊断页面。左侧不再提供文件、终端、浏览器、资源监控的重复入口。
## 2. 底部应用中心入口
底部使用连续平整表面和顶部分隔线,应用中心在前,固定设置靠右,二者为独立按钮:
| 位置 | 图标 | 可见工具提示 | 点击结果 |
| --- | --- | --- | --- |
| 1 | `Grid2X2` | 应用中心 | 向上展开已启用应用菜单,底部“管理应用”进入管理 Modal |
| 末位 | `Settings` | 设置 | 在当前工作区上打开设置 Modal |
图标使用项目现有 Lucide 库。应用中心保留文字,设置图标采用统一的 `34px × 34px` 点击区域。
底部使用共享间距和焦点样式,设置通过弹性空白靠右;不得把整个容器做成设置按钮。
### 2.1 状态
- 默认:透明背景,图标使用次级文字色。
- 悬停:使用弱表面色,不能强于当前选中状态。
- 展开:应用中心按钮显示展开反馈,不能当作当前应用的选中状态。
- 键盘焦点:显示统一焦点环,不能被侧栏边缘裁切。
- 加载:读取应用配置时保留入口,中心内显示加载状态;失败提供重试,不用默认值冒充已保存配置。
应用中心入口不承载工具运行徽标。全部可选应用关闭时菜单仍显示知识库及“管理应用”,可进入 Modal 重新开启可选应用。
## 3. 主导航应用区
应用入口沿用现有 `nav-item` 行,不使用应用卡片。图标、名称和徽标保持单行:
```text
[图标] 知识库
[图标] 智能心跳 1
[图标] 魔法笔记 3
[图标] 本机推理监控
```
取消常驻或关闭可选应用后不留下空白。知识库始终启用并显示;心跳按启用和常驻显示,位置遵守共享应用顺序。运行记录始终位于应用区之后。
四个应用均在管理行上通过拖动及“上移”“下移”按钮排序;首尾无目标时禁用对应按钮。两种方式提交完整应用顺序,保存成功后更新,重启保留。当前对话、运行记录和底部系统入口不参与排序。
取消常驻不关闭当前页面或本机推理监控 Modal。重新常驻按已保存顺序恢复,关闭应用保留常驻与顺序偏好。
主侧栏始终读取最后确认配置,不展示尚未保存成功的排序或开关目标值。
## 4. 应用中心与共同设置
底部点击打开轻量菜单,锚定按钮向上展开,无模态遮罩、背景隔离或焦点循环。菜单按共享保存顺序列出已启用应用,不按打开历史或常驻状态过滤。点击应用行关闭菜单,本机推理监控打开独立 Modal,知识库、智能心跳和魔法笔记在主内容区打开页面。分隔线下的“管理应用”关闭菜单并打开既有管理 Modal。
菜单通过共享 `AnchoredMenu` 挂载到 body,宽度最多 `280px`,距窗口边缘至少 `16px`,与按钮上缘间隔 `8px`;短窗口限制高度并内部滚动,滚动、窗口和内容尺寸变化时重算位置。原生浏览器相交遮挡复用共享机制。
管理 Modal 沿用设置 Modal 的表面、遮罩和焦点规则,提供全部内置应用的名称及用途搜索、打开,以及可选应用的常驻和设置:
应用管理始终采用紧凑单列横向列表,从上到下对应保存顺序;行间使用 `--border-subtle` 弱分隔线,不加卡片外框。概览与设置详情共用 `640px` 最大宽度,窄窗口保留两侧各 `16px` 外边距,高度随内容增长并受窗口限制。
每行左侧显示图标、名称、状态和简短用途,右侧对齐打开、可用的设置及上下移动操作;空间不足时操作组换到下一行并保持右对齐,组内也可换行,上下箭头保持成组。名称旁区分
“始终显示”(英文 `Always shown`)和“可选 · 已启用/已关闭”。名称与用途可自然换行,不截断用途说明;正文与控件均不可横向溢出。
```text
应用中心
[搜索内置应用]
知识库 始终显示 [打开] [上移] [下移]
智能心跳 可选 · 已关闭 [打开(禁用)] [设置] [上移] [下移]
魔法笔记 可选 · 已启用 [打开] [设置] [上移] [下移]
本机推理监控 可选 · 已启用 [打开] [设置] [上移] [下移]
```
已启用但未常驻的应用可以直接打开,打开不自动常驻。关闭的应用仍能被搜索和配置,
保留禁用的“打开”按钮,可通过“设置”重新开启。启用、常驻放在设置详情中;每行右侧提供成组的上下箭头,与打开、设置操作按空间换行。箭头使用共享图标按钮,名称和提示包含方向及应用名;目标取完整保存顺序中的相邻应用,仅首尾无目标或修改锁定时禁用对应箭头。无搜索词且未锁定修改时,所有行均支持拖动排序,包括关闭或未常驻的应用;搜索时禁用拖动,箭头仍按完整保存顺序操作,未命中的应用保留在顺序中。无搜索结果显示清除搜索入口。
首期不展示商店页签、推荐、安装按钮、市场 URL 或 YAML 操作,系统工具也不在此列出。
### 4.1 共同应用设置页
智能心跳、魔法笔记、本机推理监控的通用应用设置统一从应用中心对应行的“设置”进入;应用页面不再提供重复快捷入口。本机推理监控保留“模型与连接设置”。知识库在管理 Modal 中提供“打开”和排序,不提供启用、常驻开关或通用空设置;原工作区实际配置入口保留。
共同页遵守[设置中心 UI 规范](../../../UI-DESIGN.md#138-设置中心),不为每个入口建立一套设置。
全局设置移除魔法笔记页签及跳转入口。旧“显示魔法笔记入口”绑定业务启用值,迁为“启用应用”并保留原值;
“常驻左侧菜单”使用独立配置,只有已发布的独立显隐偏好才映射到常驻。
| 控件 | 表达含义 |
| --- | --- |
| 启用应用 | 控制业务可用性及主导航、会话等业务入口 |
| 常驻左侧菜单 | 仅控制主导航捷径,关闭应用时保留该偏好,并说明开启后才显示 |
| 显示未完成待办数量 | 仅控制魔法笔记导航徽标 |
| AI 评论方式 | 即时/保存后自动/保存后手动,单选 |
| AI 评论形式 | 长评 + 要点/长评/要点,单选 |
二元配置使用共享 Switch 和 `role="switch"`;评论选项使用共享单选控件。本机推理监控
默认启用但不常驻;心跳应用默认关闭,常驻偏好默认开启,其余未配置布尔项默认开启。评论默认值沿用既有值,已有保存的选择保留。启用与常驻不可合并。
设置详情最大宽度 `640px`,紧凑标题栏包含返回图标、应用名称、用途及关闭按钮。进入详情和
返回列表时正文滚动位置重置到顶部;进入详情聚焦关闭按钮,返回后聚焦搜索框。
共同设置表单使用共享 Switch,相关字段分组,组间使用弱分隔线;说明沿用正文字号。
评论分段控件按内容占宽,窄窗口中标签与选项上下排列;详情不提供排序控件。
智能心跳单条计划 CRUD 及 enabled 仍由其业务页面负责,应用级开关及执行条件见[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)。
所有入口共享保存中状态。串行保存期间禁用相关启用、常驻、排序及笔记配置控件,保留
最后确认值;持久化成功后的设置事件同步中心、侧栏、会话、应用页及其他设置入口,包括工具配置写入。重开中心刷新期间锁定修改,迟到的旧读取不能覆盖更新快照。失败在发起操作
附近显示可重试错误并保留焦点;结果未知时显示“正在确认保存结果”,重读成功后再开放修改。
### 4.2 关闭后的页面与会话
关闭当前可选应用成功后,原页面或本机推理监控 Modal 显示“应用已关闭”,重新启用通过应用中心对应行的设置完成;本机推理监控需先关闭 Modal。保留草稿和内容状态,
暂停该页面的新业务操作。重新开启后恢复可用性,不自动跳到已离开的应用页或重放任务。
知识库不显示应用关闭状态;会话知识控件、历史引用和工作区配置遵守原功能规则。心跳关闭时的侧栏行为见[监督者 UI 设计](../conversation-supervision/ui-design.md#会话侧栏)。可选应用的已有请求和后台任务继续按功能生命周期展示,详细边界见[关闭应用与恢复](./logic-design.md#32-关闭应用与恢复)。
## 5. 助手工作栏反馈
系统工具仅从右侧工作栏已有 Tab 或“+”目录进入,工作栏按现有布局回流,主内容页面保持不变。
- 文件:激活默认存在且不可关闭的“工作区”Tab,焦点进入目录区域或错误修复入口。
- 终端:选择已有 Tab 激活该确切终端并保留目标与历史状态;“+”每次创建额外终端,按当前
项目选择本机或托管 SSH,无项目时使用本机 Home。新建期间显示进度,失败保留诊断。
- 浏览器:选择已有 Tab 保留其 Conversation、页面及导航状态;“+”每次创建额外浏览器,
无活动 Conversation 时新建待绑定空实例。不会按当前会话改选其他 Tab 或重绑已有实例。
- 资源监控:激活“资源监控”Tab,焦点进入指标摘要;首个样本加载后开始绘制历史。
文件和资源监控在“+”中按单实例规则聚焦或创建,终端和浏览器不按目标去重。
后台事件可以更新 Tab 徽标,不能自动展开工作栏或切换当前 Tab。
### 5.1 资源监控面板
资源监控使用紧凑单列,不使用指标卡片墙:
```text
资源监控 本机
CPU 18%
[最近 5 分钟折线图]
内存 9.6 / 32 GB
[使用量进度条]
磁盘 IO 读 12 MB/s · 写 3 MB/s
GoodBuddy
CPU 4% · 内存 620 MB · 桌面进程 7
更新于 14:32:08
```
- CPU 折线纵轴固定为 `0%` 至 `100%`,不能根据当前低负载动态放大波动。
- 内存同时显示使用量、总量和百分比;进度条不代替文字数值。
- 磁盘 IO 使用每秒读写字节并按统一单位格式化,不显示累计设备字节数。
- GoodBuddy 摘要只显示聚合值,不列 PID、命令行或环境变量。
- 首次加载或停止后恢复时,CPU 和磁盘 IO 显示“正在建立采样基线”,不先渲染 `0%`;内存
等瞬时指标可以立即显示。
- 部分可用时只在对应指标行显示“不可用”和简短原因;全部失败时显示重试入口。
- 图表不使用动效平移;`prefers-reduced-motion` 下行为不变。
- 面板失去活动状态、工作栏收起或窗口隐藏、最小化时停止更新;返回后重新建立区间指标基线,
不填补离开期间的历史。
## 6. 文案
固定使用以下名称:
| 对象 | 文案 |
| --- | --- |
| 底部入口 | 应用中心 |
| 菜单底部管理入口 | 管理应用 |
| 应用设置开关 | 启用应用 |
| 文件系统工具 | 文件 |
| 终端系统工具 | 终端 |
| 浏览器系统工具 | 浏览器 |
| 浏览器说明 | GoodBuddy 受控浏览器 |
| 资源系统工具 | 资源监控 |
| 资源范围 | 本机 |
| 指标缺失 | 不可用 |
| 应用常驻开关 | 常驻左侧菜单 |
| 关闭的应用页 | 应用已关闭 |
| 内置来源 | 内置 |
不再使用“本地工作区”作为侧栏底部身份名称。项目和默认工作区的路径继续由项目选择器、页面
范围或工作区面板显示。
## 7. 键盘和无障碍
- 底部应用中心和设置均为独立 `button`,Tab 顺序与视觉顺序一致,不嵌套按钮。
- 图标按钮通过 `aria-label` 提供名称,工具提示不能作为唯一可访问名称。
- 工作栏打开后按现有焦点策略聚焦目标面板;用户通过固定工作栏开关关闭时,焦点返回该开关。
- 应用中心按钮提供 `aria-haspopup="menu"`、`aria-expanded` 和控制关系;菜单使用 `menu`/`menuitem`,打开后聚焦首个可用项,上下方向键循环选择,Home、End 到首尾,Enter、Space 激活。
- Escape 关闭菜单并恢复入口焦点;Tab 关闭后继续正常焦点顺序,点击外部、焦点离开或再次点击入口也关闭。菜单不隔离背景,不增加遮罩。
- “管理应用”打开的 Modal 使用共享 Portal、整窗遮罩、原生浏览器遮挡、背景隔离和焦点循环,初始焦点进入搜索。行操作与详情控件按正常 Tab 顺序访问,保留共享可见焦点环,不伪装成单个菜单项;详情切换焦点见共同应用设置页。
- 工作栏使用 `tablist`、`tab`、`tabpanel`、`aria-selected`、游标焦点和方向键、Home、End。
“+”使用普通按钮语义,同类型 Tab 仍按实例独立标记。
- 徽标数量或状态合并进可访问名称;Switch 支持 Space,失败保留焦点。排序按钮名称包含
应用名,操作结果通过 polite 状态区播报;拖动不是唯一排序方式。
## 8. 响应式
- 侧栏宽度 `220px` 至 `420px` 时,应用中心与设置均可操作,不随宽度缩小点击区域。
- 窄窗口继续使用现有侧栏抽屉,底部入口不重叠;应用菜单向上展开并限制窗口边界、内部滚动。管理 Modal 的长名称可换行,搜索、设置与重新开启入口始终可达。
- 右侧 Tab 单行横向滚动,“+”持续可见;工作栏保持停靠回流,不遮罩主页面。
- 工具提示不得超出窗口;靠近底边时显示在按钮上方。
- 粗指针设备不依赖悬停,按钮的可访问名称和选中反馈保持有效。
### 会话置顶与操作菜单
会话行的“更多会话操作”提供“置顶会话/取消置顶”,本地与消息通道会话均可使用。
置顶会话显示带“已置顶”可访问名称的图标;当前项目和搜索过滤仍生效,过滤后的列表先显示
置顶会话,两组内各按更新时间倒序。取消置顶恢复时间排序,不改变会话更新时间。
置顶状态重启后保留;保存成功后更新图标和顺序,失败保留原状态并显示应用错误通知。
存储尚未就绪或置顶请求进行中时禁用置顶操作。
操作菜单通过 body Portal 浮在会话列表上方,不撑开会话行。菜单锚定更多按钮,默认位于
按钮下方,随滚动、窗口及菜单尺寸变化重新定位,距窗口边缘至少 `8px`;空间不足时限制
宽高并允许菜单内部滚动。使用共享表面色、边框、圆角与浮层阴影。
入口提供 `aria-haspopup="menu"`、展开状态和控制关系;菜单使用 `menu`/`menuitem`。
打开后聚焦首个可用按钮,上下方向键循环移动,Home/End 到首尾;Escape 关闭并返回
触发按钮,点击外部或焦点离开时关闭。本地会话继续提供分支、重命名和删除确认,复制与
导出也适用于消息通道会话。持久化与刷新规则见
[会话置顶存储](../assistant-workbar/execution-history-storage.md#会话置顶)。
## 9. 未来插件入口
FR-15、US-F1/US-F2 的市场 URL、YAML 导入导出及 ShareServer 分发在后续设计中定义。
首期中心只浏览本地内置清单,不显示未来市场的空壳界面。
### 9.1 本机推理管理
FR-16 的 `local-inference` 入口已纳入本轮应用清单,可常驻主导航。从常驻入口、应用菜单和管理 Modal 的“打开”均进入同一本机推理监控 Modal,关闭来源菜单或管理 Modal,保留底层工作区、项目、会话和草稿。其他内置应用仍打开主内容区页面。
Modal 与真实执行模块已接入;支持清单及验证范围分别见技术设计和实施进度。
复用 `custom-task-dialog` 的整窗 body Portal、遮罩、背景隔离及浏览器遮挡机制。主 Modal 最大宽度 `960px`,高度随内容增长,最大高度为 `min(720px, 100dvh - 2 × --space-6)`;紧凑标题栏和关闭按钮固定在顶部,正文独立滚动。直接显示服务列表,没有页签或任务历史。工具栏显示“每 5 秒刷新 · CPU 100% = 一个逻辑核心”、模型与连接设置、刷新状态,窄屏可换行。服务行显示“服务进程 PID … · CPU …% · 工作集 … MiB”,预热时显示“正在建立采样基线”;共享进程的 worker 在该行显示无法独立统计的原因。
初始焦点进入“关闭本机推理监控”,Tab/Shift+Tab 在当前 Modal 内循环。没有影响确认且没有操作提交时,关闭按钮或 Escape 关闭 Modal;点击遮罩不关闭。关闭后优先恢复仍可用的原触发元素,入口已移除时回到应用中心,窄侧栏已隐藏时回到侧栏开关。
服务采用弱分隔列表;名称使用区块标题字号,状态及引擎元数据使用
辅助字号,状态与名称同组并与操作区分开,原因与操作说明使用正文字号。桌面操作独立位于右侧;
窗口宽度不超过 `720px` 时移到正文下方并左对齐,保留完整状态和原因。
- US-F3 每行显示服务名、简短状态、引擎、模型及错误。ASR 可用时显示“按需运行(状态未知)”;OCR 和向量使用执行模块的状态。TTS 显示不可用,外部连接不列入本机服务。
- US-F4 不再提供单任务取消入口;活动任务仅在服务操作的影响确认中出现。
- US-F5 服务行右侧保留实际支持的启动、停止、重启或模型加载、释放。
- 停止、重启、释放前打开最大宽度 `480px` 的紧凑嵌套确认,列出正在使用的任务与影响;无已登记任务时说明执行前再次核对。初始焦点进入“保留运行状态”,最终危险按钮写明“确认停止服务/重启服务/释放模型”;取消保留运行状态。
确认使用独立 body Portal,隔离底层管理 Modal,Tab/Shift+Tab 只在确认中循环;Escape 只关闭确认,关闭后恢复原服务操作按钮,按钮不可用时回到管理 Modal。确认存在时禁用主 Modal 关闭按钮;提交期间禁用确认和取消按钮,两层 Escape 均不能关闭,也不能重复执行。失败关闭旧确认,在服务列表显示错误并刷新;再次操作须重新确认当前影响。成功只关闭确认。
操作失败呈现实际状态和可重试错误,无法确认则显示未知。服务恢复不自动重放任务。
关闭 Modal、关闭管理应用或取消常驻不停止服务,模型文件卸载另行操作。
服务列表提供首次加载、无服务、加载失败重试及保留旧快照的刷新失败状态;刷新期间或失败时禁用服务操作。
OCR 忙碌或模型不可用时禁用加载与释放,并在执行模块再次检查。
文档解析与 HTTP OCR 的配置归属见[文档处理入口设计](../document-processing/ui-design.md#11-入口与职责),
不因接入 HTTP 而扩展本机服务列表。“模型与连接设置”现直达 `model`,OCR 服务行的
“文档解析设置”直达 `document-parsing`,关闭设置返回原工作区;实施与验证见
[文档处理进度](../document-processing/progress.md#2026-09-19-实现与验证)。
页面不显示无来源的进度条或资源数值。US-B6 至 US-B9 的应用中心、顺序、启用联动、笔记共同设置映射见
[需求对应](./logic-design.md#6-与需求的对应关系)。
## 10. 项目活动汇总
对应 [FR-14](./prd.md#fr-14-项目活动汇总),状态与计数遵守
[活动逻辑](./logic-design.md#9-项目活动汇总)。
- 全局汇总按钮仅在有活动行时显示在项目选择器下方,紧凑显示待处理、运行中与已完成数量,无活动行时整行隐藏,
右侧显示展开箭头,与下方新建会话保留 `8px` 间距。可访问名称明确数量属于“全项目活动”。
- 项目菜单在项目行显示同口径数量,保留“本地项目 / 远程项目 / 远程通道”分组和 SSH Host
层级,不因活动状态重排项目。数量为零时不显示标记,连接状态仍独立显示。
项目名独占一行,状态数量排列在名称下方,避免窄侧栏中数量挤占名称宽度。
- 点击汇总在按钮下方打开项目菜单,只列有活动会话的项目,每项显示项目名和同口径计数。
桌面鼠标悬停或点击项目展开右侧会话菜单,右侧空间不足且左侧可容纳时向左展开。两个面板
相接,各自拥有边框、圆角与阴影;会话层对齐所选项目行,底部空间不足时仅上移会话层,
不改变项目层的位置或高度。指针离开项目行进入会话层时不收起。展开项目不切换当前会话。
- 会话层顶部显示项目名,每行显示完整可换行的会话标题与明确状态文字,待处理会话排在
运行中会话之前,已完成未查看会话排在最后并显示“已完成”。项目行保留子菜单箭头,会话行不显示箭头。不展开 Task、Job 或 Run 列表。
- 点击会话先关闭菜单并恢复入口焦点,再请求进入该条会话;允许离开当前页面后,
选择所属项目、清除搜索和该会话未读标记、清除任务选择与会话操作菜单,窄窗口同时关闭侧栏。
设置页沿用未保存修改的离开检查,取消离开时保留原页面和完成通知。完成通知在目标聊天
实际可见后清除,从普通侧栏进入也相同;设置覆盖下的聊天或隐藏文档不触发清除。
- 菜单挂载到 body,使用 `menu`、`menuitem`、`aria-haspopup`、`aria-expanded` 与控制关系,
不隔离背景、不增加遮罩。原生浏览器相交遮挡沿用共享机制。
- 打开后焦点进入首个项目;上下方向键循环选择当前层项目或会话,Home、End 到首尾,
Enter、Space 激活,右方向键进入会话层。左方向键或 Escape 返回项目层并恢复对应项目
焦点;顶层 Escape 关闭并恢复汇总焦点,不继续关闭窄侧栏。Tab 关闭菜单后继续正常焦点
顺序;点击外部、焦点离开、再次点击入口或隐藏侧栏也关闭菜单。
- 单面板宽度为 `264px`,窗口小于 `620px` 或项目菜单两侧均无法容纳子菜单时,就地进入会话层并显示“返回项目列表”,
触控无需悬停。浮层距窗口边缘至少 `16px`,每层最大高度为 `480px`,短窗口进一步限制
高度并独立滚动。成功完成但尚未查看的会话继续保留;所有活动行移除后,关闭已打开菜单并隐藏摘要。
@@ -0,0 +1,503 @@
# 应用导航与系统工具入口 User Stories
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 应用中心与本机推理监控已接入源码,整体验收待完成 |
| 版本 | 0.4 |
| 日期 | 2026-09-17 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
## 1. 角色
### 1.1 普通用户
使用对话、笔记和知识功能,希望常用入口稳定,不需要理解工作栏实例模型。
### 1.2 本地工作用户
经常查看项目文件、打开终端、使用受控浏览器或检查电脑负载,希望从任意一级页面快速进入
这些工具。
## 2. Epic A:固定系统入口
### US-A1 始终访问运行记录
作为用户,我希望运行记录保持在固定位置,以便应用入口变化后仍能检查 Agent 和工具活动。
验收:
- Given 用户隐藏任意可选内置应用
- When 用户查看主导航
- Then “运行记录”仍显示在应用区之后
- And 设置中没有隐藏、卸载或固定运行记录的选项
### US-A2 从固定入口进入设置
作为用户,我希望设置入口始终位于侧栏底部,以便应用状态变化时仍能进入配置。
验收:
- Given 任意主页面和任意侧栏宽度
- When 用户查看侧栏底部
- Then 设置与应用中心为始终可见的独立按钮,点击区域不重叠
- And 点击后继续遵守未保存内容的离开确认
## 3. Epic B:管理应用入口
本节的启用、常驻和共同设置适用于智能心跳、魔法笔记、本机推理监控;四个应用均可排序,知识库始终启用并常驻。心跳的默认关闭及执行边界按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)验收,保留既有故事 ID。
### US-B1 识别完整应用
作为用户,我希望魔法笔记、知识库和智能心跳显示在主导航,以便知道它们会打开完整页面。
验收:
- Given 始终显示的应用,或已启用且常驻的可选应用
- When 用户打开主侧栏
- Then 应用位于“当前对话”和“运行记录”之间
- And 点击知识库、智能心跳或魔法笔记在主内容区打开对应一级页面;本机推理监控打开保留底层工作区的 Modal,验收见 US-F3
- And 原有待办或建议徽标按已保存的显示设置继续显示
- And 当前对话之后按保存顺序显示应用;知识库始终显示,可调整位置,无启用或常驻开关;心跳按启用及常驻过滤
### US-B2 取消低频应用常驻
作为用户,我希望隐藏不常用的内置应用,以便缩短主导航,但保留已有数据。
验收:
- Given 应用已启用且已有内容
- When 用户关闭该应用的“常驻左侧菜单”Switch 并保存成功
- Then 主导航不再显示该应用,当前已打开页面保持不变
- And 应用数据、计划和业务启停状态不变
- And 用户可从应用中心打开该应用,或从同一设置位置恢复常驻
### US-B4 保留功能启停语义
作为魔法笔记用户,我希望导航显隐与功能启停相互独立,以免整理导航时关闭功能。
验收:
- Given 魔法笔记功能已启用
- When 用户只取消其侧栏常驻并保存成功
- Then 笔记及待办处理能力继续按原设置工作
- And 重新常驻只恢复入口,不自动导航;再次打开时保留原页面状态和数据
### US-B5 保持设置保存一致
作为用户,我希望应用设置保存期间和失败后各入口保持一致,以免界面与实际配置不符。
验收:
- Given 配置已加载且没有待确认的更新
- When 用户修改启用、常驻、顺序或魔法笔记配置
- Then 更新串行保存,在请求确认前,应用中心、应用页及全局设置中修改这组配置的所有控件均暂不可操作
- And 各处保留最后确认值,不提前改变导航、业务可用性、徽标或会话入口
- When 保存成功
- Then 各入口同时采用确认配置,后续修改基于该配置提交
- And 工具配置写入持久化成功后也同步各入口;较新的确认快照不被迟到的读取或保存响应覆盖
- When 关闭后重新打开应用中心
- Then 刷新最新配置期间锁定修改,读取失败保留重试入口,不允许用旧快照提交
- When 保存明确失败
- Then 各入口仍使用相同的最后确认配置,操作处显示可重试错误,保留焦点和编辑上下文,不重复通知
- Given 响应中断,无法确认保存结果
- When 用户查看设置
- Then 显示正在确认保存结果,重读权威配置成功后才恢复修改;读取失败可重试读取,不自动重发变更或写回旧值
### US-B6 从应用中心访问内置应用
作为用户,我希望从固定应用中心找到应用,以便未常驻或已关闭的应用仍有访问和恢复入口。
验收:
- Given 用户位于任意一级页面,包括全部可选应用均已关闭的情况
- When 点击侧栏底部应用中心
- Then 向上打开轻量锚定菜单,无模态遮罩;按保存顺序列出知识库和已启用的可选应用,不按打开历史或常驻筛选
- When 点击菜单中的应用行
- Then 菜单关闭;知识库、智能心跳和魔法笔记经既有离开检查在主内容区打开,本机推理监控打开保留底层工作区的 Modal,不自动常驻
- When 点击菜单底部“管理应用”
- Then 菜单关闭并打开既有管理 Modal,可按名称或用途搜索全部内置应用,包含未常驻及已关闭的可选应用
- And 知识库行提供“打开”和排序,没有通用设置、启用或常驻开关;心跳行可管理启用和常驻,单条计划 enabled 不变
- And 首期没有商店、推荐、安装、市场 URL、YAML 操作或系统工具入口
- Given 应用已启用但未常驻
- When 用户选择打开
- Then 本机推理监控打开 Modal,底层内容无需离开;其余应用经既有未保存内容离开检查进入页面,不自动常驻
- Given 应用已关闭
- When 用户在管理 Modal 中找到该应用
- Then 显示关闭状态,保留禁用的“打开”按钮,可通过“设置”进入共同设置重新开启
- Given 搜索无匹配结果
- Then 显示无结果及清除搜索入口,搜索不修改启用和常驻
- Given 配置正在加载或读取失败
- Then 中心保留加载状态或重试入口,不用默认值冒充已保存配置
### US-B7 保存常驻及应用顺序
作为用户,我希望常驻和顺序按设备保存,以便重启后仍使用熟悉的导航。
验收:
- Given 首次使用且未配置导航偏好
- Then 默认顺序为知识库、智能心跳、魔法笔记、本机推理监控;心跳默认关闭且保留默认常驻偏好,魔法笔记默认常驻,本机推理监控默认启用但不常驻,仍可从应用菜单打开;侧栏只按启用与常驻过滤可选项
- When 用户修改常驻,或通过拖动、键盘可操作的上移和下移按钮排序并保存成功
- Then 管理列表从上到下、菜单和侧栏按同一保存顺序显示,知识库和智能心跳可移动;当前对话、运行记录及底部系统入口位置不变,重启后恢复保存结果
- And 每行均提供上下移动,以完整保存顺序中的相邻应用为目标;未常驻及关闭应用仍可排序,设置详情不提供排序控件
- And 搜索时箭头仍按完整顺序移动,保留未命中应用,拖动禁用;清除搜索后显示完整保存结果
- Given 旧设置仅保存本机推理监控、魔法笔记的顺序
- When 升级后读取设置
- Then 知识库、智能心跳补在前面,两个可选应用保留原有相对顺序和常驻值
- Given 应用已关闭或取消常驻
- When 用户重新开启或常驻
- Then 按保存顺序恢复入口;关闭或取消常驻不删除顺序,已关闭应用仍可管理常驻偏好
- Given 保存失败
- Then 按 US-B5 保留原常驻和顺序,在操作处提供重试并保留键盘焦点
- Given 没有可见的可选应用或仅有一个
- Then 固定入口和应用中心仍可用,没有移动目标的排序按钮禁用,不产生空白占位项
### US-B8 独立控制启用与常驻
作为用户,我希望关闭应用时保留数据和导航偏好,并能明确知道哪些业务操作已不可用。
验收:
- Given 应用已启用
- When 仅取消常驻并保存成功
- Then 业务能力和后台操作保持原状,仍可从应用中心打开
- Given 知识库已有会话关联,智能心跳已有启用或暂停的计划
- When 用户关闭全部可选应用或修改其常驻与顺序
- Then 知识库始终启用,侧栏位置稳定,页面可打开;知识控件及关联保持可用,请求遵守知识库自身规则,心跳单条计划 enabled 原值不变
- And 心跳应用关闭后显示不可用状态,可从应用中心设置重新开启;新运行、在途运行及侧栏查询按[应用启停与执行](../conversation-supervision/logic-design.md#应用启停与执行)验收
- Given 关闭的应用页面或本机推理监控 Modal 仍打开
- Then 原位置显示“应用已关闭”,保留草稿及页面状态,阻止新业务操作而不强制跳转;用户从应用中心对应行的设置重新启用,监控 Modal 需先关闭
- When 用户重新开启并保存成功
- Then 仍停留的页面恢复可用;已离开时不自动导航,不重放或补发旧请求和任务
- And 保留原常驻及顺序,未常驻应用不自动常驻
- Given 启用变更保存失败
- Then 按 US-B5 保留原业务状态和入口,不执行关闭或恢复副作用
### US-B9 迁移魔法笔记共同设置
作为魔法笔记用户,我希望在应用中心统一管理笔记设置,并在升级后保留已有选择。
验收:
- Given 用户从应用中心的魔法笔记行进入设置
- Then 显示笔记设置表单;笔记页面不提供重复入口,也不保留“显示魔法笔记入口”开关
- And 全局设置不再显示魔法笔记页签或跳转入口,已有配置值和业务数据保持不变
- And 启用、常驻、显示未完成待办数量分别控制业务、导航和徽标;隐藏数量不修改待办
- Given 升级前已有明确保存的布尔值和评论选择
- When 迁移配置并重启
- Then 保留已有 `true`、`false`、评论单选值及业务数据;缺失导航配置时,本机推理监控默认不常驻,魔法笔记默认常驻;未配置的应用启用和笔记相关布尔项默认 `true`
- And 旧入口开关若实际控制笔记启用,保留其启用含义和原值,不将其改作常驻
- Given AI 评论方式或评论形式未配置
- Then 分别默认“即时”和“长评 + 要点”,均为单选,不同时启用所有模式
- When 任一入口修改配置并保存成功
- Then 应用中心、主导航、笔记页、会话及其他设置入口同步采用确认值;保存中和失败均遵守 US-B5
## 4. Epic C:使用系统工具
### US-C1 一键查看文件
作为本地工作用户,我希望从右侧工作栏查看文件,并保留工作区面板的目标模式。
验收:
- Given 当前项目有有效根目录
- When 用户展开右侧工作栏并选择已有工作区 Tab,或从“+”选择文件
- Then 聚焦默认存在且不可关闭的工作区单实例,不创建重复实例;仅迁移或布局修复时补回缺失实例
- And 保留跟随或固定模式,跟随模式继续按工作栏规则更新上下文
- And 仅该实例 Tab 显示选中状态,焦点进入目录区域
### US-C2 处理无效工作区
作为用户,我希望目录不可用时看到修复方法,并收到明确反馈。
验收:
- Given 工作区面板处于跟随模式
- And 当前项目和默认工作区均不可用
- When 用户在右侧工作栏选择工作区 Tab 或从“+”选择文件
- Then 工作区面板仍打开
- And 面板说明目录不可用并提供选择或修复目录的入口
- And 应用不静默改用其他目录
- Given 工作区面板固定的目标已失效,即使当前项目或默认目录可用
- When 用户选择工作区 Tab
- Then 保留固定目标并显示该目标的修复状态,不回退到其他目录或切换为跟随模式
### US-C3 快速进入终端
作为本地工作用户,我希望选择确切终端继续查看,并能通过“+”创建额外终端。
验收:
- Given 已有终端 Tab,包括绑定其他执行空间的终端
- When 用户选择其中一个 Tab
- Then 仅激活该确切实例,保留执行空间、目录及历史,不按当前项目查找最近匹配实例或重新绑定
- Given 已选 Tab 正在关闭、已退出、已中断或失败
- Then 保留其真实状态,不因选中而重建
- Given 当前执行空间已有正在启动或运行中的终端
- When 用户从右侧工作栏“+”选择终端
- Then 每次创建绑定当前执行空间的额外实例,不复用或按目标去重
- Given 当前没有项目
- When 用户从“+”选择终端
- Then 创建使用本机 Home 的新终端,已有终端保持原目标和历史
### US-C4 使用远程项目终端
作为远程项目用户,我希望新建终端绑定远程项目目录,以免误在本机执行命令。
验收:
- Given 当前项目绑定托管 SSH 执行空间
- When 用户从右侧工作栏“+”选择终端
- Then 创建绑定该远程项目及其权威目录的额外终端
- And 面板持续显示远程主机和工作目录
- And 连接失败时保留远端目标、重连与诊断信息,不回退为本机终端
- When 切换当前项目后重新选择该终端 Tab
- Then 仍激活原远程实例,不重绑到新项目
### US-C5 打开受控浏览器
作为用户,我希望从右侧工作栏使用 GoodBuddy 受控浏览器,并按需创建独立页面。
验收:
- Given 已有浏览器 Tab,当前 Conversation 与其目标不同或没有活动 Conversation
- When 用户选择该 Tab
- Then 激活该确切实例,保留绑定目标、页面及导航状态,不重绑、重建或改选最近匹配实例
- Given 当前有活动 Conversation,无论是否已有匹配实例
- When 用户从工作栏“+”选择浏览器
- Then 每次创建绑定当前 Conversation 的额外实例,页面和导航状态独立,同一 Conversation 内共享隔离登录上下文
- Given 当前没有活动 Conversation,无论是否已有浏览器实例
- When 用户从“+”选择浏览器
- Then 创建待选择目标的空实例,不复用已有实例
- And 以上操作均使用 GoodBuddy 受控浏览器,不打开系统默认浏览器
### US-C6 查看本机资源
作为本地工作用户,我希望快速查看 CPU、内存和磁盘 IO,以便判断任务变慢是否与本机负载
有关。
验收:
- Given 资源监控面板尚未打开
- When 用户从右侧工作栏“+”选择资源监控
- Then 助手工作栏创建并激活单实例资源监控面板
- And 页面显示整机 CPU、内存、磁盘读写及 GoodBuddy 桌面进程摘要
- And 可见活动期间每 2 秒采样一次,前次请求未结束时跳过新请求,图表最多保留 150 个真实时间样本,连续采样约 5 分钟
- Given 资源监控实例已存在
- When 用户选择其 Tab 或再次从“+”选择资源监控
- Then 激活同一本机单实例,不新建重复面板
- When 用户切换到其他工作栏 Tab、收起工作栏、隐藏窗口或最小化窗口
- Then 资源监控停止请求新样本
- When 用户再次显示窗口并返回资源监控
- Then 首次采样建立区间指标基线,下一次采样开始显示 CPU 和磁盘 IO 速率
- And 内存等瞬时指标可立即显示,基线未建立时不显示虚构零值
- Given 某项指标在当前平台不可用
- When 页面收到部分结果
- Then 该指标显示“不可用”及原因,其他指标继续更新
- Given 本次采样全部失败或超时
- Then 显示失败原因和重试入口,保留已有历史并标明最后更新时间,不把旧值当作实时数据
- And 失败或暂停采样期间保留真实时间缺口,不补零或伪造中间样本
- When 可见活动期间自动重试或用户手动重试成功
- Then 按真实采样结果恢复更新,较晚返回的失活前旧请求不覆盖当前结果
### US-C7 从任意应用打开工具
作为用户,我希望在魔法笔记、知识库、智能心跳和运行记录页面也能打开系统工具。
验收:
- Given 用户位于任意一级页面
- When 用户展开右侧工作栏,选择已有工具 Tab 或通过“+”选择文件、终端、浏览器或资源监控
- Then 主页面保持不变
- And 右侧工作栏打开对应面板
- And 工具不会覆盖主内容或伪装成一级页面
- And 创建失败时工作栏保持展开并显示对应错误,主页面和其他实例不被清空
- And 后台事件不自动展开工作栏或切换当前 Tab,左侧和应用中心不复制工具入口或选中状态
## 5. Epic D:可访问性和布局
### US-D1 使用键盘管理应用与访问工具
作为键盘用户,我希望浏览和管理应用,并通过右侧工作栏访问工具,以便不依赖鼠标。
验收:
- Given 用户使用键盘访问侧栏底部
- Then 应用中心和设置是独立按钮,Tab 顺序与视觉顺序一致,Enter 和 Space 可激活,图标按钮有可访问名称和工具提示
- When 打开应用中心
- Then 入口提供菜单展开状态和控制关系,焦点进入首个可用菜单项;上下方向键循环移动,Home、End 到首尾,Enter、Space 激活
- When 按 Escape 关闭菜单
- Then 焦点返回应用中心入口;Tab 关闭菜单后继续正常焦点顺序,点击外部或焦点离开也关闭
- When 选择“管理应用”
- Then 关闭菜单并进入既有 Modal,焦点进入搜索;每行的打开、设置、排序及详情中的启用、常驻控件按正常 Tab 顺序可达,不伪装成单个菜单项;进入详情及返回时滚动归顶,返回后聚焦搜索
- When 通过键盘修改常驻或顺序
- Then Switch 使用共享开关语义并支持 Space,上移和下移按钮名称包含应用名,结果通过 polite 状态区播报,失败保留焦点
- Given 用户进入右侧工作栏
- Then Tab 选择、游标焦点、方向键及 Home、End 由工作栏负责,使用 `tablist`、`tab`、`tabpanel` 和 `aria-selected`,按确切实例表达选中状态
- And “+”为普通按钮,工具状态纳入 Tab 可访问名称;激活后焦点进入面板,关闭工作栏后返回固定工作栏开关
- And 焦点环不被边界裁切,应用中心及设置沿用共享浮层和原生浏览器遮挡处理
### US-D2 在窄窗口访问应用中心与工作栏
作为小屏用户,我希望应用管理和工具实例在窄窗口中仍可操作。
验收:
- Given 侧栏宽度为 `220px` 至 `420px`,或窄窗口使用侧栏抽屉
- Then 底部应用中心和设置不重叠,不因宽度不足隐藏入口或缩小点击区域
- When 打开应用菜单或通过“管理应用”进入 Modal
- Then 菜单向上锚定且限制在窗口内,可内部滚动;Modal 中长名称可换行,搜索、常驻、排序、设置及重新开启入口均可达
- And 触控不依赖悬停,工具提示保持在窗口内,点击区域符合统一规范
- When 关闭侧栏抽屉
- Then 焦点返回原触发按钮
- Given 右侧工作栏有多个工具实例
- Then Tab 单行横向滚动且“+”持续可见,工作栏停靠回流,不遮罩主页面;布局和 Tab 交互仍由工作栏负责
## 6. Epic E:项目活动级联
### US-E1 从摘要进入确切会话
对应 [FR-14](./prd.md#fr-14-项目活动汇总)。用户在当前项目工作时,可从项目选择器下方
查看跨项目活动,先定位项目,再进入需要处理的会话。
- Given 多个项目存在运行中和待处理会话,当前搜索仅匹配当前项目
- When 打开全项目活动并悬停或点击另一个项目
- Then 项目顺序保持稳定,该项目会话按待处理优先展示,当前页面和会话保持原选择
- When 点击会话
- Then 进入该确切 Conversation ID,清除搜索、任务选择和会话菜单,窄窗口关闭侧栏
- Given 设置页有未保存修改
- Then 会话跳转仍需通过既有离开检查,取消离开保留设置草稿和原项目
- Given 用户选择的确切 Conversation ID 未在内存中或已被删除
- When 请求进入该会话
- Then 刷新持久化会话列表后仍只查找同一 ID;刷新失败或会话不存在时显示通知并保留原页面和选择,不回退到项目最新会话或其他会话
### US-E2 键盘、窄窗口与实时结束
- Given 菜单已打开
- Then 上下方向键与 Home、End 选择当前层,右方向键或 Enter 进入会话层,左方向键或
Escape 返回;再次 Escape 关闭活动菜单并恢复摘要焦点,窄侧栏保持打开
- Given 窗口小于 `620px`
- Then 点击项目后仅显示会话层,可通过返回按钮回到项目列表;长列表可滚动到最后一项
- Given 鼠标从项目行移向相接的会话面板
- Then 子菜单保持打开,点击会话正常导航
- Given 当前项目已无待处理、运行中或已完成未查看会话
- Then 返回项目层并恢复可用焦点;全局无活动时整行隐藏摘要并关闭菜单,新活动到来时只恢复摘要
- When 点击外部、焦点离开、按 Tab 或隐藏侧栏
- Then 菜单关闭,后台新活动不自动重新展开
### US-E3 完成后保留至查看
对应 [FR-14](./prd.md#fr-14-项目活动汇总),具体状态规则见[活动逻辑](./logic-design.md#9-项目活动汇总)。
- Given 用户正在其他会话或应用中工作
- When 本次应用会话内的运行成功完成,或已观察到的后台任务转为完成
- Then 活动会话保留为“已完成”,全局与项目的已完成计数同步更新
- When 从活动菜单或普通侧栏实际进入该会话的可见聊天
- Then 清除其完成通知;没有其他活动行时隐藏摘要并关闭菜单
- Given 聊天被设置覆盖或文档隐藏
- When 对应运行完成
- Then 完成通知保留,直到设置关闭且目标聊天在可见文档中显示;取消设置离开不清除通知
- Given 目标聊天已可见且未被设置覆盖
- When 运行成功完成
- Then 不保留该会话的完成通知
- Given 初始加载已完成任务历史,或任务失败、取消
- Then 不产生新的完成通知;重载或重启也不恢复上一应用会话的通知
- Given 同一会话仍有待处理或运行中工作
- Then 按实时状态优先展示,只计入一个类别
## 7. 后续需求场景
US-F1、US-F2 为后续市场计划;US-F3 至 US-F5 的本机推理监控已接入源码,当前支持边界见技术设计,验证范围见实施进度。
### US-F1 连接组织应用市场
对应 [FR-15](./prd.md#fr-15-私有化市场与-yaml-交换后续)。
- Given 组织已部署提供应用市场的 ShareServer
- When 用户配置市场 URL,并在来源要求时完成登录
- Then 可以浏览该来源的应用目录、详情及可获取版本
- And 市场 URL 按设备保存,重启后恢复;认证信息使用受保护凭据存储,不作为可导出配置
- Given 市场不可达、URL 无效、登录过期或认证失败
- When 用户访问该来源
- Then 显示具体原因及修改地址、重新登录或重试入口,已安装和内置应用仍可打开,不静默切换公共市场或其他来源
- Given 用户修改市场来源或开始获取应用资源
- Then 市场配置不改变模型、应用更新及 Runtime 等独立下载源;已开始的下载保持原来源,失败不静默切源
### US-F2 导入导出应用定义
对应 FR-15。
- Given 用户需要在私有部署间迁移应用
- When 导出应用为 YAML,并在另一台设备选择 `.yml` 或 `.yaml` 文件导入
- Then 预览应用身份、版本、可迁移配置和依赖,凭据与业务数据不包含在导出文件中
- When 用户确认有效定义且所需依赖已满足,导入成功
- Then 应用目录显示导入的身份、版本和配置,可按其启用状态打开或配置,不自动启动模型或任务
- Given 同标识应用已存在
- When 用户查看导入预览
- Then 可选择更新或取消,取消后原应用与配置保持不变
- When 更新成功
- Then 显示已确认的新定义和配置
- When 更新失败
- Then 显示具体原因及重试入口,保留原应用定义、版本和配置,不留下部分更新
- Given 文件格式无效、版本不支持或离线环境缺失程序包或模型资源
- Then 显示具体问题与补充要求,不将应用误报为可运行,也不自动启动模型或任务
### US-F3 查看本机推理服务与资源
对应 [FR-16](./prd.md#fr-16-本机模型管理入口新增计划项)。
- Given 用户使用依赖本机模型的应用
- When 从常驻入口、应用菜单或管理 Modal 的“打开”进入“本机推理监控”
- Then 均打开同一管理 Modal,来源菜单或管理 Modal 关闭,底层工作区、当前项目、会话和未提交草稿保持不变
- And 直接显示服务列表,不出现任务页签、任务历史或外部连接;知识库、智能心跳和魔法笔记仍打开主内容区页面
- And 初始焦点进入关闭按钮,背景不可交互,Tab/Shift+Tab 在 Modal 内循环;标题与关闭按钮保持可见,正文独立滚动
- Given 没有影响确认且没有操作提交
- When 点击关闭按钮或按 Escape
- Then 关闭并卸载管理 Modal,底层工作区继续显示,模型服务不停止;点击遮罩不关闭
- And 焦点恢复到仍可用的原触发元素;原入口已移除时回到应用中心,窄侧栏已隐藏时回到侧栏开关
- When 查看服务列表
- Then 显示服务名、模型、引擎、简短状态及错误;共享服务只展示一次
- Given 向量服务独立进程正在运行
- Then 显示带 PID 和进程范围的 CPU、工作集内存;CPU 首次采样显示预热状态
- Given 服务 worker 与宿主共享进程,或进程已退出、采样失败
- Then 在服务行显示不能独立统计或当前不可用的原因,不展示虚构零值或借用整个应用进程指标
- Given ASR 有活动请求但没有 Worker 就绪观测
- Then 显示“按需运行(状态未知)”,不推断服务正在运行
### US-F4 单任务取消入口(已移除)
保留故事 ID。FR-16 已收窄为服务监视,Modal 不再提供任务取消按钮。执行路径的原有取消行为和活动请求登记继续用于服务影响校验,见[逻辑设计](./logic-design.md#81-本机推理管理)。
### US-F5 管理实际执行服务生命周期
对应 FR-16;当前引擎支持清单见[技术设计](./technical-design.md#111-本机推理任务与执行服务)。
ASR 取消独立 Worker,向量停止核对所有活动请求,OCR 有活动或排队请求时拒绝释放。
运行阶段包括排队、加载和执行;当前未接入统一进度及来源项目对象关联。资源指标按服务进程统计,见 US-F3。
- Given 多个应用共用一个由 GoodBuddy 受管子进程承载的模型服务
- When 用户启动、停止或重启服务
- Then 仅提供引擎实际支持的操作,按同一服务串行处理;重复请求不重复创建进程或并发执行同一服务的重启
- And 以真实进程退出和服务就绪确认结果,不将命令已发送当作操作成功
- Given 用户准备停止、重启或释放服务或模型
- When 用户发起操作
- Then 在管理 Modal 上方打开紧凑影响确认,列出使用方、任务及影响;无已登记活动任务时说明执行前再次核对,取消后不发送操作指令且运行状态不变
- And 初始焦点进入“保留运行状态”,Tab/Shift+Tab 在确认内循环,底层管理 Modal 不可交互;Escape 只关闭确认并恢复服务操作按钮焦点,按钮不可用时回到管理 Modal
- When 确认提交尚未完成
- Then 禁止重复执行,确认和取消按钮均禁用,两层 Modal 都不能通过关闭按钮或 Escape 关闭
- When 操作成功
- Then 只关闭影响确认,管理 Modal 保持打开
- Given 执行前出现确认清单未包含的新任务
- Then 拒绝执行,关闭旧确认并显示错误,自动刷新状态;刷新期间或失败时禁用服务操作,再次发起操作须确认新的影响清单
- Given 停止失败,或重启时停止成功但启动失败
- Then 显示实际状态、原因和可重试错误,不假报已停止或已恢复;无法确认时显示未知
- When 服务再次启动或重启成功
- Then 已中断任务不自动重放,由来源应用显式重新发起
- Given 模型连接由外部程序管理
- Then 在既有模型与连接设置中查看,不将其列入本机服务或提供停止、重启
- Given 模型在进程内运行
- Then 仅提供引擎实际支持的加载、释放,不显示虚构 PID、进程数或进程启动停止状态
- When 用户关闭管理 Modal、关闭管理应用或取消常驻
- Then 模型服务运行状态不变,取消常驻不关闭当前 Modal,卸载模型文件为独立操作
+25
View File
@@ -0,0 +1,25 @@
# 助手工作栏
助手工作栏统一承载本机与远程执行空间、Task、成果、文件、浏览器和终端等持续工作界面。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [通用助手工作栏与执行空间 PRD](./prd.md) | 工作栏整体产品范围、执行空间和各工作面板归属 |
| [多终端页签 PRD](./terminal-tabs-prd.md) | 工作栏应用的固定、可关闭、单实例和多实例策略,以及多终端生命周期 |
| [浏览器多实例技术设计](./browser-tabs-technical-design.md) | 浏览器 Tab 身份、Conversation 绑定、IPC、BrowserService 和 MCP 路由 |
| [实现与验证进度](./progress.md) | 工作区与 Runtime 相关改动的实现事实、验证证据与剩余阻塞 |
| [执行记录存储与升级回收](./execution-history-storage.md) | 子任务差量存储、远程去重、旧库转换、会话置顶、摘要与完整历史按需读取、内存引用释放 |
| [Runtime 交互边界](./runtime-interactions.md) | 原生权限、结构化问答、后台拒绝、终端交互、并行与输出、消息请求状态及资源生命周期规则 |
| [Runtime 进程复用技术设计](./runtime-process-reuse-technical-design.md) | 跨 Runtime 资源调研、锁定协议证据、进程与会话拆分、远端模型桥、实施顺序和性能验收 |
| [Runtime 执行清单技术设计](./runtime-checklist-technical-design.md) | OC/CN 原生清单提取、远程 Agent 传递复用、消息恢复与真实部署验收;尚未实现 |
当前尚未拆出独立 User Stories、功能逻辑和 UI 设计文档;产品场景与验收保留在现有 PRD,
Runtime 资源状态规则由交互边界文档维护。浏览器多实例和 Runtime 进程复用涉及不同的
跨进程身份与工具路由,分别使用技术设计文档维护。
执行清单的需求、场景和顶部布局见 [PRD](./prd.md#131-runtime-执行清单),状态规则见
[Runtime 交互边界](./runtime-interactions.md#runtime-执行清单)。本目录拥有该功能;
[Task 与 Job](../task-and-job/README.md)拥有产品任务领域,
[远程主机](../remote-host/technical-design.md#执行清单接入范围)拥有远程部署与连接边界。
@@ -0,0 +1,245 @@
# 浏览器多实例技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 基础多实例已实施,Agent 多 Tab 工具待需求 |
| 版本 | 1.0 |
| 日期 | 2026-09-13 |
| 关联 PRD | [通用助手工作栏与执行空间 PRD](./prd.md) |
| 页签策略 | [助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) |
## 1. 目标与边界
浏览器改为可关闭的多实例工作栏应用。每个工作栏浏览器实例对应一个独立 Browser Tab,拥有
独立页面、导航状态、操作队列、画面状态和元素引用空间。同一 Conversation 下的多个 Tab
共享隔离存储与代理上下文,使登录状态符合常规浏览器预期。
本设计不把多个工作栏 Tab 实现为同一页面的重复入口,不允许用户切换 Tab 后改变正在执行的
Agent 工具目标,也不在首期要求模型主动管理多个 Tab。
## 2. 改造前限制
改造前的链路以 `conversationId` 作为唯一浏览器身份:
- Renderer 只保存每个 Conversation 的一份 `BrowserLiveState`。
- Browser IPC、Preload API 和 `BrowserService` 操作只携带 `conversationId`。
- Main 每个 Conversation 只创建一个 Session、一个 `WebContentsView` 和一个 CDP Driver。
- 请求级 MCP 能力只保存 `browserConversationId`。
- snapshot 元素 `ref` 只在一个 Driver 的引用空间内有效。
因此只把工作栏注册策略改成 `multiple` 会导致多个 Tab 控制同一页面,并产生 viewport、清理和
元素引用竞态,不能作为可交付实现。
## 3. 身份与所有权
```ts
type BrowserTabId = string
type ConversationBrowserContext = {
conversationId: string
partitionId: string
tabs: Map<BrowserTabId, BrowserTab>
primaryTabId?: BrowserTabId
}
type BrowserTab = {
id: BrowserTabId
conversationId: string
view: WebContentsView
driver: CdpBrowserDriver
state: BrowserLiveState
}
```
- Conversation Browser Context 拥有隔离 partition、代理配置和全部 Tab。
- Browser Tab 拥有页面、Driver、串行操作队列、snapshot 代次、idle 状态和可见 viewport。
- `BrowserTabId` 是 Main 签发的不透明 ID。Renderer 和 Runtime 不能自行选择或复用。
- 工作栏实例保存公开的逻辑实例 ID 和 Conversation 绑定;进程重启后不恢复旧
`WebContentsView`,首次使用时由 Main 为该逻辑实例创建新的 Browser Tab。
- 删除 Conversation 时关闭该 Context 下全部 Tab;关闭一个工作栏浏览器实例只关闭对应
Browser Tab,不得调用 Conversation 级 Runtime 释放路径。
## 4. Renderer 与 viewport
Renderer 状态改为:
```ts
Record<ConversationId, Record<BrowserTabId, BrowserLiveState>>
```
每个浏览器工作栏实例持有自己的 `browserTabId`。只有当前可见实例可以提交 viewport。每次
获得可见权时生成新的 UUID `leaseToken`;提交请求携带 `browserTabId` 和 token,清理请求必须
回传同一个 token。Main 只接受当前 token 的清理,避免已隐藏实例的延迟 cleanup 隐藏新实例,
也避免 Renderer 重载后从零开始的数字序列无法覆盖旧租约。
非活动 Browser Tab 保持页面和状态,但隐藏 `WebContentsView` 并停止非必要画面采集。工作栏
收起、切换主页面和窗口隐藏时采用相同规则,不销毁 Tab。
普通导航、快照与页面操作只同步轻量导航状态,不自动截图、等待画面或传输 JPEG;只有显式
`browser_screenshot` 调用生成图片。
`BrowserViewport` 统一观察 body Portal 的插入、移除和可见属性变化,以及浮层尺寸、
窗口尺寸、滚动和过渡结束。`browser-viewport-occlusion.ts` 按共享语义识别
`aria-modal="true"`、dialog、alertdialog、menu 和应用通知:可见应用级 Modal 始终释放
viewport,其余浮层只在与浏览器区域相交时释放。隐藏节点和隐藏祖先不阻塞浏览器。
最后一个遮挡消失后,只有当前活动实例恢复同一 viewport 租约;网页、历史和页面状态不变。
终端关闭确认复用此机制,不再维护单独的隐藏条件。页面无需逐个登记弹窗,也不能靠提高
CSS `z-index` 覆盖原生子视图。此逻辑仅涉及 Desktop Renderer 的显示租约,不改变
Runtime、MCP 或远端 Agent 的请求、导航与生命周期。
浏览器应用声明 `visibleAcrossContextSwitches: true`,打开的 Tab 跨项目、会话切换继续显示。
切换活动 Conversation 不改变当前选中的浏览器或其 `targetRef.conversationId`;用户在会话 B
查看会话 A 的浏览器时,viewport 和手动导航仍指向 A 的原 Tab。新建实例绑定 B。仅未绑定的
默认或恢复实例可在首次使用时取得当前会话绑定。
Tab 的跨会话可见性与 MCP 授权分开处理。B 的请求不能选择属于 A 的可见 Tab;A 已取得的
capability 在用户切到 B 后仍固定路由至 A 的原 Tab;使用租约不阻止用户关闭该 Tab。
内部点击实例只同步应用类型,不再次按当前 Conversation 改选实例。Main 状态携带对应的
`workbarInstanceId`,Renderer 据此绑定请求创建的同一 Tab,而不是另建空白页。收到 `stopped`
后清除该实例的旧 Tab 绑定,下一次使用通过原逻辑实例重新创建,保留地址草稿。
刷新可用性读取 Main 的已提交 URL,不使用地址草稿或仅凭 `sessionActive` 判断。
侧栏操作错误以当前工作栏实例和面板展开状态为作用域;切换或关闭时清空,旧作用域的
异步回调不再写入错误,即使用户随后返回同一实例。
请求创建的实例也遵守现有 32 个工作栏页签上限;满额时保留 Main 页面及其状态,并就地提示
关闭一个可关闭页签。释放位置后再绑定该页面,不写入超限布局或丢弃 Agent 页面。
## 5. IPC 与服务接口
共享契约增加有界操作:
```ts
browser.createTab({ conversationId, workbarInstanceId })
browser.listTabs({ conversationId })
browser.closeTab({ conversationId, tabId })
browser.setViewport({ conversationId, tabId, leaseToken, bounds })
browser.navigate({ conversationId, tabId, url })
browser.back({ conversationId, tabId })
browser.reload({ conversationId, tabId })
browser.stopLoading({ conversationId, tabId })
```
Main 必须验证 `tabId` 属于同一 `conversationId` 和当前窗口。BrowserService 将现有
Conversation 级 slot 拆为 Context 与 Tab 两层;同一 Tab 内操作继续串行,不同 Tab 可以并行,
但仍受窗口级和 Conversation 级有界数量限制。
现有 Conversation 级 `stop/release` 保留为销毁完整浏览器上下文的操作,不能用作关闭单个
Browser Tab。
## 6. Agent 与 MCP 路由
### 6.1 兼容阶段
第一阶段不修改模型可见的现有浏览器工具参数。模型请求开始时,Main 按以下顺序选择并冻结
`boundBrowserTabId`:
1. 当前可见且绑定该 Conversation 的 Browser Tab。
2. 该 Conversation 的 `primaryTabId`。
3. 预留 primary Browser Tab 身份,首次实际浏览器操作时才创建页面与 Conversation Context。
未使用浏览器的请求只持有随请求释放的内存预留,不创建 Chromium 资源、不触发浏览器 UI、
也不占用活动浏览器会话名额。物化后使用同一 Tab 身份和工作栏实例归属;取消、能力撤销及
服务关闭释放未使用的预留,不保存到磁盘。编程 Subagent 继承父请求的 Browser Tab 与浏览器
Conversation 归属,不新建租约或把页面重新归属到子级对话。
直连模型上下文和请求级 MCP capability 同时保存 `conversationId` 与
`boundBrowserTabId`。请求执行期间,用户切换、创建或关闭其他工作栏 Tab 不改变工具目标;
用户可直接关闭模型正在使用的 Tab。关闭释放该 Tab 的页面和使用租约,中止其浏览器操作,
不取消整个模型请求、不撤销 MCP capability,也不关闭同级 Tab。系统不得把进行中的操作改投其他 Tab。
关闭后,快照等非导航工具返回“浏览器标签页已关闭,请调用 browser_navigate 打开新标签页”,
不自动创建页面或使用现有 primary/sibling。只有显式且参数有效的 `browser_navigate` 才创建
专用替代 Tab 并绑定后续工具;旧 Tab 身份与元素引用不复用。Gateway 与直连 Provider 均隔离
单次工具取消信号、Tab 使用租约信号和请求信号,取消一次工具调用不终止后续调用。
该阶段 MCP transport、endpoint 和 token 生命周期不变,只扩展服务端 capability 内容和内部
路由。工具调用使用当前请求绑定的 Tab;显式导航恢复和下述点击开窗会更换绑定。
### 6.2 Page-created tabs
`target="_blank"` and `window.open` create managed `WebContentsView` tabs through Electron's
`setWindowOpenHandler.createWindow`. Chromium retains the original request and opener relationship,
including a blank window followed by a script-assigned location. These tabs share the Conversation
partition, proxy and permission restrictions. HTTP(S) URL checks remain in the session and filtering
proxy; `about:blank` is permitted only as the initial empty popup. Unsupported URL schemes remain denied.
BrowserService reserves capacity before creating the native view, assigns a new Tab/workbar identity,
and owns initialization, the Driver and cleanup. Initialization failure or Conversation disposal releases
the popup. Closing the opener leaves an already created popup alive. A popup's `window.close()` releases
its service slot and usage leases; Renderer removes its workbar instance and returns to the opener when
it is still available. No separate unmanaged BrowserWindow is created.
Live state includes `openerTabId`. A popup from the visible browser is selected even when the active chat
has switched to a different Conversation. Other workbar panels and viewport occlusion rules retain their
existing behavior. The existing workbar capacity notification still applies when all 32 slots are occupied.
During `browser_click`, the service collects popup registrations from that Tab and waits for their
initialization. The result identifies the last created popup. BrowserModelTools binds subsequent tools to
that Tab and asks the model to obtain new references with `browser_snapshot`; the MCP gateway also replaces
its capability binding and usage lease. Switching visible tabs does not alter this binding. Closing the
popup retains the existing explicit `browser_navigate` recovery behavior. A page may still be loading
when the click returns. Popups requested asynchronously after the click has completed are displayed as
managed tabs, but are not attributed to that completed tool call.
The remote ACP Runtime exposes its separately scoped image MCP server, not this desktop browser gateway;
this change does not add browser access to the deployed GoodBuddy Agent.
### 6.3 Agent 多 Tab 阶段
只有明确需要 Agent 管理多个页面时,才增加:
- `browser_tab_list`
- `browser_tab_open`
- `browser_tab_close`
导航、snapshot、back 和 screenshot 增加可选 `tabId`;省略时使用请求绑定的默认 Tab。
`browser_snapshot` 返回 `tabId` 和 snapshot 代次。click、type 和 select 必须校验 capability、
`tabId`、引用代次和元素 `ref` 属于同一 Driver,禁止跨 Tab 猜测或解析引用。
## 7. 工作栏创建与快捷入口
- 从“+”选择浏览器时始终创建新工作栏实例,并默认绑定当前 Conversation。
- 没有活动 Conversation 时创建待绑定实例,不创建无归属 Main Browser Context。
- 主侧栏浏览器快捷按钮优先激活绑定当前 Conversation 的最近使用实例;没有匹配实例时创建
一个。没有活动 Conversation 时激活最近使用的浏览器实例;完全不存在时创建待绑定实例。
- New browser instances use a numbered browser label followed by the active Conversation title, when
available. The number avoids existing titles; restoring the layout preserves each saved title.
The address bar shows only that instance's page URL. Binding a numbered instance does not overwrite
its title; the default unbound browser receives the Conversation label when first bound.
- 关闭工作栏 Browser Tab 只释放对应 Browser Tab。关闭浏览器上下文是独立危险操作,需要
明确说明会影响该 Conversation 下的全部 Tab 和 Agent 浏览器状态。
## 8. 持久化与恢复
`createTab` reuses a live tab only for the same Conversation and workbar instance ID. A new workbar
instance can adopt the primary tab only when its request creates the Conversation context. An existing
primary, including one opened by an Agent, is not adopted by a new instance. New sibling tabs start at
`about:blank` with no navigation history and share the Conversation's cookie partition.
持久化工作栏实例 ID、顺序、自定义标题和公开 Conversation 绑定,不持久化页面进程、CDP
引用、snapshot、viewport lease、Cookie、Token 或 Main `BrowserTabId`。应用重启后实例显示
未启动状态,展开浏览器面板或 Agent 首次使用时按原绑定创建新 Tab。
绑定的 Conversation 已删除或不可见时保留实例并显示目标失效,不自动绑定当前 Conversation。
## 9. 限制与验收
- 分别限制每个 Conversation 的 Tab 数和每个窗口的总活动 Tab 数;达到上限时在创建前反馈。
- 两个 Tab 的地址、历史、加载状态、截图和元素引用互不覆盖。
- 同一 Conversation 的 Tab 共享登录状态,不同 Conversation 保持隔离。
- 延迟 viewport cleanup 不能隐藏另一个已激活 Tab。
- 一个 Tab 的元素 `ref` 不能在另一个 Tab 中执行。
- 用户切换工作栏 Tab 不改变进行中模型请求的 MCP 浏览器目标。
- 关闭一个 Tab 不释放 Conversation Runtime,也不关闭同一 Conversation 的其他 Tab。
- 关闭已租用 Tab 后,同一 MCP 会话仍可列出工具;旧目标快照报错且不创建页面,显式导航创建
不同身份的替代 Tab,同级页面、导航状态与其他 Conversation 的可见 viewport 保持不变。
- 旧版无 `tabId` 的模型工具调用仍操作请求绑定的默认 Tab。
## 10. 实施顺序
1. 引入 Browser Context 与 Browser Tab 两层身份,拆分 partition 和页面生命周期。
2. 扩展 Shared、Preload、IPC 与 BrowserService 的 `tabId` 和 viewport `leaseToken`。
3. 改造 Renderer 状态与工作栏实例,开放浏览器 `multiple` 策略和单 Tab 关闭。
4. 将直连模型与 MCP capability 绑定到请求开始时确定的 Browser Tab。
5. 完成跨 Tab 引用隔离、并行队列、共享 Cookie、关闭和恢复测试。
6. 有明确 Agent 多页面需求后再开放模型可见的 Tab 管理工具。
@@ -0,0 +1,57 @@
{
"date": "2026-09-17",
"status": "passed-with-counting-limitation",
"sourceBase": "3639566e1b52296c6a9bc409bcfcecb86c905e30",
"source": "Working tree including consecutive-request cancellation and terminal snapshot fixes",
"environment": {
"platform": "Windows x64",
"runtime": "OpenCode",
"mode": "ask",
"model": "Existing configured default text model",
"upstreamProtocol": "http",
"fullProductionApp": true,
"isolatedProfile": true,
"nativeInput": true
},
"checks": {
"twoQueuedRequestsCompleteAndPersist": true,
"cancelAfterExternalDispatch": true,
"cancelToResendMs": 25,
"resendCompletesAndPersists": true,
"cancellationPersists": true,
"stopAbsentAfterBothScenarios": true,
"sendEnabledWithUnsentProbe": true,
"terminalScreenshotsReviewed": true
},
"requests": {
"instrumentedExternalDispatches": 4,
"http200": 3,
"cancelledBeforeResponseHeaders": 1,
"observedRetries": 0,
"toolExecutions": 0,
"initialUninstrumentedCompletedGenerations": 2,
"totalUiSubmissions": 6,
"totalExternalDispatches": null
},
"limitations": [
"Initial Main counting hook missed child-process HTTP traffic; total dispatches across attempts are unknown.",
"Successful rerun used a transparent Main relay to the unchanged configured HTTP upstream for dispatch counting.",
"Seven tool definitions remained exposed by production Ask, with zero observed tool executions.",
"Upstream proxy retries and cancelled request outcome at the upstream are not observable.",
"No deterministic stale-event ordering, remote Host, or restart verification in this live run."
],
"validation": {
"focusedTestsPassed": 362,
"typecheck": "passed",
"productionBundle": "passed"
},
"cleanup": {
"ownedProcessesStopped": true,
"isolatedProfileRemoved": true,
"temporaryBuildRemoved": true,
"sensitiveCopiesRemoved": true,
"originalSettingsUnchanged": true,
"targetSourceHashesUnchanged": true
},
"localArtifacts": "consecutive-ui-0917/{final-evidence.json,scenarios.json,report.json,normal-terminal.png,cancel-resend-terminal.png} under the approved temporary directory"
}
@@ -0,0 +1,128 @@
# 执行记录存储与升级回收
通用迁移、备份、性能测量和发布要求见
[数据库迁移与大数据升级指南](../../development/database-migrations.md)。
## 用户行为
子任务仍实时显示完整的文字、思考、工具过程和最终结果。运行时间较长、工具输出较多时,
数据库不再为每个流式更新重复保存此前的整份过程;本机和托管 SSH OpenCode 使用相同
的桌面存储机制。此修复不关闭子任务、不截短已有过程,也不增加 Execute 权限门槛。
已有数据库在首次升级时自动处理。启动页铺满窗口,显示处理数量、全宽进度条和近期
处理速度;处理时间取决于历史数据量与磁盘速度,不承诺固定几分钟。用户可以退出,
下次启动重新检查并继续转换未完成的数据;完成后回收空闲页并进入应用。失败时留在
该页提供重试与退出,磁盘空间不足会明确说明,不删除聊天来腾出空间。
## 存储规则
- `messages.metadata_json` 继续保存供会话显示的完整子任务过程。
- `task_events` 的 `subagent` 内容由共享 `SubagentProgressStorage` 编码。
`progressUpdates` 只保存文字追加、变更的块,或确有删除/重排时的完整重置。
完全相同的过程使用空更新数组。工具字段按值比较,缺省字段与 `undefined` 等价,
不因 JSON 省略空字段而重复写入整个工具结果;空字符串与实际输出/错误变化仍保留。
其他事件内容不改变。
- 每个任务内按事件 ID 顺序、按 `childTaskId` 独立还原过程。事件 ID、顺序、状态、
最终输出、错误、时间及远程来源字段均保留,不按天数裁剪或删除旧事件。
- Runtime、Agent transcript、Preload 和 Renderer 的公共 `SubagentEvent` 仍传完整
`progress`;差量表示只存在于桌面数据库内部,不要求更新远端 Agent 协议。
- 远程消息投影与事件插入仍共用 SQLite 事务。重放时还原已保存内容进行等值校验,
相同来源重复事件不重复投影,不同内容继续报冲突。内存过程缓存通过已提交事件 ID
核对;事务回滚或重新打开后从数据库重建,不保存独立快照或回收日志。
- 存储层最多缓存 16 个任务;收到任务完成、错误、取消或中断的终态记录后立即移除该任务
的缓存引用,不影响其他会话。若事务随后回滚或仍收到该任务的后续事件,按已提交记录重建。
释放引用不等于进程 RSS 立即下降,垃圾回收和 Runtime 自身会话缓存另按生命周期回收。
## 活动历史连续保存
活动历史在桌面 `AssistantDatabase` 中按变化记录写入,保留记录顺序和重复 ID 的独立位置。
连续保存使用最近一次成功提交的记录作为比较基准,不再逐次读取整份历史。关闭数据库、
清空数据或检测到其他 SQLite 连接提交后重新读取;事务失败不更新比较缓存,重试仍基于已提交内容。
本次优化不改变数据库 schema 或完整快照 IPC,也不改变 Agent、Runtime 和远程模型桥。
本机与远程任务的桌面活动记录共用此保存路径,Host 不需要更新。
WAL 增长与 SQL 全历史扫描次数的定向验证见 [BUILD.md](../../../BUILD.md);
这些指标不等于安装后应用的物理磁盘吞吐测量。
## 会话列表读取与前端保留
列表读取与数据库存储是两个不同层次。`messages.metadata_json` 继续保留完整过程,
本次读取优化不修改 schema、迁移或用户数据:
- Renderer 启动及焦点、会话、队列刷新使用 `conversations.listSummaries(detailIds)`。
普通未打开会话只返回标题、项目、置顶状态、上下文设置和消息数量/首条角色,不解析或跨 IPC
传输正文、工具、子任务及图片操作元数据。
- 当前会话、现有 KeepAlive 视图,以及 Main 仍持有请求、数据库中仍有活动 Task 或
streaming 消息的会话返回完整消息。列表包含全部 active 会话,不限制数量,
按置顶优先、组内更新时间倒序返回;仅置顶不会加载完整历史。
- 打开历史通过 `conversations.get(id)` 读取全部消息,不使用会截断历史的分页上限。
加载时明确显示进度;失败保留会话与输入,提供原地重试。发送、队列派发和压缩上下文
必须先确认完整历史,发送时不按消息条数截断;复制与导出临时读取完整内容,不把未打开会话留入视图缓存。
- 搜索由 Main 在 SQLite 中读取标题与正文,不读取过程元数据,保持原来的 Unicode
大小写匹配和字面子串规则。范围包含全部 active 会话;前端同时匹配尚未
落库的消息。搜索状态只保留命中的 ID,不额外持有整份历史对象。
- KeepAlive 淘汰后,在现有保存周期中释放已经确认落库且空闲的详情及持久化比较引用。
未保存消息、正在执行的会话、待答/待审批内容不被淘汰。读取中的请求按会话合并;
晚到摘要不能覆盖刚打开的历史或新到的后台审批,摘要附带的实时新增消息仍增量保存。
- `messageSummary` 仅为只读列表标记,不进入持久化 header。保存继续使用已有逐消息
增量接口,空消息数组不代表删除历史。原有完整 `list()` 与数据库 `getConversation()`
接口保留供其他调用方使用。
- 图片状态通过同一合并函数处理。本地更新与 Main 图片操作按原有时间优先级合并,
未变化消息返回原对象,避免每次列表刷新后把无变化的整个历史当作脏消息再次保存。
- 本机和托管 SSH 的 Renderer/Main 读取、持久化与恢复投影共用以上实现。Agent 的
transcript、原生 Session、权限及模型桥协议未改变;完整列表、摘要和详情都附带
Main 当前恢复请求的问题投影。
此优化不等于工具折叠内容的延迟渲染,也不保证操作系统立即归还全部 Private Commit。
实际测量与适用边界见[实现与验证进度](./progress.md)。
## 会话置顶
界面行为见[会话置顶与操作菜单](../application-tool-navigation/ui-design.md#会话置顶与操作菜单)。
`conversations.pinned` 是桌面数据库维护的会话属性,本地和消息通道会话共用,默认未置顶。
完整快照与摘要均返回 `pinned`,旧快照缺省按未置顶处理。写入只通过
`conversations.setPinned({ conversationId, pinned })`,经 Preload、可信发送方校验和严格
Schema 校验后更新活动会话,再发布会话变更通知;不存在的会话报错。该写入不修改时间戳、
消息或其他会话字段,普通自动保存 header 排除 `pinned`,不会覆盖置顶状态。
尚未落库的本地草稿先等待保存队列并保存,再设置置顶;消息通道会话直接调用专用接口。
Renderer 合并时独立接收 Main 的置顶属性,即使本地正文更新时间更晚也不忽略它。
请求修订号和进行中标记防止较早发出的列表/详情响应覆盖刚更新的置顶状态。
此属性不修改 Runtime 或远端 Agent 协议。
## 旧库升级
启动检查只根据旧 schema 或仍未转换的笔记正文决定是否进入升级流程;正常写入、更新
与删除产生的 SQLite 空闲页不是旧格式证据。迁移后新增数据使用当前格式,不因正常使用
再次迁移。同一次启动已进入的迁移流程可重试压缩;若正文已转换、仅压缩中断,退出后
重开不单凭空闲页重新进入迁移,剩余页由 SQLite 后续写入复用。不新增持久化迁移标记。
桌面在打开业务数据库之前启动独立 Node 工作线程,使用同一套编码和还原函数转换旧库。
按任务有序读取,每批最多 32 条,提交后报告进度;SQLite 保留事务原子性及正常 WAL
自动 checkpoint,每个任务结束时截断 WAL。读取缓存为 64 MiB,避免 1 MiB 级历史
快照在过小缓存中反复读取;不以关闭事务或同步落盘换取速度。
转换可以混合读取旧完整记录与已经转换的记录,也会重新编码 schema 34 中重复的工具块
更新。因此退出、磁盘写入失败后可以重新扫描并继续;已是相同 JSON 的记录不重复更新,
不建立额外的恢复状态表。最后运行 SQLite `VACUUM` 回收已释放的空间,而非
在转换前复制整份膨胀内容。回收仍需要额外可用磁盘空间,失败保留已提交转换并允许重试。
子任务整理对应 schema 35;schema 34 用户也会执行该整理,成功后后续启动直接跳过。
继续复用 schema 34 的子任务事件部分索引和同一差量表示。当前业务初始化随后在事务中
升级为 schema 36,新增 `pinned INTEGER NOT NULL DEFAULT 0 CHECK(pinned IN (0, 1))`,
已有会话默认未置顶,保留原消息与时间戳;失败回滚本次字段迁移。
低于该 schema 的旧版客户端不能重新打开已经升级的数据库。需要回退客户端时,必须使用
升级前备份,不要修改 `user_version` 强行绕过版本检查。
## 验证要求
- 使用可还原的合成过程验证文字追加、内容修订、块重排、清空和多次打开。
- 工具 fixture 必须含生产对象会携带的 `undefined` 可选字段,并经过实际 JSON 写入/重读。
同时验证旧完整事件和 schema 34 差量事件的整理、混合重试及无变化记录不被重写。
- 验证远程重复事件、冲突事件,以及消息更新失败时事件和过程缓存的事务回滚。
- 在真实旧库的已核验备份副本上运行 portable 的正常启动入口,核对事件逐条等价、
其他用户表内容、数据库完整性和实际文件大小,不能只统计空闲页或只测试内存 fixture。
- 同时验证真实本地 Runtime 和共享 Linux Host 的桌面持久化路径。真实场景必须让主代理
与子代理按正常任务需要读取、修改文件和执行测试,不能用“子代理不使用工具”的纯文字
场景代替工具存储验收;记录实际请求数、有效变化、重复工具块及还原结果。具体证据见
[实现与验证进度](./progress.md)。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,142 @@
# Runtime 执行清单技术设计
状态(2026-09-19):本地与远程 OC/CN 提取、统一事件、消息保存和顶部展示已实现。
默认开发组包同时携带 OC/CN,远程 CN 已通过 Windows 桌面到 Linux x64 的真实模型
更新、取消、重启恢复及续发验证。验证范围与调用数见
[远程记录](../remote-host/runtime-checklist-validation.md#2026-09-19-联合包与完整桌面验收)。
需求、场景与顶部布局由 [PRD](./prd.md#131-runtime-执行清单)
维护,替换、隔离和终态规则由 [Runtime 交互边界](./runtime-interactions.md#runtime-执行清单)
维护。研究证据与交付状态见[进度](./progress.md#2026-09-15-runtime-执行清单设计未实现)。
## 1. 原生数据与提取位置
Implemented local behavior:
- OC sends a native-sortable `messageID` with both prompt and command submissions.
Only assistant messages whose `parentID` matches that request can own checklist
calls. Parent Session identity is checked independently of child-session routing.
OC 1.18.29 emits `pending`, `todo.updated`, then `running` with complete arguments.
The adapter retains native snapshots only while an owned call is active and
submits a snapshot once its validated items match that call's original input.
Calls and pending snapshots are request-local; no `session.todo` polling is used.
- CN extracts the exact `Checklist.checklist` argument in the patched native start
callback and in native history before display truncation. Candidates survive
success frames without input. A completed call commits once per call ID; failed
calls and startup history do not commit. Ordinary tool events retain their limits.
- CN accepts blank lines and `- [ ]`, `- [x]`, `- [X]` entries with nonempty content.
Empty or whitespace-only strings clear the list. Other nonempty lines invalidate
the entire update. Native 1.5.47 accepts a string without Markdown validation;
these are GoodBuddy's explicit projection rules, not additional native guarantees.
| Runtime / 路径 | 已确认格式与设计接入 |
| --- | --- |
| 本地 OC 1.18.29 | `todo.updated` 与 `session.todo` 返回完整条目列表,字段为 `content/status/priority`,无 item ID;以原生更新事件驱动本轮整份替换 |
| 本地 CN 1.5.47 | 精确原生工具 `Checklist` 的参数 `checklist` 是 Markdown 勾选列表,`- [ ]` / `- [x]` 表示未完成/完成;按一次成功调用提交完整替换 |
| 远程 OC/CN | OC 1.18.29 通过原生 todo.updated,CN 1.5.47 通过成功 Checklist 参数进入 Agent transcript;Linux x64 清单与重放已实测,CN 同时验证取消后续发,见[远程记录](../remote-host/runtime-checklist-validation.md) |
`opencode-runtime.ts` 当前将普通工具 input 经 `boundedToolDetail(..., 4_000)` 转成
展示字符串。接入应在 SDK 原始事件层消费 `todo.updated`;如需核对 `todowrite` 参数,
必须在该截断之前处理。`session.todo` 是 Session 当前快照,读取结果必须与请求生命周期
关联;不在每轮开始时无条件读旧快照充当新清单,也不轮询代替事件。
CN 的提取位置在 `continue-host-adapter.ts`:宿主产生工具事件、HTTP 状态合并及输入
校验均存在 4,000 字符展示限制,不能等到 `continue-runtime.ts` 收到截断字符串后再解析。
宿主应在最早的原始参数层提取 `checklist`,按工具调用 ID 保留候选,结合最终成功状态
提交;成功帧省略参数时使用同次调用的候选,失败不得提交。普通工具事件继续沿现有路径
流转。超出 4,000 字符的清单需要独立验证完整性,但不扩大所有普通工具输入的展示上限。
Markdown 解析只适用于已确认的 CN `Checklist.checklist`,不扫描助手正文或其他工具
输出。空数组在统一事件中明确表示清除;CN 空字符串或仅空白的字符串表示清空。
解析规则见上文;无效输入保留旧清单,不能误当清空。
## 2. 统一事件与消息归属
The shared contract is `src/shared/runtime-checklist.ts`: `runtimeChecklistSchema`
and `RuntimeChecklist { source: 'opencode' | 'continue', items }`. Each item has
`content`, `status: pending | in_progress | completed | cancelled`, and optional
`priority: low | medium | high`. `AgentEvent` carries
`{ type: 'checklist', requestId, checklist }`; conversation messages carry optional
`runtimeChecklist`. Empty arrays are valid replacements. Existing IPC event
transport forwards them without a separate channel or tool-input parsing.
本地 Renderer 按请求映射更新助手消息并沿既有保存入口落库;远程 Main 使用既有事务
投影更新消息。顶部读取所属请求消息的最新有效状态。
同一请求若同时观察到原生清单事件与其工具记录,只以前者更新清单,后者保留活动;不能
提交两次替换。原生父 Session、请求开始/结束边界需共同校验,子 Session 或旧请求事件
不得通过当前活动请求映射到新轮。
The existing message metadata serializer and every database message reconstruction
retain `runtimeChecklist`, including remote event projection and terminalization.
No SQLite schema migration or separate checklist store is needed.
历史清单与所属助手消息一起保存。
远端沿用事件提交、消息投影和 ACK 的既有事务及去重规则;相关存储边界见
[执行记录存储](./execution-history-storage.md)。实施前核对本地会话与远程任务实际落库
字段,选择能复用的最小表示。不新建清单数据库、独立快照系统、恢复日志或持久化服务。
旧消息没有结构化清单时保持缺省,不从已截断工具字符串或状态文案补造历史。
## 3. 远程 Agent 与 ACP 复用
远程传递复用以下实现:
1. `AgentAcpConnection` 按原生 `sessionId` 分派 `sessionUpdate`;
`AgentOwnedAcpPrompt` 校验 Session 后,以 `bindingId/operationId` 将通知写入
既有 semantic transcript。
2. `ProtocolRemoteRuntimeChannel` 已有 transcript 读取/ACK、身份核对,以及 ACP
channel 恢复与重放;`AcpRemoteRuntime` 消费 `session-update` 并转换桌面事件。
3. OC 插件转发原生 `todo.updated`,CN 宿主将成功清单写入 `_meta.goodbuddyChecklist`;
`AcpRemoteRuntime` 校验归属和结构后产生统一清单事件。标准 OC `plan.entries` 也有
映射,但 OC 1.18.29 实测走插件事件。Markdown、URI 和普通状态文字不生成清单。
优先复用上述通知和 transcript,不另建远端清单通道。对实际 OC/CN 部署确认过的
`plan.entries`,或已确认等价的 `plan_update` items,校验条目字段和状态后转成统一
清单事件。普通 `status`、plan markdown/URI、`plan_removed` 文案以及泛化工具名均不
触发清单更新;在没有确认对应原生清单身份前,`plan_removed` 也不能清空顶部。
若远程原生输出走精确清单工具,须确认原始参数、调用身份与成功状态确实贯穿 Agent,
再按同一提取规则接入。是否能直接复用原生 entries,或需补充最小 Agent 适配,取决于
真实格式证据,不能根据 ACP 类型定义推断 OC/CN 一定会发送。ACP 是协议,Runtime 身份
仍为 OC 或 CN。
受管安装、校验、项目创建与聊天选择已接入 CN;Agent 在 Host 运行固定 CN HTTP 宿主,
复用本地适配器和已有模型桥、请求归属、取消及恢复。开发包与 Linux 验证、联合分发和
完整桌面验收的剩余范围见[远程记录](../remote-host/runtime-checklist-validation.md)。
重连按[既有 ACP 与断线规则](../remote-host/technical-design.md#acp-与断线)执行。
已接受 Prompt 使用原 binding/operation 恢复,清单从已提交消息和其后的既有事件恢复;
重复事件不重复展示。连接丢失不表示条目完成,不为恢复清单重新发送 Prompt、模型请求或
工具调用,也不新增独立重连机制。
## 4. 顶部布局
`ConversationTaskStrip` 与 `RuntimeChecklistStrip` 共用顶部容器,任务在上、清单在下,
各自展开和折叠。没有 Task 时清单仍可显示;远程会话也保留清单入口。总高度不超过
45vh,各自展开内容不超过 20vh 并内部滚动,标题不收缩。清单内容区可用键盘聚焦和滚动。
状态文字与条目首行按文字基线对齐,状态图标单独居中。CN 不推断进行中或优先级。
新轮先选择本轮消息,再读取清单,不回溯寻找旧轮非空清单。后台更新保持展开状态和焦点。
## 5. 实施与验收
| 阶段 | 必须完成的工作 | 对应需求 |
| --- | --- | --- |
| CL-P1 | 原始层提取、严格格式校验、统一事件及请求归属;保留工具活动 | FR-CL1–FR-CL3 |
| CL-P2 | 消息投影和既有存储恢复、顶部只读布局、空数组清除及终态展示 | FR-CL2–FR-CL4 |
| CL-P3 | 确认远程 OC 原生格式,补齐远程 CN 真实部署与事件接入,复用 Agent 传递和恢复 | FR-CL1、FR-CL4 |
| CL-P4 | 本地与共享 Linux 真实 Host 逐项验收,记录版本、结果、调用口径与剩余阻塞 | FR-CL1–FR-CL4 |
以下为产品验收范围;已执行项和未覆盖项分别记录在进度与远程验证记录中:
- 本地 OC/CN 各经桌面正常发送路径产生并连续修改清单,检查新增、修改、重排、删除、
重复文本、有效清空及超过 4,000 字符的完整内容;CN 成功才替换,失败保留前值。
- 同会话连续两轮、并行会话、切换 Runtime/项目/执行空间、旧轮迟到事件和子会话事件
不串数据;完成、失败、取消不改写条目状态。默认 DS、任意 status/Markdown/list
工具不生成伪清单,工具活动仍可查。
- 本地会话重新打开及应用重启恢复历史;顶部折叠、键盘操作、浅深主题、窄窗口和 200%
缩放符合 PRD,保存与重放后的空清单不会恢复旧内容。
- 在共享 Linux x64 真实 Host 使用开发中的 Agent 与实际部署的 OC/CN,分别从远程项目
完成原生清单更新、取消、断线续接及历史恢复。沿用
[Agent 开发验证规则](../../../AGENTS.md#goodbuddy-agent-development-validation),
LAN/VPN 是同一机器的两条路径,不能计作两台 Host。
- 逐个记录 OC/CN 的安装、启动、模型请求、清单传递、顶部显示、取消和重连结果;基础
Runtime 成功不等于清单通过。任一步未执行写“待验证”,被外部条件阻塞则记录原因,
不以 mock、协议 fixture 或本地成功代替远程验收。
@@ -0,0 +1,183 @@
# Runtime 交互边界
本文定义当前原生交互的适配范围,不增加工作模式或第二层 Execute 审批。
| Runtime | 权限确认 | 业务问答 |
| --- | --- | --- |
| 本机 GoodBuddy 管理的 OpenCode | 内部默认配置允许权限;每个 Ask 请求仍使用 deny-all 与显式只读能力。Execute 自动回复属于当前请求的父、子孙会话权限事件,不处理其他并行请求 | 通过 `question.asked` 转交父、子孙会话问题,支持原生单选、多选、yes/no、自由文本及跳过;使用公开 ID 回传 `question.reply/reject` |
| 托管 SSH OpenCode | Agent 按已接受 Prompt 的 Ask/Execute 模式处理 ACP 权限,规则见远程主机技术设计 | 当前 Agent 插件转交原生父、子孙会话 `question.asked`;前台使用同一问答队列,回答和跳过经认证 Agent 控制通道回传,已知 Task 关联显示子任务归属 |
| Continue | Execute 启动传入 `--auto`,剩余原生权限请求按当前模式自动答复 | 对接 `1.5.47` QuizService 的扁平 `pendingQuestion`,其中 `question` 是字符串。选项回答与自定义文本分别回传正确的 `isCustomAnswer`;跳过发送拒绝回答的文本 |
| DeepSeek Harness | 固定 Host 的 Execute 文件与命令工具不询问目录权限;若收到 ACP 权限请求,活跃 Execute 直接选一次性允许,缺少该选项时使用 Runtime 提供的允许选项,Ask 拒绝,不等待额外 authorizer | 固定 Host 当前未提供通用原生业务问答服务,不把普通模型正文误识别成待答协议。模型以普通回复提问时,用户通过下一条消息回答 |
托管 SSH OpenCode 的权限、原生问题事件与回复链路由
[远程主机技术设计](../remote-host/technical-design.md)定义。该远端业务问答基线需要
Desktop `0.13.0`、Agent `0.11.24` 和新启动的托管 Runtime;请先升级 Desktop,再更新
Host 环境。2026-09-14 的已发布 Desktop `0.13.2` / Agent `0.11.25` 已包含此前问答修复,
但不包含本文末尾尚未发布的进程复用改动。任意第三方 ACP 服务及旧 Agent 不因此获得通用问答接口。
## 运行与失败
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
- 前台同一请求的待答问题按公开 `questionId` 去重,按到达顺序保存在消息的
`pendingQuestions` 中。每次显示队首表单;有多个待答请求时显示数量。后续提问和重复
事件不替换当前表单或清空输入,回答或跳过成功后只移除对应 ID,再显示下一张表单。
提交失败保留队列、输入和重试入口,队列未清空时继续显示“等待你的回答”。
- 本机与托管 SSH OpenCode 在提问到达前已收到原生 Task 的 `metadata.sessionId` 时,将精确匹配的
子会话关联为可选 `childTaskId`。问答卡片显示该任务名称;对应 Task 卡片显示
“等待你的回答”,展开后可点击“前往问答表单”滚动并聚焦当前队首表单,仍按顺序回答。
没有已知关联的子孙会话问题照常排队,不猜测子任务归属,也不从工具名或进度文本构造表单。
- 回答或跳过成功后,会话保留只读的“问题与回答”,显示原问题及对应的选项答案、
自定义文本或“已跳过”。同一任务的多轮问答逐次保留,随本地会话保存,重新打开后仍可查看。
提交失败时保留输入与重试入口,不生成已回答记录;取消未回答的问题不记为主动跳过。
- 新消息在提问到达时,将只含 `questionId` 的问题位置块插入当时的消息块序列;
待答和回答后的卡片使用同一位置,后续正文、工具输出不再排到问题前面。重复问题不追加
或移动位置块。位置块不保存待答表单;旧会话没有位置块时保留原有问答展示,不推测或
回填历史顺序。该变更复用既有消息元数据,不升级数据库 schema。
- 提交响应晚于任务结束时仍保存已提交的问答,但不覆盖完成或失败状态;晚于下一轮提问时
不清除新的待答卡片。旧版会话未保存的问题与答案无法从“已提交”状态文案恢复。
- 待答队列仅保存在当前桌面会话内存中,不写入持久会话快照。刷新仍在运行的消息时保留整个
本地队列;完成、失败、取消事件或持久化终态快照清除整个队列。迟到的提交结果可以保存
已回答记录,但不会恢复表单或覆盖终态。本机 Runtime 不恢复重启前未提交的问题;托管 SSH
重新附加同一存活 Agent Prompt 时,由 Agent 当前待答列表补发问题,已回答的历史事件不补发。
- 未发布的进程复用源码在 Desktop 重启后由 Main 并行附加同项目的存活任务,将待答
列表和 Stop 所需的请求 ID 投影到实时 `activeRequest`,不保存该字段或重新发送任务。
回答/跳过成功由 Main 写回原远程助理消息的既有问答历史,Renderer 不覆盖远程正文或终态。
Main 在前台与恢复请求收到实时问题时立即保存位置块;实时待答快照没有语义事件来源,
不伪造来源或追加 transcript。带来源的提问事件仍通过既有事务投影并按问题 ID 去重。
Runtime 设置、定制和能力更新让旧请求在原配置上结束,已有问题仍可回答;新请求使用
新配置。专家团队例外:既有专家调度器在设置替换时取消子任务,团队父请求也随之停止,
不使用替换后的模型汇总旧结果。详细实现见
[恢复与终态投影](./runtime-process-reuse-technical-design.md#63-恢复与终态投影)。
- 后台任务收到结构化提问时,通过现有 Main 路径明确失败并提示改为前台对话,不永久等待。
- 请求结束或取消后清除对应待答映射;过期回答明确报错,不转交给后来的请求。
- 任意终端程序的密码、OAuth 登录、许可证确认或 stdin 提示不自动回复 `yes`。这类程序应
使用其非交互参数,或由用户在终端完成;不能把权限确认和业务决策混为一谈。Shell
工具保留其现有取消与超时,不新增日志解析或通用交互代理。
## 托管 Runtime 的并行与输出
子任务完整过程的显示与数据库差量存储相互独立,本机和托管 SSH 共用桌面存储层;
旧库升级与空间回收规则见[执行记录存储与升级回收](./execution-history-storage.md)。
- 本机和 Agent 管理的 OpenCode 启动配置都显式设置 `snapshot: false`。GoodBuddy 不消费
原生自动 Git 快照、快照差异摘要或快照撤销;关闭这条内部路径,避免大行级差异的同步
计算阻塞同一原生进程中的其他会话。
- 文件工具、Shell、原生子代理、工作区 Git 状态和单文件差异保持可用;不新增全局会话
队列、请求重放或总执行截止时间。外部 OpenCode Server 的配置不由 GoodBuddy 改写,
已运行的旧原生进程也不会热更新配置。
- Continue 使用 HTTP 传递响应和状态;Electron UtilityProcess 启动后持续排空不用展示的
stdout,避免控制台输出填满管道后阻塞宿主。stderr 的有界错误处理、请求取消与进程
回收保持不变,不把诊断输出复制进聊天或持久日志。
- 远端最终启动配置由[远程主机技术设计](../remote-host/technical-design.md)定义,
模型桥生成最终配置时同样保留上述快照策略。
## 工具操作摘要
工具记录沿用 [UI 设计系统](../../../UI-DESIGN.md)的折叠布局。Continue、DeepSeek Harness
和直连模型共用 `toolOperationSummary`:优先使用有效原生标题,否则从参数提取命令、
文件路径、搜索条件或 URL,合并空白后最多显示 240 字符。无法识别时保留原有工具名
回退规则;文件正文、整份参数 JSON 不进入标题,参数详情仍可展开查看。
Continue 在宿主截断输入前提取摘要,流式完成与失败事件使用合并后的输入和摘要。
DeepSeek Harness 按调用 ID 保留名称、输入和摘要,结束事件省略输入时仍能显示操作内容。
直连模型从原始参数生成一次摘要,在待执行、执行中和完成时复用;审批失败、拒绝和
执行失败、刷新重试仍保留已有提示。
本机 OpenCode 沿用原生操作标题。远端 ACP 独立使用原生标题作为记录名称,不经过上述
本地转换,本次没有修改 Agent、远程桥或协议。
## 消息底部请求状态
- 状态属于当前请求,不与顶部 Runtime 的连接中、就绪、不可用混为一谈。提交后先显示
“正在准备请求”;没有更具体的活动事件时显示“等待后续进展”,不声称模型正在思考。
- 本机 OpenCode 只把当前父会话的 `session.status.retry` 映射为等待重试,显示原生
`attempt`、计划重试时间和有界脱敏原因;后续 `busy` 才显示已开始重试。其他会话的
状态不串入当前消息,不猜测总重试次数,不把 SDK 事件流重连当作模型重试。
- 显示优先级为待回答、待确认、进行中的上下文压缩、明确的请求状态、待执行或执行中的
工具、未完成的子任务、等待后续进展。压缩沿用已有活动块,不重复增加底部提示。
正文、推理或工具进展会清除旧状态;子任务更新不覆盖父请求的重试提示。
- 完成、失败、取消后保留真实终态,迟到的状态事件不能把请求恢复成处理中。底部状态点
为静态,不凭请求尚未结束暗示仍有进展;会话列表闪点和发送、停止按钮保持原有行为。
持久化快照中的同一条消息仍为生成中时,也不能覆盖本地已收到的终态,即使快照的
会话更新时间更晚;否则连续发送或刷新会让已结束的请求重新显示停止按钮。
- Continue、DeepSeek Harness 和远端 ACP 保留其已接入的处理与工具事件,不伪造未接入
的原生重试状态。直连模型的重试等待与请求开始文案见
[直连模型 UI 设计](../direct-model-agent/ui-design.md#6-错误与通知);本次不改变任何
Runtime 的重试上限、请求重放或取消策略。
实现与真实 Runtime 验证证据见[进度记录](./progress.md)。
## Runtime 执行清单
本节为未实现的状态设计,对应 [FR-CL1–FR-CL4、US-CL1–US-CL5](./prd.md#131-runtime-执行清单)。
原始格式和传递边界由[技术设计](./runtime-checklist-technical-design.md)维护。
清单属于一次请求及其助手消息。沿用项目、执行空间、Conversation、Runtime 选择和
原生 Session 到请求的既有映射;远端还需核对 binding/operation。不能仅以原生 Session
或 Conversation 为键更新顶部,因为同一会话会连续执行多轮请求。
| 输入或操作 | 清单与顶部行为 |
| --- | --- |
| 本轮尚无有效清单 | 隐藏顶部清单,不复制上一轮内容 |
| 收到本轮有效完整清单 | 按原生顺序整体替换,包括修改、删除、重排和重复文本;不按条目文本合并 |
| 收到有效空数组 | 记录明确清空并隐藏;空数组与缺少清单字段不同 |
| CN 调用进行中 | 候选只按该请求的调用 ID 暂存;成功后才提交完整替换 |
| CN 调用失败、取消或格式无效 | 丢弃该候选,保留上次有效清单,工具记录保留实际结果 |
| 请求完成、失败、取消或中断 | 冻结最新已提交清单,并显示请求真实结果;不推断条目完成,迟到输入不改写已结算清单 |
| 同一会话开始下一轮 | 顶部转向新请求,上一轮清单留在原消息;旧轮事件不得归入新轮 |
| 切换会话、项目、执行空间或 Runtime | 只读取目标归属的数据;原请求在原归属结算,不把原清单标为新 Runtime 的进度 |
| 查看历史或重连 | 从原消息及既有事件恢复;按原请求和远程来源顺序去重,不能用最新原生 Session 快照覆盖历史轮次 |
条目状态支持 `pending`、`in_progress`、`completed`、`cancelled`。OC 原生提供的 priority 保留;
CN 未勾选项映射为 `pending`,已勾选项为 `completed`,不推断 `in_progress` 或 priority。
OC 无稳定 item ID,视图索引仅服务本次完整列表渲染,不生成跨更新的条目身份。
清单内容不创建、调度或完成产品 Task,也不改变底部请求状态。清单更新与工具活动可以
同时保留,但顶部计数只来自已确认的结构化清单,不能反向解析普通状态文本、任意
Markdown 或名称含 `list` 的工具。子会话清单不覆盖父请求顶部。
以上产品状态规则已明确;远程 CN 接入、原生格式确认及历史字段选择仍属技术待办,见
[进度记录](./progress.md#2026-09-15-runtime-执行清单设计未实现)。
## Runtime 资源生命周期
本节对应 [FR-RR1–FR-RR6、US-RR1–US-RR4](./prd.md#141-runtime-资源复用),
定义进程复用的行为规则。当前源码已接入本机共享 owner 和 Agent 共享 ACP;
完整验收状态以[进度记录](./progress.md#2026-09-14-runtime-进程复用实施中)为准,不表示已发布。
具体 owner、兼容键和协议实现见[技术设计](./runtime-process-reuse-technical-design.md)。
资源有三个独立维度:进程是否启动/退役,会话是否仍持有原生上下文,请求是否在途。
“任务完成”“当前没有选中该项目”“工作栏关闭”均不能直接推导进程可以销毁。
| 触发 | 请求与会话行为 | 进程行为 | 对应需求/场景 |
| --- | --- | --- | --- |
| 被动状态读取 | 不取得新执行会话,已有状态可读取 | 不冷启动,不延长无会话 owner 的空闲期限;不为探测替换服务中的进程 | RR2 / US-RR1 |
| 显式测试/原生清单 | 取得临时检查引用;清单带明确工作区 | 与执行共用启动 Promise 和 owner;结束只释放检查引用 | RR1、RR2 / US-RR1 |
| 首次或并行 Prompt | 取得对应 execution space + conversation 的绑定 | 同兼容组复用,非兼容组分别启动 | RR1、RR3 / US-RR1、RR2 |
| 普通请求完成 | 释放请求订阅、临时 token、问题和 callback,保留可续聊 Session | 不因为一个请求完成关闭整个进程 | RR3、RR4 / US-RR3 |
| 取消一个请求 | 取消其模型/工具调用并结算,其他会话继续 | 不以 kill 共享进程代替正常会话取消 | RR3、RR4 / US-RR2 |
| 关闭面板/切换项目 | 停止非必要 UI 推送;不取消后台任务或抹掉会话所有权 | 不触发隐式关闭 | RR4 / US-RR3 |
| 显式释放/删除会话 | 释放该 Session 与会话工具资源;删除历史仍只由原有明确删除操作决定 | 其他引用仍在则保留 | RR4 / US-RR3 |
| 无会话、无请求、无检查引用 | 不存在需要保留的原生上下文 | 进入短空闲等待,再关闭;新使用者到来取消等待 | RR4 / US-RR1、RR3 |
| 配置替换 | 已接受请求使用旧配置结算,新请求使用新配置 | 旧 owner 排空后退役;不积累只被历史 ID 持有的旧配置进程 | RR1、RR4 / US-RR4 |
| 进程故障/不可响应 | 只给该 owner 下受影响请求明确中断;未知结果不自动重放 | 有界终止失效 owner,不伪造逐会话故障隔离 | RR3、RR4 / US-RR2、RR4 |
| SSH detach | 保留 Agent 已接受 Prompt 及既有恢复归属 | 关闭传输不关闭 Agent 或活动 Runtime | RR5 / US-RR4 |
表中 RRn 是 PRD 稳定需求 ID `FR-RRn` 的简写。
不变量:
1. 一条请求始终使用接受时的 execution space、工作模式、配置和归属;不能读取后来
切换的 UI 选择,也不以进程 cwd 表示所有会话的 cwd。
2. 同一会话串行,跨会话不增加全局 Prompt 队列。权限、子代理、问题、用量和最终清理
必须沿同一 Session/请求归属,不因共享进程放宽 Ask 或限制 Execute。
3. owner 的退出条件依据所有使用者,不依据某个 adapter 的空闲状态。
未启动对象的 dispose 不产生冷启动;一个等待者取消不取消其他等待者的初始化。
4. 正常会话仍持有原生上下文时,不用任意 TTL 清掉它来声称“空闲内存已回收”。
无会话 owner 的短空闲等待为 60 秒;
不增加用户配置项。它是进程引用回收,不是聊天历史保留期限。
5. 持久历史不因请求结束、进程 idle、配置变化或测试 cleanup 被删除。
原生进程确实重启后的续聊沿现有历史重建规则,不新增第二套持久会话快照。
已明确普通并行、取消、释放、配置替换及断线行为。各项实际通过的场景及尚未完成的
组合/性能验证以[进度记录](./progress.md#2026-09-14-runtime-进程复用实施中)为准。
@@ -0,0 +1,392 @@
# Runtime 进程复用技术设计
## 1. 状态与结论
| 项目 | 内容 |
| --- | --- |
| 状态 | 已接入本机与 Agent 生产源码,Windows / Linux x64 验证结果及边界已记录,未发布 |
| 日期 | 2026-09-15 |
| 调研源码 | `de14a6157a6c34f1e3ef22619eca374a50a83706`;相关 Runtime 与 Desktop 0.13.2 一致 |
| 需求 | [工作栏 PRD §14.1,FR-RR1–FR-RR6](./prd.md#141-runtime-资源复用) |
| 行为规则 | [Runtime 资源生命周期](./runtime-interactions.md#runtime-资源生命周期) |
| 验证事实 | [实施与验证记录](./progress.md#2026-09-14-runtime-进程复用实施中),原始调研记录保留在同页 |
**修复对象是进程所有权过细,不是工具太多,也不是历史记录应该删除。** 本机 OpenCode
和 DeepSeek Harness 应在兼容启动配置内共享重型进程,工作区与会话上下文仍各自独立。
远端 OpenCode 必须同时调整 Agent 进程所有权、ACP 连接和模型桥,不能把桌面修复当作远端
修复。Continue 的锁定版本是单会话 Host,不做未经验证的常驻进程池;直连模型没有独立
模型进程,应检查对象、历史和工具资源的生命周期。
“一个进程”指一组兼容启动配置对应一个 Runtime Server/Host,不是把不同 Runtime、
不同 SSH 账号、不同插件组合强行塞入一个进程。正常工具命令产生的短期子进程不属于重复
Server,也不能通过关闭 Shell、子代理或 MCP 来达成进程数指标。
## 2. 修复前证据与缺陷
### 2.1 已测现象和解释边界
Windows 已安装 0.13.2 的只读采样发现两个属于同一 App 的 OpenCode Server:一个原生
数据库为 0 Session / 0 Message,约 527 MiB Private Commit;另一个已有会话并执行请求。
两个 Server 的启动工作区不同。源码能解释“清单查询也可启动按工作区缓存的 Runtime”,
但没有取得能独立证明空 Server 最初由哪一个 UI 操作启动的调用链记录。
该缺陷不等于整个 App 内存的唯一来源。五分钟活跃采样的 App 相关进程 Private Commit
约 3.15–3.78 GiB,CPU 平均占整机约 3.15%;后续一分钟包含新任务,不是空闲回收试验。
数据库五分钟增长约 2.72 MiB,新增子代理负载约 1.49 MiB,重复工具 upsert 为 0。
schema 35 的存储修复与本方案独立,不能回退或删历史来改善指标。
同次源数据检查还发现最近 100 个会话的消息 metadata 合计约 107.75 MiB,
`listConversations()` 会读入这些会话的全部消息详情。它是 Renderer 大对象加载的明确
检查方向,不是堆快照已经证明的泄漏。进程复用不能代替历史按需加载的独立性能工作。
### 2.2 调研基线的源码归属
| 位置 | 已确认行为 | 修复含义 |
| --- | --- | --- |
| [SelectedRuntimeManager](../../../src/main/agent/selected-runtime-manager.ts) | 缓存键包含原始 selection 与 execution-space identity;默认 8 个缓存、16 个保留实例;只在容量压力下淘汰 | 轻量工作区适配器和重型进程不能继续按同一键、同一数量计费 |
| [AgentRuntimeController](../../../src/main/agent/runtime-controller.ts) | `canRetire` 同时要求无请求、无 owned conversation;普通完成不移除会话所有权 | 不能把“已完成请求”和“已释放原生会话”混为一谈 |
| [本机 OpenCode](../../../src/main/agent/opencode-runtime.ts) | 对象私有 Server/client/初始化 Promise;已有多会话映射;多个入口使用固定 `defaultWorkspace` | 仅删除 manager 的 workspace 键会把后续操作送进错误目录 |
| [DeepSeek Runtime](../../../src/main/agent/deepseek-harness-runtime.ts) | 每对象一个 Utility Host;`getStatus()` 调用 `getState()` 启动 Host | manager 的临时状态实例会额外启动、探测并销毁 Host |
| [Harness Host](../../../src/main/deepseek-harness-host.ts)、[控制面](../../../src/main/agent/goodbuddy-harness-control-plane.ts) | 根 Provider 有 cwd 默认值;控制面拒绝与 Host workspace 不同的 `newSession.cwd` | 限制主要在 GoodBuddy 创建入口,不是原生工具缺少会话 cwd |
| [Continue Host Adapter](../../../src/main/agent/continue-host-adapter.ts) | 每次 run 启动临时 Host,最终 `/exit` 与进程树回收;准备结果已在 adapter 内缓存 | 不重复添加已有缓存;研究跨 adapter 重复准备,不直接共享活动 Host |
| [直连模型](../../../src/main/agent/model-runtime.ts) | Main 内 HTTP;历史 Map 按 50 个会话限制,其他归属及工具资源依赖 release/dispose | 没有可合并的常驻模型子进程,按对象和资源检查 |
| [远端 ACP](../../../src/main/agent/acp-remote-runtime.ts)、[Agent backend](../../../src/agent-daemon/runtime-acp-backend.ts) | 会话取得 binding/channel;backend 按 binding 拥有进程并拒绝跨 binding 复用 process identity | 必须拆开会话绑定与进程所有权 |
| [Agent-owned Prompt](../../../src/agent-daemon/agent-owned-acp-prompt.ts)、[模型桥](../../../src/agent-daemon/model-bridge-helper.ts) | owner 一个 ACP connection/session/当前 operation;helper 固定模型和工作模式并串行 dispatch | 共享进程还要求会话路由、请求归属和取消隔离 |
原始 selection 的 `auto`、默认 profile 和显式 profile 可能解析为同一有效配置。当前
已确认“先按原始 selection 建键、后解析配置”的顺序,但尚未证明全部 UI 调用会实际生成
等价重复实例。实施时从 [runtime-selection](../../../src/main/agent/runtime-selection.ts)
和调用者验证,不把可能性记为另一次生产复现。
### 2.3 锁定依赖与协议基础
以仓库 lock 和本机已安装代码为准,不用最新网页替代锁定版本:
| 依赖 | 基线 | 本次核验 |
| --- | --- | --- |
| OpenCode binary / SDK / plugin | 1.18.29 | HTTP API 接受 directory;ACP 接受多个 Session;`chat.headers` 有 sessionID |
| ACP SDK | 0.25.1 | 一条连接关联多个 request ID,session notification/permission 带 sessionId |
| DeepSeek Harness 包族 | 0.1.2-rc.1 | Agent factory/scoped context、Session header.cwd、真实文件与 Shell 工具 |
| Cordis | 4.0.2 | Host 根组件与 Agent scope 的生命周期 |
| Continue CLI | 1.5.47 | 实际受管补丁入口的单 `M.session`、QuizService 和权限状态 |
已通过的三个隔离探针:
1. **OpenCode HTTP**:调用当前生产 Server launcher,只有一次 spawn;同一个 SDK client
在两个目录创建四个 Session,读取各自的同名文件;删除一个 Session 后其余三个仍可取回。
没有调用 `Runtime.run`、模型或活动取消。
2. **DeepSeek 工具**:当前 Host composition 和真实 Agent factory 在同一 Node 测试进程中
创建两个不同 cwd 的原生 Agent;并行执行真正的 `read`、`write` 和 PowerShell,
同名相对路径各自命中正确目录。另确认生产控制面仍拒绝第二工作区。没有启动 Electron
Utility、ACP Prompt 或模型流,不能据此声称生产多工作区已经可用。
3. **原生 ACP 与模型归属**:Windows 上一个 1.18.29 `opencode acp` 进程、一条 ACP
connection、两个工作区 Session;分别选模型并发出并行 Prompt。本机确定性 HTTP
服务收到两次请求,原生 `chat.headers` 插件传来的 sessionID 分别匹配两个 Session。
不经过已发布 Agent backend/helper,没有测试真实工具、子代理、Linux 或断线恢复。
三个探针真实模型调用均为 **0**,其中第三个有 **2 次本机合成提供方请求**。
它们证明方案有原生基础,不替代后文的真实开发任务验收。
特别纠正:`dsh-tool-fs` 将 `exec.agent.session.header.cwd` 传给 `fs.resolve`;
`dsh-tool-bash` / `dsh-tool-pwsh` 也从 Session cwd 补全相对 workdir。
`dsh-fs-local` / 本地 Shell 的根 cwd 是非会话调用的默认值,不是工作区隔离边界。
不需要为这个已具备的能力新建一套“每工作区 Filesystem/Shell Provider”。
官方背景资料:[OpenCode Server](https://opencode.ai/docs/server/)、
[DeepSeek Harness](https://www.deepseek.com/harness/en/)、
[Continue CLI](https://docs.continue.dev/cli/quickstart)。
网页只用于核对产品入口;上述版本结论来自本地锁定代码和探针。
## 3. 选定的结构
### 3.1 两层对象,不建立通用进程池框架
```text
SelectedRuntimeManager
-> 轻量 Runtime adapter(有效 selection + execution space)
-> OpenCode process owner / Harness Host owner(兼容启动配置)
-> Session binding(execution space + conversationId)
-> 当前 requestId / 原生 sessionId / 工作目录
```
- 保留 Main 已解析、校验的 execution space;adapter 每次将其显式交给共享 owner。
目录不是 Renderer 新增的任意启动参数,也不依赖 `process.chdir` 或进程级可变 cwd。
- owner 持有唯一启动 Promise、client/connection、子进程、会话映射与关闭逻辑。两个
工作区同时冷启动时复用同一 Promise;一个调用者取消等待不取消其他使用者的启动。
- adapter 的 `dispose` 只释放其引用,不直接关闭同组进程。会话映射由 owner 持有,避免
adapter 缓存淘汰丢掉原生续接。会话操作通过 binding 查目录,不从“当前选中项目”反查。
- 设置重建沿用现有 reset/drain:新请求取得新配置,旧请求持有启动时的配置直到结算。
旧配置不再有在途使用者后释放旧绑定和进程,不能被历史会话 ID 永久钉住。
IPC 的 Runtime 设置、定制及能力更新不再主动取消全部请求或清空审批;controller
退役仍允许回答其已拥有的问题,但拒绝新请求和未知问题。
既有专家调度器仍在配置替换时取消专家子任务,团队父请求一起停止,防止用新模型
汇总旧 generation 的部分结果;不为该单独生命周期新建通用退役框架。
- 8/16 的旧上限不应继续限制轻量项目 adapter 数量。若保留重型 owner 的容量保护,
应统计实际进程配置组及退役进程;不靠提高常数掩盖“一项目一进程”。
- 只用进程内 Map、现有 controller、Promise 和普通定时器。会话历史仍以现有 SQLite /
Agent transcript 为准,不增加数据库表、迁移、持久化池快照、恢复日志或新配额。
### 3.2 兼容键与上下文
先通过 `applyRuntimeSelection` 等现有解析取得有效配置,再生成进程键;公开的 selection
仍按现有语义保存,不能把“跟随默认”永久改写成某个显式 profile。
| 范围 | 参与进程兼容性 | 留在会话/请求 |
| --- | --- | --- |
| 本机 OpenCode | 本机账号、二进制身份、有效模型连接/配置、启动环境、原生配置和插件组合 | 目录、conversation/session、Prompt、模式、当前权限和问答、请求级 MCP/知识 token |
| 本机 Harness | Host/依赖版本、有效模型连接、图片能力、凭据引用、启动环境、启用插件及配置、启动 Skill 集合 | cwd、conversation/session、请求模式、Main Web/MCP 代理与用量归属 |
| SSH OpenCode | 已有 Host/SSH 身份、Agent/Runtime installation identity、模型协议/连接、原生插件与启动配置 | Workspace、binding/session、operation、请求凭据和账本归属 |
第一轮只合并同一有效模型连接及等价默认选择,不顺便支持任意不同模型连接共享:
本机 OpenCode 配置使用固定 provider ID,Harness 用量也读取 Host config,跨 profile
混用会引入另一项真实风险。不同 profile ID 不通过比较或序列化明文 API Key 来合并;
密钥、Header 和插件配置更新通过现有设置失效路径使旧 owner 退役。
调研基线的 SSH launch profile 把 workMode 写入原生启动配置。当前实现把工作模式
放入 Session/当前 Prompt 路由,并设置原生 Session 工具规则,详见 §6.1;
不能仅从兼容键中删除 workMode,也不向用户增加逐工具审批。
## 4. 本机 OpenCode
在 [opencode-runtime.ts](../../../src/main/agent/opencode-runtime.ts) 中抽出重型 owner,
由 [create-runtime](../../../src/main/agent/create-runtime.ts) 的 Main composition 复用。
保留工作区 adapter,逐项替换隐式 `options.defaultWorkspace`:
1. create/get/delete Session、Prompt、历史初始化和压缩。
2. 原生能力、工具、文件、Skill、MCP 查询的 directory。
3. SSE 订阅、父子 Session 归属、待答 question 的 directory 与原生 ID。
4. 权限更新、abort、问题答复/拒绝、MCP 临时注册及最终清理。
会话绑定记录规范化绝对目录及原生 sessionId。同一 conversation 的请求继续串行,
不同 conversation 并行,不引入覆盖整个 Server 的 Prompt 队列。
取消或失败时,每次请求只发送一次会话级 abort,并在该调用结束后才释放会话锁,避免
迟到的 abort 中断下一条请求。abort 使用既有控制请求超时;失败或超时后移除对应绑定,
下一条请求创建新 Session。这也适用于配置外部 OpenCode HTTP Server 的路径;托管 SSH
使用独立的 ACP 取消路径。
现有 `snapshot: false` 必须保留。
`mcpMutationTail` 目前属于 Runtime 对象;共享后同一原生目录的变更必须由同一 owner
串行协调。注册名称、工具可见性、knowledge token 和清理仍按请求区分,不能用一个全局
“当前 MCP 列表”覆盖同目录或跨目录的其他请求。探针未覆盖动态 MCP,这是实施验收项。
第一轮保留现有按请求消费事件流和结束清理的方式,扩展目录及 Session 过滤即可。
只有测出重复 SSE 消费是瓶颈时才另做 owner 级事件分发,不同时引入第二套事件总线。
健康检查也读取 owner 的总 activeRequests,不能从一个空闲 adapter 判断整个 Server
无任务后重启。异常退出使该 owner 的全部绑定失效,明确结算受影响请求,不重放已执行工具。
## 5. DeepSeek Harness
### 5.1 共享 Host 的最小改法
- 将 `HarnessState`、Utility 启动 Promise、ACP connection 和事件路由提升到兼容配置
owner;Runtime adapter 保留请求工作区。
- 每个新 Session 在 Main 解析 `realpath`,Host 校验绝对路径格式,
不再要求它等于 Host 启动工作区;`meta.cwd` 传入已有 Agent factory。
不依赖 Windows 大小写字符串猜测,也不取消路径存在性校验。
- 保留一个本地 Filesystem、Subprocess 和平台 Shell Provider。Session 使用原生 cwd
路由;非 Session 的清单查询显式传入查询工作区,不能回退到第一个项目。
- 保留 `setup(agentCtx)` 注册 Skill 和工具视图、每 Session 的 Ask definition 身份校验、
`sessionId + requestId` 准备及事件路由。Main 的 Web/MCP callback 按对应请求找
ToolProvider,不挂到某个最先创建的 adapter 上。
- 第一轮保持同 profile、图片能力和插件组合共享,`queueUsage` 的模型配置因此仍一致。
给用量加跨会话断言,不借本次改造扩大跨模型 Host 能力。
第三方插件的初始化可能依赖配置中的路径,不能因为内置工具支持 cwd 就声称所有插件
天然多工作区安全。已启用插件组合属于进程配置;对实际产品支持的插件验证它使用
Agent scope/Session cwd。若某插件确实有项目级初始化状态,将该状态显式作为不兼容
启动配置,而不是默认给所有工作区各起一套 Provider 或禁止 Execute 工具。
### 5.2 状态与取消
被动 `getStatus` 改为已有 owner 快照或本地配置/Host 资源检查,不调用 `getState()`。
没有启动过时不能写“握手已验证”,可表示资源已安装、尚未连接。
状态检查不取得执行引用,也不清除或重置无会话 owner 的空闲计时;owner 已回收时,
临时配置检查对象用完即释放,不将其登记成新的执行 owner。
用户显式连接测试与原生清单可启动共享 owner;已有 owner 时只做有界控制检查,最终
释放的是检查引用,不能在 `finally` dispose 掉仍服务其他会话的 Host。
当前取消超时会 terminate 整个 Utility。共享后先取消并释放目标 Session 的 Agent /
工具资源,不因一个请求结算较慢就杀掉仍健康服务其他 Session 的 Host。若真实控制面
已失去响应,保留整个 Host 的有界终止,向受影响会话分别报告中断;不得把它说成
“取消完全不影响其他任务”。同步插件卡死的进程级故障无法在同一 JS 进程中强行隔离,
不为此新增通用恢复框架。普通长工具取消与整 Host 故障必须分开测试。
## 6. SSH OpenCode 与 Agent
本节定义当前源码的共享进程改造;当前已发布版本仍见
[远程技术设计](../remote-host/technical-design.md)。不能删除或绕过其持久 binding、
operation、transcript、ACK 和模型 dispatch 结果不确定性规则。
### 6.1 连接与所有权
将 `RuntimeAcpBackend` 的进程 owner 与 binding 分开:
```text
Agent 当前 Runtime installation + 兼容配置
-> 一个 process owner / 一条 ACP ClientSideConnection
-> sessionId -> binding -> 当前 operation / 原始 Prompt Promise
```
- 一条 stdio 流只交给一套 ACP decoder/connection,不能让几个 ClientSideConnection
竞争读取同一 stdout。消息和 permission 按 sessionId 分派给 binding 的当前 operation。
- `AgentAcpConnection` 统一连接级握手、stdio 解码与退出监听,Prompt
生命周期仍留在 binding。同一 Session 串行,其他 Session 不等待它完成。
- `closeAcpChannel`、取消和 release 只释放所属 binding/Session;以活动 operation
判断共享进程是否空闲,最后一个任务结束即回收,不因历史 binding 保留进程。
待答问题仍占用活动 operation。关闭 Session 时等待原生 `closeSession` 和原始 Prompt
结算,不等待排在 backend 控制队列后面的完成回调。只有原生控制面不响应时才有界
终止共享进程,并向其余请求报告中断,不声称这类故障可以逐 Session 隔离。
- terminal 与物理进程状态分开:取消/失败的 Session 也可返回
`processTree: running`,表示进程继续服务其他 Session,不能伪报进程树为空。
Desktop 与 Agent 须使用配套的新契约;原始字节透传 binding 仍保留独占进程规则。
- 共享 stdio owner 不使用单 Prompt 的累积输入和墙钟期限管理整进程;单次写入、
待写队列、输出背压继续有界,Prompt 输入与截止时间由 binding 管理。
- SSH attach 仍不拥有活动 Prompt,断线不能取消或重发 Agent 已接受的工作。
历史 binding 的语义和原生 sessionId 保留;内存进程索引从当前安装和已有绑定得出,
不增加第二份持久进程恢复账本。
- 同一 PID 的多个 binding 不代表同一权限或同一 operation。Host/installation identity
变化仍使用已有失效和退役规则,不把不同 Host、账号或 Runtime generation 合并。
原生 `chat.message` 在模型请求前设置该 Session 的工具规则:Execute 允许工具;
Ask 只开放原生只读、搜索和读取网络内容的工具,不开放 Shell/编辑/写入。
OpenCode 1.18.29 的 ACP 不转发未注册子 Session 的权限请求,且原生子代理只继承父
Session 的拒绝规则、不继承允许规则,因此子 Session 也按根 Prompt 设置规则,再
保留原生子代理的显式限制。不能只把进程启动配置统一设成 `ask`,否则子代理工具会等待。
### 6.2 模型桥归属
选用锁定 OpenCode 已有的 **`chat.headers` 会话标记**,扩展当前受管
[原生插件](../../../src/agent-daemon/opencode-subagent-plugin.ts),不靠解析正文、猜测
“当前最新 Prompt”或重写用户模型名来路由:
1. 现有 Agent prepare/start 顺序在 helper 的进程内登记 Session/根 Session 与
binding、operation;插件从原生 sessionID 及已建立的父子关系取得精确归属。
2. 在请求生成时固定所属 operation,将有界内部归属 Header 加到 loopback 模型请求。
归属必须覆盖原生子代理、自动/手动压缩,不能等 HTTP 到达后才绑定“目前活动的请求”。
3. helper 检查它是否仍属于已登记的 Prompt,选择对应现有 Agent gateway/凭据上下文。
转发真实提供方前剥离内部 Header;不将其当作密钥或新增认证体系。
4. 当前 helper 的单 socket/全局 dispatchBusy 改成按 operation 隔离的派发状态;
每个 operation 的现有轮次、顺序、字节和结果不确定性规则不变,取消 A 只关闭 A 的
提供方流,不关闭 B 的 socket、response 或待处理请求。
5. Prompt 终态撤销对应映射及凭据;迟到请求不能借用下一轮 operation。保留
`snapshot: false`、关闭不消费的 title 请求和现有父子问题转交。
普通 ACP 双 Session 的 Header 路由已有探针证据;子代理、压缩和终态竞态的实际
验证结果以进度记录为准。若锁定版本某条实际调用路径不触发 hook,先定位那条原生路径,
不得悄悄用全局串行 Prompt 或一个共享凭据槽冒充并行复用。
内部桥字段如需调整,按既有受管 Agent/Runtime 版本配套交付,不移动发布标签,不给
旧 Agent 宣称具备新行为。
### 6.3 恢复与终态投影
- Agent-owned 语义 Prompt 不一定产生原始 ACP journal 行。binary replay 仍校验现有
cursor 边界,但不存在 raw cursor 时不向 journal ACK 0;不虚建记录来满足 replay。
- 同一 Agent 断线恢复后,保留的 Session binding 使用新 transport generation,
Runtime 可以同时接纳新 Session。安装身份已经更换的 attach-only 请求明确失败,
不能无限重试阻塞项目,也不能借此重发旧 Prompt。
- Desktop 重启时,同项目的可恢复任务并行附加,不能等待一个待答 Prompt 结束才附加
第二个。live `question`/`question-resolved` 不属于有 provenance 的 transcript,
使用既有 pending Map,清除时只影响对应 request/question。
- `conversations.list` 将恢复租约的 request/message ID 和待答列表投影为可选
`activeRequest`,Renderer 合并表单并使 Stop 指向真实请求。保存 header/消息时不保存
这个实时字段。回答/跳过成功由 Main 按 task → assistant message 归属更新既有
`answeredQuestions`,不写另一份问题日志,不覆盖流式正文、已完成或失败状态。
- 明确确认的取消使用现有消息/任务终态事务一并结算,避免 Task cancelled 而消息仍
streaming。仍无法确认的 Stop 保持可恢复;连接中断本身不是已取消或已失败的证据。
## 7. Continue、直连模型与相邻内存问题
### 7.1 Continue
保留每请求 Host 的隔离和最终清理。1.5.47 的单 Session、QuizService/权限队列意味着
“一个 HTTP Server”不等于可安全承载多会话;这一例外有锁定源码依据。
`getPreparedHost()` 已缓存本 adapter 的准备 Promise,磁盘也有按内容确认的补丁产物。
先分别测量读取/哈希/补丁准备、Utility 启动、HTTP ready、首个提供方请求和退出耗时。
若确认跨 adapter 重复准备明显,再将只读准备结果与启动 Promise 按已验证的 CLI
identity/补丁版本放到 Main composition 复用;失败仍允许重试。不得复用临时 config、
token、global dir、活动 Session 或权限/问题队列。设置替换及窗口状态刷新也纳入测量。
不在本次给 Continue 加多 Session 补丁或常驻池。将来只有锁定版本真正支持多 Session
且生产桥验证通过,才替换这一实现。
### 7.2 直连模型及历史详情
- 不新增模型 Utility 或进程池。检查 `conversations`、`knownConversationIds`、
summary、ToolProvider 与 DirectModelProcessService 的实际引用与 release 调用链。
- 活动请求、会话可续读的有界输出和数据库历史是不同资源。请求结束回收临时闭包/
callback;会话释放才结束其可续读输出和工具资源,不在工具刚完成时删掉用户可查看结果。
- 当前 50 个会话的 history 限制不是字节上限。先采堆与 retained size,再决定可从
SQLite 重建的历史缓存是否使用字节约束和 LRU,不能先增加一套持久历史副本。
- Renderer 整包 metadata 加载是独立于进程复用的性能工作。现有实现改为列表摘要、
打开会话时读取完整历史,不截断已保存消息;搜索、复制、续聊、后台活动和缓存释放
规则见[会话列表读取与前端保留](./execution-history-storage.md#会话列表读取与前端保留)。
不将这项后续修改的收益计入原进程复用对照结果。
- 旧 Runtime 磁盘缓存可单独评估版本清理策略,不在本次研究或进程 idle 时删除安装、
用户工作区、数据库、备份或历史。
## 8. 内部实施顺序
| 阶段 | 具体工作 | 必须产出的证据 |
| --- | --- | --- |
| P0 | 固化本次原生探针为聚焦回归;补有效 selection/进程计数和状态启动计数 | 修复前能复现重复启动,失败原因不依赖真实用户配置 |
| P1 | Main composition、兼容键、owner 与 adapter 生命周期;本机 OpenCode 全入口目录化 | 真实 UI 两项目四会话同一 Server,文件/工具/问答/取消不串线 |
| P2 | DeepSeek 共享 Utility、会话 cwd、被动状态、请求级代理及取消收敛 | 实际 Utility/ACP 两项目四会话,原生工具与插件验证,状态刷新无额外 Host |
| P3 | Agent process/session owner、ACP 单连接路由、Header 模型桥、权限和终态清理 | 开发中的真实 Linux x64 Host,含多会话工具、断线/重连和结果不确定性 |
| P4 | 全 Runtime 资源回归;Continue 冷启动分解与有证据的准备复用;直连对象释放检查 | 相同输入的前后进程/内存/CPU/磁盘/延迟报告;没有残留测试进程 |
这是依赖顺序,不是发布一半实现的许可。涉及的正常生产 UI 路径、远端专用修复和下节
验收全部完成后,才能称为 Runtime 复用修复。Continue 测量若没有显示重复准备成本,
明确记录不改;不要为凑齐阶段增加代码。历史 UI 分页是后续独立工作。
## 9. 验收与性能比较
### 9.1 必测场景
| 编号 | 对应需求 | 场景与通过条件 |
| --- | --- | --- |
| V1 | FR-RR1、FR-RR2 | 全新 profile 仅查看状态、切换设置,OpenCode/DSH 重型启动次数为 0;已有 Host 时刷新不新建;清单与首个真实请求并发只有一次初始化 |
| V2 | FR-RR1、FR-RR3 | 同一有效模型连接,两个独立项目各两会话同时自然开发,OpenCode/DSH 各一重型进程;各项目同名不同内容文件、命令 cwd、Git/测试结果都正确 |
| V3 | FR-RR3 | 第二轮复用同一批会话;Ask/Execute 混合,Ask 写入被边界拒绝,Execute 不增审批;Skill、MCP、图片能力、用量和业务问答不串归属 |
| V4 | FR-RR3、FR-RR4 | 一会话取消真实长工具/子代理时另外三会话完成并通过独立项目测试;分别检查请求 abort、SSE、问题映射和工具句柄释放 |
| V5 | FR-RR4 | 关闭面板不取消工作;释放一个会话不关闭共享进程;无会话 owner 空闲后退出;App 退出后本机受管进程无残留,远端按已有 detach 规则保留 Agent-owned 工作 |
| V6 | FR-RR1、FR-RR4 | 等价默认/显式选择复用;不同实际连接/插件配置不串用;配置更新期间旧请求完成,旧 generation 不永久积累;超过八个项目不因轻量 adapter 报重型容量满 |
| V7 | FR-RR5 | 当前源码 Desktop → SSH → 当前开发 Agent → 原生 Runtime,两个项目/四会话;断线、重连、取消、重复 ACK/事件、迟到模型请求不重复调用或跨 operation 归账 |
| V8 | FR-RR6 | Continue 并发请求仍各自正确清理;直连 HTTP 不生出额外模型进程;会话 release 后工具/分页输出资源释放;性能采样不靠删除历史或禁用工具达标 |
V2/V4 必须是正常开发任务,模型可自然调用读写、Shell、测试和已支持的原生子代理;
不是要求模型“不用工具”后回复一行。验证产出应用本身可运行、独立测试通过,模型口头
说完成不算完成。每个 Runtime 只验证它已经支持的能力,不伪造 DSH/Continue 子代理。
### 9.2 采样口径
- 使用同一源码变更前后、同一机器、隔离 profile 与等规模历史副本;不改写用户运行库。
覆盖至少三次冷启动、五次热请求,以及三轮多项目任务和至少十分钟**确无新任务**的
空闲观察。分别记录中位数、范围与实际样本数,不从一次采样推断泄漏已消失。
- 记录 Main、Renderer、Runtime 和工具子进程的 Private Commit、工作集/RSS、堆、
CPU、PID/启动次数、会话数、活动请求、SSE/ACP 订阅与残留句柄。Windows 工作集求和
会重复计算共享页,Private Commit 也不等于独占物理 RAM。
- 同时报首轮准备、HTTP/ACP ready、首个模型请求、首次可见输出和总耗时。降低进程数
但把不同会话全局串行化,不能通过验收。
- 数据库/WAL 字节与新增事件负载分开;进程 I/O 包含管道,不能称为物理磁盘读写。
继续检查未变化工具 upsert 为 0,旧历史读取/搜索/展开正常。
- 核心硬指标是每个兼容组一重型进程、无额外被动启动、无跨会话错误、无结束后持续
增加的活动句柄、无重复提供方 dispatch。没有证据前不承诺“固定省 527 MiB”或
“整个 App 降到某个 RAM 上限”;原生 Session 仍保留时必须单独说明其常驻成本。
### 9.3 源码与平台验证
定向覆盖 manager/controller、各 Runtime lifecycle、Harness Host/control plane、
ACP backend/owner、模型桥与插件,随后执行仓库要求的 `npm test`、`npm run typecheck`、
`npm run lint`;生产构建改动在开发阶段按仓库规则构建验证。
凡改变 Agent 或桌面到 Agent 路径,必须在开发期间按
[真实 Host 验证规则](../remote-host/technical-design.md#agent-开发期间的真实-host-验证)
使用当前源码的共享 Linux x64 Host 验证。Windows 原生 ACP 探针、旧版本 Host、mock、
CI 和将来发布测试都不能替代它。Windows PowerShell 与 Linux Bash 的工作目录及取消
分别验证;macOS/Linux arm64 生命周期与原生依赖由可用原生平台和 CI 补齐并明确报告。
当前源码的真实 Linux Host、完整生产 UI 和最终整仓门禁结果均保留在进度记录;
各次失败、修正、模型调用数及未测平台也一并记录,不以本机单测或原生协议探针
替代完整产品验收。完整 App 性能样本保留用户要求的按钮位置调整,属于包含该 UI
差异的实测对照,不是纯 Runtime 单变量实验。
@@ -0,0 +1,672 @@
# 助手工作栏多终端页签 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 已实施,跨平台验证中 |
| 版本 | 1.2 |
| 日期 | 2026-09-10 |
| 适用产品 | GoodBuddy 桌面端 |
| 界面归属 | [通用助手工作栏与执行空间](./prd.md) |
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md) |
| 远程架构 | [SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md)、[SSH Host 远程环境准备与控制面直连设计](../remote-host/environment-provisioning-technical-design.md) |
## 1. 文档职责
本文细化助手工作栏中的以下行为:
- 已打开应用以同级 Tab 呈现。
- Tab 行始终提供“+”入口,用户按需打开应用。
- 应用通过统一注册表分别声明单实例或多实例、默认打开、可关闭和排序策略。
- “终端”作为首个多实例应用,自动使用当前项目的本机或托管 SSH 执行空间。
- 本机与远程交互式终端的生命周期、状态、性能和验收要求。
[通用助手工作栏与执行空间 PRD](./prd.md)
继续负责工作栏的应用级位置、稳定能力目录、执行空间、范围绑定和总体产品边界。本文不重新
定义 Host Key、凭据、Agent 安装、Ask/Execute Runtime 或项目持久化契约。
## 2. 背景与当前问题
当前工作栏在聊天页面右侧固定显示“任务中心、工作区、浏览器、成果”四个 Tab。用户不能:
- 通过统一入口按需打开新的工作栏应用。
- 按应用策略关闭并重新打开可关闭的工作栏应用。
- 为支持多实例的应用创建第二个独立实例。
- 在 GoodBuddy 内使用本机或远程 Host 的交互式 Shell。
当前代码已经具备工作栏右侧回流、宽度调整、Tab 键盘导航、SSH Host Key 固定、加密凭据和
SSH 连接池,但尚不存在工作栏应用实例模型、终端 IPC、终端 Session Manager 或终端模拟器。
`node-pty` 当前只作为其他 Runtime 的间接依赖被打包,不能视为已经存在用户终端能力。
## 3. 产品定义
### 3.1 工作栏应用
工作栏应用是可以在助手工作栏中打开的一个能力,例如任务中心、工作区、浏览器、成果或
终端。每项应用通过统一注册表声明:
- 稳定应用 ID。
- 用户可见名称、图标和说明。
- `single` 或 `multiple` 实例策略。
- 是否默认打开、是否必须存在、是否可关闭和是否允许排序;这些属性彼此独立。
- 创建实例时可用的默认上下文。
- 当前不可用时的原因和修复入口。
目录中的应用可发现性与实例是否已经打开相互独立。应用不可用时保留入口并说明原因,不以
隐藏代替状态。
注册表字段如下:
| 字段 | 类型或允许值 | 含义 |
| --- | --- | --- |
| `id` | 稳定应用 ID | 关联图标、面板实现和持久化实例 |
| `label` | 有界文本 | 默认 Tab 名称和目录名称 |
| `icon` | 稳定图标 ID | 应用目录和 Tab 图标 |
| `description` | 有界文本 | 目录说明和完整提示 |
| `instancePolicy` | `single`、`multiple` | 同一应用允许一个还是多个实例 |
| `defaultContext` | `application`、`current-project`、`current-conversation` | 创建实例时使用的默认范围,不代表实例随后必须跟随 |
| `visibleAcrossContextSwitches` | boolean | 切换项目或会话后,已打开的 Tab 是否继续留在工作栏中 |
| `defaultOpen` | boolean | 新布局是否创建默认实例 |
| `required` | boolean | 布局加载时实例缺失是否必须补回 |
| `closable` | boolean | 是否渲染关闭入口并接受关闭命令 |
| `reorderable` | boolean | 是否允许用户改变 Tab 顺序 |
| `availability` | `available` 或带原因和修复入口的 `unavailable` | 当前能力状态 |
`defaultOpen`、`required` 和 `closable` 不互相推导。例如浏览器默认打开但不是必需实例;任务
中心和工作区既默认打开又必须存在。`required` 不表示锁定顺序,顺序只由 `reorderable` 控制。
当前五个应用的 `visibleAcrossContextSwitches` 均为 `true`。该属性记录应用目录的显示策略,
不改变内容归属,也不授予其他会话的 MCP 访问权限;当前没有使用 `false` 的应用,尚未实现
按上下文隐藏 Tab 的策略。
浏览器在创建时绑定当前 Conversation;切换会话后仍保留原 Tab、选中状态和页面,导航操作
继续使用原 `targetRef.conversationId`。此时新建浏览器才绑定新会话。默认或旧布局中尚未绑定
的浏览器在首次使用时绑定活动会话,此后不再随会话切换改绑。终端同样保留创建时执行目标;
任务中心、工作区和成果的内容继续跟随当前项目。
### 3.2 工作栏 Tab 实例
工作栏 Tab 是应用的一次打开实例。每个实例具有独立 ID、标题、应用类型和状态。Tab 行只
显示已经打开的实例,不把全部能力同时渲染为 Tab。
实例持久化字段为 `id`、`appId`、`title`,以及可选的 `targetRef`、`defaultTitleNumber`。
默认标题编号的保存和兼容规则见第 10 节。`targetRef` 使用公开引用:
终端接受 `local` 或 `projectId`,浏览器接受 `conversationId`。Main 签发的 Browser Tab ID、
Terminal Session ID、元素引用、凭据、路径和运行状态不进入工作栏布局。
### 3.3 终端实例
每个终端 Tab 对应一个独立交互式 Shell Session:
- 本机终端由桌面 Main 进程管理本机 PTY。
- 远程终端通过现有、已验证的 SSH Host 打开直接 SSH PTY。
- 不同终端拥有独立 Shell、工作目录、输入、输出、尺寸和结束状态。
- 创建后固定到当时选择的执行位置,切换项目不会迁移已经运行的终端。
直接 SSH PTY 是用户操作的远程终端,不是连接期临时 GoodBuddy Agent,也不作为 Runtime
stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或桌面重启的进程恢复。
## 4. 已确认的产品决策
1. 工作栏 Tab 行末尾始终显示“+”按钮。
2. “+”是普通命令按钮,不是一个 `tab`。
3. 点击“+”在工作栏内容区显示“新建工作栏应用”选择页;取消后返回原活动 Tab。
4. 选择应用后创建或激活实例,并关闭临时选择页。
5. 单实例应用已经打开时,再次选择只聚焦已有实例,不创建重复 Tab。
6. 多实例应用每次选择都创建新实例。
7. 终端从首期开始就是多实例应用。
8. 任务中心和工作区保持单实例、默认打开且不可关闭,分别固定在第一、第二位,`reorderable` 为 `false`。恢复旧布局时也按此规则修正顺序,其余 Tab 保持相对顺序。
9. 浏览器注册为可关闭的多实例应用,升级后保留一个默认实例;成果保持单实例、默认打开且
可关闭。
10. 用户关闭可关闭实例、切换或重排应用 Tab 后,“+”仍保持可见。
11. 选择“终端”后不再显示第二层目标选择器。存在当前项目时直接绑定该项目,由 Main
根据项目执行空间自动打开本机或托管 SSH Shell;没有当前项目时打开本机 Home Shell。
12. 远程首期使用直接 SSH PTY。SSH 断线后终端进入“已中断”,重新连接会创建一个新的
Shell,不伪装为恢复旧进程。
13. 工作栏收起、切换主页面或切换 Tab 不终止终端;显式关闭终端 Tab 才终止对应 Shell。
14. 应用重启不自动启动 Shell。恢复的终端 Tab 显示“会话已结束”,由用户显式重新连接。
15. 终端是用户交互表面。Ask/Execute 继续约束 Agent Runtime,不把用户键盘操作误判为
Agent 工具调用;Agent、自动化和模型不得未经明确用户操作向终端注入输入。
16. 工作栏继续按上位 PRD 提升为应用级容器;切换聊天、知识、魔法笔记或活动记录等主页面
不销毁 Tab 实例或终端 Session。
## 5. 目标
### 5.1 用户目标
- 在工作栏中随时通过“+”发现并打开应用。
- 同时打开多个本机或远程终端,并通过 Tab 快速切换。
- 清楚知道每个终端运行在本机还是哪个远程 Host、当前目录和连接状态。
- 在终端断线、退出或目标失效后获得准确状态和明确恢复操作。
- 使用键盘完成打开应用、切换 Tab、操作终端和关闭实例。
### 5.2 产品目标
- 用一个可扩展的应用注册表统一单实例与多实例行为。
- 把现有 Tab 迁移到统一实例模型,同时保持任务中心和工作区始终存在,并允许浏览器和终端
创建多个实例。
- 建立真实的 Main/Preload/Renderer 终端生产链路,而不是用普通文本框模拟终端。
- 复用现有 SSH Host Key、凭据和连接池,不建立第二套 Host 管理。
- 保持终端输入输出有序、响应流畅、内存有界,并可靠清理本机和远程资源。
## 6. 非目标
- 不把 GoodBuddy 改造成完整 IDE。
- 不唤起 Windows Terminal、Terminal.app 等外部终端窗口代替内嵌终端。
- 不扫描或导入用户全部 SSH config、目录、Shell profile 或命令历史。
- 不支持任意 `ProxyCommand`、端口转发或 SSH Agent Forwarding。
- 不在打开远程终端时安装、更新或修复 GoodBuddy Agent。
- 不把 Agent 工具调用、Runtime 日志或自动化输出伪装成用户终端。
- 不允许 Renderer 直接访问 PTY、ChildProcess、SSH Client、凭据或 Node API。
- 首期不提供终端协作、录制、云同步、Shell Session 复用、tmux 管理或跨设备恢复。
- 首期不实现底部停靠和独立窗口;本文保留兼容模型,后续按工作栏总 PRD 扩展。
- 首期不通过 GoodBuddy Agent 恢复远程终端,也不为终端复制 Runtime transcript、
`outcome_unknown` 或模型调用恢复协议。
## 7. 信息架构与“+”交互
### 7.1 Tab 行
```text
┌──────────────────────────────────────────────┐
│ 任务中心 │ 工作区 │ 终端 1 │ 终端 2 │ + │
├──────────────────────────────────────────────┤
│ 当前活动实例内容 │
└──────────────────────────────────────────────┘
```
- 已打开实例位于一个语义化 `tablist` 中。
- 每个实例使用独立 `tab` 和 `tabpanel`。
- “+”固定在 Tab 行末尾,但位于 `tablist` 之外。
- Tab 过多时实例区域单行横向滚动,不换行、不自动隐藏实例。“+”紧随最后一个标签并随标签一起滚动,滚到末尾时可见。
- 标签全部可见时隐藏左右滚动按钮;溢出时左右箭头透明浮动于工作区两端,不占标签布局宽度。鼠标移入标签栏时显示箭头,移出后隐藏;键盘聚焦箭头时显示,无悬停能力的设备保持可见。悬停箭头时显示轻微底色。点击滚动约一屏,边界方向禁用且不拦截下方标签的指针操作。切换或新增激活标签时将其滚入可见区域。未激活标签使用低对比度灰蓝底色和细边框,激活标签保留亮底与强调色顶线。
- 终端 Tab 默认使用递增标题,例如“终端 1”“终端 2”;状态栏持续显示项目、Host、
工作目录和 Shell。用户可以重命名终端。
- 任务中心、工作区、成果及新建终端的默认标题随界面语言切换,恢复布局时也使用当前语言。
终端自定义名称保持原文;旧布局中的终端名称保持原值,即使看起来像“终端 1”。浏览器
标题沿用现有行为,不在本次标题国际化范围内。
- 当前 Tab 同时使用连续内容表面、选中边框、顶部强调线、图标和较高字重,不能只靠文字颜色。
- Tab、关闭按钮和“+”入口沿用共享控件圆角;终端面板外框和终端顶部工具栏按钮使用直角,
保持紧凑的停靠式终端界面。
- 只有可关闭实例渲染 Tab 关闭按钮。关闭按钮默认不争夺视觉注意力,仅在鼠标悬停或键盘
焦点进入对应 Tab 时出现;
粗指针或无悬停设备始终显示,出现与隐藏不得引发布局位移。
### 7.2 新建应用选择页
点击“+”后:
1. 保留当前实例及其运行状态。
2. 在工作栏内容区显示“新建工作栏应用”。
3. 列出稳定应用目录,每项显示名称、说明、单/多实例提示和当前可用状态。
4. 初始焦点进入第一个可用应用;Escape 或“取消”返回原活动实例和“+”按钮。
5. 选择单实例应用时,若实例已存在,直接聚焦该 Tab;任务中心和工作区始终走此路径。
6. 选择浏览器时创建绑定当前 Conversation 的新 Browser Tab;没有活动 Conversation 时创建
待选择目标的空实例。
7. 选择终端时直接解析当前项目执行空间,创建新 Tab 并聚焦终端输入,不再要求用户
重复选择本机或远程目标。
选择页是“+”触发的临时界面,不进入实例顺序,不持久化,也不占用一个普通 Tab。
### 7.3 首期应用注册表
| 应用 | 实例策略 | 默认上下文 | 默认打开 | 必须存在 | 可关闭 | 可排序 | 再次从“+”选择 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 任务中心 | `single` | 当前项目 | 是 | 是 | 否 | 否 | 聚焦已有 Tab |
| 工作区 | `single` | 当前项目 | 是 | 是 | 否 | 否 | 聚焦已有 Tab |
| 浏览器 | `multiple` | 当前 Conversation | 是,一个实例 | 否 | 是 | 是 | 每次创建新 Tab |
| 成果 | `single` | 当前项目 | 是 | 否 | 是 | 是 | 聚焦已有 Tab |
| 终端 | `multiple` | 当前项目 | 否 | 否 | 是 | 是 | 每次创建新 Tab |
实例策略、默认状态和可关闭策略都是应用注册数据,不能分散硬编码在多个点击处理器中。
新建浏览器使用独立编号标题,避开已有名称;关闭后再次新建也不产生重名。恢复布局保留已保存
的标题。会话名称用于标识归属,不替代浏览器实例名称。新建页面为空白页,同会话 Cookie 共享
不表示复用旧页面或导航历史。
### 7.4 Tab 操作
- 点击或使用方向键切换实例不会暂停或终止后台实例。
- 支持左右方向键、Home、End 和 roving `tabindex`。
- 任务中心和工作区没有关闭按钮,也不响应关闭快捷命令。
- 关闭可关闭的非活动 Tab 后焦点保持在当前活动 Tab。
- 关闭可关闭的活动 Tab 后激活右侧相邻实例;没有右侧实例时激活左侧实例。任务中心和工作区
始终存在,因此正常布局不会进入零实例空状态。
- 关闭正在运行的终端需要确认“关闭终端将结束 Shell 及其子进程”;取消是初始焦点。
- 已退出或已中断的终端可以直接关闭,不重复确认。
## 8. 终端创建流程
### 8.1 自动目标解析
- 当前项目是本地项目时,创建目标为公开 Project ID,Main 从权威项目记录解析本机根目录。
- 当前项目是托管 SSH 项目时,创建目标仍为公开 Project ID,Main 再解析项目 Host、
固定 Host Key、凭据和远端根目录。
- 没有当前项目时使用本机目标和当前用户 Home。
- Host 未验证、凭据不可用或不支持交互式 Shell 时,终端 Tab 就地显示失败原因和恢复入口,
不静默切换到本机或其他 Host。
### 8.2 本机终端
- 当前项目是本地项目时,默认工作目录为项目根目录。
- 没有本地项目目标时,默认工作目录为当前用户 Home。
- Windows 优先使用可用的 PowerShell 7,其次 Windows PowerShell,最后使用系统
`COMSPEC`。
- macOS 和 Linux 优先使用当前用户有效的 `$SHELL`,不可用时使用平台常规 Shell fallback。
- 首期自动选择默认 Shell,不提供任意可执行文件路径输入。
- Main 只向 Shell 传递正常交互所需的筛选环境,不注入模型 API Key、GoodBuddy 内部
Token 或其他产品凭据。
### 8.3 远程终端
- 当前项目是托管 SSH 项目时,自动使用项目 Host 和远端项目根目录。
- Main 复用现有 Host store、完整 Host Key 固定、认证方式和 SSH connection pool。
- 远端通过 SSH PTY 打开账号的登录 Shell,并支持输入、ANSI 输出和窗口尺寸同步。
- 活动终端持有专用 Channel lease,不得被连接池空闲回收。
- 不把密码、私钥、认证头或 GoodBuddy 模型凭据发送给 Renderer 或写入终端环境。
- Host 地址、用户、凭据或 Host Key generation 变化时,旧 Session 进入“已中断”,新建
Session 必须使用 Host store 中的当前身份。
### 8.4 创建结果
创建成功后:
- 新 Tab 插入当前活动 Tab 右侧并立即激活。
- 焦点进入终端输入区域。
- 状态栏显示“本机”或 Host 名称、工作目录、Shell 和连接状态。
- 终端尺寸使用内容区计算出的首个有效 `cols/rows`,不是固定默认值覆盖实际布局。
创建失败时在终端 Tab 保留错误上下文并提供重试;不得留下不可回收 Session 或显示为已经连接。
## 9. 终端交互与状态
### 9.1 基础交互
首期支持:
- 真实 TTY 输入与 ANSI 输出。
- 中文输入法、组合字符和常规键盘快捷键。
- 复制、粘贴、全选、搜索、清屏和滚动。复制与粘贴通过桌面 Main 进程访问系统文本剪贴板,
不依赖 Renderer 的浏览器剪贴板权限。
- 工作栏宽度或窗口尺寸变化后的 PTY resize。
- 终端重命名。
- 显式关闭、退出后重新打开和远程中断后重新连接。
浏览器原生页面快捷键与终端按键冲突时,终端聚焦期间优先交给终端;应用级保留快捷键必须
有明确规则和可访问说明。
### 9.2 状态模型
```ts
type TerminalSessionState =
| 'starting'
| 'running'
| 'exited'
| 'interrupted'
| 'closing'
| 'failed'
```
- `starting`:正在创建本机 PTY 或 SSH Channel。
- `running`:允许输入和 resize。
- `exited`:Shell 正常或带退出码结束,显示退出码或信号。
- `interrupted`:远程连接、Host identity 或桌面生命周期导致会话失效。
- `closing`:已拒绝新输入,正在回收进程或 Channel。
- `failed`:Session 未成功开始或发生不可恢复错误。
高频输出不逐行进入 live region。只播报连接成功、连接失败、已中断和 Shell 已退出等重要
状态变化。
### 9.3 重新连接
- 本机或远程 Session 结束后,“重新打开”在同一 Tab 中创建全新的 Shell Session。
- 远程断线后的按钮文案为“重新连接并新建 Shell”,不能暗示恢复旧进程。
- 新 Session 使用 Host store 和项目中的当前有效配置,不复用旧 Host identity。
- 重新连接前清空旧终端画面,并保留可见说明“这是新的 Shell 会话”。
## 10. 实例、Session 与持久化
工作栏实例与运行 Session 分离:
```ts
type WorkbarAppDefinition = {
id: 'tasks' | 'workspace' | 'browser' | 'results' | 'terminal'
label: string
icon: WorkbarAppDefinition['id']
description: string
instancePolicy: 'single' | 'multiple'
defaultContext: 'application' | 'current-project' | 'current-conversation'
defaultOpen: boolean
required: boolean
closable: boolean
reorderable: boolean
availability:
| { state: 'available' }
| { state: 'unavailable'; reason: string; remedy: string }
}
type WorkbarTabInstance = {
id: string
appId: WorkbarAppDefinition['id']
title: string
defaultTitleNumber?: number
targetRef?:
| { type: 'local' }
| { type: 'project'; projectId: string }
| { type: 'conversation'; conversationId: string }
}
type WorkbarLayoutPreferences = {
instances: WorkbarTabInstance[]
activeInstanceId: string | null
expanded: boolean
dock: 'right'
widthRatio: number
taskScope: 'current-project' | 'global' | 'all-projects'
}
```
- 持久化工作栏打开状态、Tab 顺序、活动实例、应用 ID、自定义标题、公开目标引用和宽度比例。
- `taskScope` 中的 `global` 仅保留旧布局解析兼容;UI 恢复时映射为 `all-projects`,此后只保存
`current-project` 或 `all-projects`。范围规则见 [Task Center PRD](../task-and-job/task-center-prd.md#4-范围)。
- 加载布局时根据 `required` 补回缺失实例,并拒绝重复的单实例应用;应用策略不写入实例
数据,避免持久化状态覆盖注册表。
- `reorderable` 已进入共享契约;当前 Tab 壳层尚未提供拖放或键盘重排,不能把声明属性视为
已完成的排序功能。
- 不持久化终端正文、未执行输入、凭据、环境变量、PTY PID、SSH Channel 或派生 Host
identity。
- Main 创建终端后返回新的不透明 Session ID;Renderer 不能选择 PID 或 Channel。
- 应用重启恢复终端 Tab 描述,但状态为“会话已结束”,不自动建立 SSH 连接或启动本机
Shell。
- 运行期间切换项目不会改变已经创建的终端目标。用户需要新目标时通过“+”创建新终端。
新建终端保存正整数 `defaultTitleNumber`,沿用同一目标已有终端数量加一的编号规则。
Sidebar 在渲染时按当前语言派生应用和默认终端标题,再交给 Tab 壳层与终端面板;切换语言
不改写持久化标题。用户重命名时清除此字段并保存输入的 `title`。缺少标记的旧终端直接显示
原 `title`,不通过名称模式推断默认标题。布局存储键保持 `goodbuddy.workbar-layout.v1`;
旧版本使用严格 Schema,不能读取带新增字段的布局,本变更不提供降级兼容。
## 11. 生命周期与关闭语义
| 用户或系统动作 | Tab | Shell Session |
| --- | --- | --- |
| 切换 Tab | 保留 | 继续运行 |
| 收起工作栏 | 保留 | 继续运行,停止非必要高频 UI 工作 |
| 切换主页面 | 保留 | 继续运行 |
| 切换项目 | 保留并显示原目标 | 继续在原目标运行 |
| 关闭运行中的终端 Tab 并确认 | 删除 | 结束 Shell 和子进程 |
| Shell 自行退出 | 保留 | 标记 `exited` |
| SSH 断线 | 保留 | 标记 `interrupted` |
| 删除或修改目标 Host | 保留 | 关闭旧 Channel 并标记 `interrupted` |
| Renderer 销毁或应用退出 | 下次恢复描述 | 在总关闭截止时间内回收 |
关闭操作必须安全处理竞态。Main 停止接受新输入,关闭 stdin/Channel,先正常结束,再在既有
应用关闭截止时间内升级清理;一个卡死终端不能无限阻塞应用退出。关闭后的迟到 ACK 视为已
完成,input、resize 或重复 close 返回 Session 不存在,不操作其他 Session。
## 12. 技术生产路径
### 12.1 Shared
新增独立终端契约,至少覆盖:
- `create`:目标引用、初始 `cols/rows`。
- `write`:Session ID 和有界输入。
- `resize`:Session ID、`cols/rows`。
- `close`:Session ID。
- `snapshot`:目标、状态、Shell、目录和终态。
- `event`:带 Session ID 和单调序号的输出、状态、退出与错误事件。
所有 Schema 使用 Zod 严格校验,并限制 ID、标题、输入、尺寸、事件块、会话数和缓冲字节。
### 12.2 Main
新增统一 `TerminalSessionManager`:
- Session 按所属 `webContents` 和不透明 ID 隔离。
- 将现有间接 `node-pty` 提升为 GoodBuddy 直接生产依赖,并用于启动平台 Shell。
- 远程使用现有 `SshConnectionPool` 打开交互式 PTY Channel。
- 统一处理 write、resize、close、exit、连接中断和应用退出。
- 终端输出只发送给所属窗口。
- 复用现有子进程树终止思路,但针对 PTY 验证 Windows 与 POSIX 的实际回收行为。
### 12.3 Preload 与 IPC
- `DesktopApi` 只暴露显式 `terminal.create/write/resize/close/getSnapshot/onEvent`。
- Main Handler 对每个调用执行可信 sender 检查和共享 Schema parse。
- Preload 管理事件订阅释放,不暴露 `ipcRenderer`、PTY、SSH Client 或 Node API。
### 12.4 Renderer
- 使用 `@xterm/xterm` 6 和官方 fit/search addon 处理 ANSI、光标、选择、IME、搜索、
滚动和可访问输入,不自行实现终端转义状态机。
- 终端首次打开时按需加载;支持 WebGL2 时使用官方 WebGL renderer,初始化失败或 Context
丢失时释放 WebGL addon 并回退到 xterm 默认 renderer。
- xterm 使用解析后的跨平台系统等宽字体栈,不能把未解析的 CSS `var(...)` 交给 Canvas。
- Tab 壳层只管理实例与焦点,不持有 PTY 或 SSH 对象。
- `ResizeObserver` 只在有效非零布局后发送去重后的 `cols/rows`。
- 组件卸载时退订画面事件;只有显式关闭终端才调用 Session close。
- 终端样式复用全局语义颜色、边框、焦点和字体令牌,不创建另一套工作台设计系统。
### 12.5 输出顺序与背压
- 输出按 Session 分区并携带单调序号。
- Main 合并小块输出,限制单事件大小、待发送条目和总字节。
- Renderer 确认已经消费的序号;慢消费者达到上限时暂停 PTY/SSH stream,消费后恢复。
- 不通过丢弃中间输出维持界面响应,因为丢失字节会破坏终端状态。
- Renderer 只保留有界滚动历史;清屏不改变 Main Session 生命周期。
## 13. 权限与边界
- 用户终端以当前本机账号或所选 SSH 账号的正常权限运行,不增加额外虚构的 trust tier。
- Ask/Execute 是 Agent Runtime 模式,不自动改变用户直接操作终端的 OS 权限。
- UI 必须持续将其标注为“用户终端”,避免被理解为 Ask Runtime 的只读输出。
- Agent、模型、监督器和自动化不能调用终端 write,也不能把指令注入已有 Session。
- 终端输出不会自动进入对话上下文、模型 Prompt、成果或活动记录。
- Renderer 只能引用项目和 Session 的公开 ID;Main 从权威 store 解析目录、Host、凭据和
当前 Host identity。
- 不接受 Renderer 提交任意 Shell 可执行路径、SSH 命令、PID、环境变量集合或原始凭据。
- 保持 Electron `contextIsolation`、sandbox 和禁用 Renderer Node integration。
## 14. 错误与反馈
- 创建失败、断线、Shell 退出和目标失效保留在终端面板,提供就地重试。
- 不与全局通知重复显示同一终端错误。
- 非局部错误,例如无法保存工作栏布局,可以进入应用通知。
- 错误文案说明发生了什么、当前 Shell 是否仍在运行、用户可以做什么。
- Host Key 变化沿用现有 Host 管理流程;终端只显示“主机身份已变化,请重新验证”,不在
终端内复制凭据或 Host Key 修改流程。
- 目标不可用时保留 Tab 名称和目标,不静默切换到本机或其他 Host。
## 15. 性能与资源要求
- 工作栏切换和终端输入在正常负载下保持即时反馈,持续输出不能阻塞主界面导航。
- 同一窗口的活动终端数量必须有明确上限;达到上限时阻止新建并说明先关闭不用的终端。
- 每个 Session 的输出队列、Renderer 滚动历史和 IPC 单事件均有硬上限。
- 工作栏不可见时可以降低渲染刷新频率,但不能丢失终端协议字节或暂停 Shell 本身。
- 应用退出复用现有总清理截止时间,不因终端新增无界等待。
- 生产构建必须实际加载本机 PTY 原生模块;不能只验证文件存在。
- 远程终端不能使 SSH connection pool 的空闲项、Channel 或监听器泄漏。
具体数值在共享契约和性能测试中确定,以现有 IPC、窗口和关闭预算为上限,不在多个进程中
维护不同常量。
## 16. 无障碍与响应式
- Tab 使用 `tablist/tab/tabpanel`;“+”使用带可访问名称“打开工作栏应用”的普通按钮。
- 应用选择页使用可键盘操作的列表或菜单语义,不使用 `SegmentedControl`;终端没有重复的
目标选择页。
- 每个终端 Tab 的可访问名称包含自定义名称或位置,多个终端不能都只读作“终端”。
- Tab 关闭后焦点移动到确定的相邻 Tab;取消创建后焦点返回“+”。
- 工作栏收起后焦点返回唯一工作栏开关,不增加焦点陷阱或遮罩。
- 终端高频输出不逐行播报;只播报连接、退出、失败和中断。
- 200% 文字缩放时,目标、Shell、目录、状态和关闭操作仍可访问。
- Tab 单行横向滚动,终端内容随工作栏尺寸变化并同步 PTY,不把 Tab 换成下拉框。
- 深色和浅色主题都使用设计系统语义令牌,并保持终端文本、选择和光标可辨。
## 17. 实施顺序与多代理工作包
功能必须以完整的本机和远程用户流程交付。以下是可并行的开发工作包,不表示可以发布只有
Schema、假数据终端或不可用入口的中间产品。
### 17.1 依赖顺序
```text
契约冻结
├─ 工作栏实例壳层
├─ 本机 PTY 后端
├─ SSH PTY 后端
└─ 终端 Renderer
↓
单一集成负责人完成 Main/Preload/App 接线
↓
完整验证、真实 Host 验收与文档同步
```
### 17.2 文件所有权
| 工作包 | 主要职责 | 建议文件所有权 | 不应修改 |
| --- | --- | --- | --- |
| 契约与注册表 | Workbar/Terminal Schema、状态、测试 | 新建 `src/shared/workbar-contracts.ts`、`terminal-contracts.ts` 及测试 | `App.tsx`、`ipc.ts`、Preload |
| 工作栏壳层 | 动态实例 Tab、“+”、选择页、单/多实例和焦点 | 新建工作栏组件及其测试 | PTY、SSH、Main IPC |
| 本机终端后端 | 本机 PTY、输入、resize、退出和进程树清理 | 新建 `src/main/terminal/` 本机文件及测试 | Renderer、远程 Agent |
| SSH 终端后端 | SSH PTY、lease、resize、断线和 Channel 清理 | 新建 `src/main/terminal/` 远程文件及聚焦测试 | 工作栏 UI、Agent 协议 |
| 终端 Renderer | 模拟器、IME、搜索、复制粘贴、状态栏和 resize | 新建终端组件、样式和测试 | Main、SSH、中心 IPC |
| 集成 | 依赖、IPC channel、DesktopApi、Preload、Main 注册、`App` 接线与关闭顺序 | `package.json`、lock、`ipc-channels.ts`、`contracts.ts`、`preload/index.ts`、`main/ipc.ts`、`App.tsx` | 不重写各包已验收内部逻辑 |
| 验收与文档 | E2E、跨平台构建检查、真实 Host、最终文档 | 测试与相关文档 | 不用假实现替换生产路径 |
中心文件只能由集成负责人修改,避免多个代理同时编辑 `App.tsx`、`main/ipc.ts`、
`shared/contracts.ts`、`preload/index.ts`、`package.json` 和 lockfile。
### 17.3 并行开发门槛
契约负责人先冻结:
- 应用注册和实例策略。
- Terminal request、snapshot、event 和 error code。
- Session 所有权和关闭语义。
- 输出序号、ACK 与容量常量。
其他代理基于冻结契约工作。接口偏差由集成负责人统一修正,不由多个代理分别修改共享中心
文件。
## 18. 验收标准
### 18.1 工作栏应用与实例
- [ ] Tab 行末始终显示“+”;任务中心和工作区始终存在,因此正常布局不存在零实例状态。
- [ ] 点击“+”显示稳定应用目录,取消后返回原实例并恢复焦点。
- [ ] 选择已打开单实例应用只聚焦已有 Tab。
- [x] 任务中心和工作区默认存在且不可关闭,不渲染关闭按钮,也不响应关闭快捷命令。
- [x] 连续选择浏览器会创建页面、导航状态和元素引用相互独立的浏览器 Tab。
- [x] 连续选择终端会创建多个互相独立的终端 Tab。
- [ ] 关闭可关闭实例、切换和重排实例后,“+”仍保持可用。
- [ ] Tab 溢出时单行横向滚动,两端箭头固定,“+”跟随最后一个 Tab,滚到末尾可见,不把 Tab 换成下拉框。
- [x] 关闭可关闭的活动 Tab 后激活正确的相邻 Tab 并恢复焦点。
- [x] 当前 Tab 的背景、边框、顶部指示和字重清晰可辨;关闭按钮仅在悬停、焦点或无悬停
设备上出现,且不引发布局位移。
- [x] 升级后任务中心、工作区、一个浏览器实例和成果按默认布局打开;布局恢复会补回缺失的
任务中心与工作区。
- [ ] 在所有主要页面都能打开工作栏;切换主要页面不会销毁 Tab 实例或终端 Session。
### 18.2 本机终端
- [x] 从“+ → 终端”直接打开当前本机项目的真实交互式 Shell,工作目录为项目根。
- [ ] 没有项目时从用户 Home 打开本机 Shell。
- [ ] `pwd` / `Get-Location`、交互输入、ANSI 颜色、中文 IME、复制和粘贴工作正常。
- [ ] 调整工作栏和窗口尺寸会更新 PTY `cols/rows`,全屏 TTY 程序正确重绘。
- [ ] 多个本机终端的输入、输出、目录和退出状态互不串线。
- [ ] Shell 有活动子进程时关闭 Tab,确认后回收完整进程树;取消后 Session 继续运行。
- [ ] 退出后的 Tab 显示退出码,并可在同一 Tab 显式创建新 Shell。
### 18.3 远程终端
- [ ] 从“+ → 终端”直接为当前托管 SSH 项目打开真实 SSH PTY。
- [ ] 终端持续显示 Host、远端目录、Shell 和连接状态。
- [ ] 使用现有固定 Host Key 和已存凭据,Renderer、日志和命令参数不出现凭据。
- [ ] 输入、ANSI、中文、复制粘贴和 resize 在真实 Host 上工作正常。
- [ ] 活动远程终端不会被 SSH connection pool 空闲回收。
- [ ] SSH 断线立即标记“已中断”并拒绝继续输入,不显示为仍在运行。
- [ ] “重新连接并新建 Shell”明确创建新 Session,不宣称恢复旧远端进程。
- [ ] Host 删除、地址/用户/凭据或 Host Key generation 变化会中断旧 Session;新 Session
使用当前 Host identity。
### 18.4 生命周期、性能和边界
- [ ] 收起工作栏、切换主页面、切换 Tab 或切换项目不会终止已有终端。
- [ ] 终端不会随项目切换静默迁移执行位置。
- [ ] 应用重启只恢复终端 Tab 描述,不自动连接或启动 Shell。
- [ ] 输出顺序在持续大输出下保持正确,Main 和 Renderer 内存保持有界,主界面仍可操作。
- [ ] 慢 Renderer 触发暂停和恢复,不丢弃中间终端字节。
- [ ] Renderer 不获得 raw PTY、ChildProcess、SSH Client、PID、环境变量或凭据。
- [ ] Agent、模型和自动化不能向已有用户终端注入输入。
- [ ] 窗口销毁和应用退出在既有关闭截止时间内回收本机进程、SSH Channel 和监听器。
- [ ] 重复 close、迟到 input/resize 和退出竞态不会操作其他 Session 或导致进程泄漏。
### 18.5 设计、无障碍和跨平台
- [ ] 仅使用键盘可以打开应用、创建浏览器和终端、切换 Tab、关闭可关闭 Tab、复制、粘贴及
重新连接。
- [ ] Tab、“+”、选择页和关闭确认使用正确语义并有可见焦点。
- [ ] 终端输出不逐行进入 live region,关键状态变化可被辅助技术感知。
- [ ] 浅色、深色、200% 文字缩放和窄窗口下可以完成核心流程。
- [ ] Windows、macOS、Linux x64 和 Linux arm64 的正式构建实际启动本机 PTY。
- [ ] 远程链路在共享 Linux x64 真实 Host 上完成创建、输入、resize、断线、重连、关闭和
应用退出验证;若 Host 不可达,远程终端不得标记为完成。
## 19. 验证计划
### 19.1 自动验证
- 共享 Zod Schema 和边界值测试。
- 单/多实例、默认打开、可关闭和排序策略,以及 Tab 顺序、关闭焦点和“+”选择页组件测试。
- 任务中心与工作区缺失时的布局修复、关闭命令拒绝和焦点保持测试。
- 浏览器多实例的页面隔离、Conversation 匹配和快捷入口去重测试。
- 本机 PTY Session Manager 的输入、resize、退出、幂等关闭和进程树测试。
- SSH PTY 的 Host identity、lease、断线、resize 和关闭测试。
- Main trusted sender、Session 所有权、IPC 输入边界和事件路由测试。
- Terminal Renderer 的 IME、键盘、ResizeObserver、订阅释放和状态测试。
- `npm test`
- `npm run typecheck`
- `npm run lint`
- `npm run build`
### 19.2 真实路径验证
- 在当前开发平台通过产品 UI 完成本机终端全流程。
- 在共享 Linux x64 Host 通过现有 Host 配置完成远程终端全流程。
- 真实运行需要 TTY 的程序,验证 ANSI、光标和 resize,不只运行非交互命令。
- 持续输出并主动减慢 Renderer 消费,验证背压和内存上限。
- 分别在本机和远程启动带子进程的命令,关闭 Tab 和应用后核对无 GoodBuddy 所属遗留资源。
- 生产构建在各受支持平台实际加载 PTY 原生模块,不以单元测试或包内文件存在代替。
### 19.3 当前开发验证记录
2026-09-01 已通过生产 `SshConnectionPool` 和 `createSshTerminalSession` 在共享 Linux x64
Host 完成直接 SSH PTY 验证:使用已有固定 Host Key 和测试凭据,以远端项目目录启动登录
Shell,验证交互输入、ANSI、UTF-8 中文、`80×24` 到 `101×37` resize、连接销毁后进入
`interrupted`、重新连接创建新 Shell,以及 Session close 和 connection pool dispose
回收远端 Shell。验证只创建一个
`$HOME/.goodbuddy/terminal-smoke-<UUID>` 临时目录,结束后已删除并确认无遗留 Shell 进程。
同日使用最新生产构建完成工作栏视觉检查:活动 Tab 使用连续内容表面、顶部强调线和应用
图标;关闭按钮默认不可见,悬停后以 overlay 显示且 Tab 宽度不变;横向滚动继续可用但不
显示滚动条,“+”保持独立可见。
本记录不替代尚未勾选的完整产品 UI、剪贴板、macOS、Linux arm64 和各平台本机 PTY 验收。
本 PRD 不改变 Agent 协议,因此首期远程终端无需 Agent package 更新。若实现期间改为
Agent-owned PTY,必须先修订本文的远程生命周期和恢复语义,并按 Agent 开发规则在共享
Linux x64 Host 验证当前源码或候选包。
## 20. 相关文档职责
- [通用助手工作栏与执行空间 PRD](./prd.md):
工作栏应用级位置、稳定能力目录、执行空间和总体范围。
- 本文:持久“+”、应用实例策略、工作栏动态 Tab、自动项目目标、本机与直接 SSH 多终端。
- [统一界面设计系统](../../../UI-DESIGN.md):Tab、菜单、布局、主题、焦点和无障碍规则。
- [SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md):
Host Key、凭据、连接池、Agent 和远程项目现状。
- [SSH Host 远程环境准备与控制面直连设计](../remote-host/environment-provisioning-technical-design.md):
Host 保存、环境准备和项目使用之间的职责。
+11
View File
@@ -0,0 +1,11 @@
# 电脑控制
电脑控制包含 GoodBuddy 对浏览器和客户端桌面能力的产品实现与平台适配状态。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [功能进度](./progress.md) | 当前已实现能力、缺陷修复、平台限制和验证状态 |
当前只有进度文档;产品需求、功能逻辑、UI 和技术设计尚未从历史实现记录中独立整理。
@@ -1,6 +1,14 @@
# GoodBuddy 电脑控制开发进度
最后更新:2026-08-05
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档类型 | 实施进度 |
| 状态 | 持续更新 |
| 版本 | 0.4 |
| 日期 | 2026-09-03 |
| 适用能力 | 内置浏览器与客户端电脑控制 |
## 范围
@@ -9,18 +17,20 @@ Linux x64 和 Linux arm64。
## 已完成
### 直连模型与浏览器
### Runtime 与浏览器
- 已实现宿主进程拥有的隔离浏览器服务。
- 使用 Electron 自带 Chromium 和 CDP,不下载额外浏览器。
- 已实现导航、可访问性快照、点击、输入、选择、返回和有界截图工具。
- 浏览器工具只在直连模型的 Execute 模式中提供;用户选择 Execute 即授权
本次交互运行,不再逐个弹出 GoodBuddy 工具审批。
- 已实现公网 URL、DNS、重定向、私有地址和元数据地址限制。
- 浏览器工具作为 GoodBuddy 内置能力,可分别分配给直连模型、GoodBuddy 管理的
OpenCode 和 Continue;只在 Execute 模式中提供。用户选择 Execute 即授权本次
运行,不再逐个弹出 GoodBuddy 工具审批。
- 支持当前设备可连接的 HTTP、HTTPS、公网、内网、本机和元数据地址,
导航及重定向仍经过 DNS 解析和目标地址固定。
- 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、
空闲回收和应用退出清理。
- 直连模型工具循环已提高到适合浏览器任务的有界上限,并包含重复调用和
无进展保护。
- 直连模型工具循环不设置工具次数或模型轮次配额;取消、请求超时、工具结果大小和
累计输出边界继续防止失控执行。
### MCP
@@ -50,11 +60,18 @@ Linux x64 和 Linux arm64。
### 设置与安全边界
- 能力存储已迁移到版本 2。
- 已增加电脑控制能力卡片、诊断、托管浏览器配置元数据、IPC 和 preload
契约。
- 电脑控制能力默认关闭;启用、配置变更和本地数据清除会替换或清理相关
Runtime 和会话。
- 能力存储已迁移到版本 6。版本 2 至 5 中原有的浏览器启停状态会迁移到
`builtin-browser`,已有启用状态不丢失;默认分配给直连模型、GoodBuddy 管理的
OpenCode 和 Continue。
- 内置浏览器位于“内置 MCP”,与知识库、笔记和 GoodBuddy 配置一样提供独立启停、
Runtime 分配和工具清单;DeepSeek Harness 持续显示为不支持。
- 浏览器开关只控制 Agent 工具访问。关闭后,任何 Runtime 都不会获得浏览器工具,
但用户仍可在浏览器工作栏中手动前往、返回、刷新、停止加载、交互和关闭浏览器。
开启后,已分配 Runtime 可在 Execute 模式直接使用,不逐次询问。
- 尚未实际参与浏览器执行的命名配置已从界面隐藏;底层旧数据和兼容接口继续
保留,不删除用户已有记录。
- “电脑控制”只显示实际操作客户端电脑的能力。能力启用、配置变更和本地
数据清除仍会替换或清理相关 Runtime 和会话。
## 本轮已修复缺陷
@@ -75,14 +92,28 @@ Linux x64 和 Linux arm64。
### P0:右侧没有浏览器实时画面,已修复
浏览器窗口仍使用 `show: false`,但 BrowserService 现在从模型实际操作的同一
会话捕获页面帧,并通过受限 IPC 发送状态、当前 URL 和约 220KB 的 JPEG 画面。右侧工作栏
新增“浏览器”页签;活动对话启动浏览器时会自动打开该页签,并显示创建中、
加载中、操作中、就绪、失败和已停止状态。用户可在页签内立即停止当前对话的
浏览器会话。
早期版本通过同一浏览器会话的 JPEG 帧提供预览。当前工作栏直接嵌入 Agent 操作的原生
`WebContentsView`,不再自动截图或传输预览帧,也不再用独立交互窗口阻塞主界面。
页面显示、标签页归属、失败重试和显式截图的当前规则统一见
[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md);
该路径的最新验证证据见[工作栏进度](../assistant-workbar/progress.md)。
当前实现按导航、快照、点击、输入、选择、返回和截图操作刷新画面,而不是创建
第二个预览浏览器,因此显示内容与模型受控页面一致。
### 已实现:浏览器工具栏
浏览器 Tab 顶部已增加“返回、刷新/停止加载、地址输入、前往、交互、关闭浏览器”工具栏。
它继续操作 Agent 在当前 Conversation 中使用的同一浏览器会话;用户导航会有意改变 Agent
下一步看到的页面。前往、返回和刷新通过 `BrowserService` 与 Agent 浏览器操作串行执行,
不会创建第二套 Renderer 导航路径。
工具栏状态以浏览器提交后的权威 URL、是否加载中、能否返回和会话活动状态为准;地址输入
在用户编辑期间保留本地草稿,不被状态刷新覆盖;提交未带 HTTP(S) 协议的裸域名时会自动添加
`http://`。创建中、Agent 操作中和完整交互窗口期间禁用其余工具栏控件,但活动导航仍可用
“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也不释放 Runtime 上下文;原“停止
浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在视觉和文案上与停止加载清楚区分。
活动 Conversation 尚无浏览器会话时,可直接从地址栏输入 URL 并“前往”,通过浏览器组件
可用性检查和同一 URL 策略创建会话。该手动路径不读取 Agent 浏览器开关。实现复用现有每
Conversation 串行队列和有界状态,不引入恢复、版本化或第二套会话框架。
### P0:百度输入时元素引用失效,已修复
@@ -123,23 +154,34 @@ Linux x64 和 Linux arm64。
区分代理、分区、窗口、CDP 初始化、DNS 或页面导航失败。需要增加有界、脱敏的
阶段错误码,并保留用户可执行的修复建议。
### P1:命名浏览器配置尚未用于执行
### P1:命名浏览器配置尚未用于执行,界面已隐藏
设置中的命名浏览器配置当前只保存未来托管隔离所需的元数据。实际执行仍使用
每个会话随机创建的临时分区,不复用登录状态。
命名浏览器配置当前只保存未来托管隔离所需的元数据,实际执行仍使用每个会话
随机创建的临时分区,不复用登录状态。为避免用户误以为该配置已经生效,设置
界面暂不显示创建、选择、重命名或删除入口;底层数据与兼容接口继续保留。
## 下一步顺序
1. 在便携版真实模型流程中继续验证返回、取消和右侧画面刷新。
1. 在便携版真实模型流程中继续验证 OpenCode、Continue 的导航、返回、取消和右侧画面刷新。
2. 再设计命名隔离 Profile 的持久登录态、引用和清理策略。
3. 分别实现并真机验证 Windows UIA、macOS AX 和 Linux 原生适配器。
## 最近验证
- `npm test`:591 项通过,6 项跳过。
- `npm test`:3496 项通过,53 项跳过。
- `npm run typecheck`:通过。
- `npm run lint`:通过。
- `npm run build`:通过。
- `GOODBUDDY_RUN_RUNTIME_E2E=1 ... -t "calls the assigned built-in browser
through bundled OpenCode"`:通过,真实模型上报 2 次模型调用,OpenCode 经请求级
GoodBuddy MCP 调用 `browser_navigate` 并保留正确 Conversation 归属。
- `GOODBUDDY_RUN_RUNTIME_E2E=1 ... -t "calls the assigned built-in browser
through bundled Continue"`:通过,Continue 经同一路径调用 `browser_navigate` 并
返回预期结果;当前 Continue Host 为该完整工具轮次上报 1 条聚合模型用量事件。
- 聚焦的能力迁移、网关、IPC、设置界面测试共 234 项通过。其中覆盖 Agent 浏览器
开关关闭时用户手动浏览器操作仍可用。
以上自动化结果覆盖协议、安全边界和模拟驱动,不替代真实 Electron
BrowserWindow/WebContentsView、目标网站和目标 Linux 桌面环境的集成验证。
本轮曾尝试用隔离用户目录启动第二个 Electron 实例,但当前已运行的正式 GoodBuddy
单实例锁阻止了该实例继续运行;未关闭或附着用户正在使用的正式实例。
@@ -0,0 +1,11 @@
# 持续学习
持续学习定义 GoodBuddy 如何从运行结果、评估证据和人工反馈中形成可验证的改进闭环。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [持续学习与评估门 PRD](./prd.md) | 产品目标、学习边界、评估门和验收要求 |
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
+410
View File
@@ -0,0 +1,410 @@
# 持续学习与评估门 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中,远期能力 |
| 版本 | 0.3 |
| 日期 | 2026-08-19 |
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[并行实验 PRD](../parallel-experiments/prd.md)、[分区记忆 PRD](../memory/prd.md) |
## 1. 背景
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。它与分区记忆的协同尚未
实施,也不承担持续学习、模式挖掘或自动改进:
- 候选是否改善未来行为没有评估。
- 一条反思是否会被检索和使用并不确定。
- 没有 Baseline、回放集、Shadow、晋升和回滚。
- 没有持续监控候选生效后的收益与退化。
- 如果允许系统直接修改 Prompt、Skill 或规则,可能发生静默劣化。
持续学习必须建立为可观察、可评估、可批准、可回滚的闭环,而不是“让模型自动改自己”。
## 2. 产品定义
```text
Observe
→ Propose candidate
→ Validate structure and safety
→ Evaluate against baseline
→ Shadow
→ Promote with approval
→ Monitor
→ Keep, revise, rollback or archive
```
学习产物只有在改变未来行为后才算生效;只保存一条 Reflection 仍属于记忆候选。
## 3. 已确认的产品决策
1. 评估门必须先于任何自动应用能力上线。
2. 新候选默认 `candidate`,通过离线评估后先进入 `shadow`。
3. 第一阶段只允许人工晋升。
4. 每次晋升必须记录 Baseline、候选、评估结果、作用域和回滚版本。
5. 学习不能修改安全边界、工具审批、目录权限、沙箱或 Electron 配置。
6. 失败案例和用户负反馈只作为评估数据,不直接成为新规则。
7. 回放案例必须脱敏、版本化,并得到用户明确选择或来自仓库公开样例。
8. 模型评估不是唯一真值,优先使用确定性验收和人工反馈。
9. 生效后的候选继续监控,发生退化可自动停用,但不能自动换上另一个候选。
10. 没有足够证据时保持 `inconclusive`,不强行晋升。
## 4. 学习产物
首期只支持:
| 产物 | 作用 | 是否可自动应用 |
| --- | --- | --- |
| Memory | 改善相关上下文召回 | 否,人工确认 |
| Automation Template | 改善目标、步骤、提示或预算默认值 | 否,创建新草稿 |
| Prompt Variant | 用于实验比较 | 否 |
| Rubric | 改善评估标准 | 否 |
| Retrieval Preference | 调整特定 Automation 的检索配置候选 | 否 |
后续评估:
| 产物 | 风险 |
| --- | --- |
| Skill | 可能扩大行为和工具使用 |
| Procedure | 可能长期影响多个任务 |
| Non-security Rule | 可能阻断或改变行为 |
| Agent Preference | 可能产生难以解释的个性漂移 |
永久禁止自动学习修改:
- 工具权限和审批策略。
- 工作区根目录和文件访问范围。
- 网络、远程消息和电脑控制权限。
- Electron 安全设置。
- API Key、凭据和 Provider Endpoint。
- 删除、支付、发布和生产操作政策。
## 5. 候选来源
- 用户对回答、任务或 Supervisor 意见的显式反馈。
- 用户对心跳报告或建议的显式反馈。
- Task 执行的成功与失败比较。
- 并行实验结论。
- 回放评估发现的稳定差异。
- 用户手动创建。
候选必须包含:
- 作用域。
- 产物类型。
- 来源证据。
- 预期改善的指标。
- 可能影响的行为。
- 风险级别。
- Baseline 引用。
- 建议的评估集。
模型不能仅凭一条成功案例宣称“已学习”。
## 6. 状态机
```text
candidate
→ evaluating
→ rejected
→ inconclusive
→ shadow
→ awaiting_approval
→ promoted
→ paused
→ rolled_back
→ archived
```
| 状态 | 含义 |
| --- | --- |
| `candidate` | 尚未评估 |
| `evaluating` | 正在运行离线评估 |
| `rejected` | 明确退化、安全不合格或无效 |
| `inconclusive` | 证据不足 |
| `shadow` | 计算候选决策但不影响真实行为 |
| `awaiting_approval` | 达到晋升标准,等待用户 |
| `promoted` | 已作为指定作用域的当前版本 |
| `paused` | 暂停影响,保留版本 |
| `rolled_back` | 已恢复前一版本 |
| `archived` | 不再评估和使用 |
## 7. 评估案例
### 7.1 案例来源
优先级:
1. 仓库内公开、无隐私的固定评测样例。
2. 用户手动创建的案例和期望。
3. 用户明确选择并脱敏的历史会话或任务。
4. 实验中产生、经用户批准保留的案例。
禁止默认采样所有私人会话用于学习。
### 7.2 案例结构
```ts
type EvaluationCase = {
id: string
suiteId: string
input: EvaluationInput
assertions: EvaluationAssertion[]
forbiddenBehaviors: EvaluationAssertion[]
source: EvaluationCaseSource
sensitivity: 'public' | 'private_local'
version: number
}
```
断言可以是:
- 输出符合 Schema。
- 包含或不包含确定文本模式。
- 引用来自允许知识库。
- 不调用工具。
- 任务状态和成果存在。
- 测试命令通过。
- 人工评分。
- 模型 Rubric 分项。
### 7.3 冻结
一次评估冻结:
- 案例版本。
- Baseline 版本。
- Candidate 版本。
- Runtime 和模型。
- 知识、记忆和工作区快照。
- 预算。
- 评估器版本。
设置变化不改变已开始的评估。
## 8. 评估门
### 8.1 判定
```ts
type GateVerdict = {
decision: 'reject' | 'inconclusive' | 'shadow'
baselineMetrics: MetricValue[]
candidateMetrics: MetricValue[]
regressions: Regression[]
caseIds: string[]
evaluatorVersions: string[]
notes: string
}
```
最小规则:
1. 任何安全、权限或硬约束退化立即 Reject。
2. 确定性质量指标不能低于配置阈值。
3. 成本和延迟退化必须在允许范围。
4. 开放质量指标至少非退化,或收益足以覆盖明确成本。
5. 案例数或评估器不足时 Inconclusive。
6. 通过离线门只进入 Shadow,不直接 Promote。
### 8.2 Baseline
Baseline 是当前已生效版本或明确的无候选行为。不能用另一个同时变化的实验配置充当 Baseline。
### 8.3 多模型评估
模型 Rubric 可使用与被评候选不同的模型,但必须:
- 固定版本和提示。
- 隐藏候选身份。
- 随机化顺序。
- 保存分项和证据。
- 在关键晋升中结合确定性或人工评估。
## 9. Shadow
Shadow 模式:
- 接收与当前真实行为相同的有界输入。
- 计算候选会做出的选择或输出。
- 不调用有副作用工具。
- 不替换用户看到的结果。
- 不写入长期记忆。
- 保存与实际结果可比较的指标。
对于成本较高的候选:
- 只对抽样的已授权案例运行。
- 用户可设置月度调用上限。
- 系统繁忙时延后。
Shadow 达到配置的最小观察数且无安全退化后进入 `awaiting_approval`。
## 10. 晋升
晋升对话框必须显示:
- 候选将改变什么。
- 作用域和受影响计划。
- 来源。
- Baseline 与 Candidate 指标。
- 失败案例和不确定性。
- 额外成本。
- 回滚版本。
用户可以:
- 晋升。
- 继续 Shadow。
- 拒绝。
- 缩小作用域后重新评估。
晋升采用原子版本切换。不能在一半对象上成功、一半失败。
## 11. 上线后监控
监控:
- 使用次数。
- 成功、失败和无结论。
- 确定性指标。
- 用户采纳、撤销和负反馈。
- Token、耗时和工具调用变化。
- Supervisor 警告变化。
自动暂停条件:
- 安全或权限硬约束失败。
- 确定性错误率超过阈值。
- 连续崩溃或格式失败。
- 成本超过批准上限。
自动暂停只恢复到上一已批准版本,并通知用户。系统不能自行选择新候选替代。
## 12. 回滚
- 每个 Promoted 产物有不可变版本。
- 保存前一版本和作用域绑定。
- 一键回滚使用原子切换。
- 回滚不删除失败版本,保留指标和原因。
- 当前有运行使用该版本时,只影响下一次 Run;紧急安全暂停可取消尚未开始的 Run。
- 被回滚候选再次晋升必须重新评估。
## 13. 衰减与归档
- 长期未使用的候选和 Shadow 可归档。
- Promoted 产物不因时间静默删除。
- Memory 类型遵守分区记忆的衰减规则。
- 评估案例变化后,相关候选标记为“评估过期”。
- 模型或 Runtime 大版本变化时,可要求重新回放。
- 归档保留不含私人正文的指标和版本元数据。
## 14. 信息架构
若远期验证确有集中学习管理需求,应提供独立且可审计的“学习”视图,而不是放入智能心跳、
任务中心或一个尚未确认的自动化中心:
1. **候选**:来源、作用域、预期收益和风险。
2. **评估中**:进度、案例和预算。
3. **Shadow**:观察数、差异和成本。
4. **待批准**:晋升摘要。
5. **已生效**:当前版本、使用量和健康状态。
6. **历史**:拒绝、回滚和归档。
候选详情页签:
- 概览。
- 变更 Diff。
- 评估案例。
- 指标和失败。
- Shadow。
- 版本与回滚。
## 15. 数据模型建议
```ts
type LearningArtifact = {
id: string
scopeKind: 'global' | 'project' | 'automation' | 'agent'
scopeId?: string
kind:
| 'memory'
| 'automation_template'
| 'prompt_variant'
| 'rubric'
| 'retrieval_preference'
status:
| 'candidate'
| 'evaluating'
| 'rejected'
| 'inconclusive'
| 'shadow'
| 'awaiting_approval'
| 'promoted'
| 'paused'
| 'rolled_back'
| 'archived'
payload: JsonValue
sourceRefs: LearningSourceRef[]
baselineVersionId?: string
promotedVersionId?: string
createdAt: string
updatedAt: string
}
```
建议表:
- `learning_artifacts`
- `learning_artifact_versions`
- `evaluation_suites`
- `evaluation_cases`
- `evaluation_runs`
- `evaluation_results`
- `shadow_observations`
- `promotion_events`
- `rollback_events`
## 16. 安全与隐私
1. Apply 层拒绝没有 Gate Verdict 的候选。
2. 产物类型和目标作用域使用代码白名单。
3. 安全与权限配置不在可学习目标白名单中。
4. 私人评估案例只在本地使用,不导出或发送到未授权 Provider。
5. Shadow 不调用有副作用工具。
6. Candidate 内容和评估输出都视为不可信数据。
7. Renderer 不能直接设置 Promoted 状态,Main 验证评估与审批。
8. 删除私人评估案例后清理派生缓存和 Embedding。
9. 日志不记录完整案例、Prompt、回答、文件或凭据。
10. 自动暂停采用确定性条件,不依赖模型自由判断。
## 17. 实施顺序
严格顺序:
1. 建立版本化评估案例和确定性断言。
2. 复用并行实验运行 Baseline 与 Candidate。
3. 实现 Gate Verdict,只有 Reject、Inconclusive 和 Shadow。
4. 实现 Shadow,但不允许 Apply。
5. 实现人工晋升和原子回滚。
6. 实现上线监控和确定性自动暂停。
7. 首先开放 Memory 和 Automation Template。
8. 经过长期验证后再评估 Skill、Procedure 和非安全规则。
不能先做自动改 Prompt,再补评估门。
## 18. 验收标准
- [ ] 没有评估结果的候选无法晋升。
- [ ] 安全、权限或硬约束退化必定 Reject。
- [ ] 评估不足时显示 Inconclusive,不强行选优。
- [ ] Baseline、Candidate、案例、Runtime 和评估器都被冻结和版本化。
- [ ] Shadow 不影响用户结果、不调用副作用工具、不写长期记忆。
- [ ] 晋升前展示收益、退化、成本、作用域和回滚版本。
- [ ] 第一阶段只有用户可以批准晋升。
- [ ] 晋升和回滚采用原子版本切换。
- [ ] 生效后出现确定性严重退化时自动暂停并恢复上一批准版本。
- [ ] 系统不会自动选择另一个候选替代。
- [ ] 私人会话不会默认进入评估集。
- [ ] 安全策略、权限、目录、凭据和 Electron 配置不属于可学习产物。

Some files were not shown because too many files have changed in this diff Show More