diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 3634bf3..760e2d7 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -542,7 +542,11 @@ export async function GET() { agents: agentsWithStatus, providers, defaults: { model: defaultModel, fallbacks }, - gateway: { port: config.gateway?.port || 18789, token: config.gateway?.auth?.token || "" }, + gateway: { + port: config.gateway?.port || 18789, + token: config.gateway?.auth?.token || "", + host: config.gateway?.host || config.gateway?.hostname || "", + }, groupChats, }; configCache = { data, ts: Date.now() }; diff --git a/app/api/test-agents/route.ts b/app/api/test-agents/route.ts index 20535a8..b2948b1 100644 --- a/app/api/test-agents/route.ts +++ b/app/api/test-agents/route.ts @@ -126,3 +126,7 @@ export async function POST() { return NextResponse.json({ error: err.message }, { status: 500 }); } } + +export async function GET() { + return POST(); +} diff --git a/app/api/test-dm-sessions/route.ts b/app/api/test-dm-sessions/route.ts index 1df2e81..5910335 100644 --- a/app/api/test-dm-sessions/route.ts +++ b/app/api/test-dm-sessions/route.ts @@ -130,3 +130,7 @@ export async function POST() { return NextResponse.json({ error: err.message }, { status: 500 }); } } + +export async function GET() { + return POST(); +} diff --git a/app/page.tsx b/app/page.tsx index 584eb0f..490b472 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -41,7 +41,7 @@ interface ConfigData { agents: Agent[]; defaults: { model: string; fallbacks: string[] }; providers?: { id: string; accessMode?: "auth" | "api_key" }[]; - gateway?: { port: number; token?: string }; + gateway?: { port: number; token?: string; host?: string }; groupChats?: GroupChat[]; } @@ -183,7 +183,7 @@ interface PlatformTestResult { } // 平台标签颜色 -function PlatformBadge({ platform, agentId, gatewayPort, gatewayToken, t, testResult }: { platform: Platform; agentId: string; gatewayPort: number; gatewayToken?: string; t: TFunc; testResult?: PlatformTestResult | null }) { +function PlatformBadge({ platform, agentId, gatewayPort, gatewayToken, gatewayHost, t, testResult }: { platform: Platform; agentId: string; gatewayPort: number; gatewayToken?: string; gatewayHost?: string; t: TFunc; testResult?: PlatformTestResult | null }) { const pName = platform.name; let sessionKey: string; @@ -198,8 +198,8 @@ function PlatformBadge({ platform, agentId, gatewayPort, gatewayToken, t, testRe } else { sessionKey = `agent:${agentId}:main`; } - let sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey }); - if (gatewayToken) sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey, token: gatewayToken }); + let sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey }, gatewayHost); + if (gatewayToken) sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey, token: gatewayToken }, gatewayHost); const badgeStyle = pName === "feishu" ? "bg-blue-500/20 text-blue-300 border border-blue-500/30 hover:bg-blue-500/40 hover:border-blue-400" @@ -331,10 +331,10 @@ function AgentStatusBadge({ state, t }: { state?: string; t: TFunc }) { } // Agent 卡片 -function AgentCard({ agent, gatewayPort, gatewayToken, t, testResult, platformTestResults, sessionTestResult, agentState, dmSessionResults, providerAccessModeMap }: { agent: Agent; gatewayPort: number; gatewayToken?: string; t: TFunc; testResult?: { ok: boolean; text?: string; error?: string; elapsed: number } | null; platformTestResults?: Record; sessionTestResult?: { ok: boolean; reply?: string; error?: string; elapsed: number } | null; agentState?: string; dmSessionResults?: Record; providerAccessModeMap?: Record }) { +function AgentCard({ agent, gatewayPort, gatewayToken, gatewayHost, t, testResult, platformTestResults, sessionTestResult, agentState, dmSessionResults, providerAccessModeMap }: { agent: Agent; gatewayPort: number; gatewayToken?: string; gatewayHost?: string; t: TFunc; testResult?: { ok: boolean; text?: string; error?: string; elapsed: number } | null; platformTestResults?: Record; sessionTestResult?: { ok: boolean; reply?: string; error?: string; elapsed: number } | null; agentState?: string; dmSessionResults?: Record; providerAccessModeMap?: Record }) { const sessionKey = `agent:${agent.id}:main`; - let sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey }); - if (gatewayToken) sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey, token: gatewayToken }); + let sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey }, gatewayHost); + if (gatewayToken) sessionUrl = buildGatewayUrl(gatewayPort, "/chat", { session: sessionKey, token: gatewayToken }, gatewayHost); const modelProvider = agent.model.includes("/") ? agent.model.split("/", 1)[0] : "default"; const modelAccessMode = providerAccessModeMap?.[modelProvider]; @@ -406,7 +406,7 @@ function AgentCard({ agent, gatewayPort, gatewayToken, t, testResult, platformTe const dmResult = dmSessionResults ? dmSessionResults[dmKey] : undefined; return (
- +
{dmResult === undefined ? ( DM Session: -- @@ -524,6 +524,33 @@ export default function Home() { { label: t("refresh.10m"), value: 600 }, ]; + const parseApiPayload = useCallback(async (resp: Response) => { + const raw = await resp.text(); + let parsed: any = null; + try { + parsed = JSON.parse(raw); + } catch {} + const errorText = parsed?.error || raw || `HTTP ${resp.status}`; + return { ok: resp.ok, status: resp.status, data: parsed, errorText }; + }, []); + + const callTestApi = useCallback(async (url: string) => { + const requestWithMethod = async (method: "POST" | "GET") => { + const resp = await fetch(url, { method, cache: "no-store" }); + return parseApiPayload(resp); + }; + + const first = await requestWithMethod("POST"); + if (first.ok) return first.data; + + const methodIssue = first.status === 405 || /method not allowed/i.test(first.errorText || ""); + if (!methodIssue) throw new Error(first.errorText); + + const fallback = await requestWithMethod("GET"); + if (fallback.ok) return fallback.data; + throw new Error(fallback.errorText || first.errorText); + }, [parseApiPayload]); + const fetchData = useCallback((silent = false) => { if (!silent) setLoading(true); Promise.all([ @@ -629,8 +656,7 @@ export default function Home() { const pending: Record = {}; if (data) for (const a of data.agents) pending[a.id] = null; setTestResults(pending); - fetch("/api/test-agents", { method: "POST" }) - .then((r) => r.json()) + callTestApi("/api/test-agents") .then((resp) => { if (resp.results) { const map: Record = {}; @@ -638,9 +664,14 @@ export default function Home() { setTestResults(map); } }) - .catch(() => {}) + .catch((e) => { + const msg = e instanceof Error ? e.message : "Request failed"; + const failed: Record = {}; + if (data) for (const a of data.agents) failed[a.id] = { ok: false, error: msg, elapsed: 0 }; + setTestResults(failed); + }) .finally(() => setTesting(false)); - }, [data]); + }, [data, callTestApi]); const testAllPlatforms = useCallback(() => { setTestingPlatforms(true); @@ -654,8 +685,7 @@ export default function Home() { } } setPlatformTestResults(pending); - fetch("/api/test-platforms", { method: "POST" }) - .then((r) => r.json()) + callTestApi("/api/test-platforms") .then((resp) => { if (resp.results) { const map: Record = {}; @@ -663,17 +693,31 @@ export default function Home() { setPlatformTestResults(map); } }) - .catch(() => {}) + .catch((e) => { + const msg = e instanceof Error ? e.message : "Request failed"; + const failed: Record = {}; + if (data) { + for (const a of data.agents) { + for (const p of a.platforms) { + failed[`${a.id}:${p.name}`] = { + ok: false, + error: msg, + elapsed: 0, + }; + } + } + } + setPlatformTestResults(failed); + }) .finally(() => setTestingPlatforms(false)); - }, [data]); + }, [data, callTestApi]); const testAllSessions = useCallback(() => { setTestingSessions(true); const pending: Record = {}; if (data) for (const a of data.agents) pending[a.id] = null; setSessionTestResults(pending); - fetch("/api/test-sessions", { method: "POST" }) - .then((r) => r.json()) + callTestApi("/api/test-sessions") .then((resp) => { if (resp.results) { const map: Record = {}; @@ -681,9 +725,14 @@ export default function Home() { setSessionTestResults(map); } }) - .catch(() => {}) + .catch((e) => { + const msg = e instanceof Error ? e.message : "Request failed"; + const failed: Record = {}; + if (data) for (const a of data.agents) failed[a.id] = { ok: false, error: msg, elapsed: 0 }; + setSessionTestResults(failed); + }) .finally(() => setTestingSessions(false)); - }, [data]); + }, [data, callTestApi]); const testAllDmSessions = useCallback(() => { setTestingDmSessions(true); @@ -696,8 +745,7 @@ export default function Home() { } } setDmSessionResults(pending); - fetch("/api/test-dm-sessions", { method: "POST" }) - .then((r) => r.json()) + callTestApi("/api/test-dm-sessions") .then((resp) => { if (resp.results) { const map: Record = {}; @@ -705,9 +753,24 @@ export default function Home() { setDmSessionResults(map); } }) - .catch(() => {}) + .catch((e) => { + const msg = e instanceof Error ? e.message : "Request failed"; + const failed: Record = {}; + if (data) { + for (const a of data.agents) { + for (const p of a.platforms) { + failed[`${a.id}:${p.name}`] = { + ok: false, + error: msg, + elapsed: 0, + }; + } + } + } + setDmSessionResults(failed); + }) .finally(() => setTestingDmSessions(false)); - }, [data]); + }, [data, callTestApi]); // 定时刷新 useEffect(() => { @@ -837,7 +900,7 @@ export default function Home() { {/* 卡片墙 */}
{data.agents.map((agent) => ( - + ))}
diff --git a/app/pixel-office/page.tsx b/app/pixel-office/page.tsx index c6ff11e..322dd3c 100644 --- a/app/pixel-office/page.tsx +++ b/app/pixel-office/page.tsx @@ -18,7 +18,7 @@ import { TILE_SIZE } from '@/lib/pixel-office/constants' import { TileType, EditTool } from '@/lib/pixel-office/types' import type { TileType as TileTypeVal, FloorColor, OfficeLayout } from '@/lib/pixel-office/types' import { getCatalogEntry, isRotatable } from '@/lib/pixel-office/layout/furnitureCatalog' -import { createDefaultLayout, serializeLayout } from '@/lib/pixel-office/layout/layoutSerializer' +import { createDefaultLayout, migrateLayoutColors, serializeLayout } from '@/lib/pixel-office/layout/layoutSerializer' import { playDoneSound, unlockAudio, setSoundEnabled, isSoundEnabled } from '@/lib/pixel-office/notificationSound' import { loadCharacterPNGs, loadWallPNG } from '@/lib/pixel-office/sprites/pngLoader' import { useI18n } from '@/lib/i18n' @@ -139,7 +139,7 @@ export default function PixelOfficePage() { const agentStatsRef = useRef>(new Map()) const contributionsRef = useRef(null) const photographRef = useRef(null) - const gatewayRef = useRef<{ port: number; token?: string }>({ port: 18789 }) + const gatewayRef = useRef<{ port: number; token?: string; host?: string }>({ port: 18789 }) const providersRef = useRef; usedBy: Array<{ id: string; emoji: string; name: string }> }>>([]) const [isEditMode, setIsEditMode] = useState(cachedIsEditMode) const [soundOn, setSoundOn] = useState(true) @@ -180,8 +180,9 @@ export default function PixelOfficePage() { const res = await fetch('/api/pixel-office/layout') const data = await res.json() if (data.layout) { - officeRef.current = new OfficeState(data.layout) - savedLayoutRef.current = data.layout + const migrated = migrateLayoutColors(data.layout) + officeRef.current = new OfficeState(migrated) + savedLayoutRef.current = migrated } else { officeRef.current = new OfficeState() } @@ -477,7 +478,7 @@ export default function PixelOfficePage() { } } agentStatsRef.current = map - if (data.gateway) gatewayRef.current = { port: data.gateway.port || 18789, token: data.gateway.token } + if (data.gateway) gatewayRef.current = { port: data.gateway.port || 18789, token: data.gateway.token, host: data.gateway.host } if (data.providers) providersRef.current = data.providers } catch {} } @@ -715,8 +716,8 @@ export default function PixelOfficePage() { // Click on PC — open gateway chat for main agent const gw = gatewayRef.current const sessionKey = 'agent:main:main' - let chatUrl = buildGatewayUrl(gw.port, '/chat', { session: sessionKey }) - if (gw.token) chatUrl = buildGatewayUrl(gw.port, '/chat', { session: sessionKey, token: gw.token }) + let chatUrl = buildGatewayUrl(gw.port, '/chat', { session: sessionKey }, gw.host) + if (gw.token) chatUrl = buildGatewayUrl(gw.port, '/chat', { session: sessionKey, token: gw.token }, gw.host) window.open(chatUrl, '_blank') } else if (office.layout.furniture.some(f => { if (f.uid !== 'library-r') return false diff --git a/app/sessions/page.tsx b/app/sessions/page.tsx index fd900a6..db06b13 100644 --- a/app/sessions/page.tsx +++ b/app/sessions/page.tsx @@ -33,6 +33,7 @@ interface Session { interface GatewayInfo { port: number; token?: string; + host?: string; } const TYPE_EMOJI_COLOR: Record = { @@ -289,8 +290,8 @@ function SessionList({ agentId }: { agentId: string }) {
{sessions.map((s) => { const typeInfo = getTypeLabel(s.type); - let chatUrl = buildGatewayUrl(gateway.port, "/chat", { session: s.key }); - if (gateway.token) chatUrl = buildGatewayUrl(gateway.port, "/chat", { session: s.key, token: gateway.token }); + let chatUrl = buildGatewayUrl(gateway.port, "/chat", { session: s.key }, gateway.host); + if (gateway.token) chatUrl = buildGatewayUrl(gateway.port, "/chat", { session: s.key, token: gateway.token }, gateway.host); return (
): string { - const host = typeof window !== "undefined" ? window.location.hostname : "localhost"; - const url = new URL(`http://${host}:${port}${path}`); +export function buildGatewayUrl( + port: number, + path: string, + params?: Record, + hostOverride?: string, +): string { + const host = (hostOverride && hostOverride.trim()) || (typeof window !== "undefined" ? window.location.hostname : "localhost"); + const normalizedHost = host.includes("://") ? new URL(host).hostname : host; + const url = new URL(`http://${normalizedHost}:${port}${path}`); if (params) { for (const [k, v] of Object.entries(params)) { if (v) url.searchParams.set(k, v); diff --git a/lib/pixel-office/layout/layoutSerializer.ts b/lib/pixel-office/layout/layoutSerializer.ts index 4b02a95..10e900e 100644 --- a/lib/pixel-office/layout/layoutSerializer.ts +++ b/lib/pixel-office/layout/layoutSerializer.ts @@ -67,7 +67,6 @@ export function layoutToFurnitureInstances(furniture: PlacedFurniture[]): Furnit } } - // Colorize sprite if this furniture has a color override let sprite = entry.sprite if (item.color) { const { h, s, b: bv, c: cv } = item.color