diff --git a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md b/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md index 8834e693..bbc97dd8 100644 --- a/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md +++ b/project-docs/AI合同起草与审查助手-当前工作进度保存-2026-05-11.md @@ -2063,6 +2063,62 @@ - 本轮无 required 阻塞项。 - 当前剩余均为 non-required 运维增强项:真实外部 Webhook 地址、可选容量清理、后续法条批次扩展。 +## 2026-05-17 低负载生产值守调度与 Webhook 通知入口 +### 本轮完成项 +- 已把生产值守调度改为适合资源紧张服务器的低负载模式: + - 计划任务:`AIContractProductionWatchdog` + - 执行时间:每天 `02:00` + - 通知策略:`WarnOrFail`,正常 PASS 不通知,只有 WARN/FAIL 才通知。 + - 日常巡检只做轻量生产 ops check;容量治理 dry-run 不放进每日任务,保留给维护窗口单独执行。 + - 日常任务跳过本机开发工作区 clean 检查,避免开发机未提交前端改动造成假告警。 +- 已增强 `scripts/run-production-watchdog-scheduler.ps1`: + - 默认时间从 `07:30` 调整为 `02:00`。 + - 新增 `WebhookKind=auto`,可按 URL 自动识别企业微信、钉钉或通用 Webhook。 + - 新增钉钉 markdown payload。 + - 注册任务不再硬编码 `-RequireCleanWorktree` 和 `-FailOnWarn`。 + - 新增 `-SkipWorktreeCheck`、`-SkipCapacityGovernance` 透传能力。 +- 已增强相关子脚本: + - `scripts/run-production-watchdog.ps1` + - `scripts/run-production-ops-check.ps1` + - `scripts/run-capacity-governance.ps1` + - 支持日常巡检场景跳过本机工作区噪声。 + +### 本轮验证 +- 企业微信 payload dry-run: + - `PHASE7_WATCHDOG_SCHEDULER_PASS` + - 产物:`project-docs/qa-artifacts/phase7.5-watchdog-scheduler/2026-05-17T18-34-32-390/watchdog-notification-payload.json` +- 钉钉 payload dry-run: + - `PHASE7_WATCHDOG_SCHEDULER_PASS` + - 产物:`project-docs/qa-artifacts/phase7.5-watchdog-scheduler/2026-05-17T18-34-32-389/watchdog-notification-payload.json` +- 计划任务 dry-run: + - `PHASE7_WATCHDOG_SCHEDULER_PASS` + - 产物:`project-docs/qa-artifacts/phase7.5-watchdog-scheduler/2026-05-17T18-34-32-406/watchdog-scheduler-summary.md` +- 真实注册计划任务: + - `PHASE7_WATCHDOG_SCHEDULER_PASS` + - 产物:`project-docs/qa-artifacts/phase7.5-watchdog-scheduler/2026-05-17T18-35-03-112/watchdog-scheduler-summary.md` + - Windows 任务下一次执行时间:`2026-05-18 02:00:00` +- 同配置手动轻量巡检: + - `PHASE7_WATCHDOG_SCHEDULER_PASS` + - 产物:`project-docs/qa-artifacts/phase7.5-watchdog-scheduler/2026-05-17T18-35-27-004/watchdog-scheduler-summary.md` + - 子巡检:production ops check PASS,capacity governance SKIP,working tree SKIP。 + +### 当前状态 +- 每日凌晨 2 点低负载生产巡检已经启用。 +- 计划任务命令只引用 `AI_CONTRACT_WATCHDOG_WEBHOOK_URL`,不把真实 Webhook URL 写进 Git、脚本或任务命令。 +- 当前没有真实 Webhook URL 时,巡检仍会正常产出本地证据;配置 URL 后,企业微信或钉钉会在 WARN/FAIL 时收到通知。 + +### 仍需配合项 +- 需要提供一个真实 Webhook URL: + - 企业微信机器人 URL,或 + - 钉钉机器人 URL,或 + - 其他能接收 JSON POST 的通用 Webhook。 +- URL 应只配置到运行环境变量 `AI_CONTRACT_WATCHDOG_WEBHOOK_URL`,不能写入仓库文档或代码。 +- 如果使用钉钉且机器人开启“关键词”安全策略,关键词建议包含 `AI Contract Platform`,否则钉钉会拒收消息。 + +### 阻塞项 +- 本轮无 required 阻塞项。 +- 真实外部告警到人仍等待 Webhook URL;在 URL 提供前,系统已完成本地证据闭环和 02:00 低负载巡检调度。 + ## 2026-05-16 Phase14 生产巡检与 Chat/报告法条引用闭环 ### 本轮完成项 - 已完成 Chat/报告法条引用闭环代码补齐并推送远程 `main`: diff --git a/scripts/run-capacity-governance.ps1 b/scripts/run-capacity-governance.ps1 index fd46e492..cb427720 100644 --- a/scripts/run-capacity-governance.ps1 +++ b/scripts/run-capacity-governance.ps1 @@ -17,7 +17,8 @@ param( [switch]$Apply, [switch]$SkipBackupCleanup, [switch]$SkipDockerPrune, - [switch]$AllowDirtyWorktree + [switch]$AllowDirtyWorktree, + [switch]$SkipWorktreeCheck ) $ErrorActionPreference = "Stop" @@ -132,7 +133,9 @@ $commit = (git -C $repoRoot rev-parse HEAD).Trim() $remoteUrl = (git -C $repoRoot remote get-url origin).Trim() $gitStatus = git -C $repoRoot status --short ($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath -if ($gitStatus) { +if ($SkipWorktreeCheck) { + Add-Check -Key "working_tree" -Label "working tree" -Status "skip" -Required $false -Detail "Skipped for low-noise scheduled runtime monitoring" -Meta @{ artifact = $gitStatusPath } +} elseif ($gitStatus) { if ($AllowDirtyWorktree) { Add-Check -Key "working_tree" -Label "working tree" -Status "warn" -Required $false -Detail "Working tree has uncommitted changes; capacity governance allowed by -AllowDirtyWorktree" -Meta @{ artifact = $gitStatusPath } } else { @@ -386,6 +389,7 @@ $summary = [ordered]@{ skip_backup_cleanup = [bool]$SkipBackupCleanup skip_docker_prune = [bool]$SkipDockerPrune allow_dirty_worktree = [bool]$AllowDirtyWorktree + skip_worktree_check = [bool]$SkipWorktreeCheck } checks = $checks totals = [ordered]@{ diff --git a/scripts/run-production-ops-check.ps1 b/scripts/run-production-ops-check.ps1 index 5ff06612..d7f1ac17 100644 --- a/scripts/run-production-ops-check.ps1 +++ b/scripts/run-production-ops-check.ps1 @@ -15,7 +15,8 @@ param( [int]$DiskFailPercent = 90, [int]$MinFrontendBackupCount = 1, [int]$MaxNewestBackupAgeHours = 168, - [switch]$RequireCleanWorktree + [switch]$RequireCleanWorktree, + [switch]$SkipWorktreeCheck ) $ErrorActionPreference = "Stop" @@ -177,7 +178,9 @@ $commit = (git -C $repoRoot rev-parse HEAD).Trim() $remoteUrl = (git -C $repoRoot remote get-url origin).Trim() $gitStatus = git -C $repoRoot status --short ($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath -if ($gitStatus) { +if ($SkipWorktreeCheck) { + Add-Check -Key "working_tree" -Label "working tree" -Status "skip" -Required $false -Detail "Skipped for low-noise scheduled runtime monitoring" -Meta @{ artifact = $gitStatusPath } +} elseif ($gitStatus) { if ($RequireCleanWorktree) { Add-Check -Key "working_tree" -Label "working tree" -Status "fail" -Detail "Working tree has uncommitted changes and -RequireCleanWorktree is set" -Meta @{ artifact = $gitStatusPath } } else { @@ -388,6 +391,7 @@ $summary = [ordered]@{ backend_base_url = $BackendBaseUrl remote_backup_root = $RemoteBackupRoot remote_rollback_root = $RemoteRollbackRoot + skip_worktree_check = [bool]$SkipWorktreeCheck } thresholds = [ordered]@{ disk_warn_percent = $DiskWarnPercent diff --git a/scripts/run-production-watchdog-scheduler.ps1 b/scripts/run-production-watchdog-scheduler.ps1 index ddad86cc..627f980f 100644 --- a/scripts/run-production-watchdog-scheduler.ps1 +++ b/scripts/run-production-watchdog-scheduler.ps1 @@ -5,14 +5,14 @@ param( [string]$TaskDescription = "AI Contract Platform production watchdog", [ValidateSet("Daily", "Hourly")] [string]$Schedule = "Daily", - [string]$At = "07:30", + [string]$At = "02:00", [int]$IntervalMinutes = 60, [string]$ArtifactRoot = "", [string]$WatchdogArtifactRoot = "", [string]$WebhookUrl = "", [string]$WebhookUrlEnvVar = "AI_CONTRACT_WATCHDOG_WEBHOOK_URL", - [ValidateSet("generic", "wecom")] - [string]$WebhookKind = "generic", + [ValidateSet("auto", "generic", "wecom", "dingtalk")] + [string]$WebhookKind = "auto", [ValidateSet("All", "WarnOrFail", "FailOnly", "Never")] [string]$NotifyOnStatus = "WarnOrFail", [int]$WebhookTimeoutSeconds = 10, @@ -21,6 +21,8 @@ param( [switch]$SkipWebhook, [switch]$RequireCleanWorktree, [switch]$AllowDirtyWorktree, + [switch]$SkipWorktreeCheck, + [switch]$SkipCapacityGovernance, [switch]$IncludeReleaseGate, [switch]$IncludeBackupDrill, [switch]$FailOnWarn, @@ -102,6 +104,20 @@ function Test-ShouldNotify { } } +function Get-EffectiveWebhookKind { + $effectiveWebhookUrl = Get-EffectiveWebhookUrl + if ($WebhookKind -ne "auto") { + return $WebhookKind + } + if ($effectiveWebhookUrl -match "qyapi\.weixin\.qq\.com") { + return "wecom" + } + if ($effectiveWebhookUrl -match "oapi\.dingtalk\.com|dingtalk\.com") { + return "dingtalk" + } + return "generic" +} + function Get-WebhookPayload { param( [string]$Status, @@ -111,7 +127,8 @@ function Get-WebhookPayload { ) $text = "AI Contract Platform production watchdog $Status ($Severity). Artifact: $WatchdogArtifactDir" - if ($WebhookKind -eq "wecom") { + $effectiveWebhookKind = Get-EffectiveWebhookKind + if ($effectiveWebhookKind -eq "wecom") { return [ordered]@{ msgtype = "markdown" markdown = [ordered]@{ @@ -119,6 +136,15 @@ function Get-WebhookPayload { } } } + if ($effectiveWebhookKind -eq "dingtalk") { + return [ordered]@{ + msgtype = "markdown" + markdown = [ordered]@{ + title = "AI Contract Platform Watchdog" + text = "AI Contract Platform Watchdog: $text" + } + } + } return [ordered]@{ project = "ai-contract-platform" @@ -167,7 +193,7 @@ function Add-WebhookResult { Add-Check -Key "webhook" -Label "webhook notification" -Status "pass" -Detail "Webhook payload generated in dry-run mode" -Meta @{ payload = $notificationPayloadJson url_source = if ([string]::IsNullOrWhiteSpace($WebhookUrl)) { $WebhookUrlEnvVar } else { "parameter" } - webhook_kind = $WebhookKind + webhook_kind = Get-EffectiveWebhookKind dry_run = $true } return @@ -179,14 +205,14 @@ function Add-WebhookResult { Add-Check -Key "webhook" -Label "webhook notification" -Status "pass" -Detail "Webhook notification delivered" -Meta @{ payload = $notificationPayloadJson url_source = if ([string]::IsNullOrWhiteSpace($WebhookUrl)) { $WebhookUrlEnvVar } else { "parameter" } - webhook_kind = $WebhookKind + webhook_kind = Get-EffectiveWebhookKind dry_run = $false } } catch { Add-Check -Key "webhook" -Label "webhook notification" -Status "fail" -Detail "Webhook delivery failed: $($_.Exception.Message)" -Meta @{ payload = $notificationPayloadJson url_source = if ([string]::IsNullOrWhiteSpace($WebhookUrl)) { $WebhookUrlEnvVar } else { "parameter" } - webhook_kind = $WebhookKind + webhook_kind = Get-EffectiveWebhookKind dry_run = $false } } @@ -204,8 +230,21 @@ function Get-TaskRunArgumentLine { $args.Add("RunOnce") | Out-Null $args.Add("-ArtifactRoot") | Out-Null $args.Add(('"{0}"' -f $ArtifactRoot)) | Out-Null - $args.Add("-RequireCleanWorktree") | Out-Null - $args.Add("-FailOnWarn") | Out-Null + if ($RequireCleanWorktree) { + $args.Add("-RequireCleanWorktree") | Out-Null + } + if ($AllowDirtyWorktree) { + $args.Add("-AllowDirtyWorktree") | Out-Null + } + if ($SkipWorktreeCheck) { + $args.Add("-SkipWorktreeCheck") | Out-Null + } + if ($SkipCapacityGovernance) { + $args.Add("-SkipCapacityGovernance") | Out-Null + } + if ($FailOnWarn) { + $args.Add("-FailOnWarn") | Out-Null + } if ($SkipWebhook) { $args.Add("-SkipWebhook") | Out-Null } @@ -240,6 +279,12 @@ function Invoke-RunOnce { if ($AllowDirtyWorktree) { $watchdogArgs += "-AllowDirtyWorktree" } + if ($SkipWorktreeCheck) { + $watchdogArgs += "-SkipWorktreeCheck" + } + if ($SkipCapacityGovernance) { + $watchdogArgs += "-SkipCapacityGovernance" + } if ($IncludeReleaseGate) { $watchdogArgs += "-IncludeReleaseGate" } @@ -438,12 +483,15 @@ $summary = [ordered]@{ webhook_url_env_var = $WebhookUrlEnvVar webhook_url_configured = -not [string]::IsNullOrWhiteSpace((Get-EffectiveWebhookUrl)) webhook_kind = $WebhookKind + effective_webhook_kind = Get-EffectiveWebhookKind notify_on_status = $NotifyOnStatus dry_run_webhook = [bool]$DryRunWebhook require_webhook = [bool]$RequireWebhook skip_webhook = [bool]$SkipWebhook require_clean_worktree = [bool]$RequireCleanWorktree allow_dirty_worktree = [bool]$AllowDirtyWorktree + skip_worktree_check = [bool]$SkipWorktreeCheck + skip_capacity_governance = [bool]$SkipCapacityGovernance include_release_gate = [bool]$IncludeReleaseGate include_backup_drill = [bool]$IncludeBackupDrill fail_on_warn = [bool]$FailOnWarn diff --git a/scripts/run-production-watchdog.ps1 b/scripts/run-production-watchdog.ps1 index ffab9d87..df1f151c 100644 --- a/scripts/run-production-watchdog.ps1 +++ b/scripts/run-production-watchdog.ps1 @@ -16,6 +16,7 @@ param( [int]$DiskFailPercent = 90, [switch]$RequireCleanWorktree, [switch]$AllowDirtyWorktree, + [switch]$SkipWorktreeCheck, [switch]$IncludeReleaseGate, [switch]$IncludeBackupDrill, [switch]$SkipOpsCheck, @@ -133,7 +134,9 @@ $commit = (git -C $repoRoot rev-parse HEAD).Trim() $remoteUrl = (git -C $repoRoot remote get-url origin).Trim() $gitStatus = git -C $repoRoot status --short ($gitStatus -join [Environment]::NewLine) | Set-Content -Encoding UTF8 -Path $gitStatusPath -if ($gitStatus) { +if ($SkipWorktreeCheck) { + Add-Check -Key "working_tree" -Label "working tree" -Status "skip" -Required $false -Detail "Skipped for low-noise scheduled runtime monitoring" -Meta @{ artifact = $gitStatusPath } +} elseif ($gitStatus) { if ($RequireCleanWorktree -and -not $AllowDirtyWorktree) { Add-Check -Key "working_tree" -Label "working tree" -Status "fail" -Detail "Working tree has uncommitted changes and -RequireCleanWorktree is set" -Meta @{ artifact = $gitStatusPath } } else { @@ -181,6 +184,9 @@ if ($failedPreflight.Count -eq 0) { if ($RequireCleanWorktree -and -not $AllowDirtyWorktree) { $opsArgs += "-RequireCleanWorktree" } + if ($SkipWorktreeCheck) { + $opsArgs += "-SkipWorktreeCheck" + } Invoke-WatchStep -Key "ops_check" -Label "production ops check" -ScriptName "run-production-ops-check.ps1" -Arguments $opsArgs } @@ -201,6 +207,9 @@ if ($failedPreflight.Count -eq 0) { if ($AllowDirtyWorktree) { $capacityArgs += "-AllowDirtyWorktree" } + if ($SkipWorktreeCheck) { + $capacityArgs += "-SkipWorktreeCheck" + } Invoke-WatchStep -Key "capacity_governance" -Label "capacity governance dry run" -ScriptName "run-capacity-governance.ps1" -Arguments $capacityArgs } @@ -262,6 +271,7 @@ $summary = [ordered]@{ include_backup_drill = [bool]$IncludeBackupDrill skip_ops_check = [bool]$SkipOpsCheck skip_capacity_governance = [bool]$SkipCapacityGovernance + skip_worktree_check = [bool]$SkipWorktreeCheck fail_on_warn = [bool]$FailOnWarn } thresholds = [ordered]@{