Files
TencentDB-Agent-Memory/scripts/export-tencent-vdb/export-tencent-vdb.ts
T
2026-05-29 17:51:28 +08:00

635 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 腾讯云 VDB (Tencent VectorDB) 数据导出脚本
*
* 连接腾讯云向量数据库实例,查询指定数据库下 collection 的文档,导出为 .jsonl 文件。
* 仅支持腾讯云向量数据库(Tencent VectorDB),不支持其他厂商的向量数据库。
*
* 所有连接参数通过 CLI 传入,无需 .env 文件。
*
* 用法:
* node ./bin/export-tencent-vdb.mjs --url <地址> --username <用户名> --api-key <密钥> --database <库名>
* node ./bin/export-tencent-vdb.mjs --url <地址> --username <用户名> --api-key <密钥> --database <库名> --probe
* node ./bin/export-tencent-vdb.mjs --url <地址> --username <用户名> --api-key <密钥> --database <库名> -c <collection> -o /tmp/backup
*
* 输出:
* 默认输出到当前工作目录下的 ./vdb-export-YYYY-MM-DD/,可通过 -o 指定。
* <outputDir>/
* ├── <collection>.jsonl — 每行一个 JSON 文档
* ├── schemas.json — 导出的 collection 表结构(索引、embedding 配置等)
* └── export-meta.json — 导出元信息
*
* 导出字段说明:
* 默认行为:导出所有字段,但跳过 vector(稠密向量,1024维浮点数组,体积大)。
* 加 --include-vectors:导出全部字段,包括 vector,不跳过任何内容。
* 注:sparse_vectorBM25 稀疏向量)始终导出,不受此开关影响。
*
* 依赖:Node.js >= 18(内置 fetch
*/
import fs from "node:fs";
import path from "node:path";
// ============================================================
// CLI 参数解析(含 VDB 连接信息)
// ============================================================
interface VDBConfig {
url: string;
username: string;
apiKey: string;
database: string;
timeout: number;
}
interface CliArgs {
// 连接参数
url?: string;
username?: string;
apiKey?: string;
database?: string;
timeout: number;
// 导出参数
output: string;
collection?: string;
filter?: string;
limit?: number;
offset: number;
includeVectors: boolean;
probe: boolean;
help: boolean;
}
const PAGE_SIZE = 100;
function parseArgs(): CliArgs {
const args = process.argv.slice(2);
const result: CliArgs = {
timeout: 30000,
output: `./vdb-export-${new Date().toISOString().slice(0, 10)}`,
offset: 0,
includeVectors: false,
probe: false,
help: false,
};
for (let i = 0; i < args.length; i++) {
switch (args[i]) {
case "--url":
result.url = args[++i];
break;
case "--username":
result.username = args[++i];
break;
case "--api-key":
result.apiKey = args[++i];
break;
case "--database":
result.database = args[++i];
break;
case "--timeout":
result.timeout = parseInt(args[++i], 10) || 30000;
break;
case "--output":
case "-o":
result.output = args[++i];
break;
case "--collection":
case "-c":
result.collection = args[++i];
break;
case "--filter":
case "-f":
result.filter = args[++i];
break;
case "--limit":
case "-l": {
const v = parseInt(args[++i], 10);
if (isNaN(v) || v < 1) {
console.error(`❌ --limit 必须 >= 1,收到: ${args[i]}`);
process.exit(1);
}
result.limit = v;
break;
}
case "--offset": {
const v = parseInt(args[++i], 10);
if (isNaN(v) || v < 0) {
console.error(`❌ --offset 必须 >= 0,收到: ${args[i]}`);
process.exit(1);
}
result.offset = v;
break;
}
case "--include-vectors":
result.includeVectors = true;
break;
case "--probe":
result.probe = true;
break;
case "--help":
case "-h":
result.help = true;
break;
}
}
return result;
}
function validateConfig(args: CliArgs): VDBConfig {
const missing: string[] = [];
if (!args.url) missing.push("--url");
if (!args.username) missing.push("--username");
if (!args.apiKey) missing.push("--api-key");
if (!args.database) missing.push("--database");
if (missing.length > 0) {
console.error("❌ 缺少必填参数:");
for (const k of missing) {
console.error(` - ${k}`);
}
console.error();
console.error("示例:");
console.error();
console.error(' node ./bin/export-tencent-vdb.mjs \\');
console.error(' --url "http://your-vdb-host:8100" \\');
console.error(' --username "root" \\');
console.error(' --api-key "your-api-key" \\');
console.error(' --database "your-database"');
console.error();
console.error("使用 --help 查看完整参数说明。");
process.exit(1);
}
return {
url: args.url!,
username: args.username!,
apiKey: args.apiKey!,
database: args.database!,
timeout: args.timeout,
};
}
function printHelp(): void {
console.log(`
腾讯云 VDB (Tencent VectorDB) 数据导出脚本
用法:
node ./bin/export-tencent-vdb.mjs [连接参数] [选项]
连接参数(必填):
--url <地址> VDB 实例 HTTP 地址(如 http://your-vdb-host:8100
--username <用户名> 认证用户名(如 root)
--api-key <密钥> 认证密钥
--database <库名> 数据库名称
选项:
--timeout <毫秒> 单次请求超时(默认: 30000)
-o, --output <目录> 输出目录(默认: ./vdb-export-YYYY-MM-DD
-c, --collection <全名> 只导出指定 collection(全名匹配,不指定则导出所有)
-f, --filter <表达式> VDB Filter 过滤条件(如 'agent_id = "xxx"'
-l, --limit <数量> 最多导出多少条(不指定则导出全部)
--offset <偏移> 从第几条开始(默认: 0),须为分页大小的整数倍
--include-vectors 保留 vector 稠密向量字段(默认跳过)
--probe 仅测试连通性,列出 collection 信息后退出
-h, --help 显示帮助
输出:
<outputDir>/
├── <collection全名>.jsonl 每行一个 JSON 文档
├── schemas.json 表结构
└── export-meta.json 导出元信息
导出字段说明:
默认跳过 vector(稠密向量),保留 sparse_vectorBM25)。
加 --include-vectors 导出全部字段。
示例:
# 测试连通性
node ./bin/export-tencent-vdb.mjs \\
--url "http://gz-vdb-xxx:8100" --username root --api-key "xxx" --database mydb \\
--probe
# 全量导出
node ./bin/export-tencent-vdb.mjs \\
--url "http://gz-vdb-xxx:8100" --username root --api-key "xxx" --database mydb
# 导出指定 collection 到指定目录
node ./bin/export-tencent-vdb.mjs \\
--url "http://gz-vdb-xxx:8100" --username root --api-key "xxx" --database mydb \\
-c mydb_l0_conversations -o /tmp/backup
# 带过滤条件
node ./bin/export-tencent-vdb.mjs \\
--url "http://gz-vdb-xxx:8100" --username root --api-key "xxx" --database mydb \\
-f 'role = "user"'
`);
}
// ============================================================
// VDB HTTP Client
// ============================================================
class VDBClient {
private baseUrl: string;
private authHeader: string;
private database: string;
private timeout: number;
constructor(cfg: VDBConfig) {
this.baseUrl = cfg.url.replace(/\/$/, "");
this.authHeader = `Bearer account=${cfg.username}&api_key=${cfg.apiKey}`;
this.database = cfg.database;
this.timeout = cfg.timeout;
}
async request<T>(apiPath: string, body: Record<string, unknown>): Promise<T> {
const url = `${this.baseUrl}${apiPath}`;
const resp = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: this.authHeader,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(this.timeout),
});
if (!resp.ok) {
const text = await resp.text().catch(() => "(unable to read body)");
throw new Error(`VDB API error: HTTP ${resp.status}${text.slice(0, 500)}`);
}
const json = (await resp.json()) as { code: number; msg: string } & T;
if (json.code !== 0) {
throw new Error(`VDB API error [${apiPath}]: code=${json.code}, msg=${json.msg}`);
}
return json;
}
async listCollections(): Promise<
Array<{ collection: string; documentCount: number }>
> {
const result = await this.request<{
collections: Array<{
collection: string;
documentCount: number;
[key: string]: unknown;
}>;
}>("/collection/list", {
database: this.database,
});
return (result.collections || []).map((c) => ({
collection: c.collection,
documentCount: c.documentCount ?? 0,
}));
}
async queryDocuments(
collection: string,
options: {
limit: number;
offset: number;
filter?: string;
retrieveVector?: boolean;
},
): Promise<{
documents: Array<Record<string, unknown>>;
count: number;
}> {
const query: Record<string, unknown> = {
limit: options.limit,
offset: options.offset,
};
if (options.filter) {
query.filter = options.filter;
}
if (options.retrieveVector) {
query.retrieveVector = true;
}
const result = await this.request<{
documents: Array<Record<string, unknown>>;
count: number;
}>("/document/query", {
database: this.database,
collection,
readConsistency: "strongConsistency",
query,
});
return {
documents: result.documents || [],
count: result.count ?? 0,
};
}
async describeCollection(collection: string): Promise<Record<string, unknown>> {
const result = await this.request<{
collection: Record<string, unknown>;
}>("/collection/describe", {
database: this.database,
collection,
});
return result.collection || {};
}
}
// ============================================================
// 导出逻辑
// ============================================================
interface ExportOptions {
filter?: string;
limit?: number;
offset: number;
includeVectors: boolean;
expectedTotal?: number;
}
async function exportCollection(
client: VDBClient,
collection: string,
outputDir: string,
options: ExportOptions,
): Promise<{ docCount: number; filePath: string }> {
const filePath = path.join(outputDir, `${collection}.jsonl`);
const writeStream = fs.createWriteStream(filePath, { encoding: "utf-8" });
const isRangeMode = options.limit !== undefined;
const maxDocs = options.limit ?? Infinity;
const pageSize = isRangeMode ? Math.min(options.limit!, PAGE_SIZE) : PAGE_SIZE;
let currentOffset = options.offset;
let totalExported = 0;
let hasMore = true;
console.log(` 📦 ${collection}`);
if (options.expectedTotal !== undefined) {
console.log(` 文档总数: ${options.expectedTotal}`);
}
if (options.filter) {
console.log(` 过滤条件: ${options.filter}`);
}
if (isRangeMode) {
console.log(` 导出范围: offset=${options.offset}, limit=${options.limit}`);
}
while (hasMore && totalExported < maxDocs) {
const remaining = maxDocs - totalExported;
const thisPageSize = Math.min(pageSize, remaining);
try {
const result = await client.queryDocuments(collection, {
limit: thisPageSize,
offset: currentOffset,
filter: options.filter,
retrieveVector: options.includeVectors,
});
const docs = result.documents;
if (!docs || docs.length === 0) {
hasMore = false;
break;
}
for (const doc of docs) {
const exportDoc = { ...doc };
if (!options.includeVectors) {
delete exportDoc.vector;
}
writeStream.write(JSON.stringify(exportDoc) + "\n");
}
totalExported += docs.length;
currentOffset += docs.length;
if (options.expectedTotal !== undefined && !isRangeMode) {
const pct = Math.min(
100,
Math.round((totalExported / options.expectedTotal) * 100),
);
process.stdout.write(
`\r 进度: ${totalExported}/${options.expectedTotal} (${pct}%)`,
);
} else {
process.stdout.write(`\r 已导出: ${totalExported} 条`);
}
if (docs.length < thisPageSize) {
hasMore = false;
}
} catch (err) {
console.error(
`\n ❌ 查询失败 (offset=${currentOffset}): ${err instanceof Error ? err.message : String(err)}`,
);
hasMore = false;
}
}
writeStream.end();
await new Promise<void>((resolve) => writeStream.on("finish", resolve));
console.log(
`\n ✅ 完成: ${totalExported} 条 → ${path.basename(filePath)}`,
);
return { docCount: totalExported, filePath };
}
// ============================================================
// Main
// ============================================================
async function main(): Promise<void> {
const args = parseArgs();
if (args.help) {
printHelp();
process.exit(0);
}
const config = validateConfig(args);
console.log("╔═══════════════════════════════════════════════════╗");
console.log("║ 腾讯云 VDB (Tencent VectorDB) 数据导出工具 ║");
console.log("╚═══════════════════════════════════════════════════╝");
console.log();
console.log(`📌 VDB 地址: ${config.url}`);
console.log(`📌 数据库: ${config.database}`);
console.log(`📌 输出目录: ${args.output}`);
if (args.collection) {
console.log(`📌 指定导出: ${args.collection}`);
}
if (args.filter) {
console.log(`📌 过滤条件: ${args.filter}`);
}
if (args.limit !== undefined) {
console.log(`📌 导出上限: ${args.limit} 条`);
}
if (args.offset > 0) {
console.log(`📌 起始偏移: ${args.offset}`);
}
if (args.includeVectors) {
console.log(`📌 包含向量: 是`);
}
console.log();
fs.mkdirSync(args.output, { recursive: true });
const client = new VDBClient(config);
let allCollections: Array<{ collection: string; documentCount: number }>;
try {
allCollections = await client.listCollections();
} catch (err) {
console.error(
`❌ 列出 collection 失败: ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
}
let targetCollections: Array<{ collection: string; documentCount: number }>;
if (args.collection) {
const found = allCollections.find((c) => c.collection === args.collection);
if (!found) {
console.error(
`❌ Collection "${args.collection}" 不存在。可用的 collection`,
);
for (const c of allCollections) {
console.error(` - ${c.collection} (${c.documentCount} 条)`);
}
process.exit(1);
}
targetCollections = [found];
} else {
targetCollections = allCollections;
console.log(
`🔍 找到 ${targetCollections.length} 个 collection`,
);
for (const c of targetCollections) {
console.log(` - ${c.collection} (${c.documentCount} 条)`);
}
}
if (targetCollections.length === 0) {
console.log("⚠️ 数据库中没有 collection,无数据可导出");
process.exit(0);
}
// --probe 模式:只测试连通性,列出信息后退出
if (args.probe) {
console.log();
console.log("✅ 连通性测试通过");
console.log();
console.log(` VDB 地址: ${config.url}`);
console.log(` 数据库: ${config.database}`);
console.log(` Collection: ${targetCollections.length} 个`);
const totalDocs = targetCollections.reduce((s, c) => s + c.documentCount, 0);
console.log(` 总文档数: ${totalDocs}`);
console.log();
for (const c of targetCollections) {
console.log(` - ${c.collection} (${c.documentCount} 条)`);
}
console.log();
process.exit(0);
}
console.log();
// 获取并保存表结构
const schemas: Record<string, Record<string, unknown>> = {};
console.log("📐 获取表结构...");
for (const col of targetCollections) {
try {
const schema = await client.describeCollection(col.collection);
schemas[col.collection] = schema;
const indexCount = Array.isArray(schema.indexes) ? schema.indexes.length : 0;
const emb = schema.embedding as Record<string, unknown> | undefined;
const embInfo = emb ? `embedding=${emb.field}${emb.model}` : "无 embedding";
console.log(` ✅ ${col.collection} (${indexCount} 个索引, ${embInfo})`);
} catch (err) {
console.error(
` ⚠️ ${col.collection} 表结构获取失败: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
console.log();
const schemaPath = path.join(args.output, "schemas.json");
fs.writeFileSync(schemaPath, JSON.stringify(schemas, null, 2) + "\n");
const exportResults: Array<{
collection: string;
docCount: number;
filePath: string;
}> = [];
for (const col of targetCollections) {
try {
const result = await exportCollection(client, col.collection, args.output, {
filter: args.filter,
limit: args.limit,
offset: args.offset,
includeVectors: args.includeVectors,
expectedTotal: col.documentCount,
});
exportResults.push({ collection: col.collection, ...result });
} catch (err) {
console.error(
`❌ 导出 ${col.collection} 失败: ${err instanceof Error ? err.message : String(err)}`,
);
exportResults.push({
collection: col.collection,
docCount: 0,
filePath: "",
});
}
console.log();
}
const meta = {
exportedAt: new Date().toISOString(),
vdbUrl: config.url,
database: config.database,
filter: args.filter ?? null,
offset: args.offset,
limit: args.limit ?? null,
includeVectors: args.includeVectors,
collections: exportResults.map((r) => ({
collection: r.collection,
documentCount: r.docCount,
file: r.filePath ? path.basename(r.filePath) : null,
})),
totalDocuments: exportResults.reduce((sum, r) => sum + r.docCount, 0),
};
const metaPath = path.join(args.output, "export-meta.json");
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n");
console.log("═══════════════════════════════════════════════════");
console.log(" ✅ 导出完成");
console.log("═══════════════════════════════════════════════════");
console.log();
console.log(` 📁 输出目录: ${args.output}`);
console.log(` 📊 总文档数: ${meta.totalDocuments}`);
for (const r of exportResults) {
const status = r.docCount > 0 ? "✅" : "⚠️";
console.log(
` ${status} ${r.collection}: ${r.docCount} 条`,
);
}
console.log(` 📋 元信息: ${path.basename(metaPath)}`);
console.log(` 📐 表结构: ${path.basename(schemaPath)}`);
console.log();
}
main().catch((err) => {
console.error(
`\n❌ 导出失败: ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
});