feat(frontend/admin): M10 后台管理面板(/admin hash 路由 + 15 大模块 + RBAC(owner/admin/viewer) + 审计 + Miuix);Docs/api-client.md v0.3 契约补全;tsc+vite build 通过

This commit is contained in:
Aska committed 2026-09-06 07:55:04 +08:00
1 parent 4c48e94659
commit f43eb17030
26 files changed
+2253 -4

No files matched your search

+123 -2
View File
@@ -1,8 +1,16 @@
import type {
AdminAutomationRecord,
AdminChannelRecord,
AdminIoTDevice,
AdminLlmConfig,
AdminPluginRecord,
AdminUserRecord,
ApiErrorShape,
ChatSyncResult,
LoginResult,
MemoryRecord,
PersonaRecord,
Role,
SessionRecord,
} from "./types";
@@ -25,13 +33,58 @@ export class ApiError extends Error {
const API_BASE = "/api/v1";
const TOKEN_KEY = "cyrene.jwt";
const ROLE_KEY = "cyrene.role";
const API_KEY_KEY = "cyrene.apikey";
export function getAuthToken(): string | null {
return localStorage.getItem(TOKEN_KEY);
}
export function setAuthToken(token: string | null): void {
if (token) localStorage.setItem(TOKEN_KEY, token);
else localStorage.removeItem(TOKEN_KEY);
if (token) {
localStorage.setItem(TOKEN_KEY, token);
setStoredRole(decodeJwtRole(token));
} else {
localStorage.removeItem(TOKEN_KEY);
setStoredRole(null);
}
}
/** Decode the RBAC role from a JWT payload (client-side only; never verified).
* base64url is converted to base64 before decoding to survive `-`/`_`. */
export function decodeJwtRole(token: string): Role | null {
const parts = token.split(".");
if (parts.length < 2) return null;
try {
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
const payload = JSON.parse(atob(padded)) as Record<string, unknown>;
const r = payload.role;
return r === "owner" || r === "admin" || r === "viewer" ? r : null;
} catch {
return null;
}
}
/** Persisted RBAC role, refreshed when the token changes. Falls back to
* decoding the token so a session created before role persistence still works. */
export function getStoredRole(): Role | null {
const r = localStorage.getItem(ROLE_KEY);
if (r === "owner" || r === "admin" || r === "viewer") return r;
const token = getAuthToken();
return token ? decodeJwtRole(token) : null;
}
export function setStoredRole(role: Role | null): void {
if (role) localStorage.setItem(ROLE_KEY, role);
else localStorage.removeItem(ROLE_KEY);
}
/** Optional API key for admin endpoints (X-API-Key). */
export function getApiKey(): string | null {
return localStorage.getItem(API_KEY_KEY);
}
export function setApiKey(key: string | null): void {
if (key) localStorage.setItem(API_KEY_KEY, key);
else localStorage.removeItem(API_KEY_KEY);
}
interface Envelope {
@@ -48,6 +101,8 @@ async function request<T>(
headers.set("X-API-Version", "1");
const token = getAuthToken();
if (token) headers.set("Authorization", `Bearer ${token}`);
const apiKey = getApiKey();
if (apiKey) headers.set("X-API-Key", apiKey);
let res: Response;
try {
@@ -115,3 +170,69 @@ export function fetchSessions(): Promise<SessionRecord[]> {
export function fetchPersonas(): Promise<PersonaRecord[]> {
return request<PersonaRecord[]>("/personas");
}
/** Is this error a "backend has not implemented it yet" case (404/501/403 on a
* not-yet-wired route) rather than a real connectivity problem? */
export function isNotImplemented(err: unknown): boolean {
if (err instanceof ApiError) {
return err.status === 404 || err.status === 501 || err.code === "not_found";
}
return false;
}
/* ----------------------------------------------------------------------------
* Admin API (/api/v1/admin/*). Reference: Docs/api-client.md §2 + design/10.
* Implemented endpoints return real data; the rest are contract-shaped and are
* expected to 404 until the backend wires them — callers show a graceful shell.
* -------------------------------------------------------------------------- */
/** GET /api/v1/admin/personas */
export function adminListPersonas(): Promise<PersonaRecord[]> {
return request<PersonaRecord[]>("/admin/personas");
}
/** GET /api/v1/admin/sessions */
export function adminListSessions(): Promise<SessionRecord[]> {
return request<SessionRecord[]>("/admin/sessions");
}
/** GET /api/v1/admin/memories */
export function adminListMemories(): Promise<MemoryRecord[]> {
return request<MemoryRecord[]>("/admin/memories");
}
/** GET /api/v1/admin/plugins */
export function adminListPlugins(): Promise<AdminPluginRecord[]> {
return request<AdminPluginRecord[]>("/admin/plugins");
}
/** POST /api/v1/admin/plugins/policy — owner-only, audited. */
export function adminSetPluginPolicy(
name: string,
allowInGroup: boolean,
allowInStranger: boolean
): Promise<AdminPluginRecord> {
return request<AdminPluginRecord>("/admin/plugins/policy", {
method: "POST",
body: JSON.stringify({ name, allowInGroup, allowInStranger }),
});
}
/** GET /api/v1/admin/channels */
export function adminListChannels(): Promise<AdminChannelRecord[]> {
return request<AdminChannelRecord[]>("/admin/channels");
}
/** GET /api/v1/admin/iot */
export function adminListIoT(): Promise<AdminIoTDevice[]> {
return request<AdminIoTDevice[]>("/admin/iot");
}
// --- Planned M10 endpoints (contract-shaped, backend may 404). ---
/** GET /api/v1/admin/users */
export function adminListUsers(): Promise<AdminUserRecord[]> {
return request<AdminUserRecord[]>("/admin/users");
}
/** GET /api/v1/admin/llm */
export function adminGetLlm(): Promise<AdminLlmConfig> {
return request<AdminLlmConfig>("/admin/llm");
}
/** GET /api/v1/admin/automations */
export function adminListAutomations(): Promise<AdminAutomationRecord[]> {
return request<AdminAutomationRecord[]>("/admin/automations");
}
+98
View File
@@ -160,3 +160,101 @@ export interface ApiErrorShape {
code?: string;
message?: string;
}
/** RBAC roles for the admin panel (owner > admin > viewer). */
export type Role = "owner" | "admin" | "viewer";
/** A memory entry as returned by GET /api/v1/admin/memories. */
export interface MemoryRecord {
id: string;
scope: string;
text: string;
}
/** A capability/plugin as returned by GET /api/v1/admin/plugins. */
export interface AdminPluginRecord {
name: string;
kind: string;
description: string;
allowInGroup: boolean;
allowInStranger: boolean;
}
/** A channel adapter as returned by GET /api/v1/admin/channels. */
export interface AdminChannelRecord {
name: string;
connected: boolean;
}
/** An IoT device as returned by GET /api/v1/admin/iot. */
export interface AdminIoTDevice {
id: string;
name: string;
type: string;
state: Record<string, unknown>;
online: boolean;
}
/** A backend user / API key record (GET /api/v1/admin/users, planned M10). */
export interface AdminUserRecord {
id: string;
username: string;
role: string;
createdAt?: string;
}
/** LLM provider/model configuration (GET/PUT /api/v1/admin/llm, planned M10). */
export interface AdminLlmProvider {
id: string;
name: string;
baseUrl: string;
model: string;
multimodal: boolean;
enabled: boolean;
fallback?: string;
}
export interface AdminLlmConfig {
providers: AdminLlmProvider[];
}
/** An automation / reminder entry (GET/POST /api/v1/admin/automations, planned). */
export interface AdminAutomationRecord {
id: string;
name: string;
kind: string;
cron?: string;
enabled: boolean;
lastRun?: string;
nextRun?: string;
}
/** A structured log line (GET /api/v1/admin/logs, planned). */
export interface AdminLogRecord {
ts: string;
level: string;
source: string;
msg: string;
}
/** One audit entry: who / what / when / result. */
export interface AuditRecord {
id: string;
who: string;
what: string;
when: string;
result: string;
}
/** The owner profile (mirrors configs/users/owner.yaml). */
export interface OwnerProfile {
id: string;
displayName: string;
defaultPersona: string;
identityFile: string;
notes: string;
accounts: OwnerAccount[];
}
export interface OwnerAccount {
platform: string;
accountId: string;
}