@linxin666/dsh-ssh 0.3.23 → 0.4.0
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/icon.svg +3 -0
- package/lib/client.js +120 -10
- package/lib/client.js.map +1 -1
- package/lib/index.js +34 -1050
- package/lib/types/client/index.d.ts +0 -13
- package/lib/types/client/settings-binding.d.ts +61 -0
- package/lib/types/http.d.ts +18 -1
- package/lib/types/index.d.ts +46 -13
- package/package.json +22 -18
- package/src/client/index.ts +18 -22
- package/src/client/panel/panel.module.css +30 -11
- package/src/client/settings-binding.ts +165 -0
- package/src/client/sidebar-entry-core.ts +3 -3
- package/src/client/sidebar-entry.ts +2 -2
- package/src/http.ts +23 -1
- package/src/index.ts +88 -55
|
@@ -16,8 +16,8 @@ import { mountSidebarEntry as mountSharedSidebarEntry } from './sidebar-entry-co
|
|
|
16
16
|
/** Stable data attribute identifying the injected entry row. */
|
|
17
17
|
export const ENTRY_SELECTOR = '[data-dsh-ssh-entry]'
|
|
18
18
|
|
|
19
|
-
/** Inline terminal glyph sized to the shell's
|
|
20
|
-
const ICON = '<svg viewBox="0 0 16 16" width="
|
|
19
|
+
/** Inline terminal glyph sized to the shell's panel-row navigation icons. */
|
|
20
|
+
const ICON = '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.75"/><path d="M4.25 5.25l2.75 2.75-2.75 2.75"/><path d="M8.5 10.75h3.25"/></svg>'
|
|
21
21
|
|
|
22
22
|
/** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
|
|
23
23
|
export interface LocaleRefreshSource { subscribe(listener: () => void): () => void }
|
package/src/http.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared JSON body/response helpers for the host route families: one strict
|
|
4
4
|
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
5
|
-
* narrow,
|
|
5
|
+
* narrow, one JSON writer, and the outbound request-init helper that keeps a
|
|
6
|
+
* host fetch body decodable (withIdentityEncoding). Previously the readers
|
|
7
|
+
* were copy-pasted across the
|
|
6
8
|
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
7
9
|
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
8
10
|
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
@@ -88,6 +90,26 @@ export function asJsonObject(value: unknown): Record<string, unknown> | undefine
|
|
|
88
90
|
return isJsonObject(value) ? value : undefined
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Request init that asks a remote origin for an uncompressed body.
|
|
95
|
+
*
|
|
96
|
+
* The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
|
|
97
|
+
* the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
|
|
98
|
+
* Node's built-in `fetch()` reads. That cross-major wrapper drops the
|
|
99
|
+
* `content-encoding` header and automatic decompression, so a plain fetch
|
|
100
|
+
* resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
|
|
101
|
+
* consumer can read. Every host fetch that parses a remote body must request
|
|
102
|
+
* identity encoding. Caller headers survive; `accept-encoding` is forced to
|
|
103
|
+
* `identity` because a caller value has no decoder behind it in this host.
|
|
104
|
+
* @param init - the caller's request init (signal, headers, method, ...).
|
|
105
|
+
* @returns a copy with `accept-encoding: identity` merged in.
|
|
106
|
+
*/
|
|
107
|
+
export function withIdentityEncoding(init: RequestInit = {}): RequestInit {
|
|
108
|
+
const headers = new Headers(init.headers)
|
|
109
|
+
headers.set('accept-encoding', 'identity')
|
|
110
|
+
return { ...init, headers }
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
/**
|
|
92
114
|
* Write one JSON response. Default headers are the family defaults
|
|
93
115
|
* (content-type and referrer-policy); caller headers are appended or
|
package/src/index.ts
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
* no dsh source changes.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
-
import
|
|
13
|
-
import z from 'schemastery'
|
|
11
|
+
import type { Context, Fiber } from '@deepseek-ai/cordis'
|
|
12
|
+
import z from '@deepseek-ai/schemastery'
|
|
14
13
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
15
14
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
16
15
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
@@ -26,18 +25,23 @@ export const name = 'ssh'
|
|
|
26
25
|
/** Services required before the SSH surfaces can mount. */
|
|
27
26
|
export const inject = ['webServer', 'tools', 'systemPrompt']
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
declare module '@deepseek-ai/cordis' {
|
|
29
|
+
interface Events {
|
|
30
|
+
/**
|
|
31
|
+
* Volatile config values were committed into the running instance without a
|
|
32
|
+
* remount (cordis-plugin-loader); dispatched to the owning fiber only.
|
|
33
|
+
* @param paths - the changed config paths, as key arrays.
|
|
34
|
+
* @mode emit
|
|
35
|
+
*/
|
|
36
|
+
'loader/volatile-update'(paths: readonly (readonly string[])[]): void
|
|
37
|
+
}
|
|
38
|
+
}
|
|
35
39
|
|
|
36
|
-
/** Plugin config
|
|
40
|
+
/** Plugin config as a profile patch declares it, before schema resolution. */
|
|
37
41
|
export interface Config {
|
|
38
42
|
/**
|
|
39
|
-
* When true
|
|
40
|
-
*
|
|
43
|
+
* When true, a system-prompt section announces the SSH plugin to every agent
|
|
44
|
+
* (tools + host store). Off by default to keep prompts clean.
|
|
41
45
|
*/
|
|
42
46
|
announceToAgent?: boolean
|
|
43
47
|
/** Master switch for the plugin (routes, tools, prompt section). */
|
|
@@ -51,14 +55,67 @@ export interface Config {
|
|
|
51
55
|
terminalFontFamily?: string
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
/** The stable reference a `volatile()` config field resolves to; its owner updates it in place. */
|
|
59
|
+
interface ConfigRef<T> {
|
|
60
|
+
/** @returns the field's current value. */
|
|
61
|
+
get(): T
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** One resolved config field: a live reference, or a plain value from a hand-built context. */
|
|
65
|
+
type ConfigField<T> = ConfigRef<T> | T
|
|
66
|
+
|
|
67
|
+
/** The config the Host hands to {@link apply} — the runtime face of {@link Config}. */
|
|
68
|
+
export interface ResolvedConfig {
|
|
69
|
+
announceToAgent?: ConfigField<boolean>
|
|
70
|
+
enabled?: ConfigField<boolean>
|
|
71
|
+
terminalFontFamily?: ConfigField<string>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Plugin config schema. Under the 0.1.7 settings model this schema IS the
|
|
76
|
+
* entry's settings page: the Host derives one form per profile entry from it
|
|
77
|
+
* and serves it through the shared configuration forms. Every field is
|
|
78
|
+
* `volatile()`, which is what puts it on that page and what lets an edit reach
|
|
79
|
+
* a running instance without a remount: the loader commits the new value into
|
|
80
|
+
* the field's reference and announces `loader/volatile-update` on this fiber.
|
|
81
|
+
*/
|
|
82
|
+
export const Config = z.object({
|
|
83
|
+
announceToAgent: z.boolean().default(false).volatile(),
|
|
84
|
+
enabled: z.boolean().default(true).volatile(),
|
|
85
|
+
terminalFontFamily: z.string().default('').volatile(),
|
|
58
86
|
})
|
|
59
87
|
|
|
60
|
-
/** Schema
|
|
88
|
+
/** Schema defaults, re-read for hand-built test contexts (the loader applies them normally). */
|
|
61
89
|
const DEFAULT_ANNOUNCE = false
|
|
90
|
+
const DEFAULT_ENABLED = true
|
|
91
|
+
|
|
92
|
+
/** Read one resolved config field, following the live reference the schema produces. */
|
|
93
|
+
function readConfigField<T>(field: ConfigField<T> | undefined, fallback: T): T {
|
|
94
|
+
if (field === undefined) return fallback
|
|
95
|
+
if (typeof field === 'object' && field !== null && typeof (field as ConfigRef<T>).get === 'function') {
|
|
96
|
+
const value = (field as ConfigRef<T>).get()
|
|
97
|
+
return value === undefined ? fallback : value
|
|
98
|
+
}
|
|
99
|
+
return field as T
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The loader's own record of the profile entry this instance was activated
|
|
104
|
+
* from, which carries the row's raw config — the user's own declaration, as
|
|
105
|
+
* opposed to the schema defaults and the inherited bundle layers. Absent on a
|
|
106
|
+
* host without the loader, which reads as "the profile declares nothing".
|
|
107
|
+
*/
|
|
108
|
+
interface LoaderEntry {
|
|
109
|
+
options?: { config?: unknown }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Raw config the profile declares for this entry, when the loader exposes it. */
|
|
113
|
+
function profileConfig(ctx: Context): Record<string, unknown> | undefined {
|
|
114
|
+
const entry = (ctx.fiber as Fiber & { entry?: LoaderEntry }).entry
|
|
115
|
+
const config = entry?.options?.config
|
|
116
|
+
if (typeof config !== 'object' || config === null || Array.isArray(config)) return undefined
|
|
117
|
+
return config as Record<string, unknown>
|
|
118
|
+
}
|
|
62
119
|
|
|
63
120
|
/** Order of the announcement section within the tool-guidance band. */
|
|
64
121
|
const SECTION_ORDER = 150
|
|
@@ -73,28 +130,22 @@ export const SSH_GUIDANCE = '本机已安装 dsh-ssh 插件(DSH 远程 SSH 运
|
|
|
73
130
|
*/
|
|
74
131
|
export const apply = mountOnce('@linxin666/dsh-ssh', applyImpl)
|
|
75
132
|
|
|
76
|
-
function applyImpl(ctx: Context, config?:
|
|
77
|
-
// The live source the surfaces read: the settings section once the web
|
|
78
|
-
// settings surface is served, the composition entry otherwise.
|
|
79
|
-
let current: () => Config = () => config ?? {}
|
|
80
|
-
let isSettingsBound = false
|
|
81
|
-
|
|
133
|
+
function applyImpl(ctx: Context, config?: ResolvedConfig): void {
|
|
82
134
|
const store = new HostStore()
|
|
83
135
|
const engine = new SshEngine(store)
|
|
84
136
|
ctx.effect(() => () => { engine.dispose() }, 'dsh-ssh: engine')
|
|
85
137
|
|
|
86
|
-
const resolve = ():
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
// AND already has active host records in dsh-ssh.json, keep the plugin enabled so
|
|
138
|
+
const resolve = (): { announceToAgent: boolean; enabled: boolean } => {
|
|
139
|
+
let enabled = readConfigField(config?.enabled, DEFAULT_ENABLED)
|
|
140
|
+
// When dsh-ssh was seeded with enabled: false (e.g. from an aggregate profile
|
|
141
|
+
// line), but the profile's own entry config never set the switch AND the user
|
|
142
|
+
// already has active host records in dsh-ssh.json, keep the plugin enabled so
|
|
92
143
|
// existing users are not broken upon upgrade (#1250).
|
|
93
|
-
if (enabled === false &&
|
|
144
|
+
if (enabled === false && profileConfig(ctx)?.enabled === undefined && store.list().length > 0) {
|
|
94
145
|
enabled = true
|
|
95
146
|
}
|
|
96
147
|
return {
|
|
97
|
-
announceToAgent:
|
|
148
|
+
announceToAgent: readConfigField(config?.announceToAgent, DEFAULT_ANNOUNCE),
|
|
98
149
|
enabled,
|
|
99
150
|
}
|
|
100
151
|
}
|
|
@@ -117,7 +168,7 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
117
168
|
// System-prompt announcement.
|
|
118
169
|
let disposeSection: (() => void) | undefined
|
|
119
170
|
|
|
120
|
-
// Register (or drop) every surface to match the
|
|
171
|
+
// Register (or drop) every surface to match the resolved config. Each group
|
|
121
172
|
// is kept under one disposer: re-registering first tears the old one down
|
|
122
173
|
// so duplicate-name registrations never throw.
|
|
123
174
|
const sync = (): void => {
|
|
@@ -162,29 +213,11 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
162
213
|
)
|
|
163
214
|
}
|
|
164
215
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
setSource: (source) => {
|
|
170
|
-
isSettingsBound = true
|
|
171
|
-
current = source
|
|
172
|
-
sync()
|
|
173
|
-
},
|
|
174
|
-
onChange: sync,
|
|
175
|
-
})
|
|
176
|
-
} else if (typeof settingsCtx.settings?.register === 'function') {
|
|
177
|
-
const scope = settingsCtx.settings.register(SSH_SETTINGS_NAMESPACE, Config, { base: config ?? {} })
|
|
178
|
-
isSettingsBound = true
|
|
179
|
-
current = () => scope?.get?.() ?? (config ?? {})
|
|
180
|
-
scope?.watch?.(() => { sync() })
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
// Defensive fallback against settings registration differences
|
|
184
|
-
}
|
|
185
|
-
})
|
|
216
|
+
// A settings edit is committed into this instance's config references and
|
|
217
|
+
// announced on the owning fiber (the entry is not remounted), so the
|
|
218
|
+
// surfaces are re-derived from the new values here.
|
|
219
|
+
ctx.on('loader/volatile-update', () => { sync() })
|
|
186
220
|
|
|
187
|
-
// Initial registration from the
|
|
188
|
-
// no settings service, whose installSection never fires its hooks).
|
|
221
|
+
// Initial registration from the config the Host activated this row with.
|
|
189
222
|
sync()
|
|
190
223
|
}
|