@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 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
- - **Automatic Diff Capture**: Tasks running in Git worktrees capture modified files, stat summary, and unified diffs (up to 256 KiB) automatically.
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
 
@@ -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
- 'modal.confirmDiscard': '放弃未保存的内容?',
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": "请选择导出模式:\\n确定 = 净化导出(移除自定义环境变量与请求头)\\n取消 = 完整原始导出",
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": "已成功导出 {count} 个任务。",
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": "支持变量: {title}, {status}, {output}, {error}, {durationMs}, {time}",
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": "FOO=bar\\nAPI_KEY=secret",
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') return;
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 && this.ctx.get('permissionPresets');
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, task.permissionPreset);
657
+ presets.set(handle.agent.session, targetPreset);
654
658
  }
655
659
  } catch (ppErr) {
656
- logger.warn('[dsh-cron] permission preset not applied:', ppErr.message);
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 custom git cwd (#351)
146
- const diffDir = task.worktree ? (runInfo.worktreePath || task.cwd) : task.cwd;
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
- // Non-fatal if diff capture fails
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
- * Append rotated overflow history entries into tasks-history-archive.json (#134).
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
- archive[id].push(...overflowRuns);
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, JSON.stringify(archive), { encoding: 'utf-8', mode: 0o600 });
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));
@@ -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';
@@ -44,6 +44,7 @@ export const PATCHABLE_TASK_FIELDS = [
44
44
  'workspaceId',
45
45
  'worktree',
46
46
  'keepWorktree',
47
+ 'captureGitDiff',
47
48
  'httpMethod',
48
49
  'httpUrl',
49
50
  'httpHeaders',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.3.20",
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",