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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Without `enabled: true` it records nothing and keeps no files; without `sidebarS

- **Node.js 22.19+** and **pnpm** (`npm install -g pnpm`, or `corepack enable`).
- **Git**, to install from GitHub.
- An API key for at least one model provider (for example DeepSeek).
- An API key for at least one model provider (for example DeepSeek: see [Set a DeepSeek API key](docs/deepseek-api-key.md)).

### 2. Install FlyBot (core + plugins)

Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ npx -y @deepseek-ai/dsh@0.2.0-rc.2 plugin --profile web add "github:ThinkFlowLab

- **Node.js 22.19+** 与 **pnpm**
- **Git**(从 GitHub 安装)
- 至少一个模型提供商的 API Key(例如 DeepSeek)
- 至少一个模型提供商的 API Key(例如 DeepSeek,设置方法见 [Set a DeepSeek API key](docs/deepseek-api-key.md),英文)

### 2. 安装 FlyBot(核心 + 插件)

Expand Down
1 change: 1 addition & 0 deletions changelog.d/451-deepseek-key-doc.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A guide, [Set a DeepSeek API key](docs/deepseek-api-key.md), explains the ways to give DeepSeek Harness your key (the key dialog, its credentials file, or `DEEPSEEK_API_KEY`), and the missing-key error and the no-model setup choices link to it ([#451](https://github.com/ThinkFlowLab/FlyBot/issues/451)).
16 changes: 13 additions & 3 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ var ZH = {
"Hi, I'm {name}. Pick a model for me first. You can change it later in my details.": "你好,我是 {name}。先给我选一个模型吧,以后可以在我的详情里换。",
"Sign in to DeepSeek": "登录 DeepSeek 账号",
"Enter a DeepSeek API key": "填入 DeepSeek API key",
"How to set a DeepSeek API key": "如何设置 DeepSeek API key",
"Open settings to set up a custom model": "打开设置配置自定义模型",
"FlyBot": "FlyBot",
"FlyBot core": "FlyBot 核心",
Expand Down Expand Up @@ -8500,7 +8501,7 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) {
}).finally(() => setBusy(""));
};
const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name });
const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react15.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [
const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react15.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react15.createElement)(KeyHelpLink, { key: "key-help" })] : [
...groupModels(models).map(([provider, list]) => (0, import_react15.createElement)(
"div",
{ key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider },
Expand Down Expand Up @@ -8566,6 +8567,10 @@ function setupChoices(actions) {
[t("Open settings to set up a custom model"), openHarnessModels]
].filter(Boolean);
}
var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md";
function KeyHelpLink() {
return (0, import_react15.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key"));
}
function missingModelKey(message) {
const text = String(message ?? "");
if (!/\bno api key\b|\bmissing (?:an? )?api key\b|\bapi key (?:is )?(?:missing|not set|not configured)\b/i.test(text)) return void 0;
Expand All @@ -8587,6 +8592,7 @@ function TurnErrorCell({ node, t: chatT, actions }) {
const title = code === "ACCOUNT_SIGNED_OUT" ? shell("message.accountStopped", t("This turn failed")) : shell("message.turnError", t("This turn failed"));
const message = missing ? t("This Bot's model has no API key yet. Set one up, then send your message again.") : code === "ACCOUNT_SIGNED_OUT" ? shell("message.failure.accountSignedOut", raw) : code === "ACCOUNT_SIGN_IN_REQUIRED" ? shell("message.failure.accountSignInRequired", raw) : code === "QUOTA" || code === "ACCOUNT_QUOTA" ? shell("message.failure.quota", raw) : code === "AUTH" ? shell("message.failure.auth", raw) : raw;
const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : [];
const deepseek = missing ? /^deepseek/i.test(missing.route) : false;
return (0, import_react15.createElement)(
"div",
{ className: "bt-turn-error", role: "status", "data-missing-key": missing ? "" : void 0 },
Expand All @@ -8601,7 +8607,8 @@ function TurnErrorCell({ node, t: chatT, actions }) {
choices.length > 0 ? (0, import_react15.createElement)(
"div",
{ className: "bt-turn-error-actions" },
choices.map(([label, run]) => (0, import_react15.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label))
choices.map(([label, run]) => (0, import_react15.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)),
deepseek ? (0, import_react15.createElement)(KeyHelpLink, { key: "key-help" }) : null
) : null
);
}
Expand Down Expand Up @@ -8634,7 +8641,10 @@ var MODEL_CSS = `
.bt-turn-error-title{margin-right:6px;font-weight:600;color:#e02135}
.bt-turn-error-message{color:var(--bt-ink-2)}
.bt-turn-error-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)}
.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;gap:6px}
.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.bt-model-help{display:inline-flex;align-items:center;min-height:28px;padding:0 4px;font-size:12px;line-height:16px;color:var(--bt-ink-2);text-decoration:underline;text-underline-offset:2px}
.bt-model-help:hover{color:var(--bt-ink)}
.bt-model-help:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px}
.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease}
.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)}
.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
Expand Down
1 change: 1 addition & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ const ZH = {
"Hi, I'm {name}. Pick a model for me first. You can change it later in my details.": '你好,我是 {name}。先给我选一个模型吧,以后可以在我的详情里换。',
'Sign in to DeepSeek': '登录 DeepSeek 账号',
'Enter a DeepSeek API key': '填入 DeepSeek API key',
'How to set a DeepSeek API key': '如何设置 DeepSeek API key',
'Open settings to set up a custom model': '打开设置配置自定义模型',
'FlyBot': 'FlyBot',
'FlyBot core': 'FlyBot 核心',
Expand Down
19 changes: 16 additions & 3 deletions core/src/client/model-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ export function ModelChoiceDock({ sessionId, useRoster, actions }) {
? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name })
: t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name })
const choices = models.length === 0
? setupChoices(actions).map(([label, run]) => h('button', { key: label, type: 'button', className: 'bt-model-chip', onClick: run }, label))
? [...setupChoices(actions).map(([label, run]) => h('button', { key: label, type: 'button', className: 'bt-model-chip', onClick: run }, label)), h(KeyHelpLink, { key: 'key-help' })]
: [
...groupModels(models).map(([provider, list]) => h('div', { key: list[0].provider ?? provider, className: 'bt-model-choice-group', role: 'group', 'aria-label': provider },
h('div', { className: 'bt-model-choice-provider', 'aria-hidden': true }, provider),
Expand Down Expand Up @@ -263,6 +263,14 @@ function setupChoices(actions) {
].filter(Boolean)
}

// How to set the key without the shell's dialog (#451): the dialog keeps only a typed or
// pasted value, so the hint under the DeepSeek choices links FlyBot's own guide.
export const KEY_HELP_URL = 'https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md'

export function KeyHelpLink() {
return h('a', { className: 'bt-model-help', href: KEY_HELP_URL, target: '_blank', rel: 'noopener noreferrer' }, t('How to set a DeepSeek API key'))
}

// A run that failed because its model has no credential names the route DSH could
// not reach (llm-deepseek: `no API key for provider route "deepseek-official"`).
// `undefined` for any other failure; `{ route }` (route may be '') for a missing key.
Expand Down Expand Up @@ -300,14 +308,16 @@ export function TurnErrorCell({ node, t: chatT, actions }) {
: code === 'QUOTA' || code === 'ACCOUNT_QUOTA' ? shell('message.failure.quota', raw)
: code === 'AUTH' ? shell('message.failure.auth', raw) : raw
const choices = missing ? modelKeyChoices(actions ?? {}, missing.route) : []
const deepseek = missing ? /^deepseek/i.test(missing.route) : false
return h('div', { className: 'bt-turn-error', role: 'status', 'data-missing-key': missing ? '' : undefined },
h('span', { className: 'bt-turn-error-dot', 'aria-hidden': true }),
h('div', { className: 'bt-turn-error-copy' },
h('span', { className: 'bt-turn-error-title' }, title),
h('span', { className: 'bt-turn-error-message' }, message),
code ? h('code', { className: 'bt-turn-error-code' }, code) : null),
choices.length > 0 ? h('div', { className: 'bt-turn-error-actions' },
choices.map(([label, run]) => h('button', { key: label, type: 'button', className: 'bt-model-chip', onClick: run }, label))) : null)
choices.map(([label, run]) => h('button', { key: label, type: 'button', className: 'bt-model-chip', onClick: run }, label)),
deepseek ? h(KeyHelpLink, { key: 'key-help' }) : null) : null)
}

