fix: map nested device telemetry in console
This commit is contained in:
323
sdk/android-ui/src/lib/consoleApi.ts
Normal file
323
sdk/android-ui/src/lib/consoleApi.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
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<string, unknown>;
|
||||
|
||||
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<unknown> {
|
||||
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<SecurityModuleState> {
|
||||
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<SecurityModuleState> {
|
||||
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<string, unknown> }): Promise<SecurityModule> {
|
||||
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<string, unknown> }): Promise<SecurityModule> {
|
||||
return await writeJson("/api/v3/security/modules", "POST", body) as SecurityModule;
|
||||
}
|
||||
|
||||
export async function removeSecurityModule(id: string): Promise<void> {
|
||||
await writeJson(`/api/v3/security/modules/${encodeURIComponent(id)}`, "DELETE");
|
||||
}
|
||||
|
||||
const resource = <T>(source: string, result: Awaited<ReturnType<typeof readJson>>, data?: T, sourceAt?: string): ApiResource<T> => {
|
||||
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<T>(paths: string[], source: string, parse: (payload: unknown) => { data?: T; sourceAt?: string }): Promise<ApiResource<T>> {
|
||||
let last: Awaited<ReturnType<typeof readJson>> | 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<ConsoleApiData> {
|
||||
const [overview, devices, groups, release, aiStatus, aiTasks, brain, connection, process, integrationHealth, applications, authorizations, usage, manifest, documents, knowledge] = await Promise.all([
|
||||
readFirst<ConsoleOverview>(["/api/v3/workbench/overview"], "GET /api/v3/workbench/overview", (value) => ({ data: isRecord(value) ? value as ConsoleOverview : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<ConsoleDevice[]>(["/api/v3/fleet/devices", "/api/v3/devices"], "GET /api/v3/fleet/devices", (value) => ({ data: parseDevices(value), sourceAt: sourceTime(value) })),
|
||||
readFirst<ConsoleDeviceGroup[]>(["/api/v3/device-groups"], "GET /api/v3/device-groups", (value) => ({ data: parseGroups(value), sourceAt: sourceTime(value) })),
|
||||
readFirst<ConsoleRelease>(["/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<RecordValue>(["/api/v3/ai/status"], "GET /api/v3/ai/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>(["/api/v3/ai/tasks?limit=20"], "GET /api/v3/ai/tasks", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>(["/api/v3/ai/brain/dashboard"], "GET /api/v3/ai/brain/dashboard", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>(["/api/v3/connection/status"], "GET /api/v3/connection/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>(["/api/v3/process/status"], "GET /api/v3/process/status", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>(["/api/v3/integration/health"], "GET /api/v3/integration/health", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<IntegrationApplication[]>(["/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<AuthorizationGrant[]>(["/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<IntegrationUsage>(["/api/v3/integrations/usage"], "GET /api/v3/integrations/usage", (value) => ({ data: isRecord(value) ? value as IntegrationUsage : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>(["/api/v3/integration/manifest"], "GET /api/v3/integration/manifest", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<Array<Record<string, unknown>>>(["/api/v3/integrations/docs/catalog", "/api/v3/docs/catalog"], "GET /api/v3/integrations/docs/catalog", (value) => ({ data: asList(value, ["items", "documents"]) as Array<Record<string, unknown>>, sourceAt: sourceTime(value) })),
|
||||
readFirst<Array<Record<string, unknown>>>(["/api/v3/kb/search?limit=10"], "GET /api/v3/kb/search", (value) => ({ data: asList(value, ["items", "results", "documents"]) as Array<Record<string, unknown>>, sourceAt: sourceTime(value) })),
|
||||
]);
|
||||
|
||||
const readonlyFixture = readReadonlyDeviceStateFixture();
|
||||
const deviceResource: ApiResource<ConsoleDevice[]> = 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<RecordValue>(
|
||||
[`/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<RecordValue>("GET /api/v3/wechat/hook/status");
|
||||
|
||||
const taskItems = aiTasks.state === "ready" ? asList(aiTasks.data, ["items", "tasks"]) as Array<Record<string, unknown>> : [];
|
||||
const selectedTaskId = taskItems.length && isRecord(taskItems[0]) ? text(taskItems[0].task_id) : undefined;
|
||||
const [taskDetail, taskLogs, taskAudit] = selectedTaskId
|
||||
? await Promise.all([
|
||||
readFirst<RecordValue>([`/api/v3/ai/tasks/${encodeURIComponent(selectedTaskId)}`], "GET /api/v3/ai/tasks/{task_id}", (value) => ({ data: isRecord(value) ? value : undefined, sourceAt: sourceTime(value) })),
|
||||
readFirst<RecordValue>([`/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<RecordValue>([`/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<RecordValue>("GET /api/v3/ai/tasks/{task_id}"), emptyResource<RecordValue>("GET /api/v3/ai/tasks/{task_id}/logs"), emptyResource<RecordValue>("GET /api/v3/ai/tasks/{task_id}/audit")];
|
||||
const aiOperations: ApiResource<AIOperationSummary> = 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<Record<string, unknown>>,
|
||||
audit: asList(taskAudit.data, ["items", "audit"]) as Array<Record<string, unknown>>,
|
||||
},
|
||||
}
|
||||
: { state: aiTasks.state, source: aiTasks.source, status: aiTasks.status, error: aiTasks.error };
|
||||
|
||||
const engineResource: ApiResource<EngineSummary> =
|
||||
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<unknown>) => item.error || (item.status === 403 ? "当前账号没有读取权限" : item.status === 401 ? "登录状态已失效" : "接口未接通或暂无数据");
|
||||
|
||||
export { EMPTY_CONSOLE_DATA };
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#f4f7ff" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<title>工作手机总控平台</title>
|
||||
<script type="module" crossorigin src="/static/console/assets/index-BzwMJihg.js"></script>
|
||||
<script type="module" crossorigin src="/static/console/assets/index-_B4qz-iN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/console/assets/style-Dxgu53Kz.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
24
开发文档/8、部署/06-存客宝宝塔/20260810_NAS设备详情字段映射修复.md
Normal file
24
开发文档/8、部署/06-存客宝宝塔/20260810_NAS设备详情字段映射修复.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# NAS 设备详情字段映射修复
|
||||
|
||||
- 日期:2026-08-10
|
||||
- 入口:`http://open.quwanzhi.com:8899/?tab=devices`
|
||||
- 设备:工作手机 Android(WSS Agent 在线)
|
||||
|
||||
## 已修复
|
||||
|
||||
控制台设备详情此前只读取顶层字段,而 NAS 设备上报将注册档案放在 `device_profile`、实时心跳放在 `quick_status`。现已合并读取两层数据,详情页可显示:
|
||||
|
||||
- Android 版本、型号、厂商、Agent 版本
|
||||
- 微信运行状态与微信版本
|
||||
- 电量、网络、屏幕/前台应用等实时状态
|
||||
- 设备能力与心跳时间
|
||||
|
||||
## 微信号与好友数
|
||||
|
||||
这两个字段必须由微信 Frida Hook 只读回读。真机回执显示 Frida 已注入,当前处于短暂附着冷却,采集接口返回 `attach_in_progress`;控制台保持“未获取”,不会用历史或模拟数据填充。Hook 完成附着后,资料与联系人计数会通过同一 WSS 链路回传。
|
||||
|
||||
## 验收
|
||||
|
||||
- 新控制台静态资源:`/static/console/assets/index-_B4qz-iN.js`
|
||||
- NAS 设备列表接口:HTTP 200
|
||||
- NAS SDK:健康探针通过后可用
|
||||
Reference in New Issue
Block a user