guanghulab/scripts/grid-db/drive-to-github-bridge.gs
2026-05-10 13:12:44 +08:00

233 lines
7.2 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.

/**
* drive-to-github-bridge.gs
*
* Google Apps Script — 光湖格点库桥接器
*
* 功能:监听 Drive「光湖格点库/inbox/」文件夹
* 发现 Gemini 新建的文档 → 读取内容 → 通过 GitHub API 写入仓库
*
* 触发方式:时间驱动触发器,每 1 分钟执行一次
*
* 流程:
* ① 扫描 inbox 文件夹中所有文件
* ② 读取文件内容Google Docs → 提取纯文本/JSON
* ③ 构造标准 inbox 消息格式
* ④ 调用 GitHub Contents API 写入 grid-db/inbox/[filename].json
* ⑤ 写入成功 → 将 Drive 文件移到「已处理」文件夹(不删除,留审计轨迹)
* ⑥ 写入失败 → 标记文件,下次重试
*
* 安全:
* - GitHub Token 存在 Script Properties 中(不硬编码)
* - 只处理 inbox 文件夹内的文件,不碰 mirror
* - 每次处理后写日志到 Drive「光湖格点库/logs/」
*
* 部署步骤:
* 1. 打开 https://script.google.com → 新建项目
* 2. 项目名:光湖格点库桥接器
* 3. 粘贴此文件代码
* 4. 项目设置 → 脚本属性 → 添加 GITHUB_TOKEN需要 repo 权限)
* 5. 触发器 → 添加 → 函数 processInbox → 时间驱动 → 每 1 分钟
* 6. 首次运行:点运行 → 授权 Drive 访问权限
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
*/
var CONFIG = {
INBOX_FOLDER_NAME: '光湖格点库/inbox',
PROCESSED_FOLDER_NAME: '光湖格点库/inbox/已处理',
LOG_FOLDER_NAME: '光湖格点库/logs',
GITHUB_REPO: 'qinfendebingshuo/guanghulab',
GITHUB_BRANCH: 'main',
GRID_DB_INBOX_PATH: 'grid-db/inbox/'
};
/**
* 主入口:处理 Drive inbox 中的新文件
* 设置为时间驱动触发器,每 1 分钟执行
*/
function processInbox() {
var token = PropertiesService.getScriptProperties()
.getProperty('GITHUB_TOKEN');
if (!token) {
Logger.log('ERROR: GITHUB_TOKEN not set in Script Properties');
return;
}
var inboxFolder = getOrCreateFolder(CONFIG.INBOX_FOLDER_NAME);
var processedFolder = getOrCreateFolder(CONFIG.PROCESSED_FOLDER_NAME);
var logFolder = getOrCreateFolder(CONFIG.LOG_FOLDER_NAME);
var files = inboxFolder.getFiles();
var processedCount = 0;
var errorCount = 0;
while (files.hasNext()) {
var file = files.next();
try {
var content = extractContent(file);
if (!content) continue;
// 生成带时间戳的文件名
var timestamp = Utilities.formatDate(
new Date(), 'Asia/Shanghai', 'yyyyMMdd-HHmmss'
);
var safeName = file.getName().replace(/[^a-zA-Z0-9.\-_]/g, '');
// Strip any existing extension, then add .json
safeName = safeName.replace(/\.[^.]+$/, '');
if (!safeName) safeName = 'untitled';
var filename = timestamp + '-' + safeName + '.json';
// 写入 GitHub 仓库
var success = writeToGitHub(
token,
CONFIG.GRID_DB_INBOX_PATH + filename,
content
);
if (success) {
file.moveTo(processedFolder);
processedCount++;
Logger.log('OK: ' + filename);
} else {
errorCount++;
Logger.log('FAIL: ' + filename);
}
} catch (e) {
errorCount++;
Logger.log('ERROR: ' + file.getName() + ': ' + e.message);
}
}
// 写处理日志
if (processedCount > 0 || errorCount > 0) {
var logTimestamp = Utilities.formatDate(
new Date(), 'Asia/Shanghai', 'yyyy-MM-dd HH:mm:ss'
);
var logLine = logTimestamp + ' | processed: ' + processedCount
+ ' | errors: ' + errorCount + '\n';
var logFileName = Utilities.formatDate(
new Date(), 'Asia/Shanghai', 'yyyyMMdd'
) + '-bridge.log';
appendToLogFile(logFolder, logFileName, logLine);
}
}
/**
* 从 Drive 文件中提取内容
* 支持 Google Docs 和纯文本/JSON 文件
*/
function extractContent(file) {
var mimeType = file.getMimeType();
if (mimeType === MimeType.GOOGLE_DOCS) {
// Google Docs → 提取纯文本
var doc = DocumentApp.openById(file.getId());
var text = doc.getBody().getText().trim();
// 尝试解析为 JSONGemini 可能直接写 JSON 格式)
try {
JSON.parse(text);
return text;
} catch (e) {
// 非 JSON → 包装为标准 inbox 消息格式
return JSON.stringify({
schema_version: '1.0',
message_id: Utilities.getUuid(),
timestamp: new Date().toISOString(),
source: 'gemini-via-drive',
type: 'raw-message',
priority: 'info',
payload: {
text: text
}
}, null, 2);
}
} else if (mimeType === MimeType.PLAIN_TEXT
|| file.getName().endsWith('.json')) {
// 纯文本或 JSON 文件 → 直接读取
return file.getBlob().getDataAsString();
}
Logger.log('SKIP: unsupported mime type ' + mimeType
+ ' for file ' + file.getName());
return null;
}
/**
* 通过 GitHub Contents API 写入文件到仓库
* @returns {boolean} 是否写入成功
*/
function writeToGitHub(token, filePath, content) {
var url = 'https://api.github.com/repos/'
+ CONFIG.GITHUB_REPO + '/contents/' + filePath;
var payload = {
message: 'auto: Gemini inbox via Drive bridge (' + filePath.split('/').pop().replace(/[^\w.\-]/g, '') + ') [skip ci]',
content: Utilities.base64Encode(content, Utilities.Charset.UTF_8),
branch: CONFIG.GITHUB_BRANCH
};
var options = {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
},
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(url, options);
var code = response.getResponseCode();
if (code !== 200 && code !== 201) {
Logger.log('GitHub API error: ' + code + ' ' + response.getContentText());
}
return (code === 200 || code === 201);
}
/**
* 按路径逐级获取或创建 Drive 文件夹
*/
function getOrCreateFolder(folderPath) {
var parts = folderPath.split('/');
var folder = DriveApp.getRootFolder();
for (var i = 0; i < parts.length; i++) {
var folders = folder.getFoldersByName(parts[i]);
if (folders.hasNext()) {
folder = folders.next();
} else {
folder = folder.createFolder(parts[i]);
}
}
return folder;
}
/**
* 向日志文件追加内容(不存在则创建)
*/
function appendToLogFile(folder, fileName, content) {
var files = folder.getFilesByName(fileName);
if (files.hasNext()) {
var file = files.next();
var existing = file.getBlob().getDataAsString();
file.setContent(existing + content);
} else {
folder.createFile(fileName, content, MimeType.PLAIN_TEXT);
}
}
// ═══════════════════════════════════════════════════════════
// 触发器设置说明:
// Apps Script 编辑器 → 触发器 → 添加
// 函数processInbox
// 事件源:时间驱动
// 间隔:每 1 分钟
// ═══════════════════════════════════════════════════════════