@linxin666/dsh-client-ui-task-board 0.1.16 → 0.1.17

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.
@@ -22,7 +22,7 @@ import {
22
22
  import { applyCreateTask } from './use-cases/task-create.ts'
23
23
  import { applyDeleteTask } from './use-cases/task-delete.ts'
24
24
  import { applyScheduleNextRun as applyScheduleRollForward, applySetSchedule } from './use-cases/task-schedule.ts'
25
- import { applyUpdateTask } from './use-cases/task-update.ts'
25
+ import { applyUpdateTask, type TaskUpdatePatch } from './use-cases/task-update.ts'
26
26
 
27
27
  /** The sessions face the controller needs for navigation awareness. */
28
28
  export interface SessionsControllerFace {
@@ -47,11 +47,36 @@ export interface ControllerDeps {
47
47
  reconcileDebounceMs?: number
48
48
  }
49
49
 
50
+ /** One workspace option the execution-target pickers offer. */
51
+ export interface ExecutionWorkspaceOption {
52
+ workspaceId: string
53
+ /** Display label (workspace title; the wiring falls back to the path). */
54
+ title: string
55
+ }
56
+
57
+ /** One agent-preset option the execution-target pickers offer. */
58
+ export interface ExecutionPresetOption {
59
+ id: string
60
+ name?: string
61
+ description?: string
62
+ /** Why this preset cannot compose a session; the pickers disable it. */
63
+ broken?: string
64
+ isDefault: boolean
65
+ }
66
+
67
+ /** The execution-target option sets the UI feeds into the controller. */
68
+ export interface ExecutionOptionsSnapshot {
69
+ workspaces: readonly ExecutionWorkspaceOption[]
70
+ presets: readonly ExecutionPresetOption[]
71
+ }
72
+
50
73
  /** Immutable controller snapshot for UI subscriptions. */
51
74
  export interface ControllerSnapshot {
52
75
  tasks: readonly TaskRecord[]
53
76
  boardOpen: boolean
54
77
  selectedTaskId: string | undefined
78
+ /** Picker option sets (workspace list + agent-preset roster). */
79
+ executionOptions: ExecutionOptionsSnapshot
55
80
  }
56
81
 
57
82
  /** The selected task (resolved from the ledger), or undefined. */
@@ -85,6 +110,7 @@ export class BoardController {
85
110
  private tasks: TaskRecord[] = []
86
111
  private boardOpen = false
87
112
  private selectedTaskId: string | undefined
113
+ private executionOptions: ExecutionOptionsSnapshot = { workspaces: [], presets: [] }
88
114
  private listeners = new Set<() => void>()
89
115
  private disposers: Array<() => void> = []
90
116
  private readonly now: () => number
@@ -132,6 +158,7 @@ export class BoardController {
132
158
  tasks: this.tasks,
133
159
  boardOpen: this.boardOpen,
134
160
  selectedTaskId: this.selectedTaskId,
161
+ executionOptions: this.executionOptions,
135
162
  }
136
163
  }
137
164
 
@@ -186,11 +213,20 @@ export class BoardController {
186
213
  return task
187
214
  }
188
215
 
189
- updateTask(id: string, patch: Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>): void {
216
+ updateTask(id: string, patch: TaskUpdatePatch): void {
190
217
  this.tasks = [...applyUpdateTask(this.tasks, id, patch, this.now())]
191
218
  this.persistAndNotify()
192
219
  }
193
220
 
221
+ /**
222
+ * Replace (a part of) the picker option sets the UI feeds (workspace list
223
+ * and agent-preset roster come from the runtime, not the ledger).
224
+ */
225
+ setExecutionOptions(patch: Partial<ExecutionOptionsSnapshot>): void {
226
+ this.executionOptions = { ...this.executionOptions, ...patch }
227
+ this.notify()
228
+ }
229
+
194
230
  moveTask(id: string, status: TaskStatus): void {
195
231
  this.tasks = this.tasks.map(task => task.id === id ? withStatus(task, status, this.now()) : task)
196
232
  this.persistAndNotify()
@@ -15,17 +15,35 @@
15
15
  */
16
16
  import type { ExecutionRecord, TaskRecord } from './tasks.ts'
17
17
 
18
+ /** One list-row summary the execution service reads (the narrow slice of a SessionSummary). */
19
+ export interface ExecutionSessionSummary {
20
+ running: boolean
21
+ completed?: boolean
22
+ /** Empty-log bit: the preset can only be recomposed while the session is blank. */
23
+ blank?: boolean
24
+ /** The preset the session currently runs (absent on deployments without presets). */
25
+ agentPreset?: string
26
+ }
27
+
18
28
  /** The narrow sessions face the service needs. */
19
29
  export interface SessionsExecutionFace {
20
30
  list: {
21
31
  getSnapshot(): {
22
32
  /** Baseline arrival lifecycle — 'pending' until the host list has loaded. */
23
33
  phase: 'pending' | 'ready'
24
- byId: Record<string, { running: boolean; completed?: boolean }>
34
+ byId: Record<string, ExecutionSessionSummary>
25
35
  }
26
36
  subscribe(fn: () => void): () => void
27
37
  }
28
38
  binding(id: string): { session: SessionDriver } | undefined
39
+ /** Record a host-confirmed preset switch so the list label moves immediately. */
40
+ noteAgentPreset?(sessionId: string, agentPreset: string): void
41
+ }
42
+
43
+ /** The narrow agent-preset wire face the service needs (`agentPreset.select`). */
44
+ export interface PresetsExecutionFace {
45
+ /** Recompose a blank session's agent from a preset. */
46
+ select(sessionId: string, agentPreset: string): Promise<{ ok: true } | { ok: false; error: unknown }>
29
47
  }
30
48
 
31
49
  /** The narrow workspaces face the service needs. */
@@ -57,6 +75,12 @@ export interface SessionDriver {
57
75
  content: readonly unknown[],
58
76
  mode: 'queue',
59
77
  ): Promise<{ ok: true } | { ok: false; error: unknown }>
78
+ /**
79
+ * Admit one slash-command line against the session's agent (the
80
+ * `/permission <id>` mechanism). `matched` reports whether a command
81
+ * claimed the line.
82
+ */
83
+ command(line: string): Promise<{ ok: true; matched: boolean } | { ok: false; error: unknown }>
60
84
  getSnapshot(): { running: boolean; lastAgentError: string | null; turnEnds: ReadonlyMap<number, number> }
61
85
  subscribe(fn: () => void): () => void
62
86
  }
@@ -65,6 +89,8 @@ export interface SessionDriver {
65
89
  export interface ExecutionEnvironment {
66
90
  sessions: SessionsExecutionFace
67
91
  workspaces: WorkspacesExecutionFace
92
+ /** Agent-preset wire face; absent on deployments without preset support. */
93
+ presets?: PresetsExecutionFace
68
94
  /** Raw-history reader for failure detection of never-opened sessions. */
69
95
  history?: HistoryExecutionFace
70
96
  }
@@ -80,6 +106,19 @@ function messageOf(error: unknown): string {
80
106
  return String(error)
81
107
  }
82
108
 
109
+ /**
110
+ * Whether a rejected preset switch actually means "the session already runs
111
+ * this preset" (the host's agent-preset-conflict with a matching
112
+ * existingPreset). A blank-session reuse race can produce this even though
113
+ * the requested composition is in place.
114
+ */
115
+ function presetAlreadyRuns(error: unknown, mode: string): boolean {
116
+ if (typeof error !== 'object' || error === null) return false
117
+ const details = (error as { details?: unknown }).details
118
+ if (typeof details !== 'object' || details === null) return false
119
+ return (details as { existingPreset?: unknown }).existingPreset === mode
120
+ }
121
+
83
122
  /** Whether a `turn/end` payload closed the turn with an error reason. */
84
123
  function isErrorTurnEnd(data: unknown): boolean {
85
124
  if (typeof data !== 'object' || data === null) return false
@@ -106,14 +145,24 @@ export class ExecutionService {
106
145
  execution: ExecutionRecord,
107
146
  onEvent: (event: ExecutionEvent) => void,
108
147
  ): Promise<void> {
148
+ const settleFailed = (error: string): void => {
149
+ onEvent({ kind: 'settled', taskId: task.id, executionId: execution.id, outcome: 'failed', error })
150
+ }
109
151
  try {
110
- const sessionId = await this.connectSession()
152
+ const sessionId = await this.connectSession(task.workspaceId)
111
153
  onEvent({ kind: 'started', taskId: task.id, executionId: execution.id, sessionId })
112
154
  const driver = this.driverOf(sessionId)
113
155
  if (driver === undefined) {
114
- onEvent({ kind: 'settled', taskId: task.id, executionId: execution.id, outcome: 'failed', error: 'execution session is not ready' })
156
+ settleFailed('execution session is not ready')
115
157
  return
116
158
  }
159
+ // Task-pinned execution targets are applied BEFORE any prompt: a preset
160
+ // can only be recomposed while the session is blank, and the permission
161
+ // command must run before the task's turn starts. A rejected target
162
+ // fails the run without sending the prompt — running the task under
163
+ // different settings than it declared would be worse than not running.
164
+ if (!await this.applyMode(driver, task, sessionId, settleFailed)) return
165
+ if (!await this.applyPermission(driver, task, settleFailed)) return
117
166
  // Best-effort rename so the execution is recognizable in the session list.
118
167
  await driver.rename(task.title).catch(() => { /* rename is cosmetic */ })
119
168
  // Baseline the turn counter BEFORE the prompt round-trip: a turn that
@@ -122,21 +171,92 @@ export class ExecutionService {
122
171
  const baseline = driver.getSnapshot().turnEnds.size
123
172
  const accepted = await this.sendPrompt(driver, task)
124
173
  if (!accepted.ok) {
125
- onEvent({
126
- kind: 'settled', taskId: task.id, executionId: execution.id, outcome: 'failed',
127
- error: messageOf(accepted.error),
128
- })
174
+ settleFailed(messageOf(accepted.error))
129
175
  return
130
176
  }
131
177
  this.watchForSettlement(driver, task.id, execution.id, onEvent, baseline)
132
178
  } catch (error) {
133
- onEvent({
134
- kind: 'settled', taskId: task.id, executionId: execution.id, outcome: 'failed',
135
- error: messageOf(error),
136
- })
179
+ settleFailed(messageOf(error))
137
180
  }
138
181
  }
139
182
 
183
+ /**
184
+ * Recompose the execution session's agent from the task-pinned preset.
185
+ * No-op when the task pins none or the session already runs it; fails the
186
+ * run when the session is no longer blank, the preset face is missing, or
187
+ * the wire refuses.
188
+ */
189
+ private async applyMode(
190
+ driver: SessionDriver,
191
+ task: TaskRecord,
192
+ sessionId: string,
193
+ settleFailed: (error: string) => void,
194
+ ): Promise<boolean> {
195
+ const mode = task.mode
196
+ if (mode === undefined || mode === '') return true
197
+ const summary = this.env.sessions.list.getSnapshot().byId[sessionId]
198
+ if (summary?.blank === false) {
199
+ settleFailed(`cannot switch agent preset to ${mode}: the execution session is not blank`)
200
+ return false
201
+ }
202
+ if (summary?.agentPreset === mode) return true
203
+ if (this.env.presets === undefined) {
204
+ settleFailed(`this deployment does not support agent presets (task asks for ${mode})`)
205
+ return false
206
+ }
207
+ try {
208
+ const result = await this.env.presets.select(sessionId, mode)
209
+ if (!result.ok) {
210
+ // A list race can leave the summary without the preset label even
211
+ // though the blank session already runs it; the wire answers that
212
+ // case with agent-preset-conflict (existingPreset === requested).
213
+ // The requested composition is already in place, so count it as
214
+ // applied instead of failing the run.
215
+ if (presetAlreadyRuns(result.error, mode)) {
216
+ this.env.sessions.noteAgentPreset?.(sessionId, mode)
217
+ return true
218
+ }
219
+ settleFailed(`agent preset switch to ${mode} rejected: ${messageOf(result.error)}`)
220
+ return false
221
+ }
222
+ } catch (error) {
223
+ settleFailed(`agent preset switch to ${mode} failed: ${messageOf(error)}`)
224
+ return false
225
+ }
226
+ this.env.sessions.noteAgentPreset?.(sessionId, mode)
227
+ return true
228
+ }
229
+
230
+ /**
231
+ * Apply the task-pinned permission preset through the `/permission <id>`
232
+ * slash command. No-op when the task pins none; fails the run when the
233
+ * admission is rejected or no command claimed the line.
234
+ */
235
+ private async applyPermission(
236
+ driver: SessionDriver,
237
+ task: TaskRecord,
238
+ settleFailed: (error: string) => void,
239
+ ): Promise<boolean> {
240
+ const permission = task.permission
241
+ if (permission === undefined) return true
242
+ const line = `/permission ${permission}`
243
+ try {
244
+ const result = await driver.command(line)
245
+ if (!result.ok) {
246
+ settleFailed(`permission command rejected: ${messageOf(result.error)}`)
247
+ return false
248
+ }
249
+ if (!result.matched) {
250
+ settleFailed(`permission command not recognized: ${line}`)
251
+ return false
252
+ }
253
+ } catch (error) {
254
+ settleFailed(`permission command failed: ${messageOf(error)}`)
255
+ return false
256
+ }
257
+ return true
258
+ }
259
+
140
260
  /**
141
261
  * Inspect a reloaded/background task that was left 'running' and emit a
142
262
  * settled event when its session already finished.
@@ -198,8 +318,16 @@ export class ExecutionService {
198
318
  }
199
319
  }
200
320
 
201
- private async connectSession(): Promise<string> {
321
+ private async connectSession(taskWorkspaceId: string | undefined): Promise<string> {
202
322
  const workspace = this.env.workspaces.list.getSnapshot()
323
+ if (taskWorkspaceId !== undefined && taskWorkspaceId !== '') {
324
+ // A task-pinned workspace must exist in the list: connecting an
325
+ // unknown id would only defer the failure into the wire.
326
+ if (!workspace.items.some(item => item.workspaceId === taskWorkspaceId)) {
327
+ throw new Error(`task workspace is not available: ${taskWorkspaceId}`)
328
+ }
329
+ return this.env.workspaces.connectWorkspace(taskWorkspaceId)
330
+ }
203
331
  const workspaceId = workspace.recentWorkspaceId ?? workspace.items[0]?.workspaceId
204
332
  if (workspaceId === undefined) {
205
333
  throw new Error('no workspace available to run the task in')
package/src/core/store.ts CHANGED
@@ -14,8 +14,7 @@
14
14
  * localStorage backend.
15
15
  */
16
16
  import { isValidCron } from './schedule.ts'
17
- import type { ScheduleRule, TaskRecord, TaskStatus } from './tasks.ts'
18
- import { isTaskStatus } from './tasks.ts'
17
+ import { isTaskPermission, isTaskStatus, type ScheduleRule, type TaskRecord, type TaskPermission, type TaskStatus } from './tasks.ts'
19
18
 
20
19
  /** Persistence seam for the task ledger. */
21
20
  export interface TaskStore {
@@ -64,6 +63,9 @@ function isTaskRecordShape(value: unknown): value is Omit<TaskRecord, 'status'>
64
63
  if (typeof record.prompt !== 'string') return false
65
64
  if (typeof record.createdAt !== 'number') return false
66
65
  if (typeof record.updatedAt !== 'number') return false
66
+ if (record.workspaceId !== undefined && typeof record.workspaceId !== 'string') return false
67
+ if (record.mode !== undefined && typeof record.mode !== 'string') return false
68
+ if (record.permission !== undefined && typeof record.permission !== 'string') return false
67
69
  if (!Array.isArray(record.executions)) return false
68
70
  for (const execution of record.executions) {
69
71
  if (typeof execution !== 'object' || execution === null) return false
@@ -78,6 +80,11 @@ function isTaskRecordShape(value: unknown): value is Omit<TaskRecord, 'status'>
78
80
  return true
79
81
  }
80
82
 
83
+ /** Collapse a blank persisted target string to undefined (clears the pin). */
84
+ function normalizeTargetId(value: string | undefined): string | undefined {
85
+ return value !== undefined && value.trim() === '' ? undefined : value
86
+ }
87
+
81
88
  /** A task record is structurally valid if it round-trips through the UI. */
82
89
  export function isTaskRecord(value: unknown): value is TaskRecord {
83
90
  return isTaskRecordShape(value) && isTaskStatus(value.status)
@@ -136,6 +143,12 @@ export function parseLedger(raw: string | null): TaskRecord[] {
136
143
  // clear a malformed persisted rule rather than leave it in the row.
137
144
  const task: TaskRecord = { ...row, status: normalizeStatus(row.status) }
138
145
  task.schedule = normalizeSchedule(row.schedule)
146
+ // Execution targets are normalized like the schedule: blank strings
147
+ // clear the pin and unknown permission strings from a future version
148
+ // fall back to the session default instead of dropping the row.
149
+ task.workspaceId = normalizeTargetId(row.workspaceId)
150
+ task.mode = normalizeTargetId(row.mode)
151
+ task.permission = isTaskPermission(row.permission) ? row.permission as TaskPermission : undefined
139
152
  tasks.push(task)
140
153
  }
141
154
  return tasks
package/src/core/tasks.ts CHANGED
@@ -64,6 +64,32 @@ export interface TaskRecord {
64
64
  executions: ExecutionRecord[]
65
65
  /** Optional scheduled-run rule (absent on tasks without a schedule). */
66
66
  schedule?: ScheduleRule
67
+ /**
68
+ * Workspace the execution must run in (a workspace-list id); absent means
69
+ * the recent-workspace fallback at execution time.
70
+ */
71
+ workspaceId?: string
72
+ /**
73
+ * Agent preset the execution session must be composed from (an
74
+ * `agentPreset.list` id); absent means the deployment default.
75
+ */
76
+ mode?: string
77
+ /**
78
+ * Permission preset applied to the execution session through the
79
+ * `/permission <id>` slash command; absent leaves the session default.
80
+ */
81
+ permission?: TaskPermission
82
+ }
83
+
84
+ /** Permission presets a task may pin on its execution session (the `/permission <id>` ids). */
85
+ export const TASK_PERMISSIONS = ['read-only', 'workspace-write', 'danger-full-access'] as const
86
+
87
+ /** One permission preset id. */
88
+ export type TaskPermission = typeof TASK_PERMISSIONS[number]
89
+
90
+ /** Whether an unknown value is a known permission preset id. */
91
+ export function isTaskPermission(value: unknown): value is TaskPermission {
92
+ return typeof value === 'string' && (TASK_PERMISSIONS as readonly string[]).includes(value)
67
93
  }
68
94
 
69
95
  /** Input for creating a task. */
@@ -71,6 +97,12 @@ export interface NewTaskInput {
71
97
  title: string
72
98
  description: string
73
99
  prompt: string
100
+ /** Workspace the execution must run in; empty/absent = the recent workspace. */
101
+ workspaceId?: string
102
+ /** Agent preset the execution session must be composed from; empty/absent = deployment default. */
103
+ mode?: string
104
+ /** Permission preset applied to the execution session; absent = session default. */
105
+ permission?: TaskPermission
74
106
  }
75
107
 
76
108
  /** The five kanban columns, in display order. */
@@ -103,6 +135,12 @@ export function canMoveManually(_from: TaskStatus, to: TaskStatus): boolean {
103
135
  return (MANUAL_STATUSES as readonly TaskStatus[]).includes(to)
104
136
  }
105
137
 
138
+ /** Normalize one optional execution-target string: trim; blank collapses to undefined. */
139
+ function normalizeTargetId(value: string | undefined): string | undefined {
140
+ const trimmed = value?.trim()
141
+ return trimmed === undefined || trimmed === '' ? undefined : trimmed
142
+ }
143
+
106
144
  /** Create a task from user input. */
107
145
  export function createTask(input: NewTaskInput, now: number, id: string): TaskRecord {
108
146
  return {
@@ -114,6 +152,9 @@ export function createTask(input: NewTaskInput, now: number, id: string): TaskRe
114
152
  createdAt: now,
115
153
  updatedAt: now,
116
154
  executions: [],
155
+ workspaceId: normalizeTargetId(input.workspaceId),
156
+ mode: normalizeTargetId(input.mode),
157
+ permission: isTaskPermission(input.permission) ? input.permission : undefined,
117
158
  }
118
159
  }
119
160
 
@@ -1,12 +1,31 @@
1
1
  /**
2
2
  * Update-task use case: apply an editable-field patch (title/description/
3
- * prompt) with a fresh updatedAt. Pure ledger transition (no persistence or
4
- * notify — the controller orchestrates those).
3
+ * prompt plus the execution targets workspaceId/mode/permission) with a
4
+ * fresh updatedAt. Pure ledger transition (no persistence or notify — the
5
+ * controller orchestrates those).
6
+ *
7
+ * An explicit `undefined` in the patch clears the field (the task falls
8
+ * back to the runtime default); an unknown permission string is ignored so
9
+ * stale UI can never persist a value the execution service rejects.
5
10
  */
6
- import type { TaskRecord } from '../tasks.ts'
11
+ import { isTaskPermission, type TaskRecord, type TaskPermission } from '../tasks.ts'
7
12
 
8
13
  /** Editable fields on a task (the update patch surface). */
9
- export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>
14
+ export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>>
15
+
16
+ /** Collapse a blank target string to undefined (clears the pin). */
17
+ function normalizeTargetId(value: string | undefined): string | undefined {
18
+ return value !== undefined && value.trim() === '' ? undefined : value
19
+ }
20
+
21
+ /** Keep an unknown permission string from entering the ledger. */
22
+ function normalizePermission(
23
+ current: TaskPermission | undefined,
24
+ value: TaskPermission | undefined,
25
+ ): TaskPermission | undefined {
26
+ if (value === undefined) return undefined
27
+ return isTaskPermission(value) ? value : current
28
+ }
10
29
 
11
30
  /**
12
31
  * Apply an update across the ledger. Tasks that do not match the id are left
@@ -22,5 +41,15 @@ export function applyUpdateTask(
22
41
  patch: TaskUpdatePatch,
23
42
  now: number,
24
43
  ): readonly TaskRecord[] {
25
- return tasks.map(task => task.id === id ? { ...task, ...patch, updatedAt: now } : task)
44
+ return tasks.map(task => {
45
+ if (task.id !== id) return task
46
+ const workspaceId = 'workspaceId' in patch ? normalizeTargetId(patch.workspaceId) : undefined
47
+ const mode = 'mode' in patch ? normalizeTargetId(patch.mode) : undefined
48
+ const permission = 'permission' in patch ? normalizePermission(task.permission, patch.permission) : undefined
49
+ const next: TaskRecord = { ...task, ...patch, updatedAt: now }
50
+ if (workspaceId !== undefined || 'workspaceId' in patch) next.workspaceId = workspaceId
51
+ if (mode !== undefined || 'mode' in patch) next.mode = mode
52
+ if (permission !== undefined || 'permission' in patch) next.permission = permission
53
+ return next
54
+ })
26
55
  }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ const SECTION_ORDER = 200
22
22
  export const inject = ['systemPrompt']
23
23
 
24
24
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
25
- export const TASK_BOARD_GUIDANCE = '本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;任务可真实执行(驱动 agent 会话);任务支持 5 段 cron 定时执行(如 0 23 * * *);数据存浏览器 localStorage(键 dsh.taskBoard.v1)。限制:定时调度在浏览器端,需 GUI 标签页打开,错过即跳过;执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。'
25
+ export const TASK_BOARD_GUIDANCE = '本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;任务可真实执行(驱动 agent 会话);任务可钉住执行目标——工作区 / 模式(agent 预设)/ 权限(read-only / workspace-write / danger-full-access),缺省用运行时默认;任务支持 5 段 cron 定时执行(如 0 23 * * *);数据存浏览器 localStorage(键 dsh.taskBoard.v1)。限制:定时调度在浏览器端,需 GUI 标签页打开,错过即跳过;执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。'
26
26
 
27
27
  /**
28
28
  * Settings namespace of the board's announcement capability — the section the