Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
async function adminFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
const payload = (await response.json().catch(() => ({}))) as {
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: { code: string; message: string };
|
||||
};
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.error?.message ?? payload?.message ?? `请求失败 (${response.status})`);
|
||||
}
|
||||
return (payload.data !== undefined ? payload.data : payload) as T;
|
||||
}
|
||||
|
||||
export type AdminUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
slug: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
role: string;
|
||||
status: string;
|
||||
balanceCents: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type AdminSummary = {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
totalBalanceCents: number;
|
||||
llm?: {
|
||||
keyCount: number;
|
||||
selectedKeyName: string | null;
|
||||
globalModel: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type LlmKey = {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
model?: string;
|
||||
models?: string[];
|
||||
isSelected: boolean;
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
export type LedgerEntry = {
|
||||
id: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
type: string;
|
||||
amountCents: number;
|
||||
note: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type UsageRecord = {
|
||||
id: string;
|
||||
userId: string;
|
||||
username?: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
costCents: number;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
// ─── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchAdminSummary() {
|
||||
return adminFetch<{ summary: AdminSummary }>('/admin-api/summary');
|
||||
}
|
||||
|
||||
// ─── Users ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchAdminUsers(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
role?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
const q = new URLSearchParams();
|
||||
if (params.page) q.set('page', String(params.page));
|
||||
if (params.pageSize) q.set('pageSize', String(params.pageSize));
|
||||
if (params.search) q.set('search', params.search);
|
||||
if (params.role) q.set('role', params.role);
|
||||
if (params.status) q.set('status', params.status);
|
||||
return adminFetch<{ users: AdminUser[]; total: number; page: number; pageSize: number; totalPages: number }>(
|
||||
`/admin-api/users?${q}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createAdminUser(body: {
|
||||
username: string;
|
||||
password: string;
|
||||
displayName?: string;
|
||||
role?: string;
|
||||
}) {
|
||||
return adminFetch<{ user: AdminUser }>('/admin-api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export async function patchAdminUser(
|
||||
userId: string,
|
||||
patch: { role?: string; status?: string; displayName?: string; password?: string },
|
||||
) {
|
||||
return adminFetch<{ user: AdminUser }>(`/admin-api/users/${userId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
export async function rechargeUser(userId: string, amountCents: number, note = '') {
|
||||
return adminFetch<{ user: AdminUser }>(`/admin-api/users/${userId}/recharge`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ amountCents, note }),
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Billing ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchAdminLedger(params: { page?: number; pageSize?: number } = {}) {
|
||||
const q = new URLSearchParams();
|
||||
if (params.page) q.set('page', String(params.page));
|
||||
if (params.pageSize) q.set('pageSize', String(params.pageSize));
|
||||
return adminFetch<{ entries: LedgerEntry[]; total: number; page: number; totalPages: number }>(
|
||||
`/admin-api/ledger?${q}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAdminUsage(params: { page?: number; pageSize?: number } = {}) {
|
||||
const q = new URLSearchParams();
|
||||
if (params.page) q.set('page', String(params.page));
|
||||
if (params.pageSize) q.set('pageSize', String(params.pageSize));
|
||||
return adminFetch<{ records: UsageRecord[]; total: number; page: number; totalPages: number }>(
|
||||
`/admin-api/usage?${q}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── LLM Providers ───────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchLlmKeys() {
|
||||
return adminFetch<{ keys: LlmKey[] }>('/admin-api/llm-providers/keys');
|
||||
}
|
||||
|
||||
export async function createLlmKey(body: {
|
||||
name: string;
|
||||
provider: string;
|
||||
apiKey: string;
|
||||
model?: string;
|
||||
models?: string;
|
||||
baseUrl?: string;
|
||||
}) {
|
||||
return adminFetch<{ key: LlmKey }>('/admin-api/llm-providers/keys', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
export async function patchLlmKey(keyId: string, patch: { name?: string; apiKey?: string; model?: string }) {
|
||||
return adminFetch<{ key: LlmKey }>(`/admin-api/llm-providers/keys/${keyId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
export async function selectLlmKey(keyId: string) {
|
||||
return adminFetch(`/admin-api/llm-providers/keys/${keyId}/select`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export async function deleteLlmKey(keyId: string) {
|
||||
return adminFetch(`/admin-api/llm-providers/keys/${keyId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function testLlmKey(keyId: string) {
|
||||
return adminFetch<{ ok: boolean; model?: string; error?: string }>(
|
||||
`/admin-api/llm-providers/keys/${keyId}/test`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchLlmGlobal() {
|
||||
return adminFetch<{ settings: { model: string | null } }>('/admin-api/llm-providers/global');
|
||||
}
|
||||
|
||||
export async function putLlmGlobal(model: string) {
|
||||
return adminFetch('/admin-api/llm-providers/global', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ model }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function syncLlmProviders() {
|
||||
return adminFetch<{ synced: number }>('/admin-api/llm-providers/sync', { method: 'POST' });
|
||||
}
|
||||
Reference in New Issue
Block a user