feat: implement PI core chat timeline

This commit is contained in:
brother7 committed 2026-08-24 00:34:03 +08:00
1 parent 54d8bb2e4a
commit 612135f911
40 files changed
+3881 -119

No files matched your search

+63
View File
@@ -66,6 +66,8 @@ type HostApiProxyResponse = {
ok?: boolean;
json?: unknown;
text?: string;
bytes?: Uint8Array;
contentType?: string;
};
error?: { message?: string } | string;
// backward compatibility fields
@@ -80,6 +82,8 @@ type HostApiProxyData = {
ok?: boolean;
json?: unknown;
text?: string;
bytes?: Uint8Array;
contentType?: string;
transport?: 'dispatcher' | 'loopback';
};
@@ -297,6 +301,65 @@ export async function hostApiFetch<T>(path: string, init?: RequestInit): Promise
}
}
export interface HostApiBinaryData {
bytes: Uint8Array;
contentType: string;
}
export async function hostApiFetchBytes(path: string): Promise<HostApiBinaryData> {
const startedAt = Date.now();
try {
const response = await invokeIpc<HostApiProxyResponse>('hostapi:fetch', {
path,
method: 'GET',
headers: { Accept: 'application/octet-stream' },
body: null,
});
if (!response.ok) throw new Error(resolveProxyErrorMessage(response.error));
const data = response.data;
if (!data || data.ok === false || (typeof data.status === 'number' && data.status >= 400)) {
throw createProxyHttpError(data ?? {});
}
if (!(data.bytes instanceof Uint8Array) || !data.contentType) {
throw new Error('Host API binary response is unavailable');
}
trackUiEvent('hostapi.fetch', {
route: telemetryRoute(path),
method: 'GET',
source: 'ipc-proxy',
durationMs: Date.now() - startedAt,
status: data.status ?? 200,
});
return { bytes: data.bytes, contentType: data.contentType };
} catch (error) {
const normalized = normalizeAppError(error, { source: 'ipc-proxy', path, method: 'GET' });
if (!shouldFallbackToBrowser(normalized.message) || !allowLocalhostFallback()) {
throw normalized;
}
}
const [token, hostApiBase] = await Promise.all([getHostApiToken(), getHostApiBaseUrl()]);
const response = await fetch(`${hostApiBase}${path}`, {
headers: {
Accept: 'application/octet-stream',
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw normalizeAppError(new Error(`HTTP ${response.status}`), {
source: 'browser-fallback',
path,
method: 'GET',
status: response.status,
});
}
return {
bytes: new Uint8Array(await response.arrayBuffer()),
contentType: response.headers.get('content-type')?.split(';', 1)[0]?.trim()
|| 'application/octet-stream',
};
}
export function createHostEventSource(path = '/api/events'): EventSource {
// EventSource does not support custom headers, so pass the auth token
// as a query parameter. The server accepts both mechanisms.