feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { stopProcess } from './controlBridge';
|
||||
|
||||
/** Explicit live-test helper: keys stay in Python memory, only the ephemeral bridge
|
||||
* token crosses to the browser. Tighten (never bypass) production admission limits. */
|
||||
export async function startLiveDecisionService() {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
const directory = await mkdtemp(join(tmpdir(), 'lekiwi-live-decision-'));
|
||||
const script = `
|
||||
import asyncio, json, os, signal, sys
|
||||
from pathlib import Path
|
||||
from aiohttp import web
|
||||
from decision_server.server import create_app, STATE
|
||||
from decision_server.credentials import deepseek_llm, openrouter_jev
|
||||
from decision_server.protocol import DecisionError
|
||||
async def main():
|
||||
app = create_app(Path(sys.argv[1]), os.environ.pop('DECISION_SMOKE_TOKEN'), origins={'http://127.0.0.1:4176'}, port=0)
|
||||
service = app[STATE]
|
||||
service.connections.values['llm'] = deepseek_llm(Path('.env'))
|
||||
service.connections.values['jev'] = openrouter_jev(Path('.env'))
|
||||
original = service.admit
|
||||
counts = {'llm': 0, 'jev': 0}
|
||||
def admit(role, stamp):
|
||||
if counts[role] >= {'llm': 1, 'jev': 12}[role]:
|
||||
raise DecisionError('live_smoke_budget', 429)
|
||||
original(role, stamp)
|
||||
counts[role] += 1
|
||||
service.admit = admit
|
||||
runner = web.AppRunner(app, access_log=None, handler_cancellation=True)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, '127.0.0.1', 0)
|
||||
await site.start()
|
||||
port = site._server.sockets[0].getsockname()[1]
|
||||
service.hosts = {f'127.0.0.1:{port}'}
|
||||
stop = asyncio.Event()
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
asyncio.get_running_loop().add_signal_handler(sig, stop.set)
|
||||
print('DECISION_READY ' + json.dumps({'endpoint': f'http://127.0.0.1:{port}'}), flush=True)
|
||||
try:
|
||||
await stop.wait()
|
||||
finally:
|
||||
await runner.cleanup()
|
||||
asyncio.run(main())
|
||||
`;
|
||||
const child = spawn(resolve('.venv/bin/python'), ['-u', '-c', script, directory], {
|
||||
env: { ...process.env, DECISION_SMOKE_TOKEN: token, PYTHONNOUSERSITE: '1', PYTHONPATH: '' },
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
const stop = async () => {
|
||||
if (child.pid) await stopProcess(child);
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
};
|
||||
try {
|
||||
const endpoint = await new Promise<string>((resolve, reject) => {
|
||||
let output = '';
|
||||
const timer = setTimeout(
|
||||
() => reject(new Error('真实模型服务启动超时(不输出认证日志)')),
|
||||
15000,
|
||||
);
|
||||
child.once('error', () => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error('真实模型服务启动失败'));
|
||||
});
|
||||
child.once('exit', (code) => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`真实模型服务退出:${code}`));
|
||||
});
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
output = (output + chunk.toString()).slice(-8192);
|
||||
const match = output.match(/DECISION_READY (\{[^\n]+\})/);
|
||||
if (match) {
|
||||
clearTimeout(timer);
|
||||
resolve((JSON.parse(match[1]) as { endpoint: string }).endpoint);
|
||||
}
|
||||
});
|
||||
});
|
||||
return { endpoint, token, stop };
|
||||
} catch (error) {
|
||||
await stop();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createConnection } from 'node:net';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
/** Same-user 0600 Unix channel. Never print the connect response or persist its token. */
|
||||
export async function subscriptionOperation(operation: 'connect' | 'status' | 'limits' | 'stop') {
|
||||
const pointer = JSON.parse(
|
||||
await readFile('build/lekiwi-agent/subscription-session.json', 'utf8'),
|
||||
) as { socket?: string; status: string };
|
||||
if (!pointer.socket || pointer.status === 'closed')
|
||||
throw new Error('隔离订阅会话已关闭,请重新发起官方登录');
|
||||
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
||||
const channel = createConnection(pointer.socket!);
|
||||
let buffer = '';
|
||||
channel.setTimeout(20000);
|
||||
channel.once('connect', () => channel.write(JSON.stringify(operation) + '\n'));
|
||||
channel.on('data', (chunk) => {
|
||||
buffer += String(chunk);
|
||||
if (buffer.length > 16384) {
|
||||
channel.destroy();
|
||||
reject(new Error('订阅管理响应过大'));
|
||||
return;
|
||||
}
|
||||
if (buffer.includes('\n')) {
|
||||
try {
|
||||
const result = JSON.parse(buffer.split('\n')[0]) as Record<string, unknown>;
|
||||
if (result.error) reject(new Error('订阅管理请求失败'));
|
||||
else resolve(result);
|
||||
} catch {
|
||||
reject(new Error('订阅管理响应非法'));
|
||||
}
|
||||
channel.end();
|
||||
}
|
||||
});
|
||||
channel.once('timeout', () => {
|
||||
channel.destroy();
|
||||
reject(new Error('订阅管理请求超时'));
|
||||
});
|
||||
channel.once('error', () => reject(new Error('无法连接隔离订阅会话')));
|
||||
channel.once('end', () => {
|
||||
if (!buffer.includes('\n')) reject(new Error('订阅管理连接提前关闭'));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user