@@ -1224,6 +1953,19 @@ function RecoveryPage() {
);
}
+function RecoveryPage() {
+ const { taskId = "", platform = "tmall" } = useParams();
+ const [searchParams] = useSearchParams();
+ const from = searchParams.get("from") ?? `/tasks/${taskId}/run`;
+ const platformId = platform as PlatformId;
+
+ if (isOpsManagedPlatform(platformId)) {
+ return
;
+ }
+
+ return
;
+}
+
export function App() {
return (
@@ -1231,9 +1973,14 @@ export function App() {
} path="/tasks/new" />
} path="/tasks/:taskId/confirm" />
} path="/tasks/:taskId/run" />
+ } path="/ops/tasks/:taskId/recovery/:platform" />
} path="/tasks/:taskId/recovery/:platform" />
} path="/tasks/:taskId/report" />
} path="/history" />
+ } path="/ops/session-manager" />
+ } path="/ops/jd/session-manager" />
+ } path="/ops/tmall/session-manager" />
+ } path="/ops/platforms/:platform/prepare" />
} path="/sessions/:platform/prepare" />
);
diff --git a/apps/web/src/HistoryPage.test.tsx b/apps/web/src/HistoryPage.test.tsx
index 6c53532..575c318 100644
--- a/apps/web/src/HistoryPage.test.tsx
+++ b/apps/web/src/HistoryPage.test.tsx
@@ -6,18 +6,28 @@ import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("./lib/api", () => ({
+ clearJdManagedSession: vi.fn(),
+ clearJdSessionManagerConfig: vi.fn(),
clearPlatformSession: vi.fn(),
confirmTask: vi.fn(),
createTask: vi.fn(),
deleteTask: vi.fn(),
+ getJdKeywordPreview: vi.fn(),
+ getJdLiveSession: vi.fn(),
+ getJdSessionManager: vi.fn(),
getHistoryTasks: vi.fn(),
getPlatformReadiness: vi.fn(),
getPlatformSession: vi.fn(),
getTask: vi.fn(),
getTaskCandidates: vi.fn(),
getTaskReport: vi.fn(),
+ importJdManagedSession: vi.fn(),
preparePlatform: vi.fn(),
+ runJdSessionManagerHealthCheck: vi.fn(),
+ runJdSessionManagerRecovery: vi.fn(),
retryTaskPlatform: vi.fn()
+ ,
+ updateJdSessionManagerConfig: vi.fn()
}));
import { HistoryPage } from "./App";
diff --git a/apps/web/src/NewTaskPage.test.tsx b/apps/web/src/NewTaskPage.test.tsx
index c30498e..b5cda64 100644
--- a/apps/web/src/NewTaskPage.test.tsx
+++ b/apps/web/src/NewTaskPage.test.tsx
@@ -2,31 +2,60 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import type { ReactNode } from "react";
-import { MemoryRouter, Route, Routes } from "react-router-dom";
+import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("./lib/api", () => ({
+ clearJdManagedSession: vi.fn(),
+ clearJdSessionManagerConfig: vi.fn(),
clearPlatformSession: vi.fn(),
+ clearTmallManagedSession: vi.fn(),
+ clearTmallSessionManagerConfig: vi.fn(),
confirmTask: vi.fn(),
createTask: vi.fn(),
deleteTask: vi.fn(),
+ getJdKeywordPreview: vi.fn(),
+ getJdLiveSession: vi.fn(),
+ getJdSessionManager: vi.fn(),
getHistoryTasks: vi.fn(),
getPlatformReadiness: vi.fn(),
getPlatformSession: vi.fn(),
getTask: vi.fn(),
getTaskCandidates: vi.fn(),
getTaskReport: vi.fn(),
+ getTmallLiveSession: vi.fn(),
+ getTmallSessionManager: vi.fn(),
+ importJdManagedSession: vi.fn(),
+ importTmallManagedSession: vi.fn(),
preparePlatform: vi.fn(),
- retryTaskPlatform: vi.fn()
+ runJdSessionManagerHealthCheck: vi.fn(),
+ runJdSessionManagerRecovery: vi.fn(),
+ runTmallSessionManagerHealthCheck: vi.fn(),
+ retryTaskPlatform: vi.fn(),
+ updateJdSessionManagerConfig: vi.fn(),
+ updateTmallSessionManagerConfig: vi.fn()
}));
-import { NewTaskPage, SessionPreparePage } from "./App";
+import { App, NewTaskPage } from "./App";
import {
+ clearJdManagedSession,
clearPlatformSession,
+ clearTmallManagedSession,
+ getJdKeywordPreview,
+ getJdLiveSession,
+ getJdSessionManager,
getHistoryTasks,
getPlatformReadiness,
getPlatformSession,
- preparePlatform
+ getTmallLiveSession,
+ getTmallSessionManager,
+ importJdManagedSession,
+ importTmallManagedSession,
+ preparePlatform,
+ runJdSessionManagerHealthCheck,
+ runJdSessionManagerRecovery,
+ runTmallSessionManagerHealthCheck,
+ updateJdSessionManagerConfig
} from "./lib/api";
function renderWithProviders(node: ReactNode, initialEntries?: string[]) {
@@ -51,6 +80,69 @@ function renderWithProviders(node: ReactNode, initialEntries?: string[]) {
describe("task composer and session console", () => {
beforeEach(() => {
+ const jdManagerState = {
+ status: "healthy",
+ enabled: true,
+ autoLoginMode: "command",
+ commandConfigured: true,
+ accountConfigured: true,
+ passwordConfigured: false,
+ accountLabel: "jd***86",
+ browserProfilePath: "D:\\ops\\jd-profile",
+ heartbeatQuery: "iPhone 15",
+ checkIntervalMs: 600000,
+ runnerTimeoutMs: 300000,
+ pendingManualAction: false,
+ note: "京东会话当前可用。",
+ publicNote: "京东会话由运维后台维护,当前可用。",
+ configuredAt: "2026-04-02T08:00:00.000Z",
+ lastCheckAt: "2026-04-02T10:00:00.000Z",
+ lastRecoveredAt: "2026-04-02T09:00:00.000Z",
+ session: {
+ configured: true,
+ importedAt: "2026-04-02T10:00:00.000Z",
+ hasCookie: true,
+ userAgent: "stub-user-agent",
+ searchApiTemplate: {
+ available: true
+ },
+ detailTemplate: {
+ available: true,
+ skuId: "100068388533"
+ },
+ reviewsTemplate: {
+ available: true,
+ skuId: "100068388533"
+ }
+ }
+ };
+ const tmallManagerState = {
+ status: "healthy",
+ enabled: true,
+ heartbeatItemId: "934454505228",
+ checkIntervalMs: 600000,
+ pendingManualAction: false,
+ note: "天猫会话当前可用。",
+ publicNote: "天猫会话由运维后台维护,当前可用。",
+ configuredAt: "2026-04-02T08:00:00.000Z",
+ lastCheckAt: "2026-04-02T10:00:00.000Z",
+ lastHealthyAt: "2026-04-02T10:00:00.000Z",
+ session: {
+ configured: true,
+ importedAt: "2026-04-02T10:00:00.000Z",
+ hasCookie: true,
+ userAgent: "stub-user-agent",
+ detailTemplate: {
+ available: true,
+ itemId: "934454505228"
+ },
+ reviewsTemplate: {
+ available: true,
+ itemId: "934454505228"
+ }
+ }
+ };
+
vi.mocked(getPlatformReadiness).mockResolvedValue({
platforms: [
{
@@ -96,10 +188,10 @@ describe("task composer and session console", () => {
} as any);
vi.mocked(getPlatformSession).mockResolvedValue({
session: {
- platform: "jd",
+ platform: "tmall",
ready: true,
status: "ready",
- searchRequirement: "required",
+ searchRequirement: "recommended",
scope: "workspace",
ttlHours: 24,
lastPreparedAt: "2026-04-02T10:00:00.000Z",
@@ -108,7 +200,218 @@ describe("task composer and session console", () => {
cipherLabel: "mock-aes-gcm-v1"
}
} as any);
+ vi.mocked(getJdLiveSession).mockResolvedValue({
+ session: {
+ configured: true,
+ importedAt: "2026-04-02T10:00:00.000Z",
+ hasCookie: true,
+ userAgent: "stub-user-agent",
+ searchApiTemplate: {
+ available: false
+ },
+ detailTemplate: {
+ available: true,
+ skuId: "100068388533"
+ },
+ reviewsTemplate: {
+ available: true,
+ skuId: "100068388533"
+ }
+ }
+ } as any);
+ vi.mocked(getJdKeywordPreview).mockResolvedValue({
+ preview: {
+ query: "小米手环10",
+ search: {
+ source: "html",
+ candidateCount: 2,
+ selected: {
+ skuId: "222222222222",
+ score: 168,
+ summary: "标题完整命中关键词;位于搜索结果前列;已解析出可回放 SKU",
+ matchedTokens: ["小米手环10"],
+ candidate: {
+ candidateId: "jd-222222222222",
+ platform: "jd",
+ title: "小米手环10 标准版 智能手环",
+ price: 269,
+ priceLabel: "¥269",
+ storeName: "小米京东自营旗舰店",
+ productUrl: "https://item.jd.com/222222222222.html",
+ imageUrl: "https://img14.360buyimg.com/n2/jfs/t1/example-10.jpg",
+ salesHint: "已售50万+",
+ specLabel: "标准版",
+ highlights: ["14天续航"]
+ }
+ },
+ alternatives: [
+ {
+ skuId: "111111111111",
+ score: 118,
+ summary: "标题/卖点命中 1 个关键词片段;已解析出可回放 SKU",
+ matchedTokens: ["小米"],
+ candidate: {
+ candidateId: "jd-111111111111",
+ platform: "jd",
+ title: "小米手环9 NFC版",
+ price: 249,
+ priceLabel: "¥249",
+ storeName: "小米京东自营旗舰店",
+ productUrl: "https://item.jd.com/111111111111.html",
+ imageUrl: "https://img14.360buyimg.com/n2/jfs/t1/example-9.jpg",
+ salesHint: "已售20万+",
+ specLabel: "NFC版",
+ highlights: ["健康监测"]
+ }
+ }
+ ]
+ },
+ product: {
+ skuId: "222222222222",
+ source: "api",
+ detail: {
+ skuId: "222222222222",
+ title: "小米手环10 标准版 智能手环",
+ price: "269.00",
+ originalPrice: "299.00",
+ estimatedPrice: "269.00",
+ shopName: "小米京东自营旗舰店",
+ vendorId: null,
+ categoryPath: ["智能设备", "智能手环"],
+ stockState: "现货",
+ mainImage: "https://img14.360buyimg.com/n2/jfs/t1/example-10.jpg",
+ averageScore: "4.9"
+ },
+ pagination: {
+ requestedPage: 1,
+ requestedCommentCount: 12,
+ maxPages: 2,
+ pagesFetched: 2,
+ pageKey: "page"
+ },
+ reviews: {
+ skuId: "222222222222",
+ total: "10000+",
+ goodRate: "97%",
+ pictureCount: "800",
+ tags: [
+ {
+ tagId: "tag-1",
+ name: "续航很久",
+ count: "5300"
+ }
+ ],
+ comments: [
+ {
+ id: "comment-1",
+ content: "表带舒适,睡眠监测比较准。",
+ score: "5",
+ creationTime: "2026-04-03 09:00:00",
+ userLevelName: "PLUS会员"
+ }
+ ]
+ }
+ }
+ }
+ } as any);
+ vi.mocked(getJdSessionManager).mockResolvedValue({
+ manager: jdManagerState
+ } as any);
+ vi.mocked(getTmallSessionManager).mockResolvedValue({
+ manager: tmallManagerState
+ } as any);
+ vi.mocked(getTmallLiveSession).mockResolvedValue({
+ session: tmallManagerState.session
+ } as any);
vi.mocked(clearPlatformSession).mockResolvedValue(undefined);
+ vi.mocked(importJdManagedSession).mockResolvedValue({
+ manager: {
+ ...jdManagerState,
+ note: "京东会话已更新。",
+ lastRecoveredAt: "2026-04-02T10:05:00.000Z",
+ session: {
+ ...jdManagerState.session,
+ importedAt: "2026-04-02T10:05:00.000Z",
+ }
+ }
+ } as any);
+ vi.mocked(importTmallManagedSession).mockResolvedValue({
+ manager: {
+ ...tmallManagerState,
+ note: "天猫会话已更新。",
+ lastHealthyAt: "2026-04-02T10:05:00.000Z",
+ session: {
+ ...tmallManagerState.session,
+ importedAt: "2026-04-02T10:05:00.000Z"
+ }
+ }
+ } as any);
+ vi.mocked(clearJdManagedSession).mockResolvedValue({
+ manager: {
+ status: "idle",
+ enabled: true,
+ autoLoginMode: "command",
+ commandConfigured: true,
+ accountConfigured: true,
+ passwordConfigured: false,
+ accountLabel: "jd***86",
+ heartbeatQuery: "iPhone 15",
+ checkIntervalMs: 600000,
+ runnerTimeoutMs: 300000,
+ pendingManualAction: false,
+ note: "京东会话已清理。",
+ publicNote: "京东会话由运维后台维护,当前尚未就绪。",
+ session: {
+ configured: false,
+ hasCookie: false,
+ searchApiTemplate: {
+ available: false
+ },
+ detailTemplate: {
+ available: false
+ },
+ reviewsTemplate: {
+ available: false
+ }
+ }
+ }
+ } as any);
+ vi.mocked(clearTmallManagedSession).mockResolvedValue({
+ manager: {
+ status: "idle",
+ enabled: true,
+ heartbeatItemId: "934454505228",
+ checkIntervalMs: 600000,
+ pendingManualAction: false,
+ note: "天猫会话已清理。",
+ publicNote: "天猫会话由运维后台维护,当前尚未就绪。",
+ session: {
+ configured: false,
+ hasCookie: false,
+ detailTemplate: {
+ available: false
+ },
+ reviewsTemplate: {
+ available: false
+ }
+ }
+ }
+ } as any);
+ vi.mocked(updateJdSessionManagerConfig).mockResolvedValue({
+ manager: jdManagerState
+ } as any);
+ vi.mocked(runJdSessionManagerHealthCheck).mockResolvedValue({
+ state: jdManagerState,
+ recovered: false
+ } as any);
+ vi.mocked(runJdSessionManagerRecovery).mockResolvedValue({
+ state: jdManagerState,
+ recovered: true
+ } as any);
+ vi.mocked(runTmallSessionManagerHealthCheck).mockResolvedValue({
+ state: tmallManagerState,
+ recovered: false
+ } as any);
vi.mocked(preparePlatform).mockResolvedValue({
platform: "jd",
session_ready: true,
@@ -136,23 +439,104 @@ describe("task composer and session console", () => {
).toBeInTheDocument();
});
- it("shows session details and allows clearing the current session", async () => {
+ it("runs the jd keyword-only preview loop from the new task page", async () => {
const user = userEvent.setup();
- renderWithProviders(
-
- } path="/sessions/:platform/prepare" />
- ,
- ["/sessions/jd/prepare?from=/tasks/new"]
- );
+ renderWithProviders(
);
- expect(await screen.findByText("已加密保存")).toBeInTheDocument();
- expect(screen.getByText(/完成后将返回:\/tasks\/new/)).toBeInTheDocument();
+ await user.clear(await screen.findByLabelText("商品关键词 / 描述"));
+ await user.type(screen.getByLabelText("商品关键词 / 描述"), "小米手环10");
+ await user.click(screen.getByRole("button", { name: "只用关键词抓京东" }));
+
+ await waitFor(() => {
+ expect(getJdKeywordPreview).toHaveBeenCalledWith("小米手环10", {
+ commentCount: 12,
+ maxPages: 2
+ });
+ });
+
+ expect(await screen.findByText("小米手环10 标准版 智能手环")).toBeInTheDocument();
+ expect(await screen.findByText(/SKU 222222222222/)).toBeInTheDocument();
+ expect(await screen.findByText("好评率 97%")).toBeInTheDocument();
+ expect(await screen.findByText("表带舒适,睡眠监测比较准。")).toBeInTheDocument();
+ });
+
+ it("redirects the tmall prepare route to the unified ops page and clears the managed session", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
, ["/ops/platforms/tmall/prepare?from=/tasks/new"]);
await user.click(screen.getByRole("button", { name: "清理当前会话" }));
await waitFor(() => {
- expect(clearPlatformSession).toHaveBeenCalledWith("jd");
+ expect(clearTmallManagedSession).toHaveBeenCalled();
+ });
+ });
+
+ it("redirects the legacy jd prepare route to the ops session manager page", async () => {
+ renderWithProviders(
, ["/sessions/jd/prepare?from=/tasks/new"]);
+
+ expect(await screen.findByText("京东运维会话管理")).toBeInTheDocument();
+ expect(screen.getByText(/返回业务页面:\/tasks\/new/)).toBeInTheDocument();
+ });
+
+ it("imports jd managed session payload from the ops page", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
, ["/ops/session-manager?platform=jd&from=/tasks/new"]);
+
+ await user.clear(await screen.findByLabelText("Cookie Header"));
+ await user.type(screen.getByLabelText("Cookie Header"), "thor=masked; pin=masked;");
+ await user.type(
+ screen.getByLabelText("Detail Template URL"),
+ "https://api.m.jd.com/?functionId=pc_detailpage_wareBusiness"
+ );
+ await user.type(
+ screen.getByLabelText("Reviews Template URL"),
+ "https://api.m.jd.com/?functionId=getLegoWareDetailComment"
+ );
+ await user.click(screen.getByRole("button", { name: "注入京东会话" }));
+
+ await waitFor(() => {
+ expect(importJdManagedSession).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cookieHeader: "thor=masked; pin=masked;",
+ detailTemplateUrl: "https://api.m.jd.com/?functionId=pc_detailpage_wareBusiness",
+ reviewsTemplateUrl: "https://api.m.jd.com/?functionId=getLegoWareDetailComment"
+ })
+ );
+ });
+ });
+
+ it("imports tmall managed session payload from the unified ops page", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
, ["/ops/session-manager?platform=tmall&from=/tasks/new"]);
+
+ await user.clear(await screen.findByLabelText("Cookie Header"));
+ await user.type(
+ screen.getByLabelText("Cookie Header"),
+ "_m_h5_tk=masked_token_123; cookie2=masked;"
+ );
+ await user.type(
+ screen.getByLabelText("Detail Template URL"),
+ "https://detail.tmall.com/item.htm?id=934454505228"
+ );
+ await user.type(
+ screen.getByLabelText("Reviews Template URL"),
+ "https://h5api.m.tmall.com/h5/mtop.taobao.rate.detaillist.get/6.0/?data=%7B%22auctionNumId%22%3A%22934454505228%22%7D"
+ );
+ await user.click(screen.getByRole("button", { name: "注入天猫会话" }));
+
+ await waitFor(() => {
+ expect(importTmallManagedSession).toHaveBeenCalledWith(
+ expect.objectContaining({
+ cookieHeader: "_m_h5_tk=masked_token_123; cookie2=masked;",
+ detailTemplateUrl: "https://detail.tmall.com/item.htm?id=934454505228",
+ reviewsTemplateUrl:
+ "https://h5api.m.tmall.com/h5/mtop.taobao.rate.detaillist.get/6.0/?data=%7B%22auctionNumId%22%3A%22934454505228%22%7D"
+ })
+ );
});
});
});
diff --git a/apps/web/src/OpsSessionManagerPage.tsx b/apps/web/src/OpsSessionManagerPage.tsx
new file mode 100644
index 0000000..159710d
--- /dev/null
+++ b/apps/web/src/OpsSessionManagerPage.tsx
@@ -0,0 +1,936 @@
+import type { PlatformId } from "@cross-ai/domain";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useEffect, useState } from "react";
+import { useSearchParams } from "react-router-dom";
+
+import {
+ clearJdManagedSession,
+ clearJdSessionManagerConfig,
+ clearTmallManagedSession,
+ clearTmallSessionManagerConfig,
+ getJdLiveSession,
+ getJdSessionManager,
+ getTmallLiveSession,
+ getTmallSessionManager,
+ importJdManagedSession,
+ importTmallManagedSession,
+ runJdSessionManagerHealthCheck,
+ runJdSessionManagerRecovery,
+ runTmallSessionManagerHealthCheck,
+ type JdLiveSessionInput,
+ type JdSessionManagerConfigInput,
+ type TmallLiveSessionInput,
+ type TmallSessionManagerConfigInput,
+ updateJdSessionManagerConfig,
+ updateTmallSessionManagerConfig
+} from "./lib/api";
+
+function Layout(props: { children: React.ReactNode }) {
+ return (
+
+
+
{props.children}
+
+ );
+}
+
+function formatTimestamp(timestamp?: string) {
+ if (!timestamp) {
+ return "暂无";
+ }
+
+ return new Date(timestamp).toLocaleString("zh-CN");
+}
+
+export function OpsSessionManagerPage() {
+ const queryClient = useQueryClient();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const from = searchParams.get("from") ?? "/history";
+ const activePlatform: PlatformId = searchParams.get("platform") === "tmall" ? "tmall" : "jd";
+
+ const [jdConfigDirty, setJdConfigDirty] = useState(false);
+ const [jdConfigForm, setJdConfigForm] = useState
({
+ enabled: true,
+ autoLoginMode: "disabled",
+ loginCommand: "",
+ browserProfilePath: "",
+ heartbeatQuery: "iPhone 15",
+ account: "",
+ password: "",
+ checkIntervalMs: 10 * 60 * 1000,
+ runnerTimeoutMs: 5 * 60 * 1000
+ });
+ const [jdManualSessionForm, setJdManualSessionForm] = useState({
+ cookieHeader: "",
+ userAgent: "",
+ searchApiTemplateUrl: "",
+ detailTemplateUrl: "",
+ reviewsTemplateUrl: "",
+ searchReferer: "",
+ detailReferer: ""
+ });
+
+ const [tmallConfigDirty, setTmallConfigDirty] = useState(false);
+ const [tmallConfigForm, setTmallConfigForm] = useState({
+ enabled: true,
+ heartbeatItemId: "934454505228",
+ checkIntervalMs: 10 * 60 * 1000
+ });
+ const [tmallManualSessionForm, setTmallManualSessionForm] = useState({
+ cookieHeader: "",
+ userAgent: "",
+ detailTemplateUrl: "",
+ reviewsTemplateUrl: "",
+ detailReferer: ""
+ });
+
+ const jdManagerQuery = useQuery({
+ queryKey: ["jd-session-manager"],
+ queryFn: getJdSessionManager
+ });
+ const jdLiveSessionQuery = useQuery({
+ queryKey: ["jd-live-session"],
+ queryFn: getJdLiveSession
+ });
+ const tmallManagerQuery = useQuery({
+ queryKey: ["tmall-session-manager"],
+ queryFn: getTmallSessionManager
+ });
+ const tmallLiveSessionQuery = useQuery({
+ queryKey: ["tmall-live-session"],
+ queryFn: getTmallLiveSession
+ });
+
+ const invalidateJdOpsQueries = async () => {
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ["jd-session-manager"] }),
+ queryClient.invalidateQueries({ queryKey: ["jd-live-session"] }),
+ queryClient.invalidateQueries({ queryKey: ["platform-readiness"] })
+ ]);
+ };
+
+ const invalidateTmallOpsQueries = async () => {
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ["tmall-session-manager"] }),
+ queryClient.invalidateQueries({ queryKey: ["tmall-live-session"] }),
+ queryClient.invalidateQueries({ queryKey: ["platform-readiness"] })
+ ]);
+ };
+
+ const jdSaveConfigMutation = useMutation({
+ mutationFn: (payload: JdSessionManagerConfigInput) => updateJdSessionManagerConfig(payload),
+ onSuccess: async () => {
+ setJdConfigDirty(false);
+ await invalidateJdOpsQueries();
+ }
+ });
+ const jdClearConfigMutation = useMutation({
+ mutationFn: clearJdSessionManagerConfig,
+ onSuccess: async () => {
+ setJdConfigDirty(false);
+ await invalidateJdOpsQueries();
+ }
+ });
+ const jdHealthCheckMutation = useMutation({
+ mutationFn: runJdSessionManagerHealthCheck,
+ onSuccess: invalidateJdOpsQueries
+ });
+ const jdRecoverMutation = useMutation({
+ mutationFn: runJdSessionManagerRecovery,
+ onSuccess: invalidateJdOpsQueries
+ });
+ const jdImportMutation = useMutation({
+ mutationFn: (payload: JdLiveSessionInput) => importJdManagedSession(payload),
+ onSuccess: invalidateJdOpsQueries
+ });
+ const jdClearSessionMutation = useMutation({
+ mutationFn: clearJdManagedSession,
+ onSuccess: invalidateJdOpsQueries
+ });
+
+ const tmallSaveConfigMutation = useMutation({
+ mutationFn: (payload: TmallSessionManagerConfigInput) =>
+ updateTmallSessionManagerConfig(payload),
+ onSuccess: async () => {
+ setTmallConfigDirty(false);
+ await invalidateTmallOpsQueries();
+ }
+ });
+ const tmallClearConfigMutation = useMutation({
+ mutationFn: clearTmallSessionManagerConfig,
+ onSuccess: async () => {
+ setTmallConfigDirty(false);
+ await invalidateTmallOpsQueries();
+ }
+ });
+ const tmallHealthCheckMutation = useMutation({
+ mutationFn: runTmallSessionManagerHealthCheck,
+ onSuccess: invalidateTmallOpsQueries
+ });
+ const tmallImportMutation = useMutation({
+ mutationFn: (payload: TmallLiveSessionInput) => importTmallManagedSession(payload),
+ onSuccess: invalidateTmallOpsQueries
+ });
+ const tmallClearSessionMutation = useMutation({
+ mutationFn: clearTmallManagedSession,
+ onSuccess: invalidateTmallOpsQueries
+ });
+
+ const jdManager = jdManagerQuery.data?.manager;
+ const jdLiveSession = jdLiveSessionQuery.data?.session;
+ const tmallManager = tmallManagerQuery.data?.manager;
+ const tmallLiveSession = tmallLiveSessionQuery.data?.session;
+
+ useEffect(() => {
+ if (!jdManager || jdConfigDirty) {
+ return;
+ }
+
+ setJdConfigForm({
+ enabled: jdManager.enabled,
+ autoLoginMode: jdManager.autoLoginMode,
+ loginCommand: "",
+ browserProfilePath: jdManager.browserProfilePath ?? "",
+ heartbeatQuery: jdManager.heartbeatQuery,
+ account: "",
+ password: "",
+ checkIntervalMs: jdManager.checkIntervalMs,
+ runnerTimeoutMs: jdManager.runnerTimeoutMs
+ });
+ }, [jdConfigDirty, jdManager]);
+
+ useEffect(() => {
+ if (!tmallManager || tmallConfigDirty) {
+ return;
+ }
+
+ setTmallConfigForm({
+ enabled: tmallManager.enabled,
+ heartbeatItemId: tmallManager.heartbeatItemId ?? "",
+ checkIntervalMs: tmallManager.checkIntervalMs
+ });
+ }, [tmallConfigDirty, tmallManager]);
+
+ const switchPlatform = (platform: PlatformId) => {
+ const next = new URLSearchParams(searchParams);
+ next.set("platform", platform);
+ next.set("from", from);
+ setSearchParams(next, { replace: true });
+ };
+
+ const submitJdConfig = () => {
+ const payload: JdSessionManagerConfigInput = {
+ loginCommand: jdConfigForm.loginCommand?.trim() || null,
+ browserProfilePath: jdConfigForm.browserProfilePath?.trim() || null,
+ heartbeatQuery: jdConfigForm.heartbeatQuery?.trim() || null,
+ account: jdConfigForm.account?.trim() || null,
+ password: jdConfigForm.password?.trim() || null,
+ checkIntervalMs: jdConfigForm.checkIntervalMs ?? null,
+ runnerTimeoutMs: jdConfigForm.runnerTimeoutMs ?? null
+ };
+
+ if (typeof jdConfigForm.enabled === "boolean") {
+ payload.enabled = jdConfigForm.enabled;
+ }
+
+ if (jdConfigForm.autoLoginMode) {
+ payload.autoLoginMode = jdConfigForm.autoLoginMode;
+ }
+
+ jdSaveConfigMutation.mutate(payload);
+ };
+
+ const submitJdManualSession = () => {
+ const payload = Object.fromEntries(
+ Object.entries(jdManualSessionForm).flatMap(([key, value]) => {
+ const normalizedValue = value?.trim();
+ return normalizedValue ? [[key, normalizedValue]] : [];
+ })
+ ) as JdLiveSessionInput;
+
+ jdImportMutation.mutate(payload);
+ };
+
+ const submitTmallConfig = () => {
+ const payload: TmallSessionManagerConfigInput = {
+ heartbeatItemId: tmallConfigForm.heartbeatItemId?.trim() || null,
+ checkIntervalMs: tmallConfigForm.checkIntervalMs ?? null
+ };
+
+ if (typeof tmallConfigForm.enabled === "boolean") {
+ payload.enabled = tmallConfigForm.enabled;
+ }
+
+ tmallSaveConfigMutation.mutate(payload);
+ };
+
+ const submitTmallManualSession = () => {
+ const payload = Object.fromEntries(
+ Object.entries(tmallManualSessionForm).flatMap(([key, value]) => {
+ const normalizedValue = value?.trim();
+ return normalizedValue ? [[key, normalizedValue]] : [];
+ })
+ ) as TmallLiveSessionInput;
+
+ tmallImportMutation.mutate(payload);
+ };
+
+ return (
+
+
+ Ops Console
+ 统一运维会话管理
+ 京东和天猫的登录态、模板与健康检查统一在这里维护,普通用户页面只消费任务结果。
+
+
+
+
+ 返回业务页面
+
+
+ {activePlatform === "jd" ? (
+
+
+
+
+
Platform
+
京东运维会话管理
+
+ 京东支持命令式自动恢复。命令成功后输出 JSON,字段与
+ JdLiveSessionInput 一致,Session Manager 会自动导入。
+
+
+
+
+
+
+
Manual
+
手工注入会话
+
+ 如果自动恢复遇到验证码或短信验证,请在运维浏览器里完成后,把最新 Cookie 和模板贴回这里。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
当前模式:ops-session-manager
+
当前平台:京东。普通用户页面只消费结果和任务状态;登录态维护完全由后台运维接管。
+
+
+ Manager Status
+ {jdManager?.status ?? "加载中"}
+
+
+ Public Note
+ {jdManager?.publicNote ?? "加载中"}
+
+
+ Auto Mode
+ {jdManager?.autoLoginMode ?? "disabled"}
+
+
+ Login Command
+ {jdManager?.commandConfigured ? "已配置" : "未配置"}
+
+
+ Account
+ {jdManager?.accountLabel ?? "未配置"}
+
+
+ Live Session
+ {jdLiveSession?.configured ? "已导入" : "未导入"}
+
+
+ Detail Template
+
+ {jdLiveSession?.detailTemplate.available
+ ? `可用${jdLiveSession.detailTemplate.skuId ? ` · SKU ${jdLiveSession.detailTemplate.skuId}` : ""}`
+ : "缺失"}
+
+
+
+ Reviews Template
+
+ {jdLiveSession?.reviewsTemplate.available
+ ? `可用${jdLiveSession.reviewsTemplate.skuId ? ` · SKU ${jdLiveSession.reviewsTemplate.skuId}` : ""}`
+ : "缺失"}
+
+
+
{jdManager?.note ?? "正在读取京东运维状态..."}
+
+ 最近检查:{formatTimestamp(jdManager?.lastCheckAt)} · 最近恢复:
+ {formatTimestamp(jdManager?.lastRecoveredAt)}
+
+ {jdManager?.lastFailureMessage ? (
+
+ {jdManager.lastFailureCode ?? "UNKNOWN"} · {jdManager.lastFailureMessage}
+
+ ) : null}
+
返回业务页面:{from}
+
+ {jdSaveConfigMutation.error instanceof Error ? (
+
{jdSaveConfigMutation.error.message}
+ ) : null}
+ {jdClearConfigMutation.error instanceof Error ? (
+
+ {jdClearConfigMutation.error.message}
+
+ ) : null}
+ {jdHealthCheckMutation.error instanceof Error ? (
+
+ {jdHealthCheckMutation.error.message}
+
+ ) : null}
+ {jdRecoverMutation.error instanceof Error ? (
+
{jdRecoverMutation.error.message}
+ ) : null}
+ {jdImportMutation.error instanceof Error ? (
+
{jdImportMutation.error.message}
+ ) : null}
+ {jdClearSessionMutation.error instanceof Error ? (
+
+ {jdClearSessionMutation.error.message}
+
+ ) : null}
+
+
+ ) : (
+
+
+
+
+
Platform
+
天猫运维会话管理
+
+ 天猫当前不走命令式自动恢复。运维侧负责在浏览器里完成登录和模板刷新,这里统一做巡检配置和手工回注。
+
+
+
+
+
Automation
+
自动巡检配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Manual
+
手工注入会话
+
+ 请在运维浏览器里打开真实商品页和评论页后,把最新 Cookie、详情模板、评论模板贴回这里。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
当前模式:ops-session-manager
+
当前平台:天猫。登录态和模板刷新统一由运维后台负责,用户页不再暴露准备入口。
+
+
+ Manager Status
+ {tmallManager?.status ?? "加载中"}
+
+
+ Public Note
+ {tmallManager?.publicNote ?? "加载中"}
+
+
+ Heartbeat Item
+ {tmallManager?.heartbeatItemId ?? "未配置"}
+
+
+ Check Interval
+ {tmallManager?.checkIntervalMs ?? 0} ms
+
+
+ Live Session
+ {tmallLiveSession?.configured ? "已导入" : "未导入"}
+
+
+ Detail Template
+
+ {tmallLiveSession?.detailTemplate.available
+ ? `可用${tmallLiveSession.detailTemplate.itemId ? ` · item ${tmallLiveSession.detailTemplate.itemId}` : ""}`
+ : "缺失"}
+
+
+
+ Reviews Template
+
+ {tmallLiveSession?.reviewsTemplate.available
+ ? `可用${tmallLiveSession.reviewsTemplate.itemId ? ` · item ${tmallLiveSession.reviewsTemplate.itemId}` : ""}`
+ : "缺失"}
+
+
+
{tmallManager?.note ?? "正在读取天猫运维状态..."}
+
+ 最近检查:{formatTimestamp(tmallManager?.lastCheckAt)} · 最近健康:
+ {formatTimestamp(tmallManager?.lastHealthyAt)}
+
+ {tmallManager?.lastFailureMessage ? (
+
+ {tmallManager.lastFailureCode ?? "UNKNOWN"} · {tmallManager.lastFailureMessage}
+
+ ) : null}
+
返回业务页面:{from}
+
+ {tmallSaveConfigMutation.error instanceof Error ? (
+
+ {tmallSaveConfigMutation.error.message}
+
+ ) : null}
+ {tmallClearConfigMutation.error instanceof Error ? (
+
+ {tmallClearConfigMutation.error.message}
+
+ ) : null}
+ {tmallHealthCheckMutation.error instanceof Error ? (
+
+ {tmallHealthCheckMutation.error.message}
+
+ ) : null}
+ {tmallImportMutation.error instanceof Error ? (
+
+ {tmallImportMutation.error.message}
+
+ ) : null}
+ {tmallClearSessionMutation.error instanceof Error ? (
+
+ {tmallClearSessionMutation.error.message}
+
+ ) : null}
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index a48f339..525c3a4 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -607,6 +607,12 @@ a {
color: var(--text-secondary);
}
+.session-import-form {
+ width: 100%;
+ align-self: stretch;
+ color: var(--text-primary);
+}
+
.session-details {
display: grid;
gap: 12px;
@@ -632,6 +638,47 @@ a {
font-size: 13px;
}
+.quick-preview-panel {
+ display: grid;
+ gap: 18px;
+}
+
+.quick-preview-panel__header {
+ display: flex;
+ align-items: start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.quick-preview-summary {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 16px;
+}
+
+.tag-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.tag-chip {
+ display: inline-flex;
+ align-items: center;
+ min-height: 32px;
+ padding: 0 12px;
+ border-radius: 999px;
+ background: rgba(20, 108, 110, 0.08);
+ color: var(--brand-primary);
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.comment-list {
+ display: grid;
+ gap: 12px;
+}
+
.list {
margin: 0;
padding-left: 18px;
@@ -655,6 +702,7 @@ a {
.history-toolbar,
.field-grid,
.metrics-grid,
+ .quick-preview-summary,
.session-placeholder {
grid-template-columns: 1fr;
}