refactor: OAuth2 代理人重构 · Service Account→OAuth2 全量替换 · ZY-OAUTH2-REBUILD-2026-0324-001 [skip ci]
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/75c92776-b0de-4ee6-86c0-9e774d5ec717
This commit is contained in:
parent
368a7f139f
commit
7d6122a5c8
|
|
@ -21,7 +21,9 @@ jobs:
|
||||||
|
|
||||||
- name: ⚡ 执行部署
|
- name: ⚡ 执行部署
|
||||||
env:
|
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 }}
|
DEPLOY_GITHUB_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
for f in grid-db/deploy-queue/*.json; do
|
for f in grid-db/deploy-queue/*.json; do
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
# 🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit)
|
# 🛰️ 天眼 OAuth2 凭据审计 (Sky-Eye Credential Audit)
|
||||||
#
|
#
|
||||||
# 守护: PER-ZY001 铸渊 (Zhuyuan)
|
# 守护: PER-ZY001 铸渊 (Zhuyuan)
|
||||||
# 系统: SYS-GLW-0001
|
# 系统: SYS-GLW-0001
|
||||||
# 主控: TCS-0002∞ (冰朔)
|
# 主控: TCS-0002∞ (冰朔)
|
||||||
#
|
#
|
||||||
# 功能:
|
# 功能:
|
||||||
# ① GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性校验
|
# ① OAuth2 凭据环境变量完整性校验
|
||||||
# ② 依赖图扫描(工作流 + 脚本)
|
# ② 依赖图扫描(工作流 + 脚本)
|
||||||
# ③ YAML 语法全量扫描
|
# ③ YAML 语法全量扫描
|
||||||
# ④ 审计报告归档至 System_Logs/
|
# ④ 审计报告归档至 System_Logs/
|
||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
name: "🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit)"
|
name: "🛰️ 天眼 OAuth2 凭据审计 (Sky-Eye Credential Audit)"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
|
|
@ -32,7 +32,9 @@ jobs:
|
||||||
|
|
||||||
- name: "🛰️ Run Sky-Eye Credential Audit"
|
- name: "🛰️ Run Sky-Eye Credential Audit"
|
||||||
env:
|
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
|
run: node scripts/skyeye/credential-audit.js
|
||||||
|
|
||||||
- name: "📝 Commit audit report"
|
- name: "📝 Commit audit report"
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ jobs:
|
||||||
|
|
||||||
- name: 🪞 同步到 Google Drive
|
- name: 🪞 同步到 Google Drive
|
||||||
env:
|
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 }}
|
DRIVE_FOLDER_ID: ${{ secrets.DRIVE_MIRROR_FOLDER_ID }}
|
||||||
run: node scripts/grid-db/sync-to-drive.js
|
run: node scripts/grid-db/sync-to-drive.js
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
# 系统: SYS-GLW-0001
|
# 系统: SYS-GLW-0001
|
||||||
# 主控: TCS-0002∞ (冰朔)
|
# 主控: TCS-0002∞ (冰朔)
|
||||||
#
|
#
|
||||||
# 方案: 宿主机直装 rclone(替代 wei/rclone 容器方案)
|
# 方案: 宿主机直装 rclone(OAuth2 代理人模式)
|
||||||
# 凭证: GOOGLE_DRIVE_SERVICE_ACCOUNT (Service Account JSON)
|
# 凭证: GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
|
||||||
# 目标: Google Drive 文件夹 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G
|
# 目标: Google Drive 文件夹 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G
|
||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
|
@ -36,39 +36,32 @@ jobs:
|
||||||
sudo apt-get install -y -qq rclone
|
sudo apt-get install -y -qq rclone
|
||||||
rclone version
|
rclone version
|
||||||
|
|
||||||
- name: "🔑 Configure rclone (Service Account)"
|
- name: "🔑 Configure rclone (OAuth2)"
|
||||||
env:
|
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: |
|
run: |
|
||||||
if [ -z "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" ]; then
|
if [ -z "${GDRIVE_CLIENT_ID}" ] || [ -z "${GDRIVE_CLIENT_SECRET}" ] || [ -z "${GDRIVE_REFRESH_TOKEN}" ]; then
|
||||||
echo "::error::GOOGLE_DRIVE_SERVICE_ACCOUNT secret is not set"
|
echo "::error::OAuth2 secrets not set (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SA_FILE="$(mktemp "${RUNNER_TEMP}/gd-sa-XXXXXX.json")"
|
echo "✅ OAuth2 credentials validated"
|
||||||
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"
|
|
||||||
|
|
||||||
mkdir -p "${HOME}/.config/rclone"
|
mkdir -p "${HOME}/.config/rclone"
|
||||||
{
|
{
|
||||||
echo "[gdrive]"
|
echo "[gdrive]"
|
||||||
echo "type = drive"
|
echo "type = drive"
|
||||||
echo "scope = 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"
|
echo "root_folder_id = 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G"
|
||||||
} > "${HOME}/.config/rclone/rclone.conf"
|
} > "${HOME}/.config/rclone/rclone.conf"
|
||||||
chmod 600 "${HOME}/.config/rclone/rclone.conf"
|
chmod 600 "${HOME}/.config/rclone/rclone.conf"
|
||||||
|
|
||||||
echo "SA_FILE=${SA_FILE}" >> "${GITHUB_ENV}"
|
echo "✅ rclone configured with OAuth2"
|
||||||
echo "✅ rclone configured with Service Account"
|
|
||||||
|
|
||||||
- name: "🪞 Sync to Google Drive"
|
- name: "🪞 Sync to Google Drive"
|
||||||
# 使用 copy(非 sync)—— 仅添加/更新文件,不删除 Drive 端已有文件(防误删)
|
# 使用 copy(非 sync)—— 仅添加/更新文件,不删除 Drive 端已有文件(防误删)
|
||||||
|
|
@ -90,6 +83,5 @@ jobs:
|
||||||
- name: "🧹 Cleanup credentials"
|
- name: "🧹 Cleanup credentials"
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
rm -f "${SA_FILE}"
|
|
||||||
rm -f "${HOME}/.config/rclone/rclone.conf"
|
rm -f "${HOME}/.config/rclone/rclone.conf"
|
||||||
echo "✅ Credentials cleaned up"
|
echo "✅ Credentials cleaned up"
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ jobs:
|
||||||
|
|
||||||
- name: 🛰️ 执行语义落盘
|
- name: 🛰️ 执行语义落盘
|
||||||
env:
|
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'
|
DRIVE_LANDING_FOLDER_ID: '1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,11 @@ Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作
|
||||||
### A.1 Google Cloud 配置
|
### A.1 Google Cloud 配置
|
||||||
|
|
||||||
1. 前往 [Google Cloud Console](https://console.cloud.google.com)
|
1. 前往 [Google Cloud Console](https://console.cloud.google.com)
|
||||||
2. 创建项目或使用已有项目
|
2. 创建项目或使用已有项目(项目名:`guanghu-drive-bridge`)
|
||||||
3. 启用 **Google Drive API**
|
3. 启用 **Google Drive API**、**Google Docs API**、**Google Sheets API**
|
||||||
4. 创建 **Service Account**,下载 JSON 密钥文件
|
4. 创建 **OAuth 2.0 客户端凭据**(桌面应用类型)
|
||||||
5. 在用户的 Google Drive 中创建「光湖格点库」文件夹
|
5. 完成 OAuth 授权流程,获取 Refresh Token
|
||||||
6. 将 Service Account 邮箱分享到该文件夹(**编辑者**权限)
|
6. 在用户的 Google Drive 中创建「光湖格点库」文件夹
|
||||||
|
|
||||||
### A.2 配置 GitHub Secrets
|
### A.2 配置 GitHub Secrets
|
||||||
|
|
||||||
|
|
@ -40,7 +40,9 @@ Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作
|
||||||
|
|
||||||
| Secret 名称 | 值 |
|
| 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 中最后一段) |
|
| `DRIVE_MIRROR_FOLDER_ID` | Drive「光湖格点库」文件夹的 ID(URL 中最后一段) |
|
||||||
|
|
||||||
### A.3 自动触发
|
### A.3 自动触发
|
||||||
|
|
@ -109,7 +111,7 @@ Gemini 通过 Personal Context 读取 Drive `mirror/` 中的文件,通过在 `
|
||||||
## 安全注意事项
|
## 安全注意事项
|
||||||
|
|
||||||
- **GITHUB_TOKEN** 必须存在 Apps Script 的 Script Properties 中,禁止硬编码
|
- **GITHUB_TOKEN** 必须存在 Apps Script 的 Script Properties 中,禁止硬编码
|
||||||
- **Service Account** 密钥只存在 GitHub Secrets 中,不入库
|
- **OAuth2 凭据** 只存在 GitHub Secrets 中,不入库
|
||||||
- Drive 中的所有文件都是副本,丢失可从仓库重新同步
|
- Drive 中的所有文件都是副本,丢失可从仓库重新同步
|
||||||
- Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/`
|
- Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/`
|
||||||
- 所有自动提交包含 `[skip ci]` 防止循环触发
|
- 所有自动提交包含 `[skip ci]` 防止循环触发
|
||||||
|
|
@ -164,6 +166,8 @@ Schema 见 `grid-db/schema/deploy-command.schema.json`。
|
||||||
|
|
||||||
| Secret | 用途 |
|
| 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 |
|
| `DRIVE_MIRROR_FOLDER_ID` | 镜像同步目标文件夹 ID |
|
||||||
| `DEPLOY_GITHUB_TOKEN` | 部署脚本使用的 GitHub Token(repo 权限) |
|
| `DEPLOY_GITHUB_TOKEN` | 部署脚本使用的 GitHub Token(repo 权限) |
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* 功能:一键部署 / 一键恢复 Drive 桥接层
|
* 功能:一键部署 / 一键恢复 Drive 桥接层
|
||||||
*
|
*
|
||||||
* 读取 grid-db/deploy-queue/*.json 中的部署指令,执行:
|
* 读取 grid-db/deploy-queue/*.json 中的部署指令,执行:
|
||||||
* ① 用 Service Account 在用户 Drive 创建「光湖格点库」目录结构
|
* ① 用 OAuth2 代理人在用户 Drive 创建「光湖格点库」目录结构
|
||||||
* ② 从仓库同步初始数据到 Drive mirror/
|
* ② 从仓库同步初始数据到 Drive mirror/
|
||||||
* ③ 生成用户专属的 index.json(含 DEV 编号和人格体信息)
|
* ③ 生成用户专属的 index.json(含 DEV 编号和人格体信息)
|
||||||
* ④ 生成 Gemini 启动指令 → 写入 Drive
|
* ④ 生成 Gemini 启动指令 → 写入 Drive
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
* 用法:node scripts/grid-db/deploy-drive-bridge.js <deploy-command.json>
|
* 用法:node scripts/grid-db/deploy-drive-bridge.js <deploy-command.json>
|
||||||
*
|
*
|
||||||
* 环境变量:
|
* 环境变量:
|
||||||
* - 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)
|
* - DEPLOY_GITHUB_TOKEN: 具有 repo 权限的 GitHub Token(用于配置 Apps Script)
|
||||||
*
|
*
|
||||||
* 守护: PER-ZY001 铸渊
|
* 守护: PER-ZY001 铸渊
|
||||||
|
|
@ -296,41 +298,17 @@ async function deploy(commandFilePath) {
|
||||||
|
|
||||||
console.log(`[deploy] ${action} for ${dev_id} (${dev_name}) → ${google_email}`);
|
console.log(`[deploy] ${action} for ${dev_id} (${dev_name}) → ${google_email}`);
|
||||||
|
|
||||||
// 验证环境变量
|
// OAuth2 认证(统一入口)
|
||||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
let drive;
|
||||||
if (!serviceAccountJson) {
|
try {
|
||||||
writeReceipt(deploy_id, dev_id, 'failed', 'GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
|
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;
|
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 {
|
try {
|
||||||
// ① 在用户 Drive 创建或复用根目录
|
// ① 在用户 Drive 创建或复用根目录
|
||||||
const rootFolderName = config.drive_root_folder || '光湖格点库';
|
const rootFolderName = config.drive_root_folder || '光湖格点库';
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -19,7 +19,9 @@
|
||||||
* - 不同步 inbox/ 和 processing/(系统内部流转区)
|
* - 不同步 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
|
* - DRIVE_FOLDER_ID: Drive 镜像根文件夹 ID
|
||||||
*
|
*
|
||||||
* 守护: PER-ZY001 铸渊
|
* 守护: PER-ZY001 铸渊
|
||||||
|
|
@ -169,48 +171,24 @@ async function uploadFile(drive, folderId, fileName, content) {
|
||||||
*/
|
*/
|
||||||
async function main() {
|
async function main() {
|
||||||
// 验证环境变量
|
// 验证环境变量
|
||||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
|
||||||
const rootFolderId = process.env.DRIVE_FOLDER_ID;
|
const rootFolderId = process.env.DRIVE_FOLDER_ID;
|
||||||
|
|
||||||
if (!serviceAccountJson) {
|
|
||||||
console.log('[sync-to-drive] GOOGLE_DRIVE_SERVICE_ACCOUNT not set, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!rootFolderId) {
|
if (!rootFolderId) {
|
||||||
console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping');
|
console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 Service Account 凭证(天眼密钥流校验)
|
// OAuth2 认证(统一入口)
|
||||||
let credentials;
|
let drive;
|
||||||
try {
|
try {
|
||||||
const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator');
|
const { getDriveClient } = require('./drive-auth');
|
||||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
drive = getDriveClient();
|
||||||
if (!validation.valid) {
|
console.log('[sync-to-drive] ✅ OAuth2 credentials configured');
|
||||||
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');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fallback: if validator module is unavailable, try direct parse
|
console.error(`[sync-to-drive] 🔴 OAuth2 auth failed: ${err.message}`);
|
||||||
try {
|
process.exit(1);
|
||||||
credentials = JSON.parse(serviceAccountJson);
|
|
||||||
} catch (parseErr) {
|
|
||||||
console.error('[sync-to-drive] Failed to parse service account JSON:', parseErr.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 索引文件
|
// 预生成 Drive 索引文件
|
||||||
try {
|
try {
|
||||||
const { main: generateIndex } = require('./generate-drive-index');
|
const { main: generateIndex } = require('./generate-drive-index');
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@
|
||||||
* 天眼全域系统审计 (Sky-Eye Credential Audit)
|
* 天眼全域系统审计 (Sky-Eye Credential Audit)
|
||||||
*
|
*
|
||||||
* 功能:
|
* 功能:
|
||||||
* ① 校验 GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性
|
* ① 校验 OAuth2 凭据环境变量完整性
|
||||||
* ② 扫描所有依赖该密钥的工作流和脚本
|
* ② 扫描所有依赖 Drive 凭据的工作流和脚本
|
||||||
* ③ 验证 YAML 语法(.github/workflows/ 下所有配置文件)
|
* ③ 验证 YAML 语法(.github/workflows/ 下所有配置文件)
|
||||||
* ④ 生成审计报告写入 System_Logs/
|
* ④ 生成审计报告写入 System_Logs/
|
||||||
*
|
*
|
||||||
* 环境变量:
|
* 环境变量:
|
||||||
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: (可选) 用于实际校验
|
* - GDRIVE_CLIENT_ID: (可选) OAuth 客户端 ID
|
||||||
|
* - GDRIVE_CLIENT_SECRET: (可选) OAuth 客户端密钥
|
||||||
|
* - GDRIVE_REFRESH_TOKEN: (可选) 长效刷新令牌
|
||||||
*
|
*
|
||||||
* 用法:node scripts/skyeye/credential-audit.js
|
* 用法:node scripts/skyeye/credential-audit.js
|
||||||
*
|
*
|
||||||
|
|
@ -24,7 +26,7 @@
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { validateServiceAccountJSON, formatDiagnosticReport } = require('./credential-validator');
|
const { validateOAuth2Credentials, formatDiagnosticReport } = require('./credential-validator');
|
||||||
|
|
||||||
const ROOT = path.resolve(__dirname, '../..');
|
const ROOT = path.resolve(__dirname, '../..');
|
||||||
const WORKFLOWS_DIR = path.join(ROOT, '.github/workflows');
|
const WORKFLOWS_DIR = path.join(ROOT, '.github/workflows');
|
||||||
|
|
@ -43,24 +45,20 @@ function getDateStr() {
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
function auditCredential() {
|
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 {
|
return {
|
||||||
status: 'skipped',
|
status: 'skipped',
|
||||||
reason: 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available in environment',
|
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 secret configured'
|
recommendation: 'Run this audit in a GitHub Actions workflow with the secrets configured'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
const validation = validateOAuth2Credentials();
|
||||||
return {
|
return {
|
||||||
status: validation.valid ? 'pass' : 'fail',
|
status: validation.valid ? 'pass' : 'fail',
|
||||||
diagnostics: {
|
diagnostics: validation.diagnostics,
|
||||||
...validation.diagnostics,
|
|
||||||
// Never expose credential values in reports
|
|
||||||
credentials: undefined
|
|
||||||
},
|
|
||||||
issues: validation.issues,
|
issues: validation.issues,
|
||||||
report: formatDiagnosticReport(validation)
|
report: formatDiagnosticReport(validation)
|
||||||
};
|
};
|
||||||
|
|
@ -77,7 +75,7 @@ function scanCredentialDependencies() {
|
||||||
total_dependents: 0
|
total_dependents: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEARCH_PATTERN = 'GOOGLE_DRIVE_SERVICE_ACCOUNT';
|
const SEARCH_PATTERN = 'GDRIVE_CLIENT_ID';
|
||||||
|
|
||||||
// 扫描 workflows
|
// 扫描 workflows
|
||||||
if (fs.existsSync(WORKFLOWS_DIR)) {
|
if (fs.existsSync(WORKFLOWS_DIR)) {
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
/**
|
/**
|
||||||
* scripts/skyeye/credential-validator.js
|
* scripts/skyeye/credential-validator.js
|
||||||
*
|
*
|
||||||
* 天眼密钥流完整性校验器 (Sky-Eye Credential Validator)
|
* 天眼 OAuth2 凭据校验器 (Sky-Eye Credential Validator)
|
||||||
*
|
*
|
||||||
* 功能:对 Google Drive Service Account JSON 执行结构化检测
|
* 功能:对 Google Drive OAuth2 环境变量执行存在性检测
|
||||||
* - JSON 语法完整性(检测未闭合 {} / 截断)
|
* - GDRIVE_CLIENT_ID 存在性
|
||||||
* - 必需字段存在性验证
|
* - GDRIVE_CLIENT_SECRET 存在性
|
||||||
* - 字段值格式校验(private_key PEM 格式等)
|
* - GDRIVE_REFRESH_TOKEN 存在性
|
||||||
*
|
*
|
||||||
* 可作为模块 require() 使用,也可直接运行:
|
* 可作为模块 require() 使用,也可直接运行:
|
||||||
* GOOGLE_DRIVE_SERVICE_ACCOUNT='...' node credential-validator.js
|
* GDRIVE_CLIENT_ID='...' GDRIVE_CLIENT_SECRET='...' GDRIVE_REFRESH_TOKEN='...' node credential-validator.js
|
||||||
*
|
*
|
||||||
* 守护: PER-ZY001 铸渊
|
* 守护: PER-ZY001 铸渊
|
||||||
* 系统: SYS-GLW-0001
|
* 系统: SYS-GLW-0001
|
||||||
|
|
@ -19,147 +19,46 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Google Service Account JSON 必需字段
|
// OAuth2 必需环境变量
|
||||||
const REQUIRED_FIELDS = [
|
const REQUIRED_OAUTH2_VARS = [
|
||||||
'type',
|
'GDRIVE_CLIENT_ID',
|
||||||
'project_id',
|
'GDRIVE_CLIENT_SECRET',
|
||||||
'private_key_id',
|
'GDRIVE_REFRESH_TOKEN'
|
||||||
'private_key',
|
|
||||||
'client_email',
|
|
||||||
'client_id',
|
|
||||||
'auth_uri',
|
|
||||||
'token_uri'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证 Service Account JSON 字符串
|
* 验证 OAuth2 环境变量是否齐全
|
||||||
* @param {string} jsonStr - JSON 字符串
|
* @returns {{ valid: boolean, issues: string[], diagnostics: object }}
|
||||||
* @returns {{ valid: boolean, credentials: object|null, issues: string[], diagnostics: object }}
|
|
||||||
*/
|
*/
|
||||||
function validateServiceAccountJSON(jsonStr) {
|
function validateOAuth2Credentials() {
|
||||||
const result = {
|
const result = {
|
||||||
valid: false,
|
valid: false,
|
||||||
credentials: null,
|
|
||||||
issues: [],
|
issues: [],
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
input_length: 0,
|
auth_mode: 'oauth2',
|
||||||
is_empty: true,
|
vars_checked: REQUIRED_OAUTH2_VARS.length,
|
||||||
json_parseable: false,
|
vars_present: 0,
|
||||||
is_object: false,
|
missing_vars: []
|
||||||
has_all_required_fields: false,
|
|
||||||
missing_fields: [],
|
|
||||||
field_checks: {},
|
|
||||||
truncation_suspected: false
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ① 空值检查
|
for (const varName of REQUIRED_OAUTH2_VARS) {
|
||||||
if (!jsonStr || typeof jsonStr !== 'string') {
|
const val = process.env[varName];
|
||||||
result.issues.push('Credential string is empty or not a string');
|
if (!val || val.trim().length === 0) {
|
||||||
return result;
|
result.diagnostics.missing_vars.push(varName);
|
||||||
}
|
result.issues.push(`Missing or empty environment variable: ${varName}`);
|
||||||
|
} else {
|
||||||
const trimmed = jsonStr.trim();
|
result.diagnostics.vars_present++;
|
||||||
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}'`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
result.valid = result.issues.length === 0;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成人类可读的诊断报告
|
* 生成人类可读的诊断报告
|
||||||
* @param {object} validationResult - validateServiceAccountJSON 的返回值
|
* @param {object} validationResult - validateOAuth2Credentials 的返回值
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatDiagnosticReport(validationResult) {
|
function formatDiagnosticReport(validationResult) {
|
||||||
|
|
@ -167,31 +66,15 @@ function formatDiagnosticReport(validationResult) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
|
||||||
lines.push('╔════════════════════════════════════════════════════════╗');
|
lines.push('╔════════════════════════════════════════════════════════╗');
|
||||||
lines.push('║ 🛰️ 天眼密钥流完整性校验报告 (Credential Audit) ║');
|
lines.push('║ 🛰️ 天眼 OAuth2 凭据校验报告 (Credential Audit) ║');
|
||||||
lines.push('╚════════════════════════════════════════════════════════╝');
|
lines.push('╚════════════════════════════════════════════════════════╝');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(`状态: ${r.valid ? '✅ 绿色信号 (PASS)' : '🔴 逻辑红区 (FAIL)'}`);
|
lines.push(`状态: ${r.valid ? '✅ 绿色信号 (PASS)' : '🔴 逻辑红区 (FAIL)'}`);
|
||||||
lines.push(`输入长度: ${r.diagnostics.input_length} bytes`);
|
lines.push(`认证模式: OAuth2 代理人`);
|
||||||
lines.push(`JSON 可解析: ${r.diagnostics.json_parseable ? '是' : '否'}`);
|
lines.push(`环境变量检查: ${r.diagnostics.vars_present}/${r.diagnostics.vars_checked} 齐全`);
|
||||||
lines.push(`是否为对象: ${r.diagnostics.is_object ? '是' : '否'}`);
|
|
||||||
lines.push(`全部必需字段: ${r.diagnostics.has_all_required_fields ? '齐全' : '缺失'}`);
|
|
||||||
lines.push(`截断嫌疑: ${r.diagnostics.truncation_suspected ? '⚠️ 是' : '否'}`);
|
|
||||||
|
|
||||||
if (r.diagnostics.missing_fields.length > 0) {
|
if (r.diagnostics.missing_vars.length > 0) {
|
||||||
lines.push(`缺失字段: ${r.diagnostics.missing_fields.join(', ')}`);
|
lines.push(`缺失变量: ${r.diagnostics.missing_vars.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.issues.length > 0) {
|
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 模式
|
// CLI 模式
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const jsonStr = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
const result = validateOAuth2Credentials();
|
||||||
if (!jsonStr) {
|
|
||||||
console.error('[credential-validator] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = validateServiceAccountJSON(jsonStr);
|
|
||||||
console.log(formatDiagnosticReport(result));
|
console.log(formatDiagnosticReport(result));
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('---CREDENTIAL_AUDIT_JSON---');
|
console.log('---CREDENTIAL_AUDIT_JSON---');
|
||||||
// 不输出 credentials 本身以避免泄露密钥
|
console.log(JSON.stringify(result, null, 2));
|
||||||
const safeResult = { ...result, credentials: result.credentials ? '[REDACTED]' : null };
|
|
||||||
console.log(JSON.stringify(safeResult, null, 2));
|
|
||||||
|
|
||||||
process.exit(result.valid ? 0 : 1);
|
process.exit(result.valid ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
* 完成后在 Issue 下回复文档/表格直连 ID。
|
* 完成后在 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
|
* - DRIVE_LANDING_FOLDER_ID: 目标 Drive 文件夹 ID
|
||||||
* - GITHUB_TOKEN: GitHub API 令牌
|
* - GITHUB_TOKEN: GitHub API 令牌
|
||||||
* - ISSUE_NUMBER: Issue 编号
|
* - ISSUE_NUMBER: Issue 编号
|
||||||
|
|
@ -36,35 +38,12 @@ const TAG_TABLE = '[TCS-TABLE]';
|
||||||
const LANDING_SUBFOLDER = 'tcs-semantic-landing';
|
const LANDING_SUBFOLDER = 'tcs-semantic-landing';
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
// Google API 认证
|
// Google API 认证(OAuth2 代理人模式)
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
function buildAuth(serviceAccountJson) {
|
function buildAuth() {
|
||||||
let credentials;
|
const { getOAuth2Client } = require('./grid-db/drive-auth');
|
||||||
try {
|
return getOAuth2Client();
|
||||||
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'
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
@ -465,7 +444,6 @@ function postIssueComment(token, repo, issueNumber, body) {
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
|
|
||||||
const rootFolderId = process.env.DRIVE_LANDING_FOLDER_ID;
|
const rootFolderId = process.env.DRIVE_LANDING_FOLDER_ID;
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
const issueNumber = process.env.ISSUE_NUMBER;
|
const issueNumber = process.env.ISSUE_NUMBER;
|
||||||
|
|
@ -474,10 +452,6 @@ async function main() {
|
||||||
const repo = process.env.GITHUB_REPOSITORY;
|
const repo = process.env.GITHUB_REPOSITORY;
|
||||||
|
|
||||||
// 验证必需环境变量
|
// 验证必需环境变量
|
||||||
if (!serviceAccountJson) {
|
|
||||||
console.error('[semantic-landing] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (!rootFolderId) {
|
if (!rootFolderId) {
|
||||||
console.error('[semantic-landing] DRIVE_LANDING_FOLDER_ID not set');
|
console.error('[semantic-landing] DRIVE_LANDING_FOLDER_ID not set');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
@ -499,9 +473,10 @@ async function main() {
|
||||||
console.log(`[semantic-landing] Processing Issue #${issueNumber}: ${issueTitle}`);
|
console.log(`[semantic-landing] Processing Issue #${issueNumber}: ${issueTitle}`);
|
||||||
console.log(`[semantic-landing] Type: ${isMemo ? 'MEMO (Google Docs)' : 'TABLE (Google Sheets)'}`);
|
console.log(`[semantic-landing] Type: ${isMemo ? 'MEMO (Google Docs)' : 'TABLE (Google Sheets)'}`);
|
||||||
|
|
||||||
// 初始化 Google API
|
// 初始化 Google API(OAuth2 代理人模式)
|
||||||
const auth = buildAuth(serviceAccountJson);
|
const auth = buildAuth();
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
console.log('[semantic-landing] ✅ OAuth2 credentials configured');
|
||||||
|
|
||||||
// 确保子文件夹存在
|
// 确保子文件夹存在
|
||||||
const landingFolderId = await getOrCreateFolder(drive, rootFolderId, LANDING_SUBFOLDER);
|
const landingFolderId = await getOrCreateFolder(drive, rootFolderId, LANDING_SUBFOLDER);
|
||||||
|
|
|
||||||
|
|
@ -199,27 +199,27 @@ function ensureDirectories() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 天眼密钥流校验 — 验证 GOOGLE_DRIVE_SERVICE_ACCOUNT 环境变量
|
* 天眼 OAuth2 凭据校验 — 验证 GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
|
||||||
* 仅在环境变量可用时执行(CI 环境)
|
* 仅在环境变量可用时执行(CI 环境)
|
||||||
*/
|
*/
|
||||||
function validateCredentials() {
|
function validateCredentials() {
|
||||||
const result = { status: 'skipped', issues: [] };
|
const result = { status: 'skipped', issues: [] };
|
||||||
|
|
||||||
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) {
|
||||||
result.reason = 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available';
|
result.reason = 'OAuth2 credentials not available (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)';
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { validateServiceAccountJSON } = require('../../scripts/skyeye/credential-validator');
|
const { validateOAuth2Credentials } = require('../../scripts/skyeye/credential-validator');
|
||||||
const validation = validateServiceAccountJSON(serviceAccountJson);
|
const validation = validateOAuth2Credentials();
|
||||||
result.status = validation.valid ? 'pass' : 'fail';
|
result.status = validation.valid ? 'pass' : 'fail';
|
||||||
result.issues = validation.issues;
|
result.issues = validation.issues;
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`);
|
console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(' [CREDENTIAL] ✅ Service account credentials valid');
|
console.log(' [CREDENTIAL] ✅ OAuth2 credentials valid');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.status = 'error';
|
result.status = 'error';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue