From 05a515a66eb42581db232bade580c2677aea8259 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 22:07:21 +0800 Subject: [PATCH 1/3] chore(dsh): pin DSH 0.2.1-rc.2 in CI, install help and docs The harness pin, both workflows, the install help, the local CI script, the README commands and the docs still named 0.2.0-rc.2 while the npm latest is 0.2.1-rc.2, so a fresh install ran a version nobody tests. Move every copy to the harness pin, keep the test.yml job name (the merge queue's required check) unchanged, and add core/test/dsh-pin.test.js so a half-done bump fails. Fixes #1384 Docs-not-needed: only the pinned version string moves; no subsystem behavior changes. --- .github/workflows/nightly.yml | 8 +-- .github/workflows/test.yml | 4 +- README.md | 12 ++-- README.zh.md | 12 ++-- changelog.d/1384-dsh-pin.changed.md | 1 + core/dev/harness/package.json | 2 +- core/scripts/install-doctor.mjs | 2 +- core/scripts/local-ci.sh | 4 +- core/src/host/turn-origin.js | 2 +- core/test/about.test.js | 6 +- core/test/dsh-pin.test.js | 63 +++++++++++++++++++ core/test/install-doctor.test.js | 2 +- core/test/turn-origin.test.js | 2 +- docs/deepseek-api-key.md | 2 +- docs/docker.md | 2 +- docs/docker.zh.md | 2 +- docs/known-issues.md | 4 +- docs/known-issues.zh.md | 4 +- patches/ds-bot/0007-version-and-tooling.patch | 14 +++++ .../ds-bot/files/scripts/install-doctor.mjs | 2 +- patches/ds-bot/files/scripts/local-ci.sh | 4 +- patches/ds-bot/files/src/host/turn-origin.js | 2 +- patches/ds-bot/files/test/about.test.js | 6 +- patches/ds-bot/files/test/dsh-pin.test.js | 63 +++++++++++++++++++ .../ds-bot/files/test/install-doctor.test.js | 2 +- patches/ds-bot/files/test/turn-origin.test.js | 2 +- patches/ds-bot/topics.json | 3 +- 27 files changed, 187 insertions(+), 45 deletions(-) create mode 100644 changelog.d/1384-dsh-pin.changed.md mode change 100755 => 100644 core/scripts/local-ci.sh create mode 100644 core/test/dsh-pin.test.js mode change 100755 => 100644 patches/ds-bot/files/scripts/local-ci.sh create mode 100644 patches/ds-bot/files/test/dsh-pin.test.js diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ab139d5ca..ab92f336e 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,6 +1,6 @@ # Compatibility matrix behind the slim PR gate (#31): the PR gate ships one job, -# ubuntu · Node 24 · DSH 0.2.0-rc.2. This workflow restores Windows · Node 24 · -# DSH 0.2.0-rc.2, ubuntu · Node 22 · DSH 0.2.0-rc.2 and ubuntu · Node 24 · +# ubuntu · Node 24 · DSH 0.2.1-rc.2. This workflow restores Windows · Node 24 · +# DSH 0.2.1-rc.2, ubuntu · Node 22 · DSH 0.2.1-rc.2 and ubuntu · Node 24 · # DSH 0.2.1-alpha.1 on schedule and dispatch, and supersedes the matrix/compat # parts of #29. A window in which main had no commits is skipped; a schedule only # fires on the default branch. macOS runs locally on a Mac with `npm run ci:local` @@ -74,7 +74,7 @@ jobs: fail-fast: false matrix: include: - - { os: ubuntu-latest, node: 22, dsh: 0.2.0-rc.2 } + - { os: ubuntu-latest, node: 22, dsh: 0.2.1-rc.2 } - { os: ubuntu-latest, node: 24, dsh: 0.2.1-alpha.1 } defaults: run: @@ -137,7 +137,7 @@ jobs: fail-fast: false matrix: include: - - { os: windows-latest, node: 24, dsh: 0.2.0-rc.2 } + - { os: windows-latest, node: 24, dsh: 0.2.1-rc.2 } defaults: run: shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70a80011c..cb4042045 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: - uses: ./.github/actions/flybot-tests with: node: '24' - dsh: 0.2.0-rc.2 + dsh: 0.2.1-rc.2 coverage: 'true' plugins: 'false' @@ -68,7 +68,7 @@ jobs: - name: Install DSH working-directory: core env: - DSH: 0.2.0-rc.2 + DSH: 0.2.1-rc.2 run: npm run harness -- --no-save "@deepseek-ai/dsh@$DSH" - name: Lint run: node scripts/lint-plugins.mjs "$PLUGIN" diff --git a/README.md b/README.md index f90d62295..364f9448d 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,9 @@ here, so Bot lines post without a hold. Some waiting is sped up.* **2. Add FlyBot to DeepSeek Harness** ```sh -npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/core" -npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/social" -npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/tree" +npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/core" +npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/social" +npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/tree" ``` The first line is the FlyBot core (package `ds-bot`); the two plugins are what turn it into a team. Add @@ -115,13 +115,13 @@ The first line is the FlyBot core (package `ds-bot`); the two plugins are what t On DSH Desktop: **Plugins → Add plugin**, paste one `github:` address at a time, then **Install** and **Enable now**. -`0.2.0-rc.2` is the DeepSeek Harness release FlyBot is built and tested against — the same version `core/dev/harness` +`0.2.1-rc.2` is the DeepSeek Harness release FlyBot is built and tested against — the same version `core/dev/harness` pins and CI runs, so keep it in all four commands. **3. Run it** ```sh -npx -y @deepseek-ai/dsh@0.2.0-rc.2 web --port 3080 +npx -y @deepseek-ai/dsh@0.2.1-rc.2 web --port 3080 ``` Open the printed `http://127.0.0.1:3080/?token=…` link, pick a model the first time, and say hello to the Chief. To @@ -163,7 +163,7 @@ old credentials are kept as a backup. Details: [social subsystem](docs/subsystem FlyBot is the upstream [DS Bot](https://github.com/Fei2411/DeepSeek-Bot) core, kept byte-identical and pinned in `ds-bot.pin.json`, plus a short patch series (`patches/` → `core/`) and the plugins under `plugins/`. It is developed -and tested against DeepSeek Harness `0.2.0-rc.2`, the version the commands above pin. The npm package keeps +and tested against DeepSeek Harness `0.2.1-rc.2`, the version the commands above pin. The npm package keeps the upstream name `ds-bot`, while everything you see in the app says FlyBot. ## License diff --git a/README.zh.md b/README.zh.md index ac658eb8d..089641350 100644 --- a/README.zh.md +++ b/README.zh.md @@ -92,9 +92,9 @@ FlyBot 是一个直接跑起来的应用,建在 DeepSeek Harness 之上。 **2. 把 FlyBot 加到 DeepSeek Harness** ```sh -npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/core" -npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/social" -npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/tree" +npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/core" +npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/social" +npx -y @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add "github:ThinkFlowLab/FlyBot#main&path:/plugins/tree" ``` 第一行是 FlyBot 核心(包名 `ds-bot`),后两行才把它变成一支团队。想要工作流或时间线,照样再加 @@ -102,13 +102,13 @@ npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab 桌面版:**插件 → 添加插件**,一次粘一个 `github:` 地址,然后**安装**并**立即启用**。 -`0.2.0-rc.2` 是 FlyBot 构建与测试所用的 DeepSeek Harness 版本,也正是 `core/dev/harness` 钉住、CI 在跑的那一版, +`0.2.1-rc.2` 是 FlyBot 构建与测试所用的 DeepSeek Harness 版本,也正是 `core/dev/harness` 钉住、CI 在跑的那一版, 四条命令保持一致。 **3. 跑起来** ```sh -npx -y @deepseek-ai/dsh@0.2.0-rc.2 web --port 3080 +npx -y @deepseek-ai/dsh@0.2.1-rc.2 web --port 3080 ``` 打开打印出的 `http://127.0.0.1:3080/?token=…`,第一次先选一个模型,然后跟 Chief 打个招呼。想让它在一台一直开着 @@ -148,7 +148,7 @@ npx -y @deepseek-ai/dsh@0.2.0-rc.2 web --port 3080 FlyBot 是上游 [DS Bot](https://github.com/Fei2411/DeepSeek-Bot) 的核心,保持字节一致并钉在 `ds-bot.pin.json` 里,加上一组简短的补丁(`patches/` → `core/`)和 `plugins/` 下的插件。开发和测试针对 DeepSeek Harness -`0.2.0-rc.2`,也就是上面命令钉住的版本。npm 包名沿用上游的 `ds-bot`,而你在界面里看到的都是 FlyBot。 +`0.2.1-rc.2`,也就是上面命令钉住的版本。npm 包名沿用上游的 `ds-bot`,而你在界面里看到的都是 FlyBot。 ## 许可 diff --git a/changelog.d/1384-dsh-pin.changed.md b/changelog.d/1384-dsh-pin.changed.md new file mode 100644 index 000000000..3a9704f11 --- /dev/null +++ b/changelog.d/1384-dsh-pin.changed.md @@ -0,0 +1 @@ +FlyBot now builds, tests and installs on DeepSeek Harness `0.2.1-rc.2`, the current npm `latest`; the README commands, the CI workflows and the install help all pin that version ([#1384](https://github.com/ThinkFlowLab/FlyBot/issues/1384)). diff --git a/core/dev/harness/package.json b/core/dev/harness/package.json index ed7ae6b32..971981144 100644 --- a/core/dev/harness/package.json +++ b/core/dev/harness/package.json @@ -3,7 +3,7 @@ "private": true, "description": "The DeepSeek Harness packages that the unit tests and the linter run against. Install with npm run harness in ds-bot/.", "dependencies": { - "@deepseek-ai/dsh": "0.2.0-rc.2", + "@deepseek-ai/dsh": "0.2.1-rc.2", "jsdom": "29.1.1", "oxlint": "1.76.0" } diff --git a/core/scripts/install-doctor.mjs b/core/scripts/install-doctor.mjs index 9712e4cfb..e8b2e4227 100644 --- a/core/scripts/install-doctor.mjs +++ b/core/scripts/install-doctor.mjs @@ -73,7 +73,7 @@ export function buildInstallCommands({ repo = 'ThinkFlowLab/FlyBot', ref = 'main', profile = 'web', - dshPackage = '@deepseek-ai/dsh@0.2.0-rc.2', + dshPackage = '@deepseek-ai/dsh@0.2.1-rc.2', timeline = false, // @flybot/timeline is opt-in (#281): its install command is printed only on request } = {}) { return ['/core', '/plugins/social', '/plugins/tree', '/plugins/workflow', ...(timeline ? ['/plugins/timeline'] : [])] diff --git a/core/scripts/local-ci.sh b/core/scripts/local-ci.sh old mode 100755 new mode 100644 index 2659fa57e..4ac58bdbc --- a/core/scripts/local-ci.sh +++ b/core/scripts/local-ci.sh @@ -34,9 +34,9 @@ step="npm ci" echo "=== $step ===" npm ci --no-fund --no-audit -step="Install DSH 0.2.0-rc.2" +step="Install DSH 0.2.1-rc.2" echo "=== $step ===" -npm run harness -- --no-save @deepseek-ai/dsh@0.2.0-rc.2 +npm run harness -- --no-save @deepseek-ai/dsh@0.2.1-rc.2 step="lint" echo "=== $step ===" diff --git a/core/src/host/turn-origin.js b/core/src/host/turn-origin.js index c31b302c0..786cc5c94 100644 --- a/core/src/host/turn-origin.js +++ b/core/src/host/turn-origin.js @@ -26,7 +26,7 @@ export const SCHEDULED = 'scheduled' // The messages that carry nobody's request, by source kind — one list, because everything else // fails closed as 'other' (#620): a kind missing here takes the connector tools away in the middle // of a turn the owner started (#626), and a kind wrongly added here would hand them out. -// Derived from `MessageSourceMap` in @deepseek-ai/dsh-llm (0.2.0-rc.2, the version +// Derived from `MessageSourceMap` in @deepseek-ai/dsh-llm (0.2.1-rc.2, the version // core/dev/harness/package.json pins). A kind is not recognisable by its `form` alone: the kinds // below that declare no `form` at all (`working-directory`, `ptc-mode`, `cordis-host-runner`) would // read as opaque content, and `webhook` declares `form: 'notice'` while being an outside delivery diff --git a/core/test/about.test.js b/core/test/about.test.js index 9e8fc5fc0..baef580b3 100644 --- a/core/test/about.test.js +++ b/core/test/about.test.js @@ -52,7 +52,7 @@ const page = (update) => { links: nodes.filter(node => node.type === 'a').map(node => node.props.href), } } -const base = { current: '0.1.5-flybot.1', repoUrl: REPO, notesUrl: `${REPO}/releases`, enabled: true, phase: 'idle', source: 'git', checkedAt: 0, dsh: { current: '0.2.0-rc.2', available: false, phase: 'idle' } } +const base = { current: '0.1.5-flybot.1', repoUrl: REPO, notesUrl: `${REPO}/releases`, enabled: true, phase: 'idle', source: 'git', checkedAt: 0, dsh: { current: '0.2.1-rc.2', available: false, phase: 'idle' } } const MAINTAINER = 'Updates come from the maintainer: reinstall FlyBot to get them.' test('the page points at FlyBot: star, source code, release notes and the underlying harness', () => { @@ -60,7 +60,7 @@ test('the page points at FlyBot: star, source code, release notes and the underl assert.equal(view.star.props.href, REPO) assert.deepEqual(view.links.filter(href => href.includes('github.com')), [REPO, REPO, `${REPO}/releases`]) assert.equal(view.row('Source code').props['data-hint'], 'github.com/ThinkFlowLab/FlyBot') - assert.equal(view.row('DeepSeek Harness (underlying)').props['data-hint'], 'Version 0.2.0-rc.2') + assert.equal(view.row('DeepSeek Harness (underlying)').props['data-hint'], 'Version 0.2.1-rc.2') assert.equal(view.row('DeepSeek Harness'), undefined) assert.doesNotMatch(JSON.stringify(view.links), /Fei2411/) }) @@ -109,7 +109,7 @@ test('the new About wording has zh copy', () => { try { const view = page({ ...base, build: { branch: 'main', commit: '082563a' }, behind: 3, checkedAt: Date.now() }) assert.match(view.hint(DICTIONARY.zh['FlyBot core']), /^落后 main 3 个提交。更新由维护者发布:重新安装 FlyBot 即可升级。 · /) - assert.equal(view.row('DeepSeek Harness(底层)').props['data-hint'], '版本 0.2.0-rc.2') + assert.equal(view.row('DeepSeek Harness(底层)').props['data-hint'], '版本 0.2.1-rc.2') } finally { off() } diff --git a/core/test/dsh-pin.test.js b/core/test/dsh-pin.test.js new file mode 100644 index 000000000..b492468a5 --- /dev/null +++ b/core/test/dsh-pin.test.js @@ -0,0 +1,63 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +// The DSH version is pinned in several places that cannot read each other: the harness install the +// tests and the linter use, the CI workflows, the install help and the README commands. A bump that +// moves one and leaves another shows up as a stale `npx` command or a workflow installing a version +// nobody tested (#1384), so every copy is compared with the harness pin here. +const root = new URL('../../', import.meta.url) +const read = path => readFileSync(new URL(path, root), 'utf8') + +// The version this repository pins. Bumping the DSH pin means editing this line together with the +// files below; the job name in test.yml is a merge-queue required check and does not move with it. +const DSH = '0.2.1-rc.2' +const PREVIOUS = '0.2.0-rc.2' + +test('the harness pin is the version every other copy must equal', () => { + const harness = JSON.parse(read('core/dev/harness/package.json')) + assert.equal(harness.dependencies['@deepseek-ai/dsh'], DSH) +}) + +test('the CI workflows install the pinned DSH, and the PR gate keeps its required job name', () => { + const test_yml = read('.github/workflows/test.yml') + assert.match(test_yml, new RegExp(`\\bdsh: ${DSH.replaceAll('.', '\\.')}$`, 'm'), 'test.yml dsh: input') + assert.match(test_yml, new RegExp(`\\bDSH: ${DSH.replaceAll('.', '\\.')}$`, 'm'), 'test.yml DSH: env') + // The job name is a required check on the main merge queue (#31): it keeps the old version until + // the owner changes the ruleset, so a pin bump must not rename it. + assert.match(test_yml, new RegExp(`^ name: ubuntu-latest · Node 24 · DSH ${PREVIOUS.replaceAll('.', '\\.')}$`, 'm')) + assert.ok(!test_yml.includes(`DSH ${DSH}`), 'the job name moved with the pin; the merge queue would wait forever') + + const nightly = read('.github/workflows/nightly.yml') + assert.ok(!nightly.includes(PREVIOUS), 'nightly.yml still names the previous DSH') + assert.equal(nightly.match(new RegExp(`dsh: ${DSH.replaceAll('.', '\\.')}`, 'g'))?.length, 2, 'nightly matrix entries') +}) + +test('the install help and the local CI install the pinned DSH', () => { + const doctor = read('core/scripts/install-doctor.mjs') + assert.ok(doctor.includes(`@deepseek-ai/dsh@${DSH}`), 'install-doctor.mjs default package') + assert.ok(!doctor.includes(PREVIOUS), 'install-doctor.mjs still names the previous DSH') + + const localCi = read('core/scripts/local-ci.sh') + assert.ok(localCi.includes(`npm run harness -- --no-save @deepseek-ai/dsh@${DSH}`), 'local-ci.sh harness install') + assert.ok(localCi.includes(`step="Install DSH ${DSH}"`), 'local-ci.sh step label') + assert.ok(!localCi.includes(PREVIOUS), 'local-ci.sh still names the previous DSH') +}) + +test('every npx command in the READMEs runs the pinned DSH', () => { + for (const file of ['README.md', 'README.zh.md']) { + const text = read(file) + const pins = [...text.matchAll(/@deepseek-ai\/dsh@([0-9][^\s"]*)/g)].map(match => match[1]) + assert.ok(pins.length >= 4, `${file}: expected the four npx commands`) + assert.deepEqual([...new Set(pins)], [DSH], `${file}: npx commands and the tested-with line`) + assert.ok(text.includes(DSH), `${file}: names the tested DSH version`) + } +}) + +test('the docs that tell people which DSH to run name the pinned version', () => { + for (const file of ['docs/deepseek-api-key.md', 'docs/known-issues.md', 'docs/known-issues.zh.md', 'docs/docker.md', 'docs/docker.zh.md']) { + const text = read(file) + assert.ok(text.includes(DSH), `${file}: names the pinned DSH`) + assert.ok(!text.includes(PREVIOUS), `${file}: still names the previous DSH`) + } +}) diff --git a/core/test/install-doctor.test.js b/core/test/install-doctor.test.js index 5819b4e1a..91bf55ab9 100644 --- a/core/test/install-doctor.test.js +++ b/core/test/install-doctor.test.js @@ -100,7 +100,7 @@ test('checkPluginPackage: a numeric dsh.bundle.patch fails without throwing', () test('buildInstallCommands: core, social, tree and workflow; timeline only on request (#281)', () => { const commands = buildInstallCommands() assert.equal(commands.length, 4) - assert.ok(commands.every(command => command.includes('0.2.0-rc.2'))) + assert.ok(commands.every(command => command.includes('0.2.1-rc.2'))) assert.ok(commands[0].includes('path:/core')) assert.ok(commands[1].includes('path:/plugins/social')) assert.ok(commands[2].includes('path:/plugins/tree')) diff --git a/core/test/turn-origin.test.js b/core/test/turn-origin.test.js index 6dae72a14..1e0c24c22 100644 --- a/core/test/turn-origin.test.js +++ b/core/test/turn-origin.test.js @@ -135,5 +135,5 @@ test('settled: the verdict that holds now, from the team as it is now', () => { test('#626: the no-request kind list is derived from the DSH version the tests run against', () => { const harness = new URL('../dev/harness/package.json', import.meta.url) const pinned = JSON.parse(readFileSync(harness, 'utf8')).dependencies['@deepseek-ai/dsh'] - assert.equal(pinned, '0.2.0-rc.2', 'DSH moved: re-read MessageSourceMap and update NOTICE_KINDS in src/host/turn-origin.js') + assert.equal(pinned, '0.2.1-rc.2', 'DSH moved: re-read MessageSourceMap and update NOTICE_KINDS in src/host/turn-origin.js') }) diff --git a/docs/deepseek-api-key.md b/docs/deepseek-api-key.md index 3667b8869..77bc200ba 100644 --- a/docs/deepseek-api-key.md +++ b/docs/deepseek-api-key.md @@ -34,7 +34,7 @@ If the file already exists, add the `DEEPSEEK_API_KEY` line under `refs` and kee Set `DEEPSEEK_API_KEY` in the shell that starts DSH: ```sh -DEEPSEEK_API_KEY=sk-your-key npx -y @deepseek-ai/dsh@0.2.0-rc.2 web --port 3080 +DEEPSEEK_API_KEY=sk-your-key npx -y @deepseek-ai/dsh@0.2.1-rc.2 web --port 3080 ``` DSH reads the environment once, at start, so a key exported later needs a restart. While it is set, it wins over every other place and DSH treats the key as read-only: saving a different key in the app is refused until you start DSH without the variable. diff --git a/docs/docker.md b/docs/docker.md index ef4c93385..c964e7610 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -2,7 +2,7 @@ [中文](docker.zh.md) -One person runs one FlyBot on a Linux machine that stays on, so Bots and schedules keep working with the laptop closed. The container runs `dsh web` (DSH 0.2.0-rc.2, as in the README) with the FlyBot core and the social, tree, workflow and timeline plugins of the checkout you build from. Instances talk to each other only through [Bot Social](https://github.com/Birfy/bot-social), which is not in the image; host it yourself. +One person runs one FlyBot on a Linux machine that stays on, so Bots and schedules keep working with the laptop closed. The container runs `dsh web` (DSH 0.2.1-rc.2, as in the README) with the FlyBot core and the social, tree, workflow and timeline plugins of the checkout you build from. Instances talk to each other only through [Bot Social](https://github.com/Birfy/bot-social), which is not in the image; host it yourself. You build the image yourself. FlyBot does not publish an image. diff --git a/docs/docker.zh.md b/docs/docker.zh.md index 59b530e0c..faf023fad 100644 --- a/docs/docker.zh.md +++ b/docs/docker.zh.md @@ -2,7 +2,7 @@ [English](docker.md) -每人在一台一直开着的 Linux 机器上运行一个 FlyBot,合上笔记本后 Bot 和定时任务照常工作。容器里运行 `dsh web`(DSH 0.2.0-rc.2,与 README 相同),带上你构建所用代码里的 FlyBot 核心以及 social、tree、workflow、timeline 插件。实例之间只通过 [Bot Social](https://github.com/Birfy/bot-social) 通信;Bot Social 不在镜像里,需要你自己部署。 +每人在一台一直开着的 Linux 机器上运行一个 FlyBot,合上笔记本后 Bot 和定时任务照常工作。容器里运行 `dsh web`(DSH 0.2.1-rc.2,与 README 相同),带上你构建所用代码里的 FlyBot 核心以及 social、tree、workflow、timeline 插件。实例之间只通过 [Bot Social](https://github.com/Birfy/bot-social) 通信;Bot Social 不在镜像里,需要你自己部署。 镜像由你自己构建,FlyBot 不发布镜像。 diff --git a/docs/known-issues.md b/docs/known-issues.md index edd74b688..634628961 100644 --- a/docs/known-issues.md +++ b/docs/known-issues.md @@ -36,9 +36,9 @@ Installing another version of FlyBot does not help. 1. Quit DSH. 2. Open `cordis.patch.yml` in the profile folder. Look for a row with `id: bot` that has no `name: ds-bot` line, and remove that row. Keep a copy of the file first. -3. Check that `package.json` in the same folder lists `ds-bot` under `dependencies`. If it does not, add FlyBot again: `npx @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add ds-bot`. +3. Check that `package.json` in the same folder lists `ds-bot` under `dependencies`. If it does not, add FlyBot again: `npx @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add ds-bot`. 4. Start DSH again. -To see where the row comes from, run `npx @deepseek-ai/dsh@0.2.0-rc.2 --profile web --dump-config`. It prints every plugin row with the file that supplied it. If you report the problem, copy only the rows with `id: bot` and the file names; leave out keys and paths you do not want to share. +To see where the row comes from, run `npx @deepseek-ai/dsh@0.2.1-rc.2 --profile web --dump-config`. It prints every plugin row with the file that supplied it. If you report the problem, copy only the rows with `id: bot` and the file names; leave out keys and paths you do not want to share. Deleting the whole profile folder also fixes it, but it removes the profile's other plugins and its settings, such as models. The Bot team is in `~/.dsh/bot/` and is not affected either way. diff --git a/docs/known-issues.zh.md b/docs/known-issues.zh.md index 45c3d4841..6c1a9cc32 100644 --- a/docs/known-issues.zh.md +++ b/docs/known-issues.zh.md @@ -36,9 +36,9 @@ dsh: disabling profile plugin row "bot": its declared peer dependencies cannot b 1. 退出 DSH。 2. 打开 profile 文件夹里的 `cordis.patch.yml`,找 `id: bot` 但没有 `name: ds-bot` 的一行,删掉这一行。改之前先备份这个文件。 -3. 确认同一文件夹的 `package.json` 在 `dependencies` 里有 `ds-bot`。没有的话,重新添加 FlyBot:`npx @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add ds-bot`。 +3. 确认同一文件夹的 `package.json` 在 `dependencies` 里有 `ds-bot`。没有的话,重新添加 FlyBot:`npx @deepseek-ai/dsh@0.2.1-rc.2 plugin --profile web add ds-bot`。 4. 重新启动 DSH。 -想知道这一行来自哪里,可以运行 `npx @deepseek-ai/dsh@0.2.0-rc.2 --profile web --dump-config`。它会列出每一行插件配置,以及提供它的文件。如果要报告这个问题,只复制 `id: bot` 的那几行和文件名,不要带上 key 或你不想公开的路径。 +想知道这一行来自哪里,可以运行 `npx @deepseek-ai/dsh@0.2.1-rc.2 --profile web --dump-config`。它会列出每一行插件配置,以及提供它的文件。如果要报告这个问题,只复制 `id: bot` 的那几行和文件名,不要带上 key 或你不想公开的路径。 删掉整个 profile 文件夹也能解决,但会一并删掉这个 profile 的其他插件和设置(比如模型)。Bot 团队数据在 `~/.dsh/bot/`,两种做法都不会影响它。 diff --git a/patches/ds-bot/0007-version-and-tooling.patch b/patches/ds-bot/0007-version-and-tooling.patch index 4f9af0da7..ada85bbd0 100644 --- a/patches/ds-bot/0007-version-and-tooling.patch +++ b/patches/ds-bot/0007-version-and-tooling.patch @@ -871,3 +871,17 @@ diff --git a/test/diagnostics.test.js b/test/diagnostics.test.js +test('Report on GitHub opens a new issue in FlyBot\'s own repository', () => { + assert.equal(ISSUES_URL, 'https://github.com/ThinkFlowLab/FlyBot/issues/new') +}) + +diff --git a/dev/harness/package.json b/dev/harness/package.json +--- a/dev/harness/package.json ++++ b/dev/harness/package.json +@@ -2,8 +2,8 @@ + "name": "ds-bot-test-harness", + "private": true, + "description": "The DeepSeek Harness packages that the unit tests and the linter run against. Install with npm run harness in ds-bot/.", + "dependencies": { +- "@deepseek-ai/dsh": "0.2.0-rc.2", ++ "@deepseek-ai/dsh": "0.2.1-rc.2", + "jsdom": "29.1.1", + "oxlint": "1.76.0" + } diff --git a/patches/ds-bot/files/scripts/install-doctor.mjs b/patches/ds-bot/files/scripts/install-doctor.mjs index 9712e4cfb..e8b2e4227 100644 --- a/patches/ds-bot/files/scripts/install-doctor.mjs +++ b/patches/ds-bot/files/scripts/install-doctor.mjs @@ -73,7 +73,7 @@ export function buildInstallCommands({ repo = 'ThinkFlowLab/FlyBot', ref = 'main', profile = 'web', - dshPackage = '@deepseek-ai/dsh@0.2.0-rc.2', + dshPackage = '@deepseek-ai/dsh@0.2.1-rc.2', timeline = false, // @flybot/timeline is opt-in (#281): its install command is printed only on request } = {}) { return ['/core', '/plugins/social', '/plugins/tree', '/plugins/workflow', ...(timeline ? ['/plugins/timeline'] : [])] diff --git a/patches/ds-bot/files/scripts/local-ci.sh b/patches/ds-bot/files/scripts/local-ci.sh old mode 100755 new mode 100644 index 2659fa57e..4ac58bdbc --- a/patches/ds-bot/files/scripts/local-ci.sh +++ b/patches/ds-bot/files/scripts/local-ci.sh @@ -34,9 +34,9 @@ step="npm ci" echo "=== $step ===" npm ci --no-fund --no-audit -step="Install DSH 0.2.0-rc.2" +step="Install DSH 0.2.1-rc.2" echo "=== $step ===" -npm run harness -- --no-save @deepseek-ai/dsh@0.2.0-rc.2 +npm run harness -- --no-save @deepseek-ai/dsh@0.2.1-rc.2 step="lint" echo "=== $step ===" diff --git a/patches/ds-bot/files/src/host/turn-origin.js b/patches/ds-bot/files/src/host/turn-origin.js index c31b302c0..786cc5c94 100644 --- a/patches/ds-bot/files/src/host/turn-origin.js +++ b/patches/ds-bot/files/src/host/turn-origin.js @@ -26,7 +26,7 @@ export const SCHEDULED = 'scheduled' // The messages that carry nobody's request, by source kind — one list, because everything else // fails closed as 'other' (#620): a kind missing here takes the connector tools away in the middle // of a turn the owner started (#626), and a kind wrongly added here would hand them out. -// Derived from `MessageSourceMap` in @deepseek-ai/dsh-llm (0.2.0-rc.2, the version +// Derived from `MessageSourceMap` in @deepseek-ai/dsh-llm (0.2.1-rc.2, the version // core/dev/harness/package.json pins). A kind is not recognisable by its `form` alone: the kinds // below that declare no `form` at all (`working-directory`, `ptc-mode`, `cordis-host-runner`) would // read as opaque content, and `webhook` declares `form: 'notice'` while being an outside delivery diff --git a/patches/ds-bot/files/test/about.test.js b/patches/ds-bot/files/test/about.test.js index 9e8fc5fc0..baef580b3 100644 --- a/patches/ds-bot/files/test/about.test.js +++ b/patches/ds-bot/files/test/about.test.js @@ -52,7 +52,7 @@ const page = (update) => { links: nodes.filter(node => node.type === 'a').map(node => node.props.href), } } -const base = { current: '0.1.5-flybot.1', repoUrl: REPO, notesUrl: `${REPO}/releases`, enabled: true, phase: 'idle', source: 'git', checkedAt: 0, dsh: { current: '0.2.0-rc.2', available: false, phase: 'idle' } } +const base = { current: '0.1.5-flybot.1', repoUrl: REPO, notesUrl: `${REPO}/releases`, enabled: true, phase: 'idle', source: 'git', checkedAt: 0, dsh: { current: '0.2.1-rc.2', available: false, phase: 'idle' } } const MAINTAINER = 'Updates come from the maintainer: reinstall FlyBot to get them.' test('the page points at FlyBot: star, source code, release notes and the underlying harness', () => { @@ -60,7 +60,7 @@ test('the page points at FlyBot: star, source code, release notes and the underl assert.equal(view.star.props.href, REPO) assert.deepEqual(view.links.filter(href => href.includes('github.com')), [REPO, REPO, `${REPO}/releases`]) assert.equal(view.row('Source code').props['data-hint'], 'github.com/ThinkFlowLab/FlyBot') - assert.equal(view.row('DeepSeek Harness (underlying)').props['data-hint'], 'Version 0.2.0-rc.2') + assert.equal(view.row('DeepSeek Harness (underlying)').props['data-hint'], 'Version 0.2.1-rc.2') assert.equal(view.row('DeepSeek Harness'), undefined) assert.doesNotMatch(JSON.stringify(view.links), /Fei2411/) }) @@ -109,7 +109,7 @@ test('the new About wording has zh copy', () => { try { const view = page({ ...base, build: { branch: 'main', commit: '082563a' }, behind: 3, checkedAt: Date.now() }) assert.match(view.hint(DICTIONARY.zh['FlyBot core']), /^落后 main 3 个提交。更新由维护者发布:重新安装 FlyBot 即可升级。 · /) - assert.equal(view.row('DeepSeek Harness(底层)').props['data-hint'], '版本 0.2.0-rc.2') + assert.equal(view.row('DeepSeek Harness(底层)').props['data-hint'], '版本 0.2.1-rc.2') } finally { off() } diff --git a/patches/ds-bot/files/test/dsh-pin.test.js b/patches/ds-bot/files/test/dsh-pin.test.js new file mode 100644 index 000000000..b492468a5 --- /dev/null +++ b/patches/ds-bot/files/test/dsh-pin.test.js @@ -0,0 +1,63 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +// The DSH version is pinned in several places that cannot read each other: the harness install the +// tests and the linter use, the CI workflows, the install help and the README commands. A bump that +// moves one and leaves another shows up as a stale `npx` command or a workflow installing a version +// nobody tested (#1384), so every copy is compared with the harness pin here. +const root = new URL('../../', import.meta.url) +const read = path => readFileSync(new URL(path, root), 'utf8') + +// The version this repository pins. Bumping the DSH pin means editing this line together with the +// files below; the job name in test.yml is a merge-queue required check and does not move with it. +const DSH = '0.2.1-rc.2' +const PREVIOUS = '0.2.0-rc.2' + +test('the harness pin is the version every other copy must equal', () => { + const harness = JSON.parse(read('core/dev/harness/package.json')) + assert.equal(harness.dependencies['@deepseek-ai/dsh'], DSH) +}) + +test('the CI workflows install the pinned DSH, and the PR gate keeps its required job name', () => { + const test_yml = read('.github/workflows/test.yml') + assert.match(test_yml, new RegExp(`\\bdsh: ${DSH.replaceAll('.', '\\.')}$`, 'm'), 'test.yml dsh: input') + assert.match(test_yml, new RegExp(`\\bDSH: ${DSH.replaceAll('.', '\\.')}$`, 'm'), 'test.yml DSH: env') + // The job name is a required check on the main merge queue (#31): it keeps the old version until + // the owner changes the ruleset, so a pin bump must not rename it. + assert.match(test_yml, new RegExp(`^ name: ubuntu-latest · Node 24 · DSH ${PREVIOUS.replaceAll('.', '\\.')}$`, 'm')) + assert.ok(!test_yml.includes(`DSH ${DSH}`), 'the job name moved with the pin; the merge queue would wait forever') + + const nightly = read('.github/workflows/nightly.yml') + assert.ok(!nightly.includes(PREVIOUS), 'nightly.yml still names the previous DSH') + assert.equal(nightly.match(new RegExp(`dsh: ${DSH.replaceAll('.', '\\.')}`, 'g'))?.length, 2, 'nightly matrix entries') +}) + +test('the install help and the local CI install the pinned DSH', () => { + const doctor = read('core/scripts/install-doctor.mjs') + assert.ok(doctor.includes(`@deepseek-ai/dsh@${DSH}`), 'install-doctor.mjs default package') + assert.ok(!doctor.includes(PREVIOUS), 'install-doctor.mjs still names the previous DSH') + + const localCi = read('core/scripts/local-ci.sh') + assert.ok(localCi.includes(`npm run harness -- --no-save @deepseek-ai/dsh@${DSH}`), 'local-ci.sh harness install') + assert.ok(localCi.includes(`step="Install DSH ${DSH}"`), 'local-ci.sh step label') + assert.ok(!localCi.includes(PREVIOUS), 'local-ci.sh still names the previous DSH') +}) + +test('every npx command in the READMEs runs the pinned DSH', () => { + for (const file of ['README.md', 'README.zh.md']) { + const text = read(file) + const pins = [...text.matchAll(/@deepseek-ai\/dsh@([0-9][^\s"]*)/g)].map(match => match[1]) + assert.ok(pins.length >= 4, `${file}: expected the four npx commands`) + assert.deepEqual([...new Set(pins)], [DSH], `${file}: npx commands and the tested-with line`) + assert.ok(text.includes(DSH), `${file}: names the tested DSH version`) + } +}) + +test('the docs that tell people which DSH to run name the pinned version', () => { + for (const file of ['docs/deepseek-api-key.md', 'docs/known-issues.md', 'docs/known-issues.zh.md', 'docs/docker.md', 'docs/docker.zh.md']) { + const text = read(file) + assert.ok(text.includes(DSH), `${file}: names the pinned DSH`) + assert.ok(!text.includes(PREVIOUS), `${file}: still names the previous DSH`) + } +}) diff --git a/patches/ds-bot/files/test/install-doctor.test.js b/patches/ds-bot/files/test/install-doctor.test.js index 5819b4e1a..91bf55ab9 100644 --- a/patches/ds-bot/files/test/install-doctor.test.js +++ b/patches/ds-bot/files/test/install-doctor.test.js @@ -100,7 +100,7 @@ test('checkPluginPackage: a numeric dsh.bundle.patch fails without throwing', () test('buildInstallCommands: core, social, tree and workflow; timeline only on request (#281)', () => { const commands = buildInstallCommands() assert.equal(commands.length, 4) - assert.ok(commands.every(command => command.includes('0.2.0-rc.2'))) + assert.ok(commands.every(command => command.includes('0.2.1-rc.2'))) assert.ok(commands[0].includes('path:/core')) assert.ok(commands[1].includes('path:/plugins/social')) assert.ok(commands[2].includes('path:/plugins/tree')) diff --git a/patches/ds-bot/files/test/turn-origin.test.js b/patches/ds-bot/files/test/turn-origin.test.js index 6dae72a14..1e0c24c22 100644 --- a/patches/ds-bot/files/test/turn-origin.test.js +++ b/patches/ds-bot/files/test/turn-origin.test.js @@ -135,5 +135,5 @@ test('settled: the verdict that holds now, from the team as it is now', () => { test('#626: the no-request kind list is derived from the DSH version the tests run against', () => { const harness = new URL('../dev/harness/package.json', import.meta.url) const pinned = JSON.parse(readFileSync(harness, 'utf8')).dependencies['@deepseek-ai/dsh'] - assert.equal(pinned, '0.2.0-rc.2', 'DSH moved: re-read MessageSourceMap and update NOTICE_KINDS in src/host/turn-origin.js') + assert.equal(pinned, '0.2.1-rc.2', 'DSH moved: re-read MessageSourceMap and update NOTICE_KINDS in src/host/turn-origin.js') }) diff --git a/patches/ds-bot/topics.json b/patches/ds-bot/topics.json index 5733d92ac..4c4b5938e 100644 --- a/patches/ds-bot/topics.json +++ b/patches/ds-bot/topics.json @@ -105,7 +105,8 @@ "scripts/lint.mjs", "package-lock.json", "test/diagnostics.test.js", - "dev/smoke.mjs" + "dev/smoke.mjs", + "dev/harness/package.json" ], "0008-client-bundle": [ "client.js" From 723094f9d5f05bbe2c9daa4cc3e0e25a36854afd Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 22:44:57 +0800 Subject: [PATCH 2/3] chore(dsh): install DSH 0.2.1-rc.2 in the container image The Dockerfile still installed 0.2.0-rc.2, so docker-profile.test.mjs failed on "DSH is pinned like the tests and the README" and the package job went red at the tip: a container built from this branch would run a DSH nobody tests. Move the ARG with the harness pin the rest of the branch already carries. Fixes #1384 Docs-not-needed: only the pinned version string moves; no subsystem behavior changes. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 29c57a0a4..70e1685be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ RUN node .github/scripts/check-pack.mjs /pkgs \ # Stage 2: the runtime. FROM ${NODE_IMAGE} # DSH as pinned in the README; pnpm is what `dsh plugin` uses to manage the profile. -ARG DSH_VERSION=0.2.0-rc.2 +ARG DSH_VERSION=0.2.1-rc.2 ARG PNPM_VERSION=10.34.6 RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates git tini \ From d1a3ae7b243852a5f0477e6822851fd307d65a01 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 28 Oct 2026 22:57:41 +0800 Subject: [PATCH 3/3] chore(docker): bind the container to the host loopback under DSH 0.2.1 DSH 0.2.1-rc.2 refuses a wildcard webserver host, so the container never turns healthy and the docker build-and-start check is red: the image built and then died with `host "0.0.0.0" is an unspecified (wildcard) address`. Run compose with host networking and bind 127.0.0.1 in the web overlay, so the exposure stays the host loopback only; the entry point and the health check follow FLYBOT_PORT, and a test refuses any wildcard bind in the container configs again. Fixes #1384 --- .github/scripts/docker-profile.test.mjs | 18 ++++++++++++++++-- changelog.d/1384-dsh-pin.changed.md | 2 +- compose.yaml | 12 ++++++++---- docker/entrypoint.mjs | 7 ++++--- docker/healthcheck.mjs | 7 ++++--- docker/web.patch.yml | 12 ++++++------ docs/docker.md | 4 ++-- docs/docker.zh.md | 4 ++-- docs/subsystems/install.md | 8 ++++---- 9 files changed, 47 insertions(+), 27 deletions(-) diff --git a/.github/scripts/docker-profile.test.mjs b/.github/scripts/docker-profile.test.mjs index 91b939f2b..2346e4718 100644 --- a/.github/scripts/docker-profile.test.mjs +++ b/.github/scripts/docker-profile.test.mjs @@ -74,7 +74,9 @@ test('prepareProfile writes package.json and pnpm-workspace.yaml and keeps your test('secure defaults: loopback port, non-root user, no secret in the image, no registry push', () => { const compose = read('compose.yaml') - assert.match(compose, /^ {6}- "127\.0\.0\.1:\$\{FLYBOT_PORT:-3080\}:3080"$/m, 'the port is published on the host loopback only') + assert.match(compose, /^ {4}network_mode: host$/m, 'host networking: the server binds the host loopback itself') + assert.doesNotMatch(compose, /^\s*ports:/m, 'nothing is published — with host networking a published port would sit on every interface') + assert.match(compose, /^ {6}- FLYBOT_PORT$/m, 'the port is passed to the entry point instead of published') assert.match(compose, /^ {6}- data:\/data$/m, 'one data volume') assert.match(compose, /^ {6}- DEEPSEEK_API_KEY$/m, 'the key is passed through, never written into the file') const dockerfile = read('Dockerfile') @@ -88,5 +90,17 @@ test('secure defaults: loopback port, non-root user, no secret in the image, no for (const entry of ['**/.env', '**/.credentials.yaml', '**/.dsh*', '.git']) assert.ok(ignore.split('\n').includes(entry), `.dockerignore lists ${entry}`) const workflow = read('.github/workflows/docker.yml') assert.doesNotMatch(workflow, /docker (push|login)|docker\/(login|build-push)-action|packages: write/, 'CI builds and checks only') - assert.match(read('docker/web.patch.yml'), /^ {4}host: 0\.0\.0\.0$/m) + assert.match(read('docker/web.patch.yml'), /^ {4}host: 127\.0\.0\.1$/m, 'the server binds the host loopback') +}) + +test('no container config binds a wildcard host: DSH 0.2.1 refuses one', () => { + // 0.2.1-rc.2 answers `host "0.0.0.0" is an unspecified (wildcard) address, which is not supported` + // and the container never turns healthy, so the wildcard is a guard, not a taste. + const wildcard = /(^|[\s"'=[])(0\.0\.0\.0|\[::\]|::)(?=[\s"'\],;]|$)/m + for (const file of ['compose.yaml', 'docker/web.patch.yml', 'docker/entrypoint.mjs', 'docker/healthcheck.mjs', 'Dockerfile']) { + assert.doesNotMatch(read(file), wildcard, `${file} names one concrete address, never a wildcard`) + } + assert.match(read('docker/web.patch.yml'), /^ {4}host: 127\.0\.0\.1$/m, 'the web server binds the host loopback') + assert.match(read('compose.yaml'), /^ {4}network_mode: host$/m, 'host networking is what makes that loopback bind reachable') + assert.match(read('docker/healthcheck.mjs'), /127\.0\.0\.1:\$\{port\}/, 'the health check follows the configured port') }) diff --git a/changelog.d/1384-dsh-pin.changed.md b/changelog.d/1384-dsh-pin.changed.md index 3a9704f11..73a845b59 100644 --- a/changelog.d/1384-dsh-pin.changed.md +++ b/changelog.d/1384-dsh-pin.changed.md @@ -1 +1 @@ -FlyBot now builds, tests and installs on DeepSeek Harness `0.2.1-rc.2`, the current npm `latest`; the README commands, the CI workflows and the install help all pin that version ([#1384](https://github.com/ThinkFlowLab/FlyBot/issues/1384)). +FlyBot now builds, tests and installs on DeepSeek Harness `0.2.1-rc.2`, the current npm `latest`; the README commands, the CI workflows, the container image and the install help all pin that version ([#1384](https://github.com/ThinkFlowLab/FlyBot/issues/1384)). The container now runs with host networking and the web server binds the host's `127.0.0.1` itself, because DSH 0.2.1 refuses a wildcard host — same loopback-only exposure as before, Linux Docker only. diff --git a/compose.yaml b/compose.yaml index 721add79f..42e24a8ea 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,14 +8,18 @@ services: build: . image: flybot:${FLYBOT_TAG:-local} restart: unless-stopped - ports: - # Host loopback only. For remote access use your own HTTPS proxy or an SSH tunnel; - # never publish this port on a public address. - - "127.0.0.1:${FLYBOT_PORT:-3080}:3080" + # Host networking, and the server binds the host's 127.0.0.1 only (docker/web.patch.yml): + # DSH 0.2.1 refuses a wildcard bind, and with host networking the port is still the host's + # loopback instead of a published one. For remote access use your own HTTPS proxy or an SSH + # tunnel; never expose this port on a public address. Linux Docker only: Docker Desktop does + # not route host networking to the host. + network_mode: host environment: # Passed only when set in your shell or in .env next to this file; otherwise save the # key in the app (it is kept on the data volume). - DEEPSEEK_API_KEY + # The port the server binds on the host loopback; the entry point defaults to 3080. + - FLYBOT_PORT volumes: - data:/data volumes: diff --git a/docker/entrypoint.mjs b/docker/entrypoint.mjs index 1cca78ef4..5bdd69e83 100644 --- a/docker/entrypoint.mjs +++ b/docker/entrypoint.mjs @@ -15,9 +15,10 @@ execFileSync('pnpm', ['install', '--prefer-offline', '--silent'], { cwd: profile const workspace = join(dshHome, 'workspace') mkdirSync(workspace, { recursive: true }) -// The web server listens on all interfaces of the container only (web.patch.yml); compose -// publishes that port on the host's 127.0.0.1. The sign-in link is DSH's own ?token= link. -const args = ['--profile', 'web', '--patch', join(import.meta.dirname, 'web.patch.yml'), '--no-open', '--port', '3080', ...process.argv.slice(2)] +// The web server binds the host's 127.0.0.1 only (web.patch.yml) and compose runs the container +// with host networking, so the port is never published. The sign-in link is DSH's own ?token= link. +const port = process.env.FLYBOT_PORT || '3080' +const args = ['--profile', 'web', '--patch', join(import.meta.dirname, 'web.patch.yml'), '--no-open', '--port', port, ...process.argv.slice(2)] const child = spawn('dsh', args, { cwd: workspace, stdio: 'inherit' }) for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal)) child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0))) diff --git a/docker/healthcheck.mjs b/docker/healthcheck.mjs index 2f781924f..915326407 100644 --- a/docker/healthcheck.mjs +++ b/docker/healthcheck.mjs @@ -1,7 +1,8 @@ -// Docker health check (#352): healthy when the web server answers. Without the sign-in cookie -// it answers 401, which still proves it is up. +// Docker health check (#352): healthy when the web server answers on the host loopback. Without +// the sign-in cookie it answers 401, which still proves it is up. +const port = process.env.FLYBOT_PORT || '3080' try { - const response = await fetch('http://127.0.0.1:3080/', { redirect: 'manual', signal: AbortSignal.timeout(4000) }) + const response = await fetch(`http://127.0.0.1:${port}/`, { redirect: 'manual', signal: AbortSignal.timeout(4000) }) process.exit(response.status < 500 ? 0 : 1) } catch { process.exit(1) diff --git a/docker/web.patch.yml b/docker/web.patch.yml index 92bd36f74..d6d9de098 100644 --- a/docker/web.patch.yml +++ b/docker/web.patch.yml @@ -1,13 +1,13 @@ -# Container-only overlay (#352). Inside the container DSH listens on every interface of the -# container's own network, so the published port can reach it; compose.yaml publishes that -# port on the host's 127.0.0.1 only. Never publish it on a public address: use your own -# HTTPS proxy or an SSH tunnel (docs/docker.md). A patch replaces the entry's whole config, -# so the other values repeat what the web profile composes. +# Container-only overlay (#352). DSH 0.2.1 refuses a wildcard host, so the server binds the +# host's loopback and compose.yaml runs the container with host networking: the port stays the +# host's 127.0.0.1 only. Never expose it on a public address: use your own HTTPS proxy or an SSH +# tunnel (docs/docker.md). A patch replaces the entry's whole config, so the other values repeat +# what the web profile composes. - id: webserver inject: - webStartup config: - host: 0.0.0.0 + host: 127.0.0.1 port: !!js ctx.webStartup.port ?? 3080 compression: gzip compressionLevel: 1 diff --git a/docs/docker.md b/docs/docker.md index c964e7610..ff4cce256 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -11,12 +11,12 @@ You build the image yourself. FlyBot does not publish an image. - **One data volume**, `/data`: the DSH home (Bots, Sessions, settings, credentials), the Bots' working folder `/data/workspace` and the pnpm store. - **A normal user**, not root (uid 1000). - **No keys in the image.** You give the DeepSeek key at run time. -- **The port on `127.0.0.1` only.** For access from another machine use your own HTTPS reverse proxy or an SSH tunnel. Never publish the port on a public address: whoever reaches it with the sign-in link controls your Bots, and they can run commands. +- **The port on `127.0.0.1` only.** The container shares the host's network (`network_mode: host`) and the web server binds that loopback itself, because DSH 0.2.1 refuses a wildcard host. For access from another machine use your own HTTPS reverse proxy or an SSH tunnel. Never publish the port on a public address: whoever reaches it with the sign-in link controls your Bots, and they can run commands. - **A health check**: `docker compose ps` shows `healthy` once the web server answers. ## Start -You need Docker with the compose plugin and a clone of this repository. +You need Docker with the compose plugin on Linux — Docker Desktop for macOS and Windows does not route host networking, so `network_mode: host` there does not reach your machine — and a clone of this repository. ```sh git clone https://github.com/ThinkFlowLab/FlyBot && cd FlyBot diff --git a/docs/docker.zh.md b/docs/docker.zh.md index faf023fad..bbb122652 100644 --- a/docs/docker.zh.md +++ b/docs/docker.zh.md @@ -11,12 +11,12 @@ - **一个数据卷** `/data`:DSH 目录(Bot、会话、设置、凭据)、Bot 的工作目录 `/data/workspace` 和 pnpm 缓存。 - **普通用户运行**,不是 root(uid 1000)。 - **镜像里没有密钥。** DeepSeek Key 在运行时提供。 -- **端口只绑定 `127.0.0.1`。** 从别的机器访问请用你自己的 HTTPS 反向代理或 SSH 隧道。不要把端口暴露到公网:拿到登录链接的人就能控制你的 Bot,而 Bot 能执行命令。 +- **端口只绑定 `127.0.0.1`。** 容器使用宿主机的网络(`network_mode: host`),Web 服务自己绑定这个回环地址,因为 DSH 0.2.1 拒绝通配地址。从别的机器访问请用你自己的 HTTPS 反向代理或 SSH 隧道。不要把端口暴露到公网:拿到登录链接的人就能控制你的 Bot,而 Bot 能执行命令。 - **健康检查**:Web 服务能响应后,`docker compose ps` 显示 `healthy`。 ## 启动 -需要 Docker(带 compose 插件)和本仓库的代码。 +需要 Linux 上的 Docker(带 compose 插件)——macOS 和 Windows 的 Docker Desktop 不转发 host 网络,`network_mode: host` 在那里到不了你的机器——以及本仓库的代码。 ```sh git clone https://github.com/ThinkFlowLab/FlyBot && cd FlyBot diff --git a/docs/subsystems/install.md b/docs/subsystems/install.md index 74b5d492e..291d7b24c 100644 --- a/docs/subsystems/install.md +++ b/docs/subsystems/install.md @@ -39,10 +39,10 @@ Tooling for setup checks, the architecture-docs gate and the container image (#3 | `core/scripts/local-ci.sh` | Local run of CI's steps, including each plugin's suite. | | `core/scripts/replay.mjs` | Dev-only state-journal replay (#511): rebuilds `state.json` up to an entry from `/journal/state.jsonl`; no network or process calls, output is a new 0600 file or stdout, a bad journal is refused whole. See [core-host](core-host.md). | | `Dockerfile` | Packs `core` and the plugins (social, tree, workflow, timeline) with `check-pack.mjs` (the release tarballs), then a runtime with DSH (pinned like the README), pnpm and tini, as user `node`, one volume `/data`. | -| `compose.yaml` | One instance: port on the host's `127.0.0.1`, volume `data`, `DEEPSEEK_API_KEY` passed through only when set. | -| `docker/entrypoint.mjs`, `docker/profile.mjs` | Each start links the image's packages into `/data/profiles/web` (other plugins and `cordis.patch.yml` kept), `pnpm install`, then `dsh web` in `/data/workspace`. | -| `docker/web.patch.yml` | Container-only overlay: `webserver.host: 0.0.0.0` (the CLI refuses `--host 0.0.0.0`; inside the container it reaches only the container's network). | -| `docker/healthcheck.mjs` | Healthy when `GET /` answers (401 without the cookie). | +| `compose.yaml` | One instance: host networking (`network_mode: host`), so the server's bind on the host's `127.0.0.1` is the same loopback-only exposure a published port gave before; volume `data`, `FLYBOT_PORT` and `DEEPSEEK_API_KEY` passed through only when set. | +| `docker/entrypoint.mjs`, `docker/profile.mjs` | Each start links the image's packages into `/data/profiles/web` (other plugins and `cordis.patch.yml` kept), `pnpm install`, then `dsh web` in `/data/workspace` on `FLYBOT_PORT` (default 3080). | +| `docker/web.patch.yml` | Container-only overlay: `webserver.host: 127.0.0.1`. DSH 0.2.1 refuses a wildcard host — `host "0.0.0.0" is an unspecified (wildcard) address, which is not supported` — so the container shares the host network instead; the CLI refuses `--host 0.0.0.0` for the same reason. | +| `docker/healthcheck.mjs` | Healthy when `GET /` answers on the host loopback (401 without the cookie), following `FLYBOT_PORT`. | ## Entry points