Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

247 lines
7.1 KiB
TypeScript

import { apiFetch, apiUrl, wsUrl } from "@/lib/api";
import type {
Book,
BookDetail,
BookProposal,
Page,
Spine,
Block,
} from "@/lib/book-types";
const BASE = "/api/v1/book";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await apiFetch(apiUrl(`${BASE}${path}`), {
headers: { "Content-Type": "application/json", ...(init?.headers || {}) },
...init,
});
if (!res.ok) {
let detail: string;
try {
const data = await res.json();
detail = (data && (data.detail || data.message)) || res.statusText;
} catch {
detail = res.statusText;
}
throw new Error(`book api ${path}${res.status}: ${detail}`);
}
return (await res.json()) as T;
}
export interface CreateBookPayload {
user_intent: string;
chat_session_id?: string;
chat_selections?: Array<{ session_id: string; message_ids: number[] }>;
notebook_refs?: Array<Record<string, unknown>>;
knowledge_bases?: string[];
question_categories?: number[];
question_entries?: number[];
language?: string;
}
export const bookApi = {
list: () => request<{ books: Book[] }>("/books"),
get: (book_id: string) =>
request<BookDetail>(`/books/${encodeURIComponent(book_id)}`),
delete: (book_id: string) =>
request<{ deleted: boolean; book_id: string }>(
`/books/${encodeURIComponent(book_id)}`,
{ method: "DELETE" },
),
getSpine: (book_id: string) =>
request<{ spine: Spine }>(`/books/${encodeURIComponent(book_id)}/spine`),
getPage: (book_id: string, page_id: string) =>
request<{ page: Page }>(
`/books/${encodeURIComponent(book_id)}/pages/${encodeURIComponent(page_id)}`,
),
create: (payload: CreateBookPayload) =>
request<{ book: Book; proposal: BookProposal }>("/books", {
method: "POST",
body: JSON.stringify(payload),
}),
confirmProposal: (book_id: string, proposal?: BookProposal) =>
request<{ book: Book; spine: Spine }>("/books/confirm-proposal", {
method: "POST",
body: JSON.stringify({ book_id, proposal: proposal ?? null }),
}),
confirmSpine: (book_id: string, spine?: Spine, auto_compile = true) =>
request<{ pages: Page[] }>("/books/confirm-spine", {
method: "POST",
body: JSON.stringify({ book_id, spine: spine ?? null, auto_compile }),
}),
compilePage: (book_id: string, page_id: string, force = false) =>
request<{ page: Page }>("/books/compile-page", {
method: "POST",
body: JSON.stringify({ book_id, page_id, force }),
}),
regenerateBlock: (
book_id: string,
page_id: string,
block_id: string,
params_override?: Record<string, unknown>,
) =>
request<{ block: Block | null }>("/books/regenerate-block", {
method: "POST",
body: JSON.stringify({
book_id,
page_id,
block_id,
params_override: params_override ?? null,
}),
}),
insertBlock: (params: {
book_id: string;
page_id: string;
block_type: string;
params?: Record<string, unknown>;
position?: number;
compile_now?: boolean;
}) =>
request<{ block: Block }>("/books/insert-block", {
method: "POST",
body: JSON.stringify({
compile_now: true,
...params,
}),
}),
deleteBlock: (book_id: string, page_id: string, block_id: string) =>
request<{ ok: boolean }>("/books/delete-block", {
method: "POST",
body: JSON.stringify({ book_id, page_id, block_id }),
}),
moveBlock: (
book_id: string,
page_id: string,
block_id: string,
new_position: number,
) =>
request<{ ok: boolean }>("/books/move-block", {
method: "POST",
body: JSON.stringify({ book_id, page_id, block_id, new_position }),
}),
changeBlockType: (params: {
book_id: string;
page_id: string;
block_id: string;
new_type: string;
params_override?: Record<string, unknown>;
}) =>
request<{ block: Block }>("/books/change-block-type", {
method: "POST",
body: JSON.stringify(params),
}),
deepDive: (params: {
book_id: string;
parent_page_id: string;
topic: string;
block_id?: string;
content_type?: string;
}) =>
request<{ page: Page }>("/books/deep-dive", {
method: "POST",
body: JSON.stringify({ content_type: "concept", ...params }),
}),
recordQuizAttempt: (params: {
book_id: string;
page_id: string;
block_id: string;
question_id?: string;
user_answer?: string;
is_correct: boolean;
}) =>
request<{ progress: Record<string, unknown> }>("/books/quiz-attempt", {
method: "POST",
body: JSON.stringify(params),
}),
supplement: (book_id: string, page_id: string, topic: string) =>
request<{ block: Block }>("/books/supplement", {
method: "POST",
body: JSON.stringify({ book_id, page_id, topic }),
}),
setPageChatSession: (book_id: string, page_id: string, session_id: string) =>
request<{ book: Book }>("/books/page-chat-session", {
method: "POST",
body: JSON.stringify({ book_id, page_id, session_id }),
}),
rebuild: (book_id: string, auto_compile = true) =>
request<{ pages: Page[] }>("/books/rebuild", {
method: "POST",
body: JSON.stringify({ book_id, auto_compile }),
}),
health: (book_id: string) =>
request<{
kb_drift: {
book_id: string;
has_drift: boolean;
new_kbs?: string[];
removed_kbs?: string[];
changed_kbs?: string[];
stale_page_ids?: string[];
};
log_health: {
book_id: string;
total_entries: number;
error_entries: number;
block_failures: number;
last_compile_at?: string;
last_error_at?: string;
repeated_failures?: { signature: string; count: number }[];
};
}>(`/books/${encodeURIComponent(book_id)}/health`),
refreshFingerprints: (book_id: string) =>
request<{
book_id: string;
kb_fingerprints: Record<string, string>;
stale_page_ids: string[];
}>(`/books/${encodeURIComponent(book_id)}/refresh-fingerprints`, {
method: "POST",
}),
};
export interface LegacyChatSession {
session_id: string;
messages?: Array<{ role: string; content: string }>;
}
export async function getLegacyChatSession(
session_id: string,
): Promise<LegacyChatSession | null> {
const res = await apiFetch(
apiUrl(`/api/v1/chat/sessions/${encodeURIComponent(session_id)}`),
);
if (res.status === 404) return null;
if (!res.ok) throw new Error(`chat session ${session_id}${res.status}`);
return (await res.json()) as LegacyChatSession;
}
// ── WebSocket helper ─────────────────────────────────────────────────
export type BookWsEvent = { type: string; [key: string]: unknown };
export function openBookSocket(
onEvent: (event: BookWsEvent) => void,
onError?: (error: Event) => void,
): WebSocket {
const socket = new WebSocket(wsUrl(`${BASE}/ws`));
socket.onmessage = (event) => {
try {
onEvent(JSON.parse(event.data));
} catch {
// ignore malformed frames
}
};
if (onError) socket.onerror = onError;
return socket;
}