This commit is contained in:
@@ -8,12 +8,12 @@ SpacetimeDB reducer 必须保持确定性,不能访问文件系统和网络。
|
|||||||
|
|
||||||
1. `spacetime-module` 内的导出 procedure 读取迁移白名单表,并直接返回迁移 JSON 字符串。
|
1. `spacetime-module` 内的导出 procedure 读取迁移白名单表,并直接返回迁移 JSON 字符串。
|
||||||
2. Node 运维脚本默认通过 `spacetime call` 调用导出 procedure,把返回的 JSON 字符串写入本地文件。
|
2. Node 运维脚本默认通过 `spacetime call` 调用导出 procedure,把返回的 JSON 字符串写入本地文件。
|
||||||
3. Node 运维脚本读取本地 JSON 文件内容,并通过 HTTP request body 作为字符串参数传给导入 procedure。
|
3. Node 运维脚本读取本地 JSON 文件内容。导入时默认先通过 `POST /v1/identity` 创建临时 Web API identity/token,再用当前 CLI 登录态把该 identity 授权为迁移操作员,最后通过 HTTP request body 把 JSON 字符串传给导入 procedure。
|
||||||
4. 导入 procedure 校验 JSON 与表白名单后,在事务中写入目标数据库。
|
4. 导入 procedure 校验 JSON 与表白名单后,在事务中写入目标数据库。
|
||||||
|
|
||||||
procedure 不再访问 HTTP 文件桥,也不接收部署机本地文件路径。这样可以避开 SpacetimeDB 对 private/special-purpose 地址的 HTTP 访问限制,并避免把 private 表内容通过临时 HTTP 服务转发。
|
procedure 不再访问 HTTP 文件桥,也不接收部署机本地文件路径。这样可以避开 SpacetimeDB 对 private/special-purpose 地址的 HTTP 访问限制,并避免把 private 表内容通过临时 HTTP 服务转发。
|
||||||
|
|
||||||
`spacetime login show --token` 输出的是 CLI 登录 token,不是 HTTP `/v1/database/.../call` 所需的数据库连接 token。运维脚本默认走 CLI 登录态,迁移时不要把 CLI token 传给 `--token`;只有显式传 `--use-http` 时才需要数据库连接 token。
|
`spacetime login show --token` 输出的是 CLI 登录 token,不是 HTTP `/v1/database/.../call` 所需的数据库连接 token。导入脚本如果没有显式传 `--token`,会自动调用 `POST /v1/identity` 获取 Web API token;迁移时不要把 CLI token 传给 `--token`。
|
||||||
|
|
||||||
## 接口
|
## 接口
|
||||||
|
|
||||||
@@ -145,16 +145,26 @@ node scripts/spacetime-authorize-migration-operator.mjs \
|
|||||||
--note "server import"
|
--note "server import"
|
||||||
```
|
```
|
||||||
|
|
||||||
导入脚本负责读取服务器本地文件并把 JSON 字符串传入目标库 procedure:
|
导入脚本负责读取服务器本地文件并把 JSON 字符串通过 Web API request body 传入目标库 procedure。因为 JSON 不再放进 `spacetime call` 命令行参数,所以不会触发 Linux `spawn E2BIG`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/spacetime-import-migration-json.mjs \
|
node scripts/spacetime-import-migration-json.mjs \
|
||||||
--server maincloud \
|
--server maincloud \
|
||||||
--database xushi-p4wfr \
|
--database xushi-p4wfr \
|
||||||
|
--bootstrap-secret <服务器目标库发布时输出的随机密钥> \
|
||||||
--in tmp/spacetime-migrations/source-2026-04-27.json \
|
--in tmp/spacetime-migrations/source-2026-04-27.json \
|
||||||
--replace-existing
|
--replace-existing
|
||||||
```
|
```
|
||||||
|
|
||||||
|
默认情况下,脚本会自动完成三步:
|
||||||
|
|
||||||
|
1. `POST /v1/identity` 创建临时 Web API identity/token。
|
||||||
|
2. 使用当前机器 `spacetime` CLI 登录态调用 `authorize_database_migration_operator`,授权这个临时 identity。
|
||||||
|
3. 使用 `Authorization: Bearer <临时 token>` 调用 `import_database_migration_from_file`,把完整迁移 JSON 放在 HTTP body 中。
|
||||||
|
4. 导入请求结束后,脚本会用同一个临时 Web API token 调用 `revoke_database_migration_operator`,撤销该临时 identity。
|
||||||
|
|
||||||
|
如果你已经有可用的数据库连接 token,也可以显式传 `--token <web-api-token>`。这种情况下脚本不会自动授权;该 token 对应的 identity 必须已经是迁移操作员。
|
||||||
|
|
||||||
正式导入前建议先加 `--dry-run`,确认 JSON 可解析、版本匹配、表名都在迁移白名单内。
|
正式导入前建议先加 `--dry-run`,确认 JSON 可解析、版本匹配、表名都在迁移白名单内。
|
||||||
|
|
||||||
如需分批迁移,可用逗号分隔表名:
|
如需分批迁移,可用逗号分隔表名:
|
||||||
@@ -166,7 +176,7 @@ node scripts/spacetime-export-migration-json.mjs \
|
|||||||
--include ai_task,ai_task_stage,ai_text_chunk,ai_result_reference
|
--include ai_task,ai_task_stage,ai_text_chunk,ai_result_reference
|
||||||
```
|
```
|
||||||
|
|
||||||
`--server` 支持 `dev`、`local`、`maincloud`,也可以直接传 SpacetimeDB 服务器 URL。脚本默认走 `spacetime call`,使用当前机器的 CLI 登录态。数据库名可通过 `--database`、`GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE` 或 `GENARRATIVE_SPACETIME_DATABASE` 提供。
|
`--server` 支持 `dev`、`local`、`maincloud`,也可以直接传 SpacetimeDB 服务器 URL。导出、授权、撤销默认走 `spacetime call`,使用当前机器的 CLI 登录态;导入默认走 Web API request body,避免大 JSON 触发命令行长度限制。数据库名可通过 `--database`、`GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE` 或 `GENARRATIVE_SPACETIME_DATABASE` 提供。
|
||||||
|
|
||||||
授权脚本额外支持:
|
授权脚本额外支持:
|
||||||
|
|
||||||
@@ -193,4 +203,4 @@ node scripts/spacetime-export-migration-json.mjs \
|
|||||||
|
|
||||||
迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。数据量较大时,先按 `include_tables` 分批迁移;若单表本身过大,再补充分片 procedure,而不是恢复 HTTP 文件桥。
|
迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。数据量较大时,先按 `include_tables` 分批迁移;若单表本身过大,再补充分片 procedure,而不是恢复 HTTP 文件桥。
|
||||||
|
|
||||||
`spacetime call` 在 PowerShell 中手写 JSON 容易被剥掉双引号。推荐使用仓库里的 Node 脚本,由脚本直接走 HTTP API,避免 shell 二次处理和命令行长度限制。
|
`spacetime call` 在 PowerShell 中手写 JSON 容易被剥掉双引号。导入大文件时也不能把完整 JSON 放进命令行参数,否则 Linux 会在启动子进程时返回 `spawn E2BIG`。推荐使用仓库里的 Node 脚本,由脚本直接走 Web API request body,避免 shell 二次处理和命令行长度限制。
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { readFile } from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import {
|
import {
|
||||||
assertReadableFile,
|
assertReadableFile,
|
||||||
callSpacetimeProcedureAuto,
|
callSpacetimeProcedure,
|
||||||
|
callSpacetimeProcedureViaCli,
|
||||||
|
createSpacetimeWebIdentity,
|
||||||
ensureProcedureOk,
|
ensureProcedureOk,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
} from './spacetime-migration-common.mjs';
|
} from './spacetime-migration-common.mjs';
|
||||||
@@ -22,17 +24,13 @@ try {
|
|||||||
throw new Error(`迁移文件为空: ${inPath}`);
|
throw new Error(`迁移文件为空: ${inPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = {
|
const webOptions = await prepareWebImportOptions(options);
|
||||||
migration_json: migrationJson,
|
let result;
|
||||||
include_tables: options.includeTables,
|
try {
|
||||||
replace_existing: options.replaceExisting === true,
|
result = await importMigrationJsonDirect(webOptions, migrationJson);
|
||||||
dry_run: options.dryRun === true,
|
} finally {
|
||||||
};
|
await revokeTemporaryWebIdentity(webOptions);
|
||||||
const result = await callSpacetimeProcedureAuto(
|
}
|
||||||
options,
|
|
||||||
'import_database_migration_from_file',
|
|
||||||
input,
|
|
||||||
);
|
|
||||||
ensureProcedureOk(result);
|
ensureProcedureOk(result);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -46,6 +44,68 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function prepareWebImportOptions(options) {
|
||||||
|
if (options.token) {
|
||||||
|
return { ...options, useHttp: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await createSpacetimeWebIdentity(options);
|
||||||
|
console.log(
|
||||||
|
`[spacetime:migration:import] 已通过 Web API 创建临时 identity: ${identity.identity}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const authorizeResult = await callSpacetimeProcedureViaCli(
|
||||||
|
options,
|
||||||
|
'authorize_database_migration_operator',
|
||||||
|
{
|
||||||
|
bootstrap_secret: options.bootstrapSecret || '',
|
||||||
|
operator_identity_hex: identity.identity,
|
||||||
|
note: options.note || 'temporary web api migration import',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ensureProcedureOk(authorizeResult);
|
||||||
|
console.log(`[spacetime:migration:import] 已授权临时 Web API identity`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
token: identity.token,
|
||||||
|
temporaryWebIdentity: identity.identity,
|
||||||
|
useHttp: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importMigrationJsonDirect(options, migrationJson) {
|
||||||
|
const input = {
|
||||||
|
migration_json: migrationJson,
|
||||||
|
include_tables: options.includeTables,
|
||||||
|
replace_existing: options.replaceExisting === true,
|
||||||
|
dry_run: options.dryRun === true,
|
||||||
|
};
|
||||||
|
return callSpacetimeProcedure(options, 'import_database_migration_from_file', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function revokeTemporaryWebIdentity(options) {
|
||||||
|
if (!options.temporaryWebIdentity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const revokeResult = await callSpacetimeProcedure(
|
||||||
|
options,
|
||||||
|
'revoke_database_migration_operator',
|
||||||
|
{ operator_identity_hex: options.temporaryWebIdentity },
|
||||||
|
);
|
||||||
|
ensureProcedureOk(revokeResult);
|
||||||
|
console.log(`[spacetime:migration:import] 已撤销临时 Web API identity`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`[spacetime:migration:import] 撤销临时 Web API identity 失败: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function printTableStats(tableStats) {
|
function printTableStats(tableStats) {
|
||||||
if (!Array.isArray(tableStats) || tableStats.length === 0) {
|
if (!Array.isArray(tableStats) || tableStats.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -133,6 +133,42 @@ export async function callSpacetimeProcedure(options, procedureName, input) {
|
|||||||
return parseProcedureResult(text);
|
return parseProcedureResult(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createSpacetimeWebIdentity(options) {
|
||||||
|
const serverUrl = resolveServerUrl(options).replace(/\/+$/u, '');
|
||||||
|
const url = `${serverUrl}/v1/identity`;
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { method: 'POST' });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`SpacetimeDB identity 请求失败: ${url}; ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`SpacetimeDB identity HTTP ${response.status}: ${trimPreview(text)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`SpacetimeDB identity 响应不是合法 JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity =
|
||||||
|
payload.identity ?? payload.Identity ?? payload.identity_hex ?? payload.identityHex;
|
||||||
|
const token = payload.token ?? payload.Token;
|
||||||
|
if (typeof identity !== 'string' || typeof token !== 'string') {
|
||||||
|
throw new Error(`SpacetimeDB identity 响应缺少 identity/token: ${trimPreview(text)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { identity, token };
|
||||||
|
}
|
||||||
|
|
||||||
export async function callSpacetimeProcedureAuto(options, procedureName, input) {
|
export async function callSpacetimeProcedureAuto(options, procedureName, input) {
|
||||||
if (options.useHttp) {
|
if (options.useHttp) {
|
||||||
return callSpacetimeProcedure(options, procedureName, input);
|
return callSpacetimeProcedure(options, procedureName, input);
|
||||||
@@ -266,7 +302,7 @@ function normalizeTableStats(value) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveServerUrl(options) {
|
export function resolveServerUrl(options) {
|
||||||
if (options.serverUrl) {
|
if (options.serverUrl) {
|
||||||
return options.serverUrl;
|
return options.serverUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user