Import verified AzerothAI snapshot from GitHub
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
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
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { D1Store } from '@/core/store';
|
||||
import { execute } from '@/core/service';
|
||||
import { runLiveAutonomyTick } from '@/core/autonomy';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
function requestOriginAllowed(request:Request){
|
||||
const origin=request.headers.get('origin');
|
||||
if(!origin)return true;
|
||||
if(origin===new URL(request.url).origin)return true;
|
||||
const proto=(request.headers.get('x-forwarded-proto')??'').split(',')[0]?.trim().toLowerCase();
|
||||
const host=(request.headers.get('x-forwarded-host')??request.headers.get('host')??'').split(',')[0]?.trim();
|
||||
if((proto==='http'||proto==='https')&&host&&origin===`${proto}://${host}`)return true;
|
||||
return false;
|
||||
}
|
||||
export async function POST(request:Request){
|
||||
if(!requestOriginAllowed(request))return Response.json({error:'Cross-origin action rejected'},{status:403});
|
||||
try{
|
||||
const text=await request.text();
|
||||
if(text.length>16000)return Response.json({error:'Request too large'},{status:413});
|
||||
const body=JSON.parse(text);
|
||||
const store=new D1Store();
|
||||
if(body?.type==='think'&&typeof body.botId==='string'){
|
||||
const state=await store.load();
|
||||
const bot=state.bots.find(b=>b.id===body.botId);
|
||||
if(bot?.autonomy?.managed){
|
||||
const result=await runLiveAutonomyTick(store,env.DB!,env as unknown as RuntimeConfig,{forceBotId:bot.id});
|
||||
if(!result.ran)throw new Error(result.reason);
|
||||
return Response.json(result);
|
||||
}
|
||||
}
|
||||
return Response.json(await execute(store,body,env as unknown as RuntimeConfig));
|
||||
}
|
||||
catch(e){const message=e instanceof Error?e.message:'Action failed';return Response.json({error:message},{status:400});}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { z } from 'zod';
|
||||
import { requireAleOperator,rawAleEnabled } from '@/core/ale-auth';
|
||||
import { compileAleOperatorRequest } from '@/core/ale';
|
||||
import { queueAleCommand } from '@/core/ale-store';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
|
||||
const scalar=z.union([z.string(),z.number(),z.boolean(),z.null()]);
|
||||
const schema=z.discriminatedUnion('kind',[
|
||||
z.object({kind:z.literal('command'),command:z.string().min(1).max(500)}),
|
||||
z.object({kind:z.literal('global'),method:z.string().min(1).max(100),args:z.array(scalar).max(32).optional()}),
|
||||
z.object({kind:z.literal('player'),player:z.string().min(1).max(24),method:z.string().min(1).max(100),args:z.array(scalar).max(32).optional()}),
|
||||
z.object({kind:z.literal('playerbotAI'),player:z.string().min(1).max(24),method:z.string().min(1).max(100),args:z.array(scalar).max(32).optional()}),
|
||||
z.object({kind:z.literal('raw'),source:z.string().min(1).max(12000)})
|
||||
]);
|
||||
|
||||
export async function POST(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
try{
|
||||
requireAleOperator(request,config);
|
||||
const text=await request.text();if(text.length>16000)return Response.json({error:'Request too large'},{status:413});
|
||||
const body=schema.parse(JSON.parse(text));
|
||||
const source=compileAleOperatorRequest(body,rawAleEnabled(config));
|
||||
const queued=await queueAleCommand(env.DB!,source);
|
||||
return Response.json({ok:true,command:queued},{status:202});
|
||||
}catch(e){
|
||||
const message=e instanceof Error?e.message:'ALE operator failure';
|
||||
return Response.json({error:message},{status:message.startsWith('Unauthorized')?401:400});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { requireAleOperator,rawAleEnabled } from '@/core/ale-auth';
|
||||
import { recentAleCommands,recentAleEvents } from '@/core/ale-store';
|
||||
import { ALE_SURFACE } from '@/core/ale';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
|
||||
export async function GET(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
try{
|
||||
requireAleOperator(request,config);
|
||||
const [commands,events]=await Promise.all([recentAleCommands(env.DB!,50),recentAleEvents(env.DB!,100)]);
|
||||
return Response.json({ok:true,rawEnabled:rawAleEnabled(config),surface:ALE_SURFACE,commands,events},{headers:{'Cache-Control':'no-store'}});
|
||||
}catch(e){
|
||||
const message=e instanceof Error?e.message:'ALE status failure';
|
||||
return Response.json({error:message},{status:message.startsWith('Unauthorized')?401:500});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {env} from 'cloudflare:workers';
|
||||
import {runLiveAutonomyTick} from '@/core/autonomy';
|
||||
import {D1Store} from '@/core/store';
|
||||
import type {RuntimeConfig} from '@/core/types';
|
||||
|
||||
function authorized(request:Request,config:RuntimeConfig){
|
||||
const token=config.AUTONOMY_TOKEN;
|
||||
if(!token)return false;
|
||||
return request.headers.get('authorization')===`Bearer ${token}`;
|
||||
}
|
||||
|
||||
export async function POST(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
if(!authorized(request,config))return Response.json({error:'Unauthorized autonomy heartbeat'},{status:401});
|
||||
try{
|
||||
return Response.json(await runLiveAutonomyTick(new D1Store(),env.DB!,config));
|
||||
}catch(e){
|
||||
return Response.json({error:e instanceof Error?e.message:'Autonomy heartbeat failed'},{status:500});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { z } from 'zod';
|
||||
import { requireAleBridge } from '@/core/ale-auth';
|
||||
import { recordAleEvent } from '@/core/ale-store';
|
||||
import { queueConversationEvent } from '@/core/conversation-store';
|
||||
import { D1Store } from '@/core/store';
|
||||
import { applyAuthoritativeAleEvent,execute } from '@/core/service';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
|
||||
const schema=z.object({
|
||||
kind:z.string().min(1).max(100),
|
||||
payload:z.record(z.unknown()).default({})
|
||||
}).strict();
|
||||
|
||||
const STATEFUL_EVENTS=new Set([
|
||||
'bridge.ready',
|
||||
'player.login',
|
||||
'player.logout',
|
||||
'player.snapshot',
|
||||
'player.first-login',
|
||||
'player.zone',
|
||||
'player.map',
|
||||
'player.level',
|
||||
'player.quest.accept',
|
||||
'player.achievement',
|
||||
'player.close-call',
|
||||
'player.killed-by-creature',
|
||||
'player.repop',
|
||||
'player.resurrect'
|
||||
]);
|
||||
|
||||
export async function POST(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
try{
|
||||
requireAleBridge(request,config);
|
||||
const text=await request.text();if(text.length>40000)return Response.json({error:'Event too large'},{status:413});
|
||||
const body=schema.parse(JSON.parse(text));
|
||||
await recordAleEvent(env.DB!,body.kind,body.payload);
|
||||
|
||||
if((body.kind==='player.chat'||body.kind==='player.whisper')&&body.payload.isBot!==true){
|
||||
await queueConversationEvent(env.DB!,body.kind,body.payload);
|
||||
}
|
||||
|
||||
const store=new D1Store();
|
||||
if(STATEFUL_EVENTS.has(body.kind)){
|
||||
await applyAuthoritativeAleEvent(store,body.kind,body.payload);
|
||||
}
|
||||
|
||||
if(body.kind==='player.zone'&&body.payload.isBot!==true){
|
||||
const zone=String(body.payload.zoneName??body.payload.zone??'').slice(0,120);
|
||||
if(zone)await execute(store,{type:'worldTrigger',kind:'zone-change',zone,playerName:String(body.payload.name??'').slice(0,40),detail:'Authoritative ALE human-player zone event'},config);
|
||||
}
|
||||
|
||||
return Response.json({ok:true});
|
||||
}catch(e){
|
||||
const message=e instanceof Error?e.message:'ALE event failure';
|
||||
return Response.json({error:message},{status:message.startsWith('Unauthorized')?401:400});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { requireAleBridge } from '@/core/ale-auth';
|
||||
import { pollAleCommand } from '@/core/ale-store';
|
||||
import { wrapAleCommand } from '@/core/ale';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
|
||||
async function handlePoll(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
try{
|
||||
requireAleBridge(request,config);
|
||||
const command=await pollAleCommand(env.DB!);
|
||||
if(!command)return new Response('return',{status:200,headers:{'Content-Type':'text/plain; charset=utf-8','Cache-Control':'no-store'}});
|
||||
return new Response(wrapAleCommand(command.id,command.source),{status:200,headers:{'Content-Type':'text/plain; charset=utf-8','Cache-Control':'no-store','X-AzerothAI-Command':command.id}});
|
||||
}catch(e){
|
||||
const message=e instanceof Error?e.message:'ALE bridge failure';
|
||||
return Response.json({error:message},{status:message.startsWith('Unauthorized')?401:500});
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request:Request){ return handlePoll(request); }
|
||||
export async function POST(request:Request){ return handlePoll(request); }
|
||||
@@ -0,0 +1,21 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { z } from 'zod';
|
||||
import { requireAleBridge } from '@/core/ale-auth';
|
||||
import { completeAleCommand } from '@/core/ale-store';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
|
||||
const schema=z.object({id:z.string().uuid(),ok:z.boolean(),result:z.string().max(16000)}).strict();
|
||||
|
||||
export async function POST(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
try{
|
||||
requireAleBridge(request,config);
|
||||
const text=await request.text();if(text.length>20000)return Response.json({error:'Result too large'},{status:413});
|
||||
const body=schema.parse(JSON.parse(text));
|
||||
await completeAleCommand(env.DB!,body.id,body.ok,body.result);
|
||||
return Response.json({ok:true});
|
||||
}catch(e){
|
||||
const message=e instanceof Error?e.message:'ALE result failure';
|
||||
return Response.json({error:message},{status:message.startsWith('Unauthorized')?401:400});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {env} from 'cloudflare:workers';
|
||||
import {runConversationTick} from '@/core/conversation';
|
||||
import {D1Store} from '@/core/store';
|
||||
import type {RuntimeConfig} from '@/core/types';
|
||||
|
||||
function authorized(request:Request,config:RuntimeConfig){
|
||||
const token=config.AUTONOMY_TOKEN;
|
||||
if(!token)return false;
|
||||
return request.headers.get('authorization')===`Bearer ${token}`;
|
||||
}
|
||||
|
||||
export async function POST(request:Request){
|
||||
const config=env as unknown as RuntimeConfig;
|
||||
if(!authorized(request,config))return Response.json({error:'Unauthorized conversation heartbeat'},{status:401});
|
||||
try{
|
||||
return Response.json(await runConversationTick(new D1Store(),env.DB!,config));
|
||||
}catch(e){
|
||||
return Response.json({error:e instanceof Error?e.message:'Conversation heartbeat failed'},{status:500});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { D1Store } from '@/core/store';
|
||||
// Reconnectable SSE batches with Last-Event-ID replay. Closing each batch avoids
|
||||
// tying a Worker invocation to an unbounded loop; EventSource reconnects in 3s.
|
||||
export async function GET(request:Request){
|
||||
try{const s=await new D1Store().load();const raw=request.headers.get('last-event-id')??new URL(request.url).searchParams.get('after')??'0';const after=Math.max(0,Number(raw)||0);let data='retry: 3000\n\n';if(after&&s.events[0]?.seq>after+1)data+='event: gap\ndata: {"reload":true}\n\n';for(const e of s.events.filter(e=>e.seq>after))data+=`id: ${e.seq}\nevent: realm\ndata: ${JSON.stringify(e)}\n\n`;return new Response(data+': heartbeat\n\n',{headers:{'Content-Type':'text/event-stream','Cache-Control':'no-cache'}});}catch{return Response.json({error:'Event stream unavailable'},{status:503});}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { D1Store } from '@/core/store';
|
||||
import { rawAleEnabled } from '@/core/ale-auth';
|
||||
import { recentAleEvents } from '@/core/ale-store';
|
||||
import type { RuntimeConfig } from '@/core/types';
|
||||
|
||||
export async function GET(){
|
||||
const c=env as unknown as RuntimeConfig;
|
||||
try{
|
||||
await new D1Store().load();
|
||||
const aleEvents=await recentAleEvents(env.DB!,20);
|
||||
const lastAleAt=aleEvents[0]?.at??null;
|
||||
const aleActiveRecently=!!lastAleAt&&Date.now()-Date.parse(lastAleAt)<90_000;
|
||||
return Response.json({
|
||||
ok:true,
|
||||
aleActiveRecently,
|
||||
lastAleAt,
|
||||
bridgeConfigured:!!(c.BRIDGE_URL&&c.BRIDGE_TOKEN),
|
||||
aleConfigured:!!c.ALE_BRIDGE_TOKEN,
|
||||
aleOperatorConfigured:!!c.ALE_OPERATOR_TOKEN,
|
||||
aleRawEnabled:rawAleEnabled(c),
|
||||
llmConfigured:!!c.LLM_URL,
|
||||
soapConfigured:!!(c.SOAP_URL&&c.SOAP_USER&&c.SOAP_PASSWORD)
|
||||
});
|
||||
}catch{
|
||||
return Response.json({ok:false},{status:503});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { D1Store } from '@/core/store';
|
||||
export async function GET(){try{return Response.json(await new D1Store().load(),{headers:{'Cache-Control':'no-store'}});}catch{return Response.json({error:'Realm database unavailable. Please retry.'},{status:503});}}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { env } from 'cloudflare:workers';
|
||||
import { recentAleCommands,recentAleEvents } from '@/core/ale-store';
|
||||
|
||||
type SafeWorldEvent={
|
||||
id:number;
|
||||
at:string;
|
||||
kind:string;
|
||||
name?:string;
|
||||
zone?:string;
|
||||
level?:number;
|
||||
isBot?:boolean;
|
||||
};
|
||||
|
||||
function safeEvent(event:{id:number;at:string;kind:string;payload:unknown}):SafeWorldEvent{
|
||||
const payload=(event.payload&&typeof event.payload==='object'?event.payload:{}) as Record<string,unknown>;
|
||||
const out:SafeWorldEvent={id:event.id,at:event.at,kind:event.kind};
|
||||
if(typeof payload.name==='string')out.name=payload.name.slice(0,40);
|
||||
const zone=typeof payload.zoneName==='string'?payload.zoneName:typeof payload.zone==='string'?payload.zone:undefined;
|
||||
if(zone)out.zone=zone.slice(0,120);
|
||||
if(typeof payload.level==='number'&&Number.isFinite(payload.level))out.level=payload.level;
|
||||
if(typeof payload.isBot==='boolean')out.isBot=payload.isBot;
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function GET(){
|
||||
try{
|
||||
const [events,commands]=await Promise.all([
|
||||
recentAleEvents(env.DB!,80),
|
||||
recentAleCommands(env.DB!,30)
|
||||
]);
|
||||
const newest=events[0];
|
||||
const now=Date.now();
|
||||
const lastEventAt=newest?.at??null;
|
||||
const ageMs=lastEventAt?Math.max(0,now-Date.parse(lastEventAt)):null;
|
||||
const recentWindow=events.filter(e=>now-Date.parse(e.at)<5*60_000);
|
||||
return Response.json({
|
||||
ok:true,
|
||||
ale:{
|
||||
lastEventAt,
|
||||
ageMs,
|
||||
activeRecently:recentWindow.length>0,
|
||||
bridgeReady:events.some(e=>e.kind==='bridge.ready')
|
||||
},
|
||||
playerbots:{
|
||||
observedBots:new Set(recentWindow.flatMap(e=>{
|
||||
const p=(e.payload&&typeof e.payload==='object'?e.payload:{}) as Record<string,unknown>;
|
||||
if(p.isBot!==true)return [];
|
||||
if(typeof p.characterGuid==='number')return [`guid:${p.characterGuid}`];
|
||||
if(typeof p.name==='string'&&p.name.trim())return [`name:${p.name.toLowerCase()}`];
|
||||
return [];
|
||||
})).size
|
||||
},
|
||||
commands:{
|
||||
pending:commands.filter(c=>c.status==='pending').length,
|
||||
dispatched:commands.filter(c=>c.status==='dispatched').length,
|
||||
completed:commands.filter(c=>c.status==='completed').length,
|
||||
failed:commands.filter(c=>c.status==='failed').length
|
||||
},
|
||||
events:events.slice(0,30).map(safeEvent)
|
||||
},{headers:{'Cache-Control':'no-store'}});
|
||||
}catch{
|
||||
return Response.json({
|
||||
ok:false,
|
||||
ale:{lastEventAt:null,ageMs:null,activeRecently:false,bridgeReady:false},
|
||||
playerbots:{observedBots:0},
|
||||
commands:{pending:0,dispatched:0,completed:0,failed:0},
|
||||
events:[]
|
||||
},{status:200,headers:{'Cache-Control':'no-store'}});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user