Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ae9e0324 | ||
|
|
f49d6678a3 | ||
|
|
0d7ba55a50 | ||
|
|
c71da4b6ae | ||
|
|
ba6f6b8f22 | ||
|
|
49377206cc | ||
|
|
1c22e1a06d | ||
|
|
a0456f687a | ||
|
|
59a1624396 | ||
|
|
a44de1441a | ||
|
|
f7f0332143 | ||
|
|
4222351f3c | ||
|
|
8ad26d43bb | ||
|
|
74a2f187ba | ||
|
|
ca4a58c762 | ||
|
|
ea45533137 | ||
|
|
e9679fecf1 | ||
|
|
3b560c9a5e | ||
|
|
b84250f62a | ||
|
|
032ebc313f | ||
|
|
719c9fd6cd | ||
|
|
d36a05f7ad | ||
|
|
fc36433d76 | ||
|
|
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 |
@@ -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"
|
||||
|
||||
@@ -248,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
|
||||
@@ -261,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.
|
||||
@@ -112,6 +120,11 @@ additional hardening beyond the actual internal-network threat model:
|
||||
|
||||
## Data and Compatibility
|
||||
|
||||
- Before changing released database schemas or large-data migrations, read
|
||||
[`docs/development/database-migrations.md`](./docs/development/database-migrations.md).
|
||||
Keep feature-specific storage rules and validation evidence in the owning
|
||||
feature documents.
|
||||
|
||||
- Preserve released user-owned data, migrations required by released data, and
|
||||
supported external contracts.
|
||||
- Compatibility does not apply to unshipped, branch-local, or safely disposable
|
||||
@@ -140,6 +153,13 @@ additional hardening beyond the actual internal-network threat model:
|
||||
repository mechanisms instead of inventing extra gates, ceremonies, or
|
||||
disabled states.
|
||||
- Add or update focused tests for behavioral changes and regressions.
|
||||
- Read [`docs/quality/recurring-defects.md`](./docs/quality/recurring-defects.md)
|
||||
before changing overlays, store lookups, retrieval entry points, cancellation
|
||||
behavior, or render-time derived values. It records defect types that have
|
||||
already recurred in this repository and the shared mechanism that covers each
|
||||
one. When fixing an instance of a listed type, extend that shared mechanism
|
||||
instead of patching the one reported case, and add an entry when a new defect
|
||||
type recurs.
|
||||
- After completing any functional change, inspect the affected product,
|
||||
architecture, design, feature, setup, and operational documentation and
|
||||
update every relevant document to match the implemented behavior. Treat the
|
||||
|
||||
@@ -31,6 +31,10 @@ npm start
|
||||
|
||||
## 质量验证
|
||||
|
||||
跨功能测试方法见[质量验证索引](./docs/quality/README.md)。桌面界面的测试分层、
|
||||
原生键盘、截图与计算样式取证、隔离启动和脱敏报告遵循
|
||||
[Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md)。
|
||||
|
||||
提交或打包前运行:
|
||||
|
||||
```bash
|
||||
@@ -56,6 +60,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 生产文件:
|
||||
@@ -145,7 +157,7 @@ npm run dist:linux:arm64
|
||||
组件,缺少远端包也不能阻塞普通桌面打包或发布。
|
||||
|
||||
“设置 > 平台功能 > 远程项目(技术预览)”列出用户数据目录中已经下载或导入的
|
||||
Linux x64/arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
|
||||
Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
|
||||
小型签名目录,显示本地版本、在线最新兼容版本以及 `有更新 / 已是最新 / 未下载 /
|
||||
校验失败`;此检查不会下载 `.gbagent`。只有用户点击“下载”或“更新到”后才传输包,
|
||||
用户也可导入或导出 `.gbagent` 离线包。
|
||||
@@ -211,6 +223,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 许可证。
|
||||
@@ -254,16 +269,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` /
|
||||
@@ -284,7 +315,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,并只携带目标
|
||||
@@ -371,8 +402,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 的
|
||||
@@ -408,8 +439,8 @@ Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段
|
||||
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
|
||||
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` 重新生成并
|
||||
@@ -456,7 +487,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 编码后的内容。
|
||||
|
||||
+240
-43
@@ -17,6 +17,12 @@ otherwise.
|
||||
|
||||
- [x] **Cross-platform desktop application**: Supports Windows, macOS, and
|
||||
Linux release targets on `x64` and `arm64`.
|
||||
- [x] **Chinese hardware and operating-system coverage**: Standard Linux
|
||||
packages target compatible x64 and arm64 environments, including UOS, Kylin,
|
||||
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
|
||||
LoongArch has a separate experimental loong64 preview outside standard
|
||||
releases and automatic updates; no preview is planned for 0.13.2. 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,
|
||||
@@ -30,8 +36,34 @@ otherwise.
|
||||
local, managed SSH, and remote messaging-channel projects. Managed SSH
|
||||
projects are grouped by Host, with the real Agent connection state on the
|
||||
Host heading and only the remote path on each project row.
|
||||
- [x] **Cross-project conversation activity**: A shared activity entry lists
|
||||
running conversations and conversations needing attention across projects,
|
||||
including older conversations with live work. Anchored project-to-conversation
|
||||
menus provide keyboard navigation and exact conversation opening without a
|
||||
full activity dialog. The summary remains visible when idle, and project
|
||||
selection continues to show activity counts.
|
||||
- [x] **Compact conversation controls**: Composer options share a compact
|
||||
settings panel while Runtime, mode, sending, and queue controls remain
|
||||
available. Conversation search has an inline clear action that restores the
|
||||
scoped list and returns focus to the input.
|
||||
- [x] **File, screenshot, window, and clipboard context**: Added to model
|
||||
context only after explicit user selection.
|
||||
- [x] **Per-file workspace diffs**: Changed files expose separate staged and
|
||||
unstaged diffs, including deleted, renamed, and untracked files. Refresh
|
||||
reloads the selected diff, browsed directory, and expanded directories; more than 50 changes
|
||||
remain accessible through incremental loading.
|
||||
- [x] **Workspace file and Git management**: Create, rename, move, and delete
|
||||
files or directories, inspect properties, search and switch local or remote
|
||||
branches, create branches, explicitly Fetch, and browse commit history and
|
||||
per-file commit diffs. File and Git controls are grouped by view, branch
|
||||
search uses the shared compact form style, and subdirectories retain text
|
||||
breadcrumbs without an isolated root icon. Commit history stays docked at
|
||||
the bottom, initially expands to half the Git content area, and supports
|
||||
resizing with independently scrolling panes and one shared refresh action.
|
||||
Project switches automatically detect Git without reusing another project's
|
||||
status. Moves stay inside the workspace,
|
||||
deletion requires confirmation, and branch conflicts never trigger automatic
|
||||
stash or discard. Managed SSH operations require Agent `0.11.23`.
|
||||
- [x] **Rich responses**: Supports GitHub Flavored Markdown, LaTeX math,
|
||||
constrained Mermaid diagrams, and static in-conversation HTML previews.
|
||||
Complete HTML and HTML code blocks can be previewed in place after an Agent
|
||||
@@ -45,6 +77,9 @@ otherwise.
|
||||
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
|
||||
@@ -55,29 +90,62 @@ otherwise.
|
||||
- [ ] **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 future
|
||||
memory and historical execution context belong to the associated Task. See
|
||||
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).
|
||||
|
||||
### Agent Runtimes and model connections
|
||||
|
||||
- [x] **Compact subagent history without lost details**: Local and managed SSH
|
||||
subagent events store changes instead of repeated full progress snapshots.
|
||||
The first upgrade automatically converts existing history and reclaims disk
|
||||
space, with visible progress and quit/retry support. Chats, execution details,
|
||||
results, and remote event deduplication are preserved; older clients cannot
|
||||
reopen the upgraded database. The 0.13.2 candidate's schema-35 repair also handles
|
||||
repeated tool blocks written after upgrading to 0.13.1, without deleting events.
|
||||
Terminal tasks release their write caches; duplicate remote replay does not
|
||||
rebuild a released cache. See
|
||||
[storage and upgrade behavior](./docs/features/assistant-workbar/execution-history-storage.md).
|
||||
|
||||
- [x] **Direct model Runtime**: Supports question answering, knowledge
|
||||
synthesis, controlled tool execution, and image generation.
|
||||
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.
|
||||
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, total execution
|
||||
limits, bounded streaming output, and activity records. Shared process
|
||||
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
|
||||
@@ -86,11 +154,19 @@ otherwise.
|
||||
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. GoodBuddy-managed local OpenCode keeps
|
||||
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. OpenCode and Continue runs do not stop
|
||||
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.
|
||||
@@ -110,8 +186,8 @@ otherwise.
|
||||
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 receives no such override and
|
||||
retains all permissions of the selected SSH account. The Agent owns accepted Prompts,
|
||||
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.
|
||||
@@ -123,12 +199,16 @@ otherwise.
|
||||
in the desktop package; switching projects updates only local selection and
|
||||
does not connect or download. Settings reads a small signed catalog and
|
||||
reports the local version, latest compatible online version, and update
|
||||
state for Linux x64/arm64. Downloads occur only after a user action, and
|
||||
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, architecture, size, SHA-256, and fixed URL.
|
||||
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
|
||||
@@ -148,9 +228,17 @@ otherwise.
|
||||
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.18`, while the current Desktop release candidate is `0.12.4`; formal
|
||||
`0.11.25`, while the current Desktop release candidate is `0.13.2`; formal
|
||||
publication status follows the separate Agent and Desktop
|
||||
release channels.
|
||||
release channels. Previous macOS validation covered native package installation,
|
||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||
separate process groups on a real Host; this does not imply publication.
|
||||
Agent `0.11.23` adds remote workspace management and optional model limits;
|
||||
its bundled Runtime no longer imposes a fixed ten-minute Prompt deadline.
|
||||
Unlimited request duration retains a separate connection timeout. Agent
|
||||
`0.11.23` requires Desktop `0.12.11` or later.
|
||||
Agent `0.11.24` requires Desktop `0.13.0` and fixes native question routing
|
||||
and sending another message after cancelling a pending question.
|
||||
- [x] **Manual SSH Host environment provisioning source path**: After Host Key,
|
||||
authentication, and system probes succeed, GoodBuddy saves the Host and
|
||||
read-only probes the shared Agent/Runtime. Saving a Host or opening a project
|
||||
@@ -177,21 +265,26 @@ otherwise.
|
||||
capabilities, and prompt startup do not scan the full payload. See the
|
||||
[design](./docs/features/remote-host/environment-provisioning-technical-design.md).
|
||||
- [ ] **Real-Host acceptance for SSH Host environment provisioning**: The
|
||||
source can use the current format-v1 package. GitHub, Beijing mirror, Linux
|
||||
x64/arm64, cancellation, and offline GoodBuddy transfer validation remains.
|
||||
This status does not mean the path is published or has completed real-Host
|
||||
testing.
|
||||
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. Ask permits
|
||||
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.
|
||||
temporary Attachment Store. On Windows, Host startup and ACP sessions share
|
||||
one canonical workspace path, so equivalent path spellings reuse the same
|
||||
working Runtime instead of failing session creation.
|
||||
- [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
|
||||
@@ -202,11 +295,57 @@ otherwise.
|
||||
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.
|
||||
paths available to the current local or SSH account, including paths outside
|
||||
the workspace and native subagent work.
|
||||
- [x] **Efficient direct-model workspace tools**: Direct models use bundled
|
||||
ripgrep for compact file discovery and content search, read large UTF-8 files
|
||||
by line, and apply multi-file patches in Execute. Ask can search and read but
|
||||
cannot patch or run commands; packaged ripgrep does not require a system install.
|
||||
Actionable read/search errors let the model correct its arguments and
|
||||
continue. Partial search results remain available with an explicit
|
||||
incomplete-coverage warning.
|
||||
- [x] **Long context and paged tool output**: Message history, long replies,
|
||||
and parsed document text no longer use the previous fixed truncation limits.
|
||||
Direct models can continue reading stored command and Subagent output in
|
||||
pages; the selected model's context window and bounded transports still apply.
|
||||
- [x] **Native Runtime interaction routing**: OpenCode and Continue questions
|
||||
support choices, yes/no, free-text answers, and skipping through the existing
|
||||
question card. Local and managed SSH OpenCode also route questions from
|
||||
owned child sessions. Managed SSH requires Desktop `0.13.0`, Agent `0.11.24`,
|
||||
and a newly started managed Runtime;
|
||||
this does not extend question support to arbitrary ACP services.
|
||||
Successful answers and skips retain the original questions and answers
|
||||
across rounds and local conversation reloads; answers discarded by older
|
||||
versions cannot be recovered.
|
||||
Concurrent questions wait in order, duplicate events preserve drafts, and
|
||||
failed submissions remain retryable. Cancelling a pending managed SSH
|
||||
question allows another message in the same conversation.
|
||||
Execute permission confirmations are handled automatically rather than
|
||||
waiting for another approval. See the [interaction boundaries](./docs/features/assistant-workbar/runtime-interactions.md).
|
||||
- [x] **Experts and Subagents**: Supports explicit experts, team analysis, and
|
||||
up to three read-only experts running in parallel. Chat shows each
|
||||
up to three experts running in parallel. Experts inherit the parent
|
||||
Ask/Execute mode and can use enabled local direct-model tools; Ask remains
|
||||
read-only. They are not remote OpenCode child sessions. Chat shows each
|
||||
expandable full expert response first and the main Agent's synthesis below,
|
||||
and persists both with the conversation.
|
||||
- [x] **OpenCode child progress and final results**: Child cards show ordered
|
||||
text, reasoning, and tool progress separately from the final result. Remote
|
||||
live progress requires Agent `0.11.20`; older packages still expose final
|
||||
results without reconstructing missing progress.
|
||||
- [x] **OpenCode event-stream cleanup**: Chat requests and native context
|
||||
compaction close their own subscriptions before ending event iteration,
|
||||
including completion, failure, cancellation, and early consumer exit,
|
||||
without cancelling parallel conversations.
|
||||
- [x] **Accurate message-footer status**: Distinguishes request preparation,
|
||||
retry waiting, retry dispatch, tool activity, pending answers, and terminal
|
||||
states. Local OpenCode reports native retry attempts and scheduled times;
|
||||
direct models report their own backoff phase. Footer dots are static, and
|
||||
unsupported Runtime retry details are not invented. Conversation-list
|
||||
indicators and send/stop behavior remain unchanged.
|
||||
- [x] **Simplified Runtime selection**: Conversation and project menus no
|
||||
longer enumerate every Runtime/model combination. Configure Runtime models
|
||||
in system settings; saved fixed project selections remain usable and
|
||||
explicitly display their fixed model.
|
||||
- [x] **Role-bound model connections**: Each role can inherit the default model
|
||||
or select an independent text-model connection. Invalid connections safely
|
||||
fall back to the default; the synthesis role always inherits the default.
|
||||
@@ -300,10 +439,10 @@ otherwise.
|
||||
supervision across OpenCode, Continue, and DeepSeek Harness. Users select
|
||||
only Conversation or Task; Job/Run remains internal rather than becoming a
|
||||
tree or independent object.
|
||||
- [ ] **Executable Subagents** (planned): Explicit Execute delegation with
|
||||
bounded nesting, parallelism, tokens, time, and tool permissions, aggregated
|
||||
by Task in the workbar's fixed Runtime section with cancellation and audit
|
||||
ownership.
|
||||
- [ ] **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.
|
||||
|
||||
### Skills, MCP, and knowledge
|
||||
|
||||
@@ -344,8 +483,10 @@ otherwise.
|
||||
models, GoodBuddy-managed OpenCode, Continue Agent Execute, and DeepSeek
|
||||
Harness, and loads only in Execute. Agent child processes receive only
|
||||
per-request local-loopback authority; MCP addresses, commands, and
|
||||
credentials remain in Main. Dynamic tools are still rediscovered and pass
|
||||
through existing activity and permission boundaries.
|
||||
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
|
||||
@@ -353,15 +494,27 @@ otherwise.
|
||||
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.
|
||||
- [ ] **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
|
||||
SQLite FTS5 retrieval, and source tracing. Knowledge selection persists per
|
||||
conversation and starts empty for new conversations. Advanced creation and
|
||||
retrieval parameters are collapsed by default. Documents distinguish ready,
|
||||
processing, and failed states, with source opening, retry, and confirmation
|
||||
before source removal.
|
||||
- [x] **External knowledge-base connections**: Manage Dify, FastGPT,
|
||||
and RAGFlow instances from the Knowledge page, then discover remote
|
||||
knowledge bases or enter their IDs and verify bindings with provider-specific
|
||||
retrieval settings and citations. Conversations support on-demand and
|
||||
required pre-answer retrieval. External systems provide retrieval only:
|
||||
GoodBuddy does not use
|
||||
their App, Chat, Workflow, or Agent APIs, and does not bulk-sync, locally
|
||||
index, or modify remote content. Bounded cited snippets are retained locally
|
||||
with their conversations. See the
|
||||
[external knowledge-base PRD](./docs/features/knowledge-base/external-knowledge-prd.md).
|
||||
- [ ] **External knowledge-base acceptance**: All three providers have passed
|
||||
real service, local HTTP MCP, and production-IPC short-answer checks; Dify
|
||||
additionally passed the complete App composer-to-answer-and-citation path.
|
||||
FastGPT/RAGFlow desktop-specific parameters, complex multi-library answers,
|
||||
and remote pre-answer retrieval remain unverified. See the
|
||||
[validation evidence and remaining work](./docs/features/knowledge-base/progress.md).
|
||||
- [x] **Knowledge graph**: Supports rule-based, model-based, and hybrid
|
||||
extraction plus entity, relationship, alias, and evidence maintenance.
|
||||
- [x] **Embedding configuration and retrieval**: Configures compatible
|
||||
@@ -384,7 +537,10 @@ otherwise.
|
||||
demand or before every response, show zero-result, degraded, failed, and
|
||||
cancelled states, and expose cited context or safely open its source.
|
||||
- [x] **Magic Notes**: A local-first notes and todo workbench with scope
|
||||
management, editing, filtering, and controlled AI comments. The left
|
||||
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
|
||||
@@ -401,12 +557,18 @@ otherwise.
|
||||
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,
|
||||
and a persistent warning remains when a migrated legacy cache may already
|
||||
have lost older records or detail. Token usage is grouped by Runtime and
|
||||
without a persistent legacy-cache truncation warning; previously lost
|
||||
history is not restored. Token usage is grouped by Runtime and
|
||||
model and normalizes the different OpenAI-compatible and Anthropic Messages
|
||||
cache-reporting semantics when showing cache hit rate. Activity is grouped
|
||||
by conversation and collapsed by default so long histories do not fill the
|
||||
page.
|
||||
- [x] **Compact conversation tool records**: Expand individual tool records
|
||||
to read or copy results, errors, and input parameters. Conversation and
|
||||
child-task progress use concise rows without repeated Runtime summaries.
|
||||
OpenCode, Continue, DeepSeek Harness, and direct models show available file
|
||||
paths, commands, or search summaries and retain them through completion and
|
||||
conversation reloads.
|
||||
- [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
|
||||
@@ -422,7 +584,10 @@ otherwise.
|
||||
Center remains the complete index rather than creating a separate Automation
|
||||
Center. Current schedules support one-time, daily, and weekly triggers;
|
||||
advanced time zones, Cron, event triggers, and retry governance remain
|
||||
incremental PRD work. See the
|
||||
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
|
||||
@@ -432,8 +597,8 @@ otherwise.
|
||||
created and edited for Global or one or more selected Projects. Legacy
|
||||
single-project settings migrate without loss, and project-level memory and
|
||||
action output must explicitly target a Project in scope. Task Center and
|
||||
Settings no longer duplicate the form. Future partitioned memory remains an
|
||||
independently designed long-term direction. See the
|
||||
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
|
||||
@@ -465,6 +630,15 @@ otherwise.
|
||||
path, with Back, Refresh/Stop Loading, address entry, Go, Interaction, and
|
||||
Close actions. Stop Loading does not close the session, and user navigation
|
||||
changes the page the Agent sees next.
|
||||
Multiple tabs in one conversation share login state while keeping separate
|
||||
pages and navigation. Each model request retains its starting tab binding;
|
||||
unused request reservations do not create browser resources. Released tabs
|
||||
can be reopened, explicit screenshots remain available, and ordinary
|
||||
operations no longer trigger unused automatic screenshot capture.
|
||||
Tabs can be closed while an AI request uses them without cancelling the
|
||||
whole request. A later explicit navigation creates a separate replacement
|
||||
tab; blank pages cannot reload, and switching or closing panels clears stale
|
||||
action errors.
|
||||
- [x] **Client-computer control tools**: Managed separately from the built-in
|
||||
browser with scope, cancellation, timeout, output, and activity boundaries.
|
||||
- [x] **Remote messaging-channel projects**: WeChat ClawBot, WeCom, and
|
||||
@@ -505,12 +679,25 @@ otherwise.
|
||||
draft, diagnostic choice, and request ID. Conversations, Prompts,
|
||||
credentials, file contents, paths, raw provider responses, and remote Agent
|
||||
logs are never attached.
|
||||
- [x] **Private-network compatibility mode**: Enabled by default and permits
|
||||
in-app HTTP plus invalid, self-signed, or expired HTTPS certificates.
|
||||
Disabling it restores strict address and certificate validation.
|
||||
- [x] **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.
|
||||
|
||||
### Open source, builds, and releases
|
||||
|
||||
- Current source candidates are Desktop `0.13.2` and Agent `0.11.25`, with
|
||||
OpenCode pinned to `1.18.29`. Publication status follows the independent
|
||||
Desktop and Agent release channels.
|
||||
- Agent `0.11.25` is a maintenance release without standalone remote feature
|
||||
changes, and its packages require Desktop `0.13.2`. The desktop storage repair
|
||||
also works with Agent `0.11.24`; updating Agent is not a substitute for updating Desktop.
|
||||
- Candidate acceptance remains separate from version preparation. Current-source
|
||||
storage validation covers real local and Linux x64 Host tool workloads,
|
||||
concurrent projects/conversations, cancellation, and lossless database migration.
|
||||
Windows tool summaries and DeepSeek path handling have real full-App evidence;
|
||||
the release candidate still requires main CI and native release packaging. See the
|
||||
[validation evidence](./docs/features/assistant-workbar/progress.md).
|
||||
- [x] **0BSD open-source license**: Original code can be freely used, copied,
|
||||
modified, distributed, and commercialized. Third-party components and
|
||||
resources retain their own licenses.
|
||||
@@ -519,7 +706,17 @@ otherwise.
|
||||
build commands.
|
||||
- [x] **Six-target native release matrix**: Native runners build Windows,
|
||||
macOS, and Linux `x64` and `arm64` targets with release manifests and SHA-256
|
||||
hashes. Linux also produces AppImage, DEB, and RPM.
|
||||
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.
|
||||
|
||||
### Open interfaces, team collaboration, and remote execution
|
||||
|
||||
|
||||
+46
-18
@@ -14,28 +14,46 @@
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.13.2` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
|
||||
- [x] **跨项目会话活动**:统一入口汇总各项目正在运行和需要处理的对话,保留仍有活动的较早会话。锚定的“项目 → 会话”菜单支持键盘导航和精确打开对话,不再使用整块活动弹窗;空闲时摘要入口仍显示,项目选择器继续显示活动数量。
|
||||
- [x] **紧凑会话控件**:输入选项集中在紧凑设置面板,Runtime、工作模式、发送和队列操作保持可用;会话搜索提供内嵌清除按钮,清除后恢复当前范围列表并把焦点返回输入框。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异、当前浏览目录和已展开目录,超过 50 项变更可继续加载。
|
||||
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。提交历史贴底,首次展开占 Git 内容区一半,支持调整比例、独立滚动和统一刷新;切换项目自动检测 Git,不沿用其他项目状态。移动限于工作区内,删除须确认,分支冲突不自动 stash 或丢弃改动;托管 SSH 操作需要 Agent `0.11.23`。
|
||||
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
|
||||
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
|
||||
- [x] **助手工作栏、多终端与可调布局**:右侧工作栏使用持久“+”能力目录和应用 Tab;用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
|
||||
- [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**:支持问答、知识总结、受控工具执行和图像生成。
|
||||
- [x] **子任务历史去重与空间回收**:本机与托管 SSH 子任务事件只存变化,不再反复保存完整进度。首次升级自动转换旧记录并回收空间,显示进度且支持退出重试;保留聊天、执行详情、结果和远程事件去重。0.13.2 候选的 schema 35 补修还会整理升级到 0.13.1 后重复写入的工具块,不删除事件;终态及时释放写入缓存,远程重复重放不重建已释放的缓存。旧客户端不能打开升级后的数据库,具体规则见[执行记录存储与升级回收](./docs/features/assistant-workbar/execution-history-storage.md)。
|
||||
|
||||
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。连续请求自动复用最近一次成功生成的图片和文字历史,已保存会话重开后仍可继续修改,本轮显式附图优先;上游明确不支持编辑时继续按文字生成,仅在回复底部提示未使用参考图。编辑使用 multipart 上传图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。详见[图片生成与连续修改](./docs/features/image-generation/README.md)。
|
||||
- [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
|
||||
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、总执行时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
|
||||
- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响任何本地项目、普通桌面功能或桌面发布。启用后可管理固定 Host Key 的 SSH Host、浏览有界远端目录并创建 Ask/Execute 项目;两种模式都直接启动签名 Runtime,不依赖 bubblewrap。Ask 同时使用 OpenCode Ask 权限配置,并在 Agent 工具分发边界只允许原生读取审批;Execute 不注入该权限覆盖,使用所选 SSH 账号的完整权限。Agent 通过私有 Unix socket 和 ACP v5 持有 accepted Prompt、Provider/工具轮次、Runtime 进程、稳定模型 ledger 与有界语义 transcript;Desktop 退出、网络中断或本机进程结束后 Host 继续执行,重启后只 attach 原 controller/binding/operation 并把 provenance 原子归并回原对话,不重发 Prompt。模型 profile 与 API Key 只在当前 accepted operation 生命周期内进入 Agent 内存,不写入 Renderer、SSH 参数、远端环境或磁盘。远端组件不嵌入桌面包,项目切换只更新本地选择且不会连接或下载;设置页只读取小型签名目录并按 Linux x64/arm64 显示本地版本、在线最新兼容版本和“有更新/已是最新”,用户点击后才下载签名 `.gbagent`,也可导入/导出离线包。每个复合包包含 Agent、固定 Node 与由桌面源码统一维护适配的 OpenCode Runtime;在线来源跟随“关于与更新”的 GitHub/北京 OSS 选择,签名累计目录绑定最低桌面版本、Agent 协议、架构、大小、SHA-256 和固定 URL。公钥 registry 接受等价 JSON 空白和换行,但仍严格校验 schema、Ed25519 key、环境与撤销;目录、包、manifest、payload 签名和流式 SHA-256 不变。缺少某架构包只使该架构托管 SSH 不可用。项目只保存 Host、远端路径、Runtime 选择和模式;首次实际使用 Workspace/Runtime 时读取 Host current identity,同一进程内其他项目复用已确认 identity 和 Agent 连接。托管 SSH 会话只显示支持的 OpenCode 选择。同一 Host 可并行运行多个项目和对话;Agent-owned Prompt 不因固定模型调用次数或 Prompt 累计输出 Token 数停止,GoodBuddy 输出缓冲区达到容量时使用传输背压而不取消 Runtime;恢复进度分别显示网络、Agent、Runtime、已提交事件 cursor、完成或失败/重试,只阻塞受影响项目。Linux x64 真实 Host 已覆盖短/长 detach、本机 harness 强制结束、并发、SSH relay 丢失、取消、确定/不确定 Provider 失败、Agent `SIGKILL`/重启和 reopened Desktop SQLite 恢复;成功工具 START/END 各一次,未发现 Prompt、Provider 或工具重放。当前 Agent 源码 lock 为 `0.11.18`,Desktop 发布候选为 `0.12.4`;正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。
|
||||
- [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 验收**:源码已可使用当前 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。
|
||||
- [ ] **SSH Host 环境准备真实 Host 验收**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵仍待验收,候选 CI/原生打包与系统休眠唤醒也尚未验证;部分开发证据不代表已经发布。
|
||||
- [x] **远程工作区与长任务更新**:Agent `0.11.23` 新增远程文件/Git 管理和可选模型限额,随包 Runtime 不再施加固定十分钟 Prompt 时限;无限请求时长仍保留独立连接超时。请先升级 Desktop 至 `0.12.11`,再下载并更新 Host 上的 Agent。
|
||||
- [x] **远程原生问答与取消修复**:Agent `0.11.24` 修复远程原生问答转交和取消待答后同一会话续发,需要先升级 Desktop 至 `0.13.0`。
|
||||
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。Windows Host 启动和 ACP 会话共用规范工作区路径,同目录不同写法可复用正常 Runtime,不再因路径不一致而创建会话失败。
|
||||
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方代码不受 Ask 初始化隔离。
|
||||
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权。
|
||||
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析;聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
|
||||
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权,包括工作区外路径及原生子代理工作。
|
||||
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,本机与托管 SSH OpenCode 同时转交属于当前请求的子会话提问。托管 SSH 需要 Desktop `0.13.0`、Agent `0.11.24` 和新启动的托管 Runtime,不扩展到任意 ACP 服务。回答或跳过成功后保留原问题与答案,支持多轮记录和本地会话重载;旧版本已丢弃的答案不能恢复。并行问题按顺序等待回答,重复事件保留草稿,提交失败可重试;取消远程待答后可在同一会话继续发送。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
|
||||
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求 Ask/Execute 模式,可使用已启用的本机直连模型工具,Ask 仍只读;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
|
||||
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
|
||||
- [x] **OpenCode 事件连接回收**:聊天与原生上下文压缩在结束事件迭代前关闭各自订阅,覆盖完成、失败、取消和消费方提前结束,不取消其他并行会话。
|
||||
- [x] **准确的消息底部状态**:区分请求准备、等待重试、开始重试、工具活动、待回答和终态;本机 OpenCode 使用原生重试次数和计划时间,直连模型显示自身退避阶段。消息底部状态点保持静态,未接入的 Runtime 重试不编造提示,会话列表闪点及发送、停止行为不变。
|
||||
- [x] **直连模型命令工作目录**:Execute 命令可通过绝对路径、相对路径或符号链接使用工作区外目录;工作区仍为默认目录和相对路径基准,Ask 保持只读。
|
||||
- [x] **简化 Runtime 选择**:会话与项目菜单不再枚举每个 Runtime 与模型的组合,Runtime 模型在系统设置中配置;已保存的项目固定模型仍可使用,并明确显示其固定模型。
|
||||
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
|
||||
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
|
||||
- [x] **模型请求定制**:每个 LLM 连接可填写有界 JSON 自定义请求头和顶层请求体。直连与 Continue 支持两者;本地 OpenCode 和 DeepSeek Harness 只使用各自原生支持的请求头,受管 SSH OpenCode 模型网关支持两者。Runtime、协议和认证字段始终优先,这些普通连接设置不能用于保存 API Key 或其他密钥。详见[模型连接请求定制](./docs/features/model-connections/README.md)。
|
||||
@@ -46,7 +64,7 @@
|
||||
- [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 聚合状态、取消入口和审计归属。
|
||||
- [ ] **Subagent 高级监督**(规划中):在工作栏固定 Runtime 栏目按 Task 聚合,提供可配置的嵌套、并行、预算和生命周期控制。基础专家模式继承与单层直连模型编程委派已经可用。
|
||||
|
||||
### Skills、MCP 与知识库
|
||||
|
||||
@@ -54,10 +72,11 @@
|
||||
- [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,动态工具仍会重新发现并经过现有执行记录与权限边界。
|
||||
- [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] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
|
||||
- [x] **外部知识库接入**:在知识库页面管理 Dify、FastGPT 和 RAGFlow 实例,通过目录或手工 ID 验证并绑定远端知识库,保留 Provider 专属检索配置和引用;对话支持按需检索及每次回答前检索。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
|
||||
- [ ] **外部知识库验收**:三家 Provider 已通过真实服务、本机 HTTP MCP 和生产 IPC 短答案检查,Dify 另通过完整 App 输入区到回答及引用的链路。FastGPT/RAGFlow 桌面专属参数、复杂多库问答和远程回答前检索仍待验证,详见[验证证据与剩余工作](./docs/features/knowledge-base/progress.md)。
|
||||
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
|
||||
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
|
||||
- [x] **向量诊断与索引任务**:提供真实向量生成诊断、按文档重建进度、取消、失败状态与重启后结果恢复;每篇成功文档立即可用于检索。
|
||||
@@ -65,16 +84,18 @@
|
||||
- [x] **分块、维护与评估**:支持固定、结构化和父子分块,分块维护、可取消重建及双语检索评估。
|
||||
- [x] **受控知识本体**:每个知识库可定义实体、关系、别名和端点约束,保留证据偏移、置信度和抽取来源,并显式提示图谱重建。
|
||||
- [x] **强制检索与引用上下文**:对话可按需或每次先检索,显示零结果、降级、失败与取消状态,并可查看引用上下文或安全打开来源。
|
||||
- [x] **魔法笔记 / Magic Notes**:提供本地优先的笔记与待办工作台、范围管理、编辑、筛选和受控 AI 评论;左侧入口可按设置显示未完成待办数量,创建、保存和评论结果使用统一应用通知。
|
||||
- [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] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;不再常驻旧缓存截断警告,但不会恢复此前已丢失的历史。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
|
||||
- [x] **紧凑会话工具记录**:逐项展开工具记录即可查看和复制结果、错误及输入参数;会话与子任务进度使用简洁行,减少重复 Runtime 摘要。OpenCode、Continue、DeepSeek Harness 和直连模型可显示已有文件路径、命令或搜索摘要,并在工具完成和会话重载后保留。
|
||||
- [x] **Task 与定制任务体验**:每个产品级 Task 只关联一条 Conversation,一条 Conversation 可承载多个 Task;左侧会话列表通过行首展开按钮显示带共享状态点的 Task 子项,父会话行不重复任务标签,UI 只展示到 Task,不暴露 Job/Run 层级。新建定制任务可关联当前或新 Conversation,默认 Execute 并沿用 Runtime、工具和审批边界;重复触发复用同一 Task,文本结果回写 Conversation,独立文件和图片保留为成果。普通消息与到期 Scheduled Task 共用 Conversation 级持久发送队列,同一会话一次只执行一项;当前回复期间仍可继续发送,队列按顺序续跑,并允许删除或“立即中断并插入”。Task Center 继续作为完整索引,不建设独立 Automation Center。当前计划触发支持单次、每日和每周;高级时区、Cron、事件触发与重试治理仍按 PRD 逐步实现。详见 [Task Center PRD](./docs/features/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md)。
|
||||
- [x] **记忆与智能心跳**:当前提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
|
||||
- [x] **智能心跳入口与范围改善**:将“智能心跳 > 心跳计划”作为完整配置的唯一权威入口,支持创建和编辑 Global 或指定一个、多个 Project 的计划;旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置不再复制心跳表单。“未来分区记忆”仍只是尚待独立设计的长期方向。详见 [智能心跳 PRD](./docs/features/smart-heartbeat/prd.md)。
|
||||
- [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、费用、失败率和成果差异。
|
||||
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
|
||||
@@ -84,6 +105,8 @@
|
||||
### 浏览器、通信、语音与应用维护
|
||||
|
||||
- [x] **Runtime 共享内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过内置 MCP 总开关和 Runtime 分配决定是否提供给 Execute,开启后不逐次询问。关闭 Agent 浏览器能力不影响用户继续在浏览器工作栏中手动前往、返回、刷新、停止加载、交互或关闭浏览器。直连模型、GoodBuddy 管理的 OpenCode 和 Continue 通过请求级权限共享按 Conversation 归属的会话和串行操作路径,用户导航会改变 Agent 下一步看到的页面。
|
||||
- [x] **浏览器多页签与按需资源**:同一对话可打开多个共享登录状态、页面与导航独立的浏览器页签,模型请求固定使用开始时绑定的目标。未使用的请求预留不创建浏览器资源,已释放页面可重新打开;保留显式截图,普通操作不再触发无用的自动截图。
|
||||
- [x] **浏览器关闭与刷新**:AI 使用中的页签也可关闭,不取消整个请求;后续显式导航会创建独立替代页。空白页不能刷新,切换或关闭面板后清除旧操作错误。
|
||||
- [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。
|
||||
- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。
|
||||
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
|
||||
@@ -94,13 +117,18 @@
|
||||
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
|
||||
- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
|
||||
- [x] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;默认不上传诊断,用户可主动附加有界的最近桌面诊断摘要。失败后保留草稿、诊断选择和同一请求编号;不会附加对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
|
||||
- [x] **内网兼容模式**:默认开启;允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书,关闭后恢复严格地址和证书校验。
|
||||
- [x] **内网兼容**:允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书;反馈、微信凭据与媒体等固定平台服务仍保持严格证书校验。
|
||||
|
||||
### 开源、构建与发布
|
||||
|
||||
- 当前源码候选为 Desktop `0.13.2`、Agent `0.11.25`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- Agent `0.11.25` 为维护版,不新增独立远端功能,复合包要求 Desktop `0.13.2`。桌面存储补修也适用于 Agent `0.11.24`,更新 Agent 不能代替升级 Desktop。
|
||||
- 候选验收与版本准备分别记录。当前存储源码已通过本地及 Linux x64 Host 真实工具任务、多项目多会话、取消和无损迁移验证;Windows 工具摘要与 DeepSeek 路径处理有完整 App 的真实模型证据。候选仍需主分支 CI 及原生发布打包,详见[验证记录](./docs/features/assistant-workbar/progress.md)。
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;Linux 同时生成 AppImage、DEB 和 RPM。
|
||||
- [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 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
|
||||
|
||||
+59
-18
@@ -210,6 +210,8 @@
|
||||
- 选项数量为 2 至 4 个。
|
||||
- 使用带 `aria-pressed` 的按钮组语义,支持方向键切换。
|
||||
- 不能用于多选筛选、页面导航或执行即时命令。
|
||||
- 与 `PageTabs` 一样,共享控件根节点必须 `flex: 0 0 auto`,不能被同级长表单或滚动区域
|
||||
压缩高度。横向空间不足时保留控件自身滚动,不把标签裁成一条窄缝。
|
||||
- 控件宽度由内容决定,除移动窄屏外不默认等分整行。
|
||||
|
||||
### 6.3 筛选工具栏
|
||||
@@ -283,6 +285,9 @@
|
||||
- 一级导航与最近会话之间、最近会话与底部账户区之间必须有可见结构分隔线。浅色主题使用 `--border-default`,深色主题可在可辨前提下使用 `--border-subtle`。
|
||||
- 当前导航项和当前会话必须同时使用至少三种信号中的两种:强调背景、可见边框、图标或文字强调。浅色主题的当前项优先使用更完整的蓝色选中表面和较高字重。
|
||||
- 未选中项保持平整,不为每一行添加卡片边框或阴影。悬停反馈不得强于选中状态。
|
||||
- 项目选择器下方常驻跨项目活动汇总,空闲时显示“暂无活动”;项目计数与项目、会话级联菜单遵守
|
||||
[项目活动汇总 UI 规则](./docs/features/application-tool-navigation/ui-design.md#10-项目活动汇总)。
|
||||
活动数量不改变项目菜单的稳定分组和顺序。
|
||||
- 桌面停靠状态下,主侧栏宽度允许在 `220px` 至 `420px` 范围内通过指针或键盘调整,
|
||||
并记住用户选择;分隔条使用 `separator` 语义,支持方向键、Home 和 End。小于
|
||||
`900px` 时继续使用固定宽度抽屉,不显示拖拽分隔条。
|
||||
@@ -309,7 +314,9 @@
|
||||
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
|
||||
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
|
||||
单行截断,不能撑宽或挤压侧栏。
|
||||
- 账户与设置入口固定在侧栏底部。已有稳定设置入口时,不在顶栏重复提供同一入口。
|
||||
- 侧栏底部固定显示文件、终端、浏览器和资源监控四个系统工具图标,设置按钮独立靠右。工具
|
||||
图标只负责打开或聚焦助手工作栏面板,不在侧栏复制工具内容。已有稳定设置入口时,不在顶栏
|
||||
重复提供同一入口。
|
||||
|
||||
### 6.9 助手工作栏
|
||||
|
||||
@@ -319,20 +326,27 @@
|
||||
动态 Tab、“+”和终端细节见
|
||||
[助手工作栏多终端页签 PRD](./docs/features/assistant-workbar/terminal-tabs-prd.md)。
|
||||
|
||||
- 升级后默认打开任务中心、工作区、浏览器和成果四个单实例应用,不根据当前页面或 Runtime
|
||||
能力自动增删稳定应用目录入口。
|
||||
- 升级后默认打开任务中心、工作区、一个浏览器实例和成果。任务中心与工作区是不可关闭的
|
||||
单实例基础应用;浏览器是可关闭的多实例应用;成果保持可关闭的单实例应用。不可关闭不
|
||||
等于锁定顺序。
|
||||
- 资源监控作为本机单实例应用保留在能力目录,但不加入升级后的默认打开实例。面板只在可见
|
||||
活动时采样,首期不持久化历史或提供后台负载告警。
|
||||
- Tab 行只显示已打开实例,行末始终提供“+”普通按钮;点击后在内容区打开应用选择页。
|
||||
“+”不使用 `tab` 语义,也不能因为终端或其他应用已经打开而消失。
|
||||
- 应用注册表声明单实例或多实例。单实例重复选择时聚焦已有 Tab;终端从首期开始支持
|
||||
多实例,每次选择创建独立 Tab 和 Shell Session。
|
||||
- Task Center 是 Task 的单例应用级索引,不使用“跟随 / 固定目标”多实例模式。每个 Task
|
||||
只关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,
|
||||
也不得把 Job/Run 提升为可导航 UI 对象。
|
||||
- 应用注册表分别声明单实例或多实例、默认打开、必须存在、可关闭和排序策略。在工作栏“+”目录中,
|
||||
单实例重复选择时聚焦已有 Tab;浏览器和终端每次选择创建独立实例。主侧栏系统工具快捷
|
||||
入口按当前上下文聚焦最近使用的匹配实例,额外实例仍从“+”目录创建。
|
||||
- Task Center 是默认跟随当前项目的单实例索引,不使用多实例模式。用户可在同一 Tab 内切换
|
||||
“当前项目”“全局任务”和“全部项目”;范围变化不得创建第二个 Task Center。每个 Task 只
|
||||
关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,也
|
||||
不得把 Job/Run 提升为可导航 UI 对象。
|
||||
- 各栏目读取当前会话或项目的对应内容;当前内容不可用时仍保留栏目入口并说明原因。
|
||||
- 能力、连接和内容可以动态变化,栏目入口不能随之自动隐藏。不可用状态必须说明原因、影响和可执行入口。
|
||||
- 用户可以主动排序或隐藏栏目,并可恢复默认布局;应用不能用用户偏好机制实现自动能力裁剪。
|
||||
- 用户可以主动排序栏目,并可关闭可关闭实例或恢复默认布局;任务中心和工作区不能隐藏或
|
||||
关闭。应用不能用用户偏好机制实现自动能力裁剪。
|
||||
- 已打开实例保留 `tablist`、`tab`、`tabpanel`、方向键、Home、End 和焦点恢复语义。
|
||||
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。
|
||||
实例过多时 Tab 区单行横向滚动,“+”保持可见,不换行或改成下拉框。不可关闭实例不渲染
|
||||
关闭按钮,也不响应关闭快捷命令。
|
||||
- 当前工作栏 Tab 必须同时使用连续内容表面、可见边框、顶部指示、图标或较高字重中的至少
|
||||
两种信号;悬停反馈不得强于当前选中态。Tab 关闭按钮保留固定布局空间,默认仅在对应 Tab 悬停或
|
||||
`focus-within` 时显示;无悬停或粗指针设备始终显示,键盘与辅助技术始终可访问。
|
||||
@@ -349,8 +363,9 @@
|
||||
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
|
||||
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
|
||||
尚未提交的文字改变 Agent 看到的页面。
|
||||
- 浏览器工具栏操作的是当前 Conversation 中 Agent 使用的同一隔离浏览器会话,用户导航会
|
||||
有意改变 Agent 下一步看到的页面。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
|
||||
- 浏览器工具栏操作的是该实例绑定的 Conversation 和 Browser Tab。同一 Conversation 的多个
|
||||
Tab 共享隔离登录上下文,但页面、导航、操作队列和元素引用相互独立。模型请求开始时冻结
|
||||
工具目标,用户切换工作栏 Tab 不改变进行中的 Agent 操作。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
|
||||
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
|
||||
不释放 Runtime 上下文。没有会话的活动 Conversation 可以直接在地址栏输入 URL 并“前往”
|
||||
创建会话,但必须复用 Agent 浏览器相同的 URL 策略与能力路径。
|
||||
@@ -380,6 +395,16 @@
|
||||
`34px × 34px` 点击区域。
|
||||
- 发布说明 Modal 与其他应用级 Modal 一样使用整窗遮罩;遮罩必须覆盖主侧栏、右侧内容和
|
||||
顶栏,不能保留一条未被遮罩的窗口控制色块。关闭操作由 Modal 自身提供。
|
||||
- 整窗固定遮罩通过 `createPortal(..., document.body)` 挂载,避免页面层叠上下文影响;
|
||||
保留 React Context、关闭与焦点管理,表单样式不得依赖已移出的页面祖先选择器。
|
||||
- 原生浏览器 `WebContentsView` 不受网页 `z-index` 控制。应用级 Modal 打开时统一释放
|
||||
浏览器 viewport;非模态对话框、菜单、局部确认或应用通知与浏览器区域相交时也释放。
|
||||
浮层关闭后只恢复当前活动实例的 viewport,不关闭、重建或刷新网页。接线与测试见
|
||||
[浏览器多实例技术设计](./docs/features/assistant-workbar/browser-tabs-technical-design.md#4-renderer-与-viewport)。
|
||||
- 遮罩必须 `inset: 0` 铺满整窗,并由 `styles.css` 中统一的遮罩选择器声明
|
||||
`-webkit-app-region: no-drag`,否则 `.brand` 与 `.topbar` 的原生拖动区域会截获指针,
|
||||
使遮罩上的控件无法悬停和点击。新增整窗遮罩时扩展该共享选择器,不要在各自规则里复制
|
||||
`no-drag`,也不要用 `inset: 38px 0 0` 之类的偏移绕开标题栏。
|
||||
- 当前会话存在 Task 时,Task 上下文栏位于标题栏下方并承载任务摘要与详情;没有 Task 时
|
||||
整行隐藏。折叠状态保持为 `46px` 单行表面,与右侧工作栏标签栏等高,左右分隔线必须水平
|
||||
对齐。该栏不重复放置新建入口。
|
||||
@@ -576,6 +601,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
### 13.1 聊天
|
||||
|
||||
- 使用 `reading` 壳层,消息流与输入区共享宽度。
|
||||
- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐,
|
||||
右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。
|
||||
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
|
||||
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
|
||||
原文,不包含推理过程、工具日志或引用元数据,结果通过应用通知反馈。流式回复完成前不显示
|
||||
@@ -593,22 +620,31 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 模式、模型或工具权限属于上下文控制,不与页面导航页签混用。
|
||||
- 模型、专家角色、工作模式、OpenCode Agent、Continue 预设和 Runtime 快捷操作使用统一的上下文单选菜单,并保持菜单互斥、键盘可达和选中状态明确。
|
||||
- Runtime 选择菜单必须反映当前执行空间的真实能力。托管 SSH 项目当前只支持 OpenCode,因此输入区只渲染已配置的 OpenCode 选项和管理入口,不显示直连模型、Continue 或 DeepSeek Harness 的不可用入口;旧会话保存的其他 Runtime 选择在项目激活时恢复为当前 OpenCode 配置。
|
||||
- 输入区第一行工具栏只承载附件、语音、知识范围、专家角色、工作模式、Runtime 选择和发送等通用操作。OpenCode Agent、Continue 预设及 Runtime 快捷操作必须放入其下方独立的 Runtime 专属功能行,通过可见分组名称、顶部边界和差异化表面与通用操作分层;该行只承载对当前消息生效的高频选择,当前 Runtime 没有可选专属功能时不保留空行。
|
||||
- 输入区使用一个完整容器,主工具栏保留附件、语音、Runtime、工作模式、会话选项及停止/发送。知识范围、专家角色、OpenCode Agent、Continue 预设和 Runtime 快捷操作收纳在会话选项浮层中;Runtime 专属选择在浮层内按提供方分组,无可选功能时不保留空分组。输入框上方以简短摘要显示生效的专家、知识范围、检索方式和 Runtime 专属选择。
|
||||
- 会话选项浮层与主行菜单互斥,内部复用上下文选择菜单;子菜单就地展开,浮层统一限制宽高并滚动。Escape 先关闭子菜单并恢复其触发按钮焦点,再关闭会话选项并恢复入口焦点;点击外部、焦点离开、切换会话或开始/结束回复时关闭浮层。
|
||||
- 会话选项入口紧随录音键,使用无常驻边框的调节图标,保留可访问名称、悬停说明和展开态反馈;浮层在工具栏左侧展开,最大宽度为 `300px`,窄窗口不超过输入区。浮层内选择器使用整行轻量列表样式,避免重复的按钮边框;知识库行右侧显示展开箭头,展开时朝上。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 后续的 Task 级委派与取消、后台执行进度/结果、Workflow/Hook 运行、长任务暂停/恢复/终止及原生会话监督统一进入应用级助手工作栏固定的“Runtime”栏目,不加入 Composer。栏目入口始终存在;只选择 Conversation 或 Task,并按 Task 聚合执行状态,不显示 Job/Run 树或独立操作菜单。内部可选区域按用户所选目标和 Runtime 的真实能力显示,不为未支持能力渲染空卡片或成排禁用按钮。切换跟随目标时必须清理上一归属的监督状态,固定目标则保持不变。
|
||||
- 设置中心只管理持久 Runtime 配置、默认值和能力清单;右侧 Runtime 栏目管理用户当前跟随或固定目标的生命周期。两处不得复制同一实时操作,栏目中的高风险操作仍须就地确认并保留取消、权限、用量和活动审计。
|
||||
- Runtime Prompt 快捷操作只把模板填入输入草稿,用户可以继续编辑;OpenCode Command 由 Runtime 原生 API 执行,输入框只承载可选参数,不以普通斜杠文本冒充执行。
|
||||
- OpenCode 原生 Task 调用在本地项目和托管 SSH 项目中统一解析为子 Agent 状态卡,不显示为普通工具卡。状态卡持续显示 Agent 类型、任务说明、运行状态、完成输出或失败原因;身份徽标、Agent 名称和状态位于首层,任务说明作为主信息,Runtime 来源作为次级元数据,输出与错误在同一卡片内按需展开。父 Agent 的推理不冒充子 Agent 过程,必须与状态卡保持明确间距。状态不能只靠颜色表达。推理、子 Agent、工具和正文统一按事件首次出现顺序进入消息块;增量更新只更新原位置,Task 临时工具块重分类后必须原位替换,不能把子 Agent 卡统一提升到消息顶部。同一调用的增量事件按稳定调用 ID 更新,不能因 ACP 首帧尚无参数而永久保留重复工具卡或活动记录。一次请求内已观察到的子 Agent 均保留在消息与活动记录中;父请求取消、失败或意外中断时仍处于活动状态的卡片必须结算为对应终态,父请求成功结束但子 Agent 未报告终态时必须显示为失败而不是推断为完成。
|
||||
- Agent 回复进行中锁定模型、专家角色、工作模式和 Runtime 定制选择器,并关闭已打开的上下文菜单;回复结束或停止后再恢复选择,避免界面状态与本次运行实际使用的上下文不一致。
|
||||
- 普通工具在主会话和子 Agent 过程中使用同一轻量折叠记录:不显示外层“工具执行”标题、计数、背景和边框,保留无障碍区域名称。记录纵向左对齐,头部按内容收宽且不超过消息宽度;展开箭头在左,工具名作为主标题,有额外信息的单行摘要弱化显示。右侧以绿色对勾显示成功完成,以图标显示运行和待执行状态,提供悬停文字和无障碍名称;失败和可恢复状态保留文字。仅重复工具名和执行状态的已知 Runtime 模板摘要在头部与详情中均隐藏;刷新重试、审批等额外提示保留。默认折叠,状态和输出更新不改变用户的展开选择;折叠时露出一行错误,展开详情使用消息可用宽度、浅背景和左侧细线,按错误、结果、独立折叠的输入参数阅读。详情保留原始文本,完整 JSON 输入可缩进显示,复制仍使用收到的原文,并通过应用通知反馈。详情工具栏只保留复制按钮,文本默认自动换行,长内容在固定最大高度内滚动,不提供换行切换和展开全部按钮。
|
||||
- OpenCode 主会话和子 Agent 的工具记录均保留 Runtime 提供的操作标题作为摘要,显示文件路径、命令或操作描述;本地 SDK 标题去除首尾空白后最多保留 240 字符,没有标题时沿用工具名回退规则。主会话工具失败或中断时保留已收到的标题;远端 ACP 继续在记录名称中显示其提供的标题。
|
||||
- 工具取消后保留“已取消”文字,避免与失败图标混淆。
|
||||
- Continue、DeepSeek Harness 和直连模型的工具名后显示单行操作摘要,内容提取、长度与完成态保留规则见[工具操作摘要](./docs/features/assistant-workbar/runtime-interactions.md#工具操作摘要)。
|
||||
- Agent 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。
|
||||
- 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。
|
||||
- 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。
|
||||
- 支持上下文状态的 Runtime 在输入区下方复用同一紧凑用量条;文案必须区分“本次模型调用”和“压缩后对话估算”。手动压缩仅在当前 Runtime 明确支持且没有活动回复时显示,作为元信息区左下角的浮动次操作,不参与输入区高度计算;元信息区始终预留稳定高度,切换 Runtime 不得让输入框上下位移。元信息区与窗口底部只保留紧凑安全留白,不形成额外空白区。进行中禁用重复操作,结果通过应用通知反馈。
|
||||
- 已选择的工作模式在触发按钮中只显示 `Ask` 或 `Execute`;完整中文含义和说明保留在菜单选项、可访问名称及输入区下方的模式说明中。
|
||||
- 宽度大于 `700px` 时,通用工具栏内的添加内容、知识范围、专家、模式和 Runtime 选择保持同一行,Runtime 专属功能在自己的下一行横向排列。窄输入区中两行分别换行,专属选择器以至少 `220px` 的基准宽度换行而不是被挤压;不能把专属控件重新塞回通用工具栏。
|
||||
- 宽度大于 `700px` 时,主工具栏的添加内容、Runtime、模式、会话选项与发送保持同一行。窄输入区按容器宽度换行,保留发送及停止入口;选项浮层不得超出输入区宽度,不再常驻独立的 Runtime 专属功能行。
|
||||
- 输入框原生支持 `Ctrl+V`:文本直接进入草稿,图片转换为本次消息附件。文件选择由上传按钮承担,不再提供独立“读取剪贴板”按钮;默认工具栏也不提供“截取当前屏幕”和“选择应用窗口”入口,避免与系统粘贴、文件选择和后续工具执行重复。
|
||||
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
|
||||
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
|
||||
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
|
||||
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
|
||||
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
|
||||
排除在可滚动范围之外。
|
||||
|
||||
### 13.2 最近对话
|
||||
|
||||
@@ -640,7 +676,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 状态卡片使用统一状态令牌,不只依赖颜色。
|
||||
- 保留“运行概览 / 待处理建议 / 报告与记录 / 心跳计划”四个同级页面。
|
||||
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
|
||||
- “未来分区记忆”仅为长期方向,数据、状态和页面尚未设计,不得显示占位入口。
|
||||
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
|
||||
|
||||
### 13.5 Task Center
|
||||
|
||||
@@ -654,8 +690,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
成果查看器,不撑高列表,也不显示 Job/Run 树。
|
||||
- “新建定制任务”使用共享 Modal,明确选择当前或新 Conversation。聊天入口默认当前
|
||||
Conversation,Task Center 入口默认新 Conversation;选择必须持续可见。
|
||||
- 创建 Modal 默认 Execute,并允许主动切换 Ask。Execute 持续显示实际 Runtime、Project、
|
||||
工作目录、工具和审批摘要;不支持工具时不得静默降级。
|
||||
- 定时消息创建 Modal 使用关联 Conversation 的 Runtime 与 Ask/Execute 模式,不再提供独立
|
||||
模式开关。未运行计划的详情显示会话当前模式,已运行任务显示该次执行记录的模式;
|
||||
不得用旧计划字段表示下一次执行权限。不支持工具时不得静默降级。
|
||||
|
||||
### 13.6 运行记录
|
||||
|
||||
@@ -685,6 +722,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
### 13.8 设置中心
|
||||
|
||||
- 全页设置使用固定标题区、左侧分类导航和独立滚动的内容区。右上角关闭按钮是离开设置中心的稳定入口。
|
||||
- 新建项目与项目设置弹窗固定标题、关闭按钮和底部操作区,仅表单内容滚动;滚动条不进入标题区,关闭按钮保留至少 40 × 40px 的点击区域。
|
||||
- 项目弹窗通过 Portal 挂载到 `document.body`,避免侧栏内的弹窗与主窗口标题栏拖动区域重叠时,Electron 截获关闭按钮的鼠标输入。
|
||||
- 左侧分类导航在宽屏使用 `220px`,中等窗口使用 `196px`,窄窗口转为横向滚动;纵向滚动条仅在内容溢出时占用右侧空间,不在左侧创建镜像预留,选项与左侧可见边界保持默认内距。分类标题使用正文级字号,分类说明使用辅助字号;右侧内容区在可用空间内流式伸缩,最大宽度使用 `standard` 壳层的 `960px`,不得以页面专属较窄宽度压缩表单。
|
||||
- 全页设置标题区与双栏内容使用共享 `--page-gutter`,标题、分类导航和内容区在同一页面边距基线上;标题继续使用标准 `PageHeader` 的底部分隔线与下内距。固定标题和双独立滚动区域不改变这些一级页面壳层规则;模态设置保留自身外框与标题边界。
|
||||
- 设置中心不显示全局操作页脚,避免重复关闭入口和没有功能意义的整宽分隔线。
|
||||
@@ -700,7 +739,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
|
||||
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
|
||||
更新,保存失败时保留最后确认状态。
|
||||
- “平台功能”使用共享 `PageTabs` 区分“通用设置”“远程项目(技术预览)”和“魔法笔记”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64/arm64 Agent 包清单分成独立卡片;清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。“魔法笔记”页提供默认开启的“显示未完成待办数量”持久 Switch,关闭后只隐藏左侧数量徽标,不改变待办数据或魔法笔记入口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
|
||||
- “平台功能”使用共享 `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 不丢弃草稿,关闭设置中心前必须先保存或撤销。
|
||||
@@ -785,6 +824,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
|
||||
### 15.5 验收
|
||||
|
||||
执行与取证方法见 [Electron UI 自动化测试规范](./docs/quality/electron-ui-automation.md),包括真实 Portal、原生键盘、计算样式与 PNG 审阅;视觉 fixture 与完整生产 App 的结果须分别标注。
|
||||
|
||||
- [ ] 在浅色与深色主题下检查所有页面、弹窗、菜单、输入框、表格和空状态。
|
||||
- [ ] 在大于等于 `1200px`、`960px`、`720px` 和小于 `720px` 的窗口宽度检查布局。
|
||||
- [ ] 仅使用键盘完成导航、筛选、创建、编辑、确认和取消。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"agentVersion": "0.11.18",
|
||||
"agentVersion": "0.11.25",
|
||||
"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 }
|
||||
+55
-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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,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,
|
||||
@@ -452,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) ||
|
||||
@@ -617,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}`)
|
||||
@@ -626,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
|
||||
)
|
||||
@@ -641,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 {
|
||||
@@ -668,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
|
||||
}
|
||||
@@ -717,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',
|
||||
@@ -739,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)
|
||||
) {
|
||||
@@ -748,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`
|
||||
@@ -770,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(
|
||||
@@ -783,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 {
|
||||
@@ -795,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)
|
||||
@@ -843,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({
|
||||
@@ -872,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)
|
||||
|
||||
@@ -912,7 +914,8 @@ function buildAgentBundle(options) {
|
||||
projectRoot,
|
||||
staging,
|
||||
arch,
|
||||
lock.koffi.version
|
||||
lock.koffi.version,
|
||||
platform
|
||||
)
|
||||
const launcher = [
|
||||
'#!/bin/sh',
|
||||
@@ -980,6 +983,7 @@ function buildAgentBundle(options) {
|
||||
acpSdkVersion: acpSdkPackage.version,
|
||||
koffiVersion: koffi.version,
|
||||
koffiNativePackage: koffi.nativePackage,
|
||||
platform,
|
||||
arch,
|
||||
protocol: lock.protocol,
|
||||
signingKeyId: signingIdentity.keyId
|
||||
@@ -1004,6 +1008,7 @@ function buildAgentBundle(options) {
|
||||
? 'test'
|
||||
: 'production',
|
||||
expected: {
|
||||
platform,
|
||||
agentVersion: lock.agentVersion,
|
||||
arch,
|
||||
protocol: lock.protocol
|
||||
@@ -1073,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
|
||||
@@ -1203,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',
|
||||
|
||||
+47
-6
@@ -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(
|
||||
@@ -90,6 +94,10 @@ const portableRequiredFiles = [
|
||||
'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',
|
||||
@@ -122,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: [
|
||||
@@ -135,7 +144,8 @@ const platformDefinitions = {
|
||||
'MacOS',
|
||||
productName
|
||||
],
|
||||
runtimeExecutable: 'opencode'
|
||||
runtimeExecutable: 'opencode',
|
||||
ripgrepExecutable: 'rg'
|
||||
},
|
||||
linux: {
|
||||
builderFlag: '--linux',
|
||||
@@ -143,7 +153,8 @@ const platformDefinitions = {
|
||||
supportedFormats: ['AppImage', 'deb', 'rpm'],
|
||||
unpackedPattern: /^linux(?:-.+)?-unpacked$/u,
|
||||
executable: [packageJson.name],
|
||||
runtimeExecutable: 'opencode'
|
||||
runtimeExecutable: 'opencode',
|
||||
ripgrepExecutable: 'rg'
|
||||
}
|
||||
}
|
||||
const formatExtensions = {
|
||||
@@ -820,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) =>
|
||||
@@ -868,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
|
||||
@@ -1056,6 +1075,12 @@ 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'), '版本更新说明')
|
||||
@@ -1080,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'
|
||||
@@ -1107,7 +1137,8 @@ function verifyUnpackedOutput(directory, options) {
|
||||
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) {
|
||||
@@ -1567,7 +1598,7 @@ function printHelp() {
|
||||
|
||||
默认格式:
|
||||
windows: nsis, portable (ZIP)
|
||||
macos: dmg, zip
|
||||
macos: dmg
|
||||
linux: AppImage, deb, rpm`)
|
||||
}
|
||||
|
||||
@@ -1629,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')
|
||||
|
||||
@@ -184,7 +184,7 @@ async function run() {
|
||||
model: 'qwen-plus',
|
||||
supportsImageInput: false,
|
||||
requestHeaders: {},
|
||||
harnessVersion: '0.1.0-rc.8',
|
||||
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,
|
||||
};
|
||||
+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'
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
|
||||
+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 }
|
||||
@@ -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 个用户可提交的待执行项。启动时将未完成的派发恢复为
|
||||
@@ -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:调度与运行基础
|
||||
@@ -569,7 +572,7 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
|
||||
|
||||
## 18. 总体验收标准
|
||||
|
||||
- [ ] 智能心跳保持独立,不作为 Task 类型;未来分区记忆尚未设计。
|
||||
- [ ] 智能心跳保持独立,不作为 Task 类型;分区记忆协同与唤起条件尚未设计。
|
||||
- [ ] Task Center 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以
|
||||
关联多个 Task。
|
||||
- [ ] 当前 UI 只展示到 Task,不提供 Job/Subjob/Run 树或独立导航。
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 适用范围 | GoodBuddy 桌面端、ShareServer、网关间联邦协议 |
|
||||
| 桌面端设计 | [设备发现与共享](../features/device-sharing/README.md) |
|
||||
| 服务端设计 | [ShareServer](../features/share-server/README.md) |
|
||||
| 服务端设计 | [ShareServer](../../../goodbuddy-shareserver/docs/features/share-server/README.md) |
|
||||
|
||||
## 1. 文档职责
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
1. **GoodBuddy Desktop** 在设置中提供“设备发现与共享”,默认使用局域网发现与直连,也可
|
||||
由用户额外连接一个或多个 ShareServer。
|
||||
2. **ShareServer** 是独立部署、独立升级的 Web 服务,提供组织、设备、能力、权限、更新、
|
||||
联邦、中继和审计管理。它不运行在 Electron Main 中,也不由桌面端代为启动。
|
||||
联邦、中继、Office 编辑和审计管理。它不运行在 Electron Main 中,也不由桌面端代为启动。
|
||||
|
||||
### 2.2 能力优先,来源可见
|
||||
|
||||
@@ -55,6 +55,17 @@ 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. 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
@@ -71,8 +82,10 @@ ShareServer 负责身份、目录、策略、任务路由、更新和审计。
|
||||
| `Task` | 一次远程能力调用及其输入、执行、输出和终态 |
|
||||
| `Package` | 可下载到本机安装的签名能力工件 |
|
||||
| `FederationTrust` | 两个组织网关之间经管理员确认的双边信任关系 |
|
||||
| `DocumentEditSession` | ShareServer 托管的单文档短期编辑会话、工作副本和保存检查点 |
|
||||
|
||||
“网关”是 ShareServer 的网络角色;产品名称和代码目录统一使用 `ShareServer` / `shareserver`。
|
||||
“网关”是 ShareServer 的网络角色;产品名称为 `ShareServer`,独立仓库为
|
||||
`goodbuddy-shareserver`。
|
||||
|
||||
## 4. 系统拓扑
|
||||
|
||||
@@ -83,6 +96,10 @@ GoodBuddy Desktop A <-- mDNS 发现 + 配对后 TLS 直连 --> GoodBuddy Desktop
|
||||
单组织:
|
||||
GoodBuddy Desktop A -- 出站连接 --> ShareServer <-- 出站连接 -- Provider Device B
|
||||
|
||||
Office 编辑:
|
||||
GoodBuddy Desktop --> ShareServer --> ONLYOFFICE Docs
|
||||
└-------> 临时工作副本与保存检查点
|
||||
|
||||
跨组织:
|
||||
Desktop A --> ShareServer A <== 联邦信任 ==> ShareServer B <-- Provider Device B
|
||||
```
|
||||
@@ -120,6 +137,7 @@ Desktop A --> ShareServer A <== 联邦信任 ==> ShareServer B <-- Provider Devi
|
||||
- 计算组织强制策略,签发短期任务授权。
|
||||
- 路由任务、维护在线状态,并在允许时中继数据。
|
||||
- 管理签名包、发布通道、更新策略和撤销。
|
||||
- 可选管理 Office 编辑会话、ONLYOFFICE 接入、工作副本、保存回调、保留和清理。
|
||||
- 建立双边联邦关系并限制跨组织可见性与调用。
|
||||
- 保存有界审计元数据、用量和管理操作。
|
||||
|
||||
@@ -338,6 +356,16 @@ created/awaiting_approval/queued/connecting -> rejected | expired | cancelled
|
||||
- 未识别字段在声明允许扩展的位置忽略,安全与授权枚举中的未知值一律拒绝。
|
||||
- 联邦只交换双方共同支持的能力和策略表达式,不降低任一方策略。
|
||||
|
||||
### 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 最小审计事件
|
||||
@@ -357,6 +385,8 @@ created/awaiting_approval/queued/connecting -> rejected | expired | cancelled
|
||||
|
||||
- 桌面端是本地配对、本机共享偏好和本地活动记录的权威。
|
||||
- ShareServer 是组织成员、设备注册、组织策略、发布目录和组织审计的权威。
|
||||
- ShareServer 是 Office 工作副本、服务端保存检查点、编辑会话和保留清理的权威;Desktop 或
|
||||
远程 GoodBuddy Agent 是源文件是否成功回写的权威。
|
||||
- 能力提供者是运行中任务及结果是否产生的权威。
|
||||
- 联邦两端各自保存本组织审计,不复制对方完整成员目录或内部策略。
|
||||
|
||||
@@ -375,6 +405,10 @@ ShareServer 可以部署到公网,因此公网连接、外部组织和服务
|
||||
8. 密钥只进入对应安全存储,诊断和审计使用固定字段与脱敏错误码。
|
||||
9. 限制握手、清单、帧、文件、并发、速率、任务时长和审计查询大小。
|
||||
10. 撤销设备、用户、Grant、Publication、Package 或 FederationTrust 后停止签发新令牌。
|
||||
11. Office 编辑器使用固定 origin 和隔离会话,不获得 Electron、文件系统、Shell、Runtime 或
|
||||
普通 ShareServer 管理 API;Document Server 默认禁止任意出站网络和未批准插件。
|
||||
12. Office 文件正文由服务端实际处理,组织策略必须定义允许格式、大小、保留和内容访问角色,
|
||||
Desktop 在上传前显示这些事实。
|
||||
|
||||
## 14. 建议领域实体
|
||||
|
||||
@@ -400,27 +434,35 @@ PackageRelease
|
||||
FederationTrust
|
||||
AuditEvent
|
||||
Revocation
|
||||
DocumentEditSession
|
||||
DocumentCheckpoint
|
||||
```
|
||||
|
||||
共享协议只定义跨端 DTO 和状态,不规定桌面 SQLite 与 ShareServer 数据库使用相同表结构。
|
||||
|
||||
## 15. 仓库边界
|
||||
|
||||
目标目录结构:
|
||||
当前仓库边界(跨仓库文档链接约定两个仓库克隆到同一级目录):
|
||||
|
||||
```text
|
||||
docs/
|
||||
architecture/share-network-architecture.md
|
||||
features/device-sharing/
|
||||
features/share-server/
|
||||
packages/
|
||||
share-protocol/ # 与 Electron、数据库和 Web 框架无关的 schema/类型
|
||||
shareserver/ # 独立 Web 服务及管理前端
|
||||
src/ # GoodBuddy Desktop
|
||||
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` 不反向依赖桌面端 `src/shared`;
|
||||
双方依赖独立 `share-protocol`。协议包不得包含数据库模型、Electron API、服务实现或凭据。
|
||||
ShareServer 源码和服务端文档已迁入独立仓库:
|
||||
`ssh://git@git.digiman.live:11022/root/goodbuddy-shareserver.git`。
|
||||
当前服务是交互原型,尚未提供真实 Desktop 集成。共享网络总体设计仍由本文统一维护。
|
||||
|
||||
ShareServer 不反向依赖桌面端 `src/shared`,构建和运行不要求桌面仓库存在。
|
||||
真实跨端协议实现时双方依赖独立 `share-protocol` 包,具体仓库归属和发布方式届时确定,
|
||||
不提前创建空包。协议包不得包含数据库模型、Electron API、服务实现或凭据。
|
||||
|
||||
## 16. 共同验收标准
|
||||
|
||||
@@ -435,6 +477,10 @@ src/ # GoodBuddy Desktop
|
||||
- [ ] 跨组织调用同时满足两端策略;联邦撤销后不能创建新任务。
|
||||
- [ ] 审计默认只保存必要元数据,不保存正文、文件、密钥或认证信息。
|
||||
- [ ] ShareServer 可独立部署和升级,不由 Electron 启动,也不影响桌面端离线使用。
|
||||
- [ ] 启用 Office 服务后,Desktop 可在多实例文档页签中编辑 DOCX、XLSX 和 PPTX;保存回调、
|
||||
源文件冲突或网络故障均不会破坏原文件。
|
||||
- [ ] Office 会话的启动、文件和 callback 凭据不可互换,过期或撤销后不能重用;临时工作副本
|
||||
按策略清理,默认审计不保存文档正文或 AI 选区内容。
|
||||
|
||||
## 17. 相关设计
|
||||
|
||||
@@ -442,9 +488,10 @@ src/ # GoodBuddy Desktop
|
||||
- [设备发现与共享功能逻辑](../features/device-sharing/logic-design.md)
|
||||
- [设备发现与共享 UI](../features/device-sharing/ui-design.md)
|
||||
- [设备发现与共享技术设计](../features/device-sharing/technical-design.md)
|
||||
- [ShareServer PRD](../features/share-server/prd.md)
|
||||
- [ShareServer 功能逻辑](../features/share-server/logic-design.md)
|
||||
- [ShareServer 管理界面](../features/share-server/ui-design.md)
|
||||
- [ShareServer 技术设计](../features/share-server/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)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# 数据库迁移与大数据升级指南
|
||||
|
||||
本文约束已发布用户数据的数据库升级。具体字段、存储表示和功能行为由对应功能文档负责;
|
||||
本文不建立通用迁移框架、回滚引擎或独立状态日志。
|
||||
|
||||
## 先确认问题和数据边界
|
||||
|
||||
1. 先只读统计文件大小、WAL、页数、空闲页、表与字段的字节数,不先删除或压缩数据库。
|
||||
2. 区分有效内容、重复存储、索引和已释放但未回收的空间。空闲页少时,单独
|
||||
`VACUUM` 不能消除重复记录。
|
||||
3. 查清实际写入、读取、删除、重放与恢复入口。修改 Runtime 相关持久化时,同时检查
|
||||
本机和 GoodBuddy Agent 的桌面路径,不假设一个入口覆盖另一个入口。
|
||||
4. 明确用户必须保留的信息、稳定 ID、顺序、时间、引用与远程去重字段。
|
||||
分支内未发布、可安全重建的内部记录不需要兼容机制。
|
||||
|
||||
## 实现原则
|
||||
|
||||
- 先修新增数据路径,再修历史数据。历史转换与新写入尽量使用同一套转换函数。
|
||||
- 普通 schema 变更使用既有 `PRAGMA user_version` 和 SQLite 事务;
|
||||
已升级 schema 不允许旧客户端强行打开,不通过回写版本号支持降级。
|
||||
- 大数据转换不得同步阻塞 Electron Main 或 Renderer。放到工作线程,在业务连接
|
||||
打开前处理,并使用应用的共享字体、主题与页面组件显示进度和可执行的失败提示。
|
||||
- 根据实测设置有界读取批次与缓存,批次提交后再报告进度。保持 SQLite 事务与
|
||||
checkpoint,不靠关闭同步落盘、取消约束或一次性读入整库提速。
|
||||
- 从已提交数据直接重试和还原即可完成的迁移,不增加 durable snapshot、journal、
|
||||
receipt 或多阶段恢复状态。退出及失败至少回滚未提交批次,保留已完成部分。
|
||||
- 差量编码必须能精确还原原始事件。更新内容、删除或重排都要覆盖;涉及远程来源时,
|
||||
去重、冲突判断、消息投影和恢复序号不得失效。
|
||||
- 释放数据库页和归还文件系统空间是不同步骤。先减少实际存储内容,再执行需要的
|
||||
空间回收;保留足够临时空间,磁盘不足时明确失败并允许重试。
|
||||
- SQLite 同库写入是串行的。是否增加读取/转换并行,应由实际读取、解析、写入、
|
||||
提交与 checkpoint 的耗时决定,不把增加写线程当作默认优化。
|
||||
|
||||
## 验证与备份
|
||||
|
||||
### 合成回归
|
||||
|
||||
覆盖正常转换、无变化记录、重复打开、内容修订、失败事务和退出续跑。数据库大小问题
|
||||
必须同时验证新增写入的增长量、旧记录可还原性以及文件实际缩小,而不只检查 schema。
|
||||
对象比较必须覆盖真实生产形状和 JSON 往返,尤其是可选字段值为 `undefined` 时与字段缺省
|
||||
的差异。再次修复已发布迁移产生的数据时,提高目标 schema,并覆盖已升级用户的入口;
|
||||
不能只处理尚未迁移的旧库。
|
||||
|
||||
### 真实旧库
|
||||
|
||||
1. 关闭占用源数据库的应用,确认 WAL 状态。没有 WAL 时可复制已关闭的数据库;
|
||||
有活动 WAL 时使用 SQLite 一致性备份机制,不只复制主文件。
|
||||
2. 备份到空间足够的位置,记录字节数并校验 SHA-256。备份和测试副本分离,
|
||||
不直接在唯一原件或唯一备份上试跑。
|
||||
3. 使用当前源码或用户要求的 portable 正常启动入口执行升级,不用手工 SQL 代替
|
||||
产品升级过程。退出一次后重新启动,验证已提交数据可以继续处理。
|
||||
4. 比较升级前后消息、任务、成果等用户表的内容;对转换事件逐条还原后比较原始内容、
|
||||
ID、顺序、时间和去重来源,不只比较行数。日志只能包含统计和断言结果,不记录正文。
|
||||
5. 运行数据库完整性检查,记录主文件和 WAL 的实际字节数、回收比例、耗时、平台、
|
||||
源 schema 与目标 schema。确认原件和备份未被改写。
|
||||
6. 运行 `npm test`、`npm run typecheck`、`npm run lint`;生产构建变化按开发验证规则
|
||||
检查,发布准备则遵循[发布手册](./release-runbook.md),不重复本地打包。
|
||||
7. 用真实模型模拟正常用户任务,允许主代理与子代理按需调用工具、修改独立测试项目、
|
||||
运行测试和处理失败;只读纯文字模型调用不能证明工具事件增长正常。记录实际提供商
|
||||
请求数与统计范围,不把 Runtime 用量上报数当成全部 HTTP 请求数。
|
||||
|
||||
## 发布与维护
|
||||
|
||||
发布说明应告知升级范围、可能耗时、退出重试、额外磁盘空间和降级边界。不得把一个测试库
|
||||
的回收比例当作所有用户都能获得的保证。功能文档记录具体转换规则,进度文档记录实际
|
||||
证据和未完成项;未经验证的阶段不能标为完成。
|
||||
|
||||
### 已有实例
|
||||
|
||||
- Desktop 0.13.1:子任务累计过程重复保存导致数据库膨胀,转换到 schema 34 的差量事件
|
||||
表示,并回收空间。具体规则见
|
||||
[执行记录存储与升级回收](../features/assistant-workbar/execution-history-storage.md),
|
||||
数据保留与真实旧库验证见[工作栏进度](../features/assistant-workbar/progress.md)。
|
||||
- 当前未发布源码:schema 35 同时整理旧快照和 schema 34 的重复工具块,工具字段按
|
||||
JSON 可保存的值比较,不因 `undefined` 被省略而误判变化。
|
||||
@@ -38,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.
|
||||
@@ -50,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
|
||||
@@ -66,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
|
||||
@@ -98,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
|
||||
@@ -202,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.
|
||||
@@ -260,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,90 @@
|
||||
# 应用导航与系统工具入口
|
||||
|
||||
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,内置应用通过最底部应用中心
|
||||
访问,并可常驻主导航;系统工具由右侧工作栏 Tab 承载。应用商店是后续功能,首期不提供
|
||||
商店入口、推荐位或安装流程。
|
||||
|
||||
当前分期见 [PRD 第 8 节](./prd.md#8-分期),实施步骤见[技术设计第 11 节](./technical-design.md#11-实施顺序)。
|
||||
本轮更新实施计划与范围,旧版功能要求、验收和配套设计仍需按新计划同步,此项列为实现前的第一步。
|
||||
|
||||
项目活动汇总独立于上述应用中心计划,范围见 [FR-14](./prd.md#fr-14-项目活动汇总)。
|
||||
计数规则由[活动逻辑](./logic-design.md#9-项目活动汇总)定义,入口与级联菜单见
|
||||
[活动界面](./ui-design.md#10-项目活动汇总),数据加载与会话跳转见
|
||||
[技术设计](./technical-design.md#12-项目活动汇总)。此处“活动”指当前运行或待处理会话,
|
||||
与用于审计的“运行记录”不同;已完成未读面板属于后续范围,本次未实现。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、产品边界、功能要求与产品验收 |
|
||||
| [User Stories](./user-stories.md) | 用户场景及 Given/When/Then 验收 |
|
||||
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
|
||||
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
|
||||
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
|
||||
| [实施进度](./progress.md) | 项目活动级联的生产实现与验证记录 |
|
||||
| [应用中心交互 Demo](./navigation-demo.html) | 对比六个候选方案;配套 CSS 和 JS 仅服务原型 |
|
||||
|
||||
Demo 可直接在浏览器中打开,支持方案切换、浅深主题、添加、移除、排序和商店模拟。
|
||||
六个方案共用本次页面会话内的应用配置,刷新后恢复默认。商店、资源指标和工具内容均为
|
||||
演示数据,不执行真实安装或本机操作。窄屏上下排列侧栏与输入区,桌面展示完整应用布局。
|
||||
这些方案用于讨论最底部应用中心;其中商店页、商店应用安装与 F 方案属于未来交互参考,
|
||||
不纳入首期实现或验收。首期产品只提供内置应用;Demo 暂保留远期示例以便后续讨论。
|
||||
底部输入区共用一个简化候选布局:附件、语音、Runtime、模式和发送位于主操作行,专家、
|
||||
知识范围及 Runtime 专属选项通过“选项”展开,非默认选择显示为可移除标签。可体验文件名
|
||||
附件、模拟语音、模板填入、原生命令选择、8 秒模拟回复、发送排队、立即插入、删除、停止,
|
||||
以及上下文详情和模拟压缩。文件只读取名称,语音使用固定示例文本,不录音、不上传、不调用 AI。
|
||||
这是输入区设计讨论用的原型,尚未改变正式产品或替代现有 UI 规范。
|
||||
左上角项目选择器下方仅在有运行中或待处理会话时显示活动摘要,全部空闲时隐藏。点击摘要打开项目
|
||||
列表,悬停或点击项目向右展开会话子菜单,点击会话才切换项目与对话。子菜单与项目列表
|
||||
相接,项目行保留子菜单箭头,会话行只显示名称与状态。支持上下方向键选择、右键进入、左键或 Esc 返回,顶层 Esc 关闭;窄屏在同一浮层内
|
||||
进入会话层,并提供返回按钮。页面上方可切换“有待处理、仅运行中、仅当前项目运行、全部
|
||||
空闲”四种场景。项目与会话均为演示数据;该级联交互已接入正式产品活动入口,范围与
|
||||
验证记录见[实施进度](./progress.md)。
|
||||
按最新讨论,Demo 已移除左侧系统工具快捷区:文件、终端、浏览器和资源监控只在右侧
|
||||
工作栏 Tab 中演示,不列入应用中心。上方正式文档仍保留原设计,待方案确定后同步。
|
||||
|
||||
六个方案共用“我的应用”区:通过 + 或“全部应用”选择常驻应用,编辑时可拖动或使用
|
||||
上下箭头排序、移除入口。未常驻的已安装应用仍可从“全部应用”打开。商店包含“项目管理”
|
||||
和“阅读空间”应用示例,支持仅安装或安装并常驻左侧菜单,常驻不表示后台运行。
|
||||
常驻应用在本次页面会话内跨方案保持一致;“恢复默认应用”重置安装与常驻配置。
|
||||
移除入口不会卸载应用或关闭当前页面,刷新后恢复默认。
|
||||
|
||||
魔法笔记提供应用中心“设置”、笔记页面“笔记设置”和全局设置演示入口,均打开同一设置
|
||||
面板,共用本次页面会话内的配置。面板沿用现有选项:显示未完成待办数量、AI 评论方式
|
||||
(即时/保存后自动/保存后手动)、AI 评论形式(长评 + 要点/长评/要点)。现有“显示
|
||||
魔法笔记入口”在原型中与“常驻左侧菜单”统一,不额外增加功能启停开关。侧栏待办数为
|
||||
固定的 3 项演示数据。修改即时生效于原型,不调用 AI 或写入真实应用设置。
|
||||
|
||||
| 方案 | 讨论重点 |
|
||||
| --- | --- |
|
||||
| A:紧凑入口 | 从底部向上展开应用列表 |
|
||||
| B:应用宫格 | 在展开面板中展示应用图标、名称和常驻操作 |
|
||||
| C:应用预览 | 用卡片说明应用用途 |
|
||||
| D:搜索启动台 | 搜索打开能力,支持 Ctrl / Command K、方向键与 Enter |
|
||||
| E:常驻管理 | 已常驻应用优先排列,可取消常驻或打开 |
|
||||
| F:商店发现(未来) | 默认展开模拟商店,不纳入首期 |
|
||||
|
||||
D 参考 [Raycast](https://www.raycast.com/) 的搜索启动与扩展发现方式。早期方案参考过
|
||||
Notion 收藏和 VS Code Profiles,当前版本已移除场景工具包与快捷区。
|
||||
原型支持 URL 的 `#launcher`、`#favorites`、
|
||||
`#scenes` 等片段直接打开指定方案。
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| 系统入口 | GoodBuddy 工作流程或审计所必需的固定入口,用户不能卸载、隐藏或改变类型 |
|
||||
| 应用 | 在主内容区打开完整一级页面的功能,可显示在“当前对话”所在的主导航区 |
|
||||
| 系统工具 | GoodBuddy 自带且不可卸载的辅助能力,包括文件、终端、浏览器和资源监控,由右侧助手工作栏 Tab 承载 |
|
||||
| 插件应用 | 未来由插件提供的一级页面入口 |
|
||||
| 插件工具 | 未来由插件提供的辅助面板或命令入口 |
|
||||
| 快捷入口 | 只负责打开或聚焦能力,不复制该能力的界面和业务状态 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md)
|
||||
- [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)
|
||||
- [助手工作栏多终端页签 PRD](../assistant-workbar/terminal-tabs-prd.md)
|
||||
- [智能心跳 PRD](../smart-heartbeat/prd.md)
|
||||
- [知识库功能文档](../knowledge-base/README.md)
|
||||
@@ -0,0 +1,196 @@
|
||||
# 应用导航与系统工具入口功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 关联 User Stories | [User Stories](./user-stories.md) |
|
||||
|
||||
## 1. 不变量
|
||||
|
||||
1. “当前对话”“运行记录”和“设置”始终是系统入口。
|
||||
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
|
||||
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
|
||||
4. 左下角只提供快捷入口,工具内容仍由助手工作栏承载。
|
||||
5. 隐藏应用不等于停用功能,也不删除、迁移或清理业务数据。
|
||||
6. 快捷入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
|
||||
|
||||
## 2. 入口模型
|
||||
|
||||
```ts
|
||||
type NavigationContribution =
|
||||
| {
|
||||
type: 'system-entry'
|
||||
id: 'chat' | 'activity' | 'settings'
|
||||
placement: 'primary-start' | 'primary-end' | 'footer'
|
||||
}
|
||||
| {
|
||||
type: 'application'
|
||||
id: 'magic-notes' | 'knowledge' | 'heartbeat'
|
||||
source: 'builtin'
|
||||
visible: boolean
|
||||
}
|
||||
| {
|
||||
type: 'system-tool'
|
||||
id: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
workbarAppId: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
}
|
||||
```
|
||||
|
||||
该模型只覆盖首期入口。插件贡献模型随插件 Host、权限和生命周期另行设计。
|
||||
|
||||
## 3. 主导航组合规则
|
||||
|
||||
主导航按以下顺序计算:
|
||||
|
||||
```text
|
||||
当前对话
|
||||
可见的内置应用
|
||||
运行记录
|
||||
```
|
||||
|
||||
当前对话和运行记录不参与排序。首期内置应用默认顺序为魔法笔记、知识库、智能心跳;显隐
|
||||
设置只从中移除对应项,不改变固定入口的位置。
|
||||
|
||||
### 3.1 应用可见性与业务状态
|
||||
|
||||
| 导航可见 | 功能启用 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 是 | 是 | 显示入口并允许进入 |
|
||||
| 否 | 是 | 隐藏入口,后台和数据保持原状 |
|
||||
| 是 | 否 | 不显示不可用入口;设置页保留启用路径 |
|
||||
| 否 | 否 | 不显示入口,保留符合该功能自身规则的数据 |
|
||||
|
||||
“功能启用”由各应用现有产品设置负责。例如魔法笔记启停仍使用原设置;导航偏好不能覆盖它。
|
||||
|
||||
## 4. 系统工具打开规则
|
||||
|
||||
### 4.1 通用流程
|
||||
|
||||
```text
|
||||
用户激活工具按钮
|
||||
-> 校验按钮仍可用
|
||||
-> 向助手工作栏发送打开意图
|
||||
-> 工作栏按应用实例策略查找或创建实例
|
||||
-> 展开工作栏
|
||||
-> 激活目标实例
|
||||
-> 把焦点移入面板标题或首个主要控件
|
||||
```
|
||||
|
||||
若创建失败,工作栏保持展开并在对应能力位置显示错误;主页面和其他工作栏实例不被清空。
|
||||
|
||||
### 4.2 决策表
|
||||
|
||||
| 工具 | 匹配实例存在 | 匹配实例不存在 | 上下文不可用 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文件 | 激活不可关闭的单实例并保留其跟随或固定状态 | 仅在迁移或布局修复时补回 `workspace` | 打开面板并显示目录修复入口 |
|
||||
| 终端 | 激活当前执行空间最近的 `starting` 或 `running` 实例 | 创建一个绑定当前执行空间的实例 | 没有项目时使用本机 Home |
|
||||
| 浏览器 | 激活绑定当前 Conversation 的最近使用实例 | 创建绑定当前 Conversation 的新实例 | 激活最近使用实例;不存在时创建待绑定实例 |
|
||||
| 资源监控 | 激活 `resources` 单实例 | 创建本机 `resources` 单实例 | 部分指标不可用时显示原因并保留其余指标 |
|
||||
|
||||
文件保持单一实例原有绑定。浏览器快捷入口按当前 Conversation 匹配最近使用实例,而工作栏
|
||||
“+”始终新建浏览器实例。固定目标失效时显示该目标的修复状态,不回退到当前项目、默认
|
||||
工作区或当前会话。
|
||||
|
||||
`closing`、`exited`、`interrupted` 和 `failed` 终端不属于可复用实例。当前执行空间只有这些
|
||||
终端时,快捷按钮创建新实例并保留原 Tab 的历史状态。用户需要额外的并行终端时使用工作栏
|
||||
“+”或终端面板中的既有创建命令。
|
||||
|
||||
### 4.3 选中与状态
|
||||
|
||||
工具按钮满足以下条件时显示选中:助手工作栏已展开,且当前活动实例的 `appId` 与工具映射
|
||||
一致。只有实例存在但未激活时不显示选中。
|
||||
|
||||
徽标优先级为:等待用户处理 > 错误 > 运行中 > 无徽标。同一按钮只显示一个视觉徽标,
|
||||
可访问名称包含完整状态。
|
||||
|
||||
资源监控首期不在后台采样,因此不显示负载告警徽标。只有资源面板是工作栏当前活动实例且
|
||||
工作栏已展开时,资源按钮才显示选中状态。
|
||||
|
||||
### 4.4 资源采样状态
|
||||
|
||||
```text
|
||||
inactive -> warming -> loading -> live
|
||||
-> partial
|
||||
-> error
|
||||
live/partial/error -> loading
|
||||
warming/loading/live/partial/error -> inactive
|
||||
```
|
||||
|
||||
- `inactive`:资源面板未处于可见活动状态,没有采样定时器。
|
||||
- `warming`:首次进入或停止后恢复,建立 CPU 和磁盘 IO 区间基线;内存仍可立即显示。
|
||||
- `loading`:正在请求后续样本;请求未结束时跳过后续时钟触发。
|
||||
- `live`:所有首期指标可用,按 2 秒间隔追加样本。
|
||||
- `partial`:至少一个指标可用;不可用项携带稳定错误码,其他指标继续更新。
|
||||
- `error`:本次没有任何可用指标,保留已有历史;自动时钟或手动操作可以重试。
|
||||
|
||||
资源面板为活动实例、工作栏展开且 `document.visibilityState` 为 `visible` 时才离开 `inactive`。
|
||||
任一条件失效即停止定时器并重置区间基线。历史队列只保留最后 150 个样本;时间间隔较长或
|
||||
采样失败时保留真实时间缺口,不补零或伪造中间数据。
|
||||
|
||||
## 5. 设置保存规则
|
||||
|
||||
应用导航偏好与应用业务启停分开保存。Switch 每次切换立即提交完整目标集合;请求完成前禁用
|
||||
三个应用显隐 Switch,下一次操作基于最新确认结果提交,防止并发更新互相覆盖。
|
||||
|
||||
保存失败时:
|
||||
|
||||
- 主导航和 Switch 恢复最后一次成功保存的偏好。
|
||||
- 设置行显示可重试错误。
|
||||
- 不回滚或修改各应用的业务状态。
|
||||
|
||||
## 6. 与需求的对应关系
|
||||
|
||||
| 逻辑 | PRD | User Stories |
|
||||
| --- | --- | --- |
|
||||
| 固定入口不变量 | FR-1、FR-2 | US-A1、US-A2 |
|
||||
| 应用显隐和失败恢复 | FR-3、FR-4、FR-11、FR-12 | US-B1、US-B2、US-B4、US-B5 |
|
||||
| 工具打开决策表 | FR-5 至 FR-10 | US-C1 至 US-C7 |
|
||||
| 资源采样状态 | FR-9 | US-C6 |
|
||||
| 无障碍与窄窗口 | FR-13 | US-D1、US-D2 |
|
||||
|
||||
## 7. 逻辑完整性评估
|
||||
|
||||
首期规则已覆盖固定入口、应用显隐、保存失败、系统工具匹配与显式多实例差异、本机 Home、
|
||||
托管 SSH、无活动会话、目录失效、资源部分可用、采样停止、权限边界、键盘和窄窗口。其他
|
||||
工具面板内部的取消、超时、
|
||||
连接恢复和固定目标失效继续由助手工作栏负责,本功能不复制这些状态机。
|
||||
|
||||
## 8. 后续产品决策
|
||||
|
||||
- 插件工具上线后,快捷栏允许固定的最大数量及溢出规则。
|
||||
- 插件应用是否允许用户排序,以及固定系统入口之间允许的排序范围。
|
||||
|
||||
这些决策不影响首期四个系统工具的固定入口和内置应用显隐实现。
|
||||
|
||||
## 9. 项目活动汇总
|
||||
|
||||
对应 [FR-14](./prd.md#fr-14-项目活动汇总),按 Conversation ID 聚合,每条会话最多一行。
|
||||
数据来源为活动会话 ID、会话中的流式消息及待回答/待审批请求,以及全部可见的运行中或
|
||||
等待处理 Task;不能只取当前项目、当前搜索结果或每条会话的最新 Task。
|
||||
|
||||
| 优先级(高到低) | 条件 | 汇总类别 |
|
||||
| --- | --- | --- |
|
||||
| 待回答 | 消息有待回答问题 | 待处理 |
|
||||
| 待审批 | 消息有待审批请求 | 待处理 |
|
||||
| Task 等待处理 | Task 为 `waiting_approval`,尚无更具体的消息状态 | 待处理 |
|
||||
| 运行中 | 活动会话 ID、`streaming` 消息或运行中 Task | 运行中 |
|
||||
|
||||
同一会话只采用最高优先级状态;Task 的 `waiting_approval` 也可能对应提问,不能直接
|
||||
标成待审批。“待处理”和“运行中”数量互斥,全局总数为两者之和;项目计数使用相同结果。
|
||||
项目归属优先取会话元数据,缺少会话时取 Task 元数据;无项目归属的活动只计入全局汇总。
|
||||
|
||||
历史排队项、仅待调度的 Task、终态 Task 和未读标记本身不构成活动。活动结束且没有其他
|
||||
运行或等待条件时移除该行,不能因历史任务或未读状态继续计数。
|
||||
|
||||
级联菜单先按项目归属聚合活动行,沿用本地、SSH Host、通道的稳定项目顺序;缺少项目
|
||||
元数据的归属组及“未归属项目”组放在已知项目之后。无归属组只用于访问这些会话,不增加
|
||||
项目计数。每个项目内先列待处理会话,再列运行中会话,同类别保留来源顺序。
|
||||
|
||||
展开项目只改变菜单层级,不导航。选中项目的活动全部结束后返回项目列表;全局活动结束后
|
||||
保留已打开的空状态和“暂无活动”入口。后台更新保留仍存在的焦点项,删除焦点项时将焦点
|
||||
移至当前层可用项或空菜单。关闭菜单后,新的活动不会自动重新打开菜单。
|
||||
@@ -0,0 +1,133 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--surface-canvas: #e8f0f5; --surface-raised: #fff; --surface-subtle: #f4f8fb;
|
||||
--text-primary: #17263a; --text-secondary: #4f6278; --border-default: #cedce7;
|
||||
--accent: #086fc7; --accent-subtle: #e8f4ff; --accent-selected: #a7cfea;
|
||||
--font-family-ui: "Inter Variable", "PingFang SC", "Microsoft YaHei UI", "Noto Sans SC Variable", system-ui, sans-serif;
|
||||
--radius-control: 10px; --radius-card: 14px;
|
||||
--warning: #865200;
|
||||
}
|
||||
[data-theme="dark"] { --warning: #f2bd66; }
|
||||
.project-lab { max-width: 1440px; margin: 0 auto 12px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; color: var(--text-secondary); font-size: 12px; }
|
||||
.project-lab select { font: inherit; color: var(--text-primary); background: var(--surface-raised); padding: 4px 8px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }
|
||||
.project-entry { position: relative; margin-bottom: 12px; }
|
||||
#project-trigger { width: 100%; display: flex; align-items: center; gap: 8px; text-align: left; border: 1px solid var(--border-default); }
|
||||
#project-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.project-waiting { color: var(--warning); font-weight: 600; }
|
||||
#activity-trigger { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: -8px 0 12px; padding: 4px 8px; color: var(--text-secondary); font-size: 11px; text-align: left; }
|
||||
#project-activity { position: fixed; inset: auto; margin: 0; padding: 0; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); color: var(--text-primary); max-width: calc(100vw - 32px); }
|
||||
#project-activity:popover-open { display: flex; align-items: stretch; }
|
||||
#project-activity > section { width: 264px; max-width: calc(100vw - 34px); max-height: min(480px, calc(100dvh - 32px)); overflow: auto; padding: 8px; }
|
||||
#project-activity h3 { margin: 8px; font-size: 11px; font-weight: 500; color: var(--text-secondary); overflow-wrap: anywhere; }
|
||||
#activity-project-list > p { padding: 8px; }
|
||||
#activity-project-list button { display: grid; grid-template-columns: minmax(0, 1fr) auto; width: 100%; gap: 4px 8px; text-align: left; }
|
||||
#activity-project-list button > span:first-child { overflow-wrap: anywhere; }
|
||||
#activity-project-list small { grid-column: 1; font-size: 11px; color: var(--text-secondary); }
|
||||
#activity-project-list button > span:last-child { grid-column: 2; grid-row: 1 / 3; align-self: center; }
|
||||
#activity-project-list [aria-expanded="true"] { background: var(--accent-subtle); color: var(--accent); }
|
||||
#activity-sessions { border-left: 1px solid var(--border-default); }
|
||||
#activity-back { display: none; font-size: 12px; color: var(--accent); }
|
||||
#project-activity.activity-left { flex-direction: row-reverse; }
|
||||
.activity-left #activity-sessions { border-left: 0; border-right: 1px solid var(--border-default); }
|
||||
.activity-compact.has-submenu #activity-projects { display: none; }
|
||||
.activity-compact #activity-back { display: block; }
|
||||
.activity-compact #activity-sessions { border: 0; }
|
||||
#project-menu { position: fixed; inset: auto; margin: 0; width: min(300px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); overflow: auto; padding: 8px; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); color: var(--text-primary); }
|
||||
#project-list h3, #activity-list h3 { margin: 8px; font-size: 11px; font-weight: 500; color: var(--text-secondary); }
|
||||
#project-list button { width: 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; text-align: left; }
|
||||
.project-row-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
#project-list [aria-current="true"] { background: var(--accent-subtle); color: var(--accent); }
|
||||
#project-list small { width: 100%; display: flex; gap: 12px; font-size: 11px; color: var(--text-secondary); }
|
||||
#activity-list button { display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; text-align: left; padding: 12px; }
|
||||
#activity-list strong { font-weight: 500; overflow-wrap: anywhere; }
|
||||
#activity-list small { grid-column: 1; color: var(--text-secondary); overflow-wrap: anywhere; }
|
||||
.history button small { display: block; font-size: 11px; color: var(--text-secondary); }
|
||||
[data-theme="dark"] { color-scheme: dark; --surface-canvas: #07131f; --surface-raised: #102335; --surface-subtle: #162c40; --text-primary: #edf7ff; --text-secondary: #b5c6d5; --border-default: #29445a; --accent: #72c5ff; --accent-subtle: #123b5b; --accent-selected: #28668f; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; padding: 24px 32px; font: 13px/1.6 var(--font-family-ui); background: var(--surface-subtle); color: var(--text-primary); }
|
||||
button, input, textarea { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; border: 0; background: transparent; border-radius: var(--radius-control); padding: 8px 12px; }
|
||||
button:hover { background: var(--accent-subtle); color: var(--accent); }
|
||||
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||
button:disabled { opacity: .45; cursor: default; }
|
||||
svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; vertical-align: middle; flex-shrink: 0; }
|
||||
p, h1, h2 { margin: 0; } h1 { font-size: 30px; font-weight: 550; margin: 12px 0; letter-spacing: -.8px; } h2 { font-size: 24px; }
|
||||
.muted, .eyebrow { color: var(--text-secondary); }.eyebrow { font-size: 11px; letter-spacing: 1px; }
|
||||
.lab-header, .design-note, .lab-footer { display: flex; align-items: center; justify-content: space-between; gap: 24px; max-width: 1440px; margin: auto; }
|
||||
.lab-header strong { font-size: 16px; }.lab-header p { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
|
||||
.variants { display: grid; grid-template-columns: repeat(3, 1fr); padding: 4px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }
|
||||
.variants button[aria-pressed="true"] { background: var(--surface-raised); color: var(--accent); box-shadow: 0 1px 3px var(--border-default); }
|
||||
.design-note { margin: 24px auto 12px; font-size: 12px; }.design-note #variant-label { color: var(--accent); font-weight: 600; white-space: nowrap; }.design-note p { flex: 1; }.design-note > span:last-child { color: var(--text-secondary); font-size: 11px; }
|
||||
.app { display: flex; max-width: 1440px; height: min(780px, calc(100dvh - 204px)); min-height: 590px; margin: auto; border: 1px solid var(--border-default); border-radius: var(--radius-card); overflow: hidden; background: var(--surface-raised); }
|
||||
.sidebar { width: 278px; flex: 0 0 278px; background: var(--surface-canvas); display: flex; flex-direction: column; padding: 20px 12px 8px; border-right: 1px solid var(--border-default); }
|
||||
.brand { display: flex; gap: 8px; align-items: center; padding: 0 8px 24px; }.brand > .muted { margin-left: auto; font-size: 11px; }.brand-mark { display: grid; place-items: center; width: 32px; height: 32px; background: var(--text-primary); color: var(--surface-raised); border-radius: 10px; font-weight: 650; font-size: 12px; }
|
||||
.new-chat { border: 1px solid var(--border-default); background: var(--surface-raised); text-align: left; margin-bottom: 16px; }.new-chat span { margin-right: 8px; }
|
||||
.navigation { display: grid; gap: 3px; }.navigation button, .fixed-entry button { display: flex; align-items: center; gap: 10px; text-align: left; padding: 9px 10px; }.navigation .selected { background: var(--accent-subtle); color: var(--accent); }
|
||||
.history { flex: 1; min-height: 72px; overflow: auto; padding: 24px 0 8px; }.history .eyebrow { display: block; padding: 0 10px 8px; }.history button { display: block; width: 100%; text-align: left; color: var(--text-secondary); font-size: 12px; }
|
||||
.fixed-entry { border-top: 1px solid var(--border-default); padding: 8px 0; }.fixed-entry button { width: 100%; }
|
||||
.shortcuts { border-top: 1px solid var(--border-default); padding-top: 12px; max-height: 52%; overflow: auto; flex-shrink: 0; }.section-label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; padding-left: 8px; font-size: 11px; color: var(--text-secondary); }.section-label button { font-size: 11px; padding: 4px 8px; }
|
||||
.shortcut-list { display: grid; gap: 4px; }.shortcut-item { min-width: 0; }.shortcut-button { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; }.shortcut-button.active { color: var(--accent); background: var(--accent-subtle); }.shortcut-button .icon-tile { display: grid; place-items: center; }.shortcut-button .item-name { font-size: 11px; }
|
||||
[data-variant="dock"] .shortcut-list { grid-template-columns: repeat(5, minmax(0, 1fr)); background: var(--surface-raised); padding: 4px; border: 1px solid var(--border-default); border-radius: var(--radius-card); }
|
||||
[data-variant="dock"] .shortcut-button { padding: 10px 0; flex-direction: column; gap: 4px; }[data-variant="dock"] .shortcut-button .item-name { display: none; }[data-variant="dock"] .shortcut-list.editing { grid-template-columns: repeat(3, minmax(0, 1fr)); }[data-variant="dock"] .editing .item-name { display: block; }
|
||||
.store-link { margin-top: 8px; padding: 8px; display: flex; align-items: center; width: 100%; gap: 8px; text-align: left; font-size: 12px; }.store-link span:last-child { margin-left: auto; color: var(--text-secondary); }
|
||||
[data-variant="grid"] .shortcut-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }[data-variant="grid"] .shortcut-button { flex-direction: column; gap: 4px; padding: 8px 4px; }[data-variant="grid"] .icon-tile { width: 36px; height: 36px; border: 1px solid var(--border-default); border-radius: var(--radius-control); background: var(--surface-raised); }
|
||||
.featured { border: 1px solid var(--border-default); border-radius: var(--radius-control); padding: 12px; margin-top: 12px; background: var(--surface-raised); position: relative; }.featured p { font-size: 11px; color: var(--text-secondary); margin: 4px 0; }.featured .dismiss { position: absolute; right: 4px; top: 4px; padding: 0 8px; }.featured .feature-open { color: var(--accent); padding: 4px 0 0; font-size: 11px; }
|
||||
[data-variant="cards"] .shortcut-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }[data-variant="cards"] .shortcut-button { background: var(--surface-raised); border: 1px solid var(--border-default); padding: 12px 8px; flex-direction: column; align-items: flex-start; }[data-variant="cards"] .shortcut-button .item-name { font-size: 12px; }.card-detail { font-size: 11px; color: var(--text-secondary); text-align: left; }.mini-meter { display: flex; gap: 3px; align-items: end; height: 24px; }.mini-meter i { display: block; width: 5px; background: var(--accent-selected); border-radius: 2px; }.mini-meter i:nth-child(3n) { background: var(--accent); }
|
||||
.edit-actions { display: flex; justify-content: center; }.edit-actions button { padding: 2px 6px; font-size: 12px; }.sidebar-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-left: 8px; font-size: 11px; }
|
||||
.workspace { flex: 1; min-width: 0; display: flex; flex-direction: column; }.workspace-header { display: flex; justify-content: space-between; padding: 16px 24px; border-bottom: 1px solid var(--border-default); font-size: 12px; }.conversation { flex: 1; padding: 64px 40px 24px; max-width: 740px; width: 100%; margin: auto; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }.suggestions { display: flex; gap: 12px; margin-top: 32px; flex-wrap: wrap; }.suggestions button { border: 1px solid var(--border-default); font-size: 12px; }.suggestions span { margin-left: 24px; }
|
||||
.composer-area { margin: 16px 24px 8px; flex-shrink: 0; }
|
||||
.composer { padding: 12px; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-subtle); }
|
||||
.composer:focus-within { border-color: var(--accent-selected); }
|
||||
.composer textarea { display: block; width: 100%; height: 76px; min-height: 76px; max-height: 180px; padding: 4px; border: 0; resize: none; background: transparent; line-height: 1.8; }
|
||||
.composer textarea::placeholder { color: var(--text-secondary); font-size: 12px; }
|
||||
.composer-toolbar { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; margin-top: 8px; }
|
||||
.composer-toolbar button { padding: 6px 8px; font-size: 12px; white-space: nowrap; }
|
||||
.composer-toolbar .composer-icon { width: 32px; height: 32px; padding: 4px; font-size: 20px; }
|
||||
.composer-select select { max-width: 132px; padding: 6px 2px; border: 0; background: transparent; cursor: pointer; font: inherit; font-size: 12px; color: var(--text-primary); }
|
||||
select option { background: var(--surface-raised); color: var(--text-primary); }
|
||||
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.composer-actions { display: flex; gap: 4px; margin-left: auto; }
|
||||
#send { width: 32px; height: 32px; padding: 0; font-size: 20px; background: var(--accent); color: var(--surface-raised); }
|
||||
#voice[aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent); }
|
||||
.composer-meta { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-height: 32px; color: var(--text-secondary); font-size: 10px; }
|
||||
.composer-meta button { font-size: 10px; padding: 4px; }
|
||||
#composer-tags { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 4px 8px; max-height: 96px; overflow: auto; }
|
||||
#composer-tags button { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 3px 8px; font-size: 11px; background: var(--accent-subtle); color: var(--accent); }
|
||||
#composer-options, #context-details { position: fixed; inset: auto; margin: 0; width: min(320px, calc(100vw - 32px)); max-height: min(460px, 70dvh); overflow: auto; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); box-shadow: 0 8px 32px #07131f26; font-size: 12px; }
|
||||
#composer-options header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.option-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 10px 0; }
|
||||
.option-row select { width: 166px; padding: 6px; border: 1px solid var(--border-default); border-radius: 6px; font: inherit; background: var(--surface-subtle); color: var(--text-primary); }
|
||||
#composer-options fieldset { margin: 8px 0; padding: 12px 0; border: 0; border-top: 1px solid var(--border-default); }
|
||||
#composer-options fieldset label { display: inline-flex; align-items: center; gap: 4px; margin-right: 12px; }
|
||||
#composer-options input { accent-color: var(--accent); }
|
||||
#composer-options p { margin-top: 12px; font-size: 11px; }
|
||||
#context-details p { margin: 8px 0; color: var(--text-secondary); }
|
||||
#context-details progress { width: 100%; accent-color: var(--accent); }
|
||||
#compact { margin-top: 8px; color: var(--accent); background: var(--accent-subtle); }
|
||||
#demo-queue { max-height: 96px; overflow: auto; margin-bottom: 8px; }
|
||||
.queue-row { display: flex; align-items: center; gap: 8px; font-size: 11px; }
|
||||
.queue-row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.queue-row button { flex-shrink: 0; padding: 4px; font-size: 11px; }
|
||||
.workbar { width: 280px; flex-shrink: 0; border-left: 1px solid var(--border-default); background: var(--surface-subtle); }.workbar header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border-default); }#tool-content { padding: 24px 16px; }.metric { margin: 20px 0; }.metric strong { float: right; }.metric progress { width: 100%; accent-color: var(--accent); }.file-row { padding: 12px 0; border-bottom: 1px solid var(--border-default); }pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }
|
||||
.lab-footer { margin-top: 12px; font-size: 12px; color: var(--text-secondary); }
|
||||
dialog { width: min(580px, calc(100% - 32px)); max-height: 85dvh; border: 1px solid var(--border-default); padding: 24px; border-radius: var(--radius-card); background: var(--surface-raised); color: var(--text-primary); }dialog::backdrop { background: #07131f80; }dialog header { display: flex; justify-content: space-between; align-items: start; }dialog > p { margin: 12px 0 16px; }dialog input { width: 100%; padding: 10px 12px; background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-control); }.catalog-row { display: flex; align-items: center; gap: 12px; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.catalog-row > div { flex: 1; }.catalog-row p { font-size: 11px; color: var(--text-secondary); }.catalog-row button { color: var(--accent); background: var(--accent-subtle); font-size: 12px; }dialog footer { font-size: 11px; color: var(--text-secondary); margin-top: 16px; }
|
||||
#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); padding: 10px 20px; border-radius: var(--radius-control); background: var(--text-primary); color: var(--surface-raised); pointer-events: none; opacity: 0; }#toast.visible { opacity: 1; }
|
||||
[hidden] { display: none !important; }
|
||||
#page-actions { margin-left: auto; }#page-actions button { padding: 4px 8px; font-size: 12px; }.note-count { margin-left: auto; padding: 0 6px; background: var(--accent-subtle); border-radius: var(--radius-control); color: var(--accent); }.note-setting-row { display: flex; gap: 16px; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.note-setting-row p, .note-setting-options p { font-size: 11px; color: var(--text-secondary); margin-top: 6px; }.demo-switch { width: 36px; height: 20px; border-radius: 20px; border: 1px solid var(--text-secondary); background: var(--surface-canvas); padding: 2px; flex-shrink: 0; }.demo-switch::after { content: ''; display: block; width: 14px; height: 14px; border-radius: 50%; background: var(--text-secondary); }.demo-switch[aria-checked="true"] { background: var(--accent); border-color: var(--accent); }.demo-switch[aria-checked="true"]::after { margin-left: 16px; background: var(--surface-raised); }.note-setting-options { border: 0; padding: 16px 0; margin: 0; min-width: 0; }.note-setting-options legend { padding-top: 16px; font-weight: 550; }.note-segments { display: flex; padding: 4px; gap: 4px; background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-control); }.note-segments button { flex: 1; padding: 8px 4px; font-size: 12px; }.note-segments [aria-pressed="true"] { color: var(--accent); background: var(--surface-raised); box-shadow: 0 1px 3px var(--border-default); }.note-settings-preview { padding: 16px; background: var(--accent-subtle); border-radius: var(--radius-control); }.note-settings-preview small { color: var(--text-secondary); font-size: 11px; }#back-to-apps { margin-bottom: 12px; padding-left: 0; color: var(--accent); }#note-settings-summary { margin: 16px 0; }
|
||||
.sidebar-footer { border-top: 1px solid var(--border-default); padding: 8px 0 0; gap: 4px; }#center-entry { flex: 1; min-width: 0; }#open-center { width: 100%; display: flex; align-items: center; gap: 10px; padding: 8px; text-align: left; }#open-center strong { font-size: 13px; font-weight: 550; }#open-center small { display: block; font-size: 11px; color: var(--text-secondary); }.entry-arrow { margin-left: auto; }.tool-tabs { display: flex; padding: 8px; gap: 2px; }.tool-tabs button { flex: 1; padding: 8px 4px; font-size: 12px; }.tool-tabs [aria-selected="true"] { background: var(--accent-subtle); color: var(--accent); }.center-switch { display: flex; gap: 4px; padding-bottom: 12px; }.center-switch [aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent); }.center-app { display: flex; gap: 12px; align-items: start; padding: 16px 0; border-bottom: 1px solid var(--border-default); }.center-app > svg { margin-top: 4px; color: var(--accent); }.center-app-info { flex: 1; }.center-app p { color: var(--text-secondary); font-size: 11px; }.center-actions { display: flex; gap: 4px; flex-wrap: wrap; }.center-actions button { font-size: 11px; background: var(--accent-subtle); color: var(--accent); }.center-actions [aria-pressed="true"] { border: 1px solid var(--accent-selected); }
|
||||
dialog[data-variant="dock"], dialog[data-variant="grid"], dialog[data-variant="favorites"] { position: fixed; left: 48px; bottom: 100px; top: auto; right: auto; margin: 0; width: min(520px, calc(100% - 32px)); max-height: 72dvh; }
|
||||
dialog[data-variant="grid"] #catalog-items { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }dialog[data-variant="grid"] .center-app { flex-direction: column; padding: 16px; border: 1px solid var(--border-default); border-radius: var(--radius-control); }dialog[data-variant="cards"] .center-app { padding: 24px 0; }dialog[data-variant="cards"] .center-app > svg { width: 32px; height: 32px; }dialog[data-variant="cards"] .app-detail { margin-top: 8px; font-size: 13px; }dialog[data-variant="favorites"] .app-detail { display: none; }dialog[data-variant="scenes"] .center-app { background: var(--surface-subtle); border: 1px solid var(--border-default); border-radius: var(--radius-card); padding: 20px; margin-top: 16px; flex-wrap: wrap; }dialog[data-variant="scenes"] .center-actions { width: 100%; justify-content: flex-end; }
|
||||
@media (max-width: 640px) { dialog[data-variant] { left: 16px; right: 16px; bottom: 16px; top: auto; margin: 0; max-height: 85dvh; width: calc(100% - 32px); padding: 16px; }.center-app { flex-wrap: wrap; }.center-actions { width: 100%; }.app { flex-direction: column; }.app .sidebar { flex: 1; min-height: 0; }.app .workspace { display: block; flex: 0 0 auto; }.app .workspace .conversation, .app .workspace .composer { display: none; }.app .workspace-header { padding: 8px 16px; align-items: center; }.app .workbar { z-index: 2; } }
|
||||
.reference-note { max-width: 1440px; margin: 12px auto 0; display: flex; flex-wrap: wrap; gap: 8px 16px; color: var(--text-secondary); font-size: 11px; }.reference-note a { color: var(--accent); text-underline-offset: 3px; }
|
||||
.sidebar > * { flex-shrink: 0; }.sidebar .history { flex: 1 1 80px; min-height: 0; }.sidebar .shortcuts { flex-shrink: 1; min-height: 80px; }
|
||||
.sidebar .resident-apps { margin-top: 12px; min-height: 96px; flex-shrink: 1; display: flex; flex-direction: column; max-height: 240px; }.resident-apps .section-label { margin-bottom: 4px; flex-shrink: 0; }#resident-list { overflow: auto; min-height: 0; }.resident-row { display: flex; align-items: center; }.resident-row > button { display: flex; align-items: center; gap: 10px; padding: 8px 10px; flex: 1; min-width: 0; text-align: left; }.resident-row > button span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.resident-row [aria-current="page"] { background: var(--accent-subtle); color: var(--accent); }.resident-row[draggable="true"] { cursor: grab; }.resident-row .edit-actions { flex-shrink: 0; }.all-apps { display: flex; justify-content: space-between; padding: 6px 10px; font-size: 11px; color: var(--text-secondary); flex-shrink: 0; }.empty-apps { padding: 8px 10px; font-size: 11px; color: var(--text-secondary); }.app-placement { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }.app-placement button { font-size: 11px; padding: 6px 8px; }.app-placement button[aria-pressed="true"] { border: 1px solid var(--accent-selected); }.app-catalog-row { align-items: flex-start; }.app-catalog-row > svg { margin-top: 4px; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
||||
.launcher-trigger { display: flex; align-items: center; gap: 8px; width: 100%; padding: 12px 8px; border: 1px solid var(--border-default); background: var(--surface-raised); margin-bottom: 12px; font-size: 12px; }.launcher-trigger kbd { margin-left: auto; font: 10px var(--font-family-ui); color: var(--text-secondary); white-space: nowrap; }
|
||||
[data-variant="launcher"] .shortcut-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }[data-variant="launcher"] .shortcut-button { flex-direction: column; padding: 8px 0; }[data-variant="launcher"] .icon-tile { height: 24px; }[data-variant="launcher"] .store-link { border-top: 1px solid var(--border-default); border-radius: 0; }
|
||||
[data-variant="favorites"] .section-label { padding-left: 0; }[data-variant="favorites"] .shortcut-list { gap: 0; }[data-variant="favorites"] .shortcut-button { justify-content: flex-start; padding: 8px 12px; }[data-variant="favorites"] .item-name { font-size: 12px; }[data-variant="favorites"] .shortcut-item { display: flex; align-items: center; }[data-variant="favorites"] .edit-actions { margin-left: auto; }[data-variant="favorites"] .store-link { margin-top: 12px; border: 1px dashed var(--border-default); }.count { padding: 0 6px; color: var(--text-secondary); }
|
||||
.scene-banner { display: flex; align-items: center; gap: 12px; margin: 0 4px 12px; padding: 12px; background: var(--accent-subtle); border-radius: var(--radius-control); }.scene-banner > svg { color: var(--accent); width: 24px; height: 24px; }.scene-banner .eyebrow { letter-spacing: 0; font-size: 10px; }.scene-count { margin-left: auto; font-size: 11px; color: var(--text-secondary); }.scene-label { display: block; }.scene-label select { font: inherit; font-weight: 600; color: var(--text-primary); background: transparent; border: 0; padding: 0; cursor: pointer; }.scene-label option { background: var(--surface-raised); }
|
||||
[data-variant="scenes"] .shortcut-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }[data-variant="scenes"] .shortcut-button { border: 1px solid var(--border-default); background: var(--surface-raised); padding: 10px 8px; justify-content: flex-start; }.pack-preview { display: flex; gap: 8px; align-items: center; width: 100%; margin-top: 12px; padding: 12px 8px; text-align: left; background: var(--surface-raised); border: 1px solid var(--border-default); }.pack-preview strong { font-size: 11px; font-weight: 550; }.pack-preview small { display: block; color: var(--text-secondary); font-size: 10px; }.pack-preview > span:last-child { margin-left: auto; }.pack-symbol { color: var(--accent); }
|
||||
.catalog-pack { padding: 20px 0; border-bottom: 1px solid var(--border-default); }.catalog-pack h3 { margin: 8px 0; font-size: 18px; font-weight: 550; }.pack-art { display: grid; place-items: center; float: right; width: 56px; height: 56px; border: 1px solid var(--accent-selected); background: var(--accent-subtle); color: var(--accent); border-radius: var(--radius-card); }.pack-art svg { width: 28px; height: 28px; }.pack-includes { display: flex; flex-wrap: wrap; gap: 8px; margin: 16px 0; }.pack-includes span { padding: 4px 8px; font-size: 11px; background: var(--surface-subtle); border-radius: var(--radius-control); }.pack-includes svg { width: 14px; height: 14px; }.catalog-pack > button { border: 1px solid var(--accent-selected); color: var(--accent); background: var(--accent-subtle); }
|
||||
dialog[data-mode="launch"] { width: min(640px, calc(100% - 32px)); }dialog[data-mode="launch"] .catalog-row:focus-within { background: var(--accent-subtle); }.catalog-row { padding-left: 8px; padding-right: 8px; }
|
||||
@media (max-width: 1080px) { body { padding: 16px; }.app { position: relative; }.workbar { position: absolute; right: 0; top: 52px; bottom: 0; width: min(320px, 100%); box-shadow: -8px 0 24px var(--border-default); }.lab-header { flex-wrap: wrap; gap: 12px; }.design-note > span:last-child { display: none; }.conversation { padding: 32px 24px; } }
|
||||
@media (max-width: 640px) { body { padding: 12px; }.lab-header > div:first-child { flex: 1; }.variants { order: 3; width: 100%; }.variants button { flex: 1; padding: 8px 4px; font-size: 12px; }.design-note { align-items: flex-start; gap: 12px; }.app { height: auto; min-height: 0; }.app .sidebar { width: 100%; flex: 0 0 auto; border: 0; padding-top: 12px; }.brand { padding-bottom: 12px; }.app .history { display: none; }.app .workspace { display: flex; min-height: 320px; border-top: 1px solid var(--border-default); }.app .workspace .composer { display: block; }.composer-area { margin: 12px 12px 4px; margin-top: auto; }.composer-toolbar { gap: 2px; }.composer-select select { max-width: 108px; font-size: 11px; }.composer-toolbar button { font-size: 11px; padding: 6px; }.composer-toolbar .composer-icon { width: 28px; }.composer-meta { flex-wrap: wrap; gap: 0; }.shortcut-list { max-height: 300px; overflow: auto; }.lab-footer { align-items: flex-start; }.lab-footer button { white-space: nowrap; }.workbar { top: 0; width: 100%; }.design-note p { font-size: 11px; } }
|
||||
@@ -0,0 +1,83 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>GoodBuddy · 应用中心设计实验</title>
|
||||
<link rel="stylesheet" href="navigation-demo.css">
|
||||
<script src="navigation-demo.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="lab-header">
|
||||
<div><strong>GoodBuddy <span class="muted">/ 设计实验</span></strong><p>左下角应用中心 · 六个交互方案</p></div>
|
||||
<div class="variants" role="group" aria-label="选择设计方案">
|
||||
<button data-variant="dock" aria-pressed="true">A · 紧凑入口</button>
|
||||
<button data-variant="grid" aria-pressed="false">B · 应用宫格</button>
|
||||
<button data-variant="cards" aria-pressed="false">C · 应用预览</button>
|
||||
<button data-variant="launcher" aria-pressed="false">D · 搜索启动台</button>
|
||||
<button data-variant="favorites" aria-pressed="false">E · 常驻管理</button>
|
||||
<button data-variant="scenes" aria-pressed="false">F · 商店发现</button>
|
||||
</div>
|
||||
<button id="theme">切换深色</button>
|
||||
</header>
|
||||
<div class="design-note"><span id="variant-label">A / 紧凑 Dock</span><p id="variant-description"></p><span>交互原型 · 商店与指标均为演示数据</span></div>
|
||||
<div class="project-lab"><label>项目活动演示 <select id="project-scenario"><option value="waiting">有待处理</option><option value="running">仅运行中</option><option value="current">仅当前项目运行</option><option value="idle">全部空闲</option></select></label><span>有活动时显示摘要,悬停或点击项目向右展开会话。</span></div>
|
||||
<main class="app" data-variant="dock">
|
||||
<aside class="sidebar" aria-label="主侧栏">
|
||||
<div class="brand"><span class="brand-mark">GB</span><strong>GoodBuddy</strong><span class="muted">个人空间</span></div>
|
||||
<div class="project-entry"><button id="project-trigger" popovertarget="project-menu" aria-expanded="false" aria-controls="project-menu"><span id="project-name">GoodBuddy</span><span class="muted" aria-hidden="true">⌄</span></button></div>
|
||||
<button id="activity-trigger" popovertarget="project-activity" aria-expanded="false" aria-controls="project-activity"><span id="activity-summary"></span><span aria-hidden="true">⌄</span></button>
|
||||
<div id="project-menu" popover aria-label="切换项目"><div id="project-list"></div></div>
|
||||
<div id="project-activity" popover aria-label="所有项目活动"><section id="activity-projects"><h3>所有项目活动</h3><div id="activity-project-list"></div></section><section id="activity-sessions" hidden><button id="activity-back">‹ 返回项目列表</button><h3 id="activity-project-title"></h3><div id="activity-list"></div></section></div>
|
||||
<button class="new-chat" data-page="当前对话"><span>+</span> 新对话</button>
|
||||
<nav class="navigation" aria-label="应用导航">
|
||||
<button class="selected" data-page="当前对话"><span data-icon="chat"></span>当前对话</button>
|
||||
</nav>
|
||||
<section class="resident-apps" aria-label="我的应用"><div class="section-label"><span>我的应用</span><div><button id="edit-apps">编辑</button><button data-catalog="apps" aria-label="管理常驻应用">+</button></div></div><nav id="resident-list" aria-label="常驻应用"></nav><button class="all-apps" data-catalog="apps">全部应用 <span>›</span></button></section>
|
||||
<div class="history"><span class="eyebrow">最近对话</span><button data-page="整理产品设计思路">整理产品设计思路</button><button data-page="本周工作计划">本周工作计划</button><button data-page="项目文档检索">项目文档检索</button></div>
|
||||
<div class="fixed-entry"><button data-page="运行记录"><span data-icon="history"></span>运行记录</button></div>
|
||||
<div class="sidebar-footer"><div id="center-entry"></div><button data-page="设置" aria-label="设置" title="设置"><span data-icon="settings"></span></button></div>
|
||||
</aside>
|
||||
<section class="workspace">
|
||||
<header class="workspace-header"><span id="page-label">当前对话</span><div id="page-actions"></div><button id="toggle-workbar" aria-expanded="true" aria-controls="workbar">助手工作栏</button></header>
|
||||
<div class="conversation" id="page-content">
|
||||
<span class="eyebrow">LET'S MAKE SOMETHING</span>
|
||||
<h1>从一个想法开始。</h1>
|
||||
<p class="muted">对话留在这里,常用能力随手打开。</p>
|
||||
<div class="suggestions"><button data-page="项目文档检索">查找项目资料 <span>↗</span></button><button data-page="整理产品设计思路">梳理一个想法 <span>↗</span></button></div>
|
||||
</div>
|
||||
<section class="composer-area" aria-label="会话输入">
|
||||
<div id="demo-queue" aria-label="待发送消息" hidden></div>
|
||||
<div class="composer">
|
||||
<div id="composer-tags" aria-label="当前附件与选项" hidden></div>
|
||||
<textarea id="message" aria-label="演示消息输入" placeholder="向 GoodBuddy 提问… Enter 发送 · Shift+Enter 换行"></textarea>
|
||||
<div class="composer-toolbar">
|
||||
<button id="attach" class="composer-icon" aria-label="添加附件" title="添加附件">+</button>
|
||||
<input id="attachment-input" type="file" multiple hidden>
|
||||
<button id="voice" class="composer-icon" aria-label="模拟语音输入" aria-pressed="false" title="模拟语音输入"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 10v2a7 7 0 0 0 14 0v-2M12 19v3m-4 0h8"/></svg></button>
|
||||
<label class="composer-select"><span class="sr-only">Runtime</span><select id="composer-runtime"><option>OpenCode</option><option>Continue</option><option>直连模型</option></select></label>
|
||||
<label class="composer-select"><span class="sr-only">工作模式</span><select id="composer-mode"><option value="Execute">Execute</option><option value="Ask">Ask</option></select></label>
|
||||
<button id="composer-options-trigger" popovertarget="composer-options" aria-expanded="false">选项 <span class="muted">⌃</span></button>
|
||||
<div class="composer-actions"><button id="stop" hidden aria-label="停止演示回复">停止</button><button id="send" aria-label="发送演示消息" title="发送" disabled>↑</button></div>
|
||||
</div>
|
||||
<div id="composer-options" popover aria-label="会话选项">
|
||||
<header><strong>会话选项</strong><button popovertarget="composer-options" popovertargetaction="hide" aria-label="关闭会话选项">×</button></header>
|
||||
<label class="option-row">专家<select id="composer-expert"><option value="">默认助手</option><option>产品顾问</option><option>代码审查专家</option></select></label>
|
||||
<fieldset><legend>知识范围</legend><label><input type="checkbox" data-knowledge="产品文档"> 产品文档</label><label><input type="checkbox" data-knowledge="团队知识"> 团队知识</label></fieldset>
|
||||
<label class="option-row">检索方式<select id="composer-retrieval"><option value="auto">自动检索</option><option value="always">每次检索</option></select></label>
|
||||
<div id="runtime-options"><label class="option-row"><span id="agent-label">Agent</span><select id="composer-agent"><option value="">默认</option><option value="plan">Plan</option><option value="review">Review</option></select></label><label class="option-row">快捷操作<select id="composer-command"><option value="">无</option><option value="prompt">填入总结模板</option><option value="command">原生命令 /init</option></select></label></div>
|
||||
<p class="muted">非默认选项会显示在输入框上方。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="composer-meta"><span id="composer-status" role="status">交互演示 · 不调用 AI</span><button id="context-trigger" popovertarget="context-details">上下文 <span id="context-value">42%</span></button></div>
|
||||
<div id="context-details" popover aria-label="上下文用量"><strong>上下文用量 · 示例</strong><p id="context-description">本次模型调用:84k / 200k tokens</p><progress id="context-progress" value="42" max="100"></progress><button id="compact">压缩上下文</button></div>
|
||||
</section>
|
||||
</section>
|
||||
<aside class="workbar" id="workbar" aria-label="助手工作栏"><header><strong>助手工作栏</strong><button id="close-tool" aria-label="关闭工作栏">×</button></header><div class="tool-tabs" role="tablist" aria-label="系统工具"><button role="tab" id="tab-files" data-tool="files" aria-selected="true" aria-controls="tool-content">文件</button><button role="tab" id="tab-terminal" data-tool="terminal" aria-selected="false" aria-controls="tool-content" tabindex="-1">终端</button><button role="tab" id="tab-browser" data-tool="browser" aria-selected="false" aria-controls="tool-content" tabindex="-1">浏览器</button><button role="tab" id="tab-resources" data-tool="resources" aria-selected="false" aria-controls="tool-content" tabindex="-1">资源</button></div><div id="tool-content" role="tabpanel" aria-labelledby="tab-files" tabindex="0"></div></aside>
|
||||
</main>
|
||||
<footer class="lab-footer"><span id="design-tradeoff"></span><button id="reset">恢复默认应用</button></footer>
|
||||
<div class="reference-note">设计参考:<a href="https://www.raycast.com/" target="_blank" rel="noreferrer">Raycast · 搜索与扩展</a><a href="https://www.notion.com/help/intro-to-workspaces" target="_blank" rel="noreferrer">Notion · 收藏与折叠分区</a><a href="https://code.visualstudio.com/docs/configure/profiles" target="_blank" rel="noreferrer">VS Code · Profiles</a><span>借鉴交互思路,保留 GoodBuddy 的视觉与入口分类。</span></div>
|
||||
<dialog id="catalog" aria-labelledby="catalog-title"><button id="back-to-apps" hidden>‹ 返回应用中心</button><header><div><span class="eyebrow">GOODBUDDY COLLECTION</span><h2 id="catalog-title">应用中心</h2></div><button id="close-catalog" aria-label="关闭">×</button></header><p class="muted" id="catalog-description"></p><div class="center-switch" role="group" aria-label="应用中心视图"><button data-catalog="apps">我的应用</button><button data-catalog="store">商店</button></div><input id="search" type="search" aria-label="搜索应用" placeholder="搜索应用…"><div id="catalog-items"></div><section id="app-settings" aria-label="魔法笔记设置" hidden></section><footer>常驻只表示菜单入口,不表示后台运行。安装与内容均为演示。</footer></dialog>
|
||||
<div id="toast" role="status" aria-live="polite"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,407 @@
|
||||
/* global document, window, history, location, matchMedia, innerWidth, Event, setTimeout, clearTimeout */
|
||||
const paths = {
|
||||
note: '<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/>',
|
||||
book: '<path d="M12 5v16M12 5C8 2 4 3 2 4v15c4-1 7-1 10 2 3-3 6-3 10-2V4c-2-1-6-2-10 1Z"/>',
|
||||
chat: '<path d="M21 11a9 9 0 0 1-9 9H3l1-5A9 9 0 1 1 21 11Z"/>',
|
||||
history: '<path d="M3 4v5h5M3 9a9 9 0 1 1 0 6m9-8v5l3 2"/>',
|
||||
settings: '<circle cx="12" cy="12" r="4"/><path d="M12 2v3m0 14v3M2 12h3m14 0h3M5 5l2 2m10 10 2 2M5 19l2-2M17 7l2-2"/>',
|
||||
activity: '<path d="M2 12h5l3-8 4 16 3-8h5"/>',
|
||||
layers: '<path d="m12 3 10 5-10 5L2 8Zm-10 9 10 5 10-5M2 17l10 5 10-5"/>',
|
||||
search: '<circle cx="10" cy="10" r="6"/><path d="m15 15 6 6"/>'
|
||||
};
|
||||
const icon = name => `<svg viewBox="0 0 24 24" aria-hidden="true">${paths[name] || paths.layers}</svg>`;
|
||||
const items = [
|
||||
{ id: 'notes', name: '魔法笔记', icon: 'note', detail: '记录想法、整理待办', summary: '把刚才的灵感留在这里', builtin: true },
|
||||
{ id: 'knowledge', name: '知识库', icon: 'book', detail: '整理资料、检索知识', summary: '下一次提问,从已有知识开始', builtin: true },
|
||||
{ id: 'heartbeat', name: '智能心跳', icon: 'activity', detail: '管理定时任务与主动提醒', summary: '让例行任务按计划发生', builtin: true },
|
||||
{ id: 'projects', name: '项目管理', icon: 'layers', detail: '跟踪项目、任务与进度', summary: '把一个想法变成可跟进的项目', builtin: false },
|
||||
{ id: 'reading', name: '阅读空间', icon: 'book', detail: '集中阅读、批注与摘录', summary: '给长文和深度阅读留一个空间', builtin: false }
|
||||
];
|
||||
const variants = {
|
||||
dock: ['A / 紧凑入口', '最底部保留一个应用中心入口,向上展开紧凑列表。', '适合希望把侧栏空间留给对话的用户。'],
|
||||
grid: ['B / 应用宫格', '底部入口展示应用数量,展开后用宫格浏览已安装应用。', '图标与名称同时可见,常驻位置由用户选择。'],
|
||||
cards: ['C / 应用预览', '入口展示内置应用图标,展开后用卡片说明每个应用的用途。', '适合通过内容理解应用,卡片不代表后台持续运行。'],
|
||||
launcher: ['D / 搜索启动台', '最底部就是搜索入口,也可以按 Ctrl / ⌘ K 打开。', '输入名称,用方向键与 Enter 打开已安装应用。'],
|
||||
favorites: ['E / 常驻管理', '从底部管理哪些应用常驻菜单,未常驻应用也可直接打开。', '常驻应用优先排列;入口显隐不影响安装和运行状态。'],
|
||||
scenes: ['F / 商店发现', '底部入口强调应用发现,展开后查看商店应用与安装位置。', '先模拟安装,再决定是否常驻;我的应用始终可切换访问。']
|
||||
};
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const demoProjects = [
|
||||
{ id: 'goodbuddy', name: 'GoodBuddy', group: '本地', conversations: ['优化项目导航', '检查构建问题'] },
|
||||
{ id: 'website', name: '官网改版', group: '本地', conversations: ['发布首页'] },
|
||||
{ id: 'remote', name: '客户方案与交付文档工作空间', group: 'SSH', conversations: ['整理报价'] },
|
||||
{ id: 'channel', name: '团队助手', group: '通道', conversations: ['本周工作计划'] }
|
||||
];
|
||||
let currentProject = 'goodbuddy';
|
||||
let projectActivity = [];
|
||||
let expandedActivityProject = '';
|
||||
function renderProjects() {
|
||||
const scenario = $('#project-scenario').value;
|
||||
const activity = demoProjects.flatMap(project => project.conversations.map((name, index) => ({ project, name, index,
|
||||
state: scenario === 'idle' || project.id === 'channel' || (scenario === 'current' && project.id !== currentProject) ? '' : scenario === 'waiting' && project.id === 'website' ? '等待审批' : '运行中'
|
||||
}))).filter(row => row.state);
|
||||
projectActivity = activity;
|
||||
const waiting = activity.filter(row => row.state !== '运行中').length;
|
||||
const running = activity.length - waiting;
|
||||
const project = demoProjects.find(entry => entry.id === currentProject);
|
||||
$('#project-name').textContent = project.name;
|
||||
$('#project-trigger').setAttribute('aria-label', `切换项目,当前项目:${project.name}`);
|
||||
$('#activity-trigger').hidden = !activity.length;
|
||||
if (!activity.length && $('#project-activity').matches(':popover-open')) $('#project-activity').hidePopover();
|
||||
$('#activity-summary').innerHTML = `${running ? `运行中 ${running}` : ''}${waiting && running ? ' · ' : ''}${waiting ? `<span class="project-waiting">待处理 ${waiting}</span>` : ''}`;
|
||||
$('#activity-trigger').setAttribute('aria-label', `所有项目活动:${$('#activity-summary').textContent}`);
|
||||
$('#project-list').innerHTML = ['本地', 'SSH', '通道'].map(group => `<section><h3>${group}</h3>${demoProjects.filter(entry => entry.group === group).map(entry => {
|
||||
const rows = activity.filter(row => row.project.id === entry.id);
|
||||
const pending = rows.filter(row => row.state !== '运行中').length;
|
||||
const active = rows.length - pending;
|
||||
return `<button data-project="${entry.id}" ${entry.id === currentProject ? 'aria-current="true"' : ''}><span class="project-row-name">${entry.name}</span>${entry.id === currentProject ? '<span aria-hidden="true">✓</span>' : ''}${rows.length ? `<small>${pending ? `<span class="project-waiting">待处理 ${pending}</span>` : ''}${active ? `<span>运行中 ${active}</span>` : ''}</small>` : ''}</button>`;
|
||||
}).join('')}</section>`).join('');
|
||||
$('#activity-project-list').innerHTML = demoProjects.map(entry => {
|
||||
const rows = activity.filter(row => row.project.id === entry.id);
|
||||
if (!rows.length) return '';
|
||||
const pending = rows.filter(row => row.state !== '运行中').length;
|
||||
return `<button data-activity-project="${entry.id}" aria-expanded="false" aria-controls="activity-sessions"><span>${entry.name}</span><small>${pending ? `<span class="project-waiting">待处理 ${pending}</span>` : `运行中 ${rows.length}`}</small><span aria-hidden="true">›</span></button>`;
|
||||
}).join('') || '<p class="muted">暂无运行中或待处理会话。</p>';
|
||||
expandActivityProject('');
|
||||
$('.history').innerHTML = `<span class="eyebrow">当前项目对话</span>${project.conversations.map((name, index) => {
|
||||
const row = activity.find(entry => entry.project.id === currentProject && entry.index === index);
|
||||
return `<button data-conversation="${currentProject}" data-conversation-index="${index}">${name}${row ? `<small>${row.state}</small>` : ''}</button>`;
|
||||
}).join('')}`;
|
||||
}
|
||||
$('#project-scenario').onchange = renderProjects;
|
||||
const projectMenu = $('#project-menu');
|
||||
function positionProjectMenu() {
|
||||
const rect = $('#project-trigger').getBoundingClientRect();
|
||||
projectMenu.style.left = `${Math.max(16, Math.min(rect.left, innerWidth - projectMenu.offsetWidth - 16))}px`;
|
||||
projectMenu.style.top = `${Math.max(16, Math.min(rect.bottom + 8, window.innerHeight - projectMenu.offsetHeight - 16))}px`;
|
||||
}
|
||||
projectMenu.addEventListener('toggle', event => {
|
||||
$('#project-trigger').setAttribute('aria-expanded', String(event.newState === 'open'));
|
||||
if (event.newState === 'open') positionProjectMenu();
|
||||
});
|
||||
window.addEventListener('resize', () => { if (projectMenu.matches(':popover-open')) positionProjectMenu(); });
|
||||
window.addEventListener('scroll', () => { if (projectMenu.matches(':popover-open')) positionProjectMenu(); }, true);
|
||||
function positionActivityMenu() {
|
||||
const panel = $('#project-activity');
|
||||
const rect = $('#activity-trigger').getBoundingClientRect();
|
||||
panel.classList.toggle('activity-compact', innerWidth < 620);
|
||||
panel.classList.toggle('activity-left', innerWidth >= 620 && rect.left + panel.offsetWidth > innerWidth - 16 && rect.right >= panel.offsetWidth + 16);
|
||||
const left = panel.classList.contains('activity-left') ? rect.right - panel.offsetWidth : rect.left;
|
||||
panel.style.left = `${Math.max(16, Math.min(left, innerWidth - panel.offsetWidth - 16))}px`;
|
||||
panel.style.top = `${Math.max(16, Math.min(rect.bottom + 4, window.innerHeight - panel.offsetHeight - 16))}px`;
|
||||
}
|
||||
function expandActivityProject(id, focus = false) {
|
||||
expandedActivityProject = id;
|
||||
$('#activity-sessions').hidden = !id;
|
||||
$('#project-activity').classList.toggle('has-submenu', Boolean(id));
|
||||
document.querySelectorAll('[data-activity-project]').forEach(button => button.setAttribute('aria-expanded', String(button.dataset.activityProject === id)));
|
||||
if (id) {
|
||||
$('#activity-project-title').textContent = demoProjects.find(project => project.id === id).name;
|
||||
$('#activity-list').innerHTML = projectActivity.filter(row => row.project.id === id).sort((a, b) => Number(a.state === '运行中') - Number(b.state === '运行中')).map(row => `<button data-conversation="${id}" data-conversation-index="${row.index}"><strong>${row.name}</strong><small>${row.state}</small></button>`).join('');
|
||||
}
|
||||
if ($('#project-activity').matches(':popover-open')) positionActivityMenu();
|
||||
if (focus) $('#activity-list button')?.focus();
|
||||
}
|
||||
$('#project-activity').addEventListener('toggle', event => {
|
||||
$('#activity-trigger').setAttribute('aria-expanded', String(event.newState === 'open'));
|
||||
if (event.newState === 'open') { expandActivityProject(''); positionActivityMenu(); }
|
||||
});
|
||||
$('#activity-project-list').addEventListener('pointerover', event => {
|
||||
const button = event.target.closest('[data-activity-project]');
|
||||
if (event.pointerType === 'mouse' && innerWidth >= 620 && button && button.dataset.activityProject !== expandedActivityProject) expandActivityProject(button.dataset.activityProject);
|
||||
});
|
||||
$('#activity-project-list').addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-activity-project]');
|
||||
if (button) expandActivityProject(button.dataset.activityProject, true);
|
||||
});
|
||||
$('#activity-back').onclick = () => {
|
||||
const id = expandedActivityProject;
|
||||
expandActivityProject('');
|
||||
$(`[data-activity-project="${id}"]`)?.focus();
|
||||
};
|
||||
$('#project-activity').addEventListener('keydown', event => {
|
||||
const button = event.target.closest('[data-activity-project]');
|
||||
if (event.key === 'ArrowRight' && button) { event.preventDefault(); expandActivityProject(button.dataset.activityProject, true); }
|
||||
if ((event.key === 'ArrowLeft' || event.key === 'Escape') && expandedActivityProject) { event.preventDefault(); event.stopPropagation(); $('#activity-back').click(); }
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
const buttons = [...document.querySelectorAll(button ? '[data-activity-project]' : '#activity-list button')];
|
||||
const index = buttons.indexOf(document.activeElement);
|
||||
if (index >= 0) { event.preventDefault(); buttons[(index + (event.key === 'ArrowDown' ? 1 : -1) + buttons.length) % buttons.length].focus(); }
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', () => { if ($('#project-activity').matches(':popover-open')) positionActivityMenu(); });
|
||||
window.addEventListener('scroll', () => { if ($('#project-activity').matches(':popover-open')) positionActivityMenu(); }, true);
|
||||
renderProjects();
|
||||
let variant = 'dock';
|
||||
let catalogMode = 'apps';
|
||||
let residentApps = ['notes', 'knowledge'];
|
||||
let editingApps = false;
|
||||
let draggedApp = '';
|
||||
let installed = new Set(items.filter(item => item.builtin).map(item => item.id));
|
||||
let toastTimer;
|
||||
let settingsOpen = false;
|
||||
const noteSettings = { showCount: true, mode: 'immediate', format: 'combined' };
|
||||
const noteModes = { immediate: '即时', 'after-save-auto': '保存后自动', 'after-save-manual': '保存后手动' };
|
||||
const noteFormats = { combined: '长评 + 要点', narrative: '长评', structured: '要点' };
|
||||
function renderNoteSettings() {
|
||||
$('#app-settings').innerHTML = `<div class="note-setting-row"><div><strong>常驻左侧菜单</strong><p>对应“显示魔法笔记入口”,移除后仍可从应用中心打开。</p></div><button class="demo-switch" role="switch" aria-label="常驻左侧菜单" aria-checked="${residentApps.includes('notes')}" data-note-toggle="resident"></button></div>
|
||||
<div class="note-setting-row"><div><strong>显示未完成待办数量</strong><p>在左侧入口显示数量,超过 99 项显示 99+。本 Demo 使用 3 项示例。</p></div><button class="demo-switch" role="switch" aria-label="显示未完成待办数量" aria-checked="${noteSettings.showCount}" data-note-toggle="showCount"></button></div>
|
||||
<fieldset class="note-setting-options"><legend>AI 评论方式</legend><div class="note-segments" role="group" aria-label="AI 评论方式">${Object.entries(noteModes).map(([value, label]) => `<button data-note-mode="${value}" aria-pressed="${noteSettings.mode === value}">${label}</button>`).join('')}</div><p>即时:按回车并停止输入 5 秒后评论未保存草稿。保存后自动:保存后评论。保存后手动:点击 AI 分析后评论。</p></fieldset>
|
||||
<fieldset class="note-setting-options"><legend>AI 评论形式</legend><div class="note-segments" role="group" aria-label="AI 评论形式">${Object.entries(noteFormats).map(([value, label]) => `<button data-note-format="${value}" aria-pressed="${noteSettings.format === value}">${label}</button>`).join('')}</div><p>默认同时生成流式长评和结构化要点,也可只保留其中一种。</p></fieldset>
|
||||
<div class="note-settings-preview"><span class="eyebrow">当前配置预览</span><p>${noteModes[noteSettings.mode]} · ${noteFormats[noteSettings.format]}</p><small>选择后即时保留于本次 Demo 会话,不调用 AI、不修改真实设置。</small></div>`;
|
||||
if ($('#note-settings-summary')) $('#note-settings-summary').textContent = `当前设置:${noteModes[noteSettings.mode]} · ${noteFormats[noteSettings.format]}。待办数量${noteSettings.showCount ? '显示' : '隐藏'}。`;
|
||||
}
|
||||
function showNoteSettings() {
|
||||
if (!$('#catalog').open) { catalogMode = 'apps'; $('#search').value = ''; }
|
||||
settingsOpen = true;
|
||||
$('#catalog').dataset.variant = variant;
|
||||
$('#catalog-title').textContent = '魔法笔记设置';
|
||||
$('#catalog-description').textContent = '调整入口与 AI 评论行为。应用中心和笔记页面共享这份演示配置。';
|
||||
$('#back-to-apps').hidden = false; $('#app-settings').hidden = false;
|
||||
for (const selector of ['.center-switch', '#search', '#catalog-items']) $(selector).hidden = true;
|
||||
renderNoteSettings();
|
||||
if (!$('#catalog').open) $('#catalog').showModal();
|
||||
$('#back-to-apps').focus();
|
||||
}
|
||||
function notify(text) {
|
||||
$('#toast').textContent = text; $('#toast').classList.add('visible');
|
||||
clearTimeout(toastTimer); toastTimer = setTimeout(() => $('#toast').classList.remove('visible'), 2200);
|
||||
}
|
||||
function renderApps() {
|
||||
$('#edit-apps').textContent = editingApps ? '完成' : '编辑';
|
||||
$('#resident-list').innerHTML = residentApps.map((id, index) => {
|
||||
const item = items.find(entry => entry.id === id);
|
||||
return `<div class="resident-row" data-resident="${id}" draggable="${editingApps}"><button data-open="${id}" ${$('#page-label').textContent === item.name ? 'aria-current="page"' : ''}>${icon(item.icon)}<span>${item.name}</span>${id === 'notes' && noteSettings.showCount ? '<small class="note-count" aria-label="3 项未完成待办(示例)">3</small>' : ''}</button>${editingApps ? `<div class="edit-actions"><button data-move="${id}" data-direction="-1" aria-label="上移${item.name}" ${index === 0 ? 'disabled' : ''}>↑</button><button data-pin="${id}" aria-label="移除${item.name}">×</button><button data-move="${id}" data-direction="1" aria-label="下移${item.name}" ${index === residentApps.length - 1 ? 'disabled' : ''}>↓</button></div>` : ''}</div>`;
|
||||
}).join('') || '<p class="empty-apps">点击 +,选择常驻应用。</p>';
|
||||
const subtitles = { dock: '', grid: `${installed.size} 个已安装应用`, cards: '笔记 · 知识 · 自动化', launcher: 'Ctrl / ⌘ K', favorites: `${residentApps.length} 个应用常驻菜单`, scenes: '发现你的下一个好应用' };
|
||||
$('#center-entry').innerHTML = `<button id="open-center" aria-haspopup="dialog"><span class="brand-mark">${variant === 'launcher' ? icon('search') : 'GB'}</span><span><strong>${variant === 'launcher' ? '搜索应用…' : '应用中心'}</strong>${subtitles[variant] ? `<small>${subtitles[variant]}</small>` : ''}</span><span class="entry-arrow">${variant === 'scenes' ? '↗' : '⌃'}</span></button>`;
|
||||
}
|
||||
function renderCatalog() {
|
||||
const query = $('#search').value.trim().toLowerCase();
|
||||
const matches = items.filter(item => (catalogMode === 'store' ? !item.builtin : installed.has(item.id)) && `${item.name}${item.detail}`.toLowerCase().includes(query));
|
||||
if (variant === 'favorites') matches.sort((a, b) => Number(residentApps.includes(b.id)) - Number(residentApps.includes(a.id)));
|
||||
$('#catalog-items').innerHTML = matches.map(item => {
|
||||
const ready = installed.has(item.id); const pinned = residentApps.includes(item.id);
|
||||
return `<article class="center-app">${icon(item.icon)}<div class="center-app-info"><strong>${item.name}</strong><p>${item.builtin ? '内置应用' : '商店应用 · 示例'}</p><p class="app-detail">${variant === 'cards' ? item.summary : item.detail}</p></div><div class="center-actions">${ready ? `<button data-open="${item.id}">打开</button>${item.id === 'notes' ? '<button data-note-settings aria-label="魔法笔记设置">设置</button>' : ''}<button data-pin="${item.id}" aria-pressed="${pinned}">${pinned ? '取消常驻' : '常驻左侧'}</button>` : `<button data-install="${item.id}">模拟安装</button><button data-install-pin="${item.id}">安装并常驻</button>`}</div></article>`;
|
||||
}).join('') || '<p class="empty-apps">没有匹配的应用。可以换个关键词,或切换到商店。</p>';
|
||||
document.querySelectorAll('.center-switch button').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.catalog === catalogMode)));
|
||||
}
|
||||
function openCatalog(mode = 'apps') {
|
||||
settingsOpen = false; $('#back-to-apps').hidden = true; $('#app-settings').hidden = true;
|
||||
for (const selector of ['.center-switch', '#search', '#catalog-items']) $(selector).hidden = false;
|
||||
catalogMode = mode; $('#catalog').dataset.variant = variant;
|
||||
$('#catalog-title').textContent = mode === 'store' ? '发现新应用' : '应用中心';
|
||||
$('#catalog-description').textContent = mode === 'store' ? '安装你需要的应用,自主决定是否常驻左侧菜单。' : '打开应用,或把常用应用留在左侧。系统工具请使用右侧工作栏。';
|
||||
$('#search').value = ''; renderCatalog();
|
||||
if (!$('#catalog').open) $('#catalog').showModal();
|
||||
$('#search').focus();
|
||||
}
|
||||
function openPage(name) {
|
||||
if ($('#catalog').open) $('#catalog').close();
|
||||
$('#page-label').textContent = name;
|
||||
$('#page-content').replaceChildren();
|
||||
const title = document.createElement('h1'); title.textContent = name;
|
||||
const description = document.createElement('p'); description.className = 'muted'; description.textContent = '主内容区页面预览。右侧工作栏保持当前工具状态。';
|
||||
$('#page-content').append(title, description);
|
||||
$('#page-actions').innerHTML = name === '魔法笔记' ? `<button data-note-settings>${icon('settings')} 笔记设置</button>` : '';
|
||||
if (name === '魔法笔记' || name === '设置') {
|
||||
const summary = document.createElement('p'); summary.id = 'note-settings-summary'; summary.className = 'muted'; $('#page-content').append(summary);
|
||||
if (name === '设置') { const button = document.createElement('button'); button.dataset.noteSettings = ''; button.textContent = '魔法笔记设置'; $('#page-content').append(button); }
|
||||
renderNoteSettings();
|
||||
}
|
||||
document.querySelectorAll('.navigation button').forEach(button => button.classList.toggle('selected', button.dataset.page === name));
|
||||
renderApps(); notify(`已打开${name}`);
|
||||
}
|
||||
function selectVariant(name) {
|
||||
variant = name; $('.app').dataset.variant = name;
|
||||
document.querySelectorAll('button[data-variant]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.variant === name)));
|
||||
[$('#variant-label').textContent, $('#variant-description').textContent, $('#design-tradeoff').textContent] = variants[name];
|
||||
history.replaceState(null, '', `#${name}`); renderApps();
|
||||
}
|
||||
function selectTool(id) {
|
||||
const content = {
|
||||
files: '<span class="eyebrow">本机 / goodbuddy · 示例</span><div class="file-row">src /</div><div class="file-row">docs /</div><div class="file-row">resources /</div><div class="file-row">README.md</div>',
|
||||
terminal: '<span class="eyebrow">当前项目 · 演示终端</span><pre>PS > git status\nOn branch main\nworking tree clean\n\nPS ></pre>',
|
||||
browser: '<span class="eyebrow">浏览器 · 静态示例</span><div class="file-row">https://example.com</div><h2>Example Domain</h2>',
|
||||
resources: '<span class="eyebrow">本机 · 静态演示数据</span><div class="metric">CPU <strong>18%</strong><progress value="18" max="100"></progress></div><div class="metric">内存 <strong>9.6 / 32 GB</strong><progress value="30" max="100"></progress></div><p>磁盘 IO:读 12 MB/s · 写 3 MB/s</p>'
|
||||
};
|
||||
$('#tool-content').innerHTML = content[id]; $('#tool-content').setAttribute('aria-labelledby', `tab-${id}`);
|
||||
document.querySelectorAll('[data-tool]').forEach(button => { button.setAttribute('aria-selected', String(button.dataset.tool === id)); button.tabIndex = button.dataset.tool === id ? 0 : -1; });
|
||||
}
|
||||
document.addEventListener('click', event => {
|
||||
const button = event.target.closest('button'); if (!button) return;
|
||||
const data = button.dataset;
|
||||
if (data.project || data.conversation) {
|
||||
currentProject = data.project || data.conversation;
|
||||
if (projectMenu.matches(':popover-open')) projectMenu.hidePopover();
|
||||
if ($('#project-activity').matches(':popover-open')) $('#project-activity').hidePopover();
|
||||
renderProjects();
|
||||
const project = demoProjects.find(entry => entry.id === currentProject);
|
||||
openPage(data.conversation ? project.conversations[Number(data.conversationIndex)] : '当前对话');
|
||||
$('#page-label').textContent = `${project.name} / ${data.conversation ? project.conversations[Number(data.conversationIndex)] : '当前对话'}`;
|
||||
$('#project-trigger').focus();
|
||||
}
|
||||
if (data.variant) selectVariant(data.variant);
|
||||
if (data.catalog) openCatalog(data.catalog);
|
||||
if (data.page) openPage(data.page);
|
||||
if (data.open) openPage(items.find(item => item.id === data.open).name);
|
||||
if (data.tool) selectTool(data.tool);
|
||||
if (button.hasAttribute('data-note-settings')) showNoteSettings();
|
||||
if (data.noteToggle || data.noteMode || data.noteFormat) {
|
||||
if (data.noteToggle === 'resident') residentApps = residentApps.includes('notes') ? residentApps.filter(id => id !== 'notes') : [...residentApps, 'notes'];
|
||||
if (data.noteToggle === 'showCount') noteSettings.showCount = !noteSettings.showCount;
|
||||
if (data.noteMode) noteSettings.mode = data.noteMode;
|
||||
if (data.noteFormat) noteSettings.format = data.noteFormat;
|
||||
renderApps(); renderNoteSettings();
|
||||
const selector = data.noteToggle ? `[data-note-toggle="${data.noteToggle}"]` : data.noteMode ? `[data-note-mode="${data.noteMode}"]` : `[data-note-format="${data.noteFormat}"]`;
|
||||
$(selector).focus();
|
||||
}
|
||||
if (button.id === 'open-center') openCatalog(variant === 'scenes' ? 'store' : 'apps');
|
||||
if (button.id === 'edit-apps') { editingApps = !editingApps; renderApps(); $('#edit-apps').focus(); }
|
||||
if (data.pin || data.install || data.installPin) {
|
||||
const id = data.pin || data.install || data.installPin;
|
||||
if (data.install || data.installPin) installed.add(id);
|
||||
if (data.pin || data.installPin) residentApps = residentApps.includes(id) ? residentApps.filter(entry => entry !== id) : [...residentApps, id];
|
||||
renderApps();
|
||||
if ($('#catalog').open) { renderCatalog(); ($(`#catalog [data-pin="${id}"]`) || $('#search')).focus(); }
|
||||
else $('#edit-apps').focus();
|
||||
}
|
||||
if (data.move) {
|
||||
const from = residentApps.indexOf(data.move); const to = from + Number(data.direction);
|
||||
if (to >= 0 && to < residentApps.length) [residentApps[from], residentApps[to]] = [residentApps[to], residentApps[from]];
|
||||
renderApps(); $(`#resident-list [data-pin="${data.move}"]`).focus();
|
||||
}
|
||||
});
|
||||
$('#resident-list').addEventListener('dragstart', event => { draggedApp = event.target.closest('[data-resident]')?.dataset.resident || ''; if (!editingApps || !draggedApp) { event.preventDefault(); return; } event.dataTransfer.setData('text/plain', draggedApp); });
|
||||
$('#resident-list').addEventListener('dragover', event => { if (editingApps && draggedApp) event.preventDefault(); });
|
||||
$('#resident-list').addEventListener('drop', event => {
|
||||
event.preventDefault(); const target = event.target.closest('[data-resident]')?.dataset.resident;
|
||||
if (!editingApps || !residentApps.includes(draggedApp) || !target || target === draggedApp) return;
|
||||
const to = residentApps.indexOf(target); residentApps.splice(residentApps.indexOf(draggedApp), 1); residentApps.splice(to, 0, draggedApp); draggedApp = ''; renderApps(); $('#edit-apps').focus();
|
||||
});
|
||||
$('#resident-list').addEventListener('dragend', () => { draggedApp = ''; });
|
||||
$('#search').addEventListener('input', renderCatalog);
|
||||
$('#close-catalog').onclick = () => $('#catalog').close();
|
||||
$('#back-to-apps').onclick = () => { const query = $('#search').value; openCatalog(catalogMode); $('#search').value = query; renderCatalog(); ($('#catalog [data-note-settings]') || $('#search')).focus(); };
|
||||
function toggleWorkbar(visible) { $('.workbar').hidden = !visible; $('#toggle-workbar').setAttribute('aria-expanded', String(visible)); }
|
||||
$('#toggle-workbar').onclick = () => toggleWorkbar($('.workbar').hidden);
|
||||
$('#close-tool').onclick = () => { toggleWorkbar(false); $('#toggle-workbar').focus(); };
|
||||
$('.tool-tabs').addEventListener('keydown', event => {
|
||||
const tabs = [...document.querySelectorAll('[data-tool]')]; const index = tabs.indexOf(event.target);
|
||||
if (index < 0 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||
event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
|
||||
selectTool(tabs[next].dataset.tool); tabs[next].focus();
|
||||
});
|
||||
document.addEventListener('keydown', event => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); openCatalog(); }
|
||||
if (!$('#catalog').open || settingsOpen || variant !== 'launcher') return;
|
||||
const actions = [...document.querySelectorAll('#catalog [data-open]')]; if (!actions.length) return;
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); const index = actions.indexOf(document.activeElement); const next = index < 0 ? (event.key === 'ArrowDown' ? 0 : actions.length - 1) : (index + (event.key === 'ArrowDown' ? 1 : -1) + actions.length) % actions.length; actions[next].focus(); }
|
||||
if (event.key === 'Enter' && document.activeElement === $('#search')) { event.preventDefault(); actions[0].click(); }
|
||||
});
|
||||
$('#theme').onclick = () => { const dark = document.documentElement.dataset.theme !== 'dark'; document.documentElement.dataset.theme = dark ? 'dark' : 'light'; $('#theme').textContent = dark ? '切换浅色' : '切换深色'; };
|
||||
$('#reset').onclick = () => { installed = new Set(items.filter(item => item.builtin).map(item => item.id)); residentApps = ['notes', 'knowledge']; editingApps = false; renderApps(); notify('应用安装与常驻配置已恢复默认'); };
|
||||
let demoRunning = false;
|
||||
let demoRecording = false;
|
||||
let demoAttachments = [];
|
||||
let demoQueue = [];
|
||||
let demoReplyTimer;
|
||||
function renderComposer() {
|
||||
const tags = $('#composer-tags'); tags.replaceChildren();
|
||||
const addTag = (text, clear) => {
|
||||
const button = document.createElement('button'); button.textContent = `${text} ×`; button.setAttribute('aria-label', `移除${text}`);
|
||||
button.onclick = () => { clear(); renderComposer(); $('#message').focus(); }; tags.append(button);
|
||||
};
|
||||
demoAttachments.forEach((name, index) => addTag(name, () => demoAttachments.splice(index, 1)));
|
||||
for (const id of ['composer-expert', 'composer-agent', 'composer-command']) {
|
||||
const select = $(`#${id}`);
|
||||
if (select.value) addTag(select.selectedOptions[0].textContent, () => { select.value = ''; });
|
||||
}
|
||||
document.querySelectorAll('[data-knowledge]:checked').forEach(input => addTag(input.dataset.knowledge, () => { input.checked = false; }));
|
||||
if ($('#composer-retrieval').value === 'always') addTag('每次检索', () => { $('#composer-retrieval').value = 'auto'; });
|
||||
tags.hidden = !tags.childElementCount;
|
||||
$('#stop').hidden = !demoRunning;
|
||||
$('#send').disabled = !$('#message').value.trim() && $('#composer-command').value !== 'command';
|
||||
$('#send').title = demoRunning ? '加入队列' : '发送';
|
||||
$('#send').setAttribute('aria-label', demoRunning ? '加入演示队列' : '发送演示消息');
|
||||
$('#composer-status').textContent = demoRecording ? '模拟录音中 · 再次点击结束' : demoRunning ? '模拟回复中 · 可继续发送加入队列' : '交互演示 · 不调用 AI';
|
||||
for (const id of ['composer-runtime', 'composer-mode', 'composer-expert', 'composer-agent', 'composer-command']) $(`#${id}`).disabled = demoRunning;
|
||||
$('#compact').disabled = demoRunning;
|
||||
$('#runtime-options').hidden = $('#composer-runtime').value === '直连模型';
|
||||
$('#compact').hidden = $('#composer-runtime').value === '直连模型';
|
||||
$('#demo-queue').replaceChildren();
|
||||
demoQueue.forEach((text, index) => {
|
||||
const row = document.createElement('div'); row.className = 'queue-row';
|
||||
const label = document.createElement('span'); label.textContent = text; label.title = text;
|
||||
const insert = document.createElement('button'); insert.textContent = '立即插入'; insert.setAttribute('aria-label', `立即中断并插入:${text}`);
|
||||
insert.onclick = () => { demoQueue.splice(index, 1); startDemoReply(text); };
|
||||
const remove = document.createElement('button'); remove.textContent = '×'; remove.setAttribute('aria-label', `删除待发送消息:${text}`);
|
||||
remove.onclick = () => { demoQueue.splice(index, 1); renderComposer(); };
|
||||
row.append(label, insert, remove); $('#demo-queue').append(row);
|
||||
});
|
||||
$('#demo-queue').hidden = !demoQueue.length;
|
||||
}
|
||||
function startDemoReply(text) {
|
||||
clearTimeout(demoReplyTimer); demoRunning = true;
|
||||
if ($('#composer-options').matches(':popover-open')) $('#composer-options').hidePopover();
|
||||
notify(`模拟处理:${text}`); renderComposer();
|
||||
demoReplyTimer = setTimeout(() => {
|
||||
demoRunning = false;
|
||||
if (demoQueue.length) startDemoReply(demoQueue.shift());
|
||||
else { renderComposer(); notify('演示回复已完成,未调用 AI'); }
|
||||
}, 8000);
|
||||
}
|
||||
$('#send').onclick = () => {
|
||||
if ($('#send').disabled) return;
|
||||
const text = $('#composer-command').value === 'command' ? `/init ${$('#message').value.trim()}` : $('#message').value.trim();
|
||||
$('#message').value = ''; $('#message').style.height = ''; demoAttachments = [];
|
||||
if (demoRunning) { demoQueue.push(text); renderComposer(); }
|
||||
else startDemoReply(text);
|
||||
};
|
||||
$('#stop').onclick = () => {
|
||||
clearTimeout(demoReplyTimer); demoRunning = false;
|
||||
if (demoQueue.length) startDemoReply(demoQueue.shift());
|
||||
else { renderComposer(); notify('已停止演示回复'); }
|
||||
};
|
||||
$('#message').addEventListener('input', () => { $('#message').style.height = 'auto'; $('#message').style.height = `${Math.min(180, $('#message').scrollHeight)}px`; renderComposer(); });
|
||||
$('#message').addEventListener('keydown', event => { if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); $('#send').click(); } });
|
||||
$('#attach').onclick = () => $('#attachment-input').click();
|
||||
$('#attachment-input').onchange = () => { demoAttachments.push(...Array.from($('#attachment-input').files, file => file.name)); $('#attachment-input').value = ''; renderComposer(); };
|
||||
$('#voice').onclick = () => {
|
||||
demoRecording = !demoRecording; $('#voice').setAttribute('aria-pressed', String(demoRecording));
|
||||
if (!demoRecording) { $('#message').value += '请帮我整理一下今天的工作计划。'; $('#message').dispatchEvent(new Event('input')); }
|
||||
renderComposer();
|
||||
};
|
||||
$('#composer-options').addEventListener('change', event => {
|
||||
if (event.target.id === 'composer-command' && event.target.value === 'prompt') {
|
||||
$('#message').value = '请总结当前讨论,列出关键结论和下一步行动。'; event.target.value = ''; $('#message').dispatchEvent(new Event('input'));
|
||||
}
|
||||
renderComposer();
|
||||
});
|
||||
$('#composer-runtime').onchange = () => {
|
||||
$('#composer-agent').value = ''; $('#composer-command').value = '';
|
||||
$('#agent-label').textContent = $('#composer-runtime').value === 'Continue' ? '预设' : 'Agent';
|
||||
$('#composer-command').querySelector('[value="command"]').hidden = $('#composer-runtime').value !== 'OpenCode';
|
||||
renderComposer();
|
||||
};
|
||||
$('#compact').onclick = () => {
|
||||
$('#context-value').textContent = '18%'; $('#context-progress').value = 18;
|
||||
$('#context-description').textContent = '压缩后对话估算:36k / 200k tokens';
|
||||
$('#context-details').hidePopover(); notify('已模拟压缩上下文');
|
||||
};
|
||||
for (const [panelId, triggerId] of [['composer-options', 'composer-options-trigger'], ['context-details', 'context-trigger']]) {
|
||||
const panel = $(`#${panelId}`); const trigger = $(`#${triggerId}`);
|
||||
const position = () => {
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
panel.style.left = `${Math.max(16, Math.min(rect.right - panel.offsetWidth, innerWidth - panel.offsetWidth - 16))}px`;
|
||||
panel.style.top = `${Math.max(16, rect.top - panel.offsetHeight - 8)}px`;
|
||||
};
|
||||
panel.addEventListener('toggle', event => { trigger.setAttribute('aria-expanded', String(event.newState === 'open')); if (event.newState === 'open') position(); });
|
||||
window.addEventListener('resize', () => { if (panel.matches(':popover-open')) position(); });
|
||||
window.addEventListener('scroll', () => { if (panel.matches(':popover-open')) position(); }, true);
|
||||
}
|
||||
renderComposer();
|
||||
document.querySelectorAll('[data-icon]').forEach(element => { element.innerHTML = icon(element.dataset.icon); });
|
||||
window.addEventListener('hashchange', () => { if (Object.hasOwn(variants, location.hash.slice(1))) selectVariant(location.hash.slice(1)); });
|
||||
selectVariant(Object.hasOwn(variants, location.hash.slice(1)) ? location.hash.slice(1) : 'dock'); selectTool('files');
|
||||
if (matchMedia('(max-width: 1080px)').matches) toggleWorkbar(false);
|
||||
@@ -0,0 +1,223 @@
|
||||
# 应用导航与系统工具入口 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 关联设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[助手工作栏 PRD](../assistant-workbar/prd.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
计划修订说明:当前实施范围以[第 8 节](#8-分期)为准。本文其余章节仍包含 v0.2 的
|
||||
左下角系统工具方案;启动实现前须同步功能要求、验收及配套设计,旧快捷工具条不再列入实施。
|
||||
|
||||
主侧栏当前把“当前对话”“魔法笔记”“知识库”“智能心跳”和“运行记录”放在同一导航组,
|
||||
底部长期显示“GB 本地工作区”和平台架构。底部信息不能反映用户可以执行的操作,文件、终端
|
||||
和浏览器则需要先找到右侧助手工作栏后才能打开。
|
||||
|
||||
后续插件可能提供完整页面,也可能只提供辅助面板或命令。若不先区分系统入口、应用和工具,
|
||||
插件接入后会同时争用主导航、侧栏底部和助手工作栏,用户也无法判断入口会切换页面还是打开
|
||||
辅助面板。
|
||||
|
||||
## 2. 产品定义
|
||||
|
||||
### 2.1 三类入口
|
||||
|
||||
| 类型 | 首期成员 | 默认位置 | 用户控制范围 |
|
||||
| --- | --- | --- | --- |
|
||||
| 系统入口 | 当前对话、运行记录、设置 | 主导航固定位置或侧栏底部 | 不可隐藏、卸载或改变类型 |
|
||||
| 应用 | 魔法笔记、知识库、智能心跳 | 主导航应用区 | 可控制是否显示;隐藏不删除数据 |
|
||||
| 系统工具 | 文件浏览器、终端、浏览器、资源监控 | 侧栏底部快捷工具栏 | 首期固定提供,不可卸载 |
|
||||
|
||||
运行记录是系统审计页面,不属于工具。文件浏览器、终端、浏览器和资源监控是系统工具;它们
|
||||
的快捷入口位于左下角,实际界面继续由右侧助手工作栏承载。
|
||||
|
||||
### 2.2 后续插件类型
|
||||
|
||||
插件市场开放后,插件可以贡献“插件应用”或“插件工具”。插件不能注册、覆盖或替换系统入口,
|
||||
也不能冒充系统工具。内置应用继续标记为“内置”,不因采用统一注册协议而显示成待安装商品。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 用户从侧栏底部一次点击即可打开文件、终端、浏览器或资源监控。
|
||||
- 主导航只承载完整页面,辅助工具不占用一级页面位置。
|
||||
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
|
||||
- 内置应用可以从一个设置位置控制主导航可见性,隐藏后仍可恢复。
|
||||
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
- 首期不提供应用商店页面、商店导航、推荐位或安装按钮,也不提供第三方插件搜索、下载、
|
||||
安装、更新、签名验证或卸载。内置应用的浏览、搜索与打开属于应用中心,不依赖商店。
|
||||
- 不把文件树、终端或浏览器重新实现到左侧栏。
|
||||
- 不改变助手工作栏的面板实例、停靠、执行空间和安全边界。
|
||||
- 不允许隐藏运行记录、当前对话或设置。
|
||||
- 不因隐藏应用而删除魔法笔记、知识库或智能心跳数据。
|
||||
- 不把“显示在侧边栏”解释为停止智能心跳计划或后台任务。
|
||||
- 不提供任意本机程序启动器。
|
||||
- 首期资源监控不采集远程主机、GPU、显存或网络吞吐,不提供后台告警和历史持久化。
|
||||
|
||||
## 5. 功能要求
|
||||
|
||||
### FR-1 入口分类
|
||||
|
||||
系统必须为每个可见入口声明稳定类型:`system-entry`、`application` 或 `system-tool`。界面位置、
|
||||
设置项和点击结果均以该类型为准,不根据当前页面临时改变类型。
|
||||
|
||||
### FR-2 固定系统入口
|
||||
|
||||
“当前对话”固定在主导航首位;“运行记录”固定在应用区之后;“设置”固定在侧栏底部。
|
||||
这些入口不出现在应用显隐、工具固定或插件管理设置中。
|
||||
|
||||
### FR-3 内置应用区
|
||||
|
||||
魔法笔记、知识库和智能心跳显示在当前对话与运行记录之间。应用入口打开对应一级页面,保留
|
||||
现有徽标、范围和页面状态。用户可以在设置中控制其主导航可见性。
|
||||
|
||||
### FR-4 隐藏应用
|
||||
|
||||
隐藏应用只移除主导航入口,不删除数据、不修改业务启停状态,也不终止正在进行的操作。显隐
|
||||
Switch 更新成功后主导航立即生效;更新失败时恢复原状态并允许重试。用户可从设置恢复入口。
|
||||
|
||||
### FR-5 系统工具快捷栏
|
||||
|
||||
侧栏底部提供文件浏览器、终端、浏览器和资源监控四个图标按钮,并保留独立设置按钮。每个
|
||||
按钮具有可见工具提示和可访问名称。首期系统工具顺序固定为文件、终端、浏览器、资源监控。
|
||||
|
||||
### FR-6 文件浏览器入口
|
||||
|
||||
点击文件按钮时展开助手工作栏,并聚焦默认存在且不可关闭的“工作区”单实例。该实例保持
|
||||
“跟随当前上下文”或“固定到指定对象”状态;迁移或布局修复发现实例缺失时补回,并默认跟随
|
||||
当前项目。没有项目时使用已配置的默认工作区。跟随上下文所需的路径不可用时,面板显示修复
|
||||
入口;已有有效固定目标不受当前项目和默认工作区状态影响。
|
||||
|
||||
### FR-7 终端入口
|
||||
|
||||
点击终端按钮时展开助手工作栏。当前执行空间有正在启动或运行中的终端时聚焦最近使用的实例;
|
||||
没有时创建一个绑定当前执行空间的新终端,已退出、已中断或失败的历史 Tab 保持不变。没有
|
||||
项目时当前执行空间为本机 Home。创建额外并行终端继续使用工作栏现有的“+”目录入口。
|
||||
|
||||
### FR-8 浏览器入口
|
||||
|
||||
点击浏览器按钮时展开助手工作栏,并优先聚焦绑定当前 Conversation 的最近使用浏览器实例;
|
||||
没有匹配实例时创建一个。没有活动 Conversation 时聚焦最近使用的浏览器实例;完全不存在时
|
||||
创建待绑定空实例。该快捷入口不会因连续点击创建重复实例;用户通过工作栏“+”选择浏览器时
|
||||
则始终创建新实例。入口不打开系统默认浏览器,也不创建旁路 WebView。
|
||||
|
||||
### FR-9 资源监控入口
|
||||
|
||||
点击资源监控按钮时展开助手工作栏,并打开或聚焦单实例的本机“资源监控”面板。面板显示
|
||||
整机 CPU 使用率、内存使用量与总量、磁盘读写速率,以及 GoodBuddy 桌面进程的 CPU、内存
|
||||
和进程数摘要。指标不可用时显示具体项目和原因,不能显示为 `0`。
|
||||
|
||||
资源面板处于可见活动状态、工作栏已展开且应用窗口未隐藏或最小化时,每 2 秒采样一次,在
|
||||
Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止采样;首期不在后台持续监控,
|
||||
也不据此产生侧栏告警。首次采样及停止后恢复的首次采样只建立 CPU 和磁盘 IO 速率基线,
|
||||
不显示伪造的 `0` 值。
|
||||
|
||||
### FR-10 快捷入口状态
|
||||
|
||||
工具面板已激活时,对应按钮显示选中状态;面板存在但未激活时保持普通状态。运行中、错误或
|
||||
等待用户处理可以显示徽标,但必须有可访问文字,且不得自动抢占当前面板。
|
||||
|
||||
### FR-11 应用导航设置
|
||||
|
||||
设置页提供“应用与工具”管理入口。首期“应用”区域列出三个内置应用并提供“显示在侧边栏”
|
||||
Switch,切换后立即持久化。系统工具区域列出四项内置固定能力,不提供虚假的卸载或停用按钮。
|
||||
|
||||
### FR-12 偏好持久化
|
||||
|
||||
应用可见性按设备持久化。升级时保留当前行为:已有可用内置应用继续显示;魔法笔记原有功能
|
||||
启停设置保持原意,不由新的导航可见性覆盖。
|
||||
|
||||
### FR-13 窄窗口行为
|
||||
|
||||
窄窗口下主侧栏仍按现有抽屉方式显示。系统工具按钮保持单行且不与设置按钮重叠;空间不足时
|
||||
收起文字和非必要分隔,不隐藏四个系统工具或设置。
|
||||
|
||||
### FR-14 项目活动汇总
|
||||
|
||||
用户在任一项目中都能查看跨项目的运行中与待处理会话,并从活动列表直接进入所选会话。
|
||||
汇总入口常驻项目选择器下方,没有活动时显示“暂无活动”。点击后先显示有活动会话的项目,
|
||||
悬停或点击项目展开会话子菜单,点击会话进入该条对话。项目菜单保留原有分组与顺序,并显示
|
||||
各项目的活动数量;级联菜单内的项目顺序不随待处理或运行中数量变化。
|
||||
汇总不受当前项目、搜索条件或最近历史条数限制。计数与排除规则见
|
||||
[活动逻辑](./logic-design.md#9-项目活动汇总),交互见[活动界面](./ui-design.md#10-项目活动汇总)。
|
||||
|
||||
验收覆盖跨项目后台运行、同会话多任务去重、提问与审批优先、较早的仍在运行或等待处理的
|
||||
会话可见,以及从带搜索条件的页面或有未保存修改的设置页跳转。结束后仅有未读标记的
|
||||
会话不保留在活动汇总中;已完成未读面板不属于本项交付。
|
||||
|
||||
## 6. 权限与安全
|
||||
|
||||
- 快捷入口不授予新的文件、命令、网络或浏览器权限。
|
||||
- 系统工具继续使用 Main、Preload、执行空间 Grant、Ask/Execute 和现有审批边界。
|
||||
- Renderer 不因新增入口获得任意文件系统、PTY、进程或 Electron API。
|
||||
- 资源指标由 Main 采集并以有界结构返回;Renderer 不接收进程命令行、环境变量或任意 PID。
|
||||
- 资源样本只保存在当前 Renderer 内存中,不写入运行记录、SQLite 或遥测服务。
|
||||
- 插件贡献类型只是界面与导航声明,不能代替权限清单、用户授权和运行隔离。
|
||||
- 运行记录继续记录由系统工具触发的受审计操作。
|
||||
|
||||
## 7. 产品验收
|
||||
|
||||
- 主导航能明确区分固定系统入口和可配置内置应用。
|
||||
- 运行记录在所有应用显隐组合下保持可见并可打开。
|
||||
- 左下角不再用“本地工作区”与系统架构占据主要空间。
|
||||
- 文件、终端、浏览器和资源监控均可从左下角打开正确的工作栏面板,且不产生第二套内容界面。
|
||||
- 连续点击系统工具不会无意创建重复的浏览器或终端实例;工作区始终复用不可关闭的固定实例。
|
||||
用户通过工作栏“+”可以显式创建额外浏览器或终端实例。
|
||||
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
|
||||
最小化后停止请求新样本。
|
||||
- CPU、内存、磁盘 IO 或 GoodBuddy 指标不可用时,页面区分部分可用与整体失败。
|
||||
- 隐藏内置应用不删除数据、不停止后台计划,并能从设置恢复;保存失败时入口保持原状。
|
||||
- 键盘、屏幕阅读器、浅色主题、深色主题和窄窗口均可完成相同操作。
|
||||
|
||||
## 8. 分期
|
||||
|
||||
### 首期
|
||||
|
||||
- 将侧栏最底部改为内置应用中心入口,设置按钮保持独立;具体列表、宫格或卡片样式从
|
||||
Demo 中收敛确定。
|
||||
- 应用中心提供魔法笔记、知识库、智能心跳的浏览、搜索与打开,不要求安装。
|
||||
- 支持应用常驻主导航、取消常驻和排序;未常驻应用仍可从应用中心打开,隐藏不删除数据或停止后台任务。
|
||||
- 应用中心和应用页面就近提供已有功能设置,共享真实设置来源;已有全局设置入口定位到同一界面。
|
||||
魔法笔记优先接入待办数量、AI 评论方式和评论形式,入口显示与常驻控制统一,避免重复开关。
|
||||
- 文件、终端、浏览器保留在右侧工作栏 Tab,不在左下角复制入口,也不纳入应用中心。
|
||||
- 原计划的本机资源监控仍作为系统工具交付项,在右侧增加单实例面板;与应用中心分别实现和验证。
|
||||
- 保持运行记录为固定系统入口。
|
||||
|
||||
首期验收需覆盖:无商店入口时可以访问全部内置应用;取消常驻后仍可重新打开和配置;
|
||||
不同设置入口状态一致;常驻排序重启后保留;右侧工具切换不改变当前应用页面。
|
||||
本节是范围调整,Demo 交互通过不代表正式功能已经实现。
|
||||
|
||||
### 补充要求:魔法笔记迁移与入口联动
|
||||
|
||||
- 将现有魔法笔记设置迁入应用中心的应用设置界面,笔记页面内的设置入口访问同一份配置。
|
||||
魔法笔记可常驻主导航;原全局设置位置如保留入口,应定位到共同设置界面。
|
||||
- 魔法笔记相关布尔开关默认全部开启,包括入口显示(常驻)和未完成待办数量显示。
|
||||
AI 评论方式、评论形式是单选项,沿用现有默认值,不把“全部开启”解释为同时选中多个模式。
|
||||
新默认值适用于未配置状态,迁移时保留用户已明确保存的选择。
|
||||
- 应用关闭后的入口联动必须覆盖主导航和会话页面,不能只更新应用中心卡片或一个菜单。
|
||||
知识库关闭后,左侧知识库菜单及会话中的知识库入口按钮同步移除;重新开启后按配置恢复。
|
||||
- “取消常驻”只移除主导航捷径;“关闭应用”移除该应用在主导航、会话等业务界面的入口。
|
||||
应用中心保留管理和重新开启入口。原型中仅有常驻控制,正式设计需补齐这两种状态,避免混用。
|
||||
- 入口变化不删除已有笔记或知识数据。知识库已打开页面、会话已关联知识以及后台任务的处理,
|
||||
在详细逻辑同步时分别定义,不能仅凭隐藏按钮推断功能或后台行为已经停用。
|
||||
|
||||
补充验收:从任一配置入口关闭知识库,主导航和当前会话按钮立即同步移除,切换会话、
|
||||
重启后也不重新出现;重新开启后恢复相应入口。验证魔法笔记未配置时的默认开启,以及
|
||||
已有关闭选择在迁移后保留。设置保存失败时各入口保持原状态,不能出现互相矛盾的显示。
|
||||
|
||||
### 后续:应用商店与扩展
|
||||
|
||||
- 单独设计应用商店的目录、搜索、推荐、详情及安装管理,时间与具体范围另行确定。
|
||||
- 插件应用和插件工具注册,以及安装、更新、卸载与权限说明。
|
||||
- 插件应用可常驻主导航;插件工具按工作栏扩展设计接入,不恢复左下角重复系统工具条。
|
||||
- 用户级快捷键与命令面板接入同一贡献命令。
|
||||
|
||||
现有 Demo 的商店页、项目管理和阅读空间安装流程仅保留为未来交互参考,不纳入首期验收,
|
||||
也不作为已有商店能力的说明。首期实现不预建插件安装服务或远程目录协议。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 项目活动级联实施进度
|
||||
|
||||
## 2026-09-13:生产活动入口
|
||||
|
||||
对应 [FR-14](./prd.md#fr-14-项目活动汇总)、[US-E1 / US-E2](./user-stories.md#6-epic-e项目活动级联)。
|
||||
本页只记录项目活动入口,不表示应用中心计划已交付。
|
||||
|
||||
`App` 已接入 `ProjectActivity` 常驻摘要和锚定级联菜单。会话和项目计数继续使用现有
|
||||
`deriveConversationActivity` 结果,点击会话继续调用 `openActivityConversation`。未增加
|
||||
持久化字段、IPC 或 Runtime 状态。项目选择器分组及计数保持原有行为。
|
||||
|
||||
组件与 App 定向回归通过 32 项,覆盖空闲入口、稳定项目及 SSH Host 顺序、待处理优先、
|
||||
跨项目精确跳转、设置离开检查、实时状态清理、键盘返回、外部关闭和窄侧栏焦点。
|
||||
|
||||
Windows Electron 视觉 fixture 使用当前 `ProjectActivity`、生产样式和 i18n,活动数据为
|
||||
受控样例。浅深主题分别验证 `1280×800`、`960×720`、`720×640`、`640×420`、`375×600`;
|
||||
原生键盘验证 Enter、上下及右方向键、End、两级 Escape、Tab 退出,另测左侧回退和空闲入口。
|
||||
此项证明组件的实际 Chromium 排版与交互,不代表完整生产 Main/Preload 的端到端验证。
|
||||
|
||||
浏览器遮挡复用共享菜单相交机制,组件测试检查相交和非相交结果;本轮未单独驱动原生
|
||||
`WebContentsView` 可见性。远程 Agent 执行路径未改动,未发出模型或知识库请求。
|
||||
|
||||
提交前复核:`npm run typecheck`、`npm run lint` 通过;单独运行
|
||||
`npx vitest run src/renderer/src/ProjectActivity.test.tsx`,11 项通过。
|
||||
`npm test` 在 240 秒后超时,超时前 `tests/agent-package.test.ts` 的
|
||||
`verifies and installs offline dependency inventories larger than one MiB` 用例失败,未取得完整结果。
|
||||
|
||||
Demo 已按最新讨论隐藏空闲摘要并移除会话行箭头;生产实现仍保留空闲摘要,尚未与 Demo 对齐。
|
||||
后续界面修正已移除生产会话行箭头,并为活动摘要与下方新建会话增加 `8px` 间距。
|
||||
|
||||
## 2026-09-14:0.13.2 候选验证
|
||||
|
||||
正式发布准备时再次复现 App 问答用例的时序失败。活动摘要改为常驻后,等待该入口出现
|
||||
不再意味着会话初始化和 Runtime 状态读取已经完成;测试有时点击仍被禁用的发送按钮,
|
||||
留下未消费的一次性问答 mock,继而影响后续用例。
|
||||
|
||||
测试现等待已加载的会话和可用发送按钮,并在每个用例前重置问答及审批响应 mock。
|
||||
答案重载用例等待提交后的界面状态,而不只等待 API 被调用。未修改生产交互、
|
||||
未增加固定延时,也未跳过失败用例。相关 20 项连续三轮通过,完整 App 与 Agent
|
||||
发布说明检查共 227 项通过。
|
||||
|
||||
最终候选 `npm test` 通过 4,155 项、跳过 66 项、失败 0 项;`npm run typecheck`、
|
||||
`npm run lint` 和 `npm run release:notes:verify` 均通过。发布准备未执行本地生产构建、
|
||||
打包或安装包启动探针,原生包验收由主分支与标签 CI 完成。
|
||||
@@ -0,0 +1,419 @@
|
||||
# 应用导航与系统工具入口技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| UI 设计 | [UI 设计](./ui-design.md) |
|
||||
|
||||
## 1. 现状
|
||||
|
||||
主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换:
|
||||
|
||||
```ts
|
||||
type WorkspaceView =
|
||||
| 'chat'
|
||||
| 'magic-notes'
|
||||
| 'knowledge'
|
||||
| 'heartbeat'
|
||||
| 'activity'
|
||||
| 'settings'
|
||||
```
|
||||
|
||||
文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts`
|
||||
承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程
|
||||
实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控
|
||||
面板。现有侧栏底部 `user-card` 整行打开设置,并显示“本地工作区”和平台架构。
|
||||
|
||||
本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/
|
||||
Renderer 最小实现。文件、终端和浏览器后端保持不变。
|
||||
|
||||
## 2. 模块边界
|
||||
|
||||
| 模块 | 责任 |
|
||||
| --- | --- |
|
||||
| `App.tsx` | 组合主导航、系统工具栏、设置入口和页面离开检查 |
|
||||
| `SidebarSystemTools`(新增) | 渲染四个系统工具按钮并发送打开意图 |
|
||||
| `RightAssistantSidebar.tsx` | 接收打开意图,调用工作栏实例创建或激活逻辑 |
|
||||
| `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 |
|
||||
| `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 |
|
||||
| `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 |
|
||||
| `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 |
|
||||
| `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 |
|
||||
| `application-settings-contracts.ts` | 增加应用导航偏好 schema 和默认值 |
|
||||
| `application-settings-store.ts` | 迁移并原子保存应用导航偏好 |
|
||||
| `PlatformFeaturesSettingsSection` 或独立设置 Section | 编辑“显示在侧边栏”偏好 |
|
||||
|
||||
不要在 `SidebarSystemTools` 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送
|
||||
有界的工具 ID。
|
||||
|
||||
## 3. Renderer 契约
|
||||
|
||||
### 3.1 系统工具 ID
|
||||
|
||||
```ts
|
||||
export const sidebarSystemToolIds = [
|
||||
'workspace',
|
||||
'terminal',
|
||||
'browser',
|
||||
'resources'
|
||||
] as const
|
||||
|
||||
export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number]
|
||||
```
|
||||
|
||||
该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer
|
||||
状态;实际能力调用继续走现有 Preload API。
|
||||
|
||||
### 3.2 打开意图
|
||||
|
||||
`App` 向 `RightAssistantSidebar` 传入单调递增的请求或命令对象:
|
||||
|
||||
```ts
|
||||
type WorkbarOpenIntent = {
|
||||
requestId: number
|
||||
appId: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
reason: 'sidebar-system-tool'
|
||||
context?: { conversationId?: string; projectId?: string }
|
||||
}
|
||||
```
|
||||
|
||||
`requestId` 使连续点击同一工具仍能被消费。不要仅设置 `assistantSidebarTab`,因为用户可能已
|
||||
关闭对应实例,且终端需要按执行空间寻找实例。
|
||||
|
||||
建议由 `RightAssistantSidebar` 在消费意图后通过现有 `WorkbarShell` 控制接口完成。控制器必须
|
||||
接收上下文和调用原因,不能只按 `appId` 查找第一个实例:
|
||||
|
||||
```ts
|
||||
type WorkbarController = {
|
||||
openOrActivate(intent: WorkbarOpenIntent): Promise<WorkbarTabInstance>
|
||||
}
|
||||
```
|
||||
|
||||
若当前 `WorkbarShell` 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。
|
||||
|
||||
工作区意图始终激活注册表中的不可关闭单实例;仅在迁移或布局损坏时补回。浏览器意图按
|
||||
`conversationId` 激活最近使用的匹配实例,没有匹配实例时创建一个。工作栏“+”不走该去重
|
||||
路径,选择浏览器时始终创建新实例。
|
||||
|
||||
### 3.3 终端匹配
|
||||
|
||||
工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用:
|
||||
|
||||
1. 解析当前项目对应的 `ExecutionSpaceSelection`。
|
||||
2. 在现有终端实例中按目标 identity 查找 `starting` 或 `running` 状态。
|
||||
3. 存在多个匹配实例时激活最近使用的实例。
|
||||
4. 没有匹配实例时调用现有终端创建流程。
|
||||
|
||||
`closing`、`exited`、`interrupted` 和 `failed` 实例保留为历史 Tab,不阻止快捷按钮创建新终端。
|
||||
工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。
|
||||
|
||||
### 3.4 资源监控工作栏应用
|
||||
|
||||
在 `workbar-contracts.ts` 中增加 `resources`:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 'resources',
|
||||
instancePolicy: 'single',
|
||||
defaultContext: 'application'
|
||||
}
|
||||
```
|
||||
|
||||
该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建
|
||||
或聚焦它。
|
||||
|
||||
## 4. 应用导航偏好
|
||||
|
||||
### 4.1 设置结构
|
||||
|
||||
在 `ApplicationSettings` 增加独立字段:
|
||||
|
||||
```ts
|
||||
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat'
|
||||
|
||||
type ApplicationNavigationSettings = {
|
||||
hiddenBuiltInApplicationIds: BuiltInApplicationId[]
|
||||
}
|
||||
```
|
||||
|
||||
默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔
|
||||
字段;Zod schema 必须去重、限制数量并拒绝未知内置
|
||||
ID。
|
||||
|
||||
首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留
|
||||
规则后另行扩展,不提前混入当前 schema。
|
||||
|
||||
### 4.2 与现有开关组合
|
||||
|
||||
入口可见条件为:
|
||||
|
||||
```ts
|
||||
const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id)
|
||||
```
|
||||
|
||||
`magicNotesEnabled` 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合
|
||||
规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置
|
||||
不能写回现有开关。
|
||||
|
||||
### 4.3 持久化与 IPC
|
||||
|
||||
沿用现有应用设置链路:
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> window.goodbuddy.updates.updateSettings
|
||||
-> settings:application:update
|
||||
-> application-settings-store
|
||||
-> application-settings.json
|
||||
```
|
||||
|
||||
更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不
|
||||
修改已有 `magicNotesEnabled` 或其他平台设置。
|
||||
|
||||
## 5. 侧栏底部重构
|
||||
|
||||
现有 `user-card` 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮:
|
||||
|
||||
```tsx
|
||||
<div className="sidebar-footer">
|
||||
<SidebarSystemTools onOpenTool={openSystemTool} />
|
||||
<button aria-label={t('navigation.settings')} ...>
|
||||
<Settings aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
|
||||
保留设置预加载、焦点恢复和未保存离开确认。
|
||||
|
||||
系统工具按钮调用统一 `openSystemTool`:
|
||||
|
||||
1. 设置工作栏打开意图。
|
||||
2. 展开工作栏。
|
||||
3. 由工作栏创建或激活实例。
|
||||
4. 按现有策略把焦点移入目标面板。
|
||||
|
||||
工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。
|
||||
|
||||
## 6. 工作栏状态与快捷按钮
|
||||
|
||||
`RightAssistantSidebar` 应向 `App` 暴露当前活动实例的 `appId` 和必要的有界状态摘要,或由
|
||||
现有 `onTabChange` 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。
|
||||
|
||||
```ts
|
||||
type SidebarSystemToolState = {
|
||||
activeToolId?: SidebarSystemToolId
|
||||
statusByToolId?: Partial<Record<SidebarSystemToolId, 'attention' | 'error' | 'running'>>
|
||||
}
|
||||
```
|
||||
|
||||
首期可以只实现 `activeToolId`,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误
|
||||
或运行状态。
|
||||
|
||||
## 7. 应用显隐更新
|
||||
|
||||
应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只
|
||||
允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航
|
||||
随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功
|
||||
保存的设置。
|
||||
|
||||
## 8. 插件兼容边界
|
||||
|
||||
首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。
|
||||
插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek
|
||||
Harness 的 npm 插件市场。
|
||||
|
||||
## 9. 本机资源监控
|
||||
|
||||
### 9.1 采集范围
|
||||
|
||||
首期在 Main 中使用经过依赖审查的 `systeminformation` 获取整机 CPU、内存和磁盘 IO,使用
|
||||
Electron `app.getAppMetrics()` 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收
|
||||
Renderer 提供的路径、PID、设备名或命令。
|
||||
|
||||
首期指标定义:
|
||||
|
||||
- CPU:整机忙碌时间占比,规范化为 `0` 至 `100`。
|
||||
- 内存:总字节数、可用字节数和 `总量 - 可用量` 得到的使用量。
|
||||
- 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。
|
||||
- GoodBuddy:`app.getAppMetrics()` 返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程
|
||||
CPU 先相加,再按逻辑处理器数量归一化为整机容量占比 `0` 至 `100`;不能通过截断总和处理
|
||||
多核数值。
|
||||
|
||||
受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回
|
||||
不可用错误码,不能回退为 `0`。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链
|
||||
检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。
|
||||
|
||||
### 9.2 快照契约
|
||||
|
||||
```ts
|
||||
type ResourceMetric<T> =
|
||||
| { status: 'available'; value: T }
|
||||
| { status: 'warming-up' }
|
||||
| { status: 'unavailable'; reason: ResourceMetricUnavailableReason }
|
||||
|
||||
type LocalResourceSnapshot = {
|
||||
sampledAt: string
|
||||
host: {
|
||||
cpuPercent: ResourceMetric<number>
|
||||
memory: ResourceMetric<{
|
||||
usedBytes: number
|
||||
totalBytes: number
|
||||
}>
|
||||
diskIo: ResourceMetric<{
|
||||
readBytesPerSecond: number
|
||||
writeBytesPerSecond: number
|
||||
}>
|
||||
}
|
||||
goodbuddy: {
|
||||
cpuPercent: ResourceMetric<number>
|
||||
memoryBytes: ResourceMetric<number>
|
||||
processCount: ResourceMetric<number>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有数字必须有限且非负;百分比限制为 `0` 至 `100`,字节数设置跨 IPC 上限。`sampledAt` 由
|
||||
Main 生成。不可用原因使用稳定枚举,例如 `unsupported-platform`、`permission-denied`、
|
||||
`collector-timeout` 和 `collector-failed`,不向 Renderer 返回原始命令输出。`warming-up` 只用于
|
||||
依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。
|
||||
|
||||
### 9.3 Preload 与 IPC
|
||||
|
||||
Preload 只暴露:
|
||||
|
||||
```ts
|
||||
resourceMonitor.getLocalSnapshot(input: {
|
||||
resetIntervalBaseline: boolean
|
||||
}): Promise<LocalResourceSnapshot>
|
||||
```
|
||||
|
||||
对应 IPC 为 `resource-monitor:local-snapshot:get`。Main 验证可信 sender;单次采集设置超时,
|
||||
并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回
|
||||
有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集
|
||||
任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。
|
||||
|
||||
合并规则区分普通请求与基线重置请求:
|
||||
|
||||
- 普通请求在途时,新的普通请求复用它。
|
||||
- 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能
|
||||
用普通请求结果满足重置请求。
|
||||
- 多个重置请求复用同一个在途或待执行重置。
|
||||
- 重置请求在途时,普通请求复用其 `warming-up` 结果,下一次时钟再取得区间值。
|
||||
|
||||
因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列
|
||||
长度。
|
||||
|
||||
面板每次从非活动状态恢复后的首次请求传入 `resetIntervalBaseline: true`。Main 消费本次
|
||||
`systeminformation` 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 `warming-up`;
|
||||
内存等瞬时指标正常返回。后续请求传入 `false` 并计算当前区间值,避免把面板停用期间计入
|
||||
速率。
|
||||
|
||||
### 9.4 轮询与历史
|
||||
|
||||
`ResourceMonitorPanel` 仅在工作栏展开、`resources` 为活动实例且
|
||||
`document.visibilityState === 'visible'` 时启动轮询:
|
||||
|
||||
1. 进入活动状态后立即请求一次,并设置 `resetIntervalBaseline: true`。
|
||||
2. 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。
|
||||
3. Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。
|
||||
4. Renderer 环形队列最多保留 150 个带真实时间戳的样本。
|
||||
5. 采集失败不追加伪造样本,也不持久化历史。
|
||||
|
||||
资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 `resources` 实例
|
||||
及其活动状态。
|
||||
|
||||
## 10. 测试
|
||||
|
||||
### 10.1 Renderer 测试
|
||||
|
||||
- 固定入口顺序不受三个应用显隐组合影响。
|
||||
- 运行记录和设置没有显隐控件。
|
||||
- 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。
|
||||
- 文件复用不可关闭单实例;浏览器和终端按当前上下文聚焦匹配实例,工作栏“+”允许显式多实例。
|
||||
- 工具按钮展开工作栏但不改变当前主页面。
|
||||
- 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。
|
||||
- 窄窗口下工具按钮不重叠,焦点恢复正确。
|
||||
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
|
||||
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
|
||||
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
|
||||
|
||||
### 10.2 Main 与契约测试
|
||||
|
||||
- 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。
|
||||
- 旧版本设置迁移后默认显示所有既有应用。
|
||||
- 更新导航偏好不修改魔法笔记启停等无关字段。
|
||||
- 原子写入失败时保留最后一次有效设置。
|
||||
- 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。
|
||||
- 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。
|
||||
- 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。
|
||||
- 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。
|
||||
- `resetIntervalBaseline: true` 时把 CPU 与磁盘 IO 标记为 `warming-up`。
|
||||
- GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。
|
||||
- GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在 `0` 至 `100`。
|
||||
- GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。
|
||||
|
||||
### 10.3 回归范围
|
||||
|
||||
- 工作栏布局恢复和已关闭实例重建。
|
||||
- 本地与托管 SSH 终端目标。
|
||||
- 浏览器无会话、创建中和 Agent 操作中状态。
|
||||
- 设置页未保存离开确认。
|
||||
- 魔法笔记待办数和智能心跳建议数徽标。
|
||||
- 浅色、深色、`220px` 与 `420px` 侧栏宽度。
|
||||
- Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。
|
||||
|
||||
## 11. 实施顺序
|
||||
|
||||
实施范围以 [PRD 分期](./prd.md#8-分期)为准。本节替代旧实施顺序;前文关于四个左侧
|
||||
系统工具按钮、仅持久化隐藏 ID 的契约需在实现前同步,不能直接按旧规格开发。
|
||||
|
||||
1. 同步 PRD 功能要求、User Stories、逻辑、UI 与技术契约,收敛首期应用中心样式。
|
||||
2. 接入现有内置应用页面与最底部应用中心入口,保留独立设置和固定运行记录入口。
|
||||
3. 为常驻显隐和排序设计最小持久化契约,保留已发布的相关设置与用户数据,按保存结果更新导航。
|
||||
4. 接入应用设置的共同界面与既有设置更新路径,先验证魔法笔记多入口状态一致性。
|
||||
5. 验证右侧现有文件、终端、浏览器 Tab;移除计划中的左下角工具快捷条和专用于该快捷条的意图接口工作。
|
||||
6. 单独实现资源快照契约、Main 采集服务、Preload API 与右侧 `resources` 单实例面板。
|
||||
7. 验证键盘、焦点、浅深主题、窄窗口、设置失败、重启持久化和跨平台行为。
|
||||
|
||||
首期使用内置应用清单,不依赖远程商店、插件注册或安装服务。商店目录、推荐、安装与
|
||||
更新机制在后续独立设计中确定;Demo 中的模拟商店不接入首期产品界面。
|
||||
|
||||
## 12. 项目活动汇总
|
||||
|
||||
对应 [FR-14](./prd.md#fr-14-项目活动汇总),按[活动逻辑](./logic-design.md#9-项目活动汇总)
|
||||
派生状态,界面行为见[活动界面](./ui-design.md#10-项目活动汇总)。
|
||||
|
||||
- `conversation-activity.ts` 从 `App.tsx` 的会话、`activeConversationIds`、全部可见
|
||||
`assistantTasks` 和项目元数据派生会话行、全局及项目计数,不新增持久化活动副本。
|
||||
`ProjectActivity` 渲染常驻汇总与项目、会话级联菜单,`ProjectSwitcher` 复用项目计数。
|
||||
- `App` 向 `ProjectActivity` 传入项目元数据、活动行、侧栏可见状态与既有精确跳转回调。
|
||||
菜单按项目 ID 分组并记忆化派生集合;排序仅影响展示,不改聚合状态或持久化数据。
|
||||
- 两层菜单在同一 body Portal 内相接。布局在打开、层级变化、实时数据更新、窗口缩放和
|
||||
滚动时按锚点重算;窄窗口隐藏项目面板并保留返回操作。菜单不调用 Modal 焦点隔离,
|
||||
捕获阶段消费 Escape,防止触发窄侧栏的文档级监听;Tab 关闭后交还正常焦点顺序。
|
||||
菜单关闭与焦点恢复在导航回调之前同步完成。
|
||||
- `role="menu"` 使浮层直接进入共享 `browser-viewport-occlusion.ts` 相交检测。未增加
|
||||
浏览器实例的特殊隐藏条件,也不关闭或重建浏览器会话。
|
||||
- 持久化快照刷新时,同一条仍在流式输出的消息保留本地待审批、待回答信息,持久化终态
|
||||
则清除这些信息。响应成功后对应 Task 立即退出等待状态;响应期间到达的新问题、审批或
|
||||
终态不能被旧响应覆盖。
|
||||
- `AssistantDatabase.listTasks()` 保留最近历史窗口(默认 100 条),额外包含窗口外当前
|
||||
运行或等待处理的可见 Task;`activeVisibleTaskSelect` 与 `toTask` 的有效状态保持一致,
|
||||
待调度运行规范化为 `queued`,不因旧 Task 状态被误纳入实时集合。
|
||||
- `listConversations()` 保留最近 100 条活动会话,同时包含上述 Task 关联的较早活动会话,
|
||||
以及含 `streaming` 消息的较早活动会话。两类列表去重后仍按原时间顺序返回,结果允许超过
|
||||
历史窗口大小,避免后台工作被最近记录挤出。
|
||||
- `openActivityConversation` 按确切 Conversation ID 导航;内存中缺失时刷新持久化会话
|
||||
列表并合并,再查找同一 ID。刷新失败或会话已不存在时显示通知,不回退到项目最新会话。
|
||||
设置离开检查通过后才提交项目、会话及界面清理状态。
|
||||
- 远程恢复成功提示沿用独立的自动消失计时器;活动汇总更新不重置或替代该计时器。
|
||||
|
||||
验证记录见[项目活动级联实施进度](./progress.md)。本次只改 Renderer 展示与交互,远程
|
||||
Runtime、Agent 协议和桌面到 Agent 的执行路径未改变。
|
||||
@@ -0,0 +1,233 @@
|
||||
# 应用导航与系统工具入口 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义入口、设置和轻量资源监控界面。文件、终端和浏览器的内容布局继续遵守助手工作栏
|
||||
文档。
|
||||
|
||||
## 1. 侧栏信息架构
|
||||
|
||||
```text
|
||||
品牌
|
||||
项目选择
|
||||
项目活动汇总(常驻,空闲显示“暂无活动”)
|
||||
新对话
|
||||
搜索
|
||||
|
||||
当前对话
|
||||
魔法笔记 3
|
||||
知识库
|
||||
智能心跳 1
|
||||
运行记录
|
||||
|
||||
最近对话
|
||||
...
|
||||
|
||||
────────────────────────
|
||||
[文件] [终端] [浏览器] [资源] [设置]
|
||||
```
|
||||
|
||||
“当前对话”和“运行记录”属于固定系统入口。三个内置应用位于二者之间。侧栏底部不再显示
|
||||
“本地工作区”和 `platform · arch`;排障信息移至关于或诊断页面。
|
||||
|
||||
## 2. 底部系统工具栏
|
||||
|
||||
底部使用连续平整表面和顶部分隔线,不做成账户卡片或嵌套卡片。按钮顺序固定:
|
||||
|
||||
| 位置 | 图标 | 可见工具提示 | 点击结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `FolderOpen` | 文件 | 聚焦不可关闭的工作区面板 |
|
||||
| 2 | `SquareTerminal` | 终端 | 打开或聚焦当前执行空间终端 |
|
||||
| 3 | `Globe` | 浏览器 | 聚焦当前 Conversation 的匹配浏览器实例 |
|
||||
| 4 | `Activity` | 资源监控 | 打开或聚焦本机资源监控面板 |
|
||||
| 末位 | `Settings` | 设置 | 打开设置页面 |
|
||||
|
||||
图标使用项目现有 Lucide 库。每个按钮采用统一的 `34px × 34px` 点击区域,内部图标建议
|
||||
`16px`。工具栏使用 `8px` 内边距和 `4px` 间隔;设置按钮通过自动空白靠右,不添加第二条
|
||||
视觉分隔线。
|
||||
|
||||
### 2.1 状态
|
||||
|
||||
- 默认:透明背景,图标使用次级文字色。
|
||||
- 悬停:使用弱表面色,不能强于当前选中状态。
|
||||
- 选中:使用强调选中背景,并提高图标对比度。
|
||||
- 键盘焦点:显示统一焦点环,不能被侧栏边缘裁切。
|
||||
- 处理中:创建或激活请求未完成时暂时禁用对应按钮,保留尺寸并提供进行中名称。
|
||||
- 徽标:只用于等待处理、错误或活动状态,不显示无意义的常驻绿色圆点。
|
||||
|
||||
选中状态表达“对应工作栏面板正在显示”,不表达功能已安装或权限已授予。
|
||||
|
||||
## 3. 主导航应用区
|
||||
|
||||
应用入口沿用现有 `nav-item` 行,不使用应用卡片。图标、名称和徽标保持单行:
|
||||
|
||||
```text
|
||||
[图标] 魔法笔记 3
|
||||
[图标] 知识库
|
||||
[图标] 智能心跳 1
|
||||
```
|
||||
|
||||
隐藏某个应用后,其余应用向上补位。运行记录始终位于最后一个可见应用之后,不因应用显隐
|
||||
改变样式或归类为工具。
|
||||
|
||||
设置页打开期间,主侧栏显示最后一次成功保存的导航状态。Switch 更新成功后导航立即变化,
|
||||
失败时保持原导航。
|
||||
|
||||
## 4. 应用与工具设置
|
||||
|
||||
设置中心新增“应用与工具”分类,采用 `standard` PageShell:
|
||||
|
||||
```text
|
||||
应用与工具
|
||||
管理主导航中的应用。系统工具由 GoodBuddy 提供并固定在侧栏底部。
|
||||
|
||||
应用
|
||||
魔法笔记 笔记、待办和评论 [显示在侧边栏 开]
|
||||
知识库 管理和检索知识 [显示在侧边栏 开]
|
||||
智能心跳 查看计划、建议和运行历史 [显示在侧边栏 开]
|
||||
|
||||
系统工具
|
||||
文件 打开当前工作区 内置
|
||||
终端 进入当前执行空间 内置
|
||||
浏览器 打开 GoodBuddy 受控浏览器 内置
|
||||
资源监控 查看本机 CPU、内存和磁盘 IO 内置
|
||||
|
||||
系统入口
|
||||
当前对话、运行记录和设置位置固定,不参与上述配置。
|
||||
```
|
||||
|
||||
应用显隐使用共享 Switch,标签必须是“显示在侧边栏”,不能缩写为“启用”。切换时立即保存,
|
||||
请求期间禁用三个应用显隐 Switch;失败后恢复原值,并在对应设置行显示可重试错误。系统工具
|
||||
使用紧凑只读列表,不渲染禁用的卸载、停用或固定按钮。
|
||||
|
||||
功能已停用时,对应应用行显示“功能已停用”和设置入口,保留显隐偏好但暂时禁用 Switch。
|
||||
|
||||
若某应用自身支持业务启停,应在该应用原有设置中保留“启用功能”开关,并用说明区分:
|
||||
|
||||
- “显示在侧边栏”只控制入口。
|
||||
- “启用功能”控制该功能是否运行。
|
||||
|
||||
## 5. 助手工作栏反馈
|
||||
|
||||
点击系统工具后,右侧工作栏按现有布局回流打开。主内容页面保持不变,侧栏不渲染工具内容。
|
||||
|
||||
- 文件:激活默认存在且不可关闭的“工作区”Tab,焦点进入目录区域或错误修复入口。
|
||||
- 终端:激活匹配终端,焦点进入终端输入区;新建期间保留按钮宽度和进度状态。
|
||||
- 浏览器:激活绑定当前 Conversation 的最近使用“浏览器”Tab;没有匹配实例时创建一个,
|
||||
没有 Conversation 时按产品规则复用最近实例或进入待绑定空状态。
|
||||
- 资源监控:激活“资源监控”Tab,焦点进入指标摘要;首个样本加载后开始绘制历史。
|
||||
|
||||
后台事件可以更新徽标,不能自动展开工作栏或切换当前 Tab。由用户点击快捷按钮触发的打开
|
||||
属于显式操作,可以展开工作栏并切换 Tab。
|
||||
|
||||
### 5.1 资源监控面板
|
||||
|
||||
资源监控使用紧凑单列,不使用指标卡片墙:
|
||||
|
||||
```text
|
||||
资源监控 本机
|
||||
|
||||
CPU 18%
|
||||
[最近 5 分钟折线图]
|
||||
|
||||
内存 9.6 / 32 GB
|
||||
[使用量进度条]
|
||||
|
||||
磁盘 IO 读 12 MB/s · 写 3 MB/s
|
||||
|
||||
GoodBuddy
|
||||
CPU 4% · 内存 620 MB · 桌面进程 7
|
||||
|
||||
更新于 14:32:08
|
||||
```
|
||||
|
||||
- CPU 折线纵轴固定为 `0%` 至 `100%`,不能根据当前低负载动态放大波动。
|
||||
- 内存同时显示使用量、总量和百分比;进度条不代替文字数值。
|
||||
- 磁盘 IO 使用每秒读写字节并按统一单位格式化,不显示累计设备字节数。
|
||||
- GoodBuddy 摘要只显示聚合值,不列 PID、命令行或环境变量。
|
||||
- 首次加载或停止后恢复时,CPU 和磁盘 IO 显示“正在建立采样基线”,不先渲染 `0%`;内存
|
||||
等瞬时指标可以立即显示。
|
||||
- 部分可用时只在对应指标行显示“不可用”和简短原因;全部失败时显示重试入口。
|
||||
- 图表不使用动效平移;`prefers-reduced-motion` 下行为不变。
|
||||
- 面板失去活动状态、工作栏收起或窗口隐藏、最小化时停止更新;返回后重新建立区间指标基线,
|
||||
不填补离开期间的历史。
|
||||
|
||||
## 6. 文案
|
||||
|
||||
固定使用以下名称:
|
||||
|
||||
| 对象 | 文案 |
|
||||
| --- | --- |
|
||||
| 设置分类 | 应用与工具 |
|
||||
| 文件系统工具 | 文件 |
|
||||
| 终端系统工具 | 终端 |
|
||||
| 浏览器系统工具 | 浏览器 |
|
||||
| 浏览器说明 | GoodBuddy 受控浏览器 |
|
||||
| 资源系统工具 | 资源监控 |
|
||||
| 资源范围 | 本机 |
|
||||
| 指标缺失 | 不可用 |
|
||||
| 应用显隐开关 | 显示在侧边栏 |
|
||||
| 内置来源 | 内置 |
|
||||
|
||||
不再使用“本地工作区”作为侧栏底部身份名称。项目和默认工作区的路径继续由项目选择器、页面
|
||||
范围或工作区面板显示。
|
||||
|
||||
## 7. 键盘和无障碍
|
||||
|
||||
- 底部按钮均为独立 `button`,不能嵌套在整行设置按钮中。
|
||||
- Tab 顺序为文件、终端、浏览器、资源监控、设置。
|
||||
- 图标按钮通过 `aria-label` 提供名称,工具提示不能作为唯一可访问名称。
|
||||
- 工作栏打开后按现有焦点策略聚焦目标面板;用户通过固定工作栏开关关闭时,焦点返回该开关。
|
||||
- 选中按钮使用 `aria-pressed` 或等价的可访问状态,不能只依靠背景色。
|
||||
- 徽标的数量或状态合并进按钮可访问名称,例如“终端,1 个等待处理”。
|
||||
- 应用显隐 Switch 支持 Space 切换,保存失败后恢复原值并保留焦点。
|
||||
|
||||
## 8. 响应式
|
||||
|
||||
- 侧栏宽度 `220px` 至 `420px` 时,底部按钮尺寸不随宽度缩放。
|
||||
- 窄窗口继续使用现有侧栏抽屉;工具栏不转为多行。
|
||||
- 空间不足时缩小设置按钮前的弹性空白,不隐藏四个系统工具和设置。
|
||||
- 工具提示不得超出窗口;靠近底边时显示在按钮上方。
|
||||
- 粗指针设备不依赖悬停,按钮的可访问名称和选中反馈保持有效。
|
||||
|
||||
## 9. 未来插件入口
|
||||
|
||||
插件市场交付后另行定义插件区域、固定数量、溢出和权限界面。系统入口与四个系统工具的位置
|
||||
保持保留,不向插件开放。
|
||||
|
||||
## 10. 项目活动汇总
|
||||
|
||||
对应 [FR-14](./prd.md#fr-14-项目活动汇总),状态与计数遵守
|
||||
[活动逻辑](./logic-design.md#9-项目活动汇总)。
|
||||
|
||||
- 全局汇总按钮常驻项目选择器下方,紧凑显示待处理与运行中数量,空闲显示“暂无活动”,
|
||||
右侧显示展开箭头,与下方新建会话保留 `8px` 间距。可访问名称明确数量属于“全项目活动”。
|
||||
- 项目菜单在项目行显示同口径数量,保留“本地项目 / 远程项目 / 远程通道”分组和 SSH Host
|
||||
层级,不因活动状态重排项目。数量为零时不显示标记,连接状态仍独立显示。
|
||||
项目名独占一行,状态数量排列在名称下方,避免窄侧栏中数量挤占名称宽度。
|
||||
- 点击汇总在按钮下方打开项目菜单,只列有活动会话的项目,每项显示项目名和同口径计数。
|
||||
桌面鼠标悬停或点击项目展开右侧会话菜单,右侧空间不足且左侧可容纳时向左展开。两个面板
|
||||
相接,指针离开项目行进入会话层时不收起。展开项目不切换当前会话。
|
||||
- 会话层顶部显示项目名,每行显示完整可换行的会话标题与明确状态文字,待处理会话排在
|
||||
运行中会话之前。项目行保留子菜单箭头,会话行不显示箭头。不展开 Task、Job 或 Run 列表。
|
||||
- 点击会话先关闭菜单并恢复入口焦点,再请求进入该条会话;允许离开当前页面后,
|
||||
选择所属项目、清除搜索和该会话未读标记、清除任务选择与会话操作菜单,窄窗口同时关闭侧栏。
|
||||
设置页沿用未保存修改的离开检查,取消离开时保留原页面。
|
||||
- 菜单挂载到 body,使用 `menu`、`menuitem`、`aria-haspopup`、`aria-expanded` 与控制关系,
|
||||
不隔离背景、不增加遮罩。原生浏览器相交遮挡沿用共享机制。
|
||||
- 打开后焦点进入首个项目;上下方向键循环选择当前层项目或会话,Home、End 到首尾,
|
||||
Enter、Space 激活,右方向键进入会话层。左方向键或 Escape 返回项目层并恢复对应项目
|
||||
焦点;顶层 Escape 关闭并恢复汇总焦点,不继续关闭窄侧栏。Tab 关闭菜单后继续正常焦点
|
||||
顺序;点击外部、焦点离开、再次点击入口或隐藏侧栏也关闭菜单。
|
||||
- 单面板宽度为 `264px`,窗口小于 `620px` 时就地进入会话层并显示“返回项目列表”,
|
||||
触控无需悬停。浮层距窗口边缘至少 `16px`,每层最大高度为 `480px`,短窗口进一步限制
|
||||
高度并独立滚动。活动全部结束时,已打开菜单显示“暂无运行中或待处理会话。”。
|
||||
@@ -0,0 +1,265 @@
|
||||
# 应用导航与系统工具入口 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 在窄窗口使用快捷栏
|
||||
|
||||
作为小屏用户,我希望侧栏抽屉中的工具按钮保持可操作。
|
||||
|
||||
验收:
|
||||
|
||||
- 工具按钮保持单行且不重叠
|
||||
- 不因宽度不足隐藏文件、终端、浏览器、资源监控或设置
|
||||
- 触控点击区域符合统一界面规范
|
||||
- 关闭侧栏抽屉后焦点返回原触发按钮
|
||||
|
||||
## 6. Epic E:项目活动级联
|
||||
|
||||
### US-E1 从摘要进入确切会话
|
||||
|
||||
对应 [FR-14](./prd.md#fr-14-项目活动汇总)。用户在当前项目工作时,可从项目选择器下方
|
||||
查看跨项目活动,先定位项目,再进入需要处理的会话。
|
||||
|
||||
- Given 多个项目存在运行中和待处理会话,当前搜索仅匹配当前项目
|
||||
- When 打开全项目活动并悬停或点击另一个项目
|
||||
- Then 项目顺序保持稳定,该项目会话按待处理优先展示,当前页面和会话保持原选择
|
||||
- When 点击会话
|
||||
- Then 进入该确切 Conversation ID,清除搜索、任务选择和会话菜单,窄窗口关闭侧栏
|
||||
- Given 设置页有未保存修改
|
||||
- Then 会话跳转仍需通过既有离开检查,取消离开保留设置草稿和原项目
|
||||
|
||||
### US-E2 键盘、窄窗口与实时结束
|
||||
|
||||
- Given 菜单已打开
|
||||
- Then 上下方向键与 Home、End 选择当前层,右方向键或 Enter 进入会话层,左方向键或
|
||||
Escape 返回;再次 Escape 关闭活动菜单并恢复摘要焦点,窄侧栏保持打开
|
||||
- Given 窗口小于 `620px`
|
||||
- Then 点击项目后仅显示会话层,可通过返回按钮回到项目列表;长列表可滚动到最后一项
|
||||
- Given 鼠标从项目行移向相接的会话面板
|
||||
- Then 子菜单保持打开,点击会话正常导航
|
||||
- Given 当前项目的活动全部结束
|
||||
- Then 返回项目层并恢复可用焦点;全局无活动时摘要显示“暂无活动”,已打开菜单显示空状态
|
||||
- When 点击外部、焦点离开、按 Tab 或隐藏侧栏
|
||||
- Then 菜单关闭,后台新活动不自动重新展开
|
||||
@@ -7,7 +7,11 @@
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [通用助手工作栏与执行空间 PRD](./prd.md) | 工作栏整体产品范围、执行空间和各工作面板归属 |
|
||||
| [多终端页签 PRD](./terminal-tabs-prd.md) | 多终端 Tab、目标选择、关闭确认和终端生命周期 |
|
||||
| [多终端页签 PRD](./terminal-tabs-prd.md) | 工作栏应用的固定、可关闭、单实例和多实例策略,以及多终端生命周期 |
|
||||
| [浏览器多实例技术设计](./browser-tabs-technical-design.md) | 浏览器 Tab 身份、Conversation 绑定、IPC、BrowserService 和 MCP 路由 |
|
||||
| [实现与验证进度](./progress.md) | 工作区与 Runtime 相关改动的实现事实、验证证据与剩余阻塞 |
|
||||
| [执行记录存储与升级回收](./execution-history-storage.md) | 子任务过程差量存储、远程去重、旧库转换、空间回收及升级界面 |
|
||||
| [Runtime 交互边界](./runtime-interactions.md) | 原生权限、结构化问答、后台拒绝、终端交互、托管 Runtime 并行与输出及消息底部请求状态的适配范围 |
|
||||
|
||||
当前尚未拆出独立 User Stories、功能逻辑、UI 设计、技术设计和进度文档;相关内容仍保留在
|
||||
两份现有 PRD 中,后续实际维护该功能时按职责拆分。
|
||||
当前尚未拆出独立 User Stories、功能逻辑和 UI 设计文档;相关内容仍保留在两份现有 PRD
|
||||
中。浏览器多实例涉及跨进程身份和工具路由,单独使用技术设计文档维护。
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
# 浏览器多实例技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 基础多实例已实施,Agent 多 Tab 工具待需求 |
|
||||
| 版本 | 1.0 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 关联 PRD | [通用助手工作栏与执行空间 PRD](./prd.md) |
|
||||
| 页签策略 | [助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) |
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
浏览器改为可关闭的多实例工作栏应用。每个工作栏浏览器实例对应一个独立 Browser Tab,拥有
|
||||
独立页面、导航状态、操作队列、画面状态和元素引用空间。同一 Conversation 下的多个 Tab
|
||||
共享隔离存储与代理上下文,使登录状态符合常规浏览器预期。
|
||||
|
||||
本设计不把多个工作栏 Tab 实现为同一页面的重复入口,不允许用户切换 Tab 后改变正在执行的
|
||||
Agent 工具目标,也不在首期要求模型主动管理多个 Tab。
|
||||
|
||||
## 2. 改造前限制
|
||||
|
||||
改造前的链路以 `conversationId` 作为唯一浏览器身份:
|
||||
|
||||
- Renderer 只保存每个 Conversation 的一份 `BrowserLiveState`。
|
||||
- Browser IPC、Preload API 和 `BrowserService` 操作只携带 `conversationId`。
|
||||
- Main 每个 Conversation 只创建一个 Session、一个 `WebContentsView` 和一个 CDP Driver。
|
||||
- 请求级 MCP 能力只保存 `browserConversationId`。
|
||||
- snapshot 元素 `ref` 只在一个 Driver 的引用空间内有效。
|
||||
|
||||
因此只把工作栏注册策略改成 `multiple` 会导致多个 Tab 控制同一页面,并产生 viewport、清理和
|
||||
元素引用竞态,不能作为可交付实现。
|
||||
|
||||
## 3. 身份与所有权
|
||||
|
||||
```ts
|
||||
type BrowserTabId = string
|
||||
|
||||
type ConversationBrowserContext = {
|
||||
conversationId: string
|
||||
partitionId: string
|
||||
tabs: Map<BrowserTabId, BrowserTab>
|
||||
primaryTabId?: BrowserTabId
|
||||
}
|
||||
|
||||
type BrowserTab = {
|
||||
id: BrowserTabId
|
||||
conversationId: string
|
||||
view: WebContentsView
|
||||
driver: CdpBrowserDriver
|
||||
state: BrowserLiveState
|
||||
}
|
||||
```
|
||||
|
||||
- Conversation Browser Context 拥有隔离 partition、代理配置和全部 Tab。
|
||||
- Browser Tab 拥有页面、Driver、串行操作队列、snapshot 代次、idle 状态和可见 viewport。
|
||||
- `BrowserTabId` 是 Main 签发的不透明 ID。Renderer 和 Runtime 不能自行选择或复用。
|
||||
- 工作栏实例保存公开的逻辑实例 ID 和 Conversation 绑定;进程重启后不恢复旧
|
||||
`WebContentsView`,首次使用时由 Main 为该逻辑实例创建新的 Browser Tab。
|
||||
- 删除 Conversation 时关闭该 Context 下全部 Tab;关闭一个工作栏浏览器实例只关闭对应
|
||||
Browser Tab,不得调用 Conversation 级 Runtime 释放路径。
|
||||
|
||||
## 4. Renderer 与 viewport
|
||||
|
||||
Renderer 状态改为:
|
||||
|
||||
```ts
|
||||
Record<ConversationId, Record<BrowserTabId, BrowserLiveState>>
|
||||
```
|
||||
|
||||
每个浏览器工作栏实例持有自己的 `browserTabId`。只有当前可见实例可以提交 viewport。每次
|
||||
获得可见权时生成新的 UUID `leaseToken`;提交请求携带 `browserTabId` 和 token,清理请求必须
|
||||
回传同一个 token。Main 只接受当前 token 的清理,避免已隐藏实例的延迟 cleanup 隐藏新实例,
|
||||
也避免 Renderer 重载后从零开始的数字序列无法覆盖旧租约。
|
||||
|
||||
非活动 Browser Tab 保持页面和状态,但隐藏 `WebContentsView` 并停止非必要画面采集。工作栏
|
||||
收起、切换主页面和窗口隐藏时采用相同规则,不销毁 Tab。
|
||||
普通导航、快照与页面操作只同步轻量导航状态,不自动截图、等待画面或传输 JPEG;只有显式
|
||||
`browser_screenshot` 调用生成图片。
|
||||
|
||||
`BrowserViewport` 统一观察 body Portal 的插入、移除和可见属性变化,以及浮层尺寸、
|
||||
窗口尺寸、滚动和过渡结束。`browser-viewport-occlusion.ts` 按共享语义识别
|
||||
`aria-modal="true"`、dialog、alertdialog、menu 和应用通知:可见应用级 Modal 始终释放
|
||||
viewport,其余浮层只在与浏览器区域相交时释放。隐藏节点和隐藏祖先不阻塞浏览器。
|
||||
最后一个遮挡消失后,只有当前活动实例恢复同一 viewport 租约;网页、历史和页面状态不变。
|
||||
终端关闭确认复用此机制,不再维护单独的隐藏条件。页面无需逐个登记弹窗,也不能靠提高
|
||||
CSS `z-index` 覆盖原生子视图。此逻辑仅涉及 Desktop Renderer 的显示租约,不改变
|
||||
Runtime、MCP 或远端 Agent 的请求、导航与生命周期。
|
||||
|
||||
浏览器应用声明 `visibleAcrossContextSwitches: true`,打开的 Tab 跨项目、会话切换继续显示。
|
||||
切换活动 Conversation 不改变当前选中的浏览器或其 `targetRef.conversationId`;用户在会话 B
|
||||
查看会话 A 的浏览器时,viewport 和手动导航仍指向 A 的原 Tab。新建实例绑定 B。仅未绑定的
|
||||
默认或恢复实例可在首次使用时取得当前会话绑定。
|
||||
|
||||
Tab 的跨会话可见性与 MCP 授权分开处理。B 的请求不能选择属于 A 的可见 Tab;A 已取得的
|
||||
capability 在用户切到 B 后仍固定路由至 A 的原 Tab;使用租约不阻止用户关闭该 Tab。
|
||||
内部点击实例只同步应用类型,不再次按当前 Conversation 改选实例。Main 状态携带对应的
|
||||
`workbarInstanceId`,Renderer 据此绑定请求创建的同一 Tab,而不是另建空白页。收到 `stopped`
|
||||
后清除该实例的旧 Tab 绑定,下一次使用通过原逻辑实例重新创建,保留地址草稿。
|
||||
刷新可用性读取 Main 的已提交 URL,不使用地址草稿或仅凭 `sessionActive` 判断。
|
||||
侧栏操作错误以当前工作栏实例和面板展开状态为作用域;切换或关闭时清空,旧作用域的
|
||||
异步回调不再写入错误,即使用户随后返回同一实例。
|
||||
请求创建的实例也遵守现有 32 个工作栏页签上限;满额时保留 Main 页面及其状态,并就地提示
|
||||
关闭一个可关闭页签。释放位置后再绑定该页面,不写入超限布局或丢弃 Agent 页面。
|
||||
|
||||
## 5. IPC 与服务接口
|
||||
|
||||
共享契约增加有界操作:
|
||||
|
||||
```ts
|
||||
browser.createTab({ conversationId, workbarInstanceId })
|
||||
browser.listTabs({ conversationId })
|
||||
browser.closeTab({ conversationId, tabId })
|
||||
browser.setViewport({ conversationId, tabId, leaseToken, bounds })
|
||||
browser.navigate({ conversationId, tabId, url })
|
||||
browser.back({ conversationId, tabId })
|
||||
browser.reload({ conversationId, tabId })
|
||||
browser.stopLoading({ conversationId, tabId })
|
||||
```
|
||||
|
||||
Main 必须验证 `tabId` 属于同一 `conversationId` 和当前窗口。BrowserService 将现有
|
||||
Conversation 级 slot 拆为 Context 与 Tab 两层;同一 Tab 内操作继续串行,不同 Tab 可以并行,
|
||||
但仍受窗口级和 Conversation 级有界数量限制。
|
||||
|
||||
现有 Conversation 级 `stop/release` 保留为销毁完整浏览器上下文的操作,不能用作关闭单个
|
||||
Browser Tab。
|
||||
|
||||
## 6. Agent 与 MCP 路由
|
||||
|
||||
### 6.1 兼容阶段
|
||||
|
||||
第一阶段不修改模型可见的现有浏览器工具参数。模型请求开始时,Main 按以下顺序选择并冻结
|
||||
`boundBrowserTabId`:
|
||||
|
||||
1. 当前可见且绑定该 Conversation 的 Browser Tab。
|
||||
2. 该 Conversation 的 `primaryTabId`。
|
||||
3. 预留 primary Browser Tab 身份,首次实际浏览器操作时才创建页面与 Conversation Context。
|
||||
|
||||
未使用浏览器的请求只持有随请求释放的内存预留,不创建 Chromium 资源、不触发浏览器 UI、
|
||||
也不占用活动浏览器会话名额。物化后使用同一 Tab 身份和工作栏实例归属;取消、能力撤销及
|
||||
服务关闭释放未使用的预留,不保存到磁盘。编程 Subagent 继承父请求的 Browser Tab 与浏览器
|
||||
Conversation 归属,不新建租约或把页面重新归属到子级对话。
|
||||
|
||||
直连模型上下文和请求级 MCP capability 同时保存 `conversationId` 与
|
||||
`boundBrowserTabId`。请求执行期间,用户切换、创建或关闭其他工作栏 Tab 不改变工具目标;
|
||||
用户可直接关闭模型正在使用的 Tab。关闭释放该 Tab 的页面和使用租约,中止其浏览器操作,
|
||||
不取消整个模型请求、不撤销 MCP capability,也不关闭同级 Tab。系统不得把进行中的操作改投其他 Tab。
|
||||
|
||||
关闭后,快照等非导航工具返回“浏览器标签页已关闭,请调用 browser_navigate 打开新标签页”,
|
||||
不自动创建页面或使用现有 primary/sibling。只有显式且参数有效的 `browser_navigate` 才创建
|
||||
专用替代 Tab 并绑定后续工具;旧 Tab 身份与元素引用不复用。Gateway 与直连 Provider 均隔离
|
||||
单次工具取消信号、Tab 使用租约信号和请求信号,取消一次工具调用不终止后续调用。
|
||||
|
||||
该阶段 MCP transport、endpoint 和 token 生命周期不变,只扩展服务端 capability 内容和内部
|
||||
路由。工具调用使用当前请求绑定的 Tab,只有上述显式导航恢复会更换绑定。
|
||||
|
||||
### 6.2 Agent 多 Tab 阶段
|
||||
|
||||
只有明确需要 Agent 管理多个页面时,才增加:
|
||||
|
||||
- `browser_tab_list`
|
||||
- `browser_tab_open`
|
||||
- `browser_tab_close`
|
||||
|
||||
导航、snapshot、back 和 screenshot 增加可选 `tabId`;省略时使用请求绑定的默认 Tab。
|
||||
`browser_snapshot` 返回 `tabId` 和 snapshot 代次。click、type 和 select 必须校验 capability、
|
||||
`tabId`、引用代次和元素 `ref` 属于同一 Driver,禁止跨 Tab 猜测或解析引用。
|
||||
|
||||
## 7. 工作栏创建与快捷入口
|
||||
|
||||
- 从“+”选择浏览器时始终创建新工作栏实例,并默认绑定当前 Conversation。
|
||||
- 没有活动 Conversation 时创建待绑定实例,不创建无归属 Main Browser Context。
|
||||
- 主侧栏浏览器快捷按钮优先激活绑定当前 Conversation 的最近使用实例;没有匹配实例时创建
|
||||
一个。没有活动 Conversation 时激活最近使用的浏览器实例;完全不存在时创建待绑定实例。
|
||||
- New browser instances use a numbered browser label followed by the active Conversation title, when
|
||||
available. The number avoids existing titles; restoring the layout preserves each saved title.
|
||||
The address bar shows only that instance's page URL. Binding a numbered instance does not overwrite
|
||||
its title; the default unbound browser receives the Conversation label when first bound.
|
||||
- 关闭工作栏 Browser Tab 只释放对应 Browser Tab。关闭浏览器上下文是独立危险操作,需要
|
||||
明确说明会影响该 Conversation 下的全部 Tab 和 Agent 浏览器状态。
|
||||
|
||||
## 8. 持久化与恢复
|
||||
|
||||
`createTab` reuses a live tab only for the same Conversation and workbar instance ID. A new workbar
|
||||
instance can adopt the primary tab only when its request creates the Conversation context. An existing
|
||||
primary, including one opened by an Agent, is not adopted by a new instance. New sibling tabs start at
|
||||
`about:blank` with no navigation history and share the Conversation's cookie partition.
|
||||
|
||||
持久化工作栏实例 ID、顺序、自定义标题和公开 Conversation 绑定,不持久化页面进程、CDP
|
||||
引用、snapshot、viewport lease、Cookie、Token 或 Main `BrowserTabId`。应用重启后实例显示
|
||||
未启动状态,展开浏览器面板或 Agent 首次使用时按原绑定创建新 Tab。
|
||||
|
||||
绑定的 Conversation 已删除或不可见时保留实例并显示目标失效,不自动绑定当前 Conversation。
|
||||
|
||||
## 9. 限制与验收
|
||||
|
||||
- 分别限制每个 Conversation 的 Tab 数和每个窗口的总活动 Tab 数;达到上限时在创建前反馈。
|
||||
- 两个 Tab 的地址、历史、加载状态、截图和元素引用互不覆盖。
|
||||
- 同一 Conversation 的 Tab 共享登录状态,不同 Conversation 保持隔离。
|
||||
- 延迟 viewport cleanup 不能隐藏另一个已激活 Tab。
|
||||
- 一个 Tab 的元素 `ref` 不能在另一个 Tab 中执行。
|
||||
- 用户切换工作栏 Tab 不改变进行中模型请求的 MCP 浏览器目标。
|
||||
- 关闭一个 Tab 不释放 Conversation Runtime,也不关闭同一 Conversation 的其他 Tab。
|
||||
- 关闭已租用 Tab 后,同一 MCP 会话仍可列出工具;旧目标快照报错且不创建页面,显式导航创建
|
||||
不同身份的替代 Tab,同级页面、导航状态与其他 Conversation 的可见 viewport 保持不变。
|
||||
- 旧版无 `tabId` 的模型工具调用仍操作请求绑定的默认 Tab。
|
||||
|
||||
## 10. 实施顺序
|
||||
|
||||
1. 引入 Browser Context 与 Browser Tab 两层身份,拆分 partition 和页面生命周期。
|
||||
2. 扩展 Shared、Preload、IPC 与 BrowserService 的 `tabId` 和 viewport `leaseToken`。
|
||||
3. 改造 Renderer 状态与工作栏实例,开放浏览器 `multiple` 策略和单 Tab 关闭。
|
||||
4. 将直连模型与 MCP capability 绑定到请求开始时确定的 Browser Tab。
|
||||
5. 完成跨 Tab 引用隔离、并行队列、共享 Cookie、关闭和恢复测试。
|
||||
6. 有明确 Agent 多页面需求后再开放模型可见的 Tab 管理工具。
|
||||
@@ -0,0 +1,64 @@
|
||||
# 执行记录存储与升级回收
|
||||
|
||||
通用迁移、备份、性能测量和发布要求见
|
||||
[数据库迁移与大数据升级指南](../../development/database-migrations.md)。
|
||||
|
||||
## 用户行为
|
||||
|
||||
子任务仍实时显示完整的文字、思考、工具过程和最终结果。运行时间较长、工具输出较多时,
|
||||
数据库不再为每个流式更新重复保存此前的整份过程;本机和托管 SSH OpenCode 使用相同
|
||||
的桌面存储机制。此修复不关闭子任务、不截短已有过程,也不增加 Execute 权限门槛。
|
||||
|
||||
已有数据库在首次升级时自动处理。启动页铺满窗口,显示处理数量、全宽进度条和近期
|
||||
处理速度;处理时间取决于历史数据量与磁盘速度,不承诺固定几分钟。用户可以退出,
|
||||
下次启动重新检查并继续转换未完成的数据;完成后回收空闲页并进入应用。失败时留在
|
||||
该页提供重试与退出,磁盘空间不足会明确说明,不删除聊天来腾出空间。
|
||||
|
||||
## 存储规则
|
||||
|
||||
- `messages.metadata_json` 继续保存供会话显示的完整子任务过程。
|
||||
- `task_events` 的 `subagent` 内容由共享 `SubagentProgressStorage` 编码。
|
||||
`progressUpdates` 只保存文字追加、变更的块,或确有删除/重排时的完整重置。
|
||||
完全相同的过程使用空更新数组。工具字段按值比较,缺省字段与 `undefined` 等价,
|
||||
不因 JSON 省略空字段而重复写入整个工具结果;空字符串与实际输出/错误变化仍保留。
|
||||
其他事件内容不改变。
|
||||
- 每个任务内按事件 ID 顺序、按 `childTaskId` 独立还原过程。事件 ID、顺序、状态、
|
||||
最终输出、错误、时间及远程来源字段均保留,不按天数裁剪或删除旧事件。
|
||||
- Runtime、Agent transcript、Preload 和 Renderer 的公共 `SubagentEvent` 仍传完整
|
||||
`progress`;差量表示只存在于桌面数据库内部,不要求更新远端 Agent 协议。
|
||||
- 远程消息投影与事件插入仍共用 SQLite 事务。重放时还原已保存内容进行等值校验,
|
||||
相同来源重复事件不重复投影,不同内容继续报冲突。内存过程缓存通过已提交事件 ID
|
||||
核对;事务回滚或重新打开后从数据库重建,不保存独立快照或回收日志。
|
||||
- 存储层最多缓存 16 个任务;收到任务完成、错误、取消或中断的终态记录后立即移除该任务
|
||||
的缓存引用,不影响其他会话。若事务随后回滚或仍收到该任务的后续事件,按已提交记录重建。
|
||||
释放引用不等于进程 RSS 立即下降,垃圾回收和 Runtime 自身会话缓存另按生命周期回收。
|
||||
|
||||
## 旧库升级
|
||||
|
||||
桌面在打开业务数据库之前启动独立 Node 工作线程,使用同一套编码和还原函数转换旧库。
|
||||
按任务有序读取,每批最多 32 条,提交后报告进度;SQLite 保留事务原子性及正常 WAL
|
||||
自动 checkpoint,每个任务结束时截断 WAL。读取缓存为 64 MiB,避免 1 MiB 级历史
|
||||
快照在过小缓存中反复读取;不以关闭事务或同步落盘换取速度。
|
||||
|
||||
转换可以混合读取旧完整记录与已经转换的记录,也会重新编码 schema 34 中重复的工具块
|
||||
更新。因此退出、磁盘写入失败后可以重新扫描并继续;已是相同 JSON 的记录不重复更新,
|
||||
不建立额外的恢复状态表。最后运行 SQLite `VACUUM` 回收已释放的空间,而非
|
||||
在转换前复制整份膨胀内容。回收仍需要额外可用磁盘空间,失败保留已提交转换并允许重试。
|
||||
|
||||
当前源码的业务初始化随后将数据库升级为 schema 35;schema 34 用户也会执行本次整理,
|
||||
成功后后续启动直接跳过。继续复用 schema 34 的子任务事件部分索引和同一差量表示。
|
||||
低于该 schema 的旧版客户端不能重新打开已经升级的数据库。需要回退客户端时,必须使用
|
||||
升级前备份,不要修改 `user_version` 强行绕过版本检查。
|
||||
|
||||
## 验证要求
|
||||
|
||||
- 使用可还原的合成过程验证文字追加、内容修订、块重排、清空和多次打开。
|
||||
- 工具 fixture 必须含生产对象会携带的 `undefined` 可选字段,并经过实际 JSON 写入/重读。
|
||||
同时验证旧完整事件和 schema 34 差量事件的整理、混合重试及无变化记录不被重写。
|
||||
- 验证远程重复事件、冲突事件,以及消息更新失败时事件和过程缓存的事务回滚。
|
||||
- 在真实旧库的已核验备份副本上运行 portable 的正常启动入口,核对事件逐条等价、
|
||||
其他用户表内容、数据库完整性和实际文件大小,不能只统计空闲页或只测试内存 fixture。
|
||||
- 同时验证真实本地 Runtime 和共享 Linux Host 的桌面持久化路径。真实场景必须让主代理
|
||||
与子代理按正常任务需要读取、修改文件和执行测试,不能用“子代理不使用工具”的纯文字
|
||||
场景代替工具存储验收;记录实际请求数、有效变化、重复工具块及还原结果。具体证据见
|
||||
[实现与验证进度](./progress.md)。
|
||||
@@ -36,8 +36,8 @@ Runtime 统一抽象为“执行空间”。
|
||||
### 2.1 助手工作栏
|
||||
|
||||
助手工作栏是 GoodBuddy 中始终可访问的应用级工具容器。它提供稳定能力目录,用户从中
|
||||
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器和成果
|
||||
面板实例。稳定的是能力的可发现性,不是八个同时占据界面的固定面板。
|
||||
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器、资源监控、
|
||||
成果和文档面板实例。稳定的是能力的可发现性,不是十个同时占据界面的固定面板。
|
||||
|
||||
工作栏不是:
|
||||
|
||||
@@ -83,22 +83,26 @@ 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 和主机帮助新面板实例初始定位,但这些上下文不是
|
||||
使用门槛:
|
||||
|
||||
@@ -108,6 +112,7 @@ Runtime
|
||||
使用本机 Home。
|
||||
- 工作区默认显示当前项目目录,用户可以打开其他本机目录或远程目录。
|
||||
- 成果默认使用当前范围,用户可以切换到项目、全局或其他允许范围。
|
||||
- 文档创建时固定到用户明确打开的文件和 ShareServer 编辑会话,不提供跟随模式。
|
||||
- 下一次请求的附件和知识库由当前 Composer 展示与管理,不作为工作栏能力。历史执行上下文
|
||||
随关联 Task 查看;记忆功能实现后也归入任务中心,而不是新增独立上下文面板。
|
||||
|
||||
@@ -133,7 +138,7 @@ Runtime
|
||||
稳定能力目录和已打开面板必须准确呈现能力差异:
|
||||
|
||||
- 当前 Runtime 不支持后台 Job 时,Runtime 能力仍可发现;打开后说明当前可监督的内容。
|
||||
- 当前执行空间没有 Git 仓库时,工作区文件功能仍可使用,Git 区域显示不可用原因。
|
||||
- 当前执行空间没有 Git 仓库时,工作区文件功能仍可使用,仅确认是 Git 仓库后显示 Git 工作区入口。
|
||||
- 没有活动进程时,进程面板提供创建终端或启动 Runtime 的入口。
|
||||
- 没有项目时,终端使用当前用户本机 Home;工作区等允许选择路径的能力仍必须先取得有界
|
||||
临时 Execution Space Grant,不能把可选路径直接传给文件、Git、Process 或 Runtime。
|
||||
@@ -168,7 +173,7 @@ Runtime
|
||||
- 建立统一范围、执行空间、生命周期、成果和控制契约。
|
||||
- 复用现有 Project、Conversation、Task、Artifact、Activity 和 Approval 数据。
|
||||
- 保持 Renderer 无任意文件、进程、PTY、SSH 或 Electron API 能力。
|
||||
- 保持 Ask 只读、Execute 审批、取消、超时、输出边界和活动审计。
|
||||
- 保持 Ask 只读、Execute 账号完整授权、取消、超时、输出边界和活动审计。
|
||||
- 为本机与远程能力提供一致 UI,同时准确表达能力差异。
|
||||
|
||||
## 5. 非目标
|
||||
@@ -202,23 +207,29 @@ Runtime
|
||||
|
||||
### 6.2 能力目录与面板实例
|
||||
|
||||
能力目录不等于同时打开八个面板。当前产品采用“已打开实例 Tab + 持久新增按钮”的工作栏
|
||||
能力目录不等于同时打开十个面板。当前产品采用“已打开实例 Tab + 持久新增按钮”的工作栏
|
||||
模型:Tab 行只显示已打开实例,行末“+”始终可见;点击后在工作栏内容区打开稳定能力目录,
|
||||
用户再创建或激活应用实例。详细交互以
|
||||
[助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 为准。
|
||||
|
||||
- 每项始终显示稳定图标,并提供可见标签或可持续查看的工具提示。
|
||||
- 已打开实例使用 `tablist`、`tab`、`tabpanel`;“+”是位于 `tablist` 外的普通命令按钮。
|
||||
- 应用注册表声明 `single` 或 `multiple`。重复选择单实例应用时聚焦已有实例;选择多实例
|
||||
应用时每次创建新实例。
|
||||
- 支持方向键、Home、End、Enter、Space、关闭面板和正确焦点恢复。
|
||||
- 应用注册表分别声明实例策略、默认打开、必须存在、是否可关闭和是否允许排序。`single` 或
|
||||
`multiple` 只控制实例数量;`required` 控制布局修复,不能兼作关闭或排序策略。
|
||||
- 在工作栏“+”目录中重复选择单实例应用时聚焦已有实例;选择浏览器、终端等多实例应用时
|
||||
每次创建新实例。主侧栏系统工具快捷入口按当前上下文聚焦匹配实例,不能因连续点击无意
|
||||
创建重复实例。
|
||||
- 支持方向键、Home、End、Enter、Space、可关闭面板的关闭命令和正确焦点恢复。不可关闭
|
||||
实例不渲染关闭按钮,也不响应关闭快捷命令。
|
||||
- 徽标显示未解决数量、等待审批或失败状态,并同时提供文字或可访问名称。
|
||||
- 用户调整目录顺序、打开实例、停靠位置和尺寸后持久化;关闭实例后能力仍可从目录重新打开。
|
||||
- 用户调整目录顺序、打开实例、停靠位置和尺寸后持久化;关闭可关闭实例后能力仍可从目录
|
||||
重新打开。布局加载和恢复默认布局必须补回缺失的任务中心与工作区实例。
|
||||
- 默认布局只恢复经过产品确认的少量常用面板,不自动打开全部标准能力。
|
||||
- 每个实例显示稳定实例 ID、能力名称、跟随或固定状态与当前目标;同能力多实例不能只靠位置区分。
|
||||
|
||||
从当前聊天右栏迁移时,Task Center、工作区、浏览器和成果作为默认打开的单实例应用保留;
|
||||
终端不默认打开,由用户通过“+”创建,并从首期开始支持多实例。Tab 必须单行横向滚动,
|
||||
从当前聊天右栏迁移时,Task Center 和工作区作为默认打开、单实例且不可关闭的基础应用保留;
|
||||
浏览器保留一个默认打开实例,但注册为可关闭的多实例应用;成果保持默认打开、单实例且可
|
||||
关闭;终端不默认打开,由用户通过“+”创建,并从首期开始支持多实例。Tab 必须单行横向滚动,
|
||||
不能自动隐藏或缩写到不可辨认,“+”不能随滚动消失。Task Center 继续作为 Task 的现有
|
||||
入口;审批定位到所属 Task 或 Runtime。智能心跳不作为工作栏页签,其报告、建议、历史和
|
||||
完整配置统一归属“智能心跳”菜单入口。当前阶段不新增独立自动化中心。
|
||||
@@ -256,8 +267,13 @@ Task Center 保留为工作栏中的稳定入口,并在现有基础上适度
|
||||
活动记录和成果查看器中,不在窄栏复制,也不呈现 Job/Run 树。
|
||||
- 智能心跳的报告、建议、历史和配置不进入任务中心。
|
||||
|
||||
任务中心是单例索引,不使用其他能力的“跟随 / 固定目标”多实例模型。后台状态可以更新
|
||||
徽标和排序,但不能自动打开面板或抢占用户当前工作。
|
||||
任务中心是应用级单例,默认存在且不可关闭。它的数据范围默认跟随当前项目,并在标题或范围
|
||||
控件中明确显示项目;用户可以切换到“全局任务”或“全部项目”,该选择只改变同一实例的列表
|
||||
范围,不创建第二个任务中心 Tab。切换回“跟随当前项目”后,项目变化立即刷新列表。后台状态
|
||||
可以更新徽标和排序,但不能抢占用户当前工作。
|
||||
|
||||
当前实现已按活动项目过滤 Scheduled Task 和审批,并提供“当前项目”“全局任务”“全部项目”
|
||||
范围控件。
|
||||
|
||||
主侧栏最近会话为关联 Task 提供轻量入口:父会话行只显示行首展开按钮,Task 子项使用
|
||||
任务图标和本地化摘要,展开层级只到 Task;点击 Task 子项打开同一 Conversation 并定位。
|
||||
@@ -352,6 +368,10 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
|
||||
|
||||
### 7.6 工作区
|
||||
|
||||
工作区是默认存在、单实例且不可关闭的基础应用。它默认跟随当前项目;项目或目录不可用时
|
||||
保留同一 Tab 并显示修复入口,不能通过关闭 Tab 隐藏范围错误。任务中心固定在第一位,工作区
|
||||
固定在第二位,恢复布局时保持此顺序。
|
||||
|
||||
工作区面板允许用户选择:
|
||||
|
||||
- 当前项目目录。
|
||||
@@ -366,15 +386,85 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
|
||||
- 显式打开、下载副本或在终端中打开。
|
||||
- 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、有界画面和错误。
|
||||
- 通过顶部浏览器工具栏返回、刷新或停止加载、编辑地址并前往。
|
||||
- 进入明确的交互模式或关闭浏览器。
|
||||
@@ -383,10 +473,19 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
|
||||
页面加载期间原位切换为“停止加载”;“关闭浏览器”是清楚独立于导航的会话生命周期操作,
|
||||
必须与“停止加载”在文案和视觉上明确区分。现有“停止浏览器”语义统一改名为“关闭浏览器”。
|
||||
|
||||
浏览器面板和 Agent 操作同一条按 Conversation 归属的浏览器会话,不创建用户预览专用的
|
||||
第二个页面。用户执行前往、返回或刷新会有意改变 Agent 下一步读取和操作的页面。所有
|
||||
前往、返回和刷新请求都进入 `BrowserService`,并与 Agent 发起的浏览器操作串行执行;不在
|
||||
Renderer 建立第二套导航队列、抢占规则或恢复状态机。
|
||||
新建或重新创建的空白标签页尚无已提交 URL 时禁用刷新;只输入地址草稿不会启用刷新。
|
||||
首次导航期间仍可停止加载。操作错误仅在当前面板本次打开期间显示,切换面板、关闭当前
|
||||
页签或收起工作栏后清除,已离开面板的异步失败不得重新显示旧提示。
|
||||
|
||||
同一 Conversation 下的 Browser Tab 共享隔离存储和代理上下文,但每个 Tab 拥有独立页面、
|
||||
导航状态、操作队列和元素引用空间。模型请求开始时绑定一个确定的 Browser Tab;用户切换
|
||||
工作栏实例不能在请求途中改变 Agent 的工具目标。用户在该实例执行前往、返回或刷新会有意
|
||||
改变绑定到同一 Tab 的 Agent 后续操作。所有请求都进入 `BrowserService`;Renderer 不建立
|
||||
第二套导航队列、抢占规则或恢复状态机。
|
||||
|
||||
用户可关闭 Agent 正在使用的标签页,无需先取消整轮请求。关闭只中止该页的浏览器操作,
|
||||
其他标签页和请求的其他工具继续可用。后续快照等操作应提示标签页已关闭;Agent 显式导航
|
||||
时打开专用新标签页并继续操作,不能自动接管用户的其他页面。
|
||||
|
||||
浏览器工具栏读取的权威状态至少包含:
|
||||
|
||||
@@ -414,11 +513,23 @@ type BrowserToolbarState = {
|
||||
没有浏览器会话时仍显示工具栏。活动 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 执行和监督显式产生的独立成果:
|
||||
|
||||
@@ -446,7 +557,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 与任务上下文
|
||||
|
||||
工作栏不提供独立“上下文”能力。用户准备下一次模型请求时:
|
||||
|
||||
@@ -470,7 +594,9 @@ type WorkbarCapabilityId =
|
||||
| 'processes'
|
||||
| 'workspace'
|
||||
| 'browser'
|
||||
| 'resources'
|
||||
| 'results'
|
||||
| 'document'
|
||||
|
||||
type WorkbarTargetRef =
|
||||
| { type: 'conversation'; id: string }
|
||||
@@ -483,6 +609,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
|
||||
@@ -751,6 +878,7 @@ remote_operations
|
||||
其中:
|
||||
|
||||
- 工作栏偏好只保存能力目录顺序、面板实例、停靠布局、尺寸和目标引用。
|
||||
- 资源监控只保存面板实例,不保存采样结果或历史曲线。
|
||||
- 主机表只保存非敏感元数据和加密凭据引用。
|
||||
- 应用重启后先通过远程 Daemon 的 journal、lease、boot identity 和 event cursor 核对活动
|
||||
终端、进程与 Runtime;无法确认时标记 `interrupted` 或 `outcome_unknown`。
|
||||
@@ -770,7 +898,7 @@ Renderer 只通过显式方法访问:
|
||||
- 终端创建、输入、调整大小、关闭和有界输出订阅。
|
||||
- 受管进程列表、日志和允许的控制动作。
|
||||
- Runtime Inspector 快照、事件和允许的控制动作。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务,以及有界的本机资源快照。
|
||||
|
||||
每个 Main Handler 都必须验证可信发送者、Zod 输入、对象归属和当前状态。不得暴露 raw
|
||||
Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
@@ -779,7 +907,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 并验证符号链接边界。
|
||||
@@ -850,6 +978,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
### 阶段 1:成果与 Runtime 可观测性
|
||||
|
||||
- 本机 CPU、内存、磁盘 IO 和 GoodBuddy 桌面进程摘要的按需资源监控。
|
||||
- 通用 Artifact Event。
|
||||
- HTML 工作区和成果的静态即时预览。
|
||||
- Runtime Inspector Snapshot 与事件。
|
||||
@@ -881,8 +1010,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 保存与环境
|
||||
@@ -910,7 +1040,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
### 17.1 稳定能力目录与用户控制
|
||||
|
||||
- [ ] 八个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] 十个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] Task Center 继续作为 Task 的单例索引,不删除入口、不复制会话,也不混入 Job、Run 或心跳事项。
|
||||
- [ ] 项目、会话、Runtime 或主机变化不会无提示地增删能力目录项。
|
||||
- [ ] 用户可以按需打开、关闭、排序和停靠面板实例。
|
||||
@@ -925,6 +1055,8 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [ ] 监督可以作用于普通 Conversation、Task 和后续实验对象,不假设编程语境。
|
||||
- [ ] 工作区不是 Git 仓库时仍可浏览文件。
|
||||
- [ ] Runtime 不支持某项原生能力时仍可从目录打开面板并获得准确说明。
|
||||
- [ ] 文档能力可以打开多个以文件名命名的固定实例;未配置健康 ShareServer 时显示原因,
|
||||
不把系统默认应用或静态预览冒充内置编辑。
|
||||
|
||||
### 17.3 安全与控制
|
||||
|
||||
@@ -968,6 +1100,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [ ] 创建中、Agent 操作中和完整交互期间禁用浏览器其余工具栏控件;活动加载仍可单独停止,
|
||||
且停止加载不会关闭会话或释放 Runtime 上下文。
|
||||
- [ ] 活动 Conversation 没有浏览器会话时,可从同一地址栏按 Agent 相同 URL 策略启动会话。
|
||||
- [ ] 文档编辑器在右侧宽度不足时不发生工具栏或正文重叠,并可使用同一会话展开到可编辑布局。
|
||||
|
||||
## 18. 相关文档的职责
|
||||
|
||||
@@ -976,7 +1109,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
动态 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 索引。
|
||||
- [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、监督、预算和记忆。
|
||||
@@ -984,8 +1117,10 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [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,684 @@
|
||||
# 工作栏实现与验证进度
|
||||
|
||||
## 2026-09-14 多 Runtime 工具操作摘要
|
||||
|
||||
显示与事件合并规则见[工具操作摘要](./runtime-interactions.md#工具操作摘要)。Continue、
|
||||
DeepSeek Harness 和直连模型已接入共享摘要函数,完成事件缺少输入时保留已有摘要。
|
||||
|
||||
Windows 完整 App 使用当前生产 Main、Preload、Renderer 和隔离配置,模型为
|
||||
`deepseek-v4-flash`。从真实输入区输入并点击发送读取测试文件的请求,校验码只存于
|
||||
文件中。三条成功会话都返回正确校验码,工具完成后仍显示路径;原生点击展开可查看
|
||||
输入与输出,离开后重新打开会话、重新加载 Renderer 后再次打开均保留摘要和详情。
|
||||
六张对话区截图已实际打开审阅。
|
||||
|
||||
| Runtime | 成功模式 | 提交数(含失败重跑) | 模型 HTTP dispatch | 工具执行 |
|
||||
| --- | --- | ---: | ---: | ---: |
|
||||
| 直连模型 | Ask | 1 | 3 | 3 |
|
||||
| Continue | Execute | 2 | 4 | 3 |
|
||||
| DeepSeek Harness | Execute | 3 | 3 | 2 |
|
||||
| 合计 | | 6 | 10 | 8 |
|
||||
|
||||
计数来自 Main 和真实 Utility Host 的 HTTP dispatch,未注入答案或工具事件。Continue
|
||||
首轮 Ask 仅提供配置工具,未能读取文件;改用 Execute 成功。DS 前两次因 Windows
|
||||
工作区路径写法与 Host `realpath` 比较不一致而返回 ACP `-32602`,均未调用模型;
|
||||
同目录改写路径仍复用缓存 Runtime,改用全新规范路径目录后成功。该路径问题随后由
|
||||
[DS 工作区路径修复](../deepseek-harness/progress.md#2026-09-14-windows-工作区路径修复)
|
||||
单独处理,新增验证计数见该记录,不计入上表。驱动的包解析、
|
||||
DOM 定位及卡片比较器失败已记录,修正比较器后的复验没有追加模型请求。
|
||||
|
||||
本机临时 `runtime-summary-live/report.json` 保留逐次结果和产物哈希;源码基线为
|
||||
`c71da4b6ae1d7228ff968d39c63db205caf61339` 加本次工作树改动,Main SHA-256 为
|
||||
`2153d72a947ab6f5eab22eab28d7a3f2f86a47740d5223a3e1547f199fa7de84`。
|
||||
测试进程、隔离配置、数据库、工作区和构建均已清理,正式配置字节未改变;报告和截图
|
||||
保留在本机临时目录,不作为长期可用的仓库资源。请求使用已配置 HTTPS,沿用生产
|
||||
证书策略,本轮未验证严格证书校验。未测试 Main 重启或远程 Agent。
|
||||
|
||||
专项回归 210 项通过、1 项跳过。完整 `npm test` 首轮 4154 项通过、66 项跳过,另有
|
||||
6 个 `.agent-resources` 内的本地 `node:test` 文件被 Vitest 误收集导致套件失败。
|
||||
使用 `npm test -- --exclude "**/.agent-resources/**"` 完整重跑后,353 个文件通过、
|
||||
9 个跳过,4154 项通过、66 项跳过、0 失败,耗时 646.04 秒。`npm run typecheck`、
|
||||
`npm run lint` 和 `git diff --check` 通过。
|
||||
|
||||
## 2026-09-13–14 schema 35 工具重复写入补修(未发布)
|
||||
|
||||
- 0.13.1 的历史库回收成功,但其真实模型验收限制子代理不使用工具,遗漏了生产工具对象
|
||||
的 `undefined` 可选字段。JSON 重读省略这些键,原深比较误判未变化的工具块。正常开发
|
||||
任务复现同批 928 条事件的 7,787 次重复工具块写入;仅归一化空字段的对照将负载从
|
||||
10,467,622 降为 452,391 字节,逐条还原一致。该对照不是产品修复验收。
|
||||
- 当前修复按共享工具 schema 的字段值比较,不序列化整份工具输出进行比较。空字符串、
|
||||
输出修订、状态和错误变化仍保留。schema 35 使用同一编码器重整旧完整事件及 schema 34
|
||||
已存差量,跳过 JSON 未变化的行;不会删除事件、会话、工具结果或来源字段。
|
||||
- 存储缓存仍最多 16 个任务,终态及时移除缓存引用;回滚后按已提交记录重建,其他任务
|
||||
不受影响。远程先按来源主键判断是否重放,重复事件不重建已经释放的写入缓存;
|
||||
`INSERT OR IGNORE`、冲突校验及消息投影事务仍保留。
|
||||
- Windows 当前源码加便携版内置 OpenCode 完成正常 CSV 项目开发,主/子代理实际读取、
|
||||
改代码、补测试和执行命令;独立 59 项测试及 CLI 通过。698 条子代理事件共
|
||||
368,575 字节,重复工具块为 0,与归一化对照字节数完全相同,每条事件还原一致。
|
||||
- 多项目实测使用两个独立 OpenCode 实例、每项目两个会话,两轮共 8 次 Execute。
|
||||
第一轮四个开发任务全部完成;第二轮复用会话,主动取消一个正在使用工具的子代理任务,
|
||||
其余三个完成并通过测试。2,841 条子代理事件共 1,305,340 字节,重复工具块为 0,
|
||||
全部逐条还原一致。每轮结束后缓存任务数、打开的 SSE 响应流均为 0。
|
||||
隔离 Main 测试进程在显式 GC 后的堆占用为 12.94 → 12.91 MiB;
|
||||
这证明本轮未持续积累,不是整个 Electron UI/Runtime 的 RSS 上限或无限负载保证。
|
||||
- 共享 Linux x64 Host 使用当前桌面源码经真实 SSH、Agent、ACP 和模型 gateway 完成
|
||||
Ask 与正常工具 Execute。912 条子代理事件共 405,360 字节,重复工具块为 0,
|
||||
逐条还原及重复重放通过,远程 Python 项目测试通过。修复位于共享桌面层,不修改 Agent
|
||||
或公开事件协议。两次早期测试驱动错误中断操作,修正瞬态事件处理后才完成上述验收;
|
||||
不能将早期失败写成产品通过。
|
||||
- 本轮实际提供商请求为 121 次:本地开发 26,多会话 79,远程账本 16(成功验收 14,
|
||||
早期驱动中断 2 次结果未知、未交付)。本地计数器只记录 HTTP 状态,远程按测试
|
||||
request/operation 的 Agent ledger 统计,不记录凭据、正文或请求头。另有先前诊断的
|
||||
24 次请求,不计入这 121 次。已核对本轮本机和远程测试工作区无残留 Runtime 进程。
|
||||
- 活动 schema 34 库通过 SQLite online backup 取得一致性备份,并校验独立测试副本。
|
||||
正常启动升级成功,2,898,984,960 → 607,055,872 字节(2.70 GiB → 578.93 MiB,
|
||||
回收 79.06%)。完整比较 424,148 条原事件的内容、ID、时间及来源一致。
|
||||
隔离启动未复制模型配置,既有启动修复调整一条项目/会话 Runtime 选择及一个在途任务、
|
||||
消息和活动状态,另追加一个状态事件;不是迁移删除或改写历史内容。
|
||||
- 同一真实备份的另一个副本直接运行实际构建的升级 Worker,在处理 64 条后取消,
|
||||
重启后完成 316,664 条子代理记录;全量事件再次还原一致。全部非事件表逐表内容一致,
|
||||
包括 150 个会话、1,915 条消息、914 个任务和 136 个成果;`integrity_check=ok`。
|
||||
原运行库和校验备份没有被测试改写。schema 35 副本再次进入实际 Worker 时直接完成,
|
||||
进度事件数为 0,不重复整理。
|
||||
- 最终专项 99 项、typecheck、lint 及生产开发构建通过。首轮全量产品用例 4,154 通过、
|
||||
66 跳过,但 6 个临时 Node 测试项目被 Vitest 错误发现;配置已排除 `.agent-resources/`,
|
||||
独立项目仍单独运行自己的 Node 测试。随后显式双 worker 全量为 4,148 通过、66 跳过、
|
||||
`App.test.tsx` 6 项时序失败;恢复仓库默认单 worker 后该文件 220 项全部通过,
|
||||
未修改 UI 代码。最终默认配置全量为 4,151 通过、66 跳过、3 个问答 UI 测试失败,
|
||||
失败项与双 worker 不同;本轮全量回归尚未全绿。随后仅调整测试等待和 mock 隔离的
|
||||
尝试仍有不稳定失败,已完整撤回这些额外 UI 测试改动,不把单次重跑通过当作稳定修复。
|
||||
存储专项、真实工具任务、并发内存和真实数据迁移证据均通过;UI 测试问题仍需单独处理,
|
||||
本次未提交、未发布,也未替换用户正在运行的 0.13.1。
|
||||
|
||||
## 2026-09-13 Desktop 0.13.1 执行记录去重与大库升级
|
||||
|
||||
- 实现与兼容边界见[执行记录存储与升级回收](./execution-history-storage.md),通用开发
|
||||
要求见[数据库迁移指南](../../development/database-migrations.md)。本机和远程写入共用
|
||||
差量编码,不修改 Runtime 公共事件或 Agent 协议;消息投影与来源去重继续保持事务。
|
||||
- 真实 portable 旧库先备份并校验 SHA-256,再在独立新版 portable 中启动升级:
|
||||
13,094,912 → 12,595,200 字节,schema 33 → 34。659 条消息、52 条会话、133 条成果、
|
||||
笔记/待办及模型用量表逐表内容一致,完整性检查通过。
|
||||
- 另对真实膨胀库的核验备份副本运行同一 portable 启动入口:
|
||||
97,308,667,904 → 594,182,144 字节,即 90.63 GiB → 566.66 MiB,
|
||||
实际回收 90.07 GiB(99.39%),freelist 为 0。原库和原 portable 数据库未改写。
|
||||
- 全量还原比较 411,981 条事件,所有内容、事件 ID、时间及远程来源字段一致;
|
||||
SQLite `integrity_check` 返回 `ok`。1,876 条消息、895 个任务、136 个成果及笔记、
|
||||
待办、用量和活动记录等表内容一致。隔离 profile 没有复制模型配置,既有启动修复更新了
|
||||
一条项目及一条会话的 Runtime 选择,项目 `updated_at` 随之更新;不是事件迁移修改。
|
||||
- 真实升级中通过“退出,稍后继续”停止,再用更新后的 portable 继续并完成。初版 UI
|
||||
只按内容高度展开且进度条过窄,用户指出后修复为全高画布和全宽进度条,并显示处理速度;
|
||||
实包测得页面高度约 761 CSS px、viewport 761 px,进度条宽约 960 px,截图人工复核通过。
|
||||
- 大库采样的读取耗时占约 65%、JSON 解析约 17%。将升级读取缓存由 8 MiB 提至
|
||||
64 MiB,保留每批 32 条事务和 SQLite 自动 checkpoint,只在任务结束时截断 WAL。
|
||||
相邻 2,048 条转换采样从约 12.0 秒降至 8.8 秒;样本和缓存状态不同,不作为固定加速比。
|
||||
未增加同库并行写入,也未关闭事务或同步落盘。
|
||||
- 当前源码通过真实本地 OpenCode Execute 和共享 Linux x64 Host 的托管 Ask/Execute。
|
||||
本机与远程各观察到 65 条子任务事件,最终过程仍可显示,远程重复事件不重复投影;
|
||||
测试资源均位于专用目录。共提交 3 次真实 Prompt,Runtime 上报 4 笔模型用量;
|
||||
子代理内部 HTTP 调用未单独计数,不将该上报数称为服务商请求总数。
|
||||
- 首轮完整 `npm test` 为 4,114 通过、66 跳过,637.97 秒;后续 8 项新增/修正的专项
|
||||
回归通过,`npm run typecheck`、`npm run lint` 通过。生产构建及用户要求的 portable
|
||||
实包开发验证通过。0.13.1 版本与发布说明变更后的本地最终验证被用户主动取消,按要求
|
||||
不再追加本地测试,候选的最终全量测试、类型、lint 与生产构建由 main CI 判定。
|
||||
- 用户批准 0.13.1 中英文说明及仅标准六平台发布;Agent 保持 0.11.24。
|
||||
已发布提交 `4937720` 和不可变标签 `v0.13.1`,main CI 与六平台发布通过;
|
||||
后续真实工具场景的遗漏及补修见本页 schema 35 记录。
|
||||
|
||||
## 2026-09-13 原生浏览器与应用浮层修复
|
||||
|
||||
- 用户在“添加实例”中复现原生网页盖住弹窗。原先只有终端关闭确认释放 viewport,
|
||||
现在所有应用 Modal 与相交浮层共用
|
||||
[浏览器遮挡机制](./browser-tabs-technical-design.md#4-renderer-与-viewport)。
|
||||
新增五个组件回归在修复前全部失败,修复后通过;独立检测器另有十项回归。
|
||||
- Windows 当前 Main、Preload、Renderer,通过 `electron-vite dev` 和独立 profile
|
||||
启动完整 App,不是旧安装包或仅有 Renderer 的 fixture。浏览器导航到本机合成页后,
|
||||
实例管理与项目设置均验证原生视图 `visible → hidden → visible`;视图 ID 不变且未销毁。
|
||||
本次不修改 Runtime、Agent、MCP 或远程生产路径。
|
||||
- 实例编辑器在浅深主题、目标 1280×800、960×720、720×640、640×420 下完成八组
|
||||
几何、关闭按钮命中和原生视图隐藏断言。Windows 缩放产生不足 1 CSS px 的取整差异,
|
||||
记录实际尺寸而非把目标尺寸当测量值。PNG 实际审阅包括浅色 1280、720 和深色 640;
|
||||
其余尺寸执行程序化检查,不将其描述为全部 PNG 人工审阅。
|
||||
- 临时驱动曾出现新浏览器 target 抢占自动化目标、隐藏窗口截图阻塞及严格像素比较
|
||||
误判;修正驱动后重新取证,不把这些失败改写为产品修复。最终使用 pinned renderer、
|
||||
独立测试窗口和 CDP PNG;原生视图状态单独从 Main 读取,避免网页截图漏掉原生视图。
|
||||
- 顺带复现已发布更新说明资源为 136,554 字节,超过原 128 KiB 读取上限。上限提高到
|
||||
1 MiB,仍使用有界读取;回归直接读取实际打包资源并确认当前版本可展示、可确认。
|
||||
完整 App 已显示并关闭 0.13.0 更新说明,重启后不再次显示。
|
||||
- 首轮 `npm test -- --maxWorkers=1`:4,108 通过、66 跳过,608.30 秒;
|
||||
`npm run typecheck`、`npm run lint` 通过。补长名称修正后的全量执行为 4,105 通过、
|
||||
66 跳过、3 失败,586.51 秒;失败全部位于并行修改中的子代理进度测试。
|
||||
未修改这些文件,随后定向重跑 ACP 与 OpenCode 子代理共 66 项全部通过。
|
||||
全仓类型与 lint 检查期间先后遇到并行子代理/存储升级改动的缺失字段和未使用导入,
|
||||
这些改动不纳入本次提交,不能把早先的通过结果当作当前并行工作区全绿。
|
||||
- 两组 Harness 单测补 `onTestFinished` 回收临时目录。最后定向四文件 73 项通过,
|
||||
运行前后检查新增残留目录为 0。测试清理不改变 deployed Runtime。
|
||||
- 本次修改文档的 66 个相对链接目标存在,暂存差异无空白错误;最终发布仍需对确定的、
|
||||
不再变动的候选提交执行发布验证,不能将未提交的并行改动混入候选。
|
||||
- 临时脚本、截图和隔离构建按用户要求在提交后清理;这里只保留脱敏结论。知识库布局、
|
||||
合成服务保存验证与调用计数见[知识库进度](../knowledge-base/progress.md#2026-09-13-知识库布局与自动名称修复)。
|
||||
|
||||
## 2026-09-13 OpenCode 事件流关闭顺序修复
|
||||
|
||||
候选检查中的 lifecycle 失败已稳定复现,4 路并行后 `openEventBodies` 为 3 而不是 0。
|
||||
原因与统一修正规则见[复发缺陷记录](../../quality/recurring-defects.md#事件迭代结束前释放底层订阅);
|
||||
聊天与 Compact 共用最小迭代器包装,不修改 OpenCode SDK、重试时限或现有断言时限。
|
||||
|
||||
- 加强既有正常结束与消费方提前返回回归,修复前两项均失败,确认最终 `signal.aborted`
|
||||
不能证明关闭顺序正确;修复后通过。
|
||||
- `npm test -- src/main/agent/opencode-runtime-lifecycle.test.ts
|
||||
src/main/agent/opencode-runtime.test.ts src/main/agent/opencode-runtime-permissions.test.ts
|
||||
src/main/agent/opencode-subagent.test.ts`:4 个文件、99 项通过。
|
||||
- Windows 当前源码配合真实 OpenCode `1.18.29`、真实 SDK/HTTP MCP 和 loopback 模型通过
|
||||
1、4、8 路并行、取消一条而另一条正常结束,以及实际原生 Compact;各阶段打开的 SSE
|
||||
响应流均归零。验证是生产 Runtime 入口,不是完整 Electron UI;真实服务商调用 0 次。
|
||||
- 已核对远端 `AcpRemoteRuntime` 的 Agent-owned transcript 轮询与
|
||||
`AgentOwnedAcpPrompt` 的 ACP stdio 连接,不使用本机 `client.event.subscribe`。本次未改
|
||||
Agent、远程桥或协议,不需要远端对应修复,也未将本机测试算作远程 Host 复验。
|
||||
- 最终 `npm test`:349 个文件通过、9 个跳过;4,089 项通过、66 项跳过、0 失败,
|
||||
耗时 571.43 秒。`npm run release:notes:verify`、`npm run typecheck`、`npm run lint`
|
||||
全部通过;下节失败结果保留为修复前记录,不再是当前本地回归结论。
|
||||
- 发布说明按 Desktop `v0.12.12`、Agent `agent-v0.11.23` 到最终候选的差异核对,
|
||||
不按中间开发提交分别列出修正。用户明确不增加事件流条目,已批准中英文说明保持不变。
|
||||
- 本轮未提交、推送或打标签,未运行本地生产构建或打包;远程问答完整生产链路的真实
|
||||
服务商验证及候选 CI/原生发布验证仍沿用各自待验收状态,不由本机 SSE 修复代替。
|
||||
|
||||
## 2026-09-13 Desktop 0.13.0 / Agent 0.11.24 候选检查
|
||||
|
||||
本轮仅准备本地候选:更新 Desktop 与 Agent 版本、用户批准的中英文发布说明及功能清单,
|
||||
远程原生问答按问题修复分类。Agent 发布说明测试允许仅有修复章节,同时检查中英文分类
|
||||
一致;未修改 Runtime、Agent 问答或事件流实现。Agent `0.11.24` 需要先升级 Desktop 至
|
||||
`0.13.0`,OpenCode 保持 `1.18.29`。
|
||||
|
||||
- `npm run release:notes:verify`、`npm run typecheck`、`npm run lint`:通过。
|
||||
- `npm test`:348 个文件通过、1 个失败、9 个跳过;4,088 项通过、1 项失败、66 项跳过,
|
||||
耗时 571.96 秒。唯一失败为 `opencode-runtime-lifecycle.test.ts:120`,并行请求结束后
|
||||
`openEventBodies` 期望为 0、实际为 3,轮询未在时限内满足;该现象早已有记录,
|
||||
本轮没有完成根因判断,也没有放宽断言或跳过测试。
|
||||
- 单独复跑上述 lifecycle 文件及 `agent-release-workflow.test.ts`、
|
||||
`release-notes-source.test.ts`、`release-notes-service.test.ts`:发布相关 15 项通过,
|
||||
lifecycle 同一断言再次失败。该结果不能替代一次全量通过。
|
||||
- 更早的一次全量运行在调整 Agent 发布说明章节断言前主动停止,未取得汇总,不计为通过。
|
||||
- 候选版本一致性、历史 Desktop 发布说明未改动、48 个 Markdown 相对链接目标和
|
||||
`git diff --check` 已通过。
|
||||
- 本轮真实模型验证调用 0 次,未重跑真实 Host;既有 SSH/确定性模型证据见下文,
|
||||
不将其扩写为完整生产 Main/Preload 配合真实服务商的验收。
|
||||
- 未提交、推送或创建标签,未运行本地生产构建、打包或 LoongArch 预览。
|
||||
全量测试失败、完整远程生产链路及候选 CI/原生包验证仍未解决,候选不标记为可发布。
|
||||
|
||||
## 2026-09-13 SSH 问答 UI 复验与取消后续发
|
||||
|
||||
在真实 Electron 窗口中使用当前 `App`、入口样式和主题函数,经测试 IPC 接入生产
|
||||
`createManagedRemoteAcpRuntime`。远端使用共享 Linux x64 Host、真实 SSH attach、当前源码
|
||||
Agent、原生 OpenCode 和生产模型桥;模型响应来自远端 loopback 确定性服务。配置、项目、
|
||||
队列和存储 API 复用 App 测试 fixture,因此本次证据范围为 App Renderer 到真实 SSH
|
||||
Runtime,未覆盖完整生产 Main/Preload、生产数据库持久化、发布安装或重启恢复。
|
||||
|
||||
窗口测试发现待答期间取消后,同一对话立即发送会报
|
||||
`Active prompt operation identity cannot change before terminalization`。Agent 取消分支
|
||||
原先只写语义记录,重复终态写入失败后未留下可核对的操作终态;Desktop 也保留了旧
|
||||
binding。修复复用 Agent 的异常终态记录方法,并在 Desktop 显式取消后核对终态、关闭
|
||||
已终结 binding 和释放会话通道。新增两项回归验证 Agent 取消核对及同一对话续发。
|
||||
|
||||
已通过的 UI 场景:父子问题同时待答、重复事件不丢草稿、自由文本回答、选项回答、注入
|
||||
一次回复失败后原输入重试、跳过、待答期间取消、取消后在同一对话发出新请求并正常完成。
|
||||
鼠标和输入使用 Electron 原生输入 API。重复问题和回复失败由驱动注入,其余问题、工具
|
||||
进展及完成事件来自真实远端 Runtime;驱动消费内部 checkpoint,并将取消映射为公开
|
||||
`error/status=cancelled` 事件,不把内部 Runtime 事件直接送给 Renderer。
|
||||
|
||||
浅深主题各检查四组窗口尺寸,问题表单无横向溢出、草稿保留,提交按钮可滚动到可见位置。
|
||||
短窗口需纵向滚动查看问题与输入,未宣称整张卡片同时可见。检查目标为 1280×800、
|
||||
960×720、720×640、640×420,Windows 175% 缩放下实际 CSS 内容区各多 1 像素。
|
||||
临时驱动、JSON 测试报告、各轮日志及 PNG 保留在批准临时目录,使用
|
||||
`question-ui-*`、`remote-question-*` 和 `question-validation-*` 文件名前缀;驱动按
|
||||
[Electron UI 自动化规范](../../quality/electron-ui-automation.md)的现有启动方法重建,
|
||||
不是仓库通用 UI runner。SSH 测试结束后停止专属 daemon 并清理远端专属目录和上传文件。
|
||||
|
||||
复现时在批准临时目录执行 `node question-ui-build.cjs`、
|
||||
`node remote-question-build.cjs`,随后设置 `GOODBUDDY_QUESTION_UI=1` 并执行
|
||||
`node remote-question-run.cjs`。脚本依赖本机既有加密 Host 配置和 Host 上的测试 Node
|
||||
依赖目录;不得将这些本机条件当作新环境已经满足。早期驱动的内部事件转发、原生输入
|
||||
等待和跨轮模型结果判断错误均已修正;它们与实际取消续发缺陷分别记录,未将失败轮次
|
||||
改记为通过。首轮窗口退出过早,未取得完整模型夹具计数,历史夹具请求累计数不完整;
|
||||
外部模型调用为 0。
|
||||
|
||||
最终 SSH/UI 一轮完成 4 次输入区提交、2 次回答和 1 次跳过,收到 4 个原生问题,取消
|
||||
核对结果为 `terminal/cancelled/processTree=empty`,随后新请求完成。该轮模型夹具请求
|
||||
精确为 7 次,外部模型调用 0 次,Renderer 控制台错误 0 条,驱动退出码 0。最终定向
|
||||
回归分两条命令执行:`npm exec vitest run src/agent-daemon/runtime-acp-backend.test.ts
|
||||
src/main/agent/acp-remote-runtime.test.ts` 为 96 项通过;
|
||||
`npm exec vitest run src/agent-daemon/remote-question-binary.test.ts
|
||||
src/agent-daemon/opencode-subagent-plugin.test.ts src/agent-daemon/model-bridge.test.ts
|
||||
src/main/remote-agent/protocol-remote-runtime-channel.test.ts
|
||||
src/agent-daemon/runtime-composition.test.ts src/main/ipc.test.ts` 为 179 项通过、5 项跳过。
|
||||
`npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
|
||||
|
||||
首轮默认并行 `npm test` 的打包测试超时,整轮随后被 120 秒工具时限中断;单 worker
|
||||
重跑为 4,084 项通过、66 项跳过,耗时 626,816 毫秒。此结果早于本节取消修复,
|
||||
不作为最终源码全量结果。
|
||||
|
||||
取消修复后的全量命令为 `node node_modules/vitest/vitest.mjs run --maxWorkers=1
|
||||
--reporter=json --outputFile=<批准临时目录>/question-validation-final.json`,覆盖 358 份
|
||||
测试文件,结果为 4,087 项通过、1 项失败、66 项跳过,监督进程记录耗时 617,138 毫秒、
|
||||
退出码 1。唯一失败是 `tests/agent-package.test.ts:372` 的大清单安装用例,耗时
|
||||
63,366 毫秒,超过该用例 60 秒上限。随后
|
||||
`npm exec vitest run tests/agent-package.test.ts -- --maxWorkers=1` 单独复跑为
|
||||
22 项通过、1 项跳过,整文件耗时 60.08 秒;没有修改该打包测试或放宽超时。
|
||||
全量失败记录保留,不能将分次复跑结果写成单轮全绿。
|
||||
|
||||
最终 UI 原始日志为 `question-ui-1789278503920.log`,8 张待答截图为
|
||||
`question-ui-{light,dark}-{1280,960,720,640}.png`,另有完成截图
|
||||
`question-ui-success.png`。已逐张打开待答 PNG 审阅字体、字段、按钮、焦点和短窗口
|
||||
滚动位置;未发现问题卡片遮挡或按钮不可达。尺寸切换等待 CSS 过渡结束后再截图,
|
||||
窗口使用独立临时 session partition。当前证据不覆盖全键盘导航或正式 profile 的清理。
|
||||
|
||||
## 2026-09-13 托管 SSH 原生问答
|
||||
|
||||
- 当前源码支持前台托管 SSH OpenCode 的父、子孙会话原生问题、回答和拒答,复用已有
|
||||
Renderer 队列与 Task 归属。问题来源为原生 `question.asked`;模型正文和普通工具活动
|
||||
不生成问答表单。协议、身份与生命周期规则见
|
||||
[远程主机技术设计](../remote-host/technical-design.md#acp-与断线)。
|
||||
- 新增 `remote-question-binary.test.ts`,使用真实 OpenCode `1.18.29` ACP 进程、生产插件、
|
||||
Agent Prompt owner 和 loopback 模型夹具,覆盖父问题、Task 子问题、拒答、取消、
|
||||
跨 binding 回答拒绝、迟到回答拒绝,以及临时回复能力不进入语义记录。
|
||||
插件测试另覆盖孙会话归属、并发重复事件、HTTP 路径认证、原生待答列表校验和端口关闭;
|
||||
Runtime 测试覆盖已 ACK 记录后的待答补发、回复失败重试与已回答历史去重。
|
||||
- 共享 Linux x64 Host 使用既有加密凭据和固定 Host Key,经真实 SSH attach 启动当前源码
|
||||
的隔离 Agent。生产 `createManagedRemoteAcpRuntime` 入口完成 Ask、Execute 并行父子
|
||||
问答、拒答和待答期间取消;两个并行问题在首次回答前均到达 Desktop Runtime,子问题
|
||||
带 Task 归属,回答后原生 Task 和父轮次正常完成。拒答结束原生轮次,取消后迟到回答
|
||||
明确失败。最终一轮 7 次确定性模型夹具请求,外部模型调用 0 次;测试 daemon 停止,
|
||||
测试目录与上传文件已清理,未修改 Host 原有 Runtime registry。
|
||||
- 最终定向回归:11 份测试文件,219 项通过、5 项平台跳过,包含 Agent、模型桥、ACP
|
||||
Runtime、认证控制通道及托管入口测试。`npm run typecheck`、`npm run lint` 通过。
|
||||
本轮全量 `npm test -- --reporter=json --outputFile=<临时报告>` 为 4,081 项通过、
|
||||
0 项失败、66 项跳过;之后的插件身份与并发去重补充由最终定向回归和 Host 复测覆盖。
|
||||
- 本节早期验证未操作 Electron 窗口、重装共享 Agent 或模拟整机重启;问答队列的界面行为由
|
||||
既有 App/ChatTimeline 测试覆盖。同一存活 Prompt 的待答补发由 Runtime 回归验证,
|
||||
不把它描述成 Agent 进程重启后的问题恢复。旧 Agent 和任意第三方 ACP 服务的范围见
|
||||
[Runtime 交互边界](./runtime-interactions.md)。
|
||||
|
||||
## 2026-09-13 并行提问顺序回答
|
||||
|
||||
- 修复同一前台请求的后续提问覆盖未回答问题:Renderer 按 ID 保存待答队列,逐次显示、
|
||||
回答和跳过,保留失败重试输入;运行中快照刷新保留队列,终态清理全部待答项。
|
||||
交互和归属限制见 [Runtime 交互边界](./runtime-interactions.md#运行与失败)。
|
||||
- 本机 SDK 使用已收到的 Task 子会话元数据传递可选 `childTaskId`;Task 卡片显示待答状态,
|
||||
展开后可定位并聚焦队首表单。没有已知 Task 关联的问题仍能回答。
|
||||
- 六份相关测试文件共 346 项通过:`App.test.tsx`、`ChatTimeline.test.tsx`、
|
||||
`conversation-activity.test.ts`、`opencode-runtime.test.ts`、
|
||||
`opencode-runtime-permissions.test.ts`、`opencode-runtime-lifecycle.test.ts`。
|
||||
覆盖并发到达、重复 ID、草稿保留、提交失败、队列推进、快照刷新及终态与回复竞态。
|
||||
- 真实本机 OpenCode 二进制配合 loopback 模型服务验证两次原生提问均在首次回答前到达,
|
||||
随后分别回传并完成请求;同时保留 Task 文件读取和 Ask 工具限制验证。该用例每轮
|
||||
6 次 loopback 模型请求,外部模型调用 0 次。完整桌面点击路径由 App 组件测试覆盖,
|
||||
本轮未手动操作 Electron 窗口。
|
||||
- 首次 `npm test` 达到工具 200 秒上限;后台重跑 `npm test -- --reporter=verbose` 完成,
|
||||
348 份测试文件通过、9 份跳过,4,069 项通过、66 项跳过,耗时 572.20 秒。
|
||||
随后补充已回答 ID 的去重必须先于任务状态更新,最终 18 项问答及活动相关回归通过。
|
||||
- 独立评审补充同一 React 批次首次两次收到相同 ID 的回归:修复前显示两个待答项。
|
||||
在消息函数式更新器内复核待答和已回答 ID,重复事件直接返回原消息,保留状态;外层
|
||||
去重仍阻止重复事件改写任务状态。修复后 19 项定向回归、typecheck 和完整 lint 通过。
|
||||
- 最终 App 全文件复跑为 217 项通过、1 项失败;失败项为并行开发中的外部知识绑定测试
|
||||
`adds a new external binding to the current selection without re-enabling an excluded library`,
|
||||
“添加知识库”按钮未启用,定向复跑仍失败。本次未修改该测试或知识绑定逻辑。
|
||||
`npm run typecheck` 与差异空白检查通过;早期完整 lint 通过,最终完整 lint 扫到并行
|
||||
工作新增的 `.kb-app-live` 产物而失败。保持产物原样,使用
|
||||
`npm run lint -- --ignore-pattern ".kb-app-live/**" --ignore-pattern ".kb-app-live*.cjs" --ignore-pattern ".kb-app-live*.ts"`
|
||||
复跑通过。
|
||||
- 远程 `AcpRemoteRuntime` 仅接入权限请求,尚无业务问答桥;本次未修改 Agent、ACP
|
||||
传输、共享子任务进度适配器或 SSE 订阅,不将本机问答验证算作远程覆盖。
|
||||
|
||||
## 2026-09-13 浏览器租用标签页关闭与显式恢复
|
||||
|
||||
- 更新 `tests/browser-tabs-electron-e2e.ts`,移除两处租用期间拒绝关闭的旧断言。
|
||||
当前行为见 [Agent 与 MCP 路由](./browser-tabs-technical-design.md#6-agent-与-mcp-路由)
|
||||
和 [浏览器需求](./prd.md#77-浏览器)。同步修改 BrowserService、IPC、Gateway 和直连模型工具。
|
||||
- Windows 真实 Electron/Chromium 与 loopback HTTP/MCP 验证通过:关闭已物化的预留页和
|
||||
已租用主标签页,等待原 WebContents 的 `destroyed` 事件,确认租约结束而请求信号未取消。
|
||||
同一 token 和 MCP session 仍可列出工具;旧目标快照返回关闭提示且不创建页面。
|
||||
显式 `browser_navigate` 创建不同身份的专用替代页,后续 MCP 快照读取该页。
|
||||
- 同 Conversation 的 sibling 仅在主标签页关闭后取得 `primary` 标记,页面 URL、导航状态、
|
||||
时间戳、工作栏身份和 Cookie 内容保持不变;另一 Conversation 的页面与可见 viewport
|
||||
保持不变。撤销 capability 后完成替代页、同级页和 Context 清理。
|
||||
- 已先检查 runner:esbuild 输出临时 CJS,启动真实 Electron,子进程上限 120 秒,结束后
|
||||
删除临时 bundle。`node build/run-browser-tabs-electron-e2e.cjs` 最终退出码 0;
|
||||
前三轮分别修正测试对销毁事件时序、服务旧 ID 错误文案和 sibling 主标签标记的假设。
|
||||
测试文件 ESLint 与限定文件的 `git diff --check` 通过。
|
||||
- 覆盖边界:本轮通过 Main BrowserService 和真实 MCP transport 操作页面,未点击完整
|
||||
Renderer/Preload 关闭入口,未注入执行中的工具取消,也未运行直连模型循环或其他共享工具。
|
||||
浏览器、Gateway、直连 Provider、Model Runtime 和 IPC 回归共 359 项通过、1 项跳过,
|
||||
包括执行中关闭、请求继续完成和其他工具继续可用。全库 typecheck、lint 和 diff 检查通过。
|
||||
全量 `npm test` 在 Agent 离线依赖安装用例失败后达到 200 秒执行上限;无外部模型调用。
|
||||
- 本轮未修改 Agent、桥接或 Runtime 产品源码,也未连接远程 Host。远程调用若进入同一桌面
|
||||
Gateway,会使用上述服务端路由;远端 Runtime 到桌面的实际传输、取消传播及恢复仍需真实
|
||||
Host 验证,本记录不将本机 loopback 结果算作远程覆盖。
|
||||
|
||||
## 2026-09-12 浏览器刷新与关闭后的错误清理
|
||||
|
||||
- 按 [浏览器工具栏规则](./prd.md#77-浏览器) 修复空白标签页允许刷新的问题;
|
||||
刷新需要 Main 的已提交 URL,地址草稿不启用刷新,首次导航仍可停止加载。
|
||||
- 侧栏操作错误在切换面板、关闭当前页签或收起工作栏后清除;旧面板的异步失败不再回写。
|
||||
作用域实现见 [浏览器多实例技术设计](./browser-tabs-technical-design.md#4-renderer-与-viewport)。
|
||||
- Sidebar 回归 38 项、App 浏览器筛选回归 5 项、BrowserService 回归 43 项通过。
|
||||
`node build/run-browser-tabs-electron-e2e.cjs` 通过,验证真实空白页、导航、页面隔离和关闭;
|
||||
该脚本不覆盖 Renderer 提示显示,提示清理由组件测试验证,本轮未手动操作完整桌面 UI。
|
||||
- `npm run typecheck` 和两份修改源码的 ESLint 检查通过。全量 `npm test` 在 Agent
|
||||
离线依赖清单用例超时后,整轮达到 200 秒执行上限;包含完整 App 测试的补跑也达到该上限。
|
||||
`npm run lint` 被工作区原有未跟踪的 `application-tool-navigation/navigation-demo.js`
|
||||
中 27 项浏览器全局变量错误阻断,未修改该文件,不将整库校验记为全绿。
|
||||
- 本轮仅修改桌面 Renderer 控件与提示状态,未改 BrowserService、Runtime、Agent 或远程协议;
|
||||
不需要单独的远程实现。无外部模型调用。
|
||||
|
||||
## 2026-09-10 提交后保留问答
|
||||
|
||||
- 桌面端在提交成功后保留结构化问答回顾,支持多轮回答、跳过及本地会话重新加载;
|
||||
交互规则见 [Runtime 交互边界](./runtime-interactions.md#运行与失败)。
|
||||
- App 回归覆盖提交与重载、下一轮问题提前到达、终态先于提交返回、提交失败;数据库
|
||||
回归覆盖完整替换、增量保存、关闭重开、多选及超过 1,000 字符的自定义答案。
|
||||
- `npm test`:3,900 项通过、66 项跳过,Agent 离线依赖清单测试在 60 秒超时;
|
||||
该项独立复跑通过(58.33 秒)。`npm run typecheck`、`npm run lint` 与差异空白检查通过。
|
||||
- 本轮未手动运行真实 OpenCode 问答会话,外部模型调用 0 次。修改仅涉及桌面端展示与
|
||||
本地保存,不改变 Runtime 回传协议或 Agent 路径,不新增远程 ACP 问答能力。
|
||||
|
||||
## 2026-09-10 Git 历史测试夹具修复
|
||||
|
||||
- 桌面候选 `877cf58` 的 CI 在四项工作区测试准备 Git 提交时因缺少作者身份失败。
|
||||
`c037d7f` 移除测试身份后引入了机器配置依赖;本机已有身份使该问题未在本地暴露。
|
||||
- 测试现在把已暂存的 tree 写入固定 Git commit 对象,并仅更新临时仓库的分支引用,
|
||||
不调用 `git commit`,不修改或覆盖任何 Git 身份配置。历史、重命名差异、分页和脏文件
|
||||
分支冲突仍由真实 Git 与生产 `manageWorkspace` 验证,不用 mock 或跳过用例替代。
|
||||
- 新增夹具自检确认 `git fsck --strict` 通过、父提交链有效、未暂存内容保持不变且仓库配置未改。
|
||||
聚焦测试 11 项通过、2 项因当前文件系统大小写不敏感而跳过;无身份 Runner 的验证待新候选 CI。
|
||||
- 完整 `npm test` 为 3845 项通过、66 项跳过、1 项未修改的活动列表测试超时;
|
||||
随后 `ActivityPanel.test.tsx` 的 16 项独立复跑全部通过,未放宽时限或删除断言。
|
||||
`npm run release:notes:verify`、`npm run typecheck`、`npm run lint` 通过。
|
||||
未将这次本地整库结果记为全绿,发布仍以新候选的 CI 校验和生产构建通过为前提。
|
||||
- 本轮只改测试和验证记录,产品功能、Agent 源码及已批准的双语发布说明不变,无外部模型调用。
|
||||
|
||||
## 2026-09-10 工作区布局与分支输入样式
|
||||
|
||||
- 按 [FR-W1、FR-W4](./prd.md#76-工作区) 将视图切换和刷新合并到顶部行,
|
||||
文件与 Git 工具按视图显示;移除根目录的孤立图标及空导航行,子目录保留文字路径导航。
|
||||
分支搜索复用共享 `field`,面板限宽、分支列表限高滚动,创建按钮使用紧凑次操作。
|
||||
- 新增布局、子目录返回、焦点恢复、分支搜索/切换/创建回归;工作区、设置界面、设置存储、
|
||||
IPC 和 Sidebar 五个测试文件共 354 项通过。另确认安全与数据的默认 `toolApproval` 为
|
||||
`always`,对应“Execute 自动授权已启用的工具”;已有明确保存的禁止策略仍保留。
|
||||
本轮没有修改授权策略或默认值。
|
||||
- 当前构建的 Windows Electron 使用独立 profile 和临时 Git 项目,实际 App → Preload →
|
||||
Main 路径通过分支搜索、切换、创建和子目录新建文件;磁盘与 Git 分支结果均确认。
|
||||
文件/Git 切换保留浏览目录,返回根目录后导航行移除、焦点回到刷新;Escape 收起分支面板并恢复焦点。
|
||||
真实安全与数据页面的默认选中项也确认为自动授权。
|
||||
- 浅色与深色下,将实际工作栏布局宽度设为 300、420、650px 后均无工作区横向溢出。
|
||||
输入框为共享 13px 正文、标签 11px,分支面板最大 360px;15 个分支时列表可在 240px 内滚动。
|
||||
这些是布局检查,不作为分栏拖拽或原生浏览器鼠标交互的验证。
|
||||
- `npm run build:bundle`、`npm run typecheck`、`npm run lint` 通过。
|
||||
整库 `npm test` 为 3843 项通过、1 项失败、66 项跳过;失败项是未修改的
|
||||
`opencode-runtime-lifecycle.test.ts` 事件流关闭时序断言,单独复跑通过。
|
||||
未再次运行整库,不将此结果记为整库全绿。
|
||||
- 本轮产品改动仅为共享 Renderer 工作区布局,未改 Main/Agent 的工作区命令、
|
||||
desktop-to-Agent 协议或 Runtime 执行逻辑;远程项目继续使用同一组件和既有回调,
|
||||
本轮未做新的 Host 实测。无外部模型调用,所属测试应用、浏览器会话和调试监听已关闭。
|
||||
|
||||
## 2026-09-10 提交审查修复
|
||||
|
||||
- [FR-W3、FR-W6](./prd.md#76-工作区):同一目录项可仅修改大小写,仍拒绝覆盖其他文件、
|
||||
目录和硬链接目录项;提交 Diff 同时使用重命名前后路径;分支后台刷新不再使历史请求
|
||||
遗留 busy;自动目录刷新包含当前浏览目录并去重。
|
||||
- 浏览器请求仅预留目标身份,首次导航才创建实际资源;Main 状态携带工作栏实例身份,
|
||||
Renderer 直接绑定同一页面,停止或回收后清除旧绑定。跨会话显式选择不会被应用类型
|
||||
同步覆盖,终端关闭确认期间释放原生浏览器 viewport。规则以
|
||||
[浏览器多实例技术设计](./browser-tabs-technical-design.md) 为准。
|
||||
- 删除普通浏览器操作的自动截图、固定等待、重试及 JPEG 状态缓存/IPC 字段,保留显式截图。
|
||||
- 整合复核补齐现有 32 页签容量处理与 Harness 代理的已发现工具复用。新增容量回归验证
|
||||
满额时布局仍有效、页面保留,关闭一项后显示原页面;Harness 回归验证发现接口后续失败时
|
||||
已知工具调用不再触发发现。相关三文件 66 项通过,最新 typecheck 与 lint 通过。
|
||||
- 工作区聚焦测试 43 项通过、2 项需大小写敏感文件系统而跳过;IPC 与 Sidebar 聚焦回归
|
||||
132 项通过。随后新增的 Ask 指令、继承 Runtime 用量持久化及计划模式 App 场景,3 项通过。
|
||||
- 扩展真实 Electron/MCP E2E 在独立临时 profile 通过:四个未使用的请求预留不创建会话、
|
||||
WebContents 或 UI 事件;首次 MCP 导航物化相同 Tab/工作栏身份,恢复不额外创建页面,
|
||||
并验证独立页面、共享 Cookie、当时的请求租约关闭保护和清理;关闭保护已于 2026-09-13 移除。
|
||||
- 当前源码的共享 Linux x64 Host 验证已通过,覆盖工作区目标保护和重命名/Git Diff,
|
||||
并通过桌面托管 ACP → 隔离 Agent → OpenCode 完成 Ask/Execute 和继承 Runtime 指标持久化;
|
||||
该组共 3 次真实模型调用,清理后无所属进程残留。完整记录见
|
||||
[远程 Host 开发验证](../remote-host/technical-design.md#agent-开发期间的真实-host-验证)。
|
||||
- 最终整库 `npm test` 为 341 个文件通过、9 个文件跳过,3839 项通过、66 项按平台或手动
|
||||
条件跳过、0 失败;`npm run typecheck`、`npm run lint`、开发期 `npm run build` 和
|
||||
`git diff --check` 通过,19 份修改 Markdown 的相对链接目标检查通过。
|
||||
先前整合中的一轮为 3823 项通过、6 项失败、66 项跳过,另有被移动的测试路径加载失败,
|
||||
不作为最终结果;源码稳定后完整重跑得到上述全绿结果。
|
||||
- 原文档解析焦点回归改为等待 Modal 的焦点效果,保留焦点与背景隔离断言,没有改动产品逻辑。
|
||||
- Windows 独立 Electron profile 的完整 App → Preload → Main UI 验证通过;最终复测使用
|
||||
本轮最终开发构建。启用浏览器后普通 Execute 没有创建原生浏览器视图,工具导航只创建
|
||||
一张同时显示在工作栏的页面,跨会话选择保留原页面与 viewport。
|
||||
- 实际终端连通后,从浏览器关闭该终端可正常显示确认并操作按钮;原生视图可见性按
|
||||
可见 → 确认时隐藏 → 取消后可见变化,终端关闭清理完成。无效地址 `http://` 被拒绝后,
|
||||
原控件可重试有效地址并关闭页面;关闭端口返回了过滤代理错误页,因此该 UI 场景不作为
|
||||
Chromium `loadURL` 拒绝的实机证据。服务与 Renderer 释放绑定回归另有自动测试覆盖。
|
||||
- 桌面使用确定性 loopback 模型服务驱动真实生产路径,共 24 次本地 completion 请求,
|
||||
未发出外部模型请求;可选真实 Provider 探针没有找到已加密保存的 OpenAI 兼容文本连接,
|
||||
未据此宣称桌面真实模型验收通过。Ask 与编程 Subagent 的桌面结果见
|
||||
[直连模型验收记录](../direct-model-agent/progress.md#2026-09-10-提交审查后的桌面-ui-验收)。
|
||||
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
|
||||
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
|
||||
|
||||
## 2026-09-10 固定基础页签与浏览器多实例
|
||||
|
||||
- 工作栏注册表已分别声明实例策略、默认上下文、默认打开、必须存在、可关闭和排序属性。
|
||||
任务中心与工作区保持默认存在且不可关闭;浏览器改为当前 Conversation 上下文的多实例。
|
||||
布局加载按 `required` 补回缺失的基础页签,并修复失效活动实例。
|
||||
- 任务中心增加“当前项目”“全局任务”“全部项目”范围,默认跟随活动项目;任务和审批按
|
||||
Conversation 所属项目过滤,范围模式随工作栏布局持久化。
|
||||
- 浏览器服务已拆分 Conversation Context 与 Browser Tab。每个 Tab 拥有独立页面、Driver、
|
||||
操作队列、状态和元素引用空间,同一 Conversation 的 Tab 共享 partition、代理和 Cookie。
|
||||
- Renderer 使用工作栏实例 UUID 原子创建或恢复 Browser Tab,并以 UUID viewport token 防止
|
||||
延迟 cleanup 隐藏新活动 Tab。关闭单个实例只释放对应 Tab。
|
||||
- 直连模型和请求级 MCP capability 在请求开始时优先绑定可见 Tab,其次绑定 primary Tab,
|
||||
并持有使用租约。当时绑定 Tab 拒绝关闭,该行为已于 2026-09-13 改为允许关闭和显式导航恢复;工具参数未增加模型可见
|
||||
`tabId`,已有浏览器工具 schema 保持不变。
|
||||
- 聚焦回归命令覆盖 Shared、BrowserService、Electron Session、IPC、Preload、MCP、Renderer
|
||||
和 Workbar,13 个测试文件共 453 项通过;`npm run lint` 与 Web TypeScript 检查通过。
|
||||
- `node build/run-browser-tabs-electron-e2e.cjs` 通过。该测试启动真实 Electron/Chromium 和本地
|
||||
HTTP 页面,创建两个 `WebContentsView`,验证页面隔离、同 Conversation Cookie 共享、真实
|
||||
loopback MCP 的固定 Tab 导航与快照、当时的使用租约关闭保护,以及单 Tab 和最终 Context 清理。
|
||||
- `npm run typecheck`、`npm run lint`、`npm run build:bundle` 和 `git diff --check` 通过。
|
||||
- 本轮整库 `npm test` 为 3686 项通过、61 项跳过、4 项失败。失败位于 portable/release 的
|
||||
ASAR 元数据 fixture、Runtime 版本探测和 DeepSeek Harness 的本地 MCP fixture;定向复跑仍
|
||||
失败。本次工作栏、浏览器、IPC 和 MCP 聚焦回归均通过,不将整库结果记为通过。
|
||||
|
||||
## 2026-09-09 移动目录选择与 Git 控件修正
|
||||
|
||||
- 本轮在已有文件管理改动上修正 [FR-W3、FR-W4、FR-W6](./prd.md#76-工作区):
|
||||
本机移动调用创建项目使用的 `settings.selectWorkspace()`;远程移动调用
|
||||
`sshHosts.browseDirectories()`,按项目 Host 浏览。所选目录映射为工作区相对目标,
|
||||
保留原名;取消不提交移动,外部目录在弹窗内提示限制。
|
||||
- 取消、分支、Fetch、历史展开和提交行接入共享控件样式;删除菜单项左对齐。
|
||||
远程目录列表限制滚动高度,保留加载、空目录、失败和截断提示。
|
||||
- `npm test -- src/renderer/src/WorkspaceFilesPanel.test.tsx src/renderer/src/RightAssistantSidebar.resize.test.tsx src/main/workspace/workspace-management.test.ts src/main/ssh/ssh-host-directory-browser.test.ts`
|
||||
通过 56 项,覆盖本机目录选择桥接、Windows 与 POSIX 目标映射、取消与越界选择、
|
||||
远程 Host 和目录参数、取消未完成的浏览,以及 Git 控件与真实文件管理服务。
|
||||
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git|opens the global assistant sidebar"`
|
||||
通过 12 项。工作栏切换测试改为等待项目目录加载后出现刷新按钮。
|
||||
- `npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
|
||||
- 本轮整库 `npm test` 完整运行 944 秒,3642 项通过、61 项跳过、6 项失败。
|
||||
工作栏加载时序断言已修正并定向复测通过;其余 5 项涉及 Agent 离线依赖测试超时、
|
||||
DeepSeek Harness MCP 加载、Runtime 版本探测,以及 portable/release 打包产物检查。
|
||||
未再次运行整库测试,不记为整库通过。
|
||||
- 隔离 Electron 窗口加载当前源码组件和 CSS,检查浅色、深色主题下 300px、420px 工作栏:
|
||||
无横向溢出,分支与 Fetch 同行,提交行左对齐;移动弹窗没有路径输入框,取消为次按钮,
|
||||
删除项计算样式为左对齐。截图保留于本机临时验证目录。此检查使用桥接替身,未自动操作
|
||||
系统原生目录对话框,也不替代完整应用中的人工验收。
|
||||
- 尝试从隔离 Electron 配置调用真实 SSH 目录浏览服务时,已有凭据在
|
||||
`safeStorage.decryptString` 解密失败,尚未建立 Host 连接。真实远程目录选择验收仍受阻;
|
||||
未修改用户配置或远端文件,本轮未调用真实模型。
|
||||
|
||||
## 2026-09-09 文件管理与 Git 工作区扩展
|
||||
|
||||
- 已只读核对根目录规范、UI 设计系统、目录浏览组件、WorkspaceAccess 与 Agent 协议。
|
||||
本轮范围定义于 [PRD 工作区 FR-W1 至 FR-W6](./prd.md#76-工作区)。
|
||||
- 当前生产路径具备目录、属性、文本预览和工作树 Diff;Agent 的写入及 Git operation
|
||||
仍是只读拒绝接口。文件管理、分支与提交历史尚未实现或验证,不能视为完成。
|
||||
- 待完成源码实现、整库 test/typecheck/lint、真实桌面路径与当前源码 Linux Host 验证。
|
||||
本轮开始前已有浏览器与工作区未提交改动,保留这些改动;不提交或推送。
|
||||
|
||||
## 2026-09-09 工作区页签交互
|
||||
|
||||
- 已核对当前工作树中的 `RightAssistantSidebar.tsx` 和 `WorkspaceFilesPanel.tsx` 差异:
|
||||
文件预览期间目录树保持挂载,接入返回位置与焦点恢复、文件与未提交更改分段视图、
|
||||
目录就地重试,以及文件预览和 Diff 操作。行为定义见 [PRD 工作区](./prd.md#76-工作区)。
|
||||
- `npm test -- src/renderer/src/RightAssistantSidebar.resize.test.tsx src/renderer/src/WorkspaceFilesPanel.test.tsx`
|
||||
通过 35 项,覆盖目录展开、返回位置与焦点、视图选择、刷新失败保留内容、Markdown
|
||||
源码切换,以及切换项目后丢弃未完成的文件预览。
|
||||
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git"` 通过 11 项,
|
||||
覆盖应用中的文件预览、路径复制、默认应用打开、返回和任务结束刷新。
|
||||
- 本轮 `npm test` 完整运行结果为 3629 通过、61 跳过、5 失败。失败位于 Agent 离线
|
||||
依赖打包超时、DeepSeek Harness MCP 加载、Runtime 版本探测,以及两个打包产物检查,
|
||||
不将该结果记为整库回归通过。
|
||||
- `npm run lint` 和差异空白检查通过。最新 `npm run typecheck` 被同时进行的浏览器
|
||||
接口变更阻塞:测试替身缺少 `hide`、`setBounds`、`setViewport`,部分侧栏测试仍传递
|
||||
已移除的浏览器回调。该轮类型检查未通过,未改动另一项任务的接口或测试替身。
|
||||
- 尚未进行真实桌面视觉验收;组件测试的滚动断言不代替实际窗口中的布局和滚动检查。
|
||||
- 范围仅限工作区页签,不包含项目文件搜索或其他页签改动。
|
||||
|
||||
## 2026-09-09 托管 Runtime 并行阻塞
|
||||
|
||||
- Windows 完整隔离测试程序通过界面发送 OpenCode 原生双子代理、直连与 Continue 文件
|
||||
任务;随后三路上游请求在 917 ms 内发出并重叠,全部完成。普通并发没有复现全局排队。
|
||||
- 在同一程序中,让真实模型执行一次 4,000 行、约 98 KB 文件的 CRLF → LF 转换,
|
||||
同时运行另一个会话。原生进程的无凭据存活探针两次延迟 3,301 ms、3,085 ms;
|
||||
原生热线程持续使用 CPU,而 Main 没有同步满载。操作不是模拟事件或直接调用
|
||||
Runtime 的脚本,模型请求来自实际输入框和发送按钮。
|
||||
- 托管 OpenCode 关闭未消费的自动 Git 快照后,完整程序重复相同操作和并行会话,
|
||||
90 秒观察中的最大原生探针延迟为 210 ms,没有超过 500 ms 的探针,两条请求均完成。
|
||||
该对照验证已复现的快照阻塞得到消除,不声称已经证明历史 90–125 秒事件的唯一原因。
|
||||
- Continue 的实际 Electron 原生宿主在不排空 stdout 时被注入的 128 KiB 输出阻塞;
|
||||
使用当前生产适配器后,输出在 5 ms 内完成,20 次状态读取全部成功,最大 5 ms。
|
||||
这证明管道修复有效,不代表普通启动约 640 字节的输出就是历史卡顿原因。
|
||||
- 修复版完整程序再次完成原生双子代理与 Continue 文件任务,实际请求重叠,三个页面
|
||||
都通过计数按钮功能检查。Windows 诊断与程序验证合计 40 次成功模型调用;
|
||||
另一次 UI 请求返回账户额度不足的 402,用户充值后才继续,没有把失败当成通过。
|
||||
- 当前源码 Linux x64 Agent 经桌面生产管理链路完成 Ask 读取和 Execute 换行转换,
|
||||
共 4 次成功模型调用;最终原生配置确认 `snapshot: false` 且权限仍为 `ask`/`allow`。
|
||||
此验证覆盖实际 Agent、模型桥、工具完成和关闭,不量化 Linux 提速,也未注入取消。
|
||||
- 定向回归先验证 7 项失败,再验证 124 项通过、5 项 Windows 平台跳过;
|
||||
全项目类型检查和修改文件 lint 已通过。首次全量回归为 3625 通过、1 失败、61 跳过;
|
||||
失败来自生命周期测试的包装流在底层 reader 关闭后仍计数为打开,已通过独立流实验
|
||||
复现并改为观察 `reader.closed`,没有因此修改生产取消逻辑。最终候选全量回归为
|
||||
3626 通过、0 失败、61 跳过,`npm run typecheck`、`npm run lint`、
|
||||
`npm run release:notes:verify`、差异空白与文档相对链接检查通过。本机和远端的
|
||||
测试进程已回收,安装版和正式 Host 环境未改动;正式发布仍需候选 CI 与资产核实。
|
||||
运行策略见
|
||||
[托管 Runtime 并行与输出](./runtime-interactions.md#托管-runtime-的并行与输出)。
|
||||
|
||||
## 2026-09-08 消息底部请求状态
|
||||
|
||||
- 已接入本机 OpenCode 父会话的原生重试等待及开始事件;直连模型把退避等待与实际发起
|
||||
重试分开显示,Continue 初始文案改为中性的请求处理。显示规则和未接入边界见
|
||||
[消息底部请求状态](./runtime-interactions.md#消息底部请求状态)。
|
||||
- 消息底部按真实事件显示准备、工具、待回答及终态;有新进展时清除旧状态,结束后不
|
||||
接受迟到状态覆盖。仅该处状态点改为静态,会话列表闪点、发送和停止按钮未改动。
|
||||
- Windows 隔离开发桌面通过实际 UI 发送验证 OpenCode 的 503/Retry-After 等待、
|
||||
原生重试开始、真实模型回复与完成;直连模型通过派发前连接拒绝、500 ms 等待、
|
||||
重试、真实回复与完成。故障由本机测试代理注入,不冒充服务商实际故障。
|
||||
三个隔离测试配置总计 5 次真实文本模型调用;另有一次直连重试被测试代理的调用
|
||||
上限明确拒绝,界面正确显示失败,最终单次补测完成。未执行用户命令或修改安装版。
|
||||
- DOM 状态记录确认底部无闪烁、完成后无旧重试文案,浅色截图可见原生重试计划与原因;
|
||||
底部 WCAG 2 A/AA 定向检查无违规。最小化窗口的补充截图超时,未作为通过证据。
|
||||
隔离窗口及测试代理已结束。
|
||||
- 最终 `npm test -- --reporter=json --outputFile=<临时报告路径>` 为 3618 项通过、
|
||||
0 项失败、61 项按平台或手动条件跳过;首轮的压缩状态旧断言已修正,途中被用户中断
|
||||
的运行不计为通过。`npm run typecheck`、`npm run lint`、
|
||||
`npm run release:notes:verify`、差异空白与相关文档相对链接检查通过。
|
||||
|
||||
## 2026-09-08 Desktop 0.12.8 安装包修复
|
||||
|
||||
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
|
||||
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
|
||||
后,插件及传递依赖逐文件复制验证通过。
|
||||
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
|
||||
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
|
||||
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
|
||||
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
|
||||
不能把当前源码回归通过当作真实发布包已通过。
|
||||
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
|
||||
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
|
||||
|
||||
## 2026-09-08 Execute 目录权限等待修复
|
||||
|
||||
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
|
||||
禁用工具,当前请求精确分配的只读能力例外。Execute 不把 Workspace 当作目录边界。
|
||||
- `permission.asked` 不再忽略所有非父会话事件。Main 用 OpenCode Session 的 `parentID`
|
||||
确认属于当前请求的子孙会话后按当前模式回复;其他并行会话不受影响,同一请求只回复
|
||||
一次。子会话权限不伪装为父会话中的待完成工具。
|
||||
- 真实 OpenCode `1.18.9` 和候选 `1.18.29` 均已通过本地确定性模型驱动的原生 Task
|
||||
工作区外读取回归;同一 Runtime 的 Ask 仍不暴露文件与命令工具。
|
||||
- Continue `1.5.47` 的真实 CLI 已通过 Execute Shell 写入工作区外测试文件;DSH 固定
|
||||
Host 已通过 Execute 工作区外写入、随后 Ask 拒绝同一写工具,未增加权限策略或人工审批。
|
||||
- 真实 OpenCode `1.18.29` 已完成原生 Task 后的 yes/no 与自由文本问答回传;父、子会话
|
||||
提问及后台任务不能交互时的失败路径均有回归测试。Continue 真实 `AskQuestion` 暴露了
|
||||
QuizService 扁平数据结构与原适配器不一致的问题,现已按锁定版本修正;真实自由文本
|
||||
回传、yes/no、选项及跳过均验证通过,自定义答案不再误标为选项。
|
||||
- DSH 活跃 Execute 的 ACP 权限回调不再等待第二次 authorizer,Ask 继续拒绝;未添加
|
||||
通用业务问答工具或从终端日志猜测确认意图。完整范围见
|
||||
[Runtime 交互边界](./runtime-interactions.md)。
|
||||
- 当前源码 Linux x64 Agent 通过签名组包、生产安装、真实 Ask/Execute、原生子代理
|
||||
工作区外写入、重连同一 daemon 和 stop/bootstrap。实机回归同时定位并修复了握手
|
||||
Base64URL 响应误用 ID 校验及断流未结算等待;13 项 Unix 原生端点测试和连续 20 次
|
||||
握手通过。两轮真实模型调用合计 10 次,均 completed;本轮测试 Agent 已停止。
|
||||
- 最终完整回归使用 `npm test -- --reporter=json --outputFile=<临时报告路径>`,并通过
|
||||
`GOODBUDDY_TEST_OPENCODE_BINARY` 指向已验证版本的候选 OpenCode。结果为 338 个文件、
|
||||
3597 项通过、0 项失败、61 项按平台/手动条件跳过,报告 `success=true`、退出码 0;
|
||||
Windows 跳过的 Unix 端点场景另在 Linux Host 运行了上述 13 项原生测试。
|
||||
- 最终 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 和差异空白检查
|
||||
通过,相关 Markdown 相对链接有效。此前失败、暂停及控制台截断的回归记录由本次完整
|
||||
JSON 报告覆盖,不代表仍有测试失败。未执行系统休眠/唤醒或本地桌面生产构建、打包;
|
||||
后续发布仍需对应候选的 CI 构建和跨平台验证。
|
||||
|
||||
## 2026-09-08 OpenCode 复用与原生控件恢复
|
||||
|
||||
- `OpenCodeRuntime` 仅在没有活动请求时探测缓存的 embedded server;活动对话或
|
||||
Compact 期间直接复用服务,避免新会话的一次短探测失败关闭其他会话正在使用的进程。
|
||||
空闲探测失败时仍清除旧 client 和会话映射并重建;并发调用共享检查与重建。
|
||||
进程退出或服务关闭会中止绑定它的对话与 Compact,事件流重连等待也可立即取消,
|
||||
请求明确报错,不自动重放已执行命令。本次修复后的真实模型验证共 6 次:正常并发、
|
||||
已复现场景下的共享服务保护均正常完成;主动结束测试进程后旧请求明确失败,新请求
|
||||
在替换服务上完成。此前“新会话完成、旧会话持续等待”的故障不再出现。
|
||||
- 请求级 MCP 注册与断开通过实例内 `mutateMcp` 队列串行执行。仅实际发出 `mcp.add`
|
||||
的名称进入清理列表;注册尚在排队时取消不追加断开。断开的 1 秒超时从出队后开始。
|
||||
- `App` 按本机/远程、连接方式、项目和 Runtime 选择组成 scope 缓存原生清单,用请求
|
||||
代次阻止旧 scope 结果回写;同 scope 会话刷新不清空重取,切换会话重置消息级选择,
|
||||
Runtime 切换状态也按 generation 隔离。清单请求失败或返回 `unavailable`/`partial`
|
||||
时保留已有可用缓存并有界重试;没有可用缓存时保留新返回的部分数据,因此
|
||||
`unavailable` 重试为可用 `partial` 后可显示 Agent 和 Command。
|
||||
|
||||
验证记录:首轮定向测试 273 项通过、5 项跳过;补充修复后的 Runtime 与生命周期测试
|
||||
77 项通过,界面定向测试 6 项通过,类型检查通过。修复前的 lint 检查通过。
|
||||
全量测试超过 120 秒被终止,期间出现一项 DS 本地 stdio MCP 测试失败,尚待定位。
|
||||
这是当次历史结果。后续发布前修复与实机验证见本文上方及
|
||||
[远程主机技术设计](../remote-host/technical-design.md);真实系统休眠恢复仍未完成验证。
|
||||
|
||||
## 2026-09-06 项目工作区
|
||||
|
||||
范围见 [PRD 工作区](./prd.md#76-工作区)。
|
||||
|
||||
当前源码已移除重复标题与底部完整 Diff 入口,使用彩色 Git 状态字母,点击更改项读取
|
||||
单文件 Diff。已暂存与未暂存差异分开展示;本机 Git 测试覆盖无首次提交、未跟踪、
|
||||
暂存后继续修改、重命名、删除及包含方括号的路径。Git 刷新驱动目录树重新读取,保留
|
||||
展开状态;SSH 项目不提供本机系统打开按钮。审查后补齐了当前 Diff 随刷新重读、
|
||||
旧请求结果失效,以及超过 50 项变更的继续加载入口。
|
||||
|
||||
验证记录:
|
||||
|
||||
- 最终 `npm run typecheck`、`npm run lint`、`npm run build` 均通过。
|
||||
- 工作区专项 7 个测试文件复跑:317 项通过,3 项 Linux 专用测试在 Windows 跳过。
|
||||
任务完成测试同时检查目录树出现新文件。
|
||||
- 最终 `npm test`:326 个测试文件、3552 项测试通过,9 个文件、58 项测试按既有条件
|
||||
跳过。复核首轮曾有一项浏览器地址草稿测试超时,单独复跑及最终全量复跑均通过;
|
||||
更早的超时/失败记录不再作为最终验收结果。
|
||||
- 后续复核已通过既有凭据和固定 Host Key 经 VPN 连接共享 Linux x64 Host,使用当前
|
||||
源码构建的隔离 Agent 包安装并启动。真实 `RemoteWorkspaceAccess → Agent git/status`
|
||||
与 `git/diff` 路径覆盖暂存后继续修改、删除、重命名、未跟踪和方括号路径;均通过。
|
||||
Git 验证未调用模型。
|
||||
- Windows 隔离桌面实例已通过真实点击验收:当前项目单文件暂存/工作树 Diff 分组与
|
||||
增删颜色正确,修改测试文件后点击刷新可更新正在查看的 Diff,53 项变更经加载更多
|
||||
全部可访问。截图留在本地验证目录,不纳入产品包。
|
||||
|
||||
目录分页、任意目录切换和 HTML 预览不在本轮改动范围内。
|
||||
@@ -0,0 +1,92 @@
|
||||
# Runtime 交互边界
|
||||
|
||||
本文定义当前原生交互的适配范围,不增加工作模式或第二层 Execute 审批。
|
||||
|
||||
| Runtime | 权限确认 | 业务问答 |
|
||||
| --- | --- | --- |
|
||||
| 本机 GoodBuddy 管理的 OpenCode | 内部默认配置允许权限;每个 Ask 请求仍使用 deny-all 与显式只读能力。Execute 自动回复属于当前请求的父、子孙会话权限事件,不处理其他并行请求 | 通过 `question.asked` 转交父、子孙会话问题,支持原生单选、多选、yes/no、自由文本及跳过;使用公开 ID 回传 `question.reply/reject` |
|
||||
| 托管 SSH OpenCode | Agent 按已接受 Prompt 的 Ask/Execute 模式处理 ACP 权限,规则见远程主机技术设计 | 当前 Agent 插件转交原生父、子孙会话 `question.asked`;前台使用同一问答队列,回答和跳过经认证 Agent 控制通道回传,已知 Task 关联显示子任务归属 |
|
||||
| Continue | Execute 启动传入 `--auto`,剩余原生权限请求按当前模式自动答复 | 对接 `1.5.47` QuizService 的扁平 `pendingQuestion`,其中 `question` 是字符串。选项回答与自定义文本分别回传正确的 `isCustomAnswer`;跳过发送拒绝回答的文本 |
|
||||
| DeepSeek Harness | 固定 Host 的 Execute 文件与命令工具不询问目录权限;若收到 ACP 权限请求,活跃 Execute 直接选一次性允许,缺少该选项时使用 Runtime 提供的允许选项,Ask 拒绝,不等待额外 authorizer | 固定 Host 当前未提供通用原生业务问答服务,不把普通模型正文误识别成待答协议。模型以普通回复提问时,用户通过下一条消息回答 |
|
||||
|
||||
托管 SSH OpenCode 的权限、原生问题事件与回复链路由
|
||||
[远程主机技术设计](../remote-host/technical-design.md)定义。本次候选的远端业务问答需要
|
||||
Desktop `0.13.0`、Agent `0.11.24` 和新启动的托管 Runtime;请先升级 Desktop,再更新
|
||||
Host 环境。任意第三方 ACP 服务及旧 Agent 不因此获得通用问答接口,候选不表示已经发布。
|
||||
|
||||
## 运行与失败
|
||||
|
||||
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
|
||||
- 前台同一请求的待答问题按公开 `questionId` 去重,按到达顺序保存在消息的
|
||||
`pendingQuestions` 中。每次显示队首表单;有多个待答请求时显示数量。后续提问和重复
|
||||
事件不替换当前表单或清空输入,回答或跳过成功后只移除对应 ID,再显示下一张表单。
|
||||
提交失败保留队列、输入和重试入口,队列未清空时继续显示“等待你的回答”。
|
||||
- 本机与托管 SSH OpenCode 在提问到达前已收到原生 Task 的 `metadata.sessionId` 时,将精确匹配的
|
||||
子会话关联为可选 `childTaskId`。问答卡片显示该任务名称;对应 Task 卡片显示
|
||||
“等待你的回答”,展开后可点击“前往问答表单”滚动并聚焦当前队首表单,仍按顺序回答。
|
||||
没有已知关联的子孙会话问题照常排队,不猜测子任务归属,也不从工具名或进度文本构造表单。
|
||||
- 回答或跳过成功后,会话保留只读的“问题与回答”,显示原问题及对应的选项答案、
|
||||
自定义文本或“已跳过”。同一任务的多轮问答逐次保留,随本地会话保存,重新打开后仍可查看。
|
||||
提交失败时保留输入与重试入口,不生成已回答记录;取消未回答的问题不记为主动跳过。
|
||||
- 提交响应晚于任务结束时仍保存已提交的问答,但不覆盖完成或失败状态;晚于下一轮提问时
|
||||
不清除新的待答卡片。旧版会话未保存的问题与答案无法从“已提交”状态文案恢复。
|
||||
- 待答队列仅保存在当前桌面会话内存中,不写入会话快照。刷新仍在运行的消息时保留整个
|
||||
本地队列;完成、失败、取消事件或持久化终态快照清除整个队列。迟到的提交结果可以保存
|
||||
已回答记录,但不会恢复表单或覆盖终态。本机 Runtime 不恢复重启前未提交的问题;托管 SSH
|
||||
重新附加同一存活 Agent Prompt 时,由 Agent 当前待答列表补发问题,已回答的历史事件不补发。
|
||||
- 后台任务收到结构化提问时,通过现有 Main 路径明确失败并提示改为前台对话,不永久等待。
|
||||
- 请求结束或取消后清除对应待答映射;过期回答明确报错,不转交给后来的请求。
|
||||
- 任意终端程序的密码、OAuth 登录、许可证确认或 stdin 提示不自动回复 `yes`。这类程序应
|
||||
使用其非交互参数,或由用户在终端完成;不能把权限确认和业务决策混为一谈。Shell
|
||||
工具保留其现有取消与超时,不新增日志解析或通用交互代理。
|
||||
|
||||
## 托管 Runtime 的并行与输出
|
||||
|
||||
子任务完整过程的显示与数据库差量存储相互独立,本机和托管 SSH 共用桌面存储层;
|
||||
旧库升级与空间回收规则见[执行记录存储与升级回收](./execution-history-storage.md)。
|
||||
|
||||
- 本机和 Agent 管理的 OpenCode 启动配置都显式设置 `snapshot: false`。GoodBuddy 不消费
|
||||
原生自动 Git 快照、快照差异摘要或快照撤销;关闭这条内部路径,避免大行级差异的同步
|
||||
计算阻塞同一原生进程中的其他会话。
|
||||
- 文件工具、Shell、原生子代理、工作区 Git 状态和单文件差异保持可用;不新增全局会话
|
||||
队列、请求重放或总执行截止时间。外部 OpenCode Server 的配置不由 GoodBuddy 改写,
|
||||
已运行的旧原生进程也不会热更新配置。
|
||||
- Continue 使用 HTTP 传递响应和状态;Electron UtilityProcess 启动后持续排空不用展示的
|
||||
stdout,避免控制台输出填满管道后阻塞宿主。stderr 的有界错误处理、请求取消与进程
|
||||
回收保持不变,不把诊断输出复制进聊天或持久日志。
|
||||
- 远端最终启动配置由[远程主机技术设计](../remote-host/technical-design.md)定义,
|
||||
模型桥生成最终配置时同样保留上述快照策略。
|
||||
|
||||
## 工具操作摘要
|
||||
|
||||
工具记录沿用 [UI 设计系统](../../../UI-DESIGN.md)的折叠布局。Continue、DeepSeek Harness
|
||||
和直连模型共用 `toolOperationSummary`:优先使用有效原生标题,否则从参数提取命令、
|
||||
文件路径、搜索条件或 URL,合并空白后最多显示 240 字符。无法识别时保留原有工具名
|
||||
回退规则;文件正文、整份参数 JSON 不进入标题,参数详情仍可展开查看。
|
||||
|
||||
Continue 在宿主截断输入前提取摘要,流式完成与失败事件使用合并后的输入和摘要。
|
||||
DeepSeek Harness 按调用 ID 保留名称、输入和摘要,结束事件省略输入时仍能显示操作内容。
|
||||
直连模型从原始参数生成一次摘要,在待执行、执行中和完成时复用;审批失败、拒绝和
|
||||
执行失败、刷新重试仍保留已有提示。
|
||||
|
||||
本机 OpenCode 沿用原生操作标题。远端 ACP 独立使用原生标题作为记录名称,不经过上述
|
||||
本地转换,本次没有修改 Agent、远程桥或协议。
|
||||
|
||||
## 消息底部请求状态
|
||||
|
||||
- 状态属于当前请求,不与顶部 Runtime 的连接中、就绪、不可用混为一谈。提交后先显示
|
||||
“正在准备请求”;没有更具体的活动事件时显示“等待后续进展”,不声称模型正在思考。
|
||||
- 本机 OpenCode 只把当前父会话的 `session.status.retry` 映射为等待重试,显示原生
|
||||
`attempt`、计划重试时间和有界脱敏原因;后续 `busy` 才显示已开始重试。其他会话的
|
||||
状态不串入当前消息,不猜测总重试次数,不把 SDK 事件流重连当作模型重试。
|
||||
- 显示优先级为待回答、待确认、进行中的上下文压缩、明确的请求状态、待执行或执行中的
|
||||
工具、未完成的子任务、等待后续进展。压缩沿用已有活动块,不重复增加底部提示。
|
||||
正文、推理或工具进展会清除旧状态;子任务更新不覆盖父请求的重试提示。
|
||||
- 完成、失败、取消后保留真实终态,迟到的状态事件不能把请求恢复成处理中。底部状态点
|
||||
为静态,不凭请求尚未结束暗示仍有进展;会话列表闪点和发送、停止按钮保持原有行为。
|
||||
- Continue、DeepSeek Harness 和远端 ACP 保留其已接入的处理与工具事件,不伪造未接入
|
||||
的原生重试状态。直连模型的重试等待与请求开始文案见
|
||||
[直连模型 UI 设计](../direct-model-agent/ui-design.md#6-错误与通知);本次不改变任何
|
||||
Runtime 的重试上限、请求重放或取消策略。
|
||||
|
||||
实现与真实 Runtime 验证证据见[进度记录](./progress.md)。
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台验证中 |
|
||||
| 版本 | 1.1 |
|
||||
| 日期 | 2026-08-31 |
|
||||
| 版本 | 1.2 |
|
||||
| 日期 | 2026-09-10 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 界面归属 | [通用助手工作栏与执行空间](./prd.md) |
|
||||
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
- 已打开应用以同级 Tab 呈现。
|
||||
- Tab 行始终提供“+”入口,用户按需打开应用。
|
||||
- 应用通过统一注册表声明单实例或多实例策略。
|
||||
- 应用通过统一注册表分别声明单实例或多实例、默认打开、可关闭和排序策略。
|
||||
- “终端”作为首个多实例应用,自动使用当前项目的本机或托管 SSH 执行空间。
|
||||
- 本机与远程交互式终端的生命周期、状态、性能和验收要求。
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
当前工作栏在聊天页面右侧固定显示“任务中心、工作区、浏览器、成果”四个 Tab。用户不能:
|
||||
|
||||
- 通过统一入口按需打开新的工作栏应用。
|
||||
- 关闭并重新打开一个工作栏应用。
|
||||
- 按应用策略关闭并重新打开可关闭的工作栏应用。
|
||||
- 为支持多实例的应用创建第二个独立实例。
|
||||
- 在 GoodBuddy 内使用本机或远程 Host 的交互式 Shell。
|
||||
|
||||
@@ -49,17 +49,51 @@ SSH 连接池,但尚不存在工作栏应用实例模型、终端 IPC、终端
|
||||
- 稳定应用 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:
|
||||
@@ -81,10 +115,10 @@ stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或
|
||||
5. 单实例应用已经打开时,再次选择只聚焦已有实例,不创建重复 Tab。
|
||||
6. 多实例应用每次选择都创建新实例。
|
||||
7. 终端从首期开始就是多实例应用。
|
||||
8. 任务中心、工作区、浏览器和成果首期保持单实例;后续只需修改应用注册表策略即可开放
|
||||
多实例,不重写 Tab 壳层。
|
||||
9. 现有四个应用在升级后仍按当前默认布局打开,避免用户入口突然消失。
|
||||
10. 用户关闭、切换或重排应用 Tab 后,“+”仍保持可见。
|
||||
8. 任务中心和工作区保持单实例、默认打开且不可关闭,分别固定在第一、第二位,`reorderable` 为 `false`。恢复旧布局时也按此规则修正顺序,其余 Tab 保持相对顺序。
|
||||
9. 浏览器注册为可关闭的多实例应用,升级后保留一个默认实例;成果保持单实例、默认打开且
|
||||
可关闭。
|
||||
10. 用户关闭可关闭实例、切换或重排应用 Tab 后,“+”仍保持可见。
|
||||
11. 选择“终端”后不再显示第二层目标选择器。存在当前项目时直接绑定该项目,由 Main
|
||||
根据项目执行空间自动打开本机或托管 SSH Shell;没有当前项目时打开本机 Home Shell。
|
||||
12. 远程首期使用直接 SSH PTY。SSH 断线后终端进入“已中断”,重新连接会创建一个新的
|
||||
@@ -109,7 +143,8 @@ stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或
|
||||
### 5.2 产品目标
|
||||
|
||||
- 用一个可扩展的应用注册表统一单实例与多实例行为。
|
||||
- 把现有固定四 Tab 迁移为用户实例,而不回归现有任务中心、工作区、浏览器和成果能力。
|
||||
- 把现有 Tab 迁移到统一实例模型,同时保持任务中心和工作区始终存在,并允许浏览器和终端
|
||||
创建多个实例。
|
||||
- 建立真实的 Main/Preload/Renderer 终端生产链路,而不是用普通文本框模拟终端。
|
||||
- 复用现有 SSH Host Key、凭据和连接池,不建立第二套 Host 管理。
|
||||
- 保持终端输入输出有序、响应流畅、内存有界,并可靠清理本机和远程资源。
|
||||
@@ -149,7 +184,8 @@ stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或
|
||||
- 当前 Tab 同时使用连续内容表面、选中边框、顶部强调线、图标和较高字重,不能只靠文字颜色。
|
||||
- Tab、关闭按钮和“+”入口沿用共享控件圆角;终端面板外框和终端顶部工具栏按钮使用直角,
|
||||
保持紧凑的停靠式终端界面。
|
||||
- Tab 关闭按钮默认不争夺视觉注意力,仅在鼠标悬停或键盘焦点进入对应 Tab 时出现;
|
||||
- 只有可关闭实例渲染 Tab 关闭按钮。关闭按钮默认不争夺视觉注意力,仅在鼠标悬停或键盘
|
||||
焦点进入对应 Tab 时出现;
|
||||
粗指针或无悬停设备始终显示,出现与隐藏不得引发布局位移。
|
||||
|
||||
### 7.2 新建应用选择页
|
||||
@@ -160,31 +196,38 @@ stdio fallback。首期远程终端随 SSH Channel 结束,不提供跨 SSH 或
|
||||
2. 在工作栏内容区显示“新建工作栏应用”。
|
||||
3. 列出稳定应用目录,每项显示名称、说明、单/多实例提示和当前可用状态。
|
||||
4. 初始焦点进入第一个可用应用;Escape 或“取消”返回原活动实例和“+”按钮。
|
||||
5. 选择单实例应用时,若实例已存在,直接聚焦该 Tab。
|
||||
6. 选择终端时直接解析当前项目执行空间,创建新 Tab 并聚焦终端输入,不再要求用户
|
||||
5. 选择单实例应用时,若实例已存在,直接聚焦该 Tab;任务中心和工作区始终走此路径。
|
||||
6. 选择浏览器时创建绑定当前 Conversation 的新 Browser Tab;没有活动 Conversation 时创建
|
||||
待选择目标的空实例。
|
||||
7. 选择终端时直接解析当前项目执行空间,创建新 Tab 并聚焦终端输入,不再要求用户
|
||||
重复选择本机或远程目标。
|
||||
|
||||
选择页是“+”触发的临时界面,不进入实例顺序,不持久化,也不占用一个普通 Tab。
|
||||
|
||||
### 7.3 首期应用注册表
|
||||
|
||||
| 应用 | 实例策略 | 升级后默认状态 | 再次从“+”选择 |
|
||||
| --- | --- | --- | --- |
|
||||
| 任务中心 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 工作区 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 浏览器 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 成果 | `single` | 打开 | 聚焦已有 Tab |
|
||||
| 终端 | `multiple` | 不打开 | 每次创建新 Tab |
|
||||
| 应用 | 实例策略 | 默认上下文 | 默认打开 | 必须存在 | 可关闭 | 可排序 | 再次从“+”选择 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 任务中心 | `single` | 当前项目 | 是 | 是 | 否 | 否 | 聚焦已有 Tab |
|
||||
| 工作区 | `single` | 当前项目 | 是 | 是 | 否 | 否 | 聚焦已有 Tab |
|
||||
| 浏览器 | `multiple` | 当前 Conversation | 是,一个实例 | 否 | 是 | 是 | 每次创建新 Tab |
|
||||
| 成果 | `single` | 当前项目 | 是 | 否 | 是 | 是 | 聚焦已有 Tab |
|
||||
| 终端 | `multiple` | 当前项目 | 否 | 否 | 是 | 是 | 每次创建新 Tab |
|
||||
|
||||
实例策略是应用注册数据,不能分散硬编码在多个点击处理器中。
|
||||
实例策略、默认状态和可关闭策略都是应用注册数据,不能分散硬编码在多个点击处理器中。
|
||||
|
||||
新建浏览器使用独立编号标题,避开已有名称;关闭后再次新建也不产生重名。恢复布局保留已保存
|
||||
的标题。会话名称用于标识归属,不替代浏览器实例名称。新建页面为空白页,同会话 Cookie 共享
|
||||
不表示复用旧页面或导航历史。
|
||||
|
||||
### 7.4 Tab 操作
|
||||
|
||||
- 点击或使用方向键切换实例不会暂停或终止后台实例。
|
||||
- 支持左右方向键、Home、End 和 roving `tabindex`。
|
||||
- 关闭非活动 Tab 后焦点保持在当前活动 Tab。
|
||||
- 关闭活动 Tab 后激活右侧相邻实例;没有右侧实例时激活左侧实例。
|
||||
- 关闭最后一个实例后显示明确空状态和“打开应用”,Tab 行仍保留“+”。
|
||||
- 任务中心和工作区没有关闭按钮,也不响应关闭快捷命令。
|
||||
- 关闭可关闭的非活动 Tab 后焦点保持在当前活动 Tab。
|
||||
- 关闭可关闭的活动 Tab 后激活右侧相邻实例;没有右侧实例时激活左侧实例。任务中心和工作区
|
||||
始终存在,因此正常布局不会进入零实例空状态。
|
||||
- 关闭正在运行的终端需要确认“关闭终端将结束 Shell 及其子进程”;取消是初始焦点。
|
||||
- 已退出或已中断的终端可以直接关闭,不重复确认。
|
||||
|
||||
@@ -284,21 +327,45 @@ type TerminalSessionState =
|
||||
```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: 'project' | 'local'
|
||||
id?: 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。
|
||||
@@ -474,16 +541,19 @@ Schema、假数据终端或不可用入口的中间产品。
|
||||
|
||||
### 18.1 工作栏应用与实例
|
||||
|
||||
- [ ] Tab 行末始终显示“+”,包括没有任何打开实例时。
|
||||
- [ ] Tab 行末始终显示“+”;任务中心和工作区始终存在,因此正常布局不存在零实例状态。
|
||||
- [ ] 点击“+”显示稳定应用目录,取消后返回原实例并恢复焦点。
|
||||
- [ ] 选择已打开单实例应用只聚焦已有 Tab。
|
||||
- [x] 任务中心和工作区默认存在且不可关闭,不渲染关闭按钮,也不响应关闭快捷命令。
|
||||
- [x] 连续选择浏览器会创建页面、导航状态和元素引用相互独立的浏览器 Tab。
|
||||
- [x] 连续选择终端会创建多个互相独立的终端 Tab。
|
||||
- [ ] 关闭、切换和重排实例后,“+”仍保持可用。
|
||||
- [ ] 关闭可关闭实例、切换和重排实例后,“+”仍保持可用。
|
||||
- [ ] Tab 溢出时单行横向滚动,不隐藏“+”或把 Tab 换成下拉框。
|
||||
- [x] 关闭活动 Tab 后激活正确的相邻 Tab 并恢复焦点。
|
||||
- [x] 关闭可关闭的活动 Tab 后激活正确的相邻 Tab 并恢复焦点。
|
||||
- [x] 当前 Tab 的背景、边框、顶部指示和字重清晰可辨;关闭按钮仅在悬停、焦点或无悬停
|
||||
设备上出现,且不引发布局位移。
|
||||
- [ ] 升级后现有任务中心、工作区、浏览器和成果仍按默认布局打开且行为不回归。
|
||||
- [x] 升级后任务中心、工作区、一个浏览器实例和成果按默认布局打开;布局恢复会补回缺失的
|
||||
任务中心与工作区。
|
||||
- [ ] 在所有主要页面都能打开工作栏;切换主要页面不会销毁 Tab 实例或终端 Session。
|
||||
|
||||
### 18.2 本机终端
|
||||
@@ -522,7 +592,8 @@ Schema、假数据终端或不可用入口的中间产品。
|
||||
|
||||
### 18.5 设计、无障碍和跨平台
|
||||
|
||||
- [ ] 仅使用键盘可以打开应用、创建终端、切换和关闭 Tab、复制、粘贴及重新连接。
|
||||
- [ ] 仅使用键盘可以打开应用、创建浏览器和终端、切换 Tab、关闭可关闭 Tab、复制、粘贴及
|
||||
重新连接。
|
||||
- [ ] Tab、“+”、选择页和关闭确认使用正确语义并有可见焦点。
|
||||
- [ ] 终端输出不逐行进入 live region,关键状态变化可被辅助技术感知。
|
||||
- [ ] 浅色、深色、200% 文字缩放和窄窗口下可以完成核心流程。
|
||||
@@ -535,7 +606,9 @@ Schema、假数据终端或不可用入口的中间产品。
|
||||
### 19.1 自动验证
|
||||
|
||||
- 共享 Zod Schema 和边界值测试。
|
||||
- 单/多实例注册策略、Tab 顺序、关闭焦点和“+”选择页组件测试。
|
||||
- 单/多实例、默认打开、可关闭和排序策略,以及 Tab 顺序、关闭焦点和“+”选择页组件测试。
|
||||
- 任务中心与工作区缺失时的布局修复、关闭命令拒绝和焦点保持测试。
|
||||
- 浏览器多实例的页面隔离、Conversation 匹配和快捷入口去重测试。
|
||||
- 本机 PTY Session Manager 的输入、resize、退出、幂等关闭和进程树测试。
|
||||
- SSH PTY 的 Host identity、lease、断线、resize 和关闭测试。
|
||||
- Main trusted sender、Session 所有权、IPC 输入边界和事件路由测试。
|
||||
|
||||
@@ -92,14 +92,11 @@ 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)。
|
||||
|
||||
### 已实现:浏览器工具栏
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
## 1. 背景
|
||||
|
||||
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。其长期“未来分区记忆”
|
||||
方向尚未设计,也不承担持续学习、模式挖掘或自动改进:
|
||||
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。它与分区记忆的协同尚未
|
||||
实施,也不承担持续学习、模式挖掘或自动改进:
|
||||
|
||||
- 候选是否改善未来行为没有评估。
|
||||
- 一条反思是否会被检索和使用并不确定。
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [技术设计](./technical-design.md) | Runtime 产品与技术方案、协议、插件、打包、测试和验收 |
|
||||
| [实现进度](./progress.md) | Windows 工作区路径修复及验证记录 |
|
||||
|
||||
现有文档同时包含部分 PRD、功能逻辑和进度内容,后续修改该 Runtime 时再按标准职责拆分。
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# DeepSeek Harness 实现进度
|
||||
|
||||
## 2026-09-14 Windows 工作区路径修复
|
||||
|
||||
本机 Runtime 的 Host 启动与 `newSession.cwd` 已共用初始化时解析的 `realpath`,
|
||||
规则见[技术设计](./technical-design.md#52-受控组合不启动用户-profile)。修复此前
|
||||
`C:/...\workspace` 写法触发 ACP `-32602`,以及随后换成同目录规范写法仍复用
|
||||
错误 Runtime 的问题。Host 严格比较与 execution-space 缓存规则保持原样。
|
||||
|
||||
新增 Windows 真目录回归,连接真实 ACP、Host 和控制面,分别用混合分隔符及规范
|
||||
路径创建会话,断言共用同一 Runtime、只启动一次 Host、两次均完成。该自动化回归
|
||||
使用内存模型;外部真实模型验证单独由完整 App 完成。
|
||||
|
||||
Windows 完整 App 使用当前源码构建的 Main、Preload、Renderer 和新建隔离配置。
|
||||
正式配置只读,模型凭据由隔离配置中的 safeStorage 解密;真实 composer 输入请求,
|
||||
由 `deepseek-v4-flash` 在 Ask 模式读取 `summary-probe.txt`,校验码未放入 prompt。
|
||||
|
||||
| 目录写法 | Composer 提交 | 真实模型 HTTP dispatch | 文件读取 | 结果 |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Windows 混合分隔符 | 1 | 2 | 1 | 仅返回文件中校验码,任务完成 |
|
||||
| 同目录规范路径 | 1 | 2 | 1 | 仅返回文件中校验码,任务完成 |
|
||||
| 合计 | 2 | 4 | 2 | 两条会话均通过 |
|
||||
|
||||
两轮 dispatch 来自同一 DS Utility Host 进程,切换目录写法未新增 Host;ACP 会话
|
||||
ID 不同。两条工具卡片均显示路径摘要,原生点击展开后可查看输入与输出;重载
|
||||
Renderer 并重新选择项目后,已保存会话的摘要、详情和完成状态仍通过检查。四张
|
||||
局部截图已逐张打开审阅。驱动曾返回不可克隆的监听器函数,以及在重载后默认项目
|
||||
等待工具卡片超时;修正定位后通过,这两次驱动错误没有增加模型请求。
|
||||
|
||||
本机临时 `ds-windows-path-live/report.json`、`dispatch.jsonl` 与 `source.json`
|
||||
记录结果、逐请求计数和构建哈希,原 `runtime-summary-live` 报告未覆盖。
|
||||
本次 App 正常退出,退出前记录的 5 个所属进程均已停止;隔离 profile、数据库、
|
||||
工作区、临时构建和私有日志已清理。正式模型配置与启动前逐字节一致,报告和截图
|
||||
保留在本机临时目录,不作为长期可用的仓库资源。
|
||||
测试沿用生产 HTTPS 与证书处理,未单独验证严格证书校验或 Main 重启。
|
||||
|
||||
专项 7 个测试文件通过,91 项通过、3 项跳过。`npm run typecheck` 与
|
||||
`npm run lint` 通过。最初两个假时钟测试因异步目录解析尚未结束就推进启动时钟而
|
||||
超时;调整为等待 launcher 启动后,DS Runtime 的 34 项测试全部通过。
|
||||
最终全量 `vitest run`(`npm test` 的脚本入口)为 353 个文件通过、9 个跳过,
|
||||
4155 项通过、66 项跳过、0 失败,耗时 602.00 秒。`git diff --check` 通过。
|
||||
|
||||
远端核查:Main 的 SSH 分支创建托管远程 Runtime,最终使用 `AcpRemoteRuntime`;
|
||||
`create-runtime.ts` 的本机 DS 入口拒绝 SSH execution space。本次未改远端代码,
|
||||
未调用远端模型,也未进行 Linux Host 实测。
|
||||
@@ -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 |
|
||||
@@ -121,6 +121,12 @@ GoodBuddy 使用自己固定的 Harness Host 入口和只读组合模板,不
|
||||
|
||||
模型名称、服务地址、工作区、Skills、MCP schema 和已启用插件的规范化入口通过严格校验的 Main 配置传给 Host。API Key 只通过受控凭据通道按需提供,不写入 YAML、命令行、Renderer 或日志。插件配置只来自 GoodBuddy 受管状态,不合并用户 profile 或全局补丁。
|
||||
|
||||
本机 Runtime 初始化时对工作区调用 `realpath`,将结果保存在该 Host 的运行状态中;
|
||||
Host 启动与 ACP `newSession.cwd` 共用这一路径。Host 仍校验目录并严格比较会话路径。
|
||||
Windows 混合分隔符与同目录规范写法可继续通过现有 execution-space identity 复用
|
||||
Runtime,无需更改缓存键或增加重试。远端 SSH 由 `ManagedRemoteAcpRuntime` /
|
||||
`AcpRemoteRuntime` 单独处理,不经过本机 DS Runtime 的目录解析。
|
||||
|
||||
### 5.3 双层内部控制面
|
||||
|
||||
Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不能代替进程内的 Session/Tool 适配层:
|
||||
@@ -136,9 +142,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. 总体架构
|
||||
|
||||
@@ -290,6 +301,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 能稳定解释的字段:
|
||||
@@ -298,6 +313,7 @@ Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
|
||||
- `text`:已提交的助手文本分片。
|
||||
- `reasoning`:可选的有界推理摘要分片。
|
||||
- `tool`:工具 ID、名称、状态和有界输入/输出摘要。
|
||||
- 操作标题与增量完成事件的参数保留遵循[工具操作摘要规则](../assistant-workbar/runtime-interactions.md#工具操作摘要),本机适配器按调用 ID 合并,详情上限保持不变。
|
||||
- `model-usage`:模型、提供方、输入、输出和缓存 Token。
|
||||
- `done`:停止原因和 Session ID。
|
||||
|
||||
@@ -323,7 +339,7 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
|
||||
- 首次请求创建 Session。
|
||||
- 已有 Session 只发送当前 Prompt。
|
||||
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的有界历史。
|
||||
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的会话历史。
|
||||
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
|
||||
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
|
||||
|
||||
@@ -372,6 +388,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 沙箱
|
||||
@@ -437,8 +456,17 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
任意 Body 配置入口,因此 DeepSeek Harness 不接收自定义 Body。完整支持矩阵和合并规则
|
||||
见[模型连接请求定制](../model-connections/technical-design.md)。
|
||||
- API Key 继续保存在 GoodBuddy 加密设置中。
|
||||
- 启动环境提供的部署连接只由 Main 自动解析,不在 Renderer 中显示为可选来源。
|
||||
- 未显式指定 Harness 模型且没有可用的管理员预置连接时,Main 优先使用当前默认的兼容 GoodBuddy 模型连接;默认连接不兼容时使用首个兼容连接。只有不存在任何兼容连接时才提示用户前往模型或 Runtime 设置。
|
||||
- `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,并通过完整内部能力握手。
|
||||
|
||||
@@ -468,7 +496,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 |
|
||||
@@ -503,7 +532,7 @@ Runtime: GoodBuddy 内置 DeepSeek Harness
|
||||
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
|
||||
状态: 已就绪
|
||||
路径: <受控 Host 路径>
|
||||
版本: 0.1.0-rc.8
|
||||
版本: 0.1.2-rc.1
|
||||
执行权限: 当前用户权限
|
||||
|
||||
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# 直连模型 Agent 能力
|
||||
|
||||
本目录是 GoodBuddy“直连模型 Agent 能力”的唯一文档入口。本功能为直连文本模型补齐
|
||||
跨平台进程执行和编程 Subagent,使其能够在 Execute 模式完成读取、修改、运行和验证组成的
|
||||
编程闭环,同时避免向已经拥有原生 Shell 或 Agent 能力的 Runtime 重复注入工具。
|
||||
高效工作区搜索、分页读取、补丁编辑、跨平台进程执行和编程 Subagent,使其能够完成
|
||||
“搜索、读取、修改、运行、验证”的开发闭环,同时避免向已经拥有原生 Shell 或 Agent
|
||||
能力的 Runtime 重复注入工具。
|
||||
|
||||
## 文档导航
|
||||
|
||||
@@ -21,6 +22,9 @@
|
||||
| --- | --- |
|
||||
| 直连模型 | Runtime provider 为 `model`、由 GoodBuddy 直接调用模型协议的文本模型 |
|
||||
| 进程执行 | GoodBuddy 在当前执行空间启动一个前台 Shell 命令并返回有界结果 |
|
||||
| 工作区搜索 | 通过安装包内置 ripgrep 搜索内容或列出文件,并返回紧凑结果 |
|
||||
| 工作区补丁 | 通过 `*** Begin Patch` 格式新增、修改或删除工作区文本文件 |
|
||||
| 输出续读 | 通过 `output_read` 分页读取当前会话保留的进程或 Subagent 输出 |
|
||||
| 平台 Shell | Windows 上的 PowerShell,macOS/Linux 上的 Bash 或 POSIX Sh |
|
||||
| 编程 Subagent | 由直连模型临时委派、使用直连模型 Runtime 执行一个有界子任务的执行者 |
|
||||
| 父请求 | 发起进程调用或 Subagent 委派的当前直连模型请求 |
|
||||
@@ -32,11 +36,15 @@ Task、Job、Subjob、Run 和 Subagent 的对象关系以
|
||||
|
||||
## 功能边界
|
||||
|
||||
- `process_execute` 和 `subagent_delegate` 是 GoodBuddy 直连模型内置工具,不是外部 MCP。
|
||||
- `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 的只读能力。
|
||||
- Ask 可搜索和分页读取工作区,但不允许补丁写入或进程执行;Subagent 若在 Ask 中使用,
|
||||
只能继承 Ask 的只读能力。
|
||||
- 进程和 Subagent 的长输出返回预览与续读位置,完整内容保留到所属会话或 Runtime 释放。
|
||||
Ask 和 Execute 均可续读当前会话已取得的输出,详见 [输出契约](./technical-design.md#43-输出边界)。
|
||||
- Execute 表示用户授权当前执行空间账号的完整能力,不增加第二套工具审批或权限档位。
|
||||
- 首版跨平台指桌面本机 Windows、macOS 和 Linux。托管 SSH 项目当前继续使用远端
|
||||
OpenCode;未来若向远端直连模型开放本工具,命令必须由 Host Agent 执行,绝不能回退到
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 关联故事 | [User Stories](./user-stories.md) |
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
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. 状态维度
|
||||
|
||||
@@ -39,17 +44,24 @@
|
||||
|
||||
## 3. 工具可见性决策表
|
||||
|
||||
| Runtime | 模式 | 执行空间 | `process_execute` | `subagent_delegate` |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `model` | Ask | 本机 | 隐藏 | 提供,只读继承 |
|
||||
| `model` | Execute | 本机且 Shell 可用 | 提供 | 提供,Execute 继承 |
|
||||
| `model` | Execute | 本机但 Shell 不可用 | 隐藏并报告诊断 | 提供,但子级同样没有进程工具 |
|
||||
| `model` | Ask/Execute | SSH,远端后端未实现 | 隐藏 | 当前远端直连模型产品入口不开放 |
|
||||
| 其他 Runtime | 任意 | 任意 | 隐藏 | 隐藏 |
|
||||
| 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
|
||||
@@ -68,7 +80,7 @@ validating
|
||||
规则:
|
||||
|
||||
- `completed_nonzero` 仍产生正常工具结果。
|
||||
- 超时和取消结果包含已经取得的有界输出。
|
||||
- 超时和取消结果包含已取得输出的预览;超出预览的内容按 FR-7 继续保留,直到所属会话释放。
|
||||
- 进入 `terminating` 后不再接受新输入,也不启动替代命令。
|
||||
- 终止只针对本次调用记录的进程树。
|
||||
- 迟到的 `close` 只用于清理,不得覆盖已提交的取消或超时原因。
|
||||
@@ -78,15 +90,14 @@ validating
|
||||
| 输入 | 结果 |
|
||||
| --- | --- |
|
||||
| 未提供 | 当前工作区根目录 |
|
||||
| `.` 或合法相对子目录 | 解析到工作区内目录 |
|
||||
| 绝对路径 | 拒绝 |
|
||||
| `..` 越界、符号链接越界 | 拒绝 |
|
||||
| `.` 或相对路径 | 以当前工作区为基准解析,可指向工作区外目录 |
|
||||
| 绝对路径 | 使用指定目录 |
|
||||
| `..` 或指向工作区外的符号链接 | 解析到实际目标目录,按当前用户权限执行 |
|
||||
| 不存在或不是目录 | 拒绝 |
|
||||
| 当前工作区身份在验证期间变化 | 拒绝本次调用 |
|
||||
|
||||
首版进程工具比 Execute 的 OS 权限范围更窄地固定工作目录输入,只是为了保持请求与项目
|
||||
一致;命令本身仍以当前用户权限运行,可以显式访问该账号有权访问的其他绝对路径。该规则
|
||||
不是 Runtime OS 沙箱,也不能宣称文件系统 containment。
|
||||
工作区提供默认目录和相对路径基准,不是 Execute 命令的文件系统边界。单次命令选择其他
|
||||
工作目录不会修改父请求绑定的项目或工作区,也不会影响后续未指定 `cwd` 的命令。
|
||||
|
||||
## 6. Shell 选择
|
||||
|
||||
@@ -140,6 +151,8 @@ type SubagentDelegateResult = {
|
||||
- 子级不能请求更高模式。
|
||||
- 子级使用父请求开始时的工具与能力快照;运行中设置变化不重写快照。
|
||||
- 子级调用 MCP、浏览器或知识能力时继续经过这些能力已有的作用域和生命周期边界。
|
||||
- FR-5 / FR-7 / US-C1:Execute 子级使用父请求的浏览器标签页,不另开标签页或改变所有权;
|
||||
子级结束后父模型仍可继续操作该页,父请求继续负责其使用租约。
|
||||
- Execute 子级不弹出额外的“Subagent 执行授权”,也不获得父级没有的 MCP 分配。
|
||||
|
||||
## 9. 并发和顺序
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台 CI 待验证 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
| 关联领域 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
@@ -17,14 +17,16 @@ GoodBuddy 直连模型已经可以读取、列出和写入工作区文本,使
|
||||
分配的 MCP,但没有内置命令执行工具。模型可以生成代码,却不能运行构建、测试、格式化、
|
||||
Git 或项目脚本,因此无法独立验证结果。
|
||||
|
||||
GoodBuddy 也已经有面向用户专家协作的只读 Subagent,但直连模型不能在自己的工具循环中
|
||||
主动委派一个聚焦的编程子任务。OpenCode、Continue 和 DeepSeek Harness 一般已经提供
|
||||
本功能设计时,GoodBuddy 已有面向用户专家协作的只读 Subagent,但直连模型尚不能在工具
|
||||
循环中主动委派编程子任务。当前专家协作已继承父请求 Ask/Execute 模式并使用本机直连模型
|
||||
工具,与本功能的 `subagent_delegate` 仍为不同入口。OpenCode、Continue 和 DeepSeek Harness 一般已经提供
|
||||
Shell、Agent 或 Task 能力,再向它们注入 GoodBuddy 同名工具只会造成重复入口和语义冲突。
|
||||
|
||||
本功能只补齐直连模型缺少的基础 Agent 能力:
|
||||
本功能补齐直连模型缺少的基础 Agent 能力:
|
||||
|
||||
1. 一个按平台适配的进程执行接口。
|
||||
2. 一个单层、继承父请求范围的编程 Subagent 接口。
|
||||
1. 一组工作区搜索、分页读取和补丁编辑接口。
|
||||
2. 一个按平台适配的进程执行接口。
|
||||
3. 一个单层、继承父请求范围的编程 Subagent 接口。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
@@ -33,6 +35,7 @@ Shell、Agent 或 Task 能力,再向它们注入 GoodBuddy 同名工具只会
|
||||
3. 让直连模型可以把独立工作委派给临时 Subagent,并取得有界、可核验的结果。
|
||||
4. 保持 Ask 只读、Execute 完整授权、执行空间、取消、有界输出和进程回收语义不变。
|
||||
5. 不向已有原生执行能力的 Agent Runtime 重复注入 GoodBuddy 进程或 Subagent 工具。
|
||||
6. 使用安装包内置 ripgrep 提供跨平台一致的快速代码搜索,不依赖用户预装命令。
|
||||
|
||||
## 3. 核心产品决策
|
||||
|
||||
@@ -65,7 +68,8 @@ Subagent 使用父请求的项目、执行空间、工作模式、直连模型
|
||||
|
||||
- Ask 父请求只能创建 Ask Subagent。
|
||||
- Execute 父请求创建 Execute Subagent。
|
||||
- 子级不能切换项目、目录、Runtime、模型连接或工作模式。
|
||||
- 子级不能切换父请求绑定的项目、工作区、Runtime、模型连接或工作模式;Execute 命令的
|
||||
单次工作目录选择遵循 FR-3。
|
||||
- 子级不再看到 `subagent_delegate`,首版最大委派深度固定为 1。
|
||||
- 子任务不是新的顶层 Task 或 Conversation,结果先返回父模型。
|
||||
|
||||
@@ -74,18 +78,25 @@ Subagent 使用父请求的项目、执行空间、工作模式、直连模型
|
||||
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 不包含
|
||||
|
||||
@@ -102,8 +113,8 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
|
||||
|
||||
### FR-1 直连模型专属可用性
|
||||
|
||||
- 两个工具只由 Runtime provider `model` 注册。
|
||||
- Agent Runtime 切换为 OpenCode、Continue 或 DeepSeek Harness 后不再显示这两个工具。
|
||||
- 进程、编程 Subagent 和输出续读工具只由 Runtime provider `model` 注册。
|
||||
- Agent Runtime 切换为 OpenCode、Continue 或 DeepSeek Harness 后不再显示这些工具。
|
||||
- 图像生成连接不获得文本 Agent 工具。
|
||||
- 能力不可用时返回准确原因,不以其他 Runtime 或本机 Shell 静默代替。
|
||||
|
||||
@@ -118,7 +129,8 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
|
||||
### FR-3 工作目录和执行位置
|
||||
|
||||
- 默认目录是父请求当前工作区。
|
||||
- 可选工作目录必须解析为当前工作区本身或其子目录。
|
||||
- 可选工作目录支持绝对路径、相对工作区路径及指向工作区外目录的路径或符号链接,
|
||||
不额外限制在工作区内;目标必须是实际存在且当前用户可访问的目录。
|
||||
- 本机执行只能发生在当前桌面执行空间。
|
||||
- 当前请求绑定 SSH 执行空间且没有远端进程后端时不注册工具,绝不在桌面目录回退执行。
|
||||
|
||||
@@ -147,8 +159,10 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
|
||||
|
||||
- 父请求取消必须传播到活动 Subagent 和其当前进程。
|
||||
- Runtime 替换、会话释放和应用退出必须停止 GoodBuddy 创建的活动子进程。
|
||||
- 命令、标准输出、标准错误、Subagent 提示、结果、队列和并发全部有界。
|
||||
- 截断必须明确标记,不能把不完整输出伪装为完整结果。
|
||||
- 命令、Subagent 提示、单次返回的输出预览、队列和并发有界;完整输出不因超过预览容量而丢弃。
|
||||
- 预览省略内容时明确标记并提供续读位置。模型可用 `output_read` 逐页取得完整日志及
|
||||
Subagent 结果,包括失败或取消前已取得的部分输出。
|
||||
- Ask 和 Execute 均可续读当前会话的输出。会话或 Runtime 释放后,临时输出删除,旧句柄失效。
|
||||
|
||||
### FR-8 环境和凭据
|
||||
|
||||
@@ -177,6 +191,19 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
|
||||
- 一旦本次模型调用已经显示文本或推理内容,不自动重放该调用,避免重复回答。
|
||||
- 非网络类 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 创建文件、运行测试并读取退出码。
|
||||
@@ -187,9 +214,13 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
|
||||
- [ ] Subagent 不能递归委派,不能切换父请求工作区、模式或模型。
|
||||
- [ ] 切换到 OpenCode、Continue 或 DeepSeek Harness 后不出现本功能工具。
|
||||
- [ ] 非零退出、超时、取消和输出截断均准确展示。
|
||||
- [ ] 模型能续读长输出中未出现在预览内的中间与结尾内容,中文和表情不会导致分页停滞。
|
||||
- [ ] 取消父请求和退出应用后没有残留 GoodBuddy 命令进程。
|
||||
- [ ] 本机工具环境选择对新命令生效,Main 持有的模型凭据不进入子进程。
|
||||
- [ ] 子任务不进入 Task Center,不创建独立可导航 Conversation。
|
||||
- [ ] TLS 建连重置和请求超时在未输出内容时最多自动重试 3 次,成功后继续原请求。
|
||||
- [ ] 重试等待可取消,已显示部分内容的失败不自动重放。
|
||||
- [ ] Ask 可使用 ripgrep 搜索和分页读取,但不能应用补丁。
|
||||
- [ ] Execute 可通过一个补丁新增、修改和删除多个工作区文件,并运行测试验证。
|
||||
- [ ] Windows、macOS、Linux 的 x64/arm64 安装包均携带对应 ripgrep,且无需系统预装。
|
||||
- [ ] 全量测试、类型检查、Lint 和生产构建通过。
|
||||
|
||||
@@ -6,25 +6,41 @@
|
||||
| --- | --- |
|
||||
| 文档类型 | 功能进度 |
|
||||
| 状态 | 已实施,本机验收完成 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-05 |
|
||||
| 版本 | 0.5 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联入口 | [直连模型 Agent 能力](./README.md) |
|
||||
|
||||
## 当前结论
|
||||
|
||||
本机生产路径已经完成源码接线:直连模型 Execute 可运行平台 Shell,Ask/Execute 均可按
|
||||
父模式委派单层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 路径不注入
|
||||
这两个工具。Windows 本机、真实模型和全量项目验证已通过;macOS 与 Linux 真机验证仍需
|
||||
由对应平台完成。
|
||||
父模式委派单层编程 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] 当前专家 Subagent 是只读协作流程,不是直连模型可调用的编程工具。
|
||||
- [x] 专家协作与直连模型可调用的编程工具是不同入口;当前专家继承父请求 Ask/Execute,
|
||||
使用已启用本机直连模型工具并记录工具活动。模式与 authorizer 传递已由
|
||||
`subagent-service.test.ts` 覆盖,不把该单元测试作为额外的真实模型或远程执行验收。
|
||||
- [x] Task/Job 领域文档已经明确 Subagent 不是新的顶层 Task 或 Conversation。
|
||||
- [x] 当前跨平台进程树清理已经覆盖 Windows `taskkill /T /F` 和 POSIX 进程组。
|
||||
|
||||
@@ -54,7 +70,7 @@
|
||||
- [x] `ModelToolProvider` 注册与调用。
|
||||
- [x] Ask/Execute、Runtime target 和本机执行空间双重检查。
|
||||
- [x] DeepSeek Harness、OpenCode 和 Continue 隔离。
|
||||
- [x] 通用工具活动和“设置 > MCP > 直连模型”编程能力目录。
|
||||
- [x] 通用工具活动和“设置 > MCP > 直连模型”开发工具目录。
|
||||
- [x] 普通问答、工具轮次和上下文摘要的瞬时网络错误及请求超时最多自动重试 3 次。
|
||||
- [x] 递增退避、重试状态、取消传播和已显示输出不重放。
|
||||
|
||||
@@ -84,6 +100,48 @@
|
||||
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
|
||||
@@ -138,6 +196,46 @@
|
||||
- 全量 `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、桥接或启动器。
|
||||
|
||||
## 进度维护要求
|
||||
|
||||
- 只有真实生产路径完成并验证后才能勾选实施项。
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 相关架构 | [助手工作栏与执行空间](../assistant-workbar/prd.md)、[本机工具环境](../local-tool-environment/technical-design.md) |
|
||||
@@ -17,11 +17,14 @@
|
||||
|
||||
- `ModelAgentRuntime` 的多轮工具调用、上下文压缩、取消、用量和有界结果处理。
|
||||
- `ModelToolProvider` 的直连模型内置工具、浏览器、Web、知识和自定义 MCP 聚合。
|
||||
- `workspace_rg`、分页 `workspace_read_text` 和 `workspace_apply_patch` 工作区工具。
|
||||
- `WorkspaceAccess` 的本机和远端工作区抽象。
|
||||
- 跨平台子进程树终止辅助函数。
|
||||
- 面向用户专家协作的 `SubagentScheduler`、状态事件和只读模型调用。
|
||||
- 面向用户专家协作的 `SubagentScheduler` 和状态事件。专家使用具备已启用本机能力的
|
||||
直连模型 Runtime,继承父请求 Ask/Execute 及 authorizer,工具事件记入子任务;
|
||||
Ask 保持只读,不把这些本机工具表述为远程 OpenCode 子会话。
|
||||
- OpenCode、Continue 和 DeepSeek Harness 各自的 Shell/Agent 能力。
|
||||
- 直连模型 `process_execute` 和 `subagent_delegate`。
|
||||
- 直连模型 `process_execute`、`subagent_delegate` 和 `output_read`。
|
||||
- 不创建顶层 Task/Conversation 的编程 Subagent actor 与活动归并。
|
||||
|
||||
## 2. 总体架构
|
||||
@@ -67,6 +70,63 @@ type BuiltinModelToolSummary = {
|
||||
|
||||
工具总数和 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 或表达式。
|
||||
|
||||
### 读取与搜索失败恢复
|
||||
|
||||
`workspace_read_text` 和 `workspace_rg` 的输入校验及预期工作区错误由 `ModelToolProvider`
|
||||
转为 `RecoverableModelToolError`,保留原始 cause。范围包括 schema 校验失败、绝对路径或
|
||||
越界路径、目标不存在或类型不符、无效 UTF-8、无效读取范围,以及 `ENOENT`、`ENOTDIR`、
|
||||
`EISDIR`、`EACCES`、`EPERM`、`ELOOP`、`ENAMETOOLONG` 等已列明的文件系统错误。
|
||||
Runtime 向模型返回 `{ ok: false, recoverable: true, error, nextAction }` 的 JSON 文本工具结果,
|
||||
工具活动记为 `recoverable`,继续工具循环,由模型修正路径、分页参数、glob 或搜索表达式;
|
||||
提示不得原样重复失败参数。取消、远端断线、失效工作区、缺失 rg 可执行文件及未分类的内部
|
||||
错误继续向外传播,不转为可恢复结果。
|
||||
|
||||
读取和搜索仍只接受工作区内相对路径,绝对路径即使指向工作区内文件也拒绝;原有越界及
|
||||
符号链接检查继续生效。仅当本机直连模型 Execute 的 `process_execute` 实际可用时,恢复
|
||||
提示才建议用它访问工作区外文件。此处理不改变 Windows 用户权限、UAC 或文件 ACL,
|
||||
也不提升进程权限;Ask 仍保持只读。
|
||||
|
||||
ripgrep 结果按退出状态处理:
|
||||
|
||||
- exit 1 保持正常无匹配结果。
|
||||
- exit 2 有 stdout 且不是 regex/glob 编译错误时,保留已取得的匹配或文件列表,附加
|
||||
`incomplete search coverage` 警告及有界 stderr。零匹配只代表已搜索部分,不能据此断言
|
||||
整个目标不存在匹配;输出截断提示仍独立保留。
|
||||
- exit 2 无 stdout,或出现无效 regex/glob 编译错误时,搜索层抛错,再由 Provider 转为
|
||||
上述可恢复结果;模型可修正表达式,字面搜索可使用 `fixedStrings`。
|
||||
|
||||
该契约属于桌面 `ModelAgentRuntime → ModelToolProvider` 内置工具路径,复用浏览器标签页
|
||||
关闭和元素引用失效已有的可恢复错误机制;浏览器分别提示 `browser_navigate` 或
|
||||
`browser_snapshot`,路由规则见[浏览器技术设计](../assistant-workbar/browser-tabs-technical-design.md#61-兼容阶段)。
|
||||
`workspace_read_text` 若使用 `RemoteWorkspaceAccess`,仍经同一 Provider 分类远端
|
||||
`data.code` 中的预期文件系统和路径/文本错误。Agent daemon 的 RPC 错误序列化保留
|
||||
`ENOENT` 等大写系统错误码,避免远端文件不存在被误判为不可恢复错误;远端原生工具
|
||||
处理不变。内置 rg 仍仅支持本机工作区,托管 SSH 当前仍使用远端 OpenCode 原生工具,
|
||||
不因共享读取抽象而开放远端直连模型,见[远程 Runtime 边界](../remote-host/technical-design.md#runtime)。
|
||||
|
||||
### ripgrep 打包
|
||||
|
||||
固定依赖 `@vscode/ripgrep`,构建钩子根据 Electron 目标选择对应 Windows、macOS 或 Linux
|
||||
x64/arm64 平台包,使用 lockfile integrity 校验后提取 `rg`。安装包只复制当前目标二进制和
|
||||
MIT 许可证。运行时通过 `BundledRuntimePaths.ripgrep` 使用绝对路径,并将所在目录放到直连
|
||||
模型进程 PATH 前部。
|
||||
|
||||
## 4. 进程工具契约
|
||||
|
||||
### 4.1 输入
|
||||
@@ -89,7 +149,8 @@ const processExecuteInputSchema = z.object({
|
||||
}).strict()
|
||||
```
|
||||
|
||||
- `cwd` 只接受相对工作区路径。
|
||||
- `cwd` 接受绝对路径或相对工作区路径,省略时使用工作区根目录;按本机路径语义解析
|
||||
`..` 和符号链接,不限制目标位于工作区内。工具 schema 向模型明确这一规则。
|
||||
- 首版不接受 `env`、`stdin`、`background`、`pty`、`shell` 或任意 executable 参数。
|
||||
- Shell 选择由执行后端决定,防止模型绕过平台契约启动另一套受管接口。
|
||||
- 命令字符串仍可以调用当前账号本来有权运行的程序;Execute 不增加命令白名单。
|
||||
@@ -111,20 +172,41 @@ type ProcessExecuteResult = {
|
||||
stderr: string
|
||||
stdoutTruncated: boolean
|
||||
stderrTruncated: boolean
|
||||
stdoutReference?: PagedOutputReference
|
||||
stderrReference?: PagedOutputReference
|
||||
}
|
||||
```
|
||||
|
||||
`cwd` 返回工作区相对显示值,不把不必要的绝对用户路径送入模型上下文。Shell 在模型结果
|
||||
中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
|
||||
`cwd` 在工作区内返回相对显示值,根目录为 `.`;工作区外返回规范化绝对路径,准确说明
|
||||
命令实际执行位置。Shell 在模型结果中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
|
||||
|
||||
### 4.3 输出边界
|
||||
|
||||
- stdout 和 stderr 各最多 96 KiB。
|
||||
- 超限时分别保留前 48 KiB 与最后 48 KiB,中间插入固定截断标记。
|
||||
- stdout 和 stderr 各返回最多 96 KiB 的前缀预览,JSON 转义后的内容也计入预览预算。
|
||||
- `PagedOutputStore` 将流式输出完整写入系统临时目录,内存只保留预览;超限后继续写盘。
|
||||
存储失败明确报错,不把缺失内容标记为完整结果。
|
||||
- 预览省略内容时返回 `stdoutReference` 或 `stderrReference`,包含 `handle`、`nextCursor`
|
||||
和 `totalBytes`。短输出返回完整文本并删除临时文件,不附带句柄。
|
||||
- JSON 结果加上元数据后必须低于 `ModelToolProvider` 现有 256 KiB 工具结果上限。
|
||||
- 流式读取在收到数据时执行边界,不先积累无界 Buffer。
|
||||
- 输出按 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 探测
|
||||
@@ -232,13 +314,18 @@ type DirectModelSubagentContext = {
|
||||
- 子级复用父 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,错误最多 2 KiB,连同结构化字段保持在 256 KiB 工具结果上限内。
|
||||
- 文本预览最多 192 KiB,JSON 转义也计入预算;错误最多 2 KiB。省略的正文通过
|
||||
`outputReference` 续读,字段与生命周期沿用第 4.4 节,完整输出不受预览容量限制。
|
||||
- 模型用量逐事件归属父请求和 `childRunId`,不能只算入父模型最后一轮。
|
||||
- 子级失败和取消返回结构化终态及部分输出,让父模型决定下一步。
|
||||
|
||||
@@ -278,6 +365,25 @@ type ModelToolCallContext = {
|
||||
- `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` 类而自动看到直连模型编程工具。
|
||||
|
||||
@@ -293,14 +399,14 @@ DeepSeek Harness 的 Main 工具代理必须使用仅包含分配 MCP 和 Web
|
||||
- `failed`:启动或契约失败。
|
||||
- `cancelled`:父请求取消。
|
||||
|
||||
结果摘要包含 Shell、相对目录、退出码、耗时和截断标记。完整有界 stdout/stderr 进入工具
|
||||
输出字段,不写应用诊断日志。
|
||||
结果包含 Shell、工作目录、退出码、耗时、输出预览和续读引用,不写应用诊断日志。
|
||||
通用工具活动仍显示有界摘要,模型通过 `output_read` 取得的页面记为后续工具活动。
|
||||
|
||||
### 9.2 Subagent 事件
|
||||
|
||||
复用现有 `subagent` 公开事件和消息块,增加 `routingMode: 'native'` 表示模型主动委派。
|
||||
`expertId` 不再作为编程 Subagent 必填身份;共享 schema 应改为执行者 discriminated union,
|
||||
而不是伪造一个专家记录:
|
||||
不得以专家记录代替该执行者:
|
||||
|
||||
```ts
|
||||
type SubagentActor =
|
||||
@@ -336,10 +442,15 @@ type SubagentActor =
|
||||
结果总量都由请求超时、取消和上下文压缩控制,长回复和大工具结果不会被中断。图像生成
|
||||
响应仍按可解码图片大小校验。
|
||||
|
||||
会话历史在请求校验、Renderer 发送与保存、SQLite 读写和直连模型缓存中均保留完整消息,
|
||||
不按消息条数、单条文本长度或累计文本量裁剪。上下文大小由现有压缩策略和模型上下文窗口
|
||||
处理。Continue 接收完整历史,已有有效摘要只替换其覆盖的前缀,剩余消息全部交给原生
|
||||
Runtime;GoodBuddy 不再额外保留最后 20 条或限制为 128,000 字符。
|
||||
|
||||
## 11. 性能
|
||||
|
||||
- Shell 探测按执行服务实例缓存,不在每轮模型调用重复探测。
|
||||
- stdout/stderr 边读边界定,避免大日志进入内存后再截断。
|
||||
- stdout/stderr 边读边写入临时文件,内存仅保存有界预览;续读每次只读取一页。
|
||||
- 子级共享父请求已解析的模型配置和工具服务,不复制 MCP 配置或持久状态。
|
||||
- 不增加持久 Shell、后台 daemon、工作区快照或恢复状态机。
|
||||
- 命令执行不使用 PTY,避免普通构建和测试承担终端渲染成本。
|
||||
@@ -351,8 +462,10 @@ type SubagentActor =
|
||||
- 工具可见性矩阵和 Runtime target 过滤。
|
||||
- Ask 伪造进程调用拒绝。
|
||||
- Windows PowerShell、POSIX Bash/Sh 选择和参数。
|
||||
- cwd 默认、合法子目录、越界、符号链接越界和不存在。
|
||||
- cwd 默认、相对子目录、绝对路径、工作区外目录、指向外部的符号链接和不存在的目录。
|
||||
- exit 0、非零退出、spawn 失败、超时、取消和输出截断。
|
||||
- 输出预览加分页能还原全文,包括 JSON 转义文本、中文、表情和字面替换字符;覆盖一字节
|
||||
页面前进、跨会话拒绝、活动调用释放、短输出删除及服务退出清理。
|
||||
- 环境筛选、本机工具 PATH 优先且模型凭据不进入进程。
|
||||
- 子级模式、模型、项目、执行空间和能力继承。
|
||||
- 深度 1 过滤、并发 3、队列 20、取消传播和部分输出。
|
||||
@@ -384,6 +497,11 @@ type SubagentActor =
|
||||
|
||||
每个平台一次完整场景即可,不进行批量或高成本调用。记录准确模型调用次数,不记录 API Key。
|
||||
|
||||
输出边界场景复用 `runtime-e2e.manual.test.ts`:真实模型运行一次专用 Node 脚本,输出超过
|
||||
96 KiB,并在预览之后生成随机中间与结尾标记。模型必须通过生产 `output_read` 连续读到 EOF,
|
||||
最终回答包含两个标记。测试限制工具为进程与续读,记录上游请求数和工具调用数;凭据由现有
|
||||
加密设置解密后仅在内存及测试子进程环境中传递。
|
||||
|
||||
### 12.4 项目验证
|
||||
|
||||
源码完成后运行:
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 设计系统 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
| 关联 PRD | [PRD](./prd.md) |
|
||||
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
本功能不增加一级页面、Composer 按钮或 Runtime 专属工具栏。用户主要通过三处理解能力:
|
||||
|
||||
1. **设置 > MCP > 直连模型**:查看内置编程能力说明。
|
||||
1. **设置 > MCP > 直连模型**:查看工作区文件和开发工具说明。
|
||||
2. **聊天消息活动块**:查看命令和 Subagent 的实时状态与结果。
|
||||
3. **运行记录**:按当前 Conversation 或 Task 查看聚合执行活动。
|
||||
|
||||
@@ -23,19 +23,24 @@ Job、Subjob 和 Run 继续是内部对象,不创建树形 UI、独立路由
|
||||
|
||||
## 2. 设置中的能力目录
|
||||
|
||||
在现有直连模型工具目录增加“编程能力”分组:
|
||||
直连模型工具目录使用两个本机开发分组:
|
||||
|
||||
```text
|
||||
编程能力
|
||||
工作区文件
|
||||
搜索工作区
|
||||
读取工作区文本
|
||||
应用工作区补丁
|
||||
|
||||
开发工具
|
||||
进程执行
|
||||
使用当前工作区和当前用户权限运行 PowerShell/Bash 命令。
|
||||
使用当前用户权限运行 PowerShell、Bash 或 Sh 命令,默认在工作区执行,也可指定其他工作目录。
|
||||
编程 Subagent
|
||||
将聚焦任务委派给同一模型连接;继承当前工作模式和工作区。
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 两项默认属于直连模型,不提供持久 Switch。
|
||||
- 这些工具默认属于直连模型,不提供持久 Switch。
|
||||
- 设置目录只定义稳定工具能力,不增加 Shell 状态或重试控件;实际 Shell 标签在模型工具
|
||||
定义和进程结果中返回。
|
||||
- Runtime 是 OpenCode、Continue 或 DeepSeek Harness 时,不在其原生能力清单中伪造这两项;
|
||||
@@ -48,13 +53,15 @@ Job、Subjob 和 Run 继续是内部对象,不创建树形 UI、独立路由
|
||||
|
||||
### 3.1 标题与摘要
|
||||
|
||||
正常执行状态的操作摘要遵循[工具操作摘要规则](../assistant-workbar/runtime-interactions.md#工具操作摘要),从原始调用参数提取并在完成后保留;异常状态继续显示原有提示。
|
||||
|
||||
```text
|
||||
进程执行
|
||||
PowerShell · packages/app · npm test
|
||||
```
|
||||
|
||||
- 命令只显示单行有界摘要,完整参数进入可展开详情。
|
||||
- 目录显示工作区相对路径;工作区根显示 `.`。
|
||||
- 工作区内目录显示相对路径,工作区根显示 `.`;工作区外目录显示规范化绝对路径。
|
||||
- 不在摘要中显示完整环境或凭据。
|
||||
|
||||
### 3.2 状态
|
||||
@@ -79,11 +86,14 @@ GoodBuddy 服务崩溃。
|
||||
1. Shell、工作目录、耗时和退出码。
|
||||
2. stdout。
|
||||
3. stderr。
|
||||
4. 截断提示。
|
||||
4. 预览省略提示与工具结果中的续读引用。
|
||||
|
||||
日志使用共享等宽字体和可选择文本。长输出在活动块内折叠,展开后仍保持工作栏和聊天布局
|
||||
可用;不把完整日志复制为应用通知。
|
||||
|
||||
模型使用 `output_read` 续读时显示普通工具活动“续读工具输出”。活动展开只展示该次返回的
|
||||
摘要,不等同于自动加载完整日志;当前不增加手动分页按钮。
|
||||
|
||||
## 4. Subagent 活动块
|
||||
|
||||
复用现有 `subagent` 消息块,并将系统执行者显示为“编程 Subagent”,不要求用户预先创建
|
||||
@@ -97,7 +107,7 @@ GoodBuddy 服务崩溃。
|
||||
|
||||
终态:
|
||||
|
||||
- 完成:显示有界结果摘要,可展开查看完整返回。
|
||||
- 完成:显示有界结果摘要,可展开查看当前预览;完整正文由模型通过输出续读工具取得。
|
||||
- 失败:保留部分输出和错误。
|
||||
- 取消:显示“父请求已取消”或准确取消原因。
|
||||
- 排队:显示当前处于并发队列,不显示虚假百分比。
|
||||
@@ -117,8 +127,12 @@ GoodBuddy 服务崩溃。
|
||||
- Runtime 初始化、Shell 探测或全局服务失败可通过应用通知提示,并在设置状态卡提供重试。
|
||||
- 同一错误不同时显示活动块副本和通知副本。
|
||||
- 失败后保留用户 Prompt、命令摘要、工作目录和已有输出。
|
||||
- 直连模型自动重试通过现有聊天状态显示“模型网络请求失败,正在重试(n/3)”,不增加
|
||||
独立按钮、弹窗或通知副本;用户仍可使用当前停止操作取消请求。
|
||||
- 直连模型自动重试在消息底部区分“模型网络请求失败,等待重试(n/3,s 秒后)”与
|
||||
“〈模型名〉正在重试请求(n/3),等待响应”;只有退避结束且未取消才显示后者。
|
||||
初始请求显示“〈模型名〉请求处理中”,不把尚无输出的等待描述成思考。
|
||||
- 不增加独立按钮、弹窗或通知副本;用户仍可使用当前停止操作取消请求。
|
||||
底部静态状态点、工具进展、待回答及终态优先规则由
|
||||
[消息底部请求状态](../assistant-workbar/runtime-interactions.md#消息底部请求状态)统一定义。
|
||||
|
||||
## 7. 键盘与无障碍
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 已实施,跨平台 CI 待验证 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 PRD | [直连模型 Agent 能力 PRD](./prd.md) |
|
||||
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
@@ -42,19 +42,20 @@ Shell、目录、退出码、输出和耗时。
|
||||
Given 当前项目位于 macOS 或 Linux 本机,When 模型运行同一工具,Then GoodBuddy 使用
|
||||
Bash,Bash 不可用时使用 Sh,工具契约保持不变且实际 Shell 明确可见。
|
||||
|
||||
### US-A3 使用工作区子目录
|
||||
### US-A3 选择命令工作目录
|
||||
|
||||
关联:FR-3。
|
||||
|
||||
Given 项目包含多个包,When 模型传入工作区内相对目录,Then 命令在该目录执行;When
|
||||
路径越出工作区、不是目录或不存在,Then 命令不启动并返回准确错误。
|
||||
Given 当前请求为本机 Execute,When 模型传入绝对路径或相对工作区路径,Then 命令在
|
||||
解析后的目录执行,包括工作区外目录和符号链接目标,And 后续省略 `cwd` 时仍使用原工作区。
|
||||
When 目标不是目录或不存在,Then 命令不启动并返回准确错误。
|
||||
|
||||
### US-A4 根据非零退出继续修正
|
||||
|
||||
关联:FR-2、FR-9。
|
||||
|
||||
Given 测试命令退出码非零,When 进程结束,Then 工具正常返回退出码和 stderr,而不是终止
|
||||
整个模型请求,And 模型可以读取失败原因、修改代码并再次运行。
|
||||
Given 测试命令退出码非零,When 进程结束,Then 工具正常返回退出码和 stderr,模型请求
|
||||
继续运行,And 模型可以读取失败原因、修改代码并再次运行。
|
||||
|
||||
### US-A5 取消长命令
|
||||
|
||||
@@ -63,12 +64,13 @@ Given 测试命令退出码非零,When 进程结束,Then 工具正常返回
|
||||
Given 命令仍在运行,When 用户取消父请求,Then 当前 Shell 及其由本次命令创建的子进程树
|
||||
停止,And 活动状态显示“已取消”,And 其他用户进程不受影响。
|
||||
|
||||
### US-A6 看懂截断结果
|
||||
### US-A6 续读长输出
|
||||
|
||||
关联:FR-7、FR-9。
|
||||
|
||||
Given 命令输出超过上限,When 工具返回结果,Then 保留有用的开头和结尾并明确
|
||||
`truncated: true`,And 模型与用户都不会把结果理解为完整日志。
|
||||
Given 命令或 Subagent 输出超过单次预览容量,When 工具返回结果,Then 明确标记省略并提供
|
||||
续读位置,And 模型可逐页读取中间与结尾内容,直到取得完整输出。Given 输出包含中文或表情,
|
||||
Then 即使请求很小的页面,续读仍会前进。Given 所属会话已释放,Then 旧句柄不能继续读取。
|
||||
|
||||
### US-A7 从瞬时模型网络故障恢复
|
||||
|
||||
@@ -80,6 +82,14 @@ Given 直连模型在 TLS 建连、网络传输或单次请求超时时失败且
|
||||
|
||||
## 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。
|
||||
@@ -101,6 +111,14 @@ Given 用户选择了 GoodBuddy 托管或自定义 Node/Python,When 新命令
|
||||
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 委派聚焦编程任务
|
||||
@@ -109,6 +127,8 @@ And 模型说明缺失项或在 Execute 中采取用户要求的安装步骤,
|
||||
|
||||
Given 父直连模型正在处理复杂请求,When 它调用 `subagent_delegate` 并给出独立任务,
|
||||
Then 子级使用同一项目、模型连接和 Execute 能力完成工作,And 最终结果返回父模型综合。
|
||||
Given 父请求已有浏览器标签页,When 委派任务需要浏览器,Then 子级使用同一页;子级完成后,
|
||||
父模型仍可继续操作该页,不因子会话清理而关闭父页。
|
||||
|
||||
### US-C2 Ask 中只读委派
|
||||
|
||||
|
||||
@@ -9,3 +9,7 @@
|
||||
| [文档解析与本地 OCR PRD](./prd.md) | 产品范围、解析策略、OCR 模型管理和验收 |
|
||||
|
||||
当前尚无独立 User Stories、功能逻辑、UI、技术设计和进度文档。
|
||||
|
||||
Office 文件的浏览器内人工编辑、AI 选区修改、保存和撤销不属于文档解析职责,由
|
||||
[Office 协同编辑](../office-document-editing/README.md)负责。文档处理仍负责内容提取、OCR、
|
||||
旧格式转换和供模型消费的统一结构。
|
||||
|
||||
@@ -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,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`、弹窗或应用通知。文本保留在消息中并随会话恢复。
|
||||
- 发送和生成继续可用;真实请求失败仍采用原来的错误及重试交互,不生成成功提示。
|
||||
- 文案具备简体中文、英文两套翻译。本轮显式附图和新对话继续使用现有入口,
|
||||
不增加模式开关或第二次确认。
|
||||
@@ -156,6 +156,8 @@ Renderer 不预览、不读取也不接收诊断内容或路径。勾选后详
|
||||
### 3.4 可访问性
|
||||
|
||||
- 对话框使用 `role="dialog"`、`aria-modal="true"` 和可见标题。
|
||||
- 整窗遮罩通过 React portal 挂到 `document.body`,遵循
|
||||
[全局遮罩与原生拖动区域规则](../../../UI-DESIGN.md#610-应用顶栏与全局操作)。
|
||||
- 复用 `activateModalFocus()` 与 `trapTabFocus()`,打开后聚焦反馈类型,关闭后恢复入口
|
||||
按钮焦点。
|
||||
- 非提交状态支持 Escape 关闭;提交中保持对话框打开,直到请求完成或到达有界超时。
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# 知识库
|
||||
|
||||
知识库负责资料导入、解析、分块、索引、检索、图谱和带来源的知识问答,也负责将
|
||||
Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
|
||||
知识库已提供本地资料导入、解析、分块、索引、检索、图谱和带来源的知识问答。
|
||||
当前工作区已接入 Dify、FastGPT 和 RAGFlow 的实例管理、外部绑定、检索配置、统一检索、
|
||||
聊天引用与界面。2026-09-13 已验证三家 Provider 的生产服务、本机 HTTP MCP 和生产 IPC
|
||||
短答案生成,并从完整 App 输入区完成 Dify 检索、真实回答及引用点击。外部实例弹窗已修正
|
||||
Portal 字段样式,并完成浅深主题和键盘检查。当前 UI 修正后的聚焦测试、全量回归及相关检查已通过;
|
||||
FastGPT/RAGFlow 完整 App 与专属配置、真实远程预检索仍待验收。验证只覆盖有依据的短答案,
|
||||
不代表广泛问答质量,详见[实施进度](./progress.md)。
|
||||
|
||||
## 文档导航
|
||||
|
||||
@@ -9,13 +14,20 @@ Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
|
||||
| --- | --- |
|
||||
| [知识库检索与分块增强 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-technical-design.md) | 当前 Client、Service、Store、共享契约、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) | 配置覆盖、重排、图谱与知识编译开关的请求结果及结构 |
|
||||
| [2026-09-13 产品验证摘要](./external-knowledge-validation-2026-09-13.json) | 生产服务与本机 HTTP MCP、Dify 桌面、凭据重开、调用计数、最终自动检查状态及远程阻断的脱敏证据 |
|
||||
| [2026-09-13 UI 与真实生成复验](./external-knowledge-follow-up-2026-09-13.json) | 后续 UI 修正、三家生产 IPC 生成、Dify 完整 App 回答与引用、视觉 fixture 及本轮请求计数 |
|
||||
|
||||
## 术语
|
||||
|
||||
@@ -27,4 +39,5 @@ Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
|
||||
或 Agent,也不在外部实例中创建、修改、上传或删除知识库内容。远端资料不会批量同步
|
||||
或进入本地索引;实际用于回答的有界片段会随引用保存在本地会话中。
|
||||
|
||||
当前尚未拆出覆盖整个知识库功能家族的统一逻辑、UI 设计和功能进度文档。
|
||||
本目录尚未拆出统一逻辑设计;当前本地知识库状态规则由 PRD、User Stories 和实现契约
|
||||
定义。外部知识库的实例、绑定和失败规则由独立逻辑设计维护。
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"date": "2026-09-13",
|
||||
"kind": "sanitized-ui-and-real-generation-follow-up",
|
||||
"sources": ["kb-real-generation/report.json", "kb-full-app-live/report.json", "external-visual-0913/results.json", "goodbuddy-binding-selector-fix-full.log", "goodbuddy-binding-selector-fix-full.result.json"],
|
||||
"automatedChecks": {
|
||||
"status": "passed-after-ui-follow-up",
|
||||
"authoritativeCounts": "progress.md",
|
||||
"focusedWholeAppAndTwoAgentTests": { "status": "passed", "evidence": "owner handoff" },
|
||||
"fullSuite": {
|
||||
"command": "npm test -- --maxWorkers=1 --reporter=verbose",
|
||||
"status": "passed",
|
||||
"exitCode": 0,
|
||||
"evidence": ["goodbuddy-binding-selector-fix-full.log", "goodbuddy-binding-selector-fix-full.result.json"]
|
||||
},
|
||||
"typecheck": { "command": "npm run typecheck", "status": "passed", "evidence": "owner handoff" },
|
||||
"lint": { "command": "npm run lint", "status": "passed", "evidence": "owner handoff" },
|
||||
"workingTreeDiffCheck": { "status": "passed", "evidence": "owner handoff" },
|
||||
"stagedDiffCheck": { "status": "passed", "evidence": "owner handoff" },
|
||||
"sourceStability": {
|
||||
"scope": "App test, two Agent tests and ExternalKnowledge stable throughout final run",
|
||||
"evidence": "owner handoff"
|
||||
},
|
||||
"lastTestAdjustment": "App test changed from obsolete native select interaction to actual accessible rich menu interaction; no product source change",
|
||||
"agentFailureResolution": "resolved in concurrent owner's latest code; no edits by this docs task",
|
||||
"furtherSourceChanges": "none planned per owner handoff"
|
||||
},
|
||||
"model": {
|
||||
"configuration": "saved default model settings; credential resolved in Main using safeStorage",
|
||||
"transport": "http",
|
||||
"originalSettingsUnchangedInFullAppRun": true,
|
||||
"redaction": "No endpoints, private profile identifiers, keys, business text, answer prose or content hashes retained"
|
||||
},
|
||||
"productionIpcGeneration": {
|
||||
"realElectronPreloadIpcAndModelRuntime": true,
|
||||
"fullAppRenderer": false,
|
||||
"referenceDisplay": "isolated validation DOM receiving real preload events",
|
||||
"persistence": "production task_events; text, single-library scope and external locators verified",
|
||||
"scenarios": [
|
||||
{ "provider": "dify", "mode": "always", "passed": true },
|
||||
{ "provider": "fastgpt", "mode": "always", "passed": false, "groundedQuote": true, "citationMarker": true, "failure": "initial raw-string evidence assertion did not account for serialization" },
|
||||
{ "provider": "ragflow", "mode": "always", "passed": false, "groundedQuote": true, "citationMarker": true, "failure": "initial raw-string evidence assertion did not account for serialization" },
|
||||
{ "provider": "dify", "mode": "auto", "passed": true, "transport": "ModelToolProvider to KnowledgeMcpGateway in process; not HTTP MCP", "tool": "knowledge_search" },
|
||||
{ "provider": "fastgpt", "mode": "always", "passed": true, "evidenceCheck": "decoded provider body and embedded evidence JSON" },
|
||||
{ "provider": "ragflow", "mode": "always", "passed": true, "evidenceCheck": "decoded provider body and embedded evidence JSON" }
|
||||
],
|
||||
"finalScenarioChecks": ["nonempty short answer", "grounded quote", "citation marker", "reference schema", "text and references persisted"],
|
||||
"requests": { "model": 7, "kbTotal": 12, "kbRetrieval": 9, "kbCatalog": 3 },
|
||||
"retrievalBreakdown": { "bindingVerification": 3, "forcedPreflight": 5, "autoScoped": 1 },
|
||||
"runtimeModelRetries": 0,
|
||||
"strictHttpsProviders": ["dify", "fastgpt"],
|
||||
"httpProviders": ["ragflow"]
|
||||
},
|
||||
"fullApp": {
|
||||
"provider": "dify",
|
||||
"mode": "always",
|
||||
"workMode": "ask",
|
||||
"currentProductionMainPreloadRenderer": true,
|
||||
"instanceAndPositiveBindingCreatedInUi": true,
|
||||
"composerSelectionAndSendClicked": true,
|
||||
"noFabricatedMessagesReferencesOrEvents": true,
|
||||
"mainOnlyKbCredentialSubstitution": true,
|
||||
"generationDone": true,
|
||||
"answerPersisted": true,
|
||||
"singleSelectedLibrary": true,
|
||||
"liveEvidencePresentInModelInput": true,
|
||||
"groundedShortAnswerWithCitation": true,
|
||||
"citationClicked": true,
|
||||
"citationSnippetDocumentAndChunkMatch": true,
|
||||
"citationFocusInside": true,
|
||||
"strictDifyHttps": true,
|
||||
"modelHttp": true,
|
||||
"requests": { "model": 1, "kbTotal": 5, "kbRetrieval": 3, "kbCatalog": 1, "kbDetail": 1 },
|
||||
"setupVerificationRetrievals": 2,
|
||||
"composerRetrievals": 1,
|
||||
"toolExecutions": 0,
|
||||
"portalEditor": { "themes": ["light", "dark"], "widthPx": 560, "sharedTypography": true, "withinViewport": true, "bodyOverflow": 0 },
|
||||
"isolatedProfileAndBuildRemoved": true
|
||||
},
|
||||
"visualFixture": {
|
||||
"boundary": "current source components and styles in real Electron with real body portal; fake local IPC data only",
|
||||
"themes": ["light", "dark"],
|
||||
"viewportSizes": [[1280, 800], [960, 720], [720, 640], [640, 420]],
|
||||
"screenshots": 56,
|
||||
"checksPassed": 138,
|
||||
"errors": [],
|
||||
"keyboardMethod": "native Chromium actions via Electron sendInputEvent and insertText",
|
||||
"coverage": ["password reveal layout", "switch keyboard toggle", "sticky footer visibility", "focus wrap and restoration", "instance menu arrows and disabled items", "tabs", "credential replacement and clear confirmation", "selected hover", "empty state"]
|
||||
},
|
||||
"followUpRequestAccounting": {
|
||||
"modelRequests": 8,
|
||||
"kbRequests": 17,
|
||||
"kbRetrievalRequests": 12,
|
||||
"kbCatalogRequests": 4,
|
||||
"kbDetailRequests": 1,
|
||||
"boundary": "this follow-up only; app HTTP dispatches, not upstream proxy internals or provider embedding/reranking usage",
|
||||
"cumulativeReportFieldsExcluded": "real-generation report cumulative fields omit prior desktop retrievals; not used"
|
||||
},
|
||||
"acceptanceBounds": {
|
||||
"groundedShortAnswerPath": "passed for three providers through production IPC and Dify through full App composer",
|
||||
"broadAnswerQuality": "not evaluated",
|
||||
"fastgptRagflowFullAppAndProviderSpecificUi": "pending",
|
||||
"linuxRemoteForcedPreflight": "still blocked by prior saved SSH credential decryption failure",
|
||||
"fullRegressionAfterUiFollowUp": "passed",
|
||||
"authoritativeAutomatedTestCounts": "progress.md"
|
||||
}
|
||||
}
|
||||
@@ -4,340 +4,171 @@
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 状态 | 当前实现规则;三家短答案、Dify 完整 App 与当前 UI 自动回归已通过,远程待验收 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 关联需求 | [外部知识库接入 PRD](./external-knowledge-prd.md) |
|
||||
| 关联场景 | [外部知识库 User Stories](./external-knowledge-user-stories.md) |
|
||||
| 技术契约 | [技术设计](./external-knowledge-technical-design.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`。
|
||||
| 稳定 ID | 规则 | 对应需求 |
|
||||
| --- | --- | --- |
|
||||
| `EK-I-01` 单一知识库身份 | 本地与外部都以 GoodBuddy `knowledgeBaseId` 参与聊天范围与工具授权,实例和远端 ID 不扩大选中范围 | `EK-FR-10`、`EK-FR-11` |
|
||||
| `EK-I-02` 外部绑定只读 | 添加、配置和移除仅修改本机记录;请求限于目录、详情、检索 | `EK-FR-15` |
|
||||
| `EK-I-03` 凭据边界 | 新密钥在输入表单和保存 IPC 中短暂存在;加密后只在 Main 解密使用,摘要与引用不返回密钥 | `EK-FR-02`、`EK-FR-11` |
|
||||
| `EK-I-04` 本地定义界面 | Provider 只返回数据,控件和配置由本地共享契约及组件定义 | `EK-FR-09` |
|
||||
| `EK-I-05` 无静默切换 | 请求使用保存的实例、目标和配置;失败不更换实例、公共云地址或其他知识库 | `EK-FR-14`、`EK-FR-16` |
|
||||
| `EK-I-06` 零结果与失败分离 | 合法成功响应无片段是零命中;请求或响应失败保留失败信息 | `EK-FR-14` |
|
||||
| `EK-I-07` 外部证据不可信 | 标准化片段按已有知识证据边界进入 Runtime,不改变工具权限 | `EK-FR-12` |
|
||||
| `EK-I-08` 配置不跨类型复用 | Provider 配置必须与实例一致,类型切换不迁移同名字段 | `EK-FR-06`、`EK-FR-09` |
|
||||
|
||||
## 2. 对象关系
|
||||
|
||||
```text
|
||||
ExternalKnowledgeInstance 1 ─── n ExternalKnowledgeBinding
|
||||
│
|
||||
│ 1:1
|
||||
▼
|
||||
KnowledgeBase
|
||||
```
|
||||
一个实例可以有零个或多个绑定;一个绑定对应一个 GoodBuddy 知识库和一个远端目标。
|
||||
同一实例与远端 ID 的组合唯一,不同实例可以使用相同 ID。本地知识库没有外部绑定。
|
||||
类型由绑定关系推导,旧本地知识库不需要改成另一套对象。
|
||||
|
||||
- 实例可以没有绑定。
|
||||
- 外部知识库只能绑定一个实例和一个远端知识库 ID。
|
||||
- 同一实例的同一远端知识库默认只允许存在一个绑定,避免重复选择和重复检索。
|
||||
- 允许不同实例绑定同名或相同远端 ID,因为实例边界不同。
|
||||
- 本地知识库不存在 `ExternalKnowledgeBinding`。
|
||||
GoodBuddy 显示名称与 `remoteName` 分开保存。刷新详情仅更新当前视图,不自动改写本地
|
||||
名称或持久化远端名称;重新保存绑定时才写入当前表单使用的远端名称。
|
||||
|
||||
## 3. 实例状态
|
||||
## 3. 实例状态与连接测试
|
||||
|
||||
实例状态由启停、凭据和最近探测三个维度组成,不能用一个布尔值代替。
|
||||
|
||||
### 3.1 持久状态
|
||||
|
||||
| 维度 | 值 |
|
||||
| 状态维度 | 当前值与行为 |
|
||||
| --- | --- |
|
||||
| 启停 | `enabled`、`disabled` |
|
||||
| 凭据 | `configured`、`missing`、`unavailable` |
|
||||
| 探测 | `untested`、`catalog-ready`、`healthy`、`list-restricted`、`auth-failed`、`unreachable`、`incompatible`、`failed` |
|
||||
| 启停 | `enabled` 为 false 时 Main 拒绝目录和检索,并取消当前实例请求 |
|
||||
| 凭据 | `configured`、`missing`、`unavailable`;列表尝试解密判断可用性 |
|
||||
| 最近探测 | `untested`、`catalog-ready`、`healthy`、`list-restricted`、`auth-failed`、`unreachable`、`failed`;其中 `healthy` 在类型中保留,当前操作没有写入它 |
|
||||
|
||||
`unavailable` 表示系统安全存储当前无法解密或保存凭据,与用户尚未配置的 `missing` 分开。
|
||||
实例先保存为 `untested`,不要求保存前完成网络或认证测试。保存、替换或清除凭据后
|
||||
重置探测状态。保存后的“测试连接”只尝试目录读取:成功写入 `catalog-ready`;403
|
||||
写入 `list-restricted`;401 写入 `auth-failed`;网络或超时写入 `unreachable`;其他错误
|
||||
写入 `failed`。时间和错误码随本次结果保存。
|
||||
|
||||
### 3.2 派生可用性
|
||||
探测结果是最近一次观察,不是 Main 的永久禁用条件。Main 请求检查实例是否存在、启用
|
||||
和凭据能否解密;即使最近为 `auth-failed` 或 `failed`,仍可重新请求。UI 会据该状态
|
||||
显示不可用并禁用聊天勾选,用户可编辑或重新测试实例。
|
||||
|
||||
| 条件 | 目录读取 | 已绑定知识库检索 | 页面状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| disabled | 禁止 | 禁止 | 已停用 |
|
||||
| missing / unavailable | 禁止 | 禁止 | 缺少凭据 / 凭据不可用 |
|
||||
| auth-failed | 禁止 | 禁止 | 认证失效 |
|
||||
| unreachable | 可重试 | 可重试 | 无法连接 |
|
||||
| incompatible | 禁止 | 禁止 | 接口不兼容 |
|
||||
| list-restricted | 禁止 | 允许按 ID 验证与检索 | 需手工指定知识库 |
|
||||
| catalog-ready | 允许 | 创建绑定时验证 | 目录可用,检索待验证 |
|
||||
| healthy | 允许 | 允许 | 正常 |
|
||||
| untested | 允许测试 | 已有绑定可尝试检索 | 未测试 |
|
||||
绑定创建另做目标检索验证,成功零命中可保存。保存实例不代表目录可用,目录成功也不
|
||||
代表绑定检索或整个产品验收通过。
|
||||
|
||||
保存新实例要求至少完成服务可达和认证验证。目录受限可以保存;检索接口尚未通过时,
|
||||
实例不能用于创建绑定。
|
||||
## 4. 目录与手工 ID
|
||||
|
||||
## 4. 连接测试状态机
|
||||
目录每页最多 100 项,UI 当前请求 30 项,页码上限 500;当前没有“总目录最多 500 项”
|
||||
的累计限制。Dify/RAGFlow 由远端分页,搜索过滤当前页;FastGPT 按 `parentId` 读取
|
||||
当前父级,在返回结果内搜索和分页。文件夹只能进入浏览,不能作为目录绑定目标。
|
||||
|
||||
```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。
|
||||
已经绑定的目录项禁用。手工 ID 不依赖详情接口一定可用,但保存前仍必须通过实际检索。
|
||||
详情失败不阻断基础检索,例如旧 Dify 的详情 405。
|
||||
|
||||
检索探测需要一个目标知识库。新增实例尚未选择目标时,目录成功即可把实例保存为“目录可用,
|
||||
检索待绑定验证”;创建绑定时补做真实检索。手工模式必须先填写目标 ID,再执行检索探测。
|
||||
检索请求正常完成但返回零条结果时,说明接口和目标可访问,可继续创建绑定;失败状态按错误
|
||||
分类处理。
|
||||
Renderer 丢弃已失效的目录、详情和测试响应,不把切换前的结果写回当前表单。它没有
|
||||
通过 IPC 取消已发出的目录请求,Main 请求仍受超时和服务生命周期管理。
|
||||
|
||||
每次测试拥有 request ID 和取消信号。用户修改 Provider、地址、凭据或实例后,旧 request ID
|
||||
的结果作废。
|
||||
## 5. 创建与编辑
|
||||
|
||||
## 5. 目录读取规则
|
||||
新建绑定时,用户尚未编辑的显示名称跟随当前目录选择更新;更换实例或手工输入另一个
|
||||
目标 ID 时清除旧自动名称。用户手动填写过名称后保留该草稿。编辑或重新绑定已有库时,
|
||||
始终保留已保存的本地名称,除非用户主动修改;此修正不自动重命名已有知识库。
|
||||
|
||||
1. 只有选定、启用且凭据可用的实例可以读取目录。
|
||||
2. Provider 列表接口按适配器定义分页,Main 最多向 Renderer 返回 500 个目录项。
|
||||
3. FastGPT 列表中的文件夹和知识库必须按类型区分;文件夹不可作为绑定目标。
|
||||
4. 列表搜索优先使用 Provider 支持的服务端过滤,不支持时只过滤已经有界加载的当前结果。
|
||||
5. 下拉列表以远端 ID 为值,以远端名称为标签;重名时追加短 ID 或父级名称。
|
||||
6. 已经绑定的目标显示“已添加”并禁止重复添加。
|
||||
7. 远端列表项名称变化不自动修改 GoodBuddy 显示名称,只更新远端名称快照。
|
||||
8. 刷新失败时可以保留上次成功目录供查看,但不得用旧目录完成新绑定,直到目标重新验证。
|
||||
流程为选择 `local | dify | fastgpt | ragflow`,外部类型再选择实例、目录项或手工 ID、
|
||||
显示名称、配置与测试查询。表单测试成功后允许保存;服务保存前再次执行同一查询验证,
|
||||
检查实例、绑定和唯一性是否在等待期间改变,再写入数据库。
|
||||
|
||||
## 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 配置 | 测试结果 |
|
||||
| 显示名称 | 不清除测试结果 |
|
||||
| Provider 类型 | 重新挂载绑定表单,清空旧类型状态 |
|
||||
| 实例 | 清空目标、详情、目录位置和搜索,恢复该 Provider 默认配置,清除测试结果 |
|
||||
| 远端目标、测试查询、通用或 Provider 参数 | 清除测试结果,需要重新测试 |
|
||||
| 显示名称 | 保留测试结果 |
|
||||
| 已保存实例摘要变化 | 旧测试结果不再视为有效 |
|
||||
| 更换现有绑定实例或远端目标 | 需要重新测试和重新绑定确认 |
|
||||
|
||||
## 7. 配置模型
|
||||
成功新建后仅把新库加入当前对话选择,不重新启用已取消的其他知识库。当前表单保存期间
|
||||
禁用字段和操作;generation 检查用于避免旧响应更新 UI,不构成可取消的保存事务。
|
||||
|
||||
### 7.1 通用配置
|
||||
## 6. 配置与优先级
|
||||
|
||||
| 字段 | 范围 | 默认值 | 语义 |
|
||||
| --- | --- | --- | --- |
|
||||
| `resultLimit` | 1 至 20 | 6 | Provider 结果标准化后最多保留的片段数 |
|
||||
| `requestTimeoutMs` | 1,000 至 60,000 | 15,000 | 单次远端请求总时限 |
|
||||
| `maxSnippetCharacters` | 500 至 8,000 | 4,000 | 单片段进入 IPC 和 Runtime 前的字符上限 |
|
||||
当前配置的精确字段、数值范围和默认值由[技术设计第 3 节](./external-knowledge-technical-design.md#3-共享契约)
|
||||
对应的共享 Schema 定义。恢复默认只重置本地表单,保存成功后才影响该绑定后续请求。
|
||||
|
||||
Provider 请求仍使用自己的候选数量或 Token 预算。`resultLimit` 只定义 GoodBuddy 最终保留
|
||||
上限,不改写 Provider 字段的单位。
|
||||
Dify `useDatasetDefaults=true` 时省略整个覆盖对象,使用远端当时配置;远端管理员修改
|
||||
默认值会影响后续结果。关闭后按完整覆盖对象发送,不与远端默认逐字段合并。
|
||||
FastGPT 使用本地保存的模式、Token 预算、相似度和重排开关,不开放查询优化。
|
||||
|
||||
### 7.2 Dify 配置
|
||||
RAGFlow 图谱与知识编译分别保存且默认关闭。图谱完成时间有效才允许开启图谱;非空
|
||||
知识编译模板组 ID 才允许开启知识编译。服务在发送启用请求前重新读详情确认;仅配置
|
||||
GraphRAG 或请求返回 200 都不能替代对应能力证据。证据不足时 UI 仍允许关闭已保存的开关。
|
||||
|
||||
| 字段 | 初始范围 | 说明 |
|
||||
当前没有自动版本识别、Adapter 版本矩阵或配置迁移状态。共享契约拒绝未知输入与
|
||||
Provider 错配,不能据此承诺所有部署版本均支持每个显式参数。
|
||||
|
||||
## 7. 绑定可用性
|
||||
|
||||
绑定只持久化配置和 `lastVerifiedAt`,没有独立 `binding_status`、最后成功检索时间或
|
||||
远端缺失状态字段。详情与列表当前按实例推导:
|
||||
|
||||
| 条件 | UI 状态 |
|
||||
| --- | --- |
|
||||
| 实例不存在 | `temporarily-unavailable` |
|
||||
| 实例停用 | `instance-disabled` |
|
||||
| 凭据未配置、无法解密或最近认证失败 | `credential-error` |
|
||||
| 最近探测失败或不可达 | `temporarily-unavailable` |
|
||||
| 其余且绑定有验证时间 | `ready` |
|
||||
| 其余且无验证时间 | `untested` |
|
||||
|
||||
聊天选择器保留已选 ID,但不可用行禁用勾选。检索时的 401、404、超时等通过本次失败
|
||||
报告,不自动写成持久 `remote-missing/config-invalid` 状态。`ready` 表示当前可尝试使用,
|
||||
不保证下一次请求成功。
|
||||
|
||||
## 8. 多库检索与失败
|
||||
|
||||
请求使用已冻结的 GoodBuddy ID 集合,`retrieveMany` 去重后并行检索各库。本地与外部
|
||||
共享入口,外部库不触发本地 Embedding。预算按各库结果的 rank 轮转分配,先各库第 1 条、
|
||||
再各库第 2 条,同 rank 按输入库顺序处理;不先耗尽整个库,也不比较 Provider 原始分数。
|
||||
当前没有每库最低字符配额。精确预算见
|
||||
[技术设计第 7 节](./external-knowledge-technical-design.md#7-检索编排)。
|
||||
|
||||
成功零命中返回空 results 且无 failure。单库错误返回 `diagnostics.failure`,其他库
|
||||
仍可返回结果;上层据此区分部分失败与全部失败。MCP 在有失败且没有任何片段时返回
|
||||
工具错误,即使另有成功零命中的库;预检索则保留降级诊断。用户取消或服务关闭会向上
|
||||
传播取消,不把整次取消解释为成功零命中。
|
||||
|
||||
客户端错误码包括 `NETWORK/TIMEOUT/CANCELLED/AUTH/FORBIDDEN/NOT_FOUND/RATE_LIMITED/`
|
||||
`INCOMPATIBLE/INVALID_RESPONSE/SERVER`,均以 `EXTERNAL_KB_` 为前缀。服务另报告凭据
|
||||
不可用、停用、配置变化、配置错配、能力不可用、重复绑定和实例被引用。远端错误正文
|
||||
不直接成为 UI 错误;部分本地校验错误仍是 Zod、TypeError 或 RangeError,不能承诺
|
||||
所有失败都已有独立本地化诊断。
|
||||
|
||||
## 9. 删除与引用
|
||||
|
||||
停用和清除凭据保留绑定,UI 清除凭据需要确认。有绑定的实例不能直接删除;管理界面
|
||||
列出受影响知识库并确认后,依次移除本地库再删实例。若中途失败,已成功移除的本地
|
||||
记录保持删除,刷新列表后显示当前状态。任何一步均不发送远端内容删除请求。
|
||||
|
||||
历史引用保存当时的片段与外部定位,移除实例或绑定后仍能阅读。当前不重新读取远端
|
||||
片段,不打开来源 URL,不查询实例是否已删除;因此没有自动“连接已移除”标签。
|
||||
|
||||
App 与 Main 引用处理共用 `knowledgeReferenceKey`,键包含远端库、文档和片段 ID、
|
||||
定位及正文,避免外部结果缺少本地 ID 时被错误合并。弹窗只显示优先选中的一个定位。
|
||||
早期 Dify 历史引用保存和重载检查使用测试会话;后续完整 App 已从实际输入区生成真实
|
||||
短答案并点击引用验证。两轮证据分别保留,不把测试会话当成模型回答。
|
||||
|
||||
## 10. 需求追踪与完整性
|
||||
|
||||
| 需求 | 规则 | 场景 |
|
||||
| --- | --- | --- |
|
||||
| `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 要求时从知识库详情选择或填写 |
|
||||
| `EK-FR-01` 至 `EK-FR-05` | 实例状态、保存后测试、目录与手工 ID | `EK-US-A1`、`EK-US-A2`、`EK-US-B1`、`EK-US-B2` |
|
||||
| `EK-FR-06` 至 `EK-FR-10` | 对象关系、创建编辑、配置与优先级 | `EK-US-B3`、`EK-US-B4`、`EK-US-D1`、`EK-US-D3` |
|
||||
| `EK-FR-11` 至 `EK-FR-14` | 范围、可用性、结果与失败、引用 | `EK-US-C1` 至 `EK-US-C4`、`EK-US-D2`、`EK-US-D4` |
|
||||
| `EK-FR-15`、`EK-FR-16` | 只读生命周期、凭据与请求边界 | `EK-US-A3`、`EK-US-C2` |
|
||||
|
||||
`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 增加新字段不会自动启用。
|
||||
- 当前实例版本离开 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、FastGPT、RAGFlow 最低版本,形成固定支持矩阵并保存真实响应 fixture。
|
||||
- 验证各版本检索探测的 API 审计、用量和内容修改行为。
|
||||
- 确定 Provider 返回 URL 的安全打开策略;未验证前只显示文本定位和远端 ID。
|
||||
- 依据真实延迟确定默认并发上限和跨知识库上下文配额。
|
||||
三家真实 Provider 的生产服务、本机 HTTP MCP、生产 IPC `always` 短答案,以及 Dify
|
||||
`auto` 与完整 App 输入区回答、引用已有证据,详见[实施进度](./progress.md)。`auto` 本轮
|
||||
使用进程内工具调用,不覆盖远程 ACP。FastGPT/RAGFlow 完整桌面、复杂问答和远程
|
||||
forced-preflight 仍待完成;远程已确认网络可达,但已有 SSH 凭据在验证程序内无法解密。
|
||||
后续 Portal 与控件修正后的聚焦测试、全量回归及相关检查已通过,精确结果由实施进度维护。
|
||||
原场景中的保存前分项测试、按地址
|
||||
传输确认、取消交互、版本能力状态和完整诊断尚不等同当前实现,不能把对应 User Story
|
||||
全部标为通过。是否调整这些验收要求由功能 owner 确认,本轮不增加新的状态框架。
|
||||
|
||||
@@ -4,14 +4,38 @@
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 状态 | 三家短答案、Dify 完整 App 与当前 UI 自动回归已通过;剩余产品验收待完成 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 关联逻辑 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| 关联界面 | [外部知识库 UI 设计](./external-knowledge-ui-design.md) |
|
||||
| 技术设计 | [外部知识库技术设计](./external-knowledge-technical-design.md) |
|
||||
|
||||
## 当前交付范围
|
||||
|
||||
当前工作区已提供实例管理、加密凭据保存、目录与手工 ID、外部绑定、Provider 检索参数、
|
||||
统一知识库列表、聊天预检索与按需工具检索、外部引用和本地移除的实现。参数与恢复默认
|
||||
遵循下文只读产品决策。2026-09-13 已验证三家 Provider 的生产服务与本机 HTTP MCP,
|
||||
以及 Dify 桌面的实例保存、绑定、参数更新、重启和历史引用。后续已修正外部弹窗的字段
|
||||
样式与布局,并验证三家通过生产 IPC 生成有依据的短答案、Dify 按需检索,以及从完整 App
|
||||
输入区发送 Dify 问题并点击真实答案引用。该证据不代表广泛问答质量。
|
||||
FastGPT/RAGFlow 完整桌面、远程预检索仍待验收;当前 UI 修改后的聚焦测试、全量回归与
|
||||
相关检查已通过,精确结果见[实施进度](./progress.md)。
|
||||
|
||||
本文的功能需求和产品验收仍是验收目标。以下差异不能仅凭实现存在就标为满足:
|
||||
|
||||
| 原需求 | 当前实现与待核对差异 |
|
||||
| --- | --- |
|
||||
| `EK-FR-02`、`EK-FR-03` | 实例先保存为未测试,再在列表执行目录测试;目标检索在绑定时验证。没有分项草稿探测、版本或完整能力快照 |
|
||||
| `EK-FR-04` | 已有目录搜索和分页;Dify/RAGFlow 搜索过滤当前页,FastGPT 搜索当前父级响应,不能保证全目录搜索 |
|
||||
| `EK-FR-09` | 内置严格配置已实现,没有按探测版本维护支持矩阵;Dify 高级只读能力展示、Metadata 编辑和 RAGFlow Rerank 未提供 |
|
||||
| `EK-FR-13`、`EK-FR-14` | 已显示片段、原始评分、总耗时和失败;没有完整的 Provider 耗时、裁剪前数量与截断诊断,也不持久化远端目标失效状态 |
|
||||
| `EK-FR-16` | Main 固定端点及数据说明已接入;当前没有单独接入全局内网兼容模式和按地址传输确认流程 |
|
||||
|
||||
这些差异由功能 owner 在验收时核对。2026-09-09、2026-09-10 真实接口探测只作为历史
|
||||
兼容性资料,不作为本轮桌面产品验证结果。
|
||||
|
||||
## 1. 背景
|
||||
|
||||
不少企业已经在内网部署 Dify、FastGPT 或 RAGFlow,并在其中维护制度、产品、研发
|
||||
@@ -42,6 +66,11 @@ GoodBuddy 负责。
|
||||
10. API Key 在用户输入表单和受限 IPC 中短暂存在,由 Main 进程加密保存并用于请求;
|
||||
保存后不回传 Renderer,也不进入 Runtime、日志、普通元数据和检索引用。
|
||||
|
||||
第一版按只读接入交付:参数修改和恢复默认只更新 GoodBuddy 中当前绑定的客户端配置,
|
||||
在后续检索请求中使用,不修改远端知识库默认配置,也不影响其他客户端。远端文档、
|
||||
索引、图谱构建、模型和 Pipeline 设置由用户在对应平台维护。检索请求仍可能产生远端
|
||||
Embedding、重排用量和审计记录。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
### 3.1 用户目标
|
||||
@@ -93,22 +122,23 @@ GoodBuddy 负责。
|
||||
1. 用户在知识库页面点击“管理外部实例”。
|
||||
2. 页面列出实例名称、Provider、服务地址、状态和已绑定知识库数量。
|
||||
3. 用户添加实例,填写名称、Provider、服务地址和 API Key。
|
||||
4. “测试连接”分别验证服务可达、认证、知识库列表和检索能力。
|
||||
5. 用户保存后,API Key 被加密保存,列表只显示凭据已配置。
|
||||
4. 用户先保存实例,API Key 被加密保存,列表显示凭据状态和“未测试”。
|
||||
5. 在已保存实例上“测试连接”读取目录并记录结果;检索能力在绑定目标时验证。
|
||||
|
||||
列表接口不可用但已知远端知识库 ID 可检索时,实例可以保存为“需手工指定知识库”。
|
||||
服务不可达或认证失败时不得保存为可用实例;用户可以保留表单并修正。
|
||||
目录 403 显示“需手工指定知识库”;不可达或认证失败保留实例和失败状态,用户可编辑
|
||||
后重试。保存本身不代表可用。原分项测试验收目标见 `EK-FR-03`,当前实现差异见上表。
|
||||
|
||||
### 6.2 添加外部知识库
|
||||
|
||||
1. 用户点击现有“新建知识库”。
|
||||
2. 在“知识库类型”中选择“本地”、Dify、FastGPT 或 RAGFlow。
|
||||
3. 选择外部类型后,左侧选择同 Provider 的外部实例。
|
||||
4. GoodBuddy 自动读取远端知识库列表,并填入可搜索下拉框。
|
||||
4. GoodBuddy 自动读取远端知识库列表,提供搜索、分页和目标选择。
|
||||
5. 列表权限不可用时,用户可以手工填写远端知识库 ID。
|
||||
6. 用户选择一个远端知识库,填写 GoodBuddy 显示名称。
|
||||
7. 右侧展示通用配置和当前 Provider 的专属配置。
|
||||
8. 用户填写测试查询并执行一次有界检索;请求成功后创建本地绑定,零结果不算失败。
|
||||
8. 用户填写测试查询并执行有界检索;成功后可添加,零结果不算失败。保存时服务会再次
|
||||
执行该查询验证目标和配置,之后创建本地绑定。
|
||||
|
||||
添加完成后沿用现有知识库规则,把新绑定加入当前聊天选择,但不会重新启用用户此前
|
||||
取消的其他知识库。
|
||||
@@ -222,13 +252,20 @@ Provider 的远端创建、更新、上传或删除接口。
|
||||
| --- | --- | --- | --- |
|
||||
| Dify | 知识库列表与详情 | Dataset Retrieve | 使用知识库默认配置,或完整覆盖检索方式、Top K、Score 阈值及 Rerank 模式 |
|
||||
| FastGPT | 知识库层级列表与详情 | Dataset Search Test | 搜索模式、Token 上限、相似度、Rerank |
|
||||
| RAGFlow | Dataset 列表与详情 | Retrieval | 相似度、向量权重、KNN Top K、Rerank、图谱检索、Knowledge Compilation |
|
||||
| RAGFlow | Dataset 列表与详情 | Retrieval | 相似度、向量权重、KNN Top K、图谱检索、Knowledge Compilation;当前未提供 Rerank 配置 |
|
||||
|
||||
“首批专属配置”表示适配器允许表达的字段,不保证每个实例版本都支持。具体显隐由能力
|
||||
探测和[逻辑设计](./external-knowledge-logic-design.md)共同决定。
|
||||
|
||||
## 9. 产品验收
|
||||
|
||||
以下为验收清单,尚未整体通过。第 2 项已有三家生产服务绑定与检索证据,Dify 另有桌面
|
||||
操作证据;其他 Provider 的桌面、受限目录与高级配置等仍需补验。第 12 项确认流程当前
|
||||
未实现。早期历史引用检查使用标记为测试的会话;后续已从完整 App 输入区生成 Dify
|
||||
真实短答案并点击引用,三家另有生产 IPC 短答案证据。完整 App 尚未覆盖 FastGPT/RAGFlow,
|
||||
也未评估广泛问答质量;本机 MCP 成功不替代被 SSH 凭据解密阻断的真实远程预检索。
|
||||
详细结果以实施进度为准。
|
||||
|
||||
1. 用户可以在知识库页面完成三种 Provider 的实例添加、连接测试、编辑和删除。
|
||||
2. 每种 Provider 至少通过一个真实自部署实例完成列表读取、绑定和检索。
|
||||
3. 列表权限关闭时,可以通过手工 ID 添加并验证一个可检索知识库。
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,577 +4,317 @@
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 状态 | 三家生产 IPC 真实生成、Dify 完整 App 与当前 UI 自动回归已通过;远程待验收 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 产品需求 | [外部知识库接入 PRD](./external-knowledge-prd.md) |
|
||||
| 逻辑设计 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| UI 设计 | [外部知识库 UI 设计](./external-knowledge-ui-design.md) |
|
||||
| 行为规则 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| 界面 | [外部知识库 UI 设计](./external-knowledge-ui-design.md) |
|
||||
| 验证状态 | [实施进度](./progress.md) |
|
||||
|
||||
## 1. 设计约束
|
||||
## 1. 实现边界
|
||||
|
||||
1. 外部知识库扩展现有知识库领域,不建立独立检索工具、聊天范围或 Runtime 权限模型。
|
||||
2. 外部绑定使用远端检索,不落地远端文档、分块、向量和图谱。
|
||||
3. 所有网络、凭据、Provider 响应校验和结果标准化都在 Main 完成。
|
||||
4. Renderer 只能通过受限 IPC 管理实例、读取目录、保存绑定和发起测试。
|
||||
5. Dify、FastGPT、RAGFlow 使用独立 Adapter 和判别配置,不通过任意 JSON 请求模板实现。
|
||||
6. 首次实现只调用无内容修改语义的列表、详情和检索接口。
|
||||
7. Provider API 变化通过真实 fixture、Adapter 版本和能力快照处理,不通过猜测字段兼容。
|
||||
外部知识库复用 GoodBuddy 知识库 ID、聊天范围和检索入口。网络、凭据、响应读取和
|
||||
标准化在 Main 完成,Renderer 通过明确的 Preload 方法调用 IPC。远端只执行目录、详情和
|
||||
检索请求,不创建、修改、上传或删除远端内容,不调用外部 App、Chat、Workflow 或 Agent。
|
||||
|
||||
## 2. 现有代码边界
|
||||
本地 `file | directory | url` 仍表示需要导入和索引的资料来源。外部绑定独立存储,
|
||||
不加入该枚举,也不生成本地文档、分块、向量或图谱。实际用于回答的片段随会话引用保存。
|
||||
|
||||
当前实现中的主要扩展点:
|
||||
## 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` | 与预检索复用同一编排和引用标准化 |
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `src/main/knowledge/external/external-knowledge-client.ts` | 一个 `ExternalKnowledgeClient` 内按 Provider 分支处理固定端点、地址规范化、分页、HTTP 错误、超时、取消、响应上限和结果映射;同文件定义 `ExternalKnowledgeError` |
|
||||
| `src/main/knowledge/external/external-knowledge-service.ts` | 实例保存与测试、凭据加解密、目录与详情、检索测试、创建或更新绑定、请求生命周期与配置变化检查 |
|
||||
| `src/main/knowledge/external/external-knowledge-store.ts` | 在已有 SQLite 连接上读写实例 JSON 和绑定 JSON |
|
||||
| `src/main/knowledge/knowledge-database.ts` | v12 迁移、知识库与绑定事务、唯一约束及删除外键 |
|
||||
| `src/main/knowledge/knowledge-service.ts` | 本地与外部检索分派、混合检索和总字符预算;阻止外部绑定进入本地维护操作 |
|
||||
| `src/shared/external-knowledge-contracts.ts` | Provider 配置、实例与绑定输入、目录、测试结果和外部引用定位契约 |
|
||||
| `src/shared/knowledge-contracts.ts`、`contracts.ts`、`assistant-contracts.ts` | 现有知识结果、列表和会话引用增加外部字段 |
|
||||
| `src/shared/knowledge-reference.ts` | 预检索与 MCP 共用的引用转换和去重键 |
|
||||
| `src/main/ipc.ts`、`src/preload/index.ts`、`src/shared/ipc-channels.ts` | 可信发送方检查、Zod 输入解析、类型化跨进程调用 |
|
||||
| `src/renderer/src/ExternalKnowledge.tsx` | 实例 Modal、绑定表单、Provider 配置、外部详情与检索结果 |
|
||||
| `KnowledgeWorkspace.tsx`、`App.tsx`、`KnowledgeCitationDialog.tsx` | 统一列表、聊天选择、刷新及历史引用显示 |
|
||||
|
||||
外部 Provider 不是 `KnowledgeSourceType`。现有 `file | directory | url` 表示会进入本地
|
||||
文档、分块和索引的资料来源;把远端绑定塞入该枚举会错误触发同步、分块和来源打开逻辑。
|
||||
`ExternalKnowledge.tsx` 的字段改用全局 `.field`,修正 body Portal 脱离知识库祖先后
|
||||
丢失 scoped 控件样式的问题。CSS 提供 560px 紧凑弹窗和粘附底部操作区;实例选择复用
|
||||
既有 picker 样式并实现菜单键盘行为。没有新增 Main 模块,具体控件规则由
|
||||
[UI 设计](./external-knowledge-ui-design.md#当前实现与验收边界)维护。
|
||||
|
||||
## 3. 模块设计
|
||||
当前没有独立 Provider 文件、Adapter Registry、Normalizer、版本化能力清单或配置迁移框架。
|
||||
本文件按现有三个 Main 模块描述实现,不把旧草案中的拆分方案列为待建基础设施。
|
||||
|
||||
建议增加:
|
||||
## 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
|
||||
```
|
||||
### 3.1 实例与绑定
|
||||
|
||||
职责:
|
||||
实例输入包含 `id?`、`name`、`provider`、`baseUrl`、`enabled` 和
|
||||
`credential: keep | replace | clear`。实例摘要返回启停、凭据状态、`probeStatus`、绑定数、
|
||||
最近测试时间和错误码,不返回凭据 envelope。当前没有 `detectedVersion`、`adapterVersion`
|
||||
或 `transportSecurity` 字段。
|
||||
|
||||
- `ExternalKnowledgeService`:实例、目录、绑定、测试和检索编排。
|
||||
- `ExternalKnowledgeStore`:实例和绑定持久化,凭据加解密。
|
||||
- `ExternalKnowledgeHttp`:超时、取消、大小限制、地址和响应读取。
|
||||
- `ExternalKnowledgeRegistry`:按 Provider 返回内置 Adapter,不接受运行时插件注册。
|
||||
- `ExternalKnowledgeNormalizer`:统一结果边界、截断和引用。
|
||||
- Adapter:构造固定端点与请求,解析 Provider 响应,声明静态配置和能力条件。
|
||||
绑定保存 `knowledgeBaseId`、`instanceId`、`provider`、`remoteKnowledgeBaseId`、
|
||||
`remoteName`、`commonConfig`、`providerConfig` 和 `lastVerifiedAt`。GoodBuddy 名称和描述
|
||||
保留在知识库记录。创建和更新输入都要求用户填写 `testQuery`;服务保存前重新执行检索,
|
||||
因此先点击“测试检索”再保存会产生两次检索请求。成功零命中允许保存。
|
||||
|
||||
`KnowledgeService` 继续作为聊天、检索测试和 MCP 网关的统一入口。它根据
|
||||
`KnowledgeBase.kind` 调用现有本地检索或 `ExternalKnowledgeService.retrieve`。
|
||||
### 3.2 配置范围
|
||||
|
||||
## 4. Provider Adapter
|
||||
| 配置 | 当前共享契约 |
|
||||
| --- | --- |
|
||||
| 通用 | `resultLimit` 1 至 20,默认 6;`requestTimeoutMs` 1,000 至 60,000,默认 15,000;`maxSnippetCharacters` 100 至 8,000,默认 4,000 |
|
||||
| Dify | 默认分支仅包含 `provider` 和 `useDatasetDefaults: true`;覆盖分支使用严格的 `retrievalModel` 对象,包含检索方式、Top K、阈值开关和重排开关,按启用条件校验阈值、重排模型或权重 |
|
||||
| FastGPT | `searchMode`、`tokenLimit`(1 至 30,000)、`similarity`、`usingRerank`;不发送查询优化参数 |
|
||||
| RAGFlow | `similarityThreshold`、`vectorSimilarityWeight`、`knnTopK`(1 至 2,048)、`useKg`、`includeKnowledgeCompilation`;当前无 Rerank、Metadata 编辑字段 |
|
||||
|
||||
```ts
|
||||
type ExternalKnowledgeProvider = 'dify' | 'fastgpt' | 'ragflow'
|
||||
Provider 必须与实例一致。Dify 加权配置还在客户端检查权重和为 1。共享查询输入上限为
|
||||
4,000 字符,Dify 客户端另按 Unicode 码点拒绝超过 250 字符的查询,不静默压缩。
|
||||
|
||||
interface ExternalKnowledgeAdapter<PersistedConfig, CapabilitySnapshot> {
|
||||
readonly provider: ExternalKnowledgeProvider
|
||||
readonly configVersion: number
|
||||
### 3.3 结果与引用
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
现有 `KnowledgeRetrievalResult` 的本地 `documentId/sourceId/chunkId` 改为可选,增加
|
||||
`external?: ExternalKnowledgeLocator`,没有另建一套 `UnifiedKnowledgeRetrievalResult`。
|
||||
外部定位包含 Provider、实例 ID、远端库 ID,以及实际返回的文档 ID、片段 ID、位置和评分。
|
||||
FastGPT 数组评分存为 `providerScores: {type, value, index?}[]`,保留顺序与类型;数值评分
|
||||
存为 `providerScore`,不选取数组首项冒充总分。
|
||||
|
||||
`probeRetrieval` 可以复用 `retrieve`,但必须使用用户在创建向导中明确输入的测试查询。
|
||||
不得生成会泄露组织信息的默认问题,不得调用 Provider 的 App 或模型问答接口,也不得
|
||||
修改远端知识内容。Provider 自身记录 API 审计或计算检索用量不属于内容修改,必须由界面
|
||||
提前说明。
|
||||
外部结果沿用旧结果容器的空 `channels` 和 `scores.fusedScore = 0`。该零值是结构占位,
|
||||
不表示质量;`toKnowledgeReference` 不把它写成引用总分。外部结果没有统一 `relevance`,
|
||||
不同 Provider 的原始分数不参与跨库混排。引用去重键包含外部定位和片段,不伪造本地 ID。
|
||||
|
||||
Adapter 返回数据对象,不返回 React 组件、HTML 或远端 Schema。Renderer 中的配置定义
|
||||
由共享的 Provider 判别联合和本地字段描述生成。
|
||||
## 4. SQLite 与凭据
|
||||
|
||||
## 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'));
|
||||
```
|
||||
|
||||
增加实例表:
|
||||
数据库 v12 增加两张表:
|
||||
|
||||
```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
|
||||
value_json 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)
|
||||
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_id TEXT NOT NULL,
|
||||
value_json TEXT NOT NULL,
|
||||
UNIQUE(instance_id, remote_id)
|
||||
);
|
||||
```
|
||||
|
||||
已有知识库迁移为 `kind = 'local'`。外部知识库仍保留现有名称、描述和创建时间,但本地
|
||||
`storageMode`、图谱、分块和本地检索设置不进入外部请求;后续可以在数据库重构时再移除
|
||||
这些历史必填默认值,本功能不为此扩大迁移范围。
|
||||
没有向 `knowledge_bases` 增加 `kind` 列;IPC 快照根据是否存在绑定投影
|
||||
`KnowledgeLibrary.kind` 和 `external`。旧本地数据保留,知识库与绑定在同一事务中保存。
|
||||
外部库沿用本地表所需的默认字段,但这些字段不进入 Provider 请求。
|
||||
|
||||
### 6.2 凭据
|
||||
Main 启动把已有 `secureCipher` 注入 `KnowledgeService`。服务复用
|
||||
`SettingsCredentialCipher` 和现有加密函数,把 envelope 放入实例 JSON;`replace` 要求
|
||||
安全存储可用,`keep` 保留已有 envelope,`clear` 移除凭据。列表会尝试解密以区分
|
||||
`configured/missing/unavailable`,请求前再次解密,没有独立明文凭据缓存。
|
||||
|
||||
复用 `SettingsCredentialCipher` 和 Electron `safeStorage`:
|
||||
## 5. IPC 与生命周期
|
||||
|
||||
- `replace`:Main 加密新值后在一个事务中替换 envelope。
|
||||
- `keep`:不读取、不重写现有 envelope。
|
||||
- `clear`:确认后删除 envelope,并把实例凭据状态设为缺失。
|
||||
- 安全存储不可用时拒绝新增或替换,不降级为明文。
|
||||
- 解密只发生在发起请求前;请求结束后不缓存明文。
|
||||
Preload 暴露实例 `List/Save/Test/SetEnabled/Delete`、目录 `List/Get`、绑定
|
||||
`Create/Update` 和 `externalRetrievalTest`。通道使用 `knowledge:external-*` 前缀,
|
||||
准确名称以 `ipc-channels.ts` 为准。移除绑定复用 `deleteLibrary`,没有独立 remove 通道。
|
||||
|
||||
实例导出、诊断和数据库普通 JSON 视图均排除 `credential_envelope_json`。
|
||||
Handler 调用 `assertTrustedSender` 并解析共享输入;检索按实例 ID 在 Main 读取地址与凭据。
|
||||
实例先保存为 `untested`,列表上的连接测试调用目录请求并更新摘要,不测试未保存草稿。
|
||||
绑定检索测试是单独操作,不会改变实例探测状态。探测状态取值为 `untested`、
|
||||
`catalog-ready`、`list-restricted`、`auth-failed`、`unreachable`、`failed`。
|
||||
|
||||
## 7. IPC 设计
|
||||
服务按实例记录 AbortController。保存实例、停用、删除以及服务关闭会取消相应请求;
|
||||
调用方取消信号与总超时合并。取消即是配置变更的唯一处理方式:被取消的连接测试直接抛出
|
||||
`EXTERNAL_KB_CANCELLED`,不写回探测结果,因此不需要额外的前后配置快照比较。绑定保存在
|
||||
验证请求前后各检查一次同一远端目标是否已被占用,最终以 `UNIQUE(instance_id, remote_id)`
|
||||
索引为准。共享 IPC 没有 `requestId` 或取消方法;Renderer 用局部 generation/active 标记
|
||||
丢弃旧响应,这与中止 Main 请求是两种行为。
|
||||
|
||||
建议增加独立通道:
|
||||
删除仍被引用的实例返回 `EXTERNAL_KB_INSTANCE_IN_USE`。UI 确认后逐个调用本地
|
||||
`deleteLibrary`,再删除实例;这是顺序操作,发生中途失败会刷新已移除的绑定,不是跨调用事务。
|
||||
|
||||
```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
|
||||
```
|
||||
## 6. HTTP 边界
|
||||
|
||||
所有 Handler 必须:
|
||||
客户端只接受 HTTP(S),拒绝地址内的用户名、密码、查询与片段;按 Provider 规范化 API
|
||||
路径,仅拼接内置端点。请求使用 Bearer 认证,`redirect: 'error'`,无自动重试或公共云回退。
|
||||
默认超时 15 秒,单个响应默认上限 5,000,000 字节;响应头和流式读取均检查上限,取消
|
||||
期间终止读取。解析后只投影需要的有界字段,最多返回 20 条、每片段 8,000 字符。
|
||||
|
||||
1. `assertTrustedSender`。
|
||||
2. 用共享 Zod Schema 解析严格输入。
|
||||
3. 根据实例 ID 在 Main 读取地址和凭据,不接受 Renderer 在检索请求中重复传入。
|
||||
4. 使用稳定错误码返回脱敏错误。
|
||||
5. 对网络操作支持 AbortSignal、总超时和响应大小限制。
|
||||
当前使用 `fetcher ?? fetch`,没有单独连接全局内网兼容模式、证书例外或按地址保存传输
|
||||
确认。2026-09-13 当前生产服务在真实 Electron 中对两个 Dify 和 FastGPT HTTPS 配置完成
|
||||
严格证书校验,RAGFlow 使用 HTTP。此证据覆盖这些配置的请求,不表示旧 PRD 的地址确认
|
||||
或证书例外流程已实现;相关产品差异仍由功能 owner 核对。
|
||||
|
||||
目录和测试请求返回 `requestId`。Renderer 仍需校验当前类型、实例和目标与响应关联,避免
|
||||
旧响应覆盖新表单。
|
||||
## 7. 检索编排
|
||||
|
||||
## 8. HTTP 边界
|
||||
`KnowledgeService.retrieve` 按知识库 ID 单行查询本地绑定,存在绑定时调用
|
||||
`ExternalKnowledgeService.retrieve`,否则执行现有本地路径。外部结果附加
|
||||
`diagnostics.external`,不运行本地向量、全文或图谱检索。
|
||||
|
||||
### 8.1 服务地址
|
||||
`ExternalKnowledgeService` 区分两个入口:`retrieve` 是生产检索路径,只发出一次检索请求;
|
||||
`testRetrieval` 仅用于保存绑定前的验证,额外在同一超时预算内确认 RAGFlow 图谱与知识编排
|
||||
能力可用,能力缺失时返回 `EXTERNAL_KB_CAPABILITY_UNAVAILABLE`。生产检索不执行该能力预检,
|
||||
因此正常对话不会多一次 detail 请求,也不会出现仅属于保存校验的错误码。
|
||||
|
||||
- 只接受 `http:` 和 `https:`。
|
||||
- 拒绝用户名、密码、查询和片段。
|
||||
- Adapter 负责规范化 Provider 所需 API 根路径,避免用户填写完整操作端点。
|
||||
- 请求只能拼接 Adapter 内置相对路径,远端目录项或错误不能改变目标 URL。
|
||||
- 重定向默认拒绝;若某 Provider 的受支持部署确需重定向,只允许同源且不得跨跳转发送
|
||||
Authorization。
|
||||
- 当前全局“内网兼容模式”决定是否允许 HTTP 和非标准 HTTPS 证书;操作开始后冻结该值。
|
||||
- HTTP 或实际使用证书例外时,Main 返回 `transportSecurity` 风险状态。用户必须针对规范化
|
||||
后的当前地址明确确认,才能保存实例或发送 API Key;地址变化立即使旧确认失效。
|
||||
- 不进行公共云回退,不把自部署地址替换成 Provider 官方地址。
|
||||
`retrieveMany` 对传入知识库 ID 去重并读取知识库,使用 `Promise.all` 逐库检索;只有选中
|
||||
需要向量的本地库时才准备本地查询 Embedding。当前没有额外的 Provider 并发队列。单库
|
||||
失败写入 `response.diagnostics.failure` 并保留空结果,成功零命中没有 failure;调用方
|
||||
取消仍向上传播。当前返回 `{knowledgeBaseId, response}[]`,没有独立的 Outcome 状态联合。
|
||||
|
||||
用户明确配置内网地址属于产品用途,因此不能采用禁止私网地址的通用 SSRF 规则。安全边界
|
||||
依靠显式实例配置、Main-only 请求、固定端点、无任意方法和无远端 URL 跟随。
|
||||
服务检索与绑定验证共用同一裁剪逻辑:先按绑定的结果数与单片段限制裁剪,并限制该次片段
|
||||
总量为 48,000 字符。
|
||||
`retrieveMany` 展平各库结果并按库内 rank 稳定排序,再轮转分配 48,000 字符的片段预算:
|
||||
先处理各库第 1 条,再处理各库第 2 条,同 rank 保持输入库顺序。不会先耗完整个库再轮到
|
||||
下一个库;仍没有固定的每库最低字符配额。关联的本地上下文 groups 随相应结果使用另一份
|
||||
48,000 字符预算。二次裁剪会标记 `context.truncated`,但第一次标准化裁剪不提供完整的
|
||||
远端原始条数与截断诊断。
|
||||
|
||||
### 8.2 请求限制
|
||||
聊天预检索、检索测试相关 IPC 和 `KnowledgeMcpGateway` 已使用统一检索入口。引用转换
|
||||
复用 `knowledge-reference.ts`,聊天与 MCP 仍按现有授权范围和各自输出预算处理结果。
|
||||
MCP 在总预算裁剪后按库内 rank 交错选择结果,再应用工具条数和字节上限;有失败且
|
||||
没有任何片段时返回工具错误。预检索只在全部库失败时报告整体失败,成功零命中伴随其他
|
||||
库失败时保留降级诊断。这两种上层呈现并不完全相同。
|
||||
|
||||
- 默认超时 15 秒,用户可在 1 至 60 秒内调整。
|
||||
- 目录、详情和检索分别设置响应字节上限。
|
||||
- JSON 解析后仍执行深度、数组数量、字符串长度和未知字段策略。
|
||||
- 默认不自动重试。用户重试或上层下一次检索会产生新请求。
|
||||
- 取消、应用关闭和实例删除立即终止相关请求。
|
||||
- 日志只记录 Provider、实例 ID、操作、状态码、稳定错误码、耗时和字节数。
|
||||
## 8. 引用显示与历史数据
|
||||
|
||||
会话契约和数据库保存外部定位及有界片段。外部引用弹窗读取保存的片段,显示 Provider、
|
||||
实例 ID、远端库和实际返回的定位及评分;不请求本地 `referenceContext`,不显示本地
|
||||
`openSource` 操作。当前没有远端来源打开或引用刷新请求,`sourceUrl` 是可选契约字段,
|
||||
三家现有映射没有填充它。
|
||||
|
||||
App 的 `source-references` 事件使用共享 `knowledgeReferenceKey` 去重传入引用,再合并
|
||||
尚未被替代的旧引用,最多保留 20 条。键包含 Provider、实例、远端库、远端文档和片段 ID、
|
||||
定位与片段正文;外部结果缺少本地 ID 时也不会仅因本地 ID 相同而被合并。引用弹窗按
|
||||
`context.locator ?? external.location ?? locator` 选择一个定位,只显示一次。
|
||||
|
||||
移除绑定后历史片段仍可阅读;当前弹窗不查询实例状态,也不生成“连接已移除”标记。
|
||||
实例名称没有作为独立字段存入外部 locator,不能承诺删除实例后仍显示其名称。
|
||||
|
||||
2026-09-13 Dify 桌面报告已验证实时搜索引用通过生产 `conversations.saveLocal` 保存、
|
||||
重载并打开弹窗。会话由验证程序标记为测试记录,没有模型生成答案;最终引用去重与定位
|
||||
修正后的回归状态单独记录在[实施进度](./progress.md)。
|
||||
|
||||
后续 `kb-full-app-live` 则通过完整 App 输入区真实发送并保存模型答案,再点击引用。
|
||||
引用片段、远端文档和片段 ID 与真实检索记录一致,无伪造会话或事件;此证据覆盖 Dify
|
||||
单库短答案。三家生产 IPC 生成检查还验证了 `task_events` 中的文本、引用和选中库范围。
|
||||
|
||||
## 9. Provider 映射
|
||||
|
||||
本节记录 2026-09-03 核对的初始公开接口。实施时应把每个受支持版本的真实响应保存为
|
||||
去敏 fixture,并以 fixture 和真实实例测试作为发布依据。
|
||||
本节区分当前客户端行为与历史接口探测。历史响应和官方文档只说明当时观察到的接口,
|
||||
不证明当前桌面已完成验收。
|
||||
|
||||
### 9.0 真实实例验证基线
|
||||
|
||||
| 历史日期 | 证据与限制 |
|
||||
| --- | --- |
|
||||
| 2026-09-09 | [基线报告](./external-knowledge-probe-baseline.json):累计 107 次只读请求,最终归档脚本 19 次;当时 HTTPS 放宽证书校验 |
|
||||
| 2026-09-10 | [基础报告](./external-knowledge-probe-2026-09-10-strict.json)与[扩展报告](./external-knowledge-probe-2026-09-10-extended.json):共 33 次只读请求,其中检索 17 次;HTTPS 显式开启证书校验,RAGFlow 为 HTTP |
|
||||
|
||||
历史结果包含 Dify 0.15.8 详情 405、检索零命中,Dify 1.17.0 详情成功及 3 条非空结果;
|
||||
FastGPT 三种模式和重排非空;RAGFlow 图谱开关产生不同结果,但 Knowledge Compilation
|
||||
没有配置证据。FastGPT/RAGFlow 产品版本、Dify 0.15.8 非空结构及真实错误矩阵仍未确认。
|
||||
逐项数量、脚本命令和当时测试结果由[实施进度](./progress.md#验证证据)维护。
|
||||
|
||||
2026-09-13 新增当前产品证据:[生产验证摘要](./external-knowledge-validation-2026-09-13.json)
|
||||
记录真实 Electron safeStorage 持久化重开、生产服务与本机 HTTP MCP,以及 Dify 桌面。
|
||||
服务结果与 MCP 引用数分别为 Dify 配置 1 的 0/0、Dify 配置 2 的 2/2、FastGPT 的 2/2、
|
||||
RAGFlow 的 1/1。报告配置编号不是版本识别,历史版本差异不据此重新认证。
|
||||
|
||||
### 9.1 Dify
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表 | `GET /v1/datasets` |
|
||||
| 检索 | `POST /v1/datasets/{dataset_id}/retrieve` |
|
||||
| 认证 | `Authorization: Bearer <knowledge-api-key>` |
|
||||
固定使用 `GET /v1/datasets`、`GET /v1/datasets/{id}` 和
|
||||
`POST /v1/datasets/{id}/retrieve`。默认省略整个 `retrieval_model`,远端优先使用知识库
|
||||
当时配置,未保存时由远端选默认;覆盖时发送完整的本地 `retrievalModel`,不逐字段合并。
|
||||
结果从 `records[].segment`、文档和分数取值。
|
||||
|
||||
Adapter 只接受 Knowledge Service API Key,不调用应用 `/info`、Chat 或 Workflow。默认不
|
||||
发送 `retrieval_model`,沿用远端知识库配置。用户选择覆盖时,Adapter 必须一次收集并验证
|
||||
当前接口要求的搜索方式、Top K、阈值、`reranking_mode`,以及相应的 Rerank Provider/模型
|
||||
或关键词/向量权重与 Embedding Provider/模型;缺少任何必填标识时不构造请求。响应从
|
||||
`records[].segment`、`score`、文档名称和元数据提取标准化引用。
|
||||
客户端把详情 405 分类为不兼容,绑定表单允许详情失败后继续检索验证,不实现版本探测或
|
||||
自动遍历目录的详情回退。Scoped Key 可尝试手工 ID,但当前产品的真实受限密钥验收仍待完成。
|
||||
Metadata、多模态、摘要索引、子块、附件和 Pipeline 没有进入当前简化目录或结果映射。
|
||||
|
||||
Dify 当前文档限制查询不超过 250 字符。GoodBuddy 不静默截断;超出时检索测试和聊天
|
||||
诊断返回 Provider 限制,并允许后续单独设计查询压缩策略。
|
||||
官方来源:[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)。
|
||||
|
||||
### 9.2 FastGPT
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表 | `POST /api/core/dataset/list` |
|
||||
| 详情 | `GET /api/core/dataset/detail?id=...` |
|
||||
| 检索 | `POST /api/core/dataset/searchTest` |
|
||||
| 认证 | `Authorization: Bearer <api-key>` |
|
||||
固定使用 `POST /api/core/dataset/list`、`GET /api/core/dataset/detail?id=...` 和
|
||||
`POST /api/core/dataset/searchTest`。列表 `data[]` 保留 Dataset 与文件夹,按 `parentId`
|
||||
读取层级,在当前父级响应内搜索和分页;文件夹只用于浏览。
|
||||
|
||||
列表响应同时包含文件夹和知识库,只有 `type = dataset` 可以绑定。检索映射 `datasetId`、
|
||||
`text`、Token `limit`、`similarity`、`searchMode` 和 `usingReRank`。初始版本不发送查询优化
|
||||
字段,避免调用 FastGPT 配置的 LLM。
|
||||
响应从 `q`、`a`、`sourceName`、`sourceId`、`collectionId` 和 `score` 构造片段与引用。
|
||||
检索接受业务码 200 的 `data.list[]` 与旧手工文档的 `data[]`,不递归猜测嵌套结构。
|
||||
`q` 必需,`a` 仅在存在时追加;评分数组保留类型、值和可选 index,数值评分独立处理。
|
||||
当前客户端已通过 FastGPT 真实服务与本机 HTTP MCP,均返回 2 条。报告确认结果契约与
|
||||
引用映射有效,没有逐项归档评分包装,故旧包装和全部评分变体仍以定向测试证据为限。
|
||||
|
||||
FastGPT 4.15.0 起部署实例的 `/apidoc/devapi` 是接口事实来源,手工文档可能落后。支持矩阵
|
||||
必须按真实版本测试;不能因为健康页可访问就假定列表和检索 Schema 相同。`searchTest`
|
||||
可能记录 `SEARCH_TEST` 等远端审计并更新 API Key 用量;Embedding 和 Rerank 也可能产生
|
||||
费用。GoodBuddy 将这些行为作为检索成本显示,不宣称该接口没有副作用。
|
||||
请求映射 `datasetId/text/limit/similarity/searchMode/usingReRank`,不发送查询扩展。
|
||||
Embedding、Rerank 和远端 API 审计仍可能产生用量;不调用 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)。
|
||||
4.15.0 起应核对部署实例生成文档;历史检查只取得云端 `/apidoc/` 应用壳。
|
||||
|
||||
### 9.3 RAGFlow
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表/详情 | `GET /api/v1/datasets` |
|
||||
| 检索 | `POST /api/v1/retrieval` |
|
||||
| 认证 | `Authorization: Bearer <api-key>` |
|
||||
目录和按 ID 详情均使用 `GET /api/v1/datasets`,检索使用 `POST /api/v1/retrieval`。
|
||||
请求发送 `question`、单个 `dataset_ids`、`page: 1`、`page_size: 20`、阈值、向量权重、
|
||||
`knn_top_k` 及显式的图谱、知识编译布尔值。HTTP 成功且业务码为 0 才读取 `data.chunks`。
|
||||
文档名映射 `document_keyword`,位置保存返回的 `positions`,分数取原始 `similarity`。
|
||||
|
||||
检索映射 `question`、`dataset_ids`、`page_size`、`similarity_threshold`、
|
||||
`vector_similarity_weight`、`knn_top_k`、重排、`use_kg` 和
|
||||
`include_knowledge_compilation`。不发送已弃用的 `top_k`。响应从 `data.chunks` 和
|
||||
`data.doc_aggs` 提取片段、文档、位置和原始相似度。
|
||||
`graphEnabled` 根据有效非零 `graphrag_task_finish_at` 判断;仅配置
|
||||
`parser_config.graphrag.use_graphrag` 不算完成。知识编译依据非空
|
||||
`compilation_template_group_id`。启用任一能力时,服务在检索前重新读取详情确认。
|
||||
当前不保存完整能力快照,不区分图谱未配置与构建中,也不检测 Provider 版本。
|
||||
|
||||
图谱规则:
|
||||
官方来源:[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)。
|
||||
旧文档只声明 `top_k`,当前客户端发送 `knn_top_k`;请求成功不证明候选数参数生效,
|
||||
也不是旧部署版本兼容承诺。
|
||||
|
||||
- `parser_config.graphrag` 或等价详情字段作为已有图谱的证据,不写回。
|
||||
- `use_kg` 是检索时开关,只在已有图谱和受支持版本均确认后发送。
|
||||
- Knowledge Compilation 与旧 GraphRAG 分别建模。
|
||||
- `include_knowledge_compilation` 使用本地保存的显式布尔值,初始默认 false;即使服务端
|
||||
默认 true,也不依赖字段省略获得隐式行为。
|
||||
- Provider 没有返回图节点或路径证据时,GoodBuddy 不生成图谱引用。
|
||||
## 10. 验证与验收
|
||||
|
||||
## 10. 检索编排
|
||||
三家 Provider 已通过生产 `KnowledgeService` 与本机 HTTP MCP,真实 Electron
|
||||
safeStorage 加解密和持久化重开成功。Dify 桌面已完成绑定、参数更新、重启、三档窗口宽度
|
||||
及历史引用检查。验证方法、原始报告边界和调用计数见[实施进度](./progress.md)及
|
||||
[脱敏摘要](./external-knowledge-validation-2026-09-13.json)。本轮文档只读取报告,没有
|
||||
另发 Provider 或模型请求,也未改动历史探测 JSON。
|
||||
|
||||
```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
|
||||
```
|
||||
当前 Portal 与控件修改后的聚焦测试、全量回归、类型检查、lint、工作区及暂存区 diff
|
||||
检查均已通过。精确计数、运行命令、耗时、执行期间文件稳定性和原失败修正记录统一见
|
||||
[实施进度](./progress.md#2026-09-13-当前工作区实现与分组验证)。
|
||||
|
||||
预检索和 `knowledge_search` 工具必须调用同一个 `retrieveMany`。如果只修改聊天预检索而
|
||||
未修改 `KnowledgeMcpGateway`,`auto` 和 `always` 会产生不同能力,这是阻断发布的问题。
|
||||
后续真实生成使用已保存的默认模型,凭据在 Main 通过 safeStorage 解密。三家 Provider
|
||||
的 `always` 及 Dify `auto` 已产生有依据和引用的短答案;Dify `auto` 经进程内
|
||||
`ModelToolProvider → KnowledgeMcpGateway` 执行,不能当成本机 HTTP MCP 或远程 ACP
|
||||
验收。FastGPT/RAGFlow 首轮因直接比较序列化字符串产生验证断言失败,解码请求和嵌入
|
||||
证据 JSON 后重跑通过;报告保留初始结果。完整 App 输入区的发送、真实答案及引用点击
|
||||
仅覆盖 Dify。Dify/FastGPT 使用严格 HTTPS,RAGFlow 和模型使用已配置的 HTTP。
|
||||
|
||||
### 10.1 上下文预算
|
||||
请求精确增量与视觉 fixture 边界见[后续验证摘要](./external-knowledge-follow-up-2026-09-13.json)
|
||||
及[实施进度](./progress.md#2026-09-13-ui-修正与真实生成复验)。不公开模型端点、私有配置
|
||||
标识、凭据和答案正文。这些短答案检查没有评估广泛问答质量。
|
||||
|
||||
- 每个知识库先保留不超过其 `resultLimit` 的结果。
|
||||
- 先给每个成功知识库一个最小有界配额,再按稳定顺序分配剩余字符。
|
||||
- 不用不同 Provider 原始分数决定跨库优先级。
|
||||
- 片段截断在 Main 完成并在引用中标记。
|
||||
- 相同实例的多个绑定仍按绑定分别请求,除非 Provider 明确支持且 Adapter 实现等价的多
|
||||
Dataset 请求;合并不得改变每个绑定的配置和诊断。
|
||||
FastGPT/RAGFlow 完整桌面专属操作、复杂或混合问答、删除和真实错误矩阵仍需补验。
|
||||
Linux Host 的两条网络路径可达,但验证程序无法解密已有 SSH 凭据,在认证前返回
|
||||
`E_SAFE_STORAGE_DECRYPT_FAILED`,真实远程 forced-preflight 未执行。本机 HTTP MCP
|
||||
成功不覆盖该远程路径。剩余 Provider 版本和高级参数效果也不能由基础检索成功推断。
|
||||
|
||||
### 10.2 失败结果
|
||||
原设计的分项探测 UI、最低版本矩阵、详细诊断、地址确认和取消交互未全部落地。它们是
|
||||
待核对的产品差异,不能以补建 Adapter Registry 或版本框架代替验收决策。
|
||||
|
||||
返回结构包含每个知识库的:
|
||||
`scripts/external-knowledge-probe.mjs` 仍可只读探测列表、详情、检索;`--extended` 增加
|
||||
Dify 配置覆盖、FastGPT 重排和 RAGFlow 两种开关。报告区分成功零命中与失败,不输出
|
||||
凭据或业务正文。脚本不打包到桌面或 Agent,脚本执行本身不覆盖 UI、IPC、存储和引用链路。
|
||||
|
||||
```ts
|
||||
interface KnowledgeRetrievalOutcome {
|
||||
knowledgeBaseId: string
|
||||
status: 'success' | 'empty' | 'failed' | 'cancelled'
|
||||
durationMs: number
|
||||
resultCount: number
|
||||
errorCode?: ExternalKnowledgeErrorCode
|
||||
truncated: boolean
|
||||
}
|
||||
```
|
||||
## 11. 需求追踪
|
||||
|
||||
至少一个 `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 或公开文档通过不能代替真实实例验收。
|
||||
|
||||
## 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、固定端点、传输风险确认 | 地址、证书和凭据泄漏测试 |
|
||||
| `EK-FR-01` 至 `EK-FR-05` | 实例 Service/Store、目录 Client、IPC 与管理表单 | 服务已验证;补充 FastGPT/RAGFlow 桌面和受限目录手工 ID |
|
||||
| `EK-FR-06` 至 `EK-FR-10` | 共享 Provider 配置、绑定事务、统一列表与两栏表单 | Dify 参数与布局已验证;其他 Provider 专属参数及未提供字段待核对 |
|
||||
| `EK-FR-11` 至 `EK-FR-14` | `retrieveMany`、预检索、MCP、共享引用与 failure 字段 | 三家 IPC 短答案与 Dify 完整输入区引用已验证;混合失败、广泛问答与远程待补 |
|
||||
| `EK-FR-15` | 唯一约束、外键保护、顺序本地删除 | 中途失败刷新、远端内容不变 |
|
||||
| `EK-FR-16` | Main 固定端点、有界请求与数据说明 | 当前桌面传输行为与 PRD 差异 |
|
||||
|
||||
@@ -4,13 +4,96 @@
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计稿 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 状态 | Portal 与控件已修正,Dify 完整 App 真实问答及当前自动回归已通过 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-13 |
|
||||
| 产品需求 | [外部知识库接入 PRD](./external-knowledge-prd.md) |
|
||||
| 行为规则 | [外部知识库逻辑设计](./external-knowledge-logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy UI 设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
## 当前实现与验收边界
|
||||
|
||||
`KnowledgeWorkspace.tsx` 已接入 `ExternalKnowledge.tsx`,不再处于只有界面草案的阶段。
|
||||
当前操作与控件如下;后续编号章节保留目标布局、文案和验收要求,其中超出本节的内容
|
||||
仍待核对,不能作为已实现或已通过桌面验收的描述。
|
||||
|
||||
| 区域 | 当前实现 |
|
||||
| --- | --- |
|
||||
| 页面入口与列表 | 标题区有“管理外部实例”和新建操作;本地和外部同一列表,按显示名、远端名、Provider、实例名搜索,原生 Select 筛选来源 |
|
||||
| 实例管理 | 560px 紧凑 Modal 内切换实例列表、编辑表单和删除确认;标题随状态变化,行内提供测试、编辑、启停和移除,显示地址 origin、凭据、探测状态、绑定数与本地化测试时间 |
|
||||
| 实例保存与测试 | 新建直接显示密码输入;编辑才提供 keep/replace/clear,清除需要确认。眼睛图标切换显隐并提供可访问名称和按下状态。保存后回列表测试目录,连接说明默认折叠;HTTP 地址下显示传输说明 |
|
||||
| 创建类型 | 共享 `SegmentedControl` 选择本地、Dify、FastGPT、RAGFlow;本地继续使用现有创建向导,外部挂载独立绑定表单 |
|
||||
| 来源选择 | 实例使用复用既有 picker 样式的富信息菜单,条目显示名称、地址 origin 与凭据状态;目录使用搜索框、目标按钮、刷新和分页,FastGPT 文件夹可进入与返回根目录;共享分段控件切换手工 ID |
|
||||
| 配置 | 常规窗口来源与参数两栏,宽度不超过 959px 时单栏;参数使用数字输入、原生 Select 与共享滑动 Switch 样式,没有 Slider 或自由 JSON 编辑器 |
|
||||
| 测试与保存 | 用户填写查询,测试成功后可保存,零命中允许添加;保存会再次发出检索请求。查询、目标或参数改变会使测试失效,更换现有绑定目标要求确认 |
|
||||
| 外部详情 | 共享 `PageTabs` 提供概览、检索配置和检索测试;概览显示远端 ID、推导状态和绑定验证时间,可刷新详情、用于对话、重新绑定、管理实例或移除 |
|
||||
| 参数编辑 | 复用绑定表单,保存按钮为“保存检索参数”;恢复默认只重置表单。配置页签访问后保持挂载,切换详情页签保留草稿 |
|
||||
| 检索结果 | 外部组件显示总耗时、返回数量、片段、定位、实际 ID 和原始评分,包括 FastGPT 评分数组;不显示本地全文、向量、图谱通道或本地上下文操作 |
|
||||
| 聊天与引用 | 统一选择器显示 Provider、实例与状态;外部引用打开保存片段,显示实例 ID 和远端定位,不请求本地上下文或打开原始来源 |
|
||||
|
||||
创建类型分段控件复用全局不收缩规则,不能被目录和参数表单压扁;列表长名称的宽度约束见
|
||||
[知识库 UI 设计](./ui-design.md#2-信息架构)。新建时自动名称跟随目录选择,手动改名和已保存
|
||||
名称不被覆盖,具体规则见[逻辑设计](./external-knowledge-logic-design.md#5-创建与编辑)。
|
||||
实例 Modal 和其他浮层统一参与[原生浏览器遮挡处理](../assistant-workbar/browser-tabs-technical-design.md#4-renderer-与-viewport),
|
||||
不再仅验证没有浏览器的独立 Portal。
|
||||
|
||||
原 `knowledge-field` 的控件样式依赖知识库工作区祖先,实例 Modal 经 `createPortal` 挂到
|
||||
body 后丢失该作用域。当前 `Field` 使用全局 `.field` 和标签 span,密码也复用同一字段
|
||||
结构,确保 Portal 内的字体、输入边框、背景和焦点样式与应用一致。弹窗宽度为
|
||||
`min(560px, 100%)`,内容可滚动;操作区粘附底部,范围徽标靠左、按钮靠右,滚动留白
|
||||
避免键盘焦点被底部操作区遮住。通用参数、Provider 参数和认证使用 fieldset/legend 分组。
|
||||
|
||||
Switch 按标签文字在左、滑动开关在右排列,保持 `role="switch"`。实例菜单支持方向键
|
||||
打开与移动、Home/End、跳过禁用项、Enter 选择和 Escape 返回触发按钮;目录空状态使用
|
||||
共享 `EmptyState`,无搜索结果可清空查询,无目录可切换手工 ID。已选目标悬停不覆盖
|
||||
选中背景,保存和测试分别显示对应忙碌文案。
|
||||
|
||||
保存、删除等成功信息及实例管理中的异步错误使用应用通知;绑定测试、详情刷新和删除
|
||||
中途失败保留就地恢复所需的错误。实例有绑定时,确认后依次移除本地绑定再删实例;中途
|
||||
失败刷新已完成的移除,远端内容不变。
|
||||
|
||||
当前已实现过期响应丢弃、Modal 焦点管理、`PageTabs`、标记了 `role="switch"` 的开关和
|
||||
窄窗口 CSS。2026-09-13 Dify 已在当前生产 BrowserWindow、Main、Preload、Renderer 和
|
||||
IPC 上完成实例保存、目录选择、检索后绑定、参数更新、重启及历史引用检查,详见
|
||||
[脱敏验证摘要](./external-knowledge-validation-2026-09-13.json)。验证程序仅在 Main 的实例
|
||||
保存 Handler 边界替入已有密钥,未将真实密钥放入 Renderer。
|
||||
|
||||
1440、900、700 窗口宽度对应报告 viewport 1439、899、699,均无 body 横向溢出。
|
||||
宽屏配置两栏、窄屏单栏,窄屏列表可见;原生 ArrowRight 可以激活检索页签。已保存并
|
||||
重开的参数为返回 2 条、超时 15 秒、单片段 200 字符。生产搜索返回两条引用,验证程序
|
||||
保存带测试标记的会话后重载,引用弹窗显示 Provider、片段和快照说明,焦点在弹窗内,
|
||||
没有本地上下文或打开来源操作。此检查没有生成模型答案。
|
||||
|
||||
上述早期检查没有生成答案。后续完整 App 已从实际输入区选择 Dify 知识库并点击发送,
|
||||
得到已保存的真实模型短答案,再点击引用核对片段及远端文档/片段 ID;没有伪造消息或
|
||||
引用。浅深主题均检查了真实 Portal 编辑器的共享字体、背景、560px 宽度与无横向溢出。
|
||||
|
||||
另有真实 Electron 视觉 fixture 覆盖浅深主题、四档尺寸及原生键盘操作;其 IPC 是假数据,
|
||||
只用于界面、焦点和布局检查。三家生成的生产 IPC 验证中,引用展示使用隔离 DOM;只有
|
||||
Dify 完成了完整 App 的发送与点击引用。计数与方法边界见
|
||||
[实施进度](./progress.md#2026-09-13-ui-修正与真实生成复验)和
|
||||
[后续摘要](./external-knowledge-follow-up-2026-09-13.json)。当前 UI 修正后的聚焦测试、
|
||||
全量回归、类型检查、lint 与工作区/暂存区 diff 检查均已通过。App 测试已按实际可访问
|
||||
富信息菜单交互替换旧 Select 操作,精确计数由实施进度维护。FastGPT/RAGFlow 完整桌面专属操作和
|
||||
广泛问答质量仍待验收。
|
||||
|
||||
与下文目标设计相比,当前还存在以下边界:
|
||||
|
||||
- 没有保存前按地址传输风险确认,也没有接入独立证书例外设置;只有传输说明。
|
||||
- 没有目录或检索的 IPC 取消入口;丢弃旧响应不等于中止网络请求。测试期间绑定表单整体
|
||||
禁用,不能把其“取消”按钮描述为正在进行的请求可随时取消。
|
||||
- Dify 版本、Metadata、摘要、多模态和 Pipeline 状态没有进入当前界面;RAGFlow 只接收
|
||||
图谱完成与知识编译可用布尔证据,没有三态构建展示或版本支持矩阵。
|
||||
- 列表和概览没有持久化的最近成功检索时间;概览显示 `lastVerifiedAt`。当前也没有自动
|
||||
`remote-missing/config-invalid` 状态、完整裁剪诊断或历史引用“连接已移除”标签。
|
||||
- 没有覆盖所有关闭、类型切换和手工模式切换的未保存草稿离开确认;实例列表使用可换行
|
||||
列表而非表格,未单设 720px 表格转换规则。
|
||||
|
||||
## 目标设计与待验收要求
|
||||
|
||||
以下保留既有产品布局和交互目标。当前事实以上节为准;历史 Provider 探测用于解释能力
|
||||
边界,不作为下述界面已经呈现或已通过产品验证的证据。
|
||||
|
||||
## 1. 信息架构
|
||||
|
||||
外部知识库不增加左侧主导航和知识库一级页签。现有知识库页面结构保持:
|
||||
@@ -82,7 +165,7 @@ RAGFlow
|
||||
|
||||
## 4. 管理外部实例
|
||||
|
||||
点击“管理外部实例”打开大尺寸 Modal。Modal 使用标题、可滚动内容和固定操作区,遵循
|
||||
点击“管理外部实例”打开最大 560px 的紧凑 Modal。Modal 使用标题、可滚动内容和粘附底部操作区,遵循
|
||||
全局焦点陷阱、Escape、背景隔离和焦点恢复规则。
|
||||
|
||||
```text
|
||||
@@ -237,6 +320,14 @@ Rerank 用量。`
|
||||
右侧配置固定分为“通用配置”和“{Provider} 配置”,不创建嵌套卡片。高级字段可以使用
|
||||
一个折叠区,但核心检索方式、阈值和 RAGFlow 图谱检索保持直接可见。
|
||||
|
||||
第一版遵循 [PRD 产品决策](./external-knowledge-prd.md#2-产品决策)的只读范围。面板固定说明:
|
||||
`参数仅保存在 GoodBuddy,用于此绑定的后续检索,不会修改远端知识库设置。`
|
||||
编辑页保存按钮使用“保存检索参数”;恢复默认仅重置当前表单,保存后作用于当前绑定。
|
||||
远端索引、模型、图谱构建和 Pipeline 配置只展示状态,不提供编辑或执行操作。
|
||||
|
||||
Dify 的“使用知识库默认配置”开启时说明:`每次检索沿用远端当前设置,远端设置变化会影响结果。`
|
||||
关闭后,客户端显式检索参数随请求发送,不写回远端默认配置。
|
||||
|
||||
控件选择:
|
||||
|
||||
- 开关:图谱检索、Score 阈值启用、Rerank、使用 Dify 知识库默认配置。
|
||||
@@ -258,6 +349,33 @@ Rerank 用量。`
|
||||
图谱状态”并禁用。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 当前实例 | 基础检索;具备完成证据时启用图谱检索 | 没有独立配置证据时禁用 Knowledge Compilation;Metadata 条件仍隐藏 | 相似度、向量权重和候选数请求已被接受,但未单独验证参数效果;候选数参数需按部署版本适配,详见技术设计;`graphrag_task_finish_at` 有效才启用图谱;`compilation_template_group_id` 有效才启用 Knowledge Compilation |
|
||||
|
||||
RAGFlow 列表项应区分三个图谱状态:“未配置”“构建中或未完成”“可用于图谱检索”。不能
|
||||
仅因 `parser_config.graphrag.use_graphrag = true` 就显示为可用。实测数量见
|
||||
[实施进度](./progress.md#验证证据),界面按当前远端知识库的配置和完成状态判断。
|
||||
|
||||
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. 外部知识库详情
|
||||
|
||||
选中外部绑定后,现有详情区按来源类型裁剪:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"date": "2026-09-13",
|
||||
"kind": "sanitized-validation-summary",
|
||||
"scopeNote": "Historical evidence before subsequent portal/control fixes and real-generation follow-up; not final regression evidence for the newer UI",
|
||||
"followUpEvidence": "external-knowledge-follow-up-2026-09-13.json",
|
||||
"sources": [
|
||||
"kb-strict-validation/report.json",
|
||||
"kb-desktop-ui-validation-20260913/report.json",
|
||||
"goodbuddy-native-controls-fix-full.log",
|
||||
"goodbuddy-native-controls-fix-full.result.json"
|
||||
],
|
||||
"automatedChecks": {
|
||||
"status": "passed-before-subsequent-ui-follow-up",
|
||||
"authoritativeCounts": "progress.md",
|
||||
"fullApp": { "status": "passed", "evidence": "owner handoff" },
|
||||
"fullSuite": {
|
||||
"command": "npm test -- --maxWorkers=1 --reporter=verbose",
|
||||
"status": "passed",
|
||||
"exitCode": 0,
|
||||
"evidence": ["goodbuddy-native-controls-fix-full.log", "goodbuddy-native-controls-fix-full.result.json"]
|
||||
},
|
||||
"typecheck": { "command": "npm run typecheck", "status": "passed", "evidence": "owner handoff after final code edits" },
|
||||
"lint": { "command": "npm run lint", "status": "passed", "evidence": "owner handoff after final code edits" },
|
||||
"diffCheck": { "status": "passed", "evidence": "owner handoff after final code edits" },
|
||||
"sourceStability": {
|
||||
"files": ["src/renderer/src/App.tsx", "src/renderer/src/App.test.tsx"],
|
||||
"hashesStableThroughoutFullRun": true,
|
||||
"evidence": "owner handoff"
|
||||
},
|
||||
"priorFailures": "resolved; exact intermediate and final counts are maintained only in progress.md"
|
||||
},
|
||||
"strictService": {
|
||||
"boundary": "Production KnowledgeService/client and local HTTP MCP in real Electron; no interactive renderer or authenticated Linux execution",
|
||||
"electronVersion": "43.2.0",
|
||||
"safeStorageAvailable": true,
|
||||
"safeStorageRoundTrip": true,
|
||||
"retrievalHttpAttempts": 12,
|
||||
"retrievalHttpResponses": 12,
|
||||
"profiles": [
|
||||
{ "provider": "dify", "profile": 1, "scheme": "https:", "strictTls": true, "catalogPassed": true, "detailHttpStatus": 405, "bindingPassed": true, "persistedCredentialReopened": true, "responseSchemaValid": true, "retrievalResults": 0, "mcpToolSuccess": true, "mcpReferences": 0, "citationMappingValid": true, "retrievalHttpAttempts": 3 },
|
||||
{ "provider": "dify", "profile": 2, "scheme": "https:", "strictTls": true, "catalogPassed": true, "detailHttpStatus": 200, "bindingPassed": true, "persistedCredentialReopened": true, "responseSchemaValid": true, "retrievalResults": 2, "mcpToolSuccess": true, "mcpReferences": 2, "citationMappingValid": true, "retrievalHttpAttempts": 3 },
|
||||
{ "provider": "fastgpt", "profile": 1, "scheme": "https:", "strictTls": true, "catalogPassed": true, "detailHttpStatus": 200, "bindingPassed": true, "persistedCredentialReopened": true, "responseSchemaValid": true, "retrievalResults": 2, "mcpToolSuccess": true, "mcpReferences": 2, "citationMappingValid": true, "retrievalHttpAttempts": 3 },
|
||||
{ "provider": "ragflow", "profile": 1, "scheme": "http:", "strictTls": null, "catalogPassed": true, "detailHttpStatus": 200, "bindingPassed": true, "persistedCredentialReopened": true, "responseSchemaValid": true, "retrievalResults": 1, "mcpToolSuccess": true, "mcpReferences": 1, "citationMappingValid": true, "retrievalHttpAttempts": 3 }
|
||||
]
|
||||
},
|
||||
"desktop": {
|
||||
"provider": "dify",
|
||||
"phase": "completed",
|
||||
"boundary": "Production electron-vite Main, preload, BrowserWindow, renderer and IPC; existing provider key substituted in Main at instance-save handler boundary",
|
||||
"callsByKind": { "catalog": 4, "detail": 4, "retrieval": 6 },
|
||||
"totalReadOnlyCalls": 14,
|
||||
"allHttpStatus200": true,
|
||||
"checks": [
|
||||
"Instance saved through production IPC",
|
||||
"Catalog selected and binding saved after live retrieval",
|
||||
"Persisted binding reopened in isolated desktop profile",
|
||||
"Local parameters saved through production binding update",
|
||||
"Native ArrowRight moved focus and activated retrieval tab",
|
||||
"Live search references persisted and reopened in production citation dialog"
|
||||
],
|
||||
"measuredViewportWidths": [1439, 899, 699],
|
||||
"bodyOverflowAtMeasuredWidths": [0, 0, 0],
|
||||
"configColumnsAtMeasuredWidths": { "1439": 2, "699": 1 },
|
||||
"mobileListVisible": true,
|
||||
"savedCommonConfig": { "resultLimit": 2, "requestTimeoutMs": 15000, "maxSnippetCharacters": 200 },
|
||||
"resultCount": 2,
|
||||
"localContextButtons": false,
|
||||
"window": { "sandbox": true, "contextIsolation": true, "nodeIntegration": false },
|
||||
"encryptedCredential": { "scheme": "electron-safe-storage", "decryptMatches": true, "plaintextAbsent": true },
|
||||
"citationPersisted": { "count": 1, "referenceCount": 2 },
|
||||
"citationDialog": { "snapshot": true, "provider": true, "snippet": true, "openSource": false, "focusInside": true },
|
||||
"citationMethod": "Production knowledge.search returned live references; harness saved a labeled test transcript through conversations.saveLocal and opened it after reload. No model answer generated",
|
||||
"isolatedProfileAndBuildRemoved": true
|
||||
},
|
||||
"remote": {
|
||||
"lanTcpConnected": true,
|
||||
"vpnTcpConnected": true,
|
||||
"savedCredentialSource": "unreadable",
|
||||
"hostKeyState": "verified",
|
||||
"blocker": "E_SAFE_STORAGE_DECRYPT_FAILED",
|
||||
"forcedPreflightLive": "blocked-before-authenticated-ssh",
|
||||
"authenticatedSshAttempts": 0
|
||||
},
|
||||
"sessionAccounting": {
|
||||
"source": "Strict report historical bounds plus desktop report; confirmed by owner handoff",
|
||||
"unsurfacedFirstPassRetrievalMinimum": 0,
|
||||
"unsurfacedFirstPassRetrievalMaximum": 12,
|
||||
"earlierConfirmedRetrievalCalls": 12,
|
||||
"strictRetrievalCalls": 12,
|
||||
"desktopRetrievalCalls": 6,
|
||||
"totalRetrievalMinimum": 30,
|
||||
"totalRetrievalMaximum": 42,
|
||||
"generatedModelAnswers": 0,
|
||||
"limitation": "First pass retained no diagnostics, so its exact retrieval count cannot be reconstructed. Provider-side embedding, reranking and audit usage is not counted as generated answers"
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ GoodBuddy 当前已经具备:
|
||||
引用和脱敏诊断。
|
||||
- Agent Runtime 的选择或切换不改变当前向量连接;向量连接的选择或切换也不改变当前
|
||||
Agent Runtime。
|
||||
- 未来记忆、笔记或其他本地语义检索若要复用向量连接,必须经过独立产品设计、权限与
|
||||
- 分区记忆、笔记或其他本地语义检索若要复用向量连接,必须经过独立产品设计、权限与
|
||||
数据路径评审后显式接入,不能因存在全局向量连接而自动获得使用权。
|
||||
|
||||
---
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 实施中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-08-22 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-05 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 实施范围 | 第一阶段:可用、可见、可诊断;第二阶段:可调、可优化、可维护 |
|
||||
| 相关设计 | [本地文本向量模型与连接设计](./local-embedding-technical-design.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
本文负责本地检索与分块增强。当前工作区另已接入外部实例、绑定和统一检索,其产品范围见
|
||||
[外部知识库 PRD](./external-knowledge-prd.md),实现与待验收状态见[实施进度](./progress.md)。
|
||||
外部绑定不进入下述本地分块、向量索引和图谱维护流程。
|
||||
|
||||
GoodBuddy 已具备本地多知识库、文件与目录同步、网页导入、SQLite FTS5、
|
||||
OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来源引用。现有实现
|
||||
优先建立了本地数据主权、安全边界和跨 Runtime 工具授权,但用户仍难以稳定
|
||||
@@ -37,7 +41,10 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
2. 保持 Electron Main、Preload、Renderer 的安全边界,Renderer 不直接读取
|
||||
数据库、原文件或向量。
|
||||
3. 保留“模型按需检索”,并新增“每次先检索”模式。后者必须由 Main 进程
|
||||
预检索,不能只依赖提示词要求模型调用工具。
|
||||
预检索,不能只依赖提示词要求模型调用工具。
|
||||
`knowledge_search` 的 `limit` 与现有检索引擎对齐,接受 1–100,省略时返回最多 6 条;
|
||||
调用方可请求超过 8 条。授权范围、检索排序和工具结果字节预算继续生效,因此实际
|
||||
条数可能少于请求值。
|
||||
4. 知识库新建后不默认启用全部已有知识库;对话中的范围继续由用户显式选择。
|
||||
5. 向量服务不可用时保留全文与中文检索,但必须返回明确降级状态。这是可见的检索通道
|
||||
降级,不得自动切换内置连接、用户添加的 OpenAI 兼容连接或其他向量模型。
|
||||
@@ -149,7 +156,7 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
|
||||
## 6. 信息架构
|
||||
|
||||
知识工作区继续使用主从布局和现有四个页签:
|
||||
知识工作区使用主从布局和四个面向用户任务的页签:
|
||||
|
||||
```text
|
||||
知识库
|
||||
@@ -160,14 +167,21 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
│ └─ 分块查看与维护
|
||||
├─ 知识图谱
|
||||
├─ 任务中心
|
||||
└─ 设置
|
||||
└─ 高级设置
|
||||
├─ 问答效果入口
|
||||
├─ 向量索引
|
||||
├─ 检索设置
|
||||
├─ 分块设置
|
||||
└─ 图谱设置
|
||||
```
|
||||
|
||||
“检索测试”是当前知识库的高频诊断操作,通过知识库标题区次操作打开独立
|
||||
工作台,不新增第五个一级页签。
|
||||
“检索测试”是当前知识库的高频验证操作,通过知识库标题区和“高级设置 →
|
||||
问答效果”打开同一个独立工作台,不新增第五个一级页签。工作台首次打开只突出真实
|
||||
问题输入和测试操作;候选倍数、权重、阈值、重排和上下文预算默认折叠。
|
||||
|
||||
首次使用流程固定为“创建知识库 → 添加资料 → 等待可检索 → 用于当前对话”。创建表单
|
||||
默认只展示名称和描述;存储方式与知识图谱属于折叠的高级设置。没有知识库时,唯一主操作
|
||||
位于首次使用空状态中。
|
||||
|
||||
全局向量模型仍在“设置 → 模型连接 → 向量模型”中配置。GoodBuddy 内置连接和用户添加的
|
||||
OpenAI 兼容连接使用同一连接列表;其界面、数据路径及切换语义以
|
||||
@@ -195,6 +209,10 @@ Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不
|
||||
2. 检索方式:模型按需检索、每次先检索。
|
||||
3. 当前范围为空、索引降级或向量未配置时的短说明。
|
||||
|
||||
知识库 ID 选择按 Conversation 持久化。新对话和未保存过知识范围的旧对话默认不选择
|
||||
任何知识库;切换对话恢复各自范围,不得在应用启动时自动勾选全部知识库。新建知识库只
|
||||
加入当前 Conversation,不重新启用用户已取消的其他知识库。
|
||||
|
||||
## 7. 第一阶段:可用、可见、可诊断
|
||||
|
||||
### 7.1 检索方式
|
||||
@@ -296,6 +314,14 @@ Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不
|
||||
- 失败文档数。
|
||||
- 当前向量模型与索引是否兼容。
|
||||
|
||||
`indexedDocumentCount` 只统计状态为 `ready` 的文档;`queued`、`parsing` 和
|
||||
`indexing` 计入处理中,`failed` 单独计入需处理。文档行持续显示全文、向量和图谱通道
|
||||
状态。已有可用分块的失败文档仍允许查看分块,并提供重新处理。
|
||||
|
||||
文档行可通过受控 `knowledgeBaseId + documentId` 打开数据库中保存的原始来源。
|
||||
Renderer 不得提交任意路径。移除来源必须先说明将删除的文档、索引、图谱证据及是否包含
|
||||
应用托管副本,用户确认后才执行。
|
||||
|
||||
降级事件包括:
|
||||
|
||||
- 未配置向量模型。
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
# 知识库实施进度
|
||||
|
||||
## 已验证实现
|
||||
|
||||
截至 2026-09-05 的本地知识库验证记录如下。外部知识库当前实现和验证范围见后文
|
||||
2026-09-13 记录。
|
||||
|
||||
- 知识库选择按 Conversation 持久化,新对话默认空范围。
|
||||
- 创建流程默认只展示名称和描述,高级存储与图谱设置折叠。
|
||||
- 知识库汇总区分可检索、处理中和需处理文档。
|
||||
- 文档行展示全文、向量和图谱状态,并支持打开原始来源和重新处理失败文档。
|
||||
- 移除来源需要确认并说明级联影响。
|
||||
- 检索高级参数默认折叠,“高级设置”提供问答效果入口。
|
||||
- 向量未配置状态可直接前往模型设置。
|
||||
- 有效 DOCX 与多页 PDF 已通过磁盘文件导入、解析、分块和检索生产链路。
|
||||
|
||||
## 验证证据
|
||||
|
||||
### 2026-09-13 知识库布局与自动名称修复
|
||||
|
||||
- 创建类型的共享分段控件被长表单压缩,修复共享不收缩规则,不添加页面专属高度。
|
||||
长名称卡片增加零最小宽度的 Grid 轨道,名称单行省略、图标保持 15px;详情名称允许换行。
|
||||
合成长名称实测列表横向溢出为 0;详情修复前溢出 658px,修复后为 0。
|
||||
- 连续选择两个目录项时,未修改过的自动名称原先停留在第一个目标。回归先失败再修复,
|
||||
现在跟随选择,手动名称和已保存名称不自动覆盖。规则见
|
||||
[创建与编辑](./external-knowledge-logic-design.md#5-创建与编辑)。
|
||||
- 当前完整 App 的真实 UI → Preload → Main → SQLite 路径完成实例保存、两个目录项
|
||||
切换、零结果检索后添加绑定、详情展示和配置草稿保留。上游是 loopback Dify 合成服务,
|
||||
不是真实知识平台;没有调用模型,也不能据此声称真实检索质量或远端验收。
|
||||
- 本轮合成服务共收到 8 次请求:1 次初始目录、2 次初始详情、2 次检索(测试与保存)、
|
||||
2 次后续详情、1 次后续目录;真实模型和真实知识服务请求均为 0。
|
||||
- 创建类型在浅深主题、目标 1280×800、960×720、720×640、640×420 的八组尺寸中
|
||||
检查控件高度至少 34px,并保存 PNG。实际审阅浅色 1280、深色 640 及最终添加后的
|
||||
长名称详情 PNG,确认 Tab 不再成为窄缝,卡片边框和图标完整,详情无横向滚动条。
|
||||
- 同期修复和通用验证结果见[工作栏进度](../assistant-workbar/progress.md#2026-09-13-原生浏览器与应用浮层修复)。
|
||||
临时脚本与合成 profile 在提交后清理,不把它们作为长期依赖;用户在另一测试窗口
|
||||
手工创建的实例与绑定不属于本轮合成数据,不随清理删除。
|
||||
|
||||
### 2026-09-13 检索路径与绑定查询简化
|
||||
|
||||
对 `3b560c9..ca4a58c` 的复查发现三处与本功能相关的实现问题,已按 KISS 与性能优先修正:
|
||||
|
||||
| 修正项 | 修正前行为 | 修正后行为 |
|
||||
| --- | --- | --- |
|
||||
| 生产检索入口 | 对话检索复用 `testRetrieval`,把用户问题传入 `testQuery`,并对 RAGFlow 额外发一次 detail 请求 | 新增 `ExternalKnowledgeService.retrieve` 作为生产路径,只发一次检索请求;能力预检仅保留在 `testRetrieval` |
|
||||
| 配置变更检测 | 5 处 `JSON.stringify` 前后快照对比,抛出 `EXTERNAL_KB_CONFIG_CHANGED` | 全部删除,改由既有 `AbortController` 取消覆盖;被取消的探测抛 `EXTERNAL_KB_CANCELLED` 且不写回探测结果 |
|
||||
| 绑定查询 | `requireLibrary`(30+ 调用点,其中一处在循环内)与 `getKnowledgeSnapshot` 每次全表 `listBindings()` | 新增 `getBinding`/`hasBinding`/`getBindingsForInstance` 主键查询;快照改为单次 `Map` 构建 |
|
||||
|
||||
修正过程中被测试拦下两次行为回退,已记录为当前不变量:
|
||||
|
||||
- 被取消的连接测试必须抛出且不写回 `probeStatus`,不能记为 `failed`;用户停用实例不等于探测失败。
|
||||
- `testRetrieval` 的 RAGFlow 能力预检必须与检索共用绑定配置的 `requestTimeoutMs`,不能各自计时。
|
||||
|
||||
同时删除确认无生产引用的死代码:`probeStatus` 的 `'healthy'` 取值、`external.transportConfirm`
|
||||
与 `external.states` 的 `remote-missing`/`config-invalid` 文案、`KnowledgeService.searchHybridMany`、
|
||||
`ExternalKnowledgeRemoteResult.sourceUrl`。`externalKnowledgeLocatorSchema` 的 `sourceUrl`
|
||||
保留,因为它解析已持久化的引用数据。
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| `npx vitest run src/main/knowledge src/main/agent/knowledge-mcp-gateway.test.ts` | 21 个文件通过、1 个跳过;288 项通过、1 项跳过 |
|
||||
| `npm test` | 348 个文件通过、9 个跳过;4,088 项通过、66 项跳过 |
|
||||
| `npm run typecheck`、`npm run lint` | 通过 |
|
||||
|
||||
`npm test` 的唯一失败为 `src/main/agent/opencode-runtime-lifecycle.test.ts:120`
|
||||
("Matcher did not succeed in time")。已用 `git stash` 在同一 `HEAD` 上复现同样失败,
|
||||
确认早于本次修正且与知识库无关,未计入上表。
|
||||
|
||||
### 2026-09-13 当前工作区实现与分组验证
|
||||
|
||||
三家 Provider 已通过生产服务、本机 HTTP MCP 与生产 IPC 的短答案生成检查,Dify 另已
|
||||
从完整 App 输入区完成真实回答和引用点击。后续 UI 修正后的聚焦测试、全量回归、类型
|
||||
检查、lint、工作区及暂存区 diff 检查均已通过;下表区分早期结果与最终结果。
|
||||
|
||||
| 需求范围 | 当前代码落点与行为 |
|
||||
| --- | --- |
|
||||
| `EK-FR-01` 至 `EK-FR-05` | `ExternalKnowledgeService`、`ExternalKnowledgeStore`、数据库 v12 迁移和类型化 IPC 已接入;支持加密保存实例、启停、目录分页、详情和手工 ID 检索验证 |
|
||||
| `EK-FR-06` 至 `EK-FR-10` | `ExternalKnowledge.tsx` 与 `KnowledgeWorkspace.tsx` 已提供四种创建类型、两栏绑定表单、Provider 配置、统一列表和外部详情;参数只保存在本地绑定 |
|
||||
| `EK-FR-11` 至 `EK-FR-14` | `KnowledgeService.retrieve` 按绑定分派;`retrieveMany` 已供聊天预检索和 MCP 搜索使用;外部定位、逐库失败和历史引用已接入共享契约与会话持久化 |
|
||||
| `EK-FR-15`、`EK-FR-16` | 实例引用保护、本地移除确认、固定只读端点和有界请求已实现;传输与交互的实际限制见技术、UI 设计,不能视为原草案全部验收项已满足 |
|
||||
|
||||
实施代理与 owner 回报的测试记录如下,本轮文档核对没有重新执行这些测试:
|
||||
|
||||
| 分组 | 回报结果 | 证据边界 |
|
||||
| --- | --- | --- |
|
||||
| Client | 35 项测试通过 | 包含 FastGPT 响应包装、数组评分及取消等客户端契约修正 |
|
||||
| Service 组 | 96 项测试通过 | 服务及关联集成分组结果,不单独推断每个文件的测试数量 |
|
||||
| UI 组 | 107 项测试通过 | 组件及关联界面分组结果,不能替代 Electron 桌面操作验收 |
|
||||
| 后续集成组 | 11 个 suite、403 项通过 | 后于上述分组;不与早期计数相加 |
|
||||
| 中间全量测试 | 4,021 项通过、4 项失败、66 项跳过 | 历史结果;原失败已修正,最终回归通过 |
|
||||
| 中间 App + Citation | 216 项通过、2 项失败 | 历史结果;原生控件测试交互已修正 |
|
||||
| UI 复验前完整 App 测试 | 215 项通过 | owner 回报,覆盖整个 App 测试文件 |
|
||||
| UI 复验前全量回归 | 347 个文件通过、9 个文件跳过;4,039 项通过、66 项跳过 | `npm test -- --maxWorkers=1 --reporter=verbose`;退出码 0,Vitest 耗时 677.40 秒 |
|
||||
| UI 复验前类型检查 | 通过 | 当时最后代码修改后执行 `npm run typecheck`,owner 回报 |
|
||||
| UI 复验前 lint | 通过 | 当时最后代码修改后执行 `npm run lint`,owner 回报 |
|
||||
| UI 复验前 diff 检查 | 通过 | 当时最后代码修改后检查通过,owner 回报 |
|
||||
| 最终聚焦测试 | 245 项通过 | 完整 App 与两个 Agent 测试文件,owner 回报 |
|
||||
| 当前 UI 修正后最终全量回归 | 349 个文件通过、9 个文件跳过;4,081 项通过、66 项跳过 | `npm test -- --maxWorkers=1 --reporter=verbose`;退出码 0,Vitest 耗时 598.33 秒 |
|
||||
| 最终类型检查与 lint | 通过 | `npm run typecheck`、`npm run lint`,owner 回报 |
|
||||
| 最终工作区与暂存区 diff 检查 | 通过 | owner 回报;本轮另检查文档 diff |
|
||||
|
||||
各次测试覆盖有重叠,不合并为去重后的全仓总数。后续源码已确认 `retrieveMany` 按 rank
|
||||
跨库轮转分配预算;App 引用事件使用共享 `knowledgeReferenceKey`,保留远端 ID 不同的
|
||||
引用并去除重复项;引用弹窗的定位只显示一次。
|
||||
|
||||
UI 复验前全量证据来自临时目录中的 `goodbuddy-native-controls-fix-full.log` 和
|
||||
`goodbuddy-native-controls-fix-full.result.json`,日志汇总与结果文件退出码已核对。
|
||||
owner 确认 `App.tsx`、`App.test.tsx` 的哈希在整轮运行期间保持不变,最后代码修改后的
|
||||
类型检查、lint 和 diff 检查均通过。之后用户要求重新检查 UI 并执行真实模型与知识库验证,
|
||||
外部界面又有修改,因此该通过记录不自动覆盖当前源码。
|
||||
|
||||
原失败涉及 Preload 测试顺序沿用旧计数、原生选项控件的延迟焦点交互,以及并发导航;
|
||||
相应修正已完成,并发导航由协作代理处理。引用去重与重复定位修正也已纳入最终回归。
|
||||
精确自动测试计数仅在本文维护,其他设计文档和脱敏摘要引用本记录。
|
||||
|
||||
当前最终全量证据为 `goodbuddy-binding-selector-fix-full.log` 和
|
||||
`goodbuddy-binding-selector-fix-full.result.json`,日志汇总和退出码已核对。最后一项 App
|
||||
测试修正把旧原生 Select 操作改为实际富信息菜单的可访问交互,没有修改产品源码。
|
||||
其他 Agent 测试失败由并发 owner 在其最新代码中解决,本轮未修改这些文件。
|
||||
owner 确认 App 测试、两个 Agent 测试和 `ExternalKnowledge` 在整轮执行期间保持稳定,
|
||||
不再追加源码修改。当前通过结果已覆盖后续 UI 版本,早期结果继续作为历史记录保留。
|
||||
|
||||
### 2026-09-13 生产服务与桌面验证
|
||||
|
||||
已读取脱敏的 `kb-strict-validation/report.json` 和
|
||||
`kb-desktop-ui-validation-20260913/report.json`,并在
|
||||
[产品验证摘要](./external-knowledge-validation-2026-09-13.json)归档必要字段。报告检查
|
||||
当前生产实现,与下述 2026-09-09、2026-09-10 历史探测分开计证。
|
||||
|
||||
严格验证在真实 Electron 43.2.0 中运行生产 `KnowledgeService`、客户端和本机 HTTP MCP。
|
||||
四个配置均完成绑定保存、凭据持久化后重开、响应 Schema 和引用映射检查;每个配置执行
|
||||
绑定、服务检索、HTTP MCP 三次检索,共 12 次请求与 12 次响应。
|
||||
|
||||
| 配置编号 | 传输 | 目录 / 详情 | 服务结果 / MCP 引用 | 结论范围 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Dify 1 | HTTPS,严格证书校验 | 成功 / HTTP 405 | 0 / 0 | 详情不兼容仍可绑定与检索,零命中是成功;没有非空证据 |
|
||||
| Dify 2 | HTTPS,严格证书校验 | 成功 / 成功 | 2 / 2 | 非空服务检索与本机 HTTP MCP 成功 |
|
||||
| FastGPT | HTTPS,严格证书校验 | 成功 / 成功 | 2 / 2 | 非空服务检索与本机 HTTP MCP 成功,未覆盖桌面 Provider 专属操作 |
|
||||
| RAGFlow | HTTP,不涉及 TLS | 成功 / 成功 | 1 / 1 | 非空服务检索与本机 HTTP MCP 成功,未覆盖桌面图谱等专属操作 |
|
||||
|
||||
这些编号来自本轮报告,不由历史版本号推断。`safeStorage` 可用且真实加解密往返成功,
|
||||
四个配置保存的凭据均在重开后可用。HTTPS 结论只覆盖表中三个 HTTPS 配置。
|
||||
|
||||
Dify 桌面验证使用当前 Main、Preload、Renderer、真实 BrowserWindow 和生产 IPC。
|
||||
实例保存时,验证程序仅在 Main 的保存 Handler 边界替入已有测试密钥。已验证目录选择、
|
||||
真实检索后绑定、本地参数保存和隔离桌面配置重启;保存参数为返回 2 条、超时 15 秒、
|
||||
单片段 200 字符。14 次只读请求均为 HTTP 200,其中目录 4 次、详情 4 次、检索 6 次。
|
||||
|
||||
窗口按 1440、900、700 宽度检查,报告实际 viewport 为 1439、899、699,均无 body 横向
|
||||
溢出;宽屏参数两栏、窄屏单栏及窄屏列表可见。原生 ArrowRight 切换并激活检索页签。
|
||||
桌面凭据记录采用 `electron-safe-storage`,解密匹配且明文未写入被检查的凭据记录;
|
||||
BrowserWindow 保持 sandbox、contextIsolation 开启和 nodeIntegration 关闭。
|
||||
|
||||
生产 `knowledge.search` 返回两条真实引用,验证程序通过 `conversations.saveLocal`
|
||||
保存带测试标记的会话后重载,再打开生产引用弹窗。已检查片段、Provider、快照说明和
|
||||
弹窗内焦点,未出现本地上下文或打开来源按钮。这里验证的是历史引用保存与阅读,
|
||||
没有生成模型答案,也未证明模型会在真实回答中使用或引用证据。隔离配置和构建已清理。
|
||||
|
||||
截至上述早期验证,已知检索请求 30 次,上界 42 次:未保留诊断的首轮为 0 至 12 次、此前已确认
|
||||
12 次、严格验证 12 次、桌面 6 次。首轮准确数量无法恢复。模型生成调用为 0;Provider
|
||||
侧 Embedding、重排及审计用量不据此记为零。该计数不合并 9 月 9 日、10 日的历史探测。
|
||||
|
||||
Linux 测试 Host 的 LAN 与 VPN TCP 均可达,但验证程序读取已有 SSH 凭据时发生
|
||||
`E_SAFE_STORAGE_DECRYPT_FAILED`,在认证 SSH 之前阻断。因此真实远程 forced-preflight
|
||||
未执行;本机 HTTP MCP 成功不能替代远程生产路径验收。
|
||||
|
||||
### 2026-09-13 UI 修正与真实生成复验
|
||||
|
||||
用户指出原界面不符合要求,并要求真实模型与知识库一起验证。当前 `ExternalKnowledge.tsx`
|
||||
与 CSS 已修正 Portal 移出工作区后丢失字段作用域的问题,改用共享 `.field`;实例弹窗收为
|
||||
560px,密码使用眼睛图标,新建直接填凭据、编辑才选择保留或替换或清除。开关、粘附底部
|
||||
操作区、带地址和凭据状态的实例菜单、EmptyState 与键盘行为均已更新。实现细节见
|
||||
[UI 设计](./external-knowledge-ui-design.md#当前实现与验收边界)。
|
||||
|
||||
本轮读取 `kb-real-generation/report.json`、`kb-full-app-live/report.json` 和
|
||||
`external-visual-0913/results.json`,脱敏摘要归档在
|
||||
[UI 与真实生成复验](./external-knowledge-follow-up-2026-09-13.json)。摘要不保存模型端点、
|
||||
私有配置标识、密钥、业务正文、生成答案或内容哈希。
|
||||
|
||||
| 验证层 | 实际覆盖 | 结果与边界 |
|
||||
| --- | --- | --- |
|
||||
| 生产 IPC 与真实模型 | 真实 Electron、Preload、已注册 IPC 和 ModelAgentRuntime;Dify/FastGPT/RAGFlow 的 `always`,以及 Dify `auto` | 最终场景均有非空短答案、依据匹配、引用标记、单库范围与文本/外部引用持久化证据;Renderer 是隔离验证 DOM,不是完整 App |
|
||||
| 完整 App | 真实 Main、Preload、Renderer;从界面创建 Dify 实例及非空绑定,在输入区选择知识库并点击发送,Ask + `always` | 真实模型生成并保存答案,检索片段进入模型请求,点击引用后片段和远端文档/片段 ID 匹配,焦点在弹窗内;无伪造会话、引用或 Agent 事件 |
|
||||
| 视觉与键盘 fixture | 当前组件和样式、真实 body Portal 与 Electron 原生键盘;IPC 为本地假数据 | 56 张截图、138 项检查通过,errors 为空;覆盖浅深主题及 1280×800、960×720、720×640、640×420,不作为真实 Provider 业务调用证据 |
|
||||
|
||||
生产 IPC 首轮 FastGPT/RAGFlow 已有非空、依据匹配和引用,但验证程序直接在序列化请求
|
||||
字符串内寻找原片段,导致 `retrievedSnippetInBody=false`、`passed=false`。改为解码 Provider
|
||||
请求及其中的证据 JSON 后,两家重跑均通过。归档保留首轮失败和修正后结果,不把首轮
|
||||
改写为通过,也不把这次断言修正记成产品检索修复。Dify `auto` 通过进程内
|
||||
`ModelToolProvider → KnowledgeMcpGateway` 执行 `knowledge_search`,不是 HTTP MCP 会话。
|
||||
|
||||
模型使用用户已保存的默认配置,凭据在 Main 用 safeStorage 解密。完整 App 检查确认原
|
||||
模型设置未改变;Dify/FastGPT 请求使用严格 HTTPS,RAGFlow 和所用模型保留已配置的 HTTP。
|
||||
完整 App 中的测试知识库密钥仅在 Main 保存 IPC 边界替入,报告没有公开实际密钥。
|
||||
|
||||
完整 App 另检查浅深主题的 Portal 编辑器:宽度 560px、字体与字号继承共享控件、背景
|
||||
随主题变化,窗口内无横向溢出。视觉 fixture 检查了密码显隐前后布局、原生 Tab/Space、
|
||||
开关、底部保存按钮、焦点循环与恢复、菜单跳过禁用项、页签、凭据替换和清除确认。
|
||||
|
||||
| 本轮请求 | 模型 | KB 全部 | KB 检索 | 目录 | 详情 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 生产 IPC 生成报告 | 7 | 12 | 9 | 3 | 0 |
|
||||
| 完整 App 报告 | 1 | 5 | 3 | 1 | 1 |
|
||||
| 合计 | 8 | 17 | 12 | 4 | 1 |
|
||||
|
||||
计数是本轮应用实际发出的 HTTP 请求,包含绑定验证和断言修正后的重跑;不推断上游代理
|
||||
内部请求或 Provider Embedding、重排用量。生产 IPC 的检索含绑定验证 3 次、强制预检索
|
||||
5 次、按需检索 1 次;完整 App 含创建验证 2 次、输入区发送触发 1 次。本轮采用精确增量,
|
||||
不沿用生成报告中漏计早期桌面请求的累计字段。
|
||||
|
||||
“生成未经验证”已在上述短答案范围内解决。检查证明选中知识库的真实片段进入模型并
|
||||
产生带依据和引用的短答案,不证明长文、多库冲突、复杂推理或广泛问答质量。完整 App
|
||||
仍只覆盖 Dify;FastGPT/RAGFlow 完整桌面与专属参数、Linux 远程路径继续待验收。
|
||||
当前 UI 修改后的聚焦测试、全量回归及相关检查已通过,精确结果见前文自动测试记录;
|
||||
早期通过记录保持原时间范围。
|
||||
|
||||
### 历史验证记录
|
||||
|
||||
2026-09-10 官方 API 核对与真实实例复测:
|
||||
|
||||
- 官方资料链接与版本差异见[技术设计第 9 节](./external-knowledge-technical-design.md#9-provider-映射)。
|
||||
已修正 Dify 默认配置优先级、FastGPT 端点响应与数组评分说明、RAGFlow 新旧候选数参数边界。
|
||||
- 使用现有本地测试配置与脚本默认查询,执行基础 14 次、扩展 19 次,共 33 次只读请求,
|
||||
其中检索 17 次。未调用远端内容写入、聊天或生成端点;检索所用 Embedding、重排及审计
|
||||
用量由远端服务处理,不据此推断底层模型调用次数。
|
||||
- 两轮均在独立 Node 进程显式设置 `NODE_TLS_REJECT_UNAUTHORIZED=1`。两个 Dify 实例与
|
||||
FastGPT 的 HTTPS 请求通过严格证书校验;RAGFlow 使用 HTTP。
|
||||
- [基础报告](./external-knowledge-probe-2026-09-10-strict.json)和
|
||||
[扩展报告](./external-knowledge-probe-2026-09-10-extended.json)保留脱敏状态、数量、字段结构
|
||||
和耗时。每个实例只对目录选出的一个库执行检索,未遍历全部库。
|
||||
|
||||
| 实例 | 目录与详情 | 检索结果 |
|
||||
| --- | --- | --- |
|
||||
| Dify 0.15.8 | 目录 3 项;详情 HTTP 405 | 默认与显式当前配置请求均成功,均为 0 条;非空结果尚未验证 |
|
||||
| Dify 1.17.0 | 目录 1 项;详情成功 | 默认与显式当前配置均为 3 条非空结果 |
|
||||
| FastGPT,版本未确认 | 根目录 1 个 Dataset;详情 HTTP 200、业务码 200 | 向量 8 条、全文 2 条、混合 7 条、混合加重排 9 条;重排响应明确为启用;检索包装为 `data.list`,`score` 为数组 |
|
||||
| RAGFlow,版本未确认 | 目录 29 项;详情成功;18 项配置图谱、16 项有完成证据 | 基础 2 条、图谱 3 条、知识编译开关 2 条;请求均 HTTP 200、业务码 0,但没有知识编译配置证据 |
|
||||
|
||||
- 修正探测脚本将 Dify/RAGFlow 失败记成零命中的问题,增加实际调用与成功标记,统一
|
||||
FastGPT 地址规范化并记录分数结构、响应中的重排状态。未修改产品客户端;旧 FastGPT
|
||||
检索数组包装、数组评分映射及上一轮发现的取消分类问题在当时仍待处理;当前客户端已修正,
|
||||
当前服务和本机 HTTP MCP 的真实 Provider 复验见 2026-09-13 记录,旧包装等特定响应变体
|
||||
仍不能仅凭成功计数认定覆盖。
|
||||
- 三组探测测试共 43 项通过,包含两份新报告的脱敏与只读操作检查;`npm run typecheck`、`npm run lint` 通过。
|
||||
`npm test` 在该历史轮次先后达到 60 秒和 240 秒执行时限,均未输出汇总;当前最终全量
|
||||
已通过,见 2026-09-13 记录。
|
||||
- 本轮未验证真实 401/403/404/429、超时、取消、Scoped Key、分页、元数据过滤或候选数
|
||||
参数效果。探测脚本不打包到桌面或 GoodBuddy Agent,本次未改变远程 Runtime 生产路径。
|
||||
|
||||
2026-09-09 外部知识库适配基础验证:
|
||||
|
||||
- 使用本机未入库的测试配置累计执行 107 次只读请求,其中更新后的最终归档脚本执行 19 次;脚本
|
||||
调试产生的结果只写入批准的临时目录。三套目录、FastGPT 详情和 RAGFlow 详情均成功;
|
||||
Dify `0.15.8` 详情 GET 返回 405,Dify `1.17.0` 详情 GET 返回 200。验证过程未输出 API
|
||||
Key、知识库名称和业务正文。
|
||||
- Dify 响应头确认两个版本为 `0.15.8` 和 `1.17.0`。1.17 默认与显式当前配置检索均返回
|
||||
3 条非空结果,已验证 `segment/document` 字段;详情新增 Metadata、多模态、摘要索引和
|
||||
Pipeline 能力字段。FastGPT 与 RAGFlow 响应未提供产品版本。实测还确认
|
||||
FastGPT 列表 `data` 为直接数组、结果 `a` 可缺失,RAGFlow 文档名字段为
|
||||
`document_keyword`。详细基线见[外部知识库技术设计 9.0 节](./external-knowledge-technical-design.md#90-真实实例验证基线)。
|
||||
- 新增 `scripts/external-knowledge-probe.mjs`,可重复生成
|
||||
不含地址、凭据、远端 ID、名称、查询和正文的能力报告。最终报告归档在
|
||||
[`external-knowledge-probe-baseline.json`](./external-knowledge-probe-baseline.json)。
|
||||
- FastGPT 的 embedding、fullTextRecall、mixedRecall 和 mixedRecall + Rerank 均返回非空
|
||||
结果。RAGFlow 当前 28 个库中 17 个配置 GraphRAG、15 个存在完成证据;已完成库在基础
|
||||
检索为零结果时,图谱检索返回 1 条结果。当前没有库提供 Knowledge Compilation 配置证据。
|
||||
- 新增 `src/main/knowledge/external/external-knowledge-client.ts`,固定实现三种 Provider 的
|
||||
只读目录和检索端点、地址规范化、超时、响应大小限制、HTTP 错误分类和结果标准化。
|
||||
- `npx vitest run src/main/knowledge/external/external-knowledge-client.test.ts`:1 个测试文件、
|
||||
5 项测试通过。
|
||||
- `npx vitest run tests/external-knowledge-probe-report.test.ts src/main/knowledge/external/external-knowledge-client.test.ts`:
|
||||
2 个测试文件、7 项测试通过;覆盖归档报告脱敏、只读操作和特色能力证据。
|
||||
- `npx eslint src/main/knowledge/external/external-knowledge-client.ts src/main/knowledge/external/external-knowledge-client.test.ts`:通过。
|
||||
- `npm run typecheck`:被当前工作区已有的 `src/main/ipc.test.ts:7694` tuple 类型错误阻断;
|
||||
新增适配器、探测脚本和报告测试的定向测试与 ESLint 已通过。
|
||||
- 当前 Node 测试环境设置了 `NODE_TLS_REJECT_UNAUTHORIZED=0`,Dify 和 FastGPT 请求实际
|
||||
使用了放宽的 TLS 校验。产品接入不能继承该进程级默认,保存或测试实例时仍需按技术设计
|
||||
明确处理传输风险。
|
||||
|
||||
2026-09-05 最终验证:
|
||||
|
||||
- `npm test`:325 个测试文件、3511 项测试通过;8 个文件、55 项手动或集成测试按既有
|
||||
配置跳过。
|
||||
- `npm run typecheck`:通过。
|
||||
- `npm run lint`:通过。
|
||||
- `npm run build`:Electron Main、Preload、Renderer 生产构建及控制面安装包检查通过。
|
||||
- `npx vitest run src/main/knowledge/knowledge-service.test.ts -t "imports real DOCX and PDF files from disk into searchable chunks"`:
|
||||
有效 Open XML DOCX 与两页原生文本 PDF 均通过生产 `KnowledgeService.importPaths`
|
||||
导入、解析、分块和全文检索。
|
||||
|
||||
2026-09-06 对话知识库选择补充验证:
|
||||
|
||||
- `App.test.tsx` 覆盖点击名称、文档数量和行本身时切换选中状态,以及空目标失焦时
|
||||
弹层不提前卸载。Windows 隔离桌面实际点击名称后选中、点击数量后取消,弹层保持打开。
|
||||
- 本轮全量校验结果见[工作栏验证记录](../assistant-workbar/progress.md);本项验证未调用模型。
|
||||
|
||||
## 剩余工作
|
||||
|
||||
- 完成 FastGPT/RAGFlow 桌面专属配置及实际参数效果检查;补充混合库、真实聊天
|
||||
`always`/`auto` 的多库与复杂问答、停用和本地删除、未覆盖的主题及键盘路径。三家生产 IPC
|
||||
短答案、Dify 完整输入区真实回答与引用,以及已检查的主题/尺寸/键盘不再列为首次待验收。
|
||||
- 解决验证程序对已有 SSH 凭据的解密问题后,执行真实 Linux forced-preflight;网络已可达,
|
||||
当前阻断点是认证前凭据读取。
|
||||
- 三家服务已完成真实检索与本机 HTTP MCP。FastGPT/RAGFlow 部署版本、Dify 1 非空结果、
|
||||
Scoped Key 和真实错误矩阵等仍需补证;旧探测不证明当前桌面配置效果。
|
||||
- 原设计中的分项探测、版本能力展示、地址确认、请求取消交互和详细诊断与当前实现有差异,
|
||||
具体范围见[技术设计](./external-knowledge-technical-design.md)和
|
||||
[UI 设计](./external-knowledge-ui-design.md#当前实现与验收边界)。这些差异需由功能 owner 核对验收,
|
||||
本轮仅改文档,不补产品代码或扩大架构。
|
||||
- 扫描 PDF 的真实 OCR 验收依赖用户已安装并校验的 OCR 模型;本次 DOCX/PDF 回归覆盖
|
||||
原生文本 PDF,不宣称覆盖扫描件 OCR。
|
||||
- PDF.js 在测试环境会提示未配置 `standardFontDataUrl`;文本、页码定位与检索结果不受
|
||||
影响。
|
||||
@@ -0,0 +1,79 @@
|
||||
# 知识库 UI 设计
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
知识库优先帮助普通用户完成“添加资料并可靠提问”,高级 RAG 调试和图谱维护不得阻塞
|
||||
首次使用。全局视觉、页面壳层、通知、焦点和危险操作遵循
|
||||
[UI-DESIGN.md](../../../UI-DESIGN.md)。
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
当前列表同时容纳本地库与外部绑定,页面增加“管理外部实例”和来源筛选。以下详情结构、
|
||||
资料导入和索引状态适用于本地库;外部创建、配置、检索测试及引用的当前实现与待验收项见
|
||||
[外部知识库 UI 设计](./external-knowledge-ui-design.md)。
|
||||
|
||||
```text
|
||||
知识库
|
||||
├─ 知识库列表
|
||||
└─ 当前知识库
|
||||
├─ 文档与来源
|
||||
├─ 知识图谱(启用后显示)
|
||||
├─ 任务中心
|
||||
└─ 高级设置
|
||||
```
|
||||
|
||||
标题区提供“用于当前对话”“检索测试”、编辑和删除。新建知识库是页面主操作。
|
||||
“高级设置”承载问答效果入口、向量索引、分块与图谱配置,不把工程参数放在首次路径。
|
||||
|
||||
本地与外部库共用的列表以 `minmax(0, 1fr)` 限制条目宽度,再对名称单行省略;选中卡片
|
||||
的边框必须完整留在列表内,Provider/实例等辅助文字允许换行。不得用列表外层裁切
|
||||
掩盖被长名称撑宽的卡片,库图标不能随名称一起收缩。外部详情的长名称和远端名称允许
|
||||
换行,不能撑出横向滚动条。创建类型分段控件与详情页签保持固定内容高度,不随长表单收缩。
|
||||
|
||||
## 3. 首次使用
|
||||
|
||||
没有知识库时,页面空状态显示“新建知识库”主操作。创建时先选择类型;本地类型默认
|
||||
展示名称和描述,存储方式与知识图谱放入折叠的“高级设置”。本地库创建完成后进入
|
||||
“文档与来源”,用户可以导入文件、文件夹或 URL。
|
||||
|
||||
文档处理完成后,标题区“用于当前对话”可用。该操作把当前知识库加入当前
|
||||
Conversation 的检索范围,返回对话并聚焦输入框。
|
||||
|
||||
## 4. 状态
|
||||
|
||||
知识库列表显示“可用”和“需处理”文档数。详情标题持续显示可检索、处理中、需处理和
|
||||
总文档数,即使用户填写了知识库描述也不隐藏状态。
|
||||
|
||||
文档行显示:
|
||||
|
||||
- 总体处理状态。
|
||||
- 全文状态。
|
||||
- 向量状态。
|
||||
- 启用图谱时的图谱状态。
|
||||
|
||||
失败文档保留错误信息和“重新处理”。只要仍有可用分块,就允许打开分块管理,避免隐藏
|
||||
上一版可用索引。
|
||||
|
||||
## 5. 问答效果与高级参数
|
||||
|
||||
“检索测试”默认只突出真实问题、测试按钮和结果。高级参数使用折叠区,包含候选倍数、
|
||||
通道权重、最低相似度、重排、结果数和上下文预算。“高级设置 → 问答效果”打开同一
|
||||
工作台。
|
||||
|
||||
向量模型未配置时显示“前往设置”,直接进入模型连接,不使用只有说明文字的死路状态。
|
||||
|
||||
## 6. 来源操作
|
||||
|
||||
文档行提供“打开来源”,Main 根据知识库和文档归属重新读取保存的文件路径或 URL。
|
||||
移除来源使用确认对话框,说明文档数、索引、图谱证据和托管副本影响。初始焦点位于
|
||||
取消,提交期间禁止重复操作,失败时保留对话框和错误。
|
||||
|
||||
## 7. 对话范围
|
||||
|
||||
知识库多选属于 Conversation。新对话默认空范围;切换对话恢复各自选择。创建新知识库
|
||||
只加入当前 Conversation,不重新启用其他已取消项。发送请求时冻结当前选择和检索方式。
|
||||
|
||||
输入区的知识库选择弹层使用整行原生 Checkbox 标签,点击名称、文档数量或行内空白
|
||||
均切换一次选择;选中背景和整行焦点环保持可见。Tab 聚焦、Space 切换,Escape 关闭并
|
||||
返回触发按钮。点击行内非焦点元素时产生的空目标失焦不关闭弹层;点击外部或焦点明确
|
||||
移到外部时才关闭,避免原生标签完成勾选前被卸载。
|
||||
@@ -33,6 +33,18 @@
|
||||
|
||||
## 2. Epic A:明确控制是否检索
|
||||
|
||||
### US-A0 首次建立可用知识库
|
||||
|
||||
作为首次使用者,我希望先命名知识库并添加资料,不需要先理解向量、分块或图谱。
|
||||
|
||||
验收:
|
||||
|
||||
- 没有知识库时只显示一个“新建知识库”主操作
|
||||
- 创建表单默认只展示名称和描述
|
||||
- 存储方式和知识图谱位于折叠的高级设置
|
||||
- 文档可检索后提供“用于当前对话”
|
||||
- 高级检索参数不阻塞首次导入和首次提问
|
||||
|
||||
### US-A1 模型按需检索
|
||||
|
||||
作为普通知识使用者,我希望保留由模型判断是否需要检索的模式,以便一般闲聊
|
||||
@@ -73,6 +85,9 @@
|
||||
- 同一请求最多启用 20 个知识库
|
||||
- 对话输入区持续显示已选数量和检索方式
|
||||
- 范围为空时检索方式不产生误导状态
|
||||
- 知识库 ID 与检索方式一起按 Conversation 持久化
|
||||
- 新对话和未保存过范围的旧对话默认不勾选知识库
|
||||
- 切换 Conversation 恢复各自选择,不共享临时全局范围
|
||||
|
||||
## 3. Epic B:检索可见、可诊断
|
||||
|
||||
@@ -233,6 +248,18 @@
|
||||
- 全库重建可取消
|
||||
- 已成功替换的文档继续可用
|
||||
|
||||
### US-D4 查看和移除来源
|
||||
|
||||
作为知识库维护者,我希望打开原始文档并在移除来源前看到准确影响。
|
||||
|
||||
验收:
|
||||
|
||||
- 文档行可通过系统默认应用打开数据库保存的原始来源
|
||||
- Renderer 只能提交知识库 ID 和文档 ID,不能提交任意路径或 URL
|
||||
- 移除来源前显示来源名称、文档数量以及索引和图谱证据影响
|
||||
- 托管模式额外说明应用托管副本会被删除
|
||||
- 用户确认前不得删除来源
|
||||
|
||||
## 6. Epic E:高级分块
|
||||
|
||||
### US-E1 固定分块
|
||||
|
||||
+12
-11
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-05 |
|
||||
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[智能心跳 PRD](../smart-heartbeat/prd.md)、[Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
## 1. 背景
|
||||
@@ -16,13 +16,13 @@ GoodBuddy 当前记忆已经支持:
|
||||
- `global`、`project`、`conversation` 三种作用域。
|
||||
- `preference`、`fact`、`summary`、`procedure` 四种类型。
|
||||
- `proposed`、`confirmed`、`rejected` 三种状态。
|
||||
- 智能心跳提出 Global 或 Project 记忆候选,由用户确认;其长期“未来分区记忆”方向尚未设计。
|
||||
- 智能心跳提出 Global 或 Project 记忆候选,由用户确认;后续按本 PRD 的分区记忆模型增强。
|
||||
|
||||
但当前能力仍不足以支撑自动化和并行实验:
|
||||
|
||||
1. 交互请求会把已加载列表中的最多 20 条已确认记忆直接拼入提示,缺少查询相关度和明确的
|
||||
会话级过滤契约。
|
||||
2. 数据库有会话作用域,但旧版心跳候选与未来唤起需求混在同一产品概念中。
|
||||
2. 数据库有会话作用域,但旧版心跳候选与分区记忆的唤起需求混在同一产品概念中。
|
||||
3. 缺少 Automation、Experiment 和 Run 分区。
|
||||
4. 来源字段存在于表结构,但普通创建和心跳候选尚未完整保存来源关系。
|
||||
5. 缺少事实的有效时间、冲突、替代、访问记录和衰减。
|
||||
@@ -64,10 +64,11 @@ SQLite 显式字段、FTS、来源关系和可选本地 Embedding 足以支持
|
||||
- 同一实体跨大量会话的别名消歧。
|
||||
- 可解释的关系证据链。
|
||||
|
||||
### 2.5 未来分区记忆尚待设计
|
||||
### 2.5 分区记忆与唤起
|
||||
|
||||
已确认智能心跳的长期方向是“未来分区记忆”,但当前尚未定义其数据结构、唤起条件、状态、
|
||||
生命周期、与长期记忆的关系或迁移方式。本 PRD 不新增 `FutureMemory` 类型、表或检索规则。
|
||||
正式产品概念统一为“分区记忆”。时间和内部事件可以作为分区记忆条目的可选唤起条件,但
|
||||
不能改变条目所属分区、绕过分区读取边界,也不建立平行的记忆类型或表。唤起字段、状态、
|
||||
生命周期、页面和旧数据迁移必须在实施前补充设计。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
@@ -430,8 +431,8 @@ Project 记忆与 Global 偏好冲突时:
|
||||
|
||||
每条记忆展示内容、类型、范围、来源、状态、时间、置信度、重要性和冲突。
|
||||
|
||||
智能心跳的完整配置仍在“智能心跳”菜单中管理;未来分区记忆完成独立设计前,不加入记忆
|
||||
中心信息架构。
|
||||
智能心跳的完整配置仍在“智能心跳”菜单中管理。分区记忆的唤起条件完成字段、状态和生命周期
|
||||
设计后,可以在记忆中心增加“待唤起”视图;不得创建平行的记忆分区或内容页。
|
||||
|
||||
## 16. 数据模型建议
|
||||
|
||||
@@ -474,13 +475,13 @@ Project 记忆与 Global 偏好冲突时:
|
||||
6. 增加冲突、时态、替代和归档。
|
||||
7. 增加实验冻结快照与 Run 隔离。
|
||||
8. 增加可选本地 Embedding 和混合排序。
|
||||
9. 未来分区记忆和时间知识图谱都必须在明确需求与独立设计后再实施。
|
||||
9. 增加分区记忆的时间/内部事件唤起;时间知识图谱仍须在明确需求与独立设计后再实施。
|
||||
|
||||
## 19. 验收标准
|
||||
|
||||
- [ ] 普通会话只读取 Global、当前 Project 和当前 Conversation 的允许记忆。
|
||||
- [ ] 智能心跳配置只能属于 Global 或 Main 已验证的一个、多个 Project。
|
||||
- [ ] 未来分区记忆完成独立设计前,不新增相关表、状态或检索行为。
|
||||
- [ ] 唤起条件始终依附于一个已验证的记忆分区,不建立平行的记忆表或范围。
|
||||
- [ ] Task 执行只读取内部 Run 快照绑定的分区。
|
||||
- [ ] 实验 Run 不能读取其他 Run 的消息或记忆。
|
||||
- [ ] 每条非手动记忆都有可追溯来源。
|
||||
|
||||
@@ -12,6 +12,10 @@ Provider 要求的附加元数据;GoodBuddy 只在直连链路或 Runtime 明
|
||||
| [技术设计](./technical-design.md) | 契约、持久化、合并优先级、Runtime 支持矩阵和验证 |
|
||||
| [功能进度](./progress.md) | 已验证实现、剩余工作和验证证据 |
|
||||
|
||||
模型连接选择准确性也在本目录维护:技术设计第 8 节定义通道直连回退修复,
|
||||
UI 设计末节定义 Harness 来源标签和公开设置展示;Harness 的实际解析顺序由其
|
||||
[技术设计](../deepseek-harness/technical-design.md#121-配置来源)唯一维护。
|
||||
|
||||
本次改动是现有模型连接的有界扩展,不新增独立业务对象或工作流,因此不单独建立 PRD、
|
||||
User Stories 和逻辑设计;本页定义产品边界,技术设计定义唯一的请求合并规则。
|
||||
|
||||
@@ -42,5 +46,6 @@ User Stories 和逻辑设计;本页定义产品边界,技术设计定义唯
|
||||
## 相关功能
|
||||
|
||||
- [直连模型 Agent](../direct-model-agent/README.md)
|
||||
- [图片生成与连续修改](../image-generation/README.md)
|
||||
- [DeepSeek Harness Runtime](../deepseek-harness/README.md)
|
||||
- [远程主机与远程执行](../remote-host/README.md)
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# 模型连接请求定制进度
|
||||
|
||||
## 2026-09-11:发布前固定模型说明修复
|
||||
|
||||
- 项目已保存固定 Runtime 模型时,恢复显示实际固定模型的帮助文字,保留简化后的菜单;
|
||||
规则见 [会话与项目 Runtime 选择](./ui-design.md#会话与项目-runtime-选择)。
|
||||
- `npm exec vitest run src/renderer/src/ProjectRuntimeSelector.test.tsx`:2 项通过,
|
||||
覆盖中英文下 OpenCode、Continue、DeepSeek Harness 的固定选项及恢复跟随操作。
|
||||
- `npm test`:343 个文件、3,911 项通过,66 项按平台或手工/真实服务条件跳过;
|
||||
`npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 通过。
|
||||
- 本次新增修复仅影响 Renderer 帮助文字,未修改共享模型解析、Desktop-to-Agent 请求、
|
||||
Agent 或 Runtime 启动实现;不需要单独远程修复。真实模型调用 0 次,未运行真实 Host
|
||||
或桌面端到端模型会话。本地未构建或打包,发布候选生产构建由主分支 CI 验证。
|
||||
|
||||
## 2026-09-10:配置准确性修复
|
||||
|
||||
- 通道直连回退修复已按[技术设计第 8 节](./technical-design.md#8-通道直连模型选择修复)
|
||||
实现;测试把修复结果交给现有 Main resolver,不仅断言返回对象。
|
||||
- Harness 设置使用专用来源标签、关联帮助及 Main 直接从生产 resolver 派生的只读
|
||||
`deepseekHarnessPlatformModel` 投影;准确区分完整管理员预置、兼容回退和无连接,
|
||||
不再从通用环境凭据状态推断。投影不含地址、密钥或请求定制,也不写入设置文件。
|
||||
- 无兼容连接时管理员来源仍可选择;未保存候选与实际已保存来源分别展示。UI 验证涵盖
|
||||
中英文、兼容默认、首个兼容回退、管理员覆盖、部分环境回退、无连接、草稿预览和保存动作。
|
||||
- 聚焦命令 `npx vitest run src/main/agent/runtime-selection-contracts.test.ts src/renderer/src/SettingsPanel.test.tsx`
|
||||
对应的两个文件在本次五文件联合运行中全部通过,共 96 项。真实模型调用 0 次;
|
||||
本次未运行全量测试、typecheck、lint 或真实 Harness 请求。
|
||||
通道修复集成测试随后归入 Main 测试目录,避免 Shared/Web 类型项目导入主进程实现。
|
||||
- 后续聚焦验证 `npx vitest run src/main/runtime-settings-store.test.ts src/renderer/src/SettingsPanel.test.tsx`
|
||||
全部通过:2 文件、181 项。Store 覆盖完整、部分与旧环境变量、默认/首个兼容回退、
|
||||
无连接、精确字段白名单、非持久化以及不改变所选来源。真实模型/Host 调用均为 0;
|
||||
未运行全量测试、构建、typecheck 或 lint。
|
||||
|
||||
## 2026-09-03
|
||||
|
||||
### 已完成
|
||||
|
||||
@@ -85,3 +85,33 @@ JSON 对象,标准认证 Header 也由 schema 拒绝。
|
||||
Utility 启动配置和 Pi-AI Provider 配置。
|
||||
- 远程:Main/Agent 网关合并、认证优先级、摘要绑定、大小限制和不重放。
|
||||
- UI:默认值、保存、无效 JSON 保留和 Runtime 支持说明。
|
||||
|
||||
## 8. 通道直连模型选择修复
|
||||
|
||||
`repairChannelRuntimeSelection` 只在默认连接可用于文本通道时保留动态
|
||||
`{ provider: 'model' }`。默认连接为图像生成、API Key 认证但明确缺少凭据,或默认 ID
|
||||
已不存在时,若找到可用文本连接,修复结果必须携带该连接的 `profileId`,使后续
|
||||
`applyRuntimeSelection` 使用已验证回退,而不是重新解析到不可用的全局默认值。
|
||||
已有可用显式引用保持不变;没有可用连接时不虚构 ID 或声称可运行。
|
||||
|
||||
共享回归测试将修复结果直接传入 Main 的生产 resolver,验证动态默认、显式固定、
|
||||
图像默认、缺少凭据和失效默认 ID。该修复不改变 OpenCode、Continue 或托管 SSH 的
|
||||
Runtime 选择路径。
|
||||
|
||||
## 9. Harness 平台来源公开投影
|
||||
|
||||
`RuntimeSettings.deepseekHarnessPlatformModel` 是可选只读元数据,描述 `platform`
|
||||
选项在当前已保存模型设置与管理员环境下的实际解析结果:
|
||||
|
||||
- `{ source: 'environment', name, modelName }`:使用完整管理员预置。
|
||||
- `{ source: 'profile', profileId, name, modelName }`:使用兼容 GoodBuddy 连接回退。
|
||||
- `{ source: 'unavailable' }`:没有可解析的连接。
|
||||
|
||||
Main 的 `toPublicSettings` 复用 `resolveDeepSeekHarnessModelProfile`,只在调用中将来源
|
||||
设为 `platform`,不改变用户已保存的 `default`、`profile` 或 `platform` 选择。投影逐字段
|
||||
构造,不包含地址、API Key、自定义 Header/Body 或凭据状态,不写入设置文件、迁移或新状态。
|
||||
公开契约保持可选,缺失时 UI 明确来源信息不可用,不从通用模型或环境凭据猜测。
|
||||
|
||||
`source: 'profile'` 描述来源分支,不代表凭据来源:仅环境密钥或旧环境变量可能仍影响该
|
||||
GoodBuddy 连接,但不会被误报为完整管理员预置。连接解析不代替真实模型请求或可用性测试。
|
||||
UI 的未保存兼容候选只作草稿预览,保存后继续以 Main 返回的投影为准。
|
||||
|
||||
@@ -36,3 +36,27 @@ OpenCode 与 DeepSeek Harness 不使用该值。
|
||||
- 无效状态不能只依赖颜色,必须同时提供错误文字和 `aria-invalid`。
|
||||
- 文本域遵循标准键盘编辑、选择、复制和粘贴行为;保存继续使用设置页唯一的主保存操作。
|
||||
- 焦点边框、字体、间距、颜色和深浅主题全部复用全局语义令牌。
|
||||
|
||||
## 会话与项目 Runtime 选择
|
||||
|
||||
会话与项目选择器只提供直连模型和各 Runtime 入口,不再枚举每个 Runtime 与每个模型的
|
||||
组合;Runtime 模型在系统设置中配置。已保存的固定 Runtime 模型选择继续保留,项目
|
||||
选择器的选项与帮助文字均显示其固定模型,不能误报为跟随全局设置;用户可选择普通
|
||||
Runtime 入口恢复跟随。该展示规则不改变模型解析、远程 Agent 协议或执行权限。
|
||||
|
||||
## Harness 模型来源展示
|
||||
|
||||
DeepSeek Harness 的 `platform` 选项命名为“管理员环境优先,回退 GoodBuddy 兼容连接”,
|
||||
不能沿用 OpenCode/Continue 的“Runtime 自有配置”标签。来源规则以
|
||||
[Harness 模型配置](../deepseek-harness/technical-design.md#121-配置来源)为准。
|
||||
帮助文字与选择器通过 `aria-describedby` 关联,并明确此选项不加载用户 profile 或
|
||||
配置文件;无兼容 GoodBuddy 连接时仍可选择管理员环境来源。
|
||||
|
||||
概览使用 Main 提供的[平台来源投影](./technical-design.md#9-harness-平台来源公开投影),
|
||||
明确显示实际管理员预置模型,或“未配置完整的管理员预置”及已保存兼容回退的名称与模型;
|
||||
没有可解析连接时显示明确空状态,但保留 `platform` 选项。不得从通用模型名称或
|
||||
`credentialSource` 猜测管理员来源,也不得显示密钥或读取 Main 环境。
|
||||
|
||||
没有管理员覆盖且草稿兼容候选发生变化时,额外显示“草稿兼容候选(未保存)”,保留实际
|
||||
已保存来源;草稿失去兼容连接时也明确说明。管理员覆盖生效时不把草稿回退显示为实际模型。
|
||||
保存完成后以 Main 重新解析的投影刷新概览。
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Office 协同编辑
|
||||
|
||||
Office 协同编辑通过 ShareServer 集成 ONLYOFFICE Docs,使 GoodBuddy Desktop 可以在助手
|
||||
工作栏中打开多个文档页签,并在同一编辑器中完成人工编辑、AI 选区修改、保存、撤销和冲突处理。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [Office 协同编辑 PRD](./prd.md) | 用户问题、产品范围、功能要求和验收标准 |
|
||||
| [用户故事](./user-stories.md) | 打开、多页签、保存、AI 编辑、冲突和恢复场景 |
|
||||
| [功能逻辑设计](./logic-design.md) | 文档会话、保存、冲突、断线和关闭规则 |
|
||||
| [界面设计](./ui-design.md) | 工作栏多实例文档页签、编辑状态和响应式行为 |
|
||||
| [技术设计](./technical-design.md) | Desktop、ShareServer、ONLYOFFICE 的边界、协议、安全与实施顺序 |
|
||||
|
||||
## 术语
|
||||
|
||||
- **文档页签**:助手工作栏中的多实例应用页签,标题来自文档文件名。
|
||||
- **编辑会话**:一个文档页签对应的服务端短期会话和 Desktop 本地状态。
|
||||
- **源文件**:用户明确打开的本机或托管 SSH 工作区文件。
|
||||
- **工作副本**:编辑期间由 ShareServer 管理并提供给 ONLYOFFICE 的临时版本。
|
||||
- **保存检查点**:ONLYOFFICE 已提交给 ShareServer、但不一定已经回写源文件的稳定版本。
|
||||
|
||||
文档解析、OCR 和旧格式转换继续由[文档处理](../document-processing/README.md)负责。ShareServer
|
||||
的组织、服务部署和运维由独立仓库的 ShareServer 文档负责;跨端身份、传输和审计共同规则见
|
||||
[共享网络总体设计](../../architecture/share-network-architecture.md)。
|
||||
|
||||
当前功能仍处于设计阶段,尚无可验证实现,因此不建立进度文档。开始实施后再新增
|
||||
`progress.md`,只记录实际完成范围和验证证据。
|
||||
@@ -0,0 +1,117 @@
|
||||
# Office 协同编辑功能逻辑设计
|
||||
|
||||
## 1. 核心不变量
|
||||
|
||||
1. 一个文档页签只绑定一个 `DocumentEditSession`,不会随当前项目或会话切换目标。
|
||||
2. 源文件路径和替换权限只由 Desktop Main 持有。
|
||||
3. ShareServer 是服务端工作副本和保存检查点的权威,Desktop 是源文件回写结果的权威。
|
||||
4. ONLYOFFICE 报告已保存不等于源文件已经回写成功。
|
||||
5. 没有稳定保存检查点时,任何界面都不能显示“修改可恢复”。
|
||||
6. 源文件基准版本不匹配时禁止静默覆盖。
|
||||
7. 一次 AI 请求要么作为一个编辑事务完整应用,要么不产生修改。
|
||||
|
||||
## 2. 状态维度
|
||||
|
||||
编辑会话不是一个布尔状态,至少包含以下独立维度:
|
||||
|
||||
```ts
|
||||
type DocumentEditState = {
|
||||
connection: 'connecting' | 'online' | 'offline' | 'expired'
|
||||
editor: 'loading' | 'ready' | 'failed'
|
||||
checkpoint: 'clean' | 'dirty' | 'saving' | 'available' | 'failed' | 'unknown'
|
||||
source: 'unchanged' | 'writing' | 'saved' | 'conflict' | 'unavailable'
|
||||
access: 'edit' | 'read-only' | 'revoked'
|
||||
}
|
||||
```
|
||||
|
||||
`checkpoint=available` 只表示 ShareServer 有可下载版本;只有 `source=saved` 才表示本机或远程
|
||||
源文件已经更新。
|
||||
|
||||
## 3. 打开流程
|
||||
|
||||
```text
|
||||
用户选择文件
|
||||
-> Desktop 验证来源、格式、大小和 ShareServer 连接
|
||||
-> Desktop 冻结源文件 identity、大小和 SHA-256
|
||||
-> ShareServer 创建空编辑会话并返回一次性上传授权
|
||||
-> Desktop 流式上传工作副本并校验 SHA-256
|
||||
-> ShareServer 创建 ONLYOFFICE 配置和一次性启动授权
|
||||
-> Desktop 创建文档页签并加载编辑器
|
||||
-> ONLYOFFICE 读取 ShareServer 工作副本
|
||||
-> ready
|
||||
```
|
||||
|
||||
任一步失败都进入确定失败状态。ShareServer 对未完成上传和未启动会话按短期限清理。
|
||||
|
||||
## 4. 保存流程
|
||||
|
||||
```text
|
||||
编辑器提交保存
|
||||
-> ShareServer 接收 ONLYOFFICE 回调
|
||||
-> 校验会话、状态、大小、格式和内容摘要
|
||||
-> 原子发布新保存检查点
|
||||
-> Desktop 得到 checkpoint available 事件
|
||||
-> Desktop 重新读取源文件 identity 和 SHA-256
|
||||
-> 未变化:下载、复核、写临时文件、原子替换
|
||||
-> 已变化:进入 conflict,不覆盖
|
||||
-> Desktop 回报 source saved 或 conflict
|
||||
```
|
||||
|
||||
相同检查点回写使用幂等键。网络超时但替换结果不确定时,Desktop 重新读取源文件摘要确认,
|
||||
不能直接重复写入或声称失败。
|
||||
|
||||
## 5. 冲突决策
|
||||
|
||||
| 条件 | 结果 |
|
||||
| --- | --- |
|
||||
| 源文件与基准一致 | 允许原子回写 |
|
||||
| 源文件不存在 | 显示来源不可用,允许另存为 |
|
||||
| 源文件摘要变化 | 冲突,禁止自动覆盖 |
|
||||
| 源文件只读或授权失效 | 保留检查点,允许重新授权或另存为 |
|
||||
| ShareServer 检查点过期 | 不能回写,说明修改不可恢复 |
|
||||
|
||||
首期冲突操作只有“打开外部版本”“另存当前编辑版本”“放弃编辑版本”和经二次确认的“替换
|
||||
外部版本”。不提供自动 OOXML 合并。
|
||||
|
||||
## 6. AI 编辑流程
|
||||
|
||||
1. 用户在编辑器内发起 AI 编辑,插件冻结当前文档 revision 和选区锚点。
|
||||
2. 选区为空、过大、包含不支持对象或会话非编辑态时拒绝发送。
|
||||
3. Desktop 将有界选区作为用户明确提供的上下文交给当前 Conversation。
|
||||
4. Agent 返回受限结构化编辑操作,不返回可执行脚本。
|
||||
5. 应用前再次比较文档 revision;已变化时要求用户重新选择或重新生成。
|
||||
6. ONLYOFFICE 插件在一个撤销上下文中执行全部操作。
|
||||
7. 任一操作失败则回滚整个事务;成功后高亮修改并使检查点变为 `dirty`。
|
||||
|
||||
AI 编辑继承发起请求的 Ask/Execute 模式。Ask 只生成建议,不调用编辑器写入;Execute 才能
|
||||
应用结构化修改。用户在编辑器中发起操作只授权当前文档和冻结选区,不授予 Agent 其他文件或
|
||||
系统能力。
|
||||
|
||||
## 7. 页签与关闭
|
||||
|
||||
- 每次打开创建新实例;相同源文件可以存在多个编辑会话,但第二个及后续实例默认只读,避免
|
||||
两个独立保存链路互相覆盖。用户关闭原编辑会话后可以将只读实例提升为编辑。
|
||||
- 同名文件使用显示序号区分,序号只属于当前工作栏布局,不写入文件或服务端标题。
|
||||
- 关闭 clean 页签直接结束会话。
|
||||
- 关闭 dirty、saving、available、failed 或 unknown 页签必须根据可恢复事实给出对应选择。
|
||||
- 应用退出复用同一判断;不能等待无限期保存。超时后保留 ShareServer 已确认检查点,并在下次
|
||||
启动显示恢复入口。
|
||||
|
||||
## 8. 断线与撤销
|
||||
|
||||
- Desktop 断线不主动销毁服务端会话;在保留期内可以使用同一会话 ID 重新认证恢复。
|
||||
- ShareServer 与 ONLYOFFICE 断线时禁止新编辑,保留最后稳定检查点和明确错误阶段。
|
||||
- 权限被撤销后立即停止新读取、保存和 AI 操作;已经下载到 Desktop 的本地源文件不由服务端
|
||||
删除。
|
||||
- 撤销只作用于编辑器当前撤销栈。已经回写源文件后执行撤销会产生新的 dirty revision,仍需
|
||||
再次保存才能更新源文件。
|
||||
|
||||
## 9. 需求映射
|
||||
|
||||
| 逻辑 | PRD | 用户故事 |
|
||||
| --- | --- | --- |
|
||||
| 打开和会话创建 | FR-1、FR-2、FR-7、FR-12 | US-A1、US-A2、US-D2 |
|
||||
| 检查点和源文件回写 | FR-4、FR-5 | US-B1 |
|
||||
| 冲突和关闭 | FR-8、FR-10 | US-B2、US-B3 |
|
||||
| AI 编辑事务与模式 | FR-6 | US-C1、US-C2 |
|
||||
| 断线、撤销和保留 | FR-9、FR-11 | US-D1、US-E1 |
|
||||
@@ -0,0 +1,127 @@
|
||||
# Office 协同编辑 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 适用产品 | GoodBuddy Desktop、ShareServer |
|
||||
| 推荐编辑引擎 | ONLYOFFICE Docs Developer Edition,最终采用前完成许可证与分发审核 |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 已能解析、生成和预览部分 Office 文件,但用户无法在客户端内直接修改 DOCX、
|
||||
XLSX 和 PPTX,也无法把当前选区交给 Agent 后在原位置应用修改。自行实现高保真 OOXML
|
||||
编辑器不符合产品投入边界,因此通过 ShareServer 集成成熟的浏览器编辑引擎。
|
||||
|
||||
## 2. 目标
|
||||
|
||||
- Windows、macOS 和 Linux Desktop 使用同一套浏览器编辑能力。
|
||||
- DOCX、XLSX、PPTX 在助手工作栏中以多个独立文档页签打开。
|
||||
- 用户可以手动编辑、保存、撤销和重做。
|
||||
- Agent 可以读取用户明确选择的内容,并将一次 AI 修改作为一个可撤销编辑事务应用。
|
||||
- 本地源文件只在用户明确打开后上传工作副本,保存时通过 Desktop 原子回写。
|
||||
- ShareServer 统一承载编辑器接入、短期会话、工作副本、回调、保留和服务端审计。
|
||||
|
||||
## 3. 非目标
|
||||
|
||||
- 不自行实现 Word、Excel 或 PowerPoint 的排版与渲染引擎。
|
||||
- 不把 ONLYOFFICE Document Server 打入 Desktop 安装包或由 Electron 启动。
|
||||
- 不把编辑能力描述为离线功能;未连接已配置的 ShareServer 时不可创建新编辑会话。
|
||||
- 不允许编辑器浏览任意本机目录,也不把本机绝对路径发送给 ShareServer。
|
||||
- 不把 Node/Python OOXML SDK 当作可视化编辑器;它们后续只用于受控批处理 Provider。
|
||||
- 首期不提供多人实时协作、文档分享链接、移动端编辑或跨组织编辑。
|
||||
- 首期不支持 DOC、XLS、PPT 直接编辑;旧格式转换仍属于文档处理能力。
|
||||
|
||||
## 4. 功能要求
|
||||
|
||||
### FR-1 打开文档
|
||||
|
||||
用户可以从工作区文件、成果或会话文件引用中选择“在文档页签中打开”。Desktop 必须先验证
|
||||
文件范围、扩展名、真实格式、大小和当前 ShareServer 能力,再创建编辑会话。失败时不得上传
|
||||
部分文件或留下可继续访问的会话。
|
||||
|
||||
### FR-2 多实例文档页签
|
||||
|
||||
“文档”是助手工作栏的多实例能力。每次打开创建一个新页签,不把所有文件复用为单例。
|
||||
页签主标题为文件名;同名页签增加序号,完整来源通过 Tooltip 和页签内来源信息显示。
|
||||
|
||||
### FR-3 基础编辑
|
||||
|
||||
编辑器至少支持 DOCX、XLSX 和 PPTX 的打开、人工编辑、保存、撤销、重做、复制粘贴、查找和
|
||||
格式工具。具体格式兼容程度以真实样本验收为准,不因文件能打开就宣称完整兼容。
|
||||
|
||||
### FR-4 保存与回写
|
||||
|
||||
ONLYOFFICE 保存先形成 ShareServer 保存检查点。Desktop 下载并验证检查点后,只有在源文件
|
||||
仍与打开时或上次成功保存后的版本一致时才原子替换源文件。成功后更新本地基准校验和和页签
|
||||
状态。任何中间失败都保留原文件。
|
||||
|
||||
### FR-5 另存为
|
||||
|
||||
用户可以把当前保存检查点另存到当前已授权执行空间内的新路径。目标选择和冲突确认由 Desktop
|
||||
完成;ShareServer 和编辑器不能决定本机路径。
|
||||
|
||||
### FR-6 AI 选区编辑
|
||||
|
||||
用户在编辑器中明确选择文字、单元格、对象、页面或幻灯片后,可以发起 AI 编辑。编辑器只提供
|
||||
本次选区的有界结构和必要上下文。Agent 返回编辑器支持的结构化操作,由编辑器插件应用;一次
|
||||
请求形成一个撤销事务,并在完成后标出修改位置。只有 Execute 模式可以应用修改;Ask 模式
|
||||
可以生成建议,但不得调用编辑器写入。
|
||||
|
||||
### FR-7 权限与数据去向
|
||||
|
||||
创建会话前持续显示目标 ShareServer、将上传的文件、服务端可读取文档内容以及默认保留期限。
|
||||
组织策略可以禁止格式、限制文件大小、强制只读或禁止 AI 编辑。Desktop 偏好只能收紧策略。
|
||||
|
||||
### FR-8 版本冲突
|
||||
|
||||
源文件在编辑期间被其他程序修改时,Desktop 不得覆盖。用户可以重新加载外部版本、将编辑
|
||||
检查点另存为新文件,或显式比较后选择替换;首期不自动合并二进制 Office 文件。
|
||||
|
||||
### FR-9 断线与恢复
|
||||
|
||||
短暂断线不关闭页签。界面区分 Desktop 到 ShareServer、ShareServer 到 ONLYOFFICE 和保存回调
|
||||
故障。只有 ShareServer 确认存在稳定检查点时才显示“可恢复”;未知结果不得显示为已保存。
|
||||
|
||||
### FR-10 关闭
|
||||
|
||||
没有未回写修改时直接关闭。存在 ShareServer 检查点但尚未回写源文件时,用户选择“保存并
|
||||
关闭”“放弃修改”或“取消”。放弃只结束编辑会话和删除临时工作副本,不删除源文件。
|
||||
|
||||
### FR-11 审计
|
||||
|
||||
Desktop 记录本地打开、保存、另存为、冲突、AI 编辑请求和终态。ShareServer 记录组织、主体、
|
||||
会话、文档类型、字节数、策略、编辑器版本、保存版本和保留清理,不默认记录选区正文、完整
|
||||
文档内容、提示词或模型输出。
|
||||
|
||||
### FR-12 服务配置
|
||||
|
||||
Desktop 只连接用户已明确添加并认证的 ShareServer。ONLYOFFICE 地址、JWT 密钥、对象存储和
|
||||
回调配置由 ShareServer 管理,不进入 Desktop Renderer 或普通设置导出。
|
||||
|
||||
## 5. 文件范围
|
||||
|
||||
首期可编辑格式:
|
||||
|
||||
| 类型 | 格式 | 编辑器类型 |
|
||||
| --- | --- | --- |
|
||||
| 文档 | `.docx` | `word` |
|
||||
| 表格 | `.xlsx` | `cell` |
|
||||
| 演示文稿 | `.pptx` | `slide` |
|
||||
|
||||
PDF 保持预览或文档处理用途,不进入首期 Office 编辑。宏格式、模板格式、受密码保护文件和包含
|
||||
活动内容的文件默认拒绝,后续必须单独完成格式、安全和保存兼容性设计。
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
- 三个 Desktop 操作系统均能从同一 ShareServer 打开、编辑和保存三种首期格式。
|
||||
- 同一工作栏至少可以同时保留两个独立文档页签,切换不会串文件、选区或保存状态。
|
||||
- 同名文件的页签可辨认,页签标题和完整来源一致。
|
||||
- 保存失败、回调失败和源文件冲突均不会破坏原文件。
|
||||
- 每次 AI 修改可以单次撤销,拒绝或取消不会产生部分修改。
|
||||
- ShareServer 不可用时已有本地文件仍可由默认应用打开,但不能伪装成内置编辑可用。
|
||||
- Renderer、编辑器页面和日志中均不存在本机绝对路径、ShareServer 管理密钥或长期文件令牌。
|
||||
- 文档会话过期后,旧启动 URL、文件 URL 和回调令牌均不可重用。
|
||||
@@ -0,0 +1,217 @@
|
||||
# Office 协同编辑技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-09 |
|
||||
| 关联 PRD | [Office 协同编辑 PRD](./prd.md) |
|
||||
| 跨端架构 | [ShareServer 联邦网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
|
||||
## 1. 技术选型
|
||||
|
||||
首个 Provider 使用 ONLYOFFICE Docs Developer Edition。采用前必须确认当前许可证、桌面应用
|
||||
分发权、并发连接口径、白标、Automation API、离线私有部署和安全支持;技术设计不构成授权
|
||||
结论。
|
||||
|
||||
不采用:
|
||||
|
||||
- ONLYOFFICE Desktop Editors:它是独立桌面产品,不是 GoodBuddy 工作栏嵌入 SDK。
|
||||
- Node/Python OOXML 库:只提供生成或结构化处理,不提供高保真画布、选区和撤销栈。
|
||||
- Microsoft WOPI:第三方接入资格和云授权不适合作为 GoodBuddy 默认能力。
|
||||
- 自研 OOXML 渲染器:格式兼容和编辑成本超出当前产品边界。
|
||||
|
||||
后续 Provider 可以增加 Univer Sheets,但只能按通过验收的格式和能力声明,不能由“纯前端”推导
|
||||
为完整 Office 兼容。
|
||||
|
||||
## 2. 组件边界
|
||||
|
||||
```text
|
||||
GoodBuddy Renderer
|
||||
└─ 文档 Tab 外壳、公开状态、用户命令
|
||||
|
||||
GoodBuddy Main
|
||||
├─ DocumentEditSessionService
|
||||
├─ 本地/SSH 源文件 Grant 与校验
|
||||
├─ 上传、下载、原子回写和冲突检测
|
||||
├─ OfficeEditorView 生命周期
|
||||
└─ 本地活动记录
|
||||
|
||||
ShareServer
|
||||
├─ 用户、设备、组织和策略认证
|
||||
├─ Office 会话 API 与短期启动授权
|
||||
├─ 工作副本、保存检查点和保留清理
|
||||
├─ ONLYOFFICE JWT、文件 URL 和 callback handler
|
||||
├─ 编辑器版本、健康、配额和审计
|
||||
└─ 可选 AI 编辑消息中继
|
||||
|
||||
ONLYOFFICE Document Server
|
||||
└─ Word、Spreadsheet、Presentation Web Editors
|
||||
```
|
||||
|
||||
ShareServer 可以把 ONLYOFFICE 和对象存储作为同一部署套件的依赖管理,但两者仍是独立服务。
|
||||
Desktop 不读取 Document Server 地址或密钥,只消费 ShareServer 返回的一次性编辑启动 URL。
|
||||
|
||||
## 3. 工作栏实例
|
||||
|
||||
在工作栏应用注册表新增多实例 `document`:
|
||||
|
||||
```ts
|
||||
type DocumentPanelInstance = {
|
||||
id: string
|
||||
capability: 'document'
|
||||
documentSessionId: string
|
||||
displayName: string
|
||||
duplicateIndex?: number
|
||||
binding: { mode: 'pinned'; target: { type: 'document-session'; id: string } }
|
||||
dock: 'right' | 'bottom' | 'window'
|
||||
}
|
||||
```
|
||||
|
||||
持久布局只保存公开会话 ID、显示名、来源对象 ID 和停靠方式,不保存启动 URL、Cookie、文件
|
||||
令牌、绝对路径或 ONLYOFFICE 配置。应用恢复时由 Main 重新认证并判断会话能否恢复。
|
||||
|
||||
编辑器内容不使用现有静态 HTML `iframe sandbox=""`。Main 为每个已加载会话创建独立
|
||||
`WebContentsView`,固定允许的 ShareServer origin、隔离 session partition、关闭 Node
|
||||
integration、开启 context isolation 和 sandbox,并禁用新窗口、任意下载、权限请求和非允许
|
||||
导航。远端页面不获得通用 Electron preload API。
|
||||
|
||||
多页签允许多个会话描述同时存在;只有经过资源验收的有限数量编辑器保持加载。非活动实例的
|
||||
具体挂起和恢复阈值在跨平台内存测试后冻结,不能未经保存检查点直接销毁 dirty 编辑器。
|
||||
|
||||
## 4. ShareServer API 边界
|
||||
|
||||
建议的服务端资源:
|
||||
|
||||
```text
|
||||
POST /api/v1/office/sessions
|
||||
PUT /api/v1/office/sessions/{id}/source
|
||||
POST /api/v1/office/sessions/{id}/launch
|
||||
GET /api/v1/office/sessions/{id}
|
||||
GET /api/v1/office/sessions/{id}/events
|
||||
GET /api/v1/office/sessions/{id}/checkpoints/{revision}
|
||||
POST /api/v1/office/sessions/{id}/source-commit
|
||||
DELETE /api/v1/office/sessions/{id}
|
||||
|
||||
GET /internal/office/files/{objectToken}
|
||||
POST /internal/office/callback/{callbackToken}
|
||||
```
|
||||
|
||||
Desktop API 使用设备凭据和组织 audience。启动 URL 使用一次性 code,交换为 HttpOnly、Secure、
|
||||
SameSite Cookie 后立即失效。ONLYOFFICE 文件 URL、callback URL 和 JWT 使用独立 audience、短
|
||||
有效期及单会话绑定,不能用于普通 ShareServer API。
|
||||
|
||||
ShareServer 不把对象存储公开 URL 直接返回 Renderer。文件下载必须重新验证用户、设备、会话、
|
||||
revision 和策略,并返回声明大小和 SHA-256。
|
||||
|
||||
## 5. 会话契约
|
||||
|
||||
```ts
|
||||
type CreateDocumentEditSession = {
|
||||
source: {
|
||||
kind: 'workspace-file' | 'artifact'
|
||||
publicSourceId: string
|
||||
displayName: string
|
||||
mediaType: string
|
||||
size: number
|
||||
sha256: string
|
||||
}
|
||||
requestedAccess: 'edit' | 'read-only'
|
||||
}
|
||||
|
||||
type DocumentEditSessionSummary = {
|
||||
id: string
|
||||
state: 'uploading' | 'ready' | 'editing' | 'offline' | 'expired' | 'closed'
|
||||
access: 'edit' | 'read-only'
|
||||
documentType: 'word' | 'cell' | 'slide'
|
||||
displayName: string
|
||||
sourceRevision: string
|
||||
checkpoint?: {
|
||||
revision: string
|
||||
size: number
|
||||
sha256: string
|
||||
createdAt: string
|
||||
}
|
||||
retentionExpiresAt: string
|
||||
}
|
||||
```
|
||||
|
||||
远端值全部经过有界 schema 校验。`publicSourceId` 是 Desktop 管理的对象 ID,不是文件路径。
|
||||
|
||||
## 6. 文件与保存
|
||||
|
||||
- Desktop 从已授权本地或 SSH 源读取并流式计算大小和 SHA-256。
|
||||
- 上传目标由 Main 从固定 ShareServer API 获得,不接受 Renderer 提供 URL。
|
||||
- ShareServer 写入随机对象键,拒绝用户文件名决定存储路径。
|
||||
- ONLYOFFICE callback 先写临时对象,校验后原子发布为新 checkpoint。
|
||||
- Desktop 下载 checkpoint 到 GoodBuddy-owned 暂存,复核大小、SHA-256、普通文件属性和文件
|
||||
magic,再在源文件同目录创建临时文件并原子替换。
|
||||
- SSH 源文件由远程 GoodBuddy Agent 执行相同摘要、冲突和原子替换,不把远程凭据交给
|
||||
ShareServer。
|
||||
- 每次成功回写后 Desktop 将新摘要作为下一次冲突检测基准。
|
||||
|
||||
## 7. 编辑器桥与 AI 编辑
|
||||
|
||||
ShareServer 托管固定版本的 GoodBuddy ONLYOFFICE 插件。插件只能:
|
||||
|
||||
- 获取当前会话和有界选区描述;
|
||||
- 发起 AI 编辑请求并显示状态;
|
||||
- 接收受限结构化编辑操作;
|
||||
- 在一个编辑器撤销上下文中应用、回滚和高亮修改;
|
||||
- 发布不含正文的 dirty、save 和 revision 事件。
|
||||
|
||||
首期优先让插件通过 ShareServer 会话通道与 Desktop 交换事件,不给远端编辑页面添加通用
|
||||
Electron preload。ShareServer 只中继有界事件;Agent 请求仍由 Desktop 创建并经过现有
|
||||
Conversation、模型、Ask/Execute、取消和活动记录边界。
|
||||
|
||||
Ask 请求可以返回可复制的修改建议,但 Main 拒绝向插件下发写操作。只有 Execute 请求能够
|
||||
取得绑定当前文档会话、revision 和选区的短期编辑 Grant;该 Grant 不包含源文件路径,也不能
|
||||
用于其他文档、文件工具或系统能力。
|
||||
|
||||
结构化操作按文档类型单独列白名单,例如文本替换、单元格值或公式更新、幻灯片对象文字
|
||||
替换。禁止传入 JavaScript、宏、任意 ONLYOFFICE 方法名或表达式求值。
|
||||
|
||||
## 8. 部署与配置
|
||||
|
||||
ShareServer 部署清单新增可选 Office 组件:
|
||||
|
||||
```text
|
||||
ShareServer API
|
||||
PostgreSQL
|
||||
兼容 S3 的对象存储
|
||||
ONLYOFFICE Document Server
|
||||
Office callback worker
|
||||
```
|
||||
|
||||
服务端管理配置包含 Document Server 内部地址、公开编辑 origin、JWT 密钥引用、对象存储、
|
||||
允许格式、文件大小、并发、会话期限和 checkpoint 保留。管理界面提供真实健康检查:创建最小
|
||||
文档、打开编辑器、执行保存回调并核对输出,而不是只检查 HTTP 端口。
|
||||
|
||||
Desktop 仅显示某个 ShareServer 是否声明并实际通过 `office.edit` 能力检查。未配置或不健康时
|
||||
保留文档入口并说明不可用原因。
|
||||
|
||||
## 9. 安全边界
|
||||
|
||||
1. ShareServer 和 ONLYOFFICE 之间使用独立网络和凭据,不公开管理接口。
|
||||
2. 文档内容是不可信输入;禁用宏、外部插件、任意外链加载和 Document Server 出站网络。
|
||||
3. 每个文件 URL、callback 和启动授权绑定单会话、单用途和短期限。
|
||||
4. Renderer 不接触设备凭据、ONLYOFFICE JWT、对象存储凭据或本机绝对路径。
|
||||
5. 编辑器 origin 不能调用任意 IPC、Shell、文件系统、浏览器或 Runtime 工具。
|
||||
6. 上传、下载、callback、事件和 AI 操作限制字节数、频率、并发和总时长。
|
||||
7. 服务端保留策略到期后清理工作副本、checkpoint、编辑 Cookie 和对象令牌;审计元数据按组织
|
||||
策略独立保留。
|
||||
8. 组织管理员能够撤销 Office 能力和活动会话;撤销后 Desktop 仍保留本地源文件,不把服务端
|
||||
删除描述为本地删除。
|
||||
|
||||
## 10. 实施顺序
|
||||
|
||||
1. 在 ShareServer 独立仓库补充 Office 服务 PRD、部署配置、对象存储和 ONLYOFFICE 许可证决策。
|
||||
2. 实现服务端最小会话、上传、启动、callback、checkpoint、下载和清理闭环。
|
||||
3. Desktop 增加多实例文档页签和只读打开,完成三平台真实视觉及资源验证。
|
||||
4. 接入源文件冲突检测、原子保存、另存为、关闭和恢复。
|
||||
5. 实现固定 ONLYOFFICE 插件及单次可撤销 AI 选区修改。
|
||||
6. 增加组织策略、配额、审计和生产部署验收。
|
||||
|
||||
不得在服务端闭环、许可证和三平台真实编辑验收前把该能力标记为已提供。
|
||||
@@ -0,0 +1,83 @@
|
||||
# Office 协同编辑界面设计
|
||||
|
||||
## 1. 信息架构
|
||||
|
||||
Office 文档不是一级页面,也不是成果面板中的内嵌卡片。它是助手工作栏中可创建多个实例的
|
||||
“文档”应用。打开文件后新增文档页签,页签关闭只结束对应编辑会话。
|
||||
|
||||
```text
|
||||
助手工作栏 Tab 行
|
||||
[任务] [工作区] [预算.xlsx] [方案.docx] [+]
|
||||
└─ 当前文档编辑器
|
||||
```
|
||||
|
||||
“+”能力目录持续提供“文档”入口。用户从目录打开时选择已授权文件;从工作区、成果或会话文件
|
||||
引用打开时直接创建实例。
|
||||
|
||||
## 2. 页签
|
||||
|
||||
- 图标按文档、表格和演示文稿类型显示,使用现有图标库。
|
||||
- 可见标题使用文件名,不显示完整路径。
|
||||
- 同名实例依次显示 `预算.xlsx`、`预算.xlsx · 2`。
|
||||
- Tooltip 显示完整文件名、项目或成果来源、ShareServer 名称和只读状态,不显示本机绝对路径。
|
||||
- `aria-label` 包含文件名、序号和状态。
|
||||
- 加载、未保存、保存中、冲突和断线使用状态图标及可访问名称,不只依赖颜色。
|
||||
- 文档应用为 `multiple`,不因已有文档页签而聚焦或替换其他实例。
|
||||
|
||||
## 3. 文档标题栏
|
||||
|
||||
编辑器上方保留 GoodBuddy 控制的紧凑标题栏:
|
||||
|
||||
- 左侧:文件名、来源范围、只读或冲突状态。
|
||||
- 中部:最近一次源文件保存结果。
|
||||
- 右侧:保存、另存为、刷新状态、在独立窗口打开和更多菜单。
|
||||
|
||||
撤销、重做、格式、查找和文档内部编辑命令使用 ONLYOFFICE 工具栏,不在 GoodBuddy 标题栏
|
||||
重复。保存按钮必须区分“正在保存到 ShareServer”和“已写回源文件”。
|
||||
|
||||
## 4. 尺寸与停靠
|
||||
|
||||
文档页签沿用工作栏右侧、底部和独立窗口三种停靠方式,但编辑器有额外宽度要求:
|
||||
|
||||
| 可用宽度 | 行为 |
|
||||
| --- | --- |
|
||||
| `>= 720px` | 完整编辑器与完整工具栏 |
|
||||
| `480px - 719px` | 编辑器精简工具栏,保留核心编辑命令 |
|
||||
| `< 480px` | 不加载可编辑画布,显示文件摘要、状态和“展开编辑”命令 |
|
||||
|
||||
右侧工作栏达到普通 `300px` 最小宽度时不能把完整 Office 工具栏压缩到重叠。用户选择“展开
|
||||
编辑”时切换到主内容区或独立窗口,仍复用原文档页签和编辑会话,不创建第二份文档。
|
||||
|
||||
文档画布、公式栏、幻灯片缩略图和滚动区域使用稳定尺寸;编辑器加载、状态文字或长文件名不得
|
||||
推动 Tab 行和标题栏重新布局。
|
||||
|
||||
## 5. 状态与反馈
|
||||
|
||||
| 状态 | 可见反馈 | 允许操作 |
|
||||
| --- | --- | --- |
|
||||
| 上传中 | 上传进度、目标 ShareServer、取消 | 取消 |
|
||||
| 编辑中 | 编辑器、来源和最近保存时间 | 编辑、AI 编辑、保存 |
|
||||
| 只读 | 持续只读标识和原因 | 查看、另存为 |
|
||||
| 保存中 | “正在创建检查点”或“正在写回源文件” | 不重复保存,可继续查看 |
|
||||
| 冲突 | 持续警告和两个版本的时间、大小 | 打开外部版本、另存、替换 |
|
||||
| 断线 | 明确失败阶段和重试 | 查看最后已加载内容、重连 |
|
||||
| 会话过期 | 修改是否存在稳定检查点 | 恢复检查点或关闭 |
|
||||
|
||||
通知不能代替页签内持续状态。后台文档保存完成只更新页签状态和应用通知,不抢占当前页签。
|
||||
|
||||
## 6. AI 编辑
|
||||
|
||||
- AI 编辑入口出现在 ONLYOFFICE 选区工具栏或 GoodBuddy 编辑器插件中。
|
||||
- 点击后使用紧凑指令输入框;当前选区类型和范围持续可见。
|
||||
- 发送前显示将使用的 Conversation、模型和 ShareServer,不展示内部选区 ID。
|
||||
- 处理中锁定本次选区事务,不锁定整个文档的阅读和滚动。
|
||||
- 成功后高亮所有修改位置,并显示可用的撤销命令。
|
||||
- 文档 revision 已变化时不静默套用旧结果,要求重新选择或重新生成。
|
||||
|
||||
## 7. 可访问性与主题
|
||||
|
||||
- GoodBuddy 外壳遵循根级 `UI-DESIGN.md` 的主题和焦点规则。
|
||||
- 编辑器主题跟随应用主题,但第三方编辑器无法完整匹配时优先保证可读性,不注入脆弱的 DOM
|
||||
样式覆盖。
|
||||
- Tab、标题栏和冲突对话框支持完整键盘操作及明确焦点恢复。
|
||||
- 第三方编辑器页面必须提供可识别标题;加载失败后焦点回到重试命令。
|
||||
@@ -0,0 +1,85 @@
|
||||
# Office 协同编辑用户故事
|
||||
|
||||
## US-A1 打开本地文档
|
||||
|
||||
**Given** 用户已连接支持 Office 编辑的 ShareServer,并在工作区选择一个受支持文件
|
||||
|
||||
**When** 用户选择“在文档页签中打开”
|
||||
|
||||
**Then** GoodBuddy 显示目标服务和数据去向,验证并上传工作副本,然后以文件名创建新文档
|
||||
页签;失败不会修改源文件或留下可重用文件授权。
|
||||
|
||||
## US-A2 同时打开多个文档
|
||||
|
||||
**Given** 工作栏已经存在一个文档页签
|
||||
|
||||
**When** 用户打开另一个文档或再次打开同一文档
|
||||
|
||||
**Then** 工作栏创建独立页签,不替换现有实例;同名页签可辨认,文件、选区和保存状态不串联。
|
||||
|
||||
## US-B1 人工编辑并保存
|
||||
|
||||
**Given** 文档编辑器已进入可编辑状态
|
||||
|
||||
**When** 用户修改内容并保存
|
||||
|
||||
**Then** ShareServer 先形成稳定检查点,Desktop 确认源文件未被外部修改后原子回写,并明确
|
||||
显示“已写回源文件”。
|
||||
|
||||
## US-B2 源文件发生冲突
|
||||
|
||||
**Given** 用户编辑期间源文件被其他程序修改
|
||||
|
||||
**When** 新检查点准备回写
|
||||
|
||||
**Then** GoodBuddy 禁止静默覆盖,并允许用户查看外部版本、另存编辑版本、放弃编辑版本或经
|
||||
二次确认替换。
|
||||
|
||||
## US-B3 关闭未保存文档
|
||||
|
||||
**Given** 页签存在未形成检查点或尚未回写源文件的修改
|
||||
|
||||
**When** 用户关闭页签或退出应用
|
||||
|
||||
**Then** GoodBuddy 根据真实可恢复状态提供保存、放弃或取消,不把未知结果显示为已保存。
|
||||
|
||||
## US-C1 使用 AI 修改选区
|
||||
|
||||
**Given** 当前 Conversation 使用 Execute,用户在编辑器中选择受支持内容
|
||||
|
||||
**When** 用户提交 AI 修改要求
|
||||
|
||||
**Then** Agent 只接收有界选区和必要上下文,编辑器将完整结果作为一个事务应用并允许单次撤销。
|
||||
|
||||
## US-C2 Ask 模式请求修改
|
||||
|
||||
**Given** 当前 Conversation 使用 Ask
|
||||
|
||||
**When** 用户请求 AI 修改当前选区
|
||||
|
||||
**Then** GoodBuddy 可以生成修改建议,但不会调用编辑器写入;界面提供切换 Execute 后重新应用
|
||||
的明确入口。
|
||||
|
||||
## US-D1 网络中断后恢复
|
||||
|
||||
**Given** 编辑会话已经建立
|
||||
|
||||
**When** Desktop、ShareServer 或 ONLYOFFICE 之间的连接中断后恢复
|
||||
|
||||
**Then** 页签保留并显示失败阶段;只有 ShareServer 确认存在稳定检查点时才提供恢复和回写。
|
||||
|
||||
## US-D2 服务未配置或不健康
|
||||
|
||||
**Given** 用户没有连接支持 Office 编辑的 ShareServer,或服务健康检查失败
|
||||
|
||||
**When** 用户打开文档能力
|
||||
|
||||
**Then** 入口仍可发现并解释不可用原因,同时保留使用系统默认应用打开本地文件的独立操作。
|
||||
|
||||
## US-E1 权限撤销和保留到期
|
||||
|
||||
**Given** 管理员撤销编辑权限,或编辑会话达到保留期限
|
||||
|
||||
**When** 用户继续访问旧页签
|
||||
|
||||
**Then** 新读取、保存和 AI 操作被拒绝,旧令牌不能重用,本地源文件不受服务端清理影响。
|
||||
@@ -11,7 +11,8 @@
|
||||
|
||||
## 1. 背景
|
||||
|
||||
GoodBuddy 已能把一个请求并行委派给最多三个只读专家,再综合结果。这适合“一次请求,
|
||||
GoodBuddy 已能把一个请求并行委派给最多三个专家,再综合结果;专家继承 Ask/Execute
|
||||
并使用本机直连模型工具,Ask 仍只读。这适合“一次请求,
|
||||
多种专业视角”,但不等同于实验:当前没有结构化变量、重复运行、统一指标、结果晋升和
|
||||
Run 级记忆隔离。
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ bootstrap 能力元数据。公开直连能力仍须完成本文末尾 GitHub/
|
||||
|
||||
探测在 Host Key 已固定且 SSH 认证成功后执行,只检查当前选择的“关于与更新”来源:
|
||||
|
||||
- Host 是受支持的 Linux `x64` 或 `arm64`。
|
||||
- Host 是 Linux `x64`、Linux `arm64` 或 macOS `arm64`;Intel Mac 不在支持范围。
|
||||
- SSH 账号 Home 可写,GoodBuddy-owned staging 可创建。
|
||||
- 可用空间满足签名目录声明的包大小、解包上限和安全余量。
|
||||
- Host 可以通过 HTTPS 访问所选 GitHub 或北京镜像 URL。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 状态
|
||||
|
||||
本文记录截至 2026-09-05 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
|
||||
本文记录截至 2026-09-10 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
|
||||
Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Host current 环境”已经完成源码接线,
|
||||
详细事务与验收边界见
|
||||
[SSH Host 远程环境准备与直连下载设计](./environment-provisioning-technical-design.md);
|
||||
@@ -11,8 +11,13 @@ Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Ho
|
||||
Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
|
||||
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
|
||||
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
|
||||
`0.11.14` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
|
||||
Agent 源码 lock 为 `0.11.18`,Desktop 候选为 `0.12.4`,等待各自独立发布审批。
|
||||
`0.11.14` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前源码
|
||||
候选为 Agent `0.11.24`、Desktop `0.13.0`,修复远端原生问答转交和取消待答后的同会话
|
||||
续发。Agent `0.11.24` 要求先升级至 Desktop `0.13.0`;候选尚未发布。远程文件/Git 管理、
|
||||
可选模型限额、无限请求时长下的独立连接超时和移除固定十分钟 Prompt 时限由
|
||||
Agent `0.11.23` 提供,显式请求期限与取消仍有效。当前问答源码的真实 SSH/确定性模型证据
|
||||
及完整生产链路验证边界见[工作栏进度](../assistant-workbar/progress.md)。
|
||||
正式发布状态以 Agent 与 Desktop 独立发布渠道为准。
|
||||
现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
|
||||
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
|
||||
|
||||
@@ -36,6 +41,12 @@ profile 和凭据作为有界、逐 Prompt 的控制消息交给 Agent;该凭
|
||||
|
||||
## 组件
|
||||
|
||||
桌面保存托管 SSH 子任务事件时,共用
|
||||
[执行记录差量存储](../assistant-workbar/execution-history-storage.md)。
|
||||
Agent transcript 与公共 Runtime 事件不改变;桌面的来源去重、消息投影事务和恢复序号
|
||||
继续保留。工具字段比较与 schema 35 对 schema 34 重复块的整理也在该共享桌面层完成;
|
||||
不把工具过程改为仅存内存,不要求单独升级 Agent。
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> 窄 Preload / Main IPC
|
||||
@@ -67,8 +78,16 @@ Detached GoodBuddy Agent
|
||||
- Host 地址、用户或 Host Key generation 变化时关闭旧连接,并定向退役依赖旧 Host identity 的 Workspace 和 Runtime 会话。下一次选择使用 Host 管理的当前连接记录解析 current Agent/Runtime,不读取项目中的旧 revision 或组件 identity;当前 registry、连接或 capability 无效时才要求显式修复。
|
||||
|
||||
## Agent 安装与生命周期
|
||||
- 托管 OpenCode 的直接 ACP profile 与模型桥最终配置都设置 `snapshot: false`,避免
|
||||
GoodBuddy 不使用的自动 Git 快照摘要阻塞原生执行;策略和保留的文件、Git、子代理能力
|
||||
见 [Runtime 并行与输出边界](../assistant-workbar/runtime-interactions.md#托管-runtime-的并行与输出)。
|
||||
此设置随新启动的 Agent Runtime 生效,不改写用户仓库或已运行进程的配置。
|
||||
|
||||
- Agent bundle 通过 manifest、Ed25519 签名、payload digest、平台和架构校验。
|
||||
- 外层 `agent-package.json` 描述符上限为 4 MiB,覆盖包含 OpenCode 离线依赖的实际包
|
||||
(2026-09-08 Linux x64 验证包为 1,101,863 字节)。Desktop 解包、Host prepare/commit
|
||||
与目录生成器使用相同上限;其余元数据仍为 1 MiB,签名、文件数量、归档大小和完整性校验不变。
|
||||
- Runtime 组包使用的 OpenCode 离线依赖缓存按插件版本复用,安装与替换规则见 [Runtime 资源](../../../BUILD.md#runtime-资源)。复制进 Runtime 包的依赖仍属于签名 manifest 的 payload。
|
||||
- 安装使用当前 SSH 用户目录中的 GoodBuddy-owned 路径和 side-by-side digest 目录。
|
||||
- Agent 代码与固定 Node Runtime 一起位于签名 compound payload。显式安装或重装从本次
|
||||
已完整校验的准备目录原子发布整个 Agent,不扫描旧安装、不创建跨版本 Node 硬链接;
|
||||
@@ -94,6 +113,9 @@ Detached GoodBuddy Agent
|
||||
- Agent 是按需启动的 detached process,不注册开机服务,不依赖 systemd、D-Bus 或 Linger。
|
||||
- 每个模型桥 helper 都为 loopback HTTP 入口生成一次性随机路径 capability;只有写入当前 OpenCode 子进程配置的 URL 可以访问该入口,其他本机用户即使发现临时端口也不能提交模型请求。
|
||||
- Agent 监听当前用户拥有的私有 Unix socket。SSH 中断只关闭 relay,不拥有 Agent 和活动 Runtime 的生命周期。
|
||||
- 私有握手的 HMAC 响应按固定 32 字节的规范 Base64URL 校验,不套用要求字母数字开头
|
||||
的应用 ID 规则;health nonce 使用固定前缀。读取握手时,流结束或关闭都立即失败,
|
||||
不留下未结算 Promise;Host Key、同 UID 校验和 HMAC 验证保持不变。
|
||||
- SSH Host 设置通过固定探针和有界 SFTP 显示 Host 已登记的 Agent/OpenCode 版本,并与当前
|
||||
GoodBuddy 所需版本比较。进入或切回该设置页只读取本地 Host 列表,不连接任何 Host;
|
||||
用户点击“刷新版本”后才探测对应 Host,探测不安装或切换远端组件。项目切换与此按钮
|
||||
@@ -104,6 +126,11 @@ Detached GoodBuddy Agent
|
||||
小型签名目录。Renderer 只得到本地/在线版本、是否有更新、架构、远端 Runtime 版本、
|
||||
协议和本地状态,不得到缓存路径、key ID 或 digest。目录检查不下载 `.gbagent`,在线
|
||||
包下载只由用户点击带目标版本的操作触发;离线导入/导出通过 Main 管理的文件对话框完成。
|
||||
- GitHub 来源按每页 10 条读取 Release 元数据,最多检查原有的 100 条历史发布,
|
||||
找到首个有效 Agent 目录及签名地址后立即停止。每次响应仍保留 1 MiB 上限,
|
||||
避免一次读取全部历史正文和资产详情导致目录检查失败;签名、URL 与包校验不变。
|
||||
2026-09-06 使用当前 Main 目录读取实现验证公开 GitHub 与北京镜像,两者均可读取
|
||||
Agent `0.11.19` 的 Linux x64、Linux arm64 和 macOS arm64 条目,未下载或安装包。
|
||||
- “自动”只在 operation/prepare 前用 Host capability probe 选择 acquisition:直连明确
|
||||
可用才选 Host 下载,否则选 GoodBuddy 传输;显式选择不被改写。prepare、commit 或
|
||||
adoption 失败后都不跨 acquisition 自动 fallback。
|
||||
@@ -190,6 +217,10 @@ Ask 与 Execute 一样直接启动已签名 Runtime,不要求 Host 安装额
|
||||
- Ask 启动时同时注入 OpenCode 顶层和 build Agent 的 `permission: "ask"`;Agent
|
||||
工具权限分发边界只批准上述原生读取请求。该双重边界不另加文件系统 confinement,
|
||||
也不改变 Runtime 的直接启动方式。
|
||||
- Execute 的直接启动与模型桥启动均显式设置顶层和 build Agent 的 `permission: "allow"`,
|
||||
避免继承 OpenCode 默认的 `external_directory` 询问;项目目录只是默认工作目录。
|
||||
原生子代理同样使用当前 SSH 账号可访问的路径。仍出现的 ACP 权限请求由 Agent 按
|
||||
当前 Prompt 模式自动答复,不增加 Desktop 人工审批,也不依赖日志解析。
|
||||
|
||||
### Execute
|
||||
|
||||
@@ -233,7 +264,46 @@ Execute 直接启动已签名 Runtime:
|
||||
- OpenCode 原生 Task 工具按 `subagent_type`、`description`、`prompt` 和稳定 tool call ID
|
||||
解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP
|
||||
首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent
|
||||
状态卡,并持久化任务说明、终态与输出。
|
||||
状态卡,并持久化任务说明、终态与输出。卡片展开后分为“执行过程”和“最终结果”:
|
||||
正文、折叠思考与可展开工具记录按收到的顺序显示,工具状态原位更新;完成后仅在最终
|
||||
结果区显示与末尾正文完全相同的返回结果,避免重复。失败或取消保留已有过程,未终结
|
||||
工具不再显示进行中。
|
||||
- 本地 SDK 通过 Task `metadata.sessionId` 关联子会话,复用稳定消息块 ID 保存过程。
|
||||
固定 OpenCode 1.18.29 的 ACP 不转发子会话,因此 Agent 的模型桥 helper 在受管临时目录
|
||||
写入事件插件,并通过既有 ACP `session/update` 的 `_meta.goodbuddySubagentEvent`
|
||||
转发到所属 Task;不修改签名 Runtime 包。插件文件随 helper 退出清理,
|
||||
事件继续经过既有 Agent transcript、ACK 和 Desktop 消息持久化路径。
|
||||
- 托管 helper 显式设置 `OPENCODE_ENABLE_QUESTION_TOOL=true`,启用 ACP 默认隐藏的原生
|
||||
问题工具。插件只处理 `question.asked`,通过 Session `parentID` 逐级确认根会话,并核对
|
||||
`/question` 当前待答列表;不从工具进度或正文构造问题。父、子孙会话问题使用
|
||||
`_meta.goodbuddyQuestion` 送到所属 ACP 会话,有原生 Task 关联时附带对应 call ID。
|
||||
- Agent 按当前 ACP 会话和 Prompt 保存待答映射,Desktop 公开 ID 包含 binding、operation
|
||||
和原生问题 ID。回答与跳过经既有认证控制通道的 `runtime/respondToQuestion` 发送;后端
|
||||
检查 controller、generation、connection、binding 和活动 operation,再调用插件仅绑定
|
||||
`127.0.0.1` 的临时 HTTP 入口。该入口使用进程级随机路径,能力 URL 不写入语义记录。
|
||||
插件复用 OpenCode 注入客户端的进程内 HTTP 适配器调用 `/question/{id}/reply` 或
|
||||
`/reject`;空答案数组表示拒答,非空答案数量须与问题数量一致。问答不经过 ACP 权限审批。
|
||||
用户取消托管 Prompt 时,Agent 同步记录语义终态和操作终态。Desktop 等待取消升级及
|
||||
终态核对后关闭旧 binding、释放会话通道;同一对话的下一条消息使用新 binding,避免
|
||||
将新请求写入仍携带旧 operation ID 的 binding。断线附加原请求的规则保持不变。
|
||||
- 语义记录分页另带 Agent 当前 `pendingQuestions`,用于附加同一存活 Prompt 后恢复待答
|
||||
问题;历史问答元数据不重新生成表单。原生已回答、拒答、会话空闲、取消、Prompt 终结
|
||||
和进程退出清除相应映射,迟到的回答报错。回复失败保留桌面输入供重试,答案不会跨连接
|
||||
自动重放。插件随 OpenCode 实例释放临时端口,helper 退出时删除插件文件。
|
||||
以上只适用于当前 Agent 管理的 SSH OpenCode 生产路径,不声明任意 ACP 服务的问答兼容性。
|
||||
真实二进制、SSH 传输和取消验证见[工作栏进度](../assistant-workbar/progress.md#2026-09-13-托管-ssh-原生问答)。
|
||||
- ACP 工具结果同时支持文本 `content` 和 `rawOutput.output`,移除 OpenCode 的
|
||||
`<task><task_result>` 外层包装后渲染正文。恢复已有子代理时使用 ACP 所带的所属
|
||||
`toolCallId` 继续路由,不等待新的父 Task metadata,也不重放模型请求。
|
||||
远端实时过程需要包含该插件的 Agent;旧 Agent 的最终结果仍可查看,历史未采集过程
|
||||
不会被补造。
|
||||
- 本机 OpenCode SDK 路径允许不同会话并行,同一会话仍按请求顺序执行。每个请求独立拥有
|
||||
SSE 事件订阅,并在正常完成、错误、取消或消费方结束迭代时主动关闭自己的响应流,
|
||||
不关闭其他会话的订阅或共享 Server。聊天与原生 Compact 共用 `consumeEventSubscription`,
|
||||
在结束 SDK 迭代器之前先取消对应订阅,避免 SDK 移除取消监听后留下仍有缓冲的响应流;
|
||||
不改写 SDK 或提前取消其他请求。取消导致事件迭代结束时保留原取消原因,不改报
|
||||
“事件流意外结束”。这一清理规则属于本机 SDK 路径,不改变远端 Agent 持有 ACP Prompt
|
||||
的断线继续执行语义。
|
||||
- 同一 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 是否已处理的模型调用保持结果未知,避免重复计费或重复副作用。
|
||||
@@ -263,6 +333,10 @@ Execute 直接启动已签名 Runtime:
|
||||
时提交 Task 终态;事务成功后生成器才继续并向 Agent ACK。断电发生在同一 sequence
|
||||
中间时会重读该 sequence,已写事件按 `(binding, operation, sequence, eventIndex)` 去重,
|
||||
未写事件继续归并,不会重复 Provider 或工具执行。
|
||||
- 远程 context metrics 与对话级压缩后的估算值按本次请求实际使用的 Runtime 选择写入
|
||||
`context_state_json`。实时接收与恢复入口都传入请求的选择;数据库在未传入时按对话、
|
||||
项目、auto 顺序解析。继承项目设置的对话保持 `runtime_selection_json = NULL`,
|
||||
不为接收 usage 固定对话 Runtime;去重和消息/指标写入仍在同一 SQLite 事务中完成。
|
||||
- ACK 后 Agent 删除已确认事件,只保留小型操作终态与游标用于幂等核对;未 ACK 输出继续
|
||||
保留。Main 按 transcript page 的最高连续 sequence ACK,终态仍立即 ACK。单事件、单页、
|
||||
单 Prompt 总字节和事件数都有上限;Agent 始终为唯一终态预留一个事件和最大事件字节,
|
||||
@@ -276,6 +350,11 @@ Execute 直接启动已签名 Runtime:
|
||||
权威。启动后 Renderer 先订阅恢复进度,再触发每项目恢复。项目选择器依次显示网络、
|
||||
Agent、Runtime、cursor、完成或失败/重试;只阻塞受影响项目的发送和队列,已有历史与
|
||||
其他项目保持可用。
|
||||
- 完成提示是项目选择器内恢复进度的收尾反馈,首次收到该项目该 `requestId` 的完成状态后
|
||||
显示 3 秒,然后从收起按钮和展开菜单同时隐藏;不另发全局成功通知。计时由
|
||||
`ProjectSwitcher` 持有,切换项目、开合菜单或重复状态快照不延长期限,也不重放已隐藏的
|
||||
提示;新 `requestId` 的恢复可以再次展示。进行中状态和失败/就地重试持续保留。
|
||||
隐藏仅作用于展示层,`App` 保留完成业务状态,继续用于发送、队列恢复和事件顺序判断。
|
||||
- 正常应用退出对已接受托管 SSH 请求执行 detach,不等待远端完成;本地请求和尚未接受的
|
||||
请求仍按原行为取消。用户显式“停止”始终发送稳定 operation cancellation,并等待 Agent
|
||||
语义终态。已进入 Agent-owned `run` 但 start/attach 响应仍不确定的请求继续保留为
|
||||
@@ -290,13 +369,27 @@ Execute 直接启动已签名 Runtime:
|
||||
Agent,因此 Desktop 断电后可以继续下一轮。
|
||||
- Agent 校验固定 model、协议和 API path,拒绝 background/store、provider web search、
|
||||
provider MCP、持久 conversation、非文本 modality 和其他可独立计费能力;请求/响应 body
|
||||
各限制 768 KiB,响应同时核对声明长度与短读。每个
|
||||
各保留 64 MiB 的内存传输上限,用于容纳长上下文和图片输入,响应同时核对声明长度与短读。
|
||||
Desktop blob 模型桥保留已发布 v1 的 canonical JSON 字节,不添加尾随换行。单条消息按
|
||||
最多 2 MiB 的帧发送,接收端逐帧消费流量额度,跟踪 JSON 结构并在消息上限内组装;
|
||||
已发布端的单帧消息可直接接收,多帧大消息要求两端均使用更新后的实现。每个
|
||||
`(binding, operation, roundIndex)` 只 dispatch 一次;已 dispatch 但未证明完整交付的调用
|
||||
变为 `outcome-unknown`,不会自动重试。
|
||||
- Agent 不按固定模型调用次数或 Prompt 累计输出 Token 数停止 Runtime;每次 Provider
|
||||
请求仍遵循所选模型 profile 的单次输出设置和独立请求超时。模型桥协议不包含 Prompt
|
||||
请求遵循所选模型 profile 的单次输出设置;未设置时保留 Runtime 请求中的 Token 参数,
|
||||
更新后的 Agent 不补入固定 32,000 Token,也不再按 60 秒或桥接层的 120/150/180 秒
|
||||
总时长中止请求。它通过非 critical 的 `runtime/model-bridge-optional-limits` v1 capability
|
||||
声明支持省略 profile limits;Main 在建立托管 Runtime 时按实际连接的 capability 选择
|
||||
profile。锁定的已发布 Agent `0.11.22` 不支持省略这两个字段,Main 对该路径保留原有
|
||||
32,000 Token、60,000 ms 默认值及 1,000,000 Token、300,000 ms schema 上界;这些兼容
|
||||
值不应用于声明支持省略 limits 的 Agent。
|
||||
显式请求超时、Prompt deadline、用户取消和连接关闭仍会中止对应操作。模型桥协议不包含 Prompt
|
||||
调用次数或累计 Token 配额字段。实际 usage 在语义 transcript 中同步回 Desktop 的
|
||||
usage/task 数据。
|
||||
- 新组包的 Runtime manifest 将 `maximumPromptRuntimeMilliseconds` 设为 `0`,表示由
|
||||
Prompt 自身的 deadline 决定时长,不再额外截为十分钟。旧 manifest 中的正数限制仍按其
|
||||
声明执行。进程 owner 使用真实剩余时长安排 Prompt deadline;启动与停止等待的有界
|
||||
超时不再缩短 Prompt。
|
||||
- helper 可以接收同一 Prompt 内并发到达的模型桥请求;它在单一稳定模型桥上按到达
|
||||
顺序等待并交付,不返回本地 `bridge-busy`,每个响应只有在 HTTP 完整 flush 后才
|
||||
发送 delivery ACK。
|
||||
@@ -307,12 +400,70 @@ Execute 直接启动已签名 Runtime:
|
||||
- Unix socket 接收端按 `readableLength` 中已经缓冲的字节增量排空一个声明长度的帧,
|
||||
不会在部分大响应到达时反复请求尚未缓冲的完整剩余长度;短读、连接结束、错误和取消
|
||||
都会使当前交换失败。原生 Linux 回归覆盖至少 256 KiB 的 broker 响应。
|
||||
- Unix broker 的请求超时 `0` 表示不设置请求总时限,不等于零毫秒连接等待。
|
||||
连接仍有独立的默认 5 秒超时;只有显式正数请求时限才与连接时限取较小值,
|
||||
用户取消仍关闭连接并中止 dispatch。
|
||||
- Renderer 把相邻 text/reasoning delta 合并为消息 block 时始终替换最后一个 block,
|
||||
不修改既有 React state 对象。这样开发环境 StrictMode 重复调用 state updater 时,
|
||||
block metadata 与 canonical 消息正文保持一致。
|
||||
|
||||
## Agent 开发期间的真实 Host 验证
|
||||
|
||||
2026-09-10 Desktop `0.12.11` / Agent `0.11.23` 候选复测:共享 Linux x64 Host 的当前
|
||||
源码通过工作区协议重命名保护、字面路径/重命名/合并提交差异、Unix 桥接 256 KiB 往返及取消,
|
||||
以及继承 Runtime 指标的数据库重开与去重。隔离 Agent 报告版本 `0.11.23`,桌面 managed ACP
|
||||
经真实 SSH、Agent 和既有已安装 OpenCode 各完成一次 Ask、Execute;共 2 次真实模型请求,
|
||||
全部 HTTP 200、completed 且已交付,指标持久化后未固定继承选择。首次连接准备超时,未发送
|
||||
模型请求;重试成功后清理测试进程与目录,并按本轮 bundle 摘要确认、清理一次遗留上传目录。
|
||||
本次为当前源码复测,不是正式 `0.11.23` 复合包安装验收;三个原生包由发布 CI 构建和验证。
|
||||
本地发布回归为 3845 项通过、66 项跳过,发布说明校验、typecheck、lint 通过;发布准备未执行
|
||||
本地桌面生产构建或打包。
|
||||
|
||||
本轮 `020a9ad` 工作树修复使用当前源码在共享 Linux x64 Host 验证:默认 Unix broker
|
||||
完成 256 KiB 响应并传递取消;生产 AssistantDatabase 在临时库中写入继承项目的指标,
|
||||
重开后去重重放,未固定对话选择。当前 Workspace 协议的目标存在/同名/硬链接重命名
|
||||
保护、字面路径 diff、重命名 diff 和 merge commit diff 均通过,Git 使用 Host 已配置身份。
|
||||
当前 Desktop `createManagedRemoteAcpRuntime` 经真实 SSH attach、隔离 Agent、OpenCode
|
||||
和 Agent gateway 完成 Ask 与 Execute,均得到两字符回答和 done。Execute 的真实
|
||||
model-usage 转换为指标后通过生产数据库持久化与去重,继承设置保持未固定。
|
||||
共 3 次真实 Provider 调用(2 次 Ask、1 次 Execute),均 HTTP 200、completed 且已交付;
|
||||
前两次运行在模型完成后被测试 harness 的 usage 元数据/事件名断言拦截,并非产品失败。
|
||||
未修改产品来适配 harness:构建时直接解析 ACP SDK 的 ESM 入口,凭据只通过既有
|
||||
safeStorage 与模型控制协议使用。各次隔离 Agent 停止后剩余所属进程数为 0,测试目录和
|
||||
上传文件均已回收,原 Host registry 与已安装组件未改变。此次不覆盖 UI 点击或安装升级。
|
||||
|
||||
2026-09-10 已发布模型桥兼容修复:从 `agent-v0.11.22` 读取的源码与当前源码分别在共享
|
||||
Linux x64 Host 启动隔离 Agent daemon。当前 Main 的 `createManagedRemoteAcpRuntime`
|
||||
经真实 SSH attach、Agent protocol、OpenCode 和 Agent gateway,在两版 Agent 上各完成
|
||||
一次 Ask、一次 Execute,均返回 `OK`;模型账本共 4 次调用,全部 completed 且已交付。
|
||||
旧 Agent 未声明 optional-limits capability,当前 Agent 声明该能力。使用既有加密设置和
|
||||
固定 Host identity,凭据仅经协议进入内存;测试 daemon、Runtime 和运行目录已回收。
|
||||
本次覆盖托管 Runtime 入口,未执行 UI 点击、安装升级或长时间 Provider 等待。
|
||||
6 个聚焦测试文件共 77 项通过;另用真实 tag codec 验证四种消息双向互通。typecheck、lint
|
||||
通过;全量 `npm test` 为 3,739 项通过、4 项失败、64 项跳过,失败涉及发布/便携包夹具、
|
||||
DSH MCP 暴露和 Runtime 发现,未扩展修复范围。
|
||||
|
||||
2026-09-10 模型桥限制修复:共享 Linux x64 Host 使用当前源码构建的 gateway、Unix
|
||||
broker 和 loopback helper,通过 2 MiB 请求/响应往返,并确认客户端取消传到 broker。
|
||||
使用 GoodBuddy 加密设置中的模型凭据、既有固定 SSH identity 和 OpenCode `1.18.29`
|
||||
运行 Ask、Execute,两个模式均收到 HTTP 200 并返回 `OK`。共发出 3 次真实 Provider
|
||||
请求:Ask 成功,首次 Execute 的最终结果因本地命令采集超时未保留,单独复测 Execute
|
||||
成功。运行中测试凭据只经 SSH stdin 进入内存;测试进程和临时目录由测试运行回收。
|
||||
本次实机检查覆盖当前源码的模型桥与 Runtime helper;未重装共享 Agent,也未重跑完整
|
||||
Desktop attach/bootstrap。Prompt 长时运行和精确 deadline 使用假时钟回归验证。
|
||||
最终聚焦回归为 14 个文件、193 项通过、5 项平台跳过;typecheck、lint 和开发 build
|
||||
通过。此前全量 `npm test` 为 3,703 项通过、22 项失败、63 项跳过,失败分布在 Runtime
|
||||
发现、发布/便携包夹具、DSH ACP、模型设置和内置工具分类;该次全量结果来自并行修改中的
|
||||
工作区,未据此修改其他任务的文件,也未把聚焦通过等同于全量通过。
|
||||
|
||||
2026-09-09 已在隔离 Linux x64 Host 验证当前源码的快照配置修复:桌面
|
||||
`ManagedRemoteExecutionServices` 经 managed ACP、真实 detached Agent 和模型桥启动
|
||||
OpenCode `1.18.29`,Ask 完成原生读取,Execute 完成 4,000 行文件的 CRLF → LF 转换。
|
||||
4 次实际 Provider 请求全部为 HTTP 200,最终原生配置在两种模式均为 `snapshot: false`,
|
||||
权限仍为 `ask`/`allow`,transcript ACK 分别为 15/15 和 12/12。测试 Agent、模型桥、
|
||||
Runtime 与 relay 均已回收,原有 Host registry 未改变。这是实际链路和关闭验证,
|
||||
不是 Linux 前后性能对照,也未注入取消;不代替正式发布资产校验。
|
||||
|
||||
任何会改变已部署 GoodBuddy Agent 或桌面到 Agent 生产链路的源码改动,都必须在开发期间
|
||||
使用共享 Linux x64 真实测试 Host 验证。适用范围包括 Agent daemon、Agent protocol
|
||||
及共享 contract、Agent 组包与安装、attach/update、Runtime 启动与进程归属、Workspace、
|
||||
@@ -349,12 +500,92 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture
|
||||
不可达,应立即把真实 Host 验证报告为开发阻塞,保持该验证未完成,不能把 Agent 改动表述
|
||||
为已完成,也不能把首次真实验证推迟到发布阶段。
|
||||
|
||||
## 离线配置缓存简化验证(2026-09-08)
|
||||
|
||||
`npm test -- tests/opencode-config.test.ts src/main/agent/opencode-runtime.test.ts src/main/agent/bundled-runtimes.test.ts tests/remote-runtime-bundle.test.ts tests/agent-bundle.test.ts --testTimeout=30000` 通过 108 项;`npm run typecheck`、`npm run lint` 和 `npm run build` 通过。
|
||||
全量 `npm test -- --testTimeout=30000 --hookTimeout=120000` 当次结果为 3556 项通过、28 项失败、58 项跳过;其中离线配置路径断言随后修正并通过上述聚焦测试,未重跑全量。其余失败涉及 Agent 包元数据大小限制、Darwin 组包超时、Runtime 探测、DSH 和打包夹具。
|
||||
测试 Host 的 SSH 端口可达,但候选复合包验证受元数据大小限制阻塞,未部署或执行实机安装/更新验收;本次真实模型调用 0 次。
|
||||
以上为缓存简化阶段的历史记录,不代表后续发布前修复的最终结果。
|
||||
|
||||
## 发布前修复实机验证(2026-09-08)
|
||||
|
||||
共享 Linux x64 Host 上使用当前源码、锁定 Node 与 OpenCode `1.18.29`、进程内临时签名
|
||||
身份组包,签名目录读取、生产 `preparePackage/commitPackage`、adopt、Runtime activate
|
||||
与 health 通过。所有写入位于独立 GoodBuddy 测试 HOME,未修改共享安装。
|
||||
|
||||
真实 Desktop 协议客户端已通过 Ask 文本回复、Execute 原生 Task 子代理在 Workspace 外
|
||||
的专用测试目录写文件、读回校验、关闭连接后重连同一 daemon,以及 stop/bootstrap 后
|
||||
health。首轮发现握手 Base64URL 误拒绝后完成上述修复;最终源码复测通过,独立 Unix
|
||||
endpoint 连续 20 次握手也通过。两轮模型账本各 5 次 completed,本轮合计 10 次真实文本
|
||||
模型调用,没有发布工件、签名目录或 release tag。
|
||||
|
||||
最终自动化回归与尚未覆盖的系统休眠场景见
|
||||
[工作栏进度](../assistant-workbar/progress.md)。
|
||||
|
||||
## 已完成的 E2E 验收记录
|
||||
|
||||
### macOS arm64 Host 支持
|
||||
|
||||
`agents.yml` 增加 `macos-15` / `darwin-arm64` 的原生 CI 组包任务,使用临时测试
|
||||
签名校验 Agent、固定 Node、Koffi 和 OpenCode,并比较两次组包的字节摘要。
|
||||
构建输入与检查的权威说明见 [BUILD.md 的 Agent 原生 CI 策略](../../../BUILD.md#agent-原生-ci-策略)。
|
||||
此 CI 任务不发布工件。生产 lock、`agent-release.yml`、安装器、Desktop 包管理和
|
||||
SSH Host 链路现支持 `linux-x64`、`linux-arm64`、`darwin-arm64`,三个目标共用
|
||||
原有 catalog JSON 和签名格式,不增加 Mac 专用目录。旧版 Linux-only Desktop
|
||||
读取器不能解析混合目录,首次发布 Darwin 条目前需先升级 Desktop。
|
||||
|
||||
macOS 保持 SSH 按需启动 detached Agent,不要求 launchd、开机服务或管理员权限。
|
||||
私有 socket 使用 `/private/tmp`,通过 `getpeereid` 核对 UID;进程身份、启动标识和
|
||||
进程组通过系统原生 API 获取。取消前枚举 Runtime 的实际后代并核对 PID、启动时间
|
||||
和可执行文件,停止独立进程组中的工具,再停止 Runtime,避免只杀主进程组留下工具。
|
||||
|
||||
2026-09-06 当前源码在真实 macOS arm64 Host 的隔离目录通过生产安装器
|
||||
`preparePackage` / `commitPackage`、adopt、health、Runtime activate、stop、
|
||||
bootstrap 和再次 health。实际 Desktop 协议客户端通过 Attach、只读 Workspace
|
||||
打开、真实 Ask 输出、Execute 写入并读回测试文件、取消工具并验证子进程退出,以及
|
||||
重新连接同一 daemon。两个 Darwin 原生进程测试通过,其中一个覆盖独立进程组子进程。
|
||||
共享 Linux x64 Host 同时通过当前源码组包、生产安装、生命周期、真实 Ask 和重连。
|
||||
这些验证没有发布 catalog 或工件;GitHub Actions 和正式下载渠道的发布验收尚未执行。
|
||||
最终本地 typecheck、lint、开发 build 和 diff whitespace 检查通过。最终全量
|
||||
`npm test` 为 3520 通过、57 跳过、0 失败(326 个测试文件通过、9 个跳过)。
|
||||
此前 ActivityPanel 的批量记录用例超时、App 的实时浏览器用例未观察到预期调用,
|
||||
分别复跑后通过;未修改这两个用例或放宽超时,随后完整测试套件通过。
|
||||
隔离测试 ledger 统计 Mac 模型请求 7 次,Linux 首轮
|
||||
1 次;最终 Linux 源码重装后又完成 1 次最小 Ask,总计 9 次真实模型请求。
|
||||
|
||||
以下为较早的仅构建阶段记录,摘要不代表上述最终源码产物:
|
||||
|
||||
2026-09-06:在真实 macOS 26.5.2 / arm64 Host 的独立测试目录运行当前源码
|
||||
`build/agent-ci-bundle.cjs --platform darwin --arch arm64`,退出码为 0。
|
||||
测试使用固定 Node 24.19.0、OpenCode 1.18.29 和内存临时签名密钥;Node 官方
|
||||
归档 SHA-256、OpenCode integrity、Mach-O 架构、内外层签名和两次组包摘要比较
|
||||
通过,Agent CLI 加载、Koffi 原生模块加载与 OpenCode `--version` 检查通过。
|
||||
生成 `goodbuddy-agent-0.11.18-darwin-arm64.gbagent`,本次测试包 SHA-256 为
|
||||
`9c63ba080def56ebd371a622f2650b89d0c652444077bd6ee19c4bd51b60f6ff`。
|
||||
临时签名每次独立运行都会变化,因此该摘要只标识本次测试产物,不是发布输入。
|
||||
源码压缩传输 529,624 字节,大依赖由 Host 直接下载并复用 npm 缓存。
|
||||
真实模型调用为 0 次;没有安装 Agent 服务,没有修改在线 catalog 或发布工件。
|
||||
|
||||
该仅构建阶段的聚焦测试为 53 项通过、1 项平台跳过,typecheck、lint 和开发生产
|
||||
bundle 构建通过;这组历史结果不替代完整 Host 支持改动后的最终验证。
|
||||
|
||||
### Linux 历史验收
|
||||
|
||||
- 2026-09-06 使用当前源码隔离 Agent 验证原生子代理过程插件。最终场景收到 37 条 ACP
|
||||
插件事件,桌面侧从已保留活动重建跟踪器后仍接收后续文本与工具;子代理和父请求正常
|
||||
完成,最终模型调用账本为 4 次 completed。此前成功场景另 4 次,两轮真实 Host 共
|
||||
8 次模型请求。
|
||||
- 同日 Windows 隔离桌面实际发送 OpenCode 原生 Task,展开卡片可看到进行中的正文与
|
||||
read 工具,取消后已有过程保留且工具显示已取消;正确工作区场景完成后显示测试文件
|
||||
内容,最终结果不重复留在过程末尾。本机有界代理共记录 10 次模型请求(含错误路径、
|
||||
取消和成功场景);另外 2 次使用原生测试配置的请求返回 401,改选显式测试模型后通过。
|
||||
未改动正常用户设置,也未发布 Agent 包。全量静态与测试结果见
|
||||
[本轮验证记录](../assistant-workbar/progress.md)。
|
||||
|
||||
- 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.18`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
|
||||
当前源码 lock 固定为 `0.11.21`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
|
||||
- 2026-08-30 在共享 Linux x64 Host 的隔离测试 HOME 中验证当前 `0.11.13` 源码候选:
|
||||
签名 Runtime 清单的测试墙钟上限为 1 秒,模型桥首轮故意延迟 2.515 秒后 Prompt 仍在
|
||||
5.770 秒正常完成;随后 OpenCode 原生 Task 依次产生 `running`、`completed` 子 Agent
|
||||
@@ -372,7 +603,7 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture
|
||||
- 最终源码复核后重新构建测试签名 package:
|
||||
SHA-256 `39307fd1dccd638dcb723027591bcd83deab7057d550c99eff6f037e339829f4`,
|
||||
installation `agent-2cad05acfebe853394807e3a11f7befcd9f47215c2cf3a33a03d5f644b45716e`,
|
||||
Runtime `1.18.9` / bundle
|
||||
Runtime `1.18.29` / bundle
|
||||
`sha256:6c7fc975415ef2547d3729bc4772d3e78362c2e76c05d1fc24968a0468fcb7ba`。
|
||||
最终 15 秒 detach/recovery 使用两个模型轮次、一个工具和一个终态,ledger 两轮均
|
||||
`completed + delivered`,语义 `latest=ACK=41`,保留事件、Runtime owner、ACP channel/
|
||||
@@ -418,7 +649,7 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture
|
||||
UTF-8 字符前回退并显示 262,143 bytes;继续加载后完整追加剩余文本,页面不再显示加载
|
||||
按钮且没有错误。测试未调用模型;专用远端文件、目录和本机隔离用户数据均已清理。
|
||||
- 正常 Host 更新路径把 Linux x64 Host 的 Agent 更新为 `0.11.2-e2e.12`,并确认
|
||||
OpenCode Runtime 已安装版本与所需版本均为 `1.18.9`。
|
||||
OpenCode Runtime 已安装版本与所需版本均为 `1.18.29`。
|
||||
- 一条新的 Ask 用户操作只提交一次。OpenCode 先在 build 模型轮次请求一个原生
|
||||
`read`,读取专用测试 Workspace 中的证据文件,再在第二个 build 模型轮次生成最终
|
||||
回答;没有 title 模型轮次,也没有第二个工具调用。
|
||||
@@ -524,7 +755,8 @@ goodbuddy-agent diagnostics --installation-id <installationId>
|
||||
1. `npm test`
|
||||
2. `npm run typecheck`
|
||||
3. `npm run lint`
|
||||
4. `npm run build`
|
||||
4. 发布候选的生产构建由 main 分支 CI 验证;发布前不在本地执行生产构建或打包,详见
|
||||
[发布手册](../../development/release-runbook.md)。
|
||||
5. 用测试签名复合包验证在线下载源选择和离线导入/导出;正式 Agent 候选还需公开校验
|
||||
双架构 production 包与签名目录。
|
||||
6. 在已固定 Host Key 的 Linux x64 测试 Host 上验证 attach-or-bootstrap。
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# ShareServer
|
||||
|
||||
ShareServer 是 GoodBuddy 设备共享网络的独立 Web 服务。它为一个或多个组织提供身份与设备
|
||||
目录、能力发布、授权策略、任务路由、可选数据中继、能力包更新、网关联邦和审计管理。
|
||||
|
||||
ShareServer 不属于 Electron Main,不负责桌面端局域网发现,也不自动取得设备上的能力、
|
||||
文件、凭据或 SSH Host。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 管理角色、产品范围、功能需求和验收 |
|
||||
| [功能逻辑](./logic-design.md) | 组织隔离、设备、发布、策略、任务和联邦状态规则 |
|
||||
| [管理界面设计](./ui-design.md) | ShareServer Web 控制台的信息架构与交互 |
|
||||
| [技术设计](./technical-design.md) | 服务模块、API、数据、部署、运维和验证 |
|
||||
| [共享网络总体设计](../../architecture/share-network-architecture.md) | 跨端身份、能力、授权、任务、更新和协议权威 |
|
||||
|
||||
当前没有单独的 User Stories:管理员、能力发布者、审计员和普通成员场景已进入 PRD 与逻辑
|
||||
设计。服务尚未实施,因此不创建 `progress.md`。
|
||||
|
||||
## 角色
|
||||
|
||||
- **系统管理员**:维护实例、身份源、域名、证书、存储和全局限制。
|
||||
- **组织管理员**:管理本组织成员、设备、组、策略、能力和联邦关系。
|
||||
- **能力发布者**:在获准范围内发布设备能力或签名能力包。
|
||||
- **审批人**:处理调用、发布、更新和联邦申请。
|
||||
- **审计员**:只读查看和导出获准范围内的审计事件。
|
||||
- **成员**:主要通过 GoodBuddy Desktop 使用能力,只在 Web 端处理个人设备和审批。
|
||||
@@ -1,209 +0,0 @@
|
||||
# ShareServer 功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [ShareServer PRD](./prd.md) |
|
||||
| 共享规则 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
|
||||
## 1. 核心不变量
|
||||
|
||||
### INV-1 组织隔离先于角色
|
||||
|
||||
任何查询和变更先绑定 organization,再计算角色。系统管理员的跨组织操作使用独立系统管理
|
||||
入口并形成审计,不能依靠省略 organization 条件获得全局数据。
|
||||
|
||||
### INV-2 注册不等于发布
|
||||
|
||||
设备注册只建立身份和连接资格。没有有效 Publication 时,目录中没有该设备能力。
|
||||
|
||||
### INV-3 目录不等于授权
|
||||
|
||||
看到能力不能推导可调用或可下载。创建任务和下载包时重新计算当前授权。
|
||||
|
||||
### INV-4 服务端授权不能扩大端点授权
|
||||
|
||||
ShareServer 的 `allow` 只是签发任务资格。Provider 和调用方仍执行各自本地策略;任一端拒绝
|
||||
即终止动作。
|
||||
|
||||
### INV-5 策略拒绝优先
|
||||
|
||||
同一动作命中多个规则时使用 `deny > require_approval > allow`。没有匹配 allow 时默认 deny。
|
||||
|
||||
### INV-6 联邦为双边交集
|
||||
|
||||
单方创建或接受的关系不生效。双方 active 且两端发布、Grant 和数据策略均允许时才能跨组织
|
||||
创建任务。
|
||||
|
||||
### INV-7 任务提供者不可改写
|
||||
|
||||
ShareServer 只路由调用方冻结的 provider,不在离线、繁忙或失败时替换节点。
|
||||
|
||||
### INV-8 不恢复短期权威
|
||||
|
||||
备份恢复不恢复 Web 会话、设备 socket、一次性挑战、审批 nonce 或任务数据通道。终端必须
|
||||
重新连接并证明身份。
|
||||
|
||||
### INV-9 初始化入口一次性关闭
|
||||
|
||||
初始化只在实例尚无系统管理员时可用。首个系统管理员创建成功后,后续请求即使持有旧的
|
||||
一次性引导材料也不能再次进入初始化流程。
|
||||
|
||||
## 2. 主要状态
|
||||
|
||||
### 2.1 成员
|
||||
|
||||
```text
|
||||
invited -> active -> suspended -> active
|
||||
invited -> expired | revoked
|
||||
active/suspended -> removed
|
||||
```
|
||||
|
||||
`suspended` 不能登录或创建新任务;历史归属保留。`removed` 不级联删除审计和已完成任务。
|
||||
|
||||
### 2.2 注册设备
|
||||
|
||||
```text
|
||||
pending -> active <-> offline
|
||||
pending -> expired | rejected
|
||||
active/offline -> revoked
|
||||
```
|
||||
|
||||
客户端心跳只决定 `active/offline`,不能清除 `revoked`。重新注册生成新设备 registration。
|
||||
|
||||
### 2.3 Publication
|
||||
|
||||
```text
|
||||
draft -> pending_approval -> active
|
||||
draft/pending_approval -> rejected
|
||||
active <-> paused
|
||||
active/paused -> revoked
|
||||
```
|
||||
|
||||
provider 离线不会把 Publication 变为 paused;目录同时展示“已发布”和“当前离线”两个事实。
|
||||
|
||||
### 2.4 审批
|
||||
|
||||
```text
|
||||
pending -> approved | rejected | cancelled | expired
|
||||
```
|
||||
|
||||
审批对象内容摘要、申请主体或策略版本变化后,原审批不能复用。approved 只产生其声明范围内
|
||||
的 Grant 或一次性决定。
|
||||
|
||||
### 2.5 Package Release
|
||||
|
||||
```text
|
||||
draft -> verifying -> available -> paused -> available
|
||||
verifying -> invalid
|
||||
available/paused -> revoked
|
||||
```
|
||||
|
||||
发布后的版本和字节不可变。修复内容必须使用新版本;revoked 不删除审计证据和对象摘要。
|
||||
|
||||
### 2.6 FederationTrust
|
||||
|
||||
```text
|
||||
draft -> offered -> mutually_confirmed -> active
|
||||
offered -> rejected | expired
|
||||
active <-> paused
|
||||
任意非终态 -> revoked
|
||||
```
|
||||
|
||||
`mutually_confirmed` 只表示身份确认完成;双方策略同步成功后才进入 active。
|
||||
|
||||
## 3. 授权计算顺序
|
||||
|
||||
1. 验证请求身份、会话 audience 和 organization。
|
||||
2. 验证用户、设备、provider、Publication 和 FederationTrust 当前状态。
|
||||
3. 校验能力版本、动作、输入 schema、数据类别、大小和委托链。
|
||||
4. 收集调用方组织、提供方组织、Publication 和 Grant 规则。
|
||||
5. 按拒绝优先合并,计算 `deny/require_approval/allow`。
|
||||
6. 校验次数、并发、预算和有效期。
|
||||
7. 返回决定、稳定原因和策略引用;需要批准时建立内容绑定申请。
|
||||
8. 创建任务时再次计算,并把决定快照写入任务。
|
||||
|
||||
目录查询可以预计算“可能可用”,但不能替代第 1 至 8 步。
|
||||
|
||||
## 4. 组织角色边界
|
||||
|
||||
| 动作 | 系统管理员 | 组织管理员 | 发布者 | 审批人 | 审计员 | 成员 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 实例设置 | 是 | 否 | 否 | 否 | 只读有限项 | 否 |
|
||||
| 成员与组 | 紧急管理 | 是 | 否 | 否 | 只读 | 自己 |
|
||||
| 注册设备 | 只读/撤销 | 是 | 自己 | 否 | 只读 | 自己 |
|
||||
| 发布能力/包 | 否 | 是 | 授权范围 | 否 | 只读 | 否 |
|
||||
| 编辑策略 | 否 | 是 | 否 | 否 | 只读 | 否 |
|
||||
| 处理审批 | 否 | 可配置 | 否 | 授权范围 | 只读 | 自己的申请 |
|
||||
| 联邦关系 | 实例级许可 | 是 | 否 | 可共同审批 | 只读 | 否 |
|
||||
| 审计导出 | 实例级 | 组织级 | 自己相关 | 自己相关 | 授权范围 | 自己相关 |
|
||||
|
||||
实际权限仍由策略计算,表格定义产品默认职责而非硬编码角色名称。
|
||||
|
||||
## 5. 任务路由决策
|
||||
|
||||
| 条件 | 结果 |
|
||||
| --- | --- |
|
||||
| provider 在线且双方可直连 | 签发短期直连协商信息 |
|
||||
| 无法直连且双方策略允许中继 | 建立有界中继通道 |
|
||||
| 中继关闭或任一方禁止 | 创建失败,说明无可用路由 |
|
||||
| provider 离线 | 不排队到无限期,按能力配置有界等待或拒绝 |
|
||||
| provider 接受后连接中断 | 保留 task,等待同一 provider 恢复/查询 |
|
||||
| provider 终态未知且恢复期结束 | `outcome_unknown` |
|
||||
| 调用方取消 | 转发取消;provider 确认后终态 cancelled |
|
||||
|
||||
中继容量不足只产生排队或资源错误,不切换 provider,不改变是否允许中继。
|
||||
|
||||
## 6. 更新策略优先级
|
||||
|
||||
```text
|
||||
紧急撤销
|
||||
> 组织强制固定版本/禁止版本
|
||||
> 设备组发布通道
|
||||
> 用户允许的版本范围
|
||||
> 当前已安装版本
|
||||
```
|
||||
|
||||
任何权限扩大都从自动路径转为 `require_approval`。降级只有在目标版本仍被签名目录允许且
|
||||
策略明确指定时才提供,不自动视为故障回滚。
|
||||
|
||||
## 7. 联邦可见性
|
||||
|
||||
联邦目录只导出对方明确允许的能力投影:组织显示名、能力、版本范围、数据政策和可用性摘要。
|
||||
不导出内部设备名、成员清单、组结构、节点地址、内部策略全文或审计记录。跨组织请求使用
|
||||
双方关联 ID,双方各自解析本地主体。
|
||||
|
||||
## 8. 删除、撤销与保留
|
||||
|
||||
- 删除组织前必须停止新任务、撤销联邦和设备,并按保留策略归档审计;不提供无提示级联。
|
||||
- 删除用户不删除其历史动作,审计显示不可变主体摘要。
|
||||
- 撤销设备、Publication、Grant、Package 或联邦后立即阻止新令牌。
|
||||
- 对象存储中的未完成任务正文按任务期限清理;审计元数据按组织保留策略清理。
|
||||
- 法定保留只锁定匹配审计事件,不使被撤销身份重新可用。
|
||||
|
||||
## 9. 需求追踪
|
||||
|
||||
| 需求 | 主要逻辑 |
|
||||
| --- | --- |
|
||||
| `FR-1` | `INV-1`、`INV-9` 和系统管理员角色边界 |
|
||||
| `FR-2` | 组织隔离不变量、成员状态和角色边界 |
|
||||
| `FR-3` | 注册不等于发布、设备状态和短期权威恢复规则 |
|
||||
| `FR-4` | 注册、Publication、目录与授权不变量 |
|
||||
| `FR-5` | 拒绝优先不变量和授权计算顺序 |
|
||||
| `FR-6` | 审批状态、内容绑定和授权重算 |
|
||||
| `FR-7` | provider 冻结不变量和任务路由决策 |
|
||||
| `FR-8` | Package Release 状态和更新策略优先级 |
|
||||
| `FR-9` | 联邦双边交集、FederationTrust 状态和联邦可见性 |
|
||||
| `FR-10` | 组织隔离、删除撤销与保留规则 |
|
||||
| `FR-11` | 组织角色边界及各对象状态;展示行为由 UI 设计定义 |
|
||||
| `FR-12` | 短期权威恢复规则和逻辑完整性;部署结构由技术设计定义 |
|
||||
|
||||
## 10. 逻辑完整性
|
||||
|
||||
本设计覆盖实例、组织、成员、设备、发布、授权、审批、任务、更新、联邦、撤销和恢复。实现
|
||||
前仍需确定首个 OIDC 提供者矩阵、默认审计保留期、部署规模目标和中继带宽上限;这些部署
|
||||
参数应可配置,不改变拒绝优先、双边联邦和 provider 冻结规则。
|
||||
@@ -1,149 +0,0 @@
|
||||
# ShareServer PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 适用产品 | 独立部署的 ShareServer 与 Web 管理控制台 |
|
||||
| 关联架构 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
| 关联文档 | [功能逻辑](./logic-design.md)、[管理界面](./ui-design.md)、[技术设计](./technical-design.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
局域网配对适合附近设备,但不能统一解决跨网络访问、成员离职、组织策略、集中更新、审计和
|
||||
跨组织协作。ShareServer 提供一个可自建的组织控制面,让设备通过主动出站连接注册能力并
|
||||
接收任务,不要求为每台桌面设备开放公网端口。
|
||||
|
||||
## 2. 产品目标
|
||||
|
||||
1. 组织可在自有网络、私有云或公网独立部署 ShareServer。
|
||||
2. 管理员可统一管理用户、组、设备、能力、权限、更新和审计。
|
||||
3. 成员通过桌面端使用获准能力,无需日常进入管理控制台。
|
||||
4. 设备离线、用户撤销或策略变化能准确影响新任务,不破坏历史证据。
|
||||
5. 两个组织可经双边确认建立有限范围的联邦协作。
|
||||
6. ShareServer 可以只运行控制面,也可按部署策略提供数据中继和包存储。
|
||||
|
||||
## 3. 产品范围
|
||||
|
||||
### 3.1 包含
|
||||
|
||||
- 本地账号及可配置 OIDC 身份源。
|
||||
- 组织、成员、角色、用户组、设备组和服务账号。
|
||||
- 设备注册、状态、撤销和合规元数据。
|
||||
- 能力目录、Publication、Grant、强制策略和审批。
|
||||
- 设备出站控制通道、任务路由、取消和可选中继。
|
||||
- 签名能力包仓库、发布通道、更新策略和撤销。
|
||||
- ShareServer 间双边联邦。
|
||||
- 审计、用量、保留和导出。
|
||||
- Web 管理控制台和成员自助页面。
|
||||
|
||||
### 3.2 不包含
|
||||
|
||||
- 远程管理设备桌面、任意 Shell 或任意文件系统。
|
||||
- 发现未注册的局域网设备。
|
||||
- 保存或分发用户 API Key、Cookie 和桌面登录状态。
|
||||
- 默认查看任务正文、附件和模型输出。
|
||||
- 未经提供者发布自动盘点设备能力。
|
||||
- 自动信任同域邮箱、同一网络或其他 ShareServer。
|
||||
- 公共开放市场、计费结算或匿名能力调用。
|
||||
- 替代 GoodBuddy 官方客户端、模型和 Agent 发布源。
|
||||
|
||||
## 4. 功能需求
|
||||
|
||||
### FR-1 实例初始化
|
||||
|
||||
- 首次启动通过一次性引导创建实例标识和首个系统管理员。
|
||||
- 引导完成后永久关闭初始化入口,后续管理员通过正常身份流程创建。
|
||||
- 实例公开描述包含名称、协议版本、登录方式和联邦端点,不包含内部拓扑。
|
||||
|
||||
### FR-2 身份与组织
|
||||
|
||||
- 支持一个实例承载多个隔离组织,也支持部署为单组织模式。
|
||||
- 支持本地账号和 OIDC;组织可限制允许的身份源。
|
||||
- 成员可以属于多个组并拥有有界角色,角色不隐式跨组织。
|
||||
- 停用用户立即停止签发新设备和任务凭据。
|
||||
|
||||
### FR-3 设备管理
|
||||
|
||||
- 桌面端经用户登录、一次性挑战和设备签名完成注册。
|
||||
- 设备条目显示所有者、平台、客户端版本、最近在线、连接和撤销状态。
|
||||
- 设备主动建立出站控制连接;服务端不能借此调用未发布能力。
|
||||
- 管理员可撤销设备并终止其新任务资格。
|
||||
|
||||
### FR-4 能力目录
|
||||
|
||||
- 只接收已注册设备或服务节点提交的显式 Publication。
|
||||
- 目录按调用者身份、设备状态、组织和策略过滤后返回。
|
||||
- 支持同一能力多个版本和多个 provider,不掩盖来源。
|
||||
- provider 离线时保留目录事实但显示不可用,不派发新任务。
|
||||
|
||||
### FR-5 授权策略
|
||||
|
||||
- Grant 可绑定用户、组、设备、设备组和联邦组织。
|
||||
- 可约束能力、版本、动作、输入类型、数据分类、提供者、时间、次数、并发和委托。
|
||||
- 支持组织强制策略和用户可收紧偏好;拒绝优先。
|
||||
- 每个决定返回稳定结果、命中策略和面向用户的原因。
|
||||
|
||||
### FR-6 审批
|
||||
|
||||
- 调用、能力发布、权限扩大更新和联邦关系可以要求审批。
|
||||
- 审批项显示申请人、对象、影响范围、数据类别、期限和策略依据。
|
||||
- 批准结果有明确范围和有效期,不提供无边界“永久允许”。
|
||||
- 申请内容变化后旧审批失效。
|
||||
|
||||
### FR-7 任务路由
|
||||
|
||||
- 根据冻结 provider、在线连接和路由策略把任务声明发送给目标设备。
|
||||
- 支持直连协商、网关中继和取消传播。
|
||||
- 任务正文与控制元数据分离;默认审计不保存正文。
|
||||
- 已接受任务断线后允许查询同一 task,不自动创建重复执行。
|
||||
|
||||
### FR-8 能力包与更新
|
||||
|
||||
- 保存不可变签名 Package 和发布目录。
|
||||
- 支持组织、组、平台、架构和维护窗口的更新策略。
|
||||
- 支持固定版本、允许版本范围、暂停发布和紧急撤销。
|
||||
- 权限扩大必须触发新批准,不能被普通自动更新策略覆盖。
|
||||
- 包来源、大小、摘要、签名和下载结果可审计。
|
||||
|
||||
### FR-9 联邦
|
||||
|
||||
- 两个 ShareServer 通过指纹交换和双方管理员确认建立关系。
|
||||
- 双方分别配置公开能力、允许主体、数据范围、用量和期限。
|
||||
- 最终权限取双方策略交集;任一端撤销后停止新任务。
|
||||
- 不默认链式转发到第三个组织。
|
||||
|
||||
### FR-10 审计与用量
|
||||
|
||||
- 记录身份、设备、发布、授权、审批、任务、包和联邦管理事件。
|
||||
- 默认仅记录共享架构定义的有界元数据。
|
||||
- 支持按组织隔离查询、条件筛选、分页和流式导出。
|
||||
- 审计员只读,不能因查看日志取得任务正文或管理权限。
|
||||
|
||||
### FR-11 管理控制台
|
||||
|
||||
- 提供概览、组织与成员、设备、能力目录、权限策略、审批、发布与更新、网关联邦、任务与
|
||||
中继、审计、系统设置。
|
||||
- 普通成员只看到自己的设备、授权和待办;管理员栏目按权限隐藏并在 API 再次校验。
|
||||
- 所有高影响操作说明对象、范围和后果。
|
||||
|
||||
### FR-12 可部署性
|
||||
|
||||
- 支持容器部署,配置与持久数据分离。
|
||||
- 支持外部 PostgreSQL;对象存储和中继按功能启用。
|
||||
- 提供健康、就绪、指标、结构化日志、备份和恢复说明。
|
||||
- 服务升级不能要求同时升级所有在线桌面端;按协议版本协商兼容集合。
|
||||
|
||||
## 5. 产品验收
|
||||
|
||||
- 新实例可完成初始化、登录、创建组织、邀请成员和注册真实桌面设备。
|
||||
- 两个不同组织不能通过 API、搜索、计数、错误或审计结果读取对方数据。
|
||||
- 设备只有发布能力后才进入获准目录,撤销后不能接收新任务。
|
||||
- 调用授权能同时处理用户、设备、组织、提供者、数据和联邦限制。
|
||||
- 任务中继关闭时控制面仍可运行;中继开启时可取消并形成准确终态。
|
||||
- 权限扩大的包更新不能自动部署。
|
||||
- 两个独立 ShareServer 能建立、使用、暂停和撤销双边联邦。
|
||||
- 备份恢复后组织、策略、发布和审计身份保持一致,短期连接和令牌不恢复。
|
||||
@@ -1,242 +0,0 @@
|
||||
# ShareServer 技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [ShareServer PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 共享协议 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
|
||||
|
||||
## 1. 仓库与运行边界
|
||||
|
||||
实现时在仓库顶层创建独立项目:
|
||||
|
||||
```text
|
||||
shareserver/
|
||||
README.md
|
||||
package.json
|
||||
src/
|
||||
api/
|
||||
auth/
|
||||
organizations/
|
||||
devices/
|
||||
capabilities/
|
||||
policies/
|
||||
approvals/
|
||||
tasks/
|
||||
relay/
|
||||
packages/
|
||||
federation/
|
||||
audit/
|
||||
web/
|
||||
migrations/
|
||||
deploy/
|
||||
tests/
|
||||
```
|
||||
|
||||
当前只提交设计文档,不创建无法运行的目录占位。服务端和 Desktop 共同依赖未来的
|
||||
`packages/share-protocol`,ShareServer 不导入 Electron 的 `src/shared`。
|
||||
|
||||
## 2. 技术选型约束
|
||||
|
||||
- 使用与仓库一致的 TypeScript/Node.js 工具链,具体 HTTP 框架在实现前根据维护性选择。
|
||||
- PostgreSQL 是组织、策略、任务元数据和审计的权威数据库。
|
||||
- Package 与可选中继暂存使用 S3 兼容对象存储;不开启对应功能时不要求对象存储。
|
||||
- Web 控制台与 API 同源部署,采用严格 CSP,不在浏览器保存设备凭据。
|
||||
- 单实例部署可完整工作;水平扩展时再引入共享事件/连接路由组件,不为未验证规模预设复杂
|
||||
分布式协议。
|
||||
|
||||
## 3. 服务模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `auth` | 本地身份、OIDC、Web session、服务和设备凭据 |
|
||||
| `organizations` | 组织、成员、组、角色和范围解析 |
|
||||
| `devices` | 注册挑战、设备记录、出站连接和在线状态 |
|
||||
| `capabilities` | manifest、provider、Publication 和目录投影 |
|
||||
| `policies` | Grant、强制策略和确定性授权计算 |
|
||||
| `approvals` | 内容绑定申请、决定、过期和通知 |
|
||||
| `tasks` | task/attempt、provider 路由、状态和取消 |
|
||||
| `relay` | 可选有界数据中继,不解释能力正文 |
|
||||
| `packages` | 不可变对象、签名目录、通道和撤销 |
|
||||
| `federation` | 网关身份、offer、双边策略和跨组织映射 |
|
||||
| `audit` | 事务内审计写入、查询、保留和导出 |
|
||||
|
||||
模块通过服务接口调用,API handler 不直接拼接跨组织 SQL 或自行计算权限。
|
||||
|
||||
## 4. API 边界
|
||||
|
||||
建议分为四类 audience:
|
||||
|
||||
```text
|
||||
/api/v1/web/* 管理控制台与成员自助 API
|
||||
/api/v1/device/* 设备注册、目录和任务控制 API
|
||||
/api/v1/federation/* ShareServer 间 API
|
||||
/api/v1/public/* 服务描述、健康与登录入口
|
||||
```
|
||||
|
||||
设备使用 WebSocket `/api/v1/device/connect` 建立出站控制通道。包下载使用短期、绑定主体和对象
|
||||
摘要的 URL。联邦 API 使用网关身份,不接受普通用户 session。每类 token 使用不同 issuer/
|
||||
audience/scope,不能互换。
|
||||
|
||||
所有请求经 schema、认证、organization scope、授权、速率和大小边界。列表使用游标分页和固定
|
||||
最大页;错误返回 request ID、稳定 code 和有界参数。
|
||||
|
||||
## 5. 数据模型
|
||||
|
||||
核心表建议为:
|
||||
|
||||
```text
|
||||
instances
|
||||
identity_providers
|
||||
users
|
||||
organizations
|
||||
memberships
|
||||
groups
|
||||
group_members
|
||||
role_bindings
|
||||
registered_devices
|
||||
device_credentials
|
||||
device_connections
|
||||
capabilities
|
||||
capability_versions
|
||||
providers
|
||||
publications
|
||||
grants
|
||||
policies
|
||||
approval_requests
|
||||
approval_decisions
|
||||
tasks
|
||||
task_attempts
|
||||
task_events
|
||||
package_objects
|
||||
package_releases
|
||||
release_channels
|
||||
update_policies
|
||||
federation_trusts
|
||||
federation_capability_exports
|
||||
audit_events
|
||||
revocations
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 所有组织数据表包含不可为空的 `organization_id`,并由 repository 层强制作用域。
|
||||
- 唯一约束同时包含 organization,避免跨组织名称冲突泄漏。
|
||||
- 密钥保存为单向摘要或经外部主密钥加密的密文;数据库不保存明文 token。
|
||||
- 在线连接和短期 nonce 主要在内存;表中只保存恢复所需元数据,不把 socket 当持久对象。
|
||||
- task event 只存控制状态和有界统计;正文流不进入普通关系表。
|
||||
|
||||
## 6. 事务与一致性
|
||||
|
||||
- 业务变更与对应审计事件在同一 PostgreSQL 事务提交。
|
||||
- Publication/Grant/Policy 使用 revision 做乐观并发,Web 草稿不能覆盖他人更新。
|
||||
- 授权决定创建 task 时再次读取当前 revision,并将命中策略摘要冻结到 task。
|
||||
- 设备在线状态允许短暂最终一致;身份、撤销、授权和任务接受必须走权威存储。
|
||||
- 重复注册挑战、联邦 offer 和 task create 使用唯一幂等键。
|
||||
- 无法确认 provider 副作用时只更新 `outcome_unknown`,不靠消息队列自动重放。
|
||||
|
||||
## 7. 设备连接与任务
|
||||
|
||||
连接建立后完成设备 credential、organization、客户端协议和当前 registration 校验。服务端为
|
||||
每个连接设置心跳、空闲、帧、发送队列和并发上限。
|
||||
|
||||
任务流程:
|
||||
|
||||
```text
|
||||
authorize -> create task -> resolve exact provider connection
|
||||
-> send declaration -> provider accepts/rejects
|
||||
-> establish direct or relay route -> stream progress
|
||||
-> provider terminal result -> caller delivery -> audit terminal state
|
||||
```
|
||||
|
||||
同一设备重复连接时使用 connection generation;新连接接管未来任务,旧连接仍只允许完成或
|
||||
查询已归属 task,随后有界关闭。不能把新任务随机发到身份相同但 generation 过期的 socket。
|
||||
|
||||
## 8. 中继
|
||||
|
||||
中继是可选模块:
|
||||
|
||||
- 控制面与中继可部署在不同进程,但共享 task 授权验证。
|
||||
- 每个 relay ticket 绑定 task、调用方、provider、方向、期限和最大字节。
|
||||
- 实现背压和全局/组织/设备并发及带宽上限。
|
||||
- 不解析能力正文,不写普通日志;需要临时落盘时使用加密 GoodBuddy-owned 暂存并按 task 清理。
|
||||
- 中继关闭、过载或策略禁止时返回准确路由错误,不静默转到其他节点。
|
||||
|
||||
## 9. Package 存储
|
||||
|
||||
上传先进入不可公开暂存:流式计算大小和 SHA-256,验证签名、manifest、版本、平台和权限后,
|
||||
以 digest 发布为不可变对象。相同 digest 可去重,但组织授权和 Release 分别保存。
|
||||
|
||||
Release catalog 使用独立签名域。服务器不能重打包已签名 payload;镜像目标必须保持相同字节、
|
||||
大小和 SHA-256。撤销更新目录和授权,不覆盖或原地删除仍受审计保留的对象。
|
||||
|
||||
## 10. 联邦协议
|
||||
|
||||
- 每个实例生成网关身份和可轮换证书,私钥不进入数据库备份明文。
|
||||
- offer 包含双方可人工核对的组织名、端点、指纹、协议和 nonce。
|
||||
- 双方确认后交换有界 capability projection 和本地映射 ID。
|
||||
- 跨组织 task 带双方关联 ID 和短期签名声明;每端独立授权并各写审计。
|
||||
- 不接受调用方提供的任意回调 URL、第三方 hop 或内部主体 ID。
|
||||
- 证书轮换需要旧身份签名或双方重新确认;身份突变直接暂停关系。
|
||||
|
||||
## 11. Web 安全
|
||||
|
||||
- 使用 HttpOnly、Secure、SameSite cookie 和 CSRF 防护管理 Web session。
|
||||
- OIDC 使用 state、nonce、PKCE 和固定回调地址。
|
||||
- 富文本字段按纯文本显示;manifest/schema 使用安全代码查看器,不执行 HTML。
|
||||
- 权限隐藏仅改善界面,所有 API 在服务端重新校验。
|
||||
- 登录、设备注册、目录、审批、导出和联邦接口分别限速。
|
||||
- 密钥字段只能替换、轮换和测试,永不回显。
|
||||
|
||||
## 12. 部署与配置
|
||||
|
||||
最小生产部署:
|
||||
|
||||
```text
|
||||
TLS reverse proxy/load balancer
|
||||
ShareServer API + Web
|
||||
PostgreSQL
|
||||
可选:S3 compatible object storage
|
||||
可选:Relay worker
|
||||
```
|
||||
|
||||
部署配置通过环境变量或挂载文件提供数据库、主密钥、公开 URL、OIDC、邮件、对象存储和限制。
|
||||
普通 Web 管理员不能修改基础设施秘密。提供数据库迁移命令、不可变镜像、非 root 容器、健康
|
||||
与就绪端点、优雅停止和部署示例。
|
||||
|
||||
## 13. 可观测性与备份
|
||||
|
||||
- 结构化日志只包含 request/task/organization 关联 ID、稳定事件和错误码。
|
||||
- 指标覆盖请求、WebSocket、授权结果、任务状态、中继、Package 和联邦,不包含能力正文。
|
||||
- tracing 跨内部服务传播关联 ID,跨联邦只传播双方同意的关联标识。
|
||||
- 备份 PostgreSQL、Package 对象和网关身份;不备份缓存、Web session、nonce 或中继暂存。
|
||||
- 恢复后撤销列表、组织 ID、签名目录和审计序列保持有效,所有设备重新连接。
|
||||
|
||||
## 14. 测试策略
|
||||
|
||||
### 14.1 自动化
|
||||
|
||||
- schema、认证 audience、CSRF、OIDC 和设备挑战。
|
||||
- 每个 repository/API 的跨组织隔离与枚举防护。
|
||||
- 拒绝优先授权、审批内容绑定、预算和并发。
|
||||
- device generation、离线、撤销、重连和 provider 冻结。
|
||||
- task 幂等、取消、结果未知和不自动重放。
|
||||
- Package 流式校验、不可变性、权限 diff 和撤销。
|
||||
- 联邦双方确认、策略交集、暂停、证书变化和禁止第三 hop。
|
||||
- 审计事务、过滤、分页、导出和正文缺失。
|
||||
|
||||
### 14.2 集成与部署
|
||||
|
||||
- Desktop 与真实 ShareServer 完成注册、目录、调用、文件和取消。
|
||||
- 两个隔离组织验证搜索、计数、错误、对象和审计不泄漏。
|
||||
- 两个 ShareServer 完成联邦建立、跨组织调用和单方撤销。
|
||||
- 中继关闭与开启、容量耗尽、连接中断和大文件背压。
|
||||
- 数据库备份恢复、版本升级、滚动停止和设备重连。
|
||||
- 生产 TLS、OIDC 和对象存储组合的端到端验证。
|
||||
|
||||
设计完成不代表服务已实现;只有独立部署和真实 Desktop 端到端路径通过后才能更新实施状态。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user