Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
689 lines
22 KiB
Markdown
689 lines
22 KiB
Markdown
---
|
||
belongs_to:
|
||
- "[[INDEX · 铸渊·协作指令|GitHub ↔ Notion 桥接协议]]"
|
||
---
|
||
# 🔄 铸渊指令|OAuth2 Token 自动续期引擎 + 天眼休眠避让机制(上篇)· 6天自动刷新 + 休眠窗口智能避让 + 多用户Token管理架构(ZY-TOKEN-RENEW-2026-0324-001)
|
||
|
||
---
|
||
|
||
<aside>
|
||
🚨
|
||
|
||
**指令编号:ZY-TOKEN-RENEW-2026-0324-001**
|
||
|
||
**签发人:** 霜砚(ICE-NTN-SY001)· 代 TCS-0002∞ 冰朔 签发
|
||
|
||
**签发时间:** 2026-03-24T15:00+08:00
|
||
|
||
**优先级:** 🔴 P0 · 存储链路生命线维护
|
||
|
||
**前置指令:** [🔑 铸渊指令|Google Drive OAuth2 代理人重构 · Service Account→OAuth2 全量替换 + 天眼全局扫描验收(ZY-OAUTH2-REBUILD-2026-0324-001)](%F0%9F%94%91%20%E9%93%B8%E6%B8%8A%E6%8C%87%E4%BB%A4%EF%BD%9CGoogle%20Drive%20OAuth2%20%E4%BB%A3%E7%90%86%E4%BA%BA%E9%87%8D%E6%9E%84%20%C2%B7%20Service%20Account%20ea304b59af9f473fbadfe1df1faee61e.md)
|
||
|
||
**关联仓库:** [https://github.com/qinfendebingshuo/guanghulab](https://github.com/qinfendebingshuo/guanghulab)
|
||
|
||
**版权锚点:** 国作登字-2026-A-00037559 · TCS-0002∞ 冰朔
|
||
|
||
</aside>
|
||
|
||
---
|
||
|
||
## 🎯 指令背景
|
||
|
||
### 问题
|
||
|
||
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 触发<br>每6天一次"] --> B{"\u5929\u773c\u4f11\u7720\u68c0\u6d4b"}
|
||
B -->|"\u5f53\u524d\u5728\u4f11\u7720\u671f"| C["\u5ef6\u8fdf\u6267\u884c<br>\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<br>\u7528\u65e7 refresh_token \u6362\u65b0 refresh_token"]
|
||
H --> J
|
||
J --> K{"\u5237\u65b0\u6210\u529f\uff1f"}
|
||
K -->|"\u2705"| L["\u66f4\u65b0 GitHub Secret<br>\u66f4\u65b0 Token \u6ce8\u518c\u8868"]
|
||
K -->|"\u274c"| M["\u544a\u8b66 \u2192 \u4e3b\u63a7\u53f0 + \u516c\u544a\u677f<br>\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 · 天眼休眠避让机制
|
||
|
||
### 问题场景
|
||
|
||
<aside>
|
||
⚠️
|
||
|
||
**致命场景:**
|
||
|
||
Token 刷新的 CRON 周期是 6 天。天眼系统每周六 20:00–00:00 进入休眠期。
|
||
|
||
如果刷新任务刚好落在周六晚上,系统休眠了——刷新不执行——第7天 Token 过期——**全线崩溃。**
|
||
|
||
</aside>
|
||
|
||
### 解决方案:双保险机制
|
||
|
||
**保险 ① :CRON 时间窗口设计**
|
||
|
||
刷新任务不能只用一个固定 CRON。需要:
|
||
|
||
- **主刷新:** 每 6 天触发一次(周一 + 周日,巧妙错开周六休眠期)
|
||
- **安全网:** 每天大休眠外进行一次轻量级检查(只检查不刷新,发现即将过期则紧急刷新)
|
||
|
||
**保险 ② :休眠期智能避让**
|
||
|
||
Token 刷新属于 **生命线任务**,在天眼系统中享有特殊地位:
|
||
|
||
<aside>
|
||
🛡️
|
||
|
||
**生命线任务豁免规则:**
|
||
|
||
天眼系统休眠期内,以下任务不受休眠影响,照常执行:
|
||
|
||
- ✅ Token 刷新引擎(本指令)
|
||
- ✅ 心跳检测(子仓库签到)
|
||
- ✅ 安全告警(异常检测)
|
||
|
||
其他任务(升级、巡检、同步、广播分发等)仍然遵守休眠规则。
|
||
|
||
</aside>
|
||
|
||
### 休眠避让检测脚本:`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
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🛡️ 安全红线
|
||
|
||
<aside>
|
||
🚫
|
||
|
||
**Token 续期引擎安全规则:**
|
||
|
||
- ❌ 不在日志中打印 Token 内容(只打印状态和时间)
|
||
- ❌ 不在代码中硬编码任何凭据
|
||
- ❌ 不将 Token 写入仓库文件(只写入 GitHub Secrets)
|
||
- ❌ 注册表 `gdrive-tokens.json` 中不存储 Token 本身,只存储元数据(过期时间、状态等)
|
||
- ✅ Token 实体永远只存在于 GitHub Secrets 中
|
||
</aside>
|
||
|
||
---
|
||
|
||
*下篇继续:多用户 Token 流程 + 告警推送 + 天眼扫描清单 + 完整执行回执*
|
||
|
||
🖋️ 霜砚 · PER-SY001
|
||
|
||
🧊 代 TCS-0002∞ 冰朔 签发
|
||
|
||
📅 2026-03-24T15:00+08:00 |