feat: refine wooden fish runtime generation

This commit is contained in:
2026-05-22 03:49:35 +08:00
parent d81cc49549
commit 5f1128540e
30 changed files with 804 additions and 126 deletions

View File

@@ -816,7 +816,28 @@ class DevRunner {
console.log(`[dev:spacetime] 迁移引导密钥: ${this.options.migrationBootstrapSecret}`);
}
startApiServer(service) {
async ensureApiServerSpacetimeToken() {
const existingToken = String(this.baseEnv.GENARRATIVE_SPACETIME_TOKEN ?? '').trim();
if (existingToken && shouldTrustExistingSpacetimeToken(existingToken, this.state.spacetimeServer)) {
return;
}
const identityUrl = buildUrl(this.state.spacetimeServer, '/v1/identity');
if (!identityUrl) {
throw new Error(`无法构造 SpacetimeDB identity 地址: ${this.state.spacetimeServer}`);
}
const response = await fetchSpacetimeIdentity(identityUrl);
this.baseEnv.GENARRATIVE_SPACETIME_TOKEN = response.token;
this.state.spacetimeIdentity = response.identity;
console.log(
`[dev:spacetime] 已创建本地 Web identity: ${response.identity.slice(0, 12)}...`,
);
}
async startApiServer(service) {
await this.ensureApiServerSpacetimeToken();
const mergedEnv = {
...this.baseEnv,
GENARRATIVE_API_HOST: this.options.apiHost,
@@ -1413,6 +1434,75 @@ async function isHttpReady(url, timeoutMs = 1000) {
}
}
async function fetchSpacetimeIdentity(url) {
let response;
try {
response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
} 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};
}
function shouldTrustExistingSpacetimeToken(existingToken, serverUrl) {
const shellToken = String(process.env.GENARRATIVE_SPACETIME_TOKEN ?? '').trim();
if (shellToken && shellToken === existingToken) {
return true;
}
return !isLoopbackSpacetimeServer(serverUrl);
}
function isLoopbackSpacetimeServer(serverUrl) {
try {
const url = new URL(serverUrl);
return ['127.0.0.1', 'localhost', '::1'].includes(url.hostname);
} catch {
return false;
}
}
function trimPreview(text, maxLength = 300) {
const normalized = String(text ?? '').replace(/\s+/gu, ' ').trim();
return normalized.length > maxLength
? `${normalized.slice(0, maxLength)}...`
: normalized;
}
function runForeground(command, args, {cwd, env, label}) {
return new Promise((resolveRun, rejectRun) => {
const child = spawn(command, args, {

View File

@@ -271,4 +271,62 @@ describe('dev scheduler spacetime refresh', () => {
expect(runner.waitForSpacetime).not.toHaveBeenCalled();
expect(runner.publishSpacetimeModule).not.toHaveBeenCalled();
});
test('启动 api-server 前为空 token 自动创建本地 Web identity', async () => {
const {explicitOptions, options} = parseArgs([], {
GENARRATIVE_SPACETIME_TOKEN: '',
});
const runner = new DevRunner(options, {}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
identity: 'c200localidentity',
token: 'local-web-token',
}),
})) as unknown as typeof fetch;
await runner.ensureApiServerSpacetimeToken();
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('local-web-token');
expect(globalThis.fetch).toHaveBeenCalledWith(
'http://127.0.0.1:3101/v1/identity',
expect.objectContaining({
method: 'POST',
}),
);
});
test('本地 SpacetimeDB 不信任 env 文件中的陈旧 token', async () => {
const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN;
delete process.env.GENARRATIVE_SPACETIME_TOKEN;
try {
const {explicitOptions, options} = parseArgs([], {
GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token',
});
const runner = new DevRunner(options, {GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token'}, explicitOptions);
runner.state.spacetimeServer = 'http://127.0.0.1:3101';
globalThis.fetch = vi.fn(async () => ({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
identity: 'c200freshidentity',
token: 'fresh-web-token',
}),
})) as unknown as typeof fetch;
await runner.ensureApiServerSpacetimeToken();
expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('fresh-web-token');
} finally {
if (originalToken === undefined) {
delete process.env.GENARRATIVE_SPACETIME_TOKEN;
} else {
process.env.GENARRATIVE_SPACETIME_TOKEN = originalToken;
}
}
});
});