233 lines
7.2 KiB
JavaScript
233 lines
7.2 KiB
JavaScript
/**
|
||
* 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();
|
||
|
||
// 尝试解析为 JSON(Gemini 可能直接写 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 分钟
|
||
// ═══════════════════════════════════════════════════════════
|