From fcc8f75560be3ada6e3c6cacacc9961a3f837409 Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 9 Oct 2026 15:31:33 +0800 Subject: [PATCH] Adapt settings client for DSH 0.2.0 configForms without breaking boot. Soft-inject configForms/settingsScope and mark Config fields volatile so desktop 0.2.0 no longer waits forever on the removed settingsScope service. Co-authored-by: Cursor --- docs/NOTES.md | 2 +- lib/client.js | 54 ++++++++++++++++++------- lib/index.d.ts | 1 + lib/index.js | 94 ++++++++++++++++++++++++++++++++------------ package.json | 6 +-- test/package.test.js | 4 +- 6 files changed, 117 insertions(+), 44 deletions(-) diff --git a/docs/NOTES.md b/docs/NOTES.md index 5faacae..d74d5bf 100644 --- a/docs/NOTES.md +++ b/docs/NOTES.md @@ -36,7 +36,7 @@ Loader id:`dsh-cron-tasks` `package.json` 必须同时有 `dsh.bundle.patch` 和 `dsh.client`(`platform: web`,`immediately: true`)。Host **不要** import `@deepseek-ai/*`,也不要把它写进 `dependencies`。工具用纯 `ToolDefinition`,不要 `defineTool`。 -Client `inject` 只写 `slots` / `locale` / `settingsScope`。静态 `inject: ['sessions']` 会在 boot 死锁;`sessions` / `workspaces` 用 `ctx.inject` 可选 + `internal/service`。 +Client 硬 `inject` 只写 `slots` / `locale`。不要硬注入 `settingsScope`:DSH ≥0.1.7 / 0.2.0 已删掉该服务(换成 `configForms`),硬等会让 web boot 卡在 pending。设置卡用软注入:`configForms` 优先,否则 `settingsScope`。静态 `inject: ['sessions']` 会在 boot 死锁;`sessions` / `workspaces` 用 `ctx.inject` 可选 + `internal/service`。 --- diff --git a/lib/client.js b/lib/client.js index a62f296..a6e614c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -158,7 +158,9 @@ window.__ModuleLoader__.load({ const name = 'dsh-ops-cron' const NS = 'dsh-ops-cron' - const inject = ['slots', 'locale', 'settingsScope'] + // Hard-inject only services present on both DSH ≤0.1.5 and ≥0.1.7. + // Do NOT hard-inject settingsScope: DSH ≥0.1.7 / 0.2.0 removed it (configForms). + const inject = ['slots', 'locale'] const API = '/dsh-ops-cron' function readCookie(name) { @@ -2222,19 +2224,43 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba } }, 'dsh-ops-cron: locale') - const scope = ctx.settingsScope.bind({ namespace: NS }) - const form = createForm(scope) - ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ - name: 'settings.plugin.item', - key: NS, - locale: LOCALE_NS, - inject: () => ({ - hooks: { card: form.store }, - edit: (field, value) => form.edit(field, value), - save: () => { form.save() }, - discard: () => form.discard(), - }), - }, SettingsCard)) + // Soft-mount Plugins card: configForms (DSH ≥0.1.7 / 0.2.0) or settingsScope (≤0.1.5). + let settingsMounted = false + const mountSettingsCard = (scope) => { + if (settingsMounted || !scope) return + settingsMounted = true + const form = createForm(scope) + ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ + name: 'settings.plugin.item', + key: NS, + locale: LOCALE_NS, + inject: () => ({ + hooks: { card: form.store }, + edit: (field, value) => form.edit(field, value), + save: () => { form.save() }, + discard: () => form.discard(), + }), + }, SettingsCard)) + } + ctx.inject(['configForms'], (formsCtx) => { + const forms = formsCtx.configForms || formsCtx.get?.('configForms') + if (!forms || typeof forms.get !== 'function') { + formsCtx.logger?.warn?.('[dsh-ops-cron] configForms present but .get missing') + return + } + formsCtx.logger?.info?.('[dsh-ops-cron] settings card via configForms') + mountSettingsCard(forms.get(NS)) + }) + ctx.inject(['settingsScope'], (scopeCtx) => { + if (settingsMounted) return + const binder = scopeCtx.settingsScope || scopeCtx.get?.('settingsScope') + if (!binder || typeof binder.bind !== 'function') { + scopeCtx.logger?.warn?.('[dsh-ops-cron] settingsScope present but .bind missing') + return + } + scopeCtx.logger?.info?.('[dsh-ops-cron] settings card via settingsScope') + mountSettingsCard(binder.bind({ namespace: NS })) + }) const faces = { sessions: undefined, diff --git a/lib/index.d.ts b/lib/index.d.ts index 4679e3e..c41dcf6 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -10,6 +10,7 @@ export interface Config { historyLimit?: number overlapPolicy?: 'skip' misfirePolicy?: 'skip' + allowExternalCwd?: boolean } export function apply(ctx: Context, config?: Config): void diff --git a/lib/index.js b/lib/index.js index aadfe65..ce836f5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -109,13 +109,22 @@ function loadPkg(id) { const SchemaMod = loadPkg('@deepseek-ai/schemastery') const Schema = SchemaMod.default || SchemaMod +/** + * DSH ≥0.1.7 / 0.2.0 only projects Config fields marked `.volatile()` into the + * settings UI. Zero volatile fields → the whole entry is silently filtered. + * Older schemastery without `volatile` keeps the bare schema (≤0.1.5 path). + */ +function vol(schema) { + return typeof schema?.volatile === 'function' ? schema.volatile() : schema +} + export const Config = Schema.object({ - enabled: Schema.boolean().default(DEFAULT_SETTINGS.enabled), - timezone: Schema.string().default(DEFAULT_SETTINGS.timezone), - historyLimit: Schema.number().min(10).max(2000).step(1).default(DEFAULT_SETTINGS.historyLimit), - overlapPolicy: Schema.union(['skip']).default(DEFAULT_SETTINGS.overlapPolicy), - misfirePolicy: Schema.union(['skip']).default(DEFAULT_SETTINGS.misfirePolicy), - allowExternalCwd: Schema.boolean().default(DEFAULT_SETTINGS.allowExternalCwd), + enabled: vol(Schema.boolean().default(DEFAULT_SETTINGS.enabled)), + timezone: vol(Schema.string().default(DEFAULT_SETTINGS.timezone)), + historyLimit: vol(Schema.number().min(10).max(2000).step(1).default(DEFAULT_SETTINGS.historyLimit)), + overlapPolicy: vol(Schema.union(['skip']).default(DEFAULT_SETTINGS.overlapPolicy)), + misfirePolicy: vol(Schema.union(['skip']).default(DEFAULT_SETTINGS.misfirePolicy)), + allowExternalCwd: vol(Schema.boolean().default(DEFAULT_SETTINGS.allowExternalCwd)), }) function resolveConfig(config = {}) { @@ -178,8 +187,60 @@ export function apply(ctx, config = {}) { }) ctx.inject(['settings'], (sctx) => { - sctx.settings.register(NS, Config, { base: entry, applies: 'live' }) - sctx.logger?.info?.(`[dsh-ops-cron] settings namespace "${NS}" registered`) + const settings = sctx.settings + // ≤0.1.5: explicit namespace registration. ≥0.1.7: Config is projected from + // the Loader entry — register may be absent or refuse; follow describe(). + if (typeof settings?.register === 'function') { + try { + settings.register(NS, Config, { base: entry, applies: 'live' }) + sctx.logger?.info?.(`[dsh-ops-cron] settings namespace "${NS}" registered`) + } catch (error) { + sctx.logger?.warn?.( + `[dsh-ops-cron] settings.register failed (will follow describe): ${error instanceof Error ? error.message : error}`, + ) + } + } + + const describeRows = () => { + try { + const raw = typeof settings?.describe === 'function' ? settings.describe() : null + if (Array.isArray(raw)) return raw + if (raw && typeof raw === 'object' && Array.isArray(raw.namespaces)) return raw.namespaces + } catch { /* settling */ } + return [] + } + const readLive = () => { + const row = describeRows().find((item) => item && item.ns === NS) + if (row?.value !== null && typeof row?.value === 'object' && !Array.isArray(row.value)) { + return resolveConfig({ ...entry, ...row.value }) + } + return entry + } + + if (typeof settings?.watch === 'function') { + try { + const unwatch = settings.watch(NS, (next) => { + service.updateSettings(resolveConfig(next)).catch(() => {}) + }) + if (typeof unwatch === 'function') { + sctx.effect(() => () => unwatch(), 'dsh-ops-cron: settings.watch') + } + } catch { + // watch optional + } + } else { + // ≥0.1.7: no scope.watch — poll describe so UI writes reach the scheduler. + let lastFingerprint = JSON.stringify(readLive()) + const timer = setInterval(() => { + const next = readLive() + const fingerprint = JSON.stringify(next) + if (fingerprint === lastFingerprint) return + lastFingerprint = fingerprint + service.updateSettings(next).catch(() => {}) + }, 2_000) + sctx.effect(() => () => clearInterval(timer), 'dsh-ops-cron: settings describe poll') + sctx.logger?.info?.('[dsh-ops-cron] following settings via describe() (no watch — DSH ≥0.1.7 path)') + } }) ctx.inject(['tools'], (tctx) => { @@ -193,23 +254,6 @@ export function apply(ctx, config = {}) { } }) - ctx.effect(() => { - const off = ctx.on('internal/service', (serviceName, value) => { - if (serviceName !== 'settings' || !value?.watch) return - try { - const unwatch = value.watch(NS, (next) => { - service.updateSettings(resolveConfig(next)).catch(() => {}) - }) - if (typeof unwatch === 'function') { - // Replaced when the effect disposes. - } - } catch { - // watch is optional. - } - }) - return () => off() - }, 'dsh-ops-cron: settings watch') - ctx.effect(() => registerWebRoute(ctx, (req, res) => { service.handleRequest(req, res).catch(() => { if (!res.headersSent) { diff --git a/package.json b/package.json index 3d25487..b2de255 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-ops-cron", "description": "Scheduled tasks for DeepSeek Harness: Agent cron_* tools + sidebar 定时任务, with DSH or WhatsApp/IM delivery per job.", - "version": "0.1.15", + "version": "0.1.16", "private": false, "type": "module", "repository": { @@ -52,8 +52,8 @@ "platform": "web", "immediately": true, "inject": [ - "@deepseek-ai/dsh-client-ui-settings-plugins", - "@deepseek-ai/dsh-client-ui-sidebar" + "@deepseek-ai/dsh-client-ui-slots", + "@deepseek-ai/dsh-client-locale" ] } }, diff --git a/test/package.test.js b/test/package.test.js index 8e97378..de57476 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -70,7 +70,9 @@ test('installable bundle declares host apply, client half, unique id, and no @de assert.match(client, /PresetField/) assert.match(client, /agentPreset/) assert.match(client, /mirrorToSession/) - assert.match(client, /inject = \['slots', 'locale', 'settingsScope'\]/) + assert.match(client, /inject = \['slots', 'locale'\]/) + assert.match(client, /configForms/) + assert.match(client, /settingsScope/) assert.match(client, /locale\.bind/) assert.match(client, /locale\/change/) assert.match(client, /loginRequired/)