/** * 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 分钟 // ═══════════════════════════════════════════════════════════