---
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