import { EMPTY_CONSOLE_DATA, emptyResource, type ApiResource, type AuthorizationGrant, type AIOperationSummary, type ConsoleApiData, type ConsoleDevice, type ConsoleDeviceGroup, type ConsoleOverview, type ConsoleRelease, type EngineSummary, type IntegrationApplication, type IntegrationUsage, type OverviewMetric, type SecurityModule, type SecurityModuleState, } from "../types/console"; type RecordValue = Record; const isRecord = (value: unknown): value is RecordValue => typeof value === "object" && value !== null && !Array.isArray(value); const text = (value: unknown): string | undefined => { if (typeof value === "string" && value.trim()) return value; if (typeof value === "number") return String(value); return undefined; }; const boolean = (value: unknown): boolean | undefined => { if (typeof value === "boolean") return value; if (["true", "1", "online", "connected", "running", "ready"].includes(String(value).toLowerCase())) return true; if (["false", "0", "offline", "disconnected", "stopped"].includes(String(value).toLowerCase())) return false; return undefined; }; const number = (value: unknown): number | undefined => { if (value === null || value === undefined || value === "") return undefined; const parsed = typeof value === "number" ? value : Number(value); return Number.isFinite(parsed) ? parsed : undefined; }; const unwrap = (payload: unknown): unknown => { if (!isRecord(payload)) return payload; return payload.data ?? payload; }; const errorMessage = (payload: unknown, fallback: string) => { if (isRecord(payload)) { const detail = payload.detail; if (isRecord(detail)) return text(detail.message) || text(payload.message) || fallback; return text(payload.message) || text(detail) || fallback; } return fallback; }; const sourceTime = (value: unknown): string | undefined => { if (!isRecord(value)) return undefined; return text(value.sampled_at) || text(value.sampledAt) || text(value.server_time) || text(value.serverTime) || text(value.updated_at) || text(value.updatedAt); }; async function readJson(path: string): Promise<{ ok: true; payload: unknown; status: number } | { ok: false; status: number; message: string }> { try { const response = await fetch(path, { method: "GET", credentials: "same-origin", headers: { Accept: "application/json" } }); const payload = await response.json().catch(() => ({})); if (!response.ok) return { ok: false, status: response.status, message: errorMessage(payload, `读取接口失败(${response.status})`) }; return { ok: true, payload, status: response.status }; } catch (error) { return { ok: false, status: 0, message: error instanceof Error ? error.message : "读取接口失败" }; } } async function writeJson(path: string, method: "POST" | "PUT" | "DELETE", body?: unknown): Promise { const response = await fetch(path, { method, credentials: "same-origin", headers: { Accept: "application/json", "Content-Type": "application/json", "X-Actor-Id": "console" }, body: body === undefined ? undefined : JSON.stringify(body) }); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(errorMessage(payload, `操作失败(${response.status})`)); return unwrap(payload); } export async function readSecurityModules(): Promise { const result = await readJson("/api/v3/security/modules"); if (!result.ok) throw new Error(result.message); const value = unwrap(result.payload); if (!isRecord(value) || !Array.isArray(value.modules)) throw new Error("安全模块接口返回格式错误"); return value as unknown as SecurityModuleState; } export async function setSecurityGlobal(enabled: boolean): Promise { return await writeJson("/api/v3/security/modules/global", "PUT", { enabled }) as SecurityModuleState; } export async function updateSecurityModule(id: string, body: { enabled?: boolean; name?: string; description?: string; config?: Record }): Promise { return await writeJson(`/api/v3/security/modules/${encodeURIComponent(id)}`, "PUT", body) as SecurityModule; } export async function createSecurityModule(body: { id: string; name: string; description: string; runtime_binding: string; config: Record }): Promise { return await writeJson("/api/v3/security/modules", "POST", body) as SecurityModule; } export async function removeSecurityModule(id: string): Promise { await writeJson(`/api/v3/security/modules/${encodeURIComponent(id)}`, "DELETE"); } const resource = (source: string, result: Awaited>, data?: T, sourceAt?: string): ApiResource => { if (result.ok) return { state: "ready", data, source, sourceAt, status: result.status }; return { state: result.status === 401 || result.status === 403 ? "forbidden" : "unavailable", source, status: result.status, error: result.message, }; }; async function readFirst(paths: string[], source: string, parse: (payload: unknown) => { data?: T; sourceAt?: string }): Promise> { let last: Awaited> | undefined; for (const path of paths) { const result = await readJson(path); if (result.ok) { const parsed = parse(unwrap(result.payload)); return resource(`GET ${path}`, result, parsed.data, parsed.sourceAt || sourceTime(unwrap(result.payload))); } last = result; if (result.status !== 404) break; } return resource(source, last || { ok: false, status: 0, message: "读取接口失败" }); } const asList = (value: unknown, keys: string[]): unknown[] => { if (Array.isArray(value)) return value; if (!isRecord(value)) return []; for (const key of keys) if (Array.isArray(value[key])) return value[key] as unknown[]; return []; }; const stringList = (value: unknown): string[] | undefined => { if (!Array.isArray(value)) return undefined; const values = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0); return values.length ? values : undefined; }; const normalizeDevice = (item: unknown): ConsoleDevice | undefined => { if (!isRecord(item)) return undefined; const deviceId = text(item.device_id) || text(item.deviceId) || text(item.id); if (!deviceId) return undefined; const rawWechat = isRecord(item.wechat) ? item.wechat : {}; const rawAgent = isRecord(item.agent) ? item.agent : {}; const rawHook = isRecord(item.hook) ? item.hook : {}; // NAS 设备目录保留注册档案和心跳快照;控制台必须同时读取这两层真实上报。 const deviceProfile = isRecord(item.device_profile) ? item.device_profile : {}; const quickStatus = isRecord(item.quick_status) ? item.quick_status : {}; const lastStatus = isRecord(item.last_status) ? item.last_status : {}; const runtime = Object.keys(quickStatus).length ? quickStatus : lastStatus; return { deviceId, name: text(item.name) || text(item.device_name) || text(deviceProfile.name), model: text(item.model) || text(item.model_name) || text(deviceProfile.model), androidVersion: text(item.android_version) || text(item.androidVersion) || text(item.os_version) || text(deviceProfile.android_version) || text(deviceProfile.os_version), projectId: text(item.project_id) || text(item.projectId), groupId: text(item.group_id) || text(item.groupId) || text(item.project_id), status: text(item.status), online: boolean(item.online) ?? boolean(item.ws_online) ?? (text(item.status) === "online" ? true : undefined), agentVersion: text(item.agent_version) || text(item.agentVersion) || text(item.app_version) || text(deviceProfile.app_version), agentRunning: boolean(item.agent_running) ?? boolean(rawAgent.running) ?? boolean(runtime.agent_running), hookAvailable: boolean(item.hook_available) ?? boolean(item.supports_hook) ?? boolean(rawHook.available), wechatRunning: boolean(item.wechat_running) ?? boolean(rawWechat.running) ?? boolean(runtime.wechat_running) ?? boolean(isRecord(runtime.wechat) ? runtime.wechat.running : undefined), wechatId: text(item.wxid) || text(item.wechat_id) || text(rawWechat.id) || text(rawWechat.wechat_id) || text(runtime.wxid) || text(runtime.wechat_id), friendCount: number(item.friend_count) ?? number(rawWechat.friend_count) ?? number(runtime.friend_count), wechatVersion: text(item.wechat_version) || text(rawWechat.version) || text(runtime.wechat_version) || text(deviceProfile.wechat_version), batteryPercent: number(item.battery_percent) ?? number(item.batteryPercent) ?? number(isRecord(item.battery) ? item.battery.percent : undefined) ?? number(runtime.battery_level) ?? number(runtime.battery), networkType: text(item.network_type) || text(item.networkType) || text(isRecord(item.network) ? item.network.type : undefined) || text(runtime.network_type), healthScore: number(item.health_score) ?? number(item.healthScore) ?? number(isRecord(item.health) ? item.health.score : undefined) ?? number(runtime.health_score), tags: stringList(item.tags) || stringList(item.labels) || stringList(item.tag_names), lastHeartbeat: text(item.last_heartbeat) || text(item.lastHeartbeat), sourceAt: sourceTime(item) || text(item.last_heartbeat), capabilities: Array.isArray(item.capabilities) ? item.capabilities.filter((entry): entry is string => typeof entry === "string") : undefined, sourceKind: item.source_kind === "history" || item.sourceKind === "history" ? "history" : item.source_kind === "fixture" || item.sourceKind === "fixture" ? "fixture" : "live", sourceLabel: text(item.source_label) || text(item.sourceLabel), statusSource: text(item.status_source) || text(item.statusSource), raw: item, }; }; const parseDevices = (value: unknown) => { const rows = asList(value, ["devices", "items"]); return rows.flatMap((item) => { const normalized = normalizeDevice(item); return normalized ? [normalized] : []; }); }; const readReadonlyDeviceStateFixture = (): { label: string; devices: ConsoleDevice[] } | undefined => { const encoded = new URLSearchParams(window.location.search).get("device_state_fixture"); if (!encoded) return undefined; try { const payload = JSON.parse(decodeURIComponent(escape(window.atob(encoded)))) as RecordValue; if (payload.mode !== "readonly_fixture" || typeof payload.label !== "string") return undefined; const devices = parseDevices(payload.devices).map((device) => ({ ...device, sourceKind: device.sourceKind || "fixture" })); return { label: payload.label, devices }; } catch { return undefined; } }; const parseGroups = (value: unknown): ConsoleDeviceGroup[] => asList(value, ["items", "groups"]).flatMap((item) => { if (!isRecord(item)) return []; const groupId = text(item.group_id) || text(item.groupId) || text(item.id); if (!groupId) return []; return [{ groupId, name: text(item.name) || groupId, memberCount: number(item.member_count) ?? number(item.memberCount), deviceIds: Array.isArray(item.device_ids) ? item.device_ids.filter((entry): entry is string => typeof entry === "string") : undefined, sourceAt: sourceTime(item), }]; }); const parseMetric = (value: unknown): unknown => value; export async function readConsoleData(): Promise { const [overview, devices, groups, release, aiStatus, aiTasks, brain, connection, process, integrationHealth, applications, authorizations, usage, manifest, documents, knowledge] = await Promise.all([ readFirst(["/api/v3/workbench/overview"], "GET /api/v3/workbench/overview", (value) => ({ data: isRecord(value) ? value as ConsoleOverview : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/fleet/devices", "/api/v3/devices"], "GET /api/v3/fleet/devices", (value) => ({ data: parseDevices(value), sourceAt: sourceTime(value) })), readFirst(["/api/v3/device-groups"], "GET /api/v3/device-groups", (value) => ({ data: parseGroups(value), sourceAt: sourceTime(value) })), readFirst(["/api/v3/releases/latest"], "GET /api/v3/releases/latest", (value) => ({ data: isRecord(value) ? value as ConsoleRelease : undefined, sourceAt: sourceTime(value) || (isRecord(value) ? text(value.published_at) : undefined) })), readFirst(["/api/v3/ai/status"], "GET /api/v3/ai/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/ai/tasks?limit=20"], "GET /api/v3/ai/tasks", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/ai/brain/dashboard"], "GET /api/v3/ai/brain/dashboard", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/connection/status"], "GET /api/v3/connection/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/process/status"], "GET /api/v3/process/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/integration/health"], "GET /api/v3/integration/health", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/integrations/apps", "/api/v3/integrations/clients"], "GET /api/v3/integrations/apps", (value) => ({ data: asList(value, ["items", "applications"]) as IntegrationApplication[], sourceAt: sourceTime(value) })), readFirst(["/api/v3/integrations/authorizations", "/api/v3/authorization-grants"], "GET /api/v3/integrations/authorizations", (value) => ({ data: asList(value, ["items", "grants"]) as AuthorizationGrant[], sourceAt: sourceTime(value) })), readFirst(["/api/v3/integrations/usage"], "GET /api/v3/integrations/usage", (value) => ({ data: isRecord(value) ? value as IntegrationUsage : undefined, sourceAt: sourceTime(value) })), readFirst(["/api/v3/integration/manifest"], "GET /api/v3/integration/manifest", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst>>(["/api/v3/integrations/docs/catalog", "/api/v3/docs/catalog"], "GET /api/v3/integrations/docs/catalog", (value) => ({ data: asList(value, ["items", "documents"]) as Array>, sourceAt: sourceTime(value) })), readFirst>>(["/api/v3/kb/search?limit=10"], "GET /api/v3/kb/search", (value) => ({ data: asList(value, ["items", "results", "documents"]) as Array>, sourceAt: sourceTime(value) })), ]); const readonlyFixture = readReadonlyDeviceStateFixture(); const deviceResource: ApiResource = readonlyFixture && devices.state === "ready" ? { ...devices, source: `${devices.source} + readonly_fixture:${readonlyFixture.label}`, data: [...(devices.data || []), ...readonlyFixture.devices], } : devices; const hookDeviceId = deviceResource.data?.[0]?.deviceId; const hookStatus = hookDeviceId ? await readFirst( [`/api/v3/wechat/hook/status?device_id=${encodeURIComponent(hookDeviceId)}`], "GET /api/v3/wechat/hook/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) }), ) : emptyResource("GET /api/v3/wechat/hook/status"); const taskItems = aiTasks.state === "ready" ? asList(aiTasks.data, ["items", "tasks"]) as Array> : []; const selectedTaskId = taskItems.length && isRecord(taskItems[0]) ? text(taskItems[0].task_id) : undefined; const [taskDetail, taskLogs, taskAudit] = selectedTaskId ? await Promise.all([ readFirst([`/api/v3/ai/tasks/${encodeURIComponent(selectedTaskId)}`], "GET /api/v3/ai/tasks/{task_id}", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst([`/api/v3/ai/tasks/${encodeURIComponent(selectedTaskId)}/logs`], "GET /api/v3/ai/tasks/{task_id}/logs", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), readFirst([`/api/v3/ai/tasks/${encodeURIComponent(selectedTaskId)}/audit`], "GET /api/v3/ai/tasks/{task_id}/audit", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })), ]) : [emptyResource("GET /api/v3/ai/tasks/{task_id}"), emptyResource("GET /api/v3/ai/tasks/{task_id}/logs"), emptyResource("GET /api/v3/ai/tasks/{task_id}/audit")]; const aiOperations: ApiResource = aiTasks.state === "ready" ? { state: "ready", source: "GET /api/v3/ai/tasks + /logs + /audit", sourceAt: [aiTasks.sourceAt, taskDetail.sourceAt, taskLogs.sourceAt, taskAudit.sourceAt].filter(Boolean).sort().at(-1), data: { items: taskItems, selected: taskDetail.data, logs: asList(taskLogs.data, ["items", "logs"]) as Array>, audit: asList(taskAudit.data, ["items", "audit"]) as Array>, }, } : { state: aiTasks.state, source: aiTasks.source, status: aiTasks.status, error: aiTasks.error }; const engineResource: ApiResource = aiStatus.state === "ready" || brain.state === "ready" || connection.state === "ready" || process.state === "ready" || integrationHealth.state === "ready" ? { state: "ready", source: "GET /api/v3/ai/status + /api/v3/ai/brain/dashboard + /api/v3/connection/status + /api/v3/process/status", sourceAt: [aiStatus.sourceAt, brain.sourceAt, connection.sourceAt, process.sourceAt, integrationHealth.sourceAt].filter(Boolean).sort().at(-1), data: { aiStatus: aiStatus.data, brain: brain.data, connection: connection.data, process: process.data, integrationHealth: integrationHealth.data }, } : { state: aiStatus.state === "forbidden" ? "forbidden" : "unavailable", source: "GET /api/v3/ai/status + /api/v3/ai/brain/dashboard", error: aiStatus.error || brain.error, status: aiStatus.status || brain.status }; return { overview, devices: deviceResource, hookStatus, groups, release, engine: engineResource, aiOperations, integrations: { applications, authorizations, usage, manifest, documents, knowledge }, readAt: new Date().toISOString(), }; } export const apiMetric = (metric: unknown): OverviewMetric => { if (!isRecord(metric)) return {}; return { value: metric.value as string | number | null | undefined, display: text(metric.display), status: text(metric.status), source: text(metric.source), sampled_at: text(metric.sampled_at), }; }; export const stale = (sourceAt?: string, maxAgeMinutes = 15) => { if (!sourceAt) return false; const timestamp = Date.parse(sourceAt); return Number.isFinite(timestamp) && Date.now() - timestamp > maxAgeMinutes * 60 * 1000; }; export const readFailureReason = (item: ApiResource) => item.error || (item.status === 403 ? "当前账号没有读取权限" : item.status === 401 ? "登录状态已失效" : "接口未接通或暂无数据"); export { EMPTY_CONSOLE_DATA };