决策「L2 同时暴露 MCP+控制台」。此前 system.audit.l2 只存在于 被控端实现里:MCP 无工具(模型看不到)、控制台无入口无类型 (用户点不到)、审计事件写成 agent.start(与进程生命周期混类筛不出)。 MCP(41 → 42 个工具): - defs.ts 在 na_audit 后新增 na_audit_l2(action: status/enable/ collect/verify + since/limit,additionalProperties: false) - index.ts TOOL_TO_CAPABILITY 映射 CapabilityNames.AuditL2, 分发走 default 直通(参数名与被控端一致,无需改写) - mcp-tools 守卫同步:41→42、BASELINE_SEQ_SHA 重算、 新增「na_audit_l2 必须紧随 na_audit」顺序断言 控制台: - conn.ts WINDOWS_ONLY_CAPS += system.audit.l2(非 Windows 被控端 上按钮禁用并明示原因,不搞「点了才报错」) - types/index.ts 新增 AuditL2Result(四 action 属性并集,对齐 manifest returns_schema)+ AuditType += agent.l2; AuditEntry 同步补 args_bytes/args_truncated(A-P0-4 新字段) - AuditView.vue 新增 L2 卡片:查状态/拉取采集/校验 L2 链/启用策略 (NPopconfirm 二次确认 + 管理员权限提示);信任边界如实说明 (采集器与 agent 同权限,防篡改依赖转发出机,不夸大本地副本) agent.l2 审计类型四端同源: - protocol audit-types.ts AUDIT_TYPES(真源)+= agent.l2 - agent audit.ts AuditType 联合同步(AssertExact 编译期守卫兜底) - audit-l2.ts 两处写入点 agent.start → agent.l2(L2 策略操作与 进程生命周期无关,混类会污染两边统计与筛选) 守卫加强(console-contract 平台门控): - audit-l2.ts 平台门控由裸 throw 改用统一 assertWindows (错误信息格式统一 + 可被守卫扫到) - 守卫扫描范围 +audit-l2.ts、模式 +assertWindows —— 覆盖面扩大; 否则把 system.audit.l2 加进 WINDOWS_ONLY_CAPS 反而报红 - mcp-tools 正则盲区修复:原 na_[a-z_]+ 匹配不了带数字的工具名, na_audit_l2 会整个漏扫(新增了工具两个守卫却无觉)→ 放至 [a-z0-9_]+ 新增 tests/unit/console-l2.test.mjs 5 例:前端字段 ⊆ returns_schema / 契约关键字段 ⊇ 前端渲染 / MCP 工具存在且映射 / agent.l2 四端同源 / 界面纪律(信任边界文案 + enable 二次确认 + 平台门控) 故障注入实测:审计类型退回 agent.start、WINDOWS_ONLY_CAPS 删项 → 3 处红;恢复 → 绿 单测 451/451、e2e 33/33、typecheck 全绿
172 lines
5.5 KiB
TypeScript
172 lines
5.5 KiB
TypeScript
/** 连接状态 store:连接/断开、能力清单、元信息、日志与被控端事件。 */
|
|
import { defineStore } from "pinia";
|
|
import { computed, ref } from "vue";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import * as api from "../api";
|
|
import type { ConnectInfo, InvokeResult, SystemInfo } from "../types";
|
|
|
|
export interface AgentEvent {
|
|
ts: number;
|
|
payload: unknown;
|
|
}
|
|
|
|
/**
|
|
* 仅在被控端为 Windows 时可用(其余平台返回 E_UNSUPPORTED_PLATFORM)。
|
|
* 前端据此**事前门控**,而不是发出注定失败的请求再解释错误。
|
|
*/
|
|
export const WINDOWS_ONLY_CAPS = ["app.list", "app.install", "system.service.list", "system.audit.l2"] as const;
|
|
|
|
export const useConnStore = defineStore("conn", () => {
|
|
const state = ref<string>("closed");
|
|
// 默认 wss:被控端默认 tls=true(明文 ws 连 TLS 端口必然握手失败,2026-10-17 真机踩过)
|
|
const url = ref(localStorage.getItem("na.url") || "wss://127.0.0.1:8765");
|
|
const key = ref(localStorage.getItem("na.key") || "");
|
|
const info = ref<ConnectInfo | null>(null);
|
|
const logs = ref<string[]>([]);
|
|
const events = ref<AgentEvent[]>([]);
|
|
const connecting = ref(false);
|
|
const bound = ref(false);
|
|
/** 被控端平台:'Windows' | 'macOS' | 'Linux' | ... (连接后自动探测) */
|
|
const nodeOs = ref<string | null>(null);
|
|
|
|
const connected = computed(() => state.value === "connected" && !!info.value);
|
|
const capabilities = computed(() => info.value?.capabilities ?? []);
|
|
/** 被控端能力名集合,便于按名判存在性。 */
|
|
const capabilityNames = computed(() => new Set(capabilities.value.map((c) => c.name)));
|
|
const isWindows = computed(() => nodeOs.value === "Windows");
|
|
/** 平台门控能力当前是否可用(未探测到平台时按「乐观可用」处理)。 */
|
|
const platformReady = computed(() => nodeOs.value === null || isWindows.value);
|
|
|
|
function pushLog(msg: string) {
|
|
logs.value.push(`[${new Date().toLocaleTimeString()}] ${msg}`);
|
|
if (logs.value.length > 500) logs.value.shift();
|
|
}
|
|
|
|
/** 绑定后端事件(应用启动时调用一次)。 */
|
|
async function bind() {
|
|
if (bound.value) return;
|
|
bound.value = true;
|
|
await listen<string>("na-log", (e) => pushLog(e.payload));
|
|
await listen<string>("na-state", (e) => {
|
|
state.value = e.payload;
|
|
pushLog(`状态 → ${e.payload}`);
|
|
});
|
|
await listen<unknown>("na-event", (e) => {
|
|
events.value.unshift({ ts: Date.now(), payload: e.payload });
|
|
if (events.value.length > 200) events.value.pop();
|
|
pushLog(`被控端事件:${JSON.stringify(e.payload)}`);
|
|
});
|
|
state.value = await api.getState();
|
|
}
|
|
|
|
/** 探测被控端平台(`system.info.os`)——决定 Windows 专属能力是否可用。 */
|
|
async function loadPlatform() {
|
|
try {
|
|
const i = await api.ok<SystemInfo>("system.info", { fields: ["os"] });
|
|
nodeOs.value = i.os ?? null;
|
|
pushLog(`被控端平台:${nodeOs.value ?? "未知"}`);
|
|
return nodeOs.value;
|
|
} catch (e) {
|
|
pushLog(`平台探测失败:${e}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function connect(u = url.value, k = key.value) {
|
|
connecting.value = true;
|
|
url.value = u;
|
|
key.value = k;
|
|
pushLog(`正在连接 ${u} ...`);
|
|
try {
|
|
const i = await api.connect(u, k, "console");
|
|
info.value = i;
|
|
localStorage.setItem("na.url", u);
|
|
localStorage.setItem("na.key", k);
|
|
pushLog(`握手成功:${i.capabilities.length} 项能力,agent ${i.agent_version ?? "?"}`);
|
|
// 握手后立刻探测平台,供各页面事前门控。
|
|
await loadPlatform();
|
|
return i;
|
|
} catch (e) {
|
|
pushLog(`连接失败:${e}`);
|
|
throw e;
|
|
} finally {
|
|
connecting.value = false;
|
|
}
|
|
}
|
|
|
|
async function disconnect() {
|
|
await api.disconnect();
|
|
info.value = null;
|
|
nodeOs.value = null;
|
|
pushLog("已断开");
|
|
}
|
|
|
|
/** 泛型调用;连接断开时抛错。 */
|
|
async function call<T = unknown>(
|
|
capability: string,
|
|
args: Record<string, unknown> = {},
|
|
timeoutMs?: number,
|
|
): Promise<InvokeResult<T>> {
|
|
if (!connected.value) throw new api.CapabilityFailure(capability, "E_NODE_OFFLINE", "未连接");
|
|
const t0 = performance.now();
|
|
const res = await api.call<T>(capability, args, timeoutMs);
|
|
const ms = Math.round(performance.now() - t0);
|
|
if (res.status === "ok") {
|
|
pushLog(`${capability} → ok (${ms}ms)`);
|
|
} else {
|
|
pushLog(`${capability} → 失败:[${res.error?.name}] ${res.error?.message ?? "未知错误"}`);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* 调用并要求成功,直接返回 data。
|
|
* 失败抛 `CapabilityFailure`(携带 `errorName`),便于页面按错误名分支
|
|
* ——如 `isUnsupportedPlatform(e)` 判平台不支持。
|
|
*/
|
|
async function ok<T = unknown>(
|
|
capability: string,
|
|
args: Record<string, unknown> = {},
|
|
timeoutMs?: number,
|
|
): Promise<T> {
|
|
const res = await call<T>(capability, args, timeoutMs);
|
|
if (res.status !== "ok") {
|
|
throw new api.CapabilityFailure(
|
|
capability,
|
|
res.error?.name ?? "E_UNKNOWN",
|
|
res.error?.message ?? `${capability} 执行失败`,
|
|
res.error?.data,
|
|
);
|
|
}
|
|
return res.data as T;
|
|
}
|
|
|
|
function clearLogs() {
|
|
logs.value = [];
|
|
}
|
|
|
|
return {
|
|
state,
|
|
url,
|
|
key,
|
|
info,
|
|
logs,
|
|
events,
|
|
connecting,
|
|
connected,
|
|
capabilities,
|
|
capabilityNames,
|
|
nodeOs,
|
|
isWindows,
|
|
platformReady,
|
|
bind,
|
|
loadPlatform,
|
|
connect,
|
|
disconnect,
|
|
call,
|
|
ok,
|
|
pushLog,
|
|
clearLogs,
|
|
};
|
|
});
|