@goodandready/dsh-cron 0.3.20 → 0.3.22
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 +4 -3
- package/lib/api-collection.js +2 -1
- package/lib/client.js +219 -4
- package/lib/cron-tool.js +5 -1
- package/lib/locales.json +120 -53
- package/lib/runner.js +26 -5
- package/lib/scheduler-completion.js +8 -4
- package/lib/store.js +69 -3
- package/lib/task-create.js +2 -1
- package/lib/task-patch.js +10 -0
- package/lib/task-transfer.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,8 +96,9 @@ graph TD
|
|
|
96
96
|
- **Automated Synthesis**: Aggregates worker outputs into a structured Markdown digest or delegates synthesis to a designated synthesizer model.
|
|
97
97
|
- **Swarm Metrics & History**: Run history records total, succeeded, failed counts, and duration alongside detailed per-worker execution logs.
|
|
98
98
|
|
|
99
|
-
### 🔍 Git Diff & Interactive Patch Review (#351)
|
|
100
|
-
- **
|
|
99
|
+
### 🔍 Git Diff & Interactive Patch Review (#351, #379)
|
|
100
|
+
- **Scoped Diff Capture**: Tasks running in Git worktrees capture modified files, stat summary, and unified diffs (up to 256 KiB) automatically. Non-worktree tasks ignore repository diffs by default, avoiding storage bloat, but can opt in explicitly via `captureGitDiff: true`.
|
|
101
|
+
- **Lightweight Archive Storage**: Historical run archives automatically strip heavy raw diff payloads into concise excerpts with byte-budget caps (max 15MB) and automatic cleanup of orphaned `.tmp.*` files.
|
|
101
102
|
- **1-Click Discard**: Revert uncommitted working directory changes safely with a single click (`git reset --hard HEAD && git clean -fd`).
|
|
102
103
|
- **1-Click Apply**: Re-apply previously captured diffs directly to the repository (`git apply --whitespace=fix`).
|
|
103
104
|
- **Interactive Run Preview**: Color-coded diff inspector embedded directly into the execution history drawer.
|
|
@@ -215,7 +216,7 @@ Every task picks its own runtime; non-LLM runtimes need no model and consume no
|
|
|
215
216
|
|
|
216
217
|
### 8. Session Integration & Permissions
|
|
217
218
|
* **Real Assistant Output & Terminal Status Extraction** — Isolates current turn session events, extracts the actual assistant message text (excluding previous turn history in persistent sessions), and validates terminal turn status (`turn/end` errors or interruptions) to guarantee accurate reporting, chaining, and structured action execution.
|
|
218
|
-
* **Per-task permission presets** — `default`, `read-only`, `workspace-write`, or `full` are applied to the task's agent session before the prompt runs.
|
|
219
|
+
* **Per-task permission presets & unattended approval** — `default`, `read-only`, `workspace-write`, or `danger-full-access` (aliased as `full`) are mapped directly to host `SandboxMode` and applied to the task's agent session before the prompt runs. For unattended background runs, approval policy is pinned to `never` via host `approval` service to eliminate interactive approval stalls.
|
|
219
220
|
* **Session auto-archive** — isolated cron sessions are archived after each run (best-effort) so they do not clutter the chat list.
|
|
220
221
|
* **History → session navigation** — every LLM run records its session; open it straight from the run history entry.
|
|
221
222
|
|
package/lib/api-collection.js
CHANGED
|
@@ -169,7 +169,7 @@ export function buildTaskRecord(store, body, taskType, parsed) {
|
|
|
169
169
|
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(body.misfirePolicy) ? body.misfirePolicy : (current ? (current.misfirePolicy || 'skip') : 'skip'),
|
|
170
170
|
maxRetries: body.maxRetries !== undefined ? Math.max(0, Number(body.maxRetries) || 0) : (current ? (Number(current.maxRetries) || 0) : 0),
|
|
171
171
|
retryBackoffMs: body.retryBackoffMs !== undefined ? Math.max(1000, Number(body.retryBackoffMs) || 30000) : (current ? (Number(current.retryBackoffMs) || 30000) : 30000),
|
|
172
|
-
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
172
|
+
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(body.permissionPreset) ? body.permissionPreset : (current && !EXTERNAL_RUNTIMES.includes(taskType) ? (current.permissionPreset || 'default') : 'default'),
|
|
173
173
|
validators: Array.isArray(body.validators) ? sanitizeValidators(body.validators) : (current ? current.validators : undefined),
|
|
174
174
|
...mergeExecutionFields(current, body, parsed),
|
|
175
175
|
};
|
|
@@ -194,6 +194,7 @@ export function mergeExecutionFields(current, body, parsed) {
|
|
|
194
194
|
workspaceId: body.workspaceId !== undefined ? String(body.workspaceId).trim() : (current ? current.workspaceId : ''),
|
|
195
195
|
worktree: body.worktree !== undefined ? Boolean(body.worktree) : (current ? Boolean(current.worktree) : false),
|
|
196
196
|
keepWorktree: body.keepWorktree !== undefined ? Boolean(body.keepWorktree) : (current ? Boolean(current.keepWorktree) : false),
|
|
197
|
+
captureGitDiff: body.captureGitDiff !== undefined ? Boolean(body.captureGitDiff) : (body.captureDiff !== undefined ? Boolean(body.captureDiff) : (current ? current.captureGitDiff : undefined)),
|
|
197
198
|
httpMethod: body.httpMethod ? String(body.httpMethod).toUpperCase() : (current ? current.httpMethod : 'GET'),
|
|
198
199
|
httpUrl: body.httpUrl !== undefined ? String(body.httpUrl).trim() : (current ? current.httpUrl : ''),
|
|
199
200
|
httpHeaders: body.httpHeaders !== undefined ? body.httpHeaders : (current ? current.httpHeaders : undefined),
|
package/lib/client.js
CHANGED
|
@@ -313,7 +313,206 @@ en: {
|
|
|
313
313
|
'form.previewScheduleTitle': 'Next 5 scheduled runs:',
|
|
314
314
|
},
|
|
315
315
|
zh: {
|
|
316
|
-
'
|
|
316
|
+
'sidebar.label': '定时任务', 'sidebar.jobsTitle': '运行中任务',
|
|
317
|
+
'sidebar.noActiveJobs': '无运行中任务', 'sidebar.moreJobs': '及另外 {count} 个',
|
|
318
|
+
'chip.title': '定时任务 (Cron)', 'panel.back': '返回对话',
|
|
319
|
+
'panel.title': '定时任务', 'panel.subtitle': '让 DSH 规划任务、设置提醒或跟踪状态',
|
|
320
|
+
'actions.create': '新建', 'actions.createWithDsh': '💬 让 DSH 创建',
|
|
321
|
+
'actions.setupManually': '✏️ 手动配置', 'actions.runNow': '立即运行',
|
|
322
|
+
'actions.runNowShort': '运行', 'actions.edit': '编辑 / 详情',
|
|
323
|
+
'actions.duplicate': '复制任务', 'actions.tuneWithDsh': '让 DSH 优化',
|
|
324
|
+
'tune.startFailed': '无法启动优化会话: {error}', 'actions.delete': '删除任务',
|
|
325
|
+
'duplicate.done': '已创建副本并处于暂停状态:恢复前请先核对调度时间。', 'duplicate.oneShotHint': '该副本为一次性任务且已暂停,请设定新的执行时间后再启用。',
|
|
326
|
+
'duplicate.failed': '复制任务失败: {error}', 'transfer.exportTitle': '导出任务',
|
|
327
|
+
'transfer.importTitle': '导入任务', 'transfer.exported': '已成功导出 {count} 个任务。',
|
|
328
|
+
'transfer.exportFailed': '导出失败: {error}', 'transfer.importTitleModal': '导入任务',
|
|
329
|
+
'transfer.importHint': '仅导出任务静态配置:不包含历史记录与计数器。渠道通过凭据名称引用;注意完整导出保留自定义环境变量和 HTTP 请求头。', 'transfer.exportPrompt': '选择导出模式:\n确定 = 净化导出(移除自定义环境变量与请求头)\n取消 = 完整原始导出',
|
|
330
|
+
'transfer.strategyAdd': '作为新任务导入(分配新 ID)', 'transfer.strategyReplace': '覆盖同 ID 任务',
|
|
331
|
+
'transfer.strategySkip': '跳过已存在的同 ID 任务', 'transfer.summary': '该文件包含 {total} 个任务:{add} 个新增,{replace} 个覆盖,{skip} 个跳过。',
|
|
332
|
+
'transfer.confirm': '开始导入', 'transfer.imported': '已成功导入 {count} 个任务。',
|
|
333
|
+
'transfer.importFailed': '导入失败: {error}', 'transfer.badFile': '所选文件不是有效的 JSON 导出文件: {error}',
|
|
334
|
+
'actions.pause': '暂停', 'actions.resume': '恢复',
|
|
335
|
+
'actions.settings': '通知与渠道设置', 'search.placeholder': '搜索定时任务',
|
|
336
|
+
'filters.title': '筛选', 'filters.type': '类型',
|
|
337
|
+
'filters.model': '模型', 'filters.channel': '渠道',
|
|
338
|
+
'filters.any': '全部', 'filters.reset': '重置筛选',
|
|
339
|
+
'filters.empty': '没有匹配当前筛选条件的任务。', 'filters.showing': '显示 {shown} / 共 {total}',
|
|
340
|
+
'stats.activeTasks': '活跃任务', 'stats.totalRuns': '总运行次数',
|
|
341
|
+
'stats.totalTokens': '消耗 Token', 'stats.totalCost': '预估成本',
|
|
342
|
+
'tabs.all': '全部', 'tabs.active': '运行中',
|
|
343
|
+
'tabs.paused': '已暂停', 'tabs.completed': '已完成',
|
|
344
|
+
'banner.loadError': '加载任务失败: {error}', 'banner.retry': '重试',
|
|
345
|
+
'list.loading': '正在加载定时任务...', 'list.empty': '暂无定时任务',
|
|
346
|
+
'list.typeShell': 'Shell 脚本', 'list.typeLlm': 'LLM 智能体',
|
|
347
|
+
'list.oneShot': '一次性', 'list.configManaged': '配置只读',
|
|
348
|
+
'list.configManagedHint': '由配置文件中的 config.jobs 声明;要修改或删除请编辑该文件', 'list.next': ' · 下次: {time}',
|
|
349
|
+
'recs.title': '推荐任务', 'recs.hint': '开箱即用的配方。点一下就会把表单填好——保存之前不会创建任何任务。',
|
|
350
|
+
'modal.editTitle': '编辑任务', 'modal.newTitle': '新建定时任务',
|
|
351
|
+
'modal.statusActive': '活跃', 'modal.statusPaused': '已暂停',
|
|
352
|
+
'modal.params': '配置参数', 'modal.history': '运行历史 ({count})',
|
|
353
|
+
'modal.cancel': '取消', 'modal.confirmDiscard': '放弃未保存的内容?',
|
|
354
|
+
'modal.save': '保存修改', 'modal.create': '创建任务',
|
|
355
|
+
'modal.close': '关闭', 'history.loading': '正在加载历史记录...',
|
|
356
|
+
'history.empty': '暂无运行记录', 'history.stoppedReasonTitle': '停止原因:',
|
|
357
|
+
'history.retryDelivery': '重试推送', 'history.retryingDelivery': '正在重试...',
|
|
358
|
+
'history.statusValidationFailed': '校验失败', 'history.stage_queue': '排队',
|
|
359
|
+
'history.stage_preflight': '预检', 'history.stage_execution': '执行',
|
|
360
|
+
'history.stage_validation': '校验', 'history.stage_delivery': '推送',
|
|
361
|
+
'history.refresh': '刷新历史', 'history.statusSuccess': '成功',
|
|
362
|
+
'history.statusUncertain': '不确定', 'history.artifacts': '产物文件',
|
|
363
|
+
'history.resolveCompleted': '标记已完成', 'history.resolveFailed': '标记失败',
|
|
364
|
+
'history.statusUncertainHint': '服务重启中断 — 请确认结果', 'history.statusSkipped': '已跳过',
|
|
365
|
+
'history.statusMissed': '已错过', 'history.statusTimeout': '超时',
|
|
366
|
+
'history.statusFailed': '失败', 'form.nameLabel': '任务名称',
|
|
367
|
+
'form.namePlaceholder': '例如: dsh-tags-watch', 'form.typeLabel': '执行类型 *',
|
|
368
|
+
'form.typeLlm': 'LLM(AI 智能体)', 'form.typeScript': 'Shell 命令 / 脚本',
|
|
369
|
+
'form.typeNode': 'Node.js', 'form.typePython': 'Python',
|
|
370
|
+
'form.typeHttp': 'HTTP 请求 / Webhook', 'form.typeSsh': '远程 SSH',
|
|
371
|
+
'form.typeDocker': 'Docker 容器', 'form.typeSkill': 'DSH 技能',
|
|
372
|
+
'form.typeWorkflow': 'DSH 工作流', 'form.envLabel': '环境变量 (KEY=VALUE, 每行一个)',
|
|
373
|
+
'form.envPlaceholder': 'NODE_ENV=production\nTZ=Europe/Berlin', 'form.envHint': '仅对非 LLM 运行时生效。不要在这里存放密钥。',
|
|
374
|
+
'form.cwdLabel': '工作目录 (绝对路径)', 'form.workspaceLabel': '工作区 ID 或路径(可选)',
|
|
375
|
+
'form.worktreeLabel': '在独立的临时 Git Worktree 中执行', 'form.keepWorktreeLabel': '执行结束后保留 Git Worktree (不自动清理)',
|
|
376
|
+
'form.httpMethodLabel': 'HTTP 方法', 'form.httpUrlLabel': 'URL',
|
|
377
|
+
'form.httpHeadersLabel': '请求头(JSON,可选)', 'form.httpBodyLabel': '请求体(可选)',
|
|
378
|
+
'form.sshProfileLabel': 'dsh-remote-workspace 配置 ID', 'form.sshProfileHint': '推荐用法:主机与凭据都放在 remote-workspace 插件里。',
|
|
379
|
+
'form.sshTargetLabel': '备用目标(user@host,可选)', 'form.sshKeyLabel': '私钥路径(备用,可选)',
|
|
380
|
+
'form.dockerImageLabel': '容器镜像', 'form.skillNameLabel': '技能名称',
|
|
381
|
+
'form.workflowNameLabel': '工作流名称', 'form.scheduleLabel': '调度计划 (Cron 或相对时间) *',
|
|
382
|
+
'form.schedulePlaceholder': '例如: 0 9 * * 1-5 或 15m 或 in 2 hours', 'form.timeoutLabel': '超时时间 (秒)',
|
|
383
|
+
'form.overlapLabel': '并发策略', 'form.overlapSkip': '跳过 (默认)',
|
|
384
|
+
'form.overlapQueue': '排队等待', 'form.overlapReplace': '取消当前运行并重新开始 (replace)',
|
|
385
|
+
'form.kanbanLabel': '看板同步模式', 'form.kanbanNone': '不同步到看板',
|
|
386
|
+
'form.kanbanOnFailure': '失败时创建工单(进 Backlog)', 'form.kanbanAlways': '每次完成都建卡片(Done / Backlog)',
|
|
387
|
+
'form.providerLabel': 'LLM 服务商', 'form.providerDefault': '默认(DSH)',
|
|
388
|
+
'form.modelLabel': 'AI 模型', 'form.modelDefault': '默认模型',
|
|
389
|
+
'form.silentRuleLabel': '静默规则 (可选)', 'form.silentRulePlaceholder': '例如: ALL_OK 或 status == 200',
|
|
390
|
+
'form.silentRuleHint': '若输出符合此规则,将不发送任何通知。', 'history.silentSkip': '保持静默:',
|
|
391
|
+
'history.diagnosis': '诊断', 'history.applySuggestion': '用这条建议打开编辑表单',
|
|
392
|
+
'form.inspectOnFailureLabel': '用模型诊断失败原因', 'form.inspectOnFailureHint': '运行失败后,由模型读一遍提示词、错误和输出,把诊断结论和修改建议随这次运行一起保存。不会自动应用任何改动。',
|
|
393
|
+
'settings.inspectorModelLabel': '失败诊断模型', 'form.fallbackModelLabel': '备用模型 (主模型失败时使用)',
|
|
394
|
+
'form.fallbackModelPlaceholder': '例如: deepseek-reasoner', 'form.fallbackModelHint': '主模型运行失败时,用这个模型再试一次。两次尝试都计入成本。',
|
|
395
|
+
'form.promptLabelScript': 'Shell 命令或脚本', 'form.promptLabelLlm': '给智能体的指令 / 提示词',
|
|
396
|
+
'form.promptPlaceholderScript': 'curl -fsSL https://... || exit 1', 'form.promptPlaceholderLlm': '精确描述智能体要做什么...',
|
|
397
|
+
'form.timezoneLabel': '时区 (IANA 时区名)', 'form.timezonePlaceholder': '例如: Asia/Shanghai 或 UTC (留空使用服务器时区)',
|
|
398
|
+
'form.misfireLabel': '错过执行策略 (Misfire Policy)', 'form.misfireSkip': '跳过 (默认)',
|
|
399
|
+
'form.misfireRunOnce': '补跑一次', 'form.misfireCatchUp': '迟到时补跑一次并记录缺口 (catchUpAll)',
|
|
400
|
+
'form.retriesLabel': '失败自动重试次数(0 = 关闭)', 'form.permissionLabel': '会话权限',
|
|
401
|
+
'form.permissionDefault': '默认 (继承主配置)', 'form.permissionReadonly': '只读',
|
|
402
|
+
'form.permissionWrite': '工作区写入', 'form.permissionFull': '完全访问权限',
|
|
403
|
+
'list.runningNow': '正在运行', 'history.openSession': '打开会话',
|
|
404
|
+
'form.notifyTitle': 'Telegram 通知', 'form.notifyTelegram': '把运行报告发送到 Telegram',
|
|
405
|
+
'form.notifyOnlyFailure': '仅失败时发送(onlyOnFailure / 成功静默)', 'form.channelsTitle': '推送渠道',
|
|
406
|
+
'form.channelsHint': '任务执行完毕后推送报告的目标渠道。', 'form.templateLabel': '自定义消息模板',
|
|
407
|
+
'form.templatePlaceholder': '⏰ {title} — {status},耗时 {duration}', 'form.templateHint': '可用变量: {title} {id} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost} {model}。留空使用内置文案。',
|
|
408
|
+
'form.channelTelegram': 'Telegram', 'form.channelKanban': 'dsh-kanban 卡片',
|
|
409
|
+
'form.channelDiscord': 'Discord', 'form.channelSlack': 'Slack',
|
|
410
|
+
'form.channelNtfy': 'ntfy', 'form.channelBark': 'Bark',
|
|
411
|
+
'form.channelPushplus': 'PushPlus', 'form.channelTts': '语音(dsh-tts)',
|
|
412
|
+
'form.channelGitea': 'Gitea Issue', 'settings.deliverySection': '推送渠道',
|
|
413
|
+
'settings.secretsSection': '凭据(引用)', 'settings.secretsHint': '这里填 DSH 凭据的「名称」,不是密钥本身。密钥保存在凭据服务里。',
|
|
414
|
+
'settings.telegramSection': 'Telegram', 'settings.templatesSection': '消息模板',
|
|
415
|
+
'settings.channelTemplateLabel': '{channel} 的消息模板', 'settings.botTokenRefLabel': 'Bot Token 凭据名称',
|
|
416
|
+
'settings.botTokenRefPlaceholder': '例如: TELEGRAM_BOT_TOKEN', 'settings.apiTokenLabel': '外部 API Token',
|
|
417
|
+
'settings.apiTokenPlaceholder': '用于 /dsh-cron/api/* 的 Bearer token(留空 = 关闭)', 'settings.discordLabel': 'Discord Webhook URL',
|
|
418
|
+
'settings.slackLabel': 'Slack Webhook URL', 'settings.ntfyUrlLabel': 'ntfy 服务器地址',
|
|
419
|
+
'settings.ntfyTopicLabel': 'ntfy 主题', 'settings.ntfyTokenRefLabel': 'ntfy Token 凭据名称',
|
|
420
|
+
'settings.barkServerLabel': 'Bark 服务器地址', 'settings.barkKeyLabel': 'Bark 设备 Key',
|
|
421
|
+
'settings.pushplusTokenRefLabel': 'PushPlus Token 凭据名称', 'settings.pushplusUrlLabel': 'PushPlus 接口地址',
|
|
422
|
+
'settings.ttsBaseUrlLabel': 'dsh-tts 服务地址', 'settings.giteaBaseUrlLabel': 'Gitea 服务地址',
|
|
423
|
+
'settings.giteaRepoLabel': 'Gitea 仓库(owner/repo)', 'settings.giteaTokenRefLabel': 'Gitea Token 凭据名称',
|
|
424
|
+
'settings.templateLabel': '默认消息模板', 'settings.templateHint': '作用于所有没有单独模板的渠道。留空用内置文案。',
|
|
425
|
+
'settings.deliveryTimeoutMsLabel': '每个渠道的推送超时(毫秒)', 'settings.automationSection': '自动化模型',
|
|
426
|
+
'settings.automationHint': '用于静默规则研判和故障诊断的辅助模型。留空沿用任务自身模型。', 'settings.silentRuleModelLabel': '静默规则研判模型',
|
|
427
|
+
'settings.secretsOnCard': '设置中引用的凭据将在前端脱敏展示,并通过名称引用。', 'form.nextRunLabel': '下次执行: ',
|
|
428
|
+
'form.notScheduled': '未安排计划', 'settings.loading': '正在加载设置...',
|
|
429
|
+
'settings.unavailable': '设置当前不可用。', 'settings.retry': '重试',
|
|
430
|
+
'settings.cardTitle': '⏰ 定时任务调度器 (Cron)', 'settings.cardDesc': '定时计划任务(Cron/间隔)、后台智能体场景以及支持 Telegram、Discord、Slack、ntfy、Bark、PushPlus、语音和 Gitea 的通知推送。',
|
|
431
|
+
'settings.openPanel': '打开任务管理看板', 'settings.modalTitle': '通知与渠道设置',
|
|
432
|
+
'settings.tokenLabel': 'Telegram Bot Token', 'settings.tokenPlaceholder': '123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ',
|
|
433
|
+
'settings.tokenPlaceholderDefault': '使用 DSH 全局配置的机器人 (或在此输入自定义 Token)', 'settings.defaultHint': '✓ 检测到 DSH 环境已配置默认 Bot',
|
|
434
|
+
'settings.chatIdLabel': 'Telegram Chat ID', 'settings.chatIdPlaceholder': '例如: 345678901',
|
|
435
|
+
'settings.kanbanUrlLabel': 'Kanban 服务基础地址', 'settings.globalNotify': '全局开启所有任务结果推送至 Telegram',
|
|
436
|
+
'settings.globalOnlyFailure': '全局模式:仅在失败时推送 (成功静默)', 'settings.dynamicCredentials': '动态凭据解析 (credential://...)',
|
|
437
|
+
'settings.dynamicCredentialsHint': '运行时从凭据服务动态解析 credential://... 且不泄露到存储 (#335)', 'settings.testTelegram': '🔔 测试 Telegram 发送',
|
|
438
|
+
'settings.testKanban': '📋 测试 Kanban', 'settings.sending': '正在发送...',
|
|
439
|
+
'settings.checkingKanban': '正在连接 Kanban...', 'settings.testOk': '✅ 测试消息已成功发送至 Telegram!',
|
|
440
|
+
'settings.testFail': '❌ 发送失败: {error}', 'settings.kanbanOk': '✅ Kanban 卡片创建成功',
|
|
441
|
+
'settings.kanbanFail': '❌ {error}', 'settings.save': '保存设置',
|
|
442
|
+
'settings.saved': '✓ 已保存', 'settings.saveError': '保存失败: {error}',
|
|
443
|
+
'settings.saveErrorUnknown': '未知错误', 'updater.title': '插件自更新',
|
|
444
|
+
'updater.btnCheck': '检查更新', 'updater.checking': '正在检查...',
|
|
445
|
+
'updater.btnUpdate': '升级至 v{version}', 'updater.updating': '正在更新...',
|
|
446
|
+
'updater.desc': '通过 npm 仓库在线检查并就地升级插件,无需 SSH 终端操作。', 'updater.current': '当前版本: v{version}',
|
|
447
|
+
'updater.available': '发现新版本: v{version}', 'updater.upToDate': '已是最新版本',
|
|
448
|
+
'updater.success': '已成功升级至 v{version}。请重启 DSH 或重载配置以应用更新。', 'dsh.modalTitle': '让 DSH 智能体创建任务',
|
|
449
|
+
'dsh.descLabel': '请描述你的任务目标和执行频率:', 'dsh.placeholder': '例如: 每两小时检查一次磁盘空间和失败的 systemd 守护进程,并在异常时报警...',
|
|
450
|
+
'dsh.sendHint': '按 Ctrl+Enter 发送', 'dsh.sendTitle': '提交给智能体',
|
|
451
|
+
'dsh.startFailed': '启动智能体对话失败: {error}', 'dsh.launchError': '执行错误: {error}',
|
|
452
|
+
'confirm.delete': '确定删除该任务吗?', 'list.loadErrorPrefix': '加载任务列表失败',
|
|
453
|
+
'actions.dryRun': '空跑测试', 'actions.dryRunTitle': '空跑测试(模拟执行,不产生副作用且不发送通知)',
|
|
454
|
+
'actions.archive': '历史归档', 'actions.archiveTitle': '查看执行历史与日志归档',
|
|
455
|
+
'dryRun.title': '🧪 空跑模拟结果', 'dryRun.task': '任务',
|
|
456
|
+
'dryRun.status': '模拟状态', 'dryRun.duration': '耗时',
|
|
457
|
+
'dryRun.output': '模拟输出', 'dryRun.preflight': '预检结果',
|
|
458
|
+
'dryRun.close': '关闭', 'archive.title': '📜 运行历史归档',
|
|
459
|
+
'archive.totalRuns': '共记录 {total} 次运行', 'archive.empty': '该任务暂无历史运行归档。',
|
|
460
|
+
'archive.loadMore': '加载更多 ({shown} / {total})', 'archive.close': '关闭',
|
|
461
|
+
'form.reliabilityTitle': '🛡️ 可靠性、心跳监听与自愈', 'form.preflightLabel': '预检健康探测',
|
|
462
|
+
'form.preflightNone': '无', 'form.preflightHttp': 'HTTP (2xx 探测)',
|
|
463
|
+
'form.preflightShell': 'Shell 探测 (退出码 0)', 'form.preflightDisk': '磁盘空间探测 (挂载点:最小百分比)',
|
|
464
|
+
'form.preflightTargetLabel': '预检目标参数', 'form.preflightTargetPlaceholder': 'https://example.com/healthz 或 ls /data 或 /:10%',
|
|
465
|
+
'form.heartbeatIntervalLabel': '心跳打卡时限 (秒, 0 = 禁用)', 'form.gracePeriodLabel': '心跳宽限期 (秒)',
|
|
466
|
+
'form.priorityLabel': '队列优先级 (1-10,数值越大越先执行)', 'form.concurrencyGroupLabel': '并发分组名称',
|
|
467
|
+
'form.selfHealingLabel': '故障自愈脚本 (Shell)', 'form.selfHealingPlaceholder': 'systemctl restart app || true',
|
|
468
|
+
'form.autoDiagnoseLabel': '任务失败时启用 AI 智能分析诊断', 'form.agentPresetLabel': '智能体预设 (Agent Preset)',
|
|
469
|
+
'form.agentPresetPlaceholder': 'standard (留空使用全局默认预设)', 'form.agentPresetHint': '来自 dsh-agent-presets 的智能体预设(如 standard, minimal, cordis, ptc),挂载工作区工具与文件读写能力。',
|
|
470
|
+
'form.sessionTitleLabel': '会话标题模板 (Session Title)', 'form.sessionTitlePlaceholder': '如 {{date}} 市场早报 (支持 {{date}} 变量)',
|
|
471
|
+
'form.sessionTitleHint': '此任务运行生成的会话标题。支持 {{date}} (年-月-日)。留空默认为 "<YYYY-MM-DD> <任务名>"。', 'form.targetSessionIdLabel': '目标常驻会话 ID (Target Session ID)',
|
|
472
|
+
'form.targetSessionIdPlaceholder': '如 audit-chat 或 dev-summary(留空为单次隔离运行)', 'form.targetSessionIdHint': '配置后定时任务将在此常驻会话中持续交互,跨周期保留历史上下文对话记忆,不再每次新建孤立会话。',
|
|
473
|
+
'form.targetSessionResetLabel': '上下文轮转策略 (Session Rotation)', 'form.targetSessionResetNever': '永不轮转(单一会话持续累积)',
|
|
474
|
+
'form.targetSessionResetDaily': '每日轮转(每日自动开启新会话)', 'form.targetSessionResetWeekly': '每周轮转(每周自动开启新会话)',
|
|
475
|
+
'form.targetSessionResetHint': '自动按周期加注日期后缀轮转会话,有效防止长时间运行导致的模型上下文窗口超限与 Token 浪费。', 'form.requireApprovalLabel': '执行前需人工确认审批',
|
|
476
|
+
'form.requireApprovalHint': '开启后,任务执行前将挂起并等待授权用户批准提议 (#318)。', 'actions.approve': '批准',
|
|
477
|
+
'actions.reject': '拒绝', 'approval.waiting': '等待人工审批',
|
|
478
|
+
'approval.approved': '已批准', 'approval.rejected': '已拒绝',
|
|
479
|
+
'chainGraph.modalTitle': '任务链与检查点依赖图', 'chainGraph.viewGraph': '任务链图',
|
|
480
|
+
'chainGraph.noNodes': '未找到该任务或运行的关联步骤。', 'chainGraph.retryStep': '重试此步骤',
|
|
481
|
+
'chainGraph.stepSuccess': '成功', 'chainGraph.stepFailed': '失败',
|
|
482
|
+
'chainGraph.stepPending': '等待中', 'chainGraph.close': '关闭',
|
|
483
|
+
'chainGraph.decisionNode': '条件决策节点', 'chainGraph.stepSkipped': '已跳过',
|
|
484
|
+
'history.doctorDiagnosis': 'Doctor 诊断与建议修复', 'history.doctorApply': '一键应用修复',
|
|
485
|
+
'history.doctorRevert': '恢复上一版本代码', 'history.autoHealed': '已由 Doctor 自动修复',
|
|
486
|
+
'form.autoHealLabel': '自主 Self-Healing Doctor 自动修复', 'form.autoHealHint': '当任务失败时自动诊断并安全修复(每天上限 1 次)',
|
|
487
|
+
'form.elseTaskLabel': '默认后备任务 (elseTask)', 'form.elseTaskHint': '当没有分支条件匹配时触发的目标任务',
|
|
488
|
+
'form.elseTaskPlaceholder': '如 task-fallback', 'adaptive.badge': '自适应',
|
|
489
|
+
'adaptive.idle': '空闲', 'adaptive.backoff': '避让中',
|
|
490
|
+
'form.adaptiveEnabled': '自适应轮询 (Adaptive Polling)', 'form.adaptiveHint': '数据无变化时指数级延长执行间隔',
|
|
491
|
+
'liveLog.modalTitle': '实时任务执行日志流', 'liveLog.viewLog': '实时日志',
|
|
492
|
+
'liveLog.autoScroll': '自动滚动', 'liveLog.connecting': '正在连接日志流...',
|
|
493
|
+
'liveLog.waiting': '等待运行器输出...', 'liveLog.completed': '日志流已结束',
|
|
494
|
+
'liveLog.close': '关闭日志流', 'timeline.modalTitle': '24小时调度时间线',
|
|
495
|
+
'timeline.buttonLabel': '时间线', 'timeline.buttonTitle': '查看24小时预测任务调度与并发时间线',
|
|
496
|
+
'timeline.tabTimeline': '调度时间线', 'timeline.legendDesc': '未来24小时各时段任务预计执行分布:',
|
|
497
|
+
'timeline.peakWarning': '并发峰值预警', 'heatmap.modalTitle': '成本与Token消耗热力图',
|
|
498
|
+
'heatmap.buttonLabel': '热力图', 'heatmap.tabHeatmap': '成本热力图',
|
|
499
|
+
'heatmap.matrixDesc': '每周各时段成本分布矩阵(7天 × 24小时):', 'digest.modalTitle': '自动化与成本分析摘要',
|
|
500
|
+
'digest.buttonLabel': '分析摘要', 'digest.buttonTitle': '查看 7 天或 30 天运行统计与花费摘要',
|
|
501
|
+
'digest.period7': '最近 7 天', 'digest.period30': '最近 30 天',
|
|
502
|
+
'digest.executions': '执行次数', 'digest.tokens': 'Token 消耗',
|
|
503
|
+
'digest.cost': '花费 (USD)', 'digest.fleet': '任务总览',
|
|
504
|
+
'digest.unstableTitle': '最不稳定任务排行', 'digest.expensiveTitle': '资源消耗排行',
|
|
505
|
+
'digest.sendTelegram': '发送至 Telegram', 'digest.copyMarkdown': '复制 Markdown',
|
|
506
|
+
'digest.copied': '已复制!', 'digest.sentSuccess': '已成功发送至 Telegram!',
|
|
507
|
+
'prompt.version': '提示词版本', 'prompt.rollback': '回退至 v',
|
|
508
|
+
'prompt.history': '提示词历史', 'prompt.successRate': '成功率',
|
|
509
|
+
'form.chainingTitle': '任务链与 DAG 编排', 'form.onSuccessLabel': '成功后触发 (任务 ID)',
|
|
510
|
+
'form.onSuccessHint': '成功后并发触发的任务 ID,逗号分隔 (Fan-Out)', 'form.onFailureLabel': '失败后触发 (任务 ID)',
|
|
511
|
+
'form.onFailureHint': '失败后触发的任务 ID,逗号分隔', 'form.dependsOnLabel': '依赖任务 (dependsOn)',
|
|
512
|
+
'form.dependsOnHint': '执行前需等待的前置任务 ID,逗号分隔 (Fan-In)', 'form.joinPolicyLabel': '合并策略',
|
|
513
|
+
'form.joinPolicyAll': '所有前置均成功 (all_success)', 'form.joinPolicyAny': '任一前置成功即可 (any_success)',
|
|
514
|
+
'recipe.dependenciesMissing': '缺少必要集成服务或凭据', 'form.previewSchedule': '预测下次运行时间',
|
|
515
|
+
'form.previewScheduleTitle': '未来 5 次计划运行时间:',
|
|
317
516
|
}
|
|
318
517
|
};
|
|
319
518
|
let translate = (key, vars) => {
|
|
@@ -1441,6 +1640,7 @@ const [formCwd, setFormCwd] = React.useState('');
|
|
|
1441
1640
|
const [formWorkspaceId, setFormWorkspaceId] = React.useState('');
|
|
1442
1641
|
const [formWorktree, setFormWorktree] = React.useState(false);
|
|
1443
1642
|
const [formKeepWorktree, setFormKeepWorktree] = React.useState(false);
|
|
1643
|
+
const [formCaptureGitDiff, setFormCaptureGitDiff] = React.useState(false);
|
|
1444
1644
|
const [formHttpMethod, setFormHttpMethod] = React.useState('GET');
|
|
1445
1645
|
const [formHttpUrl, setFormHttpUrl] = React.useState('');
|
|
1446
1646
|
const [formHttpHeaders, setFormHttpHeaders] = React.useState('');
|
|
@@ -1496,6 +1696,7 @@ cwd: String(formCwd || '').trim(),
|
|
|
1496
1696
|
workspaceId: String(formWorkspaceId || '').trim(),
|
|
1497
1697
|
worktree: Boolean(formWorktree),
|
|
1498
1698
|
keepWorktree: Boolean(formKeepWorktree),
|
|
1699
|
+
captureGitDiff: Boolean(formCaptureGitDiff),
|
|
1499
1700
|
httpMethod: String(formHttpMethod || 'GET'),
|
|
1500
1701
|
httpUrl: String(formHttpUrl || '').trim(),
|
|
1501
1702
|
httpHttpHeaders: String(formHttpHeaders || '').trim(),
|
|
@@ -2130,6 +2331,7 @@ setFormCwd('');
|
|
|
2130
2331
|
setFormWorkspaceId('');
|
|
2131
2332
|
setFormWorktree(false);
|
|
2132
2333
|
setFormKeepWorktree(false);
|
|
2334
|
+
setFormCaptureGitDiff(false);
|
|
2133
2335
|
setFormHttpMethod('GET');
|
|
2134
2336
|
setFormHttpUrl('');
|
|
2135
2337
|
setFormHttpHeaders('');
|
|
@@ -2217,6 +2419,7 @@ cwd: '',
|
|
|
2217
2419
|
workspaceId: '',
|
|
2218
2420
|
worktree: false,
|
|
2219
2421
|
keepWorktree: false,
|
|
2422
|
+
captureGitDiff: false,
|
|
2220
2423
|
httpMethod: 'GET',
|
|
2221
2424
|
httpUrl: '',
|
|
2222
2425
|
httpHttpHeaders: '',
|
|
@@ -2342,7 +2545,7 @@ setSchedulePreviewRuns(null);
|
|
|
2342
2545
|
setFormTimezone(task.timezone || '');
|
|
2343
2546
|
setFormMisfirePolicy(task.misfirePolicy || 'skip');
|
|
2344
2547
|
setFormMaxRetries(task.maxRetries !== undefined ? task.maxRetries : 0);
|
|
2345
|
-
setFormPermissionPreset(task.permissionPreset || 'default');
|
|
2548
|
+
setFormPermissionPreset(task.permissionPreset === 'full' ? 'danger-full-access' : (task.permissionPreset || 'default'));
|
|
2346
2549
|
setFormAgentPreset(task.agentPreset || '');
|
|
2347
2550
|
setFormSessionTitle(task.sessionTitle || '');
|
|
2348
2551
|
setFormTargetSessionId(task.targetSessionId || '');
|
|
@@ -2352,6 +2555,7 @@ setFormCwd(task.cwd || '');
|
|
|
2352
2555
|
setFormWorkspaceId(task.workspaceId || '');
|
|
2353
2556
|
setFormWorktree(Boolean(task.worktree));
|
|
2354
2557
|
setFormKeepWorktree(Boolean(task.keepWorktree));
|
|
2558
|
+
setFormCaptureGitDiff(task.captureGitDiff !== undefined ? Boolean(task.captureGitDiff) : Boolean(task.worktree));
|
|
2355
2559
|
setFormHttpMethod(task.httpMethod || 'GET');
|
|
2356
2560
|
setFormHttpUrl(task.httpUrl || '');
|
|
2357
2561
|
setFormHttpHeaders(typeof task.httpHeaders === 'string' ? task.httpHeaders : (task.httpHeaders ? JSON.stringify(task.httpHeaders) : ''));
|
|
@@ -2426,6 +2630,7 @@ cwd: formCwd.trim(),
|
|
|
2426
2630
|
workspaceId: formWorkspaceId.trim(),
|
|
2427
2631
|
worktree: isAgentType ? Boolean(formWorktree) : false,
|
|
2428
2632
|
keepWorktree: isAgentType ? Boolean(formKeepWorktree) : false,
|
|
2633
|
+
captureGitDiff: Boolean(formCaptureGitDiff),
|
|
2429
2634
|
httpMethod: formHttpMethod,
|
|
2430
2635
|
httpUrl: formHttpUrl.trim(),
|
|
2431
2636
|
httpHeaders: formHttpHeaders.trim() || undefined,
|
|
@@ -3423,7 +3628,7 @@ onChange: (e) => setFormPermissionPreset(e.target.value)
|
|
|
3423
3628
|
_h('option', { value: 'default' }, T_KEY(t, 'form.permissionDefault')),
|
|
3424
3629
|
_h('option', { value: 'read-only' }, T_KEY(t, 'form.permissionReadonly')),
|
|
3425
3630
|
_h('option', { value: 'workspace-write' }, T_KEY(t, 'form.permissionWrite')),
|
|
3426
|
-
_h('option', { value: 'full' }, T_KEY(t, 'form.permissionFull'))
|
|
3631
|
+
_h('option', { value: 'danger-full-access' }, T_KEY(t, 'form.permissionFull'))
|
|
3427
3632
|
)
|
|
3428
3633
|
)
|
|
3429
3634
|
)
|
|
@@ -3683,6 +3888,14 @@ checked: formKeepWorktree,
|
|
|
3683
3888
|
onChange: (e) => setFormKeepWorktree(e.target.checked)
|
|
3684
3889
|
}),
|
|
3685
3890
|
_h('span', null, T_KEY(t, 'form.keepWorktreeLabel'))
|
|
3891
|
+
),
|
|
3892
|
+
_h('label', { className: 'dsh-cron-checkbox-row', style: { marginLeft: formWorktree ? '24px' : '0px', marginTop: '6px' } },
|
|
3893
|
+
_h('input', {
|
|
3894
|
+
type: 'checkbox',
|
|
3895
|
+
checked: formCaptureGitDiff,
|
|
3896
|
+
onChange: (e) => setFormCaptureGitDiff(e.target.checked)
|
|
3897
|
+
}),
|
|
3898
|
+
_h('span', null, T_KEY(t, 'form.captureGitDiffLabel'))
|
|
3686
3899
|
)
|
|
3687
3900
|
),
|
|
3688
3901
|
formType !== 'http' && _h('div', { className: 'dsh-cron-form-group' },
|
|
@@ -5002,7 +5215,9 @@ return false;
|
|
|
5002
5215
|
function apply(ctx) {
|
|
5003
5216
|
const stopStyleSelfHeal = startStyleSelfHeal();
|
|
5004
5217
|
try {
|
|
5005
|
-
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
5218
|
+
if (ctx.locale && typeof ctx.locale.register === 'function' && typeof ctx.effect === 'function') {
|
|
5219
|
+
ctx.effect(() => ctx.locale.register(NS, STRINGS), 'dsh-cron: locale dictionaries');
|
|
5220
|
+
} else if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
5006
5221
|
ctx.locale.register(NS, STRINGS);
|
|
5007
5222
|
}
|
|
5008
5223
|
} catch (e) {
|
package/lib/cron-tool.js
CHANGED
|
@@ -149,9 +149,13 @@ export const cronToolParameters = {
|
|
|
149
149
|
},
|
|
150
150
|
permissionPreset: {
|
|
151
151
|
type: 'string',
|
|
152
|
-
enum: ['default', 'read-only', 'workspace-write', 'full'],
|
|
152
|
+
enum: ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'],
|
|
153
153
|
description: 'Permission preset applied to the task session (only used for agent session tasks: llm, skill, workflow)',
|
|
154
154
|
},
|
|
155
|
+
captureGitDiff: {
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
description: 'Whether to capture git diff on run completion. Defaults to true for worktree tasks and false for non-worktree tasks.',
|
|
158
|
+
},
|
|
155
159
|
sessionTitle: {
|
|
156
160
|
type: 'string',
|
|
157
161
|
description: 'Custom session title template for isolated agent runs (supports {{date}} placeholder, e.g. "{{date}} [Daily] Report")',
|
package/lib/locales.json
CHANGED
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"transfer.exportFailed": "导出失败: {error}",
|
|
28
28
|
"transfer.importTitleModal": "导入任务",
|
|
29
29
|
"transfer.importHint": "仅导出任务静态配置:不包含历史记录与计数器。渠道通过凭据名称引用;注意完整导出保留自定义环境变量和 HTTP 请求头。",
|
|
30
|
-
"transfer.exportPrompt": "
|
|
30
|
+
"transfer.exportPrompt": "选择导出模式:\n确定 = 净化导出(移除自定义环境变量与请求头)\n取消 = 完整原始导出",
|
|
31
31
|
"transfer.strategyAdd": "作为新任务导入(分配新 ID)",
|
|
32
32
|
"transfer.strategyReplace": "覆盖同 ID 任务",
|
|
33
33
|
"transfer.strategySkip": "跳过已存在的同 ID 任务",
|
|
34
34
|
"transfer.summary": "该文件包含 {total} 个任务:{add} 个新增,{replace} 个覆盖,{skip} 个跳过。",
|
|
35
35
|
"transfer.confirm": "开始导入",
|
|
36
|
-
"transfer.imported": "
|
|
36
|
+
"transfer.imported": "已成功导入 {count} 个任务。",
|
|
37
37
|
"transfer.importFailed": "导入失败: {error}",
|
|
38
38
|
"transfer.badFile": "所选文件不是有效的 JSON 导出文件: {error}",
|
|
39
39
|
"actions.pause": "暂停",
|
|
@@ -104,25 +104,8 @@
|
|
|
104
104
|
"tabs.paused": "已暂停",
|
|
105
105
|
"tabs.completed": "已完成",
|
|
106
106
|
"list.empty": "暂无定时任务",
|
|
107
|
-
"list.emptyHint": "点击下方按钮让 DSH 规划任务,或手动配置。",
|
|
108
107
|
"list.oneShot": "一次性",
|
|
109
|
-
"list.runCount": "{count} 次运行",
|
|
110
|
-
"list.consecutiveFailures": "连续 {count} 次失败",
|
|
111
|
-
"list.lastRun": "上次: {time}",
|
|
112
|
-
"list.nextRun": "下次: {time}",
|
|
113
|
-
"list.badgeActive": "活跃",
|
|
114
|
-
"list.badgePaused": "已暂停",
|
|
115
|
-
"list.badgeCompleted": "已完成",
|
|
116
108
|
"list.configManaged": "配置只读",
|
|
117
|
-
"channel.telegram": "Telegram",
|
|
118
|
-
"channel.discord": "Discord",
|
|
119
|
-
"channel.slack": "Slack",
|
|
120
|
-
"channel.ntfy": "ntfy",
|
|
121
|
-
"channel.bark": "Bark",
|
|
122
|
-
"channel.pushplus": "PushPlus",
|
|
123
|
-
"channel.voice": "语音通话",
|
|
124
|
-
"channel.gitea": "Gitea",
|
|
125
|
-
"channel.webhook": "Webhook",
|
|
126
109
|
"modal.editTitle": "编辑任务",
|
|
127
110
|
"modal.newTitle": "新建定时任务",
|
|
128
111
|
"modal.statusActive": "活跃",
|
|
@@ -135,73 +118,44 @@
|
|
|
135
118
|
"modal.create": "创建任务",
|
|
136
119
|
"history.loading": "正在加载历史记录...",
|
|
137
120
|
"history.empty": "暂无运行记录",
|
|
138
|
-
"history.summaryTokens": "消耗 Token: {count}",
|
|
139
|
-
"history.summaryCost": "预估成本: ${cost}",
|
|
140
|
-
"history.errorLabel": "错误详情:",
|
|
141
|
-
"history.outputLabel": "执行输出:",
|
|
142
121
|
"history.statusUncertain": "不确定",
|
|
143
122
|
"history.artifacts": "产物文件",
|
|
144
123
|
"history.resolveCompleted": "标记已完成",
|
|
145
124
|
"history.resolveFailed": "标记失败",
|
|
146
125
|
"history.statusUncertainHint": "服务重启中断 — 请确认结果",
|
|
147
|
-
"form.titleLabel": "任务名称 *",
|
|
148
|
-
"form.titlePlaceholder": "例如: 每周备份检查",
|
|
149
126
|
"form.typeLabel": "执行类型 *",
|
|
150
|
-
"form.promptLabel": "提示词内容 *",
|
|
151
|
-
"form.promptPlaceholder": "输入发送给智能体的执行指令...",
|
|
152
127
|
"form.modelLabel": "AI 模型",
|
|
153
128
|
"form.modelDefault": "默认模型",
|
|
154
129
|
"form.fallbackModelLabel": "备用模型 (主模型失败时使用)",
|
|
155
|
-
"form.fallbackModelNone": "不使用备用模型",
|
|
156
|
-
"form.pipelineGroupLabel": "任务流水线串联",
|
|
157
130
|
"form.onSuccessLabel": "成功后触发 (任务 ID)",
|
|
158
|
-
"form.onSuccessPlaceholder": "例如: task_upload_backup",
|
|
159
131
|
"form.onFailureLabel": "失败后触发 (任务 ID)",
|
|
160
|
-
"form.onFailurePlaceholder": "例如: task_notify_incident",
|
|
161
132
|
"form.scheduleLabel": "调度计划 (Cron 或相对时间) *",
|
|
162
133
|
"form.schedulePlaceholder": "例如: 0 9 * * 1-5 或 15m 或 in 2 hours",
|
|
163
|
-
"form.channelsLabel": "通知渠道",
|
|
164
134
|
"form.channelsHint": "任务执行完毕后推送报告的目标渠道。",
|
|
165
135
|
"form.templateLabel": "自定义消息模板",
|
|
166
|
-
"form.templatePlaceholder": "
|
|
167
|
-
"form.templateHint": "
|
|
136
|
+
"form.templatePlaceholder": "⏰ {title} — {status},耗时 {duration}",
|
|
137
|
+
"form.templateHint": "可用变量: {title} {id} {status} {output} {error} {duration} {schedule} {time} {tokens} {cost} {model}。留空使用内置文案。",
|
|
168
138
|
"form.silentRuleLabel": "静默规则 (可选)",
|
|
169
139
|
"form.silentRulePlaceholder": "例如: ALL_OK 或 status == 200",
|
|
170
140
|
"form.silentRuleHint": "若输出符合此规则,将不发送任何通知。",
|
|
171
|
-
"form.advancedToggle": "高级执行选项",
|
|
172
141
|
"form.timezoneLabel": "时区 (IANA 时区名)",
|
|
173
142
|
"form.timezonePlaceholder": "例如: Asia/Shanghai 或 UTC (留空使用服务器时区)",
|
|
174
143
|
"form.misfireLabel": "错过执行策略 (Misfire Policy)",
|
|
175
144
|
"form.misfireSkip": "跳过 (默认)",
|
|
176
145
|
"form.misfireRunOnce": "补跑一次",
|
|
177
|
-
"form.maxRetriesLabel": "失败重试次数 (0 = 不重试)",
|
|
178
|
-
"form.permissionPresetLabel": "权限预设",
|
|
179
146
|
"form.permissionDefault": "默认 (继承主配置)",
|
|
180
|
-
"form.permissionRestricted": "受限模式 (只读)",
|
|
181
147
|
"form.permissionFull": "完全访问权限",
|
|
182
148
|
"form.envLabel": "环境变量 (KEY=VALUE, 每行一个)",
|
|
183
|
-
"form.envPlaceholder": "
|
|
149
|
+
"form.envPlaceholder": "NODE_ENV=production\nTZ=Europe/Berlin",
|
|
184
150
|
"form.cwdLabel": "工作目录 (绝对路径)",
|
|
185
|
-
"form.cwdPlaceholder": "/var/www/my-project",
|
|
186
|
-
"form.workspaceIdLabel": "关联 DSH Workspace ID",
|
|
187
|
-
"form.workspaceIdPlaceholder": "留空使用默认工作区",
|
|
188
151
|
"form.worktreeLabel": "在独立的临时 Git Worktree 中执行",
|
|
189
152
|
"form.keepWorktreeLabel": "执行结束后保留 Git Worktree (不自动清理)",
|
|
190
153
|
"form.timeoutLabel": "超时时间 (秒)",
|
|
191
154
|
"form.overlapLabel": "并发策略",
|
|
192
155
|
"form.overlapSkip": "跳过 (默认)",
|
|
193
156
|
"form.overlapQueue": "排队等待",
|
|
194
|
-
"form.overlapTerminate": "终止前次运行",
|
|
195
|
-
"form.overlapAllow": "允许并发",
|
|
196
157
|
"form.kanbanLabel": "看板同步模式",
|
|
197
158
|
"form.kanbanNone": "不同步到看板",
|
|
198
|
-
"form.kanbanCreate": "仅创建新卡片",
|
|
199
|
-
"form.kanbanSync": "保持卡片同步更新",
|
|
200
|
-
"settings.deliveryChannels": "推送渠道配置",
|
|
201
|
-
"settings.credentialsHint": "安全凭据在 DSH Credentials 服务中统一管理。",
|
|
202
|
-
"settings.saveChannels": "保存渠道配置",
|
|
203
|
-
"settings.testing": "正在测试...",
|
|
204
|
-
"settings.testSuccess": "✅ 测试成功!",
|
|
205
159
|
"settings.automationHint": "用于静默规则研判和故障诊断的辅助模型。留空沿用任务自身模型。",
|
|
206
160
|
"settings.silentRuleModelLabel": "静默规则研判模型",
|
|
207
161
|
"settings.secretsOnCard": "设置中引用的凭据将在前端脱敏展示,并通过名称引用。",
|
|
@@ -331,6 +285,119 @@
|
|
|
331
285
|
"form.joinPolicyAll": "所有前置均成功 (all_success)",
|
|
332
286
|
"form.joinPolicyAny": "任一前置成功即可 (any_success)",
|
|
333
287
|
"recipe.dependenciesMissing": "缺少必要集成服务或凭据",
|
|
334
|
-
"list.loadErrorPrefix": "加载任务列表失败"
|
|
288
|
+
"list.loadErrorPrefix": "加载任务列表失败",
|
|
289
|
+
"banner.loadError": "加载任务失败: {error}",
|
|
290
|
+
"banner.retry": "重试",
|
|
291
|
+
"list.loading": "正在加载定时任务...",
|
|
292
|
+
"list.typeShell": "Shell 脚本",
|
|
293
|
+
"list.typeLlm": "LLM 智能体",
|
|
294
|
+
"list.configManagedHint": "由配置文件中的 config.jobs 声明;要修改或删除请编辑该文件",
|
|
295
|
+
"list.next": " · 下次: {time}",
|
|
296
|
+
"recs.title": "推荐任务",
|
|
297
|
+
"recs.hint": "开箱即用的配方。点一下就会把表单填好——保存之前不会创建任何任务。",
|
|
298
|
+
"modal.close": "关闭",
|
|
299
|
+
"history.stoppedReasonTitle": "停止原因:",
|
|
300
|
+
"history.retryDelivery": "重试推送",
|
|
301
|
+
"history.retryingDelivery": "正在重试...",
|
|
302
|
+
"history.statusValidationFailed": "校验失败",
|
|
303
|
+
"history.stage_queue": "排队",
|
|
304
|
+
"history.stage_preflight": "预检",
|
|
305
|
+
"history.stage_execution": "执行",
|
|
306
|
+
"history.stage_validation": "校验",
|
|
307
|
+
"history.stage_delivery": "推送",
|
|
308
|
+
"history.refresh": "刷新历史",
|
|
309
|
+
"history.statusSuccess": "成功",
|
|
310
|
+
"history.statusSkipped": "已跳过",
|
|
311
|
+
"history.statusMissed": "已错过",
|
|
312
|
+
"history.statusTimeout": "超时",
|
|
313
|
+
"history.statusFailed": "失败",
|
|
314
|
+
"form.nameLabel": "任务名称",
|
|
315
|
+
"form.namePlaceholder": "例如: dsh-tags-watch",
|
|
316
|
+
"form.typeLlm": "LLM(AI 智能体)",
|
|
317
|
+
"form.typeScript": "Shell 命令 / 脚本",
|
|
318
|
+
"form.typeNode": "Node.js",
|
|
319
|
+
"form.typePython": "Python",
|
|
320
|
+
"form.typeHttp": "HTTP 请求 / Webhook",
|
|
321
|
+
"form.typeSsh": "远程 SSH",
|
|
322
|
+
"form.typeDocker": "Docker 容器",
|
|
323
|
+
"form.typeSkill": "DSH 技能",
|
|
324
|
+
"form.typeWorkflow": "DSH 工作流",
|
|
325
|
+
"form.envHint": "仅对非 LLM 运行时生效。不要在这里存放密钥。",
|
|
326
|
+
"form.workspaceLabel": "工作区 ID 或路径(可选)",
|
|
327
|
+
"form.httpMethodLabel": "HTTP 方法",
|
|
328
|
+
"form.httpUrlLabel": "URL",
|
|
329
|
+
"form.httpHeadersLabel": "请求头(JSON,可选)",
|
|
330
|
+
"form.httpBodyLabel": "请求体(可选)",
|
|
331
|
+
"form.sshProfileLabel": "dsh-remote-workspace 配置 ID",
|
|
332
|
+
"form.sshProfileHint": "推荐用法:主机与凭据都放在 remote-workspace 插件里。",
|
|
333
|
+
"form.sshTargetLabel": "备用目标(user@host,可选)",
|
|
334
|
+
"form.sshKeyLabel": "私钥路径(备用,可选)",
|
|
335
|
+
"form.dockerImageLabel": "容器镜像",
|
|
336
|
+
"form.skillNameLabel": "技能名称",
|
|
337
|
+
"form.workflowNameLabel": "工作流名称",
|
|
338
|
+
"form.overlapReplace": "取消当前运行并重新开始 (replace)",
|
|
339
|
+
"form.kanbanOnFailure": "失败时创建工单(进 Backlog)",
|
|
340
|
+
"form.kanbanAlways": "每次完成都建卡片(Done / Backlog)",
|
|
341
|
+
"form.providerLabel": "LLM 服务商",
|
|
342
|
+
"form.providerDefault": "默认(DSH)",
|
|
343
|
+
"history.silentSkip": "保持静默:",
|
|
344
|
+
"history.diagnosis": "诊断",
|
|
345
|
+
"history.applySuggestion": "用这条建议打开编辑表单",
|
|
346
|
+
"form.inspectOnFailureLabel": "用模型诊断失败原因",
|
|
347
|
+
"form.inspectOnFailureHint": "运行失败后,由模型读一遍提示词、错误和输出,把诊断结论和修改建议随这次运行一起保存。不会自动应用任何改动。",
|
|
348
|
+
"settings.inspectorModelLabel": "失败诊断模型",
|
|
349
|
+
"form.fallbackModelPlaceholder": "例如: deepseek-reasoner",
|
|
350
|
+
"form.fallbackModelHint": "主模型运行失败时,用这个模型再试一次。两次尝试都计入成本。",
|
|
351
|
+
"form.promptLabelScript": "Shell 命令或脚本",
|
|
352
|
+
"form.promptLabelLlm": "给智能体的指令 / 提示词",
|
|
353
|
+
"form.promptPlaceholderScript": "curl -fsSL https://... || exit 1",
|
|
354
|
+
"form.promptPlaceholderLlm": "精确描述智能体要做什么...",
|
|
355
|
+
"form.misfireCatchUp": "迟到时补跑一次并记录缺口 (catchUpAll)",
|
|
356
|
+
"form.retriesLabel": "失败自动重试次数(0 = 关闭)",
|
|
357
|
+
"form.permissionLabel": "会话权限",
|
|
358
|
+
"form.permissionReadonly": "只读",
|
|
359
|
+
"form.permissionWrite": "工作区写入",
|
|
360
|
+
"list.runningNow": "正在运行",
|
|
361
|
+
"history.openSession": "打开会话",
|
|
362
|
+
"form.notifyTitle": "Telegram 通知",
|
|
363
|
+
"form.notifyTelegram": "把运行报告发送到 Telegram",
|
|
364
|
+
"form.notifyOnlyFailure": "仅失败时发送(onlyOnFailure / 成功静默)",
|
|
365
|
+
"form.channelsTitle": "推送渠道",
|
|
366
|
+
"form.channelTelegram": "Telegram",
|
|
367
|
+
"form.channelKanban": "dsh-kanban 卡片",
|
|
368
|
+
"form.channelDiscord": "Discord",
|
|
369
|
+
"form.channelSlack": "Slack",
|
|
370
|
+
"form.channelNtfy": "ntfy",
|
|
371
|
+
"form.channelBark": "Bark",
|
|
372
|
+
"form.channelPushplus": "PushPlus",
|
|
373
|
+
"form.channelTts": "语音(dsh-tts)",
|
|
374
|
+
"form.channelGitea": "Gitea Issue",
|
|
375
|
+
"settings.deliverySection": "推送渠道",
|
|
376
|
+
"settings.secretsSection": "凭据(引用)",
|
|
377
|
+
"settings.secretsHint": "这里填 DSH 凭据的「名称」,不是密钥本身。密钥保存在凭据服务里。",
|
|
378
|
+
"settings.telegramSection": "Telegram",
|
|
379
|
+
"settings.templatesSection": "消息模板",
|
|
380
|
+
"settings.channelTemplateLabel": "{channel} 的消息模板",
|
|
381
|
+
"settings.botTokenRefLabel": "Bot Token 凭据名称",
|
|
382
|
+
"settings.botTokenRefPlaceholder": "例如: TELEGRAM_BOT_TOKEN",
|
|
383
|
+
"settings.apiTokenLabel": "外部 API Token",
|
|
384
|
+
"settings.apiTokenPlaceholder": "用于 /dsh-cron/api/* 的 Bearer token(留空 = 关闭)",
|
|
385
|
+
"settings.discordLabel": "Discord Webhook URL",
|
|
386
|
+
"settings.slackLabel": "Slack Webhook URL",
|
|
387
|
+
"settings.ntfyUrlLabel": "ntfy 服务器地址",
|
|
388
|
+
"settings.ntfyTopicLabel": "ntfy 主题",
|
|
389
|
+
"settings.ntfyTokenRefLabel": "ntfy Token 凭据名称",
|
|
390
|
+
"settings.barkServerLabel": "Bark 服务器地址",
|
|
391
|
+
"settings.barkKeyLabel": "Bark 设备 Key",
|
|
392
|
+
"settings.pushplusTokenRefLabel": "PushPlus Token 凭据名称",
|
|
393
|
+
"settings.pushplusUrlLabel": "PushPlus 接口地址",
|
|
394
|
+
"settings.ttsBaseUrlLabel": "dsh-tts 服务地址",
|
|
395
|
+
"settings.giteaBaseUrlLabel": "Gitea 服务地址",
|
|
396
|
+
"settings.giteaRepoLabel": "Gitea 仓库(owner/repo)",
|
|
397
|
+
"settings.giteaTokenRefLabel": "Gitea Token 凭据名称",
|
|
398
|
+
"settings.templateLabel": "默认消息模板",
|
|
399
|
+
"settings.templateHint": "作用于所有没有单独模板的渠道。留空用内置文案。",
|
|
400
|
+
"settings.deliveryTimeoutMsLabel": "每个渠道的推送超时(毫秒)",
|
|
401
|
+
"settings.automationSection": "自动化模型"
|
|
335
402
|
}
|
|
336
|
-
}
|
|
403
|
+
}
|
package/lib/runner.js
CHANGED
|
@@ -644,16 +644,37 @@ export class SessionRunner {
|
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
/** Per-job permission preset (#32): read-only | workspace-write | full. */
|
|
647
|
+
/** Per-job permission preset (#32, #380, GH #18): read-only | workspace-write | danger-full-access (or full alias). */
|
|
648
648
|
_applyPermissionPreset(task, handle) {
|
|
649
|
-
if (!task.permissionPreset || task.permissionPreset === 'default')
|
|
649
|
+
if (!task.permissionPreset || task.permissionPreset === 'default') {
|
|
650
|
+
this._pinApprovalPolicy(handle);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const targetPreset = task.permissionPreset === 'full' ? 'danger-full-access' : task.permissionPreset;
|
|
650
654
|
try {
|
|
651
|
-
const presets = this.ctx.get
|
|
655
|
+
const presets = this.ctx.get ? this.ctx.get('permissionPresets') : this.ctx.permissionPresets;
|
|
652
656
|
if (presets && typeof presets.set === 'function') {
|
|
653
|
-
presets.set(handle.agent.session,
|
|
657
|
+
presets.set(handle.agent.session, targetPreset);
|
|
654
658
|
}
|
|
655
659
|
} catch (ppErr) {
|
|
656
|
-
logger.warn(
|
|
660
|
+
logger.warn(`[dsh-cron] permission preset "${task.permissionPreset}" (target: "${targetPreset}") not applied to task ${task.id || ''}:`, ppErr.message);
|
|
661
|
+
}
|
|
662
|
+
this._pinApprovalPolicy(handle);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
_pinApprovalPolicy(handle) {
|
|
666
|
+
try {
|
|
667
|
+
const approval = this.ctx.get ? this.ctx.get('approval') : this.ctx.approval;
|
|
668
|
+
if (approval && typeof approval.setPolicy === 'function') {
|
|
669
|
+
if (handle?.agent) {
|
|
670
|
+
bestEffort('approval-set-policy-agent', () => approval.setPolicy(handle.agent, 'never'), logger);
|
|
671
|
+
}
|
|
672
|
+
if (handle?.agent?.session) {
|
|
673
|
+
bestEffort('approval-set-policy-session', () => approval.setPolicy(handle.agent.session, 'never'), logger);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} catch (appErr) {
|
|
677
|
+
logger.warn('[dsh-cron] approval setPolicy not applied:', appErr.message);
|
|
657
678
|
}
|
|
658
679
|
}
|
|
659
680
|
|
|
@@ -142,16 +142,20 @@ export async function handleCompleteRun(scheduler, task, taskId, currentRun, out
|
|
|
142
142
|
runInfo.swarmStats = outcome.swarmStats;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Capture git diff if task runs in a worktree or
|
|
146
|
-
const
|
|
145
|
+
// Capture git diff if task runs in a worktree or explicitly opted in (#351, #379, GH #17)
|
|
146
|
+
const explicitCapture = (typeof task.captureGitDiff === 'boolean')
|
|
147
|
+
? task.captureGitDiff
|
|
148
|
+
: ((typeof task.captureDiff === 'boolean') ? task.captureDiff : null);
|
|
149
|
+
const shouldCaptureDiff = explicitCapture !== null ? explicitCapture : Boolean(task.worktree);
|
|
150
|
+
const diffDir = shouldCaptureDiff ? (task.worktree ? (runInfo.worktreePath || task.cwd) : task.cwd) : null;
|
|
147
151
|
if (diffDir) {
|
|
148
152
|
try {
|
|
149
153
|
const diffResult = await captureGitDiff(diffDir);
|
|
150
154
|
if (diffResult && diffResult.hasChanges) {
|
|
151
155
|
runInfo.gitDiff = diffResult;
|
|
152
156
|
}
|
|
153
|
-
} catch {
|
|
154
|
-
|
|
157
|
+
} catch (diffErr) {
|
|
158
|
+
bestEffort('log-diff-error', () => scheduler?.logger?.debug?.(`[dsh-cron] git diff capture failed: ${diffErr?.message}`));
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
package/lib/store.js
CHANGED
|
@@ -60,6 +60,7 @@ export class TaskStore {
|
|
|
60
60
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
61
|
}
|
|
62
62
|
bestEffort('chmod-dir', () => fs.chmodSync(dir, 0o700));
|
|
63
|
+
this._cleanOrphanedTmpFiles(dir);
|
|
63
64
|
const bakPath = `${this.filePath}.bak`;
|
|
64
65
|
|
|
65
66
|
const loadData = (raw) => {
|
|
@@ -823,6 +824,17 @@ export class TaskStore {
|
|
|
823
824
|
},
|
|
824
825
|
...prevRuns,
|
|
825
826
|
];
|
|
827
|
+
// Truncate raw diff in older active history entries to bound memory and JSON size (#379, GH #17)
|
|
828
|
+
for (let i = 10; i < runs.length; i++) {
|
|
829
|
+
const r = runs[i];
|
|
830
|
+
if (r && r.gitDiff && r.gitDiff.diff && r.gitDiff.diff.length > 2000) {
|
|
831
|
+
r.gitDiff = {
|
|
832
|
+
...r.gitDiff,
|
|
833
|
+
diff: r.gitDiff.diff.slice(0, 2000) + '\n... [diff truncated in history]',
|
|
834
|
+
truncated: true,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
826
838
|
// Keep up to 100 runs in active store; archive older runs to tasks-history-archive.json (#134)
|
|
827
839
|
if (runs.length > 100) {
|
|
828
840
|
const overflow = runs.splice(100);
|
|
@@ -833,7 +845,32 @@ export class TaskStore {
|
|
|
833
845
|
}
|
|
834
846
|
|
|
835
847
|
/**
|
|
836
|
-
*
|
|
848
|
+
* Remove orphaned temporary write files older than 10 minutes (#379, GH #17).
|
|
849
|
+
*/
|
|
850
|
+
_cleanOrphanedTmpFiles(dir) {
|
|
851
|
+
try {
|
|
852
|
+
if (!fs.existsSync(dir)) return;
|
|
853
|
+
const entries = fs.readdirSync(dir);
|
|
854
|
+
const now = Date.now();
|
|
855
|
+
const tenMinutesMs = 10 * 60 * 1000;
|
|
856
|
+
for (const entry of entries) {
|
|
857
|
+
if (entry.includes('.tmp.')) {
|
|
858
|
+
const fullPath = path.join(dir, entry);
|
|
859
|
+
bestEffort('clean-tmp-file', () => {
|
|
860
|
+
const stat = fs.statSync(fullPath);
|
|
861
|
+
if (stat.isFile() && (now - stat.mtimeMs > tenMinutesMs)) {
|
|
862
|
+
fs.unlinkSync(fullPath);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch (dirErr) {
|
|
868
|
+
bestEffort('log-clean-tmp-dir-error', () => logger.debug?.(`[dsh-cron] clean orphaned tmp dir error: ${dirErr?.message}`));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Append rotated overflow history entries into tasks-history-archive.json (#134, #379, GH #17).
|
|
837
874
|
*/
|
|
838
875
|
_archiveHistoryRuns(id, overflowRuns) {
|
|
839
876
|
if (!Array.isArray(overflowRuns) || overflowRuns.length === 0) return;
|
|
@@ -845,14 +882,43 @@ export class TaskStore {
|
|
|
845
882
|
archive = bestEffort('parse-history-archive', () => JSON.parse(fs.readFileSync(archivePath, 'utf-8'))) || {};
|
|
846
883
|
}
|
|
847
884
|
if (!Array.isArray(archive[id])) archive[id] = [];
|
|
848
|
-
|
|
885
|
+
|
|
886
|
+
// Strip full raw diff payload from archived overflow runs to prevent runaway JSON growth (#379, GH #17)
|
|
887
|
+
const sanitizedOverflow = overflowRuns.map((r) => {
|
|
888
|
+
if (!r || !r.gitDiff) return r;
|
|
889
|
+
const { diff, ...gitDiffMeta } = r.gitDiff;
|
|
890
|
+
return {
|
|
891
|
+
...r,
|
|
892
|
+
gitDiff: {
|
|
893
|
+
...gitDiffMeta,
|
|
894
|
+
diffExcerpt: diff ? diff.slice(0, 500) : '',
|
|
895
|
+
hasChanges: Boolean(gitDiffMeta.hasChanges),
|
|
896
|
+
archived: true,
|
|
897
|
+
},
|
|
898
|
+
};
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
archive[id].push(...sanitizedOverflow);
|
|
849
902
|
if (archive[id].length > 1000) {
|
|
850
903
|
archive[id] = archive[id].slice(-1000);
|
|
851
904
|
}
|
|
905
|
+
|
|
906
|
+
// Enforce total archive size budget (max 15MB) across tasks (#379, GH #17)
|
|
907
|
+
const MAX_ARCHIVE_BYTES = 15 * 1024 * 1024;
|
|
908
|
+
let serialized = JSON.stringify(archive);
|
|
909
|
+
if (serialized.length > MAX_ARCHIVE_BYTES) {
|
|
910
|
+
for (const taskId of Object.keys(archive)) {
|
|
911
|
+
if (Array.isArray(archive[taskId]) && archive[taskId].length > 100) {
|
|
912
|
+
archive[taskId] = archive[taskId].slice(-100);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
serialized = JSON.stringify(archive);
|
|
916
|
+
}
|
|
917
|
+
|
|
852
918
|
// Write atomically with tmp file using compact JSON (avoiding mega-lines and null, 2 serialization bloat)
|
|
853
919
|
const rand = Math.random().toString(36).slice(2, 8);
|
|
854
920
|
const tmp = `${archivePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
855
|
-
fs.writeFileSync(tmp,
|
|
921
|
+
fs.writeFileSync(tmp, serialized, { encoding: 'utf-8', mode: 0o600 });
|
|
856
922
|
bestEffort('chmod-tmp', () => fs.chmodSync(tmp, 0o600));
|
|
857
923
|
fs.renameSync(tmp, archivePath);
|
|
858
924
|
bestEffort('chmod-archive', () => fs.chmodSync(archivePath, 0o600));
|
package/lib/task-create.js
CHANGED
|
@@ -85,7 +85,8 @@ export function executeCreateTask(store, scheduler, args) {
|
|
|
85
85
|
misfirePolicy: ['skip', 'runOnce', 'catchUpAll'].includes(args.misfirePolicy) ? args.misfirePolicy : 'skip',
|
|
86
86
|
maxRetries: Number(args.maxRetries) > 0 ? Number(args.maxRetries) : 0,
|
|
87
87
|
retryBackoffMs: Number(args.retryBackoffMs) > 0 ? Number(args.retryBackoffMs) : 30000,
|
|
88
|
-
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
88
|
+
permissionPreset: !EXTERNAL_RUNTIMES.includes(taskType) && ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(args.permissionPreset) ? args.permissionPreset : 'default',
|
|
89
|
+
captureGitDiff: typeof args.captureGitDiff === 'boolean' ? args.captureGitDiff : (typeof args.captureDiff === 'boolean' ? args.captureDiff : undefined),
|
|
89
90
|
kanbanMode: args.kanbanMode || 'none',
|
|
90
91
|
channels: Array.isArray(args.channels) ? args.channels.filter((c) => CHANNEL_IDS.includes(c)) : undefined,
|
|
91
92
|
template: args.template ? String(args.template) : '',
|
package/lib/task-patch.js
CHANGED
|
@@ -156,6 +156,16 @@ export function buildTaskPatch(current, body) {
|
|
|
156
156
|
if (patch.tokenLimit !== undefined) {
|
|
157
157
|
patch.tokenLimit = patch.tokenLimit === null ? null : (Number(patch.tokenLimit) > 0 ? Number(patch.tokenLimit) : null);
|
|
158
158
|
}
|
|
159
|
+
if (patch.permissionPreset !== undefined) {
|
|
160
|
+
patch.permissionPreset = ['default', 'read-only', 'workspace-write', 'danger-full-access', 'full'].includes(patch.permissionPreset)
|
|
161
|
+
? patch.permissionPreset
|
|
162
|
+
: 'default';
|
|
163
|
+
}
|
|
164
|
+
if (patch.captureGitDiff !== undefined) {
|
|
165
|
+
patch.captureGitDiff = Boolean(patch.captureGitDiff);
|
|
166
|
+
} else if (patch.captureDiff !== undefined) {
|
|
167
|
+
patch.captureGitDiff = Boolean(patch.captureDiff);
|
|
168
|
+
}
|
|
159
169
|
const effectiveType = patch.type !== undefined ? normalizeTaskType(patch.type) : current.type;
|
|
160
170
|
if (EXTERNAL_RUNTIMES.includes(effectiveType) && patch.permissionPreset !== undefined) {
|
|
161
171
|
patch.permissionPreset = 'default';
|
package/lib/task-transfer.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.22",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|