popiart-server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

436 lines
9.3 KiB

import { cookies } from "next/headers";
export const SESSION_COOKIE_NAME = "popiart_session";
type ApiEnvelope<T> = {
ok: boolean;
data?: T;
error?: {
code?: string;
message?: string;
details?: unknown;
};
};
export type PopiartUser = {
id: string;
email: string;
name: string;
scopes?: string[];
};
export type LoginResponse = {
key: string;
token: string;
user: PopiartUser;
};
export type AuthSessionView = {
user: PopiartUser;
session_key?: string;
upstream_key_masked?: string;
gateway_user_id?: number;
gateway_access_token_masked?: string;
gateway_bound: boolean;
};
export type Skill = {
id: string;
name: string;
description: string;
tags: string[];
version: string;
model_type: string;
route_key?: string;
estimated_duration_s: number;
};
export type SkillListResponse = {
items: Skill[];
total: number;
limit: number;
offset: number;
};
export type BudgetSummary = {
period: {
start: string;
end: string;
};
used: {
tokens: number;
cost_usd: number;
};
limit: {
monthly_tokens: number;
monthly_cost_usd: number;
};
remaining: {
tokens: number;
cost_usd: number;
};
};
export type BudgetUsageRow = {
dimension: string;
tokens_used: number;
cost_usd: number;
job_count: number;
};
export type BudgetUsage = {
rows: BudgetUsageRow[];
total: {
tokens_used: number;
cost_usd: number;
job_count: number;
};
};
export type Project = {
id: string;
name: string;
};
export type ProjectListResponse = {
items: Project[];
total: number;
limit: number;
offset: number;
};
export type BillingPlan = {
id: string;
product_type: string;
badge: string;
name: string;
price: string;
cadence: string;
summary: string;
features: string[];
cta: string;
highlight?: boolean;
};
export type BillingCatalog = {
plans: BillingPlan[];
capabilities: {
subscription_status: boolean;
credit_balance: boolean;
invoices: boolean;
checkout: boolean;
};
source: string;
note?: string;
};
export type BillingSubscription = {
billing_preference: string;
subscriptions: Array<{
subscription?: {
id?: number;
status?: string;
amount_total?: number;
amount_used?: number;
start_time?: number;
end_time?: number;
next_reset_time?: number;
member_level?: string;
};
}>;
all_subscriptions: Array<Record<string, unknown>>;
subscription_points: Record<
string,
{
available_points?: number;
total_points?: number;
used_points?: number;
}
>;
};
export type BillingCredits = {
wallets: Array<{
id: number;
source_type: string;
points: number;
points_total: number;
expire_at: number;
status: string;
priority: number;
origin_currency: string;
origin_amount: number;
}>;
balance: number;
};
export type BillingSubscriptionPlan = {
plan: {
id: number;
title: string;
subtitle: string;
description: string;
price_amount: number;
currency: string;
price_amount_cny?: number;
price_amount_usd?: number;
points_amount: number;
duration_unit: string;
duration_value: number;
recommended?: boolean;
member_level?: string;
total_amount?: number;
quota_reset_period?: string;
};
};
export type BillingSubscriptionPlans = {
items: BillingSubscriptionPlan[];
};
export type BillingPointsPackage = {
id: number;
name: string;
currency: string;
price_amount: number;
points_amount: number;
bonus_points: number;
enabled: boolean;
};
export type BillingPointsPackages = {
items: BillingPointsPackage[];
};
export type BillingCheckoutResult = {
message?: string;
trade_no?: string;
url?: string;
code_url?: string;
data?: Record<string, unknown>;
};
export type BillingOrderPage = {
page: number;
page_size: number;
total: number;
items: Array<Record<string, unknown>>;
};
export type BillingInvoices = {
subscription_orders: BillingOrderPage;
point_orders: BillingOrderPage;
};
export type GatewayBindResponse = {
gateway_user_id: number;
gateway_username?: string;
gateway_display_name?: string;
gateway_email?: string;
gateway_bound: boolean;
};
export class PopiartApiError extends Error {
status: number;
code?: string;
details?: unknown;
constructor(status: number, message: string, code?: string, details?: unknown) {
super(message);
this.name = "PopiartApiError";
this.status = status;
this.code = code;
this.details = details;
}
}
function stripTrailingSlash(value: string) {
return value.replace(/\/+$/, "");
}
export function getPopiartEndpoint() {
const configured =
process.env.POPIART_ENDPOINT ??
process.env.POPIART_SERVER_URL ??
"http://127.0.0.1:8080/v1";
return stripTrailingSlash(configured);
}
export function buildPopiartUrl(pathname: string) {
const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
return `${getPopiartEndpoint()}${path}`;
}
async function readEnvelope<T>(response: Response) {
try {
return (await response.json()) as ApiEnvelope<T>;
} catch {
return null;
}
}
export async function popiartFetchEnvelope<T>(pathname: string, init: RequestInit = {}) {
const response = await fetch(buildPopiartUrl(pathname), {
...init,
cache: "no-store",
});
const payload = await readEnvelope<T>(response);
return { response, payload };
}
export async function popiartRequest<T>(
pathname: string,
options: {
method?: string;
body?: BodyInit | object;
headers?: HeadersInit;
token?: string;
} = {},
) {
const headers = new Headers(options.headers);
let body: BodyInit | undefined;
if (options.token) {
headers.set("Authorization", `Bearer ${options.token}`);
}
if (options.body instanceof FormData || typeof options.body === "string") {
body = options.body;
} else if (options.body !== undefined) {
headers.set("Content-Type", "application/json");
body = JSON.stringify(options.body);
}
const { response, payload } = await popiartFetchEnvelope<T>(pathname, {
method: options.method ?? (body ? "POST" : "GET"),
headers,
body,
});
if (!response.ok || !payload?.ok) {
throw new PopiartApiError(
response.status,
payload?.error?.message ?? `Request failed with status ${response.status}`,
payload?.error?.code,
payload?.error?.details,
);
}
return payload.data as T;
}
export async function getSessionToken() {
const cookieStore = await cookies();
return cookieStore.get(SESSION_COOKIE_NAME)?.value ?? null;
}
export async function getViewerSession() {
const token = await getSessionToken();
if (!token) {
return null;
}
try {
return await popiartRequest<AuthSessionView>("/auth/me", { token });
} catch (error) {
if (error instanceof PopiartApiError && error.status === 401) {
return null;
}
throw error;
}
}
export async function getSkillsCatalog(search?: string) {
const token = await getSessionToken();
if (!token) {
return null;
}
const query = new URLSearchParams();
query.set("limit", "100");
if (search) {
query.set("search", search);
}
return popiartRequest<SkillListResponse>(`/skills?${query.toString()}`, { token });
}
export async function getBudgetSummary() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BudgetSummary>("/budget", { token });
}
export async function getBudgetUsage() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BudgetUsage>("/budget/usage", { token });
}
export async function getProjects() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<ProjectListResponse>("/projects", { token });
}
export async function getBillingCatalog() {
return popiartRequest<BillingCatalog>("/billing/catalog");
}
export async function getBillingSubscription() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BillingSubscription>("/billing/subscription", { token });
}
export async function getBillingCredits() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BillingCredits>("/billing/credits", { token });
}
export async function getBillingSubscriptionPlans() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BillingSubscriptionPlans>("/billing/subscription/plans", { token });
}
export async function getBillingPointsPackages() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BillingPointsPackages>("/billing/points/packages", { token });
}
export async function getBillingInvoices(options?: {
keyword?: string;
page?: number;
pageSize?: number;
}) {
const token = await getSessionToken();
if (!token) {
return null;
}
const query = new URLSearchParams();
if (options?.keyword) {
query.set("keyword", options.keyword);
}
if (options?.page && options.page > 0) {
query.set("p", String(options.page));
}
if (options?.pageSize && options.pageSize > 0) {
query.set("page_size", String(options.pageSize));
}
const suffix = query.toString();
return popiartRequest<BillingInvoices>(`/billing/invoices${suffix ? `?${suffix}` : ""}`, { token });
}