// The shell's own DeepSeek sign-in dialog, borrowed from its account plugin: the
Expand Down Expand Up @@ -340,7 +350,10 @@ export const MODEL_CSS = `
.bt-turn-error-title{margin-right:6px;font-weight:600;color:#e02135}
.bt-turn-error-message{color:var(--bt-ink-2)}
.bt-turn-error-code{margin-left:6px;font-size:11px;color:var(--bt-ink-3)}
.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;gap:6px}
.bt-turn-error-actions{grid-column:2;display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.bt-model-help{display:inline-flex;align-items:center;min-height:28px;padding:0 4px;font-size:12px;line-height:16px;color:var(--bt-ink-2);text-decoration:underline;text-underline-offset:2px}
.bt-model-help:hover{color:var(--bt-ink)}
.bt-model-help:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px}
.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease}
.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)}
.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
Expand Down
16 changes: 15 additions & 1 deletion core/test/model-menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ runInNewContext(bundle.outputFiles[0].text, {
return platform[name]
},
})
const { groupModels, ModelChoiceDock, effortRows, TurnErrorCell, missingModelKey } = module.exports
const { groupModels, ModelChoiceDock, effortRows, TurnErrorCell, missingModelKey, KeyHelpLink, KEY_HELP_URL } = module.exports

function nodes(tree, match) {
if (!tree || typeof tree !== 'object') return []
Expand Down Expand Up @@ -89,6 +89,8 @@ test('an empty catalog offers setup choices, and a ready model needs no choice d
assert.deepEqual(nodes(tree, node => node.type === 'button').map(button => button.children[0]), [
'Enter a DeepSeek API key', 'Open settings to set up a custom model',
])
// The shell's key dialog keeps only a typed or pasted key, so the guide is linked too (#451).
assert.equal(nodes(tree, node => node.type === KeyHelpLink).length, 1)
assert.equal(dock(models, { model: models[0].ref }).tree, null)
assert.equal(dock(models, { ready: false }).tree, null)
})
Expand Down Expand Up @@ -125,13 +127,15 @@ test('a run that failed for a missing API key offers the model setup right under
assert.doesNotMatch(text(tree), /DEEPSEEK_API_KEY|launching environment/)
const labels = nodes(tree, node => node.type === 'button').map(button => button.children[0])
assert.deepEqual([...labels], ['Enter a DeepSeek API key', 'Open settings to set up a custom model'])
assert.equal(nodes(tree, node => node.type === KeyHelpLink).length, 1)
// The raw provider text is never drawn, not even on hover (a provider may echo a key).
assert.equal(nodes(tree, node => node.props.className === 'bt-turn-error-message')[0].props.title, undefined)
const echoed = TurnErrorCell({ node: { data: { message: 'No API key found for sk-abc123', code: 'AUTH' } }, t: chatT, actions: {} })
assert.ok(!JSON.stringify(echoed).includes('sk-abc123'))
// Another route opens the model settings; any other failure reads as DSH writes it, no buttons.
const other = TurnErrorCell({ node: { data: { message: 'no API key for provider route "openrouter"' } }, t: chatT, actions: {} })
assert.deepEqual([...nodes(other, node => node.type === 'button').map(button => button.children[0])], ['Manage models'])
assert.equal(nodes(other, node => node.type === KeyHelpLink).length, 0)
// A provider that does not name its route is not assumed to be DeepSeek.
const unnamed = TurnErrorCell({ node: { data: { message: 'API key not configured' } }, t: chatT, actions: {} })
assert.deepEqual([...nodes(unnamed, node => node.type === 'button').map(button => button.children[0])], ['Manage models'])
Expand All @@ -142,3 +146,13 @@ test('a run that failed for a missing API key offers the model setup right under
const plain = TurnErrorCell({ node: { data: { message: 'socket hang up' } }, t: chatT, actions: {} })
assert.match(text(plain), /socket hang up/)
})

test('the DeepSeek key guide opens FlyBot\'s doc in a new tab (#451)', () => {
const link = KeyHelpLink()
assert.equal(link.type, 'a')
assert.equal(link.props.href, KEY_HELP_URL)
assert.match(KEY_HELP_URL, /^https:\/\/github\.com\/ThinkFlowLab\/FlyBot\/blob\/main\/docs\/deepseek-api-key\.md$/)
assert.equal(link.props.target, '_blank')
assert.equal(link.props.rel, 'noopener noreferrer')
assert.deepEqual([...link.children], ['How to set a DeepSeek API key'])
})
58 changes: 58 additions & 0 deletions docs/deepseek-api-key.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Set a DeepSeek API key

A Bot whose model is on the DeepSeek API route (`deepseek-official`, for example **DeepSeek-V41-Flash**) needs a DeepSeek API key. Without one, its turn fails with **This Bot's model has no API key yet** (`MISSING_CREDENTIAL`).

FlyBot does not keep the key. DeepSeek Harness (DSH) stores it and sends it with each model request, under the name `DEEPSEEK_API_KEY`. Get a key on the [DeepSeek platform](https://platform.deepseek.com/api_keys). On DSH Desktop, **Sign in to DeepSeek** is another way: it uses your DeepSeek account and needs no key.

There are three ways to set the key. Pick one.

## 1. In the app

1. Under the error, click **Enter a DeepSeek API key** (a Bot with no model offers the same button). You can also open Settings → **Models** and set up DeepSeek there.
2. **Type or paste** the key into the **API key** field.
3. Click **Save and continue**, then send your message again.

The dialog belongs to DSH. It keeps only what you type or paste: a value that a script, an accessibility tool or some password managers put into the field can be cleared when you click **Save and continue**, and nothing is saved ([#451](https://github.com/ThinkFlowLab/FlyBot/issues/451)). In that case paste the key by hand, or use way 2 or 3.

## 2. In the credentials file

The dialog saves the key to a file in your DSH home folder. You can write that file yourself, for example on a server or before the first start:

- `~/.dsh/.credentials.yaml`, or `.credentials.yaml` in `DSH_HOME` when it is set
- Windows: `C:\Users\<user>\.dsh\.credentials.yaml`

```yaml
version: 1
refs:
DEEPSEEK_API_KEY: sk-your-key
```

If the file already exists, add the `DEEPSEEK_API_KEY` line under `refs` and keep the other entries. Only you may read the file: on Linux and macOS run `chmod 600 ~/.dsh/.credentials.yaml`, because DSH refuses to load a file that other users can read. DSH also refuses a file with anything else at the top level. A running DSH picks up the change by itself; no restart is needed.

## 3. In the environment

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
```

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.

A `.env` file with a `DEEPSEEK_API_KEY=sk-your-key` line also works, either in the folder you start DSH from or in your DSH home folder (`~/.dsh/.env`). A key saved in the app or in the credentials file wins over both.

## Which key DSH uses

The first place that has a key wins:

| Place | Set by |
| --- | --- |
| The environment DSH was started in | `DEEPSEEK_API_KEY=… dsh …` |
| The credentials file | the app (way 1) or you (way 2) |
| `.env` in the folder you start DSH from | you |
| `.env` in your DSH home folder | you |

## Keep the key safe

- Never send the key to a Bot in a chat, and never commit a `.env` or `.credentials.yaml` file.
- Bots run as your own OS user, so a Bot that runs commands could read the credentials file. Use a key that you can revoke on the DeepSeek platform.
Loading
Loading