@goodandready/dsh-cron 0.2.27 → 0.2.29

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.
package/README.md CHANGED
@@ -43,7 +43,7 @@ Autonomous AI agents often need to perform recurring duties: generating daily mo
43
43
 
44
44
  1. **Rich Visual Task Manager** — a sidebar button with a collapsible list of active jobs (next run or live state, capped and persisted), plus a full panel to inspect, filter by type/model/channel, pause, trigger, duplicate, export/import and create tasks.
45
45
  2. **Interactive "Create with DSH" Workflow** — chat with your agent to translate high-level requirements into a well-formed scheduled task.
46
- 3. **Autonomous AI Tool Calling** — native `cron_*` tools let agents schedule their own follow-up executions during conversations.
46
+ 3. **Autonomous AI Tool Calling** — single unified `cron` tool (`action: 'create' | 'list' | 'get' | 'update' | 'pause' | 'resume' | 'run' | 'delete'`) lets agents inspect, trigger, and manage background automation without schema bloat.
47
47
  4. **Robust Scheduler & Atomic Storage** — built on `croner` with interval aliases, one-shot delays, atomic file persistence, run histories, and cost tracking.
48
48
  5. **Six Execution Runtimes** — shell, Node.js, Python, HTTP/webhook, remote SSH and Docker, plus per-task environment variables, workspace binding and isolated git worktrees for code-modifying agent tasks.
49
49
  6. **Multi-Channel Delivery With Templates** — one run fans out to Telegram, dsh-kanban, Discord, Slack, ntfy, Bark, PushPlus, voice (`dsh-tts`) and Gitea, with `{variable}` message templates and secrets referenced by DSH credential name.
@@ -64,7 +64,7 @@ graph TD
64
64
 
65
65
  subgraph Server ["Server Runtime (Cordis & DSH Services)"]
66
66
  HttpRoutes["HTTP REST API<br/>(/dsh-cron/*)"]
67
- AgentTools["AI Tool Calling Gateway<br/>(cron_create_task, cron_list_tasks, ...)"]
67
+ AgentTools["AI Tool Calling Gateway<br/>(cron)"]
68
68
  Scheduler["TaskScheduler Engine<br/>(Croner instances + one-shot timers)"]
69
69
  Store["Atomic TaskStore<br/>(tasks.json with atomic write)"]
70
70
  AgentRunner["Agent Session Dispatcher<br/>(Executes prompt with chosen model)"]
