diff --git a/lib/client.js b/lib/client.js index 1ed115c..9c267f5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -251,7 +251,7 @@ function NetxopsCard(props) { }), /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", - children: t("apiTokenHint") + children: state.apiTokenRemoteReady ? t("apiTokenHint") : t("apiTokenUnavailable") }) ] }), @@ -533,7 +533,12 @@ class NetxopsCardController { ctx; form; store; - credential = { ref: "", configured: false, writable: true }; + credential = { + ref: "", + configured: false, + writable: false, + remoteReady: false + }; constructor(scope, ctx) { this.scope = scope; this.ctx = ctx; @@ -544,6 +549,18 @@ class NetxopsCardController { }); this.readCredential(); } + setCredentialsAvailable(ready) { + if (this.credential.remoteReady === ready) + return; + this.credential = { + ...this.credential, + remoteReady: ready, + writable: ready + }; + this.store.set(this.projection()); + if (ready) + this.readCredential(); + } projection() { return { ...this.form.shell(), @@ -552,25 +569,43 @@ class NetxopsCardController { pythonCommand: this.form.field("pythonCommand"), apiToken: this.form.field(API_TOKEN_FIELD), apiTokenConfigured: this.credential.configured, - apiTokenWritable: this.credential.writable + apiTokenWritable: this.credential.remoteReady && this.credential.writable, + apiTokenRemoteReady: this.credential.remoteReady }; } + credentials() { + return this.ctx.get("remote.credentials"); + } async readCredential() { const ref = refOf(this.scope.getSnapshot()); + const api = this.credentials(); + if (api === undefined) { + if (ref !== this.credential.ref || this.credential.remoteReady) { + this.credential = { + ref, + configured: false, + writable: false, + remoteReady: false + }; + this.store.set(this.projection()); + } + return; + } if (ref !== this.credential.ref) { - this.credential = { ref, configured: false, writable: true }; + this.credential = { ref, configured: false, writable: true, remoteReady: true }; this.store.set(this.projection()); } - const response = await this.ctx.remote.credentials.describe([ref]); + const response = await api.describe([ref]); if (!response.ok || ref !== refOf(this.scope.getSnapshot())) return; - const view = response.value[ref]; + const view = response.value?.[ref]; const next = { ref, configured: view?.configured ?? false, - writable: view?.writable ?? true + writable: view?.writable ?? true, + remoteReady: true }; - if (next.configured === this.credential.configured && next.writable === this.credential.writable) + if (next.configured === this.credential.configured && next.writable === this.credential.writable && next.remoteReady === this.credential.remoteReady) return; this.credential = next; this.store.set(this.projection()); @@ -584,7 +619,10 @@ class NetxopsCardController { return { hooks: { netxopsCard: this.store }, ...this.form.actions() }; } async writeToken(value) { - await this.ctx.remote.credentials.set(refOf(this.scope.getSnapshot()), value); + const api = this.credentials(); + if (api === undefined) + return false; + await api.set(refOf(this.scope.getSnapshot()), value); await this.readCredential(); return this.credential.configured; } @@ -606,6 +644,7 @@ var en = { pythonCommandHint: "Executable that can run `python -m netx_mcp`.", apiToken: "API token", apiTokenHint: "Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.", + apiTokenUnavailable: "This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.", apiTokenSet: "Configured", apiTokenUnset: "Not set", overridden: "Overridden", @@ -631,6 +670,7 @@ var zh = { pythonCommandHint: "能执行 `python -m netx_mcp` 的解释器。", apiToken: "API Token", apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。", + apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。", apiTokenSet: "已配置", apiTokenUnset: "未设置", overridden: "已覆盖", @@ -652,15 +692,23 @@ var inject = [ "slots", "locale", "remote", - "remote.credentials", "settingsScope" ]; function apply(ctx) { ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), "netxops: locales"); const card = new NetxopsCardController(ctx.settingsScope.bind({ namespace: NETXOPS_NS }), ctx); - ctx.effect(() => ctx.remote.$on("credentials/reference-updated", (ref) => { - card.refreshCredential(ref); - }), "netxops: credential invalidations"); + ctx.inject(["remote.credentials"], (credCtx) => { + card.setCredentialsAvailable(true); + credCtx.effect(() => { + const off = credCtx.remote.$on("credentials/reference-updated", (ref) => { + card.refreshCredential(String(ref)); + }); + return () => { + off(); + card.setCredentialsAvailable(false); + }; + }, "netxops: credential invalidations"); + }); ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({ name: "settings.plugin.item", key: NETXOPS_NS, diff --git a/package.json b/package.json index fccb823..ca2d43b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-netxops", - "version": "0.1.7", + "version": "0.1.8", "description": "DeepSeek Harness Netx Ops: host bridge + Plugins settings card + agent preset", "license": "MIT", "type": "module", diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx index 28bd6b8..dd75e7a 100644 --- a/src/client/NetxopsCard.tsx +++ b/src/client/NetxopsCard.tsx @@ -119,7 +119,9 @@ export function NetxopsCard(props: NetxopsCardProps) { disabled={!state.apiTokenWritable} onChange={(event) => { props.edit('apiToken', event.target.value) }} /> -

{t('apiTokenHint')}

+

+ {state.apiTokenRemoteReady ? t('apiTokenHint') : t('apiTokenUnavailable')} +

Promise<{ ok: boolean; value?: Record }> + set: (ref: string, value: string) => Promise +} + export class NetxopsCardController { private readonly form: CardForm private readonly store: SnapshotStore - private credential: CredentialState = { ref: '', configured: false, writable: true } + private credential: CredentialState = { + ref: '', + configured: false, + writable: false, + remoteReady: false, + } constructor( private readonly scope: SettingsScope, @@ -62,6 +74,18 @@ export class NetxopsCardController { void this.readCredential() } + /** Toggle when soft-injected `remote.credentials` arrives / leaves. */ + setCredentialsAvailable(ready: boolean): void { + if (this.credential.remoteReady === ready) return + this.credential = { + ...this.credential, + remoteReady: ready, + writable: ready, + } + this.store.set(this.projection()) + if (ready) void this.readCredential() + } + private projection(): NetxopsCardState { return { ...this.form.shell(), @@ -70,25 +94,48 @@ export class NetxopsCardController { pythonCommand: this.form.field('pythonCommand'), apiToken: this.form.field(API_TOKEN_FIELD), apiTokenConfigured: this.credential.configured, - apiTokenWritable: this.credential.writable, + apiTokenWritable: this.credential.remoteReady && this.credential.writable, + apiTokenRemoteReady: this.credential.remoteReady, } } + private credentials(): CredentialsRemote | undefined { + return this.ctx.get('remote.credentials') as CredentialsRemote | undefined + } + private async readCredential(): Promise { const ref = refOf(this.scope.getSnapshot()) + const api = this.credentials() + if (api === undefined) { + if (ref !== this.credential.ref || this.credential.remoteReady) { + this.credential = { + ref, + configured: false, + writable: false, + remoteReady: false, + } + this.store.set(this.projection()) + } + return + } if (ref !== this.credential.ref) { - this.credential = { ref, configured: false, writable: true } + this.credential = { ref, configured: false, writable: true, remoteReady: true } this.store.set(this.projection()) } - const response = await this.ctx.remote.credentials.describe([ref]) + const response = await api.describe([ref]) if (!response.ok || ref !== refOf(this.scope.getSnapshot())) return - const view = response.value[ref] + const view = response.value?.[ref] const next: CredentialState = { ref, configured: view?.configured ?? false, writable: view?.writable ?? true, + remoteReady: true, } - if (next.configured === this.credential.configured && next.writable === this.credential.writable) return + if ( + next.configured === this.credential.configured + && next.writable === this.credential.writable + && next.remoteReady === this.credential.remoteReady + ) return this.credential = next this.store.set(this.projection()) } @@ -103,7 +150,9 @@ export class NetxopsCardController { } private async writeToken(value: string): Promise { - await this.ctx.remote.credentials.set(refOf(this.scope.getSnapshot()), value) + const api = this.credentials() + if (api === undefined) return false + await api.set(refOf(this.scope.getSnapshot()), value) await this.readCredential() return this.credential.configured } diff --git a/src/client/index.ts b/src/client/index.ts index f7203d4..993da9d 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,5 +1,9 @@ /** * Browser half — Settings → Plugins → Netx Ops card. + * + * Do not hard-inject `remote.credentials`: shipped `@deepseek-ai/dsh` 0.1.1-rc.2 + * remotes assembly does not mount that namespace (Models/Plugins cards only + * inject `remote`). Soft-inject when a newer Host provides it. */ import type { Context as ClientContext } from '@deepseek-ai/cordis' @@ -20,11 +24,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { const LOCALE_NS = 'settings.netxops' +/** Match shipped ui-settings-plugins inject (no remote.credentials). */ export const inject = [ 'slots', 'locale', 'remote', - 'remote.credentials', 'settingsScope', ] @@ -36,10 +40,19 @@ export function apply(ctx: ClientContext): void { ctx, ) - ctx.effect( - () => ctx.remote.$on('credentials/reference-updated', (ref) => { card.refreshCredential(ref) }), - 'netxops: credential invalidations', - ) + // Optional: newer remotes that mount credentials unlock the token field. + ctx.inject(['remote.credentials'], (credCtx) => { + card.setCredentialsAvailable(true) + credCtx.effect(() => { + const off = credCtx.remote.$on('credentials/reference-updated', (ref) => { + card.refreshCredential(String(ref)) + }) + return () => { + off() + card.setCredentialsAvailable(false) + } + }, 'netxops: credential invalidations') + }) ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', diff --git a/src/client/locales.ts b/src/client/locales.ts index 9ab88f7..752b75a 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -11,6 +11,7 @@ export type NetxopsLocaleKey = | 'pythonCommandHint' | 'apiToken' | 'apiTokenHint' + | 'apiTokenUnavailable' | 'apiTokenSet' | 'apiTokenUnset' | 'overridden' @@ -36,6 +37,7 @@ export const en: Record = { pythonCommandHint: 'Executable that can run `python -m netx_mcp`.', apiToken: 'API token', apiTokenHint: 'Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.', + apiTokenUnavailable: 'This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.', apiTokenSet: 'Configured', apiTokenUnset: 'Not set', overridden: 'Overridden', @@ -62,6 +64,7 @@ export const zh: Record = { pythonCommandHint: '能执行 `python -m netx_mcp` 的解释器。', apiToken: 'API Token', apiTokenHint: '写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。', + apiTokenUnavailable: '当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。', apiTokenSet: '已配置', apiTokenUnset: '未设置', overridden: '已覆盖',