feat: open source multi-user AI notebook with observability
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
export type Fragment = {
|
||||
id: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
label: string;
|
||||
role: "admin" | "member";
|
||||
debug_sharing: boolean;
|
||||
};
|
||||
|
||||
export type Snapshot = {
|
||||
maturity_ai: number;
|
||||
motion: string;
|
||||
position: string;
|
||||
tension: string;
|
||||
trajectory: string;
|
||||
possible_moves: string[];
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type Idea = {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
maturity_ai: number;
|
||||
maturity_override: number | null;
|
||||
maturity: number;
|
||||
is_overridden: boolean;
|
||||
confidence: number;
|
||||
motion: string;
|
||||
position: string;
|
||||
tension: string;
|
||||
trajectory: string;
|
||||
possible_moves: string[];
|
||||
fragment_count: number;
|
||||
latest_fragment_at: string | null;
|
||||
updated_at: string;
|
||||
fragments?: Fragment[];
|
||||
snapshots?: Snapshot[];
|
||||
};
|
||||
|
||||
export type AdminOverview = {
|
||||
totals: {
|
||||
users: number;
|
||||
fragments: number;
|
||||
ideas: number;
|
||||
pending: number;
|
||||
audit_events: number;
|
||||
};
|
||||
last_24h: {
|
||||
runs: number;
|
||||
successes: number;
|
||||
errors: number;
|
||||
success_rate: number;
|
||||
avg_duration_ms: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
reasoning_tokens: number;
|
||||
cached_tokens: number;
|
||||
model_rounds: number;
|
||||
tool_calls: number;
|
||||
};
|
||||
runtime: {
|
||||
uptime_seconds: number;
|
||||
curator_configured: boolean;
|
||||
queue_depth: number;
|
||||
model: string;
|
||||
prompt_version: string;
|
||||
};
|
||||
daily: Array<{
|
||||
day: string;
|
||||
runs: number;
|
||||
successes: number;
|
||||
errors: number;
|
||||
reasoning_tokens: number;
|
||||
}>;
|
||||
recent_errors: Array<{
|
||||
id: string;
|
||||
started_at: string;
|
||||
error_type: string;
|
||||
error_message: string;
|
||||
user_label: string;
|
||||
}>;
|
||||
audit_24h: {
|
||||
events: number;
|
||||
failed_logins: number;
|
||||
};
|
||||
narrative: string;
|
||||
};
|
||||
|
||||
export type AgentRun = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
fragment_id: string;
|
||||
status: "running" | "success" | "error";
|
||||
prompt_version: string;
|
||||
model: string;
|
||||
thinking_effort: string;
|
||||
started_at: string;
|
||||
finished_at: string | null;
|
||||
duration_ms: number | null;
|
||||
attempt_count: number;
|
||||
model_rounds: number;
|
||||
tool_calls: number;
|
||||
search_calls: number;
|
||||
inspection_calls: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
reasoning_tokens: number;
|
||||
cached_tokens: number;
|
||||
error_type: string | null;
|
||||
error_message: string | null;
|
||||
user_label: string;
|
||||
user_role: string;
|
||||
debug_sharing: boolean;
|
||||
content_length: number;
|
||||
};
|
||||
|
||||
export type AgentEvent = {
|
||||
id: string;
|
||||
event_type: string;
|
||||
event_at: string;
|
||||
duration_ms: number | null;
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type AgentRunDetail = {
|
||||
run: AgentRun & { content_sha256: string };
|
||||
fragment:
|
||||
| { content_visible: true; content: string }
|
||||
| { content_visible: false; content_length: number; content_sha256: string };
|
||||
events: AgentEvent[];
|
||||
privacy: {
|
||||
content_visible: boolean;
|
||||
reasoning_content_stored: boolean;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AdminUser = User & {
|
||||
created_at: string;
|
||||
fragment_count: number;
|
||||
idea_count: number;
|
||||
};
|
||||
|
||||
export type AuditEvent = {
|
||||
id: string;
|
||||
event_type: string;
|
||||
created_at: string;
|
||||
user_label: string | null;
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ApiOptions = RequestInit & { body?: string };
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: ApiOptions = {}): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
...options,
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Note-Client": "xuxiang-web",
|
||||
...(options.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
let message = "请求没有完成";
|
||||
try {
|
||||
const body = await response.json();
|
||||
if (typeof body.detail === "string") message = body.detail;
|
||||
} catch {
|
||||
// Keep the calm fallback message.
|
||||
}
|
||||
throw new ApiError(response.status, message);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
session: () =>
|
||||
request<{ authenticated: boolean; user: User }>("/api/session"),
|
||||
login: (accessKey: string) =>
|
||||
request<{ authenticated: boolean; user: User }>("/api/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ access_key: accessKey }),
|
||||
}),
|
||||
logout: () =>
|
||||
request<{ authenticated: boolean }>("/api/logout", { method: "POST" }),
|
||||
fragments: () => request<{ items: Fragment[] }>("/api/fragments"),
|
||||
createFragment: (content: string) =>
|
||||
request<Fragment>("/api/fragments", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
ideas: () => request<{ items: Idea[] }>("/api/ideas"),
|
||||
idea: (id: string) => request<Idea>(`/api/ideas/${id}`),
|
||||
updateMaturity: (id: string, maturity: number | null) =>
|
||||
request<Idea>(`/api/ideas/${id}/position`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ maturity }),
|
||||
}),
|
||||
status: () =>
|
||||
request<{ pending: number; curator_configured: boolean }>(
|
||||
"/api/system/status",
|
||||
),
|
||||
updateDebugSharing: (enabled: boolean) =>
|
||||
request<User>("/api/account/debug-sharing", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ enabled }),
|
||||
}),
|
||||
adminOverview: () => request<AdminOverview>("/api/admin/overview"),
|
||||
adminUsers: () => request<{ items: AdminUser[] }>("/api/admin/users"),
|
||||
adminRuns: (limit = 80) =>
|
||||
request<{ items: AgentRun[] }>(`/api/admin/runs?limit=${limit}`),
|
||||
adminRun: (id: string) =>
|
||||
request<AgentRunDetail>(`/api/admin/runs/${id}`),
|
||||
adminAudit: (limit = 80) =>
|
||||
request<{ items: AuditEvent[] }>(`/api/admin/audit?limit=${limit}`),
|
||||
};
|
||||
Reference in New Issue
Block a user