nexu-io--open-design
288 行
8.7 KiB
TypeScript
288 行
8.7 KiB
TypeScript
import { chmod, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
export type FakeVelaOptions = {
|
|
assistantText?: string;
|
|
endpoints?: VelaEndpoints;
|
|
failAuthAtPrompt?: boolean;
|
|
failAuthAtPromptOnce?: boolean;
|
|
failBalanceAtPrompt?: boolean;
|
|
failBalanceAtPromptOnce?: boolean;
|
|
failModelListInvalidApiKey?: boolean;
|
|
requireLoginConfig?: boolean;
|
|
requireSetModel?: boolean;
|
|
sessionId?: string;
|
|
};
|
|
|
|
export type VelaEndpoints = {
|
|
apiUrl: string;
|
|
linkUrl: string;
|
|
};
|
|
|
|
const DEFAULT_ASSISTANT_TEXT = 'Hello from the e2e fake vela.';
|
|
const PRESET_MODELS_JSON = JSON.stringify({
|
|
source: 'preset',
|
|
data: [
|
|
{ id: 'glm-5' },
|
|
],
|
|
});
|
|
const REMOTE_MODELS_JSON = JSON.stringify({
|
|
source: 'remote',
|
|
data: [
|
|
{ id: 'glm-5' },
|
|
],
|
|
});
|
|
|
|
export async function writeFakeVelaBin(root: string, options: FakeVelaOptions = {}): Promise<string> {
|
|
await mkdir(root, { recursive: true });
|
|
const bin = join(root, 'vela');
|
|
await writeFile(bin, renderFakeVelaScript(options), 'utf8');
|
|
await chmod(bin, 0o755);
|
|
return bin;
|
|
}
|
|
|
|
export async function seedVelaLoginConfig(
|
|
homeDir: string,
|
|
options: {
|
|
endpoints?: VelaEndpoints;
|
|
profile?: string;
|
|
email?: string;
|
|
runtimeKey?: string;
|
|
controlKey?: string;
|
|
} = {},
|
|
): Promise<string> {
|
|
const profile = options.profile ?? 'local';
|
|
const endpoints = options.endpoints ?? defaultVelaEndpoints();
|
|
const configDir = join(homeDir, '.amr');
|
|
const file = join(configDir, 'config.json');
|
|
await mkdir(configDir, { recursive: true });
|
|
await writeFile(
|
|
file,
|
|
JSON.stringify(
|
|
{
|
|
profiles: {
|
|
[profile]: {
|
|
runtimeKey: options.runtimeKey ?? 'fake-runtime-key',
|
|
controlKey: options.controlKey ?? 'fake-control-key',
|
|
apiUrl: endpoints.apiUrl,
|
|
linkUrl: endpoints.linkUrl,
|
|
user: {
|
|
id: 'fake-user-id',
|
|
email: options.email ?? 'e2e@example.com',
|
|
plan: 'free',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
return file;
|
|
}
|
|
|
|
export async function clearVelaLoginConfig(homeDir: string): Promise<void> {
|
|
await rm(join(homeDir, '.amr', 'config.json'), { force: true });
|
|
}
|
|
|
|
function renderFakeVelaScript(options: FakeVelaOptions): string {
|
|
const endpoints = options.endpoints ?? defaultVelaEndpoints();
|
|
return `#!/usr/bin/env node
|
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { argv, stdin, stdout, stderr, env, exit } from 'node:process';
|
|
|
|
const ASSISTANT_TEXT = ${JSON.stringify(options.assistantText ?? DEFAULT_ASSISTANT_TEXT)};
|
|
const SESSION_ID = env.FAKE_VELA_SESSION_ID || ${JSON.stringify(options.sessionId ?? 'fake-amr-session-1')};
|
|
const AUTH_FAIL = ${options.failAuthAtPrompt === true ? 'true' : 'false'};
|
|
const AUTH_FAIL_ONCE = ${options.failAuthAtPromptOnce === true ? 'true' : 'false'};
|
|
const BALANCE_FAIL = ${options.failBalanceAtPrompt === true ? 'true' : 'false'};
|
|
const BALANCE_FAIL_ONCE = ${options.failBalanceAtPromptOnce === true ? 'true' : 'false'};
|
|
const MODEL_LIST_INVALID_API_KEY = ${options.failModelListInvalidApiKey === true ? 'true' : 'false'};
|
|
const REQUIRE_LOGIN = ${options.requireLoginConfig === false ? 'false' : 'true'};
|
|
const REQUIRE_SET_MODEL = ${options.requireSetModel === false ? 'false' : 'true'};
|
|
|
|
function writeMessage(obj) {
|
|
stdout.write(JSON.stringify(obj) + '\\n');
|
|
}
|
|
function writeResult(id, result) {
|
|
writeMessage({ jsonrpc: '2.0', id, result });
|
|
}
|
|
function writeError(id, message) {
|
|
writeMessage({ jsonrpc: '2.0', id, error: { code: -32001, message } });
|
|
}
|
|
function writeNotification(method, params) {
|
|
writeMessage({ jsonrpc: '2.0', method, params });
|
|
}
|
|
function currentProfile() {
|
|
return (env.OPEN_DESIGN_AMR_PROFILE || env.VELA_PROFILE || 'local').trim() || 'local';
|
|
}
|
|
function fakeHomeDir() {
|
|
return homedir();
|
|
}
|
|
function readLoginConfig() {
|
|
const file = join(fakeHomeDir(), '.amr', 'config.json');
|
|
if (!existsSync(file)) return null;
|
|
try {
|
|
return JSON.parse(readFileSync(file, 'utf8'));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
function hasRuntimeKey() {
|
|
if ((env.VELA_RUNTIME_KEY || '').trim() && (env.VELA_LINK_URL || '').trim()) return true;
|
|
const profile = currentProfile();
|
|
const cfg = readLoginConfig();
|
|
return Boolean(cfg && cfg.profiles && cfg.profiles[profile] && cfg.profiles[profile].runtimeKey);
|
|
}
|
|
function shouldFailOnce(kind) {
|
|
const file = join(fakeHomeDir(), '.amr', 'fake-vela-state.json');
|
|
let state = {};
|
|
try {
|
|
if (existsSync(file)) state = JSON.parse(readFileSync(file, 'utf8'));
|
|
} catch {
|
|
state = {};
|
|
}
|
|
const key = SESSION_ID + ':' + currentProfile() + ':' + kind;
|
|
if (state[key]) return false;
|
|
state[key] = true;
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, JSON.stringify(state, null, 2), 'utf8');
|
|
return true;
|
|
}
|
|
|
|
if (argv[2] === 'login') {
|
|
const file = join(fakeHomeDir(), '.amr', 'config.json');
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
const profile = currentProfile();
|
|
writeFileSync(file, JSON.stringify({
|
|
profiles: {
|
|
[profile]: {
|
|
runtimeKey: 'fake-runtime-key-0000000000000000000000',
|
|
controlKey: 'fake-control-key-0000000000000000000000',
|
|
apiUrl: ${JSON.stringify(endpoints.apiUrl)},
|
|
linkUrl: ${JSON.stringify(endpoints.linkUrl)},
|
|
user: { id: 'fake-user-id', email: env.FAKE_VELA_LOGIN_USER_EMAIL || 'e2e@example.com', plan: 'free' },
|
|
},
|
|
},
|
|
}, null, 2), 'utf8');
|
|
exit(0);
|
|
}
|
|
|
|
if (argv[2] === 'models') {
|
|
stdout.write('public_model_glm_5 vela\\n');
|
|
exit(0);
|
|
}
|
|
|
|
if (argv[2] === 'model' && argv[3] === 'preset' && argv[4] === '--format' && argv[5] === 'json') {
|
|
stdout.write(${JSON.stringify(PRESET_MODELS_JSON)} + '\\n');
|
|
exit(0);
|
|
}
|
|
|
|
if (argv[2] === 'model' && argv[3] === 'list' && argv[4] === '--format' && argv[5] === 'json') {
|
|
if (MODEL_LIST_INVALID_API_KEY) {
|
|
stderr.write('Error: list Link models: API request failed with status 401: invalid_api_key\\n');
|
|
exit(1);
|
|
}
|
|
stdout.write(${JSON.stringify(REMOTE_MODELS_JSON)} + '\\n');
|
|
exit(0);
|
|
}
|
|
|
|
if (REQUIRE_LOGIN && !hasRuntimeKey()) {
|
|
stderr.write('AMR login missing or expired. Please sign in again.\\n');
|
|
exit(1);
|
|
}
|
|
|
|
const sessionsWithModel = new Set();
|
|
let buffer = '';
|
|
stdin.setEncoding('utf8');
|
|
stdin.on('data', (chunk) => {
|
|
buffer += chunk;
|
|
const lines = buffer.split('\\n');
|
|
buffer = lines.pop() || '';
|
|
for (const raw of lines) {
|
|
const line = raw.trim();
|
|
if (!line) continue;
|
|
let msg;
|
|
try { msg = JSON.parse(line); } catch { continue; }
|
|
handle(msg);
|
|
}
|
|
});
|
|
stdin.on('end', () => { stdout.end(); exit(0); });
|
|
|
|
function handle(msg) {
|
|
const { id, method, params } = msg;
|
|
if (method === 'initialize') {
|
|
writeResult(id, {
|
|
protocolVersion: 1,
|
|
agentCapabilities: { promptCapabilities: { text: true } },
|
|
models: {
|
|
currentModelId: 'glm-5',
|
|
availableModels: [{ modelId: 'glm-5', name: 'glm-5' }],
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
if (method === 'session/new') {
|
|
writeResult(id, {
|
|
sessionId: SESSION_ID,
|
|
models: {
|
|
currentModelId: 'glm-5',
|
|
availableModels: [{ modelId: 'glm-5', name: 'glm-5' }],
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
if (method === 'session/set_model' || method === 'session/set_config_option') {
|
|
const sid = (params && params.sessionId) || SESSION_ID;
|
|
sessionsWithModel.add(sid);
|
|
writeResult(id, {});
|
|
return;
|
|
}
|
|
if (method === 'session/prompt') {
|
|
const sid = (params && params.sessionId) || SESSION_ID;
|
|
if (REQUIRE_SET_MODEL && !sessionsWithModel.has(sid)) {
|
|
writeError(id, 'session/set_model must be called before session/prompt');
|
|
return;
|
|
}
|
|
if (AUTH_FAIL || (AUTH_FAIL_ONCE && shouldFailOnce('auth'))) {
|
|
writeError(id, 'Your authentication token has expired. Please sign in again.');
|
|
return;
|
|
}
|
|
if (BALANCE_FAIL || (BALANCE_FAIL_ONCE && shouldFailOnce('balance'))) {
|
|
writeMessage({
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: {
|
|
code: -32000,
|
|
message: 'HTTP 429: {"code":"insufficient_balance","message":"insufficient balance"}',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
writeNotification('session/update', {
|
|
sessionId: sid,
|
|
update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: ASSISTANT_TEXT } },
|
|
});
|
|
writeResult(id, {
|
|
stopReason: 'end_turn',
|
|
usage: { inputTokens: 7, outputTokens: 5, totalTokens: 12 },
|
|
});
|
|
return;
|
|
}
|
|
if (typeof id !== 'undefined') {
|
|
writeMessage({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown method ' + method } });
|
|
}
|
|
}
|
|
`;
|
|
}
|
|
|
|
function defaultVelaEndpoints(): VelaEndpoints {
|
|
return {
|
|
apiUrl: 'http://localhost:18080',
|
|
linkUrl: 'http://localhost:18081',
|
|
};
|
|
}
|