dsh-connect 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +119 -18
- package/README.zh.md +87 -13
- package/client/client.js +18 -15
- package/client/client.js.map +2 -2
- package/client/settings-client.mjs +22 -13
- package/lib/channels/dingtalk/index.d.ts +49 -49
- package/lib/channels/dingtalk/index.d.ts.map +1 -1
- package/lib/channels/feishu/adapter.d.ts +8 -1
- package/lib/channels/feishu/adapter.d.ts.map +1 -1
- package/lib/channels/feishu/adapter.js +120 -39
- package/lib/channels/feishu/adapter.js.map +1 -1
- package/lib/channels/feishu/index.d.ts +27 -27
- package/lib/channels/feishu/index.d.ts.map +1 -1
- package/lib/channels/telegram/index.d.ts +13 -13
- package/lib/channels/telegram/index.d.ts.map +1 -1
- package/lib/channels/web/adapter.d.ts +3 -1
- package/lib/channels/web/adapter.d.ts.map +1 -1
- package/lib/channels/web/adapter.js +3 -1
- package/lib/channels/web/adapter.js.map +1 -1
- package/lib/channels/web/index.d.ts +5 -5
- package/lib/channels/web/index.d.ts.map +1 -1
- package/lib/index.d.ts +42 -58
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +162 -57
- package/lib/index.js.map +1 -1
- package/lib/interaction.d.ts +90 -50
- package/lib/interaction.d.ts.map +1 -1
- package/lib/interaction.js +233 -272
- package/lib/interaction.js.map +1 -1
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +7 -1
- package/lib/retry.js.map +1 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +0 -2
- package/lib/service.js.map +1 -1
- package/lib/settings/legacy-import.d.ts +190 -0
- package/lib/settings/legacy-import.d.ts.map +1 -0
- package/lib/settings/legacy-import.js +373 -0
- package/lib/settings/legacy-import.js.map +1 -0
- package/lib/settings/namespace.d.ts +204 -116
- package/lib/settings/namespace.d.ts.map +1 -1
- package/lib/settings/namespace.js +235 -148
- package/lib/settings/namespace.js.map +1 -1
- package/lib/settings/settings-model.d.ts.map +1 -1
- package/lib/settings/settings-model.js +10 -1
- package/lib/settings/settings-model.js.map +1 -1
- package/lib/settings/settings-service.d.ts +13 -0
- package/lib/settings/settings-service.d.ts.map +1 -1
- package/lib/settings/settings-service.js +73 -4
- package/lib/settings/settings-service.js.map +1 -1
- package/lib/types.d.ts +14 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +12 -11
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record with:
|
|
4
4
|
# git hash-object README.md README.zh.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 66b5e97ca9fcfabb7c9d9a63d9f7d6c610185c39
|
|
6
|
+
README.zh.md: c3d1f3118d2c4e45c4cea67436df869c1a3f87ac
|
package/README.md
CHANGED
|
@@ -25,10 +25,10 @@ The **all-in-one plugin** for connecting [DeepSeek Harness](https://github.com/d
|
|
|
25
25
|
|
|
26
26
|
| Aspect | Value |
|
|
27
27
|
|---|---|
|
|
28
|
-
| DSH version | `^0.
|
|
28
|
+
| DSH version | `^0.2.0-rc.2` (peer `@deepseek-ai/dsh-agent`, `dsh-llm`, `dsh-session`) |
|
|
29
29
|
| Cordis | `^4.0.1` |
|
|
30
30
|
| Node.js | ≥ 20 (ESM, `NodeNext`) |
|
|
31
|
-
| Last verified | **2026-
|
|
31
|
+
| Last verified | **2026-10-02** against DSH `0.2.0-rc.2` on Windows (host load, Feishu WebSocket transport, web-settings pane) |
|
|
32
32
|
|
|
33
33
|
**Keep the peer range in step with the host.** Upstream ships no changelog or
|
|
34
34
|
migration guide, so a stale range is the only thing standing between this plugin
|
|
@@ -39,6 +39,11 @@ on the same line. When you upgrade DSH, bump `peerDependencies` (and
|
|
|
39
39
|
`tsc`, and re-run the test suite — a range that no longer overlaps the host
|
|
40
40
|
version is the signal that the bridge needs another migration.
|
|
41
41
|
|
|
42
|
+
DSH refuses to load a plugin whose range does not cover it, so a stale range is
|
|
43
|
+
at least loud: installing `0.9.0` on `0.2.0-rc.2` is rejected with *"may cause
|
|
44
|
+
crashes or data loss"* before anything runs. `0.9.2` is the version that covers
|
|
45
|
+
`0.2.0-rc.2`; see [Upgrading from 0.9.0](#upgrading-from-090).
|
|
46
|
+
|
|
42
47
|
The plugin runs on the DSH **Host plane** (process-level singleton services), not inside an agent preset.
|
|
43
48
|
|
|
44
49
|
## Install / Uninstall
|
|
@@ -98,6 +103,18 @@ rm -f ~/.dsh/.dsh-connect/feishu-credentials.json
|
|
|
98
103
|
|
|
99
104
|
A fully reproducible example is the [`examples/`](examples/) folder plus the repository's [Feishu setup manual](https://github.com/IvanWu2015/dsh-connect/blob/main/docs/feishu-setup.md) (Feishu app creation, event subscriptions, publishing).
|
|
100
105
|
|
|
106
|
+
## Questions and approvals in a conversation
|
|
107
|
+
|
|
108
|
+
When the agent needs a decision from you it stops and asks, and the asking happens in the chat — no switching to the Web GUI:
|
|
109
|
+
|
|
110
|
+
- **A question with options** renders as a card with one button per option. One tap answers it and the card immediately moves on to the next question.
|
|
111
|
+
- **A question without options** has no buttons to offer: the bot sends the question as a prompt and you **reply in the chat**. That message is the answer.
|
|
112
|
+
- **A tool approval** (an action that needs your go-ahead) is a card too, with **Allow once** / **Reject** buttons. This one accepts **only a tap** — a plain chat message sent while an approval is waiting is not recorded as its result.
|
|
113
|
+
|
|
114
|
+
One chat holds one pending card at a time. A second request is handed back to the host's own path (the Web GUI) rather than fighting the first card for the same message — and so is a card that could not be delivered, or a request cancelled before you answered; in each case the chat is **released**, because a leaked pending entry would silently swallow your next message.
|
|
115
|
+
|
|
116
|
+
"This action is no longer active" means the card has expired — it auto-closes after 60 s idle, so ask again. A double tap, or a tap landing right after the previous question was answered, falls inside the card's redraw window and is ignored silently rather than misreported as expired.
|
|
117
|
+
|
|
101
118
|
## Configuration
|
|
102
119
|
|
|
103
120
|
Configuration lives in the DSH profile patch (`cordis.patch.yml`) under the plugin's `config:`. `dsh.shared.config.json` in the project root (or its parent) can supply workspace/state defaults that take precedence for those keys.
|
|
@@ -125,7 +142,7 @@ Configuration lives in the DSH profile patch (`cordis.patch.yml`) under the plug
|
|
|
125
142
|
|---|---|---|
|
|
126
143
|
| `channels` | all built-in | Which channels to activate: `feishu` / `telegram` / `dingtalk` / `web`. Omit to activate all built-in channels. |
|
|
127
144
|
| `channelDefaults` | `{}` | Keys applied to every channel that doesn't set its own (e.g. `{ language: "zh" }`). |
|
|
128
|
-
| `settingsStatePath` | `<stateDir>/dsh-connect-settings.json` | Where the web-settings pane mirrors non-secret config. Defaults to `dsh-connect-settings.json` *inside* `stateDir`, so it lands beside `bindings.json` and can never disagree with the stores; set it to override.
|
|
145
|
+
| `settingsStatePath` | `<stateDir>/dsh-connect-settings.json` | Where the web-settings pane mirrors non-secret config. Defaults to `dsh-connect-settings.json` *inside* `stateDir`, so it lands beside `bindings.json` and can never disagree with the stores; set it to override. The pane's authoritative store is this plugin's entry in the active profile patch (see [User settings](#user-settings)); this file is only the compatibility mirror the legacy `/dsh-connect` RPC reads and writes, and a pre-0.2 install's copy of it is read back once by the upgrade (see [Upgrading from 0.9.0](#upgrading-from-090)). |
|
|
129
146
|
|
|
130
147
|
### `feishu` (Feishu / Lark channel)
|
|
131
148
|
|
|
@@ -186,7 +203,8 @@ Environment variables (`FEISHU_*`, `TELEGRAM_*`, `DINGTALK_*`, `DSH_CONNECT_STAT
|
|
|
186
203
|
- **Files written**
|
|
187
204
|
- `<stateDir>/bindings.json` (default `.dsh-connect/`) — the chat ⇄ session route store (chat keys, session ids, mirror and lock state).
|
|
188
205
|
- `<stateDir>/dsh-connect-settings.json` (default `.dsh-connect/`) — the non-secret compatibility mirror, see `settingsStatePath`.
|
|
189
|
-
-
|
|
206
|
+
- `<profile dir>/.dsh-connect-legacy-imported` — a marker recording that the one-shot upgrade import ran. It sits beside the profile entry the import writes to (see [Upgrading from 0.9.0](#upgrading-from-090)), and its contents are a sentence saying where the settings came from; nothing is stored in it.
|
|
207
|
+
- this plugin's entry in the active **profile patch** (`profileContext.patchPath`, `cordis.patch.yml`) — written through DSH's first-party `settings` service (atomic, file-locked, comment-preserving).
|
|
190
208
|
- `~/.dsh/.dsh-connect/feishu-credentials.json` — **legacy**, read-only. One-click onboarding used to save Feishu credentials here instead of the credential store, so a user who had just scanned the QR code still saw `未配置凭据` forever. Onboarding now writes to the credential store, and an existing install is backfilled from this file once on boot; after that it is never read or written again.
|
|
191
209
|
- `<workDir>/.dsh-connect-images/` — user images/attachments staged for the agent's tools.
|
|
192
210
|
- DSH's own session logs and settings under `~/.dsh/` (sessions, settings, etc.).
|
|
@@ -223,23 +241,39 @@ opens it too.
|
|
|
223
241
|
|
|
224
242
|
## User settings
|
|
225
243
|
|
|
226
|
-
The web settings pane (`dsh-connect` under **Settings**) edits
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
244
|
+
The web settings pane (`dsh-connect` under **Settings**) edits **this plugin's
|
|
245
|
+
entry in the active profile patch** — the same
|
|
246
|
+
`~/.dsh/profiles/<profile>/cordis.patch.yml` you would edit by hand — through
|
|
247
|
+
DSH's own first-party `settings` service. That service writes atomically under a
|
|
248
|
+
file lock and preserves your comments, and the loader hot-reloads the result, so
|
|
249
|
+
a save takes effect without a restart.
|
|
250
|
+
|
|
251
|
+
The fields the pane owns are declared `volatile` in the plugin's config schema.
|
|
252
|
+
That declaration is what makes a save *reconcile* instead of remounting: the
|
|
253
|
+
loader hands the plugin a live reference for each declared field, and a settings
|
|
254
|
+
write commits them in place, so the running adapters pick the values up on their
|
|
255
|
+
next message. Fields the pane does not own are simply not declared — which is
|
|
256
|
+
the mechanism that keeps it from writing them.
|
|
231
257
|
|
|
232
258
|
Values resolve in three layers, most specific last:
|
|
233
259
|
|
|
234
260
|
1. the schema defaults shipped with the plugin;
|
|
235
|
-
2. the plugin
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
2. the config the plugin is composed with (its inherited entry);
|
|
262
|
+
3. this plugin's entry in the active profile patch — both what you hand-write
|
|
263
|
+
there and what the pane saves.
|
|
264
|
+
|
|
265
|
+
A save is projected onto the declared fields only, so an undeclared key — a
|
|
266
|
+
credential, `settingsStatePath`, something you added yourself — cannot reach the
|
|
267
|
+
document even if a caller sends it. The converse is load-bearing too: the host
|
|
268
|
+
resets a declared field that an update *omits* to its inherited value, so a save
|
|
269
|
+
always writes the complete declared section. Undeclared keys you hand-wrote in
|
|
270
|
+
that entry are preserved across a pane save, untouched.
|
|
271
|
+
|
|
272
|
+
**The pane never writes credentials.** A profile patch is a plain document users
|
|
273
|
+
are invited to paste into bug reports, so a secret you type into the pane goes to
|
|
274
|
+
the DSH credential store (`ctx.credentials`) instead — which is also where
|
|
275
|
+
one-click onboarding and the `FEISHU_*`-style environment variables put them. A
|
|
276
|
+
secret you hand-wrote in the entry yourself is left where it is.
|
|
243
277
|
|
|
244
278
|
The legacy `/dsh-connect` HTTP RPC is retained for panel compatibility; it now
|
|
245
279
|
reads and writes the same namespace, and mirrors non-secret config to
|
|
@@ -296,12 +330,71 @@ bot as unconfigured:
|
|
|
296
330
|
So a DingTalk bot using only webhook push (no stream credentials) is correctly
|
|
297
331
|
reported as configured, as is one using only stream mode.
|
|
298
332
|
|
|
333
|
+
## Upgrading from 0.9.0
|
|
334
|
+
|
|
335
|
+
Applies to the in-repo `0.9.1` as well — it was committed but never published
|
|
336
|
+
to npm, so `0.9.0` is the version users are actually upgrading from.
|
|
337
|
+
|
|
338
|
+
DSH 0.2 keeps per-plugin settings in the **profile patch**, not in
|
|
339
|
+
`$DSH_HOME/settings.yaml`, and it does not know the old document's `dsh-connect:`
|
|
340
|
+
section — its own migration renames that file and imports the sections it
|
|
341
|
+
recognises, leaving ours to be dropped with a warning. Without help, an
|
|
342
|
+
upgrading user's channels keep working (their config is in the patch) but every
|
|
343
|
+
pane-only choice silently reverts to its default the first time the pane opens.
|
|
344
|
+
|
|
345
|
+
So **0.9.2 imports it once, on the first boot after the upgrade**:
|
|
346
|
+
|
|
347
|
+
- It looks for the `dsh-connect:` section in `$DSH_HOME/settings.yaml` first,
|
|
348
|
+
then in `settings.yaml.imported` (where the host's own migration renames the
|
|
349
|
+
document, and which may have happened before or after this ran), and finally in
|
|
350
|
+
this plugin's own `dsh-connect-settings.json` — the fallback store a user who
|
|
351
|
+
never had a live settings peer would be carrying all their choices in.
|
|
352
|
+
- The section is projected onto the fields the pane owns, so **credentials cannot
|
|
353
|
+
travel**: they are in the credential store, and anything else in that file
|
|
354
|
+
stays where it is.
|
|
355
|
+
- It **merges, it does not replace.** The values in force are the base and the
|
|
356
|
+
legacy values are layered on top, because the host resets a declared field that
|
|
357
|
+
an update omits — an import carrying only per-channel keys would otherwise
|
|
358
|
+
clear `channels` and switch every adapter off.
|
|
359
|
+
- It writes through the same path a pane save uses, so the running adapters
|
|
360
|
+
reconcile immediately — no restart.
|
|
361
|
+
- **Nothing is deleted or renamed.** Unlike the host's own import, ours never
|
|
362
|
+
writes to `settings.yaml`.
|
|
363
|
+
- The outcome is recorded once in a marker named after the profile entry itself
|
|
364
|
+
(`profileContext.patchPath`), so it lives at
|
|
365
|
+
`<profile dir>/.dsh-connect-legacy-imported` — including the benign "there was
|
|
366
|
+
nothing to import" case, so a later boot cannot re-apply the old values over
|
|
367
|
+
edits you have made since. The anchor is the entry, not the state file it used
|
|
368
|
+
to sit beside: the state path is yours to move (`stateDir`,
|
|
369
|
+
`DSH_CONNECT_STATE_DIR`, `settingsStatePath`) and to delete, and either would
|
|
370
|
+
have made the next boot believe the import had never run. A profile directory
|
|
371
|
+
moves only if the profile itself does, which is the one case where re-importing
|
|
372
|
+
is right.
|
|
373
|
+
|
|
374
|
+
The retry rule is narrower than "any failure is retried", and deliberately so.
|
|
375
|
+
Three outcomes are final and get the marker: a document that is not there, a
|
|
376
|
+
document with no `dsh-connect:` section, and a successful import. Everything else
|
|
377
|
+
— a document that will not parse, one that is **there but cannot be read**
|
|
378
|
+
(a permission, or a `settings.yaml` that is really a directory), a refused write,
|
|
379
|
+
a host with no settings service — imports nothing, **leaves both files alone, and
|
|
380
|
+
writes no marker**, so the next boot simply tries again once you have fixed the
|
|
381
|
+
cause. That last one is why "not there" and "could not be read" are told apart:
|
|
382
|
+
until `0.9.2` they were the same outcome, so a single unreadable document ended
|
|
383
|
+
the migration for good, silently.
|
|
384
|
+
|
|
385
|
+
Each of those failures is one `connect: …` line naming the file and the reason;
|
|
386
|
+
the troubleshooting table below lists them. A marker that cannot be **written** is
|
|
387
|
+
reported too — without it every boot would re-run the migration and layer the
|
|
388
|
+
legacy values back over your newer edits. If you never used the pane, none of
|
|
389
|
+
this is visible.
|
|
390
|
+
|
|
299
391
|
## Troubleshooting
|
|
300
392
|
|
|
301
393
|
Logs come from the DSH host logger (run `dsh web` in a terminal); plugin messages are prefixed `connect:` / `connect-feishu:`.
|
|
302
394
|
|
|
303
395
|
| Symptom | Likely cause / fix |
|
|
304
396
|
|---|---|
|
|
397
|
+
| Installing `dsh-connect@0.9.0` on DSH `0.2.0-rc.2` is refused: *"`dsh-connect@0.9.0` 与 DSH `0.2.0-rc.2` 不兼容 … 运行它可能导致崩溃或数据丢失"* | Not a bug and not a warning to click past: DSH's compatibility gate rejects any plugin whose declared peer range does not cover the running host, and `0.9.0` predates the `0.2.0` line. Install **`0.9.2`** (or newer), whose peers require `^0.2.0-rc.2`. |
|
|
305
398
|
| `connect-feishu: adapter init failed` / `start failed` | Bad credentials, app not published, or network blocked. Check `appId`/`appSecret`, re-run onboarding, verify the bot is online in the Feishu console. |
|
|
306
399
|
| `connect: resume of <id> failed, creating fresh session` | The persisted session could not be resumed (missing workdir, persistence issue). Check `workDir` and `~/.dsh/sessions`. |
|
|
307
400
|
| The bot answers every message with a raw `agent-presets: preset "…" not found` line, and nothing reaches the agent | A stale `agent-presets.default` in `$DSH_HOME/settings.yaml` names an id no installed build ships. Fixed in **0.9.0**, which retries `standard` and logs the decision rather than failing the turn; on an older build, set the key to a shipped id (`standard`). |
|
|
@@ -310,7 +403,15 @@ Logs come from the DSH host logger (run `dsh web` in a terminal); plugin message
|
|
|
310
403
|
| `[用户发送了图片,但下载失败…]` | Feishu `im:resource` permission is missing on the app; grant it and re-approve. |
|
|
311
404
|
| Streaming reply is one unbroken blob | Fixed in **0.9.0**: block boundaries and the reasoning/answer split now insert blank lines (and reasoning soft breaks are expanded for Feishu cards). Upgrade, then restart `dsh web`. |
|
|
312
405
|
| Card frozen on "Thinking…" with no progress on a long task | Fixed in **0.9.0**: reasoning now streams live, tool calls show as `🔧` progress lines, and a liveness heartbeat updates the card during silent stretches. Upgrade, then restart `dsh web`. |
|
|
313
|
-
|
|
|
406
|
+
| The agent offers options / asks for tool approval and nothing appears in Feishu | Fixed in **0.9.2** (the in-repo `0.9.1` was never published): the bridge subscribed to a host service that does not exist, so every question fell silently back to the host. Upgrade, then restart `dsh web`. |
|
|
407
|
+
| Tapping a card button says it is no longer active, on a card that was just posted | Fixed in **0.9.2** (the in-repo `0.9.1` was never published): a tap landing while the card was being redrawn (a double tap, or one right after the previous question was answered) was misread as a stale action. Upgrade, then restart `dsh web`. |
|
|
408
|
+
| `connect: the legacy settings at <path> could not be parsed …` | The pre-0.2 document has a YAML error, so the one-shot migration ([Upgrading from 0.9.0](#upgrading-from-090)) skipped it and left it in place. Fix the YAML and restart; nothing is imported until then, and no marker is written, so the retry is automatic. |
|
|
409
|
+
| `connect: could not import the legacy dsh-connect settings from <path> …` | The migration found the section but DSH refused the write (usually a value that fails validation). The section is still in the file — fix the named field and restart. |
|
|
410
|
+
| `connect: could not read the legacy settings candidate at <path> …` | The candidate is *there* but could not be read: a permission, a path that is really a directory, or a `~` in `$DSH_HOME` that was not expanded. This is the one failure that does **not** mark the migration done — nothing is imported, nothing is marked, and the next start retries on its own, so fixing the cause is all that is needed. The distinction from "not there" is the point: the two were the same outcome until 0.9.2, so **a single `EACCES` ended the entire migration silently and permanently**. |
|
|
411
|
+
| `connect: could not write the one-shot import marker at <path> …` | The import itself succeeded; the marker that records it could not be written (usually a read-only home). Not harmless: with no marker every start re-runs the whole migration and layers the legacy values back over whatever you changed in the pane after upgrading — which shows up as settings reverting on their own. Fix the profile directory's write permission, or create the marker file by hand. |
|
|
412
|
+
| `connect: the import marker at <path> could not be read …` | A marker exists but cannot be read. It is treated as **already imported** and reported: better to skip an import than to re-apply old values over your newer settings. Delete the marker and restart to trigger the import again. |
|
|
413
|
+
| Saving the pane fails with *`Configuration for "connect" is overridden by a home patch or command-line overlay`* | The pane writes the profile patch, but resolution layers `bundle → profile → $DSH_HOME/cordis.patch.yml → --patch`, so a value set in one of the last two wins over anything the pane saves and DSH refuses the write rather than let a save that could never take effect look successful. Edit the home patch (or drop the overlay) if you want the pane to own these settings. |
|
|
414
|
+
| Menu cards don't update / expire | Cards auto-close after 60 s idle by design; re-open the menu. Question and approval cards behave the same — see [Questions and approvals in a conversation](#questions-and-approvals-in-a-conversation). |
|
|
314
415
|
|
|
315
416
|
**Rollback** — reinstall a previous release (`dsh plugin --profile web add dsh-connect@<version>` after removing the current one), or `git checkout` the pinned commit in a source install.
|
|
316
417
|
|
package/README.zh.md
CHANGED
|
@@ -25,13 +25,15 @@
|
|
|
25
25
|
|
|
26
26
|
| 方面 | 值 |
|
|
27
27
|
|---|---|
|
|
28
|
-
| DSH 版本 | `^0.
|
|
28
|
+
| DSH 版本 | `^0.2.0-rc.2`(peer `@deepseek-ai/dsh-agent`、`dsh-llm`、`dsh-session`) |
|
|
29
29
|
| Cordis | `^4.0.1` |
|
|
30
30
|
| Node.js | ≥ 20(ESM,`NodeNext`) |
|
|
31
|
-
| 最后验证 | **2026-
|
|
31
|
+
| 最后验证 | **2026-10-02**,在 Windows 上针对 DSH `0.2.0-rc.2` 验证(宿主加载、飞书 WebSocket 传输、Web 设置面板) |
|
|
32
32
|
|
|
33
33
|
**peer 版本线必须与宿主保持同步。** 上游不提供 changelog 或迁移说明,因此过期的版本范围是插件与静默损坏之间唯一的屏障:DSH `0.1.5-rc.2` 直接删除了 `Session.events` 访问器和 `assistant/chunk` 事件类型,而所有 `dsh-*` 包共用同一条版本线。升级 DSH 时,请把 `dsh-agent`、`dsh-llm`、`dsh-session` 的 `peerDependencies`(以及 `devDependencies`)**一起**上调,重新运行 `tsc`,并重跑测试套件 —— 当版本范围与宿主版本不再有交集时,就是桥接需要再次迁移的信号。
|
|
34
34
|
|
|
35
|
+
DSH 会拒绝加载版本范围覆盖不到自己的插件,所以过期的范围至少是响亮的:在 `0.2.0-rc.2` 上安装 `0.9.0` 会在任何代码运行之前被拒绝,并提示 *“可能导致崩溃或数据丢失”*。覆盖 `0.2.0-rc.2` 的版本是 `0.9.2`,见[从 0.9.0 升级](#从-090-升级)。
|
|
36
|
+
|
|
35
37
|
插件运行在 DSH **Host 平面**(进程级单例服务)上,而不是在智能体预设内部。
|
|
36
38
|
|
|
37
39
|
## 安装 / 卸载
|
|
@@ -91,6 +93,18 @@ rm -f ~/.dsh/.dsh-connect/feishu-credentials.json
|
|
|
91
93
|
|
|
92
94
|
一个完全可复现的示例是 [`examples/`](examples/) 文件夹加上仓库里的[飞书配置手册](https://github.com/IvanWu2015/dsh-connect/blob/main/docs/feishu-setup.zh.md)(飞书应用创建、事件订阅、发布)。
|
|
93
95
|
|
|
96
|
+
## 对话中的提问与授权
|
|
97
|
+
|
|
98
|
+
智能体需要你拍板时会停下来问你,这些问答直接出现在飞书里,不用切到 Web GUI:
|
|
99
|
+
|
|
100
|
+
- **带选项的问题** —— 渲染成一张卡片,一个选项一个按钮,点一下即作答,卡片随即切到下一问。
|
|
101
|
+
- **不带选项的问题** —— 没有按钮可用,机器人把问题当提示发出来,你**直接在聊天里回一条消息**即可,这条消息就是答案。
|
|
102
|
+
- **工具授权**(需要你点头才能执行的操作)—— 同样是一张卡片,按钮是**允许一次** / **拒绝**。这类请求**只认按钮**:等待授权时发来的普通聊天消息不会被当成授权结果。
|
|
103
|
+
|
|
104
|
+
同一时刻一个聊天只有一张待答卡片。第二个请求会交还给宿主自己的处理路径(即 Web GUI),既不跟第一张抢同一条消息,也不会被丢掉;卡片投递失败、或请求在你作答前被取消,同样如此,并且会**释放该聊天**——否则你接下来那条消息会被静默吞掉。
|
|
105
|
+
|
|
106
|
+
看到「此操作已失效」说明这张卡片已经过期(空闲 60 秒后自动关闭),重新发起一次即可。连点两下、或上一问刚答完时紧接着落下的那一下,属于卡片重绘的窗口,会被静默忽略,**不会**误报失效。
|
|
107
|
+
|
|
94
108
|
## 配置
|
|
95
109
|
|
|
96
110
|
配置位于 DSH profile patch(`cordis.patch.yml`)中该插件的 `config:` 下。项目根目录(或其父目录)中的 `dsh.shared.config.json` 可以提供工作区/状态默认值,并对这些键具有更高优先级。
|
|
@@ -118,7 +132,7 @@ rm -f ~/.dsh/.dsh-connect/feishu-credentials.json
|
|
|
118
132
|
|---|---|---|
|
|
119
133
|
| `channels` | 全部内置 | 启用哪些通道:`feishu` / `telegram` / `dingtalk` / `web`。省略则启用全部内置通道。 |
|
|
120
134
|
| `channelDefaults` | `{}` | 应用到未单独设置该键的每个通道(如 `{ language: "zh" }`)。 |
|
|
121
|
-
| `settingsStatePath` | `<stateDir>/dsh-connect-settings.json` | Web 设置面板镜像非密钥配置的路径。默认落在 `stateDir` **之内**的 `dsh-connect-settings.json`,与 `bindings.json`
|
|
135
|
+
| `settingsStatePath` | `<stateDir>/dsh-connect-settings.json` | Web 设置面板镜像非密钥配置的路径。默认落在 `stateDir` **之内**的 `dsh-connect-settings.json`,与 `bindings.json` 同目录,二者不会各说各话;设置该键可覆盖。面板的权威数据源是当前 profile patch 中本插件的条目(见[用户设置](#用户设置)),本文件只是旧版 `/dsh-connect` RPC 读写的兼容镜像;0.2 之前的安装留在其中的那份副本会在升级时被读取一次(见[从 0.9.0 升级](#从-090-升级))。 |
|
|
122
136
|
|
|
123
137
|
### `feishu`(飞书 / Lark 通道)
|
|
124
138
|
|
|
@@ -179,7 +193,8 @@ rm -f ~/.dsh/.dsh-connect/feishu-credentials.json
|
|
|
179
193
|
- **写入的文件**
|
|
180
194
|
- `<stateDir>/bindings.json`(默认 `.dsh-connect/`)—— 聊天 ⇄ 会话路由存储(聊天键、会话 id、镜像与锁状态)。
|
|
181
195
|
- `<stateDir>/dsh-connect-settings.json`(默认 `.dsh-connect/`)—— 非密钥配置的兼容镜像,见 `settingsStatePath`。
|
|
182
|
-
-
|
|
196
|
+
- `<profile 目录>/.dsh-connect-legacy-imported` —— 记录一次性升级导入已经跑过的标记文件。它就放在导入所写的那个 profile 条目旁边(见[从 0.9.0 升级](#从-090-升级)),内容只有一句话说明设置来自哪里,不存任何数据。
|
|
197
|
+
- 当前 profile patch 中属于本插件的条目(`profileContext.patchPath`,即 `cordis.patch.yml`)—— 经由 DSH 第一方 `settings` 服务写入,原子、加锁、保留注释。
|
|
183
198
|
- `~/.dsh/.dsh-connect/feishu-credentials.json` —— **旧版、只读**。一键开通过去把飞书凭据存在这里而不是凭据库,导致刚扫码授权完的用户永远看到「未配置凭据」。现在开通流程写入凭据库,已有安装会在启动时从这个文件回填一次;此后不再读写它。
|
|
184
199
|
- `<workDir>/.dsh-connect-images/` —— 为用户图片/附件暂存,供智能体工具使用。
|
|
185
200
|
- DSH 自身在 `~/.dsh/` 下的会话日志与设置(sessions、settings 等)。
|
|
@@ -211,19 +226,30 @@ rm -f ~/.dsh/.dsh-connect/feishu-credentials.json
|
|
|
211
226
|
|
|
212
227
|
## 用户设置
|
|
213
228
|
|
|
214
|
-
Web 设置面板(**设置** 下的 `dsh-connect
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
Web 设置面板(**设置** 下的 `dsh-connect`)编辑的是**当前 profile patch 中属于本插件的
|
|
230
|
+
条目**——也就是你手工编辑的那个 `~/.dsh/profiles/<profile>/cordis.patch.yml`——走 DSH
|
|
231
|
+
自带的(第一方)`settings` 服务。该服务在文件锁下原子写入并保留你的注释,loader 会热重载
|
|
232
|
+
结果,因此保存无需重启即可生效。
|
|
233
|
+
|
|
234
|
+
面板能编辑的字段在插件 config schema 里声明为 `volatile`。正是这个声明让保存变成**就地
|
|
235
|
+
reconcile 而不是重挂载**:loader 为每个声明字段交给插件一个活引用,设置写入会就地提交它们,
|
|
236
|
+
运行中的适配器在收到下一条消息时即采用新值。面板不拥有的字段则根本没有声明——这正是它写不
|
|
237
|
+
进去的原因。
|
|
217
238
|
|
|
218
239
|
取值分三层解析,越靠后越具体:
|
|
219
240
|
|
|
220
241
|
1. 插件内置的 schema 默认值;
|
|
221
|
-
2.
|
|
222
|
-
3.
|
|
242
|
+
2. 插件被组合进来时的配置(其继承条目);
|
|
243
|
+
3. 当前 profile patch 中属于本插件的条目——你手写的值和面板保存的值都在这里。
|
|
223
244
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
245
|
+
保存只会投影到已声明的字段上,因此未声明的键——凭据、`settingsStatePath`、你自己加的
|
|
246
|
+
任何东西——即使调用方发过来也到不了文档里。反过来同样关键:宿主会把一次更新中**被省略**
|
|
247
|
+
的已声明字段重置回其继承值,所以每次保存写入的都是完整的已声明段。而你在该条目里手写的
|
|
248
|
+
未声明键,会在面板保存时原样保留。
|
|
249
|
+
|
|
250
|
+
**面板永远不会写入凭据。** profile patch 是一份普通的、鼓励用户贴进 issue 的文档,所以你在
|
|
251
|
+
面板里输入的密钥会存进 DSH 凭据库(`ctx.credentials`)——一键开通流程与 `FEISHU_*` 这类
|
|
252
|
+
环境变量也正是写在那里。你自己在该条目里手写的密钥则原样保留。
|
|
227
253
|
|
|
228
254
|
旧版 `/dsh-connect` HTTP RPC 为面板兼容而保留;它现在读写同一个 namespace,并把非密钥配置
|
|
229
255
|
镜像到 `settingsStatePath`(见[公共(所有通道)](#公共所有通道)),以兼容旧面板。
|
|
@@ -272,12 +298,52 @@ Web 设置面板(**设置** 下的 `dsh-connect`)编辑的是 `$DSH_HOME/set
|
|
|
272
298
|
因此,只用 webhook 推送(没有 Stream 凭据)的钉钉机器人会被正确判定为已配置,只用 Stream
|
|
273
299
|
模式的同样如此。
|
|
274
300
|
|
|
301
|
+
## 从 0.9.0 升级
|
|
302
|
+
|
|
303
|
+
仓库里的 `0.9.1` 同样适用——它只提交过、从未发布到 npm,所以用户实际都是从 `0.9.0` 升级上来的。
|
|
304
|
+
|
|
305
|
+
DSH 0.2 把每个插件的设置保存在 **profile patch** 里,而不是 `$DSH_HOME/settings.yaml`,
|
|
306
|
+
并且它不认识旧文档里的 `dsh-connect:` 段——宿主自己的迁移会重命名该文件并导入它认得的那几段,
|
|
307
|
+
我们这一段只会被丢下并记一条警告。不加处理的话,升级后各渠道照常工作(配置在 patch 里),
|
|
308
|
+
但每项只存在于面板里的选择都会在面板第一次打开时静默回到默认值。
|
|
309
|
+
|
|
310
|
+
因此 **0.9.2 会在升级后的第一次启动时导入一次**:
|
|
311
|
+
|
|
312
|
+
- 先在 `$DSH_HOME/settings.yaml` 里找 `dsh-connect:` 段,再找
|
|
313
|
+
`settings.yaml.imported`(宿主自己的迁移会重命名到那里,而它可能早于也可能晚于本次运行),
|
|
314
|
+
最后是插件自己的 `dsh-connect-settings.json` —— 一个从来没有过可用设置对端的用户,
|
|
315
|
+
他的全部选择都在这个兜底存储里。
|
|
316
|
+
- 该段会被投影到面板拥有的字段上,因此**凭据不可能随之迁移**:它们在凭据库里,文件里其他
|
|
317
|
+
任何内容都原样留在原地。
|
|
318
|
+
- 它**是合并,不是替换**。当前生效的值是基底,旧值叠在其上——因为宿主会把一次更新中被省略的
|
|
319
|
+
已声明字段重置回继承值,一份只带单渠道键的导入否则会清掉 `channels`,把所有适配器关掉。
|
|
320
|
+
- 它走与面板保存完全相同的写入路径,所以运行中的适配器会立即对上,无需重启。
|
|
321
|
+
- **不删除、不重命名任何东西。** 与宿主自己的导入不同,我们从不写 `settings.yaml`。
|
|
322
|
+
- 结果会一次性记录在一个以 profile 条目本身(`profileContext.patchPath`)命名的标记里,
|
|
323
|
+
也就是 `<profile 目录>/.dsh-connect-legacy-imported`,包括「本来就没有可导入内容」这种良性
|
|
324
|
+
情形,这样后续启动就不会把旧值重新盖到你此后的修改上。锚点是那条**条目**,而不是它过去所在
|
|
325
|
+
的状态文件:状态路径归你所有,你可以改(`stateDir`、`DSH_CONNECT_STATE_DIR`、
|
|
326
|
+
`settingsStatePath`)也可以删,两者任一都会让下次启动误以为导入从未跑过。而 profile 目录只会
|
|
327
|
+
在 profile 本身移动时移动——那恰好就是「该重新导入」的唯一情形。
|
|
328
|
+
|
|
329
|
+
重试规则比「凡失败就重试」更窄,而且是有意为之。只有三种结果是终局、会写标记:文档不在、
|
|
330
|
+
文档里没有 `dsh-connect:` 段、以及导入成功。其余全部——文档解析不了、文档**在却读不了**
|
|
331
|
+
(权限问题,或 `settings.yaml` 其实是个目录)、写入被拒、宿主根本没有 settings 服务——
|
|
332
|
+
都不导入任何东西、**两个文件都不动、也不写标记**,于是下次启动在你修好原因后会自己再试。
|
|
333
|
+
把「不存在」和「读不了」分开的正是最后这条:在 `0.9.2` 之前它们是同一种结果,于是一份读不了的
|
|
334
|
+
文档就此静默地、永久地结束了整段迁移。
|
|
335
|
+
|
|
336
|
+
每一种失败都只留一行 `connect: …`,点名文件与原因;下面的故障排查表逐条列出。标记**写不出去**
|
|
337
|
+
同样会报告——没有它,每次启动都会重跑迁移,把旧值重新盖到你更新的修改上。如果你从未用过面板,
|
|
338
|
+
这一切你都看不到。
|
|
339
|
+
|
|
275
340
|
## 故障排查
|
|
276
341
|
|
|
277
342
|
日志来自 DSH 宿主日志器(在终端运行 `dsh web`);插件消息带有 `connect:` / `connect-feishu:` 前缀。
|
|
278
343
|
|
|
279
344
|
| 症状 | 可能原因 / 修复 |
|
|
280
345
|
|---|---|
|
|
346
|
+
| 在 DSH `0.2.0-rc.2` 上安装 `dsh-connect@0.9.0` 被拒绝:*“`dsh-connect@0.9.0` 与 DSH `0.2.0-rc.2` 不兼容 …… 运行它可能导致崩溃或数据丢失”* | 这不是 bug,也不是可以忽略的警告:DSH 的兼容性闸门会拒绝任何声明范围覆盖不到当前宿主的插件,而 `0.9.0` 早于 `0.2.0` 这条线。请安装 **`0.9.2`**(或更新的版本),它的 peer 要求 `^0.2.0-rc.2`。 |
|
|
281
347
|
| `connect-feishu: adapter init failed` / `start failed` | 凭据错误、应用未发布或网络被阻断。检查 `appId`/`appSecret`,重新运行开通流程,确认机器人在飞书开放平台后台处于在线状态。 |
|
|
282
348
|
| `connect: resume of <id> failed, creating fresh session` | 持久化会话无法恢复(工作目录缺失、持久化问题)。检查 `workDir` 和 `~/.dsh/sessions`。 |
|
|
283
349
|
| 机器人对每条消息都回一行原始的 `agent-presets: preset "…" not found`,内容到不了智能体 | `$DSH_HOME/settings.yaml` 里的 `agent-presets.default` 指向了任何已安装版本都不提供的 id。**0.9.0** 已修复:改为重试 `standard` 并记录决策,而不是让这一轮失败;在更旧的版本上,请把该键改成一个确实存在的 id(`standard`)。 |
|
|
@@ -286,7 +352,15 @@ Web 设置面板(**设置** 下的 `dsh-connect`)编辑的是 `$DSH_HOME/set
|
|
|
286
352
|
| `[用户发送了图片,但下载失败…]` | 应用缺少飞书 `im:resource` 权限;授予该权限并重新授权。 |
|
|
287
353
|
| 流式回复是一整块没有分段 | 已在 **0.9.0** 修复:块边界与推理/回答分隔现在会插入空行(推理软换行已针对飞书卡片扩展)。升级后重启 `dsh web`。 |
|
|
288
354
|
| 长时间任务中卡片卡在「思考中…」没有进展 | 已在 **0.9.0** 修复:推理现在实时流出,工具调用显示为 `🔧` 进度行,静默期间心跳保活会更新卡片。升级后重启 `dsh web`。 |
|
|
289
|
-
|
|
|
355
|
+
| 智能体给出选项 / 请求工具授权,飞书里却什么都没有 | 已在 **0.9.2** 修复(仓库里的 `0.9.1` 从未发布):桥接此前订阅了一个宿主上并不存在的服务,问题只会静默落回宿主。升级后重启 `dsh web`。 |
|
|
356
|
+
| 点击卡片按钮提示「此操作已失效」,但卡片明明是刚发出来的 | 已在 **0.9.2** 修复(仓库里的 `0.9.1` 从未发布):卡片重绘期间(连点两下、上一问刚答完)落下的点击被误判为过期操作。升级后重启 `dsh web`。 |
|
|
357
|
+
| `connect: the legacy settings at <path> could not be parsed …` | 旧(0.2 之前)文档存在 YAML 错误,一次性迁移(见[从 0.9.0 升级](#从-090-升级))因而跳过它并原样留下文件。修好 YAML 后重启;在那之前不会导入任何内容,也不会写标记,所以重试是自动的。 |
|
|
358
|
+
| `connect: could not import the legacy dsh-connect settings from <path> …` | 迁移找到了该段,但 DSH 拒绝了这次写入(通常是某个值没通过校验)。该段仍在文件里——修好被点名的那一项后重启。 |
|
|
359
|
+
| `connect: could not read the legacy settings candidate at <path> …` | 候选文件**在**那里但读不了:权限、路径其实是个目录、或者 `$DSH_HOME` 里的 `~` 没被展开。这是迁移唯一**不**标记完成的失败——什么都没导入,什么都没标记,下次启动自动重试,所以修好之后不用做别的。之所以要区分「不存在」和「读不了」:两件事此前是同一种结果,于是**一次 `EACCES` 就让整个迁移静默地、永久地结束了**。 |
|
|
360
|
+
| `connect: could not write the one-shot import marker at <path> …` | 导入本身成功了,但记录它的标记写不出去(通常是 profile 目录只读)。这不是无害的:没有标记,每次启动都会重跑整段迁移,把旧值重新盖到你升级后在面板里改过的设置上——面板看起来会「自己变回去」。修好 profile 目录的写权限,或手动创建该标记文件。 |
|
|
361
|
+
| `connect: the import marker at <path> could not be read …` | 标记存在但读不了。此时按**已导入**处理并提示:宁可不再导入,也不愿把旧值盖到你更新的设置上。要重新触发导入,删掉这个标记文件再重启。 |
|
|
362
|
+
| 保存面板时报 *`Configuration for "connect" is overridden by a home patch or command-line overlay`* | 面板写的是 profile patch,但取值层级是 `bundle → profile → $DSH_HOME/cordis.patch.yml → --patch`,后两者的值会盖过面板保存的任何内容;DSH 因此直接拒绝这次写入,而不是让一次永远不可能生效的保存看起来成功了。想让面板接管这些设置,请改 home patch(或去掉 overlay)。 |
|
|
363
|
+
| 菜单卡片不更新 / 过期 | 设计如此:卡片空闲 60 秒后自动关闭;重新打开菜单即可。提问与授权卡片同理——详见[对话中的提问与授权](#对话中的提问与授权)。 |
|
|
290
364
|
|
|
291
365
|
**回滚** —— 重新安装之前的版本(先移除当前版本,再执行 `dsh plugin --profile web add dsh-connect@<version>`),或在源码安装中 `git checkout` 到固定的提交。
|
|
292
366
|
|
package/client/client.js
CHANGED
|
@@ -162,8 +162,9 @@ function snapshotToForm(snapshot) {
|
|
|
162
162
|
const config = snapshot.config ?? {};
|
|
163
163
|
const channels = snapshot.enabled ?? [];
|
|
164
164
|
const channelConfigs = {};
|
|
165
|
-
for (const ch of
|
|
165
|
+
for (const ch of Object.keys(CHANNEL_CONFIG_FIELDS)) {
|
|
166
166
|
channelConfigs[ch] = config[ch] ?? {};
|
|
167
|
+
}
|
|
167
168
|
return {
|
|
168
169
|
channels,
|
|
169
170
|
channelDefaults: config.channelDefaults ?? {},
|
|
@@ -473,11 +474,11 @@ var STYLE = `
|
|
|
473
474
|
.dsh-connect-settings .ds-check{flex:none;width:16px;height:16px;accent-color:var(--ds-accent)}
|
|
474
475
|
.dsh-connect-settings .ds-input{height:30px;width:100%;min-width:0;padding:0 9px;border:1px solid var(--ds-border-2);border-radius:6px;background:var(--ds-bg);color:var(--ds-text);font:inherit}
|
|
475
476
|
.dsh-connect-settings select.ds-input{cursor:pointer}
|
|
476
|
-
.dsh-connect-settings .ds-input:focus{outline:none;border-color:var(--ds-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--ds-accent) 25%,transparent)}
|
|
477
|
+
.dsh-connect-settings .ds-input:focus{outline:none;border-color:var(--ds-accent);box-shadow:0 0 0 2px rgba(59,130,246,.25);box-shadow:0 0 0 2px color-mix(in srgb,var(--ds-accent) 25%,transparent)}
|
|
477
478
|
.dsh-connect-settings .ds-adv{display:flex;flex-direction:column;gap:8px}
|
|
478
479
|
.dsh-connect-settings .ds-advanced-toggle{align-self:flex-start;display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border:1px dashed var(--ds-border-2);border-radius:99px;background:transparent;color:var(--ds-muted);font:inherit;font-size:11px;cursor:pointer}
|
|
479
480
|
.dsh-connect-settings .ds-advanced-toggle:hover{background:var(--ds-hover);color:var(--ds-text)}
|
|
480
|
-
.dsh-connect-settings .ds-footer{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;gap:12px;
|
|
481
|
+
.dsh-connect-settings .ds-footer{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--ds-border);border-radius:10px;background:var(--ds-bg)}
|
|
481
482
|
.dsh-connect-settings .ds-btn{height:32px;padding:0 18px;border:0;border-radius:6px;background:var(--dsw-alias-button-primary-fill,var(--ds-accent));color:var(--dsw-alias-label-primary-foreground,#ffffff);font:inherit;font-weight:500;cursor:pointer}
|
|
482
483
|
.dsh-connect-settings .ds-btn:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--ds-accent))}
|
|
483
484
|
.dsh-connect-settings .ds-btn:disabled{opacity:.55;cursor:default}
|
|
@@ -687,7 +688,7 @@ function ConnectSettingsTab({ rpcCall, t }) {
|
|
|
687
688
|
};
|
|
688
689
|
const setChannels = (ch, on) => {
|
|
689
690
|
setForm((f) => ({ ...f, channels: on ? [...f.channels, ch] : f.channels.filter((x) => x !== ch) }));
|
|
690
|
-
|
|
691
|
+
setOpenOverride(on && !open.has(ch) ? new Set(open).add(ch) : new Set(open));
|
|
691
692
|
};
|
|
692
693
|
const setField = (ch, field, value) => setForm((f) => ({ ...f, secrets: { ...f.secrets, [ch]: { ...f.secrets[ch] ?? {}, [field]: value } } }));
|
|
693
694
|
const setChannelConfig = (ch, key, raw) => setForm((f) => {
|
|
@@ -783,17 +784,19 @@ function ConnectSettingsTab({ rpcCall, t }) {
|
|
|
783
784
|
h("p", { className: "ds-hint" }, t("statePathHint"))
|
|
784
785
|
)
|
|
785
786
|
),
|
|
786
|
-
h("div", { className: "ds-status" }, form.live ? t("livePlane") : t("filePlane"))
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
)
|
|
787
|
+
h("div", { className: "ds-status" }, form.live ? t("livePlane") : t("filePlane"))
|
|
788
|
+
),
|
|
789
|
+
// Last child of the root, not of a card: `position:sticky` pins to the
|
|
790
|
+
// nearest scrollport only while its containing block is the scrolled box.
|
|
791
|
+
// Nested in the defaults card it could never move outside that card, so it
|
|
792
|
+
// pinned to nothing and Save scrolled away with the channel list.
|
|
793
|
+
h(
|
|
794
|
+
"div",
|
|
795
|
+
{ className: "ds-footer" },
|
|
796
|
+
h("button", { className: "ds-btn", type: "button", onClick: onSave, disabled: status === "saving" }, t("save")),
|
|
797
|
+
// Rendering `status` directly leaks the raw state ids (`idle`, `saving`)
|
|
798
|
+
// into the UI; every state has a locale entry instead.
|
|
799
|
+
h("span", { className: "ds-status" }, tr(t, `status.${status}`, status))
|
|
797
800
|
)
|
|
798
801
|
);
|
|
799
802
|
}
|