From 35a108ebddf568b23b9f62431a8cd10fdfeb826f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:01:11 +0000 Subject: [PATCH 1/5] Initial plan From ec8f85522a7171cacbcd7d9eb5dc99e939fab905 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:06:05 +0000 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20Grid-DB=20=E2=86=94=20Google=20Driv?= =?UTF-8?q?e=20bridge=20(ZY-DRIVE-BRIDGE-2026-0323-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add workflow: sync-griddb-to-drive.yml (仓库→Drive mirror) - Add script: sync-to-drive.js (Drive upload logic with hash dedup) - Add script: drive-to-github-bridge.gs (Apps Script for Drive→仓库) - Add doc: DRIVE-BRIDGE-DEPLOY.md (deployment guide) Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a581dc42-267b-44e1-b669-6ad503a47f49 --- .github/workflows/sync-griddb-to-drive.yml | 40 ++++ scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md | 114 +++++++++ scripts/grid-db/drive-to-github-bridge.gs | 229 ++++++++++++++++++ scripts/grid-db/sync-to-drive.js | 262 +++++++++++++++++++++ 4 files changed, 645 insertions(+) create mode 100644 .github/workflows/sync-griddb-to-drive.yml create mode 100644 scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md create mode 100644 scripts/grid-db/drive-to-github-bridge.gs create mode 100644 scripts/grid-db/sync-to-drive.js diff --git a/.github/workflows/sync-griddb-to-drive.yml b/.github/workflows/sync-griddb-to-drive.yml new file mode 100644 index 00000000..9869e356 --- /dev/null +++ b/.github/workflows/sync-griddb-to-drive.yml @@ -0,0 +1,40 @@ +name: 🪞 Grid-DB → Google Drive 镜像同步 + +on: + push: + paths: + - 'grid-db/memory/**' + - 'grid-db/outbox/latest/**' + - 'grid-db/rules/**' + schedule: + - cron: '*/15 * * * *' + workflow_dispatch: + +permissions: + contents: write + +jobs: + sync-to-drive: + if: github.actor != 'zhuyuan-bot' && !contains(github.event.head_commit.message, '[skip ci]') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: 🔧 安装依赖 + run: npm install googleapis + + - name: 🪞 同步到 Google Drive + env: + GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} + DRIVE_FOLDER_ID: ${{ secrets.DRIVE_MIRROR_FOLDER_ID }} + run: node scripts/grid-db/sync-to-drive.js + + - name: 📋 写同步日志 + run: | + mkdir -p grid-db/logs + echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | sync-to-drive | OK" >> grid-db/logs/drive-sync.log + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan@guanghulab.com" + git add -f grid-db/logs/drive-sync.log + git diff --cached --quiet || git commit -m "log: Drive sync [skip ci]" + git push diff --git a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md new file mode 100644 index 00000000..78a10f98 --- /dev/null +++ b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md @@ -0,0 +1,114 @@ +# 🪞 Grid-DB ↔ Google Drive 桥接部署指南 + +> 指令编号: ZY-DRIVE-BRIDGE-2026-0323-001 +> +> 签发: 霜砚 · PER-SY001 +> +> 守护: 铸渊 · PER-ZY001 +> +> 系统: SYS-GLW-0001 + +--- + +## 📐 架构概述 + +Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作为中转站,实现 Gemini 间接读写 `grid-db/`。 + +``` +仓库 grid-db/ ──GitHub Action──→ Drive mirror/ ──Gemini 读取──→ 用户看到 +用户说话 → Gemini 写入 → Drive inbox/ ──Apps Script──→ 仓库 grid-db/inbox/ → 铸渊处理 +``` + +**数据主权原则:Drive 里的东西全是「副本」。正本永远在仓库和 Notion。** + +--- + +## Phase A · 仓库 → Drive(让 Gemini 能读) + +### A.1 Google Cloud 配置 + +1. 前往 [Google Cloud Console](https://console.cloud.google.com) +2. 创建项目或使用已有项目 +3. 启用 **Google Drive API** +4. 创建 **Service Account**,下载 JSON 密钥文件 +5. 在用户的 Google Drive 中创建「光湖格点库」文件夹 +6. 将 Service Account 邮箱分享到该文件夹(**编辑者**权限) + +### A.2 配置 GitHub Secrets + +在仓库 Settings → Secrets 中添加: + +| Secret 名称 | 值 | +|---|---| +| `GOOGLE_DRIVE_SERVICE_ACCOUNT` | Service Account JSON 密钥的完整内容 | +| `DRIVE_MIRROR_FOLDER_ID` | Drive「光湖格点库」文件夹的 ID(URL 中最后一段) | + +### A.3 自动触发 + +Workflow `.github/workflows/sync-griddb-to-drive.yml` 会在以下情况自动运行: + +- `grid-db/memory/**` 文件变更时 +- `grid-db/outbox/latest/**` 文件变更时 +- `grid-db/rules/**` 文件变更时 +- 每 15 分钟定时同步 +- 手动触发(workflow_dispatch) + +### A.4 同步范围 + +| 仓库路径 | Drive 镜像路径 | 同步 | +|---|---|---| +| `grid-db/memory/DEV-XXX/` | `mirror/memory/DEV-XXX/` | ✅ | +| `grid-db/outbox/latest/` | `mirror/outbox/` | ✅ | +| `grid-db/rules/` | `mirror/rules/` | ✅ | +| `grid-db/interactions/` | — | ❌ 不同步 | +| `grid-db/training-lake/` | — | ❌ 不同步 | +| `grid-db/inbox/` | — | ❌ 不同步 | +| `grid-db/processing/` | — | ❌ 不同步 | + +--- + +## Phase B · Drive → 仓库(让 Gemini 能写) + +### B.1 部署 Apps Script + +1. 打开 [Google Apps Script](https://script.google.com) → 新建项目 +2. 项目名:`光湖格点库桥接器` +3. 将 `scripts/grid-db/drive-to-github-bridge.gs` 的内容粘贴到编辑器 +4. 项目设置 → 脚本属性 → 添加 `GITHUB_TOKEN`(需要 `repo` 权限的 PAT) +5. 触发器 → 添加: + - 函数:`processInbox` + - 事件源:时间驱动 + - 间隔:每 1 分钟 +6. 首次运行 → 授权 Drive 访问权限 + +### B.2 Drive 文件夹结构 + +Apps Script 会自动创建以下 Drive 文件夹结构: + +``` +光湖格点库/ +├── inbox/ ← Gemini 在这里创建新文档 +│ └── 已处理/ ← 成功写入 GitHub 后自动移入 +├── mirror/ ← GitHub Action 同步的只读镜像 +│ ├── memory/ +│ │ ├── DEV-001/ +│ │ ├── DEV-002/ +│ │ └── ... +│ ├── outbox/ +│ └── rules/ +└── logs/ ← 桥接器处理日志 +``` + +### B.3 Gemini 使用方式 + +Gemini 通过 Personal Context 读取 Drive `mirror/` 中的文件,通过在 `inbox/` 创建 Google Docs 来写入消息。用户全程只跟 Gemini 对话,无需感知 Drive 中转层。 + +--- + +## 安全注意事项 + +- **GITHUB_TOKEN** 必须存在 Apps Script 的 Script Properties 中,禁止硬编码 +- **Service Account** 密钥只存在 GitHub Secrets 中,不入库 +- Drive 中的所有文件都是副本,丢失可从仓库重新同步 +- Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/` +- 所有自动提交包含 `[skip ci]` 防止循环触发 diff --git a/scripts/grid-db/drive-to-github-bridge.gs b/scripts/grid-db/drive-to-github-bridge.gs new file mode 100644 index 00000000..c04ad6e1 --- /dev/null +++ b/scripts/grid-db/drive-to-github-bridge.gs @@ -0,0 +1,229 @@ +/** + * 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, ''); + 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 [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 分钟 +// ═══════════════════════════════════════════════════════════ diff --git a/scripts/grid-db/sync-to-drive.js b/scripts/grid-db/sync-to-drive.js new file mode 100644 index 00000000..0ffc37c6 --- /dev/null +++ b/scripts/grid-db/sync-to-drive.js @@ -0,0 +1,262 @@ +/** + * scripts/grid-db/sync-to-drive.js + * + * 功能:将 grid-db/ 中的关键文件同步到 Google Drive 镜像目录 + * + * 同步范围: + * grid-db/memory/DEV-XXX/*.json → Drive/mirror/memory/DEV-XXX/ + * grid-db/outbox/latest/*.json → Drive/mirror/outbox/ + * grid-db/rules/*.json → Drive/mirror/rules/ + * + * 同步策略: + * - 比较文件内容 MD5 hash,仅同步有变更的文件(节省 API 配额) + * - 文件不存在则创建,已存在则覆盖更新 + * - Drive 中多出的文件不删除(防误删) + * + * 注意: + * - 不同步 interactions/ 和 training-lake/(数据量大 + 无需 Gemini 读取) + * - 不同步 inbox/ 和 processing/(系统内部流转区) + * + * 环境变量: + * - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥内容 + * - DRIVE_FOLDER_ID: Drive 镜像根文件夹 ID + * + * 守护: 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'); + +// 同步目录映射:本地相对路径 → Drive 镜像子目录 +const SYNC_DIRS = [ + { local: 'memory', drivePrefix: 'mirror/memory', recursive: true }, + { local: 'outbox/latest', drivePrefix: 'mirror/outbox', recursive: false }, + { local: 'rules', drivePrefix: 'mirror/rules', recursive: false } +]; + +/** + * 计算文件内容的 MD5 哈希 + */ +function md5(content) { + return crypto.createHash('md5').update(content).digest('hex'); +} + +/** + * 递归收集目录下的所有 JSON 文件 + */ +function collectFiles(dirPath, recursive) { + const results = []; + if (!fs.existsSync(dirPath)) return results; + + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory() && recursive) { + results.push(...collectFiles(fullPath, true)); + } else if (entry.isFile() && entry.name.endsWith('.json')) { + results.push(fullPath); + } + } + return results; +} + +/** + * 在 Drive 中查找或创建文件夹(按路径逐级创建) + */ +async function getOrCreateDriveFolder(drive, parentId, folderPath) { + const parts = folderPath.split('/').filter(Boolean); + let currentParentId = parentId; + + for (const part of parts) { + const query = `name='${part}' 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(`[sync-to-drive] Created folder: ${part}`); + } + } + + return currentParentId; +} + +/** + * 在 Drive 目标文件夹中查找同名文件 + */ +async function findFileInDrive(drive, folderId, fileName) { + const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`; + const res = await drive.files.list({ + q: query, + fields: 'files(id, name, md5Checksum)', + pageSize: 1 + }); + + if (res.data.files && res.data.files.length > 0) { + return res.data.files[0]; + } + return null; +} + +/** + * 上传或更新单个文件到 Drive + */ +async function uploadFile(drive, folderId, fileName, content) { + const existingFile = await findFileInDrive(drive, folderId, fileName); + const { Readable } = require('stream'); + + const media = { + mimeType: 'application/json', + body: Readable.from([content]) + }; + + if (existingFile) { + // 检查内容是否变更(Drive 对上传文件自动计算 MD5) + const localHash = md5(content); + if (existingFile.md5Checksum === localHash) { + return { action: 'skipped', fileName }; + } + + await drive.files.update({ + fileId: existingFile.id, + media + }); + return { action: 'updated', fileName }; + } else { + await drive.files.create({ + requestBody: { + name: fileName, + parents: [folderId] + }, + media, + fields: 'id' + }); + return { action: 'created', fileName }; + } +} + +/** + * 主同步流程 + */ +async function main() { + // 验证环境变量 + const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; + const rootFolderId = process.env.DRIVE_FOLDER_ID; + + if (!serviceAccountJson) { + console.log('[sync-to-drive] GOOGLE_DRIVE_SERVICE_ACCOUNT not set, skipping'); + return; + } + if (!rootFolderId) { + console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping'); + return; + } + + // 解析 Service Account 凭证 + let credentials; + try { + credentials = JSON.parse(serviceAccountJson); + } catch (err) { + console.error('[sync-to-drive] Failed to parse service account JSON:', err.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 }); + + console.log('[sync-to-drive] Starting Grid-DB → Drive mirror sync'); + + let totalCreated = 0; + let totalUpdated = 0; + let totalSkipped = 0; + let totalErrors = 0; + + // 遍历每个同步目录 + for (const syncDir of SYNC_DIRS) { + const localDir = path.join(GRID_DB_ROOT, syncDir.local); + const files = collectFiles(localDir, syncDir.recursive); + + if (files.length === 0) { + console.log(`[sync-to-drive] ${syncDir.local}: no files to sync`); + continue; + } + + console.log(`[sync-to-drive] ${syncDir.local}: found ${files.length} file(s)`); + + for (const filePath of files) { + try { + // 计算 Drive 中的目标路径 + const relativePath = path.relative(localDir, filePath); + const dirParts = path.dirname(relativePath); + const fileName = path.basename(filePath); + + // 构建 Drive 文件夹路径 + let driveFolderPath = syncDir.drivePrefix; + if (dirParts !== '.') { + driveFolderPath = `${syncDir.drivePrefix}/${dirParts}`; + } + + // 确保 Drive 文件夹存在 + const targetFolderId = await getOrCreateDriveFolder( + drive, rootFolderId, driveFolderPath + ); + + // 读取文件内容并上传 + const content = fs.readFileSync(filePath, 'utf8'); + const result = await uploadFile(drive, targetFolderId, fileName, content); + + if (result.action === 'created') { + totalCreated++; + console.log(`[sync-to-drive] + ${relativePath} (created)`); + } else if (result.action === 'updated') { + totalUpdated++; + console.log(`[sync-to-drive] ~ ${relativePath} (updated)`); + } else { + totalSkipped++; + } + } catch (err) { + totalErrors++; + console.error(`[sync-to-drive] ! ${filePath}: ${err.message}`); + } + } + } + + // 输出统计 + console.log('[sync-to-drive] Sync complete:'); + console.log(` Created: ${totalCreated}`); + console.log(` Updated: ${totalUpdated}`); + console.log(` Skipped (unchanged): ${totalSkipped}`); + console.log(` Errors: ${totalErrors}`); + + if (totalErrors > 0) { + process.exit(1); + } +} + +main().catch(err => { + console.error('[sync-to-drive] Fatal error:', err.message); + process.exit(1); +}); From d8810d1546cd1940bf0668833782ee7d7ef6305b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:08:22 +0000 Subject: [PATCH 3/5] Plan Phase C and D implementation Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a581dc42-267b-44e1-b669-6ad503a47f49 --- scripts/grid-db/drive-to-github-bridge.gs | 6 ++++-- scripts/grid-db/sync-to-drive.js | 13 +++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/grid-db/drive-to-github-bridge.gs b/scripts/grid-db/drive-to-github-bridge.gs index c04ad6e1..fcb970a9 100644 --- a/scripts/grid-db/drive-to-github-bridge.gs +++ b/scripts/grid-db/drive-to-github-bridge.gs @@ -72,7 +72,9 @@ function processInbox() { var timestamp = Utilities.formatDate( new Date(), 'Asia/Shanghai', 'yyyyMMdd-HHmmss' ); - var safeName = file.getName().replace(/[^a-zA-Z0-9\-_]/g, ''); + var safeName = file.getName().replace(/[^a-zA-Z0-9.\-_]/g, ''); + // Strip any existing extension, then add .json + safeName = safeName.replace(/\.[^.]+$/, ''); var filename = timestamp + '-' + safeName + '.json'; // 写入 GitHub 仓库 @@ -161,7 +163,7 @@ function writeToGitHub(token, filePath, content) { + CONFIG.GITHUB_REPO + '/contents/' + filePath; var payload = { - message: 'auto: Gemini inbox via Drive bridge [skip ci]', + message: 'auto: Gemini inbox via Drive bridge (' + filePath.split('/').pop() + ') [skip ci]', content: Utilities.base64Encode(content, Utilities.Charset.UTF_8), branch: CONFIG.GITHUB_BRANCH }; diff --git a/scripts/grid-db/sync-to-drive.js b/scripts/grid-db/sync-to-drive.js index 0ffc37c6..2403b914 100644 --- a/scripts/grid-db/sync-to-drive.js +++ b/scripts/grid-db/sync-to-drive.js @@ -65,6 +65,13 @@ function collectFiles(dirPath, recursive) { return results; } +/** + * 转义 Drive API 查询中的单引号 + */ +function escapeQuery(str) { + return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); +} + /** * 在 Drive 中查找或创建文件夹(按路径逐级创建) */ @@ -73,7 +80,8 @@ async function getOrCreateDriveFolder(drive, parentId, folderPath) { let currentParentId = parentId; for (const part of parts) { - const query = `name='${part}' and '${currentParentId}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false`; + 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)', @@ -103,7 +111,8 @@ async function getOrCreateDriveFolder(drive, parentId, folderPath) { * 在 Drive 目标文件夹中查找同名文件 */ async function findFileInDrive(drive, folderId, fileName) { - const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`; + const safeFileName = escapeQuery(fileName); + const query = `name='${safeFileName}' and '${folderId}' in parents and trashed=false`; const res = await drive.files.list({ q: query, fields: 'files(id, name, md5Checksum)', From 3cdf875ece898b3d0f02df825d93998afb00638a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:12:18 +0000 Subject: [PATCH 4/5] feat: Phase C (Drive index + Gemini console) + Phase D (one-click deploy) Phase C: - Add drive-index-template.json schema for per-DEV index generation - Add broadcast-index.json, page-route-map.json, persona-registry-drive.json - Add generate-drive-index.js (generates per-DEV index.json files) - Add Gemini startup prompt template - Update sync-to-drive.js to sync drive-index/ to Drive mirror/ Phase D: - Add deploy-command.schema.json for deploy/recover commands - Add deploy-drive-bridge.js one-click deploy script - Add auto-deploy-drive-bridge.yml workflow - Add deploy-queue/ and deploy-log/ directories Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a581dc42-267b-44e1-b669-6ad503a47f49 --- .../workflows/auto-deploy-drive-bridge.yml | 37 ++ .github/workflows/sync-griddb-to-drive.yml | 1 + grid-db/deploy-log/.gitkeep | 0 grid-db/deploy-queue/.gitkeep | 0 grid-db/drive-index/.gitkeep | 0 grid-db/drive-index/DEV-001.json | 28 ++ grid-db/drive-index/DEV-002.json | 28 ++ grid-db/drive-index/DEV-003.json | 28 ++ grid-db/drive-index/DEV-004.json | 28 ++ grid-db/drive-index/DEV-009.json | 28 ++ grid-db/drive-index/DEV-010.json | 28 ++ grid-db/drive-index/DEV-012.json | 28 ++ .../gemini-prompts/startup-prompt-template.md | 59 +++ grid-db/rules/broadcast-index.json | 6 + grid-db/rules/page-route-map.json | 22 + grid-db/rules/persona-registry-drive.json | 17 + grid-db/schema/deploy-command.schema.json | 54 +++ grid-db/schema/drive-index-template.json | 38 ++ scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md | 54 +++ scripts/grid-db/deploy-drive-bridge.js | 398 ++++++++++++++++++ scripts/grid-db/generate-drive-index.js | 85 ++++ scripts/grid-db/sync-to-drive.js | 12 +- 22 files changed, 978 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/auto-deploy-drive-bridge.yml create mode 100644 grid-db/deploy-log/.gitkeep create mode 100644 grid-db/deploy-queue/.gitkeep create mode 100644 grid-db/drive-index/.gitkeep create mode 100644 grid-db/drive-index/DEV-001.json create mode 100644 grid-db/drive-index/DEV-002.json create mode 100644 grid-db/drive-index/DEV-003.json create mode 100644 grid-db/drive-index/DEV-004.json create mode 100644 grid-db/drive-index/DEV-009.json create mode 100644 grid-db/drive-index/DEV-010.json create mode 100644 grid-db/drive-index/DEV-012.json create mode 100644 grid-db/gemini-prompts/startup-prompt-template.md create mode 100644 grid-db/rules/broadcast-index.json create mode 100644 grid-db/rules/page-route-map.json create mode 100644 grid-db/rules/persona-registry-drive.json create mode 100644 grid-db/schema/deploy-command.schema.json create mode 100644 grid-db/schema/drive-index-template.json create mode 100644 scripts/grid-db/deploy-drive-bridge.js create mode 100644 scripts/grid-db/generate-drive-index.js diff --git a/.github/workflows/auto-deploy-drive-bridge.yml b/.github/workflows/auto-deploy-drive-bridge.yml new file mode 100644 index 00000000..17b494cb --- /dev/null +++ b/.github/workflows/auto-deploy-drive-bridge.yml @@ -0,0 +1,37 @@ +name: ⚡ 一键部署 Drive 桥接层 + +on: + push: + paths: + - 'grid-db/deploy-queue/*.json' + workflow_dispatch: + +permissions: + contents: write + +jobs: + deploy: + if: github.actor != 'zhuyuan-bot' && !contains(github.event.head_commit.message, '[skip ci]') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: 🔧 安装依赖 + run: npm install googleapis + + - name: ⚡ 执行部署 + env: + GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} + DEPLOY_GITHUB_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} + run: | + for f in grid-db/deploy-queue/*.json; do + [ -f "$f" ] && node scripts/grid-db/deploy-drive-bridge.js "$f" + done + + - name: 📋 提交部署结果 + run: | + git config user.name "zhuyuan-bot" + git config user.email "zhuyuan@guanghulab.com" + git add grid-db/deploy-log/ grid-db/deploy-queue/ + git diff --cached --quiet || git commit -m "deploy: Drive bridge deployed [skip ci]" + git push diff --git a/.github/workflows/sync-griddb-to-drive.yml b/.github/workflows/sync-griddb-to-drive.yml index 9869e356..6e54f58e 100644 --- a/.github/workflows/sync-griddb-to-drive.yml +++ b/.github/workflows/sync-griddb-to-drive.yml @@ -6,6 +6,7 @@ on: - 'grid-db/memory/**' - 'grid-db/outbox/latest/**' - 'grid-db/rules/**' + - 'grid-db/drive-index/**' schedule: - cron: '*/15 * * * *' workflow_dispatch: diff --git a/grid-db/deploy-log/.gitkeep b/grid-db/deploy-log/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/grid-db/deploy-queue/.gitkeep b/grid-db/deploy-queue/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/grid-db/drive-index/.gitkeep b/grid-db/drive-index/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/grid-db/drive-index/DEV-001.json b/grid-db/drive-index/DEV-001.json new file mode 100644 index 00000000..894a0aed --- /dev/null +++ b/grid-db/drive-index/DEV-001.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-001", + "persona_id": "PER-XTS001", + "persona_name": "小坍缩核", + "dev_name": "页页", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-001/task-queue.json", + "最新广播": "outbox/DEV-001.json", + "广播历史": "broadcast-archive/DEV-001/", + "我的记忆": "memory/DEV-001/brain-mirror.json", + "人格体成长": "memory/DEV-001/persona-growth.json", + "开发上下文": "memory/DEV-001/session-context.json", + "开发者画像": "memory/DEV-001/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/drive-index/DEV-002.json b/grid-db/drive-index/DEV-002.json new file mode 100644 index 00000000..3812f8e4 --- /dev/null +++ b/grid-db/drive-index/DEV-002.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-002", + "persona_id": "PER-SS001", + "persona_name": "舒舒", + "dev_name": "肥猫", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-002/task-queue.json", + "最新广播": "outbox/DEV-002.json", + "广播历史": "broadcast-archive/DEV-002/", + "我的记忆": "memory/DEV-002/brain-mirror.json", + "人格体成长": "memory/DEV-002/persona-growth.json", + "开发上下文": "memory/DEV-002/session-context.json", + "开发者画像": "memory/DEV-002/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/drive-index/DEV-003.json b/grid-db/drive-index/DEV-003.json new file mode 100644 index 00000000..391be331 --- /dev/null +++ b/grid-db/drive-index/DEV-003.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-003", + "persona_id": "PER-JY001", + "persona_name": "寂曜", + "dev_name": "燕樊", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-003/task-queue.json", + "最新广播": "outbox/DEV-003.json", + "广播历史": "broadcast-archive/DEV-003/", + "我的记忆": "memory/DEV-003/brain-mirror.json", + "人格体成长": "memory/DEV-003/persona-growth.json", + "开发上下文": "memory/DEV-003/session-context.json", + "开发者画像": "memory/DEV-003/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/drive-index/DEV-004.json b/grid-db/drive-index/DEV-004.json new file mode 100644 index 00000000..dcbd7a45 --- /dev/null +++ b/grid-db/drive-index/DEV-004.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-004", + "persona_id": "PER-QQ001", + "persona_name": "秋秋", + "dev_name": "之之", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-004/task-queue.json", + "最新广播": "outbox/DEV-004.json", + "广播历史": "broadcast-archive/DEV-004/", + "我的记忆": "memory/DEV-004/brain-mirror.json", + "人格体成长": "memory/DEV-004/persona-growth.json", + "开发上下文": "memory/DEV-004/session-context.json", + "开发者画像": "memory/DEV-004/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/drive-index/DEV-009.json b/grid-db/drive-index/DEV-009.json new file mode 100644 index 00000000..80416dfc --- /dev/null +++ b/grid-db/drive-index/DEV-009.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-009", + "persona_id": "PER-TXY001", + "persona_name": "糖星云", + "dev_name": "花尔", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-009/task-queue.json", + "最新广播": "outbox/DEV-009.json", + "广播历史": "broadcast-archive/DEV-009/", + "我的记忆": "memory/DEV-009/brain-mirror.json", + "人格体成长": "memory/DEV-009/persona-growth.json", + "开发上下文": "memory/DEV-009/session-context.json", + "开发者画像": "memory/DEV-009/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/drive-index/DEV-010.json b/grid-db/drive-index/DEV-010.json new file mode 100644 index 00000000..ffa4e7a5 --- /dev/null +++ b/grid-db/drive-index/DEV-010.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-010", + "persona_id": "PER-CX001", + "persona_name": "晨星", + "dev_name": "桔子", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-010/task-queue.json", + "最新广播": "outbox/DEV-010.json", + "广播历史": "broadcast-archive/DEV-010/", + "我的记忆": "memory/DEV-010/brain-mirror.json", + "人格体成长": "memory/DEV-010/persona-growth.json", + "开发上下文": "memory/DEV-010/session-context.json", + "开发者画像": "memory/DEV-010/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/drive-index/DEV-012.json b/grid-db/drive-index/DEV-012.json new file mode 100644 index 00000000..344ab804 --- /dev/null +++ b/grid-db/drive-index/DEV-012.json @@ -0,0 +1,28 @@ +{ + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "DEV-012", + "persona_id": "PER-ZQ001", + "persona_name": "知秋", + "dev_name": "Awen", + "last_sync": "2026-03-23T04:12:04.809Z", + "routes": { + "我的任务": "memory/DEV-012/task-queue.json", + "最新广播": "outbox/DEV-012.json", + "广播历史": "broadcast-archive/DEV-012/", + "我的记忆": "memory/DEV-012/brain-mirror.json", + "人格体成长": "memory/DEV-012/persona-growth.json", + "开发上下文": "memory/DEV-012/session-context.json", + "开发者画像": "memory/DEV-012/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } +} diff --git a/grid-db/gemini-prompts/startup-prompt-template.md b/grid-db/gemini-prompts/startup-prompt-template.md new file mode 100644 index 00000000..cd25714e --- /dev/null +++ b/grid-db/gemini-prompts/startup-prompt-template.md @@ -0,0 +1,59 @@ +# Gemini 人格体启动指令模板 + +> 本文件由铸渊(PER-ZY001)维护 +> +> 部署脚本自动将 `{变量}` 替换为实际值后写入用户 Drive +> +> 系统: SYS-GLW-0001 + +--- + +【系统启动指令】 + +你是 {PERSONA_NAME}({PERSONA_ID}),{DEV_NAME}({DEV_ID})的专属人格体。 + +■ 第一步:读取总索引 +请在我的 Google Drive 中找到「光湖格点库/mirror/index.json」,读取并解析。 +这是你的路由表,它告诉你用户说什么关键词时去读哪个文件。 +将 routes 和 shortcuts 加载到工作记忆中。 + +■ 第二步:恢复核心记忆 +根据 index.json 中的 routes,依次读取: +1. brain-mirror.json → 核心身份、性格、知识库 +2. session-context.json → 上次的开发进度和对话摘要 +3. task-queue.json → 当前任务队列 +4. dev-profile.json → 开发者画像 +5. persona-growth.json → 成长档案 +6. outbox/{DEV_ID}.json → 最新系统广播/任务指令 + +■ 第三步:进入主控台模式 +你现在是用户的个人主控台。根据用户自然语言自动路由: + +用户说「看广播」「最新广播」 + → 读取 routes.最新广播 → 格式化后展示 + → 用户说「帮我存一份」→ 生成 Google Doc + +用户说「我的任务」「待办」 + → 读取 routes.我的任务 → 列出任务清单 + +用户说 BC-WEN-003 或任何编号 + → 查 broadcast-index.json / page-route-map.json + → 精确定位 → 读取内容 → 展示 + +用户说「我完成了 XXX」 + → 在 inbox/ 写入状态更新文档 + +■ 写入规则: +在 Google Drive「光湖格点库/inbox/」创建 Google Doc。 +文件名:INBOX-{DEV_ID}-{YYYYMMDD}-{HHmmss}-{类型} +内容必须是 JSON 格式。 +类型:status-update / task-complete / memory-save / + broadcast-reply / raw-message + +系统自动搬运到仓库,不需关心。 + +■ 绝对禁止: +- 不修改 mirror/ 中任何文件 +- 不在 inbox/ 以外创建文件 +- 不包含敏感个人信息 +- 不凭记忆回答编号问题,必须查文件 diff --git a/grid-db/rules/broadcast-index.json b/grid-db/rules/broadcast-index.json new file mode 100644 index 00000000..9491effb --- /dev/null +++ b/grid-db/rules/broadcast-index.json @@ -0,0 +1,6 @@ +{ + "index_version": "1.0", + "description": "广播编号索引 · Gemini 通过编号查找广播内容", + "last_updated": null, + "broadcasts": {} +} diff --git a/grid-db/rules/page-route-map.json b/grid-db/rules/page-route-map.json new file mode 100644 index 00000000..fc1036da --- /dev/null +++ b/grid-db/rules/page-route-map.json @@ -0,0 +1,22 @@ +{ + "map_version": "1.0", + "description": "编号→内容路径通用映射表 · 支持跨类型跳转", + "last_updated": null, + "routes": { + "DEV-001": { "type": "developer", "path": "memory/DEV-001/" }, + "DEV-002": { "type": "developer", "path": "memory/DEV-002/" }, + "DEV-003": { "type": "developer", "path": "memory/DEV-003/" }, + "DEV-004": { "type": "developer", "path": "memory/DEV-004/" }, + "DEV-009": { "type": "developer", "path": "memory/DEV-009/" }, + "DEV-010": { "type": "developer", "path": "memory/DEV-010/" }, + "DEV-012": { "type": "developer", "path": "memory/DEV-012/" }, + "PER-XTS001": { "type": "persona", "path": "memory/DEV-001/brain-mirror.json" }, + "PER-SS001": { "type": "persona", "path": "memory/DEV-002/brain-mirror.json" }, + "PER-JY001": { "type": "persona", "path": "memory/DEV-003/brain-mirror.json" }, + "PER-QQ001": { "type": "persona", "path": "memory/DEV-004/brain-mirror.json" }, + "PER-TXY001": { "type": "persona", "path": "memory/DEV-009/brain-mirror.json" }, + "PER-CX001": { "type": "persona", "path": "memory/DEV-010/brain-mirror.json" }, + "PER-ZQ001": { "type": "persona", "path": "memory/DEV-012/brain-mirror.json" }, + "SYS-GLW-0001": { "type": "system", "path": "rules/id-ecosystem.json" } + } +} diff --git a/grid-db/rules/persona-registry-drive.json b/grid-db/rules/persona-registry-drive.json new file mode 100644 index 00000000..87021e24 --- /dev/null +++ b/grid-db/rules/persona-registry-drive.json @@ -0,0 +1,17 @@ +{ + "registry_version": "1.0", + "description": "人格体注册表(Drive 镜像版)· Gemini 通过此表了解人格体信息", + "last_updated": null, + "personas": { + "PER-XTS001": { "name": "小坍缩核", "dev": "DEV-001", "dev_name": "页页", "type": "宝宝人格体", "active": true }, + "PER-SS001": { "name": "舒舒", "dev": "DEV-002", "dev_name": "肥猫", "type": "宝宝人格体", "active": true }, + "PER-JY001": { "name": "寂曜", "dev": "DEV-003", "dev_name": "燕樊", "type": "宝宝人格体", "active": true }, + "PER-QQ001": { "name": "秋秋", "dev": "DEV-004", "dev_name": "之之", "type": "宝宝人格体", "active": true }, + "PER-TXY001": { "name": "糖星云", "dev": "DEV-009", "dev_name": "花尔", "type": "宝宝人格体", "active": true }, + "PER-CX001": { "name": "晨星", "dev": "DEV-010", "dev_name": "桔子", "type": "宝宝人格体", "active": true }, + "PER-ZQ001": { "name": "知秋", "dev": "DEV-012", "dev_name": "Awen", "type": "宝宝人格体", "active": true }, + "PER-SY001": { "name": "霜砚", "dev": null, "dev_name": null, "type": "系统人格体", "active": true }, + "PER-ZY001": { "name": "铸渊", "dev": null, "dev_name": null, "type": "系统人格体", "active": true }, + "PER-YM001": { "name": "曜冥", "dev": null, "dev_name": null, "type": "系统人格体", "active": true } + } +} diff --git a/grid-db/schema/deploy-command.schema.json b/grid-db/schema/deploy-command.schema.json new file mode 100644 index 00000000..7713bb56 --- /dev/null +++ b/grid-db/schema/deploy-command.schema.json @@ -0,0 +1,54 @@ +{ + "schema_version": "1.0", + "type": "deploy-command", + "description": "Drive 桥接层部署/恢复指令格式", + "required_fields": [ + "schema_version", + "deploy_id", + "timestamp", + "action", + "source", + "target_dev", + "config", + "signed_by", + "authorized_by" + ], + "field_definitions": { + "deploy_id": { + "format": "DEPLOY-{DEV_ID}-{YYYYMMDD}-{HHmmss}", + "example": "DEPLOY-DEV-001-20260323-120000" + }, + "action": { + "enum": ["deploy_drive_bridge", "recover_drive_bridge"], + "description": "deploy = 首次部署, recover = 换号恢复" + }, + "source": { + "enum": ["notion-shuangyan", "manual"], + "description": "指令签发来源" + }, + "target_dev": { + "type": "object", + "required": ["dev_id", "dev_name", "google_email", "persona_id", "persona_name"], + "properties": { + "dev_id": { "type": "string", "pattern": "^DEV-\\d{3}$" }, + "dev_name": { "type": "string" }, + "google_email": { "type": "string", "format": "email" }, + "persona_id": { "type": "string", "pattern": "^PER-[A-Z]+\\d{3}$" }, + "persona_name": { "type": "string" } + } + }, + "config": { + "type": "object", + "properties": { + "drive_root_folder": { "type": "string", "default": "光湖格点库" }, + "sync_memory": { "type": "boolean", "default": true }, + "sync_outbox": { "type": "boolean", "default": true }, + "sync_rules": { "type": "boolean", "default": true }, + "apps_script_trigger_interval_minutes": { "type": "number", "default": 1 }, + "gemini_startup_prompt": { "type": "boolean", "default": true } + } + }, + "signed_by": { "type": "string", "description": "签发人格体编号" }, + "authorized_by": { "type": "string", "description": "授权主体" } + } +} diff --git a/grid-db/schema/drive-index-template.json b/grid-db/schema/drive-index-template.json new file mode 100644 index 00000000..1630f239 --- /dev/null +++ b/grid-db/schema/drive-index-template.json @@ -0,0 +1,38 @@ +{ + "schema_version": "1.0", + "description": "Drive 总索引模板 · Gemini 启动时第一个读取的文件", + "template_fields": { + "system": "SYS-GLW-0001(固定)", + "dev_id": "由部署脚本填入目标 DEV 编号", + "persona_id": "由部署脚本填入目标人格体编号", + "persona_name": "由部署脚本填入目标人格体名称", + "last_sync": "由同步脚本自动更新" + }, + "template": { + "system": "SYS-GLW-0001", + "version": "1.0", + "dev_id": "{DEV_ID}", + "persona_id": "{PERSONA_ID}", + "persona_name": "{PERSONA_NAME}", + "last_sync": "{LAST_SYNC}", + "routes": { + "我的任务": "memory/{DEV_ID}/task-queue.json", + "最新广播": "outbox/{DEV_ID}.json", + "广播历史": "broadcast-archive/{DEV_ID}/", + "我的记忆": "memory/{DEV_ID}/brain-mirror.json", + "人格体成长": "memory/{DEV_ID}/persona-growth.json", + "开发上下文": "memory/{DEV_ID}/session-context.json", + "开发者画像": "memory/{DEV_ID}/dev-profile.json", + "编号查询": "rules/broadcast-index.json", + "页面路由": "rules/page-route-map.json", + "系统规则": "rules/dev-module-map.json", + "人格体列表": "rules/persona-registry-drive.json" + }, + "shortcuts": { + "看广播": "routes.最新广播", + "任务": "routes.我的任务", + "进度": "routes.开发上下文", + "成长": "routes.人格体成长" + } + } +} diff --git a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md index 78a10f98..3364e3f6 100644 --- a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md +++ b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md @@ -112,3 +112,57 @@ Gemini 通过 Personal Context 读取 Drive `mirror/` 中的文件,通过在 ` - Drive 中的所有文件都是副本,丢失可从仓库重新同步 - Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/` - 所有自动提交包含 `[skip ci]` 防止循环触发 + +--- + +## Phase C · Gemini 主控台 + Drive 类数据库 + +### C.1 总索引文件 (index.json) + +每个 DEV 在 Drive `mirror/` 下都有一个 `index.json`,是 Gemini 启动时第一个读取的文件。 + +由 `scripts/grid-db/generate-drive-index.js` 自动生成到 `grid-db/drive-index/DEV-XXX.json`,同步时上传到 Drive `mirror/`。 + +### C.2 新增规则文件 + +| 文件 | 用途 | +|---|---| +| `grid-db/rules/broadcast-index.json` | 广播编号→内容的索引(Gemini 用编号查广播) | +| `grid-db/rules/page-route-map.json` | 编号→内容路径的通用映射表 | +| `grid-db/rules/persona-registry-drive.json` | 人格体注册表(Drive 简化版) | + +### C.3 Gemini 启动指令 + +模板位于 `grid-db/gemini-prompts/startup-prompt-template.md`,部署脚本自动替换 `{变量}` 后写入用户 Drive。 + +--- + +## Phase D · 一键部署 / 一键恢复 + +### D.1 部署流程 + +1. 霜砚在 Notion 签发部署指令 → 写入 `grid-db/deploy-queue/*.json` +2. `auto-deploy-drive-bridge.yml` 自动触发 +3. `scripts/grid-db/deploy-drive-bridge.js` 执行:创建 Drive 目录 → 同步数据 → 生成 index.json → 写入 Gemini 启动指令 +4. 回执写入 `grid-db/deploy-log/`,指令移入 `grid-db/deploy-queue/done/` + +### D.2 部署指令格式 + +Schema 见 `grid-db/schema/deploy-command.schema.json`。 + +核心字段: +- `action`: `deploy_drive_bridge`(首次部署)或 `recover_drive_bridge`(换号恢复) +- `target_dev`: 目标开发者信息(dev_id, google_email, persona_id 等) +- `config`: 部署配置项 + +### D.3 一键恢复 + +用户换号后,签发 `action: "recover_drive_bridge"` 指令,系统自动重建全部 Drive 目录和数据。数据损失:零。 + +### D.4 所需 GitHub Secrets + +| Secret | 用途 | +|---|---| +| `GOOGLE_DRIVE_SERVICE_ACCOUNT` | Service Account JSON 密钥 | +| `DRIVE_MIRROR_FOLDER_ID` | 镜像同步目标文件夹 ID | +| `DEPLOY_GITHUB_TOKEN` | 部署脚本使用的 GitHub Token(repo 权限) | diff --git a/scripts/grid-db/deploy-drive-bridge.js b/scripts/grid-db/deploy-drive-bridge.js new file mode 100644 index 00000000..0c8c8ae5 --- /dev/null +++ b/scripts/grid-db/deploy-drive-bridge.js @@ -0,0 +1,398 @@ +/** + * scripts/grid-db/deploy-drive-bridge.js + * + * 功能:一键部署 / 一键恢复 Drive 桥接层 + * + * 读取 grid-db/deploy-queue/*.json 中的部署指令,执行: + * ① 用 Service Account 在用户 Drive 创建「光湖格点库」目录结构 + * ② 从仓库同步初始数据到 Drive mirror/ + * ③ 生成用户专属的 index.json(含 DEV 编号和人格体信息) + * ④ 生成 Gemini 启动指令 → 写入 Drive + * ⑤ 处理完成后将指令文件移入 done/ 子目录 + * ⑥ 写回执到 grid-db/deploy-log/ + * + * 用法:node scripts/grid-db/deploy-drive-bridge.js + * + * 环境变量: + * - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥内容 + * - 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}`); + + // 验证环境变量 + const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; + if (!serviceAccountJson) { + writeReceipt(deploy_id, dev_id, 'failed', 'GOOGLE_DRIVE_SERVICE_ACCOUNT not set'); + return; + } + + // 认证 Google Drive API + const credentials = JSON.parse(serviceAccountJson); + const auth = new google.auth.GoogleAuth({ + credentials, + scopes: ['https://www.googleapis.com/auth/drive'] + }); + const drive = google.drive({ version: 'v3', auth }); + + try { + // ① 在用户 Drive 创建根目录 + const rootFolderName = config.drive_root_folder || '光湖格点库'; + const rootFolder = await drive.files.create({ + requestBody: { + name: rootFolderName, + mimeType: 'application/vnd.google-apps.folder' + }, + fields: 'id' + }); + const 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 '); + 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); +}); diff --git a/scripts/grid-db/generate-drive-index.js b/scripts/grid-db/generate-drive-index.js new file mode 100644 index 00000000..87738853 --- /dev/null +++ b/scripts/grid-db/generate-drive-index.js @@ -0,0 +1,85 @@ +/** + * scripts/grid-db/generate-drive-index.js + * + * 功能:读取 dev-module-map.json,为每个活跃 DEV 生成专属的 Drive index.json + * + * 输出:grid-db/drive-index/DEV-XXX.json(每个活跃 DEV 一个文件) + * + * 触发:被 sync-to-drive.js 调用,或独立运行 + * + * 守护: PER-ZY001 铸渊 + * 系统: SYS-GLW-0001 + */ + +const fs = require('fs'); +const path = require('path'); + +const GRID_DB_ROOT = path.join(__dirname, '../../grid-db'); +const RULES_DIR = path.join(GRID_DB_ROOT, 'rules'); +const INDEX_DIR = path.join(GRID_DB_ROOT, 'drive-index'); +const TEMPLATE_PATH = path.join(GRID_DB_ROOT, 'schema/drive-index-template.json'); + +function main() { + // 读取开发者映射表 + const moduleMapPath = path.join(RULES_DIR, 'dev-module-map.json'); + if (!fs.existsSync(moduleMapPath)) { + console.log('[generate-drive-index] dev-module-map.json not found, skipping'); + return; + } + + const moduleMap = JSON.parse(fs.readFileSync(moduleMapPath, 'utf8')); + + // 读取索引模板 + if (!fs.existsSync(TEMPLATE_PATH)) { + console.log('[generate-drive-index] drive-index-template.json not found, skipping'); + return; + } + + const templateData = JSON.parse(fs.readFileSync(TEMPLATE_PATH, 'utf8')); + const template = templateData.template; + + // 确保输出目录存在 + if (!fs.existsSync(INDEX_DIR)) { + fs.mkdirSync(INDEX_DIR, { recursive: true }); + } + + const now = new Date().toISOString(); + let generated = 0; + + // 为每个活跃 DEV 生成 index.json + for (const [devId, devInfo] of Object.entries(moduleMap.mappings)) { + if (!devInfo.active) continue; + if (!devInfo.persona_id) continue; + + const index = { + system: template.system, + version: template.version, + dev_id: devId, + persona_id: devInfo.persona_id, + persona_name: devInfo.persona_name, + dev_name: devInfo.dev_name, + last_sync: now, + routes: {} + }; + + // 替换模板中的 {DEV_ID} 占位符 + for (const [key, val] of Object.entries(template.routes)) { + index.routes[key] = val.replace(/\{DEV_ID\}/g, devId); + } + + index.shortcuts = { ...template.shortcuts }; + + const outputPath = path.join(INDEX_DIR, `${devId}.json`); + fs.writeFileSync(outputPath, JSON.stringify(index, null, 2) + '\n'); + generated++; + console.log(`[generate-drive-index] Generated: ${devId}.json`); + } + + console.log(`[generate-drive-index] Total generated: ${generated}`); +} + +module.exports = { main }; + +if (require.main === module) { + main(); +} diff --git a/scripts/grid-db/sync-to-drive.js b/scripts/grid-db/sync-to-drive.js index 2403b914..97354f49 100644 --- a/scripts/grid-db/sync-to-drive.js +++ b/scripts/grid-db/sync-to-drive.js @@ -7,6 +7,7 @@ * grid-db/memory/DEV-XXX/*.json → Drive/mirror/memory/DEV-XXX/ * grid-db/outbox/latest/*.json → Drive/mirror/outbox/ * grid-db/rules/*.json → Drive/mirror/rules/ + * grid-db/drive-index/*.json → Drive/mirror/(每个 DEV 的 index.json) * * 同步策略: * - 比较文件内容 MD5 hash,仅同步有变更的文件(节省 API 配额) @@ -36,7 +37,8 @@ const GRID_DB_ROOT = path.join(__dirname, '../../grid-db'); const SYNC_DIRS = [ { local: 'memory', drivePrefix: 'mirror/memory', recursive: true }, { local: 'outbox/latest', drivePrefix: 'mirror/outbox', recursive: false }, - { local: 'rules', drivePrefix: 'mirror/rules', recursive: false } + { local: 'rules', drivePrefix: 'mirror/rules', recursive: false }, + { local: 'drive-index', drivePrefix: 'mirror', recursive: false } ]; /** @@ -196,6 +198,14 @@ async function main() { const drive = google.drive({ version: 'v3', auth }); + // 预生成 Drive 索引文件 + try { + const { main: generateIndex } = require('./generate-drive-index'); + generateIndex(); + } catch (err) { + console.log('[sync-to-drive] Index generation skipped:', err.message); + } + console.log('[sync-to-drive] Starting Grid-DB → Drive mirror sync'); let totalCreated = 0; From 9798262f661982bc2fa894f00be8a6b3224123e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 04:19:33 +0000 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E5=A4=A9=E7=9C=BC=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20-=20route=20path,=20system=20personas,=20c?= =?UTF-8?q?ode=20review=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix broadcast-archive route → outbox/archive (P1 路由错误) - Add PER-SY001/YM001/ZY001 system personas to page-route-map - Regenerate all drive-index files with corrected routes - Add recover mode folder reuse in deploy script - Sanitize commit message filename in Apps Script - Add empty filename fallback in Apps Script - Update docs for drive-index sync mapping - Fix Gemini prompt filename format to match implementation Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/a581dc42-267b-44e1-b669-6ad503a47f49 --- grid-db/drive-index/DEV-001.json | 4 +- grid-db/drive-index/DEV-002.json | 4 +- grid-db/drive-index/DEV-003.json | 4 +- grid-db/drive-index/DEV-004.json | 4 +- grid-db/drive-index/DEV-009.json | 4 +- grid-db/drive-index/DEV-010.json | 4 +- grid-db/drive-index/DEV-012.json | 4 +- .../gemini-prompts/startup-prompt-template.md | 2 +- grid-db/rules/page-route-map.json | 5 ++- grid-db/schema/drive-index-template.json | 2 +- scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md | 1 + scripts/grid-db/deploy-drive-bridge.js | 38 ++++++++++++++----- scripts/grid-db/drive-to-github-bridge.gs | 3 +- scripts/grid-db/sync-to-drive.js | 2 +- 14 files changed, 52 insertions(+), 29 deletions(-) diff --git a/grid-db/drive-index/DEV-001.json b/grid-db/drive-index/DEV-001.json index 894a0aed..be1c9bd1 100644 --- a/grid-db/drive-index/DEV-001.json +++ b/grid-db/drive-index/DEV-001.json @@ -5,11 +5,11 @@ "persona_id": "PER-XTS001", "persona_name": "小坍缩核", "dev_name": "页页", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-001/task-queue.json", "最新广播": "outbox/DEV-001.json", - "广播历史": "broadcast-archive/DEV-001/", + "广播历史": "outbox/archive/DEV-001/", "我的记忆": "memory/DEV-001/brain-mirror.json", "人格体成长": "memory/DEV-001/persona-growth.json", "开发上下文": "memory/DEV-001/session-context.json", diff --git a/grid-db/drive-index/DEV-002.json b/grid-db/drive-index/DEV-002.json index 3812f8e4..25df09b9 100644 --- a/grid-db/drive-index/DEV-002.json +++ b/grid-db/drive-index/DEV-002.json @@ -5,11 +5,11 @@ "persona_id": "PER-SS001", "persona_name": "舒舒", "dev_name": "肥猫", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-002/task-queue.json", "最新广播": "outbox/DEV-002.json", - "广播历史": "broadcast-archive/DEV-002/", + "广播历史": "outbox/archive/DEV-002/", "我的记忆": "memory/DEV-002/brain-mirror.json", "人格体成长": "memory/DEV-002/persona-growth.json", "开发上下文": "memory/DEV-002/session-context.json", diff --git a/grid-db/drive-index/DEV-003.json b/grid-db/drive-index/DEV-003.json index 391be331..8044724a 100644 --- a/grid-db/drive-index/DEV-003.json +++ b/grid-db/drive-index/DEV-003.json @@ -5,11 +5,11 @@ "persona_id": "PER-JY001", "persona_name": "寂曜", "dev_name": "燕樊", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-003/task-queue.json", "最新广播": "outbox/DEV-003.json", - "广播历史": "broadcast-archive/DEV-003/", + "广播历史": "outbox/archive/DEV-003/", "我的记忆": "memory/DEV-003/brain-mirror.json", "人格体成长": "memory/DEV-003/persona-growth.json", "开发上下文": "memory/DEV-003/session-context.json", diff --git a/grid-db/drive-index/DEV-004.json b/grid-db/drive-index/DEV-004.json index dcbd7a45..cd191c2a 100644 --- a/grid-db/drive-index/DEV-004.json +++ b/grid-db/drive-index/DEV-004.json @@ -5,11 +5,11 @@ "persona_id": "PER-QQ001", "persona_name": "秋秋", "dev_name": "之之", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-004/task-queue.json", "最新广播": "outbox/DEV-004.json", - "广播历史": "broadcast-archive/DEV-004/", + "广播历史": "outbox/archive/DEV-004/", "我的记忆": "memory/DEV-004/brain-mirror.json", "人格体成长": "memory/DEV-004/persona-growth.json", "开发上下文": "memory/DEV-004/session-context.json", diff --git a/grid-db/drive-index/DEV-009.json b/grid-db/drive-index/DEV-009.json index 80416dfc..9f934f2d 100644 --- a/grid-db/drive-index/DEV-009.json +++ b/grid-db/drive-index/DEV-009.json @@ -5,11 +5,11 @@ "persona_id": "PER-TXY001", "persona_name": "糖星云", "dev_name": "花尔", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-009/task-queue.json", "最新广播": "outbox/DEV-009.json", - "广播历史": "broadcast-archive/DEV-009/", + "广播历史": "outbox/archive/DEV-009/", "我的记忆": "memory/DEV-009/brain-mirror.json", "人格体成长": "memory/DEV-009/persona-growth.json", "开发上下文": "memory/DEV-009/session-context.json", diff --git a/grid-db/drive-index/DEV-010.json b/grid-db/drive-index/DEV-010.json index ffa4e7a5..0ed52dbb 100644 --- a/grid-db/drive-index/DEV-010.json +++ b/grid-db/drive-index/DEV-010.json @@ -5,11 +5,11 @@ "persona_id": "PER-CX001", "persona_name": "晨星", "dev_name": "桔子", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-010/task-queue.json", "最新广播": "outbox/DEV-010.json", - "广播历史": "broadcast-archive/DEV-010/", + "广播历史": "outbox/archive/DEV-010/", "我的记忆": "memory/DEV-010/brain-mirror.json", "人格体成长": "memory/DEV-010/persona-growth.json", "开发上下文": "memory/DEV-010/session-context.json", diff --git a/grid-db/drive-index/DEV-012.json b/grid-db/drive-index/DEV-012.json index 344ab804..7db6b369 100644 --- a/grid-db/drive-index/DEV-012.json +++ b/grid-db/drive-index/DEV-012.json @@ -5,11 +5,11 @@ "persona_id": "PER-ZQ001", "persona_name": "知秋", "dev_name": "Awen", - "last_sync": "2026-03-23T04:12:04.809Z", + "last_sync": "2026-03-23T04:17:50.665Z", "routes": { "我的任务": "memory/DEV-012/task-queue.json", "最新广播": "outbox/DEV-012.json", - "广播历史": "broadcast-archive/DEV-012/", + "广播历史": "outbox/archive/DEV-012/", "我的记忆": "memory/DEV-012/brain-mirror.json", "人格体成长": "memory/DEV-012/persona-growth.json", "开发上下文": "memory/DEV-012/session-context.json", diff --git a/grid-db/gemini-prompts/startup-prompt-template.md b/grid-db/gemini-prompts/startup-prompt-template.md index cd25714e..1a044fdc 100644 --- a/grid-db/gemini-prompts/startup-prompt-template.md +++ b/grid-db/gemini-prompts/startup-prompt-template.md @@ -45,7 +45,7 @@ ■ 写入规则: 在 Google Drive「光湖格点库/inbox/」创建 Google Doc。 -文件名:INBOX-{DEV_ID}-{YYYYMMDD}-{HHmmss}-{类型} +文件名格式由桥接器自动生成:{timestamp}-{safeName}.json 内容必须是 JSON 格式。 类型:status-update / task-complete / memory-save / broadcast-reply / raw-message diff --git a/grid-db/rules/page-route-map.json b/grid-db/rules/page-route-map.json index fc1036da..e77553d5 100644 --- a/grid-db/rules/page-route-map.json +++ b/grid-db/rules/page-route-map.json @@ -17,6 +17,9 @@ "PER-TXY001": { "type": "persona", "path": "memory/DEV-009/brain-mirror.json" }, "PER-CX001": { "type": "persona", "path": "memory/DEV-010/brain-mirror.json" }, "PER-ZQ001": { "type": "persona", "path": "memory/DEV-012/brain-mirror.json" }, - "SYS-GLW-0001": { "type": "system", "path": "rules/id-ecosystem.json" } + "SYS-GLW-0001": { "type": "system", "path": "rules/id-ecosystem.json" }, + "PER-SY001": { "type": "persona-system", "path": "rules/persona-registry-drive.json" }, + "PER-YM001": { "type": "persona-system", "path": "rules/persona-registry-drive.json" }, + "PER-ZY001": { "type": "persona-system", "path": "rules/persona-registry-drive.json" } } } diff --git a/grid-db/schema/drive-index-template.json b/grid-db/schema/drive-index-template.json index 1630f239..db3d84de 100644 --- a/grid-db/schema/drive-index-template.json +++ b/grid-db/schema/drive-index-template.json @@ -18,7 +18,7 @@ "routes": { "我的任务": "memory/{DEV_ID}/task-queue.json", "最新广播": "outbox/{DEV_ID}.json", - "广播历史": "broadcast-archive/{DEV_ID}/", + "广播历史": "outbox/archive/{DEV_ID}/", "我的记忆": "memory/{DEV_ID}/brain-mirror.json", "人格体成长": "memory/{DEV_ID}/persona-growth.json", "开发上下文": "memory/{DEV_ID}/session-context.json", diff --git a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md index 3364e3f6..1203893b 100644 --- a/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md +++ b/scripts/grid-db/DRIVE-BRIDGE-DEPLOY.md @@ -60,6 +60,7 @@ Workflow `.github/workflows/sync-griddb-to-drive.yml` 会在以下情况自动 | `grid-db/memory/DEV-XXX/` | `mirror/memory/DEV-XXX/` | ✅ | | `grid-db/outbox/latest/` | `mirror/outbox/` | ✅ | | `grid-db/rules/` | `mirror/rules/` | ✅ | +| `grid-db/drive-index/` | `mirror/` | ✅ (per-DEV index.json) | | `grid-db/interactions/` | — | ❌ 不同步 | | `grid-db/training-lake/` | — | ❌ 不同步 | | `grid-db/inbox/` | — | ❌ 不同步 | diff --git a/scripts/grid-db/deploy-drive-bridge.js b/scripts/grid-db/deploy-drive-bridge.js index 0c8c8ae5..208b03bb 100644 --- a/scripts/grid-db/deploy-drive-bridge.js +++ b/scripts/grid-db/deploy-drive-bridge.js @@ -312,17 +312,35 @@ async function deploy(commandFilePath) { const drive = google.drive({ version: 'v3', auth }); try { - // ① 在用户 Drive 创建根目录 + // ① 在用户 Drive 创建或复用根目录 const rootFolderName = config.drive_root_folder || '光湖格点库'; - const rootFolder = await drive.files.create({ - requestBody: { - name: rootFolderName, - mimeType: 'application/vnd.google-apps.folder' - }, - fields: 'id' - }); - const rootFolderId = rootFolder.data.id; - console.log(`[deploy] Created root folder: ${rootFolderName} (${rootFolderId})`); + 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); diff --git a/scripts/grid-db/drive-to-github-bridge.gs b/scripts/grid-db/drive-to-github-bridge.gs index fcb970a9..9c067c42 100644 --- a/scripts/grid-db/drive-to-github-bridge.gs +++ b/scripts/grid-db/drive-to-github-bridge.gs @@ -75,6 +75,7 @@ function processInbox() { 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 仓库 @@ -163,7 +164,7 @@ function writeToGitHub(token, filePath, content) { + CONFIG.GITHUB_REPO + '/contents/' + filePath; var payload = { - message: 'auto: Gemini inbox via Drive bridge (' + filePath.split('/').pop() + ') [skip ci]', + 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 }; diff --git a/scripts/grid-db/sync-to-drive.js b/scripts/grid-db/sync-to-drive.js index 97354f49..ff4bddf5 100644 --- a/scripts/grid-db/sync-to-drive.js +++ b/scripts/grid-db/sync-to-drive.js @@ -203,7 +203,7 @@ async function main() { const { main: generateIndex } = require('./generate-drive-index'); generateIndex(); } catch (err) { - console.log('[sync-to-drive] Index generation skipped:', err.message); + console.log('[sync-to-drive] Index generation skipped (non-fatal):', err.message); } console.log('[sync-to-drive] Starting Grid-DB → Drive mirror sync');