feat: OAuth2 Token 自动续期引擎 + 天眼生命线任务豁免 + 多用户Token管理 · ZY-TOKEN-RENEW-2026-0324-001 [skip ci]

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/bc1b54c6-5276-4d00-8ea5-5e8c3c2bef38
This commit is contained in:
copilot-swe-agent[bot] 2026-03-24 07:14:28 +00:00
parent 4422b12805
commit 0d842e52df
10 changed files with 622 additions and 0 deletions

View File

@ -1095,6 +1095,32 @@
"daily_checkin_required": false, "daily_checkin_required": false,
"parent_sys": "SYS-GLW-0001", "parent_sys": "SYS-GLW-0001",
"owner": "ICE-0002∞" "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开始顺序分配", "note": "铸渊已扫描所有workflow文件按此格式逐一注册编号从AG-ZY-001开始顺序分配",

View File

@ -0,0 +1,73 @@
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 🛡️ 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 grid-db/logs/
git diff --cached --quiet || git commit -m "auto: Token health check $(date -u +%Y-%m-%dT%H:%M:%SZ) [skip ci]"
git push

View File

@ -0,0 +1,69 @@
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 🔄 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 grid-db/logs/
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 "需要人类手动重新授权。"

26
config/gdrive-tokens.json Normal file
View File

@ -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": []
}

View File

View File

View File

@ -0,0 +1,39 @@
/**
* check-hibernation.js
*
* 检查当前是否在天眼休眠期内
*
* 休眠规则从天眼治理层配置读取
* - 周休眠每周六 20:00 ~ 00:00 CST4小时
* - 日休眠每日 04:00 ~ 04:10 CST默认窗口
*
* 对于生命线任务输出提示但不阻止执行
* 对于普通任务输出提示并建议延迟
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
* 主控: TCS-0002 冰朔
*/
function isInHibernation() {
const now = new Date();
// 转换为北京时间
const cst = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' }));
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 };

View File

@ -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();

View File

@ -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
console.log(` ⚠️ Google 未返回新 refresh_tokenToken 可能仍然有效但无法续期`);
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 response from Google: ${data.substring(0, 200)}`));
}
});
});
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;
});

View File

@ -105,6 +105,46 @@
"weekly_hibernation_minutes": 15, "weekly_hibernation_minutes": 15,
"monthly_budget_minutes": 210, "monthly_budget_minutes": 210,
"comment": "日休眠 30天×5min=150 + 周休眠 4×15min=60 ≈ 210 min/month" "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)"
} }
} }
} }