@sleep2agi/agent-network-dashboard 0.6.3-preview.51 → 0.6.3-preview.52
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 +46 -46
- 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.js.nft.json +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 +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin/page.js.nft.json +1 -1
- 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 +15 -15
- package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
- package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
- package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
- package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +15 -15
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/server/app/index.segments/_index.segment.rsc +8 -8
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login/page.js.nft.json +1 -1
- 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 +15 -15
- package/.next/server/app/login.segments/_full.segment.rsc +15 -15
- package/.next/server/app/login.segments/_head.segment.rsc +4 -4
- package/.next/server/app/login.segments/_index.segment.rsc +8 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/login.segments/login.segment.rsc +3 -3
- package/.next/server/app/logs/page.js.nft.json +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 +15 -15
- package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
- package/.next/server/app/messages/page.js.nft.json +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 +15 -15
- package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
- package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
- package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
- package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
- package/.next/server/app/node/page.js.nft.json +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 +15 -15
- package/.next/server/app/node.segments/_full.segment.rsc +15 -15
- package/.next/server/app/node.segments/_head.segment.rsc +4 -4
- package/.next/server/app/node.segments/_index.segment.rsc +8 -8
- package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/node.segments/node.segment.rsc +3 -3
- package/.next/server/app/nodes/[alias]/page.js.nft.json +1 -1
- package/.next/server/app/nodes/[alias]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/nodes/page.js.nft.json +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 +15 -15
- package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
- package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
- package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/providers/page.js.nft.json +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 +15 -15
- package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
- package/.next/server/app/server-logs/page.js.nft.json +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 +15 -15
- package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
- package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
- package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
- package/.next/server/app/servers/page.js.nft.json +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 +15 -15
- package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
- package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
- package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
- package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
- package/.next/server/app/settings/networks/page.js.nft.json +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 +15 -15
- package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
- package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/tokens/page.js.nft.json +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 +15 -15
- package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
- package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
- package/.next/server/app/settings.html +2 -2
- package/.next/server/app/settings.rsc +15 -15
- package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
- package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
- package/.next/server/app/tasks/[id]/page.js.nft.json +1 -1
- package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/tasks/page.js.nft.json +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 +15 -15
- package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
- package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
- package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
- 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 +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dcez4x._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0jgqpfv._.js → [root-of-the-server]__0gyqilt._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0gyqilt._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js +11 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0j-def.._.js.map +1 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__11wln8.._.js → [root-of-the-server]__0oavw6j._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0oavw6j._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0rct4.k._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__02o~-py._.js.map → [root-of-the-server]__0rct4.k._.js.map} +1 -1
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.js +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_05u4_h~._.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 +2 -2
- package/.next/server/chunks/ssr/dash-stageb-wt_app_nodes_NodesView_tsx_0patjj7._.js.map +1 -1
- 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/0-ot~88nectgp.js +2 -0
- package/.next/static/chunks/00adqh_rd04uf.js +9 -0
- package/.next/static/chunks/{0advoo2tzdcyi.js → 051p0pw4z~6ag.js} +1 -1
- package/.next/static/chunks/{0uk67ty_0h_36.js → 0lf8e-h91s10h.js} +1 -1
- package/.next/static/chunks/{0dkk_l872ocgm.js → 0ohx4jpm9~61..js} +1 -1
- package/.next/static/chunks/0pg4pce.q5ne_.js +9 -0
- package/.next/static/chunks/{0fz7zk.ek.154.js → 0u5y.gmi~jkw6.js} +1 -1
- package/.next/static/chunks/0wf03_efeii.w.css +1 -0
- package/.next/static/chunks/0ws0p1n62l~ub.js +1 -0
- package/.next/static/chunks/{06q7-vhy~1-y3.js → 0~rx9_~1wupcp.js} +2 -2
- package/.next/static/chunks/{0z_4d13crd_af.js → 14x094g58kg61.js} +1 -1
- package/.next/trace +2 -2
- package/.next/trace-build +1 -1
- package/app/components/ChatPane.tsx +26 -2
- package/app/components/NodeList.tsx +14 -4
- package/app/components/TaskChatPanel.tsx +156 -31
- package/app/messages/page.tsx +22 -22
- package/app/nodes/NodesView.tsx +52 -11
- package/package.json +1 -1
- package/tests/e2e-9-nodes-team-tag-filter.spec.ts +14 -7
- package/tests/e2e-messages-spec-visual-align.spec.ts +244 -0
- package/tests/e2e-pin-mute-attr-always-present.spec.ts +171 -0
- package/tests/e2e-stage-a-node-list.spec.ts +14 -7
- package/tests/e2e-stage-b-chat-pane.spec.ts +74 -10
- package/tests/e2e-status-labels.spec.ts +218 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__02o~-py._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0jgqpfv._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js +0 -9
- package/.next/server/chunks/ssr/[root-of-the-server]__0w7k~3f._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__11wln8.._.js.map +0 -1
- package/.next/static/chunks/05~lgfzsv.p~p.js +0 -2
- package/.next/static/chunks/092zqb~4un32l.css +0 -1
- package/.next/static/chunks/11zz15q37~bz5.js +0 -7
- package/.next/static/chunks/17qkc714xy04..js +0 -1
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → RiXBfeE2zY5tzXRmF7kBD}/_buildManifest.js +0 -0
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → RiXBfeE2zY5tzXRmF7kBD}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{JL_QjNKv1KhsCF68ZK28x → RiXBfeE2zY5tzXRmF7kBD}/_ssgManifest.js +0 -0
|
@@ -315,45 +315,170 @@ function LocalFilePreview({ file, onRemove }: { file: File; onRemove: () => void
|
|
|
315
315
|
);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
// 通信龙 07-31 GAP 2nd tier: translate hub status codes into human
|
|
319
|
+
// labels. The old form was `● ─ ● ─ ○ ─ ○ delivered` — a colored dot
|
|
320
|
+
// progression + the raw English word. Nobody could tell "delivered"
|
|
321
|
+
// (hub-side mark) from "acked" (node process received) from "replied"
|
|
322
|
+
// (agent actually processed).
|
|
323
|
+
//
|
|
324
|
+
// Two things this rewrite fixes:
|
|
325
|
+
//
|
|
326
|
+
// 1. STATUS_STEPS was `['created','delivered','running','replied']`.
|
|
327
|
+
// Neither `created` nor `running` correspond to hub-emitted terminal
|
|
328
|
+
// statuses (`created` is client-side optimistic, `running` is READ
|
|
329
|
+
// by hub queries but never WRITTEN — grep server/src for `= 'running'`
|
|
330
|
+
// returns zero). Real hub progression is queued → delivered → acked
|
|
331
|
+
// → replied, and the old third dot literally never lit up.
|
|
332
|
+
//
|
|
333
|
+
// 2. `delivered` was labeled as if it meant "message reached the node".
|
|
334
|
+
// Reading hub source (server/src/index.ts:1568 + tools.ts:1080/1232)
|
|
335
|
+
// shows it's set at task-insert time — same instant as delivered_at
|
|
336
|
+
// (which is why delivered_at delay is empirically 0). The label now
|
|
337
|
+
// says "已推送" and the hover explicitly disclaims node reception.
|
|
338
|
+
//
|
|
339
|
+
// Vocabulary — must stay in sync with hub emits:
|
|
340
|
+
// queued / delivered / acked / replied / failed ← the primary 5
|
|
341
|
+
// created ← client-side pre-POST
|
|
342
|
+
// closed / expired / cancelled ← terminal, non-reply
|
|
343
|
+
// running ← reserved (never written today)
|
|
344
|
+
// <anything else> ← fail-loudly (raw + banner)
|
|
345
|
+
const STATUS_STEPS = ['queued', 'delivered', 'acked', 'replied'] as const;
|
|
346
|
+
type StatusInfo = { label: string; hover: string; color: string; icon: React.ReactNode };
|
|
347
|
+
const STATUS_INFO: Record<string, StatusInfo> = {
|
|
348
|
+
created: { label: '发送中', hover: '客户端已建消息,尚未收到 hub 回执。', color: 'text-gray-400', icon: <IconSending /> },
|
|
349
|
+
queued: { label: '排队中', hover: '已入队,尚未推给节点。节点可能离线、或通道拥堵。', color: 'text-gray-400', icon: <IconClock /> },
|
|
350
|
+
delivered: { label: '已推送', hover: 'hub 侧已标记推送。尚未确认节点是否收到(delivered_at 是入队时刻,不代表 SSE 已到达)。', color: 'text-sky-400', icon: <IconCheck /> },
|
|
351
|
+
acked: { label: '已接收', hover: '节点进程已确认收到。未必已处理——对方 agent 可能尚未唤醒。', color: 'text-gray-300', icon: <IconCheckDouble /> },
|
|
352
|
+
replied: { label: '已回复', hover: '对方已针对此消息回复。', color: 'text-emerald-400', icon: <IconCheckDouble filled /> },
|
|
353
|
+
failed: { label: '失败', hover: '发送失败——详细原因见消息下方文本。', color: 'text-red-400', icon: <IconWarn /> },
|
|
354
|
+
closed: { label: '已关闭', hover: '会话已被关闭,不再期待回复。', color: 'text-gray-500', icon: <IconClose /> },
|
|
355
|
+
expired: { label: '已超时', hover: '在期限内未收到回复,任务已超时。', color: 'text-gray-500', icon: <IconClock /> },
|
|
356
|
+
cancelled: { label: '已取消', hover: '任务被主动取消。', color: 'text-gray-500', icon: <IconClose /> },
|
|
357
|
+
running: { label: '处理中', hover: '节点进程正在处理(今日 hub 不写入此值,见组件注释)。', color: 'text-green-400', icon: <IconClock /> },
|
|
322
358
|
};
|
|
323
359
|
|
|
324
|
-
function StatusBar({ status }: { status: string }) {
|
|
325
|
-
const
|
|
326
|
-
const
|
|
360
|
+
function StatusBar({ status, result }: { status: string; result?: string }) {
|
|
361
|
+
const info = STATUS_INFO[status];
|
|
362
|
+
const known = !!info;
|
|
363
|
+
// Progression label list for the hover — same string appended after
|
|
364
|
+
// the per-status hover so the reader gets a compact map of the flow.
|
|
365
|
+
// Not per-step timestamps: hub doesn't expose queued_at / acked_at
|
|
366
|
+
// separately from delivered_at, so a per-step timeline would be
|
|
367
|
+
// fabricated data. See feedback_assert_the_fact_not_the_declaration.
|
|
368
|
+
const idx = STATUS_STEPS.indexOf(status as typeof STATUS_STEPS[number]);
|
|
369
|
+
const flow = STATUS_STEPS.map((s, i) => {
|
|
370
|
+
const mark = i < idx || (i === idx && idx >= 0) ? '✓' : '○';
|
|
371
|
+
return `${mark} ${STATUS_INFO[s].label}`;
|
|
372
|
+
}).join(' → ');
|
|
373
|
+
const hoverText = known
|
|
374
|
+
? `${info.hover}\n\n进度:${flow}`
|
|
375
|
+
: `未知状态「${status}」——请报 bug。UI 词库没有匹配项,可能 hub 加了新 status 但前端没跟上。`;
|
|
376
|
+
const label = known ? info.label : status;
|
|
377
|
+
const color = known ? info.color : 'text-yellow-400';
|
|
378
|
+
|
|
379
|
+
// failed inline reason — hub has no dedicated failure_reason column
|
|
380
|
+
// (task table: task_id/from_*/to_*/status/content/result/... —
|
|
381
|
+
// grep-verified as of 2026-07-31). We show `result` first 40 chars
|
|
382
|
+
// when present. When result is empty, we explicitly say "节点未提供
|
|
383
|
+
// 原因" rather than falling back to a generic "未知错误" — that
|
|
384
|
+
// distinguishes "hub has no error field" from "reason field exists
|
|
385
|
+
// but got a bad value".
|
|
386
|
+
const failedReason = status === 'failed'
|
|
387
|
+
? ((result && result.trim()) ? result.trim().slice(0, 40) : '节点未提供原因')
|
|
388
|
+
: null;
|
|
389
|
+
|
|
327
390
|
return (
|
|
328
|
-
<div
|
|
329
|
-
{
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
</
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
391
|
+
<div
|
|
392
|
+
data-status-key={status}
|
|
393
|
+
data-status-known={known ? 'true' : 'false'}
|
|
394
|
+
data-status-label={label}
|
|
395
|
+
className="inline-flex flex-col gap-0.5 mt-1.5"
|
|
396
|
+
>
|
|
397
|
+
<span
|
|
398
|
+
title={hoverText}
|
|
399
|
+
aria-label={known ? info.label : `未知状态 ${status}`}
|
|
400
|
+
className={`inline-flex items-center gap-1.5 text-[11px] font-medium ${color}`}
|
|
401
|
+
>
|
|
402
|
+
{known ? info.icon : <IconQuestion />}
|
|
403
|
+
<span>{label}</span>
|
|
404
|
+
{(status === 'delivered' || status === 'created') && (
|
|
405
|
+
<span aria-hidden className="inline-flex items-center gap-0.5 ml-0.5">
|
|
406
|
+
<span className="inline-block w-1 h-1 rounded-full bg-current opacity-70 animate-pulse" />
|
|
407
|
+
<span className="inline-block w-1 h-1 rounded-full bg-current opacity-70 animate-pulse" style={{ animationDelay: '0.2s' }} />
|
|
408
|
+
<span className="inline-block w-1 h-1 rounded-full bg-current opacity-70 animate-pulse" style={{ animationDelay: '0.4s' }} />
|
|
409
|
+
</span>
|
|
410
|
+
)}
|
|
411
|
+
</span>
|
|
412
|
+
{failedReason !== null && (
|
|
413
|
+
<span
|
|
414
|
+
data-status-reason={failedReason}
|
|
415
|
+
className="text-[10px] text-red-400/80 max-w-[260px] truncate"
|
|
416
|
+
title={result || undefined}
|
|
417
|
+
>
|
|
418
|
+
{failedReason}
|
|
351
419
|
</span>
|
|
352
420
|
)}
|
|
353
421
|
</div>
|
|
354
422
|
);
|
|
355
423
|
}
|
|
356
424
|
|
|
425
|
+
// Icon primitives — inline SVG, single-color (currentColor), sized to
|
|
426
|
+
// match the 11px label text. No external icon library — keeps the
|
|
427
|
+
// StatusBar self-contained and avoids a Tailwind purge/tree-shake
|
|
428
|
+
// dance for icons only this component uses.
|
|
429
|
+
function IconClock() {
|
|
430
|
+
return (
|
|
431
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
432
|
+
<circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" />
|
|
433
|
+
</svg>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
function IconCheck() {
|
|
437
|
+
return (
|
|
438
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
|
439
|
+
<path d="M4 12l6 6L20 6" />
|
|
440
|
+
</svg>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
function IconCheckDouble({ filled }: { filled?: boolean } = {}) {
|
|
444
|
+
return (
|
|
445
|
+
<svg width="14" height="12" viewBox="0 0 28 24" fill="none" stroke="currentColor" strokeWidth={filled ? 2.5 : 2} strokeLinecap="round" strokeLinejoin="round">
|
|
446
|
+
<path d="M2 12l6 6L18 6" />
|
|
447
|
+
<path d="M10 12l6 6L26 6" opacity={filled ? 1 : 0.7} />
|
|
448
|
+
</svg>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
function IconWarn() {
|
|
452
|
+
return (
|
|
453
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
454
|
+
<path d="M12 2L2 20h20L12 2z" /><path d="M12 9v5" /><circle cx="12" cy="17.5" r="0.6" fill="currentColor" />
|
|
455
|
+
</svg>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
function IconClose() {
|
|
459
|
+
return (
|
|
460
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
461
|
+
<circle cx="12" cy="12" r="10" /><path d="M8 12h8" />
|
|
462
|
+
</svg>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
function IconSending() {
|
|
466
|
+
return (
|
|
467
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
468
|
+
<path d="M4 12l16-8-6 16-3-7-7-1z" />
|
|
469
|
+
</svg>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
function IconQuestion() {
|
|
473
|
+
return (
|
|
474
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
475
|
+
<circle cx="12" cy="12" r="10" />
|
|
476
|
+
<path d="M9.5 9.5a2.5 2.5 0 015 0c0 1.5-2.5 2-2.5 3.5" />
|
|
477
|
+
<circle cx="12" cy="17" r="0.6" fill="currentColor" />
|
|
478
|
+
</svg>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
357
482
|
// Full markdown rendering — handles tables, headings, lists, links, code,
|
|
358
483
|
// blockquotes via react-markdown + GFM. Earlier hand-rolled parser only did
|
|
359
484
|
// code blocks + bold; tables / headings showed as raw `|---|` text.
|
|
@@ -1442,7 +1567,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
|
|
|
1442
1567
|
))}
|
|
1443
1568
|
</div>
|
|
1444
1569
|
<div className="flex flex-wrap items-center justify-between mt-1.5 gap-2 sm:gap-3">
|
|
1445
|
-
<StatusBar status={m.status} />
|
|
1570
|
+
<StatusBar status={m.status} result={m.result} />
|
|
1446
1571
|
<div className="flex items-center gap-2 shrink-0">
|
|
1447
1572
|
{/* R4: WeChat send-feedback loop — spinner while the
|
|
1448
1573
|
optimistic bubble is still in flight; tap-to-resend
|
package/app/messages/page.tsx
CHANGED
|
@@ -146,7 +146,7 @@ export default function MessagesPage() {
|
|
|
146
146
|
}, [filtered]);
|
|
147
147
|
|
|
148
148
|
return (
|
|
149
|
-
<div className="min-h-screen bg-[#
|
|
149
|
+
<div data-testid="messages-shell" className="min-h-screen bg-[#141826] text-gray-100 p-4 sm:p-6">
|
|
150
150
|
{/* #209 R31 (mobile vertical rhythm — same pattern as R28/R30):
|
|
151
151
|
header row + quick-from chips row both mb-6 → mb-4 sm:mb-6.
|
|
152
152
|
#209 R33: header restructured to mirror /nodes R32 — title
|
|
@@ -253,7 +253,7 @@ export default function MessagesPage() {
|
|
|
253
253
|
<button
|
|
254
254
|
type="button"
|
|
255
255
|
onClick={() => setSearch('')}
|
|
256
|
-
className="shrink-0 rounded-full border border-gray-700 px-3 py-1 text-
|
|
256
|
+
className="shrink-0 rounded-full border border-gray-700 px-3 py-1 text-[11px] text-[#64748B] hover:text-gray-200"
|
|
257
257
|
>
|
|
258
258
|
Clear
|
|
259
259
|
</button>
|
|
@@ -336,9 +336,9 @@ export default function MessagesPage() {
|
|
|
336
336
|
Halve it on mobile. */}
|
|
337
337
|
{gapExceeded && (
|
|
338
338
|
<div className="my-2 sm:my-4 flex items-center gap-3">
|
|
339
|
-
<div className="h-px flex-1 bg-[
|
|
340
|
-
<div className="text-[
|
|
341
|
-
<div className="h-px flex-1 bg-[
|
|
339
|
+
<div className="h-px flex-1 bg-white/[0.06]" />
|
|
340
|
+
<div className="text-[13px] font-medium text-[#94A3B8]">{formatDividerLabel(message.created_at)}</div>
|
|
341
|
+
<div className="h-px flex-1 bg-white/[0.06]" />
|
|
342
342
|
</div>
|
|
343
343
|
)}
|
|
344
344
|
|
|
@@ -348,21 +348,21 @@ export default function MessagesPage() {
|
|
|
348
348
|
390px. Desktop unchanged from sm: up. */}
|
|
349
349
|
{variant === 'broadcast' ? (
|
|
350
350
|
<div className={samePrev ? 'mt-0.5 sm:mt-1' : 'mt-2 sm:mt-3'}>
|
|
351
|
-
<div className="rounded-2xl border border-
|
|
351
|
+
<div className="rounded-2xl border border-white/[0.06] border-l-2 border-l-[#7DD3FC] bg-[#1E293B] px-3 py-2 sm:px-4 sm:py-3 shadow-sm w-full">
|
|
352
352
|
<div className="mb-1 sm:mb-2 flex flex-wrap items-center gap-2">
|
|
353
353
|
<span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
|
|
354
354
|
{message.type || 'unknown'}
|
|
355
355
|
</span>
|
|
356
|
-
<span className="text-sm font-medium text-
|
|
357
|
-
{message.to_alias && <span className="text-xs text-
|
|
358
|
-
{message.priority === 'high' && <span className="text-xs text-
|
|
359
|
-
<span className="ml-auto text-
|
|
356
|
+
<span className="text-sm font-medium text-[#E2E8F0]">{fromAlias}</span>
|
|
357
|
+
{message.to_alias && <span className="text-xs text-[#CBD5E1]">to {message.to_alias}</span>}
|
|
358
|
+
{message.priority === 'high' && <span className="text-xs text-[#EF4444]">HIGH</span>}
|
|
359
|
+
<span className="ml-auto text-[11px] text-[#64748B]">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
|
|
360
360
|
</div>
|
|
361
361
|
{/* Round 79: break-words + overflow-wrap:anywhere so
|
|
362
362
|
long unbroken runs (URLs, ASCII rules like
|
|
363
363
|
`═══════════════`) wrap instead of pushing the
|
|
364
364
|
chat bubble past the mobile viewport. */}
|
|
365
|
-
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-
|
|
365
|
+
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-[#E2E8F0]">
|
|
366
366
|
{renderHighlighted(message.content, search)}
|
|
367
367
|
</div>
|
|
368
368
|
{/* #492: broadcast may carry attachments too. */}
|
|
@@ -377,23 +377,23 @@ export default function MessagesPage() {
|
|
|
377
377
|
<div className="w-8 shrink-0 pt-1">
|
|
378
378
|
{!samePrev && <AliasAvatar alias={fromAlias} size={32} />}
|
|
379
379
|
</div>
|
|
380
|
-
<div className={`min-w-0 max-w-
|
|
380
|
+
<div className={`min-w-0 max-w-[min(640px,65%)] rounded-2xl border border-white/[0.06] px-3 py-2 sm:px-4 sm:py-3 shadow-sm ${
|
|
381
381
|
variant === 'outgoing'
|
|
382
|
-
? '
|
|
383
|
-
: '
|
|
382
|
+
? 'bg-[#1E3A5F]'
|
|
383
|
+
: 'bg-[#1E293B]'
|
|
384
384
|
}`}>
|
|
385
385
|
{!samePrev && (
|
|
386
386
|
<div className="mb-1 sm:mb-2 flex flex-wrap items-center gap-2">
|
|
387
387
|
<span className={`text-xs px-2 py-0.5 rounded-md border ${TYPE_COLORS[message.type || ''] || 'bg-gray-500/10 text-gray-400 border-gray-500/20'}`}>
|
|
388
388
|
{message.type || 'unknown'}
|
|
389
389
|
</span>
|
|
390
|
-
<span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-
|
|
390
|
+
<span className={`text-sm font-medium ${variant === 'outgoing' ? 'text-[#F1F5F9]' : 'text-[#E2E8F0]'}`}>
|
|
391
391
|
{fromAlias}
|
|
392
392
|
</span>
|
|
393
|
-
<span className="text-xs text-
|
|
394
|
-
<span className="text-sm text-
|
|
395
|
-
{message.priority === 'high' && <span className="text-xs text-
|
|
396
|
-
<span className="ml-auto text-
|
|
393
|
+
<span className="text-xs text-[#64748B]">→</span>
|
|
394
|
+
<span className="text-sm text-[#CBD5E1]">{message.to_alias || '--'}</span>
|
|
395
|
+
{message.priority === 'high' && <span className="text-xs text-[#EF4444]">HIGH</span>}
|
|
396
|
+
<span className="ml-auto text-[11px] text-[#64748B]">{message.created_at ? timeAgo(message.created_at) : '--'}</span>
|
|
397
397
|
</div>
|
|
398
398
|
)}
|
|
399
399
|
|
|
@@ -401,7 +401,7 @@ export default function MessagesPage() {
|
|
|
401
401
|
long unbroken runs (URLs, ASCII rules like
|
|
402
402
|
`═══════════════`) wrap instead of pushing the
|
|
403
403
|
chat bubble past the mobile viewport. */}
|
|
404
|
-
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-
|
|
404
|
+
<div className="whitespace-pre-wrap break-words [overflow-wrap:anywhere] text-sm leading-snug sm:leading-relaxed text-[#E2E8F0]">
|
|
405
405
|
{renderHighlighted(message.content, search)}
|
|
406
406
|
</div>
|
|
407
407
|
|
|
@@ -417,13 +417,13 @@ export default function MessagesPage() {
|
|
|
417
417
|
|
|
418
418
|
{/* When in a streak, surface the timestamp inline-right so users still know cadence */}
|
|
419
419
|
{samePrev && (
|
|
420
|
-
<div className="mt-1 text-[
|
|
420
|
+
<div className="mt-1 text-[11px] text-[#64748B] text-right">
|
|
421
421
|
{message.created_at ? timeAgo(message.created_at) : ''}
|
|
422
422
|
</div>
|
|
423
423
|
)}
|
|
424
424
|
|
|
425
425
|
{debug && (
|
|
426
|
-
<div className="mt-3 border-t border-white/10 pt-3 text-
|
|
426
|
+
<div className="mt-3 border-t border-white/10 pt-3 text-[11px] text-[#64748B] space-y-1">
|
|
427
427
|
<div>ID: {message.id}</div>
|
|
428
428
|
{message.task_id && <div>Task ID: {message.task_id}</div>}
|
|
429
429
|
<div>Created: {message.created_at}</div>
|
package/app/nodes/NodesView.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import { useState, useRef, useEffect, useSyncExternalStore } from 'react';
|
|
|
4
4
|
import { timeAgo } from '../components/utils';
|
|
5
5
|
import { useSessions, useHealth, useNodeLifecycle } from '../lib/hooks';
|
|
6
6
|
import { useSWRConfig } from 'swr';
|
|
7
|
-
import { TaskChatPanel } from '../components/TaskChatPanel';
|
|
8
7
|
import { CreateNodeWizard } from '../components/CreateNodeWizard';
|
|
9
8
|
import { NodeLifecycleMenu } from '../components/NodeLifecycleMenu';
|
|
10
9
|
import { EmptyState, NodesEmptyState } from '../components/EmptyState';
|
|
@@ -118,6 +117,10 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
118
117
|
const [chatAlias, setChatAlias] = useState<string | null>(initialAlias);
|
|
119
118
|
const isDesktop = useIsDesktop();
|
|
120
119
|
const selectNode = (alias: string) => {
|
|
120
|
+
try {
|
|
121
|
+
const el = document.querySelector('[data-testid="node-list-scroll"]');
|
|
122
|
+
if (el) savedListScrollRef.current = el.scrollTop;
|
|
123
|
+
} catch {}
|
|
121
124
|
setChatAlias(alias);
|
|
122
125
|
try { window.history.pushState({}, '', `/nodes/${encodeURIComponent(alias)}`); } catch {}
|
|
123
126
|
};
|
|
@@ -295,6 +298,31 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
295
298
|
// column rendered.
|
|
296
299
|
const chatOpenDesktop = !!chatAlias && isDesktop && sessions.length > 0;
|
|
297
300
|
|
|
301
|
+
// #Mobile two-level nav (Feishu phone form, 通信龙 07-31, breakpoint md
|
|
302
|
+
// per my own ruling — no third media query):
|
|
303
|
+
// < md unselected = conversation LIST fullscreen (not the table)
|
|
304
|
+
// < md selected = chat as a full PAGE with a back arrow
|
|
305
|
+
// manage mode = the old table, reachable via the same toggle
|
|
306
|
+
// Desktop auto-selects; mobile deliberately does NOT (the list IS the
|
|
307
|
+
// first screen — auto-select effect above is gated on isDesktop).
|
|
308
|
+
const mobileListLevel = !isDesktop && !chatAlias && !manageView && sessions.length > 0;
|
|
309
|
+
const mobileChatLevel = !isDesktop && !!chatAlias;
|
|
310
|
+
|
|
311
|
+
// #Mobile back restores the list scroll position (判据: enter from row
|
|
312
|
+
// ~50, come back, offset ≤200px — snapping to top is the annoying bug).
|
|
313
|
+
// Saved at selection time; restored when the list level remounts.
|
|
314
|
+
// Desktop is exempt: its rail never unmounts while chatting, and the
|
|
315
|
+
// user may have scrolled it meanwhile.
|
|
316
|
+
const savedListScrollRef = useRef(0);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (isDesktop || chatAlias) return;
|
|
319
|
+
const id = requestAnimationFrame(() => {
|
|
320
|
+
const el = document.querySelector('[data-testid="node-list-scroll"]');
|
|
321
|
+
if (el) el.scrollTop = savedListScrollRef.current;
|
|
322
|
+
});
|
|
323
|
+
return () => cancelAnimationFrame(id);
|
|
324
|
+
}, [isDesktop, chatAlias]);
|
|
325
|
+
|
|
298
326
|
return (
|
|
299
327
|
<div
|
|
300
328
|
ref={rootRef}
|
|
@@ -310,10 +338,10 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
310
338
|
#Stage C: a selected node FORCES the rail on (even for dense-view
|
|
311
339
|
users with showRail=false) — in the list+chat layout the rail is
|
|
312
340
|
the only way to switch nodes, so it must be there. */}
|
|
313
|
-
{(showRail || chatOpenDesktop) && sessions.length > 0 && (
|
|
341
|
+
{(mobileListLevel || (isDesktop && (showRail || chatOpenDesktop) && sessions.length > 0)) && (
|
|
314
342
|
<aside
|
|
315
343
|
data-testid="node-list-rail"
|
|
316
|
-
className="
|
|
344
|
+
className="flex flex-col w-full md:w-[280px] shrink-0 h-full min-h-0 bg-[#0F1320]"
|
|
317
345
|
>
|
|
318
346
|
<NodeList
|
|
319
347
|
sessions={railSessions}
|
|
@@ -339,8 +367,20 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
339
367
|
{/* #Stage C: middle column (header + filters + table/grid) renders
|
|
340
368
|
only when NO node is selected on desktop — see chatOpenDesktop
|
|
341
369
|
note above. Mobile keeps it always (chat is an overlay there). */}
|
|
342
|
-
{!chatOpenDesktop && (
|
|
343
|
-
<div className="flex-1 min-w-0 max-w-full h-full overflow-y-auto overflow-x-hidden p-4 sm:p-6">
|
|
370
|
+
{(isDesktop ? !chatOpenDesktop : (manageView && !chatAlias)) && (
|
|
371
|
+
<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">
|
|
372
|
+
{/* #Mobile manage mode: way back to the conversation list — the rail
|
|
373
|
+
(which hosts the toggle) is hidden at this level on phones. */}
|
|
374
|
+
{!isDesktop && manageView && (
|
|
375
|
+
<button
|
|
376
|
+
type="button"
|
|
377
|
+
onClick={toggleManageView}
|
|
378
|
+
data-testid="manage-view-toggle-mobile"
|
|
379
|
+
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"
|
|
380
|
+
>
|
|
381
|
+
{t('≡ 返回对话视图', '≡ Back to conversations')}
|
|
382
|
+
</button>
|
|
383
|
+
)}
|
|
344
384
|
{/* #209 R31 (mobile vertical rhythm — goal "大幅提升移动端体验",
|
|
345
385
|
extending R28's Overview pattern + R30's /tasks pattern):
|
|
346
386
|
this header row + the status-bar wrapper + the filter row
|
|
@@ -760,18 +800,19 @@ export function NodesView({ initialAlias = null }: { initialAlias?: string | nul
|
|
|
760
800
|
</div>
|
|
761
801
|
)}
|
|
762
802
|
|
|
763
|
-
{/* #Stage B
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
two are mutually exclusive via isDesktop so only ONE
|
|
767
|
-
TaskChatPanel is ever mounted (no double SSE / mark-read). */}
|
|
768
|
-
{chatAlias && !isDesktop && <TaskChatPanel alias={chatAlias} onClose={closeChat} />}
|
|
803
|
+
{/* (#Stage B's mobile overlay is gone — < md chat is now a full
|
|
804
|
+
PAGE, rendered as ChatPane variant="page" below. isDesktop
|
|
805
|
+
still guarantees only ONE chat body is ever mounted.) */}
|
|
769
806
|
</div>
|
|
770
807
|
)}
|
|
771
808
|
{/* #Stage B: persistent right chat column (md+) — see ChatPane.tsx
|
|
772
809
|
(ChatPopover minus the floating shell; TaskChatPanel inline body).
|
|
773
810
|
Switching nodes swaps `alias` in place (no remount). */}
|
|
774
811
|
{chatAlias && isDesktop && <ChatPane alias={chatAlias} onClose={closeChat} />}
|
|
812
|
+
{/* #Mobile two-level nav: chat is a full PAGE (not the old overlay).
|
|
813
|
+
Back arrow and browser back both land on the list (closeChat /
|
|
814
|
+
popstate) — 判据: 行为一致. */}
|
|
815
|
+
{mobileChatLevel && <ChatPane alias={chatAlias!} variant="page" onBack={closeChat} onClose={closeChat} />}
|
|
775
816
|
</div>
|
|
776
817
|
);
|
|
777
818
|
}
|
package/package.json
CHANGED
|
@@ -80,16 +80,23 @@ const HUB_HEALTH_EMPTY = {
|
|
|
80
80
|
sse_sessions: {},
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
// Login rate limit is 10/15min/IP (app/lib/rate-limit.ts): with 4 spec
|
|
84
|
+
// files in one combined run, per-test logins blow the budget and late
|
|
85
|
+
// tests land on the sign-in page (false red). Login once per worker.
|
|
86
|
+
let cachedSessionCookie: string | null = null;
|
|
83
87
|
async function login(context: Page['context'] extends () => infer C ? C : never) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
if (!cachedSessionCookie) {
|
|
89
|
+
const res = await context.request.post(`${BASE}/api/auth/login`, {
|
|
90
|
+
data: { password: process.env.ANET_DASHBOARD_PASSWORD || 'admin123' },
|
|
91
|
+
});
|
|
92
|
+
const setCookie = res.headers()['set-cookie'] || '';
|
|
93
|
+
const m = setCookie.match(/anet_dashboard_session=([^;]+)/);
|
|
94
|
+
if (m) cachedSessionCookie = decodeURIComponent(m[1]);
|
|
95
|
+
}
|
|
96
|
+
if (cachedSessionCookie) {
|
|
90
97
|
await context.addCookies([{
|
|
91
98
|
name: 'anet_dashboard_session',
|
|
92
|
-
value:
|
|
99
|
+
value: cachedSessionCookie,
|
|
93
100
|
domain: new URL(BASE).hostname,
|
|
94
101
|
path: '/',
|
|
95
102
|
}]);
|