@@ -104,27 +104,49 @@ Click the clock icon in the DSH sidebar (positioned next to the new-session butt
104
104
  Transform natural language into a scheduled job without guessing cron syntax:
105
105
  1. Click **Create ⌄** ➔ **Create with DSH**.
106
106
  2. Describe what you want to automate (e.g. *"Check open PRs every weekday at 9:00 and draft review comments"*).
107
- 3. The plugin spawns a dedicated agent session pre-injected with scheduler instructions. The agent clarifies the details with you — LLM vs no-LLM shell task, the exact cron expression, an economical model from those available in your DSH installation, and whether a "silent rule" (alert only on new events or failures) should apply — and registers the task through the `cron_create_task` tool only after your confirmation.
108
-
109
- ### 3. Agent Tools (Tool Calling)
110
- Autonomous agents can manage schedules directly:
111
-
112
- | Tool | Description |
113
- |:---|:---|
114
- | `cron_create_task` | Creates a scheduled task: `title`, `schedule`, `prompt`, `fallbackModel` (one retry on a stronger model when a run fails), optional `type` (`llm`/`script`/`node`/`python`/`http`/`ssh`/`docker`/`skill`/`workflow`), `delivery`, `provider`, `model`, `channels`, `template`, `notifyTelegram`, `onlyOnFailure`, `timeoutSeconds`, `overlapPolicy`, `kanbanMode` |
115
- | `cron_schedule_task` | Alias of `cron_create_task` kept for compatibility with existing agent prompts |
116
- | `cron_list_tasks` | Lists tasks with statuses, next run timestamps, token totals, and cost estimates |
117
- | `cron_pause_task` | Pauses a schedule without deleting its configuration |
118
- | `cron_resume_task` | Resumes a paused schedule |
119
- | `cron_delete_task` | Permanently removes a task and its history |
120
- | `cron_run_task` | Triggers an immediate out-of-band run |
121
- | `cron_get_task` | Reads the full configuration of one task, including fields the list does not show |
122
- | `cron_update_task` | Changes an existing task in place (whitelisted fields, same validation as the HTTP route); the model is told to confirm code-executing changes with the user first |
107
+ 3. The plugin spawns a dedicated agent session pre-injected with scheduler instructions. The agent clarifies the details with you — LLM vs no-LLM shell task, the exact cron expression, an economical model from those available in your DSH installation, and whether a "silent rule" (alert only on new events or failures) should apply — evaluates simple in-chat reminder (`schedule_create`) vs background automation (`cron`), and registers the task through the `cron` tool (`action: 'create'`) only after your confirmation.
108
+
109
+ ### 2. DSH Core schedule vs dsh-cron
110
+
111
+ DeepSeek Harness includes a lightweight built-in `@deepseek-ai/dsh-schedule` extension for basic in-chat reminders. Use this guide to choose the right tool:
112
+
113
+ | Capability | DSH Core `schedule` (`@deepseek-ai/dsh-schedule`) | `@goodandready/dsh-cron` |
114
+ |:---|:---|:---|
115
+ | **Primary Purpose** | In-chat reminders & timed prompts back into the active dialogue | Unattended background automation runner & orchestrator |
116
+ | **Execution Context** | Active conversation session | Isolated dedicated agent sessions or background runner |
117
+ | **Runtimes** | Chat session turn only (LLM prompt) | 9 runtimes: `llm`, `script` (bash/sh), `node`, `python`, `http` (REST/webhook), `ssh`, `docker`, `skill`, `workflow` |
118
+ | **Model Tools** | `schedule_create`, `schedule_list`, `schedule_delete` | Unified `cron` tool (action: `create`, `list`, `get`, `update`, `pause`, `resume`, `run`, `delete`) |
119
+ | **Tool Schema Footprint** | ~1.5k characters | ~1.5k characters (consolidated from 9 tools down to 1, saving ~12k characters of LLM context) |
120
+ | **Delivery Channels** | Current chat only | Multi-channel: Telegram, Discord, Slack, Webhook, Kanban, ntfy, Bark, PushPlus, Voice (TTS), Gitea |
121
+ | **Code Modifying Isolation** | None | Ephemeral or retained git worktrees (`worktree: true`) |
122
+ | **Cost & Token Limits** | None | Guard rails: `costLimitUsd`, `dailyCostLimitUsd`, `tokenLimit` auto-pausing |
123
+ | **Failure Handling & Health** | None | Automatic retries with exponential backoff, failure inspector, Dead Man's Snitch / Better Uptime heartbeats |
124
+ | **Silent Rule** | None | Suppress delivery when no new events or changes occur (zero noise / zero spam) |
125
+ | **Task Management** | Basic list / delete | Full UI manager, execution history, log viewer, run metrics, manual trigger, import/export, profile config sync |
126
+
127
+ ### 3. Agent Tool (`cron`)
128
+
129
+ Autonomous agents manage schedules directly through a single unified `cron` tool, keeping LLM schema overhead minimal:
130
+
131
+ | Action | Description | Key Parameters |
132
+ |:---|:---|:---|
133
+ | `create` | Creates a new background scheduled task or automation job | `title`, `schedule`, `prompt`, `type`, `model`, `channels`, `delivery`, etc. |
134
+ | `list` | Lists tasks with status, next run timestamp, tokens, and cost | `status` (`'all'`, `'active'`, `'paused'`, `'completed'`) |
135
+ | `get` | Reads the full detailed configuration of one task | `id` |
136
+ | `update` | Modifies an existing task in place (requires `confirmCodeSwitch: true` when switching to code-executing runtimes) | `id`, patch fields |
137
+ | `pause` | Pauses an active schedule without deleting its configuration | `id` |
138
+ | `resume` | Resumes a paused schedule | `id` |
139
+ | `run` | Triggers an immediate out-of-band execution | `id` |
140
+ | `delete` | Permanently removes a task and its run history | `id` |
141
+
142
+ > [!NOTE]
143
+ > **Context Optimization & Migration**: Previously, 9 separate tool schemas consumed ~13.6k characters of context in every model turn. The consolidated `cron` tool cuts this footprint by ~88% down to ~1.5k characters. Legacy tool names (`cron_create_task`, `cron_schedule_task`, `cron_list_tasks`, etc.) are gracefully rejected with helpful guidance directing the model to `cron` with the matching `action`. For simple in-conversation reminders, models are instructed to recommend core `schedule_create`.
123
144
 
124
145
  Example invocation the model can make during a conversation:
125
146
 
126
- ```
127
- cron_create_task({
147
+ ```json
148
+ cron({
149
+ "action": "create",
128
150
  "title": "Morning digest",
129
151
  "schedule": "0 8 * * 1-5",
130
152
  "prompt": "Prepare a brief morning digest of active tasks and open tickets.",
@@ -517,7 +539,7 @@ Notes:
517
539
 
518
540
  ## 🔌 HTTP API Reference
519
541
 
520
- All endpoints are served by the DSH web server under `/dsh-cron/`. Read endpoints are open to the local UI; **mutating endpoints reject cross-origin requests** and accept bodies up to 1 MB. Creating `script`-type tasks over HTTP additionally requires the `x-dsh-cron-confirm: script` header, which forged cross-site posts cannot attach.
542
+ All endpoints are served by the DSH web server under `/dsh-cron/`. Endpoints are protected by a hardened HTTP source guard (`isTrustedRequest`): non-loopback remote callers require token authentication (`Authorization: Bearer <token>` or `x-dsh-cron-token`), and browser requests enforce strict `Host` and `Origin` matching, reject `Origin: null`, and restrict `Sec-Fetch-Site` to `same-origin` or `none`. Heartbeat ping routes strictly require the `POST` method. Task `GET` responses automatically redact sensitive fields (`env`, `httpHeaders`, `httpBody`) as `'[REDACTED]'`, and update operations preserve existing secrets when `'[REDACTED]'` is passed. Creating `script`-type tasks over HTTP additionally requires the `x-dsh-cron-confirm: script` header, which forged cross-site posts cannot attach. Body size is capped at 1 MB.
521
543
 
522
544
  | Method | Path | Description |
523
545
  |:---|:---|:---|
package/README.ru.md CHANGED
@@ -64,7 +64,7 @@ graph TD
64
64
 
65
65
  subgraph Server ["Серверная часть (Cordis и сервисы DSH)"]
66
66
  HttpRoutes["HTTP REST API<br/>(/dsh-cron/*)"]
67
- AgentTools["Шлюз tool calling<br/>(cron_create_task, cron_list_tasks, ...)"]
67
+ AgentTools["Шлюз tool calling<br/>(cron)"]
68
68
  Scheduler["Движок TaskScheduler<br/>(экземпляры Croner + таймеры one-shot)"]
69
69
  Store["Атомарный TaskStore<br/>(tasks.json, атомарная запись)"]
70
70
  AgentRunner["Диспетчер агентских сессий<br/>(запуск промпта выбранной моделью)"]
@@ -104,26 +104,49 @@ graph TD
104
104
  Превратите естественный язык в задачу без подбора cron-синтаксиса:
105
105
  1. Нажмите **Создать ⌄** ➔ **Создать с DSH**.
106
106
  2. Опишите, что нужно автоматизировать (например: *«Проверяй открытые PR по будням в 9:00 и готовь черновики комментариев»*).
107
- 3. Плагин создаст отдельную агентскую сессию с системными инструкциями планировщика. Агент уточнит детали — LLM или NO-LLM shell-задача, точное cron-выражение, экономичная модель из доступных в вашей установке DSH, нужно ли «правило тишины» (алерт только при новых событиях или сбоях) — и создаст задачу через инструмент `cron_create_task` только после вашего подтверждения.
108
-
109
- ### 3. Инструменты агентов (tool calling)
110
-
111
- | Инструмент | Описание |
112
- |:---|:---|
113
- | `cron_create_task` | Создаёт задачу: `title`, `schedule`, `prompt`, `fallbackModel` (одна повторная попытка на сильной модели при сбое), опционально `type` (`llm`/`script`/`node`/`python`/`http`/`ssh`/`docker`/`skill`/`workflow`), `delivery`, `provider`, `model`, `channels`, `template`, `notifyTelegram`, `onlyOnFailure`, `timeoutSeconds`, `overlapPolicy`, `kanbanMode` |
114
- | `cron_schedule_task` | Псевдоним `cron_create_task` для совместимости с существующими промптами |
115
- | `cron_list_tasks` | Список задач со статусами, временем следующего запуска, токенами и стоимостью |
116
- | `cron_pause_task` | Приостанавливает расписание без удаления конфигурации |
117
- | `cron_resume_task` | Возобновляет приостановленное расписание |
118
- | `cron_delete_task` | Полностью удаляет задачу и её историю |
119
- | `cron_run_task` | Немедленный внеплановый запуск |
120
- | `cron_get_task` | Полная конфигурация одной задачи, включая поля, которых нет в списке |
121
- | `cron_update_task` | Изменяет существующую задачу на месте (whitelisted-поля, та же валидация, что у HTTP-маршрута); модели предписано сперва подтверждать с пользователем изменения, исполняющие код |
107
+ 3. Плагин создаст отдельную агентскую сессию с системными инструкциями планировщика. Агент уточнит детали — LLM или NO-LLM shell-задача, точное cron-выражение, экономичная модель из доступных в вашей установке DSH, нужно ли «правило тишины» (алерт только при новых событиях или сбоях) — оценивает тип задачи (простое напоминание в текущем чате через `schedule_create` ядра vs фоновая автоматизация через `cron`) и создаёт задачу через инструмент `cron` (`action: 'create'`) только после вашего подтверждения.
108
+
109
+ ### 2. Встроенный schedule ядра DSH vs dsh-cron
110
+
111
+ В состав DeepSeek Harness входит легковесное встроенное расширение `@deepseek-ai/dsh-schedule` для базовых напоминаний в чате. Сравнительная таблица помогает выбрать правильный инструмент:
112
+
113
+ | Возможность | Встроенный `schedule` ядра (`@deepseek-ai/dsh-schedule`) | `@goodandready/dsh-cron` |
114
+ |:---|:---|:---|
115
+ | **Основное назначение** | Напоминания и таймеры, доставляемые обратно в текущий диалог пользователя | Автономный фоновый раннер и оркестратор запланированных задач |
116
+ | **Контекст исполнения** | Текущая активная сессия чата | Изолированные выделенные агентские сессии или фоновые процессы |
117
+ | **Рантаймы** | Только ход в диалоге (LLM-промпт) | 9 рантаймов: `llm`, `script` (bash/sh), `node`, `python`, `http` (REST/webhook), `ssh`, `docker`, `skill`, `workflow` |
118
+ | **Инструменты модели** | `schedule_create`, `schedule_list`, `schedule_delete` | Единый инструмент `cron` (action: `create`, `list`, `get`, `update`, `pause`, `resume`, `run`, `delete`) |
119
+ | **Размер схемы инструментов** | ~1.5k символов | ~1.5k символов (консолидация 9 инструментов в 1, экономия ~12k символов контекста) |
120
+ | **Каналы доставки** | Только текущий чат | Мультиканальность: Telegram, Discord, Slack, Webhook, Kanban, ntfy, Bark, PushPlus, Голос (TTS), Gitea |
121
+ | **Изоляция кода** | Отсутствует | Одноразовые или сохраняемые git worktree (`worktree: true`) |
122
+ | **Лимиты расходов и токенов** | Отсутствуют | Защита от перерасхода: `costLimitUsd`, `dailyCostLimitUsd`, `tokenLimit` с автопаузой |
123
+ | **Надёжность и мониторинг** | Отсутствуют | Повторы с экспоненциальным backoff, failure inspector, heartbeat-пинги (Dead Man's Snitch / Better Uptime) |
124
+ | **Тихое правило (Silent Rule)** | Отсутствует | Полная тишина при отсутствии изменений (ноль шума и спама в каналы) |
125
+ | **Управление задачами** | Базовый список / удаление | Полноценный UI-менеджер, история запусков, логи, метрики, ручной запуск, импорт/экспорт, синхронизация с конфигом |
126
+
127
+ ### 3. Инструмент агента (`cron`)
128
+
129
+ Автономные агенты управляют расписаниями через единый компактный инструмент `cron`:
130
+
131
+ | Действие (action) | Описание | Основные параметры |
132
+ |:---|:---|:---|
133
+ | `create` | Создаёт фоновую задачу или периодический процесс | `title`, `schedule`, `prompt`, `type`, `model`, `channels`, `delivery` и др. |
134
+ | `list` | Список задач со статусами, временем следующего запуска, токенами и стоимостью | `status` (`'all'`, `'active'`, `'paused'`, `'completed'`) |
135
+ | `get` | Полная конфигурация одной задачи по её идентификатору | `id` |
136
+ | `update` | Изменяет существующую задачу на месте (переключение в код требует `confirmCodeSwitch: true`) | `id`, поля патча |
137
+ | `pause` | Приостанавливает расписание без удаления конфигурации | `id` |
138
+ | `resume` | Возобновляет приостановленное расписание | `id` |
139
+ | `run` | Немедленный внеплановый запуск задачи | `id` |
140
+ | `delete` | Полностью удаляет задачу и её историю | `id` |
141
+
142
+ > [!NOTE]
143
+ > **Оптимизация контекста и миграция**: Ранее 9 отдельных схем инструментов занимали ~13.6k символов в каждом ходе модели. Консолидация в один инструмент `cron` сократила этот объём на ~88% до ~1.5k символов. Легаси-имена инструментов (`cron_create_task`, `cron_schedule_task`, `cron_list_tasks` и т.д.) отклоняются с понятной подсказкой использовать `cron` с соответствующим `action`. Для простых напоминаний в диалоге модель ориентируется на встроенный `schedule_create`.
122
144
 
123
145
  Пример вызова модели в диалоге:
124
146
 
125
- ```
126
- cron_create_task({
147
+ ```json
148
+ cron({
149
+ "action": "create",
127
150
  "title": "Утренняя сводка",
128
151
  "schedule": "0 8 * * 1-5",
129
152
  "prompt": "Подготовь короткую утреннюю сводку активных задач и открытых тикетов.",
@@ -506,7 +529,7 @@ bash deploy.sh verify [exact-version]
506
529
 
507
530
  ## 🔌 HTTP API
508
531
 
509
- Все эндпоинты обслуживаются веб-сервером DSH под `/dsh-cron/`. Чтение открыто локальному интерфейсу; **мутирующие эндпоинты отклоняют cross-origin запросы** и принимают тела до 1 МБ. Для создания `script`-задач по HTTP дополнительно требуется заголовок `x-dsh-cron-confirm: script`, который подделанный межсайтовый запрос приложить не может.
532
+ Все эндпоинты обслуживаются веб-сервером DSH под `/dsh-cron/`. Эндпоинты защищены строгой проверкой источника (`isTrustedRequest`): удалённые не-loopback клиенты требуют аутентификацию токеном (`Authorization: Bearer <token>` или `x-dsh-cron-token`), а браузерные запросы проходят строгую валидацию совпадения `Host` и `Origin` с блокировкой `Origin: null` и ограничением `Sec-Fetch-Site` (`same-origin` или `none`). Маршруты heartbeat-ping строго требуют метод `POST`. В ответах `GET` чувствительные данные задач (`env`, `httpHeaders`, `httpBody`) маскируются как `'[REDACTED]'`, а при обновлении задач переданные плейсхолдеры `'[REDACTED]'` сохраняют оригинальные сохранённые секреты. Для создания `script`-задач по HTTP дополнительно требуется заголовок `x-dsh-cron-confirm: script`. Максимальный размер тела запроса ограничен 1 МБ.
510
533
 
511
534
  | Метод | Путь | Описание |
512
535
  |:---|:---|:---|
package/README.zh.md CHANGED
@@ -64,7 +64,7 @@ graph TD
64
64
 
65
65
  subgraph Server ["服务端 (Cordis 与 DSH 服务)"]
66
66
  HttpRoutes["HTTP REST API<br/>(/dsh-cron/*)"]
67
- AgentTools["工具调用网关<br/>(cron_create_task, cron_list_tasks, ...)"]
67
+ AgentTools["工具调用网关<br/>(cron)"]
68
68
  Scheduler["TaskScheduler 引擎<br/>(Croner 实例 + one-shot 定时器)"]
69
69
  Store["原子 TaskStore<br/>(tasks.json 原子写入)"]
70
70
  AgentRunner["智能体会话调度器<br/>(以指定模型执行提示词)"]
@@ -104,26 +104,49 @@ graph TD
104
104
  无需猜测 cron 语法,用自然语言即可创建任务:
105
105
  1. 点击 **Create ⌄** ➔ **Create with DSH**。
106
106
  2. 描述要自动化的内容(例如:*“每个工作日早上 9 点检查未处理的 PR 并起草评论”*)。
107
- 3. 插件会创建一个注入了调度器指令的专属智能体会话。智能体会与你确认细节 —— LLM 还是 NO-LLM shell 任务、准确的 cron 表达式、在你的 DSH 安装中可用的经济型模型,以及是否启用“静默规则”(仅在新事件或故障时告警)—— 并在你确认后才通过 `cron_create_task` 工具注册任务。
108
-
109
- ### 3. 智能体工具(Tool Calling)
110
-
111
- | 工具 | 说明 |
112
- |:---|:---|
113
- | `cron_create_task` | 创建任务:`title`、`schedule`、`prompt`、`fallbackModel`(失败时改用更强模型重试一次),可选 `type`(`llm`/`script`/`node`/`python`/`http`/`ssh`/`docker`/`skill`/`workflow`)、`delivery`、`provider`、`model`、`channels`、`template`、`notifyTelegram`、`onlyOnFailure`、`timeoutSeconds`、`overlapPolicy`、`kanbanMode` |
114
- | `cron_schedule_task` | `cron_create_task` 的别名,保持与既有提示词兼容 |
115
- | `cron_list_tasks` | 列出任务的状态、下次运行时间、token 总量与成本估算 |
116
- | `cron_pause_task` | 暂停调度而不删除配置 |
117
- | `cron_resume_task` | 恢复已暂停的调度 |
118
- | `cron_delete_task` | 永久删除任务及其历史 |
119
- | `cron_run_task` | 触发一次立即的带外运行 |
120
- | `cron_get_task` | 读取单个任务的完整配置,包括列表中看不到的字段 |
121
- | `cron_update_task` | 就地修改现有任务(白名单字段,校验与 HTTP 路由一致);提示模型先与用户确认会执行代码的改动 |
107
+ 3. 插件会创建一个注入了调度器指令的专属智能体会话。智能体会与你确认细节 —— LLM 还是 NO-LLM shell 任务、准确的 cron 表达式、在你的 DSH 安装中可用的经济型模型,以及是否启用“静默规则”(仅在新事件或故障时告警)—— 评估属于对话内定时提醒(`schedule_create`)还是后台自动化(`cron`),并在获得您的确认后通过 `cron` 工具(`action: 'create'`)注册任务。
108
+
109
+ ### 2. DSH 核心内置 schedule 与 dsh-cron 对比
110
+
111
+ DeepSeek Harness 内置了轻量级扩展 `@deepseek-ai/dsh-schedule`,用于会话内的基础定时提醒。下表帮助您根据场景选择合适的工具:
112
+
113
+ | 功能维度 | DSH 核心 `schedule` (`@deepseek-ai/dsh-schedule`) | `@goodandready/dsh-cron` |
114
+ |:---|:---|:---|
115
+ | **主要定位** | 当前会话内的定时提醒与催办消息 | 无人值守的后台自动化执行器与任务编排引擎 |
116
+ | **执行上下文** | 当前活动会话内 | 独立的隔离智能体会话或外部后台进程 |
117
+ | **执行运行时** | 仅当前会话提示词(LLM) | 9 种运行时:`llm`、`script` (bash/sh)、`node`、`python`、`http` (REST/webhook)、`ssh`、`docker`、`skill`、`workflow` |
118
+ | **模型工具** | `schedule_create`、`schedule_list`、`schedule_delete` | 统一 `cron` 工具(action: `create`、`list`、`get`、`update`、`pause`、`resume`、`run`、`delete`) |
119
+ | **工具模式体积** | 约 1.5k 字符 | 约 1.5k 字符(由 9 个工具合并为 1 个,节省约 12k 字符上下文) |
120
+ | **推送渠道** | 仅限当前会话 | 多渠道:Telegram、Discord、Slack、Webhook、Kanban、ntfy、Bark、PushPlus、语音 (TTS)、Gitea |
121
+ | **代码修改隔离** | 无 | 临时或保留的 git worktree 隔离环境(`worktree: true`) |
122
+ | **成本与 Token 限制** | 无 | 成本熔断防护:`costLimitUsd`、`dailyCostLimitUsd`、`tokenLimit` 自动暂停 |
123
+ | **容错与健康检查** | 无 | 指数退避自动重试、失败自动诊断、心跳监测 (Dead Man's Snitch / Better Uptime) |
124
+ | **静默规则 (Silent Rule)** | 无 | 无新事件或变更时完全静默(杜绝通道垃圾消息) |
125
+ | **任务管理** | 基础列表与删除 | 完整 UI 管理器、运行历史、日志查看器、指标统计、手动触发、导入导出、配置同步 |
126
+
127
+ ### 3. 智能体工具 (`cron`)
128
+
129
+ 自主智能体通过单个统一的 `cron` 工具直接管理定时任务,大幅降低模型模式开销:
130
+
131
+ | 动作 (action) | 说明 | 核心参数 |
132
+ |:---|:---|:---|
133
+ | `create` | 创建新的后台定时任务或自动化作业 | `title`、`schedule`、`prompt`、`type`、`model`、`channels`、`delivery` 等 |
134
+ | `list` | 列出任务的状态、下次运行时间、token 总量与成本估算 | `status` (`'all'`、`'active'`、`'paused'`、`'completed'`) |
135
+ | `get` | 根据任务 ID 获取单项任务的完整配置 | `id` |
136
+ | `update` | 就地修改现有任务(切换到代码执行运行时需 `confirmCodeSwitch: true`) | `id`、修改字段 |
137
+ | `pause` | 暂停调度而不删除配置 | `id` |
138
+ | `resume` | 恢复已暂停的调度 | `id` |
139
+ | `run` | 触发一次立即的带外运行 | `id` |
140
+ | `delete` | 永久删除任务及其历史 | `id` |
141
+
142
+ > [!NOTE]
143
+ > **上下文优化与平滑迁移**:此前 9 个单独的工具模式在每次模型轮次中消耗约 13.6k 字符。整合为单一 `cron` 工具后,模式开销减少约 88%(降至约 1.5k 字符)。旧工具名(`cron_create_task`、`cron_schedule_task`、`cron_list_tasks` 等)被优雅拦截,并返回清晰迁移提示,引导模型使用带对应 `action` 的 `cron` 工具。对于简单的会话内提醒,模型将建议使用核心内置的 `schedule_create`。
122
144
 
123
145
  会话中模型可进行的调用示例:
124
146
 
125
- ```
126
- cron_create_task({
147
+ ```json
148
+ cron({
149
+ "action": "create",
127
150
  "title": "Morning digest",
128
151
  "schedule": "0 8 * * 1-5",
129
152
  "prompt": "Prepare a brief morning digest of active tasks and open tickets.",
@@ -506,7 +529,7 @@ dsh-cron:
506
529
 
507
530
  ## 🔌 HTTP API 参考
508
531
 
509
- 所有端点由 DSH Web 服务器在 `/dsh-cron/` 下提供。读端点对本地 UI 开放;**变更端点拒绝跨域请求**且请求体最大 1 MB。通过 HTTP 创建 `script` 类型任务还需要 `x-dsh-cron-confirm: script` 请求头 —— 伪造的跨站请求无法附加该头。
532
+ 所有端点由 DSH Web 服务器在 `/dsh-cron/` 下提供。所有端点均受到强化的 HTTP 来源防护(`isTrustedRequest`):非回环远程客户端必须携带有效令牌(`Authorization: Bearer <token>` 或 `x-dsh-cron-token`),浏览器请求严格校验 `Host` 与 `Origin` 一致性并拒绝 `Origin: null`,同时限制 `Sec-Fetch-Site` 仅允许 `same-origin` 或 `none`。心跳 ping 端点严格要求 `POST` 方法。任务 `GET` 响应自动将敏感字段(`env`、`httpHeaders`、`httpBody`)掩码为 `'[REDACTED]'`,并在更新操作提交 `'[REDACTED]'` 时安全保留已有原密钥。通过 HTTP 创建 `script` 类型任务还需要 `x-dsh-cron-confirm: script` 请求头。请求体大小上限为 1 MB。
510
533
 
511
534
  | 方法 | 路径 | 说明 |
512
535
  |:---|:---|:---|
@@ -4,6 +4,13 @@ import { sendJson, readBody, rejectCrossOrigin } from './http-utils.js';
4
4
  const NOT_ALLOWED = { ok: false, error: 'Method not allowed' };
5
5
 
6
6
  export function handleHeartbeatPing({ store, req, res, taskId }) {
7
+ if (req.method !== 'POST') {
8
+ sendJson(res, 405, NOT_ALLOWED);
9
+ return;
10
+ }
11
+ const apiToken = store?.getSettings?.()?.apiToken;
12
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
13
+
7
14
  const task = store.get(taskId);
8
15
  if (!task) {
9
16
  sendJson(res, 404, { ok: false, error: 'Task not found' });
@@ -17,7 +24,10 @@ export function handleHeartbeatPing({ store, req, res, taskId }) {
17
24
  sendJson(res, 200, result);
18
25
  }
19
26
 
20
- export async function handleSchedulePreview({ req, res, url }) {
27
+ export async function handleSchedulePreview({ req, res, url, store }) {
28
+ const apiToken = store?.getSettings?.()?.apiToken;
29
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
30
+
21
31
  let schedule = '';
22
32
  let timezone = '';
23
33
  let count = 5;
@@ -51,7 +61,8 @@ export async function handleSchedulePreview({ req, res, url }) {
51
61
  }
52
62
 
53
63
  export async function handleDryRunTask({ store, scheduler, req, res, id }) {
54
- if (rejectCrossOrigin(req, res)) return;
64
+ const apiToken = store?.getSettings?.()?.apiToken;
65
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
55
66
  const task = store.get(id);
56
67
  if (!task) {
57
68
  sendJson(res, 404, { ok: false, error: 'Task not found' });
@@ -65,4 +76,3 @@ export async function handleDryRunTask({ store, scheduler, req, res, id }) {
65
76
  sendJson(res, 500, { ok: false, error: err.message });
66
77
  }
67
78
  }
68
-
package/lib/api.js CHANGED
@@ -11,7 +11,7 @@ import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
11
11
  import { CHANNEL_IDS, unknownChannelIds } from './channels.js';
12
12
  import { applyTaskPatch } from './task-patch.js';
13
13
  import { MANAGED_BY_CONFIG, configOwnedMessage } from './config-jobs.js';
14
- import { sendJson, rejectCrossOrigin, readBody, pickPatchableFields, SCRIPT_CONFIRM_HEADER } from './http-utils.js';
14
+ import { sendJson, rejectCrossOrigin, readBody, pickPatchableFields, SCRIPT_CONFIRM_HEADER, redactTaskSecrets, restoreTaskSecrets } from './http-utils.js';
15
15
  import {
16
16
  validateTaskType,
17
17
  buildDuplicateTask,
@@ -64,9 +64,10 @@ function refuseConfigOwned(res, id) {
64
64
  export function createCronApiHandler(store, scheduler, collection) {
65
65
  return async function handleCronApi(req, res) {
66
66
  try {
67
+ const apiToken = store?.getSettings?.()?.apiToken;
67
68
  const url = new URL(req.url, 'http://127.0.0.1');
68
69
  const parts = url.pathname.split('/').filter(Boolean);
69
- const scope = parts[1]; // 'tasks' | 'action'
70
+ const scope = parts[1]; // 'tasks' | 'action' | 'heartbeat-ping'
70
71
  const id = parts[2];
71
72
  const action = parts[3];
72
73
 
@@ -75,22 +76,22 @@ export function createCronApiHandler(store, scheduler, collection) {
75
76
  return;
76
77
  }
77
78
 
78
- if (scope === 'heartbeat' && id) {
79
+ if ((scope === 'heartbeat' || scope === 'heartbeat-ping') && id) {
79
80
  handleHeartbeatPing({ store, req, res, taskId: id });
80
81
  return;
81
82
  }
82
83
 
83
84
  if (scope === 'schedule' && id === 'preview') {
84
- await handleSchedulePreview({ req, res, url });
85
+ await handleSchedulePreview({ req, res, url, store });
85
86
  return;
86
87
  }
87
88
 
88
89
  if (!id) {
89
- await handleTaskCollection({ store, scheduler, collection, req, res, url, scope });
90
+ await handleTaskCollection({ store, scheduler, collection, req, res, url, scope, apiToken });
90
91
  return;
91
92
  }
92
93
 
93
- if (req.method !== 'GET' && rejectCrossOrigin(req, res)) return;
94
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
94
95
 
95
96
  // Export/import are collection operations that live under /tasks/:id/…,
96
97
  // so they are matched before the generic id handling below.
@@ -103,7 +104,7 @@ export function createCronApiHandler(store, scheduler, collection) {
103
104
  return;
104
105
  }
105
106
 
106
- await handleTaskItem({ store, scheduler, req, res, url, id, action });
107
+ await handleTaskItem({ store, scheduler, req, res, url, id, action, apiToken });
107
108
  } catch (err) {
108
109
  sendJson(res, err.statusCode || 500, { ok: false, error: err.message });
109
110
  }
@@ -112,18 +113,19 @@ export function createCronApiHandler(store, scheduler, collection) {
112
113
 
113
114
  // ------------------------------------------------------------- collection
114
115
 
115
- async function handleTaskCollection({ store, scheduler, collection, req, res, url, scope }) {
116
+ async function handleTaskCollection({ store, scheduler, collection, req, res, url, scope, apiToken }) {
116
117
  // The alias family only exists for individual tasks.
117
118
  if (scope === 'action' || !collection) {
118
119
  sendJson(res, 405, NOT_ALLOWED);
119
120
  return;
120
121
  }
121
122
  if (req.method === 'GET') {
123
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
122
124
  listTasks({ store, scheduler, collection, req, res, url });
123
125
  return;
124
126
  }
125
127
  if (req.method === 'POST') {
126
- await createOrUpdateTask({ store, scheduler, req, res });
128
+ await createOrUpdateTask({ store, scheduler, req, res, apiToken });
127
129
  return;
128
130
  }
129
131
  sendJson(res, 405, NOT_ALLOWED);
@@ -133,7 +135,7 @@ function listTasks({ store, scheduler, collection, req, res, url }) {
133
135
  const status = url.searchParams.get('status') || 'all';
134
136
  const query = url.searchParams.get('query') || '';
135
137
  const list = store.list({ status, query }).map((t) => ({
136
- ...t,
138
+ ...redactTaskSecrets(t),
137
139
  running: scheduler.isRunning(t.id),
138
140
  runningSince: scheduler.runningSince(t.id),
139
141
  }));
@@ -160,10 +162,12 @@ function listTasks({ store, scheduler, collection, req, res, url }) {
160
162
  sendJson(res, 200, payload);
161
163
  }
162
164
 
163
- async function createOrUpdateTask({ store, scheduler, req, res }) {
164
- if (rejectCrossOrigin(req, res)) return;
165
- const { body, error } = await readBody(req, res);
165
+ async function createOrUpdateTask({ store, scheduler, req, res, apiToken }) {
166
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
167
+ const { body: rawBody, error } = await readBody(req, res);
166
168
  if (error) return;
169
+ const existing = rawBody.id ? store.get(rawBody.id) : null;
170
+ const body = existing ? restoreTaskSecrets(rawBody, existing) : rawBody;
167
171
  if (!body.title || !body.schedule || !body.prompt) {
168
172
  sendJson(res, 400, { ok: false, error: 'Fields title, schedule and prompt are required' });
169
173
  return;
@@ -211,7 +215,7 @@ async function createOrUpdateTask({ store, scheduler, req, res }) {
211
215
  } else {
212
216
  scheduler.pauseTask(task.id);
213
217
  }
214
- sendJson(res, 200, { ok: true, task });
218
+ sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
215
219
  }
216
220
 
217
221
  /** Merge a create/update payload onto the stored task, if any. */
@@ -249,8 +253,8 @@ function buildTaskRecord(store, body, taskType, parsed) {
249
253
 
250
254
  // ------------------------------------------------------------ single task
251
255
 
252
- async function handleTaskItem({ store, scheduler, req, res, url, id, action }) {
253
- if ((req.method === 'GET' || req.method === 'POST') && action === 'heartbeat') {
256
+ async function handleTaskItem({ store, scheduler, req, res, url, id, action, apiToken }) {
257
+ if (action === 'heartbeat') {
254
258
  handleHeartbeatPing({ store, req, res, taskId: id });
255
259
  return;
256
260
  }
@@ -260,25 +264,44 @@ async function handleTaskItem({ store, scheduler, req, res, url, id, action }) {
260
264
  return;
261
265
  }
262
266
 
263
- if (req.method === 'GET' && action === 'archive') {
264
- const limit = parseInt(url.searchParams.get('limit') || '50', 10);
265
- const offset = parseInt(url.searchParams.get('offset') || '0', 10);
266
- const search = url.searchParams.get('search') || '';
267
- const result = typeof store.getArchivedRuns === 'function'
268
- ? store.getArchivedRuns(id, { limit, offset, search })
269
- : { runs: [], total: 0 };
270
- sendJson(res, 200, { ok: true, taskId: id, ...result });
271
- return;
272
- }
273
- if (req.method === 'GET' && action === 'stats') {
274
- const stats = typeof store.getTaskStats === 'function' ? store.getTaskStats(id) : null;
275
- sendJson(res, 200, { ok: true, taskId: id, stats });
276
- return;
277
- }
278
- if (req.method === 'GET' && (action === 'history' || !action)) {
279
- const limit = parseInt(url.searchParams.get('limit') || '20', 10);
280
- sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
281
- return;
267
+ if (req.method === 'GET') {
268
+ if (rejectCrossOrigin(req, res, { apiToken })) return;
269
+ if (action === 'archive') {
270
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
271
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
272
+ const search = url.searchParams.get('search') || '';
273
+ const result = typeof store.getArchivedRuns === 'function'
274
+ ? store.getArchivedRuns(id, { limit, offset, search })
275
+ : { runs: [], total: 0 };
276
+ sendJson(res, 200, { ok: true, taskId: id, ...result });
277
+ return;
278
+ }
279
+ if (action === 'stats') {
280
+ const stats = typeof store.getTaskStats === 'function' ? store.getTaskStats(id) : null;
281
+ sendJson(res, 200, { ok: true, taskId: id, stats });
282
+ return;
283
+ }
284
+ if (action === 'history') {
285
+ const limit = parseInt(url.searchParams.get('limit') || '20', 10);
286
+ sendJson(res, 200, { ok: true, history: store.getHistory(id, limit) });
287
+ return;
288
+ }
289
+ if (!action) {
290
+ const task = store.get(id);
291
+ if (!task) {
292
+ sendJson(res, 404, NOT_FOUND);
293
+ return;
294
+ }
295
+ sendJson(res, 200, {
296
+ ok: true,
297
+ task: {
298
+ ...redactTaskSecrets(task),
299
+ running: scheduler.isRunning(task.id),
300
+ runningSince: scheduler.runningSince(task.id),
301
+ },
302
+ });
303
+ return;
304
+ }
282
305
  }
283
306
  if (req.method === 'POST') {
284
307
  const handled = await handleItemPost({ store, scheduler, req, res, url, id, action });
@@ -303,7 +326,7 @@ async function handleItemPost({ store, scheduler, req, res, url, id, action }) {
303
326
  return true;
304
327
  }
305
328
  await scheduler.triggerManualRun(id);
306
- sendJson(res, 200, { ok: true, task: store.get(id) });
329
+ sendJson(res, 200, { ok: true, task: redactTaskSecrets(store.get(id)) });
307
330
  return true;
308
331
  }
309
332
  if (action === 'duplicate') {
@@ -317,7 +340,7 @@ async function handleItemPost({ store, scheduler, req, res, url, id, action }) {
317
340
  // A copy starts paused: it must never fire on its own before the user
318
341
  // reviews the schedule (a duplicated one-shot may point at a past time).
319
342
  scheduler.pauseTask(copy.id);
320
- sendJson(res, 200, { ok: true, task: copy });
343
+ sendJson(res, 200, { ok: true, task: redactTaskSecrets(copy) });
321
344
  return true;
322
345
  }
323
346
  if (action === 'pause' || action === 'resume') {
@@ -330,7 +353,7 @@ async function handleItemPost({ store, scheduler, req, res, url, id, action }) {
330
353
  sendJson(res, 404, NOT_FOUND);
331
354
  return true;
332
355
  }
333
- sendJson(res, 200, { ok: true, task });
356
+ sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
334
357
  return true;
335
358
  }
336
359
  if (action === 'toggle' || (!action && url.searchParams.get('action') === 'toggle')) {
@@ -344,7 +367,7 @@ async function handleItemPost({ store, scheduler, req, res, url, id, action }) {
344
367
  return true;
345
368
  }
346
369
  const task = scheduler.toggleTask(id);
347
- sendJson(res, 200, { ok: true, task });
370
+ sendJson(res, 200, { ok: true, task: redactTaskSecrets(task) });
348
371
  return true;
349
372
  }
350
373
  return false;
@@ -381,7 +404,7 @@ async function patchTask({ store, scheduler, req, res, id }) {
381
404
  });
382
405
  return;
383
406
  }
384
- sendJson(res, 200, { ok: true, task: result.task });
407
+ sendJson(res, 200, { ok: true, task: redactTaskSecrets(result.task) });
385
408
  }
386
409
 
387
410
  function deleteTask({ store, scheduler, res, id }) {
@@ -464,7 +487,7 @@ export async function handleExternalTaskRequest({ store, scheduler, req, res, ur
464
487
  if (req.method === 'GET') {
465
488
  const list = store
466
489
  .list({ status: url.searchParams.get('status') || 'all', query: url.searchParams.get('query') || '' })
467
- .map((task) => ({ ...task, running: scheduler.isRunning(task.id) }));
490
+ .map((task) => ({ ...redactTaskSecrets(task), running: scheduler.isRunning(task.id) }));
468
491
  sendJson(res, 200, { ok: true, tasks: list });
469
492
  return;
470
493
  }
@@ -486,7 +509,7 @@ export async function handleExternalTaskRequest({ store, scheduler, req, res, ur
486
509
  sendJson(res, 404, NOT_FOUND);
487
510
  return;
488
511
  }
489
- sendJson(res, 200, { ok: true, task: { ...task, running: scheduler.isRunning(id) } });
512
+ sendJson(res, 200, { ok: true, task: { ...redactTaskSecrets(task), running: scheduler.isRunning(id) } });
490
513
  return;
491
514
  }
492
515
  if (req.method === 'DELETE' && !action) {
package/lib/client.js CHANGED
@@ -1636,7 +1636,7 @@ window.__ModuleLoader__.load({
1636
1636
  Array.isArray(task.channels) && task.channels.length ? `- channels: ${task.channels.join(', ')}` : null,
1637
1637
  `- prompt: ${String(task.prompt || '').slice(0, 400)}`,
1638
1638
  '',
1639
- 'Ask what should change, then apply it with the cron_update_task tool (read the current state again with cron_get_task if you need the full configuration).',
1639
+ 'Ask what should change, then apply it with the cron tool (action: "update", or action: "get" if you need the full configuration).',
1640
1640
  'If the change makes the task execute code (script/node/python/ssh/docker), confirm it with me explicitly before applying it.',
1641
1641
  ].filter(Boolean);
1642
1642
  try {