@sleep2agi/agent-network-dashboard 0.6.3-preview.52 → 0.6.3-preview.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_COMMIT +1 -1
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/diagnostics/route-bundle-stats.json +17 -17
- package/.next/fallback-build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
- package/.next/server/app/admin.html +2 -2
- package/.next/server/app/admin.rsc +2 -2
- package/.next/server/app/admin.segments/_full.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_head.segment.rsc +1 -1
- package/.next/server/app/admin.segments/_index.segment.rsc +2 -2
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/admin.segments/admin.segment.rsc +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +3 -3
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +2 -2
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +2 -2
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +2 -2
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
- package/.next/server/app/messages.html +2 -2
- package/.next/server/app/messages.rsc +2 -2
- package/.next/server/app/messages.segments/_full.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_head.segment.rsc +1 -1
- package/.next/server/app/messages.segments/_index.segment.rsc +2 -2
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/messages.segments/messages.segment.rsc +1 -1
- package/.next/server/app/node/page_client-reference-manifest.js +1 -1
- package/.next/server/app/node.html +2 -2
- package/.next/server/app/node.rsc +3 -3
- package/.next/server/app/node.segments/_full.segment.rsc +3 -3
- package/.next/server/app/node.segments/_head.segment.rsc +1 -1
- package/.next/server/app/node.segments/_index.segment.rsc +2 -2
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/node.segments/node.segment.rsc +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes.html +2 -2
- package/.next/server/app/nodes.rsc +3 -3
- package/.next/server/app/nodes.segments/_full.segment.rsc +3 -3
- package/.next/server/app/nodes.segments/_head.segment.rsc +1 -1
- package/.next/server/app/nodes.segments/_index.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes.segment.rsc +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers.html +2 -2
- package/.next/server/app/providers.rsc +2 -2
- package/.next/server/app/providers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
- package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/server-logs.html +2 -2
- package/.next/server/app/server-logs.rsc +2 -2
- package/.next/server/app/server-logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/_index.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +1 -1
- package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
- package/.next/server/app/servers.html +2 -2
- package/.next/server/app/servers.rsc +2 -2
- package/.next/server/app/servers.segments/_full.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/servers.segments/_index.segment.rsc +2 -2
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/servers.segments/servers.segment.rsc +1 -1
- package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/networks.html +2 -2
- package/.next/server/app/settings/networks.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +1 -1
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens.html +2 -2
- package/.next/server/app/settings/tokens.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +1 -1
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +1 -1
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks.html +2 -2
- package/.next/server/app/tasks.rsc +3 -3
- package/.next/server/app/tasks.segments/_full.segment.rsc +3 -3
- package/.next/server/app/tasks.segments/_head.segment.rsc +1 -1
- package/.next/server/app/tasks.segments/_index.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks.segment.rsc +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0t--g3k._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__06w5i08._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0qn7hvc._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_0ss0pj-._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_tasks_TasksView_tsx_0pnz7dd._.js.map +1 -1
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/{0pg4pce.q5ne_.js → 051a-ut_4_f~3.js} +3 -3
- package/.next/static/chunks/{051p0pw4z~6ag.js → 0hhij-plsekos.js} +1 -1
- package/.next/static/chunks/{0_4ljrbp.stxy.js → 0i465ex5.l573.js} +1 -1
- package/.next/static/chunks/{10px7ehmo7eq0.js → 0jbkmykrnr~q6.js} +1 -1
- package/.next/static/chunks/{0wf03_efeii.w.css → 0lc115vavown3.css} +1 -1
- package/.next/static/chunks/{14x094g58kg61.js → 0n_7389-uuzik.js} +1 -1
- package/.next/static/chunks/{00adqh_rd04uf.js → 0oai8y23-yy20.js} +3 -3
- package/.next/static/chunks/{0lf8e-h91s10h.js → 0p3f6dx723h6w.js} +1 -1
- package/.next/static/chunks/0sy8dlbnq8b.5.js +2 -0
- package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_clientMiddlewareManifest.js +4 -0
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/CHANGELOG.md +143 -0
- package/app/components/ChatPane.tsx +40 -23
- package/app/components/ChatPaneOverflowMenu.tsx +152 -0
- package/app/components/TaskChatPanel.tsx +16 -0
- package/app/tasks/TasksView.tsx +12 -2
- package/package.json +1 -1
- package/proxy.ts +50 -1
- package/tests/e2e-batch-list-actions.spec.ts +6 -0
- package/tests/e2e-chatpane-header-menu.spec.ts +259 -0
- package/tests/e2e-proxy-auth-boundary.spec.ts +196 -0
- package/.next/static/chunks/0-ot~88nectgp.js +0 -2
- /package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_buildManifest.js +0 -0
- /package/.next/static/{RiXBfeE2zY5tzXRmF7kBD → wbJm2a-VxMJqS77WvU7--}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// 通信龙 07-31 SPEC §12 remaining — ChatPane header ⋮/📋/🔍 wire-up.
|
|
6
|
+
//
|
|
7
|
+
// Each test double-asserts (positive + negative) per 通信龙 discipline
|
|
8
|
+
// "接上了(正向)+ 该不生效时不生效(反向)", and each will have a
|
|
9
|
+
// dedicated witnessed-red mutation applied after this file lands green.
|
|
10
|
+
//
|
|
11
|
+
// Covered:
|
|
12
|
+
// ① ⋮ opens overflow menu with 4 items (add tab / view tasks /
|
|
13
|
+
// settings / separator). Menu container reuses SPEC §10 tokens.
|
|
14
|
+
// Reverse: outside click closes the menu.
|
|
15
|
+
// ② 🔍 dispatches chat:open-search → TaskChatPanel search UI shows.
|
|
16
|
+
// Reverse: no click → search UI hidden.
|
|
17
|
+
// ③ 📋 link href resolves to `/tasks?to_name=<alias>` (URL param
|
|
18
|
+
// name must match TasksView's useSearchParams keys — 'to_name',
|
|
19
|
+
// not 'alias').
|
|
20
|
+
// ④ /tasks?from_name=<alias> filters list to matching from_name
|
|
21
|
+
// entries only. Reverse: without the param, list is unfiltered.
|
|
22
|
+
// ⑤ overflow "添加标签页" → row acquires data-node-pinned="true".
|
|
23
|
+
// Reverse: before click, attr is "false".
|
|
24
|
+
|
|
25
|
+
let cachedCookie: string | undefined;
|
|
26
|
+
|
|
27
|
+
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
28
|
+
if (cachedCookie) {
|
|
29
|
+
await context.addCookies([{ name: 'anet_dashboard_session', value: cachedCookie,
|
|
30
|
+
domain: new URL(BASE).hostname, path: '/', httpOnly: true, sameSite: 'Lax' }]);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
34
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
35
|
+
});
|
|
36
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
37
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
38
|
+
if (m) {
|
|
39
|
+
cachedCookie = decodeURIComponent(m[1]);
|
|
40
|
+
await context.addCookies([{ name: 'anet_dashboard_session', value: cachedCookie,
|
|
41
|
+
domain: new URL(BASE).hostname, path: '/', httpOnly: true, sameSite: 'Lax' }]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FIXTURE_SESSIONS = {
|
|
46
|
+
sessions: [
|
|
47
|
+
{ alias: 'node-alpha', status: 'idle', network_id: 'net_x' },
|
|
48
|
+
{ alias: 'node-beta', status: 'idle', network_id: 'net_x' },
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
const FIXTURE_NODES = {
|
|
52
|
+
nodes: [
|
|
53
|
+
{ alias: 'node-alpha', node_id: 'n_a', team: null, tags: [], attrs_revision: 0 },
|
|
54
|
+
{ alias: 'node-beta', node_id: 'n_b', team: null, tags: [], attrs_revision: 0 },
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// For /tasks filter test: 6 tasks total — 2 from node-alpha, 2 from
|
|
59
|
+
// node-beta, 2 with a third from_name. This makes "带 from_name 筛"
|
|
60
|
+
// vs "不带 → 全量" measurably different (2 vs 6).
|
|
61
|
+
const FIXTURE_TASKS_ALL = [
|
|
62
|
+
{ task_id: 't1', from_name: 'node-alpha', to_name: 'node-beta', status: 'replied', content: 'a1', result: 'ok', created_at: '2026-07-31T10:00:00', priority: 'normal' },
|
|
63
|
+
{ task_id: 't2', from_name: 'node-alpha', to_name: 'node-gamma', status: 'delivered', content: 'a2', result: '', created_at: '2026-07-31T10:01:00', priority: 'normal' },
|
|
64
|
+
{ task_id: 't3', from_name: 'node-beta', to_name: 'node-alpha', status: 'acked', content: 'b1', result: '', created_at: '2026-07-31T10:02:00', priority: 'normal' },
|
|
65
|
+
{ task_id: 't4', from_name: 'node-beta', to_name: 'node-alpha', status: 'replied', content: 'b2', result: 'ok', created_at: '2026-07-31T10:03:00', priority: 'normal' },
|
|
66
|
+
{ task_id: 't5', from_name: 'node-omega', to_name: 'node-alpha', status: 'queued', content: 'o1', result: '', created_at: '2026-07-31T10:04:00', priority: 'normal' },
|
|
67
|
+
{ task_id: 't6', from_name: 'node-omega', to_name: 'node-beta', status: 'failed', content: 'o2', result: 'x', created_at: '2026-07-31T10:05:00', priority: 'normal' },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
async function stubHubBase(page: Page) {
|
|
71
|
+
await page.route('**/api/hub/status**', r =>
|
|
72
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_SESSIONS) }));
|
|
73
|
+
await page.route('**/api/hub/nodes**', r =>
|
|
74
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(FIXTURE_NODES) }));
|
|
75
|
+
await page.route('**/api/hub/networks**', r =>
|
|
76
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
77
|
+
body: JSON.stringify({ networks: [{ network_id: 'net_x', network_name: 'net_x' }] }) }));
|
|
78
|
+
await page.route('**/api/hub/health', r =>
|
|
79
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
80
|
+
body: JSON.stringify({ ok: true, version: '0.9.0-test', sse_connections: 0, sse_sessions: {} }) }));
|
|
81
|
+
await page.route('**/api/hub/stats**', r =>
|
|
82
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) }));
|
|
83
|
+
await page.route('**/api/hub/messages**', r =>
|
|
84
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ messages: [] }) }));
|
|
85
|
+
await page.route('**/api/hub/events**', r =>
|
|
86
|
+
r.fulfill({ status: 200, contentType: 'text/event-stream', body: '' }));
|
|
87
|
+
await page.route('**/api/anet/node-config**', r =>
|
|
88
|
+
r.fulfill({ status: 200, contentType: 'application/json',
|
|
89
|
+
body: JSON.stringify({ model: '', flags: {}, channels: [], config_revision: 0 }) }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// /api/hub/tasks stub that honors from_name/to_name query params —
|
|
93
|
+
// mirrors what the real hub does. If our tested URL-param wiring is
|
|
94
|
+
// wrong, the filter won't be applied and the test reds.
|
|
95
|
+
async function stubTasks(page: Page) {
|
|
96
|
+
await page.route('**/api/hub/tasks**', r => {
|
|
97
|
+
const url = new URL(r.request().url());
|
|
98
|
+
const from = url.searchParams.get('from_name');
|
|
99
|
+
const to = url.searchParams.get('to_name');
|
|
100
|
+
let out = FIXTURE_TASKS_ALL.slice();
|
|
101
|
+
if (from) out = out.filter(t => t.from_name === from);
|
|
102
|
+
if (to) out = out.filter(t => t.to_name === to);
|
|
103
|
+
r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ tasks: out, total: out.length }) });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test.describe('ChatPane header — overflow menu / search event / /tasks URL params', () => {
|
|
108
|
+
test('① ⋮ opens overflow menu with 4 items; outside click closes it', async ({ page, context }) => {
|
|
109
|
+
await login(context);
|
|
110
|
+
await stubHubBase(page);
|
|
111
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
112
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
113
|
+
|
|
114
|
+
const more = page.locator('[data-testid="chat-pane-more"]').first();
|
|
115
|
+
await expect(more).toBeVisible();
|
|
116
|
+
|
|
117
|
+
// Reverse: menu is NOT open before we click. If a prior render
|
|
118
|
+
// leaked open state, "click → check open" would still pass.
|
|
119
|
+
await expect(page.locator('[data-testid="chat-pane-overflow-menu"]')).toHaveCount(0);
|
|
120
|
+
|
|
121
|
+
await more.click();
|
|
122
|
+
const menu = page.locator('[data-testid="chat-pane-overflow-menu"]');
|
|
123
|
+
await expect(menu).toBeVisible({ timeout: 3000 });
|
|
124
|
+
|
|
125
|
+
// All 4 menu items are present. Each check pulls a distinct
|
|
126
|
+
// testid — if two collapsed (e.g. only 3 items rendered because
|
|
127
|
+
// one is conditional), this reds precisely on the missing one.
|
|
128
|
+
// Locale-agnostic assertions: Playwright defaults to en-US and
|
|
129
|
+
// t(zh, en) returns en. Accept both — the test is proving "this
|
|
130
|
+
// item exists with a non-empty user-facing label", not policing
|
|
131
|
+
// the exact copy (that job belongs to a separate copy-review).
|
|
132
|
+
await expect(menu.locator('[data-testid="chat-pane-overflow-pin"]')).toHaveText(/添加标签页|移除标签页|Add as tab|Remove tab/);
|
|
133
|
+
await expect(menu.locator('[data-testid="chat-pane-overflow-view-tasks"]')).toHaveText(/查看任务|View tasks/);
|
|
134
|
+
await expect(menu.locator('[data-testid="chat-pane-settings"]')).toHaveText(/会话设置|Conversation settings/);
|
|
135
|
+
await expect(menu.locator('[data-testid="chat-pane-overflow-separator"]')).toHaveCount(1);
|
|
136
|
+
|
|
137
|
+
// Reverse: outside click closes.
|
|
138
|
+
await page.mouse.click(10, 10);
|
|
139
|
+
await expect(page.locator('[data-testid="chat-pane-overflow-menu"]')).toHaveCount(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('② 🔍 dispatches chat:open-search → TaskChatPanel search UI mounts; without click stays hidden', async ({ page, context }) => {
|
|
143
|
+
await login(context);
|
|
144
|
+
await stubHubBase(page);
|
|
145
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
146
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
147
|
+
|
|
148
|
+
// Reverse: search input is NOT present before click. TaskChatPanel
|
|
149
|
+
// has a search input (searchInputRef) that mounts only when
|
|
150
|
+
// searchOpen is true. Its DOM presence is our signal.
|
|
151
|
+
// We look for input[placeholder*="搜索"] scoped to the chat pane.
|
|
152
|
+
const searchInput = page.locator('[data-testid="chat-pane"] input[placeholder*="搜索"], [data-testid="chat-pane"] input[placeholder*="Search"]').first();
|
|
153
|
+
await expect(searchInput).toHaveCount(0);
|
|
154
|
+
|
|
155
|
+
// Confirm the header button is enabled (previously disabled).
|
|
156
|
+
const searchBtn = page.locator('[data-testid="chat-pane-search"]').first();
|
|
157
|
+
await expect(searchBtn).toBeVisible();
|
|
158
|
+
await expect(searchBtn).toBeEnabled();
|
|
159
|
+
await expect(searchBtn).toHaveAttribute('data-search-enabled', 'true');
|
|
160
|
+
|
|
161
|
+
await searchBtn.click();
|
|
162
|
+
// TaskChatPanel schedules focus with setTimeout(50); allow it.
|
|
163
|
+
await expect(searchInput).toBeVisible({ timeout: 3000 });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('③ 📋 view-tasks link points to /tasks?to_name=<alias> (encoded)', async ({ page, context }) => {
|
|
167
|
+
await login(context);
|
|
168
|
+
await stubHubBase(page);
|
|
169
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
170
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
171
|
+
|
|
172
|
+
const link = page.locator('[data-testid="chat-pane-view-tasks"]').first();
|
|
173
|
+
await expect(link).toBeVisible();
|
|
174
|
+
// Assert the EXACT href, not just contains: catches both wrong
|
|
175
|
+
// param name (?alias= would still contain the alias) and wrong
|
|
176
|
+
// scheme/path (something like /tasks/#... won't match).
|
|
177
|
+
const href = await link.getAttribute('href');
|
|
178
|
+
expect(href).toBe('/tasks?to_name=node-alpha');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('④ /tasks reads from_name / to_name URL params → prefills filter inputs', async ({ page, context }) => {
|
|
182
|
+
// The wire we're testing is TasksView.tsx:75-76 —
|
|
183
|
+
// useState(searchParams.get('from_name') || '')
|
|
184
|
+
// The most direct fact-side check is: after navigating to
|
|
185
|
+
// /tasks?from_name=X, the visible "From" input's value === X.
|
|
186
|
+
// We DO NOT depend on the /api/hub/tasks fetch/response
|
|
187
|
+
// (previous version did — /tasks has a client-side "401 →
|
|
188
|
+
// /login" redirect in TasksView.tsx:126 that got tripped by
|
|
189
|
+
// rate-limit flakiness in local dev, red-herring for this
|
|
190
|
+
// wire). The input value assertion catches the exact bug M4
|
|
191
|
+
// targets: silent drop of URL param.
|
|
192
|
+
await login(context);
|
|
193
|
+
await stubHubBase(page);
|
|
194
|
+
await stubTasks(page);
|
|
195
|
+
|
|
196
|
+
// (a) Baseline — no URL param → both inputs empty.
|
|
197
|
+
await page.goto(`${BASE}/tasks`);
|
|
198
|
+
// Filter inputs render early in TasksView (not gated on a
|
|
199
|
+
// successful fetch), so waiting on them avoids the auth-redirect
|
|
200
|
+
// race entirely.
|
|
201
|
+
const fromInput = page.locator('[data-testid="tasks-filter-from"]');
|
|
202
|
+
const toInput = page.locator('[data-testid="tasks-filter-to"]');
|
|
203
|
+
await expect(fromInput).toBeVisible({ timeout: 15000 });
|
|
204
|
+
await expect(fromInput).toHaveValue('');
|
|
205
|
+
await expect(toInput).toHaveValue('');
|
|
206
|
+
|
|
207
|
+
// (b) With ?from_name=node-alpha → From input prefilled, To empty.
|
|
208
|
+
await page.goto(`${BASE}/tasks?from_name=node-alpha`);
|
|
209
|
+
await expect(fromInput).toBeVisible({ timeout: 15000 });
|
|
210
|
+
await expect(fromInput).toHaveValue('node-alpha');
|
|
211
|
+
await expect(toInput).toHaveValue('');
|
|
212
|
+
|
|
213
|
+
// (c) With ?to_name=node-beta → To input prefilled, From empty.
|
|
214
|
+
// This is the direction ChatPane's link uses (`?to_name=<alias>`),
|
|
215
|
+
// so both directions must work — not just one.
|
|
216
|
+
await page.goto(`${BASE}/tasks?to_name=node-beta`);
|
|
217
|
+
await expect(fromInput).toBeVisible({ timeout: 15000 });
|
|
218
|
+
await expect(toInput).toHaveValue('node-beta');
|
|
219
|
+
await expect(fromInput).toHaveValue('');
|
|
220
|
+
|
|
221
|
+
// (d) Both — belt and suspenders.
|
|
222
|
+
await page.goto(`${BASE}/tasks?from_name=node-alpha&to_name=node-beta`);
|
|
223
|
+
await expect(fromInput).toBeVisible({ timeout: 15000 });
|
|
224
|
+
await expect(fromInput).toHaveValue('node-alpha');
|
|
225
|
+
await expect(toInput).toHaveValue('node-beta');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('⑤ overflow "添加标签页" toggles pin (data-node-pinned false→true)', async ({ page, context }) => {
|
|
229
|
+
await login(context);
|
|
230
|
+
await stubHubBase(page);
|
|
231
|
+
// Clear localStorage so we start from unpinned.
|
|
232
|
+
await page.addInitScript(() => {
|
|
233
|
+
try { window.localStorage.removeItem('anet_chat_pin_v1'); } catch {}
|
|
234
|
+
});
|
|
235
|
+
await page.goto(`${BASE}/nodes/node-alpha`);
|
|
236
|
+
await expect(page.locator('[data-testid="chat-pane"]').first()).toBeVisible({ timeout: 15000 });
|
|
237
|
+
|
|
238
|
+
// Premise: node-alpha's rail row starts unpinned (attr === 'false').
|
|
239
|
+
const alphaRow = page.locator('[data-node-list-alias="node-alpha"]').first();
|
|
240
|
+
await expect(alphaRow).toHaveAttribute('data-node-pinned', 'false');
|
|
241
|
+
|
|
242
|
+
await page.locator('[data-testid="chat-pane-more"]').click();
|
|
243
|
+
await expect(page.locator('[data-testid="chat-pane-overflow-menu"]')).toBeVisible({ timeout: 3000 });
|
|
244
|
+
// "添加标签页" — label depends on state; assert menu item then click.
|
|
245
|
+
const pinItem = page.locator('[data-testid="chat-pane-overflow-pin"]');
|
|
246
|
+
// Locale-agnostic: could be zh or en; premise is "starts in the
|
|
247
|
+
// ADD (not REMOVE) state" — we cleared localStorage above so it
|
|
248
|
+
// should be Add. Assert against both language forms.
|
|
249
|
+
await expect(pinItem).toHaveText(/添加标签页|Add as tab/);
|
|
250
|
+
await pinItem.click();
|
|
251
|
+
|
|
252
|
+
// Menu should have auto-closed via onClose in the handler.
|
|
253
|
+
await expect(page.locator('[data-testid="chat-pane-overflow-menu"]')).toHaveCount(0);
|
|
254
|
+
|
|
255
|
+
// 🔴 Core: the rail row's data-node-pinned flipped to 'true'.
|
|
256
|
+
// useSyncExternalStore reacts to the storage event; give it a tick.
|
|
257
|
+
await expect(alphaRow).toHaveAttribute('data-node-pinned', 'true', { timeout: 2000 });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { test, expect, APIRequestContext } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const BASE = process.env.TEST_URL || 'http://localhost:3100';
|
|
4
|
+
|
|
5
|
+
// 07-31 通信龙 catch — proxy.ts:22 matcher was missing `/nodes/:path*`.
|
|
6
|
+
// A no-cookie GET to `/nodes/<alias>` returned 200 with the full 36KB
|
|
7
|
+
// page shell, while every other matcher entry correctly returned
|
|
8
|
+
// 307 → /login.
|
|
9
|
+
//
|
|
10
|
+
// What this test verifies — and doesn't:
|
|
11
|
+
//
|
|
12
|
+
// This spec proves the ROUTE GATE (proxy.ts) redirects no-cookie
|
|
13
|
+
// requests. It does NOT prove authentication: `request.cookies.has()`
|
|
14
|
+
// checks presence only, so any garbage cookie value passes the gate.
|
|
15
|
+
// Real authentication is in /api/hub which validates the token
|
|
16
|
+
// contents when the page tries to fetch. See proxy.ts top comment
|
|
17
|
+
// for the full framing.
|
|
18
|
+
//
|
|
19
|
+
// The class of bug caught here: a matcher-list slip letting a
|
|
20
|
+
// dynamic-child route bypass the "no cookie → /login" bounce and
|
|
21
|
+
// serve page bytes to a visitor who has no cookie at all. Today
|
|
22
|
+
// /nodes/[alias] doesn't do a server-side data fetch (通信龙
|
|
23
|
+
// confirmed with a nonexistent alias — 200 + 36108 bytes, page
|
|
24
|
+
// echoes the alias but hits no DB), so no rows leak. Structural,
|
|
25
|
+
// not sensitive; leaks the moment the page grows a server-side
|
|
26
|
+
// fetch that uses its own credentials.
|
|
27
|
+
//
|
|
28
|
+
// The enumeration IS the contract: if you add a new page route
|
|
29
|
+
// with a dynamic child, add its :path* variant to proxy.ts AND
|
|
30
|
+
// add a representative case to CASES here.
|
|
31
|
+
//
|
|
32
|
+
// Uses playwright.request.newContext (no browser, no cookie state).
|
|
33
|
+
// maxRedirects:0 so we see the 307 directly instead of following it.
|
|
34
|
+
|
|
35
|
+
const CASES: Array<{ path: string; label: string }> = [
|
|
36
|
+
// Static pages in matcher.
|
|
37
|
+
{ path: '/', label: 'root landing' },
|
|
38
|
+
{ path: '/node', label: 'legacy /node singular' },
|
|
39
|
+
{ path: '/tasks', label: '/tasks index' },
|
|
40
|
+
{ path: '/nodes', label: '/nodes index' },
|
|
41
|
+
{ path: '/messages', label: '/messages page' },
|
|
42
|
+
{ path: '/logs', label: '/logs page' },
|
|
43
|
+
{ path: '/settings', label: '/settings index' },
|
|
44
|
+
{ path: '/admin', label: '/admin page' },
|
|
45
|
+
|
|
46
|
+
// Dynamic-child variants — one representative alias per, to prove
|
|
47
|
+
// the `/:path*` glob catches typical values (unicode alias, hex id,
|
|
48
|
+
// slugish segment). If a new dynamic route is added upstream,
|
|
49
|
+
// ADD IT HERE too — the enumeration IS the contract.
|
|
50
|
+
{ path: '/nodes/node-alpha', label: '/nodes/<alias> ascii ← 07-31 regression case' },
|
|
51
|
+
{ path: '/nodes/%E6%94%AF%E4%BB%98%E5%8A%A9%E6%89%8B', label: '/nodes/<支付助手> unicode ← prod-observed alias shape' },
|
|
52
|
+
{ path: '/tasks/abc123', label: '/tasks/<id> hex' },
|
|
53
|
+
{ path: '/settings/general', label: '/settings/<subroute>' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
async function assertRedirectsToLogin(request: APIRequestContext, path: string, label: string) {
|
|
57
|
+
const res = await request.get(`${BASE}${path}`, {
|
|
58
|
+
// Do NOT follow the redirect — we want to see the 307 itself.
|
|
59
|
+
maxRedirects: 0,
|
|
60
|
+
});
|
|
61
|
+
const status = res.status();
|
|
62
|
+
const location = res.headers()['location'] || '';
|
|
63
|
+
const bodyLen = (await res.body()).length;
|
|
64
|
+
|
|
65
|
+
// Bundle both attributes in one assertion so we always see BOTH
|
|
66
|
+
// when it fails — not "status wrong" then discover on next line
|
|
67
|
+
// that location was also wrong. Message spells out the request so
|
|
68
|
+
// a red doesn't force digging into locators.
|
|
69
|
+
const actual = { status, location, bodyLen };
|
|
70
|
+
void actual; // referenced in the assertion below; explicit for readers
|
|
71
|
+
expect({ status, location }, `${label} (${path}) — no-cookie GET must 307 → /login`).toMatchObject({
|
|
72
|
+
status: 307,
|
|
73
|
+
location: expect.stringMatching(/\/login/),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Extra floor: a redirect response body is basically empty
|
|
77
|
+
// (browsers ignore it). If bodyLen > 1024, the server is likely
|
|
78
|
+
// rendering the page anyway and only "suggesting" a redirect via
|
|
79
|
+
// a soft mechanism, which is not what proxy.ts is doing here —
|
|
80
|
+
// that would still be a security regression.
|
|
81
|
+
expect(bodyLen, `${label} redirect body should be small (<1KB), got ${bodyLen}B`).toBeLessThan(1024);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test.describe('proxy.ts cookie-presence gate — no-cookie access redirects to /login', () => {
|
|
85
|
+
test('every matcher-covered path redirects a no-cookie GET to /login (fresh cookieless request context)', async ({ playwright }) => {
|
|
86
|
+
// Fresh, cookie-less request context. Not `page`/`context` from
|
|
87
|
+
// the test fixture, which may have accumulated cookies via test
|
|
88
|
+
// parallelism or a prior test in this session.
|
|
89
|
+
const request = await playwright.request.newContext({
|
|
90
|
+
baseURL: BASE,
|
|
91
|
+
// Explicitly no storageState → no cookies. This is the whole
|
|
92
|
+
// premise of the test.
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
// Sanity — verify the request context has no cookie. If a
|
|
96
|
+
// stray cookie somehow leaks in, our redirects would all
|
|
97
|
+
// become 200 and the loop would falsely pass. Check /nodes
|
|
98
|
+
// first (a matcher-covered path) and confirm 307 before
|
|
99
|
+
// running the enumeration.
|
|
100
|
+
const sanity = await request.get(`${BASE}/nodes`, { maxRedirects: 0 });
|
|
101
|
+
expect(sanity.status(), 'sanity: bare /nodes must 307 with no cookie (else test context has stray cookies)').toBe(307);
|
|
102
|
+
|
|
103
|
+
for (const c of CASES) {
|
|
104
|
+
await assertRedirectsToLogin(request, c.path, c.label);
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
await request.dispose();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('positive direction: with ANY cookie value, matcher-covered routes are NOT 307 (gate lets them through — else we broke logged-in users)', async ({ playwright }) => {
|
|
112
|
+
// Reviewer's ask: the previous two tests only prove "no cookie
|
|
113
|
+
// → gate redirects". A matcher like ['/(.*)'] would also make
|
|
114
|
+
// those pass — while breaking every logged-in flow. This test
|
|
115
|
+
// is the other direction of the two-direction discipline
|
|
116
|
+
// (feedback_verify_both_directions_with_real_data): the gate
|
|
117
|
+
// must LET THROUGH requests that hold the cookie.
|
|
118
|
+
//
|
|
119
|
+
// We assert "NOT 307" (not "== 200") deliberately. The gate is
|
|
120
|
+
// upstream of page rendering — proving the redirect doesn't
|
|
121
|
+
// fire is proving the gate let it pass. Whether the page then
|
|
122
|
+
// renders 200 or 500 or 404 depends on backend state / dev
|
|
123
|
+
// hardlink quirks / route existence — none of which is this
|
|
124
|
+
// gate's job. Assert only what THIS layer controls.
|
|
125
|
+
//
|
|
126
|
+
// Cookie value here is INTENTIONALLY garbage — same as the
|
|
127
|
+
// baseline 通信龙 established:
|
|
128
|
+
// Cookie: anet_dashboard_session=complete_garbage_xyz → 200
|
|
129
|
+
// "Has cookie" is the gate's whole judgment; value doesn't
|
|
130
|
+
// matter to it. Using a garbage value here also documents that
|
|
131
|
+
// fact in the test itself.
|
|
132
|
+
const request = await playwright.request.newContext({
|
|
133
|
+
baseURL: BASE,
|
|
134
|
+
extraHTTPHeaders: {
|
|
135
|
+
cookie: 'anet_dashboard_session=audit-probe-not-a-real-token',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
// Sanity — the cookie header we set is actually attached.
|
|
140
|
+
// If Playwright drops it (e.g. domain mismatch), every "not
|
|
141
|
+
// 307" assertion below would false-pass on a redirect that
|
|
142
|
+
// actually shouldn't happen. So: hit /login (NOT in matcher)
|
|
143
|
+
// with the cookie, expect 307 → / (session redirects logged-
|
|
144
|
+
// in users away from login) OR 200 (the login page itself).
|
|
145
|
+
// Actually cleanest sanity: hit / with cookie and expect
|
|
146
|
+
// NOT 307-to-/login — same shape as the assertions below.
|
|
147
|
+
const sanity = await request.get(`${BASE}/`, { maxRedirects: 0 });
|
|
148
|
+
const sanityLoc = sanity.headers()['location'] || '';
|
|
149
|
+
expect(
|
|
150
|
+
!(sanity.status() === 307 && sanityLoc.includes('/login')),
|
|
151
|
+
`sanity: bare / with cookie must NOT redirect to /login (got ${sanity.status()} → ${sanityLoc}). If it did, the cookie header did not attach and the loop below is meaningless.`,
|
|
152
|
+
).toBe(true);
|
|
153
|
+
|
|
154
|
+
for (const c of CASES) {
|
|
155
|
+
const res = await request.get(`${BASE}${c.path}`, { maxRedirects: 0 });
|
|
156
|
+
const status = res.status();
|
|
157
|
+
const location = res.headers()['location'] || '';
|
|
158
|
+
// "NOT 307 → /login" — either any status other than 307,
|
|
159
|
+
// OR 307 that redirects somewhere other than /login (unlikely
|
|
160
|
+
// but not our concern; a 307 → /foo isn't proxy.ts kicking
|
|
161
|
+
// the user out).
|
|
162
|
+
const bouncedByGate = status === 307 && location.includes('/login');
|
|
163
|
+
expect(
|
|
164
|
+
bouncedByGate,
|
|
165
|
+
`${c.label} (${c.path}) — with cookie MUST NOT be gate-redirected. Got ${status} → ${location || '(no location)'}. This means proxy.ts is rejecting cookie-holders — the matcher is over-broad or the has() check is inverted.`,
|
|
166
|
+
).toBe(false);
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
await request.dispose();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('🔴 07-31 regression: no-cookie /nodes/<alias> MUST redirect (this was reachable in prod v52)', async ({ playwright }) => {
|
|
174
|
+
// Standalone version of the specific case that broke, so a red
|
|
175
|
+
// here anchors the historical incident. If someone loosens the
|
|
176
|
+
// matcher again, this test names the exact prod URL that broke.
|
|
177
|
+
const request = await playwright.request.newContext({ baseURL: BASE });
|
|
178
|
+
try {
|
|
179
|
+
const res = await request.get(`${BASE}/nodes/node-alpha`, { maxRedirects: 0 });
|
|
180
|
+
const status = res.status();
|
|
181
|
+
const location = res.headers()['location'] || '';
|
|
182
|
+
const bodyLen = (await res.body()).length;
|
|
183
|
+
expect({ status, location, bodyLen }).toMatchObject({
|
|
184
|
+
status: 307,
|
|
185
|
+
location: expect.stringMatching(/\/login/),
|
|
186
|
+
});
|
|
187
|
+
// Pre-fix behavior: status 200, bodyLen ~= 36000. Assert an
|
|
188
|
+
// upper floor as belt-and-suspenders (the toMatchObject above
|
|
189
|
+
// catches the primary status miss; this catches "someone made
|
|
190
|
+
// proxy.ts return a soft redirect with a full-body page").
|
|
191
|
+
expect(bodyLen).toBeLessThan(1024);
|
|
192
|
+
} finally {
|
|
193
|
+
await request.dispose();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,35599,e=>{"use strict";var t=e.i(90724),s=e.i(87854),a=e.i(91342),n=e.i(74069),r=e.i(55480),l=e.i(25879);function i({networkId:e,value:a,onChange:n}){let[r,l]=(0,s.useState)("loading"),[c,x]=(0,s.useState)([]),[u,m]=(0,s.useState)(""),[h,p]=(0,s.useState)(!1);if((0,s.useEffect)(()=>{let t=!0;return l("loading"),m(""),fetch(e?`/api/anet/host-supervisors?network_id=${encodeURIComponent(e)}`:"/api/anet/host-supervisors",{cache:"no-store"}).then(e=>e.json().then(t=>({status:e.status,body:t}))).then(({status:e,body:s})=>{if(!t)return;if(501===e||s?.unconfirmed){l("unconfirmed"),m(s?.error||"hub /api/host-supervisors unavailable — upgrade to >=0.9.0-preview.8"),x([]);return}if(!s?.ok){l("error"),m(s?.error||`hub ${e}`),x([]);return}let r=Array.isArray(s.daemons)?s.daemons:[];x(r),l("ready"),1!==r.length||a||n(r[0].daemon_node_id,r[0])}).catch(e=>{t&&(l("error"),m(e instanceof Error?e.message:String(e)),x([]))}),()=>{t=!1}},[e]),"loading"===r)return(0,t.jsxs)("div",{className:"flex items-center gap-2 rounded-md border border-[#26262b] bg-[#0e0e10] px-3 py-3 text-xs text-gray-500",children:[(0,t.jsxs)("svg",{className:"h-3.5 w-3.5 animate-spin",viewBox:"0 0 24 24",fill:"none","aria-hidden":!0,children:[(0,t.jsx)("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),(0,t.jsx)("path",{className:"opacity-90",fill:"currentColor",d:"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"})]}),"正在查询可用的 host_supervisor 节点…"]});if("unconfirmed"===r)return(0,t.jsxs)("div",{className:"rounded-md border border-amber-700/40 bg-amber-900/10 px-3 py-3 text-xs text-amber-200",children:[(0,t.jsx)("div",{className:"font-semibold mb-1",children:"hub 暂未升级"}),(0,t.jsx)("div",{className:"text-amber-200/80",children:u}),(0,t.jsxs)("div",{className:"mt-1 text-[11px] text-amber-200/60",children:["升级到 ",(0,t.jsx)("code",{className:"rounded bg-black/40 px-1 py-0.5",children:"@sleep2agi/commhub-server@0.9.0-preview.8"})," 后选服务器功能才会出现。"]})]});if("error"===r)return(0,t.jsxs)("div",{className:"rounded-md border border-red-700/40 bg-red-900/10 px-3 py-3 text-xs text-red-200",children:["查询 host_supervisor 节点失败:",u]});if(0===c.length)return(0,t.jsxs)("div",{className:"rounded-md border border-[#26262b] bg-[#0e0e10] px-3 py-4 text-xs text-gray-300",children:[(0,t.jsx)("div",{className:"text-sm font-semibold text-gray-200",children:"还没有可用的 host_supervisor 节点"}),(0,t.jsx)("p",{className:"mt-1 text-gray-500",children:"要在某台机器上创建节点,先在那台机器上跑一次 daemon 初始化命令:"}),(0,t.jsx)("pre",{className:"mt-2 overflow-x-auto rounded bg-black/40 p-2 text-[11px] text-cyan-300",children:`# 任选一台目标机器(你的笔记本 / 一台服务器都行)
|
|
2
|
-
anet daemon up my-daemon`}),(0,t.jsx)("p",{className:"mt-2 text-gray-500",children:"注册成功后这里会自动出现,无需刷新。"})]});if(1===c.length&&!h){let e=c[0];return(0,t.jsxs)("div",{className:"rounded-md border border-cyan-700/40 bg-cyan-900/10 px-3 py-3 text-xs text-cyan-100",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"text-cyan-400/80",children:"将在"})," ",(0,t.jsx)("span",{className:"font-semibold",children:e.alias}),(0,t.jsxs)("span",{className:"text-cyan-300/60",children:[" (",e.hostname||"—",") "]}),(0,t.jsx)("span",{className:"text-cyan-400/80",children:"上创建"})]}),(0,t.jsx)("button",{type:"button",onClick:()=>p(!0),className:"text-[11px] text-cyan-400 hover:text-cyan-200 underline-offset-2 hover:underline",children:"详情 →"})]}),(0,t.jsx)(o,{runtimes:e.runtimes_supported,className:"mt-1"})]})}return(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between text-xs text-gray-400",children:[(0,t.jsxs)("span",{children:["选择一台 host_supervisor 节点(",c.length," 台在线)"]}),h&&1===c.length&&(0,t.jsx)("button",{type:"button",onClick:()=>p(!1),className:"text-[11px] text-gray-500 hover:text-gray-300",children:"← 折叠"})]}),(0,t.jsx)("div",{className:"grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3",children:c.map(e=>(0,t.jsx)(d,{daemon:e,selected:e.daemon_node_id===a,onClick:()=>n(e.daemon_node_id,e)},e.daemon_node_id))})]})}function d({daemon:e,selected:s,onClick:a}){let n=e.host_telemetry?.alert_level||"gray";return(0,t.jsxs)("button",{type:"button",onClick:a,"aria-pressed":s,className:`flex w-full flex-col gap-1.5 rounded-md border px-3 py-2.5 text-left text-xs transition-colors ${s?"border-cyan-600 bg-cyan-900/15 text-cyan-100":"border-[#26262b] bg-[#0e0e10] text-gray-300 hover:border-[#3a3a41]"}`,children:[(0,t.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,t.jsx)("span",{className:"truncate font-semibold",children:e.alias}),(0,t.jsx)(c,{level:n})]}),(0,t.jsx)("div",{className:"truncate text-[11px] text-gray-500",children:e.hostname||"—"}),(0,t.jsx)(o,{runtimes:e.runtimes_supported}),(e.host_telemetry?.cpu_cores!=null||e.host_telemetry?.mem_gb!=null)&&(0,t.jsxs)("div",{className:"flex gap-3 text-[10px] text-gray-500",children:[e.host_telemetry?.cpu_cores!=null&&(0,t.jsxs)("span",{children:[e.host_telemetry.cpu_cores," 核"]}),e.host_telemetry?.mem_gb!=null&&(0,t.jsxs)("span",{children:[e.host_telemetry.mem_gb," GB"]})]})]})}function o({runtimes:e,className:s=""}){return e&&0!==e.length?(0,t.jsx)("div",{className:`flex flex-wrap gap-1 ${s}`,children:e.map(e=>(0,t.jsx)("span",{className:"rounded bg-[#1c1c1f] px-1.5 py-0.5 text-[10px] text-gray-400",children:e},e))}):(0,t.jsx)("div",{className:`text-[10px] text-gray-600 ${s}`,children:"runtimes_supported: —"})}function c({level:e}){return(0,t.jsx)("span",{className:`rounded border px-1.5 py-0.5 text-[10px] ${{green:"bg-green-600/20 text-green-300 border-green-700/40",yellow:"bg-amber-600/20 text-amber-300 border-amber-700/40",red:"bg-red-600/20 text-red-300 border-red-700/40",gray:"bg-gray-600/20 text-gray-400 border-gray-700/40"}[e]}`,children:{green:"正常",yellow:"注意",red:"警报",gray:"离线"}[e]})}let x=[{id:"claude-agent-sdk",label:"Claude Agent SDK",models:["deepseek-v4-pro","MiniMax-M3","claude-sonnet-4-6","claude-opus-4-x"],defaultModel:"claude-sonnet-4-6"},{id:"codex-sdk",label:"Codex SDK",models:["gpt-5.5"],defaultModel:"gpt-5.5"},{id:"grok-build-acp",label:"Grok (build-acp)",models:["grok-build"],defaultModel:"grok-build"}],u=["default","acceptEdits","plan","bypassPermissions"],m=["服务器","名字","Runtime","模型","参数","确认"];function h({networkId:e,onClose:a,onCreated:n}){let[r,d]=(0,s.useState)(0),[o,c]=(0,s.useState)(null),[p,g]=(0,s.useState)(null),[b,f]=(0,s.useState)(""),[y,j]=(0,s.useState)(x[0].id),[v,N]=(0,s.useState)(""),[w,k]=(0,s.useState)("default"),[S,C]=(0,s.useState)(""),[_,M]=(0,s.useState)(""),[$,A]=(0,s.useState)(""),[E,L]=(0,s.useState)("form"),[T,I]=(0,s.useState)(""),[B,R]=(0,s.useState)(!1),{networkId:D}=(0,l.useNetworkId)(),U=e||D;(0,s.useEffect)(()=>{let e=e=>{"Escape"===e.key&&"creating"!==E&&a()};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[a,E]),(0,s.useEffect)(()=>{if("done"!==E||B)return;let e=!0,t=0,s=b.trim(),a=async()=>{t+=1;try{let t=U?`/api/hub/status?network_id=${encodeURIComponent(U)}`:"/api/hub/status",a=await fetch(t,{cache:"no-store"}),n=await a.json().catch(()=>({})),r=Array.isArray(n?.sessions)?n.sessions:Array.isArray(n)?n:[];if(e&&r.some(e=>e?.alias===s))return void R(!0)}catch{}e&&t<16&&window.setTimeout(a,1500)},n=window.setTimeout(a,1200);return()=>{e=!1,window.clearTimeout(n)}},[E,B,U,b]);let P=x.find(e=>e.id===y)||x[0],O=b.trim().length>0,W=0===r&&!!o||1===r&&O||r>=2&&r<m.length,z="creating"===E;async function F(){L("creating"),I("");let t=e=>""===e.trim()?void 0:Number(e),s=v||P.defaultModel,a={name:b.trim(),runtime:y,model:s,flags:{permissionMode:w,maxTurns:t(S),budget:t(_),timeout:t($)}};try{let t=await fetch("/api/anet/node-create",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({node_spec:a,...o?{daemon_node_id:o}:{},...e?{network_id:e}:{}})}),s=await t.json().catch(()=>({}));t.ok&&s?.ok?(L("done"),I("创建请求已下发,节点正在拉起…"),n?.(s.result)):s?.unconfirmed?(L("unconfirmed"),I(`后端 create_node 未就绪:${s?.error||"不可用"}(测试 hub 可能未含 #299)`)):(L("error"),I(s?.error?`创建失败:${s.error}`:"创建失败"))}catch(e){L("error"),I(`创建失败:${e instanceof Error?e.message:"网络错误"}`)}}let H="w-full rounded-md border border-[#26262b] bg-[#0e0e10] px-3 py-2 text-sm text-gray-200 focus:border-cyan-600 focus:outline-none";return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("div",{className:"fixed inset-0 bg-black/50 z-40",onClick:a,"aria-hidden":!0}),(0,t.jsxs)("div",{role:"dialog","aria-label":"新建节点",className:"fixed left-1/2 top-1/2 z-50 w-[min(94vw,440px)] md:w-[min(92vw,720px)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-[#26262b] bg-[#161618] shadow-2xl",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between border-b border-[#26262b] px-4 py-3",children:[(0,t.jsx)("div",{className:"text-sm font-semibold text-white",children:"新建节点"}),(0,t.jsx)("button",{onClick:a,"aria-label":"关闭",className:"inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:bg-white/5 hover:text-white",children:(0,t.jsx)("svg",{width:"16",height:"16",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round","aria-hidden":!0,children:(0,t.jsx)("path",{d:"M18 6 6 18M6 6l12 12"})})})]}),(0,t.jsx)("div",{className:"flex items-center gap-1.5 px-4 pt-3",children:m.map((e,s)=>(0,t.jsxs)("div",{className:"flex flex-1 flex-col items-center gap-1",children:[(0,t.jsx)("div",{className:`h-1 w-full rounded-full ${s<=r?"bg-cyan-500":"bg-[#26262b]"}`}),(0,t.jsx)("span",{className:`text-[10px] ${s===r?"text-cyan-300":"text-gray-600"}`,children:e})]},e))}),(0,t.jsx)("div",{className:"min-h-[180px] px-4 py-4",children:"form"!==E?(0,t.jsxs)("div",{className:"flex h-[180px] flex-col items-center justify-center gap-3 text-center",children:[z&&(0,t.jsxs)("svg",{className:"h-7 w-7 animate-spin text-cyan-400",viewBox:"0 0 24 24",fill:"none","aria-hidden":!0,children:[(0,t.jsx)("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),(0,t.jsx)("path",{className:"opacity-90",fill:"currentColor",d:"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"})]}),(0,t.jsx)("div",{className:`text-sm ${"done"===E?"text-green-300":"error"===E?"text-red-300":"unconfirmed"===E?"text-amber-300":"text-gray-300"}`,children:z?"正在创建…":"done"===E&&B?`✓ ${b.trim()} 已上线`:T}),"done"===E&&(B?(0,t.jsx)("p",{className:"text-[11px] text-green-500/80",children:"节点已注册,已出现在节点列表。"}):(0,t.jsxs)("p",{className:"inline-flex items-center gap-1.5 text-[11px] text-gray-500",children:[(0,t.jsxs)("svg",{className:"h-3 w-3 animate-spin",viewBox:"0 0 24 24",fill:"none","aria-hidden":!0,children:[(0,t.jsx)("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),(0,t.jsx)("path",{className:"opacity-90",fill:"currentColor",d:"M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"})]}),"正在监测 ",b.trim()," 注册…(最长 ~24s)"]}))]}):(0,t.jsxs)(t.Fragment,{children:[0===r&&(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"在哪台机器上创建"}),(0,t.jsx)(i,{networkId:U||void 0,value:o,onChange:(e,t)=>{c(e),g(t);let s=t?.runtimes_supported;s&&s.length>0&&!s.includes(y)&&(j(s[0]),N(""))}}),!o&&(0,t.jsxs)("p",{className:"text-[11px] text-gray-600",children:["没选服务器之前无法继续;如果列表是空的,按提示在目标机器上跑 ",(0,t.jsx)("code",{className:"rounded bg-black/40 px-1 py-0.5",children:"anet daemon up <name>"}),"。"]})]}),1===r&&(0,t.jsxs)("label",{className:"block space-y-1.5",children:[(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"节点名字"}),(0,t.jsx)("input",{autoFocus:!0,value:b,onChange:e=>f(e.target.value),placeholder:"例如 my-agent-1",className:H}),!O&&(0,t.jsx)("span",{className:"text-[11px] text-gray-600",children:"名字不能为空"})]}),2===r&&(0,t.jsxs)("div",{className:"space-y-2",children:[(0,t.jsxs)("span",{className:"text-xs text-gray-400",children:["Runtime",p?`(${p.alias} 支持)`:""]}),x.map(e=>{let s=p?.runtimes_supported,a=Array.isArray(s)&&s.length>0&&!s.includes(e.id),n=y===e.id;return(0,t.jsxs)("button",{type:"button",disabled:a,onClick:()=>{a||(j(e.id),N(""))},title:a?`${p?.alias||"该 daemon"} 未声明支持此 runtime`:void 0,className:`flex w-full items-center justify-between rounded-md border px-3 py-2 text-sm transition-colors ${a?"border-[#1c1c1f] bg-[#0a0a0c] text-gray-600 cursor-not-allowed":n?"border-cyan-600 bg-cyan-900/15 text-cyan-200":"border-[#26262b] bg-[#0e0e10] text-gray-300 hover:border-[#3a3a41]"}`,children:[(0,t.jsxs)("span",{children:[e.label,a&&(0,t.jsx)("span",{className:"ml-2 text-[10px] text-gray-700",children:"该 daemon 不支持"})]}),n&&!a&&(0,t.jsx)("svg",{width:"15",height:"15",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,children:(0,t.jsx)("path",{d:"M20 6 9 17l-5-5"})})]},e.id)})]}),3===r&&(0,t.jsxs)("label",{className:"block space-y-1.5",children:[(0,t.jsxs)("span",{className:"text-xs text-gray-400",children:["模型(",P.label,")"]}),(0,t.jsxs)("select",{value:v,onChange:e=>N(e.target.value),className:H,children:[(0,t.jsxs)("option",{value:"",children:["默认(",P.defaultModel,")"]}),P.models.map(e=>(0,t.jsx)("option",{value:e,children:e},e))]})]}),4===r&&(0,t.jsxs)("div",{className:"space-y-3",children:[(0,t.jsxs)("label",{className:"block space-y-1.5",children:[(0,t.jsx)("span",{className:"text-xs text-gray-400",children:"permissionMode"}),(0,t.jsx)("select",{value:w,onChange:e=>k(e.target.value),className:H,children:u.map(e=>(0,t.jsx)("option",{value:e,children:e},e))})]}),(0,t.jsxs)("div",{className:"grid grid-cols-3 gap-2",children:[(0,t.jsxs)("label",{className:"space-y-1",children:[(0,t.jsx)("span",{className:"text-[11px] text-gray-500",children:"maxTurns"}),(0,t.jsx)("input",{inputMode:"numeric",value:S,onChange:e=>C(e.target.value),className:H})]}),(0,t.jsxs)("label",{className:"space-y-1",children:[(0,t.jsx)("span",{className:"text-[11px] text-gray-500",children:"budget"}),(0,t.jsx)("input",{inputMode:"numeric",value:_,onChange:e=>M(e.target.value),className:H})]}),(0,t.jsxs)("label",{className:"space-y-1",children:[(0,t.jsx)("span",{className:"text-[11px] text-gray-500",children:"timeout"}),(0,t.jsx)("input",{inputMode:"numeric",value:$,onChange:e=>A(e.target.value),className:H})]})]})]}),5===r&&(0,t.jsx)("div",{className:"space-y-2 text-sm",children:(0,t.jsx)("div",{className:"rounded-md border border-[#26262b] bg-[#0e0e10] divide-y divide-[#1c1c1f]",children:[["服务器",o?`${o.slice(0,24)}…`:"—"],["名字",b.trim()||"—"],["Runtime",P.label],["模型",v||`默认(${P.defaultModel})`],["permissionMode",w],["maxTurns / budget / timeout",`${S||"—"} / ${_||"—"} / ${$||"—"}`]].map(([e,s])=>(0,t.jsxs)("div",{className:"flex items-start justify-between gap-3 px-3 py-2",children:[(0,t.jsx)("span",{className:"shrink-0 text-gray-500 text-xs",children:e}),(0,t.jsx)("span",{className:"text-right text-gray-200 text-xs break-all",children:s})]},e))})})]})}),"form"===E?(0,t.jsxs)("div",{className:"flex items-center justify-between border-t border-[#26262b] px-4 py-3",children:[(0,t.jsx)("button",{type:"button",onClick:()=>0===r?a():d(r-1),className:"rounded-md px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200",children:0===r?"取消":"上一步"}),r<m.length-1?(0,t.jsx)("button",{type:"button",disabled:!W,onClick:()=>d(r+1),className:`rounded-md px-4 py-1.5 text-sm font-medium ${W?"bg-cyan-600 text-white hover:bg-cyan-500":"bg-[#1c1c1f] text-gray-600 cursor-not-allowed"}`,children:"下一步"}):(0,t.jsx)("button",{type:"button",onClick:F,className:"rounded-md bg-green-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-green-500",children:"创建节点"})]}):(0,t.jsxs)("div",{className:"flex items-center justify-end border-t border-[#26262b] px-4 py-3",children:[(0,t.jsx)("button",{type:"button",onClick:a,className:"rounded-md bg-[#1c1c1f] px-4 py-1.5 text-sm text-gray-200 hover:bg-[#26262b]",children:"done"===E?"完成":"关闭"}),("error"===E||"unconfirmed"===E)&&(0,t.jsx)("button",{type:"button",onClick:()=>L("form"),className:"ml-2 rounded-md bg-cyan-600 px-4 py-1.5 text-sm text-white hover:bg-cyan-500",children:"返回修改"})]})]})]})}var p=e.i(576),g=e.i(3068),b=e.i(57835),f=e.i(30842),y=e.i(56827),j=e.i(25371),v=e.i(99825),N=e.i(54258),w=e.i(32182),k=e.i(23996),S=e.i(519);function C({alias:e,x:a,y:n,onClose:r}){let i=(0,j.usePinned)(e),d=(0,y.useMuted)(e),{networkId:o}=(0,l.useNetworkId)(),{unreadCount:c}=(0,w.useChatUnread)(),x=c(e),u=(0,s.useRef)(null),[m,h]=(0,s.useState)({top:n,left:a,flippedY:!1,flippedX:!1});(0,s.useLayoutEffect)(()=>{let e=u.current;if(!e)return;let t=e.getBoundingClientRect().height||120,s=window.innerHeight,r=window.innerWidth,l=n,i=a,d=!1,o=!1;n+t+8>s&&(l=Math.max(8,n-t),d=!0),a+200+8>r&&(i=Math.max(8,a-200),o=!0),h({top:l=Math.max(8,Math.min(l,s-t-8)),left:i=Math.max(8,Math.min(i,r-200-8)),flippedY:d,flippedX:o})},[a,n]),(0,s.useEffect)(()=>{let e=e=>{u.current&&!u.current.contains(e.target)&&r()},t=e=>{"Escape"===e.key&&(e.stopPropagation(),r())};return document.addEventListener("mousedown",e,!0),document.addEventListener("keydown",t),()=>{document.removeEventListener("mousedown",e,!0),document.removeEventListener("keydown",t)}},[r]);let p=e=>()=>{e(),r()},g="group w-full text-left flex items-center gap-2.5 px-2.5 py-1.5 text-[13px] font-medium text-[#E2E8F0] hover:bg-[rgba(125,211,252,0.10)] hover:text-white active:bg-[rgba(125,211,252,0.18)] disabled:text-[#475569] disabled:cursor-not-allowed disabled:hover:bg-transparent",b=(0,t.jsxs)("div",{ref:u,role:"menu","aria-label":(0,S.t)(`节点 ${e} 操作菜单`,`Actions for node ${e}`),"data-testid":"node-context-menu","data-target-alias":e,"data-flipped-y":m.flippedY?"true":void 0,"data-flipped-x":m.flippedX?"true":void 0,style:{position:"fixed",top:m.top,left:m.left,width:200,zIndex:60},className:"rounded-lg border border-[rgba(255,255,255,0.10)] bg-[#1E293B] py-1",onContextMenu:e=>e.preventDefault(),children:[(0,t.jsxs)("button",{type:"button",role:"menuitem","data-testid":"ctx-pin",onClick:p(()=>(0,j.togglePin)(e)),className:g,children:[(0,t.jsx)(_,{className:"shrink-0 w-5 h-5 text-[#94A3B8] group-hover:text-[#7DD3FC]"}),i?(0,S.t)("取消置顶","Unpin"):(0,S.t)("置顶会话","Pin conversation")]}),(0,t.jsxs)("button",{type:"button",role:"menuitem","data-testid":"ctx-mute",onClick:p(()=>(0,y.toggleMute)(e)),className:g,children:[(0,t.jsx)(M,{muted:!0,className:"shrink-0 w-5 h-5 text-[#94A3B8] group-hover:text-[#7DD3FC]"}),d?(0,S.t)("取消免打扰","Unmute"):(0,S.t)("免打扰","Mute")]}),(0,t.jsxs)("button",{type:"button",role:"menuitem","data-testid":"ctx-mark-read",onClick:p(()=>(0,w.markChatRead)(e,o)),disabled:0===x,className:g,children:[(0,t.jsx)($,{className:"shrink-0 w-5 h-5 text-[#94A3B8] group-hover:text-[#7DD3FC] group-disabled:text-[#475569]"}),(0,S.t)(x>0?`标记已读 (${x})`:"标记已读",x>0?`Mark ${x} read`:"Mark read")]})]});return"u"<typeof document?null:(0,k.createPortal)(b,document.body)}function _({className:e}){return(0,t.jsxs)("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round",children:[(0,t.jsx)("path",{d:"M12 17v5"}),(0,t.jsx)("path",{d:"M5 10l7 7 7-7"}),(0,t.jsx)("path",{d:"M5 10l3-8h8l3 8z"})]})}function M({className:e,muted:s}){return(0,t.jsxs)("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round",children:[(0,t.jsx)("path",{d:"M15 17h5l-1.4-1.4A2 2 0 0118 14.2V11a6 6 0 00-12 0v3.2c0 .5-.2 1-.6 1.4L4 17h5"}),(0,t.jsx)("path",{d:"M13.7 21a2 2 0 01-3.4 0"}),s&&(0,t.jsx)("path",{d:"M3 3l18 18"})]})}function $({className:e}){return(0,t.jsx)("svg",{className:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round",children:(0,t.jsx)("path",{d:"M5 13l4 4L19 7"})})}function A({session:e,selected:s,unread:a,onSelect:n,onContextMenu:r}){let l=(0,j.usePinned)(e.alias),i=(0,y.useMuted)(e.alias);return(0,t.jsxs)("button",{type:"button","data-node-list-item":!0,"data-node-list-alias":e.alias,"data-selected":s?"true":"false","data-node-pinned":l?"true":"false","data-node-muted":i?"true":"false",onClick:()=>n(e.alias),onContextMenu:t=>r(e.alias,t),className:`w-full flex items-center gap-2.5 px-3 py-2.5 text-left transition-colors border-b border-[#1a1a1d] ${s?"bg-cyan-500/8":l?"bg-[rgba(125,211,252,0.04)] hover:bg-[rgba(125,211,252,0.08)]":"hover:bg-[#161618]"}`,children:[(0,t.jsxs)("div",{className:"relative shrink-0",children:[(0,t.jsx)(b.AliasAvatar,{alias:e.alias,size:32}),e.online&&(0,t.jsx)("span",{"aria-hidden":!0,className:"absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full bg-green-400 ring-2 ring-[#0F1320]"})]}),(0,t.jsxs)("div",{className:"flex-1 min-w-0",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between gap-2",children:[(0,t.jsxs)("span",{className:`text-[13px] truncate flex items-center gap-1.5 min-w-0 ${s?"text-cyan-200 font-medium":"text-gray-200"}`,children:[(0,t.jsx)("span",{className:"truncate",children:e.alias}),i&&(0,t.jsxs)("svg",{"aria-label":(0,S.t)("已免打扰","Muted"),"data-mute-icon":!0,width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"#64748B",strokeWidth:1.5,strokeLinecap:"round",strokeLinejoin:"round",className:"shrink-0",children:[(0,t.jsx)("path",{d:"M15 17h5l-1.4-1.4A2 2 0 0118 14.2V11a6 6 0 00-4-5.7"}),(0,t.jsx)("path",{d:"M6 8v3.2c0 .5-.2 1-.6 1.4L4 14h9"}),(0,t.jsx)("path",{d:"M13.7 21a2 2 0 01-3.4 0"}),(0,t.jsx)("path",{d:"M3 3l18 18"})]})]}),a>0&&(0,t.jsx)("span",{"aria-label":i?(0,S.t)(`已免打扰,未读 ${a}`,`Muted, ${a} unread`):(0,S.t)(`${a} 条未读`,`${a} unread`),"data-unread-badge":!0,"data-badge-variant":i?"muted":"default",className:`shrink-0 min-w-[16px] h-4 px-1 rounded-full text-[9px] font-semibold leading-4 text-center ${i?"bg-[#475569] text-[#CBD5E1]":"bg-[#EF4444] text-white"}`,children:a>99?"99+":a})]}),(0,t.jsx)("div",{className:"mt-0.5 flex items-center gap-1.5",children:(0,t.jsx)(E,{alias:e.alias,session:e})})]})]},e.alias)}function E({alias:e,session:s}){return(0,f.useHasDraft)(e)?(0,t.jsxs)("span",{className:"text-[11px] text-red-400/90 truncate","aria-label":(0,S.t)("有未发送草稿","Unsent draft"),children:[(0,S.t)("[草稿]","[draft]")," ",s.task?s.task.slice(0,40):""]}):s.task?(0,t.jsx)("span",{className:"text-[11px] text-gray-500 truncate",children:s.task}):s.online?(0,t.jsx)("span",{className:"text-[11px] text-gray-600",children:(0,S.t)("空闲","idle")}):(0,t.jsx)("span",{className:"text-[11px] text-gray-600 italic",children:(0,S.t)("离线","offline")})}function L({sessions:e,selectedAlias:a,onSelect:n,search:r,onSearchChange:l}){let{unreadCount:i}=(0,w.useChatUnread)(),[d,o]=(0,s.useState)(null),c=(0,s.useRef)(null),x=(0,s.useRef)({sessions:e,selectedAlias:a,onSelect:n});return(0,s.useEffect)(()=>{x.current={sessions:e,selectedAlias:a,onSelect:n}}),(0,s.useEffect)(()=>{let e=e=>{if("ArrowUp"!==e.key&&"ArrowDown"!==e.key&&"Enter"!==e.key)return;let t=document.activeElement;if(t){let e=t.tagName;if("INPUT"===e||"TEXTAREA"===e||"SELECT"===e||t.isContentEditable)return}let{sessions:s,selectedAlias:a,onSelect:n}=x.current;if(0===s.length)return;let r=a?s.findIndex(e=>e.alias===a):-1;if("Enter"===e.key){-1===r&&(e.preventDefault(),n(s[0].alias));return}e.preventDefault();let l=-1===r?"ArrowDown"===e.key?0:s.length-1:"ArrowDown"===e.key?Math.min(s.length-1,r+1):Math.max(0,r-1);l!==r&&n(s[l].alias)};return window.addEventListener("keydown",e),()=>window.removeEventListener("keydown",e)},[]),(0,s.useEffect)(()=>{if(!a)return;let e=c.current?.querySelector(`[data-node-list-alias="${CSS.escape(a)}"]`);e&&e.scrollIntoView({block:"nearest"})},[a]),(0,t.jsxs)("div",{"data-node-list-rail":!0,"data-node-list-count":e.length,className:"flex flex-col w-full overflow-hidden bg-[#0F1320] border-r border-[rgba(255,255,255,0.06)]","aria-label":(0,S.t)("节点列表","Node list"),children:[(0,t.jsx)("div",{className:"p-2 border-b border-[#1e1e22] shrink-0",children:(0,t.jsx)("input",{type:"search",value:r,onChange:e=>l(e.target.value),placeholder:(0,S.t)("搜索节点(拼音可)","Search nodes (pinyin ok)"),"aria-label":(0,S.t)("搜索节点","Search nodes"),"data-testid":"rail-search",className:"w-full rounded-md bg-[#0A0D18] border border-[rgba(255,255,255,0.08)] px-2.5 py-1.5 text-[12px] text-gray-200 placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/40"})}),(0,t.jsx)("div",{className:"px-3 py-2 text-[11px] text-gray-500 tracking-wide uppercase border-b border-[#1e1e22] shrink-0",children:(0,S.t)(`节点 ${e.length}`,`Nodes \xb7 ${e.length}`)}),d&&(0,t.jsx)(C,{alias:d.alias,x:d.x,y:d.y,onClose:()=>o(null)}),(0,t.jsx)("div",{ref:c,"data-testid":"node-list-scroll",className:"flex-1 min-h-0 overflow-y-auto pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0",children:0===e.length?r?(0,t.jsxs)("div",{className:"p-4 text-[11px] text-gray-500","data-testid":"rail-search-empty",children:[(0,S.t)(`没有节点匹配 “${r}”`,`No match — no nodes named "${r}"`),(0,t.jsx)("button",{type:"button",onClick:()=>l(""),className:"ml-2 text-cyan-400/80 hover:text-cyan-300 underline-offset-2 hover:underline",children:(0,S.t)("清除","Clear")})]}):(0,t.jsx)("div",{className:"p-4 text-[11px] text-gray-600",children:(0,S.t)("列表为空 —— 右侧的筛选控件或空态说明会告诉你原因。","Rail is empty — the filter controls or empty state on the right explain why.")}):e.map(e=>(0,t.jsx)(A,{session:e,selected:e.alias===a,unread:i(e.alias),onSelect:n,onContextMenu:(e,t)=>{t.preventDefault(),o({alias:e,x:t.clientX,y:t.clientY})}},e.alias))})]})}var T=e.i(96650),I=e.i(58895);function B({alias:e,onClose:a,variant:r="column",onBack:l}){let[i,d]=(0,s.useState)(!1),{sessions:o}=(0,n.useSessions)(),c=o.find(t=>t.alias===e);return(0,t.jsxs)("aside",{"data-testid":"chat-pane","data-chat-alias":e,className:"page"===r?"relative flex flex-col w-full min-w-0 h-full bg-[#141826] pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0":"relative hidden md:flex md:flex-col flex-1 min-w-0 h-full bg-[#141826] pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-0",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between gap-2 px-3 py-2.5 border-b border-[rgba(255,255,255,0.06)] bg-[#141826] shrink-0",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 min-w-0",children:["page"===r&&(0,t.jsx)("button",{type:"button",onClick:l??a,"aria-label":(0,S.t)("返回列表","Back to list"),"data-testid":"chat-back",className:"inline-flex h-8 w-8 -ml-1 items-center justify-center rounded-lg text-gray-400 hover:text-gray-100 hover:bg-[#1c1c1f] transition-colors shrink-0",children:(0,t.jsx)("svg",{className:"w-5 h-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:2,children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 19l-7-7 7-7"})})}),(0,t.jsx)(b.AliasAvatar,{alias:e,size:26}),(0,t.jsx)("span",{className:"truncate text-sm font-semibold text-white",children:e})]}),(0,t.jsxs)("div",{className:"flex items-center gap-1 shrink-0",children:[(0,t.jsx)("button",{type:"button",onClick:()=>d(!0),"aria-label":(0,S.t)("打开会话设置","Open conversation settings"),title:(0,S.t)("会话设置","Conversation settings"),"data-testid":"chat-pane-settings","data-chat-pane-more":!0,className:"inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:text-gray-200 hover:bg-[#1c1c1f] transition-colors",children:(0,t.jsxs)("svg",{className:"w-4 h-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:2,children:[(0,t.jsx)("circle",{cx:"12",cy:"5",r:"1.5",fill:"currentColor"}),(0,t.jsx)("circle",{cx:"12",cy:"12",r:"1.5",fill:"currentColor"}),(0,t.jsx)("circle",{cx:"12",cy:"19",r:"1.5",fill:"currentColor"})]})}),(0,t.jsx)("a",{href:`/tasks?alias=${encodeURIComponent(e)}`,"aria-label":(0,S.t)(`查看 ${e} 的任务`,`View ${e} tasks`),title:(0,S.t)("查看任务","View tasks"),"data-testid":"chat-pane-view-tasks",className:"inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:text-gray-200 hover:bg-[#1c1c1f] transition-colors",children:(0,t.jsx)("svg",{className:"w-4 h-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:1.5,children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8 7V3M16 7V3M4 11h16M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"})})}),(0,t.jsx)("button",{type:"button",disabled:!0,"aria-label":(0,S.t)("会话内搜索(即将支持)","In-chat search (coming soon)"),title:(0,S.t)("即将支持","Coming soon"),"data-testid":"chat-pane-search",className:"inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-700 cursor-not-allowed",children:(0,t.jsx)("svg",{className:"w-4 h-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:1.5,children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"})})}),(0,t.jsx)("button",{type:"button",onClick:a,"aria-label":(0,S.t)("关闭会话","Close chat"),"data-testid":"chat-pane-close",className:"inline-flex h-8 w-8 items-center justify-center rounded-md text-gray-500 hover:text-gray-200 hover:bg-[#1c1c1f] transition-colors",children:(0,t.jsx)("svg",{className:"w-4 h-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",strokeWidth:2,children:(0,t.jsx)("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M6 18L18 6M6 6l12 12"})})})]})]}),(0,t.jsx)("div",{className:"flex-1 min-h-0",children:(0,t.jsx)(T.TaskChatPanel,{alias:e,onClose:a,inline:!0})}),i&&c&&(0,t.jsx)(I.NodeSettingsPanel,{session:c,onClose:()=>d(!1),positioning:"absolute"})]})}var R=e.i(90991);function D(e){return e?e.length>12?`${e.slice(0,8)}…`:e:"—"}function U(e){try{return decodeURIComponent(e)}catch{return e}}let P="(min-width: 768px)";function O(e){let t=window.matchMedia(P);return t.addEventListener("change",e),()=>t.removeEventListener("change",e)}function W({alias:e}){return(0,f.useHasDraft)(e)?(0,t.jsx)("span",{className:"shrink-0 text-[9px] text-red-400/90","aria-label":(0,S.t)("有未发送草稿","Unsent draft"),children:(0,S.t)("[草稿]","[draft]")}):null}e.s(["NodesView",0,function({initialAlias:e=null}){let l,{sessions:i,isLoading:d}=(0,n.useSessions)(),{health:o}=(0,n.useHealth)(),{lifecycleByAlias:c,nodeIdByAlias:x,attrsByAlias:u}=(0,n.useNodeLifecycle)(),{hasUnread:m,unreadCount:f,lastActivityAt:k}=(0,w.useChatUnread)(),{mutate:C}=(0,r.useSWRConfig)(),_=()=>{C(e=>"string"==typeof e&&(e.startsWith("/api/hub/nodes")||e.startsWith("/api/hub/status")))},M=o?.sse_sessions||{},$=e=>(e.network_id?M[`${e.network_id}:${e.alias}`]:void 0)??M[e.alias],[A,E]=(0,s.useState)(""),[T,I]=(0,s.useState)(""),[z,F]=(0,s.useState)([]),[H,V]=(0,s.useState)(""),K=Array.from(new Set(i.map(e=>u[e.alias]?.team).filter(e=>!!e&&e.length>0))).sort(),q=Array.from(new Set(i.flatMap(e=>u[e.alias]?.tags??[]).filter(e=>e&&e.length>0))).sort(),X=K.length>0||q.length>0;(0,v.usePinyinReady)(),(0,y.useMuteVersion)(),(0,j.usePinVersion)();let[Y,G]=(0,s.useState)("list"),[J,Q]=(0,s.useState)(e),Z=(0,s.useSyncExternalStore)(O,()=>window.matchMedia(P).matches,()=>!0),ee=e=>{try{let e=document.querySelector('[data-testid="node-list-scroll"]');e&&(eN.current=e.scrollTop)}catch{}Q(e);try{window.history.pushState({},"",`/nodes/${encodeURIComponent(e)}`)}catch{}},et=()=>{el.current=!0,Q(null);try{window.history.pushState({},"","/nodes")}catch{}};(0,s.useEffect)(()=>{let e=()=>{let e=window.location.pathname.match(/^\/nodes\/([^/]+)\/?$/);Q(e?U(e[1]):null)};return window.addEventListener("popstate",e),()=>window.removeEventListener("popstate",e)},[]);let es=(0,s.useRef)(null);(0,s.useEffect)(()=>{let e=es.current;if(!e)return;let t=()=>{let t=e.getBoundingClientRect().top+window.scrollY;e.style.height=`${Math.max(240,window.innerHeight-t)}px`};t(),window.addEventListener("resize",t);let s=new ResizeObserver(t);return s.observe(document.body),()=>{window.removeEventListener("resize",t),s.disconnect()}},[]);let[ea,en]=(0,s.useState)(()=>"1"===window.localStorage.getItem("anet-nodes-manage-view")),er=(0,s.useRef)(!1),el=(0,s.useRef)(!1),ei=()=>{en(e=>{let t=!e;try{window.localStorage.setItem("anet-nodes-manage-view",t?"1":"0")}catch{}return t?(el.current=!0,J&&et()):(el.current=!1,er.current=!1),t})},[ed,eo]=(0,s.useState)(!1),[ec,ex]=(0,s.useState)(()=>{let e=window.localStorage.getItem("anet-nodes-rail");return null===e||"0"!==e}),eu=(0,N.useCollapsibleSearch)({value:H,onChange:V,placeholder:(0,S.t)("搜索节点(支持拼音/首字母)…","Search nodes (pinyin ok)…"),label:"Search nodes",enabled:i.length>0}),em=(0,s.useRef)(new Map),eh=Date.now(),ep=(e,t)=>(t&&em.current.set(e,eh),+(eh-(em.current.get(e)||0)<6e4)),eg=i.map(e=>({...e,online:!!$(e)})).filter(e=>"online"===A?e.online:"offline"===A?!e.online:!A||A===e.status).filter(e=>{if(!T)return!0;let t=u[e.alias]?.team??null;return"__NO_TEAM__"===T?null===t||""===t:t===T}).filter(e=>{if(0===z.length)return!0;let t=u[e.alias]?.tags??[];return z.every(e=>t.includes(e))}).filter(e=>!H||(0,v.pinyinMatch)(e.alias,H)||(0,v.pinyinMatch)(e.agent||"",H)).sort((e,t)=>!!(0,j.isPinned)(t.alias)-!!(0,j.isPinned)(e.alias)||ep(t.alias,t.online)-ep(e.alias,e.online)||k(t.alias)-k(e.alias)),eb=i.filter(e=>$(e)).length,ef=eg.length>0?eg[0].alias:null;(0,s.useEffect)(()=>{if(Z&&!ea&&!J&&!el.current&&!er.current&&ef){er.current=!0,Q(ef);try{window.history.replaceState({},"",`/nodes/${encodeURIComponent(ef)}`)}catch{}}},[Z,ea,J,ef]);let ey=!!J&&Z&&i.length>0,ej=!Z&&!J&&!ea&&i.length>0,ev=!Z&&!!J,eN=(0,s.useRef)(0);return(0,s.useEffect)(()=>{if(Z||J)return;let e=requestAnimationFrame(()=>{let e=document.querySelector('[data-testid="node-list-scroll"]');e&&(e.scrollTop=eN.current)});return()=>cancelAnimationFrame(e)},[Z,J]),(0,t.jsxs)("div",{ref:es,"data-testid":"nodes-layout","data-rail-visible":ec?"true":"false",className:"h-[100dvh] max-w-full overflow-hidden bg-[#0b0b0d] text-gray-100 flex",children:[(ej||Z&&(ec||ey)&&i.length>0)&&(0,t.jsxs)("aside",{"data-testid":"node-list-rail",className:"flex flex-col w-full md:w-[280px] shrink-0 h-full min-h-0 bg-[#0F1320]",children:[(0,t.jsx)(L,{sessions:eg,selectedAlias:J,onSelect:ee,search:H,onSearchChange:V}),(0,t.jsx)("button",{type:"button",onClick:ei,"data-testid":"manage-view-toggle",className:"shrink-0 border-t border-[rgba(255,255,255,0.06)] bg-[#0F1320] px-3 py-2 text-left text-[11px] text-gray-500 hover:text-gray-200 transition-colors",children:ea?(0,S.t)("≡ 对话视图","≡ Conversation view"):(0,S.t)("☷ 管理视图","☷ Manage view")})]}),(Z?!ey:ea&&!J)&&(0,t.jsxs)("div",{className:"flex-1 min-w-0 max-w-full h-full overflow-y-auto overflow-x-hidden p-4 sm:p-6 pb-[calc(5rem+env(safe-area-inset-bottom))] lg:pb-6",children:[!Z&&ea&&(0,t.jsx)("button",{type:"button",onClick:ei,"data-testid":"manage-view-toggle-mobile",className:"md:hidden mb-3 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#0F1320] px-3 py-1.5 text-[12px] text-gray-400 hover:text-gray-200",children:(0,S.t)("≡ 返回对话视图","≡ Back to conversations")}),(0,t.jsxs)("div",{className:"flex items-center gap-4 mb-4 sm:mb-6",children:[(0,t.jsxs)("div",{className:"flex items-center gap-3 min-w-0 flex-1",children:[(0,t.jsx)("h1",{className:"text-2xl font-bold text-white lg:ml-0 ml-10",children:"Nodes"}),(0,t.jsxs)("span",{className:"text-xs bg-green-900/30 text-green-400 px-2 py-0.5 rounded-full border border-green-800/30 shrink-0",children:[eb," online"]}),(0,t.jsxs)("span",{className:"text-xs bg-gray-900/30 text-gray-400 px-2 py-0.5 rounded-full border border-gray-800/30 shrink-0",children:[i.length," total"]})]}),(0,t.jsxs)("div",{className:"flex items-center gap-2 shrink-0",children:[(0,t.jsxs)("button",{type:"button",onClick:()=>{ex(e=>{let t=!e;try{window.localStorage.setItem("anet-nodes-rail",t?"1":"0")}catch{}return t})},"aria-pressed":ec,"data-testid":"toggle-rail",title:ec?(0,S.t)("隐藏节点列表","Hide node list"):(0,S.t)("显示节点列表","Show node list"),className:"hidden sm:inline-flex items-center gap-1 rounded-lg border border-[#26262b] bg-[#161618] px-2.5 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:border-[#3a3a40] transition-colors",children:[(0,t.jsx)("span",{"aria-hidden":!0,className:"text-sm leading-none",children:ec?"☷":"☰"}),(0,t.jsx)("span",{className:"hidden md:inline",children:ec?(0,S.t)("密视图","Dense"):(0,S.t)("分栏","Split")})]}),(0,t.jsxs)("button",{type:"button",onClick:()=>eo(!0),"aria-label":"新建节点",className:"inline-flex items-center gap-1 rounded-lg border border-cyan-700/50 bg-cyan-900/15 px-3 py-1.5 text-sm font-medium text-cyan-300 hover:bg-cyan-900/25 transition-colors",children:[(0,t.jsx)("span",{"aria-hidden":!0,className:"text-base leading-none",children:"+"}),(0,t.jsx)("span",{className:"hidden sm:inline",children:"新建节点"})]}),(0,t.jsx)(eu.Button,{})]})]}),ed&&(0,t.jsx)(h,{onClose:()=>eo(!1)}),(0,t.jsx)(eu.Row,{}),i.length>0&&(0,t.jsxs)("div",{className:"flex flex-wrap gap-3 mb-4 sm:mb-6",children:[(0,t.jsxs)("select",{value:A,onChange:e=>E(e.target.value),className:"bg-[#161618] border border-[#26262b] rounded-lg px-3 py-2 text-base sm:text-sm text-white focus:border-blue-500/50 focus:outline-none",children:[(0,t.jsx)("option",{value:"",children:"All"}),(0,t.jsx)("option",{value:"online",children:"Online"}),(0,t.jsx)("option",{value:"offline",children:"Offline"}),(0,t.jsx)("option",{value:"working",children:"Working"}),(0,t.jsx)("option",{value:"idle",children:"Idle"}),(0,t.jsx)("option",{value:"blocked",children:"Blocked"}),(0,t.jsx)("option",{value:"error",children:"Error"})]}),K.length>0&&(0,t.jsxs)("select",{value:T,onChange:e=>I(e.target.value),"aria-label":(0,S.t)("按 Team 过滤","Filter by team"),"data-testid":"filter-team",className:"bg-[#161618] border border-[#26262b] rounded-lg px-3 py-2 text-base sm:text-sm text-white focus:border-blue-500/50 focus:outline-none",children:[(0,t.jsx)("option",{value:"",children:(0,S.t)("全部 Team","All teams")}),(0,t.jsx)("option",{value:"__NO_TEAM__",children:(0,S.t)("未分组","(no team)")}),K.map(e=>(0,t.jsx)("option",{value:e,children:e},e))]}),(0,t.jsx)("div",{className:"hidden sm:flex ml-auto rounded-lg border border-[#26262b] bg-[#161618] p-1 text-sm",children:["list","grid"].map(e=>(0,t.jsx)("button",{type:"button",onClick:()=>G(e),className:`rounded-md px-3 py-1.5 transition-colors ${Y===e?"bg-cyan-500/10 text-cyan-300":"text-gray-500 hover:text-gray-200"}`,children:"list"===e?"List":"Grid"},e))})]}),i.length>0&&q.length>0&&(0,t.jsxs)("div",{className:"mb-4 sm:mb-6 flex flex-wrap items-center gap-1.5","data-testid":"filter-tags-row",children:[(0,t.jsx)("span",{className:"text-[11px] text-gray-500 mr-1",children:(0,S.t)("Tags:","Tags:")}),q.map((e,s)=>{let a=z.includes(e);return a&&z.slice(0,z.indexOf(e)).length,(0,t.jsx)("button",{type:"button",onClick:()=>F(t=>t.includes(e)?t.filter(t=>t!==e):[...t,e]),"aria-pressed":a,"data-tag":e,"data-selected":a?"true":"false",className:`text-xs rounded-full px-2.5 py-1 border transition-colors ${a?"bg-cyan-500/15 text-cyan-200 border-cyan-500/40":"bg-[#161618] text-gray-400 border-[#26262b] hover:text-gray-200 hover:border-[#3a3a40]"}`,children:e},e)}),z.length>=2&&(0,t.jsx)("span",{className:"text-[10px] text-cyan-300/80 ml-1 tabular-nums","data-testid":"filter-tags-and-hint",children:(0,S.t)(`= 同时具有 ${z.length} 个标签`,`= AND of ${z.length} tags`)}),(z.length>0||T)&&(0,t.jsx)("button",{type:"button",onClick:()=>{F([]),I("")},className:"text-[11px] text-gray-500 hover:text-gray-200 underline-offset-2 hover:underline ml-2","data-testid":"clear-attr-filters",children:(0,S.t)("清除","Clear")})]}),d?(0,t.jsx)("div",{className:"animate-pulse space-y-2",children:[1,2,3,4].map(e=>(0,t.jsx)("div",{className:"h-16 bg-gray-800/20 rounded-lg"},e))}):0===eg.length?0===i.length?(0,t.jsx)(g.NodesEmptyState,{}):T||z.length>0?(0,t.jsx)(g.EmptyState,{variant:"nodes",title:(0,S.t)("没有节点符合当前属性筛选","No nodes match the current attribute filters"),sub:(l=[],"__NO_TEAM__"===T?l.push((0,S.t)("未分组","no-team")):T&&l.push((0,S.t)(`Team = ${T}`,`Team = ${T}`)),1===z.length?l.push((0,S.t)(`标签 ${z[0]}`,`tag ${z[0]}`)):z.length>=2&&l.push((0,S.t)(`同时具有标签 ${z.join(" 和 ")}`,`all of tags: ${z.join(" AND ")}`)),(0,S.t)(`没有节点满足:${l.join(" + ")}。清除筛选或减少标签。`,`No node satisfies: ${l.join(" + ")}. Clear filters or drop a tag.`))}):!X&&(A||H)?(0,t.jsx)(g.EmptyState,{variant:"nodes",title:(0,S.t)("还没有节点被打过标签","No nodes have been tagged yet"),sub:(0,S.t)("先在节点详情里给几个节点分配 team / tag,然后就可以按属性筛选。","Assign team / tag to a few nodes first, then attribute filters will show up.")}):(0,t.jsx)(g.EmptyState,{variant:"nodes",title:"No nodes match your filters",sub:"Try clearing search or status filters, or wait for an agent to register."}):"grid"===Y?(0,t.jsx)("div",{className:"grid min-w-0 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",children:eg.map(e=>{let s=e.online?e.status:"offline",n="number"==typeof e.progress?e.progress:0,r=m(e.alias);return(0,t.jsxs)("div",{role:"button",tabIndex:0,"data-node-card":!0,"data-node-alias":e.alias,onClick:()=>ee(e.alias),onKeyDown:t=>{("Enter"===t.key||" "===t.key)&&(t.preventDefault(),ee(e.alias))},className:`relative min-w-0 max-w-full overflow-hidden rounded-xl border border-[#26262b] bg-[#161618] p-4 transition-colors hover:border-cyan-500/40 cursor-pointer ${!e.online?"opacity-60":""}`,children:[!e.online&&(0,t.jsx)("div",{className:"pointer-events-none absolute inset-0 flex items-center justify-center",children:(0,t.jsx)("span",{className:"rotate-[-20deg] rounded-xl border border-white/10 bg-black/20 px-5 py-2 text-xl font-bold uppercase tracking-[0.35em] text-white/10",children:"Offline"})}),(0,t.jsxs)("div",{className:"flex items-start gap-3",children:[(0,t.jsxs)("div",{className:"relative shrink-0",children:[(0,t.jsxs)("span",{className:"relative inline-flex shrink-0",children:[(0,t.jsx)(b.AliasAvatar,{alias:e.alias,size:36}),f(e.alias)>0&&(0,y.isMuted)(e.alias)&&(0,t.jsx)("span",{"aria-label":(0,S.t)("有未读(已静音)","Unread (muted)"),className:"absolute -top-0.5 -right-1 h-2.5 w-2.5 rounded-full bg-red-500/80 ring-2 ring-[#161618]"}),f(e.alias)>0&&!(0,y.isMuted)(e.alias)&&(0,t.jsx)("span",{"aria-label":(0,S.t)(`${f(e.alias)} 条未读`,`${f(e.alias)} unread`),className:"absolute -top-1 -right-1.5 min-w-[16px] h-4 px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-4 text-center ring-2 ring-[#161618]",children:f(e.alias)>99?"99+":f(e.alias)})]}),r&&(0,t.jsx)("span",{className:"absolute -right-1 -top-1 h-3 w-3 rounded-full border-2 border-[#161618] bg-red-500"})]}),(0,t.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:"truncate text-base font-semibold text-white",children:e.alias}),(0,t.jsx)(W,{alias:e.alias}),r&&(0,t.jsx)("span",{className:"shrink-0 rounded-full bg-red-500/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-red-300",children:"New"}),(0,t.jsx)("span",{className:`shrink-0 rounded-md border px-2 py-0.5 text-[10px] uppercase tracking-wide ${R.SESSION_STATUS_CHIP_CLASS[s]||R.SESSION_STATUS_CHIP_CLASS.offline}`,children:s})]}),(0,t.jsxs)("div",{className:"mt-0.5 truncate text-xs text-gray-500",children:[e.agent||"—",(0,t.jsx)("span",{className:"text-gray-700 mx-1.5",children:"·"}),(0,t.jsx)("span",{title:e.server||"",children:D(e.server)})]})]}),(0,t.jsx)("span",{className:"hidden shrink-0 rounded-lg border border-cyan-500/15 bg-cyan-500/5 px-2 py-1 text-[10px] text-cyan-300/70 sm:inline",children:"Tap to chat"}),(0,t.jsx)(p.NodeLifecycleMenu,{nodeId:x[e.alias]||e.node_id||e.alias,alias:e.alias,lifecycleState:c[e.alias],networkId:e.network_id||null,onCompleted:_,attrsSnapshot:u[e.alias]})]}),(0,t.jsxs)("div",{className:"mt-3 rounded-lg border border-[#1c1c1f] bg-[#0e0e10] px-3 py-2 text-xs",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between gap-3",children:[(0,t.jsx)("span",{className:"text-[10px] uppercase tracking-wide text-gray-600",children:"Current task"}),(0,t.jsx)("span",{className:"text-[10px] text-gray-600",children:(0,a.timeAgo)(e.last_seen_at||e.updated_at)})]}),(0,t.jsx)("div",{className:`mt-1 line-clamp-2 ${e.task?"text-gray-300":"text-gray-600 italic"}`,children:e.task||"No current task"})]}),n>0&&n<100&&(0,t.jsx)("div",{className:"mt-3 h-1.5 overflow-hidden rounded-full bg-gray-800",children:(0,t.jsx)("div",{className:"h-1.5 rounded-full bg-cyan-400 transition-all",style:{width:`${n}%`}})})]},e.alias)})}):(0,t.jsxs)("div",{className:"space-y-1 sm:space-y-2",children:[(0,t.jsxs)("div",{className:"hidden sm:grid sm:grid-cols-10 gap-2 px-4 py-2 text-xs text-gray-600 uppercase",children:[(0,t.jsx)("div",{className:"col-span-1",children:"Status"}),(0,t.jsx)("div",{className:"col-span-2",children:"Alias"}),(0,t.jsx)("div",{className:"col-span-2",children:"Agent · Server"}),(0,t.jsx)("div",{className:"col-span-4",children:"Current Task"}),(0,t.jsx)("div",{className:"col-span-1",children:"Updated"})]}),eg.map(e=>{let s=e.online?e.status:"offline",n=m(e.alias);return(0,t.jsxs)("div",{role:"button",tabIndex:0,"data-node-card":!0,"data-node-alias":e.alias,onClick:()=>ee(e.alias),onKeyDown:t=>{("Enter"===t.key||" "===t.key)&&(t.preventDefault(),ee(e.alias))},className:`rounded-lg border border-[#26262b] bg-[#161618] px-3 py-2 sm:px-4 sm:py-3 transition-colors hover:border-cyan-500/40 cursor-pointer ${!e.online?"opacity-50":""}`,children:[(0,t.jsxs)("div",{className:"hidden sm:grid sm:grid-cols-10 gap-2 items-center",children:[(0,t.jsx)("div",{className:"col-span-1",children:(0,t.jsx)("span",{className:`text-xs px-2 py-0.5 rounded-md border ${R.SESSION_STATUS_CHIP_CLASS[s]||R.SESSION_STATUS_CHIP_CLASS.offline}`,children:s})}),(0,t.jsx)("div",{className:"col-span-2 min-w-0",children:(0,t.jsxs)("div",{className:"flex items-center gap-2 min-w-0",children:[(0,t.jsxs)("div",{className:"relative shrink-0",children:[(0,t.jsxs)("span",{className:"relative inline-flex shrink-0",children:[(0,t.jsx)(b.AliasAvatar,{alias:e.alias,size:20}),f(e.alias)>0&&(0,y.isMuted)(e.alias)&&(0,t.jsx)("span",{"aria-label":(0,S.t)("有未读(已静音)","Unread (muted)"),className:"absolute -top-1 -right-1 h-2 w-2 rounded-full bg-red-500/80"}),f(e.alias)>0&&!(0,y.isMuted)(e.alias)&&(0,t.jsx)("span",{"aria-label":(0,S.t)(`${f(e.alias)} 条未读`,`${f(e.alias)} unread`),className:"absolute -top-1.5 -right-2 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center",children:f(e.alias)>99?"99+":f(e.alias)})]}),n&&(0,t.jsx)("span",{className:"absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full border border-[#161618] bg-red-500"})]}),(0,t.jsx)("span",{className:"truncate text-sm font-medium text-white",children:e.alias}),(0,t.jsx)(W,{alias:e.alias}),n&&(0,t.jsx)("span",{className:"shrink-0 rounded-full bg-red-500/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-red-300",children:"New"})]})}),(0,t.jsx)("div",{className:"col-span-2 truncate text-xs text-gray-400",title:e.server||"",children:(0,t.jsxs)("span",{className:"truncate",children:[e.agent||"--",(0,t.jsx)("span",{className:"text-gray-700 mx-1.5",children:"·"}),D(e.server)]})}),(0,t.jsx)("div",{className:"col-span-4 truncate text-xs text-gray-500",title:e.task||"",children:e.task||"--"}),(0,t.jsxs)("div",{className:"col-span-1 flex items-center justify-between gap-2 text-xs text-gray-500",children:[(0,t.jsx)("span",{className:"truncate",children:(0,a.timeAgo)(e.last_seen_at||e.updated_at)}),(0,t.jsx)(p.NodeLifecycleMenu,{nodeId:x[e.alias]||e.node_id||e.alias,alias:e.alias,lifecycleState:c[e.alias],networkId:e.network_id||null,onCompleted:_,attrsSnapshot:u[e.alias]})]})]}),(0,t.jsxs)("div",{className:"sm:hidden space-y-1",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2.5",children:[(0,t.jsxs)("div",{className:"relative shrink-0",children:[(0,t.jsx)(b.AliasAvatar,{alias:e.alias,size:28}),n&&(0,t.jsx)("span",{className:"absolute -right-1 -top-1 h-3 w-3 rounded-full border-2 border-[#161618] bg-red-500"})]}),(0,t.jsxs)("div",{className:"min-w-0 flex-1",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2 min-w-0",children:[(0,t.jsx)("div",{className:"truncate text-sm font-medium text-white",children:e.alias}),n&&(0,t.jsx)("span",{className:"shrink-0 rounded-full bg-red-500/15 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wide text-red-300",children:"New"})]}),(0,t.jsxs)("div",{className:"truncate text-[10px] text-gray-500",children:[e.agent||"—"," · ",(0,a.timeAgo)(e.last_seen_at||e.updated_at)]})]}),(0,t.jsx)("span",{className:`shrink-0 text-xs px-2 py-0.5 rounded-md border ${R.SESSION_STATUS_CHIP_CLASS[s]||R.SESSION_STATUS_CHIP_CLASS.offline}`,children:s}),(0,t.jsx)(p.NodeLifecycleMenu,{nodeId:x[e.alias]||e.node_id||e.alias,alias:e.alias,lifecycleState:c[e.alias],networkId:e.network_id||null,onCompleted:_,attrsSnapshot:u[e.alias]})]}),e.task&&(0,t.jsx)("div",{className:"truncate text-xs text-gray-500",children:e.task})]})]},e.alias)})]})]}),J&&Z&&(0,t.jsx)(B,{alias:J,onClose:et}),ev&&(0,t.jsx)(B,{alias:J,variant:"page",onBack:et,onClose:et})]})},"decodeAliasSegment",0,U],35599)}]);
|
|
File without changes
|
|
File without changes
|