301 lines
8.2 KiB
JavaScript
301 lines
8.2 KiB
JavaScript
|
|
/**
|
|||
|
|
* ═══════════════════════════════════════════════════════════
|
|||
|
|
* 🗄️ COS 双桶桥接模块 · COS Dual-Bucket Bridge
|
|||
|
|
* ═══════════════════════════════════════════════════════════
|
|||
|
|
*
|
|||
|
|
* 编号: ZY-COS-BRIDGE-001
|
|||
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
|||
|
|
* 版权: 国作登字-2026-A-00037559
|
|||
|
|
*
|
|||
|
|
* 双桶架构:
|
|||
|
|
* zy-core-bucket — 核心人格体大脑 (铸渊 + Notion人格体各占一半)
|
|||
|
|
* zy-corpus-bucket — 语料库 (GPT-4o聊天记录 + Notion数据导出)
|
|||
|
|
*
|
|||
|
|
* 轻量级COS HTTP API封装 — 不依赖SDK,直接用签名请求
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
const https = require('https');
|
|||
|
|
const path = require('path');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
|
|||
|
|
// ─── 配置 ───
|
|||
|
|
const COS_CONFIG = {
|
|||
|
|
secretId: process.env.ZY_OSS_KEY || '',
|
|||
|
|
secretKey: process.env.ZY_OSS_SECRET || '',
|
|||
|
|
region: process.env.ZY_COS_REGION || 'ap-guangzhou',
|
|||
|
|
buckets: {
|
|||
|
|
core: {
|
|||
|
|
name: 'zy-core-bucket-1317346199',
|
|||
|
|
purpose: '核心人格体大脑',
|
|||
|
|
structure: {
|
|||
|
|
'zhuyuan/': '铸渊大脑 — GitHub侧人格体记忆',
|
|||
|
|
'notion-personas/': 'Notion侧人格体记忆',
|
|||
|
|
'shared/': '共享认知层 — 双侧共用',
|
|||
|
|
'users/': '用户记忆档案 — 按编号存储',
|
|||
|
|
'index/': '索引文件 — 快速检索'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
corpus: {
|
|||
|
|
name: 'zy-corpus-bucket-1317346199',
|
|||
|
|
purpose: '语料库',
|
|||
|
|
structure: {
|
|||
|
|
'gpt4o-exports/': 'GPT-4o聊天记录导出',
|
|||
|
|
'notion-exports/': 'Notion数据库导出',
|
|||
|
|
'processed/': '已处理的结构化数据',
|
|||
|
|
'thinking-patterns/': '思维模式提取结果',
|
|||
|
|
'training/': '训练数据集'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成COS API签名
|
|||
|
|
*/
|
|||
|
|
function generateSignature(method, pathname, headers, secretId, secretKey) {
|
|||
|
|
const now = Math.floor(Date.now() / 1000);
|
|||
|
|
const expiry = now + 600; // 10分钟有效
|
|||
|
|
const keyTime = `${now};${expiry}`;
|
|||
|
|
|
|||
|
|
// SignKey
|
|||
|
|
const signKey = crypto.createHmac('sha1', secretKey).update(keyTime).digest('hex');
|
|||
|
|
|
|||
|
|
// HttpString
|
|||
|
|
const httpString = `${method.toLowerCase()}\n${pathname}\n\nhost=${headers.Host}\n`;
|
|||
|
|
|
|||
|
|
// StringToSign
|
|||
|
|
const sha1HttpString = crypto.createHash('sha1').update(httpString).digest('hex');
|
|||
|
|
const stringToSign = `sha1\n${keyTime}\n${sha1HttpString}\n`;
|
|||
|
|
|
|||
|
|
// Signature
|
|||
|
|
const signature = crypto.createHmac('sha1', signKey).update(stringToSign).digest('hex');
|
|||
|
|
|
|||
|
|
return `q-sign-algorithm=sha1&q-ak=${secretId}&q-sign-time=${keyTime}&q-key-time=${keyTime}&q-header-list=host&q-url-param-list=&q-signature=${signature}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取桶的完整域名
|
|||
|
|
* 注意: bucketName需包含appid后缀,如 zy-core-bucket-1234567890
|
|||
|
|
* 冰朔在腾讯云创建桶时的完整名称需配置在COS_CONFIG中
|
|||
|
|
*/
|
|||
|
|
function getBucketHost(bucketName, region) {
|
|||
|
|
return `${bucketName}.cos.${region}.myqcloud.com`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 发起COS请求
|
|||
|
|
*/
|
|||
|
|
function cosRequest(bucketName, objectKey, method, body, contentType) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const host = getBucketHost(bucketName, COS_CONFIG.region);
|
|||
|
|
const pathname = '/' + objectKey;
|
|||
|
|
|
|||
|
|
const headers = {
|
|||
|
|
Host: host,
|
|||
|
|
'Content-Type': contentType || 'application/octet-stream'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (body) {
|
|||
|
|
headers['Content-Length'] = Buffer.byteLength(body);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const authorization = generateSignature(
|
|||
|
|
method, pathname, headers,
|
|||
|
|
COS_CONFIG.secretId, COS_CONFIG.secretKey
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
headers['Authorization'] = authorization;
|
|||
|
|
|
|||
|
|
const options = {
|
|||
|
|
hostname: host,
|
|||
|
|
port: 443,
|
|||
|
|
path: pathname,
|
|||
|
|
method: method,
|
|||
|
|
headers: headers,
|
|||
|
|
timeout: 30000
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const req = https.request(options, (res) => {
|
|||
|
|
const chunks = [];
|
|||
|
|
res.on('data', chunk => chunks.push(chunk));
|
|||
|
|
res.on('end', () => {
|
|||
|
|
const responseBody = Buffer.concat(chunks).toString();
|
|||
|
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|||
|
|
resolve({ statusCode: res.statusCode, body: responseBody, headers: res.headers });
|
|||
|
|
} else {
|
|||
|
|
reject(new Error(`COS ${method} ${pathname} failed: ${res.statusCode} - ${responseBody}`));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.on('error', reject);
|
|||
|
|
req.on('timeout', () => { req.destroy(); reject(new Error('COS request timeout')); });
|
|||
|
|
|
|||
|
|
if (body) req.write(body);
|
|||
|
|
req.end();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─── 公开API ───
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 写入对象到核心桶
|
|||
|
|
*/
|
|||
|
|
async function writeCore(key, data) {
|
|||
|
|
const body = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|||
|
|
return cosRequest(COS_CONFIG.buckets.core.name, key, 'PUT', body, 'application/json');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取核心桶对象
|
|||
|
|
*/
|
|||
|
|
async function readCore(key) {
|
|||
|
|
const result = await cosRequest(COS_CONFIG.buckets.core.name, key, 'GET');
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(result.body);
|
|||
|
|
} catch {
|
|||
|
|
return result.body;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 写入对象到语料桶
|
|||
|
|
*/
|
|||
|
|
async function writeCorpus(key, data) {
|
|||
|
|
const body = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|||
|
|
return cosRequest(COS_CONFIG.buckets.corpus.name, key, 'PUT', body, 'application/json');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取语料桶对象
|
|||
|
|
*/
|
|||
|
|
async function readCorpus(key) {
|
|||
|
|
const result = await cosRequest(COS_CONFIG.buckets.corpus.name, key, 'GET');
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(result.body);
|
|||
|
|
} catch {
|
|||
|
|
return result.body;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查COS连接状态
|
|||
|
|
*/
|
|||
|
|
async function checkConnection() {
|
|||
|
|
if (!COS_CONFIG.secretId || !COS_CONFIG.secretKey) {
|
|||
|
|
return {
|
|||
|
|
connected: false,
|
|||
|
|
reason: 'COS密钥未配置 — 需要在GitHub Secrets配置ZY_OSS_KEY和ZY_OSS_SECRET',
|
|||
|
|
buckets: {
|
|||
|
|
core: { name: COS_CONFIG.buckets.core.name, status: 'unconfigured' },
|
|||
|
|
corpus: { name: COS_CONFIG.buckets.corpus.name, status: 'unconfigured' }
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const results = { connected: true, buckets: {} };
|
|||
|
|
|
|||
|
|
for (const [key, bucket] of Object.entries(COS_CONFIG.buckets)) {
|
|||
|
|
try {
|
|||
|
|
await cosRequest(bucket.name, '', 'HEAD');
|
|||
|
|
results.buckets[key] = { name: bucket.name, status: 'connected', purpose: bucket.purpose };
|
|||
|
|
} catch (err) {
|
|||
|
|
results.buckets[key] = { name: bucket.name, status: 'error', error: err.message };
|
|||
|
|
results.connected = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return results;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存用户记忆到核心桶(团队内测用户 → COS)
|
|||
|
|
*/
|
|||
|
|
async function saveUserMemory(userId, memoryData) {
|
|||
|
|
const key = `users/${userId}/memory.json`;
|
|||
|
|
const wrapped = {
|
|||
|
|
user_id: userId,
|
|||
|
|
user_tier: 'team', // team = 内测团队 / public = 普通用户(后期单独存储)
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
version: 1,
|
|||
|
|
memories: memoryData
|
|||
|
|
};
|
|||
|
|
return writeCore(key, wrapped);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取用户记忆
|
|||
|
|
*/
|
|||
|
|
async function loadUserMemory(userId) {
|
|||
|
|
try {
|
|||
|
|
const key = `users/${userId}/memory.json`;
|
|||
|
|
return await readCore(key);
|
|||
|
|
} catch {
|
|||
|
|
return { user_id: userId, user_tier: 'team', memories: [], version: 0 };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 保存用户作品到核心桶(团队用户)
|
|||
|
|
*/
|
|||
|
|
async function saveUserWorks(userId, worksData) {
|
|||
|
|
const key = `users/${userId}/works.json`;
|
|||
|
|
return writeCore(key, {
|
|||
|
|
user_id: userId,
|
|||
|
|
user_tier: 'team',
|
|||
|
|
updated_at: new Date().toISOString(),
|
|||
|
|
works: worksData
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 读取用户作品
|
|||
|
|
*/
|
|||
|
|
async function loadUserWorks(userId) {
|
|||
|
|
try {
|
|||
|
|
const key = `users/${userId}/works.json`;
|
|||
|
|
return await readCore(key);
|
|||
|
|
} catch {
|
|||
|
|
return { user_id: userId, works: [] };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取COS配置信息(不含密钥)
|
|||
|
|
*/
|
|||
|
|
function getConfig() {
|
|||
|
|
return {
|
|||
|
|
region: COS_CONFIG.region,
|
|||
|
|
configured: !!(COS_CONFIG.secretId && COS_CONFIG.secretKey),
|
|||
|
|
buckets: {
|
|||
|
|
core: {
|
|||
|
|
name: COS_CONFIG.buckets.core.name,
|
|||
|
|
purpose: COS_CONFIG.buckets.core.purpose,
|
|||
|
|
structure: COS_CONFIG.buckets.core.structure
|
|||
|
|
},
|
|||
|
|
corpus: {
|
|||
|
|
name: COS_CONFIG.buckets.corpus.name,
|
|||
|
|
purpose: COS_CONFIG.buckets.corpus.purpose,
|
|||
|
|
structure: COS_CONFIG.buckets.corpus.structure
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
writeCore,
|
|||
|
|
readCore,
|
|||
|
|
writeCorpus,
|
|||
|
|
readCorpus,
|
|||
|
|
checkConnection,
|
|||
|
|
saveUserMemory,
|
|||
|
|
loadUserMemory,
|
|||
|
|
saveUserWorks,
|
|||
|
|
loadUserWorks,
|
|||
|
|
getConfig,
|
|||
|
|
COS_CONFIG
|
|||
|
|
};
|