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] =?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';