dsh-tool-adapt 0.2.5 → 0.2.9
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.md +25 -7
- package/lib/client.js +129 -14
- package/lib/index.js +163 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## English
|
|
4
4
|
|
|
5
|
-
**Current release: 0.2.
|
|
5
|
+
**Current release: 0.2.9** — Portable settings transport: the plugin now resolves its settings scope on both the `settingsScope` (≤ 0.1.5) and `configForms` (≥ 0.1.7-rc.1) hosts, so the Settings card and pill switch keep working across the rename.
|
|
6
6
|
|
|
7
7
|
A compatibility and safety adaptation layer for non-DeepSeek model families in DeepSeek Harness Web. It guards dead escalation states, injects model-family conventions, fuses repeated failures, and provides a lifecycle-safe Settings card and optional composer pill. Its DSH 0.1.2+ `remote.settings` path falls back to the legacy connection API on older RC hosts.
|
|
8
8
|
|
|
@@ -26,9 +26,15 @@ them run with the fields stripped (strip) — no error, no loop.
|
|
|
26
26
|
veto.
|
|
27
27
|
|
|
28
28
|
Config is hot. When Host `ctx.settings` is available the plugin registers the
|
|
29
|
-
`tool-adapt` namespace and an official-style expandable Settings Card
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
`tool-adapt` namespace and an official-style expandable Settings Card (same
|
|
30
|
+
disclosure chrome as Shell / Agent loop) becomes the writable source of truth.
|
|
31
|
+
That card registers on both settings seats DSH has used: the legacy
|
|
32
|
+
`settings.plugin.item` (key `tool-adapt`) on ≤ 0.1.5, and the keyed row seat
|
|
33
|
+
`plugins.row.config` (key `dsh-tool-adapt#tool-adapt`, i.e.
|
|
34
|
+
`<package name>#<row id>`) on ≥ 0.1.7-rc.2, where the **插件** panel (the first
|
|
35
|
+
sidebar panel icon) shows a
|
|
36
|
+
configure control for the row only while that exact key is registered.
|
|
37
|
+
A legacy `plugins/tool-adapt.config.json` is migrated once if the
|
|
32
38
|
settings user layer is empty; the old file is kept for rollback. The
|
|
33
39
|
`GET/POST /api/tool-adapt/status|set` routes remain as a compatibility
|
|
34
40
|
surface (loopback + same-origin fenced, 8 KiB body cap). The pill snaps to
|
|
@@ -36,7 +42,8 @@ one of the chat input's four corners (drag to switch); the anchor is
|
|
|
36
42
|
remembered in `localStorage` (`dsh.toolAdapt.anchor`). The pill mounts inside
|
|
37
43
|
the composer seat (same stacking level as the input box) at a normal
|
|
38
44
|
`z-index`, so DSH web popups (modal / menu / toast) can cover it. The pill
|
|
39
|
-
itself is status-only — edit the full form
|
|
45
|
+
itself is status-only — edit the full form in the **插件** panel (on ≤ 0.1.5:
|
|
46
|
+
Settings → Plugins). The
|
|
40
47
|
pill is hidden by DEFAULT: the「显示状态胶囊」switch (`ui.pill`, default
|
|
41
48
|
`false`) at the top of the ADAPT card owns its visibility, and the pill
|
|
42
49
|
follows the hot `/status` config within one poll (or instantly after a save
|
|
@@ -44,8 +51,19 @@ in the same tab).
|
|
|
44
51
|
|
|
45
52
|
## Install
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
```powershell
|
|
55
|
+
dsh plugin --profile web add dsh-tool-adapt
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Restart the existing DSH Web process afterwards: the Host scans the browser plugin roster at startup, so the Settings card and the optional pill appear only after that restart. Then open the ADAPT card — on ≤ 0.1.5 it is a card in **Settings → Plugins**; on ≥ 0.1.7-rc.2 open the **插件** panel (the first sidebar panel icon), find the `dsh-tool-adapt` bundle, and use the `tool-adapt` row's configure control; the「显示状态胶囊」switch (`ui.pill`, default `false`) owns the composer pill's visibility.
|
|
59
|
+
|
|
60
|
+
Local development, from this package directory:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
dsh plugin --profile web add .
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Either form records the package in the profile's `dsh.profile.bundles`, which is what mounts the Host half and serves the client bundle.
|
|
49
67
|
|
|
50
68
|
## Config
|
|
51
69
|
|
package/lib/client.js
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Browser-only bundle: consumed by the DSH web loader (window.__ModuleLoader__).
|
|
4
4
|
// Not importable in Node. Renders the ADAPT status pill and an official-style
|
|
5
|
-
// expandable Settings Card
|
|
5
|
+
// expandable Settings Card on BOTH settings seats, because DSH moved it when
|
|
6
|
+
// 0.1.7-rc.2 removed the old one: `settings.plugin.item` / key `tool-adapt` on
|
|
7
|
+
// ≤ 0.1.5, and the keyed row seat `plugins.row.config` / key ROW_CONFIG_KEY on
|
|
8
|
+
// ≥ 0.1.7-rc.2.
|
|
6
9
|
//
|
|
7
10
|
// The pill is hidden by DEFAULT: the ui.pill switch in the Settings Card
|
|
8
11
|
// (设置 → 插件 → ADAPT「显示状态胶囊」) owns visibility; the polled /status
|
|
@@ -24,6 +27,51 @@ window.__ModuleLoader__.load({
|
|
|
24
27
|
const React = require('react')
|
|
25
28
|
|
|
26
29
|
const NS = 'tool-adapt'
|
|
30
|
+
|
|
31
|
+
// ── the rc.2 settings seat key ───────────────────────────────────────────
|
|
32
|
+
//
|
|
33
|
+
// DSH 0.1.7-rc.2 REMOVED the slot `settings.plugin.item` — the seat this card
|
|
34
|
+
// registers on for ≤ 0.1.5. On the corridor a bundle ROW's configuration seat
|
|
35
|
+
// is the keyed slot `plugins.row.config`, and the official plugin-manager
|
|
36
|
+
// renders a row's configure control only while the EXACT key below sits on
|
|
37
|
+
// that slot's registration ledger:
|
|
38
|
+
//
|
|
39
|
+
// rowConfigKey(pkg.name, row.rowId) → `${pkg.name}#${row.rowId}`
|
|
40
|
+
// has: (row) => ledger.rows.has(rowConfigKey(pkg.name, row.rowId))
|
|
41
|
+
//
|
|
42
|
+
// So the key IS the contract: package.json#name plus the row id this package's
|
|
43
|
+
// own cordis.patch.yml declares. One constant, exported for inspection;
|
|
44
|
+
// test/client-source.test.mjs re-derives both halves from those two files and
|
|
45
|
+
// fails on any drift.
|
|
46
|
+
const ROW_CONFIG_KEY = 'dsh-tool-adapt#tool-adapt'
|
|
47
|
+
|
|
48
|
+
// The rc.2 `view === 'summary'` one-liner carries its style inline on purpose:
|
|
49
|
+
// the card's style tag is created lazily by a SettingsCard render, and the
|
|
50
|
+
// summary can render before any SettingsCard ever has.
|
|
51
|
+
const SUMMARY_STYLE = { color: 'var(--dsw-alias-label-tertiary)', fontSize: '13px', lineHeight: 1.5 }
|
|
52
|
+
|
|
53
|
+
// ── settings-scope portability (DSH 0.1.5 ↔ 0.1.7-rc.1) ──────────────────
|
|
54
|
+
//
|
|
55
|
+
// The two supported hosts expose the same scope CONTRACT under different
|
|
56
|
+
// service names, so the adapter lives here instead of in every call site:
|
|
57
|
+
//
|
|
58
|
+
// ≤ 0.1.5 settingsScope.bind({ namespace }) → SettingsScope<T>
|
|
59
|
+
// ≥ 0.1.7 configForms.get(entryId) → ConfigForm<T>
|
|
60
|
+
//
|
|
61
|
+
// Both return getSnapshot()/subscribe()/set()/unset()/mutate() over the same
|
|
62
|
+
// { status, value, base, user, revision, writable, mode } snapshot, so the
|
|
63
|
+
// resolved object is used unchanged by the card, the pill and the tests.
|
|
64
|
+
// `ctx.get` (never a direct property read) keeps the lookup safe when only
|
|
65
|
+
// one service is mounted: an uninjected property read throws.
|
|
66
|
+
// For this plugin the settings namespace and the profile entry id are both
|
|
67
|
+
// 'tool-adapt', so one key resolves on either host.
|
|
68
|
+
function resolveSettingsScopeFrom(ctx, namespace) {
|
|
69
|
+
const binder = ctx.get('settingsScope')
|
|
70
|
+
if (binder && typeof binder.bind === 'function') return binder.bind({ namespace })
|
|
71
|
+
const forms = ctx.get('configForms')
|
|
72
|
+
if (forms && typeof forms.get === 'function') return forms.get(namespace)
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
27
75
|
const API = '/api/tool-adapt'
|
|
28
76
|
const POLL_MS = 5000
|
|
29
77
|
const e = React.createElement
|
|
@@ -388,7 +436,11 @@ window.__ModuleLoader__.load({
|
|
|
388
436
|
let pillMounted = false
|
|
389
437
|
let pillDispose = null
|
|
390
438
|
|
|
391
|
-
|
|
439
|
+
// `getScope` is a GETTER: the settings transport is awaited with a
|
|
440
|
+
// non-gating ctx.inject callback, so the pill may start before (or entirely
|
|
441
|
+
// without) a scope. Reading it lazily keeps the pill working either way and
|
|
442
|
+
// lets the settings subscription attach if/when the transport appears.
|
|
443
|
+
function startPill(getScope) {
|
|
392
444
|
if (typeof document === 'undefined') return
|
|
393
445
|
if (pillMounted) {
|
|
394
446
|
// Previous mount leaked (disposal was skipped). Dispose it first, then
|
|
@@ -396,7 +448,7 @@ window.__ModuleLoader__.load({
|
|
|
396
448
|
const previous = pillDispose
|
|
397
449
|
pillDispose = null
|
|
398
450
|
if (typeof previous === 'function') previous()
|
|
399
|
-
return startPill()
|
|
451
|
+
return startPill(getScope)
|
|
400
452
|
}
|
|
401
453
|
pillMounted = true
|
|
402
454
|
|
|
@@ -764,6 +816,7 @@ window.__ModuleLoader__.load({
|
|
|
764
816
|
applyVisibility()
|
|
765
817
|
}
|
|
766
818
|
|
|
819
|
+
const scope = getScope()
|
|
767
820
|
if (scope && typeof scope.subscribe === 'function') {
|
|
768
821
|
// A settings save reaches the host config immediately; re-fetch right
|
|
769
822
|
// away so the pill flips without waiting up to POLL_MS. The 5s poll
|
|
@@ -802,23 +855,78 @@ window.__ModuleLoader__.load({
|
|
|
802
855
|
}
|
|
803
856
|
|
|
804
857
|
function apply(ctx) {
|
|
805
|
-
// Settings card first: pill mounting waits for the composer seat
|
|
806
|
-
// asynchronously and must never delay or break the Settings Slot.
|
|
807
|
-
const scope = ctx.settingsScope.bind({ namespace: NS })
|
|
808
858
|
// DSH 0.1.2+ exposes fine-grained remote settings through
|
|
809
859
|
// @deepseek-ai/dsh-api-remotes (ctx.remote). The legacy RC connection API
|
|
810
860
|
// is no longer provided on the Client, so there is no fallback branch.
|
|
861
|
+
// Defined BEFORE the settings wait: the inject callback can fire while
|
|
862
|
+
// apply is still running, and the card closure reads both of these.
|
|
811
863
|
const remote = ctx.get('remote')
|
|
812
864
|
const api = remote && remote.settings
|
|
813
865
|
? { settings: { mutate: (payload) => remote.settings.mutate(payload.ns, payload.ops, payload.expectedRevision).then((result) => ({ result })) } }
|
|
814
866
|
: undefined
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
867
|
+
let scope
|
|
868
|
+
const disposeSlots = []
|
|
869
|
+
|
|
870
|
+
// Settings card FIRST (pill mounting waits for the composer seat
|
|
871
|
+
// asynchronously and must never delay or break the Settings Slot), and
|
|
872
|
+
// registered from inside the settings-transport callback on the CHILD
|
|
873
|
+
// context it hands us. Registering `settings.plugin.item` from the bare
|
|
874
|
+
// apply context puts the entry where the Plugins tab's ledger never sees
|
|
875
|
+
// it: the tab reads `ctx.slots.entries('settings.plugin.item')` from its
|
|
876
|
+
// own context, got an empty list, and the card never rendered even though
|
|
877
|
+
// `register` returned normally. (dshmarket, an out-of-repo bundle that
|
|
878
|
+
// does render, uses this nested shape.)
|
|
879
|
+
// Both transport names are awaited WITHOUT gating activation, and whichever
|
|
880
|
+
// this host mounts resolves; see resolveSettingsScopeFrom.
|
|
881
|
+
const registerCard = (sctx) => {
|
|
882
|
+
if (scope !== undefined) return
|
|
883
|
+
scope = resolveSettingsScopeFrom(sctx, NS)
|
|
884
|
+
if (scope === undefined) return
|
|
885
|
+
disposeSlots.push(sctx.slots.inject('settings.plugin.item', () => sctx.slots.register({
|
|
886
|
+
name: 'settings.plugin.item',
|
|
887
|
+
key: NS,
|
|
888
|
+
label: 'ADAPT',
|
|
889
|
+
}, function ToolAdaptCard() {
|
|
890
|
+
return e(SettingsCard, { scope, api })
|
|
891
|
+
})))
|
|
892
|
+
}
|
|
893
|
+
// The first wait fires only when settingsScope exists; the second covers a
|
|
894
|
+
// host that renamed the transport, and its guard keeps the card from
|
|
895
|
+
// registering twice when both names exist.
|
|
896
|
+
ctx.inject(['settingsScope'], registerCard)
|
|
897
|
+
ctx.inject(['configForms'], (sctx) => { if (scope === undefined) registerCard(sctx) })
|
|
898
|
+
|
|
899
|
+
// ── rc.2 seat: the keyed slot `plugins.row.config` ──────────────────────
|
|
900
|
+
//
|
|
901
|
+
// 0.1.7-rc.2 removed `settings.plugin.item`; a bundle row's configuration
|
|
902
|
+
// seat is `plugins.row.config`, declared by the official plugin-manager
|
|
903
|
+
// page, and the row's configure control exists only while an occupant holds
|
|
904
|
+
// the exact ledger key ROW_CONFIG_KEY. The old seat is still declared (and
|
|
905
|
+
// still used) on ≤ 0.1.5, so both registrations sit side by side with no
|
|
906
|
+
// version sniffing: `slots.inject` fires only where its own slot is declared
|
|
907
|
+
// and disposes the entry when that declaration collapses.
|
|
908
|
+
//
|
|
909
|
+
// The page renders this ONE component in two views:
|
|
910
|
+
// view === 'summary' → the row's one-liner: text only, no controls
|
|
911
|
+
// view === 'page' → the configuration page, i.e. the existing card
|
|
912
|
+
// The optional, host-owned `form` prop is deliberately NOT consumed: values
|
|
913
|
+
// keep flowing through the single transport resolved by
|
|
914
|
+
// resolveSettingsScopeFrom (settingsScope ≤ 0.1.5 / configForms ≥ 0.1.7), so
|
|
915
|
+
// this plugin keeps exactly one read path and one write path.
|
|
916
|
+
//
|
|
917
|
+
// `slots` is awaited NON-GATINGLY, and the callback returns the registration
|
|
918
|
+
// disposer so the entry is owned by the registering fiber and released with
|
|
919
|
+
// the rest of disposeSlots on stop / update / HMR.
|
|
920
|
+
const registerRowConfig = (sctx) => sctx.slots.inject('plugins.row.config', () => sctx.slots.register({
|
|
921
|
+
name: 'plugins.row.config',
|
|
922
|
+
key: ROW_CONFIG_KEY,
|
|
923
|
+
}, function ToolAdaptRowConfig(props) {
|
|
924
|
+
if (props && props.view === 'summary') {
|
|
925
|
+
return e('span', { style: SUMMARY_STYLE }, '非 DeepSeek 模型的工具适配开关(守卫 / L2 惯例 / L0 熔断 / 状态胶囊)。')
|
|
926
|
+
}
|
|
820
927
|
return e(SettingsCard, { scope, api })
|
|
821
928
|
}))
|
|
929
|
+
ctx.inject(['slots'], registerRowConfig)
|
|
822
930
|
|
|
823
931
|
// Pill: fully lifecycle-owned. The effect runs startPill() now and the
|
|
824
932
|
// returned disposer on stop / update / HMR, idempotently releasing the
|
|
@@ -826,17 +934,24 @@ window.__ModuleLoader__.load({
|
|
|
826
934
|
// and every listener so the next apply re-mounts from scratch. The pill
|
|
827
935
|
// starts hidden: startPill gates visibility on ui.pill (default false)
|
|
828
936
|
// via the settings scope plus the /status poll fallback.
|
|
829
|
-
ctx.effect(() => startPill(scope), 'dsh-tool-adapt: composer pill')
|
|
937
|
+
ctx.effect(() => startPill(() => scope), 'dsh-tool-adapt: composer pill')
|
|
830
938
|
|
|
831
939
|
// Plugin-card style tag: created lazily by SettingsCard renders and
|
|
832
940
|
// removed here so no style is left behind after stop / update.
|
|
833
941
|
ctx.effect(() => () => removeCardStyles(), 'dsh-tool-adapt: plugin card style')
|
|
834
942
|
|
|
835
|
-
return
|
|
943
|
+
return () => { for (const dispose of disposeSlots) { try { dispose() } catch (_) {} } }
|
|
836
944
|
}
|
|
837
945
|
|
|
838
946
|
exports.apply = apply
|
|
839
|
-
|
|
947
|
+
// The rc.2 ledger key lives in one exported constant so a test (and anyone
|
|
948
|
+
// inspecting the bundle) can compare it with package.json#name plus the row id
|
|
949
|
+
// in cordis.patch.yml instead of trusting a second literal.
|
|
950
|
+
exports.ROW_CONFIG_KEY = ROW_CONFIG_KEY
|
|
951
|
+
// 'settingsScope' and 'configForms' are alternatives, not both-required:
|
|
952
|
+
// cordis resolves each inject name as its own gate, so activation succeeds on
|
|
953
|
+
// whichever settings transport the host mounts (0.1.5 vs 0.1.7-rc.1).
|
|
954
|
+
exports.inject = ['slots', 'remote', 'remote.settings']
|
|
840
955
|
return module.exports
|
|
841
956
|
},
|
|
842
957
|
})
|
package/lib/index.js
CHANGED
|
@@ -60,31 +60,122 @@ import {
|
|
|
60
60
|
export { DEFAULT_CONFIG, SETTINGS_NS, validateConfig } from './config.js'
|
|
61
61
|
|
|
62
62
|
export const name = 'dsh-tool-adapt'
|
|
63
|
+
// `settings` is deliberately NOT listed here: cordis treats every inject name as a
|
|
64
|
+
// REQUIRED gate, and the settings transport is version-dependent (see the
|
|
65
|
+
// `ctx.inject(['settings'], ...)` wait inside apply).
|
|
63
66
|
export const inject = ['webServer', 'fs', 'systemPrompt']
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
// ── settings schema: one shape, two hosts ────────────────────────────────────
|
|
69
|
+
// ≤ 0.1.5 the schema is handed to `ctx.settings.register()` and plain fields are
|
|
70
|
+
// enough.
|
|
71
|
+
// ≥ 0.1.7 the same shape must be the entry's own `static Config`, and the
|
|
72
|
+
// settings service only exposes fields marked `.volatile()`: a schema
|
|
73
|
+
// without a volatile field produces no settings form at all.
|
|
74
|
+
//
|
|
75
|
+
// `Schema.prototype.volatile` exists only from schemastery 3.18.4 (the 0.1.7
|
|
76
|
+
// corridor); the 0.1.5 line resolves 3.18.2, where it is `undefined` and calling
|
|
77
|
+
// it throws. So it is applied capability-detected, and one expression serves both
|
|
78
|
+
// hosts. Volatility is marked on the individual LEAVES (and on `excludeModels` as
|
|
79
|
+
// a whole) — never on an enclosing object too, because a volatile field inside a
|
|
80
|
+
// volatile field throws at resolve time.
|
|
81
|
+
const volatile = (schema) => (typeof schema?.volatile === 'function' ? schema.volatile() : schema)
|
|
82
|
+
|
|
83
|
+
// A volatile field's parsed value is a cosmokit wrapper: a `get()` method plus a
|
|
84
|
+
// registered write symbol, and nothing else. Key on the symbol — not on `set`,
|
|
85
|
+
// which does NOT exist (verified against schemastery 3.18.4 + cosmokit 1.8.5).
|
|
86
|
+
const VOLATILE_WRITE = Symbol.for('cosmokit.volatile.write')
|
|
87
|
+
function readVolatile(value) {
|
|
88
|
+
if (value === null || typeof value !== 'object') return value
|
|
89
|
+
if (typeof value.get !== 'function') return value
|
|
90
|
+
if (!(VOLATILE_WRITE in value)) return value
|
|
91
|
+
return value.get()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function settingsFields() {
|
|
95
|
+
return {
|
|
67
96
|
guard: Schema.object({
|
|
68
|
-
enabled: Schema.boolean().default(DEFAULT_CONFIG.guard.enabled),
|
|
97
|
+
enabled: volatile(Schema.boolean().default(DEFAULT_CONFIG.guard.enabled)),
|
|
69
98
|
}).default(cloneConfig(DEFAULT_CONFIG.guard)),
|
|
70
99
|
l2: Schema.object({
|
|
71
|
-
enabled: Schema.boolean().default(DEFAULT_CONFIG.l2.enabled),
|
|
72
|
-
excludeModels: Schema.array(Schema.string().pattern(/^[A-Za-z0-9.*_-]{1,64}$/)).max(50).default(DEFAULT_CONFIG.l2.excludeModels.slice()),
|
|
73
|
-
block: Schema.string().min(1).max(4000).default(DEFAULT_CONFIG.l2.block),
|
|
100
|
+
enabled: volatile(Schema.boolean().default(DEFAULT_CONFIG.l2.enabled)),
|
|
101
|
+
excludeModels: volatile(Schema.array(Schema.string().pattern(/^[A-Za-z0-9.*_-]{1,64}$/)).max(50).default(DEFAULT_CONFIG.l2.excludeModels.slice())),
|
|
102
|
+
block: volatile(Schema.string().min(1).max(4000).default(DEFAULT_CONFIG.l2.block)),
|
|
74
103
|
}).default(cloneConfig(DEFAULT_CONFIG.l2)),
|
|
75
104
|
l0: Schema.object({
|
|
76
|
-
enabled: Schema.boolean().default(DEFAULT_CONFIG.l0.enabled),
|
|
77
|
-
remindAfter: Schema.number().step(1).min(2).max(20).default(DEFAULT_CONFIG.l0.remindAfter),
|
|
78
|
-
vetoAfter: Schema.number().step(1).min(0).max(20).default(DEFAULT_CONFIG.l0.vetoAfter),
|
|
79
|
-
reminderText: Schema.string().min(1).max(2000).default(DEFAULT_CONFIG.l0.reminderText),
|
|
80
|
-
vetoText: Schema.string().min(1).max(2000).default(DEFAULT_CONFIG.l0.vetoText),
|
|
105
|
+
enabled: volatile(Schema.boolean().default(DEFAULT_CONFIG.l0.enabled)),
|
|
106
|
+
remindAfter: volatile(Schema.number().step(1).min(2).max(20).default(DEFAULT_CONFIG.l0.remindAfter)),
|
|
107
|
+
vetoAfter: volatile(Schema.number().step(1).min(0).max(20).default(DEFAULT_CONFIG.l0.vetoAfter)),
|
|
108
|
+
reminderText: volatile(Schema.string().min(1).max(2000).default(DEFAULT_CONFIG.l0.reminderText)),
|
|
109
|
+
vetoText: volatile(Schema.string().min(1).max(2000).default(DEFAULT_CONFIG.l0.vetoText)),
|
|
81
110
|
}).default(cloneConfig(DEFAULT_CONFIG.l0)),
|
|
82
111
|
ui: Schema.object({
|
|
83
|
-
pill: Schema.boolean().default(DEFAULT_CONFIG.ui.pill),
|
|
112
|
+
pill: volatile(Schema.boolean().default(DEFAULT_CONFIG.ui.pill)),
|
|
84
113
|
}).default(cloneConfig(DEFAULT_CONFIG.ui)),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ≤ 0.1.5 registration schema: the live settings section only.
|
|
118
|
+
function createSettingsSchema() {
|
|
119
|
+
return Schema.object(settingsFields())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ≥ 0.1.7 entry Config: the live settings section PLUS the row field `configFile`,
|
|
123
|
+
// which is what the mounting row in cordis.patch.yml carries. The settings fields
|
|
124
|
+
// keep the identical names, nesting and defaults, so the client card's reads and
|
|
125
|
+
// writes of `guard.enabled`, `l2.*`, `l0.*` and `ui.pill` work on both hosts.
|
|
126
|
+
function createEntryConfig() {
|
|
127
|
+
return Schema.object({
|
|
128
|
+
configFile: Schema.string().default('plugins/tool-adapt.config.json'),
|
|
129
|
+
...settingsFields(),
|
|
85
130
|
})
|
|
86
131
|
}
|
|
87
132
|
|
|
133
|
+
// Project the parsed entry Config onto the live settings shape, reading volatile
|
|
134
|
+
// fields on demand. Never cache the result: the declarative settings service
|
|
135
|
+
// writes volatile fields IN PLACE without remounting the entry, so the value must
|
|
136
|
+
// be re-read at every access.
|
|
137
|
+
function readDeclarativeConfig(raw) {
|
|
138
|
+
if (raw === null || typeof raw !== 'object') return undefined
|
|
139
|
+
const guard = readVolatile(raw.guard)
|
|
140
|
+
const l2 = readVolatile(raw.l2)
|
|
141
|
+
const l0 = readVolatile(raw.l0)
|
|
142
|
+
const ui = readVolatile(raw.ui)
|
|
143
|
+
return {
|
|
144
|
+
guard: { enabled: readVolatile(guard && guard.enabled) },
|
|
145
|
+
l2: {
|
|
146
|
+
enabled: readVolatile(l2 && l2.enabled),
|
|
147
|
+
excludeModels: readVolatile(l2 && l2.excludeModels),
|
|
148
|
+
block: readVolatile(l2 && l2.block),
|
|
149
|
+
},
|
|
150
|
+
l0: {
|
|
151
|
+
enabled: readVolatile(l0 && l0.enabled),
|
|
152
|
+
remindAfter: readVolatile(l0 && l0.remindAfter),
|
|
153
|
+
vetoAfter: readVolatile(l0 && l0.vetoAfter),
|
|
154
|
+
reminderText: readVolatile(l0 && l0.reminderText),
|
|
155
|
+
vetoText: readVolatile(l0 && l0.vetoText),
|
|
156
|
+
},
|
|
157
|
+
ui: { pill: readVolatile(ui && ui.pill) },
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The plugin's own entry Config (`static Config`): what the declarative (≥ 0.1.7)
|
|
162
|
+
// settings service reads as this entry's schema, keyed by the loader entry id
|
|
163
|
+
// 'tool-adapt' — the same key the client half resolves.
|
|
164
|
+
//
|
|
165
|
+
// It is reached two ways, and both must work:
|
|
166
|
+
// - through the plugin value cordis resolves. The loader's `unwrapExports`
|
|
167
|
+
// collapses a namespace carrying a `default` export down to that single value,
|
|
168
|
+
// so this module deliberately exports NO `default`: with none, the namespace
|
|
169
|
+
// itself is the plugin object, `resolve()` finds `apply`, and `plugin.Config`
|
|
170
|
+
// is read off that same namespace.
|
|
171
|
+
// - as a plain named export, so a host half loaded directly (the workspace's
|
|
172
|
+
// `scripts/settings-host-portability-check.mjs`) reads `module.Config`.
|
|
173
|
+
// `Config` is therefore the single shared schema instance attached to the exported
|
|
174
|
+
// `apply`. Function declarations are hoisted, so this is safe even though `apply`
|
|
175
|
+
// is defined further down, and the ≤ 0.1.5 line simply never reads it.
|
|
176
|
+
export const Config = createEntryConfig()
|
|
177
|
+
Object.assign(apply, { Config })
|
|
178
|
+
|
|
88
179
|
async function readLegacyFile(ctx, configFile) {
|
|
89
180
|
try {
|
|
90
181
|
const target = await ctx.fs.resolve(configFile)
|
|
@@ -187,14 +278,71 @@ export function apply(ctx, config) {
|
|
|
187
278
|
},
|
|
188
279
|
}
|
|
189
280
|
|
|
190
|
-
|
|
281
|
+
// Validate an already-projected live config; on any failure fall back to the
|
|
282
|
+
// shipped defaults, exactly like the legacy registration path.
|
|
283
|
+
function liveOrDefault(raw) {
|
|
284
|
+
const validated = validateConfig(raw)
|
|
285
|
+
return validated.ok ? validated.config : entry
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── settings transport: two independent hosts ─────────────────────────────
|
|
289
|
+
//
|
|
290
|
+
// ≤ 0.1.5 `ctx.settings.register(namespace, schema, opts)` exists. This plugin
|
|
291
|
+
// owns the `tool-adapt` namespace, its plain schema is enough, and a
|
|
292
|
+
// legacy `settings.yaml` may still be migrated into it.
|
|
293
|
+
//
|
|
294
|
+
// ≥ 0.1.7 register() is gone. A settings namespace exists only as the plugin
|
|
295
|
+
// entry's own `Config` schema (the `static Config` attached to `apply`
|
|
296
|
+
// at module scope), keyed by the loader entry id — which is
|
|
297
|
+
// `tool-adapt`, the same key the client half resolves. Only
|
|
298
|
+
// `.volatile()` fields are exposed, so the entry Config marks the
|
|
299
|
+
// editable leaves volatile and no schema is registered here. The
|
|
300
|
+
// settings service writes those fields IN PLACE, without remounting the
|
|
301
|
+
// entry, so `source` must re-read the parsed `config` argument on every
|
|
302
|
+
// access rather than cache it. The legacy `settings.yaml` migration does
|
|
303
|
+
// not apply on this host: the entry Config is the storage.
|
|
304
|
+
//
|
|
305
|
+
// Both branches keep `source` non-throwing and fall back to the shipped
|
|
306
|
+
// defaults; with no settings service at all the legacy JSON file is the store.
|
|
307
|
+
const settingsApi = ctx.get('settings')
|
|
308
|
+
const declarative = settingsApi !== undefined
|
|
309
|
+
&& typeof settingsApi.register !== 'function'
|
|
310
|
+
&& typeof settingsApi.configure === 'function'
|
|
311
|
+
|
|
312
|
+
if (settingsApi === undefined) {
|
|
313
|
+
// No settings transport: the legacy JSON file stays authoritative.
|
|
191
314
|
void loadLegacyFile(ctx, configFile, state)
|
|
192
315
|
}
|
|
193
316
|
|
|
194
317
|
ctx.inject(['settings'], (sctx) => {
|
|
318
|
+
if (declarative) {
|
|
319
|
+
try {
|
|
320
|
+
// Suppress the official UI's generated generic page for this entry: the
|
|
321
|
+
// plugin ships its own settings card. `auto: false` only affects that
|
|
322
|
+
// generated page — the namespace still reaches the client mirror, so the
|
|
323
|
+
// custom card keeps working. `configure` throws if called twice for one
|
|
324
|
+
// fiber, so the effect owns the call and disposes it.
|
|
325
|
+
sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber), 'dsh-tool-adapt: settings presentation')
|
|
326
|
+
source = () => liveOrDefault(readDeclarativeConfig(config))
|
|
327
|
+
state.settingsReady = true
|
|
328
|
+
sctx.effect(() => () => {
|
|
329
|
+
state.settingsReady = false
|
|
330
|
+
source = () => entry
|
|
331
|
+
}, 'dsh-tool-adapt: settings fallback')
|
|
332
|
+
} catch (err) {
|
|
333
|
+
state.fileError = String((err && err.message) || err)
|
|
334
|
+
source = () => entry
|
|
335
|
+
}
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!settingsApi || typeof settingsApi.register !== 'function') {
|
|
340
|
+
source = () => entry
|
|
341
|
+
return
|
|
342
|
+
}
|
|
195
343
|
try {
|
|
196
344
|
const schema = createSettingsSchema()
|
|
197
|
-
const scope =
|
|
345
|
+
const scope = settingsApi.register(SETTINGS_NS, schema, {
|
|
198
346
|
base: entry,
|
|
199
347
|
applies: 'live',
|
|
200
348
|
validate: (value) => {
|
|
@@ -202,11 +350,7 @@ export function apply(ctx, config) {
|
|
|
202
350
|
if (!validated.ok) throw new Error((validated.errors || []).join('; '))
|
|
203
351
|
},
|
|
204
352
|
})
|
|
205
|
-
source = () =>
|
|
206
|
-
const resolved = scope.get()
|
|
207
|
-
const validated = validateConfig(resolved)
|
|
208
|
-
return validated.ok ? validated.config : entry
|
|
209
|
-
}
|
|
353
|
+
source = () => liveOrDefault(scope.get())
|
|
210
354
|
state.settingsReady = true
|
|
211
355
|
sctx.effect(() => scope.watch(() => {}), 'dsh-tool-adapt: settings watch')
|
|
212
356
|
sctx.effect(() => () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-tool-adapt",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "EN: Compatibility and safety adaptation layer for non-DeepSeek models in DeepSeek Harness Web. ZH: 面向 DeepSeek Harness Web 非 DeepSeek 模型的兼容与安全适配层。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|