feat: 飞书→Notion全自动闭环 — Pipeline B/C人格体唤醒+飞书推送 + write-notion-syslog.js + push-broadcast-feishu.yml

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-14 16:06:01 +00:00
parent fa60ea92a3
commit 78b000ca71
4 changed files with 460 additions and 0 deletions

View File

@ -0,0 +1,87 @@
name: 铸渊 · 广播推送飞书(聊天消息)
# 独立 workflow接收广播内容推送飞书消息卡片到原聊天窗口
# 用于自动链路推送失败时的手动兜底,或独立触发推送
#
# 依赖 Secrets
# PUSH_BROADCAST_TOKEN 广播推送接口鉴权 token
#
# dispatch payload
# chat_id 飞书聊天窗口 ID优先
# sender_open_id 发送者飞书 Open ID备选
# broadcast_title 广播标题
# broadcast_content 广播摘要内容
# broadcast_url Notion 广播页面链接
on:
repository_dispatch:
types: [push-broadcast-feishu]
workflow_dispatch:
inputs:
chat_id:
description: '飞书聊天窗口 ID'
required: false
sender_open_id:
description: '发送者飞书 Open ID'
required: false
broadcast_title:
description: '广播标题'
required: true
broadcast_content:
description: '广播摘要'
required: true
broadcast_url:
description: 'Notion 广播页面链接'
required: true
jobs:
push-to-feishu:
name: 📢 推送广播到飞书聊天
runs-on: ubuntu-latest
steps:
- name: 📢 推送广播消息卡片
env:
PUSH_URL: https://guanghulab.com/webhook/push-broadcast
PUSH_TOKEN: ${{ secrets.PUSH_BROADCAST_TOKEN }}
CHAT_ID: ${{ github.event.client_payload.chat_id || github.event.inputs.chat_id || '' }}
SENDER_OPEN_ID: ${{ github.event.client_payload.sender_open_id || github.event.inputs.sender_open_id || '' }}
BROADCAST_TITLE: ${{ github.event.client_payload.broadcast_title || github.event.inputs.broadcast_title || '' }}
BROADCAST_CONTENT: ${{ github.event.client_payload.broadcast_content || github.event.inputs.broadcast_content || '' }}
BROADCAST_URL: ${{ github.event.client_payload.broadcast_url || github.event.inputs.broadcast_url || '' }}
run: |
if [ -z "$PUSH_TOKEN" ]; then
echo "⚠️ PUSH_BROADCAST_TOKEN 未配置,跳过推送"
exit 0
fi
if [ -z "$CHAT_ID" ] && [ -z "$SENDER_OPEN_ID" ]; then
echo "⚠️ 缺少 chat_id 和 sender_open_id无法推送"
exit 1
fi
echo "📢 推送广播到飞书..."
echo " 标题: $BROADCAST_TITLE"
echo " 目标: ${CHAT_ID:-$SENDER_OPEN_ID}"
HTTP_CODE=$(curl -s -o /tmp/push-response.txt -w "%{http_code}" \
-X POST "$PUSH_URL" \
-H "Content-Type: application/json" \
-H "x-push-token: $PUSH_TOKEN" \
-d "$(jq -n \
--arg chat_id "$CHAT_ID" \
--arg sender_open_id "$SENDER_OPEN_ID" \
--arg broadcast_title "$BROADCAST_TITLE" \
--arg broadcast_content "$BROADCAST_CONTENT" \
--arg broadcast_url "$BROADCAST_URL" \
'{chat_id: $chat_id, sender_open_id: $sender_open_id, broadcast_title: $broadcast_title, broadcast_content: $broadcast_content, broadcast_url: $broadcast_url}')")
echo " HTTP 响应: $HTTP_CODE"
cat /tmp/push-response.txt
echo ""
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ 广播已推送到飞书"
else
echo "❌ 推送失败 (HTTP $HTTP_CODE)"
exit 1
fi

View File

@ -23,6 +23,11 @@ jobs:
issues: read
outputs:
processed_count: ${{ steps.process.outputs.processed_count }}
has_syslog: ${{ steps.parse.outputs.has_syslog }}
sender_open_id: ${{ steps.parse.outputs.sender_open_id }}
chat_id: ${{ steps.parse.outputs.chat_id }}
sender_name: ${{ steps.parse.outputs.sender_name }}
syslog_file: ${{ steps.parse.outputs.syslog_file }}
steps:
- uses: actions/checkout@v4
@ -39,6 +44,41 @@ jobs:
- name: Install dependencies
run: npm ci
# Step 0: 解析 sender 信息(在 process-syslog.js 归档之前)
- name: 📋 解析 SYSLOG sender 信息
id: parse
run: |
LATEST=$(ls -t syslog-inbox/*.json 2>/dev/null | grep -v '.gitkeep' | head -1 || true)
if [ -z "$LATEST" ]; then
echo "📭 syslog-inbox/ 无待处理文件"
echo "has_syslog=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "has_syslog=true" >> "$GITHUB_OUTPUT"
echo "syslog_file=$LATEST" >> "$GITHUB_OUTPUT"
SENDER_OPEN_ID=$(jq -r '.sender_open_id // empty' "$LATEST" 2>/dev/null || true)
CHAT_ID=$(jq -r '.chat_id // empty' "$LATEST" 2>/dev/null || true)
SENDER_NAME=$(jq -r '.sender_name // empty' "$LATEST" 2>/dev/null || true)
echo "sender_open_id=$SENDER_OPEN_ID" >> "$GITHUB_OUTPUT"
echo "chat_id=$CHAT_ID" >> "$GITHUB_OUTPUT"
echo "sender_name=$SENDER_NAME" >> "$GITHUB_OUTPUT"
echo "📋 SYSLOG 文件: $LATEST"
echo " sender_open_id: ${SENDER_OPEN_ID:-无}"
echo " chat_id: ${CHAT_ID:-无}"
echo " sender_name: ${SENDER_NAME:-无}"
# Step 1: 写入 Notion SYSLOG 收件箱
- name: 📝 写入 Notion SYSLOG 收件箱
if: steps.parse.outputs.has_syslog == 'true'
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DB_SYSLOG: ${{ secrets.NOTION_DB_SYSLOG }}
run: node scripts/write-notion-syslog.js "${{ steps.parse.outputs.syslog_file }}"
# Step 2: 处理 syslog-inbox 条目(归档到 syslog-processed/
- name: 📥 处理 syslog-inbox 条目
id: process
run: |
@ -54,6 +94,115 @@ jobs:
git diff --staged --quiet || git commit -m "📥 syslog: 处理 inbox 条目,更新大脑记忆 [skip ci]"
git push
# ── Pipeline BClaude API 唤醒人格体 ────────────────────
pipeline-b-persona:
name: 🧠 Pipeline B · 人格体唤醒
runs-on: ubuntu-latest
needs: pipeline-a-syslog
if: needs.pipeline-a-syslog.outputs.has_syslog == 'true'
permissions:
contents: read
outputs:
broadcast_title: ${{ steps.persona.outputs.broadcast_title }}
broadcast_url: ${{ steps.persona.outputs.broadcast_url }}
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: main
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: 🧠 唤醒人格体处理 SYSLOG
id: persona
env:
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
CORE_BRAIN_PAGE_ID: ${{ secrets.CORE_BRAIN_PAGE_ID }}
PORTRAIT_DB_ID: ${{ secrets.PORTRAIT_DB_ID }}
FINGERPRINT_DB_ID: ${{ secrets.FINGERPRINT_DB_ID }}
SUBMIT_TYPE: syslog
SUBMIT_CONTENT: ${{ needs.pipeline-a-syslog.outputs.syslog_file }}
AUTHOR: ${{ needs.pipeline-a-syslog.outputs.sender_name }}
run: |
if [ -z "$LLM_API_KEY" ] || [ -z "$LLM_BASE_URL" ]; then
echo "⚠️ LLM_API_KEY 或 LLM_BASE_URL 未配置,跳过人格体唤醒"
echo "broadcast_title=" >> "$GITHUB_OUTPUT"
echo "broadcast_url=" >> "$GITHUB_OUTPUT"
exit 0
fi
node scripts/wake-persona.js || true
# 读取人格体输出的广播信息(如果有)
if [ -f /tmp/broadcast-result.json ]; then
BROADCAST_TITLE=$(jq -r '.title // empty' /tmp/broadcast-result.json)
BROADCAST_URL=$(jq -r '.notion_url // empty' /tmp/broadcast-result.json)
echo "broadcast_title=$BROADCAST_TITLE" >> "$GITHUB_OUTPUT"
echo "broadcast_url=$BROADCAST_URL" >> "$GITHUB_OUTPUT"
echo "✅ 人格体处理完成,广播标题: $BROADCAST_TITLE"
else
echo "⚠️ /tmp/broadcast-result.json 未生成(人格体可能未产出广播)"
echo "broadcast_title=" >> "$GITHUB_OUTPUT"
echo "broadcast_url=" >> "$GITHUB_OUTPUT"
fi
# ── Pipeline C推送广播到飞书 ───────────────────────────
pipeline-c-push:
name: 📢 Pipeline C · 飞书广播推送
runs-on: ubuntu-latest
needs: [pipeline-a-syslog, pipeline-b-persona]
if: >-
needs.pipeline-b-persona.outputs.broadcast_title != '' &&
(needs.pipeline-a-syslog.outputs.chat_id != '' || needs.pipeline-a-syslog.outputs.sender_open_id != '')
steps:
- name: 📢 推送广播到飞书
env:
PUSH_URL: https://guanghulab.com/webhook/push-broadcast
PUSH_TOKEN: ${{ secrets.PUSH_BROADCAST_TOKEN }}
CHAT_ID: ${{ needs.pipeline-a-syslog.outputs.chat_id }}
SENDER_OPEN_ID: ${{ needs.pipeline-a-syslog.outputs.sender_open_id }}
BROADCAST_TITLE: ${{ needs.pipeline-b-persona.outputs.broadcast_title }}
BROADCAST_URL: ${{ needs.pipeline-b-persona.outputs.broadcast_url }}
run: |
if [ -z "$PUSH_TOKEN" ]; then
echo "⚠️ PUSH_BROADCAST_TOKEN 未配置,跳过飞书推送"
exit 0
fi
echo "📢 推送广播到飞书..."
echo " 标题: $BROADCAST_TITLE"
echo " 目标: ${CHAT_ID:-$SENDER_OPEN_ID}"
HTTP_CODE=$(curl -s -o /tmp/push-response.txt -w "%{http_code}" \
-X POST "$PUSH_URL" \
-H "Content-Type: application/json" \
-H "x-push-token: $PUSH_TOKEN" \
-d "$(jq -n \
--arg chat_id "$CHAT_ID" \
--arg sender_open_id "$SENDER_OPEN_ID" \
--arg broadcast_title "$BROADCAST_TITLE" \
--arg broadcast_content "人格体已验收,请查看完整广播。" \
--arg broadcast_url "$BROADCAST_URL" \
'{chat_id: $chat_id, sender_open_id: $sender_open_id, broadcast_title: $broadcast_title, broadcast_content: $broadcast_content, broadcast_url: $broadcast_url}')")
echo " HTTP 响应: $HTTP_CODE"
cat /tmp/push-response.txt 2>/dev/null || true
echo ""
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ 广播已推送到飞书"
else
echo "❌ 推送失败 (HTTP $HTTP_CODE),可通过 push-broadcast-feishu workflow 手动补推"
fi
# ── Pipeline DIssues 巡检 ─────────────────────────────
pipeline-d-issues:
name: 🔍 Pipeline D · Issues 巡检

View File

@ -885,6 +885,35 @@ async function main() {
fs.appendFileSync(outputFile, 'result<<' + delimiter + '\n' + result + '\n' + delimiter + '\n');
}
// 输出广播结果到 /tmp/broadcast-result.json供飞书推送步骤使用
try {
const broadcastResult = {
title: '',
summary: '',
notion_url: '',
dev_id: AUTHOR,
raw_length: result.length,
timestamp: new Date().toISOString(),
};
// 尝试从人格体输出中提取广播标题BC-xxx 格式)
const bcMatch = result.match(/BC-[\w]+-\d+-[\w]+/);
if (bcMatch) {
broadcastResult.title = bcMatch[0];
}
// 尝试提取摘要取前100字符
const summaryLines = result.split('\n').filter(function (l) { return l.trim().length > 0; });
if (summaryLines.length > 0) {
broadcastResult.summary = summaryLines[0].slice(0, 100);
}
fs.writeFileSync('/tmp/broadcast-result.json', JSON.stringify(broadcastResult, null, 2));
console.log(' 📄 broadcast-result.json 已写入 /tmp/');
} catch (e) {
console.log(' ⚠️ broadcast-result.json 写入失败: ' + e.message);
}
// 同时输出到 stdout 供调试
console.log('');
console.log('═══════════════════════════════════════════');

View File

@ -0,0 +1,195 @@
// scripts/write-notion-syslog.js
// 铸渊 · 飞书 SYSLOG → Notion 收件箱写入
//
// 读取 syslog-inbox/ 下的 JSON 文件,写入 Notion SYSLOG 收件箱数据库。
// 用法: node scripts/write-notion-syslog.js <syslog-file-path>
//
// 环境变量:
// NOTION_TOKEN Notion API token必须
// NOTION_DB_SYSLOG SYSLOG 收件箱数据库 ID必须
'use strict';
const https = require('https');
const fs = require('fs');
const path = require('path');
// ══════════════════════════════════════════════════════════
// 常量
// ══════════════════════════════════════════════════════════
const NOTION_VERSION = '2022-06-28';
const NOTION_API_HOSTNAME = 'api.notion.com';
const NOTION_RICH_TEXT_MAX = 2000;
// ══════════════════════════════════════════════════════════
// HTTP 请求工具(复用 receive-syslog.js 模式)
// ══════════════════════════════════════════════════════════
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', chunk => data += chunk);
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)));
}
} catch (e) {
reject(new Error('Notion API parse error: ' + data));
}
});
});
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, 120) } }] };
}
function richTextProp(text) {
return { rich_text: richText(text) };
}
function selectProp(name) {
return { select: { name: String(name) } };
}
function dateProp(dateStr) {
return { date: { start: dateStr } };
}
function checkboxProp(val) {
return { checkbox: !!val };
}
// ══════════════════════════════════════════════════════════
// 主流程
// ══════════════════════════════════════════════════════════
async function main() {
const filePath = process.argv[2];
const notionToken = process.env.NOTION_TOKEN;
const syslogDbId = process.env.NOTION_DB_SYSLOG;
if (!filePath) {
console.error('❌ 用法: node write-notion-syslog.js <syslog-file-path>');
process.exit(1);
}
if (!notionToken || !syslogDbId) {
console.log('⚠️ 缺少 NOTION_TOKEN 或 NOTION_DB_SYSLOG跳过 Notion 写入');
process.exit(0);
}
// 读取 SYSLOG 文件
let entry;
try {
entry = JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
console.error('❌ 读取/解析文件失败: ' + e.message);
process.exit(1);
}
const filename = path.basename(filePath);
const syslogText = entry.syslog || entry.content || '';
const source = entry.source || 'unknown';
const senderName = entry.sender_name || '';
const senderOpenId = entry.sender_open_id || '';
const chatId = entry.chat_id || '';
const timestamp = entry.timestamp || new Date().toISOString();
const dateStr = timestamp.split('T')[0] || new Date().toISOString().split('T')[0];
console.log('📋 写入 Notion SYSLOG 收件箱...');
console.log(' 文件: ' + filename);
console.log(' 来源: ' + source);
console.log(' 发送者: ' + (senderName || senderOpenId || '未知'));
// 构建 Notion 页面属性
const properties = {
'标题': titleProp('飞书SYSLOG · ' + (senderName || source) + ' · ' + dateStr),
'提交日期': dateProp(dateStr),
'霜砚已读': checkboxProp(false),
};
// 可选字段(如果 Notion 数据库有这些列)
if (source) properties['来源'] = richTextProp(source);
if (senderName) properties['发送者'] = richTextProp(senderName);
if (senderOpenId) properties['发送者ID'] = richTextProp(senderOpenId);
// 构建页面内容
const contentBlocks = [];
// SYSLOG 正文
if (syslogText) {
contentBlocks.push({
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: richText(syslogText),
},
});
}
// 原始 JSON调试用
contentBlocks.push({
object: 'block',
type: 'code',
code: {
rich_text: richText(JSON.stringify(entry, null, 2)),
language: 'json',
},
});
const body = {
parent: { database_id: syslogDbId },
properties,
children: contentBlocks,
};
try {
const result = await notionPost('/v1/pages', body, notionToken);
console.log(' ✅ Notion SYSLOG 收件箱条目已创建: ' + result.id);
} catch (err) {
console.error(' ❌ Notion 写入失败: ' + err.message);
// 不退出,允许后续步骤继续
}
}
main().catch(err => {
console.error('❌ write-notion-syslog 失败: ' + err.message);
process.exit(1);
});