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
@@ -5,11 +5,14 @@ import { getApiKey } from '@/utils/key-vault'
|
||||
import type { ImageAttachment } from '@aiui/core/types/message'
|
||||
import { usePersonaStore } from '@/stores/personas'
|
||||
import { useMemoryStore } from '@/stores/memory'
|
||||
import { useArchy } from '@/composables/useArchy'
|
||||
|
||||
type Provider = 'claude' | 'openrouter' | 'mock'
|
||||
|
||||
const CLAUDE_PATH = '/api/claude/v1/messages'
|
||||
const OPENROUTER_PATH = '/api/openrouter'
|
||||
// API paths are relative to the base URL so they work both in dev (/) and Archy (/aiui/)
|
||||
const BASE = import.meta.env.BASE_URL || '/'
|
||||
const CLAUDE_PATH = `${BASE}api/claude/v1/messages`
|
||||
const OPENROUTER_PATH = `${BASE}api/openrouter`
|
||||
|
||||
import { mockFilms } from '@/mocks/films'
|
||||
import { mockSongs } from '@/mocks/songs'
|
||||
@@ -343,6 +346,10 @@ function buildSystemPrompt(chatStore: ReturnType<typeof useChatStore>): string {
|
||||
**Web search:** You have access to WebSearch and WebFetch tools. Use them to look up current information, news, and facts when the user asks. You can search the web and fetch page content. Web search is enabled for this session—do not tell the user it is unavailable.`
|
||||
}
|
||||
|
||||
// Append Archy node context when running embedded in Archipelago
|
||||
const archy = useArchy()
|
||||
prompt += archy.buildArchyContext()
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import { ref, readonly } from 'vue'
|
||||
import { archyBridge } from '@/services/archyBridge'
|
||||
|
||||
type AIContextCategory = 'apps' | 'system' | 'network' | 'wallet' | 'files'
|
||||
|
||||
interface ArchyApp {
|
||||
id: string
|
||||
name: string
|
||||
state: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface ArchySystemInfo {
|
||||
version?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface ArchyNetworkInfo {
|
||||
connected?: boolean
|
||||
}
|
||||
|
||||
// Singleton reactive state (shared across all components using this composable)
|
||||
const isEmbedded = ref(false)
|
||||
const isInitialized = ref(false)
|
||||
const permissions = ref<AIContextCategory[]>([])
|
||||
const accentColor = ref<string | null>(null)
|
||||
const installedApps = ref<ArchyApp[]>([])
|
||||
const systemInfo = ref<ArchySystemInfo>({})
|
||||
const networkInfo = ref<ArchyNetworkInfo>({})
|
||||
let cleanups: (() => void)[] = []
|
||||
|
||||
/**
|
||||
* Reactive composable wrapping archyBridge for Archy ↔ AIUI integration.
|
||||
* Call `init()` once in App.vue when `?embedded=true` is detected.
|
||||
*/
|
||||
export function useArchy() {
|
||||
/** Initialize the bridge and start listening for Archy messages */
|
||||
function init() {
|
||||
if (isInitialized.value) return
|
||||
|
||||
const embedded = !!(window as unknown as Record<string, unknown>).__AIUI_EMBEDDED__
|
||||
isEmbedded.value = embedded
|
||||
if (!embedded || !archyBridge.isInArchy()) return
|
||||
|
||||
archyBridge.init()
|
||||
isInitialized.value = true
|
||||
|
||||
// Listen for permission updates
|
||||
const unsubPerms = archyBridge.onPermissionsUpdate((cats) => {
|
||||
permissions.value = cats
|
||||
// Auto-fetch context for newly permitted categories
|
||||
fetchPermittedContext(cats)
|
||||
})
|
||||
cleanups.push(unsubPerms)
|
||||
|
||||
// Listen for theme updates
|
||||
const unsubTheme = archyBridge.onThemeUpdate((theme) => {
|
||||
accentColor.value = theme.accent
|
||||
applyAccentColor(theme.accent)
|
||||
})
|
||||
cleanups.push(unsubTheme)
|
||||
|
||||
// Request theme on init
|
||||
archyBridge.requestTheme()
|
||||
}
|
||||
|
||||
/** Fetch context for all permitted categories */
|
||||
async function fetchPermittedContext(cats: AIContextCategory[]) {
|
||||
const fetches: Promise<void>[] = []
|
||||
|
||||
if (cats.includes('apps')) {
|
||||
fetches.push(
|
||||
archyBridge.requestContext('apps').then((res) => {
|
||||
if (res.permitted && Array.isArray(res.data)) {
|
||||
installedApps.value = res.data as ArchyApp[]
|
||||
}
|
||||
}).catch(() => {}),
|
||||
)
|
||||
}
|
||||
|
||||
if (cats.includes('system')) {
|
||||
fetches.push(
|
||||
archyBridge.requestContext('system').then((res) => {
|
||||
if (res.permitted && res.data) {
|
||||
systemInfo.value = res.data as ArchySystemInfo
|
||||
}
|
||||
}).catch(() => {}),
|
||||
)
|
||||
}
|
||||
|
||||
if (cats.includes('network')) {
|
||||
fetches.push(
|
||||
archyBridge.requestContext('network').then((res) => {
|
||||
if (res.permitted && res.data) {
|
||||
networkInfo.value = res.data as ArchyNetworkInfo
|
||||
}
|
||||
}).catch(() => {}),
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(fetches)
|
||||
}
|
||||
|
||||
/** Refresh context data (call when user returns to chat) */
|
||||
async function refreshContext() {
|
||||
if (!isInitialized.value) return
|
||||
await fetchPermittedContext(permissions.value)
|
||||
}
|
||||
|
||||
/** Request Archy to perform an action */
|
||||
async function requestAction(action: string, params: Record<string, string> = {}) {
|
||||
if (!isInitialized.value) return { success: false, error: 'Not initialized' }
|
||||
return archyBridge.requestAction(action, params)
|
||||
}
|
||||
|
||||
/** Apply accent color as CSS custom property */
|
||||
function applyAccentColor(color: string) {
|
||||
document.documentElement.style.setProperty('--color-accent', color)
|
||||
}
|
||||
|
||||
/** Build context string for AI system prompt */
|
||||
function buildArchyContext(): string {
|
||||
if (!isInitialized.value) return ''
|
||||
|
||||
const sections: string[] = []
|
||||
|
||||
if (permissions.value.includes('apps') && installedApps.value.length > 0) {
|
||||
const appList = installedApps.value
|
||||
.map((a) => `- ${a.name} (${a.state}${a.status ? ', ' + a.status : ''})`)
|
||||
.join('\n')
|
||||
sections.push(`**Installed apps on this node:**\n${appList}`)
|
||||
}
|
||||
|
||||
if (permissions.value.includes('system') && systemInfo.value.name) {
|
||||
const sys = systemInfo.value
|
||||
sections.push(`**System:** ${sys.name}${sys.version ? ' v' + sys.version : ''}`)
|
||||
}
|
||||
|
||||
if (permissions.value.includes('network')) {
|
||||
const net = networkInfo.value
|
||||
sections.push(`**Network:** ${net.connected ? 'Connected' : 'Disconnected'}`)
|
||||
}
|
||||
|
||||
if (sections.length === 0) return ''
|
||||
|
||||
return `\n\n**Archy Node Context** (this user is running AIUI on their Archipelago node):\n${sections.join('\n')}\n\nYou can help the user manage their node. Available actions: open an app (open-app), install an app (install-app), navigate in Archy (navigate). When recommending apps, check if they're already installed.`
|
||||
}
|
||||
|
||||
/** Clean up on component unmount */
|
||||
function destroy() {
|
||||
for (const cleanup of cleanups) cleanup()
|
||||
cleanups = []
|
||||
archyBridge.destroy()
|
||||
isInitialized.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
isEmbedded: readonly(isEmbedded),
|
||||
isInitialized: readonly(isInitialized),
|
||||
permissions: readonly(permissions),
|
||||
accentColor: readonly(accentColor),
|
||||
installedApps: readonly(installedApps),
|
||||
systemInfo: readonly(systemInfo),
|
||||
networkInfo: readonly(networkInfo),
|
||||
init,
|
||||
destroy,
|
||||
refreshContext,
|
||||
requestAction,
|
||||
buildArchyContext,
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,8 @@ export function useSimilarContent() {
|
||||
const typeLabel = type === 'tv' ? 'TV series' : type
|
||||
const prompt = `List exactly 3 ${typeLabel}s similar to "${title}". For each, respond with ONLY a JSON array like: [{"title":"Name","reason":"one sentence why"}]. No other text.`
|
||||
|
||||
const res = await fetch('/api/claude/v1/messages', {
|
||||
const base = import.meta.env.BASE_URL || '/'
|
||||
const res = await fetch(`${base}api/claude/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user