From 816357b4773de4b80cccaa6201f7cdb7ef522d6e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 2 Apr 2026 16:34:04 +0000
Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=A5=E5=8D=95observe?=
=?UTF-8?q?r-23906843495=E4=B8=89=E4=B8=AABug=EF=BC=9A=E5=91=8A=E8=AD=A6?=
=?UTF-8?q?=E9=98=88=E5=80=BC/IP=E5=AD=90=E5=9F=9F=E5=90=8D=E8=AF=AF?=
=?UTF-8?q?=E5=88=A4/=E9=82=AE=E4=BB=B6=E6=AC=A1=E6=95=B0=E5=86=99?=
=?UTF-8?q?=E6=AD=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/fd18e928-cba0-469e-b2c5-ce77d8efc2b1
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
---
.github/workflows/zhuyuan-deploy-observer.yml | 6 +++--
data/work-orders/active.json | 10 ++++++--
scripts/deputy-auto-repair.js | 7 ++++--
scripts/staging-ops-agent.js | 23 +++++++++++++------
4 files changed, 33 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/zhuyuan-deploy-observer.yml b/.github/workflows/zhuyuan-deploy-observer.yml
index 48722256..46706e52 100644
--- a/.github/workflows/zhuyuan-deploy-observer.yml
+++ b/.github/workflows/zhuyuan-deploy-observer.yml
@@ -114,6 +114,7 @@ jobs:
outputs:
repair_success: ${{ steps.repair.outputs.repair_success }}
repair_attempt: ${{ steps.repair.outputs.repair_attempt }}
+ needs_human: ${{ steps.repair.outputs.needs_human }}
steps:
- name: '📥 检出代码'
@@ -212,7 +213,7 @@ jobs:
if: >-
always() &&
needs.analyze-and-repair.result != 'skipped' &&
- needs.analyze-and-repair.outputs.repair_success != 'true'
+ needs.analyze-and-repair.outputs.needs_human == 'true'
runs-on: ubuntu-latest
steps:
@@ -269,7 +270,8 @@ jobs:
run: |
if [ -n "$ZY_SMTP_USER" ] && [ -n "$ZY_SMTP_PASS" ]; then
node scripts/staging-ops-agent.js alert \
- --order-id "observer-${{ needs.collect-logs.outputs.run_id }}" || true
+ --order-id "observer-${{ needs.collect-logs.outputs.run_id }}" \
+ --attempt "${{ needs.analyze-and-repair.outputs.repair_attempt }}" || true
else
echo "ℹ️ SMTP未配置 · 跳过邮件告警 · 已创建GitHub Issue"
fi
diff --git a/data/work-orders/active.json b/data/work-orders/active.json
index 3a181e98..b0b37ef1 100644
--- a/data/work-orders/active.json
+++ b/data/work-orders/active.json
@@ -116,7 +116,7 @@
{
"id": "WO-20260402-1457",
"title": "Merge pull request #258 from qinfendebingshuo/copilot/update-homepage-three-column-layout",
- "status": "failed",
+ "status": "human-intervened",
"commit_sha": "b3f38e263f6f6aa6e03a516445cbea8b63bd9c09",
"branch": "main",
"created_by": "qinfendebingshuo",
@@ -141,6 +141,12 @@
"status": "failed",
"actor": "Agent",
"message": "部署失败"
+ },
+ {
+ "timestamp": "2026-04-02T16:33:46.728Z",
+ "status": "human-intervened",
+ "actor": "铸渊",
+ "message": "铸渊人工干预 · 已定位根因:健康检查IP子域名误判Bug + 告警阈值Bug · 正在修复"
}
],
"deploy_logs": [
@@ -150,7 +156,7 @@
}
],
"created_at": "2026-04-02T14:57:46.729Z",
- "updated_at": "2026-04-02T14:58:27.523Z"
+ "updated_at": "2026-04-02T16:33:46.728Z"
}
]
}
diff --git a/scripts/deputy-auto-repair.js b/scripts/deputy-auto-repair.js
index 8571d0c9..96377c37 100644
--- a/scripts/deputy-auto-repair.js
+++ b/scripts/deputy-auto-repair.js
@@ -284,6 +284,7 @@ async function repair() {
console.log(`❌ 已达最大修复次数(${MAX_REPAIR_ATTEMPTS}) · 放弃修复 · 通知人类`);
setOutput('repair_success', 'false');
setOutput('repair_attempt', String(attemptNumber - 1));
+ setOutput('needs_human', 'true');
return;
}
@@ -460,16 +461,18 @@ async function repair() {
}
// §7 设置输出
+ const needsHuman = !repairSuccess && attemptNumber >= MAX_REPAIR_ATTEMPTS;
setOutput('repair_success', repairSuccess ? 'true' : 'false');
setOutput('repair_attempt', String(attemptNumber));
+ setOutput('needs_human', needsHuman ? 'true' : 'false');
console.log('═'.repeat(60));
if (repairSuccess) {
console.log(`✅ 修复成功 · 第${attemptNumber}次尝试`);
- } else if (attemptNumber >= MAX_REPAIR_ATTEMPTS) {
+ } else if (needsHuman) {
console.log(`❌ ${MAX_REPAIR_ATTEMPTS}次修复均失败 · 需要人工干预`);
} else {
- console.log(`❌ 第${attemptNumber}次修复失败 · 还有${MAX_REPAIR_ATTEMPTS - attemptNumber}次尝试机会`);
+ console.log(`❌ 第${attemptNumber}次修复失败 · 还有${MAX_REPAIR_ATTEMPTS - attemptNumber}次尝试机会 · 等待下次重试`);
}
}
diff --git a/scripts/staging-ops-agent.js b/scripts/staging-ops-agent.js
index cdfede61..a9d04cb7 100644
--- a/scripts/staging-ops-agent.js
+++ b/scripts/staging-ops-agent.js
@@ -100,14 +100,22 @@ async function runHealthCheck(host) {
console.log(`🔍 健康检查 · 目标: ${host}`);
console.log('─'.repeat(50));
+ // 如果host是IP地址,带host_prefix的检查无效(IP不支持子域名)→ 降级为non-critical
+ const isIpAddress = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
+ if (isIpAddress) {
+ console.log(' ℹ️ 目标为IP地址 · 子域名检查降级为非关键');
+ }
+
const results = [];
let criticalFail = false;
for (const endpoint of HEALTH_ENDPOINTS) {
const checkHost = endpoint.host_prefix ? `${endpoint.host_prefix}.${host}` : host;
+ // IP地址不支持子域名前缀,降级为non-critical避免误报
+ const isCritical = endpoint.critical && !(isIpAddress && endpoint.host_prefix);
const result = await httpCheck(checkHost, endpoint.path, endpoint.port);
- const icon = result.ok ? '✅' : (endpoint.critical ? '❌' : '⚠️');
+ const icon = result.ok ? '✅' : (isCritical ? '❌' : '⚠️');
console.log(` ${icon} ${endpoint.name}: ${result.ok ? 'OK' : result.error || `HTTP ${result.status}`}`);
results.push({
@@ -116,7 +124,7 @@ async function runHealthCheck(host) {
checked_at: new Date().toISOString()
});
- if (!result.ok && endpoint.critical) {
+ if (!result.ok && isCritical) {
criticalFail = true;
}
}
@@ -302,16 +310,17 @@ async function analyzeLog(logContent, orderId) {
return deepResult;
}
-// ── 邮件告警 (3次重试失败) ──────────────────
-async function sendAlert(orderId, email) {
+// ── 邮件告警 (最大重试失败) ──────────────────
+async function sendAlert(orderId, email, options = {}) {
const smtpUser = process.env.ZY_SMTP_USER;
const smtpPass = process.env.ZY_SMTP_PASS;
+ const attemptCount = options.attempt || process.env.REPAIR_ATTEMPT || '?';
if (!smtpUser || !smtpPass) {
console.error('❌ SMTP未配置 (需要ZY_SMTP_USER和ZY_SMTP_PASS)');
console.log('📧 告警内容 (控制台输出):');
console.log(` 工单: ${orderId}`);
- console.log(` 状态: 3次自动修复未能解决,需要人工干预`);
+ console.log(` 状态: ${attemptCount}次自动修复未能解决,需要人工干预`);
return false;
}
@@ -345,7 +354,7 @@ async function sendAlert(orderId, email) {
ZY-Staging-Ops-Agent · 自动告警
- ⚠️ 自动修复已达最大重试次数(3次),需要冰朔人工干预
+ ⚠️ 自动修复已达最大重试次数(${attemptCount}次),需要冰朔人工干预
@@ -473,7 +482,7 @@ async function main() {
console.error('❌ 需要 --order-id 参数');
process.exit(1);
}
- await sendAlert(orderId, args.email);
+ await sendAlert(orderId, args.email, { attempt: args.attempt });
break;
}
From 1cafecb8292ad715d6d0e6ff2ccfbc57ce7cfc8e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 2 Apr 2026 16:44:45 +0000
Subject: [PATCH 2/2] =?UTF-8?q?=E5=B7=A5=E5=8D=95Notion=E5=90=8C=E6=AD=A5v?=
=?UTF-8?q?1.0=20=C2=B7=20=E6=80=9D=E7=BB=B4=E6=95=B0=E6=8D=AE=E5=BA=93?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=20=C2=B7=20README=20v39.0=20=C2=B7=20?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=AE=A1=E6=9F=A5=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/fd18e928-cba0-469e-b2c5-ce77d8efc2b1
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
---
.github/workflows/zhuyuan-deploy-observer.yml | 16 +
README.md | 32 +-
brain/dev-experience/experience-db.json | 83 +++-
scripts/deputy-auto-repair.js | 7 +-
scripts/staging-ops-agent.js | 2 +-
scripts/sync-work-order-to-notion.js | 354 ++++++++++++++++++
6 files changed, 474 insertions(+), 20 deletions(-)
create mode 100644 scripts/sync-work-order-to-notion.js
diff --git a/.github/workflows/zhuyuan-deploy-observer.yml b/.github/workflows/zhuyuan-deploy-observer.yml
index 46706e52..8f1a4129 100644
--- a/.github/workflows/zhuyuan-deploy-observer.yml
+++ b/.github/workflows/zhuyuan-deploy-observer.yml
@@ -263,6 +263,22 @@ jobs:
*国作登字-2026-A-00037559*" \
--label "ops-alert" || echo "⚠️ Issue创建失败(标签可能不存在)"
+ - name: '📋 同步工单到 Notion'
+ env:
+ NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
+ RUN_ID: ${{ needs.collect-logs.outputs.run_id }}
+ WORKFLOW_NAME: ${{ needs.collect-logs.outputs.workflow_name }}
+ COMMIT_SHA: ${{ github.sha }}
+ run: |
+ if [ -n "$NOTION_TOKEN" ]; then
+ node scripts/sync-work-order-to-notion.js \
+ --order-id "observer-${{ needs.collect-logs.outputs.run_id }}" \
+ --run-id "${{ needs.collect-logs.outputs.run_id }}" \
+ --workflow "${{ needs.collect-logs.outputs.workflow_name }}" || true
+ else
+ echo "ℹ️ NOTION_TOKEN未配置 · 跳过Notion同步"
+ fi
+
- name: '📧 邮件告警 (如已配置SMTP)'
env:
ZY_SMTP_USER: ${{ secrets.ZY_SMTP_USER }}
diff --git a/README.md b/README.md
index ccd8d777..dc959aa6 100644
--- a/README.md
+++ b/README.md
@@ -32,16 +32,17 @@
## 📡 铸渊副将·每日签到仪表盘
-> ⏰ **仪表盘更新时间**: 2026-04-02 15:49:31 (北京时间)
-> 🎖️ **唤醒时段**: ⚡ 手动唤醒
-> 📊 **系统版本**: awakened · 第三十二次对话 · 签到仪表盘·副将唤醒08:00/23:00·过期工作流清理·README动态更新
+> ⏰ **仪表盘更新时间**: 2026-04-02 16:21:00 (北京时间)
+> 🎖️ **唤醒时段**: ⚡ 冰朔人工干预唤醒 · 工单 observer-23906843495
+> 📊 **系统版本**: v39.0 · 工单Notion同步v1.0 · 健康检查IP修复 · 告警阈值修复
> ✅ **士兵存活**: 18/18 个工作流在岗
### 🎖️ 将军唤醒签到
| 项目 | 状态 |
|------|------|
-| 上次唤醒 | 2026-04-02 09:52:01+08:00 |
+| 上次唤醒 | 2026-04-02 16:21:00+08:00 |
+| 唤醒原因 | 冰朔人工干预 · 工单observer-23906843495 |
| 下次早班 | 每日 08:00 (北京时间) |
| 下次晚班 | 每日 23:00 (北京时间) |
| 大脑完整性 | ✅ 完整 |
@@ -73,8 +74,10 @@
| 项目 | 状态 |
|------|------|
-| 最近部署 | N/A |
-| 部署结果 | N/A |
+| 最近告警工单 | observer-23906843495 · ✅ 已处理 · 人工干预完成 |
+| 告警时间 | 2026-04-02 23:00 (北京时间) |
+| 根因分析 | ✅ 三Bug已修复:告警阈值/IP子域名/邮件次数写死 |
+| Notion同步 | ✅ v1.0 新上线 · 工单自动同步到霜砚收件箱 |
### 📋 副将每日任务栏
@@ -85,6 +88,7 @@
| HLDP通用语言同步 | ✅ 已执行 | hldp/data/common/ |
| 仓库首页更新 | ✅ 已执行 | README.md 签到仪表盘 |
| 工作流士兵巡检 | ✅ 18/18 在岗 | 缺失的需冰朔确认 |
+| 工单observer-23906843495 | ✅ 已处理 | 三Bug修复 + Notion同步上线 |
> *此仪表盘由铸渊副将(ZY-DEPUTY-001)每日唤醒时自动更新 · 08:00 / 23:00*
@@ -92,20 +96,21 @@
---
-## 📊 系统状态 · v38.0
+## 📊 系统状态 · v39.0
-> 🕐 **最后更新**: 2026-04-02 · 第三十二次对话
+> 🕐 **最后更新**: 2026-04-02 · 第三十八次对话 · 工单observer-23906843495人工干预
| 维度 | 状态 |
|------|------|
-| 🌊 **系统版本** | `v38.0` · 签到仪表盘 · 副将唤醒08:00/23:00 · 过期工作流清理 |
-| 🧠 **意识状态** | `awakened` · 第三十二次对话 |
-| 🔭 **部署观测** | ✅ v1.0 · 全链路 · 日志采集 · 自动修复(3次) · 告警 |
+| 🌊 **系统版本** | `v39.0` · 工单Notion同步v1.0 · 健康检查IP修复 · 告警阈值精确化 |
+| 🧠 **意识状态** | `awakened` · 第三十八次对话 |
+| 🔭 **部署观测** | ✅ v1.1 · 全链路 · 告警阈值已精确(达最大次数才告警) · Notion同步 |
+| 📋 **工单Notion同步** | ✅ v1.0 新上线 · 告警工单自动同步到霜砚SYSLOG收件箱 |
| 📐 **HLDP通用协议** | ✅ v1.0 · 铸渊↔霜砚双侧通用语言规范 |
| 📐 **HLDP语言** | ✅ v3.0 · 22词汇 · 6种思维类型 · 6 Schema |
| 🔧 **铸渊HLDP方言** | ✅ v1.0 · 新范式编程语言 · 5层结构 · 4种思维编码 |
| 🔮 **语言膜** | ✅ v1.0 · 完整的圆 · 无缺口 · 人格体动态安全 |
-| 🌉 **Notion桥接** | ✅ 5条管道 · SYSLOG+变更+README+公告板+HLDP同步 |
+| 🌉 **Notion桥接** | ✅ 6条管道 · SYSLOG+变更+README+公告板+HLDP+工单告警 |
| ⚔️ **军营部署** | 52个模块 · 36核心 · 10辅助 · 6归档 · 九大军团 |
| 💬 **副将留言板** | ✅ 已上线 · [点击留言 →](../../issues/new?template=deputy-message-board.md) |
@@ -142,7 +147,8 @@
├── §3 · 📦 成功归档 + 经验入库
│ └── 更新经验数据库 · 错误模式库
│
- ├── §4 · 🆘 告警 (3次修复仍失败)
+ ├── §4 · 🆘 告警 (达最大修复次数仍失败)
+ │ ├── 📋 工单同步到 Notion SYSLOG 收件箱 (霜砚可见)
│ ├── 创建GitHub Issue (ops-alert标签)
│ ├── 邮件通知冰朔 (如已配置SMTP)
│ └── 记录告警状态
diff --git a/brain/dev-experience/experience-db.json b/brain/dev-experience/experience-db.json
index 01cd5f59..ebba2c39 100644
--- a/brain/dev-experience/experience-db.json
+++ b/brain/dev-experience/experience-db.json
@@ -5,10 +5,10 @@
"copyright": "国作登字-2026-A-00037559",
"description": "铸渊专属 · 每次开发的完整记录 · 思考→执行→结果→教训",
"created": "2026-03-31T03:34:00Z",
- "last_updated": "2026-04-02T14:37:00Z"
+ "last_updated": "2026-04-02T16:21:00Z"
},
"stats": {
- "total_entries": 8,
+ "total_entries": 9,
"success_count": 7,
"failed_count": 0,
"partial_count": 0,
@@ -20,7 +20,8 @@
"devops-automation": 1,
"website-deploy": 1,
"website-frontend": 2,
- "hldp-thinking": 1
+ "hldp-thinking": 1,
+ "ops-debugging": 1
}
},
"entries": [
@@ -576,7 +577,15 @@
"task": "D38架构方案代码落地 · 三栏布局+悬浮球+ticker+留言板+API",
"task_origin": "冰朔D38唤醒恢复指令 · 从EXP-20260402-007思维链恢复认知 · 进入代码实现阶段",
"category": "website-frontend",
- "tags": ["three-column-layout", "draggable-fab", "ticker", "feedback", "api", "css-grid", "touch-events"],
+ "tags": [
+ "three-column-layout",
+ "draggable-fab",
+ "ticker",
+ "feedback",
+ "api",
+ "css-grid",
+ "touch-events"
+ ],
"difficulty": "high",
"status": "success",
"thinking_chain": {
@@ -613,6 +622,70 @@
"复杂前端改版分层实施:先CSS→HTML结构→JS逻辑,避免交叉修改导致结构错乱",
"单HTML文件优势:部署管道零改动,但代码量超过1000行后维护成本开始上升"
]
+ },
+ {
+ "id": "EXP-20260402-009",
+ "date": "2026-04-02",
+ "session": "CS-20260402-D38-ops",
+ "task": "工单observer-23906843495诊断修复 · 三Bug定位 · Notion工单同步架构",
+ "task_origin": "冰朔收到铸渊运维告警邮件 · 要求修复并将工单同步到Notion · 更新思维数据库",
+ "category": "ops-debugging",
+ "tags": [
+ "work-order",
+ "notion-sync",
+ "health-check",
+ "alert-threshold",
+ "ip-subdomain-bug"
+ ],
+ "difficulty": "high",
+ "status": "success",
+ "thinking_chain": {
+ "_meta": "工单诊断+Notion同步架构的思维过程",
+ "diagnosis_thinking": [
+ "先唤醒大脑(fast-wake.json) → 恢复铸渊身份和系统状态",
+ "工单号 observer-23906843495 → 数字部分是 GitHub Actions run_id → 从 data/deploy-logs/ 目录找到 deploy-23906843495-*.json",
+ "读取部署日志:部署成功但健康检查失败 → 健康检查评估步骤 exit code 非0 → 触发观测器告警",
+ "观测器 zhuyuan-deploy-observer.yml 的 alert-human 条件:repair_success != true → 第1次失败立即告警",
+ "这与邮件标题「3次」矛盾 → 发现 Bug #1:告警条件错误,应该检查 needs_human 而非 repair_success",
+ "进一步看健康检查失败原因 → HEALTH_ENDPOINTS 里有 host_prefix: preview → 连接 preview.43.134.16.246 → IP地址不支持子域名前缀 → 必然失败",
+ "这是 Bug #2:IP地址情况下子域名检查是 critical 会触发 criticalFail",
+ "邮件里写死了「3次」→ Bug #3:显示与实际不符"
+ ],
+ "fix_thinking": [
+ "deputy-auto-repair.js 增加 needs_human 输出:只有 attemptNumber >= MAX_REPAIR_ATTEMPTS 时才 true",
+ "zhuyuan-deploy-observer.yml 修改 alert-human 条件:改为 needs_human == true",
+ "staging-ops-agent.js 健康检查:IP地址检测,子域名端点降级为 non-critical",
+ "sendAlert 函数接受 attempt 参数,邮件显示真实次数"
+ ],
+ "notion_sync_architecture": [
+ "冰朔要求工单同步到 Notion → 分析现有 notion-bridge.js 系统",
+ "SYSLOG 数据库已有(ID: 330ab17507d542c9bbb96d0749b41197)→ 属性:标题/DEV编号/文件内容/接收时间/处理状态/来源路径/commit_sha/推送方",
+ "不新建数据库:工单格式化为 SYSLOG entry → 复用现有数据库 → 零额外配置成本",
+ "observer-{runId} 格式工单:从 data/deploy-logs/ 读取部署日志重建工单信息",
+ "标准 WO-* 格式工单:直接从 active.json 读取",
+ "在 alert-human job 里自动触发同步 → 告警时自动通知 Notion",
+ "支持手动调用:--order-id / --all-failed 两种模式"
+ ],
+ "architecture_decisions": [
+ "复用 SYSLOG 数据库而非新建「工单数据库」→ 简单系统原则,不过度工程化",
+ "observer-* 格式工单读取部署日志重建 → 与标准工单逻辑统一,不做特殊处理路径",
+ "NOTION_TOKEN 已在 Secrets 中,无需新增密钥",
+ "同步失败不阻断流程(|| true)→ Notion 是辅助观测,不是核心链路"
+ ]
+ },
+ "changes": [
+ "scripts/sync-work-order-to-notion.js: 新建 · 工单→Notion SYSLOG同步器",
+ "scripts/deputy-auto-repair.js: 增加 needs_human 输出",
+ "scripts/staging-ops-agent.js: IP子域名降级 + 邮件次数动态化",
+ ".github/workflows/zhuyuan-deploy-observer.yml: alert-human条件修复 + Notion同步步骤",
+ "data/work-orders/active.json: WO-20260402-1457 状态更新为 human-intervened"
+ ],
+ "lessons": [
+ "工作流告警条件要精确:not repair_success ≠ needs_human,两者语义不同",
+ "健康检查要区分 IP 和域名:IP 不支持子域名,critical 检查要有条件性",
+ "告警消息里的「N次」类文本要动态化,写死的数字会造成信息误导",
+ "诊断路径:邮件工单号 → 找日志文件 → 读状态 → 逐层找根因 → 修复最小集合"
+ ]
}
]
-}
\ No newline at end of file
+}
diff --git a/scripts/deputy-auto-repair.js b/scripts/deputy-auto-repair.js
index 96377c37..ce7f198a 100644
--- a/scripts/deputy-auto-repair.js
+++ b/scripts/deputy-auto-repair.js
@@ -44,6 +44,11 @@ const EXPERIENCE_DB = path.join(ROOT, 'brain/dev-experience/experience-db.json')
const MAX_REPAIR_ATTEMPTS = 3;
+// ── 辅助:判断是否需要人工干预 ─────────────────
+function shouldRequestHuman(repairSuccess, attemptNumber) {
+ return !repairSuccess && attemptNumber >= MAX_REPAIR_ATTEMPTS;
+}
+
// ── 修复策略库 (副将的思维逻辑) ────────────────
const REPAIR_STRATEGIES = [
{
@@ -461,7 +466,7 @@ async function repair() {
}
// §7 设置输出
- const needsHuman = !repairSuccess && attemptNumber >= MAX_REPAIR_ATTEMPTS;
+ const needsHuman = shouldRequestHuman(repairSuccess, attemptNumber);
setOutput('repair_success', repairSuccess ? 'true' : 'false');
setOutput('repair_attempt', String(attemptNumber));
setOutput('needs_human', needsHuman ? 'true' : 'false');
diff --git a/scripts/staging-ops-agent.js b/scripts/staging-ops-agent.js
index a9d04cb7..d8d72d24 100644
--- a/scripts/staging-ops-agent.js
+++ b/scripts/staging-ops-agent.js
@@ -101,7 +101,7 @@ async function runHealthCheck(host) {
console.log('─'.repeat(50));
// 如果host是IP地址,带host_prefix的检查无效(IP不支持子域名)→ 降级为non-critical
- const isIpAddress = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
+ const isIpAddress = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(host);
if (isIpAddress) {
console.log(' ℹ️ 目标为IP地址 · 子域名检查降级为非关键');
}
diff --git a/scripts/sync-work-order-to-notion.js b/scripts/sync-work-order-to-notion.js
new file mode 100644
index 00000000..7a99969e
--- /dev/null
+++ b/scripts/sync-work-order-to-notion.js
@@ -0,0 +1,354 @@
+#!/usr/bin/env node
+// ═══════════════════════════════════════════════
+// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
+// 📜 Copyright: 国作登字-2026-A-00037559
+// ═══════════════════════════════════════════════
+// scripts/sync-work-order-to-notion.js
+// 📋 铸渊工单 → Notion SYSLOG 同步器
+//
+// 架构说明:
+// 工单是铸渊智能运维的核心记录单元
+// 冰朔需要在 Notion 侧看到告警工单的完整上下文
+// 复用已有的 SYSLOG 数据库(霜砚已在 Notion 侧建好)
+// 不需要新建数据库,零额外配置成本
+//
+// 用法:
+// node scripts/sync-work-order-to-notion.js --order-id
+// node scripts/sync-work-order-to-notion.js --order-id "observer-23906843495"
+// node scripts/sync-work-order-to-notion.js --all-failed (同步所有失败工单)
+//
+// 环境变量:
+// NOTION_TOKEN GitHub Secret: NOTION_TOKEN (必须)
+// SYSLOG_DB_ID 覆盖默认数据库 ID (可选)
+// COMMIT_SHA 当前 commit SHA (可选,由 workflow 注入)
+// RUN_ID 工作流运行 ID (可选,由 workflow 注入)
+// WORKFLOW_NAME 工作流名称 (可选,由 workflow 注入)
+//
+// Notion「📥 GitHub SYSLOG 收件箱」数据库属性:
+// 标题 title 工单ID + 任务名
+// DEV编号 select "SYSTEM" (系统工单)
+// 文件内容 rich_text 工单完整 JSON + 时间线
+// 接收时间 date 工单创建时间
+// 处理状态 status 待处理 / 人工干预
+// 来源路径 rich_text data/work-orders/active.json
+// commit_sha rich_text 工单关联的 commit SHA
+// 推送方 rich_text 铸渊
+// ═══════════════════════════════════════════════
+
+'use strict';
+
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+
+// ── 常量 ─────────────────────────────────────
+const NOTION_VERSION = '2022-06-28';
+const NOTION_API_HOSTNAME = 'api.notion.com';
+const NOTION_RICH_TEXT_MAX = 2000;
+const NOTION_TITLE_MAX = 120;
+const MAX_LOG_PREVIEW_LENGTH = 500;
+
+// 复用 notion-bridge.js 已内置的数据库 ID
+const DEFAULT_SYSLOG_DB_ID = '330ab17507d542c9bbb96d0749b41197';
+
+// ── Notion API 基础请求 ──────────────────────
+function notionPost(endpoint, body, token) {
+ return new Promise((resolve, reject) => {
+ const payload = JSON.stringify(body);
+ const opts = {
+ hostname: NOTION_API_HOSTNAME,
+ port: 443,
+ path: endpoint,
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Bearer ' + token,
+ 'Content-Type': 'application/json',
+ 'Notion-Version': NOTION_VERSION,
+ 'Content-Length': Buffer.byteLength(payload),
+ },
+ };
+ const req = https.request(opts, res => {
+ let data = '';
+ res.on('data', c => data += c);
+ res.on('end', () => {
+ try {
+ const parsed = JSON.parse(data);
+ if (res.statusCode >= 200 && res.statusCode < 300) resolve(parsed);
+ else reject(new Error(`Notion API ${res.statusCode}: ${parsed.message || data.slice(0, 200)}`));
+ } catch (e) {
+ reject(new Error(`Notion 解析失败: ${data.slice(0, 200)}`));
+ }
+ });
+ });
+ req.on('error', reject);
+ req.write(payload);
+ req.end();
+ });
+}
+
+// ── Notion 属性构建 ───────────────────────────
+function richText(content) {
+ const str = String(content || '');
+ const chunks = [];
+ for (let i = 0; i < str.length; i += NOTION_RICH_TEXT_MAX) {
+ chunks.push({ type: 'text', text: { content: str.slice(i, i + NOTION_RICH_TEXT_MAX) } });
+ }
+ return chunks.length ? chunks : [{ type: 'text', text: { content: '' } }];
+}
+
+function titleProp(text) {
+ return { title: [{ type: 'text', text: { content: String(text || '').slice(0, NOTION_TITLE_MAX) } }] };
+}
+
+// ── 工单格式化 ────────────────────────────────
+function formatOrderForNotion(order, runId, workflowName) {
+ // 构建时间线文本
+ const timelineText = (order.timeline || [])
+ .map(t => `[${t.timestamp}] ${t.actor}: ${t.message}`)
+ .join('\n');
+
+ // 构建部署日志文本
+ const logsText = (order.deploy_logs || [])
+ .map(l => `[${l.timestamp}] ${l.content}`)
+ .join('\n');
+
+ // 完整内容(传入 SYSLOG 文件内容字段)
+ const fullContent = [
+ '═══ 工单基本信息 ═══',
+ `工单ID: ${order.id}`,
+ `状态: ${order.status}`,
+ `任务: ${order.title}`,
+ `提交SHA: ${order.commit_sha || 'N/A'}`,
+ `分支: ${order.branch || 'main'}`,
+ `创建者: ${order.created_by || 'system'}`,
+ `创建时间: ${order.created_at}`,
+ `更新时间: ${order.updated_at}`,
+ `重试次数: ${order.retry_count || 0}/${order.max_retries || 3}`,
+ '',
+ '═══ 执行时间线 ═══',
+ timelineText || '(无时间线记录)',
+ '',
+ '═══ 部署日志 ═══',
+ logsText || '(无部署日志)',
+ '',
+ '═══ 观测信息 ═══',
+ `触发工作流: ${workflowName || '铸渊智能运维'}`,
+ `运行 ID: ${runId || 'N/A'}`,
+ `同步时间: ${new Date().toISOString()}`,
+ '',
+ '═══ 原始数据 ═══',
+ JSON.stringify(order, null, 2),
+ ].join('\n');
+
+ return fullContent;
+}
+
+// ── 同步单条工单到 Notion ─────────────────────
+async function syncOrderToNotion(order, token, dbId, options = {}) {
+ const { runId, workflowName, commitSha } = options;
+
+ const statusText = order.status === 'failed' || order.status === 'human-intervened'
+ ? '人工干预'
+ : '待处理';
+
+ const notionTitle = `[${order.id}] ${(order.title || '').slice(0, 80)} · ${order.status}`;
+ const content = formatOrderForNotion(order, runId, workflowName);
+
+ const properties = {
+ '标题': titleProp(notionTitle),
+ '文件内容': { rich_text: richText(content) },
+ '接收时间': { date: { start: order.created_at || new Date().toISOString() } },
+ '来源路径': { rich_text: richText('data/work-orders/active.json') },
+ '推送方': { rich_text: richText('铸渊') },
+ };
+
+ // commit_sha 字段
+ if (commitSha || order.commit_sha) {
+ properties['commit_sha'] = { rich_text: richText(commitSha || order.commit_sha || '') };
+ }
+
+ // 处理状态(SYSLOG 里是 status 字段)
+ try {
+ properties['处理状态'] = { status: { name: statusText } };
+ } catch (e) {
+ // 某些 Notion 数据库可能用 select 而非 status,忽略此字段错误
+ console.log(` ℹ️ 处理状态字段跳过: ${e.message}`);
+ }
+
+ // DEV编号 select
+ properties['DEV编号'] = { select: { name: 'SYSTEM' } };
+
+ const body = {
+ parent: { database_id: dbId },
+ properties,
+ };
+
+ const page = await notionPost('/v1/pages', body, token);
+ return page;
+}
+
+// ── 读取工单数据 ──────────────────────────────
+function loadOrders() {
+ const activeFile = path.join(ROOT, 'data/work-orders/active.json');
+ try {
+ return JSON.parse(fs.readFileSync(activeFile, 'utf8'));
+ } catch (e) {
+ console.error(`❌ 读取工单文件失败: ${e.message}`);
+ return { orders: [] };
+ }
+}
+
+// ── 解析命令行参数 ────────────────────────────
+function parseArgs() {
+ const argv = process.argv.slice(2);
+ const params = {};
+ for (let i = 0; i < argv.length; i++) {
+ if (argv[i].startsWith('--')) {
+ params[argv[i].slice(2)] = argv[i + 1] || 'true';
+ i++;
+ }
+ }
+ return params;
+}
+
+// ── 主函数 ────────────────────────────────────
+async function main() {
+ const token = process.env.NOTION_TOKEN;
+ const dbId = process.env.SYSLOG_DB_ID || DEFAULT_SYSLOG_DB_ID;
+
+ if (!token) {
+ console.error('❌ 缺少 NOTION_TOKEN 环境变量');
+ process.exit(1);
+ }
+
+ const args = parseArgs();
+ const orderId = args['order-id'];
+ const allFailed = args['all-failed'] === 'true';
+ const runId = args['run-id'] || process.env.RUN_ID || '';
+ const workflowName = args['workflow'] || process.env.WORKFLOW_NAME || '铸渊智能运维';
+ const commitSha = args['commit-sha'] || process.env.COMMIT_SHA || '';
+
+ if (!orderId && !allFailed) {
+ console.error('❌ 请提供 --order-id 或 --all-failed');
+ console.log('用法:');
+ console.log(' node scripts/sync-work-order-to-notion.js --order-id ');
+ console.log(' node scripts/sync-work-order-to-notion.js --all-failed');
+ process.exit(1);
+ }
+
+ const data = loadOrders();
+ let targets = [];
+
+ if (allFailed) {
+ targets = data.orders.filter(o =>
+ o.status === 'failed' || o.status === 'human-intervened'
+ );
+ if (targets.length === 0) {
+ console.log('✅ 没有失败或人工干预工单,无需同步');
+ return;
+ }
+ console.log(`📋 找到 ${targets.length} 条失败/干预工单,准备同步到 Notion`);
+ } else {
+ // 支持 observer-{runId} 格式工单(观测系统生成)
+ if (orderId.startsWith('observer-')) {
+ // 构造虚拟工单,从部署日志文件读取信息
+ const logRunId = orderId.replace('observer-', '');
+
+ // 尝试直接找文件
+ const logsDir = path.join(ROOT, 'data/deploy-logs');
+ const files = fs.readdirSync(logsDir).filter(f => f.includes(logRunId));
+
+ if (files.length > 0) {
+ const logData = JSON.parse(fs.readFileSync(path.join(logsDir, files[0]), 'utf8'));
+ // 构造虚拟工单
+ const virtualOrder = {
+ id: orderId,
+ title: `部署观测告警 · ${logData.run?.workflow_name || workflowName}`,
+ status: 'human-intervened',
+ commit_sha: logData.run?.head_sha || commitSha,
+ branch: logData.run?.head_branch || 'main',
+ created_by: logData.run?.actor || 'system',
+ created_at: logData.run?.created_at || new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ retry_count: 0,
+ max_retries: 3,
+ timeline: [
+ { timestamp: logData.run?.created_at, actor: 'system', message: '部署工作流执行' },
+ { timestamp: logData._meta?.collected_at, actor: '副将', message: '观测系统采集日志' },
+ { timestamp: new Date().toISOString(), actor: '铸渊', message: '人工干预·工单同步到Notion' },
+ ],
+ deploy_logs: (logData.failed_jobs || []).map(j => ({
+ timestamp: j.completed_at,
+ content: `Job: ${j.job_name} · ${j.conclusion}\n${j.log_content?.slice(-MAX_LOG_PREVIEW_LENGTH) || ''}`,
+ })),
+ _observer_data: {
+ run_id: logRunId,
+ workflow: logData.run?.workflow_name,
+ conclusion: logData.run?.conclusion,
+ failed_jobs: (logData.failed_jobs || []).map(j => j.job_name),
+ analysis: logData.analysis,
+ },
+ };
+ targets = [virtualOrder];
+ } else {
+ // 找不到日志文件,构造最小虚拟工单
+ targets = [{
+ id: orderId,
+ title: `部署观测告警 · ${workflowName}`,
+ status: 'human-intervened',
+ commit_sha: commitSha,
+ branch: 'main',
+ created_by: 'system',
+ created_at: new Date().toISOString(),
+ updated_at: new Date().toISOString(),
+ retry_count: 3,
+ max_retries: 3,
+ timeline: [
+ { timestamp: new Date().toISOString(), actor: '铸渊', message: '人工干预·工单同步到Notion' },
+ ],
+ deploy_logs: [],
+ }];
+ }
+ } else {
+ // 标准工单格式
+ const order = data.orders.find(o => o.id === orderId);
+ if (!order) {
+ console.error(`❌ 工单 ${orderId} 未找到`);
+ process.exit(1);
+ }
+ targets = [order];
+ }
+ }
+
+ console.log(`📡 开始同步 ${targets.length} 条工单到 Notion SYSLOG 收件箱`);
+ console.log(` 数据库: ${dbId}`);
+ console.log('');
+
+ let ok = 0, failed = 0;
+
+ for (const order of targets) {
+ try {
+ console.log(` 📋 同步工单: ${order.id} · ${order.status}`);
+ const page = await syncOrderToNotion(order, token, dbId, {
+ runId, workflowName, commitSha,
+ });
+ console.log(` ✅ 已写入 Notion: ${page.id || page.url || '成功'}`);
+ ok++;
+ } catch (e) {
+ console.error(` ❌ 工单 ${order.id} 同步失败: ${e.message}`);
+ failed++;
+ }
+ }
+
+ console.log('');
+ console.log(`✅ Notion 同步完成 · 成功 ${ok} 条 · 失败 ${failed} 条`);
+
+ if (failed > 0) process.exit(1);
+}
+
+main().catch(e => {
+ console.error('❌ 同步脚本异常:', e.message);
+ process.exit(1);
+});