From 4422b12805fe1011c625e7c16e15154c901f63ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:06:57 +0000 Subject: [PATCH 1/4] Initial plan From 0d842e52dfac0e0ea50a2aa5ba5c8ad814133b9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:14:28 +0000 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20OAuth2=20Token=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=BB=AD=E6=9C=9F=E5=BC=95=E6=93=8E=20+=20=E5=A4=A9=E7=9C=BC?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E7=BA=BF=E4=BB=BB=E5=8A=A1=E8=B1=81=E5=85=8D?= =?UTF-8?q?=20+=20=E5=A4=9A=E7=94=A8=E6=88=B7Token=E7=AE=A1=E7=90=86=20?= =?UTF-8?q?=C2=B7=20ZY-TOKEN-RENEW-2026-0324-001=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/bc1b54c6-5276-4d00-8ea5-5e8c3c2bef38 --- .github/persona-brain/agent-registry.json | 26 ++ .github/workflows/check-token-health.yml | 73 ++++++ .github/workflows/renew-gdrive-tokens.yml | 69 ++++++ config/gdrive-tokens.json | 26 ++ grid-db/bulletin/.gitkeep | 0 grid-db/notifications/.gitkeep | 0 scripts/gdrive/check-hibernation.js | 39 +++ scripts/gdrive/push-token-alerts.js | 64 +++++ scripts/gdrive/renew-tokens.js | 285 ++++++++++++++++++++++ skyeye/infra-manifest.json | 40 +++ 10 files changed, 622 insertions(+) create mode 100644 .github/workflows/check-token-health.yml create mode 100644 .github/workflows/renew-gdrive-tokens.yml create mode 100644 config/gdrive-tokens.json create mode 100644 grid-db/bulletin/.gitkeep create mode 100644 grid-db/notifications/.gitkeep create mode 100644 scripts/gdrive/check-hibernation.js create mode 100644 scripts/gdrive/push-token-alerts.js create mode 100644 scripts/gdrive/renew-tokens.js diff --git a/.github/persona-brain/agent-registry.json b/.github/persona-brain/agent-registry.json index 2e726198..30f994f2 100644 --- a/.github/persona-brain/agent-registry.json +++ b/.github/persona-brain/agent-registry.json @@ -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开始顺序分配", diff --git a/.github/workflows/check-token-health.yml b/.github/workflows/check-token-health.yml new file mode 100644 index 00000000..6d177ed2 --- /dev/null +++ b/.github/workflows/check-token-health.yml @@ -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 diff --git a/.github/workflows/renew-gdrive-tokens.yml b/.github/workflows/renew-gdrive-tokens.yml new file mode 100644 index 00000000..fd232535 --- /dev/null +++ b/.github/workflows/renew-gdrive-tokens.yml @@ -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 "需要人类手动重新授权。" diff --git a/config/gdrive-tokens.json b/config/gdrive-tokens.json new file mode 100644 index 00000000..1bb99f19 --- /dev/null +++ b/config/gdrive-tokens.json @@ -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": [] +} diff --git a/grid-db/bulletin/.gitkeep b/grid-db/bulletin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/grid-db/notifications/.gitkeep b/grid-db/notifications/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/scripts/gdrive/check-hibernation.js b/scripts/gdrive/check-hibernation.js new file mode 100644 index 00000000..f79376e9 --- /dev/null +++ b/scripts/gdrive/check-hibernation.js @@ -0,0 +1,39 @@ +/** + * 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(); + // 转换为北京时间 + 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 }; diff --git a/scripts/gdrive/push-token-alerts.js b/scripts/gdrive/push-token-alerts.js new file mode 100644 index 00000000..29bf94f8 --- /dev/null +++ b/scripts/gdrive/push-token-alerts.js @@ -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(); diff --git a/scripts/gdrive/renew-tokens.js b/scripts/gdrive/renew-tokens.js new file mode 100644 index 00000000..215b1f01 --- /dev/null +++ b/scripts/gdrive/renew-tokens.js @@ -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_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 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; +}); diff --git a/skyeye/infra-manifest.json b/skyeye/infra-manifest.json index a528740a..03b9a6c8 100644 --- a/skyeye/infra-manifest.json +++ b/skyeye/infra-manifest.json @@ -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)" } } } From 368a7f139f3759406960c409d52b92b3e2fcdfe2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:15:55 +0000 Subject: [PATCH 3/4] fix: address code review - sanitize error output, fix timezone calc, narrow git add scope [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 --- .github/workflows/check-token-health.yml | 3 ++- .github/workflows/renew-gdrive-tokens.yml | 3 ++- scripts/gdrive/check-hibernation.js | 5 +++-- scripts/gdrive/renew-tokens.js | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/check-token-health.yml b/.github/workflows/check-token-health.yml index 6d177ed2..cbc88c58 100644 --- a/.github/workflows/check-token-health.yml +++ b/.github/workflows/check-token-health.yml @@ -68,6 +68,7 @@ jobs: run: | git config user.name "zhuyuan-bot" git config user.email "zhuyuan@guanghulab.com" - git add config/gdrive-tokens.json grid-db/logs/ + 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 diff --git a/.github/workflows/renew-gdrive-tokens.yml b/.github/workflows/renew-gdrive-tokens.yml index fd232535..a11db3a9 100644 --- a/.github/workflows/renew-gdrive-tokens.yml +++ b/.github/workflows/renew-gdrive-tokens.yml @@ -58,7 +58,8 @@ jobs: run: | git config user.name "zhuyuan-bot" git config user.email "zhuyuan@guanghulab.com" - git add config/gdrive-tokens.json grid-db/logs/ + 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 diff --git a/scripts/gdrive/check-hibernation.js b/scripts/gdrive/check-hibernation.js index f79376e9..cde238c2 100644 --- a/scripts/gdrive/check-hibernation.js +++ b/scripts/gdrive/check-hibernation.js @@ -17,8 +17,9 @@ function isInHibernation() { const now = new Date(); - // 转换为北京时间 - const cst = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Shanghai' })); + // 转换为北京时间(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(); diff --git a/scripts/gdrive/renew-tokens.js b/scripts/gdrive/renew-tokens.js index 215b1f01..7ad5b471 100644 --- a/scripts/gdrive/renew-tokens.js +++ b/scripts/gdrive/renew-tokens.js @@ -104,8 +104,8 @@ async function main() { console.log(` ✅ 刷新成功!新过期时间: ${token.expires_at}`); results.renewed.push(token.user_id); } else { - // Google 有时不返回新的 refresh_token - console.log(` ⚠️ Google 未返回新 refresh_token,Token 可能仍然有效但无法续期`); + // 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, @@ -202,7 +202,7 @@ async function refreshOAuth2Token(refreshToken) { resolve(json); } } catch (e) { - reject(new Error(`Invalid response from Google: ${data.substring(0, 200)}`)); + reject(new Error('Invalid JSON response from Google OAuth2 endpoint')); } }); }); From 7d6122a5c8e89b6d48c03bf14a6fe85a13cf3ce8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 07:25:56 +0000 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20OAuth2=20=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E4=BA=BA=E9=87=8D=E6=9E=84=20=C2=B7=20Service=20Account?= =?UTF-8?q?=E2=86=92OAuth2=20=E5=85=A8=E9=87=8F=E6=9B=BF=E6=8D=A2=20=C2=B7?= =?UTF-8?q?=20ZY-OAUTH2-REBUILD-2026-0324-001=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/75c92776-b0de-4ee6-86c0-9e774d5ec717 --- .../workflows/auto-deploy-drive-bridge.yml | 4 +- .github/workflows/skyeye-credential-audit.yml | 10 +- .github/workflows/sync-griddb-to-drive.yml | 4 +- .github/workflows/sync-repo-to-drive.yml | 34 ++- .github/workflows/tcs-semantic-landing.yml | 4 +- scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md | 20 +- scripts/grid-db/deploy-drive-bridge.js | 46 ++--- scripts/grid-db/drive-auth.js | 47 +++++ scripts/grid-db/sync-to-drive.js | 42 +--- scripts/skyeye/credential-audit.js | 28 ++- scripts/skyeye/credential-validator.js | 193 +++--------------- scripts/tcs-semantic-landing.js | 45 +--- skyeye/scripts/self-healer.js | 14 +- 13 files changed, 173 insertions(+), 318 deletions(-) create mode 100644 scripts/grid-db/drive-auth.js diff --git a/.github/workflows/auto-deploy-drive-bridge.yml b/.github/workflows/auto-deploy-drive-bridge.yml index 17b494cb..530b6c4c 100644 --- a/.github/workflows/auto-deploy-drive-bridge.yml +++ b/.github/workflows/auto-deploy-drive-bridge.yml @@ -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 diff --git a/.github/workflows/skyeye-credential-audit.yml b/.github/workflows/skyeye-credential-audit.yml index c52d98e8..2df72c52 100644 --- a/.github/workflows/skyeye-credential-audit.yml +++ b/.github/workflows/skyeye-credential-audit.yml @@ -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" diff --git a/.github/workflows/sync-griddb-to-drive.yml b/.github/workflows/sync-griddb-to-drive.yml index 6e54f58e..22cdd31a 100644 --- a/.github/workflows/sync-griddb-to-drive.yml +++ b/.github/workflows/sync-griddb-to-drive.yml @@ -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 diff --git a/.github/workflows/sync-repo-to-drive.yml b/.github/workflows/sync-repo-to-drive.yml index 9a66dcf4..509aa8bd 100644 --- a/.github/workflows/sync-repo-to-drive.yml +++ b/.github/workflows/sync-repo-to-drive.yml @@ -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" diff --git a/.github/workflows/tcs-semantic-landing.yml b/.github/workflows/tcs-semantic-landing.yml index 8d6ef25b..3a31fa37 100644 --- a/.github/workflows/tcs-semantic-landing.yml +++ b/.github/workflows/tcs-semantic-landing.yml @@ -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 }} diff --git a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md index 1203893b..9d5eff53 100644 --- a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md +++ b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md @@ -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 权限) | diff --git a/scripts/grid-db/deploy-drive-bridge.js b/scripts/grid-db/deploy-drive-bridge.js index 79834f18..f5273334 100644 --- a/scripts/grid-db/deploy-drive-bridge.js +++ b/scripts/grid-db/deploy-drive-bridge.js @@ -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 * * 环境变量: - * - 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 || '光湖格点库'; diff --git a/scripts/grid-db/drive-auth.js b/scripts/grid-db/drive-auth.js new file mode 100644 index 00000000..c31bb891 --- /dev/null +++ b/scripts/grid-db/drive-auth.js @@ -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 }; diff --git a/scripts/grid-db/sync-to-drive.js b/scripts/grid-db/sync-to-drive.js index 1b6c0188..24049ef4 100644 --- a/scripts/grid-db/sync-to-drive.js +++ b/scripts/grid-db/sync-to-drive.js @@ -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'); diff --git a/scripts/skyeye/credential-audit.js b/scripts/skyeye/credential-audit.js index fd59699b..e654301b 100644 --- a/scripts/skyeye/credential-audit.js +++ b/scripts/skyeye/credential-audit.js @@ -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)) { diff --git a/scripts/skyeye/credential-validator.js b/scripts/skyeye/credential-validator.js index 4a210300..7831151f 100644 --- a/scripts/skyeye/credential-validator.js +++ b/scripts/skyeye/credential-validator.js @@ -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); } diff --git a/scripts/tcs-semantic-landing.js b/scripts/tcs-semantic-landing.js index 764557b0..b50a53d6 100644 --- a/scripts/tcs-semantic-landing.js +++ b/scripts/tcs-semantic-landing.js @@ -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); diff --git a/skyeye/scripts/self-healer.js b/skyeye/scripts/self-healer.js index 14181ec9..e34d4aef 100644 --- a/skyeye/scripts/self-healer.js +++ b/skyeye/scripts/self-healer.js @@ -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';