Merge pull request #161 from qinfendebingshuo/copilot/zy-drive-bridge-2026-0323-001
feat: Grid-DB ↔ Google Drive bridge (ZY-DRIVE-BRIDGE-2026-0323-001)
This commit is contained in:
commit
f53d887fc1
|
|
@ -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
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
name: 🪞 Grid-DB → Google Drive 镜像同步
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'grid-db/memory/**'
|
||||
- 'grid-db/outbox/latest/**'
|
||||
- 'grid-db/rules/**'
|
||||
- 'grid-db/drive-index/**'
|
||||
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
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-001/task-queue.json",
|
||||
"最新广播": "outbox/DEV-001.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-002/task-queue.json",
|
||||
"最新广播": "outbox/DEV-002.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-003/task-queue.json",
|
||||
"最新广播": "outbox/DEV-003.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-004/task-queue.json",
|
||||
"最新广播": "outbox/DEV-004.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-009/task-queue.json",
|
||||
"最新广播": "outbox/DEV-009.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-010/task-queue.json",
|
||||
"最新广播": "outbox/DEV-010.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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:17:50.665Z",
|
||||
"routes": {
|
||||
"我的任务": "memory/DEV-012/task-queue.json",
|
||||
"最新广播": "outbox/DEV-012.json",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
|
|
@ -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。
|
||||
文件名格式由桥接器自动生成:{timestamp}-{safeName}.json
|
||||
内容必须是 JSON 格式。
|
||||
类型:status-update / task-complete / memory-save /
|
||||
broadcast-reply / raw-message
|
||||
|
||||
系统自动搬运到仓库,不需关心。
|
||||
|
||||
■ 绝对禁止:
|
||||
- 不修改 mirror/ 中任何文件
|
||||
- 不在 inbox/ 以外创建文件
|
||||
- 不包含敏感个人信息
|
||||
- 不凭记忆回答编号问题,必须查文件
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"index_version": "1.0",
|
||||
"description": "广播编号索引 · Gemini 通过编号查找广播内容",
|
||||
"last_updated": null,
|
||||
"broadcasts": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"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" },
|
||||
"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" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "授权主体" }
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
"广播历史": "outbox/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.人格体成长"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# 🪞 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/drive-index/` | `mirror/` | ✅ (per-DEV index.json) |
|
||||
| `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]` 防止循环触发
|
||||
|
||||
---
|
||||
|
||||
## 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 权限) |
|
||||
|
|
@ -0,0 +1,416 @@
|
|||
/**
|
||||
* 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 <deploy-command.json>
|
||||
*
|
||||
* 环境变量:
|
||||
* - 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 || '光湖格点库';
|
||||
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);
|
||||
});
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
/**
|
||||
* 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 分钟
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
/**
|
||||
* 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/
|
||||
* grid-db/drive-index/*.json → Drive/mirror/(每个 DEV 的 index.json)
|
||||
*
|
||||
* 同步策略:
|
||||
* - 比较文件内容 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 },
|
||||
{ local: 'drive-index', drivePrefix: 'mirror', 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 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(`[sync-to-drive] Created folder: ${part}`);
|
||||
}
|
||||
}
|
||||
|
||||
return currentParentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 Drive 目标文件夹中查找同名文件
|
||||
*/
|
||||
async function findFileInDrive(drive, folderId, fileName) {
|
||||
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)',
|
||||
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 });
|
||||
|
||||
// 预生成 Drive 索引文件
|
||||
try {
|
||||
const { main: generateIndex } = require('./generate-drive-index');
|
||||
generateIndex();
|
||||
} catch (err) {
|
||||
console.log('[sync-to-drive] Index generation skipped (non-fatal):', err.message);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
Loading…
Reference in New Issue