Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/scripts/docker-profile.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
})
8 changes: 4 additions & 4 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -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`
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,23 +105,23 @@ 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
`/plugins/workflow` or `/plugins/timeline` the same way if you want them.

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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,23 @@ 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`),后两行才把它变成一支团队。想要工作流或时间线,照样再加
`/plugins/workflow`、`/plugins/timeline`。

桌面版:**插件 → 添加插件**,一次粘一个 `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 打个招呼。想让它在一台一直开着
Expand Down Expand Up @@ -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。

## 许可

Expand Down
1 change: 1 addition & 0 deletions changelog.d/1384-dsh-pin.changed.md
Original file line number Diff line number Diff line change
@@ -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, 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.
12 changes: 8 additions & 4 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion core/dev/harness/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
2 changes: 1 addition & 1 deletion core/scripts/install-doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'] : [])]
Expand Down
4 changes: 2 additions & 2 deletions core/scripts/local-ci.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==="
Expand Down
2 changes: 1 addition & 1 deletion core/src/host/turn-origin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions core/test/about.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ 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', () => {
const view = page({ ...base, build: { branch: 'main', commit: '082563a' }, behind: null })
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/)
})
Expand Down Expand Up @@ -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()
}
Expand Down
63 changes: 63 additions & 0 deletions core/test/dsh-pin.test.js
Original file line number Diff line number Diff line change
@@ -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`)
}
})
2 changes: 1 addition & 1 deletion core/test/install-doctor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
2 changes: 1 addition & 1 deletion core/test/turn-origin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
Loading
Loading