统一 Session 协议模型与表格展示边界

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Ubuntu 2026-08-03 19:14:46 +00:00
parent af0b0e450b
commit 7d0805667c
5 changed files with 89 additions and 23 deletions

View File

@ -1,6 +1,8 @@
import { useReducer, useState } from "react"; import { useReducer, useState } from "react";
import { SessionQueryIntent, SessionTable } from "./SessionTable"; import { SessionQueryIntent, SessionTable } from "./SessionTable";
import { demoSessions } from "./protocol/fixtures";
import { initializeSessionTableStates, updateSessionTableState } from "./sessionTableState"; import { initializeSessionTableStates, updateSessionTableState } from "./sessionTableState";
import { toSessionRow } from "./sessionPresentation";
import { import {
createInitialWorkspaceState, createInitialWorkspaceState,
selectActiveTab, selectActiveTab,
@ -8,12 +10,8 @@ import {
workspaceReducer, workspaceReducer,
} from "./workspace"; } from "./workspace";
const sessions = [ const fixtureNow = new Date("2026-08-03T19:12:02Z");
{ id: "NW-042", status: "ONLINE", host: "ws-fin-042", user: "nora.chen", os: "Windows 11", address: "10.42.8.17", seen: "14s ago", tags: ["finance", "priority"] }, const sessions = demoSessions.map((session) => toSessionRow(session, fixtureNow));
{ id: "NW-038", status: "ONLINE", host: "srv-app-03", user: "svc.deploy", os: "Ubuntu 24.04", address: "10.42.3.8", seen: "51s ago", tags: ["server"] },
{ id: "NW-031", status: "IDLE", host: "mac-design-07", user: "alex.k", os: "macOS 15.6", address: "10.42.12.27", seen: "8m ago", tags: ["design"] },
{ id: "NW-019", status: "OFFLINE", host: "ws-ops-019", user: "operator", os: "Windows 10", address: "10.42.6.91", seen: "2h ago", tags: ["legacy"] },
];
export function App() { export function App() {
const [workspace, dispatch] = useReducer(workspaceReducer, undefined, createInitialWorkspaceState); const [workspace, dispatch] = useReducer(workspaceReducer, undefined, createInitialWorkspaceState);

View File

@ -1,18 +1,43 @@
import type { Session } from "../model"; import type { CapabilityId, ProjectId, ServerId, Session, SessionId, Timestamp } from "../model";
/** UI 迁移期可直接使用的最小 Session fixture。 */ const serverId = "atlas" as ServerId;
export const demoSession = { const projectId = "northwind" as ProjectId;
server_id: "demo-server", const overview = "session.overview" as CapabilityId;
project_id: "demo-project",
session_id: "demo-session", interface DemoSessionInput {
name: "演示会话", readonly session_id: string;
state: "ONLINE", readonly name: string;
hostname: "demo-host", readonly state: Session["state"];
username: "operator", readonly hostname?: string;
os: "linux", readonly username?: string;
architecture: "amd64", readonly os?: string;
first_seen_at: "2026-08-03T00:00:00Z", readonly architecture?: string;
last_active_at: "2026-08-03T00:00:05Z", readonly source_address?: string;
tags: ["fixture"], readonly first_seen_at: string;
capabilities: ["session.overview"], readonly last_active_at: string;
} as unknown as Session; readonly tags: readonly string[];
}
function session(
value: DemoSessionInput,
): Session {
return {
...value,
server_id: serverId,
project_id: projectId,
session_id: value.session_id as SessionId,
first_seen_at: value.first_seen_at as Timestamp,
last_active_at: value.last_active_at as Timestamp,
capabilities: [overview],
};
}
/** 界面只通过展示适配器消费的标准协议 Session fixture。 */
export const demoSessions: readonly Session[] = [
session({ session_id: "NW-042", name: "NW-042", state: "ONLINE", hostname: "ws-fin-042", username: "nora.chen", os: "Windows 11", architecture: "amd64", source_address: "10.42.8.17", first_seen_at: "2026-08-01T08:00:00Z", last_active_at: "2026-08-03T19:11:48Z", tags: ["finance", "priority"] }),
session({ session_id: "NW-038", name: "NW-038", state: "ONLINE", hostname: "srv-app-03", username: "svc.deploy", os: "Ubuntu 24.04", architecture: "amd64", source_address: "10.42.3.8", first_seen_at: "2026-07-28T10:30:00Z", last_active_at: "2026-08-03T19:11:11Z", tags: ["server"] }),
session({ session_id: "NW-031", name: "NW-031", state: "DORMANT", hostname: "mac-design-07", username: "alex.k", os: "macOS 15.6", architecture: "arm64", source_address: "10.42.12.27", first_seen_at: "2026-07-22T09:15:00Z", last_active_at: "2026-08-03T19:04:00Z", tags: ["design"] }),
session({ session_id: "NW-019", name: "NW-019", state: "OFFLINE", hostname: "ws-ops-019", username: "operator", os: "Windows 10", architecture: "amd64", source_address: "10.42.6.91", first_seen_at: "2026-07-10T14:00:00Z", last_active_at: "2026-08-03T17:12:00Z", tags: ["legacy"] }),
];
export const demoSession = demoSessions[0];

View File

@ -86,6 +86,7 @@ export interface Session extends SessionContext, Extensible {
readonly username?: string; readonly username?: string;
readonly os?: string; readonly os?: string;
readonly architecture?: string; readonly architecture?: string;
readonly source_address?: string;
readonly first_seen_at: Timestamp; readonly first_seen_at: Timestamp;
readonly last_active_at: Timestamp; readonly last_active_at: Timestamp;
readonly tags: readonly string[]; readonly tags: readonly string[];

View File

@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { demoSessions } from "./protocol/fixtures";
import { toSessionRow } from "./sessionPresentation";
describe("toSessionRow", () => {
it("将协议 Session 转换为稳定的表格展示值", () => {
expect(toSessionRow(demoSessions[0], new Date("2026-08-03T19:12:02Z"))).toEqual({
id: "NW-042",
status: "ONLINE",
host: "ws-fin-042",
user: "nora.chen",
os: "Windows 11 / amd64",
address: "10.42.8.17",
seen: "14s ago",
tags: ["finance", "priority"],
});
});
});

View File

@ -0,0 +1,24 @@
import type { Session } from "./protocol/model";
import type { SessionRow } from "./SessionTable";
/** 将服务端权威模型转换为表格展示值,不在 UI 复制业务模型。 */
export function toSessionRow(session: Session, now: Date = new Date()): SessionRow {
return {
id: session.session_id,
status: session.state,
host: session.hostname ?? session.name,
user: session.username ?? "—",
os: [session.os, session.architecture].filter(Boolean).join(" / ") || "未知",
address: session.source_address ?? "—",
seen: formatRelativeTime(session.last_active_at, now),
tags: [...session.tags],
};
}
function formatRelativeTime(timestamp: string, now: Date): string {
const elapsedSeconds = Math.max(0, Math.floor((now.getTime() - Date.parse(timestamp)) / 1000));
if (elapsedSeconds < 60) return `${elapsedSeconds}s ago`;
if (elapsedSeconds < 3600) return `${Math.floor(elapsedSeconds / 60)}m ago`;
if (elapsedSeconds < 86400) return `${Math.floor(elapsedSeconds / 3600)}h ago`;
return `${Math.floor(elapsedSeconds / 86400)}d ago`;
}