mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-10 16:13:33 +08:00
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 <cursoragent@cursor.com>
This commit is contained in:
parent
a41a2b53c9
commit
fcc8f75560
6 changed files with 117 additions and 44 deletions
|
|
@ -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`。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
1
lib/index.d.ts
vendored
1
lib/index.d.ts
vendored
|
|
@ -10,6 +10,7 @@ export interface Config {
|
|||
historyLimit?: number
|
||||
overlapPolicy?: 'skip'
|
||||
misfirePolicy?: 'skip'
|
||||
allowExternalCwd?: boolean
|
||||
}
|
||||
|
||||
export function apply(ctx: Context, config?: Config): void
|
||||
|
|
|
|||
94
lib/index.js
94
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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue