Merge pull request #174 from qinfendebingshuo/copilot/zy-token-renew-2026-0324-001
OAuth2 proxy auth migration + token auto-renewal engine
This commit is contained in:
commit
8ab2a8d702
|
|
@ -1095,6 +1095,32 @@
|
|||
"daily_checkin_required": false,
|
||||
"parent_sys": "SYS-GLW-0001",
|
||||
"owner": "ICE-0002∞"
|
||||
},
|
||||
{
|
||||
"id": "AG-ZY-085",
|
||||
"name": "OAuth2 Token 自动续期",
|
||||
"workflow": "renew-gdrive-tokens.yml",
|
||||
"duty": "OAuth2 Token 自动续期",
|
||||
"trigger": "定时 周一/周四 10:00 CST + 手动",
|
||||
"self_repair": true,
|
||||
"report_to": "ICE-GL-ZY001",
|
||||
"status": "registered",
|
||||
"daily_checkin_required": false,
|
||||
"parent_sys": "SYS-GLW-0001",
|
||||
"owner": "ICE-0002∞"
|
||||
},
|
||||
{
|
||||
"id": "AG-ZY-086",
|
||||
"name": "Token 健康检查(每日安全网)",
|
||||
"workflow": "check-token-health.yml",
|
||||
"duty": "Token 健康检查(每日安全网)",
|
||||
"trigger": "定时 每天 12:15 CST + 手动",
|
||||
"self_repair": true,
|
||||
"report_to": "ICE-GL-ZY001",
|
||||
"status": "registered",
|
||||
"daily_checkin_required": false,
|
||||
"parent_sys": "SYS-GLW-0001",
|
||||
"owner": "ICE-0002∞"
|
||||
}
|
||||
],
|
||||
"note": "铸渊已扫描所有workflow文件,按此格式逐一注册,编号从AG-ZY-001开始顺序分配",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ jobs:
|
|||
|
||||
- name: ⚡ 执行部署
|
||||
env:
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
DEPLOY_GITHUB_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }}
|
||||
run: |
|
||||
for f in grid-db/deploy-queue/*.json; do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# 🛡️ Token 健康检查(每日安全网)
|
||||
#
|
||||
# 守护: PER-ZY001 铸渊 (Zhuyuan)
|
||||
# 系统: SYS-GLW-0001
|
||||
# 主控: TCS-0002∞ (冰朔)
|
||||
#
|
||||
# CRON: 每天 12:15 CST (04:15 UTC) — 避开日休眠 04:00-04:10 CST
|
||||
# 功能: 只检查不刷新,即将过期才紧急刷新
|
||||
#
|
||||
# 生命线任务:天眼休眠期内豁免执行
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
name: 🛡️ Token 健康检查(每日安全网)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 每天 12:15 CST (04:15 UTC) — 在日休眠 04:10 CST 之后
|
||||
- cron: '15 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
check-tokens:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 🛡️ 检查 Token 健康状态
|
||||
id: health_check
|
||||
run: |
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const reg = JSON.parse(fs.readFileSync('config/gdrive-tokens.json', 'utf8'));
|
||||
let urgent = false;
|
||||
for (const t of reg.tokens) {
|
||||
const hoursLeft = (new Date(t.expires_at) - new Date()) / 3600000;
|
||||
console.log(t.user_name + ': ' + hoursLeft.toFixed(1) + 'h remaining');
|
||||
if (hoursLeft < 48) {
|
||||
console.log(' ⚠️ URGENT: < 48h, triggering emergency renewal');
|
||||
urgent = true;
|
||||
}
|
||||
}
|
||||
if (urgent) process.exit(1);
|
||||
console.log('✅ All tokens healthy.');
|
||||
"
|
||||
|
||||
- name: ⚡ 紧急刷新(仅在即将过期时)
|
||||
if: failure()
|
||||
env:
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FORCE_RENEW: 'true'
|
||||
# 新增开发者 Token(每次加人时追加)
|
||||
# GDRIVE_REFRESH_TOKEN_DEV001: ${{ secrets.GDRIVE_REFRESH_TOKEN_DEV001 }}
|
||||
# GDRIVE_REFRESH_TOKEN_DEV002: ${{ secrets.GDRIVE_REFRESH_TOKEN_DEV002 }}
|
||||
run: |
|
||||
npm install tweetsodium
|
||||
node scripts/gdrive/renew-tokens.js
|
||||
|
||||
- name: 📋 提交
|
||||
if: always()
|
||||
run: |
|
||||
git config user.name "zhuyuan-bot"
|
||||
git config user.email "zhuyuan@guanghulab.com"
|
||||
git add config/gdrive-tokens.json
|
||||
git add -f grid-db/logs/token-alert.json 2>/dev/null || true
|
||||
git diff --cached --quiet || git commit -m "auto: Token health check $(date -u +%Y-%m-%dT%H:%M:%SZ) [skip ci]"
|
||||
git push
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# 🔄 OAuth2 Token 自动续期
|
||||
#
|
||||
# 守护: PER-ZY001 铸渊 (Zhuyuan)
|
||||
# 系统: SYS-GLW-0001
|
||||
# 主控: TCS-0002∞ (冰朔)
|
||||
#
|
||||
# CRON 策略:
|
||||
# 主刷新 A: 每周一 10:00 CST (02:00 UTC) — 距周六休眠最远
|
||||
# 主刷新 B: 每周四 10:00 CST (02:00 UTC) — 3天后补刷
|
||||
# 三层保险:周一 + 周四 + 每日安全网(check-token-health.yml)
|
||||
#
|
||||
# 生命线任务:天眼休眠期内豁免执行
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
name: 🔄 OAuth2 Token 自动续期
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 主刷新 A: 每周一 10:00 CST (02:00 UTC)
|
||||
- cron: '0 2 * * 1'
|
||||
# 主刷新 B: 每周四 10:00 CST (02:00 UTC)
|
||||
- cron: '0 2 * * 4'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_renew:
|
||||
description: '强制刷新所有 Token(忽略过期时间检查)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
renew-tokens:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 🔧 安装依赖
|
||||
run: npm install tweetsodium
|
||||
|
||||
- name: 🔄 执行 Token 刷新
|
||||
env:
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FORCE_RENEW: ${{ inputs.force_renew || 'false' }}
|
||||
# 新增开发者 Token(每次加人时追加)
|
||||
# GDRIVE_REFRESH_TOKEN_DEV001: ${{ secrets.GDRIVE_REFRESH_TOKEN_DEV001 }}
|
||||
# GDRIVE_REFRESH_TOKEN_DEV002: ${{ secrets.GDRIVE_REFRESH_TOKEN_DEV002 }}
|
||||
run: node scripts/gdrive/renew-tokens.js
|
||||
|
||||
- name: 📋 提交注册表更新
|
||||
run: |
|
||||
git config user.name "zhuyuan-bot"
|
||||
git config user.email "zhuyuan@guanghulab.com"
|
||||
git add config/gdrive-tokens.json
|
||||
git add -f grid-db/logs/token-alert.json 2>/dev/null || true
|
||||
git diff --cached --quiet || git commit -m "auto: Token renewal $(date -u +%Y-%m-%dT%H:%M:%SZ) [skip ci]"
|
||||
git push
|
||||
|
||||
- name: ⚠️ 失败告警
|
||||
if: failure()
|
||||
run: |
|
||||
echo "⚠️ Token 刷新失败!检查 grid-db/logs/token-alert.json"
|
||||
echo "需要人类手动重新授权。"
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# 🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit)
|
||||
# 🛰️ 天眼 OAuth2 凭据审计 (Sky-Eye Credential Audit)
|
||||
#
|
||||
# 守护: PER-ZY001 铸渊 (Zhuyuan)
|
||||
# 系统: SYS-GLW-0001
|
||||
# 主控: TCS-0002∞ (冰朔)
|
||||
#
|
||||
# 功能:
|
||||
# ① GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性校验
|
||||
# ① OAuth2 凭据环境变量完整性校验
|
||||
# ② 依赖图扫描(工作流 + 脚本)
|
||||
# ③ YAML 语法全量扫描
|
||||
# ④ 审计报告归档至 System_Logs/
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
name: "🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit)"
|
||||
name: "🛰️ 天眼 OAuth2 凭据审计 (Sky-Eye Credential Audit)"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
|
@ -32,7 +32,9 @@ jobs:
|
|||
|
||||
- name: "🛰️ Run Sky-Eye Credential Audit"
|
||||
env:
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
run: node scripts/skyeye/credential-audit.js
|
||||
|
||||
- name: "📝 Commit audit report"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ jobs:
|
|||
|
||||
- name: 🪞 同步到 Google Drive
|
||||
env:
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
DRIVE_FOLDER_ID: ${{ secrets.DRIVE_MIRROR_FOLDER_ID }}
|
||||
run: node scripts/grid-db/sync-to-drive.js
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
# 系统: SYS-GLW-0001
|
||||
# 主控: TCS-0002∞ (冰朔)
|
||||
#
|
||||
# 方案: 宿主机直装 rclone(替代 wei/rclone 容器方案)
|
||||
# 凭证: GOOGLE_DRIVE_SERVICE_ACCOUNT (Service Account JSON)
|
||||
# 方案: 宿主机直装 rclone(OAuth2 代理人模式)
|
||||
# 凭证: GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
|
||||
# 目标: Google Drive 文件夹 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
|
|
@ -36,39 +36,32 @@ jobs:
|
|||
sudo apt-get install -y -qq rclone
|
||||
rclone version
|
||||
|
||||
- name: "🔑 Configure rclone (Service Account)"
|
||||
- name: "🔑 Configure rclone (OAuth2)"
|
||||
env:
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
run: |
|
||||
if [ -z "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" ]; then
|
||||
echo "::error::GOOGLE_DRIVE_SERVICE_ACCOUNT secret is not set"
|
||||
if [ -z "${GDRIVE_CLIENT_ID}" ] || [ -z "${GDRIVE_CLIENT_SECRET}" ] || [ -z "${GDRIVE_REFRESH_TOKEN}" ]; then
|
||||
echo "::error::OAuth2 secrets not set (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SA_FILE="$(mktemp "${RUNNER_TEMP}/gd-sa-XXXXXX.json")"
|
||||
printf '%s\n' "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" > "${SA_FILE}"
|
||||
chmod 600 "${SA_FILE}"
|
||||
|
||||
# 天眼密钥流校验:验证 JSON 完整性
|
||||
if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "${SA_FILE}" 2>/dev/null; then
|
||||
echo "::error::GOOGLE_DRIVE_SERVICE_ACCOUNT contains invalid JSON (possible truncation)"
|
||||
rm -f "${SA_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Service account JSON validated"
|
||||
echo "✅ OAuth2 credentials validated"
|
||||
|
||||
mkdir -p "${HOME}/.config/rclone"
|
||||
{
|
||||
echo "[gdrive]"
|
||||
echo "type = drive"
|
||||
echo "scope = drive"
|
||||
echo "service_account_file = ${SA_FILE}"
|
||||
echo "client_id = ${GDRIVE_CLIENT_ID}"
|
||||
echo "client_secret = ${GDRIVE_CLIENT_SECRET}"
|
||||
echo "token = {\"access_token\":\"\",\"token_type\":\"Bearer\",\"refresh_token\":\"${GDRIVE_REFRESH_TOKEN}\",\"expiry\":\"2000-01-01T00:00:00Z\"}"
|
||||
echo "root_folder_id = 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G"
|
||||
} > "${HOME}/.config/rclone/rclone.conf"
|
||||
chmod 600 "${HOME}/.config/rclone/rclone.conf"
|
||||
|
||||
echo "SA_FILE=${SA_FILE}" >> "${GITHUB_ENV}"
|
||||
echo "✅ rclone configured with Service Account"
|
||||
echo "✅ rclone configured with OAuth2"
|
||||
|
||||
- name: "🪞 Sync to Google Drive"
|
||||
# 使用 copy(非 sync)—— 仅添加/更新文件,不删除 Drive 端已有文件(防误删)
|
||||
|
|
@ -90,6 +83,5 @@ jobs:
|
|||
- name: "🧹 Cleanup credentials"
|
||||
if: always()
|
||||
run: |
|
||||
rm -f "${SA_FILE}"
|
||||
rm -f "${HOME}/.config/rclone/rclone.conf"
|
||||
echo "✅ Credentials cleaned up"
|
||||
|
|
|
|||
|
|
@ -31,7 +31,9 @@ jobs:
|
|||
|
||||
- name: 🛰️ 执行语义落盘
|
||||
env:
|
||||
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
|
||||
GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
|
||||
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
|
||||
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
|
||||
DRIVE_LANDING_FOLDER_ID: '1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"schema_version": "1.0",
|
||||
"oauth_app": {
|
||||
"project_id": "guanghu-drive-bridge",
|
||||
"client_id_secret": "GDRIVE_CLIENT_ID",
|
||||
"client_secret_secret": "GDRIVE_CLIENT_SECRET",
|
||||
"scope": "https://www.googleapis.com/auth/drive",
|
||||
"token_ttl_days": 7,
|
||||
"renew_interval_days": 6,
|
||||
"renew_safety_margin_hours": 24
|
||||
},
|
||||
"tokens": [
|
||||
{
|
||||
"user_id": "TCS-0002",
|
||||
"user_name": "冰朔",
|
||||
"role": "master",
|
||||
"github_secret_name": "GDRIVE_REFRESH_TOKEN",
|
||||
"last_renewed": "2026-03-24T14:30:00+08:00",
|
||||
"expires_at": "2026-03-31T14:30:00+08:00",
|
||||
"status": "active",
|
||||
"renew_count": 0,
|
||||
"notes": "主控账号 · 格点库核心 Token"
|
||||
}
|
||||
],
|
||||
"renew_log": []
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* check-hibernation.js
|
||||
*
|
||||
* 检查当前是否在天眼休眠期内
|
||||
*
|
||||
* 休眠规则(从天眼治理层配置读取):
|
||||
* - 周休眠:每周六 20:00 ~ 00:00 CST(4小时)
|
||||
* - 日休眠:每日 04:00 ~ 04:10 CST(默认窗口)
|
||||
*
|
||||
* 对于生命线任务:输出提示但不阻止执行
|
||||
* 对于普通任务:输出提示并建议延迟
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
* 系统: SYS-GLW-0001
|
||||
* 主控: TCS-0002∞ 冰朔
|
||||
*/
|
||||
|
||||
function isInHibernation() {
|
||||
const now = new Date();
|
||||
// 转换为北京时间(UTC+8),使用 UTC 偏移计算避免 locale 差异
|
||||
const utcMs = now.getTime() + now.getTimezoneOffset() * 60 * 1000;
|
||||
const cst = new Date(utcMs + 8 * 60 * 60 * 1000);
|
||||
const day = cst.getDay(); // 0=周日, 6=周六
|
||||
const hour = cst.getHours();
|
||||
const minute = cst.getMinutes();
|
||||
|
||||
// 周休眠:周六 20:00 ~ 周日 00:00
|
||||
if (day === 6 && hour >= 20) {
|
||||
return { hibernating: true, type: 'weekly', endsAt: '周日 00:00 CST' };
|
||||
}
|
||||
|
||||
// 日休眠:每日 04:00 ~ 04:10(默认窗口)
|
||||
if (hour === 4 && minute < 10) {
|
||||
return { hibernating: true, type: 'daily', endsAt: '04:10 CST' };
|
||||
}
|
||||
|
||||
return { hibernating: false };
|
||||
}
|
||||
|
||||
module.exports = { isInHibernation };
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* push-token-alerts.js
|
||||
*
|
||||
* 天眼系统调用此脚本读取 token-alert.json
|
||||
* 并将告警信息写入主控台 + 公告板
|
||||
*
|
||||
* 输出:
|
||||
* - grid-db/notifications/token-alert-{timestamp}.md → 主控台通知
|
||||
* - grid-db/bulletin/token-alert-{timestamp}.md → 公告板通知
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
* 系统: SYS-GLW-0001
|
||||
* 主控: TCS-0002∞ 冰朔
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function pushAlerts() {
|
||||
const alertPath = path.join(__dirname, '../../grid-db/logs/token-alert.json');
|
||||
|
||||
if (!fs.existsSync(alertPath)) {
|
||||
console.log('✅ 无告警。');
|
||||
return;
|
||||
}
|
||||
|
||||
const alert = JSON.parse(fs.readFileSync(alertPath, 'utf8'));
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
// 生成主控台通知
|
||||
const dashboardContent = [
|
||||
`# ⚠️ Token 续期告警`,
|
||||
``,
|
||||
`**时间:** ${alert.time}`,
|
||||
`**严重级:** ${alert.severity || 'ALERT'}`,
|
||||
`**影响用户:** ${alert.failed_users.join(', ')}`,
|
||||
``,
|
||||
`## 描述`,
|
||||
alert.message,
|
||||
``,
|
||||
`## 需要人类操作`,
|
||||
...(alert.instructions || []).map((s) => s),
|
||||
``,
|
||||
`---`,
|
||||
`*此告警由 Token 续期引擎自动生成*`
|
||||
].join('\n');
|
||||
|
||||
// 写入主控台
|
||||
const notifDir = path.join(__dirname, '../../grid-db/notifications');
|
||||
if (!fs.existsSync(notifDir)) fs.mkdirSync(notifDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(notifDir, `token-alert-${timestamp}.md`), dashboardContent);
|
||||
|
||||
// 写入公告板
|
||||
const bulletinDir = path.join(__dirname, '../../grid-db/bulletin');
|
||||
if (!fs.existsSync(bulletinDir)) fs.mkdirSync(bulletinDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(bulletinDir, `token-alert-${timestamp}.md`), dashboardContent);
|
||||
|
||||
// 清除已处理的告警文件
|
||||
fs.unlinkSync(alertPath);
|
||||
|
||||
console.log(`⚠️ 告警已推送到主控台 + 公告板: token-alert-${timestamp}.md`);
|
||||
}
|
||||
|
||||
pushAlerts();
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
/**
|
||||
* renew-tokens.js
|
||||
*
|
||||
* OAuth2 Token 自动续期引擎
|
||||
*
|
||||
* 功能:
|
||||
* 1. 读取 config/gdrive-tokens.json 注册表
|
||||
* 2. 遍历所有 active 的 Token
|
||||
* 3. 检查每个 Token 的剩余有效期
|
||||
* 4. 剩余 < 48小时 → 立即刷新
|
||||
* 5. 刷新成功 → 用 GitHub API 更新 Secret + 更新注册表
|
||||
* 6. 刷新失败 → 写告警日志
|
||||
*
|
||||
* 环境变量:
|
||||
* GDRIVE_CLIENT_ID — OAuth 客户端 ID
|
||||
* GDRIVE_CLIENT_SECRET — OAuth 客户端密钥
|
||||
* GITHUB_TOKEN — 有 repo 权限的 GitHub Token(用于更新 Secrets)
|
||||
* GDRIVE_REFRESH_TOKEN — 主控的当前 Refresh Token
|
||||
* GDRIVE_REFRESH_TOKEN_* — 各开发者的 Refresh Token
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
* 系统: SYS-GLW-0001
|
||||
* 主控: TCS-0002∞ 冰朔
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const REGISTRY_PATH = path.join(__dirname, '../../config/gdrive-tokens.json');
|
||||
|
||||
async function main() {
|
||||
console.log('\n🔄 Token 自动续期引擎启动');
|
||||
console.log(`当前时间: ${new Date().toISOString()}`);
|
||||
|
||||
// 1. 读取注册表
|
||||
const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
||||
const results = { renewed: [], skipped: [], failed: [] };
|
||||
|
||||
for (const token of registry.tokens) {
|
||||
if (token.status === 'expired') {
|
||||
console.log(`❌ ${token.user_name} (${token.user_id}): 已过期,需人类重新授权`);
|
||||
results.failed.push(token.user_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. 检查剩余有效期
|
||||
const expiresAt = new Date(token.expires_at);
|
||||
const now = new Date();
|
||||
const hoursLeft = (expiresAt - now) / (1000 * 60 * 60);
|
||||
|
||||
console.log(`\n👤 ${token.user_name} (${token.user_id}): 剩余 ${hoursLeft.toFixed(1)} 小时`);
|
||||
|
||||
if (hoursLeft > 72 && process.env.FORCE_RENEW !== 'true') {
|
||||
console.log(` ✅ 跳过(剩余充足)`);
|
||||
results.skipped.push(token.user_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hoursLeft <= 0) {
|
||||
console.log(` ❌ 已过期!`);
|
||||
token.status = 'expired';
|
||||
results.failed.push(token.user_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 执行刷新
|
||||
console.log(` ⚡ 执行刷新...`);
|
||||
const currentToken = process.env[token.github_secret_name];
|
||||
|
||||
if (!currentToken) {
|
||||
console.log(` ❌ 环境变量 ${token.github_secret_name} 不存在`);
|
||||
results.failed.push(token.user_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const newTokenData = await refreshOAuth2Token(currentToken);
|
||||
|
||||
if (newTokenData.refresh_token) {
|
||||
// 4. 更新 GitHub Secret
|
||||
await updateGitHubSecret(
|
||||
token.github_secret_name,
|
||||
newTokenData.refresh_token
|
||||
);
|
||||
|
||||
// 5. 更新注册表
|
||||
const renewTime = new Date().toISOString();
|
||||
token.last_renewed = renewTime;
|
||||
token.expires_at = new Date(
|
||||
Date.now() + registry.oauth_app.token_ttl_days * 24 * 60 * 60 * 1000
|
||||
).toISOString();
|
||||
token.status = 'active';
|
||||
token.renew_count += 1;
|
||||
|
||||
// 6. 写刷新日志
|
||||
registry.renew_log.push({
|
||||
user_id: token.user_id,
|
||||
time: renewTime,
|
||||
result: 'success',
|
||||
hours_remaining: hoursLeft.toFixed(1)
|
||||
});
|
||||
|
||||
console.log(` ✅ 刷新成功!新过期时间: ${token.expires_at}`);
|
||||
results.renewed.push(token.user_id);
|
||||
} else {
|
||||
// Google 有时不返回新的 refresh_token(access_type 非 offline 或非首次授权时常见)
|
||||
console.log(` ⚠️ Google 未返回新 refresh_token,需人类重新授权(access_type=offline + prompt=consent)`);
|
||||
token.status = 'expiring';
|
||||
registry.renew_log.push({
|
||||
user_id: token.user_id,
|
||||
time: new Date().toISOString(),
|
||||
result: 'no_new_token',
|
||||
hours_remaining: hoursLeft.toFixed(1)
|
||||
});
|
||||
results.failed.push(token.user_id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(` ❌ 刷新失败: ${err.message}`);
|
||||
token.status = hoursLeft < 24 ? 'expired' : 'expiring';
|
||||
registry.renew_log.push({
|
||||
user_id: token.user_id,
|
||||
time: new Date().toISOString(),
|
||||
result: 'error',
|
||||
error: err.message,
|
||||
hours_remaining: hoursLeft.toFixed(1)
|
||||
});
|
||||
results.failed.push(token.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 保留最近 100 条日志
|
||||
if (registry.renew_log.length > 100) {
|
||||
registry.renew_log = registry.renew_log.slice(-100);
|
||||
}
|
||||
|
||||
// 写回注册表
|
||||
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
||||
|
||||
// 输出摘要
|
||||
console.log('\n═══ 刷新摘要 ═══');
|
||||
console.log(`✅ 已刷新: ${results.renewed.length} 个`);
|
||||
console.log(`⏭️ 已跳过: ${results.skipped.length} 个`);
|
||||
console.log(`❌ 失败/过期: ${results.failed.length} 个`);
|
||||
|
||||
// 如果有失败,输出告警标记(供天眼读取)
|
||||
if (results.failed.length > 0) {
|
||||
const logsDir = path.join(__dirname, '../../grid-db/logs');
|
||||
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
||||
|
||||
const alertFile = path.join(logsDir, 'token-alert.json');
|
||||
fs.writeFileSync(alertFile, JSON.stringify({
|
||||
alert_type: 'TOKEN_RENEWAL_FAILURE',
|
||||
severity: results.failed.some(id =>
|
||||
registry.tokens.find(t => t.user_id === id && t.status === 'expired')
|
||||
) ? 'CRITICAL' : 'ALERT',
|
||||
time: new Date().toISOString(),
|
||||
failed_users: results.failed,
|
||||
message: `${results.failed.length} 个 Token 刷新失败,需要人类介入`,
|
||||
action_required: 'HUMAN_REAUTH',
|
||||
instructions: [
|
||||
'1. 登录 Google Cloud Console',
|
||||
'2. 访问授权链接获取新 code',
|
||||
'3. 运行换 token 脚本获取新 refresh_token',
|
||||
'4. 更新 GitHub Secret: GDRIVE_REFRESH_TOKEN',
|
||||
'5. 手动触发 renew-gdrive-tokens workflow 验证'
|
||||
]
|
||||
}, null, 2));
|
||||
|
||||
// 设置非零退出码,让 workflow 知道有问题
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用旧的 refresh_token 向 Google 换新的 token
|
||||
*
|
||||
* 重要:Google OAuth2 刷新时可能会返回新的 refresh_token(也可能不返回)
|
||||
* 如果返回了新的,旧的就失效了。必须更新 Secret。
|
||||
*/
|
||||
async function refreshOAuth2Token(refreshToken) {
|
||||
const params = new URLSearchParams({
|
||||
client_id: process.env.GDRIVE_CLIENT_ID,
|
||||
client_secret: process.env.GDRIVE_CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
if (json.error) {
|
||||
reject(new Error(`${json.error}: ${json.error_description}`));
|
||||
} else {
|
||||
resolve(json);
|
||||
}
|
||||
} catch (e) {
|
||||
reject(new Error('Invalid JSON response from Google OAuth2 endpoint'));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(params.toString());
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 GitHub API 更新 Repository Secret
|
||||
*
|
||||
* 流程:
|
||||
* 1. 获取仓库的 public key(用于加密 secret)
|
||||
* 2. 用 tweetsodium 加密新值
|
||||
* 3. PUT 更新 secret
|
||||
*/
|
||||
async function updateGitHubSecret(secretName, secretValue) {
|
||||
const repo = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab';
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
|
||||
// 获取 public key
|
||||
const pubKey = await githubAPI(`/repos/${repo}/actions/secrets/public-key`, 'GET', token);
|
||||
|
||||
// 加密
|
||||
const sodium = require('tweetsodium');
|
||||
const messageBytes = Buffer.from(secretValue);
|
||||
const keyBytes = Buffer.from(pubKey.key, 'base64');
|
||||
const encryptedBytes = sodium.seal(messageBytes, keyBytes);
|
||||
const encrypted = Buffer.from(encryptedBytes).toString('base64');
|
||||
|
||||
// 更新 secret
|
||||
await githubAPI(`/repos/${repo}/actions/secrets/${secretName}`, 'PUT', token, {
|
||||
encrypted_value: encrypted,
|
||||
key_id: pubKey.key_id
|
||||
});
|
||||
|
||||
console.log(` 🔑 GitHub Secret ${secretName} 已更新`);
|
||||
}
|
||||
|
||||
function githubAPI(apiPath, method, token, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'api.github.com',
|
||||
path: apiPath,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'User-Agent': 'zhuyuan-token-renewer',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
};
|
||||
if (body) options.headers['Content-Type'] = 'application/json';
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 400) {
|
||||
reject(new Error(`GitHub API ${res.statusCode}: ${data}`));
|
||||
} else {
|
||||
try {
|
||||
resolve(data ? JSON.parse(data) : {});
|
||||
} catch (e) {
|
||||
resolve({});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('☠️ Token 续期引擎崩溃:', err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
@ -28,11 +28,11 @@ Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作
|
|||
### A.1 Google Cloud 配置
|
||||
|
||||
1. 前往 [Google Cloud Console](https://console.cloud.google.com)
|
||||
2. 创建项目或使用已有项目
|
||||
3. 启用 **Google Drive API**
|
||||
4. 创建 **Service Account**,下载 JSON 密钥文件
|
||||
5. 在用户的 Google Drive 中创建「光湖格点库」文件夹
|
||||
6. 将 Service Account 邮箱分享到该文件夹(**编辑者**权限)
|
||||
2. 创建项目或使用已有项目(项目名:`guanghu-drive-bridge`)
|
||||
3. 启用 **Google Drive API**、**Google Docs API**、**Google Sheets API**
|
||||
4. 创建 **OAuth 2.0 客户端凭据**(桌面应用类型)
|
||||
5. 完成 OAuth 授权流程,获取 Refresh Token
|
||||
6. 在用户的 Google Drive 中创建「光湖格点库」文件夹
|
||||
|
||||
### A.2 配置 GitHub Secrets
|
||||
|
||||
|
|
@ -40,7 +40,9 @@ Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作
|
|||
|
||||
| Secret 名称 | 值 |
|
||||
|---|---|
|
||||
| `GOOGLE_DRIVE_SERVICE_ACCOUNT` | Service Account JSON 密钥的完整内容 |
|
||||
| `GDRIVE_CLIENT_ID` | OAuth 客户端 ID |
|
||||
| `GDRIVE_CLIENT_SECRET` | OAuth 客户端密钥 |
|
||||
| `GDRIVE_REFRESH_TOKEN` | OAuth 长效刷新令牌 |
|
||||
| `DRIVE_MIRROR_FOLDER_ID` | Drive「光湖格点库」文件夹的 ID(URL 中最后一段) |
|
||||
|
||||
### A.3 自动触发
|
||||
|
|
@ -109,7 +111,7 @@ Gemini 通过 Personal Context 读取 Drive `mirror/` 中的文件,通过在 `
|
|||
## 安全注意事项
|
||||
|
||||
- **GITHUB_TOKEN** 必须存在 Apps Script 的 Script Properties 中,禁止硬编码
|
||||
- **Service Account** 密钥只存在 GitHub Secrets 中,不入库
|
||||
- **OAuth2 凭据** 只存在 GitHub Secrets 中,不入库
|
||||
- Drive 中的所有文件都是副本,丢失可从仓库重新同步
|
||||
- Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/`
|
||||
- 所有自动提交包含 `[skip ci]` 防止循环触发
|
||||
|
|
@ -164,6 +166,8 @@ Schema 见 `grid-db/schema/deploy-command.schema.json`。
|
|||
|
||||
| Secret | 用途 |
|
||||
|---|---|
|
||||
| `GOOGLE_DRIVE_SERVICE_ACCOUNT` | Service Account JSON 密钥 |
|
||||
| `GDRIVE_CLIENT_ID` | OAuth 客户端 ID |
|
||||
| `GDRIVE_CLIENT_SECRET` | OAuth 客户端密钥 |
|
||||
| `GDRIVE_REFRESH_TOKEN` | OAuth 长效刷新令牌 |
|
||||
| `DRIVE_MIRROR_FOLDER_ID` | 镜像同步目标文件夹 ID |
|
||||
| `DEPLOY_GITHUB_TOKEN` | 部署脚本使用的 GitHub Token(repo 权限) |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* 功能:一键部署 / 一键恢复 Drive 桥接层
|
||||
*
|
||||
* 读取 grid-db/deploy-queue/*.json 中的部署指令,执行:
|
||||
* ① 用 Service Account 在用户 Drive 创建「光湖格点库」目录结构
|
||||
* ① 用 OAuth2 代理人在用户 Drive 创建「光湖格点库」目录结构
|
||||
* ② 从仓库同步初始数据到 Drive mirror/
|
||||
* ③ 生成用户专属的 index.json(含 DEV 编号和人格体信息)
|
||||
* ④ 生成 Gemini 启动指令 → 写入 Drive
|
||||
|
|
@ -14,7 +14,9 @@
|
|||
* 用法:node scripts/grid-db/deploy-drive-bridge.js <deploy-command.json>
|
||||
*
|
||||
* 环境变量:
|
||||
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥内容
|
||||
* - GDRIVE_CLIENT_ID: OAuth 客户端 ID
|
||||
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
|
||||
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
|
||||
* - DEPLOY_GITHUB_TOKEN: 具有 repo 权限的 GitHub Token(用于配置 Apps Script)
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
|
|
@ -296,41 +298,17 @@ async function deploy(commandFilePath) {
|
|||
|
||||
console.log(`[deploy] ${action} for ${dev_id} (${dev_name}) → ${google_email}`);
|
||||
|
||||
// 验证环境变量
|
||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
||||
if (!serviceAccountJson) {
|
||||
writeReceipt(deploy_id, dev_id, 'failed', 'GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
|
||||
// OAuth2 认证(统一入口)
|
||||
let drive;
|
||||
try {
|
||||
const { getDriveClient } = require('./drive-auth');
|
||||
drive = getDriveClient();
|
||||
console.log('[deploy] ✅ OAuth2 credentials configured');
|
||||
} catch (err) {
|
||||
writeReceipt(deploy_id, dev_id, 'failed', `OAuth2 auth failed: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 认证 Google Drive API(天眼密钥流校验)
|
||||
let credentials;
|
||||
try {
|
||||
const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator');
|
||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
||||
if (!validation.valid) {
|
||||
console.error('[deploy] 🔴 Credential validation failed:');
|
||||
console.error(formatDiagnosticReport(validation));
|
||||
writeReceipt(deploy_id, dev_id, 'failed', 'Credential validation failed: invalid service account JSON format');
|
||||
return;
|
||||
}
|
||||
credentials = validation.credentials;
|
||||
console.log('[deploy] ✅ Service account credentials validated');
|
||||
} catch (err) {
|
||||
// Fallback: if validator module is unavailable, try direct parse
|
||||
try {
|
||||
credentials = JSON.parse(serviceAccountJson);
|
||||
} catch (parseErr) {
|
||||
writeReceipt(deploy_id, dev_id, 'failed', `JSON parse error: ${parseErr.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auth = new google.auth.GoogleAuth({
|
||||
credentials,
|
||||
scopes: ['https://www.googleapis.com/auth/drive']
|
||||
});
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
try {
|
||||
// ① 在用户 Drive 创建或复用根目录
|
||||
const rootFolderName = config.drive_root_folder || '光湖格点库';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* scripts/grid-db/drive-auth.js
|
||||
*
|
||||
* OAuth2 认证模块 · 替代原 Service Account 模式
|
||||
*
|
||||
* 环境变量:
|
||||
* GDRIVE_CLIENT_ID — OAuth 客户端 ID
|
||||
* GDRIVE_CLIENT_SECRET — OAuth 客户端密钥
|
||||
* GDRIVE_REFRESH_TOKEN — 长效刷新令牌
|
||||
*
|
||||
* 返回已认证的 google.drive 客户端实例
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
* 系统: SYS-GLW-0001
|
||||
* 主控: TCS-0002∞ 冰朔
|
||||
*/
|
||||
|
||||
const { google } = require('googleapis');
|
||||
|
||||
function getOAuth2Client() {
|
||||
const clientId = process.env.GDRIVE_CLIENT_ID;
|
||||
const clientSecret = process.env.GDRIVE_CLIENT_SECRET;
|
||||
const refreshToken = process.env.GDRIVE_REFRESH_TOKEN;
|
||||
|
||||
if (!clientId || !clientSecret || !refreshToken) {
|
||||
const missing = [];
|
||||
if (!clientId) missing.push('GDRIVE_CLIENT_ID');
|
||||
if (!clientSecret) missing.push('GDRIVE_CLIENT_SECRET');
|
||||
if (!refreshToken) missing.push('GDRIVE_REFRESH_TOKEN');
|
||||
throw new Error(`Missing OAuth2 environment variables: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret);
|
||||
|
||||
oauth2Client.setCredentials({
|
||||
refresh_token: refreshToken
|
||||
});
|
||||
|
||||
return oauth2Client;
|
||||
}
|
||||
|
||||
function getDriveClient() {
|
||||
const auth = getOAuth2Client();
|
||||
return google.drive({ version: 'v3', auth });
|
||||
}
|
||||
|
||||
module.exports = { getDriveClient, getOAuth2Client };
|
||||
|
|
@ -19,7 +19,9 @@
|
|||
* - 不同步 inbox/ 和 processing/(系统内部流转区)
|
||||
*
|
||||
* 环境变量:
|
||||
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥内容
|
||||
* - GDRIVE_CLIENT_ID: OAuth 客户端 ID
|
||||
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
|
||||
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
|
||||
* - DRIVE_FOLDER_ID: Drive 镜像根文件夹 ID
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
|
|
@ -169,48 +171,24 @@ async function uploadFile(drive, folderId, fileName, content) {
|
|||
*/
|
||||
async function main() {
|
||||
// 验证环境变量
|
||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
||||
const rootFolderId = process.env.DRIVE_FOLDER_ID;
|
||||
|
||||
if (!serviceAccountJson) {
|
||||
console.log('[sync-to-drive] GOOGLE_DRIVE_SERVICE_ACCOUNT not set, skipping');
|
||||
return;
|
||||
}
|
||||
if (!rootFolderId) {
|
||||
console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析 Service Account 凭证(天眼密钥流校验)
|
||||
let credentials;
|
||||
// OAuth2 认证(统一入口)
|
||||
let drive;
|
||||
try {
|
||||
const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator');
|
||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
||||
if (!validation.valid) {
|
||||
console.error('[sync-to-drive] 🔴 Credential validation failed:');
|
||||
console.error(formatDiagnosticReport(validation));
|
||||
process.exit(1);
|
||||
}
|
||||
credentials = validation.credentials;
|
||||
console.log('[sync-to-drive] ✅ Service account credentials validated');
|
||||
const { getDriveClient } = require('./drive-auth');
|
||||
drive = getDriveClient();
|
||||
console.log('[sync-to-drive] ✅ OAuth2 credentials configured');
|
||||
} catch (err) {
|
||||
// Fallback: if validator module is unavailable, try direct parse
|
||||
try {
|
||||
credentials = JSON.parse(serviceAccountJson);
|
||||
} catch (parseErr) {
|
||||
console.error('[sync-to-drive] Failed to parse service account JSON:', parseErr.message);
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(`[sync-to-drive] 🔴 OAuth2 auth failed: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 认证 Google Drive API
|
||||
const auth = new google.auth.GoogleAuth({
|
||||
credentials,
|
||||
scopes: ['https://www.googleapis.com/auth/drive']
|
||||
});
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
// 预生成 Drive 索引文件
|
||||
try {
|
||||
const { main: generateIndex } = require('./generate-drive-index');
|
||||
|
|
|
|||
|
|
@ -5,13 +5,15 @@
|
|||
* 天眼全域系统审计 (Sky-Eye Credential Audit)
|
||||
*
|
||||
* 功能:
|
||||
* ① 校验 GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性
|
||||
* ② 扫描所有依赖该密钥的工作流和脚本
|
||||
* ① 校验 OAuth2 凭据环境变量完整性
|
||||
* ② 扫描所有依赖 Drive 凭据的工作流和脚本
|
||||
* ③ 验证 YAML 语法(.github/workflows/ 下所有配置文件)
|
||||
* ④ 生成审计报告写入 System_Logs/
|
||||
*
|
||||
* 环境变量:
|
||||
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: (可选) 用于实际校验
|
||||
* - GDRIVE_CLIENT_ID: (可选) OAuth 客户端 ID
|
||||
* - GDRIVE_CLIENT_SECRET: (可选) OAuth 客户端密钥
|
||||
* - GDRIVE_REFRESH_TOKEN: (可选) 长效刷新令牌
|
||||
*
|
||||
* 用法:node scripts/skyeye/credential-audit.js
|
||||
*
|
||||
|
|
@ -24,7 +26,7 @@
|
|||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { validateServiceAccountJSON, formatDiagnosticReport } = require('./credential-validator');
|
||||
const { validateOAuth2Credentials, formatDiagnosticReport } = require('./credential-validator');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '../..');
|
||||
const WORKFLOWS_DIR = path.join(ROOT, '.github/workflows');
|
||||
|
|
@ -43,24 +45,20 @@ function getDateStr() {
|
|||
// ═══════════════════════════════════════════════
|
||||
|
||||
function auditCredential() {
|
||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
||||
const hasAnyVar = process.env.GDRIVE_CLIENT_ID || process.env.GDRIVE_CLIENT_SECRET || process.env.GDRIVE_REFRESH_TOKEN;
|
||||
|
||||
if (!serviceAccountJson) {
|
||||
if (!hasAnyVar) {
|
||||
return {
|
||||
status: 'skipped',
|
||||
reason: 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available in environment',
|
||||
recommendation: 'Run this audit in a GitHub Actions workflow with the secret configured'
|
||||
reason: 'OAuth2 credentials not available in environment (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)',
|
||||
recommendation: 'Run this audit in a GitHub Actions workflow with the secrets configured'
|
||||
};
|
||||
}
|
||||
|
||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
||||
const validation = validateOAuth2Credentials();
|
||||
return {
|
||||
status: validation.valid ? 'pass' : 'fail',
|
||||
diagnostics: {
|
||||
...validation.diagnostics,
|
||||
// Never expose credential values in reports
|
||||
credentials: undefined
|
||||
},
|
||||
diagnostics: validation.diagnostics,
|
||||
issues: validation.issues,
|
||||
report: formatDiagnosticReport(validation)
|
||||
};
|
||||
|
|
@ -77,7 +75,7 @@ function scanCredentialDependencies() {
|
|||
total_dependents: 0
|
||||
};
|
||||
|
||||
const SEARCH_PATTERN = 'GOOGLE_DRIVE_SERVICE_ACCOUNT';
|
||||
const SEARCH_PATTERN = 'GDRIVE_CLIENT_ID';
|
||||
|
||||
// 扫描 workflows
|
||||
if (fs.existsSync(WORKFLOWS_DIR)) {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
/**
|
||||
* scripts/skyeye/credential-validator.js
|
||||
*
|
||||
* 天眼密钥流完整性校验器 (Sky-Eye Credential Validator)
|
||||
* 天眼 OAuth2 凭据校验器 (Sky-Eye Credential Validator)
|
||||
*
|
||||
* 功能:对 Google Drive Service Account JSON 执行结构化检测
|
||||
* - JSON 语法完整性(检测未闭合 {} / 截断)
|
||||
* - 必需字段存在性验证
|
||||
* - 字段值格式校验(private_key PEM 格式等)
|
||||
* 功能:对 Google Drive OAuth2 环境变量执行存在性检测
|
||||
* - GDRIVE_CLIENT_ID 存在性
|
||||
* - GDRIVE_CLIENT_SECRET 存在性
|
||||
* - GDRIVE_REFRESH_TOKEN 存在性
|
||||
*
|
||||
* 可作为模块 require() 使用,也可直接运行:
|
||||
* GOOGLE_DRIVE_SERVICE_ACCOUNT='...' node credential-validator.js
|
||||
* GDRIVE_CLIENT_ID='...' GDRIVE_CLIENT_SECRET='...' GDRIVE_REFRESH_TOKEN='...' node credential-validator.js
|
||||
*
|
||||
* 守护: PER-ZY001 铸渊
|
||||
* 系统: SYS-GLW-0001
|
||||
|
|
@ -19,147 +19,46 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
// Google Service Account JSON 必需字段
|
||||
const REQUIRED_FIELDS = [
|
||||
'type',
|
||||
'project_id',
|
||||
'private_key_id',
|
||||
'private_key',
|
||||
'client_email',
|
||||
'client_id',
|
||||
'auth_uri',
|
||||
'token_uri'
|
||||
// OAuth2 必需环境变量
|
||||
const REQUIRED_OAUTH2_VARS = [
|
||||
'GDRIVE_CLIENT_ID',
|
||||
'GDRIVE_CLIENT_SECRET',
|
||||
'GDRIVE_REFRESH_TOKEN'
|
||||
];
|
||||
|
||||
/**
|
||||
* 验证 Service Account JSON 字符串
|
||||
* @param {string} jsonStr - JSON 字符串
|
||||
* @returns {{ valid: boolean, credentials: object|null, issues: string[], diagnostics: object }}
|
||||
* 验证 OAuth2 环境变量是否齐全
|
||||
* @returns {{ valid: boolean, issues: string[], diagnostics: object }}
|
||||
*/
|
||||
function validateServiceAccountJSON(jsonStr) {
|
||||
function validateOAuth2Credentials() {
|
||||
const result = {
|
||||
valid: false,
|
||||
credentials: null,
|
||||
issues: [],
|
||||
diagnostics: {
|
||||
input_length: 0,
|
||||
is_empty: true,
|
||||
json_parseable: false,
|
||||
is_object: false,
|
||||
has_all_required_fields: false,
|
||||
missing_fields: [],
|
||||
field_checks: {},
|
||||
truncation_suspected: false
|
||||
auth_mode: 'oauth2',
|
||||
vars_checked: REQUIRED_OAUTH2_VARS.length,
|
||||
vars_present: 0,
|
||||
missing_vars: []
|
||||
}
|
||||
};
|
||||
|
||||
// ① 空值检查
|
||||
if (!jsonStr || typeof jsonStr !== 'string') {
|
||||
result.issues.push('Credential string is empty or not a string');
|
||||
return result;
|
||||
}
|
||||
|
||||
const trimmed = jsonStr.trim();
|
||||
result.diagnostics.input_length = trimmed.length;
|
||||
result.diagnostics.is_empty = trimmed.length === 0;
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
result.issues.push('Credential string is empty after trimming');
|
||||
return result;
|
||||
}
|
||||
|
||||
// ② 基本结构检查(闭合括号)
|
||||
if (!trimmed.startsWith('{')) {
|
||||
result.issues.push(`JSON does not start with '{' (starts with '${trimmed[0]}')`);
|
||||
}
|
||||
if (!trimmed.endsWith('}')) {
|
||||
result.issues.push(`JSON does not end with '}' (ends with '${trimmed[trimmed.length - 1]}')`);
|
||||
result.diagnostics.truncation_suspected = true;
|
||||
}
|
||||
|
||||
// 检查大括号平衡
|
||||
let braceDepth = 0;
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
if (trimmed[i] === '{') braceDepth++;
|
||||
else if (trimmed[i] === '}') braceDepth--;
|
||||
}
|
||||
if (braceDepth !== 0) {
|
||||
result.issues.push(`Unbalanced braces: depth=${braceDepth} (${braceDepth > 0 ? 'missing closing }' : 'extra closing }'})`);
|
||||
result.diagnostics.truncation_suspected = braceDepth > 0;
|
||||
}
|
||||
|
||||
// ③ JSON 解析
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
result.diagnostics.json_parseable = true;
|
||||
} catch (err) {
|
||||
result.issues.push(`JSON parse error: ${err.message}`);
|
||||
// 尝试诊断截断位置
|
||||
const posMatch = err.message.match(/position (\d+)/);
|
||||
if (posMatch) {
|
||||
const pos = parseInt(posMatch[1], 10);
|
||||
result.diagnostics.error_position = pos;
|
||||
result.diagnostics.context_at_error = trimmed.substring(
|
||||
Math.max(0, pos - 30),
|
||||
Math.min(trimmed.length, pos + 30)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ④ 类型检查
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
result.issues.push('Parsed JSON is not a plain object');
|
||||
return result;
|
||||
}
|
||||
|
||||
result.diagnostics.is_object = true;
|
||||
result.credentials = parsed;
|
||||
|
||||
// ⑤ 必需字段检查
|
||||
for (const field of REQUIRED_FIELDS) {
|
||||
if (!(field in parsed) || parsed[field] === undefined || parsed[field] === null || parsed[field] === '') {
|
||||
result.diagnostics.missing_fields.push(field);
|
||||
result.issues.push(`Missing or empty required field: '${field}'`);
|
||||
for (const varName of REQUIRED_OAUTH2_VARS) {
|
||||
const val = process.env[varName];
|
||||
if (!val || val.trim().length === 0) {
|
||||
result.diagnostics.missing_vars.push(varName);
|
||||
result.issues.push(`Missing or empty environment variable: ${varName}`);
|
||||
} else {
|
||||
result.diagnostics.vars_present++;
|
||||
}
|
||||
}
|
||||
|
||||
result.diagnostics.has_all_required_fields = result.diagnostics.missing_fields.length === 0;
|
||||
|
||||
// ⑥ 字段格式校验
|
||||
if (parsed.type) {
|
||||
const typeOk = parsed.type === 'service_account';
|
||||
result.diagnostics.field_checks.type = typeOk ? 'ok' : `expected 'service_account', got '${parsed.type}'`;
|
||||
if (!typeOk) result.issues.push(`Field 'type' should be 'service_account', got '${parsed.type}'`);
|
||||
}
|
||||
|
||||
if (parsed.private_key) {
|
||||
const pkStr = String(parsed.private_key);
|
||||
const hasBegin = pkStr.includes('-----BEGIN');
|
||||
const hasEnd = pkStr.includes('-----END');
|
||||
result.diagnostics.field_checks.private_key_format = hasBegin && hasEnd ? 'ok' : 'PEM markers missing';
|
||||
if (!hasBegin || !hasEnd) {
|
||||
result.issues.push('Field \'private_key\' does not contain valid PEM BEGIN/END markers');
|
||||
result.diagnostics.truncation_suspected = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.client_email) {
|
||||
const emailStr = String(parsed.client_email);
|
||||
const emailOk = /^[^@]+@[^@]+\.iam\.gserviceaccount\.com$/.test(emailStr);
|
||||
result.diagnostics.field_checks.client_email_format = emailOk ? 'ok' : 'not a valid service account email';
|
||||
if (!emailOk) result.issues.push('Field \'client_email\' does not match service account email pattern');
|
||||
}
|
||||
|
||||
// ⑦ 最终判定
|
||||
result.valid = result.issues.length === 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成人类可读的诊断报告
|
||||
* @param {object} validationResult - validateServiceAccountJSON 的返回值
|
||||
* @param {object} validationResult - validateOAuth2Credentials 的返回值
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDiagnosticReport(validationResult) {
|
||||
|
|
@ -167,31 +66,15 @@ function formatDiagnosticReport(validationResult) {
|
|||
const lines = [];
|
||||
|
||||
lines.push('╔════════════════════════════════════════════════════════╗');
|
||||
lines.push('║ 🛰️ 天眼密钥流完整性校验报告 (Credential Audit) ║');
|
||||
lines.push('║ 🛰️ 天眼 OAuth2 凭据校验报告 (Credential Audit) ║');
|
||||
lines.push('╚════════════════════════════════════════════════════════╝');
|
||||
lines.push('');
|
||||
lines.push(`状态: ${r.valid ? '✅ 绿色信号 (PASS)' : '🔴 逻辑红区 (FAIL)'}`);
|
||||
lines.push(`输入长度: ${r.diagnostics.input_length} bytes`);
|
||||
lines.push(`JSON 可解析: ${r.diagnostics.json_parseable ? '是' : '否'}`);
|
||||
lines.push(`是否为对象: ${r.diagnostics.is_object ? '是' : '否'}`);
|
||||
lines.push(`全部必需字段: ${r.diagnostics.has_all_required_fields ? '齐全' : '缺失'}`);
|
||||
lines.push(`截断嫌疑: ${r.diagnostics.truncation_suspected ? '⚠️ 是' : '否'}`);
|
||||
lines.push(`认证模式: OAuth2 代理人`);
|
||||
lines.push(`环境变量检查: ${r.diagnostics.vars_present}/${r.diagnostics.vars_checked} 齐全`);
|
||||
|
||||
if (r.diagnostics.missing_fields.length > 0) {
|
||||
lines.push(`缺失字段: ${r.diagnostics.missing_fields.join(', ')}`);
|
||||
}
|
||||
|
||||
if (r.diagnostics.error_position !== undefined) {
|
||||
lines.push(`错误位置: position ${r.diagnostics.error_position}`);
|
||||
lines.push(`上下文: ...${r.diagnostics.context_at_error}...`);
|
||||
}
|
||||
|
||||
if (Object.keys(r.diagnostics.field_checks).length > 0) {
|
||||
lines.push('');
|
||||
lines.push('字段检查:');
|
||||
for (const [k, v] of Object.entries(r.diagnostics.field_checks)) {
|
||||
lines.push(` ${k}: ${v}`);
|
||||
}
|
||||
if (r.diagnostics.missing_vars.length > 0) {
|
||||
lines.push(`缺失变量: ${r.diagnostics.missing_vars.join(', ')}`);
|
||||
}
|
||||
|
||||
if (r.issues.length > 0) {
|
||||
|
|
@ -209,26 +92,18 @@ function formatDiagnosticReport(validationResult) {
|
|||
// 模块导出
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
module.exports = { validateServiceAccountJSON, formatDiagnosticReport, REQUIRED_FIELDS };
|
||||
module.exports = { validateOAuth2Credentials, formatDiagnosticReport, REQUIRED_OAUTH2_VARS };
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// CLI 模式
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
if (require.main === module) {
|
||||
const jsonStr = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
||||
if (!jsonStr) {
|
||||
console.error('[credential-validator] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = validateServiceAccountJSON(jsonStr);
|
||||
const result = validateOAuth2Credentials();
|
||||
console.log(formatDiagnosticReport(result));
|
||||
console.log('');
|
||||
console.log('---CREDENTIAL_AUDIT_JSON---');
|
||||
// 不输出 credentials 本身以避免泄露密钥
|
||||
const safeResult = { ...result, credentials: result.credentials ? '[REDACTED]' : null };
|
||||
console.log(JSON.stringify(safeResult, null, 2));
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
process.exit(result.valid ? 0 : 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
* 完成后在 Issue 下回复文档/表格直连 ID。
|
||||
*
|
||||
* 环境变量:
|
||||
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥
|
||||
* - GDRIVE_CLIENT_ID: OAuth 客户端 ID
|
||||
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
|
||||
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
|
||||
* - DRIVE_LANDING_FOLDER_ID: 目标 Drive 文件夹 ID
|
||||
* - GITHUB_TOKEN: GitHub API 令牌
|
||||
* - ISSUE_NUMBER: Issue 编号
|
||||
|
|
@ -36,35 +38,12 @@ const TAG_TABLE = '[TCS-TABLE]';
|
|||
const LANDING_SUBFOLDER = 'tcs-semantic-landing';
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Google API 认证
|
||||
// Google API 认证(OAuth2 代理人模式)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
function buildAuth(serviceAccountJson) {
|
||||
let credentials;
|
||||
try {
|
||||
const { validateServiceAccountJSON, formatDiagnosticReport } = require('./skyeye/credential-validator');
|
||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
||||
if (!validation.valid) {
|
||||
console.error('[semantic-landing] 🔴 Credential validation failed:');
|
||||
console.error(formatDiagnosticReport(validation));
|
||||
throw new Error('Service account credential validation failed');
|
||||
}
|
||||
credentials = validation.credentials;
|
||||
console.log('[semantic-landing] ✅ Service account credentials validated');
|
||||
} catch (err) {
|
||||
if (err.message === 'Service account credential validation failed') throw err;
|
||||
// Fallback: validator module unavailable, try direct parse
|
||||
console.log('[semantic-landing] Validator unavailable, using direct JSON.parse');
|
||||
credentials = JSON.parse(serviceAccountJson);
|
||||
}
|
||||
return new google.auth.GoogleAuth({
|
||||
credentials,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/documents',
|
||||
'https://www.googleapis.com/auth/spreadsheets'
|
||||
]
|
||||
});
|
||||
function buildAuth() {
|
||||
const { getOAuth2Client } = require('./grid-db/drive-auth');
|
||||
return getOAuth2Client();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
|
|
@ -465,7 +444,6 @@ function postIssueComment(token, repo, issueNumber, body) {
|
|||
// ═══════════════════════════════════════════════
|
||||
|
||||
async function main() {
|
||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
||||
const rootFolderId = process.env.DRIVE_LANDING_FOLDER_ID;
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
const issueNumber = process.env.ISSUE_NUMBER;
|
||||
|
|
@ -474,10 +452,6 @@ async function main() {
|
|||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
|
||||
// 验证必需环境变量
|
||||
if (!serviceAccountJson) {
|
||||
console.error('[semantic-landing] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!rootFolderId) {
|
||||
console.error('[semantic-landing] DRIVE_LANDING_FOLDER_ID not set');
|
||||
process.exit(1);
|
||||
|
|
@ -499,9 +473,10 @@ async function main() {
|
|||
console.log(`[semantic-landing] Processing Issue #${issueNumber}: ${issueTitle}`);
|
||||
console.log(`[semantic-landing] Type: ${isMemo ? 'MEMO (Google Docs)' : 'TABLE (Google Sheets)'}`);
|
||||
|
||||
// 初始化 Google API
|
||||
const auth = buildAuth(serviceAccountJson);
|
||||
// 初始化 Google API(OAuth2 代理人模式)
|
||||
const auth = buildAuth();
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
console.log('[semantic-landing] ✅ OAuth2 credentials configured');
|
||||
|
||||
// 确保子文件夹存在
|
||||
const landingFolderId = await getOrCreateFolder(drive, rootFolderId, LANDING_SUBFOLDER);
|
||||
|
|
|
|||
|
|
@ -105,6 +105,46 @@
|
|||
"weekly_hibernation_minutes": 15,
|
||||
"monthly_budget_minutes": 210,
|
||||
"comment": "日休眠 30天×5min=150 + 周休眠 4×15min=60 ≈ 210 min/month"
|
||||
},
|
||||
"lifeline_tasks": [
|
||||
{
|
||||
"id": "TOKEN-RENEW",
|
||||
"name": "OAuth2 Token 自动续期",
|
||||
"workflow": "renew-gdrive-tokens.yml",
|
||||
"hibernate_exempt": true,
|
||||
"priority": "P0",
|
||||
"alert_on_failure": true,
|
||||
"alert_channels": ["notion-dashboard", "agent-bulletin"]
|
||||
},
|
||||
{
|
||||
"id": "TOKEN-HEALTH",
|
||||
"name": "Token 健康检查",
|
||||
"workflow": "check-token-health.yml",
|
||||
"hibernate_exempt": true,
|
||||
"priority": "P0",
|
||||
"alert_on_failure": true,
|
||||
"alert_channels": ["notion-dashboard", "agent-bulletin"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gdrive_oauth2": {
|
||||
"service": "Google Drive OAuth2 Token Management",
|
||||
"plan": "Internal",
|
||||
"description": "OAuth2 Token 自动续期引擎 · 6天刷新 + 三层保险 + 天眼生命线豁免",
|
||||
"registry": "config/gdrive-tokens.json",
|
||||
"scripts": {
|
||||
"renew_tokens": "scripts/gdrive/renew-tokens.js",
|
||||
"check_hibernation": "scripts/gdrive/check-hibernation.js",
|
||||
"push_alerts": "scripts/gdrive/push-token-alerts.js"
|
||||
},
|
||||
"workflows": {
|
||||
"renew": "renew-gdrive-tokens.yml",
|
||||
"health_check": "check-token-health.yml"
|
||||
},
|
||||
"schedule": {
|
||||
"renew_a": "每周一 10:00 CST (02:00 UTC)",
|
||||
"renew_b": "每周四 10:00 CST (02:00 UTC)",
|
||||
"health_check": "每天 12:15 CST (04:15 UTC)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,27 +199,27 @@ function ensureDirectories() {
|
|||
}
|
||||
|
||||
/**
|
||||
* 天眼密钥流校验 — 验证 GOOGLE_DRIVE_SERVICE_ACCOUNT 环境变量
|
||||
* 天眼 OAuth2 凭据校验 — 验证 GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
|
||||
* 仅在环境变量可用时执行(CI 环境)
|
||||
*/
|
||||
function validateCredentials() {
|
||||
const result = { status: 'skipped', issues: [] };
|
||||
|
||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
||||
if (!serviceAccountJson) {
|
||||
result.reason = 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available';
|
||||
const hasAnyVar = process.env.GDRIVE_CLIENT_ID || process.env.GDRIVE_CLIENT_SECRET || process.env.GDRIVE_REFRESH_TOKEN;
|
||||
if (!hasAnyVar) {
|
||||
result.reason = 'OAuth2 credentials not available (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)';
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const { validateServiceAccountJSON } = require('../../scripts/skyeye/credential-validator');
|
||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
||||
const { validateOAuth2Credentials } = require('../../scripts/skyeye/credential-validator');
|
||||
const validation = validateOAuth2Credentials();
|
||||
result.status = validation.valid ? 'pass' : 'fail';
|
||||
result.issues = validation.issues;
|
||||
if (!validation.valid) {
|
||||
console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`);
|
||||
} else {
|
||||
console.log(' [CREDENTIAL] ✅ Service account credentials valid');
|
||||
console.log(' [CREDENTIAL] ✅ OAuth2 credentials valid');
|
||||
}
|
||||
} catch (e) {
|
||||
result.status = 'error';
|
||||
|
|
|
|||
Loading…
Reference in New Issue