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:
1 parent
4c48e94659
commit
f43eb17030
26 files changed
+2253
-4
No files matched your search
+123
-2
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user