Compare commits

..
271 Commits
Author SHA1 Message Date
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
890 changed files with 317381 additions and 30349 deletions
+617
View File
@@ -0,0 +1,617 @@
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"
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" \
--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 }}
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_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"
+353
View File
@@ -0,0 +1,353 @@
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")"
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" \
--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"
+317 -13
View File
@@ -26,7 +26,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
cache: npm
- name: Verify release tag version
@@ -50,7 +50,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
@@ -58,10 +60,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:
@@ -71,7 +130,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
@@ -92,7 +151,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
cache: npm
- name: Cache packaging toolsets
@@ -115,7 +174,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
@@ -131,14 +304,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
@@ -147,7 +328,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.19.0
- name: Verify release tag
shell: bash
@@ -181,7 +362,112 @@ 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_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_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 }}
@@ -196,3 +482,21 @@ jobs:
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
+251 -77
View File
@@ -4,9 +4,72 @@
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.
- 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
@@ -19,7 +82,8 @@ primarily Simplified Chinese.
- `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.
@@ -30,10 +94,25 @@ Keep Electron security boundaries intact:
## Runtime Behavior
- Ask mode must remain read-only at the runtime boundary.
- Execute mode may use tools only through the existing approval controls.
- 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
@@ -41,7 +120,11 @@ Keep Electron security boundaries intact:
## Data and Compatibility
- Preserve existing user data and migrations.
- 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.
@@ -52,12 +135,53 @@ 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.
- Keep UI accessible with labels, keyboard behavior, semantic roles, and visible
focus states.
## Product Design Documentation
- 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
@@ -84,78 +208,84 @@ Keep Electron security boundaries intact:
- Do not show the same event both inline and as an application notification.
Preserve user input and actionable error context when an operation fails.
## Release Packaging
## Commit Messages and Release Notes
- `.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.
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.
### Tagged Release Process
- 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.
Every version-tag release must follow this sequence. A branch-only push does
not require release notes.
Example:
1. Confirm that the user wants a release tag and identify the exact release
commit and the new `package.json` version.
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. Only after approval, verify that `package.json` and `package-lock.json`
contain the same release version, verify the candidate tag does not already
point elsewhere, create `v${package.version}` at the exact approved commit,
and push the branch and tag according to the synchronized-remote rules.
6. 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.
```text
fix: unify project settings across channel entry points
Never create or push a release tag, and never push a previously created
release tag, before the release-note draft has received explicit approval.
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.
- 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 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.
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
@@ -167,14 +297,58 @@ 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.
- 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. When the user requests a release tag, push the new tag
+403 -14
View File
@@ -31,6 +31,10 @@ npm start
## 质量验证
跨功能测试方法见[质量验证索引](./docs/quality/README.md)。桌面界面的测试分层、
原生键盘、截图与计算样式取证、隔离启动和脱敏报告遵循
[Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md)。
提交或打包前运行:
```bash
@@ -51,7 +55,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 +78,19 @@ npm run build
中间构建输出位于 `out`。该目录为生成内容,应修改源文件后重新构建,不要直接编辑。
## 图标生成
应用图标源文件位于 `icons`。修改源图或图标处理逻辑后运行:
```bash
npm run icons
```
脚本会精确裁出亮色和深色圆角卡片,清理圆角外侧背景并使用适合主题的
边缘颜色生成透明抗锯齿,避免缩放后出现白边。它会统一更新 `build` 中的
PNG / ICO、Renderer 图标,以及官网使用的亮色和深色品牌图标。任务栏和
托盘图标保持透明背景。
## 平台打包
### 当前平台默认包
@@ -111,26 +139,250 @@ 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 标记。
### 远程 OpenCode 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 许可证。
远程 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 +390,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 +413,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,8 +527,12 @@ 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 可单独执行无模型、无凭据冒烟测试:
@@ -187,5 +541,40 @@ npm run smoke:deepseek-harness
```
该命令先生成 production bundle,再从 CommonJS Electron 主入口启动实际
`utilityProcess`,等待固定 Host 完成沙箱探测与内部 ready 握手。它不会发起
`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 和应用主程序。
+717 -63
View File
@@ -1,83 +1,737 @@
# GoodBuddy 功能矩阵与路线图
# GoodBuddy Feature Matrix and Roadmap
本文记录 GoodBuddy 的已提供能力和路线图。未完成项目不代表已包含在当前版本中。
**English** | [简体中文](./FEATURES.zh-CN.md)
## 状态说明
This document records the capabilities available in GoodBuddy and its roadmap.
Unchecked items are not included in the current release unless the item says
otherwise.
- [x] 已提供
- [ ] 开发中或规划中,具体以条目标注为准
## Status
## 功能总表
- [x] Available
- [ ] In development or planned, as described by the item
### 桌面基础、工作空间与上下文
## Feature Matrix
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
- [x] **Projects 与独立对话**:按项目隔离上下文,管理会话、附件和 Git 工作区变更。
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式和受控 Mermaid 图表;大图可缩放、拖动或查看源码,失败时保留原始图表代码。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
### Desktop foundation, workspaces, and context
### Agent Runtime 与模型连接
- [x] **Cross-platform desktop application**: Supports Windows, macOS, and
Linux release targets on `x64` and `arm64`.
- [x] **Chinese hardware and operating-system coverage**: Standard Linux
packages target compatible x64 and arm64 environments, including UOS, Kylin,
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
LoongArch has a separate experimental loong64 preview outside standard
releases and automatic updates; no preview is planned for 0.13.0. See the
[preview boundaries](./docs/development/loongarch-preview-build.md).
- [x] **Configurable global shortcut**: Enable, disable, or record an Electron
accelerator under Platform Features / General. The default remains
`CommandOrControl+Shift+Space`; if registration conflicts or saving fails,
GoodBuddy keeps the previously registered shortcuts and shows an actionable
status.
- [x] **Projects, independent conversations, and conversation branches**:
Isolates context by project and manages conversations, attachments, and Git
workspace changes. A stable local conversation can be copied into an
independent branch; the branch retains a source badge but does not copy
Tasks, queues, or artifact ownership. The project selector distinguishes
local, managed SSH, and remote messaging-channel projects. Managed SSH
projects are grouped by Host, with the real Agent connection state on the
Host heading and only the remote path on each project row.
- [x] **Cross-project conversation activity**: A shared activity entry lists
running conversations and conversations needing attention across projects,
including older conversations with live work. Open the associated
conversation directly; project selection also shows activity counts.
- [x] **Compact conversation controls**: Composer options share a compact
settings panel while Runtime, mode, sending, and queue controls remain
available. Conversation search has an inline clear action that restores the
scoped list and returns focus to the input.
- [x] **File, screenshot, window, and clipboard context**: Added to model
context only after explicit user selection.
- [x] **Per-file workspace diffs**: Changed files expose separate staged and
unstaged diffs, including deleted, renamed, and untracked files. Refresh
reloads the selected diff, browsed directory, and expanded directories; more than 50 changes
remain accessible through incremental loading.
- [x] **Workspace file and Git management**: Create, rename, move, and delete
files or directories, inspect properties, search and switch local or remote
branches, create branches, explicitly Fetch, and browse commit history and
per-file commit diffs. File and Git controls are grouped by view, branch
search uses the shared compact form style, and subdirectories retain text
breadcrumbs without an isolated root icon. Commit history stays docked at
the bottom, initially expands to half the Git content area, and supports
resizing with independently scrolling panes and one shared refresh action.
Project switches automatically detect Git without reusing another project's
status. Moves stay inside the workspace,
deletion requires confirmation, and branch conflicts never trigger automatic
stash or discard. Managed SSH operations require Agent `0.11.23`.
- [x] **Rich responses**: Supports GitHub Flavored Markdown, LaTeX math,
constrained Mermaid diagrams, and static in-conversation HTML previews.
Complete HTML and HTML code blocks can be previewed in place after an Agent
reply finishes, with icon actions to reveal the source or open a full-screen
preview. The feature is on by default and can be disabled under Platform
Features / General; previews cannot run scripts, access the network, submit
forms, or open windows.
- [x] **AI response and full-conversation copy**: Completed AI responses expose
a bottom action that copies Markdown source without reasoning, tool logs, or
citation metadata. Full-conversation copy uses the same validated
Preload/Main clipboard path.
- [x] **Assistant workbar, multiple terminals, and resizable layouts**: The
right workbar uses a persistent “+” capability catalog and application tabs.
Task Center and Workspace are permanent singleton tabs; browser tabs are
independent instances. Task Center can show the current project, global
tasks, or all projects, and retains the selected scope with the layout.
Users can open multiple independent terminals for the current local or
managed SSH project with bounded output, resizing, termination, and explicit
reconnection. Closing a terminal tab ends its Shell; restarting the app
restores only an ended tab description and never restarts the Shell. The
main sidebar and Magic Notes list support pointer and keyboard resizing;
the notes list can be hidden, remembers its layout, and stacks at narrow
widths.
- [ ] **Project Agent Space** (planned): Unifies roles, knowledge, Skills/MCP,
models, approval policy, budgets, and timeouts in a Project, with reusable
templates.
- [ ] **Application navigation and system-tool shortcuts** (design only):
Plans fixed system entries, configurable built-in application visibility,
and footer shortcuts into the existing workbar. These are not implemented
interface changes. See the [design](./docs/features/application-tool-navigation/README.md).
- [ ] **Additional assistant workbar and execution-space capabilities**
(planned): Builds on the current workbar and multiple terminals with
supervision, unified Runtime monitoring, managed processes, safe static HTML
preview, target-pinnable workspace/browser/artifact instances, bottom
docking, and separate windows. Task Center remains the singleton Task index;
attachments and knowledge remain in the conversation composer, while memory,
when implemented, and historical execution context belong to the associated Task. See
the [Feature PRD](./docs/features/assistant-workbar/prd.md).
- [ ] **ShareServer Office co-editing** (design): Uses an optional ShareServer
integration with ONLYOFFICE Docs to open multiple filename-labelled document
tabs in the assistant workbar. The planned scope covers DOCX, XLSX, and PPTX
manual editing, save and undo, source-file conflict protection, and later AI
selection edits. The selected ShareServer and editor process document content;
Desktop neither bundles nor starts Document Server and does not claim offline
editing when no service is configured. See the
[design](./docs/features/office-document-editing/README.md).
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行和图像生成。
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、超时和活动记录。
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 运行已启用且受边界约束的工具。
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析。
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Chat Completions、OpenAI Images 和无认证本机模型。
- [x] **Main-only 凭据保护**:API Key 使用系统安全存储加密,不暴露给 Renderer。
- [ ] **可执行 Subagent**(规划中):提供显式 Execute 委派,限制嵌套、并行、Token、时间和工具权限,并保留父子任务审计。
### Agent Runtimes and model connections
### Skills、MCP 与知识库
- [x] **Direct model Runtime**: Supports question answering, knowledge
synthesis, controlled tool execution, image generation, and reference-image
editing through providers supporting the OpenAI-compatible image-editing
endpoint. Editing sends attached images as multipart data and still requires
validated inline image output rather than fetching provider-returned URLs.
Follow-up image requests reuse the latest successful image and conversation
text, including after reopening a saved conversation; explicit attachments
take priority. Missing editing support does not block text-based generation:
the affected reply shows a quiet footer explaining that reference images were
not used. See [image generation](./docs/features/image-generation/README.md).
- [x] **Direct model programming agent**: Local direct text models can run the
platform Shell in Execute mode and delegate one level of programming
Subagent work while inheriting the parent request's mode, model, workspace,
and capability scope. OpenCode, Continue, DeepSeek Harness, and managed SSH
do not receive duplicate copies of these tools. The Windows local command
path and a real-model edit, test, fix, and review loop have passed; native
macOS and Linux command validation remains. Execute commands can select
absolute, relative, or symbolic-link directories outside the workspace;
the workspace stays the default and relative-path base. Ask remains read-only.
- [x] **OpenCode and Continue**: Use isolated child processes, an environment
variable allowlist, unified configuration, cancellation, startup and control-request
deadlines, bounded streaming output, and activity records. Shared process
cleanup preserves complete Windows process-tree termination and terminates
POSIX process groups when a child uses an independent process group. Chat
status checks use a one-shot Runtime probe that is immediately cleaned up
rather than entering the model-and-project execution cache. For managed
local OpenCode, this passive check validates the selected path and model
credential without starting a throwaway server; the explicit settings test,
native inventory still performs full startup and health checks, then shares
that execution Runtime with the first real request instead of cold-starting
another server. Execution Runtimes remain reusable per project, while local
OpenCode configuration dependencies and content-addressed Skill snapshots are
shared globally across projects. Session data, tool output, and request-scoped
MCP state remain isolated. GoodBuddy-managed local OpenCode keeps
requests in the same conversation ordered, while different conversations in
the same project and different projects can run in parallel. Request-scoped
dynamic MCP tools remain isolated through default wildcard disablement and
explicit current-request enablement. Managed local and remote OpenCode skip
unused automatic Git snapshots to avoid synchronous diff stalls; file tools,
subagents, and workspace Git diffs remain available. Continue continuously
drains unused utility-host stdout so console output cannot fill its pipe.
These changes apply to newly started Runtimes, not external OpenCode Servers.
OpenCode and Continue runs do not stop
at a fixed tool-call or activity count, and every observed tool and Subagent
activity remains in the local conversation instead of being discarded while
the native task continues.
Interactive questions are answered only by foreground conversations; scheduled tasks, remote channels,
delegated work, and other background runs fail immediately with guidance to
rerun in the foreground
instead of waiting indefinitely. Local OpenCode startup uses the selected
absolute path directly without checking the file or running `--version`
first; invalid paths fail at actual launch. On managed Linux ARM Hosts,
Runtime activation reuses the registry and manifest verified during setup
instead of rehashing or rechecking the complete OpenCode binary.
- [x] **Managed SSH OpenCode loop (technical preview)**: Controlled by the
separate Remote Projects (Technical Preview) tab under Settings / Platform
Features and disabled by default. Disabling it does not affect local
projects, ordinary desktop capabilities, or desktop releases. When enabled,
users can manage SSH Hosts with pinned Host Keys, browse bounded remote
directories, and create Ask or Execute projects. Both modes start the
signed Runtime directly without bubblewrap. Ask applies OpenCode's Ask
permission configuration and permits only native read approvals at the
Agent tool-dispatch boundary, while Execute explicitly allows native permissions
and retains all permissions of the selected SSH account. The Agent owns accepted Prompts,
provider/tool rounds, Runtime processes, a stable model ledger, and a bounded
semantic transcript over a private Unix socket and ACP v5. Work continues on
the Host after Desktop exit, network loss, or local-process termination.
Reconnection attaches only to the original controller, binding, and
operation, then atomically merges provenance into the original conversation
without resending the Prompt. Model profiles and API keys enter Agent memory
only for the accepted operation and are not written to the Renderer, SSH
arguments, remote environment, or disk. Remote components are not embedded
in the desktop package; switching projects updates only local selection and
does not connect or download. Settings reads a small signed catalog and
reports the local version, latest compatible online version, and update
state for Linux x64, Linux arm64, and macOS arm64. Intel Macs are not supported.
Downloads occur only after a user action, and
packages can also be imported or exported offline. Each compound `.gbagent`
package contains the Agent, fixed Node, and the compatible OpenCode Runtime
maintained by the desktop source. Online sources follow the GitHub/Beijing
OSS choice in About and Updates. The cumulative signed catalog binds minimum
Desktop version, Agent protocol, platform, architecture, size, SHA-256, and fixed URL.
All three targets share one catalog. Users must upgrade Desktop before the
first mixed-platform catalog is published because older Linux-only readers
reject catalogs containing Darwin entries.
The public-key registry accepts equivalent JSON whitespace and line endings
while still strictly validating schema, Ed25519 keys, environment, and
revocation; catalog, package, manifest, payload signatures, and streaming
SHA-256 remain unchanged. A missing architecture package disables managed
SSH only for that architecture. Projects store only Host, remote path,
Runtime selection, and mode. Current Host identity is read when a
Workspace/Runtime is first used, then reused by other projects in the same
process. Managed SSH conversations expose only supported OpenCode choices.
Multiple projects and conversations can run concurrently on one Host;
Agent-owned prompts are not stopped at a fixed model-call count or
prompt-wide output-token total, and Runtime output uses transport
backpressure instead of cancellation when GoodBuddy buffers fill;
recovery separately reports network, Agent, Runtime, committed-event cursor,
completion, or failure/retry and blocks only the affected project. A real
Linux x64 Host has covered short and long detach, forced local harness exit,
concurrency, SSH relay loss, cancellation, definite and uncertain provider
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
database. Successful tool START/END events appear exactly once, with no
Prompt, provider, or tool replay observed. The current Agent source lock is
`0.11.24`, while the current Desktop release candidate is `0.13.0`; formal
publication status follows the separate Agent and Desktop
release channels. Previous macOS validation covered native package installation,
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
separate process groups on a real Host; this does not imply publication.
Agent `0.11.23` adds remote workspace management and optional model limits;
its bundled Runtime no longer imposes a fixed ten-minute Prompt deadline.
Unlimited request duration retains a separate connection timeout. Agent
`0.11.23` requires Desktop `0.12.11` or later.
Agent `0.11.24` requires Desktop `0.13.0` and fixes native question routing
and sending another message after cancelling a pending question.
- [x] **Manual SSH Host environment provisioning source path**: After Host Key,
authentication, and system probes succeed, GoodBuddy saves the Host and
read-only probes the shared Agent/Runtime. Saving a Host or opening a project
never installs automatically. A Host card has one primary action based on
version facts, Install Remote Environment, Update Remote Environment, or
Reinstall, plus a secondary SegmentedControl for Auto, Host Download, or
GoodBuddy Transfer. The selection defaults to Auto and is not persisted; a
Version Matched badge does not mean the environment is healthy. Auto probes
only before operation preparation and chooses one acquisition route.
Explicit choices remain in effect, and prepare, commit, or adoption failures
do not silently fall back across acquisition routes. Both routes deliver the
same signed compound `.gbagent` into fixed staging and share control-plane
prepare, commit, Agent activation/health, Runtime activation, finalize, and
explicit cleanup. The GoodBuddy route can download and validate a missing
candidate during the same operation, cache it with a lease, and stream one
archive plus its verified bootstrap Node over bounded SFTP without loading
the roughly 294 MiB package into a Main-process `Buffer`; the Host performs
one complete payload verification while extracting. An unfinished operation
stores only the operation ID needed to clean staging. A later update makes a
best-effort cleanup and starts prepare again, without persisting a remote
metadata copy or allowing cleanup failure to block a new update or roll back
a healthy environment. Existing projects resolve current Host identity on
demand and run a fixed `attach-or-bootstrap`; registered health,
capabilities, and prompt startup do not scan the full payload. See the
[design](./docs/features/remote-host/environment-provisioning-technical-design.md).
- [ ] **Real-Host acceptance for SSH Host environment provisioning**: The
current source passed isolated Linux x64 package installation, Ask/Execute,
native subagent external writes, reconnection, and stop/bootstrap.
The complete Host-card acquisition matrix across GitHub, Beijing mirror,
Linux x64/arm64, cancellation, and offline GoodBuddy transfer remains.
Candidate CI/native packaging and system sleep/wake are not yet verified;
this partial development evidence does not imply publication.
- [x] **DeepSeek Harness (preview)**: Uses the fixed GoodBuddy Host and an
OpenAI-compatible model connection. It prefers an administrator-provided
connection, otherwise follows the compatible default model or first
compatible connection without requiring a duplicate selection. Settings
displays the actual administrator or fallback model source. Ask permits
only real Host-registered `read` and `skill` tools plus Main-managed Web
Search/Fetch proxies, and rejects plugin impersonation of those names.
Execute allows all enabled built-in and plugin tools with the current user's
permissions. Image input follows the selected model connection's declared
capability: text models reject images before Host or model invocation, while
image-capable models receive bounded inline JPEG/PNG content through a
temporary Attachment Store.
- [x] **DSH npm plugin marketplace**: Disabled by default and searches public
npm `dsh-plugin` packages only after explicit user enablement. It uses the
bundled npm to install exact versions with ordinary lifecycle scripts and
supports enable/disable, JSON configuration, removal, automatic disablement
after startup failure, and offline management of installed plugins.
Disabling the marketplace hides only the catalog and management interface;
it does not change the enabled state of installed plugins, and third-party
code is not subject to Ask initialization isolation.
- [x] **Ask and Execute work modes**: Ask remains read-only. Execute is the
user's authorization for all tools, processes, network access, and writable
paths available to the current local or SSH account, including paths outside
the workspace and native subagent work.
- [x] **Efficient direct-model workspace tools**: Direct models use bundled
ripgrep for compact file discovery and content search, read large UTF-8 files
by line, and apply multi-file patches in Execute. Ask can search and read but
cannot patch or run commands; packaged ripgrep does not require a system install.
Actionable read/search errors let the model correct its arguments and
continue. Partial search results remain available with an explicit
incomplete-coverage warning.
- [x] **Long context and paged tool output**: Message history, long replies,
and parsed document text no longer use the previous fixed truncation limits.
Direct models can continue reading stored command and Subagent output in
pages; the selected model's context window and bounded transports still apply.
- [x] **Native Runtime interaction routing**: OpenCode and Continue questions
support choices, yes/no, free-text answers, and skipping through the existing
question card. Local and managed SSH OpenCode also route questions from
owned child sessions. Managed SSH requires Desktop `0.13.0`, Agent `0.11.24`,
and a newly started managed Runtime;
this does not extend question support to arbitrary ACP services.
Successful answers and skips retain the original questions and answers
across rounds and local conversation reloads; answers discarded by older
versions cannot be recovered.
Concurrent questions wait in order, duplicate events preserve drafts, and
failed submissions remain retryable. Cancelling a pending managed SSH
question allows another message in the same conversation.
Execute permission confirmations are handled automatically rather than
waiting for another approval. See the [interaction boundaries](./docs/features/assistant-workbar/runtime-interactions.md).
- [x] **Experts and Subagents**: Supports explicit experts, team analysis, and
up to three experts running in parallel. Experts inherit the parent
Ask/Execute mode and can use enabled local direct-model tools; Ask remains
read-only. They are not remote OpenCode child sessions. Chat shows each
expandable full expert response first and the main Agent's synthesis below,
and persists both with the conversation.
- [x] **OpenCode child progress and final results**: Child cards show ordered
text, reasoning, and tool progress separately from the final result. Remote
live progress requires Agent `0.11.20`; older packages still expose final
results without reconstructing missing progress.
- [x] **OpenCode event-stream cleanup**: Chat requests and native context
compaction close their own subscriptions before ending event iteration,
including completion, failure, cancellation, and early consumer exit,
without cancelling parallel conversations.
- [x] **Accurate message-footer status**: Distinguishes request preparation,
retry waiting, retry dispatch, tool activity, pending answers, and terminal
states. Local OpenCode reports native retry attempts and scheduled times;
direct models report their own backoff phase. Footer dots are static, and
unsupported Runtime retry details are not invented. Conversation-list
indicators and send/stop behavior remain unchanged.
- [x] **Simplified Runtime selection**: Conversation and project menus no
longer enumerate every Runtime/model combination. Configure Runtime models
in system settings; saved fixed project selections remain usable and
explicitly display their fixed model.
- [x] **Role-bound model connections**: Each role can inherit the default model
or select an independent text-model connection. Invalid connections safely
fall back to the default; the synthesis role always inherits the default.
- [x] **Multiple model protocols**: Supports Anthropic Messages, OpenAI
Responses, OpenAI Chat Completions, OpenAI Images, and unauthenticated local
models. New users receive a local Ollama-compatible default and no
preconfigured third-party cloud provider. During upgrades, GoodBuddy
replaces a historical default only when it has never received credentials
and still exactly matches the old built-in value; explicitly saved
connections and encrypted credentials remain unchanged. Existing
deployments that supply credentials only through compatible environment
variables retain their historical connection parameters, while generic
model environment variables still take precedence. Save and Test Model sends
a bounded real text or image-generation request and validates the generated
result instead of testing HTTP reachability alone, so it may incur a small
provider usage charge.
- [x] **Custom model request fields**: Each LLM connection accepts bounded
JSON objects for additional request headers and top-level body fields.
Direct requests and Continue support both; local OpenCode and DeepSeek
Harness use only their natively supported headers, while the managed SSH
OpenCode model gateway supports both. Runtime, protocol, and authentication
fields take precedence, and these ordinary connection settings must not be
used to store API keys or other secrets. See
[model request customization](./docs/features/model-connections/README.md).
- [x] **Context usage and automatic compaction**: Direct models update usage
from each successful provider call. Images and tool rounds use the same
accounting, with estimation only when the provider omits usage. The UI
distinguishes This Model Call from Post-Compaction Conversation Estimate,
and the compaction line is recalculated from current settings and the
selected model window rather than storing old configuration per
conversation. Before/after compaction markers use one estimation method,
while activity history retains provider usage for each call. Conversations
and multi-round tool Agents can compact repeatedly after a completed call
crosses the threshold, reserving budget first for fixed prompts, tool
definitions, and summaries. One response separately preserves compaction
markers for Agent tool context and conversation history, and summaries remain
reusable after app restart or when older messages leave the local history
window.
- [x] **Persistent credential protection**: Main encrypts API keys with secure
system storage and never exposes them to the Renderer. A managed SSH accepted
Prompt places the current profile and key in Agent memory only, never in SSH
arguments, the remote environment, or disk; other paths keep credentials
inside Main. Credentials remain attached to their model connection when the
service URL changes or authentication is temporarily disabled, and are
removed only when the user explicitly clears them or deletes the connection.
- [x] **Bounded failure diagnostics**: Desktop stores a fixed allowlist of
startup, Runtime, and remote-connection failure stages in the user-data
directory, rotating at four 256 KiB files. Each GoodBuddy Agent rotates
daemon, connection, recovery, and Runtime lifecycle diagnostics at three
64 KiB files in its private installation state and exposes them through the
fixed `diagnostics --installation-id` command. Both sides store only
allowlisted stages, stable error codes/types, and fixed short messages, never
Prompts, credentials, file contents, paths, environments, SSH arguments, or
raw provider responses. Diagnostic-write failure does not alter normal
operation.
- [x] **OpenCode Runtime customization**: GoodBuddy-managed OpenCode can
discover native Agents, Tools, Commands, LSPs, Formatters, MCP, Skills,
Prompts, and Resources. Tools show read, file-edit, command, network, Agent
orchestration, and other types, source, and Ask/Execute availability while
hiding OpenCode's internal `invalid` tool and temporary GoodBuddy MCP tools.
Users can save a default Agent, override it per request, run Commands through
the native SDK, view context usage, and invoke native Compact with a total
timeout. Questions from concurrent external Server conversations are mapped
by public request ID so answers cannot cross conversations. External
OpenCode Servers report connection state only and do not claim native
catalog access. Arbitrary plugin installation, Session Share, automatic
Worktree, and OpenCode native-session persistence remain unavailable.
- [x] **Continue Runtime customization**: Exposes native Rules, prompt
templates, and MCP from static configuration plus editable GoodBuddy Rules
and Prompt presets. Chat can select a preset per request and insert an
editable Prompt. The current Continue Host has no trustworthy static native
Tool discovery API and runs with an isolated `CONTINUE_GLOBAL_DIR`, so the UI
explicitly marks static Tool discovery unsupported and does not present
workspace or user Skills that the Host will not load as native capabilities.
GoodBuddy-assigned Skills still execute through per-request staging. The
temporary Continue Host does not reuse native session compaction; manual
compaction uses the GoodBuddy summary model and verifies persisted summary
coverage. Agent questions become unified question cards. Resources, Hooks,
background Jobs, and Continue native-session management remain deferred.
- [x] **Native Runtime catalog semantics**: Native capabilities are displayed
across 11 tabs: Agents, Tools, Commands, Skills, MCP, Rules, Prompts,
Resources, LSP, Formatters, and Context. Catalog state is independent from
Runtime connectivity and distinguishes complete, partial, unavailable,
connection-only, and unsupported. DeepSeek Harness enumerates bounded
built-in/plugin Tools and Skills through the Host Registry, reports real
Ask/Execute boundaries, and excludes per-request GoodBuddy Skills and
Web/MCP proxies.
- [ ] **Runtime supervision section** (planned): A fixed section in the
application-level assistant workbar for Task-level delegation, background
execution, Workflows/Hooks, long-running work, and native-session
supervision across OpenCode, Continue, and DeepSeek Harness. Users select
only Conversation or Task; Job/Run remains internal rather than becoming a
tree or independent object.
- [ ] **Advanced Subagent supervision** (planned): Task-level aggregation in
the workbar's fixed Runtime section, configurable nesting, parallelism,
budgets, and lifecycle controls. Basic expert-mode inheritance and
single-level direct-model programming delegation are already available.
- [x] **Skills 按需接入**:使用有界资源和受控 Runtime 边界。
- [x] **MCP Tools**:直连模型可使用显式启用的 MCP Tools,并可在模型轮次间按需刷新动态 MCP 工具。
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
- [x] **混合检索测试台**:支持全文、中文词组、向量和图谱通道诊断,可调 Top K、阈值、权重、本地或学习型重排及上下文预算。
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;创建、保存和评论结果使用统一应用通知。
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
### Skills, MCP, and knowledge
### 工作管理、长期协作与工作流
- [x] **On-demand Skills**: Assignable to direct models, OpenCode, Continue,
and DeepSeek Harness with bounded resources and controlled Runtime boundaries.
- [x] **Local tool-execution environment source path**: Under Capabilities and
Tools / Tool Environment, users can select GoodBuddy-managed Node.js,
on-demand managed Python, or a genuinely validated custom interpreter for
local Skills and stdio MCP. The page provides an independent Native/OSS
source choice, diagnostics, installation progress, cancellation, and
removal. New local Runtimes and stdio MCP processes receive immutable PATH
snapshots without changing ordinary terminals, the system environment, or
remote Hosts. Managed Node and native-source Python have passed real
installation validation on Windows x64. Managed Python archive validation
follows the target filesystem: Linux preserves valid case-distinct paths,
while every target still rejects exact duplicates and unsafe entries.
- [ ] **Local tool-execution environment release acceptance**: All six
platform/architecture OSS mirror objects have passed public byte, size, and
SHA-256 verification. Managed Python remains an on-demand download and does
not add license files to the Desktop package. Each standard package job uses
a native target-architecture Runner and performs a real managed-Python
install, SSL, pip, and venv probe before packaging. The immutable `v0.12.0`
attempt passed Windows and macOS but exposed Linux TAR case handling before
publication. The immutable `v0.12.1` attempt was cancelled before native
packaging while synchronizing the Agent release. Published `v0.12.2`
subsequently passed all six native package jobs and their managed-Python
install probes. Real Skill/MCP execution, custom interpreter execution, and
coordination with affected running processes remain
acceptance work. Six-platform release acceptance must not be claimed before
the applicable checks pass.
- [x] **On-demand built-in MCP**: Knowledge, Magic Notes, GoodBuddy
configuration, and built-in browser MCP can be enabled independently and assigned to direct
models, GoodBuddy-managed OpenCode, and Continue. Settings explicitly marks
DeepSeek Harness unsupported. Built-in MCP uses short-lived local authority
for the current request, and user configuration cannot loosen Ask/Execute
read/write boundaries.
- [x] **MCP Tools**: Explicitly enabled custom MCP can be assigned to direct
models, GoodBuddy-managed OpenCode, Continue Agent Execute, and DeepSeek
Harness, and loads only in Execute. Agent child processes receive only
per-request local-loopback authority; MCP addresses, commands, and
credentials remain in Main. Dynamic tools pass through discovery and the
existing activity and permission boundaries; healthy direct-model
and Harness calls reuse the discovered request catalog instead of
rediscovering it before every call.
- [x] **MCP Prompts and Resource metadata**: MCP testing discovers bounded
Prompt, parameter, and Resource metadata only when the Server declares the
corresponding capability and does not read Resource content. Supported
Prompts can be inserted into the chat draft for further editing. OpenCode can
report an experimental Resource catalog; the current Continue version
explicitly does not support Resources.
- [x] **Local knowledge bases**: Supports file, directory, and web imports,
SQLite FTS5 retrieval, and source tracing. Knowledge selection persists per
conversation and starts empty for new conversations. Advanced creation and
retrieval parameters are collapsed by default. Documents distinguish ready,
processing, and failed states, with source opening, retry, and confirmation
before source removal.
- [x] **External knowledge-base connections**: Manage Dify, FastGPT,
and RAGFlow instances from the Knowledge page, then discover remote
knowledge bases or enter their IDs and verify bindings with provider-specific
retrieval settings and citations. Conversations support on-demand and
required pre-answer retrieval. External systems provide retrieval only:
GoodBuddy does not use
their App, Chat, Workflow, or Agent APIs, and does not bulk-sync, locally
index, or modify remote content. Bounded cited snippets are retained locally
with their conversations. See the
[external knowledge-base PRD](./docs/features/knowledge-base/external-knowledge-prd.md).
- [ ] **External knowledge-base acceptance**: All three providers have passed
real service, local HTTP MCP, and production-IPC short-answer checks; Dify
additionally passed the complete App composer-to-answer-and-citation path.
FastGPT/RAGFlow desktop-specific parameters, complex multi-library answers,
and remote pre-answer retrieval remain unverified. See the
[validation evidence and remaining work](./docs/features/knowledge-base/progress.md).
- [x] **Knowledge graph**: Supports rule-based, model-based, and hybrid
extraction plus entity, relationship, alias, and evidence maintenance.
- [x] **Embedding configuration and retrieval**: Configures compatible
Embeddings endpoints and uses them for semantic retrieval.
- [x] **Embedding diagnostics and indexing jobs**: Provides real embedding
diagnostics, per-document rebuild progress, cancellation, failure state, and
result recovery after restart. Each successful document becomes immediately
searchable.
- [x] **Hybrid retrieval workbench**: Diagnoses full-text, Chinese phrase,
vector, and graph channels with configurable Top K, thresholds, weights,
local or learned reranking, and context budgets.
- [x] **Chunking, maintenance, and evaluation**: Supports fixed, structured,
and parent-child chunking, chunk maintenance, cancellable rebuilds, and
bilingual retrieval evaluation.
- [x] **Controlled knowledge ontology**: Each knowledge base can define
entities, relationships, aliases, and endpoint constraints while retaining
evidence offsets, confidence, and extraction source, with explicit guidance
to rebuild the graph.
- [x] **Required retrieval and cited context**: Conversations can retrieve on
demand or before every response, show zero-result, degraded, failed, and
cancelled states, and expose cited context or safely open its source.
- [x] **Magic Notes**: A local-first notes and todo workbench with scope
management, editing, filtering, and controlled AI comments. The todo view
displays the source entry inside the selected item's details, without
showing the previous item's source after a selection change. Failed reads
use application notifications, and missing sources have an explicit fallback. The left
navigation can show the incomplete-todo count, and create, save, and comment
results use application-wide notifications.
- [ ] **MCP Server Control Plane** (planned): Unified MCP lifecycle, health
checks, reconnection, schema cache, isolation, approval, and audit.
- [ ] **Traceable note excerpts and AI editing** (planned): Collects sourced
excerpts from conversations, knowledge, and the web and provides
confirmation-based summarization, rewriting, and organization.
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;活动按会话分组并默认收起,避免长历史占满页面。
- [x] **记忆与智能心跳**:提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
- [ ] **可视化受控工作流**(规划中):提供版本化 DAG、条件分支、审批、取消和恢复,执行节点继续经过 Main Runtime 边界。
- [ ] **统一 Run Graph 与回放**(规划中):关联任务、Subagent、模型、知识、工具审批、用量和成果,支持失败定位、重试和脱敏导出。
### Work management, long-term collaboration, and workflows
### 浏览器、通信、语音与应用维护
- [x] **Tasks, activity, and artifacts**: Centrally manages task state, audit
activity, and independent artifact files. Ordinary chat responses remain
only in the conversation and are no longer copied into Artifacts; existing
duplicate chat Markdown is hidden from the artifact list but not physically
deleted. Run history is stored in Main SQLite without the previous 500-item,
4,000-character, or 2 MB Renderer limits; the page renders it in batches,
without a persistent legacy-cache truncation warning; previously lost
history is not restored. Token usage is grouped by Runtime and
model and normalizes the different OpenAI-compatible and Anthropic Messages
cache-reporting semantics when showing cache hit rate. Activity is grouped
by conversation and collapsed by default so long histories do not fill the
page.
- [x] **Compact conversation tool records**: Expand individual tool records
to read or copy results, errors, and input parameters. Conversation and
child-task progress use concise rows without repeated Runtime summaries.
- [x] **Task and custom-task experience**: Each product-level Task belongs to
one Conversation, while one Conversation can contain multiple Tasks. The
left conversation list exposes Task children through a leading expand
button; children share status dots, the parent does not repeat task badges,
and the UI stops at Task rather than exposing Job/Run levels. A new custom
Task can use the current or a new Conversation, defaults to Execute, and
preserves Runtime, tool, and approval boundaries. Repeated triggers reuse the
same Task; text returns to the Conversation, while independent files and
images remain artifacts. Ordinary messages and due Scheduled Tasks share a
persistent Conversation queue that runs one item at a time. Users can keep
sending while a response is active; queued items continue in order and can
be removed or inserted immediately by interrupting the current item. Task
Center remains the complete index rather than creating a separate Automation
Center. Current schedules support one-time, daily, and weekly triggers;
advanced time zones, Cron, event triggers, and retry governance remain
incremental PRD work. Scheduled messages use the conversation's current
history, Runtime, work mode, and saved knowledge retrieval settings; task
details show the actual mode used, and an active occurrence blocks duplicate
Run Now actions. See the
[Task Center PRD](./docs/features/task-and-job/task-center-prd.md) and
[Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md).
- [x] **Memory and Smart Heartbeat**: Provides periodic review, suggested
memories, insights, follow-up tasks, and auditable run history.
- [x] **Improved Smart Heartbeat entry and scope**: Smart Heartbeat / Heartbeat
Plans is the sole authoritative full-configuration entry. Plans can be
created and edited for Global or one or more selected Projects. Legacy
single-project settings migrate without loss, and project-level memory and
action output must explicitly target a Project in scope. Task Center and
Settings no longer duplicate the form. Partition-aware review, candidate
generation, and recall triggers remain to be designed. See the
[Smart Heartbeat PRD](./docs/features/smart-heartbeat/prd.md).
- [ ] **General supervision** (planned): Uses a fixed supervision section to
observe user-selected conversations, Tasks, automations, or experiments and
provide evidence-backed comments and requests for human intervention,
without automatically speaking, approving tools, or switching to Execute.
See the
[Conversation Supervision PRD](./docs/features/conversation-supervision/prd.md).
- [ ] **Batch runs and comparison lab** (planned): Compares model, Prompt,
role, and workflow configurations in batches and summarizes quality,
duration, tokens, cost, failure rate, and artifact differences.
- [ ] **Temporal memory and fact-conflict detection** (planned): Adds validity
periods, current/expired/conflicting fact detection, fact checking, and
evidence tracing to memory and the knowledge graph.
- [ ] **Visual controlled workflows** (planned): Versioned DAGs, conditional
branches, approvals, cancellation, and recovery, with execution still
passing through Main Runtime boundaries.
- [ ] **Unified Run Graph and replay** (planned): Connects Tasks, Subagents,
models, knowledge, tool approvals, usage, and artifacts for failure
diagnosis, retry, and redacted export.
- [x] **浏览器和桌面受控工具**:保留范围、取消、超时、输出边界和执行记录。
- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属。
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
- [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。
- [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
- [x] **可选本地语音模型管理**:应用不内置模型权重;提供校验下载、进度与取消、来源链接、本地目录导入、切换和删除。
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
- [x] **版本检查**:仅检查固定官方 Release 和当前平台清单,不自动下载或安装。
- [x] **内网兼容模式**:默认开启;允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书,关闭后恢复严格地址和证书校验。
### Browser, communication, voice, and application maintenance
### 开源、构建与发布
- [x] **Shared built-in browser for runtimes**: Uses GoodBuddy's isolated
Chromium and never controls a browser installed by the user. A separate
master switch decides whether assigned runtimes receive the capability in
Execute, with no per-use prompt after enablement. Turning Agent access off
does not disable manual browser workbar actions. The Browser workbar and
Agent share the same Conversation-owned session and serialized operation
path, with Back, Refresh/Stop Loading, address entry, Go, Interaction, and
Close actions. Stop Loading does not close the session, and user navigation
changes the page the Agent sees next.
Multiple tabs in one conversation share login state while keeping separate
pages and navigation. Each model request retains its starting tab binding;
unused request reservations do not create browser resources. Released tabs
can be reopened, explicit screenshots remain available, and ordinary
operations no longer trigger unused automatic screenshot capture.
Tabs can be closed while an AI request uses them without cancelling the
whole request. A later explicit navigation creates a separate replacement
tab; blank pages cannot reload, and switching or closing panels clears stale
action errors.
- [x] **Client-computer control tools**: Managed separately from the built-in
browser with scope, cancellation, timeout, output, and activity boundaries.
- [x] **Remote messaging-channel projects**: WeChat ClawBot, WeCom, and
DingTalk each have a system-managed project, independent remote
conversations, working directory, processing backend, default Ask/Execute
mode, and task/activity ownership. Each channel controls complete-response
length and segmentation according to platform capabilities rather than
relying on a shared service truncation.
- [x] **WeChat ClawBot QR login and media**: A separate Sidecar handles local
QR scanning, verification codes, encrypted credentials, and text messaging.
Personal WeChat chats support images and files, with at most four
attachments and 12 MB total after decryption per message.
- [x] **Safe WeChat responses**: Can return images generated by the current
task, or generate a Markdown attachment from the current final text after an
explicit user request. It never automatically reads or sends existing
workspace files.
- [x] **WeCom and DingTalk connections**: Main-only encrypted settings,
read-only environment-variable overrides, connection tests, dynamic
enable/disable, sender scope, and status diagnostics.
- [x] **Managed local-model download source**: Platform Features / General
selects ModelScope (default) or Hugging Face globally for future speech-input
and OCR model downloads. A source without a complete verified file is
explicitly unavailable and never silently falls back or combines files.
- [x] **Optional local speech-model management**: Model weights are not bundled.
Provides verified downloads, progress and cancellation, source links, ZIP or
local-directory import, switching, and removal.
- [x] **Local recording and offline transcription**: Captures microphone audio
and transcribes with the selected local model, with stop, cancel, state
feedback, and resource cleanup.
- [x] **Version checks and mirror source**: About and Updates selects GitHub
(default) or the mirror. Manual checks, startup checks, and the download page
use the same choice and read only fixed trusted release indexes; GoodBuddy
does not automatically download or install updates.
- [x] **In-app feedback**: About and Updates can submit problems, suggestions,
or experience feedback with an optional email and one screenshot.
Diagnostics are not uploaded by default; users can explicitly attach a
bounded recent Desktop diagnostic summary. Failed submission preserves the
draft, diagnostic choice, and request ID. Conversations, Prompts,
credentials, file contents, paths, raw provider responses, and remote Agent
logs are never attached.
- [x] **Private-network compatibility**: Permits in-app HTTP plus invalid,
self-signed, or expired HTTPS certificates. Fixed platform services such as
feedback and WeChat credential or media endpoints retain strict certificate
validation.
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。
### Open source, builds, and releases
### 开放接口、团队协作与远程执行
- Current source candidates are Desktop `0.13.0` and Agent `0.11.24`, with
OpenCode pinned to `1.18.29`. Publication status follows the independent
Desktop and Agent release channels.
- Candidate acceptance remains separate from version preparation. The current
remote-question source has passed real Linux x64 SSH and Electron question
tests with a deterministic loopback model, not the complete production
Main/Preload path with a real provider. That real-model path and candidate
native CI/package validation remain outstanding; see the
[remote-question evidence](./docs/features/assistant-workbar/progress.md).
- [x] **0BSD open-source license**: Original code can be freely used, copied,
modified, distributed, and commercialized. Third-party components and
resources retain their own licenses.
- [x] **Reproducible dependency installation and source builds**: Uses locked
dependencies, Node.js 24, and unified test, type-check, lint, and production
build commands.
- [x] **Six-target native release matrix**: Native runners build Windows,
macOS, and Linux `x64` and `arm64` targets with release manifests and SHA-256
hashes. Windows provides NSIS and portable ZIP, macOS DMG only, and Linux
AppImage, DEB, and RPM, for 12 installers and 20 published assets.
- [x] **Desktop Runtime package verification**: Offline OpenCode dependencies
are copied without electron-builder's root node_modules exclusion; DSH
session projection is bundled into the unpacked Host. Final package checks
compare the offline dependency tree and reject external DeepSeek imports.
Windows x64 CI additionally imports the packaged OpenCode plugin and checks
the real DSH UtilityProcess handshake before publishing.
- [x] **DMG-only update manifests**: Current Desktop and website readers accept
DMG-only and historical DMG/ZIP manifests. Older macOS GitHub readers and all
older mirror readers require a manual upgrade from the download page.
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
- [ ] **多云远程沙盒 Agent**(规划中):通过云厂商 API 和 SSH Agent 管理专用 Linux 沙盒;凭据留在 Main 进程,高风险控制面操作单独确认。
### Open interfaces, team collaboration, and remote execution
## 规划原则
- [x] **Remote task delegation**: Enabled only after the user explicitly
configures an endpoint and token, uses HTTP(S) according to the global
private-network compatibility setting, and writes results to a persistent
outbox.
- [ ] **Headless Runtime API** (planned): A local-first Task, event, state, and
artifact API with scoped, expiring, rate-limited, and revocable tokens.
- [ ] **GoodBuddy Team Hub** (planned): An optional service for organizations,
RBAC, project sharing, remote Agents, policy distribution, and tenant audit.
- [ ] **SSH Host and remote execution-space release acceptance**: Host CRUD,
Host Key, encrypted credentials, Project UI, Workspace, OpenCode ACP v5,
Agent-owned Prompt/gateway/transcript, read-only Ask, full-account Execute,
cancellation, exact detached-Agent reconnection, and release-only
dual-architecture resource verification are wired. Current Linux x64 source
has passed a real-model and tool matrix covering detach, process exit, relay
loss, concurrency, cancellation, provider failures, Agent restart, and
Desktop SQLite recovery. A current-source, test-signed Agent `0.11.17`
package on Linux x64 additionally passed two concurrent Ask reads, an
Execute write/read, and six real managed-gateway model requests with the
configured custom headers and body fields. No provider request was replayed,
and scanning the isolated Agent state found no API key or custom request
value. The public signing-key registry is provisioned. Direct control-plane
source does not wait for a new installer-bearing package, but the current
formally signed Linux x64/arm64 artifact matrix still requires public
verification, along with GitHub/Beijing mirror, dual-architecture, and
offline GoodBuddy transfer release acceptance. Until these gates pass, this
path must not be described as published or as having passed formal release
acceptance.
- [ ] **Multi-cloud remote sandbox Agents** (planned): Manages dedicated Linux
sandboxes through cloud-provider APIs and SSH Agents. Credentials remain in
Main, and high-risk control-plane operations receive separate confirmation.
规划中的工作流、Subagent、MCP、远程 API 和沙盒能力不得绕过现有 Main Runtime、Ask/Execute、权限、取消、超时和审计边界。
## Roadmap Principles
Planned workflows, Subagents, MCP, remote APIs, and sandbox capabilities must
not bypass existing Main Runtime, Ask/Execute, permission, cancellation,
timeout, or audit boundaries.
+140
View File
@@ -0,0 +1,140 @@
# GoodBuddy 功能矩阵与路线图
[English](./FEATURES.md) | **简体中文**
本文记录 GoodBuddy 的已提供能力和路线图。未完成项目不代表已包含在当前版本中。
## 状态说明
- [x] 已提供
- [ ] 开发中或规划中,具体以条目标注为准
## 功能总表
### 桌面基础、工作空间与上下文
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.13.0` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
- [x] **跨项目会话活动**:统一入口汇总各项目正在运行和需要处理的对话,保留仍有活动的较早会话,可直接打开对应对话;项目选择器也显示活动数量。
- [x] **紧凑会话控件**:输入选项集中在紧凑设置面板,Runtime、工作模式、发送和队列操作保持可用;会话搜索提供内嵌清除按钮,清除后恢复当前范围列表并把焦点返回输入框。
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异、当前浏览目录和已展开目录,超过 50 项变更可继续加载。
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。提交历史贴底,首次展开占 Git 内容区一半,支持调整比例、独立滚动和统一刷新;切换项目自动检测 Git,不沿用其他项目状态。移动限于工作区内,删除须确认,分支冲突不自动 stash 或丢弃改动;托管 SSH 操作需要 Agent `0.11.23`。
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
- [x] **助手工作栏、多终端与可调布局**:右侧工作栏使用持久“+”能力目录和应用 Tab;任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目、全局任务和全部项目,范围随布局保存。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
- [ ] **应用导航与系统工具快捷入口**(仅设计):规划固定系统入口、内置应用显隐和打开现有工作栏的底部工具按钮;尚未实现新界面。详见[设计文档](./docs/features/application-tool-navigation/README.md)。
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入监督、统一 Runtime、受管进程、静态安全 HTML 预览、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。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] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。连续请求自动复用最近一次成功生成的图片和文字历史,已保存会话重开后仍可继续修改,本轮显式附图优先;上游明确不支持编辑时继续按文字生成,仅在回复底部提示未使用参考图。编辑使用 multipart 上传图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。详见[图片生成与连续修改](./docs/features/image-generation/README.md)。
- [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 紧凑地发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并在 Execute 中应用多文件补丁。Ask 可以搜索和读取,但不能应用补丁或运行命令;用户无需另行安装 ripgrep。可处理的读取或搜索错误允许模型调整参数后继续,部分搜索结果保留并明确提示覆盖不完整。
- [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 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 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、固定 Node 和 OpenCode 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。macOS 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证;这不等于新版本已经发布。详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。
- [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 环境准备真实 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。
- [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 需要 Desktop `0.13.0`、Agent `0.11.24` 和新启动的托管 Runtime,不扩展到任意 ACP 服务。回答或跳过成功后保留原问题与答案,支持多轮记录和本地会话重载;旧版本已丢弃的答案不能恢复。并行问题按顺序等待回答,重复事件保留草稿,提交失败可重试;取消远程待答后可在同一会话继续发送。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.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] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
- [x] **混合检索测试台**:支持全文、中文词组、向量和图谱通道诊断,可调 Top K、阈值、权重、本地或学习型重排及上下文预算。
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;来源记录直接显示在所选待办的详情中,切换事项不会混入上一项的来源内容,读取失败使用应用通知,来源缺失有明确提示。左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
### 工作管理、长期协作与工作流
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
- [x] **紧凑会话工具记录**:逐项展开工具记录即可查看和复制结果、错误及输入参数;会话与子任务进度使用简洁行,减少重复 Runtime 摘要。
- [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)。
- [ ] **通用监督**(规划中):通过固定监督栏目观察用户选择的会话、任务、自动化或实验对象,提供带证据的评论与人工介入请求,但不自动发言、批准工具或切换 Execute。详见 [会话监督 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] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;默认不上传诊断,用户可主动附加有界的最近桌面诊断摘要。失败后保留草稿、诊断选择和同一请求编号;不会附加对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
- [x] **内网兼容**:允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书;反馈、微信凭据与媒体等固定平台服务仍保持严格证书校验。
### 开源、构建与发布
- 当前源码候选为 Desktop `0.13.0`、Agent `0.11.24`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
- 候选验收与版本准备分别记录。当前远程问答源码已通过真实 Linux x64 SSH 与 Electron 问答测试,但模型为确定性 loopback 服务,不是完整生产 Main/Preload 配合真实服务商;该真实模型链路和候选原生 CI/打包验证仍待完成,详见[远程问答证据](./docs/features/assistant-workbar/progress.md)。
- [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] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 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、权限、取消、超时和审计边界。
-77
View File
@@ -1,77 +0,0 @@
# GoodBuddy
English | [简体中文](README.md)
A secure, cross-platform, local-first desktop AI assistant and Agent workspace.
![GoodBuddy workspace](docs/screenshots/workspace-overview.png)
## Highlights
- **Controlled execution**: `Ask` stays read-only; `Execute` runs only enabled tools within defined boundaries and records their activity.
- **Local-first data**: Conversations, tasks, artifacts, memory, knowledge bases, and graphs are stored in local SQLite. API keys are encrypted by the operating system.
- **Multiple runtimes**: Connect directly to models or use OpenCode and Continue, with cancellation, timeouts, output limits, and process cleanup.
- **Open integrations**: Supports OpenAI Responses, OpenAI-compatible Chat Completions, Anthropic Messages, OpenAI Images, Embeddings, Skills, and MCP.
- **Knowledge workspace**: Import files, folders, and web pages, then search them with full-text, phrase, vector, and graph retrieval.
- **Work management**: Organize projects, conversations, tasks, activity, artifacts, memory, Magic Notes, and Smart Heartbeat.
- **Remote channels**: Connect WeChat ClawBot, WeCom, and DingTalk with separate remote sessions for each sender.
- **Desktop context**: Add selected files, screenshots, application windows, clipboard content, and voice.
- **Offline speech**: Use local SenseVoice, Paraformer, and Whisper models.
- **Rich responses**: Render Markdown, LaTeX, and controlled Mermaid diagrams.
![GoodBuddy knowledge workspace](docs/screenshots/knowledge-workspace.png)
![GoodBuddy knowledge graph](docs/screenshots/knowledge-graph.png)
![GoodBuddy Magic Notes](docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy Smart Heartbeat](docs/screenshots/smart-heartbeat.png)
See [FEATURES.md](FEATURES.md) for the detailed feature matrix and roadmap.
## Install
Download a build from [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases):
| Platform | Architectures | Formats |
| --- | --- | --- |
| Windows | `x64`, `arm64` | NSIS, portable ZIP |
| macOS | `x64`, `arm64` | DMG, ZIP |
| Linux | `x64`, `arm64` | AppImage, DEB |
Code signing and macOS notarization are not configured yet, so your 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.
## Privacy and security
- Model requests are sent only to services selected by the user.
- Local data stays in the operating system's application data directory by default.
- The Renderer has no access to raw Electron APIs or model credentials.
- Remote delegation is disabled until 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
```
## 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.
+129 -39
View File
@@ -1,49 +1,113 @@
<div align="center">
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
# GoodBuddy
[English](README.en.md) | 简体中文
**Desktop access to model chat, coding agents, knowledge, and scheduled work**
安全、跨平台、本地优先的桌面 AI 助手与 Agent 工作空间。
**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)
## 核心能力
[![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>
- **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。
- **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite;API Key 由系统安全存储加密。
- **多 Runtime**:支持直连模型、OpenCode 和 Continue,统一处理取消、超时、输出限制和进程退出。
- **开放连接**:支持 OpenAI Responses、OpenAI 兼容 Chat Completions、Anthropic Messages、OpenAI Images、Embeddings、Skills 和 MCP。
- **知识工作区**:支持文件、目录和网页导入,以及全文、词组、向量和图谱混合检索。
- **工作管理**:集中管理 Projects、对话、任务、活动、成果、记忆、魔法笔记和智能心跳。
- **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。
- **桌面上下文**:可选择文件、截图、应用窗口、剪贴板和语音作为上下文。
- **离线语音**:支持 SenseVoice、Paraformer 和 Whisper 本地模型。
- **富文本回答**:支持 Markdown、LaTeX 公式和受控 Mermaid 图表。
![GoodBuddy workspace](./docs/screenshots/workspace-overview.png)
![GoodBuddy 知识工作区](docs/screenshots/knowledge-workspace.png)
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.
![GoodBuddy 知识图谱](docs/screenshots/knowledge-graph.png)
## Highlights
![GoodBuddy 魔法笔记](docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
- **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.
![GoodBuddy 智能心跳](docs/screenshots/smart-heartbeat.png)
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.
完整功能和路线图见 [FEATURES.md](FEATURES.md)。
## Capabilities
## 安装
| Area | Available in GoodBuddy |
| --- | --- |
| Agent Runtimes | Direct model connections, OpenCode, Continue, and the preview DeepSeek Harness |
| Data and execution | Local SQLite 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 |
从 [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases) 下载:
## Product tour
| 系统 | 架构 | 格式 |
### Knowledge bases and graphs
![GoodBuddy knowledge workspace](./docs/screenshots/knowledge-workspace.png)
![GoodBuddy knowledge graph](./docs/screenshots/knowledge-graph.png)
### Magic Notes and Smart Heartbeat
![GoodBuddy Magic Notes](./docs/screenshots/GoodBuddy_MFSGeK0NoT.gif)
![GoodBuddy Smart Heartbeat](./docs/screenshots/smart-heartbeat.png)
## Agent Runtimes
| Runtime | Functions | Configuration |
| --- | --- | --- |
| Windows | `x64`、`arm64` | NSIS、便携 ZIP |
| macOS | `x64`、`arm64` | DMG、ZIP |
| Linux | `x64`、`arm64` | AppImage、DEB |
| 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 |
当前尚未配置代码签名和 macOS notarization,系统可能显示安全提示。
Each run reports cancellation, timeouts, token usage, tool calls, and history.
Runtime-specific capabilities and configuration remain separate.
## 从源码运行
## Download
需要 Node.js 24 和 npm:
Download a build from
[GitHub Releases](https://github.com/mesalogo/goodbuddy/releases):
| 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).
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
@@ -52,19 +116,35 @@ npm ci
npm run dev
```
构建与打包说明见 [BUILD.md](BUILD.md)。
See [BUILD.md](./BUILD.md) for build and packaging instructions.
## 隐私与安全
## Documentation
- 模型请求只发送到用户选择的服务。
- 本地数据默认保存在系统应用数据目录。
- Renderer 不接触原始 Electron API 或模型凭据。
- 远程委派仅在用户配置端点和令牌后启用。
- 内网兼容模式允许应用内 HTTP 和非标准 HTTPS 证书;微信凭据和媒体端点仍执行严格校验。
- [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
欢迎提交 Issue 和 Pull Request。请先阅读 [AGENTS.md](AGENTS.md),提交前运行:
- 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
@@ -72,6 +152,16 @@ npm run typecheck
npm run lint
```
## 开源许可
## Community communication
GoodBuddy 的原创代码采用 [0BSD License](LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
Scan the QR code to join the GoodBuddy WeChat group:
<img width="1250" height="1194" alt="9627ce196b0de42f123c33dd9dea1568" src="https://github.com/user-attachments/assets/e806543f-6d8a-4c5c-ba44-19e74dac9a78" />
## 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.
+136
View File
@@ -0,0 +1,136 @@
<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)
### 魔法笔记与智能心跳
![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="1194" alt="9627ce196b0de42f123c33dd9dea1568" src="https://github.com/user-attachments/assets/ff87dfac-d72f-47b7-ac9a-c88b0b4c96d9" />
## 开源许可
GoodBuddy 原创代码采用 [0BSD License](./LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
+260 -29
View File
@@ -2,7 +2,8 @@
## 1. 目的与适用范围
本文定义 GoodBuddy 桌面端的统一界面规则,适用于聊天与最近对话、知识库、智能心跳、任务与活动记录,以及后续新增的一级页面。
本文定义 GoodBuddy 桌面端的统一界面规则,适用于聊天与最近对话、任务中心、知识库、
智能心跳、运行记录,以及后续新增的一级页面。
设计系统解决两类问题:
@@ -144,7 +145,7 @@
| 变体 | 最大内容宽度 | 适用场景 | 页面映射 |
| --- | --- | --- | --- |
| `reading` | `820px` | 连续阅读、单列编辑、对话撰写 | 聊天正文与输入区 |
| `standard` | `960px` | 常规列表、设置、表单与任务管理 | 最近对话、任务 |
| `standard` | `960px` | 常规列表、设置与表单 | 最近对话、设置 |
| `dashboard` | `1040px` | 指标、卡片网格、宽表格与审计数据 | 智能心跳、活动记录 |
| `master-detail` | 可用空间内流式铺开 | 左侧选择、右侧编辑或预览 | 知识库 |
@@ -192,7 +193,7 @@
### 6.1 PageTabs
用于同一一级页面内的同级内容面板,例如心跳的“成长概览”和“心跳计划”。
用于同一一级页面内的同级内容面板,例如智能心跳的“运行概览”和“心跳计划”。
- 使用 `tablist`、`tab` 和 `tabpanel` 语义,当前项使用 `aria-selected="true"`。
- 一级页面之间的导航由应用主导航承担,不复用 `PageTabs`。
@@ -200,6 +201,7 @@
- 项目过多时优先重组信息架构,不把一级页签做成多行。
- `PageTabs` 可使用默认视觉或共享的 `segmented` 视觉变体。紧凑主从工作台中的 2 至 4 个同级面板可使用与模型设置一致的分段外观,但不得因此改用按钮组语义。
- 视觉变体不改变组件含义:分段外观的 `PageTabs` 仍使用页签语义、单一激活面板、游标焦点和方向键切换,不复制页面专属样式。
- 在独立滚动的 Flex 页面中,`PageTabs` 及其直接包装层必须保持 `flex: 0 0 auto`,不得被剩余内容压缩到零高度。新增页签容器时必须提供对应的 CSS 回归测试。
### 6.2 SegmentedControl
@@ -281,32 +283,156 @@
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
- 当前导航项和当前会话必须同时使用至少三种信号中的两种:强调背景、可见边框、图标或文字强调。浅色主题的当前项优先使用更完整的蓝色选中表面和较高字重。
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
- 账户与设置入口固定在侧栏底部。已有稳定设置入口时,不在顶栏重复提供同一入口。
- 项目选择器下方承载跨项目活动汇总,空闲时隐藏;项目计数与活动对话框遵守
[项目活动汇总 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。名称或副标题溢出品牌区时
单行截断,不能撑宽或挤压侧栏。
- 侧栏底部固定显示文件、终端、浏览器和资源监控四个系统工具图标,设置按钮独立靠右。工具
图标只负责打开或聚焦助手工作栏面板,不在侧栏复制工具内容。已有稳定设置入口时,不在顶栏
重复提供同一入口。
### 6.9 应用顶栏与全局操作
### 6.9 助手工作栏
应用顶栏用于窗口级状态、侧栏开关和低频全局操作,不承担页面标题或主要导航。顶栏必须保持紧凑,不能与页面内容争夺注意力。
助手工作栏是应用级右侧工具容器,不归属于聊天页面,也不根据当前页面、项目或 Runtime
自动增删入口。产品契约见
[通用助手工作栏与执行空间 PRD](./docs/features/assistant-workbar/prd.md),
动态 Tab、“+”和终端细节见
[助手工作栏多终端页签 PRD](./docs/features/assistant-workbar/terminal-tabs-prd.md)。
- 顶栏高度默认为 `58px`,图标按钮使用 `34px × 34px` 点击区域。
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。
- 升级后默认打开任务中心、工作区、一个浏览器实例和成果。任务中心与工作区是不可关闭的
单实例基础应用;浏览器是可关闭的多实例应用;成果保持可关闭的单实例应用。不可关闭不
等于锁定顺序。
- 资源监控作为本机单实例应用保留在能力目录,但不加入升级后的默认打开实例。面板只在可见
活动时采样,首期不持久化历史或提供后台负载告警。
- 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`
最小宽度;拖动保存两者在可用分栏宽度中的比例,任一侧到达最小值后都不得继续侵占。
所有可容纳两侧最小值的尺寸均支持指针和键盘调整,并须保留全部栏目入口。
- 工作栏开关固定在窗口关闭按钮正下方,同一个按钮负责展开与收起并持续暴露状态;图标中的
箭头方向必须随当前动作切换,收起状态提示展开,展开状态提示收起。按钮保持普通图标按钮
视觉,不通过持续高亮表达展开状态。工作栏内部不再重复关闭按钮;当前会话没有 Task 时,
不得为了放置该开关而保留一条空上下文栏。
- 停靠工作栏不得获得对话框语义、焦点陷阱或背景遮罩。关闭后焦点返回同一个工作栏开关;
主工作区与工作栏在展开期间都保持可达。
- 对话消息流、输入区和输入区元信息必须在主工作区内部保留对称的左右安全边距。工作栏
分割线只能位于主工作区边界之外,不能覆盖、裁切或挤掉输入框边框、发送按钮或右侧边距。
- 终端、宽日志和大型成果可以由用户切换到底部停靠或独立窗口,应用不得因内容变化自动改变用户已选布局。
### 6.10 应用顶栏与全局操作
应用顶栏用于当前会话标题、范围、窗口级状态、侧栏开关和低频全局操作,不承担主要导航。
顶栏必须保持紧凑,不能与页面内容争夺注意力。
- 应用壳层在水平方向分为全高主侧栏和右侧内容框架。主侧栏从窗口顶部贯穿到底部,品牌区、
项目切换器、导航和账户区保持同一连续表面;不能用全宽标题栏把整条侧栏向下推移。
- 无边框窗口的紧凑标题栏只覆盖右侧内容框架,保留侧栏开关、当前会话标题、范围、Runtime
状态、主题开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
`34px × 34px` 点击区域。
- 发布说明 Modal 与其他应用级 Modal 一样使用整窗遮罩;遮罩必须覆盖主侧栏、右侧内容和
顶栏,不能保留一条未被遮罩的窗口控制色块。关闭操作由 Modal 自身提供。
- 整窗固定遮罩通过 `createPortal(..., document.body)` 挂载,避免页面层叠上下文影响;
保留 React Context、关闭与焦点管理,表单样式不得依赖已移出的页面祖先选择器。
- 遮罩必须 `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/设置入口;使用帮助优先放在相关操作附近,而不是为单个帮助项创建“更多”菜单。
- 助手工作栏开关固定在上下文栏最右侧并与窗口关闭按钮纵向对齐;展开后该开关保持原位并
负责收起,不在工作栏标题区域增加第二个关闭入口。
- 只有存在至少两个无法由稳定入口承载的低频全局操作时才增加全局菜单,不为了容纳一个冗余入口而显示省略号按钮。
- 窄窗口下优先压缩状态标签并保留图标按钮,不隐藏窗口控制、当前范围或进行中的风险状态。
- 使用全局菜单时,菜单项使用 `--font-body`、`14px` 图标和约 `32px` 单项高度;标签使用短名称。菜单保留 `menu`、`menuitem` 语义,支持上下方向键、Home、End 和 Escape,关闭后焦点返回触发按钮。
### 6.10 上下文单选菜单
### 6.11 单选选择器
模型、专家角色和工作模式属于同一输入上下文,其选择器必须共享结构、尺寸和菜单视觉,不能出现一个精细菜单与两个风格不一致的原生下拉框。
模型、专家角色、工作模式、Runtime Agent、Runtime 预设和 Runtime 快捷操作属于同一输入上下文,其选择器必须共享结构、尺寸和菜单视觉,不能出现一个精细菜单与多个风格不一致的原生下拉框。
- 只有选项能由单行短标签充分区分、不需要补充来源、位置、状态或说明时,才使用原生 `select`。
- 当用户必须在选择前比较来源、范围、位置、连接状态或其他辅助信息时,使用与输入区选择器同类的自定义富信息单选菜单,不用 CSS 修饰原生 `select` 冒充富信息菜单。
- 触发按钮复用统一的模型选择按钮样式,保持相同高度、圆角、边框、展开指示和焦点状态。
- 菜单使用 `menu` 与 `menuitemradio` 语义,当前项同时显示选中标记和 `aria-checked`。选项可以包含一行简短说明,但标签和说明不得被截断到无法区分。
- 支持上、下方向键、Home、End、Enter 或 Space、Escape;打开后焦点进入当前项,关闭后返回触发按钮。
- 点击或聚焦菜单外部时关闭;同一输入区内的模型、专家和模式菜单互斥展开。
- 不可用选项保持可读并说明原因,键盘导航不得停留在不可选择项上。
- 仅在选项简单且不需要说明、禁用原因或一致菜单行为时使用原生 `select`。
- 当某个选项需要用户在其展开区域内完成路径选择、授权或其他前置操作时,不得因为前置条件
尚未完成而禁用该入口。选择入口只展开流程,验证成功前不提交实际值;不得自动采用第一个
候选。取消保持原值,验证错误在对应操作附近显示。
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称,并按数据可用性补充目录、远程来源或其他能帮助辨认项目的信息。托管 SSH 项目在“远程项目”内按已保存 SSH Host 做一层轻量分组,Host 标题旁显示真实 Agent 连接状态;项目副标题只显示远端路径,不重复“托管 SSH”类型。
- 项目菜单区分“本地项目 / 远程项目 / 远程通道”,不把消息通道命名为远程项目。“远程项目(技术预览)”关闭时完整移除远程项目分组,而不是显示不可操作的残留选项;本地项目和远程通道继续可用。Agent 连接状态使用“未连接 / 连接中 / 就绪 / 异常”等文字标签,读取状态不得发起连接,且不能只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
### 6.11 应用通知与就地反馈
### 6.12 应用通知与就地反馈
应用级通知统一进入全局通知视口,页面不得自行复制通知卡片或在内容流中长期堆放短期消息。
@@ -317,7 +443,7 @@
- 就地错误必须与对应字段或操作建立程序化关联;全局错误使用 `alert` 和 assertive 实时区域,成功与信息使用 `status` 和 polite 实时区域。
- 一个事件只能选择一种主要反馈位置,不得同时显示页内横幅和全局通知。失败时不得因通知切换而清空用户输入、筛选或未提交草稿。
### 6.12 Switch 与 Checkbox
### 6.13 Switch 与 Checkbox
Switch 用于在两个持久状态之间立即切换,例如启用能力、开启索引、允许群消息或显示平台入口。Checkbox 用于独立多选、范围分配或执行前确认,例如选择多个 Runtime、选择知识库、清除已保存密钥。两者不得只因底层都使用 `input[type="checkbox"]` 而混用视觉或语义。
@@ -469,21 +595,60 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 13.1 聊天
- 使用 `reading` 壳层,消息流与输入区共享宽度。
- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐,
右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
原文,不包含推理过程、工具日志或引用元数据,结果通过应用通知反馈。流式回复完成前不显示
复制入口。
- 默认开启“会话 HTML 渲染”时,已完成 Agent 回复中的完整 HTML 与 `html` / `htm`
代码块在原位置显示隔离静态预览;用户消息、推理过程和流式中的回复不得执行 HTML。
每个预览持续标注脚本和网络已禁用,并在标题右侧复用与 Mermaid 图表相同的图标按钮组:
源码按钮使用代码图标并暴露 `aria-expanded`,全屏按钮使用放大图标打开统一 Modal。
全屏预览遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape 关闭和焦点恢复规则,
并沿用同一套清理与 Sandbox/CSP 边界。关闭平台功能后必须显示 HTML 源码,不能隐藏内容
或保留旧预览。
- 本地会话的更多操作提供“在新会话中继续”。创建前必须先持久化当前稳定消息;有活动运行
或待发送内容时不可创建。新会话复制聊天内容与当前 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 输入可缩进显示,复制仍使用收到的原文,并通过应用通知反馈。详情工具栏只保留复制按钮,文本默认自动换行,长内容在固定最大高度内滚动,不提供换行切换和展开全部按钮。
- 工具取消后保留“已取消”文字,避免与失败图标混淆。
- Agent 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。
- 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。
- 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。
- 支持上下文状态的 Runtime 在输入区下方复用同一紧凑用量条;文案必须区分“本次模型调用”和“压缩后对话估算”。手动压缩仅在当前 Runtime 明确支持且没有活动回复时显示,作为元信息区左下角的浮动次操作,不参与输入区高度计算;元信息区始终预留稳定高度,切换 Runtime 不得让输入框上下位移。元信息区与窗口底部只保留紧凑安全留白,不形成额外空白区。进行中禁用重复操作,结果通过应用通知反馈。
- 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。
- 宽度大于 `700px` 时,添加内容、知识范围、专家、模式和模型控件保持同一行;仅在窄输入区中换行,不能因为允许换行而让所有窗口都固定显示两行。
- 宽度大于 `700px` 时,主工具栏的添加内容、Runtime、模式、会话选项与发送保持同一行。窄输入区按容器宽度换行,保留发送及停止入口;选项浮层不得超出输入区宽度,不再常驻独立的 Runtime 专属功能行。
- 输入框原生支持 `Ctrl+V`:文本直接进入草稿,图片转换为本次消息附件。文件选择由上传按钮承担,不再提供独立“读取剪贴板”按钮;默认工具栏也不提供“截取当前屏幕”和“选择应用窗口”入口,避免与系统粘贴、文件选择和后续工具执行重复。
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
排除在可滚动范围之外。
### 13.2 最近对话
- 使用 `standard` 壳层和统一 `PageHeader`。
- 搜索、范围和时间筛选位于筛选工具栏。
- 行项目统一显示标题、范围、最近更新时间和必要状态。
- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。
- 侧栏中有关联 Task 的 Conversation 显示行首展开按钮,父行不显示任务标签或数量;展开后
最多直接显示 3 个 Task,“查看全部 N 个任务”打开该 Conversation 的完整任务区。
- Task 子项左侧显示名称,右侧显示本地化的 Ask/Execute、计划摘要和共享状态标记,与父会话
的更新时间位置一致;已完成使用带勾成功点,不重复显示完成文字。点击子项打开同一
Conversation 并定位 Task,不继续展开 Job/Run。
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
### 13.3 知识库
@@ -497,36 +662,86 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
### 13.4 智能心跳
- 使用 `dashboard` 壳层。
- 顶部先呈现运行状态、当前范围和主操作,再呈现指标和配置。
- 顶部先呈现运行状态、实际范围和主操作,再呈现指标、建议、历史与配置。
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
Checkbox,不能依靠进入页面时的当前项目推断。
- 状态卡片使用统一状态令牌,不只依赖颜色。
- 运行历史与配置使用明确区块,不以多套相似页签混合导航、开关和筛选。
- 保留“运行概览 / 待处理建议 / 报告与记录 / 心跳计划”四个同级页面。
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
### 13.5 任务与活动
### 13.5 Task Center
- 任务使用 `standard` 壳层,活动记录使用 `dashboard` 壳层。
- “任务 / 活动”作为同级页面时使用 `PageTabs`。
- 任务状态筛选使用 `SegmentedControl` 或筛选工具栏,不再模拟页签。
- 活动记录保留审计字段和范围,支持独立容器横向滚动。
- 批量停止、删除和清空历史遵循破坏性操作政策。
- 保留现有助手工作栏入口,首期在窄栏内适度完善,不先扩张成新的独立一级页面。
- 只展示 Task;普通 Conversation、Job、Run、工具步骤、Subagent 和心跳事项不独立占行。
- 每项显示名称、关联 Conversation、Global 或 Project 范围、Ask/Execute、状态、最近进展、
最近真实活动时间及需要关注信息。
- 点击列表项打开关联 Conversation 并定位该 Task,不显示第二份内容载体。
- 需要关注、进行中、已暂停和已结束使用共享 `SegmentedControl`;窄栏不足时单行滚动。
- 完整消息留在 Conversation;长错误、工具、审批和成果按 Task 关联到 Runtime、活动记录和
成果查看器,不撑高列表,也不显示 Job/Run 树。
- “新建定制任务”使用共享 Modal,明确选择当前或新 Conversation。聊天入口默认当前
Conversation,Task Center 入口默认新 Conversation;选择必须持续可见。
- 定时消息创建 Modal 使用关联 Conversation 的 Runtime 与 Ask/Execute 模式,不再提供独立
模式开关。未运行计划的详情显示会话当前模式,已运行任务显示该次执行记录的模式;
不得用旧计划字段表示下一次执行权限。不支持工具时不得静默降级。
### 13.6 魔法笔记
### 13.6 运行记录
- 使用 `dashboard` 壳层,并通过 `PageTabs` 提供“任务与会话 / 活动时间线 / 用量统计”三个同级视图。
- 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠。
- 任务或会话的综合状态以最近一次顶层请求对应的最终 Agent 结果为准;最终结果尚未产生时使用该请求的当前状态。中间工具或子专家的失败、取消和中断保留在活动详情中,但不得覆盖最终成功状态。
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系。节点内使用 `U / G / S / T / A` 拉丁字母简称,节点下显示完整身份或名称;选择节点后显示所属会话、不可变范围快照和完整详情。
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度,宽表格在独立容器内横向滚动。
- 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
### 13.7 魔法笔记
- “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。
- 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。
- 笔记编辑器保留 Quill 原生列表快捷输入:输入 `-`、`*`、`1.`、`[ ]` 或 `[x]` 后按空格,分别转换为项目符号、编号或待办清单。快捷输入只生成现有 Delta 富文本,不引入 Markdown 源码模式、第二套持久化格式或第三方 Markdown 插件;编辑区附近持续提供简短提示。
- 左侧笔记与待办列表在双栏布局中允许在 `180px` 至 `360px` 范围内通过指针或键盘调整,
也可从页面标题区隐藏和恢复;宽度与显示状态均记住用户选择。分隔条保留 `separator`
语义及方向键、Home、End 操作,窄窗口堆叠布局不显示拖拽分隔条。
- 左侧笔记列表依次显示标题、最多两行内容预览,以及同一行的记录数和本地化更新时间。记录数靠左、更新时间靠右,窄宽度下均保持单行省略,不挤压标题或扩大列表栏。
- 左侧“魔法笔记”入口可显示未完成待办数量。数量为 0 时隐藏徽标,超过 99 时显示 `99+`,可访问名称仍说明精确数量;勾选、取消勾选以及 Agent / MCP 对笔记清单的写入都必须刷新数量。
- 未完成待办徽标的高度不得超过主导航图标;数量从 0 变为可见或再次隐藏时,主导航项行高和相邻菜单位置必须保持不变。
- 在待办面板勾选或取消勾选后,当前缓存中的源笔记正文必须立即同步对应清单状态,不得依赖离开工作区后重新加载。
- 当前记录草稿非空时,切换笔记、待办或内容面板必须先在编辑器旁就地确认;继续编辑时保留草稿并恢复编辑焦点,只有明确选择放弃后才切换。
- 创建、保存、更新、删除和 AI 评论完成等短期结果进入应用级通知,不在编辑区或列表上方堆放页内通知。
- 标题或正文校验、删除确认、同步进度和可就地恢复的错误仍靠近对应编辑器或操作呈现。
### 13.7 设置中心
### 13.8 设置中心
- 全页设置使用固定标题区、左侧分类导航和独立滚动的内容区。右上角关闭按钮是离开设置中心的稳定入口。
- 全页设置标题区依靠留白与内容区分层,不在标题下方绘制贯穿整个工作区的分隔线;模态设置可以保留标题边界。
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
- 全页设置标题区与双栏内容使用共享 `--page-gutter`,标题、分类导航和内容区在同一页面边距基线上;标题继续使用标准 `PageHeader` 的底部分隔线与下内距。固定标题和双独立滚动区域不改变这些一级页面壳层规则;模态设置保留自身外框与标题边界。
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
- 所有分类使用共享的 `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 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。“魔法笔记”页提供默认开启的“显示未完成待办数量”持久 Switch,关闭后只隐藏左侧数量徽标,不改变待办数据或魔法笔记入口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“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,添加与删除使用明确按钮和可访问名称。
- 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 内容。
### 13.8 文档解析设置
### 13.9 文档解析设置
- 设置中心新增独立的“文档解析”分类,统一管理聊天附件、知识库导入以及后续文档审阅场景使用的提取、转换和 OCR 策略。OCR 不作为普通对话模型出现在“模型连接”中。
- 分类页头说明文档解析的跨场景作用,右侧依次显示“测试解析”和“保存设置”;保存位于最右侧。测试必须选择真实文件并执行实际解析,不能只检查模型文件或接口连通性。
@@ -593,13 +808,16 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- [ ] 将输入快捷键与附件提示置于空输入框内部,输入区下方保持单行说明。
- [ ] 最近对话迁移到 `standard`,统一搜索、范围、时间和删除行为。
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
- [ ] 智能心跳迁移到 `dashboard`,统一状态卡片、配置和运行历史层级。
- [ ] 任务迁移到 `standard`,活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
- [x] 智能心跳使用 `dashboard`,保留概览、建议、轨迹和计划,并在计划中支持 Global / 多 Project 范围。
- [x] 在现有工作栏中完善任务中心,统一范围、状态、最近进展和筛选,不新建平行任务平台。
- [ ] 活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
- [ ] 设置中心使用共享分类定义与 `SettingsCategoryHeader`,将保存与测试操作统一放到分类页头右侧,并把成功反馈接入应用通知。
- [ ] 文档解析设置统一聊天附件与知识库的解析预设、OCR 状态、转换状态、隐私限制和真实文件测试。
### 15.5 验收
执行与取证方法见 [Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md),包括真实 Portal、原生键盘、计算样式与 PNG 审阅;视觉 fixture 与完整生产 App 的结果须分别标注。
- [ ] 在浅色与深色主题下检查所有页面、弹窗、菜单、输入框、表格和空状态。
- [ ] 在大于等于 `1200px`、`960px`、`720px` 和小于 `720px` 的窗口宽度检查布局。
- [ ] 仅使用键盘完成导航、筛选、创建、编辑、确认和取消。
@@ -620,3 +838,16 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
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.11.24",
"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"
}
}
+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
+942
View File
@@ -0,0 +1,942 @@
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 ||
descriptor.remoteRuntime.runtimeId !== 'opencode' ||
descriptor.remoteRuntime.provider !== 'opencode' ||
!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')
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' ||
entry.remoteRuntime?.runtimeId !== 'opencode' ||
entry.remoteRuntime?.provider !== 'opencode' ||
!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')
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 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 {
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
}
}
+265
View File
@@ -0,0 +1,265 @@
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',
'--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',
'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),
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 assemble = (archive) =>
assembleAgentPackage({
platform,
projectRoot,
architecture: options.arch,
minimumDesktopVersion:
require('../package.json').version,
output: archive,
agentBundle: options.outputDirectory,
runtimeBundle: runtime.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
}
+640
View File
@@ -0,0 +1,640 @@
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$/u.test(path) ? '0755' : '0644'
}
function stagePackagePayload(options) {
const staging = options.staging
copyTree(options.agentBundle, join(staging, 'agent'))
copyTree(
options.runtimeBundle,
join(
staging,
'runtime',
'opencode',
basename(options.runtimeBundle)
)
)
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 {
stagePackagePayload({
staging,
registry,
agentLock,
runtimeLock,
agentBundle: resolve(options.agentBundle),
runtimeBundle: runtime.bundleDirectory
})
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
},
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({
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 })
})
return assembleAgentPackage({
...options,
projectRoot,
registry,
...(options.testSigningIdentity
? {}
: { signingIdentity }),
agentBundle,
runtimeBundle: runtime.bundleDirectory
})
} 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',
'--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 = {
platform: options.platform ?? 'linux',
architecture: options.arch,
minimumDesktopVersion: options.minimumDesktopVersion,
output: resolve(options.output)
}
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)
}
}
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)
}
}
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()
}
+618 -239
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
}
}
+12 -18
View File
@@ -17,8 +17,9 @@ const {
const { app, utilityProcess } = require('electron/main')
const protocol = 'goodbuddy.deepseek-harness.control'
const version = 1
const controlVersion = 3
const byteProtocol = 'goodbuddy.deepseek-harness.byte-stream'
const byteProtocolVersion = 1
const configuredHostPath =
process.env.GOODBUDDY_HARNESS_SMOKE_HOST
const hostPath = configuredHostPath
@@ -132,12 +133,12 @@ async function run() {
child.on('message', (message) => {
if (
message?.protocol === protocol &&
message.version === version &&
message.version === controlVersion &&
message.type === 'ready'
) {
child.postMessage({
protocol: byteProtocol,
version,
version: byteProtocolVersion,
type: 'data',
stream: 'stdin',
seq: 0,
@@ -147,7 +148,7 @@ async function run() {
}
if (
message?.protocol === byteProtocol &&
message.version === version &&
message.version === byteProtocolVersion &&
message.type === 'ack' &&
message.stream === 'stdin' &&
message.seq === 0
@@ -158,7 +159,7 @@ async function run() {
}
if (
message?.protocol === protocol &&
message.version === version &&
message.version === controlVersion &&
message.type === 'fatal'
) {
finish('fatal', String(message.code))
@@ -172,7 +173,7 @@ async function run() {
})
child.postMessage({
protocol,
version,
version: controlVersion,
type: 'start',
config: {
workspace,
@@ -181,20 +182,13 @@ async function run() {
api: 'openai-completions',
provider: 'goodbuddy',
model: 'qwen-plus',
harnessVersion: '0.1.0-rc.6',
sandbox: {
provider:
process.platform === 'win32'
? 'windows-acl'
: process.platform === 'darwin'
? 'seatbelt'
: 'local-linux',
enforcement:
process.platform === 'win32' ? 'partial' : 'full'
},
supportsImageInput: false,
requestHeaders: {},
harnessVersion: '0.1.2-rc.1',
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
skillPackages: [],
maxFrameBytes: 1024 * 1024
extensionPackages: [],
maxFrameBytes: 8 * 1024 * 1024
}
})
+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,
};
+59 -20
View File
@@ -32,13 +32,21 @@ function validateItems(value, label) {
fail(`${label} contains a non-string item`)
}
const normalized = item.trim()
if (!normalized || normalized.length > 240) {
fail(`${label} contains an empty or oversized item`)
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'])) {
@@ -63,28 +71,50 @@ function validateRelease(value, index) {
const notes = Object.fromEntries(
['zh-CN', 'en-US'].map((locale) => {
const localized = value.notes[locale]
if (!hasExactKeys(localized, ['features', 'fixes'])) {
const isCurrentFormat = hasExactKeys(
localized,
releaseNoteSections
)
const isLegacyFormat = hasExactKeys(
localized,
legacyReleaseNoteSections
)
if (!isCurrentFormat && !isLegacyFormat) {
fail(`${label}.notes.${locale} has invalid fields`)
}
const features = validateItems(
localized.features,
`${label}.notes.${locale}.features`
const normalized = Object.fromEntries(
releaseNoteSections.map((section) => [
section,
section in localized
? validateItems(
localized[section],
`${label}.notes.${locale}.${section}`
)
: []
])
)
const fixes = validateItems(
localized.fixes,
`${label}.notes.${locale}.fixes`
)
if (features.length + fixes.length === 0) {
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, { features, fixes }]
return [locale, normalized]
})
)
if (
notes['zh-CN'].features.length !== notes['en-US'].features.length ||
notes['zh-CN'].fixes.length !== notes['en-US'].fixes.length
) {
fail(`${label} localized section counts do not match`)
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,
@@ -126,14 +156,18 @@ const localizedDefinitions = [
{
locale: 'zh-CN',
title: `GoodBuddy ${release.version} 更新内容`,
highlights: '本次亮点',
features: '功能更新',
fixes: '问题修复'
fixes: '问题修复',
notices: '使用前请留意'
},
{
locale: 'en-US',
title: `What's New in GoodBuddy ${release.version}`,
highlights: 'Highlights',
features: 'Features',
fixes: 'Bug Fixes'
fixes: 'Bug Fixes',
notices: 'Before You Start'
}
]
@@ -151,8 +185,13 @@ const markdown = localizedDefinitions
...(index === 0 ? [] : ['---', '']),
`# ${definition.title}`,
'',
...markdownSection(
definition.highlights,
notes.highlights
),
...markdownSection(definition.features, notes.features),
...markdownSection(definition.fixes, notes.fixes)
...markdownSection(definition.fixes, notes.fixes),
...markdownSection(definition.notices, notes.notices)
]
})
.join('\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 })
}
+1 -1
View File
@@ -88,7 +88,7 @@ async function main() {
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version: '0.1.0-rc.6',
version: '0.1.2-rc.1',
private: true,
type: 'module'
},
+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
}
+165 -7
View File
@@ -2,6 +2,7 @@
const { spawn } = require('node:child_process')
const {
chmod,
copyFile,
mkdir,
mkdtemp,
@@ -11,7 +12,8 @@ const {
} = require('node:fs/promises')
const { statSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { join, resolve } = require('node:path')
const { delimiter, join, resolve } = require('node:path')
const { pathToFileURL } = require('node:url')
const unpackedPath = process.argv[2]
? resolve(process.argv[2])
@@ -28,20 +30,57 @@ const host = join(
'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']
[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 run(command, args, env) {
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: resolve('.'),
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
@@ -67,7 +106,24 @@ async function main() {
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'),
@@ -106,7 +162,7 @@ async function main() {
)
const packageArguments = [
resolve('node_modules/electron-builder/cli.js'),
resolve('build/run-electron-builder.cjs'),
'--projectDir',
project,
'--win',
@@ -116,9 +172,16 @@ async function main() {
'never',
`--config.directories.output=${join(root, 'dist')}`
]
if (process.env.GOODBUDDY_ELECTRON_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=${resolve(process.env.GOODBUDDY_ELECTRON_DIST)}`
`--config.electronDist=${electronDist}`
)
}
const packaged = await run(
@@ -153,7 +216,102 @@ async function main() {
`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 })
}
+206 -146
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,81 +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 projectPackage = JSON.parse(
await readFile(join(projectDir, 'package.json'), 'utf8')
)
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)
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.
@@ -191,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 }
+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
}
@@ -5,39 +5,49 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-08-13 |
| 版本 | 0.3 |
| 日期 | 2026-08-19 |
| 适用产品 | GoodBuddy 桌面端 |
| 文档角色 | 自动任务、目标、并行实验、会话监督、分区记忆与持续学习的总纲 |
| 文档角色 | Task/Job、调度、目标、并行实验、会话监督、分区记忆与持续学习的总纲 |
| 领域模型 | [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md) |
## 1. 背景
GoodBuddy 当前已经具备若干长期助手能力,但它们仍是彼此分离的功能:
1. 定时任务支持单次、每日和每周触发,创建 Ask 任务并保存任务和成果。
2. 智能心跳支持全局或项目范围的每日、每周回顾,读取有界会话、任务和已确认记忆,
1. 当前 Schedule 已支持单次、每日和每周触发,并在创建时绑定稳定产品级 Task 与真实
Conversation;重复触发复用同一身份,文本结果写回 Conversation,独立文件和图片保存为
Artifact。到期执行与用户在回复期间继续发送的普通消息共用 Conversation 级持久队列,
因而不会与当前回复并发写入同一时间线。IANA 时区、Cron、事件触发、租约、重试和完整
Job/Run 抽象仍待实现。
2. 当前智能心跳支持全局或项目范围的每日、每周回顾,读取有界会话、任务和已确认记忆,
生成摘要、记忆建议和后续任务。
3. 专家子任务支持有限并发和只读综合,但没有实验变量、重复运行、统一指标和结果晋升。
3. 专家执行的 Job 支持有限并发和只读综合,但没有实验变量、重复运行、统一指标和结果晋升。
4. 记忆已有全局、项目、会话三种作用域,以及偏好、事实、摘要、流程四种类型,
但检索、来源、时态、冲突和运行级隔离仍不完整。
5. 魔法笔记已经提供“内容旁持续出现 AI 评论”的交互,可作为会话监督的体验参考,
但它只分析笔记或待办,不观察会话和任务运行。
如果继续把更多能力加入“智能心跳”,心跳将同时承担调度、总结、执行、监督、学习和
记忆管理,最终无法解释一次后台行为为什么发生、读取了什么、是否越权、产生了什么影响。
智能心跳长期方向是面向未来的分区记忆,但该模型尚未设计。近期只改善现有心跳的权威入口
与 Global / 多 Project 范围,并保留 Task Center 作为 Task 索引。若继续把 Task、调度、
监督、学习和执行加入“智能心跳”,将无法解释一次后台行为为什么发生、读取了什么、是否
越权、产生了什么影响。
本设计将这些能力统一到一个平台模型中,同时保留不同产品的清晰边界。
## 2. 核心产品判断
### 2.1 不把心跳升级成万能后台 Agent
### 2.1 智能心跳保持独立,分区记忆协同另行设计
智能心跳应继续承担周期性观察和回顾,不直接成为所有自动化的宿主。
当前智能心跳继续承担周期回顾、报告和建议,并支持 Global 或指定 Project 范围。它不是
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。按分区回顾、候选生成
以及时间/内部事件唤起需要独立设计,不能从当前心跳行为直接推导,也不创建平行记忆类型。
- 定时任务解决“何时执行一个已知任务”。
- 目标任务解决“围绕结果持续规划和推进”。
- Scheduled Task 解决“何时在一个 Task 中执行新的 Job”。
- Goal Task 解决“围绕结果在同一 Task 中持续规划和推进”。
- 并行实验解决“隔离多个候选并用相同标准比较”。
- 会话监督解决“独立观察并在必要时评论、告警或暂停”。
- 智能心跳当前解决“在什么范围周期回顾并提出报告和建议”。
- 记忆系统解决“哪些经验可以在什么范围内被未来运行读取”。
- 持续学习解决“候选经验如何经过评估后改变未来行为”。
@@ -77,7 +87,7 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
- 工具权限和审批策略。
- Electron 安全边界。
- 项目根目录和数据访问范围。
- Runtime 沙箱。
- Runtime 当前用户执行权限与 Ask/Execute 边界。
- 系统级提示词。
- 远程消息发送或其他外部副作用策略。
@@ -85,8 +95,9 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
### 3.1 用户目标
- 用统一入口创建定时、事件、目标和实验型自动任务。
- 清楚知道自动任务的触发原因、当前目标、运行状态、预算和停止条件。
- 在现有 Task Center 中找到 Scheduled、Event 和 Goal Task,并直接打开关联 Conversation
和对应 Task。
- 清楚知道 Task 的触发原因、当前目标、聚合执行状态、预算和停止条件。
- 在一个工作台中观察多个候选运行,并追溯结论到原始证据。
- 为重要会话启用独立监督,及时发现偏题、遗漏、矛盾、证据不足和风险。
- 知道每条记忆属于哪个范围、从哪里产生、何时有效以及被哪些运行使用。
@@ -95,6 +106,8 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
### 3.2 产品目标
- 复用现有 Project、Conversation、Task、Run、Artifact、Approval 和 Notification 能力。
- 保持每个 Task 只关联一条 Conversation,同时允许一条 Conversation 承载多个 Task,不为
同一项工作建立第二份内容载体。
- 为所有后台工作提供统一的幂等、租约、恢复、取消、预算和审计语义。
- 保持 Ask 只读,Execute 继续经过现有能力和审批控制。
- 保持本地优先,应用退出后不虚假承诺后台持续执行。
@@ -117,20 +130,28 @@ SQLite、FTS 和可选本地向量已经足够支撑第一阶段。只有出现
## 5. 统一领域模型
以下模型是 Scheduled Task、Goal Task 和实验共享的技术基础,不要求新增独立
Automation Center。`AutomationPlan` 是 Task 的计划配置,`Job` 是 Task 内执行单位,
`Run` 是执行尝试。用户主要通过 Task Center、左侧会话 Task 列表和关联 Conversation
理解工作;当前 UI 不展示 Job/Run 层级。智能心跳
不属于此模型。
### 5.1 核心实体
```text
AutomationPlan
├─ TriggerPolicy
├─ ObjectiveSet
├─ ExecutionProtocol
├─ BudgetPolicy
├─ ApprovalPolicy
├─ SupervisorPolicy
└─ MemoryBinding
│
└─ AutomationRun
├─ Task / Child Task
Conversation
└─ Task 0..N
├─ AutomationPlan(可选)
│ ├─ TriggerPolicy
│ ├─ ObjectiveSet
│ ├─ ExecutionProtocol
│ ├─ BudgetPolicy
│ ├─ ApprovalPolicy
│ ├─ SupervisorPolicy
│ └─ MemoryBinding
└─ Job
├─ Run
├─ Subjob
├─ Observation
├─ SupervisorDecision
├─ Artifact
@@ -140,15 +161,17 @@ AutomationPlan
| 实体 | 职责 |
| --- | --- |
| `AutomationPlan` | 用户可编辑的长期定义,描述做什么、为何做、何时做和允许做什么 |
| `Task` | 用户可见工作单位,只关联一条 Conversation;Conversation 可以承载多个 Task |
| `Job` | Task 内部一次步骤、触发、并行分支或委派执行 |
| `Run` | Task/Job 的一次执行尝试和审计记录 |
| `AutomationPlan` | Task 的可编辑计划配置,描述做什么、为何做、何时做和允许做什么 |
| `TriggerPolicy` | 手动、时间、事件或条件触发,以及错过执行策略 |
| `ObjectiveSet` | 成功标准、优化指标、约束和停止条件 |
| `ExecutionProtocol` | 本次运行冻结的提示、步骤模板、变量、Runtime、工具和数据范围 |
| `BudgetPolicy` | 最大耗时、模型调用、Token、工具次数、子任务数、成果大小和并发 |
| `BudgetPolicy` | 最大耗时、模型调用、Token、工具次数、Job/Subjob 数、成果大小和并发 |
| `ApprovalPolicy` | 哪些动作可自动执行、哪些等待批准、哪些禁止 |
| `SupervisorPolicy` | 观察维度、触发频率、干预级别和确定性门禁 |
| `MemoryBinding` | 运行可读取和可写入哪些记忆分区 |
| `AutomationRun` | 一次触发产生的不可变运行快照和聚合状态 |
| `Observation` | 对消息、步骤、工具、指标或系统状态的结构化观察 |
| `SupervisorDecision` | `continue`、`comment`、`warn`、`request_review`、`pause` 或 `stop` |
| `Metric` | 可复现的运行指标及其计算来源 |
@@ -161,12 +184,11 @@ AutomationPlan
| 类型 | 说明 |
| --- | --- |
| `scheduled_task` | 到点运行一个固定任务 |
| `heartbeat_review` | 周期性观察会话、任务和记忆,输出回顾和建议 |
| `goal_loop` | 围绕目标重复执行“观察、计划、行动、评估” |
| `experiment` | 生成隔离候选 Run,按统一协议评估和比较 |
会话监督不是独立执行任务。它是可附着到 Conversation、Task、AutomationRun 或
Experiment 的 `SupervisorPolicy` 和监督会话。
会话监督不是独立 Task。用户选择 Conversation、Task 或 Experiment 作为监督对象;
`SupervisorPolicy` 可以在内部观察所属 Job/Run 事件,但当前 UI 不把它们作为独立目标。
### 5.3 运行快照
@@ -183,7 +205,8 @@ Experiment 的 `SupervisorPolicy` 和监督会话。
- 预算和并发限制。
- 审批策略。
运行开始后的设置变化只影响下一次 Run。用户可以查看当前 Run 与最新 Plan 的差异。
运行开始后的设置变化只影响下一次 Run。用户可以在 Task 执行记录中查看当次快照与最新
Plan 的差异,但 Run 不作为独立导航对象。
## 6. 统一状态模型
@@ -235,34 +258,59 @@ inactive → observing → attention_required → paused → resolved
```text
Trigger
→ AutomationCoordinator 声明 Run
→ RunQueue 按优先级和预算排队
→ AutomationExecutor 创建 Task
→ AutomationCoordinator 在所属 Task 内声明 Job
→ ExecutionQueue 按优先级和预算排队
→ AutomationExecutor 为 Job 创建或恢复 Run
→ Runtime 执行
→ Supervisor 观察
→ Evaluator 计算指标
→ 结果、证据和候选记忆入库
→ 用户审查或后续 Run
→ 用户审查或后续执行
```
`AutomationCoordinator` 只负责触发、声明和恢复,不直接调用模型。执行仍通过任务和 Runtime
边界完成。
`AutomationCoordinator` 只负责触发、声明和恢复,不直接调用模型。执行仍通过 Job 和
Runtime 边界完成,用户可见结果通过所属 Task 汇入关联 Conversation。
### 7.2 优先级
默认优先级从高到低:
1. 用户正在等待的前台对话。
2. 用户手动启动的 Run。
3. 等待批准后恢复的 Run。
4. 到期定时任务。
5. 目标循环和实验 Run。
2. 用户手动启动的 Task 执行。
3. 等待批准后恢复的 Task 执行。
4. 到期 Scheduled Task 的执行。
5. 目标循环和实验执行。
6. 心跳回顾、记忆巩固和维护。
后台任务必须可被背压延后。延后记录为 `deferred`,不得丢失,也不得在系统恢复空闲时一次性
释放全部积压。
### 7.3 幂等和租约
### 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` 形成幂等键。
- 手动触发使用调用方提供的单次幂等键。
@@ -372,43 +420,40 @@ Trigger
## 12. 信息架构
建议将现有“智能心跳”逐步扩展为“自动化中心”,但保留心跳作为一种计划:
当前阶段保留任务中心并适度完善,不新增独立自动化中心。智能心跳使用自己的菜单入口,
并已在现有模型上实现 Global / 多 Project 范围与唯一配置入口:
```text
自动化中心
├─ 概览
│ ├─ 正在运行
│ ├─ 等待审批
│ ├─ 需要关注
│ └─ 最近结果
├─ 计划
│ ├─ 定时任务
│ ├─ 智能心跳
│ ├─ 目标任务
│ └─ 实验
├─ 运行
│ ├─ 时间线
│ ├─ 任务与步骤
│ ├─ 监督记录
│ ├─ 指标与证据
│ └─ 成果
├─ 建议
│ ├─ 记忆候选
│ ├─ 后续任务
│ └─ 学习候选
└─ 设置
├─ 全局预算
├─ 后台优先级
├─ 通知
└─ 数据保留
任务中心
├─ 需要关注
├─ 进行中
├─ 已暂停
└─ 已结束
└─ 打开任务自身
任务自身
├─ 消息时间线
├─ Run、步骤与审批活动
├─ 监督、指标与证据
└─ 独立成果
智能心跳
├─ 运行概览
├─ 待处理建议
├─ 报告与记录
└─ 心跳计划
└─ Global / 指定 Project
```
会话页面增加可折叠“监督”右栏,与任务、上下文和成果并列,或在已有右侧工作栏中新增页签。
监督统一进入应用级助手工作栏中固定且始终可访问的“监督”栏目,不再保留“独立可折叠右栏”
和“动态新增页签”两种实现。栏目默认跟随当前上下文,用户可以固定到其他 Conversation、
Task 或实验对象;详细范围与交互契约见
[通用助手工作栏与执行空间 PRD](../features/assistant-workbar/prd.md)。
## 13. 安全与隐私
1. Ask 在 Runtime 边界保持只读,而不只是提示词要求只读。
2. Execute 继续通过现有审批、沙箱、工具和目录控制。
2. Execute 继续通过现有审批、主机执行策略、工具和目录控制。
3. 无人值守只允许用户显式批准的能力集合;遇到未预授权动作时进入等待审批。
4. Supervisor、Evaluator 和 Heartbeat 都把消息、工具输出、记忆和成果视为不可信数据。
5. 监督器不能读取隐藏推理,只能读取产品允许持久化和展示的事件。
@@ -421,7 +466,7 @@ Trigger
## 14. 可观测性
每个 Run 至少展示:
每次 Task 执行的内部 Job/Run 记录至少保存:
- 触发来源和计划版本。
- 计划目标和当前 `goalStatus`。
@@ -429,13 +474,14 @@ Trigger
- 实际读取的知识库与记忆分区。
- 实际调用的模型、Token、工具、耗时和成果大小。
- 当前预算和剩余预算。
- 任务、步骤和子任务状态。
- Task、Job 和 Subjob 状态。
- Supervisor 评论、证据、严重度和处理结果。
- 评估器版本、指标和证据。
- 产生的候选记忆或学习产物。
- 重试、延后、中断和恢复原因。
不得只显示一个模糊的“自动化成功率”而隐藏失败 Run、跳过 Run 或无结论 Run。
UI 在 Task 下呈现上述信息的有界摘要和活动,不提供 Job/Run 树或独立导航。不得只显示一个
模糊的“自动化成功率”而隐藏失败、跳过或无结论的 Task 执行。
## 15. 建议的数据模型增量
@@ -462,15 +508,20 @@ experiment_runs
```
现有 `schedules`、`schedule_runs`、`heartbeat_configs`、`heartbeat_runs`、
`heartbeat_entries`、`tasks` 和 `runs` 不应一次性重写。迁移顺序应先增加统一只读视图和
关联字段,再逐步让新计划使用统一模型。
`heartbeat_entries`、`tasks` 和 `runs` 不应一次性重写。Schedule 可渐进建立稳定 Task 与
Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心跳数据保持独立,不得
静默转成 `AutomationPlan` 或顶层 Task。分区记忆协同和唤起条件完成设计前,不新增迁移目标。
## 16. 分阶段实施
### 阶段 0:统一术语和可观测性
- 固定 Plan、Run、Goal、Protocol、Supervisor、Observation、Memory Candidate 等概念。
- 为现有定时任务、心跳和专家子任务建立统一活动视图。
- 固定 Task、Conversation、Job、Subjob、Run、Plan、Goal、Protocol、Supervisor、
Observation、Memory Candidate 等概念。
- 明确 Task N:1 Conversation 关系、左侧行首展开按钮与 Task 子项图标,以及 Task Center
索引边界,不复制内容。
- 为现有 Scheduled Task 和专家执行建立按 Task 聚合的活动视图。
- 明确当前心跳保持独立,分区记忆协同与唤起条件尚待设计。
- 补充触发来源、运行版本、预算和读写范围展示。
### 阶段 1:调度与运行基础
@@ -483,6 +534,7 @@ experiment_runs
### 阶段 2:会话监督与分区记忆
- 上线评论型会话监督。
- 智能心跳配置支持 Global 或指定一个、多个 Project。
- 增加 Automation 和 Run 记忆分区。
- 建立来源、证据、时态、冲突和晋升流程。
@@ -506,20 +558,29 @@ experiment_runs
## 17. 相关文档
- [自动任务、目标与调度 PRD](./automation-goals-and-scheduling-prd.md)
- [并行实验工作台 PRD](./parallel-experiments-prd.md)
- [会话监督 PRD](./conversation-supervision-prd.md)
- [分区记忆 PRD](./partitioned-memory-prd.md)
- [持续学习与评估门 PRD](./continuous-learning-prd.md)
- [GoodBuddy 长期助手功能规划](../long-term-assistant-roadmap.md)
- [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. 总体验收标准
- [ ] 心跳、定时、目标和实验使用统一的 Plan 与 Run 术语。
- [ ] 每个自动 Run 都能解释触发原因、目标、范围、预算、状态和结果。
- [ ] 智能心跳保持独立,不作为 Task 类型;分区记忆协同与唤起条件尚未设计。
- [ ] Task Center 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以
关联多个 Task。
- [ ] 当前 UI 只展示到 Task,不提供 Job/Subjob/Run 树或独立导航。
- [ ] Scheduled Task 的重复触发和并行 Job 不创建新的顶层 Task。
- [ ] 每次自动执行的内部 Run 都记录触发原因、目标、范围、预算、状态和结果,并在所属
Task 下提供有界可观测信息。
- [ ] Ask 自动化无法调用写工具或产生外部副作用。
- [ ] Execute 自动化不能绕过现有审批、沙箱和能力控制。
- [ ] Execute 自动化不能绕过现有审批、主机执行策略和能力控制。
- [ ] 会话监督默认只评论,不能替用户发言或批准工具。
- [ ] 并行 Run 的变量、会话、运行记忆、任务和成果相互隔离。
- [ ] 失败 Run 不参与最佳结果选择,全部失败不报告成功。
File diff suppressed because it is too large Load Diff
@@ -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)
File diff suppressed because it is too large Load Diff
-678
View File
@@ -1,678 +0,0 @@
# GoodBuddy 自维护 DeepSeek Harness Runtime 设计
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档状态 | 实现与发布验收基线 |
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
| Runtime 标识 | `deepseek-harness` |
| 首版依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.6` |
| 上游状态 | Developer Preview,允许出现破坏性变更 |
| 上游许可证 | MIT |
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 本文性质 | 设计与发布验收约定 |
本文定义 DeepSeek Harness 在 GoodBuddy 中的架构边界、协议、安全策略、界面、打包和验收要求。实现必须继续遵守 GoodBuddy 已有的 Main 进程安全边界、Ask/Execute 语义、授权、取消、超时、有界输出和资源回收约定。
## 2. 摘要
DeepSeek Harness 的底层库使用 Cordis 组合服务。GoodBuddy 不采用官方产品 profile、插件安装或市场机制,也不让用户配置覆盖安全服务,而是增加一个实验性的第三 Runtime,并完全自行维护 Host、控制协议、生命周期和兼容层。上游 DSH 包只是精确锁定并逐次审查的实现依赖,不构成 GoodBuddy 对 DSH 插件 ABI、插件目录或产品路线的承诺。
GoodBuddy 并不迫切于把该能力做成 DSH 插件或进入插件市场。当前优先级是向用户提供稳定、可靠、可审计且可完整回收的 Runtime;只有未来真实用户需求和成熟度证明插件化确有价值时,才重新评估该形态。
整体分成两个互相约束的部分:
1. **GoodBuddy Main Control Plane**
- 运行在 Electron Main 进程。
- 持有加密设置、模型连接选择、Ask 拒绝与 Execute 自动授权策略、Runtime 生命周期和审计归属。
- 通过 Electron `utilityProcess` 启动受控 Harness 子进程。
- 对环境、输入、输出、超时、取消和进程树执行强制限制。
2. **GoodBuddy Harness Control Plane**
- 运行在 Harness 子进程内,是 Host 私有的内部控制组件,不导出 Cordis 插件入口。
- 使用 ACP 兼容的 JSON-RPC stdio 作为基础控制面。
- 增加 GoodBuddy 所需的能力握手、每轮权限准备、会话释放、工具事件、推理、用量和安全凭据请求扩展。
- 与 GoodBuddy Host 一起维护、构建和发布,不设计为独立 npm 包、`dsh.bundle` 或市场插件。
DeepSeek Harness 不替换 OpenCode、Continue 或直连模型 Runtime。用户可以按全局、项目、会话或消息通道继续选择现有 Runtime。
## 3. 背景与上游能力
### 3.1 已确认的官方能力
- `@deepseek-ai/dsh` 是官方 profile 启动器。
- Harness 插件是导出 `apply(ctx, config)` 的 Cordis 模块。
- npm 包可通过 `dsh.bundle` 声明配置补丁,再通过 `dsh plugin --profile <name> add <package>` 安装。
- ACP 支持:
- 初始化。
- 创建多个会话。
- 发送 Prompt。
- 按会话取消。
- 一次性权限选择。
- 已提交的助手文本。
- 官方本地沙箱支持:
- Linux:Bubblewrap,或 Landlock 降级。
- macOS:Seatbelt。
- Windows:ACL 受限令牌,官方明确标记为部分强制执行。
### 3.2 官方通道的缺口
官方 ACP 插件有意只输出已提交文本,不输出推理、工具进度、计划、标题和用量。它也没有标准的会话关闭方法。SDK JSON-RPC 的展示事件更完整,但缺少 GoodBuddy 需要的单轮取消和权限回传。
因此,首版不单独选用其中一个官方通道作为完整实现。GoodBuddy Harness Control Plane 以 ACP 语义为基础,补充有命名空间的扩展方法和事件。
### 3.3 自维护边界
GoodBuddy 不急于把该 Runtime 包装成标准 DSH 插件,也不以进入官方或第三方插件市场为近期目标。所有入口都随 GoodBuddy 发布,只有 GoodBuddy Main 可以启动并使用内部 Host。是否采用上游新版本或未来重新评估插件形态,只由真实用户价值、安全审查和六平台稳定性决定,不跟随市场机制或上游发布节奏。
## 4. 目标与非目标
### 4.1 首版目标
- 增加 `deepseek-harness` Runtime,并在设置、聊天和消息通道中可选择。
- 使用 GoodBuddy 管理的模型连接,不在 Renderer 或持久化 Harness 配置中写入 API Key。
- Ask 模式在 Runtime 边界强制只读,并禁止任何权限升级。
- Execute 模式下的工具权限请求由 Main 自动给予单次授权,不弹出交互审批;默认文件模式仍为 `workspace-write`,越界仅允许在真实沙箱拒绝后对完全相同操作单次重试。
- 支持多会话、同会话串行、跨会话并行。
- 支持按请求取消、超时、会话释放和应用退出时完整回收。
- 输出文本、推理、工具参数、工具结果、stderr 和协议队列全部有界。
- 使用真实 OpenAI 兼容 Chat Completions 模型验证调用,而不在日志、测试产物或提交中暴露凭据。
- 保留 Windows、macOS、Linux 的 x64 和 arm64 发布能力。
### 4.2 首版非目标
- 不替换 OpenCode、Continue 或直连模型 Runtime。
- 不开放用户 Cordis profile、cordis.patch.yml 或 $DSH_HOME 全局补丁覆盖。
- 不提供外部 Host、自定义 Harness Control Plane、DSH 插件安装或市场入口。
- 不加载 Harness Web UI、HMR、遥测、自动更新或目录选择器。
- 不支持 `danger-full-access` 作为会话默认值或持久设置。
- 不向 Utility 暴露 MCP 凭据或建立直连 MCP Client。只有用户明确分配给 Harness 的 MCP 工具可以通过 Main 代理调用。
- 不在首版向 Harness 暴露 GoodBuddy 浏览器控制、知识库或 Magic Notes。
- 不在首版支持图像输入、会话恢复、Harness Subagent、后台 Job、Hook、Web Search 或 Workflow。
- 不发布独立 npm 包,也不创建上游 PR。
## 5. 核心设计决策
### 5.1 第三个独立 Runtime
`deepseek-harness` 是明确的 Runtime 类型,不伪装成 `model`、`opencode` 或 `continue`。共享契约、设置迁移、Runtime 选择、检测、聊天标签、消息通道和模型用量都使用同一个稳定标识。
### 5.2 受控组合,不启动用户 profile
GoodBuddy 使用自己固定的 Harness Host 入口和只读组合模板,不调用 `dsh web`,也不启动用户已有 profile。运行时禁止以下来源参与组合:
- 当前工作目录的 `.env`。
- 用户 Harness Home 的 `.env`。
- `$DSH_HOME/cordis.patch.yml`。
- 用户 profile 的 `cordis.patch.yml`。
- 任意 `--patch`。
- HMR 和动态插件安装。
模型名称、服务地址、工作区和非秘密策略通过严格校验的 Main 配置传给 Host。API Key 只通过受控凭据通道按需提供,不写入 YAML、命令行、Renderer 或日志。
### 5.3 双层内部控制面
Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不能代替进程内的 Session/Tool 适配层:
- Harness Control Plane 最接近 Session、Agent、Tool、Usage 和权限 seam,适合做内部协议转换。
- Main 控制面是可信安全边界,适合持有模式授权策略、加密设置、进程控制和 IPC。
任何一侧缺失能力握手时,Runtime 必须报告不可用,不能降级为不受控执行。
### 5.4 GoodBuddy 继续拥有持久会话
首版不启用 Harness JSONL 会话持久化和 SQLite 会话索引。原因如下:
- GoodBuddy 已经持久化对话、消息、活动、工具事件和用量。
- 再写一份 Harness 日志会扩大敏感数据副本和清理范围。
- GoodBuddy 在 Runtime 重启后可以用现有的有界历史创建新 Harness Session。
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
## 6. 总体架构
```text
Renderer
│ 显式、经 schema 验证的 preload API
▼
Electron Main
├─ RuntimeSettingsStore
├─ AgentRuntimeController
├─ RuntimeAuthorizer(Ask 拒绝 / Execute 自动单次授权)
└─ DeepSeekHarnessRuntime / Main Control Plane
│ ACP + goodbuddy/* 扩展,stdin/stdout
▼
Electron utilityProcess
└─ GoodBuddy Harness Host
├─ 固定 Cordis 组合
├─ GoodBuddy Harness Control Plane(内部组件)
├─ DSH Agent 与 LLM seam
├─ DSH Sandbox Policy
├─ 沙箱 Shell / Filesystem
└─ 最小工具集
│ HTTPS
▼
用户选择的 OpenAI 兼容模型连接
```
### 6.1 信任边界
| 区域 | 信任级别 | 允许持有的内容 |
| --- | --- | --- |
| Renderer | 不可信展示层 | 脱敏设置、状态、用户可见事件 |
| Preload | 窄桥 | 明确方法和共享 schema |
| Electron Main | 可信控制面 | 加密设置、模式授权策略、Runtime 生命周期 |
| Harness utilityProcess | 不可信执行面 | 当前请求、临时凭据、受控工具和工作区权限 |
| Harness 工具子进程 | 最低信任 | 单次命令所需的最小环境和沙箱能力 |
Harness 子进程崩溃、输出异常、拒绝协议、加载错误或沙箱不可用时,Main 必须失败关闭。
## 7. GoodBuddy Harness Control Plane
### 7.1 内部组件职责
控制面负责:
- 启动 ACP 兼容的 JSON-RPC stdio 服务。
- 创建、查找和释放 Harness Agent。
- 在 Prompt 前应用 GoodBuddy 指定的 Ask/Execute 权限。
- 将 DSH Session 事件转换为有界的 GoodBuddy 事件。
- 将权限请求转发到 Main,并只接受一次性结果。
- 将 LLM 用量转换为稳定的模型用量事件。
- 在 dispose 时先取消 Agent,再等待子 Agent 和工具清理。
- 保证 stdout 只包含协议帧,诊断只写 stderr。
控制面不负责:
- 保存 GoodBuddy 设置。
- 持久保存 API Key。
- 决定 Main 的模式授权结果。
- 直接访问 Renderer 或 Electron API。
- 接受用户提供的插件、Host 或 profile 覆盖。
- 自行上传遥测。
### 7.2 非插件约束
控制面不导出 `apply(ctx, config)`,不提供默认 stdin/stdout 入口,不包含 `dsh.bundle`、`cordis.patch.yml` 或可安装 manifest,也不接受 Host 之外创建的 transport。它可以保留清晰的内部模块边界以便测试和维护,但该边界不是公开扩展点。
若未来确有来自 GoodBuddy 真实用户、经过研究验证的扩展需求,应先重新完成产品需求、威胁模型和兼容策略评审;不得因为上游已经提供插件或市场机制而默认开放。
## 8. 协议设计
### 8.1 传输
- stdin/stdout 使用换行分隔 JSON-RPC。
- stdout 不得出现日志、Banner、进度条或调试输出。
- stderr 只允许有界诊断,不得包含 Prompt、工具完整输出或凭据。
- 每一帧、每一字段和每个请求累计输出都必须在解析前或接收时限流。
### 8.2 标准 ACP 方法
首版保留 ACP 的初始化、`session/new`、`session/prompt` 和 `session/cancel` 语义。标准 ACP 客户端可以使用只读默认行为,但只有完成 GoodBuddy 能力握手的客户端才能启用 Execute。
### 8.3 GoodBuddy 扩展
扩展统一使用 `goodbuddy/` 命名空间:
| 方法或事件 | 方向 | 用途 |
| --- | --- | --- |
| `goodbuddy/handshake` | Main → Control Plane | 交换控制协议、Harness、ACP 版本和能力 |
| `goodbuddy/session/prepare` | Main → Control Plane | 在下一次 Prompt 前设置工作模式和请求标识 |
| `goodbuddy/session/release` | Main → Control Plane | 取消并释放指定 Session |
| `goodbuddy/session/event` | Control Plane → Main | 文本、推理、工具、状态和用量事件 |
| `goodbuddy/credential/resolve` | Control Plane → Main | 按已登记引用请求当前 Runtime 的临时凭据 |
| `goodbuddy/tools/list` | Control Plane → Main | 取得用户分配给 Harness 的有界 MCP 工具 schema |
| `goodbuddy/tools/call` | Control Plane → Main | 通过当前 Execute 请求、schema 校验和自动单次授权调用 MCP |
| `goodbuddy/shutdown` | Main → Control Plane | 停止接收新请求并有序清理 |
扩展版本独立于 ACP 版本。握手响应至少包含:
```ts
type GoodBuddyHarnessCapabilities = {
controlProtocolVersion: 1
harnessVersion: string
acpProtocolVersion: number
supports: {
cancellation: true
sessionRelease: true
oneShotApproval: true
reasoningEvents: boolean
toolEvents: boolean
usageEvents: boolean
}
sandbox: {
provider: string
enforcement: 'full' | 'partial'
}
}
```
版本不兼容、必需能力缺失或 `sandbox.enforcement` 不满足设置要求时,Main 不得开始模型请求。
### 8.4 每轮权限准备
GoodBuddy 的工作模式属于每个请求,不属于 Runtime 进程全局状态。同一对话可以在 Ask 和 Execute 之间切换。因此:
1. `session/new` 后默认是 `read-only + never`。
2. 每个 Prompt 前,Main 发送一次 `goodbuddy/session/prepare`。
3. Harness Control Plane 将准备状态绑定到 `sessionId + requestId`。
4. `session/prompt` 只能消费匹配且尚未使用的准备状态。
5. 缺少准备状态、重复使用、请求标识不匹配时,Control Plane 使用只读且禁止授权的安全默认值,或直接拒绝请求。
6. 同一 Session 只允许一个 Prompt 在途。
### 8.5 事件模型
Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
- `status`:简短运行状态。
- `text`:已提交的助手文本分片。
- `reasoning`:可选的有界推理摘要分片。
- `tool`:工具 ID、名称、状态和有界输入/输出摘要。
- `model-usage`:模型、提供方、输入、输出和缓存 Token。
- `done`:停止原因和 Session ID。
禁止发送原始 Cordis Context、完整环境、内部对象、堆栈中的凭据或无界 Session 日志。
## 9. Runtime 生命周期
### 9.1 进程模型
- 每个活动的 DeepSeek Harness Runtime 实例拥有一个 `utilityProcess`。
- 一个进程可以承载多个 Harness Session。
- 同一 GoodBuddy 对话的 Prompt 串行执行。
- 不同对话可以并行,但受全局并发上限控制。
- Runtime 设置变化时创建新实例,旧实例等待在途请求结束或在宽限期后被取消。
### 9.2 会话映射
Main 保存内存映射:
```text
GoodBuddy conversationId -> Harness sessionId + process generation
```
- 首次请求创建 Session。
- 已有 Session 只发送当前 Prompt。
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的有界历史。
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
### 9.3 取消与超时
- 用户取消时立即发送 `session/cancel`。
- 取消等待有界,超时后关闭连接并终止整个 Harness 进程。
- 初始化、握手、Session 创建、Prompt、权限回传和关闭分别使用独立超时。
- Prompt 超时与用户取消使用不同错误类型,不能被宽泛 catch 抹平。
- 取消后仍可接收并丢弃该请求的最终协议结算帧,但不得写入下一请求。
### 9.4 释放与退出
- 删除或释放对话时调用 `goodbuddy/session/release`。
- Runtime dispose 时先拒绝新请求,再取消所有 Session。
- Harness Control Plane 完成 Agent、工具和会话清理,Host 完成 Cordis Fiber 与子进程的反向清理。
- Main 在宽限期内等待正常退出。
- 超时后终止 utilityProcess,并在平台允许时清理完整进程树。
- 应用退出不得因 Harness 清理无限阻塞。
## 10. 权限与沙箱
### 10.1 模式映射
| GoodBuddy 模式 | DSH 文件模式 | DSH 权限策略 | 行为 |
| --- | --- | --- | --- |
| Ask | `read-only` | `never` | 允许受控读取,不允许写入,不允许升级 |
| Execute | `workspace-write` | `ask` | 允许工作区与受控临时目录写入;权限请求由 Main 自动单次授权,不弹出交互审批 |
`danger-full-access` 只能作为某个已被沙箱拒绝的完全相同操作的一次性、更宽重试。Main 仅对该次重试自动返回 `allow-once`;它不能保存为默认值、复用于后续操作,或通过“始终允许”返回。
### 10.2 Ask 模式
- Main 即使收到权限请求也固定拒绝。
- Harness Control Plane 禁止 `sandbox_permissions` 升级。
- 文件写入和 Shell 写入都由 DSH 共享 Sandbox Policy 强制拒绝。
- 只读不等于无限输出,读取仍受路径、字节和工具结果上限控制。
- 首版不向 Ask 暴露 GoodBuddy 的可变数据工具。
### 10.3 Execute 模式
- 工作区根来自 Session 创建时的规范化绝对路径。
- 工具不能自行更换工作区根。
- 工作区内操作按 DSH `workspace-write` 执行。
- 只有真实沙箱拒绝后的同一操作,才可请求一次升级。
- Main 不调用 `ToolApprovalBroker`,而是对当前 Execute 请求自动返回 `allow-once`;界面不进入等待审批状态,也不弹出审批对话框。
- 所有工具调用仍作为活动事件记录;Ask 和 delegation 路径继续固定拒绝。
- Harness Control Plane 不接受 `allow_always`,也不把未知结果解释为允许。
### 10.4 沙箱可用性
- `strict`:要求完整强制执行。仅有 `partial` 或无 Runner 时 Runtime 不可用。
- `auto`:允许官方报告的 `full` 或 `partial`,但必须在状态卡显示实际强制程度。
- `off`:不允许 Harness 退化到无限制工具执行。首版将 Execute 标记为不可用,Ask 仍只能在可强制只读时运行。
Windows ACL 和旧 Linux Landlock 可能只报告 `partial`。界面和诊断必须如实显示,不能写成“完全隔离”。
### 10.5 环境与凭据
- 使用环境变量白名单构造 utilityProcess 环境。
- 不继承 `NODE_OPTIONS`、调试端口、任意 npm 配置、用户 `DSH_*` 覆盖或白名单之外的凭据。
- `DSH_TELEMETRY_DISABLED=1` 必须固定设置。
- Harness Home 指向 GoodBuddy 管理的隔离目录。
- 不调用官方 `loadEnv` 或 `loadLayeredEnv`。
- API Key 由 Main 从加密设置中解析。
- Harness Control Plane 只能用已握手登记的引用通过 `goodbuddy/credential/resolve` 请求当前 Runtime 的凭据。
- 凭据只在模型请求所需的子进程内存中短暂存在,不写磁盘、不进入工具环境、不打印。
## 11. 受控 Harness 组合
首版只加载完成文本对话、受控代码操作和用户明确分配能力所需的固定服务:
- Agent、Session、LLM 和 Tool Registry 基础服务。
- GoodBuddy Harness Control Plane。
- OpenAI 兼容 Chat Completions LLM 适配器。
- Sandbox Policy 与平台 Sandbox Provider。
- 平台对应的受沙箱 Shell。
- 受沙箱 Filesystem。
- 一次性权限请求服务。
- Token Meter 和必要的上下文压缩。
- 有界的读取、写入、编辑和 Shell 工具。
- Agent scope 的 Skill Registry 与 `skill` 工具。Skill 目录由 Main 选择并在 Launcher 和 Host 两次规范化、校验。
- Main 代理的 MCP schema 工具。Utility 不持有 MCP URL 凭据或 Transport。
首版明确不加载:
- Web UI、HMR、Host API 和目录选择器。
- Harness 遥测。
- Settings File 和 Local Credentials。
- 用户 profile 与全局补丁。
- Web Search、Fetch、Utility 直连 MCP、Hooks。
- Subagent、Workflow、Ralph、后台 Job。
- JSONL Session Persistence 和 SQLite Session Query。
- 自动技能发现和市场技能加载。
如果某个首版工具依赖被排除服务,启动审计必须失败,而不是自动加载更大的默认 bundle。
## 12. 模型配置
### 12.1 配置来源
DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
- 协议必须是 `openai-chat-completions`。
- 认证必须是 API Key。
- 公网服务地址必须使用 HTTPS;`localhost`、`127.0.0.1` 和 `::1` 回环地址可以使用 HTTP。
- 服务地址可以使用自定义主机、端口和部署路径,但不得包含用户名、密码、查询参数或片段。
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
- 模型名称和服务地址由 Main 传入受控 Host。
- API Key 继续保存在 GoodBuddy 加密设置中。
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。
### 12.2 设置变化
模型、凭据、沙箱、Skill 或 MCP 分配变化时,GoodBuddy 创建新 Runtime 实例。Harness Host 路径始终由当前 GoodBuddy 构建提供,不能由设置或环境变量替换。旧实例按现有 Runtime Controller 语义退役,不在一个活动进程内热替换安全配置。
### 12.3 输入限制
- 首版只支持文本。
- 图片输入应在发起网络调用前返回明确错误。
- GoodBuddy 历史、Prompt、系统指令分别保持不同信任层。
- 任何用户文本都不能进入 Cordis 配置表达式或模块名。
## 13. 输出和资源边界
建议首版默认限制:
| 项目 | 默认上限 |
| --- | --- |
| 单个 JSON-RPC 帧 | 1 MiB |
| 单个文本或推理事件 | 64 KiB |
| 单次请求累计协议输出 | 4 MiB |
| 工具输入摘要 | 4,000 字符 |
| 工具输出摘要 | 4,000 字符 |
| 待处理事件数 | 1,000 |
| stderr 累计 | 64 KiB |
| 初始化 | 10 秒 |
| 单次 Prompt | 10 分钟 |
| 有序关闭宽限期 | 2 秒 |
超过限制时应取消当前请求。协议帧、队列或 stderr 持续异常时,应终止 Runtime 进程,避免继续信任已失控的通道。
## 14. Runtime 检测与状态
### 14.1 检测
检测只验证:
- 内置 Host 路径是规范化文件。
- 版本可读取且在支持范围内。
- 内部控制面能力握手成功。
- 必需 Sandbox Provider 可用并报告强制程度。
检测不得调用付费模型,也不得读取或输出 API Key。真实模型测试是单独的显式操作。
### 14.2 设置界面
Agent Runtime 使用共享 `SegmentedControl` 展示 OpenCode、Continue 和 DeepSeek Harness。DeepSeek Harness 必须标记为“开发者预览”,并说明上游 RC 可能发生破坏性变更。
Runtime 的概览、模型配置和检测信息放在同一张详情卡中。当前单独显示的一行“已就绪”应移入卡片,与路径、版本号归为同一组:
```text
Runtime: GoodBuddy 内置 DeepSeek Harness
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
状态: 已就绪
路径: <受控 Host 路径>
版本: 0.1.0-rc.6
安全强制: 完整 / 部分
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
```
界面要求:
- 不再在卡片外重复一行检测结果。
- 使用语义化键值结构,路径允许换行,不截断关键信息。
- 状态不能只依靠绿色表达,必须同时有文字。
- 检测中、不可用和部分强制分别显示明确文案。
- 高级设置默认收起。
聊天顶栏只显示简短 Runtime 状态,不显示文件路径和版本。完整诊断只在设置页展示。
## 15. IPC 与共享契约
共享 schema 需要覆盖:
- `deepseek-harness` provider 和 Runtime ID。
- Runtime 选择中的 `deepseekHarness` 分支。
- 检测结果中的路径、版本、详情和沙箱强制程度。
- GoodBuddy 模型连接选择。
- DeepSeek Harness 模型用量归属。
- Skill 与 MCP 对 `deepseek-harness` 的显式分配。
Renderer 只接收脱敏状态。任何凭据、完整环境、启动参数或内部 Cordis 配置都不能进入共享契约。
已有设置迁移必须:
- 对没有新字段的用户使用安全默认值。
- 保留 OpenCode、Continue 和模型连接选择。
- 修复失效的 DeepSeek Harness 模型引用时给出可报告的迁移警告。
- 不把旧 Runtime 自动迁移为 DeepSeek Harness。
## 16. 打包与供应链
### 16.1 版本策略
- 官方 RC 包全部精确锁定,不使用 `^` 或 `~`。
- 同一 Harness 核心包族必须保持同一 RC 版本。
- 升级前检查 release diff、协议 diff、沙箱 diff和依赖闭包。
- 内部握手同时检查锁定的 Harness 基线和 GoodBuddy 控制协议版本。
### 16.2 原生依赖
受控组合可能需要:
- `node-pty`,用于受管理的工具子进程。
- `koffi`,用于 Windows ACL 或相关本地能力。
- `@deepseek-ai/node-addon-landlock-run` 的平台包。
不得广泛批准所有安装脚本。只允许生产组合实际需要、来源已审查、版本已锁定的脚本。六个平台的构建必须验证:
- 对应架构的原生文件存在。
- Electron Utility Process 可加载原生模块。
- Runner 或 spawn helper 的权限正确。
- 包中没有混入其他平台不需要的可执行内容,除非上游包无法拆分且已记录。
### 16.3 生产闭包
发布包只包含受控 Host 需要的插件和许可证。应尽量避免把 Harness Web profile、HMR 和其他未加载产品面带入生产闭包。若 npm 依赖结构无法拆分,必须:
- 确认这些模块不会被加载。
- 评估它们带来的 audit 和体积风险。
- 在后续上游版本允许时改为最小包族。
- 确认 `tests/fixtures` 以及 Web3D 测试 Skill/MCP 不进入正式发布资源。
### 16.4 漏洞门禁
当前安装后的 `npm audit` 报告不能直接用 `npm audit fix --force` 处理。每项漏洞需要区分:
- GoodBuddy 既有依赖。
- Harness 新增生产依赖。
- 仅开发或打包依赖。
- 未加载但被带入的 Web 依赖。
进入 Harness 执行路径且有可利用条件的高危问题必须在发布前修复、替换或移出生产闭包。
### 16.5 发布验证
`build/build-release.cjs` 需要验证:
- Harness Host 和受控配置存在。
- GoodBuddy Host、内部控制协议与 Harness 依赖版本清单存在。
- 平台原生 Sandbox/PTY 依赖架构正确。
- Harness、ACP SDK 和其他新增第三方许可证已打包。
- `app.asar` 外需要执行或动态加载的资源位于预期目录。
- Web3D Skill/MCP 等测试 fixture 不在 `app.asar` 或 `extraResources` 中。
## 17. 测试策略
### 17.1 单元测试
- Runtime 选择、设置迁移和失效引用修复。
- 二进制检测、版本解析和路径规范化。
- ACP 握手、事件转换和请求关联。
- 每个会话单请求、跨会话并行。
- Ask 固定拒绝升级。
- Execute 权限请求由 Main 自动返回单次授权,Ask 与 delegation 固定拒绝。
- 未分配 Skill/MCP 不可见;分配后的 Skill catalog 可调用 `skill` 加载。
- Ask 不注册 MCP 工具;Execute 每轮刷新有界 schema,并在调用前再次校验活动请求、模式、参数和自动单次授权。
- MCP URL、启动命令和凭据不进入 Utility 启动配置或协议结果。
- 未知授权结果失败关闭。
- 超时、取消、迟到帧和进程意外退出。
- 协议帧、事件队列、工具摘要和 stderr 上限。
- release 和 dispose 的幂等性。
- 状态卡中的状态、路径、版本和强制程度。
### 17.2 本地集成测试
使用无网络的假控制面/模型验证:
- utilityProcess 管道。
- 多 Session。
- Session 释放。
- Runtime 替换。
- 进程树回收。
- 受控配置不会读取工作区 `.env` 和用户 DSH 配置。
### 17.3 真实模型测试
真实测试已经获得用户授权,但必须由显式环境门禁启用。Web3D Skill 和 MCP 仅作为 `tests/fixtures` 下的测试资产使用,不属于内置发布能力。至少验证:
1. 文本问答成功,并记录正确 Runtime 和模型用量。
2. Ask 可以读取工作区,但写入被拒绝,且不会弹出权限对话框。
3. Execute 可以在工作区创建测试文件。
4. Execute 越界操作先被拒绝,再对完全相同的重试自动给予单次授权,全程不弹出审批。
5. 不匹配的重试、Ask 和 delegation 不能换路径或重复绕过。
6. 取消长请求后不再产生文本,并可继续使用其他 Session。
7. 两个 Session 可并行,事件不会串线。
8. 释放会话和关闭应用后没有残留 Harness 或工具进程。
9. 从全新用户设置流程启用一个 3D 游戏 Skill 和实际本地或开放 MCP,工具事件能够证明二者确实被调用。
10. Harness 生成的 3D 游戏项目可以安装、启动和实际游玩,包含 3D 渲染、玩家控制、目标和反馈,浏览器无关键错误。
测试不得打印、快照或提交 API Key。测试创建的文件只能位于专用临时工作区,并在确认可再现后清理。
### 17.4 项目验证
源码完成后必须运行:
```text
npm test
npm run typecheck
npm run lint
npm run build
```
涉及发布资源后,还要按可用原生平台运行聚焦的 `release:package` 验证。无法在当前主机执行的目标必须由六平台 CI 验证。
## 18. 验收标准
功能只有同时满足以下条件才算完成:
- `deepseek-harness` 可被保存、选择、检测和显示。
- Runtime 详情卡内显示状态、路径、版本和沙箱强制程度。
- Skills 与 MCP 设置页可把能力分配给 DeepSeek Harness,布局、键盘语义、文案和保存回显通过真机检查。
- Ask 写入测试在 Runtime 边界失败。
- Execute 工作区内写入成功。
- 越界写入只有同一操作获得自动单次授权后才能执行一次,且不弹出审批。
- 取消、超时、切换 Runtime 和退出应用均能回收进程。
- 多会话不串流、不串权限请求、不串用量。
- 用户 DSH 配置、`.env`、遥测和 Web UI 未被加载。
- API Key 不进入 Renderer、配置文件、日志、错误文本或测试产物。
- 全量测试、类型检查、Lint 和生产构建通过。
- 真实 OpenAI 兼容 Chat Completions 请求成功。
- 真实请求调用已分配 Skill 和 MCP,并生成、启动和实际游玩一个可用的 3D 游戏项目。
- 新增第三方许可证和发布校验完整。
## 19. 已知限制
- DeepSeek Harness 底层库当前是 RC,但 GoodBuddy 不自动跟随升级;每次升级都可能要求同步修改内部控制面。
- Windows ACL 和部分 Linux Landlock 环境只能提供部分强制执行。
- 首版不恢复 Harness 原生 Session,Runtime 重启后由 GoodBuddy 历史重建。
- 首版不支持图片、知识库、浏览器工具和 Harness Subagent;MCP 仅支持用户分配、Main 代理和 Execute 自动单次授权路径。
- 推理、工具和用量扩展属于 GoodBuddy 协议,不是标准 ACP 保证。
- 不支持 DSH 插件、市场包、用户 profile 或自定义 Host。
## 20. 自维护与升级策略
GoodBuddy 对该 Runtime 采用内部维护策略:
1. 当前通过验证的 Host、控制协议和依赖锁定随 GoodBuddy 一起版本化。
2. 不自动跟随 DSH RC、插件 ABI、profile 格式或市场元数据变化。
3. 升级前审查实际用户收益、上游 diff、沙箱与工具语义、协议行为、依赖闭包和许可证。
4. 六个平台的单元、假模型、UtilityProcess、沙箱和真实模型门禁全部通过后才能更新基线。
5. 若上游方向不再满足 GoodBuddy 用户需求或安全边界,允许维护兼容补丁、替换单个底层包,或逐步移除 DSH 依赖;`goodbuddy/*` 内部协议保持由 GoodBuddy 控制。
6. 不以进入官方插件目录、适配市场机制或服务非 GoodBuddy 客户端作为目标。
## 21. 备选方案记录
### 21.1 每次调用 `dsh --profile headless`
未采用。它适合一次性任务,但不能满足流式事件、多会话、细粒度取消、权限回传和低延迟复用。
### 21.2 只使用官方 ACP 插件
未采用。取消和一次性权限选择符合需求,但缺少工具、推理、用量和会话释放事件。
### 21.3 只使用官方 SDK JSON-RPC
未采用。事件更完整,但单轮取消和权限回传能力不足。
### 21.4 把全部安全逻辑放进 Harness 子进程
未采用。Harness 子进程属于不可信执行面,不能拥有最终模式授权策略、加密设置和进程回收权限。
### 21.5 把全部控制适配放在 Main
未采用。Main 无法可靠观察 Cordis 内部 Session、Tool、Usage 和权限 seam,只能得到不完整的外部进程行为。
当前选择的双层内部控制面放弃标准 DSH 插件形态,只复用锁定的底层库,并维持 GoodBuddy 的可信 Main 控制权。
+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。
+317
View File
@@ -0,0 +1,317 @@
# 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.
## 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.
- 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
Runtime. 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 npm archive, 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.
+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,83 @@
# 应用导航与系统工具入口
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,内置应用通过最底部应用中心
访问,并可常驻主导航;系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
商店入口、推荐位或安装流程。
当前分期见 [PRD 第 8 节](./prd.md#8-分期),实施步骤见[技术设计第 11 节](./technical-design.md#11-实施顺序)。
本轮更新实施计划与范围,旧版功能要求、验收和配套设计仍需按新计划同步,此项列为实现前的第一步。
项目活动汇总独立于上述应用中心计划,范围见 [FR-14](./prd.md#fr-14-项目活动汇总)。
计数规则由[活动逻辑](./logic-design.md#9-项目活动汇总)定义,入口与弹窗见
[活动界面](./ui-design.md#10-项目活动汇总),数据加载与会话跳转见
[技术设计](./technical-design.md#12-项目活动汇总)。此处“活动”指当前运行或待处理会话,
与用于审计的“运行记录”不同;已完成未读面板属于后续范围,本次未实现。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、产品边界、功能要求与产品验收 |
| [User Stories](./user-stories.md) | 用户场景及 Given/When/Then 验收 |
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
| [应用中心交互 Demo](./navigation-demo.html) | 对比六个候选方案;配套 CSS 和 JS 仅服务原型 |
Demo 可直接在浏览器中打开,支持方案切换、浅深主题、添加、移除、排序和商店模拟。
六个方案共用本次页面会话内的应用配置,刷新后恢复默认。商店、资源指标和工具内容均为
演示数据,不执行真实安装或本机操作。窄屏上下排列侧栏与输入区,桌面展示完整应用布局。
这些方案用于讨论最底部应用中心;其中商店页、商店应用安装与 F 方案属于未来交互参考,
不纳入首期实现或验收。首期产品只提供内置应用;Demo 暂保留远期示例以便后续讨论。
底部输入区共用一个简化候选布局:附件、语音、Runtime、模式和发送位于主操作行,专家、
知识范围及 Runtime 专属选项通过“选项”展开,非默认选择显示为可移除标签。可体验文件名
附件、模拟语音、模板填入、原生命令选择、8 秒模拟回复、发送排队、立即插入、删除、停止,
以及上下文详情和模拟压缩。文件只读取名称,语音使用固定示例文本,不录音、不上传、不调用 AI。
这是输入区设计讨论用的原型,尚未改变正式产品或替代现有 UI 规范。
按最新讨论,Demo 已移除左侧系统工具快捷区:文件、终端、浏览器和资源监控只在右侧
工作栏 Tab 中演示,不列入应用中心。上方正式文档仍保留原设计,待方案确定后同步。
六个方案共用“我的应用”区:通过 + 或“全部应用”选择常驻应用,编辑时可拖动或使用
上下箭头排序、移除入口。未常驻的已安装应用仍可从“全部应用”打开。商店包含“项目管理”
和“阅读空间”应用示例,支持仅安装或安装并常驻左侧菜单,常驻不表示后台运行。
常驻应用在本次页面会话内跨方案保持一致;“恢复默认应用”重置安装与常驻配置。
移除入口不会卸载应用或关闭当前页面,刷新后恢复默认。
魔法笔记提供应用中心“设置”、笔记页面“笔记设置”和全局设置演示入口,均打开同一设置
面板,共用本次页面会话内的配置。面板沿用现有选项:显示未完成待办数量、AI 评论方式
(即时/保存后自动/保存后手动)、AI 评论形式(长评 + 要点/长评/要点)。现有“显示
魔法笔记入口”在原型中与“常驻左侧菜单”统一,不额外增加功能启停开关。侧栏待办数为
固定的 3 项演示数据。修改即时生效于原型,不调用 AI 或写入真实应用设置。
| 方案 | 讨论重点 |
| --- | --- |
| A:紧凑入口 | 从底部向上展开应用列表 |
| B:应用宫格 | 在展开面板中展示应用图标、名称和常驻操作 |
| C:应用预览 | 用卡片说明应用用途 |
| D:搜索启动台 | 搜索打开能力,支持 Ctrl / Command K、方向键与 Enter |
| E:常驻管理 | 已常驻应用优先排列,可取消常驻或打开 |
| F:商店发现(未来) | 默认展开模拟商店,不纳入首期 |
D 参考 [Raycast](https://www.raycast.com/) 的搜索启动与扩展发现方式。早期方案参考过
Notion 收藏和 VS Code Profiles,当前版本已移除场景工具包与快捷区。
原型支持 URL 的 `#launcher`、`#favorites`、
`#scenes` 等片段直接打开指定方案。
## 术语
| 术语 | 定义 |
| --- | --- |
| 系统入口 | GoodBuddy 工作流程或审计所必需的固定入口,用户不能卸载、隐藏或改变类型 |
| 应用 | 在主内容区打开完整一级页面的功能,可显示在“当前对话”所在的主导航区 |
| 系统工具 | 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)
@@ -0,0 +1,188 @@
# 应用导航与系统工具入口功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 关联 User Stories | [User Stories](./user-stories.md) |
## 1. 不变量
1. “当前对话”“运行记录”和“设置”始终是系统入口。
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
4. 左下角只提供快捷入口,工具内容仍由助手工作栏承载。
5. 隐藏应用不等于停用功能,也不删除、迁移或清理业务数据。
6. 快捷入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
## 2. 入口模型
```ts
type NavigationContribution =
| {
type: 'system-entry'
id: 'chat' | 'activity' | 'settings'
placement: 'primary-start' | 'primary-end' | 'footer'
}
| {
type: 'application'
id: 'magic-notes' | 'knowledge' | 'heartbeat'
source: 'builtin'
visible: boolean
}
| {
type: 'system-tool'
id: 'workspace' | 'terminal' | 'browser' | 'resources'
workbarAppId: 'workspace' | 'terminal' | 'browser' | 'resources'
}
```
该模型只覆盖首期入口。插件贡献模型随插件 Host、权限和生命周期另行设计。
## 3. 主导航组合规则
主导航按以下顺序计算:
```text
当前对话
可见的内置应用
运行记录
```
当前对话和运行记录不参与排序。首期内置应用默认顺序为魔法笔记、知识库、智能心跳;显隐
设置只从中移除对应项,不改变固定入口的位置。
### 3.1 应用可见性与业务状态
| 导航可见 | 功能启用 | 结果 |
| --- | --- | --- |
| 是 | 是 | 显示入口并允许进入 |
| 否 | 是 | 隐藏入口,后台和数据保持原状 |
| 是 | 否 | 不显示不可用入口;设置页保留启用路径 |
| 否 | 否 | 不显示入口,保留符合该功能自身规则的数据 |
“功能启用”由各应用现有产品设置负责。例如魔法笔记启停仍使用原设置;导航偏好不能覆盖它。
## 4. 系统工具打开规则
### 4.1 通用流程
```text
用户激活工具按钮
-> 校验按钮仍可用
-> 向助手工作栏发送打开意图
-> 工作栏按应用实例策略查找或创建实例
-> 展开工作栏
-> 激活目标实例
-> 把焦点移入面板标题或首个主要控件
```
若创建失败,工作栏保持展开并在对应能力位置显示错误;主页面和其他工作栏实例不被清空。
### 4.2 决策表
| 工具 | 匹配实例存在 | 匹配实例不存在 | 上下文不可用 |
| --- | --- | --- | --- |
| 文件 | 激活不可关闭的单实例并保留其跟随或固定状态 | 仅在迁移或布局修复时补回 `workspace` | 打开面板并显示目录修复入口 |
| 终端 | 激活当前执行空间最近的 `starting` 或 `running` 实例 | 创建一个绑定当前执行空间的实例 | 没有项目时使用本机 Home |
| 浏览器 | 激活绑定当前 Conversation 的最近使用实例 | 创建绑定当前 Conversation 的新实例 | 激活最近使用实例;不存在时创建待绑定实例 |
| 资源监控 | 激活 `resources` 单实例 | 创建本机 `resources` 单实例 | 部分指标不可用时显示原因并保留其余指标 |
文件保持单一实例原有绑定。浏览器快捷入口按当前 Conversation 匹配最近使用实例,而工作栏
“+”始终新建浏览器实例。固定目标失效时显示该目标的修复状态,不回退到当前项目、默认
工作区或当前会话。
`closing`、`exited`、`interrupted` 和 `failed` 终端不属于可复用实例。当前执行空间只有这些
终端时,快捷按钮创建新实例并保留原 Tab 的历史状态。用户需要额外的并行终端时使用工作栏
“+”或终端面板中的既有创建命令。
### 4.3 选中与状态
工具按钮满足以下条件时显示选中:助手工作栏已展开,且当前活动实例的 `appId` 与工具映射
一致。只有实例存在但未激活时不显示选中。
徽标优先级为:等待用户处理 > 错误 > 运行中 > 无徽标。同一按钮只显示一个视觉徽标,
可访问名称包含完整状态。
资源监控首期不在后台采样,因此不显示负载告警徽标。只有资源面板是工作栏当前活动实例且
工作栏已展开时,资源按钮才显示选中状态。
### 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. 设置保存规则
应用导航偏好与应用业务启停分开保存。Switch 每次切换立即提交完整目标集合;请求完成前禁用
三个应用显隐 Switch,下一次操作基于最新确认结果提交,防止并发更新互相覆盖。
保存失败时:
- 主导航和 Switch 恢复最后一次成功保存的偏好。
- 设置行显示可重试错误。
- 不回滚或修改各应用的业务状态。
## 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-5 至 FR-10 | US-C1 至 US-C7 |
| 资源采样状态 | FR-9 | US-C6 |
| 无障碍与窄窗口 | FR-13 | US-D1、US-D2 |
## 7. 逻辑完整性评估
首期规则已覆盖固定入口、应用显隐、保存失败、系统工具匹配与显式多实例差异、本机 Home、
托管 SSH、无活动会话、目录失效、资源部分可用、采样停止、权限边界、键盘和窄窗口。其他
工具面板内部的取消、超时、
连接恢复和固定目标失效继续由助手工作栏负责,本功能不复制这些状态机。
## 8. 后续产品决策
- 插件工具上线后,快捷栏允许固定的最大数量及溢出规则。
- 插件应用是否允许用户排序,以及固定系统入口之间允许的排序范围。
这些决策不影响首期四个系统工具的固定入口和内置应用显隐实现。
## 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 和未读标记本身不构成活动。活动结束且没有其他
运行或等待条件时移除该行,不能因历史任务或未读状态继续计数。
@@ -0,0 +1,97 @@
: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;
}
[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%); }
.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 .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,78 @@
<!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>
<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>
<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><legend>知识范围</legend><label><input type="checkbox" data-knowledge="产品文档"> 产品文档</label><label><input type="checkbox" data-knowledge="团队知识"> 团队知识</label></fieldset>
<label class="option-row">检索方式<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="应用中心视图"><button data-catalog="apps">我的应用</button><button data-catalog="store">商店</button></div><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,291 @@
/* 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: '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 / 商店发现', '底部入口强调应用发现,展开后查看商店应用与安装位置。', '先模拟安装,再决定是否常驻;我的应用始终可切换访问。']
};
const $ = selector => document.querySelector(selector);
let variant = 'dock';
let catalogMode = 'apps';
let residentApps = ['notes', 'knowledge'];
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 renderNoteSettings() {
$('#app-settings').innerHTML = `<div class="note-setting-row"><div><strong>常驻左侧菜单</strong><p>对应“显示魔法笔记入口”,移除后仍可从应用中心打开。</p></div><button class="demo-switch" role="switch" aria-label="常驻左侧菜单" aria-checked="${residentApps.includes('notes')}" data-note-toggle="resident"></button></div>
<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 showNoteSettings() {
if (!$('#catalog').open) { catalogMode = 'apps'; $('#search').value = ''; }
settingsOpen = true;
$('#catalog').dataset.variant = variant;
$('#catalog-title').textContent = '魔法笔记设置';
$('#catalog-description').textContent = '调整入口与 AI 评论行为。应用中心和笔记页面共享这份演示配置。';
$('#back-to-apps').hidden = false; $('#app-settings').hidden = false;
for (const selector of ['.center-switch', '#search', '#catalog-items']) $(selector).hidden = true;
renderNoteSettings();
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.map((id, index) => {
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: `${installed.size} 个已安装应用`, cards: '笔记 · 知识 · 自动化', launcher: 'Ctrl / ⌘ K', favorites: `${residentApps.length} 个应用常驻菜单`, 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' ? !item.builtin : installed.has(item.id)) && `${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 ? '内置应用' : '商店应用 · 示例'}</p><p class="app-detail">${variant === 'cards' ? item.summary : item.detail}</p></div><div class="center-actions">${ready ? `<button data-open="${item.id}">打开</button>${item.id === 'notes' ? '<button data-note-settings aria-label="魔法笔记设置">设置</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">没有匹配的应用。可以换个关键词,或切换到商店。</p>';
document.querySelectorAll('.center-switch button').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.catalog === catalogMode)));
}
function openCatalog(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;
catalogMode = mode; $('#catalog').dataset.variant = variant;
$('#catalog-title').textContent = mode === 'store' ? '发现新应用' : '应用中心';
$('#catalog-description').textContent = mode === 'store' ? '安装你需要的应用,自主决定是否常驻左侧菜单。' : '打开应用,或把常用应用留在左侧。系统工具请使用右侧工作栏。';
$('#search').value = ''; renderCatalog();
if (!$('#catalog').open) $('#catalog').showModal();
$('#search').focus();
}
function openPage(name) {
if ($('#catalog').open) $('#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);
$('#page-actions').innerHTML = name === '魔法笔记' ? `<button data-note-settings>${icon('settings')} 笔记设置</button>` : '';
if (name === '魔法笔记' || name === '设置') {
const summary = document.createElement('p'); summary.id = 'note-settings-summary'; summary.className = 'muted'; $('#page-content').append(summary);
if (name === '设置') { const button = document.createElement('button'); button.dataset.noteSettings = ''; button.textContent = '魔法笔记设置'; $('#page-content').append(button); }
renderNoteSettings();
}
document.querySelectorAll('.navigation button').forEach(button => button.classList.toggle('selected', button.dataset.page === name));
renderApps(); 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();
}
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; });
}
document.addEventListener('click', event => {
const button = event.target.closest('button'); if (!button) return;
const data = button.dataset;
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 (button.hasAttribute('data-note-settings')) showNoteSettings();
if (data.noteToggle || data.noteMode || data.noteFormat) {
if (data.noteToggle === 'resident') residentApps = residentApps.includes('notes') ? residentApps.filter(id => id !== 'notes') : [...residentApps, 'notes'];
if (data.noteToggle === 'showCount') noteSettings.showCount = !noteSettings.showCount;
if (data.noteMode) noteSettings.mode = data.noteMode;
if (data.noteFormat) noteSettings.format = data.noteFormat;
renderApps(); renderNoteSettings();
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) installed.add(id);
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-note-settings]') || $('#search')).focus(); };
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 = ['notes', 'knowledge']; editingApps = false; renderApps(); 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]: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,221 @@
# 应用导航与系统工具入口 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 适用产品 | GoodBuddy 桌面端 |
| 关联设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[助手工作栏 PRD](../assistant-workbar/prd.md) |
## 1. 背景
计划修订说明:当前实施范围以[第 8 节](#8-分期)为准。本文其余章节仍包含 v0.2 的
左下角系统工具方案;启动实现前须同步功能要求、验收及配套设计,旧快捷工具条不再列入实施。
主侧栏当前把“当前对话”“魔法笔记”“知识库”“智能心跳”和“运行记录”放在同一导航组,
底部长期显示“GB 本地工作区”和平台架构。底部信息不能反映用户可以执行的操作,文件、终端
和浏览器则需要先找到右侧助手工作栏后才能打开。
后续插件可能提供完整页面,也可能只提供辅助面板或命令。若不先区分系统入口、应用和工具,
插件接入后会同时争用主导航、侧栏底部和助手工作栏,用户也无法判断入口会切换页面还是打开
辅助面板。
## 2. 产品定义
### 2.1 三类入口
| 类型 | 首期成员 | 默认位置 | 用户控制范围 |
| --- | --- | --- | --- |
| 系统入口 | 当前对话、运行记录、设置 | 主导航固定位置或侧栏底部 | 不可隐藏、卸载或改变类型 |
| 应用 | 魔法笔记、知识库、智能心跳 | 主导航应用区 | 可控制是否显示;隐藏不删除数据 |
| 系统工具 | 文件浏览器、终端、浏览器、资源监控 | 侧栏底部快捷工具栏 | 首期固定提供,不可卸载 |
运行记录是系统审计页面,不属于工具。文件浏览器、终端、浏览器和资源监控是系统工具;它们
的快捷入口位于左下角,实际界面继续由右侧助手工作栏承载。
### 2.2 后续插件类型
插件市场开放后,插件可以贡献“插件应用”或“插件工具”。插件不能注册、覆盖或替换系统入口,
也不能冒充系统工具。内置应用继续标记为“内置”,不因采用统一注册协议而显示成待安装商品。
## 3. 目标
- 用户从侧栏底部一次点击即可打开文件、终端、浏览器或资源监控。
- 主导航只承载完整页面,辅助工具不占用一级页面位置。
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
- 内置应用可以从一个设置位置控制主导航可见性,隐藏后仍可恢复。
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
## 4. 非目标
- 首期不提供应用商店页面、商店导航、推荐位或安装按钮,也不提供第三方插件搜索、下载、
安装、更新、签名验证或卸载。内置应用的浏览、搜索与打开属于应用中心,不依赖商店。
- 不把文件树、终端或浏览器重新实现到左侧栏。
- 不改变助手工作栏的面板实例、停靠、执行空间和安全边界。
- 不允许隐藏运行记录、当前对话或设置。
- 不因隐藏应用而删除魔法笔记、知识库或智能心跳数据。
- 不把“显示在侧边栏”解释为停止智能心跳计划或后台任务。
- 不提供任意本机程序启动器。
- 首期资源监控不采集远程主机、GPU、显存或网络吞吐,不提供后台告警和历史持久化。
## 5. 功能要求
### FR-1 入口分类
系统必须为每个可见入口声明稳定类型:`system-entry`、`application` 或 `system-tool`。界面位置、
设置项和点击结果均以该类型为准,不根据当前页面临时改变类型。
### FR-2 固定系统入口
“当前对话”固定在主导航首位;“运行记录”固定在应用区之后;“设置”固定在侧栏底部。
这些入口不出现在应用显隐、工具固定或插件管理设置中。
### FR-3 内置应用区
魔法笔记、知识库和智能心跳显示在当前对话与运行记录之间。应用入口打开对应一级页面,保留
现有徽标、范围和页面状态。用户可以在设置中控制其主导航可见性。
### FR-4 隐藏应用
隐藏应用只移除主导航入口,不删除数据、不修改业务启停状态,也不终止正在进行的操作。显隐
Switch 更新成功后主导航立即生效;更新失败时恢复原状态并允许重试。用户可从设置恢复入口。
### FR-5 系统工具快捷栏
侧栏底部提供文件浏览器、终端、浏览器和资源监控四个图标按钮,并保留独立设置按钮。每个
按钮具有可见工具提示和可访问名称。首期系统工具顺序固定为文件、终端、浏览器、资源监控。
### FR-6 文件浏览器入口
点击文件按钮时展开助手工作栏,并聚焦默认存在且不可关闭的“工作区”单实例。该实例保持
“跟随当前上下文”或“固定到指定对象”状态;迁移或布局修复发现实例缺失时补回,并默认跟随
当前项目。没有项目时使用已配置的默认工作区。跟随上下文所需的路径不可用时,面板显示修复
入口;已有有效固定目标不受当前项目和默认工作区状态影响。
### FR-7 终端入口
点击终端按钮时展开助手工作栏。当前执行空间有正在启动或运行中的终端时聚焦最近使用的实例;
没有时创建一个绑定当前执行空间的新终端,已退出、已中断或失败的历史 Tab 保持不变。没有
项目时当前执行空间为本机 Home。创建额外并行终端继续使用工作栏现有的“+”目录入口。
### FR-8 浏览器入口
点击浏览器按钮时展开助手工作栏,并优先聚焦绑定当前 Conversation 的最近使用浏览器实例;
没有匹配实例时创建一个。没有活动 Conversation 时聚焦最近使用的浏览器实例;完全不存在时
创建待绑定空实例。该快捷入口不会因连续点击创建重复实例;用户通过工作栏“+”选择浏览器时
则始终创建新实例。入口不打开系统默认浏览器,也不创建旁路 WebView。
### FR-9 资源监控入口
点击资源监控按钮时展开助手工作栏,并打开或聚焦单实例的本机“资源监控”面板。面板显示
整机 CPU 使用率、内存使用量与总量、磁盘读写速率,以及 GoodBuddy 桌面进程的 CPU、内存
和进程数摘要。指标不可用时显示具体项目和原因,不能显示为 `0`。
资源面板处于可见活动状态、工作栏已展开且应用窗口未隐藏或最小化时,每 2 秒采样一次,在
Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止采样;首期不在后台持续监控,
也不据此产生侧栏告警。首次采样及停止后恢复的首次采样只建立 CPU 和磁盘 IO 速率基线,
不显示伪造的 `0` 值。
### FR-10 快捷入口状态
工具面板已激活时,对应按钮显示选中状态;面板存在但未激活时保持普通状态。运行中、错误或
等待用户处理可以显示徽标,但必须有可访问文字,且不得自动抢占当前面板。
### FR-11 应用导航设置
设置页提供“应用与工具”管理入口。首期“应用”区域列出三个内置应用并提供“显示在侧边栏”
Switch,切换后立即持久化。系统工具区域列出四项内置固定能力,不提供虚假的卸载或停用按钮。
### FR-12 偏好持久化
应用可见性按设备持久化。升级时保留当前行为:已有可用内置应用继续显示;魔法笔记原有功能
启停设置保持原意,不由新的导航可见性覆盖。
### 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. 产品验收
- 主导航能明确区分固定系统入口和可配置内置应用。
- 运行记录在所有应用显隐组合下保持可见并可打开。
- 左下角不再用“本地工作区”与系统架构占据主要空间。
- 文件、终端、浏览器和资源监控均可从左下角打开正确的工作栏面板,且不产生第二套内容界面。
- 连续点击系统工具不会无意创建重复的浏览器或终端实例;工作区始终复用不可关闭的固定实例。
用户通过工作栏“+”可以显式创建额外浏览器或终端实例。
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
最小化后停止请求新样本。
- CPU、内存、磁盘 IO 或 GoodBuddy 指标不可用时,页面区分部分可用与整体失败。
- 隐藏内置应用不删除数据、不停止后台计划,并能从设置恢复;保存失败时入口保持原状。
- 键盘、屏幕阅读器、浅色主题、深色主题和窄窗口均可完成相同操作。
## 8. 分期
### 首期
- 将侧栏最底部改为内置应用中心入口,设置按钮保持独立;具体列表、宫格或卡片样式从
Demo 中收敛确定。
- 应用中心提供魔法笔记、知识库、智能心跳的浏览、搜索与打开,不要求安装。
- 支持应用常驻主导航、取消常驻和排序;未常驻应用仍可从应用中心打开,隐藏不删除数据或停止后台任务。
- 应用中心和应用页面就近提供已有功能设置,共享真实设置来源;已有全局设置入口定位到同一界面。
魔法笔记优先接入待办数量、AI 评论方式和评论形式,入口显示与常驻控制统一,避免重复开关。
- 文件、终端、浏览器保留在右侧工作栏 Tab,不在左下角复制入口,也不纳入应用中心。
- 原计划的本机资源监控仍作为系统工具交付项,在右侧增加单实例面板;与应用中心分别实现和验证。
- 保持运行记录为固定系统入口。
首期验收需覆盖:无商店入口时可以访问全部内置应用;取消常驻后仍可重新打开和配置;
不同设置入口状态一致;常驻排序重启后保留;右侧工具切换不改变当前应用页面。
本节是范围调整,Demo 交互通过不代表正式功能已经实现。
### 补充要求:魔法笔记迁移与入口联动
- 将现有魔法笔记设置迁入应用中心的应用设置界面,笔记页面内的设置入口访问同一份配置。
魔法笔记可常驻主导航;原全局设置位置如保留入口,应定位到共同设置界面。
- 魔法笔记相关布尔开关默认全部开启,包括入口显示(常驻)和未完成待办数量显示。
AI 评论方式、评论形式是单选项,沿用现有默认值,不把“全部开启”解释为同时选中多个模式。
新默认值适用于未配置状态,迁移时保留用户已明确保存的选择。
- 应用关闭后的入口联动必须覆盖主导航和会话页面,不能只更新应用中心卡片或一个菜单。
知识库关闭后,左侧知识库菜单及会话中的知识库入口按钮同步移除;重新开启后按配置恢复。
- “取消常驻”只移除主导航捷径;“关闭应用”移除该应用在主导航、会话等业务界面的入口。
应用中心保留管理和重新开启入口。原型中仅有常驻控制,正式设计需补齐这两种状态,避免混用。
- 入口变化不删除已有笔记或知识数据。知识库已打开页面、会话已关联知识以及后台任务的处理,
在详细逻辑同步时分别定义,不能仅凭隐藏按钮推断功能或后台行为已经停用。
补充验收:从任一配置入口关闭知识库,主导航和当前会话按钮立即同步移除,切换会话、
重启后也不重新出现;重新开启后恢复相应入口。验证魔法笔记未配置时的默认开启,以及
已有关闭选择在迁移后保留。设置保存失败时各入口保持原状态,不能出现互相矛盾的显示。
### 后续:应用商店与扩展
- 单独设计应用商店的目录、搜索、推荐、详情及安装管理,时间与具体范围另行确定。
- 插件应用和插件工具注册,以及安装、更新、卸载与权限说明。
- 插件应用可常驻主导航;插件工具按工作栏扩展设计接入,不恢复左下角重复系统工具条。
- 用户级快捷键与命令面板接入同一贡献命令。
现有 Demo 的商店页、项目管理和阅读空间安装流程仅保留为未来交互参考,不纳入首期验收,
也不作为已有商店能力的说明。首期实现不预建插件安装服务或远程目录协议。
@@ -0,0 +1,408 @@
# 应用导航与系统工具入口技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| UI 设计 | [UI 设计](./ui-design.md) |
## 1. 现状
主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换:
```ts
type WorkspaceView =
| 'chat'
| 'magic-notes'
| 'knowledge'
| 'heartbeat'
| 'activity'
| 'settings'
```
文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts`
承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程
实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控
面板。现有侧栏底部 `user-card` 整行打开设置,并显示“本地工作区”和平台架构。
本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/
Renderer 最小实现。文件、终端和浏览器后端保持不变。
## 2. 模块边界
| 模块 | 责任 |
| --- | --- |
| `App.tsx` | 组合主导航、系统工具栏、设置入口和页面离开检查 |
| `SidebarSystemTools`(新增) | 渲染四个系统工具按钮并发送打开意图 |
| `RightAssistantSidebar.tsx` | 接收打开意图,调用工作栏实例创建或激活逻辑 |
| `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 |
| `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 |
| `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 |
| `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 |
| `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 |
| `application-settings-contracts.ts` | 增加应用导航偏好 schema 和默认值 |
| `application-settings-store.ts` | 迁移并原子保存应用导航偏好 |
| `PlatformFeaturesSettingsSection` 或独立设置 Section | 编辑“显示在侧边栏”偏好 |
不要在 `SidebarSystemTools` 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送
有界的工具 ID。
## 3. Renderer 契约
### 3.1 系统工具 ID
```ts
export const sidebarSystemToolIds = [
'workspace',
'terminal',
'browser',
'resources'
] as const
export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number]
```
该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer
状态;实际能力调用继续走现有 Preload API。
### 3.2 打开意图
`App` 向 `RightAssistantSidebar` 传入单调递增的请求或命令对象:
```ts
type WorkbarOpenIntent = {
requestId: number
appId: 'workspace' | 'terminal' | 'browser' | 'resources'
reason: 'sidebar-system-tool'
context?: { conversationId?: string; projectId?: string }
}
```
`requestId` 使连续点击同一工具仍能被消费。不要仅设置 `assistantSidebarTab`,因为用户可能已
关闭对应实例,且终端需要按执行空间寻找实例。
建议由 `RightAssistantSidebar` 在消费意图后通过现有 `WorkbarShell` 控制接口完成。控制器必须
接收上下文和调用原因,不能只按 `appId` 查找第一个实例:
```ts
type WorkbarController = {
openOrActivate(intent: WorkbarOpenIntent): Promise<WorkbarTabInstance>
}
```
若当前 `WorkbarShell` 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。
工作区意图始终激活注册表中的不可关闭单实例;仅在迁移或布局损坏时补回。浏览器意图按
`conversationId` 激活最近使用的匹配实例,没有匹配实例时创建一个。工作栏“+”不走该去重
路径,选择浏览器时始终创建新实例。
### 3.3 终端匹配
工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用:
1. 解析当前项目对应的 `ExecutionSpaceSelection`。
2. 在现有终端实例中按目标 identity 查找 `starting` 或 `running` 状态。
3. 存在多个匹配实例时激活最近使用的实例。
4. 没有匹配实例时调用现有终端创建流程。
`closing`、`exited`、`interrupted` 和 `failed` 实例保留为历史 Tab,不阻止快捷按钮创建新终端。
工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。
### 3.4 资源监控工作栏应用
在 `workbar-contracts.ts` 中增加 `resources`:
```ts
{
id: 'resources',
instancePolicy: 'single',
defaultContext: 'application'
}
```
该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建
或聚焦它。
## 4. 应用导航偏好
### 4.1 设置结构
在 `ApplicationSettings` 增加独立字段:
```ts
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat'
type ApplicationNavigationSettings = {
hiddenBuiltInApplicationIds: BuiltInApplicationId[]
}
```
默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔
字段;Zod schema 必须去重、限制数量并拒绝未知内置
ID。
首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留
规则后另行扩展,不提前混入当前 schema。
### 4.2 与现有开关组合
入口可见条件为:
```ts
const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id)
```
`magicNotesEnabled` 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合
规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置
不能写回现有开关。
### 4.3 持久化与 IPC
沿用现有应用设置链路:
```text
Renderer
-> window.goodbuddy.updates.updateSettings
-> settings:application:update
-> application-settings-store
-> application-settings.json
```
更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不
修改已有 `magicNotesEnabled` 或其他平台设置。
## 5. 侧栏底部重构
现有 `user-card` 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮:
```tsx
<div className="sidebar-footer">
<SidebarSystemTools onOpenTool={openSystemTool} />
<button aria-label={t('navigation.settings')} ...>
<Settings aria-hidden="true" />
</button>
</div>
```
不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
保留设置预加载、焦点恢复和未保存离开确认。
系统工具按钮调用统一 `openSystemTool`:
1. 设置工作栏打开意图。
2. 展开工作栏。
3. 由工作栏创建或激活实例。
4. 按现有策略把焦点移入目标面板。
工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。
## 6. 工作栏状态与快捷按钮
`RightAssistantSidebar` 应向 `App` 暴露当前活动实例的 `appId` 和必要的有界状态摘要,或由
现有 `onTabChange` 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。
```ts
type SidebarSystemToolState = {
activeToolId?: SidebarSystemToolId
statusByToolId?: Partial<Record<SidebarSystemToolId, 'attention' | 'error' | 'running'>>
}
```
首期可以只实现 `activeToolId`,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误
或运行状态。
## 7. 应用显隐更新
应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只
允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航
随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功
保存的设置。
## 8. 插件兼容边界
首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。
插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek
Harness 的 npm 插件市场。
## 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 测试
- 固定入口顺序不受三个应用显隐组合影响。
- 运行记录和设置没有显隐控件。
- 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。
- 文件复用不可关闭单实例;浏览器和终端按当前上下文聚焦匹配实例,工作栏“+”允许显式多实例。
- 工具按钮展开工作栏但不改变当前主页面。
- 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。
- 窄窗口下工具按钮不重叠,焦点恢复正确。
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
### 10.2 Main 与契约测试
- 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。
- 旧版本设置迁移后默认显示所有既有应用。
- 更新导航偏好不修改魔法笔记启停等无关字段。
- 原子写入失败时保留最后一次有效设置。
- 资源快照拒绝 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 采集;不支持项显示准确原因。
## 11. 实施顺序
实施范围以 [PRD 分期](./prd.md#8-分期)为准。本节替代旧实施顺序;前文关于四个左侧
系统工具按钮、仅持久化隐藏 ID 的契约需在实现前同步,不能直接按旧规格开发。
1. 同步 PRD 功能要求、User Stories、逻辑、UI 与技术契约,收敛首期应用中心样式。
2. 接入现有内置应用页面与最底部应用中心入口,保留独立设置和固定运行记录入口。
3. 为常驻显隐和排序设计最小持久化契约,保留已发布的相关设置与用户数据,按保存结果更新导航。
4. 接入应用设置的共同界面与既有设置更新路径,先验证魔法笔记多入口状态一致性。
5. 验证右侧现有文件、终端、浏览器 Tab;移除计划中的左下角工具快捷条和专用于该快捷条的意图接口工作。
6. 单独实现资源快照契约、Main 采集服务、Preload API 与右侧 `resources` 单实例面板。
7. 验证键盘、焦点、浅深主题、窄窗口、设置失败、重启持久化和跨平台行为。
首期使用内置应用清单,不依赖远程商店、插件注册或安装服务。商店目录、推荐、安装与
更新机制在后续独立设计中确定;Demo 中的模拟商店不接入首期产品界面。
## 12. 项目活动汇总
对应 [FR-14](./prd.md#fr-14-项目活动汇总),按[活动逻辑](./logic-design.md#9-项目活动汇总)
派生状态,界面行为见[活动界面](./ui-design.md#10-项目活动汇总)。
- `conversation-activity.ts` 从 `App.tsx` 的会话、`activeConversationIds`、全部可见
`assistantTasks` 和项目元数据派生会话行、全局及项目计数,不新增持久化活动副本。
`ProjectActivity` 渲染汇总与弹窗,`ProjectSwitcher` 复用项目计数。
- 持久化快照刷新时,同一条仍在流式输出的消息保留本地待审批、待回答信息,持久化终态
则清除这些信息。响应成功后对应 Task 立即退出等待状态;响应期间到达的新问题、审批或
终态不能被旧响应覆盖。
- `AssistantDatabase.listTasks()` 保留最近历史窗口(默认 100 条),额外包含窗口外当前
运行或等待处理的可见 Task;`activeVisibleTaskSelect` 与 `toTask` 的有效状态保持一致,
待调度运行规范化为 `queued`,不因旧 Task 状态被误纳入实时集合。
- `listConversations()` 保留最近 100 条活动会话,同时包含上述 Task 关联的较早活动会话,
以及含 `streaming` 消息的较早活动会话。两类列表去重后仍按原时间顺序返回,结果允许超过
历史窗口大小,避免后台工作被最近记录挤出。
- `openActivityConversation` 按确切 Conversation ID 导航;内存中缺失时刷新持久化会话
列表并合并,再查找同一 ID。刷新失败或会话已不存在时显示通知,不回退到项目最新会话。
设置离开检查通过后才提交项目、会话及界面清理状态。
- 远程恢复成功提示沿用独立的自动消失计时器;活动汇总更新不重置或替代该计时器。
@@ -0,0 +1,223 @@
# 应用导航与系统工具入口 UI 设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
本文定义入口、设置和轻量资源监控界面。文件、终端和浏览器的内容布局继续遵守助手工作栏
文档。
## 1. 侧栏信息架构
```text
品牌
项目选择
项目活动汇总(有活动时)
新对话
搜索
当前对话
魔法笔记 3
知识库
智能心跳 1
运行记录
最近对话
...
────────────────────────
[文件] [终端] [浏览器] [资源] [设置]
```
“当前对话”和“运行记录”属于固定系统入口。三个内置应用位于二者之间。侧栏底部不再显示
“本地工作区”和 `platform · arch`;排障信息移至关于或诊断页面。
## 2. 底部系统工具栏
底部使用连续平整表面和顶部分隔线,不做成账户卡片或嵌套卡片。按钮顺序固定:
| 位置 | 图标 | 可见工具提示 | 点击结果 |
| --- | --- | --- | --- |
| 1 | `FolderOpen` | 文件 | 聚焦不可关闭的工作区面板 |
| 2 | `SquareTerminal` | 终端 | 打开或聚焦当前执行空间终端 |
| 3 | `Globe` | 浏览器 | 聚焦当前 Conversation 的匹配浏览器实例 |
| 4 | `Activity` | 资源监控 | 打开或聚焦本机资源监控面板 |
| 末位 | `Settings` | 设置 | 打开设置页面 |
图标使用项目现有 Lucide 库。每个按钮采用统一的 `34px × 34px` 点击区域,内部图标建议
`16px`。工具栏使用 `8px` 内边距和 `4px` 间隔;设置按钮通过自动空白靠右,不添加第二条
视觉分隔线。
### 2.1 状态
- 默认:透明背景,图标使用次级文字色。
- 悬停:使用弱表面色,不能强于当前选中状态。
- 选中:使用强调选中背景,并提高图标对比度。
- 键盘焦点:显示统一焦点环,不能被侧栏边缘裁切。
- 处理中:创建或激活请求未完成时暂时禁用对应按钮,保留尺寸并提供进行中名称。
- 徽标:只用于等待处理、错误或活动状态,不显示无意义的常驻绿色圆点。
选中状态表达“对应工作栏面板正在显示”,不表达功能已安装或权限已授予。
## 3. 主导航应用区
应用入口沿用现有 `nav-item` 行,不使用应用卡片。图标、名称和徽标保持单行:
```text
[图标] 魔法笔记 3
[图标] 知识库
[图标] 智能心跳 1
```
隐藏某个应用后,其余应用向上补位。运行记录始终位于最后一个可见应用之后,不因应用显隐
改变样式或归类为工具。
设置页打开期间,主侧栏显示最后一次成功保存的导航状态。Switch 更新成功后导航立即变化,
失败时保持原导航。
## 4. 应用与工具设置
设置中心新增“应用与工具”分类,采用 `standard` PageShell:
```text
应用与工具
管理主导航中的应用。系统工具由 GoodBuddy 提供并固定在侧栏底部。
应用
魔法笔记 笔记、待办和评论 [显示在侧边栏 开]
知识库 管理和检索知识 [显示在侧边栏 开]
智能心跳 查看计划、建议和运行历史 [显示在侧边栏 开]
系统工具
文件 打开当前工作区 内置
终端 进入当前执行空间 内置
浏览器 打开 GoodBuddy 受控浏览器 内置
资源监控 查看本机 CPU、内存和磁盘 IO 内置
系统入口
当前对话、运行记录和设置位置固定,不参与上述配置。
```
应用显隐使用共享 Switch,标签必须是“显示在侧边栏”,不能缩写为“启用”。切换时立即保存,
请求期间禁用三个应用显隐 Switch;失败后恢复原值,并在对应设置行显示可重试错误。系统工具
使用紧凑只读列表,不渲染禁用的卸载、停用或固定按钮。
功能已停用时,对应应用行显示“功能已停用”和设置入口,保留显隐偏好但暂时禁用 Switch。
若某应用自身支持业务启停,应在该应用原有设置中保留“启用功能”开关,并用说明区分:
- “显示在侧边栏”只控制入口。
- “启用功能”控制该功能是否运行。
## 5. 助手工作栏反馈
点击系统工具后,右侧工作栏按现有布局回流打开。主内容页面保持不变,侧栏不渲染工具内容。
- 文件:激活默认存在且不可关闭的“工作区”Tab,焦点进入目录区域或错误修复入口。
- 终端:激活匹配终端,焦点进入终端输入区;新建期间保留按钮宽度和进度状态。
- 浏览器:激活绑定当前 Conversation 的最近使用“浏览器”Tab;没有匹配实例时创建一个,
没有 Conversation 时按产品规则复用最近实例或进入待绑定空状态。
- 资源监控:激活“资源监控”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-pressed` 或等价的可访问状态,不能只依靠背景色。
- 徽标的数量或状态合并进按钮可访问名称,例如“终端,1 个等待处理”。
- 应用显隐 Switch 支持 Space 切换,保存失败后恢复原值并保留焦点。
## 8. 响应式
- 侧栏宽度 `220px` 至 `420px` 时,底部按钮尺寸不随宽度缩放。
- 窄窗口继续使用现有侧栏抽屉;工具栏不转为多行。
- 空间不足时缩小设置按钮前的弹性空白,不隐藏四个系统工具和设置。
- 工具提示不得超出窗口;靠近底边时显示在按钮上方。
- 粗指针设备不依赖悬停,按钮的可访问名称和选中反馈保持有效。
## 9. 未来插件入口
插件市场交付后另行定义插件区域、固定数量、溢出和权限界面。系统入口与四个系统工具的位置
保持保留,不向插件开放。
## 10. 项目活动汇总
对应 [FR-14](./prd.md#fr-14-项目活动汇总),状态与计数遵守
[活动逻辑](./logic-design.md#9-项目活动汇总)。
- 全局汇总按钮位于项目选择器下方,上行标明“全项目活动”,下行显示待处理与运行中数量;
箭头跨两行居中,无活动时整个按钮隐藏。
- 项目菜单在项目行显示同口径数量,保留“本地项目 / 远程项目 / 远程通道”分组和 SSH Host
层级,不因活动状态重排项目。数量为零时不显示标记,连接状态仍独立显示。
项目名独占一行,状态数量排列在名称下方,避免窄侧栏中数量挤占名称宽度。
- 点击汇总打开活动对话框,先列“待处理”,再列“运行中”,空分组隐藏。每行显示项目名称、
会话标题及明确状态文字,不展开 Task、Job 或 Run 列表,也不自动切换用户正在查看的会话。
- 点击一行先关闭对话框、释放 Modal 焦点隔离,再请求进入该条会话;允许离开当前页面后,
选择所属项目、清除搜索和该会话未读标记、清除任务选择与会话操作菜单,窄窗口同时关闭侧栏。
设置页沿用未保存修改的离开检查,取消离开时保留原页面。
- 对话框沿用全局 Modal 规范,支持 Tab 循环、Escape、关闭按钮、点击遮罩关闭及焦点恢复;
窄窗口限制宽高并在内容区滚动。活动全部结束时关闭对话框并隐藏入口。
@@ -0,0 +1,236 @@
# 应用导航与系统工具入口 User Stories
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 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:管理应用入口
### US-B1 识别完整应用
作为用户,我希望魔法笔记、知识库和智能心跳显示在主导航,以便知道它们会打开完整页面。
验收:
- Given 对应功能已启用且应用设置为显示
- When 用户打开主侧栏
- Then 应用位于“当前对话”和“运行记录”之间
- And 点击应用在主内容区打开对应一级页面
- And 原有待办或建议徽标继续显示
### US-B2 隐藏低频应用
作为用户,我希望隐藏不常用的内置应用,以便缩短主导航,但保留已有数据。
验收:
- Given 应用已有内容
- When 用户关闭该应用的“显示在侧边栏”Switch
- Then 主导航不再显示该应用
- And 应用数据、计划和业务启停状态不变
- And 用户可从同一设置位置恢复显示
### US-B4 保留功能启停语义
作为魔法笔记用户,我希望导航显隐与功能启停相互独立,以免整理导航时关闭功能。
验收:
- Given 魔法笔记功能已启用
- When 用户只隐藏其侧栏入口
- Then 笔记及待办处理能力继续按原设置工作
- And 再次显示时恢复原页面和数据
### US-B5 恢复显隐保存失败
作为用户,我希望入口设置保存失败时继续使用原导航,以免界面状态与实际设置不一致。
验收:
- Given 应用显隐更新尚未完成
- When 用户切换“显示在侧边栏”Switch
- Then 三个应用显隐 Switch 在请求期间暂时不可操作
- And 更新成功后主导航立即变化
- And 更新失败时 Switch 和主导航恢复原状态并显示可重试错误
## 4. Epic C:使用系统工具
### US-C1 一键查看文件
作为本地工作用户,我希望点击左下角文件按钮后直接查看当前工作区文件。
验收:
- Given 当前项目有有效根目录
- When 用户点击文件按钮
- Then 助手工作栏展开
- And 聚焦默认存在且不可关闭的工作区单实例
- And 文件按钮通过选中状态表明该面板正在显示
### US-C2 处理无效工作区
作为用户,我希望目录不可用时看到修复方法,并收到明确反馈。
验收:
- Given 工作区面板处于跟随模式
- And 当前项目和默认工作区均不可用
- When 用户点击文件按钮
- Then 工作区面板仍打开
- And 面板说明目录不可用并提供选择或修复目录的入口
- And 应用不静默改用其他目录
### US-C3 快速进入终端
作为本地工作用户,我希望点击终端按钮后进入当前执行空间的终端。
验收:
- Given 当前执行空间已有正在启动或运行中的终端
- When 用户点击终端按钮
- Then 工作栏聚焦最近使用的匹配终端,不创建重复实例
- Given 当前执行空间没有终端
- When 用户点击终端按钮
- Then 创建一个绑定该执行空间的新终端
- Given 当前没有项目
- When 用户点击终端按钮
- Then 创建或聚焦使用本机 Home 的终端
- Given 当前执行空间只有已退出、已中断或失败的终端
- When 用户点击终端按钮
- Then 创建新终端并保留原终端的历史状态
### US-C4 使用远程项目终端
作为远程项目用户,我希望同一个终端按钮进入远程项目目录,以免误在本机执行命令。
验收:
- Given 当前项目绑定托管 SSH 执行空间
- When 用户点击终端按钮
- Then 创建或聚焦绑定该远程项目及其权威目录的终端
- And 面板持续显示远程主机和工作目录
- And 连接失败时保留重连与诊断信息
### US-C5 打开受控浏览器
作为用户,我希望浏览器按钮打开 GoodBuddy 受控浏览器,以便查看与 Agent 相同的会话。
验收:
- Given 浏览器面板跟随当前上下文且当前会话已有浏览器会话
- When 用户点击浏览器按钮
- Then 工作栏聚焦绑定当前 Conversation 的最近使用浏览器实例
- And 不打开系统默认浏览器或无意创建重复实例
- Given 现有浏览器实例都绑定到其他 Conversation
- When 用户点击浏览器按钮
- Then 工作栏创建并聚焦绑定当前 Conversation 的新浏览器实例
- Given 当前没有活动 Conversation
- And 已存在浏览器实例
- When 用户点击浏览器按钮
- Then 工作栏聚焦最近使用的浏览器实例并保持其目标
- Given 当前没有活动 Conversation
- And 不存在浏览器实例
- When 用户点击浏览器按钮
- Then 创建待选择目标的空实例
- Given 用户需要同一 Conversation 的另一个独立页面
- When 用户从工作栏“+”选择浏览器
- Then 每次创建拥有独立页面和导航状态的新实例
- And 同一 Conversation 内共享隔离登录上下文
### US-C6 查看本机资源
作为本地工作用户,我希望快速查看 CPU、内存和磁盘 IO,以便判断任务变慢是否与本机负载
有关。
验收:
- Given 资源监控面板尚未打开
- When 用户点击左下角资源监控按钮
- Then 助手工作栏创建并激活单实例资源监控面板
- And 页面显示整机 CPU、内存、磁盘读写及 GoodBuddy 桌面进程摘要
- And 页面每 2 秒更新一次,并用图表保留最近 5 分钟
- When 用户切换到其他工作栏 Tab、收起工作栏、隐藏窗口或最小化窗口
- Then 资源监控停止请求新样本
- When 用户再次显示窗口并返回资源监控
- Then 首次采样建立区间指标基线,下一次采样开始显示 CPU 和磁盘 IO 速率
- Given 某项指标在当前平台不可用
- When 页面收到部分结果
- Then 该指标显示“不可用”及原因,其他指标继续更新
### US-C7 从任意应用打开工具
作为用户,我希望在魔法笔记、知识库、智能心跳和运行记录页面也能打开系统工具。
验收:
- Given 用户位于任意一级页面
- When 用户点击文件、终端、浏览器或资源监控
- Then 主页面保持不变
- And 右侧工作栏打开对应面板
- And 工具不会覆盖主内容或伪装成一级页面
## 5. Epic D:可访问性和布局
### US-D1 使用键盘打开系统工具
作为键盘用户,我希望按顺序聚焦并打开左下角工具,以便不依赖鼠标。
验收:
- 每个系统工具和设置都是独立按钮
- Tab 顺序与视觉顺序一致
- Enter 和 Space 均可激活按钮
- 每个纯图标按钮都有可访问名称和工具提示
- 焦点样式不被侧栏边界裁切
### US-D2 在窄窗口使用快捷栏
作为小屏用户,我希望侧栏抽屉中的工具按钮保持可操作。
验收:
- 工具按钮保持单行且不重叠
- 不因宽度不足隐藏文件、终端、浏览器、资源监控或设置
- 触控点击区域符合统一界面规范
- 关闭侧栏抽屉后焦点返回原触发按钮
+16
View File
@@ -0,0 +1,16 @@
# 助手工作栏
助手工作栏统一承载本机与远程执行空间、Task、成果、文件、浏览器和终端等持续工作界面。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [通用助手工作栏与执行空间 PRD](./prd.md) | 工作栏整体产品范围、执行空间和各工作面板归属 |
| [多终端页签 PRD](./terminal-tabs-prd.md) | 工作栏应用的固定、可关闭、单实例和多实例策略,以及多终端生命周期 |
| [浏览器多实例技术设计](./browser-tabs-technical-design.md) | 浏览器 Tab 身份、Conversation 绑定、IPC、BrowserService 和 MCP 路由 |
| [实现与验证进度](./progress.md) | 工作区与 Runtime 相关改动的实现事实、验证证据与剩余阻塞 |
| [Runtime 交互边界](./runtime-interactions.md) | 原生权限、结构化问答、后台拒绝、终端交互、托管 Runtime 并行与输出及消息底部请求状态的适配范围 |
当前尚未拆出独立 User Stories、功能逻辑和 UI 设计文档;相关内容仍保留在两份现有 PRD
中。浏览器多实例涉及跨进程身份和工具路由,单独使用技术设计文档维护。
@@ -0,0 +1,208 @@
# 浏览器多实例技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 基础多实例已实施,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` 调用生成图片。侧栏内终端关闭确认显示期间,同样隐藏原生浏览器视图,
确认或取消后恢复当前实例,避免原生子视图遮挡 HTML 确认按钮。
浏览器应用声明 `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 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 管理工具。
File diff suppressed because it is too large Load Diff
+528
View File
@@ -0,0 +1,528 @@
# 工作栏实现与验证进度
## 2026-09-13 OpenCode 事件流关闭顺序修复
候选检查中的 lifecycle 失败已稳定复现,4 路并行后 `openEventBodies` 为 3 而不是 0。
原因与统一修正规则见[复发缺陷记录](../../quality/recurring-defects.md#事件迭代结束前释放底层订阅);
聊天与 Compact 共用最小迭代器包装,不修改 OpenCode SDK、重试时限或现有断言时限。
- 加强既有正常结束与消费方提前返回回归,修复前两项均失败,确认最终 `signal.aborted`
不能证明关闭顺序正确;修复后通过。
- `npm test -- src/main/agent/opencode-runtime-lifecycle.test.ts
src/main/agent/opencode-runtime.test.ts src/main/agent/opencode-runtime-permissions.test.ts
src/main/agent/opencode-subagent.test.ts`:4 个文件、99 项通过。
- Windows 当前源码配合真实 OpenCode `1.18.29`、真实 SDK/HTTP MCP 和 loopback 模型通过
1、4、8 路并行、取消一条而另一条正常结束,以及实际原生 Compact;各阶段打开的 SSE
响应流均归零。验证是生产 Runtime 入口,不是完整 Electron UI;真实服务商调用 0 次。
- 已核对远端 `AcpRemoteRuntime` 的 Agent-owned transcript 轮询与
`AgentOwnedAcpPrompt` 的 ACP stdio 连接,不使用本机 `client.event.subscribe`。本次未改
Agent、远程桥或协议,不需要远端对应修复,也未将本机测试算作远程 Host 复验。
- 最终 `npm test`:349 个文件通过、9 个跳过;4,089 项通过、66 项跳过、0 失败,
耗时 571.43 秒。`npm run release:notes:verify`、`npm run typecheck`、`npm run lint`
全部通过;下节失败结果保留为修复前记录,不再是当前本地回归结论。
- 发布说明按 Desktop `v0.12.12`、Agent `agent-v0.11.23` 到最终候选的差异核对,
不按中间开发提交分别列出修正。用户明确不增加事件流条目,已批准中英文说明保持不变。
- 本轮未提交、推送或打标签,未运行本地生产构建或打包;远程问答完整生产链路的真实
服务商验证及候选 CI/原生发布验证仍沿用各自待验收状态,不由本机 SSE 修复代替。
## 2026-09-13 Desktop 0.13.0 / Agent 0.11.24 候选检查
本轮仅准备本地候选:更新 Desktop 与 Agent 版本、用户批准的中英文发布说明及功能清单,
远程原生问答按问题修复分类。Agent 发布说明测试允许仅有修复章节,同时检查中英文分类
一致;未修改 Runtime、Agent 问答或事件流实现。Agent `0.11.24` 需要先升级 Desktop 至
`0.13.0`,OpenCode 保持 `1.18.29`。
- `npm run release:notes:verify`、`npm run typecheck`、`npm run lint`:通过。
- `npm test`:348 个文件通过、1 个失败、9 个跳过;4,088 项通过、1 项失败、66 项跳过,
耗时 571.96 秒。唯一失败为 `opencode-runtime-lifecycle.test.ts:120`,并行请求结束后
`openEventBodies` 期望为 0、实际为 3,轮询未在时限内满足;该现象早已有记录,
本轮没有完成根因判断,也没有放宽断言或跳过测试。
- 单独复跑上述 lifecycle 文件及 `agent-release-workflow.test.ts`、
`release-notes-source.test.ts`、`release-notes-service.test.ts`:发布相关 15 项通过,
lifecycle 同一断言再次失败。该结果不能替代一次全量通过。
- 更早的一次全量运行在调整 Agent 发布说明章节断言前主动停止,未取得汇总,不计为通过。
- 候选版本一致性、历史 Desktop 发布说明未改动、48 个 Markdown 相对链接目标和
`git diff --check` 已通过。
- 本轮真实模型验证调用 0 次,未重跑真实 Host;既有 SSH/确定性模型证据见下文,
不将其扩写为完整生产 Main/Preload 配合真实服务商的验收。
- 未提交、推送或创建标签,未运行本地生产构建、打包或 LoongArch 预览。
全量测试失败、完整远程生产链路及候选 CI/原生包验证仍未解决,候选不标记为可发布。
## 2026-09-13 SSH 问答 UI 复验与取消后续发
在真实 Electron 窗口中使用当前 `App`、入口样式和主题函数,经测试 IPC 接入生产
`createManagedRemoteAcpRuntime`。远端使用共享 Linux x64 Host、真实 SSH attach、当前源码
Agent、原生 OpenCode 和生产模型桥;模型响应来自远端 loopback 确定性服务。配置、项目、
队列和存储 API 复用 App 测试 fixture,因此本次证据范围为 App Renderer 到真实 SSH
Runtime,未覆盖完整生产 Main/Preload、生产数据库持久化、发布安装或重启恢复。
窗口测试发现待答期间取消后,同一对话立即发送会报
`Active prompt operation identity cannot change before terminalization`。Agent 取消分支
原先只写语义记录,重复终态写入失败后未留下可核对的操作终态;Desktop 也保留了旧
binding。修复复用 Agent 的异常终态记录方法,并在 Desktop 显式取消后核对终态、关闭
已终结 binding 和释放会话通道。新增两项回归验证 Agent 取消核对及同一对话续发。
已通过的 UI 场景:父子问题同时待答、重复事件不丢草稿、自由文本回答、选项回答、注入
一次回复失败后原输入重试、跳过、待答期间取消、取消后在同一对话发出新请求并正常完成。
鼠标和输入使用 Electron 原生输入 API。重复问题和回复失败由驱动注入,其余问题、工具
进展及完成事件来自真实远端 Runtime;驱动消费内部 checkpoint,并将取消映射为公开
`error/status=cancelled` 事件,不把内部 Runtime 事件直接送给 Renderer。
浅深主题各检查四组窗口尺寸,问题表单无横向溢出、草稿保留,提交按钮可滚动到可见位置。
短窗口需纵向滚动查看问题与输入,未宣称整张卡片同时可见。检查目标为 1280×800、
960×720、720×640、640×420,Windows 175% 缩放下实际 CSS 内容区各多 1 像素。
临时驱动、JSON 测试报告、各轮日志及 PNG 保留在批准临时目录,使用
`question-ui-*`、`remote-question-*` 和 `question-validation-*` 文件名前缀;驱动按
[Electron UI 自动化规范](../../quality/electron-ui-automation.md)的现有启动方法重建,
不是仓库通用 UI runner。SSH 测试结束后停止专属 daemon 并清理远端专属目录和上传文件。
复现时在批准临时目录执行 `node question-ui-build.cjs`、
`node remote-question-build.cjs`,随后设置 `GOODBUDDY_QUESTION_UI=1` 并执行
`node remote-question-run.cjs`。脚本依赖本机既有加密 Host 配置和 Host 上的测试 Node
依赖目录;不得将这些本机条件当作新环境已经满足。早期驱动的内部事件转发、原生输入
等待和跨轮模型结果判断错误均已修正;它们与实际取消续发缺陷分别记录,未将失败轮次
改记为通过。首轮窗口退出过早,未取得完整模型夹具计数,历史夹具请求累计数不完整;
外部模型调用为 0。
最终 SSH/UI 一轮完成 4 次输入区提交、2 次回答和 1 次跳过,收到 4 个原生问题,取消
核对结果为 `terminal/cancelled/processTree=empty`,随后新请求完成。该轮模型夹具请求
精确为 7 次,外部模型调用 0 次,Renderer 控制台错误 0 条,驱动退出码 0。最终定向
回归分两条命令执行:`npm exec vitest run src/agent-daemon/runtime-acp-backend.test.ts
src/main/agent/acp-remote-runtime.test.ts` 为 96 项通过;
`npm exec vitest run src/agent-daemon/remote-question-binary.test.ts
src/agent-daemon/opencode-subagent-plugin.test.ts src/agent-daemon/model-bridge.test.ts
src/main/remote-agent/protocol-remote-runtime-channel.test.ts
src/agent-daemon/runtime-composition.test.ts src/main/ipc.test.ts` 为 179 项通过、5 项跳过。
`npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
首轮默认并行 `npm test` 的打包测试超时,整轮随后被 120 秒工具时限中断;单 worker
重跑为 4,084 项通过、66 项跳过,耗时 626,816 毫秒。此结果早于本节取消修复,
不作为最终源码全量结果。
取消修复后的全量命令为 `node node_modules/vitest/vitest.mjs run --maxWorkers=1
--reporter=json --outputFile=<批准临时目录>/question-validation-final.json`,覆盖 358 份
测试文件,结果为 4,087 项通过、1 项失败、66 项跳过,监督进程记录耗时 617,138 毫秒、
退出码 1。唯一失败是 `tests/agent-package.test.ts:372` 的大清单安装用例,耗时
63,366 毫秒,超过该用例 60 秒上限。随后
`npm exec vitest run tests/agent-package.test.ts -- --maxWorkers=1` 单独复跑为
22 项通过、1 项跳过,整文件耗时 60.08 秒;没有修改该打包测试或放宽超时。
全量失败记录保留,不能将分次复跑结果写成单轮全绿。
最终 UI 原始日志为 `question-ui-1789278503920.log`,8 张待答截图为
`question-ui-{light,dark}-{1280,960,720,640}.png`,另有完成截图
`question-ui-success.png`。已逐张打开待答 PNG 审阅字体、字段、按钮、焦点和短窗口
滚动位置;未发现问题卡片遮挡或按钮不可达。尺寸切换等待 CSS 过渡结束后再截图,
窗口使用独立临时 session partition。当前证据不覆盖全键盘导航或正式 profile 的清理。
## 2026-09-13 托管 SSH 原生问答
- 当前源码支持前台托管 SSH OpenCode 的父、子孙会话原生问题、回答和拒答,复用已有
Renderer 队列与 Task 归属。问题来源为原生 `question.asked`;模型正文和普通工具活动
不生成问答表单。协议、身份与生命周期规则见
[远程主机技术设计](../remote-host/technical-design.md#acp-与断线)。
- 新增 `remote-question-binary.test.ts`,使用真实 OpenCode `1.18.29` ACP 进程、生产插件、
Agent Prompt owner 和 loopback 模型夹具,覆盖父问题、Task 子问题、拒答、取消、
跨 binding 回答拒绝、迟到回答拒绝,以及临时回复能力不进入语义记录。
插件测试另覆盖孙会话归属、并发重复事件、HTTP 路径认证、原生待答列表校验和端口关闭;
Runtime 测试覆盖已 ACK 记录后的待答补发、回复失败重试与已回答历史去重。
- 共享 Linux x64 Host 使用既有加密凭据和固定 Host Key,经真实 SSH attach 启动当前源码
的隔离 Agent。生产 `createManagedRemoteAcpRuntime` 入口完成 Ask、Execute 并行父子
问答、拒答和待答期间取消;两个并行问题在首次回答前均到达 Desktop Runtime,子问题
带 Task 归属,回答后原生 Task 和父轮次正常完成。拒答结束原生轮次,取消后迟到回答
明确失败。最终一轮 7 次确定性模型夹具请求,外部模型调用 0 次;测试 daemon 停止,
测试目录与上传文件已清理,未修改 Host 原有 Runtime registry。
- 最终定向回归:11 份测试文件,219 项通过、5 项平台跳过,包含 Agent、模型桥、ACP
Runtime、认证控制通道及托管入口测试。`npm run typecheck`、`npm run lint` 通过。
本轮全量 `npm test -- --reporter=json --outputFile=<临时报告>` 为 4,081 项通过、
0 项失败、66 项跳过;之后的插件身份与并发去重补充由最终定向回归和 Host 复测覆盖。
- 本节早期验证未操作 Electron 窗口、重装共享 Agent 或模拟整机重启;问答队列的界面行为由
既有 App/ChatTimeline 测试覆盖。同一存活 Prompt 的待答补发由 Runtime 回归验证,
不把它描述成 Agent 进程重启后的问题恢复。旧 Agent 和任意第三方 ACP 服务的范围见
[Runtime 交互边界](./runtime-interactions.md)。
## 2026-09-13 并行提问顺序回答
- 修复同一前台请求的后续提问覆盖未回答问题:Renderer 按 ID 保存待答队列,逐次显示、
回答和跳过,保留失败重试输入;运行中快照刷新保留队列,终态清理全部待答项。
交互和归属限制见 [Runtime 交互边界](./runtime-interactions.md#运行与失败)。
- 本机 SDK 使用已收到的 Task 子会话元数据传递可选 `childTaskId`;Task 卡片显示待答状态,
展开后可定位并聚焦队首表单。没有已知 Task 关联的问题仍能回答。
- 六份相关测试文件共 346 项通过:`App.test.tsx`、`ChatTimeline.test.tsx`、
`conversation-activity.test.ts`、`opencode-runtime.test.ts`、
`opencode-runtime-permissions.test.ts`、`opencode-runtime-lifecycle.test.ts`。
覆盖并发到达、重复 ID、草稿保留、提交失败、队列推进、快照刷新及终态与回复竞态。
- 真实本机 OpenCode 二进制配合 loopback 模型服务验证两次原生提问均在首次回答前到达,
随后分别回传并完成请求;同时保留 Task 文件读取和 Ask 工具限制验证。该用例每轮
6 次 loopback 模型请求,外部模型调用 0 次。完整桌面点击路径由 App 组件测试覆盖,
本轮未手动操作 Electron 窗口。
- 首次 `npm test` 达到工具 200 秒上限;后台重跑 `npm test -- --reporter=verbose` 完成,
348 份测试文件通过、9 份跳过,4,069 项通过、66 项跳过,耗时 572.20 秒。
随后补充已回答 ID 的去重必须先于任务状态更新,最终 18 项问答及活动相关回归通过。
- 独立评审补充同一 React 批次首次两次收到相同 ID 的回归:修复前显示两个待答项。
在消息函数式更新器内复核待答和已回答 ID,重复事件直接返回原消息,保留状态;外层
去重仍阻止重复事件改写任务状态。修复后 19 项定向回归、typecheck 和完整 lint 通过。
- 最终 App 全文件复跑为 217 项通过、1 项失败;失败项为并行开发中的外部知识绑定测试
`adds a new external binding to the current selection without re-enabling an excluded library`,
“添加知识库”按钮未启用,定向复跑仍失败。本次未修改该测试或知识绑定逻辑。
`npm run typecheck` 与差异空白检查通过;早期完整 lint 通过,最终完整 lint 扫到并行
工作新增的 `.kb-app-live` 产物而失败。保持产物原样,使用
`npm run lint -- --ignore-pattern ".kb-app-live/**" --ignore-pattern ".kb-app-live*.cjs" --ignore-pattern ".kb-app-live*.ts"`
复跑通过。
- 远程 `AcpRemoteRuntime` 仅接入权限请求,尚无业务问答桥;本次未修改 Agent、ACP
传输、共享子任务进度适配器或 SSE 订阅,不将本机问答验证算作远程覆盖。
## 2026-09-13 浏览器租用标签页关闭与显式恢复
- 更新 `tests/browser-tabs-electron-e2e.ts`,移除两处租用期间拒绝关闭的旧断言。
当前行为见 [Agent 与 MCP 路由](./browser-tabs-technical-design.md#6-agent-与-mcp-路由)
和 [浏览器需求](./prd.md#77-浏览器)。同步修改 BrowserService、IPC、Gateway 和直连模型工具。
- Windows 真实 Electron/Chromium 与 loopback HTTP/MCP 验证通过:关闭已物化的预留页和
已租用主标签页,等待原 WebContents 的 `destroyed` 事件,确认租约结束而请求信号未取消。
同一 token 和 MCP session 仍可列出工具;旧目标快照返回关闭提示且不创建页面。
显式 `browser_navigate` 创建不同身份的专用替代页,后续 MCP 快照读取该页。
- 同 Conversation 的 sibling 仅在主标签页关闭后取得 `primary` 标记,页面 URL、导航状态、
时间戳、工作栏身份和 Cookie 内容保持不变;另一 Conversation 的页面与可见 viewport
保持不变。撤销 capability 后完成替代页、同级页和 Context 清理。
- 已先检查 runner:esbuild 输出临时 CJS,启动真实 Electron,子进程上限 120 秒,结束后
删除临时 bundle。`node build/run-browser-tabs-electron-e2e.cjs` 最终退出码 0;
前三轮分别修正测试对销毁事件时序、服务旧 ID 错误文案和 sibling 主标签标记的假设。
测试文件 ESLint 与限定文件的 `git diff --check` 通过。
- 覆盖边界:本轮通过 Main BrowserService 和真实 MCP transport 操作页面,未点击完整
Renderer/Preload 关闭入口,未注入执行中的工具取消,也未运行直连模型循环或其他共享工具。
浏览器、Gateway、直连 Provider、Model Runtime 和 IPC 回归共 359 项通过、1 项跳过,
包括执行中关闭、请求继续完成和其他工具继续可用。全库 typecheck、lint 和 diff 检查通过。
全量 `npm test` 在 Agent 离线依赖安装用例失败后达到 200 秒执行上限;无外部模型调用。
- 本轮未修改 Agent、桥接或 Runtime 产品源码,也未连接远程 Host。远程调用若进入同一桌面
Gateway,会使用上述服务端路由;远端 Runtime 到桌面的实际传输、取消传播及恢复仍需真实
Host 验证,本记录不将本机 loopback 结果算作远程覆盖。
## 2026-09-12 浏览器刷新与关闭后的错误清理
- 按 [浏览器工具栏规则](./prd.md#77-浏览器) 修复空白标签页允许刷新的问题;
刷新需要 Main 的已提交 URL,地址草稿不启用刷新,首次导航仍可停止加载。
- 侧栏操作错误在切换面板、关闭当前页签或收起工作栏后清除;旧面板的异步失败不再回写。
作用域实现见 [浏览器多实例技术设计](./browser-tabs-technical-design.md#4-renderer-与-viewport)。
- Sidebar 回归 38 项、App 浏览器筛选回归 5 项、BrowserService 回归 43 项通过。
`node build/run-browser-tabs-electron-e2e.cjs` 通过,验证真实空白页、导航、页面隔离和关闭;
该脚本不覆盖 Renderer 提示显示,提示清理由组件测试验证,本轮未手动操作完整桌面 UI。
- `npm run typecheck` 和两份修改源码的 ESLint 检查通过。全量 `npm test` 在 Agent
离线依赖清单用例超时后,整轮达到 200 秒执行上限;包含完整 App 测试的补跑也达到该上限。
`npm run lint` 被工作区原有未跟踪的 `application-tool-navigation/navigation-demo.js`
中 27 项浏览器全局变量错误阻断,未修改该文件,不将整库校验记为全绿。
- 本轮仅修改桌面 Renderer 控件与提示状态,未改 BrowserService、Runtime、Agent 或远程协议;
不需要单独的远程实现。无外部模型调用。
## 2026-09-10 提交后保留问答
- 桌面端在提交成功后保留结构化问答回顾,支持多轮回答、跳过及本地会话重新加载;
交互规则见 [Runtime 交互边界](./runtime-interactions.md#运行与失败)。
- App 回归覆盖提交与重载、下一轮问题提前到达、终态先于提交返回、提交失败;数据库
回归覆盖完整替换、增量保存、关闭重开、多选及超过 1,000 字符的自定义答案。
- `npm test`:3,900 项通过、66 项跳过,Agent 离线依赖清单测试在 60 秒超时;
该项独立复跑通过(58.33 秒)。`npm run typecheck`、`npm run lint` 与差异空白检查通过。
- 本轮未手动运行真实 OpenCode 问答会话,外部模型调用 0 次。修改仅涉及桌面端展示与
本地保存,不改变 Runtime 回传协议或 Agent 路径,不新增远程 ACP 问答能力。
## 2026-09-10 Git 历史测试夹具修复
- 桌面候选 `877cf58` 的 CI 在四项工作区测试准备 Git 提交时因缺少作者身份失败。
`c037d7f` 移除测试身份后引入了机器配置依赖;本机已有身份使该问题未在本地暴露。
- 测试现在把已暂存的 tree 写入固定 Git commit 对象,并仅更新临时仓库的分支引用,
不调用 `git commit`,不修改或覆盖任何 Git 身份配置。历史、重命名差异、分页和脏文件
分支冲突仍由真实 Git 与生产 `manageWorkspace` 验证,不用 mock 或跳过用例替代。
- 新增夹具自检确认 `git fsck --strict` 通过、父提交链有效、未暂存内容保持不变且仓库配置未改。
聚焦测试 11 项通过、2 项因当前文件系统大小写不敏感而跳过;无身份 Runner 的验证待新候选 CI。
- 完整 `npm test` 为 3845 项通过、66 项跳过、1 项未修改的活动列表测试超时;
随后 `ActivityPanel.test.tsx` 的 16 项独立复跑全部通过,未放宽时限或删除断言。
`npm run release:notes:verify`、`npm run typecheck`、`npm run lint` 通过。
未将这次本地整库结果记为全绿,发布仍以新候选的 CI 校验和生产构建通过为前提。
- 本轮只改测试和验证记录,产品功能、Agent 源码及已批准的双语发布说明不变,无外部模型调用。
## 2026-09-10 工作区布局与分支输入样式
- 按 [FR-W1、FR-W4](./prd.md#76-工作区) 将视图切换和刷新合并到顶部行,
文件与 Git 工具按视图显示;移除根目录的孤立图标及空导航行,子目录保留文字路径导航。
分支搜索复用共享 `field`,面板限宽、分支列表限高滚动,创建按钮使用紧凑次操作。
- 新增布局、子目录返回、焦点恢复、分支搜索/切换/创建回归;工作区、设置界面、设置存储、
IPC 和 Sidebar 五个测试文件共 354 项通过。另确认安全与数据的默认 `toolApproval` 为
`always`,对应“Execute 自动授权已启用的工具”;已有明确保存的禁止策略仍保留。
本轮没有修改授权策略或默认值。
- 当前构建的 Windows Electron 使用独立 profile 和临时 Git 项目,实际 App → Preload →
Main 路径通过分支搜索、切换、创建和子目录新建文件;磁盘与 Git 分支结果均确认。
文件/Git 切换保留浏览目录,返回根目录后导航行移除、焦点回到刷新;Escape 收起分支面板并恢复焦点。
真实安全与数据页面的默认选中项也确认为自动授权。
- 浅色与深色下,将实际工作栏布局宽度设为 300、420、650px 后均无工作区横向溢出。
输入框为共享 13px 正文、标签 11px,分支面板最大 360px;15 个分支时列表可在 240px 内滚动。
这些是布局检查,不作为分栏拖拽或原生浏览器鼠标交互的验证。
- `npm run build:bundle`、`npm run typecheck`、`npm run lint` 通过。
整库 `npm test` 为 3843 项通过、1 项失败、66 项跳过;失败项是未修改的
`opencode-runtime-lifecycle.test.ts` 事件流关闭时序断言,单独复跑通过。
未再次运行整库,不将此结果记为整库全绿。
- 本轮产品改动仅为共享 Renderer 工作区布局,未改 Main/Agent 的工作区命令、
desktop-to-Agent 协议或 Runtime 执行逻辑;远程项目继续使用同一组件和既有回调,
本轮未做新的 Host 实测。无外部模型调用,所属测试应用、浏览器会话和调试监听已关闭。
## 2026-09-10 提交审查修复
- [FR-W3、FR-W6](./prd.md#76-工作区):同一目录项可仅修改大小写,仍拒绝覆盖其他文件、
目录和硬链接目录项;提交 Diff 同时使用重命名前后路径;分支后台刷新不再使历史请求
遗留 busy;自动目录刷新包含当前浏览目录并去重。
- 浏览器请求仅预留目标身份,首次导航才创建实际资源;Main 状态携带工作栏实例身份,
Renderer 直接绑定同一页面,停止或回收后清除旧绑定。跨会话显式选择不会被应用类型
同步覆盖,终端关闭确认期间释放原生浏览器 viewport。规则以
[浏览器多实例技术设计](./browser-tabs-technical-design.md) 为准。
- 删除普通浏览器操作的自动截图、固定等待、重试及 JPEG 状态缓存/IPC 字段,保留显式截图。
- 整合复核补齐现有 32 页签容量处理与 Harness 代理的已发现工具复用。新增容量回归验证
满额时布局仍有效、页面保留,关闭一项后显示原页面;Harness 回归验证发现接口后续失败时
已知工具调用不再触发发现。相关三文件 66 项通过,最新 typecheck 与 lint 通过。
- 工作区聚焦测试 43 项通过、2 项需大小写敏感文件系统而跳过;IPC 与 Sidebar 聚焦回归
132 项通过。随后新增的 Ask 指令、继承 Runtime 用量持久化及计划模式 App 场景,3 项通过。
- 扩展真实 Electron/MCP E2E 在独立临时 profile 通过:四个未使用的请求预留不创建会话、
WebContents 或 UI 事件;首次 MCP 导航物化相同 Tab/工作栏身份,恢复不额外创建页面,
并验证独立页面、共享 Cookie、当时的请求租约关闭保护和清理;关闭保护已于 2026-09-13 移除。
- 当前源码的共享 Linux x64 Host 验证已通过,覆盖工作区目标保护和重命名/Git Diff,
并通过桌面托管 ACP → 隔离 Agent → OpenCode 完成 Ask/Execute 和继承 Runtime 指标持久化;
该组共 3 次真实模型调用,清理后无所属进程残留。完整记录见
[远程 Host 开发验证](../remote-host/technical-design.md#agent-开发期间的真实-host-验证)。
- 最终整库 `npm test` 为 341 个文件通过、9 个文件跳过,3839 项通过、66 项按平台或手动
条件跳过、0 失败;`npm run typecheck`、`npm run lint`、开发期 `npm run build` 和
`git diff --check` 通过,19 份修改 Markdown 的相对链接目标检查通过。
先前整合中的一轮为 3823 项通过、6 项失败、66 项跳过,另有被移动的测试路径加载失败,
不作为最终结果;源码稳定后完整重跑得到上述全绿结果。
- 原文档解析焦点回归改为等待 Modal 的焦点效果,保留焦点与背景隔离断言,没有改动产品逻辑。
- Windows 独立 Electron profile 的完整 App → Preload → Main UI 验证通过;最终复测使用
本轮最终开发构建。启用浏览器后普通 Execute 没有创建原生浏览器视图,工具导航只创建
一张同时显示在工作栏的页面,跨会话选择保留原页面与 viewport。
- 实际终端连通后,从浏览器关闭该终端可正常显示确认并操作按钮;原生视图可见性按
可见 → 确认时隐藏 → 取消后可见变化,终端关闭清理完成。无效地址 `http://` 被拒绝后,
原控件可重试有效地址并关闭页面;关闭端口返回了过滤代理错误页,因此该 UI 场景不作为
Chromium `loadURL` 拒绝的实机证据。服务与 Renderer 释放绑定回归另有自动测试覆盖。
- 桌面使用确定性 loopback 模型服务驱动真实生产路径,共 24 次本地 completion 请求,
未发出外部模型请求;可选真实 Provider 探针没有找到已加密保存的 OpenAI 兼容文本连接,
未据此宣称桌面真实模型验收通过。Ask 与编程 Subagent 的桌面结果见
[直连模型验收记录](../direct-model-agent/progress.md#2026-09-10-提交审查后的桌面-ui-验收)。
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
## 2026-09-10 固定基础页签与浏览器多实例
- 工作栏注册表已分别声明实例策略、默认上下文、默认打开、必须存在、可关闭和排序属性。
任务中心与工作区保持默认存在且不可关闭;浏览器改为当前 Conversation 上下文的多实例。
布局加载按 `required` 补回缺失的基础页签,并修复失效活动实例。
- 任务中心增加“当前项目”“全局任务”“全部项目”范围,默认跟随活动项目;任务和审批按
Conversation 所属项目过滤,范围模式随工作栏布局持久化。
- 浏览器服务已拆分 Conversation Context 与 Browser Tab。每个 Tab 拥有独立页面、Driver、
操作队列、状态和元素引用空间,同一 Conversation 的 Tab 共享 partition、代理和 Cookie。
- Renderer 使用工作栏实例 UUID 原子创建或恢复 Browser Tab,并以 UUID viewport token 防止
延迟 cleanup 隐藏新活动 Tab。关闭单个实例只释放对应 Tab。
- 直连模型和请求级 MCP capability 在请求开始时优先绑定可见 Tab,其次绑定 primary Tab,
并持有使用租约。当时绑定 Tab 拒绝关闭,该行为已于 2026-09-13 改为允许关闭和显式导航恢复;工具参数未增加模型可见
`tabId`,已有浏览器工具 schema 保持不变。
- 聚焦回归命令覆盖 Shared、BrowserService、Electron Session、IPC、Preload、MCP、Renderer
和 Workbar,13 个测试文件共 453 项通过;`npm run lint` 与 Web TypeScript 检查通过。
- `node build/run-browser-tabs-electron-e2e.cjs` 通过。该测试启动真实 Electron/Chromium 和本地
HTTP 页面,创建两个 `WebContentsView`,验证页面隔离、同 Conversation Cookie 共享、真实
loopback MCP 的固定 Tab 导航与快照、当时的使用租约关闭保护,以及单 Tab 和最终 Context 清理。
- `npm run typecheck`、`npm run lint`、`npm run build:bundle` 和 `git diff --check` 通过。
- 本轮整库 `npm test` 为 3686 项通过、61 项跳过、4 项失败。失败位于 portable/release 的
ASAR 元数据 fixture、Runtime 版本探测和 DeepSeek Harness 的本地 MCP fixture;定向复跑仍
失败。本次工作栏、浏览器、IPC 和 MCP 聚焦回归均通过,不将整库结果记为通过。
## 2026-09-09 移动目录选择与 Git 控件修正
- 本轮在已有文件管理改动上修正 [FR-W3、FR-W4、FR-W6](./prd.md#76-工作区):
本机移动调用创建项目使用的 `settings.selectWorkspace()`;远程移动调用
`sshHosts.browseDirectories()`,按项目 Host 浏览。所选目录映射为工作区相对目标,
保留原名;取消不提交移动,外部目录在弹窗内提示限制。
- 取消、分支、Fetch、历史展开和提交行接入共享控件样式;删除菜单项左对齐。
远程目录列表限制滚动高度,保留加载、空目录、失败和截断提示。
- `npm test -- src/renderer/src/WorkspaceFilesPanel.test.tsx src/renderer/src/RightAssistantSidebar.resize.test.tsx src/main/workspace/workspace-management.test.ts src/main/ssh/ssh-host-directory-browser.test.ts`
通过 56 项,覆盖本机目录选择桥接、Windows 与 POSIX 目标映射、取消与越界选择、
远程 Host 和目录参数、取消未完成的浏览,以及 Git 控件与真实文件管理服务。
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git|opens the global assistant sidebar"`
通过 12 项。工作栏切换测试改为等待项目目录加载后出现刷新按钮。
- `npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
- 本轮整库 `npm test` 完整运行 944 秒,3642 项通过、61 项跳过、6 项失败。
工作栏加载时序断言已修正并定向复测通过;其余 5 项涉及 Agent 离线依赖测试超时、
DeepSeek Harness MCP 加载、Runtime 版本探测,以及 portable/release 打包产物检查。
未再次运行整库测试,不记为整库通过。
- 隔离 Electron 窗口加载当前源码组件和 CSS,检查浅色、深色主题下 300px、420px 工作栏:
无横向溢出,分支与 Fetch 同行,提交行左对齐;移动弹窗没有路径输入框,取消为次按钮,
删除项计算样式为左对齐。截图保留于本机临时验证目录。此检查使用桥接替身,未自动操作
系统原生目录对话框,也不替代完整应用中的人工验收。
- 尝试从隔离 Electron 配置调用真实 SSH 目录浏览服务时,已有凭据在
`safeStorage.decryptString` 解密失败,尚未建立 Host 连接。真实远程目录选择验收仍受阻;
未修改用户配置或远端文件,本轮未调用真实模型。
## 2026-09-09 文件管理与 Git 工作区扩展
- 已只读核对根目录规范、UI 设计系统、目录浏览组件、WorkspaceAccess 与 Agent 协议。
本轮范围定义于 [PRD 工作区 FR-W1 至 FR-W6](./prd.md#76-工作区)。
- 当前生产路径具备目录、属性、文本预览和工作树 Diff;Agent 的写入及 Git operation
仍是只读拒绝接口。文件管理、分支与提交历史尚未实现或验证,不能视为完成。
- 待完成源码实现、整库 test/typecheck/lint、真实桌面路径与当前源码 Linux Host 验证。
本轮开始前已有浏览器与工作区未提交改动,保留这些改动;不提交或推送。
## 2026-09-09 工作区页签交互
- 已核对当前工作树中的 `RightAssistantSidebar.tsx` 和 `WorkspaceFilesPanel.tsx` 差异:
文件预览期间目录树保持挂载,接入返回位置与焦点恢复、文件与未提交更改分段视图、
目录就地重试,以及文件预览和 Diff 操作。行为定义见 [PRD 工作区](./prd.md#76-工作区)。
- `npm test -- src/renderer/src/RightAssistantSidebar.resize.test.tsx src/renderer/src/WorkspaceFilesPanel.test.tsx`
通过 35 项,覆盖目录展开、返回位置与焦点、视图选择、刷新失败保留内容、Markdown
源码切换,以及切换项目后丢弃未完成的文件预览。
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git"` 通过 11 项,
覆盖应用中的文件预览、路径复制、默认应用打开、返回和任务结束刷新。
- 本轮 `npm test` 完整运行结果为 3629 通过、61 跳过、5 失败。失败位于 Agent 离线
依赖打包超时、DeepSeek Harness MCP 加载、Runtime 版本探测,以及两个打包产物检查,
不将该结果记为整库回归通过。
- `npm run lint` 和差异空白检查通过。最新 `npm run typecheck` 被同时进行的浏览器
接口变更阻塞:测试替身缺少 `hide`、`setBounds`、`setViewport`,部分侧栏测试仍传递
已移除的浏览器回调。该轮类型检查未通过,未改动另一项任务的接口或测试替身。
- 尚未进行真实桌面视觉验收;组件测试的滚动断言不代替实际窗口中的布局和滚动检查。
- 范围仅限工作区页签,不包含项目文件搜索或其他页签改动。
## 2026-09-09 托管 Runtime 并行阻塞
- Windows 完整隔离测试程序通过界面发送 OpenCode 原生双子代理、直连与 Continue 文件
任务;随后三路上游请求在 917 ms 内发出并重叠,全部完成。普通并发没有复现全局排队。
- 在同一程序中,让真实模型执行一次 4,000 行、约 98 KB 文件的 CRLF → LF 转换,
同时运行另一个会话。原生进程的无凭据存活探针两次延迟 3,301 ms、3,085 ms;
原生热线程持续使用 CPU,而 Main 没有同步满载。操作不是模拟事件或直接调用
Runtime 的脚本,模型请求来自实际输入框和发送按钮。
- 托管 OpenCode 关闭未消费的自动 Git 快照后,完整程序重复相同操作和并行会话,
90 秒观察中的最大原生探针延迟为 210 ms,没有超过 500 ms 的探针,两条请求均完成。
该对照验证已复现的快照阻塞得到消除,不声称已经证明历史 90–125 秒事件的唯一原因。
- Continue 的实际 Electron 原生宿主在不排空 stdout 时被注入的 128 KiB 输出阻塞;
使用当前生产适配器后,输出在 5 ms 内完成,20 次状态读取全部成功,最大 5 ms。
这证明管道修复有效,不代表普通启动约 640 字节的输出就是历史卡顿原因。
- 修复版完整程序再次完成原生双子代理与 Continue 文件任务,实际请求重叠,三个页面
都通过计数按钮功能检查。Windows 诊断与程序验证合计 40 次成功模型调用;
另一次 UI 请求返回账户额度不足的 402,用户充值后才继续,没有把失败当成通过。
- 当前源码 Linux x64 Agent 经桌面生产管理链路完成 Ask 读取和 Execute 换行转换,
共 4 次成功模型调用;最终原生配置确认 `snapshot: false` 且权限仍为 `ask`/`allow`。
此验证覆盖实际 Agent、模型桥、工具完成和关闭,不量化 Linux 提速,也未注入取消。
- 定向回归先验证 7 项失败,再验证 124 项通过、5 项 Windows 平台跳过;
全项目类型检查和修改文件 lint 已通过。首次全量回归为 3625 通过、1 失败、61 跳过;
失败来自生命周期测试的包装流在底层 reader 关闭后仍计数为打开,已通过独立流实验
复现并改为观察 `reader.closed`,没有因此修改生产取消逻辑。最终候选全量回归为
3626 通过、0 失败、61 跳过,`npm run typecheck`、`npm run lint`、
`npm run release:notes:verify`、差异空白与文档相对链接检查通过。本机和远端的
测试进程已回收,安装版和正式 Host 环境未改动;正式发布仍需候选 CI 与资产核实。
运行策略见
[托管 Runtime 并行与输出](./runtime-interactions.md#托管-runtime-的并行与输出)。
## 2026-09-08 消息底部请求状态
- 已接入本机 OpenCode 父会话的原生重试等待及开始事件;直连模型把退避等待与实际发起
重试分开显示,Continue 初始文案改为中性的请求处理。显示规则和未接入边界见
[消息底部请求状态](./runtime-interactions.md#消息底部请求状态)。
- 消息底部按真实事件显示准备、工具、待回答及终态;有新进展时清除旧状态,结束后不
接受迟到状态覆盖。仅该处状态点改为静态,会话列表闪点、发送和停止按钮未改动。
- Windows 隔离开发桌面通过实际 UI 发送验证 OpenCode 的 503/Retry-After 等待、
原生重试开始、真实模型回复与完成;直连模型通过派发前连接拒绝、500 ms 等待、
重试、真实回复与完成。故障由本机测试代理注入,不冒充服务商实际故障。
三个隔离测试配置总计 5 次真实文本模型调用;另有一次直连重试被测试代理的调用
上限明确拒绝,界面正确显示失败,最终单次补测完成。未执行用户命令或修改安装版。
- DOM 状态记录确认底部无闪烁、完成后无旧重试文案,浅色截图可见原生重试计划与原因;
底部 WCAG 2 A/AA 定向检查无违规。最小化窗口的补充截图超时,未作为通过证据。
隔离窗口及测试代理已结束。
- 最终 `npm test -- --reporter=json --outputFile=<临时报告路径>` 为 3618 项通过、
0 项失败、61 项按平台或手动条件跳过;首轮的压缩状态旧断言已修正,途中被用户中断
的运行不计为通过。`npm run typecheck`、`npm run lint`、
`npm run release:notes:verify`、差异空白与相关文档相对链接检查通过。
## 2026-09-08 Desktop 0.12.8 安装包修复
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
后,插件及传递依赖逐文件复制验证通过。
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
不能把当前源码回归通过当作真实发布包已通过。
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
## 2026-09-08 Execute 目录权限等待修复
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
禁用工具,当前请求精确分配的只读能力例外。Execute 不把 Workspace 当作目录边界。
- `permission.asked` 不再忽略所有非父会话事件。Main 用 OpenCode Session 的 `parentID`
确认属于当前请求的子孙会话后按当前模式回复;其他并行会话不受影响,同一请求只回复
一次。子会话权限不伪装为父会话中的待完成工具。
- 真实 OpenCode `1.18.9` 和候选 `1.18.29` 均已通过本地确定性模型驱动的原生 Task
工作区外读取回归;同一 Runtime 的 Ask 仍不暴露文件与命令工具。
- Continue `1.5.47` 的真实 CLI 已通过 Execute Shell 写入工作区外测试文件;DSH 固定
Host 已通过 Execute 工作区外写入、随后 Ask 拒绝同一写工具,未增加权限策略或人工审批。
- 真实 OpenCode `1.18.29` 已完成原生 Task 后的 yes/no 与自由文本问答回传;父、子会话
提问及后台任务不能交互时的失败路径均有回归测试。Continue 真实 `AskQuestion` 暴露了
QuizService 扁平数据结构与原适配器不一致的问题,现已按锁定版本修正;真实自由文本
回传、yes/no、选项及跳过均验证通过,自定义答案不再误标为选项。
- DSH 活跃 Execute 的 ACP 权限回调不再等待第二次 authorizer,Ask 继续拒绝;未添加
通用业务问答工具或从终端日志猜测确认意图。完整范围见
[Runtime 交互边界](./runtime-interactions.md)。
- 当前源码 Linux x64 Agent 通过签名组包、生产安装、真实 Ask/Execute、原生子代理
工作区外写入、重连同一 daemon 和 stop/bootstrap。实机回归同时定位并修复了握手
Base64URL 响应误用 ID 校验及断流未结算等待;13 项 Unix 原生端点测试和连续 20 次
握手通过。两轮真实模型调用合计 10 次,均 completed;本轮测试 Agent 已停止。
- 最终完整回归使用 `npm test -- --reporter=json --outputFile=<临时报告路径>`,并通过
`GOODBUDDY_TEST_OPENCODE_BINARY` 指向已验证版本的候选 OpenCode。结果为 338 个文件、
3597 项通过、0 项失败、61 项按平台/手动条件跳过,报告 `success=true`、退出码 0;
Windows 跳过的 Unix 端点场景另在 Linux Host 运行了上述 13 项原生测试。
- 最终 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 和差异空白检查
通过,相关 Markdown 相对链接有效。此前失败、暂停及控制台截断的回归记录由本次完整
JSON 报告覆盖,不代表仍有测试失败。未执行系统休眠/唤醒或本地桌面生产构建、打包;
后续发布仍需对应候选的 CI 构建和跨平台验证。
## 2026-09-08 OpenCode 复用与原生控件恢复
- `OpenCodeRuntime` 仅在没有活动请求时探测缓存的 embedded server;活动对话或
Compact 期间直接复用服务,避免新会话的一次短探测失败关闭其他会话正在使用的进程。
空闲探测失败时仍清除旧 client 和会话映射并重建;并发调用共享检查与重建。
进程退出或服务关闭会中止绑定它的对话与 Compact,事件流重连等待也可立即取消,
请求明确报错,不自动重放已执行命令。本次修复后的真实模型验证共 6 次:正常并发、
已复现场景下的共享服务保护均正常完成;主动结束测试进程后旧请求明确失败,新请求
在替换服务上完成。此前“新会话完成、旧会话持续等待”的故障不再出现。
- 请求级 MCP 注册与断开通过实例内 `mutateMcp` 队列串行执行。仅实际发出 `mcp.add`
的名称进入清理列表;注册尚在排队时取消不追加断开。断开的 1 秒超时从出队后开始。
- `App` 按本机/远程、连接方式、项目和 Runtime 选择组成 scope 缓存原生清单,用请求
代次阻止旧 scope 结果回写;同 scope 会话刷新不清空重取,切换会话重置消息级选择,
Runtime 切换状态也按 generation 隔离。清单请求失败或返回 `unavailable`/`partial`
时保留已有可用缓存并有界重试;没有可用缓存时保留新返回的部分数据,因此
`unavailable` 重试为可用 `partial` 后可显示 Agent 和 Command。
验证记录:首轮定向测试 273 项通过、5 项跳过;补充修复后的 Runtime 与生命周期测试
77 项通过,界面定向测试 6 项通过,类型检查通过。修复前的 lint 检查通过。
全量测试超过 120 秒被终止,期间出现一项 DS 本地 stdio MCP 测试失败,尚待定位。
这是当次历史结果。后续发布前修复与实机验证见本文上方及
[远程主机技术设计](../remote-host/technical-design.md);真实系统休眠恢复仍未完成验证。
## 2026-09-06 项目工作区
范围见 [PRD 工作区](./prd.md#76-工作区)。
当前源码已移除重复标题与底部完整 Diff 入口,使用彩色 Git 状态字母,点击更改项读取
单文件 Diff。已暂存与未暂存差异分开展示;本机 Git 测试覆盖无首次提交、未跟踪、
暂存后继续修改、重命名、删除及包含方括号的路径。Git 刷新驱动目录树重新读取,保留
展开状态;SSH 项目不提供本机系统打开按钮。审查后补齐了当前 Diff 随刷新重读、
旧请求结果失效,以及超过 50 项变更的继续加载入口。
验证记录:
- 最终 `npm run typecheck`、`npm run lint`、`npm run build` 均通过。
- 工作区专项 7 个测试文件复跑:317 项通过,3 项 Linux 专用测试在 Windows 跳过。
任务完成测试同时检查目录树出现新文件。
- 最终 `npm test`:326 个测试文件、3552 项测试通过,9 个文件、58 项测试按既有条件
跳过。复核首轮曾有一项浏览器地址草稿测试超时,单独复跑及最终全量复跑均通过;
更早的超时/失败记录不再作为最终验收结果。
- 后续复核已通过既有凭据和固定 Host Key 经 VPN 连接共享 Linux x64 Host,使用当前
源码构建的隔离 Agent 包安装并启动。真实 `RemoteWorkspaceAccess → Agent git/status`
与 `git/diff` 路径覆盖暂存后继续修改、删除、重命名、未跟踪和方括号路径;均通过。
Git 验证未调用模型。
- Windows 隔离桌面实例已通过真实点击验收:当前项目单文件暂存/工作树 Diff 分组与
增删颜色正确,修改测试文件后点击刷新可更新正在查看的 Diff,53 项变更经加载更多
全部可访问。截图留在本地验证目录,不纳入产品包。
目录分页、任意目录切换和 HTML 预览不在本轮改动范围内。
@@ -0,0 +1,74 @@
# 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 环境。任意第三方 ACP 服务及旧 Agent 不因此获得通用问答接口,候选不表示已经发布。
## 运行与失败
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
- 前台同一请求的待答问题按公开 `questionId` 去重,按到达顺序保存在消息的
`pendingQuestions` 中。每次显示队首表单;有多个待答请求时显示数量。后续提问和重复
事件不替换当前表单或清空输入,回答或跳过成功后只移除对应 ID,再显示下一张表单。
提交失败保留队列、输入和重试入口,队列未清空时继续显示“等待你的回答”。
- 本机与托管 SSH OpenCode 在提问到达前已收到原生 Task 的 `metadata.sessionId` 时,将精确匹配的
子会话关联为可选 `childTaskId`。问答卡片显示该任务名称;对应 Task 卡片显示
“等待你的回答”,展开后可点击“前往问答表单”滚动并聚焦当前队首表单,仍按顺序回答。
没有已知关联的子孙会话问题照常排队,不猜测子任务归属,也不从工具名或进度文本构造表单。
- 回答或跳过成功后,会话保留只读的“问题与回答”,显示原问题及对应的选项答案、
自定义文本或“已跳过”。同一任务的多轮问答逐次保留,随本地会话保存,重新打开后仍可查看。
提交失败时保留输入与重试入口,不生成已回答记录;取消未回答的问题不记为主动跳过。
- 提交响应晚于任务结束时仍保存已提交的问答,但不覆盖完成或失败状态;晚于下一轮提问时
不清除新的待答卡片。旧版会话未保存的问题与答案无法从“已提交”状态文案恢复。
- 待答队列仅保存在当前桌面会话内存中,不写入会话快照。刷新仍在运行的消息时保留整个
本地队列;完成、失败、取消事件或持久化终态快照清除整个队列。迟到的提交结果可以保存
已回答记录,但不会恢复表单或覆盖终态。本机 Runtime 不恢复重启前未提交的问题;托管 SSH
重新附加同一存活 Agent Prompt 时,由 Agent 当前待答列表补发问题,已回答的历史事件不补发。
- 后台任务收到结构化提问时,通过现有 Main 路径明确失败并提示改为前台对话,不永久等待。
- 请求结束或取消后清除对应待答映射;过期回答明确报错,不转交给后来的请求。
- 任意终端程序的密码、OAuth 登录、许可证确认或 stdin 提示不自动回复 `yes`。这类程序应
使用其非交互参数,或由用户在终端完成;不能把权限确认和业务决策混为一谈。Shell
工具保留其现有取消与超时,不新增日志解析或通用交互代理。
## 托管 Runtime 的并行与输出
- 本机和 Agent 管理的 OpenCode 启动配置都显式设置 `snapshot: false`。GoodBuddy 不消费
原生自动 Git 快照、快照差异摘要或快照撤销;关闭这条内部路径,避免大行级差异的同步
计算阻塞同一原生进程中的其他会话。
- 文件工具、Shell、原生子代理、工作区 Git 状态和单文件差异保持可用;不新增全局会话
队列、请求重放或总执行截止时间。外部 OpenCode Server 的配置不由 GoodBuddy 改写,
已运行的旧原生进程也不会热更新配置。
- Continue 使用 HTTP 传递响应和状态;Electron UtilityProcess 启动后持续排空不用展示的
stdout,避免控制台输出填满管道后阻塞宿主。stderr 的有界错误处理、请求取消与进程
回收保持不变,不把诊断输出复制进聊天或持久日志。
- 远端最终启动配置由[远程主机技术设计](../remote-host/technical-design.md)定义,
模型桥生成最终配置时同样保留上述快照策略。
## 消息底部请求状态
- 状态属于当前请求,不与顶部 Runtime 的连接中、就绪、不可用混为一谈。提交后先显示
“正在准备请求”;没有更具体的活动事件时显示“等待后续进展”,不声称模型正在思考。
- 本机 OpenCode 只把当前父会话的 `session.status.retry` 映射为等待重试,显示原生
`attempt`、计划重试时间和有界脱敏原因;后续 `busy` 才显示已开始重试。其他会话的
状态不串入当前消息,不猜测总重试次数,不把 SDK 事件流重连当作模型重试。
- 显示优先级为待回答、待确认、进行中的上下文压缩、明确的请求状态、待执行或执行中的
工具、未完成的子任务、等待后续进展。压缩沿用已有活动块,不重复增加底部提示。
正文、推理或工具进展会清除旧状态;子任务更新不覆盖父请求的重试提示。
- 完成、失败、取消后保留真实终态,迟到的状态事件不能把请求恢复成处理中。底部状态点
为静态,不凭请求尚未结束暗示仍有进展;会话列表闪点和发送、停止按钮保持原有行为。
- Continue、DeepSeek Harness 和远端 ACP 保留其已接入的处理与工具事件,不伪造未接入
的原生重试状态。直连模型的重试等待与请求开始文案见
[直连模型 UI 设计](../direct-model-agent/ui-design.md#6-错误与通知);本次不改变任何
Runtime 的重试上限、请求重放或取消策略。
实现与真实 Runtime 验证证据见[进度记录](./progress.md)。
@@ -0,0 +1,658 @@
# 助手工作栏多终端页签 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`。`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。用户可以重命名终端。
- 当前 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
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、自定义标题、公开目标引用和宽度比例。
- 加载布局时根据 `required` 补回缺失实例,并拒绝重复的单实例应用;应用策略不写入实例
数据,避免持久化状态覆盖注册表。
- `reorderable` 已进入共享契约;当前 Tab 壳层尚未提供拖放或键盘重排,不能把声明属性视为
已完成的排序功能。
- 不持久化终端正文、未执行输入、凭据、环境变量、PTY PID、SSH Channel 或派生 Host
identity。
- Main 创建终端后返回新的不透明 Session ID;Renderer 不能选择 PID 或 Channel。
- 应用重启恢复终端 Tab 描述,但状态为“会话已结束”,不自动建立 SSH 连接或启动本机
Shell。
- 运行期间切换项目不会改变已经创建的终端目标。用户需要新目标时通过“+”创建新终端。
## 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 换成下拉框。
- [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 保存、环境准备和项目使用之间的职责。
@@ -1,399 +0,0 @@
# 自动任务、目标与调度 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-08-13 |
| 依赖 | [自动化、监督与记忆平台总体设计](./automation-platform-architecture.md) |
## 1. 背景
GoodBuddy 当前的定时任务支持单次、每日和每周触发固定 Ask 提示,并保存任务和成果;
智能心跳支持每日或每周回顾有界的会话、任务和已确认记忆。两者尚不能表达事件触发、
目标、成功标准、预算、停止条件和安全恢复。
## 2. 产品边界
| 类型 | 用户意图 | 是否形成循环 |
| --- | --- | --- |
| 定时任务 | 在指定时间执行已知操作 | 否 |
| 事件任务 | 当明确事件发生时执行已知操作 | 否 |
| 目标任务 | 在预算内持续推进到可验证结果 | 是 |
智能心跳是特殊的定时观察任务。并行实验属于独立产品。
## 3. 已确认的产品决策
1. 自动化定义与每次运行分离,编辑计划不改变已启动 Run。
2. 第一阶段保留现有定时任务的 Ask 限制,Execute 分阶段开放。
3. Execute 自动化不能因无人值守而绕过现有审批、沙箱和工具控制。
4. 应用退出后不承诺继续运行,重启后只进行状态恢复和错过执行结算。
5. 目标任务必须有成功标准,以及预算或人工结束条件。
6. 模型可以提出计划,确定性状态机负责预算、停止、权限和恢复。
7. 同一计划默认最多一个活动 Run。
8. 后台任务可被背压延后,不能挤占用户正在等待的前台请求。
9. 结果未知的外部副作用步骤不自动重试。
10. 项目、知识库、记忆、目录和工具范围在保存和运行页持续可见。
## 4. 目标
- 支持单次、每日、每周、每月、工作日和受限 Cron。
- 支持任务完成、失败、会话完成等内部事件触发。
- 允许用户用自然语言生成结构化草稿,再检查后启用。
- 为目标任务建立有界的“观察、计划、行动、评估”循环。
- 提供幂等、租约、错过执行、取消、重试、恢复、预算和审计。
- 为后续并行实验和持续学习复用协议、指标和运行基础。
## 5. 非目标
- 第一阶段不提供任意节点、脚本和循环的通用 DAG 编辑器。
- 不允许模型编写并执行任意 Shell、SQL 或无限频率 Cron。
- 不支持应用退出后通过未安装的系统服务继续运行。
- 不把“模型说完成了”作为唯一成功标准。
- 不允许自动任务静默修改自身权限、触发器或预算。
- 不在目标循环中无限创建子任务或专家。
## 6. 创建与启用
用户可以先输入自然语言意图:
```text
每周五下午 5 点总结本项目本周完成和失败的任务,
列出下周三个优先事项,不要修改文件。
```
模型只生成草稿:
- 名称、说明和自动化类型。
- 触发器。
- 目标、输出和成功标准建议。
- 工作模式和 Runtime 建议。
- 数据范围。
- 预算、停止条件和通知。
草稿不能自动启用。用户必须检查结构化配置。
### 6.1 所有计划必填
- 名称、范围和类型。
- 触发器。
- 工作模式和 Runtime。
- 输入、输出和通知。
- 预算和数据保留。
- 知识库、记忆、目录和工具范围。
### 6.2 目标任务额外必填
- 目标描述。
- 至少一个成功标准。
- 约束。
- 最大轮数或截止时间。
- 每轮评估方式。
- 无进展处理。
### 6.3 启用前检查
- 时区和下一次运行时间可解析。
- 项目、目录、Runtime 和模型可用。
- Ask 没有写入或外部副作用要求。
- Execute 的工具和审批范围明确。
- 预算不是无界值。
- 事件来源存在且已启用。
- 目标任务存在停止条件。
## 7. 触发器
### 7.1 时间触发
```ts
type TimeTrigger =
| { type: 'once'; at: string; timezone: string }
| { type: 'daily'; localTime: string; timezone: string }
| {
type: 'weekly'
weekdays: number[]
localTime: string
timezone: string
}
| {
type: 'monthly'
day: number | 'last'
localTime: string
timezone: string
}
| {
type: 'cron'
expression: string
timezone: string
}
```
受限 Cron 只允许五字段,不支持秒、年份、宏、`L`、`W`、`#` 或供应商扩展。
Main 负责解析并展示未来五次运行时间,默认最小间隔为 15 分钟。
### 7.2 事件触发
第二阶段支持:
- `conversation.completed`
- `task.completed`
- `task.failed`
- `artifact.created`
- `knowledge.sync.completed`
- `magic_note.updated`
事件触发必须配置来源范围、确定性过滤、去重窗口、冷却时间和并发上限。
基础匹配不调用模型。
### 7.3 手动触发
- “立即运行”创建独立 Run,不改变下次计划时间。
- 多次点击使用调用级幂等键去重。
- 未保存的变更需先保存为新版本,或明确使用当前已发布版本。
### 7.4 错过执行
| 策略 | 行为 |
| --- | --- |
| `skip` | 记录跳过,不补跑 |
| `run_once` | 无论错过多少次,只补一次 |
| `catch_up_bounded` | 在数量和时间窗口上限内补跑 |
有界补跑默认最多 3 次、最多回溯 7 天。补跑同样受并发和预算控制。
### 7.5 时区和夏令时
- 保存 IANA 时区,不保存固定 UTC 偏移。
- 春季不存在的本地时间在当日第一个有效分钟运行。
- 秋季重复时间只运行一次。
- 系统时区变化不自动修改计划时区。
- UI 显示计划时区与本机时区差异。
## 8. 目标任务
### 8.1 目标模型
```ts
type AutomationObjective = {
statement: string
successCriteria: SuccessCriterion[]
constraints: Constraint[]
deadline?: string
}
type SuccessCriterion =
| { type: 'artifact_exists'; kind: string; minimumCount: number }
| { type: 'task_state'; taskId: string; expected: 'completed' }
| {
type: 'metric_threshold'
metric: string
operator: string
value: number
}
| { type: 'checklist'; items: string[] }
| { type: 'human_review' }
| {
type: 'model_rubric'
rubricId: string
minimumScore: number
}
```
模型 Rubric 不能是唯一标准,除非任务本质是开放内容评价且 UI 明确标注。
### 8.2 有界循环
```text
Observe
→ Plan next action
→ Check permissions and budget
→ Act or request approval
→ Evaluate progress
→ Complete, pause, revise or continue
```
每轮持久化观察摘要、下一步、实际任务或工具、成果、指标、预算、进展状态和
Supervisor 决策。只保存专门生成的结构化理由摘要,不保存隐藏推理。
### 8.3 无进展检测
出现任一情况进入 `attention_required`:
- 连续两轮没有指标改善或新成果。
- 重复提出相同下一步。
- 连续失败达到上限。
- 需要的输入或权限不可用。
- 剩余预算不足。
- Supervisor 判定目标或前提需要澄清。
默认暂停并请求用户选择,不自动扩大范围。
### 8.4 计划修订
目标任务可以建议修改步骤、缩小目标、请求输入、增加预算或改变 Runtime。
修改范围、预算、Runtime、工作模式或权限必须用户确认,并形成新版本或 Run 修订记录。
## 9. 工作模式与审批
### 9.1 Ask
- 默认只读。
- 只使用明确开放的只读数据工具。
- 不写文件、不执行命令、不发送消息、不修改远程数据。
- 输出进入成果和通知。
### 9.2 Execute
按以下顺序开放:
1. 有人值守,沿用逐工具审批。
2. 预批准低风险工具和参数范围。
3. 经过专项验证的内置无人值守模板。
即使预批准,也不能扩大目录和能力。高风险或越界动作进入 `waiting_approval`。
密码输入、支付、授权、删除、公开发布和生产变更不能预批准。
## 10. 预算与背压
```ts
type AutomationBudget = {
maximumDurationMs: number
maximumIterations: number
maximumModelCalls: number
maximumInputTokens?: number
maximumOutputTokens?: number
maximumToolCalls: number
maximumChildTasks: number
maximumArtifactBytes: number
maximumConcurrentChildren: number
}
```
建议默认值:
| 类型 | 最长时间 | 模型调用 | 子任务并发 |
| --- | --- | --- | --- |
| 定时 Ask | 5 分钟 | 4 | 1 |
| 心跳回顾 | 5 分钟 | 2 | 0 |
| 目标 Ask | 30 分钟 | 12 | 2 |
| 目标 Execute | 30 分钟 | 12 | 1 |
前台请求优先。后台使用独立并发池,达到上限时排队。高负载时低优先级心跳和维护任务
记录为 `deferred`,压力解除后有界恢复,不能一次性释放全部积压。
## 11. 重试、恢复与取消
| 失败类型 | 行为 |
| --- | --- |
| 瞬时网络或限流 | 指数退避,有界重试 |
| 模型格式错误 | 最多一次结构化修复 |
| 配置或权限错误 | 不重试,等待修复 |
| 无副作用的确定性工具失败 | 按工具策略重试 |
| 结果未知或已有外部副作用 | 不自动重试 |
应用退出时停止声明新 Run,取消可取消工作,活动 Run 标记为 `interrupted` 并保存安全
检查点。重启后用户可恢复、复制剩余步骤或放弃;结果未知步骤必须先人工核实。
暂停 Plan 只阻止新 Run,不终止当前 Run。取消 Run 必须传播到子任务和 Runtime,
但不能把已发生的外部副作用假装撤销。
## 12. 输出与通知
输出可保存为文字或文件成果、创建后续任务建议,或仅通知。后续可支持更新指定魔法笔记。
通知事件:
- Run 完成或失败。
- 等待审批。
- Supervisor 要求关注。
- 目标达成。
- 预算达到 80%。
- 连续无进展。
同一事件不同时显示重复页内横幅和全局通知。
## 13. 信息架构
计划列表显示名称、类型、范围、启用状态、下次运行、最近 Run、目标状态和需要关注数量。
计划详情页签:
- 概览。
- 目标与协议。
- 触发器。
- 权限与预算。
- 运行历史。
Run 详情展示总览、时间线、任务、审批、监督、指标、证据、成果以及实际读取的知识和记忆。
## 14. 数据模型建议
```ts
type AutomationPlan = {
id: string
projectId?: string
kind: 'scheduled_task' | 'heartbeat_review' | 'goal_loop'
name: string
description: string
status: 'draft' | 'active' | 'paused' | 'archived'
currentVersion: number
nextRunAt?: string
createdAt: string
updatedAt: string
}
type AutomationPlanVersion = {
planId: string
version: number
trigger: TriggerPolicy
objective?: AutomationObjective
protocol: ExecutionProtocol
budget: AutomationBudget
approvalPolicy: ApprovalPolicy
supervisorPolicy?: SupervisorPolicy
memoryBinding: MemoryBinding
}
```
状态、范围、下次运行、版本和索引字段使用显式列;版本化协议可以使用经过共享 Schema
验证的 JSON。
## 15. 安全要求
1. 所有输入由共享 Zod Schema 验证。
2. Main 重新验证项目、目录、Runtime、工具、知识库和记忆分区归属。
3. Renderer 不可直接声明 Run 完成或批准工具。
4. 自动化提示、事件、记忆和成果都视为不可信数据。
5. 事件过滤不执行用户 JavaScript、SQL 或无限复杂表达式。
6. Cron 有复杂度和最小间隔限制。
7. 自动化不能读取未绑定知识库、桌面上下文或其他项目记忆。
8. 日志和通知对私人内容、密钥和工具输出有界脱敏。
## 16. 实施顺序
1. 统一现有 Schedule 和 Heartbeat 的 Run 视图。
2. 增加幂等、租约、月度、工作日、受限 Cron、错过执行和未来运行预览。
3. 建立内部持久事件、过滤、冷却和去重,首期只支持 Ask。
4. 上线目标 Ask、有界循环、无进展检测和人工暂停。
5. 接入会话监督。
6. 再开放有人值守和预批准低风险 Execute。
## 17. 验收标准
- [ ] 支持单次、每日、每周、每月、工作日和受限 Cron。
- [ ] UI 显示计划时区和未来五次运行时间。
- [ ] 夏令时不会造成计划漂移或双跑。
- [ ] 同一计划同一时间点只产生一个 Run。
- [ ] 错过执行按配置跳过、补一次或有界补跑。
- [ ] 手动运行不改变下次计划时间。
- [ ] Ask 自动化在 Runtime 边界拒绝写工具和外部副作用。
- [ ] 目标任务必须有成功标准和停止条件。
- [ ] 每轮都有观察、行动、评估和预算记录。
- [ ] 连续无进展会暂停,不无限循环。
- [ ] 达到预算使用 `budget_exceeded`,不伪装为成功。
- [ ] 设置变化不影响已启动 Run。
- [ ] 重启后不自动重放结果未知的副作用步骤。
- [ ] 后台任务排队时不挤占前台模型请求。
+11
View File
@@ -0,0 +1,11 @@
# 电脑控制
电脑控制包含 GoodBuddy 对浏览器和客户端桌面能力的产品实现与平台适配状态。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [功能进度](./progress.md) | 当前已实现能力、缺陷修复、平台限制和验证状态 |
当前只有进度文档;产品需求、功能逻辑、UI 和技术设计尚未从历史实现记录中独立整理。
@@ -6,9 +6,9 @@
| --- | --- |
| 文档类型 | 实施进度 |
| 状态 | 持续更新 |
| 版本 | 0.1 |
| 日期 | 2026-08-07 |
| 适用能力 | 电脑控制与托管浏览器 |
| 版本 | 0.4 |
| 日期 | 2026-09-03 |
| 适用能力 | 内置浏览器与客户端电脑控制 |
## 范围
@@ -17,19 +17,20 @@ Linux x64 和 Linux arm64。
## 已完成
### 直连模型与浏览器
### Runtime 与浏览器
- 已实现宿主进程拥有的隔离浏览器服务。
- 使用 Electron 自带 Chromium 和 CDP,不下载额外浏览器。
- 已实现导航、可访问性快照、点击、输入、选择、返回和有界截图工具。
- 浏览器工具只在直连模型的 Execute 模式中提供;用户选择 Execute 即授权
本次交互运行,不再逐个弹出 GoodBuddy 工具审批。
- 浏览器工具作为 GoodBuddy 内置能力,可分别分配给直连模型、GoodBuddy 管理的
OpenCode 和 Continue;只在 Execute 模式中提供。用户选择 Execute 即授权本次
运行,不再逐个弹出 GoodBuddy 工具审批。
- 支持当前设备可连接的 HTTP、HTTPS、公网、内网、本机和元数据地址,
导航及重定向仍经过 DNS 解析和目标地址固定。
- 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、
空闲回收和应用退出清理。
- 直连模型工具循环已提高到适合浏览器任务的有界上限,并包含重复调用和
无进展保护。
- 直连模型工具循环不设置工具次数或模型轮次配额;取消、请求超时、工具结果大小和
累计输出边界继续防止失控执行。
### MCP
@@ -59,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 和会话。
## 本轮已修复缺陷
@@ -84,15 +92,28 @@ Linux x64 和 Linux arm64。
### P0:右侧没有浏览器实时画面,已修复
浏览器窗口默认使用 `show: false`,但 BrowserService 现在从模型实际操作的同一
会话捕获页面帧,并通过受限 IPC 发送状态、当前 URL 和约 220KB 的 JPEG 画面。右侧工作栏
新增“浏览器”页签;活动对话启动浏览器时会自动打开该页签,并显示创建中、
加载中、操作中、用户交互中、就绪、失败和已停止状态。用户可点击“交互”打开
同一会话的子窗口辅助 Agent;交互时主窗口暂时禁用,关闭时先刷新最终画面再将
浏览器窗口最小化,页面和会话继续保留。用户也可立即停止当前对话的浏览器会话。
早期版本通过同一浏览器会话的 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:百度输入时元素引用失效,已修复
@@ -133,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、技术设计和进度文档。
@@ -5,13 +5,14 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中,远期能力 |
| 版本 | 0.1 |
| 日期 | 2026-08-13 |
| 依赖 | [自动化平台总体设计](./automation-platform-architecture.md)、[并行实验 PRD](./parallel-experiments-prd.md)、[分区记忆 PRD](./partitioned-memory-prd.md) |
| 版本 | 0.3 |
| 日期 | 2026-08-19 |
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[并行实验 PRD](../parallel-experiments/prd.md)、[分区记忆 PRD](../memory/prd.md) |
## 1. 背景
智能心跳已经可以生成摘要、后续任务和记忆候选,但这还不是完整学习:
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。它与分区记忆的协同尚未
实施,也不承担持续学习、模式挖掘或自动改进:
- 候选是否改善未来行为没有评估。
- 一条反思是否会被检索和使用并不确定。
@@ -82,8 +83,8 @@ Observe
## 5. 候选来源
- 用户对回答、任务或 Supervisor 意见的显式反馈。
- 智能心跳提出的重复模式。
- 自动化 Run 的成功与失败比较。
- 用户对心跳报告或建议的显式反馈。
- Task 执行的成功与失败比较。
- 并行实验结论。
- 回放评估发现的稳定差异。
- 用户手动创建。
@@ -301,7 +302,8 @@ Shadow 达到配置的最小观察数且无安全退化后进入 `awaiting_appro
## 14. 信息架构
建议在自动化中心增加“学习”:
若远期验证确有集中学习管理需求,应提供独立且可审计的“学习”视图,而不是放入智能心跳、
任务中心或一个尚未确认的自动化中心:
1. **候选**:来源、作用域、预期收益和风险。
2. **评估中**:进度、案例和预算。
@@ -0,0 +1,29 @@
# 会话 HTML 渲染
会话 HTML 渲染让用户直接查看 Agent 回复中的网页结果,而不必先复制代码到外部文件。
该能力默认开启,也可以在“设置 > 平台功能 > 通用设置”中关闭。
## 功能边界
- 只渲染已完成的 Agent 正文和已完成的子 Agent 输出。
- 支持完整 HTML 文档、单一 HTML 根元素,以及 Markdown 中的 `html` / `htm` 代码块。
- 使用无脚本、无网络、无表单提交和无窗口操作权限的静态隔离预览。
- 每个预览提供源码切换和全屏预览两个图标操作;全屏预览沿用同一清理和隔离边界。
- 用户消息、推理过程和流式中的回复不执行 HTML;关闭功能后继续显示 HTML 源码。
- 本功能不把普通聊天回复复制为成果,也不替代助手工作栏中独立成果的预览流程。
## 文档
- [产品需求](./prd.md):功能范围、状态规则和验收标准。
- [技术设计](./technical-design.md):设置持久化、渲染边界与静态隔离方案。
- [实现进度](./progress.md):已验证实现和验证记录。
## 术语
- **HTML 源码**:Agent 回复中原始、可复制的 HTML 文本。
- **静态预览**:在隔离 `iframe` 中显示的 HTML;脚本、联网和外部操作均被禁用。
- **全屏预览**:在应用级 Modal 中显示同一份静态预览文档,用于查看完整页面布局。
全局聊天和设置界面规则由根目录的
[统一界面设计系统](../../../UI-DESIGN.md)定义。独立成果预览仍由
[助手工作栏](../assistant-workbar/README.md)负责。
@@ -0,0 +1,69 @@
# 会话 HTML 渲染产品需求
## 用户问题
Agent 可以生成网页、报告和交互原型的 HTML,但此前会话只能显示源码或忽略原始标签。
用户必须把内容复制到外部文件后才能查看结果,打断了会话内的阅读和迭代流程。
## 目标
- 让 Agent 生成的 HTML 在回复完成后直接显示为网页预览。
- 保留一键查看原始 HTML 的能力。
- 允许用户通过平台功能开关恢复为只看源码。
- 预览不取得 GoodBuddy Renderer 或外部网络能力。
## 非目标
- 不执行 Agent 提供的 JavaScript。
- 不提供联网网页、表单、弹窗或 Electron `webview`。
- 不在流式输出中反复加载不完整页面。
- 不自动把普通会话回复保存为成果。
## 功能需求
### FR-1 默认设置
“会话 HTML 渲染”是应用级持久设置,默认开启。用户可在“设置 > 平台功能 >
通用设置”中立即关闭或重新开启;保存失败时保留最后确认的状态并显示错误。
### FR-2 可识别输出
开启后,已完成 Agent 回复中的以下内容显示为静态预览:
1. 完整 HTML 文档。
2. 由单一 HTML 根元素构成的完整回复。
3. Markdown 中标记为 `html` 或 `htm` 的代码块。
普通 Markdown 继续按原有规则显示。一个回复中的多个 HTML 代码块各自保持原有位置。
### FR-3 回复状态和角色
- 已完成的主 Agent 正文和已完成的子 Agent 输出可以预览 HTML。
- 用户消息和 Agent 推理只显示文本或源码。
- 流式 Agent 正文在完成前显示源码,完成后原位切换为预览。
### FR-4 源码、全屏与关闭行为
每个预览持续显示“静态安全预览,脚本和网络已禁用”,并在标题右侧提供两个图标操作:
1. 源码按钮,就地展开或收起原始 HTML。
2. 全屏按钮,在应用级 Modal 中查看同一份预览文档。
嵌入预览使用固定高度并在框内滚动,需要更大画面时使用全屏预览。全屏预览可通过关闭
按钮、Escape 或点击遮罩退出,退出后焦点回到全屏按钮。关闭平台功能后,完整 HTML 回复
和 HTML 代码块继续显示源码,不能变成空白。
### FR-5 静态隔离
预览必须禁用脚本、网络请求、表单提交、嵌套页面、插件对象和窗口操作。远程链接和远程
媒体不得从预览加载;内联样式和有界的内联位图可用于还原静态视觉。
## 产品验收
- 新安装和从旧设置升级后,开关均为开启。
- 完整 HTML 和 fenced HTML 在已完成的 Agent 回复中显示预览。
- 关闭开关后,当前会话立即恢复显示源码,重启后选择仍保留。
- 流式回复、推理和用户消息从不执行 HTML。
- 预览可通过键盘访问源码和全屏按钮,并具有明确的 iframe 标题。
- 全屏预览隔离背景、初始聚焦关闭按钮,Escape 退出后焦点回到全屏按钮。
- 恶意脚本、事件属性、远程资源、表单和嵌套 frame 不进入可执行预览。
@@ -0,0 +1,39 @@
# 会话 HTML 渲染实现进度
## 已实现
- [x] 应用级持久开关,默认开启,并提供旧设置迁移。
- [x] 平台功能通用设置中的共享 Switch。
- [x] 已完成 Agent 回复和子 Agent 输出中的完整 HTML 与 fenced HTML 识别。
- [x] 隔离静态预览、持续安全说明和源码切换。
- [x] 预览保留作者 `<style>` 与 `style` 属性,样式不再被整块丢弃。
- [x] 与 Mermaid 一致的图标按钮组:源码切换与全屏预览 Modal。
- [x] 嵌入预览固定 480px 高度,源码面板同值上限。
- [x] 用户消息、推理、流式回复和关闭状态的源码边界。
- [x] 设置变更后当前会话立即重新渲染。
## 验证记录
2026-09-05:
- `npm run typecheck`:通过。
- 聚焦测试(应用设置、Markdown、会话时间线、设置中心、App 集成):306 项通过。
- 全量 `npm test`:`3493 passed, 55 skipped`。
- `npm run lint`:通过。
- `npm run build`:Main、Preload、Renderer 与控制面安装器均成功构建。
修复样式丢失后复验:
- 原实现只要 `<style>` 或 `style` 属性中出现 `@import` / `url()` 就整块删除,带渐变或
背景图的回复因此完全失去样式;现改为保留作者样式,外部资源加载仍由 iframe CSP 拒绝。
- `npx vitest run src/renderer/src/MarkdownRenderer.test.tsx`:`19 passed`。
- 聚焦测试(会话时间线、App 集成、设置中心):`260 passed`。
- `npm run typecheck`、`npm run lint`:通过。
新增全屏预览与图标化源码按钮后复验:
- 源码按钮改为代码图标并暴露 `aria-expanded`;新增放大图标打开全屏 Modal。
- 嵌入预览高度由 `min-height: 320px` 改为固定 `height: 480px`,源码面板上限同步为 480px。
- 新增回归测试覆盖全屏 Modal 的初始焦点、背景 inert、Sandbox/CSP 与 Escape 焦点恢复。
- 聚焦测试(Markdown、会话时间线、App 集成、设置中心):`280 passed`。
- `npm run typecheck`、`npm run lint`:通过。
@@ -0,0 +1,68 @@
# 会话 HTML 渲染技术设计
## 进程和设置边界
`conversationHtmlRenderingEnabled` 属于现有 `ApplicationSettings`。Main 继续通过
版本化 JSON 文件持久化应用设置,IPC 继续复用已验证的
`settings:application:get/update` 通道,Preload 只暴露类型化设置方法。
应用设置格式升级到版本 11。版本 10 及更早设置在读取时补入默认值 `true`,无需保留第二份
状态或独立迁移文件。
## Renderer 数据流
1. `App` 启动时读取应用设置,并把当前值传给每个保活的会话视图。
2. 平台功能 Switch 保存成功后立即更新 `App` 状态。
3. `ChatTimeline` 只为 `role === "assistant"` 且 `state === "complete"` 的正文传入
HTML 渲染权限;推理始终不传入该权限。
4. `MarkdownRenderer` 识别完整 HTML 回复以及 `html` / `htm` fenced code。
5. 关闭权限或回复仍在流式阶段时,HTML 保持代码文本。
## 静态预览
`StaticHtmlPreview` 不使用 `dangerouslySetInnerHTML`。它先用 DOMPurify 清理文档,再通过
`DOMParser` 移除事件属性、非锚点 `href` 和非内联图片的 `src` / `poster`,最后把结果写入
`<iframe sandbox="" srcDoc>`。
作者样式必须保留:`<style>` 元素和 `style` 属性原样传入预览,不按 `@import` 或 `url()`
关键字整块删除,否则任何带渐变或背景图的回复都会退化成无样式文档。外部样式表和远程
字体、图片的实际加载由 iframe 文档的 CSP 拒绝,不需要在清理阶段重复拦截。
iframe 文档注入严格 CSP:
- `default-src 'none'`
- `script-src 'none'`
- `connect-src 'none'`
- `frame-src 'none'`
- `object-src 'none'`
- `form-action 'none'`
- 只允许内联样式以及 `data:` 字体和受支持的内联位图
同时设置 `referrerPolicy="no-referrer"`。Sandbox 不授予 scripts、forms、popups、
same-origin 或 navigation 权限。预览只在回复完成后创建,并使用 lazy loading。
## UI 与无障碍
预览沿用聊天阅读宽度、语义颜色和共享 `icon-button`。`figure` / `figcaption` 表达预览及其
说明,iframe 具有本地化标题。标题右侧是一个 `role="group"` 图标按钮组,与 Mermaid 图表
操作保持一致:源码按钮暴露 `aria-expanded` 与 `aria-controls`,全屏按钮打开 Modal。
嵌入预览高度固定为 480px,超出部分在框内滚动;查看源码面板使用同一数值作为最大高度。
iframe 无法按内容自适应高度,因为 `sandbox=""` 不含 `allow-same-origin` 或 `allow-scripts`,
主文档既读不到内部文档尺寸,也不能接收上报;全屏预览覆盖需要更多空间的场景。
全屏预览复用 `dialog-focus` 的 `activateModalFocus` 与 `trapTabFocus`,遵循统一 Modal 的
背景 inert、初始焦点落在关闭按钮、Tab 循环、Escape 关闭和关闭后焦点恢复。它渲染的是与
嵌入预览完全相同的清理后文档,不额外放宽 Sandbox 或 CSP。切换回复时,源码面板与全屏
预览按当前 source 派生开合状态,不使用会引发级联渲染的重置 effect。
## 验证范围
- 应用设置默认值、关闭持久化和版本 10 迁移。
- fenced HTML、完整 HTML、关闭后的源码回退。
- 作者 `<style>` 与 `style` 属性在预览中保留。
- 脚本、远程资源、表单和事件属性清理,以及 iframe Sandbox/CSP。
- 源码按钮的 `aria-expanded` 切换,以及全屏预览的初始焦点、背景 inert、Escape 关闭和
焦点恢复。
- 主 Agent、子 Agent、用户、推理和流式状态边界。
- 从设置 Switch 到当前会话重新渲染的应用级集成路径。
@@ -0,0 +1,11 @@
# 会话监督
会话监督定义对长会话、工具执行和异常状态的观察、判断与介入边界。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [会话监督 PRD](./prd.md) | 监督目标、证据、介入方式和产品验收 |
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
@@ -5,9 +5,10 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-08-13 |
| 依赖 | [自动化平台总体设计](./automation-platform-architecture.md) |
| 版本 | 0.2 |
| 日期 | 2026-08-18 |
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md) |
| 界面归属 | [通用助手工作栏与执行空间](../assistant-workbar/prd.md) |
| 体验参考 | GoodBuddy 魔法笔记 AI 评论流 |
## 1. 背景
@@ -27,8 +28,10 @@ GoodBuddy 的魔法笔记已经提供一种有价值的交互:用户持续写
## 2. 产品定义
会话监督是在明确范围和策略下,对 Conversation、Task、AutomationRun 或 ExperimentRun
的可见事件进行独立观察,产生带证据的评论、告警和人工介入请求。
会话监督是在明确范围和策略下,对普通 Conversation、Task 或 Experiment 的可见事件
进行独立观察,产生带证据的评论、告警和人工介入请求。每个 Task 只关联一条 Conversation,
一条 Conversation 可以承载多个 Task;Job/Run 是内部执行和审计对象,不作为当前 UI
监督目标。
它不是:
@@ -53,7 +56,7 @@ GoodBuddy 的魔法笔记已经提供一种有价值的交互:用户持续写
## 4. 已确认的产品决策
1. 监督默认关闭,由用户对会话、任务、自动化或实验显式启用。
1. 监督默认关闭,由用户对 Conversation、Task 或实验显式启用。
2. 监督只读取用户可查看的消息、工具事件、状态、指标、成果摘要和目标。
3. 不读取、推断或保存模型隐藏推理链。
4. 每条重要判断必须引用具体消息、工具、步骤、指标或成果。
@@ -72,7 +75,7 @@ GoodBuddy 的魔法笔记已经提供一种有价值的交互:用户持续写
- 及时发现目标偏移、缺少证据、相互矛盾、重复循环和遗漏要求。
- 点击监督意见查看对应证据,而不是接受无来源判断。
- 对监督意见进行采纳、忽略、标记误报或追问。
- 对自动任务设置更严格的监督策略和人工检查点。
- 对 Scheduled/Goal Task 设置更严格的监督策略和人工检查点。
### 5.2 产品目标
@@ -97,10 +100,8 @@ GoodBuddy 的魔法笔记已经提供一种有价值的交互:用户持续写
| 对象 | 观察内容 | 典型用途 |
| --- | --- | --- |
| 普通会话 | 用户消息、助手回答、引用、工具事件 | 质量和证据评论 |
| 任务 | 目标、步骤、状态、工具、成果 | 偏离、循环和失败分析 |
| 自动化 Run | 触发、协议、预算、审批、指标 | 无人值守关注 |
| 实验 Run | 协议、变量、指标、证据 | 协议一致性 |
| 实验整体 | 各 Run 结算和比较 | 评估公平性与无结论提示 |
| Task | 目标、状态、Conversation、成果 | 偏离、循环和失败分析 |
| Experiment | 协议、变量、各候选执行、指标和证据 | 协议一致性、评估公平性与无结论提示 |
每个监督会话只能绑定一个主对象,并继承其项目范围。
@@ -156,7 +157,7 @@ type SupervisorAction =
- 当前对象的名称、目标和约束。
- 最近有界消息。
- 工具名称、状态、参数摘要和输出摘要。
- 任务和子任务状态。
- Task、Job 和 Subjob 状态。
- 成果标题、类型、大小和有界摘要。
- 引用和知识检索诊断。
- 预算使用。
@@ -224,7 +225,7 @@ type SupervisorDecision = {
- Ask 出现写工具请求。
- 工具或路径超出计划快照。
- 未经批准的跨项目或跨分区读取。
- Token、时间、工具、子任务和成果预算。
- Token、时间、工具、Job/Subjob 和成果预算。
- 幂等键冲突或结果未知。
- 输出 Schema 不匹配。
- 实验 Run 读取其他 Run 数据。
@@ -249,7 +250,11 @@ type SupervisorDecision = {
## 13. 用户交互
### 13.1 右侧评论流
### 13.1 工作栏监督栏目评论流
监督是助手工作栏中固定且始终可访问的栏目,不是只在聊天页面出现的附属面板。栏目默认
跟随当前会话,用户也可以固定到其他普通 Conversation、Task 或 Experiment。
切换页面不会改变固定目标;目标失效时必须显示修复状态,不能静默回到当前会话。
复用魔法笔记的体验方向:
@@ -272,7 +277,8 @@ type SupervisorDecision = {
### 13.2 会话输入区
提供监督状态入口:
会话输入区可以提供当前会话监督的快捷入口,但不是监督能力的唯一入口,也不控制工作栏中
已经固定到其他对象的监督目标:
```text
监督:关闭 / 综合 / 质疑 / 证据 / 目标 / 风险
@@ -311,7 +317,7 @@ open
Supervisor 建议“暂停”时:
1. 创建 `request_review`。
2. 在任务和会话界面显示原因和证据。
2. 在任务自身的会话界面显示原因和证据。
3. 用户选择继续、暂停、调整目标或取消。
4. 用户操作进入任务审计。
+12
View File
@@ -0,0 +1,12 @@
# DeepSeek Harness Runtime
本功能定义 DeepSeek Harness 作为 GoodBuddy 本机 Agent Runtime 的产品接入、控制面、
插件市场、权限和打包边界。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [技术设计](./technical-design.md) | Runtime 产品与技术方案、协议、插件、打包、测试和验收 |
现有文档同时包含部分 PRD、功能逻辑和进度内容,后续修改该 Runtime 时再按标准职责拆分。
@@ -0,0 +1,806 @@
# GoodBuddy 自维护 DeepSeek Harness Runtime 设计
## 1. 文档信息
| 项目 | 内容 |
| --- | --- |
| 文档状态 | 实现与发布验收基线 |
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
| Runtime 标识 | `deepseek-harness` |
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.2-rc.1` |
| 上游状态 | Developer Preview,允许出现破坏性变更 |
| 上游许可证 | MIT |
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
| 本文性质 | 设计与发布验收约定 |
本文定义 DeepSeek Harness 在 GoodBuddy 中的架构边界、协议、执行策略、插件市场、界面、打包和验收要求。实现必须继续遵守 GoodBuddy 已有的 Main 进程安全边界、Ask/Execute 语义、取消、超时、有界输出和资源回收约定。
## 2. 摘要
DeepSeek Harness 的底层库使用 Cordis 组合服务。GoodBuddy 不采用官方产品 profile,也不允许用户配置覆盖内部 Host 或控制服务;GoodBuddy 自行维护 Host、控制协议和生命周期,同时提供一个由 Main 管理、默认关闭的 npm 插件市场。用户显式开启后,市场只搜索带精确 `dsh-plugin` 关键字的公共 npm 包,不代表 GoodBuddy 审核、推荐或承诺兼容这些包。
用户明确安装并启用的插件以当前用户权限运行。Ask/Execute 只控制模型经过 `tools/execute` 发起的工具调用:Ask 只允许 Host 中真实注册的 `read`、`skill` 和 Main 管理的 Web Search/Fetch 代理,Execute 放行 Host 中全部已注册工具。插件不能用同名工具冒充 Ask 允许项。插件安装脚本和初始化代码不属于模型工具调用,不能由 Ask 限制,因此界面在安装前必须明确确认这一边界。
整体分成两个互相约束的部分:
1. **GoodBuddy Main Control Plane**
- 运行在 Electron Main 进程。
- 持有加密设置、模型连接选择、Ask 只读策略、Runtime 生命周期、插件市场状态和审计归属。
- 通过 Electron `utilityProcess` 启动受控 Harness 子进程。
- 对环境、输入、输出、超时、取消和进程树执行强制限制,并只把已启用插件的受管入口传给 Host。
2. **GoodBuddy Harness Control Plane**
- 运行在 Harness 子进程内,是 Host 私有的内部控制组件,不导出 Cordis 插件入口。
- 使用 ACP 兼容的 JSON-RPC stdio 作为基础控制面。
- 增加 GoodBuddy 所需的能力握手、每轮权限准备、会话释放、工具事件、推理、用量和安全凭据请求扩展。
- 与 GoodBuddy Host 一起维护、构建和发布,不设计为独立 npm 包或市场插件;第三方插件只作为显式配置加载。
DeepSeek Harness 不替换 OpenCode、Continue 或直连模型 Runtime。用户可以按全局、项目、会话或消息通道继续选择现有 Runtime。
## 3. 背景与上游能力
### 3.1 已确认的官方能力
- `@deepseek-ai/dsh` 是官方 profile 启动器。
- Harness 插件是导出 `apply(ctx, config)` 的 Cordis 模块。
- npm 包可通过 `dsh.bundle` 声明配置补丁,再通过 `dsh plugin --profile <name> add <package>` 安装。
- ACP 支持:
- 初始化。
- 创建多个会话。
- 发送 Prompt。
- 按会话取消。
- 一次性权限选择。
- 已提交的助手文本。
- 官方本地沙箱支持:
- Linux:Bubblewrap,或 Landlock 降级。
- macOS:Seatbelt。
- Windows:ACL 受限令牌,官方明确标记为部分强制执行。
GoodBuddy 不组合上述 Runtime OS 沙箱。当前产品选择 DSH 本地 Shell 与
Filesystem Provider,以 GoodBuddy 客户端进程的当前用户权限运行工具。
### 3.2 官方通道的缺口
官方 ACP 插件有意只输出已提交文本,不输出推理、工具进度、计划、标题和用量。它也没有标准的会话关闭方法。SDK JSON-RPC 的展示事件更完整,但缺少 GoodBuddy 需要的单轮取消和权限回传。
因此,首版不单独选用其中一个官方通道作为完整实现。GoodBuddy Harness Control Plane 以 ACP 语义为基础,补充有命名空间的扩展方法和事件。
### 3.3 自维护边界
GoodBuddy 自己的 Runtime 和控制面不包装成标准 DSH 插件,也不加载用户 profile 或自定义 Host。只有 GoodBuddy Main 可以启动内部 Host、选择受管插件入口并处理启动失败。公共 npm 市场是第三方扩展来源,不改变 GoodBuddy 对内部 Host、Ask/Execute 语义和协议版本的控制。
## 4. 目标与非目标
### 4.1 首版目标
- 增加 `deepseek-harness` Runtime,并在设置、聊天和消息通道中可选择。
- 使用 GoodBuddy 管理的模型连接,不在 Renderer 或持久化 Harness 配置中写入 API Key。
- 当所选模型连接明确声明支持图像输入时,允许向 DeepSeek Harness 发送有界的 JPEG/PNG;文本模型在启动 Host 或调用模型前拒绝图片。
- Ask 模式在 Runtime 工具分发边界强制只读,阻止 Shell、写入和编辑工具。
- 在 Web Search 能力启用时,通过 Main 代理向 Ask 与 Execute 提供有界的 `web_search` 和 `web_fetch`,Harness Utility 不持有服务凭据。
- Execute 模式使用 DSH 本地 Provider,以当前用户权限执行文件与命令工具;工作区是默认工作目录,不是 OS 权限边界。
- 提供默认关闭的公共 npm DSH 插件市场总开关;用户显式开启后可搜索、查看详情、精确版本安装、启用、停用、配置和移除,首次安装前明确确认当前用户权限。
- 只加载 Main 明确传入的已启用插件;单个插件启动失败不得阻止 Host,并自动停用失败插件。
- 允许 Skills 和自定义 MCP 显式分配给 DeepSeek Harness;自定义 MCP 只在 Execute 中通过 Main 代理。
- 设置页可读取有界的 Host/插件原生 Tool 与 Skill 清单;Tool 元数据显示类型、来源及 Ask/Execute 可用性,并明确排除 GoodBuddy 分配的 Skills、Web/MCP 请求代理。
- 支持多会话、同会话串行、跨会话并行。
- 支持按请求取消、超时、会话释放和应用退出时完整回收。
- 输出文本、推理、工具参数、工具结果、stderr 和协议队列全部有界。
- 使用真实 OpenAI 兼容 Chat Completions 模型验证调用,而不在日志、测试产物或提交中暴露凭据。
- 保留 Windows、macOS、Linux 的 x64 和 arm64 发布能力。
### 4.2 首版非目标
- 不替换 OpenCode、Continue 或直连模型 Runtime。
- 不开放用户 Cordis profile、cordis.patch.yml 或 $DSH_HOME 全局补丁覆盖。
- 不提供外部 Host、自定义 Harness Control Plane、任意本地模块路径或用户 profile 插件目录。
- 不加载 Harness Web UI、HMR、遥测、自动更新或目录选择器。
- 不提供 Runtime OS 沙箱模式或相关持久设置。
- 不向 Utility 暴露 MCP 凭据或建立直连 MCP Client。只有用户明确分配给 Harness 的 MCP 工具可以通过 Main 代理调用。
- 不在首版向 Harness 暴露 GoodBuddy 浏览器控制、知识库或 Magic Notes。
- 不在首版支持会话恢复、Harness Subagent、后台 Job、Hook、浏览器控制或 Workflow;Web Search/Fetch 只通过 Main 代理提供,不加载 Harness 自有网页服务。上述长生命周期能力未来统一进入右侧 Runtime 监督栏,不进入 Composer 工具栏。
- 不发布独立 npm 包,也不创建上游 PR。
- 不为第三方插件增加权限矩阵、风险等级、逐工具审批、沙箱档位、回滚代际或兼容性背书。
## 5. 核心设计决策
### 5.1 第三个独立 Runtime
`deepseek-harness` 是明确的 Runtime 类型,不伪装成 `model`、`opencode` 或 `continue`。共享契约、设置迁移、Runtime 选择、检测、聊天标签、消息通道和模型用量都使用同一个稳定标识。
### 5.2 受控组合,不启动用户 profile
GoodBuddy 使用自己固定的 Harness Host 入口和只读组合模板,不调用 `dsh web`,也不启动用户已有 profile。运行时禁止以下来源参与组合:
- 当前工作目录的 `.env`。
- 用户 Harness Home 的 `.env`。
- `$DSH_HOME/cordis.patch.yml`。
- 用户 profile 的 `cordis.patch.yml`。
- 任意 `--patch`。
- HMR 和 profile 驱动的动态插件安装。
模型名称、服务地址、工作区、Skills、MCP schema 和已启用插件的规范化入口通过严格校验的 Main 配置传给 Host。API Key 只通过受控凭据通道按需提供,不写入 YAML、命令行、Renderer 或日志。插件配置只来自 GoodBuddy 受管状态,不合并用户 profile 或全局补丁。
### 5.3 双层内部控制面
Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不能代替进程内的 Session/Tool 适配层:
- Harness Control Plane 最接近 Session、Agent、Tool 和 Usage seam,适合做内部协议转换与 Ask 工具拦截。
- Main 控制面是可信安全边界,适合持有模式授权策略、加密设置、进程控制和 IPC。
任何一侧缺失能力握手时,Runtime 必须报告不可用,不能降级为不受控执行。
### 5.4 GoodBuddy 继续拥有持久会话
首版不启用 Harness JSONL 会话持久化和 SQLite 会话索引。原因如下:
- GoodBuddy 已经持久化对话、消息、活动、工具事件和用量。
- 再写一份 Harness 日志会扩大敏感数据副本和清理范围。
- GoodBuddy 在 Runtime 重启后可以用现有会话历史创建新 Harness Session。
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
## 6. 总体架构
```text
Renderer
│ 显式、经 schema 验证的 preload API
▼
Electron Main
├─ RuntimeSettingsStore
├─ RuntimeExtensionStore / npm Marketplace
├─ AgentRuntimeController
├─ RuntimeAuthorizer(Ask 拒绝 / Main 代理工具授权)
└─ DeepSeekHarnessRuntime / Main Control Plane
│ ACP + goodbuddy/* 扩展,stdin/stdout
▼
Electron utilityProcess
└─ GoodBuddy Harness Host
├─ 固定 Cordis 组合
├─ GoodBuddy Harness Control Plane(内部组件)
├─ DSH Agent 与 LLM seam
├─ 按模型能力挂载的有界内存图片存储
├─ 本地 Shell / Filesystem Provider
├─ 最小工具集与 Main 代理 MCP
└─ Main 明确启用的第三方 Cordis 插件
│ HTTP/HTTPS
▼
用户选择的 OpenAI 兼容模型连接
```
### 6.1 信任边界
| 区域 | 信任级别 | 允许持有的内容 |
| --- | --- | --- |
| Renderer | 不可信展示层 | 脱敏设置、状态、用户可见事件 |
| Preload | 窄桥 | 明确方法和共享 schema |
| Electron Main | 可信控制面 | 加密设置、模式授权策略、Runtime 生命周期 |
| npm 安装子进程 | 第三方执行面 | 受管暂存目录、去除模型凭据的有界环境和当前用户权限 |
| Harness utilityProcess | 不可信执行面 | 当前请求、临时凭据、受控工具、第三方插件代码和当前用户权限 |
| Harness 工具子进程 | 最低信任 | 单次命令所需的最小环境和当前用户权限 |
Harness 子进程崩溃、输出异常、拒绝协议或加载错误时,Main 必须失败关闭。
## 7. GoodBuddy Harness Control Plane
### 7.1 内部组件职责
控制面负责:
- 启动 ACP 兼容的 JSON-RPC stdio 服务。
- 创建、查找和释放 Harness Agent。
- 在 Prompt 前应用 GoodBuddy 指定的 Ask/Execute 权限。
- 将 DSH Session 事件转换为有界的 GoodBuddy 事件。
- 将 LLM 用量转换为稳定的模型用量事件。
- 根据 Main 传入的模型能力声明 ACP 图片能力,验证内联图片并转换为 DSH 的不可变 Attachment 引用。
- 在 dispose 时先取消 Agent,再等待子 Agent 和工具清理。
- 保证 stdout 只包含协议帧,诊断只写 stderr。
控制面不负责:
- 保存 GoodBuddy 设置。
- 持久保存 API Key。
- 决定 Main 的模式授权结果。
- 直接访问 Renderer 或 Electron API。
- 接受任意路径、外部 Host 或 profile 覆盖;插件入口只能来自 Main 的受管清单。
- 自行上传遥测。
### 7.2 第三方插件加载
GoodBuddy 控制面自身不导出 `apply(ctx, config)`,也不提供默认 stdin/stdout 入口或可安装 manifest。第三方插件由 Main 在启动配置中逐项指定:
- Main 只传递受管 Store 中已启用插件的稳定 ID、规范化入口文件和 JSON 配置。
- Launcher 与 Host 对消息结构和绝对入口路径执行严格校验;Host 解析真实路径并要求入口是普通文件。
- Host 动态加载 Cordis 插件并等待激活,每个插件有独立的 5 秒激活超时,完整插件序列最多占用 90 秒。
- Main 的 Host 启动预算使用 10 秒基础预算,加上每个已启用插件 5 秒激活与最多 1 秒失败清理、且整个插件序列最多占用 91 秒,再预留 2 秒保存失败插件状态;显式测试超时仍作为调用方指定的硬上限。
- 插件按清单依次加载;导入、导出形态或激活失败只记录该插件,不阻止其他插件和 Host 启动。失败 Fiber 的清理同样有界。
- 有限但超过预算的同步导入或同步 `apply` 在返回后按超时失败并继续加载后续插件;JavaScript 不能在同一事件循环内抢占永不返回的同步第三方代码,此时由 Main 的独立启动截止时间终止整个 Utility。
- 失败 ID 在 ready 握手中返回 Main;Main 原子写入停用状态和启动错误。
- 插件成功激活后可注册工具或后台生命周期逻辑。Ask 只能拦截模型工具调用,不能撤销初始化阶段已经发生的副作用。
GoodBuddy 不扫描任意目录、不读取用户 profile 插件清单,也不接受 Renderer 直接提供文件路径。
插件安装、升级和移除在目录重命名前写入受管变更日志。Main 下次初始化时以持久
Store 是否已经提交为准,确定性完成新目录或恢复旧目录,并在处理前重新验证受管
目录、入口真实路径、符号链接和根目录包含关系。旧版 `store.json` 继续原地迁移,
不要求用户重新安装插件。
## 8. 协议设计
### 8.1 传输
- stdin/stdout 使用换行分隔 JSON-RPC。
- stdout 不得出现日志、Banner、进度条或调试输出。
- stderr 只允许有界诊断,不得包含 Prompt、工具完整输出或凭据。
- 每一帧、每一字段和每个请求累计输出都必须在解析前或接收时限流。
- 图片只允许作为 ACP 内联 base64 内容传入;拒绝远程 URI,Host 不替用户获取图片 URL。
### 8.2 标准 ACP 方法
首版保留 ACP 的初始化、`session/new`、`session/prompt` 和 `session/cancel` 语义。`promptCapabilities.image` 必须与所选模型连接的 `supportsImageInput` 完全一致,不能仅根据 Provider 或模型名称猜测。标准 ACP 客户端可以使用只读默认行为,但只有完成 GoodBuddy 能力握手的客户端才能启用 Execute。
### 8.3 GoodBuddy 扩展
扩展统一使用 `goodbuddy/` 命名空间:
| 方法或事件 | 方向 | 用途 |
| --- | --- | --- |
| `goodbuddy/handshake` | Main → Control Plane | 交换控制协议、Harness、ACP 版本和能力 |
| `goodbuddy/session/prepare` | Main → Control Plane | 在下一次 Prompt 前设置工作模式和请求标识 |
| `goodbuddy/session/release` | Main → Control Plane | 取消并释放指定 Session |
| `goodbuddy/session/event` | Control Plane → Main | 文本、推理、工具、状态和用量事件 |
| `goodbuddy/credential/resolve` | Control Plane → Main | 按已登记引用请求当前 Runtime 的临时凭据 |
| `goodbuddy/tools/list` | Control Plane → Main | 取得 Main 管理的有界 Web 工具与当前 Execute 请求的 MCP 工具 schema |
| `goodbuddy/tools/call` | Control Plane → Main | 校验活动请求、工作模式、参数和精确注册代理身份后调用 Main Web/MCP 工具 |
| `goodbuddy/native/snapshot` | Main → Control Plane | 从无 Agent scope 的 Host Registry 读取有界的原生 Tool/Skill 元数据,排除 GoodBuddy 分配项与请求级代理 |
| `goodbuddy/shutdown` | Main → Control Plane | 停止接收新请求并有序清理 |
Utility 启动控制协议使用版本 3,严格携带 `supportsImageInput`、模型连接的有界自定义
Header 与固定 8 MiB 帧上限;旧版本或缺少必需字段的启动消息失败关闭,不能让 Host
自行猜测模型能力。扩展版本独立于 ACP 版本。握手响应至少包含:
```ts
type GoodBuddyHarnessCapabilities = {
controlProtocolVersion: 1
harnessVersion: string
acpProtocolVersion: number
supports: {
cancellation: true
sessionRelease: true
reasoningEvents: boolean
toolEvents: boolean
usageEvents: boolean
credentialResolution: true
}
execution: {
mode: 'host'
}
}
```
版本不兼容、必需能力缺失或 `execution.mode` 不是 `host` 时,Main 不得开始模型请求。
### 8.4 每轮权限准备
GoodBuddy 的工作模式属于每个请求,不属于 Runtime 进程全局状态。同一对话可以在 Ask 和 Execute 之间切换。因此:
1. `session/new` 后默认是 Ask。
2. 每个 Prompt 前,Main 发送一次 `goodbuddy/session/prepare`。
3. Harness Control Plane 将准备状态绑定到 `sessionId + requestId`。
4. `session/prompt` 只能消费匹配且尚未使用的准备状态。
5. 缺少准备状态、重复使用、请求标识不匹配时,Control Plane 直接拒绝请求。
6. 同一 Session 只允许一个 Prompt 在途。
若 Runtime 回传 ACP 权限请求,Main 只按活跃 Session 的当前模式答复:Execute 优先选择
`allow_once`,缺少时使用提供的 `allow_always`,不调用第二次人工 authorizer;
Ask、已关闭或未知 Session 拒绝。这里不写入永久授权规则,也不把业务问答当作权限确认。
### 8.5 事件模型
Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
- `status`:简短运行状态。
- `text`:已提交的助手文本分片。
- `reasoning`:可选的有界推理摘要分片。
- `tool`:工具 ID、名称、状态和有界输入/输出摘要。
- `model-usage`:模型、提供方、输入、输出和缓存 Token。
- `done`:停止原因和 Session ID。
禁止发送原始 Cordis Context、完整环境、内部对象、堆栈中的凭据或无界 Session 日志。
## 9. Runtime 生命周期
### 9.1 进程模型
- 每个活动的 DeepSeek Harness Runtime 实例拥有一个 `utilityProcess`。
- 一个进程可以承载多个 Harness Session。
- 同一 GoodBuddy 对话的 Prompt 串行执行。
- 不同对话可以并行,但受全局并发上限控制。
- Runtime 设置变化时创建新实例,旧实例等待在途请求结束或在宽限期后被取消。
### 9.2 会话映射
Main 保存内存映射:
```text
GoodBuddy conversationId -> Harness sessionId + process generation
```
- 首次请求创建 Session。
- 已有 Session 只发送当前 Prompt。
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的会话历史。
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
### 9.3 取消与有界控制操作
- 用户取消时立即发送 `session/cancel`。
- 取消等待有界,超时后关闭连接并终止整个 Harness 进程。
- 初始化、握手、请求准备、权限回传和关闭分别使用独立控制超时。
- 生产 Prompt 不设置固定墙钟总时限,由 Harness 自身协议终态或用户取消结束。测试可以
显式注入短 Prompt 时限验证取消传播,超时错误与用户取消仍不能被宽泛 catch 抹平。
- 取消后仍可接收并丢弃该请求的最终协议结算帧,但不得写入下一请求。
### 9.4 释放与退出
- 原生能力清单由 `SelectedRuntimeManager` 缓存的执行 Runtime 提供,与随后的对话请求共用同一实例,不再为清单单独启动并销毁一次 Host;该实例遵循执行缓存的空闲淘汰、设置变更 reset 和退出 dispose 规则。
- 删除或释放对话时调用 `goodbuddy/session/release`。
- Runtime dispose 时先拒绝新请求,再取消所有 Session。
- Harness Control Plane 完成 Agent、工具和会话清理,Host 完成 Cordis Fiber 与子进程的反向清理。
- Main 在宽限期内等待正常退出。
- 超时后终止 utilityProcess,并在平台允许时清理完整进程树。
- 应用退出时中止正在运行的 npm 插件安装并终止其完整进程树,不能让 lifecycle script 在 GoodBuddy 退出后继续运行。
- 应用退出不得因 Harness 清理无限阻塞。
## 10. 权限与主机执行
### 10.1 模式映射
| GoodBuddy 模式 | Host 内置与插件工具 | Main Web Search/Fetch | GoodBuddy 自定义 MCP | 行为 |
| --- | --- | --- | --- | --- |
| Ask | 只允许 Host Registry 中真实注册的 `read` 与 `skill`;其他 Host/插件工具一律拒绝 | 能力启用时注册 Main 的精确代理对象,无逐次审批 | 不注册 | 模型工具调用保持只读 |
| Execute | 放行 Host 中全部已注册的内置与插件工具 | 能力启用时注册 Main 代理 | 按分配注册并经过既有 RuntimeAuthorizer | 不增加插件权限层,以当前用户权限运行 |
### 10.2 Ask 模式
- Harness Control Plane 在 `tools/execute` 分发边界识别当前 Session 和在途请求。
- 采用所有权感知的只读允许列表,只接受 Registry 中真实的 `read`、`skill` 和 Main 注册的 Web 代理对象;`write`、`edit`、Shell、MCP 及任意新插件工具默认拒绝。只比较工具名不足以授权,插件注册同名工具仍会被拒绝。
- Ask 不注册 Main 代理的 MCP 工具。
- Web Search/Fetch 的凭据、传输与结果限制保留在 Main;Utility 只看到有界 schema 和结果。
- 只读不等于无限输出,读取仍受字节和工具结果上限控制。
- 插件安装脚本和 Cordis 初始化生命周期不经过 `tools/execute`。Ask 不能把已启用第三方代码变成沙箱,也不能保证第三方代码没有启动副作用。
### 10.3 Execute 模式
- 工作区来自 Session 创建时的规范化绝对路径,并作为文件与命令工具的默认工作目录。
- DSH 本地 Filesystem、Bash 或 PowerShell Provider 直接使用 GoodBuddy 客户端当前用户的 OS 权限。
- 工作区不是 containment 边界;绝对路径和命令可访问当前用户本来有权访问的主机资源。
- 已启用插件注册的工具与内置工具使用同一分发路径;GoodBuddy 不增加插件权限矩阵或逐工具确认。
- Main 代理的 MCP 工具继续执行分配、schema、活动请求、模式和 RuntimeAuthorizer 校验。
`goodbuddy/tools/list` 按 Session 保存本次发现的工具定义;`goodbuddy/tools/call` 复用该定义
校验参数与授权,不因调用健康工具而重新等待故障 MCP 的发现。发现重试留在清单刷新,
Session 释放及 Runtime dispose 清除内存定义,不持久化第二份能力状态。
- 所有工具调用仍作为活动事件记录;Ask 和 delegation 路径继续固定拒绝。
### 10.4 Runtime OS 沙箱
- GoodBuddy 不加载 DSH 平台 Sandbox Provider,也不执行启动沙箱探测。
- “安全与数据”不提供 Runtime OS 沙箱开关。
- 握手明确报告 `execution.mode = 'host'`,状态文案明确说明工具使用当前用户权限。
- Electron Renderer、Preload、Browser Session 等应用安全沙箱不在本设计变更范围内。
### 10.5 环境与凭据
- 使用环境变量白名单构造 utilityProcess 环境。
- 不继承 `NODE_OPTIONS`、调试端口、任意 npm 配置、用户 `DSH_*` 覆盖或白名单之外的凭据。
- `DSH_TELEMETRY_DISABLED=1` 必须固定设置。
- Harness Home 指向 GoodBuddy 管理的隔离目录。
- 不调用官方 `loadEnv` 或 `loadLayeredEnv`。
- API Key 由 Main 从加密设置中解析。
- Harness Control Plane 只能用已握手登记的引用通过 `goodbuddy/credential/resolve` 请求当前 Runtime 的凭据。
- 凭据只在模型请求所需的子进程内存中短暂存在,不写磁盘、不进入工具环境、不打印。
- npm 安装使用同一环境白名单并移除模型 Provider 凭据;安装脚本仍拥有当前用户的文件、进程和网络权限。
## 11. 受控 Harness 组合
首版只加载完成文本对话、受控代码操作和用户明确分配能力所需的固定服务:
- Agent、Session、LLM 和 Tool Registry 基础服务。
- GoodBuddy Harness Control Plane。
- OpenAI 兼容 Chat Completions LLM 适配器。
- 仅在所选模型声明图片能力时挂载的进程内 Attachment Store;它完整解码图片、校验格式/尺寸/摘要,以内容寻址引用保存,并随 Session 或 Host 释放。
- DSH 本地 Subprocess、Filesystem 和平台 Shell Provider。
- Token Meter 和必要的上下文压缩。
- 有界的读取、写入、编辑和 Shell 工具。
- Agent scope 的 Skill Registry 与 `skill` 工具。Skill 目录由 Main 选择并在 Launcher 和 Host 两次规范化、校验。
- Main 代理的 Web 与 MCP schema 工具。Utility 不持有 Web/MCP URL、凭据或 Transport。
- Main 明确传入的第三方 Cordis 插件及其 JSON 配置。
首版明确不加载:
- Web UI、HMR、Host API 和目录选择器。
- Harness 遥测。
- Settings File 和 Local Credentials。
- 用户 profile 与全局补丁。
- Harness 自有 Web Search/Fetch、Utility 直连 Web/MCP、Hooks。
- Subagent、Workflow、Ralph、后台 Job。
- JSONL Session Persistence 和 SQLite Session Query。
- 自动技能发现、任意目录扫描和 profile 市场状态。
如果某个首版工具依赖被排除服务,启动审计必须失败,而不是自动加载更大的默认 bundle。
## 12. 模型配置
### 12.1 配置来源
DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
- 协议必须是 `openai-chat-completions`。
- 认证必须是 API Key。
- 服务地址支持 HTTP 和 HTTPS,不根据主机位置、端口、凭据、查询参数或片段增加额外限制。
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
- 模型名称和服务地址由 Main 传入受控 Host。
- 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。
- 模型连接的自定义 Header 通过 Utility 启动配置交给 Pi-AI Provider;Pi-AI 没有受支持的
任意 Body 配置入口,因此 DeepSeek Harness 不接收自定义 Body。完整支持矩阵和合并规则
见[模型连接请求定制](../model-connections/technical-design.md)。
- API Key 继续保存在 GoodBuddy 加密设置中。
- `platform` 在设置页显示为“管理员环境优先,回退 GoodBuddy 兼容连接”,不是 Harness
自有配置文件。Main 优先读取完整的 `GOODBUDDY_MODEL_BASE_URL`、
`GOODBUDDY_MODEL_NAME` 和 `GOODBUDDY_MODEL_API_KEY` 环境预置;不完整时使用
当前默认兼容 GoodBuddy 连接,再回退首个兼容连接。
- `default` 只跟随 GoodBuddy 默认兼容连接或首个兼容连接,跳过独立管理员预置;
`profile` 使用指定的兼容连接。没有兼容连接时,仍允许选择 `platform`,因为完整
管理员预置不依赖已保存连接的协议;运行时缺少实际连接才报告不可用。
- Renderer 通过 Main 的只读平台来源投影展示实际管理员模型或兼容回退,不读取环境
或密钥,也不根据 `credentialSource` 猜测。投影直接复用本节所述的生产 resolver,
字段、非持久化与草稿展示边界见
[Harness 平台来源公开投影](../model-connections/technical-design.md#9-harness-平台来源公开投影)。
不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。
### 12.2 设置变化
模型、凭据、Skill、MCP 分配或插件安装、启停、配置、移除变化时,GoodBuddy 创建新 Runtime 实例。Harness Host 路径始终由当前 GoodBuddy 构建提供,不能由设置或环境变量替换。旧实例按现有 Runtime Controller 语义退役,不在一个活动进程内热替换配置。
### 12.3 输入限制
- 文本始终可用;图片是否可用完全取决于所选模型连接是否显式声明 `supportsImageInput: true`。
- 文本模型收到图片时必须在启动 Host 或发起模型网络调用前返回明确错误,不能静默丢弃图片。
- 图片模型只接受内联 JPEG/PNG,不接受 URL、文件路径、ACP `uri` 或其他媒体类型。
- Main 已通过 `nativeImage` 解码用户选择的图片并生成有界模型输入;Utility 仍须独立执行严格 base64、签名、容器结构、CRC(PNG)、完整解码、尺寸和摘要校验,不能把 Main 校验当作跨进程信任替代。
- 每条消息最多 8 张图,单图编码后最多 1 MiB,图片合计最多 2 MiB,单边最长 8,192 px,单图最多 1,600 万像素,累计解码像素最多 3,200 万(重复引用也计入预算)。进程内 Store 另设 32 MiB、256 个唯一对象的总上限。
- Attachment Store 只服务当前非持久 Harness Session;引用按 Session 释放,Host 退出时清空,不写入磁盘或 GoodBuddy 第二份会话日志。
- GoodBuddy 历史、Prompt、系统指令分别保持不同信任层。
- 任何用户文本都不能进入 Cordis 配置表达式或模块名。
## 13. 输出和资源边界
建议首版默认限制:
| 项目 | 默认上限 |
| --- | --- |
| 单个 JSON-RPC 帧 | 8 MiB |
| 单图 / 单条消息图片 | 1 MiB / 8 张且合计 2 MiB |
| 单图尺寸 / 单条消息解码像素 | 单边最长 8,192 px 且 1,600 万像素 / 3,200 万像素 |
| Host 临时图片存储 | 32 MiB 且最多 256 个唯一对象 |
| 单个文本或推理事件 | 64 KiB |
| 单次请求累计协议输出 | 无固定总量上限,继续校验单帧和待处理队列 |
| 单轮模型输出 Token | 由 Harness SDK 与 Provider 决定,GoodBuddy 不固定覆盖为 16K |
| 工具输入摘要 | 4,000 字符 |
| 工具输出摘要 | 4,000 字符 |
| 待处理事件数 | 1,000 |
| stderr 累计 | 64 KiB |
| Host 启动 | 10 秒基础预算 + 每插件 5 秒激活与最多 1 秒失败清理,插件序列最多 91 秒;Main 另预留 2 秒持久化失败状态 |
| ACP 初始化与内部握手 | 每阶段 10 秒 |
| 单次 Prompt | 无固定墙钟总时限,由 Harness 终态或用户取消结束 |
| 有序关闭宽限期 | 2 秒 |
超过限制时应取消当前请求。协议帧、队列或 stderr 持续异常时,应终止 Runtime 进程,避免继续信任已失控的通道。
## 14. Runtime 检测与状态
### 14.1 检测
检测只验证:
- 内置 Host 路径是规范化文件。
- 版本可读取且在支持范围内。
- 内部控制面能力握手成功。
检测不得调用付费模型,也不得读取或输出 API Key。真实模型测试是单独的显式操作。
### 14.2 设置界面
Agent Runtime 使用共享 `SegmentedControl` 展示 OpenCode、Continue 和 DeepSeek Harness。DeepSeek Harness 必须标记为“开发者预览”,并说明上游 RC 可能发生破坏性变更。
Runtime 的概览、模型配置和检测信息放在同一张详情卡中。当前单独显示的一行“已就绪”应移入卡片,与路径、版本号归为同一组:
```text
Runtime: GoodBuddy 内置 DeepSeek Harness
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
状态: 已就绪
路径: <受控 Host 路径>
版本: 0.1.2-rc.1
执行权限: 当前用户权限
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
```
界面要求:
- 不再在卡片外重复一行检测结果。
- 使用语义化键值结构,路径允许换行,不截断关键信息。
- 状态不能只依靠绿色表达,必须同时有文字。
- 检测中和不可用分别显示明确文案。
- 高级设置默认收起。
- DSH 插件市场提供共享 Switch 样式的总开关并默认关闭。关闭时不请求公共 npm 目录且隐藏市场管理界面,但不修改已有插件的逐项启停状态;因此已启用插件继续随 Host 加载,重新开启后恢复原有管理状态。
- 同一 Runtime 页面提供紧凑的 DSH 插件市场:客户端筛选名称、包名、描述和许可证,已安装插件优先显示。
- 安装前使用一个明确 Checkbox 确认 npm 安装脚本、插件初始化和 Execute 工具均使用当前用户权限;不展示权限矩阵或逐工具审批。
- 已安装插件使用共享 Switch 启停,并提供 JSON 配置、明确移除确认和启动失败信息。
- npm 目录离线时仍显示并允许管理已安装插件;目录错误就地显示并可重试。
- 安装、启停、配置和移除的短期结果通过应用通知显示,不重复保留页内成功提示。
- DSH 不提供独立的“允许图片”开关。Runtime 连接选择只引用“模型连接”中维护的图片能力声明,避免同一模型出现两份冲突配置。
聊天顶栏只显示简短 Runtime 状态,不显示文件路径和版本。完整诊断只在设置页展示。
### 14.3 Agent Runtime 交互表面归属
OpenCode、Continue 和 DeepSeek Harness 的后续能力按操作生命周期放置,不按上游产品分别堆叠入口:
| 表面 | 负责内容 | 不负责内容 |
| --- | --- | --- |
| Composer 通用行 | 附件、语音、知识范围、专家、Ask/Execute、Runtime 和发送 | Session 监督、后台进度、历史任务管理 |
| Composer Runtime 专属行 | 仅对当前消息生效且需要高频选择的 Agent、预设、Prompt/Command 快捷操作 | Task 级委派、后台执行、Workflow/Hook 生命周期 |
| 助手工作栏固定“Runtime”栏目 | 用户所选 Conversation 或 Task 的 Runtime 状态、Task 级委派与取消、后台执行进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止和会话监督;不显示 Job/Run 树 | 持久模型、程序路径、默认 Agent/预设配置 |
| 设置 > Agent Runtime | 持久 Runtime 配置、默认值、插件管理、能力清单和连接诊断 | 某次活动会话的实时控制 |
Runtime 栏目入口始终存在,并采用统一监督模型;内部再按用户所选目标及其 Runtime 的真实能力
显示 OpenCode、Continue 或 DSH 的具体区域。未支持能力不渲染空卡片或一排禁用按钮,而是
在用户需要理解缺口时显示原因和可执行入口。跟随模式切换 Runtime、Conversation 或 Task
时必须清理上一归属的聚合执行状态,固定目标则保持不变。完整工作栏契约见
[通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)。
所有未来的 Subagent、Job、Workflow、Hook 和会话操作仍须经过 Main 的 Runtime 边界,保留取消、超时、权限、Task/Job/Subjob 层级、用量和活动审计。高风险动作在侧栏就地确认,运行结果进入活动与成果记录,不以 Composer 按钮代替监督面板。DeepSeek Harness 首版仍不加载这些服务,本节只确定未来跨 Runtime 的产品位置和协议归属。
## 15. IPC 与共享契约
共享 schema 需要覆盖:
- `deepseek-harness` provider 和 Runtime ID。
- Runtime 选择中的 `deepseekHarness` 分支。
- 检测结果中的路径、版本、详情和主机执行模式。
- GoodBuddy 模型连接选择。
- 从所选模型连接解析并传到 Host 的 `supportsImageInput`,以及 ACP 图片能力的一致性。
- DeepSeek Harness 模型用量归属。
- Skill 与 MCP 对 `deepseek-harness` 的显式分配。
- 插件市场总开关、目录、已安装状态、启停状态、JSON 配置和有界启动错误。
- 插件 `set-marketplace-enabled`、`install`、`set-enabled`、`configure`、`remove` 五类严格 action。
Renderer 只接收公开 npm 元数据和受管插件状态。任何凭据、完整环境、npm 子进程参数、内部 Host 配置或任意插件文件路径都不能由 Renderer 提交;安装 action 只能引用当前目录中的精确包名与版本。
已有设置迁移必须:
- 对没有新字段的用户使用安全默认值。
- 新建或没有已安装插件的旧 Store 将市场迁移为关闭;已有安装记录的旧 Store 保持开启,避免升级后隐藏用户正在管理的插件。
- 保留 OpenCode、Continue 和模型连接选择。
- 修复失效的 DeepSeek Harness 模型引用时给出可报告的迁移警告。
- 不把旧 Runtime 自动迁移为 DeepSeek Harness。
## 16. 打包与供应链
### 16.1 版本策略
- 官方 RC 包全部精确锁定,不使用 `^` 或 `~`。
- 同一 Harness 核心包族必须保持同一 RC 版本。
- 升级前检查 release diff、协议 diff、工具执行语义和依赖闭包。
- 内部握手同时检查锁定的 Harness 基线和 GoodBuddy 控制协议版本。
### 16.2 插件市场安装
- 目录来自 npm 公共搜索 API,只保留包含精确 `dsh-plugin` 关键字的包,最多读取 1,000 项并短期缓存。
- 安装时再次读取精确版本 packument,不信任搜索结果替代版本清单。
- GoodBuddy 精确锁定并随发布包携带 npm `11.19.0`;Electron 以 `ELECTRON_RUN_AS_NODE=1` 启动该 CLI 和受管 `node` shim,不要求用户另装 Node.js 或 npm。
- npm 使用普通依赖解析并运行包及依赖声明的 lifecycle scripts。安装确认必须准确说明这些脚本以当前用户权限运行。
- 安装在 Store 的暂存目录中完成,校验包名、精确版本、`dsh.bundle` 声明、入口文件和 lockfile integrity 后才原子替换当前版本。
- 市场关闭时拒绝新安装且不请求目录,但 `getEnabledExtensions()` 继续按逐项启停状态返回已安装插件。
- 首次安装默认启用。更新保留既有启停状态和 JSON 配置;失败更新保留原安装。
- 每个插件只有一个受管目录和一条状态记录;Store 同时持久化市场总开关。状态写入原子化且 mutation 串行。
- JSON 配置限制为对象根、64 KiB、16 层、每个容器 256 项和 4,096 个节点,避免 IPC、持久化和 Host 启动载荷无界增长。
- Renderer 不接收受管入口路径;Main 只接受目录中的插件 ID 与精确包版本,不能由 IPC 指定 tarball URL、文件路径或命令。
### 16.3 原生依赖
受控组合可能需要:
- `node-pty`,用于受管理的工具子进程。
- `koffi`,用于本地 Filesystem 在 Windows 上保持文件 ACL 和原子替换。
- `@napi-rs/canvas` 及当前平台二进制,用于在 Utility 内完整解码并复核 JPEG/PNG;原生模块必须从 ASAR 解包并通过目标架构校验。
构建 GoodBuddy 自身时不得广泛批准依赖安装脚本;只允许生产组合实际需要、来源已审查、版本已锁定的脚本。这与用户确认后由市场插件执行自身 lifecycle scripts 是两个不同阶段。六个平台的构建必须验证:
- 对应架构的原生文件存在。
- Electron Utility Process 可加载原生模块。
- spawn helper 的权限正确。
- 包中没有混入其他平台不需要的可执行内容,除非上游包无法拆分且已记录。
### 16.4 生产闭包
发布包包含受控 Host、锁定的 npm 安装 Runtime 和许可证。应尽量避免把 Harness Web profile、HMR 和其他未加载产品面带入生产闭包。若 npm 依赖结构无法拆分,必须:
- 确认这些模块不会被加载。
- 评估它们带来的 audit 和体积风险。
- 在后续上游版本允许时改为最小包族。
- 确认 `tests/fixtures` 以及 Web3D 测试 Skill/MCP 不进入正式发布资源。
### 16.5 漏洞门禁
当前安装后的 `npm audit` 报告不能直接用 `npm audit fix --force` 处理。每项漏洞需要区分:
- GoodBuddy 既有依赖。
- Harness 新增生产依赖。
- 仅开发或打包依赖。
- 未加载但被带入的 Web 依赖。
进入 Harness 执行路径且有可利用条件的高危问题必须在发布前修复、替换或移出生产闭包。
### 16.6 发布验证
`build/build-release.cjs` 需要验证:
- Harness Host 和受控配置存在。
- GoodBuddy Host、内部控制协议与 Harness 依赖版本清单存在。
- 平台原生 PTY/Koffi 依赖架构正确。
- Harness、ACP SDK 和其他新增第三方许可证已打包。
- `app.asar` 外需要执行或动态加载的资源位于预期目录。
- 独立的 npm Runtime 及其捆绑依赖闭包存在,并可通过当前 Electron Node Runtime 启动和执行生命周期脚本。
- Web3D Skill/MCP 等测试 fixture 不在 `app.asar` 或 `extraResources` 中。
## 17. 测试策略
### 17.1 单元测试
- Runtime 选择、设置迁移和失效引用修复。
- 二进制检测、版本解析和路径规范化。
- ACP 握手、事件转换和请求关联。
- 每个会话单请求、跨会话并行。
- Ask 在工具分发边界只允许真实注册的 `read`、`skill` 与 Main Web 代理,并拒绝同名冒充和任意新插件工具;Execute 放行插件工具。
- 握手只接受明确的 `execution.mode = 'host'`。
- 未分配 Skill/MCP 不可见;分配后的 Skill catalog 可调用 `skill` 加载。
- 原生能力快照只包含 Host/插件原生 Skills,不包含 GoodBuddy 分配的 Skills 或 MCP。
- Ask 不注册 MCP 工具;Web 代理可用于 Ask 与 Execute;Execute 每轮刷新有界 MCP schema,并在调用前再次校验活动请求、模式、参数和 RuntimeAuthorizer 结果。
- MCP URL、启动命令和凭据不进入 Utility 启动配置或协议结果。
- 未知授权结果失败关闭。
- 超时、取消、迟到帧和进程意外退出。
- 协议帧、事件队列、工具摘要和 stderr 上限。
- 文本模型在 Host 启动前拒绝图片;图片模型的能力声明、ACP 图片块、Pi-AI 模态和 Attachment Store 保持一致。
- 图片 base64、格式签名、PNG CRC、完整解码、尺寸、单图/单消息/Store 上限、内容摘要、Session 释放和 Host 清空。
- release 和 dispose 的幂等性。
- 状态卡中的状态、路径、版本和当前用户执行权限。
- 插件 action 与目录 schema 接受严格的市场总开关并拒绝权限、回滚、任意路径和非精确版本等未支持字段。
- Store 的原子安装、失败更新保留、串行 mutation、离线管理、配置、移除和启动失败停用。
- npm 分页、精确关键字、捆绑 CLI 调用、lifecycle 参数、包身份、入口和 integrity 校验。
- Renderer 的搜索、权限确认、Switch、JSON 配置、移除确认、离线目录和通知反馈。
### 17.2 本地集成测试
使用无网络的假控制面/模型验证:
- utilityProcess 管道。
- 多 Session。
- Session 释放。
- Runtime 替换。
- 进程树回收。
- 本地 Filesystem 与 Shell Provider 使用规范化工作区作为默认工作目录,且不报告沙箱强制模式。
- 受控配置不会读取工作区 `.env` 和用户 DSH 配置。
- 插件导入或激活失败相互隔离,成功插件继续加载,失败 ID 返回 Main。
- IPC 只接受严格插件 action,可信 Renderer 操作后触发 Runtime 重建。
### 17.3 真实模型测试
真实测试已经获得用户授权,但必须由显式环境门禁启用。Web3D Skill 和 MCP 仅作为 `tests/fixtures` 下的测试资产使用,不属于内置发布能力。至少验证:
1. 文本问答成功,并记录正确 Runtime 和模型用量。
2. Ask 可以读取工作区,但写入被拒绝,且不会弹出权限对话框。
3. 启用 Web Search 后,Ask 可以调用 Main 管理的 `web_search` 与 `web_fetch`,插件同名工具仍被拒绝。
4. Execute 可以在工作区创建测试文件。
5. Execute 工具确实以当前用户权限运行,且状态和握手不宣称 OS 隔离。
6. Ask、delegation 和无活动请求不能绕过工具分发检查。
7. 取消长请求后不再产生文本,并可继续使用其他 Session。
8. 两个 Session 可并行,事件不会串线。
9. 释放会话和关闭应用后没有残留 Harness 或工具进程。
10. 从全新用户设置流程启用一个 3D 游戏 Skill 和实际本地或开放 MCP,工具事件能够证明二者确实被调用。
11. Harness 生成的 3D 游戏项目可以安装、启动和实际游玩,包含 3D 渲染、玩家控制、目标和反馈,浏览器无关键错误。
12. 使用公共 npm 搜索,通过 GoodBuddy 捆绑的 npm 安装经审查的最小第三方插件,Host 成功加载并执行其真实工具。
13. 实际 ACP 路径中 Ask 拒绝该插件工具,Execute 允许该工具,不出现 GoodBuddy 逐工具确认。
测试不得打印、快照或提交 API Key。测试创建的文件只能位于专用临时工作区,并在确认可再现后清理。
### 17.4 项目验证
源码完成后必须运行:
```text
npm test
npm run typecheck
npm run lint
npm run build
```
涉及发布资源后,还要按可用原生平台运行聚焦的 `release:package` 验证。无法在当前主机执行的目标必须由六平台 CI 验证。
## 18. 验收标准
功能只有同时满足以下条件才算完成:
- `deepseek-harness` 可被保存、选择、检测和显示。
- Runtime 详情卡内显示状态、路径、版本和当前用户执行权限。
- Skills 与自定义 MCP 设置可把能力分配给 DeepSeek Harness;请求级 GoodBuddy 内置 MCP 当前不支持分配,并在内置 MCP 卡片中以置灰、未选择状态明确显示。布局、键盘语义、文案和保存回显通过真机检查。
- DSH 市场初始关闭且不加载 npm 目录;显式开启后可以搜索、安装、启停、配置和移除插件,安装前只出现一次准确的当前用户权限确认。关闭市场后已有启用插件继续运行,重新开启后管理状态不变。
- Ask 写入测试在 Runtime 边界失败。
- Ask 可使用已启用的 Main Web Search/Fetch,且插件无法通过同名工具绕过所有权校验。
- Ask 拒绝任意插件工具,Execute 可调用全部已启用插件工具。
- Runtime 原生清单只显示 Host/插件原生 Skills,不显示 GoodBuddy 分配项。
- Execute 工作区内写入成功。
- Runtime OS 沙箱设置、平台 Runner、启动探测和原生沙箱打包产物均不存在。
- 取消、超时、切换 Runtime 和退出应用均能回收进程。
- 多会话不串流、不串权限请求、不串用量。
- 用户 DSH 配置、`.env`、遥测和 Web UI 未被加载。
- 一个插件启动失败时 Host 仍可用,失败插件自动停用并在设置中显示。
- 发布包携带可执行的锁定 npm CLI,安装插件不依赖系统 Node.js/npm。
- API Key 不进入 Renderer、配置文件、日志、错误文本或测试产物。
- 全量测试、类型检查、Lint 和生产构建通过。
- 真实 OpenAI 兼容 Chat Completions 请求成功。
- 真实请求调用已分配 Skill 和 MCP,并生成、启动和实际游玩一个可用的 3D 游戏项目。
- 新增第三方许可证和发布校验完整。
## 19. 已知限制
- DeepSeek Harness 底层库当前是 RC,但 GoodBuddy 不自动跟随升级;每次升级都可能要求同步修改内部控制面。
- Harness 文件和命令工具没有 Runtime OS 隔离,会继承 GoodBuddy 客户端当前用户能够访问的主机资源。
- 首版不恢复 Harness 原生 Session,Runtime 重启后由 GoodBuddy 历史重建。
- 图片输入仅在所选模型连接明确声明支持时可用;首版仍不支持知识库、浏览器控制和 Harness Subagent。Web Search/Fetch 仅使用 Main 代理,MCP 仅支持用户分配、Main 代理和 Execute 自动单次授权路径。
- Harness Subagent、后台 Job、Workflow、Hook 和原生会话监督尚未实现;未来按 Task 聚合到右侧 Runtime 监督栏,不扩张 Composer 工具栏或暴露 Job/Run 层级。
- 推理、工具和用量扩展属于 GoodBuddy 协议,不是标准 ACP 保证。
- 市场来自公共 npm 关键字搜索,不是精选目录;包的质量、兼容性和维护状态由发布者负责。
- 插件安装、初始化、后台生命周期和 Execute 工具使用当前用户权限,不受 Runtime OS 沙箱保护;Ask 只控制模型工具调用。
- 不支持用户 profile、自定义 Host、任意本地插件路径或 profile patch。
## 20. 自维护与升级策略
GoodBuddy 对该 Runtime 采用内部维护策略:
1. 当前通过验证的 Host、控制协议和依赖锁定随 GoodBuddy 一起版本化。
2. 不自动跟随 DSH RC、插件 ABI、profile 格式或市场元数据变化;目录只反映 npm 当前精确版本。
3. 升级前审查实际用户收益、上游 diff、主机工具语义、协议行为、依赖闭包和许可证。
4. 六个平台的单元、假模型、UtilityProcess、主机执行和真实模型门禁全部通过后才能更新基线。
5. 若上游方向不再满足 GoodBuddy 用户需求或安全边界,允许维护兼容补丁、替换单个底层包,或逐步移除 DSH 依赖;`goodbuddy/*` 内部协议保持由 GoodBuddy 控制。
6. GoodBuddy 自身不以进入官方插件目录或服务非 GoodBuddy 客户端为目标;第三方市场兼容仅限当前受测 Cordis 导出和 `dsh.bundle` 声明。
## 21. 备选方案记录
### 21.1 每次调用 `dsh --profile headless`
未采用。它适合一次性任务,但不能满足流式事件、多会话、细粒度取消、权限回传和低延迟复用。
### 21.2 只使用官方 ACP 插件
未采用。取消和一次性权限选择符合需求,但缺少工具、推理、用量和会话释放事件。
### 21.3 只使用官方 SDK JSON-RPC
未采用。事件更完整,但单轮取消和权限回传能力不足。
### 21.4 把全部安全逻辑放进 Harness 子进程
未采用。Harness 子进程属于不可信执行面,不能拥有最终模式授权策略、加密设置和进程回收权限。
### 21.5 把全部控制适配放在 Main
未采用。Main 无法可靠观察 Cordis 内部 Session、Tool、Usage 和权限 seam,只能得到不完整的外部进程行为。
当前选择让双层内部控制面保持 GoodBuddy 私有,同时允许 Main 从受管 Store 向固定 Host 注入标准 Cordis 插件;插件扩展面不会取代 GoodBuddy 的可信 Main 控制权。
+50
View File
@@ -0,0 +1,50 @@
# 桌面宠物
桌面宠物是 GoodBuddy 桌面端一个可选的常驻悬浮入口,用于随时发起提问、暂存文件或文字,
并直观查看当前任务状态。桌面宠物不建立新的对话、任务或执行体系:它只是现有 Conversation、
Task 和 Runtime 能力的一个轻量触发面和状态镜面。
## 核心边界
- 桌面宠物**不**新增 Ask/Execute 执行通道,也不在自身窗口内直接展示模型回复。快速发起
和拖放暂存的内容始终交给主窗口现有输入区确认后发送,从不自动发送或自动执行。
- 桌面宠物**不**持续读取屏幕内容、剪贴板或全局选中文本。
- 桌面宠物显示的任务状态、通知和历史记录均来自现有 Task/Conversation 数据,不建立第二套
存储或索引。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、范围、功能需求和产品验收 |
| [User Stories](./user-stories.md) | 首次启用、正常使用、失败恢复、兼容和边界场景 |
| [功能逻辑](./logic-design.md) | 不变量、状态维度、决策表和失败结果 |
| [UI 设计](./ui-design.md) | 窗口交互、设置入口、状态呈现、无障碍和响应式行为 |
| [技术设计](./technical-design.md) | 窗口与进程边界、IPC 边界、持久化、生命周期和验证策略 |
功能尚未实施,因此不创建 `progress.md`;实施开始后按 [`docs/features/AGENTS.md`](../AGENTS.md)
的证据要求补充。
## 术语
- **桌面宠物窗口**:应用启用该功能后创建的常驻、无边框、可置顶悬浮窗口。
- **快速发起**:点击宠物打开的小型输入框,用于输入文字或附加单个文件,提交后转入主窗口。
- **暂存**:快速发起或拖放产生的文字和附件在提交前的中间状态,不代表已发送或已执行。
- **状态镜面**:宠物窗口对当前任务状态(等待处理、执行中、已完成、失败、空闲)的展示,
数据权威来自 Task 与 Conversation,不是独立状态源。
- **安静模式**:抑制宠物完成/失败气泡提示的设置,可访问的状态查询不受影响。
## 相关功能
- [Task 与 Job 文档集](../task-and-job/README.md):任务状态、状态优先级和 Task Center
导航的权威定义,宠物的状态镜面必须与其保持一致,不重新定义任务生命周期。
- [助手工作栏](../assistant-workbar/README.md):主窗口执行空间和输入区的权威定义,宠物的
快速发起和拖放暂存最终落地到该输入区,不复制其行为。
- [直连模型 Agent 能力](../direct-model-agent/README.md)、[远程主机与远程执行](../remote-host/README.md):
Ask/Execute 授权边界的权威定义,宠物不扩大或绕过该边界。
- [文档处理](../document-processing/README.md):拖放文件解析和格式支持的权威定义,宠物拖放
暂存复用同一处理管线,不重新定义支持类型。
- 根 [`UI-DESIGN.md`](../../../UI-DESIGN.md):全局视觉、通知视口、Switch/Checkbox 语义和
无障碍规则的权威定义。
- [`docs/development/release-runbook.md`](../../development/release-runbook.md):跨平台打包
矩阵和交付物的权威定义。
+203
View File
@@ -0,0 +1,203 @@
# 桌面宠物功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| User Stories | [桌面宠物 User Stories](./user-stories.md) |
| 任务模型 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
本文负责桌面宠物自身的状态、不变量和决策规则,不重新定义 Task、Conversation、Ask/
Execute 或附件的既有语义;这些语义以各自权威文档为准。
## 1. 核心不变量
### INV-1 宠物不是第二条执行通道
快速起草和拖放暂存只产生主窗口会话输入区的草稿数据(文本、附件引用),不产生消息、
不产生 Task、不触发模型调用。发送、模式选择和 Runtime 选择继续完全由主窗口既有流程
拥有。
### INV-2 状态镜面是既有状态的投影
宠物状态镜面的取值只能从当前 Task 集合的既有状态推导,不允许引入宠物专属的任务状态
字段或独立于 Task Center 的状态存储。宠物状态与 Task Center 展示的状态出现分歧即视为
缺陷。
### INV-3 总开关是唯一的存在性开关
“启用桌面宠物”关闭时,宠物窗口、气泡和托盘条目文案必须整体消失;除该开关外,任何
子设置(外观、位置、安静模式)的变化都不得单独创建或销毁宠物窗口。
### INV-4 暂时隐藏不影响启用状态
“暂时隐藏”只影响窗口可见性,不修改总开关持久化值,也不清除位置、外观或安静模式设置。
### INV-5 提示与原生通知互斥
同一个任务终态事件(完成或失败)只能产生宠物气泡或现有原生系统通知中的一种,不产生
两种同时可见的独立提示。
### INV-6 位置始终落在可用显示器内
宠物窗口的持久化位置在应用于当前显示器拓扑之前必须被校验;不能得出宠物窗口完全位于
任何当前可见显示器之外的最终坐标。
### INV-7 拖放与起草共享暂存规则
拖放产生的文本和文件必须满足与快速起草附件相同的类型、大小、数量限制;两者不允许存在
两套互不一致的限制值。
## 2. 状态维度
### 2.1 总开关与可见性
```text
disabled -> enabled_visible
enabled_visible <-> enabled_hidden (暂时隐藏 / 重新显示)
enabled_visible|enabled_hidden -> disabled (关闭总开关)
```
`disabled` 是唯一没有宠物窗口存在的状态;`enabled_hidden` 窗口不可见但设置全部保留。
### 2.2 快速起草弹出框
```text
closed -> drafting -> submitted | discarded
drafting -> closed (Escape / 点击外部 / 关闭按钮)
```
`submitted` 是终态转换动作,不是持续状态:提交后弹出框立即回到 `closed`,草稿数据已经
转交给主窗口。
### 2.3 拖放交互
```text
idle -> drag_over -> dropped_accepted | dropped_rejected | drag_cancelled
```
`drag_over` 只影响视觉反馈,不产生任何持久数据;只有 `dropped_accepted` 才产生暂存
数据。`dropped_rejected` 用于文件夹或全部文件超限的场景,不产生暂存也不打开主窗口。
### 2.4 状态镜面
```text
unavailable | idle | completed_recent | running | waiting_approval | failed_recent
```
取值由当前活跃与最近终态 Task 集合计算得出,属于派生状态,不单独持久化。
### 2.5 提示气泡
```text
none -> pending -> shown -> dismissed
```
`pending` 表示事件已发生但由于主窗口可见或安静模式而被抑制,不进入 `shown`。
## 3. 状态镜面优先级
宠物在任意时刻只呈现一个状态镜面取值。存在多个活跃 Task 时按以下优先级取最高项:
| 优先级 | 状态 | 条件 |
| --- | --- | --- |
| 1 | `unavailable` | 用于确认状态的 Runtime 或网络当前不可达 |
| 2 | `waiting_approval` | 至少一个 Task 处于等待批准 |
| 3 | `failed_recent` | 至少一个 Task 在最近的“最近失败”窗口内失败,且用户尚未查看 |
| 4 | `running` | 至少一个 Task 处于执行中 |
| 5 | `completed_recent` | 至少一个 Task 在最近的“最近完成”窗口内完成,且用户尚未查看 |
| 6 | `idle` | 以上条件都不满足 |
“最近失败”“最近完成”窗口的具体时长和“已查看”判定属于技术选择,由
[技术设计](./technical-design.md)基于既有 Task 已读/未读语义确定,不在本文固定具体
数值。
## 4. 提示归并决策表
| 事件 | 主窗口状态 | 安静模式 | 结果 |
| --- | --- | --- | --- |
| 任务完成 | 不可见或未聚焦 | 关闭 | 显示宠物气泡;不触发原生通知 |
| 任务完成 | 不可见或未聚焦 | 开启 | 不显示气泡;不触发原生通知;仅更新状态镜面 |
| 任务完成 | 可见并聚焦 | 任意 | 不显示气泡;反馈通过主窗口既有机制呈现 |
| 任务失败 | 不可见或未聚焦 | 关闭 | 显示宠物气泡并保持可见直到用户查看或关闭 |
| 任务失败 | 不可见或未聚焦 | 开启 | 不显示气泡;仅更新状态镜面为 `failed_recent` |
| 任务失败 | 可见并聚焦 | 任意 | 不显示气泡;反馈通过主窗口既有机制呈现 |
“主窗口可见并聚焦”与现有 [`desktop-notification.ts`](../../../src/main/desktop-notification.ts)
判定原生通知是否触发所使用的条件保持一致,避免宠物和原生通知各自使用不同的可见性
判定导致同一事件在两处呈现或同时缺失。
## 5. 快速起草与拖放的目标会话决策表
| 场景 | 目标会话 |
| --- | --- |
| 主窗口存在当前活跃会话,用户未在弹出框内更改目标 | 该活跃会话 |
| 用户在弹出框内选择“新建对话” | 提交时新建并激活一个会话 |
| 主窗口不存在任何会话 | 提交时新建并激活一个会话 |
| 目标会话在提交前被用户在主窗口中删除 | 视为“不存在任何会话”,回退到新建会话 |
拖放没有目标切换界面,始终采用与快速起草相同的默认目标会话规则。
## 6. 附件与文本限制
快速起草和拖放暂存复用现有聊天附件规则,不定义新的限制数值:
- 单条草稿的附件数量上限、单个附件大小上限、支持的文本与图片类型,与主窗口聊天附件
的现有限制一致。
- 拖放同时包含多个文件时,逐个按上述规则校验;不合规文件被拒绝,不影响合规文件继续
暂存。
- 拖放文件夹被整体拒绝,不做递归展开或部分接受。
## 7. 位置与显示器校验
1. 加载持久化位置后,检查该坐标是否位于当前任一可见显示器的可用区域内。
2. 位于范围内:直接使用。
3. 不在范围内(例如记忆的显示器已断开):改用主显示器上一个默认可见位置,并在下一次
用户拖动宠物后覆盖持久化位置。
4. 开启“贴边”时,拖动释放位置与最近边缘的距离小于吸附阈值即吸附到该边缘;阈值和默认
位置的具体数值属于技术选择。
## 8. 失败与恢复
| 场景 | 行为 |
| --- | --- |
| 主窗口进程尚未创建或已被销毁 | 提交快速起草或拖放时先创建/恢复主窗口,再写入草稿 |
| 写入草稿时主窗口窗口对象不可用 | 提交失败,弹出框保留草稿内容供用户重试,不静默丢弃 |
| 状态查询所需的 Runtime/网络不可达 | 状态镜面显示 `unavailable`,不回退显示 `idle` |
| 显示器拓扑变化导致位置越界 | 按第 7 节回退到默认可见位置,不隐藏窗口 |
| 用户在气泡展示期间关闭总开关 | 气泡与宠物窗口一并立即消失 |
| 应用启动时总开关为开启但窗口创建失败 | 记录失败原因,不阻塞主窗口正常启动,托盘菜单提供重试入口 |
## 9. 需求追踪
| 需求 | 主要逻辑 |
| --- | --- |
| `FR-1` | `INV-3`、总开关与可见性状态 |
| `FR-2` | 位置与显示器校验、`INV-6` |
| `FR-3` | `INV-1`、快速起草状态、目标会话决策表 |
| `FR-4` | `INV-1`、`INV-7`、拖放交互状态、附件与文本限制 |
| `FR-5` | `INV-2`、状态镜面优先级 |
| `FR-6` | `INV-5`、提示归并决策表 |
| `FR-7` | 提示归并决策表中安静模式列 |
| `FR-8` | 位置与显示器校验之外的技术选择,逻辑仅约束持久化立即生效 |
| `FR-9` | `INV-4`、总开关与可见性状态 |
| `FR-10` | 快速起草状态中的键盘转换,具体按键映射见 UI 设计 |
| `FR-11` | `INV-1`、`INV-2`、`INV-7` |
## 10. 逻辑完整性评估
本设计覆盖了启用/关闭、暂时隐藏、快速起草、拖放(含超限和文件夹)、状态镜面优先级、
提示与原生通知的归并、位置越界恢复、以及主窗口不可用时的失败路径。
未在本文固定、留给技术设计选择的实现细节:
- “最近完成/最近失败”窗口的具体时长和“已查看”判定的确切事件来源。
- 贴边吸附阈值和默认回退位置的具体像素值。
- 状态查询的具体轮询或事件驱动机制。
这些是技术选择而非未解决的产品决策:本文的优先级和决策表已经足以在任何取值下保持
行为一致。
+158
View File
@@ -0,0 +1,158 @@
# 桌面宠物 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 适用产品 | GoodBuddy Desktop |
| 关联文档 | [功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md) |
## 1. 背景
GoodBuddy 当前只能通过主窗口发起提问、查看任务和管理执行空间。用户想要快速提问、丢一个
文件进去,或者只是看一眼任务有没有跑完时,仍必须先唤出并聚焦整个主窗口。这在多任务、多
显示器场景下打断当前工作流。
桌面宠物提供一个常驻、体积小、不抢焦点的悬浮入口,承担三类高频、轻量的动作:快速起草
一个问题、拖放文件或文字暂存、以及查看当前任务状态。它不是新的执行通道,只是现有
Conversation、Task 和 Runtime 能力上的一层轻量触发面和状态镜面。
## 2. 产品目标
1. 用户无需先打开或聚焦主窗口即可起草一条消息或暂存一个文件。
2. 用户可以在不打开 Task Center 的情况下,一眼看到当前是否有任务在跑、等待处理或失败。
3. 桌面宠物默认不产生新的干扰:不主动弹窗抢焦点,不重复现有原生通知。
4. 桌面宠物的开关、位置、外观和提示强度完全由用户控制,且可随时完全关闭并恢复到没有该
功能之前的界面状态。
5. 桌面宠物不新增或绕过 Ask/Execute 授权边界,也不建立第二条消息发送或任务执行路径。
## 3. 功能范围
### 3.1 包含
- 设置中的“桌面宠物”一级分类,包含总开关、外观、位置和提示设置。
- 常驻悬浮窗口:可拖动、跨显示器记忆位置、贴边、置顶开关。
- 点击宠物打开的“快速起草”弹出框:输入文字、附加至多一个本机文件,提交后转入主窗口
对应会话的输入区草稿,不自动发送。
- 拖放文字或文件到宠物窗口,效果与快速起草的附件路径一致。
- 状态镜面:显示当前活跃任务的汇总状态,点击后打开 Task Center 中对应任务。
- 完成/失败气泡提示,及独立于原生通知的安静模式开关。
- 从有限的内置角色/外观中选择,调整尺寸、透明度和动画强度。
- 全局显示/隐藏开关,托盘菜单入口,键盘可达的等效操作路径。
### 3.2 不包含
- 在宠物窗口内直接展示模型回复、执行工具调用或建立第二条 Ask/Execute 通道。
- 未经用户确认自动发送消息或自动执行任务。
- 持续读取屏幕内容、剪贴板或系统级选中文本。
- 用户上传任意自定义头像图片、养成机制、货币或每日签到。
- 多个宠物实例、宠物之间互动或联网社交功能。
- 管理员远程强制启用、配置或监控用户的桌面宠物。
- 替代 Task Center、助手工作栏或现有通知系统的权威数据来源。
## 4. 功能需求
### FR-1 总开关与默认状态
- 设置新增“桌面宠物”一级分类,包含“启用桌面宠物”总开关。
- 全新安装和现有用户升级后默认关闭,不在未经用户操作时出现在桌面上。
- 关闭总开关立即隐藏宠物窗口,保留用户此前的位置、外观和提示设置以便重新开启。
### FR-2 常驻悬浮窗口
- 启用后创建一个无边框、默认置顶、不出现在任务栏的小型窗口。
- 用户可拖动到任意位置;位置按显示器分别记忆,显示器断开重连后回退到最近的可用显示器。
- 提供“贴边”选项,拖动到屏幕边缘附近时自动吸附。
- 提供独立的“始终置顶”开关;关闭后宠物仍可见,但可被其他窗口遮挡。
- 宠物窗口不接受系统级最大化、最小化或调整大小操作。
### FR-3 快速起草
- 点击宠物打开一个紧邻宠物的小型弹出框,包含文本输入和“附加文件”按钮。
- 文本输入支持多行、粘贴图片;附加文件复用现有附件类型、数量和大小限制。
- 弹出框显示将要进入的目标:当前活跃会话,或“新建对话”。用户可在弹出框内切换目标,
但不能在弹出框内选择模型、Runtime 或 Ask/Execute 模式。
- 提交后主窗口被显示或聚焦,目标会话被激活,文字和附件作为草稿填入该会话的输入区,
输入焦点落在该输入区。提交动作本身不发送消息、不创建任务。
- 取消或关闭弹出框放弃当前草稿,不影响主窗口任何已有草稿。
### FR-4 拖放暂存
- 用户可以把本机文件或选中文字拖到宠物窗口上。
- 拖入时宠物给出可放置的视觉反馈;释放后遵循与快速起草提交相同的行为:显示主窗口、
激活目标会话、把内容放入该会话输入区草稿。
- 拖放文件的类型、数量、大小限制与现有聊天附件限制一致;超出限制时明确提示被拒绝的
文件,其余合法文件仍正常暂存。
- 拖放不支持文件夹;文件夹被拖入时提示不支持,不递归展开。
### FR-5 状态镜面
- 宠物在空闲、执行中、等待处理、已完成、失败之间切换外观,反映当前所有活跃 Task 的
汇总状态;多个状态并存时按“等待处理 > 失败 > 执行中 > 已完成 > 空闲”的优先级显示。
- 点击状态镜面打开或聚焦主窗口的 Task Center,并定位到触发当前状态的任务。
- Runtime 或网络不可用导致任务状态无法确认时,状态镜面显示“状态不可用”而不是回退成
“空闲”。
### FR-6 完成与失败提示
- 任务从执行中或等待处理转为已完成或失败,且主窗口不可见或未聚焦时,宠物旁显示一条
气泡,说明任务标题和结果。
- 气泡在用户不操作时于一段时间后自动消失;失败气泡保持可见直到用户查看或手动关闭。
- 点击气泡等价于点击状态镜面,打开对应任务详情。
- 同一事件只能触发桌面宠物气泡或现有原生系统通知中的一种,不同时出现两条提示;具体
归并规则由[功能逻辑](./logic-design.md)定义。
### FR-7 安静模式
- 独立于总开关的“安静模式”开关,开启后不再显示完成/失败气泡。
- 安静模式不影响状态镜面本身的外观切换,也不影响原生系统通知的现有行为。
### FR-8 外观与个性化
- 提供数量有限的内置角色供选择,不支持上传自定义图片。
- 提供尺寸、透明度和动画强度调整;调整立即生效并持久化。
- 提供“恢复默认外观”操作。
### FR-9 显示、隐藏与退出
- 提供独立于总开关的“暂时隐藏”动作:隐藏宠物窗口但保留启用状态,可通过托盘菜单或
设置重新显示。
- 托盘菜单新增“显示/隐藏桌面宠物”入口,随当前可见性变化文案。
- 应用退出时宠物窗口一并关闭,不残留独立进程。
### FR-10 键盘可达性
- 宠物窗口本身可通过系统级窗口切换(如 Alt+Tab)取得焦点,取得焦点后可用键盘打开快速
起草弹出框、在状态镜面和提示之间移动,并可用 Escape 关闭弹出框。
- 所有交互提供可访问名称和状态说明,不仅依赖动画或颜色表达状态。
### FR-11 与现有能力的边界
- 快速起草和拖放暂存最终使用主窗口现有的会话、附件、模型和 Ask/Execute 选择逻辑,不
新增第二套发送或授权路径。
- 状态镜面和气泡的任务状态数据来自现有 Task 与 Conversation 记录,不建立独立存储、不
与 Task Center 的状态定义产生分歧。
- 桌面宠物不改变现有全局快捷键、托盘菜单既有条目或主窗口默认行为。
## 5. 产品验收
- 全新安装和已升级用户默认看不到桌面宠物;显式启用后立即出现在预期位置。
- 关闭总开关后桌面上不再有任何宠物窗口或残留提示,重新开启后恢复此前的位置和外观。
- 通过快速起草或拖放提交的内容,在主窗口对应会话的输入区中可见为未发送草稿,用户需要
自行确认发送;关闭 GoodBuddy 或取消草稿不会产生任何任务或消息记录。
- 状态镜面呈现的状态与 Task Center 中对应任务的状态一致;两者出现分歧即视为缺陷。
- 同一任务完成或失败事件不会同时产生宠物气泡和原生系统通知。
- 安静模式开启时不出现气泡,但状态镜面外观仍随任务状态变化。
- 键盘用户无需鼠标即可打开快速起草、查看状态并关闭弹出框。
- 中英文文案、深浅主题、多显示器和 DPI 缩放下宠物窗口位置和内容均正确显示。
## 6. 明确限制
- 桌面宠物不提供在其自身窗口内完成整段对话或查看历史消息的能力;查看完整回复必须进入
主窗口。
- 桌面宠物不支持多实例、多角色同屏或角色之间的模拟互动。
- 透明背景效果依赖操作系统合成器;在不支持透明合成的 Linux 桌面环境下,宠物窗口可能
退化为不透明的小型窗口,具体行为见[技术设计](./technical-design.md)。
@@ -0,0 +1,335 @@
# 桌面宠物技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| UI 设计 | [UI 设计](./ui-design.md) |
## 1. 进程与窗口边界
```text
Main
-> createMainWindow() (既有,src/main/window.ts)
-> createPetWindow() (新增)
-> 独立 BrowserWindow:无边框、透明、置顶、不入任务栏
-> 独立 preload:src/preload/pet.ts(新增,窄 API)
-> 加载独立渲染入口:src/renderer/pet.html + src/renderer/src/pet/
Renderer(主窗口,既有)
-> window.goodbuddy(既有 DesktopApi,不变)
Renderer(宠物窗口,新增)
-> window.goodbuddyPet(新增窄 API,与既有 window.goodbuddy 完全隔离)
```
宠物窗口是与主窗口平级的第二个 `BrowserWindow`,不是主窗口的子窗口、不是 `WebContentsView`
叠加层。理由:
- 主窗口现有 `setWindowOpenHandler` 拒绝一切子窗口(`src/main/window.ts`),沿用该策略
比引入例外更简单。
- 宠物窗口需要独立于主窗口的显示/隐藏、位置和置顶状态,作为子窗口会继承父窗口的显示
状态,不满足“主窗口隐藏时宠物仍可见”的产品要求。
不复用主窗口的 preload 和渲染入口:主窗口 preload 暴露的 `DesktopApi` 面积很大(对话、
知识库、设备共享等)。给宠物窗口加载同一 preload 会让一个新增的、长期运行、暴露给
远程可控网页内容风险更低但攻击面更广的窗口拥有不必要的能力。新增窄 preload 遵循仓库
现有的“只暴露显式方法”约束。
## 2. 模块职责
| 模块 | 位置 | 职责 |
| --- | --- | --- |
| `PetWindowService` | `src/main/pet/pet-window-service.ts`(新增) | 创建/销毁宠物窗口、位置持久化与越界校验、置顶与贴边、显示/隐藏 |
| `PetSettingsStore` | `src/main/pet/pet-settings-store.ts`(新增) | 总开关、外观、位置、安静模式的持久化,遵循既有 settings-file-utils 模式 |
| `PetStatusProjector` | `src/main/pet/pet-status-projector.ts`(新增) | 从既有 Task 查询结果计算状态镜面取值,不引入新状态源 |
| `PetDraftBridge` | `src/main/pet/pet-draft-bridge.ts`(新增) | 把宠物提交的文本/附件转交主窗口渲染进程,驱动会话激活与草稿写入 |
| `PetNotificationCoordinator` | `src/main/pet/pet-notification-coordinator.ts`(新增) | 与既有 `desktop-notification.ts` 协商同一事件只走一条提示路径 |
| 既有 `context-manager.ts` | 不变 | 宠物附件校验复用其 `selectFiles`/校验规则,不新增第二套限制 |
| 既有 `assistant-database.ts` / Task 查询 | 不变 | 状态镜面查询的唯一数据来源 |
`PetStatusProjector` 只读取既有 Task 查询结果并按
[功能逻辑第 3 节](./logic-design.md#3-状态镜面优先级)的优先级归并,不写入、不缓存跨进程
重启的状态;重启后状态镜面从当前 Task 集合重新计算。
## 3. 宠物窗口创建
```ts
// src/main/pet/pet-window-service.ts(示意)
function createPetWindow(): BrowserWindow {
return new BrowserWindow({
width: PET_SIZE.width,
height: PET_SIZE.height,
show: false,
frame: false,
transparent: true,
resizable: false,
skipTaskbar: true,
alwaysOnTop: true, // 由“始终置顶”设置决定实际值
hasShadow: false,
backgroundColor: '#00000000',
webPreferences: {
preload: join(currentDirectory, '../preload/pet.cjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
}
```
- `transparent: true` 依赖操作系统合成器。按 PRD 6 节明确限制,在不支持透明合成的
Linux 桌面环境(例如未运行合成器的部分窗口管理器)下,Electron/Chromium 会把窗口
渲染为不透明;`PetWindowService` 不检测合成器能力,只在渲染进程内把背景色设置为
当前主题的画布色作为不透明回退,保证内容仍然可读,不尝试探测或声明支持特定合成器。
- `alwaysOnTop` 的实际值由“始终置顶”Switch 驱动,通过 `window.setAlwaysOnTop(bool)`
在设置变更时更新,不需要重建窗口。
- 复用既有 `resolveWindowIcon`/任务栏排除逻辑作为参考,但宠物窗口 `skipTaskbar: true`
固定为真,不提供用户设置。
- 宠物窗口不设置 `parent`,与主窗口是同级独立窗口;应用单实例锁和 `second-instance`
处理(`src/main/index.ts`)不需要改动,因为宠物窗口的存在与否由 `PetWindowService`
内部状态决定,而不是第二个 Electron 实例。
## 4. 拖放实现
Electron 渲染进程原生支持 HTML5 拖放;宠物渲染进程监听 `dragenter`/`dragover`/`drop`
即可获得视觉反馈和释放事件,不需要 Main 参与拖放事件本身。文件路径解析复用既有
`webUtils.getPathForFile`(已用于 `src/preload/index.ts` 的知识库拖放导入):
```ts
// src/preload/pet.ts(示意)
import { contextBridge, ipcRenderer, webUtils } from 'electron'
contextBridge.exposeInMainWorld('goodbuddyPet', {
submitDraft: (input: PetDraftSubmission) =>
ipcRenderer.invoke(petIpcChannels.submitDraft, input),
resolveDroppedPaths: (files: File[]) =>
files.map((file) => webUtils.getPathForFile(file)).filter(Boolean),
// ...状态订阅、设置读写等方法
})
```
拖入的文本(非文件)通过 `dataTransfer.getData('text/plain')` 直接读取,随普通文本
字段一起提交,不经过路径解析。
宠物渲染进程把解析出的路径和文本打包为 `PetDraftSubmission`,通过 IPC 交给 Main;Main
侧的路径校验、类型判断、大小限制和读取,全部复用既有 `ContextManager` 的规则(`src/main/context-manager.ts`
中的 `supportedExtensions`、`supportedImageExtensions`、`supportedDocumentExtensions`、
`maximumContextBytes`、`maximumAttachmentsPerMessage`),不新增校验逻辑。`ContextManager`
目前的公开入口是围绕原生对话框(`selectFiles`)设计的;实现时需要新增一个接受任意
canonical 路径数组的方法(例如 `stageFromPaths`),内部复用现有扩展名/大小/文档解析
分支,而不是复制一份判断逻辑。文件夹路径(`fs.stat` 判定为目录)在该方法入口即被拒绝,
不递归遍历。
## 5. 快速起草提交与主窗口联动
```text
宠物渲染进程 submitDraft(input)
-> Main: PetDraftBridge.handleSubmit(input)
-> ContextManager.stageFromPaths(input.filePaths) (复用既有校验)
-> 若主窗口不存在:createMainWindow() + loadMainWindow()(复用既有函数)
-> showWindow(mainWindow)(既有函数)
-> mainWindow.webContents.send(petIpcChannels.draftReceived, {
targetConversationId, text, stagedContextIds
})
-> 主窗口渲染进程(App.tsx 新增一个事件监听,与既有 conversation:new 监听模式一致)
-> 若 targetConversationId 无效或已删除:回退为新建会话(既有新建会话路径)
-> 激活目标会话、把 text 写入该会话输入框状态、把 stagedContextIds 追加到该
会话的附件状态(既有 App.tsx 附件状态更新路径)
-> 聚焦输入框
```
- 草稿只在主窗口渲染进程内变成“输入框状态”和“待发送附件列表”,与用户手动打字/手动
点击附件按钮产生的状态是同一份 React state,不建立并行的草稿存储。这直接满足
`INV-1`:宠物不产生消息、不产生 Task。
- `stagedContextIds` 复用现有 `context:*` IPC 产生的 Context ID;主窗口把这些 ID 加入
附件列表的方式,与用户通过“附加文件”按钮选择文件后的状态更新完全一致。
- 若提交发生时主窗口曾被销毁(用户从未打开过,或者提前研究表明当前架构不销毁主窗口
只隐藏),`PetDraftBridge` 调用既有 `createMainWindow`/`loadMainWindow`,等待
`ready-to-show` 完成后再发送 `draftReceived`,避免竞态。
## 6. IPC 边界
新增独立 IPC 通道命名空间 `pet:*`,与既有 `ipcChannels` 并列定义在
`src/shared/ipc-channels.ts`:
```text
pet:get-snapshot (设置 + 当前状态镜面取值 + 可见性)
pet:update-settings (总开关、外观、贴边、置顶、安静模式)
pet:submit-draft (快速起草/拖放提交)
pet:dismiss-bubble (关闭完成/失败气泡)
pet:open-task (点击状态镜面/气泡,打开 Task Center 对应任务)
pet:toggle-visibility (托盘“隐藏/显示桌面宠物”)
```
- 所有输入使用 `src/shared/pet-contracts.ts`(新增)中的 Zod schema 校验,与既有约定
一致。
- `trusted-ipc-sender.ts` 的 `assertTrustedSender` 按窗口实例参数化使用;宠物窗口的
IPC handler 校验 `event.sender === petWindow.webContents`,不能用主窗口的 trusted
sender 校验函数验证宠物窗口的请求,也不能反过来。
- 宠物 preload 不暴露任何既有 `DesktopApi` 方法;需要的能力(提交草稿、读取/写入宠物
设置、订阅状态镜面)都通过 `pet:*` 通道单独暴露为 `window.goodbuddyPet` 的具体方法。
- Main 侧广播状态镜面变化时,只向宠物窗口发送派生后的枚举值(第 3 节的六种状态之一)
和最小必要的任务标题/ID,不发送完整 Task 或 Conversation 对象。
## 7. 持久化
新增设置文件,遵循既有 `settings-file-utils.ts` 的原子写入、版本校验和损坏隔离模式,
不复用 `ApplicationSettingsStore` 或 `ShortcutSettingsStore` 的 schema:
```text
pet-settings.json
{
version: 1,
enabled: boolean,
quietMode: boolean,
alwaysOnTop: boolean,
snapToEdge: boolean,
appearance: { character: string, size: 'small'|'medium'|'large',
opacity: number, motion: 'off'|'reduced'|'full' },
position: { displayId: number, x: number, y: number } | null
}
```
- `position` 为 `null` 表示尚未被用户拖动过,使用默认位置计算规则(第 8 节)。
- `displayId` 使用 Electron `screen.getAllDisplays()` 返回的 `id`;加载时按第 8 节校验,
不假设跨重启保持稳定的显示器 id 一定仍然存在。
- 独立 schema 版本号(从 1 开始),与主设置版本号互不影响;未来变更只需要迁移这一个
文件。
- 是否需要独立数据库表:不需要。宠物没有需要查询、筛选或跨设备同步的历史数据,一个
JSON 文件足够,符合仓库 KISS/YAGNI 约束。
## 8. 位置与多显示器校验
```ts
function resolvePetPosition(
saved: PetPosition | null,
displays: Display[]
): { x: number; y: number } {
if (saved) {
const display = displays.find((d) => d.id === saved.displayId)
if (display && pointWithinWorkArea(saved, display.workArea)) {
return saved
}
}
const primary = screen.getPrimaryDisplay()
return defaultPositionWithin(primary.workArea) // 例如右下角,留出安全边距
}
```
- 该函数在宠物窗口创建时以及 `display-added`/`display-removed`/`display-metrics-changed`
事件触发时调用;越界立即调用 `window.setPosition` 校正,不等待用户下一次拖动。
- 用户拖动结束(`moved` 事件)时把当前 `getBounds()` 和 `screen.getDisplayNearestPoint`
得到的 `displayId` 写入 `PetSettingsStore`,去抖后原子写入,避免拖动过程中高频写盘。
- 贴边吸附在渲染进程拖动过程中做视觉预判(不移动实际窗口,只显示吸附提示),在
`moved` 事件的最终位置上由 Main 一次性计算是否落入吸附阈值并调用 `setPosition`
吸附到边缘,阈值和默认位置的具体像素值作为实现细节调优,不在此固定。
## 9. 状态镜面查询
`PetStatusProjector` 不新增事件源,而是订阅既有 Task 变化通知(主窗口渲染进程已经
用于更新 Task Center 和会话内任务卡片的同一套 Main → Renderer 事件)在 Main 侧的
对应版本,或者在 Main 侧直接查询 `assistant-database.ts` 暴露的 Task 读取方法。具体
选择两者之一属于实现阶段的技术选择,约束条件是:
- 不引入宠物专属的任务状态表或缓存;查询/订阅的结果必须与 Task Center 在同一时刻
展示的状态一致。
- 查询开销必须是有界的(只统计活跃与最近终态 Task 的数量和状态,不拉取消息正文)。
- Runtime/网络不可达导致查询失败时返回 `unavailable`,不静默返回 `idle`。
“最近完成”“最近失败”窗口时长和“已查看”判定:复用 Task 已有的已读/查看时间戳字段
(若既有模型已提供);若当前模型没有这类字段,实现时以“该 Task 终态发生后,用户是否
已经点击过对应的宠物气泡或状态镜面,或已经在 Task Center 中打开过该 Task”作为已查看
判定,窗口时长建议与现有原生通知的展示逻辑保持同量级(分钟级),具体数值在实现时
根据既有 Task 事件粒度确定。
## 10. 提示与原生通知协商
`PetNotificationCoordinator` 与 `desktop-notification.ts` 的 `showDesktopNotificationWhenUnfocused`
共享同一个“主窗口是否可见并聚焦”判定:
```ts
function shouldSuppressBecauseMainWindowVisible(mainWindow: BrowserWindow): boolean {
return !mainWindow.isDestroyed() && mainWindow.isFocused()
}
```
- 现有 `showDesktopNotificationWhenUnfocused` 已经使用等价条件(`window.isFocused()`)
决定是否弹出原生通知;`PetNotificationCoordinator` 复用同一判定函数(如需要,提取为
共享工具),保证两者的“可见性”定义永远一致,不出现原生通知判定为可见但宠物判定为
不可见(或反之)导致同一事件同时出现两条提示或完全没有提示的情况。
- 实现层面:调用触发通知的代码路径在决定“原生通知 vs 宠物气泡”时二选一,而不是
两者分别独立判断后都可能触发;例如由现有触发点先判断安静模式和宠物启用状态,
为真则调用 `PetNotificationCoordinator`,否则调用现有 `showDesktopNotificationWhenUnfocused`。
- 安静模式开启时,两者都不触发;状态镜面仍照常更新(状态镜面更新走独立的
`pet:get-snapshot`/推送通道,不经过通知协商)。
## 11. 与既有模块集成
- 快速起草和拖放最终只调用主窗口渲染进程已有的“设置输入框文本”“追加附件”“激活
会话”“创建新会话”状态更新函数,不新建与 Ask/Execute、模型选择、Runtime 选择相关
的任何路径。
- 点击状态镜面/气泡打开 Task Center 复用现有 Task Center 导航(既有 IPC/路由,与托盘
“新建对话”发送 `conversation:new` 事件属于同一类模式:Main 发送一个事件,主窗口
渲染进程消费并完成导航)。
- 托盘菜单新增项复用现有 `Menu.buildFromTemplate` 结构(`src/main/index.ts` 的
`buildTray`),插入一个受当前可见性状态驱动的条目,不重写整个菜单构建函数。
- 全局快捷键(`src/shared/shortcut.ts`、`shortcut-settings-service.ts`)不受影响,
桌面宠物不注册新的全局快捷键,因为激活方式是点击窗口本身或系统级窗口切换,不需要
额外抢占全局按键空间。
## 12. 生命周期
- 应用启动读取 `PetSettingsStore`;`enabled: true` 时在既有服务和 IPC 注册完成后创建
宠物窗口(与主窗口创建顺序一致,不阻塞主窗口 `ready-to-show`)。
- 宠物窗口创建失败(例如极端情况下操作系统拒绝创建透明窗口):记录原因,不阻塞主窗口
启动,托盘“显示桌面宠物”下次点击时重试创建。
- 应用退出(`before-quit`/既有 `isQuitting` 标记)时,宠物窗口与主窗口一并销毁,不需要
独立的关闭确认或延迟清理,因为宠物窗口没有正在进行的网络连接或未保存状态:草稿已经
转交主窗口的那一刻起就属于主窗口生命周期。
- “暂时隐藏”只调用 `window.hide()`,不销毁窗口实例,重新显示直接 `showWindow` 复用
既有实例,避免频繁创建/销毁透明窗口带来的闪烁。
## 13. 打包
- 宠物渲染入口(HTML、内置角色素材)加入 `electron.vite.config.ts` 的 renderer 构建
输入(现有配置已有 `input` 多入口结构,参考现有主渲染入口的声明方式新增一项)。
- 内置角色素材(数量有限的静态图片/精灵图)作为打包资源加入 `package.json` 的 `files`/
`extraResources`,不通过运行时网络下载,符合仓库“不新增远程资源请求”的字体/资源
约束的同一精神。
- 新增 preload 产物(`pet.cjs`)需要加入 `electron.vite.config.ts` 的 main/preload 构建
`entryFileNames` 规则,与现有主 preload 产物并列。
- 六个标准打包目标(Windows/macOS/Linux × x64/arm64)均需验证宠物窗口可创建、透明或
回退不透明背景符合第 3 节预期;LoongArch 预览目标不单独验证,遵循仓库现有策略。
## 14. 验证策略
### 14.1 自动化
- `PetSettingsStore` 的默认值、原子写入、损坏隔离和版本校验(沿用
`settings-file-utils.ts` 现有测试模式)。
- `resolvePetPosition` 在显示器存在/不存在/坐标越界三种输入下的返回值。
- `PetStatusProjector` 按第 3 节优先级表在各种 Task 组合输入下的输出。
- `PetNotificationCoordinator` 与 `desktop-notification.ts` 共享判定函数在“可见并聚焦”
“不可见”“安静模式开启”组合下只触发一条路径或都不触发。
- `ContextManager.stageFromPaths`(新增方法)对合法文件、超限文件、不支持类型、文件夹
路径的分支覆盖,复用现有 `context-manager` 测试基础设施。
- `pet:*` IPC handler 的 trusted sender、schema 校验和错误码。
- 快速起草提交后主窗口渲染进程状态更新的单元测试:目标会话激活、文本写入、附件追加、
草稿被丢弃时不产生 Task 或消息记录。
### 14.2 真实环境
- Windows、macOS、Linux 至少各一次验证宠物窗口创建、拖动、贴边吸附、透明或回退背景的
实际渲染效果。
- 真实拖放一个合法文件、一个超限文件、一个文件夹到宠物窗口,确认 UI 设计第 4.3 节的
反馈和最终主窗口状态符合预期。
- 真实触发一次任务完成和一次任务失败,在“主窗口隐藏”“主窗口可见”“安静模式开启”
三种组合下确认气泡和原生通知的出现符合第 10 节的互斥规则。
- 多显示器插拔场景下确认位置回退逻辑(断开记忆显示器后宠物仍可见)。
- 打包后的应用在六个标准目标上完成启用、拖动、快速起草、退出流程的一次冒烟验证。
设计文档完成不代表功能实施或上述验证通过。
+207
View File
@@ -0,0 +1,207 @@
# 桌面宠物 UI 设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
本文定义设置中的“桌面宠物”页面和独立宠物窗口的界面。所有颜色、间距、字体、通知、
Switch/Checkbox、焦点和响应式规则以全局设计系统为准;宠物窗口是应用内唯一不使用标准
`PageShell` 的持久界面,其例外范围在第 4 节明确限定。
## 1. 设置页面信息架构
设置中心新增“桌面宠物”一级分类,使用 `standard` PageShell,不使用页签,按以下顺序
纵向排列:
```text
桌面宠物
在桌面上保留一个轻量入口,随时起草消息、拖入文件或查看任务状态
启用桌面宠物 [Switch 关]
关闭后不在桌面上显示任何宠物窗口或提示
—— 以下设置仅在启用后可编辑,保持可见但降低强调度 ——
外观
角色 [分段选择:三到四个内置角色缩略图]
尺寸 [滑块 小 · 中 · 大]
透明度 [滑块]
动画强度 [滑块 关闭 · 轻量 · 完整]
[恢复默认外观]
位置与窗口
始终置顶 [Switch 开]
贴边吸附 [Switch 开]
提示
安静模式(不显示完成 / 失败气泡) [Switch 关]
```
- 总开关是本页唯一决定宠物窗口存在与否的控件,符合 `INV-3`;其余控件在总开关关闭时
保持可见但不可交互,说明文字保持“关闭后不在桌面上显示任何宠物窗口或提示”。
- 二元持久状态一律使用共享 Switch;角色选择使用共享 `SegmentedControl`(3-4 个内置
角色,符合数量上限);尺寸、透明度和动画强度使用标准滑块控件并显示当前取值。
- “恢复默认外观”只重置本区域三项外观设置,不改变总开关、位置或安静模式,与
[功能逻辑](./logic-design.md) 中“恢复默认外观”的范围一致。
- 页面不提供“立即显示 / 隐藏”按钮:暂时隐藏的入口固定放在托盘菜单,避免在设置页和
托盘中出现两个含义相近但状态不同步的控件。
## 2. 首次启用与关闭反馈
- 开启总开关后宠物窗口在默认位置出现,不产生任何应用通知或原生通知,因为这是用户刚
完成的显式操作,结果已经可见。
- 关闭总开关后宠物窗口和任何气泡立即消失;设置页本身不产生额外通知。
- 外观、位置相关 Switch 和滑块的变更立即生效并持久化,失败时通过应用通知报告,不静默
丢弃用户输入。
## 3. 托盘入口
- 托盘菜单在“新建对话”与“设置”之间新增一项,文案随当前可见性变化:
- 已启用且可见:“隐藏桌面宠物”。
- 已启用但隐藏:“显示桌面宠物”。
- 未启用:“显示桌面宠物”,点击后等同于在设置中开启总开关并显示窗口。
- 该菜单项不复制设置页的开关语义,只切换可见性;未启用状态下点击才会连带开启总开关,
行为需要在可访问名称或菜单说明中明确,避免用户误以为它只是显示/隐藏。
## 4. 宠物窗口
宠物窗口是无边框、透明背景、不可缩放的持久悬浮窗口,不使用 `PageShell`、不属于标准
一级页面,因此以下规则专属于该窗口:
```text
┌───────────┐
│ 角色 │ ← 状态镜面:角色外观随任务状态切换
│ (idle) │
└───────────┘
```
- 窗口本身没有标题栏、边框或系统窗口控件;关闭宠物只能通过设置总开关、托盘“隐藏桌面
宠物”或退出应用完成,窗口自身不提供关闭按钮,避免与“暂时隐藏”“关闭功能”两种含义
混淆。
- 窗口整体可拖动;拖动过程中显示当前吸附意图(开启贴边时,接近边缘会有轻微吸附反馈)。
- 窗口右键菜单提供“快速起草”“暂时隐藏”“打开设置”“退出 GoodBuddy”,与托盘菜单保持
同一套文案。
### 4.1 状态镜面外观
宠物角色在以下六种取值间切换,取值定义见[功能逻辑](./logic-design.md) 第 3 节:
| 状态 | 视觉表达 | 可访问名称 |
| --- | --- | --- |
| `unavailable` | 角色显示为暗淡的问号叠加 | “任务状态当前不可用” |
| `idle` | 角色静止或轻微待机动作 | “没有进行中的任务” |
| `running` | 角色显示持续的工作动作 | “有任务正在执行” |
| `waiting_approval` | 角色举牌或举手动作,带醒目但非警示色的强调 | “有任务等待你的确认” |
| `completed_recent` | 角色短暂展示完成动作后回到待机 | “有任务刚刚完成” |
| `failed_recent` | 角色显示明显区别于完成的姿态,并叠加警示色边缘 | “有任务失败,需要查看” |
- 状态表达必须同时使用动作/姿态和可访问名称两种信号,不能只依赖颜色区分
`completed_recent` 与 `failed_recent`。
- `prefers-reduced-motion` 或用户将动画强度设为“关闭”时,状态切换只改变静态姿态和颜色
强调,不播放位移或缩放动画。
- 点击状态镜面区域(角色本体)触发 US-D4:显示并聚焦主窗口,Task Center 定位到触发
当前状态的任务;`idle` 和 `unavailable` 状态下点击等同于显示并聚焦主窗口,不做定位。
### 4.2 快速起草弹出框
点击宠物打开的弹出框锚定在宠物窗口旁(自动避开屏幕边缘),结构如下:
```text
发送到:[当前会话名称 v]
[多行文本输入,占位文案“输入消息,或拖入文件…”]
[📎 附加文件] [已附加:contract.docx ×]
[取消] [提交]
```
- “发送到”是一个富信息单选菜单,选项为当前活跃会话与“新建对话”,遵循全局设计系统
第 6.11 节的单选选择器规则;默认选中当前活跃会话。
- 文本输入支持多行、`Ctrl+V` 粘贴图片和文字,与主窗口聊天输入框的粘贴行为一致。
- 附加文件按钮打开原生文件选择;已附加文件以行内标签形式展示,可单独移除。
- 拒绝的文件(超限或不支持)在附件区下方以就地错误文字展示,不使用应用级通知,因为
这是与当前操作直接绑定的字段级反馈。
- “提交”把文本和附件转交目标会话的输入区草稿,随后关闭弹出框;提交过程中禁用重复
点击并显示进行中状态,成功后不额外弹出通知(主窗口的显现和草稿本身就是反馈)。
- “取消”或点击弹出框外部或按 Escape 关闭弹出框并丢弃草稿,焦点返回宠物窗口。
- 弹出框打开时焦点进入文本输入;Tab 顺序为文本输入 → 附加文件 → 已附加文件的移除
按钮 → 取消 → 提交;关闭后焦点回到宠物窗口本体。
### 4.3 拖放反馈
- 文件或文字拖入宠物窗口范围时,窗口整体出现可放置的高亮边框和轻微放大反馈。
- 释放后立即按第 4.2 节“提交”的相同路径处理,不额外弹出确认框;这保持“拖动=起草并
发送到会话输入区(而非立即发送消息)”的一致语义。
- 拖入文件夹或全部文件超限时,宠物短暂显示拒绝反馈(边框变为警示色并伴随简短文字
提示,例如“不支持文件夹”),不打开主窗口、不产生暂存。
- 部分文件超限、部分合法时,合法文件正常处理,超限文件的名称通过一条会在数秒后自动
消失的就地提示列出,不使用应用级通知(拖放是即时操作,反馈应贴近操作发生的位置)。
### 4.4 完成 / 失败气泡
```text
┌─────────────────────────────┐
│ ✓ 已完成 │
│ 合同风险检查 │
│ [×] │
└─────────────────────────────┘
▲
(角色)
```
- 气泡锚定在宠物窗口旁,方向随宠物在屏幕上的位置自动避开边缘。
- 完成气泡在约 4.5 秒后自动消失,与全局应用通知的成功/信息时长一致;失败气泡保持可见
直到用户点击查看或点击关闭按钮。
- 气泡本体可点击,等同于点击状态镜面:显示并聚焦主窗口并定位到对应任务。
- 关闭按钮 `[×]` 有独立可访问名称(“关闭提示”),不与气泡主体的点击目标冲突:点击
关闭按钮只关闭气泡,不导航到主窗口。
- 气泡使用 `status`/polite 实时区域播报完成,失败气泡使用 `alert`/assertive,与全局
设计系统的通知语义保持一致,即使气泡本身不是应用通知视口的一部分。
## 5. 与应用通知视口的关系
- 宠物气泡是应用通知视口的窗口外等价物,只在主窗口不可见/未聚焦且安静模式关闭时出现;
同一事件不会同时进入应用通知视口和宠物气泡,具体归并规则见
[功能逻辑](./logic-design.md) 第 4 节。
- 快速起草和拖放产生的字段级错误(附件超限、不支持类型)始终就地展示在宠物窗口或弹出
框内,不进入应用通知视口,保持“就地错误”与“全局通知”分工一致。
## 6. 无障碍
- 状态镜面、气泡和弹出框内的所有交互元素都有持久可访问名称,不依赖占位文案代替标签。
- 宠物窗口可通过系统级窗口切换获得焦点;获得焦点后:
- 预期的激活键(例如 Enter 或 Space)打开快速起草弹出框。
- 弹出框内 Tab 顺序遵循第 4.2 节。
- Escape 关闭当前打开的弹出框或气泡,焦点回到宠物窗口本体。
- 状态镜面的状态变化通过 `status`/polite 实时区域播报简短说明,避免因动画本身无法被
屏幕阅读器感知而丢失信息;不对每次动画帧重复播报。
- 焦点环、危险色使用和触控目标尺寸遵循全局设计系统第 11 节的通用要求。
## 7. 响应式与多显示器
- 宠物窗口尺寸不随主窗口大小变化;用户通过设置中的“尺寸”滑块单独控制。
- 宠物窗口位置按显示器分别记忆;显示器不可用时的回退规则见
[功能逻辑](./logic-design.md) 第 7 节,界面表现为宠物出现在主显示器的默认可见位置,
不出现在屏幕之外或不可见状态。
- 弹出框和气泡的锚定方向根据宠物当前位置与屏幕边缘的距离自动选择展开方向(上/下/左/
右),确保始终完整可见,不被裁切到屏幕外。
## 8. 深浅主题
- 弹出框、气泡和设置页控件复用全局语义令牌,不新增页面专属颜色。
- 宠物角色素材本身分别提供适配浅色和深色桌面背景的版本,避免透明背景下角色轮廓在深色
桌面壁纸上难以辨认;具体资源交付方式属于技术设计范畴。
## 9. 固定词汇
使用“桌面宠物”“启用桌面宠物”“暂时隐藏”“快速起草”“安静模式”“状态镜面”,定义见
[README](./README.md) 术语表。不使用“挂件”“桌宠助手”或其他未在术语表中定义的别名,
保持中文文案统一。
+255
View File
@@ -0,0 +1,255 @@
# 桌面宠物 User Stories
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
## 角色
- **日常用户**:在多任务、多显示器场景下使用 GoodBuddy 处理日常工作。
- **首次体验用户**:刚升级或安装 GoodBuddy,尚未接触过桌面宠物功能。
- **键盘用户**:主要通过键盘操作,可能同时使用屏幕阅读器。
- **多显示器用户**:使用两个或更多显示器,可能随时插拔外接显示器。
## US-A 首次启用
### US-A1 默认不出现
**Given** 用户全新安装或刚升级 GoodBuddy
**When** 用户尚未进入“桌面宠物”设置
**Then** 桌面上不出现任何宠物窗口,托盘菜单显示“显示桌面宠物”而非“隐藏”
### US-A2 首次启用
**Given** 用户在设置中打开“启用桌面宠物”总开关
**When** 开关切换为开启
**Then** 宠物窗口在默认位置出现,使用默认外观,且不产生任何应用级或原生通知
### US-A3 关闭后完全消失
**Given** 桌面宠物已启用并可见
**When** 用户关闭“启用桌面宠物”总开关
**Then** 宠物窗口立即消失,未完成的气泡提示一并消失,托盘菜单条目恢复为“显示桌面宠物”,
且用户之前设置的位置、外观和提示偏好被保留供下次开启时使用
## US-B 快速起草
### US-B1 起草文字并转入主窗口
**Given** 桌面宠物已启用,主窗口当前隐藏
**When** 用户点击宠物并输入一段文字后点击提交
**Then** 主窗口被显示并聚焦,目标会话的输入区出现刚输入的文字作为草稿,输入焦点落在
该输入区,且没有任何消息被发送、没有任何任务被创建
### US-B2 起草时附加文件
**Given** 快速起草弹出框已打开
**When** 用户点击“附加文件”并选择一个受支持类型和大小内的文件
**Then** 该文件在弹出框内显示为待提交附件;提交后该附件出现在主窗口对应会话输入区的
待发送附件列表中
### US-B3 附加不受支持的文件
**Given** 快速起草弹出框已打开
**When** 用户选择一个超出大小限制或类型不受支持的文件
**Then** 弹出框就地显示明确的拒绝原因,不添加该文件,且不影响已经添加的其他文字或附件
### US-B4 取消起草
**Given** 用户已在快速起草弹出框中输入文字或添加附件
**When** 用户点击取消或按 Escape 关闭弹出框
**Then** 弹出框关闭且草稿被丢弃,主窗口任何会话的现有草稿不受影响,且不创建任何草稿或
任务记录
### US-B5 选择目标会话
**Given** 主窗口当前存在多个会话,且用户在快速起草弹出框中看到默认目标会话
**When** 用户在弹出框内切换目标为“新建对话”
**Then** 提交后主窗口创建并激活一个新会话,草稿内容进入该新会话的输入区,不影响原有
会话的草稿
## US-C 拖放暂存
### US-C1 拖放单个文件
**Given** 桌面宠物已启用并可见
**When** 用户从文件管理器拖动一个受支持类型的文件到宠物窗口上并释放
**Then** 宠物在拖入过程中显示可放置的视觉反馈,释放后主窗口被显示并聚焦,该文件出现在
目标会话输入区的待发送附件列表中
### US-C2 拖放多个文件,部分超限
**Given** 用户同时拖动数量或大小超出限制的多个文件到宠物窗口
**When** 用户释放拖放
**Then** 合法文件正常进入目标会话的待发送附件列表,超限或不支持的文件被单独列出拒绝
原因,不阻止合法文件被暂存
### US-C3 拖放文件夹
**Given** 用户拖动一个文件夹到宠物窗口
**When** 用户释放拖放
**Then** 宠物提示不支持文件夹,不递归展开文件夹内容,也不产生任何暂存
### US-C4 拖放选中文字
**Given** 用户在其他应用中选中一段文字并拖动到宠物窗口
**When** 用户释放拖放
**Then** 该文字进入目标会话输入区的草稿文本,行为与快速起草输入文字一致
## US-D 状态镜面
### US-D1 查看空闲状态
**Given** 当前没有任何活跃 Task
**When** 用户查看宠物外观
**Then** 宠物显示空闲状态,且可访问名称明确说明当前没有进行中的任务
### US-D2 查看执行中状态
**Given** 有一个 Task 处于执行中
**When** 用户查看宠物外观
**Then** 宠物切换为执行中状态,且该状态与 Task Center 中该任务的状态一致
### US-D3 多任务状态优先级
**Given** 同时存在一个等待处理的任务和一个执行中的任务
**When** 用户查看宠物外观
**Then** 宠物按照“等待处理优先于执行中”的规则显示等待处理状态
### US-D4 点击状态镜面打开任务详情
**Given** 宠物当前显示执行中或失败状态
**When** 用户点击宠物的状态镜面区域
**Then** 主窗口被显示并聚焦,Task Center 定位到触发当前状态的具体任务
### US-D5 状态不可用
**Given** 用于确认任务状态的 Runtime 或网络当前不可用
**When** 用户查看宠物外观
**Then** 宠物显示“状态不可用”,不回退显示为空闲或执行中
## US-E 完成与失败提示
### US-E1 主窗口不可见时收到完成提示
**Given** 主窗口当前隐藏或未聚焦,安静模式未开启
**When** 一个任务从执行中变为已完成
**Then** 宠物旁出现说明任务标题和结果的气泡,且该事件不重复触发原生系统通知
### US-E2 失败提示保持可见
**Given** 主窗口当前隐藏,安静模式未开启
**When** 一个任务失败
**Then** 宠物旁出现失败气泡并保持可见,直到用户点击查看或手动关闭,不自动消失
### US-E3 主窗口可见时不重复提示
**Given** 主窗口当前可见并聚焦
**When** 一个任务完成或失败
**Then** 不出现宠物气泡;反馈通过主窗口现有机制呈现,避免同一事件重复出现在两个位置
### US-E4 开启安静模式
**Given** 用户在设置中开启“安静模式”
**When** 之后任意任务完成或失败,且主窗口不可见
**Then** 不出现任何宠物气泡,但状态镜面外观仍随任务状态正常变化
## US-F 位置、外观与多显示器
### US-F1 记忆位置
**Given** 用户把宠物拖动到某个显示器的特定位置
**When** 用户重启 GoodBuddy
**Then** 宠物出现在同一显示器的同一位置
### US-F2 显示器断开后回退
**Given** 宠物位置记忆在一个已插入的外接显示器上
**When** 该外接显示器被移除后重新启动 GoodBuddy
**Then** 宠物出现在当前可用的主显示器上一个可见、不越界的位置,而不是不可见或位于屏幕
之外
### US-F3 贴边吸附
**Given** 用户开启“贴边”选项
**When** 用户把宠物拖动到接近屏幕边缘的位置并释放
**Then** 宠物自动吸附到屏幕边缘,且吸附后仍可被再次拖动离开
### US-F4 调整外观
**Given** 用户在设置中调整宠物尺寸、透明度或动画强度
**When** 用户改变任意一项取值
**Then** 宠物外观立即按新取值更新,且设置持久化,重启后保持
### US-F5 恢复默认外观
**Given** 用户已经修改过角色、尺寸、透明度或动画强度
**When** 用户点击“恢复默认外观”
**Then** 上述设置全部恢复为默认值,宠物位置和总开关状态不受影响
## US-G 暂时隐藏与托盘
### US-G1 暂时隐藏
**Given** 桌面宠物已启用并可见
**When** 用户从托盘菜单选择“隐藏桌面宠物”
**Then** 宠物窗口消失,总开关状态和其他设置不变,托盘菜单条目变为“显示桌面宠物”
### US-G2 重新显示
**Given** 桌面宠物已启用但当前隐藏
**When** 用户从托盘菜单选择“显示桌面宠物”
**Then** 宠物窗口在此前记忆的位置重新出现
### US-G3 应用退出
**Given** 桌面宠物已启用并可见
**When** 用户退出 GoodBuddy
**Then** 宠物窗口一并关闭,不留下任何独立运行的进程或残留窗口
## US-H 键盘与无障碍
### US-H1 键盘打开快速起草
**Given** 桌面宠物窗口通过系统级窗口切换取得焦点
**When** 键盘用户按下预期的激活键
**Then** 快速起草弹出框打开,焦点进入输入框
### US-H2 键盘关闭弹出框
**Given** 快速起草弹出框已打开且有键盘焦点
**When** 用户按下 Escape
**Then** 弹出框关闭,焦点回到宠物窗口本身
### US-H3 屏幕阅读器状态说明
**Given** 用户使用屏幕阅读器
**When** 焦点移动到宠物的状态镜面
**Then** 屏幕阅读器播报当前状态的文字说明,不仅依赖颜色或动画表达状态
## US-I 兼容与边界
### US-I1 升级用户保持现状
**Given** 已在使用 GoodBuddy 且从未接触过桌面宠物功能的用户升级到包含该功能的版本
**When** 用户正常启动应用
**Then** 桌面宠物保持关闭,界面和行为与升级前一致
### US-I2 不支持透明合成的环境
**Given** 用户的操作系统或桌面环境不支持窗口透明合成
**When** 用户启用桌面宠物
**Then** 宠物窗口退化为不透明的小型窗口而不是渲染异常或无法显示,具体规则见
[技术设计](./technical-design.md)
### US-I3 拖放到非目标区域
**Given** 用户尝试把文件拖到宠物窗口之外的桌面区域
**When** 用户释放拖放
**Then** GoodBuddy 不接收该拖放,不产生任何暂存或错误提示
+28
View File
@@ -0,0 +1,28 @@
# 设备发现与共享
本功能是 GoodBuddy 桌面端管理附近设备、本机共享和 ShareServer 连接的唯一入口。默认连接
方式是局域网;网关由用户额外添加。功能不包含 ShareServer 的组织管理 Web 界面。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、范围、功能需求和产品验收 |
| [功能逻辑](./logic-design.md) | 不变量、状态、授权优先级和失败结果 |
| [UI 设计](./ui-design.md) | 设置入口、四个页签、配对与调用交互 |
| [技术设计](./technical-design.md) | Desktop 进程边界、持久化、IPC、网络和验证 |
| [共享网络总体设计](../../architecture/share-network-architecture.md) | 跨端身份、能力、授权、任务、更新和协议权威 |
当前没有单独的 User Stories:正常、失败、撤销、权限和兼容场景已直接进入 PRD 验收与逻辑
决策表。功能尚未实施,因此不创建 `progress.md`。
## 术语
- **附近设备**:同一局域网内主动广播 GoodBuddy 共享服务的设备。
- **已配对设备**:双方确认设备指纹后建立点对点信任的设备。
- **网关连接**:本机与某个 ShareServer 中特定组织身份的连接。
- **本机共享**:由本机作为提供者发布给 Peer 或组织的能力。
- **远端能力**:由其他设备或组织提供、本机可以查看或调用的能力。
SSH 远程项目是用户明确配置 Host 后操作远端工作区的独立功能,不自动进入设备发现与共享,
详见[远程主机与远程执行](../remote-host/README.md)。
@@ -0,0 +1,202 @@
# 设备发现与共享功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
| 共享规则 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
本文负责桌面端状态、不变量和决策规则,不重复跨端协议定义。
## 1. 核心不变量
### INV-1 连接方式独立
局域网、每个 GatewayConnection 和本机能力互不拥有生命周期。启停、失败或删除其中一项不
改写其他项。
### INV-2 三项操作独立
`discovering`、`advertising` 和 `publishing` 是三个独立状态。任何组合均合法,界面不得用
一个“设备共享”总开关代替。
### INV-3 默认不共享
新安装没有 Publication。允许被发现只公开配对入口,不公开能力清单。
### INV-4 身份先于能力
未配对局域网设备不能取得详细能力;未注册网关设备不能取得组织目录。缓存不能绕过当前
身份和撤销状态继续调用。
### INV-5 提供者在调用前冻结
自动选择只生成候选决定。用户确认或无需确认的请求创建后,provider、route 和授权快照不可
改变。失败后切换提供者属于新的请求。
### INV-6 最小发送上下文
只发送能力 schema 要求且用户当前选择的输入。当前 Conversation、Project 和 Workspace 不
因能力被调用而整体成为输入。
### INV-7 结果未知不重放
已经被提供者接受但终态无法确认的副作用任务进入 `outcome_unknown`。重新执行必须由用户
发起,并显示可能重复产生副作用。
### INV-8 撤销只影响未来授权
撤销配对、网关连接、Grant 或 Publication 后不再创建新任务。历史记录保留;活动任务按
冻结策略取消或完成,不能追改为从未发生。
### INV-9 设置导航无业务副作用
四个 PageTabs 只切换视图,不自动开始广播、连接网关、发布能力或保存草稿。
## 2. 状态维度
### 2.1 局域网服务
```text
discover: off | starting | active | unavailable | error
advertise: off | starting | active | unavailable | error
```
操作系统或网络不支持时使用 `unavailable`;运行失败使用 `error`。两者分别重试。
### 2.2 附近设备
```text
visible_unpaired -> pairing -> paired_online
pairing -> rejected | expired | failed
paired_online <-> paired_offline
paired_* -> identity_changed | revoked
```
`identity_changed` 不自动接受新证书。用户必须撤销旧配对后重新配对。
### 2.3 网关连接
```text
draft -> authenticating -> connecting -> connected
authenticating -> cancelled | auth_failed
connecting/connected -> offline | auth_expired | policy_blocked
offline -> connecting
任意已保存状态 -> removed
```
`offline` 保留连接和缓存目录供只读说明,但不能创建新任务;`auth_expired` 要求重新登录;
`policy_blocked` 显示服务端给出的稳定原因和管理员联系路径。
### 2.4 本机发布
```text
not_published -> draft -> validating -> published
validating -> invalid
published -> paused -> published
published/paused -> removed
```
草稿和无效发布不对外可见。能力实现消失时发布进入 `paused`,不删除用户配置。
### 2.5 远端任务
采用共享架构定义的状态。本机 UI 归并为:
| 协议状态 | 用户状态 |
| --- | --- |
| `created/awaiting_approval` | 等待确认 |
| `queued/connecting` | 正在连接 |
| `running` | 正在执行 |
| `completed` | 已完成 |
| `failed/rejected/expired` | 未完成,并显示具体原因 |
| `cancelled` | 已取消 |
| `outcome_unknown` | 结果未知 |
## 3. 默认状态
| 设置 | 全新安装默认值 |
| --- | --- |
| 发现附近设备 | 开启 |
| 允许其他设备发现本机 | 关闭 |
| 自动接受配对 | 不支持 |
| 本机共享能力 | 无 |
| 网关连接 | 无 |
| 永久调用 Grant | 无 |
主动发现默认开启只浏览 GoodBuddy 主动广播,不执行端口扫描。操作系统权限尚未授予时,首次
进入页面再触发对应系统说明和授权,不在应用启动时抢占焦点。
## 4. 提供者选择
自动选择按以下顺序比较满足当前输入和授权的在线候选:
1. 本机提供者。
2. 已配对局域网提供者。
3. 当前用户所属组织提供者。
4. 联邦外部组织提供者。
5. 同级中按服务端预计排队时间和稳定能力版本排序。
自动选择不跨越 `require_approval` 与 `allow`:若排名第一的候选需要批准,界面显示该候选及
批准要求,不静默换成权限更宽但来源更远的候选。用户可以明确选择其他候选。
## 5. 调用决策表
| 身份与能力状态 | 授权结果 | 行为 |
| --- | --- | --- |
| 身份有效、输入匹配 | allow | 创建任务并展示执行位置 |
| 身份有效、输入匹配 | require_approval | 显示完整确认后创建 |
| 身份有效 | deny | 不创建任务,显示策略原因 |
| 提供者离线 | 任意 | 不创建任务,允许重新选提供者 |
| 清单过期或版本变化 | 任意 | 刷新后重新计算,不沿用确认 |
| 文件超过能力或策略上限 | 任意 | 就地拒绝并显示上限 |
| 委托链与确认不一致 | 任意 | 拒绝并要求以新执行链重新确认 |
## 6. 撤销与删除
| 操作 | 删除内容 | 保留内容 |
| --- | --- | --- |
| 撤销 Peer | 配对凭据、该 Peer 的本地 Grant 和目录缓存 | 活动记录、已安装包 |
| 移除网关 | 本机设备凭据、连接配置、目录缓存 | 服务端账号、活动记录、已安装包 |
| 停止发布 | 对外 Publication | 能力实现、发布草稿、历史任务 |
| 删除发布 | Publication 和本地发布设置 | 能力实现、历史任务 |
| 删除已安装包 | GoodBuddy-owned 安装内容和分配 | 来源审计与任务历史 |
存在活动任务时,撤销操作明确询问是否同时发送取消;无论取消是否确认,立即禁止新任务。
## 7. 失败与恢复
- mDNS 不可用:显示手动地址配对,不关闭已配对直连。
- 广播失败:保留用户开关意图,状态为错误并允许重试。
- 网关离线:指数退避重连,用户手动“重新连接”可立即尝试一次。
- 登录取消:不保存连接。
- 目录刷新失败:保留最后缓存并标记时间,不允许基于过期授权启动新任务。
- 调用前连接失败:任务失败,可安全重新执行。
- 提供者接受后断线:恢复查询同一任务;仍无法确认时进入结果未知。
- 下载或签名失败:删除候选暂存,保留现有安装,不切换来源。
## 8. 需求追踪
| 需求 | 主要逻辑 |
| --- | --- |
| `FR-1`、`FR-2` | `INV-1`、`INV-2`、`INV-9` 和默认状态 |
| `FR-3`、`FR-4` | 身份不变量和附近设备状态机 |
| `FR-5` | 连接独立不变量、网关连接状态和删除规则 |
| `FR-6` | provider 冻结不变量和提供者选择顺序 |
| `FR-7` | 默认不共享不变量、本机发布状态和撤销规则 |
| `FR-8` | 最小发送上下文和调用决策表 |
| `FR-9` | 远端任务状态、结果未知不重放和失败恢复 |
| `FR-10` | 撤销与删除、下载或签名失败规则;包协议由共享架构定义 |
| `FR-11` | 撤销后历史保留和任务状态;展示行为由 UI 设计定义 |
| `FR-12`、`FR-13` | 独立状态维度和失败恢复 |
| `FR-14` | 身份先于能力、最小发送上下文和调用决策表 |
## 9. 逻辑完整性
本设计已覆盖发现、配对、连接、发布、调用、取消、撤销、离线、身份变化、权限变化和能力
更新。实现前仍需在共享协议中固定受限 JSON Schema 子集、端到端中继加密套件和桌面端可
注册为共享能力的首批实际类型;这些是技术选择,不改变本文状态和产品规则。
+160
View File
@@ -0,0 +1,160 @@
# 设备发现与共享 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 适用产品 | GoodBuddy Desktop |
| 关联架构 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
| 关联文档 | [功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md) |
## 1. 背景
GoodBuddy 的能力当前主要在本机执行,SSH 远程项目则要求用户预先配置具体 Host。个人和组织
需要在不复制完整环境的情况下使用附近工作站、其他成员设备或组织节点上的模型、知识、硬件
和工具,也需要在经过确认后把可分发能力安装到本机。
该能力不能以“扫描局域网机器”的方式实现。产品只发现主动开放 GoodBuddy 服务的设备,
配对后才查看详细能力,授权后才发送数据或执行任务。
## 2. 产品目标
1. 用户无需配置服务器即可发现并配对同一局域网中的 GoodBuddy 设备。
2. 用户可以额外连接一个或多个 ShareServer,使用组织或合作组织提供的能力。
3. 用户能够逐项决定本机向哪些 Peer、用户组或组织开放什么能力。
4. 远程调用前、中、后都能确认提供者、执行位置、数据去向和授权依据。
5. 局域网、各网关连接和本机能力彼此独立,单点故障不阻塞其他来源。
6. 可分发能力必须在明确校验和批准后安装,不迁移对方凭据或机器配置。
## 3. 功能范围
### 3.1 包含
- 设置中的“设备发现与共享”一级分类。
- 局域网主动发现、选择、双端配对、重命名和撤销。
- 本机被发现开关和配对请求处理。
- ShareServer 添加、登录、连接状态、组织身份和移除。
- 远端能力目录、提供者选择、调用确认、进度、取消和结果。
- 本机能力逐项发布、范围、允许动作和确认策略。
- 签名能力包查看、下载、校验、安装和权限变化确认。
- 局域网与网关活动记录。
### 3.2 不包含
- 扫描未主动广播 GoodBuddy 服务的机器或端口。
- 在桌面端管理 ShareServer 成员、组织策略、联邦或服务器运维。
- 自动公开本机 Skill、MCP、知识库、模型、文件或 SSH Host。
- 同步 API Key、Cookie、系统环境变量、绝对路径或登录状态。
- 把现有 SSH Host 自动转换为共享设备。
- 无界远程 Shell、桌面控制或任意文件浏览接口。
- 在网关断开时把组织授权自动降级为局域网授权。
## 4. 功能需求
### FR-1 设置入口
- 设置中心增加“设备发现与共享”一级分类。
- 页面提供“局域网”“网关”“本机共享”“活动记录”四个同级页签。
- 普通入口默认打开“局域网”;定向入口可打开指定页签。
### FR-2 独立开关
- “发现附近设备”和“允许其他设备发现本机”使用两个独立 Switch。
- 任一开关变化不撤销既有配对、不改变网关连接、不开放能力。
- 新安装默认允许主动发现;默认不允许本机被发现,默认无共享能力。
### FR-3 附近设备
- 只展示当前收到有效主动广播的 GoodBuddy 设备。
- 未配对设备只展示配对所需的有限信息,不展示能力和资产详情。
- 列表区分附近未配对、已配对在线、已配对离线和身份变化。
- 用户可手动刷新,但持续发现不得因刷新造成列表闪烁或重复项。
### FR-4 双端配对
- 双方显示相同验证码、设备名称、系统类型和证书指纹。
- 只有双方在时限内确认才建立配对。
- 任一端拒绝、超时、身份变化或连接中断均不保存半配对关系。
- 任一端可以撤销;撤销不删除历史活动记录。
### FR-5 网关连接
- 用户可输入 HTTPS ShareServer 地址或打开受支持的邀请链接。
- 登录在系统浏览器中完成,设备凭据回到 Main 的安全存储。
- 连接条目显示服务名、组织、当前成员身份、状态和最近策略同步时间。
- 同一设备可连接多个网关;移除连接只删除本机凭据和缓存,不删除服务端账号。
### FR-6 能力目录
- 已配对 Peer 和已连接网关分别返回当前身份获准查看的能力。
- 同一能力有多个提供者时合并展示,并允许“自动选择”或明确选择提供者。
- 自动选择仅比较满足授权和输入要求的在线候选,依次选择本机、已配对局域网 Peer、
当前用户所属组织和联邦外部组织;同级候选再按预计等待时间和稳定能力版本排序。
- 选择结果必须在执行前冻结并展示,不能执行中静默更换提供者。
### FR-7 本机共享
- 只有显式注册为可共享的能力才能出现在本机共享列表。
- 每项能力分别设置目标范围、`discover/inspect/invoke/download` 动作、输入类型、大小、
并发、有效期和是否逐次确认。
- 关闭能力或目标范围后停止接受新任务,不伪装为删除能力本身。
- 知识库原文、工作区写入、进程执行和硬件控制需要独立的明确能力声明。
### FR-8 调用确认
- 首次或策略要求时,确认界面显示提供者、执行位置、接收方、输入内容类别、文件名和大小、
路由、中继可读性及下游委托。
- 用户可取消、仅本次允许,或在组织策略允许时按当前范围保存授权。
- 调用上下文不隐式包含其他会话消息、整个工作区或未选择的知识库内容。
### FR-9 执行状态
- 会话或任务活动持续显示远端能力名称、来源、状态、发送字节和停止入口。
- 支持排队、连接、执行、完成、失败、取消、过期和结果未知。
- 网络中断且副作用结果无法确认时显示“结果未知”,不自动重试。
### FR-10 能力包
- 展示来源、版本、发布者、平台、大小、签名、权限和依赖。
- 下载和安装经过共享架构规定的校验流程。
- 新版本扩大权限时必须重新批准。
- 安装到本机后按现有 Skill/MCP/Runtime 分配规则启用,不因来自可信组织自动取得全部 Runtime。
### FR-11 活动记录
- 统一记录本机发出、远端发入、局域网、网关和跨组织活动。
- 支持按方向、来源、能力、结果和时间筛选。
- 详情显示授权依据、执行链、数据类别、大小、版本和终态,不默认保存正文。
### FR-12 错误隔离
- 局域网发现失败不影响网关和本机能力。
- 一个网关认证或连接失败不影响其他网关。
- 一个提供者离线时不自动改发给另一个提供者;用户重新执行时再选择。
- 页面保留可修复配置和准确错误,不以空状态代替失败状态。
### FR-13 平台与网络
- 支持 Windows、macOS、Linux 的当前桌面目标架构。
- mDNS 被网络阻断时允许通过已知局域网地址发起配对,但仍执行相同身份确认。
- 所有公网 ShareServer 使用 HTTPS;不提供“忽略证书错误”。
### FR-14 与现有能力的边界
- 远端调用不能扩大 Ask/Execute、Workspace、MCP、Skill 和知识库现有权限。
- 本机提供任务时使用能力声明对应的 Runtime 边界。
- SSH 远程项目、本机工具环境和模型配置默认不向 Peer 或网关传播。
## 5. 产品验收
- 两台新设备能完成发现、双端配对、授权、调用、取消和撤销。
- 三个概念“主动发现、允许被发现、开放能力”在状态和界面上互不混淆。
- 不配置网关时局域网工作完整;任一网关故障不阻塞其他来源。
- 未配对或未授权主体无法查看详细能力。
- 远端调用前能准确列出实际发送的数据,执行中和历史中可追溯提供者。
- 结果未知不会被显示为失败后自动重试或成功。
- 权限扩大的能力更新不会沿用旧授权静默安装。
- 中英文、键盘、焦点、窄窗口和深浅主题符合统一设计系统。
@@ -0,0 +1,213 @@
# 设备发现与共享技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 共享协议 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
## 1. 进程边界
```text
Renderer
-> 窄 Preload API
Main
-> DeviceSharingService
-> LanDiscoveryService
-> PeerPairingService
-> GatewayConnectionService
-> CapabilityCatalogService
-> CapabilityPublicationService
-> RemoteTaskService
-> PackageAcquisitionService
-> SharingAuditStore
-> 既有 Capability / Runtime / Skill / MCP 服务
```
Renderer 只取得脱敏快照和调用显式动作。设备私钥、Peer 证书、网关凭据、网络 socket、原始
任务令牌、任意文件读取和包缓存路径只存在于 Main。
## 2. 模块职责
| 模块 | 职责 |
| --- | --- |
| `LanDiscoveryService` | mDNS browse/advertise、手动地址握手、接口变化 |
| `PeerPairingService` | 临时握手、验证码、双端确认、配对凭据与撤销 |
| `GatewayConnectionService` | 浏览器登录、设备注册、WebSocket、重连和策略同步 |
| `CapabilityCatalogService` | 合并本机、Peer 和网关目录,保留来源身份 |
| `CapabilityPublicationService` | 校验本机可共享能力并向目标范围发布 |
| `RemoteTaskService` | 授权、确认、provider 冻结、传输、取消和终态 |
| `PackageAcquisitionService` | 固定来源下载、校验、安装协调和权限差异 |
| `SharingAuditStore` | 本机有界审计查询、筛选和清理 |
目录合并只生成 UI 视图,不能把不同 provider 的授权、版本和执行位置压成一个身份。
## 3. 共享契约
实现前建立独立 `packages/share-protocol`。Desktop 只从该包导入 Zod schema 和 TypeScript
类型。协议输入在网络边界和 IPC 边界分别验证,不能因已经过网络校验而跳过 Renderer 输入
校验。
建议导出:
```text
DeviceSummary
PairingChallenge
GatewayDescriptor
CapabilityManifest
Publication
GrantConstraint
AuthorizationDecision
RemoteTaskSnapshot
RemoteTaskEvent
PackageDescriptor
AuditEventSummary
ProtocolError
```
错误只向 Renderer 返回稳定 code、可本地化参数和可恢复动作,不转发远端堆栈、响应正文或
内部地址。
## 4. 持久化
建议在现有 Desktop SQLite 中增加:
```text
device_identity_metadata
peer_pairings
gateway_connections
capability_publications
local_grants
remote_tasks
sharing_audit_events
installed_shared_packages
```
- 私钥和 refresh credential 存系统安全存储;SQLite 只存引用和脱敏元数据。
- 附近广播、在线状态、配对验证码、短期任务令牌和目录候选只存在于内存。
- 网关能力目录可有界缓存,用于离线说明,不作为离线调用授权。
- 审计正文不落库,字段和保留上限在实现前固定。
- 数据库迁移只覆盖已发布 schema;未发布设计不预建兼容读取器。
## 5. 局域网网络实现
- 使用系统支持的 mDNS/DNS-SD 库 browse `_goodbuddy-share._tcp.local`。
- 广播绑定用户允许的网络接口;接口切换时重建监听,不保留旧地址为身份。
- 服务端口由 Main 持有,使用设备证书建立 TLS。
- 未配对握手仅暴露配对所需字段,并限制并发、频率、消息大小和有效时间。
- 手动地址只替代发现,不替代证书指纹与双端确认。
- 停止广播时关闭未完成配对监听;已配对活动连接按任务生命周期处理。
## 6. 网关连接
1. Main 校验 HTTPS URL,并取得服务公开描述。
2. 生成带 PKCE、随机 state 和设备挑战的系统浏览器登录请求。
3. 本机 loopback 或已注册应用链接只接收一次性回调 code。
4. Main 交换设备凭据,展示组织身份,用户确认后持久化。
5. Main 建立带设备身份的 WebSocket 出站连接。
6. 心跳更新在线状态;断线按有界指数退避重连。
服务器返回的跳转 URL 必须符合已取得服务描述和 HTTPS 限制。Renderer 不能提交认证头、设备
证书、WebSocket URL 或任意回调地址。
## 7. 本机能力适配
不是所有现有 Capability 都自动可共享。每种可共享类型需要显式 adapter:
```ts
interface ShareableCapabilityAdapter {
describe(): CapabilityManifest
validatePublication(input: PublicationDraft): ValidationResult
invoke(input: unknown, context: ProviderTaskContext): AsyncIterable<TaskEvent>
cancel(taskId: string): Promise<CancelResult>
}
```
adapter 负责把共享输入映射到既有生产服务,并保留原权限边界。首批类型在实现前根据真实
产品需求确定;未有 adapter 的 Skill、MCP、知识库和 Runtime 不出现在共享列表。
## 8. 远端调用生命周期
```text
Renderer 选择能力和输入
-> Main 解析候选并取得当前授权
-> 必要时返回确认描述
-> 用户批准
-> Main 冻结 task/provider/route/policy
-> 建立数据通道并流式发送
-> Provider 再校验并接受
-> Main 转发有界进度和结果
-> 终态与审计事务保存
```
每次任务使用稳定 `taskId` 和 attempt ID。Provider 未接受前的连接失败可以明确失败;已接受
后的断线先查询同一任务,不新建 attempt。无法恢复权威终态时保存 `outcome_unknown`。
附件从 Main 已授权的用户选择结果读取,按声明大小流式发送,不把任意本机路径交给远端。
接收成果先写 GoodBuddy-owned 暂存并校验,再通过现有 Artifact 边界进入产品。
## 9. IPC
Preload 只暴露按产品动作命名的方法,例如:
```text
sharing.getSnapshot
sharing.updateLanSettings
sharing.startPairing / confirmPairing / cancelPairing
sharing.revokePeer
sharing.addGateway / completeGatewayLogin / removeGateway
sharing.listCapabilities
sharing.savePublication / pausePublication / deletePublication
sharing.prepareInvocation / confirmInvocation / cancelTask
sharing.listAuditEvents / getAuditEvent
```
所有变更方法校验 trusted sender、严格 schema、对象当前 revision 和归属关系。不得暴露通用
HTTP、WebSocket、mDNS、证书、文件传输或数据库查询 IPC。
## 10. 生命周期与资源边界
- 应用启动按保存设置启动主动发现;不因保存了网关而阻塞窗口创建。
- 广播只有在用户开启后启动。
- 网关连接在后台恢复,认证过期只影响对应连接。
- 应用退出停止接收新任务,取消未接受任务,并有界通知活动 Provider 后关闭连接。
- 单个 Peer/网关/任务设置连接数、帧、字节、并发和重连上限。
- 大文件全程流式处理,不进入单个 Main `Buffer` 或 Renderer state。
## 11. 与现有模块集成
- `capability-service` 提供本机能力事实;共享 adapter 明确选择可公开子集。
- 已安装共享 Skill/MCP 继续使用现有配置、启停和 Runtime 分配,不建立第二套执行器。
- SSH Host 保持独立;除非未来提供专门 adapter 和用户发布,否则不进入目录。
- 远端 Task 的公开活动复用现有工具/任务展示语义,但保留 provider 和 route 字段。
- 应用更新源、模型下载源、Agent 下载源和工具下载源不因添加 ShareServer 自动改变。
## 12. 验证策略
### 12.1 自动化
- schema 边界、未知枚举和协议版本协商。
- 三个独立开关及默认值。
- mDNS 去重、接口变化、广播停止和手动地址。
- 配对双方确认、拒绝、超时、身份变化和撤销。
- 多网关隔离、认证过期、重连和移除。
- provider 排序、冻结、授权变化和失败不切换。
- 调用确认最小输入、附件上限、取消和结果未知。
- Publication 校验、暂停、撤销和 adapter 消失。
- 包签名、权限差异、来源冻结和失败保留旧安装。
- IPC sender、revision、归属和无通用网络/文件接口。
### 12.2 真实环境
- Windows、macOS、Linux 至少两种组合完成同网段发现和配对。
- mDNS 可用、被防火墙阻断、网络接口切换和设备离线场景。
- 真实 ShareServer 登录、设备出站连接、目录、调用和认证过期。
- 小型 JSON、文件流、取消、Provider 进程失败和网络中断。
- 确认日志、Renderer、诊断和审计中没有凭据或文件正文。
设计文档完成不代表功能实施或上述验证通过。

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