Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb5a9c6f43 | ||
|
|
26b87d7ed5 | ||
|
|
93721d69b6 | ||
|
|
8ecd87381f | ||
|
|
dc36e7a967 | ||
|
|
cdecb584d5 | ||
|
|
c4231c8f9c | ||
|
|
8a14cf356a | ||
|
|
19a3bafb63 | ||
|
|
5fac1d3c41 | ||
|
|
42cbfa43f1 | ||
|
|
4d9ab72778 | ||
|
|
be39665ca9 | ||
|
|
1530fa116c | ||
|
|
b36d688900 | ||
|
|
d36889d9f6 | ||
|
|
06c1123caa | ||
|
|
4f3e0bdcfd | ||
|
|
2fca39ddb2 | ||
|
|
2f1881a414 | ||
|
|
dba3380a06 | ||
|
|
77ce13aba3 | ||
|
|
5127f7d045 | ||
|
|
27055693bf | ||
|
|
73cc55bf75 | ||
|
|
00a231da07 | ||
|
|
2833a27f83 | ||
|
|
b1729125e1 | ||
|
|
561de29f00 | ||
|
|
f6f8d5e053 | ||
|
|
8c580a9174 | ||
|
|
efd304fa1d | ||
|
|
7c6f15d3c0 | ||
|
|
6dc219f3a3 | ||
|
|
acc5a8e2d8 | ||
|
|
9f0f231e3b | ||
|
|
967796539f | ||
|
|
8e08ab0bac | ||
|
|
edba31e873 | ||
|
|
0804f5ccd2 | ||
|
|
29639ff516 | ||
|
|
22c80b2d94 | ||
|
|
85e14ca626 | ||
|
|
a99b90cc02 | ||
|
|
ff4fde282f | ||
|
|
f327341b00 | ||
|
|
459cc21e3d | ||
|
|
eb947213c8 | ||
|
|
c2095037ee | ||
|
|
93bf3b908a | ||
|
|
38f6a23306 | ||
|
|
12463ad593 | ||
|
|
618e505b8b | ||
|
|
0a3bf6094a | ||
|
|
957e2ecde7 | ||
|
|
9316fed3e3 | ||
|
|
d6fea0c6c2 | ||
|
|
d9a554b4b1 | ||
|
|
b20c10f820 | ||
|
|
49700bc7cf | ||
|
|
fcdcc8e081 | ||
|
|
de64e9768a | ||
|
|
f9137f0463 | ||
|
|
798ac814a9 | ||
|
|
6913e9ce16 | ||
|
|
9aca3b2627 | ||
|
|
0cb7007429 | ||
|
|
bcd347e092 | ||
|
|
9c50de521e | ||
|
|
da00a850ec | ||
|
|
fcedaf578e | ||
|
|
53269f8b1a | ||
|
|
9388eb79d4 | ||
|
|
500f7d2735 | ||
|
|
e5aebaa67d | ||
|
|
88658aceaa | ||
|
|
098f21b47a | ||
|
|
efecf88af7 | ||
|
|
6c74f057eb | ||
|
|
607bb33249 | ||
|
|
3829a871f5 | ||
|
|
981a174e9f | ||
|
|
1154a6d8da | ||
|
|
076b3aaa7b | ||
|
|
58d774ae0c | ||
|
|
7c34abdfbf | ||
|
|
fb51793595 | ||
|
|
fee4686750 | ||
|
|
63f0f7f0b0 | ||
|
|
97419f3ef7 | ||
|
|
b29f2d7b0d | ||
|
|
a0977645a3 | ||
|
|
aa339a73d6 | ||
|
|
65c3ab9ab0 | ||
|
|
104ac2aec1 | ||
|
|
d7cb7ba181 | ||
|
|
f44a0dc907 | ||
|
|
20c15f74c6 | ||
|
|
34033053be | ||
|
|
993c439228 | ||
|
|
43e1d162dc | ||
|
|
c41d7e8b96 | ||
|
|
ccaab25d11 | ||
|
|
3935f50017 | ||
|
|
28b1590749 | ||
|
|
87dec9baf3 | ||
|
|
0413807b8f | ||
|
|
e387c09f52 | ||
|
|
08884c127c | ||
|
|
7a46422fc1 | ||
|
|
5dddccb282 | ||
|
|
60119a4317 | ||
|
|
7ecb735db0 |
@@ -0,0 +1,602 @@
|
||||
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 Linux ${{ matrix.arch }} compound package
|
||||
needs: authorize
|
||||
if: needs.authorize.outputs.authorized == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
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: Resolve locked native inputs
|
||||
id: inputs
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
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?.[`linux-${process.env.AGENT_ARCH}`]
|
||||
const runtime = remote.runtimes?.opencode
|
||||
const opencode = runtime?.targets?.[process.env.AGENT_ARCH]
|
||||
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.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
|
||||
printf '%s %s\n' "$NODE_SHA256" "$node_archive" |
|
||||
sha256sum --check --strict -
|
||||
|
||||
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_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 \
|
||||
--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-linux-$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?.version !== version ||
|
||||
result.archive !==
|
||||
`goodbuddy-agent-${version}-linux-${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-linux-${{ 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-linux-*
|
||||
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" \
|
||||
"${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 architecture in x64 arm64; do
|
||||
name="goodbuddy-agent-$AGENT_VERSION-linux-$architecture.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="$RUNNER_TEMP/agent-release-notes.md"
|
||||
cat > "$notes" <<EOF
|
||||
GoodBuddy Agent $AGENT_VERSION
|
||||
|
||||
此版本供 GoodBuddy 的“设置 > 平台功能 > 远程项目”手动下载。软件包包含 Agent、固定 Node 和 GoodBuddy 适配的远端 OpenCode Runtime。
|
||||
|
||||
This release is downloaded manually from GoodBuddy Settings > Platform Features > Remote Projects. Each package contains the Agent, pinned Node, and the GoodBuddy-adapted remote OpenCode Runtime.
|
||||
EOF
|
||||
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"
|
||||
@@ -0,0 +1,313 @@
|
||||
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/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-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/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-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-package.test.ts \
|
||||
tests/remote-runtime-bundle.test.ts \
|
||||
tests/agents-workflow.test.ts
|
||||
npm run typecheck
|
||||
|
||||
build:
|
||||
name: Build Linux ${{ matrix.arch }} Agent
|
||||
needs: validate
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
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: Resolve locked official Node runtime
|
||||
id: runtime
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node - <<'NODE'
|
||||
const { appendFileSync } = require('node:fs')
|
||||
const lock = require('./agent-runtime-lock.json')
|
||||
const target = lock.node?.targets?.[`linux-${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.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
|
||||
printf '%s %s\n' "$NODE_SHA256" "$archive" |
|
||||
sha256sum --check --strict -
|
||||
|
||||
- name: Resolve locked OpenCode Runtime
|
||||
id: opencode
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node - <<'NODE'
|
||||
const { appendFileSync } = require('node:fs')
|
||||
const { createHash: hash } = require('node:crypto')
|
||||
const lock = require('./remote-runtime-lock.json')
|
||||
const runtime = lock.runtimes?.opencode
|
||||
const target = runtime?.targets?.[process.env.AGENT_ARCH]
|
||||
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.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
|
||||
run: |
|
||||
npm test -- \
|
||||
src/agent-daemon/model-bridge.test.ts \
|
||||
src/agent-daemon/lifecycle.test.ts
|
||||
|
||||
- name: Build and verify ephemeral signed compound Agent package twice
|
||||
id: agent
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
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 \
|
||||
--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-linux-$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 ||
|
||||
!/^[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_SHA256: ${{ steps.agent.outputs.sha256 }}
|
||||
run: |
|
||||
{
|
||||
echo "### Linux $AGENT_ARCH Agent"
|
||||
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."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
+204
-13
@@ -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 }} --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 }} --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
|
||||
@@ -132,8 +305,13 @@ jobs:
|
||||
|
||||
release:
|
||||
name: Publish GitHub and OSS release
|
||||
if: github.event_name == 'push' && github.ref_type == 'tag'
|
||||
needs: package
|
||||
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: 35
|
||||
environment:
|
||||
@@ -150,7 +328,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
node-version: 24.19.0
|
||||
|
||||
- name: Verify release tag
|
||||
shell: bash
|
||||
@@ -198,11 +376,12 @@ jobs:
|
||||
test -n "$OIDC_PROVIDER_ARN"
|
||||
test -n "$ROLE_ARN"
|
||||
case "$OSS_BUCKET" in
|
||||
*[!a-z0-9-]*|'') echo "OSS Bucket 名称无效" >&2; exit 1 ;;
|
||||
goodbuddy) ;;
|
||||
*) echo "OSS Bucket 必须与应用内置镜像地址一致" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$OSS_ENDPOINT" in
|
||||
https://oss-*.aliyuncs.com) ;;
|
||||
*) echo "OSS Endpoint 必须使用标准 HTTPS 地址" >&2; exit 1 ;;
|
||||
https://oss-cn-beijing.aliyuncs.com) ;;
|
||||
*) echo "OSS Endpoint 必须与应用内置镜像地址一致" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$OIDC_PROVIDER_ARN" in
|
||||
acs:ram::*:oidc-provider/*) ;;
|
||||
@@ -248,18 +427,25 @@ jobs:
|
||||
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"
|
||||
@@ -268,11 +454,13 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -300,12 +488,15 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -4,6 +4,8 @@ dist/
|
||||
coverage/
|
||||
.vite/
|
||||
.runtime-resources/
|
||||
.remote-runtime-resources/
|
||||
.agent-resources/
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
|
||||
@@ -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,17 @@ 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.
|
||||
- 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 +112,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,6 +127,18 @@ 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.
|
||||
- After completing any functional change, inspect the affected product,
|
||||
architecture, design, feature, setup, and operational documentation and
|
||||
@@ -64,6 +151,22 @@ Keep Electron security boundaries intact:
|
||||
- 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
|
||||
@@ -131,95 +234,43 @@ mode.
|
||||
Release note: 修复左上角项目设置与消息通道项目设置不一致的问题;现在从任一入口修改后,另一处会同步显示相同配置。
|
||||
```
|
||||
|
||||
## Release Packaging
|
||||
## Releases
|
||||
|
||||
- `.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.
|
||||
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.
|
||||
|
||||
### Tagged Release Process
|
||||
Repository-wide release invariants:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
When recovering from a version tag whose workflow never published a GitHub
|
||||
Release and its assets:
|
||||
|
||||
- If the approved source and release metadata do not need to change, rerun the
|
||||
failed jobs for the same immutable tag instead of creating another tag.
|
||||
- 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.
|
||||
|
||||
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 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.
|
||||
- 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
|
||||
|
||||
@@ -231,14 +282,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
|
||||
|
||||
@@ -127,13 +127,88 @@ 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/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 和 DSH 插件安装 Runtime:
|
||||
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
|
||||
|
||||
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
|
||||
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
|
||||
@@ -143,12 +218,136 @@ npm run dist:linux:arm64
|
||||
|
||||
跨架构打包前,确认目标架构的 OpenCode 资源已经准备完成。不要用其他架构的二进制替代目标资源。
|
||||
|
||||
GoodBuddy 管理的本机 OpenCode 启动后会轮询已认证的 loopback health 接口,最多等待
|
||||
30 秒。该窗口用于兼容 Linux ARM 等较慢环境;不要改回依赖 stdout 文案或 10 秒固定上限。
|
||||
|
||||
## 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 复合包。每个 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
|
||||
地址与 digest,并从 `remote-runtime-lock.json` 解析固定 OpenCode npm 包与 integrity;
|
||||
下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层
|
||||
测试签名验证、Agent 原生启动 smoke 和确定性 `.gbagent` 对比。
|
||||
|
||||
该验证 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 中
|
||||
原生构建两种架构,生成并签名累计目录,然后同步到非 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.9 的 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、
|
||||
Ask 的固定 `bwrap` 只读 profile 和 `runtime/model-bridge` v1。Agent 通过私有 Unix socket
|
||||
按需 detached 启动,不依赖 systemd、D-Bus 或 Linger。Execute 直接以所选 SSH 账号权限
|
||||
启动签名 Runtime;Ask 才使用只读 bubblewrap。
|
||||
|
||||
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` 请求必须拒绝,Workspace 的真正只读边界仍由 `bwrap` 执行。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 发布失败不会阻塞任何桌面目标。
|
||||
|
||||
本地构建单个平台目标:
|
||||
|
||||
@@ -156,8 +355,21 @@ Linux 的 `x64`、`arm64` 版本。生产 bundle 仅作为短期 Actions artifac
|
||||
npm run release:package -- --platform <windows|macos|linux> --arch <x64|arm64>
|
||||
```
|
||||
|
||||
`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 与
|
||||
ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直接
|
||||
ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
|
||||
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
|
||||
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
|
||||
`release-manifest.json`。
|
||||
@@ -168,10 +380,29 @@ ZIP,以及 Linux 的 AppImage 与 DEB。Windows portable ZIP 解压后可直
|
||||
|
||||
推送 `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 的不可变版本目录,并公开校验 14 个安装包。验证通过后才创建或
|
||||
更新 draft GitHub Release、上传 22 个 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。
|
||||
|
||||
中英文发布说明统一维护在 `resources/release-notes.json`。新版本按“本次
|
||||
亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前
|
||||
@@ -184,13 +415,43 @@ manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流创建
|
||||
|
||||
```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 和 ZIP,并在 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 签名凭据,并重新验证
|
||||
安装、升级和系统安全提示。
|
||||
|
||||
## 发布前冒烟测试
|
||||
|
||||
|
||||
+491
-72
@@ -1,92 +1,511 @@
|
||||
# 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] **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] **File, screenshot, window, and clipboard context**: Added to model
|
||||
context only after explicit user selection.
|
||||
- [x] **Rich responses**: Supports GitHub Flavored Markdown, LaTeX math, and
|
||||
constrained Mermaid diagrams. Large diagrams can be zoomed, panned, or
|
||||
viewed as source, and failed renders preserve the original diagram code.
|
||||
- [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.
|
||||
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.
|
||||
- [ ] **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 future
|
||||
memory and historical execution context belong to the associated Task. See
|
||||
the [Feature PRD](./docs/features/assistant-workbar/prd.md).
|
||||
|
||||
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行和图像生成。
|
||||
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、超时和活动记录。
|
||||
- [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 运行已启用且受边界约束的工具。
|
||||
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析。
|
||||
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
|
||||
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Chat Completions、OpenAI Images 和无认证本机模型。
|
||||
- [x] **上下文用量与自动压缩**:直连模型按每次成功调用更新供应商用量,图片与工具轮次使用同一口径,供应商缺失 usage 时才回退估算;界面明确区分“本次模型调用”和“压缩后对话估算”,压缩线始终根据当前设置与所选模型窗口即时计算,不在每个对话中保存旧配置;压缩标识的前后值使用同一估算口径,运行记录仍保留各次模型调用的供应商 usage。对话与多轮工具 Agent 可在已完成调用越过阈值后自动重复压缩,规划时先为固定提示、工具定义和摘要预留预算;同一回复会分别保留 Agent 工具上下文与对话历史的压缩标识,并在应用重启或较早消息滚出本地历史窗口后继续复用摘要。
|
||||
- [x] **Main-only 凭据保护**:API Key 使用系统安全存储加密,不暴露给 Renderer。
|
||||
- [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;外部 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 监督侧栏**(规划中):在聊天右侧助手工作栏统一承载 OpenCode、Continue 和 DeepSeek Harness 的 Subagent 控制、后台 Job、Workflow/Hook、长任务与原生会话监督;Composer 只保留对当前消息生效的高频上下文选择。
|
||||
- [ ] **可执行 Subagent**(规划中):提供显式 Execute 委派,限制嵌套、并行、Token、时间和工具权限,在右侧 Runtime 监督页签显示父子状态、取消入口和审计归属。
|
||||
### Agent Runtimes and model connections
|
||||
|
||||
### Skills、MCP 与知识库
|
||||
- [x] **Direct model Runtime**: Supports question answering, knowledge
|
||||
synthesis, controlled tool execution, and image generation.
|
||||
- [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.
|
||||
- [x] **OpenCode and Continue**: Use isolated child processes, an environment
|
||||
variable allowlist, unified configuration, cancellation, total execution
|
||||
limits, 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. Execution
|
||||
Runtimes remain reusable per project, so different projects can run in
|
||||
parallel. 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. Ask keeps the Workspace
|
||||
read-only at the Runtime boundary through bubblewrap; Execute has 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 v4. 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/arm64. 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, architecture, size, SHA-256, and fixed URL.
|
||||
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;
|
||||
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 source lock and
|
||||
Agent source lock is `0.11.14`, while the current Desktop release candidate
|
||||
is `0.12.0`; formal publication status follows the separate Agent and Desktop
|
||||
release channels.
|
||||
- [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
|
||||
source can use the current format-v1 package. GitHub, Beijing mirror, Linux
|
||||
x64/arm64, cancellation, and offline GoodBuddy transfer validation remains.
|
||||
This status does not mean the path is published or has completed real-Host
|
||||
testing.
|
||||
- [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. 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.
|
||||
- [x] **Experts and Subagents**: Supports explicit experts, team analysis, and
|
||||
up to three read-only experts running in parallel. Chat shows each
|
||||
expandable full expert response first and the main Agent's synthesis below,
|
||||
and persists both with the conversation.
|
||||
- [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] **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.
|
||||
- [ ] **Executable Subagents** (planned): Explicit Execute delegation with
|
||||
bounded nesting, parallelism, tokens, time, and tool permissions, aggregated
|
||||
by Task in the workbar's fixed Runtime section with cancellation and audit
|
||||
ownership.
|
||||
|
||||
- [x] **Skills 按需接入**:可分配给直连模型、OpenCode、Continue 和 DeepSeek Harness,并使用有界资源和受控 Runtime 边界。
|
||||
- [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,动态工具仍会重新发现并经过现有执行记录与权限边界。
|
||||
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
|
||||
- [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.
|
||||
- [ ] **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 `0.12.0` candidate
|
||||
must pass that matrix. 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, and GoodBuddy
|
||||
configuration 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 are still rediscovered and pass
|
||||
through existing activity and permission boundaries.
|
||||
- [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.
|
||||
- [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 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] **任务、活动与成果**:集中管理任务状态、审计活动和成果文件;Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
|
||||
- [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. 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] **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. 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. Future partitioned memory remains an
|
||||
independently designed long-term direction. 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] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
|
||||
- [x] **内网兼容模式**:默认开启;允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书,关闭后恢复严格地址和证书校验。
|
||||
### Browser, communication, voice, and application maintenance
|
||||
|
||||
### 开源、构建与发布
|
||||
- [x] **Built-in browser for direct models**: Uses GoodBuddy's isolated
|
||||
Chromium and never controls a browser installed by the user. A separate
|
||||
master switch decides whether Execute receives the capability, with no
|
||||
per-use prompt after enablement. 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.
|
||||
- [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 mode**: Enabled by default and permits
|
||||
in-app HTTP plus invalid, self-signed, or expired HTTPS certificates.
|
||||
Disabling it restores strict address and certificate validation.
|
||||
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。
|
||||
### Open source, builds, and releases
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
- [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. Linux also produces AppImage, DEB, and RPM.
|
||||
|
||||
- [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 v4,
|
||||
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. The final `0.11.14` test-signed package again
|
||||
verified two delivered model rounds, one tool, one terminal state,
|
||||
`latest=ACK`, and no owner/journal residue. 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.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# GoodBuddy 功能矩阵与路线图
|
||||
|
||||
[English](./FEATURES.md) | **简体中文**
|
||||
|
||||
本文记录 GoodBuddy 的已提供能力和路线图。未完成项目不代表已包含在当前版本中。
|
||||
|
||||
## 状态说明
|
||||
|
||||
- [x] 已提供
|
||||
- [ ] 开发中或规划中,具体以条目标注为准
|
||||
|
||||
## 功能总表
|
||||
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
|
||||
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式和受控 Mermaid 图表;大图可缩放、拖动或查看源码,失败时保留原始图表代码。
|
||||
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
|
||||
- [x] **助手工作栏、多终端与可调布局**:右侧工作栏使用持久“+”能力目录和应用 Tab;用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
|
||||
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
|
||||
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入监督、统一 Runtime、受管进程、静态安全 HTML 预览、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
|
||||
|
||||
### Agent Runtime 与模型连接
|
||||
|
||||
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行和图像生成。
|
||||
- [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
|
||||
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、总执行时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;执行 Runtime 仍按项目复用,因此不同项目可并行。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
|
||||
- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响任何本地项目、普通桌面功能或桌面发布。启用后可管理固定 Host Key 的 SSH Host、浏览有界远端目录并创建 Ask/Execute 项目;Ask 在 Runtime 边界通过 bubblewrap 保持 Workspace 只读,Execute 使用所选 SSH 账号的完整权限。Agent 通过私有 Unix socket 和 ACP v4 持有 accepted Prompt、Provider/工具轮次、Runtime 进程、稳定模型 ledger 与有界语义 transcript;Desktop 退出、网络中断或本机进程结束后 Host 继续执行,重启后只 attach 原 controller/binding/operation 并把 provenance 原子归并回原对话,不重发 Prompt。模型 profile 与 API Key 只在当前 accepted operation 生命周期内进入 Agent 内存,不写入 Renderer、SSH 参数、远端环境或磁盘。远端组件不嵌入桌面包,项目切换只更新本地选择且不会连接或下载;设置页只读取小型签名目录并按 Linux x64/arm64 显示本地版本、在线最新兼容版本和“有更新/已是最新”,用户点击后才下载签名 `.gbagent`,也可导入/导出离线包。每个复合包包含 Agent、固定 Node 与由桌面源码统一维护适配的 OpenCode Runtime;在线来源跟随“关于与更新”的 GitHub/北京 OSS 选择,签名累计目录绑定最低桌面版本、Agent 协议、架构、大小、SHA-256 和固定 URL。公钥 registry 接受等价 JSON 空白和换行,但仍严格校验 schema、Ed25519 key、环境与撤销;目录、包、manifest、payload 签名和流式 SHA-256 不变。缺少某架构包只使该架构托管 SSH 不可用。项目只保存 Host、远端路径、Runtime 选择和模式;首次实际使用 Workspace/Runtime 时读取 Host current identity,同一进程内其他项目复用已确认 identity 和 Agent 连接。托管 SSH 会话只显示支持的 OpenCode 选择。同一 Host 可并行运行多个项目和对话;恢复进度分别显示网络、Agent、Runtime、已提交事件 cursor、完成或失败/重试,只阻塞受影响项目。Linux x64 真实 Host 已覆盖短/长 detach、本机 harness 强制结束、并发、SSH relay 丢失、取消、确定/不确定 Provider 失败、Agent `SIGKILL`/重启和 reopened Desktop SQLite 恢复;成功工具 START/END 各一次,未发现 Prompt、Provider 或工具重放。当前 Agent 源码 lock 为 `0.11.14`,Desktop 发布候选为 `0.12.0`;正式发布状态以 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 验收**:源码已可使用当前 format v1 包;仍需完成 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。当前状态不表示已发布或已完成真实 Host 测试。
|
||||
- [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] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析;聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
|
||||
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
|
||||
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
|
||||
- [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**(规划中):提供显式 Execute 委派,限制嵌套、并行、Token、时间和工具权限,在助手工作栏固定的 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 已通过真实安装验证。
|
||||
- [ ] **本机工具执行环境发布验收**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv;`0.12.0` 候选仍须通过该矩阵。真实 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,动态工具仍会重新发现并经过现有执行记录与权限边界。
|
||||
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
|
||||
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。
|
||||
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
|
||||
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
|
||||
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
|
||||
- [x] **混合检索测试台**:支持全文、中文词组、向量和图谱通道诊断,可调 Top K、阈值、权重、本地或学习型重排及上下文预算。
|
||||
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
|
||||
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
|
||||
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
|
||||
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。
|
||||
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
|
||||
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
|
||||
|
||||
### 工作管理、长期协作与工作流
|
||||
|
||||
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
|
||||
- [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] **智能心跳入口与范围改善**:将“智能心跳 > 心跳计划”作为完整配置的唯一权威入口,支持创建和编辑 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] **直连模型内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过独立总开关决定是否提供给 Execute,开启后不逐次询问。浏览器工作栏与 Agent 共用同一条按 Conversation 归属的会话和串行操作路径,支持返回、刷新/停止加载、地址输入、前往、交互和关闭;停止加载不会关闭会话,用户导航会改变 Agent 下一步看到的页面。
|
||||
- [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 证书,关闭后恢复严格地址和证书校验。
|
||||
|
||||
### 开源、构建与发布
|
||||
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;Linux 同时生成 AppImage、DEB 和 RPM。
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
|
||||
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
|
||||
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
|
||||
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
|
||||
- [ ] **SSH 主机与远程执行空间发布验收**:主机 CRUD、Host Key、加密凭据、Project UI、Workspace、OpenCode ACP v4、Agent-owned Prompt/gateway/transcript、Ask 只读、Execute 完整账号权限、取消、detached Agent 精确重连和 release-only 双架构资源校验已经接线。Linux x64 当前源码已用真实模型与工具通过 detach、进程结束、relay 丢失、并发、取消、Provider 异常、Agent 重启和 Desktop SQLite 恢复矩阵;最终 `0.11.14` 测试签名包再次验证两个已交付模型轮次、一个工具、一个终态、`latest=ACK` 与零 owner/journal 残留。公开 signing key registry 已供应。控制面直连源码不等待新 installer-bearing 包,但仍需公开核对当前 Linux x64/arm64 正式签名工件矩阵,并完成 GitHub/北京镜像、双架构和离线 GoodBuddy 传输的发布验收;门槛未满足时不宣称该路径已发布或通过正式发布验收。
|
||||
- [ ] **多云远程沙盒 Agent**(规划中):通过云厂商 API 和 SSH Agent 管理专用 Linux 沙盒;凭据留在 Main 进程,高风险控制面操作单独确认。
|
||||
|
||||
## 规划原则
|
||||
|
||||
规划中的工作流、Subagent、MCP、远程 API 和沙盒能力不得绕过现有 Main Runtime、Ask/Execute、权限、取消、超时和审计边界。
|
||||
@@ -1,78 +0,0 @@
|
||||
# GoodBuddy
|
||||
|
||||
English | [简体中文](README.md)
|
||||
|
||||
A secure, cross-platform, local-first desktop AI assistant and Agent workspace.
|
||||
|
||||

|
||||
|
||||
## 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, Continue, and the preview DeepSeek Harness, with cancellation, timeouts, output limits, and process cleanup.
|
||||
- **Open integrations**: Supports OpenAI Responses, OpenAI-compatible Chat Completions, Anthropic Messages, OpenAI Images, Embeddings, cross-runtime Skills and custom MCP, plus a default-off DeepSeek Harness npm plugin marketplace that users enable explicitly.
|
||||
- **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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
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.
|
||||
- The DeepSeek Harness plugin marketplace is off by default. After it is enabled and a third-party plugin is installed, its install scripts, initialization, and Execute tools run with the current user's permissions. Turning off the marketplace only hides its catalog and management interface; it does not disable or uninstall existing plugins. Installation requires explicit confirmation, and Ask limits only model tool calls.
|
||||
- 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.
|
||||
@@ -1,49 +1,88 @@
|
||||
<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)
|
||||
|
||||

|
||||
[Website](https://mesalogo.github.io/goodbuddy/en.html) ·
|
||||
[Download](https://github.com/mesalogo/goodbuddy/releases) ·
|
||||
[Feature Matrix](./FEATURES.md) ·
|
||||
[Build Guide](./BUILD.md)
|
||||
|
||||
## 核心能力
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
</div>
|
||||
|
||||
- **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。
|
||||
- **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite;API Key 由系统安全存储加密。
|
||||
- **多 Runtime**:支持直连模型、OpenCode、Continue 和预览版 DeepSeek Harness,统一处理取消、超时、输出限制和进程退出;原生能力清单将 Tools 与 Commands、LSP、Formatters 分开,并显示来源及 Ask/Execute 可用性。内置 OpenCode 提供 Agent、Tool、Command 与原生 Compact,Continue 提供 Rules、Prompt 预设、结构化提问与 GoodBuddy 手动摘要压缩,并明确标记当前版本无法静态发现原生 Tools。
|
||||
- **开放连接**:支持 OpenAI Responses、OpenAI 兼容 Chat Completions、Anthropic Messages、OpenAI Images、Embeddings、跨 Runtime Skills 与自定义 MCP,以及默认关闭、由用户显式开启的 DeepSeek Harness npm 插件市场。MCP 测试可读取有界 Prompt/Resource 元数据,但不会读取 Resource 内容。
|
||||
- **知识工作区**:支持文件、目录和网页导入,以及全文、词组、向量和图谱混合检索。
|
||||
- **工作管理**:集中管理 Projects、对话、任务、活动、成果、记忆、魔法笔记和智能心跳。
|
||||
- **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。
|
||||
- **桌面上下文**:可选择文件、截图、应用窗口、剪贴板和语音作为上下文。
|
||||
- **离线语音**:支持 SenseVoice、Paraformer 和 Whisper 本地模型。
|
||||
- **富文本回答**:支持 Markdown、LaTeX 公式和受控 Mermaid 图表。
|
||||

|
||||
|
||||

|
||||
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.
|
||||
|
||||

|
||||
## 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 | Windows, macOS, and Linux builds for `x64` and `arm64` |
|
||||
| Integrations | Local or hosted model endpoints, custom MCP servers, WeChat ClawBot, WeCom, and DingTalk |
|
||||
|
||||

|
||||
## Product tour
|
||||
|
||||
完整功能和路线图见 [FEATURES.md](FEATURES.md)。
|
||||
### Knowledge bases and graphs
|
||||
|
||||
## 安装
|
||||

|
||||
|
||||
从 [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases) 下载:
|
||||

|
||||
|
||||
| 系统 | 架构 | 格式 |
|
||||
### Magic Notes and Smart Heartbeat
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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 |
|
||||
|
||||
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,20 +91,35 @@ npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
构建与打包说明见 [BUILD.md](BUILD.md)。
|
||||
See [BUILD.md](./BUILD.md) for build and packaging instructions.
|
||||
|
||||
## 隐私与安全
|
||||
## Documentation
|
||||
|
||||
- 模型请求只发送到用户选择的服务。
|
||||
- 本地数据默认保存在系统应用数据目录。
|
||||
- Renderer 不接触原始 Electron API 或模型凭据。
|
||||
- DeepSeek Harness 插件市场默认关闭;开启并安装第三方插件后,其安装脚本、初始化和 Execute 工具以当前用户权限运行。关闭市场只隐藏目录和管理界面,不会停用或卸载已有插件;安装前会明确确认。Ask 只允许 Host 原生 `read`/`skill` 与 Main 管理的 Web Search/Fetch,不允许调用第三方插件工具,也不能限制插件初始化代码。
|
||||
- 远程委派仅在用户配置端点和令牌后启用。
|
||||
- 内网兼容模式允许应用内 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
|
||||
@@ -73,14 +127,8 @@ npm run typecheck
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 社区交流
|
||||
## License
|
||||
|
||||
目前采用微信群的方式供大家高效交流,大家可以微信扫码进入社区群:
|
||||
|
||||
<img width="1279" height="1306" alt="056bfac87840a95547f9805a8122fc2d" src="https://github.com/user-attachments/assets/b3342635-60a1-484c-959c-f90ba3c39d69" />
|
||||
|
||||
|
||||
|
||||
## 开源许可
|
||||
|
||||
GoodBuddy 的原创代码采用 [0BSD License](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.
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
<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)
|
||||
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
[](https://github.com/mesalogo/goodbuddy/releases)
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
GoodBuddy 是用于模型对话、编程 Agent、知识检索、笔记和定时任务的桌面应用。使用 GoodBuddy 无需注册账号,工作数据默认保存在本机。模型服务可以运行在本机、组织内网或用户选择的云端服务。
|
||||
|
||||
## 主要能力
|
||||
|
||||
| 范围 | GoodBuddy 提供的能力 |
|
||||
| --- | --- |
|
||||
| Agent Runtime | 直连模型、OpenCode、Continue 和预览版 DeepSeek Harness |
|
||||
| 数据与执行 | 本地 SQLite、只读 `Ask`、使用当前账号完整权限的 `Execute`、工具调用记录 |
|
||||
| 知识库 | 导入文件、目录和网页,通过全文、中文词组、向量和知识图谱检索 |
|
||||
| 桌面版本 | Windows、macOS、Linux 的 `x64` 与 `arm64` 正式版本,以及龙芯 LoongArch 实验预览版 |
|
||||
| 外部连接 | 本机或内网模型服务、自定义 MCP、微信 ClawBot、企业微信和钉钉 |
|
||||
|
||||
## 产品界面
|
||||
|
||||
### 知识库与知识图谱
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 魔法笔记与智能心跳
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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="360" alt="GoodBuddy 微信社区群二维码" src="https://github.com/user-attachments/assets/10bb7ac2-0bc1-41a9-978d-c2862923fcd6" />
|
||||
|
||||
## 开源许可
|
||||
|
||||
GoodBuddy 原创代码采用 [0BSD License](./LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
|
||||
+190
-22
@@ -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,133 @@
|
||||
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
|
||||
- 当前导航项和当前会话必须同时使用至少三种信号中的两种:强调背景、可见边框、图标或文字强调。浅色主题的当前项优先使用更完整的蓝色选中表面和较高字重。
|
||||
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
|
||||
- 桌面停靠状态下,主侧栏宽度允许在 `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
|
||||
能力自动增删稳定应用目录入口。
|
||||
- Tab 行只显示已打开实例,行末始终提供“+”普通按钮;点击后在内容区打开应用选择页。
|
||||
“+”不使用 `tab` 语义,也不能因为终端或其他应用已经打开而消失。
|
||||
- 应用注册表声明单实例或多实例。单实例重复选择时聚焦已有 Tab;终端从首期开始支持
|
||||
多实例,每次选择创建独立 Tab 和 Shell Session。
|
||||
- Task Center 是 Task 的单例应用级索引,不使用“跟随 / 固定目标”多实例模式。每个 Task
|
||||
只关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,
|
||||
也不得把 Job/Run 提升为可导航 UI 对象。
|
||||
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
|
||||
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。
|
||||
- 用户可以主动排序或隐藏栏目,并可恢复默认布局;应用不能用用户偏好机制实现自动能力裁剪。
|
||||
- 已打开实例保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
|
||||
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。
|
||||
- 当前工作栏 Tab 必须同时使用连续内容表面、可见边框、顶部指示、图标或较高字重中的至少
|
||||
两种信号;悬停反馈不得强于当前选中态。Tab 关闭按钮保留固定布局空间,默认仅在对应 Tab 悬停或
|
||||
`focus-within` 时显示;无悬停或粗指针设备始终显示,键盘与辅助技术始终可访问。
|
||||
- 选择“终端”后直接绑定当前项目,由项目执行空间自动决定本机或托管 SSH;不再显示第二层
|
||||
目标选择器。终端面板内部不重复提供关闭会话按钮,关闭统一由 Tab 入口完成。
|
||||
- 下一次请求使用的附件和知识库由会话输入区展示与管理,不在工作栏重复提供“上下文”栏目;
|
||||
记忆功能实现前不展示占位内容,未来的记忆与历史执行上下文归入关联 Task 的任务中心详情。
|
||||
- 徽标可以提示未解决意见、等待审批、失败或连接状态,但不能成为唯一状态信号,也不能无条件抢占当前栏目。
|
||||
- 浏览器 Tab 的内容顶部固定使用一条紧凑工具栏,按“返回、刷新/停止加载、地址输入、
|
||||
前往、交互、关闭浏览器”排列。“刷新”在当前页面加载时原位切换为“停止加载”;
|
||||
“关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为
|
||||
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
|
||||
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
|
||||
尚未提交的文字改变 Agent 看到的页面。
|
||||
- 浏览器工具栏操作的是当前 Conversation 中 Agent 使用的同一隔离浏览器会话,用户导航会
|
||||
有意改变 Agent 下一步看到的页面。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
|
||||
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
|
||||
不释放 Runtime 上下文。没有会话的活动 Conversation 可以直接在地址栏输入 URL 并“前往”
|
||||
创建会话,但必须复用 Agent 浏览器相同的 URL 策略与能力路径。
|
||||
- 所有窗口宽度下均停靠在主工作区右侧并参与水平布局回流;展开时压缩主工作区,不能覆盖、
|
||||
遮罩或隔离其内容。分栏计算排除最左侧主导航,主工作区与工作栏采用相同的 `300px`
|
||||
最小宽度;拖动保存两者在可用分栏宽度中的比例,任一侧到达最小值后都不得继续侵占。
|
||||
所有可容纳两侧最小值的尺寸均支持指针和键盘调整,并须保留全部栏目入口。
|
||||
- 工作栏开关固定在窗口关闭按钮正下方,同一个按钮负责展开与收起并持续暴露状态;图标中的
|
||||
箭头方向必须随当前动作切换,收起状态提示展开,展开状态提示收起。按钮保持普通图标按钮
|
||||
视觉,不通过持续高亮表达展开状态。工作栏内部不再重复关闭按钮;当前会话没有 Task 时,
|
||||
不得为了放置该开关而保留一条空上下文栏。
|
||||
- 停靠工作栏不得获得对话框语义、焦点陷阱或背景遮罩。关闭后焦点返回同一个工作栏开关;
|
||||
主工作区与工作栏在展开期间都保持可达。
|
||||
- 对话消息流、输入区和输入区元信息必须在主工作区内部保留对称的左右安全边距。工作栏
|
||||
分割线只能位于主工作区边界之外,不能覆盖、裁切或挤掉输入框边框、发送按钮或右侧边距。
|
||||
- 终端、宽日志和大型成果可以由用户切换到底部停靠或独立窗口,应用不得因内容变化自动改变用户已选布局。
|
||||
|
||||
### 6.10 应用顶栏与全局操作
|
||||
|
||||
应用顶栏用于当前会话标题、范围、窗口级状态、侧栏开关和低频全局操作,不承担主要导航。
|
||||
顶栏必须保持紧凑,不能与页面内容争夺注意力。
|
||||
|
||||
- 应用壳层在水平方向分为全高主侧栏和右侧内容框架。主侧栏从窗口顶部贯穿到底部,品牌区、
|
||||
项目切换器、导航和账户区保持同一连续表面;不能用全宽标题栏把整条侧栏向下推移。
|
||||
- 无边框窗口的紧凑标题栏只覆盖右侧内容框架,保留侧栏开关、当前会话标题、范围、Runtime
|
||||
状态、主题开关和窗口控制,不得为了新增工作栏入口把这一整组控件下移。普通图标按钮使用
|
||||
`34px × 34px` 点击区域。
|
||||
- 发布说明 Modal 与其他应用级 Modal 一样使用整窗遮罩;遮罩必须覆盖主侧栏、右侧内容和
|
||||
顶栏,不能保留一条未被遮罩的窗口控制色块。关闭操作由 Modal 自身提供。
|
||||
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
|
||||
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
|
||||
对齐。该栏不重复放置新建入口。
|
||||
- “新建任务”只位于工作栏“任务索引”标题后方;任务状态筛选占满工作栏可用宽度,四个选项
|
||||
等分,不能保留内容宽度的短控件。
|
||||
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。
|
||||
- 浅色与深色切换属于持续可用的窗口级操作,直接显示太阳或月亮图标,并通过可访问名称说明将切换到的主题。选择必须持久化,切换不得改变布局。
|
||||
- 顶栏只直接显示当前任务所需的高频操作。已有侧栏账户设置入口时,不再重复显示 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 +420,7 @@
|
||||
- 就地错误必须与对应字段或操作建立程序化关联;全局错误使用 `alert` 和 assertive 实时区域,成功与信息使用 `status` 和 polite 实时区域。
|
||||
- 一个事件只能选择一种主要反馈位置,不得同时显示页内横幅和全局通知。失败时不得因通知切换而清空用户输入、筛选或未提交草稿。
|
||||
|
||||
### 6.12 Switch 与 Checkbox
|
||||
### 6.13 Switch 与 Checkbox
|
||||
|
||||
Switch 用于在两个持久状态之间立即切换,例如启用能力、开启索引、允许群消息或显示平台入口。Checkbox 用于独立多选、范围分配或执行前确认,例如选择多个 Runtime、选择知识库、清除已保存密钥。两者不得只因底层都使用 `input[type="checkbox"]` 而混用视觉或语义。
|
||||
|
||||
@@ -470,13 +573,24 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
|
||||
- 使用 `reading` 壳层,消息流与输入区共享宽度。
|
||||
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
|
||||
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
|
||||
原文,不包含推理过程、工具日志或引用元数据,结果通过应用通知反馈。流式回复完成前不显示
|
||||
复制入口。
|
||||
- 本地会话的更多操作提供“在新会话中继续”。创建前必须先持久化当前稳定消息;有活动运行
|
||||
或待发送内容时不可创建。新会话复制聊天内容与当前 Runtime/知识检索选择,重置压缩状态,
|
||||
使用新的会话和消息 ID,且不复制 Task、队列、运行、记忆或成果归属。
|
||||
- 模式、模型或工具权限属于上下文控制,不与页面导航页签混用。
|
||||
- 模型、专家角色、工作模式、OpenCode Agent、Continue 预设和 Runtime 快捷操作使用统一的上下文单选菜单,并保持菜单互斥、键盘可达和选中状态明确。
|
||||
- Runtime 选择菜单必须反映当前执行空间的真实能力。托管 SSH 项目当前只支持 OpenCode,因此输入区只渲染已配置的 OpenCode 选项和管理入口,不显示直连模型、Continue 或 DeepSeek Harness 的不可用入口;旧会话保存的其他 Runtime 选择在项目激活时恢复为当前 OpenCode 配置。
|
||||
- 输入区第一行工具栏只承载附件、语音、知识范围、专家角色、工作模式、Runtime 选择和发送等通用操作。OpenCode Agent、Continue 预设及 Runtime 快捷操作必须放入其下方独立的 Runtime 专属功能行,通过可见分组名称、顶部边界和差异化表面与通用操作分层;该行只承载对当前消息生效的高频选择,当前 Runtime 没有可选专属功能时不保留空行。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 后续的 Subagent 层级与取消、后台 Job 队列/进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止及原生会话监督统一进入右侧助手工作栏的“Runtime”页签,不加入 Composer。侧栏按当前会话和 Runtime 能力动态显示区块,不为未支持能力渲染空卡片或成排禁用按钮;切换会话或 Runtime 时必须同步清理上一归属的监督状态。
|
||||
- 设置中心只管理持久 Runtime 配置、默认值和能力清单;右侧 Runtime 页签只管理当前活动会话的生命周期。两处不得复制同一实时操作,侧栏中的高风险操作仍须就地确认并保留取消、权限、用量和活动审计。
|
||||
- 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 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。
|
||||
- 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。
|
||||
- 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。
|
||||
- 支持上下文状态的 Runtime 在输入区下方复用同一紧凑用量条;文案必须区分“本次模型调用”和“压缩后对话估算”。手动压缩仅在当前 Runtime 明确支持且没有活动回复时显示,作为元信息区左下角的浮动次操作,不参与输入区高度计算;元信息区始终预留稳定高度,切换 Runtime 不得让输入框上下位移。元信息区与窗口底部只保留紧凑安全留白,不形成额外空白区。进行中禁用重复操作,结果通过应用通知反馈。
|
||||
- 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。
|
||||
- 宽度大于 `700px` 时,通用工具栏内的添加内容、知识范围、专家、模式和 Runtime 选择保持同一行,Runtime 专属功能在自己的下一行横向排列。窄输入区中两行分别换行,专属选择器以至少 `220px` 的基准宽度换行而不是被挤压;不能把专属控件重新塞回通用工具栏。
|
||||
@@ -491,6 +605,11 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 搜索、范围和时间筛选位于筛选工具栏。
|
||||
- 行项目统一显示标题、范围、最近更新时间和必要状态。
|
||||
- 侧栏最近会话的更新时间在当天显示本地时间,非当天显示本地日期;非当年记录必须同时显示年份。悬停时间信息时提供完整日期和时间。
|
||||
- 侧栏中有关联 Task 的 Conversation 显示行首展开按钮,父行不显示任务标签或数量;展开后
|
||||
最多直接显示 3 个 Task,“查看全部 N 个任务”打开该 Conversation 的完整任务区。
|
||||
- Task 子项左侧显示名称,右侧显示本地化的 Ask/Execute、计划摘要和共享状态标记,与父会话
|
||||
的更新时间位置一致;已完成使用带勾成功点,不重复显示完成文字。点击子项打开同一
|
||||
Conversation 并定位 Task,不继续展开 Job/Run。
|
||||
- 删除入口使用 `danger-ghost`,并按数据可恢复性执行确认或撤销策略。
|
||||
|
||||
### 13.3 知识库
|
||||
@@ -504,11 +623,30 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
### 13.4 智能心跳
|
||||
|
||||
- 使用 `dashboard` 壳层。
|
||||
- 顶部先呈现运行状态、当前范围和主操作,再呈现指标和配置。
|
||||
- 顶部先呈现运行状态、实际范围和主操作,再呈现指标、建议、历史与配置。
|
||||
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
|
||||
Checkbox,不能依靠进入页面时的当前项目推断。
|
||||
- 状态卡片使用统一状态令牌,不只依赖颜色。
|
||||
- 运行历史与配置使用明确区块,不以多套相似页签混合导航、开关和筛选。
|
||||
- 保留“运行概览 / 待处理建议 / 报告与记录 / 心跳计划”四个同级页面。
|
||||
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
|
||||
- “未来分区记忆”仅为长期方向,数据、状态和页面尚未设计,不得显示占位入口。
|
||||
|
||||
### 13.5 运行记录
|
||||
### 13.5 Task Center
|
||||
|
||||
- 保留现有助手工作栏入口,首期在窄栏内适度完善,不先扩张成新的独立一级页面。
|
||||
- 只展示 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 默认 Execute,并允许主动切换 Ask。Execute 持续显示实际 Runtime、Project、
|
||||
工作目录、工具和审批摘要;不支持工具时不得静默降级。
|
||||
|
||||
### 13.6 运行记录
|
||||
|
||||
- 使用 `dashboard` 壳层,并通过 `PageTabs` 提供“任务与会话 / 活动时间线 / 用量统计”三个同级视图。
|
||||
- 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠。
|
||||
@@ -517,14 +655,23 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度,宽表格在独立容器内横向滚动。
|
||||
- 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
|
||||
|
||||
### 13.6 魔法笔记
|
||||
### 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 设置中心
|
||||
|
||||
- 全页设置使用固定标题区、左侧分类导航和独立滚动的内容区。右上角关闭按钮是离开设置中心的稳定入口。
|
||||
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
|
||||
@@ -533,16 +680,23 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 所有分类使用共享的 `SettingsCategoryHeader` 呈现分类标题、说明、错误与操作,不得在内容卡片内复制分类标题或创建页面专属操作栏。左侧分类名称与说明来自同一份分类定义,新增分类时不得分别维护导航和内容标题。
|
||||
- 当前分类存在“保存”或“测试”等未提交配置操作时,统一放在分类页头右侧;主保存操作在最右侧,测试等次操作排列在其左侧。
|
||||
- 自动生效、仅执行即时命令或自行管理编辑流程的分类不显示全局保存操作。窄窗口下操作区可以换行,但保存入口必须保持清晰可见。
|
||||
- 智能心跳的单条配置不在设置中心重复管理。设置中心如需呈现平台级说明,只提供
|
||||
“打开智能心跳”导航,不复制创建、暂停、恢复或删除表单。
|
||||
- 保存或测试成功统一进入应用通知视口,并按全局规则自动消失,不在分类页头或内容卡片中保留持久成功文案。加载、保存和测试错误显示在分类页头下方,并保留可处理的上下文。
|
||||
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后,下拉框置灰且不可操作。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查和下载页,不显示底层服务商名称。
|
||||
- “模型连接”的认证字段按“认证方式 → API Key → 凭据状态”排列;只有选择 API Key 时显示输入框。已保存的 API Key 属于该连接,修改模型服务地址或临时选择“无需认证”不得自动清除,也不得要求重新输入;仅由明确的“清除凭据”或“删除连接”操作移除。
|
||||
- “保存并测试模型”必须在保存后发送一次有界的真实模型请求。文本连接校验本次随机测试文本,图像连接校验实际返回的内联图片;仅收到成功 HTTP 状态、模型列表或健康检查响应不算测试成功。界面持续说明该操作可能产生少量服务商用量费用,成功反馈明确写为“真实生成测试通过”。
|
||||
- 所有显式保存的设置草稿都参与离开保护:关闭、切换分类、主侧栏或工作区导航及托盘导航不得静默丢弃,统一通过设置中心的就地确认提供继续编辑与明确放弃入口,保存失败后保留输入。“平台功能 / 通用设置”承载全局快捷唤起的共享 Switch、可访问 accelerator 录制输入、恢复默认、保存及注册、停用或冲突状态,不新增分类或页签;注册或持久化失败时保留上一组可用快捷键和当前草稿,保存或停用成功后同步更新输入区的快捷键提示。
|
||||
- “平台功能”使用共享 `PageTabs` 区分“通用设置”“远程项目(技术预览)”和“魔法笔记”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64/arm64 Agent 包清单分成独立卡片;清单延迟到首次打开该页签时由 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 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
|
||||
- MCP 设置按“内置 MCP / 直连模型 / 自定义 MCP / 电脑控制”四个同级 `PageTabs` 组织。直连模型中的联网搜索与内置浏览器使用一致的折叠卡片和独立总开关;内置浏览器必须明确说明其操作 GoodBuddy 隔离浏览器,不控制客户端已安装的浏览器,开启后可由 Execute 直接使用,不逐次询问。尚未生效的命名浏览器配置不得显示在界面中,“电脑控制”只显示实际操作客户端电脑的能力。内置 MCP 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
|
||||
- MCP Server 测试结果在同一展开卡片中分组显示 Tools、Prompts 和 Resources 的支持状态、数量与有界元数据;Prompt 参数标明必填项,Resource 只显示 URI、名称、类型和说明,不读取或渲染 Resource 内容。
|
||||
|
||||
### 13.8 文档解析设置
|
||||
### 13.9 文档解析设置
|
||||
|
||||
- 设置中心新增独立的“文档解析”分类,统一管理聊天附件、知识库导入以及后续文档审阅场景使用的提取、转换和 OCR 策略。OCR 不作为普通对话模型出现在“模型连接”中。
|
||||
- 分类页头说明文档解析的跨场景作用,右侧依次显示“测试解析”和“保存设置”;保存位于最右侧。测试必须选择真实文件并执行实际解析,不能只检查模型文件或接口连通性。
|
||||
@@ -609,8 +763,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- [ ] 将输入快捷键与附件提示置于空输入框内部,输入区下方保持单行说明。
|
||||
- [ ] 最近对话迁移到 `standard`,统一搜索、范围、时间和删除行为。
|
||||
- [ ] 知识库迁移到 `master-detail`,清除内联浅色样式并补齐窄窗口单面板流程。
|
||||
- [ ] 智能心跳迁移到 `dashboard`,统一状态卡片、配置和运行历史层级。
|
||||
- [ ] 任务迁移到 `standard`,活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
|
||||
- [x] 智能心跳使用 `dashboard`,保留概览、建议、轨迹和计划,并在计划中支持 Global / 多 Project 范围。
|
||||
- [x] 在现有工作栏中完善任务中心,统一范围、状态、最近进展和筛选,不新建平行任务平台。
|
||||
- [ ] 活动记录迁移到 `dashboard`,统一导航、筛选和表格行为。
|
||||
- [ ] 设置中心使用共享分类定义与 `SettingsCategoryHeader`,将保存与测试操作统一放到分类页头右侧,并把成功反馈接入应用通知。
|
||||
- [ ] 文档解析设置统一聊天附件与知识库的解析预设、OCR 状态、转换状态、隐私限制和真实文件测试。
|
||||
|
||||
@@ -636,3 +791,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"`,并保留明确的恢复操作。
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"agentVersion": "0.11.14",
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,914 @@
|
||||
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, '..')
|
||||
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 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 ?? ''
|
||||
) ||
|
||||
descriptor.platform !== 'linux' ||
|
||||
!['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}` +
|
||||
`-linux-${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
|
||||
if (wanted && size > maximumMetadataBytes) {
|
||||
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 ?? ''
|
||||
) ||
|
||||
entry.platform !== 'linux' ||
|
||||
!['x64', 'arm64'].includes(entry.architecture) ||
|
||||
entry.archive !==
|
||||
`goodbuddy-agent-${entry.version}` +
|
||||
`-linux-${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.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)
|
||||
]
|
||||
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
|
||||
)).size !== 2
|
||||
) {
|
||||
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.architecture}`, entry)
|
||||
}
|
||||
for (const metadata of packages) {
|
||||
const entry = catalogEntry(metadata)
|
||||
const identity = `${entry.version}:${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.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',
|
||||
'--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['--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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
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 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',
|
||||
'--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')
|
||||
}
|
||||
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,
|
||||
nodeArchive: resolve(options.nodearchive),
|
||||
opencodeArchive: resolve(options.opencodearchive),
|
||||
outputDirectory: resolve(options.outputdirectory),
|
||||
archive: resolve(options.archive)
|
||||
}
|
||||
}
|
||||
|
||||
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 lock = readRuntimeLock(projectRoot)
|
||||
const runtimeLock = readRemoteRuntimeLock(projectRoot)
|
||||
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({
|
||||
projectRoot,
|
||||
arch: options.arch,
|
||||
runtimeArchive: options.nodeArchive,
|
||||
outputDirectory: options.outputDirectory,
|
||||
registry: signing.registry,
|
||||
testSigningIdentity: identity
|
||||
})
|
||||
const runtime = buildRuntimeBundle({
|
||||
projectRoot,
|
||||
architecture: options.arch,
|
||||
runtimeArchive: options.opencodeArchive,
|
||||
outputRoot: runtimeRoot,
|
||||
registry: signing.registry,
|
||||
testSigningIdentity: identity
|
||||
})
|
||||
const assemble = (archive) =>
|
||||
assembleAgentPackage({
|
||||
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')
|
||||
}
|
||||
|
||||
return {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
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 { 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
|
||||
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,
|
||||
{
|
||||
projectRoot,
|
||||
registry,
|
||||
lock: agentLock,
|
||||
verificationEnvironment: options.testSigningIdentity
|
||||
? 'test'
|
||||
: 'production'
|
||||
}
|
||||
)
|
||||
const runtime = verifyRuntimeBundle(options.runtimeBundle, {
|
||||
projectRoot,
|
||||
architecture,
|
||||
registry,
|
||||
lock: runtimeLock,
|
||||
verificationEnvironment: options.testSigningIdentity
|
||||
? 'test'
|
||||
: 'production'
|
||||
})
|
||||
const expectedArchiveName =
|
||||
`goodbuddy-agent-${agent.manifest.agentVersion}` +
|
||||
`-linux-${architecture}.gbagent`
|
||||
if (basename(options.output) !== expectedArchiveName) {
|
||||
throw new Error(
|
||||
`Agent package output must be named ${expectedArchiveName}`
|
||||
)
|
||||
}
|
||||
const staging = mkdtempSync(
|
||||
join(tmpdir(), `goodbuddy-agent-package-linux-${architecture}-`)
|
||||
)
|
||||
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: 'linux',
|
||||
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({
|
||||
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({
|
||||
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',
|
||||
'--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 = {
|
||||
architecture: options.arch,
|
||||
minimumDesktopVersion: options.minimumDesktopVersion,
|
||||
output: resolve(options.output)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ const targetDefinitions = [
|
||||
{ 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: '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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+104
-86
@@ -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(
|
||||
@@ -219,7 +209,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 +272,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 +350,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()
|
||||
}
|
||||
|
||||
+156
-157
@@ -5,6 +5,7 @@ const {
|
||||
createWriteStream,
|
||||
existsSync,
|
||||
closeSync,
|
||||
fstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
openSync,
|
||||
@@ -35,6 +36,12 @@ const {
|
||||
} = require('@electron/asar')
|
||||
const { Zip, ZipDeflate } = require('fflate')
|
||||
const { sha256File } = require('./file-hash.cjs')
|
||||
const {
|
||||
detectBinaryArchitecture
|
||||
} = require('./binary-architecture.cjs')
|
||||
const {
|
||||
readZipCentralDirectory
|
||||
} = require('./zip-central-directory.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageJson = JSON.parse(
|
||||
@@ -45,17 +52,28 @@ const packageLock = JSON.parse(
|
||||
)
|
||||
const productName = packageJson.build?.productName ?? packageJson.name
|
||||
const releaseRoot = join(root, 'dist', 'release')
|
||||
const releaseBuilderConfig = join(
|
||||
root,
|
||||
'build',
|
||||
'electron-builder.release.cjs'
|
||||
)
|
||||
const manifestName = 'release-manifest.json'
|
||||
const portableMarkerName = '.goodbuddy-portable.json'
|
||||
const harnessHostEntry =
|
||||
'out/main/deepseek-harness-host-bootstrap.js'
|
||||
const harnessBundleManifest = 'out/main/package.json'
|
||||
const harnessPackageVersions = {
|
||||
'@deepseek-ai/dsh-agent': '0.1.0-rc.6',
|
||||
'@napi-rs/canvas': '1.0.3',
|
||||
'node-pty': '1.1.0'
|
||||
'@deepseek-ai/dsh-agent':
|
||||
packageLock.packages['node_modules/@deepseek-ai/dsh-agent']
|
||||
?.version,
|
||||
'@napi-rs/canvas':
|
||||
packageLock.packages['node_modules/@napi-rs/canvas']
|
||||
?.version,
|
||||
'node-pty':
|
||||
packageLock.packages['node_modules/node-pty']?.version
|
||||
}
|
||||
const koffiVersion = '3.1.4'
|
||||
const koffiVersion =
|
||||
packageLock.packages['node_modules/koffi']?.version
|
||||
const harnessLicenseFiles = [
|
||||
'agent-client-protocol-Apache-2.0.txt',
|
||||
'deepseek-cordis-MIT.txt',
|
||||
@@ -70,9 +88,11 @@ const portableRequiredFiles = [
|
||||
'resources/release-notes.json',
|
||||
'resources/icon.ico',
|
||||
'resources/tray-icon.png',
|
||||
'resources/tool-environment/managed-python-artifacts.json',
|
||||
'resources/runtimes/opencode/opencode.exe',
|
||||
'resources/runtimes/continue/package.json',
|
||||
'resources/runtimes/npm/bin/npm-cli.js',
|
||||
'resources/runtimes/npm/bin/npx-cli.js',
|
||||
'resources/runtimes/npm/package.json',
|
||||
'resources/runtimes/npm/node_modules/graceful-fs/package.json'
|
||||
]
|
||||
@@ -119,8 +139,8 @@ const platformDefinitions = {
|
||||
},
|
||||
linux: {
|
||||
builderFlag: '--linux',
|
||||
defaultFormats: ['AppImage', 'deb'],
|
||||
supportedFormats: ['AppImage', 'deb'],
|
||||
defaultFormats: ['AppImage', 'deb', 'rpm'],
|
||||
supportedFormats: ['AppImage', 'deb', 'rpm'],
|
||||
unpackedPattern: /^linux(?:-.+)?-unpacked$/u,
|
||||
executable: [packageJson.name],
|
||||
runtimeExecutable: 'opencode'
|
||||
@@ -132,7 +152,8 @@ const formatExtensions = {
|
||||
dmg: '.dmg',
|
||||
zip: '.zip',
|
||||
AppImage: '.AppImage',
|
||||
deb: '.deb'
|
||||
deb: '.deb',
|
||||
rpm: '.rpm'
|
||||
}
|
||||
|
||||
function normalizePlatform(value) {
|
||||
@@ -153,6 +174,7 @@ function parseArguments(argv, environment = process) {
|
||||
formats: [],
|
||||
skipBuild: false,
|
||||
dryRun: false,
|
||||
unsigned: false,
|
||||
help: false
|
||||
}
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -176,6 +198,8 @@ function parseArguments(argv, environment = process) {
|
||||
options.skipBuild = true
|
||||
} else if (argument === '--dry-run') {
|
||||
options.dryRun = true
|
||||
} else if (argument === '--unsigned') {
|
||||
options.unsigned = true
|
||||
} else if (argument === '--help' || argument === '-h') {
|
||||
options.help = true
|
||||
} else {
|
||||
@@ -191,6 +215,9 @@ function parseArguments(argv, environment = process) {
|
||||
if (!supportedArchitectures.has(options.arch)) {
|
||||
throw new Error(`不支持的架构:${options.arch}`)
|
||||
}
|
||||
if (options.unsigned && options.platform !== 'macos') {
|
||||
throw new Error('--unsigned 仅支持 macOS 发布包')
|
||||
}
|
||||
const definition = platformDefinitions[options.platform]
|
||||
const requestedFormats =
|
||||
options.formats.length > 0
|
||||
@@ -321,7 +348,9 @@ function buildElectronBuilderArguments(options, outputDirectory) {
|
||||
)
|
||||
)]
|
||||
const builderArguments = [
|
||||
join(root, 'node_modules', 'electron-builder', 'cli.js'),
|
||||
join(root, 'build', 'run-electron-builder.cjs'),
|
||||
'--config',
|
||||
releaseBuilderConfig,
|
||||
definition.builderFlag,
|
||||
...builderFormats,
|
||||
`--${options.arch}`,
|
||||
@@ -337,62 +366,43 @@ function buildElectronBuilderArguments(options, outputDirectory) {
|
||||
`--config.nsis.artifactName=${productName}-\${version}-windows-\${arch}-setup.\${ext}`
|
||||
)
|
||||
}
|
||||
if (options.platform === 'macos' && options.unsigned) {
|
||||
builderArguments.push('--config.mac.notarize=false')
|
||||
}
|
||||
return builderArguments
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
function electronBuilderEnvironment(
|
||||
options,
|
||||
environment = process.env
|
||||
) {
|
||||
const builderEnvironment = {
|
||||
...environment,
|
||||
CSC_IDENTITY_AUTO_DISCOVERY:
|
||||
environment.CSC_IDENTITY_AUTO_DISCOVERY ?? 'false'
|
||||
}
|
||||
if (
|
||||
buffer.length >= 20 &&
|
||||
buffer[0] === 0x7f &&
|
||||
buffer.toString('ascii', 1, 4) === 'ELF'
|
||||
) {
|
||||
const machine =
|
||||
buffer[5] === 2
|
||||
? buffer.readUInt16BE(18)
|
||||
: buffer.readUInt16LE(18)
|
||||
if (machine === 62) {
|
||||
return 'x64'
|
||||
}
|
||||
if (machine === 183) {
|
||||
return 'arm64'
|
||||
}
|
||||
if (options.platform !== 'macos' || !options.unsigned) {
|
||||
return builderEnvironment
|
||||
}
|
||||
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'
|
||||
}
|
||||
for (const name of [
|
||||
'CSC_LINK',
|
||||
'CSC_KEY_PASSWORD',
|
||||
'CSC_NAME',
|
||||
'CSC_INSTALLER_LINK',
|
||||
'CSC_INSTALLER_KEY_PASSWORD',
|
||||
'APPLE_API_KEY',
|
||||
'APPLE_API_KEY_ID',
|
||||
'APPLE_API_ISSUER',
|
||||
'APPLE_ID',
|
||||
'APPLE_APP_SPECIFIC_PASSWORD',
|
||||
'APPLE_TEAM_ID',
|
||||
'APPLE_KEYCHAIN',
|
||||
'APPLE_KEYCHAIN_PROFILE'
|
||||
]) {
|
||||
delete builderEnvironment[name]
|
||||
}
|
||||
return undefined
|
||||
builderEnvironment.CSC_IDENTITY_AUTO_DISCOVERY = 'false'
|
||||
return builderEnvironment
|
||||
}
|
||||
|
||||
function binaryArchitecture(filePath) {
|
||||
@@ -518,9 +528,10 @@ function targetHarnessPaths(options) {
|
||||
koffiPackage,
|
||||
koffiBinary,
|
||||
nodePtyBinary:
|
||||
options.platform === 'linux'
|
||||
? 'build/Release/pty.node'
|
||||
: `prebuilds/${platformName}-${options.arch}/pty.node`,
|
||||
`prebuilds/${platformName}-${options.arch}/` +
|
||||
(options.platform === 'windows'
|
||||
? 'conpty.node'
|
||||
: 'pty.node'),
|
||||
nodePtyDirectory: `${platformName}-${options.arch}`
|
||||
}
|
||||
}
|
||||
@@ -885,6 +896,10 @@ function verifyHarnessPackage(
|
||||
join(npmRoot, 'bin', 'npm-cli.js'),
|
||||
'DSH 插件安装 npm CLI'
|
||||
)
|
||||
assertFile(
|
||||
join(npmRoot, 'bin', 'npx-cli.js'),
|
||||
'DSH 插件安装 npx CLI'
|
||||
)
|
||||
if (
|
||||
!Array.isArray(npmManifest.bundleDependencies) ||
|
||||
npmManifest.bundleDependencies.length === 0
|
||||
@@ -996,7 +1011,7 @@ function verifyHarnessPackage(
|
||||
'DeepSeek Harness Canvas'
|
||||
)
|
||||
|
||||
if (options.platform === 'darwin') {
|
||||
if (options.platform === 'macos') {
|
||||
const helper = join(
|
||||
unpackedRoot,
|
||||
'node_modules',
|
||||
@@ -1044,6 +1059,26 @@ function verifyUnpackedOutput(directory, options) {
|
||||
assertFile(applicationExecutable, '应用主程序')
|
||||
assertFile(join(resources, 'app.asar'), '应用 ASAR')
|
||||
assertFile(join(resources, 'release-notes.json'), '版本更新说明')
|
||||
assertFile(
|
||||
join(
|
||||
resources,
|
||||
'tool-environment',
|
||||
'managed-python-artifacts.json'
|
||||
),
|
||||
'托管 Python 产物目录'
|
||||
)
|
||||
assertFile(
|
||||
join(resources, 'agent-release-keys.json'),
|
||||
'Agent 受信任签名密钥'
|
||||
)
|
||||
assertFile(
|
||||
join(resources, 'agent-runtime-lock.json'),
|
||||
'Agent Runtime 锁定清单'
|
||||
)
|
||||
assertFile(
|
||||
join(resources, 'remote-runtime-lock.json'),
|
||||
'Remote Runtime 锁定清单'
|
||||
)
|
||||
assertFile(runtimeExecutable, 'OpenCode Runtime')
|
||||
assertFile(
|
||||
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
|
||||
@@ -1059,6 +1094,16 @@ function verifyUnpackedOutput(directory, options) {
|
||||
),
|
||||
'DSH 插件安装 npm Runtime'
|
||||
)
|
||||
assertFile(
|
||||
join(
|
||||
resources,
|
||||
'runtimes',
|
||||
'npm',
|
||||
'bin',
|
||||
'npx-cli.js'
|
||||
),
|
||||
'DSH 插件安装 npx Runtime'
|
||||
)
|
||||
verifyHarnessPackage(resources, options)
|
||||
for (const [filePath, label] of [
|
||||
[applicationExecutable, '应用主程序'],
|
||||
@@ -1258,98 +1303,42 @@ async function createPortableZip(
|
||||
}
|
||||
|
||||
function readZipEntryNames(filePath) {
|
||||
const fileSize = statSync(filePath).size
|
||||
if (fileSize < 22) {
|
||||
throw new Error('Portable ZIP 缺少中央目录')
|
||||
}
|
||||
const endChunkSize = Math.min(fileSize, 65_557)
|
||||
const endChunkStart = fileSize - endChunkSize
|
||||
const endChunk = readChunk(
|
||||
filePath,
|
||||
endChunkSize,
|
||||
endChunkStart
|
||||
)
|
||||
let endOffset = -1
|
||||
for (let index = endChunk.length - 22; index >= 0; index -= 1) {
|
||||
if (
|
||||
endChunk.readUInt32LE(index) === 0x06054b50 &&
|
||||
index + 22 + endChunk.readUInt16LE(index + 20) ===
|
||||
endChunk.length
|
||||
) {
|
||||
endOffset = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if (endOffset < 0) {
|
||||
throw new Error('Portable ZIP 缺少中央目录')
|
||||
}
|
||||
const diskNumber = endChunk.readUInt16LE(endOffset + 4)
|
||||
const centralDisk = endChunk.readUInt16LE(endOffset + 6)
|
||||
const diskEntryCount = endChunk.readUInt16LE(endOffset + 8)
|
||||
const entryCount = endChunk.readUInt16LE(endOffset + 10)
|
||||
const centralSize = endChunk.readUInt32LE(endOffset + 12)
|
||||
const centralOffset = endChunk.readUInt32LE(endOffset + 16)
|
||||
const absoluteEndOffset = endChunkStart + endOffset
|
||||
if (
|
||||
diskNumber !== 0 ||
|
||||
centralDisk !== 0 ||
|
||||
diskEntryCount !== entryCount ||
|
||||
entryCount === 0xffff ||
|
||||
centralSize === 0xffffffff ||
|
||||
centralOffset === 0xffffffff ||
|
||||
entryCount < portableRequiredFiles.length + 1 ||
|
||||
entryCount > maxPortableZipEntries ||
|
||||
centralSize < 46 ||
|
||||
centralSize > maxPortableCentralDirectoryBytes ||
|
||||
centralOffset + centralSize !== absoluteEndOffset
|
||||
) {
|
||||
throw new Error('Portable ZIP 中央目录无效')
|
||||
}
|
||||
const centralDirectory = readChunk(
|
||||
filePath,
|
||||
centralSize,
|
||||
centralOffset
|
||||
)
|
||||
const names = []
|
||||
let offset = 0
|
||||
for (let index = 0; index < entryCount; index += 1) {
|
||||
if (
|
||||
offset + 46 > centralDirectory.length ||
|
||||
centralDirectory.readUInt32LE(offset) !== 0x02014b50
|
||||
) {
|
||||
throw new Error('Portable ZIP 中央目录条目无效')
|
||||
}
|
||||
const nameLength = centralDirectory.readUInt16LE(offset + 28)
|
||||
const extraLength = centralDirectory.readUInt16LE(offset + 30)
|
||||
const commentLength = centralDirectory.readUInt16LE(offset + 32)
|
||||
const entryLength = 46 + nameLength + extraLength + commentLength
|
||||
if (offset + entryLength > centralDirectory.length) {
|
||||
throw new Error('Portable ZIP 中央目录条目越界')
|
||||
}
|
||||
const name = centralDirectory
|
||||
.subarray(offset + 46, offset + 46 + nameLength)
|
||||
.toString(
|
||||
centralDirectory.readUInt16LE(offset + 8) & 0x0800
|
||||
? 'utf8'
|
||||
: 'latin1'
|
||||
const handle = openSync(filePath, 'r')
|
||||
try {
|
||||
let parsed
|
||||
try {
|
||||
parsed = readZipCentralDirectory(
|
||||
handle,
|
||||
fstatSync(handle).size,
|
||||
{
|
||||
label: 'Portable ZIP',
|
||||
minimumEntries: portableRequiredFiles.length + 1,
|
||||
maximumEntries: maxPortableZipEntries,
|
||||
maximumCentralDirectoryBytes:
|
||||
maxPortableCentralDirectoryBytes
|
||||
}
|
||||
)
|
||||
.replaceAll('\\', '/')
|
||||
if (
|
||||
!name ||
|
||||
name.startsWith('/') ||
|
||||
/^[a-z]:\//iu.test(name) ||
|
||||
name.includes('\0') ||
|
||||
name.split('/').some((part) => part === '..')
|
||||
) {
|
||||
throw new Error(`Portable ZIP 包含不安全路径:${name}`)
|
||||
} catch (error) {
|
||||
throw new Error('Portable ZIP 中央目录无效', {
|
||||
cause: error
|
||||
})
|
||||
}
|
||||
names.push(name)
|
||||
offset += entryLength
|
||||
return parsed.entries.map((entry) => {
|
||||
const name = entry.name.replaceAll('\\', '/')
|
||||
if (
|
||||
!name ||
|
||||
name.startsWith('/') ||
|
||||
/^[a-z]:\//iu.test(name) ||
|
||||
name.includes('\0') ||
|
||||
name.split('/').some((part) => part === '..')
|
||||
) {
|
||||
throw new Error(`Portable ZIP 包含不安全路径:${name}`)
|
||||
}
|
||||
return name
|
||||
})
|
||||
} finally {
|
||||
closeSync(handle)
|
||||
}
|
||||
if (offset !== centralDirectory.length) {
|
||||
throw new Error('Portable ZIP 中央目录数量不一致')
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
function verifyPortableZip(filePath) {
|
||||
@@ -1440,6 +1429,12 @@ function verifyArtifactSignature(filePath, format, arch) {
|
||||
) {
|
||||
throw new Error('deb 产物不是有效的 ar 归档')
|
||||
}
|
||||
if (
|
||||
format === 'rpm' &&
|
||||
readChunk(filePath, 4).toString('hex') !== 'edabeedb'
|
||||
) {
|
||||
throw new Error('rpm 产物缺少 RPM lead magic')
|
||||
}
|
||||
}
|
||||
|
||||
async function writeManifest(directory, options) {
|
||||
@@ -1568,11 +1563,12 @@ function printHelp() {
|
||||
--format <列表> 覆盖默认格式,逗号分隔
|
||||
--skip-build 复用已有 out 生产构建
|
||||
--dry-run 仅显示目标与 electron-builder 参数
|
||||
--unsigned 仅用于 macOS,明确禁用代码签名与公证
|
||||
|
||||
默认格式:
|
||||
windows: nsis, portable (ZIP)
|
||||
macos: dmg, zip
|
||||
linux: AppImage, deb`)
|
||||
linux: AppImage, deb, rpm`)
|
||||
}
|
||||
|
||||
async function main(argv = process.argv.slice(2)) {
|
||||
@@ -1606,6 +1602,11 @@ async function main(argv = process.argv.slice(2)) {
|
||||
`${options.platform} 包必须在对应系统构建,当前系统为 ${hostPlatform ?? process.platform}`
|
||||
)
|
||||
}
|
||||
if (options.unsigned) {
|
||||
console.warn(
|
||||
'警告:正在生成未签名、未公证的 macOS 包,Gatekeeper 可能阻止用户首次打开。'
|
||||
)
|
||||
}
|
||||
|
||||
rmSync(stagingDirectory, { recursive: true, force: true })
|
||||
let cleanupTargetDependencies = () => undefined
|
||||
@@ -1622,11 +1623,7 @@ async function main(argv = process.argv.slice(2)) {
|
||||
await run(
|
||||
process.execPath,
|
||||
builderArguments,
|
||||
{
|
||||
...process.env,
|
||||
CSC_IDENTITY_AUTO_DISCOVERY:
|
||||
process.env.CSC_IDENTITY_AUTO_DISCOVERY ?? 'false'
|
||||
}
|
||||
electronBuilderEnvironment(options)
|
||||
)
|
||||
const unpackedDirectory = verifyUnpackedOutput(
|
||||
stagingDirectory,
|
||||
@@ -1665,6 +1662,7 @@ module.exports = {
|
||||
buildElectronBuilderArguments,
|
||||
createPortableZip,
|
||||
detectBinaryArchitecture,
|
||||
electronBuilderEnvironment,
|
||||
normalizePlatform,
|
||||
parseArguments,
|
||||
parsePackedPackageMetadata,
|
||||
@@ -1673,6 +1671,7 @@ module.exports = {
|
||||
replaceOutput,
|
||||
stageTargetRuntimeDependencies,
|
||||
targetRuntimePackageNames,
|
||||
targetHarnessPaths,
|
||||
verifyHarnessPackage,
|
||||
verifyArchiveIntegrity,
|
||||
verifyUnpackedOutput,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,30 +4,34 @@ const {
|
||||
writeFileSync
|
||||
} = require('node:fs')
|
||||
const { dirname, resolve } = require('node:path')
|
||||
const {
|
||||
expectedFormatForFile,
|
||||
targetDefinitions
|
||||
} = require('./aggregate-release.cjs')
|
||||
|
||||
const supportedTargets = new Map([
|
||||
['windows-x64', ['nsis', 'portable']],
|
||||
['windows-arm64', ['nsis', 'portable']],
|
||||
['macos-x64', ['dmg', 'zip']],
|
||||
['macos-arm64', ['dmg', 'zip']],
|
||||
['linux-x64', ['AppImage', 'deb']],
|
||||
['linux-arm64', ['AppImage', 'deb']]
|
||||
])
|
||||
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]
|
||||
if (
|
||||
argument === '--manifest' ||
|
||||
argument === '--base-url' ||
|
||||
argument === '--output'
|
||||
) {
|
||||
const optionName = optionNames.get(argument)
|
||||
if (optionName) {
|
||||
const value = argv[index + 1]
|
||||
if (!value) {
|
||||
throw new Error(`${argument} 缺少值`)
|
||||
}
|
||||
options[argument.slice(2)] = value
|
||||
options[optionName] = value
|
||||
index += 1
|
||||
} else {
|
||||
throw new Error(`未知参数:${argument}`)
|
||||
@@ -79,32 +83,6 @@ function assertFile(file, targetKey) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatForFile(fileName, platform) {
|
||||
if (platform === 'windows') {
|
||||
if (/-setup\.exe$/u.test(fileName)) {
|
||||
return 'nsis'
|
||||
}
|
||||
if (/-portable\.zip$/u.test(fileName)) {
|
||||
return 'portable'
|
||||
}
|
||||
} else if (platform === 'macos') {
|
||||
if (fileName.endsWith('.dmg')) {
|
||||
return 'dmg'
|
||||
}
|
||||
if (fileName.endsWith('.zip')) {
|
||||
return 'zip'
|
||||
}
|
||||
} else if (platform === 'linux') {
|
||||
if (fileName.endsWith('.AppImage')) {
|
||||
return 'AppImage'
|
||||
}
|
||||
if (fileName.endsWith('.deb')) {
|
||||
return 'deb'
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function createSiteRelease(manifest, baseUrlValue) {
|
||||
if (
|
||||
!manifest ||
|
||||
@@ -121,7 +99,8 @@ function createSiteRelease(manifest, baseUrlValue) {
|
||||
const targets = {}
|
||||
for (const target of manifest.targets) {
|
||||
const key = `${target?.platform}-${target?.arch}`
|
||||
const expectedFormats = supportedTargets.get(key)
|
||||
const expectedTarget = supportedTargets.get(key)
|
||||
const expectedFormats = expectedTarget?.formats
|
||||
if (
|
||||
!expectedFormats ||
|
||||
seenTargets.has(key) ||
|
||||
@@ -138,7 +117,10 @@ function createSiteRelease(manifest, baseUrlValue) {
|
||||
const files = {}
|
||||
for (const file of target.files) {
|
||||
assertFile(file, key)
|
||||
const format = formatForFile(file.name, target.platform)
|
||||
const format = expectedFormatForFile(
|
||||
file.name,
|
||||
expectedTarget
|
||||
)
|
||||
if (!format || !expectedFormats.includes(format) || files[format]) {
|
||||
throw new Error(`发布文件格式无效:${file.name}`)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ async function run() {
|
||||
provider: 'goodbuddy',
|
||||
model: 'qwen-plus',
|
||||
supportsImageInput: false,
|
||||
harnessVersion: '0.1.0-rc.6',
|
||||
harnessVersion: '0.1.0-rc.8',
|
||||
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
|
||||
skillPackages: [],
|
||||
extensionPackages: [],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('../package.json').build
|
||||
@@ -0,0 +1 @@
|
||||
results
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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}"
|
||||
@@ -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}"
|
||||
@@ -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)
|
||||
@@ -32,8 +32,8 @@ function validateItems(value, label) {
|
||||
fail(`${label} contains a non-string item`)
|
||||
}
|
||||
const normalized = item.trim()
|
||||
if (!normalized || normalized.length > 500) {
|
||||
fail(`${label} contains an empty or oversized item`)
|
||||
if (!normalized) {
|
||||
fail(`${label} contains an empty item`)
|
||||
}
|
||||
return normalized
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,7 @@ async function main() {
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: '@deepseek-ai/dsh-llm',
|
||||
version: '0.1.0-rc.6',
|
||||
version: '0.1.0-rc.8',
|
||||
private: true,
|
||||
type: 'module'
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -147,7 +147,7 @@ async function main() {
|
||||
)
|
||||
|
||||
const packageArguments = [
|
||||
resolve('node_modules/electron-builder/cli.js'),
|
||||
resolve('build/run-electron-builder.cjs'),
|
||||
'--projectDir',
|
||||
project,
|
||||
'--win',
|
||||
|
||||
@@ -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 }
|
||||
@@ -1,5 +1,13 @@
|
||||
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]) {
|
||||
@@ -12,7 +20,7 @@ async function verifySiteRelease(manifest, request = fetch) {
|
||||
const files = Object.values(manifest?.targets ?? {}).flatMap(
|
||||
(target) => Object.values(target?.files ?? {})
|
||||
)
|
||||
if (files.length !== 12) {
|
||||
if (files.length !== expectedInstallerCount) {
|
||||
throw new Error(`官网发布文件数量错误:${files.length}`)
|
||||
}
|
||||
const urls = new Set()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+144
-86
@@ -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 中持续规划和推进”。
|
||||
- 并行实验解决“隔离多个候选并用相同标准比较”。
|
||||
- 会话监督解决“独立观察并在必要时评论、告警或暂停”。
|
||||
- 智能心跳当前解决“在什么范围周期回顾并提出报告和建议”。
|
||||
- 记忆系统解决“哪些经验可以在什么范围内被未来运行读取”。
|
||||
- 持续学习解决“候选经验如何经过评估后改变未来行为”。
|
||||
|
||||
@@ -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,56 @@ 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` 进入同一
|
||||
队列。Scheduler 不再绕过队列直接调用 Runtime。
|
||||
- Main 对每条 Conversation 只保留一个活动请求或 Renderer 派发保留位。默认按 FIFO 认领;
|
||||
全局 Scheduled Task 执行仍受最多 4 项并发限制。
|
||||
- 用户消息由 Main 派发给 Renderer,由 Renderer 建立用户消息和流式助手消息后调用
|
||||
`agent.run`;Scheduled Task 由 Main 直接执行。两条路径共享同一 Conversation 活动锁。
|
||||
- 当前执行到达终态后再认领下一项。删除只移除尚未执行的项;“立即中断并插入”取消当前
|
||||
请求并把所选项设为下一项,不重排其他项。
|
||||
- 每条 Conversation 最多保留 20 个用户可提交的待执行项。启动时将未完成的派发恢复为
|
||||
`pending`,但应用退出期间不会实际执行任务。
|
||||
|
||||
Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收用户队列项;Main 在接受
|
||||
`agent.run` 时校验队列项仍处于 `dispatching` 且属于同一 Conversation,防止 Renderer
|
||||
绕过顺序仲裁。
|
||||
|
||||
### 7.4 幂等和租约
|
||||
|
||||
- 每次计划触发使用 `planId + scheduledFor + planVersion` 形成幂等键。
|
||||
- 手动触发使用调用方提供的单次幂等键。
|
||||
@@ -372,38 +417,35 @@ Trigger
|
||||
|
||||
## 12. 信息架构
|
||||
|
||||
建议将现有“智能心跳”逐步扩展为“自动化中心”,但保留心跳作为一种计划:
|
||||
当前阶段保留任务中心并适度完善,不新增独立自动化中心。智能心跳使用自己的菜单入口,
|
||||
并已在现有模型上实现 Global / 多 Project 范围与唯一配置入口:
|
||||
|
||||
```text
|
||||
自动化中心
|
||||
├─ 概览
|
||||
│ ├─ 正在运行
|
||||
│ ├─ 等待审批
|
||||
│ ├─ 需要关注
|
||||
│ └─ 最近结果
|
||||
├─ 计划
|
||||
│ ├─ 定时任务
|
||||
│ ├─ 智能心跳
|
||||
│ ├─ 目标任务
|
||||
│ └─ 实验
|
||||
├─ 运行
|
||||
│ ├─ 时间线
|
||||
│ ├─ 任务与步骤
|
||||
│ ├─ 监督记录
|
||||
│ ├─ 指标与证据
|
||||
│ └─ 成果
|
||||
├─ 建议
|
||||
│ ├─ 记忆候选
|
||||
│ ├─ 后续任务
|
||||
│ └─ 学习候选
|
||||
└─ 设置
|
||||
├─ 全局预算
|
||||
├─ 后台优先级
|
||||
├─ 通知
|
||||
└─ 数据保留
|
||||
任务中心
|
||||
├─ 需要关注
|
||||
├─ 进行中
|
||||
├─ 已暂停
|
||||
└─ 已结束
|
||||
└─ 打开任务自身
|
||||
|
||||
任务自身
|
||||
├─ 消息时间线
|
||||
├─ Run、步骤与审批活动
|
||||
├─ 监督、指标与证据
|
||||
└─ 独立成果
|
||||
|
||||
智能心跳
|
||||
├─ 运行概览
|
||||
├─ 待处理建议
|
||||
├─ 报告与记录
|
||||
└─ 心跳计划
|
||||
└─ Global / 指定 Project
|
||||
```
|
||||
|
||||
会话页面增加可折叠“监督”右栏,与任务、上下文和成果并列,或在已有右侧工作栏中新增页签。
|
||||
监督统一进入应用级助手工作栏中固定且始终可访问的“监督”栏目,不再保留“独立可折叠右栏”
|
||||
和“动态新增页签”两种实现。栏目默认跟随当前上下文,用户可以固定到其他 Conversation、
|
||||
Task 或实验对象;详细范围与交互契约见
|
||||
[通用助手工作栏与执行空间 PRD](../features/assistant-workbar/prd.md)。
|
||||
|
||||
## 13. 安全与隐私
|
||||
|
||||
@@ -421,7 +463,7 @@ Trigger
|
||||
|
||||
## 14. 可观测性
|
||||
|
||||
每个 Run 至少展示:
|
||||
每次 Task 执行的内部 Job/Run 记录至少保存:
|
||||
|
||||
- 触发来源和计划版本。
|
||||
- 计划目标和当前 `goalStatus`。
|
||||
@@ -429,13 +471,14 @@ Trigger
|
||||
- 实际读取的知识库与记忆分区。
|
||||
- 实际调用的模型、Token、工具、耗时和成果大小。
|
||||
- 当前预算和剩余预算。
|
||||
- 任务、步骤和子任务状态。
|
||||
- Task、Job 和 Subjob 状态。
|
||||
- Supervisor 评论、证据、严重度和处理结果。
|
||||
- 评估器版本、指标和证据。
|
||||
- 产生的候选记忆或学习产物。
|
||||
- 重试、延后、中断和恢复原因。
|
||||
|
||||
不得只显示一个模糊的“自动化成功率”而隐藏失败 Run、跳过 Run 或无结论 Run。
|
||||
UI 在 Task 下呈现上述信息的有界摘要和活动,不提供 Job/Run 树或独立导航。不得只显示一个
|
||||
模糊的“自动化成功率”而隐藏失败、跳过或无结论的 Task 执行。
|
||||
|
||||
## 15. 建议的数据模型增量
|
||||
|
||||
@@ -462,15 +505,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 +531,7 @@ experiment_runs
|
||||
### 阶段 2:会话监督与分区记忆
|
||||
|
||||
- 上线评论型会话监督。
|
||||
- 智能心跳配置支持 Global 或指定一个、多个 Project。
|
||||
- 增加 Automation 和 Run 记忆分区。
|
||||
- 建立来源、证据、时态、冲突和晋升流程。
|
||||
|
||||
@@ -506,18 +555,27 @@ 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 自动化不能绕过现有审批、主机执行策略和能力控制。
|
||||
- [ ] 会话监督默认只评论,不能替用户发言或批准工具。
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
||||
# GoodBuddy 龙芯(LoongArch)预览版构建
|
||||
|
||||
本文档是 GoodBuddy 龙芯预览版的权威构建与功能状态说明。当前目标是
|
||||
Debian `loong64` 桌面,不代表 GoodBuddy 已将龙芯纳入正式发布支持矩阵。
|
||||
|
||||
## 当前状态
|
||||
|
||||
当前流程可以在 AMD64 Linux 主机上交叉构建一个独立的
|
||||
`goodbuddy-loongarch-preview` DEB,并校验包内 Electron、node-pty 和 Koffi
|
||||
均为 LoongArch ELF。
|
||||
|
||||
预览版使用社区维护的 Electron 42.3.0 LoongArch 构建。GoodBuddy 正式版本
|
||||
当前使用 Electron 43.2.0,因此该预览包只用于移植和真机验证,不能作为正式
|
||||
发布基线。Electron 官方目前也不提供 Linux LoongArch 预编译包。
|
||||
|
||||
已完成的实际构建结果:
|
||||
|
||||
| 项目 | 结果 |
|
||||
| --- | --- |
|
||||
| GoodBuddy 源版本 | `0.11.0` |
|
||||
| 预览版本 | `0.11.0-loong64-preview.1` |
|
||||
| Debian 架构 | `loong64` |
|
||||
| Electron | 社区版 `42.3.0` |
|
||||
| DEB 包名 | `goodbuddy-loongarch-preview` |
|
||||
| 构建产物 | `GoodBuddy-0.11.0-linux-loong64-preview.deb` |
|
||||
| 已验证构建时间(UTC) | `20260825T102937Z` |
|
||||
| 已验证产物大小 | `187053208` bytes |
|
||||
| 已验证产物 SHA-256 | `461ade49f10a20ef7b28163077a2ecd6afd2c5b1b0befc673c6d174fd58497b3` |
|
||||
| DEB 解包、架构和动态库解析 | 已通过 |
|
||||
| QEMU Electron 初始化 | 共享库解析通过;Chromium 多线程初始化在 QEMU 下异常退出 |
|
||||
| 龙芯真机启动 | 尚未验证 |
|
||||
|
||||
版本号和产物名会跟随当前 `package.json` 版本变化,上表只记录首次成功构建。
|
||||
|
||||
## 功能列表
|
||||
|
||||
“已打包”只表示所需代码或组件进入 DEB,不等同于已在龙芯桌面完成运行验证。
|
||||
|
||||
| 功能 | 预览包状态 | 当前验证 |
|
||||
| --- | --- | --- |
|
||||
| GoodBuddy Electron 桌面主体 | 已打包 | Electron 可执行文件为 LoongArch ELF;尚未完成真机 GUI 启动 |
|
||||
| React 界面和主进程业务代码 | 已打包 | 生产 bundle 构建、测试、类型检查和 lint 已通过 |
|
||||
| 直接模型调用路径 | 已打包 | JavaScript 代码与生产依赖已包含;待龙芯真机端到端验证 |
|
||||
| Continue Runtime | 已打包 | CLI JavaScript 入口及 npm Runtime 已包含;待真机执行验证 |
|
||||
| 本地终端能力 | 已打包 | 输入锁文件固定的 node-pty(当前为 1.2.0-beta.15)已针对 Electron 42.3.0 交叉编译并验证为 LoongArch ELF;待真机 PTY 验证 |
|
||||
| Koffi glibc 原生绑定 | 已打包 | Koffi 3.1.4 LoongArch glibc binding 已校验为 LoongArch ELF |
|
||||
| 内置 Skills | 已打包 | 静态资源已进入 DEB |
|
||||
| GoodBuddy Agent | **不包含** | 当前 Agent 的正式构建和双 glibc/musl 产物契约不支持 LoongArch |
|
||||
| Remote Runtime | **不包含** | 当前没有 LoongArch Remote Runtime 产物 |
|
||||
| OpenCode Runtime | **不包含** | OpenCode 所需 Bun Runtime 当前不支持 LoongArch |
|
||||
| 原生 Canvas 图像解码 | **不包含** | npm 当前没有 `@napi-rs/canvas-linux-loong64-gnu` 包 |
|
||||
| Koffi musl binding | **不包含** | Koffi 3.1.4 只发布了 `linux_loong64` glibc binding |
|
||||
| 正式自动更新和发布索引 | **不支持** | 预览包不加入正式 CI、安装包矩阵或更新索引 |
|
||||
|
||||
在真机验证完成前,不应把“已打包”功能描述为正式可用。尤其是窗口显示、GPU、
|
||||
音频、系统托盘、桌面集成、PTY 交互和模型调用都必须在真实 LoongArch 桌面上
|
||||
重新验证。
|
||||
|
||||
## 与正式 GoodBuddy 隔离
|
||||
|
||||
预览版有意使用单独身份,避免覆盖正式安装:
|
||||
|
||||
- Debian 包名:`goodbuddy-loongarch-preview`
|
||||
- 产品名:`GoodBuddy LoongArch Preview`
|
||||
- 可执行文件:`goodbuddy-loongarch-preview`
|
||||
- 安装目录:`/opt/GoodBuddy-LoongArch-Preview`
|
||||
- Electron 应用身份与正式 GoodBuddy 不同,因此使用单独的用户数据目录
|
||||
|
||||
该流程不会修改 `.github/workflows/packages.yml`,也不会把 `loong64` 加入
|
||||
正式 release manifest 或下载索引。
|
||||
|
||||
## 构建前提
|
||||
|
||||
### 本地源码主机
|
||||
|
||||
- 已完成 `npm ci`
|
||||
- Node.js 和 npm 版本满足仓库要求
|
||||
- 可以成功执行 `npm run build:bundle`
|
||||
|
||||
### AMD64 交叉构建主机
|
||||
|
||||
- Linux AMD64
|
||||
- Docker
|
||||
- 建议至少 30 GiB 内存和 50 GiB 可用磁盘
|
||||
- 可访问 Ubuntu、npm、Electron headers 和社区 Electron Release
|
||||
|
||||
### Electron LoongArch shell
|
||||
|
||||
从
|
||||
[`darkyzhou/electron-loong64`](https://github.com/darkyzhou/electron-loong64)
|
||||
的 Releases 下载:
|
||||
|
||||
```text
|
||||
electron-v42.3.0-linux-loong64.zip
|
||||
```
|
||||
|
||||
构建脚本固定校验以下 SHA-256,摘要不匹配时必须停止:
|
||||
|
||||
```text
|
||||
92b0ca0c9c18ed90166918a4ac1970266c4fa967aee9277031b3b250b905526e
|
||||
```
|
||||
|
||||
这是社区产物,不是 Electron 官方发布的 Linux LoongArch 二进制。
|
||||
|
||||
## 第一步:生成架构无关输入
|
||||
|
||||
在 GoodBuddy 仓库根目录执行:
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run build:bundle
|
||||
node build/loongarch-cross/prepare-input.cjs
|
||||
```
|
||||
|
||||
默认输出:
|
||||
|
||||
```text
|
||||
dist/loongarch-cross/goodbuddy-loongarch-cross-input.tgz
|
||||
```
|
||||
|
||||
输入归档包含生产 `out`、锁文件、Skills、Continue Runtime、npm Runtime、
|
||||
图标和发布元数据。它不包含 Agent、Remote Runtime 或 OpenCode 二进制。
|
||||
|
||||
可通过 `--output` 指定其他输出位置:
|
||||
|
||||
```sh
|
||||
node build/loongarch-cross/prepare-input.cjs \
|
||||
--output /path/to/goodbuddy-loongarch-cross-input.tgz
|
||||
```
|
||||
|
||||
## 第二步:构建交叉工具链镜像
|
||||
|
||||
将 `build/loongarch-cross`、输入归档和 Electron zip 放到 AMD64 Docker
|
||||
主机,然后执行:
|
||||
|
||||
```sh
|
||||
docker build \
|
||||
--tag goodbuddy-loongarch-cross:ubuntu-26.04 \
|
||||
build/loongarch-cross
|
||||
```
|
||||
|
||||
工具链镜像包括:
|
||||
|
||||
- `loongarch64-linux-gnu-gcc` 和 G++
|
||||
- LoongArch glibc 开发 sysroot
|
||||
- QEMU user mode
|
||||
- CMake 和 Ninja
|
||||
- Rust `loongarch64-unknown-linux-gnu` target
|
||||
- Node.js、npm 和上游 node-gyp
|
||||
|
||||
可先运行基础探针:
|
||||
|
||||
```sh
|
||||
mkdir -p build/loongarch-cross/results
|
||||
docker run --rm \
|
||||
--volume "$PWD/build/loongarch-cross/results:/results" \
|
||||
goodbuddy-loongarch-cross:ubuntu-26.04
|
||||
```
|
||||
|
||||
探针必须通过 C、C++、CMake、Rust、NAPI-RS、QEMU、DEB、Koffi 和
|
||||
node-pty 检查。Canvas 和 Koffi musl 显示为 unavailable 是当前已知限制,
|
||||
不是 Debian 桌面主体 DEB 的构建失败。
|
||||
|
||||
## 第三步:生成预览 DEB
|
||||
|
||||
假设交叉构建主机上的文件布局如下:
|
||||
|
||||
```text
|
||||
input/goodbuddy-loongarch-cross-input.tgz
|
||||
electron/electron-v42.3.0-linux-loong64.zip
|
||||
preview-output/
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```sh
|
||||
docker run --rm \
|
||||
--entrypoint /usr/local/bin/goodbuddy-loongarch-preview-deb \
|
||||
--volume "$PWD/input:/input:ro" \
|
||||
--volume "$PWD/electron:/electron:ro" \
|
||||
--volume "$PWD/preview-output:/preview-output" \
|
||||
goodbuddy-loongarch-cross:ubuntu-26.04 \
|
||||
/input/goodbuddy-loongarch-cross-input.tgz \
|
||||
/electron/electron-v42.3.0-linux-loong64.zip \
|
||||
/preview-output
|
||||
```
|
||||
|
||||
每次构建创建一个 UTC 时间戳目录,包含:
|
||||
|
||||
```text
|
||||
GoodBuddy-<version>-linux-loong64-preview.deb
|
||||
preview-manifest.json
|
||||
SHA256SUMS
|
||||
```
|
||||
|
||||
脚本会执行以下强制检查:
|
||||
|
||||
1. 校验 Electron zip 的固定 SHA-256。
|
||||
2. 删除 npm 错误解析出的宿主架构可选原生包。
|
||||
3. 按 lockfile integrity 校验并安装 Koffi LoongArch glibc binding。
|
||||
4. 读取输入 lockfile 固定的 node-pty 版本,并使用上游 node-gyp 11.4.2
|
||||
和 Electron 42.3.0 headers 交叉编译。
|
||||
5. 拒绝 `node_modules` 中任何不是 LoongArch ELF 的 `.node` 文件。
|
||||
6. 拒绝意外进入包内的 Agent、Remote Runtime 或 OpenCode 目录。
|
||||
7. 生成 `Architecture: loong64` 的 DEB,解包后再次校验关键 ELF。
|
||||
|
||||
Ubuntu 发行版自带的 node-gyp 会在 Linux addon 链接阶段无条件添加
|
||||
`-lnode`,不适合此 Electron 交叉编译;不要用它替换脚本固定的上游
|
||||
node-gyp。
|
||||
|
||||
## 验证边界
|
||||
|
||||
### 已由交叉构建验证
|
||||
|
||||
- C、C++、CMake 和 Rust 生成 LoongArch ELF
|
||||
- C、C++ 和 Rust 程序通过 QEMU user mode 执行
|
||||
- 最小 NAPI-RS addon 交叉编译
|
||||
- node-pty 针对 Electron headers 交叉编译
|
||||
- Koffi LoongArch glibc binding 架构
|
||||
- Electron、node-pty、Koffi 和 DEB `loong64` 元数据
|
||||
- DEB 完整解包
|
||||
- Debian Forky rootfs 中 Electron 全部共享库解析;该检查发现并补充了
|
||||
`libpulse0` DEB 依赖
|
||||
|
||||
### 仍需真实龙芯桌面验证
|
||||
|
||||
1. 安装 DEB 并从桌面入口启动。
|
||||
2. 验证窗口、渲染、GPU 回退、输入法、系统托盘和通知。
|
||||
3. 配置模型并执行一次真实的直接模型对话。
|
||||
4. 验证 Continue Runtime 启动、取消和退出清理。
|
||||
5. 打开本地终端,验证 node-pty 输入、输出、窗口缩放和进程回收。
|
||||
6. 验证 Koffi 使用路径。
|
||||
7. 验证音频、文件选择器、剪贴板和外部链接。
|
||||
8. 确认 Agent、Remote Runtime、OpenCode 和 Canvas 相关入口不会被误认为
|
||||
预览版已支持。
|
||||
|
||||
QEMU 可以补充动态链接器和缺库检查,但不能替代上述 GUI、PTY、GPU、音频和
|
||||
桌面集成真机测试。当前 Electron 在 QEMU user mode 下可以完成全部共享库
|
||||
解析;继续进入 Chromium 多线程初始化后,QEMU 出现 target `SIGSEGV`,因此
|
||||
不能用该结果判断真机 Electron 是否可以启动。
|
||||
|
||||
## 已知依赖问题
|
||||
|
||||
- npm 的 `--cpu=loong64 --os=linux` 仍可能安装宿主 x64 的 Koffi 可选包。
|
||||
打包脚本会删除所有该作用域的已解析原生包,并按 lockfile integrity 手工
|
||||
安装 LoongArch 包。
|
||||
- `@napi-rs/canvas` 没有发布 LoongArch 原生包。基础 NAPI-RS 探针通过只说明
|
||||
Rust addon 工具链可用,不代表 Skia/Canvas 已完成移植。
|
||||
- Koffi 没有 LoongArch musl binding,因此当前不能满足 GoodBuddy Agent 的
|
||||
双 glibc/musl 产物契约。
|
||||
- 预览 Electron 比正式 GoodBuddy 低一个主版本。正式支持必须先获得与当前
|
||||
Electron 基线一致、可重复构建并完成安全维护的 LoongArch shell。
|
||||
@@ -0,0 +1,294 @@
|
||||
# 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 and ZIP for
|
||||
macOS, and AppImage, DEB, and RPM 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.
|
||||
- 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 or arm64 `.gbagent` contains the
|
||||
Agent daemon, pinned Node, and the desktop-maintained compatible OpenCode
|
||||
Runtime. Build both architectures 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.
|
||||
- `.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.
|
||||
- The tagged commit must be reachable from protected `main`. The two native
|
||||
jobs build the compound x64/arm64 packages from the same source and locks;
|
||||
the catalog job rejects missing architectures, changed bytes for an existing
|
||||
version/architecture identity, invalid prior signatures, and incompatible
|
||||
matrix metadata.
|
||||
- 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 14 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 22 assets: 14 installers (two formats
|
||||
for each Windows/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 22 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 14 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 14 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.
|
||||
@@ -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,13 @@
|
||||
# 助手工作栏
|
||||
|
||||
助手工作栏统一承载本机与远程执行空间、Task、成果、文件、浏览器和终端等持续工作界面。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [通用助手工作栏与执行空间 PRD](./prd.md) | 工作栏整体产品范围、执行空间和各工作面板归属 |
|
||||
| [多终端页签 PRD](./terminal-tabs-prd.md) | 多终端 Tab、目标选择、关闭确认和终端生命周期 |
|
||||
|
||||
当前尚未拆出独立 User Stories、功能逻辑、UI 设计、技术设计和进度文档;相关内容仍保留在
|
||||
两份现有 PRD 中,后续实际维护该功能时按职责拆分。
|
||||
@@ -0,0 +1,988 @@
|
||||
# 通用助手工作栏与执行空间 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.6 |
|
||||
| 日期 | 2026-08-31 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md) |
|
||||
| 相关能力 | [会话监督](../conversation-supervision/prd.md)、[自动化平台](../../architecture/automation-platform-architecture.md)、[长期助手路线图](../../roadmap/long-term-assistant-roadmap.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 已经在聊天右侧提供任务中心、工作区、浏览器和成果面板,也已经具备
|
||||
Runtime 事件、Git 变更、文件预览、成果存储、受控浏览器和专家执行等基础能力。后续还
|
||||
计划增加:
|
||||
|
||||
- Conversation、Task 和实验的独立监督。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 的 Runtime 生命周期监督。
|
||||
- 用户可直接使用的终端和受管进程。
|
||||
- HTML 等成果的即时安全预览。
|
||||
- 本机与 SSH 远程主机上的工作区和 Agent Runtime。
|
||||
|
||||
这些能力不能被收束为只面向编程的工作台。监督、Runtime、终端、进程、浏览器和成果
|
||||
都可以服务于普通问答、内容分析、自动化、数据处理、远程运维、知识整理和软件开发。
|
||||
同时,能力目录也不能根据当前页面、项目类型或 Runtime 能力无提示地变化,否则用户无法在
|
||||
需要时主动打开面板并选择目标、主机或运行环境。
|
||||
|
||||
本设计把右侧区域定义为应用级的“助手工作栏”,并把本机或远程的目录、终端、进程和
|
||||
Runtime 统一抽象为“执行空间”。
|
||||
|
||||
## 2. 产品定义
|
||||
|
||||
### 2.1 助手工作栏
|
||||
|
||||
助手工作栏是 GoodBuddy 中始终可访问的应用级工具容器。它提供稳定能力目录,用户从中
|
||||
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器和成果
|
||||
面板实例。稳定的是能力的可发现性,不是八个同时占据界面的固定面板。
|
||||
|
||||
工作栏不是:
|
||||
|
||||
- 只在编程项目中出现的 IDE 面板。
|
||||
- 当前聊天消息的附属详情框。
|
||||
- 根据能力探测结果自动增删入口的动态菜单。
|
||||
- 绕过 Main、Preload、Ask/Execute 或审批边界的控制台。
|
||||
- 全系统进程管理器、任意文件浏览器或无边界远程管理工具。
|
||||
|
||||
### 2.2 执行空间
|
||||
|
||||
执行空间描述工作区、终端、受管进程和 Agent Runtime 实际运行的位置:
|
||||
|
||||
```ts
|
||||
type ExecutionSpaceSelection =
|
||||
| {
|
||||
kind: 'local'
|
||||
rootPath?: string
|
||||
}
|
||||
| {
|
||||
kind: 'ssh'
|
||||
hostId: string
|
||||
remoteRootPath?: string
|
||||
}
|
||||
```
|
||||
|
||||
执行空间可以来自当前项目,也可以由用户在工作栏中临时选择。临时选择不会静默修改项目
|
||||
设置,也不直接授予文件、终端或 Runtime 能力。Main 必须先把选择草稿验证为绑定窗口、
|
||||
controller、Host/Workspace identity、信任 revision、能力和 lease 的 opaque 临时 Grant;
|
||||
后续 IPC 只引用 Grant ID。只有用户显式保存并重新通过 Project 候选验证时才成为项目默认值。
|
||||
|
||||
## 3. 核心产品原则
|
||||
|
||||
### 3.1 能力目录稳定,面板实例由用户控制
|
||||
|
||||
工作栏能力目录提供以下标准能力:
|
||||
|
||||
```text
|
||||
任务中心
|
||||
监督
|
||||
Runtime
|
||||
终端
|
||||
进程
|
||||
工作区
|
||||
浏览器
|
||||
成果
|
||||
```
|
||||
|
||||
- 应用不得根据当前项目、会话、Runtime、主机或探测结果无提示地增删能力目录项。
|
||||
- 用户主动打开、关闭、排序和停靠面板实例;应用不默认同时挂载全部能力。
|
||||
- Task Center 是 Task 的单例应用级索引。每个 Task 只关联一条 Conversation,一条
|
||||
Conversation 可以关联多个 Task;Task Center 不复制会话内容,也不显示普通 Conversation、
|
||||
Job、Run 或心跳事项。
|
||||
- 当前能力、连接、数据和空状态可以动态变化。
|
||||
- 能力不可用时,目录项或已打开面板显示原因、影响和可执行的配置或切换入口,不能只通过隐藏表示。
|
||||
- 用户可以在设置中调整目录顺序;恢复默认布局恢复标准目录与默认打开面板,不强制打开全部能力。
|
||||
- 同一能力需要并排比较不同目标时可以创建多个实例,每个实例拥有独立身份和范围绑定。
|
||||
|
||||
### 3.2 当前上下文只提供默认值
|
||||
|
||||
任务中心作为全局索引不跟随当前会话,也不支持为同一列表打开多个目标实例。其他可绑定
|
||||
目标的能力使用当前会话、项目、Runtime 和主机帮助新面板实例初始定位,但这些上下文不是
|
||||
使用门槛:
|
||||
|
||||
- 监督默认选择当前会话,用户可以改选其他 Conversation、Task 或实验对象。
|
||||
- Runtime 默认跟随当前会话或 Task;执行事件可以查看,但 Job/Run 不作为独立选择对象。
|
||||
- 终端创建时固定到当前项目执行空间,由项目配置自动决定本机或托管 SSH;没有当前项目时
|
||||
使用本机 Home。
|
||||
- 工作区默认显示当前项目目录,用户可以打开其他本机目录或远程目录。
|
||||
- 成果默认使用当前范围,用户可以切换到项目、全局或其他允许范围。
|
||||
- 下一次请求的附件和知识库由当前 Composer 展示与管理,不作为工作栏能力。历史执行上下文
|
||||
随关联 Task 查看;记忆功能实现后也归入任务中心,而不是新增独立上下文面板。
|
||||
|
||||
每个可切换目标的面板实例都提供一致的范围模式:
|
||||
|
||||
```text
|
||||
跟随当前上下文
|
||||
固定到指定对象
|
||||
```
|
||||
|
||||
固定目标失效时,面板显示“目标不可用”和修复入口,不静默回到其他目标。
|
||||
|
||||
### 3.3 用户控制打开、切换和介入
|
||||
|
||||
- 后台事件可以更新目录徽标、面板状态和通知,但不得无条件抢占当前面板。
|
||||
- 只有用户刚刚发起且明确需要面板完成的交互,才可以打开对应面板。
|
||||
- 浏览器画面、审批、监督警告、Runtime 失败和终端退出默认通过徽标或通知提示。
|
||||
- 高风险状态必须持续可见,但不以自动切页代替用户选择。
|
||||
- 用户切换页面、会话或项目时,已固定的面板目标保持不变;跟随模式才更新目标。
|
||||
|
||||
### 3.4 入口稳定不等于虚假能力
|
||||
|
||||
稳定能力目录和已打开面板必须准确呈现能力差异:
|
||||
|
||||
- 当前 Runtime 不支持后台 Job 时,Runtime 能力仍可发现;打开后说明当前可监督的内容。
|
||||
- 当前执行空间没有 Git 仓库时,工作区文件功能仍可使用,Git 区域显示不可用原因。
|
||||
- 没有活动进程时,进程面板提供创建终端或启动 Runtime 的入口。
|
||||
- 没有项目时,终端使用当前用户本机 Home;工作区等允许选择路径的能力仍必须先取得有界
|
||||
临时 Execution Space Grant,不能把可选路径直接传给文件、Git、Process 或 Runtime。
|
||||
- 监督未启用时,监督面板提供目标、模式和“开始监督”,而不是隐藏能力。
|
||||
|
||||
不得渲染成排没有解释的禁用按钮,也不得把“进程连通”描述为已经支持完整原生监督。
|
||||
|
||||
### 3.5 通用能力与领域能力分层
|
||||
|
||||
- 监督判断目标、证据、矛盾、遗漏、质量和风险,不假设目标一定是编程。
|
||||
- Runtime 监督展示运行生命周期,不假设 Runtime 一定是 OpenCode。
|
||||
- 终端和进程是通用执行能力,不只服务代码构建。
|
||||
- 工作区可以是文档、数据、知识或代码目录;Git 是可选区域。
|
||||
- HTML 预览属于通用成果能力,不只用于网页开发。
|
||||
- SSH 主机可以承载 Agent、自动化、数据处理和工作区,不只代表远程代码仓库。
|
||||
|
||||
## 4. 目标
|
||||
|
||||
### 4.1 用户目标
|
||||
|
||||
- 从任意主要页面随时发现同一组稳定能力,并按需打开所需面板。
|
||||
- 自主选择每个可绑定目标的面板实例跟随当前上下文还是固定到指定目标。
|
||||
- 在不中断主任务的情况下观察监督意见、Runtime、进程和成果。
|
||||
- 随时创建本机或远程终端,并理解其执行位置和权限。
|
||||
- 查看 GoodBuddy 管理的进程及其来源、输出和停止状态。
|
||||
- 对生成的 HTML、Markdown、JSON、图片等成果进行即时安全预览。
|
||||
- 管理 SSH 主机,并在远程执行空间中运行受控 Agent Runtime。
|
||||
|
||||
### 4.2 产品目标
|
||||
|
||||
- 建立不依赖具体页面和 Runtime 的应用级工作栏、能力目录和面板实例壳层。
|
||||
- 建立统一范围、执行空间、生命周期、成果和控制契约。
|
||||
- 复用现有 Project、Conversation、Task、Artifact、Activity 和 Approval 数据。
|
||||
- 保持 Renderer 无任意文件、进程、PTY、SSH 或 Electron API 能力。
|
||||
- 保持 Ask 只读、Execute 审批、取消、超时、输出边界和活动审计。
|
||||
- 为本机与远程能力提供一致 UI,同时准确表达能力差异。
|
||||
|
||||
## 5. 非目标
|
||||
|
||||
- 不把 GoodBuddy 改造成完整 IDE。
|
||||
- 不提供全系统进程枚举和任意 PID 终止。
|
||||
- 不默认扫描用户全部目录、远程主机或 SSH 配置。
|
||||
- 不允许 Agent 未经现有 Runtime 边界直接向用户终端注入输入。
|
||||
- 不自动执行 HTML 中的脚本或访问网络。
|
||||
- 不让监督器自动替用户发言、批准工具、扩大范围或修改安全策略。
|
||||
- 不提供以 SSH 连接为生命周期的临时远程 Agent;远程执行必须由用户级常驻 Daemon 承载。
|
||||
- 不支持任意 ProxyCommand、任意端口转发或 SSH Agent Forwarding。
|
||||
- 不要求所有 Runtime 提供相同的 Subagent、Job、Hook 或会话能力。
|
||||
|
||||
## 6. 信息架构
|
||||
|
||||
### 6.1 应用级位置
|
||||
|
||||
助手工作栏位于主窗口右侧,但不归属于聊天页面。聊天、知识、魔法笔记、自动化、活动记录
|
||||
等主要页面都可以打开它。各页面可以提供默认范围,不能维护互不相容的右栏副本。
|
||||
|
||||
```text
|
||||
┌──────────────┬──────────────────────────────┬────────────────────────┐
|
||||
│ 主导航 │ 当前主任务 │ 助手工作栏 │
|
||||
│ │ │ │
|
||||
│ 会话 / 知识 │ 聊天、文档、自动化或数据视图 │ 稳定能力目录 │
|
||||
│ 自动化 / 活动│ │ 用户打开的面板实例 │
|
||||
│ 设置 │ │ 各实例范围与执行空间 │
|
||||
└──────────────┴──────────────────────────────┴────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.2 能力目录与面板实例
|
||||
|
||||
能力目录不等于同时打开八个面板。当前产品采用“已打开实例 Tab + 持久新增按钮”的工作栏
|
||||
模型:Tab 行只显示已打开实例,行末“+”始终可见;点击后在工作栏内容区打开稳定能力目录,
|
||||
用户再创建或激活应用实例。详细交互以
|
||||
[助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 为准。
|
||||
|
||||
- 每项始终显示稳定图标,并提供可见标签或可持续查看的工具提示。
|
||||
- 已打开实例使用 `tablist`、`tab`、`tabpanel`;“+”是位于 `tablist` 外的普通命令按钮。
|
||||
- 应用注册表声明 `single` 或 `multiple`。重复选择单实例应用时聚焦已有实例;选择多实例
|
||||
应用时每次创建新实例。
|
||||
- 支持方向键、Home、End、Enter、Space、关闭面板和正确焦点恢复。
|
||||
- 徽标显示未解决数量、等待审批或失败状态,并同时提供文字或可访问名称。
|
||||
- 用户调整目录顺序、打开实例、停靠位置和尺寸后持久化;关闭实例后能力仍可从目录重新打开。
|
||||
- 默认布局只恢复经过产品确认的少量常用面板,不自动打开全部标准能力。
|
||||
- 每个实例显示稳定实例 ID、能力名称、跟随或固定状态与当前目标;同能力多实例不能只靠位置区分。
|
||||
|
||||
从当前聊天右栏迁移时,Task Center、工作区、浏览器和成果作为默认打开的单实例应用保留;
|
||||
终端不默认打开,由用户通过“+”创建,并从首期开始支持多实例。Tab 必须单行横向滚动,
|
||||
不能自动隐藏或缩写到不可辨认,“+”不能随滚动消失。Task Center 继续作为 Task 的现有
|
||||
入口;审批定位到所属 Task 或 Runtime。智能心跳不作为工作栏页签,其报告、建议、历史和
|
||||
完整配置统一归属“智能心跳”菜单入口。当前阶段不新增独立自动化中心。
|
||||
|
||||
### 6.3 工作栏尺寸
|
||||
|
||||
- 所有窗口宽度下工作栏都停靠右侧并参与水平布局回流;展开时主工作区同步收缩,不能覆盖
|
||||
或遮罩其内容。
|
||||
- 分栏计算排除最左侧主导航,主工作区与工作栏使用相同的 `300px` 最小宽度。拖动记录两侧
|
||||
在可用分栏宽度中的比例;任一侧达到最小值后停止,窗口缩放后按原比例重新计算并再次
|
||||
应用同一最小值约束。
|
||||
- 最左侧主导航保持全高连续表面;窗口标题栏只覆盖右侧内容框架,不能横跨主导航并把品牌、
|
||||
项目切换器和导航整体向下推移。
|
||||
- 对话内容与输入区始终保留对称的左右安全边距,工作栏边界不得覆盖或裁切输入框、发送
|
||||
按钮及主工作区右侧边距。
|
||||
- 唯一展开与收起开关固定在窗口关闭按钮正下方。工作栏内部不重复关闭入口。开关图标中的
|
||||
箭头方向随当前动作切换,按钮不以持续高亮表达展开状态;当前会话没有 Task 时不渲染空
|
||||
Task 上下文栏。
|
||||
- “新建任务”入口位于工作栏“任务索引”标题后方,不在会话 Task 上下文栏重复出现;索引
|
||||
状态筛选占满可用宽度并等分各筛选项。
|
||||
- 终端、宽日志和大型成果允许用户切换到底部停靠或独立窗口。
|
||||
- 应用只建议适合的布局,不因面板内容自动改变用户已经选择的停靠位置。
|
||||
|
||||
## 7. 能力与面板定义
|
||||
|
||||
### 7.1 Task Center
|
||||
|
||||
Task Center 保留为工作栏中的稳定入口,并在现有基础上适度完善:
|
||||
|
||||
- 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以关联多个 Task。
|
||||
- 显示名称、关联 Conversation、Global 或 Project 范围、Ask/Execute、状态、最近进展、
|
||||
真实活动时间和需要关注信息。
|
||||
- 点击 Task 打开其关联 Conversation 并定位 Task。
|
||||
- 完整消息留在 Conversation;工具、Subagent、审批、错误和成果按 Task 关联到 Runtime、
|
||||
活动记录和成果查看器中,不在窄栏复制,也不呈现 Job/Run 树。
|
||||
- 智能心跳的报告、建议、历史和配置不进入任务中心。
|
||||
|
||||
任务中心是单例索引,不使用其他能力的“跟随 / 固定目标”多实例模型。后台状态可以更新
|
||||
徽标和排序,但不能自动打开面板或抢占用户当前工作。
|
||||
|
||||
主侧栏最近会话为关联 Task 提供轻量入口:父会话行只显示行首展开按钮,Task 子项使用
|
||||
任务图标和本地化摘要,展开层级只到 Task;点击 Task 子项打开同一 Conversation 并定位。
|
||||
新建定制任务使用 Modal 选择当前或新
|
||||
Conversation,默认 Execute,并持续显示 Runtime、Project、工作目录、工具和审批摘要。
|
||||
|
||||
详细产品边界以 [Task Center PRD](../task-and-job/task-center-prd.md) 和
|
||||
[Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) 为准。
|
||||
|
||||
### 7.2 监督
|
||||
|
||||
监督是通用观察与评论入口,详细行为以
|
||||
[会话监督 PRD](../conversation-supervision/prd.md) 为准。
|
||||
|
||||
监督能力在目录中稳定可发现;用户打开面板实例后可以选择:
|
||||
|
||||
- 普通会话。
|
||||
- Task。
|
||||
- 实验或实验结果。
|
||||
- 后续支持的文档分析和其他可监督对象。
|
||||
|
||||
监督面板包含:
|
||||
|
||||
- 当前目标与范围。
|
||||
- 开启状态、监督模式、触发方式和预算。
|
||||
- 评论、警告、人工复核请求和证据。
|
||||
- 未解决、已查看、已解决、忽略和误报状态。
|
||||
- “带入输入框”“查看证据”“追问”“停止当前回复”等用户介入操作。
|
||||
|
||||
“采纳”只生成可编辑草稿或显式会话操作,不自动发送、执行、切换 Execute 或批准工具。
|
||||
|
||||
### 7.3 Runtime
|
||||
|
||||
Runtime 能力统一监督直连模型、OpenCode、Continue、DeepSeek Harness 和后续 Runtime。
|
||||
能力在目录中稳定可发现,打开的面板实例依据所选 Runtime 的真实能力显示状态。
|
||||
远程实现先只交付 OpenCode 官方 ACP 闭环;Continue 与 DeepSeek Harness 后续复用同一
|
||||
面板和通道契约。产品不为并行建设多套后端而增加用户可见模式或临时入口。
|
||||
|
||||
共同区域:
|
||||
|
||||
- Runtime、模型连接、Conversation 或 Task 身份。
|
||||
- 活动请求、状态、耗时、用量和取消。
|
||||
- 工具、审批、问题、上下文压缩和错误。
|
||||
- 跳转完整活动记录和持久设置。
|
||||
|
||||
可选区域:
|
||||
|
||||
- Task 级委派状态和取消。
|
||||
- Task 级后台执行进度、结果和终止。
|
||||
- Todo、Workflow 和 Hook 运行。
|
||||
- 原生会话、暂停、恢复、压缩或释放。
|
||||
|
||||
可选区域不可用时,用一段有操作路径的状态说明替代空卡片。用户可以在面板中切换 Runtime
|
||||
或 Conversation / Task 目标,不要求先回到聊天 Composer。内部 Job/Run 事件按 Task
|
||||
聚合,不提供 Job/Run 选择器、树、页面或独立操作菜单。
|
||||
|
||||
### 7.4 终端
|
||||
|
||||
终端面板允许用户通过工作栏“+”主动创建和管理多个本机或 SSH 终端。详细产品行为、首期
|
||||
直接 SSH PTY 技术选择、生命周期和验收以
|
||||
[助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 为准。
|
||||
|
||||
- 新建、重命名、切换、关闭和重新连接终端。
|
||||
- 创建时自动绑定当前项目执行空间、权威工作目录和平台默认 Shell,不显示重复目标选择。
|
||||
- 显示本机或远程主机、目录、Shell 和连接状态。
|
||||
- 支持复制、粘贴、搜索、清屏、滚动和调整终端尺寸。
|
||||
- 支持将终端切换到右侧、底部或独立窗口。
|
||||
|
||||
终端属于用户交互表面。Agent 工具调用可以产生独立受管进程和日志,但不能伪装成用户终端,
|
||||
也不能在没有明确授权的情况下向现有终端发送按键或命令。
|
||||
|
||||
### 7.5 进程
|
||||
|
||||
进程面板只展示 GoodBuddy 创建、托管或明确接管的进程:
|
||||
|
||||
- 用户终端 Shell。
|
||||
- Runtime Host、Server、Utility 和远程 GoodBuddy Agent。
|
||||
- Runtime 后台 Job。
|
||||
- 用户通过工作栏显式启动的长运行命令。
|
||||
- 浏览器或自动化中属于 GoodBuddy 的受管子进程摘要。
|
||||
|
||||
每项显示:
|
||||
|
||||
- 名称和有界命令摘要。
|
||||
- 来源、执行空间、项目或会话归属。
|
||||
- 启动时间、状态、退出码和资源摘要。
|
||||
- 有界 stdout/stderr 或结构化日志。
|
||||
- 正常终止、必要时强制终止和打开关联对象。
|
||||
|
||||
Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发的受管进程 ID,并由 Main
|
||||
重新验证所有权、当前状态和允许操作。
|
||||
|
||||
### 7.6 工作区
|
||||
|
||||
工作区面板允许用户选择:
|
||||
|
||||
- 当前项目目录。
|
||||
- 其他本机目录。
|
||||
- 已配置 SSH 主机上的远程目录。
|
||||
|
||||
面板提供:
|
||||
|
||||
- 有界目录树和文本文件预览。
|
||||
- 当前选择、规范化路径和执行空间。
|
||||
- 可选 Git 状态、Diff 和仓库信息。
|
||||
- 显式打开、下载副本或在终端中打开。
|
||||
- HTML 文件的源码与安全预览。
|
||||
|
||||
本机和远程访问都必须由 Main 或远程 GoodBuddy Agent 在对应文件系统上执行路径规范化、相对路径和
|
||||
符号链接边界检查。Renderer 只能提交受约束的相对路径和已授权范围 ID。
|
||||
|
||||
### 7.7 浏览器
|
||||
|
||||
浏览器能力在目录中稳定可发现,打开面板后允许用户:
|
||||
|
||||
- 创建新的 GoodBuddy 隔离浏览器会话。
|
||||
- 选择当前会话或其他受控浏览器会话。
|
||||
- 查看状态、当前 URL、有界画面和错误。
|
||||
- 通过顶部浏览器工具栏返回、刷新或停止加载、编辑地址并前往。
|
||||
- 进入明确的交互模式或关闭浏览器。
|
||||
|
||||
顶部工具栏按“返回、刷新/停止加载、地址输入、前往、交互、关闭浏览器”组织。“刷新”在
|
||||
页面加载期间原位切换为“停止加载”;“关闭浏览器”是清楚独立于导航的会话生命周期操作,
|
||||
必须与“停止加载”在文案和视觉上明确区分。现有“停止浏览器”语义统一改名为“关闭浏览器”。
|
||||
|
||||
浏览器面板和 Agent 操作同一条按 Conversation 归属的浏览器会话,不创建用户预览专用的
|
||||
第二个页面。用户执行前往、返回或刷新会有意改变 Agent 下一步读取和操作的页面。所有
|
||||
前往、返回和刷新请求都进入 `BrowserService`,并与 Agent 发起的浏览器操作串行执行;不在
|
||||
Renderer 建立第二套导航队列、抢占规则或恢复状态机。
|
||||
|
||||
浏览器工具栏读取的权威状态至少包含:
|
||||
|
||||
```ts
|
||||
type BrowserToolbarState = {
|
||||
committedUrl: string
|
||||
loading: boolean
|
||||
canGoBack: boolean
|
||||
sessionActivity: 'inactive' | 'creating' | 'ready' | 'agent_action' | 'full_interaction'
|
||||
}
|
||||
```
|
||||
|
||||
`committedUrl` 是浏览器已经提交的权威 URL。地址输入在未编辑时与它同步;获得编辑意图后,
|
||||
输入文字保持为 Renderer 本地草稿,画面、加载状态和 URL 更新不得覆盖草稿,直到用户“前往”
|
||||
或放弃编辑。用户提交未带 `http://` 或 `https://` 的裸域名时,Renderer 自动补上
|
||||
`http://`,再通过既有浏览器 URL 策略提交。地址草稿不属于浏览器会话持久状态,也不需要
|
||||
恢复或版本化机制。
|
||||
|
||||
创建会话、Agent 操作和完整交互窗口期间,返回、刷新、地址提交、前往、交互和关闭浏览器
|
||||
控件禁用,避免与同一会话并发操作。例外是活动导航处于加载中时,“停止加载”保持可用,
|
||||
可中断当前页面加载;
|
||||
它只取消页面加载,不销毁浏览器会话、不执行“关闭浏览器”,也不释放当前 Runtime 上下文。
|
||||
“关闭浏览器”沿用现有停止会话的生命周期语义。
|
||||
|
||||
没有浏览器会话时仍显示工具栏。活动 Conversation 可以直接在地址栏输入非空 URL 并“前往”,
|
||||
由 `BrowserService` 通过 Agent 浏览器相同的能力检查和 URL 策略创建会话并导航;不得为这一
|
||||
空状态增加旁路 WebView 或宽松 URL 入口。没有活动 Conversation 时说明需要先选择或创建会话。
|
||||
模型或后台浏览器活动可以更新徽标,但不得无条件打开面板或切换用户当前面板。
|
||||
|
||||
浏览器面板只管理 GoodBuddy 受控浏览器,不表示可以控制用户已安装的浏览器。
|
||||
|
||||
### 7.8 成果
|
||||
|
||||
成果面板统一显示全局、项目、Conversation、Task 执行和监督显式产生的独立成果:
|
||||
|
||||
- Markdown、纯文本和 JSON。
|
||||
- 图片和图表。
|
||||
- HTML 安全预览。
|
||||
- 后续的 PDF、Office、表格和其他受支持格式。
|
||||
|
||||
普通聊天回复只保留在会话消息流中,不自动复制为成果。只有 Runtime 或受管工具显式声明的
|
||||
Artifact、自动化和监督生成的独立输出,以及用户手动导入或明确保存的内容进入成果面板。
|
||||
升级前已经自动保存的普通对话 Markdown 可以从成果列表中隐藏,但不应通过升级迁移物理
|
||||
删除用户数据库内容。
|
||||
|
||||
用户可以切换范围、搜索、预览、查看来源、导出或打开关联对象。成果必须保留项目、
|
||||
Conversation、Task、内部 Run、创建者、MIME、大小、校验值和时间等可用归属;界面按
|
||||
Task 呈现来源,不把 Run 作为导航对象。
|
||||
|
||||
#### HTML 即时预览
|
||||
|
||||
- Runtime 或受管工具通过显式 Artifact 事件声明成果,不能让 Renderer 猜测任意路径。
|
||||
- Main 验证成果属于当前授权执行空间,限制大小、类型和读取范围后再持久化。
|
||||
- HTML 使用 `iframe sandbox=""` 和严格 CSP 进行脚本关闭、网络关闭的静态预览。
|
||||
- 清理脚本、事件属性、嵌套 frame、object、embed、base、link、meta refresh、表单和活动 URL。
|
||||
- 提供“预览 / 源码”切换,并持续标注“静态安全预览,脚本和网络已禁用”。
|
||||
- 不使用 `dangerouslySetInnerHTML`,不启用 Electron `webviewTag`。
|
||||
- 外部打开是明确的用户操作,并说明外部浏览器可能执行脚本或联网。
|
||||
|
||||
### 7.9 Composer 与任务上下文
|
||||
|
||||
工作栏不提供独立“上下文”能力。用户准备下一次模型请求时:
|
||||
|
||||
- 附件、图片、文档提取结果以及知识库范围在 Composer 中展示与管理。
|
||||
- Composer 提供与发送行为直接关联的预览、移除和清空操作,不在工作栏重复入口或状态。
|
||||
- 浏览器、工作区文件、授权目录以及 Runtime 或自动化绑定内容由各自能力展示。
|
||||
- Task 的历史执行上下文作为不可变快照随 Task 查看。
|
||||
- 记忆功能实现前不展示占位内容;实现后在任务中心按关联 Task 展示和管理。
|
||||
|
||||
## 8. 范围和选择模型
|
||||
|
||||
### 8.1 通用目标引用
|
||||
|
||||
各面板实例使用不包含敏感内容的目标引用:
|
||||
|
||||
```ts
|
||||
type WorkbarCapabilityId =
|
||||
| 'supervision'
|
||||
| 'runtime'
|
||||
| 'terminal'
|
||||
| 'processes'
|
||||
| 'workspace'
|
||||
| 'browser'
|
||||
| 'results'
|
||||
|
||||
type WorkbarTargetRef =
|
||||
| { type: 'conversation'; id: string }
|
||||
| { type: 'task'; id: string }
|
||||
| { type: 'experiment'; id: string }
|
||||
| { type: 'project'; id: string }
|
||||
| { type: 'workspace'; id: string }
|
||||
| { type: 'runtime-session'; id: string }
|
||||
| { type: 'terminal'; id: string }
|
||||
| { type: 'managed-process'; id: string }
|
||||
| { type: 'browser-session'; id: string }
|
||||
| { type: 'artifact'; id: string }
|
||||
```
|
||||
|
||||
Renderer 选择目标后,Main 必须重新验证对象存在、归属范围和当前用户可见性。不能把目标 ID
|
||||
直接转换为文件、进程或远程控制权限。
|
||||
|
||||
### 8.2 跟随与固定
|
||||
|
||||
```ts
|
||||
type WorkbarScopeBinding =
|
||||
| { mode: 'follow'; source: 'active-context' }
|
||||
| { mode: 'pinned'; target: WorkbarTargetRef }
|
||||
|
||||
type WorkbarPanelInstance = {
|
||||
id: string
|
||||
capability: WorkbarCapabilityId
|
||||
binding: WorkbarScopeBinding
|
||||
dock: 'right' | 'bottom' | 'window'
|
||||
}
|
||||
```
|
||||
|
||||
- 每个面板实例独立保存绑定方式;同一能力的多个实例不能共享可变选择状态。
|
||||
- 绑定只包含公开 ID,不包含路径、凭据、Token 或日志。
|
||||
- 删除固定目标后保留失效状态,直到用户选择新目标或恢复跟随。
|
||||
- 工作栏重新打开、页面切换和窗口重建后恢复用户打开的实例与选择。
|
||||
|
||||
## 9. 主机管理与远程执行空间
|
||||
|
||||
本节定义产品入口和用户行为。Host Key、凭据、远程 Agent 协议、模型请求代理、签名更新、
|
||||
断线和 Runtime 适配的技术边界以
|
||||
[SSH 远程主机与 GoodBuddy Agent 稳定终态设计](../remote-host/technical-design.md)
|
||||
为准。
|
||||
|
||||
### 9.1 设置入口
|
||||
|
||||
设置中心“平台功能”提供独立“远程项目(技术预览)”页签及开关,默认关闭。关闭时不显示
|
||||
“主机与远程执行”分类、托管 SSH 项目创建选项和已保存远程项目,Main IPC 同时拒绝新的
|
||||
SSH/远程项目操作;已有 Host、项目和凭据保持不变。若当前项目已经是远程项目,则切回
|
||||
项目列表中的第一个普通
|
||||
本地项目。重新启用后恢复完整入口和既有数据。
|
||||
|
||||
启用后,设置中心显示“主机与远程执行”分类,管理:
|
||||
|
||||
- 主机名称、地址、端口和用户名。
|
||||
- 认证方式和凭据配置状态。
|
||||
- Host Key 算法与 SHA-256 指纹。
|
||||
- 引导式身份/认证验证、远程系统和架构。
|
||||
- GoodBuddy Agent、Runtime 和能力状态。
|
||||
- 删除、重新验证或更新 Host Key。
|
||||
|
||||
主机配置是全局资源。项目或工作栏只引用主机 ID,不能复制凭据。
|
||||
删除 Host 前列出所有引用它的本地项目记录,包括归档项目;确认后仅删除本机 Host、凭据、
|
||||
项目和关联数据,不建立 SSH 连接,也不删除远端目录或内容。若当前项目被删除,界面切回
|
||||
普通本地项目。
|
||||
|
||||
当前实施状态:设置中心已经实现主机 CRUD 和模态验证向导。新增或目标/认证编辑依次完成
|
||||
连接信息、认证前 Host Key 核对、密码/系统 SSH Agent 认证和平台/架构/Shell/Home 有界
|
||||
探针,全部成功后才原子保存主机、加密凭据和完整 Host Key。认证失败可在弹窗内修改凭据
|
||||
重试;取消或失败不保留新主机。Key 变化显示旧/新指纹并要求显式高风险确认。项目创建和
|
||||
设置现可选择托管 SSH、已保存 Host、远端工作目录、OpenCode 和 Ask/Execute,不要求
|
||||
额外 trust tier 或 consent checklist。候选完成签名 Agent/Runtime、Workspace、ACP、模型桥
|
||||
和文本模型 profile 验证后原子提交。私钥导入仍未开放。旧流程已保存但
|
||||
没有成功探针记录的主机继续读取,但在完成新向导前显示为“需要重新验证”。
|
||||
|
||||
当前源码已把 Agent/Runtime 生命周期归入 Host 管理。新增或重新验证 Host
|
||||
在身份、认证和系统探针成功后先保存 Host,再只读探测实际组件状态和瞬时下载能力,然后
|
||||
结束新增向导。产品不提供自动安装开关,也不在保存后传输包。Host 卡片只有一个按版本
|
||||
事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮;次级 SegmentedControl
|
||||
选择“自动”“Host 下载”或“GoodBuddy 传输”,默认“自动”且不持久化。版本匹配 badge
|
||||
不代表环境健康。环境准备失败或取消保留 Host 并显示可重试状态;重新安装失败时显示
|
||||
本次操作未完成,并以随后重新检查的版本卡片表达当前事实,不对不确定提交宣称旧版本
|
||||
未被替换。详细技术契约见
|
||||
[SSH Host 远程环境准备与直连下载设计](../remote-host/environment-provisioning-technical-design.md)。
|
||||
|
||||
进入或切回“主机与远程执行”页面只读取本地 Host 列表,不逐台建立 SSH 连接。版本和瞬时
|
||||
下载能力仅在用户点击某台 Host 的“刷新版本”时探测,成功结果在当前 Renderer 进程内按
|
||||
Host 持久化缓存,切换页面或重启应用后返回直接显示;Host 编辑、删除或开始更新远程环境后
|
||||
不复用旧结果。该动作不是项目连接前置条件;用户
|
||||
切换已有远程项目只更新 Renderer 的本地项目选择,不建立 Host/Agent 连接。
|
||||
新建远程项目弹窗同样只读取本地 Host 验证记录,不扫描所有 Host;浏览目录或保存时才连接
|
||||
所选 Host。版本号相同时仍允许用户显式重新安装,以修复 registry、签名或 identity 异常,
|
||||
且不删除 Host、凭据或项目。由远程主机安装只使用 SSH 和远端系统工具,不依赖已有 Agent;
|
||||
“自动”仅在操作开始时探测:直连明确可用才选 Host 下载,否则选 GoodBuddy 传输;用户
|
||||
显式选择不被改写,prepare、commit 或 adoption 失败后不在同次操作中切换 acquisition。
|
||||
|
||||
项目选择器中每个项目的管理按钮默认隐藏,在悬停、键盘焦点或触屏环境显示。该入口直接
|
||||
打开目标项目设置而不先切换项目,因此 Host 不可达或远端目录已不存在时仍可删除本地项目
|
||||
记录;删除操作从不删除远端项目内容。
|
||||
|
||||
应用重启后固定进入项目列表中的第一个普通本地项目。用户主动打开已有托管 SSH 项目时只
|
||||
切换本地项目配置;首次实际使用 Workspace 或 Runtime 时,Main 才通过 Agent/Runtime
|
||||
安装管理器解析 Host current identity,执行固定 `attach-or-bootstrap`,并使用同一 Agent
|
||||
连接。
|
||||
项目数据库不保存 Agent installation、Host revision、Workspace identity 或 Runtime
|
||||
digest。当前 registry、连接或 capability 无效时项目保持未切换,并提示到 Host 管理显式
|
||||
修复。当前进程缓存每个 Host 已确认的 Agent/Runtime identity,并复用 Agent 连接;Host
|
||||
编辑或环境更新会定向清空。Agent 的 attach/按需启动只读取 Host 管理已提交的 registry 与
|
||||
匹配 manifest 元数据,不重新验签或哈希完整 Agent payload。项目切换器立即提交本地选择,
|
||||
不展示远程激活进度;完整进度和取消只属于新建或显式保存项目。
|
||||
|
||||
项目只持久化 Host ID、规范远端路径、Runtime 选择和默认工作模式。新建和保存验证当前
|
||||
Workspace/Runtime,但不写入组件 identity;普通切换不做远程验证。未就绪或版本过旧时,
|
||||
实际 Workspace/Runtime 操作显示错误。桌面更新造成组件版本变化时,用户先在 Host 管理中
|
||||
准备环境;项目切换本身不连接、不下载、不扫描、不安装,也不改写项目。
|
||||
|
||||
已保存 Host 的地址、用户或固定 Host Key 变化后,Main 关闭旧连接并定向退役该 Host 的
|
||||
Runtime 缓存。引用项目下次选择时使用 Host 管理的当前 revision、Host Key 和用户执行固定
|
||||
attach 并读取 current registry,不存在项目中的旧 Host revision 或组件 identity;
|
||||
当前环境无效时才要求显式修复。
|
||||
|
||||
新建托管 SSH 项目时,远端工作目录既可手工输入,也可通过文件夹按钮打开 Main 管理的
|
||||
只读、有界 SFTP 目录选择器。选择器从 SSH 账号 Home 或当前有效绝对路径开始,只返回目录,
|
||||
支持上级、刷新、取消和选择当前目录;Host 变化、连接错误或取消都保留草稿,选择结果不会
|
||||
绕过正常项目验证与保存事务。
|
||||
|
||||
### 9.2 凭据和主机验证
|
||||
|
||||
- 优先支持系统 SSH Agent 或 OpenSSH 证书。
|
||||
- 导入私钥或密码时使用 Electron `safeStorage` 加密。
|
||||
- 凭据绑定主机 ID、地址、端口、用户名和认证类型。
|
||||
- Renderer 只接收 `credentialConfigured`、来源和错误状态。
|
||||
- Host Key 检查使用 Main 内五分钟有效、目标绑定的临时候选,检查阶段不接收认证凭据;
|
||||
认证和系统探针成功前不持久化新增主机,取消或失败不留下主机或密码。
|
||||
- 首次连接必须在发送密码、私钥签名或 SSH Agent 签名前取得并展示 Host Key 算法和
|
||||
SHA-256 指纹,由用户显式接受;持久化完整 Host Key 或可信 Host CA,不能只保存展示指纹。
|
||||
- Host Key 变化硬失败,并通过独立高风险流程替换。
|
||||
- 禁止 `StrictHostKeyChecking=no` 和默认 SSH Agent Forwarding。
|
||||
- 命令参数、URL、日志、SQLite 明文和 IPC 响应中不得出现私钥或密码;用户在受信任表单
|
||||
输入的密码只通过严格校验的单用途 IPC 请求交给 Main,Renderer 不得读取已保存密码。
|
||||
|
||||
### 9.3 远程 GoodBuddy Agent
|
||||
|
||||
远程能力通过版本化 GoodBuddy Agent 提供:
|
||||
|
||||
- 安装到远程当前用户的 GoodBuddy-owned 目录,不要求 root。首次连接先 attach,Agent
|
||||
不存在或未运行时幂等 bootstrap 为 detached process,再重新 attach;不注册开机服务,
|
||||
不依赖 systemd、D-Bus 或 `Linger=yes`。
|
||||
- SSH 负责 Host Key、认证、固定 bootstrap、可选 SFTP 安装和 `goodbuddy-agent attach`
|
||||
中继;attach 只连接用户私有 Unix socket。SSH relay 断开不终止 Agent 或活动 Runtime。
|
||||
- Host 环境准备可让远端按签名目录固定的 GitHub/北京镜像 URL、大小和 SHA-256 下载完整
|
||||
`.gbagent`,也可由 GoodBuddy 下载、完整验证后通过有界流式 SFTP 传输一个 compound
|
||||
archive 和归档中已验证的 bootstrap Node;完整包不一次读入 Main `Buffer`,Host 仍再次
|
||||
校验。两种 acquisition 交付到固定 operation staging 后共用
|
||||
`prepare → commit → Agent activate/health → Runtime activate → finalize → cleanup`。
|
||||
- 直连首次 bootstrap 不依赖已安装 Agent daemon。GoodBuddy 桌面控制面通过固定 SSH
|
||||
prepare channel 发送有界 one-shot installer;Host 先验证目录绑定的归档大小和 SHA-256,
|
||||
再由 format v1 归档内固定 Node 解码并运行 installer。归档无需包含
|
||||
`agent/lib/package-installer.cjs`,目录不提供或检查额外的 bootstrap 能力元数据。
|
||||
- Agent 与 Runtime 各自维护版本,签名摘要标识精确工件。Host 在解包到准备目录的同一遍
|
||||
处理中完成完整 payload 校验,commit 只复核签名 metadata 并原子替换 side-by-side
|
||||
目录,不重新读取归档、扫描旧目录或使用跨安装硬链接。
|
||||
- Main 只保存清理 operation staging 所需的 operation ID。控制通道中断或 adoption 失败
|
||||
后,下次更新尽力清理旧暂存并重新 prepare,不保存或恢复 Agent/Runtime metadata 副本。
|
||||
新 Agent/Runtime 各自最多完整验证一次;注册后的
|
||||
health、capabilities 和 prompt 启动只检查 registry、签名 manifest 与入口 metadata。
|
||||
确认 adoption 后显式 cleanup,cleanup 失败不回滚健康环境,也不阻塞下一次 fresh prepare。
|
||||
- 握手报告 installation、Daemon boot identity、协议、系统、架构、用户监督器和能力。
|
||||
- 在远程执行路径规范化、Git、文件、PTY、进程组和 Runtime 管理。
|
||||
- 持久化有界 Agent 到 Main 输出;SSH 断开后 Agent 继续运行,同一 Agent 重连时只恢复
|
||||
可确认的输出。Main 到 Runtime 的输入、工具、模型和 blob 请求不自动重放。
|
||||
- 对有副作用的请求使用稳定 operation ID 和 payload digest 去重;结果无法证明时进入
|
||||
`outcome-unknown`,禁止自动重放。
|
||||
- 对事件、日志、journal、文件、帧、超时、并发、缓存和总传输量设置硬上限。
|
||||
- Daemon、Runtime、受管进程和用户终端分别具有明确 lease、取消、drain 和回收语义。
|
||||
- Agent 不可用或身份不匹配时远程执行失败,不退化为 SSH stdio Runtime。
|
||||
|
||||
Host Key、远端用户或目标变化会关闭旧连接,并使绑定旧 Host identity 的 Workspace 和
|
||||
Runtime 会话失效。产品不维护 T1/T2/T3 等附加信任层级。
|
||||
|
||||
### 9.4 远程 Runtime
|
||||
|
||||
- Runtime 在远程执行空间内运行,不能让本机 Runtime 对远程路径进行伪本地操作。
|
||||
- Main 保持可信控制面,远程 GoodBuddy Agent 只接受有范围、有期限的请求。
|
||||
- GoodBuddy 控制协议负责 Agent、Workspace、Git、Process、Blob 和 Runtime supervision;
|
||||
Runtime Session、stream、tool、permission、usage 和取消优先复用 ACP。
|
||||
- Ask 的只读限制在远程 GoodBuddy Agent 和 Runtime 适配层共同强制。
|
||||
- Execute 是用户对所选 SSH 账号完整权限的授权,直接以该账号可用的文件、进程、网络和
|
||||
工具能力运行,不要求逐工具审批或第二个“受控执行”模式。取消、超时、输出上限和活动
|
||||
记录继续保留。
|
||||
- GoodBuddy 模型凭据和 Provider URL 必须留在 Main。远程 Runtime 只获得不含凭据的模型
|
||||
policy、私有 Unix socket、隔离 loopback endpoint 和 Prompt-bound blob channel;
|
||||
Main 按 durable round、profile digest、调用次数、最坏输出 token 预留和最终 delivery ACK
|
||||
代发,不得把原始 API Key 或认证头发送到远端。
|
||||
- 不向远程 Runtime 暴露通用本机 MCP、浏览器、文件系统或其他未分配能力。
|
||||
|
||||
## 10. Runtime 与进程统一生命周期
|
||||
|
||||
需要新增统一、受限的生命周期模型:
|
||||
|
||||
```ts
|
||||
type ManagedLifecycleState =
|
||||
| 'starting'
|
||||
| 'running'
|
||||
| 'waiting_approval'
|
||||
| 'paused'
|
||||
| 'reconciling'
|
||||
| 'stopping'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'cancelled'
|
||||
| 'interrupted'
|
||||
| 'outcome_unknown'
|
||||
```
|
||||
|
||||
每个 Runtime 会话、Task 级执行、终端或受管进程公开:
|
||||
|
||||
- GoodBuddy 受管 ID。
|
||||
- 类型、来源和父子关系。
|
||||
- 执行空间和范围。
|
||||
- 状态、开始与结束时间。
|
||||
- 支持的控制动作。
|
||||
- 有界进度、用量和日志游标。
|
||||
|
||||
控制动作按能力声明:
|
||||
|
||||
```ts
|
||||
type ManagedControl =
|
||||
| 'cancel'
|
||||
| 'terminate'
|
||||
| 'force-terminate'
|
||||
| 'pause'
|
||||
| 'resume'
|
||||
| 'reconnect'
|
||||
| 'release'
|
||||
```
|
||||
|
||||
界面不能因为状态枚举中存在某个动作就假设所有 Runtime 都支持。Main 根据当前受管对象和
|
||||
能力重新验证动作。内部 Job/Run 快照可以支持聚合与审计,但不能成为
|
||||
`WorkbarTargetRef` 或独立 UI 对象。
|
||||
|
||||
## 11. 数据与契约建议
|
||||
|
||||
### 11.1 共享 Zod 契约
|
||||
|
||||
建议新增:
|
||||
|
||||
- `workbar-contracts.ts`
|
||||
- `managed-process-contracts.ts`
|
||||
- `terminal-contracts.ts`
|
||||
- `remote-host-contracts.ts`
|
||||
- 通用 Artifact Event 和 Preview 契约
|
||||
- Runtime Inspector Snapshot 和 Control 契约
|
||||
|
||||
所有输入严格限制字符串、数组、日志、帧、路径、端口和事件数量。公开快照不得包含:
|
||||
|
||||
- 凭据和认证头。
|
||||
- 完整环境变量。
|
||||
- 任意本机或远程绝对路径,除非该路径本身是用户当前可见对象。
|
||||
- 未经限制的 stdout/stderr、文件或 Runtime 响应。
|
||||
- 可直接传给系统 kill、spawn、Shell 或 SSH 的自由参数。
|
||||
|
||||
### 11.2 持久化
|
||||
|
||||
建议增加:
|
||||
|
||||
```text
|
||||
workbar_preferences
|
||||
remote_hosts
|
||||
terminal_sessions
|
||||
managed_processes
|
||||
runtime_sessions
|
||||
runtime_jobs
|
||||
remote_agent_installations
|
||||
remote_operations
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- 工作栏偏好只保存能力目录顺序、面板实例、停靠布局、尺寸和目标引用。
|
||||
- 主机表只保存非敏感元数据和加密凭据引用。
|
||||
- 应用重启后先通过远程 Daemon 的 journal、lease、boot identity 和 event cursor 核对活动
|
||||
终端、进程与 Runtime;无法确认时标记 `interrupted` 或 `outcome_unknown`。
|
||||
- 日志使用有界环形缓冲或分页持久化,不能无限写入 SQLite。
|
||||
- Artifact 继续作为成果的权威实体,不把完整成果复制进工作栏状态。
|
||||
- 当前 Renderer `localStorage` 活动记录不能作为 Runtime、监督或进程的权威来源。
|
||||
|
||||
### 11.3 IPC 与 Preload
|
||||
|
||||
Renderer 只通过显式方法访问:
|
||||
|
||||
- 工作栏偏好和目标绑定。
|
||||
- 主机 CRUD、临时 Host Key 检查、验证并原子保存。
|
||||
- 终端创建、输入、调整大小、关闭和有界输出订阅。
|
||||
- 受管进程列表、日志和允许的控制动作。
|
||||
- Runtime Inspector 快照、事件和允许的控制动作。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务。
|
||||
|
||||
每个 Main Handler 都必须验证可信发送者、Zod 输入、对象归属和当前状态。不得暴露 raw
|
||||
Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
## 12. 安全边界
|
||||
|
||||
1. 工作栏能力目录项和面板实例不授予任何能力;权限只由 Main 中的范围和控制契约产生。
|
||||
2. Ask 在本机和远程 Runtime 边界保持只读。
|
||||
3. Execute 继续经过现有 Runtime 和审批控制,工作栏不能直接放宽。
|
||||
4. 用户终端和 Agent 工具执行使用不同身份和事件来源。
|
||||
5. 进程面板只控制 GoodBuddy 受管对象,不接受任意 PID。
|
||||
6. 本机和远程路径分别在对应文件系统上 canonicalize 并验证符号链接边界。
|
||||
7. HTML 默认静态、无脚本、无网络、无 Electron API。
|
||||
8. Supervisor 不接收授权回调,不能批准工具或替用户发送消息。
|
||||
9. SSH Host Key 必须固定,凭据保留在 Main 加密存储。
|
||||
10. 远程端只获得请求级、可撤销、最小范围能力。
|
||||
11. 关闭面板或切换目标、主机或 Runtime 时,旧订阅必须取消;其他固定实例的订阅明确保留。
|
||||
12. 通知、徽标和日志不得包含密钥、私人正文或未脱敏提供商响应。
|
||||
|
||||
## 13. 状态、错误和恢复
|
||||
|
||||
每个面板实例区分:
|
||||
|
||||
- 尚未选择目标。
|
||||
- 目标为空。
|
||||
- 正在连接或加载。
|
||||
- 正常可用。
|
||||
- 部分可用。
|
||||
- 当前能力不支持。
|
||||
- 连接失败。
|
||||
- 权限不足或只读。
|
||||
- 目标已失效。
|
||||
- 操作已取消或中断。
|
||||
|
||||
错误必须保留用户选择、终端缓冲、输入草稿、范围和可重试上下文。短期成功和非局部错误使用
|
||||
应用通知;预览失败、终端断线、Host Key 变化、监督证据失效等需要本地恢复的错误留在面板
|
||||
内。同一事件不得同时重复显示为面板警告和应用通知。
|
||||
|
||||
## 14. 性能与资源边界
|
||||
|
||||
- 工作栏关闭或面板实例关闭时停止对应非必要画面和高频日志推送,但保留 Main 中的受管运行。
|
||||
- 每个打开的面板实例只订阅其跟随或固定目标,不进行全局无界监听。
|
||||
- 终端和日志使用增量序号、环形缓冲和背压。
|
||||
- HTML、文件、目录、浏览器画面和远程传输沿用或收紧现有大小限制。
|
||||
- Runtime Snapshot 与事件流分离,重新打开时先取权威快照,再接增量事件。
|
||||
- 监督使用独立低优先级并发池和预算,不延迟前台回答。
|
||||
- 应用退出时停止新操作并让用户选择取消、等待或 detach;关闭 SSH attach 不卸载或终止
|
||||
用户级 GoodBuddy Agent Daemon。依赖 Main 模型网关、审批或本机工具的 Run 不能被描述为
|
||||
可以无人值守继续。
|
||||
|
||||
## 15. 无障碍与响应式
|
||||
|
||||
- 能力目录、所有面板实例、目标选择器、终端控制和进程操作可用键盘完成。
|
||||
- 能力目录与面板标题具有稳定可访问名称,徽标不是唯一状态信号。
|
||||
- 终端需要独立可访问说明,并允许关闭动画和声音提示。
|
||||
- 进程和 Runtime 高频日志不逐行进入实时区域,只播报重要状态变化。
|
||||
- 监督证据定位后将焦点移动到对应对象,并提供返回监督记录的方式。
|
||||
- HTML iframe 有明确标题、静态安全说明和源码替代视图。
|
||||
- 窄窗口下能力目录仍完整可达,不因空间不足隐藏能力。
|
||||
- 文字缩放到 200% 时,当前目标、执行空间、风险状态和停止操作不能被裁切。
|
||||
|
||||
## 16. 分阶段实施
|
||||
|
||||
本节只表示内部依赖顺序,不表示可以发布临时架构。SSH 远程执行只使用用户级 Daemon,
|
||||
不发布连接期 stdio Agent。OpenCode Ask、OpenCode Execute 和后续 Runtime 分别通过自身的
|
||||
安装、权限、取消和重连门槛后开放,未完成的能力继续显示不可用。
|
||||
|
||||
### 阶段 0:应用级工作栏壳层
|
||||
|
||||
- 将当前聊天专属右栏提升为应用级壳层。
|
||||
- 建立稳定能力目录和用户打开、关闭、排序、停靠的面板实例模型。
|
||||
- 建立实例级跟随、固定和失效目标语义。
|
||||
- 保留现有任务中心、工作区、浏览器和成果行为;附件与知识库继续由 Composer 管理。
|
||||
- 将 Task Center 明确为 Task 的单例索引,并补齐范围、状态、最近进展、需要关注和直接打开 Conversation。
|
||||
- 审批在所属任务或 Runtime 中持续可见,不新增独立审批面板。
|
||||
- 智能心跳菜单入口承接完整配置和范围后,再从任务中心移除重复表单;不得移除任务中心本身。
|
||||
|
||||
### 阶段 1:成果与 Runtime 可观测性
|
||||
|
||||
- 通用 Artifact Event。
|
||||
- HTML 工作区和成果的静态即时预览。
|
||||
- Runtime Inspector Snapshot 与事件。
|
||||
- OpenCode 会话、子会话、Todo、工具、用量和取消;本地与托管 SSH 的原生 Task 调用在
|
||||
Conversation 中统一显示为可追踪的 Subagent 状态,而不是普通工具参数卡。
|
||||
- 直连模型、OpenCode 原生 Subagent 及现有 GoodBuddy Subagent 的统一展示。
|
||||
|
||||
### 阶段 2:监督、终端与受管进程
|
||||
|
||||
- 普通会话手动监督和右栏评论流。
|
||||
- 工作栏持久“+”、应用注册表和单/多实例 Tab。
|
||||
- 本机 PTY 与直接 SSH PTY 多终端。
|
||||
- 受管进程注册、日志和终止。
|
||||
- 自动回复后监督、节流和独立预算。
|
||||
- 用户选择终端停靠位置。
|
||||
|
||||
### 阶段 3:Runtime 原生长期能力
|
||||
|
||||
- Continue 会话级 Host。
|
||||
- Continue Background Job、Subagent 和 Hook 的有界适配。
|
||||
- DeepSeek Harness 后续服务的能力握手。
|
||||
- Runtime Job、Workflow 和会话恢复契约。
|
||||
|
||||
### 阶段 4:SSH 主机与远程执行空间
|
||||
|
||||
- [x] 引导式主机管理、先验证后保存、密码安全存储、系统 SSH Agent、Host Key 固定和
|
||||
有界系统探针。
|
||||
- [ ] 私钥与受限 OpenSSH config 导入。
|
||||
- 签名、side-by-side GoodBuddy Agent 安装和按需 detached Daemon。
|
||||
- SSH connection pool、固定 attach relay、Agent 控制协议、Agent 本地 ACP 日志和断线重连同步。
|
||||
- 远程工作区、Git、终端和受管进程。
|
||||
- 先完成签名 OpenCode bundle、Ask bubblewrap、Execute 直接进程、官方 ACP channel、
|
||||
Main 模型网关、取消、超时和活动记录。
|
||||
- OpenCode 闭环稳定后,Continue 与 DeepSeek Harness 复用相同面板和通道契约逐个接入。
|
||||
- 新增或重新验证 Host 时只保存并探测 Agent/Runtime,不自动安装。用户在 Host 卡片用
|
||||
单一主按钮启动,并用次级控件选择自动、Host 下载或 GoodBuddy 传输;Host 保存与环境
|
||||
准备是两个可独立重试的事务。
|
||||
- 控制面直连源码直接使用既有 package format v1 包;不等待发布携带 installer 的新
|
||||
Agent 包,也不通过额外目录元数据判断 bootstrap 能力。GitHub/北京镜像、
|
||||
Linux x64/arm64、取消和离线 GoodBuddy 传输仍须真实 Host 验收,不能据此声称已发布或已完成
|
||||
真实 Host 测试。
|
||||
- 一次远程项目保存请求完成 Host/Agent/Workspace/Runtime 当前环境验证和 SQLite 原子
|
||||
提交,但只持久化 Host、路径、Runtime 选择和模式,也不获取或安装组件。
|
||||
- 桌面更新后,用户先在 Host 管理中更新当前签名 Agent/Runtime;远程项目下一次实际使用
|
||||
Workspace/Runtime 时直接使用新的 current 环境,无需刷新项目绑定。应用启动和项目切换
|
||||
本身都不连接 Host,也不下载或安装组件。
|
||||
- SSH、Main、Daemon、Runtime 和远端重启后的核对、恢复与 `outcome_unknown`。
|
||||
- 支持平台的安装、Ask、Execute、取消和重连冒烟测试通过后开放对应 OpenCode 能力。
|
||||
|
||||
### 阶段 5:开放后的扩展
|
||||
|
||||
- 更多远程系统和架构。
|
||||
- PDF、Office 和数据成果预览。
|
||||
- Conversation、Task 和实验的完整监督。
|
||||
- 用户可导入导出工作栏布局和主机非敏感配置。
|
||||
|
||||
## 17. 验收标准
|
||||
|
||||
### 17.1 稳定能力目录与用户控制
|
||||
|
||||
- [ ] 八个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] Task Center 继续作为 Task 的单例索引,不删除入口、不复制会话,也不混入 Job、Run 或心跳事项。
|
||||
- [ ] 项目、会话、Runtime 或主机变化不会无提示地增删能力目录项。
|
||||
- [ ] 用户可以按需打开、关闭、排序和停靠面板实例。
|
||||
- [ ] 用户可以独立设置每个可绑定目标的面板实例跟随或固定目标,并为同一能力打开多个目标实例。
|
||||
- [ ] 固定目标失效后显示修复状态,不静默切换。
|
||||
- [ ] 后台事件不会无条件打开面板或抢占用户当前实例。
|
||||
- [ ] 用户可一键恢复标准能力目录和默认的少量打开面板。
|
||||
|
||||
### 17.2 通用使用
|
||||
|
||||
- [ ] 没有项目时仍可创建终端、选择工作区、打开浏览器和查看成果。
|
||||
- [ ] 监督可以作用于普通 Conversation、Task 和后续实验对象,不假设编程语境。
|
||||
- [ ] 工作区不是 Git 仓库时仍可浏览文件。
|
||||
- [ ] Runtime 不支持某项原生能力时仍可从目录打开面板并获得准确说明。
|
||||
|
||||
### 17.3 安全与控制
|
||||
|
||||
- [ ] Renderer 没有任意文件、Shell、进程、PTY、SSH 或 Electron API。
|
||||
- [ ] Agent 不能未经授权向用户终端注入命令。
|
||||
- [ ] 进程面板不能枚举或终止任意系统进程。
|
||||
- [ ] Ask 在本机和远程执行空间均无法调用写入或外部副作用工具。
|
||||
- [ ] HTML 预览无法执行脚本、联网、打开窗口、提交表单或访问 Electron API。
|
||||
- [ ] Supervisor 不能自动发送消息、批准工具、切换工作模式或扩大范围。
|
||||
- [ ] SSH 首次连接和 Host Key 变化均经过明确验证流程。
|
||||
- [ ] 凭据不进入 Renderer、日志、SQLite 明文或命令参数。
|
||||
- [ ] Host 地址、用户或 Host Key 变化会关闭旧连接,并使旧 Workspace 和 Runtime 会话失效。
|
||||
|
||||
### 17.4 生命周期与恢复
|
||||
|
||||
- [ ] Runtime、终端、Task 级执行和进程具有权威 Main 快照和有序增量事件。
|
||||
- [ ] 取消、终止、失败、断线和应用退出都有确定终态。
|
||||
- [ ] 远程执行只通过按需 detached Daemon 提供,SSH attach 断开不销毁 Daemon 状态。
|
||||
- [ ] 重连先恢复 event cursor 并查询 operation;有副作用操作不会自动重放,无法确认的
|
||||
结果显示为 `outcome_unknown`。
|
||||
- [ ] Agent side-by-side 更新支持所需签名版本的健康切换,失败时保留旧项目绑定。
|
||||
- [x] 新增 Host 只探测远端下载能力,不提供自动安装开关,也不传输完整包;Host 卡片用
|
||||
一个按版本事实命名的主按钮与不持久化的 acquisition SegmentedControl 启动准备。
|
||||
- [x] 创建、保存、打开和切换托管 SSH 项目都从 Host current registry 与当前 Agent
|
||||
连接验证环境;项目只保存稳定配置,且这些流程不下载 `.gbagent`、上传 payload 或发布
|
||||
安装。
|
||||
- [ ] 切换跟随目标后不显示上一对象的过期状态。
|
||||
- [ ] 固定目标的订阅在页面切换后保持,关闭时正确释放。
|
||||
- [ ] 日志、终端、文件、成果和远程传输均有明确上限和背压。
|
||||
|
||||
### 17.5 可用性
|
||||
|
||||
- [ ] 宽、中、窄窗口均可访问完整能力目录和用户打开的面板实例。
|
||||
- [ ] 工作栏在宽、中、窄窗口展开时都推动主工作区回流,不覆盖、遮罩或隔离主内容;唯一
|
||||
开关始终固定在窗口关闭按钮正下方。
|
||||
- [ ] 仅使用键盘可以选择能力、面板实例、目标、执行空间和控制动作。
|
||||
- [ ] 状态不只依赖颜色,徽标具有文字或可访问名称。
|
||||
- [ ] 终端、HTML、监督证据和高频日志具有可访问替代或降噪行为。
|
||||
- [ ] 浏览器 Tab 顶部工具栏始终清楚区分“停止加载”和“关闭浏览器”;地址编辑草稿不会被
|
||||
权威状态刷新覆盖,用户与 Agent 的导航通过同一 `BrowserService` 会话串行执行。
|
||||
- [ ] 创建中、Agent 操作中和完整交互期间禁用浏览器其余工具栏控件;活动加载仍可单独停止,
|
||||
且停止加载不会关闭会话或释放 Runtime 上下文。
|
||||
- [ ] 活动 Conversation 没有浏览器会话时,可从同一地址栏按 Agent 相同 URL 策略启动会话。
|
||||
|
||||
## 18. 相关文档的职责
|
||||
|
||||
- 本文是助手工作栏稳定能力目录、用户面板实例、范围控制和执行空间的产品总契约。
|
||||
- [助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 定义持久“+”、应用实例策略、
|
||||
动态 Tab 以及本机与直接 SSH 多终端。
|
||||
- [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) 定义 Task、Conversation、
|
||||
Job、Subjob、Run 与 Subagent。
|
||||
- [Task Center PRD](../task-and-job/task-center-prd.md) 定义应用级 Task 索引。
|
||||
- [智能心跳 PRD](../smart-heartbeat/prd.md) 定义心跳入口、范围和长期边界。
|
||||
- [会话监督 PRD](../conversation-supervision/prd.md) 定义监督判断、证据、预算和介入边界。
|
||||
- [自动化平台总体设计](../../architecture/automation-platform-architecture.md) 定义 Plan、Job、Run、监督、预算和记忆。
|
||||
- [长期助手路线图](../../roadmap/long-term-assistant-roadmap.md) 记录整体长期能力与实施背景。
|
||||
- [DeepSeek Harness Runtime 设计](../deepseek-harness/technical-design.md) 定义该 Runtime 的具体适配边界。
|
||||
- [SSH Host 远程环境准备与直连下载设计](../remote-host/environment-provisioning-technical-design.md)
|
||||
定义 Host 级组件获取、首次准备、更新、回退和项目只验证边界。
|
||||
- [统一界面设计系统](../../../UI-DESIGN.md) 定义视觉、语义、响应式和无障碍规则。
|
||||
|
||||
若其他文档把工作栏描述为八个同时固定显示的栏目、根据项目或 Runtime 自动裁剪的动态入口,
|
||||
或仅属于当前聊天的附属区域,以本文“能力目录稳定、面板实例由用户打开、当前上下文只提供
|
||||
默认值”的产品决策为准。
|
||||
@@ -0,0 +1,582 @@
|
||||
# 助手工作栏多终端页签 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台验证中 |
|
||||
| 版本 | 1.1 |
|
||||
| 日期 | 2026-08-31 |
|
||||
| 适用产品 | 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` 实例策略。
|
||||
- 创建实例时可用的默认上下文。
|
||||
- 当前不可用时的原因和修复入口。
|
||||
|
||||
目录中的应用可发现性与实例是否已经打开相互独立。应用不可用时保留入口并说明原因,不以
|
||||
隐藏代替状态。
|
||||
|
||||
### 3.2 工作栏 Tab 实例
|
||||
|
||||
工作栏 Tab 是应用的一次打开实例。每个实例具有独立 ID、标题、应用类型和状态。Tab 行只
|
||||
显示已经打开的实例,不把全部能力同时渲染为 Tab。
|
||||
|
||||
### 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. 任务中心、工作区、浏览器和成果首期保持单实例;后续只需修改应用注册表策略即可开放
|
||||
多实例,不重写 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 时出现;
|
||||
粗指针或无悬停设备始终显示,出现与隐藏不得引发布局位移。
|
||||
|
||||
### 7.2 新建应用选择页
|
||||
|
||||
点击“+”后:
|
||||
|
||||
1. 保留当前实例及其运行状态。
|
||||
2. 在工作栏内容区显示“新建工作栏应用”。
|
||||
3. 列出稳定应用目录,每项显示名称、说明、单/多实例提示和当前可用状态。
|
||||
4. 初始焦点进入第一个可用应用;Escape 或“取消”返回原活动实例和“+”按钮。
|
||||
5. 选择单实例应用时,若实例已存在,直接聚焦该 Tab。
|
||||
6. 选择终端时直接解析当前项目执行空间,创建新 Tab 并聚焦终端输入,不再要求用户
|
||||
重复选择本机或远程目标。
|
||||
|
||||
选择页是“+”触发的临时界面,不进入实例顺序,不持久化,也不占用一个普通 Tab。
|
||||
|
||||
### 7.3 首期应用注册表
|
||||
|
||||
| 应用 | 实例策略 | 升级后默认状态 | 再次从“+”选择 |
|
||||
| --- | --- | --- | --- |
|
||||
| 任务中心 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 工作区 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 浏览器 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 成果 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 终端 | `multiple` | 不打开 | 每次创建新 Tab |
|
||||
|
||||
实例策略是应用注册数据,不能分散硬编码在多个点击处理器中。
|
||||
|
||||
### 7.4 Tab 操作
|
||||
|
||||
- 点击或使用方向键切换实例不会暂停或终止后台实例。
|
||||
- 支持左右方向键、Home、End 和 roving `tabindex`。
|
||||
- 关闭非活动 Tab 后焦点保持在当前活动 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 输出。
|
||||
- 中文输入法、组合字符和常规键盘快捷键。
|
||||
- 复制、粘贴、全选、搜索、清屏和滚动。
|
||||
- 工作栏宽度或窗口尺寸变化后的 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'
|
||||
instancePolicy: 'single' | 'multiple'
|
||||
}
|
||||
|
||||
type WorkbarTabInstance = {
|
||||
id: string
|
||||
appId: WorkbarAppDefinition['id']
|
||||
title: string
|
||||
targetRef?: {
|
||||
type: 'project' | 'local'
|
||||
id?: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 持久化工作栏打开状态、Tab 顺序、活动实例、应用 ID、自定义标题、公开目标引用和宽度比例。
|
||||
- 不持久化终端正文、未执行输入、凭据、环境变量、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] 连续选择终端会创建多个互相独立的终端 Tab。
|
||||
- [ ] 关闭、切换和重排实例后,“+”仍保持可用。
|
||||
- [ ] Tab 溢出时单行横向滚动,不隐藏“+”或把 Tab 换成下拉框。
|
||||
- [x] 关闭活动 Tab 后激活正确的相邻 Tab 并恢复焦点。
|
||||
- [x] 当前 Tab 的背景、边框、顶部指示和字重清晰可辨;关闭按钮仅在悬停、焦点或无悬停
|
||||
设备上出现,且不引发布局位移。
|
||||
- [ ] 升级后现有任务中心、工作区、浏览器和成果仍按默认布局打开且行为不回归。
|
||||
- [ ] 在所有主要页面都能打开工作栏;切换主要页面不会销毁 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、“+”、选择页和关闭确认使用正确语义并有可见焦点。
|
||||
- [ ] 终端输出不逐行进入 live region,关键状态变化可被辅助技术感知。
|
||||
- [ ] 浅色、深色、200% 文字缩放和窄窗口下可以完成核心流程。
|
||||
- [ ] Windows、macOS、Linux x64 和 Linux arm64 的正式构建实际启动本机 PTY。
|
||||
- [ ] 远程链路在共享 Linux x64 真实 Host 上完成创建、输入、resize、断线、重连、关闭和
|
||||
应用退出验证;若 Host 不可达,远程终端不得标记为完成。
|
||||
|
||||
## 19. 验证计划
|
||||
|
||||
### 19.1 自动验证
|
||||
|
||||
- 共享 Zod Schema 和边界值测试。
|
||||
- 单/多实例注册策略、Tab 顺序、关闭焦点和“+”选择页组件测试。
|
||||
- 本机 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。
|
||||
- [ ] 重启后不自动重放结果未知的副作用步骤。
|
||||
- [ ] 后台任务排队时不挤占前台模型请求。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 电脑控制
|
||||
|
||||
电脑控制包含 GoodBuddy 对浏览器和客户端桌面能力的产品实现与平台适配状态。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [功能进度](./progress.md) | 当前已实现能力、缺陷修复、平台限制和验证状态 |
|
||||
|
||||
当前只有进度文档;产品需求、功能逻辑、UI 和技术设计尚未从历史实现记录中独立整理。
|
||||
+35
-14
@@ -6,9 +6,9 @@
|
||||
| --- | --- |
|
||||
| 文档类型 | 实施进度 |
|
||||
| 状态 | 持续更新 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-07 |
|
||||
| 适用能力 | 电脑控制与托管浏览器 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-18 |
|
||||
| 适用能力 | 内置浏览器与客户端电脑控制 |
|
||||
|
||||
## 范围
|
||||
|
||||
@@ -28,8 +28,8 @@ Linux x64 和 Linux arm64。
|
||||
导航及重定向仍经过 DNS 解析和目标地址固定。
|
||||
- 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、
|
||||
空闲回收和应用退出清理。
|
||||
- 直连模型工具循环已提高到适合浏览器任务的有界上限,并包含重复调用和
|
||||
无进展保护。
|
||||
- 直连模型工具循环不设置工具次数或模型轮次配额;取消、请求超时、工具结果大小和
|
||||
累计输出边界继续防止失控执行。
|
||||
|
||||
### MCP
|
||||
|
||||
@@ -60,10 +60,14 @@ Linux x64 和 Linux arm64。
|
||||
### 设置与安全边界
|
||||
|
||||
- 能力存储已迁移到版本 2。
|
||||
- 已增加电脑控制能力卡片、诊断、托管浏览器配置元数据、IPC 和 preload
|
||||
契约。
|
||||
- 电脑控制能力默认关闭;启用、配置变更和本地数据清除会替换或清理相关
|
||||
Runtime 和会话。
|
||||
- 内置浏览器已归入“直连模型”工具,与联网搜索并列,并明确说明不会控制
|
||||
客户端已安装的浏览器;原有开关和配置 ID 保持不变,用户状态无需迁移。
|
||||
- 用户通过内置浏览器总开关决定是否向直连模型提供工具;开启后可在 Execute
|
||||
模式直接使用,不逐次询问。诊断位于同一分类。
|
||||
- 尚未实际参与浏览器执行的命名配置已从界面隐藏;底层旧数据和兼容接口继续
|
||||
保留,不删除用户已有记录。
|
||||
- “电脑控制”只显示实际操作客户端电脑的能力。能力启用、配置变更和本地
|
||||
数据清除仍会替换或清理相关 Runtime 和会话。
|
||||
|
||||
## 本轮已修复缺陷
|
||||
|
||||
@@ -89,11 +93,27 @@ Linux x64 和 Linux arm64。
|
||||
新增“浏览器”页签;活动对话启动浏览器时会自动打开该页签,并显示创建中、
|
||||
加载中、操作中、用户交互中、就绪、失败和已停止状态。用户可点击“交互”打开
|
||||
同一会话的子窗口辅助 Agent;交互时主窗口暂时禁用,关闭时先刷新最终画面再将
|
||||
浏览器窗口最小化,页面和会话继续保留。用户也可立即停止当前对话的浏览器会话。
|
||||
|
||||
浏览器窗口最小化,页面和会话继续保留。用户也可明确关闭当前对话的浏览器会话。
|
||||
当前实现按导航、快照、点击、输入、选择、返回和截图操作刷新画面,而不是创建
|
||||
第二个预览浏览器,因此显示内容与模型受控页面一致。
|
||||
|
||||
### 已实现:浏览器工具栏
|
||||
|
||||
浏览器 Tab 顶部已增加“返回、刷新/停止加载、地址输入、前往、交互、关闭浏览器”工具栏。
|
||||
它继续操作 Agent 在当前 Conversation 中使用的同一浏览器会话;用户导航会有意改变 Agent
|
||||
下一步看到的页面。前往、返回和刷新通过 `BrowserService` 与 Agent 浏览器操作串行执行,
|
||||
不会创建第二套 Renderer 导航路径。
|
||||
|
||||
工具栏状态以浏览器提交后的权威 URL、是否加载中、能否返回和会话活动状态为准;地址输入
|
||||
在用户编辑期间保留本地草稿,不被状态刷新覆盖;提交未带 HTTP(S) 协议的裸域名时会自动添加
|
||||
`http://`。创建中、Agent 操作中和完整交互窗口期间禁用其余工具栏控件,但活动导航仍可用
|
||||
“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也不释放 Runtime 上下文;原“停止
|
||||
浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在视觉和文案上与停止加载清楚区分。
|
||||
|
||||
活动 Conversation 尚无浏览器会话时,可直接从地址栏输入 URL 并“前往”,通过现有浏览器
|
||||
能力检查和同一 URL 策略创建会话。实现复用现有每 Conversation 串行队列和有界状态,不引入
|
||||
恢复、版本化或第二套会话框架。
|
||||
|
||||
### P0:百度输入时元素引用失效,已修复
|
||||
|
||||
截图中的“浏览器元素引用已失效,请重新获取快照”来自过宽的导航监听:广告或
|
||||
@@ -133,10 +153,11 @@ Linux x64 和 Linux arm64。
|
||||
区分代理、分区、窗口、CDP 初始化、DNS 或页面导航失败。需要增加有界、脱敏的
|
||||
阶段错误码,并保留用户可执行的修复建议。
|
||||
|
||||
### P1:命名浏览器配置尚未用于执行
|
||||
### P1:命名浏览器配置尚未用于执行,界面已隐藏
|
||||
|
||||
设置中的命名浏览器配置当前只保存未来托管隔离所需的元数据。实际执行仍使用
|
||||
每个会话随机创建的临时分区,不复用登录状态。
|
||||
命名浏览器配置当前只保存未来托管隔离所需的元数据,实际执行仍使用每个会话
|
||||
随机创建的临时分区,不复用登录状态。为避免用户误以为该配置已经生效,设置
|
||||
界面暂不显示创建、选择、重命名或删除入口;底层数据与兼容接口继续保留。
|
||||
|
||||
## 下一步顺序
|
||||
|
||||
@@ -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,11 @@
|
||||
# 会话监督
|
||||
|
||||
会话监督定义对长会话、工具执行和异常状态的观察、判断与介入边界。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [会话监督 PRD](./prd.md) | 监督目标、证据、介入方式和产品验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
+22
-16
@@ -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. 用户操作进入任务审计。
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# DeepSeek Harness Runtime
|
||||
|
||||
本功能定义 DeepSeek Harness 作为 GoodBuddy 本机 Agent Runtime 的产品接入、控制面、
|
||||
插件市场、权限和打包边界。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | Runtime 产品与技术方案、协议、插件、打包、测试和验收 |
|
||||
|
||||
现有文档同时包含部分 PRD、功能逻辑和进度内容,后续修改该 Runtime 时再按标准职责拆分。
|
||||
+26
-17
@@ -7,7 +7,7 @@
|
||||
| 文档状态 | 实现与发布验收基线 |
|
||||
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
|
||||
| Runtime 标识 | `deepseek-harness` |
|
||||
| 首版依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.6` |
|
||||
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.8` |
|
||||
| 上游状态 | Developer Preview,允许出现破坏性变更 |
|
||||
| 上游许可证 | MIT |
|
||||
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
@@ -163,7 +163,7 @@ Electron utilityProcess
|
||||
├─ 本地 Shell / Filesystem Provider
|
||||
├─ 最小工具集与 Main 代理 MCP
|
||||
└─ Main 明确启用的第三方 Cordis 插件
|
||||
│ HTTPS
|
||||
│ HTTP/HTTPS
|
||||
▼
|
||||
用户选择的 OpenAI 兼容模型连接
|
||||
```
|
||||
@@ -219,6 +219,10 @@ GoodBuddy 控制面自身不导出 `apply(ctx, config)`,也不提供默认 std
|
||||
- 插件成功激活后可注册工具或后台生命周期逻辑。Ask 只能拦截模型工具调用,不能撤销初始化阶段已经发生的副作用。
|
||||
|
||||
GoodBuddy 不扫描任意目录、不读取用户 profile 插件清单,也不接受 Renderer 直接提供文件路径。
|
||||
插件安装、升级和移除在目录重命名前写入受管变更日志。Main 下次初始化时以持久
|
||||
Store 是否已经提交为准,确定性完成新目录或恢复旧目录,并在处理前重新验证受管
|
||||
目录、入口真实路径、符号链接和根目录包含关系。旧版 `store.json` 继续原地迁移,
|
||||
不要求用户重新安装插件。
|
||||
|
||||
## 8. 协议设计
|
||||
|
||||
@@ -321,12 +325,13 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
|
||||
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
|
||||
|
||||
### 9.3 取消与超时
|
||||
### 9.3 取消与有界控制操作
|
||||
|
||||
- 用户取消时立即发送 `session/cancel`。
|
||||
- 取消等待有界,超时后关闭连接并终止整个 Harness 进程。
|
||||
- 初始化、握手、Session 创建、Prompt、权限回传和关闭分别使用独立超时。
|
||||
- Prompt 超时与用户取消使用不同错误类型,不能被宽泛 catch 抹平。
|
||||
- 初始化、握手、请求准备、权限回传和关闭分别使用独立控制超时。
|
||||
- 生产 Prompt 不设置固定墙钟总时限,由 Harness 自身协议终态或用户取消结束。测试可以
|
||||
显式注入短 Prompt 时限验证取消传播,超时错误与用户取消仍不能被宽泛 catch 抹平。
|
||||
- 取消后仍可接收并丢弃该请求的最终协议结算帧,但不得写入下一请求。
|
||||
|
||||
### 9.4 释放与退出
|
||||
@@ -422,13 +427,13 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
|
||||
- 协议必须是 `openai-chat-completions`。
|
||||
- 认证必须是 API Key。
|
||||
- 公网服务地址必须使用 HTTPS;`localhost`、`127.0.0.1` 和 `::1` 回环地址可以使用 HTTP。
|
||||
- 服务地址可以使用自定义主机、端口和部署路径,但不得包含用户名、密码、查询参数或片段。
|
||||
- 服务地址支持 HTTP 和 HTTPS,不根据主机位置、端口、凭据、查询参数或片段增加额外限制。
|
||||
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
|
||||
- 模型名称和服务地址由 Main 传入受控 Host。
|
||||
- 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。
|
||||
- API Key 继续保存在 GoodBuddy 加密设置中。
|
||||
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
|
||||
- 未显式指定 Harness 模型且没有可用的管理员预置连接时,Main 优先使用当前默认的兼容 GoodBuddy 模型连接;默认连接不兼容时使用首个兼容连接。只有不存在任何兼容连接时才提示用户前往模型或 Runtime 设置。
|
||||
|
||||
不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。
|
||||
|
||||
@@ -442,7 +447,7 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
- 文本模型收到图片时必须在启动 Host 或发起模型网络调用前返回明确错误,不能静默丢弃图片。
|
||||
- 图片模型只接受内联 JPEG/PNG,不接受 URL、文件路径、ACP `uri` 或其他媒体类型。
|
||||
- Main 已通过 `nativeImage` 解码用户选择的图片并生成有界模型输入;Utility 仍须独立执行严格 base64、签名、容器结构、CRC(PNG)、完整解码、尺寸和摘要校验,不能把 Main 校验当作跨进程信任替代。
|
||||
- 每条消息最多 8 张图,单图编码后最多 1 MiB,图片合计最多 2 MiB,单图最多 1,600 万像素,累计解码像素最多 3,200 万(重复引用也计入预算)。进程内 Store 另设 32 MiB、256 个唯一对象的总上限。
|
||||
- 每条消息最多 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 配置表达式或模块名。
|
||||
@@ -455,7 +460,7 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
| --- | --- |
|
||||
| 单个 JSON-RPC 帧 | 8 MiB |
|
||||
| 单图 / 单条消息图片 | 1 MiB / 8 张且合计 2 MiB |
|
||||
| 单图 / 单条消息解码像素 | 1,600 万 / 3,200 万 |
|
||||
| 单图尺寸 / 单条消息解码像素 | 单边最长 8,192 px 且 1,600 万像素 / 3,200 万像素 |
|
||||
| Host 临时图片存储 | 32 MiB 且最多 256 个唯一对象 |
|
||||
| 单个文本或推理事件 | 64 KiB |
|
||||
| 单次请求累计协议输出 | 4 MiB |
|
||||
@@ -465,7 +470,7 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
| stderr 累计 | 64 KiB |
|
||||
| Host 启动 | 10 秒基础预算 + 每插件 5 秒激活与最多 1 秒失败清理,插件序列最多 91 秒;Main 另预留 2 秒持久化失败状态 |
|
||||
| ACP 初始化与内部握手 | 每阶段 10 秒 |
|
||||
| 单次 Prompt | 10 分钟 |
|
||||
| 单次 Prompt | 无固定墙钟总时限,由 Harness 终态或用户取消结束 |
|
||||
| 有序关闭宽限期 | 2 秒 |
|
||||
|
||||
超过限制时应取消当前请求。协议帧、队列或 stderr 持续异常时,应终止 Runtime 进程,避免继续信任已失控的通道。
|
||||
@@ -493,7 +498,7 @@ Runtime: GoodBuddy 内置 DeepSeek Harness
|
||||
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
|
||||
状态: 已就绪
|
||||
路径: <受控 Host 路径>
|
||||
版本: 0.1.0-rc.6
|
||||
版本: 0.1.0-rc.8
|
||||
执行权限: 当前用户权限
|
||||
|
||||
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
|
||||
@@ -523,13 +528,17 @@ OpenCode、Continue 和 DeepSeek Harness 的后续能力按操作生命周期放
|
||||
| 表面 | 负责内容 | 不负责内容 |
|
||||
| --- | --- | --- |
|
||||
| Composer 通用行 | 附件、语音、知识范围、专家、Ask/Execute、Runtime 和发送 | Session 监督、后台进度、历史任务管理 |
|
||||
| Composer Runtime 专属行 | 仅对当前消息生效且需要高频选择的 Agent、预设、Prompt/Command 快捷操作 | Subagent 树、后台 Job、Workflow/Hook 生命周期 |
|
||||
| 右侧助手工作栏的未来“Runtime”页签 | 当前会话的 Runtime 状态、Subagent 层级与取消、后台 Job 队列/进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止和会话监督 | 持久模型、程序路径、默认 Agent/预设配置 |
|
||||
| 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 或会话时,侧栏必须明确更新归属,不能把上一 Runtime 的 Job/Subagent 状态留在当前会话中。
|
||||
Runtime 栏目入口始终存在,并采用统一监督模型;内部再按用户所选目标及其 Runtime 的真实能力
|
||||
显示 OpenCode、Continue 或 DSH 的具体区域。未支持能力不渲染空卡片或一排禁用按钮,而是
|
||||
在用户需要理解缺口时显示原因和可执行入口。跟随模式切换 Runtime、Conversation 或 Task
|
||||
时必须清理上一归属的聚合执行状态,固定目标则保持不变。完整工作栏契约见
|
||||
[通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)。
|
||||
|
||||
所有未来的 Subagent、Job、Workflow、Hook 和会话操作仍须经过 Main 的 Runtime 边界,保留取消、超时、权限、父子任务关系、用量和活动审计。高风险动作在侧栏就地确认,运行结果进入活动与成果记录,不以 Composer 按钮代替监督面板。DeepSeek Harness 首版仍不加载这些服务,本节只确定未来跨 Runtime 的产品位置和协议归属。
|
||||
所有未来的 Subagent、Job、Workflow、Hook 和会话操作仍须经过 Main 的 Runtime 边界,保留取消、超时、权限、Task/Job/Subjob 层级、用量和活动审计。高风险动作在侧栏就地确认,运行结果进入活动与成果记录,不以 Composer 按钮代替监督面板。DeepSeek Harness 首版仍不加载这些服务,本节只确定未来跨 Runtime 的产品位置和协议归属。
|
||||
|
||||
## 15. IPC 与共享契约
|
||||
|
||||
@@ -728,7 +737,7 @@ npm run build
|
||||
- Harness 文件和命令工具没有 Runtime OS 隔离,会继承 GoodBuddy 客户端当前用户能够访问的主机资源。
|
||||
- 首版不恢复 Harness 原生 Session,Runtime 重启后由 GoodBuddy 历史重建。
|
||||
- 图片输入仅在所选模型连接明确声明支持时可用;首版仍不支持知识库、浏览器控制和 Harness Subagent。Web Search/Fetch 仅使用 Main 代理,MCP 仅支持用户分配、Main 代理和 Execute 自动单次授权路径。
|
||||
- Harness Subagent、后台 Job、Workflow、Hook 和原生会话监督尚未实现;未来入口固定在右侧 Runtime 监督栏,不扩张 Composer 工具栏。
|
||||
- Harness Subagent、后台 Job、Workflow、Hook 和原生会话监督尚未实现;未来按 Task 聚合到右侧 Runtime 监督栏,不扩张 Composer 工具栏或暴露 Job/Run 层级。
|
||||
- 推理、工具和用量扩展属于 GoodBuddy 协议,不是标准 ACP 保证。
|
||||
- 市场来自公共 npm 关键字搜索,不是精选目录;包的质量、兼容性和维护状态由发布者负责。
|
||||
- 插件安装、初始化、后台生命周期和 Execute 工具使用当前用户权限,不受 Runtime OS 沙箱保护;Ask 只控制模型工具调用。
|
||||
@@ -767,4 +776,4 @@ GoodBuddy 对该 Runtime 采用内部维护策略:
|
||||
|
||||
未采用。Main 无法可靠观察 Cordis 内部 Session、Tool、Usage 和权限 seam,只能得到不完整的外部进程行为。
|
||||
|
||||
当前选择让双层内部控制面保持 GoodBuddy 私有,同时允许 Main 从受管 Store 向固定 Host 注入标准 Cordis 插件;插件扩展面不会取代 GoodBuddy 的可信 Main 控制权。
|
||||
当前选择让双层内部控制面保持 GoodBuddy 私有,同时允许 Main 从受管 Store 向固定 Host 注入标准 Cordis 插件;插件扩展面不会取代 GoodBuddy 的可信 Main 控制权。
|
||||
@@ -0,0 +1,48 @@
|
||||
# 直连模型 Agent 能力
|
||||
|
||||
本目录是 GoodBuddy“直连模型 Agent 能力”的唯一文档入口。本功能为直连文本模型补齐
|
||||
跨平台进程执行和编程 Subagent,使其能够在 Execute 模式完成读取、修改、运行和验证组成的
|
||||
编程闭环,同时避免向已经拥有原生 Shell 或 Agent 能力的 Runtime 重复注入工具。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、产品范围、功能需求、非目标和产品验收 |
|
||||
| [User Stories](./user-stories.md) | 本机开发、失败恢复、委派和 Runtime 边界场景 |
|
||||
| [功能逻辑](./logic-design.md) | 能力矩阵、不变量、状态转换、继承规则和失败结果 |
|
||||
| [UI 设计](./ui-design.md) | 设置能力目录、聊天活动、Subagent 状态和无障碍行为 |
|
||||
| [技术设计](./technical-design.md) | Main 服务、工具契约、平台适配、Subagent、生命周期和测试 |
|
||||
| [功能进度](./progress.md) | 当前实现事实、待实施阶段、阻塞项和验证证据 |
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| 直连模型 | Runtime provider 为 `model`、由 GoodBuddy 直接调用模型协议的文本模型 |
|
||||
| 进程执行 | GoodBuddy 在当前执行空间启动一个前台 Shell 命令并返回有界结果 |
|
||||
| 平台 Shell | Windows 上的 PowerShell,macOS/Linux 上的 Bash 或 POSIX Sh |
|
||||
| 编程 Subagent | 由直连模型临时委派、使用直连模型 Runtime 执行一个有界子任务的执行者 |
|
||||
| 父请求 | 发起进程调用或 Subagent 委派的当前直连模型请求 |
|
||||
| 子任务 | 父请求内部的一项委派工作,对应 Job/Subjob 语义,不是新的顶层 Task |
|
||||
|
||||
Task、Job、Subjob、Run 和 Subagent 的对象关系以
|
||||
[Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) 为准。本功能不重新定义
|
||||
这些对象。
|
||||
|
||||
## 功能边界
|
||||
|
||||
- `process_execute` 和 `subagent_delegate` 是 GoodBuddy 直连模型内置工具,不是外部 MCP。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 继续使用各自原生执行与委派能力。
|
||||
- Ask 不允许进程执行;Subagent 若在 Ask 中使用,只能继承 Ask 的只读能力。
|
||||
- Execute 表示用户授权当前执行空间账号的完整能力,不增加第二套工具审批或权限档位。
|
||||
- 首版跨平台指桌面本机 Windows、macOS 和 Linux。托管 SSH 项目当前继续使用远端
|
||||
OpenCode;未来若向远端直连模型开放本工具,命令必须由 Host Agent 执行,绝不能回退到
|
||||
桌面本机。
|
||||
|
||||
## 相关功能
|
||||
|
||||
- [本机工具环境](../local-tool-environment/README.md):为新进程提供 Node、Python 和 PATH。
|
||||
- [助手工作栏与执行空间](../assistant-workbar/prd.md):定义本机/SSH 执行位置和活动归属。
|
||||
- [远程主机与远程执行](../remote-host/README.md):定义当前托管 SSH Runtime 边界。
|
||||
- [DeepSeek Harness Runtime](../deepseek-harness/README.md):已经拥有平台 Shell 的独立 Runtime。
|
||||
@@ -0,0 +1,173 @@
|
||||
# 直连模型 Agent 能力功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-01 |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 关联故事 | [User Stories](./user-stories.md) |
|
||||
|
||||
## 1. 权威不变量
|
||||
|
||||
1. `process_execute` 和 `subagent_delegate` 只属于直连文本模型。
|
||||
2. Ask 永远不能启动命令、写入工作区或创建 Execute Subagent。
|
||||
3. 子级的项目、执行空间、工作模式、模型连接和能力范围只能继承或缩小。
|
||||
4. 子级工具清单不包含 `subagent_delegate`,最大深度固定为 1。
|
||||
5. 命令必须在父请求绑定的执行空间运行,禁止跨设备静默回退。
|
||||
6. 一个 `process_execute` 调用只拥有自己创建的前台进程树,不能管理无关进程。
|
||||
7. 非零退出码是命令结果;启动失败、输入无效和后端不可用才是工具失败。
|
||||
8. 子任务是父请求内部 Job/Subjob,不创建顶层 Task 或独立 Conversation。
|
||||
9. 父请求取消、Runtime 释放或应用退出会传播到全部活动子级和进程。
|
||||
10. OpenCode、Continue 和 DeepSeek Harness 的原生能力不被本功能替换或包装。
|
||||
|
||||
## 2. 状态维度
|
||||
|
||||
| 维度 | 值 |
|
||||
| --- | --- |
|
||||
| Runtime | `model`、`opencode`、`continue`、`deepseek-harness` |
|
||||
| 工作模式 | `ask`、`execute` |
|
||||
| 执行空间 | `local`、`ssh` |
|
||||
| 本机 Shell | `available`、`unavailable` |
|
||||
| Subagent 深度 | `0`、`1` |
|
||||
| 父请求 | `active`、`cancelling`、`terminal` |
|
||||
|
||||
## 3. 工具可见性决策表
|
||||
|
||||
| Runtime | 模式 | 执行空间 | `process_execute` | `subagent_delegate` |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `model` | Ask | 本机 | 隐藏 | 提供,只读继承 |
|
||||
| `model` | Execute | 本机且 Shell 可用 | 提供 | 提供,Execute 继承 |
|
||||
| `model` | Execute | 本机但 Shell 不可用 | 隐藏并报告诊断 | 提供,但子级同样没有进程工具 |
|
||||
| `model` | Ask/Execute | SSH,远端后端未实现 | 隐藏 | 当前远端直连模型产品入口不开放 |
|
||||
| 其他 Runtime | 任意 | 任意 | 隐藏 | 隐藏 |
|
||||
|
||||
工具清单隐藏不是最终授权边界。`callTool` 仍须重新验证 Runtime、工作模式、执行空间和父请求
|
||||
是否活动,以拒绝旧快照或伪造调用。
|
||||
|
||||
## 4. 进程状态机
|
||||
|
||||
```text
|
||||
validating
|
||||
├─ invalid → failed
|
||||
└─ valid → starting
|
||||
├─ spawn_failed → failed
|
||||
└─ running
|
||||
├─ exit 0 → completed
|
||||
├─ exit nonzero → completed_nonzero
|
||||
├─ timeout → terminating → timed_out
|
||||
├─ parent cancel → terminating → cancelled
|
||||
└─ output/lifecycle failure → terminating → failed
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `completed_nonzero` 仍产生正常工具结果。
|
||||
- 超时和取消结果包含已经取得的有界输出。
|
||||
- 进入 `terminating` 后不再接受新输入,也不启动替代命令。
|
||||
- 终止只针对本次调用记录的进程树。
|
||||
- 迟到的 `close` 只用于清理,不得覆盖已提交的取消或超时原因。
|
||||
|
||||
## 5. 工作目录决策
|
||||
|
||||
| 输入 | 结果 |
|
||||
| --- | --- |
|
||||
| 未提供 | 当前工作区根目录 |
|
||||
| `.` 或合法相对子目录 | 解析到工作区内目录 |
|
||||
| 绝对路径 | 拒绝 |
|
||||
| `..` 越界、符号链接越界 | 拒绝 |
|
||||
| 不存在或不是目录 | 拒绝 |
|
||||
| 当前工作区身份在验证期间变化 | 拒绝本次调用 |
|
||||
|
||||
首版进程工具比 Execute 的 OS 权限范围更窄地固定工作目录输入,只是为了保持请求与项目
|
||||
一致;命令本身仍以当前用户权限运行,可以显式访问该账号有权访问的其他绝对路径。该规则
|
||||
不是 Runtime OS 沙箱,也不能宣称文件系统 containment。
|
||||
|
||||
## 6. Shell 选择
|
||||
|
||||
| 平台 | 顺序 | 不可用结果 |
|
||||
| --- | --- | --- |
|
||||
| Windows | `pwsh` → `powershell.exe` | 进程工具不注册,设置诊断显示 PowerShell 不可用 |
|
||||
| macOS | `/bin/bash` → `/bin/sh` | 两者都不可用时不注册 |
|
||||
| Linux | `/bin/bash` → `/bin/sh` | 两者都不可用时不注册 |
|
||||
|
||||
一次父请求冻结实际 Shell 描述。PATH 或工具环境设置变化只影响新创建的 Runtime/执行服务,
|
||||
不在运行中的命令或 Subagent 中途切换。
|
||||
|
||||
## 7. Subagent 状态机
|
||||
|
||||
```text
|
||||
validating
|
||||
├─ invalid/depth_exceeded → failed
|
||||
└─ queued
|
||||
├─ parent_cancelled → cancelled
|
||||
└─ running
|
||||
├─ model done → completed
|
||||
├─ child failure → failed_with_partial_output
|
||||
├─ timeout → cancelled
|
||||
└─ parent cancel → cancelled
|
||||
```
|
||||
|
||||
父模型收到结构化终态:
|
||||
|
||||
```ts
|
||||
type SubagentDelegateResult = {
|
||||
status: 'completed' | 'failed' | 'cancelled'
|
||||
output: string
|
||||
error?: string
|
||||
modelUsage?: {
|
||||
inputTokens?: number
|
||||
outputTokens?: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败和取消是父模型可以解释的子任务结果。只有契约损坏、未知子任务身份或服务已经释放等
|
||||
内部不变量失败才终止父工具循环。
|
||||
|
||||
## 8. 权限与能力继承
|
||||
|
||||
| 父请求 | 子级模式 | 子级能力 |
|
||||
| --- | --- | --- |
|
||||
| Ask | Ask | 父级只读工具的子集,不含进程、写入和再次委派 |
|
||||
| Execute | Execute | 父级已启用工具的子集,包含本机进程能力,不含再次委派 |
|
||||
|
||||
- 子级不能请求更高模式。
|
||||
- 子级使用父请求开始时的工具与能力快照;运行中设置变化不重写快照。
|
||||
- 子级调用 MCP、浏览器或知识能力时继续经过这些能力已有的作用域和生命周期边界。
|
||||
- Execute 子级不弹出额外的“Subagent 执行授权”,也不获得父级没有的 MCP 分配。
|
||||
|
||||
## 9. 并发和顺序
|
||||
|
||||
- 全局编程 Subagent 最大并发为 3,额外请求进入有界队列。
|
||||
- 单个父模型的工具调用仍按直连模型现有顺序结算,不为本功能改写全部工具并发语义。
|
||||
- 不同父请求的 Subagent 可以并发;共享工作区的并发写入属于用户已授权的 Execute 行为,
|
||||
活动记录必须保留实际顺序和结果。
|
||||
- 首版不自动创建工作树、锁文件或回滚层。出现真实冲突时由父模型读取当前工作区事实并处理。
|
||||
|
||||
## 10. 结果优先级
|
||||
|
||||
当多个终止原因竞争时,使用以下优先级:
|
||||
|
||||
```text
|
||||
用户/父请求取消 > 工具超时 > 输出或协议边界 > 进程退出 > 普通工具错误
|
||||
```
|
||||
|
||||
已经提交的正常终态不被稍后到达的取消覆盖。父请求进入终态后,迟到的子级文本和工具事件
|
||||
只用于资源清理,不再写入会话。
|
||||
|
||||
## 11. 逻辑完整性评估
|
||||
|
||||
| 领域 | 结论 |
|
||||
| --- | --- |
|
||||
| Runtime 隔离 | 完整,工具只注入 `model` |
|
||||
| Ask/Execute | 完整,清单过滤和调用边界双重检查 |
|
||||
| 本机三平台 | 完整,统一契约和平台 Shell 决策已定义 |
|
||||
| Subagent 递归 | 完整,深度固定为 1 |
|
||||
| 取消与失败 | 完整,父子和进程传播及优先级已定义 |
|
||||
| 产品对象 | 完整,遵循 Task/Job 权威模型 |
|
||||
| SSH 远端执行 | 明确不在首版;未来必须增加 Agent 后端后才能开放 |
|
||||
|
||||
当前没有需要在实施前由用户补充的产品决策。
|
||||
@@ -0,0 +1,185 @@
|
||||
# 直连模型 Agent 能力 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台 CI 待验证 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-01 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 关联领域 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 直连模型已经可以读取、列出和写入工作区文本,使用浏览器、联网搜索、知识库及
|
||||
分配的 MCP,但没有内置命令执行工具。模型可以生成代码,却不能运行构建、测试、格式化、
|
||||
Git 或项目脚本,因此无法独立验证结果。
|
||||
|
||||
GoodBuddy 也已经有面向用户专家协作的只读 Subagent,但直连模型不能在自己的工具循环中
|
||||
主动委派一个聚焦的编程子任务。OpenCode、Continue 和 DeepSeek Harness 一般已经提供
|
||||
Shell、Agent 或 Task 能力,再向它们注入 GoodBuddy 同名工具只会造成重复入口和语义冲突。
|
||||
|
||||
本功能只补齐直连模型缺少的基础 Agent 能力:
|
||||
|
||||
1. 一个按平台适配的进程执行接口。
|
||||
2. 一个单层、继承父请求范围的编程 Subagent 接口。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
1. 让直连模型在 Execute 模式完成“读取代码 → 修改 → 运行 → 根据结果修正”的闭环。
|
||||
2. 在 Windows、macOS 和 Linux 使用同一个模型工具契约,不要求模型调用平台专属工具名。
|
||||
3. 让直连模型可以把独立工作委派给临时 Subagent,并取得有界、可核验的结果。
|
||||
4. 保持 Ask 只读、Execute 完整授权、执行空间、取消、有界输出和进程回收语义不变。
|
||||
5. 不向已有原生执行能力的 Agent Runtime 重复注入 GoodBuddy 进程或 Subagent 工具。
|
||||
|
||||
## 3. 核心产品决策
|
||||
|
||||
### 3.1 统一工具名,平台适配留在 Main
|
||||
|
||||
模型只看到 `process_execute`。GoodBuddy 根据当前平台选择 PowerShell、Bash 或 Sh,并在
|
||||
工具描述和结果中明确实际 Shell。模型不需要在 Windows 调用 `powershell_execute`、在
|
||||
macOS/Linux 改用 `bash_execute`。
|
||||
|
||||
### 3.2 它是内置能力,不依赖 MCP
|
||||
|
||||
进程生命周期、工作目录、取消和执行空间由 GoodBuddy 管理。用户仍可以配置提供其他命令
|
||||
能力的 MCP,但直连模型的基本编程能力不依赖第三方 MCP 是否安装或在线。
|
||||
|
||||
### 3.3 Runtime 能力不重复注入
|
||||
|
||||
| Runtime | GoodBuddy `process_execute` | GoodBuddy `subagent_delegate` |
|
||||
| --- | --- | --- |
|
||||
| 直连模型 `model` | Execute 提供 | Ask/Execute 提供 |
|
||||
| OpenCode | 不提供 | 不提供 |
|
||||
| Continue | 不提供 | 不提供 |
|
||||
| DeepSeek Harness | 不提供 | 不提供 |
|
||||
|
||||
“不提供”只表示不注入本功能工具,不限制 Runtime 自己的 Bash、PowerShell、Task、
|
||||
Agent 或插件能力。
|
||||
|
||||
### 3.4 Subagent 继承,不扩权
|
||||
|
||||
Subagent 使用父请求的项目、执行空间、工作模式、直连模型连接和已启用能力快照:
|
||||
|
||||
- Ask 父请求只能创建 Ask Subagent。
|
||||
- Execute 父请求创建 Execute Subagent。
|
||||
- 子级不能切换项目、目录、Runtime、模型连接或工作模式。
|
||||
- 子级不再看到 `subagent_delegate`,首版最大委派深度固定为 1。
|
||||
- 子任务不是新的顶层 Task 或 Conversation,结果先返回父模型。
|
||||
|
||||
### 3.5 不新增权限档位
|
||||
|
||||
Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完整授权。进程和 Subagent 不增加
|
||||
“受控执行”“仅测试”“可信命令”或逐命令风险等级,也不重复弹出第二套确认。
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
### 4.1 包含
|
||||
|
||||
- 本机 Windows、macOS、Linux 的前台 Shell 命令执行。
|
||||
- 当前工作区默认工作目录及合法子目录选择。
|
||||
- Exit Code、标准输出、标准错误、Shell、目录、耗时和截断状态。
|
||||
- 命令超时、用户取消、进程树停止和应用退出清理。
|
||||
- 新进程使用当前本机工具环境提供的 Node、Python 和 PATH。
|
||||
- 直连模型单层 Subagent 委派、排队、运行、部分结果、失败和取消。
|
||||
- 父子请求的工具、模型用量、活动和成果归属。
|
||||
- 桌面聊天和选择直连模型的消息通道项目使用相同 Runtime 能力矩阵。
|
||||
|
||||
### 4.2 不包含
|
||||
|
||||
- 交互式 PTY、终端复用、持续 stdin、密码提示或 TUI 自动化。
|
||||
- 后台守护进程、任意 PID 枚举、接管或停止用户已有进程。
|
||||
- Shell Session 持久化,环境变更不会跨 `process_execute` 调用保留。
|
||||
- 首版远程 SSH 直连模型进程执行。
|
||||
- 向 OpenCode、Continue 或 DeepSeek Harness 注入本功能工具。
|
||||
- 递归 Subagent、长期运行的自治 Agent、Workflow、Hook 或恢复未完成子任务。
|
||||
- 新的权限矩阵、逐命令批准、沙箱档位或命令白名单。
|
||||
- 自动安装项目依赖;模型可以在 Execute 中显式运行项目已有的安装命令。
|
||||
|
||||
## 5. 功能需求
|
||||
|
||||
### FR-1 直连模型专属可用性
|
||||
|
||||
- 两个工具只由 Runtime provider `model` 注册。
|
||||
- Agent Runtime 切换为 OpenCode、Continue 或 DeepSeek Harness 后不再显示这两个工具。
|
||||
- 图像生成连接不获得文本 Agent 工具。
|
||||
- 能力不可用时返回准确原因,不以其他 Runtime 或本机 Shell 静默代替。
|
||||
|
||||
### FR-2 跨平台进程执行
|
||||
|
||||
- 使用统一的 `process_execute` 工具契约。
|
||||
- Windows 选择 PowerShell;macOS/Linux 选择 Bash,Bash 不可用时可使用 POSIX Sh。
|
||||
- 每次调用启动一个前台进程树,完成后返回结构化结果。
|
||||
- 非零退出码是可观察的命令结果,不自动转成模型请求失败。
|
||||
- Shell 启动失败、非法参数或执行后端不可用才属于工具失败。
|
||||
|
||||
### FR-3 工作目录和执行位置
|
||||
|
||||
- 默认目录是父请求当前工作区。
|
||||
- 可选工作目录必须解析为当前工作区本身或其子目录。
|
||||
- 本机执行只能发生在当前桌面执行空间。
|
||||
- 当前请求绑定 SSH 执行空间且没有远端进程后端时不注册工具,绝不在桌面目录回退执行。
|
||||
|
||||
### FR-4 Ask/Execute
|
||||
|
||||
- Ask 工具清单不包含 `process_execute`。
|
||||
- Runtime 边界必须再次拒绝 Ask 发起的伪造命令调用。
|
||||
- Execute 命令使用 GoodBuddy 客户端当前用户权限,不声称 Runtime OS 沙箱。
|
||||
- 消息通道的直连模型 Execute 继续服从该通道既有工具策略。
|
||||
|
||||
### FR-5 Subagent 委派
|
||||
|
||||
- `subagent_delegate` 接受一个清晰的任务说明,不要求用户预先创建专家。
|
||||
- 子级默认使用父请求选择的直连模型连接。
|
||||
- 子级继承父请求的工作模式和能力上限,但不获得再次委派能力。
|
||||
- 子级最终输出、部分输出、错误、用量和状态返回父请求。
|
||||
- 父模型负责综合结果并向用户作最终回答。
|
||||
|
||||
### FR-6 Subagent 领域归属
|
||||
|
||||
- 子任务使用 Job/Subjob 语义,不创建 Task Center 条目或独立 Conversation。
|
||||
- UI 对象层级继续止于用户 Task;活动可以显示 Subagent 状态卡,但不显示 Job/Run 树。
|
||||
- 子任务产生的工具、成果和用量可追溯到父请求及子任务标识。
|
||||
|
||||
### FR-7 生命周期和资源边界
|
||||
|
||||
- 父请求取消必须传播到活动 Subagent 和其当前进程。
|
||||
- Runtime 替换、会话释放和应用退出必须停止 GoodBuddy 创建的活动子进程。
|
||||
- 命令、标准输出、标准错误、Subagent 提示、结果、队列和并发全部有界。
|
||||
- 截断必须明确标记,不能把不完整输出伪装为完整结果。
|
||||
|
||||
### FR-8 环境和凭据
|
||||
|
||||
- 新进程使用筛选后的当前用户环境和本机工具环境 PATH。
|
||||
- 模型 Provider 凭据、GoodBuddy 加密设置和无关桌面会话变量不进入命令环境。
|
||||
- 首版不接受模型提交任意环境变量映射;需要的普通变量由 Shell 命令在单次调用中设置。
|
||||
|
||||
### FR-9 状态与活动
|
||||
|
||||
- 进程活动显示等待、运行、成功、非零退出、超时、取消和失败。
|
||||
- Subagent 显示排队、运行、完成、失败和取消。
|
||||
- 命令和子任务失败保留可操作错误及已有部分输出。
|
||||
- 同一事件不同时显示页内错误和应用通知。
|
||||
|
||||
### FR-10 兼容
|
||||
|
||||
- 现有模型连接、MCP 分配、专家设置和 Runtime 选择无需迁移。
|
||||
- 不改变现有 Agent Runtime 的工具清单或权限语义。
|
||||
- 不修改系统 PATH、Shell Profile 或用户终端会话。
|
||||
|
||||
## 6. 产品验收
|
||||
|
||||
- [ ] Windows 直连模型 Execute 使用 PowerShell 创建文件、运行测试并读取退出码。
|
||||
- [ ] macOS/Linux 直连模型 Execute 使用 Bash/Sh 完成同一工作流。
|
||||
- [ ] Ask 中模型看不到进程工具,伪造调用也在 Main 边界失败。
|
||||
- [ ] 直连模型可以委派编程子任务,子级运行命令并把结果返回父模型。
|
||||
- [ ] Ask Subagent 保持只读;Execute Subagent 不增加第二套批准。
|
||||
- [ ] Subagent 不能递归委派,不能切换父请求工作区、模式或模型。
|
||||
- [ ] 切换到 OpenCode、Continue 或 DeepSeek Harness 后不出现本功能工具。
|
||||
- [ ] 非零退出、超时、取消和输出截断均准确展示。
|
||||
- [ ] 取消父请求和退出应用后没有残留 GoodBuddy 命令进程。
|
||||
- [ ] 本机工具环境选择对新命令生效,Main 持有的模型凭据不进入子进程。
|
||||
- [ ] 子任务不进入 Task Center,不创建独立可导航 Conversation。
|
||||
- [ ] 全量测试、类型检查、Lint 和生产构建通过。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 直连模型 Agent 能力功能进度
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档类型 | 功能进度 |
|
||||
| 状态 | 已实施,本机验收完成 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 关联入口 | [直连模型 Agent 能力](./README.md) |
|
||||
|
||||
## 当前结论
|
||||
|
||||
本机生产路径已经完成源码接线:直连模型 Execute 可运行平台 Shell,Ask/Execute 均可按
|
||||
父模式委派单层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 路径不注入
|
||||
这两个工具。Windows 本机、真实模型和全量项目验证已通过;macOS 与 Linux 真机验证仍需
|
||||
由对应平台完成。
|
||||
|
||||
## 已确认的当前基线
|
||||
|
||||
- [x] 直连模型当前内置工作区读、目录列表和文本写入,但没有进程工具。
|
||||
- [x] 内置 MCP 当前没有 Shell;自定义 MCP 不能替代开箱即用的进程能力。
|
||||
- [x] `ModelAgentRuntime` 已有多轮工具循环、取消、上下文压缩和有界结果。
|
||||
- [x] DeepSeek Harness 已按 Windows PowerShell、macOS/Linux Bash 注册原生 Shell。
|
||||
- [x] OpenCode 和 Continue 使用各自原生执行工具。
|
||||
- [x] 当前专家 Subagent 是只读协作流程,不是直连模型可调用的编程工具。
|
||||
- [x] Task/Job 领域文档已经明确 Subagent 不是新的顶层 Task 或 Conversation。
|
||||
- [x] 当前跨平台进程树清理已经覆盖 Windows `taskkill /T /F` 和 POSIX 进程组。
|
||||
|
||||
## 已完成设计
|
||||
|
||||
- [x] 工具只注入直连模型,不注入 Agent Runtime。
|
||||
- [x] 统一 `process_execute` 契约和三平台 Shell 决策。
|
||||
- [x] Ask/Execute、执行空间、工作目录和非零退出规则。
|
||||
- [x] 输出、超时、取消、环境和进程树边界。
|
||||
- [x] 单层 `subagent_delegate`、继承规则、并发和结果模型。
|
||||
- [x] 不创建 Child Task/独立 Conversation 的领域归属。
|
||||
- [x] 设置能力目录、聊天活动、错误和无障碍行为。
|
||||
- [x] 本机、真实模型和项目验证方案。
|
||||
|
||||
## 实施状态
|
||||
|
||||
### 阶段 1:进程服务
|
||||
|
||||
- [x] Shared 工具和 schema。
|
||||
- [x] 本机 Shell 探测与能力状态。
|
||||
- [x] 工作目录解析。
|
||||
- [x] 跨平台前台进程、输出边界、超时和取消。
|
||||
- [x] 当前筛选 PATH 和凭据环境过滤。
|
||||
|
||||
### 阶段 2:直连模型接入
|
||||
|
||||
- [x] `ModelToolProvider` 注册与调用。
|
||||
- [x] Ask/Execute、Runtime target 和本机执行空间双重检查。
|
||||
- [x] DeepSeek Harness、OpenCode 和 Continue 隔离。
|
||||
- [x] 通用工具活动和“设置 > MCP > 直连模型”编程能力目录。
|
||||
|
||||
### 阶段 3:编程 Subagent
|
||||
|
||||
- [x] 编程 Subagent 服务和同 Provider 深度过滤。
|
||||
- [x] 父子模式、模型、执行空间和能力继承。
|
||||
- [x] 深度、全局并发、队列、超时和取消。
|
||||
- [x] 新 Subagent actor 契约和旧专家事件兼容读取。
|
||||
- [x] 用量、部分输出、实时活动和终态归并。
|
||||
|
||||
### 阶段 4:端到端验收
|
||||
|
||||
- [x] Windows PowerShell 真实命令。
|
||||
- [ ] macOS Bash/Sh 真实命令。
|
||||
- [ ] Linux x64/arm64 Bash/Sh 真实命令。
|
||||
- [x] 直连模型“修改 → 测试 → 修复”真实闭环。
|
||||
- [x] 假模型端到端 Subagent 编程委派、真实本机命令和父模型综合。
|
||||
- [x] 真实模型 Subagent 编程委派和父模型综合。
|
||||
- [x] Runtime 隔离、取消、会话释放和进程树回收聚焦测试。
|
||||
- [x] 全量测试、类型检查、Lint 和生产构建。
|
||||
|
||||
## 当前阻塞项
|
||||
|
||||
- 当前没有实现阻塞项。
|
||||
- 本机工具环境仍处于计划阶段;在其实施前,进程工具可以使用当前筛选 PATH,但不能宣称
|
||||
GoodBuddy 托管 Node/Python 已可用。
|
||||
- 远端直连模型进程执行不在首版,后续需要独立增加 Agent 进程协议和真机验证。
|
||||
|
||||
## 2026-09-01 至 2026-09-02 实施与验证证据
|
||||
|
||||
- 新增 `direct-model-process-service.ts`:PowerShell/Bash/Sh 探测、工作区目录校验、96 KiB
|
||||
首尾输出、超时、取消、进程树回收和环境过滤。
|
||||
- 新增 `direct-model-subagent-service.ts`:单层委派、共享并发 3、队列 20、10 分钟上限、
|
||||
192 KiB 输出、父请求取消和临时会话释放。
|
||||
- `ModelToolProvider` 只在直连模型上下文注册工具;Ask 隐藏进程工具,深度 1 隐藏委派工具,
|
||||
远端工作区拒绝本机进程。
|
||||
- `ModelAgentRuntime` 复用同一已解析模型和 Tool Provider 运行子级,并把子级工具、用量和
|
||||
Subagent 活动实时归并到父请求。
|
||||
- 新 actor 联合类型兼容既有专家/OpenCode 记录;编程 Subagent 不创建 Assistant Task。
|
||||
- 聚焦验证:
|
||||
- 进程、Provider、Runtime、共享契约与聊天 UI:`290 passed, 1 skipped`。
|
||||
- 设置与 Runtime 隔离:`216 passed`。
|
||||
- Main/Shared 聚焦持久化与 Subagent:`188 passed, 1 skipped`。
|
||||
- Node 与 Web TypeScript no-emit 检查通过。
|
||||
- 假模型端到端场景实际启动 Windows PowerShell,子级运行命令后返回父模型;该场景的
|
||||
Runtime 模型调用数为 4 次。
|
||||
- 真实 DeepSeek 兼容连接端到端通过:模型创建失败程序、观察非零退出、修复并观察退出码
|
||||
0、委派编程 Subagent 再次运行最终程序并综合结果;最终源码成功验收运行包含 8 次真实
|
||||
模型调用。
|
||||
- BigToken 连接的首次验收在 TLS 建立前被 `ECONNRESET` 中断,未进入模型工具路径;切换
|
||||
到仓库已有的 DeepSeek 连接后完成验证。
|
||||
|
||||
- 全量 `npm test`:`3410 passed, 50 skipped`。
|
||||
- `npm run typecheck`:通过。
|
||||
- `npm run lint`:通过。
|
||||
- `npm run build`:Main、Preload、Renderer 和控制面安装器均成功构建。2026-09-02 按产品
|
||||
决策移除未对应实际性能问题的 Renderer bundle 基线门禁后复验通过。
|
||||
|
||||
## 进度维护要求
|
||||
|
||||
- 只有真实生产路径完成并验证后才能勾选实施项。
|
||||
- 平台模拟单元测试不能替代对应平台真实命令测试。
|
||||
- 真实模型验证必须记录准确调用次数且不得记录凭据。
|
||||
- 需求变化回写 PRD、User Stories 和功能逻辑;实现事实只写本文件。
|
||||
@@ -0,0 +1,398 @@
|
||||
# 直连模型 Agent 能力技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-01 |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 相关架构 | [助手工作栏与执行空间](../assistant-workbar/prd.md)、[本机工具环境](../local-tool-environment/technical-design.md) |
|
||||
|
||||
## 1. 实现基线
|
||||
|
||||
当前源码已经实现:
|
||||
|
||||
- `ModelAgentRuntime` 的多轮工具调用、上下文压缩、取消、用量和有界结果处理。
|
||||
- `ModelToolProvider` 的直连模型内置工具、浏览器、Web、知识和自定义 MCP 聚合。
|
||||
- `WorkspaceAccess` 的本机和远端工作区抽象。
|
||||
- 跨平台子进程树终止辅助函数。
|
||||
- 面向用户专家协作的 `SubagentScheduler`、状态事件和只读模型调用。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 各自的 Shell/Agent 能力。
|
||||
- 直连模型 `process_execute` 和 `subagent_delegate`。
|
||||
- 不创建顶层 Task/Conversation 的编程 Subagent actor 与活动归并。
|
||||
|
||||
## 2. 总体架构
|
||||
|
||||
```text
|
||||
ModelAgentRuntime
|
||||
└─ ModelToolProvider
|
||||
├─ WorkspaceAccess
|
||||
├─ DirectModelProcessService
|
||||
│ └─ LocalDirectModelProcessService
|
||||
├─ DirectModelSubagentService
|
||||
│ ├─ SubagentScheduler
|
||||
│ └─ 同一 ModelAgentRuntime 的临时子会话
|
||||
│ └─ 同一 ModelToolProvider(delegationDepth=1 时移除委派)
|
||||
├─ Browser / Web / Knowledge
|
||||
└─ assigned MCP
|
||||
```
|
||||
|
||||
OpenCode、Continue 和 DeepSeek Harness 的创建路径不接收
|
||||
`DirectModelProcessService` 或 `DirectModelSubagentService`。DeepSeek Harness 即使继续使用
|
||||
`ModelToolProvider` 代理 Web/MCP,也必须通过明确的工具视图排除直连模型专属工具。
|
||||
|
||||
## 3. 共享工具目录
|
||||
|
||||
`BuiltinModelToolSummary` 增加 `programming` 分组和 Runtime 可见性:
|
||||
|
||||
```ts
|
||||
type BuiltinModelToolSummary = {
|
||||
name: string
|
||||
displayName: string
|
||||
description: string
|
||||
access: 'read' | 'write'
|
||||
group: 'filesystem' | 'browser' | 'web' | 'programming'
|
||||
}
|
||||
```
|
||||
|
||||
`process_execute` 标记为 `write`,只在 Execute 清单中出现。
|
||||
`subagent_delegate` 的实际访问级别由父模式决定:
|
||||
|
||||
- Ask 清单中提供只读委派定义。
|
||||
- Execute 清单中提供继承 Execute 的定义。
|
||||
- `callTool` 不依赖静态 `access` 单独授权,必须检查父请求上下文。
|
||||
|
||||
工具总数和 schema 总字节继续计入直连模型现有 100 个工具与 512 KiB 上限。
|
||||
|
||||
## 4. 进程工具契约
|
||||
|
||||
### 4.1 输入
|
||||
|
||||
```ts
|
||||
const processExecuteInputSchema = z.object({
|
||||
command: z.string().trim().min(1).max(100_000),
|
||||
cwd: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(4_096)
|
||||
.optional(),
|
||||
timeoutMs: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1_000)
|
||||
.max(10 * 60_000)
|
||||
.default(120_000)
|
||||
}).strict()
|
||||
```
|
||||
|
||||
- `cwd` 只接受相对工作区路径。
|
||||
- 首版不接受 `env`、`stdin`、`background`、`pty`、`shell` 或任意 executable 参数。
|
||||
- Shell 选择由执行后端决定,防止模型绕过平台契约启动另一套受管接口。
|
||||
- 命令字符串仍可以调用当前账号本来有权运行的程序;Execute 不增加命令白名单。
|
||||
|
||||
### 4.2 结果
|
||||
|
||||
```ts
|
||||
type ProcessExecuteResult = {
|
||||
shell: {
|
||||
kind: 'powershell' | 'bash' | 'sh'
|
||||
label: string
|
||||
}
|
||||
cwd: string
|
||||
exitCode: number | null
|
||||
signal?: string
|
||||
terminationReason?: 'timeout' | 'cancelled'
|
||||
durationMs: number
|
||||
stdout: string
|
||||
stderr: string
|
||||
stdoutTruncated: boolean
|
||||
stderrTruncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
`cwd` 返回工作区相对显示值,不把不必要的绝对用户路径送入模型上下文。Shell 在模型结果
|
||||
中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
|
||||
|
||||
### 4.3 输出边界
|
||||
|
||||
- stdout 和 stderr 各最多 96 KiB。
|
||||
- 超限时分别保留前 48 KiB 与最后 48 KiB,中间插入固定截断标记。
|
||||
- JSON 结果加上元数据后必须低于 `ModelToolProvider` 现有 256 KiB 工具结果上限。
|
||||
- 流式读取在收到数据时执行边界,不先积累无界 Buffer。
|
||||
- 输出按 UTF-8 解码,无效序列使用替换字符并保留字节计数。
|
||||
|
||||
## 5. 平台执行
|
||||
|
||||
### 5.1 Shell 探测
|
||||
|
||||
启动直连模型执行服务时解析一次 Shell:
|
||||
|
||||
```text
|
||||
Windows: PATH 中 pwsh → Windows PowerShell powershell.exe
|
||||
macOS: /bin/bash → /bin/sh
|
||||
Linux: /bin/bash → /bin/sh
|
||||
```
|
||||
|
||||
Shell 不可用时保留直连模型对话能力,但不注册 `process_execute`。设置目录继续展示静态能力
|
||||
说明,实际 Shell 通过每次请求的工具定义和结果可见;当前不增加独立 Shell 诊断 IPC。
|
||||
不下载或捆绑新的 Bash/PowerShell。
|
||||
|
||||
### 5.2 启动参数
|
||||
|
||||
```text
|
||||
PowerShell:
|
||||
<shell> -NoLogo -NoProfile -NonInteractive -Command <command>
|
||||
|
||||
Bash:
|
||||
/bin/bash -c <command>
|
||||
|
||||
Sh:
|
||||
/bin/sh -c <command>
|
||||
```
|
||||
|
||||
使用 `spawn(executable, args, { shell: false })`,避免 Node 再增加一层平台 Shell。POSIX 子进程
|
||||
作为独立进程组启动;Windows 记录根 PID。停止时复用现有
|
||||
`terminateProcessTreeAndWait`:Windows 使用 `taskkill /T /F`,POSIX 终止本次进程组并等待
|
||||
有界退出。
|
||||
|
||||
### 5.3 环境
|
||||
|
||||
进程环境由 Main 构造:
|
||||
|
||||
- 继承运行普通项目命令所需的当前用户基础变量。
|
||||
- 当前使用筛选后的用户 PATH;[本机工具环境](../local-tool-environment/technical-design.md)
|
||||
实施后可通过现有 `toolBinDirectory` 注入口把受管 bin 放在 PATH 前面。
|
||||
- 排除模型 Provider Key、GoodBuddy 加密设置、调试变量和无关应用凭据。
|
||||
- 不修改 `process.env`,不写系统 PATH 或 Shell Profile。
|
||||
- 每次调用得到独立环境快照;命令内设置的变量不会进入下一次调用。
|
||||
|
||||
## 6. 执行空间绑定
|
||||
|
||||
定义一个小型进程后端接口,不把任意进程能力塞进 `WorkspaceAccess`:
|
||||
|
||||
```ts
|
||||
interface ExecutionSpaceProcessExecutor {
|
||||
getCapability(): Promise<
|
||||
| { available: true; shell: ProcessShellSummary }
|
||||
| { available: false; reason: string }
|
||||
>
|
||||
execute(
|
||||
input: ProcessExecuteInput,
|
||||
context: {
|
||||
workspace: WorkspaceAccess
|
||||
signal: AbortSignal
|
||||
}
|
||||
): Promise<ProcessExecuteResult>
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
首版只有 `LocalProcessExecutor`。`ExecutionSpaceDescriptor.kind === 'ssh'` 时使用不可用实现,
|
||||
工具清单不注册进程能力。未来远端实现必须新增 Agent 协议、由 Agent 直接拥有命令进程,并
|
||||
按远端身份、取消和断线语义验证;不得把 SSH 路径传给本机 executor。
|
||||
|
||||
工作目录通过 `WorkspaceAccess.getIdentity()` 和本机真实路径解析共同确认。路径检查与 spawn
|
||||
之间若目录被外部替换,spawn 失败并返回准确错误;不为此增加持久路径凭据或恢复协议。
|
||||
|
||||
## 7. Subagent 工具契约
|
||||
|
||||
### 7.1 输入
|
||||
|
||||
```ts
|
||||
const subagentDelegateInputSchema = z.object({
|
||||
task: z.string().trim().min(1).max(100_000)
|
||||
}).strict()
|
||||
```
|
||||
|
||||
首版一次调用只创建一个子任务。不同父请求可以通过全局调度器并发,不增加批量委派 schema。
|
||||
|
||||
### 7.2 子请求
|
||||
|
||||
`DirectModelSubagentService` 创建临时子请求:
|
||||
|
||||
```ts
|
||||
type DirectModelSubagentContext = {
|
||||
parentRequestId: string
|
||||
childRunId: string
|
||||
projectId?: string
|
||||
conversationId: string
|
||||
workMode: 'ask' | 'execute'
|
||||
}
|
||||
```
|
||||
|
||||
- 子 `conversationId` 只用于 Runtime 内存和工具会话隔离,完成后立即
|
||||
`releaseConversation`,不保存为用户 Conversation。
|
||||
- 子 Prompt 使用明确任务和 Main 可信的编程 Subagent 指令,不复制父会话历史。
|
||||
- 子级使用父请求模型连接,首版不增加单独模型选择设置。
|
||||
- 子级复用父请求已授权能力快照和知识范围。
|
||||
- 子级复用父 Provider;Main 写入的 `delegationDepth=1` 同时在清单和调用边界过滤
|
||||
`subagent_delegate`。
|
||||
- Ask 子级使用 Ask 清单;Execute 子级使用 Execute 清单。
|
||||
|
||||
### 7.3 调度和结果
|
||||
|
||||
- 复用 `SubagentScheduler` 的取消、并发和队列模式。
|
||||
- 全局最大并发 3,队列最大 20。
|
||||
- 单个子任务最长 10 分钟;父请求更早取消时立即停止。
|
||||
- 文本结果最多 192 KiB,错误最多 2 KiB,连同结构化字段保持在 256 KiB 工具结果上限内。
|
||||
- 模型用量逐事件归属父请求和 `childRunId`,不能只算入父模型最后一轮。
|
||||
- 子级失败和取消返回结构化终态及部分输出,让父模型决定下一步。
|
||||
|
||||
### 7.4 领域持久化
|
||||
|
||||
当前 `SubagentService` 为历史专家流程创建隐藏 Child Task。新编程 Subagent 不复制该行为:
|
||||
|
||||
- 运行身份使用内部 `childRunId`。
|
||||
- 父消息通过现有 `subagent` 活动块保存终态和结果摘要。
|
||||
- 若通用 Job/Subjob 表已经实现,则记录为所属父 Task/请求的 Subjob/Run。
|
||||
- 若通用 Job/Subjob 表尚未实现,首版只保存现有消息活动和模型用量,不先创建另一套持久
|
||||
子任务表、恢复日志或兼容读取器。
|
||||
|
||||
可以提取现有 Scheduler、事件归并和用量转发逻辑,但不能把旧数据库字段当成新的产品模型。
|
||||
|
||||
## 8. ModelToolProvider 接入
|
||||
|
||||
`ModelToolCallContext` 增加不可伪造的运行上下文:
|
||||
|
||||
```ts
|
||||
type ModelToolCallContext = {
|
||||
conversationId: string
|
||||
requestId: string
|
||||
runtimeTarget: 'model'
|
||||
workMode: 'ask' | 'execute'
|
||||
executionSpaceIdentity: string
|
||||
delegationDepth: 0 | 1
|
||||
knowledgeCapabilityToken?: string
|
||||
}
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- `listTools` 根据模式、执行空间能力和深度生成快照。
|
||||
- `getApproval` 不为两个工具创建第二套逐次确认;沿用父 Execute 授权和现有通道工具策略。
|
||||
- `callTool` 再次检查 `runtimeTarget`、模式、执行空间 identity、深度和请求是否活动。
|
||||
- `releaseConversation` 取消并释放该会话拥有的浏览器、Subagent 和进程。
|
||||
- `dispose` 等待有界清理,不因模型或子进程不响应而阻塞应用退出。
|
||||
|
||||
DeepSeek Harness 的 Main 工具代理必须使用仅包含分配 MCP 和 Web 的 Provider 视图,不能因为
|
||||
共享 `ModelToolProvider` 类而自动看到直连模型编程工具。
|
||||
|
||||
## 9. 事件与活动
|
||||
|
||||
### 9.1 进程事件
|
||||
|
||||
继续使用现有 Runtime `tool` 事件:
|
||||
|
||||
- `pending`:参数已接收。
|
||||
- `running`:Shell 已成功启动。
|
||||
- `completed`:返回 exit 0 或非零退出结果。
|
||||
- `failed`:启动或契约失败。
|
||||
- `cancelled`:父请求取消。
|
||||
|
||||
结果摘要包含 Shell、相对目录、退出码、耗时和截断标记。完整有界 stdout/stderr 进入工具
|
||||
输出字段,不写应用诊断日志。
|
||||
|
||||
### 9.2 Subagent 事件
|
||||
|
||||
复用现有 `subagent` 公开事件和消息块,增加 `routingMode: 'native'` 表示模型主动委派。
|
||||
`expertId` 不再作为编程 Subagent 必填身份;共享 schema 应改为执行者 discriminated union,
|
||||
而不是伪造一个专家记录:
|
||||
|
||||
```ts
|
||||
type SubagentActor =
|
||||
| { kind: 'expert'; expertId: string; expertName: string }
|
||||
| { kind: 'direct-model'; label: '编程 Subagent' }
|
||||
```
|
||||
|
||||
迁移只需让读取器接受旧专家事件和新联合类型,不重写既有消息。
|
||||
|
||||
## 10. 错误处理
|
||||
|
||||
| 情况 | 结果 |
|
||||
| --- | --- |
|
||||
| 命令非零退出 | 正常 `ProcessExecuteResult` |
|
||||
| 命令不存在 | Shell 非零退出及 stderr |
|
||||
| Shell 无法启动 | 工具失败 |
|
||||
| cwd 无效或越界 | 工具调用前失败 |
|
||||
| 超时/取消 | 终止进程树,返回已有输出和原因 |
|
||||
| 子级模型失败 | 返回 `failed`、部分输出和错误 |
|
||||
| 子级工具失败 | 由子模型处理;最终无法完成时返回子级失败 |
|
||||
| 父请求已终止 | 拒绝新调用,丢弃迟到内容并清理 |
|
||||
|
||||
不要用宽泛 catch 抹掉取消、超时、Provider HTTP 状态或进程退出上下文。
|
||||
|
||||
## 11. 性能
|
||||
|
||||
- Shell 探测按执行服务实例缓存,不在每轮模型调用重复探测。
|
||||
- stdout/stderr 边读边界定,避免大日志进入内存后再截断。
|
||||
- 子级共享父请求已解析的模型配置和工具服务,不复制 MCP 配置或持久状态。
|
||||
- 不增加持久 Shell、后台 daemon、工作区快照或恢复状态机。
|
||||
- 命令执行不使用 PTY,避免普通构建和测试承担终端渲染成本。
|
||||
|
||||
## 12. 测试策略
|
||||
|
||||
### 12.1 单元测试
|
||||
|
||||
- 工具可见性矩阵和 Runtime target 过滤。
|
||||
- Ask 伪造进程调用拒绝。
|
||||
- Windows PowerShell、POSIX Bash/Sh 选择和参数。
|
||||
- cwd 默认、合法子目录、越界、符号链接越界和不存在。
|
||||
- exit 0、非零退出、spawn 失败、超时、取消和输出截断。
|
||||
- 环境筛选、本机工具 PATH 优先且模型凭据不进入进程。
|
||||
- 子级模式、模型、项目、执行空间和能力继承。
|
||||
- 深度 1 过滤、并发 3、队列 20、取消传播和部分输出。
|
||||
- 编程 Subagent 不创建 Task/Conversation。
|
||||
- DeepSeek Harness/OpenCode/Continue 不获得本功能工具。
|
||||
|
||||
### 12.2 本机集成测试
|
||||
|
||||
- Windows PowerShell 创建临时文件、运行 Node/Python、非零退出和进程树取消。
|
||||
- macOS/Linux Bash/Sh 执行相同场景。
|
||||
- 直连模型工具循环执行“修改 → 测试失败 → 修复 → 测试通过”。
|
||||
- 父模型委派子级修改或验证专用临时项目,并正确综合结果。
|
||||
- Runtime 切换后工具清单刷新且没有重复 Shell。
|
||||
- 应用退出后无残留 GoodBuddy 命令进程。
|
||||
|
||||
所有测试只在专用临时工作区创建文件,不删除或覆盖用户工作。
|
||||
|
||||
### 12.3 真实模型验证
|
||||
|
||||
使用一个最小真实直连文本模型请求:
|
||||
|
||||
1. Execute 创建一段有缺陷的小程序。
|
||||
2. 调用进程工具运行聚焦测试并观察失败。
|
||||
3. 修改程序并再次运行到通过。
|
||||
4. 委派一个 Subagent 检查实现或补充测试。
|
||||
5. 父模型读取子级结果并给出最终结论。
|
||||
|
||||
每个平台一次完整场景即可,不进行批量或高成本调用。记录准确模型调用次数,不记录 API Key。
|
||||
|
||||
### 12.4 项目验证
|
||||
|
||||
源码完成后运行:
|
||||
|
||||
```text
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
跨平台进程实现必须由 Windows、macOS、Linux x64 和 Linux arm64 CI 验证。首版不改变
|
||||
GoodBuddy Agent 或桌面到 Agent 生产路径,因此不要求远端 Host 验证;未来增加远端 executor
|
||||
时必须按远程主机技术设计执行共享 Linux Host 真机验证。
|
||||
|
||||
## 13. 实施顺序
|
||||
|
||||
1. Shared 工具摘要、schema 和 Runtime target 过滤。
|
||||
2. 本机 Shell 探测、进程执行、输出边界和进程树取消。
|
||||
3. `ModelToolProvider` Execute 接入及 UI 活动。
|
||||
4. 编程 Subagent 服务、过滤 Provider、事件和用量。
|
||||
5. 设置能力目录与状态诊断。
|
||||
6. 三平台真实命令、真实模型和完整项目验证。
|
||||
|
||||
每一步都必须保持直连模型正常问答可用;Shell 不可用不能阻止普通对话。
|
||||
@@ -0,0 +1,134 @@
|
||||
# 直连模型 Agent 能力 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-01 |
|
||||
| 设计系统 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
## 1. 信息架构
|
||||
|
||||
本功能不增加一级页面、Composer 按钮或 Runtime 专属工具栏。用户主要通过三处理解能力:
|
||||
|
||||
1. **设置 > MCP > 直连模型**:查看内置编程能力说明。
|
||||
2. **聊天消息活动块**:查看命令和 Subagent 的实时状态与结果。
|
||||
3. **运行记录**:按当前 Conversation 或 Task 查看聚合执行活动。
|
||||
|
||||
Job、Subjob 和 Run 继续是内部对象,不创建树形 UI、独立路由或操作菜单。
|
||||
|
||||
## 2. 设置中的能力目录
|
||||
|
||||
在现有直连模型工具目录增加“编程能力”分组:
|
||||
|
||||
```text
|
||||
编程能力
|
||||
进程执行
|
||||
使用当前工作区和当前用户权限运行 PowerShell/Bash 命令。
|
||||
编程 Subagent
|
||||
将聚焦任务委派给同一模型连接;继承当前工作模式和工作区。
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 两项默认属于直连模型,不提供持久 Switch。
|
||||
- 设置目录只定义稳定工具能力,不增加 Shell 状态或重试控件;实际 Shell 标签在模型工具
|
||||
定义和进程结果中返回。
|
||||
- Runtime 是 OpenCode、Continue 或 DeepSeek Harness 时,不在其原生能力清单中伪造这两项;
|
||||
用户需要理解差异时,在直连模型分组说明“Agent Runtime 使用自身工具”。
|
||||
- 不把模型配置检查成功描述为命令或 Subagent 已经通过真实执行验证。
|
||||
|
||||
## 3. 进程活动块
|
||||
|
||||
复用现有对话工具活动视觉和语义,不创建页面专属卡片系统。
|
||||
|
||||
### 3.1 标题与摘要
|
||||
|
||||
```text
|
||||
进程执行
|
||||
PowerShell · packages/app · npm test
|
||||
```
|
||||
|
||||
- 命令只显示单行有界摘要,完整参数进入可展开详情。
|
||||
- 目录显示工作区相对路径;工作区根显示 `.`。
|
||||
- 不在摘要中显示完整环境或凭据。
|
||||
|
||||
### 3.2 状态
|
||||
|
||||
| 状态 | 文案 |
|
||||
| --- | --- |
|
||||
| pending | 等待运行 |
|
||||
| running | 正在运行 |
|
||||
| completed,exit 0 | 已完成 |
|
||||
| completed,exit 非零 | 工具已完成,详情显示退出码 N |
|
||||
| timeout | 工具已完成,详情显示 timeout |
|
||||
| cancelled | 父请求活动显示已取消 |
|
||||
| failed | 启动失败 |
|
||||
|
||||
状态必须同时有文字和状态图标,不能只依靠颜色。非零退出属于正常结构化工具结果,不显示为
|
||||
GoodBuddy 服务崩溃。
|
||||
|
||||
### 3.3 结果详情
|
||||
|
||||
详情按以下顺序展示:
|
||||
|
||||
1. Shell、工作目录、耗时和退出码。
|
||||
2. stdout。
|
||||
3. stderr。
|
||||
4. 截断提示。
|
||||
|
||||
日志使用共享等宽字体和可选择文本。长输出在活动块内折叠,展开后仍保持工作栏和聊天布局
|
||||
可用;不把完整日志复制为应用通知。
|
||||
|
||||
## 4. Subagent 活动块
|
||||
|
||||
复用现有 `subagent` 消息块,并将系统执行者显示为“编程 Subagent”,不要求用户预先创建
|
||||
专家。
|
||||
|
||||
```text
|
||||
编程 Subagent
|
||||
检查认证模块的测试失败并提出修复
|
||||
正在运行 · 直连模型 · Execute
|
||||
```
|
||||
|
||||
终态:
|
||||
|
||||
- 完成:显示有界结果摘要,可展开查看完整返回。
|
||||
- 失败:保留部分输出和错误。
|
||||
- 取消:显示“父请求已取消”或准确取消原因。
|
||||
- 排队:显示当前处于并发队列,不显示虚假百分比。
|
||||
|
||||
活动块不提供单独的“批准”“提高权限”或“继续递归”按钮。用户取消父请求即可取消子级。
|
||||
|
||||
## 5. Ask/Execute 表达
|
||||
|
||||
- Ask 中不显示不可用的进程按钮,因为模型工具没有手动调用入口。
|
||||
- Ask Subagent 活动明确显示“只读”。
|
||||
- Execute 活动显示“当前用户权限”,不声称 OS 沙箱或工作区隔离。
|
||||
- 如果用户从 Ask 切到 Execute,只有下一条请求使用新模式;运行中的 Subagent 不切换。
|
||||
|
||||
## 6. 错误与通知
|
||||
|
||||
- 参数错误、命令退出、Subagent 失败和部分输出属于当前活动上下文,保留在对应活动块。
|
||||
- Runtime 初始化、Shell 探测或全局服务失败可通过应用通知提示,并在设置状态卡提供重试。
|
||||
- 同一错误不同时显示活动块副本和通知副本。
|
||||
- 失败后保留用户 Prompt、命令摘要、工作目录和已有输出。
|
||||
|
||||
## 7. 键盘与无障碍
|
||||
|
||||
- 活动块展开按钮使用原生 `button`,提供 `aria-expanded` 和关联详情 ID。
|
||||
- 状态变化通过现有活动区域的礼貌级 live region 发布,不逐个朗读流式日志。
|
||||
- stdout/stderr 区域具有可见标签,不能只靠顺序区分。
|
||||
- 焦点不会因后台状态更新自动移动。
|
||||
- 取消父请求后的焦点保持在原取消按钮或恢复到 Composer。
|
||||
- 浅色、深色和减少动态效果模式遵守统一设计系统。
|
||||
|
||||
## 8. 响应式
|
||||
|
||||
- 窄聊天区只保留工具名、状态和一行摘要,元数据换行到下一行。
|
||||
- 日志区域横向滚动,不撑宽页面。
|
||||
- 运行记录使用紧凑摘要,不复制完整 stdout/stderr;用户回到聊天活动块展开查看详情。
|
||||
@@ -0,0 +1,168 @@
|
||||
# 直连模型 Agent 能力 User Stories
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台 CI 待验证 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-01 |
|
||||
| 关联 PRD | [直连模型 Agent 能力 PRD](./prd.md) |
|
||||
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
## 1. 角色
|
||||
|
||||
### 1.1 普通项目用户
|
||||
|
||||
希望模型直接完成脚本、文档生成或数据处理,不关心不同平台的 Shell 名称。
|
||||
|
||||
### 1.2 开发者
|
||||
|
||||
希望直连模型能够修改代码、运行测试、检查 Git 状态并根据失败结果继续修正。
|
||||
|
||||
### 1.3 Runtime 用户
|
||||
|
||||
已经使用 OpenCode、Continue 或 DeepSeek Harness,希望继续使用 Runtime 原生能力,不看到
|
||||
重复的 GoodBuddy Shell 或 Subagent。
|
||||
|
||||
## 2. Epic A:跨平台执行
|
||||
|
||||
### US-A1 在 Windows 运行项目命令
|
||||
|
||||
关联:FR-2、FR-3、FR-4。
|
||||
|
||||
Given 当前项目位于 Windows 本机且选择直连模型 Execute,When 模型调用
|
||||
`process_execute`,Then GoodBuddy 使用可用 PowerShell 在项目目录运行命令,并返回
|
||||
Shell、目录、退出码、输出和耗时。
|
||||
|
||||
### US-A2 在 macOS/Linux 运行项目命令
|
||||
|
||||
关联:FR-2、FR-3。
|
||||
|
||||
Given 当前项目位于 macOS 或 Linux 本机,When 模型运行同一工具,Then GoodBuddy 使用
|
||||
Bash,Bash 不可用时使用 Sh,工具契约保持不变且实际 Shell 明确可见。
|
||||
|
||||
### US-A3 使用工作区子目录
|
||||
|
||||
关联:FR-3。
|
||||
|
||||
Given 项目包含多个包,When 模型传入工作区内相对目录,Then 命令在该目录执行;When
|
||||
路径越出工作区、不是目录或不存在,Then 命令不启动并返回准确错误。
|
||||
|
||||
### US-A4 根据非零退出继续修正
|
||||
|
||||
关联:FR-2、FR-9。
|
||||
|
||||
Given 测试命令退出码非零,When 进程结束,Then 工具正常返回退出码和 stderr,而不是终止
|
||||
整个模型请求,And 模型可以读取失败原因、修改代码并再次运行。
|
||||
|
||||
### US-A5 取消长命令
|
||||
|
||||
关联:FR-7。
|
||||
|
||||
Given 命令仍在运行,When 用户取消父请求,Then 当前 Shell 及其由本次命令创建的子进程树
|
||||
停止,And 活动状态显示“已取消”,And 其他用户进程不受影响。
|
||||
|
||||
### US-A6 看懂截断结果
|
||||
|
||||
关联:FR-7、FR-9。
|
||||
|
||||
Given 命令输出超过上限,When 工具返回结果,Then 保留有用的开头和结尾并明确
|
||||
`truncated: true`,And 模型与用户都不会把结果理解为完整日志。
|
||||
|
||||
## 3. Epic B:编程闭环
|
||||
|
||||
### US-B1 修改后运行验证
|
||||
|
||||
关联:FR-2、FR-4。
|
||||
|
||||
Given 用户选择 Execute 并要求修复代码,When 直连模型修改文件,Then 它可以运行聚焦测试
|
||||
或检查命令,并根据真实结果继续修改,直到给出完成或阻塞结论。
|
||||
|
||||
### US-B2 使用本机工具环境
|
||||
|
||||
关联:FR-8、FR-10。
|
||||
|
||||
Given 用户选择了 GoodBuddy 托管或自定义 Node/Python,When 新命令调用 `node`、`npm`、
|
||||
`npx`、`python` 或 `pip`,Then 它们解析到当前工具环境,And 不修改系统 PATH 或已有终端。
|
||||
|
||||
### US-B3 依赖或命令不存在
|
||||
|
||||
关联:FR-2、FR-9。
|
||||
|
||||
Given 项目缺少所需命令或依赖,When Shell 返回错误,Then 工具保留退出码和错误文本,
|
||||
And 模型说明缺失项或在 Execute 中采取用户要求的安装步骤,不把环境缺失报告为模型故障。
|
||||
|
||||
## 4. Epic C:Subagent 委派
|
||||
|
||||
### US-C1 委派聚焦编程任务
|
||||
|
||||
关联:FR-5、FR-6。
|
||||
|
||||
Given 父直连模型正在处理复杂请求,When 它调用 `subagent_delegate` 并给出独立任务,
|
||||
Then 子级使用同一项目、模型连接和 Execute 能力完成工作,And 最终结果返回父模型综合。
|
||||
|
||||
### US-C2 Ask 中只读委派
|
||||
|
||||
关联:FR-4、FR-5。
|
||||
|
||||
Given 父请求为 Ask,When 模型委派分析任务,Then 子级保持 Ask,不能写文件或运行命令,
|
||||
And 不因 Subagent 存在而扩大权限。
|
||||
|
||||
### US-C3 禁止递归委派
|
||||
|
||||
关联:FR-5、FR-7。
|
||||
|
||||
Given 子级正在执行,When 它读取自己的工具清单,Then 清单中没有
|
||||
`subagent_delegate`,And 子级不能创建孙级任务。
|
||||
|
||||
### US-C4 子级失败后父级继续
|
||||
|
||||
关联:FR-5、FR-9。
|
||||
|
||||
Given 子级运行期间失败或超时,When 子任务结算,Then 父模型收到失败状态、部分输出和
|
||||
有界错误,And 可以自行继续、改变方案或向用户说明阻塞。
|
||||
|
||||
### US-C5 取消传播
|
||||
|
||||
关联:FR-7。
|
||||
|
||||
Given 一个 Subagent 正在运行模型或命令,When 用户取消父请求,Then 排队和运行中的子级
|
||||
全部取消,当前命令进程树停止,And 不再向父请求追加迟到结果。
|
||||
|
||||
### US-C6 保持产品对象简单
|
||||
|
||||
关联:FR-6。
|
||||
|
||||
Given 直连模型完成一次委派,When 用户查看会话、Task Center 和活动,Then 会话内可看到
|
||||
Subagent 状态与结果摘要,And Task Center 不出现新的子 Task,And UI 不展示 Job/Run 树。
|
||||
|
||||
## 5. Epic D:Runtime 与执行空间边界
|
||||
|
||||
### US-D1 Agent Runtime 不重复获得工具
|
||||
|
||||
关联:FR-1、FR-10。
|
||||
|
||||
Given 用户选择 OpenCode、Continue 或 DeepSeek Harness,When Runtime 启动,Then
|
||||
GoodBuddy 不注入 `process_execute` 或 `subagent_delegate`,And Runtime 原生能力保持不变。
|
||||
|
||||
### US-D2 切换 Runtime 后工具清单更新
|
||||
|
||||
关联:FR-1。
|
||||
|
||||
Given 会话从直连模型切换为 Agent Runtime,When 下一次请求开始,Then 新请求只使用目标
|
||||
Runtime 的能力;切回直连模型时重新获得符合工作模式的直连工具。
|
||||
|
||||
### US-D3 不在错误设备执行
|
||||
|
||||
关联:FR-3。
|
||||
|
||||
Given 当前执行空间是 SSH 且远端进程后端尚不可用,When 解析工具清单,Then 不注册
|
||||
`process_execute`;即使调用参数有效,也不能回退到桌面本机执行。
|
||||
|
||||
### US-D4 消息通道使用相同规则
|
||||
|
||||
关联:FR-1、FR-4。
|
||||
|
||||
Given 消息通道项目选择直连模型,When 合法消息触发 Ask 或 Execute,Then 使用与桌面会话
|
||||
相同的工具矩阵和通道既有工具策略,不维护第二套 Shell 能力配置。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 文档处理
|
||||
|
||||
文档处理负责聊天附件和知识库导入中的文本提取、格式转换与本地 OCR。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [文档解析与本地 OCR PRD](./prd.md) | 产品范围、解析策略、OCR 模型管理和验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
@@ -9,6 +9,8 @@ GoodBuddy 需要用同一条可信文档解析链路服务以下场景:
|
||||
- 后续的合同审阅、表格分析、演示文稿理解和文档转换。
|
||||
|
||||
文档解析不是对话模型的附属功能。它是主进程管理的独立基础能力,设置入口为“设置中心 / 文档解析”。
|
||||
本地 OCR 的模型下载源以
|
||||
[平台功能页签与模型下载源设计](../../architecture/model-download-source-design.md)为准。
|
||||
|
||||
## 2. 当前基线
|
||||
|
||||
@@ -72,11 +74,12 @@ PDF 不是所有文档唯一的中间格式。解析应同时保留:
|
||||
OCR 模型区沿用语音模型管理模式:
|
||||
|
||||
- 应用不内置模型权重;
|
||||
- 用户按需从 ModelScope 下载,下载完成后离线使用;
|
||||
- 用户按需从全局选择的 ModelScope 或 Hugging Face 下载,默认 ModelScope,下载完成后
|
||||
离线使用;
|
||||
- 显示来源、语言、运行时、模型体积、安装与校验状态;
|
||||
- 联网设备可导出已安装模型 ZIP,离线或内网设备可直接导入;
|
||||
- 支持下载进度、取消、删除、ZIP 导入导出、打开模型仓库和受管目录;
|
||||
- “打开 ModelScope”直接显示在 OCR 模型卡片右上角,不使用手动导入折叠区;
|
||||
- “打开模型仓库”直接显示在 OCR 模型卡片右上角,并使用当前下载源对应仓库;
|
||||
- 模型操作即时生效,解析策略仍通过分类页头的“保存设置”提交。
|
||||
|
||||
### 4.1 第一阶段字段
|
||||
@@ -182,11 +185,16 @@ type ParsedDocument = {
|
||||
|
||||
### 7.2 下载与安装
|
||||
|
||||
Tiny、Small 和 Medium 模型均由 PaddlePaddle 官方 ModelScope 仓库提供。Small 是默认推荐档位;Medium 面向更高识别质量,但具有更高内存占用和延迟。每个档位的检测模型、识别模型与字符字典配置分别使用固定提交,并在应用内记录文件字节数和 SHA-256。
|
||||
Tiny、Small 和 Medium 模型使用 PaddlePaddle 发布的规范工件。GoodBuddy 为 ModelScope
|
||||
和 Hugging Face 分别维护固定下载 Target,默认使用 ModelScope。Small 是默认推荐档位;
|
||||
Medium 面向更高识别质量,但具有更高内存占用和延迟。每个档位的检测模型、识别模型与
|
||||
字符字典配置分别使用固定提交,并在应用内记录各来源解析后的文件字节数和 SHA-256。
|
||||
当前 OCR 两个来源的对应文件字节一致;共享下载契约同时支持后续来源声明不同的固定
|
||||
工件指纹。
|
||||
|
||||
下载流程:
|
||||
|
||||
1. 主进程从固定 ModelScope `resolve/<revision>/...` 地址读取文件;
|
||||
1. 主进程读取已保存的全局模型下载源,并解析该来源的固定 Target;
|
||||
2. 禁用凭据与缓存,限制重定向次数和单文件大小;
|
||||
3. 写入受管目录下的随机临时安装目录;
|
||||
4. 边下载边计算 SHA-256,并核对完整字节数;
|
||||
@@ -194,7 +202,9 @@ Tiny、Small 和 Medium 模型均由 PaddlePaddle 官方 ModelScope 仓库提供
|
||||
6. 原子重命名为正式模型目录;
|
||||
7. 失败、取消或退出时删除临时文件。
|
||||
|
||||
模型只在下载或用户显式打开仓库时访问网络。OCR 推理从受管目录读取已校验文件,不发起网络请求。
|
||||
单次任务只使用启动时冻结的一个来源。所选来源不可用或缺少任一必需文件时明确失败,
|
||||
不请求另一个来源。模型只在下载或用户显式打开仓库时访问网络。OCR 推理从受管目录读取
|
||||
已校验文件,不发起网络请求。
|
||||
|
||||
### 7.3 离线 ZIP 迁移
|
||||
|
||||
@@ -213,7 +223,7 @@ ZIP 根目录包含模型文件和 `goodbuddy-model.json`。清单格式为 `goo
|
||||
- 文件名、角色、原始字节数和 SHA-256;
|
||||
- 导出时间。
|
||||
|
||||
导出不能直接信任已有安装清单,必须重新读取并校验每个文件。导入不能只信任 ZIP 自声明内容,模型 ID、文件角色、字节数和哈希必须再次与当前应用内置目录完全匹配。导入通过后复用普通本地安装的受控临时目录和原子重命名路径。
|
||||
导出不能直接信任已有安装清单,必须重新读取并校验每个文件。导入不能只信任 ZIP 自声明内容,模型 ID、文件角色、字节数和哈希必须再次完整匹配当前应用内置目录的默认包或任一来源包;不接受逐文件跨来源组合。导入通过后复用普通本地安装的受控临时目录和原子重命名路径。
|
||||
|
||||
归档处理使用有界流式读写,不把大型模型或整个展开结果复制到内存。主进程限制压缩包大小、条目数、清单大小、单文件大小和总展开大小,并拒绝:
|
||||
|
||||
@@ -298,7 +308,8 @@ DOC、XLS、PPT 通过 `DocumentConversionProvider` 转换:
|
||||
- 新增文档解析设置分类和持久化契约;
|
||||
- 建立 `DocumentParsingService`,供聊天和知识库共用;
|
||||
- 将无文本 PDF 识别为可触发 OCR 的明确状态;
|
||||
- 接入 PP-OCRv6 Tiny、Small、Medium 的 ModelScope 下载、校验、ZIP 离线迁移、删除与 WASM Worker;
|
||||
- 接入 PP-OCRv6 Tiny、Small、Medium 的双来源下载、校验、ZIP 离线迁移、删除与
|
||||
WASM Worker;
|
||||
- 实现真实文件测试和六平台验证入口。
|
||||
|
||||
### 阶段二
|
||||
@@ -322,7 +333,8 @@ DOC、XLS、PPT 通过 `DocumentConversionProvider` 转换:
|
||||
- 模型文件损坏时拒绝加载并显示可恢复错误;
|
||||
- 未安装模型时扫描文档提示用户前往“文档解析”下载,文本型文档仍可原生解析;
|
||||
- 下载中可显示文件与总进度并允许取消,失败或取消后不留下已安装状态;
|
||||
- ModelScope 下载与 ZIP 导入均经过同一大小和 SHA-256 校验;
|
||||
- ModelScope、Hugging Face 下载与 ZIP 导入均经过同一大小和 SHA-256 校验;
|
||||
- 所选下载源失败或缺少模型时不会请求另一个来源;
|
||||
- 语音和 OCR 模型可在联网设备导出 ZIP,并在离线设备导入后完成真实推理;
|
||||
- 路径穿越、未知条目、错误模型 ID、篡改文件和超限 ZIP 均被拒绝;
|
||||
- 超页数、超时、取消和关闭不会留下运行任务;
|
||||
@@ -0,0 +1,11 @@
|
||||
# 全双工语音
|
||||
|
||||
全双工语音定义持续听说、打断、语音活动检测、本地与远端推理以及多平台音频边界。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | 产品目标、交互流程、音频架构、模型、打包和实施状态 |
|
||||
|
||||
现有文档是综合设计,尚未按 PRD、User Stories、功能逻辑、UI 和进度职责拆分。
|
||||
@@ -0,0 +1,944 @@
|
||||
# GoodBuddy 全双工实时语音交互设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档类型 | 跨功能技术与产品架构 |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 相关基线 | [长期助手路线图](../../roadmap/long-term-assistant-roadmap.md)、[统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义 GoodBuddy 中类似自然通话的全双工实时语音能力,包括本地与云端语音引擎、
|
||||
音频平面、会话状态、打断语义、工具审批、数据留存、失败恢复、跨平台交付和验收指标。
|
||||
|
||||
本文所称“支持本地与云端”是指用户可以显式配置并选择不同语音引擎,不代表系统可以在
|
||||
它们之间自动切换。**GoodBuddy 不设计静默降级。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 摘要与核心决策
|
||||
|
||||
1. 实时语音是独立的 `VoiceSession`,不把现有一次性语音听写改名后直接复用。
|
||||
2. 系统支持三种显式引擎:
|
||||
- 本地模块化全双工:本地流式 ASR、所选 Agent Runtime、本地流式 TTS。
|
||||
- 本地原生全双工:一个本地端到端语音模型同时听、想和说。
|
||||
- 云端原生全双工:通过供应商 Realtime/Live API 进行双向流式音频交互。
|
||||
3. 用户开始会话时冻结引擎、Provider、模型、版本、地域、数据位置、声音、能力和
|
||||
Turn Detection 配置。会话过程中不得静默替换。
|
||||
4. 同一目标内允许有界重试、网络抖动恢复和语义等价的内部执行优化;任何会改变
|
||||
Provider、模型、数据位置、成本、隐私、能力、质量或可感知延迟的替代路径都必须显式。
|
||||
5. 所选引擎不可用时,会话明确进入 `blocked` 或 `failed`,保留可恢复上下文,并提供
|
||||
“重试当前引擎”或“结束后选择其他引擎”。不自动切换本地/云端,不退回听写、纯文本或
|
||||
非全双工模式。
|
||||
6. Renderer 负责麦克风采集、回声消除、低延迟播放和即时打断;Main 负责凭据、会话
|
||||
控制、Provider Adapter、工具权限、持久化和资源回收。
|
||||
7. 音频帧不进入普通 `AgentEvent` 和聊天消息持久化通道。默认只保存最终文本、会话状态和
|
||||
有界诊断,不保存原始录音。
|
||||
8. 语音不能成为新的授权通道。Ask 继续只读,Execute 的工具调用继续经过现有审批控件。
|
||||
|
||||
---
|
||||
|
||||
## 2. 背景与当前基础
|
||||
|
||||
GoodBuddy 当前已经具备:
|
||||
|
||||
- Renderer 中的麦克风入口、录音状态和取消操作。
|
||||
- `getUserMedia` 的单声道采集、回声消除和噪声抑制。
|
||||
- 将完整录音重采样为 16 kHz PCM 的能力。
|
||||
- 基于 `sherpa-onnx` 的本地离线识别、模型下载、ZIP 迁移、选择和删除。
|
||||
- Main 中受信任发送者校验、Zod IPC 输入校验、超时、取消和应用关闭回收。
|
||||
- `AgentRuntime.run()` 的流式文本、工具事件、审批、取消和会话持久化。
|
||||
- Renderer 中的流式聊天时间线、全局通知和可访问的输入控件。
|
||||
|
||||
当前链路仍是:
|
||||
|
||||
```text
|
||||
点击麦克风
|
||||
→ 最多录音 20 秒
|
||||
→ 停止并一次性发送完整 PCM
|
||||
→ 本地离线转写
|
||||
→ 把文本插入输入框
|
||||
→ 用户再次确认发送
|
||||
```
|
||||
|
||||
该链路适合听写,但不具备:
|
||||
|
||||
- 连续流式识别和临时转写。
|
||||
- 同时采集与播放。
|
||||
- 自动轮次检测。
|
||||
- 助手语音输出。
|
||||
- 用户抢话和响应截断。
|
||||
- 音频队列、背压和时钟同步。
|
||||
- 实时语音 Provider 抽象。
|
||||
- 语音会话快照和诊断。
|
||||
|
||||
因此实时语音必须新增会话层,而不是在现有 `SpeechTranscriptionService` 后面简单追加 TTS。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标
|
||||
|
||||
### 3.1 用户目标
|
||||
|
||||
- 用户可以像通话一样持续说话,不需要每轮点击开始和停止。
|
||||
- 助手可以边生成边说,并显示与实际播放进度一致的文本。
|
||||
- 用户开口时可以自然打断,助手在很短时间内停止出声并开始听取新内容。
|
||||
- 用户始终知道当前使用本地还是云端、具体引擎是什么、音频或转写文本会去哪里。
|
||||
- 本地或云端引擎失败时,用户能看到准确状态并决定下一步,不被系统暗中换模型。
|
||||
- 语音对话继续拥有文本聊天中的项目、知识库、角色、Ask/Execute、工具审批和历史能力。
|
||||
|
||||
### 3.2 产品目标
|
||||
|
||||
- 在六个平台/架构目标上提供统一的上层会话契约。
|
||||
- 先以现有 `sherpa-onnx` 和 Agent Runtime 构建可跨平台交付的本地模块化引擎。
|
||||
- 允许云端 Provider 使用 WebRTC 或 WebSocket,但不把供应商协议泄漏到通用 UI。
|
||||
- 允许高性能设备安装本地原生全双工模型,但按真实能力检测决定是否可选。
|
||||
- 保持 Main-only 凭据、上下文隔离、沙箱、取消、超时、有界输出和关机回收。
|
||||
- 为延迟、打断、回声、音频中断、Provider 错误和成本提供可诊断指标。
|
||||
|
||||
---
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
首期不包含:
|
||||
|
||||
- 唤醒词、后台常驻监听或应用退出后的麦克风采集。
|
||||
- 根据网络、负载、价格或“智能判断”自动选择语音引擎。
|
||||
- 在一个会话内自动从云端切到本地,或从本地切到云端。
|
||||
- 从原生全双工自动退到 ASR → LLM → TTS,或反向切换。
|
||||
- 在实时语音失败后自动改成一次性听写、纯文本发送或系统 TTS。
|
||||
- 默认保存、上传或训练用户原始音频。
|
||||
- 声音克隆、未成年人声音模仿、电话呼入或多人会议。
|
||||
- 使用口头“同意”替代工具审批按钮或键盘确认。
|
||||
- 绕过当前 Agent Runtime 和权限边界的 Provider 直连工具、MCP 或 Connector。
|
||||
- 保证所有本地原生语音模型都能在 CPU 或全部六个发布目标上运行。
|
||||
|
||||
---
|
||||
|
||||
## 5. 术语与全双工范围
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| `VoiceEngineProfile` | 用户保存的语音引擎配置,包含类型、Provider、模型、地域、声音和能力 |
|
||||
| `VoiceSession` | 一次从用户显式开始到结束的连续实时语音会话 |
|
||||
| `VoiceTurn` | 用户输入和助手响应形成的一次可持久化对话轮次 |
|
||||
| 系统级全双工 | 麦克风在助手播放期间继续采集,用户可以随时打断 |
|
||||
| 原生模型全双工 | 同一个模型联合处理持续输入、轮次判断和持续语音输出 |
|
||||
| 模块化全双工 | ASR、Agent Runtime 和 TTS 分离,但系统保持同时听说与可打断 |
|
||||
| 临时文本 | 尚未确认的 ASR 或尚未实际播放的助手文本,不写入长期历史 |
|
||||
| 已提交文本 | 用户轮次已确认,或助手对应音频已实际播放的文本 |
|
||||
| Barge-in | 用户在助手说话时开口,触发立即静音、取消和上下文截断 |
|
||||
| 引擎快照 | 会话开始时冻结的完整、无凭据配置及能力声明 |
|
||||
|
||||
“模块化”不等同于“回退”。当用户明确选择模块化本地引擎时,它就是该会话的唯一正式
|
||||
执行路径。原生模型和模块化引擎之间没有隐式优先级。
|
||||
|
||||
---
|
||||
|
||||
## 6. 不静默降级产品契约
|
||||
|
||||
### 6.1 必须显式的变化
|
||||
|
||||
以下变化不得在活动会话中静默发生:
|
||||
|
||||
- 本地与云端之间切换。
|
||||
- Provider、Endpoint、地域或账号切换。
|
||||
- 模型 ID、模型版本、量化档位或语音角色切换。
|
||||
- 原生全双工与模块化全双工之间切换。
|
||||
- ASR、LLM 或 TTS 组件切换。
|
||||
- 从音频输入改成仅文本输入,或从语音输出改成仅文本输出。
|
||||
- 禁用原本声明可用的工具、知识库、角色或 Execute 能力后继续运行。
|
||||
- 把原始音频改为上传,或改变云端数据地域和保留策略。
|
||||
- 采用明显更慢、更低质量或成本不同的路径。
|
||||
|
||||
### 6.2 可自动进行的恢复
|
||||
|
||||
以下操作可以自动执行,但必须保持同一引擎快照:
|
||||
|
||||
- 同一连接内的丢包恢复、抖动缓冲和音频重排。
|
||||
- 同一 Provider、模型、地域和配置的有限重连。
|
||||
- 同一本地模型进程的有限重启。
|
||||
- 不改变语义、隐私、成本和已声明性能级别的算子或执行 Provider 优化。
|
||||
|
||||
恢复在用户可感知前完成时可不打断界面;持续超过 500 ms、导致音频停顿或创建新远端
|
||||
会话时,必须显示“正在重新连接当前引擎”。所有恢复都进入有界诊断记录。
|
||||
|
||||
### 6.3 失败后的用户决策
|
||||
|
||||
恢复预算耗尽后:
|
||||
|
||||
1. 停止采集上传和音频播放。
|
||||
2. 将临时文本标记为未提交,不伪装成完整轮次。
|
||||
3. 保存已提交文本和脱敏错误。
|
||||
4. 显示当前失败的引擎、影响和建议。
|
||||
5. 提供“重试当前引擎”和“结束语音会话”。
|
||||
6. 用户结束后可以显式选择其他引擎并开始新会话。
|
||||
|
||||
首期不提供自动 Failover 列表。未来即使允许用户预先配置替代引擎,也必须在切换前获得
|
||||
明确确认,并在会话中持续显示新的活动引擎。
|
||||
|
||||
### 6.4 产品级适用范围
|
||||
|
||||
本契约不仅适用于语音。GoodBuddy 中 Provider、模型、Runtime、数据处理位置、工作模式、
|
||||
权限范围和质量档位等影响隐私、成本或能力的用户选择,都不得被静默替换。
|
||||
|
||||
用户明确选择名为“自动”的策略时,系统可以在该策略事先声明的范围内选择,但实际结果和
|
||||
任何能力退化必须可见、可诊断,不能把空结果或不完整结果表示为正常成功。
|
||||
|
||||
---
|
||||
|
||||
## 7. 用户体验
|
||||
|
||||
### 7.1 入口
|
||||
|
||||
现有麦克风入口继续表示“语音输入/听写”,转写进入可编辑输入框,不自动发送。
|
||||
|
||||
实时语音使用独立的“开始语音对话”入口,避免用户误以为点击一次听写会开启持续监听。
|
||||
入口只在以下条件满足时可用:
|
||||
|
||||
- 已选择并验证一个全双工语音引擎。
|
||||
- 当前平台满足该引擎能力要求。
|
||||
- 麦克风权限可申请。
|
||||
- 当前 Conversation 没有冲突的活动请求。
|
||||
- 当前工作模式和引擎能力兼容。
|
||||
|
||||
### 7.2 会话界面
|
||||
|
||||
活动会话显示一个持续可见的语音控制区:
|
||||
|
||||
- 当前状态:准备中、正在听、用户说话、正在思考、助手说话、正在打断、等待审批、
|
||||
正在重连、失败。
|
||||
- 本地/云端徽标、引擎名称和数据去向。
|
||||
- 实时用户转写和与播放同步的助手文本。
|
||||
- 麦克风静音、结束会话和必要的设备入口。
|
||||
- 输入音量与助手播放状态,但不得只用颜色表达。
|
||||
- 云端会话的使用量或成本提示入口。
|
||||
|
||||
“结束语音会话”是活动状态下的唯一主操作。波形和头像动效遵守
|
||||
`prefers-reduced-motion`,关闭动效后仍使用文字和图标表达状态。
|
||||
|
||||
### 7.3 打断
|
||||
|
||||
助手说话期间检测到用户有效语音:
|
||||
|
||||
1. 在 Renderer 立即对当前音频执行 20–40 ms 淡出。
|
||||
2. 清空尚未播放的音频队列。
|
||||
3. 向 Main 发送包含播放位置的 `interrupt`。
|
||||
4. Main 取消当前 Agent/TTS 响应或向 Provider 发送截断事件。
|
||||
5. 尚未播放的助手文本保持临时状态并从会话上下文中移除。
|
||||
6. 输入状态切到用户说话,继续采集,不重新建立会话。
|
||||
|
||||
键盘点击“停止说话”与语音 Barge-in 使用相同取消和提交语义。
|
||||
|
||||
### 7.4 工具和审批
|
||||
|
||||
- Ask 模式继续在 Runtime 边界保持只读。
|
||||
- Execute 模式的工具调用进入现有 Approval Broker。
|
||||
- 等待审批时暂停新的助手音频,可播放一次确定性的短提示,例如“需要你确认一个操作”。
|
||||
- 工具参数、风险、范围和确认操作使用现有可访问审批控件。
|
||||
- 麦克风中的“同意”“确认”或相似内容只作为普通用户文本,不构成授权。
|
||||
- 用户拒绝或取消后,结果作为结构化工具事件返回当前引擎,不私自换模型继续。
|
||||
|
||||
### 7.5 设置结构
|
||||
|
||||
长期设置结构使用一级“语音”分类,并以 `PageTabs` 组织:
|
||||
|
||||
1. **实时对话**:语音引擎列表、默认引擎、能力、数据位置、地域、声音和真实连接测试。
|
||||
2. **语音输入**:现有本地 ASR 模型、一次性听写和麦克风设置。
|
||||
3. **语音输出**:本地 TTS 模型、声音、语速和试听。
|
||||
|
||||
当前“模型连接”中的“语音输入”可在迁移阶段保留,之后移动现有模型管理组件时必须保存
|
||||
已安装模型和选择,不创建第二份设置。
|
||||
|
||||
模型类型选择器当前已经包含四项,不增加第五个分段项来承载实时语音,以免违反
|
||||
`SegmentedControl` 的 2–4 项约束。
|
||||
|
||||
语音引擎卡片必须持续显示:
|
||||
|
||||
- 本地或云端。
|
||||
- Provider、模型和版本。
|
||||
- 支持的语言。
|
||||
- 系统级或原生模型全双工。
|
||||
- 是否支持工具、图像和当前 Ask/Execute 模式。
|
||||
- 所需硬件或云端地域。
|
||||
- 音频和文本的数据去向。
|
||||
- 安装、已验证、不可用或需要凭据状态。
|
||||
|
||||
实际生成能力只能通过一次真实、有界、由用户触发的会话测试确认。配置保存成功或只完成
|
||||
握手不能证明麦克风输入、语音输出和打断均可工作。
|
||||
|
||||
---
|
||||
|
||||
## 8. 总体架构
|
||||
|
||||
```text
|
||||
┌──────────────────────── Renderer ────────────────────────┐
|
||||
│ VoiceSession UI │
|
||||
│ getUserMedia → AudioWorklet Capture → Fast VAD │
|
||||
│ AudioWorklet Playback ← Jitter/Playback Queue │
|
||||
└────────────── control IPC ─────── media MessagePort ───────┘
|
||||
│
|
||||
┌────────────────────────── Main ────────────────────────────┐
|
||||
│ VoiceSessionController │
|
||||
│ ├─ Session snapshot and state │
|
||||
│ ├─ Turn coordinator and interruption │
|
||||
│ ├─ Tool/approval bridge │
|
||||
│ ├─ Transcript/message persistence │
|
||||
│ ├─ Credential and provider policy │
|
||||
│ └─ VoiceEngineAdapter │
|
||||
│ ├─ LocalModularAdapter │
|
||||
│ ├─ LocalNativeDuplexAdapter │
|
||||
│ └─ CloudRealtimeAdapter │
|
||||
└───────────────┬──────────────────────┬─────────────────────┘
|
||||
│ │
|
||||
Local managed sidecar Cloud Realtime API
|
||||
or bounded worker WebRTC / WebSocket
|
||||
```
|
||||
|
||||
### 8.1 Renderer 音频平面
|
||||
|
||||
Renderer 负责需要接近音频设备的低延迟操作:
|
||||
|
||||
- 在用户操作后调用 `getUserMedia`。
|
||||
- 请求单声道、回声消除、噪声抑制和受支持时的自动增益。
|
||||
- 使用 `AudioWorklet`,不继续扩展 `ScriptProcessorNode`。
|
||||
- 将音频切成 10–20 ms 有序帧,并按引擎格式重采样。
|
||||
- 执行快速本地 VAD,用于 Barge-in,不独立提交最终轮次。
|
||||
- 维护有界播放和抖动缓冲,记录实际播放采样位置。
|
||||
- 在打断、设备变化、休眠或窗口销毁时立即静音和释放资源。
|
||||
|
||||
Renderer 不持有长期 API Key、不创建本地模型目录、不决定工具权限,也不持久化原始音频。
|
||||
|
||||
### 8.2 Preload 与 IPC
|
||||
|
||||
控制面使用显式、类型化的 preload 方法:
|
||||
|
||||
- `voice.getSnapshot()`
|
||||
- `voice.startSession(input)`
|
||||
- `voice.stopSession(sessionId)`
|
||||
- `voice.setMuted(input)`
|
||||
- `voice.interrupt(input)`
|
||||
- `voice.respondApproval(...)` 继续复用现有审批接口
|
||||
- `voice.onEvent(listener)`
|
||||
|
||||
音频帧不使用逐帧 `ipcRenderer.invoke`、JSON 或 Base64。Main 通过
|
||||
`MessageChannelMain` 向可信主 Frame 传递专用 `MessagePort`,使用可转移
|
||||
`ArrayBuffer` 和严格的帧头。控制事件和媒体帧分别限速、限长和验证。
|
||||
|
||||
### 8.3 Main 控制面
|
||||
|
||||
`VoiceSessionController` 负责:
|
||||
|
||||
- 每个窗口最多一个活动语音会话。
|
||||
- 解析并冻结 `VoiceSessionSnapshot`。
|
||||
- 建立所选 Adapter,不执行自动 Adapter 选择。
|
||||
- 维护输入、输出和生命周期状态。
|
||||
- 将 Barge-in 传播到 Provider、Agent Runtime、TTS 和播放队列。
|
||||
- 桥接工具调用、审批、问题和取消。
|
||||
- 只提交已经确认或实际播放的文本。
|
||||
- 处理超时、重连预算、应用退出、系统休眠和窗口销毁。
|
||||
- 对错误和诊断执行脱敏与边界限制。
|
||||
|
||||
### 8.4 本地进程边界
|
||||
|
||||
轻量 ONNX 能力可以运行在受控 Worker。需要 Python、CUDA、Metal/MLX 或独立依赖树的
|
||||
原生模型运行在 GoodBuddy 管理的 Sidecar:
|
||||
|
||||
- 只绑定 loopback,不监听外部网卡。
|
||||
- 使用随机端口和每次启动的短期认证值。
|
||||
- 环境变量使用最小 allowlist。
|
||||
- 不继承云端 Provider 密钥。
|
||||
- 模型路径由 Main 从受管目录解析,不接受任意相对路径。
|
||||
- 启动、健康检查、并发、输出、内存、超时和进程树有界。
|
||||
- 应用退出时终止完整进程树。
|
||||
|
||||
Sidecar 不因本地模型启动失败而自行连接云端。
|
||||
|
||||
### 8.5 云端连接边界
|
||||
|
||||
供应商支持 WebRTC 时优先使用其媒体传输、编解码和抖动能力:
|
||||
|
||||
- Main 使用长期凭据创建受限、短时的会话描述或临时凭据。
|
||||
- Renderer 只接收当前会话需要的短期材料。
|
||||
- 工具和业务事件优先由 Main sideband 连接处理。
|
||||
- Provider 不支持 sideband 时,由 Main 拥有 WebSocket,并通过媒体 `MessagePort`
|
||||
与 Renderer 交换音频。
|
||||
|
||||
长期凭据永不进入 Renderer、日志、诊断或会话快照。云端 Profile 必须固定可信 Endpoint、
|
||||
地域和数据说明,不跟随重定向切换到未声明的主机。
|
||||
|
||||
---
|
||||
|
||||
## 9. 共享契约
|
||||
|
||||
建议新增 `src/shared/voice-contracts.ts`,核心结构如下:
|
||||
|
||||
```ts
|
||||
type VoiceEngineKind =
|
||||
| 'local-modular'
|
||||
| 'local-native-duplex'
|
||||
| 'cloud-native-duplex'
|
||||
|
||||
type VoiceComponentRef = {
|
||||
providerId: string
|
||||
modelId: string
|
||||
modelVersion?: string
|
||||
endpoint?: string
|
||||
region?: string
|
||||
accountRef?: string
|
||||
credentialRef?: string
|
||||
}
|
||||
|
||||
type VoiceEngineProfile = {
|
||||
id: string
|
||||
name: string
|
||||
kind: VoiceEngineKind
|
||||
locality: 'local' | 'cloud'
|
||||
voiceId: string
|
||||
components: {
|
||||
asr?: VoiceComponentRef
|
||||
tts?: VoiceComponentRef
|
||||
nativeDuplex?: VoiceComponentRef
|
||||
}
|
||||
dataPath: {
|
||||
audioDestination:
|
||||
| { kind: 'device' }
|
||||
| {
|
||||
kind: 'provider'
|
||||
providerId: string
|
||||
endpoint: string
|
||||
region?: string
|
||||
}
|
||||
transcriptDestination:
|
||||
| { kind: 'device' }
|
||||
| {
|
||||
kind: 'provider'
|
||||
providerId: string
|
||||
endpoint: string
|
||||
region?: string
|
||||
}
|
||||
retentionPolicyId?: string
|
||||
}
|
||||
capabilities: {
|
||||
nativeDuplex: boolean
|
||||
supportsTools: boolean
|
||||
supportsAsk: boolean
|
||||
supportsExecute: boolean
|
||||
inputLanguages: string[]
|
||||
outputLanguages: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type VoiceRuntimeSnapshot = {
|
||||
selection: Exclude<AgentRuntimeSelection, { provider: 'auto' }>
|
||||
profileRevision?: string
|
||||
configurationDigest: string
|
||||
workspacePath: string
|
||||
}
|
||||
|
||||
type VoiceSessionSnapshot = {
|
||||
sessionId: string
|
||||
conversationId: string
|
||||
profile: VoiceEngineProfile
|
||||
profileRevision: string
|
||||
engineConfigurationDigest: string
|
||||
runtime?: VoiceRuntimeSnapshot
|
||||
workMode: 'ask' | 'execute'
|
||||
inputFormat: VoiceAudioFormat
|
||||
outputFormat: VoiceAudioFormat
|
||||
turnDetection: VoiceTurnDetectionConfig
|
||||
startedAt: string
|
||||
}
|
||||
```
|
||||
|
||||
`credentialRef` 和 `accountRef` 是不含凭据正文的稳定引用。Endpoint 写入 Profile 或快照前
|
||||
必须规范化并删除用户名、密码、查询参数和 Fragment;供应商部署路径仍应保留,以便检测
|
||||
Endpoint 是否发生变化。Profile 持久化时只引用 Main 加密设置,快照不包含长期或临时
|
||||
Token。
|
||||
|
||||
模块化引擎分别记录 ASR 和 TTS 组件,原生引擎记录 `nativeDuplex` 组件;不能用一个
|
||||
`modelId` 代表多组件链路。文本 Agent 使用独立 `VoiceRuntimeSnapshot`,记录已解析的明确
|
||||
Runtime、模型 Profile 修订、配置摘要和工作区。`dataPath` 分别说明原始音频和转写文本
|
||||
留在设备还是发送到哪个供应商。
|
||||
|
||||
### 9.1 Runtime Lease
|
||||
|
||||
语音会话不能在每轮请求时重新读取可变的全局 Runtime 设置。启动时必须:
|
||||
|
||||
1. 将 `auto` 解析为明确的 Runtime 和模型 Profile,并在会话界面显示实际结果。
|
||||
2. 根据已解析配置创建或取得一个不可变的 `VoiceRuntimeLease`。
|
||||
3. Lease 在整个 Voice Session 内引用同一个 Runtime 实例和配置摘要。
|
||||
4. 全局设置变化只为新请求和新 Voice Session 创建 Runtime,不替换活动 Lease。
|
||||
5. 用户删除或修改活动 Profile 时,界面说明“下次语音会话生效”;当前 Lease 继续运行。
|
||||
6. 固定实例无法继续时,当前语音会话明确失败,不能取得新的全局 Runtime 继续。
|
||||
|
||||
现有 `AgentRuntimeController` 的可变 `current` Slot 会在 `replace()` 后中断活动请求,因此
|
||||
不能直接作为长期 Voice Session Lease。实现前必须增加引用计数式 Pin/Lease,或由
|
||||
`SelectedRuntimeManager` 为会话持有独立 Runtime Slot;会话结束后再
|
||||
`releaseConversation()` 并释放 Lease。
|
||||
|
||||
### 9.2 事件
|
||||
|
||||
控制事件至少包括:
|
||||
|
||||
- `session-preparing`
|
||||
- `session-ready`
|
||||
- `session-reconnecting`
|
||||
- `input-speech-started`
|
||||
- `input-transcript-delta`
|
||||
- `input-transcript-committed`
|
||||
- `response-started`
|
||||
- `response-transcript-delta`
|
||||
- `response-audio-started`
|
||||
- `response-interrupted`
|
||||
- `response-completed`
|
||||
- `approval-required`
|
||||
- `tool-state`
|
||||
- `usage`
|
||||
- `error`
|
||||
- `session-ended`
|
||||
|
||||
音频帧使用独立二进制协议,包含:
|
||||
|
||||
- `sessionId`
|
||||
- `generationId`
|
||||
- `sequence`
|
||||
- `timestampSamples`
|
||||
- `sampleRate`
|
||||
- `channels`
|
||||
- `encoding`
|
||||
- `payload`
|
||||
|
||||
帧乱序、重复、跨会话或超过大小上限时直接拒绝,不尝试解释为其他格式。
|
||||
|
||||
---
|
||||
|
||||
## 10. 状态模型
|
||||
|
||||
全双工不能只用一个“正在听/正在说”枚举描述。会话使用三个正交状态:
|
||||
|
||||
```text
|
||||
Lifecycle:
|
||||
idle → preparing → active ↔ reconnecting → ended
|
||||
└──────────────→ failed
|
||||
|
||||
Input:
|
||||
muted ↔ listening ↔ speech
|
||||
|
||||
Output:
|
||||
idle → generating → playing → interrupting → idle
|
||||
```
|
||||
|
||||
用户可见状态由三个状态组合得出。合法示例:
|
||||
|
||||
- `input=listening + output=playing`:助手说话,同时继续监听。
|
||||
- `input=speech + output=interrupting`:用户抢话,助手正在停止。
|
||||
- `lifecycle=reconnecting + input=muted + output=idle`:当前引擎重连,停止上传。
|
||||
|
||||
`awaiting-approval` 是运行阻塞原因,不关闭会话;此时输入可以继续听取取消或补充文本,
|
||||
但不能把口头内容解释成授权。
|
||||
|
||||
---
|
||||
|
||||
## 11. Turn、文本与播放提交
|
||||
|
||||
### 11.1 用户输入
|
||||
|
||||
- 流式 ASR Delta 只用于界面。
|
||||
- Endpoint Detector 确认轮次后产生 committed transcript。
|
||||
- 空白、纯噪音和低置信度片段不创建用户消息。
|
||||
- 用户可在提交前通过键盘修正;修正结果而非原始猜测进入 Agent Runtime。
|
||||
|
||||
### 11.2 助手输出
|
||||
|
||||
模块化 TTS 可能落后于文本生成,因此助手文本分为:
|
||||
|
||||
- `generated`:模型已生成,尚未安排播放。
|
||||
- `queued`:已生成音频,尚未播放。
|
||||
- `played`:对应音频已从播放时钟确认输出。
|
||||
|
||||
助手消息需要区分“用户可见历史”和“下一轮模型上下文”:
|
||||
|
||||
- 所有已展示的有界文本和结构化内容都写入可见消息历史。
|
||||
- 可朗读文本记录 `generated`、`queued`、`played` 边界;中断后的消息标记为
|
||||
`interrupted`,并保留用户已经看见的内容及已播放边界。
|
||||
- 下一轮模型上下文只包含 `played` 可朗读文本,以及已经展示的 `visual-only` 内容。
|
||||
- 尚未播放的可朗读尾部即使曾临时显示,也不回送模型,并在历史中显示“未播完”状态。
|
||||
|
||||
代码块、表格、URL、引用和工具结果等不适合逐字朗读的内容使用 `visual-only` Block。它们
|
||||
一旦完整展示即可进入可见历史和下一轮上下文,不受语音播放边界裁切。这样既不会丢失用户
|
||||
已经看到的详细成果,也不会让模型误以为用户听到了被打断的语音尾部。
|
||||
|
||||
云端 Provider 支持会话截断时,Main 使用实际播放位置截断远端 Conversation Item;
|
||||
不支持时由 GoodBuddy 在下一轮上下文中只组装 `played` 和已展示的 `visual-only` 部分。
|
||||
现有消息契约与上下文组装器需要增加对应 Block 状态,不能用删掉完整助手消息来模拟截断。
|
||||
|
||||
### 11.3 文本转语音规划
|
||||
|
||||
模块化引擎从流式文本中产生可取消的短语块:
|
||||
|
||||
- 优先在中文标点、英文句界和自然从句边界提交。
|
||||
- 首个短语不等待完整回答,以降低首音频延迟。
|
||||
- URL、Markdown 标记、代码块、表格、引用编号和工具 JSON 不逐字符朗读。
|
||||
- 不能可靠口述的内容在界面展示,并使用确定性短提示说明“详细内容已显示在对话中”。
|
||||
- 不调用第二个未选择的模型生成“语音摘要”。
|
||||
|
||||
---
|
||||
|
||||
## 12. 引擎设计
|
||||
|
||||
### 12.1 本地模块化全双工
|
||||
|
||||
首个跨平台本地基线复用现有 `sherpa-onnx`:
|
||||
|
||||
```text
|
||||
AudioWorklet
|
||||
→ Silero/TEN VAD
|
||||
→ sherpa-onnx OnlineRecognizer
|
||||
→ selected AgentRuntime
|
||||
→ deterministic speech text planner
|
||||
→ sherpa-onnx TTS callback
|
||||
→ AudioWorklet playback
|
||||
```
|
||||
|
||||
现有 `sherpa-onnx` Node Addon 已提供在线识别、VAD、本地 TTS 和 TTS 音频回调。当前已安装
|
||||
的 SenseVoice、Paraformer 和 Whisper 目录主要用于离线识别;实时模式需要独立的在线
|
||||
模型目录和能力声明,不能把离线模型误标成流式模型。
|
||||
|
||||
“本地模块化”只保证音频采集、ASR 和 TTS 在本机。中间 Agent Runtime 是否本地取决于
|
||||
用户明确选择的模型连接:
|
||||
|
||||
- 连接到 loopback 本地模型时,完整链路可离线。
|
||||
- 连接到云端文本模型时,原始音频留在本地,但最终转写文本和 Agent 上下文会发送到
|
||||
该模型。界面必须明确显示这一数据路径。
|
||||
|
||||
不得因当前文本 Runtime 不可用而替换为另一模型连接。
|
||||
|
||||
### 12.2 本地原生全双工
|
||||
|
||||
本地原生 Adapter 面向 MiniCPM-o、Moshi/PersonaPlex、BayLing-Duplex 等能够持续接收并
|
||||
生成音频的模型。具体模型接入前必须逐个验证:
|
||||
|
||||
- 中文和目标语言质量。
|
||||
- 真正的持续输入、Barge-in 和 Backchannel,而不只是流式输出。
|
||||
- 首音频延迟和长期运行内存。
|
||||
- Windows、macOS、Linux 及 x64/arm64 Runtime 可用性。
|
||||
- NVIDIA CUDA、Apple Silicon 或 CPU 的真实硬件要求。
|
||||
- 工具调用、系统指令、上下文长度和取消支持。
|
||||
- 模型、声音、训练数据与商业分发许可。
|
||||
- 权重下载、ZIP 迁移、校验和、磁盘占用和卸载。
|
||||
|
||||
本地原生模型不作为六平台默认能力。只有能力检测和一次真实会话测试通过后才允许选择。
|
||||
缺少结构化工具能力的模型可以声明为 Ask-only;Execute 入口必须阻塞并说明原因,不能暗中
|
||||
调用另一个文本模型补齐工具。
|
||||
|
||||
### 12.3 云端原生全双工
|
||||
|
||||
云端 Adapter 可以面向 OpenAI Realtime、Gemini Live、Qwen Realtime、Azure Voice Live
|
||||
等正式配置。每个 Adapter 必须显式声明:
|
||||
|
||||
- WebRTC 或 WebSocket 传输。
|
||||
- 输入输出音频格式。
|
||||
- VAD、Semantic Turn Detection 和手动提交能力。
|
||||
- 响应取消、音频截断和实际播放对齐能力。
|
||||
- 输入与输出转写能力。
|
||||
- 工具调用和 sideband 控制能力。
|
||||
- 会话时长、上下文、速率限制和费用。
|
||||
- 可用地域、数据处理与保留说明。
|
||||
|
||||
Provider 配置不使用泛化“OpenAI compatible”推断 Realtime 能力。普通 Chat Completions
|
||||
Endpoint 不能因为 URL 相似就被标记为实时语音。
|
||||
|
||||
---
|
||||
|
||||
## 13. 音频处理
|
||||
|
||||
### 13.1 采集
|
||||
|
||||
- 浏览器设备通常以 44.1 或 48 kHz 采集,不能假定请求值就是实际值。
|
||||
- 使用 `MediaStreamTrack.getSettings()` 记录实际声道、采样率和回声消除状态。
|
||||
- AudioWorklet 以原始设备时钟采集,再按引擎要求转换为 16/24/48 kHz。
|
||||
- 默认单声道 Float32 内部格式,边界处转换为 PCM16、Opus 或 Provider 指定格式。
|
||||
- 每帧 10–20 ms,带序号和采样时间,不使用墙钟猜测播放位置。
|
||||
|
||||
### 13.2 回声与抢话
|
||||
|
||||
回声处理使用两层信号:
|
||||
|
||||
1. Chromium AEC/NS/AGC 处理后的麦克风流。
|
||||
2. GoodBuddy 已知的播放活动、播放能量和 VAD 结果。
|
||||
|
||||
只有满足最短语音持续时间、能量和回声相关性条件时才触发 Barge-in。阈值必须可测试,
|
||||
不能仅依赖一个 Provider 的 `speech_started` 事件。Provider 事件作为权威轮次信号之一,
|
||||
本地快速 VAD 负责先静音。
|
||||
|
||||
### 13.3 播放与背压
|
||||
|
||||
- 每个响应使用独立 `generationId`,旧响应帧不得进入新队列。
|
||||
- 播放队列按采样时钟排序,禁止无限积压。
|
||||
- 达到高水位时对上游施加背压;无法背压的 Provider 丢弃会话并报告协议错误,不能持续
|
||||
增长内存。
|
||||
- 音频缺口使用短静音或 Provider 编解码恢复,不重复上一段语音。
|
||||
- 切换输出设备、设备丢失或系统休眠时暂停提交时钟,避免把未播放文本标记为已听到。
|
||||
|
||||
---
|
||||
|
||||
## 14. 数据与持久化
|
||||
|
||||
### 14.1 默认保存
|
||||
|
||||
- Voice Session ID、Conversation ID 和时间。
|
||||
- 无凭据的引擎快照及其摘要哈希。
|
||||
- 最终用户文本、已展示的助手消息、`visual-only` Block、实际播放边界和中断状态。
|
||||
- 中断、失败、取消和完成状态。
|
||||
- 有界延迟、音频中断和用量指标。
|
||||
- 工具与审批事件继续进入现有任务和活动记录。
|
||||
|
||||
### 14.2 默认不保存
|
||||
|
||||
- 原始麦克风音频。
|
||||
- Provider 返回但尚未播放的音频。
|
||||
- 临时 ASR Delta。
|
||||
- 长期或临时 API Key、Cookie、会话 Token。
|
||||
- Provider 原始错误正文和可能包含用户内容的网络帧。
|
||||
- 回声参考信号、设备唯一标识和完整声学特征。
|
||||
|
||||
未来若提供录音留存,必须是独立、默认关闭的功能,说明保存位置、期限、大小、导出和删除,
|
||||
并与“改进模型”授权分离。
|
||||
|
||||
### 14.3 崩溃恢复
|
||||
|
||||
应用启动时将未结束的 Voice Session 标记为 `interrupted`。恢复文本 Conversation,
|
||||
但不自动重新打开麦克风、不自动连接 Provider,也不重播未完成音频。
|
||||
|
||||
---
|
||||
|
||||
## 15. 错误、重连与资源回收
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 麦克风权限拒绝 | 阻塞启动,保留引擎选择,提供系统权限说明 |
|
||||
| 输入/输出设备消失 | 立即静音或暂停,要求用户处理设备,不改用未选择设备 |
|
||||
| 本地模型缺失或损坏 | 阻塞启动,进入模型管理,不连接云端 |
|
||||
| 本地 Runtime 启动失败 | 在有界预算内重启同一 Runtime,之后明确失败 |
|
||||
| 云端认证或地域错误 | 明确失败,保留配置,不尝试其他 Provider/地域 |
|
||||
| 短暂断网 | 同一引擎有界重连,超过 500 ms 显示状态 |
|
||||
| Provider 限流或余额不足 | 结束生成并显示原因,不切本地模型 |
|
||||
| Agent Runtime 失败 | 终止当前轮次,允许重试同一 Runtime,不换连接 |
|
||||
| TTS 失败 | 当前轮次失败,不静默改成系统 TTS 或仅文本成功 |
|
||||
| 工具等待审批 | 暂停响应,保留会话;拒绝后把结果返回当前引擎 |
|
||||
| 应用退出/窗口销毁 | 取消请求、停止 Track、关闭 Port/PeerConnection、终止 Sidecar |
|
||||
| 系统休眠/锁屏 | 停止采集和上传;恢复后要求用户显式继续 |
|
||||
|
||||
每个会话必须有最大时长、最大连续无声时间、最大媒体队列、最大临时文本、最大重连次数和
|
||||
最大诊断大小。取消优先于重连和重试。
|
||||
|
||||
---
|
||||
|
||||
## 16. 安全与隐私
|
||||
|
||||
1. 只允许可信主窗口主 Frame 创建和控制 Voice Session。
|
||||
2. 麦克风权限只放行音频,不因实时语音放开视频。
|
||||
3. 任何音频采集都需要用户操作;活动期间持续显示应用内状态和系统麦克风指示。
|
||||
4. 云端会话在开始前显示 Provider、地域、发送内容和可能费用。
|
||||
5. API Key 只在 Main 的加密设置或受控环境变量中使用。
|
||||
6. 临时 Provider 凭据具有最短可行期限、最小能力和单会话作用域。
|
||||
7. Provider 工具调用必须回到 Main 的白名单、Schema、Ask/Execute 和审批边界。
|
||||
8. 本地 Sidecar 只监听 loopback,使用短期认证,不开放外部端口。
|
||||
9. 模型权重按受信任目录、固定来源、大小和 SHA-256 校验,导入 ZIP 防止路径穿越和压缩炸弹。
|
||||
10. 日志只记录状态、耗时、错误分类和匿名引擎 ID,不记录语音正文和音频。
|
||||
11. 窗口隐藏时若会话仍活动,托盘必须持续显示麦克风状态和停止入口;首期可以选择隐藏即
|
||||
暂停,但不能隐藏后无提示继续采集。
|
||||
12. Voice Session 不扩大项目、知识库、文件、浏览器或桌面控制范围。
|
||||
|
||||
---
|
||||
|
||||
## 17. 性能与质量指标
|
||||
|
||||
### 17.1 交互指标
|
||||
|
||||
| 指标 | 目标 |
|
||||
| --- | --- |
|
||||
| 用户开口到本地 VAD 检出 | P95 ≤ 100 ms |
|
||||
| Barge-in 检出到扬声器静音 | P95 ≤ 150 ms |
|
||||
| 播放队列常态深度 | 100–400 ms |
|
||||
| 用户轮次结束到临时文本稳定 | P50 ≤ 300 ms |
|
||||
| 用户轮次结束到首段助手音频 | 云端/原生引擎 P50 ≤ 800 ms;模块化引擎 P50 ≤ 1,200 ms |
|
||||
| 已提交文本与实际播放偏差 | ≤ 100 ms 或一个最小短语块 |
|
||||
| 连续 30 分钟会话 | 无未界定内存增长、重复播放或资源泄漏 |
|
||||
|
||||
本地指标必须注明测试硬件,不能把高端 GPU 结果宣传为 CPU 基线。未达到所选引擎声明的
|
||||
实时系数时,能力检测应标记为不满足实时要求,而不是静默切到更小模型。
|
||||
|
||||
### 17.2 质量指标
|
||||
|
||||
- 中文普通话、英文和中英混合词的 ASR 错误率。
|
||||
- 长停顿、语气词、短回答和自我修正的轮次准确率。
|
||||
- 扬声器回声、键盘声、音乐和旁人说话下的误打断率。
|
||||
- 真正用户抢话的漏检率和停止延迟。
|
||||
- TTS 首段延迟、断句、数字、日期、英文缩写和代码术语可懂度。
|
||||
- 中断后下一轮上下文不包含未播放内容。
|
||||
- Provider、模型、数据位置和能力从不发生未声明变化。
|
||||
|
||||
---
|
||||
|
||||
## 18. 测试策略
|
||||
|
||||
### 18.1 自动化
|
||||
|
||||
- Voice Contract Schema、大小边界和迁移测试。
|
||||
- 三组正交状态及非法状态组合测试。
|
||||
- 有序、乱序、重复、迟到和跨 Session 音频帧测试。
|
||||
- Barge-in 对播放、Provider、Agent、TTS 和持久化的取消传播测试。
|
||||
- 临时文本、已提交文本和播放位置对齐测试。
|
||||
- 同一引擎重连预算与超时测试。
|
||||
- “禁止静默降级”矩阵测试:任何 Adapter、Provider、模型、地域或 Runtime 变化都必须失败。
|
||||
- Ask 只读和 Execute 审批测试。
|
||||
- 窗口销毁、应用退出、休眠和设备丢失的资源释放测试。
|
||||
- 不持久化音频、临时 Token 和 Provider 原始正文的数据库测试。
|
||||
|
||||
### 18.2 模拟与声学测试
|
||||
|
||||
建立确定性 Fake Voice Engine,能够注入:
|
||||
|
||||
- 固定节奏的输入、文本和音频。
|
||||
- 网络抖动、丢包、重复和断开。
|
||||
- 超前文本、迟到音频和错误播放位置。
|
||||
- 用户抢话、回声、短噪音和长停顿。
|
||||
- 工具调用、审批、拒绝和取消。
|
||||
|
||||
真实声学测试使用预录双声道夹具,一路作为助手扬声器参考,一路作为用户麦克风输入。
|
||||
不能只通过静态单段 WAV 验证全双工。
|
||||
|
||||
### 18.3 手动与外部调用
|
||||
|
||||
- 六个发布目标分别验证麦克风权限、采集、播放、设备拔插和应用退出。
|
||||
- 本地模型在声明的最低硬件上完成 30 分钟稳定性和实时系数测试。
|
||||
- 云端 Provider 测试会产生外部调用和费用,只在明确授权的 gated 测试中运行。
|
||||
- 每个云端 Adapter 至少验证一次真实音频输入、真实音频输出、打断和工具审批。
|
||||
- 真实测试失败时不使用配置握手成功替代生成验证。
|
||||
|
||||
---
|
||||
|
||||
## 19. 跨平台交付
|
||||
|
||||
### 19.1 基线
|
||||
|
||||
- 本地模块化引擎作为 Windows、macOS、Linux x64/arm64 的统一功能基线。
|
||||
- 在线 ASR、VAD 和轻量 TTS 权重不内置,继续使用按需下载和 ZIP 离线迁移。
|
||||
- GoodBuddy 托管模型的下载遵守
|
||||
[平台功能页签与模型下载源设计](../../architecture/model-download-source-design.md),使用用户显式选择的
|
||||
ModelScope 或 Hugging Face,失败时不切换来源。
|
||||
- 云端 Adapter 在六个平台复用同一契约,并分别验证 Electron WebRTC/WebSocket 行为。
|
||||
- 本地原生引擎按 Adapter 声明平台与硬件,不伪装成全平台能力。
|
||||
|
||||
### 19.2 硬件能力等级
|
||||
|
||||
| 等级 | 目标 |
|
||||
| --- | --- |
|
||||
| CPU 基线 | 本地模块化 ASR/TTS;文本 Runtime 可以本地或云端 |
|
||||
| Apple Silicon | 可增加 MLX/Metal 本地原生 Adapter,必须单独验证 |
|
||||
| NVIDIA GPU | 可增加 CUDA 本地原生 Adapter,按显存和驱动验证 |
|
||||
| 不满足要求 | 引擎卡片显示不可用与原因,不自动选择其他引擎 |
|
||||
|
||||
安装包继续保持轻量。大模型权重、CUDA Runtime 和独立 Python 环境不得无条件加入全部
|
||||
发布包。
|
||||
|
||||
---
|
||||
|
||||
## 20. 分阶段实施
|
||||
|
||||
### 阶段 0:契约与模拟器
|
||||
|
||||
- 新增 Voice Contracts、状态机和 Fake Voice Engine。
|
||||
- 建立禁止静默降级测试矩阵。
|
||||
- 建立会话快照、事件和诊断结构。
|
||||
|
||||
### 阶段 1:Renderer 音频平面
|
||||
|
||||
- AudioWorklet 采集与播放。
|
||||
- 媒体 `MessagePort`、背压和播放时钟。
|
||||
- 快速 VAD、回声关联、Barge-in 和设备生命周期。
|
||||
- 实时语音控制区和可访问状态。
|
||||
|
||||
### 阶段 2:本地模块化基线
|
||||
|
||||
- 在线 ASR 和 VAD 模型管理。
|
||||
- Agent Runtime 流式文本桥。
|
||||
- 本地 TTS 模型管理、短语规划、音频回调和取消。
|
||||
- 最终文本持久化与工具审批。
|
||||
- 六个平台/架构验证。
|
||||
|
||||
### 阶段 3:首个云端原生 Adapter
|
||||
|
||||
- Main-only 凭据和引擎 Profile。
|
||||
- WebRTC 或 WebSocket 会话。
|
||||
- 转写、音频、截断、用量和 Provider 错误。
|
||||
- sideband 工具与审批。
|
||||
- 真实有费用的 gated 验证。
|
||||
|
||||
### 阶段 4:本地原生全双工 Adapter
|
||||
|
||||
- 选择一个中文质量、许可和硬件要求已验证的模型。
|
||||
- 建立受管 Sidecar、能力检测和真实会话测试。
|
||||
- 验证原生 Barge-in、文本提交、工具能力和长期稳定性。
|
||||
|
||||
### 阶段 5:扩展与质量
|
||||
|
||||
- 增加经过验证的云端和本地 Adapter。
|
||||
- 输出设备选择和企业语音策略。
|
||||
- 声学基准、延迟仪表盘和成本诊断。
|
||||
- 评估是否允许用户预配置仍需确认的显式替代策略。
|
||||
|
||||
---
|
||||
|
||||
## 21. 首个垂直切片
|
||||
|
||||
首个可合并实现应使用 Fake Voice Engine,不立即绑定某个云端 Provider:
|
||||
|
||||
1. 用户显式开始会话。
|
||||
2. AudioWorklet 持续采集和播放模拟流。
|
||||
3. Fake Engine 产生临时转写、助手文本和音频。
|
||||
4. 用户开口触发 150 ms 内静音和响应取消。
|
||||
5. 持久化已提交用户文本、已展示助手内容、`visual-only` Block 和实际播放边界;下一轮
|
||||
模型上下文只使用已播放文本与完整显示的 `visual-only` Block。
|
||||
6. 模拟工具审批时暂停语音,拒绝口头授权。
|
||||
7. 注入 Adapter 失败后明确结束,不切换任何引擎。
|
||||
8. 关闭窗口后所有 Track、Port、计时器和模拟任务归零。
|
||||
|
||||
该切片先验证最难改变的会话、音频、提交和安全契约,再分别接入本地和云端实现。
|
||||
|
||||
---
|
||||
|
||||
## 22. 验收标准
|
||||
|
||||
- 用户可以明确选择本地模块化、本地原生或云端原生引擎,界面持续显示当前选择。
|
||||
- 会话快照冻结 Provider、模型、地域、声音、数据位置和能力。
|
||||
- 任何引擎、Provider、模型、地域、Runtime 或模式变化都不能在测试中静默发生。
|
||||
- 助手播放期间继续采集麦克风,用户可在 P95 150 ms 内打断。
|
||||
- 中断后未播放音频与文本不进入下一轮上下文。
|
||||
- Ask 和 Execute 在语音中与文本中使用同一权限和审批边界。
|
||||
- 语音口令不能批准工具。
|
||||
- 云端长期凭据不进入 Renderer,本地 Sidecar 不监听外部地址。
|
||||
- 默认数据库、日志和 Artifact 中没有原始音频。
|
||||
- 本地引擎失败不连接云端,云端引擎失败不启动本地模型。
|
||||
- 重连只针对同一引擎快照,并在可感知时显示状态。
|
||||
- 六个平台目标完成各自声明能力的真实采集、播放、取消和资源回收验证。
|
||||
- `npm test`、`npm run typecheck`、`npm run lint` 和生产构建全部通过。
|
||||
|
||||
---
|
||||
|
||||
## 23. 参考
|
||||
|
||||
- [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx):本地在线/离线 ASR、VAD 与 TTS。
|
||||
- [OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime):云端实时音频会话与 WebRTC/WebSocket。
|
||||
- [Gemini Live API](https://ai.google.dev/gemini-api/docs/live-api):云端双向实时音频与多模态会话。
|
||||
- [Qwen Realtime](https://help.aliyun.com/zh/model-studio/realtime):云端实时音视频输入与音频/文本输出。
|
||||
- [MiniCPM-o](https://github.com/OpenBMB/MiniCPM-V):本地端到端多模态与全双工候选。
|
||||
- [Moshi](https://github.com/kyutai-labs/moshi):本地原生全双工语音模型框架。
|
||||
- [PersonaPlex](https://github.com/NVIDIA/personaplex):本地可控角色与声音的全双工候选。
|
||||
- [AudioWorklet](https://developer.mozilla.org/en-US/docs/Web/API/AudioWorklet):Renderer 低延迟音频处理基础。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 应用内反馈
|
||||
|
||||
应用内反馈负责用户在 GoodBuddy 中提交反馈、附带受控诊断并获得提交状态。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | 产品流程、数据边界、接口、UI 接入和验收 |
|
||||
|
||||
现有文档是综合设计,尚未按标准职责拆分。
|
||||
@@ -0,0 +1,710 @@
|
||||
# GoodBuddy 应用内反馈系统设计与对接
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档类型 | 跨功能产品与技术架构 |
|
||||
| 状态 | 已实施并完成生产联调,待发布 |
|
||||
| 版本 | 1.0 |
|
||||
| 日期 | 2026-08-25 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 相关基线 | [统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义 GoodBuddy 桌面端的应用内反馈入口、数据边界、Renderer / Preload /
|
||||
Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平台服务端、管理后台、
|
||||
数据库、附件存储、部署和运营流程由独立项目维护,不在本文复制其内部设计。
|
||||
|
||||
本文是 **GoodBuddy 客户端实现的权威设计**。服务端字段、状态码和限制以反馈平台
|
||||
发布的版本化公共 API 契约为准;两边的同名契约必须通过兼容性测试保持一致,不能各自
|
||||
扩展后继续沿用相同的 `schemaVersion`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 摘要与核心决策
|
||||
|
||||
1. 用户在“设置 → 关于与更新”内直接提交问题、功能建议、体验意见或其他反馈,不再跳转
|
||||
GitHub Issue 页面。
|
||||
2. Renderer 只负责表单、截图预览和状态展示。所有网络请求、安装标识、应用版本和系统
|
||||
环境组装都在 Main 中完成。
|
||||
3. Preload 只暴露用途明确的 `feedback.submit()` 方法,不暴露任意 URL、通用
|
||||
`fetch`、Electron API、文件路径或服务端管理能力。
|
||||
4. 公共请求固定使用版本化接口 `POST /api/v1/feedback`,GoodBuddy 的
|
||||
`productKey` 固定为 `goodbuddy`。
|
||||
5. 反馈服务的生产 HTTPS Origin 是客户端源代码中的受信产品常量,不是用户设置,不从
|
||||
Renderer、项目、Runtime、环境变量、远程配置或服务端重定向中获取。
|
||||
6. GoodBuddy 当前的全局 TLS 策略允许模型和内网服务使用自签名或过期证书。反馈可能包含
|
||||
用户输入和截图,因此 **反馈客户端不得复用全局不校验证书的 Dispatcher 或
|
||||
`globalThis.fetch`**,必须使用独立的 Undici Dispatcher 并显式启用正常证书校验。
|
||||
7. 首版允许一张可选截图。截图只能由用户主动选择或粘贴,发送前必须预览并允许移除;
|
||||
GoodBuddy 不自动截屏。原始输入和 PNG 标准化结果都不能超过 5 MiB。
|
||||
8. Main 在发送前解码并重编码截图为 PNG,确保原始 EXIF/XMP 等元数据不会离开设备;
|
||||
服务端仍独立执行自己的图片验证和标准化。
|
||||
9. 一次新反馈生成一个 `clientRequestId`。网络失败后的手动重试必须复用原 ID,服务端
|
||||
以此保证幂等;创建另一条反馈时才生成新 ID。
|
||||
10. GoodBuddy 使用单独的随机安装 UUID 做服务端限流。该 UUID 不得复用 Agent
|
||||
installation ID、硬件 ID、机器名、用户名、项目 ID 或任何设备指纹。
|
||||
11. 默认不发送对话、提示词、模型回复、诊断、日志、文件、项目或工作区路径、知识库、模型
|
||||
配置、Runtime 配置、API Key、Token、Cookie、SSH 信息、剪贴板或屏幕内容。用户可通过
|
||||
默认关闭的 Checkbox 主动附加有界的最近桌面诊断摘要。
|
||||
12. 反馈提交失败时保留全部表单内容和截图,成功后显示可复制的服务端反馈编号。
|
||||
13. 首版不提供反馈历史、状态查询、编辑、撤回、自动日志收集或服务端管理入口。
|
||||
14. 生产接口固定为 `https://imp.mesalogo.com/api/v1/feedback`。域名健康检查、
|
||||
TLS、v1 请求校验和客户端调用链必须在发布前一起通过。
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标与非目标
|
||||
|
||||
### 2.1 用户目标
|
||||
|
||||
- 不离开 GoodBuddy 即可报告问题或提出建议。
|
||||
- 在发送前明确知道哪些字段会离开设备。
|
||||
- 只在主动选择后上传截图,并能检查、替换或移除截图。
|
||||
- 网络失败后无需重新填写内容。
|
||||
- 提交成功后获得稳定编号,便于后续沟通。
|
||||
|
||||
### 2.2 产品目标
|
||||
|
||||
- 让 GoodBuddy 成为独立多产品反馈平台的第一个客户端。
|
||||
- 保持 Electron 上下文隔离和 Main-only 网络边界。
|
||||
- 使用小而稳定的版本化契约,不把服务端工单、数据库和管理员概念泄漏到客户端。
|
||||
- 对 Windows、macOS、Linux 的 x64 与 arm64 使用相同行为。
|
||||
- 以数据最小化为默认,不把应用内反馈演变为隐式遥测或日志上传。
|
||||
|
||||
### 2.3 非目标
|
||||
|
||||
首版不包含:
|
||||
|
||||
- GitHub Issue 创建、GitHub Token 或 GitHub 网页附件上传。
|
||||
- GoodBuddy 内的管理员后台、处理状态修改或内部备注。
|
||||
- 用户账户、登录、反馈列表、远程状态查询或推送通知。
|
||||
- 自动附加当前对话、桌面诊断、日志、错误堆栈、项目、文件、模型或 Runtime 信息。
|
||||
- 远端 Agent 日志或通用日志、诊断包和附件框架。
|
||||
- 自动截取全屏、当前窗口、浏览器、远程桌面或其他应用窗口。
|
||||
- 多截图、视频、录屏、任意文件附件或压缩包。
|
||||
- 离线队列、后台自动重试、指数退避、跨重启草稿或发送历史。
|
||||
- 用户可编辑的反馈 Endpoint、证书豁免、代理凭据或服务端 API Key。
|
||||
- 由 GoodBuddy 推断优先级、自动分类、自动生成描述或代表用户提交。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户体验
|
||||
|
||||
### 3.1 入口
|
||||
|
||||
入口放在现有“关于与更新”页面,不增加新的设置一级分类。
|
||||
|
||||
在当前版本与更新卡片之后增加“帮助改进 GoodBuddy”能力卡:
|
||||
|
||||
- 标题:`帮助改进 GoodBuddy`
|
||||
- 说明:`报告问题、提出建议或反馈使用体验,我们会在独立反馈系统中处理。`
|
||||
- 主操作:`提交反馈`
|
||||
|
||||
该卡片不显示反馈平台内部状态、管理员信息或 GitHub 链接。服务不可用时仍允许用户打开
|
||||
表单和保留输入;提交失败后提供重试,而不是把用户重定向到另一个渠道。
|
||||
|
||||
### 3.2 反馈对话框
|
||||
|
||||
对话框字段按以下顺序排列:
|
||||
|
||||
1. **反馈类型**,必填:
|
||||
- 问题:`bug`
|
||||
- 功能建议:`feature`
|
||||
- 使用体验:`experience`
|
||||
- 其他:`other`
|
||||
2. **标题**,必填,1 至 120 字符。
|
||||
3. **详细描述**,必填,去除首尾空白后 10 至 5,000 字符;选择桌面诊断时为摘要预留空间,
|
||||
用户描述上限为 3,398 字符。
|
||||
4. **附加最近桌面诊断记录**,可选的原生 Checkbox,默认关闭。
|
||||
5. **联系邮箱**,可选,最多 254 字符。
|
||||
6. **截图**,可选,最多一张,支持选择 PNG/JPEG/WebP 或在对话框中粘贴图片。
|
||||
7. **将发送的信息**,只读显示应用版本、系统、架构和界面语言。
|
||||
8. **隐私说明**,紧邻提交操作。
|
||||
|
||||
隐私说明使用明确文案:
|
||||
|
||||
> 将发送反馈类型、标题、描述、可选邮箱、GoodBuddy 版本、操作系统、架构、界面语言,
|
||||
> 以及你主动添加的截图。默认不上传桌面诊断;只有勾选后才会把有界诊断摘要追加到描述。
|
||||
> 不会发送对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
|
||||
|
||||
Checkbox 下方持续说明:诊断摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息。
|
||||
Renderer 不预览、不读取也不接收诊断内容或路径。勾选后详细描述上限为 3,398 字符,为附加
|
||||
摘要预留 1,600 字符以及分隔换行;超长草稿保留在表单中并显示字段错误,不静默截断。
|
||||
|
||||
截图区域必须包含:
|
||||
|
||||
- 缩略预览。
|
||||
- 尺寸和所选文件大小;Main 仍在发送前独立执行 PNG 标准化和大小检查。
|
||||
- `替换截图` 与 `移除截图`。
|
||||
- `截图可能包含画面中可见的个人信息,请在发送前检查。`
|
||||
|
||||
### 3.3 提交状态
|
||||
|
||||
| 状态 | 行为 |
|
||||
| --- | --- |
|
||||
| 未填写 | 字段保持可编辑,提交按钮按字段有效性启用 |
|
||||
| 校验失败 | 在对应字段旁显示错误并把焦点移到第一个无效字段 |
|
||||
| 提交中 | 禁止重复提交、关闭和修改字段,按钮显示“正在提交…” |
|
||||
| 网络或服务失败 | 保留字段和截图,在表单内显示可操作错误及“重试” |
|
||||
| 成功 | 对话框切换为确认面板,显示并允许复制反馈编号 |
|
||||
|
||||
提交成功属于必须保留编号的上下文结果,不再同时发送一条重复的全局成功通知。异步失败也
|
||||
保留在对话框内,因为错误与当前草稿和重试操作直接关联。
|
||||
|
||||
### 3.4 可访问性
|
||||
|
||||
- 对话框使用 `role="dialog"`、`aria-modal="true"` 和可见标题。
|
||||
- 复用 `activateModalFocus()` 与 `trapTabFocus()`,打开后聚焦反馈类型,关闭后恢复入口
|
||||
按钮焦点。
|
||||
- 非提交状态支持 Escape 关闭;提交中保持对话框打开,直到请求完成或到达有界超时。
|
||||
- 字段使用原生 `label`、`select`、`input` 和 `textarea`。
|
||||
- 图片预览提供说明性替代文本,删除和替换按钮有明确可访问名称。
|
||||
- 加载、失败和成功状态使用文字,不依赖颜色。
|
||||
- 浅色、深色和 `prefers-reduced-motion` 行为遵循 `UI-DESIGN.md`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据与隐私边界
|
||||
|
||||
### 4.1 明确发送的数据
|
||||
|
||||
| 数据 | 来源 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `schemaVersion` | Main 常量 | 选择公共契约版本 |
|
||||
| `productKey` | Main 常量 `goodbuddy` | 服务端产品路由 |
|
||||
| `category` | 用户选择 | 分流反馈 |
|
||||
| `title` | 用户输入 | 摘要 |
|
||||
| `description` | 用户输入;勾选时由 Main 追加桌面诊断摘要 | 问题、建议和主动提供的有界诊断详情 |
|
||||
| `contactEmail` | 用户可选输入 | 必要时联系用户 |
|
||||
| `environment.appVersion` | `app.getVersion()` | 判断受影响版本 |
|
||||
| `environment.platform` | `process.platform` 映射 | 判断受影响平台 |
|
||||
| `environment.architecture` | `process.arch` 映射 | 判断受影响架构 |
|
||||
| `environment.locale` | 当前 GoodBuddy UI 语言 | 理解用户语言环境 |
|
||||
| `installationId` | Main 本地随机 UUID | 服务端安装实例限流 |
|
||||
| `clientRequestId` | 当前草稿随机 UUID | 幂等和安全重试 |
|
||||
| `screenshot` | 用户主动选择或粘贴 | 复现视觉问题 |
|
||||
|
||||
客户端不主动发送来源 IP;IP 是 HTTPS 请求到达服务端时产生的网络元数据。服务端如何保存、
|
||||
散列和保留该元数据由独立反馈平台的隐私与运维策略负责。
|
||||
|
||||
用户填写联系邮箱后,邮箱会作为业务字段通过 HTTPS 发送并由反馈平台保存,不属于端到端
|
||||
加密内容。用户不希望提供联系方式时应留空,GoodBuddy 不尝试从系统账户或其他设置推断
|
||||
邮箱。
|
||||
|
||||
### 4.2 默认不发送的数据
|
||||
|
||||
以下数据不能因为“有助于复现”而隐式加入请求:
|
||||
|
||||
- 当前或历史对话、提示词、模型回复和 Supervisor 内容。
|
||||
- GoodBuddy、Runtime、Agent、SSH、消息通道、浏览器或操作系统日志。
|
||||
- 文件内容、附件、知识库、Magic Notes、成果和工作区改动。
|
||||
- 文件名、项目名、本地路径、远程路径、主机名、用户名或 IP 配置。
|
||||
- LLM、向量模型、语音模型、OCR、MCP、Skill、Runtime 或通道配置。
|
||||
- API Key、Access Token、Cookie、SSH 密码、私钥、代理凭据和其他秘密。
|
||||
- 剪贴板、屏幕、摄像头、麦克风或其他应用内容。
|
||||
- Remote Agent installation ID、Runtime ID、Conversation ID、Task ID 或 Project ID。
|
||||
|
||||
除上述用户主动选择、追加到描述且有界的桌面诊断摘要外,未来若增加日志、诊断包或其他
|
||||
附件,必须单独设计字段、预览、大小限制、用户选择和服务端保留策略,不能复用截图字段
|
||||
静默上传。
|
||||
|
||||
### 4.3 本地数据生命周期
|
||||
|
||||
- 表单草稿、邮箱和截图只保存在当前 Renderer 内存中。
|
||||
- 首版不把草稿、失败请求、成功响应或截图写入 GoodBuddy 数据库和设置文件。
|
||||
- Main 只在请求期间持有标准化图片字节,请求结束后释放引用,不创建临时图片文件。
|
||||
- 成功后关闭确认面板时清空草稿。
|
||||
- `installationId` 单独保存在用户数据目录中的
|
||||
`feedback-identity.json`,格式为版本化 JSON,文件权限沿用私有原子设置文件机制。
|
||||
- “清除本地数据”必须中止进行中的反馈请求并删除该身份文件;下次提交会生成新 UUID。
|
||||
|
||||
---
|
||||
|
||||
## 5. 公共 API 对接
|
||||
|
||||
### 5.1 外部契约所有权
|
||||
|
||||
反馈平台负责维护公共 API 的权威定义。GoodBuddy 只固定客户端实际需要的版本 1 子集:
|
||||
|
||||
```text
|
||||
POST https://imp.mesalogo.com/api/v1/feedback
|
||||
```
|
||||
|
||||
当前生产 Endpoint 已作为 Main-only 产品常量固定。实现和发布验证必须持续保证:
|
||||
|
||||
- 使用 `https:`。
|
||||
- URL 不含用户名、密码、查询参数或 fragment。
|
||||
- Path 精确为 `/api/v1/feedback`。
|
||||
- 对应产品 `goodbuddy` 已在服务端启用。
|
||||
- 桌面端所在网络能够直接访问该 Origin。
|
||||
|
||||
Renderer、Preload、用户设置、环境变量和远程配置都不能覆盖该 Endpoint。域名迁移必须
|
||||
同时修改客户端受信常量、测试、服务端 Origin 配置和部署文档。
|
||||
|
||||
### 5.2 请求 JSON
|
||||
|
||||
无截图时发送 `application/json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"productKey": "goodbuddy",
|
||||
"category": "bug",
|
||||
"title": "魔法笔记页签不可见",
|
||||
"description": "平台功能中的魔法笔记页签被布局压缩,无法看到或点击。",
|
||||
"contactEmail": "user@example.com",
|
||||
"environment": {
|
||||
"appVersion": "0.11.0",
|
||||
"platform": "windows",
|
||||
"architecture": "x64",
|
||||
"locale": "zh-CN"
|
||||
},
|
||||
"installationId": "00000000-0000-4000-8000-000000000101",
|
||||
"clientRequestId": "00000000-0000-4000-8000-000000000102"
|
||||
}
|
||||
```
|
||||
|
||||
有截图时发送 `multipart/form-data`:
|
||||
|
||||
- `payload`:同一 JSON 的字符串形式。
|
||||
- `screenshot`:一张标准化 PNG,文件名固定为 `feedback.png`。
|
||||
|
||||
`contactEmail` 为空时省略,不发送空字符串。请求对象不附加服务端未声明字段。
|
||||
|
||||
### 5.3 成功响应
|
||||
|
||||
HTTP 201 表示首次创建,HTTP 200 表示相同 `clientRequestId` 的幂等结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"reference": "GOODBUDDY-000001",
|
||||
"duplicate": false
|
||||
}
|
||||
```
|
||||
|
||||
客户端对两种成功状态使用相同确认界面。`duplicate: true` 不能显示为失败,它表示先前请求
|
||||
已经成功落库。
|
||||
|
||||
### 5.4 错误映射
|
||||
|
||||
| HTTP / 错误 | 用户结果 |
|
||||
| --- | --- |
|
||||
| 400 | 提示提交内容无效,保留草稿;同时记录为客户端契约缺陷 |
|
||||
| 403 | 提示客户端与反馈服务配置不匹配 |
|
||||
| 404 | 提示反馈服务暂不可用 |
|
||||
| 413 | 提示截图或请求过大,允许移除截图后重试 |
|
||||
| 429 | 提示提交过于频繁,保留草稿并让用户稍后手动重试 |
|
||||
| 500–599 | 提示服务暂时异常,保留草稿 |
|
||||
| 超时 / DNS / TLS / 离线 | 提示无法连接反馈服务,保留草稿 |
|
||||
| 响应格式错误 | 提示服务返回无效结果,不能猜测成功 |
|
||||
| 本地诊断读取失败 | 不发送反馈,保留草稿、勾选状态和请求 ID,提示重试或取消勾选 |
|
||||
|
||||
首版不自动重试。自动重试容易让用户无法判断是否已经发送,也会在服务持续异常时制造额外
|
||||
流量。用户点击“重试”时复用同一个 `clientRequestId`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 进程与模块设计
|
||||
|
||||
### 6.1 调用链
|
||||
|
||||
```text
|
||||
UpdateSettingsSection / FeedbackDialog
|
||||
→ window.goodbuddy.feedback.submit(input)
|
||||
→ preload: ipcRenderer.invoke(feedbackSubmit)
|
||||
→ Main IPC: trusted sender + Zod parse
|
||||
→ FeedbackService: identity + environment + screenshot normalization
|
||||
→ StrictFeedbackHttpClient
|
||||
→ POST /api/v1/feedback
|
||||
→ response schema parse
|
||||
→ Renderer confirmation or inline retry state
|
||||
```
|
||||
|
||||
### 6.2 Shared
|
||||
|
||||
新增 `src/shared/feedback-contracts.ts`,包含:
|
||||
|
||||
- `feedbackCategorySchema`
|
||||
- `feedbackDraftSchema`
|
||||
- `feedbackScreenshotInputSchema`
|
||||
- `feedbackSubmitInputSchema`
|
||||
- `feedbackSubmitResultSchema`
|
||||
- `feedbackPublicPayloadSchema`
|
||||
- 公共字段长度和截图限制常量
|
||||
|
||||
推荐客户端 IPC 输入:
|
||||
|
||||
```ts
|
||||
type FeedbackSubmitInput = {
|
||||
category: 'bug' | 'feature' | 'experience' | 'other'
|
||||
title: string
|
||||
description: string
|
||||
includeDiagnostics: boolean
|
||||
contactEmail?: string
|
||||
locale: 'zh-CN' | 'en-US'
|
||||
clientRequestId: string
|
||||
screenshot?: {
|
||||
data: Uint8Array
|
||||
mimeType: 'image/png' | 'image/jpeg' | 'image/webp'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Renderer 不能传入 `productKey`、`appVersion`、`platform`、`architecture` 或
|
||||
`installationId`,也不能传入诊断内容或路径。这些字段由 Main 从受信运行环境生成。
|
||||
|
||||
### 6.3 IPC 与 Preload
|
||||
|
||||
新增:
|
||||
|
||||
```text
|
||||
ipcChannels.feedbackSubmit = "feedback:submit"
|
||||
```
|
||||
|
||||
`DesktopApi` 新增必选窄接口:
|
||||
|
||||
```ts
|
||||
feedback: {
|
||||
submit: (input: FeedbackSubmitInput) => Promise<FeedbackSubmitResult>
|
||||
}
|
||||
```
|
||||
|
||||
Main handler 必须:
|
||||
|
||||
1. 调用现有 `assertTrustedSender(event, window)`。
|
||||
2. 使用 Shared Zod schema 解析 `unknown` 输入。
|
||||
3. 不接收 URL、文件路径、Cookie、Header 或任意请求选项。
|
||||
4. 由 `FeedbackService` 跟踪活动 Promise,应用关闭或清除数据时先取消再有界等待。
|
||||
|
||||
Preload 只转发已声明输入,不做网络请求或图片文件读取。
|
||||
|
||||
反馈 handler 独立放在 `feedback/feedback-ipc.ts`,复用共享的
|
||||
`assertTrustedSender()`,避免继续扩展通用 `registerIpcHandlers()` 的位置参数列表。
|
||||
|
||||
### 6.4 FeedbackIdentityStore
|
||||
|
||||
新增 Main-only `FeedbackIdentityStore`:
|
||||
|
||||
- 文件:`<userData>/feedback-identity.json`
|
||||
- Schema:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"installationId": "00000000-0000-4000-8000-000000000101"
|
||||
}
|
||||
```
|
||||
|
||||
- 缺失时使用 `crypto.randomUUID()` 创建。
|
||||
- 使用 `writeJsonFileAtomically()` 写入 0600 临时文件并原子替换。
|
||||
- 读取时使用严格 schema。
|
||||
- 损坏时隔离原文件并生成新身份,不读取或复用 Remote Agent 身份。
|
||||
- 并发首次读取必须合并为同一个 Promise,不能生成两个 UUID。
|
||||
- `clear()` 删除文件和内存缓存,供“清除本地数据”调用。
|
||||
|
||||
该 UUID 是可重置的应用安装级随机标识,不声称代表真实设备或用户。
|
||||
|
||||
### 6.5 FeedbackService
|
||||
|
||||
Main-only `FeedbackService` 负责:
|
||||
|
||||
- 读取或创建安装 UUID。
|
||||
- 映射应用版本、平台和架构。
|
||||
- 验证并标准化可选截图。
|
||||
- 组装严格的公共 payload。
|
||||
- 仅在 `includeDiagnostics: true` 时通过注入的 DesktopDiagnostics provider 读取最多 20 条
|
||||
最近记录;默认提交完全不调用诊断 provider。
|
||||
- 将诊断格式化为带
|
||||
`[GOODBUDDY_DESKTOP_DIAGNOSTICS_V1_BEGIN]` /
|
||||
`[GOODBUDDY_DESKTOP_DIAGNOSTICS_V1_END]` 明确分隔、最多 1,600 字符且只保留完整 JSON
|
||||
行的摘要。无记录时写入固定 `{"status":"no-recent-diagnostics"}` 事实。
|
||||
- 调用 HTTP Client。
|
||||
- 维持单次提交的 AbortController,并在 dispose / 清除本地数据时取消。
|
||||
|
||||
平台映射固定为:
|
||||
|
||||
| Node 值 | API 值 |
|
||||
| --- | --- |
|
||||
| `win32` | `windows` |
|
||||
| `darwin` | `macos` |
|
||||
| `linux` | `linux` |
|
||||
| 其他 | `unknown` |
|
||||
|
||||
架构只保留 `x64` 和 `arm64`,其他值映射为 `unknown`。
|
||||
|
||||
公共 API 继续使用 `schemaVersion: 1` 和原有固定字段。`includeDiagnostics` 只属于客户端
|
||||
IPC 输入,不进入公共 payload,也不新增 multipart 字段;Main 把摘要追加到
|
||||
`payload.description`。默认未勾选时,公共 payload 与既有实现逐字段一致。诊断读取失败时
|
||||
不得发送部分摘要或声称已附加,必须返回可本地化的 `diagnostics-unavailable` 错误。
|
||||
|
||||
### 6.6 严格 HTTPS Client
|
||||
|
||||
反馈客户端使用 `undici` 的独立 `Agent`:
|
||||
|
||||
```ts
|
||||
new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
这是必要边界,不是额外配置。GoodBuddy 的 `GlobalTlsPolicy` 会给模型、MCP 和内网服务
|
||||
安装不校验证书的全局 Dispatcher;反馈内容不能经过该 Dispatcher。
|
||||
|
||||
每次请求必须满足:
|
||||
|
||||
- 固定、预先校验的 HTTPS URL。
|
||||
- 使用 Undici 原始 `request()`,不安装重定向处理器;任何 3xx 都按服务不可用处理。
|
||||
- 不使用 Cookie Jar、缓存、Referrer 或浏览器凭据模式。
|
||||
- `Accept: application/json`。
|
||||
- 固定、非敏感 `User-Agent`,例如 `GoodBuddy-Feedback/<version>`。
|
||||
- 不发送 `Authorization`、Cookie、API Key 或自定义代理凭据。
|
||||
- 总超时建议 15 秒,最大不得超过 30 秒。
|
||||
- 成功响应体最多 16 KiB,并检查流式实际字节数,不能只相信
|
||||
`Content-Length`。
|
||||
- 只接受 JSON `Content-Type` 和 JSON 响应体,并使用
|
||||
`feedbackSubmitResultSchema` 解析。
|
||||
- 日志只记录有界错误类别和 HTTP 状态,不记录请求正文、邮箱、截图、安装 UUID、
|
||||
`clientRequestId` 或响应编号。
|
||||
|
||||
测试可以注入 Endpoint、Dispatcher 和 Request Transport。生产构造路径不得读取测试覆盖值或
|
||||
环境变量。
|
||||
|
||||
---
|
||||
|
||||
## 7. 截图处理
|
||||
|
||||
### 7.1 Renderer
|
||||
|
||||
- 文件输入限制为 `.png,.jpg,.jpeg,.webp`。
|
||||
- 原始文件上限固定为 5 MiB,超过后不读取完整内容或调用 IPC。
|
||||
- 粘贴事件只处理图片,不拦截普通文本粘贴。
|
||||
- 读取 `File.arrayBuffer()` 后先执行 Shared 字节上限校验。
|
||||
- 使用临时 Object URL 预览,替换、移除或关闭对话框时立即
|
||||
`URL.revokeObjectURL()`。
|
||||
- 不调用 `webUtils.getPathForFile()`,不把本地文件路径传给 Main 或服务端。
|
||||
|
||||
### 7.2 Main
|
||||
|
||||
Main 不信任 Renderer 声明的 MIME:
|
||||
|
||||
1. 校验 PNG、JPEG、WebP 文件签名。
|
||||
2. 使用 `nativeImage.createFromBuffer()` 实际解码。
|
||||
3. 拒绝空图片、异常尺寸、超过 API 公开上限的像素数。
|
||||
4. 使用 `toPNG()` 重编码,去除原始元数据。
|
||||
5. 检查标准化后字节数不超过 5 MiB,最大边长不超过 8,192 像素,总像素数不超过
|
||||
3,200 万。
|
||||
6. 仅把标准化 PNG 放入当前网络请求,不写入磁盘。
|
||||
|
||||
客户端和服务端都执行图片验证属于有意的纵深防御:客户端保证原始元数据不离开设备,
|
||||
服务端保证不能信任任意客户端。
|
||||
|
||||
---
|
||||
|
||||
## 8. Renderer 实施落点
|
||||
|
||||
### 8.1 组件
|
||||
|
||||
- `UpdateSettingsSection.tsx`
|
||||
- 继续负责应用信息和更新卡片。
|
||||
- 渲染反馈入口卡片。
|
||||
- `FeedbackDialog.tsx`
|
||||
- 独立管理草稿、预览、校验、提交、重试和成功编号。
|
||||
- 关闭后销毁草稿状态。
|
||||
对话框不与更新检查状态共用 `saving`、`checking` 或 `error`,避免一个功能失败覆盖另一个
|
||||
功能的状态。
|
||||
|
||||
### 8.2 国际化
|
||||
|
||||
在 `settingsSections` 的 `zh-CN` 与 `en-US` 资源中增加同构键:
|
||||
|
||||
- 入口卡片标题、说明和按钮。
|
||||
- 类型名称。
|
||||
- 字段标签、占位、帮助和校验。
|
||||
- 截图选择、替换、移除和隐私提醒。
|
||||
- 提交中、失败、重试、成功和复制编号。
|
||||
- 数据发送说明。
|
||||
|
||||
组件不得内嵌只存在于一种语言的产品文案。
|
||||
|
||||
### 8.3 样式
|
||||
|
||||
- 复用 `capability-card`、`field`、`primary-button`、`secondary-button` 和现有
|
||||
对话框层级。
|
||||
- 新样式使用 `UI-DESIGN.md` 的语义颜色、间距、圆角和动效令牌。
|
||||
- 对话框桌面端最大宽度建议 600px;窄窗口占可用宽度并保持 16px 页面边距。
|
||||
- 不为反馈入口创建新的一级页面、页签视觉或高饱和营销卡片。
|
||||
|
||||
---
|
||||
|
||||
## 9. 失败、生命周期与恢复
|
||||
|
||||
### 9.1 幂等
|
||||
|
||||
- 打开新草稿时生成 `clientRequestId`。
|
||||
- 字段编辑、截图替换、网络失败和手动重试都不改变 ID。
|
||||
- 成功或显式丢弃草稿后,下一条反馈生成新 ID。
|
||||
- 同一 ID 返回原编号时按成功处理。
|
||||
|
||||
### 9.2 并发
|
||||
|
||||
- 对话框同一时间只允许一个提交。
|
||||
- Main 同一 `clientRequestId` 只运行一个请求。
|
||||
- Main 全局同一时间只发送一条反馈;相同 ID 的重复调用合并,不同 ID 在已有请求期间返回
|
||||
有界 `busy` 结果,不能并行保留多份 5 MiB 截图。
|
||||
- 读取截图使用选择代次;旧文件后完成时必须丢弃并释放 Object URL,不能覆盖较新的截图。
|
||||
|
||||
### 9.3 应用退出与清除数据
|
||||
|
||||
- 反馈请求有独立 AbortController。
|
||||
- 应用退出时取消进行中的请求并关闭严格 TLS Dispatcher。
|
||||
- “清除本地数据”先取消请求,再删除 `feedback-identity.json`。
|
||||
- 失败或取消不写入本地“已发送”状态,服务端是否已接收由同 ID 重试确认。
|
||||
|
||||
### 9.4 服务不可用
|
||||
|
||||
反馈平台不可用时:
|
||||
|
||||
- 保留表单和截图。
|
||||
- 显示明确网络或服务错误。
|
||||
- 不静默丢弃、不自动切换 GitHub、不打开网页、不自动重试。
|
||||
- 其他 GoodBuddy 功能继续可用。
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施文件
|
||||
|
||||
当前实现涉及:
|
||||
|
||||
| 文件或目录 | 变更 |
|
||||
| --- | --- |
|
||||
| `src/shared/feedback-contracts.ts` | 输入、公共 payload、响应和限制 schema |
|
||||
| `src/shared/ipc-channels.ts` | `feedbackSubmit` channel |
|
||||
| `src/shared/contracts.ts` | `DesktopApi.feedback` |
|
||||
| `src/main/feedback/feedback-identity-store.ts` | 随机安装 ID |
|
||||
| `src/main/feedback/feedback-screenshot.ts` | 签名、解码和 PNG 标准化 |
|
||||
| `src/main/feedback/feedback-http-client.ts` | 固定 Endpoint、严格 TLS、有界响应 |
|
||||
| `src/main/feedback/feedback-service.ts` | payload 组装与生命周期 |
|
||||
| `src/main/feedback/feedback-ipc.ts` | 独立 trusted sender、schema parse 和 submit handler |
|
||||
| `src/main/trusted-ipc-sender.ts` | Main IPC 共享 sender / main Frame 校验 |
|
||||
| `src/main/index.ts` | Main service 构造、清理和 IPC 注入 |
|
||||
| `src/preload/index.ts` | 窄 `feedback.submit()` bridge |
|
||||
| `src/renderer/src/FeedbackDialog.tsx` | 表单和成功/失败流程 |
|
||||
| `src/renderer/src/UpdateSettingsSection.tsx` | About 入口卡片 |
|
||||
| `src/renderer/src/i18n/locales/*/settingsSections.ts` | 中英文文案 |
|
||||
| `src/renderer/src/styles.css` | 令牌化布局与响应式样式 |
|
||||
|
||||
每个生产文件应有相邻或现有测试文件覆盖。不要把 Endpoint、截图处理或网络逻辑塞进
|
||||
`ipc.ts` 或 React 组件。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验证
|
||||
|
||||
以下 Shared、Main、IPC、Preload 和 Renderer 行为均由相邻或现有自动化测试覆盖;生产
|
||||
Endpoint 的健康检查和无副作用 schema 探针也必须在发布前通过。
|
||||
|
||||
### 11.1 Shared 合同
|
||||
|
||||
- 接受四种反馈类型和有效字段。
|
||||
- 拒绝未知字段、空标题、短描述、超长描述、错误邮箱、非 UUID 和多截图。
|
||||
- 对截图按字节限制,而不是字符串长度限制。
|
||||
- 请求与反馈平台 `schemaVersion: 1` fixture 双向兼容。
|
||||
|
||||
### 11.2 Main 单元测试
|
||||
|
||||
- 安装 ID 首次创建、并发读取、持久化、损坏隔离和清除。
|
||||
- 不复用 Agent、Runtime、Project 或硬件身份。
|
||||
- 平台和架构映射。
|
||||
- PNG/JPEG/WebP 签名、实际解码、尺寸限制、PNG 重编码和元数据不保留。
|
||||
- 无截图 JSON、有截图 multipart、请求 Header 和固定 URL。
|
||||
- 独立 Dispatcher 显式 `rejectUnauthorized: true`。
|
||||
- 拒绝重定向、非 HTTPS、错误 Origin、超时、超大响应、非 JSON 和错误 response schema。
|
||||
- 4xx、429、5xx 和网络错误映射。
|
||||
- 手动重试复用 `clientRequestId`。
|
||||
- 不同 ID 的并发请求被有界拒绝,清除本地数据期间不能开始新提交。
|
||||
|
||||
### 11.3 IPC 与 Preload
|
||||
|
||||
- 未知窗口和非主 Frame 被拒绝。
|
||||
- Renderer 不能覆盖 `productKey`、版本、平台、架构、安装 ID、Endpoint 或 Header。
|
||||
- Preload 只暴露 `feedback.submit()`。
|
||||
- Sandbox 测试验证没有通用网络或文件路径能力泄漏。
|
||||
|
||||
### 11.4 Renderer
|
||||
|
||||
- About 页面展示反馈卡片。
|
||||
- 表单字段、字符限制、中英文文案和隐私说明。
|
||||
- 普通文本粘贴不被图片处理拦截。
|
||||
- 截图选择、预览、替换、移除和 Object URL 清理。
|
||||
- 并发截图读取按最新选择生效,关闭对话框后不会留下待处理 Object URL。
|
||||
- 失败保留字段、诊断勾选状态,重试使用原 `clientRequestId`。
|
||||
- 默认提交不读取诊断且公共 payload 不变;主动附加、描述预算、无记录和读取失败均有覆盖。
|
||||
- 成功显示真实编号并清空后续新草稿。
|
||||
- Dialog role、初始焦点、Tab 循环、Escape 和焦点恢复。
|
||||
- 浅色、深色、窄窗口和 reduced motion。
|
||||
|
||||
### 11.5 真实集成
|
||||
|
||||
2026-08-25 已从隔离配置的真实 Electron 生产构建完成一次端到端合成提交:
|
||||
|
||||
- 标题:`[GOODBUDDY INTEGRATION TEST] 2026-08-25`
|
||||
- 类型:`other`
|
||||
- 联系邮箱:未填写
|
||||
- 截图:GoodBuddy 公共应用图标,经 Main 重编码后以 PNG multipart 上传
|
||||
- 服务端编号:`GOODBUDDY-000001`
|
||||
|
||||
该结果确认 About UI、Renderer、Preload、trusted IPC、安装身份、Main 图片处理、独立严格
|
||||
TLS Dispatcher 和生产 v1 API 调用链可以共同工作。发布候选仍应按以下清单复验:
|
||||
|
||||
1. 从打包候选的 About 页面提交一条明确标记的合成反馈。
|
||||
2. 可选添加一张不含个人数据的测试截图。
|
||||
3. 确认客户端显示的编号与管理后台一致。
|
||||
4. 确认版本、平台、架构和语言正确。
|
||||
5. 确认服务端只保存标准化 PNG,不含原始图片元数据。
|
||||
6. 断网后提交,确认草稿保留;恢复网络后以同一 ID 重试且只产生一条记录。
|
||||
7. 使用无效证书的测试服务,确认反馈客户端拒绝连接,即使 GoodBuddy 全局内网 TLS
|
||||
策略允许其他服务继续工作。
|
||||
8. 检查 Renderer 和 Main 日志不含正文、邮箱、截图、安装 ID 或凭据。
|
||||
|
||||
最终仍需运行仓库统一验证:
|
||||
|
||||
```text
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 发布与兼容性
|
||||
|
||||
- `schemaVersion: 1` 与 `/api/v1/feedback` 是同一版本边界。
|
||||
- 向请求增加必填字段、修改枚举或改变字段语义属于破坏性变更,必须发布新 schema 或新
|
||||
API 版本。
|
||||
- GoodBuddy 对响应执行严格解析。服务端需要扩展响应时,应先协调客户端兼容策略。
|
||||
- Endpoint 域名变化属于客户端发布变更,不能依赖 301/302 迁移。
|
||||
- 入口发布与 `goodbuddy` 产品启用状态、生产 TLS 和 v1 契约保持同一发布门槛。
|
||||
- 此功能不迁移现有用户数据;只新增可重置的随机反馈安装身份。
|
||||
- 发布说明应明确:用户现在可以在“关于与更新”内直接提交反馈,邮箱和截图可选;桌面诊断
|
||||
默认不上传,只有用户勾选后才附加有界摘要,并且不会发送对话、Prompt、凭据、文件内容、
|
||||
路径、Provider 原始响应或远端 Agent 日志。
|
||||
|
||||
---
|
||||
|
||||
## 13. 当前对接状态
|
||||
|
||||
截至 2026-08-25:
|
||||
|
||||
1. 生产 Origin 已确定为 `https://imp.mesalogo.com`,HTTPS、HSTS 和健康检查正常。
|
||||
2. `/api/v1/feedback` 已返回符合 v1 契约的严格校验错误,无副作用探针未创建记录。
|
||||
3. `goodbuddy` 产品已启用,实际提交返回 `GOODBUDDY-000001`。
|
||||
4. GoodBuddy 客户端已完成 Shared、Main、IPC、Preload、About UI、双语文案、截图和
|
||||
生命周期实现。
|
||||
5. 服务端公开 schema、字段长度和 5 MiB 截图上限与客户端一致。
|
||||
6. 隔离 Electron 实例已完成浅色、深色、600×800 窄窗口、焦点与背景隔离、截图预览和
|
||||
真实生产提交验收。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 知识库
|
||||
|
||||
知识库负责资料导入、解析、分块、索引、检索、图谱和带来源的知识问答。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [知识库检索与分块增强 PRD](./prd.md) | 检索、分块、诊断和产品验收 |
|
||||
| [User Stories](./user-stories.md) | 知识使用者、维护者和调试者场景 |
|
||||
| [本地文本向量技术设计](./local-embedding-technical-design.md) | 本地与兼容向量连接、模型包、索引和进程边界 |
|
||||
| [知识检索评估](./retrieval-evaluation.md) | 检索质量评估方法、数据集、指标和门槛 |
|
||||
|
||||
当前尚未拆出统一功能逻辑、UI 设计和功能进度文档。
|
||||
File diff suppressed because it is too large
Load Diff
+50
-5
@@ -5,10 +5,11 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 实施中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-11 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-08-22 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 实施范围 | 第一阶段:可用、可见、可诊断;第二阶段:可调、可优化、可维护 |
|
||||
| 相关设计 | [本地文本向量模型与连接设计](./local-embedding-technical-design.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
@@ -38,7 +39,8 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
3. 保留“模型按需检索”,并新增“每次先检索”模式。后者必须由 Main 进程
|
||||
预检索,不能只依赖提示词要求模型调用工具。
|
||||
4. 知识库新建后不默认启用全部已有知识库;对话中的范围继续由用户显式选择。
|
||||
5. 向量服务不可用时保留全文检索,但必须返回明确降级状态。
|
||||
5. 向量服务不可用时保留全文与中文检索,但必须返回明确降级状态。这是可见的检索通道
|
||||
降级,不得自动切换内置连接、用户添加的 OpenAI 兼容连接或其他向量模型。
|
||||
6. 中文召回使用应用内可控的 CJK n-gram 索引,不新增远程服务依赖。
|
||||
7. 混合检索保留 RRF 候选融合,并增加本地确定性重排、可选的
|
||||
Cohere/Jina 兼容学习型重排、最低相关度和上下文预算。学习型重排失败时
|
||||
@@ -46,7 +48,9 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
8. 向量搜索取消 5,000 分块静默失效,使用有界内存的分页扫描。在没有稳定
|
||||
跨平台向量扩展前,接受本地 CPU 线性扫描,并持续显示性能诊断。
|
||||
9. 向量索引兼容性同时校验 Provider、Model、维度和 Provider Fingerprint。
|
||||
同名模型切换端点后,旧向量不能继续参与召回。
|
||||
同名模型切换端点后,旧向量不能继续参与召回。Fingerprint 的完整模型、编码与
|
||||
数据路径定义以[本地文本向量模型与连接设计](./local-embedding-technical-design.md)
|
||||
为准。
|
||||
10. 失败或取消的重建不能停用上一版已就绪索引。新索引只有完整校验成功后才
|
||||
原子替换当前服务版本。
|
||||
11. 分块设置属于知识库,修改后不会伪装为立即生效。用户需要显式重建索引。
|
||||
@@ -56,6 +60,12 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
用户配置决定。
|
||||
14. Ask 的运行时边界保持只读。知识库内容始终被标记为不可信证据,
|
||||
不能成为系统指令。
|
||||
15. 向量连接是 GoodBuddy Main 管理的知识服务依赖,不是 Agent Runtime 模型。它不进入
|
||||
直连模型、OpenCode、Continue、DeepSeek Harness、远程 Runtime 或项目 Runtime 的
|
||||
模型选择、配置和凭据边界。
|
||||
16. Runtime 只能获得 Main 检索后选出的有界文本证据、来源引用和脱敏诊断,不能获得
|
||||
Embedding Provider、连接 Profile、Endpoint、API Key、模型目录、Tokenizer、原始向量
|
||||
或索引 Fingerprint。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
@@ -98,6 +108,9 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
- 自动问题生成、FAQ 生成和训练数据标注平台。
|
||||
- 完整 RAG 离线评测平台。第二阶段只提供手动检索测试与可导出的诊断信息。
|
||||
- 在应用内高保真渲染所有原始 Office 和 PDF 文档。
|
||||
- 把向量连接用作 LLM、Agent Runtime 模型或 Runtime 模型网关。
|
||||
- 让 Runtime 直接调用 Embeddings Endpoint、内置推理进程或向量数据库。
|
||||
- 未经独立设计就把知识库向量连接扩展到记忆、自动化、项目文件搜索或其他应用服务。
|
||||
|
||||
## 5. 竞品基线与 GoodBuddy 定位
|
||||
|
||||
@@ -156,6 +169,26 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
“检索测试”是当前知识库的高频诊断操作,通过知识库标题区次操作打开独立
|
||||
工作台,不新增第五个一级页签。
|
||||
|
||||
全局向量模型仍在“设置 → 模型连接 → 向量模型”中配置。GoodBuddy 内置连接和用户添加的
|
||||
OpenAI 兼容连接使用同一连接列表;其界面、数据路径及切换语义以
|
||||
[本地文本向量模型与连接设计](./local-embedding-technical-design.md)
|
||||
为准,知识库页面只显示当前模型、索引兼容性、覆盖率和重建操作。“模型连接”是设置入口
|
||||
名称,不表示向量连接可以分配给 Agent Runtime;项目 Runtime 和 Agent Runtime 的模型
|
||||
选择中不显示向量连接。
|
||||
|
||||
知识检索与 Runtime 的关系固定为:
|
||||
|
||||
```text
|
||||
用户问题
|
||||
→ Main / KnowledgeService 使用当前向量连接检索
|
||||
→ Main 生成有界文本证据、引用和检索诊断
|
||||
→ 当前 Runtime 仅消费这些不可信检索结果
|
||||
```
|
||||
|
||||
`always` 模式由 Main 在启动 Runtime 前完成检索;`auto` 模式中的 `knowledge_search`
|
||||
也只允许 Runtime 调用 Main 的有界知识工具。两种模式都不把向量连接、凭据、模型文件、
|
||||
Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不改变当前向量连接。
|
||||
|
||||
对话输入区的知识范围弹层包含:
|
||||
|
||||
1. 已启用知识库多选。
|
||||
@@ -225,6 +258,10 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
- 相邻分块或父块合并后的实际上下文。
|
||||
- “查看分块”“打开来源”操作。
|
||||
|
||||
图谱通道对中文自然语言问题生成有界 CJK 二元词和拉丁关键词,在实体名称、
|
||||
别名和类型中确定种子,再沿具有有效文档证据的关系执行有界多跳扩展。图谱探索页
|
||||
搜索到实体时保留其一跳邻居和关系上下文,并继续允许从实体或关系打开原始证据。
|
||||
|
||||
检索测试不创建聊天消息、不写入会话历史、不调用 LLM,也不改变知识库内容。
|
||||
|
||||
### 7.5 可扩展向量搜索
|
||||
@@ -495,7 +532,7 @@ type KnowledgeRetrievalResponse = {
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 向量查询失败 | 继续全文和图谱检索,显示降级原因 |
|
||||
| 向量查询失败 | 继续已配置的全文、中文和图谱通道,显示降级原因,不切换向量 Provider 或模型 |
|
||||
| 部分文档无向量 | 使用可用文档,显示完成数和失败数 |
|
||||
| CJK 索引迁移失败 | 回滚迁移,不损坏旧 FTS |
|
||||
| 重排失败 | 回退 RRF 排序并显示诊断 |
|
||||
@@ -503,6 +540,7 @@ type KnowledgeRetrievalResponse = {
|
||||
| 单文档重建失败 | 保留上一版可用索引 |
|
||||
| 同名模型端点变化 | 旧 Fingerprint 索引标记不兼容,等待重建 |
|
||||
| 新向量重建失败 | 保留上一版就绪向量继续服务,单独记录失败尝试 |
|
||||
| 图谱模型空响应、超时或瞬时服务错误 | 按不超过约 8,192 个估算 Token 组织独立抽取批次,每批最多尝试两次;部分批次成功时发布已验证结果并在任务中显示具体警告;所有批次失败时图谱任务失败,但新文档的全文索引仍可发布,已有文档的显式重建继续保留上一版可用索引 |
|
||||
| 原文件已移动 | 显示来源不可用,提供重试或移除 |
|
||||
| 引用对象已删除 | 显示引用已失效,不打开任意替代路径 |
|
||||
| 上下文超预算 | 按排名截断并明确标记 |
|
||||
@@ -518,6 +556,7 @@ GoodBuddy 不上传私人检索查询或文档内容。本地诊断至少记录
|
||||
- 是否发生降级。
|
||||
- 最终结果数和上下文字符数。
|
||||
- 重建文档数、成功数、失败数和取消状态。
|
||||
- 图谱抽取批次数、部分失败警告和具体失败原因。
|
||||
|
||||
手动验收使用仓库内不含私人内容的固定样例集,覆盖:
|
||||
|
||||
@@ -558,6 +597,12 @@ GoodBuddy 不上传私人检索查询或文档内容。本地诊断至少记录
|
||||
- “每次先检索”在 Runtime 启动前产生检索诊断和引用,即使模型未调用工具。
|
||||
- 未配置向量模型时,中文改写问题仍能通过 CJK 索引召回相关分块。
|
||||
- 向量查询失败时回答可继续,界面明确显示已降级。
|
||||
- 内置连接和用户添加的 OpenAI 兼容连接之间不会自动切换;用户连接的实际数据去向持续
|
||||
可见,内置连接不重复展示固定本地路径。
|
||||
- 项目 Runtime 和 Agent Runtime 的模型选择不显示向量连接。
|
||||
- Runtime 只获得有界文本证据、引用和脱敏诊断,不获得向量 Provider、凭据或原始向量。
|
||||
- 切换直连模型、OpenCode、Continue、DeepSeek Harness 或远程 Runtime 不改变当前向量
|
||||
连接。
|
||||
- 10,000 个分块的向量测试能够返回正确 Top K,不出现固定上限空结果。
|
||||
- 同名模型切换端点后,不会读取 Fingerprint 不匹配的旧向量。
|
||||
- 重建失败时,上一版已就绪向量仍能继续召回。
|
||||
+2
-1
@@ -7,7 +7,7 @@
|
||||
| 状态 | 实施中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-11 |
|
||||
| 关联 PRD | [知识库检索与分块增强 PRD](knowledge-rag-enhancement-prd.md) |
|
||||
| 关联 PRD | [知识库检索与分块增强 PRD](./prd.md) |
|
||||
|
||||
## 1. 角色
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
- 全文完成但向量失败时,明确显示“全文可用、向量失败”
|
||||
- 向量未启用与向量失败是不同状态
|
||||
- 图谱按需、未启用和失败是不同状态
|
||||
- 图谱模型按有界 Token 批次抽取;部分批次失败时保留成功结果并在任务中显示具体警告,全部批次失败时新文档仍保留可用全文索引
|
||||
- 汇总显示全文可用数、向量完成数和失败数
|
||||
|
||||
### US-D2 修复失败文档
|
||||
@@ -0,0 +1,43 @@
|
||||
# 工具执行环境
|
||||
|
||||
本目录是 GoodBuddy“工具执行环境”功能的唯一文档入口。该功能为本机 SKILL 和本机
|
||||
stdio MCP 提供明确、可诊断的 Node.js 与 Python 环境,同时允许专业用户选择自己维护的
|
||||
解释器。
|
||||
|
||||
本功能不作用于 SSH Host 上的 GoodBuddy Agent、固定 Node、签名 Runtime、SKILL 或 MCP。
|
||||
用户从设置中心的“能力与工具”进入,并在 `Skills`、`MCP`、`工具执行环境` 三个水平
|
||||
Tab 之间切换。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、产品目标、范围、需求和产品验收 |
|
||||
| [User Stories](./user-stories.md) | 用户角色、使用场景和逐故事验收 |
|
||||
| [功能逻辑](./logic-design.md) | 不变量、状态机、决策表、失败恢复和跨场景一致性 |
|
||||
| [UI 设计](./ui-design.md) | 页面结构、组件、状态、文案和无障碍一致性 |
|
||||
| [技术设计](./technical-design.md) | Main、Runtime、MCP、打包、生命周期和测试架构 |
|
||||
| [功能进度](./progress.md) | 已完成、进行中、待实施、阻塞项和验证证据 |
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| 能力与工具 | 设置中心中统一承载 Skills、MCP 和工具执行环境的一级分类 |
|
||||
| 工具执行环境 | GoodBuddy 在桌面所在设备上向本机 Runtime 和 stdio MCP 提供的工具命令环境 |
|
||||
| GoodBuddy 托管 | 由 GoodBuddy 提供、验证并解析的 Node.js 或 Python |
|
||||
| 自定义环境 | 用户从检测结果中选择或直接指定的外部解释器 |
|
||||
| 工具下载源 | GoodBuddy 固定目录受管工具工件使用的原生地址或 OSS 镜像 |
|
||||
| 工具就绪 | 解释器及配套命令通过真实诊断 |
|
||||
| 能力依赖就绪 | 某个 SKILL 或 MCP 所需的第三方包和入口通过实际验证 |
|
||||
|
||||
## 文档维护规则
|
||||
|
||||
1. 产品范围只在 PRD 定义,其他文档链接引用。
|
||||
2. 界面视觉和交互只在 UI 设计定义,并服从仓库根目录的
|
||||
[统一界面设计系统](../../../UI-DESIGN.md)。
|
||||
3. 状态转换、优先级和冲突决策只在功能逻辑中定义。
|
||||
4. 进度文档只记录已验证事实,不复制未来设计。
|
||||
5. 技术实现变化先更新技术设计;用户行为变化同时更新 PRD、User Stories、功能逻辑和
|
||||
UI 设计。
|
||||
6. 每次完成功能阶段后立即更新进度、验证命令和剩余阻塞项。
|
||||
@@ -0,0 +1,460 @@
|
||||
# 工具执行环境功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,验收中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 关联 PRD | [工具执行环境 PRD](./prd.md) |
|
||||
| 关联场景 | [工具执行环境 User Stories](./user-stories.md) |
|
||||
|
||||
本文是本功能业务规则、状态转换、决策表和跨场景一致性的权威来源。它不定义组件视觉,也
|
||||
不规定代码模块如何实现。
|
||||
|
||||
## 1. 逻辑目标
|
||||
|
||||
1. 同一用户操作在不同本机 Runtime 和 stdio MCP 中得到一致的解释器选择。
|
||||
2. 系统不通过隐式回退掩盖配置错误。
|
||||
3. 工具可执行、配套命令可执行和具体能力依赖可用是三个可区分事实。
|
||||
4. 已运行进程使用稳定环境,不被设置变化中途改写。
|
||||
5. 安装、更新、取消、失败和删除都有唯一、可恢复的结果。
|
||||
6. 本机环境和远程 Host 环境没有隐式传播。
|
||||
|
||||
## 2. 核心不变量
|
||||
|
||||
### INV-1 单一明确来源
|
||||
|
||||
Node.js 和 Python 各自恰好保存一个来源:`managed` 或 `custom`。不存在 `auto`、候选优先级
|
||||
列表或运行时动态选择。
|
||||
|
||||
### INV-2 禁止静默回退
|
||||
|
||||
当前来源不可用时,该工具明确不可用。即使发现另一套系统解释器,也不会自动切换。
|
||||
|
||||
### INV-3 配套命令同源
|
||||
|
||||
- `node`、`npm`、`npx` 必须属于同一 Node 来源。
|
||||
- `python`、`python3`、`pip` 必须属于同一 Python 来源。
|
||||
- `pip` 等价于当前 Python 的 `-m pip`,不能解析到其他解释器。
|
||||
|
||||
### INV-4 三层状态独立
|
||||
|
||||
1. **解释器状态**:Node 或 Python 本身能否执行。
|
||||
2. **工具组合状态**:npm/npx 或 pip/venv/SSL 是否完整。
|
||||
3. **能力依赖状态**:具体 SKILL/MCP 的入口和第三方依赖是否通过验证。
|
||||
|
||||
任一上层成功都不能推导下一层成功。
|
||||
|
||||
### INV-5 进程环境不可变
|
||||
|
||||
每个 Runtime/MCP 进程在启动时取得一份环境快照。设置变化只影响之后启动的进程,不改写
|
||||
已运行进程的 PATH 或解释器。
|
||||
|
||||
### INV-6 绝对命令优先
|
||||
|
||||
stdio MCP 配置的绝对 command 表达用户对该 MCP 的明确覆盖,原样执行,不经过全局工具
|
||||
命令替换。
|
||||
|
||||
### INV-7 本机与远程隔离
|
||||
|
||||
本机工具选择、shim、Python 包、SKILL 包和 stdio MCP 配置不上传到远程 Host。
|
||||
|
||||
### INV-8 不推断能力依赖已就绪
|
||||
|
||||
能力没有专属实际诊断时,状态只能是“未验证”,不能根据解释器版本或文件存在推断“已就绪”。
|
||||
|
||||
### INV-9 受管工具工件来源明确且冻结
|
||||
|
||||
每个受管工具外部工件同时声明固定原生地址和 OSS Target。用户选择一个下载源后,任务
|
||||
启动时冻结该来源;失败不回退。OSS 必须是原生工件的字节级副本。
|
||||
|
||||
### INV-10 设置导航不改变业务状态
|
||||
|
||||
“能力与工具”的三个水平 Tab 只组织界面,不改变 Skill、MCP 或工具环境设置。切换 Tab
|
||||
不得提交、丢弃或重置当前设置面板会话中的编辑状态;关闭后不持久化外层 Tab。
|
||||
|
||||
分阶段交付期间,未实现的“工具执行环境”不进入 Tab 状态集合。第一阶段状态集合只有
|
||||
`skills | mcp`,后续实现完成后才扩展为 `skills | mcp | tool-environment`。
|
||||
|
||||
### INV-11 内置 Skill 默认值不覆盖用户状态
|
||||
|
||||
只有状态存储中没有对应 Skill 记录时才使用产品默认值。通用文档工具
|
||||
`deai-writing`、`longdoc-docx` 默认启用;产品营销类 `product-evidence`、
|
||||
`product-marketing`、`product-presentation` 默认停用。用户显式保存的启停状态和
|
||||
Runtime 分配优先,升级不得重置。
|
||||
|
||||
九种产品营销文档配方属于 `product-marketing` 包内工作流,不是独立能力状态维度,
|
||||
不进入顶层 Skill 目录,也不单独注入 Runtime。
|
||||
|
||||
## 3. 状态维度
|
||||
|
||||
### 3.1 来源选择
|
||||
|
||||
```text
|
||||
managed
|
||||
custom(executablePath)
|
||||
```
|
||||
|
||||
这是唯一持久状态。
|
||||
|
||||
候选选择还具有不持久化的界面状态:
|
||||
|
||||
```text
|
||||
closed
|
||||
choosing
|
||||
validating
|
||||
failed
|
||||
```
|
||||
|
||||
- 从 `closed` 进入 `choosing` 不改变当前持久来源。
|
||||
- `choosing` 中不得自动采用首个候选;只有用户明确选择候选或文件后才进入 `validating`。
|
||||
- 验证成功后一次性保存 `custom(executablePath)`;验证失败回到 `choosing` 并就地显示错误。
|
||||
- 文件选择取消回到 `choosing`,不显示错误且不改变当前来源。
|
||||
- 没有候选不是禁用“自定义环境”的理由,用户仍可刷新或选择其他可执行文件。
|
||||
|
||||
### 3.2 托管 Python 安装状态
|
||||
|
||||
```text
|
||||
not-installed
|
||||
downloading
|
||||
verifying
|
||||
installing
|
||||
testing
|
||||
ready
|
||||
update-available
|
||||
error
|
||||
```
|
||||
|
||||
`error` 同时携带“是否仍有旧版可用”。若旧版可用,实际工具状态仍基于旧版诊断。
|
||||
|
||||
托管 Node 随 GoodBuddy 存在,不使用下载安装状态,但仍可能因打包损坏进入诊断失败。
|
||||
|
||||
### 3.3 工具诊断状态
|
||||
|
||||
```text
|
||||
unknown
|
||||
testing
|
||||
ready
|
||||
partial
|
||||
unavailable
|
||||
```
|
||||
|
||||
- `ready`:解释器和本功能承诺的配套命令全部通过。
|
||||
- `partial`:解释器通过,至少一个配套命令失败。
|
||||
- `unavailable`:解释器本身不能使用。
|
||||
|
||||
### 3.4 能力依赖状态
|
||||
|
||||
```text
|
||||
unknown
|
||||
checking
|
||||
ready
|
||||
missing
|
||||
incompatible
|
||||
error
|
||||
```
|
||||
|
||||
- `unknown` 在 UI 中显示“依赖未验证”。
|
||||
- `missing` 表示明确缺少包或入口。
|
||||
- `incompatible` 表示解释器版本、架构或 ABI 不满足已声明要求。
|
||||
- `error` 表示诊断本身没有得出有效结论。
|
||||
|
||||
### 3.5 工具下载源
|
||||
|
||||
```text
|
||||
native
|
||||
oss
|
||||
```
|
||||
|
||||
这是独立于 Node/Python 环境来源的持久设置。全新用户默认 `native`。
|
||||
|
||||
## 4. 来源解析决策表
|
||||
|
||||
| 保存来源 | 当前事实 | 解析结果 | 是否回退 |
|
||||
| --- | --- | --- | --- |
|
||||
| 托管 Node | 完整诊断通过 | 使用 Electron Node 与锁定 npm/npx | 否 |
|
||||
| 托管 Node | Node 通过,npm/npx 失败 | Node 部分可用,显示失败项 | 否 |
|
||||
| 托管 Node | Node 失败 | Node 不可用,提示修复/重装 GoodBuddy | 否 |
|
||||
| 托管 Python | 未安装 | Python 不可用,提供安装 | 否 |
|
||||
| 托管 Python | 已安装且诊断通过 | 使用托管 Python | 否 |
|
||||
| 托管 Python | 更新失败但旧版健康 | 继续使用旧版并显示更新失败 | 否 |
|
||||
| 托管 Python | 安装损坏 | Python 不可用,提供重新安装/删除 | 否 |
|
||||
| 自定义 | 路径及诊断通过 | 使用该绝对路径 | 否 |
|
||||
| 自定义 | 路径失效 | 保持选择并显示路径不可用 | 否 |
|
||||
| 自定义 | 解释器通过、配套命令失败 | 部分可用并显示失败项 | 否 |
|
||||
| 任意 | 发现其他有效解释器 | 只作为候选展示 | 不自动使用 |
|
||||
|
||||
## 5. 命令解析决策表
|
||||
|
||||
| 调用形式 | 结果 |
|
||||
| --- | --- |
|
||||
| `node ...` | 当前 Node |
|
||||
| `npm ...` | 当前 Node 配套 npm |
|
||||
| `npx ...` | 当前 Node 配套 npx |
|
||||
| `python ...` | 当前 Python |
|
||||
| `python3 ...` | 当前 Python |
|
||||
| `pip ...` | 当前 Python `-m pip` |
|
||||
| `#!/usr/bin/env node` | 从进程工具 PATH 解析当前 Node |
|
||||
| `#!/usr/bin/env python3` | 从进程工具 PATH 解析当前 Python |
|
||||
| stdio MCP 绝对 command | 原样执行 |
|
||||
| 未托管裸命令,例如 `uvx` | 按原筛选 PATH 处理;找不到则明确失败 |
|
||||
| HTTP/SSE MCP | 不参与本机命令解析 |
|
||||
|
||||
绝对 command 的子进程若自行调用裸 `node` 或 `python`,仍会看到该 MCP 进程启动时的工具
|
||||
PATH。这不改变绝对 command 本身。
|
||||
|
||||
## 6. 首次使用和迁移
|
||||
|
||||
### 6.1 全新安装
|
||||
|
||||
| 工具 | 默认选择 | 首次状态 |
|
||||
| --- | --- | --- |
|
||||
| Node.js | GoodBuddy 托管 | 立即真实诊断 |
|
||||
| Python | GoodBuddy 托管 | 未安装,展示安装入口 |
|
||||
|
||||
系统候选可以展示,但不会改变默认选择。
|
||||
|
||||
### 6.2 旧版本升级
|
||||
|
||||
1. 设置中没有工具环境字段时执行一次迁移。
|
||||
2. 分别按旧版本实际 PATH 解析 Node 和 Python 3。
|
||||
3. 有效解释器固化为 `custom + 绝对真实路径`。
|
||||
4. 无有效解释器时选择 `managed`。
|
||||
5. 持久化后不再因 PATH 变化迁移。
|
||||
|
||||
迁移中 Node 和 Python 独立决策,不要求二者使用同一种来源。
|
||||
|
||||
## 7. 托管 Python 生命周期
|
||||
|
||||
### 7.1 首次安装
|
||||
|
||||
```text
|
||||
not-installed
|
||||
→ downloading
|
||||
→ verifying
|
||||
→ installing
|
||||
→ testing
|
||||
→ ready
|
||||
```
|
||||
|
||||
任一步失败:
|
||||
|
||||
```text
|
||||
→ 删除本次暂存
|
||||
→ error(没有活动版本)
|
||||
→ 用户可重试
|
||||
```
|
||||
|
||||
### 7.2 取消
|
||||
|
||||
| 取消阶段 | 结果 |
|
||||
| --- | --- |
|
||||
| 下载 | 终止下载、删除暂存、回到安装前状态 |
|
||||
| 校验 | 终止校验、删除暂存、回到安装前状态 |
|
||||
| 解压/安装 | 完成本次有界清理、回到安装前状态 |
|
||||
| 测试 | 停止诊断、删除未发布候选、回到安装前状态 |
|
||||
|
||||
取消不发布候选版本,不把取消显示为安装失败。
|
||||
|
||||
### 7.3 更新
|
||||
|
||||
1. 下载和验证候选时,旧版继续服务现有进程。
|
||||
2. 应用候选前,页面说明需要停止依赖托管 Python 的本机 Runtime/MCP。
|
||||
3. 用户确认后通过现有生命周期取消/停止相关进程。
|
||||
4. 无法确认停止时不替换当前版本。
|
||||
5. 候选完整诊断通过后才发布。
|
||||
6. 失败继续使用旧版,并显示更新失败。
|
||||
|
||||
首期不让两个托管 Python 版本长期并存。候选暂存只服务本次更新。
|
||||
|
||||
### 7.4 删除
|
||||
|
||||
| 条件 | 行为 |
|
||||
| --- | --- |
|
||||
| 没有使用中进程 | 确认后删除托管目录 |
|
||||
| 有使用中进程 | 说明影响,用户确认后停止相关进程再删除 |
|
||||
| 进程停止失败 | 不删除,保留当前环境 |
|
||||
| 当前选择为自定义 | 删除托管包不改变自定义来源 |
|
||||
| 当前选择为托管 | 删除后保持托管选择,状态变为未安装 |
|
||||
|
||||
## 8. 设置保存与进程生效
|
||||
|
||||
### 8.1 保存时
|
||||
|
||||
- 验证待保存来源。
|
||||
- 自定义路径必须在保存前通过解释器诊断;配套命令可以形成 `partial`。
|
||||
- 保存成功后标记本机 Runtime 环境配置已变化。
|
||||
- 不直接修改任何运行中进程。
|
||||
|
||||
### 8.2 后续进程
|
||||
|
||||
- 没有活动请求的缓存 Runtime 在下次使用前按现有重建入口重建。
|
||||
- 有活动请求的 Runtime 完成或被用户停止前保持原环境。
|
||||
- 已启动 stdio MCP 保持原环境,正常关闭、重连或显式测试后使用新环境。
|
||||
- 新启动的 Runtime/MCP 立即使用新环境。
|
||||
|
||||
每个工具调用的活动记录应归属于实际承载它的进程,不根据最新设置追改历史来源。
|
||||
|
||||
### 8.3 设置导航
|
||||
|
||||
| 入口或操作 | 打开的外层 Tab | 数据行为 |
|
||||
| --- | --- | --- |
|
||||
| 普通“能力与工具”入口 | Skills | 不修改业务数据 |
|
||||
| 用户点击 Skills | Skills | 不修改业务数据 |
|
||||
| 用户点击 MCP | MCP | 不修改业务数据 |
|
||||
| 未来的定向入口 | 指定 Tab | 不修改业务数据 |
|
||||
| 同一设置面板会话中切换 Tab | 用户选择的 Tab | 保留三个面板的会话内状态 |
|
||||
| 关闭后重新打开普通入口 | Skills | 不恢复上次外层 Tab |
|
||||
|
||||
MCP 内层页签状态属于 MCP 面板,不与外层 Tab 合并。外层 Tab 的默认值或定向入口不能改写
|
||||
MCP 的连接、凭据、启用或 Runtime 分配状态。
|
||||
|
||||
## 9. 能力依赖逻辑
|
||||
|
||||
### 9.1 诊断输入
|
||||
|
||||
能力可以声明:
|
||||
|
||||
- 需要 Node 或 Python。
|
||||
- 最低/最高解释器版本。
|
||||
- 可选依赖文件位置。
|
||||
- 可选的无副作用诊断入口。
|
||||
|
||||
首期声明只用于显示、版本比较和显式诊断,不触发自动安装。
|
||||
|
||||
### 9.2 状态推导
|
||||
|
||||
| 工具状态 | 能力诊断 | 能力状态 |
|
||||
| --- | --- | --- |
|
||||
| unavailable | 不执行 | missing 或 incompatible,附工具原因 |
|
||||
| partial | 能力不依赖失败配套项且实际诊断成功 | ready |
|
||||
| partial | 能力依赖失败配套项 | missing |
|
||||
| ready | 没有能力诊断 | unknown |
|
||||
| ready | 实际诊断成功 | ready |
|
||||
| ready | 明确缺包 | missing |
|
||||
| ready | 版本/ABI 不满足 | incompatible |
|
||||
| 任意 | 诊断自身异常 | error |
|
||||
|
||||
“依赖未验证”不是错误,也不能显示为“已就绪”。
|
||||
|
||||
## 10. Ask、Execute 与 MCP
|
||||
|
||||
| 场景 | 解释器是否可见 | 是否允许执行 |
|
||||
| --- | --- | --- |
|
||||
| 本机 Execute Runtime | 是 | 按现有 Execute 权限 |
|
||||
| 本机 Ask Runtime | 可以存在于进程环境 | 仍由 Runtime Ask 边界决定,不能因此获得命令权限 |
|
||||
| Main 启动的已启用 stdio MCP | 是 | 按当前 MCP 分配和模式规则 |
|
||||
| HTTP/SSE MCP | 不适用 | 按当前网络 MCP 规则 |
|
||||
| 远程 Runtime | 否 | 使用远程 Host 自身环境 |
|
||||
|
||||
工具环境不创建新的工具,不扩大 Runtime 当前允许的工具集合。
|
||||
|
||||
## 11. 网络受限与离线
|
||||
|
||||
### 11.1 托管 Node
|
||||
|
||||
随 GoodBuddy 提供,安装后可离线使用。
|
||||
|
||||
### 11.2 托管 Python
|
||||
|
||||
- Windows 原生 Target 固定到官方 NuGet 包;macOS/Linux 原生 Target 固定到 Astral
|
||||
`python-build-standalone`。
|
||||
- OSS Target 是对应原生工件的字节级副本。
|
||||
- 下载源不可达时保持未安装或旧版可用状态,并显示准确原因。
|
||||
- 首期不提供 Python 离线包导入,避免引入未经选型的包格式和导入流程。
|
||||
- 完全离线用户可以选择已经离线安装的自定义 Python。
|
||||
|
||||
未来若加入离线导入,必须在 PRD、User Stories、逻辑状态和技术包校验中同时定义,不能只
|
||||
增加一个文件按钮。
|
||||
|
||||
### 11.3 工具下载源决策
|
||||
|
||||
| 保存选择 | 目录事实 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 原生地址 | 原生 Target 完整 | 请求固定上游 URL |
|
||||
| 原生地址 | 原生 Target 缺失 | 工件不可下载,不请求 OSS |
|
||||
| OSS 镜像 | OSS Target 完整 | 只请求固定 OSS URL |
|
||||
| OSS 镜像 | OSS Target 缺失 | 工件不可下载,不请求原生地址 |
|
||||
| 任意 | 下载后字节数/SHA-256 不符 | 删除暂存并失败 |
|
||||
| 任意 | 设置在任务期间变化 | 当前任务不变,后续任务使用新选择 |
|
||||
|
||||
该选择只覆盖受管工具目录中的外部工件,不影响模型、应用更新、Agent、pip/npm 注册表或
|
||||
用户 URL。
|
||||
|
||||
## 12. 冲突与一致性检查
|
||||
|
||||
| 可能冲突 | 唯一规则 |
|
||||
| --- | --- |
|
||||
| 全局工具选择 vs MCP 绝对 command | 绝对 command 优先 |
|
||||
| 新设置 vs 运行中进程 | 运行中进程保留启动快照 |
|
||||
| 新托管版本 vs 健康旧版本 | 候选完整通过后才发布 |
|
||||
| 系统发现新版本 vs 已保存自定义路径 | 不自动切换 |
|
||||
| 解释器成功 vs 缺少第三方包 | 工具 ready,能力 missing/unknown |
|
||||
| Ask 环境包含解释器 vs Ask 只读 | Ask 权限边界优先 |
|
||||
| 本机设置 vs 远程项目 | 远程隔离优先 |
|
||||
| 镜像不可达 vs 其他来源可用 | 不在同一次操作中静默切换来源 |
|
||||
| 工具下载源 vs 模型/更新下载源 | 各自独立,工具执行环境设置不覆盖其他功能 |
|
||||
| OSS 对象 vs 原生工件 | 必须字节、大小和 SHA-256 相同 |
|
||||
| 合并设置入口 vs 原有 Skills/MCP 数据 | 只重组导航,不迁移或重写业务数据 |
|
||||
| 外层 Tab vs MCP 内层 Tab | 保持两个独立 tablist、标识和键盘焦点范围 |
|
||||
|
||||
## 13. 逻辑评估结论
|
||||
|
||||
### 13.1 完整性评估
|
||||
|
||||
| 评估维度 | 结论 | 依据 |
|
||||
| --- | --- | --- |
|
||||
| 首次使用 | 通过 | 托管 Node 立即诊断,托管 Python 明确未安装 |
|
||||
| 正常执行 | 通过 | SKILL 与 stdio MCP 共用同一命令解析 |
|
||||
| 自定义环境 | 通过 | 明确路径、真实诊断、禁止静默回退 |
|
||||
| 部分失败 | 通过 | 解释器、工具组合、能力依赖三层状态独立 |
|
||||
| 安装生命周期 | 通过 | 安装、取消、失败、重试有唯一结果 |
|
||||
| 更新生命周期 | 通过 | 候选通过后发布,失败保留健康旧版 |
|
||||
| 删除生命周期 | 通过 | 使用中进程先停止,失败不删除 |
|
||||
| 设置生效 | 通过 | 进程启动快照不可变,新进程使用新设置 |
|
||||
| 升级兼容 | 通过 | 旧系统解释器一次性固化为自定义路径 |
|
||||
| Ask/Execute | 通过 | 工具环境不扩大工具集合或权限 |
|
||||
| 远程边界 | 通过 | 本机配置不进入 Host |
|
||||
| 受限网络 | 通过 | 来源失败可见,不在同次操作静默切换 |
|
||||
| 完全离线 | 有明确限制 | Node 可用;Python 首期要求已有自定义环境 |
|
||||
| 受管工具工件来源 | 通过 | 原生/OSS 独立选择、任务冻结、禁止回退 |
|
||||
| 设置导航 | 通过 | 单一一级入口、分阶段水平 Tab、业务数据不变 |
|
||||
|
||||
### 13.2 一致性评估
|
||||
|
||||
- “两种来源”与自定义候选探测不冲突:候选只是自定义来源的选值,不是第三种来源。
|
||||
- “禁止回退”与升级迁移不冲突:迁移只发生一次,完成后按普通保存选择处理。
|
||||
- “进程环境不可变”与 Runtime 重建不冲突:只有空闲或后续启动的进程使用新环境。
|
||||
- “工具环境就绪”与“能力依赖就绪”不冲突:两者属于独立状态维度。
|
||||
- “绝对 MCP command 优先”与 PATH 注入不冲突:command 不替换,但其子进程继承启动快照。
|
||||
- “更新保留旧版”不要求长期多版本管理:旧版只在候选发布前保持当前,候选失败即删除。
|
||||
|
||||
### 13.3 当前未决项
|
||||
|
||||
没有未决的产品逻辑分支。以下属于实施前必须解决的技术选型,不改变业务规则:
|
||||
|
||||
1. 六平台 CPython 独立发行包、固定版本、许可证和摘要。
|
||||
2. SKILL/MCP 运行要求声明的最小 schema 和无副作用诊断入口。
|
||||
3. 各 Runtime 当前缓存进程的精确空闲判定与既有重建入口接线。
|
||||
|
||||
若技术选型无法满足当前逻辑,例如目标 Python 工件不包含可用 pip/venv,必须回到 PRD 和
|
||||
逻辑设计调整承诺,不能在实现中静默降级。
|
||||
|
||||
## 14. 需求追踪
|
||||
|
||||
| 逻辑范围 | PRD | User Stories |
|
||||
| --- | --- | --- |
|
||||
| 首次使用默认状态 | FR-1、FR-2、FR-6 | US-A0 |
|
||||
| 明确来源与禁止回退 | FR-3、FR-8 | US-C1、US-C2、US-D3 |
|
||||
| 托管 Node | FR-1 | US-A1、US-B2 |
|
||||
| 托管 Python 生命周期 | FR-2、FR-9 | US-A2、US-D2、US-D4、US-D5 |
|
||||
| 三层状态 | FR-6 | US-A3、US-A4 |
|
||||
| SKILL 命令解析 | FR-4 | US-A1、US-A3 |
|
||||
| MCP 命令解析 | FR-5 | US-B1、US-B2、US-B3 |
|
||||
| 生效时机 | FR-7 | US-C3、US-C4 |
|
||||
| Ask/Execute | FR-10 | US-D6 |
|
||||
| 远程隔离 | FR-7、FR-9 | US-D1 |
|
||||
| 受限网络与离线 | FR-2、FR-11 | US-E1、US-E2、US-E3 |
|
||||
| 统一设置入口 | FR-12 | US-F1、US-F2 |
|
||||
@@ -0,0 +1,213 @@
|
||||
# 工具执行环境 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已批准并实施,验收中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 关联文档 | [功能入口](./README.md)、[User Stories](./user-stories.md)、[功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md)、[功能进度](./progress.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 的部分 SKILL 通过 Node.js 或 Python 脚本生成成果,部分本机 stdio MCP 也以
|
||||
`node`、`npm`、`npx`、`python` 或 `python -m` 启动。当前安装包没有向这些能力提供统一
|
||||
解释器,用户是否成功取决于系统 PATH 和预装软件。
|
||||
|
||||
普通用户不应为了使用一个 SKILL 或 MCP 手工安装开发环境。专业用户已有项目 Node、
|
||||
Python、虚拟环境或 Conda 环境时,也不能被 GoodBuddy 静默替换。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
1. 全新设备无需预装 Node.js 即可运行普通 JavaScript SKILL 和 Node.js stdio MCP。
|
||||
2. 用户可在 GoodBuddy 内按需安装 Python,并运行 Python SKILL 和 stdio MCP。
|
||||
3. 专业用户可以选择检测到的解释器或指定其他可执行文件。
|
||||
4. 设置始终展示实际来源、版本、路径、工具状态和能力依赖状态。
|
||||
5. 工具执行环境不修改系统环境,也不传播到远程主机。
|
||||
6. 保持 Ask/Execute、Runtime、MCP、取消和进程清理的现有边界。
|
||||
7. 用户可以在“工具执行环境”Tab 中明确选择受管工具工件使用原生地址还是 OSS 镜像。
|
||||
8. Skills、MCP 和工具执行环境通过一个“能力与工具”设置入口形成清晰的能力配置链路。
|
||||
|
||||
## 3. 核心决策
|
||||
|
||||
### 3.1 每种工具只有两个来源
|
||||
|
||||
- **GoodBuddy 托管(推荐)**
|
||||
- **自定义环境**
|
||||
|
||||
不提供“自动选择”。“系统安装”和“指定路径”合并为“自定义环境”,因为两者最终都是
|
||||
用户明确选择的外部可执行文件,区别只在发现方式。
|
||||
|
||||
### 3.2 配套命令跟随解释器
|
||||
|
||||
- Node.js 保证 `node`、`npm`、`npx` 来自同一来源。
|
||||
- Python 保证 `python`、`python3`、`pip` 来自同一来源;`pip` 实际绑定当前 Python。
|
||||
- 首期不托管 `uv`、`uvx`、Conda、Poetry、pyenv 或 nvm。
|
||||
|
||||
### 3.3 工具就绪不等于能力依赖就绪
|
||||
|
||||
解释器通过诊断后标记“工具环境已就绪”。只有 SKILL 或 MCP 的实际入口及所需第三方包
|
||||
通过验证后,才能标记“能力依赖已就绪”。GoodBuddy 首期不自动扫描和安装任意能力依赖。
|
||||
|
||||
### 3.4 分阶段交付统一设置入口
|
||||
|
||||
第一阶段先把现有 Skills 和 MCP 合并到“能力与工具”,只显示两个真实可用的水平 Tab。
|
||||
“工具执行环境”实现完成后再加入第三个 Tab;未实现期间不展示禁用项、空白页或开发中占位。
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
### 4.1 包含
|
||||
|
||||
- 本机 OpenCode、Continue、DeepSeek Harness 加载的 SKILL。
|
||||
- Main 或本机 Runtime 启动的 stdio MCP。
|
||||
- 托管 Node.js、npm、npx。
|
||||
- 按需下载安装的托管 Python 3、pip、venv。
|
||||
- 系统解释器探测、自定义文件选择和真实诊断。
|
||||
- 工具来源、版本、路径、安装、删除、更新和错误状态。
|
||||
- 受管工具工件的原生地址/OSS 镜像选择。
|
||||
- “能力与工具”一级分类及 `Skills`、`MCP`、`工具执行环境` 三个水平 Tab。
|
||||
- 新启动本机 Runtime 和 MCP 的 PATH 注入。
|
||||
|
||||
### 4.2 不包含
|
||||
|
||||
- SSH Host 的 PATH、Agent、固定 Node、Runtime、SKILL 或 MCP。
|
||||
- HTTP/SSE MCP 服务端环境。
|
||||
- 普通助手工作栏终端的 Shell 环境。
|
||||
- 通用语言版本管理器。
|
||||
- 每项目或每 SKILL 的解释器覆盖。
|
||||
- 自动合并或求解多个 `requirements.txt`、`package.json`。
|
||||
- 自动执行任意第三方安装脚本。
|
||||
- 修改系统 PATH、注册表或 Shell Profile。
|
||||
- 无条件把完整 Python 打入桌面安装包。
|
||||
- 首期导入托管 Python 离线包;完全离线用户可选择已有的自定义 Python。
|
||||
- 修改模型下载源、应用更新源、Agent 下载源、pip/npm 注册表或用户配置的网络地址。
|
||||
|
||||
## 5. 功能需求
|
||||
|
||||
### FR-1 Node.js 托管环境
|
||||
|
||||
- 安装 GoodBuddy 后托管 Node.js 即可用,不需要额外下载。
|
||||
- 实际版本通过运行命令诊断,不仅根据 Electron 版本推断。
|
||||
- `node`、`npm`、`npx` 必须来自同一托管组合。
|
||||
- Electron Node 原生扩展兼容限制必须可见。
|
||||
|
||||
### FR-2 Python 托管环境
|
||||
|
||||
- Python 未安装时展示安装入口,不把完整 Python 放入所有桌面安装包。
|
||||
- Windows x64 使用固定官方 `python` NuGet 工件,Windows ARM64 使用固定官方
|
||||
`pythonarm64` NuGet 工件;macOS/Linux 使用固定 Astral `python-build-standalone`
|
||||
工件,最终版本以真实诊断和共同支持矩阵为准。
|
||||
- 原生地址直接指向精确上游工件;OSS 镜像保存该工件的字节完全相同副本。
|
||||
- 展示下载、校验、安装、取消、可更新、删除和失败状态。
|
||||
- 下载失败保留已验证旧安装。
|
||||
|
||||
### FR-3 自定义环境
|
||||
|
||||
- 探测 PATH 和常见安装位置中的有效候选。
|
||||
- 探测当前虚拟环境、Conda 环境以及用户/系统常见解释器位置。
|
||||
- 候选同时展示版本和规范化绝对路径。
|
||||
- 用户可以选择未被探测到的可执行文件。
|
||||
- “自定义环境”入口在没有候选时仍可操作,不自动选择首个候选。
|
||||
- 保存前执行真实诊断。
|
||||
- 已保存路径失效时保持用户选择并报错,不静默回退。
|
||||
|
||||
### FR-4 SKILL 接入
|
||||
|
||||
- SKILL 可以稳定调用 `node`、`npm`、`npx`、`python`、`python3` 和 `pip`。
|
||||
- SKILL 不需要知道解释器真实安装目录。
|
||||
- 缺失解释器或第三方依赖时返回准确错误。
|
||||
- 工具环境不自动安装 SKILL 声明的依赖。
|
||||
|
||||
### FR-5 stdio MCP 接入
|
||||
|
||||
- 裸命令通过当前工具环境解析。
|
||||
- `env node`、`env python3` 形式的脚本使用当前工具环境。
|
||||
- MCP 中配置的绝对命令路径保持原样,不被替换。
|
||||
- Python 和 Node.js MCP 均使用与 SKILL 相同的来源。
|
||||
|
||||
### FR-6 状态与诊断
|
||||
|
||||
- 工具执行环境页只显示有真实诊断结果的解释器和配套工具状态,不显示固定的能力依赖占位
|
||||
状态。
|
||||
- 具体 SKILL/MCP 只有在其实际入口及第三方依赖通过验证后,才在对应能力详情中显示能力
|
||||
依赖状态。
|
||||
- Node 诊断覆盖 Node、npm、npx、标准库和子进程。
|
||||
- Python 诊断覆盖 Python 3、架构、标准库、SSL、pip 和 venv。
|
||||
- 诊断使用临时目录并在结束后清理。
|
||||
|
||||
### FR-7 生效范围
|
||||
|
||||
- 设置只对新启动的本机 Runtime 和 stdio MCP 生效。
|
||||
- 已运行进程不被中途替换环境。
|
||||
- 不影响普通终端、系统环境和远程主机。
|
||||
|
||||
### FR-8 升级兼容
|
||||
|
||||
- 全新用户默认选择 GoodBuddy 托管。
|
||||
- 已发布版本升级时,将当前有效系统解释器一次性固化为自定义绝对路径。
|
||||
- 没有有效系统解释器时迁移到托管来源。
|
||||
- 后续系统 PATH 变化不自动改变选择。
|
||||
|
||||
### FR-9 生命周期
|
||||
|
||||
- 安装、诊断、Runtime 和 MCP 子进程支持取消和应用退出清理。
|
||||
- 删除只作用于 GoodBuddy-owned 目录。
|
||||
- 不删除系统解释器、自定义环境、用户工作区或远程文件。
|
||||
|
||||
### FR-10 权限模式
|
||||
|
||||
- Ask 不因解释器可用而获得额外命令或写入权限。
|
||||
- Execute 继续使用当前本机账号的完整授权。
|
||||
- 不为工具环境增加第二套批准流程。
|
||||
|
||||
### FR-11 工具下载源
|
||||
|
||||
- “工具执行环境”Tab 提供两个互斥选项:`原生地址` 和 `OSS 镜像`。
|
||||
- 全新用户默认使用原生地址;该选择独立于“关于与更新”和模型下载源。
|
||||
- 选择原生地址时,每个受管工件使用目录中固定的上游 URL。
|
||||
- 选择 OSS 镜像时,使用 GoodBuddy OSS 中固定的对应对象。
|
||||
- 两个 Target 必须声明同一逻辑工件、相同字节数和 SHA-256;OSS 不得重打包或修改内容。
|
||||
- 下载开始时冻结来源;运行期间切换设置只影响后续下载。
|
||||
- 当前来源失败、缺少工件或校验失败时明确失败,不自动请求另一个来源。
|
||||
- 该设置适用于当前 Python Runtime 和未来纳入受管工具目录的 Runtime/工具链。
|
||||
- 该设置不影响用户执行的 pip/npm 依赖安装、模型下载、应用更新、Agent 下载、HTTP/SSE
|
||||
MCP 或用户配置 URL。
|
||||
|
||||
### FR-12 统一设置入口
|
||||
|
||||
- 设置中心只提供一个“能力与工具”一级分类,不再并列显示独立的 Skills、MCP 或工具执行
|
||||
环境分类。
|
||||
- 第一阶段提供 `Skills`、`MCP` 两个水平 Tab;工具执行环境完成后依次提供
|
||||
`Skills`、`MCP`、`工具执行环境` 三个水平 Tab。始终默认打开 `Skills`。
|
||||
- 三个 Tab 使用共享 `PageTabs`,保留 `tablist`、`tab`、`tabpanel`、方向键和可见焦点。
|
||||
- `Skills` 继续承载现有 Skill 启停、导入、状态和 Runtime 分配。
|
||||
- 内置 Skill 顶层目录只展示五个独立能力:`deai-writing`、`longdoc-docx`、
|
||||
`product-evidence`、`product-marketing`、`product-presentation`。产品营销的功能目录、
|
||||
一页纸、技术方案、白皮书、招标、演示套件、客户案例和竞品定位作为
|
||||
`product-marketing` 内部工作流,不重复显示为独立 Skill。
|
||||
- `deai-writing` 和 `longdoc-docx` 默认启用;`product-evidence`、
|
||||
`product-marketing`、`product-presentation` 默认停用。已保存的用户启停和 Runtime
|
||||
分配始终优先于默认值;导入 Skill 的默认行为保持不变。
|
||||
- `MCP` 继续承载现有 MCP 配置、凭据、状态和 Runtime 分配;其现有内部分组页签保持不变。
|
||||
- `工具执行环境` 承载工具下载源、Node.js、Python、诊断和生效范围。
|
||||
- 合并导航不改写 Skills/MCP 业务数据;未来新增定向入口时必须进入对应水平 Tab。
|
||||
- 切换水平 Tab 不丢弃当前编辑状态;不把当前水平 Tab 持久化为产品设置。
|
||||
- 未实现的工具执行环境不得以禁用 Tab 或占位内容提前显示。
|
||||
|
||||
## 6. 产品验收
|
||||
|
||||
- 无系统 Node.js 时,托管 Node 可以真实执行 JavaScript SKILL 和 stdio MCP。
|
||||
- 无系统 Python 时,用户可安装托管 Python 并真实执行现有 Python SKILL 和 MCP。
|
||||
- Node、npm、npx 来源一致;Python、python3、pip 来源一致。
|
||||
- 用户可明确选择并锁定自定义解释器。
|
||||
- 自定义路径失效不触发静默切换。
|
||||
- 工具就绪与能力依赖就绪不会混为同一状态。
|
||||
- 本机 Skills、MCP 和工具执行环境不会应用于远程主机。
|
||||
- 工具下载使用用户明确选择的原生地址或 OSS,且不会静默回退。
|
||||
- 第一阶段通过“能力与工具”统一入口展示 Skills/MCP 两个水平 Tab;工具执行环境完成后
|
||||
追加第三个 Tab。
|
||||
- 中英文界面、键盘操作、焦点和状态反馈符合统一设计系统。
|
||||
- 全量测试、类型检查、Lint 和生产构建通过。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 工具执行环境功能进度
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档类型 | 功能进度 |
|
||||
| 状态 | 已实施,发布候选 CI 验收中 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 关联入口 | [工具执行环境](./README.md) |
|
||||
|
||||
## 当前结论
|
||||
|
||||
本机工具执行环境的生产链路已经进入源码。“能力与工具”现在显示 `Skills`、`MCP`、
|
||||
`工具执行环境` 三个真实 Tab;Main 统一解析托管或自定义 Node/Python,并把不可变 PATH
|
||||
快照应用到新启动的本机 Runtime 与 stdio MCP。Windows x64 已完成真实托管 Node 和原生
|
||||
地址托管 Python 安装验证。
|
||||
|
||||
六个 OSS 镜像对象已发布并公开回读验证。托管 Python 继续按需下载,Desktop 发行包不额外
|
||||
带入许可证文件。标准六平台打包任务已改用目标架构原生 Runner,并在打包前执行托管 Python
|
||||
的真实安装、SSL、pip 和 venv 探针;`0.12.0` 候选仍需通过这组 CI 才能完成对应跨平台验收。
|
||||
|
||||
## 已完成
|
||||
|
||||
### 设置、迁移和界面
|
||||
|
||||
- [x] Shared 严格设置、候选、诊断、进度、IPC 和 Preload 契约。
|
||||
- [x] Application Settings v10;全新用户默认托管环境和原生地址。
|
||||
- [x] v1-v9 分别执行真实 Node/Python 3 探测的一次性迁移;无有效候选时选择托管环境。
|
||||
- [x] 用户/系统 PATH、活动 venv/Conda、`CONDA_ENVS_PATH`、Conda/pyenv/nvm 和常见安装
|
||||
位置,以及已保存自定义路径候选的规范化、去重、版本及架构诊断。
|
||||
- [x] 自定义解释器保存前真实验证;无效新选择不持久化,已保存失效路径不回退。
|
||||
- [x] “自定义环境”始终可进入;不隐式采用首个候选,取消文件选择不改设置,验证错误在
|
||||
对应工具卡内显示。
|
||||
- [x] “能力与工具”统一入口及三个水平 Tab,MCP 原内层页签保持不变。
|
||||
- [x] 工具下载源、Node.js、Python、候选、诊断、安装进度、取消和删除界面。
|
||||
- [x] 工具卡只展示真实工具诊断;未接入具体能力证据前不显示固定的能力依赖占位状态。
|
||||
- [x] 使用 `PageTabs`、应用通知、就地操作错误、删除确认和具名进度条。
|
||||
|
||||
### 托管 Node 与进程注入
|
||||
|
||||
- [x] 使用 Electron-as-Node 提供 `node`,使用同一 Node 和打包 CLI 提供 `npm`、`npx`。
|
||||
- [x] 自定义 Node 只使用同安装位置且验证成功的 npm/npx,不与托管 CLI 混用。
|
||||
- [x] 每次配置生成不可变 shim 目录;已有进程继续持有原 PATH,新进程读取新快照。
|
||||
- [x] OpenCode、Continue、DeepSeek Harness 和本机通用/精选 stdio MCP 接收工具 PATH。
|
||||
- [x] 工具 PATH 不修改 `process.env`,不进入普通终端或远程 Host。
|
||||
- [x] 解释器探针使用过滤后的环境,不接收模型凭据、GoodBuddy 或 Factory 内部变量。
|
||||
|
||||
### 托管 Python
|
||||
|
||||
- [x] 锁定 CPython 3.13.15 的 Windows/macOS/Linux x64/ARM64 六平台目录、大小和 SHA-256。
|
||||
- [x] 用户选择的原生地址或 OSS 镜像在任务开始时冻结,同一次任务不回退。
|
||||
- [x] HTTPS、重定向 Host、大小、SHA-256、取消和部分文件清理。
|
||||
- [x] NuGet ZIP 与 Astral TAR 的 payload 限定、路径穿越防护、展开上限和安全链接处理。
|
||||
- [x] GoodBuddy-owned 暂存安装、发布、失败清理、更新保留和删除。
|
||||
- [x] `python`、`python3`、`pip` 同源,`pip` 固定使用所选 Python 的 `-m pip`。
|
||||
- [x] 发布前真实验证 Python 3.13.15、架构、标准库、SSL、pip,并创建和执行临时 venv。
|
||||
- [x] 取消恢复原状态且不显示为安装失败;退出时取消并等待安装清理。
|
||||
|
||||
## 待完成验收
|
||||
|
||||
- [x] 发布六个平台/架构的字节完全相同 OSS 镜像对象并公开回读校验。
|
||||
- [x] 确认托管 Python 保持按需下载,Desktop 发行包不额外带入许可证文件。
|
||||
- [ ] Windows ARM64、macOS x64/ARM64、Linux x64/ARM64 原生 CI 安装、SSL、pip、venv 探针;工作流已接线,等待 `0.12.0` 候选运行结果。
|
||||
- [ ] 最低 Windows/macOS/glibc 支持矩阵和 Windows NuGet 签名核验。
|
||||
- [ ] 真实 JavaScript/Python SKILL 成果及 Node/Python stdio MCP 调用。
|
||||
- [ ] 自定义 Node/Python 在产品 UI 中的真实选择和任务执行。
|
||||
- [ ] 更新或删除托管 Python 时,使用现有本机 Runtime/MCP 生命周期协调受影响进程。
|
||||
|
||||
## 当前阻塞项
|
||||
|
||||
- 当前机器只能原生验证 Windows x64。其余五个平台必须由对应原生 CI 执行,不能用本机
|
||||
mock 代替。
|
||||
|
||||
## 已验证证据
|
||||
|
||||
### 2026-09-02 完整实现
|
||||
|
||||
- 发布准备确认不把按需下载的托管 Python 许可证文件加入 Desktop `extraResources`、解包
|
||||
校验或 Portable ZIP 必需文件,避免该额外资源改变现有编译和打包路径。
|
||||
- 六平台打包矩阵在 `npm ci` 后先断言 Runner 架构,再执行
|
||||
`GOODBUDDY_RUN_LIVE_TOOL_ENVIRONMENT=1` 的托管 Python 安装探针;Windows arm64 改用
|
||||
`windows-11-vs2026-arm` 原生 Runner。发行资源与目录聚焦测试 `47/47` 通过。
|
||||
|
||||
- `npm run typecheck` 通过。
|
||||
- `npm run lint` 通过。
|
||||
- `npm run build` 通过,Main、Preload 和 Renderer 生产 bundle 成功。
|
||||
- 工具环境聚焦测试通过:契约、设置迁移、解析/shim、服务、下载、解压、安装、IPC、
|
||||
Preload、Runtime/MCP 注入、Renderer 和发行资源校验。
|
||||
- Windows x64 真实托管 Node:Electron Node `24.18.0`、npm `11.19.0`、npx `11.19.0`。
|
||||
- `GOODBUDDY_RUN_LIVE_TOOL_ENVIRONMENT=1` 真实下载官方 NuGet、校验摘要、解压、安装并
|
||||
验证 CPython `3.13.15`、SSL、pip 和 venv,`1/1` 通过;临时安装已清理。
|
||||
- 隔离 Electron 生产 bundle 自动化确认三个 Tab 均可见;“诊断全部”显示托管 Node
|
||||
`24.18.0`、npm/npx `11.19.0`。隔离进程和用户数据已清理。
|
||||
- 第一次全量测试发现 3 个确定性夹具/兼容失败,修复后聚焦复跑 `25/25` 通过。第二次暴露
|
||||
2 个 `App.test.tsx` 设置导航时序问题,修正为按可访问名称点击并等待设置中心。
|
||||
- 最终 `npm test` 通过:`3465` 通过、`51` 跳过;完整 `App.test.tsx` 为 `157/157` 通过。
|
||||
- 使用本机受控 Aliyun 凭据将六个已验证上游工件原样上传到
|
||||
`goodbuddy/tool-artifacts/python/3.13.15/<sha256>/<filename>`,总计 `142,251,693`
|
||||
字节。随后从目录中的六个公开 OSS URL 全量下载,逐个验证字节数和 SHA-256,全部一致。
|
||||
- 修复无候选时禁用“自定义环境”的死路交互,并扩展 venv/Conda/常见安装位置探测;
|
||||
聚焦 Main/Renderer/Settings 测试 `95/95` 通过,最终 `npm test` 为 `3477` 通过、
|
||||
`51` 跳过,类型检查、功能文件定向 Lint 和生产构建通过。仓库全量 Lint 当前被并行
|
||||
`App.tsx` 的两个 React Compiler memoization 错误阻塞,与工具执行环境文件无关。
|
||||
- 隔离 Electron 实机确认“自定义环境”始终可进入,展开和刷新不会保存来源;在 64 项以上
|
||||
PATH 的机器上优先发现活动 `<user>\miniconda3\python.exe`,真实诊断为
|
||||
Python `3.13.13 · amd64`,刷新后持久设置仍为 `managed`。
|
||||
|
||||
### 2026-09-02 菜单阶段
|
||||
|
||||
- 完成“能力与工具”一级菜单及 Skills/MCP Tab,并修复可滚动 Flex 布局将页签压缩为零
|
||||
高度的问题;加入 `flex: 0 0 auto` 回归约束。
|
||||
|
||||
## 进度维护要求
|
||||
|
||||
- 只有真实源码和生产路径完成后才能勾选对应实施项。
|
||||
- 测试失败时保留未完成状态并记录失败原因。
|
||||
- 每次更新记录日期、实际命令、结果和外部阻塞。
|
||||
- 不在本文件复制未来设计,需求变化回写 PRD/UI/技术设计。
|
||||
@@ -0,0 +1,370 @@
|
||||
# 工具执行环境技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台验收中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 关联 PRD | [工具执行环境 PRD](./prd.md) |
|
||||
| 功能逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) |
|
||||
| 相关架构 | [DeepSeek Harness Runtime 设计](../deepseek-harness/technical-design.md)、[SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md) |
|
||||
|
||||
## 1. 当前基线
|
||||
|
||||
GoodBuddy 已具备:
|
||||
|
||||
- Electron 内置 Node Runtime。
|
||||
- 安装包中的锁定 npm CLI。
|
||||
- DSH 插件安装器使用的 `process.execPath + ELECTRON_RUN_AS_NODE=1` shim。
|
||||
- OpenCode、Continue、DeepSeek Harness 的独立本机进程环境入口。
|
||||
- SKILL 按 Runtime 目标分配和暂存。
|
||||
- Main 和 Runtime 的 stdio MCP 启动链路。
|
||||
- Runtime 可执行文件探测、版本读取和自定义路径模式。
|
||||
- 下载、摘要校验、进度、取消和暂存清理模式。
|
||||
|
||||
当前缺少统一工具来源设置、通用 PATH 注入和托管 Python。
|
||||
|
||||
## 2. 设置契约
|
||||
|
||||
```ts
|
||||
type LocalToolEnvironmentSelection =
|
||||
| { source: 'managed' }
|
||||
| { source: 'custom'; executablePath: string }
|
||||
|
||||
type LocalToolEnvironmentSettings = {
|
||||
node: LocalToolEnvironmentSelection
|
||||
python: LocalToolEnvironmentSelection
|
||||
artifactDownloadSource: 'native' | 'oss'
|
||||
}
|
||||
```
|
||||
|
||||
只持久化用户意图。不持久化候选列表、版本、PATH、shim 路径、诊断结果或可重新读取的托管
|
||||
包元数据。
|
||||
|
||||
### 2.1 兼容迁移
|
||||
|
||||
- 全新用户默认 `managed`。
|
||||
- 旧设置没有该字段时,Main 按升级前 PATH 分别解析有效 Node 和 Python 3。
|
||||
- 有效候选写成 `custom + 规范化绝对路径`。
|
||||
- 没有有效候选时写成 `managed`。
|
||||
- 新增的 `artifactDownloadSource` 默认迁移为 `native`。
|
||||
- 迁移只执行一次,后续 PATH 变化不改变选择。
|
||||
|
||||
## 3. Main 服务
|
||||
|
||||
新增一个 `LocalToolEnvironmentService`,负责:
|
||||
|
||||
- 设置读取和保存。
|
||||
- Node/Python 候选探测。
|
||||
- 自定义解释器诊断。
|
||||
- 当前工具组合解析。
|
||||
- 受管工具 bin 创建。
|
||||
- 托管 Python 安装、更新、删除。
|
||||
- 有界状态和进度发布。
|
||||
- 应用退出时取消安装并清理本次暂存。
|
||||
|
||||
不为每项职责增加持久 Manager、注册表或恢复状态机。失败安装删除暂存,下次重新执行。
|
||||
|
||||
## 4. 解析结果
|
||||
|
||||
```ts
|
||||
type ResolvedLocalToolEnvironment = {
|
||||
binDirectory: string
|
||||
node?: {
|
||||
executablePath: string
|
||||
npmCliPath?: string
|
||||
npxCliPath?: string
|
||||
}
|
||||
python?: {
|
||||
executablePath: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
工具 bin:
|
||||
|
||||
| 平台 | 命令 |
|
||||
| --- | --- |
|
||||
| Windows | `node.cmd`、`npm.cmd`、`npx.cmd`、`python.cmd`、`python3.cmd`、`pip.cmd` |
|
||||
| macOS/Linux | `node`、`npm`、`npx`、`python`、`python3`、`pip` |
|
||||
|
||||
shim 使用绝对路径。`pip` 固定调用 `<python> -m pip`,避免解释器错配。
|
||||
|
||||
## 5. Node.js 托管环境
|
||||
|
||||
托管 Node shim:
|
||||
|
||||
```text
|
||||
ELECTRON_RUN_AS_NODE=1 <process.execPath> <arguments...>
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
- 复用 DSH 已有的平台转义实现。
|
||||
- `ELECTRON_RUN_AS_NODE` 不写入 Main 全局环境。
|
||||
- npm/npx 使用安装包中同一锁定 npm 发行版的 CLI。
|
||||
- npm 子进程范围设置 `npm_execpath` 和 `npm_node_execpath`。
|
||||
- 自定义 Node 只使用与其配套且验证成功的 npm/npx,不混入 GoodBuddy npm。
|
||||
- 普通 JavaScript 和 Node 内置模块属于支持路径。
|
||||
- 普通 Node 原生扩展可能受 Electron ABI 影响,不承诺兼容。
|
||||
|
||||
## 6. Python 托管环境
|
||||
|
||||
### 6.1 包
|
||||
|
||||
- 使用包含 pip、venv、SSL 和标准库的固定 CPython 独立发行包。
|
||||
- 每个平台/架构锁定一个版本。
|
||||
- Windows x64 原生工件来自官方 `python` NuGet 包,Windows ARM64 来自官方
|
||||
`pythonarm64` NuGet 包。
|
||||
- macOS/Linux 原生工件来自 Astral `python-build-standalone`。
|
||||
- 目录包含版本、平台、架构、字节数、SHA-256、原生 Target 和 OSS Target。
|
||||
- OSS 对象直接复制原生工件字节,不重新压缩或转换。
|
||||
- 安装包不无条件携带完整 Python。
|
||||
|
||||
具体发行包和版本必须在实现前完成六平台工件、维护状态和 pip/venv 可用性核验。
|
||||
|
||||
目录示例:
|
||||
|
||||
```ts
|
||||
type ManagedToolArtifact = {
|
||||
id: string
|
||||
version: string
|
||||
platform: NodeJS.Platform
|
||||
architecture: string
|
||||
size: number
|
||||
sha256: string
|
||||
downloads: {
|
||||
native: {
|
||||
url: string
|
||||
redirectHosts: string[]
|
||||
}
|
||||
oss: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
URL 只存在于 Main/构建目录,不进入 Renderer。两个 Target 共用顶层 `size` 和 `sha256`,
|
||||
从 schema 上禁止声明不同字节。
|
||||
|
||||
### 6.2 安装
|
||||
|
||||
```text
|
||||
读取固定目录
|
||||
→ 下载到 GoodBuddy-owned 暂存
|
||||
→ 校验大小和 SHA-256
|
||||
→ 解压到新版本目录
|
||||
→ 运行完整环境诊断
|
||||
→ 发布为当前托管 Python
|
||||
→ 删除暂存
|
||||
```
|
||||
|
||||
失败保留已验证旧安装。删除只作用于 GoodBuddy-owned Python 目录。
|
||||
|
||||
### 6.3 包依赖
|
||||
|
||||
首期提供解释器和 pip,不自动扫描或安装任意 `requirements.txt`。第三方包由 SKILL/MCP
|
||||
安装流程或用户明确命令准备。出现可复现的包冲突后,再评估能力级虚拟环境。
|
||||
|
||||
## 7. 自定义环境探测
|
||||
|
||||
### 7.1 Node
|
||||
|
||||
候选来源:
|
||||
|
||||
- GoodBuddy 启动时继承的合并 PATH,其中包含操作系统提供的用户 PATH 和系统 PATH。
|
||||
- 当前 `VIRTUAL_ENV`、`CONDA_PREFIX`、`NVM_HOME`/`NVM_BIN` 和 `VOLTA_HOME`。
|
||||
- Windows 用户目录和 ProgramData 下的 Conda/Miniconda/Anaconda base 与 `envs`,
|
||||
LocalAppData Python,以及 Program Files/系统盘常见 Node/Python 目录。
|
||||
- macOS/Linux 的 `/usr/local/bin`、`/usr/bin`、Homebrew、本地用户 bin、Python
|
||||
Framework、Conda、pyenv 和 nvm 常见目录。
|
||||
- `CONDA_ENVS_PATH` 中的有界环境目录。
|
||||
- 已保存路径。
|
||||
|
||||
目录枚举有总量和单目录上限,不递归扫描整块磁盘。每个候选使用分离参数启动检查,确认
|
||||
普通文件、成功退出、可解析版本和架构,并按真实路径去重。
|
||||
|
||||
### 7.2 Python
|
||||
|
||||
- Windows 探测 `python.exe`。
|
||||
- macOS/Linux 优先 `python3`,其次为明确的 Python 3 `python`。
|
||||
- Conda/venv 候选使用各环境自己的解释器,不依赖环境激活脚本。
|
||||
- 拒绝未安装的 Windows Store App Execution Alias。
|
||||
- 诊断 Python 3、版本、架构和解释器绝对路径。
|
||||
|
||||
单候选和总探测具有超时、输出上限、取消和进程树清理。
|
||||
|
||||
## 8. 完整诊断
|
||||
|
||||
### 8.1 Node
|
||||
|
||||
- `node --version`
|
||||
- 加载标准库
|
||||
- 启动最小子进程
|
||||
- `npm --version`
|
||||
- `npx --version`
|
||||
- npm/npx 实际 Node 来源一致
|
||||
|
||||
### 8.2 Python
|
||||
|
||||
- Python 3、版本和架构
|
||||
- 标准库导入
|
||||
- SSL/证书模块
|
||||
- `python -m pip --version`
|
||||
- 在临时目录创建最小 venv
|
||||
|
||||
诊断不联网,使用 GoodBuddy-owned 临时目录并清理。
|
||||
|
||||
## 9. 环境注入
|
||||
|
||||
新启动的以下本机进程将工具 bin 放到现有筛选 PATH 前面:
|
||||
|
||||
- OpenCode Server。
|
||||
- Continue Utility Process。
|
||||
- DeepSeek Harness Utility Process。
|
||||
- Main 启动的 stdio MCP。
|
||||
- Runtime 启动的 stdio MCP。
|
||||
|
||||
不注入 Main 全局环境、Renderer、普通终端、SSH 请求、Agent 或远程 Runtime。
|
||||
|
||||
设置变化后,有活动请求的 Runtime 保持启动时快照;空闲 Runtime 在下次使用前使用既有
|
||||
重建入口。已运行 MCP 在正常关闭、重连或显式测试后使用新环境,不增加热切换协议。
|
||||
|
||||
## 10. SKILL 与 MCP
|
||||
|
||||
SKILL 稳定调用命令名,不接收真实安装路径。
|
||||
|
||||
- `resources/skills` 顶层只保留五个带 `SKILL.md` 的独立包:
|
||||
`deai-writing`、`longdoc-docx`、`product-evidence`、`product-marketing`、
|
||||
`product-presentation`。
|
||||
- 九种纯 Markdown 产品营销配方位于 `product-marketing/workflows/<route-id>/`,
|
||||
使用 `WORKFLOW.md` 与同目录 `templates/`,不参与顶层 Skill 发现。
|
||||
- `CapabilityService` 仅在状态中没有该 ID 时应用默认值。三个产品营销 Skill 默认
|
||||
`enabled: false`;两个通用文档工具和导入 Skill 维持默认启用。显式保存状态不迁移、
|
||||
不覆盖。
|
||||
- 默认停用的 Skill 不进入 Runtime 指令和原生 Skill 包列表;用户启用后沿用现有四个
|
||||
Runtime 分配。
|
||||
|
||||
stdio MCP:
|
||||
|
||||
- 裸 `node`、`npm`、`npx`、`python`、`python3`、`pip` 使用工具 bin。
|
||||
- `env node`/`env python3` 脚本通过 PATH 使用同一来源。
|
||||
- 绝对 command 保持原样。
|
||||
- HTTP/SSE 不受影响。
|
||||
- 工具环境不自动安装 MCP 第三方依赖。
|
||||
|
||||
## 11. Renderer 状态与 IPC
|
||||
|
||||
### 11.1 统一设置分类
|
||||
|
||||
- `settingsCategoryList` 用一个 `capabilities` 分类替换现有 `skills`、`mcp` 分类,不增加
|
||||
`sdk` 分类。
|
||||
- 新增 `CapabilitiesAndToolsSettingsSection`,在统一分类标题下使用共享 `PageTabs` 渲染
|
||||
第一阶段的 `skills | mcp`。
|
||||
- `SkillsSettingsSection` 和 `McpSettingsSection` 继续拥有各自业务状态;嵌入时不重复渲染
|
||||
一级分类标题。MCP 原有内层 `PageTabs` 保持独立。
|
||||
- `ToolEnvironmentSettingsSection` 只承载工具下载源、Node.js、Python 和诊断。
|
||||
- 当前源码没有 Settings 外部 Skills/MCP 定向入口,第一阶段不增加未使用的
|
||||
`initialCapabilityTab`。未来出现真实定向入口时再增加会话级初始 Tab 参数。
|
||||
- 普通进入 `capabilities` 时默认 `skills`。外层选中值只存在于当前 Renderer 会话,不写入
|
||||
Shared 设置。
|
||||
- 为避免切换时丢失编辑状态,外层面板首次访问时挂载,之后在本次设置面板会话中保留并用
|
||||
`hidden` 控制可见性;关闭设置面板后按现有生命周期释放。
|
||||
- 工具执行环境生产路径完成时,把 `tool-environment` 追加到同一个组件;完成前不注册
|
||||
Tab、不渲染占位面板。
|
||||
- 页签直接包装层使用 `flex: 0 0 auto`,并由 CSS 回归测试保证在滚动设置布局中不可收缩。
|
||||
|
||||
### 11.2 工具环境状态与 IPC
|
||||
|
||||
Renderer 接收:
|
||||
|
||||
- 当前选择。
|
||||
- 当前实际来源、版本、路径。
|
||||
- 有界候选列表。
|
||||
- 托管安装状态、版本、占用空间。
|
||||
- 工具诊断分项结果。
|
||||
- 安装进度和脱敏错误。
|
||||
- 受管工具工件下载源选择,以及活动任务实际冻结的来源。
|
||||
|
||||
Renderer 不能提交 PATH、环境变量、下载 URL、摘要、安装目录、shim 内容或任意命令参数。
|
||||
所有 IPC 输入使用共享 Zod schema,Main 重新验证路径和解释器。
|
||||
|
||||
## 12. 受管工具工件下载
|
||||
|
||||
- 设置值为 `native | oss`,默认 `native`。
|
||||
- 下载操作开始时读取并冻结设置。
|
||||
- 解析目录中对应 Target;缺失即返回“当前下载源不可用”。
|
||||
- 原生 Target 按每个上游声明严格 Host/重定向 allowlist。
|
||||
- OSS Target 只允许 GoodBuddy 固定 OSS Host 和不可变对象前缀。
|
||||
- 两种来源共用大小和 SHA-256 校验、暂存、取消、发布和清理逻辑。
|
||||
- 失败不重试另一来源。
|
||||
- 构建/发布流程先下载原生工件、校验,再把原字节同步到 OSS,并复核公开对象。
|
||||
- 该设置不传给 pip/npm,不改写其 registry;也不参与模型、应用更新和 Agent 目录选择。
|
||||
|
||||
## 13. 权限与远程边界
|
||||
|
||||
- Ask/Execute 语义不变。
|
||||
- 不新增解释器批准流程。
|
||||
- 不上传 shim、Python、pip 包或自定义路径。
|
||||
- 不修改 Host PATH。
|
||||
- 不把本机 SKILL 或 stdio MCP 传给远程 ACP Session。
|
||||
- 远程 Agent 固定 Node 继续只服务 Agent、模型桥和签名 Runtime。
|
||||
|
||||
## 14. 实施顺序
|
||||
|
||||
### 阶段 1:契约、迁移、探测和 UI
|
||||
|
||||
- Shared schema、设置迁移、IPC。
|
||||
- 用“能力与工具”分类合并现有 Skills/MCP 入口,增加三个外层水平 Tab。
|
||||
- 增加“工具执行环境”面板及原生/OSS 下载源设置。
|
||||
- Node/Python 候选探测和完整诊断。
|
||||
- 设置卡片和状态。
|
||||
|
||||
### 阶段 2:托管 Node 与执行链路
|
||||
|
||||
- 提取 DSH shim。
|
||||
- 增加 node/npm/npx。
|
||||
- 注入本机 Runtime 和 stdio MCP。
|
||||
|
||||
### 阶段 3:托管 Python
|
||||
|
||||
- 锁定六平台工件。
|
||||
- 下载、校验、安装、更新、取消、删除。
|
||||
- 增加 python/python3/pip。
|
||||
|
||||
### 阶段 4:真实端到端验收
|
||||
|
||||
- Node SKILL 与 MCP。
|
||||
- Python SKILL 与 MCP。
|
||||
- 自定义解释器。
|
||||
- Ask/Execute、重建、取消和退出。
|
||||
- 六平台打包。
|
||||
|
||||
这些阶段是开发顺序,不是可长期发布的残缺功能。合并能力前完成普通用户和专业用户的完整
|
||||
生产路径。
|
||||
|
||||
## 15. 验证
|
||||
|
||||
单元测试覆盖设置迁移、探测、版本解析、shim 转义、PATH 顺序、失效路径、下载摘要和暂存
|
||||
清理,以及原生/OSS 选择冻结、Target 缺失和禁止回退。
|
||||
|
||||
Renderer 测试覆盖统一一级分类、三个外层 Tab、默认 Skills、Skills/MCP 定向入口、会话内
|
||||
状态保留、MCP 内外层 tablist 隔离、键盘切换和窄屏单行滚动。
|
||||
|
||||
集成测试覆盖安装包 Electron Node、npm/npx、托管 Python、三个本机 Runtime 和两类 stdio
|
||||
MCP 的实际进程路径。
|
||||
|
||||
项目验证:
|
||||
|
||||
```text
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
生产资源通过六平台原生 CI 打包验证。若后续改动触及 Agent、远程协议或远程 Runtime,
|
||||
再按远程架构文档执行共享 Linux x64 Host 验证。
|
||||
@@ -0,0 +1,305 @@
|
||||
# 工具执行环境 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 关联 PRD | [工具执行环境 PRD](./prd.md) |
|
||||
| 功能逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文只定义本功能的界面结构和交互。颜色、字体、间距、通知、Switch、焦点和响应式行为以
|
||||
全局设计系统为最终权威,不在本文件建立页面专属视觉体系。
|
||||
|
||||
## 1. 信息架构
|
||||
|
||||
设置中心把现有 `Skills`、`MCP` 一级分类和新增的工具执行环境合并为一个“能力与工具”
|
||||
一级分类。内容区使用三个水平 Tab:
|
||||
|
||||
```text
|
||||
[ Skills ] [ MCP ] [ 工具执行环境 ]
|
||||
```
|
||||
|
||||
一级分类副标题固定为:
|
||||
|
||||
> 管理 Skills、MCP 和本机工具执行环境
|
||||
|
||||
“工具执行环境”不放入任一 Runtime 专属卡片。该 Tab 顶部持续显示:
|
||||
|
||||
> 用于新启动的本机 Runtime 和 stdio MCP;不修改系统环境,也不应用于普通终端或远程主机。
|
||||
|
||||
### 1.1 分阶段显示
|
||||
|
||||
第一阶段只实现统一菜单和现有能力页签:
|
||||
|
||||
```text
|
||||
[ Skills ] [ MCP ]
|
||||
```
|
||||
|
||||
此阶段不显示“工具执行环境”禁用 Tab、空页面或“开发中”占位。Node.js、Python 和工具下载源
|
||||
真实可用后,再按本设计追加第三个水平 Tab。
|
||||
|
||||
“能力与工具”的页签条位于独立滚动的 Flex 设置内容区,直接包装层必须保持
|
||||
`flex: 0 0 auto`,避免被下方长列表压缩到零高度。该规则同时服从根
|
||||
[`UI-DESIGN.md`](../../../UI-DESIGN.md) 的 `PageTabs` 规范。
|
||||
|
||||
## 2. 页面结构
|
||||
|
||||
```text
|
||||
能力与工具
|
||||
管理 Skills、MCP 和本机工具执行环境
|
||||
|
||||
[ Skills ] [ MCP ] [ 工具执行环境 ]
|
||||
|
||||
Skills 面板
|
||||
现有 Skill 启停、导入、状态和 Runtime 分配
|
||||
五个内置 Skill;产品营销三个默认停用,通用文档工具两个默认启用
|
||||
|
||||
MCP 面板
|
||||
现有 MCP 配置、凭据、状态和 Runtime 分配
|
||||
[内置 MCP] [直连模型] [自定义 MCP] [电脑控制]
|
||||
|
||||
工具执行环境面板
|
||||
范围说明
|
||||
|
||||
工具下载源
|
||||
原生地址 / OSS 镜像
|
||||
|
||||
Node.js 卡片
|
||||
来源
|
||||
当前状态
|
||||
自定义候选或路径操作
|
||||
测试环境
|
||||
|
||||
Python 卡片
|
||||
来源
|
||||
当前状态
|
||||
安装/更新/删除或自定义路径操作
|
||||
测试环境
|
||||
```
|
||||
|
||||
产品营销的九种文档配方不单独占用 Skill 卡片。用户启用 `product-marketing` 后,
|
||||
由该 Skill 根据任务按需读取内部工作流。界面只展示能够独立启停和分配 Runtime 的
|
||||
五个顶层内置 Skill;已保存状态覆盖产品默认值。
|
||||
|
||||
工具执行环境面板内的区块顺序固定为工具下载源、Node.js、Python,不再增加页签。
|
||||
|
||||
## 3. 三个水平 Tab
|
||||
|
||||
外层页签使用共享 `PageTabs` 的 `segmented` 视觉变体,不使用页面自定义 Tab 或普通
|
||||
`SegmentedControl`:
|
||||
|
||||
```tsx
|
||||
<PageTabs
|
||||
ariaLabel="能力与工具设置"
|
||||
idPrefix="capabilities-and-tools"
|
||||
tabs={[
|
||||
{ id: 'skills', label: 'Skills' },
|
||||
{ id: 'mcp', label: 'MCP' },
|
||||
{ id: 'tool-environment', label: '工具执行环境' }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
交互规则:
|
||||
|
||||
- 普通入口默认打开 `Skills`。
|
||||
- 现有 Skills/MCP 应用内入口分别定位到对应水平 Tab。
|
||||
- 切换外层 Tab 不保存为产品设置,不修改任何 Skill、MCP 或工具环境值。
|
||||
- 三个面板在同一次设置面板会话中保留编辑状态;切换后返回不得重置草稿。
|
||||
- MCP 原有四个内层页签继续保留,外层和内层使用不同的 `aria-label` 与 `idPrefix`。
|
||||
- 窄屏时外层页签保持单行并允许水平滚动,不折成多行,不替换为下拉框。
|
||||
|
||||
## 4. 工具下载源
|
||||
|
||||
使用原生 Radio Group:
|
||||
|
||||
```text
|
||||
工具下载源
|
||||
选择 GoodBuddy 受管 Runtime 和工具链的下载通道。
|
||||
|
||||
● 原生地址(默认)
|
||||
从各工件的固定上游地址下载。
|
||||
|
||||
○ OSS 镜像
|
||||
从 GoodBuddy OSS 下载相同工件。
|
||||
```
|
||||
|
||||
邻近说明:
|
||||
|
||||
> 下载开始后会固定本次来源;失败时不会自动切换。该设置不影响模型、应用更新、Agent、
|
||||
> pip/npm 依赖或用户配置的网络地址。
|
||||
|
||||
切换设置即时保存,不触发下载,不取消活动任务。成功通过应用通知反馈;保存失败恢复最后
|
||||
确认值并在 Radio Group 附近显示错误。
|
||||
|
||||
## 5. 工具环境来源选择
|
||||
|
||||
每张卡片使用原生单选语义,只提供:
|
||||
|
||||
1. **GoodBuddy 托管(推荐)**
|
||||
2. **自定义环境**
|
||||
|
||||
不显示“自动选择”“系统安装”“指定路径”三个交叉选项。自定义环境选中后,在同一区域展示
|
||||
检测候选下拉框和“选择其他可执行文件”次操作。
|
||||
|
||||
“自定义环境”单选项始终可操作。尚无候选或已保存路径时,点击只展开选择区域并保持当前
|
||||
实际来源,不把入口禁用,也不自动选择第一个候选。选择区域显示“刷新候选”和“选择其他
|
||||
可执行文件”;只有用户明确选择且 Main 验证成功后才保存并切换来源。
|
||||
|
||||
文件选择取消不显示错误、不发送成功通知且保持选择区域展开。验证失败在对应 Node/Python
|
||||
卡片的路径操作附近显示,保留原来源和原路径;不得只在页面顶部显示错误。刷新后出现候选
|
||||
时通过正常列表更新呈现,不改变当前来源,焦点保持在触发操作或用户随后选择的候选上。
|
||||
|
||||
候选项格式:
|
||||
|
||||
```text
|
||||
Node.js v22.14.0 · C:\Program Files\nodejs\node.exe
|
||||
Python 3.13.2 · C:\Python313\python.exe
|
||||
```
|
||||
|
||||
版本和绝对路径不能只放在 Tooltip 中。
|
||||
|
||||
## 6. Node.js 卡片
|
||||
|
||||
```text
|
||||
Node.js
|
||||
|
||||
● GoodBuddy 托管(推荐)
|
||||
Node.js vXX · 已就绪
|
||||
npm 11.x · npx 11.x
|
||||
|
||||
○ 自定义环境
|
||||
[Node.js v22.x · C:\Program Files\nodejs\node.exe ▼]
|
||||
[选择其他可执行文件]
|
||||
|
||||
[测试环境]
|
||||
```
|
||||
|
||||
托管 Node 随应用提供,不显示安装和删除操作。测试结果分别显示 Node、npm、npx,不能用
|
||||
Node 成功代替整套组合成功。找不到配套 npm/npx 时,自定义 Node 可以保存,但状态显示
|
||||
“Node.js 已就绪,npm/npx 不可用”。
|
||||
|
||||
## 7. Python 卡片
|
||||
|
||||
托管 Python 未安装:
|
||||
|
||||
```text
|
||||
Python
|
||||
|
||||
● GoodBuddy 托管(推荐)
|
||||
尚未安装
|
||||
[安装]
|
||||
|
||||
○ 自定义环境
|
||||
[Python 3.13.x · C:\Python313\python.exe ▼]
|
||||
[选择其他可执行文件]
|
||||
```
|
||||
|
||||
安装中使用卡片内进度:
|
||||
|
||||
```text
|
||||
正在下载 Python 3.x
|
||||
[================------] 68%
|
||||
[取消]
|
||||
```
|
||||
|
||||
安装后显示:
|
||||
|
||||
- Python 版本和架构。
|
||||
- pip、venv 和 SSL 诊断状态。
|
||||
- 安装占用空间。
|
||||
- 有更新时的“更新”操作。
|
||||
- “删除托管 Python”危险次操作。
|
||||
|
||||
删除确认说明受影响范围,不要求输入确认文字:
|
||||
|
||||
> 删除后,依赖托管 Python 的本机 SKILL 和 stdio MCP 将不可用。系统 Python、自定义环境
|
||||
> 和项目文件不会被删除。
|
||||
|
||||
## 8. 两层状态
|
||||
|
||||
每张工具卡展示工具环境状态:
|
||||
|
||||
- 已就绪
|
||||
- 尚未安装
|
||||
- 正在安装
|
||||
- 路径不可用
|
||||
- 部分可用,例如 npm/npx 或 pip/venv 缺失
|
||||
- 测试失败
|
||||
- 可更新
|
||||
|
||||
SKILL/MCP 的能力详情展示能力依赖状态:
|
||||
|
||||
- 依赖已验证
|
||||
- 依赖未验证
|
||||
- 缺少第三方包
|
||||
- 入口不可用
|
||||
|
||||
工具执行环境页不掌握具体 SKILL/MCP 的验证证据,因此不显示固定的“依赖未验证”占位
|
||||
区块。只有对应能力产生真实验证结果后,才在该能力详情中展示上述状态。
|
||||
|
||||
不得把工具卡的“已就绪”复用为某个 SKILL 或 MCP 的“完全可用”。
|
||||
|
||||
## 9. 生效与刷新
|
||||
|
||||
保存后在卡片中持续显示:
|
||||
|
||||
> 对新启动的本机 Runtime 和 stdio MCP 生效。已运行进程不会中途切换环境。
|
||||
|
||||
设置保存成功、安装成功、删除成功和重新检测成功使用应用通知。版本、路径、安装进度和
|
||||
阻塞错误留在卡片内,不同时显示重复通知。
|
||||
|
||||
## 10. 错误与恢复
|
||||
|
||||
- 自定义路径失效:保留路径和来源选择,显示“重新检测”和“选择其他文件”。
|
||||
- 下载失败:保留已验证旧安装和安装按钮。
|
||||
- 诊断部分失败:逐项显示失败项,保留成功项。
|
||||
- 取消安装:恢复“尚未安装”或原已安装版本。
|
||||
- 字段错误:就地关联到选择控件。
|
||||
- Runtime/MCP 已运行:不显示为错误,只说明新进程生效。
|
||||
- 工具下载源失败:保留当前选择,显示实际来源和重试,不提示自动回退。
|
||||
|
||||
## 11. 无障碍与键盘
|
||||
|
||||
- 外层使用 `tablist`/`tab`/`tabpanel`,支持左右方向键、Home、End 和 roving focus。
|
||||
- 每个外层 `tab` 通过共享组件准确关联自己的 `tabpanel`。
|
||||
- 来源使用 `radiogroup`/`radio` 语义。
|
||||
- 安装、测试、更新、删除和文件选择使用语义按钮。
|
||||
- 下载进度使用可命名的 `progressbar`。
|
||||
- 状态不只依赖颜色,必须包含文字。
|
||||
- 尚无自定义候选时不得禁用“自定义环境”入口;真正因活动操作禁用的控件仍提供可理解的
|
||||
邻近说明。
|
||||
- 切换到自定义环境后,焦点按正常 Tab 顺序进入候选选择框。
|
||||
- 对话框关闭后焦点回到触发操作。
|
||||
- 窄屏时卡片操作换行,不压缩版本和路径到不可读宽度。
|
||||
|
||||
## 12. 文案词汇
|
||||
|
||||
固定使用:
|
||||
|
||||
- 能力与工具
|
||||
- Skills
|
||||
- MCP
|
||||
- 工具执行环境
|
||||
- GoodBuddy 托管
|
||||
- 自定义环境
|
||||
- 工具环境已就绪
|
||||
- 能力依赖已验证
|
||||
- 新启动的本机 Runtime 和 stdio MCP
|
||||
- 不应用于远程主机
|
||||
- 工具下载源
|
||||
- 原生地址
|
||||
- OSS 镜像
|
||||
|
||||
不使用:
|
||||
|
||||
- 自动环境
|
||||
- 系统模式
|
||||
- 内置开发机
|
||||
- 全局 Python
|
||||
- 远程托管环境
|
||||
- 开发环境
|
||||
@@ -0,0 +1,330 @@
|
||||
# 工具执行环境 User Stories
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,验收中 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-02 |
|
||||
| 关联 PRD | [工具执行环境 PRD](./prd.md) |
|
||||
| 关联逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) |
|
||||
|
||||
## 1. 角色
|
||||
|
||||
### 1.1 普通用户
|
||||
|
||||
希望直接使用文档、演示文稿或数据处理 SKILL,不理解 Node.js、Python、pip 或 PATH。
|
||||
|
||||
### 1.2 MCP 使用者
|
||||
|
||||
希望配置一个本机工具服务,并由 GoodBuddy 解决其基础解释器启动问题。
|
||||
|
||||
### 1.3 专业开发者
|
||||
|
||||
已有明确的 Node、Python、虚拟环境或 Conda 环境,希望控制实际版本和绝对路径。
|
||||
|
||||
### 1.4 内网与离线用户
|
||||
|
||||
无法稳定访问公网,需要使用选定镜像或离线准备环境,同时不向远程 Host 传播本机配置。
|
||||
|
||||
## 2. Epic A:普通用户无需预装环境
|
||||
|
||||
### US-A0 首次看到准确默认状态
|
||||
|
||||
作为普通用户,我希望第一次打开设置时看到 GoodBuddy 已提供什么、还需要安装什么,以免
|
||||
误以为所有工具都已就绪。
|
||||
|
||||
验收:
|
||||
|
||||
- 全新安装默认选择 GoodBuddy 托管 Node 和 Python
|
||||
- 托管 Node 立即执行真实诊断并显示版本
|
||||
- 未安装的托管 Python 显示“尚未安装”和安装入口
|
||||
- 检测到的系统解释器只作为自定义候选,不改变默认来源
|
||||
- “工具执行环境”Tab 持续说明只对新启动的本机 Runtime 和 stdio MCP 生效
|
||||
|
||||
### US-A1 直接使用托管 Node
|
||||
|
||||
作为普通用户,我希望安装 GoodBuddy 后即可使用 Node.js SKILL,以免单独安装 Node.js。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 系统 PATH 没有 Node.js
|
||||
- When 本机 Execute 请求运行普通 JavaScript SKILL
|
||||
- Then `node` 使用 GoodBuddy 托管来源
|
||||
- And `npm`、`npx` 与该 Node 来源一致
|
||||
- And 设置显示实际 Node 版本
|
||||
|
||||
### US-A2 按需安装 Python
|
||||
|
||||
作为普通用户,我希望在设置中安装 Python,以便运行 Python SKILL 而不离开 GoodBuddy。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 系统没有 Python 且托管 Python 未安装
|
||||
- When 用户点击安装
|
||||
- Then 页面显示下载、校验和安装进度
|
||||
- And 成功后真实 Python、pip、venv 诊断通过
|
||||
- And 失败不会留下半安装目录
|
||||
|
||||
### US-A3 看懂缺失依赖
|
||||
|
||||
作为普通用户,我希望区分“没有 Python”和“缺少 python-pptx”,以便采取正确修复操作。
|
||||
|
||||
验收:
|
||||
|
||||
- 工具解释器不可用时显示工具环境错误
|
||||
- 解释器可用但能力包缺失时显示能力依赖错误
|
||||
- 页面不把 `python --version` 成功描述为 SKILL 已完全可用
|
||||
- 错误保留原任务和可执行的下一步
|
||||
|
||||
### US-A4 看懂部分就绪
|
||||
|
||||
作为用户,我希望知道解释器可用但配套命令损坏,以免把所有失败都理解为没有安装环境。
|
||||
|
||||
验收:
|
||||
|
||||
- Node 可用而 npm/npx 失败时显示“部分可用”和具体失败项
|
||||
- Python 可用而 pip、venv 或 SSL 失败时显示“部分可用”和具体失败项
|
||||
- 不依赖失败项的能力仍可通过实际诊断标记就绪
|
||||
- 没有能力专属诊断时显示“依赖未验证”,不推断为就绪
|
||||
|
||||
## 3. Epic B:本机 MCP 使用一致环境
|
||||
|
||||
### US-B1 启动 Python MCP
|
||||
|
||||
作为 MCP 使用者,我希望 `python -m my_mcp_server` 使用设置中的 Python,以免配置第二套
|
||||
解释器。
|
||||
|
||||
验收:
|
||||
|
||||
- 裸 `python`、`python3` 和 `pip` 解析到当前选择
|
||||
- Main 直连工具链和本机 Runtime 使用相同来源
|
||||
- 绝对命令路径不被替换
|
||||
- MCP 自身包缺失时准确报告依赖错误
|
||||
|
||||
### US-B2 启动 Node.js MCP
|
||||
|
||||
作为 MCP 使用者,我希望 `node server.js` 或 `npx package` 使用同一 Node 组合。
|
||||
|
||||
验收:
|
||||
|
||||
- `node`、`npm`、`npx` 来源一致
|
||||
- npm lifecycle 子进程使用当前 Node
|
||||
- 取消或关闭应用后没有遗留 MCP 进程
|
||||
|
||||
### US-B3 保持网络 MCP 不变
|
||||
|
||||
作为 MCP 使用者,我希望 HTTP/SSE MCP 不受本机解释器设置影响。
|
||||
|
||||
验收:
|
||||
|
||||
- HTTP/SSE 配置不显示本机解释器要求
|
||||
- GoodBuddy 不管理远端 MCP 服务端依赖
|
||||
- 切换工具执行环境不重建 HTTP/SSE 服务
|
||||
|
||||
## 4. Epic C:专业用户明确控制环境
|
||||
|
||||
### US-C1 使用检测到的系统解释器
|
||||
|
||||
作为专业开发者,我希望从版本和绝对路径列表中选择解释器,以便使用已有工具链。
|
||||
|
||||
验收:
|
||||
|
||||
- “自定义环境”入口始终可操作,不因尚无候选而禁用
|
||||
- 点击“自定义环境”只展开选择流程,不自动采用第一个候选
|
||||
- 候选覆盖启动时继承的用户/系统 PATH、当前虚拟环境、Conda 环境和常见安装位置
|
||||
- 候选经过真实版本和架构诊断
|
||||
- 相同真实路径不重复显示
|
||||
- 用户必须明确选择一个候选后才保存并切换来源
|
||||
- 保存后状态显示实际生效来源
|
||||
- PATH 后续变化不会自动替换选择
|
||||
|
||||
### US-C2 指定项目虚拟环境
|
||||
|
||||
作为专业开发者,我希望选择未被自动发现的 Python,以便使用自己的 venv 或 Conda 环境。
|
||||
|
||||
验收:
|
||||
|
||||
- 文件选择由 Main 管理
|
||||
- 没有候选时仍可从“自定义环境”流程直接打开文件选择器
|
||||
- 取消文件选择不报错、不保存,也不改变当前来源
|
||||
- 保存前验证 Python 3、版本和架构
|
||||
- 验证失败在 Python 卡片的路径操作附近显示,并保留原设置
|
||||
- GoodBuddy 不安装、升级或删除该环境
|
||||
- 路径失效时不静默回退
|
||||
|
||||
### US-C3 返回托管环境
|
||||
|
||||
作为专业开发者,我希望随时切回 GoodBuddy 托管环境,以便恢复标准组合。
|
||||
|
||||
验收:
|
||||
|
||||
- 切换前显示新启动进程才生效
|
||||
- 新 Runtime 和 stdio MCP 使用托管来源
|
||||
- 已运行进程不中途换环境
|
||||
|
||||
### US-C4 明确设置何时生效
|
||||
|
||||
作为专业开发者,我希望知道环境切换何时影响进程,以免一个运行中的任务突然换解释器。
|
||||
|
||||
验收:
|
||||
|
||||
- 保存前后都说明只对新启动进程生效
|
||||
- 有活动请求的 Runtime 保持启动时环境
|
||||
- 空闲 Runtime 在下一次使用前按现有入口重建
|
||||
- 已启动 stdio MCP 在关闭、重连或显式测试后使用新环境
|
||||
- 活动记录不根据最新设置改写历史解释器来源
|
||||
|
||||
## 5. Epic D:范围和生命周期可理解
|
||||
|
||||
### US-D1 不影响远程主机
|
||||
|
||||
作为远程项目用户,我希望清楚知道本机环境不会上传到 Host,以免错误预期远程脚本可用。
|
||||
|
||||
验收:
|
||||
|
||||
- “工具执行环境”Tab 显示范围说明
|
||||
- 远程主机设置页显示本机 Skills 和 MCP 不适用
|
||||
- Host PATH、Agent 包和 Runtime 包不包含本机选择
|
||||
|
||||
### US-D2 删除托管 Python
|
||||
|
||||
作为用户,我希望删除不再使用的托管 Python,以便释放磁盘空间。
|
||||
|
||||
验收:
|
||||
|
||||
- 删除前说明受影响的本机 SKILL 和 MCP
|
||||
- 只删除 GoodBuddy-owned Python 目录
|
||||
- 不删除系统、自定义或工作区文件
|
||||
- 使用中的进程先按现有生命周期停止
|
||||
|
||||
### US-D3 升级后保持既有解释器
|
||||
|
||||
作为现有用户,我希望升级 GoodBuddy 后已有脚本仍使用原来的系统解释器。
|
||||
|
||||
验收:
|
||||
|
||||
- 首次迁移把有效系统解释器保存为自定义绝对路径
|
||||
- 无有效解释器时才选择托管来源
|
||||
- 迁移结果只写一次
|
||||
|
||||
### US-D4 取消 Python 安装
|
||||
|
||||
作为用户,我希望可以取消耗时的 Python 安装,并在之后重试。
|
||||
|
||||
验收:
|
||||
|
||||
- 下载、校验、安装和候选诊断阶段都响应取消
|
||||
- 取消后清理本次暂存
|
||||
- 取消不发布未验证候选
|
||||
- 页面回到操作前的未安装状态或健康旧版本
|
||||
- 取消不显示为安装失败
|
||||
|
||||
### US-D5 安全更新托管 Python
|
||||
|
||||
作为用户,我希望更新托管 Python 失败时仍能使用旧版本,以免现有能力全部中断。
|
||||
|
||||
验收:
|
||||
|
||||
- 下载和验证候选期间旧版继续服务已有进程
|
||||
- 应用更新前说明需要停止哪些本机 Runtime/MCP
|
||||
- 用户确认后才通过现有生命周期停止相关进程
|
||||
- 无法停止时不替换当前版本
|
||||
- 候选完整诊断成功后才发布
|
||||
- 更新失败继续使用健康旧版本
|
||||
|
||||
### US-D6 安装解释器不扩大 Ask 权限
|
||||
|
||||
作为 Ask 用户,我希望安装 Node/Python 后仍保持原有只读边界,以免环境准备改变授权范围。
|
||||
|
||||
验收:
|
||||
|
||||
- 工具环境不新增 Runtime 工具
|
||||
- Ask 是否允许命令仍由当前 Runtime 边界决定
|
||||
- Ask 中禁止的写入和执行不会因解释器存在而放行
|
||||
- Execute 继续使用用户已授权的本机账号权限
|
||||
|
||||
## 6. Epic E:受限网络与离线行为明确
|
||||
|
||||
### US-E1 通过所选官方来源安装 Python
|
||||
|
||||
作为内网用户,我希望在“工具执行环境”Tab 中选择原生地址或 OSS 镜像,以便符合当前
|
||||
网络条件。
|
||||
|
||||
验收:
|
||||
|
||||
- 默认使用 Python 工件的固定原生地址
|
||||
- 选择 OSS 后,Python 只请求固定 OSS 对象
|
||||
- 页面显示本次下载冻结的来源和准确失败原因
|
||||
- 同一次安装不在原生地址和 OSS 之间静默切换
|
||||
- 失败保留健康旧版本或未安装状态
|
||||
|
||||
### US-E2 完全离线时使用已有环境
|
||||
|
||||
作为完全离线用户,我希望清楚知道首期可用路径,以免等待一个无法完成的下载。
|
||||
|
||||
验收:
|
||||
|
||||
- 托管 Node 安装后可离线使用
|
||||
- 托管 Python 未缓存且来源不可达时明确说明无法下载
|
||||
- 首期不展示未实现的 Python 离线导入入口
|
||||
- 用户可以选择设备上已经离线安装的自定义 Python
|
||||
|
||||
### US-E3 所有受管工具工件遵循同一下载通道
|
||||
|
||||
作为用户,我希望未来新增的受管 Runtime 和工具链遵循同一个下载源选择,以免每种工具
|
||||
产生不同规则。
|
||||
|
||||
验收:
|
||||
|
||||
- 受管工具目录中的每个外部工件同时声明原生地址和 OSS Target
|
||||
- 两个 Target 对应相同字节数和 SHA-256
|
||||
- 下载任务开始后冻结来源
|
||||
- 所选来源缺少该工件时明确显示不可用
|
||||
- pip/npm 依赖、模型、应用更新、Agent 和用户 URL 不受该设置影响
|
||||
|
||||
## 7. Epic F:统一管理能力与执行环境
|
||||
|
||||
### US-F1 从一个入口管理相关能力
|
||||
|
||||
作为用户,我希望从一个设置入口访问 Skills、MCP 和工具执行环境,以便理解能力配置及其
|
||||
本机依赖之间的关系。
|
||||
|
||||
验收:
|
||||
|
||||
- 设置中心只显示一个“能力与工具”一级分类
|
||||
- 第一阶段依次显示 `Skills`、`MCP` 两个水平 Tab
|
||||
- 工具执行环境完成后在同一组中追加第三个 `工具执行环境` Tab
|
||||
- 未完成前不显示禁用或占位的 `工具执行环境`
|
||||
- 首次进入默认打开 `Skills`
|
||||
- 每个 Tab 使用独立、可命名的 `tabpanel`
|
||||
- 水平 Tab 支持方向键、Home、End 和可见焦点
|
||||
- 窄屏时页签保持单行横向滚动,不换成多行或下拉框
|
||||
|
||||
### US-F2 保持业务数据和编辑状态
|
||||
|
||||
作为现有用户,我希望合并设置导航后原有 Skills 和 MCP 配置保持不变,并且切换水平 Tab
|
||||
不丢失正在编辑的信息。
|
||||
|
||||
验收:
|
||||
|
||||
- 原独立 Skills/MCP 左侧分类由一个“能力与工具”分类替代,不保留重复入口
|
||||
- 合并导航不修改 Skill 启停、导入、分配或 MCP 配置、凭据和分配数据
|
||||
- MCP 原有内部分组页签继续位于 MCP 面板内
|
||||
- 切换三个外层 Tab 后返回时保留本次设置面板会话中的编辑状态
|
||||
- 关闭并重新打开设置时,普通入口仍默认显示 Skills,不持久化上次外层 Tab
|
||||
- 未来新增 Skills/MCP 定向入口时打开对应水平 Tab
|
||||
|
||||
### US-F3 聚焦默认内置能力
|
||||
|
||||
作为普通用户,我希望默认只加载通用文档工具,不让低频产品营销说明占用每次会话的上下文。
|
||||
|
||||
验收:
|
||||
|
||||
- Skills 列表只把五个具备独立入口或执行器的内置能力显示为顶层 Skill
|
||||
- 产品营销的九种文档配方从 `product-marketing` 内部按需读取
|
||||
- `deai-writing` 和 `longdoc-docx` 在没有保存状态时默认启用
|
||||
- 三个产品营销 Skill 在没有保存状态时默认停用
|
||||
- 用户显式保存的启停状态和 Runtime 分配不因版本升级而重置
|
||||
- 用户导入的 Skill 继续使用现有默认启用和 Runtime 分配规则
|
||||
@@ -0,0 +1,11 @@
|
||||
# 记忆
|
||||
|
||||
记忆定义 GoodBuddy 的用户、项目和会话记忆分区、写入边界、召回和治理。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [分区记忆 PRD](./prd.md) | 记忆范围、分区、生命周期和产品验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
@@ -5,9 +5,9 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-13 |
|
||||
| 依赖 | [自动化平台总体设计](./automation-platform-architecture.md) |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[智能心跳 PRD](../smart-heartbeat/prd.md)、[Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
@@ -16,13 +16,13 @@ GoodBuddy 当前记忆已经支持:
|
||||
- `global`、`project`、`conversation` 三种作用域。
|
||||
- `preference`、`fact`、`summary`、`procedure` 四种类型。
|
||||
- `proposed`、`confirmed`、`rejected` 三种状态。
|
||||
- 智能心跳提出 Global 或 Project 记忆候选,由用户确认。
|
||||
- 智能心跳提出 Global 或 Project 记忆候选,由用户确认;其长期“未来分区记忆”方向尚未设计。
|
||||
|
||||
但当前能力仍不足以支撑自动化和并行实验:
|
||||
|
||||
1. 交互请求会把已加载列表中的最多 20 条已确认记忆直接拼入提示,缺少查询相关度和明确的
|
||||
会话级过滤契约。
|
||||
2. 数据库有会话作用域,但心跳只提出 Global 和 Project 记忆。
|
||||
2. 数据库有会话作用域,但旧版心跳候选与未来唤起需求混在同一产品概念中。
|
||||
3. 缺少 Automation、Experiment 和 Run 分区。
|
||||
4. 来源字段存在于表结构,但普通创建和心跳候选尚未完整保存来源关系。
|
||||
5. 缺少事实的有效时间、冲突、替代、访问记录和衰减。
|
||||
@@ -64,6 +64,11 @@ SQLite 显式字段、FTS、来源关系和可选本地 Embedding 足以支持
|
||||
- 同一实体跨大量会话的别名消歧。
|
||||
- 可解释的关系证据链。
|
||||
|
||||
### 2.5 未来分区记忆尚待设计
|
||||
|
||||
已确认智能心跳的长期方向是“未来分区记忆”,但当前尚未定义其数据结构、唤起条件、状态、
|
||||
生命周期、与长期记忆的关系或迁移方式。本 PRD 不新增 `FutureMemory` 类型、表或检索规则。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 为会话、自动化和并行 Run 提供严格隔离。
|
||||
@@ -73,6 +78,7 @@ SQLite 显式字段、FTS、来源关系和可选本地 Embedding 足以支持
|
||||
- 让候选记忆经过确认或评估后再晋升。
|
||||
- 支持编辑、移动、合并、拒绝、归档、删除和要求忘记。
|
||||
- 记录哪些 Run 实际读取了哪些记忆。
|
||||
- 为现有智能心跳配置建立 Global 或指定 Project 范围,并保持当前候选记忆流程。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
@@ -134,10 +140,10 @@ agent:{expertId}
|
||||
Conversation → Project → Global
|
||||
```
|
||||
|
||||
自动化 Run:
|
||||
Task 执行(内部 Job/Run):
|
||||
|
||||
```text
|
||||
Run → Automation → Conversation(可选)→ Project → Global
|
||||
Run → Job → Task → Conversation → Project → Global
|
||||
```
|
||||
|
||||
实验 Run:
|
||||
@@ -224,12 +230,12 @@ type MemorySource =
|
||||
|
||||
候选来源:
|
||||
|
||||
- 智能心跳。
|
||||
- 用户明确“记住这个”。
|
||||
- 会话结束总结。
|
||||
- 自动化 Run 结束反思。
|
||||
- Task 执行结束反思。
|
||||
- 实验结论。
|
||||
- Supervisor 建议后用户采纳。
|
||||
- 智能心跳。
|
||||
|
||||
候选生成必须:
|
||||
|
||||
@@ -424,6 +430,9 @@ Project 记忆与 Global 偏好冲突时:
|
||||
|
||||
每条记忆展示内容、类型、范围、来源、状态、时间、置信度、重要性和冲突。
|
||||
|
||||
智能心跳的完整配置仍在“智能心跳”菜单中管理;未来分区记忆完成独立设计前,不加入记忆
|
||||
中心信息架构。
|
||||
|
||||
## 16. 数据模型建议
|
||||
|
||||
建议表:
|
||||
@@ -459,17 +468,20 @@ Project 记忆与 Global 偏好冲突时:
|
||||
|
||||
1. 修正当前交互请求的范围过滤,确保只读 Global、当前 Project 和当前 Conversation。
|
||||
2. 增加来源记录和“实际进入上下文”的诊断。
|
||||
3. 建立 Automation 和 Run Namespace。
|
||||
4. 上线有界相关检索,替换简单列表前 20 条拼接。
|
||||
5. 增加冲突、时态、替代和归档。
|
||||
6. 增加实验冻结快照与 Run 隔离。
|
||||
7. 增加可选本地 Embedding 和混合排序。
|
||||
8. 只有明确需求后再评估时间知识图谱。
|
||||
3. 为现有智能心跳配置增加 Global 或指定 Project 范围,保持候选记忆行为不变。
|
||||
4. 建立 Automation 和 Run Namespace。
|
||||
5. 上线有界相关检索,替换简单列表前 20 条拼接。
|
||||
6. 增加冲突、时态、替代和归档。
|
||||
7. 增加实验冻结快照与 Run 隔离。
|
||||
8. 增加可选本地 Embedding 和混合排序。
|
||||
9. 未来分区记忆和时间知识图谱都必须在明确需求与独立设计后再实施。
|
||||
|
||||
## 19. 验收标准
|
||||
|
||||
- [ ] 普通会话只读取 Global、当前 Project 和当前 Conversation 的允许记忆。
|
||||
- [ ] 自动化 Run 只读取运行快照绑定的分区。
|
||||
- [ ] 智能心跳配置只能属于 Global 或 Main 已验证的一个、多个 Project。
|
||||
- [ ] 未来分区记忆完成独立设计前,不新增相关表、状态或检索行为。
|
||||
- [ ] Task 执行只读取内部 Run 快照绑定的分区。
|
||||
- [ ] 实验 Run 不能读取其他 Run 的消息或记忆。
|
||||
- [ ] 每条非手动记忆都有可追溯来源。
|
||||
- [ ] 候选和被拒绝记忆不进入普通上下文。
|
||||
@@ -477,6 +489,6 @@ Project 记忆与 Global 偏好冲突时:
|
||||
- [ ] 冲突事实不被静默覆盖。
|
||||
- [ ] 当前有效事实可通过有效时间正确选择。
|
||||
- [ ] 上下文组装遵守各层和总字符预算。
|
||||
- [ ] UI 能显示某次 Run 实际使用的记忆。
|
||||
- [ ] UI 能在 Task 执行记录中显示实际使用的记忆,不把 Run 暴露为独立导航对象。
|
||||
- [ ] 删除或忘记后,文本、索引和缓存不再可检索。
|
||||
- [ ] Restricted 记忆不会自动生成或发送给外部 Embedding 服务。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 并行实验
|
||||
|
||||
并行实验定义多个候选方案、模型或执行分支的受控比较、评估与结果选择。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [并行实验工作台 PRD](./prd.md) | 实验对象、工作台流程、评估和验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
+8
-7
@@ -7,7 +7,7 @@
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-13 |
|
||||
| 依赖 | [自动化平台总体设计](./automation-platform-architecture.md)、[自动任务与目标 PRD](./automation-goals-and-scheduling-prd.md) |
|
||||
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
@@ -158,7 +158,7 @@ Token 和耗时范围,以及最大并发。超过上限时要求缩小变量
|
||||
- `experimentRunId` 和运行会话。
|
||||
- 变量快照和临时上下文。
|
||||
- Run 记忆分区。
|
||||
- 任务、子任务和成果。
|
||||
- Task、Job、Subjob 和成果。
|
||||
- 指标、证据和 Runtime 会话标识。
|
||||
|
||||
禁止:
|
||||
@@ -286,13 +286,14 @@ Supervisor 不能:
|
||||
实验工作台页签:
|
||||
|
||||
1. **设计**:问题、协议、变量、指标和预算。
|
||||
2. **运行**:总体进度、Run 表和状态。
|
||||
2. **运行**:总体进度、候选执行和聚合状态。
|
||||
3. **比较**:指标表、图表、差异和 Pareto 候选。
|
||||
4. **证据**:按结论、指标和 Run 查看证据。
|
||||
4. **证据**:按结论、指标和候选查看证据。
|
||||
5. **结论**:总结、限制和后续操作。
|
||||
|
||||
Run 详情展示参数、协议版本、时间线、消息、任务、成果、监督记录、指标、评估理由、
|
||||
上下文和记忆快照、Token、耗时与错误。
|
||||
候选详情在 Experiment 工作台内展示参数、协议版本、时间线、消息、Task、成果、监督记录、
|
||||
指标、评估理由、上下文和记忆快照、Token、耗时与错误。内部 Run ID 只用于关联和审计,
|
||||
不提供独立 Run 路由、页面或操作菜单。
|
||||
|
||||
## 15. 后续操作
|
||||
|
||||
@@ -302,7 +303,7 @@ Run 详情展示参数、协议版本、时间线、消息、任务、成果、
|
||||
- 创建自动化计划草稿。
|
||||
- 保存实验模板。
|
||||
- 创建记忆候选。
|
||||
- 追加确认 Run。
|
||||
- 追加确认执行。
|
||||
- 导出脱敏结果摘要。
|
||||
|
||||
不得自动启用新计划、覆盖现有计划、确认长期记忆、应用工作区 Patch 或扩大权限。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 远程主机与远程执行
|
||||
|
||||
本功能负责 SSH Host 管理、Host Key、远程环境安装、GoodBuddy Agent、签名 Runtime、
|
||||
远程 Workspace 和 Ask/Execute 生产路径。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [远程主机与 Agent 技术设计](./technical-design.md) | 当前产品语义、Agent、Runtime、Workspace、协议、生命周期和验证事实 |
|
||||
| [远程环境准备技术设计](./environment-provisioning-technical-design.md) | Host 下载、GoodBuddy 传输、安装事务、更新和恢复边界 |
|
||||
|
||||
现有文档包含产品、功能逻辑和进度事实,后续修改远程功能时再按标准职责拆分。
|
||||
@@ -0,0 +1,329 @@
|
||||
# SSH Host 远程环境准备与直连下载设计
|
||||
|
||||
## 状态
|
||||
|
||||
本文记录 2026-08-28 确认的目标设计及当前实施状态。源码已经实现 Host 手动准备、
|
||||
GoodBuddy 传输、Host 直连下载、control-plane one-shot installer、发布后健康 adoption,
|
||||
以及项目路径只验证已安装环境。Host 负责 Agent/Runtime 的安装与更新生命周期,项目流程
|
||||
只消费已经准备好的 Host 环境。
|
||||
|
||||
当前源码可以直接使用既有 package format v1 `.gbagent`:归档已经包含固定 Agent 和
|
||||
Node,installer 由 GoodBuddy 桌面控制面打包并通过固定 SSH prepare channel 单次发送,
|
||||
不要求归档包含 `agent/lib/package-installer.cjs`,签名目录也不提供或检查额外的
|
||||
bootstrap 能力元数据。公开直连能力仍须完成本文末尾 GitHub/北京镜像、Linux x64/arm64、
|
||||
取消和离线 GoodBuddy 传输的真实 Host 验收;本文
|
||||
不表示这些验收或新的 Agent 发布已经发生。当前实现细节同时以
|
||||
[SSH 远程主机与 GoodBuddy Agent 实现说明](./technical-design.md)
|
||||
和源码为准。
|
||||
|
||||
## 设计结论
|
||||
|
||||
将安装和更新从项目切换移到 Host 管理是合理的,原因如下:
|
||||
|
||||
- Agent、固定 Node 和 OpenCode Runtime 安装在 SSH 账号 Home 下,由同一 Host/账号上的
|
||||
多个项目共享,不属于某一个 Workspace。
|
||||
- 大文件传输发生在项目切换时会阻塞导航,也把 Host 级故障误显示成项目故障。
|
||||
- Host Key、认证、平台和架构本来就在新增 Host 时验证,此时继续只读探测并在 Host 卡片
|
||||
提供手动准备入口最容易理解。
|
||||
- 项目只保存 Host ID、远端路径、Runtime 选择和默认工作模式。Agent installation、
|
||||
Runtime digest、Workspace identity 和 Host revision 都从当次连接取得,不写入项目;
|
||||
“移出安装”不等于“跳过当前环境检查”。
|
||||
|
||||
目标职责是:
|
||||
|
||||
| 流程 | 职责 |
|
||||
| --- | --- |
|
||||
| 新增或重新验证 Host | 验证身份和认证,保存 Host 并探测环境,不传输安装包 |
|
||||
| Host 管理 | 用户明确刷新目标 Host,并可选择安装、同版本重修、取消和重试 |
|
||||
| 创建远程项目 | 只读本地 Host 验证记录,保存时连接所选 Host 并验证当前 Agent、Workspace、Runtime 和模型配置,只持久化稳定配置 |
|
||||
| 切换远程项目 | 只切换本地项目配置,不连接 Host;实际使用 Workspace 或 Runtime 时按需复用 Host 当前环境 |
|
||||
|
||||
## 不变量
|
||||
|
||||
- Host 配置与远程环境准备是两个事务。SSH 验证成功后先原子保存 Host、凭据和完整 Host
|
||||
Key,再开始可能耗时的下载和安装。
|
||||
- 环境准备失败或取消时保留已保存 Host,显示“未就绪”并允许重试,不创建半完成项目。
|
||||
- 不把“环境健康”保存为长期可信布尔值。设置页从 Host 的签名 registry 和当前 Host
|
||||
identity 派生版本状态;准备完成和实际使用时再执行 bootstrap/health。合法但暂未运行的
|
||||
detached Agent 仍可显示为已安装,打开设置不能为了刷新卡片隐式启动或更新它。
|
||||
- 所有安装继续使用 GoodBuddy-owned staging 和 side-by-side digest 目录。失败不得覆盖
|
||||
可用旧安装,也不得删除 Host 上无关文件。
|
||||
- Renderer 不接收下载 URL、缓存路径、签名 key、digest、SSH 命令或凭据。Main 负责来源
|
||||
选择、可信目录解析、操作编排和 IPC 输入校验。
|
||||
- 产品不提供“自动安装远程环境”开关。保存、重新验证或打开 Host 都不能自动传输
|
||||
`.gbagent`;只有用户点击 Host 卡片的主按钮后,Main 才开始准备。
|
||||
- Host 卡片只有一个主按钮,按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。
|
||||
次级 SegmentedControl 选择“自动”“Host 下载”或“GoodBuddy 传输”;默认“自动”,只影响
|
||||
下一次操作,不持久化为 Host 配置。
|
||||
- “自动”只在 operation/prepare 前执行一次 Host capability probe:直连明确可用时选择
|
||||
Host 下载,否则选择 GoodBuddy 传输。用户显式选择的方式保持有效;一旦 acquisition
|
||||
已选定,prepare、commit 或 adoption 的任何失败都停止本次操作,不跨 acquisition
|
||||
自动回退。
|
||||
- 直连安装只要求已验证 SSH 连接和远端系统工具,不要求现有 Agent daemon。轻量能力探测
|
||||
返回确定性缺少工具、空间、权限或来源不可达时说明原因;探测本身异常时仍允许用户点击
|
||||
直连安装,由实际安装流程重新解析签名候选并通过 SSH 重试。
|
||||
- 项目路径不得为了“方便”回退到隐式安装。Host 未就绪时应引导到 Host 设置,不在项目
|
||||
切换器中启动大文件传输。
|
||||
|
||||
## 新增 Host 流程
|
||||
|
||||
新增向导保持现有 Host Key 和认证顺序,并在系统探针之后显示一次只读环境结果:
|
||||
|
||||
1. 输入名称、地址、端口、用户名和认证方式。
|
||||
2. 在发送认证材料前读取并确认 Host Key。
|
||||
3. 完成 SSH 认证以及平台、架构、Shell、Home 有界探针。
|
||||
4. 原子保存 Host、加密凭据和完整 Host Key。
|
||||
5. 读取 Host 已安装的 Agent/Runtime registry,并执行一次轻量下载能力探测。
|
||||
6. 如果已安装 identity 满足当前 GoodBuddy 要求,显示“版本匹配”并结束;该 badge 不代表
|
||||
Agent 正在运行或环境健康。
|
||||
7. 如果组件缺失或需要更新,显示目标 Agent、Node、Runtime、架构、来源和完整包大小,
|
||||
告知用户保存后前往 Host 卡片安装,然后结束新增向导。
|
||||
|
||||
新增流程到此结束,不显示自动安装开关,也不自动开始环境安装。保存后的 Host 卡片提供
|
||||
一个手动准备主按钮,并以次级 SegmentedControl 选择“自动”“Host 下载”或
|
||||
“GoodBuddy 传输”。主按钮按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。
|
||||
本地尚无精确包不要求用户先离开当前页面;选择 GoodBuddy 传输时,同一次操作会取得并
|
||||
验证候选后继续上传。
|
||||
|
||||
探测不得自动下载完整 `.gbagent`。只有用户明确启动安装后才允许传输大包。
|
||||
|
||||
## Host 编辑
|
||||
|
||||
- 仅修改显示名称不要求重装远端环境。
|
||||
- 地址、端口、用户名或 Host Key 变化时,旧 Host identity 立即失效,关闭旧连接和对应
|
||||
Runtime 缓存。项目记录不含旧组件 identity;下一次打开直接解析新 Host 的当前环境。
|
||||
保存新身份后只重新执行环境探测,所需安装由用户在 Host 卡片手动启动。
|
||||
- 只替换同一目标的认证材料时仍重新认证并读取实际环境;如果签名 identity 和健康状态
|
||||
未变化,不重复安装。
|
||||
- 当前项目引用的 Host 被编辑时,Main 立即失效该 Host 的 identity 和连接缓存;下一次
|
||||
Workspace/Runtime 操作按需使用新目标。环境未准备好时该操作失败,不回退旧连接。
|
||||
|
||||
## 下载能力探测
|
||||
|
||||
探测在 Host Key 已固定且 SSH 认证成功后执行,只检查当前选择的“关于与更新”来源:
|
||||
|
||||
- Host 是受支持的 Linux `x64` 或 `arm64`。
|
||||
- SSH 账号 Home 可写,GoodBuddy-owned staging 可创建。
|
||||
- 可用空间满足签名目录声明的包大小、解包上限和安全余量。
|
||||
- Host 可以通过 HTTPS 访问所选 GitHub 或北京镜像 URL。
|
||||
- 首次安装所需的下载、SHA-256 和运行归档内固定 Node 的能力可用。
|
||||
- 已安装 Agent 是否报告未来的直连更新能力。
|
||||
|
||||
探测使用小型签名目录或有界请求,不以一次成功 `HEAD` 代替真实安装验证。结果只用于当次
|
||||
展示,不持久化为永久能力;开始安装前重新检查,因为网络、代理和磁盘状态可能变化。
|
||||
|
||||
探测失败不能阻止保存 Host,也不能禁用显式 GoodBuddy 传输或离线导入路径。显式选择
|
||||
Host 下载时由实际操作报告不可用原因;“自动”只把“直连明确可用”作为选择 Host 下载的
|
||||
条件,其他结果选择 GoodBuddy 传输。
|
||||
|
||||
## 工件选择与来源
|
||||
|
||||
Main 继续读取并验证 `agent-catalog.json`、`agent-catalog.sig` 和 production 公钥
|
||||
registry,按以下条件选择唯一候选:
|
||||
|
||||
- Host 平台和架构匹配;
|
||||
- 最低桌面版本满足;
|
||||
- Agent protocol major 相同,minor 不高于桌面支持值;
|
||||
- 使用满足条件的最高 SemVer;
|
||||
- URL、文件名、大小和 SHA-256 来自签名目录;
|
||||
- 下载来源跟随“关于与更新”的 GitHub/镜像选择。
|
||||
|
||||
远程下载只优化大包的网络路径。Main 仍需先取得并验证小型目录,再通过经过 Host Key
|
||||
验证的 SSH 控制面传递固定候选元数据。产品不接受 Renderer、项目、Host 文件或用户输入的
|
||||
任意下载 URL。
|
||||
|
||||
## 获取与统一安装事务
|
||||
|
||||
首次安装不能假设 Host 已有 GoodBuddy Agent、installer 或系统 Node,也不由既有 Agent
|
||||
daemon 参与。两种 acquisition 的差别只在于如何把同一个签名 compound `.gbagent` 交付到
|
||||
固定 operation staging:
|
||||
|
||||
- **Host 下载**:控制面通过固定 SSH prepare channel 发送签名目录绑定的 URL、大小和
|
||||
SHA-256;Host 有界下载并校验完整归档。
|
||||
- **GoodBuddy 传输**:显式选择时,在线目录不可用或本地已验证包版本不低于在线候选,
|
||||
则优先使用本地包,包括版本更高的离线导入;线上存在更新且可用时仍取得线上新包。
|
||||
需要下载时,同一次用户操作会验证 SHA-256 和签名、写入缓存并取得 lease。随后以有界
|
||||
流式 SFTP 上传一个 compound archive,以及从该归档中已验证的 bootstrap Node。约
|
||||
294 MiB 的完整包不会一次读入 Main `Buffer`,Host 收到后仍再次校验完整包。
|
||||
|
||||
交付后两种 acquisition 完全共用一个 control-plane 事务:
|
||||
|
||||
1. Main 创建带 operation ID、总时限、输出上限和取消信号的准备操作,并通过固定
|
||||
`prepare` channel 发送有界 one-shot installer 与结构化输入。
|
||||
2. Host 在解包写入准备目录的同一遍处理中校验归档大小、SHA-256、外层及内层签名、
|
||||
平台、架构、协议和每个 payload digest;不再先完整扫描 payload、再重复解包和哈希。
|
||||
3. `commit` 只复核有界的签名 manifest、准备回执和入口 metadata,再以目录 rename 发布
|
||||
已准备内容及包内认证的公钥 registry/Runtime lock;不会重读归档或遍历 payload。
|
||||
4. commit 后在同一条已固定身份的 SSH lease 上执行 Agent bootstrap/readiness 和 Runtime
|
||||
activate。新 Agent 最多完整验证一次,新 Runtime 最多完整验证一次;注册成功后的
|
||||
health、capabilities 和 prompt 启动只读取 registry、签名 manifest 与入口 metadata。
|
||||
5. Main 确认 adoption 后显式执行 cleanup。cleanup 失败不回滚已经健康的 Agent/Runtime,
|
||||
也不阻塞下一次更新。
|
||||
|
||||
这不是两个并行安装管理器,也不存在把 Agent 与 Runtime payload 逐文件走 SFTP 安装的
|
||||
第二条路径。两种 acquisition 共享相同的 prepare、commit、adoption、finalize 与 cleanup。
|
||||
|
||||
Host 缺少可靠的下载、SHA-256 或 ZIP 能力时,不自动安装系统软件,也不要求 root。显式
|
||||
Host 下载返回明确失败;本次操作不自动切换 acquisition,用户可改选 GoodBuddy 传输后
|
||||
重新点击主按钮。后续更新和同版本重装复用同一事务,不要求用户先通过项目切换完成
|
||||
“过渡升级”。
|
||||
|
||||
## 本机传输与离线路径
|
||||
|
||||
现有 `.gbagent` 本地缓存、在线下载、离线导入和导出继续保留:
|
||||
|
||||
- GoodBuddy 路径在本地缺少精确候选时,于同一次操作完成下载、整包大小与 SHA-256、
|
||||
全部签名和 payload 验证、缓存发布及 lease 获取。
|
||||
- SFTP 有界流式上传单个 compound archive 与其中已验证的 bootstrap Node 到操作自己的
|
||||
staging;Main 不把完整归档读入内存,Host 再次校验后进入统一 control-plane 事务。
|
||||
- Host 无公网、代理/TLS 异常或缺少 bootstrap 工具时,用户可显式使用此路径;离线导入
|
||||
后也走同一缓存 lease、上传和 Host 校验流程。
|
||||
- 本地项目和普通桌面打包不依赖任何远端 Agent 工件。
|
||||
|
||||
## 完整包与发布
|
||||
|
||||
第一阶段继续发布和下载完整复合 `.gbagent`,不同时引入组件化增量发布:
|
||||
|
||||
- 远程直连和 GoodBuddy 本机下载都会传输完整包,其中包含 Agent、固定 Node 和 OpenCode
|
||||
Runtime。
|
||||
- 每次显式安装或重装都从本次已经校验并解包的准备目录原子替换对应 Agent 与 Runtime
|
||||
digest 目录,不为节省本机磁盘写入而重新遍历旧目录,也不使用跨安装硬链接。
|
||||
- 操作中断后不续写旧 operation。下一次更新尽力清理其 staging,并从签名候选重新
|
||||
prepare;正常 cleanup 随后删除本次准备目录。
|
||||
|
||||
只有真实包体和发布频率证明网络成本仍不可接受时,才另行设计 Agent、Node 和 Runtime
|
||||
独立签名组件目录;本功能不提前引入第二套发布协议。
|
||||
|
||||
## Host 准备状态
|
||||
|
||||
设置中的 Host 卡片分开表达版本事实和操作结果:
|
||||
|
||||
- **未验证**:Host Key、认证或系统探针尚未完成。
|
||||
- **尚未安装**:Host 可连接,但 Agent 或 Runtime 缺失。
|
||||
- **需要更新**:已安装 identity 与当前兼容候选不一致。
|
||||
- **正在准备**:正在探测、下载、验签、安装或健康检查。
|
||||
- **版本匹配**:版本 badge 只表示 Agent/Runtime identity 与当前兼容候选匹配,不等同于
|
||||
进程正在运行或环境健康;设置页普通刷新不会启动已停止的 Agent。
|
||||
- **准备失败**:保留 Host 和可用旧组件,展示有界错误与重试入口。
|
||||
|
||||
主按钮按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。重新安装失败后,
|
||||
界面显示本次操作未完成,并以随后重新检查的版本卡片表达当前事实,不在提交结果不确定时
|
||||
断言旧版本未被替换,也不得把“版本匹配”改称“环境健康”。统一进度以 `探测`、
|
||||
`下载/传输`、`校验`、`应用环境`、`Agent 激活与健康检查`、`Runtime 激活` 和
|
||||
`完成设置` 表达用户可理解的阶段。
|
||||
|
||||
同一 Host identity 同时只允许一个由用户启动、Main-owned 的准备操作。重复观察者加入
|
||||
已有操作,不启动第二次发布;窗口关闭、显式取消、Host identity 改变或 Main 退出时执行
|
||||
有界取消。Main 只为该 Host 持久化一个不含凭据的 version-1 pending operation ID,用于
|
||||
下一次操作开始时尽力清理旧 staging。无论旧 cleanup 成功、目标已变化还是旧记录损坏,
|
||||
都删除这条可丢弃记录并开始 fresh prepare;不保存远端 metadata 副本,不续写旧 commit,
|
||||
也不跨重启恢复 adoption。当前操作中可确定的失败会立即尽力 cleanup。环境已健康后的
|
||||
cleanup 失败不回滚环境,也不阻塞下一次更新。
|
||||
|
||||
## 项目创建与切换
|
||||
|
||||
- 新建托管 SSH 项目弹窗只读取本地 Host Key/连接验证记录,不逐台连接或刷新版本。只有
|
||||
已完成本地验证的 Host 可选;浏览目录或保存时才连接所选 Host,并由保存流程完整检查
|
||||
Agent、Workspace 和 Runtime,但不隐式安装。
|
||||
- 项目保存验证 Host 当前 revision 与 Host Key generation、当前 Agent、Workspace、
|
||||
Runtime、模型 profile 和 Ask/Execute 配置;SQLite 事务只写入 Host ID、规范远端路径、
|
||||
Runtime 选择和默认工作模式。
|
||||
- 打开或切换已有项目只提交本地项目选择,不执行 SSH、registry、Workspace 或 Runtime
|
||||
检查。首次实际使用远程 Workspace 或 Runtime 时才解析 Host 管理的当前 Agent/Runtime,
|
||||
并建立或复用当前 Agent 连接。已安装 Agent 暂未运行时,
|
||||
固定 `attach-or-bootstrap` 命令幂等启动 registry 中的 current identity;这里的
|
||||
bootstrap 只启动进程,不安装组件,也不重新读取、哈希或验签完整 payload。
|
||||
Agent 进程读取 Host 管理已提交的 registry 和匹配 manifest 元数据,不重新验签或扫描
|
||||
完整安装 payload。
|
||||
- Workspace 和 Runtime 使用当前 Agent 连接产生的 live handle/lease。项目不保存或核对
|
||||
历史 Workspace/Runtime identity;当前 registry、连接状态或 capability 无效时停止并
|
||||
引导到 Host 管理,不在项目流程中下载或安装。
|
||||
- 桌面更新导致 Host 组件过旧时,项目保持未切换并提示前往 Host 设置。应用启动仍进入
|
||||
第一个普通本地项目,不自动连接或更新远端 Host。
|
||||
- Host 环境更新成功后,Main 定向失效该 Host 的 Agent/Runtime identity、连接和 Runtime
|
||||
会话缓存。所有项目在下一次实际操作时自然使用新的 current 环境;无需改写项目记录。
|
||||
- 环境准备失败时不改变已有项目绑定。Host identity 编辑失败时则继续保持 fail-closed,
|
||||
因为旧身份不能替代用户刚保存的新身份。
|
||||
- 项目管理入口与项目选择分离。项目选择器中的浮动管理按钮直接打开目标项目设置;删除
|
||||
不访问 Workspace、Agent 或 Host,因此远端目录已删除或 Host 不可达都不能阻止本地
|
||||
项目记录清理。删除当前项目或 Host 级联删除当前项目时只回退到普通本地项目。
|
||||
- 项目选择器把托管 SSH 项目按已保存 Host 分组,并只读取 Main 当前持有的 Agent 连接
|
||||
状态显示“未连接 / 连接中 / 就绪 / 异常”;打开菜单或读取状态不得隐式连接 Host。
|
||||
Host 已承担类型与连接语义,项目副标题只显示远端工作目录。
|
||||
- Host 删除确认使用本地数据库列出全部引用项目,包括归档项目;确认后级联删除本地项目
|
||||
及其会话、任务、成果、记忆、计划和心跳关联,再删除本地 Host 与凭据。整个流程不发起
|
||||
SSH,也不删除远端内容;跨 Host 设置文件与 SQLite 的普通失败会恢复已写入的一侧。
|
||||
|
||||
项目切换不展示远程激活进度,也不阻塞项目导航。`Host`、`Agent`、`Workspace`、`Runtime`
|
||||
和 `Saving` 进度只属于新建或显式保存项目;实际 Workspace/Runtime 操作使用正常的局部
|
||||
加载和错误反馈。
|
||||
|
||||
## 实现结构
|
||||
|
||||
当前实现复用现有机制,没有新增平行的安装管理器:
|
||||
|
||||
- 扩展当前 Host 系统探针和远程环境 inspector,返回瞬时下载准备能力。
|
||||
- 让当前 `RemoteEnvironmentUpdateService` 演进为 Host 环境准备编排器,统一用户手动
|
||||
启动的首次安装和后续更新、自动 acquisition 选择、进度、取消和最终失效。
|
||||
- Host 环境准备编排器直接完成一次 package prepare/commit 及 Agent/Runtime adoption。
|
||||
Workspace 和执行路径只读取 current registry 并调用 Host 侧轻量 identity/health 检查,
|
||||
在当前进程内复用已确认 identity 和 Agent 连接;Host 编辑或环境更新定向清空这些缓存。
|
||||
这些路径不会取得安装包、发布组件或通过 SFTP 扫描 payload。
|
||||
- Host 卡片即使显示相同版本也保留显式重装操作,用于修复 registry、签名或 payload
|
||||
identity 异常;若同一 digest 的 GoodBuddy-owned 目录校验失败,重装会先隔离旧目录,
|
||||
并在原子 commit 失败时恢复旧目录。commit 成功后的 adoption 失败保留新发布内容,
|
||||
下次重装清理旧暂存并重新 prepare/adopt。重装不要求删除 Host、凭据或项目,也不会
|
||||
处理所有者异常、符号链接或非 GoodBuddy 管理路径。
|
||||
- 由桌面控制面打包有界 one-shot installer,并通过固定短命令 `exec sh -s` 和 stdin
|
||||
发送脚本及结构化输入,避免 SSH command 长度限制;既有 package format v1 归档内固定
|
||||
Node 负责解码和运行它。
|
||||
- Preload 只暴露明确的 Host 探测、准备、取消和进度方法,不暴露 URL 或任意 SSH 命令。
|
||||
- 现有 Agent/Runtime side-by-side、固定 SSH lease、Host 端一次完整验证、注册后轻量检查、
|
||||
连接失效和项目稳定配置事务继续作为唯一实现。
|
||||
|
||||
## 迁移
|
||||
|
||||
- 已保存 Host 打开设置时只显示本地记录,不自动连接或检查版本;用户点击对应 Host 的
|
||||
“刷新版本”后才执行只读状态探测。项目切换不依赖该按钮,也不建立连接。
|
||||
- 数据库升级保留远程项目、Host ID、远端路径、Runtime 选择及关联会话/任务,并删除旧的
|
||||
Agent/Runtime/Workspace/Host 验证列和 Runtime 验证表。下一次实际远程操作从 Host
|
||||
current 环境重新取得所有 live identity。
|
||||
- 如果 Host 未就绪或版本不兼容,项目仍可在本地选择,但实际远程操作失败;不再走旧的
|
||||
隐式安装。
|
||||
- 已下载或导入的本地 `.gbagent` 保持可用,不能因加入远程直连而迁移或删除。
|
||||
- pending operation 保持 version 1,只记录 Host、目标 identity 与 operation ID,用于
|
||||
尽力清理旧 staging;它不是远端环境状态或恢复日志。
|
||||
- 远程直连继续使用现有 package format version 1,不要求归档携带 installer,也不要求
|
||||
目录声明额外的 bootstrap 能力元数据。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 新 Host 保存和探测完成后不会自动下载、上传或安装任何包,也不存在自动安装开关;
|
||||
再次进入 Host 设置不会逐台连接,只有显式“刷新版本”才探测目标 Host。
|
||||
2. Host 卡片只有一个按版本事实命名的主按钮;次级 SegmentedControl 提供默认且不持久化的
|
||||
“自动”及显式“Host 下载”“GoodBuddy 传输”。没有预装 Agent、Node 或 Runtime 的 Host
|
||||
可以完成准备;远程下载不可用时可手动改用 GoodBuddy 传输。
|
||||
3. 探测不下载完整包,失败不阻止保存 Host;非确定性探测失败不禁用直连重试,安装失败
|
||||
或取消后 Host 仍可重试。
|
||||
4. 直接下载严格匹配签名目录中的 URL、大小和 SHA-256;GoodBuddy 传输可在同一次操作中
|
||||
下载并验证缺失候选,以有界流式 SFTP 上传完整归档和已验证 bootstrap Node。Host 对
|
||||
两者完成相同的外层与内层签名、路径、架构、协议和 payload 校验。
|
||||
5. Host 对本次候选只完成一次完整 payload 校验;commit、adoption、health、capabilities
|
||||
和 prompt 启动不得再次遍历或哈希完整 Agent/Runtime,显式重装仍能用本次准备内容修复
|
||||
同 digest 的损坏目录。
|
||||
6. 创建、打开和切换项目不会产生 `.gbagent` 下载、Agent payload SFTP 上传或安装目录发布。
|
||||
7. 未就绪 Host 不能创建远程项目;已有项目仍可本地选择,但远程操作失败并引导到 Host 设置。
|
||||
8. Host 编辑或更新后,引用项目使用 Host 管理的当前连接和 current registry,不读取项目
|
||||
中的旧 Host/Agent/Runtime/Workspace identity;当前环境无效时才停止并要求在 Host 管理
|
||||
中修复。
|
||||
9. Host 地址、用户或 Host Key 在准备期间变化时停止候选发布,旧身份不会被重新启用。
|
||||
10. GitHub、北京镜像、无公网 Host、取消、应用退出、磁盘不足、损坏下载和不支持架构均有
|
||||
明确结果,且不触碰 GoodBuddy-owned 目录之外的文件。
|
||||
11. Linux x64 和 arm64 均通过 GitHub/北京镜像、取消、离线 GoodBuddy 传输,以及项目使用
|
||||
Host current 环境执行 Workspace/Runtime 操作的真实 Host 测试;这些真实 Host 验收当前
|
||||
仍待完成。
|
||||
12. Host 不可达或远端目录不存在时,用户仍可从项目浮动管理入口删除本地项目;删除 Host
|
||||
会列出并本地清理全部关联项目,但不会连接 Host 或删除任何远端目录和内容。
|
||||
13. 中断或 adoption 失败后的下一次更新只尽力清理旧 staging,然后 fresh prepare;
|
||||
pending record 不包含远端 metadata 副本。已健康环境的 cleanup 失败不触发回滚,
|
||||
也不阻塞下一次更新。
|
||||
@@ -0,0 +1,510 @@
|
||||
# SSH 远程主机与 GoodBuddy Agent 实现说明
|
||||
|
||||
## 状态
|
||||
|
||||
本文记录截至 2026-08-31 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
|
||||
Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Host current 环境”已经完成源码接线,
|
||||
详细事务与验收边界见
|
||||
[SSH Host 远程环境准备与直连下载设计](./environment-provisioning-technical-design.md);
|
||||
控制面直连源码可直接使用既有 package format v1 包,不等待携带 installer 的新 Agent
|
||||
包,也不通过额外目录元数据判断 bootstrap 能力;公开能力仍等待 GitHub/北京镜像、
|
||||
Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
|
||||
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
|
||||
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
|
||||
`0.11.10` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
|
||||
Agent 源码 lock 为 `0.11.14`,桌面版本仍保持已发布的 `0.11.13`,等待发布审批后再选择
|
||||
新版本。现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
|
||||
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
|
||||
|
||||
## 产品语义
|
||||
|
||||
远程 Host 与项目能力是默认关闭的技术预览。用户在“设置 > 平台功能”的独立
|
||||
“远程项目(技术预览)”页签启用开关后,Renderer 才显示 SSH Host 管理、托管 SSH 项目创建和已保存
|
||||
远程项目;Main 在 IPC 边界执行同一开关校验,避免隐藏界面被绕过。关闭开关不删除 Host、
|
||||
项目或凭据;当前远程项目切回第一个普通本地项目。应用启动仍固定
|
||||
选择普通本地项目,不会因为保存了远程项目而自动连接 Host。
|
||||
|
||||
远程项目只有两种工作模式:
|
||||
|
||||
- **Ask**:Runtime 在操作系统边界以只读方式访问项目 Workspace。
|
||||
- **Execute**:用户已授权使用所选 SSH 账号的完整权限。Runtime 可以使用该账号可访问的文件、进程、网络和工具,不再要求额外 trust tier、consent checklist、逐工具审批或“受控执行”授权。
|
||||
|
||||
Execute 不获得 root 或 SSH 账号本身没有的权限。托管 SSH Prompt 会把当前选中的文本模型
|
||||
profile 和凭据作为有界、逐 Prompt 的控制消息交给 Agent;该凭据不进入命令参数、环境变量、
|
||||
语义日志或模型调用账本,并在 Prompt 终态清除。此行为只适用于用户已明确启用的可信组织
|
||||
网络和已认证 SSH Host。
|
||||
|
||||
## 组件
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> 窄 Preload / Main IPC
|
||||
Main
|
||||
-> SSH Host 与 Host Key 管理
|
||||
-> SSH connection pool
|
||||
-> 签名 Agent / Runtime 安装
|
||||
-> 远程项目当前环境准备与稳定配置 SQLite 事务
|
||||
-> 永久项目、对话、Task 与已同步事件 SQLite
|
||||
SSH attach
|
||||
-> 远端私有 Unix socket
|
||||
Detached GoodBuddy Agent
|
||||
-> Workspace / Git 协议
|
||||
-> Agent-owned OpenCode ACP Prompt Promise
|
||||
-> Prompt-scoped Provider gateway / model-call ledger
|
||||
-> 有界语义 transcript / page / ACK
|
||||
-> 有界私有诊断记录
|
||||
-> 直接拥有的 Runtime 进程
|
||||
```
|
||||
|
||||
不提供把远端路径交给本机 Runtime 的伪本地模式,也不提供 SSH stdio Runtime fallback。
|
||||
|
||||
## SSH Host
|
||||
|
||||
- Renderer 只管理脱敏 Host 快照,不接触密码、密文、SSH Client 或任意命令接口。
|
||||
- Main 在认证前取得 Host Key,展示算法与 SHA-256 指纹,并保存完整 key blob。
|
||||
- 首次连接需要用户确认。Host Key 变化展示旧/新值并要求显式替换。
|
||||
- 密码使用 Electron `safeStorage`;系统 SSH Agent 认证不启用 Agent forwarding。
|
||||
- Host 地址、用户或 Host Key generation 变化时关闭旧连接,并定向退役依赖旧 Host identity 的 Workspace 和 Runtime 会话。下一次选择使用 Host 管理的当前连接记录解析 current Agent/Runtime,不读取项目中的旧 revision 或组件 identity;当前 registry、连接或 capability 无效时才要求显式修复。
|
||||
|
||||
## Agent 安装与生命周期
|
||||
|
||||
- Agent bundle 通过 manifest、Ed25519 签名、payload digest、平台和架构校验。
|
||||
- 安装使用当前 SSH 用户目录中的 GoodBuddy-owned 路径和 side-by-side digest 目录。
|
||||
- Agent 代码与固定 Node Runtime 一起位于签名 compound payload。显式安装或重装从本次
|
||||
已完整校验的准备目录原子发布整个 Agent,不扫描旧安装、不创建跨版本 Node 硬链接;
|
||||
Agent 不匹配不会阻止升级,也不会覆盖 GoodBuddy-owned 路径以外的内容。
|
||||
- 桌面包只携带版本 lock 与 production 公钥,不携带远端 payload。Host 卡片只有一个按
|
||||
版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮;次级
|
||||
SegmentedControl 选择“自动”“Host 下载”或“GoodBuddy 传输”,默认“自动”且不持久化。
|
||||
每个复合包绑定 Agent、固定 Node 与桌面维护的 OpenCode Runtime 精确工件。添加或重新
|
||||
验证 Host 只保存并探测,不自动传输完整包。
|
||||
- Host 管理独占 Agent/Runtime 的包准备、更新和完整 payload 验证。已有项目的打开和切换
|
||||
只更新本地项目选择。Workspace 和执行路径按需读取 Host current registry,
|
||||
执行固定 `attach-or-bootstrap` 并复用当前 Agent 连接;同一进程内复用已确认 identity。
|
||||
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload;已登记 Runtime
|
||||
的能力读取和 Prompt 启动只核对 registry 与 manifest 元数据,不再次执行、读取或检查
|
||||
OpenCode 二进制。
|
||||
- Agent 的固定 attach/按需启动命令只读取 Host 管理已提交的 installation registry 和
|
||||
与其 digest 匹配的有界 manifest 元数据,不在每次项目切换时重新验签或哈希完整 payload。
|
||||
完整验签、payload 扫描和 registry 写入仍只发生在显式 Host 准备/更新流程。
|
||||
- 项目切换不执行远程核对,立即选择本地项目配置。Workspace 或 Runtime 首次实际使用时
|
||||
才通过当前 Agent 连接核对所需能力;失败只影响该操作。新建或显式保存项目仍执行完整
|
||||
准备,并只事务写入 Host、路径、Runtime 选择和工作模式。
|
||||
- SSH 连接先尝试 attach;Agent 不存在或未运行时执行幂等 bootstrap,然后重新 attach。
|
||||
- Agent 是按需启动的 detached process,不注册开机服务,不依赖 systemd、D-Bus 或 Linger。
|
||||
- 每个模型桥 helper 都为 loopback HTTP 入口生成一次性随机路径 capability;只有写入当前 OpenCode 子进程配置的 URL 可以访问该入口,其他本机用户即使发现临时端口也不能提交模型请求。
|
||||
- Agent 监听当前用户拥有的私有 Unix socket。SSH 中断只关闭 relay,不拥有 Agent 和活动 Runtime 的生命周期。
|
||||
- SSH Host 设置通过固定探针和有界 SFTP 显示 Host 已登记的 Agent/OpenCode 版本,并与当前
|
||||
GoodBuddy 所需版本比较。进入或切回该设置页只读取本地 Host 列表,不连接任何 Host;
|
||||
用户点击“刷新版本”后才探测对应 Host,探测不安装或切换远端组件。项目切换与此按钮
|
||||
解耦且不建立连接;实际远程操作才建立或复用 SSH/Agent 连接。
|
||||
- “平台功能 > 远程项目”中的本地 Agent 包清单与 Host 状态分离。Main 对用户数据目录中
|
||||
Linux x64/arm64 `.gbagent` 分别执行外层可信签名、桌面/协议兼容性、内部 Agent/Runtime
|
||||
签名、架构与完整 payload 校验;首次打开和手动刷新还会读取“关于与更新”所选来源的
|
||||
小型签名目录。Renderer 只得到本地/在线版本、是否有更新、架构、远端 Runtime 版本、
|
||||
协议和本地状态,不得到缓存路径、key ID 或 digest。目录检查不下载 `.gbagent`,在线
|
||||
包下载只由用户点击带目标版本的操作触发;离线导入/导出通过 Main 管理的文件对话框完成。
|
||||
- “自动”只在 operation/prepare 前用 Host capability probe 选择 acquisition:直连明确
|
||||
可用才选 Host 下载,否则选 GoodBuddy 传输;显式选择不被改写。prepare、commit 或
|
||||
adoption 失败后都不跨 acquisition 自动 fallback。
|
||||
- Host 下载由桌面控制面通过固定 SSH prepare channel 和结构化 stdin 发送候选信息,
|
||||
Host 取得 compound `.gbagent` 并核对大小与 SHA-256。显式 GoodBuddy 传输在在线目录
|
||||
不可用,或本地已验证包版本不低于在线候选时优先租用本地包,包括更高版本离线导入;
|
||||
线上存在更新且可用时仍取得线上新包。需要下载时,同一次操作会验 SHA-256 与签名、
|
||||
缓存并取得 lease,再以有界流式 SFTP 上传一个 compound archive 和归档中已验证的
|
||||
bootstrap Node;约 294 MiB 完整包不会一次读入 Main `Buffer`,Host 仍再次校验完整包。
|
||||
- 两种 acquisition 交付到固定 operation staging 后,完全共用 control-plane
|
||||
`prepare → commit → Agent activate/health → Runtime activate → finalize → cleanup`。
|
||||
Main 只保存清理 operation staging 所需的 operation ID。中断或 adoption 失败后,下次
|
||||
更新尽力清理旧暂存并重新 prepare,不保存或恢复 Agent/Runtime metadata 副本。确认
|
||||
adoption 后才显式 cleanup;cleanup 失败不回滚健康环境,也不阻塞下一次 fresh prepare。
|
||||
Host 在 prepare 解包时
|
||||
完成一次完整 payload 校验,commit 和后续注册检查不再遍历包体。首次 bootstrap 不依赖
|
||||
既有 Agent daemon,也不要求 format v1 归档携带 `agent/lib/package-installer.cjs`。
|
||||
- 即使版本号均为当前版本,Host 卡片仍提供同版本“重新安装”,用于修复 registry、签名
|
||||
或安装 identity 异常。GoodBuddy-owned 同 digest 目录损坏时先隔离后替换,并在发布或
|
||||
原子 commit 失败时恢复;commit 成功后的激活失败保留新发布内容,下次点击重新安装时
|
||||
清理旧暂存并重新 prepare/adopt。不要求删除 Host、凭据或引用项目。
|
||||
- Host 卡片的“版本匹配”badge 只表达版本事实,不代表 Agent 正在运行或环境健康。重新安装
|
||||
失败时明确显示本次操作未完成,并以随后重新检查的版本卡片表达当前事实;提交结果
|
||||
不确定时不声称旧版本未被替换。
|
||||
- 更新成功后才定向清理该 Host 的 Agent 连接和 Runtime 选择缓存。引用项目再次打开时
|
||||
解析新的 current registry 并建立当前连接,无需刷新项目记录;当前环境无效时要求用户
|
||||
显式修复,但不在项目切换中下载或安装。失败或取消保留 Host 配置、凭据、项目、
|
||||
Workspace 和旧组件。
|
||||
- 显式 stop、升级、身份冲突或进程退出时清理 GoodBuddy 自己的 socket、状态和子进程;不得删除或覆盖无关 Host 文件。
|
||||
|
||||
### Host 级环境生命周期
|
||||
|
||||
- Agent、Node 和 Runtime 是 Host/SSH 账号级共享环境。新增或重新验证 Host 后先保存 Host
|
||||
并只读探测,不自动安装;用户随后在 Host 卡片明确选择安装或更新方式,失败保留 Host
|
||||
并允许重试。
|
||||
- Host 可以按签名目录固定的 URL、大小和 SHA-256 直接从当前 GitHub/北京镜像来源下载
|
||||
完整 `.gbagent`,也可使用 GoodBuddy 本机下载、流式 SFTP 传输和离线导入。自动模式只在
|
||||
操作开始时择一;执行失败不会在同一次操作中切换 acquisition。
|
||||
- 项目只保存 Host ID、远端路径、Runtime 选择和默认模式。创建或保存时验证 Host current
|
||||
环境;打开或切换只读取本地配置。Workspace/Runtime 实际使用时从 Host current registry
|
||||
和当前 Agent 连接取得 live identity,不再扫描、下载、上传或发布 Agent/Runtime。完整
|
||||
事务、兼容边界和验收要求以
|
||||
[Host 环境准备设计](./environment-provisioning-technical-design.md) 为准。
|
||||
- 打开新建/项目设置弹窗只读取本地 Host 验证记录,不并发检查所有 Host;目录浏览或保存
|
||||
才连接所选 Host。
|
||||
- 项目选择器中的管理操作默认浮动隐藏,在悬停、键盘焦点或触屏环境显示。管理任意已保存
|
||||
项目不先激活项目,因此 Host 不可达或远端目录已不存在时仍可删除其本地记录。
|
||||
- “远程项目”在选择器中按已保存 SSH Host 轻量分组。Host 标题显示 Main 已持有的真实
|
||||
Agent 连接状态(未连接、连接中、就绪或异常),读取和展示状态本身不连接 Host;
|
||||
项目副标题只显示远端路径,不重复“托管 SSH”类型。
|
||||
- 删除 Host 时确认框列出所有引用它的本地项目记录,包括归档项目。确认后只删除本机
|
||||
Host、凭据、项目及其关联记录,不连接 Host、不删除远端目录或内容;Host 设置写入或
|
||||
项目事务失败时恢复另一侧,避免只删掉其中一类本地记录。
|
||||
|
||||
## Workspace
|
||||
|
||||
Main 传入已规范化的绝对 POSIX root。Agent 返回 Workspace identity、访问模式、Git 状态和有界能力。
|
||||
|
||||
- 创建项目时,用户可以直接输入远端工作目录,或通过输入框右侧的文件夹按钮浏览并选择目录。
|
||||
- 项目保存前的目录浏览使用 Main 管理的只读、有界 SFTP,只返回目录;每次请求重新解析并校验当前 Host revision 与 Host Key generation,不安装 Agent、不暴露任意 shell/SFTP 接口。
|
||||
- 浏览从 SSH 账号 Home 或当前有效绝对路径开始,限制扫描次数、返回条目和总时限;取消、Host 变化、超时或连接失败时关闭 SFTP 并保留用户原先输入。
|
||||
- 选择目录只更新项目草稿,仍需通过正常的 Agent、Workspace、Runtime 验证和项目保存事务才会持久化。
|
||||
- Ask Workspace handle 不暴露写入方法。
|
||||
- Execute Workspace handle 可读写,但这不是 Execute 的唯一权限面;Execute Runtime 本身使用 SSH 账号的正常权限。
|
||||
- 文件预览、搜索、Git diff、目录项和显式文件传输保持字节、条目数和路径长度上限。
|
||||
|
||||
## Runtime
|
||||
|
||||
首个远程 Runtime 固定为签名 OpenCode ACP bundle。
|
||||
托管 SSH 项目的 Composer Runtime 菜单只显示当前配置的 OpenCode 和管理入口,不显示
|
||||
直连模型、Continue 或 DeepSeek Harness。激活旧远程会话时,若其保存了其他 Runtime
|
||||
selection,Renderer 会恢复为当前 OpenCode 配置;Main 的远程请求校验仍是最终边界。
|
||||
本机配置的 SKILL 包和 stdio MCP Server 不上传或分配给远程 Runtime;远程 ACP Session
|
||||
继续使用空 MCP Server 列表。Node.js、Python 等工具执行环境同样不修改或同步到 Host,
|
||||
完整边界见[工具执行环境](../local-tool-environment/README.md)。
|
||||
|
||||
### Ask
|
||||
|
||||
Ask 使用系统 `bwrap`:
|
||||
|
||||
- Workspace 和 Runtime bundle 只读 bind;
|
||||
- 独立可写 scratch HOME、TMPDIR 和 XDG 目录;
|
||||
- 固定环境名和固定 Runtime argv;
|
||||
- 通过只读边界阻止 Workspace 修改。
|
||||
- ACP 权限请求只有在工具种类为原生 `read` 且 Runtime 提供 `allow_once` 选项时才允许;
|
||||
search、edit、execute、未知工具以及只提供持久授权的请求全部拒绝。
|
||||
|
||||
`allow_once` 只解决 OpenCode 发起原生读取前的 ACP 协商,真正的文件系统边界仍是只读
|
||||
`bwrap` bind。Host 缺少 `bwrap` 时 Ask 不可用。
|
||||
|
||||
### Execute
|
||||
|
||||
Execute 不经过 Ask 的 bubblewrap profile:
|
||||
|
||||
- 直接启动已签名 Runtime entrypoint;
|
||||
- 使用 Agent 本地模型 gateway 时,签名 Agent launcher 会 `exec` 候选 manifest 锁定的
|
||||
Node Runtime;进程 owner 校验最终 Node executable,而不是已经被替换的 shell
|
||||
launcher 路径;
|
||||
- `cwd` 为项目 Workspace;
|
||||
- 继承 SSH 账号的正常环境、文件系统、进程和网络能力;
|
||||
- 不进行 T2/T3、confinement attestation、approval bridge 或逐工具批准。
|
||||
|
||||
两种模式都保留输入/输出字节上限、用户取消和进程组清理。生产 Prompt 不设置固定墙钟
|
||||
总时限;只要 Runtime 尚未按自身协议结束,Main 和 Agent 就允许其持续运行。启动、握手、
|
||||
单个控制 RPC、重连尝试和关闭清理仍使用独立的有界超时,测试可以显式注入短 Prompt
|
||||
时限验证取消升级,但该覆盖值不是生产默认。无界 Prompt 继续复用已发布的必填
|
||||
`deadlineAt` 字段,以 year-9999 哨兵表达;Main 与 Agent 必须同时理解该哨兵,Agent
|
||||
`0.11.10` 及更早版本仍会按其 manifest 上限夹紧,因此桌面启用这一语义前必须配套更新
|
||||
到 Agent `0.11.13` 或更高版本。
|
||||
|
||||
## ACP 与断线
|
||||
|
||||
- 管控面只负责有界 JSON-RPC:连接、查询、启动、取消、关闭和重连。数据面只负责
|
||||
ACP/blob 数据与 Agent 本机 journal。二者共享同一个经过 Host Key 校验和认证的
|
||||
SSH attach 字节流,但不共享生命周期:管控连接关闭不会隐式取消 Agent 已接受的
|
||||
Runtime 动作。
|
||||
- 帧只保留连接 identity、channel identity、单调 sequence、类型和长度。Main 与
|
||||
Agent 都按调用顺序 FIFO 写入,输入队列达到固定字节或条目上限时暂停 SSH stream。
|
||||
顺序、可靠传输、分包和网络背压全部交给 SSH;协议不实现 credit/window、优先级
|
||||
调度、跨 channel 超车、批发送或第二套拥塞控制。
|
||||
- Agent 在把 Main 输入交给 Runtime 前、以及把 Runtime 输出交给 Main 前,先把 ACP frame 写入本机 journal。ACK 只推进 cursor 并裁剪已确认 frame,不表示 channel 已终止。
|
||||
- Main 持久化 binding identity 和单调 cursor;保留中的 connection lease 即使处于 offline/reconnecting,也可以先把新 cursor 落盘。重连提交不能覆盖 resume 过程中并发落盘的更新。
|
||||
- Runtime 事件先经过单事件、单请求输出和每次 Prompt 最多 100 个不同工具调用的边界;
|
||||
通过边界后,Renderer 和 Main 将已接受的工具详情保存到本地 SQLite,不再额外缩短其
|
||||
输入、输出、错误或摘要。
|
||||
- OpenCode 原生 Task 工具按 `subagent_type`、`description`、`prompt` 和稳定 tool call ID
|
||||
解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP
|
||||
首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent
|
||||
状态卡,并持久化任务说明、终态与输出。
|
||||
- 同一 detached Agent 存活时,短暂 SSH 断线依次执行 `controller/resume`、`runtime/resumeAcpChannel` 和 `runtime/replayAcpChannel`,从 Main 已确认的 cursor 后只重放 Agent 到 Main 的已记录输出。重复 frame 会被确认但不会再次交给上层。若上一代连接只留下没有活动请求的 detached binding,完成精确 controller takeover 后会先有界停止并核对遗留 Runtime process,再用新 channel epoch 重新打开同一 binding 并恢复已有 ACP session;其他 controller、未证明 takeover 或仍有活动请求的 binding 仍被拒绝。
|
||||
- Main 到 Runtime 的 ACP 输入、模型请求、工具请求和 blob 不自动重放。
|
||||
- 无法确认外部 Provider 是否已处理的模型调用保持结果未知,避免重复计费或重复副作用。
|
||||
- 只有远端返回 identity 匹配且 `closed: true` 时,Main 才删除持久化 binding;传输失败或未确认 close 会保留 recovery identity。
|
||||
- 终态 close 由 Agent 在一个事务中为两个方向记录 sequence high-water tombstone,删除剩余 frame 和 active channel,并归零对应 journal quota。tombstone 用于拒绝迟到的旧 epoch frame,因此当前不会按时间自动裁剪。
|
||||
- 断线不主动终止正在运行的 Runtime。用户取消、显式关闭、输出边界或 Agent shutdown
|
||||
才触发停止;连接恢复只受当前用户取消信号和单次重连控制超时约束,不把 Prompt 已运行
|
||||
时长当作停止条件。
|
||||
- Agent 失联期间继续把 Runtime 输出写入本机 journal;重新连接只校验 controller
|
||||
identity、恢复 binding,并从 Main 最后确认的 cursor 后同步缺失数据。已接受的
|
||||
指令和结果不确定的 Provider 请求都不自动重放。
|
||||
|
||||
### Agent-owned Prompt 与 Desktop 恢复
|
||||
|
||||
- 托管 SSH 的生产 Prompt 从一开始就由 Agent 持有 ACP `ClientSideConnection`、原始
|
||||
Prompt Promise、Provider 轮次和 Runtime 进程。SSH relay 或 Desktop 进程不属于该
|
||||
Promise 的生命周期,因此正常退出、强制结束 Desktop 或本机网络中断后,Host 仍能继续
|
||||
后续模型/工具轮次。该路径要求 `runtime/acp` capability v4,即 Agent `0.11.14` 或更高。
|
||||
- Agent 把 ACP session update、permission decision 和唯一 Prompt 终态写入有界语义
|
||||
transcript。Main 不恢复旧 JSON-RPC Promise,也不重发 `session/prompt`;Fresh Desktop
|
||||
只对同一 controller/binding/operation 执行 takeover、attach 和 ACK-exclusive page。
|
||||
- 每个 Agent 语义 sequence 在 Main 映射为零到多个公开事件,最后追加内部 checkpoint。
|
||||
Desktop 在同一 SQLite 事务中去重 provenance、归并原 assistant message,并在 checkpoint
|
||||
时提交 Task 终态;事务成功后生成器才继续并向 Agent ACK。断电发生在同一 sequence
|
||||
中间时会重读该 sequence,已写事件按 `(binding, operation, sequence, eventIndex)` 去重,
|
||||
未写事件继续归并,不会重复 Provider 或工具执行。
|
||||
- ACK 后 Agent 删除已确认事件,只保留小型操作终态与游标用于幂等核对;未 ACK 输出继续
|
||||
保留。Main 按 transcript page 的最高连续 sequence ACK,终态仍立即 ACK。单事件、单页、
|
||||
单 Prompt 总字节和事件数都有上限;Agent 始终为唯一终态预留一个事件和最大事件字节,
|
||||
避免配额耗尽后无法记录终态或无法重启。已 ACK 终态操作和已交付模型 ledger 在 daemon
|
||||
运行期间也按固定上限裁剪,不依赖重启。Agent 重启不能重建内存中的 Runtime/Prompt
|
||||
Promise,因此启动时把遗留 nonterminal 记录变为 `outcome-unknown`,Desktop 只同步该
|
||||
终态而不自动重放。
|
||||
- prepare 已接受但 start RPC 未到达时,Agent 使用两分钟 prepare-to-start watchdog 清除
|
||||
进程和 Prompt 凭据并记录 `outcome-unknown`,避免无界 Prompt 哨兵造成永久悬挂。
|
||||
- Desktop SQLite 仍是永久可见项目/对话数据的权威;Agent transcript 只是尚未同步事件的
|
||||
权威。启动后 Renderer 先订阅恢复进度,再触发每项目恢复。项目选择器依次显示网络、
|
||||
Agent、Runtime、cursor、完成或失败/重试;只阻塞受影响项目的发送和队列,已有历史与
|
||||
其他项目保持可用。
|
||||
- 正常应用退出对已接受托管 SSH 请求执行 detach,不等待远端完成;本地请求和尚未接受的
|
||||
请求仍按原行为取消。用户显式“停止”始终发送稳定 operation cancellation,并等待 Agent
|
||||
语义终态。已进入 Agent-owned `run` 但 start/attach 响应仍不确定的请求继续保留为
|
||||
`interrupted + remote_recoverable`,并自动走 exact attach;不会因为尚未收到第一个公开
|
||||
事件而丢失恢复身份。
|
||||
|
||||
## Agent 本地模型 gateway
|
||||
|
||||
- Main 在 `runtime/preparePrompt` 中发送当前解析后的有界 profile;Agent 只在该 operation
|
||||
的内存状态中持有 Provider URL、API Key 和认证信息。OpenCode 仍通过 Prompt 私有 Unix
|
||||
socket 发出模型请求,但 HTTP Provider dispatch、响应交付和稳定 call ledger 全部位于
|
||||
Agent,因此 Desktop 断电后可以继续下一轮。
|
||||
- Agent 校验固定 model、协议和 API path,拒绝 background/store、provider web search、
|
||||
provider MCP、持久 conversation、非文本 modality 和其他可独立计费能力;请求/响应 body
|
||||
各限制 768 KiB,响应同时核对声明长度与短读。每个
|
||||
`(binding, operation, roundIndex)` 只 dispatch 一次;已 dispatch 但未证明完整交付的调用
|
||||
变为 `outcome-unknown`,不会自动重试。
|
||||
- Prompt 最多 100 个模型调用,并受单次与累计输出 Token 上限保护;这些是资源边界,不是
|
||||
Prompt 墙钟总时限。Provider 单次请求仍有独立超时。实际 usage 在语义 transcript 中同步
|
||||
回 Desktop 的 usage/task 数据。Agent 在所有进程终止路径统一清除 Prompt token 计数。
|
||||
- helper 可以接收同一 Prompt 内并发到达的模型桥请求;它在单一稳定模型桥上按到达
|
||||
顺序等待并交付,不返回本地 `bridge-busy`,每个响应只有在 HTTP 完整 flush 后才
|
||||
发送 delivery ACK。
|
||||
- GoodBuddy 自己管理会话标题,因此传给 OpenCode 的配置禁用 title Agent;一次用户
|
||||
Prompt 不会额外触发标题模型请求。
|
||||
- Agent gateway 不再把 Provider 请求往返 Desktop;legacy blob bridge 只保留给旧的
|
||||
非 Agent-owned 测试/兼容路径。凭据不进入 Renderer、SSH 命令参数或远端环境。
|
||||
- Unix socket 接收端按 `readableLength` 中已经缓冲的字节增量排空一个声明长度的帧,
|
||||
不会在部分大响应到达时反复请求尚未缓冲的完整剩余长度;短读、连接结束、错误和取消
|
||||
都会使当前交换失败。原生 Linux 回归覆盖至少 256 KiB 的 broker 响应。
|
||||
- Renderer 把相邻 text/reasoning delta 合并为消息 block 时始终替换最后一个 block,
|
||||
不修改既有 React state 对象。这样开发环境 StrictMode 重复调用 state updater 时,
|
||||
block metadata 与 canonical 消息正文保持一致。
|
||||
|
||||
## Agent 开发期间的真实 Host 验证
|
||||
|
||||
任何会改变已部署 GoodBuddy Agent 或桌面到 Agent 生产链路的源码改动,都必须在开发期间
|
||||
使用共享 Linux x64 真实测试 Host 验证。适用范围包括 Agent daemon、Agent protocol
|
||||
及共享 contract、Agent 组包与安装、attach/update、Runtime 启动与进程归属、Workspace、
|
||||
model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture、CI 或正式发布前回归
|
||||
都不能替代这一步;不得等到准备发布时才首次运行受影响的真实链路。
|
||||
|
||||
测试 Host 有两个网络入口:
|
||||
|
||||
- `192.168.0.23`:局域网入口,在本地网络可达时优先使用;
|
||||
- `10.7.0.23`:VPN 入口,局域网入口不可达时使用。
|
||||
|
||||
两者指向同一台物理 Host,只代表两条访问路由,不能计为两个独立 Host、两种架构或两份
|
||||
覆盖结果。测试只使用已固定的 Host identity 和 GoodBuddy 已有凭据存储;地址不得写入
|
||||
产品行为或默认配置,用户名、密码、私钥等凭据不得写入源码、文档、命令、日志或测试输出。
|
||||
|
||||
开发时应在改动链路达到可运行状态后尽早执行真实 Host 验证,并在最终相关改动后重复受
|
||||
影响的场景。必须使用当前源码构建或当前候选包,不能只验证 Host 上已经发布的旧 Agent。
|
||||
按改动范围至少选择以下场景:
|
||||
|
||||
1. Agent 组包、安装或升级:验证当前候选的 attach-or-bootstrap、安装/更新、版本切换及
|
||||
随后的连接。
|
||||
2. Runtime profile、进程启动/归属或 Ask/Execute 边界:通过桌面生产入口启动真实 Runtime,
|
||||
验证受影响模式;Ask 权限改动必须验证只读边界,Execute 权限改动必须验证所需写入、
|
||||
进程和网络能力。
|
||||
3. Agent protocol、RPC、stream、model bridge 或模型消息链路:执行最小有界真实模型调用,
|
||||
禁用非必要工具和附件,并记录实际调用次数;大消息或工具链路改动还应覆盖对应的真实
|
||||
payload/工具场景。
|
||||
4. 会话、owner、取消、超时、断线或恢复逻辑:执行与改动直接相关的中断、取消、Agent/
|
||||
Runtime 重启、SSH 断开或重连序列,并确认不会错误重放结果未知的操作。
|
||||
5. Workspace 或 Host 文件路径:只在 GoodBuddy 专用测试目录操作,验证真实路径和权限,
|
||||
不得读取、覆盖或删除无关 Host 文件。
|
||||
|
||||
停止或修改远端进程前必须先核对其启动身份、PID/进程组和 GoodBuddy 归属。若两个入口均
|
||||
不可达,应立即把真实 Host 验证报告为开发阻塞,保持该验证未完成,不能把 Agent 改动表述
|
||||
为已完成,也不能把首次真实验证推迟到发布阶段。
|
||||
|
||||
## 已完成的 E2E 验收记录
|
||||
|
||||
- 2026-08 的本地 fixture 完整验证 Linux x64 Agent `0.11.2-e2e.12`、Node `24.19.0`
|
||||
和 Agent protocol `2.0`;当时没有 arm64 fixture,因此该记录不能作为当前独立发布
|
||||
的双架构验收。Agent `0.11.10` 后续已由原生 workflow 发布并公开验证双架构工件;
|
||||
当前源码 lock 固定为 `0.11.14`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
|
||||
- 2026-08-30 在共享 Linux x64 Host 的隔离测试 HOME 中验证当前 `0.11.13` 源码候选:
|
||||
签名 Runtime 清单的测试墙钟上限为 1 秒,模型桥首轮故意延迟 2.515 秒后 Prompt 仍在
|
||||
5.770 秒正常完成;随后 OpenCode 原生 Task 依次产生 `running`、`completed` 子 Agent
|
||||
事件并完成父请求。测试结束后隔离 Agent、Runtime、socket 和 Workspace 已全部清理,
|
||||
Host current Agent 仍为 `0.11.10`。
|
||||
- 2026-08-31 使用隔离 Linux x64 Agent `0.11.14` 候选完成 remote-authoritative 矩阵:
|
||||
15 秒短 smoke、2 分钟正常 detach、5 分钟本机 harness 强制结束、并发对话、SSH relay
|
||||
丢失、显式取消、确定性 Provider 错误、2 秒 Provider 结果未知、Agent `SIGKILL`/重启,
|
||||
以及 reopened Desktop SQLite 恢复。成功工具的 START/END sentinel 都各出现一次;
|
||||
Provider 结果未知与 Agent 重启只提交 `outcome-unknown`,未重放 Prompt、模型调用或工具。
|
||||
- 同一矩阵中的 Desktop schema v32 实际恢复保持原 project/conversation/request/
|
||||
assistant-message 身份,在同一事务中提交语义 provenance、消息与 cursor;重新打开
|
||||
`assistant.sqlite` 后消息和 Task 都完整,重复 provenance 返回 false,recoverable Task
|
||||
归零。
|
||||
- 最终源码复核后重新构建测试签名 package:
|
||||
SHA-256 `39307fd1dccd638dcb723027591bcd83deab7057d550c99eff6f037e339829f4`,
|
||||
installation `agent-2cad05acfebe853394807e3a11f7befcd9f47215c2cf3a33a03d5f644b45716e`,
|
||||
Runtime `1.18.9` / bundle
|
||||
`sha256:6c7fc975415ef2547d3729bc4772d3e78362c2e76c05d1fc24968a0468fcb7ba`。
|
||||
最终 15 秒 detach/recovery 使用两个模型轮次、一个工具和一个终态,ledger 两轮均
|
||||
`completed + delivered`,语义 `latest=ACK=41`,保留事件、Runtime owner、ACP channel/
|
||||
frame 都为 0,START/END 各一次。最终取消另使用一个模型轮次、一个工具和一个 cancelled
|
||||
终态,语义 `latest=ACK=29`,清理计数同样为 0。最终补充验证共 3 次真实文本模型调用。
|
||||
采证后已按 PID/starttime/executable 停止精确隔离 daemon 并删除隔离 Host 测试根目录;
|
||||
`/root/.goodbuddy` 下的共享 Agent 安装和进程未被修改。
|
||||
- 2026-08-31 使用当前工作树短期测试签名 Linux x64 候选,在全新隔离 HOME 和 installation
|
||||
中验证有界诊断:bootstrap、detached daemon、attach、relay 断开与同 controller 重连、
|
||||
`controller/resume`、Runtime candidate 激活和 OpenCode ACP 进程启动均通过;新
|
||||
`diagnostics --installation-id` 命令在 daemon 重启后读取到 23 条完整 JSONL。诊断目录
|
||||
为 `0700`、文件为 `0600`,单文件 3719 bytes,未超过 64 KiB。六类测试哨兵覆盖 Prompt、
|
||||
密钥、环境、SSH 参数、用户文件内容和原始错误消息,CLI 输出与磁盘均未出现哨兵。把隔离
|
||||
diagnostics 目录临时设为不可写后,Attach 和 `agent/status` 仍正常;随后已恢复权限。
|
||||
- 2026-09-02 使用当前源码 Agent bundle 在共享 Linux x64 Host 的唯一
|
||||
`/tmp/goodbuddy-e2e-no-reverify-*` 隔离 HOME 验证已登记 Runtime 的快速激活路径。测试
|
||||
复制 Host current Runtime registry 与 bundle,在副本 OpenCode 二进制追加哨兵字节后,
|
||||
`runtime activate` 仍依据匹配的 registry 与 canonical manifest 成功返回
|
||||
`activated=true`,确认普通激活不重新哈希 payload 或执行 OpenCode 版本探测。本次没有
|
||||
模型调用;harness 成功后已删除整个隔离目录,未修改共享 Agent/Runtime 安装或进程。
|
||||
本次未注入模型凭据或执行 Prompt,真实文本模型调用 0 次;三文件轮转继续由聚焦测试覆盖。
|
||||
测试结束前按 PID、UID、starttime、executable 和唯一 installation 核对并停止所有本次
|
||||
daemon,本地及 Host 临时目录均已清理,共享 current Agent 和 `/root/.goodbuddy` 未修改。
|
||||
- 正常 Host 更新路径把 Linux x64 Host 的 Agent 更新为 `0.11.2-e2e.12`,并确认
|
||||
OpenCode Runtime 已安装版本与所需版本均为 `1.18.9`。
|
||||
- 一条新的 Ask 用户操作只提交一次。OpenCode 先在 build 模型轮次请求一个原生
|
||||
`read`,读取专用测试 Workspace 中的证据文件,再在第二个 build 模型轮次生成最终
|
||||
回答;没有 title 模型轮次,也没有第二个工具调用。
|
||||
- 两个模型轮次都记录已交付,远端 helper 保持休眠且 Unix socket 没有未读响应积压。
|
||||
持久化的 canonical 助手正文精确为 `GOODBUDDY_REMOTE_TOOL_4C6B9D8A`。验收操作没有
|
||||
重放任何结果未知或已经完成的 Provider 请求。
|
||||
|
||||
## 项目保存
|
||||
|
||||
远程项目只持久化稳定配置:
|
||||
|
||||
- Host ID;
|
||||
- 规范远端工作目录;
|
||||
- Runtime selection;
|
||||
- `ask | execute` 默认模式。
|
||||
|
||||
Agent installation、Host revision/Host Key generation、Workspace identity、Runtime
|
||||
bundle/adapter digest 和 capability generation 都属于 live connection/lease,不进入项目
|
||||
domain object 或 SQLite。打开托管 SSH 项目不接受 Renderer 回传的旧项目草稿,而是从 Host
|
||||
store 解析当前连接目标,通过 Agent/Runtime 安装管理器的 current/activate 读取路径取得
|
||||
registry identity,再用同一个 Agent 连接验证 Workspace 路径和 Runtime capability。该流程
|
||||
不会扫描完整 payload、取得安装包或发布组件。
|
||||
|
||||
实时 Runtime 创建只接受 OpenCode,使用当前解析后的模型 profile 准备 Agent 本地 gateway,
|
||||
从当前 Agent 连接和 Runtime registry 取得会话 identity,并在该连接上打开 Workspace 和
|
||||
ACP channel。Host 编辑或环境更新会定向失效 Agent 连接与 Runtime 缓存,下一次请求自然
|
||||
重新取得 current 环境;无需修改项目。Ask/Execute 权限继续由每次 Prompt 的 ACP Runtime
|
||||
边界执行。
|
||||
|
||||
Renderer 在用户主动打开已有托管 SSH 项目时立即切换,不显示远程激活状态。Host、Agent、
|
||||
Workspace、Runtime 和 Saving 进度仅用于新建或显式保存项目;进行中可显式取消并禁用会
|
||||
产生冲突的项目创建和设置入口。远端 RPC 拒绝只向
|
||||
Renderer 暴露固定方法名、数字 RPC code 和有界 service code,不转发 Host 私有错误详情。
|
||||
`runtime/preparePrompt` 在远端明确拒绝且尚未接受 Prompt 时会关闭对应 Main binding,
|
||||
不会把确定性失败错误保留为 `outcome-unknown`。未确认终态的 Prompt 仍不会自动重放;
|
||||
同一当前 Agent identity 上的 `prompt-running` binding 会阻止不同的新 operation,直到
|
||||
原 operation 恢复或取消,避免遗留 Host 工作与新 Prompt 并行。只有 Host/Agent/Runtime
|
||||
identity 已变化且旧 authority 无法恢复,或原 binding 已有 `outcome-unknown` 终态时,显式
|
||||
新 Prompt 才替换旧 binding。新 ACP session 的首次 Prompt 会把 Desktop 已持久化的有界
|
||||
会话历史作为不可信数据一并发送;成功加载或恢复原 ACP session 时不会重复注入历史。
|
||||
Ask 模式的 Linux Runtime 进程由 `bwrap` 启动后 `exec` 到已签名 Runtime 或 Agent
|
||||
model bridge helper;监督器只接受同一 PID/进程组从固定 `bwrap` 路径到预期最终可执行文件
|
||||
的有界转换,再登记为运行中进程。
|
||||
|
||||
不持久化 T2/T3、consent、approval bridge、confinement 或组件验证结果。数据库 schema
|
||||
v32 保留项目及关联用户数据,并加入 remote-recoverable Task/message/provenance/cursor;
|
||||
执行空间表保持 `project_id/kind/root_path/ssh_host_id`
|
||||
四列,并删除旧 Runtime 验证表。
|
||||
|
||||
## Agent 有界诊断
|
||||
|
||||
Detached Agent 不依赖 stdio 留存故障证据。每个 installation 在固定私有目录
|
||||
`~/.goodbuddy/state/<installationId>/diagnostics/` 中维护
|
||||
`agent-diagnostics.jsonl`、`.1` 和 `.2`,每个文件不超过 64 KiB,目录权限为 `0700`,
|
||||
文件权限为 `0600`。记录覆盖 daemon 启停、detached 启动、连接、恢复和 Runtime
|
||||
启动/退出;每条只包含固定事件、时间、PID、可选工作模式、固定原因和白名单错误码。
|
||||
|
||||
诊断写入使用最多 64 条的有界异步队列。连接、恢复和 Runtime 回调只把已经归一化的固定
|
||||
记录入队,不等待磁盘;原始 `Error` 不进入队列。队列满、目录不可写或轮转失败只丢弃诊断,
|
||||
不得改变 Attach、协议、Runtime 或退出结果。daemon 停止和 bootstrap 生命周期结束时执行
|
||||
有界 flush。多个 Agent 进程通过 GoodBuddy 私有短期锁协调 append 和轮转,不把长期锁作为
|
||||
运行前提。
|
||||
|
||||
已安装 Agent 提供只读固定命令:
|
||||
|
||||
```text
|
||||
goodbuddy-agent diagnostics --installation-id <installationId>
|
||||
```
|
||||
|
||||
该命令只能从 installation 对应的固定 state 目录读取上述有界文件,并逐行输出已验证 JSONL;
|
||||
不接受任意路径,也不扩展 Agent protocol。诊断不得包含 accepted Prompt、模型输入输出、
|
||||
模型密钥、完整配置、环境变量、SSH 参数、用户文件内容、路径或原始错误消息。应用内反馈的
|
||||
“附加最近桌面诊断记录”只读取 Desktop 诊断,不会自动取得或上传这些远端 Agent 记录。
|
||||
|
||||
## 资源与发布
|
||||
|
||||
- 所有桌面构建(包括 `portable` 与 `release:package`)只携带
|
||||
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
|
||||
`agent-release-keys.json`,不嵌入 `.agent-resources`、
|
||||
`.remote-runtime-resources` 或任何可安装 Linux 远端 payload。
|
||||
- `build/agent-package.cjs` 在对应原生 Linux 架构组装确定性 `.gbagent`,外层签名覆盖
|
||||
描述符和每个内部文件身份;内部 Agent 与 Runtime 仍分别使用既有签名 manifest 和 lock
|
||||
校验,但所有 production 层统一使用一组 GoodBuddy 通用发布身份并通过签名域区分用途,
|
||||
不要求内部 Runtime 单独配置密钥。`build/agent-catalog.cjs` 为双架构包生成签名累计目录,
|
||||
拒绝同一版本/架构改变字节。
|
||||
- `.github/workflows/agents.yml` 只使用进程内临时测试 key 做分支/PR 原生验证,不发布。
|
||||
`.github/workflows/agent-release.yml` 才可在 annotated `agent-v<version>` 标签和受保护
|
||||
`agent-signing` Environment 中构建 production 包。Agent GitHub Release 必须
|
||||
`--latest=false`;北京 OSS 先写 `agent-releases/v<version>/`,GitHub 公开后才切换
|
||||
单一 `agent-releases/latest.json` 指针,使客户端始终从同一不可变版本目录读取目录与签名。
|
||||
- Main 的本地缓存只发布完整验证成功的内容。在线目录和包都绑定固定 GitHub/OSS URL、
|
||||
大小和 SHA-256;选取规则是桌面最低版本满足、Agent protocol major 相同且 minor 不高于
|
||||
当前支持值的最高 SemVer。缺少某架构仅使该架构 Host 的托管 SSH 无法激活。
|
||||
- `agent-release-keys.json` 读取时按 UTF-8 JSON 解析并严格验证 schema、唯一 key ID、
|
||||
规范 Base64、有效 Ed25519 key、环境和撤销列表,不把 CRLF/LF 或普通 JSON 空白作为
|
||||
信任条件。上传到 Host 前重新序列化为确定性 LF JSON。签名目录、描述符、manifest 和
|
||||
payload 仍按原始签名字节及 SHA-256 验证。
|
||||
|
||||
## 发布前完整回归(不能替代开发验证)
|
||||
|
||||
以下清单只用于复核开发期间已经在真实 Host 上通过的链路,不得作为 Agent 改动的首次
|
||||
真实 Host 验证:
|
||||
|
||||
1. `npm test`
|
||||
2. `npm run typecheck`
|
||||
3. `npm run lint`
|
||||
4. `npm run build`
|
||||
5. 用测试签名复合包验证在线下载源选择和离线导入/导出;正式 Agent 候选还需公开校验
|
||||
双架构 production 包与签名目录。
|
||||
6. 在已固定 Host Key 的 Linux x64 测试 Host 上验证 attach-or-bootstrap。
|
||||
7. 在 GoodBuddy 专用测试目录验证 Ask 无法修改文件。
|
||||
8. 验证 Execute 可以写文件、启动进程和访问网络,同时不触碰无关 Host 文件。
|
||||
9. 运行一次有界的真实模型调用,确认凭据不进入 Renderer、SSH 参数、远端环境或磁盘,
|
||||
只在当前 accepted operation 生命周期内进入 Agent 内存。
|
||||
10. 中断并恢复 SSH,确认活动 Runtime 不被网络抖动终止。
|
||||
@@ -0,0 +1,11 @@
|
||||
# Smart Heartbeat
|
||||
|
||||
Smart Heartbeat 负责按计划或条件主动检查范围内信息,并生成可监督的建议或任务。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [智能心跳 PRD](./prd.md) | 用户目标、范围、计划、运行和产品验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
@@ -0,0 +1,246 @@
|
||||
# 智能心跳 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 入口与范围已实现;未来分区记忆待独立设计 |
|
||||
| 版本 | 0.5 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 文档角色 | 智能心跳当前能力、权威入口、范围和长期边界 |
|
||||
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
| 相关架构 | [自动化、监督与记忆平台总体设计](../../architecture/automation-platform-architecture.md) |
|
||||
| 相关界面 | [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md) |
|
||||
| Task 模型 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
> 本文区分近期实施与长期方向。未完成设计的“未来分区记忆”不得转写为数据结构、状态机、
|
||||
> 页面、迁移或验收条件。
|
||||
|
||||
## 1. 已确认的产品边界
|
||||
|
||||
### 1.1 智能心跳是独立能力
|
||||
|
||||
- “智能心跳”菜单入口是心跳配置、报告、建议和运行历史的权威位置。
|
||||
- 任务中心和设置中心不复制完整心跳配置表单。
|
||||
- 智能心跳不并入通用自动化中心,也不作为任务中心条目。
|
||||
- 现有心跳报告、记忆建议、行动建议和审计历史在本轮继续保留。
|
||||
|
||||
### 1.2 长期方向:未来分区记忆
|
||||
|
||||
已确认的方向只有:
|
||||
|
||||
> 智能心跳未来应成为按范围隔离的未来记忆。
|
||||
|
||||
以下内容尚未设计:
|
||||
|
||||
- “未来记忆”的数据结构与存储方式。
|
||||
- 与当前长期记忆、心跳报告和建议的关系。
|
||||
- 唤起条件、状态和生命周期。
|
||||
- 到期后的页面和交互。
|
||||
- 是否以及如何创建 Task。
|
||||
- 旧版心跳数据如何迁移。
|
||||
|
||||
本轮不得实现 `FutureMemory`、新增未来记忆数据库表,或制作“未来记忆 / 即将唤起 /
|
||||
已唤起”等页面。后续需要独立 PRD 和用户确认。
|
||||
|
||||
## 2. 本轮实施范围
|
||||
|
||||
### 2.1 目标
|
||||
|
||||
1. 完整心跳配置统一到“智能心跳”菜单入口。
|
||||
2. 心跳配置支持 `Global` 或指定一个、多个 Project。
|
||||
3. 保留现有概览、待处理建议、运行历史和报告能力。
|
||||
4. 新入口完整可用后,再移除任务中心和设置中心中的重复心跳配置。
|
||||
5. 旧心跳配置、运行、报告、建议、记忆和任务数据不丢失。
|
||||
|
||||
### 2.2 非目标
|
||||
|
||||
- 不设计或实现未来分区记忆。
|
||||
- 不修改 Task、Job 或 Task Center 模型。
|
||||
- 不新增独立 Automation Center。
|
||||
- 不移除定时任务现有入口。
|
||||
- 不扩大智能心跳的工具、目录、网络或 Execute 权限。
|
||||
- 不让智能心跳跨越配置范围读取数据。
|
||||
- 不将现有心跳建议静默转换成 Task。
|
||||
|
||||
## 3. 智能心跳范围
|
||||
|
||||
### 3.1 范围选择
|
||||
|
||||
每条心跳配置必须明确选择:
|
||||
|
||||
| 范围 | 当前执行语义 |
|
||||
| --- | --- |
|
||||
| `Global` | 回顾所有 Project 中允许读取的有界会话和任务,并读取 Global 已确认记忆 |
|
||||
| 指定 Project | 只回顾选中 Project 的有界会话和任务,并读取 Global 与选中 Project 的已确认记忆 |
|
||||
|
||||
指定 Project 可以选择一个或多个项目:
|
||||
|
||||
```text
|
||||
范围
|
||||
(•) Global
|
||||
( ) 指定项目
|
||||
[✓] 网站重构
|
||||
[ ] 内容运营
|
||||
[✓] 客户研究
|
||||
```
|
||||
|
||||
- `Global` 与指定 Project 互斥。
|
||||
- 指定 Project 时至少选择一个项目。
|
||||
- 多项目使用 Checkbox,不使用 Switch。
|
||||
- 当前项目只可以作为创建表单的建议默认值,不能在保存时隐式覆盖用户选择。
|
||||
- 切换当前项目不会改变已经保存的心跳范围。
|
||||
- Main 必须重新验证所有 Project ID,Renderer 不能扩大范围。
|
||||
|
||||
### 3.2 多项目执行
|
||||
|
||||
多项目配置的一次心跳仍然是一次运行和一份报告:
|
||||
|
||||
- 将选中项目中允许读取的会话、任务和记忆汇总后,再执行现有全局输入上限。
|
||||
- 不为每个 Project 分别创建 Run、报告、建议或 Task 副本。
|
||||
- 心跳报告成果以无项目归属保存,并在成果元数据中冻结本次配置范围。
|
||||
- 心跳产生的项目级记忆建议或行动建议必须明确目标 Project。
|
||||
- 目标 Project 不明确时,只能生成 Global 记忆建议或不绑定项目的行动建议,不能猜测。
|
||||
|
||||
### 3.3 兼容现有配置
|
||||
|
||||
旧配置按现有 `projectId` 非破坏映射:
|
||||
|
||||
- `projectId` 为空的配置迁移为 `Global`。
|
||||
- `projectId` 有值的配置迁移为“指定 Project”,且只绑定原项目。
|
||||
- 迁移不修改启停状态、重复规则、时间、回顾窗口、保留期限、下次运行或历史。
|
||||
- 项目删除时,只有该项目的配置按现有规则删除;多项目配置移除被删除项目并保留其他范围。
|
||||
|
||||
## 4. 权威入口
|
||||
|
||||
| 操作 | 权威入口 | 其他位置 |
|
||||
| --- | --- | --- |
|
||||
| 创建或编辑心跳配置 | 智能心跳 > 心跳计划 | 不复制表单 |
|
||||
| 选择 Global / Project 范围 | 智能心跳 > 心跳计划 | 不依赖当前项目推断 |
|
||||
| 暂停、恢复、立即心跳或删除 | 智能心跳 > 心跳计划 | 不复制操作 |
|
||||
| 查看报告与待处理建议 | 智能心跳 | 主导航徽标和通知可触达 |
|
||||
| 查看运行历史和失败 | 智能心跳 > 报告与记录 | 活动记录仅作审计补充 |
|
||||
| 管理平台级默认值 | 设置中心 | 不显示单条计划 CRUD |
|
||||
|
||||
移除重复入口的顺序:
|
||||
|
||||
1. 先在智能心跳中支持完整创建、编辑、范围、暂停、恢复、立即运行和删除。
|
||||
2. 验证旧数据与新范围均可管理。
|
||||
3. 再从任务中心移除 `HeartbeatSettings`。
|
||||
4. 再从设置中心移除重复 `HeartbeatSettings`,必要时保留“打开智能心跳”链接。
|
||||
5. 任一步失败都不得造成用户没有可用配置入口。
|
||||
|
||||
## 5. 智能心跳页面
|
||||
|
||||
保留现有四个页面:
|
||||
|
||||
- **运行概览**:状态、成功率、记忆、洞察和行动统计。
|
||||
- **待处理建议**:记忆建议和行动建议。
|
||||
- **报告与记录**:心跳报告与 Run 审计。
|
||||
- **心跳计划**:唯一完整配置入口。
|
||||
|
||||
### 5.1 心跳计划
|
||||
|
||||
创建和编辑至少显示:
|
||||
|
||||
- 名称。
|
||||
- `Global` 或指定 Project 范围。
|
||||
- 每日或每周重复规则。
|
||||
- 本地时间、星期与 IANA 时区。
|
||||
- 回顾窗口。
|
||||
- 保留期限。
|
||||
- 启停状态。
|
||||
- 下次运行与上次状态。
|
||||
|
||||
交互要求:
|
||||
|
||||
- 使用持久标签,不以 placeholder 代替。
|
||||
- 编辑进入明确状态,支持保存或放弃。
|
||||
- 保存失败保留草稿。
|
||||
- 删除继续使用共享破坏性确认。
|
||||
- 运行中禁用重复操作。
|
||||
- 保存成功后由计划列表直接反映结果;失败保留草稿并在编辑区就地提示。
|
||||
|
||||
### 5.2 范围呈现
|
||||
|
||||
- 页面标题不再根据当前 Project 伪装成心跳实际范围。
|
||||
- 每张计划卡片显示 `Global` 或完整 Project 摘要。
|
||||
- 多项目过多时显示“项目 A、项目 B 等 N 个”,并提供完整可访问名称。
|
||||
- 报告成果元数据保存配置冻结范围;计划卡片始终显示计划自身范围,不根据当前项目重新解释。
|
||||
- 项目失效或删除后,界面准确显示剩余范围或配置已被移除。
|
||||
|
||||
### 5.3 响应式与无障碍
|
||||
|
||||
- 宽窗口使用 `dashboard` 壳层,创建和编辑区域保持单列。
|
||||
- 窄窗口下计划列表和表单转为单列卡片。
|
||||
- `Global / 指定项目` 使用共享分段选择控件,Project 多选使用 Checkbox。
|
||||
- 范围、状态和失败不能只依赖颜色。
|
||||
- 表单错误与字段程序化关联。
|
||||
- 对话框或编辑区关闭后,焦点返回触发按钮。
|
||||
|
||||
## 6. 数据与安全要求
|
||||
|
||||
目标合同只扩展现有心跳范围,不引入 Future Memory:
|
||||
|
||||
```ts
|
||||
type HeartbeatScope =
|
||||
| { kind: 'global' }
|
||||
| { kind: 'projects'; projectIds: string[] }
|
||||
```
|
||||
|
||||
- 旧 `projectId` 只用于迁移兼容,不继续作为目标范围合同。
|
||||
- Main 在创建、编辑、列出和执行时验证 Project 归属。
|
||||
- Global 读取所有 Project 的有界会话和任务,但长期记忆仍只读取 Global。
|
||||
- 指定 Project 读取选中项目的有界会话和任务,以及 Global 与选中项目记忆。
|
||||
- 现有输入条数、字符预算、输出大小、超时、重试、租约和工具禁用边界继续生效。
|
||||
- 多项目汇总后统一应用上限,不能按项目倍增预算。
|
||||
- 心跳结果默认不在系统通知中暴露私人正文。
|
||||
- 数据迁移必须使用 SQLite 事务,保留外键、级联删除和现有历史。
|
||||
- 心跳运行失败时,运行记录与配置的 `last_status` 必须在同一 SQLite 事务中
|
||||
更新;任一写入失败时两者一起回滚,不能留下半提交状态。
|
||||
|
||||
## 7. 实施状态与后续顺序
|
||||
|
||||
### 已完成:入口和范围
|
||||
|
||||
- 扩展共享 Schema、Preload 与 Main 数据合同。
|
||||
- 增加 Global / 多 Project 持久化与旧数据迁移。
|
||||
- 让心跳执行按冻结范围构建有界输入。
|
||||
- 在“心跳计划”中完成创建、编辑和全部计划操作。
|
||||
- 补齐范围、迁移、权限和界面测试。
|
||||
|
||||
### 已完成:移除重复配置
|
||||
|
||||
- 从 Task Center 移除心跳完整表单,保留 Task 和 Scheduled Task 现有行为。
|
||||
- 从设置中心移除单条心跳 CRUD。
|
||||
- 如需要,设置中心仅保留“打开智能心跳”导航。
|
||||
- 验证应用内仍只有一个完整配置入口。
|
||||
|
||||
### 后续:独立设计
|
||||
|
||||
- 未来分区记忆。
|
||||
- 与长期记忆的关系。
|
||||
- 唤起模型与生命周期。
|
||||
- 与 Task 的关系。
|
||||
|
||||
这些内容需要新的 PRD 和明确确认,不属于本轮实现。
|
||||
|
||||
## 8. 验收状态
|
||||
|
||||
- [x] 智能心跳菜单是完整配置的唯一权威入口。
|
||||
- [x] 用户可以创建和编辑 `Global` 心跳。
|
||||
- [x] 用户可以创建和编辑绑定一个或多个 Project 的心跳。
|
||||
- [x] 切换当前 Project 不会改变已保存范围。
|
||||
- [x] Main 拒绝不存在、已归档或超出配置范围的 Project ID。
|
||||
- [x] Global 与多项目输入遵守现有总上限,不按项目倍增。
|
||||
- [x] 多项目一次运行只产生一份 Run 和报告。
|
||||
- [x] 旧 Global 和单 Project 配置无损迁移。
|
||||
- [x] 旧运行、报告、建议、记忆和任务仍可查看。
|
||||
- [x] 心跳计划支持创建、编辑、暂停、恢复、立即运行和删除。
|
||||
- [x] 任务中心不再包含完整心跳配置表单。
|
||||
- [x] 设置中心不再包含单条心跳 CRUD。
|
||||
- [x] 任务中心本身以及定时任务现有行为没有被删除。
|
||||
- [x] 智能心跳仍保持只读、禁用工具和有界输入输出。
|
||||
- [x] 没有新增 Future Memory 表、状态机或未确认页面。
|
||||
- [x] 加载失败、空状态、字段错误和危险操作满足统一设计与无障碍要求。
|
||||
@@ -0,0 +1,21 @@
|
||||
# Task 与 Job 文档集
|
||||
|
||||
本目录定义 GoodBuddy 的工作对象、内部执行单元和调度关系。
|
||||
|
||||
## 权威文档
|
||||
|
||||
1. [Task 与 Job 统一领域模型](./task-and-job-model.md):术语、身份和对象关系。
|
||||
2. [Task Center PRD](./task-center-prd.md):Task 的应用级索引。
|
||||
3. [Scheduled Task PRD](./scheduled-task-prd.md):时间或事件触发的 Task。
|
||||
4. [Goal Task PRD](./goal-task-prd.md):围绕可验证结果有界推进的 Task。
|
||||
5. [Job 与 Subjob PRD](./job-and-subjob-prd.md):Task 内部串行、并行和委派执行。
|
||||
|
||||
## 阅读顺序
|
||||
|
||||
先阅读统一领域模型。其他功能文档不得重新定义 Task、Conversation、Job、Run 或 Subagent。
|
||||
若实现与文档出现冲突,应先修正统一模型,再同步功能 PRD。
|
||||
|
||||
## 相关功能
|
||||
|
||||
- [直连模型 Agent 能力](../direct-model-agent/README.md) 使用本目录定义的 Subagent 与
|
||||
Job/Subjob 语义,为直连模型提供单层编程委派,但不重新定义产品对象层级。
|
||||
@@ -0,0 +1,55 @@
|
||||
# Goal Task PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中,未来能力 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 依赖 | [Task 与 Job 统一领域模型](./task-and-job-model.md) |
|
||||
|
||||
## 1. 产品定义
|
||||
|
||||
Goal Task 是围绕可验证结果持续推进的 Task。它只关联一条 Conversation,但该 Conversation
|
||||
也可以承载其他 Task;每轮观察、计划、行动和评估由内部 Job/Run 表达,不创建一串顶层 Task。
|
||||
|
||||
## 2. 必要配置
|
||||
|
||||
- 目标描述。
|
||||
- 至少一个成功标准。
|
||||
- 约束和停止条件。
|
||||
- 最大轮数、截止时间或预算。
|
||||
- 每轮评估方式。
|
||||
- 无进展处理。
|
||||
- Project、Runtime、知识、记忆、目录、工具和审批范围。
|
||||
|
||||
## 3. 有界循环
|
||||
|
||||
```text
|
||||
Observe Job
|
||||
→ Planning Job
|
||||
→ Permission and budget check
|
||||
→ Action Job / parallel Jobs
|
||||
→ Evaluation Job
|
||||
→ Complete, pause, revise or continue
|
||||
```
|
||||
|
||||
循环内的所有 Job 通过所属 Task 写入同一关联 Conversation。只有协调器把有意义的阶段进展
|
||||
写入消息时间线,避免每个内部步骤产生一条顶层 Task 或杂乱消息。当前 UI 只显示 Goal Task
|
||||
及其聚合状态,不显示 Job/Run 层级。
|
||||
|
||||
## 4. 完成和无进展
|
||||
|
||||
- 模型声明不能单独证明目标完成。
|
||||
- 成功标准必须可计算或可人工审查。
|
||||
- 连续两轮没有指标改善、重复下一步、连续失败、权限不可用或预算不足时暂停。
|
||||
- 修改范围、预算、Runtime、工作模式或权限必须用户确认。
|
||||
|
||||
## 5. 验收原则
|
||||
|
||||
- [ ] Goal Task 只关联一条 Conversation,Conversation 可以承载其他 Task。
|
||||
- [ ] 循环步骤以 Job 表达,不创建顶层子 Task。
|
||||
- [ ] 当前 UI 不展示 Goal Task 内部 Job/Run 层级。
|
||||
- [ ] 没有成功标准和停止条件时不能启用。
|
||||
- [ ] 无进展和预算耗尽不会伪装为成功。
|
||||
@@ -0,0 +1,102 @@
|
||||
# Job 与 Subjob PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中,未来能力 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 依赖 | [Task 与 Job 统一领域模型](./task-and-job-model.md) |
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在不创建额外顶层 Task 或 Conversation 的前提下,让一个 Task 能够分解、串行、并行和委派
|
||||
多个执行单元,并将进展和结果有序汇入 Task 的同一 Conversation。
|
||||
|
||||
## 2. Job 类型
|
||||
|
||||
首期只使用有限类型:
|
||||
|
||||
- `step`:Task 内一个明确步骤。
|
||||
- `scheduled_occurrence`:Scheduled Task 的一次到期执行。
|
||||
- `delegated`:交给 Subagent 或远程执行器。
|
||||
- `parallel_branch`:并行方案或分工。
|
||||
- `aggregation`:汇总多个前置 Job。
|
||||
|
||||
类型描述执行方式,不创造新的产品对象层级。
|
||||
|
||||
## 3. 并行模型
|
||||
|
||||
```text
|
||||
关联 Conversation
|
||||
└─ Coordinating Job
|
||||
├─ Parallel Job A
|
||||
├─ Parallel Job B
|
||||
├─ Parallel Job C
|
||||
└─ Aggregation Job
|
||||
```
|
||||
|
||||
- 并行 Job 使用同一个 `taskId` 和 `conversationId`。
|
||||
- 每个 Job 有独立输入快照、状态、Run、预算和输出缓冲。
|
||||
- 并行 Job 不直接同时追加助手消息。
|
||||
- Aggregation Job 或 Task 协调器按确定顺序生成一条进展或结果消息。
|
||||
- 用户可以按 Task 查看有界活动和聚合状态,但不选择或展开单个 Job;主 Conversation
|
||||
保持可读。
|
||||
|
||||
## 4. Subjob
|
||||
|
||||
Job 可以创建有界 Subjob:
|
||||
|
||||
- 默认最大深度 2。
|
||||
- 默认最大并发 3。
|
||||
- 默认最大子项数、模型调用、Token、耗时和输出大小由父 Job 预算限制。
|
||||
- 子级只能使用父级已授权能力的子集。
|
||||
- 父级取消、失败或超时后,活动子级必须取消。
|
||||
|
||||
## 5. Subagent
|
||||
|
||||
Subagent 是 Job 的执行者:
|
||||
|
||||
- 专家选择和路由记录在 Job 上。
|
||||
- Subagent 的原始流式输出进入有界 Job 缓冲和活动记录。
|
||||
- 完成、失败和部分输出都返回父 Job。
|
||||
- Subagent 不获得独立 Task Center 条目或 Conversation。
|
||||
|
||||
## 6. 状态与恢复
|
||||
|
||||
Job 状态至少包括:
|
||||
|
||||
```text
|
||||
queued → running → waiting_approval → completed
|
||||
↘ failed | cancelled | interrupted | budget_exceeded
|
||||
```
|
||||
|
||||
- 重试创建新 Run,不覆盖失败 Run。
|
||||
- 应用退出将活动 Job 标记为 `interrupted`。
|
||||
- 有外部副作用且结果未知的 Job 不自动重试。
|
||||
- 聚合 Job 必须明确处理部分成功、全部失败和取消。
|
||||
|
||||
## 7. 界面
|
||||
|
||||
当前产品 UI 的对象层级止于 Task,不提供 Job/Subjob 树、独立页面或导航入口。
|
||||
|
||||
关联 Conversation 和 Task Center 只显示:
|
||||
|
||||
- 当前总体进展。
|
||||
- 并行执行数量和聚合状态。
|
||||
- 需要审批或用户输入的 Task 状态。
|
||||
- 完成后的统一结果。
|
||||
|
||||
活动与 Runtime 可以按 Task 显示执行者、工具、耗时、预算、错误、审批和成果事件,但不把
|
||||
Job、Subjob 或 Run 暴露为可选择、可展开或可操作的产品对象。内部标识只用于关联与审计。
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
- [ ] 并行 Job 通过所属 Task 写入同一关联 Conversation。
|
||||
- [ ] Job 不创建顶层 Task。
|
||||
- [ ] 并行输出不会无序污染消息时间线。
|
||||
- [ ] Subjob 深度、并发、预算和输出有界。
|
||||
- [ ] Subagent 失败能够返回部分输出和明确状态。
|
||||
- [ ] 父级取消传播到所有活动子级。
|
||||
- [ ] 当前 UI 只展示到 Task,不显示 Job/Subjob/Run 层级。
|
||||
@@ -0,0 +1,370 @@
|
||||
# Scheduled Task PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 首期稳定 Task 生命周期、创建体验与 Conversation 输入仲裁已实现;高级触发和执行治理待实施 |
|
||||
| 版本 | 0.7 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 依赖 | [Task 与 Job 统一领域模型](./task-and-job-model.md) |
|
||||
| 相关架构 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md) |
|
||||
|
||||
## 1. 产品定义
|
||||
|
||||
Scheduled Task 是带时间或事件触发器的 Task。每个 Scheduled Task 关联一条 Conversation;
|
||||
一条 Conversation 可以同时承载多个 Task。
|
||||
|
||||
创建 Scheduled Task 时:
|
||||
|
||||
1. 用户选择关联当前 Conversation 或创建新 Conversation。
|
||||
2. 系统创建一个 Task,并保存稳定 `conversationId` 和 Schedule/Trigger Binding。
|
||||
3. 每次触发在同一 Task 内创建新的 Job 和 Run。
|
||||
4. 面向用户的文本进展和结果写回关联 Conversation,并标明 Task 来源。
|
||||
5. 独立交付物保存为 Artifact,并由结果消息引用。
|
||||
|
||||
因此,一个每日任务在 Task Center 中始终是一条 Task,而不是每天新增一条 Task;左侧会话
|
||||
列表通过行首展开按钮和带任务图标的子项呈现其关联。
|
||||
|
||||
## 2. 当前能力与差距
|
||||
|
||||
GoodBuddy 当前已实现首期统一生命周期:
|
||||
|
||||
- 创建 Modal 可以关联当前 Conversation 或原子创建新 Conversation,不修改当前
|
||||
Conversation 的标题和既有消息。
|
||||
- 每个 Schedule 绑定一个稳定产品级 Task 和 Conversation;重复触发复用同一身份,不再
|
||||
为每次触发创建新的顶层 Task。
|
||||
- 默认选择 Execute,并允许用户主动切换 Ask;不支持工具执行时明确禁用 Execute。
|
||||
- 单次、每日和每周计划支持暂停、恢复、立即运行、应用重启恢复和最多 4 个独立计划并发。
|
||||
- 到期和手动运行先进入关联 Conversation 的持久输入队列,与回复期间继续发送的普通消息
|
||||
顺序仲裁;默认不打断当前回复,也不与其并发写入时间线。
|
||||
- Composer 上沿显示待发送项和来源。用户可以删除尚未执行的 occurrence,或选择“立即
|
||||
中断并插入”取消当前执行并将该项提升为下一项。
|
||||
- 文本结果和失败写回关联 Conversation 并带 Task 来源;独立文件和图片继续保存为 Artifact。
|
||||
- 左侧 Conversation 列表、Conversation Task 区和 Task Center 使用同一产品 Task;普通
|
||||
模型请求、Subagent、委派和 Smart Heartbeat 内部 Task 不进入产品索引。
|
||||
- v22 迁移保留 Schedule 配置和历史运行,并为旧计划补齐稳定 Task 与 Conversation;v23
|
||||
增加可恢复的统一 Conversation 输入队列。
|
||||
|
||||
尚未实现的高级能力包括 IANA 时区与 DST 墙上时间、每月/工作日/受限 Cron、事件触发、
|
||||
可配置错过执行策略、租约、重试与结果未知治理、完整预算和权限快照,以及面向内部
|
||||
Job/Subjob/Run 的统一持久化抽象。当前每日和每周按既有 UTC 间隔递推。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 支持单次、每日、每周、每月、工作日和受限 Cron。
|
||||
- 支持 Task 完成、失败、Conversation 完成等内部事件触发。
|
||||
- 创建时明确选择当前或新 Conversation。
|
||||
- 默认使用 Execute,并允许用户主动切换到 Ask。
|
||||
- 冻结 Project、Runtime、工作目录、工具、知识、记忆和审批范围。
|
||||
- 提供时区、错过执行、幂等、租约、重试、恢复、取消、预算和审计。
|
||||
- 让所有重复触发复用同一 Task 和 Conversation 关联。
|
||||
- 为一次触发建立清晰 Job/Run,而不是创建新的顶层 Task。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
- 不提供任意脚本和循环的通用 DAG 编辑器。
|
||||
- 不允许模型生成并直接启用任意 Shell、SQL 或无限频率 Cron。
|
||||
- 不承诺应用退出后继续运行。
|
||||
- 不允许后台计划静默扩大权限、目录、知识、记忆或网络范围。
|
||||
- 不把 Smart Heartbeat 变成 Scheduled Task。
|
||||
- 不把每次触发、重试、Job 或 Run 显示为新的顶层 Task。
|
||||
- 不在左侧会话列表继续展开 Job、Subjob 或 Run。
|
||||
|
||||
## 5. 创建入口与 Modal
|
||||
|
||||
Task Center 和 Conversation 操作都可以提供“新建定制任务”,但共用同一个 Modal,不在
|
||||
窄侧栏长期展开完整表单。
|
||||
|
||||
```text
|
||||
新建定制任务
|
||||
创建一个可以按计划自动运行,并持续记录在会话中的任务
|
||||
|
||||
任务名称 *
|
||||
[ 每周项目总结 ]
|
||||
|
||||
任务要求 *
|
||||
[ 总结本周完成和失败的工作,并列出下周优先事项。 ]
|
||||
|
||||
关联会话
|
||||
◉ 当前会话
|
||||
产品发布讨论 · GoodBuddy Desktop · 已有 2 个任务
|
||||
|
||||
○ 新建会话
|
||||
为任务创建一条新会话,默认标题为任务名称
|
||||
|
||||
执行模式
|
||||
[ Execute ] [ Ask ]
|
||||
|
||||
运行频率
|
||||
[ 单次 ] [ 每日 ] [ 每周 ] [ 每月 ] [ 工作日 ] [ Cron ]
|
||||
|
||||
首次运行 [ 2026-08-21 ] [ 17:00 ]
|
||||
时区 [ Asia/Shanghai ▾ ]
|
||||
|
||||
执行范围
|
||||
GoodBuddy Desktop · OpenCode · 项目工作目录
|
||||
8 个工具可用 · 高风险操作需要审批 [编辑]
|
||||
|
||||
[取消] [创建任务]
|
||||
```
|
||||
|
||||
### 5.1 Conversation 选择
|
||||
|
||||
- 从当前聊天发起时默认选择当前 Conversation。
|
||||
- 从 Task Center 发起时默认选择新 Conversation。
|
||||
- 当前选择必须持续可见,不能根据入口静默决定后隐藏。
|
||||
- 关联当前 Conversation 不修改其标题、既有消息和普通聊天能力。
|
||||
- 当前 Conversation 已有关联 Task 时,显示 Task 数量和共享上下文说明。
|
||||
- 新 Conversation 默认使用 Task 名称作为标题,用户可以单独修改。
|
||||
- 远程通道、归档、正在删除或 Project 不匹配的 Conversation 不可选择,并显示原因。
|
||||
|
||||
### 5.2 创建摘要
|
||||
|
||||
提交前显示确定性摘要:
|
||||
|
||||
```text
|
||||
✓ 为当前 Conversation 新增一个 Task
|
||||
✓ 在左侧会话列表显示“任务 3”
|
||||
✓ 默认以 Execute 模式运行
|
||||
✓ 每周五 17:00 自动执行此 Task
|
||||
✓ 文本结果写入当前 Conversation
|
||||
✓ 独立交付物保存到成果
|
||||
```
|
||||
|
||||
创建 Task、可选新 Conversation、关联关系和 Schedule Binding 必须在 Main 中原子提交。
|
||||
失败时保持 Modal 和用户输入,不只显示短暂通知。提交期间锁定重复操作。
|
||||
|
||||
### 5.3 Modal 行为与无障碍
|
||||
|
||||
- 使用 `role="dialog"`、`aria-modal="true"`、稳定标题和说明关联。
|
||||
- 打开后聚焦首个必填字段,Tab 焦点限制在 Modal 内。
|
||||
- Escape 在未提交时关闭并恢复触发按钮焦点。
|
||||
- 窄窗口使用接近全宽布局,保留 `16px` 外边距。
|
||||
- 字段错误靠近字段;非字段异步错误保留在 Modal 内并提供重试。
|
||||
|
||||
## 6. 工作模式、Runtime 与工具
|
||||
|
||||
### 6.1 默认 Execute
|
||||
|
||||
创建 Modal 默认选择 Execute:
|
||||
|
||||
- Execute 可以调用当前 Runtime 与 Project 已启用、且被 Task 快照允许的工具。
|
||||
- Ask 保持 Runtime 边界只读,只能调用允许的只读能力。
|
||||
- 所选 Runtime 不支持工具执行时,不能静默降级为 Ask;用户必须更换 Runtime 或主动选择
|
||||
Ask。
|
||||
- Modal 持续显示实际 Runtime、Project、工作目录和权限摘要。
|
||||
|
||||
### 6.2 权限快照
|
||||
|
||||
Task 创建时冻结:
|
||||
|
||||
- Project 和工作目录。
|
||||
- Runtime 与模型选择。
|
||||
- 工作模式。
|
||||
- Skills、MCP、知识库、记忆和上下文范围。
|
||||
- 可用工具与审批策略。
|
||||
- 预算、并发和输出限制。
|
||||
|
||||
后续设置变化不修改已启动 Run。编辑 Task 配置只影响后续 Job。
|
||||
|
||||
### 6.3 审批
|
||||
|
||||
- Execute 继续遵守当前 Runtime、GoodBuddy 原生能力和工具审批控制。
|
||||
- 已启用且按现有策略允许自动执行的工具可以在后台运行。
|
||||
- 需要额外确认的动作进入 `waiting_approval`,暂停所属 Job 并发送应用内及桌面通知。
|
||||
- 用户批准后继续同一个 Job/Run;拒绝后按协议失败、跳过或请求调整。
|
||||
- 定时触发不能把高风险、越界或未授权动作转换成自动批准。
|
||||
- 结果未知的外部副作用进入 `outcome_unknown`,不得自动重试。
|
||||
|
||||
## 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
|
||||
}
|
||||
```
|
||||
|
||||
“工作日”是 `weekly` 的周一至周五预设,不增加新的持久化触发类型。
|
||||
|
||||
受限 Cron 使用五字段,不支持秒、年份、宏、`L`、`W`、`#` 或供应商扩展。Main 负责解析,
|
||||
默认最小间隔为 15 分钟,并展示未来五次触发时间。
|
||||
|
||||
### 7.2 事件触发
|
||||
|
||||
后续支持:
|
||||
|
||||
- `conversation.completed`
|
||||
- `task.completed`
|
||||
- `task.failed`
|
||||
- `artifact.created`
|
||||
- `knowledge.sync.completed`
|
||||
- `magic_note.updated`
|
||||
|
||||
事件触发配置来源范围、确定性过滤、去重窗口、冷却时间和并发上限。基础匹配不调用模型。
|
||||
|
||||
### 7.3 手动触发
|
||||
|
||||
“立即运行”在当前 Task 内创建独立 Job 和 Run,不改变下一次计划时间,不创建新 Task。
|
||||
重复点击使用调用级幂等键去重。
|
||||
|
||||
### 7.4 与普通消息的顺序
|
||||
|
||||
同一 Conversation 的普通消息和 Scheduled Task occurrence 使用同一 FIFO 队列。Agent
|
||||
正在回复时,到期 occurrence 只显示为待执行,不中断当前输出;当前执行结束后才认领下一项。
|
||||
用户显式选择“立即中断并插入”时,系统取消当前 Conversation 的活动请求,并让所选项成为
|
||||
下一项。删除待执行 occurrence 只取消该次运行,不删除稳定 Task、Conversation 或历史结果。
|
||||
|
||||
## 8. 一次触发的对象关系
|
||||
|
||||
```text
|
||||
Conversation
|
||||
└─ Scheduled Task
|
||||
├─ Schedule Binding
|
||||
└─ Job: scheduled_occurrence
|
||||
└─ Run
|
||||
```
|
||||
|
||||
- `scheduledFor` 和计划版本形成幂等键。
|
||||
- 同一 Scheduled Task 默认最多一个活动 occurrence Job。
|
||||
- 若允许并行 occurrence,它们仍属于同一 Task,并由协调器有序写回关联 Conversation。
|
||||
- 重试产生新 Run,不产生新 Task 或新 occurrence Job。
|
||||
|
||||
## 9. 左侧会话列表
|
||||
|
||||
普通 Conversation 保持单行。包含 Task 的 Conversation 显示行首展开按钮:
|
||||
|
||||
```text
|
||||
▾ 产品发布讨论 10:24
|
||||
|
||||
每周进度总结 每周 · Execute · 等待中 ●
|
||||
发布前检查 单次 · Execute ✓
|
||||
```
|
||||
|
||||
- 父会话行不重复显示任务标签或数量;Task 子项名称在左,模式、计划和状态标记收在右侧,
|
||||
与父会话的更新时间位置一致。
|
||||
- 点击 Conversation 标题打开聊天;点击 Task 子项打开同一 Conversation 并定位到该 Task。
|
||||
- 新建 Task 成功后首次自动展开。用户手动折叠后持久化其选择,后台运行不强制展开。
|
||||
- 默认最多直接显示 3 个 Task;“查看全部 N 个任务”打开该 Conversation 的完整 Task 区。
|
||||
- Task 子项显示本地化的模式、计划和非完成态状态文字;状态标记固定在行尾。已完成使用
|
||||
带勾成功点,并将完成文案保留给辅助技术,避免在紧凑元数据中重复显示且不只靠颜色表达。
|
||||
- 左侧只展开 Task;当前产品 UI 的其他区域也不提供 Job/Run 树或独立导航。
|
||||
|
||||
## 10. Conversation 内呈现
|
||||
|
||||
打开包含 Task 的 Conversation 后,顶部提供可折叠 Task 条:
|
||||
|
||||
```text
|
||||
本会话有 2 个任务
|
||||
[每周进度总结] [发布前检查] [管理任务]
|
||||
```
|
||||
|
||||
选中 Task 后显示:
|
||||
|
||||
- 名称、状态和模式。
|
||||
- 计划、下次执行和未来预览。
|
||||
- 最近一次执行结果。
|
||||
- “立即运行”“暂停”“编辑计划”等操作。
|
||||
- 需要审批时的明确恢复入口。
|
||||
|
||||
每条自动结果消息显示 Task 名称、触发来源和时间。普通文本作为消息保存;文件、图片、PDF 和
|
||||
其他独立交付物保存为 Artifact,并从消息引用。多个 Task 并发时,最终文本以完整消息写入,
|
||||
不能把流式 Token 无序混入同一消息时间线。
|
||||
|
||||
## 11. Task Center
|
||||
|
||||
Task Center 显示 Scheduled Task 的范围、关联 Conversation、状态、模式、最近进展、需要
|
||||
关注和下次触发时间:
|
||||
|
||||
- 点击条目打开关联 Conversation,并定位到该 Task。
|
||||
- “立即运行”在内部创建 Job/Run,但 UI 仍只呈现 Task,不改变计划时间。
|
||||
- 暂停只阻止新 Job,不取消已经完成的外部副作用。
|
||||
- Task Center 是完整索引;左侧展开列表只是最近 Conversation 下的轻量入口。
|
||||
- 不新增平行 Automation Center。
|
||||
|
||||
## 12. 错过执行
|
||||
|
||||
| 策略 | 行为 |
|
||||
| --- | --- |
|
||||
| `skip` | 记录跳过,不补跑 |
|
||||
| `run_once` | 无论错过多少次,只在当前 Task 内补一个 Job |
|
||||
| `catch_up_bounded` | 在数量和时间窗口上限内创建多个有界 Job |
|
||||
|
||||
默认补跑最多 3 次、最多回溯 7 天。补跑同样受 Task 的并发、权限和预算控制。
|
||||
|
||||
## 13. 时区和夏令时
|
||||
|
||||
- 保存 IANA 时区,不保存固定 UTC 偏移。
|
||||
- 春季不存在的本地时间在当日第一个有效分钟触发。
|
||||
- 秋季重复时间只触发一次。
|
||||
- 系统时区变化不自动修改计划时区。
|
||||
- UI 显示计划时区、本机时区差异和未来五次触发时间。
|
||||
|
||||
## 14. 预算、恢复和删除
|
||||
|
||||
每个 Scheduled Task 配置最大 Job 耗时、模型/Token/工具调用、成果大小、活动 Job 数和后台
|
||||
优先级。前台请求优先,后台达到上限时记录 `deferred`。
|
||||
|
||||
- 瞬时且没有未知副作用的失败可以有界重试。
|
||||
- 配置、权限和范围错误不重试。
|
||||
- 应用退出将活动 Job/Run 标记为 `interrupted`。
|
||||
- 取消 Task 必须传播到活动 Job、Subjob 和 Runtime。
|
||||
- 删除 Schedule 只停止后续触发,不删除 Task、Conversation 或历史。
|
||||
- 删除 Task 停止其计划并移除关联,默认保留 Conversation 和既有消息。
|
||||
- 删除 Conversation 前显示关联 Task 数量,并先处理活动 Job。
|
||||
|
||||
## 15. 兼容迁移
|
||||
|
||||
现有 Schedule、Schedule Run、Task 和 Conversation 数据渐进关联:
|
||||
|
||||
- 保留现有计划 ID、启停状态、下次时间和历史。
|
||||
- 为每个现有 Schedule 创建一个稳定产品级 Task。
|
||||
- 旧 Schedule 不猜测绑定已有用户 Conversation;为其创建新的关联 Conversation。
|
||||
- 历史每次执行映射为该 Task 下的 occurrence Job/Run。
|
||||
- 旧执行产生的 Task 行在映射成功后不再作为产品级 Task 索引,但其状态、活动和成果继续
|
||||
通过迁移后的 Job/Run 归属保留。
|
||||
- 旧文本 Artifact 可以保留,但迁移不得把它们重复写成新消息。
|
||||
- 迁移不得复制用户消息、独立成果或顶层 Task。
|
||||
|
||||
## 16. 验收标准
|
||||
|
||||
- [x] 创建 Scheduled Task 可以选择当前或新 Conversation。
|
||||
- [x] 关联当前 Conversation 不修改其标题、类型或既有消息。
|
||||
- [x] 一条 Conversation 可以在左侧展开一个或多个 Task。
|
||||
- [x] 默认工作模式为 Execute,且用户可以主动选择 Ask。
|
||||
- [ ] Execute 能调用快照允许的工具,但不能绕过 Runtime 和审批控制。
|
||||
- [x] 不支持工具的 Runtime 不会让 Execute 静默降级。
|
||||
- [x] 重复触发始终复用同一 Task 和 Conversation 关联。
|
||||
- [x] 每次触发创建内部运行记录,不创建新的顶层 Task。
|
||||
- [x] 内部运行记录只用于执行和审计,不在 UI 中显示为独立层级。
|
||||
- [ ] 支持单次、每日、每周、每月、工作日和受限 Cron。
|
||||
- [ ] UI 显示计划时区和未来五次触发时间。
|
||||
- [ ] 夏令时不会造成漂移或双跑。
|
||||
- [ ] 错过执行按配置跳过、补一次或有界补跑。
|
||||
- [x] 手动运行不改变下次计划时间。
|
||||
- [x] Scheduled Task 与普通消息共用 Conversation 级队列,不并发写入同一时间线。
|
||||
- [x] 当前回复期间可以继续发送普通消息,并在 Composer 上沿查看、删除或提升待发送项。
|
||||
- [x] 应用重启恢复尚未执行的队列项和有界附件上下文;已写入 Conversation 的用户输入
|
||||
通过队列项身份完成恢复对账,不会再次进入待发送队列。
|
||||
- [x] 文本结果只写入 Conversation,独立交付物才进入成果。
|
||||
- [ ] Task Center 和桌面通知可以打开正确 Conversation 并定位 Task。
|
||||
- [ ] 应用重启不自动重放结果未知的副作用。
|
||||
@@ -0,0 +1,245 @@
|
||||
# Task 与 Job 统一领域模型
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 产品边界与 Scheduled Task 首期已实现;通用 Job/Run 能力待实施 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 文档角色 | Task、Conversation、Job、Run 与 Subagent 的权威定义 |
|
||||
|
||||
## 1. 核心定义
|
||||
|
||||
### 1.1 Task
|
||||
|
||||
Task 是用户明确创建或确认的工作单位,也是 Task Center 的顶层对象。
|
||||
|
||||
- 每个 Task 必须关联且只关联一条 Conversation。
|
||||
- 创建 Task 时,用户可以关联当前 Conversation,也可以同时创建一条新 Conversation。
|
||||
- 关联当前 Conversation 不改变其对象类型、标题、既有消息或普通聊天能力,只增加 Task
|
||||
关联及其可见入口。
|
||||
- Task 的目标、状态、范围、计划、Job、审批、活动和成果使用独立 Task 身份保存。
|
||||
- 打开 Task 会打开关联的 Conversation,并定位或展开对应 Task。
|
||||
- 一个 Task 在生命周期内保持稳定的 `taskId` 和 `conversationId` 关联。
|
||||
- 删除 Task 默认停止其计划并移除关联,不删除 Conversation 或既有消息。
|
||||
|
||||
普通模型请求、工具调用或 Runtime Run 不自动成为产品级 Task。只有用户明确创建、确认或
|
||||
由已启用产品流程创建的工作,才进入 Task Center 和 Conversation 的 Task 列表。
|
||||
|
||||
### 1.2 Conversation
|
||||
|
||||
Conversation 是用户消息、助手消息和面向用户结果的内容容器,不因为关联 Task 而变成另一
|
||||
种 Conversation:
|
||||
|
||||
- 一条 Conversation 可以不关联 Task,也可以关联一个或多个 Task。
|
||||
- 多个 Task 可以共享同一 Conversation 的可见上下文,但各自拥有独立配置、计划、权限
|
||||
快照、状态、Job、Run 和成果引用。
|
||||
- Conversation 标题与 Task 名称相互独立。创建或重命名 Task 不静默修改现有会话标题。
|
||||
- 从稳定的本地 Conversation 创建分支会生成新的 Conversation 和消息 ID,并保存直接来源
|
||||
标识;它只复制可见聊天内容与会话级 Runtime/知识检索选择,不复制 Task、发送队列、Job、
|
||||
Run、记忆或 Artifact 归属。来源与分支后续独立更新,删除来源不删除已创建的分支。
|
||||
- 左侧会话列表根据显式 Task 关联显示行首展开按钮;父会话行不重复任务标签,展开后的
|
||||
Task 子项左侧显示名称,右侧显示状态标记,并只展开到 Task 层。
|
||||
- 并行 Job 不直接无序写入消息流;进度留在各自 Task/Job 状态中,最终文本以带来源元数据
|
||||
的完整消息写入 Conversation。
|
||||
- 删除 Conversation 前必须说明关联 Task 数量,并先停止或结算仍活动的 Job。
|
||||
|
||||
### 1.3 Job
|
||||
|
||||
Job 是 Task 内部的执行单位,不是新的顶层 Task:
|
||||
|
||||
- 一次计划触发、一个执行步骤、一项专家委派或一组并行工作都可以是 Job。
|
||||
- 一个 Task 可以串行或并行运行多个 Job。
|
||||
- 所有 Job 仍属于同一个 Task,并通过该 Task 关联的 Conversation 呈现用户可见结果。
|
||||
- Job 可以有自己的状态、预算、Runtime、执行者、输入快照和成果引用。
|
||||
- Job 不进入 Task Center、左侧会话列表或独立详情页。当前产品 UI 的对象层级止于 Task;
|
||||
活动和 Runtime 只按 Task 展示有界执行事件、工具、审批与错误,不呈现 Job 树。
|
||||
|
||||
### 1.4 Subjob
|
||||
|
||||
Subjob 是 Job 的子执行单元。它用于分解和并发,不创建新的 Task 或 Conversation。
|
||||
|
||||
- 父 Job 负责合并 Subjob 结果。
|
||||
- 取消父 Job 必须传播到仍活动的 Subjob。
|
||||
- Subjob 不能扩大父 Job 的 Project、目录、工具、知识、记忆或审批范围。
|
||||
- 深度、数量、并发、时间、Token 和输出大小必须有界。
|
||||
|
||||
### 1.5 Run
|
||||
|
||||
Run 是 Job 或 Subjob 的一次执行尝试和审计记录,不是用户工作对象:
|
||||
|
||||
- 重试、恢复或手动重新运行可以产生新的 Run。
|
||||
- Run 冻结当次配置、范围、预算、Runtime 和权限策略。
|
||||
- Run 进入内部审计;当前 UI 可以显示某次 Task 执行的时间、状态和活动,但不把 Run 呈现为
|
||||
可导航的产品对象。
|
||||
- `completed` 只表示该次执行按协议结束,不必然表示 Task 目标达成。
|
||||
|
||||
### 1.6 Subagent
|
||||
|
||||
Subagent 是执行 Job 或 Subjob 的受限执行者,不是对象层级:
|
||||
|
||||
- 专家、Agent Runtime 或其他执行器可以承担 Job。
|
||||
- Subagent 不自动拥有独立 Task 或 Conversation。
|
||||
- Subagent 输出先回到所属 Job,再由 Task 协调器写入关联 Conversation。
|
||||
|
||||
## 2. 对象关系
|
||||
|
||||
```text
|
||||
Conversation 1 ── 0..N Task
|
||||
│
|
||||
├─ Schedule / Trigger Binding(可选)
|
||||
├─ Job 1
|
||||
│ ├─ Run 0..N
|
||||
│ └─ Subjob 0..N
|
||||
│ └─ Run 0..N
|
||||
├─ Job 2(可与 Job 1 并行)
|
||||
└─ Artifact / Approval / Activity / Notification
|
||||
```
|
||||
|
||||
从 Task 方向看:
|
||||
|
||||
```text
|
||||
Task N ── 1 Conversation
|
||||
```
|
||||
|
||||
不允许:
|
||||
|
||||
```text
|
||||
Task → 没有关联 Conversation
|
||||
Task → 同时关联多条 Conversation
|
||||
Job → 新建顶层 Task
|
||||
Subagent → 自动新建 Conversation
|
||||
Job / Run → 成为可独立导航的 UI 对象
|
||||
```
|
||||
|
||||
## 3. 创建 Task
|
||||
|
||||
创建定制 Task 时必须明确选择 Conversation:
|
||||
|
||||
```text
|
||||
关联当前 Conversation
|
||||
或
|
||||
创建新 Conversation
|
||||
```
|
||||
|
||||
- 从当前聊天发起时,默认选择当前 Conversation。
|
||||
- 从 Task Center 发起时,默认选择新 Conversation。
|
||||
- 选择当前 Conversation 时持续显示会话标题、Project 和已有 Task 数量。
|
||||
- 选择新 Conversation 时,默认使用 Task 名称作为会话标题,但允许用户修改。
|
||||
- Task、Conversation 关联和可选 Schedule Binding 必须在 Main 中原子创建或回滚。
|
||||
|
||||
## 4. Scheduled Task
|
||||
|
||||
Scheduled Task 仍然是 Task,而不是 Schedule 定义和临时 Task 的松散组合:
|
||||
|
||||
1. 用户选择当前或新 Conversation。
|
||||
2. 系统创建一个 Task,建立稳定 `conversationId` 关联,并保存 Schedule/Trigger Binding。
|
||||
3. 到期时在该 Task 内创建新的 Job 和 Run。
|
||||
4. 每次触发的进展和文本结果写入同一关联 Conversation。
|
||||
5. 独立文件、图片和其他交付物保存为 Artifact,并从结果消息引用。
|
||||
6. 编辑计划影响后续 Job,不修改已经启动的 Run。
|
||||
|
||||
同一 Scheduled Task 默认串行触发。需要并行时,应显式允许多个 Job 并发,并继续使用同一
|
||||
Task 和 Conversation 关联,而不是复制顶层 Task。
|
||||
|
||||
## 5. 消息归属
|
||||
|
||||
Task 产生的用户可见消息至少记录:
|
||||
|
||||
```ts
|
||||
type TaskMessageMetadata = {
|
||||
taskId: string
|
||||
jobId: string
|
||||
runId: string
|
||||
trigger: 'manual' | 'scheduled' | 'event' | 'goal'
|
||||
}
|
||||
```
|
||||
|
||||
同一 Conversation 关联多个 Task 时:
|
||||
|
||||
- 消息持续显示来源 Task 名称。
|
||||
- 点击左侧展开项或 Task Center 条目可以定位对应 Task 和近期消息。
|
||||
- 任务筛选只改变定位和高亮,不隐藏用户未主动筛选的普通消息。
|
||||
- 多个活动 Job 的流式细节进入各自活动记录,最终文本有界持久化后再写入 Conversation。
|
||||
|
||||
## 6. 状态分层
|
||||
|
||||
| 层级 | 典型状态 | 用户在哪里看到 |
|
||||
| --- | --- | --- |
|
||||
| Task | idle、queued、running、waiting_approval、paused、completed、failed、cancelled、interrupted | Task Center、左侧会话展开项、Conversation |
|
||||
| Job | queued、running、waiting_approval、completed、failed、cancelled | 内部协调与审计,不作为 UI 对象 |
|
||||
| Run | claimed、running、completed、failed、cancelled、interrupted、budget_exceeded、outcome_unknown | 内部执行与审计,不作为 UI 对象 |
|
||||
|
||||
Task 状态由当前目标和所属 Job 聚合得出,但不能用“任一 Job 完成”直接推断 Task 完成。
|
||||
Conversation 折叠行只显示其关联 Task 中最高优先级的关注状态:
|
||||
|
||||
```text
|
||||
waiting_approval > failed > running > paused > idle
|
||||
```
|
||||
|
||||
## 7. UI 展示边界
|
||||
|
||||
当前产品 UI 的对象层级统一止于 Task:
|
||||
|
||||
- 左侧会话列表展开到 Task。
|
||||
- Task Center 只索引 Task。
|
||||
- Conversation 顶部任务区只选择和管理 Task。
|
||||
- 活动与 Runtime 可以展示 Task 的执行时间、工具、审批、错误、成果和状态事件,但不显示
|
||||
Job/Subjob 树,不提供 Job/Run 路由、列表或独立操作菜单。
|
||||
- “立即运行”“重试”和“恢复”在 UI 上都是 Task 操作;Job/Run 只在内部创建和审计。
|
||||
|
||||
## 8. 左侧 Conversation Task 列表
|
||||
|
||||
左侧最近会话列表是轻量发现入口,不替代 Task Center:
|
||||
|
||||
- 无 Task 的 Conversation 保持现有单行样式。
|
||||
- 有 Task 的 Conversation 显示行首展开按钮,父会话行不重复任务标签或数量。
|
||||
- 展开后只显示名称、本地化摘要和右侧状态标记,不继续显示 Job、Subjob 或 Run。
|
||||
- 新建 Task 成功后首次自动展开;用户手动折叠后保持选择,后台状态变化不强制展开。
|
||||
- 默认最多直接显示 3 个 Task;“查看全部 N 个任务”打开该 Conversation 的完整 Task 区。
|
||||
- Task 子项的状态标记固定在右侧;运行、审批、失败和暂停同时使用本地化状态文字。
|
||||
- 删除最后一个关联 Task 后,Conversation 的展开按钮自动消失。
|
||||
|
||||
## 9. 兼容映射
|
||||
|
||||
当前代码和旧文档中的对象按以下方式收敛:
|
||||
|
||||
| 旧概念 | 目标概念 |
|
||||
| --- | --- |
|
||||
| 自动任务 | Scheduled Task、Event Task 或 Goal Task |
|
||||
| 自动会话 | 删除该独立概念,使用关联 Conversation |
|
||||
| 子任务、Child Task | Job 或 Subjob |
|
||||
| 专家子任务 | 由专家 Subagent 执行的 Job/Subjob |
|
||||
| 多任务并行 | 一个或多个 Task 下的并行 Job;根据用户目标和 Conversation 归属明确建模 |
|
||||
| Schedule Run | Scheduled Task 内的 Job Run |
|
||||
| Automation Run | Task 所属 Job 或 Subjob 的 Run |
|
||||
| 普通请求 Task 行 | 内部执行/审计记录,不自动成为产品级 Task |
|
||||
|
||||
数据库字段可以在兼容期保留旧名称,但新产品文案、PRD 和新增契约必须使用本模型。
|
||||
|
||||
## 10. 安全和数据要求
|
||||
|
||||
- Main 验证 Conversation、Task、Job、Run 和 Project 的完整归属链。
|
||||
- Task 只能关联同一 Project 范围内允许使用的 Conversation。
|
||||
- Renderer 不能把任意 Task 或 Job 绑定到其他 Project 的 Conversation。
|
||||
- Job/Subjob 继承 Task 的能力上限,只能缩小,不能扩大。
|
||||
- Execute Task 冻结 Runtime、工作目录、工具和审批策略;后台触发不能扩大权限。
|
||||
- 并行输出先有界持久化,再按确定顺序汇总到 Conversation。
|
||||
- 取消、超时、审批和应用退出必须沿 Task → Job / Subjob → Run → Runtime 传播。
|
||||
- 删除 Task 默认保留 Conversation 和消息;删除 Conversation 必须处理其全部关联 Task。
|
||||
|
||||
## 11. 验收原则
|
||||
|
||||
- [ ] 每个 Task 只关联一条 Conversation。
|
||||
- [ ] 一条 Conversation 可以关联零个、一个或多个 Task。
|
||||
- [ ] 创建 Task 可以选择当前 Conversation 或新 Conversation,且不会改变当前会话类型。
|
||||
- [ ] 左侧会话列表通过行首按钮展开带任务图标和本地化摘要的 Task,但不展开 Job/Run。
|
||||
- [ ] 当前 UI 不提供 Job、Subjob 或 Run 的独立列表、树、路由或操作菜单。
|
||||
- [ ] Scheduled Task 的重复触发复用同一 Task 和 Conversation 关联。
|
||||
- [ ] 一个 Task 可以运行多个串行或并行 Job。
|
||||
- [ ] Job、Subjob、Run 和 Subagent 不进入 Task Center,也不成为其他可导航 UI 对象。
|
||||
- [ ] Task 消息可以通过 `taskId`、`jobId` 和 `runId` 追溯来源。
|
||||
- [ ] 并行 Job 不直接无序写入 Conversation。
|
||||
- [ ] 取消和权限范围能够沿层级正确传播。
|
||||
@@ -0,0 +1,120 @@
|
||||
# Task Center PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | Scheduled Task 首期已实现;Goal/Event Task 与完整操作待实施 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 依赖 | [Task 与 Job 统一领域模型](./task-and-job-model.md) |
|
||||
| 界面归属 | [通用助手工作栏与执行空间](../assistant-workbar/prd.md) |
|
||||
|
||||
## 1. 产品定义
|
||||
|
||||
Task Center 是所有产品级 Task 的应用级单例索引,不是 Automation Center,也不复制
|
||||
Conversation 内容。点击条目打开其关联 Conversation,并定位或展开对应 Task。
|
||||
|
||||
每个 Task 只关联一条 Conversation;一条 Conversation 可以关联零个、一个或多个 Task。
|
||||
Conversation 不因为关联 Task 而改变对象类型。
|
||||
|
||||
## 2. 收录边界
|
||||
|
||||
收录:
|
||||
|
||||
- 用户明确创建或确认的 Task。
|
||||
- Scheduled Task、Event Task 和 Goal Task。
|
||||
- 未来由用户确认创建的其他顶层 Task。
|
||||
|
||||
不收录:
|
||||
|
||||
- 没有显式 Task 关联的普通 Conversation。
|
||||
- 普通模型请求或工具调用产生的内部执行记录。
|
||||
- Job、Subjob、Run、工具步骤或 Subagent。
|
||||
- Smart Heartbeat 配置、报告和建议。
|
||||
- 仅用于审计的活动记录。
|
||||
|
||||
当前产品 UI 的对象层级止于 Task。Task Center、左侧会话列表和 Conversation 任务区都不显示
|
||||
Job/Subjob/Run 树、独立详情或路由。
|
||||
|
||||
## 3. 列表信息
|
||||
|
||||
每条 Task 至少显示:
|
||||
|
||||
- 名称和 Global / Project 范围。
|
||||
- 关联 Conversation 标题。
|
||||
- Task 类型和触发来源。
|
||||
- Ask / Execute 模式。
|
||||
- 当前聚合状态。
|
||||
- 最近一次面向用户的进展。
|
||||
- 最近活动时间。
|
||||
- 等待审批、失败或需要关注状态。
|
||||
- 下次计划时间(如适用)。
|
||||
|
||||
Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run。
|
||||
|
||||
## 4. 交互
|
||||
|
||||
- 点击条目打开关联 Conversation,并定位到该 Task。
|
||||
- 支持按需要关注、进行中、已暂停、已结束筛选。
|
||||
- 支持立即运行、暂停、恢复、取消、编辑和删除。
|
||||
- 后台变化更新状态和徽标,不自动抢占当前页面。
|
||||
- 立即运行、重试和恢复在 UI 上都是 Task 操作,内部 Job/Run 不单独显示。
|
||||
- 完整消息留在 Conversation;工具、审批和错误可以在活动或 Runtime 中按 Task 查看;
|
||||
独立交付物在成果中查看。
|
||||
|
||||
## 5. 左侧 Conversation Task 列表
|
||||
|
||||
左侧最近会话列表承担轻量 Task 发现,不替代 Task Center:
|
||||
|
||||
```text
|
||||
▾ 产品发布讨论 10:24
|
||||
|
||||
每周进度总结 每周 · Execute · 等待中 ●
|
||||
发布前检查 单次 · Execute ✓
|
||||
```
|
||||
|
||||
- 无 Task 的 Conversation 保持现有单行样式。
|
||||
- 有 Task 时在行最左侧显示独立展开按钮;父会话行不重复显示任务标签或数量。
|
||||
- 更新时间默认靠近行右侧,更多操作按钮不常驻占位;悬停会话行时,按钮替换右侧时间显示。
|
||||
键盘焦点进入会话行或操作菜单展开时,按钮仍保持可见。会话标题溢出时保持右侧区域固定,
|
||||
并在自身裁切区域内横向滑动展示完整名称。未溢出标题不滑动,减少动态效果偏好下使用完整
|
||||
标题提示而不产生位移。
|
||||
- 展开后每个 Task 子项左侧显示名称,右侧显示本地化的模式、计划和状态标记,与父会话
|
||||
更新时间对齐;已完成使用带勾成功点并将文案保留给辅助技术。列表不显示 Job、Subjob、
|
||||
Run 或工具步骤。
|
||||
- 点击 Conversation 标题打开聊天;点击 Task 打开同一 Conversation 并定位该 Task。
|
||||
- 新建 Task 后首次自动展开;用户手动折叠后保持选择。
|
||||
- 后台状态变化不强制展开;Task 状态持续显示在展开后的子项和 Task Center 中。
|
||||
- 默认最多显示 3 个 Task;“查看全部 N 个任务”打开该 Conversation 的完整任务区。
|
||||
|
||||
## 6. Conversation 任务区
|
||||
|
||||
包含 Task 的 Conversation 顶部显示可折叠任务区:
|
||||
|
||||
```text
|
||||
本会话有 2 个任务
|
||||
[每周进度总结] [发布前检查] [管理任务]
|
||||
```
|
||||
|
||||
选择 Task 后显示名称、模式、聚合状态、计划、下次执行、最近结果和 Task 级操作。工具、审批、
|
||||
错误和成果通过 Task 关联显示,但不暴露 Job/Run 层级。
|
||||
|
||||
## 7. 删除关系
|
||||
|
||||
- 删除 Schedule 只停止后续触发,不删除 Task、Conversation 或历史。
|
||||
- 删除 Task 停止其计划并移除关联,默认保留 Conversation 和既有消息。
|
||||
- 删除最后一个 Task 后,左侧 Conversation 的展开按钮消失。
|
||||
- 删除 Conversation 前必须显示关联 Task 数量,并先停止或结算活动执行。
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
- [x] Task Center 只展示产品级 Task。
|
||||
- [x] 一条 Conversation 可以关联并展开多个 Task。
|
||||
- [x] 点击 Task 打开正确 Conversation 并定位到对应 Task。
|
||||
- [x] 左侧会话列表通过独立展开按钮显示带任务图标和本地化摘要的 Task 子项。
|
||||
- [x] 当前 UI 不显示 Job/Subjob/Run 树或独立页面。
|
||||
- [x] Scheduled Task 显示下次时间,但每次触发不新增 Task 条目。
|
||||
- [x] 普通模型请求和工具调用不会误显示为 Task。
|
||||
- [ ] 删除 Task 默认保留 Conversation 和既有消息。
|
||||
- [x] Smart Heartbeat 不进入 Task Center。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 微信消息通道
|
||||
|
||||
微信消息通道负责将微信连接、项目、工作目录、Runtime 和工作模式接入 GoodBuddy。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [微信 ClawBot 通道 PRD](./prd.md) | 通道连接、项目绑定、消息处理和验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
+11
-5
@@ -19,7 +19,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
3. 远程执行任务使用哪个工作目录。
|
||||
4. 执行请求、处理后端、工具调用和结果归属于哪个范围。
|
||||
|
||||
腾讯官方微信 ClawBot 使用扫码授权,不使用 App ID 或 Secret,并由本机独立 Sidecar 持续运行通信服务。文字、图片和文件收发、只读对话与受控执行已经实现;真实微信账号的认证媒体端到端联调仍作为发布前手动验收。
|
||||
腾讯官方微信 ClawBot 使用扫码授权,不使用 App ID 或 Secret,并由本机独立 Sidecar 持续运行通信服务。文字、图片和文件收发、Ask 只读对话与 Execute 完整授权已经实现;真实微信账号的认证媒体端到端联调仍作为发布前手动验收。
|
||||
|
||||
为避免把通道来源、发送者身份和执行范围混在一起,本功能使用“通道项目 + 远程会话”的两级结构:
|
||||
|
||||
@@ -51,14 +51,14 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
- 在项目切换器中一眼识别微信、企业微信和钉钉来源。
|
||||
- 在通道项目中区分不同发送者和群聊。
|
||||
- 在设置卡片中完成连接、启停、处理后端、模式和工作目录配置。
|
||||
- 通过微信进行只读问答或发起受控执行任务。
|
||||
- 通过微信进行 Ask 只读问答或发起 Execute 任务。
|
||||
- 选择可信的执行后端,并查看完整执行记录。
|
||||
- 保留通道关闭前后的历史上下文和审计记录。
|
||||
|
||||
### 3.2 产品目标
|
||||
|
||||
- 将远程通道纳入 GoodBuddy 现有 Project、Conversation、Task、Activity 和 Artifact 信息架构。
|
||||
- 复用现有 ChannelService 的白名单、去重、并发、取消、输出限制和错误脱敏能力。
|
||||
- 复用现有 ChannelService 的白名单、去重、并发、取消和错误脱敏能力;回复长度与分段由各通道适配器按平台能力控制。
|
||||
- 保持 Electron Main、Preload、Renderer 和不可信子进程之间的安全边界。
|
||||
- 为后续语音、视频、多账号和更多通道提供稳定扩展点。
|
||||
|
||||
@@ -79,10 +79,10 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
|
||||
### 5.1 项目分组
|
||||
|
||||
项目选择器增加“远程通道”分组:
|
||||
项目选择器使用富信息单选菜单,不使用只显示项目名称的原生 `select`。当前区分“本地项目 / 远程通道”;未来出现真正的远程项目时,应新增独立分组,不把通道提前命名为远程项目:
|
||||
|
||||
```text
|
||||
普通项目
|
||||
本地项目
|
||||
├─ 默认项目
|
||||
└─ 用户创建的其他项目
|
||||
|
||||
@@ -92,6 +92,8 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
|
||||
└─ 钉钉
|
||||
```
|
||||
|
||||
选择器收起时显示当前项目名称;展开后每个选项至少显示项目名称和项目类型。本地项目补充目录摘要,远程通道项目补充平台来源和默认目录;接入可复用的通道状态数据后继续补充文字连接状态。未来接入其他远程项目时可以补充远程主机、工作区或服务来源,而不改变菜单的基本结构。
|
||||
|
||||
每个通道项目持续显示连接状态:
|
||||
|
||||
- 未配置
|
||||
@@ -395,6 +397,8 @@ Execute 消息通过身份、长度、去重和并发检查后:
|
||||
- 失败:回传经过脱敏、长度受限的用户可处理错误。
|
||||
- 取消:回传“任务已取消”。
|
||||
- 结果投递失败时保留发件箱记录并显示通道错误,不重复执行任务。
|
||||
- 发件箱达到五次投递尝试后进入可查询的终止状态,并继续通过现有通道错误回调
|
||||
暴露;终止记录不再发送,也不会从未投递查询中静默消失。
|
||||
|
||||
### 9.6 媒体与文件
|
||||
|
||||
@@ -645,6 +649,8 @@ Renderer 快照只返回是否已配置和脱敏标识。
|
||||
- [ ] 同名普通项目不会被占用或修改。
|
||||
- [ ] 通道项目默认根目录为当前用户目录,默认模式为 Ask。
|
||||
- [ ] 通道项目在项目选择器的“远程通道”分组中显示。
|
||||
- [ ] 项目选择器使用富信息单选菜单;每项显示项目名称和类型,本地项目显示目录摘要,远程通道显示平台来源与默认目录,并在可复用状态数据接入后显示文字连接状态。
|
||||
- [ ] 项目选择器的信息结构可扩展为“本地 / 远程”分组,不以图标或颜色作为项目来源和状态的唯一表达。
|
||||
- [ ] 停用或断开通道不会删除项目和历史。
|
||||
- [ ] 普通项目删除流程不能永久删除通道项目。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user