diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index 88e9d7f..10e119e 100644 --- a/.github/workflows/plugin-api-v3.yml +++ b/.github/workflows/plugin-api-v3.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v4 with: repository: kachofugetsu09/akashic-agent - ref: c55a1eac942d489b8f41e81f6b1218d98ec1c6f1 + ref: d5e5177092d74de03588d8675f3504db36b7bacb path: .akashic-core - uses: actions/setup-python@v5 with: diff --git a/akashic.plugin.toml b/akashic.plugin.toml index a5a23c0..84321ad 100644 --- a/akashic.plugin.toml +++ b/akashic.plugin.toml @@ -1,5 +1,5 @@ schema_version = 1 name = "observe" -version = "1.4.0" +version = "1.4.1" api_version = 3 entrypoint = "plugin.py" diff --git a/dashboard_panel.js b/dashboard_panel.js deleted file mode 100644 index e664328..0000000 --- a/dashboard_panel.js +++ /dev/null @@ -1,650 +0,0 @@ -// ../observe/dashboard_panel.tsx -import { - useCallback, - useEffect, - useRef, - useState -} from "react"; -import { Grid, MetricTile, TrendChart, Sparkline, Chip, api } from "@akashic/dashboard-ui"; -import { Fragment, jsx, jsxs } from "react/jsx-runtime"; -var RANGES = [ - { key: "24h", label: "24 \u5C0F\u65F6" }, - { key: "7d", label: "7 \u5929" }, - { key: "30d", label: "30 \u5929" }, - { key: "all", label: "\u5168\u90E8" } -]; -var SOURCE_LABEL = { - log: "\u4E3B\u52A8\u65E5\u5FD7", - uncaught: "\u672A\u6355\u83B7\u5F02\u5E38", - asyncio: "asyncio \u4EFB\u52A1", - thread: "\u5B50\u7EBF\u7A0B" -}; -var STATUS_META = { - active: { label: "\u6D3B\u8DC3", tone: "warning" }, - acknowledged: { label: "\u5DF2\u786E\u8BA4", tone: "muted" }, - ignored: { label: "\u5DF2\u5FFD\u7565", tone: "success" } -}; -var TONE_BG = { - danger: "bg-danger", - warning: "bg-warning", - success: "bg-success", - accent: "bg-accent", - muted: "bg-subtle" -}; -function _compact(value) { - if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`; - if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`; - return String(Math.round(value)); -} -function _pct(value) { - return typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "\u2014"; -} -function _bucketLabel(bucket) { - if (bucket.includes("T")) return `${bucket.slice(11, 13)}:00`; - const [, m, d] = bucket.split("-"); - return m && d ? `${Number(m)}-${d}` : bucket; -} -function _shortTs(value) { - if (!value) return "\u2014"; - const dt = new Date(value); - if (Number.isNaN(dt.getTime())) return value || "\u2014"; - return `${dt.getMonth() + 1}-${String(dt.getDate()).padStart(2, "0")} ${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`; -} -function _delta(values) { - if (values.length < 2) return null; - const last = values[values.length - 1]; - const prev = values[values.length - 2]; - if (!prev) return null; - return (last - prev) / prev * 100; -} -function _ago(ms) { - if (!Number.isFinite(ms) || ms < 0) return "\u521A\u521A"; - const s = Math.floor(ms / 1e3); - if (s < 3) return "\u521A\u521A"; - if (s < 60) return `${s}s \u524D`; - const m = Math.floor(s / 60); - if (m < 60) return `${m}m \u524D`; - return `${Math.floor(m / 60)}h \u524D`; -} -function _severity(count, spiking) { - if (spiking || count >= 20) return "danger"; - if (count >= 5) return "warning"; - return "muted"; -} -function Card({ title, children, bodyClass, style }) { - return /* @__PURE__ */ jsxs( - "div", - { - className: "flex flex-col overflow-hidden border border-border bg-surface", - style, - children: [ - /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between border-b border-border px-4 py-2.5", children: /* @__PURE__ */ jsx("h3", { className: "text-[12px] font-medium text-muted", children: title }) }), - /* @__PURE__ */ jsx("div", { className: bodyClass ?? "p-4", children }) - ] - } - ); -} -function ErrorDrill({ - portalRef, - range, - onClose -}) { - const drillRef = useRef(null); - const closeButtonRef = useRef(null); - const [overview, setOverview] = useState(null); - const [facet, setFacet] = useState("type"); - const [q, setQ] = useState(""); - const [sections, setSections] = useState([]); - const [selFp, setSelFp] = useState(null); - const [detail, setDetail] = useState(null); - const [tab, setTab] = useState("trace"); - const [variant, setVariant] = useState(0); - const loadList = useCallback(async () => { - const [ov, list] = await Promise.all([ - api(`/api/dashboard/observe/global_errors/overview?range=${range}`), - api(`/api/dashboard/observe/global_errors?range=${range}&facet=${facet}&q=${encodeURIComponent(q)}`) - ]); - setOverview(ov); - setSections(list.sections ?? []); - const flat = (list.sections ?? []).flatMap((s) => s.items); - setSelFp((cur) => cur && flat.some((i) => i.fingerprint === cur) ? cur : flat[0]?.fingerprint ?? null); - }, [range, facet, q]); - useEffect(() => { - void loadList(); - }, [loadList]); - useEffect(() => { - if (!selFp) { - setDetail(null); - return; - } - let alive = true; - void (async () => { - const d = await api(`/api/dashboard/observe/global_errors/${selFp}?range=${range}`); - if (alive) { - setDetail(d); - setVariant(0); - setTab("trace"); - } - })(); - return () => { - alive = false; - }; - }, [selFp, range]); - const close = useCallback(() => { - onClose(); - }, [onClose]); - useEffect(() => { - closeButtonRef.current?.focus(); - return () => portalRef.current?.focus(); - }, [portalRef]); - useEffect(() => { - const onKey = (e) => { - if (e.key === "Escape") close(); - if (e.key !== "Tab") return; - const focusable = Array.from( - drillRef.current?.querySelectorAll( - 'button:not([disabled]), input:not([disabled]), select:not([disabled]), [href], [tabindex]:not([tabindex="-1"])' - ) ?? [] - ); - if (focusable.length === 0) return; - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - }; - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [close]); - const setStatus = async (status) => { - if (!detail) return; - await api(`/api/dashboard/observe/global_errors/${detail.fingerprint}/status?value=${status}`, { method: "POST" }); - await loadList(); - setDetail((d) => d ? { ...d, status } : d); - }; - const gotoSession = (key) => { - window.dispatchEvent(new CustomEvent("akashic:goto-session", { detail: key })); - close(); - }; - return /* @__PURE__ */ jsxs(Fragment, { children: [ - /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "fixed inset-0 z-30 bg-black/55", onClick: close }), - /* @__PURE__ */ jsxs( - "div", - { - ref: drillRef, - role: "dialog", - "aria-modal": "true", - "aria-labelledby": "observe-error-dialog-title", - className: "fixed z-40 flex flex-col overflow-hidden rounded-md border border-border-strong bg-surface", - style: { - width: "min(1180px, 94vw)", - height: "min(84vh, 760px)", - left: "50%", - top: "50%", - marginLeft: "calc(min(1180px, 94vw) / -2)", - marginTop: "calc(min(84vh, 760px) / -2)" - }, - children: [ - /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-4 border-b border-border px-5 py-4", children: [ - /* @__PURE__ */ jsx( - "button", - { - ref: closeButtonRef, - type: "button", - onClick: close, - className: "grid h-10 w-10 place-items-center rounded-md border border-border-strong bg-surface-2 text-[18px] text-muted transition-colors hover:text-fg", - "aria-label": "\u5173\u95ED\u9519\u8BEF\u5206\u6790", - title: "\u8FD4\u56DE (Esc)", - children: "\u2039" - } - ), - /* @__PURE__ */ jsx("span", { className: "font-mono text-[26px] font-semibold tabular-nums text-danger", children: overview?.total ?? "\u2014" }), - /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [ - /* @__PURE__ */ jsxs("div", { id: "observe-error-dialog-title", className: "text-sm font-semibold", children: [ - "\u9519\u8BEF \xB7 ", - RANGES.find((r) => r.key === range)?.label ?? range - ] }), - /* @__PURE__ */ jsxs("div", { className: "mt-0.5 flex items-center gap-3 text-[11px] text-muted", children: [ - /* @__PURE__ */ jsxs("span", { children: [ - overview?.types ?? 0, - " \u4E2A\u7C7B\u578B" - ] }), - (overview?.new_types ?? 0) > 0 && /* @__PURE__ */ jsxs("span", { children: [ - overview?.new_types, - " \u4E2A\u65B0\u7C7B\u578B" - ] }), - (overview?.spiking_types ?? 0) > 0 && /* @__PURE__ */ jsxs("span", { className: "font-semibold text-danger", children: [ - overview?.spiking_types, - " \u4E2A\u6B63\u5728\u7206\u53D1" - ] }) - ] }) - ] }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-3 border-b border-border px-4 py-2.5", children: [ - /* @__PURE__ */ jsx("div", { className: "flex gap-1 rounded-md border border-border bg-bg p-0.5", children: [ - { k: "type", l: "\u6309\u7C7B\u578B" }, - { k: "source", l: "\u6309\u6765\u6E90" }, - { k: "channel", l: "\u6309\u901A\u9053" } - ].map((f) => /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick: () => setFacet(f.k), - className: `rounded-[4px] px-2.5 py-1 text-[11px] transition-colors ${facet === f.k ? "bg-surface-3 text-fg" : "text-muted hover:text-fg"}`, - children: f.l - }, - f.k - )) }), - /* @__PURE__ */ jsx( - "input", - { - value: q, - onChange: (e) => setQ(e.target.value), - "aria-label": "\u641C\u7D22\u9519\u8BEF", - placeholder: "\u6309\u6D88\u606F / \u6A21\u5757\u8FC7\u6EE4\u2026", - className: "w-[280px] rounded-md border border-border bg-bg px-3 py-1.5 text-[11.5px] text-fg outline-none focus:border-accent-deep" - } - ) - ] }), - /* @__PURE__ */ jsxs("div", { className: "grid min-h-0 flex-1 grid-cols-[340px_1fr]", children: [ - /* @__PURE__ */ jsxs("div", { className: "overflow-auto border-r border-border p-1.5", children: [ - sections.map((section) => /* @__PURE__ */ jsxs("div", { children: [ - section.label && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-2.5 pb-1 pt-3 text-[11px] font-medium text-muted", children: [ - /* @__PURE__ */ jsx("span", { children: section.label }), - /* @__PURE__ */ jsxs("span", { children: [ - section.count, - " \u6B21" - ] }) - ] }), - section.items.map((g) => /* @__PURE__ */ jsx(ErrorRow, { g, active: g.fingerprint === selFp, onClick: () => setSelFp(g.fingerprint) }, g.fingerprint)) - ] }, section.key)), - sections.length === 0 && /* @__PURE__ */ jsx("div", { className: "p-6 text-[12.5px] text-muted", children: "\u6240\u9009\u533A\u95F4\u5185\u6CA1\u6709\u9519\u8BEF\u3002" }) - ] }), - detail ? /* @__PURE__ */ jsx( - ErrorDetail, - { - detail, - tab, - setTab, - variant, - setVariant, - onStatus: setStatus, - onGoto: gotoSession - } - ) : /* @__PURE__ */ jsx("div", { className: "grid place-items-center text-[13px] text-muted", children: "\u9009\u62E9\u5DE6\u4FA7\u4E00\u4E2A\u9519\u8BEF\u67E5\u770B\u73B0\u573A" }) - ] }) - ] - } - ) - ] }); -} -function ErrorRow({ g, active, onClick }) { - const tone = _severity(g.count, g.is_spiking); - const spark = g.spark ?? []; - return /* @__PURE__ */ jsxs( - "button", - { - type: "button", - onClick, - className: `grid w-full grid-cols-[9px_1fr_auto] items-center gap-2.5 border-b border-border px-3 py-2.5 text-left transition-colors duration-150 ${active ? "bg-accent-soft" : "hover:bg-surface-2"}`, - children: [ - /* @__PURE__ */ jsx("span", { className: "relative flex h-2 w-2", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: `relative inline-flex h-2 w-2 rounded-full ${TONE_BG[tone]}` }) }), - /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [ - /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 font-mono text-[12.5px]", children: [ - /* @__PURE__ */ jsx("span", { className: "truncate", children: g.error_type }), - g.is_new && /* @__PURE__ */ jsx("span", { className: "text-[9px] font-semibold text-accent", children: "\u65B0" }), - g.is_spiking && /* @__PURE__ */ jsx("span", { className: "text-[9px] font-semibold text-danger", children: "\u7206\u53D1" }) - ] }), - /* @__PURE__ */ jsx("div", { className: "mt-0.5 truncate font-mono text-[10px] text-subtle", children: g.logger_name }), - /* @__PURE__ */ jsxs("div", { className: "mt-1 flex gap-2.5 text-[10px] text-muted", children: [ - /* @__PURE__ */ jsxs("span", { children: [ - /* @__PURE__ */ jsx("b", { className: "font-semibold text-fg", children: g.count }), - " \u6B21" - ] }), - /* @__PURE__ */ jsxs("span", { children: [ - /* @__PURE__ */ jsx("b", { className: "font-semibold text-fg", children: g.sessions }), - " session" - ] }) - ] }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-end gap-1.5", children: [ - /* @__PURE__ */ jsx("div", { className: "h-[22px] w-[62px]", children: spark.length > 1 && /* @__PURE__ */ jsx(Sparkline, { data: spark, tone, height: 22 }) }), - /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] text-subtle", children: _shortTs(g.last_ts) }) - ] }) - ] - } - ); -} -function ErrorDetail({ - detail, - tab, - setTab, - variant, - setVariant, - onStatus, - onGoto -}) { - const status = STATUS_META[detail.status] ?? STATUS_META.active; - const tone = _severity(detail.count, false); - const activeVariant = detail.variants[variant] ?? detail.variants[0]; - return /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-col", children: [ - /* @__PURE__ */ jsxs("div", { className: "border-b border-border px-5 py-4", children: [ - /* @__PURE__ */ jsx("div", { className: "font-mono text-[19px] font-semibold", children: detail.error_type }), - /* @__PURE__ */ jsx("div", { className: "mt-1.5 font-mono text-[12px] leading-relaxed text-danger", children: detail.message }), - /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap gap-1.5", children: [ - /* @__PURE__ */ jsx(Chip, { children: detail.logger_name }), - /* @__PURE__ */ jsxs(Chip, { children: [ - "\u6765\u6E90 \xB7 ", - SOURCE_LABEL[detail.source] ?? detail.source - ] }), - /* @__PURE__ */ jsx(Chip, { children: detail.channel }), - /* @__PURE__ */ jsx(Chip, { tone: "danger", children: detail.level }), - /* @__PURE__ */ jsx(Chip, { tone: status.tone, children: status.label }) - ] }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-4 gap-px border-b border-border bg-border", children: [ - /* @__PURE__ */ jsx(Blast, { label: "\u7D2F\u8BA1\u6B21\u6570", value: String(detail.count) }), - /* @__PURE__ */ jsx(Blast, { label: "\u72EC\u7ACB session", value: String(detail.sessions) }), - /* @__PURE__ */ jsx(Blast, { label: "\u9996\u6B21", value: _shortTs(detail.first_ts), small: true }), - /* @__PURE__ */ jsx(Blast, { label: "\u6700\u8FD1", value: _shortTs(detail.last_ts), small: true }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "flex gap-1 border-b border-border px-5 pt-3", children: [ - /* @__PURE__ */ jsx(TabBtn, { active: tab === "trend", onClick: () => setTab("trend"), children: "\u8D8B\u52BF" }), - /* @__PURE__ */ jsxs(TabBtn, { active: tab === "trace", onClick: () => setTab("trace"), children: [ - "Traceback", - detail.variants.length > 1 ? ` \xB7 ${detail.variants.length} \u53D8\u4F53` : "" - ] }), - /* @__PURE__ */ jsxs(TabBtn, { active: tab === "occ", onClick: () => setTab("occ"), children: [ - "\u73B0\u573A \xB7 ", - detail.occurrences.length - ] }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "min-h-0 flex-1 overflow-auto px-5 py-4", children: [ - tab === "trend" && /* @__PURE__ */ jsx( - TrendChart, - { - data: detail.trend.map((p) => ({ label: _bucketLabel(p.bucket), value: p.count })), - kind: "bar", - tone, - valueFmt: (n) => String(n), - empty: "\u533A\u95F4\u5185\u65E0\u53D1\u4F5C" - } - ), - tab === "trace" && /* @__PURE__ */ jsxs("div", { children: [ - detail.variants.length > 1 && /* @__PURE__ */ jsx("div", { className: "mb-3 flex gap-2", children: detail.variants.map((v, i) => /* @__PURE__ */ jsxs( - "button", - { - type: "button", - onClick: () => setVariant(i), - className: `rounded border px-2.5 py-1.5 text-left text-[10.5px] ${i === variant ? "border-accent-deep bg-accent-soft text-fg" : "border-border bg-bg text-muted"}`, - children: [ - /* @__PURE__ */ jsx("b", { className: "text-fg", children: v.count }), - " \u6B21 \xB7 \u53D8\u4F53 ", - i + 1 - ] - }, - v.fingerprint - )) }), - /* @__PURE__ */ jsx("pre", { className: "m-0 max-h-[280px] overflow-auto rounded border border-border bg-bg p-4 font-mono text-[11px] leading-relaxed text-muted", children: activeVariant?.traceback_text || detail.traceback_text }) - ] }), - tab === "occ" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [ - detail.occurrences.length === 0 && /* @__PURE__ */ jsx("div", { className: "text-[12px] text-muted", children: "\u65E0\u53EF\u5173\u8054\u7684 session \u73B0\u573A\u3002" }), - detail.occurrences.map((o) => /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[auto_1fr_auto] items-center gap-3.5 border-b border-border bg-bg px-3.5 py-2.5", children: [ - /* @__PURE__ */ jsx("span", { className: "font-mono text-[11px] text-accent", children: _shortTs(o.ts) }), - /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [ - /* @__PURE__ */ jsx("div", { className: "truncate text-[12px]", children: o.user_preview || "\uFF08\u65E0\u7528\u6237\u6D88\u606F\uFF09" }), - /* @__PURE__ */ jsxs("div", { className: "mt-0.5 font-mono text-[10px] text-subtle", children: [ - "session ", - o.session_key - ] }) - ] }), - /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick: () => onGoto(o.session_key), - className: "whitespace-nowrap rounded border border-accent-deep bg-accent-soft px-2.5 py-1.5 text-[10.5px] text-accent-ink", - children: "\u67E5\u770B\u5BF9\u8BDD" - } - ) - ] }, o.session_key)) - ] }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 gap-2 border-t border-border px-5 py-3", children: [ - /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick: () => detail.occurrences[0] && onGoto(detail.occurrences[0].session_key), - disabled: detail.occurrences.length === 0, - className: "rounded border border-accent-deep bg-accent-soft px-3 py-2 text-[11px] text-accent-ink transition-colors disabled:opacity-40", - children: "\u67E5\u770B\u6700\u8FD1\u5BF9\u8BDD" - } - ), - /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick: () => void navigator.clipboard?.writeText(detail.traceback_text), - className: "rounded border border-border-strong bg-surface-2 px-3 py-2 text-[11px] text-muted transition-colors hover:text-fg", - children: "\u590D\u5236 Traceback" - } - ), - /* @__PURE__ */ jsx("div", { className: "flex-1" }), - /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onStatus("acknowledged"), className: "rounded border border-border-strong bg-surface-2 px-3 py-2 text-[11px] text-muted transition-colors hover:text-fg", children: "\u6807\u8BB0\u5DF2\u786E\u8BA4" }), - /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onStatus("ignored"), className: "rounded border border-border-strong bg-surface-2 px-3 py-2 text-[11px] text-muted transition-colors hover:border-danger/40 hover:text-danger", children: "\u5FFD\u7565\u6B64\u7C7B\u578B" }) - ] }) - ] }); -} -function Blast({ label, value, small }) { - return /* @__PURE__ */ jsxs("div", { className: "bg-surface px-4 py-3", children: [ - /* @__PURE__ */ jsx("div", { className: "text-[10px] text-subtle", children: label }), - /* @__PURE__ */ jsx("div", { className: `mt-1.5 font-mono font-semibold tabular-nums ${small ? "text-[12.5px]" : "text-[18px]"}`, children: value }) - ] }); -} -function TabBtn({ active, onClick, children }) { - return /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick, - className: `-mb-px border-b-2 px-3 py-2 text-[11.5px] transition-colors ${active ? "border-accent text-fg" : "border-transparent text-muted hover:text-fg"}`, - children - } - ); -} -function SkelBlock({ className }) { - return /* @__PURE__ */ jsx("div", { className: `relative overflow-hidden rounded border border-border bg-surface-2 ${className}` }); -} -function ObserveSkeleton() { - return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-6", children: [ - /* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between", children: [ - /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [ - /* @__PURE__ */ jsx(SkelBlock, { className: "h-7 w-48" }), - /* @__PURE__ */ jsx(SkelBlock, { className: "h-3 w-64 rounded" }) - ] }), - /* @__PURE__ */ jsx(SkelBlock, { className: "h-9 w-56" }) - ] }), - /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-4", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx(SkelBlock, { className: "h-[132px]" }, i)) }), - /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-4", children: [0, 1, 2, 3].map((i) => /* @__PURE__ */ jsx(SkelBlock, { className: "h-[218px]" }, i)) }) - ] }); -} -function ObserveMain(_props) { - const [range, setRange] = useState("24h"); - const [overview, setOverview] = useState(null); - const [points, setPoints] = useState([]); - const [gErr, setGErr] = useState(null); - const [drillOpen, setDrillOpen] = useState(false); - const [updatedAt, setUpdatedAt] = useState(0); - const [nowTs, setNowTs] = useState(() => Date.now()); - const [refreshing, setRefreshing] = useState(false); - const portalRef = useRef(null); - const overviewRef = useRef(null); - const load = useCallback(async () => { - setRefreshing(true); - try { - const [ov, series, ge] = await Promise.all([ - api(`/api/dashboard/observe/overview?range=${range}`), - api(`/api/dashboard/observe/timeseries?range=${range}`), - api(`/api/dashboard/observe/global_errors/overview?range=${range}`) - ]); - setOverview(ov); - setPoints(series.points ?? []); - setGErr(ge); - setUpdatedAt(Date.now()); - } finally { - setRefreshing(false); - } - }, [range]); - useEffect(() => { - void load(); - const id = window.setInterval(() => void load(), 15e3); - return () => window.clearInterval(id); - }, [load]); - useEffect(() => { - const id = window.setInterval(() => setNowTs(Date.now()), 1e3); - return () => window.clearInterval(id); - }, []); - useEffect(() => { - if (overviewRef.current) overviewRef.current.inert = drillOpen; - }, [drillOpen]); - if (!overview) { - return /* @__PURE__ */ jsx(ObserveSkeleton, {}); - } - const turnSeries = points.map((p) => p.turns); - const errorSeries = points.map((p) => p.errors); - const tokenSeries = points.map((p) => p.input_tokens); - const passiveHitSeries = points.map((p) => (p.passive_cache_hit_rate ?? 0) * 100); - const proactiveHitSeries = points.map((p) => (p.proactive_cache_hit_rate ?? 0) * 100); - const iterSeries = points.map((p) => p.avg_iteration ?? 0); - const labelled = (vals) => points.map((p, i) => ({ label: _bucketLabel(p.bucket), value: vals[i] })); - const gErrTotal = gErr?.total ?? overview.errors; - return /* @__PURE__ */ jsxs(Fragment, { children: [ - /* @__PURE__ */ jsxs( - "div", - { - ref: overviewRef, - "aria-hidden": drillOpen || void 0, - className: "flex flex-col gap-5 p-6 transition-opacity duration-150", - style: drillOpen ? { opacity: 0.35, pointerEvents: "none" } : void 0, - children: [ - /* @__PURE__ */ jsxs("div", { className: "flex items-end justify-between", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [ - /* @__PURE__ */ jsx("span", { className: "detail-title", children: "Observe \xB7 \u76D1\u6D4B" }), - /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-success", children: "\u5B9E\u65F6\u66F4\u65B0" }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "detail-subtext", children: [ - "Agent \u4E3B\u5FAA\u73AF\u9065\u6D4B \xB7 Token / \u8FED\u4EE3 / \u9519\u8BEF", - /* @__PURE__ */ jsxs("span", { className: "ml-2 font-mono text-[11px] text-subtle", children: [ - "\u66F4\u65B0\u4E8E ", - _ago(nowTs - updatedAt) - ] }) - ] }) - ] }), - /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [ - /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick: () => void load(), - className: `grid h-10 w-10 place-items-center rounded-md border border-border bg-surface-2 text-muted transition-colors hover:border-border-strong hover:text-fg ${refreshing ? "animate-spin" : ""}`, - "aria-label": "\u5237\u65B0\u76D1\u6D4B\u6570\u636E", - title: "\u5237\u65B0", - children: "\u21BB" - } - ), - /* @__PURE__ */ jsx("div", { className: "flex gap-1 rounded-md border border-border bg-surface-2 p-1", children: RANGES.map((r) => /* @__PURE__ */ jsx( - "button", - { - type: "button", - onClick: () => setRange(r.key), - className: `min-h-10 rounded-[4px] px-2.5 py-1 text-[11px] transition-colors ${range === r.key ? "bg-accent text-accent-ink" : "text-muted hover:bg-surface-3 hover:text-fg"}`, - "aria-pressed": range === r.key, - children: r.label - }, - r.key - )) }) - ] }) - ] }), - /* @__PURE__ */ jsxs( - "section", - { - className: `flex min-h-16 items-center justify-between gap-4 border px-4 py-3 ${gErrTotal > 0 ? "border-danger/40 bg-danger/10" : "border-success/35 bg-success/10"}`, - "aria-label": "\u8FD0\u884C\u72B6\u6001", - children: [ - /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [ - /* @__PURE__ */ jsx("div", { className: `text-[13px] font-semibold ${gErrTotal > 0 ? "text-danger" : "text-success"}`, children: gErrTotal > 0 ? `${gErrTotal} \u6761\u9519\u8BEF\u9700\u8981\u67E5\u770B` : "\u5F53\u524D\u533A\u95F4\u6CA1\u6709\u91C7\u96C6\u5230\u9519\u8BEF" }), - /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11.5px] text-muted", children: gErrTotal > 0 ? `${gErr?.types ?? 0} \u4E2A\u9519\u8BEF\u7C7B\u578B\uFF0C\u5148\u67E5\u770B\u7206\u53D1\u548C\u65B0\u51FA\u73B0\u7684\u7C7B\u578B\u3002` : "\u4E3B\u5FAA\u73AF\u9065\u6D4B\u6301\u7EED\u66F4\u65B0\uFF0C\u7F13\u5B58\u4E0E\u8FED\u4EE3\u6307\u6807\u89C1\u4E0B\u65B9\u3002" }) - ] }), - gErrTotal > 0 && /* @__PURE__ */ jsx( - "button", - { - type: "button", - ref: portalRef, - onClick: () => setDrillOpen(true), - className: "min-h-10 flex-shrink-0 rounded-md border border-danger/40 bg-surface px-3 text-[12px] font-semibold text-danger hover:bg-danger/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", - children: "\u67E5\u770B\u9519\u8BEF\u5206\u6790" - } - ) - ] - } - ), - /* @__PURE__ */ jsxs(Grid, { columns: 3, children: [ - /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(MetricTile, { label: "\u5BF9\u8BDD\u8F6E\u6570", value: _compact(overview.turns), delta: _delta(turnSeries), sub: overview.last_ts ? `\u6700\u8FD1 ${_shortTs(overview.last_ts)}` : "\u65E0\u8BB0\u5F55", tone: "accent", spark: turnSeries }) }), - /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(MetricTile, { label: "\u88AB\u52A8 KV \u547D\u4E2D\u7387", value: _pct(overview.passive_cache_hit_rate), sub: `\u4E3B\u52A8 ${_pct(overview.proactive_cache_hit_rate)}`, tone: "success", spark: passiveHitSeries }) }), - /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(MetricTile, { label: "\u5E73\u5747\u8FED\u4EE3", value: overview.avg_iteration != null ? overview.avg_iteration.toFixed(1) : "\u2014", unit: `\u5CF0 ${overview.max_iteration}`, sub: "\u6BCF\u8F6E LLM \u8C03\u7528\u6B21\u6570", tone: "warning", spark: iterSeries }) }) - ] }), - /* @__PURE__ */ jsxs(Grid, { columns: 2, children: [ - /* @__PURE__ */ jsx(Card, { title: "\u8F93\u5165 Token \u8D8B\u52BF", children: /* @__PURE__ */ jsx(TrendChart, { data: labelled(tokenSeries), kind: "area", tone: "accent", valueFmt: _compact }) }), - /* @__PURE__ */ jsx(Card, { title: "\u5E73\u5747\u8FED\u4EE3\u8D8B\u52BF", children: /* @__PURE__ */ jsx(TrendChart, { data: labelled(iterSeries), kind: "area", tone: "warning", valueFmt: (n) => n.toFixed(1) }) }) - ] }), - /* @__PURE__ */ jsxs("details", { className: "border-t border-border pt-1", children: [ - /* @__PURE__ */ jsx("summary", { className: "min-h-11 cursor-pointer py-3 text-[12px] font-semibold text-fg focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent", children: "\u67E5\u770B\u7F13\u5B58\u547D\u4E2D\u4E0E\u9519\u8BEF\u8D8B\u52BF" }), - /* @__PURE__ */ jsxs(Grid, { columns: 2, children: [ - /* @__PURE__ */ jsx(Card, { title: "\u5168\u5C40\u88AB\u52A8\u94FE\u8DEF\u547D\u4E2D\u7387\u8D8B\u52BF", children: /* @__PURE__ */ jsx(TrendChart, { data: labelled(passiveHitSeries), kind: "area", tone: "success", valueFmt: (n) => `${n.toFixed(0)}%` }) }), - /* @__PURE__ */ jsx(Card, { title: "\u5168\u5C40\u4E3B\u52A8\u94FE\u8DEF\u547D\u4E2D\u7387\u8D8B\u52BF", children: /* @__PURE__ */ jsx(TrendChart, { data: labelled(proactiveHitSeries), kind: "area", tone: "accent", valueFmt: (n) => `${n.toFixed(0)}%` }) }), - /* @__PURE__ */ jsx(Card, { title: "\u9519\u8BEF\u8D8B\u52BF", children: /* @__PURE__ */ jsx(TrendChart, { data: labelled(errorSeries), kind: "bar", tone: "danger", valueFmt: (n) => String(n), empty: "\u6240\u9009\u533A\u95F4\u5185\u6CA1\u6709\u9519\u8BEF" }) }) - ] }) - ] }) - ] - } - ), - drillOpen && /* @__PURE__ */ jsx(ErrorDrill, { portalRef, range, onClose: () => setDrillOpen(false) }) - ] }); -} -window.AkashicDashboard.registerPlugin({ - id: "observe", - label: "\u8FD0\u884C\u76D1\u6D4B", - viewLabel: "\u8FD0\u884C\u76D1\u6D4B", - layout: "workbench", - pageSize: 30, - rowKey: "id", - countTitle(total) { - return `${total} \u8F6E\u9065\u6D4B`; - }, - columns: [ - { key: "session_key", label: "\u4F1A\u8BDD", width: 120, cellClass: "mono cell-session", rawTitle: true }, - { key: "ts", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, - { key: "error", label: "\u9519\u8BEF", flex: true, cellClass: "content-preview" } - ], - async getCount() { - try { - const ov = await api("/api/dashboard/observe/overview?range=all"); - return ov.turns || 0; - } catch { - return null; - } - }, - async fetchPage({ page, pageSize }) { - const data = await api( - `/api/dashboard/observe/errors?range=all&page=${page}&page_size=${pageSize}` - ); - return { items: data.items || [], total: data.total || 0 }; - }, - Main: ObserveMain -}); diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx index 40ee5aa..78cfb91 100644 --- a/dashboard_panel.tsx +++ b/dashboard_panel.tsx @@ -1,4 +1,3 @@ -/// import { useCallback, useEffect, @@ -7,7 +6,24 @@ import { type ReactElement, type ReactNode, } from "react"; -import { Grid, MetricTile, TrendChart, Sparkline, Chip, api, type ChartTone } from "@akashic/dashboard-ui"; +import { createRoot } from "react-dom/client"; +import type { WebHostContextV1, WebUiDisposer } from "@akashic/web-ui-v1"; +import type { + ChartTone, + WorkbenchDispatch, + WorkbenchPanelEntry, + WorkbenchUi, +} from "@akashic/workbench-ui-v2"; + +let dashboardRequest: WebHostContextV1["http"]["request"] | null = null; + +async function api(path: string, init?: RequestInit): Promise { + if (!dashboardRequest) throw new Error("Observe 工作台面板未激活"); + const response = await dashboardRequest(path, init); + const body = await response.json() as T & { detail?: unknown; message?: unknown }; + if (!response.ok) throw new Error(String(body.detail ?? body.message ?? `HTTP ${response.status}`)); + return body; +} interface Overview { range: string; @@ -212,12 +228,16 @@ function Card({ title, children, bodyClass, style }: { title: string; children: function ErrorDrill({ portalRef, + fallbackRef, range, onClose, + ui, }: { portalRef: React.RefObject; + fallbackRef: React.RefObject; range: string; onClose: () => void; + ui: WorkbenchUi; }): ReactElement { const drillRef = useRef(null); const closeButtonRef = useRef(null); @@ -227,41 +247,66 @@ function ErrorDrill({ const [sections, setSections] = useState([]); const [selFp, setSelFp] = useState(null); const [detail, setDetail] = useState(null); + const [listError, setListError] = useState(null); + const [detailError, setDetailError] = useState(null); + const [savingStatus, setSavingStatus] = useState(false); const [tab, setTab] = useState<"trend" | "trace" | "occ">("trace"); const [variant, setVariant] = useState(0); + const listReadRef = useRef(null); + const statusReadRef = useRef(null); const loadList = useCallback(async () => { - const [ov, list] = await Promise.all([ - api(`/api/dashboard/observe/global_errors/overview?range=${range}`), - api(`/api/dashboard/observe/global_errors?range=${range}&facet=${facet}&q=${encodeURIComponent(q)}`), - ]); - setOverview(ov); - setSections(list.sections ?? []); - const flat = (list.sections ?? []).flatMap((s) => s.items); - setSelFp((cur) => (cur && flat.some((i) => i.fingerprint === cur) ? cur : flat[0]?.fingerprint ?? null)); + listReadRef.current?.abort(); + const controller = new AbortController(); + listReadRef.current = controller; + setListError(null); + try { + const [ov, list] = await Promise.all([ + api(`/api/dashboard/observe/global_errors/overview?range=${range}`, { signal: controller.signal }), + api(`/api/dashboard/observe/global_errors?range=${range}&facet=${facet}&q=${encodeURIComponent(q)}`, { signal: controller.signal }), + ]); + if (controller.signal.aborted) return; + setOverview(ov); + setSections(list.sections ?? []); + const flat = (list.sections ?? []).flatMap((s) => s.items); + setSelFp((cur) => (cur && flat.some((i) => i.fingerprint === cur) ? cur : flat[0]?.fingerprint ?? null)); + } catch (error) { + if (!controller.signal.aborted) { + setListError(error instanceof Error ? error.message : "错误列表读取失败"); + } + } finally { + if (listReadRef.current === controller) listReadRef.current = null; + } }, [range, facet, q]); useEffect(() => { void loadList(); + return () => listReadRef.current?.abort(); }, [loadList]); useEffect(() => { if (!selFp) { setDetail(null); + setDetailError(null); return; } - let alive = true; - void (async () => { - const d = await api(`/api/dashboard/observe/global_errors/${selFp}?range=${range}`); - if (alive) { + const controller = new AbortController(); + setDetailError(null); + void api( + `/api/dashboard/observe/global_errors/${selFp}?range=${range}`, + { signal: controller.signal }, + ).then((d) => { + if (!controller.signal.aborted) { setDetail(d); setVariant(0); setTab("trace"); } - })(); - return () => { - alive = false; - }; + }, (error: unknown) => { + if (!controller.signal.aborted) { + setDetailError(error instanceof Error ? error.message : "错误详情读取失败"); + } + }); + return () => controller.abort(); }, [selFp, range]); const close = useCallback(() => { @@ -270,8 +315,11 @@ function ErrorDrill({ useEffect(() => { closeButtonRef.current?.focus(); - return () => portalRef.current?.focus(); - }, [portalRef]); + return () => { + statusReadRef.current?.abort(); + (portalRef.current ?? fallbackRef.current)?.focus(); + }; + }, [fallbackRef, portalRef]); useEffect(() => { const onKey = (e: KeyboardEvent): void => { @@ -298,10 +346,29 @@ function ErrorDrill({ }, [close]); const setStatus = async (status: string): Promise => { - if (!detail) return; - await api(`/api/dashboard/observe/global_errors/${detail.fingerprint}/status?value=${status}`, { method: "POST" }); - await loadList(); - setDetail((d) => (d ? { ...d, status } : d)); + if (!detail || statusReadRef.current) return; + const fingerprint = detail.fingerprint; + const controller = new AbortController(); + statusReadRef.current = controller; + setSavingStatus(true); + setDetailError(null); + try { + await api( + `/api/dashboard/observe/global_errors/${fingerprint}/status?value=${status}`, + { method: "POST", signal: controller.signal }, + ); + if (controller.signal.aborted) return; + setDetail((d) => (d?.fingerprint === fingerprint ? { ...d, status } : d)); + } catch (error) { + if (!controller.signal.aborted) { + setDetailError(error instanceof Error ? error.message : "错误状态更新失败"); + } + } finally { + if (statusReadRef.current === controller) { + statusReadRef.current = null; + if (!controller.signal.aborted) setSavingStatus(false); + } + } }; const gotoSession = (key: string): void => { @@ -380,6 +447,7 @@ function ErrorDrill({ {/* 左群组 / 右详情 */}
+ {listError &&
{listError}
} {sections.map((section) => (
{section.label && ( @@ -389,22 +457,26 @@ function ErrorDrill({
)} {section.items.map((g) => ( - setSelFp(g.fingerprint)} /> + setSelFp(g.fingerprint)} ui={ui} /> ))}
))} - {sections.length === 0 &&
所选区间内没有错误。
} + {!listError && sections.length === 0 &&
所选区间内没有错误。
}
- {detail ? ( + {detailError ? ( +
{detailError}
+ ) : detail ? ( ) : (
选择左侧一个错误查看现场
@@ -415,9 +487,20 @@ function ErrorDrill({ ); } -function ErrorRow({ g, active, onClick }: { g: GErrGroup; active: boolean; onClick: () => void }): ReactElement { +function ErrorRow({ + g, + active, + onClick, + ui, +}: { + g: GErrGroup; + active: boolean; + onClick: () => void; + ui: WorkbenchUi; +}): ReactElement { const tone = _severity(g.count, g.is_spiking); const spark = g.spark ?? []; + const Sparkline = ui.Sparkline; return (
- -
@@ -633,7 +721,8 @@ function ObserveSkeleton(): ReactElement { // ── 监测主面板 ──────────────────────────────────────────────────────────────── // Grafana-style monitoring overview over observe.db agent-loop telemetry. -function ObserveMain(_props: { dispatch: PluginDispatch }): ReactElement { +function ObserveMain({ dispatch }: { dispatch: WorkbenchDispatch }): ReactElement { + const { Grid, MetricTile, TrendChart } = dispatch.ui; const [range, setRange] = useState("24h"); const [overview, setOverview] = useState(null); const [points, setPoints] = useState([]); @@ -642,23 +731,38 @@ function ObserveMain(_props: { dispatch: PluginDispatch }): ReactElement { const [updatedAt, setUpdatedAt] = useState(0); const [nowTs, setNowTs] = useState(() => Date.now()); const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); const portalRef = useRef(null); + const refreshRef = useRef(null); const overviewRef = useRef(null); + const mainReadRef = useRef(null); const load = useCallback(async () => { + mainReadRef.current?.abort(); + const controller = new AbortController(); + mainReadRef.current = controller; setRefreshing(true); + setError(null); try { const [ov, series, ge] = await Promise.all([ - api(`/api/dashboard/observe/overview?range=${range}`), - api<{ points: SeriesPoint[] }>(`/api/dashboard/observe/timeseries?range=${range}`), - api(`/api/dashboard/observe/global_errors/overview?range=${range}`), + api(`/api/dashboard/observe/overview?range=${range}`, { signal: controller.signal }), + api<{ points: SeriesPoint[] }>(`/api/dashboard/observe/timeseries?range=${range}`, { signal: controller.signal }), + api(`/api/dashboard/observe/global_errors/overview?range=${range}`, { signal: controller.signal }), ]); + if (controller.signal.aborted) return; setOverview(ov); setPoints(series.points ?? []); setGErr(ge); setUpdatedAt(Date.now()); + } catch (reason) { + if (!controller.signal.aborted) { + setError(reason instanceof Error ? reason.message : "监测数据读取失败"); + } } finally { - setRefreshing(false); + if (mainReadRef.current === controller) { + mainReadRef.current = null; + if (!controller.signal.aborted) setRefreshing(false); + } } }, [range]); @@ -666,9 +770,21 @@ function ObserveMain(_props: { dispatch: PluginDispatch }): ReactElement { useEffect(() => { void load(); const id = window.setInterval(() => void load(), 15000); - return () => window.clearInterval(id); + return () => { + window.clearInterval(id); + mainReadRef.current?.abort(); + }; }, [load]); + useEffect(() => { + if (!refreshing || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + const animation = refreshRef.current?.animate( + [{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], + { duration: 1000, iterations: Infinity }, + ); + return () => animation?.cancel(); + }, [refreshing]); + // 1s tick 驱动"更新于 Xs 前"的相对时间标签。 useEffect(() => { const id = window.setInterval(() => setNowTs(Date.now()), 1000); @@ -680,7 +796,9 @@ function ObserveMain(_props: { dispatch: PluginDispatch }): ReactElement { }, [drillOpen]); if (!overview) { - return ; + return error + ?
{error}
+ : ; } const turnSeries = points.map((p) => p.turns); @@ -715,9 +833,10 @@ function ObserveMain(_props: { dispatch: PluginDispatch }): ReactElement {
+ {error &&
{error}
} +
0 ? "border-danger/40 bg-danger/10" : "border-success/35 bg-success/10"}`} aria-label="运行状态" @@ -804,15 +925,16 @@ function ObserveMain(_props: { dispatch: PluginDispatch }): ReactElement { - {drillOpen && setDrillOpen(false)} />} + {drillOpen && setDrillOpen(false)} ui={dispatch.ui} />} ); } -window.AkashicDashboard.registerPlugin({ +const panel = { id: "observe", label: "运行监测", viewLabel: "运行监测", + order: 60, layout: "workbench", pageSize: 30, rowKey: "id", @@ -827,21 +949,36 @@ window.AkashicDashboard.registerPlugin({ { key: "error", label: "错误", flex: true, cellClass: "content-preview" }, ], - async getCount(): Promise { + async getCount({ signal }: { signal: AbortSignal }): Promise { try { - const ov = await api("/api/dashboard/observe/overview?range=all"); + const ov = await api("/api/dashboard/observe/overview?range=all", { signal }); return ov.turns || 0; - } catch { + } catch (error) { + if (signal.aborted) throw error; return null; } }, - async fetchPage({ page, pageSize }: { page: number; pageSize: number }) { + async fetchPage({ page, pageSize, signal }: { page: number; pageSize: number; signal: AbortSignal }) { const data = await api<{ items: Record[]; total: number }>( `/api/dashboard/observe/errors?range=all&page=${page}&page_size=${pageSize}`, + { signal }, ); return { items: data.items || [], total: data.total || 0 }; }, - Main: ObserveMain, -}); + renderMain(container: HTMLElement, dispatch: WorkbenchDispatch): WebUiDisposer { + const root = createRoot(container); + root.render(); + return () => root.unmount(); + }, +} satisfies WorkbenchPanelEntry; + +export function activate(ctx: WebHostContextV1): WebUiDisposer { + dashboardRequest = ctx.http.request; + const release = ctx.ui.inject("workbench.panels.v2", (mount) => mount.register(panel)); + return () => { + release(); + dashboardRequest = null; + }; +} diff --git a/plugin.py b/plugin.py index e8cab77..402ecd3 100644 --- a/plugin.py +++ b/plugin.py @@ -32,10 +32,16 @@ api_version = 3 name = "observe" -version = "1.4.0" +version = "1.4.1" inject = (UI_SLOTS,) workspace_roots = ("observe",) dashboard_module = "dashboard.py" +web_module = "web_module.js" +web_requires = ("workbench.panels.v2",) +web_provides = () +web_contract_digests = { + "workbench.panels.v2": "fb6417c9bf532c1fdb344767d06065d5d3293da85deb64eff1e8088889a33bcb", +} class _ObserveWriter(Protocol): diff --git a/tests/test_plugin.py b/tests/test_plugin.py index bb22b5f..d3f5bce 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -109,6 +109,7 @@ async def _mount_observe(tmp_path: Path) -> tuple[CompositionRoot, Path]: inject=module.inject, runtime=PluginRuntime( plugin_id="observe", + generation_id=root.generation_id, plugin_dir=plugin_dir, data_dir=tmp_path / "plugin-data", workspace=workspace, @@ -587,7 +588,7 @@ def test_static_manifest_and_module_exports_match() -> None: manifest = load_static_plugin_manifest(plugin_dir) composable = ComposablePlugin.from_module(module) assert manifest.name == composable.name == "observe" - assert manifest.version == composable.version == "1.4.0" + assert manifest.version == composable.version == "1.4.1" assert manifest.api_version == composable.api_version == 3 assert manifest.entrypoint == "plugin.py" assert composable.dashboard_module == "dashboard.py" diff --git a/web_module.css b/web_module.css new file mode 100644 index 0000000..d47f38f --- /dev/null +++ b/web_module.css @@ -0,0 +1 @@ +.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.z-30{z-index:30}.z-40{z-index:40}.m-0{margin:0}.-mb-px{margin-bottom:-1px}.mb-3{margin-bottom:.75rem}.ml-2{margin-left:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-3{margin-top:.75rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-7{height:1.75rem}.h-9{height:2.25rem}.h-\[132px\]{height:132px}.h-\[218px\]{height:218px}.h-\[22px\]{height:22px}.max-h-\[280px\]{max-height:280px}.min-h-0{min-height:0}.min-h-10{min-height:2.5rem}.min-h-11{min-height:2.75rem}.min-h-16{min-height:4rem}.min-h-full{min-height:100%}.w-10{width:2.5rem}.w-2{width:.5rem}.w-48{width:12rem}.w-56{width:14rem}.w-64{width:16rem}.w-\[280px\]{width:280px}.w-\[62px\]{width:62px}.w-full{width:100%}.min-w-0{min-width:0}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-\[340px_1fr\]{grid-template-columns:340px 1fr}.grid-cols-\[9px_1fr_auto\]{grid-template-columns:9px 1fr auto}.grid-cols-\[auto_1fr_auto\]{grid-template-columns:auto 1fr auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-3\.5{gap:.875rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-px{gap:1px}.overflow-auto{overflow:auto}.overflow-hidden,.truncate{overflow:hidden}.truncate{text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded,.rounded-\[4px\]{border-radius:4px}.rounded-full{border-radius:9999px}.rounded-md{border-radius:6px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-accent{--tw-border-opacity:1;border-color:rgb(var(--ak-color-action-primary-rgb)/var(--tw-border-opacity,1))}.border-accent-deep{--tw-border-opacity:1;border-color:rgb(var(--ak-color-action-hover-rgb)/var(--tw-border-opacity,1))}.border-border{border-color:var(--ak-color-border-default)}.border-border-strong{border-color:var(--ak-color-border-strong)}.border-danger\/40{border-color:rgb(var(--ak-color-status-error-rgb)/.4)}.border-success\/35{border-color:rgb(var(--ak-color-status-success-rgb)/.35)}.border-transparent{border-color:transparent}.bg-accent{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-action-primary-rgb)/var(--tw-bg-opacity,1))}.bg-accent-soft{background-color:var(--ak-color-action-soft)}.bg-bg{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-bg-canvas-rgb)/var(--tw-bg-opacity,1))}.bg-black\/55{background-color:rgba(0,0,0,.55)}.bg-border{background-color:var(--ak-color-border-default)}.bg-danger{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-status-error-rgb)/var(--tw-bg-opacity,1))}.bg-danger\/10{background-color:rgb(var(--ak-color-status-error-rgb)/.1)}.bg-subtle{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-text-muted-rgb)/var(--tw-bg-opacity,1))}.bg-success{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-status-success-rgb)/var(--tw-bg-opacity,1))}.bg-success\/10{background-color:rgb(var(--ak-color-status-success-rgb)/.1)}.bg-surface{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-bg-surface-rgb)/var(--tw-bg-opacity,1))}.bg-surface-2{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-bg-surface-low-rgb)/var(--tw-bg-opacity,1))}.bg-surface-3{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-bg-surface-high-rgb)/var(--tw-bg-opacity,1))}.bg-warning{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-status-warning-rgb)/var(--tw-bg-opacity,1))}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-1{padding-bottom:.25rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.font-mono{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,monospace}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[18px\]{font-size:18px}.text-\[19px\]{font-size:19px}.text-\[26px\]{font-size:26px}.text-\[9px\]{font-size:9px}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.text-accent{--tw-text-opacity:1;color:rgb(var(--ak-color-action-primary-rgb)/var(--tw-text-opacity,1))}.text-accent-ink{--tw-text-opacity:1;color:rgb(var(--ak-color-on-action-primary-rgb)/var(--tw-text-opacity,1))}.text-danger{color:rgb(var(--ak-color-status-error-rgb)/var(--tw-text-opacity,1))}.text-danger,.text-fg{--tw-text-opacity:1}.text-fg{color:rgb(var(--ak-color-text-primary-rgb)/var(--tw-text-opacity,1))}.text-muted{--tw-text-opacity:1;color:rgb(var(--ak-color-text-secondary-rgb)/var(--tw-text-opacity,1))}.text-subtle{--tw-text-opacity:1;color:rgb(var(--ak-color-text-muted-rgb)/var(--tw-text-opacity,1))}.text-success{--tw-text-opacity:1;color:rgb(var(--ak-color-status-success-rgb)/var(--tw-text-opacity,1))}.outline-none{outline:2px solid transparent;outline-offset:2px}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-opacity{transition-duration:.15s}.hover\:border-border-strong:hover{border-color:var(--ak-color-border-strong)}.hover\:border-danger\/40:hover{border-color:rgb(var(--ak-color-status-error-rgb)/.4)}.hover\:bg-danger\/10:hover{background-color:rgb(var(--ak-color-status-error-rgb)/.1)}.hover\:bg-surface-2:hover{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-bg-surface-low-rgb)/var(--tw-bg-opacity,1))}.hover\:bg-surface-3:hover{--tw-bg-opacity:1;background-color:rgb(var(--ak-color-bg-surface-high-rgb)/var(--tw-bg-opacity,1))}.hover\:text-danger:hover{--tw-text-opacity:1;color:rgb(var(--ak-color-status-error-rgb)/var(--tw-text-opacity,1))}.hover\:text-fg:hover{--tw-text-opacity:1;color:rgb(var(--ak-color-text-primary-rgb)/var(--tw-text-opacity,1))}.focus\:border-accent-deep:focus{--tw-border-opacity:1;border-color:rgb(var(--ak-color-action-hover-rgb)/var(--tw-border-opacity,1))}.focus-visible\:outline:focus-visible{outline-style:solid}.focus-visible\:outline-2:focus-visible{outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\:outline-accent:focus-visible{outline-color:rgb(var(--ak-color-action-primary-rgb)/1)}.disabled\:opacity-40:disabled{opacity:.4} \ No newline at end of file diff --git a/web_module.js b/web_module.js new file mode 100644 index 0000000..fef6bba --- /dev/null +++ b/web_module.js @@ -0,0 +1 @@ +import{useCallback as Y,useEffect as C,useRef as S,useState as l}from"react";import{createRoot as ie}from"react-dom/client";import{Fragment as ae,jsx as t,jsxs as n}from"react/jsx-runtime";var J=null;async function h(e,s){if(!J)throw new Error("Observe 工作台面板未激活");let a=await J(e,s),i=await a.json();if(!a.ok)throw new Error(String(i.detail??i.message??`HTTP ${a.status}`));return i}var re=[{key:"24h",label:"24 小时"},{key:"7d",label:"7 天"},{key:"30d",label:"30 天"},{key:"all",label:"全部"}],le={log:"主动日志",uncaught:"未捕获异常",asyncio:"asyncio 任务",thread:"子线程"},Z={active:{label:"活跃",tone:"warning"},acknowledged:{label:"已确认",tone:"muted"},ignored:{label:"已忽略",tone:"success"}},ce={danger:"bg-danger",warning:"bg-warning",success:"bg-success",accent:"bg-accent",muted:"bg-subtle"};function ee(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}K`:String(Math.round(e))}function te(e){return typeof e=="number"?`${(e*100).toFixed(1)}%`:"—"}function ne(e){if(e.includes("T"))return`${e.slice(11,13)}:00`;let[,s,a]=e.split("-");return s&&a?`${Number(s)}-${a}`:e}function j(e){if(!e)return"—";let s=new Date(e);return Number.isNaN(s.getTime())?e||"—":`${s.getMonth()+1}-${String(s.getDate()).padStart(2,"0")} ${String(s.getHours()).padStart(2,"0")}:${String(s.getMinutes()).padStart(2,"0")}`}function de(e){if(e.length<2)return null;let s=e[e.length-1],a=e[e.length-2];return a?(s-a)/a*100:null}function ue(e){if(!Number.isFinite(e)||e<0)return"刚刚";let s=Math.floor(e/1e3);if(s<3)return"刚刚";if(s<60)return`${s}s 前`;let a=Math.floor(s/60);return a<60?`${a}m 前`:`${Math.floor(a/60)}h 前`}function se(e,s){return s||e>=20?"danger":e>=5?"warning":"muted"}function U({title:e,children:s,bodyClass:a,style:i}){return n("div",{className:"flex flex-col overflow-hidden border border-border bg-surface",style:i,children:[t("div",{className:"flex items-center justify-between border-b border-border px-4 py-2.5",children:t("h3",{className:"text-[12px] font-medium text-muted",children:e})}),t("div",{className:a??"p-4",children:s})]})}function be({portalRef:e,fallbackRef:s,range:a,onClose:i,ui:b}){let f=S(null),c=S(null),[g,v]=l(null),[k,L]=l("type"),[R,p]=l(""),[T,u]=l([]),[x,q]=l(null),[A,F]=l(null),[H,$]=l(null),[W,y]=l(null),[B,P]=l(!1),[G,D]=l("trace"),[I,K]=l(0),O=S(null),_=S(null),z=Y(async()=>{O.current?.abort();let r=new AbortController;O.current=r,$(null);try{let[o,d]=await Promise.all([h(`/api/dashboard/observe/global_errors/overview?range=${a}`,{signal:r.signal}),h(`/api/dashboard/observe/global_errors?range=${a}&facet=${k}&q=${encodeURIComponent(R)}`,{signal:r.signal})]);if(r.signal.aborted)return;v(o),u(d.sections??[]);let m=(d.sections??[]).flatMap(E=>E.items);q(E=>E&&m.some(oe=>oe.fingerprint===E)?E:m[0]?.fingerprint??null)}catch(o){r.signal.aborted||$(o instanceof Error?o.message:"错误列表读取失败")}finally{O.current===r&&(O.current=null)}},[a,k,R]);C(()=>(z(),()=>O.current?.abort()),[z]),C(()=>{if(!x){F(null),y(null);return}let r=new AbortController;return y(null),h(`/api/dashboard/observe/global_errors/${x}?range=${a}`,{signal:r.signal}).then(o=>{r.signal.aborted||(F(o),K(0),D("trace"))},o=>{r.signal.aborted||y(o instanceof Error?o.message:"错误详情读取失败")}),()=>r.abort()},[x,a]);let w=Y(()=>{i()},[i]);C(()=>(c.current?.focus(),()=>{_.current?.abort(),(e.current??s.current)?.focus()}),[s,e]),C(()=>{let r=o=>{if(o.key==="Escape"&&w(),o.key!=="Tab")return;let d=Array.from(f.current?.querySelectorAll('button:not([disabled]), input:not([disabled]), select:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')??[]);if(d.length===0)return;let m=d[0],E=d[d.length-1];o.shiftKey&&document.activeElement===m?(o.preventDefault(),E.focus()):!o.shiftKey&&document.activeElement===E&&(o.preventDefault(),m.focus())};return window.addEventListener("keydown",r),()=>window.removeEventListener("keydown",r)},[w]);let M=async r=>{if(!A||_.current)return;let o=A.fingerprint,d=new AbortController;_.current=d,P(!0),y(null);try{if(await h(`/api/dashboard/observe/global_errors/${o}/status?value=${r}`,{method:"POST",signal:d.signal}),d.signal.aborted)return;F(m=>m?.fingerprint===o?{...m,status:r}:m)}catch(m){d.signal.aborted||y(m instanceof Error?m.message:"错误状态更新失败")}finally{_.current===d&&(_.current=null,d.signal.aborted||P(!1))}},N=r=>{window.dispatchEvent(new CustomEvent("akashic:goto-session",{detail:r})),w()};return n(ae,{children:[t("div",{"aria-hidden":"true",className:"fixed inset-0 z-30 bg-black/55",onClick:w}),n("div",{ref:f,role:"dialog","aria-modal":"true","aria-labelledby":"observe-error-dialog-title",className:"fixed z-40 flex flex-col overflow-hidden rounded-md border border-border-strong bg-surface",style:{width:"min(1180px, 94vw)",height:"min(84vh, 760px)",left:"50%",top:"50%",marginLeft:"calc(min(1180px, 94vw) / -2)",marginTop:"calc(min(84vh, 760px) / -2)"},children:[n("div",{className:"flex flex-shrink-0 items-center gap-4 border-b border-border px-5 py-4",children:[t("button",{ref:c,type:"button",onClick:w,className:"grid h-10 w-10 place-items-center rounded-md border border-border-strong bg-surface-2 text-[18px] text-muted transition-colors hover:text-fg","aria-label":"关闭错误分析",title:"返回 (Esc)",children:"‹"}),t("span",{className:"font-mono text-[26px] font-semibold tabular-nums text-danger",children:g?.total??"—"}),n("div",{className:"min-w-0",children:[n("div",{id:"observe-error-dialog-title",className:"text-sm font-semibold",children:["错误 · ",re.find(r=>r.key===a)?.label??a]}),n("div",{className:"mt-0.5 flex items-center gap-3 text-[11px] text-muted",children:[n("span",{children:[g?.types??0," 个类型"]}),(g?.new_types??0)>0&&n("span",{children:[g?.new_types," 个新类型"]}),(g?.spiking_types??0)>0&&n("span",{className:"font-semibold text-danger",children:[g?.spiking_types," 个正在爆发"]})]})]})]}),n("div",{className:"flex flex-shrink-0 items-center gap-3 border-b border-border px-4 py-2.5",children:[t("div",{className:"flex gap-1 rounded-md border border-border bg-bg p-0.5",children:[{k:"type",l:"按类型"},{k:"source",l:"按来源"},{k:"channel",l:"按通道"}].map(r=>t("button",{type:"button",onClick:()=>L(r.k),className:`rounded-[4px] px-2.5 py-1 text-[11px] transition-colors ${k===r.k?"bg-surface-3 text-fg":"text-muted hover:text-fg"}`,children:r.l},r.k))}),t("input",{value:R,onChange:r=>p(r.target.value),"aria-label":"搜索错误",placeholder:"按消息 / 模块过滤…",className:"w-[280px] rounded-md border border-border bg-bg px-3 py-1.5 text-[11.5px] text-fg outline-none focus:border-accent-deep"})]}),n("div",{className:"grid min-h-0 flex-1 grid-cols-[340px_1fr]",children:[n("div",{className:"overflow-auto border-r border-border p-1.5",children:[H&&t("div",{className:"p-4 text-[12px] text-danger",role:"alert",children:H}),T.map(r=>n("div",{children:[r.label&&n("div",{className:"flex items-center justify-between px-2.5 pb-1 pt-3 text-[11px] font-medium text-muted",children:[t("span",{children:r.label}),n("span",{children:[r.count," 次"]})]}),r.items.map(o=>t(me,{g:o,active:o.fingerprint===x,onClick:()=>q(o.fingerprint),ui:b},o.fingerprint))]},r.key)),!H&&T.length===0&&t("div",{className:"p-6 text-[12.5px] text-muted",children:"所选区间内没有错误。"})]}),W?t("div",{className:"grid place-items-center p-6 text-[13px] text-danger",role:"alert",children:W}):A?t(pe,{detail:A,tab:G,setTab:D,variant:I,setVariant:K,savingStatus:B,onStatus:M,onGoto:N,ui:b}):t("div",{className:"grid place-items-center text-[13px] text-muted",children:"选择左侧一个错误查看现场"})]})]})]})}function me({g:e,active:s,onClick:a,ui:i}){let b=se(e.count,e.is_spiking),f=e.spark??[],c=i.Sparkline;return n("button",{type:"button",onClick:a,className:`grid w-full grid-cols-[9px_1fr_auto] items-center gap-2.5 border-b border-border px-3 py-2.5 text-left transition-colors duration-150 ${s?"bg-accent-soft":"hover:bg-surface-2"}`,children:[t("span",{className:"relative flex h-2 w-2","aria-hidden":"true",children:t("span",{className:`relative inline-flex h-2 w-2 rounded-full ${ce[b]}`})}),n("div",{className:"min-w-0",children:[n("div",{className:"flex items-center gap-1.5 font-mono text-[12.5px]",children:[t("span",{className:"truncate",children:e.error_type}),e.is_new&&t("span",{className:"text-[9px] font-semibold text-accent",children:"新"}),e.is_spiking&&t("span",{className:"text-[9px] font-semibold text-danger",children:"爆发"})]}),t("div",{className:"mt-0.5 truncate font-mono text-[10px] text-subtle",children:e.logger_name}),n("div",{className:"mt-1 flex gap-2.5 text-[10px] text-muted",children:[n("span",{children:[t("b",{className:"font-semibold text-fg",children:e.count})," 次"]}),n("span",{children:[t("b",{className:"font-semibold text-fg",children:e.sessions})," session"]})]})]}),n("div",{className:"flex flex-col items-end gap-1.5",children:[t("div",{className:"h-[22px] w-[62px]",children:f.length>1&&t(c,{data:f,tone:b,height:22})}),t("span",{className:"font-mono text-[10px] text-subtle",children:j(e.last_ts)})]})]})}function pe({detail:e,tab:s,setTab:a,variant:i,setVariant:b,savingStatus:f,onStatus:c,onGoto:g,ui:v}){let k=Z[e.status]??Z.active,L=se(e.count,!1),R=e.variants[i]??e.variants[0],{Chip:p,TrendChart:T}=v;return n("div",{className:"flex min-h-0 flex-col",children:[n("div",{className:"border-b border-border px-5 py-4",children:[t("div",{className:"font-mono text-[19px] font-semibold",children:e.error_type}),t("div",{className:"mt-1.5 font-mono text-[12px] leading-relaxed text-danger",children:e.message}),n("div",{className:"mt-3 flex flex-wrap gap-1.5",children:[t(p,{children:e.logger_name}),n(p,{children:["来源 · ",le[e.source]??e.source]}),t(p,{children:e.channel}),t(p,{tone:"danger",children:e.level}),t(p,{tone:k.tone,children:k.label})]})]}),n("div",{className:"grid grid-cols-4 gap-px border-b border-border bg-border",children:[t(Q,{label:"累计次数",value:String(e.count)}),t(Q,{label:"独立 session",value:String(e.sessions)}),t(Q,{label:"首次",value:j(e.first_ts),small:!0}),t(Q,{label:"最近",value:j(e.last_ts),small:!0})]}),n("div",{className:"flex gap-1 border-b border-border px-5 pt-3",children:[t(X,{active:s==="trend",onClick:()=>a("trend"),children:"趋势"}),n(X,{active:s==="trace",onClick:()=>a("trace"),children:["Traceback",e.variants.length>1?` · ${e.variants.length} 变体`:""]}),n(X,{active:s==="occ",onClick:()=>a("occ"),children:["现场 · ",e.occurrences.length]})]}),n("div",{className:"min-h-0 flex-1 overflow-auto px-5 py-4",children:[s==="trend"&&t(T,{data:e.trend.map(u=>({label:ne(u.bucket),value:u.count})),kind:"bar",tone:L,valueFmt:u=>String(u),empty:"区间内无发作"}),s==="trace"&&n("div",{children:[e.variants.length>1&&t("div",{className:"mb-3 flex gap-2",children:e.variants.map((u,x)=>n("button",{type:"button",onClick:()=>b(x),className:`rounded border px-2.5 py-1.5 text-left text-[10.5px] ${x===i?"border-accent-deep bg-accent-soft text-fg":"border-border bg-bg text-muted"}`,children:[t("b",{className:"text-fg",children:u.count})," 次 · 变体 ",x+1]},u.fingerprint))}),t("pre",{className:"m-0 max-h-[280px] overflow-auto rounded border border-border bg-bg p-4 font-mono text-[11px] leading-relaxed text-muted",children:R?.traceback_text||e.traceback_text})]}),s==="occ"&&n("div",{className:"flex flex-col gap-2",children:[e.occurrences.length===0&&t("div",{className:"text-[12px] text-muted",children:"无可关联的 session 现场。"}),e.occurrences.map(u=>n("div",{className:"grid grid-cols-[auto_1fr_auto] items-center gap-3.5 border-b border-border bg-bg px-3.5 py-2.5",children:[t("span",{className:"font-mono text-[11px] text-accent",children:j(u.ts)}),n("div",{className:"min-w-0",children:[t("div",{className:"truncate text-[12px]",children:u.user_preview||"(无用户消息)"}),n("div",{className:"mt-0.5 font-mono text-[10px] text-subtle",children:["session ",u.session_key]})]}),t("button",{type:"button",onClick:()=>g(u.session_key),className:"whitespace-nowrap rounded border border-accent-deep bg-accent-soft px-2.5 py-1.5 text-[10.5px] text-accent-ink",children:"查看对话"})]},u.session_key))]})]}),n("div",{className:"flex flex-shrink-0 gap-2 border-t border-border px-5 py-3",children:[t("button",{type:"button",onClick:()=>e.occurrences[0]&&g(e.occurrences[0].session_key),disabled:e.occurrences.length===0,className:"rounded border border-accent-deep bg-accent-soft px-3 py-2 text-[11px] text-accent-ink transition-colors disabled:opacity-40",children:"查看最近对话"}),t("button",{type:"button",onClick:()=>void navigator.clipboard?.writeText(e.traceback_text),className:"rounded border border-border-strong bg-surface-2 px-3 py-2 text-[11px] text-muted transition-colors hover:text-fg",children:"复制 Traceback"}),t("div",{className:"flex-1"}),t("button",{type:"button",disabled:f,onClick:()=>c("acknowledged"),className:"rounded border border-border-strong bg-surface-2 px-3 py-2 text-[11px] text-muted transition-colors hover:text-fg disabled:opacity-40",children:"标记已确认"}),t("button",{type:"button",disabled:f,onClick:()=>c("ignored"),className:"rounded border border-border-strong bg-surface-2 px-3 py-2 text-[11px] text-muted transition-colors hover:border-danger/40 hover:text-danger disabled:opacity-40",children:"忽略此类型"})]})]})}function Q({label:e,value:s,small:a}){return n("div",{className:"bg-surface px-4 py-3",children:[t("div",{className:"text-[10px] text-subtle",children:e}),t("div",{className:`mt-1.5 font-mono font-semibold tabular-nums ${a?"text-[12.5px]":"text-[18px]"}`,children:s})]})}function X({active:e,onClick:s,children:a}){return t("button",{type:"button",onClick:s,className:`-mb-px border-b-2 px-3 py-2 text-[11.5px] transition-colors ${e?"border-accent text-fg":"border-transparent text-muted hover:text-fg"}`,children:a})}function V({className:e}){return t("div",{className:`relative overflow-hidden rounded border border-border bg-surface-2 ${e}`})}function ge(){return n("div",{className:"flex flex-col gap-4 p-6",children:[n("div",{className:"flex items-end justify-between",children:[n("div",{className:"flex flex-col gap-2",children:[t(V,{className:"h-7 w-48"}),t(V,{className:"h-3 w-64 rounded"})]}),t(V,{className:"h-9 w-56"})]}),t("div",{className:"grid grid-cols-4 gap-4",children:[0,1,2,3].map(e=>t(V,{className:"h-[132px]"},e))}),t("div",{className:"grid grid-cols-2 gap-4",children:[0,1,2,3].map(e=>t(V,{className:"h-[218px]"},e))})]})}function ve({dispatch:e}){let{Grid:s,MetricTile:a,TrendChart:i}=e.ui,[b,f]=l("24h"),[c,g]=l(null),[v,k]=l([]),[L,R]=l(null),[p,T]=l(!1),[u,x]=l(0),[q,A]=l(()=>Date.now()),[F,H]=l(!1),[$,W]=l(null),y=S(null),B=S(null),P=S(null),G=S(null),D=Y(async()=>{G.current?.abort();let r=new AbortController;G.current=r,H(!0),W(null);try{let[o,d,m]=await Promise.all([h(`/api/dashboard/observe/overview?range=${b}`,{signal:r.signal}),h(`/api/dashboard/observe/timeseries?range=${b}`,{signal:r.signal}),h(`/api/dashboard/observe/global_errors/overview?range=${b}`,{signal:r.signal})]);if(r.signal.aborted)return;g(o),k(d.points??[]),R(m),x(Date.now())}catch(o){r.signal.aborted||W(o instanceof Error?o.message:"监测数据读取失败")}finally{G.current===r&&(G.current=null,r.signal.aborted||H(!1))}},[b]);if(C(()=>{D();let r=window.setInterval(()=>void D(),15e3);return()=>{window.clearInterval(r),G.current?.abort()}},[D]),C(()=>{if(!F||window.matchMedia("(prefers-reduced-motion: reduce)").matches)return;let r=B.current?.animate([{transform:"rotate(0deg)"},{transform:"rotate(360deg)"}],{duration:1e3,iterations:1/0});return()=>r?.cancel()},[F]),C(()=>{let r=window.setInterval(()=>A(Date.now()),1e3);return()=>window.clearInterval(r)},[]),C(()=>{P.current&&(P.current.inert=p)},[p]),!c)return $?t("div",{className:"grid min-h-full place-items-center p-6 text-danger",role:"alert",children:$}):t(ge,{});let I=v.map(r=>r.turns),K=v.map(r=>r.errors),O=v.map(r=>r.input_tokens),_=v.map(r=>(r.passive_cache_hit_rate??0)*100),z=v.map(r=>(r.proactive_cache_hit_rate??0)*100),w=v.map(r=>r.avg_iteration??0),M=r=>v.map((o,d)=>({label:ne(o.bucket),value:r[d]})),N=L?.total??c.errors;return n(ae,{children:[n("div",{ref:P,"aria-hidden":p||void 0,className:"flex flex-col gap-5 p-6 transition-opacity duration-150",style:p?{opacity:.35,pointerEvents:"none"}:void 0,children:[n("div",{className:"flex items-end justify-between",children:[n("div",{children:[n("div",{className:"flex items-center gap-2.5",children:[t("span",{className:"detail-title",children:"Observe · 监测"}),t("span",{className:"text-[11px] font-medium text-success",children:"实时更新"})]}),n("div",{className:"detail-subtext",children:["Agent 主循环遥测 · Token / 迭代 / 错误",n("span",{className:"ml-2 font-mono text-[11px] text-subtle",children:["更新于 ",ue(q-u)]})]})]}),n("div",{className:"flex items-center gap-2",children:[t("button",{ref:B,type:"button",onClick:()=>void D(),className:"grid h-10 w-10 place-items-center rounded-md border border-border bg-surface-2 text-muted transition-colors hover:border-border-strong hover:text-fg","aria-label":"刷新监测数据",title:"刷新",children:"↻"}),t("div",{className:"flex gap-1 rounded-md border border-border bg-surface-2 p-1",children:re.map(r=>t("button",{type:"button",onClick:()=>f(r.key),className:`min-h-10 rounded-[4px] px-2.5 py-1 text-[11px] transition-colors ${b===r.key?"bg-accent text-accent-ink":"text-muted hover:bg-surface-3 hover:text-fg"}`,"aria-pressed":b===r.key,children:r.label},r.key))})]})]}),$&&t("div",{className:"border border-danger/40 bg-danger/10 px-4 py-3 text-[12px] text-danger",role:"alert",children:$}),n("section",{className:`flex min-h-16 items-center justify-between gap-4 border px-4 py-3 ${N>0?"border-danger/40 bg-danger/10":"border-success/35 bg-success/10"}`,"aria-label":"运行状态",children:[n("div",{className:"min-w-0",children:[t("div",{className:`text-[13px] font-semibold ${N>0?"text-danger":"text-success"}`,children:N>0?`${N} 条错误需要查看`:"当前区间没有采集到错误"}),t("p",{className:"mt-1 text-[11.5px] text-muted",children:N>0?`${L?.types??0} 个错误类型,先查看爆发和新出现的类型。`:"主循环遥测持续更新,缓存与迭代指标见下方。"})]}),N>0&&t("button",{type:"button",ref:y,onClick:()=>T(!0),className:"min-h-10 flex-shrink-0 rounded-md border border-danger/40 bg-surface px-3 text-[12px] font-semibold text-danger hover:bg-danger/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",children:"查看错误分析"})]}),n(s,{columns:3,children:[t("div",{children:t(a,{label:"对话轮数",value:ee(c.turns),delta:de(I),sub:c.last_ts?`最近 ${j(c.last_ts)}`:"无记录",tone:"accent",spark:I})}),t("div",{children:t(a,{label:"被动 KV 命中率",value:te(c.passive_cache_hit_rate),sub:`主动 ${te(c.proactive_cache_hit_rate)}`,tone:"success",spark:_})}),t("div",{children:t(a,{label:"平均迭代",value:c.avg_iteration!=null?c.avg_iteration.toFixed(1):"—",unit:`峰 ${c.max_iteration}`,sub:"每轮 LLM 调用次数",tone:"warning",spark:w})})]}),n(s,{columns:2,children:[t(U,{title:"输入 Token 趋势",children:t(i,{data:M(O),kind:"area",tone:"accent",valueFmt:ee})}),t(U,{title:"平均迭代趋势",children:t(i,{data:M(w),kind:"area",tone:"warning",valueFmt:r=>r.toFixed(1)})})]}),n("details",{className:"border-t border-border pt-1",children:[t("summary",{className:"min-h-11 cursor-pointer py-3 text-[12px] font-semibold text-fg focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent",children:"查看缓存命中与错误趋势"}),n(s,{columns:2,children:[t(U,{title:"全局被动链路命中率趋势",children:t(i,{data:M(_),kind:"area",tone:"success",valueFmt:r=>`${r.toFixed(0)}%`})}),t(U,{title:"全局主动链路命中率趋势",children:t(i,{data:M(z),kind:"area",tone:"accent",valueFmt:r=>`${r.toFixed(0)}%`})}),t(U,{title:"错误趋势",children:t(i,{data:M(K),kind:"bar",tone:"danger",valueFmt:r=>String(r),empty:"所选区间内没有错误"})})]})]})]}),p&&t(be,{portalRef:y,fallbackRef:B,range:b,onClose:()=>T(!1),ui:e.ui})]})}var fe={id:"observe",label:"运行监测",viewLabel:"运行监测",order:60,layout:"workbench",pageSize:30,rowKey:"id",countTitle(e){return`${e} 轮遥测`},columns:[{key:"session_key",label:"会话",width:120,cellClass:"mono cell-session",rawTitle:!0},{key:"ts",label:"时间",width:96,fmt:"mono-time",cellClass:"mono cell-time",rawTitle:!0},{key:"error",label:"错误",flex:!0,cellClass:"content-preview"}],async getCount({signal:e}){try{return(await h("/api/dashboard/observe/overview?range=all",{signal:e})).turns||0}catch(s){if(e.aborted)throw s;return null}},async fetchPage({page:e,pageSize:s,signal:a}){let i=await h(`/api/dashboard/observe/errors?range=all&page=${e}&page_size=${s}`,{signal:a});return{items:i.items||[],total:i.total||0}},renderMain(e,s){let a=ie(e);return a.render(t(ve,{dispatch:s})),()=>a.unmount()}};function ke(e){J=e.http.request;let s=e.ui.inject("workbench.panels.v2",a=>a.register(fe));return()=>{s(),J=null}}export{ke as activate};