Core tests / core (push) Successful in 9m57s
Full verification / application (push) Failing after 5m16s
Full verification / local-brain (push) Failing after 6m15s
Realm simulation / simulated-live-world (push) Failing after 5m32s
WoW integration lab / real-worldserver (push) Canceled after 17m23s
Source branch: feature/ai-brain-router Source commit: c2a09e0460465201a58aaab56c71424e9a96405e
166 lines
6.3 KiB
JavaScript
166 lines
6.3 KiB
JavaScript
import {execFileSync} from 'node:child_process';
|
|
|
|
const BASE=(process.env.AZEROTHAI_URL||'http://127.0.0.1:8787').replace(/\/+$/,'');
|
|
const TOKEN=process.env.ALE_BRIDGE_TOKEN||'';
|
|
const DB_PASSWORD=process.env.DOCKER_DB_ROOT_PASSWORD||'';
|
|
const DB_CONTAINER=process.env.AZEROTHAI_DB_CONTAINER||'ac-database';
|
|
const POLL_MS=Math.max(100,Number(process.env.AZEROTHAI_ALE_RELAY_POLL_MS||250));
|
|
const EVENT_BATCH=Math.max(1,Math.min(20,Number(process.env.AZEROTHAI_ALE_EVENT_BATCH||8)));
|
|
const EVENT_GAP_MS=Math.max(0,Number(process.env.AZEROTHAI_ALE_EVENT_GAP_MS||75));
|
|
|
|
if(!TOKEN)throw new Error('ALE_BRIDGE_TOKEN is required');
|
|
if(!DB_PASSWORD)throw new Error('DOCKER_DB_ROOT_PASSWORD is required');
|
|
|
|
let stopping=false;
|
|
let lastCleanup=0;
|
|
process.on('SIGINT',()=>{stopping=true;});
|
|
process.on('SIGTERM',()=>{stopping=true;});
|
|
|
|
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
|
|
function sqlText(value){
|
|
const hex=Buffer.from(String(value??''),'utf8').toString('hex');
|
|
return `CONVERT(UNHEX('${hex}') USING utf8mb4)`;
|
|
}
|
|
function mysql(sql){
|
|
return execFileSync('docker',[
|
|
'exec','-e',`MYSQL_PWD=${DB_PASSWORD}`,DB_CONTAINER,
|
|
'mysql','-uroot','-N','-B','-e',sql
|
|
],{encoding:'utf8',stdio:['ignore','pipe','pipe']});
|
|
}
|
|
|
|
function ensureSchema(){
|
|
mysql(`
|
|
CREATE TABLE IF NOT EXISTS acore_auth.azerothai_command_inbox (
|
|
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
|
source MEDIUMTEXT NOT NULL,
|
|
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
consumed_at TIMESTAMP(6) NULL,
|
|
KEY idx_azerothai_command_inbox_pending (consumed_at,created_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
CREATE TABLE IF NOT EXISTS acore_auth.azerothai_event_outbox (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
kind VARCHAR(100) NOT NULL,
|
|
payload MEDIUMTEXT NOT NULL,
|
|
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
delivered_at TIMESTAMP(6) NULL,
|
|
KEY idx_azerothai_event_outbox_pending (delivered_at,id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
CREATE TABLE IF NOT EXISTS acore_auth.azerothai_result_outbox (
|
|
seq BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
command_id VARCHAR(36) NOT NULL,
|
|
ok TINYINT(1) NOT NULL,
|
|
result MEDIUMTEXT NOT NULL,
|
|
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
delivered_at TIMESTAMP(6) NULL,
|
|
UNIQUE KEY uq_azerothai_result_command (command_id),
|
|
KEY idx_azerothai_result_outbox_pending (delivered_at,seq)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`);
|
|
}
|
|
|
|
async function backend(path,init={}){
|
|
const headers=new Headers(init.headers||{});
|
|
headers.set('Authorization',`Bearer ${TOKEN}`);
|
|
headers.set('Accept','application/json');
|
|
const controller=new AbortController();
|
|
const timer=setTimeout(()=>controller.abort(),5000);
|
|
try{
|
|
return await fetch(BASE+path,{...init,headers,signal:controller.signal});
|
|
}finally{
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
async function pullCommand(){
|
|
const response=await backend('/api/bridge/ale/poll',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:'{}'
|
|
});
|
|
if(!response.ok)throw new Error(`poll HTTP ${response.status}: ${await response.text()}`);
|
|
const id=response.headers.get('x-azerothai-command');
|
|
const source=await response.text();
|
|
if(!id)return false;
|
|
mysql(`INSERT INTO acore_auth.azerothai_command_inbox(id,source)
|
|
VALUES (${sqlText(id)},${sqlText(source)})
|
|
ON DUPLICATE KEY UPDATE source=VALUES(source)`);
|
|
return true;
|
|
}
|
|
|
|
function rows(sql){
|
|
const out=mysql(sql).trim();
|
|
if(!out)return [];
|
|
return out.split('\n').filter(Boolean).map(line=>line.split('\t'));
|
|
}
|
|
|
|
async function deliverEvents(){
|
|
const batch=rows(`SELECT id,HEX(kind),HEX(payload)
|
|
FROM acore_auth.azerothai_event_outbox
|
|
WHERE delivered_at IS NULL
|
|
ORDER BY CASE WHEN kind IN ('player.whisper','player.chat') THEN 0 ELSE 1 END, id
|
|
LIMIT ${EVENT_BATCH}`);
|
|
for(const [id,kindHex,payloadHex] of batch){
|
|
const kind=Buffer.from(kindHex||'','hex').toString('utf8');
|
|
const payloadText=Buffer.from(payloadHex||'','hex').toString('utf8');
|
|
let payload;
|
|
try{payload=JSON.parse(payloadText);}catch{
|
|
console.error(`Dropping malformed ALE event ${id}: invalid JSON payload`);
|
|
mysql(`UPDATE acore_auth.azerothai_event_outbox SET delivered_at=NOW(6) WHERE id=${Number(id)}`);
|
|
continue;
|
|
}
|
|
const response=await backend('/api/bridge/ale/event',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({kind,payload})
|
|
});
|
|
if(!response.ok)throw new Error(`event HTTP ${response.status}: ${await response.text()}`);
|
|
mysql(`UPDATE acore_auth.azerothai_event_outbox SET delivered_at=NOW(6) WHERE id=${Number(id)}`);
|
|
if(EVENT_GAP_MS)await sleep(EVENT_GAP_MS);
|
|
}
|
|
return batch.length;
|
|
}
|
|
|
|
async function deliverResults(){
|
|
const batch=rows("SELECT seq,command_id,ok,HEX(result) FROM acore_auth.azerothai_result_outbox WHERE delivered_at IS NULL ORDER BY seq LIMIT 20");
|
|
for(const [seq,commandId,ok,resultHex] of batch){
|
|
const result=Buffer.from(resultHex||'','hex').toString('utf8').slice(0,16000);
|
|
const response=await backend('/api/bridge/ale/result',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({id:commandId,ok:ok==='1',result})
|
|
});
|
|
if(!response.ok)throw new Error(`result HTTP ${response.status}: ${await response.text()}`);
|
|
mysql(`UPDATE acore_auth.azerothai_result_outbox SET delivered_at=NOW(6) WHERE seq=${Number(seq)}`);
|
|
}
|
|
return batch.length;
|
|
}
|
|
|
|
function cleanup(){
|
|
mysql(`
|
|
UPDATE acore_auth.azerothai_event_outbox
|
|
SET delivered_at=NOW(6)
|
|
WHERE delivered_at IS NULL
|
|
AND kind IN ('player.combat.enter','player.combat.leave')
|
|
AND created_at < NOW() - INTERVAL 2 MINUTE;
|
|
DELETE FROM acore_auth.azerothai_command_inbox WHERE consumed_at IS NOT NULL AND consumed_at < NOW() - INTERVAL 1 DAY;
|
|
DELETE FROM acore_auth.azerothai_event_outbox WHERE delivered_at IS NOT NULL AND delivered_at < NOW() - INTERVAL 1 DAY;
|
|
DELETE FROM acore_auth.azerothai_result_outbox WHERE delivered_at IS NOT NULL AND delivered_at < NOW() - INTERVAL 1 DAY;`);
|
|
}
|
|
|
|
ensureSchema();
|
|
console.log(`AzerothAI ALE DB relay started: ${BASE}, poll ${POLL_MS}ms`);
|
|
|
|
while(!stopping){
|
|
try{
|
|
await deliverEvents();
|
|
await deliverResults();
|
|
await pullCommand();
|
|
if(Date.now()-lastCleanup>60_000){cleanup();lastCleanup=Date.now();}
|
|
await sleep(POLL_MS);
|
|
}catch(error){
|
|
console.error(new Date().toISOString(),error instanceof Error?error.message:String(error));
|
|
await sleep(1000);
|
|
}
|
|
}
|
|
|
|
console.log('AzerothAI ALE DB relay stopped.');
|