guanghulab/scripts/grid-db/deploy-drive-bridge.js
2026-05-10 13:12:44 +08:00

415 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* scripts/grid-db/deploy-drive-bridge.js
*
* 功能:一键部署 / 一键恢复 Drive 桥接层
*
* 读取 grid-db/deploy-queue/*.json 中的部署指令,执行:
* ① 用 OAuth2 代理人在用户 Drive 创建「光湖格点库」目录结构
* ② 从仓库同步初始数据到 Drive mirror/
* ③ 生成用户专属的 index.json含 DEV 编号和人格体信息)
* ④ 生成 Gemini 启动指令 → 写入 Drive
* ⑤ 处理完成后将指令文件移入 done/ 子目录
* ⑥ 写回执到 grid-db/deploy-log/
*
* 用法node scripts/grid-db/deploy-drive-bridge.js <deploy-command.json>
*
* 环境变量:
* - GDRIVE_CLIENT_ID: OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
* - DEPLOY_GITHUB_TOKEN: 具有 repo 权限的 GitHub Token用于配置 Apps Script
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
*/
const { google } = require('googleapis');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const GRID_DB_ROOT = path.join(__dirname, '../../grid-db');
const DEPLOY_LOG_DIR = path.join(GRID_DB_ROOT, 'deploy-log');
const DEPLOY_QUEUE_DIR = path.join(GRID_DB_ROOT, 'deploy-queue');
const TEMPLATE_PATH = path.join(GRID_DB_ROOT, 'schema/drive-index-template.json');
const PROMPT_TEMPLATE_PATH = path.join(GRID_DB_ROOT, 'gemini-prompts/startup-prompt-template.md');
// Drive 目录结构定义
const DRIVE_DIRS = [
'mirror',
'mirror/memory',
'mirror/outbox',
'mirror/rules',
'mirror/broadcast-archive',
'mirror/channels',
'inbox',
'inbox/已处理',
'logs'
];
/**
* 转义 Drive API 查询中的特殊字符
*/
function escapeQuery(str) {
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
/**
* 在 Drive 中逐级创建文件夹
*/
async function getOrCreateDriveFolder(drive, parentId, folderPath) {
const parts = folderPath.split('/').filter(Boolean);
let currentParentId = parentId;
for (const part of parts) {
const safePart = escapeQuery(part);
const query = `name='${safePart}' and '${currentParentId}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false`;
const res = await drive.files.list({
q: query,
fields: 'files(id, name)',
pageSize: 1
});
if (res.data.files && res.data.files.length > 0) {
currentParentId = res.data.files[0].id;
} else {
const createRes = await drive.files.create({
requestBody: {
name: part,
mimeType: 'application/vnd.google-apps.folder',
parents: [currentParentId]
},
fields: 'id'
});
currentParentId = createRes.data.id;
console.log(`[deploy] Created folder: ${part}`);
}
}
return currentParentId;
}
/**
* 上传文件到 Drive 指定文件夹
*/
async function uploadToDrive(drive, folderId, fileName, content, mimeType) {
const { Readable } = require('stream');
const media = {
mimeType: mimeType || 'application/json',
body: Readable.from([content])
};
const res = await drive.files.create({
requestBody: {
name: fileName,
parents: [folderId]
},
media,
fields: 'id'
});
return res.data.id;
}
/**
* 共享 Drive 文件夹给用户
*/
async function shareFolderWithUser(drive, folderId, email) {
await drive.permissions.create({
fileId: folderId,
requestBody: {
type: 'user',
role: 'writer',
emailAddress: email
},
sendNotificationEmail: false
});
console.log(`[deploy] Shared folder with: ${email}`);
}
/**
* 生成用户专属的 index.json
*/
function generateIndex(devId, personaId, personaName, devName) {
const templateData = JSON.parse(fs.readFileSync(TEMPLATE_PATH, 'utf8'));
const template = templateData.template;
const now = new Date().toISOString();
const index = {
system: template.system,
version: template.version,
dev_id: devId,
persona_id: personaId,
persona_name: personaName,
dev_name: devName,
last_sync: now,
routes: {},
shortcuts: { ...template.shortcuts }
};
for (const [key, val] of Object.entries(template.routes)) {
index.routes[key] = val.replace(/\{DEV_ID\}/g, devId);
}
return JSON.stringify(index, null, 2);
}
/**
* 生成用户专属的 Gemini 启动指令
*/
function generateStartupPrompt(devId, personaId, personaName, devName) {
let prompt = fs.readFileSync(PROMPT_TEMPLATE_PATH, 'utf8');
prompt = prompt.replace(/\{DEV_ID\}/g, devId);
prompt = prompt.replace(/\{PERSONA_ID\}/g, personaId);
prompt = prompt.replace(/\{PERSONA_NAME\}/g, personaName);
prompt = prompt.replace(/\{DEV_NAME\}/g, devName);
return prompt;
}
/**
* 收集需要初始同步的文件
*/
function collectInitialSyncFiles(devId, config) {
const files = [];
// 同步该 DEV 的记忆文件
if (config.sync_memory) {
const memDir = path.join(GRID_DB_ROOT, 'memory', devId);
if (fs.existsSync(memDir)) {
const entries = fs.readdirSync(memDir).filter(f => f.endsWith('.json'));
for (const entry of entries) {
files.push({
localPath: path.join(memDir, entry),
drivePath: `mirror/memory/${devId}`,
fileName: entry
});
}
}
}
// 同步规则文件
if (config.sync_rules) {
const rulesDir = path.join(GRID_DB_ROOT, 'rules');
if (fs.existsSync(rulesDir)) {
const entries = fs.readdirSync(rulesDir).filter(f => f.endsWith('.json'));
for (const entry of entries) {
files.push({
localPath: path.join(rulesDir, entry),
drivePath: 'mirror/rules',
fileName: entry
});
}
}
}
// 同步该 DEV 的最新广播
if (config.sync_outbox) {
const outboxDir = path.join(GRID_DB_ROOT, 'outbox/latest');
if (fs.existsSync(outboxDir)) {
const devOutbox = path.join(outboxDir, `${devId}.json`);
if (fs.existsSync(devOutbox)) {
files.push({
localPath: devOutbox,
drivePath: 'mirror/outbox',
fileName: `${devId}.json`
});
}
}
}
return files;
}
/**
* 写部署回执
*/
function writeReceipt(deployId, devId, status, details) {
if (!fs.existsSync(DEPLOY_LOG_DIR)) {
fs.mkdirSync(DEPLOY_LOG_DIR, { recursive: true });
}
const receipt = {
deploy_id: deployId,
dev_id: devId,
status: status,
completed_at: new Date().toISOString(),
details: details,
executor: 'PER-ZY001'
};
const fileName = `${deployId}.json`;
fs.writeFileSync(
path.join(DEPLOY_LOG_DIR, fileName),
JSON.stringify(receipt, null, 2) + '\n'
);
console.log(`[deploy] Receipt written: ${fileName}`);
}
/**
* 将处理完的指令移到 done/ 子目录
*/
function moveToComplete(commandFilePath) {
const doneDir = path.join(DEPLOY_QUEUE_DIR, 'done');
if (!fs.existsSync(doneDir)) {
fs.mkdirSync(doneDir, { recursive: true });
}
const fileName = path.basename(commandFilePath);
const destPath = path.join(doneDir, fileName);
fs.renameSync(commandFilePath, destPath);
console.log(`[deploy] Moved to done: ${fileName}`);
}
/**
* 验证部署指令格式
*/
function validateCommand(command) {
const required = ['schema_version', 'deploy_id', 'timestamp', 'action',
'source', 'target_dev', 'config', 'signed_by', 'authorized_by'];
for (const field of required) {
if (!command[field]) {
throw new Error(`Missing required field: ${field}`);
}
}
const validActions = ['deploy_drive_bridge', 'recover_drive_bridge'];
if (!validActions.includes(command.action)) {
throw new Error(`Invalid action: ${command.action}`);
}
const devRequired = ['dev_id', 'dev_name', 'google_email', 'persona_id', 'persona_name'];
for (const field of devRequired) {
if (!command.target_dev[field]) {
throw new Error(`Missing target_dev field: ${field}`);
}
}
}
/**
* 主部署流程
*/
async function deploy(commandFilePath) {
console.log(`[deploy] Processing: ${commandFilePath}`);
// 读取并验证部署指令
const command = JSON.parse(fs.readFileSync(commandFilePath, 'utf8'));
validateCommand(command);
const { deploy_id, action, target_dev, config } = command;
const { dev_id, dev_name, google_email, persona_id, persona_name } = target_dev;
console.log(`[deploy] ${action} for ${dev_id} (${dev_name}) → ${google_email}`);
// 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;
}
try {
// ① 在用户 Drive 创建或复用根目录
const rootFolderName = config.drive_root_folder || '光湖格点库';
let rootFolderId;
if (action === 'recover_drive_bridge') {
// 恢复模式:尝试查找已有文件夹
const safeName = escapeQuery(rootFolderName);
const searchRes = await drive.files.list({
q: `name='${safeName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
fields: 'files(id, name)',
pageSize: 1
});
if (searchRes.data.files && searchRes.data.files.length > 0) {
rootFolderId = searchRes.data.files[0].id;
console.log(`[deploy] Reusing existing folder: ${rootFolderName} (${rootFolderId})`);
}
}
if (!rootFolderId) {
const rootFolder = await drive.files.create({
requestBody: {
name: rootFolderName,
mimeType: 'application/vnd.google-apps.folder'
},
fields: 'id'
});
rootFolderId = rootFolder.data.id;
console.log(`[deploy] Created root folder: ${rootFolderName} (${rootFolderId})`);
}
// 共享给用户
await shareFolderWithUser(drive, rootFolderId, google_email);
// ② 创建子目录结构
for (const dir of DRIVE_DIRS) {
await getOrCreateDriveFolder(drive, rootFolderId, dir);
}
// 为该 DEV 创建记忆子目录
await getOrCreateDriveFolder(drive, rootFolderId, `mirror/memory/${dev_id}`);
// ③ 生成并上传 index.json
const indexContent = generateIndex(dev_id, persona_id, persona_name, dev_name);
const mirrorFolderId = await getOrCreateDriveFolder(drive, rootFolderId, 'mirror');
await uploadToDrive(drive, mirrorFolderId, 'index.json', indexContent);
console.log('[deploy] Uploaded: index.json');
// ④ 同步初始数据
const filesToSync = collectInitialSyncFiles(dev_id, config);
let syncCount = 0;
for (const file of filesToSync) {
try {
const folderId = await getOrCreateDriveFolder(drive, rootFolderId, file.drivePath);
const content = fs.readFileSync(file.localPath, 'utf8');
await uploadToDrive(drive, folderId, file.fileName, content);
syncCount++;
} catch (err) {
console.error(`[deploy] Failed to sync ${file.fileName}: ${err.message}`);
}
}
console.log(`[deploy] Initial sync: ${syncCount} files`);
// ⑤ 生成并上传 Gemini 启动指令
if (config.gemini_startup_prompt) {
const prompt = generateStartupPrompt(dev_id, persona_id, persona_name, dev_name);
await uploadToDrive(drive, rootFolderId, 'Gemini启动指令.md', prompt, 'text/markdown');
console.log('[deploy] Uploaded: Gemini启动指令.md');
}
// ⑥ 写回执 + 移动指令到 done/
writeReceipt(deploy_id, dev_id, 'success', {
root_folder_id: rootFolderId,
files_synced: syncCount,
action: action
});
moveToComplete(commandFilePath);
console.log(`[deploy] ✅ ${action} complete for ${dev_id}`);
} catch (err) {
console.error(`[deploy] ❌ Failed: ${err.message}`);
writeReceipt(deploy_id, dev_id, 'failed', err.message);
process.exit(1);
}
}
// 入口
const commandFile = process.argv[2];
if (!commandFile) {
console.error('[deploy] Usage: node deploy-drive-bridge.js <command.json>');
process.exit(1);
}
if (!fs.existsSync(commandFile)) {
console.error(`[deploy] File not found: ${commandFile}`);
process.exit(1);
}
deploy(commandFile).catch(err => {
console.error('[deploy] Fatal error:', err.message);
process.exit(1);
});