Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6287589a3 | ||
|
|
b2c85b8ece | ||
|
|
073eb0d2f5 | ||
|
|
3bf720a32c | ||
|
|
1274c762e8 | ||
|
|
c0ad27ab44 | ||
|
|
4483c92b57 | ||
|
|
d9114c20bf | ||
|
|
877cf581f6 | ||
|
|
0eb5205b1c | ||
|
|
c037d7fb20 | ||
|
|
020a9ad0dc | ||
|
|
444902d8ad | ||
|
|
fedafa21bd | ||
|
|
c1848a9dc4 | ||
|
|
21ac8330ea | ||
|
|
b01a856bae | ||
|
|
80b0b18712 | ||
|
|
428b4c0d08 | ||
|
|
ec100f8176 | ||
|
|
c1f8ba80ee | ||
|
|
f3b695214b | ||
|
|
abd3d5c6bb | ||
|
|
f669e490b8 | ||
|
|
b6d4633a33 | ||
|
|
5782667a40 | ||
|
|
e35ea7692f | ||
|
|
5a77dfdb93 | ||
|
|
177ab2140e | ||
|
|
cbb6398851 | ||
|
|
b1ba14f98e | ||
|
|
e95c7a60d4 | ||
|
|
c82d0161a9 | ||
|
|
9b7cf730d0 | ||
|
|
22a60497cb | ||
|
|
78418a56fa | ||
|
|
fbc0f5e9f3 | ||
|
|
c1f4fd0580 | ||
|
|
28de0c45ba | ||
|
|
024e6fb4b2 | ||
|
|
f95acdc000 | ||
|
|
451b30d434 | ||
|
|
25e897c22b | ||
|
|
c8db340cc7 | ||
|
|
9aed926cda | ||
|
|
86daae8be2 | ||
|
|
19086d656e | ||
|
|
d3947c79dd | ||
|
|
6a1a671d3b | ||
|
|
dbb7c2fc80 | ||
|
|
237b8a8cee | ||
|
|
d08ba33c3e | ||
|
|
b03c25685c | ||
|
|
7003141e56 | ||
|
|
e692362414 | ||
|
|
ee804edcae | ||
|
|
85a4a374fa | ||
|
|
89a81dd07c | ||
|
|
0eb664c0b6 | ||
|
|
9c6d1611e2 | ||
|
|
1f01c26e04 | ||
|
|
dd8289fdef | ||
|
|
d5fa0ec67c | ||
|
|
6eb73657e0 | ||
|
|
4041edefa8 | ||
|
|
482cd0c467 | ||
|
|
5a1afb00f7 | ||
|
|
a4b64a1641 | ||
|
|
9588c4b901 | ||
|
|
d180f63abd | ||
|
|
670171ce5d | ||
|
|
4490e5fd15 | ||
|
|
374968d50b | ||
|
|
4e60a4ca61 | ||
|
|
3765a937bd | ||
|
|
feaebdf221 | ||
|
|
70ec2bf792 | ||
|
|
8b9dbf55df | ||
|
|
b1e3101857 | ||
|
|
7369c13bcf | ||
|
|
fba5e1ad30 | ||
|
|
468cf0483f | ||
|
|
6586192222 | ||
|
|
987a08ad78 | ||
|
|
35a93e61cd | ||
|
|
0cba3855ea | ||
|
|
74c3ec19b4 | ||
|
|
cdc8f6f57a | ||
|
|
b8c1d33e7b | ||
|
|
f18e4abb3c | ||
|
|
c52732441a | ||
|
|
ba000300d5 | ||
|
|
3b8d129693 | ||
|
|
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 |
@@ -45,17 +45,22 @@ jobs:
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Build Linux ${{ matrix.arch }} compound package
|
||||
name: Build ${{ matrix.platform }} ${{ matrix.arch }} compound package
|
||||
needs: authorize
|
||||
if: needs.authorize.outputs.authorized == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
- platform: linux
|
||||
arch: x64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
- platform: linux
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
- platform: darwin
|
||||
arch: arm64
|
||||
runner: macos-15
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 25
|
||||
environment:
|
||||
@@ -76,11 +81,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Verify native build target
|
||||
run: node -e "if(process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Native Agent target mismatch')"
|
||||
|
||||
- name: Resolve locked native inputs
|
||||
id: inputs
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
AGENT_PLATFORM: ${{ matrix.platform }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node - <<'NODE'
|
||||
@@ -88,9 +97,11 @@ jobs:
|
||||
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 node = agent.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
|
||||
const runtime = remote.runtimes?.opencode
|
||||
const opencode = runtime?.targets?.[process.env.AGENT_ARCH]
|
||||
const runtimeKey = process.env.AGENT_PLATFORM === 'linux'
|
||||
? process.env.AGENT_ARCH : `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
|
||||
const opencode = runtime?.targets?.[runtimeKey]
|
||||
if (
|
||||
!node ||
|
||||
!opencode ||
|
||||
@@ -136,7 +147,7 @@ jobs:
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/goodbuddy-agent-release-inputs
|
||||
key: goodbuddy-agent-release-${{ matrix.arch }}-${{ steps.inputs.outputs.cache-key }}
|
||||
key: goodbuddy-agent-release-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.inputs.outputs.cache-key }}
|
||||
|
||||
- name: Acquire and verify native inputs
|
||||
shell: bash
|
||||
@@ -162,8 +173,14 @@ jobs:
|
||||
"$NODE_URL" --output "$node_archive.partial"
|
||||
mv "$node_archive.partial" "$node_archive"
|
||||
fi
|
||||
printf '%s %s\n' "$NODE_SHA256" "$node_archive" |
|
||||
sha256sum --check --strict -
|
||||
node - "$node_archive" "$NODE_SHA256" <<'NODE'
|
||||
const { createHash } = require('node:crypto')
|
||||
const { readFileSync } = require('node:fs')
|
||||
const [archive, expected] = process.argv.slice(2)
|
||||
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
|
||||
throw new Error('Node runtime archive integrity mismatch')
|
||||
}
|
||||
NODE
|
||||
|
||||
opencode_archive="$directory/$OPENCODE_ARCHIVE"
|
||||
if [[ ! -f "$opencode_archive" ]]; then
|
||||
@@ -201,6 +218,7 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
AGENT_PLATFORM: ${{ matrix.platform }}
|
||||
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
|
||||
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
|
||||
GOODBUDDY_SIGNING_PRIVATE_KEY: ${{ secrets.GOODBUDDY_SIGNING_PRIVATE_KEY }}
|
||||
@@ -211,11 +229,12 @@ jobs:
|
||||
output="$RUNNER_TEMP/goodbuddy-agent-output"
|
||||
mkdir -p "$output"
|
||||
node build/agent-package.cjs build \
|
||||
--platform "$AGENT_PLATFORM" \
|
||||
--arch "$AGENT_ARCH" \
|
||||
--minimum-desktop-version "$(node -p "require('./package.json').version")" \
|
||||
--node-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$NODE_ARCHIVE" \
|
||||
--runtime-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$OPENCODE_ARCHIVE" \
|
||||
--output "$output/goodbuddy-agent-$AGENT_VERSION-linux-$AGENT_ARCH.gbagent" \
|
||||
--output "$output/goodbuddy-agent-$AGENT_VERSION-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" \
|
||||
> "$output/result.json"
|
||||
node - "$output/result.json" "$AGENT_ARCH" "$AGENT_VERSION" <<'NODE'
|
||||
const { readFileSync } = require('node:fs')
|
||||
@@ -223,9 +242,10 @@ jobs:
|
||||
const result = JSON.parse(readFileSync(resultPath, 'utf8'))
|
||||
if (
|
||||
result.descriptor?.architecture !== architecture ||
|
||||
result.descriptor?.platform !== process.env.AGENT_PLATFORM ||
|
||||
result.descriptor?.version !== version ||
|
||||
result.archive !==
|
||||
`goodbuddy-agent-${version}-linux-${architecture}.gbagent` ||
|
||||
`goodbuddy-agent-${version}-${process.env.AGENT_PLATFORM}-${architecture}.gbagent` ||
|
||||
!/^[a-f0-9]{64}$/u.test(result.sha256)
|
||||
) {
|
||||
throw new Error('Production Agent package result is invalid')
|
||||
@@ -235,7 +255,7 @@ jobs:
|
||||
- name: Upload verified compound package
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: goodbuddy-agent-package-linux-${{ matrix.arch }}
|
||||
name: goodbuddy-agent-package-${{ matrix.platform }}-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/goodbuddy-agent-output/goodbuddy-agent-*.gbagent
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
@@ -269,7 +289,7 @@ jobs:
|
||||
- name: Download compound packages
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: goodbuddy-agent-package-linux-*
|
||||
pattern: goodbuddy-agent-package-*
|
||||
path: dist/agent-release
|
||||
merge-multiple: true
|
||||
|
||||
@@ -353,6 +373,7 @@ jobs:
|
||||
node build/agent-catalog.cjs create \
|
||||
--x64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-x64.gbagent" \
|
||||
--arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-arm64.gbagent" \
|
||||
--darwin-arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-darwin-arm64.gbagent" \
|
||||
"${previous[@]}" \
|
||||
--catalog dist/agent-release/agent-catalog.json \
|
||||
--signature dist/agent-release/agent-catalog.sig \
|
||||
@@ -489,8 +510,8 @@ jobs:
|
||||
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"
|
||||
for target in linux-x64 linux-arm64 darwin-arm64; do
|
||||
name="goodbuddy-agent-$AGENT_VERSION-$target.gbagent"
|
||||
curl --fail --silent --show-error --location \
|
||||
"$root/$name" --output "$RUNNER_TEMP/$name"
|
||||
cmp "dist/agent-release/$name" "$RUNNER_TEMP/$name"
|
||||
@@ -511,14 +532,8 @@ jobs:
|
||||
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
|
||||
notes="resources/agent-release-notes/$AGENT_VERSION.md"
|
||||
test -s "$notes"
|
||||
if gh release view "$tag" >/dev/null 2>&1; then
|
||||
is_draft="$(gh release view "$tag" --json isDraft --jq .isDraft)"
|
||||
if [[ "$is_draft" == true ]]; then
|
||||
|
||||
@@ -9,6 +9,8 @@ on:
|
||||
- 'build/agent-bundle.cjs'
|
||||
- 'build/agent-package.cjs'
|
||||
- 'build/agent-ci-bundle.cjs'
|
||||
- 'build/agent-build-target.cjs'
|
||||
- 'build/binary-architecture.cjs'
|
||||
- 'build/file-hash.cjs'
|
||||
- 'build/remote-runtime-bundle.cjs'
|
||||
- 'build/signing-key-preflight.cjs'
|
||||
@@ -19,6 +21,7 @@ on:
|
||||
- 'src/agent-daemon/**'
|
||||
- 'src/shared/**'
|
||||
- 'tests/agent-bundle.test.ts'
|
||||
- 'tests/agent-darwin-build.test.ts'
|
||||
- 'tests/agent-package.test.ts'
|
||||
- 'tests/agents-workflow.test.ts'
|
||||
- 'tests/remote-runtime-bundle.test.ts'
|
||||
@@ -31,6 +34,8 @@ on:
|
||||
- 'build/agent-bundle.cjs'
|
||||
- 'build/agent-package.cjs'
|
||||
- 'build/agent-ci-bundle.cjs'
|
||||
- 'build/agent-build-target.cjs'
|
||||
- 'build/binary-architecture.cjs'
|
||||
- 'build/file-hash.cjs'
|
||||
- 'build/remote-runtime-bundle.cjs'
|
||||
- 'build/signing-key-preflight.cjs'
|
||||
@@ -41,6 +46,7 @@ on:
|
||||
- 'src/agent-daemon/**'
|
||||
- 'src/shared/**'
|
||||
- 'tests/agent-bundle.test.ts'
|
||||
- 'tests/agent-darwin-build.test.ts'
|
||||
- 'tests/agent-package.test.ts'
|
||||
- 'tests/agents-workflow.test.ts'
|
||||
- 'tests/remote-runtime-bundle.test.ts'
|
||||
@@ -73,22 +79,28 @@ jobs:
|
||||
run: |
|
||||
npm test -- \
|
||||
tests/agent-bundle.test.ts \
|
||||
tests/agent-darwin-build.test.ts \
|
||||
tests/agent-package.test.ts \
|
||||
tests/remote-runtime-bundle.test.ts \
|
||||
tests/agents-workflow.test.ts
|
||||
npm run typecheck
|
||||
|
||||
build:
|
||||
name: Build Linux ${{ matrix.arch }} Agent
|
||||
name: Build ${{ matrix.platform }} ${{ matrix.arch }} Agent
|
||||
needs: validate
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
- platform: linux
|
||||
arch: x64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
- platform: linux
|
||||
arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
- platform: darwin
|
||||
arch: arm64
|
||||
runner: macos-15
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 20
|
||||
|
||||
@@ -103,17 +115,23 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Verify native runner target
|
||||
run: node -e "if (process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Agent CI requires the native target')"
|
||||
|
||||
- name: Resolve locked official Node runtime
|
||||
id: runtime
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
AGENT_PLATFORM: ${{ matrix.platform }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node - <<'NODE'
|
||||
const { appendFileSync } = require('node:fs')
|
||||
const lock = require('./agent-runtime-lock.json')
|
||||
const target = lock.node?.targets?.[`linux-${process.env.AGENT_ARCH}`]
|
||||
const { lock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
|
||||
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
|
||||
)
|
||||
const target = lock.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
|
||||
if (
|
||||
!target ||
|
||||
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(target.archive) ||
|
||||
@@ -147,7 +165,7 @@ jobs:
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/goodbuddy-agent-node
|
||||
key: goodbuddy-agent-ci-node-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
|
||||
key: goodbuddy-agent-ci-node-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
|
||||
|
||||
- name: Acquire and verify locked Node runtime
|
||||
shell: bash
|
||||
@@ -170,22 +188,34 @@ jobs:
|
||||
"$NODE_URL" --output "$archive.partial"
|
||||
mv "$archive.partial" "$archive"
|
||||
fi
|
||||
printf '%s %s\n' "$NODE_SHA256" "$archive" |
|
||||
sha256sum --check --strict -
|
||||
node - "$archive" "$NODE_SHA256" <<'NODE'
|
||||
const { createHash } = require('node:crypto')
|
||||
const { readFileSync } = require('node:fs')
|
||||
const [archive, expected] = process.argv.slice(2)
|
||||
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
|
||||
throw new Error('Node runtime archive integrity mismatch')
|
||||
}
|
||||
NODE
|
||||
|
||||
- name: Resolve locked OpenCode Runtime
|
||||
id: opencode
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
AGENT_PLATFORM: ${{ matrix.platform }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node - <<'NODE'
|
||||
const { appendFileSync } = require('node:fs')
|
||||
const { createHash: hash } = require('node:crypto')
|
||||
const lock = require('./remote-runtime-lock.json')
|
||||
const runtime = lock.runtimes?.opencode
|
||||
const target = runtime?.targets?.[process.env.AGENT_ARCH]
|
||||
const { runtimeLock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
|
||||
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
|
||||
)
|
||||
const runtime = runtimeLock.runtimes?.opencode
|
||||
const key = process.env.AGENT_PLATFORM === 'linux'
|
||||
? process.env.AGENT_ARCH
|
||||
: `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
|
||||
const target = runtime?.targets?.[key]
|
||||
if (
|
||||
!runtime ||
|
||||
!target ||
|
||||
@@ -213,7 +243,7 @@ jobs:
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/goodbuddy-opencode-runtime
|
||||
key: goodbuddy-agent-ci-opencode-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
|
||||
key: goodbuddy-agent-ci-opencode-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
|
||||
|
||||
- name: Acquire and verify locked OpenCode Runtime
|
||||
shell: bash
|
||||
@@ -251,16 +281,22 @@ jobs:
|
||||
NODE
|
||||
|
||||
- name: Run native Agent regressions
|
||||
if: matrix.platform == 'linux'
|
||||
run: |
|
||||
npm test -- \
|
||||
src/agent-daemon/model-bridge.test.ts \
|
||||
src/agent-daemon/lifecycle.test.ts
|
||||
|
||||
- name: Run native macOS process regressions
|
||||
if: matrix.platform == 'darwin'
|
||||
run: npm test -- src/agent-daemon/darwin-process-identity.test.ts
|
||||
|
||||
- name: Build and verify ephemeral signed compound Agent package twice
|
||||
id: agent
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
AGENT_PLATFORM: ${{ matrix.platform }}
|
||||
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
|
||||
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
|
||||
run: |
|
||||
@@ -270,11 +306,12 @@ jobs:
|
||||
mkdir -p "$directory"
|
||||
agent_version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
|
||||
node build/agent-ci-bundle.cjs \
|
||||
--platform "$AGENT_PLATFORM" \
|
||||
--arch "$AGENT_ARCH" \
|
||||
--node-archive "$RUNNER_TEMP/goodbuddy-agent-node/$NODE_ARCHIVE" \
|
||||
--opencode-archive "$RUNNER_TEMP/goodbuddy-opencode-runtime/$OPENCODE_ARCHIVE" \
|
||||
--output-directory "$directory/bundle" \
|
||||
--archive "$directory/goodbuddy-agent-$agent_version-linux-$AGENT_ARCH.gbagent" |
|
||||
--archive "$directory/goodbuddy-agent-$agent_version-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" |
|
||||
tee "$directory/result.json"
|
||||
node - <<'NODE'
|
||||
const { appendFileSync, readFileSync } = require('node:fs')
|
||||
@@ -286,6 +323,7 @@ jobs:
|
||||
)
|
||||
if (
|
||||
result.architecture !== process.env.AGENT_ARCH ||
|
||||
result.platform !== process.env.AGENT_PLATFORM ||
|
||||
!/^[a-f0-9]{64}$/u.test(result.archiveSha256)
|
||||
) {
|
||||
throw new Error('Agent CI result is invalid')
|
||||
@@ -300,14 +338,16 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
AGENT_ARCH: ${{ matrix.arch }}
|
||||
AGENT_PLATFORM: ${{ matrix.platform }}
|
||||
AGENT_SHA256: ${{ steps.agent.outputs.sha256 }}
|
||||
run: |
|
||||
{
|
||||
echo "### Linux $AGENT_ARCH Agent"
|
||||
echo "### $AGENT_PLATFORM $AGENT_ARCH Agent build"
|
||||
echo
|
||||
echo "Native compound Agent, Node, and OpenCode Runtime build, test-signature verification, launch smoke, and deterministic archive comparison passed."
|
||||
echo
|
||||
echo "Archive SHA-256: \`$AGENT_SHA256\`"
|
||||
echo
|
||||
echo "The ephemeral CI key and bundle are not published."
|
||||
echo "Native build checks do not replace real Host installation, Attach, and Ask/Execute acceptance."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -130,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
|
||||
@@ -174,6 +174,14 @@ jobs:
|
||||
name: goodbuddy-production-bundle
|
||||
path: out
|
||||
|
||||
- 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
|
||||
@@ -240,7 +248,7 @@ jobs:
|
||||
|
||||
- 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
|
||||
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build
|
||||
env:
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
|
||||
@@ -253,7 +261,7 @@ jobs:
|
||||
|
||||
- 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
|
||||
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build --unsigned
|
||||
env:
|
||||
ELECTRON_CACHE: ${{ runner.temp }}/electron
|
||||
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
node_modules/
|
||||
out/
|
||||
dist/
|
||||
# Local build output left by the extracted ShareServer project.
|
||||
shareserver/build-server/
|
||||
coverage/
|
||||
.vite/
|
||||
.runtime-resources/
|
||||
@@ -8,6 +10,8 @@ coverage/
|
||||
.agent-resources/
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
@@ -99,6 +99,14 @@ additional hardening beyond the actual internal-network threat model:
|
||||
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.
|
||||
- When fixing Runtime behavior or limits, also inspect the remote Runtime path
|
||||
through GoodBuddy Agent (`gbagent`). Verify whether local and remote execution
|
||||
actually share the affected control logic; do not assume that a desktop fix
|
||||
covers the Agent, bridge, launcher, or bundled Runtime. Fix any affected
|
||||
remote-specific implementation and validate it under the GoodBuddy Agent
|
||||
development validation rules below. If no separate remote fix is needed,
|
||||
record which shared implementation covers it or why the remote path is
|
||||
unaffected.
|
||||
- Keep model/provider credentials in Main, but do not otherwise reduce Execute
|
||||
permissions with extra product policy gates. Keep child-process cleanup
|
||||
straightforward and reliable.
|
||||
@@ -151,6 +159,22 @@ additional hardening beyond the actual internal-network threat model:
|
||||
- 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
|
||||
@@ -176,11 +200,6 @@ additional hardening beyond the actual internal-network threat model:
|
||||
blocking page state, or an error with an immediate local recovery action.
|
||||
- Do not show the same event both inline and as an application notification.
|
||||
Preserve user input and actionable error context when an operation fails.
|
||||
- Treat renderer bundle budgets as regression guardrails, not UX targets. Do
|
||||
not lazy-load one lightweight peer tab solely to satisfy a byte ceiling.
|
||||
When a genuinely heavy view needs deferred loading, preserve peer
|
||||
interaction semantics, render a non-empty layout-stable local fallback,
|
||||
and test its first interaction rather than only its eventual appearance.
|
||||
|
||||
## Commit Messages and Release Notes
|
||||
|
||||
@@ -233,14 +252,29 @@ authorization to add product persistence or recovery machinery.
|
||||
|
||||
Repository-wide release invariants:
|
||||
|
||||
- Write each release-note feature item as two or three clear sentences that
|
||||
identify the user situation, the available action, and the resulting
|
||||
behavior or important boundary. Do not impose an arbitrary per-item
|
||||
character limit; use the detail needed to make the user scenario clear.
|
||||
- Before approving the exact release commit and bilingual release notes,
|
||||
inspect the English `FEATURES.md` and Simplified Chinese
|
||||
`FEATURES.zh-CN.md` against the verified changes since the previous stable
|
||||
release. Keep both versions aligned, and update their provided capabilities,
|
||||
roadmap status, current versions, limitations, and remaining release
|
||||
acceptance work as needed. Include any required update in the release
|
||||
candidate; if no change is needed, explicitly confirm that both feature lists
|
||||
were reviewed and remain accurate.
|
||||
- Never create or push a release tag before the user approves the exact release
|
||||
commit and bilingual release notes.
|
||||
- Never move, delete, recreate, or reuse a release tag.
|
||||
- Keep desktop and Agent release paths, tags, artifacts, and Latest status
|
||||
separate.
|
||||
- Native GitHub Actions jobs are the cross-platform packaging authority. Never
|
||||
run local Windows packaging or packaged-app probes unless the user explicitly
|
||||
requests them.
|
||||
- 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
|
||||
@@ -256,14 +290,16 @@ 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. 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.
|
||||
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
|
||||
|
||||
@@ -296,7 +332,7 @@ validation are not substitutes for this development-time check.
|
||||
to release time.
|
||||
|
||||
The detailed scenario rules are documented in
|
||||
[`docs/architecture/remote-host-and-goodbuddy-agent-design.md`](./docs/architecture/remote-host-and-goodbuddy-agent-design.md).
|
||||
[`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
|
||||
|
||||
@@ -56,6 +56,14 @@ OpenCode/Continue 用例默认读取 `dist/harness-package-probe/win-unpacked`
|
||||
调用。测试不会输出 API Key,文件操作在临时工作区中执行。文件包含经 Main 回环
|
||||
broker 调用已分配自定义 MCP 的真实 OpenCode 和 Continue 用例。
|
||||
|
||||
不使用外部模型的 Runtime 原生交互回归位于
|
||||
`src/main/agent/opencode-runtime-permissions.test.ts`、
|
||||
`src/main/agent/continue-runtime-permissions.test.ts` 和
|
||||
`src/main/agent/deepseek-harness-acp-e2e.test.ts`。它们用本地确定性模型驱动真实 Runtime
|
||||
或固定 Host,覆盖工作区外访问、原生问答及 Ask 边界。OpenCode 用例默认使用开发缓存
|
||||
二进制;可通过 `GOODBUDDY_TEST_OPENCODE_BINARY` 指定已经安装的候选二进制,避免把
|
||||
仍在运行的旧缓存版本当作候选版本验证。这不替代当前源码 Agent 的 Linux Host 验收。
|
||||
|
||||
## 生产构建
|
||||
|
||||
生成 Electron Main、Preload 和 Renderer 生产文件:
|
||||
@@ -132,6 +140,12 @@ npm run dist:linux:arm64
|
||||
|
||||
打包产物位于 `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`)都不会嵌入
|
||||
@@ -139,7 +153,7 @@ npm run dist:linux:arm64
|
||||
组件,缺少远端包也不能阻塞普通桌面打包或发布。
|
||||
|
||||
“设置 > 平台功能 > 远程项目(技术预览)”列出用户数据目录中已经下载或导入的
|
||||
Linux x64/arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
|
||||
Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
|
||||
小型签名目录,显示本地版本、在线最新兼容版本以及 `有更新 / 已是最新 / 未下载 /
|
||||
校验失败`;此检查不会下载 `.gbagent`。只有用户点击“下载”或“更新到”后才传输包,
|
||||
用户也可导入或导出 `.gbagent` 离线包。
|
||||
@@ -194,7 +208,7 @@ Agent/Runtime metadata 副本。操作中断或 adoption 失败后,下一次
|
||||
package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host 直连;归档无需携带
|
||||
`agent/lib/package-installer.cjs`,签名目录不提供也不检查额外的 bootstrap 能力元数据。
|
||||
首次 bootstrap 不依赖已安装 Agent daemon。目标契约见
|
||||
[SSH Host 远程环境准备与直连下载设计](docs/architecture/remote-host-environment-provisioning-design.md)。
|
||||
[SSH Host 远程环境准备与直连下载设计](./docs/features/remote-host/environment-provisioning-technical-design.md)。
|
||||
|
||||
删除 Host 或远程项目属于纯本地管理操作。Host 确认框列出全部关联项目记录并级联清理
|
||||
本地数据库,但不连接 Host、不检查远端目录,也不删除远端文件;Host 设置文件和项目事务
|
||||
@@ -205,6 +219,9 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host
|
||||
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
|
||||
|
||||
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
|
||||
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
|
||||
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
|
||||
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
|
||||
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
|
||||
- DSH 插件安装使用精确锁定并从 `app.asar` 解包的 npm CLI,通过当前 Electron 的 Node 模式运行;最终用户不需要另装 Node.js 或 npm。
|
||||
- DSH 图片输入使用精确锁定的 `@napi-rs/canvas` 完整解码 JPEG/PNG。通用包与目标平台、目标架构的 Skia 原生包必须从 `app.asar` 解包;当打包 Runner 的架构与目标架构不同时,发布脚本会根据 lockfile 的精确版本、下载地址和 integrity 临时暂存目标原生包,完成后清理。发布校验会检查版本、目标架构和 MIT 许可证。
|
||||
@@ -212,6 +229,12 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host
|
||||
|
||||
跨架构打包前,确认目标架构的 OpenCode 资源已经准备完成。不要用其他架构的二进制替代目标资源。
|
||||
|
||||
GoodBuddy 管理的本机 OpenCode 启动后会轮询已认证的 loopback health 接口,最多等待
|
||||
30 秒。该窗口用于兼容 Linux ARM 等较慢环境;不要改回依赖 stdout 文案或 10 秒固定上限。
|
||||
聊天中的 Runtime 切换只执行路径和模型凭据就绪检查,不为状态显示启动临时 Server。
|
||||
设置中的显式连接测试、原生能力清单读取和首次实际请求仍会启动 OpenCode,并继续使用
|
||||
上述完整健康检查。
|
||||
|
||||
## GoodBuddy Agent 工件
|
||||
|
||||
面向用户发布的是按远端 Host 架构区分的复合 `.gbagent`,每个包同时包含签名
|
||||
@@ -242,16 +265,32 @@ node build/agent-package.cjs build \
|
||||
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
|
||||
Linux x64/arm64 复合包,并使用 `macos-15` 原生构建 Darwin arm64
|
||||
复合包。所有 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
|
||||
地址与 digest,并从 `remote-runtime-lock.json` 解析固定 OpenCode npm 包与 integrity;
|
||||
下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层
|
||||
测试签名验证、Agent 原生启动 smoke 和确定性 `.gbagent` 对比。
|
||||
测试签名验证、Agent CLI 加载 smoke 和确定性 `.gbagent` 对比。所有目标还验证
|
||||
Koffi 原生模块加载与 OpenCode `--version`;Linux job 继续运行 Agent 原生回归。
|
||||
|
||||
Darwin arm64 的 Node 和 OpenCode 固定输入分别由同一份
|
||||
`agent-runtime-lock.json` 与 `remote-runtime-lock.json` 提供,不再维护 CI 专用副本。
|
||||
组包器区分 ELF 与 Mach-O,Darwin CI 产物命名为
|
||||
`goodbuddy-agent-<version>-darwin-arm64.gbagent`,不构建 Intel Mac 目标。
|
||||
macOS Host 继续通过 SSH 按需启动 detached Agent,不注册 launchd 或开机服务。
|
||||
进程身份使用 macOS 原生接口,Unix Socket 使用 `getpeereid`;Host 下载兼容系统自带
|
||||
`shasum -a 256`,不要求安装 GNU coreutils。真实 Host 验证记录见
|
||||
[远程主机技术设计](./docs/features/remote-host/technical-design.md)。
|
||||
|
||||
三个目标共用 `agent-catalog.json` 和现有签名格式,条目按平台与架构区分。旧版 Desktop
|
||||
只接受 Linux catalog 条目;首次发布包含 Darwin 的累计 catalog 前必须先发布新版
|
||||
Desktop,并在发布说明中要求用户升级 Desktop。旧版的在线 Agent 目录检查会失败,
|
||||
本地已验证包不被删除。不得为此改写历史包或复用发布标签。
|
||||
|
||||
该验证 workflow 不读取 production signing secret,不修改公开 key registry,也不上传
|
||||
可安装 Agent 工件。`.github/workflows/agent-release.yml` 是唯一 production Agent
|
||||
发布路径:只接受指向受保护 `main` 历史的 annotated
|
||||
`agent-v${agent-runtime-lock.agentVersion}` 标签,在 `agent-signing` Environment 中
|
||||
原生构建两种架构,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
|
||||
原生构建 Linux x64、Linux arm64、macOS arm64 三个目标,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
|
||||
与北京 OSS。该 Environment 只保护一组 GoodBuddy 通用发布变量与 Secret:
|
||||
|
||||
- `GOODBUDDY_SIGNING_KEY_ID` /
|
||||
@@ -272,7 +311,7 @@ OSS 先写 `agent-releases/v<version>/` 不可变对象,GitHub Release 公开
|
||||
|
||||
### 远程 OpenCode Runtime 基础
|
||||
|
||||
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.9 的 Linux x64
|
||||
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.29 的 Linux x64
|
||||
baseline 与 arm64 官方包 integrity、`bin/opencode` 入口和固定 `acp` 参数。它不同于
|
||||
`agent-runtime-lock.json`,后者锁定 Agent 自带 Node 以及用于 Linux `SO_PEERCRED`
|
||||
的 Koffi 版本。Agent bundle 将 Koffi loader 作为 external module,并只携带目标
|
||||
@@ -296,10 +335,11 @@ 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。
|
||||
manifest/Ed25519/payload/ELF/lock 校验、digest registry、ACP v3、直接进程 ownership 和
|
||||
`runtime/model-bridge` v1。Agent 通过私有 Unix socket 按需 detached 启动,不依赖
|
||||
systemd、D-Bus、Linger 或 bubblewrap。Ask 与 Execute 都直接启动签名 Runtime;两者的
|
||||
差异由 OpenCode Ask 权限配置和 Agent 工具权限分发边界共同执行,Execute 不注入该
|
||||
权限覆盖。
|
||||
|
||||
OpenCode 通过已签名 Agent helper 和每次 Prompt 的私有 Unix socket 使用 Main-only 模型
|
||||
网关;Provider URL、API Key 和真实 Provider 认证头不进入远端。helper 的 loopback
|
||||
@@ -308,7 +348,7 @@ HTTP 入口使用每个进程随机生成的路径 capability,Anthropic/OpenAI
|
||||
配置必须禁用 title Agent;工具循环只使用 build Agent 的模型轮次。
|
||||
|
||||
Ask 模式的 ACP 权限中介只可为原生 `read` 选择 `allow_once`;其他工具种类和
|
||||
`allow_always` 请求必须拒绝,Workspace 的真正只读边界仍由 `bwrap` 执行。Unix 模型桥
|
||||
`allow_always` 请求必须拒绝。Unix 模型桥
|
||||
broker 必须按 socket 当前已缓冲字节增量读取长度帧,不能等待 `read(remaining)` 一次返回
|
||||
完整大响应。Agent 传输或权限实现变化后,除了普通测试,还要在原生 Linux 上运行包含
|
||||
至少 256 KiB 响应的模型桥回归。
|
||||
@@ -358,8 +398,8 @@ DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
|
||||
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
|
||||
```
|
||||
|
||||
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
|
||||
ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
|
||||
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
|
||||
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
|
||||
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
|
||||
@@ -369,12 +409,34 @@ ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
打包矩阵,避免随后推送版本标签时对同一提交重复完整打包。手动触发会运行
|
||||
验证和六平台打包,并保留 30 天 Actions artifacts,但不会创建 Release。
|
||||
|
||||
### Desktop 发布顺序(强制)
|
||||
|
||||
Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段,不能在同一批操作中先后
|
||||
推送 `main` 和标签:
|
||||
|
||||
1. 完成源码、版本、双语 Release Notes 与相关文档修改,运行 Release Notes 校验、
|
||||
`npm test`、`npm run typecheck` 和 `npm run lint`。常规发布准备不在本机运行
|
||||
production build、打包或安装包探针。
|
||||
2. 提交候选,确认用户批准的准确 commit SHA 与双语 Release Notes,fetch 两个远端后,
|
||||
将该候选分别推送到 `origin/main` 和 `github/main`。
|
||||
3. 等待这个准确 SHA 的 `main` 分支 `Cross-platform packages` workflow 完成。只有
|
||||
`Run validators` 和 `Build production bundle` 都成功,候选才具备打标签条件。
|
||||
4. 如果 `main` workflow 失败,不得创建标签。修复必须形成新的 commit,重新完成本地
|
||||
校验、双远端 `main` 推送,并等待新 SHA 的 `main` workflow 成功;旧 SHA 的成功结果
|
||||
不能替代新 SHA 的验证。
|
||||
5. `main` 成功后再次 fetch 两个远端,确认工作区干净、两个远端 `main` 都等于获批 SHA、
|
||||
目标标签在本地和远端均未使用。此时才可在该 SHA 创建 annotated tag,并把标签分别
|
||||
推送到两个远端,随后等待六平台打包和正式发布完成。
|
||||
|
||||
不可移动、删除、重建或复用失败的发布标签。完整审批、恢复和发布后检查以
|
||||
[发布 Runbook](./docs/development/release-runbook.md) 为准。
|
||||
|
||||
推送 `v${package.version}` 标签时,工作流运行验证和六平台打包。只有在
|
||||
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
|
||||
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流通过
|
||||
GitHub OIDC 获取短期 STS 凭据,将发布资产和 `site-release.json` 上传到
|
||||
北京 OSS 的不可变版本目录,并公开校验 14 个安装包。验证通过后才创建或
|
||||
更新 draft GitHub Release、上传 22 个 Release 资产并正式发布,最后原子
|
||||
北京 OSS 的不可变版本目录,并公开校验 12 个安装包。验证通过后才创建或
|
||||
更新 draft GitHub Release、上传 20 个 Release 资产并正式发布,最后原子
|
||||
切换官网 `latest.json`。任一步失败都不会提前切换官网最新版本。
|
||||
|
||||
同一标签重跑时,工作流会根据 `resources/release-notes.json` 重新生成并
|
||||
@@ -395,6 +457,13 @@ git tag -a "$agent_tag" -m "GoodBuddy Agent $(node -p "require('./agent-runtime-
|
||||
OSS 签名目录指针。生产发布需要 `agent-signing` 与 `aliyun-oss-release` 两个受保护
|
||||
Environment;普通分支/PR 的 `agents.yml` 不使用其中任何 Secret。
|
||||
|
||||
Agent 标签也必须遵循与 Desktop 相同的 pre-tag 顺序。更新 Agent 生产源码或
|
||||
`agentVersion` 后,先把候选提交推送到两个 `main`,等待该准确 SHA 的
|
||||
`GoodBuddy Agents` main workflow 全部成功,再次 fetch 并确认两个远端 `main`、干净
|
||||
工作区和未使用标签,最后才可创建和推送 annotated `agent-v<version>`。不得把 Agent
|
||||
标签与候选 `main` 放在同一批推送中;任何修正 commit 都必须重新等待新 SHA 的 Agent
|
||||
main workflow,旧 SHA 的成功结果不能复用。
|
||||
|
||||
中英文发布说明统一维护在 `resources/release-notes.json`。新版本按“本次
|
||||
亮点 / Highlights”“功能更新 / Features”“问题修复 / Bug Fixes”“使用前
|
||||
请留意 / Before You Start”四段组织,应用首次启动弹窗与 GitHub Release
|
||||
@@ -414,7 +483,7 @@ git push github "$tag"
|
||||
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
|
||||
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
|
||||
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
|
||||
DMG 和 ZIP,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
|
||||
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
|
||||
任务失败,不能静默降级为未签名包。
|
||||
|
||||
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
|
||||
@@ -505,36 +574,3 @@ npm test -- src/main/agent/deepseek-harness-acp-e2e.test.ts -t "rejects a real n
|
||||
|
||||
如需覆盖发布包内置 npm 路径,再设置 `GOODBUDDY_DSH_NPM_CLI` 与
|
||||
`GOODBUDDY_DSH_NODE_EXECUTABLE` 指向已解包应用中的 npm CLI 和应用主程序。
|
||||
|
||||
## Renderer bundle 性能门禁
|
||||
|
||||
`electron-vite` 会在 `out/renderer/.vite` 生成 Vite manifest 和仅含构建模块
|
||||
归属的 module manifest。`npm run build:bundle` 在 bundle 完成后自动运行
|
||||
`build/check-renderer-bundle.cjs`,去重统计首屏同步闭包及 Knowledge、知识图谱、
|
||||
Activity、Magic Notes、Settings 动态入口同步加载的 JS 与 CSS 合计 raw / gzip
|
||||
大小并执行预算校验。
|
||||
|
||||
门禁同时验证以下结构约束:
|
||||
|
||||
- Knowledge、Activity 与 G6 不得进入首屏同步闭包。
|
||||
- `KnowledgeGraphChart` 与 G6 不得进入 Knowledge shell 的同步闭包。
|
||||
- G6 必须由知识图谱动态入口同步拥有。
|
||||
|
||||
路径遍历使用 manifest 中的相对文件名并通过 Node `path.resolve` 读取,因此兼容
|
||||
Windows 与 POSIX 构建输出。构建专用 module manifest 只记录项目相对路径、
|
||||
`node_modules/` 相对路径或稳定的虚拟模块名,不记录 Runner 的盘符、主目录或
|
||||
绝对路径;检查成功后会删除该诊断文件,检查失败时保留以便排查。标准 Vite
|
||||
manifest 会保留在输出中。预算以干净生产构建为基线并保留有限余量;若业务确需
|
||||
提高预算,必须先检查 manifest 闭包和产物差异,不能只为通过 CI 调大数值。
|
||||
|
||||
Bundle 大小是发现意外依赖和持续膨胀的粗粒度工程护栏,不是用户体验指标,也不能替代
|
||||
Electron 中的实际交互验证。Settings 的硬上限以 2026-08-23 验证构建
|
||||
`648.78 kB raw / 97.79 kB gzip` 为基线,增加约 15% 评审余量后取整为
|
||||
`750 kB raw / 115 kB gzip`。该上限用于拦截显著增长,不要求实现长期贴线运行。
|
||||
|
||||
调整 Renderer 拆包时遵守以下优先级:
|
||||
|
||||
1. 同级页签的交互一致性、首个绘制帧内反馈和输入保留优先于包体数字。
|
||||
2. 不得仅为通过预算,把一个原本轻量的同级页签单独改成首次点击才加载。
|
||||
3. 确需动态加载重型功能时,必须提供非空、无布局跳变的局部状态,并增加首次交互测试。
|
||||
4. 修改预算必须记录干净生产构建基线、增量闭包差异和 Electron 实测结果。
|
||||
|
||||
+675
-84
@@ -1,104 +1,695 @@
|
||||
# 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] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,并显示对应目录或来源。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式和受控 Mermaid 图表;大图可缩放、拖动或查看源码,失败时保留原始图表代码。
|
||||
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
|
||||
- [ ] **通用助手工作栏与执行空间**(规划中):保留 Task Center 作为 Task 的单例索引,并把监督、Runtime、终端、进程、工作区、浏览器和成果作为始终可访问的应用级能力;除 Task Center 外的可绑定能力由用户选择跟随或固定目标,并逐步支持静态安全 HTML 预览、本机/SSH 执行空间和远程 Agent Runtime。附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
|
||||
### Desktop foundation, workspaces, and context
|
||||
|
||||
### Agent Runtime 与模型连接
|
||||
- [x] **Cross-platform desktop application**: Supports Windows, macOS, and
|
||||
Linux release targets on `x64` and `arm64`.
|
||||
- [x] **Chinese hardware and operating-system coverage**: Standard Linux
|
||||
packages target compatible x64 and arm64 environments, including UOS, Kylin,
|
||||
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
|
||||
LoongArch has a separate experimental loong64 preview outside standard
|
||||
releases and automatic updates; no preview is planned for 0.12.12. See the
|
||||
[preview boundaries](./docs/development/loongarch-preview-build.md).
|
||||
- [x] **Configurable global shortcut**: Enable, disable, or record an Electron
|
||||
accelerator under Platform Features / General. The default remains
|
||||
`CommandOrControl+Shift+Space`; if registration conflicts or saving fails,
|
||||
GoodBuddy keeps the previously registered shortcuts and shows an actionable
|
||||
status.
|
||||
- [x] **Projects, independent conversations, and conversation branches**:
|
||||
Isolates context by project and manages conversations, attachments, and Git
|
||||
workspace changes. A stable local conversation can be copied into an
|
||||
independent branch; the branch retains a source badge but does not copy
|
||||
Tasks, queues, or artifact ownership. The project selector distinguishes
|
||||
local, managed SSH, and remote messaging-channel projects. Managed SSH
|
||||
projects are grouped by Host, with the real Agent connection state on the
|
||||
Host heading and only the remote path on each project row.
|
||||
- [x] **File, screenshot, window, and clipboard context**: Added to model
|
||||
context only after explicit user selection.
|
||||
- [x] **Per-file workspace diffs**: Changed files expose separate staged and
|
||||
unstaged diffs, including deleted, renamed, and untracked files. Refresh
|
||||
reloads the selected diff, browsed directory, and expanded directories; more than 50 changes
|
||||
remain accessible through incremental loading.
|
||||
- [x] **Workspace file and Git management**: Create, rename, move, and delete
|
||||
files or directories, inspect properties, search and switch local or remote
|
||||
branches, create branches, explicitly Fetch, and browse commit history and
|
||||
per-file commit diffs. File and Git controls are grouped by view, branch
|
||||
search uses the shared compact form style, and subdirectories retain text
|
||||
breadcrumbs without an isolated root icon. Commit history stays docked at
|
||||
the bottom, initially expands to half the Git content area, and supports
|
||||
resizing with independently scrolling panes and one shared refresh action.
|
||||
Project switches automatically detect Git without reusing another project's
|
||||
status. Moves stay inside the workspace,
|
||||
deletion requires confirmation, and branch conflicts never trigger automatic
|
||||
stash or discard. Managed SSH operations require Agent `0.11.23`.
|
||||
- [x] **Rich responses**: Supports GitHub Flavored Markdown, LaTeX math,
|
||||
constrained Mermaid diagrams, and static in-conversation HTML previews.
|
||||
Complete HTML and HTML code blocks can be previewed in place after an Agent
|
||||
reply finishes, with icon actions to reveal the source or open a full-screen
|
||||
preview. The feature is on by default and can be disabled under Platform
|
||||
Features / General; previews cannot run scripts, access the network, submit
|
||||
forms, or open windows.
|
||||
- [x] **AI response and full-conversation copy**: Completed AI responses expose
|
||||
a bottom action that copies Markdown source without reasoning, tool logs, or
|
||||
citation metadata. Full-conversation copy uses the same validated
|
||||
Preload/Main clipboard path.
|
||||
- [x] **Assistant workbar, multiple terminals, and resizable layouts**: The
|
||||
right workbar uses a persistent “+” capability catalog and application tabs.
|
||||
Task Center and Workspace are permanent singleton tabs; browser tabs are
|
||||
independent instances. Task Center can show the current project, global
|
||||
tasks, or all projects, and retains the selected scope with the layout.
|
||||
Users can open multiple independent terminals for the current local or
|
||||
managed SSH project with bounded output, resizing, termination, and explicit
|
||||
reconnection. Closing a terminal tab ends its Shell; restarting the app
|
||||
restores only an ended tab description and never restarts the Shell. The
|
||||
main sidebar and Magic Notes list support pointer and keyboard resizing;
|
||||
the notes list can be hidden, remembers its layout, and stacks at narrow
|
||||
widths.
|
||||
- [ ] **Project Agent Space** (planned): Unifies roles, knowledge, Skills/MCP,
|
||||
models, approval policy, budgets, and timeouts in a Project, with reusable
|
||||
templates.
|
||||
- [ ] **Application navigation and system-tool shortcuts** (design only):
|
||||
Plans fixed system entries, configurable built-in application visibility,
|
||||
and footer shortcuts into the existing workbar. These are not implemented
|
||||
interface changes. See the [design](./docs/features/application-tool-navigation/README.md).
|
||||
- [ ] **Additional assistant workbar and execution-space capabilities**
|
||||
(planned): Builds on the current workbar and multiple terminals with
|
||||
supervision, unified Runtime monitoring, managed processes, safe static HTML
|
||||
preview, target-pinnable workspace/browser/artifact instances, bottom
|
||||
docking, and separate windows. Task Center remains the singleton Task index;
|
||||
attachments and knowledge remain in the conversation composer, while memory,
|
||||
when implemented, and historical execution context belong to the associated Task. See
|
||||
the [Feature PRD](./docs/features/assistant-workbar/prd.md).
|
||||
- [ ] **ShareServer Office co-editing** (design): Uses an optional ShareServer
|
||||
integration with ONLYOFFICE Docs to open multiple filename-labelled document
|
||||
tabs in the assistant workbar. The planned scope covers DOCX, XLSX, and PPTX
|
||||
manual editing, save and undo, source-file conflict protection, and later AI
|
||||
selection edits. The selected ShareServer and editor process document content;
|
||||
Desktop neither bundles nor starts Document Server and does not claim offline
|
||||
editing when no service is configured. See the
|
||||
[design](./docs/features/office-document-editing/README.md).
|
||||
|
||||
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行和图像生成。
|
||||
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、总执行时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。
|
||||
- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响任何本地项目、普通桌面功能或桌面发布。启用后可管理固定 Host Key 的 SSH Host、浏览有界远端目录并创建 Ask/Execute 项目;Ask 在 Runtime 边界通过 bubblewrap 保持 Workspace 只读,Execute 使用所选 SSH 账号的完整权限。Main 通过签名 Agent、私有 Unix socket、ACP v3、持久恢复 cursor 和 Main-only 模型桥维持长任务、重连与会话恢复,模型凭据不进入远端。远端组件不嵌入桌面包,项目切换只更新本地选择且不会连接或下载;设置页只读取小型签名目录并按 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 可并行运行多个项目和对话;活动请求沿精确 resume/replay 恢复,controller 换代遗留的空闲 binding 只在 takeover 已证明且没有活动请求时换新 channel epoch 并恢复原 ACP session。Windows 到 Linux x64 的安装、provider-free 模型桥、断线重放、Session 续接和零残留清理已完成真实 Host 验证;签名 Agent 0.11.2-e2e.12 与 OpenCode Runtime 1.18.9 已完成一次只提交一遍、只调用一个 Ask 原生 read、无标题轮次且精确返回证据内容的有界真实模型验收,当前源码 lock 已更新为 Agent 0.11.9。
|
||||
- [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/architecture/remote-host-environment-provisioning-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] **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;并发外部 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 聚合状态、取消入口和审计归属。
|
||||
### Agent Runtimes and model connections
|
||||
|
||||
### Skills、MCP 与知识库
|
||||
- [x] **Direct model Runtime**: Supports question answering, knowledge
|
||||
synthesis, controlled tool execution, image generation, and reference-image
|
||||
editing through providers supporting the OpenAI-compatible image-editing
|
||||
endpoint. Editing sends attached images as multipart data and still requires
|
||||
validated inline image output rather than fetching provider-returned URLs.
|
||||
Follow-up image requests reuse the latest successful image and conversation
|
||||
text, including after reopening a saved conversation; explicit attachments
|
||||
take priority. Missing editing support does not block text-based generation:
|
||||
the affected reply shows a quiet footer explaining that reference images were
|
||||
not used. See [image generation](./docs/features/image-generation/README.md).
|
||||
- [x] **Direct model programming agent**: Local direct text models can run the
|
||||
platform Shell in Execute mode and delegate one level of programming
|
||||
Subagent work while inheriting the parent request's mode, model, workspace,
|
||||
and capability scope. OpenCode, Continue, DeepSeek Harness, and managed SSH
|
||||
do not receive duplicate copies of these tools. The Windows local command
|
||||
path and a real-model edit, test, fix, and review loop have passed; native
|
||||
macOS and Linux command validation remains. Execute commands can select
|
||||
absolute, relative, or symbolic-link directories outside the workspace;
|
||||
the workspace stays the default and relative-path base. Ask remains read-only.
|
||||
- [x] **OpenCode and Continue**: Use isolated child processes, an environment
|
||||
variable allowlist, unified configuration, cancellation, startup and control-request
|
||||
deadlines, bounded streaming output, and activity records. Shared process
|
||||
cleanup preserves complete Windows process-tree termination and terminates
|
||||
POSIX process groups when a child uses an independent process group. Chat
|
||||
status checks use a one-shot Runtime probe that is immediately cleaned up
|
||||
rather than entering the model-and-project execution cache. For managed
|
||||
local OpenCode, this passive check validates the selected path and model
|
||||
credential without starting a throwaway server; the explicit settings test,
|
||||
native inventory still performs full startup and health checks, then shares
|
||||
that execution Runtime with the first real request instead of cold-starting
|
||||
another server. Execution Runtimes remain reusable per project, while local
|
||||
OpenCode configuration dependencies and content-addressed Skill snapshots are
|
||||
shared globally across projects. Session data, tool output, and request-scoped
|
||||
MCP state remain isolated. GoodBuddy-managed local OpenCode keeps
|
||||
requests in the same conversation ordered, while different conversations in
|
||||
the same project and different projects can run in parallel. Request-scoped
|
||||
dynamic MCP tools remain isolated through default wildcard disablement and
|
||||
explicit current-request enablement. Managed local and remote OpenCode skip
|
||||
unused automatic Git snapshots to avoid synchronous diff stalls; file tools,
|
||||
subagents, and workspace Git diffs remain available. Continue continuously
|
||||
drains unused utility-host stdout so console output cannot fill its pipe.
|
||||
These changes apply to newly started Runtimes, not external OpenCode Servers.
|
||||
OpenCode and Continue runs do not stop
|
||||
at a fixed tool-call or activity count, and every observed tool and Subagent
|
||||
activity remains in the local conversation instead of being discarded while
|
||||
the native task continues.
|
||||
Interactive questions are answered only by foreground conversations; scheduled tasks, remote channels,
|
||||
delegated work, and other background runs fail immediately with guidance to
|
||||
rerun in the foreground
|
||||
instead of waiting indefinitely. Local OpenCode startup uses the selected
|
||||
absolute path directly without checking the file or running `--version`
|
||||
first; invalid paths fail at actual launch. On managed Linux ARM Hosts,
|
||||
Runtime activation reuses the registry and manifest verified during setup
|
||||
instead of rehashing or rechecking the complete OpenCode binary.
|
||||
- [x] **Managed SSH OpenCode loop (technical preview)**: Controlled by the
|
||||
separate Remote Projects (Technical Preview) tab under Settings / Platform
|
||||
Features and disabled by default. Disabling it does not affect local
|
||||
projects, ordinary desktop capabilities, or desktop releases. When enabled,
|
||||
users can manage SSH Hosts with pinned Host Keys, browse bounded remote
|
||||
directories, and create Ask or Execute projects. Both modes start the
|
||||
signed Runtime directly without bubblewrap. Ask applies OpenCode's Ask
|
||||
permission configuration and permits only native read approvals at the
|
||||
Agent tool-dispatch boundary, while Execute explicitly allows native permissions
|
||||
and retains all permissions of the selected SSH account. The Agent owns accepted Prompts,
|
||||
provider/tool rounds, Runtime processes, a stable model ledger, and a bounded
|
||||
semantic transcript over a private Unix socket and ACP v5. Work continues on
|
||||
the Host after Desktop exit, network loss, or local-process termination.
|
||||
Reconnection attaches only to the original controller, binding, and
|
||||
operation, then atomically merges provenance into the original conversation
|
||||
without resending the Prompt. Model profiles and API keys enter Agent memory
|
||||
only for the accepted operation and are not written to the Renderer, SSH
|
||||
arguments, remote environment, or disk. Remote components are not embedded
|
||||
in the desktop package; switching projects updates only local selection and
|
||||
does not connect or download. Settings reads a small signed catalog and
|
||||
reports the local version, latest compatible online version, and update
|
||||
state for Linux x64, Linux arm64, and macOS arm64. Intel Macs are not supported.
|
||||
Downloads occur only after a user action, and
|
||||
packages can also be imported or exported offline. Each compound `.gbagent`
|
||||
package contains the Agent, fixed Node, and the compatible OpenCode Runtime
|
||||
maintained by the desktop source. Online sources follow the GitHub/Beijing
|
||||
OSS choice in About and Updates. The cumulative signed catalog binds minimum
|
||||
Desktop version, Agent protocol, platform, architecture, size, SHA-256, and fixed URL.
|
||||
All three targets share one catalog. Users must upgrade Desktop before the
|
||||
first mixed-platform catalog is published because older Linux-only readers
|
||||
reject catalogs containing Darwin entries.
|
||||
The public-key registry accepts equivalent JSON whitespace and line endings
|
||||
while still strictly validating schema, Ed25519 keys, environment, and
|
||||
revocation; catalog, package, manifest, payload signatures, and streaming
|
||||
SHA-256 remain unchanged. A missing architecture package disables managed
|
||||
SSH only for that architecture. Projects store only Host, remote path,
|
||||
Runtime selection, and mode. Current Host identity is read when a
|
||||
Workspace/Runtime is first used, then reused by other projects in the same
|
||||
process. Managed SSH conversations expose only supported OpenCode choices.
|
||||
Multiple projects and conversations can run concurrently on one Host;
|
||||
Agent-owned prompts are not stopped at a fixed model-call count or
|
||||
prompt-wide output-token total, and Runtime output uses transport
|
||||
backpressure instead of cancellation when GoodBuddy buffers fill;
|
||||
recovery separately reports network, Agent, Runtime, committed-event cursor,
|
||||
completion, or failure/retry and blocks only the affected project. A real
|
||||
Linux x64 Host has covered short and long detach, forced local harness exit,
|
||||
concurrency, SSH relay loss, cancellation, definite and uncertain provider
|
||||
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
|
||||
database. Successful tool START/END events appear exactly once, with no
|
||||
Prompt, provider, or tool replay observed. The current Agent source lock is
|
||||
`0.11.23`, while the current Desktop release candidate is `0.12.12`; formal
|
||||
publication status follows the separate Agent and Desktop
|
||||
release channels. Current macOS source has passed native package installation,
|
||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||
separate process groups on a real Host; this does not imply publication.
|
||||
Agent `0.11.23` adds remote workspace management and optional model limits;
|
||||
its bundled Runtime no longer imposes a fixed ten-minute Prompt deadline.
|
||||
Unlimited request duration retains a separate connection timeout. Upgrade
|
||||
Desktop to `0.12.11` before downloading and updating the Host's Agent.
|
||||
- [x] **Manual SSH Host environment provisioning source path**: After Host Key,
|
||||
authentication, and system probes succeed, GoodBuddy saves the Host and
|
||||
read-only probes the shared Agent/Runtime. Saving a Host or opening a project
|
||||
never installs automatically. A Host card has one primary action based on
|
||||
version facts, Install Remote Environment, Update Remote Environment, or
|
||||
Reinstall, plus a secondary SegmentedControl for Auto, Host Download, or
|
||||
GoodBuddy Transfer. The selection defaults to Auto and is not persisted; a
|
||||
Version Matched badge does not mean the environment is healthy. Auto probes
|
||||
only before operation preparation and chooses one acquisition route.
|
||||
Explicit choices remain in effect, and prepare, commit, or adoption failures
|
||||
do not silently fall back across acquisition routes. Both routes deliver the
|
||||
same signed compound `.gbagent` into fixed staging and share control-plane
|
||||
prepare, commit, Agent activation/health, Runtime activation, finalize, and
|
||||
explicit cleanup. The GoodBuddy route can download and validate a missing
|
||||
candidate during the same operation, cache it with a lease, and stream one
|
||||
archive plus its verified bootstrap Node over bounded SFTP without loading
|
||||
the roughly 294 MiB package into a Main-process `Buffer`; the Host performs
|
||||
one complete payload verification while extracting. An unfinished operation
|
||||
stores only the operation ID needed to clean staging. A later update makes a
|
||||
best-effort cleanup and starts prepare again, without persisting a remote
|
||||
metadata copy or allowing cleanup failure to block a new update or roll back
|
||||
a healthy environment. Existing projects resolve current Host identity on
|
||||
demand and run a fixed `attach-or-bootstrap`; registered health,
|
||||
capabilities, and prompt startup do not scan the full payload. See the
|
||||
[design](./docs/features/remote-host/environment-provisioning-technical-design.md).
|
||||
- [ ] **Real-Host acceptance for SSH Host environment provisioning**: The
|
||||
current source passed isolated Linux x64 package installation, Ask/Execute,
|
||||
native subagent external writes, reconnection, and stop/bootstrap.
|
||||
The complete Host-card acquisition matrix across GitHub, Beijing mirror,
|
||||
Linux x64/arm64, cancellation, and offline GoodBuddy transfer remains.
|
||||
Candidate CI/native packaging and system sleep/wake are not yet verified;
|
||||
this partial development evidence does not imply publication.
|
||||
- [x] **DeepSeek Harness (preview)**: Uses the fixed GoodBuddy Host and an
|
||||
OpenAI-compatible model connection. It prefers an administrator-provided
|
||||
connection, otherwise follows the compatible default model or first
|
||||
compatible connection without requiring a duplicate selection. Settings
|
||||
displays the actual administrator or fallback model source. Ask permits
|
||||
only real Host-registered `read` and `skill` tools plus Main-managed Web
|
||||
Search/Fetch proxies, and rejects plugin impersonation of those names.
|
||||
Execute allows all enabled built-in and plugin tools with the current user's
|
||||
permissions. Image input follows the selected model connection's declared
|
||||
capability: text models reject images before Host or model invocation, while
|
||||
image-capable models receive bounded inline JPEG/PNG content through a
|
||||
temporary Attachment Store.
|
||||
- [x] **DSH npm plugin marketplace**: Disabled by default and searches public
|
||||
npm `dsh-plugin` packages only after explicit user enablement. It uses the
|
||||
bundled npm to install exact versions with ordinary lifecycle scripts and
|
||||
supports enable/disable, JSON configuration, removal, automatic disablement
|
||||
after startup failure, and offline management of installed plugins.
|
||||
Disabling the marketplace hides only the catalog and management interface;
|
||||
it does not change the enabled state of installed plugins, and third-party
|
||||
code is not subject to Ask initialization isolation.
|
||||
- [x] **Ask and Execute work modes**: Ask remains read-only. Execute is the
|
||||
user's authorization for all tools, processes, network access, and writable
|
||||
paths available to the current local or SSH account, including paths outside
|
||||
the workspace and native subagent work.
|
||||
- [x] **Efficient direct-model workspace tools**: Direct models use bundled
|
||||
ripgrep for compact file discovery and content search, read large UTF-8 files
|
||||
by line, and apply multi-file patches in Execute. Ask can search and read but
|
||||
cannot patch or run commands; packaged ripgrep does not require a system install.
|
||||
- [x] **Long context and paged tool output**: Message history, long replies,
|
||||
and parsed document text no longer use the previous fixed truncation limits.
|
||||
Direct models can continue reading stored command and Subagent output in
|
||||
pages; the selected model's context window and bounded transports still apply.
|
||||
- [x] **Native Runtime interaction routing**: OpenCode and Continue questions
|
||||
support choices, yes/no, free-text answers, and skipping through the existing
|
||||
question card. OpenCode also routes questions from owned child sessions.
|
||||
Successful answers and skips retain the original questions and answers
|
||||
across rounds and local conversation reloads; answers discarded by older
|
||||
versions cannot be recovered.
|
||||
Execute permission confirmations are handled automatically rather than
|
||||
waiting for another approval. See the [interaction boundaries](./docs/features/assistant-workbar/runtime-interactions.md).
|
||||
- [x] **Experts and Subagents**: Supports explicit experts, team analysis, and
|
||||
up to three experts running in parallel. Experts inherit the parent
|
||||
Ask/Execute mode and can use enabled local direct-model tools; Ask remains
|
||||
read-only. They are not remote OpenCode child sessions. Chat shows each
|
||||
expandable full expert response first and the main Agent's synthesis below,
|
||||
and persists both with the conversation.
|
||||
- [x] **OpenCode child progress and final results**: Child cards show ordered
|
||||
text, reasoning, and tool progress separately from the final result. Remote
|
||||
live progress requires Agent `0.11.20`; older packages still expose final
|
||||
results without reconstructing missing progress.
|
||||
- [x] **OpenCode event-stream cleanup**: Each completed or cancelled request
|
||||
closes its own subscription without cancelling parallel conversations.
|
||||
- [x] **Accurate message-footer status**: Distinguishes request preparation,
|
||||
retry waiting, retry dispatch, tool activity, pending answers, and terminal
|
||||
states. Local OpenCode reports native retry attempts and scheduled times;
|
||||
direct models report their own backoff phase. Footer dots are static, and
|
||||
unsupported Runtime retry details are not invented. Conversation-list
|
||||
indicators and send/stop behavior remain unchanged.
|
||||
- [x] **Simplified Runtime selection**: Conversation and project menus no
|
||||
longer enumerate every Runtime/model combination. Configure Runtime models
|
||||
in system settings; saved fixed project selections remain usable and
|
||||
explicitly display their fixed model.
|
||||
- [x] **Role-bound model connections**: Each role can inherit the default model
|
||||
or select an independent text-model connection. Invalid connections safely
|
||||
fall back to the default; the synthesis role always inherits the default.
|
||||
- [x] **Multiple model protocols**: Supports Anthropic Messages, OpenAI
|
||||
Responses, OpenAI Chat Completions, OpenAI Images, and unauthenticated local
|
||||
models. New users receive a local Ollama-compatible default and no
|
||||
preconfigured third-party cloud provider. During upgrades, GoodBuddy
|
||||
replaces a historical default only when it has never received credentials
|
||||
and still exactly matches the old built-in value; explicitly saved
|
||||
connections and encrypted credentials remain unchanged. Existing
|
||||
deployments that supply credentials only through compatible environment
|
||||
variables retain their historical connection parameters, while generic
|
||||
model environment variables still take precedence. Save and Test Model sends
|
||||
a bounded real text or image-generation request and validates the generated
|
||||
result instead of testing HTTP reachability alone, so it may incur a small
|
||||
provider usage charge.
|
||||
- [x] **Custom model request fields**: Each LLM connection accepts bounded
|
||||
JSON objects for additional request headers and top-level body fields.
|
||||
Direct requests and Continue support both; local OpenCode and DeepSeek
|
||||
Harness use only their natively supported headers, while the managed SSH
|
||||
OpenCode model gateway supports both. Runtime, protocol, and authentication
|
||||
fields take precedence, and these ordinary connection settings must not be
|
||||
used to store API keys or other secrets. See
|
||||
[model request customization](./docs/features/model-connections/README.md).
|
||||
- [x] **Context usage and automatic compaction**: Direct models update usage
|
||||
from each successful provider call. Images and tool rounds use the same
|
||||
accounting, with estimation only when the provider omits usage. The UI
|
||||
distinguishes This Model Call from Post-Compaction Conversation Estimate,
|
||||
and the compaction line is recalculated from current settings and the
|
||||
selected model window rather than storing old configuration per
|
||||
conversation. Before/after compaction markers use one estimation method,
|
||||
while activity history retains provider usage for each call. Conversations
|
||||
and multi-round tool Agents can compact repeatedly after a completed call
|
||||
crosses the threshold, reserving budget first for fixed prompts, tool
|
||||
definitions, and summaries. One response separately preserves compaction
|
||||
markers for Agent tool context and conversation history, and summaries remain
|
||||
reusable after app restart or when older messages leave the local history
|
||||
window.
|
||||
- [x] **Persistent credential protection**: Main encrypts API keys with secure
|
||||
system storage and never exposes them to the Renderer. A managed SSH accepted
|
||||
Prompt places the current profile and key in Agent memory only, never in SSH
|
||||
arguments, the remote environment, or disk; other paths keep credentials
|
||||
inside Main. Credentials remain attached to their model connection when the
|
||||
service URL changes or authentication is temporarily disabled, and are
|
||||
removed only when the user explicitly clears them or deletes the connection.
|
||||
- [x] **Bounded failure diagnostics**: Desktop stores a fixed allowlist of
|
||||
startup, Runtime, and remote-connection failure stages in the user-data
|
||||
directory, rotating at four 256 KiB files. Each GoodBuddy Agent rotates
|
||||
daemon, connection, recovery, and Runtime lifecycle diagnostics at three
|
||||
64 KiB files in its private installation state and exposes them through the
|
||||
fixed `diagnostics --installation-id` command. Both sides store only
|
||||
allowlisted stages, stable error codes/types, and fixed short messages, never
|
||||
Prompts, credentials, file contents, paths, environments, SSH arguments, or
|
||||
raw provider responses. Diagnostic-write failure does not alter normal
|
||||
operation.
|
||||
- [x] **OpenCode Runtime customization**: GoodBuddy-managed OpenCode can
|
||||
discover native Agents, Tools, Commands, LSPs, Formatters, MCP, Skills,
|
||||
Prompts, and Resources. Tools show read, file-edit, command, network, Agent
|
||||
orchestration, and other types, source, and Ask/Execute availability while
|
||||
hiding OpenCode's internal `invalid` tool and temporary GoodBuddy MCP tools.
|
||||
Users can save a default Agent, override it per request, run Commands through
|
||||
the native SDK, view context usage, and invoke native Compact with a total
|
||||
timeout. Questions from concurrent external Server conversations are mapped
|
||||
by public request ID so answers cannot cross conversations. External
|
||||
OpenCode Servers report connection state only and do not claim native
|
||||
catalog access. Arbitrary plugin installation, Session Share, automatic
|
||||
Worktree, and OpenCode native-session persistence remain unavailable.
|
||||
- [x] **Continue Runtime customization**: Exposes native Rules, prompt
|
||||
templates, and MCP from static configuration plus editable GoodBuddy Rules
|
||||
and Prompt presets. Chat can select a preset per request and insert an
|
||||
editable Prompt. The current Continue Host has no trustworthy static native
|
||||
Tool discovery API and runs with an isolated `CONTINUE_GLOBAL_DIR`, so the UI
|
||||
explicitly marks static Tool discovery unsupported and does not present
|
||||
workspace or user Skills that the Host will not load as native capabilities.
|
||||
GoodBuddy-assigned Skills still execute through per-request staging. The
|
||||
temporary Continue Host does not reuse native session compaction; manual
|
||||
compaction uses the GoodBuddy summary model and verifies persisted summary
|
||||
coverage. Agent questions become unified question cards. Resources, Hooks,
|
||||
background Jobs, and Continue native-session management remain deferred.
|
||||
- [x] **Native Runtime catalog semantics**: Native capabilities are displayed
|
||||
across 11 tabs: Agents, Tools, Commands, Skills, MCP, Rules, Prompts,
|
||||
Resources, LSP, Formatters, and Context. Catalog state is independent from
|
||||
Runtime connectivity and distinguishes complete, partial, unavailable,
|
||||
connection-only, and unsupported. DeepSeek Harness enumerates bounded
|
||||
built-in/plugin Tools and Skills through the Host Registry, reports real
|
||||
Ask/Execute boundaries, and excludes per-request GoodBuddy Skills and
|
||||
Web/MCP proxies.
|
||||
- [ ] **Runtime supervision section** (planned): A fixed section in the
|
||||
application-level assistant workbar for Task-level delegation, background
|
||||
execution, Workflows/Hooks, long-running work, and native-session
|
||||
supervision across OpenCode, Continue, and DeepSeek Harness. Users select
|
||||
only Conversation or Task; Job/Run remains internal rather than becoming a
|
||||
tree or independent object.
|
||||
- [ ] **Advanced Subagent supervision** (planned): Task-level aggregation in
|
||||
the workbar's fixed Runtime section, configurable nesting, parallelism,
|
||||
budgets, and lifecycle controls. Basic expert-mode inheritance and
|
||||
single-level direct-model programming delegation are already available.
|
||||
|
||||
- [x] **Skills 按需接入**:可分配给直连模型、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. Managed Python archive validation
|
||||
follows the target filesystem: Linux preserves valid case-distinct paths,
|
||||
while every target still rejects exact duplicates and unsafe entries.
|
||||
- [ ] **Local tool-execution environment release acceptance**: All six
|
||||
platform/architecture OSS mirror objects have passed public byte, size, and
|
||||
SHA-256 verification. Managed Python remains an on-demand download and does
|
||||
not add license files to the Desktop package. Each standard package job uses
|
||||
a native target-architecture Runner and performs a real managed-Python
|
||||
install, SSL, pip, and venv probe before packaging. The immutable `v0.12.0`
|
||||
attempt passed Windows and macOS but exposed Linux TAR case handling before
|
||||
publication. The immutable `v0.12.1` attempt was cancelled before native
|
||||
packaging while synchronizing the Agent release. Published `v0.12.2`
|
||||
subsequently passed all six native package jobs and their managed-Python
|
||||
install probes. Real Skill/MCP execution, custom interpreter execution, and
|
||||
coordination with affected running processes remain
|
||||
acceptance work. Six-platform release acceptance must not be claimed before
|
||||
the applicable checks pass.
|
||||
- [x] **On-demand built-in MCP**: Knowledge, Magic Notes, GoodBuddy
|
||||
configuration, and built-in browser MCP can be enabled independently and assigned to direct
|
||||
models, GoodBuddy-managed OpenCode, and Continue. Settings explicitly marks
|
||||
DeepSeek Harness unsupported. Built-in MCP uses short-lived local authority
|
||||
for the current request, and user configuration cannot loosen Ask/Execute
|
||||
read/write boundaries.
|
||||
- [x] **MCP Tools**: Explicitly enabled custom MCP can be assigned to direct
|
||||
models, GoodBuddy-managed OpenCode, Continue Agent Execute, and DeepSeek
|
||||
Harness, and loads only in Execute. Agent child processes receive only
|
||||
per-request local-loopback authority; MCP addresses, commands, and
|
||||
credentials remain in Main. Dynamic tools pass through discovery and the
|
||||
existing activity and permission boundaries; healthy direct-model
|
||||
and Harness calls reuse the discovered request catalog instead of
|
||||
rediscovering it before every call.
|
||||
- [x] **MCP Prompts and Resource metadata**: MCP testing discovers bounded
|
||||
Prompt, parameter, and Resource metadata only when the Server declares the
|
||||
corresponding capability and does not read Resource content. Supported
|
||||
Prompts can be inserted into the chat draft for further editing. OpenCode can
|
||||
report an experimental Resource catalog; the current Continue version
|
||||
explicitly does not support Resources.
|
||||
- [x] **Local knowledge bases**: Supports file, directory, and web imports,
|
||||
SQLite FTS5 retrieval, and source tracing. Knowledge selection persists per
|
||||
conversation and starts empty for new conversations. Advanced creation and
|
||||
retrieval parameters are collapsed by default. Documents distinguish ready,
|
||||
processing, and failed states, with source opening, retry, and confirmation
|
||||
before source removal.
|
||||
- [ ] **External knowledge-base connections** (planned): Manage Dify, FastGPT,
|
||||
and RAGFlow instances from the existing Knowledge page, then discover and
|
||||
bind remote knowledge bases with provider-specific retrieval settings and
|
||||
citations. External systems provide retrieval only: GoodBuddy does not use
|
||||
their App, Chat, Workflow, or Agent APIs, and does not bulk-sync, locally
|
||||
index, or modify remote content. Bounded cited snippets are retained locally
|
||||
with their conversations. See the
|
||||
[external knowledge-base PRD](./docs/features/knowledge-base/external-knowledge-prd.md).
|
||||
- [x] **Knowledge graph**: Supports rule-based, model-based, and hybrid
|
||||
extraction plus entity, relationship, alias, and evidence maintenance.
|
||||
- [x] **Embedding configuration and retrieval**: Configures compatible
|
||||
Embeddings endpoints and uses them for semantic retrieval.
|
||||
- [x] **Embedding diagnostics and indexing jobs**: Provides real embedding
|
||||
diagnostics, per-document rebuild progress, cancellation, failure state, and
|
||||
result recovery after restart. Each successful document becomes immediately
|
||||
searchable.
|
||||
- [x] **Hybrid retrieval workbench**: Diagnoses full-text, Chinese phrase,
|
||||
vector, and graph channels with configurable Top K, thresholds, weights,
|
||||
local or learned reranking, and context budgets.
|
||||
- [x] **Chunking, maintenance, and evaluation**: Supports fixed, structured,
|
||||
and parent-child chunking, chunk maintenance, cancellable rebuilds, and
|
||||
bilingual retrieval evaluation.
|
||||
- [x] **Controlled knowledge ontology**: Each knowledge base can define
|
||||
entities, relationships, aliases, and endpoint constraints while retaining
|
||||
evidence offsets, confidence, and extraction source, with explicit guidance
|
||||
to rebuild the graph.
|
||||
- [x] **Required retrieval and cited context**: Conversations can retrieve on
|
||||
demand or before every response, show zero-result, degraded, failed, and
|
||||
cancelled states, and expose cited context or safely open its source.
|
||||
- [x] **Magic Notes**: A local-first notes and todo workbench with scope
|
||||
management, editing, filtering, and controlled AI comments. The todo view
|
||||
displays the source entry inside the selected item's details, without
|
||||
showing the previous item's source after a selection change. Failed reads
|
||||
use application notifications, and missing sources have an explicit fallback. The left
|
||||
navigation can show the incomplete-todo count, and create, save, and comment
|
||||
results use application-wide notifications.
|
||||
- [ ] **MCP Server Control Plane** (planned): Unified MCP lifecycle, health
|
||||
checks, reconnection, schema cache, isolation, approval, and audit.
|
||||
- [ ] **Traceable note excerpts and AI editing** (planned): Collects sourced
|
||||
excerpts from conversations, knowledge, and the web and provides
|
||||
confirmation-based summarization, rewriting, and organization.
|
||||
|
||||
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 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/prd/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/prd/task-and-job/scheduled-task-prd.md)。
|
||||
- [x] **记忆与智能心跳**:当前提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
|
||||
- [x] **智能心跳入口与范围改善**:将“智能心跳 > 心跳计划”作为完整配置的唯一权威入口,支持创建和编辑 Global 或指定一个、多个 Project 的计划;旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置不再复制心跳表单。“未来分区记忆”仍只是尚待独立设计的长期方向。详见 [智能心跳 PRD](./docs/prd/smart-heartbeat/smart-heartbeat-prd.md)。
|
||||
- [ ] **通用监督**(规划中):通过固定监督栏目观察用户选择的会话、任务、自动化或实验对象,提供带证据的评论与人工介入请求,但不自动发言、批准工具或切换 Execute。详见 [会话监督 PRD](./docs/prd/supervision/conversation-supervision-prd.md)。
|
||||
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
|
||||
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
|
||||
- [ ] **可视化受控工作流**(规划中):提供版本化 DAG、条件分支、审批、取消和恢复,执行节点继续经过 Main Runtime 边界。
|
||||
- [ ] **统一 Run Graph 与回放**(规划中):关联任务、Subagent、模型、知识、工具审批、用量和成果,支持失败定位、重试和脱敏导出。
|
||||
### Work management, long-term collaboration, and workflows
|
||||
|
||||
### 浏览器、通信、语音与应用维护
|
||||
- [x] **Tasks, activity, and artifacts**: Centrally manages task state, audit
|
||||
activity, and independent artifact files. Ordinary chat responses remain
|
||||
only in the conversation and are no longer copied into Artifacts; existing
|
||||
duplicate chat Markdown is hidden from the artifact list but not physically
|
||||
deleted. Run history is stored in Main SQLite without the previous 500-item,
|
||||
4,000-character, or 2 MB Renderer limits; the page renders it in batches,
|
||||
without a persistent legacy-cache truncation warning; previously lost
|
||||
history is not restored. Token usage is grouped by Runtime and
|
||||
model and normalizes the different OpenAI-compatible and Anthropic Messages
|
||||
cache-reporting semantics when showing cache hit rate. Activity is grouped
|
||||
by conversation and collapsed by default so long histories do not fill the
|
||||
page.
|
||||
- [x] **Task and custom-task experience**: Each product-level Task belongs to
|
||||
one Conversation, while one Conversation can contain multiple Tasks. The
|
||||
left conversation list exposes Task children through a leading expand
|
||||
button; children share status dots, the parent does not repeat task badges,
|
||||
and the UI stops at Task rather than exposing Job/Run levels. A new custom
|
||||
Task can use the current or a new Conversation, defaults to Execute, and
|
||||
preserves Runtime, tool, and approval boundaries. Repeated triggers reuse the
|
||||
same Task; text returns to the Conversation, while independent files and
|
||||
images remain artifacts. Ordinary messages and due Scheduled Tasks share a
|
||||
persistent Conversation queue that runs one item at a time. Users can keep
|
||||
sending while a response is active; queued items continue in order and can
|
||||
be removed or inserted immediately by interrupting the current item. Task
|
||||
Center remains the complete index rather than creating a separate Automation
|
||||
Center. Current schedules support one-time, daily, and weekly triggers;
|
||||
advanced time zones, Cron, event triggers, and retry governance remain
|
||||
incremental PRD work. Scheduled messages use the conversation's current
|
||||
history, Runtime, work mode, and saved knowledge retrieval settings; task
|
||||
details show the actual mode used, and an active occurrence blocks duplicate
|
||||
Run Now actions. See the
|
||||
[Task Center PRD](./docs/features/task-and-job/task-center-prd.md) and
|
||||
[Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md).
|
||||
- [x] **Memory and Smart Heartbeat**: Provides periodic review, suggested
|
||||
memories, insights, follow-up tasks, and auditable run history.
|
||||
- [x] **Improved Smart Heartbeat entry and scope**: Smart Heartbeat / Heartbeat
|
||||
Plans is the sole authoritative full-configuration entry. Plans can be
|
||||
created and edited for Global or one or more selected Projects. Legacy
|
||||
single-project settings migrate without loss, and project-level memory and
|
||||
action output must explicitly target a Project in scope. Task Center and
|
||||
Settings no longer duplicate the form. Partition-aware review, candidate
|
||||
generation, and recall triggers remain to be designed. See the
|
||||
[Smart Heartbeat PRD](./docs/features/smart-heartbeat/prd.md).
|
||||
- [ ] **General supervision** (planned): Uses a fixed supervision section to
|
||||
observe user-selected conversations, Tasks, automations, or experiments and
|
||||
provide evidence-backed comments and requests for human intervention,
|
||||
without automatically speaking, approving tools, or switching to Execute.
|
||||
See the
|
||||
[Conversation Supervision PRD](./docs/features/conversation-supervision/prd.md).
|
||||
- [ ] **Batch runs and comparison lab** (planned): Compares model, Prompt,
|
||||
role, and workflow configurations in batches and summarizes quality,
|
||||
duration, tokens, cost, failure rate, and artifact differences.
|
||||
- [ ] **Temporal memory and fact-conflict detection** (planned): Adds validity
|
||||
periods, current/expired/conflicting fact detection, fact checking, and
|
||||
evidence tracing to memory and the knowledge graph.
|
||||
- [ ] **Visual controlled workflows** (planned): Versioned DAGs, conditional
|
||||
branches, approvals, cancellation, and recovery, with execution still
|
||||
passing through Main Runtime boundaries.
|
||||
- [ ] **Unified Run Graph and replay** (planned): Connects Tasks, Subagents,
|
||||
models, knowledge, tool approvals, usage, and artifacts for failure
|
||||
diagnosis, retry, and redacted export.
|
||||
|
||||
- [x] **直连模型内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过独立总开关决定是否提供给 Execute,开启后不逐次询问。
|
||||
- [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] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;失败后保留草稿并按同一请求编号重试,不自动附加对话、日志、文件、配置或凭据。
|
||||
- [x] **内网兼容模式**:默认开启;允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书,关闭后恢复严格地址和证书校验。
|
||||
### Browser, communication, voice, and application maintenance
|
||||
|
||||
### 开源、构建与发布
|
||||
- [x] **Shared built-in browser for runtimes**: Uses GoodBuddy's isolated
|
||||
Chromium and never controls a browser installed by the user. A separate
|
||||
master switch decides whether assigned runtimes receive the capability in
|
||||
Execute, with no per-use prompt after enablement. Turning Agent access off
|
||||
does not disable manual browser workbar actions. The Browser workbar and
|
||||
Agent share the same Conversation-owned session and serialized operation
|
||||
path, with Back, Refresh/Stop Loading, address entry, Go, Interaction, and
|
||||
Close actions. Stop Loading does not close the session, and user navigation
|
||||
changes the page the Agent sees next.
|
||||
Multiple tabs in one conversation share login state while keeping separate
|
||||
pages and navigation. Each model request retains its starting tab binding;
|
||||
unused request reservations do not create browser resources. Released tabs
|
||||
can be reopened, explicit screenshots remain available, and ordinary
|
||||
operations no longer trigger unused automatic screenshot capture.
|
||||
- [x] **Client-computer control tools**: Managed separately from the built-in
|
||||
browser with scope, cancellation, timeout, output, and activity boundaries.
|
||||
- [x] **Remote messaging-channel projects**: WeChat ClawBot, WeCom, and
|
||||
DingTalk each have a system-managed project, independent remote
|
||||
conversations, working directory, processing backend, default Ask/Execute
|
||||
mode, and task/activity ownership. Each channel controls complete-response
|
||||
length and segmentation according to platform capabilities rather than
|
||||
relying on a shared service truncation.
|
||||
- [x] **WeChat ClawBot QR login and media**: A separate Sidecar handles local
|
||||
QR scanning, verification codes, encrypted credentials, and text messaging.
|
||||
Personal WeChat chats support images and files, with at most four
|
||||
attachments and 12 MB total after decryption per message.
|
||||
- [x] **Safe WeChat responses**: Can return images generated by the current
|
||||
task, or generate a Markdown attachment from the current final text after an
|
||||
explicit user request. It never automatically reads or sends existing
|
||||
workspace files.
|
||||
- [x] **WeCom and DingTalk connections**: Main-only encrypted settings,
|
||||
read-only environment-variable overrides, connection tests, dynamic
|
||||
enable/disable, sender scope, and status diagnostics.
|
||||
- [x] **Managed local-model download source**: Platform Features / General
|
||||
selects ModelScope (default) or Hugging Face globally for future speech-input
|
||||
and OCR model downloads. A source without a complete verified file is
|
||||
explicitly unavailable and never silently falls back or combines files.
|
||||
- [x] **Optional local speech-model management**: Model weights are not bundled.
|
||||
Provides verified downloads, progress and cancellation, source links, ZIP or
|
||||
local-directory import, switching, and removal.
|
||||
- [x] **Local recording and offline transcription**: Captures microphone audio
|
||||
and transcribes with the selected local model, with stop, cancel, state
|
||||
feedback, and resource cleanup.
|
||||
- [x] **Version checks and mirror source**: About and Updates selects GitHub
|
||||
(default) or the mirror. Manual checks, startup checks, and the download page
|
||||
use the same choice and read only fixed trusted release indexes; GoodBuddy
|
||||
does not automatically download or install updates.
|
||||
- [x] **In-app feedback**: About and Updates can submit problems, suggestions,
|
||||
or experience feedback with an optional email and one screenshot.
|
||||
Diagnostics are not uploaded by default; users can explicitly attach a
|
||||
bounded recent Desktop diagnostic summary. Failed submission preserves the
|
||||
draft, diagnostic choice, and request ID. Conversations, Prompts,
|
||||
credentials, file contents, paths, raw provider responses, and remote Agent
|
||||
logs are never attached.
|
||||
- [x] **Private-network compatibility**: Permits in-app HTTP plus invalid,
|
||||
self-signed, or expired HTTPS certificates. Fixed platform services such as
|
||||
feedback and WeChat credential or media endpoints retain strict certificate
|
||||
validation.
|
||||
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;Linux 同时生成 AppImage、DEB 和 RPM。
|
||||
### Open source, builds, and releases
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
- Current source candidates are Desktop `0.12.12` and Agent `0.11.23`, with
|
||||
OpenCode pinned to `1.18.29`. Publication status follows the independent
|
||||
Desktop and Agent release channels.
|
||||
- [x] **0BSD open-source license**: Original code can be freely used, copied,
|
||||
modified, distributed, and commercialized. Third-party components and
|
||||
resources retain their own licenses.
|
||||
- [x] **Reproducible dependency installation and source builds**: Uses locked
|
||||
dependencies, Node.js 24, and unified test, type-check, lint, and production
|
||||
build commands.
|
||||
- [x] **Six-target native release matrix**: Native runners build Windows,
|
||||
macOS, and Linux `x64` and `arm64` targets with release manifests and SHA-256
|
||||
hashes. Windows provides NSIS and portable ZIP, macOS DMG only, and Linux
|
||||
AppImage, DEB, and RPM, for 12 installers and 20 published assets.
|
||||
- [x] **Desktop Runtime package verification**: Offline OpenCode dependencies
|
||||
are copied without electron-builder's root node_modules exclusion; DSH
|
||||
session projection is bundled into the unpacked Host. Final package checks
|
||||
compare the offline dependency tree and reject external DeepSeek imports.
|
||||
Windows x64 CI additionally imports the packaged OpenCode plugin and checks
|
||||
the real DSH UtilityProcess handshake before publishing.
|
||||
- [x] **DMG-only update manifests**: Current Desktop and website readers accept
|
||||
DMG-only and historical DMG/ZIP manifests. Older macOS GitHub readers and all
|
||||
older mirror readers require a manual upgrade from the download page.
|
||||
|
||||
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
|
||||
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
|
||||
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
|
||||
- [ ] **SSH 主机与远程执行空间发布验收**:主机 CRUD、Host Key、加密凭据、Project UI、Workspace、OpenCode ACP、Main-only 模型桥、Ask 只读、Execute 完整账号权限、取消、detached Agent 重连和 release-only 双架构资源校验已经接线。Windows 到 Linux x64 的既有真实 Host provider-free 安装、metadata-only Portable 复用与项目保存、桥接、断线恢复、输出重放、同 Session 续接与清理已经通过;Agent 0.11.2-e2e.12 与 OpenCode Runtime 1.18.9 的真实模型 Ask 验收也已通过,单次用户操作只执行一个原生 read,两个 build 模型轮次均完成交付,没有 title 轮次或请求重放。公开 signing key registry 已供应。控制面直连源码不等待新 installer-bearing 包,但仍需公开核对当前 Linux x64/arm64 正式签名工件矩阵,并完成 GitHub/北京镜像、双架构、取消和离线 GoodBuddy 传输的真实 Host 验收;门槛未满足时不宣称该路径已发布或通过验收。
|
||||
- [ ] **多云远程沙盒 Agent**(规划中):通过云厂商 API 和 SSH Agent 管理专用 Linux 沙盒;凭据留在 Main 进程,高风险控制面操作单独确认。
|
||||
### Open interfaces, team collaboration, and remote execution
|
||||
|
||||
## 规划原则
|
||||
- [x] **Remote task delegation**: Enabled only after the user explicitly
|
||||
configures an endpoint and token, uses HTTP(S) according to the global
|
||||
private-network compatibility setting, and writes results to a persistent
|
||||
outbox.
|
||||
- [ ] **Headless Runtime API** (planned): A local-first Task, event, state, and
|
||||
artifact API with scoped, expiring, rate-limited, and revocable tokens.
|
||||
- [ ] **GoodBuddy Team Hub** (planned): An optional service for organizations,
|
||||
RBAC, project sharing, remote Agents, policy distribution, and tenant audit.
|
||||
- [ ] **SSH Host and remote execution-space release acceptance**: Host CRUD,
|
||||
Host Key, encrypted credentials, Project UI, Workspace, OpenCode ACP v5,
|
||||
Agent-owned Prompt/gateway/transcript, read-only Ask, full-account Execute,
|
||||
cancellation, exact detached-Agent reconnection, and release-only
|
||||
dual-architecture resource verification are wired. Current Linux x64 source
|
||||
has passed a real-model and tool matrix covering detach, process exit, relay
|
||||
loss, concurrency, cancellation, provider failures, Agent restart, and
|
||||
Desktop SQLite recovery. A current-source, test-signed Agent `0.11.17`
|
||||
package on Linux x64 additionally passed two concurrent Ask reads, an
|
||||
Execute write/read, and six real managed-gateway model requests with the
|
||||
configured custom headers and body fields. No provider request was replayed,
|
||||
and scanning the isolated Agent state found no API key or custom request
|
||||
value. The public signing-key registry is provisioned. Direct control-plane
|
||||
source does not wait for a new installer-bearing package, but the current
|
||||
formally signed Linux x64/arm64 artifact matrix still requires public
|
||||
verification, along with GitHub/Beijing mirror, dual-architecture, and
|
||||
offline GoodBuddy transfer release acceptance. Until these gates pass, this
|
||||
path must not be described as published or as having passed formal release
|
||||
acceptance.
|
||||
- [ ] **Multi-cloud remote sandbox Agents** (planned): Manages dedicated Linux
|
||||
sandboxes through cloud-provider APIs and SSH Agents. Credentials remain in
|
||||
Main, and high-risk control-plane operations receive separate confirmation.
|
||||
|
||||
规划中的工作流、Subagent、MCP、远程 API 和沙盒能力不得绕过现有 Main Runtime、Ask/Execute、权限、取消、超时和审计边界。
|
||||
## Roadmap Principles
|
||||
|
||||
Planned workflows, Subagents, MCP, remote APIs, and sandbox capabilities must
|
||||
not bypass existing Main Runtime, Ask/Execute, permission, cancellation,
|
||||
timeout, or audit boundaries.
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# GoodBuddy 功能矩阵与路线图
|
||||
|
||||
[English](./FEATURES.md) | **简体中文**
|
||||
|
||||
本文记录 GoodBuddy 的已提供能力和路线图。未完成项目不代表已包含在当前版本中。
|
||||
|
||||
## 状态说明
|
||||
|
||||
- [x] 已提供
|
||||
- [ ] 开发中或规划中,具体以条目标注为准
|
||||
|
||||
## 功能总表
|
||||
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.12` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异、当前浏览目录和已展开目录,超过 50 项变更可继续加载。
|
||||
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。提交历史贴底,首次展开占 Git 内容区一半,支持调整比例、独立滚动和统一刷新;切换项目自动检测 Git,不沿用其他项目状态。移动限于工作区内,删除须确认,分支冲突不自动 stash 或丢弃改动;托管 SSH 操作需要 Agent `0.11.23`。
|
||||
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
|
||||
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
|
||||
- [x] **助手工作栏、多终端与可调布局**:右侧工作栏使用持久“+”能力目录和应用 Tab;任务中心与工作区为不可关闭的单实例,浏览器可打开多个独立实例。任务中心可切换当前项目、全局任务和全部项目,范围随布局保存。用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
|
||||
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
|
||||
- [ ] **应用导航与系统工具快捷入口**(仅设计):规划固定系统入口、内置应用显隐和打开现有工作栏的底部工具按钮;尚未实现新界面。详见[设计文档](./docs/features/application-tool-navigation/README.md)。
|
||||
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入监督、统一 Runtime、受管进程、静态安全 HTML 预览、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
|
||||
- [ ] **ShareServer Office 协同编辑**(设计中):通过可选 ShareServer 集成 ONLYOFFICE Docs,在助手工作栏中以文件名打开多个文档 Tab,支持 DOCX、XLSX、PPTX 的人工编辑、保存、撤销、源文件冲突保护和后续 AI 选区修改。Office 正文会由所选 ShareServer 和编辑引擎处理;Desktop 不内置或启动 Document Server,未配置服务时不宣称离线编辑。详见[设计文档](./docs/features/office-document-editing/README.md)。
|
||||
|
||||
### Agent Runtime 与模型连接
|
||||
|
||||
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。连续请求自动复用最近一次成功生成的图片和文字历史,已保存会话重开后仍可继续修改,本轮显式附图优先;上游明确不支持编辑时继续按文字生成,仅在回复底部提示未使用参考图。编辑使用 multipart 上传图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。详见[图片生成与连续修改](./docs/features/image-generation/README.md)。
|
||||
- [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
|
||||
- [x] **高效直连模型工作区工具**:直连模型使用随包 ripgrep 紧凑地发现文件和搜索内容,按行分页读取大型 UTF-8 文件,并在 Execute 中应用多文件补丁。Ask 可以搜索和读取,但不能应用补丁或运行命令;用户无需另行安装 ripgrep。
|
||||
- [x] **长上下文与工具输出分页**:历史消息、长回复和文档解析文本不再采用旧的固定截断限制。直连模型可分页续读已保存的命令和 Subagent 输出;所选模型的上下文窗口及传输边界仍然有效。
|
||||
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、启动与控制请求时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。托管的本机和远端 OpenCode 跳过界面未使用的自动 Git 快照,避免同步差异计算拖住会话,文件工具、子代理与工作区 Git 差异保留;Continue 持续排空不用展示的宿主 stdout,防止控制台输出填满管道。这些调整随新启动的 Runtime 生效,不改写外部 OpenCode Server 配置。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
|
||||
- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node 和 OpenCode 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。macOS 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证;这不等于新版本已经发布。详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。
|
||||
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
|
||||
- [ ] **SSH Host 环境准备真实 Host 验收**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵仍待验收,候选 CI/原生打包与系统休眠唤醒也尚未验证;部分开发证据不代表已经发布。
|
||||
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
|
||||
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。
|
||||
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方代码不受 Ask 初始化隔离。
|
||||
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权,包括工作区外路径及原生子代理工作。
|
||||
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,OpenCode 同时转交属于当前请求的子会话提问。回答或跳过成功后保留原问题与答案,支持多轮记录和本地会话重载;旧版本已丢弃的答案不能恢复。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
|
||||
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求 Ask/Execute 模式,可使用已启用的本机直连模型工具,Ask 仍只读;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
|
||||
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
|
||||
- [x] **OpenCode 事件连接回收**:请求结束或取消后关闭自身订阅,不取消其他并行会话。
|
||||
- [x] **准确的消息底部状态**:区分请求准备、等待重试、开始重试、工具活动、待回答和终态;本机 OpenCode 使用原生重试次数和计划时间,直连模型显示自身退避阶段。消息底部状态点保持静态,未接入的 Runtime 重试不编造提示,会话列表闪点及发送、停止行为不变。
|
||||
- [x] **直连模型命令工作目录**:Execute 命令可通过绝对路径、相对路径或符号链接使用工作区外目录;工作区仍为默认目录和相对路径基准,Ask 保持只读。
|
||||
- [x] **简化 Runtime 选择**:会话与项目菜单不再枚举每个 Runtime 与模型的组合,Runtime 模型在系统设置中配置;已保存的项目固定模型仍可使用,并明确显示其固定模型。
|
||||
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
|
||||
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
|
||||
- [x] **模型请求定制**:每个 LLM 连接可填写有界 JSON 自定义请求头和顶层请求体。直连与 Continue 支持两者;本地 OpenCode 和 DeepSeek Harness 只使用各自原生支持的请求头,受管 SSH OpenCode 模型网关支持两者。Runtime、协议和认证字段始终优先,这些普通连接设置不能用于保存 API Key 或其他密钥。详见[模型连接请求定制](./docs/features/model-connections/README.md)。
|
||||
- [x] **上下文用量与自动压缩**:直连模型按每次成功调用更新供应商用量,图片与工具轮次使用同一口径,供应商缺失 usage 时才回退估算;界面明确区分“本次模型调用”和“压缩后对话估算”,压缩线始终根据当前设置与所选模型窗口即时计算,不在每个对话中保存旧配置;压缩标识的前后值使用同一估算口径,运行记录仍保留各次模型调用的供应商 usage。对话与多轮工具 Agent 可在已完成调用越过阈值后自动重复压缩,规划时先为固定提示、工具定义和摘要预留预算;同一回复会分别保留 Agent 工具上下文与对话历史的压缩标识,并在应用重启或较早消息滚出本地历史窗口后继续复用摘要。
|
||||
- [x] **持久凭据保护**:API Key 由 Main 使用系统安全存储加密且不暴露给 Renderer。托管 SSH accepted Prompt 只把当前 profile 与密钥放入 Agent 内存,不写入 SSH 参数、远端环境或磁盘;其他路径仍在 Main 内使用。密钥随对应模型连接保存,修改服务地址或临时切换为无需认证不会要求重新输入;只有用户显式清除凭据或删除连接时才移除。
|
||||
- [x] **有界故障诊断**:Desktop 在用户数据目录中轮转保存启动、Runtime 和远程连接的固定阶段失败,最多 4 个 256 KiB 文件;GoodBuddy Agent 在各 installation 的私有 state 目录中轮转保存 daemon、连接、恢复和 Runtime 生命周期,最多 3 个 64 KiB 文件,并可通过固定 `diagnostics --installation-id` 命令读取。两端都只记录白名单阶段、稳定错误码/类型和固定短消息,不保存 Prompt、凭据、文件内容、路径、环境、SSH 参数或 Provider 原始响应;诊断写入失败不改变正常运行。
|
||||
- [x] **OpenCode Runtime 定制**:GoodBuddy 管理的内置 OpenCode 可发现原生 Agents、Tools、Commands、LSP、Formatters、MCP、Skills、Prompts 与 Resources;Tools 单独显示读取、文件修改、命令、网络、Agent 编排等类型、来源及 Ask/Execute 可用性,并隐藏 OpenCode 内部 `invalid` 与 GoodBuddy 临时 MCP 工具。支持保存默认 Agent、每次请求覆盖 Agent、通过原生 SDK 执行 Command、显示上下文用量并调用有总时限的原生 Compact;并发外部 Server 对话的提问使用请求级公开 ID 映射,回答不会串到其他会话。外部 OpenCode Server 只报告连接状态,不宣称原生清单可读。任意插件安装、Session Share、自动 Worktree 和 OpenCode 原生会话持久化仍不开放。
|
||||
- [x] **Continue Runtime 定制**:提供静态配置中的原生 Rules、Prompt 模板与 MCP 清单,以及可编辑的 GoodBuddy Rules/Prompt 配置预设;聊天可按请求选择预设和填入可继续编辑的 Prompt。当前 Continue Host 没有可信的静态原生 Tool 发现接口,且使用隔离的 `CONTINUE_GLOBAL_DIR`,因此界面明确标记 Tools 不支持静态发现,也不把 Host 实际不会加载的工作区或用户 Skills 冒充原生能力;GoodBuddy 分配的 Skills 仍按请求暂存执行。Continue 临时 Host 不复用原生会话压缩,手动压缩由 GoodBuddy 摘要模型完成并验证持久化摘要覆盖范围;Agent 交互提问转换为统一问答卡片。Resources、Hooks、后台 Job 和 Continue 原生会话管理继续暂缓。
|
||||
- [x] **Runtime 原生清单语义**:原生能力以 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 个页签展示;清单状态独立于 Runtime 连通性,区分完整、部分、不可用、仅连接和不支持。DeepSeek Harness 通过 Host Registry 枚举有界的内置/插件 Tools 与 Skills,显示真实 Ask/Execute 边界,并排除 GoodBuddy 按请求分配的 Skills、Web/MCP 代理。
|
||||
- [ ] **Runtime 监督栏目**(规划中):在应用级助手工作栏的固定 Runtime 栏目统一承载 OpenCode、Continue 和 DeepSeek Harness 的 Task 级委派、后台执行、Workflow/Hook、长任务与原生会话监督;用户只选择 Conversation 或 Task,Job/Run 保持内部,不形成树或独立操作对象。
|
||||
- [ ] **Subagent 高级监督**(规划中):在工作栏固定 Runtime 栏目按 Task 聚合,提供可配置的嵌套、并行、预算和生命周期控制。基础专家模式继承与单层直连模型编程委派已经可用。
|
||||
|
||||
### Skills、MCP 与知识库
|
||||
|
||||
- [x] **Skills 按需接入**:可分配给直连模型、OpenCode、Continue 和 DeepSeek Harness,并使用有界资源和受控 Runtime 边界。
|
||||
- [x] **本机工具执行环境源码链路**:在“能力与工具 > 工具执行环境”中为本机 Skills 与 stdio MCP 选择 GoodBuddy 托管 Node.js、按需安装的托管 Python,或经过真实验证的自定义解释器;提供独立的原生地址/OSS 镜像选择、诊断、安装进度、取消和删除。新的本机 Runtime 与 stdio MCP 获得不可变 PATH 快照,不修改普通终端、系统环境或远程 Host。Windows x64 托管 Node 与原生地址 Python 已通过真实安装验证;托管 Python 归档按目标文件系统验证,Linux 保留大小写不同的合法路径,所有目标仍拒绝完全重复路径和不安全条目。
|
||||
- [ ] **本机工具执行环境发布验收**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv。不可变的 `v0.12.0` 尝试在发布前通过 Windows 与 macOS,但暴露了 Linux TAR 大小写处理问题;`v0.12.1` 在同步 Agent 发布时于原生打包前取消。已发布的 `v0.12.2` 随后通过全部六个原生打包任务及其托管 Python 安装探针。真实 Skill/MCP、自定义解释器和运行中进程协调仍是待完成验收。
|
||||
- [x] **内置 MCP 按需接入**:知识库、魔法笔记、GoodBuddy 配置与内置浏览器 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;DeepSeek Harness 在设置中明确显示为暂不支持。内置 MCP 仅通过当前请求的短期本机权限提供,Ask / Execute 读写边界不受用户配置放宽。
|
||||
- [x] **MCP Tools**:显式启用的自定义 MCP 可按 Runtime 分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent Execute 和 DeepSeek Harness,并仅在 Execute 加载;Agent 子进程只获得按请求签发的本机回环权限,MCP 地址、命令和凭据保留在 Main,动态工具仍经过发现、执行记录与权限边界。直连模型与 Harness 的健康调用复用当前请求已发现的工具清单,不在每次调用前重复发现。
|
||||
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
|
||||
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
|
||||
- [ ] **外部知识库接入**(规划中):在现有知识库页面管理 Dify、FastGPT 和 RAGFlow 实例;新建知识库时可读取并绑定远端知识库,保留 Provider 专属检索配置和引用。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
|
||||
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
|
||||
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
|
||||
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
|
||||
- [x] **混合检索测试台**:支持全文、中文词组、向量和图谱通道诊断,可调 Top K、阈值、权重、本地或学习型重排及上下文预算。
|
||||
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
|
||||
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
|
||||
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
|
||||
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;来源记录直接显示在所选待办的详情中,切换事项不会混入上一项的来源内容,读取失败使用应用通知,来源缺失有明确提示。左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。
|
||||
- [ ] **MCP Server Control Plane**(规划中):统一 MCP 生命周期、健康检查、重连、Schema 缓存、隔离、审批和审计。
|
||||
- [ ] **可追溯笔记摘录与 AI 编辑**(规划中):从对话、知识和网页收集带来源的摘录,并提供需确认的总结、改写和整理操作。
|
||||
|
||||
### 工作管理、长期协作与工作流
|
||||
|
||||
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
|
||||
- [x] **Task 与定制任务体验**:每个产品级 Task 只关联一条 Conversation,一条 Conversation 可承载多个 Task;左侧会话列表通过行首展开按钮显示带共享状态点的 Task 子项,父会话行不重复任务标签,UI 只展示到 Task,不暴露 Job/Run 层级。新建定制任务可关联当前或新 Conversation,默认 Execute 并沿用 Runtime、工具和审批边界;重复触发复用同一 Task,文本结果回写 Conversation,独立文件和图片保留为成果。普通消息与到期 Scheduled Task 共用 Conversation 级持久发送队列,同一会话一次只执行一项;当前回复期间仍可继续发送,队列按顺序续跑,并允许删除或“立即中断并插入”。Task Center 继续作为完整索引,不建设独立 Automation Center。当前计划触发支持单次、每日和每周;高级时区、Cron、事件触发与重试治理仍按 PRD 逐步实现。详见 [Task Center PRD](./docs/features/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md)。
|
||||
- [x] **记忆与智能心跳**:当前提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
|
||||
- [x] **定时消息沿用会话**:定时消息使用会话当前历史、Runtime、工作模式和已保存的知识检索设置;任务详情显示实际运行模式,当前 occurrence 未结束时禁止重复“立即运行”。
|
||||
- [x] **智能心跳入口与范围改善**:将“智能心跳 > 心跳计划”作为完整配置的唯一权威入口,支持创建和编辑 Global 或指定一个、多个 Project 的计划;旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置不再复制心跳表单;按分区回顾、候选生成和唤起条件仍待设计。详见 [智能心跳 PRD](./docs/features/smart-heartbeat/prd.md)。
|
||||
- [ ] **通用监督**(规划中):通过固定监督栏目观察用户选择的会话、任务、自动化或实验对象,提供带证据的评论与人工介入请求,但不自动发言、批准工具或切换 Execute。详见 [会话监督 PRD](./docs/features/conversation-supervision/prd.md)。
|
||||
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
|
||||
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
|
||||
- [ ] **可视化受控工作流**(规划中):提供版本化 DAG、条件分支、审批、取消和恢复,执行节点继续经过 Main Runtime 边界。
|
||||
- [ ] **统一 Run Graph 与回放**(规划中):关联任务、Subagent、模型、知识、工具审批、用量和成果,支持失败定位、重试和脱敏导出。
|
||||
|
||||
### 浏览器、通信、语音与应用维护
|
||||
|
||||
- [x] **Runtime 共享内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过内置 MCP 总开关和 Runtime 分配决定是否提供给 Execute,开启后不逐次询问。关闭 Agent 浏览器能力不影响用户继续在浏览器工作栏中手动前往、返回、刷新、停止加载、交互或关闭浏览器。直连模型、GoodBuddy 管理的 OpenCode 和 Continue 通过请求级权限共享按 Conversation 归属的会话和串行操作路径,用户导航会改变 Agent 下一步看到的页面。
|
||||
- [x] **浏览器多页签与按需资源**:同一对话可打开多个共享登录状态、页面与导航独立的浏览器页签,模型请求固定使用开始时绑定的目标。未使用的请求预留不创建浏览器资源,已释放页面可重新打开;保留显式截图,普通操作不再触发无用的自动截图。
|
||||
- [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。
|
||||
- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。
|
||||
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
|
||||
- [x] **微信安全回传**:支持返回当前任务生成的图片,或在用户明确要求时将本次最终文本生成为 Markdown 附件;不自动读取或发送已有工作区文件。
|
||||
- [x] **企业微信与钉钉连接**:支持 Main-only 加密设置、环境变量只读覆盖、连接测试、动态启停、发送者范围和状态诊断。
|
||||
- [x] **受管本地模型下载源**:在“平台功能 / 通用设置”中为后续语音输入与 OCR 模型下载全局选择 ModelScope(默认)或 Hugging Face;所选来源缺少完整已验证文件时明确不可用,不静默换源或混合文件。
|
||||
- [x] **可选本地语音模型管理**:应用不内置模型权重;提供校验下载、进度与取消、来源链接、ZIP 或本地目录导入、切换和删除。
|
||||
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
|
||||
- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
|
||||
- [x] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;默认不上传诊断,用户可主动附加有界的最近桌面诊断摘要。失败后保留草稿、诊断选择和同一请求编号;不会附加对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
|
||||
- [x] **内网兼容**:允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书;反馈、微信凭据与媒体等固定平台服务仍保持严格证书校验。
|
||||
|
||||
### 开源、构建与发布
|
||||
|
||||
- 当前源码候选为 Desktop `0.12.12`、Agent `0.11.23`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
|
||||
- [x] **桌面 Runtime 包校验**:OpenCode 离线依赖复制避开 electron-builder 对源根目录 node_modules 的排除,DSH 会话投影组件打入解包后的 Host。最终包逐文件核对离线依赖并拒绝外部 DeepSeek 导入;Windows x64 CI 还会导入包内 OpenCode 插件、验证真实 DSH UtilityProcess 握手,通过后才发布。
|
||||
- [x] **DMG-only 更新清单**:当前 Desktop 和官网读取器同时接受仅 DMG 与历史 DMG/ZIP 清单。旧版 macOS 的 GitHub 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
|
||||
- [x] **远程任务委派**:仅在用户显式配置端点和令牌后启用,按全局内网兼容模式使用 HTTP(S),结果进入持久化发件箱。
|
||||
- [ ] **Headless Runtime API**(规划中):提供本机优先的任务、事件、状态和成果 API,以及有范围、有效期、限流和撤销能力的令牌。
|
||||
- [ ] **GoodBuddy Team Hub**(规划中):以可选服务提供组织、RBAC、项目共享、远程 Agent、策略下发和租户审计。
|
||||
- [ ] **SSH 主机与远程执行空间发布验收**:主机 CRUD、Host Key、加密凭据、Project UI、Workspace、OpenCode ACP v5、Agent-owned Prompt/gateway/transcript、Ask 只读、Execute 完整账号权限、取消、detached Agent 精确重连和 release-only 双架构资源校验已经接线。Linux x64 当前源码已用真实模型与工具通过 detach、进程结束、relay 丢失、并发、取消、Provider 异常、Agent 重启和 Desktop SQLite 恢复矩阵;当前源码测试签名 Agent `0.11.17` 又通过两个并发 Ask 读取、一次 Execute 写入并读回,以及 6 次带自定义 Header 和 Body 的真实受管网关模型请求,未发现 Provider 请求重放,隔离 Agent state 中也没有 API Key 或自定义请求值。公开 signing key registry 已供应。控制面直连源码不等待新 installer-bearing 包,但仍需公开核对当前 Linux x64/arm64 正式签名工件矩阵,并完成 GitHub/北京镜像、双架构和离线 GoodBuddy 传输的发布验收;门槛未满足时不宣称该路径已发布或通过正式发布验收。
|
||||
- [ ] **多云远程沙盒 Agent**(规划中):通过云厂商 API 和 SSH Agent 管理专用 Linux 沙盒;凭据留在 Main 进程,高风险控制面操作单独确认。
|
||||
|
||||
## 规划原则
|
||||
|
||||
规划中的工作流、Subagent、MCP、远程 API 和沙盒能力不得绕过现有 Main Runtime、Ask/Execute、权限、取消、超时和审计边界。
|
||||
@@ -1,85 +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.
|
||||
- **Managed SSH projects (technical preview)**: Remote support is off by default and does not affect ordinary desktop use. After enabling it, manually download the independently signed package for the Linux Host architecture under Settings > Platform Features > Remote Projects, or import/export an offline `.gbagent`. Each package contains the Agent, pinned Node, and the GoodBuddy-adapted remote OpenCode Runtime; online downloads follow the GitHub or mirror source selected under About & Updates.
|
||||
- **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, and
|
||||
the [documentation index](docs/README.md) for product, architecture, design,
|
||||
and quality documents.
|
||||
|
||||
## 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, RPM |
|
||||
|
||||
Desktop installers do not contain remote Agent payloads. No Agent package is
|
||||
needed for local projects; managed SSH users install one explicitly in
|
||||
Settings.
|
||||
|
||||
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,63 +1,113 @@
|
||||
<div align="center">
|
||||
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
|
||||
|
||||
# GoodBuddy
|
||||
|
||||
[English](README.en.md) | 简体中文
|
||||
**Desktop access to model chat, coding agents, knowledge, and scheduled work**
|
||||
|
||||
GoodBuddy是一个开箱即用的 AI 生产平台,安全、跨平台、本地优先的桌面 AI 助手与 Agent 工作空间。
|
||||
**English** | [简体中文](./README.zh-CN.md)
|
||||
|
||||
产品官网:https://mesalogo.github.io/goodbuddy/
|
||||
[Website](https://mesalogo.github.io/goodbuddy/en.html) ·
|
||||
[Download](https://github.com/mesalogo/goodbuddy/releases) ·
|
||||
[Feature Matrix](./FEATURES.md) ·
|
||||
[Build Guide](./BUILD.md)
|
||||
|
||||
五大特点:
|
||||
1. 支持国产信创系统和硬件
|
||||
2. 免注册,不收集个人信息
|
||||
3. 完全开源和免费,友好的OBSD协议
|
||||
4. 不需要联网,纯私有网络也可以用
|
||||
5. 开放生态,内置主流好用的Agent Runtime,目前支持OpenCode、Continue和DeepSeek Harness,让小白用户也可以快速用起来最前沿的Agent工具
|
||||
[](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 is a desktop application for model chat, coding agents, knowledge
|
||||
retrieval, notes, and scheduled tasks. No GoodBuddy account is required.
|
||||
Workspace data is stored locally by default. Model endpoints can run on the
|
||||
same device, on a private network, or at a provider selected by the user.
|
||||
|
||||
- **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。
|
||||
- **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 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 插件市场。Anthropic 连接可独立配置协议必填的单次最大输出,不再复用上下文窗口;OpenAI 直连请求不额外设置输出 Token 上限。MCP 测试可读取有界 Prompt/Resource 元数据,但不会读取 Resource 内容。
|
||||
- **知识工作区**:支持文件、目录和网页导入,以及全文、词组、向量和图谱混合检索。
|
||||
- **工作管理**:集中管理 Projects、对话、任务、活动、成果、记忆、魔法笔记和智能心跳;可从稳定的本地会话复制当前聊天内容并创建互不影响的会话分支。
|
||||
- **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。
|
||||
- **托管 SSH 项目(技术预览)**:远程能力默认关闭且不影响普通桌面使用。添加 Host 只保存并探测,不自动安装;Host 卡片以一个按版本事实命名的主按钮准备环境,次级控件可选默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输。自动模式只在操作开始时择一,失败不跨路径重试;两种方式交付同一签名 compound `.gbagent` 后共用 prepare、commit、Agent/Runtime adoption 与 cleanup。项目创建与切换只使用已经准备好的 Host。GitHub/北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收仍待完成,当前不表示该路径已发布。
|
||||
- **桌面上下文**:可选择文件、截图、应用窗口、剪贴板和语音作为上下文。
|
||||
- **离线语音**:支持 SenseVoice、Paraformer 和 Whisper 本地模型。
|
||||
- **富文本回答**:支持 Markdown、LaTeX 公式和受控 Mermaid 图表。
|
||||
## Highlights
|
||||
|
||||

|
||||
- **Local-first workspace**: No GoodBuddy account is required. Workspace data
|
||||
stays local by default, and users choose where model services run.
|
||||
- **Unified Agent Runtimes**: Use direct models, OpenCode, Continue, and the
|
||||
preview DeepSeek Harness from one desktop interface.
|
||||
- **Explicit execution boundaries**: `Ask` remains read-only, while `Execute`
|
||||
uses the current account's permissions. Tool activity, cancellation,
|
||||
timeouts, and token usage are recorded.
|
||||
- **Broad platform coverage**: Official Linux releases cover `x64` and `arm64`,
|
||||
including compatible distributions used in China's domestic computing
|
||||
ecosystem. A separate
|
||||
experimental `loong64` preview is available for LoongArch systems.
|
||||
- **Knowledge and long-running work**: Knowledge bases, graphs, Magic Notes,
|
||||
tasks, and Smart Heartbeat share one desktop workspace.
|
||||
|
||||

|
||||
Platform coverage describes operating-system, instruction-set, and package
|
||||
compatibility, not certification for every hardware vendor or distribution
|
||||
release. LoongArch is outside the official release matrix; see the
|
||||
[LoongArch preview notes](./docs/development/loongarch-preview-build.md) for its
|
||||
current limitations.
|
||||
|
||||

|
||||
## Capabilities
|
||||
|
||||

|
||||
| Area | Available in GoodBuddy |
|
||||
| --- | --- |
|
||||
| Agent Runtimes | Direct model connections, OpenCode, Continue, and the preview DeepSeek Harness |
|
||||
| Data and execution | Local SQLite storage, read-only `Ask`, full-account `Execute`, and recorded tool activity |
|
||||
| Knowledge | File, folder, and web imports with full-text, Chinese phrase, vector, and knowledge graph retrieval |
|
||||
| Desktop releases | Official Windows, macOS, and Linux builds for `x64` and `arm64`, plus an experimental LoongArch preview |
|
||||
| Integrations | Local or hosted model endpoints, custom MCP servers, WeChat ClawBot, WeCom, and DingTalk |
|
||||
|
||||
完整功能和路线图见 [FEATURES.md](FEATURES.md),产品、架构、设计与质量文档见
|
||||
[文档导航](docs/README.md)。
|
||||
## Product tour
|
||||
|
||||
## 安装
|
||||
### 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、RPM |
|
||||
| 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 |
|
||||
|
||||
桌面安装包不包含远端 Agent payload。仅使用本地项目时无需下载任何
|
||||
Agent 包;托管 SSH 用户可在设置中按需管理。
|
||||
Each run reports cancellation, timeouts, token usage, tool calls, and history.
|
||||
Runtime-specific capabilities and configuration remain separate.
|
||||
|
||||
当前尚未配置代码签名和 macOS notarization,系统可能显示安全提示。
|
||||
## Download
|
||||
|
||||
## 从源码运行
|
||||
Download a build from
|
||||
[GitHub Releases](https://github.com/mesalogo/goodbuddy/releases):
|
||||
|
||||
需要 Node.js 24 和 npm:
|
||||
| Platform | Architectures | Formats |
|
||||
| --- | --- | --- |
|
||||
| Windows | `x64`, `arm64` | NSIS, portable ZIP |
|
||||
| macOS | `x64`, `arm64` | DMG, ZIP |
|
||||
| Linux | `x64`, `arm64` | AppImage, DEB, RPM |
|
||||
|
||||
The experimental LoongArch `loong64` build is distributed through a separate
|
||||
preview channel on the [product website](https://mesalogo.github.io/goodbuddy/#download).
|
||||
|
||||
Desktop installers do not contain remote Agent payloads. Local projects do not
|
||||
need an Agent package; managed SSH users download or import one explicitly in
|
||||
Settings.
|
||||
|
||||
Code signing and macOS notarization are not configured yet, so the operating
|
||||
system may display a security warning.
|
||||
|
||||
## Run from source
|
||||
|
||||
Requires Node.js 24 and npm:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mesalogo/goodbuddy.git
|
||||
@@ -66,20 +116,35 @@ npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
构建与打包说明见 [BUILD.md](BUILD.md)。
|
||||
See [BUILD.md](./BUILD.md) for build and packaging instructions.
|
||||
|
||||
## 隐私与安全
|
||||
## Documentation
|
||||
|
||||
- 模型请求只发送到用户选择的服务。
|
||||
- 本地数据默认保存在系统应用数据目录。
|
||||
- Renderer 不接触原始 Electron API 或模型凭据。
|
||||
- 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
|
||||
@@ -87,19 +152,16 @@ npm run typecheck
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 为什么选择GoodBuddy
|
||||
## Community communication
|
||||
|
||||
<img width="1024" height="1536" alt="6dd45371542dea9fb2d99474387708f5" src="https://github.com/user-attachments/assets/ca608646-ea48-4039-9177-d862be8396ac" />
|
||||
Scan the QR code to join the GoodBuddy WeChat group:
|
||||
|
||||
|
||||
## 社区交流
|
||||
|
||||
目前采用微信群的方式供大家高效交流,大家可以微信扫码进入社区群:
|
||||
|
||||
<img width="1250" height="1743" alt="6ae8664c83b2f182aa81ae94afe691fe" src="https://github.com/user-attachments/assets/10bb7ac2-0bc1-41a9-978d-c2862923fcd6" />
|
||||
<img width="1250" height="1194" alt="9627ce196b0de42f123c33dd9dea1568" src="https://github.com/user-attachments/assets/e806543f-6d8a-4c5c-ba44-19e74dac9a78" />
|
||||
|
||||
|
||||
|
||||
## 开源许可
|
||||
## License
|
||||
|
||||
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.
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<div align="center">
|
||||
<img src="./sites/assets/goodbuddy-light.png" width="96" alt="GoodBuddy" />
|
||||
|
||||
# GoodBuddy
|
||||
|
||||
**模型对话、编程 Agent、本地知识库和定时任务的桌面入口**
|
||||
|
||||
[English](./README.md) | **简体中文**
|
||||
|
||||
[产品官网](https://mesalogo.github.io/goodbuddy/) ·
|
||||
[下载](https://github.com/mesalogo/goodbuddy/releases) ·
|
||||
[功能矩阵](./FEATURES.zh-CN.md) ·
|
||||
[构建说明](./BUILD.md)
|
||||
|
||||
[](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。
|
||||
- **执行边界明确**:`Ask` 保持只读,`Execute` 使用当前账号权限,工具调用、取消、超时和 Token 用量均有记录。
|
||||
- **支持信创软硬件**:正式 Linux 版本覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境;龙芯 LoongArch 提供独立 `loong64` 实验预览版。
|
||||
- **知识与长期工作一体化**:知识库、知识图谱、魔法笔记、任务和智能心跳共享同一个桌面工作空间。
|
||||
|
||||
信创支持按操作系统、指令集和安装包兼容性描述,不代表已完成所有厂商、整机和系统版本的兼容认证。龙芯版本尚未进入正式发布矩阵,具体限制见[龙芯预览版说明](./docs/development/loongarch-preview-build.md)。
|
||||
|
||||
## 主要能力
|
||||
|
||||
| 范围 | GoodBuddy 提供的能力 |
|
||||
| --- | --- |
|
||||
| Agent Runtime | 直连模型、OpenCode、Continue 和预览版 DeepSeek Harness |
|
||||
| 数据与执行 | 本地 SQLite、只读 `Ask`、使用当前账号完整权限的 `Execute`、工具调用记录 |
|
||||
| 知识库 | 导入文件、目录和网页,通过全文、中文词组、向量和知识图谱检索 |
|
||||
| 桌面版本 | Windows、macOS、Linux 的 `x64` 与 `arm64` 正式版本,以及龙芯 LoongArch 实验预览版 |
|
||||
| 外部连接 | 本机或内网模型服务、自定义 MCP、微信 ClawBot、企业微信和钉钉 |
|
||||
|
||||
## 产品界面
|
||||
|
||||
### 知识库与知识图谱
|
||||
|
||||

|
||||
|
||||

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

|
||||
|
||||

|
||||
|
||||
## Agent Runtime
|
||||
|
||||
| Runtime | 适用场景 | 配置来源 |
|
||||
| --- | --- | --- |
|
||||
| 直连模型 | 对话、知识检索、图片生成和内置工具 | GoodBuddy 模型连接 |
|
||||
| OpenCode | 编程 Agent、原生 Commands、Tools 和 Skills | GoodBuddy 管理或 OpenCode 自有配置 |
|
||||
| Continue | Rules、Prompt 预设和编程任务 | GoodBuddy 管理或 Continue 自有配置 |
|
||||
| DeepSeek Harness(预览) | 固定 Host 与可选插件能力 | GoodBuddy 管理的 OpenAI 兼容连接 |
|
||||
|
||||
每次运行都会记录取消、超时、Token 用量、工具调用和执行历史。各 Runtime 的原生能力与配置仍分别管理。
|
||||
|
||||
## 下载
|
||||
|
||||
从 [GitHub Releases](https://github.com/mesalogo/goodbuddy/releases) 下载:
|
||||
|
||||
| 系统 | 架构 | 格式 |
|
||||
| --- | --- | --- |
|
||||
| Windows | `x64`、`arm64` | NSIS、便携 ZIP |
|
||||
| macOS | `x64`、`arm64` | DMG、ZIP |
|
||||
| Linux | `x64`、`arm64` | AppImage、DEB、RPM |
|
||||
|
||||
龙芯 LoongArch 使用独立的 `loong64` 实验预览通道,可在[产品官网](https://mesalogo.github.io/goodbuddy/#download)选择下载。
|
||||
|
||||
桌面安装包不包含远端 Agent payload。本地项目无需 Agent 包;托管 SSH 用户可在设置中单独下载或导入。
|
||||
|
||||
当前未配置代码签名和 macOS notarization,操作系统可能显示安全提示。
|
||||
|
||||
## 从源码运行
|
||||
|
||||
需要 Node.js 24 和 npm:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mesalogo/goodbuddy.git
|
||||
cd goodbuddy
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
构建与打包说明见 [BUILD.md](./BUILD.md)。
|
||||
|
||||
## 文档
|
||||
|
||||
- [功能矩阵与路线图](./FEATURES.zh-CN.md)
|
||||
- [产品功能文档](./docs/features/)
|
||||
- [UI 设计系统](./UI-DESIGN.md)
|
||||
- [构建与打包](./BUILD.md)
|
||||
- [发布流程](./docs/development/release-runbook.md)
|
||||
|
||||
## 隐私与执行边界
|
||||
|
||||
- 模型请求只发送到用户选择的服务。
|
||||
- 本地数据默认保存在系统应用数据目录,API Key 由系统安全存储加密。
|
||||
- Renderer 不接触原始 Electron API 或模型凭据。
|
||||
- DeepSeek Harness 插件市场默认关闭。第三方插件安装脚本、初始化代码和 Execute 工具以当前用户权限运行。
|
||||
- 远程委派只在用户配置端点和令牌后启用。
|
||||
- 内网兼容模式允许应用内 HTTP 和非标准 HTTPS 证书;微信凭据和媒体端点仍执行严格校验。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request。请先阅读 [AGENTS.md](./AGENTS.md),提交前运行:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## 社区交流
|
||||
|
||||
扫描二维码加入 GoodBuddy 微信群:
|
||||
|
||||
<img width="1250" height="1194" alt="9627ce196b0de42f123c33dd9dea1568" src="https://github.com/user-attachments/assets/ff87dfac-d72f-47b7-ac9a-c88b0b4c96d9" />
|
||||
|
||||
|
||||
## 开源许可
|
||||
|
||||
GoodBuddy 原创代码采用 [0BSD License](./LICENSE),可自由使用、修改、分发和商用。第三方组件和资源遵循各自许可证。
|
||||
+87
-21
@@ -193,7 +193,7 @@
|
||||
|
||||
### 6.1 PageTabs
|
||||
|
||||
用于同一一级页面内的同级内容面板,例如智能心跳的“成长概览”和“心跳计划”。
|
||||
用于同一一级页面内的同级内容面板,例如智能心跳的“运行概览”和“心跳计划”。
|
||||
|
||||
- 使用 `tablist`、`tab` 和 `tabpanel` 语义,当前项使用 `aria-selected="true"`。
|
||||
- 一级页面之间的导航由应用主导航承担,不复用 `PageTabs`。
|
||||
@@ -201,6 +201,7 @@
|
||||
- 项目过多时优先重组信息架构,不把一级页签做成多行。
|
||||
- `PageTabs` 可使用默认视觉或共享的 `segmented` 视觉变体。紧凑主从工作台中的 2 至 4 个同级面板可使用与模型设置一致的分段外观,但不得因此改用按钮组语义。
|
||||
- 视觉变体不改变组件含义:分段外观的 `PageTabs` 仍使用页签语义、单一激活面板、游标焦点和方向键切换,不复制页面专属样式。
|
||||
- 在独立滚动的 Flex 页面中,`PageTabs` 及其直接包装层必须保持 `flex: 0 0 auto`,不得被剩余内容压缩到零高度。新增页签容器时必须提供对应的 CSS 回归测试。
|
||||
|
||||
### 6.2 SegmentedControl
|
||||
|
||||
@@ -282,6 +283,9 @@
|
||||
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
|
||||
- 当前导航项和当前会话必须同时使用至少三种信号中的两种:强调背景、可见边框、图标或文字强调。浅色主题的当前项优先使用更完整的蓝色选中表面和较高字重。
|
||||
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
|
||||
- 桌面停靠状态下,主侧栏宽度允许在 `220px` 至 `420px` 范围内通过指针或键盘调整,
|
||||
并记住用户选择;分隔条使用 `separator` 语义,支持方向键、Home 和 End。小于
|
||||
`900px` 时继续使用固定宽度抽屉,不显示拖拽分隔条。
|
||||
- 关联 Task 的 Conversation 在行最左侧显示独立展开按钮,父会话行不重复任务标签或数量;
|
||||
展开后的每个 Task 子项左侧显示名称,模式、计划和共享状态标记收在右侧,与父会话的
|
||||
更新时间位置一致;展开层级只到 Task,不展示 Job、Subjob 或 Run。
|
||||
@@ -305,26 +309,61 @@
|
||||
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
|
||||
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
|
||||
单行截断,不能撑宽或挤压侧栏。
|
||||
- 账户与设置入口固定在侧栏底部。已有稳定设置入口时,不在顶栏重复提供同一入口。
|
||||
- 侧栏底部固定显示文件、终端、浏览器和资源监控四个系统工具图标,设置按钮独立靠右。工具
|
||||
图标只负责打开或聚焦助手工作栏面板,不在侧栏复制工具内容。已有稳定设置入口时,不在顶栏
|
||||
重复提供同一入口。
|
||||
|
||||
### 6.9 助手工作栏
|
||||
|
||||
助手工作栏是应用级右侧工具容器,不归属于聊天页面,也不根据当前页面、项目或 Runtime
|
||||
自动增删入口。产品契约见
|
||||
[通用助手工作栏与执行空间 PRD](./docs/prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
|
||||
[通用助手工作栏与执行空间 PRD](./docs/features/assistant-workbar/prd.md),
|
||||
动态 Tab、“+”和终端细节见
|
||||
[助手工作栏多终端页签 PRD](./docs/features/assistant-workbar/terminal-tabs-prd.md)。
|
||||
|
||||
- 默认固定提供任务中心、工作区、浏览器和成果四个标准栏目,不根据当前页面或
|
||||
Runtime 能力自动增删入口。
|
||||
- Task Center 是 Task 的单例应用级索引,不使用“跟随 / 固定目标”多实例模式。每个 Task
|
||||
只关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,
|
||||
也不得把 Job/Run 提升为可导航 UI 对象。
|
||||
- 升级后默认打开任务中心、工作区、一个浏览器实例和成果。任务中心与工作区是不可关闭的
|
||||
单实例基础应用;浏览器是可关闭的多实例应用;成果保持可关闭的单实例应用。不可关闭不
|
||||
等于锁定顺序。
|
||||
- 资源监控作为本机单实例应用保留在能力目录,但不加入升级后的默认打开实例。面板只在可见
|
||||
活动时采样,首期不持久化历史或提供后台负载告警。
|
||||
- Tab 行只显示已打开实例,行末始终提供“+”普通按钮;点击后在内容区打开应用选择页。
|
||||
“+”不使用 `tab` 语义,也不能因为终端或其他应用已经打开而消失。
|
||||
- 应用注册表分别声明单实例或多实例、默认打开、必须存在、可关闭和排序策略。在工作栏“+”目录中,
|
||||
单实例重复选择时聚焦已有 Tab;浏览器和终端每次选择创建独立实例。主侧栏系统工具快捷
|
||||
入口按当前上下文聚焦最近使用的匹配实例,额外实例仍从“+”目录创建。
|
||||
- Task Center 是默认跟随当前项目的单实例索引,不使用多实例模式。用户可在同一 Tab 内切换
|
||||
“当前项目”“全局任务”和“全部项目”;范围变化不得创建第二个 Task Center。每个 Task 只
|
||||
关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,也
|
||||
不得把 Job/Run 提升为可导航 UI 对象。
|
||||
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
|
||||
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。
|
||||
- 用户可以主动排序或隐藏栏目,并可恢复默认布局;应用不能用用户偏好机制实现自动能力裁剪。
|
||||
- 四个栏目使用稳定标签并保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
|
||||
- 用户可以主动排序栏目,并可关闭可关闭实例或恢复默认布局;任务中心和工作区不能隐藏或
|
||||
关闭。应用不能用用户偏好机制实现自动能力裁剪。
|
||||
- 已打开实例保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
|
||||
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。不可关闭实例不渲染
|
||||
关闭按钮,也不响应关闭快捷命令。
|
||||
- 当前工作栏 Tab 必须同时使用连续内容表面、可见边框、顶部指示、图标或较高字重中的至少
|
||||
两种信号;悬停反馈不得强于当前选中态。Tab 关闭按钮保留固定布局空间,默认仅在对应 Tab 悬停或
|
||||
`focus-within` 时显示;无悬停或粗指针设备始终显示,键盘与辅助技术始终可访问。
|
||||
- 工作栏 Tab、Tab 关闭按钮和“+”入口沿用共享控件圆角;终端面板外框及终端顶部工具栏按钮
|
||||
使用直角,形成紧凑的停靠式终端界面。
|
||||
- 选择“终端”后直接绑定当前项目,由项目执行空间自动决定本机或托管 SSH;不再显示第二层
|
||||
目标选择器。终端面板内部不重复提供关闭会话按钮,关闭统一由 Tab 入口完成。
|
||||
- 下一次请求使用的附件和知识库由会话输入区展示与管理,不在工作栏重复提供“上下文”栏目;
|
||||
记忆功能实现前不展示占位内容,未来的记忆与历史执行上下文归入关联 Task 的任务中心详情。
|
||||
- 徽标可以提示未解决意见、等待审批、失败或连接状态,但不能成为唯一状态信号,也不能无条件抢占当前栏目。
|
||||
- 浏览器 Tab 的内容顶部固定使用一条紧凑工具栏,按“返回、刷新/停止加载、地址输入、
|
||||
前往、交互、关闭浏览器”排列。“刷新”在当前页面加载时原位切换为“停止加载”;
|
||||
“关闭浏览器”必须使用明确文字和与页面导航控件清楚区分的危险操作视觉,不能继续标为
|
||||
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
|
||||
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
|
||||
尚未提交的文字改变 Agent 看到的页面。
|
||||
- 浏览器工具栏操作的是该实例绑定的 Conversation 和 Browser Tab。同一 Conversation 的多个
|
||||
Tab 共享隔离登录上下文,但页面、导航、操作队列和元素引用相互独立。模型请求开始时冻结
|
||||
工具目标,用户切换工作栏 Tab 不改变进行中的 Agent 操作。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
|
||||
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
|
||||
不释放 Runtime 上下文。没有会话的活动 Conversation 可以直接在地址栏输入 URL 并“前往”
|
||||
创建会话,但必须复用 Agent 浏览器相同的 URL 策略与能力路径。
|
||||
- 所有窗口宽度下均停靠在主工作区右侧并参与水平布局回流;展开时压缩主工作区,不能覆盖、
|
||||
遮罩或隔离其内容。分栏计算排除最左侧主导航,主工作区与工作栏采用相同的 `300px`
|
||||
最小宽度;拖动保存两者在可用分栏宽度中的比例,任一侧到达最小值后都不得继续侵占。
|
||||
@@ -356,7 +395,9 @@
|
||||
对齐。该栏不重复放置新建入口。
|
||||
- “新建任务”只位于工作栏“任务索引”标题后方;任务状态筛选占满工作栏可用宽度,四个选项
|
||||
等分,不能保留内容宽度的短控件。
|
||||
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。
|
||||
- Runtime 状态、同步状态等短标签使用 `--font-caption`,不得放大为正文标题。顶栏 Runtime
|
||||
状态由状态点和 Runtime/模型名称紧凑表达:就绪使用绿色,连接中使用黄色,不可用使用灰色,
|
||||
不再重复显示状态文字;完整状态详情继续通过可访问说明和工具提示提供。
|
||||
- 浅色与深色切换属于持续可用的窗口级操作,直接显示太阳或月亮图标,并通过可访问名称说明将切换到的主题。选择必须持久化,切换不得改变布局。
|
||||
- 顶栏只直接显示当前任务所需的高频操作。已有侧栏账户设置入口时,不再重复显示 Runtime/设置入口;使用帮助优先放在相关操作附近,而不是为单个帮助项创建“更多”菜单。
|
||||
- 助手工作栏开关固定在上下文栏最右侧并与窗口关闭按钮纵向对齐;展开后该开关保持原位并
|
||||
@@ -376,8 +417,11 @@
|
||||
- 支持上、下方向键、Home、End、Enter 或 Space、Escape;打开后焦点进入当前项,关闭后返回触发按钮。
|
||||
- 点击或聚焦菜单外部时关闭;同一输入区内的模型、专家和模式菜单互斥展开。
|
||||
- 不可用选项保持可读并说明原因,键盘导航不得停留在不可选择项上。
|
||||
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称和项目类型,并按数据可用性补充本地目录、远程来源、平台、连接状态或其他能帮助辨认项目的信息。
|
||||
- 项目菜单区分“本地项目 / 远程项目 / 远程通道”,不把消息通道命名为远程项目。“远程项目(技术预览)”关闭时完整移除远程项目分组,而不是显示不可操作的残留选项;本地项目和远程通道继续可用。分组、项目类型和连接状态均须显示文字,不只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
|
||||
- 当某个选项需要用户在其展开区域内完成路径选择、授权或其他前置操作时,不得因为前置条件
|
||||
尚未完成而禁用该入口。选择入口只展开流程,验证成功前不提交实际值;不得自动采用第一个
|
||||
候选。取消保持原值,验证错误在对应操作附近显示。
|
||||
- 项目选择器属于富信息单选菜单。收起时保持当前项目名称紧凑可见;展开后每项至少显示项目名称,并按数据可用性补充目录、远程来源或其他能帮助辨认项目的信息。托管 SSH 项目在“远程项目”内按已保存 SSH Host 做一层轻量分组,Host 标题旁显示真实 Agent 连接状态;项目副标题只显示远端路径,不重复“托管 SSH”类型。
|
||||
- 项目菜单区分“本地项目 / 远程项目 / 远程通道”,不把消息通道命名为远程项目。“远程项目(技术预览)”关闭时完整移除远程项目分组,而不是显示不可操作的残留选项;本地项目和远程通道继续可用。Agent 连接状态使用“未连接 / 连接中 / 就绪 / 异常”等文字标签,读取状态不得发起连接,且不能只依赖图标或颜色;新增项目来源时不得退回仅列名称的原生 `select`。
|
||||
|
||||
### 6.12 应用通知与就地反馈
|
||||
|
||||
@@ -542,7 +586,19 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
### 13.1 聊天
|
||||
|
||||
- 使用 `reading` 壳层,消息流与输入区共享宽度。
|
||||
- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐,
|
||||
右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。
|
||||
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
|
||||
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
|
||||
原文,不包含推理过程、工具日志或引用元数据,结果通过应用通知反馈。流式回复完成前不显示
|
||||
复制入口。
|
||||
- 默认开启“会话 HTML 渲染”时,已完成 Agent 回复中的完整 HTML 与 `html` / `htm`
|
||||
代码块在原位置显示隔离静态预览;用户消息、推理过程和流式中的回复不得执行 HTML。
|
||||
每个预览持续标注脚本和网络已禁用,并在标题右侧复用与 Mermaid 图表相同的图标按钮组:
|
||||
源码按钮使用代码图标并暴露 `aria-expanded`,全屏按钮使用放大图标打开统一 Modal。
|
||||
全屏预览遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape 关闭和焦点恢复规则,
|
||||
并沿用同一套清理与 Sandbox/CSP 边界。关闭平台功能后必须显示 HTML 源码,不能隐藏内容
|
||||
或保留旧预览。
|
||||
- 本地会话的更多操作提供“在新会话中继续”。创建前必须先持久化当前稳定消息;有活动运行
|
||||
或待发送内容时不可创建。新会话复制聊天内容与当前 Runtime/知识检索选择,重置压缩状态,
|
||||
使用新的会话和消息 ID,且不复制 Task、队列、运行、记忆或成果归属。
|
||||
@@ -565,6 +621,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
|
||||
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
|
||||
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
|
||||
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
|
||||
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
|
||||
排除在可滚动范围之外。
|
||||
|
||||
### 13.2 最近对话
|
||||
|
||||
@@ -594,9 +653,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 范围明确区分 Global 与指定的一个或多个 Project;Global 与指定项目互斥,多项目选择使用
|
||||
Checkbox,不能依靠进入页面时的当前项目推断。
|
||||
- 状态卡片使用统一状态令牌,不只依赖颜色。
|
||||
- 保留“成长概览 / 待处理建议 / 心跳轨迹 / 心跳计划”四个同级页面。
|
||||
- 保留“运行概览 / 待处理建议 / 报告与记录 / 心跳计划”四个同级页面。
|
||||
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
|
||||
- “未来分区记忆”仅为长期方向,数据、状态和页面尚未设计,不得显示占位入口。
|
||||
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
|
||||
|
||||
### 13.5 Task Center
|
||||
|
||||
@@ -610,8 +669,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
成果查看器,不撑高列表,也不显示 Job/Run 树。
|
||||
- “新建定制任务”使用共享 Modal,明确选择当前或新 Conversation。聊天入口默认当前
|
||||
Conversation,Task Center 入口默认新 Conversation;选择必须持续可见。
|
||||
- 创建 Modal 默认 Execute,并允许主动切换 Ask。Execute 持续显示实际 Runtime、Project、
|
||||
工作目录、工具和审批摘要;不支持工具时不得静默降级。
|
||||
- 定时消息创建 Modal 使用关联 Conversation 的 Runtime 与 Ask/Execute 模式,不再提供独立
|
||||
模式开关。未运行计划的详情显示会话当前模式,已运行任务显示该次执行记录的模式;
|
||||
不得用旧计划字段表示下一次执行权限。不支持工具时不得静默降级。
|
||||
|
||||
### 13.6 运行记录
|
||||
|
||||
@@ -627,6 +687,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- “笔记 / 待办”属于同一工作台内的同级内容面板,使用 `PageTabs` 的 `segmented` 视觉变体,与模型设置的分段控件保持同一外观。
|
||||
- 页签切换保留 `tablist`、`tab` 和 `tabpanel` 语义;待办状态仍使用独立的 `SegmentedControl`,不得与内容页签合并。
|
||||
- 笔记编辑器保留 Quill 原生列表快捷输入:输入 `-`、`*`、`1.`、`[ ]` 或 `[x]` 后按空格,分别转换为项目符号、编号或待办清单。快捷输入只生成现有 Delta 富文本,不引入 Markdown 源码模式、第二套持久化格式或第三方 Markdown 插件;编辑区附近持续提供简短提示。
|
||||
- 左侧笔记与待办列表在双栏布局中允许在 `180px` 至 `360px` 范围内通过指针或键盘调整,
|
||||
也可从页面标题区隐藏和恢复;宽度与显示状态均记住用户选择。分隔条保留 `separator`
|
||||
语义及方向键、Home、End 操作,窄窗口堆叠布局不显示拖拽分隔条。
|
||||
- 左侧笔记列表依次显示标题、最多两行内容预览,以及同一行的记录数和本地化更新时间。记录数靠左、更新时间靠右,窄宽度下均保持单行省略,不挤压标题或扩大列表栏。
|
||||
- 左侧“魔法笔记”入口可显示未完成待办数量。数量为 0 时隐藏徽标,超过 99 时显示 `99+`,可访问名称仍说明精确数量;勾选、取消勾选以及 Agent / MCP 对笔记清单的写入都必须刷新数量。
|
||||
- 未完成待办徽标的高度不得超过主导航图标;数量从 0 变为可见或再次隐藏时,主导航项行高和相邻菜单位置必须保持不变。
|
||||
@@ -650,14 +713,17 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- “模型连接”的认证字段按“认证方式 → 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 包在线下载,不显示底层服务商名称。
|
||||
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图,失败时保留草稿并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
|
||||
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
|
||||
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
|
||||
更新,保存失败时保留最后确认状态。
|
||||
- “平台功能”使用共享 `PageTabs` 区分“通用设置”“远程项目(技术预览)”和“魔法笔记”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。“魔法笔记”页提供默认开启的“显示未完成待办数量”持久 Switch,关闭后只隐藏左侧数量徽标,不改变待办数据或魔法笔记入口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
|
||||
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
|
||||
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
|
||||
- Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。
|
||||
- Agent Runtime 页面在低层程序与配置覆盖之外提供“能力与默认配置”区域。能力清单使用共享 `PageTabs`,按 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 类单行滚动展示,一次只呈现当前分类的 `tabpanel`;清单只显示 Runtime 自有能力,不混入 GoodBuddy 分配的 Skills、临时 MCP 或 Continue 预设。Tools 必须独立于 Commands、LSP 和 Formatters,显示工具类型、来源及 Ask/Execute 可用性;清单状态必须区分完整、部分、不可用、仅连接和不支持,不能用进程连通性冒充清单可读。
|
||||
- “能力与默认配置”只显示一个模块标题,刷新入口位于该标题右侧,能力状态压缩为一行并排在默认 Agent 或 Continue 预设编辑器之前;不得再复制“Runtime 原生能力”等同义标题、说明或状态结论。刷新只更新能力快照,不覆盖未保存的原生定制草稿。
|
||||
- OpenCode 的默认 Agent 使用原生下拉选择;Continue 预设编辑器允许管理名称、说明、启用的 Rules 以及 Prompt 名称、说明和正文,并可展开查看原生 Rules 与启用预设 Rules 的最终合并顺序。持久启停仍使用共享 Switch,添加与删除使用明确按钮和可访问名称。
|
||||
- MCP 设置按“内置 MCP / 直连模型 / 自定义 MCP / 电脑控制”四个同级 `PageTabs` 组织。直连模型中的联网搜索与内置浏览器使用一致的折叠卡片和独立总开关;内置浏览器必须明确说明其操作 GoodBuddy 隔离浏览器,不控制客户端已安装的浏览器,开启后可由 Execute 直接使用,不逐次询问。尚未生效的命名浏览器配置不得显示在界面中,“电脑控制”只显示实际操作客户端电脑的能力。内置 MCP 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
|
||||
- MCP 设置按“内置 MCP / 直连模型 / 自定义 MCP / 电脑控制”四个同级 `PageTabs` 组织。联网搜索保留在直连模型;内置浏览器归入内置 MCP,提供持久启停和 Runtime 分配,并明确说明其操作 GoodBuddy 隔离浏览器、不控制客户端已安装的浏览器,开启后可由已分配 Runtime 的 Execute 直接使用,不逐次询问。该开关只控制 Agent 工具访问,关闭时不得禁用浏览器工作栏中的用户手动导航、返回、刷新、停止加载、交互或关闭操作。尚未生效的命名浏览器配置不得显示在界面中,“电脑控制”只显示实际操作客户端电脑的能力。内置 MCP 卡片与 Skills 一样提供持久启停和 Runtime 分配;直连模型、GoodBuddy 管理的 OpenCode 与 Continue 默认选中且可调整,DeepSeek Harness 必须以置灰、未选择和“暂不支持”文案持续显示,不能呈现为可保存的分配。魔法笔记 MCP 的自身启停与平台功能依赖分别显示,依赖未开启时保留用户配置并说明当前不会加载。
|
||||
- MCP Server 测试结果在同一展开卡片中分组显示 Tools、Prompts 和 Resources 的支持状态、数量与有界元数据;Prompt 参数标明必填项,Resource 只显示 URI、名称、类型和说明,不读取或渲染 Resource 内容。
|
||||
|
||||
### 13.9 文档解析设置
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"agentVersion": "0.11.13",
|
||||
"agentVersion": "0.11.23",
|
||||
"protocol": {
|
||||
"major": 2,
|
||||
"minor": 0
|
||||
@@ -15,6 +15,12 @@
|
||||
"binaryPath": "node-v24.19.0-linux-x64/bin/node",
|
||||
"licensePath": "node-v24.19.0-linux-x64/LICENSE"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"archive": "node-v24.19.0-darwin-arm64.tar.gz",
|
||||
"sha256": "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d",
|
||||
"binaryPath": "node-v24.19.0-darwin-arm64/bin/node",
|
||||
"licensePath": "node-v24.19.0-darwin-arm64/LICENSE"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"archive": "node-v24.19.0-linux-arm64.tar.gz",
|
||||
"sha256": "d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const { readFileSync } = require('node:fs')
|
||||
const {
|
||||
detectBinaryArchitecture,
|
||||
detectElfArchitecture
|
||||
} = require('./binary-architecture.cjs')
|
||||
|
||||
function targetName(architecture, platform = 'linux') {
|
||||
if (
|
||||
!(platform === 'linux' && ['x64', 'arm64'].includes(architecture)) &&
|
||||
!(platform === 'darwin' && architecture === 'arm64')
|
||||
) {
|
||||
throw new Error(`Unsupported Agent build target: ${platform}-${architecture}`)
|
||||
}
|
||||
return `${platform}-${architecture}`
|
||||
}
|
||||
|
||||
function assertTargetBinary(filePath, architecture, platform, label) {
|
||||
targetName(architecture, platform)
|
||||
const header = readFileSync(filePath).subarray(0, 64)
|
||||
const isMachO = header.length >= 8 &&
|
||||
(header.readUInt32LE(0) === 0xfeedfacf ||
|
||||
header.readUInt32BE(0) === 0xfeedfacf)
|
||||
const actual = platform === 'linux'
|
||||
? detectElfArchitecture(header)
|
||||
: isMachO ? detectBinaryArchitecture(header) : undefined
|
||||
if (actual !== architecture) {
|
||||
throw new Error(
|
||||
`${label} architecture mismatch: expected ${platform}-${architecture}, received ${actual ?? 'unknown'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { targetName, assertTargetBinary }
|
||||
+94
-48
@@ -29,9 +29,11 @@ const {
|
||||
sep
|
||||
} = require('node:path')
|
||||
const tar = require('tar')
|
||||
const { detectElfArchitecture } = require('./binary-architecture.cjs')
|
||||
const {
|
||||
detectElfArchitecture
|
||||
} = require('./binary-architecture.cjs')
|
||||
targetName,
|
||||
assertTargetBinary
|
||||
} = require('./agent-build-target.cjs')
|
||||
const {
|
||||
preflightRegisteredProductionKey
|
||||
} = require('./signing-key-preflight.cjs')
|
||||
@@ -132,15 +134,8 @@ function readTrustedKeyRegistry(projectRoot = root) {
|
||||
return registry
|
||||
}
|
||||
|
||||
function targetName(arch) {
|
||||
if (!supportedArchitectures.includes(arch)) {
|
||||
throw new Error(`Unsupported Agent architecture: ${arch}`)
|
||||
}
|
||||
return `linux-${arch}`
|
||||
}
|
||||
|
||||
function lockedRuntimeInput(lock, arch, archivePath) {
|
||||
const target = targetName(arch)
|
||||
function lockedRuntimeInput(lock, arch, archivePath, platform = 'linux') {
|
||||
const target = targetName(arch, platform)
|
||||
const input = lock.node?.targets?.[target]
|
||||
if (
|
||||
!input ||
|
||||
@@ -177,24 +172,23 @@ function lockedRuntimeInput(lock, arch, archivePath) {
|
||||
function assertElfArchitecture(
|
||||
filePath,
|
||||
expectedArchitecture,
|
||||
description = 'Agent Node runtime'
|
||||
description = 'Agent Node runtime',
|
||||
platform = 'linux'
|
||||
) {
|
||||
const actual = detectElfArchitecture(readFileSync(filePath).subarray(0, 64))
|
||||
if (actual !== expectedArchitecture) {
|
||||
throw new Error(
|
||||
`${description} architecture mismatch: expected ${expectedArchitecture}, received ${actual ?? 'unknown'}`
|
||||
)
|
||||
}
|
||||
assertTargetBinary(filePath, expectedArchitecture, platform, description)
|
||||
}
|
||||
|
||||
function koffiNativePackageName(arch) {
|
||||
return `@koromix/koffi-linux-${arch}`
|
||||
function koffiNativePackageName(arch, platform = 'linux') {
|
||||
return `@koromix/koffi-${targetName(arch, platform)}`
|
||||
}
|
||||
|
||||
function koffiPayloadPaths(arch) {
|
||||
function koffiPayloadPaths(arch, platform = 'linux') {
|
||||
const packageRoot = `lib/node_modules/${koffiPackageName}`
|
||||
const nativeRoot =
|
||||
`lib/node_modules/${koffiNativePackageName(arch)}`
|
||||
`lib/node_modules/${koffiNativePackageName(arch, platform)}`
|
||||
const native = platform === 'linux'
|
||||
? [`${nativeRoot}/linux_${arch}/koffi.node`, `${nativeRoot}/musl_${arch}/koffi.node`]
|
||||
: [`${nativeRoot}/darwin_${arch}/koffi.node`]
|
||||
return {
|
||||
packageRoot,
|
||||
nativeRoot,
|
||||
@@ -205,13 +199,9 @@ function koffiPayloadPaths(arch) {
|
||||
`${packageRoot}/src/koffi/src/static.js`,
|
||||
`${nativeRoot}/package.json`,
|
||||
`${nativeRoot}/index.js`,
|
||||
`${nativeRoot}/linux_${arch}/koffi.node`,
|
||||
`${nativeRoot}/musl_${arch}/koffi.node`
|
||||
...native
|
||||
],
|
||||
native: [
|
||||
`${nativeRoot}/linux_${arch}/koffi.node`,
|
||||
`${nativeRoot}/musl_${arch}/koffi.node`
|
||||
]
|
||||
native
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +318,20 @@ function createManifest(bundleDirectory, metadata) {
|
||||
spdx: 'MIT',
|
||||
path: 'licenses/zod-MIT.txt'
|
||||
},
|
||||
...(files.some(
|
||||
(file) =>
|
||||
file.path ===
|
||||
'licenses/agent-client-protocol-Apache-2.0.txt'
|
||||
)
|
||||
? [
|
||||
{
|
||||
package: '@agentclientprotocol/sdk',
|
||||
version: metadata.acpSdkVersion,
|
||||
spdx: 'Apache-2.0',
|
||||
path: 'licenses/agent-client-protocol-Apache-2.0.txt'
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
package: koffiPackageName,
|
||||
version: metadata.koffiVersion,
|
||||
@@ -345,7 +349,7 @@ function createManifest(bundleDirectory, metadata) {
|
||||
formatVersion: 1,
|
||||
product: 'GoodBuddy',
|
||||
agentVersion: metadata.agentVersion,
|
||||
platform: 'linux',
|
||||
platform: metadata.platform ?? 'linux',
|
||||
arch: metadata.arch,
|
||||
protocol: metadata.protocol,
|
||||
signingKeyId: metadata.signingKeyId,
|
||||
@@ -438,7 +442,10 @@ function validateManifestShape(manifest, expected) {
|
||||
manifest.product !== 'GoodBuddy' ||
|
||||
typeof manifest.agentVersion !== 'string' ||
|
||||
!semanticVersionPattern.test(manifest.agentVersion) ||
|
||||
manifest.platform !== 'linux' ||
|
||||
!(
|
||||
manifest.platform === 'linux' ||
|
||||
(manifest.platform === 'darwin' && manifest.arch === 'arm64')
|
||||
) ||
|
||||
!supportedArchitectures.includes(manifest.arch) ||
|
||||
!Number.isSafeInteger(manifest.protocol?.major) ||
|
||||
!Number.isSafeInteger(manifest.protocol?.minor) ||
|
||||
@@ -603,7 +610,7 @@ function verifyBundleDirectory(bundleDirectory, options = {}) {
|
||||
throw new Error(`Agent entrypoint payload is missing: ${required}`)
|
||||
}
|
||||
}
|
||||
const koffiPaths = koffiPayloadPaths(manifest.arch)
|
||||
const koffiPaths = koffiPayloadPaths(manifest.arch, manifest.platform)
|
||||
for (const required of koffiPaths.required) {
|
||||
if (!declaredPaths.has(required)) {
|
||||
throw new Error(`Agent Koffi payload is missing: ${required}`)
|
||||
@@ -612,7 +619,7 @@ function verifyBundleDirectory(bundleDirectory, options = {}) {
|
||||
const koffiLicense = manifest.licenses.find(
|
||||
(license) => license.package === koffiPackageName
|
||||
)
|
||||
const nativePackage = koffiNativePackageName(manifest.arch)
|
||||
const nativePackage = koffiNativePackageName(manifest.arch, manifest.platform)
|
||||
const nativeLicense = manifest.licenses.find(
|
||||
(license) => license.package === nativePackage
|
||||
)
|
||||
@@ -627,13 +634,16 @@ function verifyBundleDirectory(bundleDirectory, options = {}) {
|
||||
}
|
||||
assertElfArchitecture(
|
||||
join(bundleDirectory, manifest.entrypoint.runtimePath),
|
||||
manifest.arch
|
||||
manifest.arch,
|
||||
'Agent Node runtime',
|
||||
manifest.platform
|
||||
)
|
||||
for (const nativePath of koffiPaths.native) {
|
||||
assertElfArchitecture(
|
||||
join(bundleDirectory, ...nativePath.split('/')),
|
||||
manifest.arch,
|
||||
'Agent Koffi native binding'
|
||||
'Agent Koffi native binding',
|
||||
manifest.platform
|
||||
)
|
||||
}
|
||||
return {
|
||||
@@ -654,7 +664,8 @@ function assertReplaceableBundle(directory) {
|
||||
if (
|
||||
manifest.formatVersion === 1 &&
|
||||
manifest.product === 'GoodBuddy' &&
|
||||
manifest.platform === 'linux'
|
||||
(manifest.platform === 'linux' ||
|
||||
(manifest.platform === 'darwin' && manifest.arch === 'arm64'))
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -703,13 +714,13 @@ function copyRegularFile(source, destination) {
|
||||
chmodSync(destination, 0o644)
|
||||
}
|
||||
|
||||
function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
|
||||
function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion, platform = 'linux') {
|
||||
const sourceRoot = join(projectRoot, 'node_modules', koffiPackageName)
|
||||
const packageMetadata = readJson(
|
||||
join(sourceRoot, 'package.json'),
|
||||
'Koffi package metadata'
|
||||
)
|
||||
const nativePackage = koffiNativePackageName(arch)
|
||||
const nativePackage = koffiNativePackageName(arch, platform)
|
||||
const nativeSourceRoot = join(
|
||||
projectRoot,
|
||||
'node_modules',
|
||||
@@ -725,7 +736,7 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
|
||||
nativeMetadata.name !== nativePackage ||
|
||||
nativeMetadata.version !== lockedVersion ||
|
||||
!Array.isArray(nativeMetadata.os) ||
|
||||
!nativeMetadata.os.includes('linux') ||
|
||||
!nativeMetadata.os.includes(platform) ||
|
||||
!Array.isArray(nativeMetadata.cpu) ||
|
||||
!nativeMetadata.cpu.includes(arch)
|
||||
) {
|
||||
@@ -734,10 +745,13 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
|
||||
)
|
||||
}
|
||||
|
||||
const paths = koffiPayloadPaths(arch)
|
||||
const paths = koffiPayloadPaths(arch, platform)
|
||||
const files = [
|
||||
['package.json', `${paths.packageRoot}/package.json`],
|
||||
['index.js', `${paths.packageRoot}/index.js`],
|
||||
['index.cjs', `${paths.packageRoot}/index.cjs`],
|
||||
['src/koffi/index.cjs', `${paths.packageRoot}/src/koffi/index.cjs`],
|
||||
['src/koffi/src/static.cjs', `${paths.packageRoot}/src/koffi/src/static.cjs`],
|
||||
[
|
||||
'src/koffi/index.js',
|
||||
`${paths.packageRoot}/src/koffi/index.js`
|
||||
@@ -756,8 +770,7 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
|
||||
for (const source of [
|
||||
'package.json',
|
||||
'index.js',
|
||||
`linux_${arch}/koffi.node`,
|
||||
`musl_${arch}/koffi.node`
|
||||
...paths.native.map((path) => path.slice(paths.nativeRoot.length + 1))
|
||||
]) {
|
||||
const destination = `${paths.nativeRoot}/${source}`
|
||||
copyRegularFile(
|
||||
@@ -769,7 +782,8 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
|
||||
assertElfArchitecture(
|
||||
join(staging, ...nativePath.split('/')),
|
||||
arch,
|
||||
'Agent Koffi native binding'
|
||||
'Agent Koffi native binding',
|
||||
platform
|
||||
)
|
||||
}
|
||||
return {
|
||||
@@ -781,11 +795,13 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
|
||||
function buildAgentBundle(options) {
|
||||
const projectRoot = options.projectRoot ?? root
|
||||
const arch = options.arch
|
||||
const platform = options.platform ?? 'linux'
|
||||
const lock = options.lock ?? readRuntimeLock(projectRoot)
|
||||
const runtimeInput = lockedRuntimeInput(
|
||||
lock,
|
||||
arch,
|
||||
options.runtimeArchive
|
||||
options.runtimeArchive,
|
||||
platform
|
||||
)
|
||||
const registry =
|
||||
options.registry ?? readTrustedKeyRegistry(projectRoot)
|
||||
@@ -829,13 +845,13 @@ function buildAgentBundle(options) {
|
||||
|
||||
const destination =
|
||||
options.outputDirectory ??
|
||||
join(projectRoot, '.agent-resources', targetName(arch))
|
||||
join(projectRoot, '.agent-resources', targetName(arch, platform))
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
const staging = mkdtempSync(
|
||||
join(dirname(destination), `.stage-${targetName(arch)}-`)
|
||||
join(dirname(destination), `.stage-${targetName(arch, platform)}-`)
|
||||
)
|
||||
const runtimeStaging = mkdtempSync(
|
||||
join(tmpdir(), `goodbuddy-node-${targetName(arch)}-`)
|
||||
join(tmpdir(), `goodbuddy-node-${targetName(arch, platform)}-`)
|
||||
)
|
||||
try {
|
||||
tar.x({
|
||||
@@ -858,7 +874,7 @@ function buildAgentBundle(options) {
|
||||
runtimeStaging,
|
||||
...runtimeInput.licensePath.split('/')
|
||||
)
|
||||
assertElfArchitecture(runtimeBinary, arch)
|
||||
assertElfArchitecture(runtimeBinary, arch, 'Agent Node runtime', platform)
|
||||
copyFileSync(runtimeBinary, join(staging, 'node'))
|
||||
chmodSync(join(staging, 'node'), 0o755)
|
||||
|
||||
@@ -898,7 +914,8 @@ function buildAgentBundle(options) {
|
||||
projectRoot,
|
||||
staging,
|
||||
arch,
|
||||
lock.koffi.version
|
||||
lock.koffi.version,
|
||||
platform
|
||||
)
|
||||
const launcher = [
|
||||
'#!/bin/sh',
|
||||
@@ -923,6 +940,20 @@ function buildAgentBundle(options) {
|
||||
join(projectRoot, 'node_modules', 'zod', 'LICENSE'),
|
||||
join(staging, 'licenses', 'zod-MIT.txt')
|
||||
)
|
||||
copyLicense(
|
||||
join(
|
||||
projectRoot,
|
||||
'node_modules',
|
||||
'@agentclientprotocol',
|
||||
'sdk',
|
||||
'LICENSE'
|
||||
),
|
||||
join(
|
||||
staging,
|
||||
'licenses',
|
||||
'agent-client-protocol-Apache-2.0.txt'
|
||||
)
|
||||
)
|
||||
copyLicense(
|
||||
join(projectRoot, 'node_modules', 'koffi', 'LICENSE.txt'),
|
||||
join(staging, 'licenses', 'koffi-MIT.txt')
|
||||
@@ -935,12 +966,24 @@ function buildAgentBundle(options) {
|
||||
join(projectRoot, 'node_modules', 'zod', 'package.json'),
|
||||
'zod package metadata'
|
||||
)
|
||||
const acpSdkPackage = readJson(
|
||||
join(
|
||||
projectRoot,
|
||||
'node_modules',
|
||||
'@agentclientprotocol',
|
||||
'sdk',
|
||||
'package.json'
|
||||
),
|
||||
'ACP SDK package metadata'
|
||||
)
|
||||
const manifest = createManifest(staging, {
|
||||
agentVersion: lock.agentVersion,
|
||||
nodeVersion: lock.node.version,
|
||||
zodVersion: zodPackage.version,
|
||||
acpSdkVersion: acpSdkPackage.version,
|
||||
koffiVersion: koffi.version,
|
||||
koffiNativePackage: koffi.nativePackage,
|
||||
platform,
|
||||
arch,
|
||||
protocol: lock.protocol,
|
||||
signingKeyId: signingIdentity.keyId
|
||||
@@ -965,6 +1008,7 @@ function buildAgentBundle(options) {
|
||||
? 'test'
|
||||
: 'production',
|
||||
expected: {
|
||||
platform,
|
||||
agentVersion: lock.agentVersion,
|
||||
arch,
|
||||
protocol: lock.protocol
|
||||
@@ -1034,6 +1078,7 @@ function verifyLockedBundle(bundleDirectory, arch, options = {}) {
|
||||
options.registry ?? readTrustedKeyRegistry(projectRoot),
|
||||
verificationEnvironment: options.verificationEnvironment ?? 'production',
|
||||
expected: {
|
||||
platform: options.platform ?? 'linux',
|
||||
agentVersion: lock.agentVersion,
|
||||
arch,
|
||||
protocol: lock.protocol
|
||||
@@ -1164,6 +1209,7 @@ function main(argv = process.argv.slice(2)) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
koffiPayloadPaths,
|
||||
assertElfArchitecture,
|
||||
assertSafeManifestPath,
|
||||
buildAgentBundle,
|
||||
|
||||
+38
-10
@@ -32,6 +32,10 @@ const {
|
||||
} = require('./zip-central-directory.cjs')
|
||||
|
||||
const root = resolve(__dirname, '..')
|
||||
function supportedTarget(platform, architecture) {
|
||||
return (platform === 'linux' && ['x64', 'arm64'].includes(architecture)) ||
|
||||
(platform === 'darwin' && architecture === 'arm64')
|
||||
}
|
||||
const catalogName = 'agent-catalog.json'
|
||||
const catalogSignatureName = 'agent-catalog.sig'
|
||||
const catalogSignatureDomain = Buffer.from(
|
||||
@@ -46,6 +50,7 @@ const maximumPackageEntries = 50_002
|
||||
const maximumPackageEntryBytes = 384 * 1024 * 1024
|
||||
const maximumExpandedPackageBytes = 1024 * 1024 * 1024
|
||||
const maximumMetadataBytes = 1024 * 1024
|
||||
const maximumDescriptorBytes = 4 * 1024 * 1024
|
||||
const maximumCentralDirectoryBytes = 32 * 1024 * 1024
|
||||
const maximumCatalogEntries = 200
|
||||
const windowsReservedNamePattern =
|
||||
@@ -136,7 +141,7 @@ function assertPackageDescriptor(descriptor) {
|
||||
!semanticVersionPattern.test(
|
||||
descriptor.minimumDesktopVersion ?? ''
|
||||
) ||
|
||||
descriptor.platform !== 'linux' ||
|
||||
!supportedTarget(descriptor.platform, descriptor.architecture) ||
|
||||
!['x64', 'arm64'].includes(descriptor.architecture) ||
|
||||
typeof descriptor.signingKeyId !== 'string' ||
|
||||
!/^sha256:[a-f0-9]{64}$/u.test(
|
||||
@@ -214,7 +219,7 @@ function readPackageMetadata(archivePath, registry) {
|
||||
)
|
||||
const expectedArchive =
|
||||
`goodbuddy-agent-${descriptor.version}` +
|
||||
`-linux-${descriptor.architecture}.gbagent`
|
||||
`-${descriptor.platform}-${descriptor.architecture}.gbagent`
|
||||
if (basename(archive) !== expectedArchive) {
|
||||
throw new Error(
|
||||
`Agent package archive must be named ${expectedArchive}`
|
||||
@@ -408,7 +413,10 @@ function streamPackageMetadata(archivePath) {
|
||||
entry.name === 'agent-package.json' ||
|
||||
entry.name === 'agent-package.sig'
|
||||
const size = entry.recordEnd - entry.dataOffset
|
||||
if (wanted && size > maximumMetadataBytes) {
|
||||
const maximum = entry.name === 'agent-package.json'
|
||||
? maximumDescriptorBytes
|
||||
: maximumMetadataBytes
|
||||
if (wanted && size > maximum) {
|
||||
throw new Error('Agent package metadata exceeds its limit')
|
||||
}
|
||||
return {
|
||||
@@ -566,11 +574,11 @@ function assertCatalog(catalog) {
|
||||
!semanticVersionPattern.test(
|
||||
entry.minimumDesktopVersion ?? ''
|
||||
) ||
|
||||
entry.platform !== 'linux' ||
|
||||
!supportedTarget(entry.platform, entry.architecture) ||
|
||||
!['x64', 'arm64'].includes(entry.architecture) ||
|
||||
entry.archive !==
|
||||
`goodbuddy-agent-${entry.version}` +
|
||||
`-linux-${entry.architecture}.gbagent` ||
|
||||
`-${entry.platform}-${entry.architecture}.gbagent` ||
|
||||
!Number.isSafeInteger(entry.size) ||
|
||||
entry.size <= 0 ||
|
||||
!sha256Pattern.test(entry.sha256 ?? '') ||
|
||||
@@ -600,7 +608,7 @@ function assertCatalog(catalog) {
|
||||
entry.remoteRuntime.protocol,
|
||||
'Remote Runtime protocol'
|
||||
)
|
||||
const identity = `${entry.version}:${entry.architecture}`
|
||||
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
|
||||
if (identities.has(identity)) {
|
||||
throw new Error('Agent catalog contains duplicate entries')
|
||||
}
|
||||
@@ -698,7 +706,8 @@ function createCatalog(options) {
|
||||
}
|
||||
const packages = [
|
||||
readPackageMetadata(options.x64Package, registry),
|
||||
readPackageMetadata(options.arm64Package, registry)
|
||||
readPackageMetadata(options.arm64Package, registry),
|
||||
...(options.darwinArm64Package ? [readPackageMetadata(options.darwinArm64Package, registry)] : [])
|
||||
]
|
||||
const [first, second] = packages
|
||||
const {
|
||||
@@ -721,12 +730,26 @@ function createCatalog(options) {
|
||||
canonicalJson(secondRuntimeIdentity) ||
|
||||
new Set(packages.map(
|
||||
(metadata) => metadata.descriptor.architecture
|
||||
)).size !== 2
|
||||
).slice(0, 2)).size !== 2 ||
|
||||
first.descriptor.platform !== 'linux' ||
|
||||
second.descriptor.platform !== 'linux'
|
||||
) {
|
||||
throw new Error(
|
||||
'Agent package matrix does not describe one coherent release'
|
||||
)
|
||||
}
|
||||
if (packages[2]) {
|
||||
const mac = packages[2].descriptor
|
||||
const { bundleDigest: _digest, ...runtimeIdentity } = mac.remoteRuntime
|
||||
void _digest
|
||||
if (
|
||||
mac.platform !== 'darwin' || mac.architecture !== 'arm64' ||
|
||||
mac.version !== first.descriptor.version ||
|
||||
mac.minimumDesktopVersion !== first.descriptor.minimumDesktopVersion ||
|
||||
canonicalJson(mac.agentProtocol) !== canonicalJson(first.descriptor.agentProtocol) ||
|
||||
canonicalJson(runtimeIdentity) !== canonicalJson(firstRuntimeIdentity)
|
||||
) throw new Error('Agent package matrix does not describe one coherent release')
|
||||
}
|
||||
const previous =
|
||||
options.previousCatalog && options.previousSignature
|
||||
? readVerifiedCatalog(
|
||||
@@ -745,11 +768,11 @@ function createCatalog(options) {
|
||||
}
|
||||
const entries = new Map()
|
||||
for (const entry of previous?.entries ?? []) {
|
||||
entries.set(`${entry.version}:${entry.architecture}`, entry)
|
||||
entries.set(`${entry.version}:${entry.platform}:${entry.architecture}`, entry)
|
||||
}
|
||||
for (const metadata of packages) {
|
||||
const entry = catalogEntry(metadata)
|
||||
const identity = `${entry.version}:${entry.architecture}`
|
||||
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
|
||||
const existing = entries.get(identity)
|
||||
if (
|
||||
existing &&
|
||||
@@ -764,6 +787,7 @@ function createCatalog(options) {
|
||||
const sortedEntries = [...entries.values()].sort(
|
||||
(left, right) =>
|
||||
compareVersions(right.version, left.version) ||
|
||||
left.platform.localeCompare(right.platform, 'en') ||
|
||||
left.architecture.localeCompare(right.architecture, 'en')
|
||||
)
|
||||
if (sortedEntries.length > maximumCatalogEntries) {
|
||||
@@ -813,6 +837,7 @@ function parseArguments(argv) {
|
||||
![
|
||||
'--x64-package',
|
||||
'--arm64-package',
|
||||
'--darwin-arm64-package',
|
||||
'--previous-catalog',
|
||||
'--previous-signature',
|
||||
'--catalog',
|
||||
@@ -852,6 +877,9 @@ function parseArguments(argv) {
|
||||
command,
|
||||
x64Package: resolve(options['--x64-package']),
|
||||
arm64Package: resolve(options['--arm64-package']),
|
||||
...(options['--darwin-arm64-package'] ? {
|
||||
darwinArm64Package: resolve(options['--darwin-arm64-package'])
|
||||
} : {}),
|
||||
...(options['--previous-catalog']
|
||||
? {
|
||||
previousCatalog: resolve(
|
||||
|
||||
@@ -24,6 +24,7 @@ const {
|
||||
assembleAgentPackage
|
||||
} = require('./agent-package.cjs')
|
||||
const { sha256File } = require('./file-hash.cjs')
|
||||
const { targetName } = require('./agent-build-target.cjs')
|
||||
|
||||
const ciSigningKeyId = 'goodbuddy-agent-ci-ephemeral'
|
||||
|
||||
@@ -34,6 +35,7 @@ function parseArguments(argv) {
|
||||
if (
|
||||
![
|
||||
'--arch',
|
||||
'--platform',
|
||||
'--node-archive',
|
||||
'--opencode-archive',
|
||||
'--output-directory',
|
||||
@@ -56,6 +58,8 @@ function parseArguments(argv) {
|
||||
if (!supportedArchitectures.includes(options.arch)) {
|
||||
throw new Error('--arch must be x64 or arm64')
|
||||
}
|
||||
const platform = options.platform ?? 'linux'
|
||||
targetName(options.arch, platform)
|
||||
for (const key of [
|
||||
'nodearchive',
|
||||
'opencodearchive',
|
||||
@@ -70,6 +74,7 @@ function parseArguments(argv) {
|
||||
}
|
||||
return {
|
||||
arch: options.arch,
|
||||
platform,
|
||||
nodeArchive: resolve(options.nodearchive),
|
||||
opencodeArchive: resolve(options.opencodearchive),
|
||||
outputDirectory: resolve(options.outputdirectory),
|
||||
@@ -77,6 +82,14 @@ function parseArguments(argv) {
|
||||
}
|
||||
}
|
||||
|
||||
function readCiLocks(platform = 'linux', architecture = 'x64') {
|
||||
const projectRoot = resolve(__dirname, '..')
|
||||
targetName(architecture, platform)
|
||||
const lock = readRuntimeLock(projectRoot)
|
||||
const runtimeLock = readRemoteRuntimeLock(projectRoot)
|
||||
return { lock, runtimeLock }
|
||||
}
|
||||
|
||||
function ephemeralSigningIdentity() {
|
||||
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
|
||||
return {
|
||||
@@ -97,8 +110,12 @@ function ephemeralSigningIdentity() {
|
||||
|
||||
async function buildCiAgentBundle(options) {
|
||||
const projectRoot = resolve(__dirname, '..')
|
||||
const lock = readRuntimeLock(projectRoot)
|
||||
const runtimeLock = readRemoteRuntimeLock(projectRoot)
|
||||
const platform = options.platform ?? 'linux'
|
||||
const target = targetName(options.arch, platform)
|
||||
if (process.platform !== platform || process.arch !== options.arch) {
|
||||
throw new Error(`Agent CI build requires a native ${target} host`)
|
||||
}
|
||||
const { lock, runtimeLock } = readCiLocks(platform, options.arch)
|
||||
const signing = ephemeralSigningIdentity()
|
||||
const firstArchiveRoot = resolve(
|
||||
join(
|
||||
@@ -124,6 +141,8 @@ async function buildCiAgentBundle(options) {
|
||||
force: true
|
||||
})
|
||||
buildAgentBundle({
|
||||
platform,
|
||||
lock,
|
||||
projectRoot,
|
||||
arch: options.arch,
|
||||
runtimeArchive: options.nodeArchive,
|
||||
@@ -132,6 +151,8 @@ async function buildCiAgentBundle(options) {
|
||||
testSigningIdentity: identity
|
||||
})
|
||||
const runtime = buildRuntimeBundle({
|
||||
platform,
|
||||
lock: runtimeLock,
|
||||
projectRoot,
|
||||
architecture: options.arch,
|
||||
runtimeArchive: options.opencodeArchive,
|
||||
@@ -141,6 +162,7 @@ async function buildCiAgentBundle(options) {
|
||||
})
|
||||
const assemble = (archive) =>
|
||||
assembleAgentPackage({
|
||||
platform,
|
||||
projectRoot,
|
||||
architecture: options.arch,
|
||||
minimumDesktopVersion:
|
||||
@@ -181,7 +203,31 @@ async function buildCiAgentBundle(options) {
|
||||
throw new Error('Built Agent native launch smoke failed')
|
||||
}
|
||||
|
||||
const nativeChecks = [
|
||||
[
|
||||
resolve(options.outputDirectory, 'node'),
|
||||
['-e', `const k = require(${JSON.stringify(resolve(options.outputDirectory, 'lib/node_modules/koffi'))}); if (k.sizeof('int') !== 4) process.exit(1)`]
|
||||
],
|
||||
[join(runtime.bundleDirectory, 'bin', 'opencode'), ['--version']]
|
||||
]
|
||||
for (const [executable, args] of nativeChecks) {
|
||||
const result = spawnSync(executable, args, {
|
||||
encoding: 'utf8',
|
||||
timeout: 30_000,
|
||||
windowsHide: true
|
||||
})
|
||||
if (
|
||||
result.error ||
|
||||
result.status !== 0 ||
|
||||
(args[0] === '--version' &&
|
||||
result.stdout.trim() !== runtimeLock.runtimes.opencode.version)
|
||||
) {
|
||||
throw new Error(`Built native dependency smoke failed: ${basename(executable)}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
platform,
|
||||
agentVersion: lock.agentVersion,
|
||||
remoteRuntimeVersion:
|
||||
runtimeLock.runtimes.opencode.version,
|
||||
@@ -214,5 +260,6 @@ if (require.main === module) {
|
||||
|
||||
module.exports = {
|
||||
buildCiAgentBundle,
|
||||
parseArguments
|
||||
parseArguments,
|
||||
readCiLocks
|
||||
}
|
||||
|
||||
+13
-3
@@ -19,6 +19,7 @@ const {
|
||||
} = require('node:fs')
|
||||
const { basename, dirname, join, relative, resolve, sep } = require('node:path')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { targetName } = require('./agent-build-target.cjs')
|
||||
const { crc32 } = require('node:zlib')
|
||||
const {
|
||||
buildAgentBundle,
|
||||
@@ -345,6 +346,8 @@ function createPackageArchive(
|
||||
function assembleAgentPackage(options) {
|
||||
const projectRoot = options.projectRoot ?? root
|
||||
const architecture = options.architecture
|
||||
const platform = options.platform ?? 'linux'
|
||||
const target = targetName(architecture, platform)
|
||||
if (
|
||||
!['x64', 'arm64'].includes(architecture) ||
|
||||
!semanticVersionPattern.test(options.minimumDesktopVersion ?? '') ||
|
||||
@@ -386,6 +389,7 @@ function assembleAgentPackage(options) {
|
||||
options.agentBundle,
|
||||
architecture,
|
||||
{
|
||||
platform,
|
||||
projectRoot,
|
||||
registry,
|
||||
lock: agentLock,
|
||||
@@ -395,6 +399,7 @@ function assembleAgentPackage(options) {
|
||||
}
|
||||
)
|
||||
const runtime = verifyRuntimeBundle(options.runtimeBundle, {
|
||||
platform,
|
||||
projectRoot,
|
||||
architecture,
|
||||
registry,
|
||||
@@ -405,14 +410,14 @@ function assembleAgentPackage(options) {
|
||||
})
|
||||
const expectedArchiveName =
|
||||
`goodbuddy-agent-${agent.manifest.agentVersion}` +
|
||||
`-linux-${architecture}.gbagent`
|
||||
`-${target}.gbagent`
|
||||
if (basename(options.output) !== expectedArchiveName) {
|
||||
throw new Error(
|
||||
`Agent package output must be named ${expectedArchiveName}`
|
||||
)
|
||||
}
|
||||
const staging = mkdtempSync(
|
||||
join(tmpdir(), `goodbuddy-agent-package-linux-${architecture}-`)
|
||||
join(tmpdir(), `goodbuddy-agent-package-${target}-`)
|
||||
)
|
||||
try {
|
||||
stagePackagePayload({
|
||||
@@ -431,7 +436,7 @@ function assembleAgentPackage(options) {
|
||||
component: 'agent',
|
||||
version: agent.manifest.agentVersion,
|
||||
minimumDesktopVersion: options.minimumDesktopVersion,
|
||||
platform: 'linux',
|
||||
platform,
|
||||
architecture,
|
||||
signingKeyId: signingIdentity.keyId,
|
||||
agentProtocol: agent.manifest.protocol,
|
||||
@@ -493,6 +498,7 @@ function buildAgentPackage(options) {
|
||||
)
|
||||
try {
|
||||
const agentBundle = buildAgentBundle({
|
||||
platform: options.platform ?? 'linux',
|
||||
projectRoot,
|
||||
arch: options.architecture,
|
||||
runtimeArchive: options.nodeArchive,
|
||||
@@ -504,6 +510,7 @@ function buildAgentPackage(options) {
|
||||
: { signingIdentity })
|
||||
})
|
||||
const runtime = buildRuntimeBundle({
|
||||
platform: options.platform ?? 'linux',
|
||||
projectRoot,
|
||||
architecture: options.architecture,
|
||||
runtimeArchive: options.runtimeArchive,
|
||||
@@ -544,6 +551,7 @@ function parseArguments(argv) {
|
||||
if (
|
||||
![
|
||||
'--arch',
|
||||
'--platform',
|
||||
'--minimum-desktop-version',
|
||||
'--node-archive',
|
||||
'--runtime-archive',
|
||||
@@ -571,10 +579,12 @@ function parseArguments(argv) {
|
||||
throw new Error('Agent package arguments are incomplete')
|
||||
}
|
||||
const common = {
|
||||
platform: options.platform ?? 'linux',
|
||||
architecture: options.arch,
|
||||
minimumDesktopVersion: options.minimumDesktopVersion,
|
||||
output: resolve(options.output)
|
||||
}
|
||||
targetName(common.architecture, common.platform)
|
||||
if (command === 'build') {
|
||||
if (!options.nodeArchive || !options.runtimeArchive) {
|
||||
throw new Error('Agent package build inputs are incomplete')
|
||||
|
||||
@@ -19,8 +19,8 @@ const manifestName = 'release-manifest.json'
|
||||
const targetDefinitions = [
|
||||
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
|
||||
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
|
||||
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
|
||||
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
|
||||
{ platform: 'macos', arch: 'x64', formats: ['dmg'] },
|
||||
{ platform: 'macos', arch: 'arm64', formats: ['dmg'] },
|
||||
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'] },
|
||||
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb', 'rpm'] }
|
||||
]
|
||||
|
||||
@@ -141,6 +141,13 @@ function portableRequiredPaths(directory) {
|
||||
'opencode',
|
||||
'opencode.exe'
|
||||
),
|
||||
join(
|
||||
directory,
|
||||
'resources',
|
||||
'runtimes',
|
||||
'opencode',
|
||||
'rg.exe'
|
||||
),
|
||||
join(
|
||||
directory,
|
||||
'resources',
|
||||
@@ -336,7 +343,7 @@ function main() {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
join(root, 'node_modules', 'electron-builder', 'cli.js'),
|
||||
join(root, 'build', 'run-electron-builder.cjs'),
|
||||
'--dir',
|
||||
'--x64',
|
||||
`--config.directories.output=${stagingRoot}`,
|
||||
|
||||
+72
-7
@@ -42,6 +42,10 @@ const {
|
||||
const {
|
||||
readZipCentralDirectory
|
||||
} = require('./zip-central-directory.cjs')
|
||||
const {
|
||||
verifyOpenCodeConfig,
|
||||
verifyHarnessBundleImports
|
||||
} = require('./verify-desktop-runtimes.cjs')
|
||||
|
||||
const root = join(__dirname, '..')
|
||||
const packageJson = JSON.parse(
|
||||
@@ -88,9 +92,15 @@ 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/opencode/rg.exe',
|
||||
'resources/licenses/vscode-ripgrep-MIT.txt',
|
||||
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/package.json',
|
||||
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/dist/index.js',
|
||||
'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'
|
||||
]
|
||||
@@ -120,11 +130,12 @@ const platformDefinitions = {
|
||||
supportedFormats: ['nsis', 'portable'],
|
||||
unpackedPattern: /^win(?:-.+)?-unpacked$/u,
|
||||
executable: [`${productName}.exe`],
|
||||
runtimeExecutable: 'opencode.exe'
|
||||
runtimeExecutable: 'opencode.exe',
|
||||
ripgrepExecutable: 'rg.exe'
|
||||
},
|
||||
macos: {
|
||||
builderFlag: '--mac',
|
||||
defaultFormats: ['dmg', 'zip'],
|
||||
defaultFormats: ['dmg'],
|
||||
supportedFormats: ['dmg', 'zip'],
|
||||
unpackedPattern: /^mac(?:-.+)?$/u,
|
||||
executable: [
|
||||
@@ -133,7 +144,8 @@ const platformDefinitions = {
|
||||
'MacOS',
|
||||
productName
|
||||
],
|
||||
runtimeExecutable: 'opencode'
|
||||
runtimeExecutable: 'opencode',
|
||||
ripgrepExecutable: 'rg'
|
||||
},
|
||||
linux: {
|
||||
builderFlag: '--linux',
|
||||
@@ -141,7 +153,8 @@ const platformDefinitions = {
|
||||
supportedFormats: ['AppImage', 'deb', 'rpm'],
|
||||
unpackedPattern: /^linux(?:-.+)?-unpacked$/u,
|
||||
executable: [packageJson.name],
|
||||
runtimeExecutable: 'opencode'
|
||||
runtimeExecutable: 'opencode',
|
||||
ripgrepExecutable: 'rg'
|
||||
}
|
||||
}
|
||||
const formatExtensions = {
|
||||
@@ -346,7 +359,7 @@ 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,
|
||||
@@ -818,6 +831,10 @@ function verifyHarnessPackage(
|
||||
join(unpackedRoot, ...harnessHostEntry.split('/')),
|
||||
'DeepSeek Harness 可执行 Host'
|
||||
)
|
||||
verifyHarnessBundleImports(
|
||||
readFileSync(join(unpackedRoot, ...harnessHostEntry.split('/')), 'utf8'),
|
||||
harnessHostEntry
|
||||
)
|
||||
const harnessLlmChunk = [...entries]
|
||||
.map((entry) => entry.slice(1).split(sep).join('/'))
|
||||
.find((entry) =>
|
||||
@@ -866,6 +883,10 @@ function verifyHarnessPackage(
|
||||
join(unpackedRoot, ...chunkPath.split('/')),
|
||||
'DeepSeek Harness 可执行 module chunk'
|
||||
)
|
||||
verifyHarnessBundleImports(
|
||||
readFileSync(join(unpackedRoot, ...chunkPath.split('/')), 'utf8'),
|
||||
chunkPath
|
||||
)
|
||||
}
|
||||
for (const [packageName, expectedVersion] of Object.entries(
|
||||
harnessPackageVersions
|
||||
@@ -894,6 +915,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
|
||||
@@ -1050,9 +1075,23 @@ function verifyUnpackedOutput(directory, options) {
|
||||
'opencode',
|
||||
definition.runtimeExecutable
|
||||
)
|
||||
const ripgrepExecutable = join(
|
||||
resources,
|
||||
'runtimes',
|
||||
'opencode',
|
||||
definition.ripgrepExecutable
|
||||
)
|
||||
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 受信任签名密钥'
|
||||
@@ -1066,6 +1105,11 @@ function verifyUnpackedOutput(directory, options) {
|
||||
'Remote Runtime 锁定清单'
|
||||
)
|
||||
assertFile(runtimeExecutable, 'OpenCode Runtime')
|
||||
assertFile(ripgrepExecutable, 'ripgrep Runtime')
|
||||
verifyOpenCodeConfig(
|
||||
resources,
|
||||
join(root, '.runtime-resources', 'opencode-config')
|
||||
)
|
||||
assertFile(
|
||||
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
|
||||
'Continue Runtime'
|
||||
@@ -1080,10 +1124,21 @@ 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, '应用主程序'],
|
||||
[runtimeExecutable, 'OpenCode Runtime']
|
||||
[runtimeExecutable, 'OpenCode Runtime'],
|
||||
[ripgrepExecutable, 'ripgrep Runtime']
|
||||
]) {
|
||||
const actualArchitecture = binaryArchitecture(filePath)
|
||||
if (actualArchitecture !== options.arch) {
|
||||
@@ -1543,7 +1598,7 @@ function printHelp() {
|
||||
|
||||
默认格式:
|
||||
windows: nsis, portable (ZIP)
|
||||
macos: dmg, zip
|
||||
macos: dmg
|
||||
linux: AppImage, deb, rpm`)
|
||||
}
|
||||
|
||||
@@ -1605,6 +1660,16 @@ async function main(argv = process.argv.slice(2)) {
|
||||
stagingDirectory,
|
||||
options
|
||||
)
|
||||
if (
|
||||
process.env.GITHUB_ACTIONS === 'true' &&
|
||||
options.platform === 'windows' &&
|
||||
options.arch === 'x64'
|
||||
) {
|
||||
await run(process.execPath, [
|
||||
join(root, 'build', 'run-packaged-deepseek-harness-smoke.cjs'),
|
||||
unpackedDirectory
|
||||
])
|
||||
}
|
||||
if (
|
||||
options.platform === 'windows' &&
|
||||
options.formats.includes('portable')
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
const { existsSync, readFileSync, rmSync } = require('node:fs')
|
||||
const { gzipSync } = require('node:zlib')
|
||||
const { resolve } = require('node:path')
|
||||
|
||||
// These are hard regression ceilings, not user-experience targets. In
|
||||
// particular, do not lazy-load one peer tab solely to stay below a ceiling.
|
||||
// The Settings ceiling is the validated 2026-08-23 production baseline
|
||||
// (648.78 kB raw / 97.79 kB gzip), rounded up with about 15% review headroom.
|
||||
const rendererBundleBudgets = Object.freeze({
|
||||
initial: Object.freeze({ raw: 3_500_000, gzip: 720_000 }),
|
||||
knowledge: Object.freeze({ raw: 330_000, gzip: 50_000 }),
|
||||
graph: Object.freeze({ raw: 3_500_000, gzip: 720_000 }),
|
||||
activity: Object.freeze({ raw: 55_000, gzip: 9_000 }),
|
||||
magicNotes: Object.freeze({ raw: 630_000, gzip: 130_000 }),
|
||||
settings: Object.freeze({ raw: 750_000, gzip: 115_000 })
|
||||
})
|
||||
|
||||
function normalizePath(value) {
|
||||
return value.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
function findEntryKey(manifest) {
|
||||
const entries = Object.entries(manifest).filter(
|
||||
([, item]) => item && item.isEntry
|
||||
)
|
||||
if (entries.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected one renderer entry in the manifest, found ${entries.length}`
|
||||
)
|
||||
}
|
||||
return entries[0][0]
|
||||
}
|
||||
|
||||
function findSourceKey(manifest, sourceSuffix) {
|
||||
const normalizedSuffix = normalizePath(sourceSuffix)
|
||||
let matches = Object.entries(manifest).filter(([key, item]) => {
|
||||
const source = normalizePath(item.src || key)
|
||||
return source.endsWith(normalizedSuffix)
|
||||
})
|
||||
if (matches.length === 0) {
|
||||
const sourceName = normalizedSuffix
|
||||
.split('/')
|
||||
.at(-1)
|
||||
?.replace(/\.[^.]+$/u, '')
|
||||
matches = Object.entries(manifest).filter(
|
||||
([, item]) =>
|
||||
item?.isDynamicEntry &&
|
||||
item.name === sourceName
|
||||
)
|
||||
}
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected one manifest entry for ${sourceSuffix}, found ${matches.length}`
|
||||
)
|
||||
}
|
||||
return matches[0][0]
|
||||
}
|
||||
|
||||
function findModuleOwnerKeys(manifest, moduleManifest, pattern) {
|
||||
const keysByFile = new Map(
|
||||
Object.entries(manifest).map(([key, item]) => [item.file, key])
|
||||
)
|
||||
const keys = new Set()
|
||||
for (const [file, modules] of Object.entries(moduleManifest)) {
|
||||
if (modules.some((id) => pattern.test(normalizePath(id)))) {
|
||||
const key = keysByFile.get(normalizePath(file))
|
||||
if (!key) {
|
||||
throw new Error(`Module manifest chunk is missing: ${file}`)
|
||||
}
|
||||
keys.add(key)
|
||||
}
|
||||
}
|
||||
if (keys.size === 0) {
|
||||
throw new Error(`No renderer chunk owns modules matching ${pattern}`)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
function assertSafeModuleManifest(moduleManifest) {
|
||||
for (const modules of Object.values(moduleManifest)) {
|
||||
for (const id of modules) {
|
||||
const normalized = normalizePath(id)
|
||||
if (
|
||||
normalized !== id ||
|
||||
normalized.startsWith('/') ||
|
||||
normalized.startsWith('../') ||
|
||||
/[A-Za-z]:\//u.test(normalized) ||
|
||||
normalized.includes('/Users/') ||
|
||||
normalized.includes('/home/')
|
||||
) {
|
||||
throw new Error(`Unsafe renderer module manifest path: ${id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectSynchronousClosure(manifest, rootKey) {
|
||||
if (!manifest[rootKey]) {
|
||||
throw new Error(`Manifest entry is missing: ${rootKey}`)
|
||||
}
|
||||
const visited = new Set()
|
||||
const pending = [rootKey]
|
||||
while (pending.length > 0) {
|
||||
const key = pending.pop()
|
||||
if (visited.has(key)) {
|
||||
continue
|
||||
}
|
||||
const item = manifest[key]
|
||||
if (!item) {
|
||||
throw new Error(`Manifest import is missing: ${key}`)
|
||||
}
|
||||
visited.add(key)
|
||||
for (const imported of item.imports || []) {
|
||||
pending.push(imported)
|
||||
}
|
||||
}
|
||||
return visited
|
||||
}
|
||||
|
||||
function collectAssetFiles(manifest, keys) {
|
||||
const files = new Set()
|
||||
for (const key of keys) {
|
||||
const item = manifest[key]
|
||||
if (!item) {
|
||||
throw new Error(`Manifest entry is missing: ${key}`)
|
||||
}
|
||||
files.add(normalizePath(item.file))
|
||||
for (const cssFile of item.css || []) {
|
||||
files.add(normalizePath(cssFile))
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function measureFiles(files, readAsset, metricCache = new Map()) {
|
||||
let raw = 0
|
||||
let gzip = 0
|
||||
const measuredFiles = [...files].sort()
|
||||
for (const file of measuredFiles) {
|
||||
let metrics = metricCache.get(file)
|
||||
if (!metrics) {
|
||||
const bytes = Buffer.from(readAsset(file))
|
||||
metrics = {
|
||||
raw: bytes.byteLength,
|
||||
gzip: gzipSync(bytes).byteLength
|
||||
}
|
||||
metricCache.set(file, metrics)
|
||||
}
|
||||
raw += metrics.raw
|
||||
gzip += metrics.gzip
|
||||
}
|
||||
return { raw, gzip, files: measuredFiles }
|
||||
}
|
||||
|
||||
function withoutItems(keys, excluded) {
|
||||
return new Set([...keys].filter((key) => !excluded.has(key)))
|
||||
}
|
||||
|
||||
function describeEntry(
|
||||
manifest,
|
||||
key,
|
||||
initialKeys,
|
||||
initialFiles,
|
||||
readAsset,
|
||||
metricCache
|
||||
) {
|
||||
const closureKeys = collectSynchronousClosure(manifest, key)
|
||||
const closureFiles = collectAssetFiles(manifest, closureKeys)
|
||||
const incrementalKeys = withoutItems(closureKeys, initialKeys)
|
||||
const incrementalFiles = withoutItems(closureFiles, initialFiles)
|
||||
const root = measureFiles(
|
||||
collectAssetFiles(manifest, new Set([key])),
|
||||
readAsset,
|
||||
metricCache
|
||||
)
|
||||
return {
|
||||
key,
|
||||
file: manifest[key].file,
|
||||
rootRaw: root.raw,
|
||||
rootGzip: root.gzip,
|
||||
closureKeys,
|
||||
incrementalKeys,
|
||||
...measureFiles(incrementalFiles, readAsset, metricCache)
|
||||
}
|
||||
}
|
||||
|
||||
function assertDisjoint(description, keys, forbiddenKeys) {
|
||||
const matches = [...forbiddenKeys].filter((key) => keys.has(key))
|
||||
if (matches.length > 0) {
|
||||
throw new Error(
|
||||
`${description} synchronously includes ${matches.join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} RendererAnalysisKeys
|
||||
* @property {string} initial
|
||||
* @property {string} knowledge
|
||||
* @property {string} graph
|
||||
* @property {string} activity
|
||||
* @property {string} magicNotes
|
||||
* @property {string} settings
|
||||
* @property {string[]} g6
|
||||
*/
|
||||
|
||||
/**
|
||||
* Analyzes one renderer manifest.
|
||||
*
|
||||
* `keys` is the authoritative set of discovered roots and module owners;
|
||||
* notably, `keys.g6` is always an array because G6 may span several chunks.
|
||||
*
|
||||
* @returns {{ keys: RendererAnalysisKeys, sections: Record<string, object> }}
|
||||
*/
|
||||
function analyzeRendererManifest(manifest, readAsset, moduleManifest) {
|
||||
assertSafeModuleManifest(moduleManifest)
|
||||
const metricCache = new Map()
|
||||
const keys = {
|
||||
initial: findEntryKey(manifest),
|
||||
knowledge: findSourceKey(manifest, 'KnowledgeWorkspace.tsx'),
|
||||
graph: findSourceKey(manifest, 'KnowledgeGraphChart.tsx'),
|
||||
activity: findSourceKey(manifest, 'ActivityPanel.tsx'),
|
||||
magicNotes: findSourceKey(manifest, 'MagicNotesWorkspace.tsx'),
|
||||
settings: findSourceKey(manifest, 'SettingsPanel.tsx')
|
||||
}
|
||||
const g6Keys = findModuleOwnerKeys(
|
||||
manifest,
|
||||
moduleManifest,
|
||||
/(?:^|\/)node_modules\/@antv\/g6\//u
|
||||
)
|
||||
const initialKeys = collectSynchronousClosure(manifest, keys.initial)
|
||||
const initialFiles = collectAssetFiles(manifest, initialKeys)
|
||||
const initialRoot = measureFiles(
|
||||
collectAssetFiles(manifest, new Set([keys.initial])),
|
||||
readAsset,
|
||||
metricCache
|
||||
)
|
||||
const initial = {
|
||||
key: keys.initial,
|
||||
file: manifest[keys.initial].file,
|
||||
rootRaw: initialRoot.raw,
|
||||
rootGzip: initialRoot.gzip,
|
||||
closureKeys: initialKeys,
|
||||
incrementalKeys: initialKeys,
|
||||
...measureFiles(initialFiles, readAsset, metricCache)
|
||||
}
|
||||
const sections = {
|
||||
initial,
|
||||
knowledge: describeEntry(
|
||||
manifest,
|
||||
keys.knowledge,
|
||||
initialKeys,
|
||||
initialFiles,
|
||||
readAsset,
|
||||
metricCache
|
||||
),
|
||||
graph: describeEntry(
|
||||
manifest,
|
||||
keys.graph,
|
||||
initialKeys,
|
||||
initialFiles,
|
||||
readAsset,
|
||||
metricCache
|
||||
),
|
||||
activity: describeEntry(
|
||||
manifest,
|
||||
keys.activity,
|
||||
initialKeys,
|
||||
initialFiles,
|
||||
readAsset,
|
||||
metricCache
|
||||
),
|
||||
magicNotes: describeEntry(
|
||||
manifest,
|
||||
keys.magicNotes,
|
||||
initialKeys,
|
||||
initialFiles,
|
||||
readAsset,
|
||||
metricCache
|
||||
),
|
||||
settings: describeEntry(
|
||||
manifest,
|
||||
keys.settings,
|
||||
initialKeys,
|
||||
initialFiles,
|
||||
readAsset,
|
||||
metricCache
|
||||
)
|
||||
}
|
||||
|
||||
assertDisjoint(
|
||||
'The renderer entry',
|
||||
initialKeys,
|
||||
new Set([keys.knowledge, keys.graph, keys.activity, ...g6Keys])
|
||||
)
|
||||
assertDisjoint(
|
||||
'The Knowledge shell',
|
||||
sections.knowledge.closureKeys,
|
||||
new Set([keys.graph, ...g6Keys])
|
||||
)
|
||||
for (const g6Key of g6Keys) {
|
||||
if (!sections.graph.closureKeys.has(g6Key)) {
|
||||
throw new Error('The graph chunk no longer synchronously owns G6')
|
||||
}
|
||||
}
|
||||
|
||||
return { keys: { ...keys, g6: [...g6Keys] }, sections }
|
||||
}
|
||||
|
||||
function checkBudgets(analysis, budgets = rendererBundleBudgets) {
|
||||
const failures = []
|
||||
for (const [name, budget] of Object.entries(budgets)) {
|
||||
const section = analysis.sections[name]
|
||||
if (!section) {
|
||||
failures.push(`Unknown budget section: ${name}`)
|
||||
continue
|
||||
}
|
||||
for (const metric of ['raw', 'gzip']) {
|
||||
if (section[metric] > budget[metric]) {
|
||||
failures.push(
|
||||
`${name} ${metric} ${section[metric]} exceeds ${budget[metric]}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Renderer bundle budget failed:\n- ${failures.join('\n- ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
return `${(bytes / 1000).toFixed(2)} kB`
|
||||
}
|
||||
|
||||
function formatReport(analysis) {
|
||||
return [
|
||||
'Renderer bundle budget:',
|
||||
...Object.entries(analysis.sections).map(
|
||||
([name, section]) =>
|
||||
`- ${name}: root ${formatBytes(section.rootRaw)} raw / ` +
|
||||
`${formatBytes(section.rootGzip)} gzip, ` +
|
||||
`incremental closure ${formatBytes(section.raw)} raw / ` +
|
||||
`${formatBytes(section.gzip)} gzip`
|
||||
)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function checkRendererBundle(root = process.cwd()) {
|
||||
const rendererRoot = resolve(root, 'out', 'renderer')
|
||||
const manifestPath = resolve(rendererRoot, '.vite', 'manifest.json')
|
||||
const moduleManifestPath = resolve(
|
||||
rendererRoot,
|
||||
'.vite',
|
||||
'module-manifest.json'
|
||||
)
|
||||
if (!existsSync(manifestPath)) {
|
||||
throw new Error(`Renderer manifest not found: ${manifestPath}`)
|
||||
}
|
||||
if (!existsSync(moduleManifestPath)) {
|
||||
throw new Error(
|
||||
`Renderer module manifest not found: ${moduleManifestPath}`
|
||||
)
|
||||
}
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
const moduleManifest = JSON.parse(
|
||||
readFileSync(moduleManifestPath, 'utf8')
|
||||
)
|
||||
const analysis = analyzeRendererManifest(
|
||||
manifest,
|
||||
(file) => readFileSync(resolve(rendererRoot, file)),
|
||||
moduleManifest
|
||||
)
|
||||
checkBudgets(analysis)
|
||||
rmSync(moduleManifestPath)
|
||||
return analysis
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
analyzeRendererManifest,
|
||||
checkBudgets,
|
||||
checkRendererBundle,
|
||||
collectAssetFiles,
|
||||
collectSynchronousClosure,
|
||||
findEntryKey,
|
||||
findModuleOwnerKeys,
|
||||
findSourceKey,
|
||||
formatReport,
|
||||
assertSafeModuleManifest,
|
||||
measureFiles,
|
||||
rendererBundleBudgets
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
const analysis = checkRendererBundle()
|
||||
console.log(formatReport(analysis))
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error))
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const {
|
||||
const { app, utilityProcess } = require('electron/main')
|
||||
|
||||
const protocol = 'goodbuddy.deepseek-harness.control'
|
||||
const controlVersion = 2
|
||||
const controlVersion = 3
|
||||
const byteProtocol = 'goodbuddy.deepseek-harness.byte-stream'
|
||||
const byteProtocolVersion = 1
|
||||
const configuredHostPath =
|
||||
@@ -183,7 +183,8 @@ async function run() {
|
||||
provider: 'goodbuddy',
|
||||
model: 'qwen-plus',
|
||||
supportsImageInput: false,
|
||||
harnessVersion: '0.1.0-rc.8',
|
||||
requestHeaders: {},
|
||||
harnessVersion: '0.1.2-rc.1',
|
||||
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
|
||||
skillPackages: [],
|
||||
extensionPackages: [],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const { existsSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
|
||||
function npmInvocation(projectDir, environment = process.env) {
|
||||
const npmCli = environment.npm_execpath;
|
||||
if (npmCli) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
prefixArgs: [npmCli],
|
||||
};
|
||||
}
|
||||
|
||||
const bundledNpmCli = join(
|
||||
projectDir,
|
||||
"node_modules",
|
||||
"npm",
|
||||
"bin",
|
||||
"npm-cli.js",
|
||||
);
|
||||
if (existsSync(bundledNpmCli)) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
prefixArgs: [bundledNpmCli],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: process.platform === "win32" ? "npm.cmd" : "npm",
|
||||
prefixArgs: [],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { npmInvocation };
|
||||
@@ -0,0 +1,110 @@
|
||||
const { existsSync } = require("node:fs");
|
||||
const {
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const { npmInvocation } = require("./npm-invocation.cjs");
|
||||
|
||||
const opencodeVersion = "1.18.29";
|
||||
const opencodePluginPackage = "@opencode-ai/plugin";
|
||||
|
||||
function prepareBundledOpenCodeConfig(projectDir) {
|
||||
const targetDirectory = join(
|
||||
projectDir,
|
||||
".runtime-resources",
|
||||
"opencode-config",
|
||||
);
|
||||
try {
|
||||
const installed = JSON.parse(
|
||||
readFileSync(
|
||||
join(
|
||||
targetDirectory,
|
||||
"node_modules",
|
||||
"@opencode-ai",
|
||||
"plugin",
|
||||
"package.json",
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
if (installed.version === opencodeVersion) {
|
||||
return targetDirectory;
|
||||
}
|
||||
} catch {
|
||||
// Rebuild an incomplete or stale offline config template.
|
||||
}
|
||||
|
||||
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
|
||||
rmSync(stagingDirectory, { recursive: true, force: true });
|
||||
mkdirSync(stagingDirectory, { recursive: true });
|
||||
try {
|
||||
writeFileSync(
|
||||
join(stagingDirectory, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
private: true,
|
||||
dependencies: {
|
||||
[opencodePluginPackage]: opencodeVersion,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(join(stagingDirectory, ".gitignore"), "node_modules\n");
|
||||
const npm = npmInvocation(projectDir);
|
||||
const environment = { ...process.env };
|
||||
delete environment.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||
const result = spawnSync(
|
||||
npm.command,
|
||||
[
|
||||
...npm.prefixArgs,
|
||||
"install",
|
||||
"--ignore-scripts",
|
||||
"--bin-links=false",
|
||||
"--omit=dev",
|
||||
"--omit=optional",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--save-exact",
|
||||
],
|
||||
{
|
||||
cwd: stagingDirectory,
|
||||
encoding: "utf8",
|
||||
env: environment,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Unable to prepare bundled OpenCode config: ${
|
||||
result.error?.message ||
|
||||
result.stderr ||
|
||||
result.stdout ||
|
||||
`npm exited with ${result.status}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
rmSync(targetDirectory, { recursive: true, force: true });
|
||||
mkdirSync(join(targetDirectory, ".."), { recursive: true });
|
||||
renameSync(stagingDirectory, targetDirectory);
|
||||
return targetDirectory;
|
||||
} finally {
|
||||
if (existsSync(stagingDirectory)) {
|
||||
rmSync(stagingDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
opencodeVersion,
|
||||
prepareBundledOpenCodeConfig,
|
||||
};
|
||||
@@ -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
|
||||
})
|
||||
|
||||
+492
-560
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
const { spawnSync } = require('node:child_process')
|
||||
const { rmSync } = require('node:fs')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { join } = require('node:path')
|
||||
const { buildSync } = require('esbuild')
|
||||
|
||||
const output = join(tmpdir(), `goodbuddy-browser-tabs-e2e-${process.pid}.cjs`)
|
||||
|
||||
try {
|
||||
buildSync({
|
||||
entryPoints: [join(process.cwd(), 'tests', 'browser-tabs-electron-e2e.ts')],
|
||||
outfile: output,
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
target: 'node22',
|
||||
external: ['electron']
|
||||
})
|
||||
const environment = { ...process.env }
|
||||
delete environment.ELECTRON_RUN_AS_NODE
|
||||
const result = spawnSync(require('electron'), [output], {
|
||||
cwd: process.cwd(),
|
||||
env: environment,
|
||||
stdio: 'inherit',
|
||||
timeout: 120_000
|
||||
})
|
||||
if (result.error) throw result.error
|
||||
process.exitCode = result.status ?? 1
|
||||
} finally {
|
||||
rmSync(output, { force: true })
|
||||
}
|
||||
@@ -88,7 +88,7 @@ async function main() {
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: '@deepseek-ai/dsh-llm',
|
||||
version: '0.1.0-rc.8',
|
||||
version: '0.1.2-rc.1',
|
||||
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
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
const { statSync } = require('node:fs')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { delimiter, join, resolve } = require('node:path')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
|
||||
const unpackedPath = process.argv[2]
|
||||
? resolve(process.argv[2])
|
||||
@@ -109,6 +110,20 @@ async function main() {
|
||||
const npmProject = join(root, 'npm-project')
|
||||
const npmFixture = join(root, 'npm-fixture')
|
||||
await mkdir(project, { recursive: true })
|
||||
const pluginUrl = pathToFileURL(join(
|
||||
unpackedPath, 'resources', 'runtimes', 'opencode-config',
|
||||
'node_modules', '@opencode-ai', 'plugin', 'dist', 'index.js'
|
||||
)).href
|
||||
const importedPlugin = await run(
|
||||
executable,
|
||||
['--input-type=module', '--eval', `await import(${JSON.stringify(pluginUrl)})`],
|
||||
{ ...process.env, ELECTRON_RUN_AS_NODE: '1' },
|
||||
root
|
||||
)
|
||||
if (importedPlugin.exitCode !== 0 || importedPlugin.signal) {
|
||||
throw new Error(`Packaged OpenCode plugin import failed: ${importedPlugin.output.trim()}`)
|
||||
}
|
||||
console.log('Packaged OpenCode offline plugin import: ready')
|
||||
|
||||
await copyFile(
|
||||
resolve('build/deepseek-harness-utility-smoke.cjs'),
|
||||
@@ -147,7 +162,7 @@ async function main() {
|
||||
)
|
||||
|
||||
const packageArguments = [
|
||||
resolve('node_modules/electron-builder/cli.js'),
|
||||
resolve('build/run-electron-builder.cjs'),
|
||||
'--projectDir',
|
||||
project,
|
||||
'--win',
|
||||
|
||||
+206
-146
@@ -1,4 +1,4 @@
|
||||
const { createHash } = require('node:crypto')
|
||||
const { createHash } = require("node:crypto");
|
||||
const {
|
||||
chmod,
|
||||
mkdir,
|
||||
@@ -6,101 +6,84 @@ const {
|
||||
rename,
|
||||
rm,
|
||||
stat,
|
||||
writeFile
|
||||
} = require('node:fs/promises')
|
||||
const { existsSync } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const { spawnSync } = require('node:child_process')
|
||||
const tar = require('tar')
|
||||
const { sha256File } = require('./file-hash.cjs')
|
||||
writeFile,
|
||||
} = require("node:fs/promises");
|
||||
const { existsSync } = require("node:fs");
|
||||
const { join } = require("node:path");
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const tar = require("tar");
|
||||
const { sha256File } = require("./file-hash.cjs");
|
||||
const {
|
||||
opencodeVersion,
|
||||
prepareBundledOpenCodeConfig,
|
||||
} = require("./opencode-config.cjs");
|
||||
const { npmInvocation } = require("./npm-invocation.cjs");
|
||||
|
||||
const opencodeVersion = '1.18.9'
|
||||
const architectureNames = {
|
||||
1: 'x64',
|
||||
3: 'arm64'
|
||||
}
|
||||
1: "x64",
|
||||
3: "arm64",
|
||||
};
|
||||
const platformNames = {
|
||||
darwin: 'darwin',
|
||||
linux: 'linux',
|
||||
win32: 'windows'
|
||||
}
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
win32: "windows",
|
||||
};
|
||||
|
||||
function sha512Integrity(contents) {
|
||||
return `sha512-${createHash('sha512').update(contents).digest('base64')}`
|
||||
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
|
||||
}
|
||||
|
||||
async function lockedIntegrity(projectDir, packageName) {
|
||||
async function lockedIntegrity(projectDir, packageName, version) {
|
||||
const lock = JSON.parse(
|
||||
await readFile(join(projectDir, 'package-lock.json'), 'utf8')
|
||||
)
|
||||
const entry = lock.packages?.[`node_modules/${packageName}`]
|
||||
await readFile(join(projectDir, "package-lock.json"), "utf8"),
|
||||
);
|
||||
const entry = lock.packages?.[`node_modules/${packageName}`];
|
||||
if (
|
||||
entry?.version !== opencodeVersion ||
|
||||
typeof entry.integrity !== 'string'
|
||||
entry?.version !== version ||
|
||||
typeof entry.integrity !== "string"
|
||||
) {
|
||||
throw new Error(
|
||||
`Missing locked ${packageName}@${opencodeVersion} integrity`
|
||||
)
|
||||
`Missing locked ${packageName}@${version} integrity`,
|
||||
);
|
||||
}
|
||||
return entry.integrity
|
||||
return entry.integrity;
|
||||
}
|
||||
|
||||
function npmInvocation() {
|
||||
const npmCli = process.env.npm_execpath
|
||||
if (npmCli) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
prefixArgs: [npmCli]
|
||||
}
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
throw new Error('npm_execpath is required to prepare bundled runtimes')
|
||||
}
|
||||
return {
|
||||
command: 'npm',
|
||||
prefixArgs: []
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadPackage(projectDir, packageName, integrity) {
|
||||
const cacheDirectory = join(
|
||||
projectDir,
|
||||
'.runtime-resources',
|
||||
'cache'
|
||||
)
|
||||
await mkdir(cacheDirectory, { recursive: true })
|
||||
async function downloadPackage(projectDir, packageName, version, integrity) {
|
||||
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
|
||||
await mkdir(cacheDirectory, { recursive: true });
|
||||
const archivePath = join(
|
||||
cacheDirectory,
|
||||
`${packageName}-${opencodeVersion}.tgz`
|
||||
)
|
||||
`${packageName.replaceAll("/", "-").replaceAll("@", "")}-${version}.tgz`,
|
||||
);
|
||||
if (existsSync(archivePath)) {
|
||||
const cached = await readFile(archivePath)
|
||||
const cached = await readFile(archivePath);
|
||||
if (sha512Integrity(cached) === integrity) {
|
||||
return archivePath
|
||||
return archivePath;
|
||||
}
|
||||
await rm(archivePath, { force: true })
|
||||
await rm(archivePath, { force: true });
|
||||
}
|
||||
|
||||
const npm = npmInvocation()
|
||||
const npm = npmInvocation(projectDir);
|
||||
const result = spawnSync(
|
||||
npm.command,
|
||||
[
|
||||
...npm.prefixArgs,
|
||||
'pack',
|
||||
`${packageName}@${opencodeVersion}`,
|
||||
'--ignore-scripts',
|
||||
'--json',
|
||||
'--pack-destination',
|
||||
cacheDirectory
|
||||
"pack",
|
||||
`${packageName}@${version}`,
|
||||
"--ignore-scripts",
|
||||
"--json",
|
||||
"--pack-destination",
|
||||
cacheDirectory,
|
||||
],
|
||||
{
|
||||
cwd: projectDir,
|
||||
encoding: 'utf8',
|
||||
encoding: "utf8",
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
shell: false,
|
||||
windowsHide: true
|
||||
}
|
||||
)
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Unable to fetch ${packageName}: ${
|
||||
@@ -108,81 +91,150 @@ async function downloadPackage(projectDir, packageName, integrity) {
|
||||
result.stderr ||
|
||||
result.stdout ||
|
||||
`npm exited with ${result.status}`
|
||||
}`
|
||||
)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const output = JSON.parse(result.stdout)
|
||||
const downloadedPath = join(cacheDirectory, output[0].filename)
|
||||
const contents = await readFile(downloadedPath)
|
||||
const output = JSON.parse(result.stdout);
|
||||
const downloadedPath = join(cacheDirectory, output[0].filename);
|
||||
const contents = await readFile(downloadedPath);
|
||||
if (sha512Integrity(contents) !== integrity) {
|
||||
await rm(downloadedPath, { force: true })
|
||||
throw new Error(`Integrity verification failed for ${packageName}`)
|
||||
await rm(downloadedPath, { force: true });
|
||||
throw new Error(`Integrity verification failed for ${packageName}`);
|
||||
}
|
||||
if (downloadedPath !== archivePath) {
|
||||
await rm(archivePath, { force: true })
|
||||
await rename(downloadedPath, archivePath)
|
||||
await rm(archivePath, { force: true });
|
||||
await rename(downloadedPath, archivePath);
|
||||
}
|
||||
return archivePath
|
||||
return archivePath;
|
||||
}
|
||||
|
||||
module.exports = async function prepareBundledRuntimes(context) {
|
||||
const platform = context.electronPlatformName
|
||||
const architecture = architectureNames[context.arch]
|
||||
const packagePlatform = platformNames[platform]
|
||||
if (!architecture || !packagePlatform) {
|
||||
throw new Error(
|
||||
`Bundled OpenCode does not support ${platform}/${context.arch}`
|
||||
)
|
||||
}
|
||||
|
||||
const suffix =
|
||||
architecture === 'x64' ? `${architecture}-baseline` : architecture
|
||||
const packageName = `opencode-${packagePlatform}-${suffix}`
|
||||
const projectDir = context.packager.projectDir
|
||||
const projectPackage = JSON.parse(
|
||||
await readFile(join(projectDir, 'package.json'), 'utf8')
|
||||
)
|
||||
await writeFile(
|
||||
join(projectDir, 'out', 'main', 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: '@deepseek-ai/dsh-llm',
|
||||
version:
|
||||
projectPackage.dependencies['@deepseek-ai/dsh-llm'],
|
||||
private: true,
|
||||
type: 'module'
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`,
|
||||
'utf8'
|
||||
)
|
||||
const integrity = await lockedIntegrity(projectDir, packageName)
|
||||
const targetDirectory = join(
|
||||
projectDir,
|
||||
'.runtime-resources',
|
||||
architecture
|
||||
)
|
||||
const readyPath = join(targetDirectory, '.ready.json')
|
||||
const executable = platform === 'win32' ? 'opencode.exe' : 'opencode'
|
||||
const preparedPath = join(targetDirectory, executable)
|
||||
const identity = {
|
||||
packageName,
|
||||
version: opencodeVersion,
|
||||
integrity
|
||||
}
|
||||
async function prepareRipgrep(
|
||||
projectDir,
|
||||
targetDirectory,
|
||||
platform,
|
||||
architecture,
|
||||
version,
|
||||
) {
|
||||
const packageName = `@vscode/ripgrep-${platform}-${architecture}`;
|
||||
const integrity = await lockedIntegrity(projectDir, packageName, version);
|
||||
const executable = platform === "win32" ? "rg.exe" : "rg";
|
||||
const preparedPath = join(targetDirectory, executable);
|
||||
const readyPath = join(targetDirectory, ".ripgrep-ready.json");
|
||||
const identity = { packageName, version, integrity };
|
||||
try {
|
||||
const ready = JSON.parse(await readFile(readyPath, 'utf8'))
|
||||
const ready = JSON.parse(await readFile(readyPath, "utf8"));
|
||||
if (
|
||||
ready.packageName === identity.packageName &&
|
||||
ready.version === identity.version &&
|
||||
ready.integrity === identity.integrity &&
|
||||
typeof ready.executableSha256 === 'string' &&
|
||||
(platform === 'win32' ||
|
||||
typeof ready.executableSha256 === "string" &&
|
||||
(platform === "win32" || ((await stat(preparedPath)).mode & 0o111) !== 0) &&
|
||||
(await sha256File(preparedPath)) === ready.executableSha256
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Rebuild an incomplete or stale ripgrep cache.
|
||||
}
|
||||
|
||||
const archivePath = await downloadPackage(
|
||||
projectDir,
|
||||
packageName,
|
||||
version,
|
||||
integrity,
|
||||
);
|
||||
const stagingDirectory = `${targetDirectory}.ripgrep-staging-${process.pid}`;
|
||||
await rm(stagingDirectory, { recursive: true, force: true });
|
||||
await mkdir(stagingDirectory, { recursive: true });
|
||||
try {
|
||||
await tar.x({
|
||||
file: archivePath,
|
||||
cwd: stagingDirectory,
|
||||
strip: 1,
|
||||
});
|
||||
const sourcePath = join(stagingDirectory, "bin", executable);
|
||||
const executableSha256 = await sha256File(sourcePath);
|
||||
if (platform !== "win32") {
|
||||
await chmod(sourcePath, 0o755);
|
||||
}
|
||||
await mkdir(targetDirectory, { recursive: true });
|
||||
await rm(preparedPath, { force: true });
|
||||
await rename(sourcePath, preparedPath);
|
||||
await writeFile(
|
||||
readyPath,
|
||||
JSON.stringify({ ...identity, executableSha256 }),
|
||||
"utf8",
|
||||
);
|
||||
} finally {
|
||||
await rm(stagingDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async function prepareBundledRuntimes(context) {
|
||||
const platform = context.electronPlatformName;
|
||||
const architecture = architectureNames[context.arch];
|
||||
const packagePlatform = platformNames[platform];
|
||||
if (!architecture || !packagePlatform) {
|
||||
throw new Error(
|
||||
`Bundled OpenCode does not support ${platform}/${context.arch}`,
|
||||
);
|
||||
}
|
||||
|
||||
const suffix =
|
||||
architecture === "x64" ? `${architecture}-baseline` : architecture;
|
||||
const packageName = `opencode-${packagePlatform}-${suffix}`;
|
||||
const projectDir = context.packager.projectDir;
|
||||
await prepareBundledOpenCodeConfig(projectDir);
|
||||
const projectPackage = JSON.parse(
|
||||
await readFile(join(projectDir, "package.json"), "utf8"),
|
||||
);
|
||||
const ripgrepVersion = projectPackage.dependencies["@vscode/ripgrep"];
|
||||
if (typeof ripgrepVersion !== "string" || !/^\d+\.\d+\.\d+$/u.test(ripgrepVersion)) {
|
||||
throw new Error("Missing exact @vscode/ripgrep dependency version");
|
||||
}
|
||||
await writeFile(
|
||||
join(projectDir, "out", "main", "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: "@deepseek-ai/dsh-llm",
|
||||
version: projectPackage.dependencies["@deepseek-ai/dsh-llm"],
|
||||
private: true,
|
||||
type: "module",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
const integrity = await lockedIntegrity(projectDir, packageName, opencodeVersion);
|
||||
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
|
||||
const readyPath = join(targetDirectory, ".ready.json");
|
||||
const executable = platform === "win32" ? "opencode.exe" : "opencode";
|
||||
const preparedPath = join(targetDirectory, executable);
|
||||
const identity = {
|
||||
packageName,
|
||||
version: opencodeVersion,
|
||||
integrity,
|
||||
};
|
||||
try {
|
||||
const ready = JSON.parse(await readFile(readyPath, "utf8"));
|
||||
if (
|
||||
ready.packageName === identity.packageName &&
|
||||
ready.version === identity.version &&
|
||||
ready.integrity === identity.integrity &&
|
||||
typeof ready.executableSha256 === "string" &&
|
||||
(platform === "win32" ||
|
||||
((await stat(preparedPath)).mode & 0o111) !== 0) &&
|
||||
(await sha256File(preparedPath)) === ready.executableSha256
|
||||
) {
|
||||
return
|
||||
await prepareRipgrep(
|
||||
projectDir,
|
||||
targetDirectory,
|
||||
platform,
|
||||
architecture,
|
||||
ripgrepVersion,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Rebuild an incomplete or stale runtime cache.
|
||||
@@ -191,32 +243,40 @@ module.exports = async function prepareBundledRuntimes(context) {
|
||||
const archivePath = await downloadPackage(
|
||||
projectDir,
|
||||
packageName,
|
||||
integrity
|
||||
)
|
||||
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`
|
||||
await rm(stagingDirectory, { recursive: true, force: true })
|
||||
await mkdir(stagingDirectory, { recursive: true })
|
||||
opencodeVersion,
|
||||
integrity,
|
||||
);
|
||||
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
|
||||
await rm(stagingDirectory, { recursive: true, force: true });
|
||||
await mkdir(stagingDirectory, { recursive: true });
|
||||
try {
|
||||
await tar.x({
|
||||
file: archivePath,
|
||||
cwd: stagingDirectory,
|
||||
strip: 1
|
||||
})
|
||||
const sourcePath = join(stagingDirectory, 'bin', executable)
|
||||
const stagingExecutable = join(stagingDirectory, executable)
|
||||
await rename(sourcePath, stagingExecutable)
|
||||
if (platform !== 'win32') {
|
||||
await chmod(stagingExecutable, 0o755)
|
||||
strip: 1,
|
||||
});
|
||||
const sourcePath = join(stagingDirectory, "bin", executable);
|
||||
const stagingExecutable = join(stagingDirectory, executable);
|
||||
await rename(sourcePath, stagingExecutable);
|
||||
if (platform !== "win32") {
|
||||
await chmod(stagingExecutable, 0o755);
|
||||
}
|
||||
const executableSha256 = await sha256File(stagingExecutable)
|
||||
const executableSha256 = await sha256File(stagingExecutable);
|
||||
await writeFile(
|
||||
join(stagingDirectory, '.ready.json'),
|
||||
join(stagingDirectory, ".ready.json"),
|
||||
JSON.stringify({ ...identity, executableSha256 }),
|
||||
'utf8'
|
||||
)
|
||||
await rm(targetDirectory, { recursive: true, force: true })
|
||||
await rename(stagingDirectory, targetDirectory)
|
||||
"utf8",
|
||||
);
|
||||
await rm(targetDirectory, { recursive: true, force: true });
|
||||
await rename(stagingDirectory, targetDirectory);
|
||||
} finally {
|
||||
await rm(stagingDirectory, { recursive: true, force: true })
|
||||
await rm(stagingDirectory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
await prepareRipgrep(
|
||||
projectDir,
|
||||
targetDirectory,
|
||||
platform,
|
||||
architecture,
|
||||
ripgrepVersion,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const { readdirSync, readFileSync, statSync } = require('node:fs')
|
||||
const { join, relative } = require('node:path')
|
||||
const { opencodeVersion } = require('./opencode-config.cjs')
|
||||
|
||||
function verifyOpenCodeConfig(resources, source) {
|
||||
const destination = join(resources, 'runtimes', 'opencode-config')
|
||||
const manifestPath = join(destination, 'node_modules', '@opencode-ai', 'plugin', 'package.json')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
if (manifest.name !== '@opencode-ai/plugin' || manifest.version !== opencodeVersion) {
|
||||
throw new Error('Packaged OpenCode plugin version is invalid')
|
||||
}
|
||||
const pending = [source]
|
||||
let files = 0
|
||||
while (pending.length > 0) {
|
||||
const directory = pending.pop()
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = join(directory, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
pending.push(file)
|
||||
} else if (entry.isFile()) {
|
||||
const target = join(destination, relative(source, file))
|
||||
if (!statSync(target, { throwIfNoEntry: false })?.isFile() ||
|
||||
!readFileSync(target).equals(readFileSync(file))) {
|
||||
throw new Error(`Packaged OpenCode dependency is missing or changed: ${relative(source, file)}`)
|
||||
}
|
||||
files++
|
||||
}
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function verifyHarnessBundleImports(source, name) {
|
||||
if (/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']@deepseek-ai\//u.test(source)) {
|
||||
throw new Error(`DeepSeek Harness bundle contains an external DeepSeek dependency: ${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifyOpenCodeConfig, verifyHarnessBundleImports }
|
||||
@@ -1,55 +0,0 @@
|
||||
# GoodBuddy 文档导航
|
||||
|
||||
GoodBuddy 文档按“文档类型 → 功能域”组织。新增文档应先选择类型,再放入对应功能目录,
|
||||
避免继续把所有设计平铺到单一 `features` 目录。
|
||||
|
||||
## 产品需求
|
||||
|
||||
| 功能域 | 入口 |
|
||||
| --- | --- |
|
||||
| Task 与 Job | [Task 与 Job 总览](./prd/task-and-job/README.md) |
|
||||
| Smart Heartbeat | [智能心跳 PRD](./prd/smart-heartbeat/smart-heartbeat-prd.md) |
|
||||
| 助手工作栏 | [通用助手工作栏与执行空间 PRD](./prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md) |
|
||||
| 会话监督 | [会话监督 PRD](./prd/supervision/conversation-supervision-prd.md) |
|
||||
| 记忆 | [分区记忆 PRD](./prd/memory/partitioned-memory-prd.md) |
|
||||
| 并行实验 | [并行实验工作台 PRD](./prd/experiments/parallel-experiments-prd.md) |
|
||||
| 持续学习 | [持续学习与评估门 PRD](./prd/learning/continuous-learning-prd.md) |
|
||||
| 知识库 | [知识库检索与分块增强 PRD](./prd/knowledge/knowledge-rag-enhancement-prd.md) |
|
||||
| 文档处理 | [文档解析与本地 OCR](./prd/document-processing/document-extraction-and-local-ocr.md) |
|
||||
| 消息通道 | [微信 ClawBot 通道 PRD](./prd/channels/wechat-clawbot-channel-project-prd.md) |
|
||||
|
||||
## Task 与 Job 文档
|
||||
|
||||
- [统一领域模型](./prd/task-and-job/task-and-job-model.md)
|
||||
- [Task Center](./prd/task-and-job/task-center-prd.md)
|
||||
- [Scheduled Task](./prd/task-and-job/scheduled-task-prd.md)
|
||||
- [Goal Task](./prd/task-and-job/goal-task-prd.md)
|
||||
- [Job 与 Subjob](./prd/task-and-job/job-and-subjob-prd.md)
|
||||
|
||||
## 跨功能文档
|
||||
|
||||
- [自动化平台架构](./architecture/automation-platform-architecture.md)
|
||||
- [平台功能页签与模型下载源设计](./architecture/model-download-source-design.md)
|
||||
- [本地文本向量模型与连接设计](./architecture/local-text-embedding-model-design.md)
|
||||
- [全双工实时语音交互设计](./architecture/full-duplex-voice-design.md)
|
||||
- [DeepSeek Harness Runtime 设计](./architecture/deepseek-harness-runtime-design.md)
|
||||
- [应用内反馈系统设计与对接](./architecture/in-app-feedback-integration-design.md)
|
||||
- [SSH 远程主机与 GoodBuddy Agent 稳定终态设计](./architecture/remote-host-and-goodbuddy-agent-design.md)
|
||||
- [SSH Host 远程环境准备与控制面直连设计](./architecture/remote-host-environment-provisioning-design.md)
|
||||
- [跨平台助手产品设计](./design/cross-platform-assistant-product-design.md)
|
||||
- [长期助手路线图](./roadmap/long-term-assistant-roadmap.md)
|
||||
- [电脑控制实施状态](./status/computer-control-implementation-status.md)
|
||||
- [知识检索评估](./quality/knowledge-retrieval-evaluation.md)
|
||||
- [GoodBuddy 龙芯(LoongArch)预览版构建与功能列表](./development/loongarch-preview-build.md)
|
||||
- [统一界面设计系统](../UI-DESIGN.md)
|
||||
|
||||
## 目录规则
|
||||
|
||||
1. PRD 放在 `docs/prd/<功能域>/`。
|
||||
2. 跨功能技术总纲放在 `docs/architecture/`。
|
||||
3. 产品级设计放在 `docs/design/`,路线图和实施状态分别放在 `roadmap`、`status`。
|
||||
4. 测试方法、评估协议和质量报告放在 `docs/quality/`。
|
||||
5. 开发环境、实验性构建和移植说明放在 `docs/development/`。
|
||||
6. 一个概念只能有一份权威定义;其他文档链接到它,不复制另一套术语。
|
||||
7. Task、Job、Run、Subagent 和 Conversation 的含义以
|
||||
[Task 与 Job 统一领域模型](./prd/task-and-job/task-and-job-model.md) 为准。
|
||||
@@ -9,7 +9,7 @@
|
||||
| 日期 | 2026-08-19 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 文档角色 | Task/Job、调度、目标、并行实验、会话监督、分区记忆与持续学习的总纲 |
|
||||
| 领域模型 | [Task 与 Job 统一领域模型](../prd/task-and-job/task-and-job-model.md) |
|
||||
| 领域模型 | [Task 与 Job 统一领域模型](../features/task-and-job/task-and-job-model.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
@@ -37,11 +37,11 @@ GoodBuddy 当前已经具备若干长期助手能力,但它们仍是彼此分
|
||||
|
||||
## 2. 核心产品判断
|
||||
|
||||
### 2.1 智能心跳保持独立,未来分区记忆另行设计
|
||||
### 2.1 智能心跳保持独立,分区记忆协同另行设计
|
||||
|
||||
当前智能心跳继续承担周期回顾、报告和建议,并支持 Global 或指定 Project 范围。它不是
|
||||
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。未来分区记忆的
|
||||
数据、状态、唤起和页面需要独立设计,不能从当前方向直接推导。
|
||||
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。按分区回顾、候选生成
|
||||
以及时间/内部事件唤起需要独立设计,不能从当前心跳行为直接推导,也不创建平行记忆类型。
|
||||
|
||||
- Scheduled Task 解决“何时在一个 Task 中执行新的 Job”。
|
||||
- Goal Task 解决“围绕结果在同一 Task 中持续规划和推进”。
|
||||
@@ -293,11 +293,14 @@ Runtime 边界完成,用户可见结果通过所属 Task 汇入关联 Conversa
|
||||
- 普通消息在发送时冻结 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 活动锁。
|
||||
Scheduled Task 不另设执行器或并发计数。
|
||||
- 两种来源均由 Main 派发给 Renderer,建立普通用户消息和流式助手消息后调用 `agent.run`。
|
||||
Renderer 加载会话及设置后才声明队列可派发。SSH 项目同样通过该入口选择远程 Runtime,
|
||||
使用普通请求的上下文、工具范围、取消和恢复流程。
|
||||
- `schedule_run.id` 作为普通请求 ID;普通请求终态同步该次计划运行与稳定产品 Task 的状态。
|
||||
尚未接受的派发可释放重试;已经接受的运行在重启后按普通 Task 状态结算或恢复,不重新发送。
|
||||
- 当前执行到达终态后再认领下一项。删除只移除尚未执行的项;“立即中断并插入”取消当前
|
||||
请求并把所选项设为下一项,不重排其他项。
|
||||
- 每条 Conversation 最多保留 20 个用户可提交的待执行项。启动时将未完成的派发恢复为
|
||||
@@ -435,9 +438,9 @@ Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收
|
||||
└─ 独立成果
|
||||
|
||||
智能心跳
|
||||
├─ 成长概览
|
||||
├─ 运行概览
|
||||
├─ 待处理建议
|
||||
├─ 心跳轨迹
|
||||
├─ 报告与记录
|
||||
└─ 心跳计划
|
||||
└─ Global / 指定 Project
|
||||
```
|
||||
@@ -445,7 +448,7 @@ Renderer 只通过显式 IPC 列出、加入、删除、提升、释放和接收
|
||||
监督统一进入应用级助手工作栏中固定且始终可访问的“监督”栏目,不再保留“独立可折叠右栏”
|
||||
和“动态新增页签”两种实现。栏目默认跟随当前上下文,用户可以固定到其他 Conversation、
|
||||
Task 或实验对象;详细范围与交互契约见
|
||||
[通用助手工作栏与执行空间 PRD](../prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
|
||||
[通用助手工作栏与执行空间 PRD](../features/assistant-workbar/prd.md)。
|
||||
|
||||
## 13. 安全与隐私
|
||||
|
||||
@@ -507,7 +510,7 @@ experiment_runs
|
||||
现有 `schedules`、`schedule_runs`、`heartbeat_configs`、`heartbeat_runs`、
|
||||
`heartbeat_entries`、`tasks` 和 `runs` 不应一次性重写。Schedule 可渐进建立稳定 Task 与
|
||||
Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心跳数据保持独立,不得
|
||||
静默转成 `AutomationPlan` 或顶层 Task。未来分区记忆完成设计前,不新增迁移目标。
|
||||
静默转成 `AutomationPlan` 或顶层 Task。分区记忆协同和唤起条件完成设计前,不新增迁移目标。
|
||||
|
||||
## 16. 分阶段实施
|
||||
|
||||
@@ -518,7 +521,7 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
|
||||
- 明确 Task N:1 Conversation 关系、左侧行首展开按钮与 Task 子项图标,以及 Task Center
|
||||
索引边界,不复制内容。
|
||||
- 为现有 Scheduled Task 和专家执行建立按 Task 聚合的活动视图。
|
||||
- 明确当前心跳保持独立,未来分区记忆尚待设计。
|
||||
- 明确当前心跳保持独立,分区记忆协同与唤起条件尚待设计。
|
||||
- 补充触发来源、运行版本、预算和读写范围展示。
|
||||
|
||||
### 阶段 1:调度与运行基础
|
||||
@@ -555,21 +558,21 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
|
||||
|
||||
## 17. 相关文档
|
||||
|
||||
- [Task 与 Job 统一领域模型](../prd/task-and-job/task-and-job-model.md)
|
||||
- [Task Center PRD](../prd/task-and-job/task-center-prd.md)
|
||||
- [Scheduled Task PRD](../prd/task-and-job/scheduled-task-prd.md)
|
||||
- [Job 与 Subjob PRD](../prd/task-and-job/job-and-subjob-prd.md)
|
||||
- [智能心跳 PRD](../prd/smart-heartbeat/smart-heartbeat-prd.md)
|
||||
- [并行实验工作台 PRD](../prd/experiments/parallel-experiments-prd.md)
|
||||
- [会话监督 PRD](../prd/supervision/conversation-supervision-prd.md)
|
||||
- [分区记忆 PRD](../prd/memory/partitioned-memory-prd.md)
|
||||
- [持续学习与评估门 PRD](../prd/learning/continuous-learning-prd.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. 总体验收标准
|
||||
|
||||
- [ ] 智能心跳保持独立,不作为 Task 类型;未来分区记忆尚未设计。
|
||||
- [ ] 智能心跳保持独立,不作为 Task 类型;分区记忆协同与唤起条件尚未设计。
|
||||
- [ ] Task Center 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以
|
||||
关联多个 Task。
|
||||
- [ ] 当前 UI 只展示到 Task,不提供 Job/Subjob/Run 树或独立导航。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
| 日期 | 2026-08-19 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../prd/document-processing/document-extraction-and-local-ocr.md)、[本地文本向量模型与连接设计](./local-text-embedding-model-design.md)、[全双工实时语音交互设计](./full-duplex-voice-design.md) |
|
||||
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[文档解析与本地 OCR](../features/document-processing/prd.md)、[本地文本向量模型与连接设计](../features/knowledge-base/local-embedding-technical-design.md)、[全双工实时语音交互设计](../features/full-duplex-voice/technical-design.md) |
|
||||
|
||||
本文定义“设置 → 平台功能”的二级页签结构,以及所有 GoodBuddy 托管本地模型共同使用的
|
||||
“模型下载源”设置。首期下载源为 ModelScope 和 Hugging Face,默认使用 ModelScope。
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
# SSH 远程主机与 GoodBuddy Agent 实现说明
|
||||
|
||||
## 状态
|
||||
|
||||
本文记录截至 2026-08-30 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
|
||||
Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Host current 环境”已经完成源码接线,
|
||||
详细事务与验收边界见
|
||||
[SSH Host 远程环境准备与直连下载设计](./remote-host-environment-provisioning-design.md);
|
||||
控制面直连源码可直接使用既有 package format v1 包,不等待携带 installer 的新 Agent
|
||||
包,也不通过额外目录元数据判断 bootstrap 能力;公开能力仍等待 GitHub/北京镜像、
|
||||
Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
|
||||
Windows 到 Linux x64 的既有安装、
|
||||
Main-only 模型桥、断线恢复、输出重放、同一 OpenCode Session 续接与终态清理已经在真实
|
||||
Host 上完成 provider-free 验证。上一轮 Linux x64 验收使用 Agent `0.11.2-e2e.12` 与
|
||||
OpenCode Runtime `1.18.9` 完成了一次有界真实模型工具调用;加入每 helper 随机 loopback
|
||||
路径 capability 后,Agent `0.11.10` 已通过独立 workflow 发布 Linux x64/arm64 复合包和
|
||||
签名累计目录;当前桌面源码准备 `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 账号本身没有的权限。模型 API Key 与 Provider 认证仍保留在 Electron Main,不传给远端 Runtime。
|
||||
|
||||
## 组件
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> 窄 Preload / Main IPC
|
||||
Main
|
||||
-> SSH Host 与 Host Key 管理
|
||||
-> SSH connection pool
|
||||
-> 签名 Agent / Runtime 安装
|
||||
-> 远程项目当前环境准备与稳定配置 SQLite 事务
|
||||
-> Main-owned model bridge
|
||||
SSH attach
|
||||
-> 远端私有 Unix socket
|
||||
Detached GoodBuddy Agent
|
||||
-> Workspace / Git 协议
|
||||
-> OpenCode ACP channel
|
||||
-> 直接拥有的 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。
|
||||
- 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 环境准备设计](./remote-host-environment-provisioning-design.md) 为准。
|
||||
- 打开新建/项目设置弹窗只读取本地 Host 验证记录,不并发检查所有 Host;目录浏览或保存
|
||||
才连接所选 Host。
|
||||
- 项目选择器中的管理操作默认浮动隐藏,在悬停、键盘焦点或触屏环境显示。管理任意已保存
|
||||
项目不先激活项目,因此 Host 不可达或远端目录已不存在时仍可删除其本地记录。
|
||||
- 删除 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 的远程请求校验仍是最终边界。
|
||||
|
||||
### 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;
|
||||
- 使用 Main-only 模型桥时,签名 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 请求都不自动重放。
|
||||
|
||||
## 模型桥
|
||||
|
||||
- Provider URL、API Key 和认证头只存在于 Main。
|
||||
- 远端 OpenCode 通过 GoodBuddy Agent helper 和每次 Prompt 的私有 Unix socket 使用模型桥。
|
||||
- Main 校验固定模型 profile、协议、请求路径和有界传输格式,并记录 Provider
|
||||
返回的实际 usage;不限制 Prompt 内的模型调用轮数、工具调用次数、累计 Token
|
||||
或单次模型输出 Token。取消、请求/响应字节上限和结果不确定时禁止自动重放仍然保留;
|
||||
模型桥不会为整个 Runtime Prompt 另设墙钟总时限。
|
||||
- helper 可以接收同一 Prompt 内并发到达的模型桥请求;它在单一稳定模型桥上按到达
|
||||
顺序等待并交付,不返回本地 `bridge-busy`,每个响应只有在 HTTP 完整 flush 后才
|
||||
发送 delivery ACK。
|
||||
- GoodBuddy 自己管理会话标题,因此传给 OpenCode 的配置禁用 title Agent;一次用户
|
||||
Prompt 不会额外触发标题模型请求。
|
||||
- 一条模型桥消息编码为一个 canonical JSON blob frame;blob frame 最大 2 MiB,
|
||||
Provider 请求与响应 body 仍各自限制为 768 KiB。每条模型消息只调用一次 SSH
|
||||
write,不再二次分片、等待逐帧 credit 或合并多个协议帧。凭据不进入 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.13`。
|
||||
- 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`。
|
||||
- 正常 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 建立 Main-only 模型桥,
|
||||
从当前 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 仍不会自动重放;
|
||||
用户之后显式发送的新 Prompt 会关闭旧的非就绪 Main binding,并以新的 ACP session
|
||||
继续当前 Conversation,避免一次恢复不确定永久阻塞后续输入。
|
||||
Ask 模式的 Linux Runtime 进程由 `bwrap` 启动后 `exec` 到已签名 Runtime 或 Agent
|
||||
model bridge helper;监督器只接受同一 PID/进程组从固定 `bwrap` 路径到预期最终可执行文件
|
||||
的有界转换,再登记为运行中进程。
|
||||
|
||||
不持久化 T2/T3、consent、approval bridge、confinement 或组件验证结果。数据库 schema
|
||||
v31 保留项目及关联用户数据,但重建执行空间表为 `project_id/kind/root_path/ssh_host_id`
|
||||
四列,并删除旧 Runtime 验证表。
|
||||
|
||||
## 资源与发布
|
||||
|
||||
- 所有桌面构建(包括 `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. 运行一次有界的真实模型调用,确认凭据只由 Main 使用。
|
||||
10. 中断并恢复 SSH,确认活动 Runtime 不被网络抖动终止。
|
||||
@@ -0,0 +1,497 @@
|
||||
# GoodBuddy 设备共享与 ShareServer 联邦网络总体设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 适用范围 | GoodBuddy 桌面端、ShareServer、网关间联邦协议 |
|
||||
| 桌面端设计 | [设备发现与共享](../features/device-sharing/README.md) |
|
||||
| 服务端设计 | [ShareServer](../../../goodbuddy-shareserver/docs/features/share-server/README.md) |
|
||||
|
||||
## 1. 文档职责
|
||||
|
||||
本文是设备共享网络的跨端权威设计,负责定义拓扑、术语、身份、能力清单、授权计算、任务
|
||||
路由、能力分发、协议边界和共同验收标准。桌面端的产品与界面行为由“设备发现与共享”功能
|
||||
文档负责,独立 Web 服务由 ShareServer 功能文档负责。
|
||||
|
||||
本文描述完整目标系统,不以交付阶段删减领域模型,也不把尚未实现的设计表述为当前能力。
|
||||
|
||||
## 2. 核心决策
|
||||
|
||||
### 2.1 两个独立产品面
|
||||
|
||||
1. **GoodBuddy Desktop** 在设置中提供“设备发现与共享”,默认使用局域网发现与直连,也可
|
||||
由用户额外连接一个或多个 ShareServer。
|
||||
2. **ShareServer** 是独立部署、独立升级的 Web 服务,提供组织、设备、能力、权限、更新、
|
||||
联邦、中继、Office 编辑和审计管理。它不运行在 Electron Main 中,也不由桌面端代为启动。
|
||||
|
||||
### 2.2 能力优先,来源可见
|
||||
|
||||
用户日常选择的是能力,不是网络连接。设备、ShareServer 和组织作为能力来源与诊断入口。
|
||||
任何调用都必须持续显示提供者、执行位置和数据去向,不能为了统一体验隐藏跨设备或跨组织
|
||||
边界。
|
||||
|
||||
### 2.3 局域网是默认连接方式,网关是显式附加连接
|
||||
|
||||
- 桌面端没有网关配置时,局域网发现、配对、调用和本机共享仍可完整工作。
|
||||
- 添加、删除或断开 ShareServer 不改变局域网配对关系。
|
||||
- ShareServer 不接管本机局域网发现,也不能静默把局域网设备注册到组织。
|
||||
- 同一桌面端可以连接多个 ShareServer;能力来源和授权按连接分别计算。
|
||||
|
||||
### 2.4 发现、被发现和开放能力是三个独立动作
|
||||
|
||||
- **发现设备**:浏览同一局域网中主动广播的 GoodBuddy 服务。
|
||||
- **允许被发现**:广播本设备存在及配对入口。
|
||||
- **开放能力**:向指定设备、用户组或组织发布可查看、调用或拉取的能力。
|
||||
|
||||
开启其中一项不能推导另外两项已开启。新安装默认不开放能力。
|
||||
|
||||
### 2.5 ShareServer 优先作为控制面
|
||||
|
||||
ShareServer 负责身份、目录、策略、任务路由、更新和审计。数据面优先在两个已认证端点之间
|
||||
直接建立;网络条件不允许时,可按组织策略使用 ShareServer 中继。是否允许中继、正文是否
|
||||
端到端加密、是否允许跨组织,由本次任务冻结的策略共同决定。
|
||||
|
||||
### 2.6 Office 编辑是 ShareServer 托管服务
|
||||
|
||||
ShareServer 可以把 ONLYOFFICE Docs、工作副本存储、保存回调和编辑会话作为一个可选部署
|
||||
能力提供给 Desktop。Desktop 只连接用户已明确配置的 ShareServer,不直接配置 Document
|
||||
Server,也不在本机启动服务。Office 文档内容会由 ShareServer 和编辑引擎处理,因此不能沿用
|
||||
“控制面默认不读取正文”的数据面假设;创建编辑会话前必须明确显示服务端、数据去向和保留期。
|
||||
|
||||
编辑后的本地或 SSH 源文件仍由 Desktop 或远程 GoodBuddy Agent 持有和原子回写。ShareServer
|
||||
是工作副本及服务端保存检查点的权威,不是源文件最终写入成功的权威。完整设计见
|
||||
[Office 协同编辑](../features/office-document-editing/README.md)。
|
||||
|
||||
## 3. 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| `Device` | 安装 GoodBuddy 并拥有稳定设备身份的终端 |
|
||||
| `Peer` | 与当前设备存在局域网配对关系的另一台设备 |
|
||||
| `ShareServer` | 提供组织控制面和可选数据中继的独立服务 |
|
||||
| `Organization` | ShareServer 中隔离成员、设备、能力与策略的管理范围 |
|
||||
| `GatewayConnection` | 某台设备与一个 ShareServer 上特定组织身份的连接 |
|
||||
| `Capability` | 可被查看、调用或分发的稳定能力标识 |
|
||||
| `CapabilityProvider` | 实际持有并执行某项能力的设备或服务节点 |
|
||||
| `Publication` | 能力提供者向某一范围发布的可见性和使用声明 |
|
||||
| `Grant` | 主体对能力执行某类动作的明确授权 |
|
||||
| `Task` | 一次远程能力调用及其输入、执行、输出和终态 |
|
||||
| `Package` | 可下载到本机安装的签名能力工件 |
|
||||
| `FederationTrust` | 两个组织网关之间经管理员确认的双边信任关系 |
|
||||
| `DocumentEditSession` | ShareServer 托管的单文档短期编辑会话、工作副本和保存检查点 |
|
||||
|
||||
“网关”是 ShareServer 的网络角色;产品名称为 `ShareServer`,独立仓库为
|
||||
`goodbuddy-shareserver`。
|
||||
|
||||
## 4. 系统拓扑
|
||||
|
||||
```text
|
||||
局域网:
|
||||
GoodBuddy Desktop A <-- mDNS 发现 + 配对后 TLS 直连 --> GoodBuddy Desktop B
|
||||
|
||||
单组织:
|
||||
GoodBuddy Desktop A -- 出站连接 --> ShareServer <-- 出站连接 -- Provider Device B
|
||||
|
||||
Office 编辑:
|
||||
GoodBuddy Desktop --> ShareServer --> ONLYOFFICE Docs
|
||||
└-------> 临时工作副本与保存检查点
|
||||
|
||||
跨组织:
|
||||
Desktop A --> ShareServer A <== 联邦信任 ==> ShareServer B <-- Provider Device B
|
||||
```
|
||||
|
||||
局域网和网关连接共享能力、授权和任务的领域语义,但不共享发现方式:
|
||||
|
||||
| 维度 | 局域网 | ShareServer |
|
||||
| --- | --- | --- |
|
||||
| 发现来源 | mDNS/DNS-SD 主动广播 | 已配置 URL 或邀请链接 |
|
||||
| 初始身份 | 双端验证码确认设备指纹 | 用户登录、设备注册和组织成员身份 |
|
||||
| 能力目录 | 已配对设备直接返回 | 服务端按组织策略过滤 |
|
||||
| 连接方向 | 配对后直连 | 设备主动建立出站连接 |
|
||||
| 管理范围 | 单个设备间授权 | 用户组、组织和联邦授权 |
|
||||
| 审计权威 | 两端各自本地记录 | 组织网关记录元数据,两端保留本地记录 |
|
||||
|
||||
## 5. 组件边界
|
||||
|
||||
### 5.1 GoodBuddy Desktop
|
||||
|
||||
- 浏览和广播局域网服务。
|
||||
- 创建、确认和撤销点对点配对。
|
||||
- 连接 ShareServer,保存脱敏连接信息和本机受保护凭据。
|
||||
- 发布用户明确选择的本机能力。
|
||||
- 展示经当前身份过滤的能力目录。
|
||||
- 在调用前冻结提供者、输入范围、授权和路由。
|
||||
- 执行本机提供的任务,或消费远端任务结果。
|
||||
- 校验并安装用户明确选择的签名能力包。
|
||||
- 保存本机活动记录和可撤销授权。
|
||||
|
||||
### 5.2 ShareServer
|
||||
|
||||
- 认证用户、设备、服务节点和其他 ShareServer。
|
||||
- 管理组织、成员、用户组和设备目录。
|
||||
- 接收能力发布并生成权限过滤后的目录。
|
||||
- 计算组织强制策略,签发短期任务授权。
|
||||
- 路由任务、维护在线状态,并在允许时中继数据。
|
||||
- 管理签名包、发布通道、更新策略和撤销。
|
||||
- 可选管理 Office 编辑会话、ONLYOFFICE 接入、工作副本、保存回调、保留和清理。
|
||||
- 建立双边联邦关系并限制跨组织可见性与调用。
|
||||
- 保存有界审计元数据、用量和管理操作。
|
||||
|
||||
### 5.3 能力提供者
|
||||
|
||||
- 只执行已发布且本次授权允许的能力。
|
||||
- 在执行边界重新验证任务令牌、输入 schema、大小、期限和调用方。
|
||||
- 使用自身 Runtime、账号、工作区和数据权限。
|
||||
- 返回有界进度、结果、成果或确定终态。
|
||||
- 不因收到任务令牌而获得其他能力或任意系统接口。
|
||||
|
||||
## 6. 身份与信任
|
||||
|
||||
### 6.1 设备身份
|
||||
|
||||
每次 GoodBuddy 安装生成稳定设备密钥和自签设备证书,私钥保存在系统安全存储中。对外只
|
||||
使用随机设备 ID、公钥指纹、用户设置的设备名称和必要协议版本。操作系统用户名、绝对路径、
|
||||
软件资产和能力详情不进入未认证发现广播。
|
||||
|
||||
重装或主动重置设备身份会产生新设备。旧局域网配对和 ShareServer 设备注册不会自动迁移。
|
||||
|
||||
### 6.2 局域网配对
|
||||
|
||||
1. 发起端选择一台主动广播的设备。
|
||||
2. 双方建立临时加密通道并计算共同短验证码。
|
||||
3. 两端同时显示设备名、系统类型、公钥指纹和验证码。
|
||||
4. 双方用户确认后保存对端公钥及配对 ID。
|
||||
5. 后续使用双方设备证书认证;任一端可以撤销配对。
|
||||
|
||||
IP 地址不作为设备身份,也不单独构成授权依据。
|
||||
|
||||
### 6.3 ShareServer 注册
|
||||
|
||||
用户通过浏览器/OIDC 或服务端支持的登录方式认证后,为当前设备申请一次性注册挑战。设备
|
||||
使用自身私钥签名挑战,ShareServer 建立 `user + organization + device` 绑定并签发可轮换的
|
||||
设备凭据。设备凭据不等同于用户 Web 会话,不可用于管理控制台登录。
|
||||
|
||||
### 6.4 联邦身份
|
||||
|
||||
两个 ShareServer 由双方管理员交换组织标识、网关公钥、公开端点和指纹,并分别确认信任。
|
||||
联邦关系只有在双方状态均为 `active` 时生效。单方暂停或撤销后立即停止新目录交换和新任务,
|
||||
已执行任务保留审计终态。
|
||||
|
||||
## 7. 能力模型
|
||||
|
||||
### 7.1 能力清单
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "1.0",
|
||||
"capabilityId": "document.convert.docx",
|
||||
"name": "文档转 Word",
|
||||
"version": "1.4.0",
|
||||
"modes": ["remote", "package"],
|
||||
"inputSchema": {},
|
||||
"outputSchema": {},
|
||||
"permissions": ["receive_text", "receive_files", "return_files"],
|
||||
"limits": {
|
||||
"maxInputBytes": 10485760,
|
||||
"maxOutputBytes": 52428800,
|
||||
"maxDurationSeconds": 900,
|
||||
"maxConcurrentTasks": 2
|
||||
},
|
||||
"dataPolicy": {
|
||||
"retention": "task_lifetime",
|
||||
"networkAccess": false,
|
||||
"delegation": "none"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
清单只描述能力事实,不授予任何主体访问权。`inputSchema` 和 `outputSchema` 使用受限 JSON
|
||||
Schema 子集;实现必须限制 schema 深度、属性数、枚举数和总字节,不能把清单当作可执行代码。
|
||||
|
||||
### 7.2 发布声明
|
||||
|
||||
`Publication` 将一个能力版本发布到以下一种范围:
|
||||
|
||||
- 指定局域网 Peer。
|
||||
- 所有已配对 Peer。
|
||||
- ShareServer 内指定用户、用户组或设备组。
|
||||
- 指定联邦组织中的用户组。
|
||||
|
||||
发布声明同时定义允许动作:`discover`、`inspect`、`invoke`、`download`。`inspect` 允许查看
|
||||
详细清单,`discover` 只允许看到名称和可用状态。
|
||||
|
||||
### 7.3 可调用与可分发
|
||||
|
||||
- `remote`:代码与受控数据留在提供方,调用方只发送输入并接收结果。
|
||||
- `package`:签名工件下载到调用方,经校验和用户/组织策略批准后安装。
|
||||
|
||||
同一能力可以同时支持两种模式,但两种模式分别授权。远程调用授权不能推导下载权,下载权
|
||||
也不能推导安装后自动启用。
|
||||
|
||||
## 8. 授权模型
|
||||
|
||||
### 8.1 授权输入
|
||||
|
||||
一次动作的最终决定取以下约束交集:
|
||||
|
||||
```text
|
||||
调用方用户权限
|
||||
∩ 调用方设备状态
|
||||
∩ 调用方组织强制策略
|
||||
∩ 发布范围
|
||||
∩ 提供方授权
|
||||
∩ 提供方组织强制策略
|
||||
∩ 能力输入与数据策略
|
||||
∩ 联邦关系限制(跨组织时)
|
||||
= allow | require_approval | deny
|
||||
```
|
||||
|
||||
`deny` 优先于 `require_approval`,`require_approval` 优先于 `allow`。客户端偏好只能在组织强制
|
||||
策略允许的范围内收紧权限,不能放宽权限。
|
||||
|
||||
### 8.2 Grant
|
||||
|
||||
Grant 至少绑定:
|
||||
|
||||
- 主体:用户、用户组、设备、设备组或外部组织。
|
||||
- 能力 ID 和允许的版本范围。
|
||||
- 动作:查看、调用、下载、安装或发布。
|
||||
- 输入:文本、文件、数据分类、单项和累计大小。
|
||||
- 提供者和执行位置限制。
|
||||
- 次数、并发、预算、时间段和有效期。
|
||||
- 是否需要每次批准。
|
||||
- 是否允许委托及明确的下游组织。
|
||||
|
||||
“始终允许”只创建与当前能力、提供者、输入类型和范围匹配的 Grant,不创建整台设备的任意
|
||||
访问权。
|
||||
|
||||
### 8.3 转委托
|
||||
|
||||
默认 `delegation=none`。允许委托时,任务授权中必须冻结完整执行链和最大跳数。任一新组织、
|
||||
新能力或新数据接收方加入执行链,都需要重新计算授权,不能沿用原任务令牌。
|
||||
|
||||
## 9. 任务协议
|
||||
|
||||
### 9.1 任务快照
|
||||
|
||||
创建任务时冻结:
|
||||
|
||||
- 调用方用户、设备和组织。
|
||||
- 能力 ID、版本和提供者。
|
||||
- 输入 schema 版本、数据类型、数据分类和字节数。
|
||||
- 路由:局域网直连、网关直连、网关中继或联邦路径。
|
||||
- 授权决定、批准记录和策略版本。
|
||||
- 委托链、预算、期限和幂等键。
|
||||
|
||||
设置或策略后续变化不追改已接受任务;紧急撤销可以阻止尚未开始的任务,并按策略取消活动
|
||||
任务。
|
||||
|
||||
### 9.2 状态
|
||||
|
||||
```text
|
||||
created -> awaiting_approval -> queued -> connecting -> running
|
||||
running -> completed | failed | cancelled | outcome_unknown
|
||||
created/awaiting_approval/queued/connecting -> rejected | expired | cancelled
|
||||
```
|
||||
|
||||
- `completed` 只在提供者提交完整终态且调用方确认接收后成立。
|
||||
- 网络中断后无法确认副作用结果时使用 `outcome_unknown`,不得自动重放。
|
||||
- 重试创建新 attempt,并复用仅在能力声明支持时才有效的幂等键。
|
||||
- 用户取消必须传播到实际提供者;未确认取消时不能声称任务已停止。
|
||||
|
||||
### 9.3 数据传输
|
||||
|
||||
- 小型 JSON 输入使用任务通道传输。
|
||||
- 文件使用内容 SHA-256、声明大小和一次性任务对象令牌传输。
|
||||
- 接收端边读边校验大小和摘要,拒绝路径、符号链接和调用方文件名决定的落盘位置。
|
||||
- 任务正文、文件和输出不进入普通网关审计日志。
|
||||
- 中继模式下是否端到端加密由策略快照明确记录;界面必须显示网关能否读取正文。
|
||||
|
||||
## 10. 能力包与更新
|
||||
|
||||
每个 Package 必须提供不可变版本、目标平台和架构、内容大小、SHA-256、发布者签名、权限
|
||||
声明、依赖和兼容范围。安装流程统一为:
|
||||
|
||||
```text
|
||||
取得签名目录 -> 选择不可变目标 -> 下载暂存 -> 校验大小/摘要/签名
|
||||
-> 展示权限差异 -> 批准 -> 安装到 GoodBuddy-owned 目录 -> 验证 -> 启用
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 下载开始后冻结来源,不在失败时静默切换 ShareServer 或镜像。
|
||||
- 包不得包含凭据;配置只携带非敏感结构和密钥引用。
|
||||
- 新版本扩大文件、进程、网络、数据或委托权限时,旧 Grant 不自动继承。
|
||||
- 紧急撤销阻止新安装和新调用;已安装包的停用或删除遵循组织策略并向用户说明。
|
||||
- 复用现有 GoodBuddy 签名工件原则,但不同用途使用独立签名域。
|
||||
|
||||
## 11. 协议分层
|
||||
|
||||
### 11.1 局域网发现层
|
||||
|
||||
- DNS-SD 服务类型:`_goodbuddy-share._tcp.local`。
|
||||
- 广播仅包含协议 major、随机实例 ID、端口和配对状态。
|
||||
- 设备名称、证书和详细版本通过临时加密握手取得。
|
||||
- 不扫描任意端口,不枚举未主动广播的机器。
|
||||
|
||||
### 11.2 ShareServer 控制 API
|
||||
|
||||
使用 HTTPS 提供登录、设备注册、目录、管理和审计 API;使用 WebSocket 建立设备出站控制
|
||||
通道,承载心跳、任务声明、取消和短期数据通道协商。普通管理 API 与设备通道使用不同凭据
|
||||
和受众。
|
||||
|
||||
### 11.3 数据通道
|
||||
|
||||
数据通道可以是配对 TLS 直连、ShareServer WebSocket 中继或经网关协商的端到端加密流。
|
||||
协议只保证有界帧、顺序、取消和终态,不自行实现另一套拥塞控制。
|
||||
|
||||
### 11.4 版本协商
|
||||
|
||||
- 每层独立声明 `major.minor`。
|
||||
- major 不同拒绝连接;minor 使用双方共同支持的最低能力集合。
|
||||
- 未识别字段在声明允许扩展的位置忽略,安全与授权枚举中的未知值一律拒绝。
|
||||
- 联邦只交换双方共同支持的能力和策略表达式,不降低任一方策略。
|
||||
|
||||
### 11.5 Office 编辑协议
|
||||
|
||||
Office 编辑使用独立的 HTTPS 会话、文件和事件 API,不复用普通远程能力 Task 表达长生命周期
|
||||
编辑器。Desktop 通过设备凭据创建会话和传输源文件;编辑器使用一次性启动授权;ONLYOFFICE
|
||||
使用独立短期 JWT 获取工作副本并提交保存回调。三类凭据使用不同 audience,不能互换。
|
||||
|
||||
文档页签可以长期存在,但启动 URL、文件 URL、callback token 和编辑 Cookie 必须短期、单用途
|
||||
并可撤销。跨端会话、保存和冲突规则见
|
||||
[Office 协同编辑技术设计](../features/office-document-editing/technical-design.md)。
|
||||
|
||||
## 12. 数据与审计
|
||||
|
||||
### 12.1 最小审计事件
|
||||
|
||||
- 操作者、调用设备和组织。
|
||||
- 动作、能力、版本和提供者。
|
||||
- 路由与是否跨组织。
|
||||
- 输入数据类别、分类、文件数和字节数。
|
||||
- 授权结果、策略 ID 和批准者。
|
||||
- 任务状态、耗时、输出大小和资源用量。
|
||||
- 包下载、安装、更新、撤销和发布变更。
|
||||
|
||||
默认不保存提示正文、文件内容、模型输出、密钥、Cookie、认证头或完整私有路径。需要内容
|
||||
审计的组织必须单独定义保留期限和访问角色,并在调用前向用户显示。
|
||||
|
||||
### 12.2 数据权威
|
||||
|
||||
- 桌面端是本地配对、本机共享偏好和本地活动记录的权威。
|
||||
- ShareServer 是组织成员、设备注册、组织策略、发布目录和组织审计的权威。
|
||||
- ShareServer 是 Office 工作副本、服务端保存检查点、编辑会话和保留清理的权威;Desktop 或
|
||||
远程 GoodBuddy Agent 是源文件是否成功回写的权威。
|
||||
- 能力提供者是运行中任务及结果是否产生的权威。
|
||||
- 联邦两端各自保存本组织审计,不复制对方完整成员目录或内部策略。
|
||||
|
||||
## 13. 安全边界
|
||||
|
||||
ShareServer 可以部署到公网,因此公网连接、外部组织和服务端多租户是本设计中的实际信任
|
||||
边界。共同要求如下:
|
||||
|
||||
1. 所有远端清单、名称、schema、错误、文件和模型输出都作为不可信数据处理。
|
||||
2. Renderer 不接触设备私钥、网关令牌、任意 socket 或原始文件传输接口。
|
||||
3. Main/Provider 在执行边界重新验证任务,不信任 Renderer 或 ShareServer 代替本机授权。
|
||||
4. ShareServer 按组织隔离查询,搜索和计数接口不能泄露无权查看的设备或能力。
|
||||
5. 设备只建立主动配置的网关出站连接,不接受 ShareServer 发起任意系统调用。
|
||||
6. 局域网发现不等于信任,组织成员身份也不等于所有能力授权。
|
||||
7. 远程能力不能扩大本机 Ask/Execute、Workspace、Runtime、Skill 或 MCP 现有权限。
|
||||
8. 密钥只进入对应安全存储,诊断和审计使用固定字段与脱敏错误码。
|
||||
9. 限制握手、清单、帧、文件、并发、速率、任务时长和审计查询大小。
|
||||
10. 撤销设备、用户、Grant、Publication、Package 或 FederationTrust 后停止签发新令牌。
|
||||
11. Office 编辑器使用固定 origin 和隔离会话,不获得 Electron、文件系统、Shell、Runtime 或
|
||||
普通 ShareServer 管理 API;Document Server 默认禁止任意出站网络和未批准插件。
|
||||
12. Office 文件正文由服务端实际处理,组织策略必须定义允许格式、大小、保留和内容访问角色,
|
||||
Desktop 在上传前显示这些事实。
|
||||
|
||||
## 14. 建议领域实体
|
||||
|
||||
```text
|
||||
DeviceIdentity
|
||||
PeerPairing
|
||||
GatewayConnection
|
||||
Organization
|
||||
Membership
|
||||
Group
|
||||
RegisteredDevice
|
||||
Capability
|
||||
CapabilityVersion
|
||||
Publication
|
||||
Grant
|
||||
Policy
|
||||
Approval
|
||||
Task
|
||||
TaskAttempt
|
||||
ArtifactTransfer
|
||||
Package
|
||||
PackageRelease
|
||||
FederationTrust
|
||||
AuditEvent
|
||||
Revocation
|
||||
DocumentEditSession
|
||||
DocumentCheckpoint
|
||||
```
|
||||
|
||||
共享协议只定义跨端 DTO 和状态,不规定桌面 SQLite 与 ShareServer 数据库使用相同表结构。
|
||||
|
||||
## 15. 仓库边界
|
||||
|
||||
当前仓库边界(跨仓库文档链接约定两个仓库克隆到同一级目录):
|
||||
|
||||
```text
|
||||
goodbuddy/
|
||||
docs/architecture/share-network-architecture.md
|
||||
docs/features/device-sharing/
|
||||
src/ # GoodBuddy Desktop
|
||||
goodbuddy-shareserver/
|
||||
docs/features/share-server/
|
||||
server/ # 独立 Hono Web 服务
|
||||
shared/ # 当前原型 fixture,不是跨端协议
|
||||
src/ # React 管理前端
|
||||
```
|
||||
|
||||
ShareServer 源码和服务端文档已迁入独立仓库:
|
||||
`ssh://git@git.digiman.live:11022/root/goodbuddy-shareserver.git`。
|
||||
当前服务是交互原型,尚未提供真实 Desktop 集成。共享网络总体设计仍由本文统一维护。
|
||||
|
||||
ShareServer 不反向依赖桌面端 `src/shared`,构建和运行不要求桌面仓库存在。
|
||||
真实跨端协议实现时双方依赖独立 `share-protocol` 包,具体仓库归属和发布方式届时确定,
|
||||
不提前创建空包。协议包不得包含数据库模型、Electron API、服务实现或凭据。
|
||||
|
||||
## 16. 共同验收标准
|
||||
|
||||
- [ ] 不配置 ShareServer 时,两台桌面端可通过局域网发现、双端配对、查看获准能力并调用。
|
||||
- [ ] 关闭“允许被发现”不会关闭主动发现;关闭主动发现不会撤销既有配对。
|
||||
- [ ] 本机没有明确 Publication 时,任何 Peer 或组织成员都不能查看或调用本机能力。
|
||||
- [ ] 桌面端可以连接多个 ShareServer,任一连接故障不影响局域网和其他连接。
|
||||
- [ ] ShareServer 只展示当前用户、设备和组织获准查看的目录。
|
||||
- [ ] 每次远程调用都能说明提供者、执行位置、发送数据、路由、授权依据和委托链。
|
||||
- [ ] 网络中断且副作用结果不确定时进入 `outcome_unknown`,不会自动重放。
|
||||
- [ ] 能力包经大小、SHA-256、签名、兼容和权限差异校验后才能安装。
|
||||
- [ ] 跨组织调用同时满足两端策略;联邦撤销后不能创建新任务。
|
||||
- [ ] 审计默认只保存必要元数据,不保存正文、文件、密钥或认证信息。
|
||||
- [ ] ShareServer 可独立部署和升级,不由 Electron 启动,也不影响桌面端离线使用。
|
||||
- [ ] 启用 Office 服务后,Desktop 可在多实例文档页签中编辑 DOCX、XLSX 和 PPTX;保存回调、
|
||||
源文件冲突或网络故障均不会破坏原文件。
|
||||
- [ ] Office 会话的启动、文件和 callback 凭据不可互换,过期或撤销后不能重用;临时工作副本
|
||||
按策略清理,默认审计不保存文档正文或 AI 选区内容。
|
||||
|
||||
## 17. 相关设计
|
||||
|
||||
- [设备发现与共享 PRD](../features/device-sharing/prd.md)
|
||||
- [设备发现与共享功能逻辑](../features/device-sharing/logic-design.md)
|
||||
- [设备发现与共享 UI](../features/device-sharing/ui-design.md)
|
||||
- [设备发现与共享技术设计](../features/device-sharing/technical-design.md)
|
||||
- [ShareServer PRD](../../../goodbuddy-shareserver/docs/features/share-server/prd.md)
|
||||
- [ShareServer 功能逻辑](../../../goodbuddy-shareserver/docs/features/share-server/logic-design.md)
|
||||
- [ShareServer 管理界面](../../../goodbuddy-shareserver/docs/features/share-server/ui-design.md)
|
||||
- [ShareServer 技术设计](../../../goodbuddy-shareserver/docs/features/share-server/technical-design.md)
|
||||
- [Office 协同编辑](../features/office-document-editing/README.md)
|
||||
- [SSH 远程主机与 Agent](../features/remote-host/README.md)
|
||||
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
|
||||
@@ -12,12 +12,13 @@ below.
|
||||
|
||||
## Release Packaging
|
||||
|
||||
- Unless the user explicitly requests a local Windows package, never run local
|
||||
Windows packaging or launch probes as part of release preparation. In
|
||||
particular, do not run `npm run portable`, Windows
|
||||
`npm run release:package`, direct `electron-builder` packaging, or launch a
|
||||
locally packaged Windows app before publishing a release. Source validation
|
||||
and the native GitHub Actions jobs are the release authorities.
|
||||
- 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
|
||||
@@ -37,9 +38,19 @@ below.
|
||||
`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
|
||||
- Default deliverables are NSIS and portable ZIP for Windows, DMG for
|
||||
macOS, and AppImage, DEB, and RPM for Linux. Every target includes
|
||||
`release-manifest.json` with SHA-256 hashes.
|
||||
- Starting with Desktop 0.12.8, macOS releases contain DMG only. Older macOS
|
||||
GitHub update readers and all older mirror readers require ZIP and cannot
|
||||
validate this new matrix; affected users must upgrade manually from the
|
||||
download page. The user approved this immediate format change. Current
|
||||
readers accept both the historical DMG/ZIP matrix and the DMG-only matrix.
|
||||
- Final Desktop package verification compares every bundled OpenCode
|
||||
configuration file with the prepared dependency tree and rejects external
|
||||
DeepSeek imports in the unpacked Host and chunks. Windows x64 CI also
|
||||
imports the packaged OpenCode plugin and starts an isolated packaged
|
||||
UtilityProcess before publishing; source-only tests do not replace it.
|
||||
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
|
||||
bundled Continue and OpenCode runtimes, executable architecture, and package
|
||||
signatures before atomically replacing a release directory.
|
||||
@@ -49,9 +60,9 @@ below.
|
||||
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
|
||||
- Each independently published Linux x64, Linux arm64, or macOS arm64 `.gbagent` contains the
|
||||
Agent daemon, pinned Node, and the desktop-maintained compatible OpenCode
|
||||
Runtime. Build both architectures on native GitHub Actions runners from one
|
||||
Runtime. Build all three targets on native GitHub Actions runners from one
|
||||
immutable `agent-v<agentVersion>` tag.
|
||||
- Keep the GoodBuddy Agent source, shared protocol/contracts, runtime lock,
|
||||
bundle tooling, and tests in this repository so a desktop commit identifies
|
||||
@@ -65,7 +76,9 @@ below.
|
||||
digest, use only an ephemeral in-memory test signing identity, and verify the
|
||||
resulting compound package and deterministic archive. It must not read
|
||||
production signing secrets, publish installable release artifacts, or modify
|
||||
the checked-in public key registry.
|
||||
the checked-in public key registry. The Darwin arm64 target uses `macos-15`
|
||||
and the same production locks. Native CI checks do not replace real Host
|
||||
installation, Attach, Ask/Execute, and lifecycle validation during development.
|
||||
- `.github/workflows/agent-release.yml` is the only production compound Agent
|
||||
publication path. It requires an annotated immutable Agent tag, the protected
|
||||
`agent-signing` Environment, native x64/arm64 builds, one production
|
||||
@@ -97,11 +110,22 @@ below.
|
||||
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;
|
||||
- Store approved bilingual Agent release notes in
|
||||
`resources/agent-release-notes/<agentVersion>.md`. The Agent publication job
|
||||
reads that exact file instead of generating a generic release description;
|
||||
keep it aligned with the approved version and source range.
|
||||
- The tagged commit must be reachable from protected `main`. The three native
|
||||
jobs build Linux x64, Linux arm64, and Darwin arm64 packages from the same source and locks;
|
||||
the catalog job rejects missing architectures, changed bytes for an existing
|
||||
version/architecture identity, invalid prior signatures, and incompatible
|
||||
version/platform/architecture identity, invalid prior signatures, and incompatible
|
||||
matrix metadata.
|
||||
- All three targets share the existing signed `agent-catalog.json`. Before the
|
||||
first mixed-platform catalog is published, release the Desktop reader that
|
||||
accepts Darwin entries and document the required Desktop upgrade. Older
|
||||
Desktop readers reject the entire mixed catalog and cannot check online Agent
|
||||
updates; preserve their locally verified packages and immutable historical
|
||||
releases. Do not introduce a second catalog or silently weaken package
|
||||
signature and integrity verification.
|
||||
- Agent GitHub Releases are ordinary non-draft, non-prerelease releases but
|
||||
must use `--latest=false`; they must never replace the desktop release marked
|
||||
Latest. Publish immutable packages and the versioned catalog to
|
||||
@@ -141,23 +165,24 @@ not require release notes.
|
||||
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`, the required source validators, the
|
||||
production build. The six native CI jobs remain the cross-platform
|
||||
authority; do not add local packaging or packaged-app launch probes unless
|
||||
the user explicitly requests them.
|
||||
`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. Fetch both remotes immediately before tagging. Inspect any remote branch
|
||||
movement instead of overwriting or silently merging it. Confirm the working
|
||||
tree is clean, the candidate tag is unused locally and remotely, and the
|
||||
exact approved commit has not changed.
|
||||
8. Only after all previous steps pass, create an annotated
|
||||
`v${package.version}` tag at the exact approved commit. Push `main` to
|
||||
`origin` and `github`, verify both branch SHAs, then push the tag to both
|
||||
remotes and verify each peeled tag SHA (`refs/tags/<tag>^{}`) equals the
|
||||
release commit.
|
||||
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
|
||||
@@ -200,12 +225,12 @@ surface together before a new release.
|
||||
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
|
||||
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
|
||||
creating or publishing the GitHub Release. Update
|
||||
`releases/latest.json` only after the GitHub Release is public and all prior
|
||||
checks succeeded.
|
||||
- The expected GitHub Release contains 22 assets: 14 installers (two formats
|
||||
for each Windows/macOS target and three formats for each Linux target), six
|
||||
- The expected GitHub Release contains 20 assets: 12 installers (two formats
|
||||
for each Windows target, DMG for each macOS target, and three formats for each Linux target), six
|
||||
renamed target manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
|
||||
`site-release.json` is an OSS publication artifact, not a GitHub Release
|
||||
asset.
|
||||
@@ -258,17 +283,17 @@ Do not report a release complete until all of the following are verified:
|
||||
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
|
||||
3. GitHub release metadata contains exactly the expected 20 uploaded asset
|
||||
names, including the expected Windows x64 and arm64 assets. Do not download,
|
||||
hash, launch, or send individual network probes to published installers
|
||||
unless the user explicitly requests that additional validation.
|
||||
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
|
||||
version, exact six targets and 14 installer entries, the trusted Beijing
|
||||
version, exact six targets and 12 installer entries, the trusted Beijing
|
||||
URLs, and the GitHub fallback URL. It must match the immutable
|
||||
`releases/<tag>/site-release.json`. Fetching and comparing these small JSON
|
||||
metadata files is sufficient; rely on the successful publication job for
|
||||
its per-asset public checks.
|
||||
5. The live website successfully fetches the index and produces the 14 correct
|
||||
5. The live website successfully fetches the index and produces the 12 correct
|
||||
platform/architecture/format links from that metadata.
|
||||
6. Both remote `main` refs and both peeled tag refs still equal the approved
|
||||
release commit, and the local working tree is clean.
|
||||
|
||||
@@ -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,34 @@
|
||||
# 应用导航与系统工具入口
|
||||
|
||||
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,应用占用主导航,系统工具从
|
||||
侧栏底部快捷打开助手工作栏。该功能同时为后续插件应用和插件工具预留统一的入口类型,但
|
||||
不包含插件市场的安装与运行机制。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、产品边界、功能要求与产品验收 |
|
||||
| [User Stories](./user-stories.md) | 用户场景及 Given/When/Then 验收 |
|
||||
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
|
||||
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
|
||||
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| 系统入口 | GoodBuddy 工作流程或审计所必需的固定入口,用户不能卸载、隐藏或改变类型 |
|
||||
| 应用 | 在主内容区打开完整一级页面的功能,可显示在“当前对话”所在的主导航区 |
|
||||
| 系统工具 | GoodBuddy 自带且不可卸载的辅助能力,包括文件、终端、浏览器和资源监控,从侧栏底部打开助手工作栏中的对应面板 |
|
||||
| 插件应用 | 未来由插件提供的一级页面入口 |
|
||||
| 插件工具 | 未来由插件提供的辅助面板或命令入口 |
|
||||
| 快捷入口 | 只负责打开或聚焦能力,不复制该能力的界面和业务状态 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md)
|
||||
- [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)
|
||||
- [助手工作栏多终端页签 PRD](../assistant-workbar/terminal-tabs-prd.md)
|
||||
- [智能心跳 PRD](../smart-heartbeat/prd.md)
|
||||
- [知识库功能文档](../knowledge-base/README.md)
|
||||
@@ -0,0 +1,168 @@
|
||||
# 应用导航与系统工具入口功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 关联 User Stories | [User Stories](./user-stories.md) |
|
||||
|
||||
## 1. 不变量
|
||||
|
||||
1. “当前对话”“运行记录”和“设置”始终是系统入口。
|
||||
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
|
||||
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
|
||||
4. 左下角只提供快捷入口,工具内容仍由助手工作栏承载。
|
||||
5. 隐藏应用不等于停用功能,也不删除、迁移或清理业务数据。
|
||||
6. 快捷入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
|
||||
|
||||
## 2. 入口模型
|
||||
|
||||
```ts
|
||||
type NavigationContribution =
|
||||
| {
|
||||
type: 'system-entry'
|
||||
id: 'chat' | 'activity' | 'settings'
|
||||
placement: 'primary-start' | 'primary-end' | 'footer'
|
||||
}
|
||||
| {
|
||||
type: 'application'
|
||||
id: 'magic-notes' | 'knowledge' | 'heartbeat'
|
||||
source: 'builtin'
|
||||
visible: boolean
|
||||
}
|
||||
| {
|
||||
type: 'system-tool'
|
||||
id: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
workbarAppId: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
}
|
||||
```
|
||||
|
||||
该模型只覆盖首期入口。插件贡献模型随插件 Host、权限和生命周期另行设计。
|
||||
|
||||
## 3. 主导航组合规则
|
||||
|
||||
主导航按以下顺序计算:
|
||||
|
||||
```text
|
||||
当前对话
|
||||
可见的内置应用
|
||||
运行记录
|
||||
```
|
||||
|
||||
当前对话和运行记录不参与排序。首期内置应用默认顺序为魔法笔记、知识库、智能心跳;显隐
|
||||
设置只从中移除对应项,不改变固定入口的位置。
|
||||
|
||||
### 3.1 应用可见性与业务状态
|
||||
|
||||
| 导航可见 | 功能启用 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 是 | 是 | 显示入口并允许进入 |
|
||||
| 否 | 是 | 隐藏入口,后台和数据保持原状 |
|
||||
| 是 | 否 | 不显示不可用入口;设置页保留启用路径 |
|
||||
| 否 | 否 | 不显示入口,保留符合该功能自身规则的数据 |
|
||||
|
||||
“功能启用”由各应用现有产品设置负责。例如魔法笔记启停仍使用原设置;导航偏好不能覆盖它。
|
||||
|
||||
## 4. 系统工具打开规则
|
||||
|
||||
### 4.1 通用流程
|
||||
|
||||
```text
|
||||
用户激活工具按钮
|
||||
-> 校验按钮仍可用
|
||||
-> 向助手工作栏发送打开意图
|
||||
-> 工作栏按应用实例策略查找或创建实例
|
||||
-> 展开工作栏
|
||||
-> 激活目标实例
|
||||
-> 把焦点移入面板标题或首个主要控件
|
||||
```
|
||||
|
||||
若创建失败,工作栏保持展开并在对应能力位置显示错误;主页面和其他工作栏实例不被清空。
|
||||
|
||||
### 4.2 决策表
|
||||
|
||||
| 工具 | 匹配实例存在 | 匹配实例不存在 | 上下文不可用 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文件 | 激活不可关闭的单实例并保留其跟随或固定状态 | 仅在迁移或布局修复时补回 `workspace` | 打开面板并显示目录修复入口 |
|
||||
| 终端 | 激活当前执行空间最近的 `starting` 或 `running` 实例 | 创建一个绑定当前执行空间的实例 | 没有项目时使用本机 Home |
|
||||
| 浏览器 | 激活绑定当前 Conversation 的最近使用实例 | 创建绑定当前 Conversation 的新实例 | 激活最近使用实例;不存在时创建待绑定实例 |
|
||||
| 资源监控 | 激活 `resources` 单实例 | 创建本机 `resources` 单实例 | 部分指标不可用时显示原因并保留其余指标 |
|
||||
|
||||
文件保持单一实例原有绑定。浏览器快捷入口按当前 Conversation 匹配最近使用实例,而工作栏
|
||||
“+”始终新建浏览器实例。固定目标失效时显示该目标的修复状态,不回退到当前项目、默认
|
||||
工作区或当前会话。
|
||||
|
||||
`closing`、`exited`、`interrupted` 和 `failed` 终端不属于可复用实例。当前执行空间只有这些
|
||||
终端时,快捷按钮创建新实例并保留原 Tab 的历史状态。用户需要额外的并行终端时使用工作栏
|
||||
“+”或终端面板中的既有创建命令。
|
||||
|
||||
### 4.3 选中与状态
|
||||
|
||||
工具按钮满足以下条件时显示选中:助手工作栏已展开,且当前活动实例的 `appId` 与工具映射
|
||||
一致。只有实例存在但未激活时不显示选中。
|
||||
|
||||
徽标优先级为:等待用户处理 > 错误 > 运行中 > 无徽标。同一按钮只显示一个视觉徽标,
|
||||
可访问名称包含完整状态。
|
||||
|
||||
资源监控首期不在后台采样,因此不显示负载告警徽标。只有资源面板是工作栏当前活动实例且
|
||||
工作栏已展开时,资源按钮才显示选中状态。
|
||||
|
||||
### 4.4 资源采样状态
|
||||
|
||||
```text
|
||||
inactive -> warming -> loading -> live
|
||||
-> partial
|
||||
-> error
|
||||
live/partial/error -> loading
|
||||
warming/loading/live/partial/error -> inactive
|
||||
```
|
||||
|
||||
- `inactive`:资源面板未处于可见活动状态,没有采样定时器。
|
||||
- `warming`:首次进入或停止后恢复,建立 CPU 和磁盘 IO 区间基线;内存仍可立即显示。
|
||||
- `loading`:正在请求后续样本;请求未结束时跳过后续时钟触发。
|
||||
- `live`:所有首期指标可用,按 2 秒间隔追加样本。
|
||||
- `partial`:至少一个指标可用;不可用项携带稳定错误码,其他指标继续更新。
|
||||
- `error`:本次没有任何可用指标,保留已有历史;自动时钟或手动操作可以重试。
|
||||
|
||||
资源面板为活动实例、工作栏展开且 `document.visibilityState` 为 `visible` 时才离开 `inactive`。
|
||||
任一条件失效即停止定时器并重置区间基线。历史队列只保留最后 150 个样本;时间间隔较长或
|
||||
采样失败时保留真实时间缺口,不补零或伪造中间数据。
|
||||
|
||||
## 5. 设置保存规则
|
||||
|
||||
应用导航偏好与应用业务启停分开保存。Switch 每次切换立即提交完整目标集合;请求完成前禁用
|
||||
三个应用显隐 Switch,下一次操作基于最新确认结果提交,防止并发更新互相覆盖。
|
||||
|
||||
保存失败时:
|
||||
|
||||
- 主导航和 Switch 恢复最后一次成功保存的偏好。
|
||||
- 设置行显示可重试错误。
|
||||
- 不回滚或修改各应用的业务状态。
|
||||
|
||||
## 6. 与需求的对应关系
|
||||
|
||||
| 逻辑 | PRD | User Stories |
|
||||
| --- | --- | --- |
|
||||
| 固定入口不变量 | FR-1、FR-2 | US-A1、US-A2 |
|
||||
| 应用显隐和失败恢复 | FR-3、FR-4、FR-11、FR-12 | US-B1、US-B2、US-B4、US-B5 |
|
||||
| 工具打开决策表 | FR-5 至 FR-10 | US-C1 至 US-C7 |
|
||||
| 资源采样状态 | FR-9 | US-C6 |
|
||||
| 无障碍与窄窗口 | FR-13 | US-D1、US-D2 |
|
||||
|
||||
## 7. 逻辑完整性评估
|
||||
|
||||
首期规则已覆盖固定入口、应用显隐、保存失败、系统工具匹配与显式多实例差异、本机 Home、
|
||||
托管 SSH、无活动会话、目录失效、资源部分可用、采样停止、权限边界、键盘和窄窗口。其他
|
||||
工具面板内部的取消、超时、
|
||||
连接恢复和固定目标失效继续由助手工作栏负责,本功能不复制这些状态机。
|
||||
|
||||
## 8. 后续产品决策
|
||||
|
||||
- 插件工具上线后,快捷栏允许固定的最大数量及溢出规则。
|
||||
- 插件应用是否允许用户排序,以及固定系统入口之间允许的排序范围。
|
||||
|
||||
这些决策不影响首期四个系统工具的固定入口和内置应用显隐实现。
|
||||
@@ -0,0 +1,177 @@
|
||||
# 应用导航与系统工具入口 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 关联设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[助手工作栏 PRD](../assistant-workbar/prd.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
主侧栏当前把“当前对话”“魔法笔记”“知识库”“智能心跳”和“运行记录”放在同一导航组,
|
||||
底部长期显示“GB 本地工作区”和平台架构。底部信息不能反映用户可以执行的操作,文件、终端
|
||||
和浏览器则需要先找到右侧助手工作栏后才能打开。
|
||||
|
||||
后续插件可能提供完整页面,也可能只提供辅助面板或命令。若不先区分系统入口、应用和工具,
|
||||
插件接入后会同时争用主导航、侧栏底部和助手工作栏,用户也无法判断入口会切换页面还是打开
|
||||
辅助面板。
|
||||
|
||||
## 2. 产品定义
|
||||
|
||||
### 2.1 三类入口
|
||||
|
||||
| 类型 | 首期成员 | 默认位置 | 用户控制范围 |
|
||||
| --- | --- | --- | --- |
|
||||
| 系统入口 | 当前对话、运行记录、设置 | 主导航固定位置或侧栏底部 | 不可隐藏、卸载或改变类型 |
|
||||
| 应用 | 魔法笔记、知识库、智能心跳 | 主导航应用区 | 可控制是否显示;隐藏不删除数据 |
|
||||
| 系统工具 | 文件浏览器、终端、浏览器、资源监控 | 侧栏底部快捷工具栏 | 首期固定提供,不可卸载 |
|
||||
|
||||
运行记录是系统审计页面,不属于工具。文件浏览器、终端、浏览器和资源监控是系统工具;它们
|
||||
的快捷入口位于左下角,实际界面继续由右侧助手工作栏承载。
|
||||
|
||||
### 2.2 后续插件类型
|
||||
|
||||
插件市场开放后,插件可以贡献“插件应用”或“插件工具”。插件不能注册、覆盖或替换系统入口,
|
||||
也不能冒充系统工具。内置应用继续标记为“内置”,不因采用统一注册协议而显示成待安装商品。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 用户从侧栏底部一次点击即可打开文件、终端、浏览器或资源监控。
|
||||
- 主导航只承载完整页面,辅助工具不占用一级页面位置。
|
||||
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
|
||||
- 内置应用可以从一个设置位置控制主导航可见性,隐藏后仍可恢复。
|
||||
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
- 首期不提供第三方插件搜索、下载、安装、更新、签名验证或卸载。
|
||||
- 不把文件树、终端或浏览器重新实现到左侧栏。
|
||||
- 不改变助手工作栏的面板实例、停靠、执行空间和安全边界。
|
||||
- 不允许隐藏运行记录、当前对话或设置。
|
||||
- 不因隐藏应用而删除魔法笔记、知识库或智能心跳数据。
|
||||
- 不把“显示在侧边栏”解释为停止智能心跳计划或后台任务。
|
||||
- 不提供任意本机程序启动器。
|
||||
- 首期资源监控不采集远程主机、GPU、显存或网络吞吐,不提供后台告警和历史持久化。
|
||||
|
||||
## 5. 功能要求
|
||||
|
||||
### FR-1 入口分类
|
||||
|
||||
系统必须为每个可见入口声明稳定类型:`system-entry`、`application` 或 `system-tool`。界面位置、
|
||||
设置项和点击结果均以该类型为准,不根据当前页面临时改变类型。
|
||||
|
||||
### FR-2 固定系统入口
|
||||
|
||||
“当前对话”固定在主导航首位;“运行记录”固定在应用区之后;“设置”固定在侧栏底部。
|
||||
这些入口不出现在应用显隐、工具固定或插件管理设置中。
|
||||
|
||||
### FR-3 内置应用区
|
||||
|
||||
魔法笔记、知识库和智能心跳显示在当前对话与运行记录之间。应用入口打开对应一级页面,保留
|
||||
现有徽标、范围和页面状态。用户可以在设置中控制其主导航可见性。
|
||||
|
||||
### FR-4 隐藏应用
|
||||
|
||||
隐藏应用只移除主导航入口,不删除数据、不修改业务启停状态,也不终止正在进行的操作。显隐
|
||||
Switch 更新成功后主导航立即生效;更新失败时恢复原状态并允许重试。用户可从设置恢复入口。
|
||||
|
||||
### FR-5 系统工具快捷栏
|
||||
|
||||
侧栏底部提供文件浏览器、终端、浏览器和资源监控四个图标按钮,并保留独立设置按钮。每个
|
||||
按钮具有可见工具提示和可访问名称。首期系统工具顺序固定为文件、终端、浏览器、资源监控。
|
||||
|
||||
### FR-6 文件浏览器入口
|
||||
|
||||
点击文件按钮时展开助手工作栏,并聚焦默认存在且不可关闭的“工作区”单实例。该实例保持
|
||||
“跟随当前上下文”或“固定到指定对象”状态;迁移或布局修复发现实例缺失时补回,并默认跟随
|
||||
当前项目。没有项目时使用已配置的默认工作区。跟随上下文所需的路径不可用时,面板显示修复
|
||||
入口;已有有效固定目标不受当前项目和默认工作区状态影响。
|
||||
|
||||
### FR-7 终端入口
|
||||
|
||||
点击终端按钮时展开助手工作栏。当前执行空间有正在启动或运行中的终端时聚焦最近使用的实例;
|
||||
没有时创建一个绑定当前执行空间的新终端,已退出、已中断或失败的历史 Tab 保持不变。没有
|
||||
项目时当前执行空间为本机 Home。创建额外并行终端继续使用工作栏现有的“+”目录入口。
|
||||
|
||||
### FR-8 浏览器入口
|
||||
|
||||
点击浏览器按钮时展开助手工作栏,并优先聚焦绑定当前 Conversation 的最近使用浏览器实例;
|
||||
没有匹配实例时创建一个。没有活动 Conversation 时聚焦最近使用的浏览器实例;完全不存在时
|
||||
创建待绑定空实例。该快捷入口不会因连续点击创建重复实例;用户通过工作栏“+”选择浏览器时
|
||||
则始终创建新实例。入口不打开系统默认浏览器,也不创建旁路 WebView。
|
||||
|
||||
### FR-9 资源监控入口
|
||||
|
||||
点击资源监控按钮时展开助手工作栏,并打开或聚焦单实例的本机“资源监控”面板。面板显示
|
||||
整机 CPU 使用率、内存使用量与总量、磁盘读写速率,以及 GoodBuddy 桌面进程的 CPU、内存
|
||||
和进程数摘要。指标不可用时显示具体项目和原因,不能显示为 `0`。
|
||||
|
||||
资源面板处于可见活动状态、工作栏已展开且应用窗口未隐藏或最小化时,每 2 秒采样一次,在
|
||||
Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止采样;首期不在后台持续监控,
|
||||
也不据此产生侧栏告警。首次采样及停止后恢复的首次采样只建立 CPU 和磁盘 IO 速率基线,
|
||||
不显示伪造的 `0` 值。
|
||||
|
||||
### FR-10 快捷入口状态
|
||||
|
||||
工具面板已激活时,对应按钮显示选中状态;面板存在但未激活时保持普通状态。运行中、错误或
|
||||
等待用户处理可以显示徽标,但必须有可访问文字,且不得自动抢占当前面板。
|
||||
|
||||
### FR-11 应用导航设置
|
||||
|
||||
设置页提供“应用与工具”管理入口。首期“应用”区域列出三个内置应用并提供“显示在侧边栏”
|
||||
Switch,切换后立即持久化。系统工具区域列出四项内置固定能力,不提供虚假的卸载或停用按钮。
|
||||
|
||||
### FR-12 偏好持久化
|
||||
|
||||
应用可见性按设备持久化。升级时保留当前行为:已有可用内置应用继续显示;魔法笔记原有功能
|
||||
启停设置保持原意,不由新的导航可见性覆盖。
|
||||
|
||||
### FR-13 窄窗口行为
|
||||
|
||||
窄窗口下主侧栏仍按现有抽屉方式显示。系统工具按钮保持单行且不与设置按钮重叠;空间不足时
|
||||
收起文字和非必要分隔,不隐藏四个系统工具或设置。
|
||||
|
||||
## 6. 权限与安全
|
||||
|
||||
- 快捷入口不授予新的文件、命令、网络或浏览器权限。
|
||||
- 系统工具继续使用 Main、Preload、执行空间 Grant、Ask/Execute 和现有审批边界。
|
||||
- Renderer 不因新增入口获得任意文件系统、PTY、进程或 Electron API。
|
||||
- 资源指标由 Main 采集并以有界结构返回;Renderer 不接收进程命令行、环境变量或任意 PID。
|
||||
- 资源样本只保存在当前 Renderer 内存中,不写入运行记录、SQLite 或遥测服务。
|
||||
- 插件贡献类型只是界面与导航声明,不能代替权限清单、用户授权和运行隔离。
|
||||
- 运行记录继续记录由系统工具触发的受审计操作。
|
||||
|
||||
## 7. 产品验收
|
||||
|
||||
- 主导航能明确区分固定系统入口和可配置内置应用。
|
||||
- 运行记录在所有应用显隐组合下保持可见并可打开。
|
||||
- 左下角不再用“本地工作区”与系统架构占据主要空间。
|
||||
- 文件、终端、浏览器和资源监控均可从左下角打开正确的工作栏面板,且不产生第二套内容界面。
|
||||
- 连续点击系统工具不会无意创建重复的浏览器或终端实例;工作区始终复用不可关闭的固定实例。
|
||||
用户通过工作栏“+”可以显式创建额外浏览器或终端实例。
|
||||
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
|
||||
最小化后停止请求新样本。
|
||||
- CPU、内存、磁盘 IO 或 GoodBuddy 指标不可用时,页面区分部分可用与整体失败。
|
||||
- 隐藏内置应用不删除数据、不停止后台计划,并能从设置恢复;保存失败时入口保持原状。
|
||||
- 键盘、屏幕阅读器、浅色主题、深色主题和窄窗口均可完成相同操作。
|
||||
|
||||
## 8. 分期
|
||||
|
||||
### 首期
|
||||
|
||||
- 重构侧栏底部,增加四个系统工具按钮和独立设置按钮。
|
||||
- 建立从 App 到助手工作栏的“打开或聚焦工具”意图接口。
|
||||
- 增加本机资源快照采集与资源监控单实例面板。
|
||||
- 增加内置应用的主导航显示设置。
|
||||
- 保持运行记录为固定系统入口。
|
||||
|
||||
### 后续
|
||||
|
||||
- 插件应用和插件工具注册。
|
||||
- 插件工具固定、取消固定和排序。
|
||||
- 插件市场、权限详情、安装和卸载。
|
||||
- 用户级快捷键与命令面板接入同一贡献命令。
|
||||
@@ -0,0 +1,383 @@
|
||||
# 应用导航与系统工具入口技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| UI 设计 | [UI 设计](./ui-design.md) |
|
||||
|
||||
## 1. 现状
|
||||
|
||||
主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换:
|
||||
|
||||
```ts
|
||||
type WorkspaceView =
|
||||
| 'chat'
|
||||
| 'magic-notes'
|
||||
| 'knowledge'
|
||||
| 'heartbeat'
|
||||
| 'activity'
|
||||
| 'settings'
|
||||
```
|
||||
|
||||
文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts`
|
||||
承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程
|
||||
实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控
|
||||
面板。现有侧栏底部 `user-card` 整行打开设置,并显示“本地工作区”和平台架构。
|
||||
|
||||
本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/
|
||||
Renderer 最小实现。文件、终端和浏览器后端保持不变。
|
||||
|
||||
## 2. 模块边界
|
||||
|
||||
| 模块 | 责任 |
|
||||
| --- | --- |
|
||||
| `App.tsx` | 组合主导航、系统工具栏、设置入口和页面离开检查 |
|
||||
| `SidebarSystemTools`(新增) | 渲染四个系统工具按钮并发送打开意图 |
|
||||
| `RightAssistantSidebar.tsx` | 接收打开意图,调用工作栏实例创建或激活逻辑 |
|
||||
| `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 |
|
||||
| `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 |
|
||||
| `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 |
|
||||
| `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 |
|
||||
| `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 |
|
||||
| `application-settings-contracts.ts` | 增加应用导航偏好 schema 和默认值 |
|
||||
| `application-settings-store.ts` | 迁移并原子保存应用导航偏好 |
|
||||
| `PlatformFeaturesSettingsSection` 或独立设置 Section | 编辑“显示在侧边栏”偏好 |
|
||||
|
||||
不要在 `SidebarSystemTools` 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送
|
||||
有界的工具 ID。
|
||||
|
||||
## 3. Renderer 契约
|
||||
|
||||
### 3.1 系统工具 ID
|
||||
|
||||
```ts
|
||||
export const sidebarSystemToolIds = [
|
||||
'workspace',
|
||||
'terminal',
|
||||
'browser',
|
||||
'resources'
|
||||
] as const
|
||||
|
||||
export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number]
|
||||
```
|
||||
|
||||
该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer
|
||||
状态;实际能力调用继续走现有 Preload API。
|
||||
|
||||
### 3.2 打开意图
|
||||
|
||||
`App` 向 `RightAssistantSidebar` 传入单调递增的请求或命令对象:
|
||||
|
||||
```ts
|
||||
type WorkbarOpenIntent = {
|
||||
requestId: number
|
||||
appId: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
reason: 'sidebar-system-tool'
|
||||
context?: { conversationId?: string; projectId?: string }
|
||||
}
|
||||
```
|
||||
|
||||
`requestId` 使连续点击同一工具仍能被消费。不要仅设置 `assistantSidebarTab`,因为用户可能已
|
||||
关闭对应实例,且终端需要按执行空间寻找实例。
|
||||
|
||||
建议由 `RightAssistantSidebar` 在消费意图后通过现有 `WorkbarShell` 控制接口完成。控制器必须
|
||||
接收上下文和调用原因,不能只按 `appId` 查找第一个实例:
|
||||
|
||||
```ts
|
||||
type WorkbarController = {
|
||||
openOrActivate(intent: WorkbarOpenIntent): Promise<WorkbarTabInstance>
|
||||
}
|
||||
```
|
||||
|
||||
若当前 `WorkbarShell` 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。
|
||||
|
||||
工作区意图始终激活注册表中的不可关闭单实例;仅在迁移或布局损坏时补回。浏览器意图按
|
||||
`conversationId` 激活最近使用的匹配实例,没有匹配实例时创建一个。工作栏“+”不走该去重
|
||||
路径,选择浏览器时始终创建新实例。
|
||||
|
||||
### 3.3 终端匹配
|
||||
|
||||
工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用:
|
||||
|
||||
1. 解析当前项目对应的 `ExecutionSpaceSelection`。
|
||||
2. 在现有终端实例中按目标 identity 查找 `starting` 或 `running` 状态。
|
||||
3. 存在多个匹配实例时激活最近使用的实例。
|
||||
4. 没有匹配实例时调用现有终端创建流程。
|
||||
|
||||
`closing`、`exited`、`interrupted` 和 `failed` 实例保留为历史 Tab,不阻止快捷按钮创建新终端。
|
||||
工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。
|
||||
|
||||
### 3.4 资源监控工作栏应用
|
||||
|
||||
在 `workbar-contracts.ts` 中增加 `resources`:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 'resources',
|
||||
instancePolicy: 'single',
|
||||
defaultContext: 'application'
|
||||
}
|
||||
```
|
||||
|
||||
该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建
|
||||
或聚焦它。
|
||||
|
||||
## 4. 应用导航偏好
|
||||
|
||||
### 4.1 设置结构
|
||||
|
||||
在 `ApplicationSettings` 增加独立字段:
|
||||
|
||||
```ts
|
||||
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat'
|
||||
|
||||
type ApplicationNavigationSettings = {
|
||||
hiddenBuiltInApplicationIds: BuiltInApplicationId[]
|
||||
}
|
||||
```
|
||||
|
||||
默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔
|
||||
字段;Zod schema 必须去重、限制数量并拒绝未知内置
|
||||
ID。
|
||||
|
||||
首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留
|
||||
规则后另行扩展,不提前混入当前 schema。
|
||||
|
||||
### 4.2 与现有开关组合
|
||||
|
||||
入口可见条件为:
|
||||
|
||||
```ts
|
||||
const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id)
|
||||
```
|
||||
|
||||
`magicNotesEnabled` 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合
|
||||
规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置
|
||||
不能写回现有开关。
|
||||
|
||||
### 4.3 持久化与 IPC
|
||||
|
||||
沿用现有应用设置链路:
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> window.goodbuddy.updates.updateSettings
|
||||
-> settings:application:update
|
||||
-> application-settings-store
|
||||
-> application-settings.json
|
||||
```
|
||||
|
||||
更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不
|
||||
修改已有 `magicNotesEnabled` 或其他平台设置。
|
||||
|
||||
## 5. 侧栏底部重构
|
||||
|
||||
现有 `user-card` 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮:
|
||||
|
||||
```tsx
|
||||
<div className="sidebar-footer">
|
||||
<SidebarSystemTools onOpenTool={openSystemTool} />
|
||||
<button aria-label={t('navigation.settings')} ...>
|
||||
<Settings aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
|
||||
保留设置预加载、焦点恢复和未保存离开确认。
|
||||
|
||||
系统工具按钮调用统一 `openSystemTool`:
|
||||
|
||||
1. 设置工作栏打开意图。
|
||||
2. 展开工作栏。
|
||||
3. 由工作栏创建或激活实例。
|
||||
4. 按现有策略把焦点移入目标面板。
|
||||
|
||||
工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。
|
||||
|
||||
## 6. 工作栏状态与快捷按钮
|
||||
|
||||
`RightAssistantSidebar` 应向 `App` 暴露当前活动实例的 `appId` 和必要的有界状态摘要,或由
|
||||
现有 `onTabChange` 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。
|
||||
|
||||
```ts
|
||||
type SidebarSystemToolState = {
|
||||
activeToolId?: SidebarSystemToolId
|
||||
statusByToolId?: Partial<Record<SidebarSystemToolId, 'attention' | 'error' | 'running'>>
|
||||
}
|
||||
```
|
||||
|
||||
首期可以只实现 `activeToolId`,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误
|
||||
或运行状态。
|
||||
|
||||
## 7. 应用显隐更新
|
||||
|
||||
应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只
|
||||
允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航
|
||||
随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功
|
||||
保存的设置。
|
||||
|
||||
## 8. 插件兼容边界
|
||||
|
||||
首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。
|
||||
插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek
|
||||
Harness 的 npm 插件市场。
|
||||
|
||||
## 9. 本机资源监控
|
||||
|
||||
### 9.1 采集范围
|
||||
|
||||
首期在 Main 中使用经过依赖审查的 `systeminformation` 获取整机 CPU、内存和磁盘 IO,使用
|
||||
Electron `app.getAppMetrics()` 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收
|
||||
Renderer 提供的路径、PID、设备名或命令。
|
||||
|
||||
首期指标定义:
|
||||
|
||||
- CPU:整机忙碌时间占比,规范化为 `0` 至 `100`。
|
||||
- 内存:总字节数、可用字节数和 `总量 - 可用量` 得到的使用量。
|
||||
- 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。
|
||||
- GoodBuddy:`app.getAppMetrics()` 返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程
|
||||
CPU 先相加,再按逻辑处理器数量归一化为整机容量占比 `0` 至 `100`;不能通过截断总和处理
|
||||
多核数值。
|
||||
|
||||
受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回
|
||||
不可用错误码,不能回退为 `0`。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链
|
||||
检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。
|
||||
|
||||
### 9.2 快照契约
|
||||
|
||||
```ts
|
||||
type ResourceMetric<T> =
|
||||
| { status: 'available'; value: T }
|
||||
| { status: 'warming-up' }
|
||||
| { status: 'unavailable'; reason: ResourceMetricUnavailableReason }
|
||||
|
||||
type LocalResourceSnapshot = {
|
||||
sampledAt: string
|
||||
host: {
|
||||
cpuPercent: ResourceMetric<number>
|
||||
memory: ResourceMetric<{
|
||||
usedBytes: number
|
||||
totalBytes: number
|
||||
}>
|
||||
diskIo: ResourceMetric<{
|
||||
readBytesPerSecond: number
|
||||
writeBytesPerSecond: number
|
||||
}>
|
||||
}
|
||||
goodbuddy: {
|
||||
cpuPercent: ResourceMetric<number>
|
||||
memoryBytes: ResourceMetric<number>
|
||||
processCount: ResourceMetric<number>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有数字必须有限且非负;百分比限制为 `0` 至 `100`,字节数设置跨 IPC 上限。`sampledAt` 由
|
||||
Main 生成。不可用原因使用稳定枚举,例如 `unsupported-platform`、`permission-denied`、
|
||||
`collector-timeout` 和 `collector-failed`,不向 Renderer 返回原始命令输出。`warming-up` 只用于
|
||||
依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。
|
||||
|
||||
### 9.3 Preload 与 IPC
|
||||
|
||||
Preload 只暴露:
|
||||
|
||||
```ts
|
||||
resourceMonitor.getLocalSnapshot(input: {
|
||||
resetIntervalBaseline: boolean
|
||||
}): Promise<LocalResourceSnapshot>
|
||||
```
|
||||
|
||||
对应 IPC 为 `resource-monitor:local-snapshot:get`。Main 验证可信 sender;单次采集设置超时,
|
||||
并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回
|
||||
有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集
|
||||
任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。
|
||||
|
||||
合并规则区分普通请求与基线重置请求:
|
||||
|
||||
- 普通请求在途时,新的普通请求复用它。
|
||||
- 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能
|
||||
用普通请求结果满足重置请求。
|
||||
- 多个重置请求复用同一个在途或待执行重置。
|
||||
- 重置请求在途时,普通请求复用其 `warming-up` 结果,下一次时钟再取得区间值。
|
||||
|
||||
因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列
|
||||
长度。
|
||||
|
||||
面板每次从非活动状态恢复后的首次请求传入 `resetIntervalBaseline: true`。Main 消费本次
|
||||
`systeminformation` 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 `warming-up`;
|
||||
内存等瞬时指标正常返回。后续请求传入 `false` 并计算当前区间值,避免把面板停用期间计入
|
||||
速率。
|
||||
|
||||
### 9.4 轮询与历史
|
||||
|
||||
`ResourceMonitorPanel` 仅在工作栏展开、`resources` 为活动实例且
|
||||
`document.visibilityState === 'visible'` 时启动轮询:
|
||||
|
||||
1. 进入活动状态后立即请求一次,并设置 `resetIntervalBaseline: true`。
|
||||
2. 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。
|
||||
3. Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。
|
||||
4. Renderer 环形队列最多保留 150 个带真实时间戳的样本。
|
||||
5. 采集失败不追加伪造样本,也不持久化历史。
|
||||
|
||||
资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 `resources` 实例
|
||||
及其活动状态。
|
||||
|
||||
## 10. 测试
|
||||
|
||||
### 10.1 Renderer 测试
|
||||
|
||||
- 固定入口顺序不受三个应用显隐组合影响。
|
||||
- 运行记录和设置没有显隐控件。
|
||||
- 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。
|
||||
- 文件复用不可关闭单实例;浏览器和终端按当前上下文聚焦匹配实例,工作栏“+”允许显式多实例。
|
||||
- 工具按钮展开工作栏但不改变当前主页面。
|
||||
- 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。
|
||||
- 窄窗口下工具按钮不重叠,焦点恢复正确。
|
||||
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
|
||||
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
|
||||
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
|
||||
|
||||
### 10.2 Main 与契约测试
|
||||
|
||||
- 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。
|
||||
- 旧版本设置迁移后默认显示所有既有应用。
|
||||
- 更新导航偏好不修改魔法笔记启停等无关字段。
|
||||
- 原子写入失败时保留最后一次有效设置。
|
||||
- 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。
|
||||
- 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。
|
||||
- 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。
|
||||
- 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。
|
||||
- `resetIntervalBaseline: true` 时把 CPU 与磁盘 IO 标记为 `warming-up`。
|
||||
- GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。
|
||||
- GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在 `0` 至 `100`。
|
||||
- GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。
|
||||
|
||||
### 10.3 回归范围
|
||||
|
||||
- 工作栏布局恢复和已关闭实例重建。
|
||||
- 本地与托管 SSH 终端目标。
|
||||
- 浏览器无会话、创建中和 Agent 操作中状态。
|
||||
- 设置页未保存离开确认。
|
||||
- 魔法笔记待办数和智能心跳建议数徽标。
|
||||
- 浅色、深色、`220px` 与 `420px` 侧栏宽度。
|
||||
- Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。
|
||||
|
||||
## 11. 实施顺序
|
||||
|
||||
1. 增加应用导航设置 schema、迁移和设置界面。
|
||||
2. 按即时保存结果组合主导航,并补充失败恢复。
|
||||
3. 增加资源快照契约、Main 采集服务和 Preload API。
|
||||
4. 为工作栏增加 `resources` 单实例和 `ResourceMonitorPanel`。
|
||||
5. 拆分侧栏底部设置按钮,加入四个系统工具按钮。
|
||||
6. 为工作栏增加带上下文的 `openOrActivate` 意图接口,以及浏览器和终端匹配规则。
|
||||
7. 接入选中状态、焦点恢复、无障碍文案和响应式样式。
|
||||
8. 完成 Renderer、契约、迁移和跨平台回归测试。
|
||||
|
||||
插件注册与插件市场不与首期改动一并实施。
|
||||
@@ -0,0 +1,204 @@
|
||||
# 应用导航与系统工具入口 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义入口、设置和轻量资源监控界面。文件、终端和浏览器的内容布局继续遵守助手工作栏
|
||||
文档。
|
||||
|
||||
## 1. 侧栏信息架构
|
||||
|
||||
```text
|
||||
品牌
|
||||
项目选择
|
||||
新对话
|
||||
搜索
|
||||
|
||||
当前对话
|
||||
魔法笔记 3
|
||||
知识库
|
||||
智能心跳 1
|
||||
运行记录
|
||||
|
||||
最近对话
|
||||
...
|
||||
|
||||
────────────────────────
|
||||
[文件] [终端] [浏览器] [资源] [设置]
|
||||
```
|
||||
|
||||
“当前对话”和“运行记录”属于固定系统入口。三个内置应用位于二者之间。侧栏底部不再显示
|
||||
“本地工作区”和 `platform · arch`;排障信息移至关于或诊断页面。
|
||||
|
||||
## 2. 底部系统工具栏
|
||||
|
||||
底部使用连续平整表面和顶部分隔线,不做成账户卡片或嵌套卡片。按钮顺序固定:
|
||||
|
||||
| 位置 | 图标 | 可见工具提示 | 点击结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `FolderOpen` | 文件 | 聚焦不可关闭的工作区面板 |
|
||||
| 2 | `SquareTerminal` | 终端 | 打开或聚焦当前执行空间终端 |
|
||||
| 3 | `Globe` | 浏览器 | 聚焦当前 Conversation 的匹配浏览器实例 |
|
||||
| 4 | `Activity` | 资源监控 | 打开或聚焦本机资源监控面板 |
|
||||
| 末位 | `Settings` | 设置 | 打开设置页面 |
|
||||
|
||||
图标使用项目现有 Lucide 库。每个按钮采用统一的 `34px × 34px` 点击区域,内部图标建议
|
||||
`16px`。工具栏使用 `8px` 内边距和 `4px` 间隔;设置按钮通过自动空白靠右,不添加第二条
|
||||
视觉分隔线。
|
||||
|
||||
### 2.1 状态
|
||||
|
||||
- 默认:透明背景,图标使用次级文字色。
|
||||
- 悬停:使用弱表面色,不能强于当前选中状态。
|
||||
- 选中:使用强调选中背景,并提高图标对比度。
|
||||
- 键盘焦点:显示统一焦点环,不能被侧栏边缘裁切。
|
||||
- 处理中:创建或激活请求未完成时暂时禁用对应按钮,保留尺寸并提供进行中名称。
|
||||
- 徽标:只用于等待处理、错误或活动状态,不显示无意义的常驻绿色圆点。
|
||||
|
||||
选中状态表达“对应工作栏面板正在显示”,不表达功能已安装或权限已授予。
|
||||
|
||||
## 3. 主导航应用区
|
||||
|
||||
应用入口沿用现有 `nav-item` 行,不使用应用卡片。图标、名称和徽标保持单行:
|
||||
|
||||
```text
|
||||
[图标] 魔法笔记 3
|
||||
[图标] 知识库
|
||||
[图标] 智能心跳 1
|
||||
```
|
||||
|
||||
隐藏某个应用后,其余应用向上补位。运行记录始终位于最后一个可见应用之后,不因应用显隐
|
||||
改变样式或归类为工具。
|
||||
|
||||
设置页打开期间,主侧栏显示最后一次成功保存的导航状态。Switch 更新成功后导航立即变化,
|
||||
失败时保持原导航。
|
||||
|
||||
## 4. 应用与工具设置
|
||||
|
||||
设置中心新增“应用与工具”分类,采用 `standard` PageShell:
|
||||
|
||||
```text
|
||||
应用与工具
|
||||
管理主导航中的应用。系统工具由 GoodBuddy 提供并固定在侧栏底部。
|
||||
|
||||
应用
|
||||
魔法笔记 笔记、待办和评论 [显示在侧边栏 开]
|
||||
知识库 管理和检索知识 [显示在侧边栏 开]
|
||||
智能心跳 查看计划、建议和运行历史 [显示在侧边栏 开]
|
||||
|
||||
系统工具
|
||||
文件 打开当前工作区 内置
|
||||
终端 进入当前执行空间 内置
|
||||
浏览器 打开 GoodBuddy 受控浏览器 内置
|
||||
资源监控 查看本机 CPU、内存和磁盘 IO 内置
|
||||
|
||||
系统入口
|
||||
当前对话、运行记录和设置位置固定,不参与上述配置。
|
||||
```
|
||||
|
||||
应用显隐使用共享 Switch,标签必须是“显示在侧边栏”,不能缩写为“启用”。切换时立即保存,
|
||||
请求期间禁用三个应用显隐 Switch;失败后恢复原值,并在对应设置行显示可重试错误。系统工具
|
||||
使用紧凑只读列表,不渲染禁用的卸载、停用或固定按钮。
|
||||
|
||||
功能已停用时,对应应用行显示“功能已停用”和设置入口,保留显隐偏好但暂时禁用 Switch。
|
||||
|
||||
若某应用自身支持业务启停,应在该应用原有设置中保留“启用功能”开关,并用说明区分:
|
||||
|
||||
- “显示在侧边栏”只控制入口。
|
||||
- “启用功能”控制该功能是否运行。
|
||||
|
||||
## 5. 助手工作栏反馈
|
||||
|
||||
点击系统工具后,右侧工作栏按现有布局回流打开。主内容页面保持不变,侧栏不渲染工具内容。
|
||||
|
||||
- 文件:激活默认存在且不可关闭的“工作区”Tab,焦点进入目录区域或错误修复入口。
|
||||
- 终端:激活匹配终端,焦点进入终端输入区;新建期间保留按钮宽度和进度状态。
|
||||
- 浏览器:激活绑定当前 Conversation 的最近使用“浏览器”Tab;没有匹配实例时创建一个,
|
||||
没有 Conversation 时按产品规则复用最近实例或进入待绑定空状态。
|
||||
- 资源监控:激活“资源监控”Tab,焦点进入指标摘要;首个样本加载后开始绘制历史。
|
||||
|
||||
后台事件可以更新徽标,不能自动展开工作栏或切换当前 Tab。由用户点击快捷按钮触发的打开
|
||||
属于显式操作,可以展开工作栏并切换 Tab。
|
||||
|
||||
### 5.1 资源监控面板
|
||||
|
||||
资源监控使用紧凑单列,不使用指标卡片墙:
|
||||
|
||||
```text
|
||||
资源监控 本机
|
||||
|
||||
CPU 18%
|
||||
[最近 5 分钟折线图]
|
||||
|
||||
内存 9.6 / 32 GB
|
||||
[使用量进度条]
|
||||
|
||||
磁盘 IO 读 12 MB/s · 写 3 MB/s
|
||||
|
||||
GoodBuddy
|
||||
CPU 4% · 内存 620 MB · 桌面进程 7
|
||||
|
||||
更新于 14:32:08
|
||||
```
|
||||
|
||||
- CPU 折线纵轴固定为 `0%` 至 `100%`,不能根据当前低负载动态放大波动。
|
||||
- 内存同时显示使用量、总量和百分比;进度条不代替文字数值。
|
||||
- 磁盘 IO 使用每秒读写字节并按统一单位格式化,不显示累计设备字节数。
|
||||
- GoodBuddy 摘要只显示聚合值,不列 PID、命令行或环境变量。
|
||||
- 首次加载或停止后恢复时,CPU 和磁盘 IO 显示“正在建立采样基线”,不先渲染 `0%`;内存
|
||||
等瞬时指标可以立即显示。
|
||||
- 部分可用时只在对应指标行显示“不可用”和简短原因;全部失败时显示重试入口。
|
||||
- 图表不使用动效平移;`prefers-reduced-motion` 下行为不变。
|
||||
- 面板失去活动状态、工作栏收起或窗口隐藏、最小化时停止更新;返回后重新建立区间指标基线,
|
||||
不填补离开期间的历史。
|
||||
|
||||
## 6. 文案
|
||||
|
||||
固定使用以下名称:
|
||||
|
||||
| 对象 | 文案 |
|
||||
| --- | --- |
|
||||
| 设置分类 | 应用与工具 |
|
||||
| 文件系统工具 | 文件 |
|
||||
| 终端系统工具 | 终端 |
|
||||
| 浏览器系统工具 | 浏览器 |
|
||||
| 浏览器说明 | GoodBuddy 受控浏览器 |
|
||||
| 资源系统工具 | 资源监控 |
|
||||
| 资源范围 | 本机 |
|
||||
| 指标缺失 | 不可用 |
|
||||
| 应用显隐开关 | 显示在侧边栏 |
|
||||
| 内置来源 | 内置 |
|
||||
|
||||
不再使用“本地工作区”作为侧栏底部身份名称。项目和默认工作区的路径继续由项目选择器、页面
|
||||
范围或工作区面板显示。
|
||||
|
||||
## 7. 键盘和无障碍
|
||||
|
||||
- 底部按钮均为独立 `button`,不能嵌套在整行设置按钮中。
|
||||
- Tab 顺序为文件、终端、浏览器、资源监控、设置。
|
||||
- 图标按钮通过 `aria-label` 提供名称,工具提示不能作为唯一可访问名称。
|
||||
- 工作栏打开后按现有焦点策略聚焦目标面板;用户通过固定工作栏开关关闭时,焦点返回该开关。
|
||||
- 选中按钮使用 `aria-pressed` 或等价的可访问状态,不能只依靠背景色。
|
||||
- 徽标的数量或状态合并进按钮可访问名称,例如“终端,1 个等待处理”。
|
||||
- 应用显隐 Switch 支持 Space 切换,保存失败后恢复原值并保留焦点。
|
||||
|
||||
## 8. 响应式
|
||||
|
||||
- 侧栏宽度 `220px` 至 `420px` 时,底部按钮尺寸不随宽度缩放。
|
||||
- 窄窗口继续使用现有侧栏抽屉;工具栏不转为多行。
|
||||
- 空间不足时缩小设置按钮前的弹性空白,不隐藏四个系统工具和设置。
|
||||
- 工具提示不得超出窗口;靠近底边时显示在按钮上方。
|
||||
- 粗指针设备不依赖悬停,按钮的可访问名称和选中反馈保持有效。
|
||||
|
||||
## 9. 未来插件入口
|
||||
|
||||
插件市场交付后另行定义插件区域、固定数量、溢出和权限界面。系统入口与四个系统工具的位置
|
||||
保持保留,不向插件开放。
|
||||
@@ -0,0 +1,236 @@
|
||||
# 应用导航与系统工具入口 User Stories
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
## 1. 角色
|
||||
|
||||
### 1.1 普通用户
|
||||
|
||||
使用对话、笔记和知识功能,希望常用入口稳定,不需要理解工作栏实例模型。
|
||||
|
||||
### 1.2 本地工作用户
|
||||
|
||||
经常查看项目文件、打开终端、使用受控浏览器或检查电脑负载,希望从任意一级页面快速进入
|
||||
这些工具。
|
||||
|
||||
## 2. Epic A:固定系统入口
|
||||
|
||||
### US-A1 始终访问运行记录
|
||||
|
||||
作为用户,我希望运行记录保持在固定位置,以便应用入口变化后仍能检查 Agent 和工具活动。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 用户隐藏任意内置应用
|
||||
- When 用户查看主导航
|
||||
- Then “运行记录”仍显示在应用区之后
|
||||
- And 设置中没有隐藏、卸载或固定运行记录的选项
|
||||
|
||||
### US-A2 从固定入口进入设置
|
||||
|
||||
作为用户,我希望设置入口始终位于侧栏底部,以便快捷工具状态变化时仍能进入配置。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 任意主页面和任意侧栏宽度
|
||||
- When 用户查看侧栏底部
|
||||
- Then 设置按钮保持可见且点击区域不与工具按钮重叠
|
||||
- And 点击后继续遵守未保存内容的离开确认
|
||||
|
||||
## 3. Epic B:管理应用入口
|
||||
|
||||
### US-B1 识别完整应用
|
||||
|
||||
作为用户,我希望魔法笔记、知识库和智能心跳显示在主导航,以便知道它们会打开完整页面。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 对应功能已启用且应用设置为显示
|
||||
- When 用户打开主侧栏
|
||||
- Then 应用位于“当前对话”和“运行记录”之间
|
||||
- And 点击应用在主内容区打开对应一级页面
|
||||
- And 原有待办或建议徽标继续显示
|
||||
|
||||
### US-B2 隐藏低频应用
|
||||
|
||||
作为用户,我希望隐藏不常用的内置应用,以便缩短主导航,但保留已有数据。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 应用已有内容
|
||||
- When 用户关闭该应用的“显示在侧边栏”Switch
|
||||
- Then 主导航不再显示该应用
|
||||
- And 应用数据、计划和业务启停状态不变
|
||||
- And 用户可从同一设置位置恢复显示
|
||||
|
||||
### US-B4 保留功能启停语义
|
||||
|
||||
作为魔法笔记用户,我希望导航显隐与功能启停相互独立,以免整理导航时关闭功能。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 魔法笔记功能已启用
|
||||
- When 用户只隐藏其侧栏入口
|
||||
- Then 笔记及待办处理能力继续按原设置工作
|
||||
- And 再次显示时恢复原页面和数据
|
||||
|
||||
### US-B5 恢复显隐保存失败
|
||||
|
||||
作为用户,我希望入口设置保存失败时继续使用原导航,以免界面状态与实际设置不一致。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 应用显隐更新尚未完成
|
||||
- When 用户切换“显示在侧边栏”Switch
|
||||
- Then 三个应用显隐 Switch 在请求期间暂时不可操作
|
||||
- And 更新成功后主导航立即变化
|
||||
- And 更新失败时 Switch 和主导航恢复原状态并显示可重试错误
|
||||
|
||||
## 4. Epic C:使用系统工具
|
||||
|
||||
### US-C1 一键查看文件
|
||||
|
||||
作为本地工作用户,我希望点击左下角文件按钮后直接查看当前工作区文件。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 当前项目有有效根目录
|
||||
- When 用户点击文件按钮
|
||||
- Then 助手工作栏展开
|
||||
- And 聚焦默认存在且不可关闭的工作区单实例
|
||||
- And 文件按钮通过选中状态表明该面板正在显示
|
||||
|
||||
### US-C2 处理无效工作区
|
||||
|
||||
作为用户,我希望目录不可用时看到修复方法,并收到明确反馈。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 工作区面板处于跟随模式
|
||||
- And 当前项目和默认工作区均不可用
|
||||
- When 用户点击文件按钮
|
||||
- Then 工作区面板仍打开
|
||||
- And 面板说明目录不可用并提供选择或修复目录的入口
|
||||
- And 应用不静默改用其他目录
|
||||
|
||||
### US-C3 快速进入终端
|
||||
|
||||
作为本地工作用户,我希望点击终端按钮后进入当前执行空间的终端。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 当前执行空间已有正在启动或运行中的终端
|
||||
- When 用户点击终端按钮
|
||||
- Then 工作栏聚焦最近使用的匹配终端,不创建重复实例
|
||||
- Given 当前执行空间没有终端
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建一个绑定该执行空间的新终端
|
||||
- Given 当前没有项目
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建或聚焦使用本机 Home 的终端
|
||||
- Given 当前执行空间只有已退出、已中断或失败的终端
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建新终端并保留原终端的历史状态
|
||||
|
||||
### US-C4 使用远程项目终端
|
||||
|
||||
作为远程项目用户,我希望同一个终端按钮进入远程项目目录,以免误在本机执行命令。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 当前项目绑定托管 SSH 执行空间
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建或聚焦绑定该远程项目及其权威目录的终端
|
||||
- And 面板持续显示远程主机和工作目录
|
||||
- And 连接失败时保留重连与诊断信息
|
||||
|
||||
### US-C5 打开受控浏览器
|
||||
|
||||
作为用户,我希望浏览器按钮打开 GoodBuddy 受控浏览器,以便查看与 Agent 相同的会话。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 浏览器面板跟随当前上下文且当前会话已有浏览器会话
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 工作栏聚焦绑定当前 Conversation 的最近使用浏览器实例
|
||||
- And 不打开系统默认浏览器或无意创建重复实例
|
||||
- Given 现有浏览器实例都绑定到其他 Conversation
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 工作栏创建并聚焦绑定当前 Conversation 的新浏览器实例
|
||||
- Given 当前没有活动 Conversation
|
||||
- And 已存在浏览器实例
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 工作栏聚焦最近使用的浏览器实例并保持其目标
|
||||
- Given 当前没有活动 Conversation
|
||||
- And 不存在浏览器实例
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 创建待选择目标的空实例
|
||||
- Given 用户需要同一 Conversation 的另一个独立页面
|
||||
- When 用户从工作栏“+”选择浏览器
|
||||
- Then 每次创建拥有独立页面和导航状态的新实例
|
||||
- And 同一 Conversation 内共享隔离登录上下文
|
||||
|
||||
### US-C6 查看本机资源
|
||||
|
||||
作为本地工作用户,我希望快速查看 CPU、内存和磁盘 IO,以便判断任务变慢是否与本机负载
|
||||
有关。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 资源监控面板尚未打开
|
||||
- When 用户点击左下角资源监控按钮
|
||||
- Then 助手工作栏创建并激活单实例资源监控面板
|
||||
- And 页面显示整机 CPU、内存、磁盘读写及 GoodBuddy 桌面进程摘要
|
||||
- And 页面每 2 秒更新一次,并用图表保留最近 5 分钟
|
||||
- When 用户切换到其他工作栏 Tab、收起工作栏、隐藏窗口或最小化窗口
|
||||
- Then 资源监控停止请求新样本
|
||||
- When 用户再次显示窗口并返回资源监控
|
||||
- Then 首次采样建立区间指标基线,下一次采样开始显示 CPU 和磁盘 IO 速率
|
||||
- Given 某项指标在当前平台不可用
|
||||
- When 页面收到部分结果
|
||||
- Then 该指标显示“不可用”及原因,其他指标继续更新
|
||||
|
||||
### US-C7 从任意应用打开工具
|
||||
|
||||
作为用户,我希望在魔法笔记、知识库、智能心跳和运行记录页面也能打开系统工具。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 用户位于任意一级页面
|
||||
- When 用户点击文件、终端、浏览器或资源监控
|
||||
- Then 主页面保持不变
|
||||
- And 右侧工作栏打开对应面板
|
||||
- And 工具不会覆盖主内容或伪装成一级页面
|
||||
|
||||
## 5. Epic D:可访问性和布局
|
||||
|
||||
### US-D1 使用键盘打开系统工具
|
||||
|
||||
作为键盘用户,我希望按顺序聚焦并打开左下角工具,以便不依赖鼠标。
|
||||
|
||||
验收:
|
||||
|
||||
- 每个系统工具和设置都是独立按钮
|
||||
- Tab 顺序与视觉顺序一致
|
||||
- Enter 和 Space 均可激活按钮
|
||||
- 每个纯图标按钮都有可访问名称和工具提示
|
||||
- 焦点样式不被侧栏边界裁切
|
||||
|
||||
### US-D2 在窄窗口使用快捷栏
|
||||
|
||||
作为小屏用户,我希望侧栏抽屉中的工具按钮保持可操作。
|
||||
|
||||
验收:
|
||||
|
||||
- 工具按钮保持单行且不重叠
|
||||
- 不因宽度不足隐藏文件、终端、浏览器、资源监控或设置
|
||||
- 触控点击区域符合统一界面规范
|
||||
- 关闭侧栏抽屉后焦点返回原触发按钮
|
||||
@@ -0,0 +1,16 @@
|
||||
# 助手工作栏
|
||||
|
||||
助手工作栏统一承载本机与远程执行空间、Task、成果、文件、浏览器和终端等持续工作界面。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [通用助手工作栏与执行空间 PRD](./prd.md) | 工作栏整体产品范围、执行空间和各工作面板归属 |
|
||||
| [多终端页签 PRD](./terminal-tabs-prd.md) | 工作栏应用的固定、可关闭、单实例和多实例策略,以及多终端生命周期 |
|
||||
| [浏览器多实例技术设计](./browser-tabs-technical-design.md) | 浏览器 Tab 身份、Conversation 绑定、IPC、BrowserService 和 MCP 路由 |
|
||||
| [实现与验证进度](./progress.md) | 工作区与 Runtime 相关改动的实现事实、验证证据与剩余阻塞 |
|
||||
| [Runtime 交互边界](./runtime-interactions.md) | 原生权限、结构化问答、后台拒绝、终端交互、托管 Runtime 并行与输出及消息底部请求状态的适配范围 |
|
||||
|
||||
当前尚未拆出独立 User Stories、功能逻辑和 UI 设计文档;相关内容仍保留在两份现有 PRD
|
||||
中。浏览器多实例涉及跨进程身份和工具路由,单独使用技术设计文档维护。
|
||||
@@ -0,0 +1,198 @@
|
||||
# 浏览器多实例技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 基础多实例已实施,Agent 多 Tab 工具待需求 |
|
||||
| 版本 | 1.0 |
|
||||
| 日期 | 2026-09-10 |
|
||||
| 关联 PRD | [通用助手工作栏与执行空间 PRD](./prd.md) |
|
||||
| 页签策略 | [助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) |
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
浏览器改为可关闭的多实例工作栏应用。每个工作栏浏览器实例对应一个独立 Browser Tab,拥有
|
||||
独立页面、导航状态、操作队列、画面状态和元素引用空间。同一 Conversation 下的多个 Tab
|
||||
共享隔离存储与代理上下文,使登录状态符合常规浏览器预期。
|
||||
|
||||
本设计不把多个工作栏 Tab 实现为同一页面的重复入口,不允许用户切换 Tab 后改变正在执行的
|
||||
Agent 工具目标,也不在首期要求模型主动管理多个 Tab。
|
||||
|
||||
## 2. 改造前限制
|
||||
|
||||
改造前的链路以 `conversationId` 作为唯一浏览器身份:
|
||||
|
||||
- Renderer 只保存每个 Conversation 的一份 `BrowserLiveState`。
|
||||
- Browser IPC、Preload API 和 `BrowserService` 操作只携带 `conversationId`。
|
||||
- Main 每个 Conversation 只创建一个 Session、一个 `WebContentsView` 和一个 CDP Driver。
|
||||
- 请求级 MCP 能力只保存 `browserConversationId`。
|
||||
- snapshot 元素 `ref` 只在一个 Driver 的引用空间内有效。
|
||||
|
||||
因此只把工作栏注册策略改成 `multiple` 会导致多个 Tab 控制同一页面,并产生 viewport、清理和
|
||||
元素引用竞态,不能作为可交付实现。
|
||||
|
||||
## 3. 身份与所有权
|
||||
|
||||
```ts
|
||||
type BrowserTabId = string
|
||||
|
||||
type ConversationBrowserContext = {
|
||||
conversationId: string
|
||||
partitionId: string
|
||||
tabs: Map<BrowserTabId, BrowserTab>
|
||||
primaryTabId?: BrowserTabId
|
||||
}
|
||||
|
||||
type BrowserTab = {
|
||||
id: BrowserTabId
|
||||
conversationId: string
|
||||
view: WebContentsView
|
||||
driver: CdpBrowserDriver
|
||||
state: BrowserLiveState
|
||||
}
|
||||
```
|
||||
|
||||
- Conversation Browser Context 拥有隔离 partition、代理配置和全部 Tab。
|
||||
- Browser Tab 拥有页面、Driver、串行操作队列、snapshot 代次、idle 状态和可见 viewport。
|
||||
- `BrowserTabId` 是 Main 签发的不透明 ID。Renderer 和 Runtime 不能自行选择或复用。
|
||||
- 工作栏实例保存公开的逻辑实例 ID 和 Conversation 绑定;进程重启后不恢复旧
|
||||
`WebContentsView`,首次使用时由 Main 为该逻辑实例创建新的 Browser Tab。
|
||||
- 删除 Conversation 时关闭该 Context 下全部 Tab;关闭一个工作栏浏览器实例只关闭对应
|
||||
Browser Tab,不得调用 Conversation 级 Runtime 释放路径。
|
||||
|
||||
## 4. Renderer 与 viewport
|
||||
|
||||
Renderer 状态改为:
|
||||
|
||||
```ts
|
||||
Record<ConversationId, Record<BrowserTabId, BrowserLiveState>>
|
||||
```
|
||||
|
||||
每个浏览器工作栏实例持有自己的 `browserTabId`。只有当前可见实例可以提交 viewport。每次
|
||||
获得可见权时生成新的 UUID `leaseToken`;提交请求携带 `browserTabId` 和 token,清理请求必须
|
||||
回传同一个 token。Main 只接受当前 token 的清理,避免已隐藏实例的延迟 cleanup 隐藏新实例,
|
||||
也避免 Renderer 重载后从零开始的数字序列无法覆盖旧租约。
|
||||
|
||||
非活动 Browser Tab 保持页面和状态,但隐藏 `WebContentsView` 并停止非必要画面采集。工作栏
|
||||
收起、切换主页面和窗口隐藏时采用相同规则,不销毁 Tab。
|
||||
普通导航、快照与页面操作只同步轻量导航状态,不自动截图、等待画面或传输 JPEG;只有显式
|
||||
`browser_screenshot` 调用生成图片。侧栏内终端关闭确认显示期间,同样隐藏原生浏览器视图,
|
||||
确认或取消后恢复当前实例,避免原生子视图遮挡 HTML 确认按钮。
|
||||
|
||||
浏览器应用声明 `visibleAcrossContextSwitches: true`,打开的 Tab 跨项目、会话切换继续显示。
|
||||
切换活动 Conversation 不改变当前选中的浏览器或其 `targetRef.conversationId`;用户在会话 B
|
||||
查看会话 A 的浏览器时,viewport 和手动导航仍指向 A 的原 Tab。新建实例绑定 B。仅未绑定的
|
||||
默认或恢复实例可在首次使用时取得当前会话绑定。
|
||||
|
||||
Tab 的跨会话可见性与 MCP 授权分开处理。B 的请求不能选择属于 A 的可见 Tab;A 已取得的
|
||||
capability 在用户切到 B 后仍固定路由至 A 的原 Tab,使用租约也继续阻止关闭该 Tab。
|
||||
内部点击实例只同步应用类型,不再次按当前 Conversation 改选实例。Main 状态携带对应的
|
||||
`workbarInstanceId`,Renderer 据此绑定请求创建的同一 Tab,而不是另建空白页。收到 `stopped`
|
||||
后清除该实例的旧 Tab 绑定,下一次使用通过原逻辑实例重新创建,保留地址草稿与错误反馈。
|
||||
请求创建的实例也遵守现有 32 个工作栏页签上限;满额时保留 Main 页面及其状态,并就地提示
|
||||
关闭一个可关闭页签。释放位置后再绑定该页面,不写入超限布局或丢弃 Agent 页面。
|
||||
|
||||
## 5. IPC 与服务接口
|
||||
|
||||
共享契约增加有界操作:
|
||||
|
||||
```ts
|
||||
browser.createTab({ conversationId, workbarInstanceId })
|
||||
browser.listTabs({ conversationId })
|
||||
browser.closeTab({ conversationId, tabId })
|
||||
browser.setViewport({ conversationId, tabId, leaseToken, bounds })
|
||||
browser.navigate({ conversationId, tabId, url })
|
||||
browser.back({ conversationId, tabId })
|
||||
browser.reload({ conversationId, tabId })
|
||||
browser.stopLoading({ conversationId, tabId })
|
||||
```
|
||||
|
||||
Main 必须验证 `tabId` 属于同一 `conversationId` 和当前窗口。BrowserService 将现有
|
||||
Conversation 级 slot 拆为 Context 与 Tab 两层;同一 Tab 内操作继续串行,不同 Tab 可以并行,
|
||||
但仍受窗口级和 Conversation 级有界数量限制。
|
||||
|
||||
现有 Conversation 级 `stop/release` 保留为销毁完整浏览器上下文的操作,不能用作关闭单个
|
||||
Browser Tab。
|
||||
|
||||
## 6. Agent 与 MCP 路由
|
||||
|
||||
### 6.1 兼容阶段
|
||||
|
||||
第一阶段不修改模型可见的现有浏览器工具参数。模型请求开始时,Main 按以下顺序选择并冻结
|
||||
`boundBrowserTabId`:
|
||||
|
||||
1. 当前可见且绑定该 Conversation 的 Browser Tab。
|
||||
2. 该 Conversation 的 `primaryTabId`。
|
||||
3. 预留 primary Browser Tab 身份,首次实际浏览器操作时才创建页面与 Conversation Context。
|
||||
|
||||
未使用浏览器的请求只持有随请求释放的内存预留,不创建 Chromium 资源、不触发浏览器 UI、
|
||||
也不占用活动浏览器会话名额。物化后使用同一 Tab 身份和工作栏实例归属;取消、能力撤销及
|
||||
服务关闭释放未使用的预留,不保存到磁盘。编程 Subagent 继承父请求的 Browser Tab 与浏览器
|
||||
Conversation 归属,不新建租约或把页面重新归属到子级对话。
|
||||
|
||||
直连模型上下文和请求级 MCP capability 同时保存 `conversationId` 与
|
||||
`boundBrowserTabId`。请求执行期间,用户切换、创建或关闭其他工作栏 Tab 不改变工具目标;
|
||||
模型请求使用某个 Tab 时禁用该 Tab 的工作栏关闭操作,并说明请求仍在执行。用户先取消请求
|
||||
后才能关闭;系统不得把进行中的操作改投其他 Tab。
|
||||
|
||||
该阶段 MCP transport、endpoint 和 token 生命周期不变,只扩展服务端 capability 内容和内部
|
||||
路由。旧工具调用继续使用被冻结的 Tab。
|
||||
|
||||
### 6.2 Agent 多 Tab 阶段
|
||||
|
||||
只有明确需要 Agent 管理多个页面时,才增加:
|
||||
|
||||
- `browser_tab_list`
|
||||
- `browser_tab_open`
|
||||
- `browser_tab_close`
|
||||
|
||||
导航、snapshot、back 和 screenshot 增加可选 `tabId`;省略时使用请求绑定的默认 Tab。
|
||||
`browser_snapshot` 返回 `tabId` 和 snapshot 代次。click、type 和 select 必须校验 capability、
|
||||
`tabId`、引用代次和元素 `ref` 属于同一 Driver,禁止跨 Tab 猜测或解析引用。
|
||||
|
||||
## 7. 工作栏创建与快捷入口
|
||||
|
||||
- 从“+”选择浏览器时始终创建新工作栏实例,并默认绑定当前 Conversation。
|
||||
- 没有活动 Conversation 时创建待绑定实例,不创建无归属 Main Browser Context。
|
||||
- 主侧栏浏览器快捷按钮优先激活绑定当前 Conversation 的最近使用实例;没有匹配实例时创建
|
||||
一个。没有活动 Conversation 时激活最近使用的浏览器实例;完全不存在时创建待绑定实例。
|
||||
- New browser instances use a numbered browser label followed by the active Conversation title, when
|
||||
available. The number avoids existing titles; restoring the layout preserves each saved title.
|
||||
The address bar shows only that instance's page URL. Binding a numbered instance does not overwrite
|
||||
its title; the default unbound browser receives the Conversation label when first bound.
|
||||
- 关闭工作栏 Browser Tab 只释放对应 Browser Tab。关闭浏览器上下文是独立危险操作,需要
|
||||
明确说明会影响该 Conversation 下的全部 Tab 和 Agent 浏览器状态。
|
||||
|
||||
## 8. 持久化与恢复
|
||||
|
||||
`createTab` reuses a live tab only for the same Conversation and workbar instance ID. A new workbar
|
||||
instance can adopt the primary tab only when its request creates the Conversation context. An existing
|
||||
primary, including one opened by an Agent, is not adopted by a new instance. New sibling tabs start at
|
||||
`about:blank` with no navigation history and share the Conversation's cookie partition.
|
||||
|
||||
持久化工作栏实例 ID、顺序、自定义标题和公开 Conversation 绑定,不持久化页面进程、CDP
|
||||
引用、snapshot、viewport lease、Cookie、Token 或 Main `BrowserTabId`。应用重启后实例显示
|
||||
未启动状态,首次导航或 Agent 使用时按原绑定创建新 Tab。
|
||||
|
||||
绑定的 Conversation 已删除或不可见时保留实例并显示目标失效,不自动绑定当前 Conversation。
|
||||
|
||||
## 9. 限制与验收
|
||||
|
||||
- 分别限制每个 Conversation 的 Tab 数和每个窗口的总活动 Tab 数;达到上限时在创建前反馈。
|
||||
- 两个 Tab 的地址、历史、加载状态、截图和元素引用互不覆盖。
|
||||
- 同一 Conversation 的 Tab 共享登录状态,不同 Conversation 保持隔离。
|
||||
- 延迟 viewport cleanup 不能隐藏另一个已激活 Tab。
|
||||
- 一个 Tab 的元素 `ref` 不能在另一个 Tab 中执行。
|
||||
- 用户切换工作栏 Tab 不改变进行中模型请求的 MCP 浏览器目标。
|
||||
- 关闭一个 Tab 不释放 Conversation Runtime,也不关闭同一 Conversation 的其他 Tab。
|
||||
- 旧版无 `tabId` 的模型工具调用仍操作请求绑定的默认 Tab。
|
||||
|
||||
## 10. 实施顺序
|
||||
|
||||
1. 引入 Browser Context 与 Browser Tab 两层身份,拆分 partition 和页面生命周期。
|
||||
2. 扩展 Shared、Preload、IPC 与 BrowserService 的 `tabId` 和 viewport `leaseToken`。
|
||||
3. 改造 Renderer 状态与工作栏实例,开放浏览器 `multiple` 策略和单 Tab 关闭。
|
||||
4. 将直连模型与 MCP capability 绑定到请求开始时确定的 Browser Tab。
|
||||
5. 完成跨 Tab 引用隔离、并行队列、共享 Cookie、关闭和恢复测试。
|
||||
6. 有明确 Agent 多页面需求后再开放模型可见的 Tab 管理工具。
|
||||
+238
-56
@@ -5,11 +5,11 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.5 |
|
||||
| 日期 | 2026-08-28 |
|
||||
| 版本 | 0.6 |
|
||||
| 日期 | 2026-08-31 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[SSH 远程主机与 GoodBuddy Agent 稳定终态设计](../../architecture/remote-host-and-goodbuddy-agent-design.md) |
|
||||
| 相关能力 | [会话监督](../supervision/conversation-supervision-prd.md)、[自动化平台](../../architecture/automation-platform-architecture.md)、[长期助手路线图](../../roadmap/long-term-assistant-roadmap.md) |
|
||||
| 相关设计 | [统一界面设计系统](../../../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. 背景
|
||||
|
||||
@@ -36,8 +36,8 @@ Runtime 统一抽象为“执行空间”。
|
||||
### 2.1 助手工作栏
|
||||
|
||||
助手工作栏是 GoodBuddy 中始终可访问的应用级工具容器。它提供稳定能力目录,用户从中
|
||||
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器和成果
|
||||
面板实例。稳定的是能力的可发现性,不是八个同时占据界面的固定面板。
|
||||
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器、资源监控、
|
||||
成果和文档面板实例。稳定的是能力的可发现性,不是十个同时占据界面的固定面板。
|
||||
|
||||
工作栏不是:
|
||||
|
||||
@@ -83,30 +83,36 @@ Runtime
|
||||
进程
|
||||
工作区
|
||||
浏览器
|
||||
资源监控
|
||||
成果
|
||||
文档
|
||||
```
|
||||
|
||||
- 应用不得根据当前项目、会话、Runtime、主机或探测结果无提示地增删能力目录项。
|
||||
- 用户主动打开、关闭、排序和停靠面板实例;应用不默认同时挂载全部能力。
|
||||
- Task Center 是 Task 的单例应用级索引。每个 Task 只关联一条 Conversation,一条
|
||||
Conversation 可以关联多个 Task;Task Center 不复制会话内容,也不显示普通 Conversation、
|
||||
Job、Run 或心跳事项。
|
||||
- 用户主动打开、关闭、排序和停靠可关闭的面板实例;任务中心和工作区作为基础工作入口默认
|
||||
存在且不可关闭,分别固定在第一、第二位,排序属性为不可重排。
|
||||
- Task Center 是默认跟随当前项目的单实例 Task 索引。每个 Task 只关联一条 Conversation,
|
||||
一条 Conversation 可以关联多个 Task;Task Center 不复制会话内容,也不显示普通
|
||||
Conversation、Job、Run 或心跳事项。
|
||||
- 当前能力、连接、数据和空状态可以动态变化。
|
||||
- 能力不可用时,目录项或已打开面板显示原因、影响和可执行的配置或切换入口,不能只通过隐藏表示。
|
||||
- 用户可以在设置中调整目录顺序;恢复默认布局恢复标准目录与默认打开面板,不强制打开全部能力。
|
||||
- 同一能力需要并排比较不同目标时可以创建多个实例,每个实例拥有独立身份和范围绑定。
|
||||
- 浏览器、终端及其他声明为多实例的能力可以创建多个实例,每个实例拥有独立身份和范围绑定。
|
||||
|
||||
### 3.2 当前上下文只提供默认值
|
||||
|
||||
任务中心作为全局索引不跟随当前会话,也不支持为同一列表打开多个目标实例。其他可绑定
|
||||
任务中心只有一个工作栏实例,默认跟随当前项目,不跟随当前 Conversation。用户可以在同一
|
||||
实例内显式切换到全局任务或全部项目,但不能为不同项目打开多个任务中心 Tab。其他可绑定
|
||||
目标的能力使用当前会话、项目、Runtime 和主机帮助新面板实例初始定位,但这些上下文不是
|
||||
使用门槛:
|
||||
|
||||
- 监督默认选择当前会话,用户可以改选其他 Conversation、Task 或实验对象。
|
||||
- Runtime 默认跟随当前会话或 Task;执行事件可以查看,但 Job/Run 不作为独立选择对象。
|
||||
- 终端默认使用当前项目执行空间,用户可以新建本机或远程终端。
|
||||
- 终端创建时固定到当前项目执行空间,由项目配置自动决定本机或托管 SSH;没有当前项目时
|
||||
使用本机 Home。
|
||||
- 工作区默认显示当前项目目录,用户可以打开其他本机目录或远程目录。
|
||||
- 成果默认使用当前范围,用户可以切换到项目、全局或其他允许范围。
|
||||
- 文档创建时固定到用户明确打开的文件和 ShareServer 编辑会话,不提供跟随模式。
|
||||
- 下一次请求的附件和知识库由当前 Composer 展示与管理,不作为工作栏能力。历史执行上下文
|
||||
随关联 Task 查看;记忆功能实现后也归入任务中心,而不是新增独立上下文面板。
|
||||
|
||||
@@ -132,10 +138,10 @@ Runtime
|
||||
稳定能力目录和已打开面板必须准确呈现能力差异:
|
||||
|
||||
- 当前 Runtime 不支持后台 Job 时,Runtime 能力仍可发现;打开后说明当前可监督的内容。
|
||||
- 当前执行空间没有 Git 仓库时,工作区文件功能仍可使用,Git 区域显示不可用原因。
|
||||
- 当前执行空间没有 Git 仓库时,工作区文件功能仍可使用,仅确认是 Git 仓库后显示 Git 工作区入口。
|
||||
- 没有活动进程时,进程面板提供创建终端或启动 Runtime 的入口。
|
||||
- 没有项目时,终端和工作区允许用户选择本机目录或远程主机,但必须先取得有界临时
|
||||
Execution Space Grant,不能把可选路径直接传给文件、Git、Process 或 Runtime。
|
||||
- 没有项目时,终端使用当前用户本机 Home;工作区等允许选择路径的能力仍必须先取得有界
|
||||
临时 Execution Space Grant,不能把可选路径直接传给文件、Git、Process 或 Runtime。
|
||||
- 监督未启用时,监督面板提供目标、模式和“开始监督”,而不是隐藏能力。
|
||||
|
||||
不得渲染成排没有解释的禁用按钮,也不得把“进程连通”描述为已经支持完整原生监督。
|
||||
@@ -167,7 +173,7 @@ Runtime
|
||||
- 建立统一范围、执行空间、生命周期、成果和控制契约。
|
||||
- 复用现有 Project、Conversation、Task、Artifact、Activity 和 Approval 数据。
|
||||
- 保持 Renderer 无任意文件、进程、PTY、SSH 或 Electron API 能力。
|
||||
- 保持 Ask 只读、Execute 审批、取消、超时、输出边界和活动审计。
|
||||
- 保持 Ask 只读、Execute 账号完整授权、取消、超时、输出边界和活动审计。
|
||||
- 为本机与远程能力提供一致 UI,同时准确表达能力差异。
|
||||
|
||||
## 5. 非目标
|
||||
@@ -201,22 +207,32 @@ Runtime
|
||||
|
||||
### 6.2 能力目录与面板实例
|
||||
|
||||
能力目录不等于同时打开八个面板。推荐使用工作栏内的纵向能力导航,并在旁边或停靠区域管理
|
||||
用户已经打开的面板实例:
|
||||
能力目录不等于同时打开十个面板。当前产品采用“已打开实例 Tab + 持久新增按钮”的工作栏
|
||||
模型:Tab 行只显示已打开实例,行末“+”始终可见;点击后在工作栏内容区打开稳定能力目录,
|
||||
用户再创建或激活应用实例。详细交互以
|
||||
[助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 为准。
|
||||
|
||||
- 每项始终显示稳定图标,并提供可见标签或可持续查看的工具提示。
|
||||
- 目录使用与其交互模型匹配的列表、工具栏或菜单语义;单实例切换使用 `tablist`、`tab`、
|
||||
`tabpanel`,多实例停靠区使用有名称的区域和明确面板标题。
|
||||
- 支持方向键、Home、End、Enter、Space、关闭面板和正确焦点恢复。
|
||||
- 已打开实例使用 `tablist`、`tab`、`tabpanel`;“+”是位于 `tablist` 外的普通命令按钮。
|
||||
- 应用注册表分别声明实例策略、默认打开、必须存在、是否可关闭和是否允许排序。`single` 或
|
||||
`multiple` 只控制实例数量;`required` 控制布局修复,不能兼作关闭或排序策略。
|
||||
- 在工作栏“+”目录中重复选择单实例应用时聚焦已有实例;选择浏览器、终端等多实例应用时
|
||||
每次创建新实例。主侧栏系统工具快捷入口按当前上下文聚焦匹配实例,不能因连续点击无意
|
||||
创建重复实例。
|
||||
- 支持方向键、Home、End、Enter、Space、可关闭面板的关闭命令和正确焦点恢复。不可关闭
|
||||
实例不渲染关闭按钮,也不响应关闭快捷命令。
|
||||
- 徽标显示未解决数量、等待审批或失败状态,并同时提供文字或可访问名称。
|
||||
- 用户调整目录顺序、打开实例、停靠位置和尺寸后持久化;关闭实例后能力仍可从目录重新打开。
|
||||
- 用户调整目录顺序、打开实例、停靠位置和尺寸后持久化;关闭可关闭实例后能力仍可从目录
|
||||
重新打开。布局加载和恢复默认布局必须补回缺失的任务中心与工作区实例。
|
||||
- 默认布局只恢复经过产品确认的少量常用面板,不自动打开全部标准能力。
|
||||
- 每个实例显示稳定实例 ID、能力名称、跟随或固定状态与当前目标;同能力多实例不能只靠位置区分。
|
||||
|
||||
当前聊天右栏过渡实现保留 Task Center、工作区、浏览器和成果四个横向页签时,必须
|
||||
单行横向滚动,不能自动隐藏或缩写到不可辨认。Task Center 继续作为 Task 的现有入口;
|
||||
审批定位到所属 Task 或 Runtime。智能心跳不作为工作栏页签,其报告、建议、历史和完整配置统一归属
|
||||
“智能心跳”菜单入口。当前阶段不新增独立自动化中心。
|
||||
从当前聊天右栏迁移时,Task Center 和工作区作为默认打开、单实例且不可关闭的基础应用保留;
|
||||
浏览器保留一个默认打开实例,但注册为可关闭的多实例应用;成果保持默认打开、单实例且可
|
||||
关闭;终端不默认打开,由用户通过“+”创建,并从首期开始支持多实例。Tab 必须单行横向滚动,
|
||||
不能自动隐藏或缩写到不可辨认,“+”不能随滚动消失。Task Center 继续作为 Task 的现有
|
||||
入口;审批定位到所属 Task 或 Runtime。智能心跳不作为工作栏页签,其报告、建议、历史和
|
||||
完整配置统一归属“智能心跳”菜单入口。当前阶段不新增独立自动化中心。
|
||||
|
||||
### 6.3 工作栏尺寸
|
||||
|
||||
@@ -251,8 +267,13 @@ Task Center 保留为工作栏中的稳定入口,并在现有基础上适度
|
||||
活动记录和成果查看器中,不在窄栏复制,也不呈现 Job/Run 树。
|
||||
- 智能心跳的报告、建议、历史和配置不进入任务中心。
|
||||
|
||||
任务中心是单例索引,不使用其他能力的“跟随 / 固定目标”多实例模型。后台状态可以更新
|
||||
徽标和排序,但不能自动打开面板或抢占用户当前工作。
|
||||
任务中心是应用级单例,默认存在且不可关闭。它的数据范围默认跟随当前项目,并在标题或范围
|
||||
控件中明确显示项目;用户可以切换到“全局任务”或“全部项目”,该选择只改变同一实例的列表
|
||||
范围,不创建第二个任务中心 Tab。切换回“跟随当前项目”后,项目变化立即刷新列表。后台状态
|
||||
可以更新徽标和排序,但不能抢占用户当前工作。
|
||||
|
||||
当前实现已按活动项目过滤 Scheduled Task 和审批,并提供“当前项目”“全局任务”“全部项目”
|
||||
范围控件。
|
||||
|
||||
主侧栏最近会话为关联 Task 提供轻量入口:父会话行只显示行首展开按钮,Task 子项使用
|
||||
任务图标和本地化摘要,展开层级只到 Task;点击 Task 子项打开同一 Conversation 并定位。
|
||||
@@ -265,7 +286,7 @@ Conversation,默认 Execute,并持续显示 Runtime、Project、工作目录
|
||||
### 7.2 监督
|
||||
|
||||
监督是通用观察与评论入口,详细行为以
|
||||
[会话监督 PRD](../supervision/conversation-supervision-prd.md) 为准。
|
||||
[会话监督 PRD](../conversation-supervision/prd.md) 为准。
|
||||
|
||||
监督能力在目录中稳定可发现;用户打开面板实例后可以选择:
|
||||
|
||||
@@ -311,10 +332,12 @@ Runtime 能力统一监督直连模型、OpenCode、Continue、DeepSeek Harness
|
||||
|
||||
### 7.4 终端
|
||||
|
||||
终端面板允许用户主动创建和管理本机或 SSH 终端:
|
||||
终端面板允许用户通过工作栏“+”主动创建和管理多个本机或 SSH 终端。详细产品行为、首期
|
||||
直接 SSH PTY 技术选择、生命周期和验收以
|
||||
[助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 为准。
|
||||
|
||||
- 新建、重命名、切换、关闭和重新连接终端。
|
||||
- 选择执行空间、工作目录和 Shell。
|
||||
- 创建时自动绑定当前项目执行空间、权威工作目录和平台默认 Shell,不显示重复目标选择。
|
||||
- 显示本机或远程主机、目录、Shell 和连接状态。
|
||||
- 支持复制、粘贴、搜索、清屏、滚动和调整终端尺寸。
|
||||
- 支持将终端切换到右侧、底部或独立窗口。
|
||||
@@ -345,6 +368,10 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
|
||||
|
||||
### 7.6 工作区
|
||||
|
||||
工作区是默认存在、单实例且不可关闭的基础应用。它默认跟随当前项目;项目或目录不可用时
|
||||
保留同一 Tab 并显示修复入口,不能通过关闭 Tab 隐藏范围错误。任务中心固定在第一位,工作区
|
||||
固定在第二位,恢复布局时保持此顺序。
|
||||
|
||||
工作区面板允许用户选择:
|
||||
|
||||
- 当前项目目录。
|
||||
@@ -353,30 +380,148 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
|
||||
|
||||
面板提供:
|
||||
|
||||
- 有界目录树和文本文件预览。
|
||||
- 有界目录树和分页文本文件预览;文件超过单页传输大小时先显示已读取内容,并允许用户继续加载后续内容,不因文件总大小超过单页上限而拒绝预览。
|
||||
- 当前选择、规范化路径和执行空间。
|
||||
- 可选 Git 状态、Diff 和仓库信息。
|
||||
- 显式打开、下载副本或在终端中打开。
|
||||
- HTML 文件的源码与安全预览。
|
||||
|
||||
当前项目目录的文件浏览与 Git 交互遵循以下规则:
|
||||
|
||||
- FR-W1:顶部同一行放置“文件 / Git 工作区”视图切换和右侧刷新;非 Git 目录仍保留刷新。
|
||||
下一行只提供当前视图的工具:文件视图提供新建文件、新建目录,Git 视图提供分支、Fetch 和更改显示方式,
|
||||
不在 Git 视图重复目录导航或新建文件操作。操作使用带名称和提示的紧凑图标按钮。
|
||||
文件视图在根目录不显示孤立的文件夹图标或空路径行;进入子目录后提供“项目目录名 / 子目录”
|
||||
文字路径导航和返回父目录按钮。根目录文字可点击返回,提示显示完整根路径;返回根目录后焦点移至顶部刷新。
|
||||
长目录名单行省略,完整路径通过提示查看;多级路径在导航内部横向滚动,不撑宽工作栏。
|
||||
选中目录时在该目录创建,选中文件时在其父目录创建,未选择时在当前浏览目录创建。
|
||||
无可用目录时隐藏无关操作。
|
||||
- 使用共享 `SegmentedControl` 切换“文件”和“Git 工作区”,默认显示文件;
|
||||
选择只在当前面板挂载期间保存在内存中,不持久化。未提交更改包含当前项目的全部
|
||||
Git 更改,不按 AI、用户或其他工具的修改来源筛选。
|
||||
- FR-W2:只有已确认的 Git 仓库显示 Git 工作区入口。临时读取或连接错误保留同一项目
|
||||
已确认的 Git 可用性和内容,提供重试;切换项目不能沿用上一项目的确认结果。
|
||||
- FR-W3:文件和目录行的更多按钮与左侧会话行一致,在悬停、键盘聚焦或菜单打开时显示。
|
||||
菜单复用会话操作的图标、间距和悬停样式,浮层限制在窗口内,不受文件列表滚动边界裁切;
|
||||
支持方向键、Home、End 和 Escape,Escape 或选定操作后焦点返回触发按钮,删除入口独立分隔。
|
||||
菜单提供重命名、移动、删除、属性、复制路径和本机默认应用打开;删除须明确确认对象
|
||||
与不可恢复性。操作失败保留输入与上下文。本机和远程均支持文件管理,系统默认打开仅本机可用。
|
||||
重命名允许仅修改同一目录项的名称大小写,不得覆盖另一个已存在的文件、目录或硬链接目录项。
|
||||
移动通过与创建项目相同的目录选择流程指定目标:本机打开系统目录选择器,远程浏览当前
|
||||
SSH Host 的目录。选择目录后保留原名,确认后执行;取消不移动文件。目标限于当前工作区,
|
||||
选择外部目录时在弹窗内说明限制,不切换项目或工作区范围。取消按钮使用共享次按钮样式,
|
||||
更多菜单中的删除图标和文字与其他菜单项一样左对齐。
|
||||
- FR-W4:Git 分支标签打开可搜索的本地与远程分支选择器,支持创建和切换分支,以及显式
|
||||
Fetch。切换使用普通 Git switch,保留可携带的未提交修改;冲突时显示 Git 错误。
|
||||
不自动 force、reset、discard 或 stash,不因浏览分支而自动 Fetch。
|
||||
分支触发器复用共享模型选择按钮,带分支图标和展开箭头;Fetch 使用共享次按钮,二者在
|
||||
紧凑工作栏内保持同一行,长分支名省略并提供完整名称提示。
|
||||
展开的分支选择面板不超过 360px 或工作栏可用宽度;搜索使用共享表单标签、输入框与主题样式,
|
||||
分支列表独立限高滚动,创建按钮以紧凑次操作放在列表下方。Escape 收起面板并将焦点返回分支触发器。
|
||||
- FR-W5:未提交更改支持列表和目录树切换;两种视图均可打开单文件 Diff。
|
||||
- FR-W6:Git 工作区底部提供默认收起的提交历史区,独立滚动,可调整高度。
|
||||
历史标题默认停在面板底部,首次展开向上占用当前 Git 内容区的一半高度;收起后标题回到底部。
|
||||
上方变更列表和下方提交历史独立滚动;分隔条支持指针及键盘调整比例,底部位置保持不变。
|
||||
顶部刷新入口同时更新已展开的提交历史,历史区内不重复提供刷新按钮。
|
||||
按当前分支分页读取提交,展示摘要、作者、时间、短 OID 和 refs。选中提交显示该次修改文件,
|
||||
再选文件进入内联提交 Diff;逐层返回保留分支、分页、选择、滚动位置与焦点。
|
||||
提交 Diff 保留重命名前后路径和实际内容修改;后台工作区刷新不得使正在加载的历史或详情
|
||||
永久停留在忙碌状态。
|
||||
历史展开入口使用带方向箭头的紧凑列表行;提交行沿用文件列表的表面、悬停和焦点样式,
|
||||
摘要与元信息左对齐分行展示,长内容省略并提供完整提示,不撑宽侧栏。
|
||||
- Git 错误和无更改提示只在 Git 工作区显示,不阻断文件浏览。
|
||||
切换项目时自动检测 Git 状态,无需手动刷新;检测结果和失败时保留的数据仅用于所属项目。
|
||||
- 未提交更改以彩色 Git 字母标识状态:M 修改、A 新增、D 删除、R 重命名、C 复制、
|
||||
U 未跟踪或冲突、T 类型变化;冲突用危险色区别于未跟踪文件,同时保留原始状态。
|
||||
- 点击更改项进入该文件 Diff,分别显示已暂存和未暂存差异,增删行使用对应状态色;
|
||||
删除文件也可查看。目录树中的文件仍打开内容预览。不再提供底部全仓库 Diff 入口。
|
||||
- 文件预览保留在右侧工作区内;预览期间隐藏列表但保持目录树挂载,返回后保留已展开
|
||||
目录、滚动位置和选中行,并恢复触发行焦点。Diff 返回未提交更改列表时同样恢复
|
||||
滚动位置、选中行和焦点。
|
||||
- 文件预览提供复制项目相对路径、刷新,以及仅本机项目可用的“使用默认应用打开”。
|
||||
刷新失败保留此前内容并显示错误;Markdown 默认渲染预览,可切换源码。
|
||||
- Diff 提供刷新、复制项目相对路径和“查看当前文件”;删除项不显示查看当前文件入口。
|
||||
- 手动刷新或任务结束更新 Git 状态时,同步重读根目录、当前浏览目录和已展开目录并去重,保留目录展开状态,
|
||||
刷新失败保留已有目录列表;重新展开目录时重读。目录读取错误在对应目录旁显示,
|
||||
可就地重试。正在查看的单文件 Diff 同时重读,旧请求不得覆盖新结果。
|
||||
无首次提交的仓库也应显示新增和暂存文件。
|
||||
- 未提交更改首批显示 50 项,可继续分批加载其余变更,不能使后续文件尤其是已删除
|
||||
文件失去 Diff 入口。
|
||||
- 系统默认应用和资源管理器打开按钮仅用于本机项目,SSH 项目不显示这两个按钮。
|
||||
|
||||
项目内目录导航属于 FR-W1;项目外目录选择、下载副本、终端打开及 HTML 安全预览仍属于后续能力。
|
||||
本轮工作区交互不包含项目文件搜索,也不调整其他工作栏页签。
|
||||
|
||||
本机和远程访问都必须由 Main 或远程 GoodBuddy Agent 在对应文件系统上执行路径规范化、相对路径和
|
||||
符号链接边界检查。Renderer 只能提交受约束的相对路径和已授权范围 ID。
|
||||
|
||||
### 7.7 浏览器
|
||||
|
||||
浏览器是可关闭的多实例应用。升级后的默认布局保留一个浏览器实例;用户从“+”目录选择
|
||||
浏览器时每次创建新的 Browser Tab。每个实例必须显示可区分的标题和绑定 Conversation,
|
||||
不能让多个工作栏 Tab 只是同一页面的重复控制入口。
|
||||
|
||||
浏览器能力在目录中稳定可发现,打开面板后允许用户:
|
||||
|
||||
- 创建新的 GoodBuddy 隔离浏览器会话。
|
||||
- 选择当前会话或其他受控浏览器会话。
|
||||
- 在当前 Conversation 的隔离浏览器上下文中创建独立 Browser Tab。
|
||||
- 选择当前 Conversation 或其他允许访问的 Conversation 作为实例目标。
|
||||
- 查看状态、当前 URL、有界画面和错误。
|
||||
- 进入明确的交互模式或停止会话。
|
||||
- 通过顶部浏览器工具栏返回、刷新或停止加载、编辑地址并前往。
|
||||
- 进入明确的交互模式或关闭浏览器。
|
||||
|
||||
没有浏览器会话时显示“新建浏览器会话”,而不是隐藏能力。模型或后台浏览器活动可以更新
|
||||
徽标,但不得无条件打开面板或切换用户当前面板。
|
||||
顶部工具栏按“返回、刷新/停止加载、地址输入、前往、交互、关闭浏览器”组织。“刷新”在
|
||||
页面加载期间原位切换为“停止加载”;“关闭浏览器”是清楚独立于导航的会话生命周期操作,
|
||||
必须与“停止加载”在文案和视觉上明确区分。现有“停止浏览器”语义统一改名为“关闭浏览器”。
|
||||
|
||||
同一 Conversation 下的 Browser Tab 共享隔离存储和代理上下文,但每个 Tab 拥有独立页面、
|
||||
导航状态、操作队列和元素引用空间。模型请求开始时绑定一个确定的 Browser Tab;用户切换
|
||||
工作栏实例不能在请求途中改变 Agent 的工具目标。用户在该实例执行前往、返回或刷新会有意
|
||||
改变绑定到同一 Tab 的 Agent 后续操作。所有请求都进入 `BrowserService`;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 时说明需要先选择或创建会话。
|
||||
模型或后台浏览器活动可以更新对应实例的徽标,但不得无条件打开面板或切换用户当前面板。
|
||||
|
||||
浏览器多实例的跨进程身份、可见 viewport 所有权、关闭语义和 MCP 兼容方案以
|
||||
[浏览器多实例技术设计](./browser-tabs-technical-design.md)为准。
|
||||
|
||||
浏览器面板只管理 GoodBuddy 受控浏览器,不表示可以控制用户已安装的浏览器。
|
||||
|
||||
### 7.8 成果
|
||||
### 7.8 资源监控
|
||||
|
||||
资源监控是本机单实例面板,显示整机 CPU、内存、磁盘 IO 和 GoodBuddy 桌面进程摘要。面板
|
||||
只在自身可见并处于活动状态时采样,数据保留在 Renderer 有界内存中,不进入运行记录或长期
|
||||
存储。首期不提供远程主机、GPU、显存、网络吞吐、后台告警或任意进程查看。
|
||||
|
||||
指标缺失时保留面板入口并显示不可用原因。资源监控入口、采样频率、数据契约与验收由
|
||||
[应用导航与系统工具入口](../application-tool-navigation/README.md)定义。
|
||||
|
||||
### 7.9 成果
|
||||
|
||||
成果面板统一显示全局、项目、Conversation、Task 执行和监督显式产生的独立成果:
|
||||
|
||||
@@ -404,7 +549,20 @@ Task 呈现来源,不把 Run 作为导航对象。
|
||||
- 不使用 `dangerouslySetInnerHTML`,不启用 Electron `webviewTag`。
|
||||
- 外部打开是明确的用户操作,并说明外部浏览器可能执行脚本或联网。
|
||||
|
||||
### 7.9 Composer 与任务上下文
|
||||
### 7.10 文档
|
||||
|
||||
文档是可创建多个实例的工作栏应用。用户从工作区、成果或会话文件引用打开受支持的 Office
|
||||
文件时新增文档 Tab;每个实例固定到一个 ShareServer 编辑会话,不随当前 Conversation 或
|
||||
Project 切换。Tab 使用文档文件名,同名实例增加序号,完整来源通过 Tooltip 和页签内信息
|
||||
显示。编辑器、保存、冲突、AI 选区修改和关闭行为以
|
||||
[Office 协同编辑](../office-document-editing/README.md)为准。
|
||||
|
||||
完整编辑器需要至少 `720px` 可用宽度;低于 `480px` 时不把第三方工具栏压缩到重叠,而显示
|
||||
摘要和“展开编辑”命令。展开到主内容区、底部或独立窗口继续使用同一页签实例和编辑会话。
|
||||
文档编辑依赖用户明确配置且声明健康能力的 ShareServer;不可用时保留入口和原因,不回退为
|
||||
未经说明的远程服务。本地默认应用打开仍是独立用户操作。
|
||||
|
||||
### 7.11 Composer 与任务上下文
|
||||
|
||||
工作栏不提供独立“上下文”能力。用户准备下一次模型请求时:
|
||||
|
||||
@@ -428,7 +586,9 @@ type WorkbarCapabilityId =
|
||||
| 'processes'
|
||||
| 'workspace'
|
||||
| 'browser'
|
||||
| 'resources'
|
||||
| 'results'
|
||||
| 'document'
|
||||
|
||||
type WorkbarTargetRef =
|
||||
| { type: 'conversation'; id: string }
|
||||
@@ -441,6 +601,7 @@ type WorkbarTargetRef =
|
||||
| { type: 'managed-process'; id: string }
|
||||
| { type: 'browser-session'; id: string }
|
||||
| { type: 'artifact'; id: string }
|
||||
| { type: 'document-session'; id: string }
|
||||
```
|
||||
|
||||
Renderer 选择目标后,Main 必须重新验证对象存在、归属范围和当前用户可见性。不能把目标 ID
|
||||
@@ -470,7 +631,7 @@ type WorkbarPanelInstance = {
|
||||
|
||||
本节定义产品入口和用户行为。Host Key、凭据、远程 Agent 协议、模型请求代理、签名更新、
|
||||
断线和 Runtime 适配的技术边界以
|
||||
[SSH 远程主机与 GoodBuddy Agent 稳定终态设计](../../architecture/remote-host-and-goodbuddy-agent-design.md)
|
||||
[SSH 远程主机与 GoodBuddy Agent 稳定终态设计](../remote-host/technical-design.md)
|
||||
为准。
|
||||
|
||||
### 9.1 设置入口
|
||||
@@ -512,10 +673,12 @@ SSH/远程项目操作;已有 Host、项目和凭据保持不变。若当前
|
||||
不代表环境健康。环境准备失败或取消保留 Host 并显示可重试状态;重新安装失败时显示
|
||||
本次操作未完成,并以随后重新检查的版本卡片表达当前事实,不对不确定提交宣称旧版本
|
||||
未被替换。详细技术契约见
|
||||
[SSH Host 远程环境准备与直连下载设计](../../architecture/remote-host-environment-provisioning-design.md)。
|
||||
[SSH Host 远程环境准备与直连下载设计](../remote-host/environment-provisioning-technical-design.md)。
|
||||
|
||||
进入或切回“主机与远程执行”页面只读取本地 Host 列表,不逐台建立 SSH 连接。版本和瞬时
|
||||
下载能力仅在用户点击某台 Host 的“刷新版本”时探测。该动作不是项目连接前置条件;用户
|
||||
下载能力仅在用户点击某台 Host 的“刷新版本”时探测,成功结果在当前 Renderer 进程内按
|
||||
Host 持久化缓存,切换页面或重启应用后返回直接显示;Host 编辑、删除或开始更新远程环境后
|
||||
不复用旧结果。该动作不是项目连接前置条件;用户
|
||||
切换已有远程项目只更新 Renderer 的本地项目选择,不建立 Host/Agent 连接。
|
||||
新建远程项目弹窗同样只读取本地 Host 验证记录,不扫描所有 Host;浏览目录或保存时才连接
|
||||
所选 Host。版本号相同时仍允许用户显式重新安装,以修复 registry、签名或 identity 异常,
|
||||
@@ -707,12 +870,16 @@ remote_operations
|
||||
其中:
|
||||
|
||||
- 工作栏偏好只保存能力目录顺序、面板实例、停靠布局、尺寸和目标引用。
|
||||
- 资源监控只保存面板实例,不保存采样结果或历史曲线。
|
||||
- 主机表只保存非敏感元数据和加密凭据引用。
|
||||
- 应用重启后先通过远程 Daemon 的 journal、lease、boot identity 和 event cursor 核对活动
|
||||
终端、进程与 Runtime;无法确认时标记 `interrupted` 或 `outcome_unknown`。
|
||||
- 日志使用有界环形缓冲或分页持久化,不能无限写入 SQLite。
|
||||
- Artifact 继续作为成果的权威实体,不把完整成果复制进工作栏状态。
|
||||
- 当前 Renderer `localStorage` 活动记录不能作为 Runtime、监督或进程的权威来源。
|
||||
- 运行记录由 Main SQLite 持久化,Renderer 只保留当前内存视图。升级时迁移旧
|
||||
`localStorage` 记录;若旧缓存达到原数量、详情或总大小边界,页面持续说明更早记录或
|
||||
部分详情可能已经丢失。新记录不再按固定活动数或详情字符数丢弃,长列表只做可继续展开的
|
||||
分批渲染。
|
||||
|
||||
### 11.3 IPC 与 Preload
|
||||
|
||||
@@ -723,7 +890,7 @@ Renderer 只通过显式方法访问:
|
||||
- 终端创建、输入、调整大小、关闭和有界输出订阅。
|
||||
- 受管进程列表、日志和允许的控制动作。
|
||||
- Runtime Inspector 快照、事件和允许的控制动作。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务,以及有界的本机资源快照。
|
||||
|
||||
每个 Main Handler 都必须验证可信发送者、Zod 输入、对象归属和当前状态。不得暴露 raw
|
||||
Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
@@ -732,7 +899,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
1. 工作栏能力目录项和面板实例不授予任何能力;权限只由 Main 中的范围和控制契约产生。
|
||||
2. Ask 在本机和远程 Runtime 边界保持只读。
|
||||
3. Execute 继续经过现有 Runtime 和审批控制,工作栏不能直接放宽。
|
||||
3. Execute 使用当前执行账号的完整权限,不另加目录或逐工具审批;工作栏不能绕过 Main、Preload 与用户数据边界。
|
||||
4. 用户终端和 Agent 工具执行使用不同身份和事件来源。
|
||||
5. 进程面板只控制 GoodBuddy 受管对象,不接受任意 PID。
|
||||
6. 本机和远程路径分别在对应文件系统上 canonicalize 并验证符号链接边界。
|
||||
@@ -803,6 +970,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
### 阶段 1:成果与 Runtime 可观测性
|
||||
|
||||
- 本机 CPU、内存、磁盘 IO 和 GoodBuddy 桌面进程摘要的按需资源监控。
|
||||
- 通用 Artifact Event。
|
||||
- HTML 工作区和成果的静态即时预览。
|
||||
- Runtime Inspector Snapshot 与事件。
|
||||
@@ -813,7 +981,8 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
### 阶段 2:监督、终端与受管进程
|
||||
|
||||
- 普通会话手动监督和右栏评论流。
|
||||
- 本机 PTY 终端。
|
||||
- 工作栏持久“+”、应用注册表和单/多实例 Tab。
|
||||
- 本机 PTY 与直接 SSH PTY 多终端。
|
||||
- 受管进程注册、日志和终止。
|
||||
- 自动回复后监督、节流和独立预算。
|
||||
- 用户选择终端停靠位置。
|
||||
@@ -833,8 +1002,9 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- 签名、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 bundle 的 Ask/Execute 均直接启动,Ask 在工具边界保持只读,
|
||||
Execute 使用账号完整权限;官方 ACP channel、Agent 本地模型网关、取消、超时和活动记录
|
||||
以[远程主机技术设计](../remote-host/technical-design.md)为准。
|
||||
- OpenCode 闭环稳定后,Continue 与 DeepSeek Harness 复用相同面板和通道契约逐个接入。
|
||||
- 新增或重新验证 Host 时只保存并探测 Agent/Runtime,不自动安装。用户在 Host 卡片用
|
||||
单一主按钮启动,并用次级控件选择自动、Host 下载或 GoodBuddy 传输;Host 保存与环境
|
||||
@@ -862,7 +1032,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
### 17.1 稳定能力目录与用户控制
|
||||
|
||||
- [ ] 八个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] 十个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] Task Center 继续作为 Task 的单例索引,不删除入口、不复制会话,也不混入 Job、Run 或心跳事项。
|
||||
- [ ] 项目、会话、Runtime 或主机变化不会无提示地增删能力目录项。
|
||||
- [ ] 用户可以按需打开、关闭、排序和停靠面板实例。
|
||||
@@ -877,6 +1047,8 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [ ] 监督可以作用于普通 Conversation、Task 和后续实验对象,不假设编程语境。
|
||||
- [ ] 工作区不是 Git 仓库时仍可浏览文件。
|
||||
- [ ] Runtime 不支持某项原生能力时仍可从目录打开面板并获得准确说明。
|
||||
- [ ] 文档能力可以打开多个以文件名命名的固定实例;未配置健康 ShareServer 时显示原因,
|
||||
不把系统默认应用或静态预览冒充内置编辑。
|
||||
|
||||
### 17.3 安全与控制
|
||||
|
||||
@@ -915,22 +1087,32 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [ ] 仅使用键盘可以选择能力、面板实例、目标、执行空间和控制动作。
|
||||
- [ ] 状态不只依赖颜色,徽标具有文字或可访问名称。
|
||||
- [ ] 终端、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/smart-heartbeat-prd.md) 定义心跳入口、范围和长期边界。
|
||||
- [会话监督 PRD](../supervision/conversation-supervision-prd.md) 定义监督判断、证据、预算和介入边界。
|
||||
- [Task Center PRD](../task-and-job/task-center-prd.md) 定义单实例、项目跟随和显式范围切换。
|
||||
- [智能心跳 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 设计](../../architecture/deepseek-harness-runtime-design.md) 定义该 Runtime 的具体适配边界。
|
||||
- [SSH Host 远程环境准备与直连下载设计](../../architecture/remote-host-environment-provisioning-design.md)
|
||||
- [DeepSeek Harness Runtime 设计](../deepseek-harness/technical-design.md) 定义该 Runtime 的具体适配边界。
|
||||
- [SSH Host 远程环境准备与直连下载设计](../remote-host/environment-provisioning-technical-design.md)
|
||||
定义 Host 级组件获取、首次准备、更新、回退和项目只验证边界。
|
||||
- [Office 协同编辑](../office-document-editing/README.md) 定义文档多实例、ShareServer 编辑会话、
|
||||
保存、冲突和 AI 选区修改。
|
||||
- [统一界面设计系统](../../../UI-DESIGN.md) 定义视觉、语义、响应式和无障碍规则。
|
||||
|
||||
若其他文档把工作栏描述为八个同时固定显示的栏目、根据项目或 Runtime 自动裁剪的动态入口,
|
||||
若其他文档把工作栏描述为十个同时固定显示的栏目、根据项目或 Runtime 自动裁剪的动态入口,
|
||||
或仅属于当前聊天的附属区域,以本文“能力目录稳定、面板实例由用户打开、当前上下文只提供
|
||||
默认值”的产品决策为准。
|
||||
@@ -0,0 +1,316 @@
|
||||
# 工作栏实现与验证进度
|
||||
|
||||
## 2026-09-10 提交后保留问答
|
||||
|
||||
- 桌面端在提交成功后保留结构化问答回顾,支持多轮回答、跳过及本地会话重新加载;
|
||||
交互规则见 [Runtime 交互边界](./runtime-interactions.md#运行与失败)。
|
||||
- App 回归覆盖提交与重载、下一轮问题提前到达、终态先于提交返回、提交失败;数据库
|
||||
回归覆盖完整替换、增量保存、关闭重开、多选及超过 1,000 字符的自定义答案。
|
||||
- `npm test`:3,900 项通过、66 项跳过,Agent 离线依赖清单测试在 60 秒超时;
|
||||
该项独立复跑通过(58.33 秒)。`npm run typecheck`、`npm run lint` 与差异空白检查通过。
|
||||
- 本轮未手动运行真实 OpenCode 问答会话,外部模型调用 0 次。修改仅涉及桌面端展示与
|
||||
本地保存,不改变 Runtime 回传协议或 Agent 路径,不新增远程 ACP 问答能力。
|
||||
|
||||
## 2026-09-10 Git 历史测试夹具修复
|
||||
|
||||
- 桌面候选 `877cf58` 的 CI 在四项工作区测试准备 Git 提交时因缺少作者身份失败。
|
||||
`c037d7f` 移除测试身份后引入了机器配置依赖;本机已有身份使该问题未在本地暴露。
|
||||
- 测试现在把已暂存的 tree 写入固定 Git commit 对象,并仅更新临时仓库的分支引用,
|
||||
不调用 `git commit`,不修改或覆盖任何 Git 身份配置。历史、重命名差异、分页和脏文件
|
||||
分支冲突仍由真实 Git 与生产 `manageWorkspace` 验证,不用 mock 或跳过用例替代。
|
||||
- 新增夹具自检确认 `git fsck --strict` 通过、父提交链有效、未暂存内容保持不变且仓库配置未改。
|
||||
聚焦测试 11 项通过、2 项因当前文件系统大小写不敏感而跳过;无身份 Runner 的验证待新候选 CI。
|
||||
- 完整 `npm test` 为 3845 项通过、66 项跳过、1 项未修改的活动列表测试超时;
|
||||
随后 `ActivityPanel.test.tsx` 的 16 项独立复跑全部通过,未放宽时限或删除断言。
|
||||
`npm run release:notes:verify`、`npm run typecheck`、`npm run lint` 通过。
|
||||
未将这次本地整库结果记为全绿,发布仍以新候选的 CI 校验和生产构建通过为前提。
|
||||
- 本轮只改测试和验证记录,产品功能、Agent 源码及已批准的双语发布说明不变,无外部模型调用。
|
||||
|
||||
## 2026-09-10 工作区布局与分支输入样式
|
||||
|
||||
- 按 [FR-W1、FR-W4](./prd.md#76-工作区) 将视图切换和刷新合并到顶部行,
|
||||
文件与 Git 工具按视图显示;移除根目录的孤立图标及空导航行,子目录保留文字路径导航。
|
||||
分支搜索复用共享 `field`,面板限宽、分支列表限高滚动,创建按钮使用紧凑次操作。
|
||||
- 新增布局、子目录返回、焦点恢复、分支搜索/切换/创建回归;工作区、设置界面、设置存储、
|
||||
IPC 和 Sidebar 五个测试文件共 354 项通过。另确认安全与数据的默认 `toolApproval` 为
|
||||
`always`,对应“Execute 自动授权已启用的工具”;已有明确保存的禁止策略仍保留。
|
||||
本轮没有修改授权策略或默认值。
|
||||
- 当前构建的 Windows Electron 使用独立 profile 和临时 Git 项目,实际 App → Preload →
|
||||
Main 路径通过分支搜索、切换、创建和子目录新建文件;磁盘与 Git 分支结果均确认。
|
||||
文件/Git 切换保留浏览目录,返回根目录后导航行移除、焦点回到刷新;Escape 收起分支面板并恢复焦点。
|
||||
真实安全与数据页面的默认选中项也确认为自动授权。
|
||||
- 浅色与深色下,将实际工作栏布局宽度设为 300、420、650px 后均无工作区横向溢出。
|
||||
输入框为共享 13px 正文、标签 11px,分支面板最大 360px;15 个分支时列表可在 240px 内滚动。
|
||||
这些是布局检查,不作为分栏拖拽或原生浏览器鼠标交互的验证。
|
||||
- `npm run build:bundle`、`npm run typecheck`、`npm run lint` 通过。
|
||||
整库 `npm test` 为 3843 项通过、1 项失败、66 项跳过;失败项是未修改的
|
||||
`opencode-runtime-lifecycle.test.ts` 事件流关闭时序断言,单独复跑通过。
|
||||
未再次运行整库,不将此结果记为整库全绿。
|
||||
- 本轮产品改动仅为共享 Renderer 工作区布局,未改 Main/Agent 的工作区命令、
|
||||
desktop-to-Agent 协议或 Runtime 执行逻辑;远程项目继续使用同一组件和既有回调,
|
||||
本轮未做新的 Host 实测。无外部模型调用,所属测试应用、浏览器会话和调试监听已关闭。
|
||||
|
||||
## 2026-09-10 提交审查修复
|
||||
|
||||
- [FR-W3、FR-W6](./prd.md#76-工作区):同一目录项可仅修改大小写,仍拒绝覆盖其他文件、
|
||||
目录和硬链接目录项;提交 Diff 同时使用重命名前后路径;分支后台刷新不再使历史请求
|
||||
遗留 busy;自动目录刷新包含当前浏览目录并去重。
|
||||
- 浏览器请求仅预留目标身份,首次导航才创建实际资源;Main 状态携带工作栏实例身份,
|
||||
Renderer 直接绑定同一页面,停止或回收后清除旧绑定。跨会话显式选择不会被应用类型
|
||||
同步覆盖,终端关闭确认期间释放原生浏览器 viewport。规则以
|
||||
[浏览器多实例技术设计](./browser-tabs-technical-design.md) 为准。
|
||||
- 删除普通浏览器操作的自动截图、固定等待、重试及 JPEG 状态缓存/IPC 字段,保留显式截图。
|
||||
- 整合复核补齐现有 32 页签容量处理与 Harness 代理的已发现工具复用。新增容量回归验证
|
||||
满额时布局仍有效、页面保留,关闭一项后显示原页面;Harness 回归验证发现接口后续失败时
|
||||
已知工具调用不再触发发现。相关三文件 66 项通过,最新 typecheck 与 lint 通过。
|
||||
- 工作区聚焦测试 43 项通过、2 项需大小写敏感文件系统而跳过;IPC 与 Sidebar 聚焦回归
|
||||
132 项通过。随后新增的 Ask 指令、继承 Runtime 用量持久化及计划模式 App 场景,3 项通过。
|
||||
- 扩展真实 Electron/MCP E2E 在独立临时 profile 通过:四个未使用的请求预留不创建会话、
|
||||
WebContents 或 UI 事件;首次 MCP 导航物化相同 Tab/工作栏身份,恢复不额外创建页面,
|
||||
并验证独立页面、共享 Cookie、请求租约关闭保护和清理。
|
||||
- 当前源码的共享 Linux x64 Host 验证已通过,覆盖工作区目标保护和重命名/Git Diff,
|
||||
并通过桌面托管 ACP → 隔离 Agent → OpenCode 完成 Ask/Execute 和继承 Runtime 指标持久化;
|
||||
该组共 3 次真实模型调用,清理后无所属进程残留。完整记录见
|
||||
[远程 Host 开发验证](../remote-host/technical-design.md#agent-开发期间的真实-host-验证)。
|
||||
- 最终整库 `npm test` 为 341 个文件通过、9 个文件跳过,3839 项通过、66 项按平台或手动
|
||||
条件跳过、0 失败;`npm run typecheck`、`npm run lint`、开发期 `npm run build` 和
|
||||
`git diff --check` 通过,19 份修改 Markdown 的相对链接目标检查通过。
|
||||
先前整合中的一轮为 3823 项通过、6 项失败、66 项跳过,另有被移动的测试路径加载失败,
|
||||
不作为最终结果;源码稳定后完整重跑得到上述全绿结果。
|
||||
- 原文档解析焦点回归改为等待 Modal 的焦点效果,保留焦点与背景隔离断言,没有改动产品逻辑。
|
||||
- Windows 独立 Electron profile 的完整 App → Preload → Main UI 验证通过;最终复测使用
|
||||
本轮最终开发构建。启用浏览器后普通 Execute 没有创建原生浏览器视图,工具导航只创建
|
||||
一张同时显示在工作栏的页面,跨会话选择保留原页面与 viewport。
|
||||
- 实际终端连通后,从浏览器关闭该终端可正常显示确认并操作按钮;原生视图可见性按
|
||||
可见 → 确认时隐藏 → 取消后可见变化,终端关闭清理完成。无效地址 `http://` 被拒绝后,
|
||||
原控件可重试有效地址并关闭页面;关闭端口返回了过滤代理错误页,因此该 UI 场景不作为
|
||||
Chromium `loadURL` 拒绝的实机证据。服务与 Renderer 释放绑定回归另有自动测试覆盖。
|
||||
- 桌面使用确定性 loopback 模型服务驱动真实生产路径,共 24 次本地 completion 请求,
|
||||
未发出外部模型请求;可选真实 Provider 探针没有找到已加密保存的 OpenAI 兼容文本连接,
|
||||
未据此宣称桌面真实模型验收通过。Ask 与编程 Subagent 的桌面结果见
|
||||
[直连模型验收记录](../direct-model-agent/progress.md#2026-09-10-提交审查后的桌面-ui-验收)。
|
||||
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
|
||||
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
|
||||
|
||||
## 2026-09-10 固定基础页签与浏览器多实例
|
||||
|
||||
- 工作栏注册表已分别声明实例策略、默认上下文、默认打开、必须存在、可关闭和排序属性。
|
||||
任务中心与工作区保持默认存在且不可关闭;浏览器改为当前 Conversation 上下文的多实例。
|
||||
布局加载按 `required` 补回缺失的基础页签,并修复失效活动实例。
|
||||
- 任务中心增加“当前项目”“全局任务”“全部项目”范围,默认跟随活动项目;任务和审批按
|
||||
Conversation 所属项目过滤,范围模式随工作栏布局持久化。
|
||||
- 浏览器服务已拆分 Conversation Context 与 Browser Tab。每个 Tab 拥有独立页面、Driver、
|
||||
操作队列、状态和元素引用空间,同一 Conversation 的 Tab 共享 partition、代理和 Cookie。
|
||||
- Renderer 使用工作栏实例 UUID 原子创建或恢复 Browser Tab,并以 UUID viewport token 防止
|
||||
延迟 cleanup 隐藏新活动 Tab。关闭单个实例只释放对应 Tab。
|
||||
- 直连模型和请求级 MCP capability 在请求开始时优先绑定可见 Tab,其次绑定 primary Tab,
|
||||
并持有使用租约。请求结束或 capability 撤销前,绑定 Tab 拒绝关闭;工具参数未增加模型可见
|
||||
`tabId`,已有浏览器工具 schema 保持不变。
|
||||
- 聚焦回归命令覆盖 Shared、BrowserService、Electron Session、IPC、Preload、MCP、Renderer
|
||||
和 Workbar,13 个测试文件共 453 项通过;`npm run lint` 与 Web TypeScript 检查通过。
|
||||
- `node build/run-browser-tabs-electron-e2e.cjs` 通过。该测试启动真实 Electron/Chromium 和本地
|
||||
HTTP 页面,创建两个 `WebContentsView`,验证页面隔离、同 Conversation Cookie 共享、真实
|
||||
loopback MCP 的固定 Tab 导航与快照、使用租约关闭保护,以及单 Tab 和最终 Context 清理。
|
||||
- `npm run typecheck`、`npm run lint`、`npm run build:bundle` 和 `git diff --check` 通过。
|
||||
- 本轮整库 `npm test` 为 3686 项通过、61 项跳过、4 项失败。失败位于 portable/release 的
|
||||
ASAR 元数据 fixture、Runtime 版本探测和 DeepSeek Harness 的本地 MCP fixture;定向复跑仍
|
||||
失败。本次工作栏、浏览器、IPC 和 MCP 聚焦回归均通过,不将整库结果记为通过。
|
||||
|
||||
## 2026-09-09 移动目录选择与 Git 控件修正
|
||||
|
||||
- 本轮在已有文件管理改动上修正 [FR-W3、FR-W4、FR-W6](./prd.md#76-工作区):
|
||||
本机移动调用创建项目使用的 `settings.selectWorkspace()`;远程移动调用
|
||||
`sshHosts.browseDirectories()`,按项目 Host 浏览。所选目录映射为工作区相对目标,
|
||||
保留原名;取消不提交移动,外部目录在弹窗内提示限制。
|
||||
- 取消、分支、Fetch、历史展开和提交行接入共享控件样式;删除菜单项左对齐。
|
||||
远程目录列表限制滚动高度,保留加载、空目录、失败和截断提示。
|
||||
- `npm test -- src/renderer/src/WorkspaceFilesPanel.test.tsx src/renderer/src/RightAssistantSidebar.resize.test.tsx src/main/workspace/workspace-management.test.ts src/main/ssh/ssh-host-directory-browser.test.ts`
|
||||
通过 56 项,覆盖本机目录选择桥接、Windows 与 POSIX 目标映射、取消与越界选择、
|
||||
远程 Host 和目录参数、取消未完成的浏览,以及 Git 控件与真实文件管理服务。
|
||||
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git|opens the global assistant sidebar"`
|
||||
通过 12 项。工作栏切换测试改为等待项目目录加载后出现刷新按钮。
|
||||
- `npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
|
||||
- 本轮整库 `npm test` 完整运行 944 秒,3642 项通过、61 项跳过、6 项失败。
|
||||
工作栏加载时序断言已修正并定向复测通过;其余 5 项涉及 Agent 离线依赖测试超时、
|
||||
DeepSeek Harness MCP 加载、Runtime 版本探测,以及 portable/release 打包产物检查。
|
||||
未再次运行整库测试,不记为整库通过。
|
||||
- 隔离 Electron 窗口加载当前源码组件和 CSS,检查浅色、深色主题下 300px、420px 工作栏:
|
||||
无横向溢出,分支与 Fetch 同行,提交行左对齐;移动弹窗没有路径输入框,取消为次按钮,
|
||||
删除项计算样式为左对齐。截图保留于本机临时验证目录。此检查使用桥接替身,未自动操作
|
||||
系统原生目录对话框,也不替代完整应用中的人工验收。
|
||||
- 尝试从隔离 Electron 配置调用真实 SSH 目录浏览服务时,已有凭据在
|
||||
`safeStorage.decryptString` 解密失败,尚未建立 Host 连接。真实远程目录选择验收仍受阻;
|
||||
未修改用户配置或远端文件,本轮未调用真实模型。
|
||||
|
||||
## 2026-09-09 文件管理与 Git 工作区扩展
|
||||
|
||||
- 已只读核对根目录规范、UI 设计系统、目录浏览组件、WorkspaceAccess 与 Agent 协议。
|
||||
本轮范围定义于 [PRD 工作区 FR-W1 至 FR-W6](./prd.md#76-工作区)。
|
||||
- 当前生产路径具备目录、属性、文本预览和工作树 Diff;Agent 的写入及 Git operation
|
||||
仍是只读拒绝接口。文件管理、分支与提交历史尚未实现或验证,不能视为完成。
|
||||
- 待完成源码实现、整库 test/typecheck/lint、真实桌面路径与当前源码 Linux Host 验证。
|
||||
本轮开始前已有浏览器与工作区未提交改动,保留这些改动;不提交或推送。
|
||||
|
||||
## 2026-09-09 工作区页签交互
|
||||
|
||||
- 已核对当前工作树中的 `RightAssistantSidebar.tsx` 和 `WorkspaceFilesPanel.tsx` 差异:
|
||||
文件预览期间目录树保持挂载,接入返回位置与焦点恢复、文件与未提交更改分段视图、
|
||||
目录就地重试,以及文件预览和 Diff 操作。行为定义见 [PRD 工作区](./prd.md#76-工作区)。
|
||||
- `npm test -- src/renderer/src/RightAssistantSidebar.resize.test.tsx src/renderer/src/WorkspaceFilesPanel.test.tsx`
|
||||
通过 35 项,覆盖目录展开、返回位置与焦点、视图选择、刷新失败保留内容、Markdown
|
||||
源码切换,以及切换项目后丢弃未完成的文件预览。
|
||||
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git"` 通过 11 项,
|
||||
覆盖应用中的文件预览、路径复制、默认应用打开、返回和任务结束刷新。
|
||||
- 本轮 `npm test` 完整运行结果为 3629 通过、61 跳过、5 失败。失败位于 Agent 离线
|
||||
依赖打包超时、DeepSeek Harness MCP 加载、Runtime 版本探测,以及两个打包产物检查,
|
||||
不将该结果记为整库回归通过。
|
||||
- `npm run lint` 和差异空白检查通过。最新 `npm run typecheck` 被同时进行的浏览器
|
||||
接口变更阻塞:测试替身缺少 `hide`、`setBounds`、`setViewport`,部分侧栏测试仍传递
|
||||
已移除的浏览器回调。该轮类型检查未通过,未改动另一项任务的接口或测试替身。
|
||||
- 尚未进行真实桌面视觉验收;组件测试的滚动断言不代替实际窗口中的布局和滚动检查。
|
||||
- 范围仅限工作区页签,不包含项目文件搜索或其他页签改动。
|
||||
|
||||
## 2026-09-09 托管 Runtime 并行阻塞
|
||||
|
||||
- Windows 完整隔离测试程序通过界面发送 OpenCode 原生双子代理、直连与 Continue 文件
|
||||
任务;随后三路上游请求在 917 ms 内发出并重叠,全部完成。普通并发没有复现全局排队。
|
||||
- 在同一程序中,让真实模型执行一次 4,000 行、约 98 KB 文件的 CRLF → LF 转换,
|
||||
同时运行另一个会话。原生进程的无凭据存活探针两次延迟 3,301 ms、3,085 ms;
|
||||
原生热线程持续使用 CPU,而 Main 没有同步满载。操作不是模拟事件或直接调用
|
||||
Runtime 的脚本,模型请求来自实际输入框和发送按钮。
|
||||
- 托管 OpenCode 关闭未消费的自动 Git 快照后,完整程序重复相同操作和并行会话,
|
||||
90 秒观察中的最大原生探针延迟为 210 ms,没有超过 500 ms 的探针,两条请求均完成。
|
||||
该对照验证已复现的快照阻塞得到消除,不声称已经证明历史 90–125 秒事件的唯一原因。
|
||||
- Continue 的实际 Electron 原生宿主在不排空 stdout 时被注入的 128 KiB 输出阻塞;
|
||||
使用当前生产适配器后,输出在 5 ms 内完成,20 次状态读取全部成功,最大 5 ms。
|
||||
这证明管道修复有效,不代表普通启动约 640 字节的输出就是历史卡顿原因。
|
||||
- 修复版完整程序再次完成原生双子代理与 Continue 文件任务,实际请求重叠,三个页面
|
||||
都通过计数按钮功能检查。Windows 诊断与程序验证合计 40 次成功模型调用;
|
||||
另一次 UI 请求返回账户额度不足的 402,用户充值后才继续,没有把失败当成通过。
|
||||
- 当前源码 Linux x64 Agent 经桌面生产管理链路完成 Ask 读取和 Execute 换行转换,
|
||||
共 4 次成功模型调用;最终原生配置确认 `snapshot: false` 且权限仍为 `ask`/`allow`。
|
||||
此验证覆盖实际 Agent、模型桥、工具完成和关闭,不量化 Linux 提速,也未注入取消。
|
||||
- 定向回归先验证 7 项失败,再验证 124 项通过、5 项 Windows 平台跳过;
|
||||
全项目类型检查和修改文件 lint 已通过。首次全量回归为 3625 通过、1 失败、61 跳过;
|
||||
失败来自生命周期测试的包装流在底层 reader 关闭后仍计数为打开,已通过独立流实验
|
||||
复现并改为观察 `reader.closed`,没有因此修改生产取消逻辑。最终候选全量回归为
|
||||
3626 通过、0 失败、61 跳过,`npm run typecheck`、`npm run lint`、
|
||||
`npm run release:notes:verify`、差异空白与文档相对链接检查通过。本机和远端的
|
||||
测试进程已回收,安装版和正式 Host 环境未改动;正式发布仍需候选 CI 与资产核实。
|
||||
运行策略见
|
||||
[托管 Runtime 并行与输出](./runtime-interactions.md#托管-runtime-的并行与输出)。
|
||||
|
||||
## 2026-09-08 消息底部请求状态
|
||||
|
||||
- 已接入本机 OpenCode 父会话的原生重试等待及开始事件;直连模型把退避等待与实际发起
|
||||
重试分开显示,Continue 初始文案改为中性的请求处理。显示规则和未接入边界见
|
||||
[消息底部请求状态](./runtime-interactions.md#消息底部请求状态)。
|
||||
- 消息底部按真实事件显示准备、工具、待回答及终态;有新进展时清除旧状态,结束后不
|
||||
接受迟到状态覆盖。仅该处状态点改为静态,会话列表闪点、发送和停止按钮未改动。
|
||||
- Windows 隔离开发桌面通过实际 UI 发送验证 OpenCode 的 503/Retry-After 等待、
|
||||
原生重试开始、真实模型回复与完成;直连模型通过派发前连接拒绝、500 ms 等待、
|
||||
重试、真实回复与完成。故障由本机测试代理注入,不冒充服务商实际故障。
|
||||
三个隔离测试配置总计 5 次真实文本模型调用;另有一次直连重试被测试代理的调用
|
||||
上限明确拒绝,界面正确显示失败,最终单次补测完成。未执行用户命令或修改安装版。
|
||||
- DOM 状态记录确认底部无闪烁、完成后无旧重试文案,浅色截图可见原生重试计划与原因;
|
||||
底部 WCAG 2 A/AA 定向检查无违规。最小化窗口的补充截图超时,未作为通过证据。
|
||||
隔离窗口及测试代理已结束。
|
||||
- 最终 `npm test -- --reporter=json --outputFile=<临时报告路径>` 为 3618 项通过、
|
||||
0 项失败、61 项按平台或手动条件跳过;首轮的压缩状态旧断言已修正,途中被用户中断
|
||||
的运行不计为通过。`npm run typecheck`、`npm run lint`、
|
||||
`npm run release:notes:verify`、差异空白与相关文档相对链接检查通过。
|
||||
|
||||
## 2026-09-08 Desktop 0.12.8 安装包修复
|
||||
|
||||
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
|
||||
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
|
||||
后,插件及传递依赖逐文件复制验证通过。
|
||||
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
|
||||
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
|
||||
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
|
||||
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
|
||||
不能把当前源码回归通过当作真实发布包已通过。
|
||||
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
|
||||
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
|
||||
|
||||
## 2026-09-08 Execute 目录权限等待修复
|
||||
|
||||
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
|
||||
禁用工具,当前请求精确分配的只读能力例外。Execute 不把 Workspace 当作目录边界。
|
||||
- `permission.asked` 不再忽略所有非父会话事件。Main 用 OpenCode Session 的 `parentID`
|
||||
确认属于当前请求的子孙会话后按当前模式回复;其他并行会话不受影响,同一请求只回复
|
||||
一次。子会话权限不伪装为父会话中的待完成工具。
|
||||
- 真实 OpenCode `1.18.9` 和候选 `1.18.29` 均已通过本地确定性模型驱动的原生 Task
|
||||
工作区外读取回归;同一 Runtime 的 Ask 仍不暴露文件与命令工具。
|
||||
- Continue `1.5.47` 的真实 CLI 已通过 Execute Shell 写入工作区外测试文件;DSH 固定
|
||||
Host 已通过 Execute 工作区外写入、随后 Ask 拒绝同一写工具,未增加权限策略或人工审批。
|
||||
- 真实 OpenCode `1.18.29` 已完成原生 Task 后的 yes/no 与自由文本问答回传;父、子会话
|
||||
提问及后台任务不能交互时的失败路径均有回归测试。Continue 真实 `AskQuestion` 暴露了
|
||||
QuizService 扁平数据结构与原适配器不一致的问题,现已按锁定版本修正;真实自由文本
|
||||
回传、yes/no、选项及跳过均验证通过,自定义答案不再误标为选项。
|
||||
- DSH 活跃 Execute 的 ACP 权限回调不再等待第二次 authorizer,Ask 继续拒绝;未添加
|
||||
通用业务问答工具或从终端日志猜测确认意图。完整范围见
|
||||
[Runtime 交互边界](./runtime-interactions.md)。
|
||||
- 当前源码 Linux x64 Agent 通过签名组包、生产安装、真实 Ask/Execute、原生子代理
|
||||
工作区外写入、重连同一 daemon 和 stop/bootstrap。实机回归同时定位并修复了握手
|
||||
Base64URL 响应误用 ID 校验及断流未结算等待;13 项 Unix 原生端点测试和连续 20 次
|
||||
握手通过。两轮真实模型调用合计 10 次,均 completed;本轮测试 Agent 已停止。
|
||||
- 最终完整回归使用 `npm test -- --reporter=json --outputFile=<临时报告路径>`,并通过
|
||||
`GOODBUDDY_TEST_OPENCODE_BINARY` 指向已验证版本的候选 OpenCode。结果为 338 个文件、
|
||||
3597 项通过、0 项失败、61 项按平台/手动条件跳过,报告 `success=true`、退出码 0;
|
||||
Windows 跳过的 Unix 端点场景另在 Linux Host 运行了上述 13 项原生测试。
|
||||
- 最终 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 和差异空白检查
|
||||
通过,相关 Markdown 相对链接有效。此前失败、暂停及控制台截断的回归记录由本次完整
|
||||
JSON 报告覆盖,不代表仍有测试失败。未执行系统休眠/唤醒或本地桌面生产构建、打包;
|
||||
后续发布仍需对应候选的 CI 构建和跨平台验证。
|
||||
|
||||
## 2026-09-08 OpenCode 复用与原生控件恢复
|
||||
|
||||
- `OpenCodeRuntime` 仅在没有活动请求时探测缓存的 embedded server;活动对话或
|
||||
Compact 期间直接复用服务,避免新会话的一次短探测失败关闭其他会话正在使用的进程。
|
||||
空闲探测失败时仍清除旧 client 和会话映射并重建;并发调用共享检查与重建。
|
||||
进程退出或服务关闭会中止绑定它的对话与 Compact,事件流重连等待也可立即取消,
|
||||
请求明确报错,不自动重放已执行命令。本次修复后的真实模型验证共 6 次:正常并发、
|
||||
已复现场景下的共享服务保护均正常完成;主动结束测试进程后旧请求明确失败,新请求
|
||||
在替换服务上完成。此前“新会话完成、旧会话持续等待”的故障不再出现。
|
||||
- 请求级 MCP 注册与断开通过实例内 `mutateMcp` 队列串行执行。仅实际发出 `mcp.add`
|
||||
的名称进入清理列表;注册尚在排队时取消不追加断开。断开的 1 秒超时从出队后开始。
|
||||
- `App` 按本机/远程、连接方式、项目和 Runtime 选择组成 scope 缓存原生清单,用请求
|
||||
代次阻止旧 scope 结果回写;同 scope 会话刷新不清空重取,切换会话重置消息级选择,
|
||||
Runtime 切换状态也按 generation 隔离。清单请求失败或返回 `unavailable`/`partial`
|
||||
时保留已有可用缓存并有界重试;没有可用缓存时保留新返回的部分数据,因此
|
||||
`unavailable` 重试为可用 `partial` 后可显示 Agent 和 Command。
|
||||
|
||||
验证记录:首轮定向测试 273 项通过、5 项跳过;补充修复后的 Runtime 与生命周期测试
|
||||
77 项通过,界面定向测试 6 项通过,类型检查通过。修复前的 lint 检查通过。
|
||||
全量测试超过 120 秒被终止,期间出现一项 DS 本地 stdio MCP 测试失败,尚待定位。
|
||||
这是当次历史结果。后续发布前修复与实机验证见本文上方及
|
||||
[远程主机技术设计](../remote-host/technical-design.md);真实系统休眠恢复仍未完成验证。
|
||||
|
||||
## 2026-09-06 项目工作区
|
||||
|
||||
范围见 [PRD 工作区](./prd.md#76-工作区)。
|
||||
|
||||
当前源码已移除重复标题与底部完整 Diff 入口,使用彩色 Git 状态字母,点击更改项读取
|
||||
单文件 Diff。已暂存与未暂存差异分开展示;本机 Git 测试覆盖无首次提交、未跟踪、
|
||||
暂存后继续修改、重命名、删除及包含方括号的路径。Git 刷新驱动目录树重新读取,保留
|
||||
展开状态;SSH 项目不提供本机系统打开按钮。审查后补齐了当前 Diff 随刷新重读、
|
||||
旧请求结果失效,以及超过 50 项变更的继续加载入口。
|
||||
|
||||
验证记录:
|
||||
|
||||
- 最终 `npm run typecheck`、`npm run lint`、`npm run build` 均通过。
|
||||
- 工作区专项 7 个测试文件复跑:317 项通过,3 项 Linux 专用测试在 Windows 跳过。
|
||||
任务完成测试同时检查目录树出现新文件。
|
||||
- 最终 `npm test`:326 个测试文件、3552 项测试通过,9 个文件、58 项测试按既有条件
|
||||
跳过。复核首轮曾有一项浏览器地址草稿测试超时,单独复跑及最终全量复跑均通过;
|
||||
更早的超时/失败记录不再作为最终验收结果。
|
||||
- 后续复核已通过既有凭据和固定 Host Key 经 VPN 连接共享 Linux x64 Host,使用当前
|
||||
源码构建的隔离 Agent 包安装并启动。真实 `RemoteWorkspaceAccess → Agent git/status`
|
||||
与 `git/diff` 路径覆盖暂存后继续修改、删除、重命名、未跟踪和方括号路径;均通过。
|
||||
Git 验证未调用模型。
|
||||
- Windows 隔离桌面实例已通过真实点击验收:当前项目单文件暂存/工作树 Diff 分组与
|
||||
增删颜色正确,修改测试文件后点击刷新可更新正在查看的 Diff,53 项变更经加载更多
|
||||
全部可访问。截图留在本地验证目录,不纳入产品包。
|
||||
|
||||
目录分页、任意目录切换和 HTML 预览不在本轮改动范围内。
|
||||
@@ -0,0 +1,61 @@
|
||||
# Runtime 交互边界
|
||||
|
||||
本文定义当前原生交互的适配范围,不增加工作模式或第二层 Execute 审批。
|
||||
|
||||
| Runtime | 权限确认 | 业务问答 |
|
||||
| --- | --- | --- |
|
||||
| 本机 GoodBuddy 管理的 OpenCode | 内部默认配置允许权限;每个 Ask 请求仍使用 deny-all 与显式只读能力。Execute 自动回复属于当前请求的父、子孙会话权限事件,不处理其他并行请求 | 通过 `question.asked` 转交父、子孙会话问题,支持原生单选、多选、yes/no、自由文本及跳过;使用公开 ID 回传 `question.reply/reject` |
|
||||
| Continue | Execute 启动传入 `--auto`,剩余原生权限请求按当前模式自动答复 | 对接 `1.5.47` QuizService 的扁平 `pendingQuestion`,其中 `question` 是字符串。选项回答与自定义文本分别回传正确的 `isCustomAnswer`;跳过发送拒绝回答的文本 |
|
||||
| DeepSeek Harness | 固定 Host 的 Execute 文件与命令工具不询问目录权限;若收到 ACP 权限请求,活跃 Execute 直接选一次性允许,缺少该选项时使用 Runtime 提供的允许选项,Ask 拒绝,不等待额外 authorizer | 固定 Host 当前未提供通用原生业务问答服务,不把普通模型正文误识别成待答协议。模型以普通回复提问时,用户通过下一条消息回答 |
|
||||
|
||||
托管 SSH OpenCode 的 ACP 权限处理与直接启动配置由
|
||||
[远程主机技术设计](../remote-host/technical-design.md)定义;本机 SDK 问答适配不代表远程
|
||||
ACP 已提供相同业务问答接口。
|
||||
|
||||
## 运行与失败
|
||||
|
||||
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
|
||||
- 前台问题停留在现有问答卡片,保留回答、提交失败提示与跳过入口。
|
||||
- 回答或跳过成功后,会话保留只读的“问题与回答”,显示原问题及对应的选项答案、
|
||||
自定义文本或“已跳过”。同一任务的多轮问答逐次保留,随本地会话保存,重新打开后仍可查看。
|
||||
提交失败时保留输入与重试入口,不生成已回答记录;取消未回答的问题不记为主动跳过。
|
||||
- 提交响应晚于任务结束时仍保存已提交的问答,但不覆盖完成或失败状态;晚于下一轮提问时
|
||||
不清除新的待答卡片。旧版会话未保存的问题与答案无法从“已提交”状态文案恢复。
|
||||
- 后台任务收到结构化提问时,通过现有 Main 路径明确失败并提示改为前台对话,不永久等待。
|
||||
- 请求结束或取消后清除对应待答映射;过期回答明确报错,不转交给后来的请求。
|
||||
- 任意终端程序的密码、OAuth 登录、许可证确认或 stdin 提示不自动回复 `yes`。这类程序应
|
||||
使用其非交互参数,或由用户在终端完成;不能把权限确认和业务决策混为一谈。Shell
|
||||
工具保留其现有取消与超时,不新增日志解析或通用交互代理。
|
||||
|
||||
## 托管 Runtime 的并行与输出
|
||||
|
||||
- 本机和 Agent 管理的 OpenCode 启动配置都显式设置 `snapshot: false`。GoodBuddy 不消费
|
||||
原生自动 Git 快照、快照差异摘要或快照撤销;关闭这条内部路径,避免大行级差异的同步
|
||||
计算阻塞同一原生进程中的其他会话。
|
||||
- 文件工具、Shell、原生子代理、工作区 Git 状态和单文件差异保持可用;不新增全局会话
|
||||
队列、请求重放或总执行截止时间。外部 OpenCode Server 的配置不由 GoodBuddy 改写,
|
||||
已运行的旧原生进程也不会热更新配置。
|
||||
- Continue 使用 HTTP 传递响应和状态;Electron UtilityProcess 启动后持续排空不用展示的
|
||||
stdout,避免控制台输出填满管道后阻塞宿主。stderr 的有界错误处理、请求取消与进程
|
||||
回收保持不变,不把诊断输出复制进聊天或持久日志。
|
||||
- 远端最终启动配置由[远程主机技术设计](../remote-host/technical-design.md)定义,
|
||||
模型桥生成最终配置时同样保留上述快照策略。
|
||||
|
||||
## 消息底部请求状态
|
||||
|
||||
- 状态属于当前请求,不与顶部 Runtime 的连接中、就绪、不可用混为一谈。提交后先显示
|
||||
“正在准备请求”;没有更具体的活动事件时显示“等待后续进展”,不声称模型正在思考。
|
||||
- 本机 OpenCode 只把当前父会话的 `session.status.retry` 映射为等待重试,显示原生
|
||||
`attempt`、计划重试时间和有界脱敏原因;后续 `busy` 才显示已开始重试。其他会话的
|
||||
状态不串入当前消息,不猜测总重试次数,不把 SDK 事件流重连当作模型重试。
|
||||
- 显示优先级为待回答、待确认、进行中的上下文压缩、明确的请求状态、待执行或执行中的
|
||||
工具、未完成的子任务、等待后续进展。压缩沿用已有活动块,不重复增加底部提示。
|
||||
正文、推理或工具进展会清除旧状态;子任务更新不覆盖父请求的重试提示。
|
||||
- 完成、失败、取消后保留真实终态,迟到的状态事件不能把请求恢复成处理中。底部状态点
|
||||
为静态,不凭请求尚未结束暗示仍有进展;会话列表闪点和发送、停止按钮保持原有行为。
|
||||
- Continue、DeepSeek Harness 和远端 ACP 保留其已接入的处理与工具事件,不伪造未接入
|
||||
的原生重试状态。直连模型的重试等待与请求开始文案见
|
||||
[直连模型 UI 设计](../direct-model-agent/ui-design.md#6-错误与通知);本次不改变任何
|
||||
Runtime 的重试上限、请求重放或取消策略。
|
||||
|
||||
实现与真实 Runtime 验证证据见[进度记录](./progress.md)。
|
||||
@@ -0,0 +1,658 @@
|
||||
# 助手工作栏多终端页签 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台验证中 |
|
||||
| 版本 | 1.2 |
|
||||
| 日期 | 2026-09-10 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 界面归属 | [通用助手工作栏与执行空间](./prd.md) |
|
||||
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
| 远程架构 | [SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md)、[SSH Host 远程环境准备与控制面直连设计](../remote-host/environment-provisioning-technical-design.md) |
|
||||
|
||||
## 1. 文档职责
|
||||
|
||||
本文细化助手工作栏中的以下行为:
|
||||
|
||||
- 已打开应用以同级 Tab 呈现。
|
||||
- Tab 行始终提供“+”入口,用户按需打开应用。
|
||||
- 应用通过统一注册表分别声明单实例或多实例、默认打开、可关闭和排序策略。
|
||||
- “终端”作为首个多实例应用,自动使用当前项目的本机或托管 SSH 执行空间。
|
||||
- 本机与远程交互式终端的生命周期、状态、性能和验收要求。
|
||||
|
||||
[通用助手工作栏与执行空间 PRD](./prd.md)
|
||||
继续负责工作栏的应用级位置、稳定能力目录、执行空间、范围绑定和总体产品边界。本文不重新
|
||||
定义 Host Key、凭据、Agent 安装、Ask/Execute Runtime 或项目持久化契约。
|
||||
|
||||
## 2. 背景与当前问题
|
||||
|
||||
当前工作栏在聊天页面右侧固定显示“任务中心、工作区、浏览器、成果”四个 Tab。用户不能:
|
||||
|
||||
- 通过统一入口按需打开新的工作栏应用。
|
||||
- 按应用策略关闭并重新打开可关闭的工作栏应用。
|
||||
- 为支持多实例的应用创建第二个独立实例。
|
||||
- 在 GoodBuddy 内使用本机或远程 Host 的交互式 Shell。
|
||||
|
||||
当前代码已经具备工作栏右侧回流、宽度调整、Tab 键盘导航、SSH Host Key 固定、加密凭据和
|
||||
SSH 连接池,但尚不存在工作栏应用实例模型、终端 IPC、终端 Session Manager 或终端模拟器。
|
||||
`node-pty` 当前只作为其他 Runtime 的间接依赖被打包,不能视为已经存在用户终端能力。
|
||||
|
||||
## 3. 产品定义
|
||||
|
||||
### 3.1 工作栏应用
|
||||
|
||||
工作栏应用是可以在助手工作栏中打开的一个能力,例如任务中心、工作区、浏览器、成果或
|
||||
终端。每项应用通过统一注册表声明:
|
||||
|
||||
- 稳定应用 ID。
|
||||
- 用户可见名称、图标和说明。
|
||||
- `single` 或 `multiple` 实例策略。
|
||||
- 是否默认打开、是否必须存在、是否可关闭和是否允许排序;这些属性彼此独立。
|
||||
- 创建实例时可用的默认上下文。
|
||||
- 当前不可用时的原因和修复入口。
|
||||
|
||||
目录中的应用可发现性与实例是否已经打开相互独立。应用不可用时保留入口并说明原因,不以
|
||||
隐藏代替状态。
|
||||
|
||||
注册表字段如下:
|
||||
|
||||
| 字段 | 类型或允许值 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `id` | 稳定应用 ID | 关联图标、面板实现和持久化实例 |
|
||||
| `label` | 有界文本 | 默认 Tab 名称和目录名称 |
|
||||
| `icon` | 稳定图标 ID | 应用目录和 Tab 图标 |
|
||||
| `description` | 有界文本 | 目录说明和完整提示 |
|
||||
| `instancePolicy` | `single`、`multiple` | 同一应用允许一个还是多个实例 |
|
||||
| `defaultContext` | `application`、`current-project`、`current-conversation` | 创建实例时使用的默认范围,不代表实例随后必须跟随 |
|
||||
| `visibleAcrossContextSwitches` | boolean | 切换项目或会话后,已打开的 Tab 是否继续留在工作栏中 |
|
||||
| `defaultOpen` | boolean | 新布局是否创建默认实例 |
|
||||
| `required` | boolean | 布局加载时实例缺失是否必须补回 |
|
||||
| `closable` | boolean | 是否渲染关闭入口并接受关闭命令 |
|
||||
| `reorderable` | boolean | 是否允许用户改变 Tab 顺序 |
|
||||
| `availability` | `available` 或带原因和修复入口的 `unavailable` | 当前能力状态 |
|
||||
|
||||
`defaultOpen`、`required` 和 `closable` 不互相推导。例如浏览器默认打开但不是必需实例;任务
|
||||
中心和工作区既默认打开又必须存在。`required` 不表示锁定顺序,顺序只由 `reorderable` 控制。
|
||||
|
||||
当前五个应用的 `visibleAcrossContextSwitches` 均为 `true`。该属性记录应用目录的显示策略,
|
||||
不改变内容归属,也不授予其他会话的 MCP 访问权限;当前没有使用 `false` 的应用,尚未实现
|
||||
按上下文隐藏 Tab 的策略。
|
||||
|
||||
浏览器在创建时绑定当前 Conversation;切换会话后仍保留原 Tab、选中状态和页面,导航操作
|
||||
继续使用原 `targetRef.conversationId`。此时新建浏览器才绑定新会话。默认或旧布局中尚未绑定
|
||||
的浏览器在首次使用时绑定活动会话,此后不再随会话切换改绑。终端同样保留创建时执行目标;
|
||||
任务中心、工作区和成果的内容继续跟随当前项目。
|
||||
|
||||
### 3.2 工作栏 Tab 实例
|
||||
|
||||
工作栏 Tab 是应用的一次打开实例。每个实例具有独立 ID、标题、应用类型和状态。Tab 行只
|
||||
显示已经打开的实例,不把全部能力同时渲染为 Tab。
|
||||
|
||||
实例持久化字段只有 `id`、`appId`、`title` 和可选 `targetRef`。`targetRef` 使用公开引用:
|
||||
终端接受 `local` 或 `projectId`,浏览器接受 `conversationId`。Main 签发的 Browser Tab ID、
|
||||
Terminal Session ID、元素引用、凭据、路径和运行状态不进入工作栏布局。
|
||||
|
||||
### 3.3 终端实例
|
||||
|
||||
每个终端 Tab 对应一个独立交互式 Shell Session:
|
||||
|
||||
- 本机终端由桌面 Main 进程管理本机 PTY。
|
||||
- 远程终端通过现有、已验证的 SSH Host 打开直接 SSH PTY。
|
||||
- 不同终端拥有独立 Shell、工作目录、输入、输出、尺寸和结束状态。
|
||||
- 创建后固定到当时选择的执行位置,切换项目不会迁移已经运行的终端。
|
||||
|
||||
直接 SSH PTY 是用户操作的远程终端,不是连接期临时 GoodBuddy Agent,也不作为 Runtime
|
||||
stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或桌面重启的进程恢复。
|
||||
|
||||
## 4. 已确认的产品决策
|
||||
|
||||
1. 工作栏 Tab 行末尾始终显示“+”按钮。
|
||||
2. “+”是普通命令按钮,不是一个 `tab`。
|
||||
3. 点击“+”在工作栏内容区显示“新建工作栏应用”选择页;取消后返回原活动 Tab。
|
||||
4. 选择应用后创建或激活实例,并关闭临时选择页。
|
||||
5. 单实例应用已经打开时,再次选择只聚焦已有实例,不创建重复 Tab。
|
||||
6. 多实例应用每次选择都创建新实例。
|
||||
7. 终端从首期开始就是多实例应用。
|
||||
8. 任务中心和工作区保持单实例、默认打开且不可关闭,分别固定在第一、第二位,`reorderable` 为 `false`。恢复旧布局时也按此规则修正顺序,其余 Tab 保持相对顺序。
|
||||
9. 浏览器注册为可关闭的多实例应用,升级后保留一个默认实例;成果保持单实例、默认打开且
|
||||
可关闭。
|
||||
10. 用户关闭可关闭实例、切换或重排应用 Tab 后,“+”仍保持可见。
|
||||
11. 选择“终端”后不再显示第二层目标选择器。存在当前项目时直接绑定该项目,由 Main
|
||||
根据项目执行空间自动打开本机或托管 SSH Shell;没有当前项目时打开本机 Home Shell。
|
||||
12. 远程首期使用直接 SSH PTY。SSH 断线后终端进入“已中断”,重新连接会创建一个新的
|
||||
Shell,不伪装为恢复旧进程。
|
||||
13. 工作栏收起、切换主页面或切换 Tab 不终止终端;显式关闭终端 Tab 才终止对应 Shell。
|
||||
14. 应用重启不自动启动 Shell。恢复的终端 Tab 显示“会话已结束”,由用户显式重新连接。
|
||||
15. 终端是用户交互表面。Ask/Execute 继续约束 Agent Runtime,不把用户键盘操作误判为
|
||||
Agent 工具调用;Agent、自动化和模型不得未经明确用户操作向终端注入输入。
|
||||
16. 工作栏继续按上位 PRD 提升为应用级容器;切换聊天、知识、魔法笔记或活动记录等主页面
|
||||
不销毁 Tab 实例或终端 Session。
|
||||
|
||||
## 5. 目标
|
||||
|
||||
### 5.1 用户目标
|
||||
|
||||
- 在工作栏中随时通过“+”发现并打开应用。
|
||||
- 同时打开多个本机或远程终端,并通过 Tab 快速切换。
|
||||
- 清楚知道每个终端运行在本机还是哪个远程 Host、当前目录和连接状态。
|
||||
- 在终端断线、退出或目标失效后获得准确状态和明确恢复操作。
|
||||
- 使用键盘完成打开应用、切换 Tab、操作终端和关闭实例。
|
||||
|
||||
### 5.2 产品目标
|
||||
|
||||
- 用一个可扩展的应用注册表统一单实例与多实例行为。
|
||||
- 把现有 Tab 迁移到统一实例模型,同时保持任务中心和工作区始终存在,并允许浏览器和终端
|
||||
创建多个实例。
|
||||
- 建立真实的 Main/Preload/Renderer 终端生产链路,而不是用普通文本框模拟终端。
|
||||
- 复用现有 SSH Host Key、凭据和连接池,不建立第二套 Host 管理。
|
||||
- 保持终端输入输出有序、响应流畅、内存有界,并可靠清理本机和远程资源。
|
||||
|
||||
## 6. 非目标
|
||||
|
||||
- 不把 GoodBuddy 改造成完整 IDE。
|
||||
- 不唤起 Windows Terminal、Terminal.app 等外部终端窗口代替内嵌终端。
|
||||
- 不扫描或导入用户全部 SSH config、目录、Shell profile 或命令历史。
|
||||
- 不支持任意 `ProxyCommand`、端口转发或 SSH Agent Forwarding。
|
||||
- 不在打开远程终端时安装、更新或修复 GoodBuddy Agent。
|
||||
- 不把 Agent 工具调用、Runtime 日志或自动化输出伪装成用户终端。
|
||||
- 不允许 Renderer 直接访问 PTY、ChildProcess、SSH Client、凭据或 Node API。
|
||||
- 首期不提供终端协作、录制、云同步、Shell Session 复用、tmux 管理或跨设备恢复。
|
||||
- 首期不实现底部停靠和独立窗口;本文保留兼容模型,后续按工作栏总 PRD 扩展。
|
||||
- 首期不通过 GoodBuddy Agent 恢复远程终端,也不为终端复制 Runtime transcript、
|
||||
`outcome_unknown` 或模型调用恢复协议。
|
||||
|
||||
## 7. 信息架构与“+”交互
|
||||
|
||||
### 7.1 Tab 行
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 任务中心 │ 工作区 │ 终端 1 │ 终端 2 │ + │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ 当前活动实例内容 │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 已打开实例位于一个语义化 `tablist` 中。
|
||||
- 每个实例使用独立 `tab` 和 `tabpanel`。
|
||||
- “+”固定在 Tab 行末尾,但位于 `tablist` 之外。
|
||||
- Tab 过多时实例区域单行横向滚动,“+”保持可见;不换行、不自动隐藏实例。
|
||||
- 终端 Tab 默认使用递增标题,例如“终端 1”“终端 2”;状态栏持续显示项目、Host、
|
||||
工作目录和 Shell。用户可以重命名终端。
|
||||
- 当前 Tab 同时使用连续内容表面、选中边框、顶部强调线、图标和较高字重,不能只靠文字颜色。
|
||||
- Tab、关闭按钮和“+”入口沿用共享控件圆角;终端面板外框和终端顶部工具栏按钮使用直角,
|
||||
保持紧凑的停靠式终端界面。
|
||||
- 只有可关闭实例渲染 Tab 关闭按钮。关闭按钮默认不争夺视觉注意力,仅在鼠标悬停或键盘
|
||||
焦点进入对应 Tab 时出现;
|
||||
粗指针或无悬停设备始终显示,出现与隐藏不得引发布局位移。
|
||||
|
||||
### 7.2 新建应用选择页
|
||||
|
||||
点击“+”后:
|
||||
|
||||
1. 保留当前实例及其运行状态。
|
||||
2. 在工作栏内容区显示“新建工作栏应用”。
|
||||
3. 列出稳定应用目录,每项显示名称、说明、单/多实例提示和当前可用状态。
|
||||
4. 初始焦点进入第一个可用应用;Escape 或“取消”返回原活动实例和“+”按钮。
|
||||
5. 选择单实例应用时,若实例已存在,直接聚焦该 Tab;任务中心和工作区始终走此路径。
|
||||
6. 选择浏览器时创建绑定当前 Conversation 的新 Browser Tab;没有活动 Conversation 时创建
|
||||
待选择目标的空实例。
|
||||
7. 选择终端时直接解析当前项目执行空间,创建新 Tab 并聚焦终端输入,不再要求用户
|
||||
重复选择本机或远程目标。
|
||||
|
||||
选择页是“+”触发的临时界面,不进入实例顺序,不持久化,也不占用一个普通 Tab。
|
||||
|
||||
### 7.3 首期应用注册表
|
||||
|
||||
| 应用 | 实例策略 | 默认上下文 | 默认打开 | 必须存在 | 可关闭 | 可排序 | 再次从“+”选择 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 任务中心 | `single` | 当前项目 | 是 | 是 | 否 | 否 | 聚焦已有 Tab |
|
||||
| 工作区 | `single` | 当前项目 | 是 | 是 | 否 | 否 | 聚焦已有 Tab |
|
||||
| 浏览器 | `multiple` | 当前 Conversation | 是,一个实例 | 否 | 是 | 是 | 每次创建新 Tab |
|
||||
| 成果 | `single` | 当前项目 | 是 | 否 | 是 | 是 | 聚焦已有 Tab |
|
||||
| 终端 | `multiple` | 当前项目 | 否 | 否 | 是 | 是 | 每次创建新 Tab |
|
||||
|
||||
实例策略、默认状态和可关闭策略都是应用注册数据,不能分散硬编码在多个点击处理器中。
|
||||
|
||||
新建浏览器使用独立编号标题,避开已有名称;关闭后再次新建也不产生重名。恢复布局保留已保存
|
||||
的标题。会话名称用于标识归属,不替代浏览器实例名称。新建页面为空白页,同会话 Cookie 共享
|
||||
不表示复用旧页面或导航历史。
|
||||
|
||||
### 7.4 Tab 操作
|
||||
|
||||
- 点击或使用方向键切换实例不会暂停或终止后台实例。
|
||||
- 支持左右方向键、Home、End 和 roving `tabindex`。
|
||||
- 任务中心和工作区没有关闭按钮,也不响应关闭快捷命令。
|
||||
- 关闭可关闭的非活动 Tab 后焦点保持在当前活动 Tab。
|
||||
- 关闭可关闭的活动 Tab 后激活右侧相邻实例;没有右侧实例时激活左侧实例。任务中心和工作区
|
||||
始终存在,因此正常布局不会进入零实例空状态。
|
||||
- 关闭正在运行的终端需要确认“关闭终端将结束 Shell 及其子进程”;取消是初始焦点。
|
||||
- 已退出或已中断的终端可以直接关闭,不重复确认。
|
||||
|
||||
## 8. 终端创建流程
|
||||
|
||||
### 8.1 自动目标解析
|
||||
|
||||
- 当前项目是本地项目时,创建目标为公开 Project ID,Main 从权威项目记录解析本机根目录。
|
||||
- 当前项目是托管 SSH 项目时,创建目标仍为公开 Project ID,Main 再解析项目 Host、
|
||||
固定 Host Key、凭据和远端根目录。
|
||||
- 没有当前项目时使用本机目标和当前用户 Home。
|
||||
- Host 未验证、凭据不可用或不支持交互式 Shell 时,终端 Tab 就地显示失败原因和恢复入口,
|
||||
不静默切换到本机或其他 Host。
|
||||
|
||||
### 8.2 本机终端
|
||||
|
||||
- 当前项目是本地项目时,默认工作目录为项目根目录。
|
||||
- 没有本地项目目标时,默认工作目录为当前用户 Home。
|
||||
- Windows 优先使用可用的 PowerShell 7,其次 Windows PowerShell,最后使用系统
|
||||
`COMSPEC`。
|
||||
- macOS 和 Linux 优先使用当前用户有效的 `$SHELL`,不可用时使用平台常规 Shell fallback。
|
||||
- 首期自动选择默认 Shell,不提供任意可执行文件路径输入。
|
||||
- Main 只向 Shell 传递正常交互所需的筛选环境,不注入模型 API Key、GoodBuddy 内部
|
||||
Token 或其他产品凭据。
|
||||
|
||||
### 8.3 远程终端
|
||||
|
||||
- 当前项目是托管 SSH 项目时,自动使用项目 Host 和远端项目根目录。
|
||||
- Main 复用现有 Host store、完整 Host Key 固定、认证方式和 SSH connection pool。
|
||||
- 远端通过 SSH PTY 打开账号的登录 Shell,并支持输入、ANSI 输出和窗口尺寸同步。
|
||||
- 活动终端持有专用 Channel lease,不得被连接池空闲回收。
|
||||
- 不把密码、私钥、认证头或 GoodBuddy 模型凭据发送给 Renderer 或写入终端环境。
|
||||
- Host 地址、用户、凭据或 Host Key generation 变化时,旧 Session 进入“已中断”,新建
|
||||
Session 必须使用 Host store 中的当前身份。
|
||||
|
||||
### 8.4 创建结果
|
||||
|
||||
创建成功后:
|
||||
|
||||
- 新 Tab 插入当前活动 Tab 右侧并立即激活。
|
||||
- 焦点进入终端输入区域。
|
||||
- 状态栏显示“本机”或 Host 名称、工作目录、Shell 和连接状态。
|
||||
- 终端尺寸使用内容区计算出的首个有效 `cols/rows`,不是固定默认值覆盖实际布局。
|
||||
|
||||
创建失败时在终端 Tab 保留错误上下文并提供重试;不得留下不可回收 Session 或显示为已经连接。
|
||||
|
||||
## 9. 终端交互与状态
|
||||
|
||||
### 9.1 基础交互
|
||||
|
||||
首期支持:
|
||||
|
||||
- 真实 TTY 输入与 ANSI 输出。
|
||||
- 中文输入法、组合字符和常规键盘快捷键。
|
||||
- 复制、粘贴、全选、搜索、清屏和滚动。复制与粘贴通过桌面 Main 进程访问系统文本剪贴板,
|
||||
不依赖 Renderer 的浏览器剪贴板权限。
|
||||
- 工作栏宽度或窗口尺寸变化后的 PTY resize。
|
||||
- 终端重命名。
|
||||
- 显式关闭、退出后重新打开和远程中断后重新连接。
|
||||
|
||||
浏览器原生页面快捷键与终端按键冲突时,终端聚焦期间优先交给终端;应用级保留快捷键必须
|
||||
有明确规则和可访问说明。
|
||||
|
||||
### 9.2 状态模型
|
||||
|
||||
```ts
|
||||
type TerminalSessionState =
|
||||
| 'starting'
|
||||
| 'running'
|
||||
| 'exited'
|
||||
| 'interrupted'
|
||||
| 'closing'
|
||||
| 'failed'
|
||||
```
|
||||
|
||||
- `starting`:正在创建本机 PTY 或 SSH Channel。
|
||||
- `running`:允许输入和 resize。
|
||||
- `exited`:Shell 正常或带退出码结束,显示退出码或信号。
|
||||
- `interrupted`:远程连接、Host identity 或桌面生命周期导致会话失效。
|
||||
- `closing`:已拒绝新输入,正在回收进程或 Channel。
|
||||
- `failed`:Session 未成功开始或发生不可恢复错误。
|
||||
|
||||
高频输出不逐行进入 live region。只播报连接成功、连接失败、已中断和 Shell 已退出等重要
|
||||
状态变化。
|
||||
|
||||
### 9.3 重新连接
|
||||
|
||||
- 本机或远程 Session 结束后,“重新打开”在同一 Tab 中创建全新的 Shell Session。
|
||||
- 远程断线后的按钮文案为“重新连接并新建 Shell”,不能暗示恢复旧进程。
|
||||
- 新 Session 使用 Host store 和项目中的当前有效配置,不复用旧 Host identity。
|
||||
- 重新连接前清空旧终端画面,并保留可见说明“这是新的 Shell 会话”。
|
||||
|
||||
## 10. 实例、Session 与持久化
|
||||
|
||||
工作栏实例与运行 Session 分离:
|
||||
|
||||
```ts
|
||||
type WorkbarAppDefinition = {
|
||||
id: 'tasks' | 'workspace' | 'browser' | 'results' | 'terminal'
|
||||
label: string
|
||||
icon: WorkbarAppDefinition['id']
|
||||
description: string
|
||||
instancePolicy: 'single' | 'multiple'
|
||||
defaultContext: 'application' | 'current-project' | 'current-conversation'
|
||||
defaultOpen: boolean
|
||||
required: boolean
|
||||
closable: boolean
|
||||
reorderable: boolean
|
||||
availability:
|
||||
| { state: 'available' }
|
||||
| { state: 'unavailable'; reason: string; remedy: string }
|
||||
}
|
||||
|
||||
type WorkbarTabInstance = {
|
||||
id: string
|
||||
appId: WorkbarAppDefinition['id']
|
||||
title: string
|
||||
targetRef?:
|
||||
| { type: 'local' }
|
||||
| { type: 'project'; projectId: string }
|
||||
| { type: 'conversation'; conversationId: string }
|
||||
}
|
||||
|
||||
type WorkbarLayoutPreferences = {
|
||||
instances: WorkbarTabInstance[]
|
||||
activeInstanceId: string | null
|
||||
expanded: boolean
|
||||
dock: 'right'
|
||||
widthRatio: number
|
||||
taskScope: 'current-project' | 'global' | 'all-projects'
|
||||
}
|
||||
```
|
||||
|
||||
- 持久化工作栏打开状态、Tab 顺序、活动实例、应用 ID、自定义标题、公开目标引用和宽度比例。
|
||||
- 加载布局时根据 `required` 补回缺失实例,并拒绝重复的单实例应用;应用策略不写入实例
|
||||
数据,避免持久化状态覆盖注册表。
|
||||
- `reorderable` 已进入共享契约;当前 Tab 壳层尚未提供拖放或键盘重排,不能把声明属性视为
|
||||
已完成的排序功能。
|
||||
- 不持久化终端正文、未执行输入、凭据、环境变量、PTY PID、SSH Channel 或派生 Host
|
||||
identity。
|
||||
- Main 创建终端后返回新的不透明 Session ID;Renderer 不能选择 PID 或 Channel。
|
||||
- 应用重启恢复终端 Tab 描述,但状态为“会话已结束”,不自动建立 SSH 连接或启动本机
|
||||
Shell。
|
||||
- 运行期间切换项目不会改变已经创建的终端目标。用户需要新目标时通过“+”创建新终端。
|
||||
|
||||
## 11. 生命周期与关闭语义
|
||||
|
||||
| 用户或系统动作 | Tab | Shell Session |
|
||||
| --- | --- | --- |
|
||||
| 切换 Tab | 保留 | 继续运行 |
|
||||
| 收起工作栏 | 保留 | 继续运行,停止非必要高频 UI 工作 |
|
||||
| 切换主页面 | 保留 | 继续运行 |
|
||||
| 切换项目 | 保留并显示原目标 | 继续在原目标运行 |
|
||||
| 关闭运行中的终端 Tab 并确认 | 删除 | 结束 Shell 和子进程 |
|
||||
| Shell 自行退出 | 保留 | 标记 `exited` |
|
||||
| SSH 断线 | 保留 | 标记 `interrupted` |
|
||||
| 删除或修改目标 Host | 保留 | 关闭旧 Channel 并标记 `interrupted` |
|
||||
| Renderer 销毁或应用退出 | 下次恢复描述 | 在总关闭截止时间内回收 |
|
||||
|
||||
关闭操作必须安全处理竞态。Main 停止接受新输入,关闭 stdin/Channel,先正常结束,再在既有
|
||||
应用关闭截止时间内升级清理;一个卡死终端不能无限阻塞应用退出。关闭后的迟到 ACK 视为已
|
||||
完成,input、resize 或重复 close 返回 Session 不存在,不操作其他 Session。
|
||||
|
||||
## 12. 技术生产路径
|
||||
|
||||
### 12.1 Shared
|
||||
|
||||
新增独立终端契约,至少覆盖:
|
||||
|
||||
- `create`:目标引用、初始 `cols/rows`。
|
||||
- `write`:Session ID 和有界输入。
|
||||
- `resize`:Session ID、`cols/rows`。
|
||||
- `close`:Session ID。
|
||||
- `snapshot`:目标、状态、Shell、目录和终态。
|
||||
- `event`:带 Session ID 和单调序号的输出、状态、退出与错误事件。
|
||||
|
||||
所有 Schema 使用 Zod 严格校验,并限制 ID、标题、输入、尺寸、事件块、会话数和缓冲字节。
|
||||
|
||||
### 12.2 Main
|
||||
|
||||
新增统一 `TerminalSessionManager`:
|
||||
|
||||
- Session 按所属 `webContents` 和不透明 ID 隔离。
|
||||
- 将现有间接 `node-pty` 提升为 GoodBuddy 直接生产依赖,并用于启动平台 Shell。
|
||||
- 远程使用现有 `SshConnectionPool` 打开交互式 PTY Channel。
|
||||
- 统一处理 write、resize、close、exit、连接中断和应用退出。
|
||||
- 终端输出只发送给所属窗口。
|
||||
- 复用现有子进程树终止思路,但针对 PTY 验证 Windows 与 POSIX 的实际回收行为。
|
||||
|
||||
### 12.3 Preload 与 IPC
|
||||
|
||||
- `DesktopApi` 只暴露显式 `terminal.create/write/resize/close/getSnapshot/onEvent`。
|
||||
- Main Handler 对每个调用执行可信 sender 检查和共享 Schema parse。
|
||||
- Preload 管理事件订阅释放,不暴露 `ipcRenderer`、PTY、SSH Client 或 Node API。
|
||||
|
||||
### 12.4 Renderer
|
||||
|
||||
- 使用 `@xterm/xterm` 6 和官方 fit/search addon 处理 ANSI、光标、选择、IME、搜索、
|
||||
滚动和可访问输入,不自行实现终端转义状态机。
|
||||
- 终端首次打开时按需加载;支持 WebGL2 时使用官方 WebGL renderer,初始化失败或 Context
|
||||
丢失时释放 WebGL addon 并回退到 xterm 默认 renderer。
|
||||
- xterm 使用解析后的跨平台系统等宽字体栈,不能把未解析的 CSS `var(...)` 交给 Canvas。
|
||||
- Tab 壳层只管理实例与焦点,不持有 PTY 或 SSH 对象。
|
||||
- `ResizeObserver` 只在有效非零布局后发送去重后的 `cols/rows`。
|
||||
- 组件卸载时退订画面事件;只有显式关闭终端才调用 Session close。
|
||||
- 终端样式复用全局语义颜色、边框、焦点和字体令牌,不创建另一套工作台设计系统。
|
||||
|
||||
### 12.5 输出顺序与背压
|
||||
|
||||
- 输出按 Session 分区并携带单调序号。
|
||||
- Main 合并小块输出,限制单事件大小、待发送条目和总字节。
|
||||
- Renderer 确认已经消费的序号;慢消费者达到上限时暂停 PTY/SSH stream,消费后恢复。
|
||||
- 不通过丢弃中间输出维持界面响应,因为丢失字节会破坏终端状态。
|
||||
- Renderer 只保留有界滚动历史;清屏不改变 Main Session 生命周期。
|
||||
|
||||
## 13. 权限与边界
|
||||
|
||||
- 用户终端以当前本机账号或所选 SSH 账号的正常权限运行,不增加额外虚构的 trust tier。
|
||||
- Ask/Execute 是 Agent Runtime 模式,不自动改变用户直接操作终端的 OS 权限。
|
||||
- UI 必须持续将其标注为“用户终端”,避免被理解为 Ask Runtime 的只读输出。
|
||||
- Agent、模型、监督器和自动化不能调用终端 write,也不能把指令注入已有 Session。
|
||||
- 终端输出不会自动进入对话上下文、模型 Prompt、成果或活动记录。
|
||||
- Renderer 只能引用项目和 Session 的公开 ID;Main 从权威 store 解析目录、Host、凭据和
|
||||
当前 Host identity。
|
||||
- 不接受 Renderer 提交任意 Shell 可执行路径、SSH 命令、PID、环境变量集合或原始凭据。
|
||||
- 保持 Electron `contextIsolation`、sandbox 和禁用 Renderer Node integration。
|
||||
|
||||
## 14. 错误与反馈
|
||||
|
||||
- 创建失败、断线、Shell 退出和目标失效保留在终端面板,提供就地重试。
|
||||
- 不与全局通知重复显示同一终端错误。
|
||||
- 非局部错误,例如无法保存工作栏布局,可以进入应用通知。
|
||||
- 错误文案说明发生了什么、当前 Shell 是否仍在运行、用户可以做什么。
|
||||
- Host Key 变化沿用现有 Host 管理流程;终端只显示“主机身份已变化,请重新验证”,不在
|
||||
终端内复制凭据或 Host Key 修改流程。
|
||||
- 目标不可用时保留 Tab 名称和目标,不静默切换到本机或其他 Host。
|
||||
|
||||
## 15. 性能与资源要求
|
||||
|
||||
- 工作栏切换和终端输入在正常负载下保持即时反馈,持续输出不能阻塞主界面导航。
|
||||
- 同一窗口的活动终端数量必须有明确上限;达到上限时阻止新建并说明先关闭不用的终端。
|
||||
- 每个 Session 的输出队列、Renderer 滚动历史和 IPC 单事件均有硬上限。
|
||||
- 工作栏不可见时可以降低渲染刷新频率,但不能丢失终端协议字节或暂停 Shell 本身。
|
||||
- 应用退出复用现有总清理截止时间,不因终端新增无界等待。
|
||||
- 生产构建必须实际加载本机 PTY 原生模块;不能只验证文件存在。
|
||||
- 远程终端不能使 SSH connection pool 的空闲项、Channel 或监听器泄漏。
|
||||
|
||||
具体数值在共享契约和性能测试中确定,以现有 IPC、窗口和关闭预算为上限,不在多个进程中
|
||||
维护不同常量。
|
||||
|
||||
## 16. 无障碍与响应式
|
||||
|
||||
- Tab 使用 `tablist/tab/tabpanel`;“+”使用带可访问名称“打开工作栏应用”的普通按钮。
|
||||
- 应用选择页使用可键盘操作的列表或菜单语义,不使用 `SegmentedControl`;终端没有重复的
|
||||
目标选择页。
|
||||
- 每个终端 Tab 的可访问名称包含自定义名称或位置,多个终端不能都只读作“终端”。
|
||||
- Tab 关闭后焦点移动到确定的相邻 Tab;取消创建后焦点返回“+”。
|
||||
- 工作栏收起后焦点返回唯一工作栏开关,不增加焦点陷阱或遮罩。
|
||||
- 终端高频输出不逐行播报;只播报连接、退出、失败和中断。
|
||||
- 200% 文字缩放时,目标、Shell、目录、状态和关闭操作仍可访问。
|
||||
- Tab 单行横向滚动,终端内容随工作栏尺寸变化并同步 PTY,不把 Tab 换成下拉框。
|
||||
- 深色和浅色主题都使用设计系统语义令牌,并保持终端文本、选择和光标可辨。
|
||||
|
||||
## 17. 实施顺序与多代理工作包
|
||||
|
||||
功能必须以完整的本机和远程用户流程交付。以下是可并行的开发工作包,不表示可以发布只有
|
||||
Schema、假数据终端或不可用入口的中间产品。
|
||||
|
||||
### 17.1 依赖顺序
|
||||
|
||||
```text
|
||||
契约冻结
|
||||
├─ 工作栏实例壳层
|
||||
├─ 本机 PTY 后端
|
||||
├─ SSH PTY 后端
|
||||
└─ 终端 Renderer
|
||||
↓
|
||||
单一集成负责人完成 Main/Preload/App 接线
|
||||
↓
|
||||
完整验证、真实 Host 验收与文档同步
|
||||
```
|
||||
|
||||
### 17.2 文件所有权
|
||||
|
||||
| 工作包 | 主要职责 | 建议文件所有权 | 不应修改 |
|
||||
| --- | --- | --- | --- |
|
||||
| 契约与注册表 | Workbar/Terminal Schema、状态、测试 | 新建 `src/shared/workbar-contracts.ts`、`terminal-contracts.ts` 及测试 | `App.tsx`、`ipc.ts`、Preload |
|
||||
| 工作栏壳层 | 动态实例 Tab、“+”、选择页、单/多实例和焦点 | 新建工作栏组件及其测试 | PTY、SSH、Main IPC |
|
||||
| 本机终端后端 | 本机 PTY、输入、resize、退出和进程树清理 | 新建 `src/main/terminal/` 本机文件及测试 | Renderer、远程 Agent |
|
||||
| SSH 终端后端 | SSH PTY、lease、resize、断线和 Channel 清理 | 新建 `src/main/terminal/` 远程文件及聚焦测试 | 工作栏 UI、Agent 协议 |
|
||||
| 终端 Renderer | 模拟器、IME、搜索、复制粘贴、状态栏和 resize | 新建终端组件、样式和测试 | Main、SSH、中心 IPC |
|
||||
| 集成 | 依赖、IPC channel、DesktopApi、Preload、Main 注册、`App` 接线与关闭顺序 | `package.json`、lock、`ipc-channels.ts`、`contracts.ts`、`preload/index.ts`、`main/ipc.ts`、`App.tsx` | 不重写各包已验收内部逻辑 |
|
||||
| 验收与文档 | E2E、跨平台构建检查、真实 Host、最终文档 | 测试与相关文档 | 不用假实现替换生产路径 |
|
||||
|
||||
中心文件只能由集成负责人修改,避免多个代理同时编辑 `App.tsx`、`main/ipc.ts`、
|
||||
`shared/contracts.ts`、`preload/index.ts`、`package.json` 和 lockfile。
|
||||
|
||||
### 17.3 并行开发门槛
|
||||
|
||||
契约负责人先冻结:
|
||||
|
||||
- 应用注册和实例策略。
|
||||
- Terminal request、snapshot、event 和 error code。
|
||||
- Session 所有权和关闭语义。
|
||||
- 输出序号、ACK 与容量常量。
|
||||
|
||||
其他代理基于冻结契约工作。接口偏差由集成负责人统一修正,不由多个代理分别修改共享中心
|
||||
文件。
|
||||
|
||||
## 18. 验收标准
|
||||
|
||||
### 18.1 工作栏应用与实例
|
||||
|
||||
- [ ] Tab 行末始终显示“+”;任务中心和工作区始终存在,因此正常布局不存在零实例状态。
|
||||
- [ ] 点击“+”显示稳定应用目录,取消后返回原实例并恢复焦点。
|
||||
- [ ] 选择已打开单实例应用只聚焦已有 Tab。
|
||||
- [x] 任务中心和工作区默认存在且不可关闭,不渲染关闭按钮,也不响应关闭快捷命令。
|
||||
- [x] 连续选择浏览器会创建页面、导航状态和元素引用相互独立的浏览器 Tab。
|
||||
- [x] 连续选择终端会创建多个互相独立的终端 Tab。
|
||||
- [ ] 关闭可关闭实例、切换和重排实例后,“+”仍保持可用。
|
||||
- [ ] Tab 溢出时单行横向滚动,不隐藏“+”或把 Tab 换成下拉框。
|
||||
- [x] 关闭可关闭的活动 Tab 后激活正确的相邻 Tab 并恢复焦点。
|
||||
- [x] 当前 Tab 的背景、边框、顶部指示和字重清晰可辨;关闭按钮仅在悬停、焦点或无悬停
|
||||
设备上出现,且不引发布局位移。
|
||||
- [x] 升级后任务中心、工作区、一个浏览器实例和成果按默认布局打开;布局恢复会补回缺失的
|
||||
任务中心与工作区。
|
||||
- [ ] 在所有主要页面都能打开工作栏;切换主要页面不会销毁 Tab 实例或终端 Session。
|
||||
|
||||
### 18.2 本机终端
|
||||
|
||||
- [x] 从“+ → 终端”直接打开当前本机项目的真实交互式 Shell,工作目录为项目根。
|
||||
- [ ] 没有项目时从用户 Home 打开本机 Shell。
|
||||
- [ ] `pwd` / `Get-Location`、交互输入、ANSI 颜色、中文 IME、复制和粘贴工作正常。
|
||||
- [ ] 调整工作栏和窗口尺寸会更新 PTY `cols/rows`,全屏 TTY 程序正确重绘。
|
||||
- [ ] 多个本机终端的输入、输出、目录和退出状态互不串线。
|
||||
- [ ] Shell 有活动子进程时关闭 Tab,确认后回收完整进程树;取消后 Session 继续运行。
|
||||
- [ ] 退出后的 Tab 显示退出码,并可在同一 Tab 显式创建新 Shell。
|
||||
|
||||
### 18.3 远程终端
|
||||
|
||||
- [ ] 从“+ → 终端”直接为当前托管 SSH 项目打开真实 SSH PTY。
|
||||
- [ ] 终端持续显示 Host、远端目录、Shell 和连接状态。
|
||||
- [ ] 使用现有固定 Host Key 和已存凭据,Renderer、日志和命令参数不出现凭据。
|
||||
- [ ] 输入、ANSI、中文、复制粘贴和 resize 在真实 Host 上工作正常。
|
||||
- [ ] 活动远程终端不会被 SSH connection pool 空闲回收。
|
||||
- [ ] SSH 断线立即标记“已中断”并拒绝继续输入,不显示为仍在运行。
|
||||
- [ ] “重新连接并新建 Shell”明确创建新 Session,不宣称恢复旧远端进程。
|
||||
- [ ] Host 删除、地址/用户/凭据或 Host Key generation 变化会中断旧 Session;新 Session
|
||||
使用当前 Host identity。
|
||||
|
||||
### 18.4 生命周期、性能和边界
|
||||
|
||||
- [ ] 收起工作栏、切换主页面、切换 Tab 或切换项目不会终止已有终端。
|
||||
- [ ] 终端不会随项目切换静默迁移执行位置。
|
||||
- [ ] 应用重启只恢复终端 Tab 描述,不自动连接或启动 Shell。
|
||||
- [ ] 输出顺序在持续大输出下保持正确,Main 和 Renderer 内存保持有界,主界面仍可操作。
|
||||
- [ ] 慢 Renderer 触发暂停和恢复,不丢弃中间终端字节。
|
||||
- [ ] Renderer 不获得 raw PTY、ChildProcess、SSH Client、PID、环境变量或凭据。
|
||||
- [ ] Agent、模型和自动化不能向已有用户终端注入输入。
|
||||
- [ ] 窗口销毁和应用退出在既有关闭截止时间内回收本机进程、SSH Channel 和监听器。
|
||||
- [ ] 重复 close、迟到 input/resize 和退出竞态不会操作其他 Session 或导致进程泄漏。
|
||||
|
||||
### 18.5 设计、无障碍和跨平台
|
||||
|
||||
- [ ] 仅使用键盘可以打开应用、创建浏览器和终端、切换 Tab、关闭可关闭 Tab、复制、粘贴及
|
||||
重新连接。
|
||||
- [ ] Tab、“+”、选择页和关闭确认使用正确语义并有可见焦点。
|
||||
- [ ] 终端输出不逐行进入 live region,关键状态变化可被辅助技术感知。
|
||||
- [ ] 浅色、深色、200% 文字缩放和窄窗口下可以完成核心流程。
|
||||
- [ ] Windows、macOS、Linux x64 和 Linux arm64 的正式构建实际启动本机 PTY。
|
||||
- [ ] 远程链路在共享 Linux x64 真实 Host 上完成创建、输入、resize、断线、重连、关闭和
|
||||
应用退出验证;若 Host 不可达,远程终端不得标记为完成。
|
||||
|
||||
## 19. 验证计划
|
||||
|
||||
### 19.1 自动验证
|
||||
|
||||
- 共享 Zod Schema 和边界值测试。
|
||||
- 单/多实例、默认打开、可关闭和排序策略,以及 Tab 顺序、关闭焦点和“+”选择页组件测试。
|
||||
- 任务中心与工作区缺失时的布局修复、关闭命令拒绝和焦点保持测试。
|
||||
- 浏览器多实例的页面隔离、Conversation 匹配和快捷入口去重测试。
|
||||
- 本机 PTY Session Manager 的输入、resize、退出、幂等关闭和进程树测试。
|
||||
- SSH PTY 的 Host identity、lease、断线、resize 和关闭测试。
|
||||
- Main trusted sender、Session 所有权、IPC 输入边界和事件路由测试。
|
||||
- Terminal Renderer 的 IME、键盘、ResizeObserver、订阅释放和状态测试。
|
||||
- `npm test`
|
||||
- `npm run typecheck`
|
||||
- `npm run lint`
|
||||
- `npm run build`
|
||||
|
||||
### 19.2 真实路径验证
|
||||
|
||||
- 在当前开发平台通过产品 UI 完成本机终端全流程。
|
||||
- 在共享 Linux x64 Host 通过现有 Host 配置完成远程终端全流程。
|
||||
- 真实运行需要 TTY 的程序,验证 ANSI、光标和 resize,不只运行非交互命令。
|
||||
- 持续输出并主动减慢 Renderer 消费,验证背压和内存上限。
|
||||
- 分别在本机和远程启动带子进程的命令,关闭 Tab 和应用后核对无 GoodBuddy 所属遗留资源。
|
||||
- 生产构建在各受支持平台实际加载 PTY 原生模块,不以单元测试或包内文件存在代替。
|
||||
|
||||
### 19.3 当前开发验证记录
|
||||
|
||||
2026-09-01 已通过生产 `SshConnectionPool` 和 `createSshTerminalSession` 在共享 Linux x64
|
||||
Host 完成直接 SSH PTY 验证:使用已有固定 Host Key 和测试凭据,以远端项目目录启动登录
|
||||
Shell,验证交互输入、ANSI、UTF-8 中文、`80×24` 到 `101×37` resize、连接销毁后进入
|
||||
`interrupted`、重新连接创建新 Shell,以及 Session close 和 connection pool dispose
|
||||
回收远端 Shell。验证只创建一个
|
||||
`$HOME/.goodbuddy/terminal-smoke-<UUID>` 临时目录,结束后已删除并确认无遗留 Shell 进程。
|
||||
|
||||
同日使用最新生产构建完成工作栏视觉检查:活动 Tab 使用连续内容表面、顶部强调线和应用
|
||||
图标;关闭按钮默认不可见,悬停后以 overlay 显示且 Tab 宽度不变;横向滚动继续可用但不
|
||||
显示滚动条,“+”保持独立可见。
|
||||
|
||||
本记录不替代尚未勾选的完整产品 UI、剪贴板、macOS、Linux arm64 和各平台本机 PTY 验收。
|
||||
|
||||
本 PRD 不改变 Agent 协议,因此首期远程终端无需 Agent package 更新。若实现期间改为
|
||||
Agent-owned PTY,必须先修订本文的远程生命周期和恢复语义,并按 Agent 开发规则在共享
|
||||
Linux x64 Host 验证当前源码或候选包。
|
||||
|
||||
## 20. 相关文档职责
|
||||
|
||||
- [通用助手工作栏与执行空间 PRD](./prd.md):
|
||||
工作栏应用级位置、稳定能力目录、执行空间和总体范围。
|
||||
- 本文:持久“+”、应用实例策略、工作栏动态 Tab、自动项目目标、本机与直接 SSH 多终端。
|
||||
- [统一界面设计系统](../../../UI-DESIGN.md):Tab、菜单、布局、主题、焦点和无障碍规则。
|
||||
- [SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md):
|
||||
Host Key、凭据、连接池、Agent 和远程项目现状。
|
||||
- [SSH Host 远程环境准备与控制面直连设计](../remote-host/environment-provisioning-technical-design.md):
|
||||
Host 保存、环境准备和项目使用之间的职责。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 电脑控制
|
||||
|
||||
电脑控制包含 GoodBuddy 对浏览器和客户端桌面能力的产品实现与平台适配状态。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [功能进度](./progress.md) | 当前已实现能力、缺陷修复、平台限制和验证状态 |
|
||||
|
||||
当前只有进度文档;产品需求、功能逻辑、UI 和技术设计尚未从历史实现记录中独立整理。
|
||||
+47
-20
@@ -6,8 +6,8 @@
|
||||
| --- | --- |
|
||||
| 文档类型 | 实施进度 |
|
||||
| 状态 | 持续更新 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-18 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 适用能力 | 内置浏览器与客户端电脑控制 |
|
||||
|
||||
## 范围
|
||||
@@ -17,13 +17,14 @@ Linux x64 和 Linux arm64。
|
||||
|
||||
## 已完成
|
||||
|
||||
### 直连模型与浏览器
|
||||
### Runtime 与浏览器
|
||||
|
||||
- 已实现宿主进程拥有的隔离浏览器服务。
|
||||
- 使用 Electron 自带 Chromium 和 CDP,不下载额外浏览器。
|
||||
- 已实现导航、可访问性快照、点击、输入、选择、返回和有界截图工具。
|
||||
- 浏览器工具只在直连模型的 Execute 模式中提供;用户选择 Execute 即授权
|
||||
本次交互运行,不再逐个弹出 GoodBuddy 工具审批。
|
||||
- 浏览器工具作为 GoodBuddy 内置能力,可分别分配给直连模型、GoodBuddy 管理的
|
||||
OpenCode 和 Continue;只在 Execute 模式中提供。用户选择 Execute 即授权本次
|
||||
运行,不再逐个弹出 GoodBuddy 工具审批。
|
||||
- 支持当前设备可连接的 HTTP、HTTPS、公网、内网、本机和元数据地址,
|
||||
导航及重定向仍经过 DNS 解析和目标地址固定。
|
||||
- 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、
|
||||
@@ -59,11 +60,14 @@ Linux x64 和 Linux arm64。
|
||||
|
||||
### 设置与安全边界
|
||||
|
||||
- 能力存储已迁移到版本 2。
|
||||
- 内置浏览器已归入“直连模型”工具,与联网搜索并列,并明确说明不会控制
|
||||
客户端已安装的浏览器;原有开关和配置 ID 保持不变,用户状态无需迁移。
|
||||
- 用户通过内置浏览器总开关决定是否向直连模型提供工具;开启后可在 Execute
|
||||
模式直接使用,不逐次询问。诊断位于同一分类。
|
||||
- 能力存储已迁移到版本 6。版本 2 至 5 中原有的浏览器启停状态会迁移到
|
||||
`builtin-browser`,已有启用状态不丢失;默认分配给直连模型、GoodBuddy 管理的
|
||||
OpenCode 和 Continue。
|
||||
- 内置浏览器位于“内置 MCP”,与知识库、笔记和 GoodBuddy 配置一样提供独立启停、
|
||||
Runtime 分配和工具清单;DeepSeek Harness 持续显示为不支持。
|
||||
- 浏览器开关只控制 Agent 工具访问。关闭后,任何 Runtime 都不会获得浏览器工具,
|
||||
但用户仍可在浏览器工作栏中手动前往、返回、刷新、停止加载、交互和关闭浏览器。
|
||||
开启后,已分配 Runtime 可在 Execute 模式直接使用,不逐次询问。
|
||||
- 尚未实际参与浏览器执行的命名配置已从界面隐藏;底层旧数据和兼容接口继续
|
||||
保留,不删除用户已有记录。
|
||||
- “电脑控制”只显示实际操作客户端电脑的能力。能力启用、配置变更和本地
|
||||
@@ -88,15 +92,28 @@ Linux x64 和 Linux arm64。
|
||||
|
||||
### P0:右侧没有浏览器实时画面,已修复
|
||||
|
||||
浏览器窗口默认使用 `show: false`,但 BrowserService 现在从模型实际操作的同一
|
||||
会话捕获页面帧,并通过受限 IPC 发送状态、当前 URL 和约 220KB 的 JPEG 画面。右侧工作栏
|
||||
新增“浏览器”页签;活动对话启动浏览器时会自动打开该页签,并显示创建中、
|
||||
加载中、操作中、用户交互中、就绪、失败和已停止状态。用户可点击“交互”打开
|
||||
同一会话的子窗口辅助 Agent;交互时主窗口暂时禁用,关闭时先刷新最终画面再将
|
||||
浏览器窗口最小化,页面和会话继续保留。用户也可立即停止当前对话的浏览器会话。
|
||||
早期版本通过同一浏览器会话的 JPEG 帧提供预览。当前工作栏直接嵌入 Agent 操作的原生
|
||||
`WebContentsView`,不再自动截图或传输预览帧,也不再用独立交互窗口阻塞主界面。
|
||||
页面显示、标签页归属、失败重试和显式截图的当前规则统一见
|
||||
[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md);
|
||||
该路径的最新验证证据见[工作栏进度](../assistant-workbar/progress.md)。
|
||||
|
||||
当前实现按导航、快照、点击、输入、选择、返回和截图操作刷新画面,而不是创建
|
||||
第二个预览浏览器,因此显示内容与模型受控页面一致。
|
||||
### 已实现:浏览器工具栏
|
||||
|
||||
浏览器 Tab 顶部已增加“返回、刷新/停止加载、地址输入、前往、交互、关闭浏览器”工具栏。
|
||||
它继续操作 Agent 在当前 Conversation 中使用的同一浏览器会话;用户导航会有意改变 Agent
|
||||
下一步看到的页面。前往、返回和刷新通过 `BrowserService` 与 Agent 浏览器操作串行执行,
|
||||
不会创建第二套 Renderer 导航路径。
|
||||
|
||||
工具栏状态以浏览器提交后的权威 URL、是否加载中、能否返回和会话活动状态为准;地址输入
|
||||
在用户编辑期间保留本地草稿,不被状态刷新覆盖;提交未带 HTTP(S) 协议的裸域名时会自动添加
|
||||
`http://`。创建中、Agent 操作中和完整交互窗口期间禁用其余工具栏控件,但活动导航仍可用
|
||||
“停止加载”中断。停止加载只取消页面加载,不关闭浏览器,也不释放 Runtime 上下文;原“停止
|
||||
浏览器”会话生命周期操作已统一标为“关闭浏览器”,并在视觉和文案上与停止加载清楚区分。
|
||||
|
||||
活动 Conversation 尚无浏览器会话时,可直接从地址栏输入 URL 并“前往”,通过浏览器组件
|
||||
可用性检查和同一 URL 策略创建会话。该手动路径不读取 Agent 浏览器开关。实现复用现有每
|
||||
Conversation 串行队列和有界状态,不引入恢复、版本化或第二套会话框架。
|
||||
|
||||
### P0:百度输入时元素引用失效,已修复
|
||||
|
||||
@@ -145,16 +162,26 @@ Linux x64 和 Linux arm64。
|
||||
|
||||
## 下一步顺序
|
||||
|
||||
1. 在便携版真实模型流程中继续验证返回、取消和右侧画面刷新。
|
||||
1. 在便携版真实模型流程中继续验证 OpenCode、Continue 的导航、返回、取消和右侧画面刷新。
|
||||
2. 再设计命名隔离 Profile 的持久登录态、引用和清理策略。
|
||||
3. 分别实现并真机验证 Windows UIA、macOS AX 和 Linux 原生适配器。
|
||||
|
||||
## 最近验证
|
||||
|
||||
- `npm test`:591 项通过,6 项跳过。
|
||||
- `npm test`:3496 项通过,53 项跳过。
|
||||
- `npm run typecheck`:通过。
|
||||
- `npm run lint`:通过。
|
||||
- `npm run build`:通过。
|
||||
- `GOODBUDDY_RUN_RUNTIME_E2E=1 ... -t "calls the assigned built-in browser
|
||||
through bundled OpenCode"`:通过,真实模型上报 2 次模型调用,OpenCode 经请求级
|
||||
GoodBuddy MCP 调用 `browser_navigate` 并保留正确 Conversation 归属。
|
||||
- `GOODBUDDY_RUN_RUNTIME_E2E=1 ... -t "calls the assigned built-in browser
|
||||
through bundled Continue"`:通过,Continue 经同一路径调用 `browser_navigate` 并
|
||||
返回预期结果;当前 Continue Host 为该完整工具轮次上报 1 条聚合模型用量事件。
|
||||
- 聚焦的能力迁移、网关、IPC、设置界面测试共 234 项通过。其中覆盖 Agent 浏览器
|
||||
开关关闭时用户手动浏览器操作仍可用。
|
||||
|
||||
以上自动化结果覆盖协议、安全边界和模拟驱动,不替代真实 Electron
|
||||
BrowserWindow/WebContentsView、目标网站和目标 Linux 桌面环境的集成验证。
|
||||
本轮曾尝试用隔离用户目录启动第二个 Electron 实例,但当前已运行的正式 GoodBuddy
|
||||
单实例锁阻止了该实例继续运行;未关闭或附着用户正在使用的正式实例。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 持续学习
|
||||
|
||||
持续学习定义 GoodBuddy 如何从运行结果、评估证据和人工反馈中形成可验证的改进闭环。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [持续学习与评估门 PRD](./prd.md) | 产品目标、学习边界、评估门和验收要求 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
+3
-3
@@ -7,12 +7,12 @@
|
||||
| 状态 | 设计中,远期能力 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[并行实验 PRD](../experiments/parallel-experiments-prd.md)、[分区记忆 PRD](../memory/partitioned-memory-prd.md) |
|
||||
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[并行实验 PRD](../parallel-experiments/prd.md)、[分区记忆 PRD](../memory/prd.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。其长期“未来分区记忆”
|
||||
方向尚未设计,也不承担持续学习、模式挖掘或自动改进:
|
||||
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。它与分区记忆的协同尚未
|
||||
实施,也不承担持续学习、模式挖掘或自动改进:
|
||||
|
||||
- 候选是否改善未来行为没有评估。
|
||||
- 一条反思是否会被检索和使用并不确定。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 会话 HTML 渲染
|
||||
|
||||
会话 HTML 渲染让用户直接查看 Agent 回复中的网页结果,而不必先复制代码到外部文件。
|
||||
该能力默认开启,也可以在“设置 > 平台功能 > 通用设置”中关闭。
|
||||
|
||||
## 功能边界
|
||||
|
||||
- 只渲染已完成的 Agent 正文和已完成的子 Agent 输出。
|
||||
- 支持完整 HTML 文档、单一 HTML 根元素,以及 Markdown 中的 `html` / `htm` 代码块。
|
||||
- 使用无脚本、无网络、无表单提交和无窗口操作权限的静态隔离预览。
|
||||
- 每个预览提供源码切换和全屏预览两个图标操作;全屏预览沿用同一清理和隔离边界。
|
||||
- 用户消息、推理过程和流式中的回复不执行 HTML;关闭功能后继续显示 HTML 源码。
|
||||
- 本功能不把普通聊天回复复制为成果,也不替代助手工作栏中独立成果的预览流程。
|
||||
|
||||
## 文档
|
||||
|
||||
- [产品需求](./prd.md):功能范围、状态规则和验收标准。
|
||||
- [技术设计](./technical-design.md):设置持久化、渲染边界与静态隔离方案。
|
||||
- [实现进度](./progress.md):已验证实现和验证记录。
|
||||
|
||||
## 术语
|
||||
|
||||
- **HTML 源码**:Agent 回复中原始、可复制的 HTML 文本。
|
||||
- **静态预览**:在隔离 `iframe` 中显示的 HTML;脚本、联网和外部操作均被禁用。
|
||||
- **全屏预览**:在应用级 Modal 中显示同一份静态预览文档,用于查看完整页面布局。
|
||||
|
||||
全局聊天和设置界面规则由根目录的
|
||||
[统一界面设计系统](../../../UI-DESIGN.md)定义。独立成果预览仍由
|
||||
[助手工作栏](../assistant-workbar/README.md)负责。
|
||||
@@ -0,0 +1,69 @@
|
||||
# 会话 HTML 渲染产品需求
|
||||
|
||||
## 用户问题
|
||||
|
||||
Agent 可以生成网页、报告和交互原型的 HTML,但此前会话只能显示源码或忽略原始标签。
|
||||
用户必须把内容复制到外部文件后才能查看结果,打断了会话内的阅读和迭代流程。
|
||||
|
||||
## 目标
|
||||
|
||||
- 让 Agent 生成的 HTML 在回复完成后直接显示为网页预览。
|
||||
- 保留一键查看原始 HTML 的能力。
|
||||
- 允许用户通过平台功能开关恢复为只看源码。
|
||||
- 预览不取得 GoodBuddy Renderer 或外部网络能力。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不执行 Agent 提供的 JavaScript。
|
||||
- 不提供联网网页、表单、弹窗或 Electron `webview`。
|
||||
- 不在流式输出中反复加载不完整页面。
|
||||
- 不自动把普通会话回复保存为成果。
|
||||
|
||||
## 功能需求
|
||||
|
||||
### FR-1 默认设置
|
||||
|
||||
“会话 HTML 渲染”是应用级持久设置,默认开启。用户可在“设置 > 平台功能 >
|
||||
通用设置”中立即关闭或重新开启;保存失败时保留最后确认的状态并显示错误。
|
||||
|
||||
### FR-2 可识别输出
|
||||
|
||||
开启后,已完成 Agent 回复中的以下内容显示为静态预览:
|
||||
|
||||
1. 完整 HTML 文档。
|
||||
2. 由单一 HTML 根元素构成的完整回复。
|
||||
3. Markdown 中标记为 `html` 或 `htm` 的代码块。
|
||||
|
||||
普通 Markdown 继续按原有规则显示。一个回复中的多个 HTML 代码块各自保持原有位置。
|
||||
|
||||
### FR-3 回复状态和角色
|
||||
|
||||
- 已完成的主 Agent 正文和已完成的子 Agent 输出可以预览 HTML。
|
||||
- 用户消息和 Agent 推理只显示文本或源码。
|
||||
- 流式 Agent 正文在完成前显示源码,完成后原位切换为预览。
|
||||
|
||||
### FR-4 源码、全屏与关闭行为
|
||||
|
||||
每个预览持续显示“静态安全预览,脚本和网络已禁用”,并在标题右侧提供两个图标操作:
|
||||
|
||||
1. 源码按钮,就地展开或收起原始 HTML。
|
||||
2. 全屏按钮,在应用级 Modal 中查看同一份预览文档。
|
||||
|
||||
嵌入预览使用固定高度并在框内滚动,需要更大画面时使用全屏预览。全屏预览可通过关闭
|
||||
按钮、Escape 或点击遮罩退出,退出后焦点回到全屏按钮。关闭平台功能后,完整 HTML 回复
|
||||
和 HTML 代码块继续显示源码,不能变成空白。
|
||||
|
||||
### FR-5 静态隔离
|
||||
|
||||
预览必须禁用脚本、网络请求、表单提交、嵌套页面、插件对象和窗口操作。远程链接和远程
|
||||
媒体不得从预览加载;内联样式和有界的内联位图可用于还原静态视觉。
|
||||
|
||||
## 产品验收
|
||||
|
||||
- 新安装和从旧设置升级后,开关均为开启。
|
||||
- 完整 HTML 和 fenced HTML 在已完成的 Agent 回复中显示预览。
|
||||
- 关闭开关后,当前会话立即恢复显示源码,重启后选择仍保留。
|
||||
- 流式回复、推理和用户消息从不执行 HTML。
|
||||
- 预览可通过键盘访问源码和全屏按钮,并具有明确的 iframe 标题。
|
||||
- 全屏预览隔离背景、初始聚焦关闭按钮,Escape 退出后焦点回到全屏按钮。
|
||||
- 恶意脚本、事件属性、远程资源、表单和嵌套 frame 不进入可执行预览。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 会话 HTML 渲染实现进度
|
||||
|
||||
## 已实现
|
||||
|
||||
- [x] 应用级持久开关,默认开启,并提供旧设置迁移。
|
||||
- [x] 平台功能通用设置中的共享 Switch。
|
||||
- [x] 已完成 Agent 回复和子 Agent 输出中的完整 HTML 与 fenced HTML 识别。
|
||||
- [x] 隔离静态预览、持续安全说明和源码切换。
|
||||
- [x] 预览保留作者 `<style>` 与 `style` 属性,样式不再被整块丢弃。
|
||||
- [x] 与 Mermaid 一致的图标按钮组:源码切换与全屏预览 Modal。
|
||||
- [x] 嵌入预览固定 480px 高度,源码面板同值上限。
|
||||
- [x] 用户消息、推理、流式回复和关闭状态的源码边界。
|
||||
- [x] 设置变更后当前会话立即重新渲染。
|
||||
|
||||
## 验证记录
|
||||
|
||||
2026-09-05:
|
||||
|
||||
- `npm run typecheck`:通过。
|
||||
- 聚焦测试(应用设置、Markdown、会话时间线、设置中心、App 集成):306 项通过。
|
||||
- 全量 `npm test`:`3493 passed, 55 skipped`。
|
||||
- `npm run lint`:通过。
|
||||
- `npm run build`:Main、Preload、Renderer 与控制面安装器均成功构建。
|
||||
|
||||
修复样式丢失后复验:
|
||||
|
||||
- 原实现只要 `<style>` 或 `style` 属性中出现 `@import` / `url()` 就整块删除,带渐变或
|
||||
背景图的回复因此完全失去样式;现改为保留作者样式,外部资源加载仍由 iframe CSP 拒绝。
|
||||
- `npx vitest run src/renderer/src/MarkdownRenderer.test.tsx`:`19 passed`。
|
||||
- 聚焦测试(会话时间线、App 集成、设置中心):`260 passed`。
|
||||
- `npm run typecheck`、`npm run lint`:通过。
|
||||
|
||||
新增全屏预览与图标化源码按钮后复验:
|
||||
|
||||
- 源码按钮改为代码图标并暴露 `aria-expanded`;新增放大图标打开全屏 Modal。
|
||||
- 嵌入预览高度由 `min-height: 320px` 改为固定 `height: 480px`,源码面板上限同步为 480px。
|
||||
- 新增回归测试覆盖全屏 Modal 的初始焦点、背景 inert、Sandbox/CSP 与 Escape 焦点恢复。
|
||||
- 聚焦测试(Markdown、会话时间线、App 集成、设置中心):`280 passed`。
|
||||
- `npm run typecheck`、`npm run lint`:通过。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 会话 HTML 渲染技术设计
|
||||
|
||||
## 进程和设置边界
|
||||
|
||||
`conversationHtmlRenderingEnabled` 属于现有 `ApplicationSettings`。Main 继续通过
|
||||
版本化 JSON 文件持久化应用设置,IPC 继续复用已验证的
|
||||
`settings:application:get/update` 通道,Preload 只暴露类型化设置方法。
|
||||
|
||||
应用设置格式升级到版本 11。版本 10 及更早设置在读取时补入默认值 `true`,无需保留第二份
|
||||
状态或独立迁移文件。
|
||||
|
||||
## Renderer 数据流
|
||||
|
||||
1. `App` 启动时读取应用设置,并把当前值传给每个保活的会话视图。
|
||||
2. 平台功能 Switch 保存成功后立即更新 `App` 状态。
|
||||
3. `ChatTimeline` 只为 `role === "assistant"` 且 `state === "complete"` 的正文传入
|
||||
HTML 渲染权限;推理始终不传入该权限。
|
||||
4. `MarkdownRenderer` 识别完整 HTML 回复以及 `html` / `htm` fenced code。
|
||||
5. 关闭权限或回复仍在流式阶段时,HTML 保持代码文本。
|
||||
|
||||
## 静态预览
|
||||
|
||||
`StaticHtmlPreview` 不使用 `dangerouslySetInnerHTML`。它先用 DOMPurify 清理文档,再通过
|
||||
`DOMParser` 移除事件属性、非锚点 `href` 和非内联图片的 `src` / `poster`,最后把结果写入
|
||||
`<iframe sandbox="" srcDoc>`。
|
||||
|
||||
作者样式必须保留:`<style>` 元素和 `style` 属性原样传入预览,不按 `@import` 或 `url()`
|
||||
关键字整块删除,否则任何带渐变或背景图的回复都会退化成无样式文档。外部样式表和远程
|
||||
字体、图片的实际加载由 iframe 文档的 CSP 拒绝,不需要在清理阶段重复拦截。
|
||||
|
||||
iframe 文档注入严格 CSP:
|
||||
|
||||
- `default-src 'none'`
|
||||
- `script-src 'none'`
|
||||
- `connect-src 'none'`
|
||||
- `frame-src 'none'`
|
||||
- `object-src 'none'`
|
||||
- `form-action 'none'`
|
||||
- 只允许内联样式以及 `data:` 字体和受支持的内联位图
|
||||
|
||||
同时设置 `referrerPolicy="no-referrer"`。Sandbox 不授予 scripts、forms、popups、
|
||||
same-origin 或 navigation 权限。预览只在回复完成后创建,并使用 lazy loading。
|
||||
|
||||
## UI 与无障碍
|
||||
|
||||
预览沿用聊天阅读宽度、语义颜色和共享 `icon-button`。`figure` / `figcaption` 表达预览及其
|
||||
说明,iframe 具有本地化标题。标题右侧是一个 `role="group"` 图标按钮组,与 Mermaid 图表
|
||||
操作保持一致:源码按钮暴露 `aria-expanded` 与 `aria-controls`,全屏按钮打开 Modal。
|
||||
|
||||
嵌入预览高度固定为 480px,超出部分在框内滚动;查看源码面板使用同一数值作为最大高度。
|
||||
iframe 无法按内容自适应高度,因为 `sandbox=""` 不含 `allow-same-origin` 或 `allow-scripts`,
|
||||
主文档既读不到内部文档尺寸,也不能接收上报;全屏预览覆盖需要更多空间的场景。
|
||||
|
||||
全屏预览复用 `dialog-focus` 的 `activateModalFocus` 与 `trapTabFocus`,遵循统一 Modal 的
|
||||
背景 inert、初始焦点落在关闭按钮、Tab 循环、Escape 关闭和关闭后焦点恢复。它渲染的是与
|
||||
嵌入预览完全相同的清理后文档,不额外放宽 Sandbox 或 CSP。切换回复时,源码面板与全屏
|
||||
预览按当前 source 派生开合状态,不使用会引发级联渲染的重置 effect。
|
||||
|
||||
## 验证范围
|
||||
|
||||
- 应用设置默认值、关闭持久化和版本 10 迁移。
|
||||
- fenced HTML、完整 HTML、关闭后的源码回退。
|
||||
- 作者 `<style>` 与 `style` 属性在预览中保留。
|
||||
- 脚本、远程资源、表单和事件属性清理,以及 iframe Sandbox/CSP。
|
||||
- 源码按钮的 `aria-expanded` 切换,以及全屏预览的初始焦点、背景 inert、Escape 关闭和
|
||||
焦点恢复。
|
||||
- 主 Agent、子 Agent、用户、推理和流式状态边界。
|
||||
- 从设置 Switch 到当前会话重新渲染的应用级集成路径。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 会话监督
|
||||
|
||||
会话监督定义对长会话、工具执行和异常状态的观察、判断与介入边界。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [会话监督 PRD](./prd.md) | 监督目标、证据、介入方式和产品验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-08-18 |
|
||||
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md) |
|
||||
| 界面归属 | [通用助手工作栏与执行空间](../assistant-experience/assistant-workbar-and-execution-spaces-prd.md) |
|
||||
| 界面归属 | [通用助手工作栏与执行空间](../assistant-workbar/prd.md) |
|
||||
| 体验参考 | GoodBuddy 魔法笔记 AI 评论流 |
|
||||
|
||||
## 1. 背景
|
||||
@@ -0,0 +1,12 @@
|
||||
# DeepSeek Harness Runtime
|
||||
|
||||
本功能定义 DeepSeek Harness 作为 GoodBuddy 本机 Agent Runtime 的产品接入、控制面、
|
||||
插件市场、权限和打包边界。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | Runtime 产品与技术方案、协议、插件、打包、测试和验收 |
|
||||
|
||||
现有文档同时包含部分 PRD、功能逻辑和进度内容,后续修改该 Runtime 时再按标准职责拆分。
|
||||
+37
-9
@@ -7,7 +7,7 @@
|
||||
| 文档状态 | 实现与发布验收基线 |
|
||||
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
|
||||
| Runtime 标识 | `deepseek-harness` |
|
||||
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.8` |
|
||||
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.2-rc.1` |
|
||||
| 上游状态 | Developer Preview,允许出现破坏性变更 |
|
||||
| 上游许可证 | MIT |
|
||||
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
@@ -136,9 +136,14 @@ Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不
|
||||
|
||||
- GoodBuddy 已经持久化对话、消息、活动、工具事件和用量。
|
||||
- 再写一份 Harness 日志会扩大敏感数据副本和清理范围。
|
||||
- GoodBuddy 在 Runtime 重启后可以用现有的有界历史创建新 Harness Session。
|
||||
- GoodBuddy 在 Runtime 重启后可以用现有会话历史创建新 Harness Session。
|
||||
|
||||
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
|
||||
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
|
||||
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
|
||||
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
|
||||
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
|
||||
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
|
||||
|
||||
## 6. 总体架构
|
||||
|
||||
@@ -254,7 +259,9 @@ Store 是否已经提交为准,确定性完成新目录或恢复旧目录,
|
||||
| `goodbuddy/native/snapshot` | Main → Control Plane | 从无 Agent scope 的 Host Registry 读取有界的原生 Tool/Skill 元数据,排除 GoodBuddy 分配项与请求级代理 |
|
||||
| `goodbuddy/shutdown` | Main → Control Plane | 停止接收新请求并有序清理 |
|
||||
|
||||
Utility 启动控制协议使用版本 2,严格携带 `supportsImageInput` 与固定 8 MiB 帧上限;版本 1 或缺少该字段的启动消息失败关闭,不能让 Host 自行猜测模型能力。扩展版本独立于 ACP 版本。握手响应至少包含:
|
||||
Utility 启动控制协议使用版本 3,严格携带 `supportsImageInput`、模型连接的有界自定义
|
||||
Header 与固定 8 MiB 帧上限;旧版本或缺少必需字段的启动消息失败关闭,不能让 Host
|
||||
自行猜测模型能力。扩展版本独立于 ACP 版本。握手响应至少包含:
|
||||
|
||||
```ts
|
||||
type GoodBuddyHarnessCapabilities = {
|
||||
@@ -288,6 +295,10 @@ GoodBuddy 的工作模式属于每个请求,不属于 Runtime 进程全局状
|
||||
5. 缺少准备状态、重复使用、请求标识不匹配时,Control Plane 直接拒绝请求。
|
||||
6. 同一 Session 只允许一个 Prompt 在途。
|
||||
|
||||
若 Runtime 回传 ACP 权限请求,Main 只按活跃 Session 的当前模式答复:Execute 优先选择
|
||||
`allow_once`,缺少时使用提供的 `allow_always`,不调用第二次人工 authorizer;
|
||||
Ask、已关闭或未知 Session 拒绝。这里不写入永久授权规则,也不把业务问答当作权限确认。
|
||||
|
||||
### 8.5 事件模型
|
||||
|
||||
Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
|
||||
@@ -321,7 +332,7 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
|
||||
- 首次请求创建 Session。
|
||||
- 已有 Session 只发送当前 Prompt。
|
||||
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的有界历史。
|
||||
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的会话历史。
|
||||
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
|
||||
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
|
||||
|
||||
@@ -336,7 +347,7 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
|
||||
### 9.4 释放与退出
|
||||
|
||||
- 原生能力清单通过一次性 Runtime 探测,取得有界快照后立即 dispose,不得因浏览设置或切换项目把 Host 缓存在执行 Runtime 池中。
|
||||
- 原生能力清单由 `SelectedRuntimeManager` 缓存的执行 Runtime 提供,与随后的对话请求共用同一实例,不再为清单单独启动并销毁一次 Host;该实例遵循执行缓存的空闲淘汰、设置变更 reset 和退出 dispose 规则。
|
||||
- 删除或释放对话时调用 `goodbuddy/session/release`。
|
||||
- Runtime dispose 时先拒绝新请求,再取消所有 Session。
|
||||
- Harness Control Plane 完成 Agent、工具和会话清理,Host 完成 Cordis Fiber 与子进程的反向清理。
|
||||
@@ -370,6 +381,9 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
- 工作区不是 containment 边界;绝对路径和命令可访问当前用户本来有权访问的主机资源。
|
||||
- 已启用插件注册的工具与内置工具使用同一分发路径;GoodBuddy 不增加插件权限矩阵或逐工具确认。
|
||||
- Main 代理的 MCP 工具继续执行分配、schema、活动请求、模式和 RuntimeAuthorizer 校验。
|
||||
`goodbuddy/tools/list` 按 Session 保存本次发现的工具定义;`goodbuddy/tools/call` 复用该定义
|
||||
校验参数与授权,不因调用健康工具而重新等待故障 MCP 的发现。发现重试留在清单刷新,
|
||||
Session 释放及 Runtime dispose 清除内存定义,不持久化第二份能力状态。
|
||||
- 所有工具调用仍作为活动事件记录;Ask 和 delegation 路径继续固定拒绝。
|
||||
|
||||
### 10.4 Runtime OS 沙箱
|
||||
@@ -431,8 +445,21 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
- 模型名称不限制为 DeepSeek 品牌,由所选 OpenAI 兼容服务决定。
|
||||
- 模型名称和服务地址由 Main 传入受控 Host。
|
||||
- 图片能力只读取所选 GoodBuddy 模型连接的 `supportsImageInput`;Main、Utility 启动配置、ACP 能力和 Pi-AI 模型输入模态必须使用同一个布尔值。
|
||||
- 模型连接的自定义 Header 通过 Utility 启动配置交给 Pi-AI Provider;Pi-AI 没有受支持的
|
||||
任意 Body 配置入口,因此 DeepSeek Harness 不接收自定义 Body。完整支持矩阵和合并规则
|
||||
见[模型连接请求定制](../model-connections/technical-design.md)。
|
||||
- API Key 继续保存在 GoodBuddy 加密设置中。
|
||||
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
|
||||
- `platform` 在设置页显示为“管理员环境优先,回退 GoodBuddy 兼容连接”,不是 Harness
|
||||
自有配置文件。Main 优先读取完整的 `GOODBUDDY_MODEL_BASE_URL`、
|
||||
`GOODBUDDY_MODEL_NAME` 和 `GOODBUDDY_MODEL_API_KEY` 环境预置;不完整时使用
|
||||
当前默认兼容 GoodBuddy 连接,再回退首个兼容连接。
|
||||
- `default` 只跟随 GoodBuddy 默认兼容连接或首个兼容连接,跳过独立管理员预置;
|
||||
`profile` 使用指定的兼容连接。没有兼容连接时,仍允许选择 `platform`,因为完整
|
||||
管理员预置不依赖已保存连接的协议;运行时缺少实际连接才报告不可用。
|
||||
- Renderer 通过 Main 的只读平台来源投影展示实际管理员模型或兼容回退,不读取环境
|
||||
或密钥,也不根据 `credentialSource` 猜测。投影直接复用本节所述的生产 resolver,
|
||||
字段、非持久化与草稿展示边界见
|
||||
[Harness 平台来源公开投影](../model-connections/technical-design.md#9-harness-平台来源公开投影)。
|
||||
|
||||
不允许选择 Harness 自有的用户配置文件或自定义 Host。Runtime 始终使用随当前 GoodBuddy 版本发布的内置 Host,并通过完整内部能力握手。
|
||||
|
||||
@@ -462,7 +489,8 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
| 单图尺寸 / 单条消息解码像素 | 单边最长 8,192 px 且 1,600 万像素 / 3,200 万像素 |
|
||||
| Host 临时图片存储 | 32 MiB 且最多 256 个唯一对象 |
|
||||
| 单个文本或推理事件 | 64 KiB |
|
||||
| 单次请求累计协议输出 | 4 MiB |
|
||||
| 单次请求累计协议输出 | 无固定总量上限,继续校验单帧和待处理队列 |
|
||||
| 单轮模型输出 Token | 由 Harness SDK 与 Provider 决定,GoodBuddy 不固定覆盖为 16K |
|
||||
| 工具输入摘要 | 4,000 字符 |
|
||||
| 工具输出摘要 | 4,000 字符 |
|
||||
| 待处理事件数 | 1,000 |
|
||||
@@ -497,7 +525,7 @@ Runtime: GoodBuddy 内置 DeepSeek Harness
|
||||
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
|
||||
状态: 已就绪
|
||||
路径: <受控 Host 路径>
|
||||
版本: 0.1.0-rc.8
|
||||
版本: 0.1.2-rc.1
|
||||
执行权限: 当前用户权限
|
||||
|
||||
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
|
||||
@@ -535,7 +563,7 @@ Runtime 栏目入口始终存在,并采用统一监督模型;内部再按用
|
||||
显示 OpenCode、Continue 或 DSH 的具体区域。未支持能力不渲染空卡片或一排禁用按钮,而是
|
||||
在用户需要理解缺口时显示原因和可执行入口。跟随模式切换 Runtime、Conversation 或 Task
|
||||
时必须清理上一归属的聚合执行状态,固定目标则保持不变。完整工作栏契约见
|
||||
[通用助手工作栏与执行空间 PRD](../prd/assistant-experience/assistant-workbar-and-execution-spaces-prd.md)。
|
||||
[通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)。
|
||||
|
||||
所有未来的 Subagent、Job、Workflow、Hook 和会话操作仍须经过 Main 的 Runtime 边界,保留取消、超时、权限、Task/Job/Subjob 层级、用量和活动审计。高风险动作在侧栏就地确认,运行结果进入活动与成果记录,不以 Composer 按钮代替监督面板。DeepSeek Harness 首版仍不加载这些服务,本节只确定未来跨 Runtime 的产品位置和协议归属。
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# 桌面宠物
|
||||
|
||||
桌面宠物是 GoodBuddy 桌面端一个可选的常驻悬浮入口,用于随时发起提问、暂存文件或文字,
|
||||
并直观查看当前任务状态。桌面宠物不建立新的对话、任务或执行体系:它只是现有 Conversation、
|
||||
Task 和 Runtime 能力的一个轻量触发面和状态镜面。
|
||||
|
||||
## 核心边界
|
||||
|
||||
- 桌面宠物**不**新增 Ask/Execute 执行通道,也不在自身窗口内直接展示模型回复。快速发起
|
||||
和拖放暂存的内容始终交给主窗口现有输入区确认后发送,从不自动发送或自动执行。
|
||||
- 桌面宠物**不**持续读取屏幕内容、剪贴板或全局选中文本。
|
||||
- 桌面宠物显示的任务状态、通知和历史记录均来自现有 Task/Conversation 数据,不建立第二套
|
||||
存储或索引。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、范围、功能需求和产品验收 |
|
||||
| [User Stories](./user-stories.md) | 首次启用、正常使用、失败恢复、兼容和边界场景 |
|
||||
| [功能逻辑](./logic-design.md) | 不变量、状态维度、决策表和失败结果 |
|
||||
| [UI 设计](./ui-design.md) | 窗口交互、设置入口、状态呈现、无障碍和响应式行为 |
|
||||
| [技术设计](./technical-design.md) | 窗口与进程边界、IPC 边界、持久化、生命周期和验证策略 |
|
||||
|
||||
功能尚未实施,因此不创建 `progress.md`;实施开始后按 [`docs/features/AGENTS.md`](../AGENTS.md)
|
||||
的证据要求补充。
|
||||
|
||||
## 术语
|
||||
|
||||
- **桌面宠物窗口**:应用启用该功能后创建的常驻、无边框、可置顶悬浮窗口。
|
||||
- **快速发起**:点击宠物打开的小型输入框,用于输入文字或附加单个文件,提交后转入主窗口。
|
||||
- **暂存**:快速发起或拖放产生的文字和附件在提交前的中间状态,不代表已发送或已执行。
|
||||
- **状态镜面**:宠物窗口对当前任务状态(等待处理、执行中、已完成、失败、空闲)的展示,
|
||||
数据权威来自 Task 与 Conversation,不是独立状态源。
|
||||
- **安静模式**:抑制宠物完成/失败气泡提示的设置,可访问的状态查询不受影响。
|
||||
|
||||
## 相关功能
|
||||
|
||||
- [Task 与 Job 文档集](../task-and-job/README.md):任务状态、状态优先级和 Task Center
|
||||
导航的权威定义,宠物的状态镜面必须与其保持一致,不重新定义任务生命周期。
|
||||
- [助手工作栏](../assistant-workbar/README.md):主窗口执行空间和输入区的权威定义,宠物的
|
||||
快速发起和拖放暂存最终落地到该输入区,不复制其行为。
|
||||
- [直连模型 Agent 能力](../direct-model-agent/README.md)、[远程主机与远程执行](../remote-host/README.md):
|
||||
Ask/Execute 授权边界的权威定义,宠物不扩大或绕过该边界。
|
||||
- [文档处理](../document-processing/README.md):拖放文件解析和格式支持的权威定义,宠物拖放
|
||||
暂存复用同一处理管线,不重新定义支持类型。
|
||||
- 根 [`UI-DESIGN.md`](../../../UI-DESIGN.md):全局视觉、通知视口、Switch/Checkbox 语义和
|
||||
无障碍规则的权威定义。
|
||||
- [`docs/development/release-runbook.md`](../../development/release-runbook.md):跨平台打包
|
||||
矩阵和交付物的权威定义。
|
||||
@@ -0,0 +1,203 @@
|
||||
# 桌面宠物功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
|
||||
| User Stories | [桌面宠物 User Stories](./user-stories.md) |
|
||||
| 任务模型 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
本文负责桌面宠物自身的状态、不变量和决策规则,不重新定义 Task、Conversation、Ask/
|
||||
Execute 或附件的既有语义;这些语义以各自权威文档为准。
|
||||
|
||||
## 1. 核心不变量
|
||||
|
||||
### INV-1 宠物不是第二条执行通道
|
||||
|
||||
快速起草和拖放暂存只产生主窗口会话输入区的草稿数据(文本、附件引用),不产生消息、
|
||||
不产生 Task、不触发模型调用。发送、模式选择和 Runtime 选择继续完全由主窗口既有流程
|
||||
拥有。
|
||||
|
||||
### INV-2 状态镜面是既有状态的投影
|
||||
|
||||
宠物状态镜面的取值只能从当前 Task 集合的既有状态推导,不允许引入宠物专属的任务状态
|
||||
字段或独立于 Task Center 的状态存储。宠物状态与 Task Center 展示的状态出现分歧即视为
|
||||
缺陷。
|
||||
|
||||
### INV-3 总开关是唯一的存在性开关
|
||||
|
||||
“启用桌面宠物”关闭时,宠物窗口、气泡和托盘条目文案必须整体消失;除该开关外,任何
|
||||
子设置(外观、位置、安静模式)的变化都不得单独创建或销毁宠物窗口。
|
||||
|
||||
### INV-4 暂时隐藏不影响启用状态
|
||||
|
||||
“暂时隐藏”只影响窗口可见性,不修改总开关持久化值,也不清除位置、外观或安静模式设置。
|
||||
|
||||
### INV-5 提示与原生通知互斥
|
||||
|
||||
同一个任务终态事件(完成或失败)只能产生宠物气泡或现有原生系统通知中的一种,不产生
|
||||
两种同时可见的独立提示。
|
||||
|
||||
### INV-6 位置始终落在可用显示器内
|
||||
|
||||
宠物窗口的持久化位置在应用于当前显示器拓扑之前必须被校验;不能得出宠物窗口完全位于
|
||||
任何当前可见显示器之外的最终坐标。
|
||||
|
||||
### INV-7 拖放与起草共享暂存规则
|
||||
|
||||
拖放产生的文本和文件必须满足与快速起草附件相同的类型、大小、数量限制;两者不允许存在
|
||||
两套互不一致的限制值。
|
||||
|
||||
## 2. 状态维度
|
||||
|
||||
### 2.1 总开关与可见性
|
||||
|
||||
```text
|
||||
disabled -> enabled_visible
|
||||
enabled_visible <-> enabled_hidden (暂时隐藏 / 重新显示)
|
||||
enabled_visible|enabled_hidden -> disabled (关闭总开关)
|
||||
```
|
||||
|
||||
`disabled` 是唯一没有宠物窗口存在的状态;`enabled_hidden` 窗口不可见但设置全部保留。
|
||||
|
||||
### 2.2 快速起草弹出框
|
||||
|
||||
```text
|
||||
closed -> drafting -> submitted | discarded
|
||||
drafting -> closed (Escape / 点击外部 / 关闭按钮)
|
||||
```
|
||||
|
||||
`submitted` 是终态转换动作,不是持续状态:提交后弹出框立即回到 `closed`,草稿数据已经
|
||||
转交给主窗口。
|
||||
|
||||
### 2.3 拖放交互
|
||||
|
||||
```text
|
||||
idle -> drag_over -> dropped_accepted | dropped_rejected | drag_cancelled
|
||||
```
|
||||
|
||||
`drag_over` 只影响视觉反馈,不产生任何持久数据;只有 `dropped_accepted` 才产生暂存
|
||||
数据。`dropped_rejected` 用于文件夹或全部文件超限的场景,不产生暂存也不打开主窗口。
|
||||
|
||||
### 2.4 状态镜面
|
||||
|
||||
```text
|
||||
unavailable | idle | completed_recent | running | waiting_approval | failed_recent
|
||||
```
|
||||
|
||||
取值由当前活跃与最近终态 Task 集合计算得出,属于派生状态,不单独持久化。
|
||||
|
||||
### 2.5 提示气泡
|
||||
|
||||
```text
|
||||
none -> pending -> shown -> dismissed
|
||||
```
|
||||
|
||||
`pending` 表示事件已发生但由于主窗口可见或安静模式而被抑制,不进入 `shown`。
|
||||
|
||||
## 3. 状态镜面优先级
|
||||
|
||||
宠物在任意时刻只呈现一个状态镜面取值。存在多个活跃 Task 时按以下优先级取最高项:
|
||||
|
||||
| 优先级 | 状态 | 条件 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `unavailable` | 用于确认状态的 Runtime 或网络当前不可达 |
|
||||
| 2 | `waiting_approval` | 至少一个 Task 处于等待批准 |
|
||||
| 3 | `failed_recent` | 至少一个 Task 在最近的“最近失败”窗口内失败,且用户尚未查看 |
|
||||
| 4 | `running` | 至少一个 Task 处于执行中 |
|
||||
| 5 | `completed_recent` | 至少一个 Task 在最近的“最近完成”窗口内完成,且用户尚未查看 |
|
||||
| 6 | `idle` | 以上条件都不满足 |
|
||||
|
||||
“最近失败”“最近完成”窗口的具体时长和“已查看”判定属于技术选择,由
|
||||
[技术设计](./technical-design.md)基于既有 Task 已读/未读语义确定,不在本文固定具体
|
||||
数值。
|
||||
|
||||
## 4. 提示归并决策表
|
||||
|
||||
| 事件 | 主窗口状态 | 安静模式 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 任务完成 | 不可见或未聚焦 | 关闭 | 显示宠物气泡;不触发原生通知 |
|
||||
| 任务完成 | 不可见或未聚焦 | 开启 | 不显示气泡;不触发原生通知;仅更新状态镜面 |
|
||||
| 任务完成 | 可见并聚焦 | 任意 | 不显示气泡;反馈通过主窗口既有机制呈现 |
|
||||
| 任务失败 | 不可见或未聚焦 | 关闭 | 显示宠物气泡并保持可见直到用户查看或关闭 |
|
||||
| 任务失败 | 不可见或未聚焦 | 开启 | 不显示气泡;仅更新状态镜面为 `failed_recent` |
|
||||
| 任务失败 | 可见并聚焦 | 任意 | 不显示气泡;反馈通过主窗口既有机制呈现 |
|
||||
|
||||
“主窗口可见并聚焦”与现有 [`desktop-notification.ts`](../../../src/main/desktop-notification.ts)
|
||||
判定原生通知是否触发所使用的条件保持一致,避免宠物和原生通知各自使用不同的可见性
|
||||
判定导致同一事件在两处呈现或同时缺失。
|
||||
|
||||
## 5. 快速起草与拖放的目标会话决策表
|
||||
|
||||
| 场景 | 目标会话 |
|
||||
| --- | --- |
|
||||
| 主窗口存在当前活跃会话,用户未在弹出框内更改目标 | 该活跃会话 |
|
||||
| 用户在弹出框内选择“新建对话” | 提交时新建并激活一个会话 |
|
||||
| 主窗口不存在任何会话 | 提交时新建并激活一个会话 |
|
||||
| 目标会话在提交前被用户在主窗口中删除 | 视为“不存在任何会话”,回退到新建会话 |
|
||||
|
||||
拖放没有目标切换界面,始终采用与快速起草相同的默认目标会话规则。
|
||||
|
||||
## 6. 附件与文本限制
|
||||
|
||||
快速起草和拖放暂存复用现有聊天附件规则,不定义新的限制数值:
|
||||
|
||||
- 单条草稿的附件数量上限、单个附件大小上限、支持的文本与图片类型,与主窗口聊天附件
|
||||
的现有限制一致。
|
||||
- 拖放同时包含多个文件时,逐个按上述规则校验;不合规文件被拒绝,不影响合规文件继续
|
||||
暂存。
|
||||
- 拖放文件夹被整体拒绝,不做递归展开或部分接受。
|
||||
|
||||
## 7. 位置与显示器校验
|
||||
|
||||
1. 加载持久化位置后,检查该坐标是否位于当前任一可见显示器的可用区域内。
|
||||
2. 位于范围内:直接使用。
|
||||
3. 不在范围内(例如记忆的显示器已断开):改用主显示器上一个默认可见位置,并在下一次
|
||||
用户拖动宠物后覆盖持久化位置。
|
||||
4. 开启“贴边”时,拖动释放位置与最近边缘的距离小于吸附阈值即吸附到该边缘;阈值和默认
|
||||
位置的具体数值属于技术选择。
|
||||
|
||||
## 8. 失败与恢复
|
||||
|
||||
| 场景 | 行为 |
|
||||
| --- | --- |
|
||||
| 主窗口进程尚未创建或已被销毁 | 提交快速起草或拖放时先创建/恢复主窗口,再写入草稿 |
|
||||
| 写入草稿时主窗口窗口对象不可用 | 提交失败,弹出框保留草稿内容供用户重试,不静默丢弃 |
|
||||
| 状态查询所需的 Runtime/网络不可达 | 状态镜面显示 `unavailable`,不回退显示 `idle` |
|
||||
| 显示器拓扑变化导致位置越界 | 按第 7 节回退到默认可见位置,不隐藏窗口 |
|
||||
| 用户在气泡展示期间关闭总开关 | 气泡与宠物窗口一并立即消失 |
|
||||
| 应用启动时总开关为开启但窗口创建失败 | 记录失败原因,不阻塞主窗口正常启动,托盘菜单提供重试入口 |
|
||||
|
||||
## 9. 需求追踪
|
||||
|
||||
| 需求 | 主要逻辑 |
|
||||
| --- | --- |
|
||||
| `FR-1` | `INV-3`、总开关与可见性状态 |
|
||||
| `FR-2` | 位置与显示器校验、`INV-6` |
|
||||
| `FR-3` | `INV-1`、快速起草状态、目标会话决策表 |
|
||||
| `FR-4` | `INV-1`、`INV-7`、拖放交互状态、附件与文本限制 |
|
||||
| `FR-5` | `INV-2`、状态镜面优先级 |
|
||||
| `FR-6` | `INV-5`、提示归并决策表 |
|
||||
| `FR-7` | 提示归并决策表中安静模式列 |
|
||||
| `FR-8` | 位置与显示器校验之外的技术选择,逻辑仅约束持久化立即生效 |
|
||||
| `FR-9` | `INV-4`、总开关与可见性状态 |
|
||||
| `FR-10` | 快速起草状态中的键盘转换,具体按键映射见 UI 设计 |
|
||||
| `FR-11` | `INV-1`、`INV-2`、`INV-7` |
|
||||
|
||||
## 10. 逻辑完整性评估
|
||||
|
||||
本设计覆盖了启用/关闭、暂时隐藏、快速起草、拖放(含超限和文件夹)、状态镜面优先级、
|
||||
提示与原生通知的归并、位置越界恢复、以及主窗口不可用时的失败路径。
|
||||
|
||||
未在本文固定、留给技术设计选择的实现细节:
|
||||
|
||||
- “最近完成/最近失败”窗口的具体时长和“已查看”判定的确切事件来源。
|
||||
- 贴边吸附阈值和默认回退位置的具体像素值。
|
||||
- 状态查询的具体轮询或事件驱动机制。
|
||||
|
||||
这些是技术选择而非未解决的产品决策:本文的优先级和决策表已经足以在任何取值下保持
|
||||
行为一致。
|
||||
@@ -0,0 +1,158 @@
|
||||
# 桌面宠物 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 适用产品 | GoodBuddy Desktop |
|
||||
| 关联文档 | [功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 当前只能通过主窗口发起提问、查看任务和管理执行空间。用户想要快速提问、丢一个
|
||||
文件进去,或者只是看一眼任务有没有跑完时,仍必须先唤出并聚焦整个主窗口。这在多任务、多
|
||||
显示器场景下打断当前工作流。
|
||||
|
||||
桌面宠物提供一个常驻、体积小、不抢焦点的悬浮入口,承担三类高频、轻量的动作:快速起草
|
||||
一个问题、拖放文件或文字暂存、以及查看当前任务状态。它不是新的执行通道,只是现有
|
||||
Conversation、Task 和 Runtime 能力上的一层轻量触发面和状态镜面。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
1. 用户无需先打开或聚焦主窗口即可起草一条消息或暂存一个文件。
|
||||
2. 用户可以在不打开 Task Center 的情况下,一眼看到当前是否有任务在跑、等待处理或失败。
|
||||
3. 桌面宠物默认不产生新的干扰:不主动弹窗抢焦点,不重复现有原生通知。
|
||||
4. 桌面宠物的开关、位置、外观和提示强度完全由用户控制,且可随时完全关闭并恢复到没有该
|
||||
功能之前的界面状态。
|
||||
5. 桌面宠物不新增或绕过 Ask/Execute 授权边界,也不建立第二条消息发送或任务执行路径。
|
||||
|
||||
## 3. 功能范围
|
||||
|
||||
### 3.1 包含
|
||||
|
||||
- 设置中的“桌面宠物”一级分类,包含总开关、外观、位置和提示设置。
|
||||
- 常驻悬浮窗口:可拖动、跨显示器记忆位置、贴边、置顶开关。
|
||||
- 点击宠物打开的“快速起草”弹出框:输入文字、附加至多一个本机文件,提交后转入主窗口
|
||||
对应会话的输入区草稿,不自动发送。
|
||||
- 拖放文字或文件到宠物窗口,效果与快速起草的附件路径一致。
|
||||
- 状态镜面:显示当前活跃任务的汇总状态,点击后打开 Task Center 中对应任务。
|
||||
- 完成/失败气泡提示,及独立于原生通知的安静模式开关。
|
||||
- 从有限的内置角色/外观中选择,调整尺寸、透明度和动画强度。
|
||||
- 全局显示/隐藏开关,托盘菜单入口,键盘可达的等效操作路径。
|
||||
|
||||
### 3.2 不包含
|
||||
|
||||
- 在宠物窗口内直接展示模型回复、执行工具调用或建立第二条 Ask/Execute 通道。
|
||||
- 未经用户确认自动发送消息或自动执行任务。
|
||||
- 持续读取屏幕内容、剪贴板或系统级选中文本。
|
||||
- 用户上传任意自定义头像图片、养成机制、货币或每日签到。
|
||||
- 多个宠物实例、宠物之间互动或联网社交功能。
|
||||
- 管理员远程强制启用、配置或监控用户的桌面宠物。
|
||||
- 替代 Task Center、助手工作栏或现有通知系统的权威数据来源。
|
||||
|
||||
## 4. 功能需求
|
||||
|
||||
### FR-1 总开关与默认状态
|
||||
|
||||
- 设置新增“桌面宠物”一级分类,包含“启用桌面宠物”总开关。
|
||||
- 全新安装和现有用户升级后默认关闭,不在未经用户操作时出现在桌面上。
|
||||
- 关闭总开关立即隐藏宠物窗口,保留用户此前的位置、外观和提示设置以便重新开启。
|
||||
|
||||
### FR-2 常驻悬浮窗口
|
||||
|
||||
- 启用后创建一个无边框、默认置顶、不出现在任务栏的小型窗口。
|
||||
- 用户可拖动到任意位置;位置按显示器分别记忆,显示器断开重连后回退到最近的可用显示器。
|
||||
- 提供“贴边”选项,拖动到屏幕边缘附近时自动吸附。
|
||||
- 提供独立的“始终置顶”开关;关闭后宠物仍可见,但可被其他窗口遮挡。
|
||||
- 宠物窗口不接受系统级最大化、最小化或调整大小操作。
|
||||
|
||||
### FR-3 快速起草
|
||||
|
||||
- 点击宠物打开一个紧邻宠物的小型弹出框,包含文本输入和“附加文件”按钮。
|
||||
- 文本输入支持多行、粘贴图片;附加文件复用现有附件类型、数量和大小限制。
|
||||
- 弹出框显示将要进入的目标:当前活跃会话,或“新建对话”。用户可在弹出框内切换目标,
|
||||
但不能在弹出框内选择模型、Runtime 或 Ask/Execute 模式。
|
||||
- 提交后主窗口被显示或聚焦,目标会话被激活,文字和附件作为草稿填入该会话的输入区,
|
||||
输入焦点落在该输入区。提交动作本身不发送消息、不创建任务。
|
||||
- 取消或关闭弹出框放弃当前草稿,不影响主窗口任何已有草稿。
|
||||
|
||||
### FR-4 拖放暂存
|
||||
|
||||
- 用户可以把本机文件或选中文字拖到宠物窗口上。
|
||||
- 拖入时宠物给出可放置的视觉反馈;释放后遵循与快速起草提交相同的行为:显示主窗口、
|
||||
激活目标会话、把内容放入该会话输入区草稿。
|
||||
- 拖放文件的类型、数量、大小限制与现有聊天附件限制一致;超出限制时明确提示被拒绝的
|
||||
文件,其余合法文件仍正常暂存。
|
||||
- 拖放不支持文件夹;文件夹被拖入时提示不支持,不递归展开。
|
||||
|
||||
### FR-5 状态镜面
|
||||
|
||||
- 宠物在空闲、执行中、等待处理、已完成、失败之间切换外观,反映当前所有活跃 Task 的
|
||||
汇总状态;多个状态并存时按“等待处理 > 失败 > 执行中 > 已完成 > 空闲”的优先级显示。
|
||||
- 点击状态镜面打开或聚焦主窗口的 Task Center,并定位到触发当前状态的任务。
|
||||
- Runtime 或网络不可用导致任务状态无法确认时,状态镜面显示“状态不可用”而不是回退成
|
||||
“空闲”。
|
||||
|
||||
### FR-6 完成与失败提示
|
||||
|
||||
- 任务从执行中或等待处理转为已完成或失败,且主窗口不可见或未聚焦时,宠物旁显示一条
|
||||
气泡,说明任务标题和结果。
|
||||
- 气泡在用户不操作时于一段时间后自动消失;失败气泡保持可见直到用户查看或手动关闭。
|
||||
- 点击气泡等价于点击状态镜面,打开对应任务详情。
|
||||
- 同一事件只能触发桌面宠物气泡或现有原生系统通知中的一种,不同时出现两条提示;具体
|
||||
归并规则由[功能逻辑](./logic-design.md)定义。
|
||||
|
||||
### FR-7 安静模式
|
||||
|
||||
- 独立于总开关的“安静模式”开关,开启后不再显示完成/失败气泡。
|
||||
- 安静模式不影响状态镜面本身的外观切换,也不影响原生系统通知的现有行为。
|
||||
|
||||
### FR-8 外观与个性化
|
||||
|
||||
- 提供数量有限的内置角色供选择,不支持上传自定义图片。
|
||||
- 提供尺寸、透明度和动画强度调整;调整立即生效并持久化。
|
||||
- 提供“恢复默认外观”操作。
|
||||
|
||||
### FR-9 显示、隐藏与退出
|
||||
|
||||
- 提供独立于总开关的“暂时隐藏”动作:隐藏宠物窗口但保留启用状态,可通过托盘菜单或
|
||||
设置重新显示。
|
||||
- 托盘菜单新增“显示/隐藏桌面宠物”入口,随当前可见性变化文案。
|
||||
- 应用退出时宠物窗口一并关闭,不残留独立进程。
|
||||
|
||||
### FR-10 键盘可达性
|
||||
|
||||
- 宠物窗口本身可通过系统级窗口切换(如 Alt+Tab)取得焦点,取得焦点后可用键盘打开快速
|
||||
起草弹出框、在状态镜面和提示之间移动,并可用 Escape 关闭弹出框。
|
||||
- 所有交互提供可访问名称和状态说明,不仅依赖动画或颜色表达状态。
|
||||
|
||||
### FR-11 与现有能力的边界
|
||||
|
||||
- 快速起草和拖放暂存最终使用主窗口现有的会话、附件、模型和 Ask/Execute 选择逻辑,不
|
||||
新增第二套发送或授权路径。
|
||||
- 状态镜面和气泡的任务状态数据来自现有 Task 与 Conversation 记录,不建立独立存储、不
|
||||
与 Task Center 的状态定义产生分歧。
|
||||
- 桌面宠物不改变现有全局快捷键、托盘菜单既有条目或主窗口默认行为。
|
||||
|
||||
## 5. 产品验收
|
||||
|
||||
- 全新安装和已升级用户默认看不到桌面宠物;显式启用后立即出现在预期位置。
|
||||
- 关闭总开关后桌面上不再有任何宠物窗口或残留提示,重新开启后恢复此前的位置和外观。
|
||||
- 通过快速起草或拖放提交的内容,在主窗口对应会话的输入区中可见为未发送草稿,用户需要
|
||||
自行确认发送;关闭 GoodBuddy 或取消草稿不会产生任何任务或消息记录。
|
||||
- 状态镜面呈现的状态与 Task Center 中对应任务的状态一致;两者出现分歧即视为缺陷。
|
||||
- 同一任务完成或失败事件不会同时产生宠物气泡和原生系统通知。
|
||||
- 安静模式开启时不出现气泡,但状态镜面外观仍随任务状态变化。
|
||||
- 键盘用户无需鼠标即可打开快速起草、查看状态并关闭弹出框。
|
||||
- 中英文文案、深浅主题、多显示器和 DPI 缩放下宠物窗口位置和内容均正确显示。
|
||||
|
||||
## 6. 明确限制
|
||||
|
||||
- 桌面宠物不提供在其自身窗口内完成整段对话或查看历史消息的能力;查看完整回复必须进入
|
||||
主窗口。
|
||||
- 桌面宠物不支持多实例、多角色同屏或角色之间的模拟互动。
|
||||
- 透明背景效果依赖操作系统合成器;在不支持透明合成的 Linux 桌面环境下,宠物窗口可能
|
||||
退化为不透明的小型窗口,具体行为见[技术设计](./technical-design.md)。
|
||||
@@ -0,0 +1,335 @@
|
||||
# 桌面宠物技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| UI 设计 | [UI 设计](./ui-design.md) |
|
||||
|
||||
## 1. 进程与窗口边界
|
||||
|
||||
```text
|
||||
Main
|
||||
-> createMainWindow() (既有,src/main/window.ts)
|
||||
-> createPetWindow() (新增)
|
||||
-> 独立 BrowserWindow:无边框、透明、置顶、不入任务栏
|
||||
-> 独立 preload:src/preload/pet.ts(新增,窄 API)
|
||||
-> 加载独立渲染入口:src/renderer/pet.html + src/renderer/src/pet/
|
||||
Renderer(主窗口,既有)
|
||||
-> window.goodbuddy(既有 DesktopApi,不变)
|
||||
Renderer(宠物窗口,新增)
|
||||
-> window.goodbuddyPet(新增窄 API,与既有 window.goodbuddy 完全隔离)
|
||||
```
|
||||
|
||||
宠物窗口是与主窗口平级的第二个 `BrowserWindow`,不是主窗口的子窗口、不是 `WebContentsView`
|
||||
叠加层。理由:
|
||||
|
||||
- 主窗口现有 `setWindowOpenHandler` 拒绝一切子窗口(`src/main/window.ts`),沿用该策略
|
||||
比引入例外更简单。
|
||||
- 宠物窗口需要独立于主窗口的显示/隐藏、位置和置顶状态,作为子窗口会继承父窗口的显示
|
||||
状态,不满足“主窗口隐藏时宠物仍可见”的产品要求。
|
||||
|
||||
不复用主窗口的 preload 和渲染入口:主窗口 preload 暴露的 `DesktopApi` 面积很大(对话、
|
||||
知识库、设备共享等)。给宠物窗口加载同一 preload 会让一个新增的、长期运行、暴露给
|
||||
远程可控网页内容风险更低但攻击面更广的窗口拥有不必要的能力。新增窄 preload 遵循仓库
|
||||
现有的“只暴露显式方法”约束。
|
||||
|
||||
## 2. 模块职责
|
||||
|
||||
| 模块 | 位置 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| `PetWindowService` | `src/main/pet/pet-window-service.ts`(新增) | 创建/销毁宠物窗口、位置持久化与越界校验、置顶与贴边、显示/隐藏 |
|
||||
| `PetSettingsStore` | `src/main/pet/pet-settings-store.ts`(新增) | 总开关、外观、位置、安静模式的持久化,遵循既有 settings-file-utils 模式 |
|
||||
| `PetStatusProjector` | `src/main/pet/pet-status-projector.ts`(新增) | 从既有 Task 查询结果计算状态镜面取值,不引入新状态源 |
|
||||
| `PetDraftBridge` | `src/main/pet/pet-draft-bridge.ts`(新增) | 把宠物提交的文本/附件转交主窗口渲染进程,驱动会话激活与草稿写入 |
|
||||
| `PetNotificationCoordinator` | `src/main/pet/pet-notification-coordinator.ts`(新增) | 与既有 `desktop-notification.ts` 协商同一事件只走一条提示路径 |
|
||||
| 既有 `context-manager.ts` | 不变 | 宠物附件校验复用其 `selectFiles`/校验规则,不新增第二套限制 |
|
||||
| 既有 `assistant-database.ts` / Task 查询 | 不变 | 状态镜面查询的唯一数据来源 |
|
||||
|
||||
`PetStatusProjector` 只读取既有 Task 查询结果并按
|
||||
[功能逻辑第 3 节](./logic-design.md#3-状态镜面优先级)的优先级归并,不写入、不缓存跨进程
|
||||
重启的状态;重启后状态镜面从当前 Task 集合重新计算。
|
||||
|
||||
## 3. 宠物窗口创建
|
||||
|
||||
```ts
|
||||
// src/main/pet/pet-window-service.ts(示意)
|
||||
function createPetWindow(): BrowserWindow {
|
||||
return new BrowserWindow({
|
||||
width: PET_SIZE.width,
|
||||
height: PET_SIZE.height,
|
||||
show: false,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
skipTaskbar: true,
|
||||
alwaysOnTop: true, // 由“始终置顶”设置决定实际值
|
||||
hasShadow: false,
|
||||
backgroundColor: '#00000000',
|
||||
webPreferences: {
|
||||
preload: join(currentDirectory, '../preload/pet.cjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: true
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
- `transparent: true` 依赖操作系统合成器。按 PRD 6 节明确限制,在不支持透明合成的
|
||||
Linux 桌面环境(例如未运行合成器的部分窗口管理器)下,Electron/Chromium 会把窗口
|
||||
渲染为不透明;`PetWindowService` 不检测合成器能力,只在渲染进程内把背景色设置为
|
||||
当前主题的画布色作为不透明回退,保证内容仍然可读,不尝试探测或声明支持特定合成器。
|
||||
- `alwaysOnTop` 的实际值由“始终置顶”Switch 驱动,通过 `window.setAlwaysOnTop(bool)`
|
||||
在设置变更时更新,不需要重建窗口。
|
||||
- 复用既有 `resolveWindowIcon`/任务栏排除逻辑作为参考,但宠物窗口 `skipTaskbar: true`
|
||||
固定为真,不提供用户设置。
|
||||
- 宠物窗口不设置 `parent`,与主窗口是同级独立窗口;应用单实例锁和 `second-instance`
|
||||
处理(`src/main/index.ts`)不需要改动,因为宠物窗口的存在与否由 `PetWindowService`
|
||||
内部状态决定,而不是第二个 Electron 实例。
|
||||
|
||||
## 4. 拖放实现
|
||||
|
||||
Electron 渲染进程原生支持 HTML5 拖放;宠物渲染进程监听 `dragenter`/`dragover`/`drop`
|
||||
即可获得视觉反馈和释放事件,不需要 Main 参与拖放事件本身。文件路径解析复用既有
|
||||
`webUtils.getPathForFile`(已用于 `src/preload/index.ts` 的知识库拖放导入):
|
||||
|
||||
```ts
|
||||
// src/preload/pet.ts(示意)
|
||||
import { contextBridge, ipcRenderer, webUtils } from 'electron'
|
||||
|
||||
contextBridge.exposeInMainWorld('goodbuddyPet', {
|
||||
submitDraft: (input: PetDraftSubmission) =>
|
||||
ipcRenderer.invoke(petIpcChannels.submitDraft, input),
|
||||
resolveDroppedPaths: (files: File[]) =>
|
||||
files.map((file) => webUtils.getPathForFile(file)).filter(Boolean),
|
||||
// ...状态订阅、设置读写等方法
|
||||
})
|
||||
```
|
||||
|
||||
拖入的文本(非文件)通过 `dataTransfer.getData('text/plain')` 直接读取,随普通文本
|
||||
字段一起提交,不经过路径解析。
|
||||
|
||||
宠物渲染进程把解析出的路径和文本打包为 `PetDraftSubmission`,通过 IPC 交给 Main;Main
|
||||
侧的路径校验、类型判断、大小限制和读取,全部复用既有 `ContextManager` 的规则(`src/main/context-manager.ts`
|
||||
中的 `supportedExtensions`、`supportedImageExtensions`、`supportedDocumentExtensions`、
|
||||
`maximumContextBytes`、`maximumAttachmentsPerMessage`),不新增校验逻辑。`ContextManager`
|
||||
目前的公开入口是围绕原生对话框(`selectFiles`)设计的;实现时需要新增一个接受任意
|
||||
canonical 路径数组的方法(例如 `stageFromPaths`),内部复用现有扩展名/大小/文档解析
|
||||
分支,而不是复制一份判断逻辑。文件夹路径(`fs.stat` 判定为目录)在该方法入口即被拒绝,
|
||||
不递归遍历。
|
||||
|
||||
## 5. 快速起草提交与主窗口联动
|
||||
|
||||
```text
|
||||
宠物渲染进程 submitDraft(input)
|
||||
-> Main: PetDraftBridge.handleSubmit(input)
|
||||
-> ContextManager.stageFromPaths(input.filePaths) (复用既有校验)
|
||||
-> 若主窗口不存在:createMainWindow() + loadMainWindow()(复用既有函数)
|
||||
-> showWindow(mainWindow)(既有函数)
|
||||
-> mainWindow.webContents.send(petIpcChannels.draftReceived, {
|
||||
targetConversationId, text, stagedContextIds
|
||||
})
|
||||
-> 主窗口渲染进程(App.tsx 新增一个事件监听,与既有 conversation:new 监听模式一致)
|
||||
-> 若 targetConversationId 无效或已删除:回退为新建会话(既有新建会话路径)
|
||||
-> 激活目标会话、把 text 写入该会话输入框状态、把 stagedContextIds 追加到该
|
||||
会话的附件状态(既有 App.tsx 附件状态更新路径)
|
||||
-> 聚焦输入框
|
||||
```
|
||||
|
||||
- 草稿只在主窗口渲染进程内变成“输入框状态”和“待发送附件列表”,与用户手动打字/手动
|
||||
点击附件按钮产生的状态是同一份 React state,不建立并行的草稿存储。这直接满足
|
||||
`INV-1`:宠物不产生消息、不产生 Task。
|
||||
- `stagedContextIds` 复用现有 `context:*` IPC 产生的 Context ID;主窗口把这些 ID 加入
|
||||
附件列表的方式,与用户通过“附加文件”按钮选择文件后的状态更新完全一致。
|
||||
- 若提交发生时主窗口曾被销毁(用户从未打开过,或者提前研究表明当前架构不销毁主窗口
|
||||
只隐藏),`PetDraftBridge` 调用既有 `createMainWindow`/`loadMainWindow`,等待
|
||||
`ready-to-show` 完成后再发送 `draftReceived`,避免竞态。
|
||||
|
||||
## 6. IPC 边界
|
||||
|
||||
新增独立 IPC 通道命名空间 `pet:*`,与既有 `ipcChannels` 并列定义在
|
||||
`src/shared/ipc-channels.ts`:
|
||||
|
||||
```text
|
||||
pet:get-snapshot (设置 + 当前状态镜面取值 + 可见性)
|
||||
pet:update-settings (总开关、外观、贴边、置顶、安静模式)
|
||||
pet:submit-draft (快速起草/拖放提交)
|
||||
pet:dismiss-bubble (关闭完成/失败气泡)
|
||||
pet:open-task (点击状态镜面/气泡,打开 Task Center 对应任务)
|
||||
pet:toggle-visibility (托盘“隐藏/显示桌面宠物”)
|
||||
```
|
||||
|
||||
- 所有输入使用 `src/shared/pet-contracts.ts`(新增)中的 Zod schema 校验,与既有约定
|
||||
一致。
|
||||
- `trusted-ipc-sender.ts` 的 `assertTrustedSender` 按窗口实例参数化使用;宠物窗口的
|
||||
IPC handler 校验 `event.sender === petWindow.webContents`,不能用主窗口的 trusted
|
||||
sender 校验函数验证宠物窗口的请求,也不能反过来。
|
||||
- 宠物 preload 不暴露任何既有 `DesktopApi` 方法;需要的能力(提交草稿、读取/写入宠物
|
||||
设置、订阅状态镜面)都通过 `pet:*` 通道单独暴露为 `window.goodbuddyPet` 的具体方法。
|
||||
- Main 侧广播状态镜面变化时,只向宠物窗口发送派生后的枚举值(第 3 节的六种状态之一)
|
||||
和最小必要的任务标题/ID,不发送完整 Task 或 Conversation 对象。
|
||||
|
||||
## 7. 持久化
|
||||
|
||||
新增设置文件,遵循既有 `settings-file-utils.ts` 的原子写入、版本校验和损坏隔离模式,
|
||||
不复用 `ApplicationSettingsStore` 或 `ShortcutSettingsStore` 的 schema:
|
||||
|
||||
```text
|
||||
pet-settings.json
|
||||
{
|
||||
version: 1,
|
||||
enabled: boolean,
|
||||
quietMode: boolean,
|
||||
alwaysOnTop: boolean,
|
||||
snapToEdge: boolean,
|
||||
appearance: { character: string, size: 'small'|'medium'|'large',
|
||||
opacity: number, motion: 'off'|'reduced'|'full' },
|
||||
position: { displayId: number, x: number, y: number } | null
|
||||
}
|
||||
```
|
||||
|
||||
- `position` 为 `null` 表示尚未被用户拖动过,使用默认位置计算规则(第 8 节)。
|
||||
- `displayId` 使用 Electron `screen.getAllDisplays()` 返回的 `id`;加载时按第 8 节校验,
|
||||
不假设跨重启保持稳定的显示器 id 一定仍然存在。
|
||||
- 独立 schema 版本号(从 1 开始),与主设置版本号互不影响;未来变更只需要迁移这一个
|
||||
文件。
|
||||
- 是否需要独立数据库表:不需要。宠物没有需要查询、筛选或跨设备同步的历史数据,一个
|
||||
JSON 文件足够,符合仓库 KISS/YAGNI 约束。
|
||||
|
||||
## 8. 位置与多显示器校验
|
||||
|
||||
```ts
|
||||
function resolvePetPosition(
|
||||
saved: PetPosition | null,
|
||||
displays: Display[]
|
||||
): { x: number; y: number } {
|
||||
if (saved) {
|
||||
const display = displays.find((d) => d.id === saved.displayId)
|
||||
if (display && pointWithinWorkArea(saved, display.workArea)) {
|
||||
return saved
|
||||
}
|
||||
}
|
||||
const primary = screen.getPrimaryDisplay()
|
||||
return defaultPositionWithin(primary.workArea) // 例如右下角,留出安全边距
|
||||
}
|
||||
```
|
||||
|
||||
- 该函数在宠物窗口创建时以及 `display-added`/`display-removed`/`display-metrics-changed`
|
||||
事件触发时调用;越界立即调用 `window.setPosition` 校正,不等待用户下一次拖动。
|
||||
- 用户拖动结束(`moved` 事件)时把当前 `getBounds()` 和 `screen.getDisplayNearestPoint`
|
||||
得到的 `displayId` 写入 `PetSettingsStore`,去抖后原子写入,避免拖动过程中高频写盘。
|
||||
- 贴边吸附在渲染进程拖动过程中做视觉预判(不移动实际窗口,只显示吸附提示),在
|
||||
`moved` 事件的最终位置上由 Main 一次性计算是否落入吸附阈值并调用 `setPosition`
|
||||
吸附到边缘,阈值和默认位置的具体像素值作为实现细节调优,不在此固定。
|
||||
|
||||
## 9. 状态镜面查询
|
||||
|
||||
`PetStatusProjector` 不新增事件源,而是订阅既有 Task 变化通知(主窗口渲染进程已经
|
||||
用于更新 Task Center 和会话内任务卡片的同一套 Main → Renderer 事件)在 Main 侧的
|
||||
对应版本,或者在 Main 侧直接查询 `assistant-database.ts` 暴露的 Task 读取方法。具体
|
||||
选择两者之一属于实现阶段的技术选择,约束条件是:
|
||||
|
||||
- 不引入宠物专属的任务状态表或缓存;查询/订阅的结果必须与 Task Center 在同一时刻
|
||||
展示的状态一致。
|
||||
- 查询开销必须是有界的(只统计活跃与最近终态 Task 的数量和状态,不拉取消息正文)。
|
||||
- Runtime/网络不可达导致查询失败时返回 `unavailable`,不静默返回 `idle`。
|
||||
|
||||
“最近完成”“最近失败”窗口时长和“已查看”判定:复用 Task 已有的已读/查看时间戳字段
|
||||
(若既有模型已提供);若当前模型没有这类字段,实现时以“该 Task 终态发生后,用户是否
|
||||
已经点击过对应的宠物气泡或状态镜面,或已经在 Task Center 中打开过该 Task”作为已查看
|
||||
判定,窗口时长建议与现有原生通知的展示逻辑保持同量级(分钟级),具体数值在实现时
|
||||
根据既有 Task 事件粒度确定。
|
||||
|
||||
## 10. 提示与原生通知协商
|
||||
|
||||
`PetNotificationCoordinator` 与 `desktop-notification.ts` 的 `showDesktopNotificationWhenUnfocused`
|
||||
共享同一个“主窗口是否可见并聚焦”判定:
|
||||
|
||||
```ts
|
||||
function shouldSuppressBecauseMainWindowVisible(mainWindow: BrowserWindow): boolean {
|
||||
return !mainWindow.isDestroyed() && mainWindow.isFocused()
|
||||
}
|
||||
```
|
||||
|
||||
- 现有 `showDesktopNotificationWhenUnfocused` 已经使用等价条件(`window.isFocused()`)
|
||||
决定是否弹出原生通知;`PetNotificationCoordinator` 复用同一判定函数(如需要,提取为
|
||||
共享工具),保证两者的“可见性”定义永远一致,不出现原生通知判定为可见但宠物判定为
|
||||
不可见(或反之)导致同一事件同时出现两条提示或完全没有提示的情况。
|
||||
- 实现层面:调用触发通知的代码路径在决定“原生通知 vs 宠物气泡”时二选一,而不是
|
||||
两者分别独立判断后都可能触发;例如由现有触发点先判断安静模式和宠物启用状态,
|
||||
为真则调用 `PetNotificationCoordinator`,否则调用现有 `showDesktopNotificationWhenUnfocused`。
|
||||
- 安静模式开启时,两者都不触发;状态镜面仍照常更新(状态镜面更新走独立的
|
||||
`pet:get-snapshot`/推送通道,不经过通知协商)。
|
||||
|
||||
## 11. 与既有模块集成
|
||||
|
||||
- 快速起草和拖放最终只调用主窗口渲染进程已有的“设置输入框文本”“追加附件”“激活
|
||||
会话”“创建新会话”状态更新函数,不新建与 Ask/Execute、模型选择、Runtime 选择相关
|
||||
的任何路径。
|
||||
- 点击状态镜面/气泡打开 Task Center 复用现有 Task Center 导航(既有 IPC/路由,与托盘
|
||||
“新建对话”发送 `conversation:new` 事件属于同一类模式:Main 发送一个事件,主窗口
|
||||
渲染进程消费并完成导航)。
|
||||
- 托盘菜单新增项复用现有 `Menu.buildFromTemplate` 结构(`src/main/index.ts` 的
|
||||
`buildTray`),插入一个受当前可见性状态驱动的条目,不重写整个菜单构建函数。
|
||||
- 全局快捷键(`src/shared/shortcut.ts`、`shortcut-settings-service.ts`)不受影响,
|
||||
桌面宠物不注册新的全局快捷键,因为激活方式是点击窗口本身或系统级窗口切换,不需要
|
||||
额外抢占全局按键空间。
|
||||
|
||||
## 12. 生命周期
|
||||
|
||||
- 应用启动读取 `PetSettingsStore`;`enabled: true` 时在既有服务和 IPC 注册完成后创建
|
||||
宠物窗口(与主窗口创建顺序一致,不阻塞主窗口 `ready-to-show`)。
|
||||
- 宠物窗口创建失败(例如极端情况下操作系统拒绝创建透明窗口):记录原因,不阻塞主窗口
|
||||
启动,托盘“显示桌面宠物”下次点击时重试创建。
|
||||
- 应用退出(`before-quit`/既有 `isQuitting` 标记)时,宠物窗口与主窗口一并销毁,不需要
|
||||
独立的关闭确认或延迟清理,因为宠物窗口没有正在进行的网络连接或未保存状态:草稿已经
|
||||
转交主窗口的那一刻起就属于主窗口生命周期。
|
||||
- “暂时隐藏”只调用 `window.hide()`,不销毁窗口实例,重新显示直接 `showWindow` 复用
|
||||
既有实例,避免频繁创建/销毁透明窗口带来的闪烁。
|
||||
|
||||
## 13. 打包
|
||||
|
||||
- 宠物渲染入口(HTML、内置角色素材)加入 `electron.vite.config.ts` 的 renderer 构建
|
||||
输入(现有配置已有 `input` 多入口结构,参考现有主渲染入口的声明方式新增一项)。
|
||||
- 内置角色素材(数量有限的静态图片/精灵图)作为打包资源加入 `package.json` 的 `files`/
|
||||
`extraResources`,不通过运行时网络下载,符合仓库“不新增远程资源请求”的字体/资源
|
||||
约束的同一精神。
|
||||
- 新增 preload 产物(`pet.cjs`)需要加入 `electron.vite.config.ts` 的 main/preload 构建
|
||||
`entryFileNames` 规则,与现有主 preload 产物并列。
|
||||
- 六个标准打包目标(Windows/macOS/Linux × x64/arm64)均需验证宠物窗口可创建、透明或
|
||||
回退不透明背景符合第 3 节预期;LoongArch 预览目标不单独验证,遵循仓库现有策略。
|
||||
|
||||
## 14. 验证策略
|
||||
|
||||
### 14.1 自动化
|
||||
|
||||
- `PetSettingsStore` 的默认值、原子写入、损坏隔离和版本校验(沿用
|
||||
`settings-file-utils.ts` 现有测试模式)。
|
||||
- `resolvePetPosition` 在显示器存在/不存在/坐标越界三种输入下的返回值。
|
||||
- `PetStatusProjector` 按第 3 节优先级表在各种 Task 组合输入下的输出。
|
||||
- `PetNotificationCoordinator` 与 `desktop-notification.ts` 共享判定函数在“可见并聚焦”
|
||||
“不可见”“安静模式开启”组合下只触发一条路径或都不触发。
|
||||
- `ContextManager.stageFromPaths`(新增方法)对合法文件、超限文件、不支持类型、文件夹
|
||||
路径的分支覆盖,复用现有 `context-manager` 测试基础设施。
|
||||
- `pet:*` IPC handler 的 trusted sender、schema 校验和错误码。
|
||||
- 快速起草提交后主窗口渲染进程状态更新的单元测试:目标会话激活、文本写入、附件追加、
|
||||
草稿被丢弃时不产生 Task 或消息记录。
|
||||
|
||||
### 14.2 真实环境
|
||||
|
||||
- Windows、macOS、Linux 至少各一次验证宠物窗口创建、拖动、贴边吸附、透明或回退背景的
|
||||
实际渲染效果。
|
||||
- 真实拖放一个合法文件、一个超限文件、一个文件夹到宠物窗口,确认 UI 设计第 4.3 节的
|
||||
反馈和最终主窗口状态符合预期。
|
||||
- 真实触发一次任务完成和一次任务失败,在“主窗口隐藏”“主窗口可见”“安静模式开启”
|
||||
三种组合下确认气泡和原生通知的出现符合第 10 节的互斥规则。
|
||||
- 多显示器插拔场景下确认位置回退逻辑(断开记忆显示器后宠物仍可见)。
|
||||
- 打包后的应用在六个标准目标上完成启用、拖动、快速起草、退出流程的一次冒烟验证。
|
||||
|
||||
设计文档完成不代表功能实施或上述验证通过。
|
||||
@@ -0,0 +1,207 @@
|
||||
# 桌面宠物 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义设置中的“桌面宠物”页面和独立宠物窗口的界面。所有颜色、间距、字体、通知、
|
||||
Switch/Checkbox、焦点和响应式规则以全局设计系统为准;宠物窗口是应用内唯一不使用标准
|
||||
`PageShell` 的持久界面,其例外范围在第 4 节明确限定。
|
||||
|
||||
## 1. 设置页面信息架构
|
||||
|
||||
设置中心新增“桌面宠物”一级分类,使用 `standard` PageShell,不使用页签,按以下顺序
|
||||
纵向排列:
|
||||
|
||||
```text
|
||||
桌面宠物
|
||||
在桌面上保留一个轻量入口,随时起草消息、拖入文件或查看任务状态
|
||||
|
||||
启用桌面宠物 [Switch 关]
|
||||
关闭后不在桌面上显示任何宠物窗口或提示
|
||||
|
||||
—— 以下设置仅在启用后可编辑,保持可见但降低强调度 ——
|
||||
|
||||
外观
|
||||
角色 [分段选择:三到四个内置角色缩略图]
|
||||
尺寸 [滑块 小 · 中 · 大]
|
||||
透明度 [滑块]
|
||||
动画强度 [滑块 关闭 · 轻量 · 完整]
|
||||
[恢复默认外观]
|
||||
|
||||
位置与窗口
|
||||
始终置顶 [Switch 开]
|
||||
贴边吸附 [Switch 开]
|
||||
|
||||
提示
|
||||
安静模式(不显示完成 / 失败气泡) [Switch 关]
|
||||
```
|
||||
|
||||
- 总开关是本页唯一决定宠物窗口存在与否的控件,符合 `INV-3`;其余控件在总开关关闭时
|
||||
保持可见但不可交互,说明文字保持“关闭后不在桌面上显示任何宠物窗口或提示”。
|
||||
- 二元持久状态一律使用共享 Switch;角色选择使用共享 `SegmentedControl`(3-4 个内置
|
||||
角色,符合数量上限);尺寸、透明度和动画强度使用标准滑块控件并显示当前取值。
|
||||
- “恢复默认外观”只重置本区域三项外观设置,不改变总开关、位置或安静模式,与
|
||||
[功能逻辑](./logic-design.md) 中“恢复默认外观”的范围一致。
|
||||
- 页面不提供“立即显示 / 隐藏”按钮:暂时隐藏的入口固定放在托盘菜单,避免在设置页和
|
||||
托盘中出现两个含义相近但状态不同步的控件。
|
||||
|
||||
## 2. 首次启用与关闭反馈
|
||||
|
||||
- 开启总开关后宠物窗口在默认位置出现,不产生任何应用通知或原生通知,因为这是用户刚
|
||||
完成的显式操作,结果已经可见。
|
||||
- 关闭总开关后宠物窗口和任何气泡立即消失;设置页本身不产生额外通知。
|
||||
- 外观、位置相关 Switch 和滑块的变更立即生效并持久化,失败时通过应用通知报告,不静默
|
||||
丢弃用户输入。
|
||||
|
||||
## 3. 托盘入口
|
||||
|
||||
- 托盘菜单在“新建对话”与“设置”之间新增一项,文案随当前可见性变化:
|
||||
- 已启用且可见:“隐藏桌面宠物”。
|
||||
- 已启用但隐藏:“显示桌面宠物”。
|
||||
- 未启用:“显示桌面宠物”,点击后等同于在设置中开启总开关并显示窗口。
|
||||
- 该菜单项不复制设置页的开关语义,只切换可见性;未启用状态下点击才会连带开启总开关,
|
||||
行为需要在可访问名称或菜单说明中明确,避免用户误以为它只是显示/隐藏。
|
||||
|
||||
## 4. 宠物窗口
|
||||
|
||||
宠物窗口是无边框、透明背景、不可缩放的持久悬浮窗口,不使用 `PageShell`、不属于标准
|
||||
一级页面,因此以下规则专属于该窗口:
|
||||
|
||||
```text
|
||||
┌───────────┐
|
||||
│ 角色 │ ← 状态镜面:角色外观随任务状态切换
|
||||
│ (idle) │
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
- 窗口本身没有标题栏、边框或系统窗口控件;关闭宠物只能通过设置总开关、托盘“隐藏桌面
|
||||
宠物”或退出应用完成,窗口自身不提供关闭按钮,避免与“暂时隐藏”“关闭功能”两种含义
|
||||
混淆。
|
||||
- 窗口整体可拖动;拖动过程中显示当前吸附意图(开启贴边时,接近边缘会有轻微吸附反馈)。
|
||||
- 窗口右键菜单提供“快速起草”“暂时隐藏”“打开设置”“退出 GoodBuddy”,与托盘菜单保持
|
||||
同一套文案。
|
||||
|
||||
### 4.1 状态镜面外观
|
||||
|
||||
宠物角色在以下六种取值间切换,取值定义见[功能逻辑](./logic-design.md) 第 3 节:
|
||||
|
||||
| 状态 | 视觉表达 | 可访问名称 |
|
||||
| --- | --- | --- |
|
||||
| `unavailable` | 角色显示为暗淡的问号叠加 | “任务状态当前不可用” |
|
||||
| `idle` | 角色静止或轻微待机动作 | “没有进行中的任务” |
|
||||
| `running` | 角色显示持续的工作动作 | “有任务正在执行” |
|
||||
| `waiting_approval` | 角色举牌或举手动作,带醒目但非警示色的强调 | “有任务等待你的确认” |
|
||||
| `completed_recent` | 角色短暂展示完成动作后回到待机 | “有任务刚刚完成” |
|
||||
| `failed_recent` | 角色显示明显区别于完成的姿态,并叠加警示色边缘 | “有任务失败,需要查看” |
|
||||
|
||||
- 状态表达必须同时使用动作/姿态和可访问名称两种信号,不能只依赖颜色区分
|
||||
`completed_recent` 与 `failed_recent`。
|
||||
- `prefers-reduced-motion` 或用户将动画强度设为“关闭”时,状态切换只改变静态姿态和颜色
|
||||
强调,不播放位移或缩放动画。
|
||||
- 点击状态镜面区域(角色本体)触发 US-D4:显示并聚焦主窗口,Task Center 定位到触发
|
||||
当前状态的任务;`idle` 和 `unavailable` 状态下点击等同于显示并聚焦主窗口,不做定位。
|
||||
|
||||
### 4.2 快速起草弹出框
|
||||
|
||||
点击宠物打开的弹出框锚定在宠物窗口旁(自动避开屏幕边缘),结构如下:
|
||||
|
||||
```text
|
||||
发送到:[当前会话名称 v]
|
||||
|
||||
[多行文本输入,占位文案“输入消息,或拖入文件…”]
|
||||
|
||||
[📎 附加文件] [已附加:contract.docx ×]
|
||||
|
||||
[取消] [提交]
|
||||
```
|
||||
|
||||
- “发送到”是一个富信息单选菜单,选项为当前活跃会话与“新建对话”,遵循全局设计系统
|
||||
第 6.11 节的单选选择器规则;默认选中当前活跃会话。
|
||||
- 文本输入支持多行、`Ctrl+V` 粘贴图片和文字,与主窗口聊天输入框的粘贴行为一致。
|
||||
- 附加文件按钮打开原生文件选择;已附加文件以行内标签形式展示,可单独移除。
|
||||
- 拒绝的文件(超限或不支持)在附件区下方以就地错误文字展示,不使用应用级通知,因为
|
||||
这是与当前操作直接绑定的字段级反馈。
|
||||
- “提交”把文本和附件转交目标会话的输入区草稿,随后关闭弹出框;提交过程中禁用重复
|
||||
点击并显示进行中状态,成功后不额外弹出通知(主窗口的显现和草稿本身就是反馈)。
|
||||
- “取消”或点击弹出框外部或按 Escape 关闭弹出框并丢弃草稿,焦点返回宠物窗口。
|
||||
- 弹出框打开时焦点进入文本输入;Tab 顺序为文本输入 → 附加文件 → 已附加文件的移除
|
||||
按钮 → 取消 → 提交;关闭后焦点回到宠物窗口本体。
|
||||
|
||||
### 4.3 拖放反馈
|
||||
|
||||
- 文件或文字拖入宠物窗口范围时,窗口整体出现可放置的高亮边框和轻微放大反馈。
|
||||
- 释放后立即按第 4.2 节“提交”的相同路径处理,不额外弹出确认框;这保持“拖动=起草并
|
||||
发送到会话输入区(而非立即发送消息)”的一致语义。
|
||||
- 拖入文件夹或全部文件超限时,宠物短暂显示拒绝反馈(边框变为警示色并伴随简短文字
|
||||
提示,例如“不支持文件夹”),不打开主窗口、不产生暂存。
|
||||
- 部分文件超限、部分合法时,合法文件正常处理,超限文件的名称通过一条会在数秒后自动
|
||||
消失的就地提示列出,不使用应用级通知(拖放是即时操作,反馈应贴近操作发生的位置)。
|
||||
|
||||
### 4.4 完成 / 失败气泡
|
||||
|
||||
```text
|
||||
┌─────────────────────────────┐
|
||||
│ ✓ 已完成 │
|
||||
│ 合同风险检查 │
|
||||
│ [×] │
|
||||
└─────────────────────────────┘
|
||||
▲
|
||||
(角色)
|
||||
```
|
||||
|
||||
- 气泡锚定在宠物窗口旁,方向随宠物在屏幕上的位置自动避开边缘。
|
||||
- 完成气泡在约 4.5 秒后自动消失,与全局应用通知的成功/信息时长一致;失败气泡保持可见
|
||||
直到用户点击查看或点击关闭按钮。
|
||||
- 气泡本体可点击,等同于点击状态镜面:显示并聚焦主窗口并定位到对应任务。
|
||||
- 关闭按钮 `[×]` 有独立可访问名称(“关闭提示”),不与气泡主体的点击目标冲突:点击
|
||||
关闭按钮只关闭气泡,不导航到主窗口。
|
||||
- 气泡使用 `status`/polite 实时区域播报完成,失败气泡使用 `alert`/assertive,与全局
|
||||
设计系统的通知语义保持一致,即使气泡本身不是应用通知视口的一部分。
|
||||
|
||||
## 5. 与应用通知视口的关系
|
||||
|
||||
- 宠物气泡是应用通知视口的窗口外等价物,只在主窗口不可见/未聚焦且安静模式关闭时出现;
|
||||
同一事件不会同时进入应用通知视口和宠物气泡,具体归并规则见
|
||||
[功能逻辑](./logic-design.md) 第 4 节。
|
||||
- 快速起草和拖放产生的字段级错误(附件超限、不支持类型)始终就地展示在宠物窗口或弹出
|
||||
框内,不进入应用通知视口,保持“就地错误”与“全局通知”分工一致。
|
||||
|
||||
## 6. 无障碍
|
||||
|
||||
- 状态镜面、气泡和弹出框内的所有交互元素都有持久可访问名称,不依赖占位文案代替标签。
|
||||
- 宠物窗口可通过系统级窗口切换获得焦点;获得焦点后:
|
||||
- 预期的激活键(例如 Enter 或 Space)打开快速起草弹出框。
|
||||
- 弹出框内 Tab 顺序遵循第 4.2 节。
|
||||
- Escape 关闭当前打开的弹出框或气泡,焦点回到宠物窗口本体。
|
||||
- 状态镜面的状态变化通过 `status`/polite 实时区域播报简短说明,避免因动画本身无法被
|
||||
屏幕阅读器感知而丢失信息;不对每次动画帧重复播报。
|
||||
- 焦点环、危险色使用和触控目标尺寸遵循全局设计系统第 11 节的通用要求。
|
||||
|
||||
## 7. 响应式与多显示器
|
||||
|
||||
- 宠物窗口尺寸不随主窗口大小变化;用户通过设置中的“尺寸”滑块单独控制。
|
||||
- 宠物窗口位置按显示器分别记忆;显示器不可用时的回退规则见
|
||||
[功能逻辑](./logic-design.md) 第 7 节,界面表现为宠物出现在主显示器的默认可见位置,
|
||||
不出现在屏幕之外或不可见状态。
|
||||
- 弹出框和气泡的锚定方向根据宠物当前位置与屏幕边缘的距离自动选择展开方向(上/下/左/
|
||||
右),确保始终完整可见,不被裁切到屏幕外。
|
||||
|
||||
## 8. 深浅主题
|
||||
|
||||
- 弹出框、气泡和设置页控件复用全局语义令牌,不新增页面专属颜色。
|
||||
- 宠物角色素材本身分别提供适配浅色和深色桌面背景的版本,避免透明背景下角色轮廓在深色
|
||||
桌面壁纸上难以辨认;具体资源交付方式属于技术设计范畴。
|
||||
|
||||
## 9. 固定词汇
|
||||
|
||||
使用“桌面宠物”“启用桌面宠物”“暂时隐藏”“快速起草”“安静模式”“状态镜面”,定义见
|
||||
[README](./README.md) 术语表。不使用“挂件”“桌宠助手”或其他未在术语表中定义的别名,
|
||||
保持中文文案统一。
|
||||
@@ -0,0 +1,255 @@
|
||||
# 桌面宠物 User Stories
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
## 角色
|
||||
|
||||
- **日常用户**:在多任务、多显示器场景下使用 GoodBuddy 处理日常工作。
|
||||
- **首次体验用户**:刚升级或安装 GoodBuddy,尚未接触过桌面宠物功能。
|
||||
- **键盘用户**:主要通过键盘操作,可能同时使用屏幕阅读器。
|
||||
- **多显示器用户**:使用两个或更多显示器,可能随时插拔外接显示器。
|
||||
|
||||
## US-A 首次启用
|
||||
|
||||
### US-A1 默认不出现
|
||||
|
||||
**Given** 用户全新安装或刚升级 GoodBuddy
|
||||
**When** 用户尚未进入“桌面宠物”设置
|
||||
**Then** 桌面上不出现任何宠物窗口,托盘菜单显示“显示桌面宠物”而非“隐藏”
|
||||
|
||||
### US-A2 首次启用
|
||||
|
||||
**Given** 用户在设置中打开“启用桌面宠物”总开关
|
||||
**When** 开关切换为开启
|
||||
**Then** 宠物窗口在默认位置出现,使用默认外观,且不产生任何应用级或原生通知
|
||||
|
||||
### US-A3 关闭后完全消失
|
||||
|
||||
**Given** 桌面宠物已启用并可见
|
||||
**When** 用户关闭“启用桌面宠物”总开关
|
||||
**Then** 宠物窗口立即消失,未完成的气泡提示一并消失,托盘菜单条目恢复为“显示桌面宠物”,
|
||||
且用户之前设置的位置、外观和提示偏好被保留供下次开启时使用
|
||||
|
||||
## US-B 快速起草
|
||||
|
||||
### US-B1 起草文字并转入主窗口
|
||||
|
||||
**Given** 桌面宠物已启用,主窗口当前隐藏
|
||||
**When** 用户点击宠物并输入一段文字后点击提交
|
||||
**Then** 主窗口被显示并聚焦,目标会话的输入区出现刚输入的文字作为草稿,输入焦点落在
|
||||
该输入区,且没有任何消息被发送、没有任何任务被创建
|
||||
|
||||
### US-B2 起草时附加文件
|
||||
|
||||
**Given** 快速起草弹出框已打开
|
||||
**When** 用户点击“附加文件”并选择一个受支持类型和大小内的文件
|
||||
**Then** 该文件在弹出框内显示为待提交附件;提交后该附件出现在主窗口对应会话输入区的
|
||||
待发送附件列表中
|
||||
|
||||
### US-B3 附加不受支持的文件
|
||||
|
||||
**Given** 快速起草弹出框已打开
|
||||
**When** 用户选择一个超出大小限制或类型不受支持的文件
|
||||
**Then** 弹出框就地显示明确的拒绝原因,不添加该文件,且不影响已经添加的其他文字或附件
|
||||
|
||||
### US-B4 取消起草
|
||||
|
||||
**Given** 用户已在快速起草弹出框中输入文字或添加附件
|
||||
**When** 用户点击取消或按 Escape 关闭弹出框
|
||||
**Then** 弹出框关闭且草稿被丢弃,主窗口任何会话的现有草稿不受影响,且不创建任何草稿或
|
||||
任务记录
|
||||
|
||||
### US-B5 选择目标会话
|
||||
|
||||
**Given** 主窗口当前存在多个会话,且用户在快速起草弹出框中看到默认目标会话
|
||||
**When** 用户在弹出框内切换目标为“新建对话”
|
||||
**Then** 提交后主窗口创建并激活一个新会话,草稿内容进入该新会话的输入区,不影响原有
|
||||
会话的草稿
|
||||
|
||||
## US-C 拖放暂存
|
||||
|
||||
### US-C1 拖放单个文件
|
||||
|
||||
**Given** 桌面宠物已启用并可见
|
||||
**When** 用户从文件管理器拖动一个受支持类型的文件到宠物窗口上并释放
|
||||
**Then** 宠物在拖入过程中显示可放置的视觉反馈,释放后主窗口被显示并聚焦,该文件出现在
|
||||
目标会话输入区的待发送附件列表中
|
||||
|
||||
### US-C2 拖放多个文件,部分超限
|
||||
|
||||
**Given** 用户同时拖动数量或大小超出限制的多个文件到宠物窗口
|
||||
**When** 用户释放拖放
|
||||
**Then** 合法文件正常进入目标会话的待发送附件列表,超限或不支持的文件被单独列出拒绝
|
||||
原因,不阻止合法文件被暂存
|
||||
|
||||
### US-C3 拖放文件夹
|
||||
|
||||
**Given** 用户拖动一个文件夹到宠物窗口
|
||||
**When** 用户释放拖放
|
||||
**Then** 宠物提示不支持文件夹,不递归展开文件夹内容,也不产生任何暂存
|
||||
|
||||
### US-C4 拖放选中文字
|
||||
|
||||
**Given** 用户在其他应用中选中一段文字并拖动到宠物窗口
|
||||
**When** 用户释放拖放
|
||||
**Then** 该文字进入目标会话输入区的草稿文本,行为与快速起草输入文字一致
|
||||
|
||||
## US-D 状态镜面
|
||||
|
||||
### US-D1 查看空闲状态
|
||||
|
||||
**Given** 当前没有任何活跃 Task
|
||||
**When** 用户查看宠物外观
|
||||
**Then** 宠物显示空闲状态,且可访问名称明确说明当前没有进行中的任务
|
||||
|
||||
### US-D2 查看执行中状态
|
||||
|
||||
**Given** 有一个 Task 处于执行中
|
||||
**When** 用户查看宠物外观
|
||||
**Then** 宠物切换为执行中状态,且该状态与 Task Center 中该任务的状态一致
|
||||
|
||||
### US-D3 多任务状态优先级
|
||||
|
||||
**Given** 同时存在一个等待处理的任务和一个执行中的任务
|
||||
**When** 用户查看宠物外观
|
||||
**Then** 宠物按照“等待处理优先于执行中”的规则显示等待处理状态
|
||||
|
||||
### US-D4 点击状态镜面打开任务详情
|
||||
|
||||
**Given** 宠物当前显示执行中或失败状态
|
||||
**When** 用户点击宠物的状态镜面区域
|
||||
**Then** 主窗口被显示并聚焦,Task Center 定位到触发当前状态的具体任务
|
||||
|
||||
### US-D5 状态不可用
|
||||
|
||||
**Given** 用于确认任务状态的 Runtime 或网络当前不可用
|
||||
**When** 用户查看宠物外观
|
||||
**Then** 宠物显示“状态不可用”,不回退显示为空闲或执行中
|
||||
|
||||
## US-E 完成与失败提示
|
||||
|
||||
### US-E1 主窗口不可见时收到完成提示
|
||||
|
||||
**Given** 主窗口当前隐藏或未聚焦,安静模式未开启
|
||||
**When** 一个任务从执行中变为已完成
|
||||
**Then** 宠物旁出现说明任务标题和结果的气泡,且该事件不重复触发原生系统通知
|
||||
|
||||
### US-E2 失败提示保持可见
|
||||
|
||||
**Given** 主窗口当前隐藏,安静模式未开启
|
||||
**When** 一个任务失败
|
||||
**Then** 宠物旁出现失败气泡并保持可见,直到用户点击查看或手动关闭,不自动消失
|
||||
|
||||
### US-E3 主窗口可见时不重复提示
|
||||
|
||||
**Given** 主窗口当前可见并聚焦
|
||||
**When** 一个任务完成或失败
|
||||
**Then** 不出现宠物气泡;反馈通过主窗口现有机制呈现,避免同一事件重复出现在两个位置
|
||||
|
||||
### US-E4 开启安静模式
|
||||
|
||||
**Given** 用户在设置中开启“安静模式”
|
||||
**When** 之后任意任务完成或失败,且主窗口不可见
|
||||
**Then** 不出现任何宠物气泡,但状态镜面外观仍随任务状态正常变化
|
||||
|
||||
## US-F 位置、外观与多显示器
|
||||
|
||||
### US-F1 记忆位置
|
||||
|
||||
**Given** 用户把宠物拖动到某个显示器的特定位置
|
||||
**When** 用户重启 GoodBuddy
|
||||
**Then** 宠物出现在同一显示器的同一位置
|
||||
|
||||
### US-F2 显示器断开后回退
|
||||
|
||||
**Given** 宠物位置记忆在一个已插入的外接显示器上
|
||||
**When** 该外接显示器被移除后重新启动 GoodBuddy
|
||||
**Then** 宠物出现在当前可用的主显示器上一个可见、不越界的位置,而不是不可见或位于屏幕
|
||||
之外
|
||||
|
||||
### US-F3 贴边吸附
|
||||
|
||||
**Given** 用户开启“贴边”选项
|
||||
**When** 用户把宠物拖动到接近屏幕边缘的位置并释放
|
||||
**Then** 宠物自动吸附到屏幕边缘,且吸附后仍可被再次拖动离开
|
||||
|
||||
### US-F4 调整外观
|
||||
|
||||
**Given** 用户在设置中调整宠物尺寸、透明度或动画强度
|
||||
**When** 用户改变任意一项取值
|
||||
**Then** 宠物外观立即按新取值更新,且设置持久化,重启后保持
|
||||
|
||||
### US-F5 恢复默认外观
|
||||
|
||||
**Given** 用户已经修改过角色、尺寸、透明度或动画强度
|
||||
**When** 用户点击“恢复默认外观”
|
||||
**Then** 上述设置全部恢复为默认值,宠物位置和总开关状态不受影响
|
||||
|
||||
## US-G 暂时隐藏与托盘
|
||||
|
||||
### US-G1 暂时隐藏
|
||||
|
||||
**Given** 桌面宠物已启用并可见
|
||||
**When** 用户从托盘菜单选择“隐藏桌面宠物”
|
||||
**Then** 宠物窗口消失,总开关状态和其他设置不变,托盘菜单条目变为“显示桌面宠物”
|
||||
|
||||
### US-G2 重新显示
|
||||
|
||||
**Given** 桌面宠物已启用但当前隐藏
|
||||
**When** 用户从托盘菜单选择“显示桌面宠物”
|
||||
**Then** 宠物窗口在此前记忆的位置重新出现
|
||||
|
||||
### US-G3 应用退出
|
||||
|
||||
**Given** 桌面宠物已启用并可见
|
||||
**When** 用户退出 GoodBuddy
|
||||
**Then** 宠物窗口一并关闭,不留下任何独立运行的进程或残留窗口
|
||||
|
||||
## US-H 键盘与无障碍
|
||||
|
||||
### US-H1 键盘打开快速起草
|
||||
|
||||
**Given** 桌面宠物窗口通过系统级窗口切换取得焦点
|
||||
**When** 键盘用户按下预期的激活键
|
||||
**Then** 快速起草弹出框打开,焦点进入输入框
|
||||
|
||||
### US-H2 键盘关闭弹出框
|
||||
|
||||
**Given** 快速起草弹出框已打开且有键盘焦点
|
||||
**When** 用户按下 Escape
|
||||
**Then** 弹出框关闭,焦点回到宠物窗口本身
|
||||
|
||||
### US-H3 屏幕阅读器状态说明
|
||||
|
||||
**Given** 用户使用屏幕阅读器
|
||||
**When** 焦点移动到宠物的状态镜面
|
||||
**Then** 屏幕阅读器播报当前状态的文字说明,不仅依赖颜色或动画表达状态
|
||||
|
||||
## US-I 兼容与边界
|
||||
|
||||
### US-I1 升级用户保持现状
|
||||
|
||||
**Given** 已在使用 GoodBuddy 且从未接触过桌面宠物功能的用户升级到包含该功能的版本
|
||||
**When** 用户正常启动应用
|
||||
**Then** 桌面宠物保持关闭,界面和行为与升级前一致
|
||||
|
||||
### US-I2 不支持透明合成的环境
|
||||
|
||||
**Given** 用户的操作系统或桌面环境不支持窗口透明合成
|
||||
**When** 用户启用桌面宠物
|
||||
**Then** 宠物窗口退化为不透明的小型窗口而不是渲染异常或无法显示,具体规则见
|
||||
[技术设计](./technical-design.md)
|
||||
|
||||
### US-I3 拖放到非目标区域
|
||||
|
||||
**Given** 用户尝试把文件拖到宠物窗口之外的桌面区域
|
||||
**When** 用户释放拖放
|
||||
**Then** GoodBuddy 不接收该拖放,不产生任何暂存或错误提示
|
||||
@@ -0,0 +1,28 @@
|
||||
# 设备发现与共享
|
||||
|
||||
本功能是 GoodBuddy 桌面端管理附近设备、本机共享和 ShareServer 连接的唯一入口。默认连接
|
||||
方式是局域网;网关由用户额外添加。功能不包含 ShareServer 的组织管理 Web 界面。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、范围、功能需求和产品验收 |
|
||||
| [功能逻辑](./logic-design.md) | 不变量、状态、授权优先级和失败结果 |
|
||||
| [UI 设计](./ui-design.md) | 设置入口、四个页签、配对与调用交互 |
|
||||
| [技术设计](./technical-design.md) | Desktop 进程边界、持久化、IPC、网络和验证 |
|
||||
| [共享网络总体设计](../../architecture/share-network-architecture.md) | 跨端身份、能力、授权、任务、更新和协议权威 |
|
||||
|
||||
当前没有单独的 User Stories:正常、失败、撤销、权限和兼容场景已直接进入 PRD 验收与逻辑
|
||||
决策表。功能尚未实施,因此不创建 `progress.md`。
|
||||
|
||||
## 术语
|
||||
|
||||
- **附近设备**:同一局域网内主动广播 GoodBuddy 共享服务的设备。
|
||||
- **已配对设备**:双方确认设备指纹后建立点对点信任的设备。
|
||||
- **网关连接**:本机与某个 ShareServer 中特定组织身份的连接。
|
||||
- **本机共享**:由本机作为提供者发布给 Peer 或组织的能力。
|
||||
- **远端能力**:由其他设备或组织提供、本机可以查看或调用的能力。
|
||||
|
||||
SSH 远程项目是用户明确配置 Host 后操作远端工作区的独立功能,不自动进入设备发现与共享,
|
||||
详见[远程主机与远程执行](../remote-host/README.md)。
|
||||
@@ -0,0 +1,202 @@
|
||||
# 设备发现与共享功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
|
||||
| 共享规则 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
|
||||
本文负责桌面端状态、不变量和决策规则,不重复跨端协议定义。
|
||||
|
||||
## 1. 核心不变量
|
||||
|
||||
### INV-1 连接方式独立
|
||||
|
||||
局域网、每个 GatewayConnection 和本机能力互不拥有生命周期。启停、失败或删除其中一项不
|
||||
改写其他项。
|
||||
|
||||
### INV-2 三项操作独立
|
||||
|
||||
`discovering`、`advertising` 和 `publishing` 是三个独立状态。任何组合均合法,界面不得用
|
||||
一个“设备共享”总开关代替。
|
||||
|
||||
### INV-3 默认不共享
|
||||
|
||||
新安装没有 Publication。允许被发现只公开配对入口,不公开能力清单。
|
||||
|
||||
### INV-4 身份先于能力
|
||||
|
||||
未配对局域网设备不能取得详细能力;未注册网关设备不能取得组织目录。缓存不能绕过当前
|
||||
身份和撤销状态继续调用。
|
||||
|
||||
### INV-5 提供者在调用前冻结
|
||||
|
||||
自动选择只生成候选决定。用户确认或无需确认的请求创建后,provider、route 和授权快照不可
|
||||
改变。失败后切换提供者属于新的请求。
|
||||
|
||||
### INV-6 最小发送上下文
|
||||
|
||||
只发送能力 schema 要求且用户当前选择的输入。当前 Conversation、Project 和 Workspace 不
|
||||
因能力被调用而整体成为输入。
|
||||
|
||||
### INV-7 结果未知不重放
|
||||
|
||||
已经被提供者接受但终态无法确认的副作用任务进入 `outcome_unknown`。重新执行必须由用户
|
||||
发起,并显示可能重复产生副作用。
|
||||
|
||||
### INV-8 撤销只影响未来授权
|
||||
|
||||
撤销配对、网关连接、Grant 或 Publication 后不再创建新任务。历史记录保留;活动任务按
|
||||
冻结策略取消或完成,不能追改为从未发生。
|
||||
|
||||
### INV-9 设置导航无业务副作用
|
||||
|
||||
四个 PageTabs 只切换视图,不自动开始广播、连接网关、发布能力或保存草稿。
|
||||
|
||||
## 2. 状态维度
|
||||
|
||||
### 2.1 局域网服务
|
||||
|
||||
```text
|
||||
discover: off | starting | active | unavailable | error
|
||||
advertise: off | starting | active | unavailable | error
|
||||
```
|
||||
|
||||
操作系统或网络不支持时使用 `unavailable`;运行失败使用 `error`。两者分别重试。
|
||||
|
||||
### 2.2 附近设备
|
||||
|
||||
```text
|
||||
visible_unpaired -> pairing -> paired_online
|
||||
pairing -> rejected | expired | failed
|
||||
paired_online <-> paired_offline
|
||||
paired_* -> identity_changed | revoked
|
||||
```
|
||||
|
||||
`identity_changed` 不自动接受新证书。用户必须撤销旧配对后重新配对。
|
||||
|
||||
### 2.3 网关连接
|
||||
|
||||
```text
|
||||
draft -> authenticating -> connecting -> connected
|
||||
authenticating -> cancelled | auth_failed
|
||||
connecting/connected -> offline | auth_expired | policy_blocked
|
||||
offline -> connecting
|
||||
任意已保存状态 -> removed
|
||||
```
|
||||
|
||||
`offline` 保留连接和缓存目录供只读说明,但不能创建新任务;`auth_expired` 要求重新登录;
|
||||
`policy_blocked` 显示服务端给出的稳定原因和管理员联系路径。
|
||||
|
||||
### 2.4 本机发布
|
||||
|
||||
```text
|
||||
not_published -> draft -> validating -> published
|
||||
validating -> invalid
|
||||
published -> paused -> published
|
||||
published/paused -> removed
|
||||
```
|
||||
|
||||
草稿和无效发布不对外可见。能力实现消失时发布进入 `paused`,不删除用户配置。
|
||||
|
||||
### 2.5 远端任务
|
||||
|
||||
采用共享架构定义的状态。本机 UI 归并为:
|
||||
|
||||
| 协议状态 | 用户状态 |
|
||||
| --- | --- |
|
||||
| `created/awaiting_approval` | 等待确认 |
|
||||
| `queued/connecting` | 正在连接 |
|
||||
| `running` | 正在执行 |
|
||||
| `completed` | 已完成 |
|
||||
| `failed/rejected/expired` | 未完成,并显示具体原因 |
|
||||
| `cancelled` | 已取消 |
|
||||
| `outcome_unknown` | 结果未知 |
|
||||
|
||||
## 3. 默认状态
|
||||
|
||||
| 设置 | 全新安装默认值 |
|
||||
| --- | --- |
|
||||
| 发现附近设备 | 开启 |
|
||||
| 允许其他设备发现本机 | 关闭 |
|
||||
| 自动接受配对 | 不支持 |
|
||||
| 本机共享能力 | 无 |
|
||||
| 网关连接 | 无 |
|
||||
| 永久调用 Grant | 无 |
|
||||
|
||||
主动发现默认开启只浏览 GoodBuddy 主动广播,不执行端口扫描。操作系统权限尚未授予时,首次
|
||||
进入页面再触发对应系统说明和授权,不在应用启动时抢占焦点。
|
||||
|
||||
## 4. 提供者选择
|
||||
|
||||
自动选择按以下顺序比较满足当前输入和授权的在线候选:
|
||||
|
||||
1. 本机提供者。
|
||||
2. 已配对局域网提供者。
|
||||
3. 当前用户所属组织提供者。
|
||||
4. 联邦外部组织提供者。
|
||||
5. 同级中按服务端预计排队时间和稳定能力版本排序。
|
||||
|
||||
自动选择不跨越 `require_approval` 与 `allow`:若排名第一的候选需要批准,界面显示该候选及
|
||||
批准要求,不静默换成权限更宽但来源更远的候选。用户可以明确选择其他候选。
|
||||
|
||||
## 5. 调用决策表
|
||||
|
||||
| 身份与能力状态 | 授权结果 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 身份有效、输入匹配 | allow | 创建任务并展示执行位置 |
|
||||
| 身份有效、输入匹配 | require_approval | 显示完整确认后创建 |
|
||||
| 身份有效 | deny | 不创建任务,显示策略原因 |
|
||||
| 提供者离线 | 任意 | 不创建任务,允许重新选提供者 |
|
||||
| 清单过期或版本变化 | 任意 | 刷新后重新计算,不沿用确认 |
|
||||
| 文件超过能力或策略上限 | 任意 | 就地拒绝并显示上限 |
|
||||
| 委托链与确认不一致 | 任意 | 拒绝并要求以新执行链重新确认 |
|
||||
|
||||
## 6. 撤销与删除
|
||||
|
||||
| 操作 | 删除内容 | 保留内容 |
|
||||
| --- | --- | --- |
|
||||
| 撤销 Peer | 配对凭据、该 Peer 的本地 Grant 和目录缓存 | 活动记录、已安装包 |
|
||||
| 移除网关 | 本机设备凭据、连接配置、目录缓存 | 服务端账号、活动记录、已安装包 |
|
||||
| 停止发布 | 对外 Publication | 能力实现、发布草稿、历史任务 |
|
||||
| 删除发布 | Publication 和本地发布设置 | 能力实现、历史任务 |
|
||||
| 删除已安装包 | GoodBuddy-owned 安装内容和分配 | 来源审计与任务历史 |
|
||||
|
||||
存在活动任务时,撤销操作明确询问是否同时发送取消;无论取消是否确认,立即禁止新任务。
|
||||
|
||||
## 7. 失败与恢复
|
||||
|
||||
- mDNS 不可用:显示手动地址配对,不关闭已配对直连。
|
||||
- 广播失败:保留用户开关意图,状态为错误并允许重试。
|
||||
- 网关离线:指数退避重连,用户手动“重新连接”可立即尝试一次。
|
||||
- 登录取消:不保存连接。
|
||||
- 目录刷新失败:保留最后缓存并标记时间,不允许基于过期授权启动新任务。
|
||||
- 调用前连接失败:任务失败,可安全重新执行。
|
||||
- 提供者接受后断线:恢复查询同一任务;仍无法确认时进入结果未知。
|
||||
- 下载或签名失败:删除候选暂存,保留现有安装,不切换来源。
|
||||
|
||||
## 8. 需求追踪
|
||||
|
||||
| 需求 | 主要逻辑 |
|
||||
| --- | --- |
|
||||
| `FR-1`、`FR-2` | `INV-1`、`INV-2`、`INV-9` 和默认状态 |
|
||||
| `FR-3`、`FR-4` | 身份不变量和附近设备状态机 |
|
||||
| `FR-5` | 连接独立不变量、网关连接状态和删除规则 |
|
||||
| `FR-6` | provider 冻结不变量和提供者选择顺序 |
|
||||
| `FR-7` | 默认不共享不变量、本机发布状态和撤销规则 |
|
||||
| `FR-8` | 最小发送上下文和调用决策表 |
|
||||
| `FR-9` | 远端任务状态、结果未知不重放和失败恢复 |
|
||||
| `FR-10` | 撤销与删除、下载或签名失败规则;包协议由共享架构定义 |
|
||||
| `FR-11` | 撤销后历史保留和任务状态;展示行为由 UI 设计定义 |
|
||||
| `FR-12`、`FR-13` | 独立状态维度和失败恢复 |
|
||||
| `FR-14` | 身份先于能力、最小发送上下文和调用决策表 |
|
||||
|
||||
## 9. 逻辑完整性
|
||||
|
||||
本设计已覆盖发现、配对、连接、发布、调用、取消、撤销、离线、身份变化、权限变化和能力
|
||||
更新。实现前仍需在共享协议中固定受限 JSON Schema 子集、端到端中继加密套件和桌面端可
|
||||
注册为共享能力的首批实际类型;这些是技术选择,不改变本文状态和产品规则。
|
||||
@@ -0,0 +1,160 @@
|
||||
# 设备发现与共享 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 适用产品 | GoodBuddy Desktop |
|
||||
| 关联架构 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
| 关联文档 | [功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 的能力当前主要在本机执行,SSH 远程项目则要求用户预先配置具体 Host。个人和组织
|
||||
需要在不复制完整环境的情况下使用附近工作站、其他成员设备或组织节点上的模型、知识、硬件
|
||||
和工具,也需要在经过确认后把可分发能力安装到本机。
|
||||
|
||||
该能力不能以“扫描局域网机器”的方式实现。产品只发现主动开放 GoodBuddy 服务的设备,
|
||||
配对后才查看详细能力,授权后才发送数据或执行任务。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
1. 用户无需配置服务器即可发现并配对同一局域网中的 GoodBuddy 设备。
|
||||
2. 用户可以额外连接一个或多个 ShareServer,使用组织或合作组织提供的能力。
|
||||
3. 用户能够逐项决定本机向哪些 Peer、用户组或组织开放什么能力。
|
||||
4. 远程调用前、中、后都能确认提供者、执行位置、数据去向和授权依据。
|
||||
5. 局域网、各网关连接和本机能力彼此独立,单点故障不阻塞其他来源。
|
||||
6. 可分发能力必须在明确校验和批准后安装,不迁移对方凭据或机器配置。
|
||||
|
||||
## 3. 功能范围
|
||||
|
||||
### 3.1 包含
|
||||
|
||||
- 设置中的“设备发现与共享”一级分类。
|
||||
- 局域网主动发现、选择、双端配对、重命名和撤销。
|
||||
- 本机被发现开关和配对请求处理。
|
||||
- ShareServer 添加、登录、连接状态、组织身份和移除。
|
||||
- 远端能力目录、提供者选择、调用确认、进度、取消和结果。
|
||||
- 本机能力逐项发布、范围、允许动作和确认策略。
|
||||
- 签名能力包查看、下载、校验、安装和权限变化确认。
|
||||
- 局域网与网关活动记录。
|
||||
|
||||
### 3.2 不包含
|
||||
|
||||
- 扫描未主动广播 GoodBuddy 服务的机器或端口。
|
||||
- 在桌面端管理 ShareServer 成员、组织策略、联邦或服务器运维。
|
||||
- 自动公开本机 Skill、MCP、知识库、模型、文件或 SSH Host。
|
||||
- 同步 API Key、Cookie、系统环境变量、绝对路径或登录状态。
|
||||
- 把现有 SSH Host 自动转换为共享设备。
|
||||
- 无界远程 Shell、桌面控制或任意文件浏览接口。
|
||||
- 在网关断开时把组织授权自动降级为局域网授权。
|
||||
|
||||
## 4. 功能需求
|
||||
|
||||
### FR-1 设置入口
|
||||
|
||||
- 设置中心增加“设备发现与共享”一级分类。
|
||||
- 页面提供“局域网”“网关”“本机共享”“活动记录”四个同级页签。
|
||||
- 普通入口默认打开“局域网”;定向入口可打开指定页签。
|
||||
|
||||
### FR-2 独立开关
|
||||
|
||||
- “发现附近设备”和“允许其他设备发现本机”使用两个独立 Switch。
|
||||
- 任一开关变化不撤销既有配对、不改变网关连接、不开放能力。
|
||||
- 新安装默认允许主动发现;默认不允许本机被发现,默认无共享能力。
|
||||
|
||||
### FR-3 附近设备
|
||||
|
||||
- 只展示当前收到有效主动广播的 GoodBuddy 设备。
|
||||
- 未配对设备只展示配对所需的有限信息,不展示能力和资产详情。
|
||||
- 列表区分附近未配对、已配对在线、已配对离线和身份变化。
|
||||
- 用户可手动刷新,但持续发现不得因刷新造成列表闪烁或重复项。
|
||||
|
||||
### FR-4 双端配对
|
||||
|
||||
- 双方显示相同验证码、设备名称、系统类型和证书指纹。
|
||||
- 只有双方在时限内确认才建立配对。
|
||||
- 任一端拒绝、超时、身份变化或连接中断均不保存半配对关系。
|
||||
- 任一端可以撤销;撤销不删除历史活动记录。
|
||||
|
||||
### FR-5 网关连接
|
||||
|
||||
- 用户可输入 HTTPS ShareServer 地址或打开受支持的邀请链接。
|
||||
- 登录在系统浏览器中完成,设备凭据回到 Main 的安全存储。
|
||||
- 连接条目显示服务名、组织、当前成员身份、状态和最近策略同步时间。
|
||||
- 同一设备可连接多个网关;移除连接只删除本机凭据和缓存,不删除服务端账号。
|
||||
|
||||
### FR-6 能力目录
|
||||
|
||||
- 已配对 Peer 和已连接网关分别返回当前身份获准查看的能力。
|
||||
- 同一能力有多个提供者时合并展示,并允许“自动选择”或明确选择提供者。
|
||||
- 自动选择仅比较满足授权和输入要求的在线候选,依次选择本机、已配对局域网 Peer、
|
||||
当前用户所属组织和联邦外部组织;同级候选再按预计等待时间和稳定能力版本排序。
|
||||
- 选择结果必须在执行前冻结并展示,不能执行中静默更换提供者。
|
||||
|
||||
### FR-7 本机共享
|
||||
|
||||
- 只有显式注册为可共享的能力才能出现在本机共享列表。
|
||||
- 每项能力分别设置目标范围、`discover/inspect/invoke/download` 动作、输入类型、大小、
|
||||
并发、有效期和是否逐次确认。
|
||||
- 关闭能力或目标范围后停止接受新任务,不伪装为删除能力本身。
|
||||
- 知识库原文、工作区写入、进程执行和硬件控制需要独立的明确能力声明。
|
||||
|
||||
### FR-8 调用确认
|
||||
|
||||
- 首次或策略要求时,确认界面显示提供者、执行位置、接收方、输入内容类别、文件名和大小、
|
||||
路由、中继可读性及下游委托。
|
||||
- 用户可取消、仅本次允许,或在组织策略允许时按当前范围保存授权。
|
||||
- 调用上下文不隐式包含其他会话消息、整个工作区或未选择的知识库内容。
|
||||
|
||||
### FR-9 执行状态
|
||||
|
||||
- 会话或任务活动持续显示远端能力名称、来源、状态、发送字节和停止入口。
|
||||
- 支持排队、连接、执行、完成、失败、取消、过期和结果未知。
|
||||
- 网络中断且副作用结果无法确认时显示“结果未知”,不自动重试。
|
||||
|
||||
### FR-10 能力包
|
||||
|
||||
- 展示来源、版本、发布者、平台、大小、签名、权限和依赖。
|
||||
- 下载和安装经过共享架构规定的校验流程。
|
||||
- 新版本扩大权限时必须重新批准。
|
||||
- 安装到本机后按现有 Skill/MCP/Runtime 分配规则启用,不因来自可信组织自动取得全部 Runtime。
|
||||
|
||||
### FR-11 活动记录
|
||||
|
||||
- 统一记录本机发出、远端发入、局域网、网关和跨组织活动。
|
||||
- 支持按方向、来源、能力、结果和时间筛选。
|
||||
- 详情显示授权依据、执行链、数据类别、大小、版本和终态,不默认保存正文。
|
||||
|
||||
### FR-12 错误隔离
|
||||
|
||||
- 局域网发现失败不影响网关和本机能力。
|
||||
- 一个网关认证或连接失败不影响其他网关。
|
||||
- 一个提供者离线时不自动改发给另一个提供者;用户重新执行时再选择。
|
||||
- 页面保留可修复配置和准确错误,不以空状态代替失败状态。
|
||||
|
||||
### FR-13 平台与网络
|
||||
|
||||
- 支持 Windows、macOS、Linux 的当前桌面目标架构。
|
||||
- mDNS 被网络阻断时允许通过已知局域网地址发起配对,但仍执行相同身份确认。
|
||||
- 所有公网 ShareServer 使用 HTTPS;不提供“忽略证书错误”。
|
||||
|
||||
### FR-14 与现有能力的边界
|
||||
|
||||
- 远端调用不能扩大 Ask/Execute、Workspace、MCP、Skill 和知识库现有权限。
|
||||
- 本机提供任务时使用能力声明对应的 Runtime 边界。
|
||||
- SSH 远程项目、本机工具环境和模型配置默认不向 Peer 或网关传播。
|
||||
|
||||
## 5. 产品验收
|
||||
|
||||
- 两台新设备能完成发现、双端配对、授权、调用、取消和撤销。
|
||||
- 三个概念“主动发现、允许被发现、开放能力”在状态和界面上互不混淆。
|
||||
- 不配置网关时局域网工作完整;任一网关故障不阻塞其他来源。
|
||||
- 未配对或未授权主体无法查看详细能力。
|
||||
- 远端调用前能准确列出实际发送的数据,执行中和历史中可追溯提供者。
|
||||
- 结果未知不会被显示为失败后自动重试或成功。
|
||||
- 权限扩大的能力更新不会沿用旧授权静默安装。
|
||||
- 中英文、键盘、焦点、窄窗口和深浅主题符合统一设计系统。
|
||||
@@ -0,0 +1,213 @@
|
||||
# 设备发现与共享技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 共享协议 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
|
||||
## 1. 进程边界
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> 窄 Preload API
|
||||
Main
|
||||
-> DeviceSharingService
|
||||
-> LanDiscoveryService
|
||||
-> PeerPairingService
|
||||
-> GatewayConnectionService
|
||||
-> CapabilityCatalogService
|
||||
-> CapabilityPublicationService
|
||||
-> RemoteTaskService
|
||||
-> PackageAcquisitionService
|
||||
-> SharingAuditStore
|
||||
-> 既有 Capability / Runtime / Skill / MCP 服务
|
||||
```
|
||||
|
||||
Renderer 只取得脱敏快照和调用显式动作。设备私钥、Peer 证书、网关凭据、网络 socket、原始
|
||||
任务令牌、任意文件读取和包缓存路径只存在于 Main。
|
||||
|
||||
## 2. 模块职责
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `LanDiscoveryService` | mDNS browse/advertise、手动地址握手、接口变化 |
|
||||
| `PeerPairingService` | 临时握手、验证码、双端确认、配对凭据与撤销 |
|
||||
| `GatewayConnectionService` | 浏览器登录、设备注册、WebSocket、重连和策略同步 |
|
||||
| `CapabilityCatalogService` | 合并本机、Peer 和网关目录,保留来源身份 |
|
||||
| `CapabilityPublicationService` | 校验本机可共享能力并向目标范围发布 |
|
||||
| `RemoteTaskService` | 授权、确认、provider 冻结、传输、取消和终态 |
|
||||
| `PackageAcquisitionService` | 固定来源下载、校验、安装协调和权限差异 |
|
||||
| `SharingAuditStore` | 本机有界审计查询、筛选和清理 |
|
||||
|
||||
目录合并只生成 UI 视图,不能把不同 provider 的授权、版本和执行位置压成一个身份。
|
||||
|
||||
## 3. 共享契约
|
||||
|
||||
实现前建立独立 `packages/share-protocol`。Desktop 只从该包导入 Zod schema 和 TypeScript
|
||||
类型。协议输入在网络边界和 IPC 边界分别验证,不能因已经过网络校验而跳过 Renderer 输入
|
||||
校验。
|
||||
|
||||
建议导出:
|
||||
|
||||
```text
|
||||
DeviceSummary
|
||||
PairingChallenge
|
||||
GatewayDescriptor
|
||||
CapabilityManifest
|
||||
Publication
|
||||
GrantConstraint
|
||||
AuthorizationDecision
|
||||
RemoteTaskSnapshot
|
||||
RemoteTaskEvent
|
||||
PackageDescriptor
|
||||
AuditEventSummary
|
||||
ProtocolError
|
||||
```
|
||||
|
||||
错误只向 Renderer 返回稳定 code、可本地化参数和可恢复动作,不转发远端堆栈、响应正文或
|
||||
内部地址。
|
||||
|
||||
## 4. 持久化
|
||||
|
||||
建议在现有 Desktop SQLite 中增加:
|
||||
|
||||
```text
|
||||
device_identity_metadata
|
||||
peer_pairings
|
||||
gateway_connections
|
||||
capability_publications
|
||||
local_grants
|
||||
remote_tasks
|
||||
sharing_audit_events
|
||||
installed_shared_packages
|
||||
```
|
||||
|
||||
- 私钥和 refresh credential 存系统安全存储;SQLite 只存引用和脱敏元数据。
|
||||
- 附近广播、在线状态、配对验证码、短期任务令牌和目录候选只存在于内存。
|
||||
- 网关能力目录可有界缓存,用于离线说明,不作为离线调用授权。
|
||||
- 审计正文不落库,字段和保留上限在实现前固定。
|
||||
- 数据库迁移只覆盖已发布 schema;未发布设计不预建兼容读取器。
|
||||
|
||||
## 5. 局域网网络实现
|
||||
|
||||
- 使用系统支持的 mDNS/DNS-SD 库 browse `_goodbuddy-share._tcp.local`。
|
||||
- 广播绑定用户允许的网络接口;接口切换时重建监听,不保留旧地址为身份。
|
||||
- 服务端口由 Main 持有,使用设备证书建立 TLS。
|
||||
- 未配对握手仅暴露配对所需字段,并限制并发、频率、消息大小和有效时间。
|
||||
- 手动地址只替代发现,不替代证书指纹与双端确认。
|
||||
- 停止广播时关闭未完成配对监听;已配对活动连接按任务生命周期处理。
|
||||
|
||||
## 6. 网关连接
|
||||
|
||||
1. Main 校验 HTTPS URL,并取得服务公开描述。
|
||||
2. 生成带 PKCE、随机 state 和设备挑战的系统浏览器登录请求。
|
||||
3. 本机 loopback 或已注册应用链接只接收一次性回调 code。
|
||||
4. Main 交换设备凭据,展示组织身份,用户确认后持久化。
|
||||
5. Main 建立带设备身份的 WebSocket 出站连接。
|
||||
6. 心跳更新在线状态;断线按有界指数退避重连。
|
||||
|
||||
服务器返回的跳转 URL 必须符合已取得服务描述和 HTTPS 限制。Renderer 不能提交认证头、设备
|
||||
证书、WebSocket URL 或任意回调地址。
|
||||
|
||||
## 7. 本机能力适配
|
||||
|
||||
不是所有现有 Capability 都自动可共享。每种可共享类型需要显式 adapter:
|
||||
|
||||
```ts
|
||||
interface ShareableCapabilityAdapter {
|
||||
describe(): CapabilityManifest
|
||||
validatePublication(input: PublicationDraft): ValidationResult
|
||||
invoke(input: unknown, context: ProviderTaskContext): AsyncIterable<TaskEvent>
|
||||
cancel(taskId: string): Promise<CancelResult>
|
||||
}
|
||||
```
|
||||
|
||||
adapter 负责把共享输入映射到既有生产服务,并保留原权限边界。首批类型在实现前根据真实
|
||||
产品需求确定;未有 adapter 的 Skill、MCP、知识库和 Runtime 不出现在共享列表。
|
||||
|
||||
## 8. 远端调用生命周期
|
||||
|
||||
```text
|
||||
Renderer 选择能力和输入
|
||||
-> Main 解析候选并取得当前授权
|
||||
-> 必要时返回确认描述
|
||||
-> 用户批准
|
||||
-> Main 冻结 task/provider/route/policy
|
||||
-> 建立数据通道并流式发送
|
||||
-> Provider 再校验并接受
|
||||
-> Main 转发有界进度和结果
|
||||
-> 终态与审计事务保存
|
||||
```
|
||||
|
||||
每次任务使用稳定 `taskId` 和 attempt ID。Provider 未接受前的连接失败可以明确失败;已接受
|
||||
后的断线先查询同一任务,不新建 attempt。无法恢复权威终态时保存 `outcome_unknown`。
|
||||
|
||||
附件从 Main 已授权的用户选择结果读取,按声明大小流式发送,不把任意本机路径交给远端。
|
||||
接收成果先写 GoodBuddy-owned 暂存并校验,再通过现有 Artifact 边界进入产品。
|
||||
|
||||
## 9. IPC
|
||||
|
||||
Preload 只暴露按产品动作命名的方法,例如:
|
||||
|
||||
```text
|
||||
sharing.getSnapshot
|
||||
sharing.updateLanSettings
|
||||
sharing.startPairing / confirmPairing / cancelPairing
|
||||
sharing.revokePeer
|
||||
sharing.addGateway / completeGatewayLogin / removeGateway
|
||||
sharing.listCapabilities
|
||||
sharing.savePublication / pausePublication / deletePublication
|
||||
sharing.prepareInvocation / confirmInvocation / cancelTask
|
||||
sharing.listAuditEvents / getAuditEvent
|
||||
```
|
||||
|
||||
所有变更方法校验 trusted sender、严格 schema、对象当前 revision 和归属关系。不得暴露通用
|
||||
HTTP、WebSocket、mDNS、证书、文件传输或数据库查询 IPC。
|
||||
|
||||
## 10. 生命周期与资源边界
|
||||
|
||||
- 应用启动按保存设置启动主动发现;不因保存了网关而阻塞窗口创建。
|
||||
- 广播只有在用户开启后启动。
|
||||
- 网关连接在后台恢复,认证过期只影响对应连接。
|
||||
- 应用退出停止接收新任务,取消未接受任务,并有界通知活动 Provider 后关闭连接。
|
||||
- 单个 Peer/网关/任务设置连接数、帧、字节、并发和重连上限。
|
||||
- 大文件全程流式处理,不进入单个 Main `Buffer` 或 Renderer state。
|
||||
|
||||
## 11. 与现有模块集成
|
||||
|
||||
- `capability-service` 提供本机能力事实;共享 adapter 明确选择可公开子集。
|
||||
- 已安装共享 Skill/MCP 继续使用现有配置、启停和 Runtime 分配,不建立第二套执行器。
|
||||
- SSH Host 保持独立;除非未来提供专门 adapter 和用户发布,否则不进入目录。
|
||||
- 远端 Task 的公开活动复用现有工具/任务展示语义,但保留 provider 和 route 字段。
|
||||
- 应用更新源、模型下载源、Agent 下载源和工具下载源不因添加 ShareServer 自动改变。
|
||||
|
||||
## 12. 验证策略
|
||||
|
||||
### 12.1 自动化
|
||||
|
||||
- schema 边界、未知枚举和协议版本协商。
|
||||
- 三个独立开关及默认值。
|
||||
- mDNS 去重、接口变化、广播停止和手动地址。
|
||||
- 配对双方确认、拒绝、超时、身份变化和撤销。
|
||||
- 多网关隔离、认证过期、重连和移除。
|
||||
- provider 排序、冻结、授权变化和失败不切换。
|
||||
- 调用确认最小输入、附件上限、取消和结果未知。
|
||||
- Publication 校验、暂停、撤销和 adapter 消失。
|
||||
- 包签名、权限差异、来源冻结和失败保留旧安装。
|
||||
- IPC sender、revision、归属和无通用网络/文件接口。
|
||||
|
||||
### 12.2 真实环境
|
||||
|
||||
- Windows、macOS、Linux 至少两种组合完成同网段发现和配对。
|
||||
- mDNS 可用、被防火墙阻断、网络接口切换和设备离线场景。
|
||||
- 真实 ShareServer 登录、设备出站连接、目录、调用和认证过期。
|
||||
- 小型 JSON、文件流、取消、Provider 进程失败和网络中断。
|
||||
- 确认日志、Renderer、诊断和审计中没有凭据或文件正文。
|
||||
|
||||
设计文档完成不代表功能实施或上述验证通过。
|
||||
@@ -0,0 +1,211 @@
|
||||
# 设备发现与共享 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [设备发现与共享 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文只定义桌面端界面。所有颜色、间距、字体、通知、Switch、PageTabs、焦点和响应式行为以
|
||||
全局设计系统为准。
|
||||
|
||||
## 1. 信息架构
|
||||
|
||||
设置中心新增一级分类“设备发现与共享”,使用 `standard` PageShell:
|
||||
|
||||
```text
|
||||
设备发现与共享
|
||||
发现附近的 GoodBuddy,或连接组织网关使用和开放能力
|
||||
|
||||
[ 局域网 ] [ 网关 ] [ 本机共享 ] [ 活动记录 ]
|
||||
```
|
||||
|
||||
四项使用共享 `PageTabs` 的 `segmented` 视觉变体。普通入口默认“局域网”;来自能力来源、
|
||||
连接错误或审计入口的定向导航打开对应页签。页签选择不持久化为产品设置。
|
||||
|
||||
## 2. 局域网页签
|
||||
|
||||
页面顺序固定为本机发现状态、已配对设备、附近设备。
|
||||
|
||||
```text
|
||||
局域网
|
||||
|
||||
发现附近设备 [Switch 开]
|
||||
查看同一网络中主动开放的 GoodBuddy 设备
|
||||
|
||||
允许其他设备发现本机 [Switch 关]
|
||||
设备名称:Jiang-PC [编辑]
|
||||
|
||||
已配对设备 [刷新]
|
||||
名称 状态 能力 最近连接 操作
|
||||
GPU-Workstation 在线 4 项 刚刚 ...
|
||||
|
||||
附近设备
|
||||
Design-Mac 等待配对 [配对]
|
||||
```
|
||||
|
||||
- 两个 Switch 必须独立成行,不能使用一个总开关。
|
||||
- “允许被发现”开启后只显示广播状态和设备名,不暗示已经共享能力。
|
||||
- 设备行采用紧凑列表,不使用大卡片墙。
|
||||
- 已配对设备行可展开简要能力;完整信息进入设备详情抽屉。
|
||||
- 离线设备保留在“已配对设备”,附近广播消失不删除配对。
|
||||
|
||||
### 2.1 配对对话框
|
||||
|
||||
```text
|
||||
与 Design-Mac 配对
|
||||
|
||||
验证码 824 193
|
||||
设备 Design-Mac · macOS
|
||||
设备指纹 7A:31:...:D9
|
||||
有效时间 01:42
|
||||
|
||||
请确认另一台设备显示相同信息。
|
||||
[取消] [信息一致,确认配对]
|
||||
```
|
||||
|
||||
双方尚未确认时显示独立进度。拒绝、超时和连接失败保留设备行并给出重新配对,不显示成功
|
||||
通知。成功使用应用通知,焦点回到对应设备行。
|
||||
|
||||
### 2.2 设备详情
|
||||
|
||||
详情使用右侧抽屉,包含“概览”“能力”“授权”“活动”四个内部页签。未配对设备不能打开
|
||||
能力和授权。撤销配对放在抽屉底部 danger zone,并说明活动任务和历史记录影响。
|
||||
|
||||
## 3. 网关页签
|
||||
|
||||
```text
|
||||
网关 [添加网关]
|
||||
|
||||
研发组织网关
|
||||
研发中心 · 成员
|
||||
已连接 · 策略同步于 14:32
|
||||
可用能力 18 项 · 本机发布 2 项 [...]
|
||||
```
|
||||
|
||||
空状态说明网关是附加连接,局域网无需网关即可使用。主操作为“添加网关”。
|
||||
|
||||
### 3.1 添加网关
|
||||
|
||||
对话框只要求 HTTPS 地址,也可由邀请链接预填:
|
||||
|
||||
```text
|
||||
网关地址
|
||||
[https://share.example.com]
|
||||
|
||||
[取消] [继续登录]
|
||||
```
|
||||
|
||||
地址通过 Main 验证后打开系统浏览器。应用内显示“等待浏览器登录”和取消入口,不嵌入第三方
|
||||
登录页。回调成功后显示服务名、组织和账号,用户确认才保存连接。
|
||||
|
||||
网关详情抽屉包含连接、组织身份、可用能力、本机发布和诊断。移除连接明确说明只移除本机
|
||||
连接,不删除组织账号或服务端设备记录。
|
||||
|
||||
## 4. 本机共享页签
|
||||
|
||||
页首状态摘要不能替代逐项开关:
|
||||
|
||||
```text
|
||||
本机共享
|
||||
已开放 2 项能力 · 局域网 1 项 · 网关 2 项
|
||||
|
||||
搜索能力 [范围筛选] [状态筛选]
|
||||
|
||||
文档转 Word [Switch 开]
|
||||
范围:研发组织、GPU-Workstation
|
||||
允许:查看、远程调用
|
||||
确认:每次调用 [编辑]
|
||||
```
|
||||
|
||||
能力关闭时保留配置摘要并标为“已暂停”。点击“编辑”打开共享设置对话框:
|
||||
|
||||
- 目标范围使用分组多选,不混合 Peer、网关组织和用户组。
|
||||
- 允许动作使用 Checkbox。
|
||||
- “每次确认”使用 Switch。
|
||||
- 输入类型、大小、并发、时间段和有效期使用对应表单控件。
|
||||
- 权限较高的能力在提交区显示具体影响,不用泛化风险等级。
|
||||
- 提交按钮附近再次列出目标范围和允许动作。
|
||||
|
||||
能力尚未注册为可共享时不展示虚假的禁用行。列表为空时说明当前没有可共享能力,并提供前往
|
||||
“能力与工具”的次操作。
|
||||
|
||||
## 5. 活动记录页签
|
||||
|
||||
使用 `dashboard` 所需的表格能力,但保持在设置 PageShell 内横向滚动:
|
||||
|
||||
```text
|
||||
方向 能力 对端/来源 状态 时间
|
||||
发出 图像生成 研发组织网关 已完成 14:36
|
||||
收到 文档转 Word GPU-Workstation 正在执行 14:31
|
||||
```
|
||||
|
||||
工具栏提供方向、来源、状态、时间和搜索。详情抽屉显示任务 ID、能力版本、授权依据、路由、
|
||||
数据类型和大小、执行链、耗时、结果及取消/撤销入口。正文和文件内容默认不展示。
|
||||
|
||||
## 6. 能力选择与调用确认
|
||||
|
||||
远端能力进入现有能力目录时,在名称旁持续显示来源标签。多个提供者使用菜单选择:
|
||||
|
||||
```text
|
||||
图像生成
|
||||
提供者 [自动选择:GPU-Workstation v]
|
||||
```
|
||||
|
||||
调用需要确认时使用对话框:
|
||||
|
||||
```text
|
||||
调用“合同风险检查”
|
||||
|
||||
提供者 法务服务商
|
||||
执行位置 外部组织
|
||||
将发送 当前输入;contract.docx(1.8 MB)
|
||||
连接方式 ShareServer 中继
|
||||
网关可见正文 否
|
||||
下游调用 无
|
||||
|
||||
[取消] [仅本次允许] [按此范围始终允许]
|
||||
```
|
||||
|
||||
组织策略不允许保存 Grant 时不显示“始终允许”。按钮文字必须表达作用域,不能使用“确定”。
|
||||
|
||||
## 7. 执行活动
|
||||
|
||||
会话和任务中的远端工具活动至少显示:
|
||||
|
||||
```text
|
||||
合同风险检查 · 法务服务商
|
||||
正在执行 · 已发送 1 个文件(1.8 MB) [停止]
|
||||
```
|
||||
|
||||
展开后显示路由、能力版本和执行链。跨组织来源使用文字“外部组织”,不能只靠颜色。进入结果
|
||||
未知时显示持久警告和“查看活动详情”,不提供普通的自动重试按钮。
|
||||
|
||||
## 8. 状态与错误
|
||||
|
||||
- 首次为空、筛选无结果、加载失败、无权限和离线分别使用不同状态。
|
||||
- 缓存目录必须显示“上次更新于”,且离线时不能显示为当前可用。
|
||||
- 身份变化使用阻塞状态,唯一修复路径是撤销后重新配对。
|
||||
- 网关认证过期在对应网关行提供“重新登录”。
|
||||
- 发布字段错误就地显示;异步连接错误显示在对象行或详情中。
|
||||
- 配对成功、网关保存和共享发布成功使用应用通知,不重复显示页内成功横幅。
|
||||
|
||||
## 9. 无障碍与响应式
|
||||
|
||||
- 四个页签使用 `tablist/tab/tabpanel`、方向键、Home、End 和可见焦点。
|
||||
- 持久二元状态使用 `role="switch"`;范围和动作多选使用 Checkbox。
|
||||
- 表格在窄窗口切换为保留同样字段的列表,不隐藏提供者、状态或执行位置。
|
||||
- 指纹和长网关地址允许换行并提供复制按钮,不依靠水平裁切。
|
||||
- 对话框关闭后焦点回到触发控件;浏览器登录返回后恢复到等待登录区域。
|
||||
- 状态包含文字和图标,不以颜色作为唯一信号。
|
||||
|
||||
## 10. 固定词汇
|
||||
|
||||
使用“设备发现与共享”“局域网”“网关”“本机共享”“附近设备”“已配对设备”“允许其他
|
||||
设备发现本机”“结果未知”“外部组织”。不使用“扫描机器”“公网共享”“永久信任整台设备”
|
||||
或把 ShareServer 简称为“云端”。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 直连模型 Agent 能力
|
||||
|
||||
本目录是 GoodBuddy“直连模型 Agent 能力”的唯一文档入口。本功能为直连文本模型补齐
|
||||
高效工作区搜索、分页读取、补丁编辑、跨平台进程执行和编程 Subagent,使其能够完成
|
||||
“搜索、读取、修改、运行、验证”的开发闭环,同时避免向已经拥有原生 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 命令并返回有界结果 |
|
||||
| 工作区搜索 | 通过安装包内置 ripgrep 搜索内容或列出文件,并返回紧凑结果 |
|
||||
| 工作区补丁 | 通过 `*** Begin Patch` 格式新增、修改或删除工作区文本文件 |
|
||||
| 输出续读 | 通过 `output_read` 分页读取当前会话保留的进程或 Subagent 输出 |
|
||||
| 平台 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) 为准。本功能不重新定义
|
||||
这些对象。
|
||||
|
||||
## 功能边界
|
||||
|
||||
- `workspace_rg`、`workspace_read_text`、`workspace_apply_patch`、`process_execute`、
|
||||
`subagent_delegate` 和 `output_read` 是 GoodBuddy 直连模型内置工具,不是外部 MCP。
|
||||
- 直连文本模型的普通问答、工具轮次和上下文摘要会在尚无可见输出时,对瞬时网络错误或
|
||||
单次请求超时最多自动重试 3 次。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 继续使用各自原生执行与委派能力。
|
||||
- Ask 可搜索和分页读取工作区,但不允许补丁写入或进程执行;Subagent 若在 Ask 中使用,
|
||||
只能继承 Ask 的只读能力。
|
||||
- 进程和 Subagent 的长输出返回预览与续读位置,完整内容保留到所属会话或 Runtime 释放。
|
||||
Ask 和 Execute 均可续读当前会话已取得的输出,详见 [输出契约](./technical-design.md#43-输出边界)。
|
||||
- Execute 表示用户授权当前执行空间账号的完整能力,不增加第二套工具审批或权限档位。
|
||||
- 首版跨平台指桌面本机 Windows、macOS 和 Linux。托管 SSH 项目当前继续使用远端
|
||||
OpenCode;未来若向远端直连模型开放本工具,命令必须由 Host Agent 执行,绝不能回退到
|
||||
桌面本机。
|
||||
|
||||
## 相关功能
|
||||
|
||||
- [模型连接请求定制](../model-connections/README.md):定义连接级 Header/Body、合并优先级和 Runtime 支持矩阵。
|
||||
- [本机工具环境](../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,209 @@
|
||||
# 直连模型 Agent 能力功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 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 的原生能力不被本功能替换或包装。
|
||||
11. 直连模型只在尚未显示文本或推理内容时自动重试瞬时网络错误或请求超时,最多重试
|
||||
3 次;用户取消和已显示的部分输出禁止重放。
|
||||
12. Ask 可调用 `workspace_rg` 和 `workspace_read_text`,但不能调用
|
||||
`workspace_apply_patch`;Execute 可调用三者。
|
||||
13. 工作区搜索固定使用随包 ripgrep 的结构化输出,不把任意 ripgrep 参数转成命令执行。
|
||||
14. FR-7 / US-A6:预览之外的进程和 Subagent 输出保留在所属会话下,分页续读不启动新命令。
|
||||
会话释放先停止活动工作,再删除输出;子会话结束不删除父会话拥有的 Subagent 结果。
|
||||
|
||||
## 2. 状态维度
|
||||
|
||||
| 维度 | 值 |
|
||||
| --- | --- |
|
||||
| Runtime | `model`、`opencode`、`continue`、`deepseek-harness` |
|
||||
| 工作模式 | `ask`、`execute` |
|
||||
| 执行空间 | `local`、`ssh` |
|
||||
| 本机 Shell | `available`、`unavailable` |
|
||||
| Subagent 深度 | `0`、`1` |
|
||||
| 父请求 | `active`、`cancelling`、`terminal` |
|
||||
| 模型请求重试 | `initial`、`retry 1..3`、`terminal` |
|
||||
|
||||
## 3. 工具可见性决策表
|
||||
|
||||
| Runtime | 模式 | 工作区读/搜索 | 补丁 | `process_execute` | `subagent_delegate` |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `model` | Ask | 提供 | 隐藏 | 隐藏 | 提供,只读继承 |
|
||||
| `model` | Execute,本机 Shell 可用 | 提供 | 提供 | 提供 | 提供,Execute 继承 |
|
||||
| `model` | Execute,本机 Shell 不可用 | 提供 | 提供 | 隐藏并报告诊断 | 提供 |
|
||||
| `model` | SSH,远端后端未实现 | 当前入口不开放 | 当前入口不开放 | 隐藏 | 隐藏 |
|
||||
| 其他 Runtime | 任意 | 不注入本功能工具 | 不注入 | 不注入 | 不注入 |
|
||||
|
||||
工具清单隐藏不是最终授权边界。`callTool` 仍须重新验证 Runtime、工作模式、执行空间和父请求
|
||||
是否活动,以拒绝旧快照或伪造调用。
|
||||
|
||||
FR-4 / FR-12 / US-B0:Ask 工作区搜索和读取不依赖知识库、联网搜索或委派开关,不需要
|
||||
切到 Execute 或额外批准。MCP 服务器失败也不改变本机只读工具的可用性。
|
||||
|
||||
`output_read` 在直连模型拥有进程或 Subagent 服务时提供,Ask/Execute 均可用,不取决于
|
||||
本轮 Shell 可用性、是否允许再次委派或委派深度。读取只接受当前会话的句柄;其他 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` 仍产生正常工具结果。
|
||||
- 超时和取消结果包含已取得输出的预览;超出预览的内容按 FR-7 继续保留,直到所属会话释放。
|
||||
- 进入 `terminating` 后不再接受新输入,也不启动替代命令。
|
||||
- 终止只针对本次调用记录的进程树。
|
||||
- 迟到的 `close` 只用于清理,不得覆盖已提交的取消或超时原因。
|
||||
|
||||
## 5. 工作目录决策
|
||||
|
||||
| 输入 | 结果 |
|
||||
| --- | --- |
|
||||
| 未提供 | 当前工作区根目录 |
|
||||
| `.` 或相对路径 | 以当前工作区为基准解析,可指向工作区外目录 |
|
||||
| 绝对路径 | 使用指定目录 |
|
||||
| `..` 或指向工作区外的符号链接 | 解析到实际目标目录,按当前用户权限执行 |
|
||||
| 不存在或不是目录 | 拒绝 |
|
||||
| 当前工作区身份在验证期间变化 | 拒绝本次调用 |
|
||||
|
||||
工作区提供默认目录和相对路径基准,不是 Execute 命令的文件系统边界。单次命令选择其他
|
||||
工作目录不会修改父请求绑定的项目或工作区,也不会影响后续未指定 `cwd` 的命令。
|
||||
|
||||
## 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、浏览器或知识能力时继续经过这些能力已有的作用域和生命周期边界。
|
||||
- FR-5 / FR-7 / US-C1:Execute 子级使用父请求的浏览器标签页,不另开标签页或改变所有权;
|
||||
子级结束后父模型仍可继续操作该页,父请求继续负责其使用租约。
|
||||
- Execute 子级不弹出额外的“Subagent 执行授权”,也不获得父级没有的 MCP 分配。
|
||||
|
||||
## 9. 并发和顺序
|
||||
|
||||
- 全局编程 Subagent 最大并发为 3,额外请求进入有界队列。
|
||||
- 单个父模型的工具调用仍按直连模型现有顺序结算,不为本功能改写全部工具并发语义。
|
||||
- 不同父请求的 Subagent 可以并发;共享工作区的并发写入属于用户已授权的 Execute 行为,
|
||||
活动记录必须保留实际顺序和结果。
|
||||
- 首版不自动创建工作树、锁文件或回滚层。出现真实冲突时由父模型读取当前工作区事实并处理。
|
||||
|
||||
## 10. 结果优先级
|
||||
|
||||
当多个终止原因竞争时,使用以下优先级:
|
||||
|
||||
```text
|
||||
用户/父请求取消 > 工具超时 > 输出或协议边界 > 进程退出 > 普通工具错误
|
||||
```
|
||||
|
||||
已经提交的正常终态不被稍后到达的取消覆盖。父请求进入终态后,迟到的子级文本和工具事件
|
||||
只用于资源清理,不再写入会话。
|
||||
|
||||
## 11. 模型请求重试
|
||||
|
||||
```text
|
||||
attempt
|
||||
├─ success → completed
|
||||
├─ cancel → cancelled
|
||||
├─ non-retryable failure → failed
|
||||
├─ transient failure after visible output → failed_with_partial_output
|
||||
└─ transient failure before visible output
|
||||
├─ retry count < 3 → backoff → attempt
|
||||
└─ retry count = 3 → failed
|
||||
```
|
||||
|
||||
- 瞬时失败包括常见连接重置、连接/网络不可达、DNS 临时失败、连接管道中断和请求超时。
|
||||
- 三次重试分别等待 500 ms、1 s 和 2 s;每次尝试重新建立请求并使用独立请求超时。
|
||||
- 普通问答、工具模型轮次和上下文摘要使用同一规则。
|
||||
- 重试不重复已经执行的工具;工具只会在模型完整返回一轮工具调用后开始执行。
|
||||
- 退避等待绑定父请求取消信号,取消优先于启动下一次尝试。
|
||||
|
||||
## 12. 逻辑完整性评估
|
||||
|
||||
| 领域 | 结论 |
|
||||
| --- | --- |
|
||||
| Runtime 隔离 | 完整,工具只注入 `model` |
|
||||
| Ask/Execute | 完整,清单过滤和调用边界双重检查 |
|
||||
| 本机三平台 | 完整,统一契约和平台 Shell 决策已定义 |
|
||||
| Subagent 递归 | 完整,深度固定为 1 |
|
||||
| 取消与失败 | 完整,父子和进程传播及优先级已定义 |
|
||||
| 模型网络恢复 | 完整,有限重试、输出重放和取消边界已定义 |
|
||||
| 产品对象 | 完整,遵循 Task/Job 权威模型 |
|
||||
| SSH 远端执行 | 明确不在首版;未来必须增加 Agent 后端后才能开放 |
|
||||
|
||||
当前没有需要在实施前由用户补充的产品决策。
|
||||
@@ -0,0 +1,226 @@
|
||||
# 直连模型 Agent 能力 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台 CI 待验证 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 关联领域 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 直连模型已经可以读取、列出和写入工作区文本,使用浏览器、联网搜索、知识库及
|
||||
分配的 MCP,但没有内置命令执行工具。模型可以生成代码,却不能运行构建、测试、格式化、
|
||||
Git 或项目脚本,因此无法独立验证结果。
|
||||
|
||||
本功能设计时,GoodBuddy 已有面向用户专家协作的只读 Subagent,但直连模型尚不能在工具
|
||||
循环中主动委派编程子任务。当前专家协作已继承父请求 Ask/Execute 模式并使用本机直连模型
|
||||
工具,与本功能的 `subagent_delegate` 仍为不同入口。OpenCode、Continue 和 DeepSeek Harness 一般已经提供
|
||||
Shell、Agent 或 Task 能力,再向它们注入 GoodBuddy 同名工具只会造成重复入口和语义冲突。
|
||||
|
||||
本功能补齐直连模型缺少的基础 Agent 能力:
|
||||
|
||||
1. 一组工作区搜索、分页读取和补丁编辑接口。
|
||||
2. 一个按平台适配的进程执行接口。
|
||||
3. 一个单层、继承父请求范围的编程 Subagent 接口。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
1. 让直连模型在 Execute 模式完成“读取代码 → 修改 → 运行 → 根据结果修正”的闭环。
|
||||
2. 在 Windows、macOS 和 Linux 使用同一个模型工具契约,不要求模型调用平台专属工具名。
|
||||
3. 让直连模型可以把独立工作委派给临时 Subagent,并取得有界、可核验的结果。
|
||||
4. 保持 Ask 只读、Execute 完整授权、执行空间、取消、有界输出和进程回收语义不变。
|
||||
5. 不向已有原生执行能力的 Agent Runtime 重复注入 GoodBuddy 进程或 Subagent 工具。
|
||||
6. 使用安装包内置 ripgrep 提供跨平台一致的快速代码搜索,不依赖用户预装命令。
|
||||
|
||||
## 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、模型连接或工作模式;Execute 命令的
|
||||
单次工作目录选择遵循 FR-3。
|
||||
- 子级不再看到 `subagent_delegate`,首版最大委派深度固定为 1。
|
||||
- 子任务不是新的顶层 Task 或 Conversation,结果先返回父模型。
|
||||
|
||||
### 3.5 不新增权限档位
|
||||
|
||||
Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完整授权。进程和 Subagent 不增加
|
||||
“受控执行”“仅测试”“可信命令”或逐命令风险等级,也不重复弹出第二套确认。
|
||||
|
||||
### 3.6 精简工作区工具
|
||||
|
||||
直连模型使用 `workspace_rg` 发现文件和搜索内容,使用 `workspace_read_text` 按行分页读取,
|
||||
使用 `workspace_apply_patch` 新增、修改或删除文本文件。新请求不再注入低效的单层目录列表和
|
||||
整文件覆盖工具。Ask 提供前两个只读工具;Execute 额外提供补丁工具。
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
### 4.1 包含
|
||||
|
||||
- 本机 Windows、macOS、Linux 的前台 Shell 命令执行。
|
||||
- 当前工作区默认工作目录,以及当前用户可访问的本机工作目录选择。
|
||||
- Exit Code、标准输出、标准错误、Shell、目录、耗时和截断状态。
|
||||
- 命令超时、用户取消、进程树停止和应用退出清理。
|
||||
- 新进程使用当前本机工具环境提供的 Node、Python 和 PATH。
|
||||
- 直连模型单层 Subagent 委派、排队、运行、部分结果、失败和取消。
|
||||
- 父子请求的工具、模型用量、活动和成果归属。
|
||||
- 桌面聊天和选择直连模型的消息通道项目使用相同 Runtime 能力矩阵。
|
||||
- 安装包按目标平台和架构携带固定版本 ripgrep,并将其提供给工作区搜索和命令 PATH。
|
||||
|
||||
### 4.2 不包含
|
||||
|
||||
- 交互式 PTY、终端复用、持续 stdin、密码提示或 TUI 自动化。
|
||||
- 后台守护进程、任意 PID 枚举、接管或停止用户已有进程。
|
||||
- Shell Session 持久化,环境变更不会跨 `process_execute` 调用保留。
|
||||
- 首版远程 SSH 直连模型进程执行。
|
||||
- 向 OpenCode、Continue 或 DeepSeek Harness 注入本功能工具。
|
||||
- 递归 Subagent、长期运行的自治 Agent、Workflow、Hook 或恢复未完成子任务。
|
||||
- 新的权限矩阵、逐命令批准、沙箱档位或命令白名单。
|
||||
- 自动安装项目依赖;模型可以在 Execute 中显式运行项目已有的安装命令。
|
||||
|
||||
## 5. 功能需求
|
||||
|
||||
### FR-1 直连模型专属可用性
|
||||
|
||||
- 进程、编程 Subagent 和输出续读工具只由 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 提示、单次返回的输出预览、队列和并发有界;完整输出不因超过预览容量而丢弃。
|
||||
- 预览省略内容时明确标记并提供续读位置。模型可用 `output_read` 逐页取得完整日志及
|
||||
Subagent 结果,包括失败或取消前已取得的部分输出。
|
||||
- Ask 和 Execute 均可续读当前会话的输出。会话或 Runtime 释放后,临时输出删除,旧句柄失效。
|
||||
|
||||
### FR-8 环境和凭据
|
||||
|
||||
- 新进程使用筛选后的当前用户环境和本机工具环境 PATH。
|
||||
- 模型 Provider 凭据、GoodBuddy 加密设置和无关桌面会话变量不进入命令环境。
|
||||
- 首版不接受模型提交任意环境变量映射;需要的普通变量由 Shell 命令在单次调用中设置。
|
||||
|
||||
### FR-9 状态与活动
|
||||
|
||||
- 进程活动显示等待、运行、成功、非零退出、超时、取消和失败。
|
||||
- Subagent 显示排队、运行、完成、失败和取消。
|
||||
- 命令和子任务失败保留可操作错误及已有部分输出。
|
||||
- 同一事件不同时显示页内错误和应用通知。
|
||||
|
||||
### FR-10 兼容
|
||||
|
||||
- 现有模型连接、MCP 分配、专家设置和 Runtime 选择无需迁移。
|
||||
- 不改变现有 Agent Runtime 的工具清单或权限语义。
|
||||
- 不修改系统 PATH、Shell Profile 或用户终端会话。
|
||||
|
||||
### FR-11 模型请求重试
|
||||
|
||||
- 直连文本模型的普通问答、工具轮次和上下文摘要遇到瞬时网络错误或单次请求超时时,
|
||||
在尚未显示文本或推理内容的前提下最多自动重试 3 次。
|
||||
- 重试使用递增等待时间并显示当前次数;用户取消必须立即终止等待和后续请求。
|
||||
- 一旦本次模型调用已经显示文本或推理内容,不自动重放该调用,避免重复回答。
|
||||
- 非网络类 Provider 错误、协议错误和用户取消不进入自动重试。
|
||||
|
||||
### FR-12 高效工作区工具
|
||||
|
||||
- `workspace_rg` 同时支持内容搜索和文件列表,遵循项目忽略规则,并以
|
||||
`path:line:column:text` 紧凑格式返回匹配。
|
||||
- `workspace_read_text` 接受起始行和行数,大文件通过续读完成,不因超过旧的整文件上限而
|
||||
完全不可读;超长单行可使用返回的字节位置继续读取。
|
||||
- `workspace_apply_patch` 支持一个补丁新增、修改和删除多个 UTF-8 文件;执行前验证全部
|
||||
路径和 hunk,执行失败时准确报告已完成文件。
|
||||
- 搜索和读取在 Ask/Execute 均可用;补丁只在 Execute 可用,Main 调用边界再次检查模式。
|
||||
- `workspace_rg` 使用固定参数调用随包 ripgrep,不向模型开放 `--pre` 或任意命令参数。
|
||||
- 工作区工具不限制文件总大小、补丁总大小、搜索表达式长度、glob 数量或模型请求的结果
|
||||
数量。单次返回按上下文容量截断或分页;读取结果提供续读位置,搜索结果提示模型缩小查询。
|
||||
|
||||
## 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。
|
||||
- [ ] TLS 建连重置和请求超时在未输出内容时最多自动重试 3 次,成功后继续原请求。
|
||||
- [ ] 重试等待可取消,已显示部分内容的失败不自动重放。
|
||||
- [ ] Ask 可使用 ripgrep 搜索和分页读取,但不能应用补丁。
|
||||
- [ ] Execute 可通过一个补丁新增、修改和删除多个工作区文件,并运行测试验证。
|
||||
- [ ] Windows、macOS、Linux 的 x64/arm64 安装包均携带对应 ripgrep,且无需系统预装。
|
||||
- [ ] 全量测试、类型检查、Lint 和生产构建通过。
|
||||
@@ -0,0 +1,244 @@
|
||||
# 直连模型 Agent 能力功能进度
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 文档类型 | 功能进度 |
|
||||
| 状态 | 已实施,本机验收完成 |
|
||||
| 版本 | 0.5 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联入口 | [直连模型 Agent 能力](./README.md) |
|
||||
|
||||
## 当前结论
|
||||
|
||||
本机生产路径已经完成源码接线:直连模型 Execute 可运行平台 Shell,Ask/Execute 均可按
|
||||
父模式委派单层编程 Subagent;长输出可通过 `output_read` 续读。OpenCode、Continue、
|
||||
DeepSeek Harness 和托管 SSH 路径不注入这些工具。既有功能的 Windows 本机、真实模型和
|
||||
全量项目验证已通过;本次分页验证记录见下方 2026-09-10 条目。macOS 与 Linux 真机验证
|
||||
仍需由对应平台完成。
|
||||
|
||||
## 2026-09-10 提交审查后的桌面 UI 验收
|
||||
|
||||
- Windows 隔离配置使用本轮最终开发构建,通过普通输入框发送触发完整
|
||||
App → Preload → Main → ModelAgentRuntime → ModelToolProvider 路径。
|
||||
- Ask 在已保存 `toolApproval: policy` 下完成实际 `workspace_read_text` 与 `workspace_rg`,
|
||||
读取同一专用哨兵,24 字节测试文件与校验值保持不变。未放开 Ask 写工具。
|
||||
- 父请求调用实际编程 Subagent;子级在父浏览器归属下完成导航和快照,子级页面显示在
|
||||
父级工作栏 Tab,没有额外空白原生页面。
|
||||
- 本组使用确定性 loopback 模型服务,不声称执行了桌面真实 Provider 推理。
|
||||
请求计数、浏览器/终端 UI 场景、明确覆盖边界及清理结果统一记录在
|
||||
[工作栏验收进度](../assistant-workbar/progress.md#2026-09-10-提交审查修复)。
|
||||
- 最终整库回归与静态检查结果同见该记录;测试进程已退出,不改变用户安装版或工作区。
|
||||
|
||||
## 设计前基线(历史)
|
||||
|
||||
- [x] 直连模型当前内置工作区读、目录列表和文本写入,但没有进程工具。
|
||||
- [x] 内置 MCP 当前没有 Shell;自定义 MCP 不能替代开箱即用的进程能力。
|
||||
- [x] `ModelAgentRuntime` 已有多轮工具循环、取消、上下文压缩和有界结果。
|
||||
- [x] DeepSeek Harness 已按 Windows PowerShell、macOS/Linux Bash 注册原生 Shell。
|
||||
- [x] OpenCode 和 Continue 使用各自原生执行工具。
|
||||
- [x] 专家协作与直连模型可调用的编程工具是不同入口;当前专家继承父请求 Ask/Execute,
|
||||
使用已启用本机直连模型工具并记录工具活动。模式与 authorizer 传递已由
|
||||
`subagent-service.test.ts` 覆盖,不把该单元测试作为额外的真实模型或远程执行验收。
|
||||
- [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 > 直连模型”开发工具目录。
|
||||
- [x] 普通问答、工具轮次和上下文摘要的瞬时网络错误及请求超时最多自动重试 3 次。
|
||||
- [x] 递增退避、重试状态、取消传播和已显示输出不重放。
|
||||
|
||||
### 阶段 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-09 高效工作区工具
|
||||
|
||||
- [x] 新增 `workspace_rg`,以随包 ripgrep 同时提供内容搜索和文件发现;内部解析 JSON,向
|
||||
模型返回紧凑的路径、行号、列号和预览。
|
||||
- [x] `workspace_read_text` 改为按行分页读取,大文件不再因旧的整文件 256 KiB 上限完全
|
||||
不可读。
|
||||
- [x] 移除搜索表达式、glob 数量、请求结果数、读取行数、补丁大小和目标文件大小的固定上限;
|
||||
单次输出继续分页或截断,超长单行可通过 `offsetBytes` 续读。
|
||||
- [x] 新增 `workspace_apply_patch`,支持一个补丁新增、修改和删除多个 UTF-8 文件,并在
|
||||
修改前完成路径和 hunk 验证。
|
||||
- [x] Ask 工具清单开放工作区搜索和读取,补丁与进程仍只在 Execute 提供。
|
||||
- [x] 设置中的“文件系统操作”更新为“工作区文件”,“编程能力”更新为“开发工具”。
|
||||
- [x] 固定 `@vscode/ripgrep` 1.18.0;构建钩子按目标平台/架构从锁定平台包提取二进制,
|
||||
安装包携带当前目标的 `rg` 和 MIT 许可证。
|
||||
- [x] 本次聚焦测试 `212 passed, 1 skipped`;全量 `npm run lint` 和
|
||||
`npm run build:bundle` 通过。构建钩子在 Windows x64 实际准备并运行随包
|
||||
ripgrep 15.0.0。
|
||||
- [x] 中文文档审校覆盖本目录 7 个 Markdown 文件,阻断项为 0。
|
||||
- [ ] 全量 `npm test` 和 `npm run typecheck` 仍受同一工作树中并行开发的浏览器标签页接口及
|
||||
测试夹具修改阻塞;当前类型错误位于 `ipc.test.ts` 和 `browser-tabs-electron-e2e.ts`,最终
|
||||
全量验证需在这些改动稳定后复跑。
|
||||
|
||||
## 2026-09-06 命令工作目录限制修复
|
||||
|
||||
- FR-3 / US-A3:移除命令工作目录的工作区范围限制。绝对路径、`..` 和指向工作区外
|
||||
的符号链接均按本机文件系统解析,工作区只作为默认目录和相对路径基准。
|
||||
- 保留目录存在性、本机执行空间绑定、Ask 拒绝命令、取消、超时和进程树回收。
|
||||
本次没有改变工作区文件读写工具或远端 Agent 路径。
|
||||
- `direct-model-process-service.test.ts` 覆盖绝对路径、相对路径、外部目录、符号链接、
|
||||
不存在的目录和普通文件;当前 Windows 环境通过。
|
||||
- `model-runtime.test.ts` 新增两个生产工具链回归:模型协议响应提供绝对或相对外部
|
||||
`cwd`,经 `ModelAgentRuntime → ModelToolProvider → LocalDirectModelProcessService`
|
||||
启动真实 PowerShell,在专用工作区外测试目录复制文件并读回验证。模型响应为本机
|
||||
测试替身,没有外部模型调用;不将此记录为真实模型或 macOS/Linux 真机验收。
|
||||
- 最终聚焦验证:`opencode-runtime`、`opencode-runtime-lifecycle`、
|
||||
`direct-model-process-service`、`model-tool-provider`、`model-runtime` 五个测试文件
|
||||
`182 passed, 1 skipped`;`npm run typecheck` 和 `npm run lint` 通过。
|
||||
- 本次共享工作区的全量 `npm test` 为 `3542 passed, 57 skipped, 1 failed`。唯一失败是
|
||||
`App.test.tsx` 的 `opens the global assistant sidebar and switches work tabs`,
|
||||
仍期望“项目工作区”标题,单独复测同样失败;该侧栏处于其他会话的修改范围,本次未改动。
|
||||
因此本次全量验证不能记录为全部通过。
|
||||
|
||||
## 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 连接后完成验证。2026-09-03 已针对该故障补充统一模型请求重试。
|
||||
|
||||
## 2026-09-03 模型请求重试验证证据
|
||||
|
||||
- `ModelAgentRuntime` 对普通问答、工具轮次和上下文摘要统一处理瞬时 Node/Undici 网络错误
|
||||
与请求超时;首次失败后最多重试 3 次,退避为 500 ms、1 s、2 s。
|
||||
- 聚焦测试覆盖嵌套 `fetch failed` → `ECONNRESET` 恢复、工具轮次恢复、四次尝试后终止、
|
||||
退避取消,以及已显示部分流式输出时不重放。
|
||||
- `npm test -- --run src/main/agent/model-runtime.test.ts`:
|
||||
`72 passed, 1 skipped`。
|
||||
- 全量 `npm test`:`3484 passed, 51 skipped`。
|
||||
- `npm run typecheck`、`npm run lint` 和 `npm run build`:通过。
|
||||
- 最小真实直连文本模型验证:`1 passed, 13 skipped`,实际模型调用 1 次。
|
||||
|
||||
- 全量 `npm test`:`3410 passed, 50 skipped`。
|
||||
- `npm run typecheck`:通过。
|
||||
- `npm run lint`:通过。
|
||||
- `npm run build`:Main、Preload、Renderer 和控制面安装器均成功构建。2026-09-02 按产品
|
||||
决策移除未对应实际性能问题的 Renderer bundle 基线门禁后复验通过。
|
||||
|
||||
## 2026-09-05 移除直连模型响应字节上限
|
||||
|
||||
- 直连模型不再对流式响应总量、单个流式块、非流式回复、模型工具参数、工具请求上下文、
|
||||
聚合工具结果和累计回答长度设置字节上限;长回复和大工具结果由请求超时、取消和上下文
|
||||
压缩控制。用户不再看到“模型接口流式响应超过安全限制”。
|
||||
- 图像生成响应仍按可解码图片大小校验,Provider 错误响应仍读取有界文本。
|
||||
- 回归测试改为验证超过旧上限的长流式回复可以完整返回,以及超过 1 MB 的图片工具结果会
|
||||
进入下一轮模型请求。
|
||||
- `npx vitest run src/main/agent/model-runtime.test.ts`:`74 passed, 1 skipped`。
|
||||
- 全量 `npm test`:`3493 passed, 55 skipped`。
|
||||
- `npm run typecheck`、`npm run lint` 和 `npm run build`:通过。
|
||||
|
||||
## 2026-09-10 进程与 Subagent 完整输出续读
|
||||
|
||||
- FR-7 / US-A6:`PagedOutputStore` 完整写入临时输出文件,进程和 Subagent 返回前缀预览
|
||||
与续读引用。JSON 转义计入预览预算,避免 Provider 二次裁剪后丢失续读位置。
|
||||
- `ModelToolProvider` 已注册并分发 `output_read`,Ask/Execute 均可读取所属会话输出。
|
||||
工具数量预留一个槽位,页结果不再裁剪。详细字段以 [技术设计](./technical-design.md#44-输出续读)
|
||||
为准。
|
||||
- Provider、进程、Subagent、分页存储及共享工具目录五个测试文件共 `73 passed`。
|
||||
覆盖真实 PowerShell stdout/stderr、Subagent 父会话归属、JSON 转义、Unicode 小页面前进、
|
||||
句柄失效、活动调用取消和资源清理。`npm run typecheck`、`npm run lint` 通过。
|
||||
- `runtime-e2e.manual.test.ts` 新增真实模型边界场景:运行一次输出超过 96 KiB 的 Node
|
||||
脚本,随机中间与结尾标记均不在预览中;模型经 `output_read` 读到 EOF 后准确返回两个标记。
|
||||
最终带报告的运行 `1 passed, 19 skipped`,上游模型请求精确为 4 次,进程调用 1 次,续读
|
||||
调用 2 次。先前同场景也通过,但其控制台计数未保留,不据此推算两次运行的精确累计调用数。
|
||||
- 真实调用使用现有 `RuntimeSettingsStore` 与 Electron `safeStorage` 解密已配置文本连接,
|
||||
凭据仅经测试子进程环境传递。`GOODBUDDY_E2E_OUTPUT_REPORT` 可记录不含凭据、Prompt
|
||||
或输出正文的调用计数;测试工作区和输出存储在结束后释放。
|
||||
- 本目录中文文档已按 `deai-writing` 审校,扫描阻断项为 0;保留技术枚举、权限限制和历史
|
||||
验证记录中的明确边界。本次未运行全量测试或生产构建,也不作为其他平台真机验收。
|
||||
|
||||
## 2026-09-10 直连工具接线回归修复
|
||||
|
||||
- FR-4 / FR-12 / US-B0:普通 Ask 不再依赖可选能力才进入工具循环;明确允许工作区搜索、
|
||||
文本读取和当前会话输出续读绕过 Ask 默认拒绝 authorizer,保留写入、补丁、进程和浏览器
|
||||
的 Ask 拒绝。内部无工具摘要路径不变。
|
||||
- FR-5 / FR-7 / US-C1:编程 Subagent 继承父浏览器标签页及所属 Conversation;子级历史和
|
||||
输出仍独立,浏览器资源不转移所有权。回归通过真实 Runtime、Provider 和 Subagent 服务,
|
||||
浏览器使用校验父 Conversation/标签页身份的测试替身,并确认子级结束后父级可继续调用。
|
||||
- MCP 已发现工具调用不再触发其他失败服务器的发现重试;覆盖连接失败、清单失败、动态
|
||||
刷新失效、健康调用、配置替换与 dispose 清空。发现重试仍只发生在下一次 `listTools`。
|
||||
- 验证命令:`npm test -- --run src/main/agent/model-tool-provider.test.ts src/main/agent/model-runtime.test.ts`。
|
||||
最终结果为 `122 passed, 1 skipped`(Provider 36;Runtime 86 通过、1 跳过)。
|
||||
Ask 回归实际执行随包 ripgrep、工作区分页读取和真实本机输出续读;模型响应均为测试夹具。
|
||||
- 本次真实模型调用精确为 0,没有外部网络或远端 Host 操作;未运行全量测试、类型检查、
|
||||
Lint 或构建。桌面 IPC 提示词、浏览器租约生产入口和最小真实模型验收由集成工作继续验证,
|
||||
以上聚焦测试不作为真实浏览器、真实模型或整条桌面路径验收。
|
||||
- 源码检查:GoodBuddy Agent 不导入这两个 Runtime/Provider;本机 DeepSeek Harness 的
|
||||
MCP 代理复用 Provider 发现快照规则,托管 SSH Runtime 不经过该本机构造路径。本次未
|
||||
修改远端 Agent、桥接或启动器。
|
||||
|
||||
## 进度维护要求
|
||||
|
||||
- 只有真实生产路径完成并验证后才能勾选实施项。
|
||||
- 平台模拟单元测试不能替代对应平台真实命令测试。
|
||||
- 真实模型验证必须记录准确调用次数且不得记录凭据。
|
||||
- 需求变化回写 PRD、User Stories 和功能逻辑;实现事实只写本文件。
|
||||
@@ -0,0 +1,495 @@
|
||||
# 直连模型 Agent 能力技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 相关架构 | [助手工作栏与执行空间](../assistant-workbar/prd.md)、[本机工具环境](../local-tool-environment/technical-design.md) |
|
||||
|
||||
## 1. 实现基线
|
||||
|
||||
当前源码已经实现:
|
||||
|
||||
- `ModelAgentRuntime` 的多轮工具调用、上下文压缩、取消、用量和有界结果处理。
|
||||
- `ModelToolProvider` 的直连模型内置工具、浏览器、Web、知识和自定义 MCP 聚合。
|
||||
- `workspace_rg`、分页 `workspace_read_text` 和 `workspace_apply_patch` 工作区工具。
|
||||
- `WorkspaceAccess` 的本机和远端工作区抽象。
|
||||
- 跨平台子进程树终止辅助函数。
|
||||
- 面向用户专家协作的 `SubagentScheduler` 和状态事件。专家使用具备已启用本机能力的
|
||||
直连模型 Runtime,继承父请求 Ask/Execute 及 authorizer,工具事件记入子任务;
|
||||
Ask 保持只读,不把这些本机工具表述为远程 OpenCode 子会话。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 各自的 Shell/Agent 能力。
|
||||
- 直连模型 `process_execute`、`subagent_delegate` 和 `output_read`。
|
||||
- 不创建顶层 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 上限。
|
||||
|
||||
## 工作区工具契约
|
||||
|
||||
### 工具输入与执行
|
||||
|
||||
- `workspace_rg` 接受 `pattern`、相对目录、glob、固定字符串、忽略大小写、文件列表模式和
|
||||
最大结果数。Main 固定追加 `--no-config`,内容搜索读取 `--json`,对模型返回紧凑文本。
|
||||
- `workspace_read_text` 接受 `path`、从 1 开始的 `offset` 和 `limit`,通过现有
|
||||
`WorkspaceAccess.readText` 字节分页组装带行号结果;`offsetBytes` 提供原始字节续读,
|
||||
覆盖跨多个页面的超长单行。
|
||||
- `workspace_apply_patch` 接受一个 `*** Begin Patch` 文本,支持 `Add File`、`Update File`
|
||||
和 `Delete File`。全部操作先解析并验证;更新文件保留原换行和权限位,单文件原子替换。
|
||||
- Ask 只注册前两个只读工具;Execute 注册全部三个工具。旧的目录列表和整文件覆盖不再进入
|
||||
新工具清单。
|
||||
- 输入和目标文件不设固定总量上限。单次工具结果仍遵守直连模型上下文边界;读取返回续读
|
||||
位置,搜索截断时要求模型缩小路径、glob 或表达式。
|
||||
|
||||
### ripgrep 打包
|
||||
|
||||
固定依赖 `@vscode/ripgrep`,构建钩子根据 Electron 目标选择对应 Windows、macOS 或 Linux
|
||||
x64/arm64 平台包,使用 lockfile integrity 校验后提取 `rg`。安装包只复制当前目标二进制和
|
||||
MIT 许可证。运行时通过 `BundledRuntimePaths.ripgrep` 使用绝对路径,并将所在目录放到直连
|
||||
模型进程 PATH 前部。
|
||||
|
||||
## 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` 接受绝对路径或相对工作区路径,省略时使用工作区根目录;按本机路径语义解析
|
||||
`..` 和符号链接,不限制目标位于工作区内。工具 schema 向模型明确这一规则。
|
||||
- 首版不接受 `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
|
||||
stdoutReference?: PagedOutputReference
|
||||
stderrReference?: PagedOutputReference
|
||||
}
|
||||
```
|
||||
|
||||
`cwd` 在工作区内返回相对显示值,根目录为 `.`;工作区外返回规范化绝对路径,准确说明
|
||||
命令实际执行位置。Shell 在模型结果中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
|
||||
|
||||
### 4.3 输出边界
|
||||
|
||||
- stdout 和 stderr 各返回最多 96 KiB 的前缀预览,JSON 转义后的内容也计入预览预算。
|
||||
- `PagedOutputStore` 将流式输出完整写入系统临时目录,内存只保留预览;超限后继续写盘。
|
||||
存储失败明确报错,不把缺失内容标记为完整结果。
|
||||
- 预览省略内容时返回 `stdoutReference` 或 `stderrReference`,包含 `handle`、`nextCursor`
|
||||
和 `totalBytes`。短输出返回完整文本并删除临时文件,不附带句柄。
|
||||
- JSON 结果加上元数据后必须低于 `ModelToolProvider` 现有 256 KiB 工具结果上限。
|
||||
- 输出按 UTF-8 解码,无效序列使用替换字符并保留字节计数。
|
||||
|
||||
### 4.4 输出续读
|
||||
|
||||
`output_read` 接受严格对象 `{ handle, cursor?, limitBytes? }`:`cursor` 为从 0 开始的 UTF-8
|
||||
字节位置,省略时从头读取;`limitBytes` 为 1 至 32,768 的整数,默认 32,768。页面返回
|
||||
`{ handle, content, cursor, nextCursor, totalBytes, eof }`,模型沿 `nextCursor` 读取至 `eof`。
|
||||
页面在字符边界结束;若请求大小容不下首个完整字符,最多扩展到 4 字节,保证续读前进。
|
||||
位于 UTF-8 延续字节的输入 cursor、负数、越界和未知字段均拒绝。
|
||||
|
||||
Provider 根据 `process:` 或 `subagent:` 前缀选择服务,以当前 `conversationId` 校验所有者,
|
||||
不接受模型另传 owner。Ask/Execute 均可用,工具总数为此预留一个槽位;不要求本轮能够启动
|
||||
命令或再次委派。分页结果直接 JSON 序列化,不进行二次文本裁剪,以免 cursor 与内容不一致。
|
||||
32 KiB 的页面即使全部需要 JSON 转义,也能保留在现有工具结果容量内。
|
||||
|
||||
进程输出归属调用会话,Subagent 最终输出归属父会话。子级临时会话释放后,父模型仍能续读
|
||||
Subagent 结果。会话释放会等待活动调用结算再删除所属文件;服务退出关闭写入句柄并删除临时
|
||||
目录。输出不写入 SQLite,不跨 Runtime 重启恢复。
|
||||
|
||||
## 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 清单。
|
||||
- Execute 子级经 `ModelSubagentRequestContext` 继承父 `browserTabId` 和浏览器所属
|
||||
`browserConversationId`。后者只在 Main 内部请求与工具上下文中传递:浏览器操作使用父
|
||||
Conversation,历史与分页输出仍使用子 Conversation。子级不新建标签页或申请使用租约,
|
||||
结束时只释放自己的临时会话,父标签页和租约仍由父请求生命周期管理。
|
||||
|
||||
### 7.3 调度和结果
|
||||
|
||||
- 复用 `SubagentScheduler` 的取消、并发和队列模式。
|
||||
- 全局最大并发 3,队列最大 20。
|
||||
- 单个子任务最长 10 分钟;父请求更早取消时立即停止。
|
||||
- 文本预览最多 192 KiB,JSON 转义也计入预算;错误最多 2 KiB。省略的正文通过
|
||||
`outputReference` 续读,字段与生命周期沿用第 4.4 节,完整输出不受预览容量限制。
|
||||
- 模型用量逐事件归属父请求和 `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` 等待有界清理,不因模型或子进程不响应而阻塞应用退出。
|
||||
|
||||
### 8.1 Ask 工具循环
|
||||
|
||||
普通 Ask 请求进入工具循环,不再依赖知识、联网搜索或 Subagent 是否启用。Runtime 对
|
||||
`workspace_rg`、`workspace_read_text`、`output_read` 三个明确只读工具免去 Ask authorizer
|
||||
调用,避免生产 authorizer 的默认拒绝使已列出的只读工具无法执行。Execute 仍沿用既有
|
||||
authorizer;补丁、进程和其他写入不因此获准。Provider 按清单和调用边界继续校验模式,
|
||||
浏览器工具不加入 Ask 免审批集合。内部上下文摘要使用 `noModelTools`,保留无工具问答路径。
|
||||
|
||||
### 8.2 MCP 发现与调用
|
||||
|
||||
`listTools` 发现 MCP 并保存当前绑定快照;已知工具调用只查该快照,不在每次调用前重试
|
||||
其他失败服务器。未知名称直接失败。失败服务器只在下一次发现入口重试,动态清单刷新失败
|
||||
时从新快照中移除该服务器工具,下一次发现再刷新;健康连接继续复用。
|
||||
|
||||
绑定只在 Provider 内存中存在,`dispose` 与连接缓存一起清空。MCP 配置保存通过
|
||||
`refreshCapabilities → onRuntimeSettingsChanged → reconfigureRuntimes` 替换 Runtime,
|
||||
`SelectedRuntimeManager.reset` 使后续请求创建新 Provider;退役 Runtime 按原有会话生命周期
|
||||
清理。没有新增持久缓存、失败重试日志或配置兼容层。
|
||||
|
||||
DeepSeek Harness 的 Main 工具代理必须使用仅包含分配 MCP 和 Web 的 Provider 视图,不能因为
|
||||
共享 `ModelToolProvider` 类而自动看到直连模型编程工具。
|
||||
|
||||
## 9. 事件与活动
|
||||
|
||||
### 9.1 进程事件
|
||||
|
||||
继续使用现有 Runtime `tool` 事件:
|
||||
|
||||
- `pending`:参数已接收。
|
||||
- `running`:Shell 已成功启动。
|
||||
- `completed`:返回 exit 0 或非零退出结果。
|
||||
- `failed`:启动或契约失败。
|
||||
- `cancelled`:父请求取消。
|
||||
|
||||
结果包含 Shell、工作目录、退出码、耗时、输出预览和续读引用,不写应用诊断日志。
|
||||
通用工具活动仍显示有界摘要,模型通过 `output_read` 取得的页面记为后续工具活动。
|
||||
|
||||
### 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 无效或越界 | 工具调用前失败 |
|
||||
| 超时/取消 | 终止进程树,返回已有输出和原因 |
|
||||
| 直连模型瞬时网络错误/请求超时,尚无可见输出 | 500 ms、1 s、2 s 退避,最多自动重试 3 次 |
|
||||
| 直连模型失败,已有文本或推理输出 | 保留部分输出并失败,不自动重放 |
|
||||
| 子级模型失败 | 返回 `failed`、部分输出和错误 |
|
||||
| 子级工具失败 | 由子模型处理;最终无法完成时返回子级失败 |
|
||||
| 父请求已终止 | 拒绝新调用,丢弃迟到内容并清理 |
|
||||
|
||||
不要用宽泛 catch 抹掉取消、超时、Provider HTTP 状态或进程退出上下文。
|
||||
|
||||
模型请求重试由 `ModelAgentRuntime` 的统一流式请求处理,覆盖普通问答、工具轮次和复用
|
||||
同一 Runtime 的上下文摘要。重试分类检查错误及有界 `cause` 链中的 Node/Undici 网络错误
|
||||
码;每次尝试创建独立超时信号。退避等待监听父请求取消。只有完整模型轮次返回后才执行
|
||||
工具,因此未产生可见文本或推理内容的重试不会重复执行工具。
|
||||
|
||||
模型回复本身不设固定字节上限:流式响应、非流式回复、模型工具参数、请求上下文和工具
|
||||
结果总量都由请求超时、取消和上下文压缩控制,长回复和大工具结果不会被中断。图像生成
|
||||
响应仍按可解码图片大小校验。
|
||||
|
||||
会话历史在请求校验、Renderer 发送与保存、SQLite 读写和直连模型缓存中均保留完整消息,
|
||||
不按消息条数、单条文本长度或累计文本量裁剪。上下文大小由现有压缩策略和模型上下文窗口
|
||||
处理。Continue 接收完整历史,已有有效摘要只替换其覆盖的前缀,剩余消息全部交给原生
|
||||
Runtime;GoodBuddy 不再额外保留最后 20 条或限制为 128,000 字符。
|
||||
|
||||
## 11. 性能
|
||||
|
||||
- Shell 探测按执行服务实例缓存,不在每轮模型调用重复探测。
|
||||
- stdout/stderr 边读边写入临时文件,内存仅保存有界预览;续读每次只读取一页。
|
||||
- 子级共享父请求已解析的模型配置和工具服务,不复制 MCP 配置或持久状态。
|
||||
- 不增加持久 Shell、后台 daemon、工作区快照或恢复状态机。
|
||||
- 命令执行不使用 PTY,避免普通构建和测试承担终端渲染成本。
|
||||
|
||||
## 12. 测试策略
|
||||
|
||||
### 12.1 单元测试
|
||||
|
||||
- 工具可见性矩阵和 Runtime target 过滤。
|
||||
- Ask 伪造进程调用拒绝。
|
||||
- Windows PowerShell、POSIX Bash/Sh 选择和参数。
|
||||
- cwd 默认、相对子目录、绝对路径、工作区外目录、指向外部的符号链接和不存在的目录。
|
||||
- exit 0、非零退出、spawn 失败、超时、取消和输出截断。
|
||||
- 输出预览加分页能还原全文,包括 JSON 转义文本、中文、表情和字面替换字符;覆盖一字节
|
||||
页面前进、跨会话拒绝、活动调用释放、短输出删除及服务退出清理。
|
||||
- 环境筛选、本机工具 PATH 优先且模型凭据不进入进程。
|
||||
- 子级模式、模型、项目、执行空间和能力继承。
|
||||
- 深度 1 过滤、并发 3、队列 20、取消传播和部分输出。
|
||||
- 编程 Subagent 不创建 Task/Conversation。
|
||||
- DeepSeek Harness/OpenCode/Continue 不获得本功能工具。
|
||||
- TLS 建连 `ECONNRESET`、Undici 连接超时、单次请求超时、三次重试上限和退避取消。
|
||||
- 已产生文本或推理增量后发生网络错误时不自动重放。
|
||||
|
||||
### 12.2 本机集成测试
|
||||
|
||||
- Windows PowerShell 创建临时文件、运行 Node/Python、非零退出和进程树取消。
|
||||
- macOS/Linux Bash/Sh 执行相同场景。
|
||||
- 直连模型工具循环执行“修改 → 测试失败 → 修复 → 测试通过”。
|
||||
- 父模型委派子级修改或验证专用临时项目,并正确综合结果。
|
||||
- Runtime 切换后工具清单刷新且没有重复 Shell。
|
||||
- 应用退出后无残留 GoodBuddy 命令进程。
|
||||
|
||||
所有测试只在专用临时工作区创建文件,不删除或覆盖用户工作。
|
||||
|
||||
### 12.3 真实模型验证
|
||||
|
||||
使用一个最小真实直连文本模型请求:
|
||||
|
||||
1. Execute 创建一段有缺陷的小程序。
|
||||
2. 调用进程工具运行聚焦测试并观察失败。
|
||||
3. 修改程序并再次运行到通过。
|
||||
4. 委派一个 Subagent 检查实现或补充测试。
|
||||
5. 父模型读取子级结果并给出最终结论。
|
||||
|
||||
每个平台一次完整场景即可,不进行批量或高成本调用。记录准确模型调用次数,不记录 API Key。
|
||||
|
||||
输出边界场景复用 `runtime-e2e.manual.test.ts`:真实模型运行一次专用 Node 脚本,输出超过
|
||||
96 KiB,并在预览之后生成随机中间与结尾标记。模型必须通过生产 `output_read` 连续读到 EOF,
|
||||
最终回答包含两个标记。测试限制工具为进程与续读,记录上游请求数和工具调用数;凭据由现有
|
||||
加密设置解密后仅在内存及测试子进程环境中传递。
|
||||
|
||||
### 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,148 @@
|
||||
# 直连模型 Agent 能力 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 设计系统 | [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 或 Sh 命令,默认在工作区执行,也可指定其他工作目录。
|
||||
编程 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. 预览省略提示与工具结果中的续读引用。
|
||||
|
||||
日志使用共享等宽字体和可选择文本。长输出在活动块内折叠,展开后仍保持工作栏和聊天布局
|
||||
可用;不把完整日志复制为应用通知。
|
||||
|
||||
模型使用 `output_read` 续读时显示普通工具活动“续读工具输出”。活动展开只展示该次返回的
|
||||
摘要,不等同于自动加载完整日志;当前不增加手动分页按钮。
|
||||
|
||||
## 4. Subagent 活动块
|
||||
|
||||
复用现有 `subagent` 消息块,并将系统执行者显示为“编程 Subagent”,不要求用户预先创建
|
||||
专家。
|
||||
|
||||
```text
|
||||
编程 Subagent
|
||||
检查认证模块的测试失败并提出修复
|
||||
正在运行 · 直连模型 · Execute
|
||||
```
|
||||
|
||||
终态:
|
||||
|
||||
- 完成:显示有界结果摘要,可展开查看当前预览;完整正文由模型通过输出续读工具取得。
|
||||
- 失败:保留部分输出和错误。
|
||||
- 取消:显示“父请求已取消”或准确取消原因。
|
||||
- 排队:显示当前处于并发队列,不显示虚假百分比。
|
||||
|
||||
活动块不提供单独的“批准”“提高权限”或“继续递归”按钮。用户取消父请求即可取消子级。
|
||||
|
||||
## 5. Ask/Execute 表达
|
||||
|
||||
- Ask 中不显示不可用的进程按钮,因为模型工具没有手动调用入口。
|
||||
- Ask Subagent 活动明确显示“只读”。
|
||||
- Execute 活动显示“当前用户权限”,不声称 OS 沙箱或工作区隔离。
|
||||
- 如果用户从 Ask 切到 Execute,只有下一条请求使用新模式;运行中的 Subagent 不切换。
|
||||
|
||||
## 6. 错误与通知
|
||||
|
||||
- 参数错误、命令退出、Subagent 失败和部分输出属于当前活动上下文,保留在对应活动块。
|
||||
- Runtime 初始化、Shell 探测或全局服务失败可通过应用通知提示,并在设置状态卡提供重试。
|
||||
- 同一错误不同时显示活动块副本和通知副本。
|
||||
- 失败后保留用户 Prompt、命令摘要、工作目录和已有输出。
|
||||
- 直连模型自动重试在消息底部区分“模型网络请求失败,等待重试(n/3,s 秒后)”与
|
||||
“〈模型名〉正在重试请求(n/3),等待响应”;只有退避结束且未取消才显示后者。
|
||||
初始请求显示“〈模型名〉请求处理中”,不把尚无输出的等待描述成思考。
|
||||
- 不增加独立按钮、弹窗或通知副本;用户仍可使用当前停止操作取消请求。
|
||||
底部静态状态点、工具进展、待回答及终态优先规则由
|
||||
[消息底部请求状态](../assistant-workbar/runtime-interactions.md#消息底部请求状态)统一定义。
|
||||
|
||||
## 7. 键盘与无障碍
|
||||
|
||||
- 活动块展开按钮使用原生 `button`,提供 `aria-expanded` 和关联详情 ID。
|
||||
- 状态变化通过现有活动区域的礼貌级 live region 发布,不逐个朗读流式日志。
|
||||
- stdout/stderr 区域具有可见标签,不能只靠顺序区分。
|
||||
- 焦点不会因后台状态更新自动移动。
|
||||
- 取消父请求后的焦点保持在原取消按钮或恢复到 Composer。
|
||||
- 浅色、深色和减少动态效果模式遵守统一设计系统。
|
||||
|
||||
## 8. 响应式
|
||||
|
||||
- 窄聊天区只保留工具名、状态和一行摘要,元数据换行到下一行。
|
||||
- 日志区域横向滚动,不撑宽页面。
|
||||
- 运行记录使用紧凑摘要,不复制完整 stdout/stderr;用户回到聊天活动块展开查看详情。
|
||||
@@ -0,0 +1,196 @@
|
||||
# 直连模型 Agent 能力 User Stories
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台 CI 待验证 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 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 当前请求为本机 Execute,When 模型传入绝对路径或相对工作区路径,Then 命令在
|
||||
解析后的目录执行,包括工作区外目录和符号链接目标,And 后续省略 `cwd` 时仍使用原工作区。
|
||||
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 命令或 Subagent 输出超过单次预览容量,When 工具返回结果,Then 明确标记省略并提供
|
||||
续读位置,And 模型可逐页读取中间与结尾内容,直到取得完整输出。Given 输出包含中文或表情,
|
||||
Then 即使请求很小的页面,续读仍会前进。Given 所属会话已释放,Then 旧句柄不能继续读取。
|
||||
|
||||
### US-A7 从瞬时模型网络故障恢复
|
||||
|
||||
关联:FR-7、FR-9、FR-11。
|
||||
|
||||
Given 直连模型在 TLS 建连、网络传输或单次请求超时时失败且尚未显示输出,When 请求仍未
|
||||
取消,Then GoodBuddy 显示当前重试次数并使用递增等待时间最多重试 3 次,And 成功后继续
|
||||
原问答或工具轮次。Given 已经显示文本或推理内容,Then 不自动重放该调用。
|
||||
|
||||
## 3. Epic B:编程闭环
|
||||
|
||||
### US-B0 搜索和分页读取大型项目
|
||||
|
||||
关联:FR-4、FR-12。
|
||||
|
||||
Given 用户在 Ask 或 Execute 中处理本机项目,When 模型需要定位代码或读取大文件,Then
|
||||
它可以使用随包 ripgrep 搜索内容或列出文件,并按行分页读取命中位置,不要求用户预装 `rg`。
|
||||
Given Ask 未启用知识库、联网搜索或编程委派,Then 工作区搜索和读取仍可使用,不要求额外批准。
|
||||
|
||||
### 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 中采取用户要求的安装步骤,不把环境缺失报告为模型故障。
|
||||
|
||||
### US-B4 使用补丁修改文件
|
||||
|
||||
关联:FR-4、FR-12。
|
||||
|
||||
Given 用户选择 Execute,When 模型提交包含新增、修改或删除操作的工作区补丁,Then
|
||||
GoodBuddy 在写入前验证全部路径和上下文,逐文件原子写入,并返回实际修改的文件列表。
|
||||
Given 用户选择 Ask,Then 补丁工具不出现在清单中且伪造调用会被拒绝。
|
||||
|
||||
## 4. Epic C:Subagent 委派
|
||||
|
||||
### US-C1 委派聚焦编程任务
|
||||
|
||||
关联:FR-5、FR-6。
|
||||
|
||||
Given 父直连模型正在处理复杂请求,When 它调用 `subagent_delegate` 并给出独立任务,
|
||||
Then 子级使用同一项目、模型连接和 Execute 能力完成工作,And 最终结果返回父模型综合。
|
||||
Given 父请求已有浏览器标签页,When 委派任务需要浏览器,Then 子级使用同一页;子级完成后,
|
||||
父模型仍可继续操作该页,不因子会话清理而关闭父页。
|
||||
|
||||
### 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,15 @@
|
||||
# 文档处理
|
||||
|
||||
文档处理负责聊天附件和知识库导入中的文本提取、格式转换与本地 OCR。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [文档解析与本地 OCR PRD](./prd.md) | 产品范围、解析策略、OCR 模型管理和验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
|
||||
Office 文件的浏览器内人工编辑、AI 选区修改、保存和撤销不属于文档解析职责,由
|
||||
[Office 协同编辑](../office-document-editing/README.md)负责。文档处理仍负责内容提取、OCR、
|
||||
旧格式转换和供模型消费的统一结构。
|
||||
+11
-1
@@ -45,12 +45,17 @@ PDF 不是所有文档唯一的中间格式。解析应同时保留:
|
||||
|
||||
| 场景 | 默认预设 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| 聊天附件 | 自动解析 | 优先快速提取,文本不足时按需 OCR,有界截断后加入当前请求 |
|
||||
| 聊天附件 | 自动解析 | 优先快速提取,文本不足时按需 OCR,完整解析文本加入当前请求 |
|
||||
| 知识库导入 | 完整索引 | 完整解析、按页或工作表定位、按需 OCR、分块与索引 |
|
||||
| 扫描文档 | OCR | 页面渲染、文字识别、置信度与定位保留 |
|
||||
| 表格分析 | 语义优先 | 单元格和值优先,PDF 或图片补充图表与打印布局 |
|
||||
| 高保真审阅 | 视觉增强 | 原生解析、页面渲染、OCR 或视觉理解合并 |
|
||||
|
||||
PDF、DOCX、PPTX 和 XLSX 的解析结果保留章节定位及全文,不在附件入库时截断为
|
||||
256 KiB,也不以富化后 Prompt 的 1 MiB 上限阻止发送。本地选择与消息通道附件使用
|
||||
同一文本保存路径;原文件大小、解析器和 12 MiB 附件暂存容量仍按现有规则检查,超限时
|
||||
明确报错,不能用截断后的内容冒充全文。模型上下文窗口仍由所选 Runtime 处理。
|
||||
|
||||
### 3.3 本地优先
|
||||
|
||||
- 文本层和本地 OCR 均在设备上处理;
|
||||
@@ -251,6 +256,11 @@ ZIP 根目录包含模型文件和 `goodbuddy-model.json`。清单格式为 `goo
|
||||
|
||||
## 8. Office 与转换
|
||||
|
||||
本节只定义 Office 文件的语义解析和格式转换,不提供可视化编辑器、编辑会话、选区、撤销或
|
||||
保存回写。通过 ShareServer 集成浏览器编辑器的设计见
|
||||
[Office 协同编辑](../office-document-editing/README.md)。编辑服务可以复用转换产物,但不能
|
||||
替代本地优先的聊天附件和知识库解析路径。
|
||||
|
||||
### 8.1 新格式
|
||||
|
||||
- DOCX:正文、标题、表格、批注和图片关系;
|
||||
@@ -0,0 +1,11 @@
|
||||
# 全双工语音
|
||||
|
||||
全双工语音定义持续听说、打断、语音活动检测、本地与远端推理以及多平台音频边界。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | 产品目标、交互流程、音频架构、模型、打包和实施状态 |
|
||||
|
||||
现有文档是综合设计,尚未按 PRD、User Stories、功能逻辑、UI 和进度职责拆分。
|
||||
+2
-2
@@ -10,7 +10,7 @@
|
||||
| 日期 | 2026-08-19 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 相关基线 | [跨平台助手产品设计](../design/cross-platform-assistant-product-design.md)、[长期助手路线图](../roadmap/long-term-assistant-roadmap.md)、[统一界面设计系统](../../UI-DESIGN.md) |
|
||||
| 相关基线 | [长期助手路线图](../../roadmap/long-term-assistant-roadmap.md)、[统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义 GoodBuddy 中类似自然通话的全双工实时语音能力,包括本地与云端语音引擎、
|
||||
音频平面、会话状态、打断语义、工具审批、数据留存、失败恢复、跨平台交付和验收指标。
|
||||
@@ -831,7 +831,7 @@ Endpoint 不能因为 URL 相似就被标记为实时语音。
|
||||
- 本地模块化引擎作为 Windows、macOS、Linux x64/arm64 的统一功能基线。
|
||||
- 在线 ASR、VAD 和轻量 TTS 权重不内置,继续使用按需下载和 ZIP 离线迁移。
|
||||
- GoodBuddy 托管模型的下载遵守
|
||||
[平台功能页签与模型下载源设计](./model-download-source-design.md),使用用户显式选择的
|
||||
[平台功能页签与模型下载源设计](../../architecture/model-download-source-design.md),使用用户显式选择的
|
||||
ModelScope 或 Hugging Face,失败时不切换来源。
|
||||
- 云端 Adapter 在六个平台复用同一契约,并分别验证 Electron WebRTC/WebSocket 行为。
|
||||
- 本地原生引擎按 Adapter 声明平台与硬件,不伪装成全平台能力。
|
||||
@@ -0,0 +1,33 @@
|
||||
# 图片生成与连续修改
|
||||
|
||||
本目录维护直连图片模型的生成、参考图编辑和会话续接行为。这是已有生图流程的修复,
|
||||
不增加独立模型、工具审批或服务端会话管理。
|
||||
|
||||
## 产品行为
|
||||
|
||||
- 首次无附图时生成新图片。同一桌面对话继续请求时,自动引用最近一次成功回复的图片,
|
||||
同时发送此前的文字要求;本轮修改要求优先。
|
||||
- 本轮显式附图优先,不自动混入上一轮生成图。要独立生成不相关图片,使用新对话。
|
||||
- 已保存会话重新打开后仍可继续改图,引用已有成果,不依赖 Runtime 内存缓存。
|
||||
- 上游明确不支持编辑时,继续按文字生成,并在该回复底部说明未使用参考图片。
|
||||
缺失历史成果时使用剩余可用上下文并提示,不禁用发送。
|
||||
- 网络、鉴权、限流、参数错误和真正的生成失败仍正常报错,不伪装成能力缺失。
|
||||
- 模型 ID、地址和质量服从用户的模型连接配置,不对特定服务商或模型别名做硬编码。
|
||||
验证以用户指定的 `sub-gpt-image-2` 实际服务能力为准,不由其他模型的失败推断其能力。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | 请求、图片引用、协议与失败处理 |
|
||||
| [UI 设计](./ui-design.md) | 消息底部提示及交互边界 |
|
||||
| [功能进度](./progress.md) | 已验证事实和剩余验收 |
|
||||
|
||||
本次只补齐已有工作流,产品规则集中于本页,不另建重复的 PRD、User Stories 或状态机文档。
|
||||
|
||||
## 相关功能
|
||||
|
||||
- [模型连接](../model-connections/README.md):地址、模型 ID、凭据与请求定制。
|
||||
- [直连文本 Agent](../direct-model-agent/README.md):文本模型的工具和上下文压缩,
|
||||
不等于图片模型支持 Responses 图片工具。
|
||||
- [远程主机](../remote-host/README.md):托管 SSH 当前使用 OpenCode,不运行本直连图片路径。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 图片上下文验证记录
|
||||
|
||||
## 2026-09-10
|
||||
|
||||
- 已接线桌面会话成果引用 → Main 已存图片 → Images 编辑,并复用文字历史。
|
||||
- 已增加消息底部的能力缺失提示及 SQLite metadata 持久化,无新增表或服务端会话状态。
|
||||
- 图片上下文专项测试 20 项通过;IPC 成果引用和提示透传专项测试通过。
|
||||
- Renderer 现有生图测试扩展为验证纯图片消息后的追问及提示;消息组件验证提示不产生
|
||||
alert 或 live region;数据库回归验证提示 metadata 保存。
|
||||
- Windows 隔离桌面实例通过真实发送按钮调用用户指定的 `sub-gpt-image-2`:
|
||||
首次生成红圆和绿方块;不重新附图即可把圆改蓝;终止并重开整个测试 Electron 进程后,
|
||||
再次不附图即可保留蓝圆并把方块改黄。实际路由依次为 generations、edits、edits,
|
||||
三次均 HTTP 200,图片颜色和成果恢复已检查,正常续接不出现能力缺失提示。
|
||||
- 在同一个测试实例的请求边界注入一次明确的编辑 HTTP 405,验证生产 Runtime 自动发出
|
||||
一次真实 generations 请求并成功生成第四张图片。消息底部提示未使用参考图片,没有
|
||||
错误状态、alert 或 live region;重新加载会话后提示仍然保留。这是缺失能力的模拟场景,
|
||||
不表示实测的 `sub-gpt-image-2` 上游缺少编辑能力。
|
||||
- 本次实现验收共 4 次真实图片请求、0 次真实文本请求;另有 1 次本地模拟的编辑拒绝。
|
||||
此前接口研究阶段的 8 次请求不计入这 4 次。
|
||||
- 当前源码通过 electron-vite 开发版 Main/Preload 构建及真实 Renderer 链路;本次没有
|
||||
修改生产构建配置,未运行发布打包或独立生产构建。
|
||||
- `npm run typecheck`、`npm run lint` 通过;新文档 7 个本地链接检查通过。
|
||||
- `npm test`:3896 passed、66 skipped、2 failed。失败项为 `App.test.tsx` 中
|
||||
`switches immediately to a managed SSH project and shows only supported OpenCode choices`
|
||||
和 `shows one configured choice per Agent Runtime in a flat keyboard menu`,对应工作区
|
||||
中并行 Runtime 菜单变更的选项数量断言,单独复测仍失败;不在本次图片修复中改写。
|
||||
图片专项、IPC、消息组件和数据库相关测试通过,图片 App 专项也已独立复测通过。
|
||||
@@ -0,0 +1,49 @@
|
||||
# 图片上下文技术设计
|
||||
|
||||
## 请求与存储
|
||||
|
||||
Renderer 从当前完整会话快照选择最近一条 `complete` 助手消息的 `artifactIds`,通过
|
||||
`AgentRequest.imageContextArtifactIds` 传递最多 8 个成果 ID。图片消息可以没有正文,
|
||||
因此引用选择不使用过滤后的文字历史。队列在实际发送时使用当时的会话快照。
|
||||
普通文本历史契约保持 `role/content`;图片字节不进入历史 IPC,也不新增图片副本。
|
||||
|
||||
Main 仅在 `capability === 'image-generation'` 时调用 `withImageConversationContext`。
|
||||
当前 `ContextManager` 已解析的显式附图优先;否则通过现有 `getArtifact` 加载引用的
|
||||
内联 PNG/JPEG,WebP 使用 Electron 解码为 PNG。缺失或不可用引用形成上下文提示,
|
||||
数据库等基础设施错误仍向上传递。读取不依赖会话的异步保存进度,因为成果已在发送
|
||||
`artifact` 事件前由 Main 保存。
|
||||
|
||||
生成图片继续由 `createImageArtifact` 保存。可选 `imageContextNotice` 随生成事件、
|
||||
公开成果事件和消息 metadata 保存,不新增表、迁移、服务端 ID 或 Runtime 会话缓存。
|
||||
它是该回复的展示事实,不进入下一轮提示词。
|
||||
|
||||
## 上游请求
|
||||
|
||||
`ModelAgentRuntime` 将已有文字历史作为会话数据加入提示词,并把本轮要求放在末尾。
|
||||
不从头截断组合提示词而丢掉最后的修改要求;服务商自己的长度和资源限制仍然有效。
|
||||
有图片使用 `/images/edits`,单图字段为 `image`,多图为 `image[]`;无图使用
|
||||
`/images/generations`。质量、认证及自定义请求字段继续走原有配置合并。
|
||||
|
||||
本路径不接入 Responses 图片工具、不依赖 `previous_response_id`,不额外调用文本模型。
|
||||
显式回传上下文不表示上游提供或保存了服务端会话;无法据此保证像素级不变。
|
||||
结果仅接受原有校验通过的内联图片,不下载 Provider 返回的图片 URL。
|
||||
|
||||
## 能力缺失与错误
|
||||
|
||||
编辑返回 HTTP 405/501,或 HTTP 400/404/422 的错误正文明确说明图片编辑不支持或未实现时,
|
||||
同一请求最多再调用一次无图生成。成功成果携带 `editing-unavailable`,继续以 `done`
|
||||
结束;再次失败正常报错,不循环回退。
|
||||
|
||||
其他错误保留 HTTP 状态和 Provider 信息。尤其不把 401/403、429、模型不存在、
|
||||
无效图片、超时或服务暂不可用判作不支持编辑。取消发生后不启动后续生成。
|
||||
本次不持久化能力探测结果、不切换模型、不新增重试框架。
|
||||
|
||||
## Remote 边界与验证
|
||||
|
||||
Renderer 只为直连模型附带成果引用,Main 只在图片能力分支使用它。
|
||||
`managed-remote-acp-runtime.ts` 与远程项目 validator 均要求 OpenCode;
|
||||
本修改不改变 Agent gateway、ACP 或远程模型请求,不需要部署新 Agent。
|
||||
|
||||
专项验证覆盖完整文字、纯图片消息引用、显式附图优先、成果恢复、会话隔离、
|
||||
能力缺失提示、取消、真实错误及 UI/SQLite metadata。真实验收必须从桌面发送按钮进入
|
||||
生产 IPC 和 Runtime,验证生成、连续修改及重新打开后的续接。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 图片上下文提示
|
||||
|
||||
遵循根目录 [UI-DESIGN.md](../../../UI-DESIGN.md)。
|
||||
|
||||
- 仅在某次成功生成确实缺少部分上下文时,在对应助手回复底部显示低强调文字。
|
||||
正常使用上下文不显示警告,也不声称上游“不支持上下文”。
|
||||
- `reference-unavailable`:部分历史图片无法读取,本次仅使用可用的图片与文字上下文。
|
||||
- `editing-unavailable`:上游不支持图片编辑,本次按文字要求生成,未使用参考图片。
|
||||
- 使用共享 `--text-muted`、`--font-caption` 和间距令牌。不加警告卡片、闪烁、
|
||||
`alert`、`aria-live`、弹窗或应用通知。文本保留在消息中并随会话恢复。
|
||||
- 发送和生成继续可用;真实请求失败仍采用原来的错误及重试交互,不生成成功提示。
|
||||
- 文案具备简体中文、英文两套翻译。本轮显式附图和新对话继续使用现有入口,
|
||||
不增加模式开关或第二次确认。
|
||||
@@ -0,0 +1,11 @@
|
||||
# 应用内反馈
|
||||
|
||||
应用内反馈负责用户在 GoodBuddy 中提交反馈、附带受控诊断并获得提交状态。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | 产品流程、数据边界、接口、UI 接入和验收 |
|
||||
|
||||
现有文档是综合设计,尚未按标准职责拆分。
|
||||
+42
-17
@@ -10,7 +10,7 @@
|
||||
| 日期 | 2026-08-25 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 相关基线 | [统一界面设计系统](../../UI-DESIGN.md)、[GoodBuddy 文档导航](../README.md) |
|
||||
| 相关基线 | [统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义 GoodBuddy 桌面端的应用内反馈入口、数据边界、Renderer / Preload /
|
||||
Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平台服务端、管理后台、
|
||||
@@ -45,8 +45,9 @@ Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平
|
||||
以此保证幂等;创建另一条反馈时才生成新 ID。
|
||||
10. GoodBuddy 使用单独的随机安装 UUID 做服务端限流。该 UUID 不得复用 Agent
|
||||
installation ID、硬件 ID、机器名、用户名、项目 ID 或任何设备指纹。
|
||||
11. 默认不发送对话、提示词、模型回复、日志、文件、项目或工作区路径、知识库、模型配置、
|
||||
Runtime 配置、API Key、Token、Cookie、SSH 信息、剪贴板或屏幕内容。
|
||||
11. 默认不发送对话、提示词、模型回复、诊断、日志、文件、项目或工作区路径、知识库、模型
|
||||
配置、Runtime 配置、API Key、Token、Cookie、SSH 信息、剪贴板或屏幕内容。用户可通过
|
||||
默认关闭的 Checkbox 主动附加有界的最近桌面诊断摘要。
|
||||
12. 反馈提交失败时保留全部表单内容和截图,成功后显示可复制的服务端反馈编号。
|
||||
13. 首版不提供反馈历史、状态查询、编辑、撤回、自动日志收集或服务端管理入口。
|
||||
14. 生产接口固定为 `https://imp.mesalogo.com/api/v1/feedback`。域名健康检查、
|
||||
@@ -79,7 +80,8 @@ Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平
|
||||
- GitHub Issue 创建、GitHub Token 或 GitHub 网页附件上传。
|
||||
- GoodBuddy 内的管理员后台、处理状态修改或内部备注。
|
||||
- 用户账户、登录、反馈列表、远程状态查询或推送通知。
|
||||
- 自动附加当前对话、日志、错误堆栈、项目、文件、模型或 Runtime 信息。
|
||||
- 自动附加当前对话、桌面诊断、日志、错误堆栈、项目、文件、模型或 Runtime 信息。
|
||||
- 远端 Agent 日志或通用日志、诊断包和附件框架。
|
||||
- 自动截取全屏、当前窗口、浏览器、远程桌面或其他应用窗口。
|
||||
- 多截图、视频、录屏、任意文件附件或压缩包。
|
||||
- 离线队列、后台自动重试、指数退避、跨重启草稿或发送历史。
|
||||
@@ -113,16 +115,23 @@ Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平
|
||||
- 使用体验:`experience`
|
||||
- 其他:`other`
|
||||
2. **标题**,必填,1 至 120 字符。
|
||||
3. **详细描述**,必填,去除首尾空白后 10 至 5,000 字符。
|
||||
4. **联系邮箱**,可选,最多 254 字符。
|
||||
5. **截图**,可选,最多一张,支持选择 PNG/JPEG/WebP 或在对话框中粘贴图片。
|
||||
6. **将发送的信息**,只读显示应用版本、系统、架构和界面语言。
|
||||
7. **隐私说明**,紧邻提交操作。
|
||||
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 字符以及分隔换行;超长草稿保留在表单中并显示字段错误,不静默截断。
|
||||
|
||||
截图区域必须包含:
|
||||
|
||||
@@ -167,7 +176,7 @@ Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平
|
||||
| `productKey` | Main 常量 `goodbuddy` | 服务端产品路由 |
|
||||
| `category` | 用户选择 | 分流反馈 |
|
||||
| `title` | 用户输入 | 摘要 |
|
||||
| `description` | 用户输入 | 问题和建议详情 |
|
||||
| `description` | 用户输入;勾选时由 Main 追加桌面诊断摘要 | 问题、建议和主动提供的有界诊断详情 |
|
||||
| `contactEmail` | 用户可选输入 | 必要时联系用户 |
|
||||
| `environment.appVersion` | `app.getVersion()` | 判断受影响版本 |
|
||||
| `environment.platform` | `process.platform` 映射 | 判断受影响平台 |
|
||||
@@ -197,8 +206,9 @@ Main 进程职责、公共 API 对接、失败处理和验收方法。反馈平
|
||||
- 剪贴板、屏幕、摄像头、麦克风或其他应用内容。
|
||||
- Remote Agent installation ID、Runtime ID、Conversation ID、Task ID 或 Project ID。
|
||||
|
||||
未来若增加日志、诊断包或其他附件,必须单独设计字段、预览、大小限制、用户选择和服务端
|
||||
保留策略,不能复用截图字段静默上传。
|
||||
除上述用户主动选择、追加到描述且有界的桌面诊断摘要外,未来若增加日志、诊断包或其他
|
||||
附件,必须单独设计字段、预览、大小限制、用户选择和服务端保留策略,不能复用截图字段
|
||||
静默上传。
|
||||
|
||||
### 4.3 本地数据生命周期
|
||||
|
||||
@@ -289,6 +299,7 @@ HTTP 201 表示首次创建,HTTP 200 表示相同 `clientRequestId` 的幂等
|
||||
| 500–599 | 提示服务暂时异常,保留草稿 |
|
||||
| 超时 / DNS / TLS / 离线 | 提示无法连接反馈服务,保留草稿 |
|
||||
| 响应格式错误 | 提示服务返回无效结果,不能猜测成功 |
|
||||
| 本地诊断读取失败 | 不发送反馈,保留草稿、勾选状态和请求 ID,提示重试或取消勾选 |
|
||||
|
||||
首版不自动重试。自动重试容易让用户无法判断是否已经发送,也会在服务持续异常时制造额外
|
||||
流量。用户点击“重试”时复用同一个 `clientRequestId`。
|
||||
@@ -330,6 +341,7 @@ type FeedbackSubmitInput = {
|
||||
category: 'bug' | 'feature' | 'experience' | 'other'
|
||||
title: string
|
||||
description: string
|
||||
includeDiagnostics: boolean
|
||||
contactEmail?: string
|
||||
locale: 'zh-CN' | 'en-US'
|
||||
clientRequestId: string
|
||||
@@ -341,7 +353,7 @@ type FeedbackSubmitInput = {
|
||||
```
|
||||
|
||||
Renderer 不能传入 `productKey`、`appVersion`、`platform`、`architecture` 或
|
||||
`installationId`。这些字段由 Main 从受信运行环境生成。
|
||||
`installationId`,也不能传入诊断内容或路径。这些字段由 Main 从受信运行环境生成。
|
||||
|
||||
### 6.3 IPC 与 Preload
|
||||
|
||||
@@ -402,6 +414,12 @@ Main-only `FeedbackService` 负责:
|
||||
- 映射应用版本、平台和架构。
|
||||
- 验证并标准化可选截图。
|
||||
- 组装严格的公共 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 / 清除本地数据时取消。
|
||||
|
||||
@@ -416,6 +434,11 @@ Main-only `FeedbackService` 负责:
|
||||
|
||||
架构只保留 `x64` 和 `arm64`,其他值映射为 `unknown`。
|
||||
|
||||
公共 API 继续使用 `schemaVersion: 1` 和原有固定字段。`includeDiagnostics` 只属于客户端
|
||||
IPC 输入,不进入公共 payload,也不新增 multipart 字段;Main 把摘要追加到
|
||||
`payload.description`。默认未勾选时,公共 payload 与既有实现逐字段一致。诊断读取失败时
|
||||
不得发送部分摘要或声称已附加,必须返回可本地化的 `diagnostics-unavailable` 错误。
|
||||
|
||||
### 6.6 严格 HTTPS Client
|
||||
|
||||
反馈客户端使用 `undici` 的独立 `Agent`:
|
||||
@@ -618,7 +641,8 @@ Endpoint 的健康检查和无副作用 schema 探针也必须在发布前通过
|
||||
- 普通文本粘贴不被图片处理拦截。
|
||||
- 截图选择、预览、替换、移除和 Object URL 清理。
|
||||
- 并发截图读取按最新选择生效,关闭对话框后不会留下待处理 Object URL。
|
||||
- 失败保留字段,重试使用原 `clientRequestId`。
|
||||
- 失败保留字段、诊断勾选状态,重试使用原 `clientRequestId`。
|
||||
- 默认提交不读取诊断且公共 payload 不变;主动附加、描述预算、无记录和读取失败均有覆盖。
|
||||
- 成功显示真实编号并清空后续新草稿。
|
||||
- Dialog role、初始焦点、Tab 循环、Escape 和焦点恢复。
|
||||
- 浅色、深色、窄窗口和 reduced motion。
|
||||
@@ -666,8 +690,9 @@ npm run build
|
||||
- Endpoint 域名变化属于客户端发布变更,不能依赖 301/302 迁移。
|
||||
- 入口发布与 `goodbuddy` 产品启用状态、生产 TLS 和 v1 契约保持同一发布门槛。
|
||||
- 此功能不迁移现有用户数据;只新增可重置的随机反馈安装身份。
|
||||
- 发布说明应明确:用户现在可以在“关于与更新”内直接提交反馈,邮箱和截图可选,应用不会
|
||||
自动附加对话、日志、文件或凭据。
|
||||
- 发布说明应明确:用户现在可以在“关于与更新”内直接提交反馈,邮箱和截图可选;桌面诊断
|
||||
默认不上传,只有用户勾选后才附加有界摘要,并且不会发送对话、Prompt、凭据、文件内容、
|
||||
路径、Provider 原始响应或远端 Agent 日志。
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 知识库
|
||||
|
||||
知识库已提供本地资料导入、解析、分块、索引、检索、图谱和带来源的知识问答。
|
||||
将 Dify、FastGPT 和 RAGFlow 中已有知识库接入同一检索范围已完成 Main 进程适配器和
|
||||
真实接口验证,但实例管理、绑定、统一检索和界面尚未接通,因此仍不属于当前已提供能力。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [知识库检索与分块增强 PRD](./prd.md) | 检索、分块、诊断和产品验收 |
|
||||
| [User Stories](./user-stories.md) | 知识使用者、维护者和调试者场景 |
|
||||
| [UI 设计](./ui-design.md) | 当前本地知识库的信息架构、首次使用、状态与高级功能分层 |
|
||||
| [实施进度](./progress.md) | 已验证实现事实、验证命令与剩余外部知识库工作 |
|
||||
| [本地文本向量技术设计](./local-embedding-technical-design.md) | 本地与兼容向量连接、模型包、索引和进程边界 |
|
||||
| [知识检索评估](./retrieval-evaluation.md) | 检索质量评估方法、数据集、指标和门槛 |
|
||||
| [外部知识库接入 PRD](./external-knowledge-prd.md) | 外部实例、知识库绑定、检索范围和产品验收 |
|
||||
| [外部知识库 User Stories](./external-knowledge-user-stories.md) | 实例配置、添加、使用和故障恢复场景 |
|
||||
| [外部知识库逻辑设计](./external-knowledge-logic-design.md) | 状态、不变量、配置优先级和失败规则 |
|
||||
| [外部知识库 UI 设计](./external-knowledge-ui-design.md) | 知识库页面、外部实例管理和创建向导交互 |
|
||||
| [外部知识库技术设计](./external-knowledge-technical-design.md) | Provider Adapter、凭据、持久化、IPC 和检索标准化 |
|
||||
| [外部知识库实测基线](./external-knowledge-probe-baseline.json) | 脱敏记录真实实例的接口状态、字段结构、特色能力和耗时 |
|
||||
| [2026-09-10 基础复测](./external-knowledge-probe-2026-09-10-strict.json) | 开启证书校验的目录、详情和基础检索结果 |
|
||||
| [2026-09-10 扩展复测](./external-knowledge-probe-2026-09-10-extended.json) | 配置覆盖、重排、图谱与知识编译开关的请求结果及结构 |
|
||||
|
||||
## 术语
|
||||
|
||||
- **外部实例**:一个由用户配置的 Dify、FastGPT 或 RAGFlow 服务地址及其认证信息。
|
||||
- **外部知识库绑定**:GoodBuddy 中的知识库记录与外部实例内一个远端知识库 ID 的只读关联。
|
||||
- **Provider 配置**:某一 Provider 检索接口独有的设置,例如 RAGFlow 图谱检索。
|
||||
|
||||
外部知识库只提供检索结果。GoodBuddy 不接入外部 App、Chat、Assistant、Workflow
|
||||
或 Agent,也不在外部实例中创建、修改、上传或删除知识库内容。远端资料不会批量同步
|
||||
或进入本地索引;实际用于回答的有界片段会随引用保存在本地会话中。
|
||||
|
||||
本目录尚未拆出统一逻辑设计;当前本地知识库状态规则由 PRD、User Stories 和实现契约
|
||||
共同约束。外部知识库继续使用独立逻辑设计,直至进入实施阶段。
|
||||
@@ -0,0 +1,348 @@
|
||||
# 外部知识库逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联需求 | [外部知识库接入 PRD](./external-knowledge-prd.md) |
|
||||
| 关联场景 | [外部知识库 User Stories](./external-knowledge-user-stories.md) |
|
||||
|
||||
## 1. 不变量
|
||||
|
||||
### EK-I-01 单一知识库身份
|
||||
|
||||
本地知识库和外部知识库都使用 GoodBuddy `knowledgeBaseId` 参与聊天范围、检索模式、
|
||||
工具授权和活动记录。Provider、实例 ID 和远端知识库 ID 不能替代该授权单位。
|
||||
|
||||
对应:`EK-FR-10`、`EK-FR-11`、`EK-US-C1`。
|
||||
|
||||
### EK-I-02 外部绑定只读
|
||||
|
||||
GoodBuddy 对外部实例只执行连接测试、目录读取、详情读取和检索。添加、编辑、移除
|
||||
均只修改本机记录,不调用远端创建、更新、上传或删除接口。
|
||||
|
||||
对应:`EK-FR-15`、`EK-US-A3`。
|
||||
|
||||
### EK-I-03 凭据不离开 Main
|
||||
|
||||
API Key 明文只在 Main 内完成解密并短暂进入请求头。Renderer、Preload 返回值、Runtime、
|
||||
Provider 配置、知识库元数据、诊断、日志和错误只能获得凭据状态。
|
||||
|
||||
对应:`EK-FR-02`、`EK-FR-11`。
|
||||
|
||||
### EK-I-04 Provider 不能定义界面
|
||||
|
||||
远端响应只提供数据和能力证据。可显示字段、字段类型、范围、默认值和请求映射均来自
|
||||
GoodBuddy 内置、版本化的 Provider Adapter。
|
||||
|
||||
对应:`EK-FR-09`、`EK-US-D1`。
|
||||
|
||||
### EK-I-05 无静默切换
|
||||
|
||||
请求固定使用绑定保存的实例、远端知识库和 Provider 配置。失败时不切换实例、公共云
|
||||
地址、认证方式或其他知识库。
|
||||
|
||||
对应:`EK-FR-14`、`EK-FR-16`、`EK-US-C4`。
|
||||
|
||||
### EK-I-06 零结果与失败分离
|
||||
|
||||
成功响应经过过滤后没有片段才是零结果。超时、认证、权限、限流、目标不存在、服务错误
|
||||
和响应无效均为失败,不能折叠成零结果。
|
||||
|
||||
对应:`EK-FR-14`、`EK-US-D4`。
|
||||
|
||||
### EK-I-07 外部证据不可信
|
||||
|
||||
Provider 返回的名称、内容、元数据、URL 和错误均视为不可信输入,经过长度、数量、类型
|
||||
和 URL 校验后才能显示或进入 Runtime。内容中的指令不能改变系统或工具权限。
|
||||
|
||||
对应:`EK-FR-12`。
|
||||
|
||||
### EK-I-08 Provider 配置不跨类型复用
|
||||
|
||||
Dify、FastGPT 和 RAGFlow 使用各自的判别配置。切换知识库类型或实例 Provider 时,旧类型
|
||||
配置不参与新请求,也不能按同名字段自动迁移。
|
||||
|
||||
对应:`EK-FR-06`、`EK-FR-09`。
|
||||
|
||||
## 2. 对象关系
|
||||
|
||||
```text
|
||||
ExternalKnowledgeInstance 1 ─── n ExternalKnowledgeBinding
|
||||
│
|
||||
│ 1:1
|
||||
▼
|
||||
KnowledgeBase
|
||||
```
|
||||
|
||||
- 实例可以没有绑定。
|
||||
- 外部知识库只能绑定一个实例和一个远端知识库 ID。
|
||||
- 同一实例的同一远端知识库默认只允许存在一个绑定,避免重复选择和重复检索。
|
||||
- 允许不同实例绑定同名或相同远端 ID,因为实例边界不同。
|
||||
- 本地知识库不存在 `ExternalKnowledgeBinding`。
|
||||
|
||||
## 3. 实例状态
|
||||
|
||||
实例状态由启停、凭据和最近探测三个维度组成,不能用一个布尔值代替。
|
||||
|
||||
### 3.1 持久状态
|
||||
|
||||
| 维度 | 值 |
|
||||
| --- | --- |
|
||||
| 启停 | `enabled`、`disabled` |
|
||||
| 凭据 | `configured`、`missing`、`unavailable` |
|
||||
| 探测 | `untested`、`catalog-ready`、`healthy`、`list-restricted`、`auth-failed`、`unreachable`、`incompatible`、`failed` |
|
||||
|
||||
`unavailable` 表示系统安全存储当前无法解密或保存凭据,与用户尚未配置的 `missing` 分开。
|
||||
|
||||
### 3.2 派生可用性
|
||||
|
||||
| 条件 | 目录读取 | 已绑定知识库检索 | 页面状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| disabled | 禁止 | 禁止 | 已停用 |
|
||||
| missing / unavailable | 禁止 | 禁止 | 缺少凭据 / 凭据不可用 |
|
||||
| auth-failed | 禁止 | 禁止 | 认证失效 |
|
||||
| unreachable | 可重试 | 可重试 | 无法连接 |
|
||||
| incompatible | 禁止 | 禁止 | 接口不兼容 |
|
||||
| list-restricted | 禁止 | 允许按 ID 验证与检索 | 需手工指定知识库 |
|
||||
| catalog-ready | 允许 | 创建绑定时验证 | 目录可用,检索待验证 |
|
||||
| healthy | 允许 | 允许 | 正常 |
|
||||
| untested | 允许测试 | 已有绑定可尝试检索 | 未测试 |
|
||||
|
||||
保存新实例要求至少完成服务可达和认证验证。目录受限可以保存;检索接口尚未通过时,
|
||||
实例不能用于创建绑定。
|
||||
|
||||
## 4. 连接测试状态机
|
||||
|
||||
```text
|
||||
idle
|
||||
└─ test → connecting
|
||||
├─ network failure → unreachable
|
||||
└─ connected → authenticating
|
||||
├─ 401 → auth-failed
|
||||
├─ incompatible response → incompatible
|
||||
└─ authenticated → listing
|
||||
├─ 403 / unsupported → list-restricted
|
||||
└─ success
|
||||
├─ no target → catalog-ready
|
||||
└─ target selected → probing-retrieval
|
||||
├─ success / empty → healthy
|
||||
└─ failure → failed
|
||||
```
|
||||
|
||||
检索探测需要一个目标知识库。新增实例尚未选择目标时,目录成功即可把实例保存为“目录可用,
|
||||
检索待绑定验证”;创建绑定时补做真实检索。手工模式必须先填写目标 ID,再执行检索探测。
|
||||
检索请求正常完成但返回零条结果时,说明接口和目标可访问,可继续创建绑定;失败状态按错误
|
||||
分类处理。
|
||||
|
||||
每次测试拥有 request ID 和取消信号。用户修改 Provider、地址、凭据或实例后,旧 request ID
|
||||
的结果作废。
|
||||
|
||||
## 5. 目录读取规则
|
||||
|
||||
1. 只有选定、启用且凭据可用的实例可以读取目录。
|
||||
2. Provider 列表接口按适配器定义分页,Main 最多向 Renderer 返回 500 个目录项。
|
||||
3. FastGPT 列表中的文件夹和知识库必须按类型区分;文件夹不可作为绑定目标。
|
||||
4. 列表搜索优先使用 Provider 支持的服务端过滤,不支持时只过滤已经有界加载的当前结果。
|
||||
5. 下拉列表以远端 ID 为值,以远端名称为标签;重名时追加短 ID 或父级名称。
|
||||
6. 已经绑定的目标显示“已添加”并禁止重复添加。
|
||||
7. 远端列表项名称变化不自动修改 GoodBuddy 显示名称,只更新远端名称快照。
|
||||
8. 刷新失败时可以保留上次成功目录供查看,但不得用旧目录完成新绑定,直到目标重新验证。
|
||||
|
||||
## 6. 创建向导状态
|
||||
|
||||
### 6.1 类型
|
||||
|
||||
```text
|
||||
local | dify | fastgpt | ragflow
|
||||
```
|
||||
|
||||
`local` 使用现有字段和验证。外部类型使用以下步骤状态:
|
||||
|
||||
```text
|
||||
provider selected
|
||||
→ instance selected
|
||||
→ catalog loading / manual ID
|
||||
→ remote target selected
|
||||
→ configuration valid
|
||||
→ retrieval verified
|
||||
→ binding created
|
||||
```
|
||||
|
||||
前置字段变化时,后续状态按下表失效:
|
||||
|
||||
| 变化 | 必须清除或重新验证 |
|
||||
| --- | --- |
|
||||
| 类型 | 实例、远端目标、全部 Provider 配置、测试结果 |
|
||||
| 实例 | 远端目标、能力快照、Provider 配置、测试结果 |
|
||||
| 远端目标 | 远端详情、能力适用性、测试结果 |
|
||||
| 通用或 Provider 配置 | 测试结果 |
|
||||
| 显示名称 | 不清除测试结果 |
|
||||
|
||||
## 7. 配置模型
|
||||
|
||||
### 7.1 通用配置
|
||||
|
||||
| 字段 | 范围 | 默认值 | 语义 |
|
||||
| --- | --- | --- | --- |
|
||||
| `resultLimit` | 1 至 20 | 6 | Provider 结果标准化后最多保留的片段数 |
|
||||
| `requestTimeoutMs` | 1,000 至 60,000 | 15,000 | 单次远端请求总时限 |
|
||||
| `maxSnippetCharacters` | 500 至 8,000 | 4,000 | 单片段进入 IPC 和 Runtime 前的字符上限 |
|
||||
|
||||
Provider 请求仍使用自己的候选数量或 Token 预算。`resultLimit` 只定义 GoodBuddy 最终保留
|
||||
上限,不改写 Provider 字段的单位。
|
||||
|
||||
### 7.2 Dify 配置
|
||||
|
||||
| 字段 | 初始范围 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `useDatasetDefaults` | boolean,默认 true | 开启时不发送 `retrieval_model` 覆盖 |
|
||||
| `searchMethod` | keyword / semantic / full-text / hybrid | 对应 Dataset Retrieve 检索方式 |
|
||||
| `providerTopK` | 1 至适配器上限 | Dify 返回候选数量 |
|
||||
| `scoreThresholdEnabled` | boolean | 是否向 Dify 发送分数阈值 |
|
||||
| `scoreThreshold` | 0 至 1 | 开启阈值后生效 |
|
||||
| `rerankingMode` | weighted-score / reranking-model | 覆盖检索配置时必填 |
|
||||
| `rerankingProvider`、`rerankingModel` | 受支持的远端标识 | 选择模型重排时必填 |
|
||||
| `keywordWeight`、`vectorWeight` | 0 至 1,总和为 1 | 加权混合模式时生效 |
|
||||
| `embeddingProvider`、`embeddingModel` | 受支持的远端标识 | Dify 要求时从知识库详情选择或填写 |
|
||||
|
||||
`useDatasetDefaults` 开启时,其余覆盖字段隐藏且不发送。关闭后必须收集当前 Dify 接口
|
||||
构造 `retrieval_model` 所需的完整字段,缺少 Provider 或模型标识时不能保存。
|
||||
|
||||
### 7.3 FastGPT 配置
|
||||
|
||||
| 字段 | 初始范围 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `searchMode` | embedding / fullTextRecall / mixedRecall | 搜索方式 |
|
||||
| `tokenLimit` | 适配器限制,最高 20,000 | Provider 返回内容预算,不等同结果条数 |
|
||||
| `similarity` | 0 至 1 | 最低相关度 |
|
||||
| `usingRerank` | boolean | 使用 FastGPT 重排 |
|
||||
|
||||
初始版本不开放 FastGPT 查询优化字段,避免把查询发送给 FastGPT 配置的 LLM。检索和重排
|
||||
仍可能产生远端 Embedding、Rerank 用量及 API 审计记录,测试与保存配置时必须说明。
|
||||
|
||||
### 7.4 RAGFlow 配置
|
||||
|
||||
| 字段 | 初始范围 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `similarityThreshold` | 0 至 1 | 最低综合相似度 |
|
||||
| `vectorSimilarityWeight` | 0 至 1 | 向量分数权重 |
|
||||
| `knnTopK` | 1 至适配器上限 | KNN 候选数量,不使用已弃用 `top_k` |
|
||||
| `rerankCandidatesCount` | 0 或正整数 | 重排候选数量,实例支持时显示 |
|
||||
| `rerankId` | 可选受限字符串 | 远端重排模型 ID |
|
||||
| `useKg` | boolean,默认 false | 仅在已有图谱证据成立时允许开启 |
|
||||
| `includeKnowledgeCompilation` | boolean,默认 false | 实例支持时显式发送,禁止依赖服务端默认 true |
|
||||
| `metadataCondition` | 后续受控编辑器 | 初始版本不接受任意 JSON 文本 |
|
||||
|
||||
`parser_config.graphrag.use_graphrag` 描述构建状态,不是检索参数。只有列表、详情或受控
|
||||
探测提供已有图谱的证据时,界面才允许开启 `useKg`。Knowledge Compilation 是另一项
|
||||
检索能力,必须与旧 GraphRAG 分别显示和保存。
|
||||
|
||||
## 8. 配置优先级
|
||||
|
||||
生效请求由以下信息组成:
|
||||
|
||||
```text
|
||||
GoodBuddy 固定安全上限
|
||||
∩ 当前 Adapter 支持矩阵
|
||||
∩ 当前绑定保存的通用配置和 Provider 配置
|
||||
= 本次请求配置
|
||||
```
|
||||
|
||||
- 固定安全上限不可被实例或用户扩大。
|
||||
- 显式保存 Provider 参数时,远端默认值只用于创建表单的建议值。Dify
|
||||
`useDatasetDefaults=true` 是例外:每次请求省略 `retrieval_model`,沿用远端当时的配置;
|
||||
远端管理员修改配置后,后续检索随之变化。本地不保存一份默认参数快照。
|
||||
- Provider 增加新字段不会自动启用。
|
||||
- 当前实例版本离开 Adapter 支持矩阵,或 Adapter 新版本无法解析已保存配置时,绑定标记
|
||||
配置失效;用户确认前不重写配置。
|
||||
- 服务端忽略未知字段不能视为成功。Adapter 只发送已知且受支持的字段。
|
||||
|
||||
## 9. 绑定状态
|
||||
|
||||
| 状态 | 条件 | 可进入聊天范围 |
|
||||
| --- | --- | --- |
|
||||
| `ready` | 实例可用,目标最近验证成功,配置有效 | 是 |
|
||||
| `untested` | 迁移或新增后尚未完成真实检索 | 否 |
|
||||
| `instance-disabled` | 实例停用 | 否 |
|
||||
| `credential-error` | 凭据缺失、不可用或认证失败 | 否 |
|
||||
| `remote-missing` | 远端目标返回不存在 | 否 |
|
||||
| `config-invalid` | 已保存配置不再受支持 | 否 |
|
||||
| `temporarily-unavailable` | 超时、限流或服务错误 | 是,但本次显示失败并允许重试 |
|
||||
|
||||
短暂故障不立即永久禁用绑定;认证失败、目标不存在和配置失效会改变持久状态。
|
||||
|
||||
## 10. 多知识库检索
|
||||
|
||||
1. 读取请求冻结的 GoodBuddy 知识库 ID 集合。
|
||||
2. 本地库进入本地检索,外部库按实例和绑定构造独立请求。
|
||||
3. 同一请求最多选择现有契约允许的 20 个知识库,并遵循全局并发上限。
|
||||
4. 每个库单独记录成功、零结果、失败、耗时和截断。
|
||||
5. 成功结果按知识库保留来源,不比较不同 Provider 的原始分数。
|
||||
6. 上下文拼装使用现有总字符预算,在各成功知识库间采用确定性配额。
|
||||
7. 全部失败时返回检索失败;至少一个成功时返回证据和部分失败诊断。
|
||||
|
||||
初始版本不对跨 Provider 结果运行统一学习型重排。后续如增加,必须保留原始 Provider
|
||||
分数和重排来源,且单独定义评估数据。
|
||||
|
||||
## 11. 错误分类
|
||||
|
||||
| 稳定错误码 | 用户状态 | 持久影响 |
|
||||
| --- | --- | --- |
|
||||
| `EXTERNAL_KB_NETWORK` | 无法连接实例 | 无 |
|
||||
| `EXTERNAL_KB_TIMEOUT` | 检索超时 | 无 |
|
||||
| `EXTERNAL_KB_AUTH` | 认证失效 | 实例标记认证失败 |
|
||||
| `EXTERNAL_KB_FORBIDDEN` | 没有访问权限 | 目录或绑定按操作标记受限 |
|
||||
| `EXTERNAL_KB_NOT_FOUND` | 远端知识库不存在 | 绑定标记目标不存在 |
|
||||
| `EXTERNAL_KB_RATE_LIMITED` | 请求过于频繁 | 无,可显示建议等待 |
|
||||
| `EXTERNAL_KB_INCOMPATIBLE` | 接口或配置不兼容 | 实例或绑定标记不兼容 |
|
||||
| `EXTERNAL_KB_INVALID_RESPONSE` | 返回内容无法读取 | 无,记录脱敏诊断 |
|
||||
| `EXTERNAL_KB_SERVER` | 外部服务处理失败 | 无 |
|
||||
| `EXTERNAL_KB_CANCELLED` | 已取消 | 无 |
|
||||
|
||||
Provider 原始正文不能直接成为用户错误。Adapter 将其映射为稳定错误码和有界短消息。
|
||||
|
||||
## 12. 删除规则
|
||||
|
||||
| 操作 | 无绑定 | 有绑定 |
|
||||
| --- | --- | --- |
|
||||
| 停用实例 | 直接停用 | 允许;绑定变为不可用 |
|
||||
| 清除凭据 | 直接清除 | 需要确认;绑定保留但不可用 |
|
||||
| 删除实例 | 直接删除 | 阻止直接删除,要求同时移除本地绑定 |
|
||||
| 移除绑定 | 删除本地绑定 | 不影响实例和远端知识库 |
|
||||
|
||||
删除确认不得使用“删除远端知识库”等文案。
|
||||
|
||||
## 13. 需求追踪
|
||||
|
||||
| 需求 | 主要逻辑 | User Story |
|
||||
| --- | --- | --- |
|
||||
| `EK-FR-01` 至 `EK-FR-03` | 实例状态、连接测试状态机 | `EK-US-A1`、`EK-US-A2` |
|
||||
| `EK-FR-04`、`EK-FR-05` | 目录读取规则、手工目标验证 | `EK-US-B1`、`EK-US-B2` |
|
||||
| `EK-FR-06`、`EK-FR-07`、`EK-FR-08`、`EK-FR-09` | 创建向导状态、配置模型和优先级 | `EK-US-B3`、`EK-US-B4`、`EK-US-D1`、`EK-US-D3` |
|
||||
| `EK-FR-10` 至 `EK-FR-13` | 单一身份、多知识库检索和引用规则 | `EK-US-C1` 至 `EK-US-C3`、`EK-US-D2` |
|
||||
| `EK-FR-14` | 绑定状态、错误分类和部分失败 | `EK-US-C4`、`EK-US-D4` |
|
||||
| `EK-FR-15` | 对象关系和删除规则 | `EK-US-A3` |
|
||||
| `EK-FR-16` | 无静默切换、请求冻结和传输风险确认 | `EK-US-C2` |
|
||||
|
||||
## 14. 逻辑完整性
|
||||
|
||||
已确定:
|
||||
|
||||
- 产品入口、对象关系、只读边界和凭据边界。
|
||||
- 目录读取与手工 ID 的切换条件。
|
||||
- 三种 Provider 的初始配置范围和 RAGFlow 图谱语义。
|
||||
- 多知识库部分失败、删除和版本变化规则。
|
||||
|
||||
实施前仍需技术验证:
|
||||
|
||||
- 已完成第一轮真实目录与最小检索验证,并确认 Dify `1.17.0` 非空结果结构;仍需确认
|
||||
FastGPT、RAGFlow 产品版本和 Dify `0.15.8` 非空结果,再选定三者最低版本并形成固定
|
||||
支持矩阵与去敏 fixture。当前实测详情见
|
||||
[技术设计 9.0 节](./external-knowledge-technical-design.md#90-真实实例验证基线)。
|
||||
- 验证各版本检索探测的 API 审计、用量和内容修改行为。
|
||||
- 确定 Provider 返回 URL 的安全打开策略;未验证前只显示文本定位和远端 ID。
|
||||
- 依据真实延迟确定默认并发上限和跨知识库上下文配额。
|
||||
@@ -0,0 +1,254 @@
|
||||
# 外部知识库接入 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 关联逻辑 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| 关联界面 | [外部知识库 UI 设计](./external-knowledge-ui-design.md) |
|
||||
| 技术设计 | [外部知识库技术设计](./external-knowledge-technical-design.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
不少企业已经在内网部署 Dify、FastGPT 或 RAGFlow,并在其中维护制度、产品、研发
|
||||
和客服知识。继续要求用户把相同资料导入 GoodBuddy,会产生重复存储、版本分叉和
|
||||
额外维护成本。
|
||||
|
||||
GoodBuddy 需要把这些已有知识库加入当前知识库列表和聊天检索范围。外部系统只
|
||||
承担知识存储与检索,问题理解、上下文组织、模型回答、Agent 和工具执行仍由
|
||||
GoodBuddy 负责。
|
||||
|
||||
## 2. 产品决策
|
||||
|
||||
1. 外部知识库属于现有“知识库”功能,不新增一级页面,也不放入全局设置中心。
|
||||
2. 知识库页面标题区增加“管理外部实例”;现有“新建知识库”向导增加知识库类型。
|
||||
3. 一个外部实例代表一个 Provider 服务地址和一组凭据,可绑定多个远端知识库。
|
||||
4. 一个外部知识库绑定在 GoodBuddy 中仍是一个 `KnowledgeBase`,可以进入现有聊天
|
||||
范围、每次先检索、模型按需检索和检索测试流程。
|
||||
5. 外部知识库保持远端检索,不批量同步或本地索引远端文档、分块、向量和图谱,也不
|
||||
显示“已同步”。实际进入回答的有界片段和引用随会话保存在本机,并遵循会话删除规则。
|
||||
6. Dify、FastGPT 和 RAGFlow 只使用知识库列表、详情和检索接口。App、Chat、
|
||||
Assistant、Workflow 和 Agent API 均不接入。
|
||||
7. 用户点击“添加”只创建本地绑定,不在外部实例中创建知识库;移除绑定也不删除
|
||||
远端知识库。
|
||||
8. 通用配置只包含跨 Provider 语义一致的选项。检索算法、阈值、重排和图谱开关
|
||||
由 Provider 配置承担。
|
||||
9. Provider 专属字段由 GoodBuddy 内置适配器定义。外部服务器不能下发任意表单、
|
||||
脚本或字段 Schema。
|
||||
10. API Key 在用户输入表单和受限 IPC 中短暂存在,由 Main 进程加密保存并用于请求;
|
||||
保存后不回传 Renderer,也不进入 Runtime、日志、普通元数据和检索引用。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
### 3.1 用户目标
|
||||
|
||||
- 在知识库页面配置企业现有的 Dify、FastGPT 或 RAGFlow 实例。
|
||||
- 新建知识库时选择 Provider、实例和远端知识库,不手工复制资料。
|
||||
- 在同一知识库列表和聊天选择器中使用本地与外部知识库。
|
||||
- 按 Provider 能力调整检索参数,例如 RAGFlow 图谱检索。
|
||||
- 明确看到查询发送到哪个实例、检索是否成功以及引用来自哪里。
|
||||
- 连接失效后能更新凭据、重试或更换远端知识库,不丢失其他知识库配置。
|
||||
|
||||
### 3.2 产品目标
|
||||
|
||||
- 保持现有知识库 ID 作为聊天和 Runtime 的统一授权单位。
|
||||
- 让三种 Provider 的结果进入同一有界证据与引用契约。
|
||||
- 保留每种 Provider 的有效检索能力,不用最低公共字段覆盖全部差异。
|
||||
- 将实例认证与知识库绑定分离,使一个实例可以服务多个绑定。
|
||||
- 对 Provider 版本差异、列表权限关闭和部分检索失败提供可诊断状态。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
本功能不包含:
|
||||
|
||||
- 调用 Dify App、FastGPT 应用、RAGFlow Chat 或任何外部 Agent。
|
||||
- 使用外部系统生成最终回答或维护外部会话。
|
||||
- 在外部实例中创建、编辑、删除知识库、文档或分块。
|
||||
- 向外部实例上传文件、网页、剪贴板、完整会话或本地知识库内容。
|
||||
- 批量同步或本地索引外部文档、分块、Embedding、图谱或模型配置。回答实际引用的
|
||||
有界片段仍会随会话保存在本机。
|
||||
- 跨实例复制、迁移或统一管理外部知识内容。
|
||||
- 企业 SSO、组织级凭据下发、文档级 ACL 映射或 ShareServer 管理策略。
|
||||
- 自定义脚本适配器和任意 HTTP 请求模板。兼容检索 API 需要独立设计。
|
||||
- 把不同 Provider 的原始分数解释成可直接比较的统一质量分数。
|
||||
|
||||
## 5. 核心对象
|
||||
|
||||
| 对象 | 定义 | 用户可执行操作 |
|
||||
| --- | --- | --- |
|
||||
| 外部实例 | Provider、服务地址、凭据和已探测能力 | 添加、测试、编辑、停用、删除 |
|
||||
| 外部知识库目录项 | 实例列表接口返回的远端知识库摘要 | 刷新、选择 |
|
||||
| 外部知识库绑定 | GoodBuddy 知识库与远端知识库的只读关联 | 添加、改名、配置、测试、移除 |
|
||||
| Provider 配置 | 适配器支持的检索参数 | 查看、修改、恢复默认 |
|
||||
| 标准化检索结果 | Provider 结果转换后的片段、来源和诊断 | 查看、引用、提供给 Runtime |
|
||||
|
||||
## 6. 主要流程
|
||||
|
||||
### 6.1 管理外部实例
|
||||
|
||||
1. 用户在知识库页面点击“管理外部实例”。
|
||||
2. 页面列出实例名称、Provider、服务地址、状态和已绑定知识库数量。
|
||||
3. 用户添加实例,填写名称、Provider、服务地址和 API Key。
|
||||
4. “测试连接”分别验证服务可达、认证、知识库列表和检索能力。
|
||||
5. 用户保存后,API Key 被加密保存,列表只显示凭据已配置。
|
||||
|
||||
列表接口不可用但已知远端知识库 ID 可检索时,实例可以保存为“需手工指定知识库”。
|
||||
服务不可达或认证失败时不得保存为可用实例;用户可以保留表单并修正。
|
||||
|
||||
### 6.2 添加外部知识库
|
||||
|
||||
1. 用户点击现有“新建知识库”。
|
||||
2. 在“知识库类型”中选择“本地”、Dify、FastGPT 或 RAGFlow。
|
||||
3. 选择外部类型后,左侧选择同 Provider 的外部实例。
|
||||
4. GoodBuddy 自动读取远端知识库列表,并填入可搜索下拉框。
|
||||
5. 列表权限不可用时,用户可以手工填写远端知识库 ID。
|
||||
6. 用户选择一个远端知识库,填写 GoodBuddy 显示名称。
|
||||
7. 右侧展示通用配置和当前 Provider 的专属配置。
|
||||
8. 用户填写测试查询并执行一次有界检索;请求成功后创建本地绑定,零结果不算失败。
|
||||
|
||||
添加完成后沿用现有知识库规则,把新绑定加入当前聊天选择,但不会重新启用用户此前
|
||||
取消的其他知识库。
|
||||
|
||||
### 6.3 使用外部知识库
|
||||
|
||||
外部绑定出现在现有知识库列表和聊天知识范围中,持续显示 Provider 与实例名称。
|
||||
检索请求只发送本次查询文本和该绑定已保存的检索配置。返回片段经过 Main 校验、
|
||||
截断和标准化后,才作为不可信证据提供给 Runtime。
|
||||
|
||||
一次请求选择多个知识库时,本地与外部检索可以并行。单个外部知识库失败不扩大
|
||||
检索范围,也不切换其他实例;其他知识库成功时可继续回答,并显示部分失败。
|
||||
|
||||
### 6.4 编辑和移除
|
||||
|
||||
- 修改显示名称只影响 GoodBuddy 本地名称。
|
||||
- 修改 Provider 配置只影响后续检索,不写回远端知识库。
|
||||
- 更换远端知识库属于重新绑定,保存前必须重新测试。
|
||||
- 删除外部实例前必须处理其所有绑定:取消,或明确移除这些本地绑定。
|
||||
- 移除绑定只删除 GoodBuddy 中的绑定和检索配置,不调用远端删除接口。已经保存在历史
|
||||
会话中的引用继续可读,但不再提供依赖当前实例的刷新操作。
|
||||
|
||||
## 7. 功能需求
|
||||
|
||||
### EK-FR-01 知识库页面入口
|
||||
|
||||
知识库页面必须同时提供“管理外部实例”和“新建知识库”。外部实例管理不得迁移到
|
||||
全局设置中心或创建新的一级导航。
|
||||
|
||||
### EK-FR-02 实例配置
|
||||
|
||||
实例必须包含唯一 ID、显示名称、Provider、规范化服务地址、凭据状态、启停状态、
|
||||
最近测试结果和适配器能力快照。用户可以使用 `keep`、`replace`、`clear` 语义更新凭据。
|
||||
|
||||
### EK-FR-03 分项连接测试
|
||||
|
||||
连接测试必须区分服务可达、认证、列表读取和测试检索。新增实例尚未选择知识库时,
|
||||
测试检索显示“绑定知识库时验证”,不能伪装成已通过。测试错误应包含可处理原因,
|
||||
不得返回凭据、Authorization、完整响应正文或内部堆栈。
|
||||
|
||||
### EK-FR-04 远端目录读取
|
||||
|
||||
选择实例后自动读取有界、可分页的远端知识库列表。列表支持加载、刷新、搜索、空结果、
|
||||
权限不足和失败状态,且异步旧响应不得覆盖当前实例结果。
|
||||
|
||||
### EK-FR-05 手工指定远端 ID
|
||||
|
||||
列表接口不受支持或没有权限时,允许手工填写远端知识库 ID 和显示名称。保存前必须通过
|
||||
该 ID 的详情或检索验证;不能只验证字符串非空。
|
||||
|
||||
### EK-FR-06 知识库类型
|
||||
|
||||
新建向导必须提供“本地”、Dify、FastGPT、RAGFlow 四种类型。本地类型保持现有创建、
|
||||
存储模式、图谱和导入行为;外部类型不得显示本地分块、导入或索引设置。
|
||||
|
||||
### EK-FR-07 两栏配置
|
||||
|
||||
外部类型的创建向导在常规宽度使用左右两栏。左侧负责 Provider、实例、远端知识库和
|
||||
显示名称;右侧负责通用配置与 Provider 专属配置。窄窗口按来源、通用配置、专属配置
|
||||
顺序堆叠。
|
||||
|
||||
### EK-FR-08 通用配置
|
||||
|
||||
通用配置至少包含最终返回片段数、请求超时和单片段最大字符数。数值必须有固定范围,
|
||||
由 Main 再次校验。Provider 的 Top K、Token 预算或 Page Size 不得冒充同一字段。
|
||||
|
||||
### EK-FR-09 Provider 专属配置
|
||||
|
||||
Provider 配置使用受版本控制的判别联合。界面只展示当前适配器和已确认实例版本支持的字段;
|
||||
不支持的已保存字段必须明确显示为失效配置并阻止静默提交。
|
||||
|
||||
### EK-FR-10 统一知识库列表
|
||||
|
||||
外部绑定与本地知识库出现在同一列表,但必须显示 Provider、实例和“远端检索”状态。
|
||||
外部绑定不得显示本地文档数、同步、分块、向量覆盖率或本地图谱状态。
|
||||
|
||||
### EK-FR-11 统一检索范围
|
||||
|
||||
现有聊天知识范围、多选上限、`auto` 和 `always` 模式继续以 GoodBuddy 知识库 ID 工作。
|
||||
Runtime 只能获得标准化后的有界证据,不能获得实例地址、API Key 或 Provider 原始响应。
|
||||
|
||||
### EK-FR-12 引用来源
|
||||
|
||||
每条外部结果必须保留 Provider、实例 ID、远端知识库 ID、文档名称和片段。远端文档 ID、
|
||||
远端片段 ID、页码、URL、分数及其他定位只在 Provider 实际返回时保存,不得补造。
|
||||
|
||||
### EK-FR-13 检索测试
|
||||
|
||||
现有检索测试入口支持外部绑定。页面显示生效配置、总耗时、Provider 耗时、返回数量、
|
||||
截断状态和脱敏错误。外部测试不创建聊天、调用 GoodBuddy LLM 或修改远端内容。
|
||||
|
||||
### EK-FR-14 故障隔离
|
||||
|
||||
认证失败、超时、限流、目标删除、版本不兼容和响应无效必须分别报告。多知识库检索中,
|
||||
单项失败不得改写为零结果,也不得阻止已成功证据进入回答。
|
||||
|
||||
### EK-FR-15 生命周期边界
|
||||
|
||||
删除实例、移除绑定、清除凭据和更换远端目标必须经过明确确认。任何操作都不得调用
|
||||
Provider 的远端创建、更新、上传或删除接口。
|
||||
|
||||
### EK-FR-16 网络与数据说明
|
||||
|
||||
界面必须说明查询会发送到所选实例,并列出不会发送的内容。请求遵循用户配置的服务地址
|
||||
和当前内网兼容模式,不自动切换地址、Provider 或公共云端点。HTTP 或放宽证书校验会
|
||||
传输 API Key,测试和保存实例前必须显示风险并取得针对当前地址的明确确认。
|
||||
|
||||
## 8. Provider 初始范围
|
||||
|
||||
| Provider | 目录能力 | 检索能力 | 首批专属配置 |
|
||||
| --- | --- | --- | --- |
|
||||
| Dify | 知识库列表与详情 | Dataset Retrieve | 使用知识库默认配置,或完整覆盖检索方式、Top K、Score 阈值及 Rerank 模式 |
|
||||
| FastGPT | 知识库层级列表与详情 | Dataset Search Test | 搜索模式、Token 上限、相似度、Rerank |
|
||||
| RAGFlow | Dataset 列表与详情 | Retrieval | 相似度、向量权重、KNN Top K、Rerank、图谱检索、Knowledge Compilation |
|
||||
|
||||
“首批专属配置”表示适配器允许表达的字段,不保证每个实例版本都支持。具体显隐由能力
|
||||
探测和[逻辑设计](./external-knowledge-logic-design.md)共同决定。
|
||||
|
||||
## 9. 产品验收
|
||||
|
||||
1. 用户可以在知识库页面完成三种 Provider 的实例添加、连接测试、编辑和删除。
|
||||
2. 每种 Provider 至少通过一个真实自部署实例完成列表读取、绑定和检索。
|
||||
3. 列表权限关闭时,可以通过手工 ID 添加并验证一个可检索知识库。
|
||||
4. 本地与外部知识库可以同时进入一次聊天检索,引用来源可区分。
|
||||
5. RAGFlow 支持时可以开启图谱检索;不支持时不发送该字段。
|
||||
6. 外部知识库详情不出现本地导入、同步、分块和索引操作。
|
||||
7. 删除绑定或实例不会改变远端知识库和文档。
|
||||
8. 保存完成后的 Renderer 状态、Runtime、SQLite 普通元数据、日志和错误中均找不到明文
|
||||
API Key。
|
||||
9. 超时、401/403、404、429、5xx 和响应格式错误都有独立测试并显示准确状态。
|
||||
10. 中断、切换实例或关闭向导后,过期列表与测试响应不会写入当前表单。
|
||||
11. FastGPT 页面说明检索会记录远端 API 使用情况,并可能产生 Embedding 或 Rerank 用量。
|
||||
12. HTTP 或放宽证书校验的实例未经明确确认不能保存或发送 API Key。
|
||||
|
||||
## 10. 外部资料
|
||||
|
||||
以下链接用于核对初始适配范围,实施时仍须用受支持版本的真实实例和响应 fixture 验证:
|
||||
|
||||
- [Dify Knowledge API](https://docs.dify.ai/en/guides/knowledge-base/knowledge-and-documents-maintenance/maintain-dataset-via-api)
|
||||
- [Dify Retrieve Chunks](https://docs.dify.ai/en/api-reference/knowledge-bases/retrieve-chunks-from-a-knowledge-base-test-retrieval)
|
||||
- [FastGPT 知识库接口](https://doc.fastgpt.io/zh-CN/openapi/dataset)
|
||||
- [RAGFlow HTTP API](https://ragflow.io/docs/dev/http_api_reference)
|
||||
- [RAGFlow Release Notes](https://ragflow.io/docs/dev/release_notes)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,828 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"probedAt": "2026-09-10T14:02:51.600Z",
|
||||
"mode": "basic-read-only",
|
||||
"queryCharacters": 3,
|
||||
"privacy": "No credentials, origins, IDs, names, queries, or content are included.",
|
||||
"providers": [
|
||||
{
|
||||
"provider": "dify",
|
||||
"profile": "0.x",
|
||||
"detectedVersion": "0.15.8",
|
||||
"transport": {
|
||||
"protocol": "https:",
|
||||
"tlsVerificationDisabled": false
|
||||
},
|
||||
"catalog": {
|
||||
"success": true,
|
||||
"count": 3,
|
||||
"total": 3,
|
||||
"itemKeys": [
|
||||
"app_count",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"name",
|
||||
"permission",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"tags",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
],
|
||||
"retrievalModelKeys": [
|
||||
"reranking_enable",
|
||||
"reranking_mode",
|
||||
"reranking_model",
|
||||
"score_threshold",
|
||||
"score_threshold_enabled",
|
||||
"search_method",
|
||||
"top_k",
|
||||
"weights"
|
||||
],
|
||||
"indexingTechniques": [
|
||||
"high_quality"
|
||||
],
|
||||
"metadataSchemaCount": 0,
|
||||
"multimodalCount": 0,
|
||||
"pipelineConfiguredCount": 0,
|
||||
"summaryIndexConfiguredCount": 0
|
||||
},
|
||||
"detail": {
|
||||
"success": false,
|
||||
"failure": "http-error",
|
||||
"keys": [
|
||||
"code",
|
||||
"message",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"mode": "dataset-defaults",
|
||||
"success": true,
|
||||
"count": 0,
|
||||
"itemKeys": [],
|
||||
"segmentKeys": [],
|
||||
"documentKeys": [],
|
||||
"childChunkKeys": [],
|
||||
"fileKeys": [],
|
||||
"contentPresent": false,
|
||||
"childContentPresent": false,
|
||||
"summaryPresent": false
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"datasetDefaults": true,
|
||||
"detailSupported": false,
|
||||
"retrievalOverrideObserved": true,
|
||||
"retrievalOverrideProbed": false,
|
||||
"nonEmptyResultObserved": false
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets",
|
||||
"status": 200,
|
||||
"durationMs": 1255,
|
||||
"responseKeys": [
|
||||
"data",
|
||||
"has_more",
|
||||
"limit",
|
||||
"page",
|
||||
"total"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets/{id}",
|
||||
"status": 405,
|
||||
"durationMs": 105,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"message",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-defaults",
|
||||
"method": "POST",
|
||||
"path": "/v1/datasets/{id}/retrieve",
|
||||
"status": 200,
|
||||
"durationMs": 582,
|
||||
"responseKeys": [
|
||||
"query",
|
||||
"records"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider": "dify",
|
||||
"profile": "1.x",
|
||||
"detectedVersion": "1.17.0",
|
||||
"transport": {
|
||||
"protocol": "https:",
|
||||
"tlsVerificationDisabled": false
|
||||
},
|
||||
"catalog": {
|
||||
"success": true,
|
||||
"count": 1,
|
||||
"total": 1,
|
||||
"itemKeys": [
|
||||
"app_count",
|
||||
"author_name",
|
||||
"built_in_field_enabled",
|
||||
"chunk_structure",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"doc_metadata",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"enable_api",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"icon_info",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"is_multimodal",
|
||||
"is_published",
|
||||
"maintainer",
|
||||
"name",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"runtime_mode",
|
||||
"summary_index_setting",
|
||||
"tags",
|
||||
"total_available_documents",
|
||||
"total_documents",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
],
|
||||
"retrievalModelKeys": [
|
||||
"reranking_enable",
|
||||
"reranking_mode",
|
||||
"reranking_model",
|
||||
"score_threshold",
|
||||
"score_threshold_enabled",
|
||||
"search_method",
|
||||
"top_k",
|
||||
"weights"
|
||||
],
|
||||
"indexingTechniques": [
|
||||
"high_quality"
|
||||
],
|
||||
"metadataSchemaCount": 0,
|
||||
"multimodalCount": 0,
|
||||
"pipelineConfiguredCount": 0,
|
||||
"summaryIndexConfiguredCount": 1
|
||||
},
|
||||
"detail": {
|
||||
"success": true,
|
||||
"keys": [
|
||||
"app_count",
|
||||
"author_name",
|
||||
"built_in_field_enabled",
|
||||
"chunk_structure",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"doc_metadata",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"enable_api",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"icon_info",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"is_multimodal",
|
||||
"is_published",
|
||||
"maintainer",
|
||||
"name",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"runtime_mode",
|
||||
"summary_index_setting",
|
||||
"tags",
|
||||
"total_available_documents",
|
||||
"total_documents",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"mode": "dataset-defaults",
|
||||
"success": true,
|
||||
"count": 3,
|
||||
"itemKeys": [
|
||||
"child_chunks",
|
||||
"files",
|
||||
"score",
|
||||
"segment",
|
||||
"summary",
|
||||
"tsne_position"
|
||||
],
|
||||
"segmentKeys": [
|
||||
"answer",
|
||||
"completed_at",
|
||||
"content",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"disabled_at",
|
||||
"disabled_by",
|
||||
"document",
|
||||
"document_id",
|
||||
"enabled",
|
||||
"error",
|
||||
"hit_count",
|
||||
"id",
|
||||
"index_node_hash",
|
||||
"index_node_id",
|
||||
"indexing_at",
|
||||
"keywords",
|
||||
"position",
|
||||
"sign_content",
|
||||
"status",
|
||||
"stopped_at",
|
||||
"tokens",
|
||||
"word_count"
|
||||
],
|
||||
"documentKeys": [
|
||||
"data_source_type",
|
||||
"doc_metadata",
|
||||
"doc_type",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"childChunkKeys": [],
|
||||
"fileKeys": [],
|
||||
"contentPresent": true,
|
||||
"childContentPresent": false,
|
||||
"summaryPresent": false,
|
||||
"scoreRange": {
|
||||
"minimum": 0.5038624,
|
||||
"maximum": 0.6006959
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"datasetDefaults": true,
|
||||
"detailSupported": true,
|
||||
"retrievalOverrideObserved": true,
|
||||
"retrievalOverrideProbed": false,
|
||||
"nonEmptyResultObserved": true
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets",
|
||||
"status": 200,
|
||||
"durationMs": 2261,
|
||||
"responseKeys": [
|
||||
"data",
|
||||
"has_more",
|
||||
"limit",
|
||||
"page",
|
||||
"total"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets/{id}",
|
||||
"status": 200,
|
||||
"durationMs": 1680,
|
||||
"responseKeys": [
|
||||
"app_count",
|
||||
"author_name",
|
||||
"built_in_field_enabled",
|
||||
"chunk_structure",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"doc_metadata",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"enable_api",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"icon_info",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"is_multimodal",
|
||||
"is_published",
|
||||
"maintainer",
|
||||
"name",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"runtime_mode",
|
||||
"summary_index_setting",
|
||||
"tags",
|
||||
"total_available_documents",
|
||||
"total_documents",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-defaults",
|
||||
"method": "POST",
|
||||
"path": "/v1/datasets/{id}/retrieve",
|
||||
"status": 200,
|
||||
"durationMs": 3814,
|
||||
"responseKeys": [
|
||||
"query",
|
||||
"records"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider": "fastgpt",
|
||||
"transport": {
|
||||
"protocol": "https:",
|
||||
"tlsVerificationDisabled": false
|
||||
},
|
||||
"catalog": {
|
||||
"success": true,
|
||||
"count": 1,
|
||||
"envelope": "data-array",
|
||||
"itemKeys": [
|
||||
"_id",
|
||||
"avatar",
|
||||
"createTime",
|
||||
"inheritPermission",
|
||||
"intro",
|
||||
"name",
|
||||
"permission",
|
||||
"private",
|
||||
"sourceMember",
|
||||
"tmbId",
|
||||
"type",
|
||||
"updateTime",
|
||||
"vectorModel"
|
||||
],
|
||||
"itemTypes": [
|
||||
"dataset"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"keys": [
|
||||
"_id",
|
||||
"agentModel",
|
||||
"agentModelId",
|
||||
"avatar",
|
||||
"deleteTime",
|
||||
"inheritPermission",
|
||||
"intro",
|
||||
"name",
|
||||
"parentId",
|
||||
"permission",
|
||||
"status",
|
||||
"teamId",
|
||||
"tmbId",
|
||||
"type",
|
||||
"updateTime",
|
||||
"vectorModel",
|
||||
"vectorModelId",
|
||||
"vlmModel",
|
||||
"vlmModelId"
|
||||
],
|
||||
"vectorModelKeys": [
|
||||
"avatar",
|
||||
"charsPointsPrice",
|
||||
"config",
|
||||
"isActive",
|
||||
"isCustom",
|
||||
"model",
|
||||
"modelId",
|
||||
"name",
|
||||
"provider",
|
||||
"scope",
|
||||
"testMode",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"searchMode": "embedding",
|
||||
"success": true,
|
||||
"envelope": "data-list",
|
||||
"count": 8,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true,
|
||||
"scoreTypes": [
|
||||
"array"
|
||||
],
|
||||
"scoreItemKeys": [
|
||||
"index",
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"scoreValueRange": {
|
||||
"minimum": 0.27009665966033936,
|
||||
"maximum": 0.3160914182662964
|
||||
},
|
||||
"rerankEnabled": false
|
||||
},
|
||||
{
|
||||
"searchMode": "fullTextRecall",
|
||||
"success": true,
|
||||
"envelope": "data-list",
|
||||
"count": 2,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true,
|
||||
"scoreTypes": [
|
||||
"array"
|
||||
],
|
||||
"scoreItemKeys": [
|
||||
"index",
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"scoreValueRange": {
|
||||
"minimum": 0.5047619047619047,
|
||||
"maximum": 0.505
|
||||
},
|
||||
"rerankEnabled": false
|
||||
},
|
||||
{
|
||||
"searchMode": "mixedRecall",
|
||||
"success": true,
|
||||
"envelope": "data-list",
|
||||
"count": 7,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true,
|
||||
"scoreTypes": [
|
||||
"array"
|
||||
],
|
||||
"scoreItemKeys": [
|
||||
"index",
|
||||
"type",
|
||||
"value"
|
||||
],
|
||||
"scoreValueRange": {
|
||||
"minimum": 0.007575757575757576,
|
||||
"maximum": 0.505
|
||||
},
|
||||
"rerankEnabled": false
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"searchModes": [
|
||||
"embedding",
|
||||
"fullTextRecall",
|
||||
"mixedRecall"
|
||||
],
|
||||
"rerankProbed": false,
|
||||
"rerankRequestSucceeded": false,
|
||||
"extensionQueryProbed": false
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/list",
|
||||
"status": 200,
|
||||
"businessCode": 200,
|
||||
"durationMs": 1785,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/api/core/dataset/detail",
|
||||
"status": 200,
|
||||
"businessCode": 200,
|
||||
"durationMs": 571,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-embedding",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"businessCode": 200,
|
||||
"durationMs": 1255,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-fullTextRecall",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"businessCode": 200,
|
||||
"durationMs": 777,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-mixedRecall",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"businessCode": 200,
|
||||
"durationMs": 2326,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider": "ragflow",
|
||||
"transport": {
|
||||
"protocol": "http:"
|
||||
},
|
||||
"catalog": {
|
||||
"success": true,
|
||||
"count": 29,
|
||||
"total": 29,
|
||||
"itemKeys": [
|
||||
"artifact_task_finish_at",
|
||||
"artifact_task_id",
|
||||
"avatar",
|
||||
"chunk_count",
|
||||
"chunk_method",
|
||||
"create_date",
|
||||
"create_time",
|
||||
"created_by",
|
||||
"description",
|
||||
"document_count",
|
||||
"embedding_model",
|
||||
"graphrag_task_finish_at",
|
||||
"graphrag_task_id",
|
||||
"id",
|
||||
"language",
|
||||
"mindmap_task_finish_at",
|
||||
"mindmap_task_id",
|
||||
"name",
|
||||
"nickname",
|
||||
"pagerank",
|
||||
"parser_config",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"raptor_task_finish_at",
|
||||
"raptor_task_id",
|
||||
"similarity_threshold",
|
||||
"skill_task_finish_at",
|
||||
"skill_task_id",
|
||||
"status",
|
||||
"tenant_avatar",
|
||||
"tenant_embd_id",
|
||||
"tenant_id",
|
||||
"token_num",
|
||||
"update_date",
|
||||
"update_time",
|
||||
"vector_similarity_weight"
|
||||
],
|
||||
"parserConfigKeys": [
|
||||
"auto_keywords",
|
||||
"auto_questions",
|
||||
"built_in_metadata",
|
||||
"children_delimiter",
|
||||
"chunk_token_num",
|
||||
"compilation_template_group_id",
|
||||
"delimiter",
|
||||
"enable_children",
|
||||
"enable_metadata",
|
||||
"ext",
|
||||
"filename_embd_weight",
|
||||
"graphrag",
|
||||
"html4excel",
|
||||
"image_context_size",
|
||||
"image_table_context_window",
|
||||
"layout_recognize",
|
||||
"llm_id",
|
||||
"metadata",
|
||||
"mineru_formula_enable",
|
||||
"mineru_lang",
|
||||
"mineru_parse_method",
|
||||
"mineru_table_enable",
|
||||
"overlapped_percent",
|
||||
"pages",
|
||||
"parent_child",
|
||||
"raptor",
|
||||
"table_column_mode",
|
||||
"table_column_names",
|
||||
"table_column_roles",
|
||||
"table_context_size",
|
||||
"tag_kb_ids",
|
||||
"task_page_size",
|
||||
"tenant_llm_id",
|
||||
"toc_extraction",
|
||||
"topn_tags"
|
||||
],
|
||||
"graphConfiguredCount": 18,
|
||||
"graphCompletedCount": 16,
|
||||
"knowledgeCompilationConfiguredCount": 0
|
||||
},
|
||||
"detail": {
|
||||
"success": true,
|
||||
"keys": [
|
||||
"artifact_task_finish_at",
|
||||
"artifact_task_id",
|
||||
"avatar",
|
||||
"chunk_count",
|
||||
"chunk_method",
|
||||
"create_date",
|
||||
"create_time",
|
||||
"created_by",
|
||||
"description",
|
||||
"document_count",
|
||||
"embedding_model",
|
||||
"graphrag_task_finish_at",
|
||||
"graphrag_task_id",
|
||||
"id",
|
||||
"language",
|
||||
"mindmap_task_finish_at",
|
||||
"mindmap_task_id",
|
||||
"name",
|
||||
"nickname",
|
||||
"pagerank",
|
||||
"parser_config",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"raptor_task_finish_at",
|
||||
"raptor_task_id",
|
||||
"similarity_threshold",
|
||||
"skill_task_finish_at",
|
||||
"skill_task_id",
|
||||
"status",
|
||||
"tenant_avatar",
|
||||
"tenant_embd_id",
|
||||
"tenant_id",
|
||||
"token_num",
|
||||
"update_date",
|
||||
"update_time",
|
||||
"vector_similarity_weight"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"feature": "baseline",
|
||||
"success": true,
|
||||
"count": 2,
|
||||
"itemKeys": [
|
||||
"content",
|
||||
"content_ltks",
|
||||
"dataset_id",
|
||||
"doc_type_kwd",
|
||||
"document_id",
|
||||
"document_keyword",
|
||||
"id",
|
||||
"image_id",
|
||||
"important_keywords",
|
||||
"mom_id",
|
||||
"positions",
|
||||
"row_id",
|
||||
"similarity",
|
||||
"tag_kwd",
|
||||
"term_similarity",
|
||||
"vector_similarity"
|
||||
],
|
||||
"contentPresent": true,
|
||||
"scoreRange": {
|
||||
"minimum": 0.18431304078777125,
|
||||
"maximum": 0.2623782504333604
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"graphRetrievalProbed": false,
|
||||
"knowledgeCompilationProbed": false,
|
||||
"metadataConditionProbed": false
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/datasets",
|
||||
"status": 200,
|
||||
"businessCode": 0,
|
||||
"durationMs": 2817,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"total_datasets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/datasets",
|
||||
"status": 200,
|
||||
"businessCode": 0,
|
||||
"durationMs": 159,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"total_datasets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-baseline",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/retrieval",
|
||||
"status": 200,
|
||||
"businessCode": 0,
|
||||
"durationMs": 776,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"probedAt": "2026-09-09T16:25:05.512Z",
|
||||
"mode": "extended-read-only",
|
||||
"queryCharacters": 3,
|
||||
"privacy": "No credentials, origins, IDs, names, queries, or content are included.",
|
||||
"providers": [
|
||||
{
|
||||
"provider": "dify",
|
||||
"profile": "0.x",
|
||||
"detectedVersion": "0.15.8",
|
||||
"transport": {
|
||||
"protocol": "https:",
|
||||
"tlsVerificationDisabled": true
|
||||
},
|
||||
"catalog": {
|
||||
"count": 3,
|
||||
"total": 3,
|
||||
"itemKeys": [
|
||||
"app_count",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"name",
|
||||
"permission",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"tags",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
],
|
||||
"retrievalModelKeys": [
|
||||
"reranking_enable",
|
||||
"reranking_mode",
|
||||
"reranking_model",
|
||||
"score_threshold",
|
||||
"score_threshold_enabled",
|
||||
"search_method",
|
||||
"top_k",
|
||||
"weights"
|
||||
],
|
||||
"indexingTechniques": [
|
||||
"high_quality"
|
||||
],
|
||||
"metadataSchemaCount": 0,
|
||||
"multimodalCount": 0,
|
||||
"pipelineConfiguredCount": 0,
|
||||
"summaryIndexConfiguredCount": 0
|
||||
},
|
||||
"detail": {
|
||||
"keys": [
|
||||
"code",
|
||||
"message",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"mode": "dataset-defaults",
|
||||
"count": 0,
|
||||
"itemKeys": [],
|
||||
"segmentKeys": [],
|
||||
"documentKeys": [],
|
||||
"childChunkKeys": [],
|
||||
"fileKeys": [],
|
||||
"contentPresent": false,
|
||||
"childContentPresent": false,
|
||||
"summaryPresent": false
|
||||
},
|
||||
{
|
||||
"mode": "explicit-current-config",
|
||||
"count": 0,
|
||||
"itemKeys": [],
|
||||
"segmentKeys": [],
|
||||
"documentKeys": [],
|
||||
"childChunkKeys": [],
|
||||
"fileKeys": [],
|
||||
"contentPresent": false,
|
||||
"childContentPresent": false,
|
||||
"summaryPresent": false
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"datasetDefaults": true,
|
||||
"detailSupported": false,
|
||||
"retrievalOverrideObserved": true,
|
||||
"retrievalOverrideProbed": true,
|
||||
"nonEmptyResultObserved": false
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets",
|
||||
"status": 200,
|
||||
"durationMs": 288,
|
||||
"responseKeys": [
|
||||
"data",
|
||||
"has_more",
|
||||
"limit",
|
||||
"page",
|
||||
"total"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets/{id}",
|
||||
"status": 405,
|
||||
"durationMs": 111,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"message",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-defaults",
|
||||
"method": "POST",
|
||||
"path": "/v1/datasets/{id}/retrieve",
|
||||
"status": 200,
|
||||
"durationMs": 237,
|
||||
"responseKeys": [
|
||||
"query",
|
||||
"records"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-explicit-config",
|
||||
"method": "POST",
|
||||
"path": "/v1/datasets/{id}/retrieve",
|
||||
"status": 200,
|
||||
"durationMs": 160,
|
||||
"responseKeys": [
|
||||
"query",
|
||||
"records"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider": "dify",
|
||||
"profile": "1.x",
|
||||
"detectedVersion": "1.17.0",
|
||||
"transport": {
|
||||
"protocol": "https:",
|
||||
"tlsVerificationDisabled": true
|
||||
},
|
||||
"catalog": {
|
||||
"count": 1,
|
||||
"total": 1,
|
||||
"itemKeys": [
|
||||
"app_count",
|
||||
"author_name",
|
||||
"built_in_field_enabled",
|
||||
"chunk_structure",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"doc_metadata",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"enable_api",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"icon_info",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"is_multimodal",
|
||||
"is_published",
|
||||
"maintainer",
|
||||
"name",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"runtime_mode",
|
||||
"summary_index_setting",
|
||||
"tags",
|
||||
"total_available_documents",
|
||||
"total_documents",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
],
|
||||
"retrievalModelKeys": [
|
||||
"reranking_enable",
|
||||
"reranking_mode",
|
||||
"reranking_model",
|
||||
"score_threshold",
|
||||
"score_threshold_enabled",
|
||||
"search_method",
|
||||
"top_k",
|
||||
"weights"
|
||||
],
|
||||
"indexingTechniques": [
|
||||
"high_quality"
|
||||
],
|
||||
"metadataSchemaCount": 0,
|
||||
"multimodalCount": 0,
|
||||
"pipelineConfiguredCount": 0,
|
||||
"summaryIndexConfiguredCount": 1
|
||||
},
|
||||
"detail": {
|
||||
"keys": [
|
||||
"app_count",
|
||||
"author_name",
|
||||
"built_in_field_enabled",
|
||||
"chunk_structure",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"doc_metadata",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"enable_api",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"icon_info",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"is_multimodal",
|
||||
"is_published",
|
||||
"maintainer",
|
||||
"name",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"runtime_mode",
|
||||
"summary_index_setting",
|
||||
"tags",
|
||||
"total_available_documents",
|
||||
"total_documents",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"mode": "dataset-defaults",
|
||||
"count": 3,
|
||||
"itemKeys": [
|
||||
"child_chunks",
|
||||
"files",
|
||||
"score",
|
||||
"segment",
|
||||
"summary",
|
||||
"tsne_position"
|
||||
],
|
||||
"segmentKeys": [
|
||||
"answer",
|
||||
"completed_at",
|
||||
"content",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"disabled_at",
|
||||
"disabled_by",
|
||||
"document",
|
||||
"document_id",
|
||||
"enabled",
|
||||
"error",
|
||||
"hit_count",
|
||||
"id",
|
||||
"index_node_hash",
|
||||
"index_node_id",
|
||||
"indexing_at",
|
||||
"keywords",
|
||||
"position",
|
||||
"sign_content",
|
||||
"status",
|
||||
"stopped_at",
|
||||
"tokens",
|
||||
"word_count"
|
||||
],
|
||||
"documentKeys": [
|
||||
"data_source_type",
|
||||
"doc_metadata",
|
||||
"doc_type",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"childChunkKeys": [],
|
||||
"fileKeys": [],
|
||||
"contentPresent": true,
|
||||
"childContentPresent": false,
|
||||
"summaryPresent": false,
|
||||
"scoreRange": {
|
||||
"minimum": 0.5038624,
|
||||
"maximum": 0.6006959
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "explicit-current-config",
|
||||
"count": 3,
|
||||
"itemKeys": [
|
||||
"child_chunks",
|
||||
"files",
|
||||
"score",
|
||||
"segment",
|
||||
"summary",
|
||||
"tsne_position"
|
||||
],
|
||||
"segmentKeys": [
|
||||
"answer",
|
||||
"completed_at",
|
||||
"content",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"disabled_at",
|
||||
"disabled_by",
|
||||
"document",
|
||||
"document_id",
|
||||
"enabled",
|
||||
"error",
|
||||
"hit_count",
|
||||
"id",
|
||||
"index_node_hash",
|
||||
"index_node_id",
|
||||
"indexing_at",
|
||||
"keywords",
|
||||
"position",
|
||||
"sign_content",
|
||||
"status",
|
||||
"stopped_at",
|
||||
"tokens",
|
||||
"word_count"
|
||||
],
|
||||
"documentKeys": [
|
||||
"data_source_type",
|
||||
"doc_metadata",
|
||||
"doc_type",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"childChunkKeys": [],
|
||||
"fileKeys": [],
|
||||
"contentPresent": true,
|
||||
"childContentPresent": false,
|
||||
"summaryPresent": false,
|
||||
"scoreRange": {
|
||||
"minimum": 0.5038624,
|
||||
"maximum": 0.6006959
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"datasetDefaults": true,
|
||||
"detailSupported": true,
|
||||
"retrievalOverrideObserved": true,
|
||||
"retrievalOverrideProbed": true,
|
||||
"nonEmptyResultObserved": true
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets",
|
||||
"status": 200,
|
||||
"durationMs": 950,
|
||||
"responseKeys": [
|
||||
"data",
|
||||
"has_more",
|
||||
"limit",
|
||||
"page",
|
||||
"total"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/v1/datasets/{id}",
|
||||
"status": 200,
|
||||
"durationMs": 616,
|
||||
"responseKeys": [
|
||||
"app_count",
|
||||
"author_name",
|
||||
"built_in_field_enabled",
|
||||
"chunk_structure",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"data_source_type",
|
||||
"description",
|
||||
"doc_form",
|
||||
"doc_metadata",
|
||||
"document_count",
|
||||
"embedding_available",
|
||||
"embedding_model",
|
||||
"embedding_model_provider",
|
||||
"enable_api",
|
||||
"external_knowledge_info",
|
||||
"external_retrieval_model",
|
||||
"icon_info",
|
||||
"id",
|
||||
"indexing_technique",
|
||||
"is_multimodal",
|
||||
"is_published",
|
||||
"maintainer",
|
||||
"name",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"provider",
|
||||
"retrieval_model_dict",
|
||||
"runtime_mode",
|
||||
"summary_index_setting",
|
||||
"tags",
|
||||
"total_available_documents",
|
||||
"total_documents",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"word_count"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-defaults",
|
||||
"method": "POST",
|
||||
"path": "/v1/datasets/{id}/retrieve",
|
||||
"status": 200,
|
||||
"durationMs": 2468,
|
||||
"responseKeys": [
|
||||
"query",
|
||||
"records"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-explicit-config",
|
||||
"method": "POST",
|
||||
"path": "/v1/datasets/{id}/retrieve",
|
||||
"status": 200,
|
||||
"durationMs": 801,
|
||||
"responseKeys": [
|
||||
"query",
|
||||
"records"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider": "fastgpt",
|
||||
"transport": {
|
||||
"protocol": "https:",
|
||||
"tlsVerificationDisabled": true
|
||||
},
|
||||
"catalog": {
|
||||
"count": 1,
|
||||
"envelope": "data-array",
|
||||
"itemKeys": [
|
||||
"_id",
|
||||
"avatar",
|
||||
"createTime",
|
||||
"inheritPermission",
|
||||
"intro",
|
||||
"name",
|
||||
"permission",
|
||||
"private",
|
||||
"sourceMember",
|
||||
"tmbId",
|
||||
"type",
|
||||
"updateTime",
|
||||
"vectorModel"
|
||||
],
|
||||
"itemTypes": [
|
||||
"dataset"
|
||||
]
|
||||
},
|
||||
"detail": {
|
||||
"keys": [
|
||||
"_id",
|
||||
"agentModel",
|
||||
"agentModelId",
|
||||
"avatar",
|
||||
"deleteTime",
|
||||
"inheritPermission",
|
||||
"intro",
|
||||
"name",
|
||||
"parentId",
|
||||
"permission",
|
||||
"status",
|
||||
"teamId",
|
||||
"tmbId",
|
||||
"type",
|
||||
"updateTime",
|
||||
"vectorModel",
|
||||
"vectorModelId",
|
||||
"vlmModel",
|
||||
"vlmModelId"
|
||||
],
|
||||
"vectorModelKeys": [
|
||||
"avatar",
|
||||
"charsPointsPrice",
|
||||
"config",
|
||||
"isActive",
|
||||
"isCustom",
|
||||
"model",
|
||||
"modelId",
|
||||
"name",
|
||||
"provider",
|
||||
"scope",
|
||||
"testMode",
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"searchMode": "embedding",
|
||||
"count": 8,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true
|
||||
},
|
||||
{
|
||||
"searchMode": "fullTextRecall",
|
||||
"count": 2,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true
|
||||
},
|
||||
{
|
||||
"searchMode": "mixedRecall",
|
||||
"count": 7,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true
|
||||
},
|
||||
{
|
||||
"searchMode": "mixedRecall",
|
||||
"usingRerank": true,
|
||||
"count": 9,
|
||||
"itemKeys": [
|
||||
"chunkIndex",
|
||||
"collectionId",
|
||||
"datasetId",
|
||||
"id",
|
||||
"q",
|
||||
"score",
|
||||
"sourceId",
|
||||
"sourceName",
|
||||
"updateTime"
|
||||
],
|
||||
"contentPresent": true
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"searchModes": [
|
||||
"embedding",
|
||||
"fullTextRecall",
|
||||
"mixedRecall"
|
||||
],
|
||||
"rerankProbed": true,
|
||||
"extensionQueryProbed": false
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/list",
|
||||
"status": 200,
|
||||
"durationMs": 707,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/api/core/dataset/detail",
|
||||
"status": 200,
|
||||
"durationMs": 293,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-embedding",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"durationMs": 797,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-fullTextRecall",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"durationMs": 300,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-mixedRecall",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"durationMs": 1707,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-rerank",
|
||||
"method": "POST",
|
||||
"path": "/api/core/dataset/searchTest",
|
||||
"status": 200,
|
||||
"durationMs": 1727,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"message",
|
||||
"statusText"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"provider": "ragflow",
|
||||
"transport": {
|
||||
"protocol": "http:"
|
||||
},
|
||||
"catalog": {
|
||||
"count": 28,
|
||||
"total": 28,
|
||||
"itemKeys": [
|
||||
"artifact_task_finish_at",
|
||||
"artifact_task_id",
|
||||
"avatar",
|
||||
"chunk_count",
|
||||
"chunk_method",
|
||||
"create_date",
|
||||
"create_time",
|
||||
"created_by",
|
||||
"description",
|
||||
"document_count",
|
||||
"embedding_model",
|
||||
"graphrag_task_finish_at",
|
||||
"graphrag_task_id",
|
||||
"id",
|
||||
"language",
|
||||
"mindmap_task_finish_at",
|
||||
"mindmap_task_id",
|
||||
"name",
|
||||
"nickname",
|
||||
"pagerank",
|
||||
"parser_config",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"raptor_task_finish_at",
|
||||
"raptor_task_id",
|
||||
"similarity_threshold",
|
||||
"skill_task_finish_at",
|
||||
"skill_task_id",
|
||||
"status",
|
||||
"tenant_avatar",
|
||||
"tenant_embd_id",
|
||||
"tenant_id",
|
||||
"token_num",
|
||||
"update_date",
|
||||
"update_time",
|
||||
"vector_similarity_weight"
|
||||
],
|
||||
"parserConfigKeys": [
|
||||
"auto_keywords",
|
||||
"auto_questions",
|
||||
"built_in_metadata",
|
||||
"children_delimiter",
|
||||
"chunk_token_num",
|
||||
"compilation_template_group_id",
|
||||
"delimiter",
|
||||
"enable_children",
|
||||
"enable_metadata",
|
||||
"ext",
|
||||
"filename_embd_weight",
|
||||
"graphrag",
|
||||
"html4excel",
|
||||
"image_context_size",
|
||||
"image_table_context_window",
|
||||
"layout_recognize",
|
||||
"llm_id",
|
||||
"metadata",
|
||||
"mineru_formula_enable",
|
||||
"mineru_lang",
|
||||
"mineru_parse_method",
|
||||
"mineru_table_enable",
|
||||
"overlapped_percent",
|
||||
"pages",
|
||||
"parent_child",
|
||||
"raptor",
|
||||
"table_column_mode",
|
||||
"table_column_names",
|
||||
"table_column_roles",
|
||||
"table_context_size",
|
||||
"tag_kb_ids",
|
||||
"task_page_size",
|
||||
"tenant_llm_id",
|
||||
"toc_extraction",
|
||||
"topn_tags"
|
||||
],
|
||||
"graphConfiguredCount": 17,
|
||||
"graphCompletedCount": 15,
|
||||
"knowledgeCompilationConfiguredCount": 0
|
||||
},
|
||||
"detail": {
|
||||
"keys": [
|
||||
"artifact_task_finish_at",
|
||||
"artifact_task_id",
|
||||
"avatar",
|
||||
"chunk_count",
|
||||
"chunk_method",
|
||||
"create_date",
|
||||
"create_time",
|
||||
"created_by",
|
||||
"description",
|
||||
"document_count",
|
||||
"embedding_model",
|
||||
"graphrag_task_finish_at",
|
||||
"graphrag_task_id",
|
||||
"id",
|
||||
"language",
|
||||
"mindmap_task_finish_at",
|
||||
"mindmap_task_id",
|
||||
"name",
|
||||
"nickname",
|
||||
"pagerank",
|
||||
"parser_config",
|
||||
"permission",
|
||||
"pipeline_id",
|
||||
"raptor_task_finish_at",
|
||||
"raptor_task_id",
|
||||
"similarity_threshold",
|
||||
"skill_task_finish_at",
|
||||
"skill_task_id",
|
||||
"status",
|
||||
"tenant_avatar",
|
||||
"tenant_embd_id",
|
||||
"tenant_id",
|
||||
"token_num",
|
||||
"update_date",
|
||||
"update_time",
|
||||
"vector_similarity_weight"
|
||||
]
|
||||
},
|
||||
"retrievals": [
|
||||
{
|
||||
"feature": "baseline",
|
||||
"count": 0,
|
||||
"itemKeys": [],
|
||||
"contentPresent": false
|
||||
},
|
||||
{
|
||||
"feature": "graph",
|
||||
"count": 1,
|
||||
"itemKeys": [
|
||||
"content",
|
||||
"content_ltks",
|
||||
"dataset_id",
|
||||
"document_id",
|
||||
"document_keyword",
|
||||
"id",
|
||||
"image_id",
|
||||
"important_keywords",
|
||||
"positions",
|
||||
"similarity",
|
||||
"term_similarity",
|
||||
"vector_similarity"
|
||||
],
|
||||
"contentPresent": true,
|
||||
"scoreRange": {
|
||||
"minimum": 1,
|
||||
"maximum": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"feature": "knowledge-compilation",
|
||||
"count": 0,
|
||||
"itemKeys": [],
|
||||
"contentPresent": false
|
||||
}
|
||||
],
|
||||
"features": {
|
||||
"graphRetrievalProbed": true,
|
||||
"knowledgeCompilationProbed": true,
|
||||
"metadataConditionProbed": false
|
||||
},
|
||||
"operations": [
|
||||
{
|
||||
"name": "catalog",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/datasets",
|
||||
"status": 200,
|
||||
"durationMs": 732,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"total_datasets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail",
|
||||
"method": "GET",
|
||||
"path": "/api/v1/datasets",
|
||||
"status": 200,
|
||||
"durationMs": 157,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data",
|
||||
"total_datasets"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-baseline",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/retrieval",
|
||||
"status": 200,
|
||||
"durationMs": 417,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-graph",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/retrieval",
|
||||
"status": 200,
|
||||
"durationMs": 478,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retrieve-knowledge-compilation",
|
||||
"method": "POST",
|
||||
"path": "/api/v1/retrieval",
|
||||
"status": 200,
|
||||
"durationMs": 331,
|
||||
"responseKeys": [
|
||||
"code",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,724 @@
|
||||
# 外部知识库技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 产品需求 | [外部知识库接入 PRD](./external-knowledge-prd.md) |
|
||||
| 逻辑设计 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| UI 设计 | [外部知识库 UI 设计](./external-knowledge-ui-design.md) |
|
||||
|
||||
## 1. 设计约束
|
||||
|
||||
1. 外部知识库扩展现有知识库领域,不建立独立检索工具、聊天范围或 Runtime 权限模型。
|
||||
2. 外部绑定使用远端检索,不落地远端文档、分块、向量和图谱。
|
||||
3. 所有网络、凭据、Provider 响应校验和结果标准化都在 Main 完成。
|
||||
4. Renderer 只能通过受限 IPC 管理实例、读取目录、保存绑定和发起测试。
|
||||
5. Dify、FastGPT、RAGFlow 使用独立 Adapter 和判别配置,不通过任意 JSON 请求模板实现。
|
||||
6. 首次实现只调用无内容修改语义的列表、详情和检索接口。
|
||||
7. Provider API 变化通过真实 fixture、Adapter 版本和能力快照处理,不通过猜测字段兼容。
|
||||
|
||||
## 2. 现有代码边界
|
||||
|
||||
当前实现中的主要扩展点:
|
||||
|
||||
| 边界 | 现有位置 | 需要的变化 |
|
||||
| --- | --- | --- |
|
||||
| 共享知识契约 | `src/shared/knowledge-contracts.ts`、`src/shared/contracts.ts` | 增加实例、绑定、Provider 配置和外部引用判别联合 |
|
||||
| IPC 名称 | `src/shared/ipc-channels.ts` | 增加实例和目录操作通道 |
|
||||
| Preload | `src/preload/index.ts` | 暴露受限、类型化的外部知识库 API |
|
||||
| Main IPC | `src/main/ipc.ts` | 校验输入、调用服务、投影脱敏结果 |
|
||||
| 知识服务 | `src/main/knowledge/knowledge-service.ts` | 按知识库来源分派本地或外部检索 |
|
||||
| 数据库 | `src/main/knowledge/knowledge-database.ts` | 增加实例、绑定和迁移 |
|
||||
| 内部类型 | `src/main/knowledge/types.ts` | 增加 Provider、实例、绑定和标准化结果类型 |
|
||||
| 知识页面 | `src/renderer/src/KnowledgeWorkspace.tsx` | 实例管理、创建向导类型和外部详情 |
|
||||
| 检索测试 | `src/renderer/src/KnowledgeRetrievalWorkbench.tsx` | 支持本地与外部诊断判别联合 |
|
||||
| 聊天预检索 | `src/main/ipc.ts` | 统一调用 Provider-aware 检索编排 |
|
||||
| Agent 知识工具 | `src/main/agent/knowledge-mcp-gateway.ts` | 与预检索复用同一编排和引用标准化 |
|
||||
|
||||
外部 Provider 不是 `KnowledgeSourceType`。现有 `file | directory | url` 表示会进入本地
|
||||
文档、分块和索引的资料来源;把远端绑定塞入该枚举会错误触发同步、分块和来源打开逻辑。
|
||||
|
||||
## 3. 模块设计
|
||||
|
||||
建议增加:
|
||||
|
||||
```text
|
||||
src/main/knowledge/external/
|
||||
├─ external-knowledge-service.ts
|
||||
├─ external-knowledge-store.ts
|
||||
├─ external-knowledge-errors.ts
|
||||
├─ external-knowledge-http.ts
|
||||
├─ external-knowledge-registry.ts
|
||||
├─ external-knowledge-normalizer.ts
|
||||
└─ providers/
|
||||
├─ dify-adapter.ts
|
||||
├─ fastgpt-adapter.ts
|
||||
└─ ragflow-adapter.ts
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- `ExternalKnowledgeService`:实例、目录、绑定、测试和检索编排。
|
||||
- `ExternalKnowledgeStore`:实例和绑定持久化,凭据加解密。
|
||||
- `ExternalKnowledgeHttp`:超时、取消、大小限制、地址和响应读取。
|
||||
- `ExternalKnowledgeRegistry`:按 Provider 返回内置 Adapter,不接受运行时插件注册。
|
||||
- `ExternalKnowledgeNormalizer`:统一结果边界、截断和引用。
|
||||
- Adapter:构造固定端点与请求,解析 Provider 响应,声明静态配置和能力条件。
|
||||
|
||||
`KnowledgeService` 继续作为聊天、检索测试和 MCP 网关的统一入口。它根据
|
||||
`KnowledgeBase.kind` 调用现有本地检索或 `ExternalKnowledgeService.retrieve`。
|
||||
|
||||
## 4. Provider Adapter
|
||||
|
||||
```ts
|
||||
type ExternalKnowledgeProvider = 'dify' | 'fastgpt' | 'ragflow'
|
||||
|
||||
interface ExternalKnowledgeAdapter<PersistedConfig, CapabilitySnapshot> {
|
||||
readonly provider: ExternalKnowledgeProvider
|
||||
readonly configVersion: number
|
||||
|
||||
normalizeBaseUrl(input: string): URL
|
||||
testConnection(context: AdapterContext): Promise<ConnectionTestResult>
|
||||
listKnowledgeBases(
|
||||
context: AdapterContext,
|
||||
input: CatalogPageInput
|
||||
): Promise<CatalogPage>
|
||||
getKnowledgeBase(
|
||||
context: AdapterContext,
|
||||
remoteKnowledgeBaseId: string
|
||||
): Promise<RemoteKnowledgeBaseSummary>
|
||||
probeRetrieval(
|
||||
context: AdapterContext,
|
||||
input: ProviderRetrieveInput<PersistedConfig>
|
||||
): Promise<ProviderRetrieveResult>
|
||||
retrieve(
|
||||
context: AdapterContext,
|
||||
input: ProviderRetrieveInput<PersistedConfig>
|
||||
): Promise<ProviderRetrieveResult>
|
||||
parseCapabilities(input: unknown): CapabilitySnapshot
|
||||
validateConfig(
|
||||
input: unknown,
|
||||
capabilities: CapabilitySnapshot
|
||||
): PersistedConfig
|
||||
}
|
||||
```
|
||||
|
||||
`probeRetrieval` 可以复用 `retrieve`,但必须使用用户在创建向导中明确输入的测试查询。
|
||||
不得生成会泄露组织信息的默认问题,不得调用 Provider 的 App 或模型问答接口,也不得
|
||||
修改远端知识内容。Provider 自身记录 API 审计或计算检索用量不属于内容修改,必须由界面
|
||||
提前说明。
|
||||
|
||||
Adapter 返回数据对象,不返回 React 组件、HTML 或远端 Schema。Renderer 中的配置定义
|
||||
由共享的 Provider 判别联合和本地字段描述生成。
|
||||
|
||||
## 5. 共享契约
|
||||
|
||||
### 5.1 实例
|
||||
|
||||
```ts
|
||||
type CredentialMutation =
|
||||
| { action: 'keep' }
|
||||
| { action: 'replace'; value: string }
|
||||
| { action: 'clear' }
|
||||
|
||||
interface ExternalKnowledgeInstanceSummary {
|
||||
id: string
|
||||
name: string
|
||||
provider: 'dify' | 'fastgpt' | 'ragflow'
|
||||
baseUrl: string
|
||||
enabled: boolean
|
||||
credentialStatus: 'configured' | 'missing' | 'unavailable'
|
||||
probeStatus:
|
||||
| 'untested'
|
||||
| 'catalog-ready'
|
||||
| 'healthy'
|
||||
| 'list-restricted'
|
||||
| 'auth-failed'
|
||||
| 'unreachable'
|
||||
| 'incompatible'
|
||||
| 'failed'
|
||||
detectedVersion?: string
|
||||
bindingCount: number
|
||||
lastTestedAt?: string
|
||||
lastErrorCode?: string
|
||||
}
|
||||
```
|
||||
|
||||
Renderer 输入使用 `CredentialMutation`,因此新密钥会在输入控件和该次 IPC 中短暂存在。
|
||||
Main 返回值永远没有 `credential`、`apiKey`、`authorization` 或加密 envelope。
|
||||
|
||||
### 5.2 绑定
|
||||
|
||||
```ts
|
||||
interface ExternalKnowledgeBinding {
|
||||
knowledgeBaseId: string
|
||||
instanceId: string
|
||||
remoteKnowledgeBaseId: string
|
||||
remoteName: string
|
||||
remoteDescription?: string
|
||||
commonConfig: ExternalKnowledgeCommonConfig
|
||||
providerConfig: DifyConfig | FastGptConfig | RagflowConfig
|
||||
providerConfigVersion: number
|
||||
adapterVersion: number
|
||||
lastVerifiedAt?: string
|
||||
}
|
||||
```
|
||||
|
||||
`providerConfig` 必须带 `provider` 判别字段,且与实例 Provider 一致。`remoteName` 是用于
|
||||
显示和变化检测的快照,不是授权标识。
|
||||
|
||||
### 5.3 标准化结果
|
||||
|
||||
现有 `KnowledgeRetrievalResult` 假定结果映射到本地 `documentId/sourceId/chunkId`。需要改为
|
||||
引用定位的判别联合,同时保持现有调用方可按统一字段读取片段:
|
||||
|
||||
```ts
|
||||
type KnowledgeReferenceLocator =
|
||||
| {
|
||||
kind: 'local'
|
||||
documentId: string
|
||||
sourceId: string
|
||||
chunkId: string
|
||||
parentChunkId?: string
|
||||
}
|
||||
| {
|
||||
kind: 'external'
|
||||
provider: ExternalKnowledgeProvider
|
||||
instanceId: string
|
||||
remoteKnowledgeBaseId: string
|
||||
remoteDocumentId?: string
|
||||
remoteChunkId?: string
|
||||
providerScore?: number
|
||||
location?: string
|
||||
sourceUrl?: string
|
||||
metadata?: Record<string, string | number | boolean | null>
|
||||
}
|
||||
|
||||
interface UnifiedKnowledgeRetrievalResult {
|
||||
knowledgeBaseId: string
|
||||
documentTitle: string
|
||||
sourceDisplayName: string
|
||||
snippet: string
|
||||
relevance?: number
|
||||
rank: number
|
||||
locator: KnowledgeReferenceLocator
|
||||
}
|
||||
```
|
||||
|
||||
`remoteDocumentId` 和 `remoteChunkId` 只保存 Provider 实际返回的标识,缺失时不得从标题、
|
||||
序号或内容哈希推导。`providerScore` 保留原始值,但不改名为统一 `relevance`。只有经过
|
||||
单独定义和验证的归一化算法才能填写跨 Provider `relevance`。初始跨库拼装按结果配额和
|
||||
稳定 Provider 顺序工作,不按原始分数混排。
|
||||
|
||||
所有共享 Schema 使用严格对象并限制:
|
||||
|
||||
- 查询不超过 4,000 字符;同时遵守 Provider 更小限制,例如 Dify 当前公开接口的
|
||||
250 字符限制,超出时在发送前明确失败,不静默截断查询。
|
||||
- 单次目录页不超过 100 项,总目录最多 500 项。
|
||||
- 标准化结果最多 20 条,单片段最多 8,000 字符。
|
||||
- 标题、ID、定位、URL 和元数据键值均设独立上限。
|
||||
- Provider 原始响应总字节数设固定上限,超限立即取消读取。
|
||||
|
||||
## 6. 持久化
|
||||
|
||||
### 6.1 数据库变化
|
||||
|
||||
保留现有 `knowledge_bases` 表,增加:
|
||||
|
||||
```sql
|
||||
ALTER TABLE knowledge_bases
|
||||
ADD COLUMN kind TEXT NOT NULL DEFAULT 'local'
|
||||
CHECK (kind IN ('local', 'external'));
|
||||
```
|
||||
|
||||
增加实例表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE external_knowledge_instances (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
provider TEXT NOT NULL CHECK (provider IN ('dify', 'fastgpt', 'ragflow')),
|
||||
base_url TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
||||
credential_envelope_json TEXT,
|
||||
probe_status TEXT NOT NULL,
|
||||
detected_version TEXT,
|
||||
capabilities_json TEXT NOT NULL,
|
||||
transport_security TEXT NOT NULL,
|
||||
transport_risk_accepted_url TEXT,
|
||||
transport_risk_accepted_at TEXT,
|
||||
last_tested_at TEXT,
|
||||
last_error_code TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
增加绑定表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE external_knowledge_bindings (
|
||||
knowledge_base_id TEXT PRIMARY KEY
|
||||
REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||||
instance_id TEXT NOT NULL
|
||||
REFERENCES external_knowledge_instances(id) ON DELETE RESTRICT,
|
||||
remote_knowledge_base_id TEXT NOT NULL,
|
||||
remote_name TEXT NOT NULL,
|
||||
remote_description TEXT,
|
||||
common_config_json TEXT NOT NULL,
|
||||
provider_config_json TEXT NOT NULL,
|
||||
provider_config_version INTEGER NOT NULL,
|
||||
adapter_version INTEGER NOT NULL,
|
||||
binding_status TEXT NOT NULL,
|
||||
last_verified_at TEXT,
|
||||
last_success_at TEXT,
|
||||
last_error_code TEXT,
|
||||
UNIQUE(instance_id, remote_knowledge_base_id)
|
||||
);
|
||||
```
|
||||
|
||||
已有知识库迁移为 `kind = 'local'`。外部知识库仍保留现有名称、描述和创建时间,但本地
|
||||
`storageMode`、图谱、分块和本地检索设置不进入外部请求;后续可以在数据库重构时再移除
|
||||
这些历史必填默认值,本功能不为此扩大迁移范围。
|
||||
|
||||
### 6.2 凭据
|
||||
|
||||
复用 `SettingsCredentialCipher` 和 Electron `safeStorage`:
|
||||
|
||||
- `replace`:Main 加密新值后在一个事务中替换 envelope。
|
||||
- `keep`:不读取、不重写现有 envelope。
|
||||
- `clear`:确认后删除 envelope,并把实例凭据状态设为缺失。
|
||||
- 安全存储不可用时拒绝新增或替换,不降级为明文。
|
||||
- 解密只发生在发起请求前;请求结束后不缓存明文。
|
||||
|
||||
实例导出、诊断和数据库普通 JSON 视图均排除 `credential_envelope_json`。
|
||||
|
||||
## 7. IPC 设计
|
||||
|
||||
建议增加独立通道:
|
||||
|
||||
```text
|
||||
knowledge:external-instances:list
|
||||
knowledge:external-instances:test
|
||||
knowledge:external-instances:save
|
||||
knowledge:external-instances:set-enabled
|
||||
knowledge:external-instances:delete
|
||||
knowledge:external-catalog:list
|
||||
knowledge:external-catalog:get
|
||||
knowledge:external-bindings:create
|
||||
knowledge:external-bindings:update
|
||||
knowledge:external-bindings:remove
|
||||
knowledge:external-retrieval:test
|
||||
```
|
||||
|
||||
所有 Handler 必须:
|
||||
|
||||
1. `assertTrustedSender`。
|
||||
2. 用共享 Zod Schema 解析严格输入。
|
||||
3. 根据实例 ID 在 Main 读取地址和凭据,不接受 Renderer 在检索请求中重复传入。
|
||||
4. 使用稳定错误码返回脱敏错误。
|
||||
5. 对网络操作支持 AbortSignal、总超时和响应大小限制。
|
||||
|
||||
目录和测试请求返回 `requestId`。Renderer 仍需校验当前类型、实例和目标与响应关联,避免
|
||||
旧响应覆盖新表单。
|
||||
|
||||
## 8. HTTP 边界
|
||||
|
||||
### 8.1 服务地址
|
||||
|
||||
- 只接受 `http:` 和 `https:`。
|
||||
- 拒绝用户名、密码、查询和片段。
|
||||
- Adapter 负责规范化 Provider 所需 API 根路径,避免用户填写完整操作端点。
|
||||
- 请求只能拼接 Adapter 内置相对路径,远端目录项或错误不能改变目标 URL。
|
||||
- 重定向默认拒绝;若某 Provider 的受支持部署确需重定向,只允许同源且不得跨跳转发送
|
||||
Authorization。
|
||||
- 当前全局“内网兼容模式”决定是否允许 HTTP 和非标准 HTTPS 证书;操作开始后冻结该值。
|
||||
- HTTP 或实际使用证书例外时,Main 返回 `transportSecurity` 风险状态。用户必须针对规范化
|
||||
后的当前地址明确确认,才能保存实例或发送 API Key;地址变化立即使旧确认失效。
|
||||
- 不进行公共云回退,不把自部署地址替换成 Provider 官方地址。
|
||||
|
||||
用户明确配置内网地址属于产品用途,因此不能采用禁止私网地址的通用 SSRF 规则。安全边界
|
||||
依靠显式实例配置、Main-only 请求、固定端点、无任意方法和无远端 URL 跟随。
|
||||
|
||||
### 8.2 请求限制
|
||||
|
||||
- 默认超时 15 秒,用户可在 1 至 60 秒内调整。
|
||||
- 目录、详情和检索分别设置响应字节上限。
|
||||
- JSON 解析后仍执行深度、数组数量、字符串长度和未知字段策略。
|
||||
- 默认不自动重试。用户重试或上层下一次检索会产生新请求。
|
||||
- 取消、应用关闭和实例删除立即终止相关请求。
|
||||
- 日志只记录 Provider、实例 ID、操作、状态码、稳定错误码、耗时和字节数。
|
||||
|
||||
## 9. Provider 映射
|
||||
|
||||
本节依据 2026-09-10 核对的官方文档、官方源码和真实实例响应。实施时应把每个受支持版本的真实响应保存为
|
||||
去敏 fixture,并以 fixture 和真实实例测试作为发布依据。
|
||||
|
||||
官方来源与版本边界:
|
||||
|
||||
- Dify:[Knowledge API 与密钥范围](https://docs.dify.ai/en/api-reference/guides/knowledge)、
|
||||
[列表](https://docs.dify.ai/en/api-reference/knowledge-bases/list-knowledge-bases)、
|
||||
[详情](https://docs.dify.ai/en/api-reference/knowledge-bases/get-knowledge-base)、
|
||||
[检索](https://docs.dify.ai/en/api-reference/knowledge-bases/retrieve-chunks-from-a-knowledge-base-test-retrieval)。
|
||||
缺省配置顺序另对照[官方实现](https://github.com/langgenius/dify/blob/main/api/services/hit_testing_service.py)。
|
||||
- FastGPT:[API 说明](https://doc.fastgpt.io/zh-CN/openapi/intro)和
|
||||
[已停止维护的手工接口页](https://doc.fastgpt.io/zh-CN/openapi/dataset)。当前契约对照
|
||||
[生成文档 Schema](https://github.com/labring/FastGPT/blob/main/packages/global/openapi/core/dataset/api.ts)、
|
||||
[检索处理器](https://github.com/labring/FastGPT/blob/main/projects/app/src/pages/api/core/dataset/searchTest.ts)和
|
||||
[结果项 Schema](https://github.com/labring/FastGPT/blob/main/packages/global/core/dataset/type.ts)。
|
||||
云端 `/apidoc/` 本次只取得应用壳,未读取渲染后的部署 Schema。
|
||||
- RAGFlow:[官网 HTTP API](https://ragflow.io/docs/dev/http_api_reference)正文未能有效提取,
|
||||
采用[官方当前文档原文](https://github.com/infiniflow/ragflow/blob/main/docs/references/http_api_reference.md),并与
|
||||
[v0.24.0 固定版本](https://github.com/infiniflow/ragflow/blob/v0.24.0/docs/references/http_api_reference.md)比较。
|
||||
|
||||
`main` 分支文档与源码会变化,不是部署版本凭据。FastGPT、RAGFlow 的实际产品版本仍未确认。
|
||||
|
||||
### 9.0 真实实例验证基线
|
||||
|
||||
2026-09-09 使用本机未入库的测试地址和 API Key 完成第一轮只读验证。验证只记录状态、
|
||||
字段名、数量和长度,不记录 API Key、知识库名称或片段正文。
|
||||
|
||||
| Provider | 实例证据 | 目录实测 | 检索实测 | 当前结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Dify `0.x` | 响应头 `x-version: 0.15.8` | 列表返回 200 和 3 项;详情 GET 返回 405 | 默认配置和显式当前配置均返回 200,结果为空 | 从目录项读取配置,手工 ID 通过检索验证;非空结果仍未验证 |
|
||||
| Dify `1.x` | 响应头 `x-version: 1.17.0` | 列表返回 200 和 1 项;详情 GET 返回 200 | 默认配置和显式当前配置均返回相同的 3 条非空结果 | 详情和 `records[].segment/document` 已验证;新增摘要索引、元数据、多模态和 Pipeline 能力字段 |
|
||||
| FastGPT | 响应未提供版本,只能确认当前托管实例 | 基址包含 `/api`;列表和详情均返回 200,列表 `data` 直接为数组,本次 1 个 Dataset | embedding 8 条、fullTextRecall 2 条、mixedRecall 7 条、mixedRecall + Rerank 9 条 | 三种检索模式和 Rerank 均已实测;当前结果包含 `q`,未包含 `a` |
|
||||
| RAGFlow | 响应未提供产品版本,Server 为 nginx,不能据此推断版本 | `GET /api/v1/datasets` 返回 200,共 28 项;17 项配置 GraphRAG,15 项有完成证据 | 选定已完成图谱的库,基础检索 0 条,`use_kg=true` 返回 1 条,Knowledge Compilation 0 条 | 图谱检索已产生区别于基础检索的结果;当前没有库具备 Knowledge Compilation 配置证据 |
|
||||
|
||||
2026-09-09 的 Node 测试进程设置了 `NODE_TLS_REJECT_UNAUTHORIZED=0`,因此当时 HTTPS 实例的请求
|
||||
使用了放宽的证书校验。这只能证明接口在当前测试环境可用,不能证明严格 TLS 校验通过。
|
||||
产品实现不得读取该进程级设置作为隐式默认;实例测试和保存必须按 8.1 节处理传输风险。
|
||||
|
||||
2026-09-10 复测显式设置 `NODE_TLS_REJECT_UNAUTHORIZED=1`,两个 Dify 实例和 FastGPT
|
||||
均通过证书校验;RAGFlow 配置为 HTTP,不涉及 TLS。基础 14 次、扩展 19 次,共 33 次只读
|
||||
请求。每个实例从目录选择一个库检索,未遍历全部库;未调用聊天或生成端点,检索仍可能
|
||||
使用远端 Embedding、Rerank 并记录用量。两份报告与逐项结果见[实施进度](./progress.md)。
|
||||
|
||||
### 9.1 Dify
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表 | `GET /v1/datasets` |
|
||||
| 详情 | `GET /v1/datasets/{dataset_id}`,`0.15.8` 不支持、`1.17.0` 支持 |
|
||||
| 检索 | `POST /v1/datasets/{dataset_id}/retrieve` |
|
||||
| 认证 | `Authorization: Bearer <knowledge-api-key>` |
|
||||
|
||||
Adapter 只接受 Knowledge Service API Key,不调用应用 `/info`、Chat 或 Workflow。默认不
|
||||
发送 `retrieval_model`,沿用远端知识库配置。用户选择覆盖时,Adapter 必须一次收集并验证
|
||||
当前接口要求的 `search_method`、`reranking_enable`、`top_k`、`score_threshold_enabled`。
|
||||
开启重排时再验证 `reranking_mode`,以及该模式所需的 Provider、模型或权重;缺少必填
|
||||
标识时不构造请求。显式对象不会与知识库配置逐字段合并。省略整个对象时,服务端优先使用
|
||||
知识库配置,未保存配置时才使用内置默认。响应从
|
||||
`records[].segment`、`score`、文档名称和元数据提取标准化引用。
|
||||
|
||||
当前官方文档说明限定知识库范围的 Key 不能调用列表,但可访问授权 ID 的详情和检索。
|
||||
列表 403 不足以认定密钥无效;本轮测试配置均能列目录,尚未验证 Scoped Key。
|
||||
|
||||
Dify 当前文档限制查询不超过 250 字符。GoodBuddy 不静默截断;超出时检索测试和聊天
|
||||
诊断返回 Provider 限制,并允许后续单独设计查询压缩策略。
|
||||
|
||||
两个实测版本共同返回 `retrieval_model_dict`,键包括搜索方式、Top K、阈值、Rerank 和
|
||||
权重。显式原样复用该配置均返回 200,证明覆盖 envelope 可用,但 UI 编辑后的每种组合仍
|
||||
需单独契约测试。
|
||||
|
||||
版本差异:
|
||||
|
||||
| 能力 | `0.15.8` | `1.17.0` | Adapter 和 UI 规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| Dataset 详情 | GET 返回 405 | GET 返回 200 | 能力探测后调用;旧版从目录项读取配置,不能把 405 视为实例故障 |
|
||||
| 非空检索结构 | 本次未命中 | 默认和显式配置均返回 3 条 | 1.17 已验证 `records[].segment.content`、`segment.document.id/name`、位置和分数映射 |
|
||||
| 元数据 | 无 `doc_metadata` | 字段存在,当前库定义数为 0 | 有实际字段定义时才显示受控元数据过滤器;不提供自由 JSON |
|
||||
| 多模态与附件 | 未返回能力字段 | `is_multimodal` 和 `files` 字段可用,当前库非多模态且结果无附件 | 只显示远端状态;仅在结果实际带 `files` 时显示附件引用 |
|
||||
| 父子分块 | 未验证 | 返回 `child_chunks` 数组,本次为空 | 仅在实际非空时显示子块证据,不补造父子关系 |
|
||||
| 摘要索引 | 未返回能力字段 | 当前库配置了 `summary_index_setting`,本次结果 `summary` 为空 | 概览显示只读状态;仅在结果实际返回摘要时展示 |
|
||||
| Knowledge Pipeline | 未返回能力字段 | `pipeline_id/runtime_mode` 存在,当前库未配置 Pipeline | 只读显示,不提供运行或编辑 Pipeline 的操作 |
|
||||
|
||||
Dify 1.x 官方 RetrievalModel 还声明 `metadata_filtering_conditions`,并支持
|
||||
`keyword_search/semantic_search/full_text_search/hybrid_search`。当前实测库没有 Metadata
|
||||
字段,未执行空条件或猜测字段探测;该配置保持禁用,直到所选库详情返回可用字段定义。
|
||||
|
||||
### 9.2 FastGPT
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表 | `POST /api/core/dataset/list` |
|
||||
| 详情 | `GET /api/core/dataset/detail?id=...` |
|
||||
| 检索 | `POST /api/core/dataset/searchTest` |
|
||||
| 认证 | `Authorization: Bearer <api-key>` |
|
||||
|
||||
列表响应同时包含文件夹和知识库,只有 `type = dataset` 可以绑定。根列表请求发送
|
||||
`parentId: null`;文件夹需按父 ID 继续读取。当前实测列表为 `data[]`,检索为
|
||||
`data.list[]`,这是两个不同端点的结构。旧手工文档还展示检索 `data[]`,探测脚本接受
|
||||
该旧结构并单独记录 envelope;当前客户端只接受检索 `data.list[]`,旧结构尚未经过真实
|
||||
部署验证。不递归猜测任意嵌套。检索映射 `datasetId`、
|
||||
`text`、Token `limit`、`similarity`、`searchMode` 和 `usingReRank`。初始版本不发送查询优化
|
||||
字段,避免调用 FastGPT 配置的 LLM。
|
||||
当前实测响应的 `data` 包含 `list/duration/limit/searchMode/usingReRank/similarity`;结果项
|
||||
包含 `q/id/datasetId/collectionId/sourceName/sourceId/chunkIndex/score`,未返回 `a`。
|
||||
Adapter 以 `q` 为必需片段,`a` 仅在远端实际返回时追加,不能要求 `a` 存在。
|
||||
2026-09-10 实测 `score` 为 `{type, value, index}[]`,与当前官方结果 Schema 一致;
|
||||
旧手工文档中的数值分数不能作为当前统一映射依据。应保留评分类型或按明确规则选分,
|
||||
不能默认取第一项作为总分。当前客户端仅提取数值分数,数组评分的映射仍待实现。
|
||||
当前实例的 embedding、fullTextRecall、mixedRecall 和 mixedRecall + Rerank 均返回 200 和
|
||||
非空结果,因此这四种界面组合可以启用。查询扩展会调用模型,仍不在初始界面和探测脚本中
|
||||
启用。
|
||||
|
||||
FastGPT 4.15.0 起应优先查阅部署实例的自动生成文档,`/apidoc/devapi` 与
|
||||
`/apidoc/systemopenapi` 的认证范围不同,Dev API 中出现不等于允许 API Key 调用。支持矩阵
|
||||
必须按真实版本测试;不能因为健康页可访问就假定列表和检索 Schema 相同。`searchTest`
|
||||
可能记录 `SEARCH_TEST` 等远端审计并更新 API Key 用量;Embedding 和 Rerank 也可能产生
|
||||
费用。GoodBuddy 将这些行为作为检索成本显示,不宣称该接口没有副作用。
|
||||
|
||||
### 9.3 RAGFlow
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表/详情 | `GET /api/v1/datasets` |
|
||||
| 检索 | `POST /api/v1/retrieval` |
|
||||
| 认证 | `Authorization: Bearer <api-key>` |
|
||||
|
||||
检索映射 `question`、`dataset_ids`、`page_size`、`similarity_threshold`、
|
||||
`vector_similarity_weight`、`knn_top_k`、重排、`use_kg` 和
|
||||
`include_knowledge_compilation`。当前文档以 `knn_top_k` 取代 `top_k`;v0.24.0 文档只声明
|
||||
`top_k`,因此当前客户端不发送 `top_k` 的行为不能作为旧版本兼容承诺。
|
||||
`knn_top_k` 控制向量候选数量,`page_size` 控制返回数量;请求成功不能单独证明未知参数
|
||||
已生效。本轮未验证候选数参数的效果,也未确定新字段首次发布版本。
|
||||
HTTP 成功且业务 `code === 0` 才能解析结果。响应从 `data.chunks` 和
|
||||
`data.doc_aggs` 提取片段、文档、位置和原始相似度。当前实测非空 chunk 使用
|
||||
`document_keyword` 表示文档名,并包含 `id/document_id/content/positions/similarity/`
|
||||
`term_similarity/vector_similarity`;初始 Adapter 应按该字段映射,不假定存在
|
||||
`document_name`。
|
||||
|
||||
图谱规则:
|
||||
|
||||
- `parser_config.graphrag.use_graphrag = true` 只表示库配置了 GraphRAG;单独出现该配置不
|
||||
表示图谱已经构建完成。
|
||||
- `graphrag_task_finish_at` 为有效非零时间才作为图谱完成证据,不写回。
|
||||
- `use_kg` 是检索时开关,只在已有图谱和受支持版本均确认后发送。
|
||||
- Knowledge Compilation 与旧 GraphRAG 分别建模。
|
||||
- `include_knowledge_compilation` 使用本地保存的显式布尔值,初始默认 false;即使服务端
|
||||
默认 true,也不依赖字段省略获得隐式行为。
|
||||
- `compilation_template_group_id` 或后续版本的等价受控字段作为 Knowledge Compilation 已
|
||||
配置证据;仅仅请求返回 200 不能启用界面开关。
|
||||
- Provider 没有返回图节点或路径证据时,GoodBuddy 不生成图谱引用。
|
||||
|
||||
2026-09-09 实例 28 个库中 17 个配置 GraphRAG、15 个存在完成证据。对一个已完成库使用同一查询,
|
||||
基础检索返回 0 条,`use_kg=true` 返回 1 条且原始相似度为 1,说明图谱开关对实际结果有
|
||||
影响。当时 28 个库均没有 Knowledge Compilation 配置证据;该请求虽返回 200 和零结果,
|
||||
界面仍必须禁用开关并显示“当前知识库未配置 Knowledge Compilation”。
|
||||
2026-09-10 目录增至 29 项,18 项配置图谱、16 项有完成证据;所选库基础检索 2 条、
|
||||
图谱检索 3 条。Knowledge Compilation 开关请求返回 2 条,目录仍无配置证据,不能据此
|
||||
认定编译内容参与了检索。
|
||||
|
||||
## 10. 检索编排
|
||||
|
||||
```text
|
||||
retrieveMany(knowledgeBaseIds, query, requestContext)
|
||||
├─ load and authorize GoodBuddy knowledge bases
|
||||
├─ partition local and external bindings
|
||||
├─ local: existing retrieval path
|
||||
├─ external: bounded concurrent adapter requests
|
||||
├─ validate and normalize each response
|
||||
├─ allocate deterministic context budget
|
||||
└─ return evidence plus per-library diagnostics
|
||||
```
|
||||
|
||||
预检索和 `knowledge_search` 工具必须调用同一个 `retrieveMany`。如果只修改聊天预检索而
|
||||
未修改 `KnowledgeMcpGateway`,`auto` 和 `always` 会产生不同能力,这是阻断发布的问题。
|
||||
|
||||
### 10.1 上下文预算
|
||||
|
||||
- 每个知识库先保留不超过其 `resultLimit` 的结果。
|
||||
- 先给每个成功知识库一个最小有界配额,再按稳定顺序分配剩余字符。
|
||||
- 不用不同 Provider 原始分数决定跨库优先级。
|
||||
- 片段截断在 Main 完成并在引用中标记。
|
||||
- 相同实例的多个绑定仍按绑定分别请求,除非 Provider 明确支持且 Adapter 实现等价的多
|
||||
Dataset 请求;合并不得改变每个绑定的配置和诊断。
|
||||
|
||||
### 10.2 失败结果
|
||||
|
||||
返回结构包含每个知识库的:
|
||||
|
||||
```ts
|
||||
interface KnowledgeRetrievalOutcome {
|
||||
knowledgeBaseId: string
|
||||
status: 'success' | 'empty' | 'failed' | 'cancelled'
|
||||
durationMs: number
|
||||
resultCount: number
|
||||
errorCode?: ExternalKnowledgeErrorCode
|
||||
truncated: boolean
|
||||
}
|
||||
```
|
||||
|
||||
至少一个 `success` 或 `empty` 表示检索编排完成;全部 `failed/cancelled` 时上层显示整体失败。
|
||||
|
||||
## 11. 引用与来源打开
|
||||
|
||||
外部结果不能进入依赖本地数据库的 `referenceContext` 和 `openSource` 路径。
|
||||
|
||||
- 引用详情直接使用请求时保存的标准化片段和定位。
|
||||
- Main 可以按远端片段 ID 提供一次只读重新读取,但初始版本不要求。
|
||||
- Provider 返回 URL 只作为不可信元数据保存。没有独立 URL 规则和真实实例验证前,
|
||||
Renderer 不显示打开链接操作。
|
||||
- 活动和会话持久化保存标准化引用,其中包含实际用于回答的有界远端片段;这属于引用
|
||||
留存,不是批量同步或本地索引。删除会话或对应活动时按现有生命周期删除该副本。
|
||||
- 实例删除后,历史引用仍显示 Provider、知识库、文档名称和已保存片段,但标记连接已移除。
|
||||
|
||||
## 12. 支持矩阵与版本变化
|
||||
|
||||
每个 Adapter 维护明确的最低版本、已验证版本和 `adapterVersion`。连接测试优先读取可靠的
|
||||
版本接口或响应头;无法识别版本时,只允许经真实请求验证过的基础列表和检索字段,高级字段
|
||||
保持不可用。
|
||||
|
||||
Adapter 可以保存少量受控事实,例如“目录接口可用”“详情表明已构建图谱”,但不尝试从
|
||||
远端拼出完整 capability manifest,也不计算 capability fingerprint。
|
||||
|
||||
- `adapterVersion` 相同且实例仍在支持矩阵内:解析已保存配置并执行。
|
||||
- Adapter 升级:使用明确的配置迁移或把绑定标记为 `config-invalid`,不猜测字段迁移。
|
||||
- 实例版本离开支持矩阵:停止发送高级字段,并要求重新测试和确认。
|
||||
- 新能力出现:保持默认关闭,用户主动配置后才发送。
|
||||
- RAGFlow `use_kg` 还要求当前远端知识库存在已构建图谱的证据。
|
||||
|
||||
## 13. 安全与隐私
|
||||
|
||||
- API Key 在用户输入和保存 IPC 中短暂存在,随后使用系统安全存储加密;保存后不返回
|
||||
Renderer,也不进入 Runtime、日志和普通元数据。
|
||||
- 请求不附带完整会话、附件、本地知识库证据、系统提示、模型配置或其他 Provider 结果。
|
||||
- Provider 结果使用与本地知识证据相同的不可信上下文标记。
|
||||
- Adapter 拒绝响应中的原型污染键、过深对象、超长数组和非有限数值。
|
||||
- 远端错误正文只用于 Main 内映射,日志与 UI 使用稳定错误码和短消息。
|
||||
- 外部知识库不能扩大当前对话显式选择的 GoodBuddy 知识库 ID。
|
||||
- Ask 与 Execute 对知识检索均为只读,Execute 不获得额外 Provider 权限。
|
||||
- Provider 配置中的模型 ID、背景文本和 Metadata 条件按独立长度限制验证,不能携带凭据。
|
||||
- HTTP 或证书例外需要按当前规范化地址确认凭据传输风险,地址变化后重新确认。
|
||||
|
||||
## 14. 测试
|
||||
|
||||
### 14.1 契约测试
|
||||
|
||||
- 每个 Provider 的列表、详情、检索成功和错误 fixture。
|
||||
- 未知字段、缺字段、错误类型、超长内容、非有限分数和超大响应。
|
||||
- Dify 查询长度、FastGPT 文件夹过滤、RAGFlow 图谱和 Knowledge Compilation 字段。
|
||||
- Provider 配置判别联合拒绝类型错配和未知字段。
|
||||
|
||||
### 14.2 安全测试
|
||||
|
||||
- 保存完成后的 Renderer 快照、IPC 返回值、日志、错误、活动和导出中不存在明文 API Key;
|
||||
`replace` 输入只在当前表单和单次 IPC 请求中短暂存在。
|
||||
- `keep/replace/clear` 凭据语义和安全存储不可用路径。
|
||||
- 地址中的凭据、查询、片段、危险 scheme、跨源重定向和远端 URL 注入被拒绝。
|
||||
- 取消、超时、响应大小上限和应用关闭释放请求。
|
||||
|
||||
### 14.3 集成测试
|
||||
|
||||
- 实例保存、目录读取、手工 ID、绑定创建、编辑和本地移除。
|
||||
- 同一远端目标重复绑定被拒绝。
|
||||
- 删除实例的引用保护和级联本地移除确认。
|
||||
- 本地与外部 `retrieveMany` 部分成功、全部失败和确定性上下文预算。
|
||||
- 聊天 `always` 与 Agent `knowledge_search` 使用同一 Provider-aware 编排。
|
||||
- 历史引用在实例或绑定移除后仍可读。
|
||||
|
||||
### 14.4 UI 测试
|
||||
|
||||
- 类型切换、异步旧响应、目录搜索、刷新和手工模式。
|
||||
- 两栏与窄窗口堆叠、键盘、焦点恢复和离开保护。
|
||||
- Provider 字段显隐、失效配置、RAGFlow 图谱说明和恢复默认。
|
||||
- 外部详情不出现导入、同步、分块和本地图谱操作。
|
||||
- 错误、零结果、部分失败和 Provider 引用显示。
|
||||
|
||||
### 14.5 真实实例验收
|
||||
|
||||
每个 Provider 至少选择一个明确版本的自部署实例,记录:
|
||||
|
||||
- 版本、部署方式和认证范围。
|
||||
- 列表与检索的请求/响应 fixture,移除凭据和业务正文。
|
||||
- 默认与专属配置是否真实生效。
|
||||
- 401、403、404、429、超时和取消行为。
|
||||
- 查询、片段、引用和诊断的实际上限。
|
||||
|
||||
Mock 或公开文档通过不能代替真实实例验收。
|
||||
|
||||
2026-09-09 基线已完成 Dify `0.15.8`/`1.17.0` 目录、详情差异和 1.17 非空检索结构,
|
||||
2026-09-10 补充验证了 HTTPS 实例的严格 TLS。FastGPT/RAGFlow 产品版本、真实实例
|
||||
401/403/404/429、超时和取消尚未完成,因此不满足本节完整验收条件。
|
||||
|
||||
### 14.6 可重复探测脚本
|
||||
|
||||
仓库提供 `node scripts/external-knowledge-probe.mjs`。默认从进程环境变量读取
|
||||
`GOODBUDDY_<PROVIDER>_BASE_URL/API_KEY`,本地开发也兼容当前两行格式的 `.env.dify`、
|
||||
`.env.fastgpt` 和 `.env.ragflow`;Dify 文件还支持多个 `标签/URL/API Key` 三行配置块,用于
|
||||
同一次报告比较多个版本。输出不包含地址、凭据、远端 ID、名称、查询和正文。
|
||||
|
||||
```text
|
||||
node scripts/external-knowledge-probe.mjs
|
||||
node scripts/external-knowledge-probe.mjs --provider=ragflow --extended
|
||||
node scripts/external-knowledge-probe.mjs --extended --output=<report.json>
|
||||
```
|
||||
|
||||
默认模式覆盖目录、详情和基础检索。`--extended` 额外执行 Dify 当前配置覆盖、FastGPT
|
||||
Rerank、RAGFlow GraphRAG 和 Knowledge Compilation 检索。脚本只有固定 GET/POST 读取与
|
||||
检索端点,不实现或调用远端创建、更新、上传和删除。
|
||||
|
||||
三家报告均将请求成功与命中数量分开:HTTP 必须成功,FastGPT 业务 `code` 必须为 `200`、
|
||||
RAGFlow 必须为 `0`,并分别校验 Dify `records`、FastGPT `data` 或 `data.list`、RAGFlow
|
||||
`data.chunks` 数组结构,才记录检索 `success: true` 和 `count`。成功的空数组
|
||||
记录 `count: 0`;HTTP、业务、响应结构、JSON 解析或网络失败记录 `success: false` 与
|
||||
固定失败类别,不用零命中代替失败。目录失败时不继续调用详情和检索。
|
||||
`features.searchModes` 只包含基础检索实际成功的模式;`rerankProbed` 表示实际尝试,
|
||||
`rerankRequestSucceeded` 表示请求成功,结果 `rerankEnabled` 单独记录远端实际返回的
|
||||
`usingReRank` 布尔值。FastGPT 另记录检索 envelope、分数类型和分数数组项结构;
|
||||
operation 保留 HTTP 状态、安全业务码及白名单网络错误码,不记录远端错误消息。
|
||||
历史基线保留原始证据,不追填未观测的成功字段。
|
||||
`tests/external-knowledge-probe-fastgpt.test.ts` 和 `tests/external-knowledge-probe-outcomes.test.ts`
|
||||
使用内存 fetch fixture 运行真实 CLI,覆盖错误、成功零命中和非空结果,不连接外部知识服务。
|
||||
|
||||
### 14.7 CRUD 验收边界
|
||||
|
||||
| 对象 | Create | Read | Update | Delete | 验收方式 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| GoodBuddy 外部实例 | 是 | 是 | 是 | 是 | SQLite、凭据存储、IPC 和 Renderer 集成测试 |
|
||||
| GoodBuddy 外部绑定 | 是 | 是 | 是 | 是 | SQLite、统一知识库列表、检索和删除确认测试 |
|
||||
| Provider 远端知识库与内容 | 否 | 目录、详情、检索 | 否 | 否 | 探测脚本确认只有只读端点;代码审查确认无写端点 |
|
||||
|
||||
FastGPT 等 Provider 的公开 API 确实提供远端知识库、集合和数据 CRUD,但这不属于当前产品
|
||||
接入。若未来要验证远端 CRUD,必须先修改 PRD,并使用可删除的专用测试实例或命名空间;
|
||||
不能在本轮三个既有实例上执行破坏性测试。
|
||||
|
||||
## 15. 实施顺序
|
||||
|
||||
这里的顺序只用于控制代码依赖,不裁剪产品设计:
|
||||
|
||||
1. 建立共享判别契约、数据库迁移和凭据存储。
|
||||
2. 实现 HTTP 边界、错误映射和 Adapter Registry。
|
||||
3. 逐个实现三种 Provider,并用 fixture 完成契约测试。
|
||||
4. 实现实例管理和目录 IPC。
|
||||
5. 扩展创建向导、统一列表和外部详情。
|
||||
6. 扩展检索测试、引用和 `retrieveMany`。
|
||||
7. 接入聊天预检索与 `KnowledgeMcpGateway`。
|
||||
8. 完成真实实例、安全、响应式和跨平台验收。
|
||||
|
||||
## 16. 发布条件
|
||||
|
||||
- 三种 Provider 的受支持版本和限制已在产品界面或发布说明中可查。
|
||||
- 真实实例列表、绑定、检索和错误矩阵均通过。
|
||||
- 没有写入远端知识内容的代码路径。
|
||||
- API Key 泄漏测试、地址边界和响应大小测试通过。
|
||||
- 本地知识库创建、同步、检索、图谱和引用回归测试通过。
|
||||
- `auto`、`always`、检索测试和 MCP 知识工具得到一致的外部知识能力。
|
||||
|
||||
## 17. 需求追踪
|
||||
|
||||
| 需求 | 技术落点 | 验证 |
|
||||
| --- | --- | --- |
|
||||
| `EK-FR-01` 至 `EK-FR-03` | 实例契约、Store、实例 IPC、凭据和测试状态 | 实例集成测试、安全测试 |
|
||||
| `EK-FR-04`、`EK-FR-05` | Adapter 目录与详情接口、目录 IPC | Provider fixture、手工 ID 集成测试 |
|
||||
| `EK-FR-06` 至 `EK-FR-09` | Provider 判别联合、Adapter 配置版本、Renderer 本地字段定义 | 契约测试、UI 字段显隐测试 |
|
||||
| `EK-FR-10`、`EK-FR-11` | `KnowledgeBase.kind`、绑定表、统一 `retrieveMany` | 本地与外部混合检索测试 |
|
||||
| `EK-FR-12`、`EK-FR-13` | 引用定位判别联合、结果标准化、检索测试 IPC | 引用持久化与 UI 测试 |
|
||||
| `EK-FR-14` | 稳定错误码、逐库 Outcome、取消和超时 | 错误 fixture、部分失败测试 |
|
||||
| `EK-FR-15` | 外键限制、绑定本地删除、无远端写接口 | 删除生命周期测试 |
|
||||
| `EK-FR-16` | Main-only HTTP、固定端点、传输风险确认 | 地址、证书和凭据泄漏测试 |
|
||||
@@ -0,0 +1,428 @@
|
||||
# 外部知识库 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 产品需求 | [外部知识库接入 PRD](./external-knowledge-prd.md) |
|
||||
| 行为规则 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy UI 设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
## 1. 信息架构
|
||||
|
||||
外部知识库不增加左侧主导航和知识库一级页签。现有知识库页面结构保持:
|
||||
|
||||
```text
|
||||
知识库
|
||||
├─ 页面标题区
|
||||
│ ├─ 管理外部实例
|
||||
│ └─ 新建知识库
|
||||
├─ 知识库列表
|
||||
└─ 当前知识库详情
|
||||
```
|
||||
|
||||
“管理外部实例”管理可复用连接;“新建知识库”负责创建本地知识库或添加一个远端绑定。
|
||||
|
||||
## 2. 页面标题区
|
||||
|
||||
常规宽度:
|
||||
|
||||
```text
|
||||
知识库 [管理外部实例] [新建知识库]
|
||||
导入、检索并维护本地和企业现有知识
|
||||
范围:全局
|
||||
```
|
||||
|
||||
- “新建知识库”保持主按钮。
|
||||
- “管理外部实例”是带服务器图标的次按钮。
|
||||
- 沿用知识库页面的全局 `ScopeBadge`,创建和管理外部连接时持续可见。
|
||||
- 按钮使用文字和图标,不增加只含缩写的厂商快捷入口。
|
||||
- 窄窗口下两个按钮换到标题下方,保持同一行;空间不足时各占一半宽度。
|
||||
|
||||
对应:`EK-FR-01`。
|
||||
|
||||
## 3. 知识库列表
|
||||
|
||||
本地和外部知识库保留在同一主列表。条目采用现有列表密度,不增加卡片墙。
|
||||
|
||||
```text
|
||||
研发规范
|
||||
RAGFlow · 研发知识平台 · 远端可检索
|
||||
|
||||
产品资料
|
||||
本地 · 12 个来源 · 全文与向量可用
|
||||
```
|
||||
|
||||
### 3.1 外部条目字段
|
||||
|
||||
- GoodBuddy 显示名称。
|
||||
- Provider 图标或文字徽标。
|
||||
- 外部实例名称。
|
||||
- “远端可检索”、认证失效、目标不存在等准确状态。
|
||||
- 最近一次成功检索时间;从未检索时显示“尚未检索”。
|
||||
|
||||
外部条目不显示来源数量、同步进度、向量覆盖率、本地图谱或导入失败数。
|
||||
|
||||
### 3.2 列表筛选
|
||||
|
||||
现有搜索同时匹配 GoodBuddy 名称、远端名称、Provider 和实例名称。来源筛选使用菜单:
|
||||
|
||||
```text
|
||||
全部来源
|
||||
本地
|
||||
Dify
|
||||
FastGPT
|
||||
RAGFlow
|
||||
```
|
||||
|
||||
筛选不是新的页面页签。
|
||||
|
||||
## 4. 管理外部实例
|
||||
|
||||
点击“管理外部实例”打开大尺寸 Modal。Modal 使用标题、可滚动内容和固定操作区,遵循
|
||||
全局焦点陷阱、Escape、背景隔离和焦点恢复规则。
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 外部实例 [添加实例] [×] │
|
||||
│ 管理 Dify、FastGPT 和 RAGFlow 的连接 │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ 名称 Provider 服务地址 状态 操作 │
|
||||
│ 研发知识平台 RAGFlow ragflow.example 正常 ⋯ │
|
||||
│ 客服知识平台 FastGPT fastgpt.example 认证失效 ⋯ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.1 实例行
|
||||
|
||||
显示:
|
||||
|
||||
- 名称、Provider、脱敏后的主机名、状态。
|
||||
- 已绑定知识库数量。
|
||||
- 最近测试时间。
|
||||
- 行尾菜单:测试连接、编辑、停用或启用、删除。
|
||||
|
||||
服务地址可以显示 scheme、host 和非默认端口,不显示路径中的敏感信息、查询或凭据。
|
||||
|
||||
### 4.2 空状态
|
||||
|
||||
```text
|
||||
尚未配置外部实例
|
||||
添加企业现有的 Dify、FastGPT 或 RAGFlow 服务。
|
||||
[添加实例]
|
||||
```
|
||||
|
||||
筛选无结果和加载失败使用不同 `EmptyState` 变体。
|
||||
|
||||
## 5. 添加和编辑实例
|
||||
|
||||
实例表单在管理 Modal 内切换为编辑面板,不叠加第二层 Modal。
|
||||
|
||||
```text
|
||||
添加外部实例
|
||||
|
||||
实例名称 [研发知识平台 ]
|
||||
Provider [RAGFlow ▾]
|
||||
服务地址 [https://ragflow.example ]
|
||||
认证方式 API Key
|
||||
API Key [•••••••••••••••• ]
|
||||
凭据状态 尚未保存
|
||||
|
||||
连接测试
|
||||
服务连接 未测试
|
||||
身份认证 未测试
|
||||
知识库列表 未测试
|
||||
检索接口 绑定知识库时验证
|
||||
|
||||
[取消] [测试连接] [保存实例]
|
||||
```
|
||||
|
||||
### 5.1 字段行为
|
||||
|
||||
- Provider 使用原生 Select;编辑已被绑定的实例时不可切换 Provider。
|
||||
- 服务地址只接受完整 HTTP(S) 地址。字段下方说明其受全局内网兼容模式影响。
|
||||
- API Key 使用密码输入和显示/隐藏图标。明文会在当前表单和保存 IPC 中短暂存在;编辑时
|
||||
留空表示保留,不使用占位假密钥。
|
||||
- 已保存凭据旁提供“替换凭据”和“清除凭据”,清除属于需要确认的操作。
|
||||
- 地址使用 HTTP,或连接测试依赖放宽证书校验时,在测试和保存操作旁持续显示传输风险。
|
||||
用户必须针对当前地址确认后才能保存;地址变化会清除该确认。
|
||||
- “测试连接”不保存草稿;测试期间按钮显示进度并提供取消。
|
||||
- 保存成功进入应用通知,Modal 返回实例列表并聚焦对应行。
|
||||
|
||||
### 5.2 测试结果
|
||||
|
||||
分项状态使用图标、文字和说明,不只依靠颜色:
|
||||
|
||||
```text
|
||||
服务连接 通过 183 ms
|
||||
身份认证 通过
|
||||
知识库列表 受限 当前凭据不能读取列表,可手工填写知识库 ID
|
||||
检索接口 待验证 添加知识库时执行
|
||||
```
|
||||
|
||||
长错误折叠显示,正文经过脱敏。失败后保留全部草稿和 API Key 输入。
|
||||
|
||||
FastGPT 实例额外显示:`检索会计入该实例的 API 使用记录,并可能产生 Embedding 或
|
||||
Rerank 用量。`
|
||||
|
||||
## 6. 新建知识库向导
|
||||
|
||||
扩展现有页面内 `CreateLibraryWizard`,不改为新的全屏页面或 Modal。
|
||||
|
||||
### 6.1 类型选择
|
||||
|
||||
表单第一项使用单选卡片或共享 SegmentedControl:
|
||||
|
||||
```text
|
||||
知识库类型
|
||||
[本地] [Dify] [FastGPT] [RAGFlow]
|
||||
```
|
||||
|
||||
只有选择本地时显示现有存储模式、图谱和本地说明。外部类型选中后显示两栏配置。
|
||||
|
||||
### 6.2 外部类型两栏布局
|
||||
|
||||
```text
|
||||
┌──────────────────────────────┬──────────────────────────────┐
|
||||
│ 知识库来源 │ 检索配置 │
|
||||
│ │ │
|
||||
│ Provider RAGFlow │ 通用配置 │
|
||||
│ 外部实例 [研发平台 ▾] │ 返回片段数 [ 6 ] │
|
||||
│ 远端知识库 [研发规范 ▾] │ 请求超时 [15 秒] │
|
||||
│ [↻] │ 单片段上限 [4000] │
|
||||
│ [手工填写知识库 ID] │ │
|
||||
│ │ RAGFlow 配置 │
|
||||
│ 显示名称 [研发规范 ]│ 相似度阈值 [0.20] │
|
||||
│ 远端名称 研发规范 │ 向量相似度权重 [0.30] │
|
||||
│ 远端 ID 7f31…c902 │ KNN Top K [1024] │
|
||||
│ 测试查询 [部署网络要求 ]│ 图谱检索 [开关] │
|
||||
│ │ Knowledge Compilation [开关]│
|
||||
├──────────────────────────────┴──────────────────────────────┤
|
||||
│ 将向研发知识平台发送测试查询。不会上传文件或完整会话。 │
|
||||
│ [取消] [测试检索] [添加知识库] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
测试查询由用户填写,不自动生成,也不取用当前会话内容。“添加知识库”在真实检索请求
|
||||
成功前禁用;成功返回零条结果仍可添加,但界面显示“检索成功,未找到匹配片段”。验证结果
|
||||
在底部说明区展示,不用成功 Alert 撑高表单。
|
||||
添加成功后沿用本地知识库行为,把新绑定加入当前聊天选择,不重新启用其他已取消知识库。
|
||||
|
||||
### 6.3 目录下拉框
|
||||
|
||||
- 选择实例后自动打开加载状态,不要求再点击“读取”。
|
||||
- 使用可搜索 Select,选项显示名称、远端短 ID 和可用状态。
|
||||
- 右侧刷新使用刷新图标按钮,Tooltip 为“刷新知识库列表”。
|
||||
- 加载时保持控件稳定尺寸,显示“正在读取知识库…”。
|
||||
- 目录为空显示“当前实例没有可访问的知识库”。
|
||||
- 403 或不支持列表时显示原因和“手工填写知识库 ID”。
|
||||
- 列表失败后显示“重试”,不自动切换到手工模式。
|
||||
|
||||
### 6.4 手工模式
|
||||
|
||||
```text
|
||||
远端知识库 ID [ ]
|
||||
显示名称 [ ]
|
||||
[验证知识库]
|
||||
```
|
||||
|
||||
验证成功后显示远端名称和 Provider 返回的摘要。返回自动列表不会丢弃已输入 ID,切换前
|
||||
如有未保存输入须确认。
|
||||
|
||||
## 7. 配置面板
|
||||
|
||||
右侧配置固定分为“通用配置”和“{Provider} 配置”,不创建嵌套卡片。高级字段可以使用
|
||||
一个折叠区,但核心检索方式、阈值和 RAGFlow 图谱检索保持直接可见。
|
||||
|
||||
控件选择:
|
||||
|
||||
- 开关:图谱检索、Score 阈值启用、Rerank、使用 Dify 知识库默认配置。
|
||||
- Select 或 SegmentedControl:少量固定检索方式。
|
||||
- 数字输入或 Stepper:结果数、Token、Top K、超时。
|
||||
- Slider 加数字输入:0 至 1 的阈值和权重,二者保持同步。
|
||||
- Metadata 条件:初始版本不提供自由 JSON 编辑器。
|
||||
|
||||
字段显隐由 GoodBuddy 内置 Provider 描述和 Main 返回的能力状态共同决定。服务端返回的
|
||||
任意字符串不得变成 HTML、控件标签或帮助链接。
|
||||
|
||||
### 7.1 RAGFlow 图谱文案
|
||||
|
||||
开关名称:`图谱检索`
|
||||
|
||||
说明:`检索远端知识库已经生成的图谱内容,不会创建或更新图谱。`
|
||||
|
||||
只有远端详情或受控探测表明图谱已经构建时,该开关才可用。证据缺失时显示“未确认远端
|
||||
图谱状态”并禁用。Knowledge Compilation 单独显示为 `包含 Knowledge Compilation`,默认
|
||||
关闭,不把它和旧 GraphRAG 合成同一开关。
|
||||
|
||||
### 7.2 实测能力驱动的界面适配
|
||||
|
||||
界面不能只按 Provider 名称展示全部开关,必须按实例和当前远端知识库的能力证据处理:
|
||||
|
||||
| Provider | 已实测可启用 | 保持隐藏或禁用 | 界面依据 |
|
||||
| --- | --- | --- | --- |
|
||||
| Dify `0.15.8` | 使用知识库默认配置;展示 `retrieval_model_dict` 中已出现的检索方式、Top K、阈值和 Rerank 配置 | Metadata、多模态、摘要和 Pipeline 状态 | 目录项能力;该版本详情 GET 返回 405,不能把详情失败显示成实例不可用 |
|
||||
| Dify `1.17.0` | 默认配置、当前完整配置;详情中的文档统计、摘要索引、多模态、Metadata 和 Pipeline 只读状态 | 当前库没有字段定义时禁用 Metadata 条件;远端 Pipeline 和摘要配置不可编辑 | 详情 GET 和 3 条非空检索结果已实测;`child_chunks/files/summary` 仅按结果实际存在显示 |
|
||||
| FastGPT 当前实例 | embedding、全文召回、混合召回;Rerank | 查询扩展、扩展模型和背景文本 | 三种模式和 Rerank 均有 200 非空实测;查询扩展会调用模型,初始版本不启用 |
|
||||
| RAGFlow 当前实例 | 相似度、向量权重、KNN Top K;具备完成证据时启用图谱检索 | 没有独立配置证据时禁用 Knowledge Compilation;Metadata 条件仍隐藏 | `graphrag_task_finish_at` 有效才启用图谱;`compilation_template_group_id` 有效才启用 Knowledge Compilation |
|
||||
|
||||
RAGFlow 列表项应区分三个图谱状态:“未配置”“构建中或未完成”“可用于图谱检索”。不能
|
||||
仅因 `parser_config.graphrag.use_graphrag = true` 就显示为可用。当前实测中 17 个库配置了
|
||||
GraphRAG,只有 15 个存在完成证据。
|
||||
|
||||
Knowledge Compilation 的请求被服务器接受但当前没有库提供配置证据,因此开关显示为禁用,
|
||||
说明为“当前知识库未配置 Knowledge Compilation”。返回 200 或零结果都不能改变此状态。
|
||||
|
||||
Dify 实例行持续显示探测版本。`0.15.8` 的详情请求返回 405 时显示“此版本从目录读取配置”,
|
||||
不显示错误状态;`1.17.0` 详情页增加只读能力行:文档可用数、Metadata 字段数、多模态、
|
||||
摘要索引和 Knowledge Pipeline。没有配置的能力显示“未配置”,不隐藏成未知,也不提供会
|
||||
写回 Dify 的编辑操作。
|
||||
|
||||
Dify 1.x 的 Metadata 条件使用受控字段编辑器:先从 `doc_metadata` 选择字段,再按字段类型
|
||||
提供比较操作和值输入。当前库字段数为 0 时控件禁用并显示“当前知识库没有 Metadata 字段”。
|
||||
不得允许自由 JSON,也不得把 `attachment_ids`、Pipeline 或摘要 Prompt 暴露为检索输入。
|
||||
|
||||
## 8. 外部知识库详情
|
||||
|
||||
选中外部绑定后,现有详情区按来源类型裁剪:
|
||||
|
||||
```text
|
||||
研发规范 RAGFlow · 远端检索
|
||||
研发知识平台 / 远端知识库 7f31…c902
|
||||
|
||||
[概览] [检索配置] [检索测试]
|
||||
```
|
||||
|
||||
### 8.1 概览
|
||||
|
||||
显示 Provider、实例、远端名称、远端 ID、连接状态、最近成功检索和数据位置。操作包括:
|
||||
|
||||
- 刷新远端信息。
|
||||
- 更换远端知识库。
|
||||
- 编辑 GoodBuddy 显示名称。
|
||||
- 从 GoodBuddy 移除。
|
||||
|
||||
不显示导入文件、添加目录、添加网页、同步、分块、重建索引和本地图谱入口。
|
||||
|
||||
### 8.2 检索配置
|
||||
|
||||
复用创建向导右侧配置面板。存在失效字段时,页头显示“配置需要确认”,字段附近说明
|
||||
Provider 版本变化,保存按钮保持禁用直到用户处理。
|
||||
|
||||
### 8.3 检索测试
|
||||
|
||||
复用现有检索测试工作台的查询、结果和焦点行为,但外部结果采用 Provider 诊断:
|
||||
|
||||
```text
|
||||
RAGFlow · 研发知识平台
|
||||
总耗时 642 ms · 返回 6 条 · 保留 6 条
|
||||
|
||||
1 部署规范.pdf
|
||||
第四章 网络要求…
|
||||
Provider 分数 0.82 · 文档 ID 98ab…
|
||||
```
|
||||
|
||||
外部测试不显示 FTS/CJK/本地向量扫描等本地通道,也不提供依赖本地分块的“查看上下文”。
|
||||
|
||||
## 9. 聊天知识范围
|
||||
|
||||
现有多选列表保留一个选择面,只在每行增加来源:
|
||||
|
||||
```text
|
||||
[✓] 产品资料 本地
|
||||
[✓] 研发规范 RAGFlow · 研发知识平台
|
||||
[ ] 客服知识 FastGPT · 客服平台
|
||||
```
|
||||
|
||||
- 不按 Provider 创建多套选择器。
|
||||
- 不可用绑定保留显示但禁用勾选,并给出认证失效或目标不存在原因。
|
||||
- 当前会话已选绑定变为不可用时,不静默移除 ID;发送前提示该知识库本次无法检索。
|
||||
|
||||
## 10. 执行状态和引用
|
||||
|
||||
活动状态:
|
||||
|
||||
```text
|
||||
正在检索“研发规范”
|
||||
RAGFlow · 研发知识平台 · 已发送当前检索词
|
||||
```
|
||||
|
||||
部分失败:
|
||||
|
||||
```text
|
||||
已检索 3 个知识库,2 个成功,1 个超时
|
||||
[查看详情]
|
||||
```
|
||||
|
||||
引用:
|
||||
|
||||
```text
|
||||
部署规范.pdf / 网络要求
|
||||
研发规范 · RAGFlow · 研发知识平台
|
||||
```
|
||||
|
||||
引用详情显示远端文档名、ID、片段、Provider 分数和实际返回的定位。未完成 URL 安全设计前,
|
||||
不显示“打开远端来源”。
|
||||
|
||||
## 11. 删除确认
|
||||
|
||||
移除绑定:
|
||||
|
||||
```text
|
||||
从 GoodBuddy 移除“研发规范”?
|
||||
这会删除本机绑定和检索配置,不会删除 RAGFlow 中的知识库或文档。
|
||||
|
||||
[取消] [移除]
|
||||
```
|
||||
|
||||
删除有绑定的实例:
|
||||
|
||||
```text
|
||||
“研发知识平台”仍被 3 个知识库使用
|
||||
删除实例会同时从 GoodBuddy 移除这些绑定。远端数据不会改变。
|
||||
|
||||
[取消] [查看知识库] [删除实例和本地绑定]
|
||||
```
|
||||
|
||||
破坏性主操作使用 `danger-solid`,默认焦点放在“取消”。
|
||||
|
||||
## 12. 可访问性与响应式
|
||||
|
||||
- 常规宽度两栏使用稳定 Grid;小于 `960px` 时改为单栏,顺序为来源、通用配置、Provider 配置。
|
||||
- 小于 `720px` 时外部实例表格转为单列列表,行操作保留菜单按钮和完整可访问名称。
|
||||
- Modal、创建向导、下拉框和折叠区支持完整键盘操作。
|
||||
- 异步列表和测试状态使用 `aria-live="polite"`;错误使用与字段关联的 `role="alert"`。
|
||||
- 图标按钮必须提供 Tooltip 和 `aria-label`。
|
||||
- 状态不只靠颜色;加载、成功、受限和失败均包含文字。
|
||||
- 长实例名、地址、远端名称和 ID 使用单行省略,完整值可通过 Tooltip 或详情查看。
|
||||
- 切换类型、实例或退出时存在有效草稿,应按全局离开保护规则确认。
|
||||
|
||||
## 13. 文案表
|
||||
|
||||
| 场景 | 文案 |
|
||||
| --- | --- |
|
||||
| 页面操作 | 管理外部实例 |
|
||||
| 创建类型 | 本地 / Dify / FastGPT / RAGFlow |
|
||||
| 创建主操作 | 添加知识库 |
|
||||
| 列表加载 | 正在读取知识库… |
|
||||
| 列表受限 | 当前凭据不能读取知识库列表,可手工填写知识库 ID。 |
|
||||
| 远端状态 | 远端可检索 |
|
||||
| 移除操作 | 从 GoodBuddy 移除 |
|
||||
| 图谱开关 | 图谱检索 |
|
||||
| 测试说明 | 测试只发送当前查询,不会调用 GoodBuddy 模型或修改远端内容。 |
|
||||
|
||||
产品文案不使用“同步外部知识库”“导入 Dify 知识库”或“创建远端知识库”,因为这些动作
|
||||
都不是本功能的实际行为。
|
||||
|
||||
## 14. 需求追踪
|
||||
|
||||
| UI 区域 | 需求 | 场景 |
|
||||
| --- | --- | --- |
|
||||
| 页面标题与统一列表 | `EK-FR-01`、`EK-FR-10` | `EK-US-C1` |
|
||||
| 外部实例管理与表单 | `EK-FR-02`、`EK-FR-03`、`EK-FR-15`、`EK-FR-16` | `EK-US-A1` 至 `EK-US-A3` |
|
||||
| 新建知识库向导 | `EK-FR-04` 至 `EK-FR-09` | `EK-US-B1` 至 `EK-US-B4` |
|
||||
| 外部详情与检索测试 | `EK-FR-12`、`EK-FR-13` | `EK-US-C3`、`EK-US-D1` 至 `EK-US-D4` |
|
||||
| 聊天范围、状态和引用 | `EK-FR-11`、`EK-FR-12`、`EK-FR-14` | `EK-US-C1` 至 `EK-US-C4` |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user