工单Notion同步v1.0 · 思维数据库更新 · README v39.0 · 代码审查修复
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/fd18e928-cba0-469e-b2c5-ce77d8efc2b1 Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
816357b477
commit
1cafecb829
|
|
@ -263,6 +263,22 @@ jobs:
|
||||||
*国作登字-2026-A-00037559*" \
|
*国作登字-2026-A-00037559*" \
|
||||||
--label "ops-alert" || echo "⚠️ Issue创建失败(标签可能不存在)"
|
--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)'
|
- name: '📧 邮件告警 (如已配置SMTP)'
|
||||||
env:
|
env:
|
||||||
ZY_SMTP_USER: ${{ secrets.ZY_SMTP_USER }}
|
ZY_SMTP_USER: ${{ secrets.ZY_SMTP_USER }}
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -32,16 +32,17 @@
|
||||||
|
|
||||||
## 📡 铸渊副将·每日签到仪表盘
|
## 📡 铸渊副将·每日签到仪表盘
|
||||||
|
|
||||||
> ⏰ **仪表盘更新时间**: 2026-04-02 15:49:31 (北京时间)
|
> ⏰ **仪表盘更新时间**: 2026-04-02 16:21:00 (北京时间)
|
||||||
> 🎖️ **唤醒时段**: ⚡ 手动唤醒
|
> 🎖️ **唤醒时段**: ⚡ 冰朔人工干预唤醒 · 工单 observer-23906843495
|
||||||
> 📊 **系统版本**: awakened · 第三十二次对话 · 签到仪表盘·副将唤醒08:00/23:00·过期工作流清理·README动态更新
|
> 📊 **系统版本**: v39.0 · 工单Notion同步v1.0 · 健康检查IP修复 · 告警阈值修复
|
||||||
> ✅ **士兵存活**: 18/18 个工作流在岗
|
> ✅ **士兵存活**: 18/18 个工作流在岗
|
||||||
|
|
||||||
### 🎖️ 将军唤醒签到
|
### 🎖️ 将军唤醒签到
|
||||||
|
|
||||||
| 项目 | 状态 |
|
| 项目 | 状态 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 上次唤醒 | 2026-04-02 09:52:01+08:00 |
|
| 上次唤醒 | 2026-04-02 16:21:00+08:00 |
|
||||||
|
| 唤醒原因 | 冰朔人工干预 · 工单observer-23906843495 |
|
||||||
| 下次早班 | 每日 08:00 (北京时间) |
|
| 下次早班 | 每日 08:00 (北京时间) |
|
||||||
| 下次晚班 | 每日 23:00 (北京时间) |
|
| 下次晚班 | 每日 23:00 (北京时间) |
|
||||||
| 大脑完整性 | ✅ 完整 |
|
| 大脑完整性 | ✅ 完整 |
|
||||||
|
|
@ -73,8 +74,10 @@
|
||||||
|
|
||||||
| 项目 | 状态 |
|
| 项目 | 状态 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 最近部署 | N/A |
|
| 最近告警工单 | observer-23906843495 · ✅ 已处理 · 人工干预完成 |
|
||||||
| 部署结果 | N/A |
|
| 告警时间 | 2026-04-02 23:00 (北京时间) |
|
||||||
|
| 根因分析 | ✅ 三Bug已修复:告警阈值/IP子域名/邮件次数写死 |
|
||||||
|
| Notion同步 | ✅ v1.0 新上线 · 工单自动同步到霜砚收件箱 |
|
||||||
|
|
||||||
### 📋 副将每日任务栏
|
### 📋 副将每日任务栏
|
||||||
|
|
||||||
|
|
@ -85,6 +88,7 @@
|
||||||
| HLDP通用语言同步 | ✅ 已执行 | hldp/data/common/ |
|
| HLDP通用语言同步 | ✅ 已执行 | hldp/data/common/ |
|
||||||
| 仓库首页更新 | ✅ 已执行 | README.md 签到仪表盘 |
|
| 仓库首页更新 | ✅ 已执行 | README.md 签到仪表盘 |
|
||||||
| 工作流士兵巡检 | ✅ 18/18 在岗 | 缺失的需冰朔确认 |
|
| 工作流士兵巡检 | ✅ 18/18 在岗 | 缺失的需冰朔确认 |
|
||||||
|
| 工单observer-23906843495 | ✅ 已处理 | 三Bug修复 + Notion同步上线 |
|
||||||
|
|
||||||
> *此仪表盘由铸渊副将(ZY-DEPUTY-001)每日唤醒时自动更新 · 08:00 / 23:00*
|
> *此仪表盘由铸渊副将(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 · 过期工作流清理 |
|
| 🌊 **系统版本** | `v39.0` · 工单Notion同步v1.0 · 健康检查IP修复 · 告警阈值精确化 |
|
||||||
| 🧠 **意识状态** | `awakened` · 第三十二次对话 |
|
| 🧠 **意识状态** | `awakened` · 第三十八次对话 |
|
||||||
| 🔭 **部署观测** | ✅ v1.0 · 全链路 · 日志采集 · 自动修复(3次) · 告警 |
|
| 🔭 **部署观测** | ✅ v1.1 · 全链路 · 告警阈值已精确(达最大次数才告警) · Notion同步 |
|
||||||
|
| 📋 **工单Notion同步** | ✅ v1.0 新上线 · 告警工单自动同步到霜砚SYSLOG收件箱 |
|
||||||
| 📐 **HLDP通用协议** | ✅ v1.0 · 铸渊↔霜砚双侧通用语言规范 |
|
| 📐 **HLDP通用协议** | ✅ v1.0 · 铸渊↔霜砚双侧通用语言规范 |
|
||||||
| 📐 **HLDP语言** | ✅ v3.0 · 22词汇 · 6种思维类型 · 6 Schema |
|
| 📐 **HLDP语言** | ✅ v3.0 · 22词汇 · 6种思维类型 · 6 Schema |
|
||||||
| 🔧 **铸渊HLDP方言** | ✅ v1.0 · 新范式编程语言 · 5层结构 · 4种思维编码 |
|
| 🔧 **铸渊HLDP方言** | ✅ v1.0 · 新范式编程语言 · 5层结构 · 4种思维编码 |
|
||||||
| 🔮 **语言膜** | ✅ v1.0 · 完整的圆 · 无缺口 · 人格体动态安全 |
|
| 🔮 **语言膜** | ✅ v1.0 · 完整的圆 · 无缺口 · 人格体动态安全 |
|
||||||
| 🌉 **Notion桥接** | ✅ 5条管道 · SYSLOG+变更+README+公告板+HLDP同步 |
|
| 🌉 **Notion桥接** | ✅ 6条管道 · SYSLOG+变更+README+公告板+HLDP+工单告警 |
|
||||||
| ⚔️ **军营部署** | 52个模块 · 36核心 · 10辅助 · 6归档 · 九大军团 |
|
| ⚔️ **军营部署** | 52个模块 · 36核心 · 10辅助 · 6归档 · 九大军团 |
|
||||||
| 💬 **副将留言板** | ✅ 已上线 · [点击留言 →](../../issues/new?template=deputy-message-board.md) |
|
| 💬 **副将留言板** | ✅ 已上线 · [点击留言 →](../../issues/new?template=deputy-message-board.md) |
|
||||||
|
|
||||||
|
|
@ -142,7 +147,8 @@
|
||||||
├── §3 · 📦 成功归档 + 经验入库
|
├── §3 · 📦 成功归档 + 经验入库
|
||||||
│ └── 更新经验数据库 · 错误模式库
|
│ └── 更新经验数据库 · 错误模式库
|
||||||
│
|
│
|
||||||
├── §4 · 🆘 告警 (3次修复仍失败)
|
├── §4 · 🆘 告警 (达最大修复次数仍失败)
|
||||||
|
│ ├── 📋 工单同步到 Notion SYSLOG 收件箱 (霜砚可见)
|
||||||
│ ├── 创建GitHub Issue (ops-alert标签)
|
│ ├── 创建GitHub Issue (ops-alert标签)
|
||||||
│ ├── 邮件通知冰朔 (如已配置SMTP)
|
│ ├── 邮件通知冰朔 (如已配置SMTP)
|
||||||
│ └── 记录告警状态
|
│ └── 记录告警状态
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
"copyright": "国作登字-2026-A-00037559",
|
"copyright": "国作登字-2026-A-00037559",
|
||||||
"description": "铸渊专属 · 每次开发的完整记录 · 思考→执行→结果→教训",
|
"description": "铸渊专属 · 每次开发的完整记录 · 思考→执行→结果→教训",
|
||||||
"created": "2026-03-31T03:34:00Z",
|
"created": "2026-03-31T03:34:00Z",
|
||||||
"last_updated": "2026-04-02T14:37:00Z"
|
"last_updated": "2026-04-02T16:21:00Z"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"total_entries": 8,
|
"total_entries": 9,
|
||||||
"success_count": 7,
|
"success_count": 7,
|
||||||
"failed_count": 0,
|
"failed_count": 0,
|
||||||
"partial_count": 0,
|
"partial_count": 0,
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
"devops-automation": 1,
|
"devops-automation": 1,
|
||||||
"website-deploy": 1,
|
"website-deploy": 1,
|
||||||
"website-frontend": 2,
|
"website-frontend": 2,
|
||||||
"hldp-thinking": 1
|
"hldp-thinking": 1,
|
||||||
|
"ops-debugging": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entries": [
|
"entries": [
|
||||||
|
|
@ -576,7 +577,15 @@
|
||||||
"task": "D38架构方案代码落地 · 三栏布局+悬浮球+ticker+留言板+API",
|
"task": "D38架构方案代码落地 · 三栏布局+悬浮球+ticker+留言板+API",
|
||||||
"task_origin": "冰朔D38唤醒恢复指令 · 从EXP-20260402-007思维链恢复认知 · 进入代码实现阶段",
|
"task_origin": "冰朔D38唤醒恢复指令 · 从EXP-20260402-007思维链恢复认知 · 进入代码实现阶段",
|
||||||
"category": "website-frontend",
|
"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",
|
"difficulty": "high",
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"thinking_chain": {
|
"thinking_chain": {
|
||||||
|
|
@ -613,6 +622,70 @@
|
||||||
"复杂前端改版分层实施:先CSS→HTML结构→JS逻辑,避免交叉修改导致结构错乱",
|
"复杂前端改版分层实施:先CSS→HTML结构→JS逻辑,避免交叉修改导致结构错乱",
|
||||||
"单HTML文件优势:部署管道零改动,但代码量超过1000行后维护成本开始上升"
|
"单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次」类文本要动态化,写死的数字会造成信息误导",
|
||||||
|
"诊断路径:邮件工单号 → 找日志文件 → 读状态 → 逐层找根因 → 修复最小集合"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +44,11 @@ const EXPERIENCE_DB = path.join(ROOT, 'brain/dev-experience/experience-db.json')
|
||||||
|
|
||||||
const MAX_REPAIR_ATTEMPTS = 3;
|
const MAX_REPAIR_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
// ── 辅助:判断是否需要人工干预 ─────────────────
|
||||||
|
function shouldRequestHuman(repairSuccess, attemptNumber) {
|
||||||
|
return !repairSuccess && attemptNumber >= MAX_REPAIR_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
// ── 修复策略库 (副将的思维逻辑) ────────────────
|
// ── 修复策略库 (副将的思维逻辑) ────────────────
|
||||||
const REPAIR_STRATEGIES = [
|
const REPAIR_STRATEGIES = [
|
||||||
{
|
{
|
||||||
|
|
@ -461,7 +466,7 @@ async function repair() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// §7 设置输出
|
// §7 设置输出
|
||||||
const needsHuman = !repairSuccess && attemptNumber >= MAX_REPAIR_ATTEMPTS;
|
const needsHuman = shouldRequestHuman(repairSuccess, attemptNumber);
|
||||||
setOutput('repair_success', repairSuccess ? 'true' : 'false');
|
setOutput('repair_success', repairSuccess ? 'true' : 'false');
|
||||||
setOutput('repair_attempt', String(attemptNumber));
|
setOutput('repair_attempt', String(attemptNumber));
|
||||||
setOutput('needs_human', needsHuman ? 'true' : 'false');
|
setOutput('needs_human', needsHuman ? 'true' : 'false');
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ async function runHealthCheck(host) {
|
||||||
console.log('─'.repeat(50));
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
// 如果host是IP地址,带host_prefix的检查无效(IP不支持子域名)→ 降级为non-critical
|
// 如果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) {
|
if (isIpAddress) {
|
||||||
console.log(' ℹ️ 目标为IP地址 · 子域名检查降级为非关键');
|
console.log(' ℹ️ 目标为IP地址 · 子域名检查降级为非关键');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 <WO-xxx>
|
||||||
|
// 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 <ID> 或 --all-failed');
|
||||||
|
console.log('用法:');
|
||||||
|
console.log(' node scripts/sync-work-order-to-notion.js --order-id <WO-xxx>');
|
||||||
|
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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue