feat(archy): wire archyBridge into app, base-aware API paths, nginx config
- Create useArchy composable wrapping archyBridge with reactive Vue state - Initialize bridge in App.vue when ?embedded=true detected - Inject Archy node context (apps, system, network) into AI system prompt - Make API paths base-aware (import.meta.env.BASE_URL) for /aiui/ deployment - Add nginx-archy.conf for production Anthropic API proxy with SSE support - Fix archyBridge.ts typecheck error, export ActionResponse type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f0eb4383bd
commit
89885269f0
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Archy Bridge — postMessage client for AIUI ↔ Archipelago communication.
|
||||
*
|
||||
* This is the ONLY way AIUI should communicate with the host Archy app.
|
||||
* Never make direct HTTP requests to the host machine.
|
||||
*/
|
||||
|
||||
type AIContextCategory = 'apps' | 'system' | 'network' | 'wallet' | 'files'
|
||||
|
||||
interface ContextResponse {
|
||||
data: unknown
|
||||
permitted: boolean
|
||||
}
|
||||
|
||||
export interface ActionResponse {
|
||||
success: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ThemeInfo {
|
||||
accent: string
|
||||
mode: 'dark'
|
||||
}
|
||||
|
||||
type PermissionsCallback = (categories: AIContextCategory[]) => void
|
||||
type ThemeCallback = (theme: ThemeInfo) => void
|
||||
|
||||
let requestId = 0
|
||||
const pendingRequests = new Map<string, {
|
||||
resolve: (value: unknown) => void
|
||||
reject: (reason: unknown) => void
|
||||
}>()
|
||||
|
||||
const permissionsCallbacks: PermissionsCallback[] = []
|
||||
const themeCallbacks: ThemeCallback[] = []
|
||||
let currentPermissions: AIContextCategory[] = []
|
||||
let currentTheme: ThemeInfo | null = null
|
||||
let initialized = false
|
||||
|
||||
function generateId(): string {
|
||||
return `aiui-${++requestId}-${Date.now()}`
|
||||
}
|
||||
|
||||
function postToParent(msg: unknown) {
|
||||
if (window.parent === window) return // Not in iframe
|
||||
window.parent.postMessage(msg, '*')
|
||||
}
|
||||
|
||||
function handleMessage(event: MessageEvent) {
|
||||
const msg = event.data
|
||||
if (!msg || typeof msg.type !== 'string') return
|
||||
|
||||
switch (msg.type) {
|
||||
case 'context:response': {
|
||||
const pending = pendingRequests.get(msg.id)
|
||||
if (pending) {
|
||||
pendingRequests.delete(msg.id)
|
||||
pending.resolve({ data: msg.data, permitted: msg.permitted })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'action:response': {
|
||||
const pending = pendingRequests.get(msg.id)
|
||||
if (pending) {
|
||||
pendingRequests.delete(msg.id)
|
||||
pending.resolve({ success: msg.success, error: msg.error })
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'permissions:update': {
|
||||
currentPermissions = msg.categories || []
|
||||
for (const cb of permissionsCallbacks) cb(currentPermissions)
|
||||
break
|
||||
}
|
||||
|
||||
case 'theme:response': {
|
||||
const theme: ThemeInfo | undefined = msg.theme
|
||||
if (theme) {
|
||||
currentTheme = theme
|
||||
for (const cb of themeCallbacks) cb(theme)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const archyBridge = {
|
||||
/**
|
||||
* Initialize the bridge. Call once on app mount.
|
||||
* Sends 'ready' to Archy so it knows the iframe is loaded.
|
||||
*/
|
||||
init() {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
window.addEventListener('message', handleMessage)
|
||||
postToParent({ type: 'ready' })
|
||||
},
|
||||
|
||||
/** Clean up listeners */
|
||||
destroy() {
|
||||
window.removeEventListener('message', handleMessage)
|
||||
pendingRequests.clear()
|
||||
initialized = false
|
||||
},
|
||||
|
||||
/** Check if running inside Archy iframe */
|
||||
isInArchy(): boolean {
|
||||
return window.parent !== window
|
||||
},
|
||||
|
||||
/**
|
||||
* Request context data from Archy.
|
||||
* Returns { data, permitted }. If permitted is false, the user hasn't enabled this category.
|
||||
*/
|
||||
requestContext(category: AIContextCategory, query?: string): Promise<ContextResponse> {
|
||||
const id = generateId()
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.set(id, { resolve: resolve as (v: unknown) => void, reject })
|
||||
postToParent({
|
||||
type: 'context:request',
|
||||
id,
|
||||
category,
|
||||
query,
|
||||
})
|
||||
|
||||
// Timeout after 10s
|
||||
setTimeout(() => {
|
||||
if (pendingRequests.has(id)) {
|
||||
pendingRequests.delete(id)
|
||||
reject(new Error(`Context request timed out: ${category}`))
|
||||
}
|
||||
}, 10000)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Request Archy to perform an action (install app, navigate, etc.)
|
||||
*/
|
||||
requestAction(action: string, params: Record<string, string> = {}): Promise<ActionResponse> {
|
||||
const id = generateId()
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.set(id, { resolve: resolve as (v: unknown) => void, reject })
|
||||
postToParent({
|
||||
type: 'action:request',
|
||||
id,
|
||||
action,
|
||||
params,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
if (pendingRequests.has(id)) {
|
||||
pendingRequests.delete(id)
|
||||
reject(new Error(`Action request timed out: ${action}`))
|
||||
}
|
||||
}, 30000)
|
||||
})
|
||||
},
|
||||
|
||||
/** Request Archy's theme info */
|
||||
requestTheme() {
|
||||
postToParent({ type: 'theme:request' })
|
||||
},
|
||||
|
||||
/** Register callback for permission updates */
|
||||
onPermissionsUpdate(callback: PermissionsCallback): () => void {
|
||||
permissionsCallbacks.push(callback)
|
||||
// If we already have permissions, fire immediately
|
||||
if (currentPermissions.length > 0) callback(currentPermissions)
|
||||
return () => {
|
||||
const idx = permissionsCallbacks.indexOf(callback)
|
||||
if (idx !== -1) permissionsCallbacks.splice(idx, 1)
|
||||
}
|
||||
},
|
||||
|
||||
/** Register callback for theme updates */
|
||||
onThemeUpdate(callback: ThemeCallback): () => void {
|
||||
themeCallbacks.push(callback)
|
||||
if (currentTheme) callback(currentTheme)
|
||||
return () => {
|
||||
const idx = themeCallbacks.indexOf(callback)
|
||||
if (idx !== -1) themeCallbacks.splice(idx, 1)
|
||||
}
|
||||
},
|
||||
|
||||
/** Get current permitted categories */
|
||||
getPermissions(): AIContextCategory[] {
|
||||
return [...currentPermissions]
|
||||
},
|
||||
|
||||
/** Get current theme */
|
||||
getTheme(): ThemeInfo | null {
|
||||
return currentTheme
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user