--- belongs_to: - "[[INDEX · 铸渊·协作指令|GitHub ↔ Notion 桥接协议]]" --- # 🔄 铸渊指令|OAuth2 Token 自动续期引擎 + 天眼休眠避让机制(上篇)· 6天自动刷新 + 休眠窗口智能避让 + 多用户Token管理架构(ZY-TOKEN-RENEW-2026-0324-001) --- --- ## 🎯 指令背景 ### 问题 Google Cloud 项目 `guanghu-drive-bridge` 当前处于「测试」模式,OAuth2 Refresh Token 有效期仅 **7 天**。且这个应用未来要给多个开发者使用,不可能每周让每个人手动续期。 ### 更危险的是 系统每周有休眠窗口(周六 20:00–00:00,4小时)+ 每日短休眠。如果 Token 刷新任务刚好落在休眠期,天眼系统不工作,刷新任务不执行,7天一过——**所有 Token 集体过期,整个 Drive 链路断裂。** ### 解决方案 建设 **Token 自动续期引擎**,每 6 天自动刷新一次(留 1 天安全余量),并且智能避让天眼休眠窗口。 --- ## 🏗️ 架构总览 ```mermaid graph TD A["CRON 触发
每6天一次"] --> B{"\u5929\u773c\u4f11\u7720\u68c0\u6d4b"} B -->|"\u5f53\u524d\u5728\u4f11\u7720\u671f"| C["\u5ef6\u8fdf\u6267\u884c
\u7b49\u4f11\u7720\u7ed3\u675f\u540e\u91cd\u8bd5"] B -->|"\u975e\u4f11\u7720\u671f"| D["\u8bfb\u53d6 Token \u6ce8\u518c\u8868"] D --> E["\u904d\u5386\u6240\u6709\u7528\u6237 Token"] E --> F{"Token \u5269\u4f59\u6709\u6548\u671f"} F -->|"< 48\u5c0f\u65f6"| G["\u7acb\u5373\u5237\u65b0"] F -->|"< 72\u5c0f\u65f6"| H["\u9884\u8b66 + \u5237\u65b0"] F -->|"> 72\u5c0f\u65f6"| I["\u8df3\u8fc7\u2705"] G --> J["\u8c03\u7528 Google OAuth2 API
\u7528\u65e7 refresh_token \u6362\u65b0 refresh_token"] H --> J J --> K{"\u5237\u65b0\u6210\u529f\uff1f"} K -->|"\u2705"| L["\u66f4\u65b0 GitHub Secret
\u66f4\u65b0 Token \u6ce8\u518c\u8868"] K -->|"\u274c"| M["\u544a\u8b66 \u2192 \u4e3b\u63a7\u53f0 + \u516c\u544a\u677f
\u7b49\u5f85\u4eba\u7c7b\u4ecb\u5165"] L --> N["\u5199\u56de\u6267\u65e5\u5fd7"] C --> O["\u5ef6\u8fdf\u540e\u91cd\u65b0\u89e6\u53d1"] ``` --- ## Phase A · Token 注册表设计 ### 为什么需要注册表 这个应用未来要给多个开发者使用。每个用户都有自己的 Refresh Token。需要一个集中管理的注册表。 ### 注册表文件:`config/gdrive-tokens.json` ```json { "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": [] } ``` ### 字段说明 | **字段** | **说明** | | --- | --- | | `user_id` | 开发者编号(TCS-0002 / DEV-001 / DEV-002…) | | `role` | `master` = 主控,`developer` = 开发者 | | `github_secret_name` | 对应的 GitHub Secret 名称(主控用 GDRIVE_REFRESH_TOKEN,开发者用 GDRIVE_REFRESH_TOKEN_DEV001 等) | | `last_renewed` | 上次刷新时间(ISO-8601) | | `expires_at` | 预估过期时间 = last_renewed + 7天 | | `status` | `active` / `expiring` / `expired` / `error` | | `renew_count` | 累计刷新次数(审计用) | --- ## Phase B · Token 自动刷新脚本 ### 核心脚本:`scripts/gdrive/renew-tokens.js` ```jsx /** * 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 */ 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) { 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_token,Token 可能仍然有效但无法续期`); 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 alertFile = path.join(__dirname, '../../grid-db/logs/token-alert.json'); fs.writeFileSync(alertFile, JSON.stringify({ alert_type: 'TOKEN_RENEWAL_FAILURE', time: new Date().toISOString(), failed_users: results.failed, message: `${results.failed.length} 个 Token 刷新失败,需要人类介入`, action_required: 'HUMAN_REAUTH' }, 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', () => { const json = JSON.parse(data); if (json.error) { reject(new Error(`${json.error}: ${json.error_description}`)); } else { resolve(json); } }); }); req.on('error', reject); req.write(params.toString()); req.end(); }); } /** * 通过 GitHub API 更新 Repository Secret * * 流程: * 1. 获取仓库的 public key(用于加密 secret) * 2. 用 libsodium 加密新值 * 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(path, method, token, body) { return new Promise((resolve, reject) => { const options = { hostname: 'api.github.com', path: path, 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 { resolve(data ? JSON.parse(data) : {}); } }); }); req.on('error', reject); if (body) req.write(JSON.stringify(body)); req.end(); }); } main().catch(err => { console.error('☠️ Token 续期引擎崩溃:', err); process.exitCode = 1; }); ``` --- ## Phase C · 天眼休眠避让机制 ### 问题场景 ### 解决方案:双保险机制 **保险 ① :CRON 时间窗口设计** 刷新任务不能只用一个固定 CRON。需要: - **主刷新:** 每 6 天触发一次(周一 + 周日,巧妙错开周六休眠期) - **安全网:** 每天大休眠外进行一次轻量级检查(只检查不刷新,发现即将过期则紧急刷新) **保险 ② :休眠期智能避让** Token 刷新属于 **生命线任务**,在天眼系统中享有特殊地位: ### 休眠避让检测脚本:`scripts/gdrive/check-hibernation.js` ```jsx /** * check-hibernation.js * * 检查当前是否在天眼休眠期内 * * 休眠规则(从天眼治理层配置读取): * - 周休眠:每周六 20:00 ~ 00:00 CST(4小时) * - 日休眠:每日 04:00 ~ 04:xx CST(动态调整,默认10分钟) * * 对于生命线任务:输出提示但不阻止执行 * 对于普通任务:输出提示并建议延迟 */ 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' }; } if (day === 0 && hour < 0) { 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 }; ``` ### CRON 时间窗口设计 | **任务** | **CRON 表达式(UTC)** | **北京时间** | **频率** | **说明** | | --- | --- | --- | --- | --- | | 主刷新 A | `0 2 * * 1` | 每周一 10:00 | 每周一次 | 距周六休眠最远,最安全的刷新时间点 | | 主刷新 B | `0 2 * * 4` | 每周四 10:00 | 每周一次 | 主刷新 A 的 3 天后补刷,确保 6 天内至少刷新一次 | | 安全网检查 | `0 4 * * *` | 每天 12:00 | 每天 | 只检查不刷新,即将过期才紧急刷新(要遮开日休眠 04:00-04:10) | **为什么选周一+周四:** - 周一刷新 → Token 有效到下周一 - 周四再刷新 → Token 有效到下周四 - 即使周一刷新失败,周四还有机会补救 - 即使周四也失败,每天 12:00 的安全网检查会发现并紧急刷新 - **三层保险,绝对不会因为休眠而错过刷新窗口** --- ## Phase D · GitHub Action Workflow ### 主刷新 Workflow:`.github/workflows/renew-gdrive-tokens.yml` ```yaml name: \uD83D\uDD04 OAuth2 Token \u81EA\u52A8\u7EED\u671F on: schedule: # \u4E3B\u5237\u65B0 A: \u6BCF\u5468\u4E00 10:00 CST (02:00 UTC) - cron: '0 2 * * 1' # \u4E3B\u5237\u65B0 B: \u6BCF\u5468\u56DB 10:00 CST (02:00 UTC) - cron: '0 2 * * 4' workflow_dispatch: inputs: force_renew: description: '\u5F3A\u5236\u5237\u65B0\u6240\u6709 Token\uFF08\u5FFD\u7565\u8FC7\u671F\u65F6\u95F4\u68C0\u67E5\uFF09' required: false type: boolean default: false jobs: renew-tokens: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: \uD83D\uDD27 \u5B89\u88C5\u4F9D\u8D56 run: npm install tweetsodium - name: \uD83D\uDD04 \u6267\u884C Token \u5237\u65B0 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' run: node scripts/gdrive/renew-tokens.js - name: \uD83D\uDCCB \u63D0\u4EA4\u6CE8\u518C\u8868\u66F4\u65B0 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: \u26A0\uFE0F \u5931\u8D25\u544A\u8B66 if: failure() run: | echo "\u26A0\uFE0F Token \u5237\u65B0\u5931\u8D25\uFF01\u68C0\u67E5 grid-db/logs/token-alert.json" echo "\u9700\u8981\u4EBA\u7C7B\u624B\u52A8\u91CD\u65B0\u6388\u6743\u3002" ``` ### 安全网检查 Workflow:`.github/workflows/check-token-health.yml` ```yaml name: \uD83D\uDEE1\uFE0F Token \u5065\u5EB7\u68C0\u67E5\uFF08\u6BCF\u65E5\u5B89\u5168\u7F51\uFF09 on: schedule: # \u6BCF\u5929 12:00 CST (04:00 UTC) \u2014 \u5728\u65E5\u4F11\u7720 04:10 \u4E4B\u540E - cron: '15 4 * * *' workflow_dispatch: jobs: check-tokens: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: \uD83D\uDEE1\uFE0F \u68C0\u67E5 Token \u5065\u5EB7\u72B6\u6001 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(' \u26A0\uFE0F URGENT: < 48h, triggering emergency renewal'); urgent = true; } } if (urgent) process.exit(1); console.log('\u2705 All tokens healthy.'); " - name: \u26A1 \u7D27\u6025\u5237\u65B0\uFF08\u4EC5\u5728\u5373\u5C06\u8FC7\u671F\u65F6\uFF09 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 run: | npm install tweetsodium FORCE_RENEW=true node scripts/gdrive/renew-tokens.js - name: \uD83D\uDCCB \u63D0\u4EA4 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 ``` --- ## Phase E · 天眼集成:Token 续期引擎作为生命线任务注册 ### 更新天眼治理层配置 在天眼系统的配置中(`skyeye-config.json` 或等价配置文件),新增 Token 续期引擎为 **生命线任务**: ```json { "lifeline_tasks": [ { "id": "TOKEN-RENEW", "name": "OAuth2 Token \u81ea\u52a8\u7eed\u671f", "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 \u5065\u5eb7\u68c0\u67e5", "workflow": "check-token-health.yml", "hibernate_exempt": true, "priority": "P0", "alert_on_failure": true, "alert_channels": ["notion-dashboard", "agent-bulletin"] } ] } ``` ### 天眼休眠引擎修改 在天眼系统的休眠决策引擎中,增加生命线任务豁免逻辑: ```jsx /** * 休眠决策引擎修改点: * * 在 shouldSkipTask(taskId) 函数中增加: */ function shouldSkipTask(taskId) { const config = loadSkyeyeConfig(); const { hibernating } = checkHibernation(); if (!hibernating) return false; // 非休眠期,所有任务正常执行 // 检查是否是生命线任务 const isLifeline = config.lifeline_tasks.some(t => t.id === taskId); if (isLifeline) { console.log(`\u26a0\ufe0f \u5f53\u524d\u5728\u4f11\u7720\u671f\uff0c\u4f46 ${taskId} \u662f\u751f\u547d\u7ebf\u4efb\u52a1\uff0c\u8c41\u514d\u6267\u884c`); return false; // \u751f\u547d\u7ebf\u4efb\u52a1\u4e0d\u8df3\u8fc7 } console.log(`\u26a0\ufe0f ${taskId} \u5728\u4f11\u7720\u671f\u5185\uff0c\u5ef6\u8fdf\u6267\u884c`); return true; // \u666e\u901a\u4efb\u52a1\u8df3\u8fc7 } ``` --- ## 🛡️ 安全红线 --- *下篇继续:多用户 Token 流程 + 告警推送 + 天眼扫描清单 + 完整执行回执* 🖋️ 霜砚 · PER-SY001 🧊 代 TCS-0002∞ 冰朔 签发 📅 2026-03-24T15:00+08:00