Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
|
||||
|
||||
@@ -56,6 +56,14 @@ OpenCode/Continue 用例默认读取 `dist/harness-package-probe/win-unpacked`
|
||||
调用。测试不会输出 API Key,文件操作在临时工作区中执行。文件包含经 Main 回环
|
||||
broker 调用已分配自定义 MCP 的真实 OpenCode 和 Continue 用例。
|
||||
|
||||
不使用外部模型的 Runtime 原生交互回归位于
|
||||
`src/main/agent/opencode-runtime-permissions.test.ts`、
|
||||
`src/main/agent/continue-runtime-permissions.test.ts` 和
|
||||
`src/main/agent/deepseek-harness-acp-e2e.test.ts`。它们用本地确定性模型驱动真实 Runtime
|
||||
或固定 Host,覆盖工作区外访问、原生问答及 Ask 边界。OpenCode 用例默认使用开发缓存
|
||||
二进制;可通过 `GOODBUDDY_TEST_OPENCODE_BINARY` 指定已经安装的候选二进制,避免把
|
||||
仍在运行的旧缓存版本当作候选版本验证。这不替代当前源码 Agent 的 Linux Host 验收。
|
||||
|
||||
## 生产构建
|
||||
|
||||
生成 Electron Main、Preload 和 Renderer 生产文件:
|
||||
@@ -145,7 +153,7 @@ npm run dist:linux:arm64
|
||||
组件,缺少远端包也不能阻塞普通桌面打包或发布。
|
||||
|
||||
“设置 > 平台功能 > 远程项目(技术预览)”列出用户数据目录中已经下载或导入的
|
||||
Linux x64/arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
|
||||
Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
|
||||
小型签名目录,显示本地版本、在线最新兼容版本以及 `有更新 / 已是最新 / 未下载 /
|
||||
校验失败`;此检查不会下载 `.gbagent`。只有用户点击“下载”或“更新到”后才传输包,
|
||||
用户也可导入或导出 `.gbagent` 离线包。
|
||||
@@ -211,6 +219,9 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host
|
||||
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
|
||||
|
||||
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
|
||||
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
|
||||
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
|
||||
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
|
||||
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
|
||||
- DSH 插件安装使用精确锁定并从 `app.asar` 解包的 npm CLI,通过当前 Electron 的 Node 模式运行;最终用户不需要另装 Node.js 或 npm。
|
||||
- DSH 图片输入使用精确锁定的 `@napi-rs/canvas` 完整解码 JPEG/PNG。通用包与目标平台、目标架构的 Skia 原生包必须从 `app.asar` 解包;当打包 Runner 的架构与目标架构不同时,发布脚本会根据 lockfile 的精确版本、下载地址和 integrity 临时暂存目标原生包,完成后清理。发布校验会检查版本、目标架构和 MIT 许可证。
|
||||
@@ -254,16 +265,32 @@ node build/agent-package.cjs build \
|
||||
Agent 源码、共享协议、lock、bundle 工具和测试与桌面应用保持在同一仓库、同一 commit;
|
||||
不维护长期分叉的 Agent 源码分支。`.github/workflows/agents.yml` 在相关 Pull Request、
|
||||
`main` push 和手工触发时,分别使用 `ubuntu-24.04` 与 `ubuntu-24.04-arm` 原生构建
|
||||
Linux x64/arm64 复合包。每个 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
|
||||
Linux x64/arm64 复合包,并使用 `macos-15` 原生构建 Darwin arm64
|
||||
复合包。所有 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
|
||||
地址与 digest,并从 `remote-runtime-lock.json` 解析固定 OpenCode npm 包与 integrity;
|
||||
下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层
|
||||
测试签名验证、Agent 原生启动 smoke 和确定性 `.gbagent` 对比。
|
||||
测试签名验证、Agent CLI 加载 smoke 和确定性 `.gbagent` 对比。所有目标还验证
|
||||
Koffi 原生模块加载与 OpenCode `--version`;Linux job 继续运行 Agent 原生回归。
|
||||
|
||||
Darwin arm64 的 Node 和 OpenCode 固定输入分别由同一份
|
||||
`agent-runtime-lock.json` 与 `remote-runtime-lock.json` 提供,不再维护 CI 专用副本。
|
||||
组包器区分 ELF 与 Mach-O,Darwin CI 产物命名为
|
||||
`goodbuddy-agent-<version>-darwin-arm64.gbagent`,不构建 Intel Mac 目标。
|
||||
macOS Host 继续通过 SSH 按需启动 detached Agent,不注册 launchd 或开机服务。
|
||||
进程身份使用 macOS 原生接口,Unix Socket 使用 `getpeereid`;Host 下载兼容系统自带
|
||||
`shasum -a 256`,不要求安装 GNU coreutils。真实 Host 验证记录见
|
||||
[远程主机技术设计](./docs/features/remote-host/technical-design.md)。
|
||||
|
||||
三个目标共用 `agent-catalog.json` 和现有签名格式,条目按平台与架构区分。旧版 Desktop
|
||||
只接受 Linux catalog 条目;首次发布包含 Darwin 的累计 catalog 前必须先发布新版
|
||||
Desktop,并在发布说明中要求用户升级 Desktop。旧版的在线 Agent 目录检查会失败,
|
||||
本地已验证包不被删除。不得为此改写历史包或复用发布标签。
|
||||
|
||||
该验证 workflow 不读取 production signing secret,不修改公开 key registry,也不上传
|
||||
可安装 Agent 工件。`.github/workflows/agent-release.yml` 是唯一 production Agent
|
||||
发布路径:只接受指向受保护 `main` 历史的 annotated
|
||||
`agent-v${agent-runtime-lock.agentVersion}` 标签,在 `agent-signing` Environment 中
|
||||
原生构建两种架构,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
|
||||
原生构建 Linux x64、Linux arm64、macOS arm64 三个目标,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
|
||||
与北京 OSS。该 Environment 只保护一组 GoodBuddy 通用发布变量与 Secret:
|
||||
|
||||
- `GOODBUDDY_SIGNING_KEY_ID` /
|
||||
@@ -284,7 +311,7 @@ OSS 先写 `agent-releases/v<version>/` 不可变对象,GitHub Release 公开
|
||||
|
||||
### 远程 OpenCode Runtime 基础
|
||||
|
||||
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.9 的 Linux x64
|
||||
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.29 的 Linux x64
|
||||
baseline 与 arm64 官方包 integrity、`bin/opencode` 入口和固定 `acp` 参数。它不同于
|
||||
`agent-runtime-lock.json`,后者锁定 Agent 自带 Node 以及用于 Linux `SO_PEERCRED`
|
||||
的 Koffi 版本。Agent bundle 将 Koffi loader 作为 external module,并只携带目标
|
||||
@@ -371,8 +398,8 @@ DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
|
||||
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
|
||||
```
|
||||
|
||||
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
|
||||
ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
|
||||
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
|
||||
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
|
||||
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
|
||||
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
|
||||
@@ -408,8 +435,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 +483,7 @@ git push github "$tag"
|
||||
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
|
||||
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
|
||||
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
|
||||
DMG 和 ZIP,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
|
||||
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
|
||||
任务失败,不能静默降级为未签名包。
|
||||
|
||||
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
|
||||
|
||||
+156
-38
@@ -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.12.11. 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,
|
||||
@@ -32,6 +38,18 @@ otherwise.
|
||||
Host heading and only the remote path on each project row.
|
||||
- [x] **File, screenshot, window, and clipboard context**: Added to model
|
||||
context only after explicit user selection.
|
||||
- [x] **Per-file workspace diffs**: Changed files expose separate staged and
|
||||
unstaged diffs, including deleted, renamed, and untracked files. Refresh
|
||||
reloads the selected diff, browsed directory, and expanded directories; more than 50 changes
|
||||
remain accessible through incremental loading.
|
||||
- [x] **Workspace file and Git management**: Create, rename, move, and delete
|
||||
files or directories, inspect properties, search and switch local or remote
|
||||
branches, create branches, explicitly Fetch, and browse commit history and
|
||||
per-file commit diffs. File and Git controls are grouped by view, branch
|
||||
search uses the shared compact form style, and subdirectories retain text
|
||||
breadcrumbs without an isolated root icon. 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 +63,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 +76,46 @@ 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] **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.
|
||||
- [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 +124,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 +156,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 +169,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 +198,15 @@ 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.23`, while the current Desktop release candidate is `0.12.11`; formal
|
||||
publication status follows the separate Agent and Desktop
|
||||
release channels.
|
||||
release channels. Current macOS source has passed native package installation,
|
||||
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
|
||||
separate process groups on a real Host; this does not imply publication.
|
||||
Agent `0.11.23` adds remote workspace management and optional model limits;
|
||||
its bundled Runtime no longer imposes a fixed ten-minute Prompt deadline.
|
||||
Unlimited request duration retains a separate connection timeout. Upgrade
|
||||
Desktop to `0.12.11` before downloading and updating the Host's Agent.
|
||||
- [x] **Manual SSH Host environment provisioning source path**: After Host Key,
|
||||
authentication, and system probes succeed, GoodBuddy saves the Host and
|
||||
read-only probes the shared Agent/Runtime. Saving a Host or opening a project
|
||||
@@ -177,14 +233,17 @@ 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
|
||||
@@ -202,11 +261,39 @@ 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.
|
||||
- [x] **Long context and paged tool output**: Message history, long replies,
|
||||
and parsed document text no longer use the previous fixed truncation limits.
|
||||
Direct models can continue reading stored command and Subagent output in
|
||||
pages; the selected model's context window and bounded transports still apply.
|
||||
- [x] **Native Runtime interaction routing**: OpenCode and Continue questions
|
||||
support choices, yes/no, free-text answers, and skipping through the existing
|
||||
question card. OpenCode also routes questions from owned child sessions.
|
||||
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**: Each completed or cancelled request
|
||||
closes its own subscription without cancelling parallel conversations.
|
||||
- [x] **Accurate message-footer status**: Distinguishes request preparation,
|
||||
retry waiting, retry dispatch, tool activity, pending answers, and terminal
|
||||
states. Local OpenCode reports native retry attempts and scheduled times;
|
||||
direct models report their own backoff phase. Footer dots are static, and
|
||||
unsupported Runtime retry details are not invented. Conversation-list
|
||||
indicators and send/stop behavior remain unchanged.
|
||||
- [x] **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 +387,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 +431,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,7 +442,11 @@ 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.
|
||||
SQLite FTS5 retrieval, and source tracing. Knowledge selection persists per
|
||||
conversation and starts empty for new conversations. Advanced creation and
|
||||
retrieval parameters are collapsed by default. Documents distinguish ready,
|
||||
processing, and failed states, with source opening, retry, and confirmation
|
||||
before source removal.
|
||||
- [ ] **External knowledge-base connections** (planned): Manage Dify, FastGPT,
|
||||
and RAGFlow instances from the existing Knowledge page, then discover and
|
||||
bind remote knowledge bases with provider-specific retrieval settings and
|
||||
@@ -384,7 +477,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,8 +497,8 @@ 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
|
||||
@@ -422,7 +518,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 +531,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 +564,11 @@ 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.
|
||||
- [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 +609,16 @@ 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.12.11` and Agent `0.11.23`, with
|
||||
OpenCode pinned to `1.18.29`. Publication status follows the independent
|
||||
Desktop and Agent release channels.
|
||||
- [x] **0BSD open-source license**: Original code can be freely used, copied,
|
||||
modified, distributed, and commercialized. Third-party components and
|
||||
resources retain their own licenses.
|
||||
@@ -519,7 +627,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
|
||||
|
||||
|
||||
+34
-17
@@ -14,28 +14,40 @@
|
||||
### 桌面基础、工作空间与上下文
|
||||
|
||||
- [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.12.11` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
|
||||
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
|
||||
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
|
||||
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
|
||||
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异、当前浏览目录和已展开目录,超过 50 项变更可继续加载。
|
||||
- [x] **工作区文件与 Git 管理**:支持文件和目录的新建、重命名、移动、删除、属性查看,以及本地/远程分支搜索与切换、新建分支、显式 Fetch、提交历史和单文件提交差异。工具按文件/Git 视图分组,分支搜索使用统一紧凑表单,子目录保留文字导航而根目录不再显示孤立图标。移动限于工作区内,删除须确认,分支冲突不自动 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] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。编辑使用 multipart 上传附件图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。
|
||||
- [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] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择,设置页显示实际管理员或回退模型来源。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。
|
||||
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方代码不受 Ask 初始化隔离。
|
||||
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权。
|
||||
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析;聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
|
||||
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权,包括工作区外路径及原生子代理工作。
|
||||
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,OpenCode 同时转交属于当前请求的子会话提问。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
|
||||
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求 Ask/Execute 模式,可使用已启用的本机直连模型工具,Ask 仍只读;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
|
||||
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
|
||||
- [x] **OpenCode 事件连接回收**:请求结束或取消后关闭自身订阅,不取消其他并行会话。
|
||||
- [x] **准确的消息底部状态**:区分请求准备、等待重试、开始重试、工具活动、待回答和终态;本机 OpenCode 使用原生重试次数和计划时间,直连模型显示自身退避阶段。消息底部状态点保持静态,未接入的 Runtime 重试不编造提示,会话列表闪点及发送、停止行为不变。
|
||||
- [x] **直连模型命令工作目录**:Execute 命令可通过绝对路径、相对路径或符号链接使用工作区外目录;工作区仍为默认目录和相对路径基准,Ask 保持只读。
|
||||
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
|
||||
- [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 +58,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,9 +66,9 @@
|
||||
- [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 检索及来源追溯。
|
||||
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
|
||||
- [ ] **外部知识库接入**(规划中):在现有知识库页面管理 Dify、FastGPT 和 RAGFlow 实例;新建知识库时可读取并绑定远端知识库,保留 Provider 专属检索配置和引用。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
|
||||
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
|
||||
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
|
||||
@@ -65,16 +77,17 @@
|
||||
- [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] **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 +97,7 @@
|
||||
### 浏览器、通信、语音与应用维护
|
||||
|
||||
- [x] **Runtime 共享内置浏览器**:使用 GoodBuddy 内置的隔离 Chromium,不控制客户端已安装的浏览器;用户通过内置 MCP 总开关和 Runtime 分配决定是否提供给 Execute,开启后不逐次询问。关闭 Agent 浏览器能力不影响用户继续在浏览器工作栏中手动前往、返回、刷新、停止加载、交互或关闭浏览器。直连模型、GoodBuddy 管理的 OpenCode 和 Continue 通过请求级权限共享按 Conversation 归属的会话和串行操作路径,用户导航会改变 Agent 下一步看到的页面。
|
||||
- [x] **浏览器多页签与按需资源**:同一对话可打开多个共享登录状态、页面与导航独立的浏览器页签,模型请求固定使用开始时绑定的目标。未使用的请求预留不创建浏览器资源,已释放页面可重新打开;保留显式截图,普通操作不再触发无用的自动截图。
|
||||
- [x] **客户端电脑控制工具**:与内置浏览器分开管理,并保留范围、取消、超时、输出边界和执行记录。
|
||||
- [x] **远程消息通道项目**:微信 ClawBot、企业微信和钉钉分别拥有系统管理的项目、独立远程会话、工作目录、处理后端、默认 Ask/Execute 模式及任务活动归属;完整回复交由各通道按平台能力控制长度与分段,不再由公共服务统一截断。
|
||||
- [x] **微信 ClawBot 扫码与媒体**:通过独立 Sidecar 完成本机扫码、验证码、加密凭据和文字收发;支持个人微信私聊图片与文件,单条消息最多 4 个附件、解密后合计不超过 12MB。
|
||||
@@ -94,13 +108,16 @@
|
||||
- [x] **本地录音与离线转写**:采集麦克风音频并使用已选择的本地模型离线转写,支持停止、取消、状态反馈和资源释放。
|
||||
- [x] **版本检查与镜像节点**:在“关于与更新”中选择 GitHub(默认)或镜像节点;手动检查、启动时检查和下载页使用同一选择,并只读取固定可信的发布索引,不自动下载或安装。
|
||||
- [x] **应用内反馈**:在“关于与更新”中直接提交问题、建议或体验反馈,支持可选邮箱和单张截图;默认不上传诊断,用户可主动附加有界的最近桌面诊断摘要。失败后保留草稿、诊断选择和同一请求编号;不会附加对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。
|
||||
- [x] **内网兼容模式**:默认开启;允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书,关闭后恢复严格地址和证书校验。
|
||||
- [x] **内网兼容**:允许应用内 HTTP 与无效、自签名或过期的 HTTPS 证书;反馈、微信凭据与媒体等固定平台服务仍保持严格证书校验。
|
||||
|
||||
### 开源、构建与发布
|
||||
|
||||
- 当前源码候选为 Desktop `0.12.11`、Agent `0.11.23`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
|
||||
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
|
||||
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
|
||||
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;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 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
|
||||
|
||||
### 开放接口、团队协作与远程执行
|
||||
|
||||
|
||||
+32
-16
@@ -309,7 +309,9 @@
|
||||
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
|
||||
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
|
||||
单行截断,不能撑宽或挤压侧栏。
|
||||
- 账户与设置入口固定在侧栏底部。已有稳定设置入口时,不在顶栏重复提供同一入口。
|
||||
- 侧栏底部固定显示文件、终端、浏览器和资源监控四个系统工具图标,设置按钮独立靠右。工具
|
||||
图标只负责打开或聚焦助手工作栏面板,不在侧栏复制工具内容。已有稳定设置入口时,不在顶栏
|
||||
重复提供同一入口。
|
||||
|
||||
### 6.9 助手工作栏
|
||||
|
||||
@@ -319,20 +321,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 +358,9 @@
|
||||
含义模糊的“停止浏览器”。地址输入显示浏览器权威提交 URL,但用户编辑期间保留本地草稿,
|
||||
不被画面或状态刷新覆盖;提交裸域名时自动添加 `http://`,提交、失焦后的同步规则不得让
|
||||
尚未提交的文字改变 Agent 看到的页面。
|
||||
- 浏览器工具栏操作的是当前 Conversation 中 Agent 使用的同一隔离浏览器会话,用户导航会
|
||||
有意改变 Agent 下一步看到的页面。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
|
||||
- 浏览器工具栏操作的是该实例绑定的 Conversation 和 Browser Tab。同一 Conversation 的多个
|
||||
Tab 共享隔离登录上下文,但页面、导航、操作队列和元素引用相互独立。模型请求开始时冻结
|
||||
工具目标,用户切换工作栏 Tab 不改变进行中的 Agent 操作。创建中、Agent 操作中或已进入完整交互窗口时禁用其余
|
||||
工具栏控件;仅当前导航仍在加载时保留“停止加载”,且该操作只中止页面加载,不关闭浏览器、
|
||||
不释放 Runtime 上下文。没有会话的活动 Conversation 可以直接在地址栏输入 URL 并“前往”
|
||||
创建会话,但必须复用 Agent 浏览器相同的 URL 策略与能力路径。
|
||||
@@ -576,6 +586,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
### 13.1 聊天
|
||||
|
||||
- 使用 `reading` 壳层,消息流与输入区共享宽度。
|
||||
- 消息头像放在正文区域外侧:助手头像居左,用户头像居右。输入框左边缘与助手正文左边缘对齐,
|
||||
右边缘与用户气泡右边缘对齐;两侧为头像保留独立安全边距,滚动条出现或工作栏缩放不得改变对齐。
|
||||
- 对话标题和当前项目范围位于 `PageHeader` 或对话上下文区,不在消息流中重复。
|
||||
- 已结束且包含正文的助手回复在底部提供带可访问名称和工具提示的复制图标;复制 Markdown
|
||||
原文,不包含推理过程、工具日志或引用元数据,结果通过应用通知反馈。流式回复完成前不显示
|
||||
@@ -609,6 +621,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
|
||||
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
|
||||
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
|
||||
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
|
||||
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
|
||||
排除在可滚动范围之外。
|
||||
|
||||
### 13.2 最近对话
|
||||
|
||||
@@ -640,7 +655,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
- 状态卡片使用统一状态令牌,不只依赖颜色。
|
||||
- 保留“运行概览 / 待处理建议 / 报告与记录 / 心跳计划”四个同级页面。
|
||||
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
|
||||
- “未来分区记忆”仅为长期方向,数据、状态和页面尚未设计,不得显示占位入口。
|
||||
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
|
||||
|
||||
### 13.5 Task Center
|
||||
|
||||
@@ -654,8 +669,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
|
||||
成果查看器,不撑高列表,也不显示 Job/Run 树。
|
||||
- “新建定制任务”使用共享 Modal,明确选择当前或新 Conversation。聊天入口默认当前
|
||||
Conversation,Task Center 入口默认新 Conversation;选择必须持续可见。
|
||||
- 创建 Modal 默认 Execute,并允许主动切换 Ask。Execute 持续显示实际 Runtime、Project、
|
||||
工作目录、工具和审批摘要;不支持工具时不得静默降级。
|
||||
- 定时消息创建 Modal 使用关联 Conversation 的 Runtime 与 Ask/Execute 模式,不再提供独立
|
||||
模式开关。未运行计划的详情显示会话当前模式,已运行任务显示该次执行记录的模式;
|
||||
不得用旧计划字段表示下一次执行权限。不支持工具时不得静默降级。
|
||||
|
||||
### 13.6 运行记录
|
||||
|
||||
@@ -700,7 +716,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 不丢弃草稿,关闭设置中心前必须先保存或撤销。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"agentVersion": "0.11.18",
|
||||
"agentVersion": "0.11.23",
|
||||
"protocol": {
|
||||
"major": 2,
|
||||
"minor": 0
|
||||
@@ -15,6 +15,12 @@
|
||||
"binaryPath": "node-v24.19.0-linux-x64/bin/node",
|
||||
"licensePath": "node-v24.19.0-linux-x64/LICENSE"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"archive": "node-v24.19.0-darwin-arm64.tar.gz",
|
||||
"sha256": "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d",
|
||||
"binaryPath": "node-v24.19.0-darwin-arm64/bin/node",
|
||||
"licensePath": "node-v24.19.0-darwin-arm64/LICENSE"
|
||||
},
|
||||
"linux-arm64": {
|
||||
"archive": "node-v24.19.0-linux-arm64.tar.gz",
|
||||
"sha256": "d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const { readFileSync } = require('node:fs')
|
||||
const {
|
||||
detectBinaryArchitecture,
|
||||
detectElfArchitecture
|
||||
} = require('./binary-architecture.cjs')
|
||||
|
||||
function targetName(architecture, platform = 'linux') {
|
||||
if (
|
||||
!(platform === 'linux' && ['x64', 'arm64'].includes(architecture)) &&
|
||||
!(platform === 'darwin' && architecture === 'arm64')
|
||||
) {
|
||||
throw new Error(`Unsupported Agent build target: ${platform}-${architecture}`)
|
||||
}
|
||||
return `${platform}-${architecture}`
|
||||
}
|
||||
|
||||
function assertTargetBinary(filePath, architecture, platform, label) {
|
||||
targetName(architecture, platform)
|
||||
const header = readFileSync(filePath).subarray(0, 64)
|
||||
const isMachO = header.length >= 8 &&
|
||||
(header.readUInt32LE(0) === 0xfeedfacf ||
|
||||
header.readUInt32BE(0) === 0xfeedfacf)
|
||||
const actual = platform === 'linux'
|
||||
? detectElfArchitecture(header)
|
||||
: isMachO ? detectBinaryArchitecture(header) : undefined
|
||||
if (actual !== architecture) {
|
||||
throw new Error(
|
||||
`${label} architecture mismatch: expected ${platform}-${architecture}, received ${actual ?? 'unknown'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { targetName, assertTargetBinary }
|
||||
+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)
|
||||
|
||||
@@ -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,34 @@
|
||||
# 应用导航与系统工具入口
|
||||
|
||||
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,应用占用主导航,系统工具从
|
||||
侧栏底部快捷打开助手工作栏。该功能同时为后续插件应用和插件工具预留统一的入口类型,但
|
||||
不包含插件市场的安装与运行机制。
|
||||
|
||||
## 文档导航
|
||||
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [PRD](./prd.md) | 用户问题、产品边界、功能要求与产品验收 |
|
||||
| [User Stories](./user-stories.md) | 用户场景及 Given/When/Then 验收 |
|
||||
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
|
||||
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
|
||||
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
| --- | --- |
|
||||
| 系统入口 | GoodBuddy 工作流程或审计所必需的固定入口,用户不能卸载、隐藏或改变类型 |
|
||||
| 应用 | 在主内容区打开完整一级页面的功能,可显示在“当前对话”所在的主导航区 |
|
||||
| 系统工具 | GoodBuddy 自带且不可卸载的辅助能力,包括文件、终端、浏览器和资源监控,从侧栏底部打开助手工作栏中的对应面板 |
|
||||
| 插件应用 | 未来由插件提供的一级页面入口 |
|
||||
| 插件工具 | 未来由插件提供的辅助面板或命令入口 |
|
||||
| 快捷入口 | 只负责打开或聚焦能力,不复制该能力的界面和业务状态 |
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md)
|
||||
- [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)
|
||||
- [助手工作栏多终端页签 PRD](../assistant-workbar/terminal-tabs-prd.md)
|
||||
- [智能心跳 PRD](../smart-heartbeat/prd.md)
|
||||
- [知识库功能文档](../knowledge-base/README.md)
|
||||
@@ -0,0 +1,168 @@
|
||||
# 应用导航与系统工具入口功能逻辑设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 关联 User Stories | [User Stories](./user-stories.md) |
|
||||
|
||||
## 1. 不变量
|
||||
|
||||
1. “当前对话”“运行记录”和“设置”始终是系统入口。
|
||||
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
|
||||
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
|
||||
4. 左下角只提供快捷入口,工具内容仍由助手工作栏承载。
|
||||
5. 隐藏应用不等于停用功能,也不删除、迁移或清理业务数据。
|
||||
6. 快捷入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
|
||||
|
||||
## 2. 入口模型
|
||||
|
||||
```ts
|
||||
type NavigationContribution =
|
||||
| {
|
||||
type: 'system-entry'
|
||||
id: 'chat' | 'activity' | 'settings'
|
||||
placement: 'primary-start' | 'primary-end' | 'footer'
|
||||
}
|
||||
| {
|
||||
type: 'application'
|
||||
id: 'magic-notes' | 'knowledge' | 'heartbeat'
|
||||
source: 'builtin'
|
||||
visible: boolean
|
||||
}
|
||||
| {
|
||||
type: 'system-tool'
|
||||
id: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
workbarAppId: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
}
|
||||
```
|
||||
|
||||
该模型只覆盖首期入口。插件贡献模型随插件 Host、权限和生命周期另行设计。
|
||||
|
||||
## 3. 主导航组合规则
|
||||
|
||||
主导航按以下顺序计算:
|
||||
|
||||
```text
|
||||
当前对话
|
||||
可见的内置应用
|
||||
运行记录
|
||||
```
|
||||
|
||||
当前对话和运行记录不参与排序。首期内置应用默认顺序为魔法笔记、知识库、智能心跳;显隐
|
||||
设置只从中移除对应项,不改变固定入口的位置。
|
||||
|
||||
### 3.1 应用可见性与业务状态
|
||||
|
||||
| 导航可见 | 功能启用 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 是 | 是 | 显示入口并允许进入 |
|
||||
| 否 | 是 | 隐藏入口,后台和数据保持原状 |
|
||||
| 是 | 否 | 不显示不可用入口;设置页保留启用路径 |
|
||||
| 否 | 否 | 不显示入口,保留符合该功能自身规则的数据 |
|
||||
|
||||
“功能启用”由各应用现有产品设置负责。例如魔法笔记启停仍使用原设置;导航偏好不能覆盖它。
|
||||
|
||||
## 4. 系统工具打开规则
|
||||
|
||||
### 4.1 通用流程
|
||||
|
||||
```text
|
||||
用户激活工具按钮
|
||||
-> 校验按钮仍可用
|
||||
-> 向助手工作栏发送打开意图
|
||||
-> 工作栏按应用实例策略查找或创建实例
|
||||
-> 展开工作栏
|
||||
-> 激活目标实例
|
||||
-> 把焦点移入面板标题或首个主要控件
|
||||
```
|
||||
|
||||
若创建失败,工作栏保持展开并在对应能力位置显示错误;主页面和其他工作栏实例不被清空。
|
||||
|
||||
### 4.2 决策表
|
||||
|
||||
| 工具 | 匹配实例存在 | 匹配实例不存在 | 上下文不可用 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文件 | 激活不可关闭的单实例并保留其跟随或固定状态 | 仅在迁移或布局修复时补回 `workspace` | 打开面板并显示目录修复入口 |
|
||||
| 终端 | 激活当前执行空间最近的 `starting` 或 `running` 实例 | 创建一个绑定当前执行空间的实例 | 没有项目时使用本机 Home |
|
||||
| 浏览器 | 激活绑定当前 Conversation 的最近使用实例 | 创建绑定当前 Conversation 的新实例 | 激活最近使用实例;不存在时创建待绑定实例 |
|
||||
| 资源监控 | 激活 `resources` 单实例 | 创建本机 `resources` 单实例 | 部分指标不可用时显示原因并保留其余指标 |
|
||||
|
||||
文件保持单一实例原有绑定。浏览器快捷入口按当前 Conversation 匹配最近使用实例,而工作栏
|
||||
“+”始终新建浏览器实例。固定目标失效时显示该目标的修复状态,不回退到当前项目、默认
|
||||
工作区或当前会话。
|
||||
|
||||
`closing`、`exited`、`interrupted` 和 `failed` 终端不属于可复用实例。当前执行空间只有这些
|
||||
终端时,快捷按钮创建新实例并保留原 Tab 的历史状态。用户需要额外的并行终端时使用工作栏
|
||||
“+”或终端面板中的既有创建命令。
|
||||
|
||||
### 4.3 选中与状态
|
||||
|
||||
工具按钮满足以下条件时显示选中:助手工作栏已展开,且当前活动实例的 `appId` 与工具映射
|
||||
一致。只有实例存在但未激活时不显示选中。
|
||||
|
||||
徽标优先级为:等待用户处理 > 错误 > 运行中 > 无徽标。同一按钮只显示一个视觉徽标,
|
||||
可访问名称包含完整状态。
|
||||
|
||||
资源监控首期不在后台采样,因此不显示负载告警徽标。只有资源面板是工作栏当前活动实例且
|
||||
工作栏已展开时,资源按钮才显示选中状态。
|
||||
|
||||
### 4.4 资源采样状态
|
||||
|
||||
```text
|
||||
inactive -> warming -> loading -> live
|
||||
-> partial
|
||||
-> error
|
||||
live/partial/error -> loading
|
||||
warming/loading/live/partial/error -> inactive
|
||||
```
|
||||
|
||||
- `inactive`:资源面板未处于可见活动状态,没有采样定时器。
|
||||
- `warming`:首次进入或停止后恢复,建立 CPU 和磁盘 IO 区间基线;内存仍可立即显示。
|
||||
- `loading`:正在请求后续样本;请求未结束时跳过后续时钟触发。
|
||||
- `live`:所有首期指标可用,按 2 秒间隔追加样本。
|
||||
- `partial`:至少一个指标可用;不可用项携带稳定错误码,其他指标继续更新。
|
||||
- `error`:本次没有任何可用指标,保留已有历史;自动时钟或手动操作可以重试。
|
||||
|
||||
资源面板为活动实例、工作栏展开且 `document.visibilityState` 为 `visible` 时才离开 `inactive`。
|
||||
任一条件失效即停止定时器并重置区间基线。历史队列只保留最后 150 个样本;时间间隔较长或
|
||||
采样失败时保留真实时间缺口,不补零或伪造中间数据。
|
||||
|
||||
## 5. 设置保存规则
|
||||
|
||||
应用导航偏好与应用业务启停分开保存。Switch 每次切换立即提交完整目标集合;请求完成前禁用
|
||||
三个应用显隐 Switch,下一次操作基于最新确认结果提交,防止并发更新互相覆盖。
|
||||
|
||||
保存失败时:
|
||||
|
||||
- 主导航和 Switch 恢复最后一次成功保存的偏好。
|
||||
- 设置行显示可重试错误。
|
||||
- 不回滚或修改各应用的业务状态。
|
||||
|
||||
## 6. 与需求的对应关系
|
||||
|
||||
| 逻辑 | PRD | User Stories |
|
||||
| --- | --- | --- |
|
||||
| 固定入口不变量 | FR-1、FR-2 | US-A1、US-A2 |
|
||||
| 应用显隐和失败恢复 | FR-3、FR-4、FR-11、FR-12 | US-B1、US-B2、US-B4、US-B5 |
|
||||
| 工具打开决策表 | FR-5 至 FR-10 | US-C1 至 US-C7 |
|
||||
| 资源采样状态 | FR-9 | US-C6 |
|
||||
| 无障碍与窄窗口 | FR-13 | US-D1、US-D2 |
|
||||
|
||||
## 7. 逻辑完整性评估
|
||||
|
||||
首期规则已覆盖固定入口、应用显隐、保存失败、系统工具匹配与显式多实例差异、本机 Home、
|
||||
托管 SSH、无活动会话、目录失效、资源部分可用、采样停止、权限边界、键盘和窄窗口。其他
|
||||
工具面板内部的取消、超时、
|
||||
连接恢复和固定目标失效继续由助手工作栏负责,本功能不复制这些状态机。
|
||||
|
||||
## 8. 后续产品决策
|
||||
|
||||
- 插件工具上线后,快捷栏允许固定的最大数量及溢出规则。
|
||||
- 插件应用是否允许用户排序,以及固定系统入口之间允许的排序范围。
|
||||
|
||||
这些决策不影响首期四个系统工具的固定入口和内置应用显隐实现。
|
||||
@@ -0,0 +1,177 @@
|
||||
# 应用导航与系统工具入口 PRD
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 关联设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[助手工作栏 PRD](../assistant-workbar/prd.md) |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
主侧栏当前把“当前对话”“魔法笔记”“知识库”“智能心跳”和“运行记录”放在同一导航组,
|
||||
底部长期显示“GB 本地工作区”和平台架构。底部信息不能反映用户可以执行的操作,文件、终端
|
||||
和浏览器则需要先找到右侧助手工作栏后才能打开。
|
||||
|
||||
后续插件可能提供完整页面,也可能只提供辅助面板或命令。若不先区分系统入口、应用和工具,
|
||||
插件接入后会同时争用主导航、侧栏底部和助手工作栏,用户也无法判断入口会切换页面还是打开
|
||||
辅助面板。
|
||||
|
||||
## 2. 产品定义
|
||||
|
||||
### 2.1 三类入口
|
||||
|
||||
| 类型 | 首期成员 | 默认位置 | 用户控制范围 |
|
||||
| --- | --- | --- | --- |
|
||||
| 系统入口 | 当前对话、运行记录、设置 | 主导航固定位置或侧栏底部 | 不可隐藏、卸载或改变类型 |
|
||||
| 应用 | 魔法笔记、知识库、智能心跳 | 主导航应用区 | 可控制是否显示;隐藏不删除数据 |
|
||||
| 系统工具 | 文件浏览器、终端、浏览器、资源监控 | 侧栏底部快捷工具栏 | 首期固定提供,不可卸载 |
|
||||
|
||||
运行记录是系统审计页面,不属于工具。文件浏览器、终端、浏览器和资源监控是系统工具;它们
|
||||
的快捷入口位于左下角,实际界面继续由右侧助手工作栏承载。
|
||||
|
||||
### 2.2 后续插件类型
|
||||
|
||||
插件市场开放后,插件可以贡献“插件应用”或“插件工具”。插件不能注册、覆盖或替换系统入口,
|
||||
也不能冒充系统工具。内置应用继续标记为“内置”,不因采用统一注册协议而显示成待安装商品。
|
||||
|
||||
## 3. 目标
|
||||
|
||||
- 用户从侧栏底部一次点击即可打开文件、终端、浏览器或资源监控。
|
||||
- 主导航只承载完整页面,辅助工具不占用一级页面位置。
|
||||
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
|
||||
- 内置应用可以从一个设置位置控制主导航可见性,隐藏后仍可恢复。
|
||||
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
|
||||
|
||||
## 4. 非目标
|
||||
|
||||
- 首期不提供第三方插件搜索、下载、安装、更新、签名验证或卸载。
|
||||
- 不把文件树、终端或浏览器重新实现到左侧栏。
|
||||
- 不改变助手工作栏的面板实例、停靠、执行空间和安全边界。
|
||||
- 不允许隐藏运行记录、当前对话或设置。
|
||||
- 不因隐藏应用而删除魔法笔记、知识库或智能心跳数据。
|
||||
- 不把“显示在侧边栏”解释为停止智能心跳计划或后台任务。
|
||||
- 不提供任意本机程序启动器。
|
||||
- 首期资源监控不采集远程主机、GPU、显存或网络吞吐,不提供后台告警和历史持久化。
|
||||
|
||||
## 5. 功能要求
|
||||
|
||||
### FR-1 入口分类
|
||||
|
||||
系统必须为每个可见入口声明稳定类型:`system-entry`、`application` 或 `system-tool`。界面位置、
|
||||
设置项和点击结果均以该类型为准,不根据当前页面临时改变类型。
|
||||
|
||||
### FR-2 固定系统入口
|
||||
|
||||
“当前对话”固定在主导航首位;“运行记录”固定在应用区之后;“设置”固定在侧栏底部。
|
||||
这些入口不出现在应用显隐、工具固定或插件管理设置中。
|
||||
|
||||
### FR-3 内置应用区
|
||||
|
||||
魔法笔记、知识库和智能心跳显示在当前对话与运行记录之间。应用入口打开对应一级页面,保留
|
||||
现有徽标、范围和页面状态。用户可以在设置中控制其主导航可见性。
|
||||
|
||||
### FR-4 隐藏应用
|
||||
|
||||
隐藏应用只移除主导航入口,不删除数据、不修改业务启停状态,也不终止正在进行的操作。显隐
|
||||
Switch 更新成功后主导航立即生效;更新失败时恢复原状态并允许重试。用户可从设置恢复入口。
|
||||
|
||||
### FR-5 系统工具快捷栏
|
||||
|
||||
侧栏底部提供文件浏览器、终端、浏览器和资源监控四个图标按钮,并保留独立设置按钮。每个
|
||||
按钮具有可见工具提示和可访问名称。首期系统工具顺序固定为文件、终端、浏览器、资源监控。
|
||||
|
||||
### FR-6 文件浏览器入口
|
||||
|
||||
点击文件按钮时展开助手工作栏,并聚焦默认存在且不可关闭的“工作区”单实例。该实例保持
|
||||
“跟随当前上下文”或“固定到指定对象”状态;迁移或布局修复发现实例缺失时补回,并默认跟随
|
||||
当前项目。没有项目时使用已配置的默认工作区。跟随上下文所需的路径不可用时,面板显示修复
|
||||
入口;已有有效固定目标不受当前项目和默认工作区状态影响。
|
||||
|
||||
### FR-7 终端入口
|
||||
|
||||
点击终端按钮时展开助手工作栏。当前执行空间有正在启动或运行中的终端时聚焦最近使用的实例;
|
||||
没有时创建一个绑定当前执行空间的新终端,已退出、已中断或失败的历史 Tab 保持不变。没有
|
||||
项目时当前执行空间为本机 Home。创建额外并行终端继续使用工作栏现有的“+”目录入口。
|
||||
|
||||
### FR-8 浏览器入口
|
||||
|
||||
点击浏览器按钮时展开助手工作栏,并优先聚焦绑定当前 Conversation 的最近使用浏览器实例;
|
||||
没有匹配实例时创建一个。没有活动 Conversation 时聚焦最近使用的浏览器实例;完全不存在时
|
||||
创建待绑定空实例。该快捷入口不会因连续点击创建重复实例;用户通过工作栏“+”选择浏览器时
|
||||
则始终创建新实例。入口不打开系统默认浏览器,也不创建旁路 WebView。
|
||||
|
||||
### FR-9 资源监控入口
|
||||
|
||||
点击资源监控按钮时展开助手工作栏,并打开或聚焦单实例的本机“资源监控”面板。面板显示
|
||||
整机 CPU 使用率、内存使用量与总量、磁盘读写速率,以及 GoodBuddy 桌面进程的 CPU、内存
|
||||
和进程数摘要。指标不可用时显示具体项目和原因,不能显示为 `0`。
|
||||
|
||||
资源面板处于可见活动状态、工作栏已展开且应用窗口未隐藏或最小化时,每 2 秒采样一次,在
|
||||
Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止采样;首期不在后台持续监控,
|
||||
也不据此产生侧栏告警。首次采样及停止后恢复的首次采样只建立 CPU 和磁盘 IO 速率基线,
|
||||
不显示伪造的 `0` 值。
|
||||
|
||||
### FR-10 快捷入口状态
|
||||
|
||||
工具面板已激活时,对应按钮显示选中状态;面板存在但未激活时保持普通状态。运行中、错误或
|
||||
等待用户处理可以显示徽标,但必须有可访问文字,且不得自动抢占当前面板。
|
||||
|
||||
### FR-11 应用导航设置
|
||||
|
||||
设置页提供“应用与工具”管理入口。首期“应用”区域列出三个内置应用并提供“显示在侧边栏”
|
||||
Switch,切换后立即持久化。系统工具区域列出四项内置固定能力,不提供虚假的卸载或停用按钮。
|
||||
|
||||
### FR-12 偏好持久化
|
||||
|
||||
应用可见性按设备持久化。升级时保留当前行为:已有可用内置应用继续显示;魔法笔记原有功能
|
||||
启停设置保持原意,不由新的导航可见性覆盖。
|
||||
|
||||
### FR-13 窄窗口行为
|
||||
|
||||
窄窗口下主侧栏仍按现有抽屉方式显示。系统工具按钮保持单行且不与设置按钮重叠;空间不足时
|
||||
收起文字和非必要分隔,不隐藏四个系统工具或设置。
|
||||
|
||||
## 6. 权限与安全
|
||||
|
||||
- 快捷入口不授予新的文件、命令、网络或浏览器权限。
|
||||
- 系统工具继续使用 Main、Preload、执行空间 Grant、Ask/Execute 和现有审批边界。
|
||||
- Renderer 不因新增入口获得任意文件系统、PTY、进程或 Electron API。
|
||||
- 资源指标由 Main 采集并以有界结构返回;Renderer 不接收进程命令行、环境变量或任意 PID。
|
||||
- 资源样本只保存在当前 Renderer 内存中,不写入运行记录、SQLite 或遥测服务。
|
||||
- 插件贡献类型只是界面与导航声明,不能代替权限清单、用户授权和运行隔离。
|
||||
- 运行记录继续记录由系统工具触发的受审计操作。
|
||||
|
||||
## 7. 产品验收
|
||||
|
||||
- 主导航能明确区分固定系统入口和可配置内置应用。
|
||||
- 运行记录在所有应用显隐组合下保持可见并可打开。
|
||||
- 左下角不再用“本地工作区”与系统架构占据主要空间。
|
||||
- 文件、终端、浏览器和资源监控均可从左下角打开正确的工作栏面板,且不产生第二套内容界面。
|
||||
- 连续点击系统工具不会无意创建重复的浏览器或终端实例;工作区始终复用不可关闭的固定实例。
|
||||
用户通过工作栏“+”可以显式创建额外浏览器或终端实例。
|
||||
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
|
||||
最小化后停止请求新样本。
|
||||
- CPU、内存、磁盘 IO 或 GoodBuddy 指标不可用时,页面区分部分可用与整体失败。
|
||||
- 隐藏内置应用不删除数据、不停止后台计划,并能从设置恢复;保存失败时入口保持原状。
|
||||
- 键盘、屏幕阅读器、浅色主题、深色主题和窄窗口均可完成相同操作。
|
||||
|
||||
## 8. 分期
|
||||
|
||||
### 首期
|
||||
|
||||
- 重构侧栏底部,增加四个系统工具按钮和独立设置按钮。
|
||||
- 建立从 App 到助手工作栏的“打开或聚焦工具”意图接口。
|
||||
- 增加本机资源快照采集与资源监控单实例面板。
|
||||
- 增加内置应用的主导航显示设置。
|
||||
- 保持运行记录为固定系统入口。
|
||||
|
||||
### 后续
|
||||
|
||||
- 插件应用和插件工具注册。
|
||||
- 插件工具固定、取消固定和排序。
|
||||
- 插件市场、权限详情、安装和卸载。
|
||||
- 用户级快捷键与命令面板接入同一贡献命令。
|
||||
@@ -0,0 +1,383 @@
|
||||
# 应用导航与系统工具入口技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| UI 设计 | [UI 设计](./ui-design.md) |
|
||||
|
||||
## 1. 现状
|
||||
|
||||
主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换:
|
||||
|
||||
```ts
|
||||
type WorkspaceView =
|
||||
| 'chat'
|
||||
| 'magic-notes'
|
||||
| 'knowledge'
|
||||
| 'heartbeat'
|
||||
| 'activity'
|
||||
| 'settings'
|
||||
```
|
||||
|
||||
文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts`
|
||||
承载。工作区是不可关闭的单实例应用,浏览器和终端是多实例应用。浏览器多实例的跨进程
|
||||
实现以[浏览器多实例技术设计](../assistant-workbar/browser-tabs-technical-design.md)为准。代码中尚无资源指标采集和资源监控
|
||||
面板。现有侧栏底部 `user-card` 整行打开设置,并显示“本地工作区”和平台架构。
|
||||
|
||||
本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/
|
||||
Renderer 最小实现。文件、终端和浏览器后端保持不变。
|
||||
|
||||
## 2. 模块边界
|
||||
|
||||
| 模块 | 责任 |
|
||||
| --- | --- |
|
||||
| `App.tsx` | 组合主导航、系统工具栏、设置入口和页面离开检查 |
|
||||
| `SidebarSystemTools`(新增) | 渲染四个系统工具按钮并发送打开意图 |
|
||||
| `RightAssistantSidebar.tsx` | 接收打开意图,调用工作栏实例创建或激活逻辑 |
|
||||
| `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 |
|
||||
| `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 |
|
||||
| `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 |
|
||||
| `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 |
|
||||
| `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 |
|
||||
| `application-settings-contracts.ts` | 增加应用导航偏好 schema 和默认值 |
|
||||
| `application-settings-store.ts` | 迁移并原子保存应用导航偏好 |
|
||||
| `PlatformFeaturesSettingsSection` 或独立设置 Section | 编辑“显示在侧边栏”偏好 |
|
||||
|
||||
不要在 `SidebarSystemTools` 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送
|
||||
有界的工具 ID。
|
||||
|
||||
## 3. Renderer 契约
|
||||
|
||||
### 3.1 系统工具 ID
|
||||
|
||||
```ts
|
||||
export const sidebarSystemToolIds = [
|
||||
'workspace',
|
||||
'terminal',
|
||||
'browser',
|
||||
'resources'
|
||||
] as const
|
||||
|
||||
export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number]
|
||||
```
|
||||
|
||||
该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer
|
||||
状态;实际能力调用继续走现有 Preload API。
|
||||
|
||||
### 3.2 打开意图
|
||||
|
||||
`App` 向 `RightAssistantSidebar` 传入单调递增的请求或命令对象:
|
||||
|
||||
```ts
|
||||
type WorkbarOpenIntent = {
|
||||
requestId: number
|
||||
appId: 'workspace' | 'terminal' | 'browser' | 'resources'
|
||||
reason: 'sidebar-system-tool'
|
||||
context?: { conversationId?: string; projectId?: string }
|
||||
}
|
||||
```
|
||||
|
||||
`requestId` 使连续点击同一工具仍能被消费。不要仅设置 `assistantSidebarTab`,因为用户可能已
|
||||
关闭对应实例,且终端需要按执行空间寻找实例。
|
||||
|
||||
建议由 `RightAssistantSidebar` 在消费意图后通过现有 `WorkbarShell` 控制接口完成。控制器必须
|
||||
接收上下文和调用原因,不能只按 `appId` 查找第一个实例:
|
||||
|
||||
```ts
|
||||
type WorkbarController = {
|
||||
openOrActivate(intent: WorkbarOpenIntent): Promise<WorkbarTabInstance>
|
||||
}
|
||||
```
|
||||
|
||||
若当前 `WorkbarShell` 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。
|
||||
|
||||
工作区意图始终激活注册表中的不可关闭单实例;仅在迁移或布局损坏时补回。浏览器意图按
|
||||
`conversationId` 激活最近使用的匹配实例,没有匹配实例时创建一个。工作栏“+”不走该去重
|
||||
路径,选择浏览器时始终创建新实例。
|
||||
|
||||
### 3.3 终端匹配
|
||||
|
||||
工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用:
|
||||
|
||||
1. 解析当前项目对应的 `ExecutionSpaceSelection`。
|
||||
2. 在现有终端实例中按目标 identity 查找 `starting` 或 `running` 状态。
|
||||
3. 存在多个匹配实例时激活最近使用的实例。
|
||||
4. 没有匹配实例时调用现有终端创建流程。
|
||||
|
||||
`closing`、`exited`、`interrupted` 和 `failed` 实例保留为历史 Tab,不阻止快捷按钮创建新终端。
|
||||
工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。
|
||||
|
||||
### 3.4 资源监控工作栏应用
|
||||
|
||||
在 `workbar-contracts.ts` 中增加 `resources`:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 'resources',
|
||||
instancePolicy: 'single',
|
||||
defaultContext: 'application'
|
||||
}
|
||||
```
|
||||
|
||||
该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建
|
||||
或聚焦它。
|
||||
|
||||
## 4. 应用导航偏好
|
||||
|
||||
### 4.1 设置结构
|
||||
|
||||
在 `ApplicationSettings` 增加独立字段:
|
||||
|
||||
```ts
|
||||
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat'
|
||||
|
||||
type ApplicationNavigationSettings = {
|
||||
hiddenBuiltInApplicationIds: BuiltInApplicationId[]
|
||||
}
|
||||
```
|
||||
|
||||
默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔
|
||||
字段;Zod schema 必须去重、限制数量并拒绝未知内置
|
||||
ID。
|
||||
|
||||
首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留
|
||||
规则后另行扩展,不提前混入当前 schema。
|
||||
|
||||
### 4.2 与现有开关组合
|
||||
|
||||
入口可见条件为:
|
||||
|
||||
```ts
|
||||
const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id)
|
||||
```
|
||||
|
||||
`magicNotesEnabled` 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合
|
||||
规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置
|
||||
不能写回现有开关。
|
||||
|
||||
### 4.3 持久化与 IPC
|
||||
|
||||
沿用现有应用设置链路:
|
||||
|
||||
```text
|
||||
Renderer
|
||||
-> window.goodbuddy.updates.updateSettings
|
||||
-> settings:application:update
|
||||
-> application-settings-store
|
||||
-> application-settings.json
|
||||
```
|
||||
|
||||
更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不
|
||||
修改已有 `magicNotesEnabled` 或其他平台设置。
|
||||
|
||||
## 5. 侧栏底部重构
|
||||
|
||||
现有 `user-card` 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮:
|
||||
|
||||
```tsx
|
||||
<div className="sidebar-footer">
|
||||
<SidebarSystemTools onOpenTool={openSystemTool} />
|
||||
<button aria-label={t('navigation.settings')} ...>
|
||||
<Settings aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
|
||||
保留设置预加载、焦点恢复和未保存离开确认。
|
||||
|
||||
系统工具按钮调用统一 `openSystemTool`:
|
||||
|
||||
1. 设置工作栏打开意图。
|
||||
2. 展开工作栏。
|
||||
3. 由工作栏创建或激活实例。
|
||||
4. 按现有策略把焦点移入目标面板。
|
||||
|
||||
工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。
|
||||
|
||||
## 6. 工作栏状态与快捷按钮
|
||||
|
||||
`RightAssistantSidebar` 应向 `App` 暴露当前活动实例的 `appId` 和必要的有界状态摘要,或由
|
||||
现有 `onTabChange` 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。
|
||||
|
||||
```ts
|
||||
type SidebarSystemToolState = {
|
||||
activeToolId?: SidebarSystemToolId
|
||||
statusByToolId?: Partial<Record<SidebarSystemToolId, 'attention' | 'error' | 'running'>>
|
||||
}
|
||||
```
|
||||
|
||||
首期可以只实现 `activeToolId`,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误
|
||||
或运行状态。
|
||||
|
||||
## 7. 应用显隐更新
|
||||
|
||||
应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只
|
||||
允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航
|
||||
随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功
|
||||
保存的设置。
|
||||
|
||||
## 8. 插件兼容边界
|
||||
|
||||
首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。
|
||||
插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek
|
||||
Harness 的 npm 插件市场。
|
||||
|
||||
## 9. 本机资源监控
|
||||
|
||||
### 9.1 采集范围
|
||||
|
||||
首期在 Main 中使用经过依赖审查的 `systeminformation` 获取整机 CPU、内存和磁盘 IO,使用
|
||||
Electron `app.getAppMetrics()` 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收
|
||||
Renderer 提供的路径、PID、设备名或命令。
|
||||
|
||||
首期指标定义:
|
||||
|
||||
- CPU:整机忙碌时间占比,规范化为 `0` 至 `100`。
|
||||
- 内存:总字节数、可用字节数和 `总量 - 可用量` 得到的使用量。
|
||||
- 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。
|
||||
- GoodBuddy:`app.getAppMetrics()` 返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程
|
||||
CPU 先相加,再按逻辑处理器数量归一化为整机容量占比 `0` 至 `100`;不能通过截断总和处理
|
||||
多核数值。
|
||||
|
||||
受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回
|
||||
不可用错误码,不能回退为 `0`。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链
|
||||
检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。
|
||||
|
||||
### 9.2 快照契约
|
||||
|
||||
```ts
|
||||
type ResourceMetric<T> =
|
||||
| { status: 'available'; value: T }
|
||||
| { status: 'warming-up' }
|
||||
| { status: 'unavailable'; reason: ResourceMetricUnavailableReason }
|
||||
|
||||
type LocalResourceSnapshot = {
|
||||
sampledAt: string
|
||||
host: {
|
||||
cpuPercent: ResourceMetric<number>
|
||||
memory: ResourceMetric<{
|
||||
usedBytes: number
|
||||
totalBytes: number
|
||||
}>
|
||||
diskIo: ResourceMetric<{
|
||||
readBytesPerSecond: number
|
||||
writeBytesPerSecond: number
|
||||
}>
|
||||
}
|
||||
goodbuddy: {
|
||||
cpuPercent: ResourceMetric<number>
|
||||
memoryBytes: ResourceMetric<number>
|
||||
processCount: ResourceMetric<number>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
所有数字必须有限且非负;百分比限制为 `0` 至 `100`,字节数设置跨 IPC 上限。`sampledAt` 由
|
||||
Main 生成。不可用原因使用稳定枚举,例如 `unsupported-platform`、`permission-denied`、
|
||||
`collector-timeout` 和 `collector-failed`,不向 Renderer 返回原始命令输出。`warming-up` 只用于
|
||||
依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。
|
||||
|
||||
### 9.3 Preload 与 IPC
|
||||
|
||||
Preload 只暴露:
|
||||
|
||||
```ts
|
||||
resourceMonitor.getLocalSnapshot(input: {
|
||||
resetIntervalBaseline: boolean
|
||||
}): Promise<LocalResourceSnapshot>
|
||||
```
|
||||
|
||||
对应 IPC 为 `resource-monitor:local-snapshot:get`。Main 验证可信 sender;单次采集设置超时,
|
||||
并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回
|
||||
有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集
|
||||
任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。
|
||||
|
||||
合并规则区分普通请求与基线重置请求:
|
||||
|
||||
- 普通请求在途时,新的普通请求复用它。
|
||||
- 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能
|
||||
用普通请求结果满足重置请求。
|
||||
- 多个重置请求复用同一个在途或待执行重置。
|
||||
- 重置请求在途时,普通请求复用其 `warming-up` 结果,下一次时钟再取得区间值。
|
||||
|
||||
因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列
|
||||
长度。
|
||||
|
||||
面板每次从非活动状态恢复后的首次请求传入 `resetIntervalBaseline: true`。Main 消费本次
|
||||
`systeminformation` 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 `warming-up`;
|
||||
内存等瞬时指标正常返回。后续请求传入 `false` 并计算当前区间值,避免把面板停用期间计入
|
||||
速率。
|
||||
|
||||
### 9.4 轮询与历史
|
||||
|
||||
`ResourceMonitorPanel` 仅在工作栏展开、`resources` 为活动实例且
|
||||
`document.visibilityState === 'visible'` 时启动轮询:
|
||||
|
||||
1. 进入活动状态后立即请求一次,并设置 `resetIntervalBaseline: true`。
|
||||
2. 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。
|
||||
3. Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。
|
||||
4. Renderer 环形队列最多保留 150 个带真实时间戳的样本。
|
||||
5. 采集失败不追加伪造样本,也不持久化历史。
|
||||
|
||||
资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 `resources` 实例
|
||||
及其活动状态。
|
||||
|
||||
## 10. 测试
|
||||
|
||||
### 10.1 Renderer 测试
|
||||
|
||||
- 固定入口顺序不受三个应用显隐组合影响。
|
||||
- 运行记录和设置没有显隐控件。
|
||||
- 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。
|
||||
- 文件复用不可关闭单实例;浏览器和终端按当前上下文聚焦匹配实例,工作栏“+”允许显式多实例。
|
||||
- 工具按钮展开工作栏但不改变当前主页面。
|
||||
- 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。
|
||||
- 窄窗口下工具按钮不重叠,焦点恢复正确。
|
||||
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
|
||||
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
|
||||
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
|
||||
|
||||
### 10.2 Main 与契约测试
|
||||
|
||||
- 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。
|
||||
- 旧版本设置迁移后默认显示所有既有应用。
|
||||
- 更新导航偏好不修改魔法笔记启停等无关字段。
|
||||
- 原子写入失败时保留最后一次有效设置。
|
||||
- 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。
|
||||
- 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。
|
||||
- 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。
|
||||
- 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。
|
||||
- `resetIntervalBaseline: true` 时把 CPU 与磁盘 IO 标记为 `warming-up`。
|
||||
- GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。
|
||||
- GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在 `0` 至 `100`。
|
||||
- GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。
|
||||
|
||||
### 10.3 回归范围
|
||||
|
||||
- 工作栏布局恢复和已关闭实例重建。
|
||||
- 本地与托管 SSH 终端目标。
|
||||
- 浏览器无会话、创建中和 Agent 操作中状态。
|
||||
- 设置页未保存离开确认。
|
||||
- 魔法笔记待办数和智能心跳建议数徽标。
|
||||
- 浅色、深色、`220px` 与 `420px` 侧栏宽度。
|
||||
- Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。
|
||||
|
||||
## 11. 实施顺序
|
||||
|
||||
1. 增加应用导航设置 schema、迁移和设置界面。
|
||||
2. 按即时保存结果组合主导航,并补充失败恢复。
|
||||
3. 增加资源快照契约、Main 采集服务和 Preload API。
|
||||
4. 为工作栏增加 `resources` 单实例和 `ResourceMonitorPanel`。
|
||||
5. 拆分侧栏底部设置按钮,加入四个系统工具按钮。
|
||||
6. 为工作栏增加带上下文的 `openOrActivate` 意图接口,以及浏览器和终端匹配规则。
|
||||
7. 接入选中状态、焦点恢复、无障碍文案和响应式样式。
|
||||
8. 完成 Renderer、契约、迁移和跨平台回归测试。
|
||||
|
||||
插件注册与插件市场不与首期改动一并实施。
|
||||
@@ -0,0 +1,204 @@
|
||||
# 应用导航与系统工具入口 UI 设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
|
||||
本文定义入口、设置和轻量资源监控界面。文件、终端和浏览器的内容布局继续遵守助手工作栏
|
||||
文档。
|
||||
|
||||
## 1. 侧栏信息架构
|
||||
|
||||
```text
|
||||
品牌
|
||||
项目选择
|
||||
新对话
|
||||
搜索
|
||||
|
||||
当前对话
|
||||
魔法笔记 3
|
||||
知识库
|
||||
智能心跳 1
|
||||
运行记录
|
||||
|
||||
最近对话
|
||||
...
|
||||
|
||||
────────────────────────
|
||||
[文件] [终端] [浏览器] [资源] [设置]
|
||||
```
|
||||
|
||||
“当前对话”和“运行记录”属于固定系统入口。三个内置应用位于二者之间。侧栏底部不再显示
|
||||
“本地工作区”和 `platform · arch`;排障信息移至关于或诊断页面。
|
||||
|
||||
## 2. 底部系统工具栏
|
||||
|
||||
底部使用连续平整表面和顶部分隔线,不做成账户卡片或嵌套卡片。按钮顺序固定:
|
||||
|
||||
| 位置 | 图标 | 可见工具提示 | 点击结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `FolderOpen` | 文件 | 聚焦不可关闭的工作区面板 |
|
||||
| 2 | `SquareTerminal` | 终端 | 打开或聚焦当前执行空间终端 |
|
||||
| 3 | `Globe` | 浏览器 | 聚焦当前 Conversation 的匹配浏览器实例 |
|
||||
| 4 | `Activity` | 资源监控 | 打开或聚焦本机资源监控面板 |
|
||||
| 末位 | `Settings` | 设置 | 打开设置页面 |
|
||||
|
||||
图标使用项目现有 Lucide 库。每个按钮采用统一的 `34px × 34px` 点击区域,内部图标建议
|
||||
`16px`。工具栏使用 `8px` 内边距和 `4px` 间隔;设置按钮通过自动空白靠右,不添加第二条
|
||||
视觉分隔线。
|
||||
|
||||
### 2.1 状态
|
||||
|
||||
- 默认:透明背景,图标使用次级文字色。
|
||||
- 悬停:使用弱表面色,不能强于当前选中状态。
|
||||
- 选中:使用强调选中背景,并提高图标对比度。
|
||||
- 键盘焦点:显示统一焦点环,不能被侧栏边缘裁切。
|
||||
- 处理中:创建或激活请求未完成时暂时禁用对应按钮,保留尺寸并提供进行中名称。
|
||||
- 徽标:只用于等待处理、错误或活动状态,不显示无意义的常驻绿色圆点。
|
||||
|
||||
选中状态表达“对应工作栏面板正在显示”,不表达功能已安装或权限已授予。
|
||||
|
||||
## 3. 主导航应用区
|
||||
|
||||
应用入口沿用现有 `nav-item` 行,不使用应用卡片。图标、名称和徽标保持单行:
|
||||
|
||||
```text
|
||||
[图标] 魔法笔记 3
|
||||
[图标] 知识库
|
||||
[图标] 智能心跳 1
|
||||
```
|
||||
|
||||
隐藏某个应用后,其余应用向上补位。运行记录始终位于最后一个可见应用之后,不因应用显隐
|
||||
改变样式或归类为工具。
|
||||
|
||||
设置页打开期间,主侧栏显示最后一次成功保存的导航状态。Switch 更新成功后导航立即变化,
|
||||
失败时保持原导航。
|
||||
|
||||
## 4. 应用与工具设置
|
||||
|
||||
设置中心新增“应用与工具”分类,采用 `standard` PageShell:
|
||||
|
||||
```text
|
||||
应用与工具
|
||||
管理主导航中的应用。系统工具由 GoodBuddy 提供并固定在侧栏底部。
|
||||
|
||||
应用
|
||||
魔法笔记 笔记、待办和评论 [显示在侧边栏 开]
|
||||
知识库 管理和检索知识 [显示在侧边栏 开]
|
||||
智能心跳 查看计划、建议和运行历史 [显示在侧边栏 开]
|
||||
|
||||
系统工具
|
||||
文件 打开当前工作区 内置
|
||||
终端 进入当前执行空间 内置
|
||||
浏览器 打开 GoodBuddy 受控浏览器 内置
|
||||
资源监控 查看本机 CPU、内存和磁盘 IO 内置
|
||||
|
||||
系统入口
|
||||
当前对话、运行记录和设置位置固定,不参与上述配置。
|
||||
```
|
||||
|
||||
应用显隐使用共享 Switch,标签必须是“显示在侧边栏”,不能缩写为“启用”。切换时立即保存,
|
||||
请求期间禁用三个应用显隐 Switch;失败后恢复原值,并在对应设置行显示可重试错误。系统工具
|
||||
使用紧凑只读列表,不渲染禁用的卸载、停用或固定按钮。
|
||||
|
||||
功能已停用时,对应应用行显示“功能已停用”和设置入口,保留显隐偏好但暂时禁用 Switch。
|
||||
|
||||
若某应用自身支持业务启停,应在该应用原有设置中保留“启用功能”开关,并用说明区分:
|
||||
|
||||
- “显示在侧边栏”只控制入口。
|
||||
- “启用功能”控制该功能是否运行。
|
||||
|
||||
## 5. 助手工作栏反馈
|
||||
|
||||
点击系统工具后,右侧工作栏按现有布局回流打开。主内容页面保持不变,侧栏不渲染工具内容。
|
||||
|
||||
- 文件:激活默认存在且不可关闭的“工作区”Tab,焦点进入目录区域或错误修复入口。
|
||||
- 终端:激活匹配终端,焦点进入终端输入区;新建期间保留按钮宽度和进度状态。
|
||||
- 浏览器:激活绑定当前 Conversation 的最近使用“浏览器”Tab;没有匹配实例时创建一个,
|
||||
没有 Conversation 时按产品规则复用最近实例或进入待绑定空状态。
|
||||
- 资源监控:激活“资源监控”Tab,焦点进入指标摘要;首个样本加载后开始绘制历史。
|
||||
|
||||
后台事件可以更新徽标,不能自动展开工作栏或切换当前 Tab。由用户点击快捷按钮触发的打开
|
||||
属于显式操作,可以展开工作栏并切换 Tab。
|
||||
|
||||
### 5.1 资源监控面板
|
||||
|
||||
资源监控使用紧凑单列,不使用指标卡片墙:
|
||||
|
||||
```text
|
||||
资源监控 本机
|
||||
|
||||
CPU 18%
|
||||
[最近 5 分钟折线图]
|
||||
|
||||
内存 9.6 / 32 GB
|
||||
[使用量进度条]
|
||||
|
||||
磁盘 IO 读 12 MB/s · 写 3 MB/s
|
||||
|
||||
GoodBuddy
|
||||
CPU 4% · 内存 620 MB · 桌面进程 7
|
||||
|
||||
更新于 14:32:08
|
||||
```
|
||||
|
||||
- CPU 折线纵轴固定为 `0%` 至 `100%`,不能根据当前低负载动态放大波动。
|
||||
- 内存同时显示使用量、总量和百分比;进度条不代替文字数值。
|
||||
- 磁盘 IO 使用每秒读写字节并按统一单位格式化,不显示累计设备字节数。
|
||||
- GoodBuddy 摘要只显示聚合值,不列 PID、命令行或环境变量。
|
||||
- 首次加载或停止后恢复时,CPU 和磁盘 IO 显示“正在建立采样基线”,不先渲染 `0%`;内存
|
||||
等瞬时指标可以立即显示。
|
||||
- 部分可用时只在对应指标行显示“不可用”和简短原因;全部失败时显示重试入口。
|
||||
- 图表不使用动效平移;`prefers-reduced-motion` 下行为不变。
|
||||
- 面板失去活动状态、工作栏收起或窗口隐藏、最小化时停止更新;返回后重新建立区间指标基线,
|
||||
不填补离开期间的历史。
|
||||
|
||||
## 6. 文案
|
||||
|
||||
固定使用以下名称:
|
||||
|
||||
| 对象 | 文案 |
|
||||
| --- | --- |
|
||||
| 设置分类 | 应用与工具 |
|
||||
| 文件系统工具 | 文件 |
|
||||
| 终端系统工具 | 终端 |
|
||||
| 浏览器系统工具 | 浏览器 |
|
||||
| 浏览器说明 | GoodBuddy 受控浏览器 |
|
||||
| 资源系统工具 | 资源监控 |
|
||||
| 资源范围 | 本机 |
|
||||
| 指标缺失 | 不可用 |
|
||||
| 应用显隐开关 | 显示在侧边栏 |
|
||||
| 内置来源 | 内置 |
|
||||
|
||||
不再使用“本地工作区”作为侧栏底部身份名称。项目和默认工作区的路径继续由项目选择器、页面
|
||||
范围或工作区面板显示。
|
||||
|
||||
## 7. 键盘和无障碍
|
||||
|
||||
- 底部按钮均为独立 `button`,不能嵌套在整行设置按钮中。
|
||||
- Tab 顺序为文件、终端、浏览器、资源监控、设置。
|
||||
- 图标按钮通过 `aria-label` 提供名称,工具提示不能作为唯一可访问名称。
|
||||
- 工作栏打开后按现有焦点策略聚焦目标面板;用户通过固定工作栏开关关闭时,焦点返回该开关。
|
||||
- 选中按钮使用 `aria-pressed` 或等价的可访问状态,不能只依靠背景色。
|
||||
- 徽标的数量或状态合并进按钮可访问名称,例如“终端,1 个等待处理”。
|
||||
- 应用显隐 Switch 支持 Space 切换,保存失败后恢复原值并保留焦点。
|
||||
|
||||
## 8. 响应式
|
||||
|
||||
- 侧栏宽度 `220px` 至 `420px` 时,底部按钮尺寸不随宽度缩放。
|
||||
- 窄窗口继续使用现有侧栏抽屉;工具栏不转为多行。
|
||||
- 空间不足时缩小设置按钮前的弹性空白,不隐藏四个系统工具和设置。
|
||||
- 工具提示不得超出窗口;靠近底边时显示在按钮上方。
|
||||
- 粗指针设备不依赖悬停,按钮的可访问名称和选中反馈保持有效。
|
||||
|
||||
## 9. 未来插件入口
|
||||
|
||||
插件市场交付后另行定义插件区域、固定数量、溢出和权限界面。系统入口与四个系统工具的位置
|
||||
保持保留,不向插件开放。
|
||||
@@ -0,0 +1,236 @@
|
||||
# 应用导航与系统工具入口 User Stories
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-09-08 |
|
||||
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
|
||||
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
## 1. 角色
|
||||
|
||||
### 1.1 普通用户
|
||||
|
||||
使用对话、笔记和知识功能,希望常用入口稳定,不需要理解工作栏实例模型。
|
||||
|
||||
### 1.2 本地工作用户
|
||||
|
||||
经常查看项目文件、打开终端、使用受控浏览器或检查电脑负载,希望从任意一级页面快速进入
|
||||
这些工具。
|
||||
|
||||
## 2. Epic A:固定系统入口
|
||||
|
||||
### US-A1 始终访问运行记录
|
||||
|
||||
作为用户,我希望运行记录保持在固定位置,以便应用入口变化后仍能检查 Agent 和工具活动。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 用户隐藏任意内置应用
|
||||
- When 用户查看主导航
|
||||
- Then “运行记录”仍显示在应用区之后
|
||||
- And 设置中没有隐藏、卸载或固定运行记录的选项
|
||||
|
||||
### US-A2 从固定入口进入设置
|
||||
|
||||
作为用户,我希望设置入口始终位于侧栏底部,以便快捷工具状态变化时仍能进入配置。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 任意主页面和任意侧栏宽度
|
||||
- When 用户查看侧栏底部
|
||||
- Then 设置按钮保持可见且点击区域不与工具按钮重叠
|
||||
- And 点击后继续遵守未保存内容的离开确认
|
||||
|
||||
## 3. Epic B:管理应用入口
|
||||
|
||||
### US-B1 识别完整应用
|
||||
|
||||
作为用户,我希望魔法笔记、知识库和智能心跳显示在主导航,以便知道它们会打开完整页面。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 对应功能已启用且应用设置为显示
|
||||
- When 用户打开主侧栏
|
||||
- Then 应用位于“当前对话”和“运行记录”之间
|
||||
- And 点击应用在主内容区打开对应一级页面
|
||||
- And 原有待办或建议徽标继续显示
|
||||
|
||||
### US-B2 隐藏低频应用
|
||||
|
||||
作为用户,我希望隐藏不常用的内置应用,以便缩短主导航,但保留已有数据。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 应用已有内容
|
||||
- When 用户关闭该应用的“显示在侧边栏”Switch
|
||||
- Then 主导航不再显示该应用
|
||||
- And 应用数据、计划和业务启停状态不变
|
||||
- And 用户可从同一设置位置恢复显示
|
||||
|
||||
### US-B4 保留功能启停语义
|
||||
|
||||
作为魔法笔记用户,我希望导航显隐与功能启停相互独立,以免整理导航时关闭功能。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 魔法笔记功能已启用
|
||||
- When 用户只隐藏其侧栏入口
|
||||
- Then 笔记及待办处理能力继续按原设置工作
|
||||
- And 再次显示时恢复原页面和数据
|
||||
|
||||
### US-B5 恢复显隐保存失败
|
||||
|
||||
作为用户,我希望入口设置保存失败时继续使用原导航,以免界面状态与实际设置不一致。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 应用显隐更新尚未完成
|
||||
- When 用户切换“显示在侧边栏”Switch
|
||||
- Then 三个应用显隐 Switch 在请求期间暂时不可操作
|
||||
- And 更新成功后主导航立即变化
|
||||
- And 更新失败时 Switch 和主导航恢复原状态并显示可重试错误
|
||||
|
||||
## 4. Epic C:使用系统工具
|
||||
|
||||
### US-C1 一键查看文件
|
||||
|
||||
作为本地工作用户,我希望点击左下角文件按钮后直接查看当前工作区文件。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 当前项目有有效根目录
|
||||
- When 用户点击文件按钮
|
||||
- Then 助手工作栏展开
|
||||
- And 聚焦默认存在且不可关闭的工作区单实例
|
||||
- And 文件按钮通过选中状态表明该面板正在显示
|
||||
|
||||
### US-C2 处理无效工作区
|
||||
|
||||
作为用户,我希望目录不可用时看到修复方法,并收到明确反馈。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 工作区面板处于跟随模式
|
||||
- And 当前项目和默认工作区均不可用
|
||||
- When 用户点击文件按钮
|
||||
- Then 工作区面板仍打开
|
||||
- And 面板说明目录不可用并提供选择或修复目录的入口
|
||||
- And 应用不静默改用其他目录
|
||||
|
||||
### US-C3 快速进入终端
|
||||
|
||||
作为本地工作用户,我希望点击终端按钮后进入当前执行空间的终端。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 当前执行空间已有正在启动或运行中的终端
|
||||
- When 用户点击终端按钮
|
||||
- Then 工作栏聚焦最近使用的匹配终端,不创建重复实例
|
||||
- Given 当前执行空间没有终端
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建一个绑定该执行空间的新终端
|
||||
- Given 当前没有项目
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建或聚焦使用本机 Home 的终端
|
||||
- Given 当前执行空间只有已退出、已中断或失败的终端
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建新终端并保留原终端的历史状态
|
||||
|
||||
### US-C4 使用远程项目终端
|
||||
|
||||
作为远程项目用户,我希望同一个终端按钮进入远程项目目录,以免误在本机执行命令。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 当前项目绑定托管 SSH 执行空间
|
||||
- When 用户点击终端按钮
|
||||
- Then 创建或聚焦绑定该远程项目及其权威目录的终端
|
||||
- And 面板持续显示远程主机和工作目录
|
||||
- And 连接失败时保留重连与诊断信息
|
||||
|
||||
### US-C5 打开受控浏览器
|
||||
|
||||
作为用户,我希望浏览器按钮打开 GoodBuddy 受控浏览器,以便查看与 Agent 相同的会话。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 浏览器面板跟随当前上下文且当前会话已有浏览器会话
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 工作栏聚焦绑定当前 Conversation 的最近使用浏览器实例
|
||||
- And 不打开系统默认浏览器或无意创建重复实例
|
||||
- Given 现有浏览器实例都绑定到其他 Conversation
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 工作栏创建并聚焦绑定当前 Conversation 的新浏览器实例
|
||||
- Given 当前没有活动 Conversation
|
||||
- And 已存在浏览器实例
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 工作栏聚焦最近使用的浏览器实例并保持其目标
|
||||
- Given 当前没有活动 Conversation
|
||||
- And 不存在浏览器实例
|
||||
- When 用户点击浏览器按钮
|
||||
- Then 创建待选择目标的空实例
|
||||
- Given 用户需要同一 Conversation 的另一个独立页面
|
||||
- When 用户从工作栏“+”选择浏览器
|
||||
- Then 每次创建拥有独立页面和导航状态的新实例
|
||||
- And 同一 Conversation 内共享隔离登录上下文
|
||||
|
||||
### US-C6 查看本机资源
|
||||
|
||||
作为本地工作用户,我希望快速查看 CPU、内存和磁盘 IO,以便判断任务变慢是否与本机负载
|
||||
有关。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 资源监控面板尚未打开
|
||||
- When 用户点击左下角资源监控按钮
|
||||
- Then 助手工作栏创建并激活单实例资源监控面板
|
||||
- And 页面显示整机 CPU、内存、磁盘读写及 GoodBuddy 桌面进程摘要
|
||||
- And 页面每 2 秒更新一次,并用图表保留最近 5 分钟
|
||||
- When 用户切换到其他工作栏 Tab、收起工作栏、隐藏窗口或最小化窗口
|
||||
- Then 资源监控停止请求新样本
|
||||
- When 用户再次显示窗口并返回资源监控
|
||||
- Then 首次采样建立区间指标基线,下一次采样开始显示 CPU 和磁盘 IO 速率
|
||||
- Given 某项指标在当前平台不可用
|
||||
- When 页面收到部分结果
|
||||
- Then 该指标显示“不可用”及原因,其他指标继续更新
|
||||
|
||||
### US-C7 从任意应用打开工具
|
||||
|
||||
作为用户,我希望在魔法笔记、知识库、智能心跳和运行记录页面也能打开系统工具。
|
||||
|
||||
验收:
|
||||
|
||||
- Given 用户位于任意一级页面
|
||||
- When 用户点击文件、终端、浏览器或资源监控
|
||||
- Then 主页面保持不变
|
||||
- And 右侧工作栏打开对应面板
|
||||
- And 工具不会覆盖主内容或伪装成一级页面
|
||||
|
||||
## 5. Epic D:可访问性和布局
|
||||
|
||||
### US-D1 使用键盘打开系统工具
|
||||
|
||||
作为键盘用户,我希望按顺序聚焦并打开左下角工具,以便不依赖鼠标。
|
||||
|
||||
验收:
|
||||
|
||||
- 每个系统工具和设置都是独立按钮
|
||||
- Tab 顺序与视觉顺序一致
|
||||
- Enter 和 Space 均可激活按钮
|
||||
- 每个纯图标按钮都有可访问名称和工具提示
|
||||
- 焦点样式不被侧栏边界裁切
|
||||
|
||||
### US-D2 在窄窗口使用快捷栏
|
||||
|
||||
作为小屏用户,我希望侧栏抽屉中的工具按钮保持可操作。
|
||||
|
||||
验收:
|
||||
|
||||
- 工具按钮保持单行且不重叠
|
||||
- 不因宽度不足隐藏文件、终端、浏览器、资源监控或设置
|
||||
- 触控点击区域符合统一界面规范
|
||||
- 关闭侧栏抽屉后焦点返回原触发按钮
|
||||
@@ -7,7 +7,10 @@
|
||||
| 文档 | 权威职责 |
|
||||
| --- | --- |
|
||||
| [通用助手工作栏与执行空间 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 相关改动的实现事实、验证证据与剩余阻塞 |
|
||||
| [Runtime 交互边界](./runtime-interactions.md) | 原生权限、结构化问答、后台拒绝、终端交互、托管 Runtime 并行与输出及消息底部请求状态的适配范围 |
|
||||
|
||||
当前尚未拆出独立 User Stories、功能逻辑、UI 设计、技术设计和进度文档;相关内容仍保留在
|
||||
两份现有 PRD 中,后续实际维护该功能时按职责拆分。
|
||||
当前尚未拆出独立 User Stories、功能逻辑和 UI 设计文档;相关内容仍保留在两份现有 PRD
|
||||
中。浏览器多实例涉及跨进程身份和工具路由,单独使用技术设计文档维护。
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# 浏览器多实例技术设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 基础多实例已实施,Agent 多 Tab 工具待需求 |
|
||||
| 版本 | 1.0 |
|
||||
| 日期 | 2026-09-10 |
|
||||
| 关联 PRD | [通用助手工作栏与执行空间 PRD](./prd.md) |
|
||||
| 页签策略 | [助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) |
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
浏览器改为可关闭的多实例工作栏应用。每个工作栏浏览器实例对应一个独立 Browser Tab,拥有
|
||||
独立页面、导航状态、操作队列、画面状态和元素引用空间。同一 Conversation 下的多个 Tab
|
||||
共享隔离存储与代理上下文,使登录状态符合常规浏览器预期。
|
||||
|
||||
本设计不把多个工作栏 Tab 实现为同一页面的重复入口,不允许用户切换 Tab 后改变正在执行的
|
||||
Agent 工具目标,也不在首期要求模型主动管理多个 Tab。
|
||||
|
||||
## 2. 改造前限制
|
||||
|
||||
改造前的链路以 `conversationId` 作为唯一浏览器身份:
|
||||
|
||||
- Renderer 只保存每个 Conversation 的一份 `BrowserLiveState`。
|
||||
- Browser IPC、Preload API 和 `BrowserService` 操作只携带 `conversationId`。
|
||||
- Main 每个 Conversation 只创建一个 Session、一个 `WebContentsView` 和一个 CDP Driver。
|
||||
- 请求级 MCP 能力只保存 `browserConversationId`。
|
||||
- snapshot 元素 `ref` 只在一个 Driver 的引用空间内有效。
|
||||
|
||||
因此只把工作栏注册策略改成 `multiple` 会导致多个 Tab 控制同一页面,并产生 viewport、清理和
|
||||
元素引用竞态,不能作为可交付实现。
|
||||
|
||||
## 3. 身份与所有权
|
||||
|
||||
```ts
|
||||
type BrowserTabId = string
|
||||
|
||||
type ConversationBrowserContext = {
|
||||
conversationId: string
|
||||
partitionId: string
|
||||
tabs: Map<BrowserTabId, BrowserTab>
|
||||
primaryTabId?: BrowserTabId
|
||||
}
|
||||
|
||||
type BrowserTab = {
|
||||
id: BrowserTabId
|
||||
conversationId: string
|
||||
view: WebContentsView
|
||||
driver: CdpBrowserDriver
|
||||
state: BrowserLiveState
|
||||
}
|
||||
```
|
||||
|
||||
- Conversation Browser Context 拥有隔离 partition、代理配置和全部 Tab。
|
||||
- Browser Tab 拥有页面、Driver、串行操作队列、snapshot 代次、idle 状态和可见 viewport。
|
||||
- `BrowserTabId` 是 Main 签发的不透明 ID。Renderer 和 Runtime 不能自行选择或复用。
|
||||
- 工作栏实例保存公开的逻辑实例 ID 和 Conversation 绑定;进程重启后不恢复旧
|
||||
`WebContentsView`,首次使用时由 Main 为该逻辑实例创建新的 Browser Tab。
|
||||
- 删除 Conversation 时关闭该 Context 下全部 Tab;关闭一个工作栏浏览器实例只关闭对应
|
||||
Browser Tab,不得调用 Conversation 级 Runtime 释放路径。
|
||||
|
||||
## 4. Renderer 与 viewport
|
||||
|
||||
Renderer 状态改为:
|
||||
|
||||
```ts
|
||||
Record<ConversationId, Record<BrowserTabId, BrowserLiveState>>
|
||||
```
|
||||
|
||||
每个浏览器工作栏实例持有自己的 `browserTabId`。只有当前可见实例可以提交 viewport。每次
|
||||
获得可见权时生成新的 UUID `leaseToken`;提交请求携带 `browserTabId` 和 token,清理请求必须
|
||||
回传同一个 token。Main 只接受当前 token 的清理,避免已隐藏实例的延迟 cleanup 隐藏新实例,
|
||||
也避免 Renderer 重载后从零开始的数字序列无法覆盖旧租约。
|
||||
|
||||
非活动 Browser Tab 保持页面和状态,但隐藏 `WebContentsView` 并停止非必要画面采集。工作栏
|
||||
收起、切换主页面和窗口隐藏时采用相同规则,不销毁 Tab。
|
||||
普通导航、快照与页面操作只同步轻量导航状态,不自动截图、等待画面或传输 JPEG;只有显式
|
||||
`browser_screenshot` 调用生成图片。侧栏内终端关闭确认显示期间,同样隐藏原生浏览器视图,
|
||||
确认或取消后恢复当前实例,避免原生子视图遮挡 HTML 确认按钮。
|
||||
|
||||
浏览器应用声明 `visibleAcrossContextSwitches: true`,打开的 Tab 跨项目、会话切换继续显示。
|
||||
切换活动 Conversation 不改变当前选中的浏览器或其 `targetRef.conversationId`;用户在会话 B
|
||||
查看会话 A 的浏览器时,viewport 和手动导航仍指向 A 的原 Tab。新建实例绑定 B。仅未绑定的
|
||||
默认或恢复实例可在首次使用时取得当前会话绑定。
|
||||
|
||||
Tab 的跨会话可见性与 MCP 授权分开处理。B 的请求不能选择属于 A 的可见 Tab;A 已取得的
|
||||
capability 在用户切到 B 后仍固定路由至 A 的原 Tab,使用租约也继续阻止关闭该 Tab。
|
||||
内部点击实例只同步应用类型,不再次按当前 Conversation 改选实例。Main 状态携带对应的
|
||||
`workbarInstanceId`,Renderer 据此绑定请求创建的同一 Tab,而不是另建空白页。收到 `stopped`
|
||||
后清除该实例的旧 Tab 绑定,下一次使用通过原逻辑实例重新创建,保留地址草稿与错误反馈。
|
||||
请求创建的实例也遵守现有 32 个工作栏页签上限;满额时保留 Main 页面及其状态,并就地提示
|
||||
关闭一个可关闭页签。释放位置后再绑定该页面,不写入超限布局或丢弃 Agent 页面。
|
||||
|
||||
## 5. IPC 与服务接口
|
||||
|
||||
共享契约增加有界操作:
|
||||
|
||||
```ts
|
||||
browser.createTab({ conversationId, workbarInstanceId })
|
||||
browser.listTabs({ conversationId })
|
||||
browser.closeTab({ conversationId, tabId })
|
||||
browser.setViewport({ conversationId, tabId, leaseToken, bounds })
|
||||
browser.navigate({ conversationId, tabId, url })
|
||||
browser.back({ conversationId, tabId })
|
||||
browser.reload({ conversationId, tabId })
|
||||
browser.stopLoading({ conversationId, tabId })
|
||||
```
|
||||
|
||||
Main 必须验证 `tabId` 属于同一 `conversationId` 和当前窗口。BrowserService 将现有
|
||||
Conversation 级 slot 拆为 Context 与 Tab 两层;同一 Tab 内操作继续串行,不同 Tab 可以并行,
|
||||
但仍受窗口级和 Conversation 级有界数量限制。
|
||||
|
||||
现有 Conversation 级 `stop/release` 保留为销毁完整浏览器上下文的操作,不能用作关闭单个
|
||||
Browser Tab。
|
||||
|
||||
## 6. Agent 与 MCP 路由
|
||||
|
||||
### 6.1 兼容阶段
|
||||
|
||||
第一阶段不修改模型可见的现有浏览器工具参数。模型请求开始时,Main 按以下顺序选择并冻结
|
||||
`boundBrowserTabId`:
|
||||
|
||||
1. 当前可见且绑定该 Conversation 的 Browser Tab。
|
||||
2. 该 Conversation 的 `primaryTabId`。
|
||||
3. 预留 primary Browser Tab 身份,首次实际浏览器操作时才创建页面与 Conversation Context。
|
||||
|
||||
未使用浏览器的请求只持有随请求释放的内存预留,不创建 Chromium 资源、不触发浏览器 UI、
|
||||
也不占用活动浏览器会话名额。物化后使用同一 Tab 身份和工作栏实例归属;取消、能力撤销及
|
||||
服务关闭释放未使用的预留,不保存到磁盘。编程 Subagent 继承父请求的 Browser Tab 与浏览器
|
||||
Conversation 归属,不新建租约或把页面重新归属到子级对话。
|
||||
|
||||
直连模型上下文和请求级 MCP capability 同时保存 `conversationId` 与
|
||||
`boundBrowserTabId`。请求执行期间,用户切换、创建或关闭其他工作栏 Tab 不改变工具目标;
|
||||
模型请求使用某个 Tab 时禁用该 Tab 的工作栏关闭操作,并说明请求仍在执行。用户先取消请求
|
||||
后才能关闭;系统不得把进行中的操作改投其他 Tab。
|
||||
|
||||
该阶段 MCP transport、endpoint 和 token 生命周期不变,只扩展服务端 capability 内容和内部
|
||||
路由。旧工具调用继续使用被冻结的 Tab。
|
||||
|
||||
### 6.2 Agent 多 Tab 阶段
|
||||
|
||||
只有明确需要 Agent 管理多个页面时,才增加:
|
||||
|
||||
- `browser_tab_list`
|
||||
- `browser_tab_open`
|
||||
- `browser_tab_close`
|
||||
|
||||
导航、snapshot、back 和 screenshot 增加可选 `tabId`;省略时使用请求绑定的默认 Tab。
|
||||
`browser_snapshot` 返回 `tabId` 和 snapshot 代次。click、type 和 select 必须校验 capability、
|
||||
`tabId`、引用代次和元素 `ref` 属于同一 Driver,禁止跨 Tab 猜测或解析引用。
|
||||
|
||||
## 7. 工作栏创建与快捷入口
|
||||
|
||||
- 从“+”选择浏览器时始终创建新工作栏实例,并默认绑定当前 Conversation。
|
||||
- 没有活动 Conversation 时创建待绑定实例,不创建无归属 Main Browser Context。
|
||||
- 主侧栏浏览器快捷按钮优先激活绑定当前 Conversation 的最近使用实例;没有匹配实例时创建
|
||||
一个。没有活动 Conversation 时激活最近使用的浏览器实例;完全不存在时创建待绑定实例。
|
||||
- New browser instances use a numbered browser label followed by the active Conversation title, when
|
||||
available. The number avoids existing titles; restoring the layout preserves each saved title.
|
||||
The address bar shows only that instance's page URL. Binding a numbered instance does not overwrite
|
||||
its title; the default unbound browser receives the Conversation label when first bound.
|
||||
- 关闭工作栏 Browser Tab 只释放对应 Browser Tab。关闭浏览器上下文是独立危险操作,需要
|
||||
明确说明会影响该 Conversation 下的全部 Tab 和 Agent 浏览器状态。
|
||||
|
||||
## 8. 持久化与恢复
|
||||
|
||||
`createTab` reuses a live tab only for the same Conversation and workbar instance ID. A new workbar
|
||||
instance can adopt the primary tab only when its request creates the Conversation context. An existing
|
||||
primary, including one opened by an Agent, is not adopted by a new instance. New sibling tabs start at
|
||||
`about:blank` with no navigation history and share the Conversation's cookie partition.
|
||||
|
||||
持久化工作栏实例 ID、顺序、自定义标题和公开 Conversation 绑定,不持久化页面进程、CDP
|
||||
引用、snapshot、viewport lease、Cookie、Token 或 Main `BrowserTabId`。应用重启后实例显示
|
||||
未启动状态,首次导航或 Agent 使用时按原绑定创建新 Tab。
|
||||
|
||||
绑定的 Conversation 已删除或不可见时保留实例并显示目标失效,不自动绑定当前 Conversation。
|
||||
|
||||
## 9. 限制与验收
|
||||
|
||||
- 分别限制每个 Conversation 的 Tab 数和每个窗口的总活动 Tab 数;达到上限时在创建前反馈。
|
||||
- 两个 Tab 的地址、历史、加载状态、截图和元素引用互不覆盖。
|
||||
- 同一 Conversation 的 Tab 共享登录状态,不同 Conversation 保持隔离。
|
||||
- 延迟 viewport cleanup 不能隐藏另一个已激活 Tab。
|
||||
- 一个 Tab 的元素 `ref` 不能在另一个 Tab 中执行。
|
||||
- 用户切换工作栏 Tab 不改变进行中模型请求的 MCP 浏览器目标。
|
||||
- 关闭一个 Tab 不释放 Conversation Runtime,也不关闭同一 Conversation 的其他 Tab。
|
||||
- 旧版无 `tabId` 的模型工具调用仍操作请求绑定的默认 Tab。
|
||||
|
||||
## 10. 实施顺序
|
||||
|
||||
1. 引入 Browser Context 与 Browser Tab 两层身份,拆分 partition 和页面生命周期。
|
||||
2. 扩展 Shared、Preload、IPC 与 BrowserService 的 `tabId` 和 viewport `leaseToken`。
|
||||
3. 改造 Renderer 状态与工作栏实例,开放浏览器 `multiple` 策略和单 Tab 关闭。
|
||||
4. 将直连模型与 MCP capability 绑定到请求开始时确定的 Browser Tab。
|
||||
5. 完成跨 Tab 引用隔离、并行队列、共享 Cookie、关闭和恢复测试。
|
||||
6. 有明确 Agent 多页面需求后再开放模型可见的 Tab 管理工具。
|
||||
@@ -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,81 @@ 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 工作区底部提供默认收起的提交历史区,独立滚动,可调整高度。按当前分支
|
||||
分页读取提交,展示摘要、作者、时间、短 OID 和 refs。选中提交显示该次修改文件,
|
||||
再选文件进入内联提交 Diff;逐层返回保留分支、分页、选择、滚动位置与焦点。
|
||||
提交 Diff 保留重命名前后路径和实际内容修改;后台工作区刷新不得使正在加载的历史或详情
|
||||
永久停留在忙碌状态。
|
||||
历史展开入口使用带方向箭头的紧凑列表行;提交行沿用文件列表的表面、悬停和焦点样式,
|
||||
摘要与元信息左对齐分行展示,长内容省略并提供完整提示,不撑宽侧栏。
|
||||
- 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 +469,11 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
|
||||
页面加载期间原位切换为“停止加载”;“关闭浏览器”是清楚独立于导航的会话生命周期操作,
|
||||
必须与“停止加载”在文案和视觉上明确区分。现有“停止浏览器”语义统一改名为“关闭浏览器”。
|
||||
|
||||
浏览器面板和 Agent 操作同一条按 Conversation 归属的浏览器会话,不创建用户预览专用的
|
||||
第二个页面。用户执行前往、返回或刷新会有意改变 Agent 下一步读取和操作的页面。所有
|
||||
前往、返回和刷新请求都进入 `BrowserService`,并与 Agent 发起的浏览器操作串行执行;不在
|
||||
Renderer 建立第二套导航队列、抢占规则或恢复状态机。
|
||||
同一 Conversation 下的 Browser Tab 共享隔离存储和代理上下文,但每个 Tab 拥有独立页面、
|
||||
导航状态、操作队列和元素引用空间。模型请求开始时绑定一个确定的 Browser Tab;用户切换
|
||||
工作栏实例不能在请求途中改变 Agent 的工具目标。用户在该实例执行前往、返回或刷新会有意
|
||||
改变绑定到同一 Tab 的 Agent 后续操作。所有请求都进入 `BrowserService`;Renderer 不建立
|
||||
第二套导航队列、抢占规则或恢复状态机。
|
||||
|
||||
浏览器工具栏读取的权威状态至少包含:
|
||||
|
||||
@@ -414,11 +501,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 +545,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 +582,9 @@ type WorkbarCapabilityId =
|
||||
| 'processes'
|
||||
| 'workspace'
|
||||
| 'browser'
|
||||
| 'resources'
|
||||
| 'results'
|
||||
| 'document'
|
||||
|
||||
type WorkbarTargetRef =
|
||||
| { type: 'conversation'; id: string }
|
||||
@@ -483,6 +597,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 +866,7 @@ remote_operations
|
||||
其中:
|
||||
|
||||
- 工作栏偏好只保存能力目录顺序、面板实例、停靠布局、尺寸和目标引用。
|
||||
- 资源监控只保存面板实例,不保存采样结果或历史曲线。
|
||||
- 主机表只保存非敏感元数据和加密凭据引用。
|
||||
- 应用重启后先通过远程 Daemon 的 journal、lease、boot identity 和 event cursor 核对活动
|
||||
终端、进程与 Runtime;无法确认时标记 `interrupted` 或 `outcome_unknown`。
|
||||
@@ -770,7 +886,7 @@ Renderer 只通过显式方法访问:
|
||||
- 终端创建、输入、调整大小、关闭和有界输出订阅。
|
||||
- 受管进程列表、日志和允许的控制动作。
|
||||
- Runtime Inspector 快照、事件和允许的控制动作。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务。
|
||||
- 工作区、成果、浏览器和监督的既有或扩展服务,以及有界的本机资源快照。
|
||||
|
||||
每个 Main Handler 都必须验证可信发送者、Zod 输入、对象归属和当前状态。不得暴露 raw
|
||||
Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
@@ -779,7 +895,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 +966,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
### 阶段 1:成果与 Runtime 可观测性
|
||||
|
||||
- 本机 CPU、内存、磁盘 IO 和 GoodBuddy 桌面进程摘要的按需资源监控。
|
||||
- 通用 Artifact Event。
|
||||
- HTML 工作区和成果的静态即时预览。
|
||||
- Runtime Inspector Snapshot 与事件。
|
||||
@@ -881,8 +998,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 +1028,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
|
||||
### 17.1 稳定能力目录与用户控制
|
||||
|
||||
- [ ] 八个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] 十个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
|
||||
- [ ] Task Center 继续作为 Task 的单例索引,不删除入口、不复制会话,也不混入 Job、Run 或心跳事项。
|
||||
- [ ] 项目、会话、Runtime 或主机变化不会无提示地增删能力目录项。
|
||||
- [ ] 用户可以按需打开、关闭、排序和停靠面板实例。
|
||||
@@ -925,6 +1043,8 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [ ] 监督可以作用于普通 Conversation、Task 和后续实验对象,不假设编程语境。
|
||||
- [ ] 工作区不是 Git 仓库时仍可浏览文件。
|
||||
- [ ] Runtime 不支持某项原生能力时仍可从目录打开面板并获得准确说明。
|
||||
- [ ] 文档能力可以打开多个以文件名命名的固定实例;未配置健康 ShareServer 时显示原因,
|
||||
不把系统默认应用或静态预览冒充内置编辑。
|
||||
|
||||
### 17.3 安全与控制
|
||||
|
||||
@@ -968,6 +1088,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
|
||||
- [ ] 创建中、Agent 操作中和完整交互期间禁用浏览器其余工具栏控件;活动加载仍可单独停止,
|
||||
且停止加载不会关闭会话或释放 Runtime 上下文。
|
||||
- [ ] 活动 Conversation 没有浏览器会话时,可从同一地址栏按 Agent 相同 URL 策略启动会话。
|
||||
- [ ] 文档编辑器在右侧宽度不足时不发生工具栏或正文重叠,并可使用同一会话展开到可编辑布局。
|
||||
|
||||
## 18. 相关文档的职责
|
||||
|
||||
@@ -976,7 +1097,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 +1105,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,305 @@
|
||||
# 工作栏实现与验证进度
|
||||
|
||||
## 2026-09-10 Git 历史测试夹具修复
|
||||
|
||||
- 桌面候选 `877cf58` 的 CI 在四项工作区测试准备 Git 提交时因缺少作者身份失败。
|
||||
`c037d7f` 移除测试身份后引入了机器配置依赖;本机已有身份使该问题未在本地暴露。
|
||||
- 测试现在把已暂存的 tree 写入固定 Git commit 对象,并仅更新临时仓库的分支引用,
|
||||
不调用 `git commit`,不修改或覆盖任何 Git 身份配置。历史、重命名差异、分页和脏文件
|
||||
分支冲突仍由真实 Git 与生产 `manageWorkspace` 验证,不用 mock 或跳过用例替代。
|
||||
- 新增夹具自检确认 `git fsck --strict` 通过、父提交链有效、未暂存内容保持不变且仓库配置未改。
|
||||
聚焦测试 11 项通过、2 项因当前文件系统大小写不敏感而跳过;无身份 Runner 的验证待新候选 CI。
|
||||
- 完整 `npm test` 为 3845 项通过、66 项跳过、1 项未修改的活动列表测试超时;
|
||||
随后 `ActivityPanel.test.tsx` 的 16 项独立复跑全部通过,未放宽时限或删除断言。
|
||||
`npm run release:notes:verify`、`npm run typecheck`、`npm run lint` 通过。
|
||||
未将这次本地整库结果记为全绿,发布仍以新候选的 CI 校验和生产构建通过为前提。
|
||||
- 本轮只改测试和验证记录,产品功能、Agent 源码及已批准的双语发布说明不变,无外部模型调用。
|
||||
|
||||
## 2026-09-10 工作区布局与分支输入样式
|
||||
|
||||
- 按 [FR-W1、FR-W4](./prd.md#76-工作区) 将视图切换和刷新合并到顶部行,
|
||||
文件与 Git 工具按视图显示;移除根目录的孤立图标及空导航行,子目录保留文字路径导航。
|
||||
分支搜索复用共享 `field`,面板限宽、分支列表限高滚动,创建按钮使用紧凑次操作。
|
||||
- 新增布局、子目录返回、焦点恢复、分支搜索/切换/创建回归;工作区、设置界面、设置存储、
|
||||
IPC 和 Sidebar 五个测试文件共 354 项通过。另确认安全与数据的默认 `toolApproval` 为
|
||||
`always`,对应“Execute 自动授权已启用的工具”;已有明确保存的禁止策略仍保留。
|
||||
本轮没有修改授权策略或默认值。
|
||||
- 当前构建的 Windows Electron 使用独立 profile 和临时 Git 项目,实际 App → Preload →
|
||||
Main 路径通过分支搜索、切换、创建和子目录新建文件;磁盘与 Git 分支结果均确认。
|
||||
文件/Git 切换保留浏览目录,返回根目录后导航行移除、焦点回到刷新;Escape 收起分支面板并恢复焦点。
|
||||
真实安全与数据页面的默认选中项也确认为自动授权。
|
||||
- 浅色与深色下,将实际工作栏布局宽度设为 300、420、650px 后均无工作区横向溢出。
|
||||
输入框为共享 13px 正文、标签 11px,分支面板最大 360px;15 个分支时列表可在 240px 内滚动。
|
||||
这些是布局检查,不作为分栏拖拽或原生浏览器鼠标交互的验证。
|
||||
- `npm run build:bundle`、`npm run typecheck`、`npm run lint` 通过。
|
||||
整库 `npm test` 为 3843 项通过、1 项失败、66 项跳过;失败项是未修改的
|
||||
`opencode-runtime-lifecycle.test.ts` 事件流关闭时序断言,单独复跑通过。
|
||||
未再次运行整库,不将此结果记为整库全绿。
|
||||
- 本轮产品改动仅为共享 Renderer 工作区布局,未改 Main/Agent 的工作区命令、
|
||||
desktop-to-Agent 协议或 Runtime 执行逻辑;远程项目继续使用同一组件和既有回调,
|
||||
本轮未做新的 Host 实测。无外部模型调用,所属测试应用、浏览器会话和调试监听已关闭。
|
||||
|
||||
## 2026-09-10 提交审查修复
|
||||
|
||||
- [FR-W3、FR-W6](./prd.md#76-工作区):同一目录项可仅修改大小写,仍拒绝覆盖其他文件、
|
||||
目录和硬链接目录项;提交 Diff 同时使用重命名前后路径;分支后台刷新不再使历史请求
|
||||
遗留 busy;自动目录刷新包含当前浏览目录并去重。
|
||||
- 浏览器请求仅预留目标身份,首次导航才创建实际资源;Main 状态携带工作栏实例身份,
|
||||
Renderer 直接绑定同一页面,停止或回收后清除旧绑定。跨会话显式选择不会被应用类型
|
||||
同步覆盖,终端关闭确认期间释放原生浏览器 viewport。规则以
|
||||
[浏览器多实例技术设计](./browser-tabs-technical-design.md) 为准。
|
||||
- 删除普通浏览器操作的自动截图、固定等待、重试及 JPEG 状态缓存/IPC 字段,保留显式截图。
|
||||
- 整合复核补齐现有 32 页签容量处理与 Harness 代理的已发现工具复用。新增容量回归验证
|
||||
满额时布局仍有效、页面保留,关闭一项后显示原页面;Harness 回归验证发现接口后续失败时
|
||||
已知工具调用不再触发发现。相关三文件 66 项通过,最新 typecheck 与 lint 通过。
|
||||
- 工作区聚焦测试 43 项通过、2 项需大小写敏感文件系统而跳过;IPC 与 Sidebar 聚焦回归
|
||||
132 项通过。随后新增的 Ask 指令、继承 Runtime 用量持久化及计划模式 App 场景,3 项通过。
|
||||
- 扩展真实 Electron/MCP E2E 在独立临时 profile 通过:四个未使用的请求预留不创建会话、
|
||||
WebContents 或 UI 事件;首次 MCP 导航物化相同 Tab/工作栏身份,恢复不额外创建页面,
|
||||
并验证独立页面、共享 Cookie、请求租约关闭保护和清理。
|
||||
- 当前源码的共享 Linux x64 Host 验证已通过,覆盖工作区目标保护和重命名/Git Diff,
|
||||
并通过桌面托管 ACP → 隔离 Agent → OpenCode 完成 Ask/Execute 和继承 Runtime 指标持久化;
|
||||
该组共 3 次真实模型调用,清理后无所属进程残留。完整记录见
|
||||
[远程 Host 开发验证](../remote-host/technical-design.md#agent-开发期间的真实-host-验证)。
|
||||
- 最终整库 `npm test` 为 341 个文件通过、9 个文件跳过,3839 项通过、66 项按平台或手动
|
||||
条件跳过、0 失败;`npm run typecheck`、`npm run lint`、开发期 `npm run build` 和
|
||||
`git diff --check` 通过,19 份修改 Markdown 的相对链接目标检查通过。
|
||||
先前整合中的一轮为 3823 项通过、6 项失败、66 项跳过,另有被移动的测试路径加载失败,
|
||||
不作为最终结果;源码稳定后完整重跑得到上述全绿结果。
|
||||
- 原文档解析焦点回归改为等待 Modal 的焦点效果,保留焦点与背景隔离断言,没有改动产品逻辑。
|
||||
- Windows 独立 Electron profile 的完整 App → Preload → Main UI 验证通过;最终复测使用
|
||||
本轮最终开发构建。启用浏览器后普通 Execute 没有创建原生浏览器视图,工具导航只创建
|
||||
一张同时显示在工作栏的页面,跨会话选择保留原页面与 viewport。
|
||||
- 实际终端连通后,从浏览器关闭该终端可正常显示确认并操作按钮;原生视图可见性按
|
||||
可见 → 确认时隐藏 → 取消后可见变化,终端关闭清理完成。无效地址 `http://` 被拒绝后,
|
||||
原控件可重试有效地址并关闭页面;关闭端口返回了过滤代理错误页,因此该 UI 场景不作为
|
||||
Chromium `loadURL` 拒绝的实机证据。服务与 Renderer 释放绑定回归另有自动测试覆盖。
|
||||
- 桌面使用确定性 loopback 模型服务驱动真实生产路径,共 24 次本地 completion 请求,
|
||||
未发出外部模型请求;可选真实 Provider 探针没有找到已加密保存的 OpenAI 兼容文本连接,
|
||||
未据此宣称桌面真实模型验收通过。Ask 与编程 Subagent 的桌面结果见
|
||||
[直连模型验收记录](../direct-model-agent/progress.md#2026-09-10-提交审查后的桌面-ui-验收)。
|
||||
测试应用与具名浏览器会话已退出,所属 Electron/Node 进程及 fixture 监听端口均为 0;
|
||||
临时截图和隔离 profile 保留在本机测试目录,不进入仓库。
|
||||
|
||||
## 2026-09-10 固定基础页签与浏览器多实例
|
||||
|
||||
- 工作栏注册表已分别声明实例策略、默认上下文、默认打开、必须存在、可关闭和排序属性。
|
||||
任务中心与工作区保持默认存在且不可关闭;浏览器改为当前 Conversation 上下文的多实例。
|
||||
布局加载按 `required` 补回缺失的基础页签,并修复失效活动实例。
|
||||
- 任务中心增加“当前项目”“全局任务”“全部项目”范围,默认跟随活动项目;任务和审批按
|
||||
Conversation 所属项目过滤,范围模式随工作栏布局持久化。
|
||||
- 浏览器服务已拆分 Conversation Context 与 Browser Tab。每个 Tab 拥有独立页面、Driver、
|
||||
操作队列、状态和元素引用空间,同一 Conversation 的 Tab 共享 partition、代理和 Cookie。
|
||||
- Renderer 使用工作栏实例 UUID 原子创建或恢复 Browser Tab,并以 UUID viewport token 防止
|
||||
延迟 cleanup 隐藏新活动 Tab。关闭单个实例只释放对应 Tab。
|
||||
- 直连模型和请求级 MCP capability 在请求开始时优先绑定可见 Tab,其次绑定 primary Tab,
|
||||
并持有使用租约。请求结束或 capability 撤销前,绑定 Tab 拒绝关闭;工具参数未增加模型可见
|
||||
`tabId`,已有浏览器工具 schema 保持不变。
|
||||
- 聚焦回归命令覆盖 Shared、BrowserService、Electron Session、IPC、Preload、MCP、Renderer
|
||||
和 Workbar,13 个测试文件共 453 项通过;`npm run lint` 与 Web TypeScript 检查通过。
|
||||
- `node build/run-browser-tabs-electron-e2e.cjs` 通过。该测试启动真实 Electron/Chromium 和本地
|
||||
HTTP 页面,创建两个 `WebContentsView`,验证页面隔离、同 Conversation Cookie 共享、真实
|
||||
loopback MCP 的固定 Tab 导航与快照、使用租约关闭保护,以及单 Tab 和最终 Context 清理。
|
||||
- `npm run typecheck`、`npm run lint`、`npm run build:bundle` 和 `git diff --check` 通过。
|
||||
- 本轮整库 `npm test` 为 3686 项通过、61 项跳过、4 项失败。失败位于 portable/release 的
|
||||
ASAR 元数据 fixture、Runtime 版本探测和 DeepSeek Harness 的本地 MCP fixture;定向复跑仍
|
||||
失败。本次工作栏、浏览器、IPC 和 MCP 聚焦回归均通过,不将整库结果记为通过。
|
||||
|
||||
## 2026-09-09 移动目录选择与 Git 控件修正
|
||||
|
||||
- 本轮在已有文件管理改动上修正 [FR-W3、FR-W4、FR-W6](./prd.md#76-工作区):
|
||||
本机移动调用创建项目使用的 `settings.selectWorkspace()`;远程移动调用
|
||||
`sshHosts.browseDirectories()`,按项目 Host 浏览。所选目录映射为工作区相对目标,
|
||||
保留原名;取消不提交移动,外部目录在弹窗内提示限制。
|
||||
- 取消、分支、Fetch、历史展开和提交行接入共享控件样式;删除菜单项左对齐。
|
||||
远程目录列表限制滚动高度,保留加载、空目录、失败和截断提示。
|
||||
- `npm test -- src/renderer/src/WorkspaceFilesPanel.test.tsx src/renderer/src/RightAssistantSidebar.resize.test.tsx src/main/workspace/workspace-management.test.ts src/main/ssh/ssh-host-directory-browser.test.ts`
|
||||
通过 56 项,覆盖本机目录选择桥接、Windows 与 POSIX 目标映射、取消与越界选择、
|
||||
远程 Host 和目录参数、取消未完成的浏览,以及 Git 控件与真实文件管理服务。
|
||||
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git|opens the global assistant sidebar"`
|
||||
通过 12 项。工作栏切换测试改为等待项目目录加载后出现刷新按钮。
|
||||
- `npm run typecheck`、`npm run lint` 和 `git diff --check` 通过。
|
||||
- 本轮整库 `npm test` 完整运行 944 秒,3642 项通过、61 项跳过、6 项失败。
|
||||
工作栏加载时序断言已修正并定向复测通过;其余 5 项涉及 Agent 离线依赖测试超时、
|
||||
DeepSeek Harness MCP 加载、Runtime 版本探测,以及 portable/release 打包产物检查。
|
||||
未再次运行整库测试,不记为整库通过。
|
||||
- 隔离 Electron 窗口加载当前源码组件和 CSS,检查浅色、深色主题下 300px、420px 工作栏:
|
||||
无横向溢出,分支与 Fetch 同行,提交行左对齐;移动弹窗没有路径输入框,取消为次按钮,
|
||||
删除项计算样式为左对齐。截图保留于本机临时验证目录。此检查使用桥接替身,未自动操作
|
||||
系统原生目录对话框,也不替代完整应用中的人工验收。
|
||||
- 尝试从隔离 Electron 配置调用真实 SSH 目录浏览服务时,已有凭据在
|
||||
`safeStorage.decryptString` 解密失败,尚未建立 Host 连接。真实远程目录选择验收仍受阻;
|
||||
未修改用户配置或远端文件,本轮未调用真实模型。
|
||||
|
||||
## 2026-09-09 文件管理与 Git 工作区扩展
|
||||
|
||||
- 已只读核对根目录规范、UI 设计系统、目录浏览组件、WorkspaceAccess 与 Agent 协议。
|
||||
本轮范围定义于 [PRD 工作区 FR-W1 至 FR-W6](./prd.md#76-工作区)。
|
||||
- 当前生产路径具备目录、属性、文本预览和工作树 Diff;Agent 的写入及 Git operation
|
||||
仍是只读拒绝接口。文件管理、分支与提交历史尚未实现或验证,不能视为完成。
|
||||
- 待完成源码实现、整库 test/typecheck/lint、真实桌面路径与当前源码 Linux Host 验证。
|
||||
本轮开始前已有浏览器与工作区未提交改动,保留这些改动;不提交或推送。
|
||||
|
||||
## 2026-09-09 工作区页签交互
|
||||
|
||||
- 已核对当前工作树中的 `RightAssistantSidebar.tsx` 和 `WorkspaceFilesPanel.tsx` 差异:
|
||||
文件预览期间目录树保持挂载,接入返回位置与焦点恢复、文件与未提交更改分段视图、
|
||||
目录就地重试,以及文件预览和 Diff 操作。行为定义见 [PRD 工作区](./prd.md#76-工作区)。
|
||||
- `npm test -- src/renderer/src/RightAssistantSidebar.resize.test.tsx src/renderer/src/WorkspaceFilesPanel.test.tsx`
|
||||
通过 35 项,覆盖目录展开、返回位置与焦点、视图选择、刷新失败保留内容、Markdown
|
||||
源码切换,以及切换项目后丢弃未完成的文件预览。
|
||||
- `npm test -- src/renderer/src/App.test.tsx -t "workspace|stale Git"` 通过 11 项,
|
||||
覆盖应用中的文件预览、路径复制、默认应用打开、返回和任务结束刷新。
|
||||
- 本轮 `npm test` 完整运行结果为 3629 通过、61 跳过、5 失败。失败位于 Agent 离线
|
||||
依赖打包超时、DeepSeek Harness MCP 加载、Runtime 版本探测,以及两个打包产物检查,
|
||||
不将该结果记为整库回归通过。
|
||||
- `npm run lint` 和差异空白检查通过。最新 `npm run typecheck` 被同时进行的浏览器
|
||||
接口变更阻塞:测试替身缺少 `hide`、`setBounds`、`setViewport`,部分侧栏测试仍传递
|
||||
已移除的浏览器回调。该轮类型检查未通过,未改动另一项任务的接口或测试替身。
|
||||
- 尚未进行真实桌面视觉验收;组件测试的滚动断言不代替实际窗口中的布局和滚动检查。
|
||||
- 范围仅限工作区页签,不包含项目文件搜索或其他页签改动。
|
||||
|
||||
## 2026-09-09 托管 Runtime 并行阻塞
|
||||
|
||||
- Windows 完整隔离测试程序通过界面发送 OpenCode 原生双子代理、直连与 Continue 文件
|
||||
任务;随后三路上游请求在 917 ms 内发出并重叠,全部完成。普通并发没有复现全局排队。
|
||||
- 在同一程序中,让真实模型执行一次 4,000 行、约 98 KB 文件的 CRLF → LF 转换,
|
||||
同时运行另一个会话。原生进程的无凭据存活探针两次延迟 3,301 ms、3,085 ms;
|
||||
原生热线程持续使用 CPU,而 Main 没有同步满载。操作不是模拟事件或直接调用
|
||||
Runtime 的脚本,模型请求来自实际输入框和发送按钮。
|
||||
- 托管 OpenCode 关闭未消费的自动 Git 快照后,完整程序重复相同操作和并行会话,
|
||||
90 秒观察中的最大原生探针延迟为 210 ms,没有超过 500 ms 的探针,两条请求均完成。
|
||||
该对照验证已复现的快照阻塞得到消除,不声称已经证明历史 90–125 秒事件的唯一原因。
|
||||
- Continue 的实际 Electron 原生宿主在不排空 stdout 时被注入的 128 KiB 输出阻塞;
|
||||
使用当前生产适配器后,输出在 5 ms 内完成,20 次状态读取全部成功,最大 5 ms。
|
||||
这证明管道修复有效,不代表普通启动约 640 字节的输出就是历史卡顿原因。
|
||||
- 修复版完整程序再次完成原生双子代理与 Continue 文件任务,实际请求重叠,三个页面
|
||||
都通过计数按钮功能检查。Windows 诊断与程序验证合计 40 次成功模型调用;
|
||||
另一次 UI 请求返回账户额度不足的 402,用户充值后才继续,没有把失败当成通过。
|
||||
- 当前源码 Linux x64 Agent 经桌面生产管理链路完成 Ask 读取和 Execute 换行转换,
|
||||
共 4 次成功模型调用;最终原生配置确认 `snapshot: false` 且权限仍为 `ask`/`allow`。
|
||||
此验证覆盖实际 Agent、模型桥、工具完成和关闭,不量化 Linux 提速,也未注入取消。
|
||||
- 定向回归先验证 7 项失败,再验证 124 项通过、5 项 Windows 平台跳过;
|
||||
全项目类型检查和修改文件 lint 已通过。首次全量回归为 3625 通过、1 失败、61 跳过;
|
||||
失败来自生命周期测试的包装流在底层 reader 关闭后仍计数为打开,已通过独立流实验
|
||||
复现并改为观察 `reader.closed`,没有因此修改生产取消逻辑。最终候选全量回归为
|
||||
3626 通过、0 失败、61 跳过,`npm run typecheck`、`npm run lint`、
|
||||
`npm run release:notes:verify`、差异空白与文档相对链接检查通过。本机和远端的
|
||||
测试进程已回收,安装版和正式 Host 环境未改动;正式发布仍需候选 CI 与资产核实。
|
||||
运行策略见
|
||||
[托管 Runtime 并行与输出](./runtime-interactions.md#托管-runtime-的并行与输出)。
|
||||
|
||||
## 2026-09-08 消息底部请求状态
|
||||
|
||||
- 已接入本机 OpenCode 父会话的原生重试等待及开始事件;直连模型把退避等待与实际发起
|
||||
重试分开显示,Continue 初始文案改为中性的请求处理。显示规则和未接入边界见
|
||||
[消息底部请求状态](./runtime-interactions.md#消息底部请求状态)。
|
||||
- 消息底部按真实事件显示准备、工具、待回答及终态;有新进展时清除旧状态,结束后不
|
||||
接受迟到状态覆盖。仅该处状态点改为静态,会话列表闪点、发送和停止按钮未改动。
|
||||
- Windows 隔离开发桌面通过实际 UI 发送验证 OpenCode 的 503/Retry-After 等待、
|
||||
原生重试开始、真实模型回复与完成;直连模型通过派发前连接拒绝、500 ms 等待、
|
||||
重试、真实回复与完成。故障由本机测试代理注入,不冒充服务商实际故障。
|
||||
三个隔离测试配置总计 5 次真实文本模型调用;另有一次直连重试被测试代理的调用
|
||||
上限明确拒绝,界面正确显示失败,最终单次补测完成。未执行用户命令或修改安装版。
|
||||
- DOM 状态记录确认底部无闪烁、完成后无旧重试文案,浅色截图可见原生重试计划与原因;
|
||||
底部 WCAG 2 A/AA 定向检查无违规。最小化窗口的补充截图超时,未作为通过证据。
|
||||
隔离窗口及测试代理已结束。
|
||||
- 最终 `npm test -- --reporter=json --outputFile=<临时报告路径>` 为 3618 项通过、
|
||||
0 项失败、61 项按平台或手动条件跳过;首轮的压缩状态旧断言已修正,途中被用户中断
|
||||
的运行不计为通过。`npm run typecheck`、`npm run lint`、
|
||||
`npm run release:notes:verify`、差异空白与相关文档相对链接检查通过。
|
||||
|
||||
## 2026-09-08 Desktop 0.12.8 安装包修复
|
||||
|
||||
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
|
||||
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
|
||||
后,插件及传递依赖逐文件复制验证通过。
|
||||
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
|
||||
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
|
||||
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
|
||||
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
|
||||
不能把当前源码回归通过当作真实发布包已通过。
|
||||
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
|
||||
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
|
||||
|
||||
## 2026-09-08 Execute 目录权限等待修复
|
||||
|
||||
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
|
||||
禁用工具,当前请求精确分配的只读能力例外。Execute 不把 Workspace 当作目录边界。
|
||||
- `permission.asked` 不再忽略所有非父会话事件。Main 用 OpenCode Session 的 `parentID`
|
||||
确认属于当前请求的子孙会话后按当前模式回复;其他并行会话不受影响,同一请求只回复
|
||||
一次。子会话权限不伪装为父会话中的待完成工具。
|
||||
- 真实 OpenCode `1.18.9` 和候选 `1.18.29` 均已通过本地确定性模型驱动的原生 Task
|
||||
工作区外读取回归;同一 Runtime 的 Ask 仍不暴露文件与命令工具。
|
||||
- Continue `1.5.47` 的真实 CLI 已通过 Execute Shell 写入工作区外测试文件;DSH 固定
|
||||
Host 已通过 Execute 工作区外写入、随后 Ask 拒绝同一写工具,未增加权限策略或人工审批。
|
||||
- 真实 OpenCode `1.18.29` 已完成原生 Task 后的 yes/no 与自由文本问答回传;父、子会话
|
||||
提问及后台任务不能交互时的失败路径均有回归测试。Continue 真实 `AskQuestion` 暴露了
|
||||
QuizService 扁平数据结构与原适配器不一致的问题,现已按锁定版本修正;真实自由文本
|
||||
回传、yes/no、选项及跳过均验证通过,自定义答案不再误标为选项。
|
||||
- DSH 活跃 Execute 的 ACP 权限回调不再等待第二次 authorizer,Ask 继续拒绝;未添加
|
||||
通用业务问答工具或从终端日志猜测确认意图。完整范围见
|
||||
[Runtime 交互边界](./runtime-interactions.md)。
|
||||
- 当前源码 Linux x64 Agent 通过签名组包、生产安装、真实 Ask/Execute、原生子代理
|
||||
工作区外写入、重连同一 daemon 和 stop/bootstrap。实机回归同时定位并修复了握手
|
||||
Base64URL 响应误用 ID 校验及断流未结算等待;13 项 Unix 原生端点测试和连续 20 次
|
||||
握手通过。两轮真实模型调用合计 10 次,均 completed;本轮测试 Agent 已停止。
|
||||
- 最终完整回归使用 `npm test -- --reporter=json --outputFile=<临时报告路径>`,并通过
|
||||
`GOODBUDDY_TEST_OPENCODE_BINARY` 指向已验证版本的候选 OpenCode。结果为 338 个文件、
|
||||
3597 项通过、0 项失败、61 项按平台/手动条件跳过,报告 `success=true`、退出码 0;
|
||||
Windows 跳过的 Unix 端点场景另在 Linux Host 运行了上述 13 项原生测试。
|
||||
- 最终 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 和差异空白检查
|
||||
通过,相关 Markdown 相对链接有效。此前失败、暂停及控制台截断的回归记录由本次完整
|
||||
JSON 报告覆盖,不代表仍有测试失败。未执行系统休眠/唤醒或本地桌面生产构建、打包;
|
||||
后续发布仍需对应候选的 CI 构建和跨平台验证。
|
||||
|
||||
## 2026-09-08 OpenCode 复用与原生控件恢复
|
||||
|
||||
- `OpenCodeRuntime` 仅在没有活动请求时探测缓存的 embedded server;活动对话或
|
||||
Compact 期间直接复用服务,避免新会话的一次短探测失败关闭其他会话正在使用的进程。
|
||||
空闲探测失败时仍清除旧 client 和会话映射并重建;并发调用共享检查与重建。
|
||||
进程退出或服务关闭会中止绑定它的对话与 Compact,事件流重连等待也可立即取消,
|
||||
请求明确报错,不自动重放已执行命令。本次修复后的真实模型验证共 6 次:正常并发、
|
||||
已复现场景下的共享服务保护均正常完成;主动结束测试进程后旧请求明确失败,新请求
|
||||
在替换服务上完成。此前“新会话完成、旧会话持续等待”的故障不再出现。
|
||||
- 请求级 MCP 注册与断开通过实例内 `mutateMcp` 队列串行执行。仅实际发出 `mcp.add`
|
||||
的名称进入清理列表;注册尚在排队时取消不追加断开。断开的 1 秒超时从出队后开始。
|
||||
- `App` 按本机/远程、连接方式、项目和 Runtime 选择组成 scope 缓存原生清单,用请求
|
||||
代次阻止旧 scope 结果回写;同 scope 会话刷新不清空重取,切换会话重置消息级选择,
|
||||
Runtime 切换状态也按 generation 隔离。清单请求失败或返回 `unavailable`/`partial`
|
||||
时保留已有可用缓存并有界重试;没有可用缓存时保留新返回的部分数据,因此
|
||||
`unavailable` 重试为可用 `partial` 后可显示 Agent 和 Command。
|
||||
|
||||
验证记录:首轮定向测试 273 项通过、5 项跳过;补充修复后的 Runtime 与生命周期测试
|
||||
77 项通过,界面定向测试 6 项通过,类型检查通过。修复前的 lint 检查通过。
|
||||
全量测试超过 120 秒被终止,期间出现一项 DS 本地 stdio MCP 测试失败,尚待定位。
|
||||
这是当次历史结果。后续发布前修复与实机验证见本文上方及
|
||||
[远程主机技术设计](../remote-host/technical-design.md);真实系统休眠恢复仍未完成验证。
|
||||
|
||||
## 2026-09-06 项目工作区
|
||||
|
||||
范围见 [PRD 工作区](./prd.md#76-工作区)。
|
||||
|
||||
当前源码已移除重复标题与底部完整 Diff 入口,使用彩色 Git 状态字母,点击更改项读取
|
||||
单文件 Diff。已暂存与未暂存差异分开展示;本机 Git 测试覆盖无首次提交、未跟踪、
|
||||
暂存后继续修改、重命名、删除及包含方括号的路径。Git 刷新驱动目录树重新读取,保留
|
||||
展开状态;SSH 项目不提供本机系统打开按钮。审查后补齐了当前 Diff 随刷新重读、
|
||||
旧请求结果失效,以及超过 50 项变更的继续加载入口。
|
||||
|
||||
验证记录:
|
||||
|
||||
- 最终 `npm run typecheck`、`npm run lint`、`npm run build` 均通过。
|
||||
- 工作区专项 7 个测试文件复跑:317 项通过,3 项 Linux 专用测试在 Windows 跳过。
|
||||
任务完成测试同时检查目录树出现新文件。
|
||||
- 最终 `npm test`:326 个测试文件、3552 项测试通过,9 个文件、58 项测试按既有条件
|
||||
跳过。复核首轮曾有一项浏览器地址草稿测试超时,单独复跑及最终全量复跑均通过;
|
||||
更早的超时/失败记录不再作为最终验收结果。
|
||||
- 后续复核已通过既有凭据和固定 Host Key 经 VPN 连接共享 Linux x64 Host,使用当前
|
||||
源码构建的隔离 Agent 包安装并启动。真实 `RemoteWorkspaceAccess → Agent git/status`
|
||||
与 `git/diff` 路径覆盖暂存后继续修改、删除、重命名、未跟踪和方括号路径;均通过。
|
||||
Git 验证未调用模型。
|
||||
- Windows 隔离桌面实例已通过真实点击验收:当前项目单文件暂存/工作树 Diff 分组与
|
||||
增删颜色正确,修改测试文件后点击刷新可更新正在查看的 Diff,53 项变更经加载更多
|
||||
全部可访问。截图留在本地验证目录,不纳入产品包。
|
||||
|
||||
目录分页、任意目录切换和 HTML 预览不在本轮改动范围内。
|
||||
@@ -0,0 +1,56 @@
|
||||
# Runtime 交互边界
|
||||
|
||||
本文定义当前原生交互的适配范围,不增加工作模式或第二层 Execute 审批。
|
||||
|
||||
| Runtime | 权限确认 | 业务问答 |
|
||||
| --- | --- | --- |
|
||||
| 本机 GoodBuddy 管理的 OpenCode | 内部默认配置允许权限;每个 Ask 请求仍使用 deny-all 与显式只读能力。Execute 自动回复属于当前请求的父、子孙会话权限事件,不处理其他并行请求 | 通过 `question.asked` 转交父、子孙会话问题,支持原生单选、多选、yes/no、自由文本及跳过;使用公开 ID 回传 `question.reply/reject` |
|
||||
| Continue | Execute 启动传入 `--auto`,剩余原生权限请求按当前模式自动答复 | 对接 `1.5.47` QuizService 的扁平 `pendingQuestion`,其中 `question` 是字符串。选项回答与自定义文本分别回传正确的 `isCustomAnswer`;跳过发送拒绝回答的文本 |
|
||||
| DeepSeek Harness | 固定 Host 的 Execute 文件与命令工具不询问目录权限;若收到 ACP 权限请求,活跃 Execute 直接选一次性允许,缺少该选项时使用 Runtime 提供的允许选项,Ask 拒绝,不等待额外 authorizer | 固定 Host 当前未提供通用原生业务问答服务,不把普通模型正文误识别成待答协议。模型以普通回复提问时,用户通过下一条消息回答 |
|
||||
|
||||
托管 SSH OpenCode 的 ACP 权限处理与直接启动配置由
|
||||
[远程主机技术设计](../remote-host/technical-design.md)定义;本机 SDK 问答适配不代表远程
|
||||
ACP 已提供相同业务问答接口。
|
||||
|
||||
## 运行与失败
|
||||
|
||||
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
|
||||
- 前台问题停留在现有问答卡片,保留回答、提交失败提示与跳过入口。
|
||||
- 后台任务收到结构化提问时,通过现有 Main 路径明确失败并提示改为前台对话,不永久等待。
|
||||
- 请求结束或取消后清除对应待答映射;过期回答明确报错,不转交给后来的请求。
|
||||
- 任意终端程序的密码、OAuth 登录、许可证确认或 stdin 提示不自动回复 `yes`。这类程序应
|
||||
使用其非交互参数,或由用户在终端完成;不能把权限确认和业务决策混为一谈。Shell
|
||||
工具保留其现有取消与超时,不新增日志解析或通用交互代理。
|
||||
|
||||
## 托管 Runtime 的并行与输出
|
||||
|
||||
- 本机和 Agent 管理的 OpenCode 启动配置都显式设置 `snapshot: false`。GoodBuddy 不消费
|
||||
原生自动 Git 快照、快照差异摘要或快照撤销;关闭这条内部路径,避免大行级差异的同步
|
||||
计算阻塞同一原生进程中的其他会话。
|
||||
- 文件工具、Shell、原生子代理、工作区 Git 状态和单文件差异保持可用;不新增全局会话
|
||||
队列、请求重放或总执行截止时间。外部 OpenCode Server 的配置不由 GoodBuddy 改写,
|
||||
已运行的旧原生进程也不会热更新配置。
|
||||
- Continue 使用 HTTP 传递响应和状态;Electron UtilityProcess 启动后持续排空不用展示的
|
||||
stdout,避免控制台输出填满管道后阻塞宿主。stderr 的有界错误处理、请求取消与进程
|
||||
回收保持不变,不把诊断输出复制进聊天或持久日志。
|
||||
- 远端最终启动配置由[远程主机技术设计](../remote-host/technical-design.md)定义,
|
||||
模型桥生成最终配置时同样保留上述快照策略。
|
||||
|
||||
## 消息底部请求状态
|
||||
|
||||
- 状态属于当前请求,不与顶部 Runtime 的连接中、就绪、不可用混为一谈。提交后先显示
|
||||
“正在准备请求”;没有更具体的活动事件时显示“等待后续进展”,不声称模型正在思考。
|
||||
- 本机 OpenCode 只把当前父会话的 `session.status.retry` 映射为等待重试,显示原生
|
||||
`attempt`、计划重试时间和有界脱敏原因;后续 `busy` 才显示已开始重试。其他会话的
|
||||
状态不串入当前消息,不猜测总重试次数,不把 SDK 事件流重连当作模型重试。
|
||||
- 显示优先级为待回答、待确认、进行中的上下文压缩、明确的请求状态、待执行或执行中的
|
||||
工具、未完成的子任务、等待后续进展。压缩沿用已有活动块,不重复增加底部提示。
|
||||
正文、推理或工具进展会清除旧状态;子任务更新不覆盖父请求的重试提示。
|
||||
- 完成、失败、取消后保留真实终态,迟到的状态事件不能把请求恢复成处理中。底部状态点
|
||||
为静态,不凭请求尚未结束暗示仍有进展;会话列表闪点和发送、停止按钮保持原有行为。
|
||||
- Continue、DeepSeek Harness 和远端 ACP 保留其已接入的处理与工具事件,不伪造未接入
|
||||
的原生重试状态。直连模型的重试等待与请求开始文案见
|
||||
[直连模型 UI 设计](../direct-model-agent/ui-design.md#6-错误与通知);本次不改变任何
|
||||
Runtime 的重试上限、请求重放或取消策略。
|
||||
|
||||
实现与真实 Runtime 验证证据见[进度记录](./progress.md)。
|
||||
@@ -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. 背景
|
||||
|
||||
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。其长期“未来分区记忆”
|
||||
方向尚未设计,也不承担持续学习、模式挖掘或自动改进:
|
||||
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。它与分区记忆的协同尚未
|
||||
实施,也不承担持续学习、模式挖掘或自动改进:
|
||||
|
||||
- 候选是否改善未来行为没有评估。
|
||||
- 一条反思是否会被检索和使用并不确定。
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| 文档状态 | 实现与发布验收基线 |
|
||||
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
|
||||
| Runtime 标识 | `deepseek-harness` |
|
||||
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.8` |
|
||||
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.2-rc.1` |
|
||||
| 上游状态 | Developer Preview,允许出现破坏性变更 |
|
||||
| 上游许可证 | MIT |
|
||||
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
|
||||
@@ -136,9 +136,14 @@ Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不
|
||||
|
||||
- GoodBuddy 已经持久化对话、消息、活动、工具事件和用量。
|
||||
- 再写一份 Harness 日志会扩大敏感数据副本和清理范围。
|
||||
- GoodBuddy 在 Runtime 重启后可以用现有的有界历史创建新 Harness Session。
|
||||
- GoodBuddy 在 Runtime 重启后可以用现有会话历史创建新 Harness Session。
|
||||
|
||||
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
|
||||
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
|
||||
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
|
||||
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
|
||||
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
|
||||
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
|
||||
|
||||
## 6. 总体架构
|
||||
|
||||
@@ -290,6 +295,10 @@ GoodBuddy 的工作模式属于每个请求,不属于 Runtime 进程全局状
|
||||
5. 缺少准备状态、重复使用、请求标识不匹配时,Control Plane 直接拒绝请求。
|
||||
6. 同一 Session 只允许一个 Prompt 在途。
|
||||
|
||||
若 Runtime 回传 ACP 权限请求,Main 只按活跃 Session 的当前模式答复:Execute 优先选择
|
||||
`allow_once`,缺少时使用提供的 `allow_always`,不调用第二次人工 authorizer;
|
||||
Ask、已关闭或未知 Session 拒绝。这里不写入永久授权规则,也不把业务问答当作权限确认。
|
||||
|
||||
### 8.5 事件模型
|
||||
|
||||
Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
|
||||
@@ -323,7 +332,7 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
|
||||
- 首次请求创建 Session。
|
||||
- 已有 Session 只发送当前 Prompt。
|
||||
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的有界历史。
|
||||
- 进程重启或映射失效时,创建新 Session,并只在这一次加入 GoodBuddy 提供的会话历史。
|
||||
- 历史以明确的“不可信会话数据”结构传入,不能拼接成系统指令。
|
||||
- 用户分配的 Skill 只通过 Main 校验的包路径进入 Host,并在 Agent scope 注册;不得把 Skill 内容伪装成用户 Prompt。
|
||||
|
||||
@@ -372,6 +381,9 @@ GoodBuddy conversationId -> Harness sessionId + process generation
|
||||
- 工作区不是 containment 边界;绝对路径和命令可访问当前用户本来有权访问的主机资源。
|
||||
- 已启用插件注册的工具与内置工具使用同一分发路径;GoodBuddy 不增加插件权限矩阵或逐工具确认。
|
||||
- Main 代理的 MCP 工具继续执行分配、schema、活动请求、模式和 RuntimeAuthorizer 校验。
|
||||
`goodbuddy/tools/list` 按 Session 保存本次发现的工具定义;`goodbuddy/tools/call` 复用该定义
|
||||
校验参数与授权,不因调用健康工具而重新等待故障 MCP 的发现。发现重试留在清单刷新,
|
||||
Session 释放及 Runtime dispose 清除内存定义,不持久化第二份能力状态。
|
||||
- 所有工具调用仍作为活动事件记录;Ask 和 delegation 路径继续固定拒绝。
|
||||
|
||||
### 10.4 Runtime OS 沙箱
|
||||
@@ -437,8 +449,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 +489,8 @@ DeepSeek Harness 首版只使用符合下列边界的 GoodBuddy 模型连接:
|
||||
| 单图尺寸 / 单条消息解码像素 | 单边最长 8,192 px 且 1,600 万像素 / 3,200 万像素 |
|
||||
| Host 临时图片存储 | 32 MiB 且最多 256 个唯一对象 |
|
||||
| 单个文本或推理事件 | 64 KiB |
|
||||
| 单次请求累计协议输出 | 4 MiB |
|
||||
| 单次请求累计协议输出 | 无固定总量上限,继续校验单帧和待处理队列 |
|
||||
| 单轮模型输出 Token | 由 Harness SDK 与 Provider 决定,GoodBuddy 不固定覆盖为 16K |
|
||||
| 工具输入摘要 | 4,000 字符 |
|
||||
| 工具输出摘要 | 4,000 字符 |
|
||||
| 待处理事件数 | 1,000 |
|
||||
@@ -503,7 +525,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,29 @@ 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 或表达式。
|
||||
|
||||
### ripgrep 打包
|
||||
|
||||
固定依赖 `@vscode/ripgrep`,构建钩子根据 Electron 目标选择对应 Windows、macOS 或 Linux
|
||||
x64/arm64 平台包,使用 lockfile integrity 校验后提取 `rg`。安装包只复制当前目标二进制和
|
||||
MIT 许可证。运行时通过 `BundledRuntimePaths.ripgrep` 使用绝对路径,并将所在目录放到直连
|
||||
模型进程 PATH 前部。
|
||||
|
||||
## 4. 进程工具契约
|
||||
|
||||
### 4.1 输入
|
||||
@@ -89,7 +115,8 @@ const processExecuteInputSchema = z.object({
|
||||
}).strict()
|
||||
```
|
||||
|
||||
- `cwd` 只接受相对工作区路径。
|
||||
- `cwd` 接受绝对路径或相对工作区路径,省略时使用工作区根目录;按本机路径语义解析
|
||||
`..` 和符号链接,不限制目标位于工作区内。工具 schema 向模型明确这一规则。
|
||||
- 首版不接受 `env`、`stdin`、`background`、`pty`、`shell` 或任意 executable 参数。
|
||||
- Shell 选择由执行后端决定,防止模型绕过平台契约启动另一套受管接口。
|
||||
- 命令字符串仍可以调用当前账号本来有权运行的程序;Execute 不增加命令白名单。
|
||||
@@ -111,20 +138,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 +280,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 +331,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 +365,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 +408,15 @@ type SubagentActor =
|
||||
结果总量都由请求超时、取消和上下文压缩控制,长回复和大工具结果不会被中断。图像生成
|
||||
响应仍按可解码图片大小校验。
|
||||
|
||||
会话历史在请求校验、Renderer 发送与保存、SQLite 读写和直连模型缓存中均保留完整消息,
|
||||
不按消息条数、单条文本长度或累计文本量裁剪。上下文大小由现有压缩策略和模型上下文窗口
|
||||
处理。Continue 接收完整历史,已有有效摘要只替换其覆盖的前缀,剩余消息全部交给原生
|
||||
Runtime;GoodBuddy 不再额外保留最后 20 条或限制为 128,000 字符。
|
||||
|
||||
## 11. 性能
|
||||
|
||||
- Shell 探测按执行服务实例缓存,不在每轮模型调用重复探测。
|
||||
- stdout/stderr 边读边界定,避免大日志进入内存后再截断。
|
||||
- stdout/stderr 边读边写入临时文件,内存仅保存有界预览;续读每次只读取一页。
|
||||
- 子级共享父请求已解析的模型配置和工具服务,不复制 MCP 配置或持久状态。
|
||||
- 不增加持久 Shell、后台 daemon、工作区快照或恢复状态机。
|
||||
- 命令执行不使用 PTY,避免普通构建和测试承担终端渲染成本。
|
||||
@@ -351,8 +428,10 @@ type SubagentActor =
|
||||
- 工具可见性矩阵和 Runtime target 过滤。
|
||||
- Ask 伪造进程调用拒绝。
|
||||
- Windows PowerShell、POSIX Bash/Sh 选择和参数。
|
||||
- cwd 默认、合法子目录、越界、符号链接越界和不存在。
|
||||
- cwd 默认、相对子目录、绝对路径、工作区外目录、指向外部的符号链接和不存在的目录。
|
||||
- exit 0、非零退出、spawn 失败、超时、取消和输出截断。
|
||||
- 输出预览加分页能还原全文,包括 JSON 转义文本、中文、表情和字面替换字符;覆盖一字节
|
||||
页面前进、跨会话拒绝、活动调用释放、短输出删除及服务退出清理。
|
||||
- 环境筛选、本机工具 PATH 优先且模型凭据不进入进程。
|
||||
- 子级模式、模型、项目、执行空间和能力继承。
|
||||
- 深度 1 过滤、并发 3、队列 20、取消传播和部分输出。
|
||||
@@ -384,6 +463,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 时,不在其原生能力清单中伪造这两项;
|
||||
@@ -54,7 +59,7 @@ PowerShell · packages/app · npm test
|
||||
```
|
||||
|
||||
- 命令只显示单行有界摘要,完整参数进入可展开详情。
|
||||
- 目录显示工作区相对路径;工作区根显示 `.`。
|
||||
- 工作区内目录显示相对路径,工作区根显示 `.`;工作区外目录显示规范化绝对路径。
|
||||
- 不在摘要中显示完整环境或凭据。
|
||||
|
||||
### 3.2 状态
|
||||
@@ -79,11 +84,14 @@ GoodBuddy 服务崩溃。
|
||||
1. Shell、工作目录、耗时和退出码。
|
||||
2. stdout。
|
||||
3. stderr。
|
||||
4. 截断提示。
|
||||
4. 预览省略提示与工具结果中的续读引用。
|
||||
|
||||
日志使用共享等宽字体和可选择文本。长输出在活动块内折叠,展开后仍保持工作栏和聊天布局
|
||||
可用;不把完整日志复制为应用通知。
|
||||
|
||||
模型使用 `output_read` 续读时显示普通工具活动“续读工具输出”。活动展开只展示该次返回的
|
||||
摘要,不等同于自动加载完整日志;当前不增加手动分页按钮。
|
||||
|
||||
## 4. Subagent 活动块
|
||||
|
||||
复用现有 `subagent` 消息块,并将系统执行者显示为“编程 Subagent”,不要求用户预先创建
|
||||
@@ -97,7 +105,7 @@ GoodBuddy 服务崩溃。
|
||||
|
||||
终态:
|
||||
|
||||
- 完成:显示有界结果摘要,可展开查看完整返回。
|
||||
- 完成:显示有界结果摘要,可展开查看当前预览;完整正文由模型通过输出续读工具取得。
|
||||
- 失败:保留部分输出和错误。
|
||||
- 取消:显示“父请求已取消”或准确取消原因。
|
||||
- 排队:显示当前处于并发队列,不显示虚假百分比。
|
||||
@@ -117,8 +125,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:正文、标题、表格、批注和图片关系;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# 知识库
|
||||
|
||||
知识库负责资料导入、解析、分块、索引、检索、图谱和带来源的知识问答,也负责将
|
||||
Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
|
||||
知识库已提供本地资料导入、解析、分块、索引、检索、图谱和带来源的知识问答。
|
||||
将 Dify、FastGPT 和 RAGFlow 中已有知识库接入同一检索范围已完成 Main 进程适配器和
|
||||
真实接口验证,但实例管理、绑定、统一检索和界面尚未接通,因此仍不属于当前已提供能力。
|
||||
|
||||
## 文档导航
|
||||
|
||||
@@ -9,6 +10,8 @@ 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) | 外部实例、知识库绑定、检索范围和产品验收 |
|
||||
@@ -16,6 +19,7 @@ Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
|
||||
| [外部知识库逻辑设计](./external-knowledge-logic-design.md) | 状态、不变量、配置优先级和失败规则 |
|
||||
| [外部知识库 UI 设计](./external-knowledge-ui-design.md) | 知识库页面、外部实例管理和创建向导交互 |
|
||||
| [外部知识库技术设计](./external-knowledge-technical-design.md) | Provider Adapter、凭据、持久化、IPC 和检索标准化 |
|
||||
| [外部知识库实测基线](./external-knowledge-probe-baseline.json) | 脱敏记录真实实例的接口状态、字段结构、特色能力和耗时 |
|
||||
|
||||
## 术语
|
||||
|
||||
@@ -27,4 +31,5 @@ Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
|
||||
或 Agent,也不在外部实例中创建、修改、上传或删除知识库内容。远端资料不会批量同步
|
||||
或进入本地索引;实际用于回答的有界片段会随引用保存在本地会话中。
|
||||
|
||||
当前尚未拆出覆盖整个知识库功能家族的统一逻辑、UI 设计和功能进度文档。
|
||||
本目录尚未拆出统一逻辑设计;当前本地知识库状态规则由 PRD、User Stories 和实现契约
|
||||
共同约束。外部知识库继续使用独立逻辑设计,直至进入实施阶段。
|
||||
|
||||
@@ -337,7 +337,10 @@ Provider 原始正文不能直接成为用户错误。Adapter 将其映射为稳
|
||||
|
||||
实施前仍需技术验证:
|
||||
|
||||
- 选定支持的 Dify、FastGPT、RAGFlow 最低版本,形成固定支持矩阵并保存真实响应 fixture。
|
||||
- 已完成第一轮真实目录与最小检索验证,并确认 Dify `1.17.0` 非空结果结构;仍需确认
|
||||
FastGPT、RAGFlow 产品版本和 Dify `0.15.8` 非空结果,再选定三者最低版本并形成固定
|
||||
支持矩阵与去敏 fixture。当前实测详情见
|
||||
[技术设计 9.0 节](./external-knowledge-technical-design.md#90-真实实例验证基线)。
|
||||
- 验证各版本检索探测的 API 审计、用量和内容修改行为。
|
||||
- 确定 Provider 返回 URL 的安全打开策略;未验证前只显示文本定位和远端 ID。
|
||||
- 依据真实延迟确定默认并发上限和跨知识库上下文配额。
|
||||
|
||||
@@ -0,0 +1,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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -358,11 +358,28 @@ knowledge:external-retrieval:test
|
||||
本节记录 2026-09-03 核对的初始公开接口。实施时应把每个受支持版本的真实响应保存为
|
||||
去敏 fixture,并以 fixture 和真实实例测试作为发布依据。
|
||||
|
||||
### 9.0 真实实例验证基线
|
||||
|
||||
2026-09-09 使用本机未入库的测试地址和 API Key 完成第一轮只读验证。验证只记录状态、
|
||||
字段名、数量和长度,不记录 API Key、知识库名称或片段正文。
|
||||
|
||||
| Provider | 实例证据 | 目录实测 | 检索实测 | 当前结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Dify `0.x` | 响应头 `x-version: 0.15.8` | 列表返回 200 和 3 项;详情 GET 返回 405 | 默认配置和显式当前配置均返回 200,结果为空 | 从目录项读取配置,手工 ID 通过检索验证;非空结果仍未验证 |
|
||||
| Dify `1.x` | 响应头 `x-version: 1.17.0` | 列表返回 200 和 1 项;详情 GET 返回 200 | 默认配置和显式当前配置均返回相同的 3 条非空结果 | 详情和 `records[].segment/document` 已验证;新增摘要索引、元数据、多模态和 Pipeline 能力字段 |
|
||||
| FastGPT | 响应未提供版本,只能确认当前托管实例 | 基址包含 `/api`;列表和详情均返回 200,列表 `data` 直接为数组,本次 1 个 Dataset | embedding 8 条、fullTextRecall 2 条、mixedRecall 7 条、mixedRecall + Rerank 9 条 | 三种检索模式和 Rerank 均已实测;当前结果包含 `q`,未包含 `a` |
|
||||
| RAGFlow | 响应未提供产品版本,Server 为 nginx,不能据此推断版本 | `GET /api/v1/datasets` 返回 200,共 28 项;17 项配置 GraphRAG,15 项有完成证据 | 选定已完成图谱的库,基础检索 0 条,`use_kg=true` 返回 1 条,Knowledge Compilation 0 条 | 图谱检索已产生区别于基础检索的结果;当前没有库具备 Knowledge Compilation 配置证据 |
|
||||
|
||||
当前 Node 测试进程设置了 `NODE_TLS_REJECT_UNAUTHORIZED=0`,因此两个 HTTPS 实例的请求
|
||||
使用了放宽的证书校验。这只能证明接口在当前测试环境可用,不能证明严格 TLS 校验通过。
|
||||
产品实现不得读取该进程级设置作为隐式默认;实例测试和保存必须按 8.1 节处理传输风险。
|
||||
|
||||
### 9.1 Dify
|
||||
|
||||
| 操作 | 接口 |
|
||||
| --- | --- |
|
||||
| 列表 | `GET /v1/datasets` |
|
||||
| 详情 | `GET /v1/datasets/{dataset_id}`,`0.15.8` 不支持、`1.17.0` 支持 |
|
||||
| 检索 | `POST /v1/datasets/{dataset_id}/retrieve` |
|
||||
| 认证 | `Authorization: Bearer <knowledge-api-key>` |
|
||||
|
||||
@@ -375,6 +392,26 @@ Adapter 只接受 Knowledge Service API Key,不调用应用 `/info`、Chat 或
|
||||
Dify 当前文档限制查询不超过 250 字符。GoodBuddy 不静默截断;超出时检索测试和聊天
|
||||
诊断返回 Provider 限制,并允许后续单独设计查询压缩策略。
|
||||
|
||||
两个实测版本共同返回 `retrieval_model_dict`,键包括搜索方式、Top K、阈值、Rerank 和
|
||||
权重。显式原样复用该配置均返回 200,证明覆盖 envelope 可用,但 UI 编辑后的每种组合仍
|
||||
需单独契约测试。
|
||||
|
||||
版本差异:
|
||||
|
||||
| 能力 | `0.15.8` | `1.17.0` | Adapter 和 UI 规则 |
|
||||
| --- | --- | --- | --- |
|
||||
| Dataset 详情 | GET 返回 405 | GET 返回 200 | 能力探测后调用;旧版从目录项读取配置,不能把 405 视为实例故障 |
|
||||
| 非空检索结构 | 本次未命中 | 默认和显式配置均返回 3 条 | 1.17 已验证 `records[].segment.content`、`segment.document.id/name`、位置和分数映射 |
|
||||
| 元数据 | 无 `doc_metadata` | 字段存在,当前库定义数为 0 | 有实际字段定义时才显示受控元数据过滤器;不提供自由 JSON |
|
||||
| 多模态与附件 | 未返回能力字段 | `is_multimodal` 和 `files` 字段可用,当前库非多模态且结果无附件 | 只显示远端状态;仅在结果实际带 `files` 时显示附件引用 |
|
||||
| 父子分块 | 未验证 | 返回 `child_chunks` 数组,本次为空 | 仅在实际非空时显示子块证据,不补造父子关系 |
|
||||
| 摘要索引 | 未返回能力字段 | 当前库配置了 `summary_index_setting`,本次结果 `summary` 为空 | 概览显示只读状态;仅在结果实际返回摘要时展示 |
|
||||
| Knowledge Pipeline | 未返回能力字段 | `pipeline_id/runtime_mode` 存在,当前库未配置 Pipeline | 只读显示,不提供运行或编辑 Pipeline 的操作 |
|
||||
|
||||
Dify 1.x 官方 RetrievalModel 还声明 `metadata_filtering_conditions`,并支持
|
||||
`keyword_search/semantic_search/full_text_search/hybrid_search`。当前实测库没有 Metadata
|
||||
字段,未执行空条件或猜测字段探测;该配置保持禁用,直到所选库详情返回可用字段定义。
|
||||
|
||||
### 9.2 FastGPT
|
||||
|
||||
| 操作 | 接口 |
|
||||
@@ -384,10 +421,17 @@ Dify 当前文档限制查询不超过 250 字符。GoodBuddy 不静默截断;
|
||||
| 检索 | `POST /api/core/dataset/searchTest` |
|
||||
| 认证 | `Authorization: Bearer <api-key>` |
|
||||
|
||||
列表响应同时包含文件夹和知识库,只有 `type = dataset` 可以绑定。检索映射 `datasetId`、
|
||||
列表响应同时包含文件夹和知识库,只有 `type = dataset` 可以绑定。当前实测列表的
|
||||
`data` 直接为数组,不是 `data.list`;Adapter 可以仅对已经实测的这两种明确 envelope
|
||||
做版本化解析,不递归猜测任意嵌套。检索映射 `datasetId`、
|
||||
`text`、Token `limit`、`similarity`、`searchMode` 和 `usingReRank`。初始版本不发送查询优化
|
||||
字段,避免调用 FastGPT 配置的 LLM。
|
||||
响应从 `q`、`a`、`sourceName`、`sourceId`、`collectionId` 和 `score` 构造片段与引用。
|
||||
当前实测响应的 `data` 包含 `list/duration/limit/searchMode/usingReRank/similarity`;结果项
|
||||
包含 `q/id/datasetId/collectionId/sourceName/sourceId/chunkIndex/score`,未返回 `a`。
|
||||
Adapter 以 `q` 为必需片段,`a` 仅在远端实际返回时追加,不能要求 `a` 存在。
|
||||
当前实例的 embedding、fullTextRecall、mixedRecall 和 mixedRecall + Rerank 均返回 200 和
|
||||
非空结果,因此这四种界面组合可以启用。查询扩展会调用模型,仍不在初始界面和探测脚本中
|
||||
启用。
|
||||
|
||||
FastGPT 4.15.0 起部署实例的 `/apidoc/devapi` 是接口事实来源,手工文档可能落后。支持矩阵
|
||||
必须按真实版本测试;不能因为健康页可访问就假定列表和检索 Schema 相同。`searchTest`
|
||||
@@ -405,17 +449,29 @@ FastGPT 4.15.0 起部署实例的 `/apidoc/devapi` 是接口事实来源,手
|
||||
检索映射 `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` 提取片段、文档、位置和原始相似度。
|
||||
`data.doc_aggs` 提取片段、文档、位置和原始相似度。当前实测非空 chunk 使用
|
||||
`document_keyword` 表示文档名,并包含 `id/document_id/content/positions/similarity/`
|
||||
`term_similarity/vector_similarity`;初始 Adapter 应按该字段映射,不假定存在
|
||||
`document_name`。
|
||||
|
||||
图谱规则:
|
||||
|
||||
- `parser_config.graphrag` 或等价详情字段作为已有图谱的证据,不写回。
|
||||
- `parser_config.graphrag.use_graphrag = true` 只表示库配置了 GraphRAG;单独出现该配置不
|
||||
表示图谱已经构建完成。
|
||||
- `graphrag_task_finish_at` 为有效非零时间才作为图谱完成证据,不写回。
|
||||
- `use_kg` 是检索时开关,只在已有图谱和受支持版本均确认后发送。
|
||||
- Knowledge Compilation 与旧 GraphRAG 分别建模。
|
||||
- `include_knowledge_compilation` 使用本地保存的显式布尔值,初始默认 false;即使服务端
|
||||
默认 true,也不依赖字段省略获得隐式行为。
|
||||
- `compilation_template_group_id` 或后续版本的等价受控字段作为 Knowledge Compilation 已
|
||||
配置证据;仅仅请求返回 200 不能启用界面开关。
|
||||
- Provider 没有返回图节点或路径证据时,GoodBuddy 不生成图谱引用。
|
||||
|
||||
当前实例 28 个库中 17 个配置 GraphRAG、15 个存在完成证据。对一个已完成库使用同一查询,
|
||||
基础检索返回 0 条,`use_kg=true` 返回 1 条且原始相似度为 1,说明图谱开关对实际结果有
|
||||
影响。当前 28 个库均没有 Knowledge Compilation 配置证据;该请求虽返回 200 和零结果,
|
||||
界面仍必须禁用开关并显示“当前知识库未配置 Knowledge Compilation”。
|
||||
|
||||
## 10. 检索编排
|
||||
|
||||
```text
|
||||
@@ -544,6 +600,48 @@ Adapter 可以保存少量受控事实,例如“目录接口可用”“详情
|
||||
|
||||
Mock 或公开文档通过不能代替真实实例验收。
|
||||
|
||||
2026-09-09 基线已完成 Dify `0.15.8`/`1.17.0` 目录、详情差异和 1.17 非空检索结构,
|
||||
FastGPT/RAGFlow 产品版本、严格 TLS、401/403/404/429、超时和取消尚未完成,因此不满足
|
||||
本节完整验收条件。
|
||||
|
||||
### 14.6 可重复探测脚本
|
||||
|
||||
仓库提供 `node scripts/external-knowledge-probe.mjs`。默认从进程环境变量读取
|
||||
`GOODBUDDY_<PROVIDER>_BASE_URL/API_KEY`,本地开发也兼容当前两行格式的 `.env.dify`、
|
||||
`.env.fastgpt` 和 `.env.ragflow`;Dify 文件还支持多个 `标签/URL/API Key` 三行配置块,用于
|
||||
同一次报告比较多个版本。输出不包含地址、凭据、远端 ID、名称、查询和正文。
|
||||
|
||||
```text
|
||||
node scripts/external-knowledge-probe.mjs
|
||||
node scripts/external-knowledge-probe.mjs --provider=ragflow --extended
|
||||
node scripts/external-knowledge-probe.mjs --extended --output=<report.json>
|
||||
```
|
||||
|
||||
默认模式覆盖目录、详情和基础检索。`--extended` 额外执行 Dify 当前配置覆盖、FastGPT
|
||||
Rerank、RAGFlow GraphRAG 和 Knowledge Compilation 检索。脚本只有固定 GET/POST 读取与
|
||||
检索端点,不实现或调用远端创建、更新、上传和删除。
|
||||
|
||||
FastGPT 报告将请求成功与命中数量分开:HTTP 必须成功、业务 `code` 必须为 `200`,
|
||||
且 `data` 或 `data.list` 必须是数组,才记录 `success: true` 和 `count`。成功的空数组
|
||||
记录 `count: 0`;HTTP、业务、响应结构、JSON 解析或网络失败记录 `success: false` 与
|
||||
固定失败类别,不用零命中代替失败。目录失败时不继续调用详情和检索。
|
||||
`features.searchModes` 只包含基础检索实际成功的模式;`rerankProbed` 表示实际尝试,
|
||||
`rerankSucceeded` 单独表示成功。历史基线保留原始证据,不追填未观测的成功字段。
|
||||
`tests/external-knowledge-probe-fastgpt.test.ts` 使用内存 fetch fixture 运行真实 CLI,
|
||||
覆盖错误、成功零命中和非空结果,不连接外部知识服务。
|
||||
|
||||
### 14.7 CRUD 验收边界
|
||||
|
||||
| 对象 | Create | Read | Update | Delete | 验收方式 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| GoodBuddy 外部实例 | 是 | 是 | 是 | 是 | SQLite、凭据存储、IPC 和 Renderer 集成测试 |
|
||||
| GoodBuddy 外部绑定 | 是 | 是 | 是 | 是 | SQLite、统一知识库列表、检索和删除确认测试 |
|
||||
| Provider 远端知识库与内容 | 否 | 目录、详情、检索 | 否 | 否 | 探测脚本确认只有只读端点;代码审查确认无写端点 |
|
||||
|
||||
FastGPT 等 Provider 的公开 API 确实提供远端知识库、集合和数据 CRUD,但这不属于当前产品
|
||||
接入。若未来要验证远端 CRUD,必须先修改 PRD,并使用可删除的专用测试实例或命名空间;
|
||||
不能在本轮三个既有实例上执行破坏性测试。
|
||||
|
||||
## 15. 实施顺序
|
||||
|
||||
这里的顺序只用于控制代码依赖,不裁剪产品设计:
|
||||
|
||||
@@ -258,6 +258,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 当前实例 | 相似度、向量权重、KNN Top K;具备完成证据时启用图谱检索 | 没有独立配置证据时禁用 Knowledge Compilation;Metadata 条件仍隐藏 | `graphrag_task_finish_at` 有效才启用图谱;`compilation_template_group_id` 有效才启用 Knowledge Compilation |
|
||||
|
||||
RAGFlow 列表项应区分三个图谱状态:“未配置”“构建中或未完成”“可用于图谱检索”。不能
|
||||
仅因 `parser_config.graphrag.use_graphrag = true` 就显示为可用。当前实测中 17 个库配置了
|
||||
GraphRAG,只有 15 个存在完成证据。
|
||||
|
||||
Knowledge Compilation 的请求被服务器接受但当前没有库提供配置证据,因此开关显示为禁用,
|
||||
说明为“当前知识库未配置 Knowledge Compilation”。返回 200 或零结果都不能改变此状态。
|
||||
|
||||
Dify 实例行持续显示探测版本。`0.15.8` 的详情请求返回 405 时显示“此版本从目录读取配置”,
|
||||
不显示错误状态;`1.17.0` 详情页增加只读能力行:文档可用数、Metadata 字段数、多模态、
|
||||
摘要索引和 Knowledge Pipeline。没有配置的能力显示“未配置”,不隐藏成未知,也不提供会
|
||||
写回 Dify 的编辑操作。
|
||||
|
||||
Dify 1.x 的 Metadata 条件使用受控字段编辑器:先从 `doc_metadata` 选择字段,再按字段类型
|
||||
提供比较操作和值输入。当前库字段数为 0 时控件禁用并显示“当前知识库没有 Metadata 字段”。
|
||||
不得允许自由 JSON,也不得把 `attachment_ids`、Pipeline 或摘要 Prompt 暴露为检索输入。
|
||||
|
||||
## 8. 外部知识库详情
|
||||
|
||||
选中外部绑定后,现有详情区按来源类型裁剪:
|
||||
|
||||
@@ -162,7 +162,7 @@ GoodBuddy 当前已经具备:
|
||||
引用和脱敏诊断。
|
||||
- Agent Runtime 的选择或切换不改变当前向量连接;向量连接的选择或切换也不改变当前
|
||||
Agent Runtime。
|
||||
- 未来记忆、笔记或其他本地语义检索若要复用向量连接,必须经过独立产品设计、权限与
|
||||
- 分区记忆、笔记或其他本地语义检索若要复用向量连接,必须经过独立产品设计、权限与
|
||||
数据路径评审后显式接入,不能因存在全局向量连接而自动获得使用权。
|
||||
|
||||
---
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 实施中 |
|
||||
| 版本 | 0.2 |
|
||||
| 日期 | 2026-08-22 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-09-05 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 实施范围 | 第一阶段:可用、可见、可诊断;第二阶段:可调、可优化、可维护 |
|
||||
| 相关设计 | [本地文本向量模型与连接设计](./local-embedding-technical-design.md) |
|
||||
@@ -37,7 +37,10 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
2. 保持 Electron Main、Preload、Renderer 的安全边界,Renderer 不直接读取
|
||||
数据库、原文件或向量。
|
||||
3. 保留“模型按需检索”,并新增“每次先检索”模式。后者必须由 Main 进程
|
||||
预检索,不能只依赖提示词要求模型调用工具。
|
||||
预检索,不能只依赖提示词要求模型调用工具。
|
||||
`knowledge_search` 的 `limit` 与现有检索引擎对齐,接受 1–100,省略时返回最多 6 条;
|
||||
调用方可请求超过 8 条。授权范围、检索排序和工具结果字节预算继续生效,因此实际
|
||||
条数可能少于请求值。
|
||||
4. 知识库新建后不默认启用全部已有知识库;对话中的范围继续由用户显式选择。
|
||||
5. 向量服务不可用时保留全文与中文检索,但必须返回明确降级状态。这是可见的检索通道
|
||||
降级,不得自动切换内置连接、用户添加的 OpenAI 兼容连接或其他向量模型。
|
||||
@@ -149,7 +152,7 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
|
||||
## 6. 信息架构
|
||||
|
||||
知识工作区继续使用主从布局和现有四个页签:
|
||||
知识工作区使用主从布局和四个面向用户任务的页签:
|
||||
|
||||
```text
|
||||
知识库
|
||||
@@ -160,14 +163,21 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
|
||||
│ └─ 分块查看与维护
|
||||
├─ 知识图谱
|
||||
├─ 任务中心
|
||||
└─ 设置
|
||||
└─ 高级设置
|
||||
├─ 问答效果入口
|
||||
├─ 向量索引
|
||||
├─ 检索设置
|
||||
├─ 分块设置
|
||||
└─ 图谱设置
|
||||
```
|
||||
|
||||
“检索测试”是当前知识库的高频诊断操作,通过知识库标题区次操作打开独立
|
||||
工作台,不新增第五个一级页签。
|
||||
“检索测试”是当前知识库的高频验证操作,通过知识库标题区和“高级设置 →
|
||||
问答效果”打开同一个独立工作台,不新增第五个一级页签。工作台首次打开只突出真实
|
||||
问题输入和测试操作;候选倍数、权重、阈值、重排和上下文预算默认折叠。
|
||||
|
||||
首次使用流程固定为“创建知识库 → 添加资料 → 等待可检索 → 用于当前对话”。创建表单
|
||||
默认只展示名称和描述;存储方式与知识图谱属于折叠的高级设置。没有知识库时,唯一主操作
|
||||
位于首次使用空状态中。
|
||||
|
||||
全局向量模型仍在“设置 → 模型连接 → 向量模型”中配置。GoodBuddy 内置连接和用户添加的
|
||||
OpenAI 兼容连接使用同一连接列表;其界面、数据路径及切换语义以
|
||||
@@ -195,6 +205,10 @@ Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不
|
||||
2. 检索方式:模型按需检索、每次先检索。
|
||||
3. 当前范围为空、索引降级或向量未配置时的短说明。
|
||||
|
||||
知识库 ID 选择按 Conversation 持久化。新对话和未保存过知识范围的旧对话默认不选择
|
||||
任何知识库;切换对话恢复各自范围,不得在应用启动时自动勾选全部知识库。新建知识库只
|
||||
加入当前 Conversation,不重新启用用户已取消的其他知识库。
|
||||
|
||||
## 7. 第一阶段:可用、可见、可诊断
|
||||
|
||||
### 7.1 检索方式
|
||||
@@ -296,6 +310,14 @@ Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不
|
||||
- 失败文档数。
|
||||
- 当前向量模型与索引是否兼容。
|
||||
|
||||
`indexedDocumentCount` 只统计状态为 `ready` 的文档;`queued`、`parsing` 和
|
||||
`indexing` 计入处理中,`failed` 单独计入需处理。文档行持续显示全文、向量和图谱通道
|
||||
状态。已有可用分块的失败文档仍允许查看分块,并提供重新处理。
|
||||
|
||||
文档行可通过受控 `knowledgeBaseId + documentId` 打开数据库中保存的原始来源。
|
||||
Renderer 不得提交任意路径。移除来源必须先说明将删除的文档、索引、图谱证据及是否包含
|
||||
应用托管副本,用户确认后才执行。
|
||||
|
||||
降级事件包括:
|
||||
|
||||
- 未配置向量模型。
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# 知识库实施进度
|
||||
|
||||
## 已验证实现
|
||||
|
||||
截至 2026-09-05:
|
||||
|
||||
- 知识库选择按 Conversation 持久化,新对话默认空范围。
|
||||
- 创建流程默认只展示名称和描述,高级存储与图谱设置折叠。
|
||||
- 知识库汇总区分可检索、处理中和需处理文档。
|
||||
- 文档行展示全文、向量和图谱状态,并支持打开原始来源和重新处理失败文档。
|
||||
- 移除来源需要确认并说明级联影响。
|
||||
- 检索高级参数默认折叠,“高级设置”提供问答效果入口。
|
||||
- 向量未配置状态可直接前往模型设置。
|
||||
- 有效 DOCX 与多页 PDF 已通过磁盘文件导入、解析、分块和检索生产链路。
|
||||
|
||||
## 验证证据
|
||||
|
||||
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);本项验证未调用模型。
|
||||
|
||||
## 剩余工作
|
||||
|
||||
- 外部 Dify、FastGPT 和 RAGFlow 已完成 Main 适配基础;实例凭据持久化、绑定迁移、IPC、
|
||||
Renderer 管理界面、`retrieveMany`、聊天预检索和 MCP 网关仍未接入,当前不能由用户使用。
|
||||
- 扫描 PDF 的真实 OCR 验收依赖用户已安装并校验的 OCR 模型;本次 DOCX/PDF 回归覆盖
|
||||
原生文本 PDF,不宣称覆盖扫描件 OCR。
|
||||
- PDF.js 在测试环境会提示未配置 `standardFontDataUrl`;文本、页码定位与检索结果不受
|
||||
影响。
|
||||
@@ -0,0 +1,70 @@
|
||||
# 知识库 UI 设计
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
知识库优先帮助普通用户完成“添加资料并可靠提问”,高级 RAG 调试和图谱维护不得阻塞
|
||||
首次使用。全局视觉、页面壳层、通知、焦点和危险操作遵循
|
||||
[UI-DESIGN.md](../../../UI-DESIGN.md)。
|
||||
|
||||
## 2. 信息架构
|
||||
|
||||
```text
|
||||
知识库
|
||||
├─ 知识库列表
|
||||
└─ 当前知识库
|
||||
├─ 文档与来源
|
||||
├─ 知识图谱(启用后显示)
|
||||
├─ 任务中心
|
||||
└─ 高级设置
|
||||
```
|
||||
|
||||
标题区提供“用于当前对话”“检索测试”、编辑和删除。新建知识库是页面主操作。
|
||||
“高级设置”承载问答效果入口、向量索引、分块与图谱配置,不把工程参数放在首次路径。
|
||||
|
||||
## 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 和逻辑设计;本页定义产品边界,技术设计定义唯一的请求合并规则。
|
||||
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# 模型连接请求定制进度
|
||||
|
||||
## 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,20 @@ OpenCode 与 DeepSeek Harness 不使用该值。
|
||||
- 无效状态不能只依赖颜色,必须同时提供错误文字和 `aria-invalid`。
|
||||
- 文本域遵循标准键盘编辑、选择、复制和粘贴行为;保存继续使用设置页唯一的主保存操作。
|
||||
- 焦点边框、字体、间距、颜色和深浅主题全部复用全局语义令牌。
|
||||
|
||||
## 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,11 @@ 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.23`、Desktop `0.12.11`,新增远程文件/Git 管理接口和可选模型限额,
|
||||
并修复无限请求时长下的连接超时。Agent `0.11.23` 要求先升级至 Desktop `0.12.11`;
|
||||
新包内的 Runtime 不再设置固定十分钟 Prompt 时限,显式请求期限与取消仍有效。
|
||||
正式发布状态以 Agent 与 Desktop 独立发布渠道为准。
|
||||
现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
|
||||
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
|
||||
|
||||
@@ -67,8 +70,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 +105,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 +118,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 +209,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 +256,25 @@ 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 消息持久化路径。
|
||||
- ACP 工具结果同时支持文本 `content` 和 `rawOutput.output`,移除 OpenCode 的
|
||||
`<task><task_result>` 外层包装后渲染正文。恢复已有子代理时使用 ACP 所带的所属
|
||||
`toolCallId` 继续路由,不等待新的父 Task metadata,也不重放模型请求。
|
||||
远端实时过程需要包含该插件的 Agent;旧 Agent 的最终结果仍可查看,历史未采集过程
|
||||
不会被补造。
|
||||
- 本机 OpenCode SDK 路径允许不同会话并行,同一会话仍按请求顺序执行。每个请求独立拥有
|
||||
SSE 事件订阅,并在正常完成、错误、取消或消费方结束迭代时主动关闭自己的响应流,
|
||||
不关闭其他会话的订阅或共享 Server。取消导致事件迭代结束时保留原取消原因,不改报
|
||||
“事件流意外结束”。这一清理规则属于本机 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 +304,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 始终为唯一终态预留一个事件和最大事件字节,
|
||||
@@ -290,13 +335,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 +366,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 +466,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 +569,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 +615,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 +721,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 端到端路径通过后才能更新实施状态。
|
||||
@@ -1,159 +0,0 @@
|
||||
# ShareServer Web 管理界面设计
|
||||
|
||||
## 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 设计中 |
|
||||
| 版本 | 0.1 |
|
||||
| 日期 | 2026-09-03 |
|
||||
| 关联 PRD | [ShareServer PRD](./prd.md) |
|
||||
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
|
||||
|
||||
ShareServer 是独立 Web 产品,不直接复用 Electron 页面壳层实现,但沿用 GoodBuddy 的语义
|
||||
词汇、状态含义和克制的运营工具风格。界面以列表、表格和主从详情为主,不使用营销首页或
|
||||
装饰性卡片墙。
|
||||
|
||||
## 1. 使用对象
|
||||
|
||||
- 普通成员主要在 GoodBuddy Desktop 使用能力;Web 端只处理自己的设备、申请和授权。
|
||||
- 组织管理员使用完整组织控制台。
|
||||
- 系统管理员使用与组织控制台分开的实例管理范围。
|
||||
- 审计员进入只读视图,不显示可变更控件。
|
||||
|
||||
登录后直接进入当前身份可用的实际控制台,不显示产品宣传页。
|
||||
|
||||
## 2. 全局框架
|
||||
|
||||
```text
|
||||
顶部栏:ShareServer 实例名 | 当前组织 | 全局搜索 | 审批 | 账号
|
||||
|
||||
侧栏:
|
||||
概览
|
||||
组织与成员
|
||||
设备
|
||||
能力目录
|
||||
权限策略
|
||||
审批
|
||||
发布与更新
|
||||
网关联邦
|
||||
任务与中继
|
||||
审计
|
||||
系统设置(仅系统管理员)
|
||||
```
|
||||
|
||||
当前组织始终显示。切换组织会重新加载整个数据范围,不保留上一组织的选择、筛选或详情。
|
||||
系统管理使用“实例范围”标识,不能伪装成某个组织。
|
||||
|
||||
## 3. 概览
|
||||
|
||||
概览只显示需要行动和运行事实:
|
||||
|
||||
- 待审批数量及最旧等待时间。
|
||||
- 在线/离线/已撤销设备。
|
||||
- 可用/离线/暂停能力。
|
||||
- 当前任务、失败和结果未知。
|
||||
- 包更新异常和紧急撤销。
|
||||
- 联邦连接异常。
|
||||
- 中继流量和容量,仅在中继启用时显示。
|
||||
|
||||
指标可点击进入已带筛选的对应页面。没有事件时不显示虚构趋势。
|
||||
|
||||
## 4. 组织与成员
|
||||
|
||||
使用“成员”“用户组”“角色”三个同级页签。成员表包含姓名、身份源、组、状态、设备数和
|
||||
最近活动。邀请是唯一主操作;暂停和移除进入行末菜单。
|
||||
|
||||
成员详情采用抽屉,显示身份、组、角色、设备、有效 Grant、申请和审计。管理员不能查看成员
|
||||
任务正文或桌面文件。
|
||||
|
||||
## 5. 设备
|
||||
|
||||
```text
|
||||
设备 所有者 平台 状态 能力 最近在线
|
||||
Jiang-PC Jiang Windows 在线 2 刚刚
|
||||
GPU-Worker-02 AI Team Linux 离线 8 2 小时前
|
||||
```
|
||||
|
||||
设备详情包含概览、连接、已发布能力、Grant 和活动。设备状态与 Publication 状态分别显示。
|
||||
撤销设备使用危险确认,说明新任务、活动任务和重新注册影响。
|
||||
|
||||
## 6. 能力目录
|
||||
|
||||
目录以能力为第一列,提供者为可展开信息:
|
||||
|
||||
```text
|
||||
能力 版本 提供者 可见范围 可用性
|
||||
图像生成 2.1.0 3 个节点 设计组 2/3 在线
|
||||
```
|
||||
|
||||
详情包含 manifest、provider、Publication、允许输入、数据政策、调用趋势和最近错误。管理员
|
||||
看到的是组织元数据,不通过目录读取能力内部数据。
|
||||
|
||||
## 7. 权限策略
|
||||
|
||||
策略页分“强制策略”和“Grant”。创建和编辑使用结构化表单:主体、能力、动作、输入、提供者、
|
||||
时间、预算、批准和委托。保存前提供自然语言摘要:
|
||||
|
||||
> 允许“设计组”从合规设备调用组织内“图像生成”2.x;只发送文本和图片;每人每日 20 次;
|
||||
> 不允许下游委托。
|
||||
|
||||
策略冲突预览显示最终结果和命中规则,但不提供允许规则覆盖拒绝规则的顺序拖拽。
|
||||
|
||||
## 8. 审批
|
||||
|
||||
审批收件箱按调用、发布、更新和联邦分类。详情必须显示申请人、能力或对象、目标范围、数据
|
||||
类别、权限变化、期限和策略依据。批准按钮使用具体文案,例如“允许本次调用”“批准此版本
|
||||
发布”,不能只有“同意”。
|
||||
|
||||
批量批准仅用于同类型、同策略且不包含权限扩大的低影响申请;联邦和权限扩大更新不支持
|
||||
批量批准。
|
||||
|
||||
## 9. 发布与更新
|
||||
|
||||
页面分“能力包”“发布通道”“更新策略”“撤销记录”。Package 表显示版本、发布者、签名、
|
||||
大小、目标平台、权限变化和状态。上传后先进入验证,不直接发布。
|
||||
|
||||
版本详情提供 manifest、摘要、签名、兼容范围、权限 diff、依赖和下载事实。紧急撤销位于
|
||||
独立 danger zone,需要输入版本标识确认,并说明受影响设备和新任务。
|
||||
|
||||
## 10. 网关联邦
|
||||
|
||||
```text
|
||||
合作组织 对端地址 状态 开放能力 有效期
|
||||
法务服务商 share.legal.example 已连接 2 项 12-31
|
||||
```
|
||||
|
||||
建立关系采用向导:填写端点、核对组织和指纹、选择公开范围、设置调用与数据限制、生成 offer、
|
||||
等待对方确认、复核双方交集。只有双方完成后显示“已连接”。
|
||||
|
||||
详情明确展示“我方开放”“对方开放”“最终可用”三个集合。暂停和撤销说明对活动任务的处理。
|
||||
|
||||
## 11. 任务与中继
|
||||
|
||||
任务表显示调用方、能力、provider、路由、数据类别、状态、耗时和时间。默认不显示正文。
|
||||
活动任务提供取消;结果未知使用独立状态并显示 provider 最后确认点。
|
||||
|
||||
中继未启用时页面显示控制面正常和启用位置,不展示零值图表。启用后显示活动连接、吞吐、
|
||||
排队、容量和拒绝原因,不提供查看正文的调试入口。
|
||||
|
||||
## 12. 审计
|
||||
|
||||
提供操作者、动作、对象、组织、结果、时间和关联 ID 的可筛选表格。导出前显示条件、估算行数、
|
||||
时区和字段范围;大导出作为后台任务生成有时效下载。审计详情保持只读,不把原始内部错误、
|
||||
认证信息或任务正文显示给管理员。
|
||||
|
||||
## 13. 系统设置
|
||||
|
||||
只包含实例名称、公开地址、身份源、邮件、数据库/对象存储状态、中继开关与上限、保留策略、
|
||||
联邦总开关、证书和版本。秘密字段只支持替换或测试,不回显原值。
|
||||
|
||||
运行时基础设施配置优先来自部署环境;Web 界面不能修改数据库连接串、主密钥或容器挂载路径。
|
||||
|
||||
## 14. 状态、响应式与无障碍
|
||||
|
||||
- 加载、无数据、无匹配、无权限、服务错误和部分不可用分别展示。
|
||||
- 管理表格支持键盘访问、可见焦点、表头关联和分页;行菜单有可访问名称。
|
||||
- 窄屏将表格转为字段列表,保持组织、来源、状态和风险信息,不把管理动作挤成不可辨图标。
|
||||
- 所有时间显示实例时区并可查看 ISO 时间;联邦事件同时显示对端时间时标明时区。
|
||||
- 状态不只依赖颜色。危险操作在确认后保持原页面上下文和可恢复错误。
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 入口与范围已实现;未来分区记忆待独立设计 |
|
||||
| 版本 | 0.5 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 状态 | 入口与范围已实现;分区记忆协同待设计 |
|
||||
| 版本 | 0.6 |
|
||||
| 日期 | 2026-09-05 |
|
||||
| 适用产品 | GoodBuddy 桌面端 |
|
||||
| 文档角色 | 智能心跳当前能力、权威入口、范围和长期边界 |
|
||||
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md) |
|
||||
@@ -14,8 +14,8 @@
|
||||
| 相关界面 | [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md) |
|
||||
| Task 模型 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
|
||||
|
||||
> 本文区分近期实施与长期方向。未完成设计的“未来分区记忆”不得转写为数据结构、状态机、
|
||||
> 页面、迁移或验收条件。
|
||||
> 本文区分近期实施与长期方向。分区记忆由[分区记忆 PRD](../memory/prd.md)定义;智能心跳
|
||||
> 不能另行创建平行的记忆类型、表或页面。
|
||||
|
||||
## 1. 已确认的产品边界
|
||||
|
||||
@@ -26,23 +26,23 @@
|
||||
- 智能心跳不并入通用自动化中心,也不作为任务中心条目。
|
||||
- 现有心跳报告、记忆建议、行动建议和审计历史在本轮继续保留。
|
||||
|
||||
### 1.2 长期方向:未来分区记忆
|
||||
### 1.2 长期方向:分区记忆协同
|
||||
|
||||
已确认的方向只有:
|
||||
|
||||
> 智能心跳未来应成为按范围隔离的未来记忆。
|
||||
> 智能心跳后续应按分区回顾本地信息,提出对应分区的记忆候选和可选唤起建议。
|
||||
|
||||
以下内容尚未设计:
|
||||
|
||||
- “未来记忆”的数据结构与存储方式。
|
||||
- 与当前长期记忆、心跳报告和建议的关系。
|
||||
- 唤起条件、状态和生命周期。
|
||||
- 新增 Automation、Run 和 Agent 等分区后的逐分区回顾规则。
|
||||
- 与当前记忆候选、心跳报告和建议的关系。
|
||||
- 时间与内部事件唤起条件的字段、状态和生命周期。
|
||||
- 到期后的页面和交互。
|
||||
- 是否以及如何创建 Task。
|
||||
- 旧版心跳数据如何迁移。
|
||||
|
||||
本轮不得实现 `FutureMemory`、新增未来记忆数据库表,或制作“未来记忆 / 即将唤起 /
|
||||
已唤起”等页面。后续需要独立 PRD 和用户确认。
|
||||
本轮不得扩展分区记忆类型或表。后续唤起能力必须作为分区记忆条目的可选条件,并继续遵守
|
||||
该条目的分区读取边界。
|
||||
|
||||
## 2. 本轮实施范围
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
### 2.2 非目标
|
||||
|
||||
- 不设计或实现未来分区记忆。
|
||||
- 不扩展分区记忆模型或实现唤起条件。
|
||||
- 不修改 Task、Job 或 Task Center 模型。
|
||||
- 不新增独立 Automation Center。
|
||||
- 不移除定时任务现有入口。
|
||||
@@ -181,7 +181,7 @@
|
||||
|
||||
## 6. 数据与安全要求
|
||||
|
||||
目标合同只扩展现有心跳范围,不引入 Future Memory:
|
||||
目标合同只扩展现有心跳范围,不扩展分区记忆合同:
|
||||
|
||||
```ts
|
||||
type HeartbeatScope =
|
||||
@@ -217,11 +217,11 @@ type HeartbeatScope =
|
||||
- 如需要,设置中心仅保留“打开智能心跳”导航。
|
||||
- 验证应用内仍只有一个完整配置入口。
|
||||
|
||||
### 后续:独立设计
|
||||
### 后续:分区记忆协同设计
|
||||
|
||||
- 未来分区记忆。
|
||||
- 与长期记忆的关系。
|
||||
- 唤起模型与生命周期。
|
||||
- 按 Global、Project、Automation、Run 和 Agent 分区独立回顾并提出候选。
|
||||
- 与已确认记忆、候选、心跳报告和建议的关系。
|
||||
- 时间与内部事件唤起条件及生命周期。
|
||||
- 与 Task 的关系。
|
||||
|
||||
这些内容需要新的 PRD 和明确确认,不属于本轮实现。
|
||||
@@ -242,5 +242,5 @@ type HeartbeatScope =
|
||||
- [x] 设置中心不再包含单条心跳 CRUD。
|
||||
- [x] 任务中心本身以及定时任务现有行为没有被删除。
|
||||
- [x] 智能心跳仍保持只读、禁用工具和有界输入输出。
|
||||
- [x] 没有新增 Future Memory 表、状态机或未确认页面。
|
||||
- [x] 没有新增平行的记忆类型、表或页面。
|
||||
- [x] 加载失败、空状态、字段错误和危险操作满足统一设计与无障碍要求。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
## 权威文档
|
||||
|
||||
1. [Task 与 Job 统一领域模型](./task-and-job-model.md):术语、身份和对象关系。
|
||||
2. [Task Center PRD](./task-center-prd.md):Task 的应用级索引。
|
||||
2. [Task Center PRD](./task-center-prd.md):Task 的单实例项目跟随索引与范围切换。
|
||||
3. [Scheduled Task PRD](./scheduled-task-prd.md):时间或事件触发的 Task。
|
||||
4. [Goal Task PRD](./goal-task-prd.md):围绕可验证结果有界推进的 Task。
|
||||
5. [Job 与 Subjob PRD](./job-and-subjob-prd.md):Task 内部串行、并行和委派执行。
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | 首期稳定 Task 生命周期、创建体验与 Conversation 输入仲裁已实现;高级触发和执行治理待实施 |
|
||||
| 版本 | 0.7 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 版本 | 0.8 |
|
||||
| 日期 | 2026-09-10 |
|
||||
| 依赖 | [Task 与 Job 统一领域模型](./task-and-job-model.md) |
|
||||
| 相关架构 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md) |
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
Scheduled Task 是带时间或事件触发器的 Task。每个 Scheduled Task 关联一条 Conversation;
|
||||
一条 Conversation 可以同时承载多个 Task。
|
||||
|
||||
当前时间触发的含义是向关联的普通 Conversation 发送一条任务要求。执行时读取该会话的
|
||||
最新历史、Runtime、工作模式和知识范围,工具配置使用普通发送的同一规则。任务不保存
|
||||
创建时的上下文或权限快照。新建会话按普通会话规则初始化,不复制来源会话的历史和配置。
|
||||
|
||||
创建 Scheduled Task 时:
|
||||
|
||||
1. 用户选择关联当前 Conversation 或创建新 Conversation。
|
||||
@@ -34,20 +38,20 @@ GoodBuddy 当前已实现首期统一生命周期:
|
||||
Conversation 的标题和既有消息。
|
||||
- 每个 Schedule 绑定一个稳定产品级 Task 和 Conversation;重复触发复用同一身份,不再
|
||||
为每次触发创建新的顶层 Task。
|
||||
- 默认选择 Execute,并允许用户主动切换 Ask;不支持工具执行时明确禁用 Execute。
|
||||
- 单次、每日和每周计划支持暂停、恢复、立即运行、应用重启恢复和最多 4 个独立计划并发。
|
||||
- 创建表单只设置任务要求、关联会话和时间;工作模式在关联会话中设置,未设置时使用项目默认值。
|
||||
- 单次、每日和每周计划支持暂停、恢复、立即运行和应用重启恢复。
|
||||
- 到期和手动运行先进入关联 Conversation 的持久输入队列,与回复期间继续发送的普通消息
|
||||
顺序仲裁;默认不打断当前回复,也不与其并发写入时间线。
|
||||
- Composer 上沿显示待发送项和来源。用户可以删除尚未执行的 occurrence,或选择“立即
|
||||
中断并插入”取消当前执行并将该项提升为下一项。
|
||||
- 文本结果和失败写回关联 Conversation 并带 Task 来源;独立文件和图片继续保存为 Artifact。
|
||||
- 任务要求成为普通用户消息,回复、工具活动、提问和失败通过普通聊天流程呈现;独立文件和图片继续保存为 Artifact。
|
||||
- 左侧 Conversation 列表、Conversation Task 区和 Task Center 使用同一产品 Task;普通
|
||||
模型请求、Subagent、委派和 Smart Heartbeat 内部 Task 不进入产品索引。
|
||||
- v22 迁移保留 Schedule 配置和历史运行,并为旧计划补齐稳定 Task 与 Conversation;v23
|
||||
增加可恢复的统一 Conversation 输入队列。
|
||||
|
||||
尚未实现的高级能力包括 IANA 时区与 DST 墙上时间、每月/工作日/受限 Cron、事件触发、
|
||||
可配置错过执行策略、租约、重试与结果未知治理、完整预算和权限快照,以及面向内部
|
||||
可配置错过执行策略、租约、重试与结果未知治理、完整预算,以及面向内部
|
||||
Job/Subjob/Run 的统一持久化抽象。当前每日和每周按既有 UTC 间隔递推。
|
||||
|
||||
## 3. 目标
|
||||
@@ -55,8 +59,8 @@ Job/Subjob/Run 的统一持久化抽象。当前每日和每周按既有 UTC 间
|
||||
- 支持单次、每日、每周、每月、工作日和受限 Cron。
|
||||
- 支持 Task 完成、失败、Conversation 完成等内部事件触发。
|
||||
- 创建时明确选择当前或新 Conversation。
|
||||
- 默认使用 Execute,并允许用户主动切换到 Ask。
|
||||
- 冻结 Project、Runtime、工作目录、工具、知识、记忆和审批范围。
|
||||
- 使用关联会话当前的工作模式;新会话使用普通项目默认值。
|
||||
- 在实际发送时解析上下文和执行配置,与手动发送共用执行流程。
|
||||
- 提供时区、错过执行、幂等、租约、重试、恢复、取消、预算和审计。
|
||||
- 让所有重复触发复用同一 Task 和 Conversation 关联。
|
||||
- 为一次触发建立清晰 Job/Run,而不是创建新的顶层 Task。
|
||||
@@ -93,18 +97,15 @@ Task Center 和 Conversation 操作都可以提供“新建定制任务”,但
|
||||
○ 新建会话
|
||||
为任务创建一条新会话,默认标题为任务名称
|
||||
|
||||
执行模式
|
||||
[ Execute ] [ Ask ]
|
||||
|
||||
运行频率
|
||||
[ 单次 ] [ 每日 ] [ 每周 ] [ 每月 ] [ 工作日 ] [ Cron ]
|
||||
|
||||
首次运行 [ 2026-08-21 ] [ 17:00 ]
|
||||
时区 [ Asia/Shanghai ▾ ]
|
||||
|
||||
执行范围
|
||||
GoodBuddy Desktop · OpenCode · 项目工作目录
|
||||
8 个工具可用 · 高风险操作需要审批 [编辑]
|
||||
关联项目
|
||||
GoodBuddy Desktop · 项目工作目录
|
||||
运行时使用关联会话的当前上下文和配置
|
||||
|
||||
[取消] [创建任务]
|
||||
```
|
||||
@@ -126,7 +127,7 @@ GoodBuddy Desktop · OpenCode · 项目工作目录
|
||||
```text
|
||||
✓ 为当前 Conversation 新增一个 Task
|
||||
✓ 在左侧会话列表显示“任务 3”
|
||||
✓ 默认以 Execute 模式运行
|
||||
✓ 使用关联会话运行时的工作模式
|
||||
✓ 每周五 17:00 自动执行此 Task
|
||||
✓ 文本结果写入当前 Conversation
|
||||
✓ 独立交付物保存到成果
|
||||
@@ -145,37 +146,30 @@ GoodBuddy Desktop · OpenCode · 项目工作目录
|
||||
|
||||
## 6. 工作模式、Runtime 与工具
|
||||
|
||||
### 6.1 默认 Execute
|
||||
### 6.1 会话工作模式
|
||||
|
||||
创建 Modal 默认选择 Execute:
|
||||
创建 Modal 不单独设置工作模式。关联已有会话时,使用该会话实际发送时的工作模式;
|
||||
新会话未单独设置时使用项目默认模式。Ask 在 Runtime 边界保持只读,Execute 使用普通
|
||||
会话的工具授权规则。
|
||||
|
||||
- Execute 可以调用当前 Runtime 与 Project 已启用、且被 Task 快照允许的工具。
|
||||
- Ask 保持 Runtime 边界只读,只能调用允许的只读能力。
|
||||
- 所选 Runtime 不支持工具执行时,不能静默降级为 Ask;用户必须更换 Runtime 或主动选择
|
||||
Ask。
|
||||
- Modal 持续显示实际 Runtime、Project、工作目录和权限摘要。
|
||||
### 6.2 执行时配置
|
||||
|
||||
### 6.2 权限快照
|
||||
计划只保存任务要求、时间和稳定会话关联。Runtime、工作目录、Skills、MCP、知识范围、
|
||||
记忆和工具配置由普通发送流程在运行时解析,不另存任务上下文快照。已有会话的后续消息
|
||||
参与下一次执行;新会话不继承来源会话历史。重复触发始终发送到同一关联会话。
|
||||
|
||||
Task 创建时冻结:
|
||||
已保存计划中的旧工作模式和 Runtime 字段不再控制执行;用户在关联会话或项目中调整配置。
|
||||
|
||||
- Project 和工作目录。
|
||||
- Runtime 与模型选择。
|
||||
- 工作模式。
|
||||
- Skills、MCP、知识库、记忆和上下文范围。
|
||||
- 可用工具与审批策略。
|
||||
- 预算、并发和输出限制。
|
||||
|
||||
后续设置变化不修改已启动 Run。编辑 Task 配置只影响后续 Job。
|
||||
会话任务详情中的模式与执行来源保持一致:从未运行的计划显示关联会话当前有效模式;
|
||||
已开始或已结束的 Task 显示该次执行记录的实际模式,不被计划旧字段或会话后续切换覆盖。
|
||||
缺少实际模式记录时明确显示不可用,不从旧计划字段推断。
|
||||
|
||||
### 6.3 审批
|
||||
|
||||
- Execute 继续遵守当前 Runtime、GoodBuddy 原生能力和工具审批控制。
|
||||
- 已启用且按现有策略允许自动执行的工具可以在后台运行。
|
||||
- 需要额外确认的动作进入 `waiting_approval`,暂停所属 Job 并发送应用内及桌面通知。
|
||||
- 用户批准后继续同一个 Job/Run;拒绝后按协议失败、跳过或请求调整。
|
||||
- 定时触发不能把高风险、越界或未授权动作转换成自动批准。
|
||||
- 结果未知的外部副作用进入 `outcome_unknown`,不得自动重试。
|
||||
- 使用普通会话的授权和提问流程。Agent Runtime 的 Execute 工具调用无需通用 GoodBuddy 审批,工具活动仍显示在会话中。
|
||||
- GoodBuddy 配置修改等原生确认保持各自的确认流程。需要用户回答的问题显示在关联会话中,不因定时触发直接判为失败。
|
||||
- Ask 在 Runtime 边界保持只读;计划不增加另一层工具限制。
|
||||
- `outcome_unknown` 等高级执行治理尚未实现,见第 2 节的范围说明。
|
||||
|
||||
## 7. 触发器
|
||||
|
||||
@@ -225,7 +219,10 @@ type TimeTrigger =
|
||||
### 7.3 手动触发
|
||||
|
||||
“立即运行”在当前 Task 内创建独立 Job 和 Run,不改变下一次计划时间,不创建新 Task。
|
||||
重复点击使用调用级幂等键去重。
|
||||
提交期间,以及该计划已有排队或运行中的 occurrence 时,“立即运行”不可重复点击。
|
||||
后台同样拒绝为已有活动 occurrence 的计划再次入队。
|
||||
任务状态随队列变化刷新:待执行显示 queued,认领执行后显示 running;结束或取消排队后
|
||||
重新读取当前状态。没有活动 occurrence 的周期计划显示 idle 或 paused。
|
||||
|
||||
### 7.4 与普通消息的顺序
|
||||
|
||||
@@ -258,16 +255,16 @@ Conversation
|
||||
```text
|
||||
▾ 产品发布讨论 10:24
|
||||
|
||||
每周进度总结 每周 · Execute · 等待中 ●
|
||||
发布前检查 单次 · Execute ✓
|
||||
每周进度总结 每周 · 等待中 ●
|
||||
发布前检查 单次 ✓
|
||||
```
|
||||
|
||||
- 父会话行不重复显示任务标签或数量;Task 子项名称在左,模式、计划和状态标记收在右侧,
|
||||
- 父会话行不重复显示任务标签或数量;Task 子项名称在左,计划和状态标记收在右侧,
|
||||
与父会话的更新时间位置一致。
|
||||
- 点击 Conversation 标题打开聊天;点击 Task 子项打开同一 Conversation 并定位到该 Task。
|
||||
- 新建 Task 成功后首次自动展开。用户手动折叠后持久化其选择,后台运行不强制展开。
|
||||
- 默认最多直接显示 3 个 Task;“查看全部 N 个任务”打开该 Conversation 的完整 Task 区。
|
||||
- Task 子项显示本地化的模式、计划和非完成态状态文字;状态标记固定在行尾。已完成使用
|
||||
- Task 子项显示本地化的计划和非完成态状态文字;状态标记固定在行尾。已完成使用
|
||||
带勾成功点,并将完成文案保留给辅助技术,避免在紧凑元数据中重复显示且不只靠颜色表达。
|
||||
- 左侧只展开 Task;当前产品 UI 的其他区域也不提供 Job/Run 树或独立导航。
|
||||
|
||||
@@ -282,19 +279,19 @@ Conversation
|
||||
|
||||
选中 Task 后显示:
|
||||
|
||||
- 名称、状态和模式。
|
||||
- 名称和状态。
|
||||
- 计划、下次执行和未来预览。
|
||||
- 最近一次执行结果。
|
||||
- “立即运行”“暂停”“编辑计划”等操作。
|
||||
- 需要审批时的明确恢复入口。
|
||||
|
||||
每条自动结果消息显示 Task 名称、触发来源和时间。普通文本作为消息保存;文件、图片、PDF 和
|
||||
其他独立交付物保存为 Artifact,并从消息引用。多个 Task 并发时,最终文本以完整消息写入,
|
||||
不能把流式 Token 无序混入同一消息时间线。
|
||||
待发送项显示计划来源。发送后使用普通用户消息和流式助手消息;文件、图片、PDF 和其他
|
||||
独立交付物保存为 Artifact,并从消息引用。同一会话按队列串行执行,避免流式输出混入
|
||||
另一条回复。
|
||||
|
||||
## 11. Task Center
|
||||
|
||||
Task Center 显示 Scheduled Task 的范围、关联 Conversation、状态、模式、最近进展、需要
|
||||
Task Center 显示 Scheduled Task 的范围、关联 Conversation、状态、最近进展、需要
|
||||
关注和下次触发时间:
|
||||
|
||||
- 点击条目打开关联 Conversation,并定位到该 Task。
|
||||
@@ -352,9 +349,9 @@ Task Center 显示 Scheduled Task 的范围、关联 Conversation、状态、模
|
||||
- [x] 创建 Scheduled Task 可以选择当前或新 Conversation。
|
||||
- [x] 关联当前 Conversation 不修改其标题、类型或既有消息。
|
||||
- [x] 一条 Conversation 可以在左侧展开一个或多个 Task。
|
||||
- [x] 默认工作模式为 Execute,且用户可以主动选择 Ask。
|
||||
- [ ] Execute 能调用快照允许的工具,但不能绕过 Runtime 和审批控制。
|
||||
- [x] 不支持工具的 Runtime 不会让 Execute 静默降级。
|
||||
- [x] 工作模式使用关联会话当前设置,未设置时使用项目默认值。
|
||||
- [x] 使用普通发送的 Runtime 和工具执行流程,不保存任务上下文快照。
|
||||
- [x] 新会话不复制来源会话历史;重复执行读取关联会话最新消息。
|
||||
- [x] 重复触发始终复用同一 Task 和 Conversation 关联。
|
||||
- [x] 每次触发创建内部运行记录,不创建新的顶层 Task。
|
||||
- [x] 内部运行记录只用于执行和审计,不在 UI 中显示为独立层级。
|
||||
|
||||
@@ -4,20 +4,24 @@
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 状态 | Scheduled Task 首期已实现;Goal/Event Task 与完整操作待实施 |
|
||||
| 版本 | 0.3 |
|
||||
| 日期 | 2026-08-19 |
|
||||
| 状态 | Scheduled Task 与项目范围跟随已实现;Goal/Event Task 与完整操作待实施 |
|
||||
| 版本 | 0.4 |
|
||||
| 日期 | 2026-09-10 |
|
||||
| 依赖 | [Task 与 Job 统一领域模型](./task-and-job-model.md) |
|
||||
| 界面归属 | [通用助手工作栏与执行空间](../assistant-workbar/prd.md) |
|
||||
|
||||
## 1. 产品定义
|
||||
|
||||
Task Center 是所有产品级 Task 的应用级单例索引,不是 Automation Center,也不复制
|
||||
Conversation 内容。点击条目打开其关联 Conversation,并定位或展开对应 Task。
|
||||
Task Center 是工作栏中的单实例 Task 索引,默认跟随当前项目。它不是 Automation Center,
|
||||
也不复制 Conversation 内容。点击条目打开其关联 Conversation,并定位或展开对应 Task。
|
||||
|
||||
每个 Task 只关联一条 Conversation;一条 Conversation 可以关联零个、一个或多个 Task。
|
||||
Conversation 不因为关联 Task 而改变对象类型。
|
||||
|
||||
“单实例”和“项目范围”是两个独立属性:应用内只有一个 Task Center Tab,但该 Tab 默认展示
|
||||
当前项目的 Task。用户可以在同一 Tab 内切换到“全局任务”或“全部项目”,不能为每个项目创建
|
||||
独立 Task Center 实例。
|
||||
|
||||
## 2. 收录边界
|
||||
|
||||
收录:
|
||||
@@ -53,7 +57,19 @@ Job/Subjob/Run 树、独立详情或路由。
|
||||
|
||||
Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run。
|
||||
|
||||
## 4. 交互
|
||||
## 4. 范围
|
||||
|
||||
范围控件提供:
|
||||
|
||||
- `当前项目`:默认值,跟随活动项目变化,只显示 `projectId` 匹配的 Task。
|
||||
- `全局任务`:只显示没有 `projectId` 的 Task。
|
||||
- `全部项目`:显示当前用户可见的项目 Task 和全局 Task,并保留每行的项目或 Global 标识。
|
||||
|
||||
没有活动项目时,“当前项目”显示明确空状态,并允许切换到“全局任务”或“全部项目”;系统不
|
||||
自动改变用户已经选择的范围。范围模式作为工作栏公开偏好持久化,但“当前项目”模式不保存
|
||||
具体项目 ID;重启后继续跟随届时的活动项目。
|
||||
|
||||
## 5. 交互
|
||||
|
||||
- 点击条目打开关联 Conversation,并定位到该 Task。
|
||||
- 支持按需要关注、进行中、已暂停、已结束筛选。
|
||||
@@ -63,7 +79,7 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
||||
- 完整消息留在 Conversation;工具、审批和错误可以在活动或 Runtime 中按 Task 查看;
|
||||
独立交付物在成果中查看。
|
||||
|
||||
## 5. 左侧 Conversation Task 列表
|
||||
## 6. 左侧 Conversation Task 列表
|
||||
|
||||
左侧最近会话列表承担轻量 Task 发现,不替代 Task Center:
|
||||
|
||||
@@ -88,7 +104,7 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
||||
- 后台状态变化不强制展开;Task 状态持续显示在展开后的子项和 Task Center 中。
|
||||
- 默认最多显示 3 个 Task;“查看全部 N 个任务”打开该 Conversation 的完整任务区。
|
||||
|
||||
## 6. Conversation 任务区
|
||||
## 7. Conversation 任务区
|
||||
|
||||
包含 Task 的 Conversation 顶部显示可折叠任务区:
|
||||
|
||||
@@ -100,14 +116,17 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
||||
选择 Task 后显示名称、模式、聚合状态、计划、下次执行、最近结果和 Task 级操作。工具、审批、
|
||||
错误和成果通过 Task 关联显示,但不暴露 Job/Run 层级。
|
||||
|
||||
## 7. 删除关系
|
||||
模式来源遵循[Scheduled Task 的执行时配置](./scheduled-task-prd.md#62-执行时配置):
|
||||
未运行计划跟随会话当前有效模式,已执行 Task 保留实际模式;计划旧值不得覆盖展示。
|
||||
|
||||
## 8. 删除关系
|
||||
|
||||
- 删除 Schedule 只停止后续触发,不删除 Task、Conversation 或历史。
|
||||
- 删除 Task 停止其计划并移除关联,默认保留 Conversation 和既有消息。
|
||||
- 删除最后一个 Task 后,左侧 Conversation 的展开按钮消失。
|
||||
- 删除 Conversation 前必须显示关联 Task 数量,并先停止或结算活动执行。
|
||||
|
||||
## 8. 验收标准
|
||||
## 9. 验收标准
|
||||
|
||||
- [x] Task Center 只展示产品级 Task。
|
||||
- [x] 一条 Conversation 可以关联并展开多个 Task。
|
||||
@@ -118,3 +137,6 @@ Task 行只显示聚合后的用户状态,不要求用户理解内部 Job/Run
|
||||
- [x] 普通模型请求和工具调用不会误显示为 Task。
|
||||
- [ ] 删除 Task 默认保留 Conversation 和既有消息。
|
||||
- [x] Smart Heartbeat 不进入 Task Center。
|
||||
- [x] Task Center 默认只显示当前项目的 Task,切换项目后在同一 Tab 更新列表。
|
||||
- [x] “全局任务”只显示无项目 Task,“全部项目”显示所有可见 Task 并标注所属范围。
|
||||
- [x] 范围切换不创建第二个 Task Center Tab;任务中心保持默认存在且不可关闭。
|
||||
|
||||
@@ -52,7 +52,7 @@ GoodBuddy 应能够:
|
||||
Task 时不保留空的 Task 上下文栏。
|
||||
- 右侧栏在对话、知识库和活动视图之间保持状态。
|
||||
- 知识图谱实体详情复用同一右栏容器,不再维护独立布局。
|
||||
- 八个标准能力固定可达;能力、连接和数据状态可以变化,但应用不按当前上下文自动隐藏入口。
|
||||
- 九个标准能力固定可达;能力、连接和数据状态可以变化,但应用不按当前上下文自动隐藏入口。
|
||||
- Task Center 是 Task 的单例索引;其他可绑定目标的能力支持跟随当前上下文或固定到用户选择的目标。
|
||||
|
||||
### 3.2 右侧工作栏
|
||||
@@ -169,8 +169,8 @@ GoodBuddy 应能够:
|
||||
- 当前智能心跳继续提供周期回顾、报告、记忆建议、行动建议和运行历史。
|
||||
- “智能心跳”菜单入口负责完整配置,并支持 Global 或指定一个、多个 Project。
|
||||
- 任务中心和设置中心不复制智能心跳 CRUD。
|
||||
- 智能心跳长期方向是“未来分区记忆”,但数据结构、唤起模型、生命周期、页面以及与任务和
|
||||
长期记忆的关系尚未设计,不能提前实现。
|
||||
- 智能心跳长期按分区回顾信息并提出记忆候选或唤起建议,但协同规则、唤起条件、生命周期、
|
||||
页面以及与任务的关系尚未设计,不能提前实现平行的记忆类型或页面。
|
||||
|
||||
### 4.6 成果和预览
|
||||
|
||||
@@ -327,9 +327,9 @@ GoodBuddy 应能够:
|
||||
### 8.1 右侧栏
|
||||
|
||||
- 三种窗口宽度下布局可用。
|
||||
- 八个标准能力在主要视图中固定可达,应用不按能力自动隐藏。
|
||||
- 九个标准能力在主要视图中固定可达,应用不按能力自动隐藏。
|
||||
- 跨主视图切换保持栏目、折叠、跟随和固定目标状态。
|
||||
- 任务中心、监督、Runtime、终端、进程、工作区、浏览器和成果更新不要求离开当前主任务。
|
||||
- 任务中心、监督、Runtime、终端、进程、工作区、浏览器、资源监控和成果更新不要求离开当前主任务。
|
||||
- 键盘可操作,并具备正确 ARIA 标签。
|
||||
|
||||
### 8.2 Projects
|
||||
@@ -355,7 +355,7 @@ GoodBuddy 应能够:
|
||||
- 用户删除后不再检索到。
|
||||
- 每条记忆显示来源与作用域。
|
||||
- 智能心跳配置明确属于 Global 或指定 Project,当前报告和建议继续沿用已有生命周期。
|
||||
- 未来分区记忆完成独立设计前,不新增相关数据、页面或任务转换。
|
||||
- 分区记忆协同和唤起条件完成设计前,不新增相关数据、页面或任务转换。
|
||||
|
||||
### 8.5 安全
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export default defineConfig({
|
||||
'@deepseek-ai/dsh-sandbox',
|
||||
'@deepseek-ai/dsh-sandbox-policy',
|
||||
'@deepseek-ai/dsh-session',
|
||||
'@deepseek-ai/dsh-session-projection',
|
||||
'@deepseek-ai/dsh-shell-env',
|
||||
'@deepseek-ai/dsh-skill',
|
||||
'@deepseek-ai/dsh-subprocess-local',
|
||||
|
||||
+9
-1
@@ -12,11 +12,19 @@ export default tseslint.config(
|
||||
'coverage/**',
|
||||
'dist/**',
|
||||
'node_modules/**',
|
||||
'out/**'
|
||||
'out/**',
|
||||
'shareserver/**'
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'src/shared/**/*.ts'],
|
||||
languageOptions: {
|
||||
|
||||
Generated
+1149
-666
File diff suppressed because it is too large
Load Diff
+45
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "goodbuddy",
|
||||
"version": "0.12.4",
|
||||
"version": "0.12.11",
|
||||
"private": true,
|
||||
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
|
||||
"desktopName": "GoodBuddy",
|
||||
@@ -124,13 +124,26 @@
|
||||
"to": "runtimes/opencode",
|
||||
"filter": [
|
||||
"opencode",
|
||||
"opencode.exe"
|
||||
"opencode.exe",
|
||||
"rg",
|
||||
"rg.exe"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": ".runtime-resources",
|
||||
"to": "runtimes",
|
||||
"filter": [
|
||||
"opencode-config{,/**/*}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": "node_modules/opencode-ai/LICENSE",
|
||||
"to": "licenses/opencode-ai-LICENSE"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/@vscode/ripgrep/LICENSE",
|
||||
"to": "licenses/vscode-ripgrep-MIT.txt"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/@deepseek-ai/dsh-agent/LICENSE",
|
||||
"to": "licenses/deepseek-harness-MIT.txt"
|
||||
@@ -263,33 +276,36 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.25.1",
|
||||
"@deepseek-ai/cordis": "4.0.1",
|
||||
"@deepseek-ai/dsh-agent": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-agent-loop": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-bash-local": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-fs-local": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-pwsh-local": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-sandbox": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-session": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-shell-env": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-skill": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-token-meter": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-tool-bash": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-tool-fs": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-tool-pwsh": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-tool-skill": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-tools": "0.1.0-rc.8",
|
||||
"@deepseek-ai/dsh-user-approval": "0.1.0-rc.8",
|
||||
"@deepseek-ai/cordis": "4.0.2",
|
||||
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-agent-loop": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-bash-local": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-fs-local": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-pwsh-local": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-sandbox": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-shell-env": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-skill": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-subprocess-local": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-tool-bash": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-tool-fs": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-tool-pwsh": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-tool-skill": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
|
||||
"@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
|
||||
"@huggingface/tokenizers": "^0.1.3",
|
||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||
"@napi-rs/canvas": "1.0.3",
|
||||
"@opencode-ai/sdk": "^1.18.9",
|
||||
"@opencode-ai/plugin": "1.18.29",
|
||||
"@opencode-ai/sdk": "1.18.29",
|
||||
"@vscode/ripgrep": "1.18.0",
|
||||
"@wecom/aibot-node-sdk": "^1.0.6",
|
||||
"@xterm/addon-fit": "0.11.0",
|
||||
"@xterm/addon-search": "0.16.0",
|
||||
@@ -350,7 +366,7 @@
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.8.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"opencode-ai": "1.18.9",
|
||||
"opencode-ai": "1.18.29",
|
||||
"png-to-ico": "^3.0.2",
|
||||
"pngjs": "^7.0.0",
|
||||
"tar": "7.5.22",
|
||||
@@ -368,7 +384,7 @@
|
||||
"@koromix/koffi-win32-x64": "3.1.4"
|
||||
},
|
||||
"allowScripts": {
|
||||
"@deepseek-ai/dsh-subprocess-local@0.1.0-rc.8": true,
|
||||
"@deepseek-ai/dsh-subprocess-local@0.1.2-rc.1": true,
|
||||
"@google/genai@1.52.0": true,
|
||||
"cpu-features@0.0.10": true,
|
||||
"electron-winstaller@5.4.0": true,
|
||||
@@ -377,7 +393,7 @@
|
||||
"fsevents@2.3.3": true,
|
||||
"koffi@3.1.4": true,
|
||||
"node-pty@1.2.0-beta.15": true,
|
||||
"opencode-ai@1.18.9": true,
|
||||
"opencode-ai@1.18.29": true,
|
||||
"protobufjs@7.6.5": true,
|
||||
"ssh2@1.17.0": true
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"runtimes": {
|
||||
"opencode": {
|
||||
"version": "1.18.9",
|
||||
"version": "1.18.29",
|
||||
"provider": "opencode",
|
||||
"entrypoint": "bin/opencode",
|
||||
"entrypointIdentity": "opencode-acp",
|
||||
@@ -27,11 +27,15 @@
|
||||
"targets": {
|
||||
"x64": {
|
||||
"package": "opencode-linux-x64-baseline",
|
||||
"integrity": "sha512-x4KiJk9EF7ktM18Ru5Jue4kTntxMvlhWb7tHniQGGRvY2KeoK1iIkyAFd7ri5H/fSkM22hNv/Gg1Jk6/h9IlxQ=="
|
||||
"integrity": "sha512-AnPvoVjeLC6qBYSeDwmj8fIVeJxspkrh1xAPow5NMSdH7IZZd0auiI2PHIW+0/3B+xfar1Zx1a6yvDd4Wd74Wg=="
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"package": "opencode-darwin-arm64",
|
||||
"integrity": "sha512-EU0qma5GPJXcDp5rENvedSfqJjqxatQW/Qzjs71pR2YdbrSh7YmBwtvnIb2/KIvfQr0DErX4ST14sbBQI2mQdg=="
|
||||
},
|
||||
"arm64": {
|
||||
"package": "opencode-linux-arm64",
|
||||
"integrity": "sha512-2IN4lLjhx2FICcMDnBsKgwrey0AvAM0SlNzzj7L71uakNxWvrhcqPYVpEhrEYUjIn+uQGMY5PjA+uupXigJE2A=="
|
||||
"integrity": "sha512-Lr7XXik5wPJZBV5RW+aR6Uogf1n5GogpB565m+D/jiO/vRXr9LhvCpixgAr1tiwuH1ZfMsqOOlbFQz5jgH3Tqg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# GoodBuddy Agent 0.11.23 更新内容
|
||||
|
||||
## 功能更新
|
||||
|
||||
- **远程文件与 Git 管理**:远程项目可通过桌面工作区管理文件和目录,并操作分支、提交历史和单文件差异。操作使用所选 SSH 账号权限,删除须确认,分支冲突不会自动丢弃修改。
|
||||
|
||||
## 问题修复
|
||||
|
||||
- **修复长任务被固定限制中断**:移除随包 Runtime 的固定十分钟 Prompt 时限,并正确处理未额外设置模型超时或输出上限的请求。较大的模型请求和响应可通过更新后的桥接传输,取消、显式期限和传输边界仍然有效。
|
||||
- **不限请求时长时仍可连接**:修复无限请求时长被误用为零连接超时的问题。连接建立仍有独立超时,不会因此立即失败。
|
||||
- **更准确的失败分类**:模型网关区分取消、超时和响应过大等失败原因。保留原有调用记录和响应交付语义,不自动重放结果不确定的模型请求。
|
||||
|
||||
## 升级说明
|
||||
|
||||
- 需要 **Desktop 0.12.11 或更高版本**。提供 Linux x64、Linux arm64 和 macOS arm64 复合包,包含 Agent、固定 Node 和适配的 OpenCode;旧的运行中进程不会热更新。
|
||||
|
||||
---
|
||||
|
||||
# What's New in GoodBuddy Agent 0.11.23
|
||||
|
||||
## Features
|
||||
|
||||
- **Remote file and Git management**: Manage remote files and directories, branches, commit history, and per-file diffs from the desktop workspace. Operations use the selected SSH account’s permissions, deletion requires confirmation, and branch conflicts never automatically discard changes.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **Fix long tasks interrupted by fixed limits**: Removed the bundled Runtime’s fixed ten-minute Prompt deadline and corrected handling of requests without additional model timeouts or output caps. Updated bridges handle larger model requests and responses while retaining cancellation, explicit deadlines, and transport bounds.
|
||||
- **Connections work with unlimited request duration**: Fixed unlimited request duration being misinterpreted as a zero connection timeout. Connection establishment retains a separate timeout instead of failing immediately.
|
||||
- **More accurate failure classification**: The model gateway distinguishes cancellation, timeout, and oversized responses. Existing call records and response-delivery semantics remain intact, without automatically replaying model requests whose outcome is uncertain.
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
- Requires **Desktop 0.12.11 or later**. Compound packages target Linux x64, Linux arm64, and macOS arm64 and include the Agent, pinned Node, and adapted OpenCode; existing running processes are not hot-updated.
|
||||
@@ -1,6 +1,280 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"releases": [
|
||||
{
|
||||
"version": "0.12.11",
|
||||
"releasedAt": "2026-09-10",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**工作栏管理项目文件与 Git**:可创建、重命名、移动和删除文件或目录,并查看属性、分支、提交历史和单文件差异。文件与 Git 工具按视图分组,分支搜索统一样式,子目录保留文字导航。",
|
||||
"**多个独立浏览器页签**:可同时打开多个浏览器页签,分别保留页面、导航和交互状态,并支持全屏显示。同一对话的页签共享登录状态,模型请求固定使用开始时绑定的页签。",
|
||||
"**直连模型开发工具**:可使用随包 ripgrep 搜索项目文件与内容,并分页读取大型文本。Execute 可应用多文件补丁,Ask 仍保持只读。",
|
||||
"**任务中心范围切换**:任务中心可查看当前项目、全局任务或全部项目。所选范围随工作栏布局保存,任务中心和工作区保留为基础页签。"
|
||||
],
|
||||
"fixes": [
|
||||
"**长上下文与工具输出不再过早截断**:移除历史消息、长回复和文档解析文本的旧固定截断限制,并支持分页续读命令与 Subagent 输出。所选模型的上下文窗口仍然有效。",
|
||||
"**定时消息沿用会话设置**:定时消息使用会话当前历史、Runtime、工作模式和知识检索设置。排队、运行和实际工作模式及时显示,同一计划尚未完成时不能重复“立即运行”。",
|
||||
"**模型选择与用量保持一致**:未固定的连接继续跟随当前兼容默认值,显式选择保持固定。手动压缩、远程会话用量和 Harness 来源显示与实际配置保持一致。",
|
||||
"**浏览器显示与恢复更可靠**:修复 Agent 页面与工作栏页签对应、已释放页面恢复以及跨会话选择的问题。未使用浏览器时不创建后台页面,普通操作不再触发无用的自动截图。",
|
||||
"**文件操作与工具调用更顺畅**:修复目录和提交历史刷新、仅修改文件名大小写,以及重命名文件的提交差异。健康的 MCP 调用复用已发现工具,减少重复发现带来的等待。"
|
||||
],
|
||||
"notices": [
|
||||
"远程新增文件/Git 操作及长任务修复需要 **Agent 0.11.23**。请先升级 Desktop 至 **0.12.11**,再下载并更新 Host 上的 Agent。",
|
||||
"删除操作经确认后永久执行,移动限于工作区内,分支切换不会自动暂存或丢弃冲突改动。Ask 保持只读,默认 Execute 自动授权已启用工具;本次不构建 LoongArch 预览。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Manage project files and Git from the workbar**: Create, rename, move, and delete files or directories, and inspect properties, branches, commit history, and per-file diffs. File and Git actions are grouped by view, branch search uses consistent styling, and subdirectories retain text breadcrumbs.",
|
||||
"**Multiple independent browser tabs**: Open several browser tabs with separate pages, navigation, and interaction state, including fullscreen display. Tabs in one conversation share login state, while each model request retains its starting tab binding.",
|
||||
"**Direct-model development tools**: Use packaged ripgrep to search project files and content, and read large text files in pages. Execute can apply multi-file patches, while Ask remains read-only.",
|
||||
"**Task Center scopes**: View tasks for the current project, global tasks, or all projects. The selected scope is saved with the workbar layout, while Task Center and Workspace remain permanent tabs."
|
||||
],
|
||||
"fixes": [
|
||||
"**Prevent premature context and tool-output truncation**: Removed previous fixed truncation limits on message history, long replies, and parsed documents, with paged access to command and Subagent output. The selected model’s context window still applies.",
|
||||
"**Scheduled messages retain conversation settings**: Scheduled messages use the conversation’s current history, Runtime, work mode, and knowledge retrieval settings. Queue state, execution state, and actual work mode stay current, and Run Now cannot duplicate an unfinished occurrence.",
|
||||
"**Consistent model selection and usage**: Unpinned connections follow the compatible current default, while explicit selections remain fixed. Manual compaction, remote conversation usage, and the displayed Harness source reflect the actual configuration.",
|
||||
"**More reliable browser display and recovery**: Fixed Agent-to-workbar tab mapping, released-page recovery, and explicit cross-conversation selection. Unused browser capabilities do not create background pages, and ordinary operations no longer trigger unused automatic screenshots.",
|
||||
"**Smoother file operations and tool calls**: Fixed directory and commit-history refresh, case-only filename changes, and commit diffs for renamed files. Healthy MCP calls reuse discovered tools to reduce repeated discovery delays."
|
||||
],
|
||||
"notices": [
|
||||
"New remote file/Git operations and long-task fixes require **Agent 0.11.23**. Upgrade Desktop to **0.12.11** before downloading and updating the Host’s Agent.",
|
||||
"Confirmed deletion is permanent, moves stay inside the workspace, and branch switching never automatically stashes or discards conflicts. Ask remains read-only, Execute automatically authorizes enabled tools by default, and this release excludes the LoongArch preview."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.10",
|
||||
"releasedAt": "2026-09-09",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**OpenCode 并行执行更顺畅**:修复批量改写文件或转换换行符后,原生自动 Git 快照计算可能拖住其他会话的问题。GoodBuddy 托管的本机与远端 OpenCode 不再生成界面未使用的自动快照,文件工具、子代理和工作区 Git 差异仍然可用。",
|
||||
"**Continue 输出不再堵住宿主**:修复控制台输出较多时可能填满子进程管道、使请求无法继续的问题。现在会持续排空不用展示的输出,正常回复、工具进度和取消仍沿用原有通道。",
|
||||
"**聊天内容与输入框对齐**:头像移到消息正文外侧,消息边缘与输入框保持一致。滚动条空间留在外侧,避免聊天区域出现多余的横向滚动。",
|
||||
"**待办来源就在详情里**:魔法笔记把来源记录放入所选待办的详情区域,查看事项和原文不必再找底部面板。切换待办时不会混入上一项的来源内容,读取失败通过应用通知提示。",
|
||||
"**便携包构建**:修复 Windows 打包准备 Runtime 时依赖特定 npm 环境变量的问题。构建流程现在也能使用项目内 npm 或系统 npm,避免因此中断便携包构建。"
|
||||
],
|
||||
"notices": [
|
||||
"本机修复随升级并重启 Desktop 生效;远程项目请先升级 Desktop 至 0.12.10,再更新 Agent 至 0.11.22。已运行的旧 Runtime 不会热更新,外部 OpenCode Server 的配置不受此调整影响。",
|
||||
"本次使用 Windows、macOS、Linux 的 x64/arm64 六平台桌面发布矩阵,不构建 LoongArch 实验预览。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**Smoother parallel OpenCode execution**: Fixed automatic native Git snapshot calculations that could stall other conversations after broad file rewrites or line-ending conversions. GoodBuddy-managed local and remote OpenCode no longer generate snapshots unused by the interface; file tools, subagents, and workspace Git diffs remain available.",
|
||||
"**Continue output no longer blocks its host**: Fixed larger console output filling a child-process pipe and preventing requests from progressing. Unused output is now continuously drained, while replies, tool progress, and cancellation keep using their existing channels.",
|
||||
"**Aligned messages and composer**: Avatars now sit outside message content, with message edges aligned to the composer. Scrollbar space stays in the outer gutter to avoid unnecessary horizontal scrolling in the chat area.",
|
||||
"**Todo sources inside their details**: Magic Notes now shows the source entry within the selected todo's detail area instead of a separate bottom panel. Switching todos cannot show the previous item's source content, and failed reads use application notifications.",
|
||||
"**Portable package builds**: Fixed Windows Runtime preparation depending on a particular npm environment variable. Builds can now also use the project's bundled npm or the system npm instead of failing portable packaging for that reason."
|
||||
],
|
||||
"notices": [
|
||||
"Local fixes take effect after upgrading and restarting Desktop. For remote projects, upgrade Desktop to 0.12.10 before updating Agent to 0.11.22. Already-running Runtimes are not hot-updated, and external OpenCode Server configuration is unchanged.",
|
||||
"This release uses the six standard Windows, macOS, and Linux x64/arm64 desktop targets, without a LoongArch experimental preview."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.9",
|
||||
"releasedAt": "2026-09-08",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**待办来源记录**:在魔法笔记的待办视图选择事项后,可直接查看对应的来源记录。遇到读取失败可以重试,不必来回切换查找笔记。"
|
||||
],
|
||||
"fixes": [
|
||||
"**消息状态更准确**:回复底部会区分准备请求、等待重试、开始重试、工具活动和等待回答,本机 OpenCode 还会显示原生重试次数与计划时间。没有新进展时不再用闪烁暗示模型仍在工作,会话列表闪点及发送、停止按钮保持不变。",
|
||||
"**并行 OpenCode 会话**:修复开启另一条本机会话时,已有会话可能一直等待的问题。服务异常退出时会明确提示失败,不会自动重复执行已运行的命令。",
|
||||
"**参考图编辑**:修复部分兼容图像服务商拒绝图片编辑请求的问题。使用参考图修改图片时,不再附带只适用于生成接口的默认参数。",
|
||||
"**原生问答与长上下文**:修复 OpenCode、Continue 的较长问题、较多选项或较长自定义回答被截断或拒绝的问题,并提高 Continue 的请求正文容量。用户可以继续通过现有问答卡片回答,较长上下文不再受旧的小容量限制阻断。",
|
||||
"**推理排版**:修复 OpenCode 流式推理片段拼接时丢失 Markdown 分段的问题。推理过程中的段落和换行会按原始内容保留。",
|
||||
"**运行记录提示**:移除常驻的旧版活动历史截断警告,减少与当前执行无关的提示。此调整不会恢复旧版本已经丢失的历史记录。"
|
||||
],
|
||||
"notices": []
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Todo source entries**: Select an item in the Magic Notes todo view to read its source entry directly. Failed reads can be retried without switching back and forth to find the original note."
|
||||
],
|
||||
"fixes": [
|
||||
"**More accurate message status**: The reply footer distinguishes request preparation, retry waiting, retry dispatch, tool activity, and pending answers; local OpenCode also reports native retry attempts and scheduled times. The footer no longer pulses to imply progress when none is reported, while conversation-list indicators and send/stop buttons remain unchanged.",
|
||||
"**Parallel OpenCode conversations**: Fixed an issue where opening another local conversation could leave an existing one waiting indefinitely. Unexpected server exits now produce an explicit failure without automatically replaying commands that already ran.",
|
||||
"**Reference-image editing**: Fixed editing requests rejected by some compatible image providers. Reference-image edits no longer include default parameters that belong only to the generation endpoint.",
|
||||
"**Native questions and longer context**: Fixed longer questions, larger option lists, and longer custom answers being truncated or rejected in OpenCode and Continue, and increased Continue's request-body capacity. Users can keep answering through the existing question card without longer context hitting the previous small request limit.",
|
||||
"**Reasoning formatting**: Fixed lost Markdown boundaries when streamed OpenCode reasoning fragments were joined. Paragraphs and line breaks now retain their original structure.",
|
||||
"**Run-history notices**: Removed the persistent legacy activity-history truncation warning to reduce notices unrelated to current work. This does not restore history already lost in older versions."
|
||||
],
|
||||
"notices": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.8",
|
||||
"releasedAt": "2026-09-08",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**修复 OpenCode 启动失败。** 安装包补齐离线配置依赖,避免因缺少 `@opencode-ai/plugin/package.json` 无法启动。升级后无需手动安装 Node.js 或 npm 依赖。",
|
||||
"**修复 DeepSeek Harness Host 启动退出。** 将必需的会话组件完整打入 Host,避免安装后提示“Host 启动前退出(code 1)”。现有模型配置保持不变。",
|
||||
"**macOS 仅提供 DMG。** Intel 与 Apple 芯片版本不再构建或发布 ZIP。Windows 和 Linux 的安装包格式保持不变。"
|
||||
],
|
||||
"notices": [
|
||||
"旧版 macOS 的 GitHub 更新检查,以及所有平台的旧版镜像更新检查可能无法识别新清单,请从官方下载页手动升级至 `0.12.8`。",
|
||||
"Agent 继续使用 `0.11.21`,本次不重发 Agent,也不构建 LoongArch 实验预览。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**Fixed OpenCode startup failures.** Desktop packages now include the offline configuration dependencies, avoiding startup failures caused by a missing `@opencode-ai/plugin/package.json`. No manual Node.js or npm dependency installation is needed after upgrading.",
|
||||
"**Fixed DeepSeek Harness Host startup exits.** Required session components are bundled into the Host, preventing the “Host exited before startup (code 1)” error after installation. Existing model settings remain unchanged.",
|
||||
"**DMG-only macOS releases.** Intel and Apple silicon versions no longer build or publish ZIP packages. Windows and Linux package formats are unchanged."
|
||||
],
|
||||
"notices": [
|
||||
"Older macOS GitHub update checks and older mirror update checks on all platforms may reject the new manifest. Upgrade manually to `0.12.8` from the official download page.",
|
||||
"Agent remains at `0.11.21`; this release does not republish the Agent or include a LoongArch preview."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.7",
|
||||
"releasedAt": "2026-09-08",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**参考图编辑。** 使用兼容的图像模型连接时,可以附加参考图片并描述修改要求。GoodBuddy 会发送图片编辑请求;服务商必须支持图片编辑接口。",
|
||||
"**专家协作沿用工作模式。** 选择专家或团队分析时,专家可以按当前 Ask/Execute 模式使用已启用的本机直连模型工具。Ask 仍只读;这些工具不是远程 OpenCode 子会话。"
|
||||
],
|
||||
"fixes": [
|
||||
"**Execute 不再卡在目录权限等待。** OpenCode、Continue 和 DeepSeek Harness 可以按当前账号权限访问工作区外目录。OpenCode 原生子代理的权限请求也会正确处理,Ask 保持只读。",
|
||||
"**原生问答正确回传。** 本机 OpenCode 和 Continue 的选项、yes/no、自由文本及跳过通过现有问答卡片处理。修复 OpenCode 子会话提问丢失,以及 Continue 自定义答案回传错误。",
|
||||
"**OpenCode 会话切换更稳定。** 切回会话时保留可用的原生操作栏,并在复用服务前检查其状态。并行请求的取消和临时 MCP 清理保持隔离。",
|
||||
"**Runtime 启动更可靠。** OpenCode 使用随包提供并按版本复用的离线配置依赖,减少重复准备。修复 DeepSeek Harness 升级后的初始化失败。",
|
||||
"**远程 Agent 安装与重连更可靠。** 修复包含 OpenCode 离线依赖的安装包被误判为元数据过大,以及部分合法握手被拒绝的问题。连接在握手期间关闭时会及时返回错误,不再一直等待。",
|
||||
"**聊天输入与布局更顺畅。** 输入时不再重复渲染未变化的聊天历史,消息正文统一显示在头像和名称下方。终端移除多余内边距,让内容使用完整面板空间。"
|
||||
],
|
||||
"notices": [
|
||||
"先升级至 Desktop `0.12.7`,再安装 Agent `0.11.21`,以获得远程修复。远程项目仍为技术预览;Agent 单独提供 Linux x64、Linux arm64 和 macOS arm64 包。",
|
||||
"OpenCode 更新至 `1.18.29`;已运行的旧 Runtime 不会热更新,需要升级并重启。结构化业务问题仍需用户回答,不会自动代答终端密码、OAuth 或任意确认提示。",
|
||||
"本次使用标准六平台桌面发布矩阵,不构建 LoongArch 实验预览。应用导航与系统工具入口文档仍为设计稿,不代表新界面已经交付。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Reference-image editing.** Attach reference images and describe the changes when using a compatible image-model connection. GoodBuddy sends an image-edit request; the provider must support the image-editing endpoint.",
|
||||
"**Experts inherit the work mode.** Selected experts and team analysis can use enabled local direct-model tools under the current Ask/Execute mode. Ask remains read-only; these tools are not remote OpenCode child sessions."
|
||||
],
|
||||
"fixes": [
|
||||
"**Execute no longer stalls on directory permissions.** OpenCode, Continue, and DeepSeek Harness can access paths outside the workspace using the current account’s permissions. OpenCode native subagent permission requests are also handled correctly, while Ask stays read-only.",
|
||||
"**Native questions return answers correctly.** Local OpenCode and Continue handle choices, yes/no, free text, and skipping through the existing question cards. This fixes lost OpenCode child-session questions and incorrect Continue custom-answer responses.",
|
||||
"**More stable OpenCode session switching.** Returning to a conversation preserves usable native controls, and cached services are checked before reuse. Cancellation and temporary MCP cleanup remain isolated between parallel requests.",
|
||||
"**More reliable Runtime startup.** OpenCode uses bundled offline configuration dependencies and reuses them by version, reducing repeated preparation. DeepSeek Harness initialization failures after upgrading are also fixed.",
|
||||
"**More reliable remote Agent installation and reconnection.** Packages containing OpenCode’s offline dependencies are no longer incorrectly rejected as oversized metadata, and valid handshakes are no longer intermittently rejected. Connections closed during the handshake now return an error promptly instead of waiting indefinitely.",
|
||||
"**Smoother typing and cleaner layouts.** Typing no longer rerenders unchanged chat history, and message content appears below the avatar and name. Removing extra terminal padding gives content the full panel area."
|
||||
],
|
||||
"notices": [
|
||||
"Upgrade to Desktop `0.12.7` before installing Agent `0.11.21` for the remote fixes. Remote Projects remain a technical preview; separate Agent packages target Linux x64, Linux arm64, and macOS arm64.",
|
||||
"OpenCode updates to `1.18.29`; running older Runtimes require an upgrade and restart rather than a live update. Structured business questions still require user answers; terminal passwords, OAuth, and arbitrary confirmation prompts are not answered automatically.",
|
||||
"This release uses the standard six-target desktop matrix without a LoongArch preview. Application navigation and system-tool entry documents remain design drafts, not delivered UI."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.6",
|
||||
"releasedAt": "2026-09-06",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**子代理过程与结果显示更完整。** 展开 OpenCode 子代理卡片,可以按顺序查看文字、推理和工具详情,最终结果单独展示。远程项目需要更新 Agent 才能查看实时过程;旧 Agent 仍可显示最终结果,但不会补造缺失的过程记录。",
|
||||
"**工作区文件变更更容易查看。** 点击变更文件,可分别查看已暂存和未暂存差异,包括删除、重命名和未跟踪文件。刷新会更新当前差异和已展开目录,超过 50 项的变更可以继续加载。",
|
||||
"**知识库支持整行勾选。** 点击知识库名称或所在行即可切换选择,不再因焦点变化导致点击无效。选择范围仍按当前对话保存。",
|
||||
"**OpenCode 连续与并行对话更稳定。** 每次请求结束后会释放自身的事件连接,避免连续使用后连接不断积累。取消一条请求不会关闭其他并行会话的连接。",
|
||||
"**命令可以使用工作区外目录。** 直连模型在 Execute 模式下,可以使用绝对路径、相对路径或符号链接指定工作目录。未指定时仍使用项目工作区,Ask 保持只读。",
|
||||
"**恢复 GitHub 来源的 Agent 更新检查。** 选择 GitHub 后,可以正常读取可用 Agent 版本,不再因历史发布信息过大提示“发布目录超出大小限制”。镜像入口和已有本地包不受影响。"
|
||||
],
|
||||
"notices": [
|
||||
"远程子代理实时过程和本轮远程文件差异改进需要更新至 Agent `0.11.20`。提供 Linux x64、Linux arm64 和 macOS arm64 包,Desktop 不内置 Agent。",
|
||||
"本次使用标准六平台桌面发布矩阵,不构建 LoongArch 实验预览。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [],
|
||||
"fixes": [
|
||||
"**More complete subagent progress and results.** Expand an OpenCode subagent card to view ordered text, reasoning, and tool details, with the final result shown separately. Remote projects require an updated Agent for live progress; older Agents still show final results without inventing missing progress history.",
|
||||
"**Clearer workspace file changes.** Select a changed file to inspect staged and unstaged diffs separately, including deleted, renamed, and untracked files. Refresh updates the selected diff and expanded directories, and lists with more than 50 changes can load additional entries.",
|
||||
"**Whole-row knowledge selection.** Click a knowledge library’s name or row to toggle its selection without focus changes losing the click. Selection remains scoped to the current conversation.",
|
||||
"**More reliable consecutive and parallel OpenCode conversations.** Each request releases its own event connection when it ends, preventing connections from accumulating during continued use. Cancelling one request does not close other parallel conversations’ connections.",
|
||||
"**Commands can use directories outside the workspace.** Direct models in Execute mode can select a working directory using an absolute path, a relative path, or a symbolic link. The project workspace remains the default, and Ask stays read-only.",
|
||||
"**Restored Agent update checks through GitHub.** Selecting GitHub now retrieves available Agent versions without failing because the release history exceeds the response-size limit. Mirror access and existing local packages are unaffected."
|
||||
],
|
||||
"notices": [
|
||||
"Update to Agent `0.11.20` for live remote subagent progress and the remote file-diff improvements in this release. Packages are available for Linux x64, Linux arm64, and macOS arm64; Desktop does not embed the Agent.",
|
||||
"This release uses the standard six-target desktop matrix and does not include an experimental LoongArch preview."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.5",
|
||||
"releasedAt": "2026-09-06",
|
||||
"notes": {
|
||||
"zh-CN": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Apple Silicon Mac 远程项目。** 可以把 macOS arm64 设备添加为 SSH Host,安装独立 Agent 后使用 Ask、Execute 和断线重连。无需管理员权限或 launchd 服务;Intel Mac 暂不支持,远程项目仍为默认关闭的技术预览。"
|
||||
],
|
||||
"fixes": [
|
||||
"**知识库使用流程更完整。** 创建和检索测试默认收起高级参数,资料可用后可直接“用于当前对话”;知识范围按对话保存,新对话默认不选择知识库。文档明确区分可用、处理中和失败,支持打开来源、失败重试,并在移除来源前说明影响和要求确认。",
|
||||
"**切换窗口不再误中断回复。** 来回切换桌面窗口时,仍在生成的回复不会被误判为“上次运行意外中断”,后续消息也不会因此滞留队列。OpenCode 专属操作栏在会话刷新时保持稳定,减少界面抖动。",
|
||||
"**长会话可以完整滚动。** 较长推理、工具记录或正文不再因估算高度导致滚动条提前到底。全部已加载内容可直接滚动查看,无需切换会话刷新。"
|
||||
],
|
||||
"notices": [
|
||||
"**先升级桌面端,再更新 Agent。** Agent `0.11.19` 提供 Linux x64、Linux arm64 和 macOS arm64 包,共用一个签名目录。旧桌面端无法解析包含 macOS 条目的新目录,需升级至 Desktop `0.12.5` 后检查在线 Agent 更新;已有本地包和历史发布仍保留。",
|
||||
"Desktop 不内置 Agent 或远程 Runtime。Ask 保持只读,Execute 使用所选 SSH 账号的完整权限。",
|
||||
"本次使用标准六平台桌面发布矩阵,不构建 LoongArch 实验预览。",
|
||||
"仓库新增独立 ShareServer 管理控制台交互原型,不包含在桌面安装包中,也不是已上线的团队共享服务;原型数据操作不持久化。"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"highlights": [],
|
||||
"features": [
|
||||
"**Remote projects on Apple Silicon Macs.** Add a macOS arm64 device as an SSH Host and install the separate Agent to use Ask, Execute, and reconnection. No administrator access or launchd service is required; Intel Macs are not supported, and Remote Projects remain an off-by-default technical preview."
|
||||
],
|
||||
"fixes": [
|
||||
"**A more complete knowledge workflow.** Creation and retrieval testing collapse advanced settings by default, and searchable libraries can be sent directly to the current conversation. Knowledge selection is saved per conversation and starts empty for new conversations; documents distinguish ready, processing, and failed states, with source opening, retry, and explicit removal confirmation.",
|
||||
"**Window switching no longer interrupts active replies.** Returning to the desktop window no longer mislabels an active reply as interrupted or leaves subsequent messages queued because of that false state. OpenCode-specific controls remain stable during conversation refreshes, reducing layout shifts.",
|
||||
"**Complete scrolling in long conversations.** Long reasoning, tool records, and replies no longer cause the scrollbar to end early because of estimated message heights. All loaded content remains reachable without switching conversations to refresh the layout."
|
||||
],
|
||||
"notices": [
|
||||
"**Upgrade Desktop before updating the Agent.** Agent `0.11.19` provides Linux x64, Linux arm64, and macOS arm64 packages through one signed catalog. Older Desktop versions cannot parse the new catalog containing macOS entries; upgrade to Desktop `0.12.5` before checking online Agent updates. Existing local packages and historical releases remain available.",
|
||||
"Desktop does not embed the Agent or Remote Runtime. Ask remains read-only; Execute uses the selected SSH account’s full permissions.",
|
||||
"This release uses the standard six-target desktop matrix and does not include an experimental LoongArch preview.",
|
||||
"The repository now includes a standalone interactive ShareServer console prototype. It is not bundled with Desktop or a deployed team-sharing service, and prototype data operations are not persisted."
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "0.12.4",
|
||||
"releasedAt": "2026-09-05",
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { performance } from 'node:perf_hooks'
|
||||
import process from 'node:process'
|
||||
import { URL } from 'node:url'
|
||||
import { TextDecoder } from 'node:util'
|
||||
|
||||
const fetch = globalThis.fetch
|
||||
const AbortSignal = globalThis.AbortSignal
|
||||
|
||||
const providers = ['dify', 'fastgpt', 'ragflow']
|
||||
const arguments_ = new Map(
|
||||
process.argv.slice(2).map((argument) => {
|
||||
const [key, ...value] = argument.replace(/^--/, '').split('=')
|
||||
return [key, value.join('=') || 'true']
|
||||
})
|
||||
)
|
||||
const selectedProvider = arguments_.get('provider') ?? 'all'
|
||||
const query = arguments_.get('query') ?? '知识库'
|
||||
const extended = arguments_.get('extended') === 'true'
|
||||
const outputPath = arguments_.get('output')
|
||||
|
||||
if (selectedProvider !== 'all' && !providers.includes(selectedProvider)) {
|
||||
throw new Error(`Unsupported provider: ${selectedProvider}`)
|
||||
}
|
||||
if (!query.trim() || query.length > 250) {
|
||||
throw new Error('Probe query must contain 1 to 250 characters')
|
||||
}
|
||||
|
||||
const asRecord = (value) =>
|
||||
value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
? value
|
||||
: undefined
|
||||
const objectKeys = (value) => Object.keys(asRecord(value) ?? {}).sort()
|
||||
const unionKeys = (values) => [...new Set(values.flatMap(objectKeys))].sort()
|
||||
const finiteNumbers = (values) => values.filter(Number.isFinite)
|
||||
|
||||
async function configurations(provider) {
|
||||
const prefix = `GOODBUDDY_${provider.toUpperCase()}`
|
||||
const environmentUrl = process.env[`${prefix}_BASE_URL`]?.trim()
|
||||
const environmentKey = process.env[`${prefix}_API_KEY`]?.trim()
|
||||
if (environmentUrl && environmentKey) {
|
||||
return [{ baseUrl: environmentUrl, apiKey: environmentKey }]
|
||||
}
|
||||
const file = resolve(`.env.${provider}`)
|
||||
const lines = (await readFile(file, 'utf8'))
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
if (lines.length === 2) {
|
||||
return [{ baseUrl: lines[0], apiKey: lines[1] }]
|
||||
}
|
||||
if (provider === 'dify' && lines.length % 3 === 0) {
|
||||
return Array.from({ length: lines.length / 3 }, (_, index) => {
|
||||
const label = lines[index * 3].replace(/:$/, '')
|
||||
const profile = /^dify[0-9.x-]+$/i.test(label)
|
||||
? label.replace(/^dify/i, '')
|
||||
: `profile-${index + 1}`
|
||||
return {
|
||||
profile,
|
||||
baseUrl: lines[index * 3 + 1],
|
||||
apiKey: lines[index * 3 + 2]
|
||||
}
|
||||
})
|
||||
}
|
||||
throw new Error(
|
||||
`${file} must contain one URL/key pair or labeled Dify URL/key triples`
|
||||
)
|
||||
}
|
||||
|
||||
function baseUrl(provider, input) {
|
||||
const url = new URL(input)
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
throw new Error(`${provider} URL must use HTTP or HTTPS`)
|
||||
}
|
||||
if (url.username || url.password || url.search || url.hash) {
|
||||
throw new Error(`${provider} URL cannot contain credentials, query, or fragment`)
|
||||
}
|
||||
url.pathname = url.pathname.replace(/\/+$/, '')
|
||||
if (provider === 'dify') url.pathname = url.pathname.replace(/\/v1$/, '')
|
||||
if (provider === 'fastgpt' && !url.pathname.endsWith('/api')) url.pathname += '/api'
|
||||
if (provider === 'ragflow') url.pathname = url.pathname.replace(/\/api\/v1$/, '')
|
||||
return url
|
||||
}
|
||||
|
||||
function endpoint(base, path) {
|
||||
const url = new URL(base)
|
||||
url.pathname = `${url.pathname}${path}`.replace(/\/{2,}/g, '/')
|
||||
return url
|
||||
}
|
||||
|
||||
async function boundedJson(response, maximumBytes = 5_000_000) {
|
||||
const bytes = new Uint8Array(await response.arrayBuffer())
|
||||
if (bytes.byteLength > maximumBytes) throw new Error('response-too-large')
|
||||
return JSON.parse(new TextDecoder().decode(bytes))
|
||||
}
|
||||
|
||||
async function request(context, name, url, init = {}) {
|
||||
const startedAt = performance.now()
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
redirect: 'error',
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.apiKey}`,
|
||||
Accept: 'application/json',
|
||||
...(init.body ? { 'Content-Type': 'application/json' } : {})
|
||||
}
|
||||
})
|
||||
const body = await boundedJson(response)
|
||||
context.operations.push({
|
||||
name,
|
||||
method: init.method ?? 'GET',
|
||||
path: url.pathname
|
||||
.replace(/[0-9a-f]{8}-[0-9a-f-]{27,}/gi, '{id}')
|
||||
.replace(/[0-9a-f]{24}/gi, '{id}'),
|
||||
status: response.status,
|
||||
durationMs: Math.round(performance.now() - startedAt),
|
||||
responseKeys: objectKeys(body)
|
||||
})
|
||||
return { response, body }
|
||||
} catch (error) {
|
||||
context.operations.push({
|
||||
name,
|
||||
method: init.method ?? 'GET',
|
||||
path: url.pathname
|
||||
.replace(/[0-9a-f]{8}-[0-9a-f-]{27,}/gi, '{id}')
|
||||
.replace(/[0-9a-f]{24}/gi, '{id}'),
|
||||
status: 'failed',
|
||||
durationMs: Math.round(performance.now() - startedAt),
|
||||
error: error?.name ?? 'Error'
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function resultShape(items, scoreKey) {
|
||||
const scores = finiteNumbers(items.map((item) => asRecord(item)?.[scoreKey]))
|
||||
return {
|
||||
count: items.length,
|
||||
itemKeys: unionKeys(items),
|
||||
contentPresent: items.some((item) => {
|
||||
const record = asRecord(item)
|
||||
return typeof record?.content === 'string' || typeof record?.q === 'string'
|
||||
}),
|
||||
scoreRange: scores.length > 0
|
||||
? { minimum: Math.min(...scores), maximum: Math.max(...scores) }
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function difyResultShape(records) {
|
||||
const segments = records.map((record) => asRecord(record)?.segment)
|
||||
const documents = segments.map((segment) => asRecord(segment)?.document)
|
||||
const childChunks = records.flatMap((record) => {
|
||||
const value = asRecord(record)?.child_chunks
|
||||
return Array.isArray(value) ? value : []
|
||||
})
|
||||
const files = records.flatMap((record) => {
|
||||
const value = asRecord(record)?.files
|
||||
return Array.isArray(value) ? value : []
|
||||
})
|
||||
const scores = finiteNumbers(records.map((record) => asRecord(record)?.score))
|
||||
return {
|
||||
count: records.length,
|
||||
itemKeys: unionKeys(records),
|
||||
segmentKeys: unionKeys(segments),
|
||||
documentKeys: unionKeys(documents),
|
||||
childChunkKeys: unionKeys(childChunks),
|
||||
fileKeys: unionKeys(files),
|
||||
contentPresent: segments.some((segment) =>
|
||||
typeof asRecord(segment)?.content === 'string'
|
||||
),
|
||||
childContentPresent: childChunks.some((chunk) =>
|
||||
typeof asRecord(chunk)?.content === 'string'
|
||||
),
|
||||
summaryPresent: records.some((record) =>
|
||||
typeof asRecord(record)?.summary === 'string'
|
||||
),
|
||||
scoreRange: scores.length > 0
|
||||
? { minimum: Math.min(...scores), maximum: Math.max(...scores) }
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function probeDify(configuration_) {
|
||||
const context = { apiKey: configuration_.apiKey, operations: [] }
|
||||
const base = baseUrl('dify', configuration_.baseUrl)
|
||||
const catalogUrl = endpoint(base, '/v1/datasets')
|
||||
catalogUrl.searchParams.set('page', '1')
|
||||
catalogUrl.searchParams.set('limit', '100')
|
||||
const catalog = await request(context, 'catalog', catalogUrl)
|
||||
const catalogBody = asRecord(catalog?.body)
|
||||
const items = Array.isArray(catalogBody?.data) ? catalogBody.data : []
|
||||
const first = asRecord(items[0])
|
||||
let detail
|
||||
const retrievals = []
|
||||
if (typeof first?.id === 'string') {
|
||||
detail = await request(
|
||||
context,
|
||||
'detail',
|
||||
endpoint(base, `/v1/datasets/${encodeURIComponent(first.id)}`)
|
||||
)
|
||||
const retrieval = await request(
|
||||
context,
|
||||
'retrieve-defaults',
|
||||
endpoint(base, `/v1/datasets/${encodeURIComponent(first.id)}/retrieve`),
|
||||
{ method: 'POST', body: JSON.stringify({ query }) }
|
||||
)
|
||||
const retrievalBody = asRecord(retrieval?.body)
|
||||
const records = Array.isArray(retrievalBody?.records) ? retrievalBody.records : []
|
||||
retrievals.push({ mode: 'dataset-defaults', ...difyResultShape(records) })
|
||||
if (extended && asRecord(first.retrieval_model_dict)) {
|
||||
const override = await request(
|
||||
context,
|
||||
'retrieve-explicit-config',
|
||||
endpoint(base, `/v1/datasets/${encodeURIComponent(first.id)}/retrieve`),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
retrieval_model: first.retrieval_model_dict
|
||||
})
|
||||
}
|
||||
)
|
||||
const overrideBody = asRecord(override?.body)
|
||||
const overrideRecords = Array.isArray(overrideBody?.records) ? overrideBody.records : []
|
||||
retrievals.push({ mode: 'explicit-current-config', ...difyResultShape(overrideRecords) })
|
||||
}
|
||||
}
|
||||
return {
|
||||
provider: 'dify',
|
||||
profile: configuration_.profile,
|
||||
detectedVersion: catalog?.response.headers.get('x-version') ?? undefined,
|
||||
transport: {
|
||||
protocol: base.protocol,
|
||||
tlsVerificationDisabled: base.protocol === 'https:'
|
||||
? process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0'
|
||||
: undefined
|
||||
},
|
||||
catalog: {
|
||||
count: items.length,
|
||||
total: Number.isFinite(catalogBody?.total) ? catalogBody.total : undefined,
|
||||
itemKeys: unionKeys(items),
|
||||
retrievalModelKeys: unionKeys(items.map((item) => asRecord(item)?.retrieval_model_dict)),
|
||||
indexingTechniques: [...new Set(items.map((item) => asRecord(item)?.indexing_technique).filter((value) => typeof value === 'string'))].sort(),
|
||||
metadataSchemaCount: items.filter((item) => {
|
||||
const value = asRecord(item)?.doc_metadata
|
||||
return Array.isArray(value) && value.length > 0
|
||||
}).length,
|
||||
multimodalCount: items.filter((item) => asRecord(item)?.is_multimodal === true).length,
|
||||
pipelineConfiguredCount: items.filter((item) => typeof asRecord(item)?.pipeline_id === 'string').length,
|
||||
summaryIndexConfiguredCount: items.filter((item) => asRecord(item)?.summary_index_setting !== null && asRecord(item)?.summary_index_setting !== undefined).length
|
||||
},
|
||||
detail: { keys: objectKeys(detail?.body) },
|
||||
retrievals,
|
||||
features: {
|
||||
datasetDefaults: true,
|
||||
detailSupported: detail?.response.ok === true,
|
||||
retrievalOverrideObserved: items.some((item) => asRecord(item)?.retrieval_model_dict !== undefined),
|
||||
retrievalOverrideProbed: retrievals.some((item) => item.mode === 'explicit-current-config'),
|
||||
nonEmptyResultObserved: retrievals.some((item) => item.count > 0)
|
||||
},
|
||||
operations: context.operations
|
||||
}
|
||||
}
|
||||
|
||||
function fastGptListResult(result) {
|
||||
if (!result) return { success: false, failure: 'request-failed' }
|
||||
if (!result.response.ok) return { success: false, failure: 'http-error' }
|
||||
const body = asRecord(result.body)
|
||||
if (body?.code !== 200) return { success: false, failure: 'business-error' }
|
||||
const data = asRecord(body.data)
|
||||
const list = Array.isArray(body.data) ? body.data : data?.list
|
||||
if (!Array.isArray(list)) return { success: false, failure: 'invalid-shape' }
|
||||
return { success: true, list }
|
||||
}
|
||||
|
||||
function fastGptRetrievalShape(result) {
|
||||
const { list, ...outcome } = fastGptListResult(result)
|
||||
return {
|
||||
...outcome,
|
||||
...(list ? resultShape(list, 'score') : {})
|
||||
}
|
||||
}
|
||||
|
||||
async function probeFastGpt(configuration_) {
|
||||
const context = { apiKey: configuration_.apiKey, operations: [] }
|
||||
const base = baseUrl('fastgpt', configuration_.baseUrl)
|
||||
const catalog = await request(
|
||||
context,
|
||||
'catalog',
|
||||
endpoint(base, '/core/dataset/list'),
|
||||
{ method: 'POST', body: JSON.stringify({ parentId: '' }) }
|
||||
)
|
||||
const catalogBody = asRecord(catalog?.body)
|
||||
const rawData = catalogBody?.data
|
||||
const { list, ...catalogOutcome } = fastGptListResult(catalog)
|
||||
const items = list ?? []
|
||||
const datasets = items.filter((item) => asRecord(item)?.type === 'dataset')
|
||||
const first = asRecord(datasets[0])
|
||||
let detail
|
||||
const retrievals = []
|
||||
if (typeof first?._id === 'string') {
|
||||
const detailUrl = endpoint(base, '/core/dataset/detail')
|
||||
detailUrl.searchParams.set('id', first._id)
|
||||
detail = await request(context, 'detail', detailUrl)
|
||||
for (const searchMode of ['embedding', 'fullTextRecall', 'mixedRecall']) {
|
||||
const result = await request(
|
||||
context,
|
||||
`retrieve-${searchMode}`,
|
||||
endpoint(base, '/core/dataset/searchTest'),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
datasetId: first._id,
|
||||
text: query,
|
||||
limit: 1_000,
|
||||
similarity: 0,
|
||||
searchMode,
|
||||
usingReRank: false
|
||||
})
|
||||
}
|
||||
)
|
||||
retrievals.push({ searchMode, ...fastGptRetrievalShape(result) })
|
||||
}
|
||||
if (extended) {
|
||||
const result = await request(
|
||||
context,
|
||||
'retrieve-rerank',
|
||||
endpoint(base, '/core/dataset/searchTest'),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
datasetId: first._id,
|
||||
text: query,
|
||||
limit: 1_000,
|
||||
similarity: 0,
|
||||
searchMode: 'mixedRecall',
|
||||
usingReRank: true
|
||||
})
|
||||
}
|
||||
)
|
||||
retrievals.push({
|
||||
searchMode: 'mixedRecall',
|
||||
usingRerank: true,
|
||||
...fastGptRetrievalShape(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
const detailData = asRecord(asRecord(detail?.body)?.data)
|
||||
return {
|
||||
provider: 'fastgpt',
|
||||
profile: configuration_.profile,
|
||||
detectedVersion: undefined,
|
||||
transport: {
|
||||
protocol: base.protocol,
|
||||
tlsVerificationDisabled: base.protocol === 'https:'
|
||||
? process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0'
|
||||
: undefined
|
||||
},
|
||||
catalog: {
|
||||
...catalogOutcome,
|
||||
...(catalogOutcome.success ? { count: datasets.length } : {}),
|
||||
envelope: Array.isArray(rawData) ? 'data-array' : 'data-object',
|
||||
itemKeys: unionKeys(items),
|
||||
itemTypes: [...new Set(items.map((item) => asRecord(item)?.type).filter((value) => typeof value === 'string'))].sort()
|
||||
},
|
||||
detail: {
|
||||
keys: objectKeys(detailData),
|
||||
vectorModelKeys: objectKeys(detailData?.vectorModel)
|
||||
},
|
||||
retrievals,
|
||||
features: {
|
||||
searchModes: [...new Set(retrievals.filter((item) => item.success && !item.usingRerank).map((item) => item.searchMode))],
|
||||
rerankProbed: retrievals.some((item) => item.usingRerank),
|
||||
rerankSucceeded: retrievals.some((item) => item.usingRerank && item.success),
|
||||
extensionQueryProbed: false
|
||||
},
|
||||
operations: context.operations
|
||||
}
|
||||
}
|
||||
|
||||
async function probeRagflow(configuration_) {
|
||||
const context = { apiKey: configuration_.apiKey, operations: [] }
|
||||
const base = baseUrl('ragflow', configuration_.baseUrl)
|
||||
const catalogUrl = endpoint(base, '/api/v1/datasets')
|
||||
catalogUrl.searchParams.set('page', '1')
|
||||
catalogUrl.searchParams.set('page_size', '100')
|
||||
const catalog = await request(context, 'catalog', catalogUrl)
|
||||
const catalogBody = asRecord(catalog?.body)
|
||||
const items = Array.isArray(catalogBody?.data) ? catalogBody.data : []
|
||||
const graphConfiguredItems = items.filter((item) => {
|
||||
const record = asRecord(item)
|
||||
const graph = asRecord(asRecord(record?.parser_config)?.graphrag)
|
||||
return graph?.use_graphrag === true
|
||||
})
|
||||
const graphCompletedItems = items.filter((item) => {
|
||||
const value = asRecord(item)?.graphrag_task_finish_at
|
||||
return (typeof value === 'number' && value > 0) || (typeof value === 'string' && value.length > 0)
|
||||
})
|
||||
const compilationConfiguredItems = items.filter((item) => {
|
||||
const value = asRecord(asRecord(item)?.parser_config)?.compilation_template_group_id
|
||||
return typeof value === 'string' && value.length > 0
|
||||
})
|
||||
const first = asRecord(graphCompletedItems[0] ?? graphConfiguredItems[0] ?? items[0])
|
||||
let detail
|
||||
const retrievals = []
|
||||
if (typeof first?.id === 'string') {
|
||||
const detailUrl = endpoint(base, '/api/v1/datasets')
|
||||
detailUrl.searchParams.set('id', first.id)
|
||||
detail = await request(context, 'detail', detailUrl)
|
||||
for (const feature of [
|
||||
{ name: 'baseline', use_kg: false, include_knowledge_compilation: false },
|
||||
...(extended
|
||||
? [
|
||||
{ name: 'graph', use_kg: true, include_knowledge_compilation: false },
|
||||
{ name: 'knowledge-compilation', use_kg: false, include_knowledge_compilation: true }
|
||||
]
|
||||
: [])
|
||||
]) {
|
||||
const result = await request(
|
||||
context,
|
||||
`retrieve-${feature.name}`,
|
||||
endpoint(base, '/api/v1/retrieval'),
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
question: query,
|
||||
dataset_ids: [first.id],
|
||||
page: 1,
|
||||
page_size: 6,
|
||||
similarity_threshold: 0,
|
||||
vector_similarity_weight: 0.3,
|
||||
knn_top_k: 128,
|
||||
use_kg: feature.use_kg,
|
||||
include_knowledge_compilation: feature.include_knowledge_compilation
|
||||
})
|
||||
}
|
||||
)
|
||||
const data = asRecord(asRecord(result?.body)?.data)
|
||||
const chunks = Array.isArray(data?.chunks) ? data.chunks : []
|
||||
retrievals.push({ feature: feature.name, ...resultShape(chunks, 'similarity') })
|
||||
}
|
||||
}
|
||||
const detailBody = asRecord(detail?.body)
|
||||
const detailData = Array.isArray(detailBody?.data) ? detailBody.data[0] : detailBody?.data
|
||||
return {
|
||||
provider: 'ragflow',
|
||||
profile: configuration_.profile,
|
||||
detectedVersion: undefined,
|
||||
transport: {
|
||||
protocol: base.protocol,
|
||||
tlsVerificationDisabled: base.protocol === 'https:'
|
||||
? process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0'
|
||||
: undefined
|
||||
},
|
||||
catalog: {
|
||||
count: items.length,
|
||||
total: Number.isFinite(catalogBody?.total_datasets) ? catalogBody.total_datasets : undefined,
|
||||
itemKeys: unionKeys(items),
|
||||
parserConfigKeys: unionKeys(items.map((item) => asRecord(item)?.parser_config)),
|
||||
graphConfiguredCount: graphConfiguredItems.length,
|
||||
graphCompletedCount: graphCompletedItems.length,
|
||||
knowledgeCompilationConfiguredCount: compilationConfiguredItems.length
|
||||
},
|
||||
detail: { keys: objectKeys(detailData) },
|
||||
retrievals,
|
||||
features: {
|
||||
graphRetrievalProbed: extended,
|
||||
knowledgeCompilationProbed: extended,
|
||||
metadataConditionProbed: false
|
||||
},
|
||||
operations: context.operations
|
||||
}
|
||||
}
|
||||
|
||||
const selectedProviders = selectedProvider === 'all' ? providers : [selectedProvider]
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
probedAt: new Date().toISOString(),
|
||||
mode: extended ? 'extended-read-only' : 'basic-read-only',
|
||||
queryCharacters: query.length,
|
||||
privacy: 'No credentials, origins, IDs, names, queries, or content are included.',
|
||||
providers: []
|
||||
}
|
||||
const sensitiveValues = []
|
||||
|
||||
for (const provider of selectedProviders) {
|
||||
for (const config of await configurations(provider)) {
|
||||
sensitiveValues.push(config.baseUrl, config.apiKey)
|
||||
report.providers.push(
|
||||
provider === 'dify'
|
||||
? await probeDify(config)
|
||||
: provider === 'fastgpt'
|
||||
? await probeFastGpt(config)
|
||||
: await probeRagflow(config)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const serialized = `${JSON.stringify(report, null, 2)}\n`
|
||||
const unsafeOutput =
|
||||
sensitiveValues.some((value) => value && serialized.includes(value)) ||
|
||||
/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i.test(serialized) ||
|
||||
/\b[0-9a-f]{24}\b/i.test(serialized) ||
|
||||
['PUT', 'PATCH', 'DELETE'].some((method) => serialized.includes(`"method": "${method}"`))
|
||||
if (unsafeOutput) {
|
||||
throw new Error('Probe report failed its redaction and read-only audit')
|
||||
}
|
||||
if (outputPath) {
|
||||
const target = resolve(outputPath)
|
||||
await mkdir(dirname(target), { recursive: true })
|
||||
await writeFile(target, serialized, 'utf8')
|
||||
globalThis.console.log(`Wrote redacted probe report to ${target}`)
|
||||
} else {
|
||||
process.stdout.write(serialized)
|
||||
}
|
||||
+2
-2
@@ -81,7 +81,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
|
||||
|
||||
校验规则与桌面更新检查保持一致:索引只能指向稳定 SemVer 版本,必须恰好
|
||||
包含 Windows、macOS、Linux 的 x64 / arm64 六个匹配目标;每个目标必须提供
|
||||
准确的两种格式和扩展名、正的安全整数大小、64 字符小写十六进制 SHA-256、
|
||||
准确的格式和扩展名(Windows 两种、macOS 仅 DMG、Linux 三种),正的安全整数大小、64 字符小写十六进制 SHA-256、
|
||||
全局唯一文件名,以及位于
|
||||
`https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/v${version}/`
|
||||
下、对文件名进行 URL 编码的精确地址。校验清单和 GitHub 回退地址也必须
|
||||
@@ -90,7 +90,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
|
||||
安装包文件名必须与发布生成器完全一致:Windows 使用
|
||||
`GoodBuddy-${version}-windows-${arch}-setup.exe` 与
|
||||
`GoodBuddy-${version}-windows-${arch}-portable.zip`;macOS 使用
|
||||
`GoodBuddy-${version}-mac-${arch}.dmg|zip`;Linux x64 的 AppImage、DEB 与
|
||||
`GoodBuddy-${version}-mac-${arch}.dmg`;读取器仍接受历史 DMG/ZIP 清单,但当前页面不提供 macOS ZIP 选项。Linux x64 的 AppImage、DEB 与
|
||||
RPM 分别使用 electron-builder 的 `x86_64`、`amd64` 与 `x86_64` 架构名。
|
||||
Linux arm64 的 AppImage/DEB 使用 `arm64`,RPM 使用 `aarch64`。
|
||||
Linux 处理器选择器另外提供龙芯 `loong64` 实验预览;选中后安装包类型固定为
|
||||
|
||||
+1
-1
@@ -261,7 +261,7 @@
|
||||
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG / ZIP</p></div>
|
||||
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG</p></div>
|
||||
<a
|
||||
class="button button--download"
|
||||
href="https://github.com/mesalogo/goodbuddy/releases/latest"
|
||||
|
||||
+1
-2
@@ -285,7 +285,7 @@
|
||||
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG / ZIP</p></div>
|
||||
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG</p></div>
|
||||
<div class="download-options" data-download-card="macos">
|
||||
<label>
|
||||
<span>处理器</span>
|
||||
@@ -298,7 +298,6 @@
|
||||
<span>类型</span>
|
||||
<select data-download-format aria-label="macOS 安装包类型">
|
||||
<option value="dmg">DMG(推荐)</option>
|
||||
<option value="zip">ZIP</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
"macos-x64": Object.freeze({
|
||||
platform: "macos",
|
||||
arch: "x64",
|
||||
formats: Object.freeze(["dmg", "zip"]),
|
||||
formats: Object.freeze(["dmg"]),
|
||||
}),
|
||||
"macos-arm64": Object.freeze({
|
||||
platform: "macos",
|
||||
arch: "arm64",
|
||||
formats: Object.freeze(["dmg", "zip"]),
|
||||
formats: Object.freeze(["dmg"]),
|
||||
}),
|
||||
"linux-x64": Object.freeze({
|
||||
platform: "linux",
|
||||
@@ -143,6 +143,9 @@
|
||||
for (const key of targetKeys) {
|
||||
const definition = targetDefinitions[key];
|
||||
const target = index.targets[key];
|
||||
const formats = definition.platform === "macos" && target?.files?.zip
|
||||
? ["dmg", "zip"]
|
||||
: definition.formats;
|
||||
assert(
|
||||
hasExactKeys(target, ["platform", "arch", "files"]) &&
|
||||
target.platform === definition.platform &&
|
||||
@@ -150,11 +153,11 @@
|
||||
`发布目标与键不匹配:${key}`,
|
||||
);
|
||||
assert(
|
||||
hasExactKeys(target.files, definition.formats),
|
||||
hasExactKeys(target.files, formats),
|
||||
`发布目标文件格式无效:${key}`,
|
||||
);
|
||||
|
||||
for (const format of definition.formats) {
|
||||
for (const format of formats) {
|
||||
const file = target.files[format];
|
||||
assert(
|
||||
hasExactKeys(file, ["name", "size", "sha256", "url"]),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user