mirror of
https://github.com/hansjone/dsh-ops-cron.git
synced 2026-10-16 19:53:55 +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`。
|
`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 name = 'dsh-ops-cron'
|
||||||
const NS = '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'
|
const API = '/dsh-ops-cron'
|
||||||
|
|
||||||
function readCookie(name) {
|
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')
|
}, 'dsh-ops-cron: locale')
|
||||||
|
|
||||||
const scope = ctx.settingsScope.bind({ namespace: NS })
|
// Soft-mount Plugins card: configForms (DSH ≥0.1.7 / 0.2.0) or settingsScope (≤0.1.5).
|
||||||
const form = createForm(scope)
|
let settingsMounted = false
|
||||||
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
const mountSettingsCard = (scope) => {
|
||||||
name: 'settings.plugin.item',
|
if (settingsMounted || !scope) return
|
||||||
key: NS,
|
settingsMounted = true
|
||||||
locale: LOCALE_NS,
|
const form = createForm(scope)
|
||||||
inject: () => ({
|
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
||||||
hooks: { card: form.store },
|
name: 'settings.plugin.item',
|
||||||
edit: (field, value) => form.edit(field, value),
|
key: NS,
|
||||||
save: () => { form.save() },
|
locale: LOCALE_NS,
|
||||||
discard: () => form.discard(),
|
inject: () => ({
|
||||||
}),
|
hooks: { card: form.store },
|
||||||
}, SettingsCard))
|
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 = {
|
const faces = {
|
||||||
sessions: undefined,
|
sessions: undefined,
|
||||||
|
|
|
||||||
1
lib/index.d.ts
vendored
1
lib/index.d.ts
vendored
|
|
@ -10,6 +10,7 @@ export interface Config {
|
||||||
historyLimit?: number
|
historyLimit?: number
|
||||||
overlapPolicy?: 'skip'
|
overlapPolicy?: 'skip'
|
||||||
misfirePolicy?: 'skip'
|
misfirePolicy?: 'skip'
|
||||||
|
allowExternalCwd?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apply(ctx: Context, config?: Config): void
|
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 SchemaMod = loadPkg('@deepseek-ai/schemastery')
|
||||||
const Schema = SchemaMod.default || SchemaMod
|
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({
|
export const Config = Schema.object({
|
||||||
enabled: Schema.boolean().default(DEFAULT_SETTINGS.enabled),
|
enabled: vol(Schema.boolean().default(DEFAULT_SETTINGS.enabled)),
|
||||||
timezone: Schema.string().default(DEFAULT_SETTINGS.timezone),
|
timezone: vol(Schema.string().default(DEFAULT_SETTINGS.timezone)),
|
||||||
historyLimit: Schema.number().min(10).max(2000).step(1).default(DEFAULT_SETTINGS.historyLimit),
|
historyLimit: vol(Schema.number().min(10).max(2000).step(1).default(DEFAULT_SETTINGS.historyLimit)),
|
||||||
overlapPolicy: Schema.union(['skip']).default(DEFAULT_SETTINGS.overlapPolicy),
|
overlapPolicy: vol(Schema.union(['skip']).default(DEFAULT_SETTINGS.overlapPolicy)),
|
||||||
misfirePolicy: Schema.union(['skip']).default(DEFAULT_SETTINGS.misfirePolicy),
|
misfirePolicy: vol(Schema.union(['skip']).default(DEFAULT_SETTINGS.misfirePolicy)),
|
||||||
allowExternalCwd: Schema.boolean().default(DEFAULT_SETTINGS.allowExternalCwd),
|
allowExternalCwd: vol(Schema.boolean().default(DEFAULT_SETTINGS.allowExternalCwd)),
|
||||||
})
|
})
|
||||||
|
|
||||||
function resolveConfig(config = {}) {
|
function resolveConfig(config = {}) {
|
||||||
|
|
@ -178,8 +187,60 @@ export function apply(ctx, config = {}) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx.inject(['settings'], (sctx) => {
|
ctx.inject(['settings'], (sctx) => {
|
||||||
sctx.settings.register(NS, Config, { base: entry, applies: 'live' })
|
const settings = sctx.settings
|
||||||
sctx.logger?.info?.(`[dsh-ops-cron] settings namespace "${NS}" registered`)
|
// ≤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) => {
|
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) => {
|
ctx.effect(() => registerWebRoute(ctx, (req, res) => {
|
||||||
service.handleRequest(req, res).catch(() => {
|
service.handleRequest(req, res).catch(() => {
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-ops-cron",
|
"name": "dsh-ops-cron",
|
||||||
"description": "Scheduled tasks for DeepSeek Harness: Agent cron_* tools + sidebar 定时任务, with DSH or WhatsApp/IM delivery per job.",
|
"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,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -52,8 +52,8 @@
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
"immediately": true,
|
"immediately": true,
|
||||||
"inject": [
|
"inject": [
|
||||||
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
"@deepseek-ai/dsh-client-ui-slots",
|
||||||
"@deepseek-ai/dsh-client-ui-sidebar"
|
"@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, /PresetField/)
|
||||||
assert.match(client, /agentPreset/)
|
assert.match(client, /agentPreset/)
|
||||||
assert.match(client, /mirrorToSession/)
|
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\.bind/)
|
||||||
assert.match(client, /locale\/change/)
|
assert.match(client, /locale\/change/)
|
||||||
assert.match(client, /loginRequired/)
|
assert.match(client, /loginRequired/)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue