From 39a457ded860dba98bcec8d2144ccf5b2e97d91b Mon Sep 17 00:00:00 2001
From: lofyer
Date: Wed, 5 Aug 2026 19:40:40 +0800
Subject: [PATCH] feat: prepare GoodBuddy 0.8.0
---
.github/workflows/packages.yml | 67 +-
BUILD.md | 35 +-
build/aggregate-release.cjs | 325 +++
docs/电脑控制开发进度.md | 22 +-
eslint.config.js | 16 +
package-lock.json | 129 +-
package.json | 4 +-
sites/README.md | 46 +
sites/app.js | 157 ++
sites/assets/favicon.svg | 12 +
sites/index.html | 538 ++++
sites/scripts/validate.mjs | 165 ++
sites/site.config.js | 5 +
sites/styles.css | 2231 +++++++++++++++++
src/main/agent/approval-summary.test.ts | 43 +-
src/main/agent/approval-summary.ts | 86 +-
src/main/agent/continue-host-adapter.test.ts | 12 +-
src/main/agent/continue-host-adapter.ts | 11 +-
src/main/agent/continue-runtime.test.ts | 44 +-
src/main/agent/continue-runtime.ts | 66 +-
src/main/agent/create-runtime.test.ts | 9 +-
src/main/agent/create-runtime.ts | 34 +
src/main/agent/model-runtime.test.ts | 28 +-
src/main/agent/model-runtime.ts | 8 +-
src/main/agent/model-tool-provider.test.ts | 9 +-
src/main/agent/opencode-runtime.test.ts | 30 +-
src/main/agent/opencode-runtime.ts | 38 +-
src/main/agent/runtime.ts | 2 +
src/main/assistant/assistant-database.test.ts | 88 +-
src/main/assistant/assistant-database.ts | 149 +-
src/main/assistant/heartbeat-database.test.ts | 2 +-
src/main/assistant/subagent-router.test.ts | 70 +
src/main/assistant/subagent-router.ts | 75 +
src/main/assistant/subagent-scheduler.test.ts | 54 +
src/main/assistant/subagent-scheduler.ts | 166 ++
src/main/assistant/subagent-service.test.ts | 110 +
src/main/assistant/subagent-service.ts | 262 ++
src/main/bounded-jpeg.test.ts | 45 +
src/main/bounded-jpeg.ts | 58 +
src/main/browser/browser-limits.ts | 2 +
src/main/browser/browser-model-tools.test.ts | 10 +-
src/main/browser/browser-model-tools.ts | 6 +-
src/main/browser/browser-screenshot.ts | 11 +
src/main/browser/browser-service.test.ts | 6 +-
src/main/browser/browser-service.ts | 6 +-
src/main/browser/cdp-browser-driver.test.ts | 285 ++-
src/main/browser/cdp-browser-driver.ts | 451 ++--
.../browser/electron-browser-session.test.ts | 25 +-
src/main/browser/electron-browser-session.ts | 52 +-
src/main/browser/filtering-proxy.test.ts | 107 +
src/main/browser/filtering-proxy.ts | 384 ++-
src/main/channels/channel-driver.ts | 154 ++
src/main/channels/channel-env.test.ts | 80 +
src/main/channels/channel-env.ts | 230 ++
src/main/channels/channel-service.test.ts | 345 +++
src/main/channels/channel-service.ts | 375 +++
.../channels/dingtalk-channel-driver.test.ts | 180 ++
src/main/channels/dingtalk-channel-driver.ts | 300 +++
src/main/channels/dingtalk-driver.test.ts | 325 +++
src/main/channels/dingtalk-driver.ts | 400 +++
.../channels/wechat-sidecar-protocol.test.ts | 155 ++
src/main/channels/wechat-sidecar-protocol.ts | 220 ++
.../channels/wecom-channel-driver.test.ts | 123 +
src/main/channels/wecom-channel-driver.ts | 146 ++
src/main/channels/wecom-driver.test.ts | 420 ++++
src/main/channels/wecom-driver.ts | 576 +++++
src/main/context-manager.test.ts | 146 +-
src/main/context-manager.ts | 109 +-
src/main/index.ts | 30 +-
src/main/ipc.test.ts | 383 ++-
src/main/ipc.ts | 340 ++-
.../knowledge/ollama-embedding-client.test.ts | 146 --
.../knowledge/openai-embedding-client.test.ts | 94 +
...g-client.ts => openai-embedding-client.ts} | 160 +-
src/main/runtime-settings-store.test.ts | 170 +-
src/main/runtime-settings-store.ts | 365 ++-
src/preload/index.ts | 12 +-
src/renderer/src/ActivityPanel.test.tsx | 32 +-
src/renderer/src/ActivityPanel.tsx | 5 +-
src/renderer/src/App.test.tsx | 411 ++-
src/renderer/src/App.tsx | 649 ++++-
.../src/RightAssistantSidebar.resize.test.tsx | 66 +-
src/renderer/src/RightAssistantSidebar.tsx | 120 +-
.../src/RolePromptSettingsSection.tsx | 90 +-
src/renderer/src/SettingsPanel.test.tsx | 157 +-
src/renderer/src/SettingsPanel.tsx | 260 +-
src/renderer/src/activity-store.test.ts | 27 +
src/renderer/src/activity-store.ts | 12 +-
src/renderer/src/styles.css | 383 ++-
src/shared/assistant-contracts.ts | 64 +-
src/shared/builtin-model-tools.ts | 2 +-
src/shared/channel-contracts.ts | 100 +
src/shared/contracts.ts | 90 +-
src/shared/ipc-channels.ts | 1 +
tests/aggregate-release.test.ts | 190 ++
tests/build-release.test.ts | 9 +-
96 files changed, 14221 insertions(+), 1027 deletions(-)
create mode 100644 build/aggregate-release.cjs
create mode 100644 sites/README.md
create mode 100644 sites/app.js
create mode 100644 sites/assets/favicon.svg
create mode 100644 sites/index.html
create mode 100644 sites/scripts/validate.mjs
create mode 100644 sites/site.config.js
create mode 100644 sites/styles.css
create mode 100644 src/main/assistant/subagent-router.test.ts
create mode 100644 src/main/assistant/subagent-router.ts
create mode 100644 src/main/assistant/subagent-scheduler.test.ts
create mode 100644 src/main/assistant/subagent-scheduler.ts
create mode 100644 src/main/assistant/subagent-service.test.ts
create mode 100644 src/main/assistant/subagent-service.ts
create mode 100644 src/main/bounded-jpeg.test.ts
create mode 100644 src/main/bounded-jpeg.ts
create mode 100644 src/main/browser/browser-limits.ts
create mode 100644 src/main/browser/browser-screenshot.ts
create mode 100644 src/main/channels/channel-driver.ts
create mode 100644 src/main/channels/channel-env.test.ts
create mode 100644 src/main/channels/channel-env.ts
create mode 100644 src/main/channels/channel-service.test.ts
create mode 100644 src/main/channels/channel-service.ts
create mode 100644 src/main/channels/dingtalk-channel-driver.test.ts
create mode 100644 src/main/channels/dingtalk-channel-driver.ts
create mode 100644 src/main/channels/dingtalk-driver.test.ts
create mode 100644 src/main/channels/dingtalk-driver.ts
create mode 100644 src/main/channels/wechat-sidecar-protocol.test.ts
create mode 100644 src/main/channels/wechat-sidecar-protocol.ts
create mode 100644 src/main/channels/wecom-channel-driver.test.ts
create mode 100644 src/main/channels/wecom-channel-driver.ts
create mode 100644 src/main/channels/wecom-driver.test.ts
create mode 100644 src/main/channels/wecom-driver.ts
delete mode 100644 src/main/knowledge/ollama-embedding-client.test.ts
create mode 100644 src/main/knowledge/openai-embedding-client.test.ts
rename src/main/knowledge/{ollama-embedding-client.ts => openai-embedding-client.ts} (61%)
create mode 100644 src/shared/channel-contracts.ts
create mode 100644 tests/aggregate-release.test.ts
diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml
index bc95a50..4aed9bb 100644
--- a/.github/workflows/packages.yml
+++ b/.github/workflows/packages.yml
@@ -10,6 +10,7 @@ on:
paths:
- '.github/workflows/packages.yml'
- 'build/build-release.cjs'
+ - 'build/aggregate-release.cjs'
- 'build/file-hash.cjs'
- 'build/runtime-hooks.cjs'
- 'package.json'
@@ -20,7 +21,7 @@ permissions:
concurrency:
group: packages-${{ github.ref }}
- cancel-in-progress: true
+ cancel-in-progress: ${{ github.ref_type != 'tag' }}
jobs:
validate:
@@ -130,3 +131,67 @@ jobs:
if-no-files-found: error
compression-level: 0
retention-days: 30
+
+ release:
+ name: Publish GitHub Release
+ if: github.event_name == 'push' && github.ref_type == 'tag'
+ needs: package
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ permissions:
+ contents: write
+ actions: read
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+
+ - name: Verify release tag
+ shell: bash
+ run: |
+ set -euo pipefail
+ expected="v$(node -p "require('./package.json').version")"
+ test "$GITHUB_REF_NAME" = "$expected"
+ test "$(git rev-parse "refs/tags/$GITHUB_REF_NAME^{commit}")" = "$GITHUB_SHA"
+
+ - name: Download Windows packages
+ uses: actions/download-artifact@v4
+ with:
+ pattern: goodbuddy-windows-*
+ path: dist/release-downloads
+
+ - name: Download macOS packages
+ uses: actions/download-artifact@v4
+ with:
+ pattern: goodbuddy-macos-*
+ path: dist/release-downloads
+
+ - name: Download Linux packages
+ uses: actions/download-artifact@v4
+ with:
+ pattern: goodbuddy-linux-*
+ path: dist/release-downloads
+
+ - 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
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ tag="$GITHUB_REF_NAME"
+ if gh release view "$tag" >/dev/null 2>&1; then
+ gh release edit "$tag" --draft
+ else
+ version="$(node -p "require('./package.json').version")"
+ gh release create "$tag" --draft --verify-tag --generate-notes --title "GoodBuddy $version"
+ fi
+ gh release upload "$tag" dist/release-upload/* --clobber
+ gh release edit "$tag" --draft=false --latest
diff --git a/BUILD.md b/BUILD.md
index 1f90b89..cc73682 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -125,9 +125,40 @@ npm run dist:linux:arm64
跨架构打包前,确认目标架构的 OpenCode 资源已经准备完成。不要用其他架构的二进制替代目标资源。
-## Linux CI
+## 跨平台 CI 与 GitHub Release
-`.github/workflows/linux-packages.yml` 支持手动触发,也会在推送 `v*` 标签时构建 Linux 包。`x64` 与 `arm64` 应分别使用对应的原生 Linux Runner 完成构建和校验。
+`.github/workflows/packages.yml` 是统一发布工作流。它先验证并生成一次
+`out` 生产 bundle,再在六个原生 Runner 上分别打包 Windows、macOS 和
+Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifact
+供打包任务复用,不会上传到 GitHub Release。
+
+本地构建单个平台目标:
+
+```bash
+npm run release:package -- --platform --arch
+```
+
+默认产物为 Windows 的 NSIS 与 portable EXE、macOS 的 DMG 与 ZIP,以及
+Linux 的 AppImage 与 DEB。每个目标目录都包含带文件大小和 SHA-256 的
+`release-manifest.json`。
+
+推送 `v${package.version}` 标签时,只有在六个打包目标全部成功后,工作流
+才会严格校验并聚合所有平台产物,生成按平台重命名的 manifests、总
+`release-manifest.json` 和 `SHA256SUMS`。随后工作流创建或更新 draft
+GitHub Release,上传全部资产成功后才发布。重跑会保留人工编辑的 Release
+notes 和未知附件。推送 `main` 或普通手动触发只构建 Actions artifacts,
+不会创建或更新 Release。
+
+发布标签必须与 `package.json` 版本完全一致。实际推送标签和触发发布前仍
+需人工确认,例如当前版本应使用:
+
+```bash
+git tag v$(node -p "require('./package.json').version")
+git push origin v$(node -p "require('./package.json').version")
+```
+
+当前未配置 Windows/macOS 代码签名或 macOS notarization。对外分发前应按
+目标平台配置签名凭据并重新验证安装、升级和系统安全提示。
## 发布前冒烟测试
diff --git a/build/aggregate-release.cjs b/build/aggregate-release.cjs
new file mode 100644
index 0000000..4d5f511
--- /dev/null
+++ b/build/aggregate-release.cjs
@@ -0,0 +1,325 @@
+const {
+ copyFileSync,
+ lstatSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ realpathSync,
+ writeFileSync
+} = require('node:fs')
+const { basename, dirname, isAbsolute, join, relative, resolve } = require('node:path')
+const { sha256File } = require('./file-hash.cjs')
+
+const root = join(__dirname, '..')
+const packageJson = JSON.parse(
+ readFileSync(join(root, 'package.json'), 'utf8')
+)
+const productName = packageJson.build?.productName ?? packageJson.name
+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'] }
+]
+const allowedExtensions = {
+ nsis: '.exe',
+ portable: '.exe',
+ dmg: '.dmg',
+ zip: '.zip',
+ AppImage: '.AppImage',
+ deb: '.deb'
+}
+
+function parseArguments(argv) {
+ const options = {}
+ for (let index = 0; index < argv.length; index += 1) {
+ const argument = argv[index]
+ if (argument === '--input' || argument === '--output') {
+ const value = argv[index + 1]
+ if (!value) {
+ throw new Error(`${argument} 缺少值`)
+ }
+ options[argument.slice(2)] = resolve(value)
+ index += 1
+ } else {
+ throw new Error(`未知参数:${argument}`)
+ }
+ }
+ if (!options.input || !options.output) {
+ throw new Error('必须指定 --input 和 --output')
+ }
+ return options
+}
+
+function assertPlainFile(filePath, description) {
+ const status = lstatSync(filePath, { throwIfNoEntry: false })
+ if (!status?.isFile() || status.isSymbolicLink()) {
+ throw new Error(`${description}必须是普通文件:${filePath}`)
+ }
+}
+
+function assertSafeName(name, description) {
+ if (
+ typeof name !== 'string' ||
+ name.length === 0 ||
+ isAbsolute(name) ||
+ basename(name) !== name ||
+ name === '.' ||
+ name === '..' ||
+ name.includes('/') ||
+ name.includes('\\') ||
+ name.includes('\0')
+ ) {
+ throw new Error(`${description}包含不安全路径:${String(name)}`)
+ }
+}
+
+function readManifest(directory) {
+ const filePath = join(directory, manifestName)
+ assertPlainFile(filePath, '平台 manifest')
+ let manifest
+ try {
+ manifest = JSON.parse(readFileSync(filePath, 'utf8'))
+ } catch (error) {
+ throw new Error(`无法解析平台 manifest:${filePath}`, {
+ cause: error
+ })
+ }
+ return { filePath, manifest }
+}
+
+function assertManifest(manifest, expected) {
+ if (
+ !manifest ||
+ manifest.formatVersion !== 1 ||
+ manifest.productName !== productName ||
+ manifest.version !== packageJson.version ||
+ manifest.platform !== expected.platform ||
+ manifest.arch !== expected.arch ||
+ !Array.isArray(manifest.formats) ||
+ manifest.formats.length !== expected.formats.length ||
+ !expected.formats.every(
+ (format, index) => manifest.formats[index] === format
+ ) ||
+ !Array.isArray(manifest.files)
+ ) {
+ throw new Error(
+ `平台 manifest 元数据错误:${expected.platform}-${expected.arch}`
+ )
+ }
+}
+
+function expectedFormatForFile(name, target) {
+ if (target.platform === 'windows') {
+ if (/-setup\.exe$/u.test(name)) {
+ return 'nsis'
+ }
+ if (/-portable\.exe$/u.test(name)) {
+ return 'portable'
+ }
+ return undefined
+ }
+ return target.formats.find((format) =>
+ name.endsWith(allowedExtensions[format])
+ )
+}
+
+function listTargetDirectories(inputDirectory) {
+ if (lstatSync(inputDirectory).isSymbolicLink()) {
+ throw new Error(`拒绝符号链接:${inputDirectory}`)
+ }
+ const inputRoot = realpathSync(inputDirectory)
+ return readdirSync(inputRoot, { withFileTypes: true })
+ .map((entry) => {
+ if (entry.isSymbolicLink()) {
+ throw new Error(`拒绝符号链接:${join(inputRoot, entry.name)}`)
+ }
+ if (!entry.isDirectory()) {
+ throw new Error(`下载目录只能包含目标目录:${entry.name}`)
+ }
+ const directory = realpathSync(join(inputRoot, entry.name))
+ const pathFromRoot = relative(inputRoot, directory)
+ if (
+ pathFromRoot.startsWith('..') ||
+ isAbsolute(pathFromRoot)
+ ) {
+ throw new Error(`目标目录越出输入目录:${directory}`)
+ }
+ return directory
+ })
+}
+
+async function aggregateRelease(inputDirectory, outputDirectory) {
+ const resolvedInput = resolve(inputDirectory)
+ const resolvedOutput = resolve(outputDirectory)
+ const outputFromInput = relative(resolvedInput, resolvedOutput)
+ const inputFromOutput = relative(resolvedOutput, resolvedInput)
+ if (
+ outputFromInput === '' ||
+ (!outputFromInput.startsWith('..') &&
+ !isAbsolute(outputFromInput)) ||
+ (!inputFromOutput.startsWith('..') &&
+ !isAbsolute(inputFromOutput))
+ ) {
+ throw new Error('输入目录和上传目录必须相互独立')
+ }
+ const directories = listTargetDirectories(inputDirectory)
+ if (directories.length !== targetDefinitions.length) {
+ throw new Error(
+ `发布目标数量错误:期望 ${targetDefinitions.length},实际 ${directories.length}`
+ )
+ }
+
+ const manifests = directories.map(readManifest)
+ const byTarget = new Map()
+ for (const item of manifests) {
+ const key = `${item.manifest.platform}-${item.manifest.arch}`
+ if (byTarget.has(key)) {
+ throw new Error(`发布目标重复:${key}`)
+ }
+ byTarget.set(key, item)
+ }
+
+ const fileNames = new Set()
+ const targets = []
+ mkdirSync(outputDirectory, { recursive: false })
+ for (const expected of targetDefinitions) {
+ const key = `${expected.platform}-${expected.arch}`
+ const item = byTarget.get(key)
+ if (!item) {
+ throw new Error(`缺少发布目标:${key}`)
+ }
+ assertManifest(item.manifest, expected)
+
+ const directory = dirname(item.filePath)
+ const entries = readdirSync(directory, { withFileTypes: true })
+ for (const entry of entries) {
+ if (entry.isSymbolicLink()) {
+ throw new Error(`拒绝符号链接:${join(directory, entry.name)}`)
+ }
+ if (!entry.isFile()) {
+ throw new Error(`目标目录只能包含普通文件:${entry.name}`)
+ }
+ }
+ if (entries.length !== item.manifest.files.length + 1) {
+ throw new Error(`目标目录包含 manifest 未声明的文件:${key}`)
+ }
+
+ const seenFormats = new Set()
+ const files = []
+ for (const file of item.manifest.files) {
+ assertSafeName(file?.name, '发布文件名')
+ if (
+ !Number.isSafeInteger(file.size) ||
+ file.size < 1 ||
+ typeof file.sha256 !== 'string' ||
+ !/^[a-f0-9]{64}$/u.test(file.sha256)
+ ) {
+ throw new Error(`发布文件元数据错误:${file?.name ?? key}`)
+ }
+ if (fileNames.has(file.name)) {
+ throw new Error(`发布文件名全局重复:${file.name}`)
+ }
+ const format = expectedFormatForFile(file.name, expected)
+ if (!format || seenFormats.has(format)) {
+ throw new Error(`发布文件格式或数量错误:${file.name}`)
+ }
+ const source = join(directory, file.name)
+ assertPlainFile(source, '发布文件')
+ const actualSize = lstatSync(source).size
+ const actualHash = await sha256File(source)
+ if (actualSize !== file.size || actualHash !== file.sha256) {
+ throw new Error(`发布文件完整性校验失败:${file.name}`)
+ }
+ assertPlainFile(source, '发布文件')
+ copyFileSync(source, join(outputDirectory, file.name))
+ fileNames.add(file.name)
+ seenFormats.add(format)
+ files.push({
+ name: file.name,
+ size: file.size,
+ sha256: file.sha256
+ })
+ }
+ if (
+ expected.formats.some((format) => !seenFormats.has(format))
+ ) {
+ throw new Error(`发布目标格式不完整:${key}`)
+ }
+
+ const renamedManifest = `release-manifest-${key}.json`
+ writeFileSync(
+ join(outputDirectory, renamedManifest),
+ `${JSON.stringify(item.manifest, null, 2)}\n`,
+ 'utf8'
+ )
+ targets.push({
+ platform: expected.platform,
+ arch: expected.arch,
+ formats: [...expected.formats],
+ manifest: renamedManifest,
+ files
+ })
+ }
+
+ if (byTarget.size !== targetDefinitions.length) {
+ throw new Error('包含未知发布目标')
+ }
+ const aggregateManifest = {
+ formatVersion: 1,
+ productName,
+ version: packageJson.version,
+ targets,
+ files: targets.flatMap((target) =>
+ target.files.map((file) => ({
+ platform: target.platform,
+ arch: target.arch,
+ ...file
+ }))
+ )
+ }
+ writeFileSync(
+ join(outputDirectory, manifestName),
+ `${JSON.stringify(aggregateManifest, null, 2)}\n`,
+ 'utf8'
+ )
+
+ const checksumNames = readdirSync(outputDirectory)
+ .sort((left, right) => left.localeCompare(right))
+ const checksums = []
+ for (const name of checksumNames) {
+ assertSafeName(name, '上传文件名')
+ const filePath = join(outputDirectory, name)
+ assertPlainFile(filePath, '上传文件')
+ checksums.push(`${await sha256File(filePath)} ${name}`)
+ }
+ writeFileSync(
+ join(outputDirectory, 'SHA256SUMS'),
+ `${checksums.join('\n')}\n`,
+ 'utf8'
+ )
+ return aggregateManifest
+}
+
+async function main(argv = process.argv.slice(2)) {
+ const options = parseArguments(argv)
+ await aggregateRelease(options.input, options.output)
+ console.log(`发布资产聚合完成:${options.output}`)
+}
+
+module.exports = {
+ aggregateRelease,
+ assertSafeName,
+ parseArguments,
+ targetDefinitions
+}
+
+if (require.main === module) {
+ main().catch((error) => {
+ console.error(error)
+ process.exitCode = 1
+ })
+}
diff --git a/docs/电脑控制开发进度.md b/docs/电脑控制开发进度.md
index 359c2e9..bf58f0e 100644
--- a/docs/电脑控制开发进度.md
+++ b/docs/电脑控制开发进度.md
@@ -71,12 +71,12 @@ Linux x64 和 Linux arm64。
现在会先加载并等待 `about:blank` 初始文档,再附加并启用 CDP 域,同时保留
超时后的迟到资源清理。真实 Electron 探针已确认 `Page.enable`、访问
-`https://example.com/` 和 PNG 画面捕获均成功。
+`https://example.com/` 和有界 JPEG 画面捕获均成功。
### P0:右侧没有浏览器实时画面,已修复
浏览器窗口仍使用 `show: false`,但 BrowserService 现在从模型实际操作的同一
-会话捕获页面帧,并通过受限 IPC 发送状态、当前 URL 和 PNG 画面。右侧工作栏
+会话捕获页面帧,并通过受限 IPC 发送状态、当前 URL 和约 220KB 的 JPEG 画面。右侧工作栏
新增“浏览器”页签;活动对话启动浏览器时会自动打开该页签,并显示创建中、
加载中、操作中、就绪、失败和已停止状态。用户可在页签内立即停止当前对话的
浏览器会话。
@@ -92,13 +92,27 @@ Linux x64 和 Linux arm64。
如果主页面确实已经变化,过期引用会作为“可重试”工具结果返回给直连模型,并
明确要求重新调用 `browser_snapshot`;模型可以用新引用继续操作,不再让整次
-任务直接失败。百度搜索结果页的原始可访问性树约为 782KB,因此内部有界读取
-上限调整为 1MB,返回给模型的节点数和最终快照大小仍分别受独立上限约束。
+任务直接失败。页面原始可访问性树不再按字节大小拒绝;无论页面多大,驱动都会
+读取页面并自动截取单次返回给模型的快照,同时用 `truncated` 明确标记,不再
+显示“浏览器可访问性树超过安全限制”。
真实 Electron 探针已完成“打开百度、获取快照、输入阿里云、点击百度一下、
读取结果页”,结果页标题为“阿里云_百度搜索”,返回约 330 个节点并正确标记
为已截断。
+海关总署网站使用会变化的 CDN 地址和 JavaScript 挑战。过滤代理现在会在经过
+策略验证的地址间有界回退,并等待真正的主框架提交,不再把旧 `about:blank`
+误报为成功;托管浏览器同时使用标准 Chromium User-Agent。真实 Electron
+探针已确认 `http://www.customs.gov.cn/` 返回“中华人民共和国海关总署”标题、
+约 310 个快照节点,以及约 161KB 的工具截图和右侧实时 JPEG 画面。
+
+### P0:截图与用户附件展示,已改善
+
+浏览器、全屏和单窗口画面统一压缩为约 220KB 的 JPEG,再送入模型或实时预览。
+用户发送的文档和图片会在知识检索及 Runtime 调用前立即显示在对应用户消息中,
+并随会话持久化。单窗口截图不再使用横向原生按钮,改为应用内纵向窗口列表,
+避免选择器被多个长应用名称横向撑宽。
+
### P1:工具错误信息过度包装,已改善
浏览器会话创建错误现在包含“启动代理、创建隔离会话、配置网络代理、创建窗口、
diff --git a/eslint.config.js b/eslint.config.js
index 24f0a63..3181552 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -42,6 +42,22 @@ export default tseslint.config(
...reactHooks.configs.recommended.rules
}
},
+ {
+ files: ['sites/**/*.js'],
+ languageOptions: {
+ globals: {
+ ...globals.browser
+ }
+ }
+ },
+ {
+ files: ['sites/**/*.mjs'],
+ languageOptions: {
+ globals: {
+ ...globals.node
+ }
+ }
+ },
{
files: ['**/*.test.{ts,tsx}', 'vitest.config.ts'],
languageOptions: {
diff --git a/package-lock.json b/package-lock.json
index 3fc9429..9cfd80e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,19 @@
{
"name": "goodbuddy",
- "version": "0.1.0",
+ "version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "goodbuddy",
- "version": "0.1.0",
+ "version": "0.8.0",
"license": "UNLICENSED",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@opencode-ai/sdk": "^1.18.9",
+ "@wecom/aibot-node-sdk": "^1.0.6",
"cross-spawn": "^7.0.6",
+ "dingtalk-stream": "^2.1.6-beta.1",
"fflate": "^0.8.3",
"html-to-text": "^10.0.0",
"lucide-react": "^1.27.0",
@@ -3534,6 +3536,17 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@wecom/aibot-node-sdk": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@wecom/aibot-node-sdk/-/aibot-node-sdk-1.0.6.tgz",
+ "integrity": "sha512-WZJN3Q+s+94Qjc0VW8d5W1cVkA3emYxiqf+mNRO9UEHoF40puHvizreNMtudjFhm7mmkYiK5ue/QzNiCk+xwLA==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.7",
+ "eventemitter3": "^5.0.1",
+ "ws": "^8.16.0"
+ }
+ },
"node_modules/@xmldom/xmldom": {
"version": "0.8.13",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
@@ -3936,7 +3949,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -3956,6 +3968,43 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/axios": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
+ "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.6",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/axios/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
@@ -4437,7 +4486,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -4767,7 +4815,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -4812,6 +4859,17 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/dingtalk-stream": {
+ "version": "2.1.6-beta.1",
+ "resolved": "https://registry.npmjs.org/dingtalk-stream/-/dingtalk-stream-2.1.6-beta.1.tgz",
+ "integrity": "sha512-uYcBnf0Z4rfHHyN1ae4YnAFA6hUW2DmGVb0OZ53r/A272kuHnZynylE5pEJIJHkNIer6R9PCqpnsfsk9IuvglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.4.0",
+ "debug": "^4.3.4",
+ "ws": "^8.13.0"
+ }
+ },
"node_modules/dir-compare": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
@@ -5307,7 +5365,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -5626,6 +5683,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -5929,11 +5992,30 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/form-data": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -6305,7 +6387,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -8037,7 +8118,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -8047,7 +8127,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -9122,6 +9201,15 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
@@ -11461,6 +11549,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.21.2",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
+ "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
diff --git a/package.json b/package.json
index 9c7e611..559aeda 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "goodbuddy",
- "version": "0.1.0",
+ "version": "0.8.0",
"private": true,
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
"desktopName": "GoodBuddy",
@@ -128,7 +128,9 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0",
"@opencode-ai/sdk": "^1.18.9",
+ "@wecom/aibot-node-sdk": "^1.0.6",
"cross-spawn": "^7.0.6",
+ "dingtalk-stream": "^2.1.6-beta.1",
"fflate": "^0.8.3",
"html-to-text": "^10.0.0",
"lucide-react": "^1.27.0",
diff --git a/sites/README.md b/sites/README.md
new file mode 100644
index 0000000..604d9cd
--- /dev/null
+++ b/sites/README.md
@@ -0,0 +1,46 @@
+# GoodBuddy 静态官网
+
+`sites` 是无需构建步骤或额外依赖的静态官网源码,可直接托管整个目录。
+
+## 本地预览
+
+在仓库根目录运行:
+
+```powershell
+python -m http.server 4173 --bind 127.0.0.1 --directory sites
+```
+
+然后访问 。也可以直接用浏览器打开 `sites/index.html`。
+
+## 校验
+
+```powershell
+node sites/scripts/validate.mjs
+node --check sites/app.js
+node --check sites/site.config.js
+```
+
+校验脚本会检查必需文件、页内链接、本地资源、关键产品文案、主题与响应式规则,以及未发布状态下的下载链接保护。
+
+## Release 配置
+
+未来 v0.8.0 Release 地址集中在 `site.config.js`:
+
+```js
+window.GOODBUDDY_SITE_CONFIG = Object.freeze({
+ version: "0.8.0",
+ releasePublished: false,
+ releaseUrl: "https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.0",
+});
+```
+
+正式 Release 确认发布后,将 `releasePublished` 改为 `true`,页面上的下载入口才会指向 Release 页面。官网不配置或猜测具体安装资产名称。
+
+## 文件
+
+- `index.html`:页面结构与简体中文内容
+- `styles.css`:语义令牌、浅深主题、焦点与响应式布局
+- `app.js`:主题、移动导航、当前章节和 Release 状态
+- `site.config.js`:版本与未来 Release 地址
+- `assets/favicon.svg`:站点图标
+- `scripts/validate.mjs`:无依赖静态检查
diff --git a/sites/app.js b/sites/app.js
new file mode 100644
index 0000000..6ab8919
--- /dev/null
+++ b/sites/app.js
@@ -0,0 +1,157 @@
+(() => {
+ "use strict";
+
+ const root = document.documentElement;
+ const header = document.querySelector("[data-site-header]");
+ const menuToggle = document.querySelector("[data-menu-toggle]");
+ const navigation = document.querySelector("[data-navigation]");
+ const themeToggle = document.querySelector("[data-theme-toggle]");
+ const themeColor = document.querySelector('meta[name="theme-color"]');
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
+ const config = window.GOODBUDDY_SITE_CONFIG;
+
+ const getSavedTheme = () => {
+ try {
+ const savedTheme = localStorage.getItem("goodbuddy-site-theme");
+ return savedTheme === "light" || savedTheme === "dark" ? savedTheme : null;
+ } catch {
+ return null;
+ }
+ };
+
+ const applyTheme = (theme, persist = false) => {
+ root.dataset.theme = theme;
+ themeToggle?.setAttribute(
+ "aria-label",
+ theme === "dark" ? "切换为浅色主题" : "切换为深色主题",
+ );
+ themeColor?.setAttribute("content", theme === "dark" ? "#07101f" : "#f6f8fb");
+
+ if (persist) {
+ try {
+ localStorage.setItem("goodbuddy-site-theme", theme);
+ } catch {
+ // The selected theme still applies for the current page.
+ }
+ }
+ };
+
+ const closeMenu = () => {
+ header?.classList.remove("is-menu-open");
+ menuToggle?.setAttribute("aria-expanded", "false");
+ menuToggle?.setAttribute("aria-label", "打开导航");
+ };
+
+ const setHeaderState = () => {
+ header?.classList.toggle("is-scrolled", window.scrollY > 12);
+ };
+
+ const configureReleaseLinks = () => {
+ const releaseLinks = document.querySelectorAll("[data-release-link]");
+ const isReady =
+ config?.releasePublished === true &&
+ typeof config.releaseUrl === "string" &&
+ /^https:\/\/github\.com\/mesalogo\/goodbuddy\/releases\/tag\/v0\.8\.0$/.test(
+ config.releaseUrl,
+ );
+
+ releaseLinks.forEach((link) => {
+ if (!isReady) {
+ link.removeAttribute("href");
+ link.removeAttribute("target");
+ link.removeAttribute("rel");
+ link.setAttribute("aria-disabled", "true");
+ link.classList.add("is-disabled");
+ link.textContent = "发布后开放";
+ return;
+ }
+
+ link.href = config.releaseUrl;
+ link.target = "_blank";
+ link.rel = "noreferrer";
+ link.removeAttribute("aria-disabled");
+ link.classList.remove("is-disabled");
+ link.innerHTML = `前往 v${config.version} Release(在新窗口打开)`;
+ });
+ };
+
+ applyTheme(getSavedTheme() ?? (systemTheme.matches ? "dark" : "light"));
+ configureReleaseLinks();
+ setHeaderState();
+
+ themeToggle?.addEventListener("click", () => {
+ applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
+ });
+
+ systemTheme.addEventListener("change", (event) => {
+ if (!getSavedTheme()) {
+ applyTheme(event.matches ? "dark" : "light");
+ }
+ });
+
+ menuToggle?.addEventListener("click", () => {
+ const willOpen = !header?.classList.contains("is-menu-open");
+ header?.classList.toggle("is-menu-open", willOpen);
+ menuToggle.setAttribute("aria-expanded", String(willOpen));
+ menuToggle.setAttribute("aria-label", willOpen ? "关闭导航" : "打开导航");
+ });
+
+ navigation?.addEventListener("click", (event) => {
+ if (event.target instanceof HTMLAnchorElement) {
+ closeMenu();
+ }
+ });
+
+ document.addEventListener("keydown", (event) => {
+ if (event.key === "Escape" && header?.classList.contains("is-menu-open")) {
+ closeMenu();
+ menuToggle?.focus();
+ }
+ });
+
+ document.addEventListener("click", (event) => {
+ if (
+ header?.classList.contains("is-menu-open") &&
+ event.target instanceof Node &&
+ !header.contains(event.target)
+ ) {
+ closeMenu();
+ }
+ });
+
+ window.addEventListener("scroll", setHeaderState, { passive: true });
+
+ const sections = [...document.querySelectorAll("main section[id]")];
+ const navLinks = [...document.querySelectorAll('.site-navigation a[href^="#"]')];
+
+ if ("IntersectionObserver" in window) {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ const visibleSection = entries
+ .filter((entry) => entry.isIntersecting)
+ .sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
+
+ if (!visibleSection) {
+ return;
+ }
+
+ navLinks.forEach((link) => {
+ const isCurrent = link.getAttribute("href") === `#${visibleSection.target.id}`;
+ if (isCurrent) {
+ link.setAttribute("aria-current", "true");
+ } else {
+ link.removeAttribute("aria-current");
+ }
+ });
+ },
+ { rootMargin: "-25% 0px -55%", threshold: [0.05, 0.2, 0.5] },
+ );
+
+ sections.forEach((section) => observer.observe(section));
+ }
+
+ const currentYear = document.querySelector("[data-current-year]");
+ if (currentYear) {
+ currentYear.textContent = String(new Date().getFullYear());
+ }
+})();
diff --git a/sites/assets/favicon.svg b/sites/assets/favicon.svg
new file mode 100644
index 0000000..4c1c657
--- /dev/null
+++ b/sites/assets/favicon.svg
@@ -0,0 +1,12 @@
+
diff --git a/sites/index.html b/sites/index.html
new file mode 100644
index 0000000..0d30fe8
--- /dev/null
+++ b/sites/index.html
@@ -0,0 +1,538 @@
+
+
+
+
+
+
+
+ GoodBuddy|安全可控的桌面智能助手
+
+
+
+
+
+ 跳到主要内容
+
+
+
+
+
+
+
+
+
+ GoodBuddy 0.8.0 即将发布
+
+
把 AI 放在桌面,
也把控制权留在手中。
+
+ GoodBuddy 是安全可控的桌面智能助手与 Agent 工作空间。连接模型、知识与工具,
+ 在清晰的范围和审批边界内完成真正的工作。
+
+
+
+ -
+
+ Windows / macOS / Linux
+
+ -
+
+ 项目范围隔离
+
+ -
+
+ 工具调用可审批
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
梳理 0.8.0 发布前还需要完成的工作。
+
+
+ G
+ GoodBuddy
+
+
我会先核对发布清单与项目知识,再给出不执行变更的计划。
+
+
+
+
+
+
+
+
+
+
+
+
+
执行前确认每次工具调用都清晰可见
+
+
+
+ 项目范围上下文不会悄悄混用
+
+
+
+
+
+
+
+
3 种问答 / 计划 / 执行模式
+
2 层全局与项目知识范围
+
明确工具权限与活动记录
+
跨平台x64 与 arm64
+
+
+
+
+
+
+
+
围绕真实工作流设计
+
不是另一个聊天窗口
+
+
+ 从上下文组织到执行审批,每一步都让范围、状态和风险保持可见。
+
+
+
+
+
+
+ 01
+ 受控 Agent 运行时
+ 问答与计划模式在运行时保持只读;执行模式中的工具操作经过现有审批控制,并保留取消、超时与输出边界。
+
+ 问答 只读
+ 计划 只读
+ 执行 需审批
+
+
+
+
+
+ 02
+ 有范围的知识
+ 区分全局与项目知识。搜索、引用和创建都围绕当前范围展开,让上下文来源清楚可追溯。
+
+
+
+
+ 03
+ 智能心跳与任务
+ 将周期计划、运行状态、结果与活动记录放在同一条可检查的工作链路中。
+
+
+
+
+ 04
+ 文档与图像输入
+ 单次最多添加 8 个附件,支持同时传入 5 张图片;在一个会话中汇集任务所需材料。
+
+
+
+
+ 05
+ 可控的图像生成
+ 生图质量支持 auto、low、medium、high 四档。结果以单张图像呈现,并作为本地工件保存。
+
+
+
+
+ 06
+ 模型与工具,由你连接
+ 在桌面端管理模型配置、MCP 工具与运行时。密钥留在主进程的加密设置存储中,不交给网页渲染层。
+
+ 模型提供商MCPOpenCodeContinue
+
+
+
+
+
+
+
+
+
+
+ VERSION
+ 0.8.0
+
+
+
下一站
+
0.8.0 更新亮点
+
更聪明地组织工作,也更诚实地标注能力边界。以下功能状态以正式 Release 说明为准。
+
+
+
+
+ -
+
01
+
+
0.8.0
+
Subagent 与智能路由
+
面向复杂任务的协作与路由能力归入 0.8.0,不将仍在开发中的路径描述为当前稳定能力。
+
+
+ 主任务
+
+
+ 研究
+ 验证
+
+
+
+ -
+
02
+
+
开发者预览
+
IM 渠道接入
+
钉钉与企业微信以开发者预览提供;个人微信处于实验性边界,不作为面向生产环境的稳定承诺。
+
+
+ 钉钉开发者预览
+ 企业微信开发者预览
+ 个人微信实验性边界
+
+
+
+ -
+
03
+
+
多模态输入
+
更多材料,一次带上
+
单次最多 8 个附件,并已验证同时传入 5 张图片。限制保持可见,避免把超出边界的输入静默带入任务。
+
+
+
+
+ -
+
04
+
+
图像生成
+
清晰选择质量档位
+
支持 auto、low、medium、high 四档质量。当前按单张结果呈现,不承诺批量多图生成。
+
+
+ autolowmediumhigh
+
+
+
+
+
+
+
+
+
+
+
原生桌面体验
+
准备好,在你的设备上运行
+
+
+ v0.8.0 Release 尚未发布。下载入口将在发布后统一开放,目前不提供虚构的资产名称或下载地址。
+
+
+
+
+
+
+ Windows
x64 / arm64 · NSIS / 便携版
+ 发布后开放
+
+
+
+ macOS
x64 / arm64 · DMG / ZIP
+ 发布后开放
+
+
+
+ Linux
x64 / arm64 · AppImage / DEB
+ 发布后开放
+
+
+
+
+
+
+ Release 状态:尚未发布
+ 本站下载按钮由单一配置控制;正式发布前不会指向占位资产。
+
+
+
+
+
+
+
+
+
+
+
+ 01
+ 密钥不进入渲染层
API 密钥留在主进程,并写入加密设置存储;网页界面不获得直接 Node 访问。
+
+
+ 02
+ 跨进程能力明确暴露
通过窄化的预加载桥接调用能力,IPC 输入经过共享模式校验,并核验可信发送方。
+
+
+ 03
+ 运行时按不可信处理
OpenCode 与 Continue 子运行时受环境白名单、沙箱检查及逐工具审批约束。
+
+
+ 04
+ 状态与审计语义可见
取消、超时、输出边界和活动记录属于执行链路的一部分,不用模糊的“已完成”掩盖风险。
+
+
+
+
+
+
+
+
+
+
+
GoodBuddy 0.8.0
+
一个更能做事,也更懂边界的桌面伙伴。
+
关注 Release,第一时间获取正式版本、校验信息与完整更新说明。
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/scripts/validate.mjs b/sites/scripts/validate.mjs
new file mode 100644
index 0000000..8c93e62
--- /dev/null
+++ b/sites/scripts/validate.mjs
@@ -0,0 +1,165 @@
+import { readFile, stat } from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const siteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const errors = [];
+
+const requiredFiles = [
+ "index.html",
+ "styles.css",
+ "app.js",
+ "site.config.js",
+ "assets/favicon.svg",
+ "README.md",
+];
+
+const report = (condition, message) => {
+ if (!condition) {
+ errors.push(message);
+ }
+};
+
+const readSiteFile = async (relativePath) => {
+ try {
+ return await readFile(path.join(siteRoot, relativePath), "utf8");
+ } catch {
+ errors.push(`缺少文件:${relativePath}`);
+ return "";
+ }
+};
+
+await Promise.all(
+ requiredFiles.map(async (relativePath) => {
+ try {
+ const fileStats = await stat(path.join(siteRoot, relativePath));
+ report(fileStats.isFile(), `不是普通文件:${relativePath}`);
+ } catch {
+ errors.push(`缺少文件:${relativePath}`);
+ }
+ }),
+);
+
+const [html, css, appJs, configJs] = await Promise.all([
+ readSiteFile("index.html"),
+ readSiteFile("styles.css"),
+ readSiteFile("app.js"),
+ readSiteFile("site.config.js"),
+]);
+
+for (const [relativePath, content] of [
+ ["index.html", html],
+ ["styles.css", css],
+ ["app.js", appJs],
+ ["site.config.js", configJs],
+]) {
+ report(!/[ \t]+$/m.test(content), `${relativePath} 包含行尾空白`);
+ report(!content.includes("\t"), `${relativePath} 包含 Tab 缩进`);
+}
+
+report(//.test(html), "页面语言必须是 zh-CN");
+report(/]/g) ?? []).length === 1, "页面必须且只能包含一个 h1");
+report(/class="skip-link"\s+href="#main-content"/.test(html), "缺少跳到主要内容链接");
+report(//.test(html), "缺少 main-content 主区域");
+report(/aria-label="主导航"/.test(html), "主导航缺少可访问名称");
+report(/data-theme-toggle/.test(html), "缺少主题切换控件");
+report(/prefers-reduced-motion:\s*reduce/.test(css), "缺少减少动态效果规则");
+report(/\[data-theme="dark"\]/.test(css), "缺少深色主题令牌");
+
+for (const breakpoint of ["1199px", "959px", "719px"]) {
+ report(css.includes(`max-width: ${breakpoint}`), `缺少 ${breakpoint} 响应式断点`);
+}
+
+const requiredCopy = [
+ "Subagent 与智能路由",
+ "钉钉与企业微信以开发者预览提供",
+ "个人微信处于实验性边界",
+ "单次最多添加 8 个附件,支持同时传入 5 张图片",
+ "auto、low、medium、high",
+ "当前按单张结果呈现,不承诺批量多图生成",
+ "发布后开放",
+ "安全不是开关",
+];
+
+for (const copy of requiredCopy) {
+ report(html.includes(copy), `缺少准确文案:${copy}`);
+}
+
+report(
+ /version:\s*"0\.8\.0"/.test(configJs),
+ "site.config.js 必须集中配置 0.8.0 版本",
+);
+report(
+ /releasePublished:\s*false/.test(configJs),
+ "Release 未发布前 releasePublished 必须为 false",
+);
+report(
+ /releaseUrl:\s*"https:\/\/github\.com\/mesalogo\/goodbuddy\/releases\/tag\/v0\.8\.0"/.test(
+ configJs,
+ ),
+ "未来 v0.8.0 Release URL 配置不正确",
+);
+report(
+ appJs.includes("config?.releasePublished === true"),
+ "下载链接必须受 releasePublished 配置保护",
+);
+
+const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
+const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);
+report(duplicateIds.length === 0, `存在重复 id:${[...new Set(duplicateIds)].join(", ")}`);
+
+const attributes = [...html.matchAll(/\s(?:href|src)="([^"]+)"/g)].map((match) => match[1]);
+const fragmentLinks = attributes.filter((value) => value.startsWith("#") && value.length > 1);
+
+for (const fragment of fragmentLinks) {
+ report(ids.includes(fragment.slice(1)), `页内链接目标不存在:${fragment}`);
+}
+
+const localAssets = attributes.filter(
+ (value) =>
+ !value.startsWith("#") &&
+ !value.startsWith("https://") &&
+ !value.startsWith("http://") &&
+ !value.startsWith("mailto:") &&
+ !value.startsWith("data:"),
+);
+
+for (const asset of localAssets) {
+ const cleanAsset = asset.split(/[?#]/, 1)[0].replace(/^\.\//, "");
+ try {
+ const assetStats = await stat(path.join(siteRoot, cleanAsset));
+ report(assetStats.isFile(), `本地资源不是文件:${asset}`);
+ } catch {
+ errors.push(`本地资源不存在:${asset}`);
+ }
+}
+
+const externalBlankLinks = [
+ ...html.matchAll(/]*target="_blank")[^>]*>/g),
+].map((match) => match[0]);
+
+for (const link of externalBlankLinks) {
+ report(/rel="[^"]*noreferrer[^"]*"/.test(link), `新窗口链接缺少 noreferrer:${link}`);
+}
+
+report(
+ !/]*href="[^"]+\.(?:exe|dmg|zip|AppImage|deb)(?:[?#][^"]*)?"/i.test(html),
+ "Release 未发布前不得提供具体安装资产链接",
+);
+report(
+ !/(?:react|vue|angular|bootstrap|tailwind)(?:\.min)?\.(?:js|css)/i.test(html),
+ "静态官网不得引入额外框架资源",
+);
+
+if (errors.length > 0) {
+ console.error(`官网静态检查失败(${errors.length} 项):`);
+ for (const error of errors) {
+ console.error(`- ${error}`);
+ }
+ process.exitCode = 1;
+} else {
+ console.log(
+ `官网静态检查通过:${requiredFiles.length} 个必需文件,${ids.length} 个唯一 id,${localAssets.length} 个本地资源引用。`,
+ );
+}
diff --git a/sites/site.config.js b/sites/site.config.js
new file mode 100644
index 0000000..67d4c75
--- /dev/null
+++ b/sites/site.config.js
@@ -0,0 +1,5 @@
+window.GOODBUDDY_SITE_CONFIG = Object.freeze({
+ version: "0.8.0",
+ releasePublished: false,
+ releaseUrl: "https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.0",
+});
diff --git a/sites/styles.css b/sites/styles.css
new file mode 100644
index 0000000..620d5f9
--- /dev/null
+++ b/sites/styles.css
@@ -0,0 +1,2231 @@
+:root {
+ color-scheme: light;
+ --surface-canvas: #f6f8fb;
+ --surface-raised: #ffffff;
+ --surface-subtle: #edf2f7;
+ --surface-muted: #e2e9f1;
+ --surface-inverse: #07152b;
+ --surface-overlay: rgba(255, 255, 255, 0.82);
+ --text-primary: #10213a;
+ --text-secondary: #4f6178;
+ --text-muted: #738198;
+ --text-on-accent: #ffffff;
+ --text-on-inverse: #f7faff;
+ --border-default: #d6dee9;
+ --border-control: #bcc8d7;
+ --border-subtle: #e6ebf2;
+ --accent: #0877e8;
+ --accent-hover: #0567ca;
+ --accent-solid: #0877e8;
+ --accent-solid-hover: #0567ca;
+ --accent-selected: #60aefd;
+ --accent-subtle: #e7f3ff;
+ --accent-cyan: #05b9c8;
+ --accent-mint: #08b89b;
+ --success: #07866f;
+ --success-subtle: #e1f8f1;
+ --warning: #a46408;
+ --danger: #c63a4b;
+ --danger-strong: #a71f34;
+ --danger-solid: #bc2d42;
+ --danger-border: #edb9c1;
+ --danger-subtle: #fff0f2;
+ --shadow-card: 0 24px 70px rgba(16, 33, 58, 0.1);
+ --shadow-dialog: 0 32px 90px rgba(5, 20, 42, 0.2);
+ --shadow-button: 0 10px 24px rgba(8, 119, 232, 0.22);
+ --space-0: 0;
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-6: 24px;
+ --space-8: 32px;
+ --space-10: 40px;
+ --space-12: 48px;
+ --space-16: 64px;
+ --space-20: 80px;
+ --font-caption: 0.625rem;
+ --font-body: 0.875rem;
+ --font-section-title: 1rem;
+ --font-page-title: clamp(2.7rem, 6vw, 5.5rem);
+ --radius-control: 8px;
+ --radius-card: 12px;
+ --radius-large: 24px;
+ --radius-pill: 999px;
+ --motion-fast: 120ms;
+ --motion-normal: 180ms;
+ --motion-slow: 240ms;
+ --z-header: 20;
+ --z-menu: 30;
+ font-family:
+ Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
+ "PingFang SC", "Microsoft YaHei", sans-serif;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+}
+
+[data-theme="dark"] {
+ color-scheme: dark;
+ --surface-canvas: #07101f;
+ --surface-raised: #101c2e;
+ --surface-subtle: #142338;
+ --surface-muted: #1b2a3f;
+ --surface-inverse: #eaf3ff;
+ --surface-overlay: rgba(7, 16, 31, 0.84);
+ --text-primary: #edf5ff;
+ --text-secondary: #b2c0d2;
+ --text-muted: #8494a9;
+ --text-on-accent: #ffffff;
+ --text-on-inverse: #10213a;
+ --border-default: #2a3b51;
+ --border-control: #40546d;
+ --border-subtle: #203148;
+ --accent: #62b0ff;
+ --accent-hover: #8bc5ff;
+ --accent-solid: #147fe9;
+ --accent-solid-hover: #3495f5;
+ --accent-selected: #65b4ff;
+ --accent-subtle: #112d4c;
+ --accent-cyan: #37c9d4;
+ --accent-mint: #35ccb1;
+ --success: #59d4bb;
+ --success-subtle: #103a34;
+ --warning: #e8ae54;
+ --danger: #ff8795;
+ --danger-strong: #ffa2ad;
+ --danger-solid: #d34257;
+ --danger-border: #763d48;
+ --danger-subtle: #3b1e27;
+ --shadow-card: 0 24px 70px rgba(0, 0, 0, 0.28);
+ --shadow-dialog: 0 32px 90px rgba(0, 0, 0, 0.46);
+ --shadow-button: 0 10px 26px rgba(20, 127, 233, 0.28);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 88px;
+}
+
+body {
+ min-width: 320px;
+ margin: 0;
+ overflow-x: hidden;
+ background: var(--surface-canvas);
+ color: var(--text-primary);
+ font-size: var(--font-body);
+ line-height: 1.7;
+}
+
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+
+button,
+input,
+textarea,
+select {
+ font: inherit;
+}
+
+a {
+ color: inherit;
+}
+
+svg {
+ display: block;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1,
+h2,
+h3 {
+ line-height: 1.16;
+}
+
+h2 {
+ margin-bottom: var(--space-4);
+ font-size: clamp(2rem, 4vw, 3.5rem);
+ letter-spacing: -0.045em;
+}
+
+h3 {
+ font-size: 1.25rem;
+ letter-spacing: -0.02em;
+}
+
+p {
+ color: var(--text-secondary);
+}
+
+:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 3px;
+}
+
+::selection {
+ background: var(--accent-selected);
+ color: var(--text-primary);
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.skip-link {
+ position: fixed;
+ z-index: calc(var(--z-menu) + 1);
+ top: var(--space-3);
+ left: var(--space-3);
+ padding: var(--space-2) var(--space-4);
+ border-radius: var(--radius-control);
+ background: var(--accent-solid);
+ color: var(--text-on-accent);
+ font-weight: 700;
+ text-decoration: none;
+ transform: translateY(-150%);
+ transition: transform var(--motion-fast) ease-out;
+}
+
+.skip-link:focus {
+ transform: translateY(0);
+}
+
+.section {
+ padding-block: 112px;
+}
+
+.section-inner,
+.header-inner {
+ width: min(100% - 64px, 1200px);
+ margin-inline: auto;
+}
+
+.site-header {
+ position: fixed;
+ z-index: var(--z-header);
+ top: 0;
+ right: 0;
+ left: 0;
+ border-bottom: 1px solid transparent;
+ background: transparent;
+ transition:
+ background-color var(--motion-normal) ease-out,
+ border-color var(--motion-normal) ease-out,
+ box-shadow var(--motion-normal) ease-out;
+}
+
+.site-header.is-scrolled,
+.site-header.is-menu-open {
+ border-bottom-color: var(--border-subtle);
+ background: var(--surface-overlay);
+ box-shadow: 0 6px 24px color-mix(in srgb, var(--text-primary) 7%, transparent);
+ backdrop-filter: blur(18px);
+}
+
+.header-inner {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ min-height: 72px;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ justify-self: start;
+ gap: var(--space-2);
+ color: var(--text-primary);
+ font-size: 1.125rem;
+ font-weight: 780;
+ letter-spacing: -0.025em;
+ text-decoration: none;
+}
+
+.brand-mark {
+ width: 34px;
+ height: 34px;
+ overflow: visible;
+ fill: none;
+ stroke: var(--accent);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 3;
+}
+
+.brand-mark path:nth-child(2) {
+ stroke: var(--accent-mint);
+}
+
+.brand-mark path:last-child {
+ stroke-width: 2.5;
+}
+
+.site-navigation {
+ display: flex;
+ align-items: center;
+ gap: var(--space-1);
+ padding: var(--space-1);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-pill);
+ background: color-mix(in srgb, var(--surface-raised) 82%, transparent);
+}
+
+.site-navigation a {
+ min-height: 36px;
+ padding: 6px var(--space-4);
+ border-radius: var(--radius-pill);
+ color: var(--text-secondary);
+ font-size: 0.8125rem;
+ font-weight: 650;
+ text-decoration: none;
+ transition:
+ color var(--motion-fast) ease-out,
+ background-color var(--motion-fast) ease-out;
+}
+
+.site-navigation a:hover,
+.site-navigation a[aria-current="true"] {
+ background: var(--surface-subtle);
+ color: var(--text-primary);
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ justify-self: end;
+ gap: var(--space-2);
+}
+
+.icon-button {
+ display: inline-grid;
+ width: 40px;
+ height: 40px;
+ padding: 0;
+ place-items: center;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-control);
+ background: var(--surface-raised);
+ color: var(--text-primary);
+ cursor: pointer;
+ transition:
+ border-color var(--motion-fast) ease-out,
+ background-color var(--motion-fast) ease-out,
+ transform var(--motion-fast) ease-out;
+}
+
+.icon-button:hover {
+ border-color: var(--border-control);
+ background: var(--surface-subtle);
+}
+
+.icon-button:active {
+ transform: translateY(1px);
+}
+
+.icon-button svg {
+ width: 19px;
+ height: 19px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.8;
+}
+
+.theme-icon--moon {
+ display: none;
+}
+
+[data-theme="dark"] .theme-icon--sun {
+ display: none;
+}
+
+[data-theme="dark"] .theme-icon--moon {
+ display: block;
+}
+
+.menu-toggle {
+ display: none;
+}
+
+.button {
+ display: inline-flex;
+ min-height: 44px;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ padding: 10px 20px;
+ border: 1px solid transparent;
+ border-radius: var(--radius-control);
+ font-weight: 720;
+ line-height: 1.2;
+ text-align: center;
+ text-decoration: none;
+ transition:
+ color var(--motion-fast) ease-out,
+ background-color var(--motion-fast) ease-out,
+ border-color var(--motion-fast) ease-out,
+ box-shadow var(--motion-fast) ease-out,
+ transform var(--motion-fast) ease-out;
+}
+
+.button:not(.is-disabled):hover {
+ transform: translateY(-1px);
+}
+
+.button--primary {
+ background: var(--accent-solid);
+ box-shadow: var(--shadow-button);
+ color: var(--text-on-accent);
+}
+
+.button--primary:not(.is-disabled):hover {
+ background: var(--accent-solid-hover);
+ box-shadow: none;
+}
+
+.button--secondary {
+ border-color: var(--border-control);
+ background: var(--surface-raised);
+ color: var(--text-primary);
+}
+
+.button--secondary:not(.is-disabled):hover,
+.button--quiet:hover {
+ border-color: var(--accent-selected);
+ background: var(--accent-subtle);
+}
+
+.button--quiet {
+ min-height: 40px;
+ padding: var(--space-2) var(--space-4);
+ border-color: var(--border-default);
+ background: var(--surface-raised);
+ color: var(--text-primary);
+}
+
+.button.is-disabled {
+ border-color: var(--border-default);
+ background: var(--surface-muted);
+ box-shadow: none;
+ color: var(--text-muted);
+ cursor: not-allowed;
+}
+
+.hero {
+ position: relative;
+ min-height: 820px;
+ padding-top: 164px;
+ overflow: hidden;
+}
+
+.hero::before {
+ position: absolute;
+ top: -240px;
+ left: 50%;
+ width: 1000px;
+ height: 650px;
+ border: 1px solid color-mix(in srgb, var(--accent) 9%, transparent);
+ border-radius: 50%;
+ background:
+ radial-gradient(circle at 32% 45%, color-mix(in srgb, var(--accent) 13%, transparent), transparent 34%),
+ radial-gradient(circle at 68% 52%, color-mix(in srgb, var(--accent-mint) 12%, transparent), transparent 36%);
+ content: "";
+ filter: blur(2px);
+ pointer-events: none;
+ transform: translateX(-50%);
+}
+
+.hero-grid {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(0, 0.86fr) minmax(520px, 1.14fr);
+ align-items: center;
+ gap: var(--space-16);
+}
+
+.hero-copy {
+ position: relative;
+ z-index: 2;
+}
+
+.eyebrow,
+.kicker {
+ color: var(--accent);
+ font-size: 0.75rem;
+ font-weight: 800;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-bottom: var(--space-6);
+ padding: 7px var(--space-3);
+ border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--border-default));
+ border-radius: var(--radius-pill);
+ background: color-mix(in srgb, var(--accent-subtle) 72%, transparent);
+}
+
+.status-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--success);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 14%, transparent);
+}
+
+.hero h1 {
+ max-width: 760px;
+ margin-bottom: var(--space-6);
+ font-size: var(--font-page-title);
+ letter-spacing: -0.065em;
+}
+
+.hero h1 span {
+ background: linear-gradient(110deg, var(--accent), var(--accent-cyan) 48%, var(--accent-mint));
+ background-clip: text;
+ color: transparent;
+}
+
+.hero-lead {
+ max-width: 620px;
+ margin-bottom: var(--space-8);
+ font-size: 1.0625rem;
+ line-height: 1.85;
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-3);
+ margin-bottom: var(--space-8);
+}
+
+.hero-facts {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-4) var(--space-6);
+ padding: 0;
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ list-style: none;
+}
+
+.hero-facts li {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.hero-facts svg {
+ width: 16px;
+ height: 16px;
+ fill: none;
+ stroke: var(--success);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 2;
+}
+
+.product-stage {
+ position: relative;
+ z-index: 1;
+ min-height: 540px;
+}
+
+.stage-glow {
+ position: absolute;
+ border-radius: 50%;
+ filter: blur(65px);
+ opacity: 0.55;
+ pointer-events: none;
+}
+
+.stage-glow--one {
+ top: 10%;
+ left: 10%;
+ width: 280px;
+ height: 280px;
+ background: color-mix(in srgb, var(--accent) 34%, transparent);
+}
+
+.stage-glow--two {
+ right: -10%;
+ bottom: 0;
+ width: 310px;
+ height: 310px;
+ background: color-mix(in srgb, var(--accent-mint) 26%, transparent);
+}
+
+.app-window {
+ position: absolute;
+ top: 22px;
+ left: 0;
+ width: 680px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--border-control) 70%, transparent);
+ border-radius: 18px;
+ background: var(--surface-raised);
+ box-shadow: var(--shadow-dialog);
+ transform: perspective(1400px) rotateY(-5deg) rotateX(2deg);
+ transform-origin: left center;
+}
+
+.window-bar {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ height: 42px;
+ padding-inline: var(--space-4);
+ border-bottom: 1px solid var(--border-subtle);
+ background: var(--surface-subtle);
+ color: var(--text-muted);
+ font-size: var(--font-caption);
+}
+
+.window-dots {
+ display: flex;
+ gap: 6px;
+}
+
+.window-dots span {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--border-control);
+}
+
+.window-dots span:first-child {
+ background: var(--danger);
+}
+
+.window-dots span:nth-child(2) {
+ background: var(--warning);
+}
+
+.window-dots span:last-child {
+ background: var(--success);
+}
+
+.window-title {
+ font-weight: 680;
+}
+
+.window-status {
+ display: flex;
+ align-items: center;
+ justify-self: end;
+ gap: 5px;
+}
+
+.window-status span {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--success);
+}
+
+.app-layout {
+ display: grid;
+ grid-template-columns: 116px 1fr;
+ height: 420px;
+}
+
+.app-sidebar {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: var(--space-3) var(--space-2);
+ border-right: 1px solid var(--border-subtle);
+ background: color-mix(in srgb, var(--surface-subtle) 65%, var(--surface-raised));
+}
+
+.mini-brand {
+ display: grid;
+ width: 30px;
+ height: 30px;
+ margin: 0 0 var(--space-3) 6px;
+ place-items: center;
+ border-radius: var(--radius-control);
+ background: var(--accent-subtle);
+}
+
+.mini-brand svg {
+ width: 22px;
+ fill: none;
+ stroke: var(--accent);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 3;
+}
+
+.mini-brand path:last-child {
+ stroke: var(--accent-mint);
+}
+
+.side-item {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 6px 7px;
+ border-radius: 6px;
+ color: var(--text-muted);
+ font-size: 9px;
+ font-weight: 650;
+ white-space: nowrap;
+}
+
+.side-item span {
+ width: 12px;
+ height: 12px;
+ border: 1.5px solid currentColor;
+ border-radius: 3px;
+}
+
+.side-item.is-active {
+ background: var(--accent-subtle);
+ color: var(--accent);
+}
+
+.sidebar-spacer {
+ flex: 1;
+}
+
+.app-content {
+ position: relative;
+ background: var(--surface-raised);
+}
+
+.app-content-header {
+ display: flex;
+ height: 62px;
+ align-items: center;
+ justify-content: space-between;
+ padding-inline: var(--space-4);
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+.app-content-header > div:first-child {
+ display: flex;
+ flex-direction: column;
+}
+
+.app-content-header strong {
+ font-size: 12px;
+}
+
+.app-content-header span {
+ color: var(--text-muted);
+ font-size: 8px;
+}
+
+.mode-pill {
+ padding: 4px 8px;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-pill);
+ background: var(--surface-subtle);
+ color: var(--text-secondary);
+ font-size: 8px;
+}
+
+.message-area {
+ padding: 22px 38px 76px;
+}
+
+.message {
+ max-width: 85%;
+ font-size: 10px;
+}
+
+.message--user {
+ padding: 10px 12px;
+ margin: 0 0 18px auto;
+ border-radius: 10px 10px 2px 10px;
+ background: var(--accent-solid);
+ color: var(--text-on-accent);
+}
+
+.assistant-label {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ margin-bottom: var(--space-2);
+}
+
+.assistant-label strong {
+ font-size: 10px;
+}
+
+.assistant-avatar {
+ display: grid;
+ width: 23px;
+ height: 23px;
+ place-items: center;
+ border-radius: 7px;
+ background: linear-gradient(135deg, var(--accent), var(--accent-mint));
+ color: var(--text-on-accent);
+ font-size: 9px;
+ font-weight: 800;
+}
+
+.message--assistant p {
+ margin-bottom: var(--space-3);
+ color: var(--text-secondary);
+ line-height: 1.7;
+}
+
+.tool-card {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: 9px;
+ padding: 9px;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-control);
+ background: var(--surface-subtle);
+}
+
+.tool-icon {
+ display: grid;
+ width: 26px;
+ height: 26px;
+ place-items: center;
+ border-radius: 6px;
+ background: var(--surface-raised);
+ color: var(--accent);
+}
+
+.tool-icon svg {
+ width: 14px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-width: 2;
+}
+
+.tool-card > div:nth-child(2) {
+ display: flex;
+ flex-direction: column;
+}
+
+.tool-card strong {
+ font-size: 8px;
+}
+
+.tool-card span {
+ color: var(--text-muted);
+ font-size: 7px;
+}
+
+.tool-card .tool-state {
+ color: var(--success);
+}
+
+.plan-lines {
+ display: grid;
+ gap: 6px;
+ margin-top: var(--space-3);
+}
+
+.plan-lines span {
+ height: 5px;
+ border-radius: var(--radius-pill);
+ background: var(--surface-muted);
+}
+
+.plan-lines span:nth-child(2) {
+ width: 88%;
+}
+
+.plan-lines span:nth-child(3) {
+ width: 62%;
+}
+
+.composer {
+ position: absolute;
+ right: 28px;
+ bottom: 18px;
+ left: 28px;
+ display: flex;
+ min-height: 48px;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 12px;
+ border: 1px solid var(--border-control);
+ border-radius: 10px;
+ background: var(--surface-raised);
+ box-shadow: 0 5px 18px color-mix(in srgb, var(--text-primary) 7%, transparent);
+ color: var(--text-muted);
+ font-size: 9px;
+}
+
+.composer-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.composer-actions span {
+ padding: 3px 7px;
+ border-radius: var(--radius-pill);
+ background: var(--surface-subtle);
+ color: var(--text-secondary);
+ font-size: 7px;
+}
+
+.composer-actions b {
+ display: grid;
+ width: 24px;
+ height: 24px;
+ place-items: center;
+ border-radius: 7px;
+ background: var(--accent-solid);
+ color: var(--text-on-accent);
+}
+
+.floating-card {
+ position: absolute;
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+ border: 1px solid color-mix(in srgb, var(--border-control) 70%, transparent);
+ border-radius: var(--radius-card);
+ background: var(--surface-overlay);
+ box-shadow: var(--shadow-card);
+ backdrop-filter: blur(14px);
+}
+
+.floating-card > span:last-child {
+ display: flex;
+ flex-direction: column;
+}
+
+.floating-card strong {
+ font-size: 10px;
+}
+
+.floating-card small {
+ color: var(--text-muted);
+ font-size: 8px;
+}
+
+.floating-card--approval {
+ right: -32px;
+ bottom: 58px;
+}
+
+.floating-card--scope {
+ top: -5px;
+ right: -20px;
+}
+
+.floating-icon {
+ display: grid;
+ width: 30px;
+ height: 30px;
+ place-items: center;
+ border-radius: var(--radius-control);
+ background: var(--success-subtle);
+ color: var(--success);
+}
+
+.floating-icon svg {
+ width: 17px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.8;
+}
+
+.scope-dot {
+ width: 9px;
+ height: 9px;
+ border: 2px solid var(--surface-raised);
+ border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-subtle);
+}
+
+.proof-strip {
+ border-block: 1px solid var(--border-subtle);
+ background: color-mix(in srgb, var(--surface-raised) 55%, transparent);
+}
+
+.proof-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ padding-block: var(--space-8);
+}
+
+.proof-grid div {
+ display: flex;
+ min-height: 56px;
+ flex-direction: column;
+ justify-content: center;
+ padding-inline: var(--space-8);
+ border-right: 1px solid var(--border-subtle);
+}
+
+.proof-grid div:first-child {
+ padding-left: 0;
+}
+
+.proof-grid div:last-child {
+ border-right: 0;
+}
+
+.proof-grid strong {
+ color: var(--text-primary);
+ font-size: 1.25rem;
+}
+
+.proof-grid span {
+ color: var(--text-muted);
+ font-size: 0.75rem;
+}
+
+.section-heading {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(300px, 0.55fr);
+ align-items: end;
+ gap: var(--space-16);
+ margin-bottom: var(--space-12);
+}
+
+.section-heading h2,
+.section-heading p {
+ margin-bottom: 0;
+}
+
+.kicker {
+ margin-bottom: var(--space-3);
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-4);
+}
+
+.feature-card {
+ position: relative;
+ min-height: 330px;
+ padding: var(--space-8);
+ overflow: hidden;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-card);
+ background: var(--surface-raised);
+ transition:
+ border-color var(--motion-normal) ease-out,
+ transform var(--motion-normal) ease-out,
+ box-shadow var(--motion-normal) ease-out;
+}
+
+.feature-card:hover {
+ border-color: var(--accent-selected);
+ box-shadow: var(--shadow-card);
+ transform: translateY(-3px);
+}
+
+.feature-card--wide {
+ grid-column: span 2;
+}
+
+.feature-card--accent {
+ background:
+ radial-gradient(circle at 100% 0, color-mix(in srgb, var(--accent-mint) 14%, transparent), transparent 40%),
+ radial-gradient(circle at 0 100%, color-mix(in srgb, var(--accent) 13%, transparent), transparent 42%),
+ var(--surface-raised);
+}
+
+.feature-icon {
+ display: grid;
+ width: 46px;
+ height: 46px;
+ margin-bottom: var(--space-8);
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--accent) 20%, var(--border-default));
+ border-radius: var(--radius-card);
+ background: var(--accent-subtle);
+ color: var(--accent);
+}
+
+.feature-icon svg {
+ width: 22px;
+ height: 22px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.6;
+}
+
+.feature-number {
+ position: absolute;
+ top: var(--space-6);
+ right: var(--space-6);
+ color: var(--text-muted);
+ font-family: "SFMono-Regular", Consolas, monospace;
+ font-size: 0.6875rem;
+}
+
+.feature-card h3 {
+ margin-bottom: var(--space-3);
+}
+
+.feature-card p {
+ max-width: 580px;
+ margin-bottom: 0;
+}
+
+.mode-row,
+.provider-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ margin-top: var(--space-6);
+}
+
+.mode-row span,
+.provider-pills span {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: 6px 10px;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-pill);
+ background: var(--surface-subtle);
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ font-weight: 680;
+}
+
+.mode-row small {
+ color: var(--text-muted);
+ font-size: var(--font-caption);
+ font-weight: 500;
+}
+
+.mode-row .is-accent {
+ border-color: color-mix(in srgb, var(--accent) 35%, var(--border-default));
+ background: var(--accent-subtle);
+ color: var(--accent);
+}
+
+.release-section {
+ border-block: 1px solid var(--border-subtle);
+ background: var(--surface-raised);
+}
+
+.release-heading {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ align-items: center;
+ gap: var(--space-16);
+ margin-bottom: var(--space-12);
+}
+
+.release-heading > div:last-child {
+ max-width: 680px;
+}
+
+.release-heading h2 {
+ margin-bottom: var(--space-4);
+}
+
+.release-heading p:last-child {
+ margin-bottom: 0;
+}
+
+.version-lockup {
+ position: relative;
+ display: flex;
+ height: 160px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-large);
+ background:
+ linear-gradient(140deg, color-mix(in srgb, var(--accent) 15%, transparent), color-mix(in srgb, var(--accent-mint) 14%, transparent)),
+ var(--surface-subtle);
+}
+
+.version-lockup::before,
+.version-lockup::after {
+ position: absolute;
+ width: 150px;
+ height: 150px;
+ border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
+ border-radius: 50%;
+ content: "";
+}
+
+.version-lockup::before {
+ top: -100px;
+ right: -50px;
+}
+
+.version-lockup::after {
+ bottom: -110px;
+ left: -30px;
+}
+
+.version-lockup span {
+ color: var(--text-muted);
+ font-family: "SFMono-Regular", Consolas, monospace;
+ font-size: var(--font-caption);
+ letter-spacing: 0.24em;
+}
+
+.version-lockup strong {
+ font-size: 3.4rem;
+ letter-spacing: -0.07em;
+}
+
+.release-list {
+ padding: 0;
+ margin: 0;
+ border-top: 1px solid var(--border-default);
+ list-style: none;
+}
+
+.release-item {
+ display: grid;
+ grid-template-columns: 56px minmax(280px, 0.8fr) minmax(340px, 1.2fr);
+ align-items: center;
+ gap: var(--space-8);
+ min-height: 210px;
+ padding-block: var(--space-8);
+ border-bottom: 1px solid var(--border-default);
+}
+
+.release-index {
+ align-self: start;
+ padding-top: 5px;
+ color: var(--text-muted);
+ font-family: "SFMono-Regular", Consolas, monospace;
+ font-size: 0.6875rem;
+}
+
+.release-copy h3 {
+ margin: var(--space-3) 0;
+ font-size: 1.45rem;
+}
+
+.release-copy p {
+ margin-bottom: 0;
+}
+
+.release-label {
+ display: inline-flex;
+ padding: 4px 9px;
+ border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border-default));
+ border-radius: var(--radius-pill);
+ background: var(--accent-subtle);
+ color: var(--accent);
+ font-size: var(--font-caption);
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.release-label--preview {
+ border-color: color-mix(in srgb, var(--warning) 30%, var(--border-default));
+ background: color-mix(in srgb, var(--warning) 10%, var(--surface-subtle));
+ color: var(--warning);
+}
+
+.release-visual {
+ min-height: 130px;
+ padding: var(--space-6);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-card);
+ background: var(--surface-subtle);
+}
+
+.route-visual {
+ position: relative;
+}
+
+.route-node {
+ position: absolute;
+ z-index: 2;
+ display: grid;
+ min-width: 64px;
+ min-height: 34px;
+ padding: var(--space-2);
+ place-items: center;
+ border: 1px solid var(--border-control);
+ border-radius: var(--radius-control);
+ background: var(--surface-raised);
+ color: var(--text-secondary);
+ font-size: 0.6875rem;
+ font-weight: 700;
+}
+
+.route-node--main {
+ top: 47px;
+ left: var(--space-6);
+ border-color: var(--accent-selected);
+ background: var(--accent-subtle);
+ color: var(--accent);
+}
+
+.route-node--sub-one {
+ top: 18px;
+ right: var(--space-6);
+}
+
+.route-node--sub-two {
+ right: var(--space-6);
+ bottom: 18px;
+}
+
+.route-line {
+ position: absolute;
+ left: 96px;
+ width: calc(100% - 185px);
+ height: 1px;
+ background: var(--border-control);
+ transform-origin: left center;
+}
+
+.route-line--one {
+ top: 62px;
+ transform: rotate(-11deg);
+}
+
+.route-line--two {
+ top: 72px;
+ transform: rotate(11deg);
+}
+
+.channel-visual {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.channel-visual span {
+ display: flex;
+ min-height: 72px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-control);
+ background: var(--surface-raised);
+}
+
+.channel-visual b {
+ font-size: 0.75rem;
+}
+
+.channel-visual small {
+ color: var(--warning);
+ font-size: var(--font-caption);
+}
+
+.channel-visual .is-experimental {
+ border-style: dashed;
+}
+
+.channel-visual .is-experimental small {
+ color: var(--text-muted);
+}
+
+.attachment-visual {
+ display: grid;
+ grid-template-columns: 1fr auto auto;
+ align-items: center;
+ gap: var(--space-8);
+}
+
+.attachment-stack {
+ position: relative;
+ height: 76px;
+}
+
+.attachment-stack span {
+ position: absolute;
+ top: 8px;
+ left: 28%;
+ width: 66px;
+ height: 66px;
+ border: 1px solid var(--border-control);
+ border-radius: var(--radius-control);
+ background:
+ linear-gradient(145deg, color-mix(in srgb, var(--accent) 22%, transparent), color-mix(in srgb, var(--accent-mint) 20%, transparent)),
+ var(--surface-raised);
+}
+
+.attachment-stack span:nth-child(2) {
+ transform: translateX(-15px) rotate(-8deg);
+}
+
+.attachment-stack span:nth-child(3) {
+ transform: translateX(15px) rotate(8deg);
+}
+
+.attachment-visual > div:not(:first-child) {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.attachment-visual strong {
+ font-size: 1.8rem;
+}
+
+.attachment-visual small {
+ color: var(--text-muted);
+ font-size: var(--font-caption);
+}
+
+.quality-visual {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ align-items: center;
+ gap: var(--space-2);
+}
+
+.quality-visual span {
+ display: grid;
+ min-height: 38px;
+ place-items: center;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-control);
+ background: var(--surface-raised);
+ color: var(--text-muted);
+ font-family: "SFMono-Regular", Consolas, monospace;
+ font-size: var(--font-caption);
+}
+
+.quality-visual .is-selected {
+ border-color: var(--accent-selected);
+ background: var(--accent-subtle);
+ color: var(--accent);
+ font-weight: 800;
+}
+
+.section-heading--center {
+ display: flex;
+ max-width: 780px;
+ flex-direction: column;
+ align-items: center;
+ gap: 0;
+ margin-inline: auto;
+ text-align: center;
+}
+
+.section-heading--center p {
+ max-width: 680px;
+}
+
+.download-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: var(--space-4);
+}
+
+.download-card {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: var(--space-4);
+ padding: var(--space-6);
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-card);
+ background: var(--surface-raised);
+}
+
+.download-card h3 {
+ margin: 3px 0 5px;
+}
+
+.download-card p {
+ margin-bottom: 0;
+ color: var(--text-muted);
+ font-size: 0.75rem;
+}
+
+.platform-icon {
+ display: grid;
+ width: 48px;
+ height: 48px;
+ place-items: center;
+ border-radius: var(--radius-card);
+ background: var(--surface-subtle);
+ color: var(--text-primary);
+}
+
+.platform-icon svg {
+ width: 23px;
+ height: 23px;
+ fill: currentColor;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.3;
+}
+
+.download-card:nth-child(3) .platform-icon svg {
+ fill: none;
+}
+
+.button--download {
+ grid-column: 1 / -1;
+ width: 100%;
+ margin-top: var(--space-2);
+}
+
+.release-notice {
+ display: flex;
+ max-width: 760px;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-4);
+ margin: var(--space-8) auto 0;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-control);
+ background: var(--surface-subtle);
+}
+
+.release-notice svg {
+ width: 22px;
+ flex: 0 0 auto;
+ fill: none;
+ stroke: var(--text-muted);
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 1.8;
+}
+
+.release-notice div {
+ display: flex;
+ flex-direction: column;
+}
+
+.release-notice strong {
+ font-size: 0.75rem;
+}
+
+.release-notice span {
+ color: var(--text-muted);
+ font-size: 0.6875rem;
+}
+
+.security-section {
+ border-block: 1px solid color-mix(in srgb, var(--text-on-inverse) 12%, transparent);
+ background: var(--surface-inverse);
+ color: var(--text-on-inverse);
+}
+
+.security-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 0.8fr) minmax(520px, 1.2fr);
+ gap: 96px;
+}
+
+.security-intro {
+ position: sticky;
+ top: 112px;
+ align-self: start;
+}
+
+.security-intro h2 {
+ color: var(--text-on-inverse);
+}
+
+.security-intro p:not(.kicker) {
+ color: color-mix(in srgb, var(--text-on-inverse) 70%, transparent);
+}
+
+.security-shield {
+ display: grid;
+ width: 76px;
+ height: 76px;
+ margin-bottom: var(--space-8);
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--accent-cyan) 40%, transparent);
+ border-radius: 20px;
+ background: color-mix(in srgb, var(--accent-cyan) 10%, transparent);
+ color: var(--accent-cyan);
+}
+
+.security-shield svg {
+ width: 42px;
+ fill: none;
+ stroke: currentColor;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-width: 2;
+}
+
+.text-link {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-top: var(--space-3);
+ color: var(--accent-cyan);
+ font-weight: 750;
+ text-decoration: none;
+}
+
+.text-link:hover {
+ text-decoration: underline;
+ text-underline-offset: 4px;
+}
+
+.security-list {
+ border-top: 1px solid color-mix(in srgb, var(--text-on-inverse) 18%, transparent);
+}
+
+.security-list article {
+ display: grid;
+ grid-template-columns: 42px 1fr;
+ gap: var(--space-6);
+ padding-block: var(--space-8);
+ border-bottom: 1px solid color-mix(in srgb, var(--text-on-inverse) 18%, transparent);
+}
+
+.security-number {
+ padding-top: 4px;
+ color: color-mix(in srgb, var(--text-on-inverse) 42%, transparent);
+ font-family: "SFMono-Regular", Consolas, monospace;
+ font-size: 0.6875rem;
+}
+
+.security-list h3 {
+ margin-bottom: var(--space-3);
+ color: var(--text-on-inverse);
+}
+
+.security-list p {
+ margin-bottom: 0;
+ color: color-mix(in srgb, var(--text-on-inverse) 67%, transparent);
+}
+
+.final-cta {
+ padding-block: var(--space-20);
+}
+
+.cta-card {
+ position: relative;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ gap: var(--space-12);
+ padding: var(--space-16);
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--accent) 20%, var(--border-default));
+ border-radius: var(--radius-large);
+ background:
+ radial-gradient(circle at 85% 10%, color-mix(in srgb, var(--accent-mint) 14%, transparent), transparent 26%),
+ radial-gradient(circle at 0 100%, color-mix(in srgb, var(--accent) 15%, transparent), transparent 32%),
+ var(--surface-raised);
+}
+
+.cta-card > div:not(.cta-orbit) {
+ position: relative;
+ z-index: 2;
+}
+
+.cta-card h2 {
+ max-width: 760px;
+ margin-bottom: var(--space-4);
+ font-size: clamp(2rem, 4vw, 3.4rem);
+}
+
+.cta-card p:last-child {
+ margin-bottom: 0;
+}
+
+.cta-actions {
+ display: flex;
+ min-width: 170px;
+ flex-direction: column;
+ gap: var(--space-3);
+}
+
+.cta-orbit,
+.cta-orbit::before,
+.cta-orbit::after {
+ position: absolute;
+ border: 1px solid color-mix(in srgb, var(--accent) 15%, transparent);
+ border-radius: 50%;
+}
+
+.cta-orbit {
+ top: -190px;
+ right: -100px;
+ width: 500px;
+ height: 500px;
+}
+
+.cta-orbit::before {
+ inset: 55px;
+ content: "";
+}
+
+.cta-orbit::after {
+ inset: 115px;
+ content: "";
+}
+
+.cta-orbit span {
+ position: absolute;
+ border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 20px color-mix(in srgb, var(--accent) 60%, transparent);
+}
+
+.cta-orbit span:first-child {
+ top: 100px;
+ left: 28px;
+ width: 8px;
+ height: 8px;
+}
+
+.cta-orbit span:last-child {
+ right: 80px;
+ bottom: 62px;
+ width: 12px;
+ height: 12px;
+ background: var(--accent-mint);
+}
+
+.site-footer {
+ padding-block: var(--space-12);
+ border-top: 1px solid var(--border-subtle);
+ background: var(--surface-raised);
+}
+
+.footer-inner {
+ display: grid;
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ gap: var(--space-8);
+}
+
+.brand--footer {
+ font-size: 1rem;
+}
+
+.brand--footer .brand-mark {
+ width: 30px;
+ height: 30px;
+}
+
+.footer-inner p {
+ margin-bottom: 0;
+ font-size: 0.75rem;
+}
+
+.footer-links {
+ display: flex;
+ gap: var(--space-6);
+}
+
+.footer-links a {
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ font-weight: 650;
+ text-decoration: none;
+}
+
+.footer-links a:hover {
+ color: var(--accent);
+}
+
+.footer-inner small {
+ grid-column: 1 / -1;
+ padding-top: var(--space-6);
+ border-top: 1px solid var(--border-subtle);
+ color: var(--text-muted);
+ font-size: var(--font-caption);
+}
+
+@media (max-width: 1199px) {
+ .section-inner,
+ .header-inner {
+ width: min(100% - 48px, 1040px);
+ }
+
+ .hero-grid {
+ grid-template-columns: minmax(0, 0.9fr) minmax(450px, 1.1fr);
+ gap: var(--space-10);
+ }
+
+ .app-window {
+ width: 620px;
+ transform: scale(0.9) perspective(1400px) rotateY(-4deg) rotateX(2deg);
+ transform-origin: left top;
+ }
+
+ .floating-card--approval {
+ right: -10px;
+ }
+
+ .security-grid {
+ gap: var(--space-16);
+ }
+}
+
+@media (max-width: 959px) {
+ .section {
+ padding-block: 88px;
+ }
+
+ .header-inner {
+ grid-template-columns: auto 1fr auto;
+ }
+
+ .site-navigation {
+ justify-self: center;
+ }
+
+ .header-github {
+ display: none;
+ }
+
+ .hero {
+ min-height: auto;
+ padding-top: 140px;
+ }
+
+ .hero-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .hero-copy {
+ max-width: 720px;
+ text-align: center;
+ }
+
+ .eyebrow {
+ margin-inline: auto;
+ }
+
+ .hero-lead {
+ margin-inline: auto;
+ }
+
+ .hero-actions,
+ .hero-facts {
+ justify-content: center;
+ }
+
+ .product-stage {
+ width: min(100%, 690px);
+ min-height: 500px;
+ margin-inline: auto;
+ }
+
+ .app-window {
+ width: 680px;
+ transform: scale(0.93);
+ transform-origin: center top;
+ }
+
+ .floating-card--scope {
+ right: 0;
+ }
+
+ .floating-card--approval {
+ right: 0;
+ bottom: 34px;
+ }
+
+ .proof-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .proof-grid div:nth-child(2) {
+ border-right: 0;
+ }
+
+ .proof-grid div:nth-child(n + 3) {
+ border-top: 1px solid var(--border-subtle);
+ }
+
+ .proof-grid div:nth-child(3) {
+ padding-left: 0;
+ }
+
+ .section-heading {
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+ }
+
+ .section-heading > p {
+ max-width: 640px;
+ }
+
+ .feature-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .feature-card--wide {
+ grid-column: span 2;
+ }
+
+ .release-item {
+ grid-template-columns: 40px minmax(230px, 0.8fr) minmax(300px, 1.2fr);
+ gap: var(--space-4);
+ }
+
+ .release-visual {
+ padding: var(--space-4);
+ }
+
+ .download-grid {
+ grid-template-columns: 1fr;
+ max-width: 680px;
+ margin-inline: auto;
+ }
+
+ .download-card {
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ }
+
+ .button--download {
+ grid-column: auto;
+ width: auto;
+ margin-top: 0;
+ }
+
+ .security-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .security-intro {
+ position: static;
+ max-width: 720px;
+ }
+
+ .cta-card {
+ grid-template-columns: 1fr;
+ padding: var(--space-12);
+ }
+
+ .cta-actions {
+ min-width: 0;
+ flex-direction: row;
+ }
+}
+
+@media (max-width: 719px) {
+ html {
+ scroll-padding-top: 72px;
+ }
+
+ .section-inner,
+ .header-inner {
+ width: calc(100% - 32px);
+ }
+
+ .section {
+ padding-block: var(--space-16);
+ }
+
+ .header-inner {
+ grid-template-columns: auto 1fr auto;
+ min-height: 64px;
+ }
+
+ .brand {
+ font-size: 1rem;
+ }
+
+ .brand-mark {
+ width: 30px;
+ height: 30px;
+ }
+
+ .menu-toggle {
+ display: inline-grid;
+ justify-self: end;
+ margin-right: var(--space-2);
+ }
+
+ .header-actions {
+ grid-column: 3;
+ grid-row: 1;
+ }
+
+ .site-navigation {
+ position: absolute;
+ top: calc(100% + 1px);
+ right: var(--space-4);
+ left: var(--space-4);
+ display: none;
+ flex-direction: column;
+ align-items: stretch;
+ gap: var(--space-1);
+ padding: var(--space-2);
+ border-radius: var(--radius-card);
+ background: var(--surface-raised);
+ box-shadow: var(--shadow-dialog);
+ }
+
+ .site-header.is-menu-open .site-navigation {
+ display: flex;
+ }
+
+ .site-navigation a {
+ display: flex;
+ min-height: 44px;
+ align-items: center;
+ border-radius: var(--radius-control);
+ }
+
+ .hero {
+ padding-top: 118px;
+ }
+
+ .hero::before {
+ width: 600px;
+ }
+
+ .hero h1 {
+ letter-spacing: -0.055em;
+ }
+
+ .hero-lead {
+ font-size: 0.9375rem;
+ }
+
+ .hero-actions {
+ flex-direction: column;
+ }
+
+ .hero-actions .button {
+ width: 100%;
+ }
+
+ .hero-facts {
+ align-items: center;
+ flex-direction: column;
+ gap: var(--space-2);
+ }
+
+ .product-stage {
+ min-height: 365px;
+ margin-top: var(--space-4);
+ }
+
+ .app-window {
+ left: 50%;
+ width: 680px;
+ transform: translateX(-50%) scale(0.59);
+ transform-origin: top center;
+ }
+
+ .floating-card--scope {
+ top: -2px;
+ right: 2px;
+ }
+
+ .floating-card--approval {
+ right: 2px;
+ bottom: 18px;
+ }
+
+ .floating-card {
+ padding: var(--space-2) 10px;
+ }
+
+ .floating-card strong {
+ font-size: 9px;
+ }
+
+ .floating-card small {
+ max-width: 130px;
+ font-size: 7px;
+ }
+
+ .floating-icon {
+ width: 26px;
+ height: 26px;
+ }
+
+ .proof-grid {
+ grid-template-columns: 1fr;
+ padding-block: var(--space-3);
+ }
+
+ .proof-grid div,
+ .proof-grid div:first-child,
+ .proof-grid div:nth-child(3) {
+ padding: var(--space-3);
+ border-top: 1px solid var(--border-subtle);
+ border-right: 0;
+ text-align: center;
+ }
+
+ .proof-grid div:first-child {
+ border-top: 0;
+ }
+
+ .section-heading {
+ margin-bottom: var(--space-8);
+ }
+
+ .feature-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .feature-card,
+ .feature-card--wide {
+ grid-column: auto;
+ min-height: 0;
+ padding: var(--space-6);
+ }
+
+ .feature-icon {
+ margin-bottom: var(--space-6);
+ }
+
+ .release-heading {
+ grid-template-columns: 1fr;
+ gap: var(--space-8);
+ }
+
+ .version-lockup {
+ width: 100%;
+ max-width: 320px;
+ }
+
+ .release-item {
+ grid-template-columns: 32px 1fr;
+ padding-block: var(--space-8);
+ }
+
+ .release-visual {
+ grid-column: 1 / -1;
+ min-width: 0;
+ margin-top: var(--space-2);
+ }
+
+ .channel-visual {
+ grid-template-columns: 1fr;
+ }
+
+ .attachment-visual {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .attachment-stack {
+ grid-column: 1 / -1;
+ }
+
+ .quality-visual {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .section-heading--center {
+ text-align: left;
+ align-items: flex-start;
+ }
+
+ .download-card {
+ grid-template-columns: auto 1fr;
+ }
+
+ .button--download {
+ grid-column: 1 / -1;
+ width: 100%;
+ }
+
+ .release-notice {
+ align-items: flex-start;
+ }
+
+ .security-grid {
+ gap: var(--space-12);
+ }
+
+ .security-shield {
+ width: 64px;
+ height: 64px;
+ }
+
+ .security-list article {
+ grid-template-columns: 32px 1fr;
+ gap: var(--space-3);
+ }
+
+ .cta-card {
+ padding: var(--space-8) var(--space-6);
+ }
+
+ .cta-actions {
+ flex-direction: column;
+ }
+
+ .footer-inner {
+ grid-template-columns: 1fr;
+ gap: var(--space-4);
+ }
+
+ .footer-links {
+ flex-wrap: wrap;
+ gap: var(--space-3) var(--space-6);
+ }
+
+ .footer-inner small {
+ grid-column: auto;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition-duration: 0.01ms !important;
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ }
+
+ .feature-card:hover,
+ .button:not(.is-disabled):hover {
+ transform: none;
+ }
+}
diff --git a/src/main/agent/approval-summary.test.ts b/src/main/agent/approval-summary.test.ts
index 0119e30..5ed006b 100644
--- a/src/main/agent/approval-summary.test.ts
+++ b/src/main/agent/approval-summary.test.ts
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
-import { safeToolArgumentSummary } from './approval-summary'
+import {
+ safeToolArgumentSummary,
+ safeToolErrorDetail
+} from './approval-summary'
describe('safeToolArgumentSummary', () => {
it('redacts nested sensitive fields', () => {
@@ -26,3 +29,41 @@ describe('safeToolArgumentSummary', () => {
).not.toContain('secret-token')
})
})
+
+describe('safeToolErrorDetail', () => {
+ it('extracts nested runtime errors while redacting secrets', () => {
+ expect(
+ safeToolErrorDetail([
+ {
+ content:
+ 'exit code 1\nAuthorization: Bearer secret-token'
+ }
+ ])
+ ).toBe('exit code 1\nAuthorization: [REDACTED]')
+ expect(
+ safeToolErrorDetail({
+ message:
+ '{"token":"json-secret","authorization":"Basic abc123"}'
+ })
+ ).toBe(
+ '{"token":"[REDACTED]","authorization":"[REDACTED]"}'
+ )
+ })
+
+ it('bounds output and ignores unrelated provider payload fields', () => {
+ expect(
+ safeToolErrorDetail(
+ {
+ content: 'parser failure '.repeat(20),
+ privateDocument: 'must not be returned'
+ },
+ 40
+ )
+ ).toHaveLength(40)
+ expect(
+ safeToolErrorDetail({
+ privateDocument: 'must not be returned'
+ })
+ ).toBeUndefined()
+ })
+})
diff --git a/src/main/agent/approval-summary.ts b/src/main/agent/approval-summary.ts
index f5b2123..d800911 100644
--- a/src/main/agent/approval-summary.ts
+++ b/src/main/agent/approval-summary.ts
@@ -35,16 +35,100 @@ function redactValue(
export function redactSensitiveText(value: string): string {
return value
.replace(
- /\bAuthorization\b(\s*[:=]\s*)Bearer\s+\S+/giu,
+ /\bAuthorization\b(\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\r\n,;}]+)/giu,
'Authorization$1[REDACTED]'
)
.replace(/\bBearer\s+\S+/giu, 'Bearer [REDACTED]')
+ .replace(
+ /(["']?)(api[-_ ]?key|token|secret|password|authorization)\1(\s*[:=]\s*)"[^"\r\n]*"/giu,
+ '$1$2$1$3"[REDACTED]"'
+ )
+ .replace(
+ /(["']?)(api[-_ ]?key|token|secret|password|authorization)\1(\s*[:=]\s*)'[^'\r\n]*'/giu,
+ "$1$2$1$3'[REDACTED]'"
+ )
.replace(
/\b(api[-_ ]?key|token|secret|password|authorization)\b(\s*[:=]\s*|\s+)(["']?)[^\s"',}]+/giu,
'$1$2[REDACTED]'
)
}
+export function safeToolErrorDetail(
+ value: unknown,
+ maximum = 2_000
+): string | undefined {
+ if (!Number.isSafeInteger(maximum) || maximum < 1) {
+ return undefined
+ }
+ const parts: string[] = []
+ let remaining = maximum
+ const seen = new WeakSet
) : (
- (tasks.length > 0 ? tasks : recentTasks).map((task) => (
+ (orderedTasks.length > 0 ? orderedTasks : recentTasks).map((task) => (
@@ -867,9 +962,12 @@ export function RightAssistantSidebar({
{tab === 'artifacts' && (
+
+ 保存并预览由对话生成或手动导入的文本、图片、PDF 与网页内容。
+
- 对话成果
+ 对话与导入成果