From dec96f1bc393a0089f6946a5c63094ef206f7b74 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 07:41:41 +0800 Subject: [PATCH 1/9] feat: register feedback workbench panel --- dashboard_panel.css | 140 ------------------------------ dashboard_panel.js | 169 ------------------------------------- dashboard_panel.tsx | 197 ------------------------------------------- plugin.py | 6 ++ tests/test_plugin.py | 13 ++- web_module.css | 37 ++++++++ web_module.js | 189 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 243 insertions(+), 508 deletions(-) delete mode 100644 dashboard_panel.css delete mode 100644 dashboard_panel.js delete mode 100644 dashboard_panel.tsx create mode 100644 web_module.css create mode 100644 web_module.js diff --git a/dashboard_panel.css b/dashboard_panel.css deleted file mode 100644 index 28c0153..0000000 --- a/dashboard_panel.css +++ /dev/null @@ -1,140 +0,0 @@ -.feedback-detail { - display: grid; - gap: 20px; - padding: 24px; - color: var(--ak-color-text-primary); -} - -.feedback-detail__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.feedback-detail__header p { - margin: 0 0 4px; - color: var(--ak-color-action-primary); - font-size: 11px; - font-weight: 700; - letter-spacing: 0.08em; -} - -.feedback-detail__header h2 { - margin: 0; - font-size: 20px; - letter-spacing: -0.02em; -} - -.feedback-detail__header span { - display: block; - margin-block-start: 5px; - color: var(--ak-color-text-muted); - font-size: 12px; -} - -.feedback-summary { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - border-block: 1px solid var(--ak-color-border-default); -} - -.feedback-summary > div { - display: grid; - gap: 5px; - padding: 14px; -} - -.feedback-summary > div + div { border-inline-start: 1px solid var(--ak-color-border-default); } -.feedback-summary span { color: var(--ak-color-text-muted); font-size: 11px; } -.feedback-summary strong { font: 650 18px var(--mono); } - -.feedback-timeline { - display: grid; - gap: 0; -} - -.feedback-timeline > h3 { - margin: 0 0 10px; - font-size: 15px; -} - -.feedback-step { - position: relative; - display: grid; - grid-template-columns: 30px minmax(0, 1fr); - gap: 12px; - padding-block: 10px; -} - -.feedback-step:not(:last-child)::after { - position: absolute; - top: 40px; - bottom: -10px; - left: 14px; - width: 1px; - content: ""; - background: var(--ak-color-border-strong); -} - -.feedback-step__index { - display: grid; - width: 30px; - height: 30px; - place-items: center; - border: 1px solid var(--ak-color-border-strong); - border-radius: 50%; - color: var(--ak-color-text-muted); - background: var(--ak-color-bg-canvas); - font: 650 11px var(--mono); -} - -.feedback-step.is-emphasis .feedback-step__index { - border-color: var(--ak-color-action-primary); - color: var(--ak-color-on-action-primary); - background: var(--ak-color-action-soft); -} - -.feedback-step h4 { margin: 3px 0 6px; font-size: 13px; } -.feedback-step p { - margin: 0; - color: var(--ak-color-text-secondary); - font-size: 13px; - line-height: 1.65; - white-space: pre-wrap; -} - -.feedback-step.is-emphasis p { - padding: 12px; - border-inline-start: 3px solid var(--ak-color-action-primary); - background: var(--ak-color-action-soft); -} - -.feedback-technical { - border-block-start: 1px solid var(--ak-color-border-default); -} - -.feedback-technical summary { - padding: 14px 2px; - font-size: 13px; - font-weight: 650; - cursor: pointer; -} - -.feedback-technical summary:focus-visible { - outline: 2px solid var(--ak-color-action-primary); - outline-offset: 2px; -} - -.feedback-technical dl { display: grid; gap: 8px; margin: 0 0 14px; } -.feedback-technical dl > div { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 12px; } -.feedback-technical dt { color: var(--ak-color-text-muted); font-size: 12px; } -.feedback-technical dd { overflow-wrap: anywhere; margin: 0; font: 12px var(--mono); } - -@media (max-width: 720px) { - .feedback-detail { padding: 18px; } - .feedback-detail__header { flex-direction: column; } - .feedback-summary { grid-template-columns: 1fr; } - .feedback-summary > div + div { border-block-start: 1px solid var(--ak-color-border-default); border-inline-start: 0; } - .feedback-technical dl > div { grid-template-columns: 1fr; gap: 3px; } -} diff --git a/dashboard_panel.js b/dashboard_panel.js deleted file mode 100644 index 6386f21..0000000 --- a/dashboard_panel.js +++ /dev/null @@ -1,169 +0,0 @@ -// ../proactive_feedback/dashboard_panel.tsx -import { Chip, api } from "@akashic/dashboard-ui"; -import { jsx, jsxs } from "react/jsx-runtime"; -function _score(value) { - return typeof value === "number" ? value.toFixed(3) : "-"; -} -function _shortTs(value) { - const text = String(value || ""); - if (!text) return "-"; - const d = new Date(text); - if (Number.isNaN(d.getTime())) return text; - return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; -} -function _lag(value) { - if (typeof value !== "number") return "-"; - if (value < 60) return `${value}s`; - if (value < 3600) return `${Math.round(value / 60)}m`; - return `${(value / 3600).toFixed(1)}h`; -} -function _tone(type) { - if (type === "explicit_quote") return "accent"; - if (type === "topic_follow") return "success"; - if (type === "unscored") return "warning"; - if (type === "no_topic_follow") return "muted"; - return "muted"; -} -function _feedbackTypeLabel(value) { - const type = String(value || ""); - if (type === "explicit_quote") return "\u660E\u786E\u5F15\u7528"; - if (type === "topic_follow") return "\u8BDD\u9898\u5EF6\u7EED"; - if (type === "no_topic_follow") return "\u672A\u5EF6\u7EED\u8BDD\u9898"; - if (type === "unscored") return "\u5F85\u8BC4\u5206"; - return type || "-"; -} -function _confidenceLabel(value) { - const confidence = String(value || ""); - if (confidence === "gold") return "\u91D1\u6807"; - if (confidence === "high") return "\u9AD8"; - if (confidence === "medium") return "\u4E2D"; - if (confidence === "low") return "\u4F4E"; - return confidence || "-"; -} -function _escape(value) { - return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); -} -function _cellText(value) { - const text = String(value || "").trim(); - return _escape(text || "-"); -} -function _typeCell(value) { - const type = String(value || ""); - const tone = type === "explicit_quote" ? "accent" : type === "topic_follow" ? "success" : type === "unscored" ? "warning" : "muted"; - return `${_escape(_feedbackTypeLabel(type))}`; -} -function _confidenceTone(value) { - const confidence = String(value || ""); - if (confidence === "gold" || confidence === "high") return "success"; - if (confidence === "medium") return "warning"; - return "muted"; -} -function _confidenceCell(value) { - const confidence = String(value || "-"); - return `${_escape(_confidenceLabel(confidence))}`; -} -function FeedbackDetail(props) { - const item = props.item; - if (!item) { - return /* @__PURE__ */ jsxs("div", { className: "detail-empty", children: [ - /* @__PURE__ */ jsx("div", { className: "detail-empty-title", children: "\u53CD\u9988\u8BE6\u60C5" }), - /* @__PURE__ */ jsx("div", { className: "detail-empty-text", children: "\u70B9\u5F00\u4E00\u6761\u8BB0\u5F55\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u7528\u6237\u56DE\u590D\u3001\u547D\u4E2D\u7684 proactive \u548C\u52A9\u624B\u540E\u7EED\u56DE\u590D\u3002" }) - ] }); - } - const type = String(item.feedback_type || ""); - return /* @__PURE__ */ jsxs("main", { className: "feedback-detail", "aria-labelledby": "feedback-detail-title", children: [ - /* @__PURE__ */ jsxs("header", { className: "feedback-detail__header", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("p", { children: "\u4E3B\u52A8\u6D88\u606F\u6548\u679C" }), - /* @__PURE__ */ jsx("h2", { id: "feedback-detail-title", children: "\u7528\u6237\u5982\u4F55\u56DE\u5E94\u8FD9\u6B21\u4E3B\u52A8\u89E6\u8FBE" }), - /* @__PURE__ */ jsx("span", { children: String(item.session_key || "\u672A\u5173\u8054\u4F1A\u8BDD") }) - ] }), - /* @__PURE__ */ jsx(Chip, { tone: _tone(type), children: _feedbackTypeLabel(type) }) - ] }), - /* @__PURE__ */ jsxs("section", { className: "feedback-summary", "aria-label": "\u53CD\u9988\u5224\u65AD\u6458\u8981", children: [ - /* @__PURE__ */ jsx(SummaryMetric, { label: "\u7F6E\u4FE1\u5EA6", value: _confidenceLabel(item.confidence) }), - /* @__PURE__ */ jsx(SummaryMetric, { label: "\u54CD\u5E94\u5EF6\u8FDF", value: _lag(item.lag_seconds) }), - /* @__PURE__ */ jsx(SummaryMetric, { label: "PUA \u5206\u6570", value: _score(item.pua_score) }) - ] }), - /* @__PURE__ */ jsxs("section", { className: "feedback-timeline", "aria-labelledby": "feedback-timeline-title", children: [ - /* @__PURE__ */ jsx("h3", { id: "feedback-timeline-title", children: "\u5BF9\u8BDD\u94FE\u8DEF" }), - /* @__PURE__ */ jsx(TimelineStep, { index: "1", title: "\u4E3B\u52A8\u6D88\u606F", text: String(item.proactive_preview || item.quoted_preview || "") }), - /* @__PURE__ */ jsx(TimelineStep, { index: "2", title: "\u7528\u6237\u53CD\u9988", text: String(item.user_reply_preview || item.user_preview || ""), emphasis: true }), - /* @__PURE__ */ jsx(TimelineStep, { index: "3", title: "\u52A9\u624B\u540E\u7EED", text: String(item.assistant_preview || "") }) - ] }), - /* @__PURE__ */ jsxs("details", { className: "feedback-technical", children: [ - /* @__PURE__ */ jsx("summary", { children: "\u67E5\u770B\u5339\u914D\u4F9D\u636E\u4E0E\u6D88\u606F\u6807\u8BC6" }), - /* @__PURE__ */ jsxs("dl", { children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("dt", { children: "\u5339\u914D\u65B9\u5F0F" }), - /* @__PURE__ */ jsx("dd", { children: String(item.matched_by || "-") }) - ] }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("dt", { children: "\u7528\u6237\u6D88\u606F ID" }), - /* @__PURE__ */ jsx("dd", { children: String(item.user_message_id || "-") }) - ] }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("dt", { children: "\u4E3B\u52A8\u6D88\u606F ID" }), - /* @__PURE__ */ jsx("dd", { children: String(item.proactive_message_id || "-") }) - ] }) - ] }) - ] }) - ] }); -} -function SummaryMetric(props) { - return /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("span", { children: props.label }), - /* @__PURE__ */ jsx("strong", { children: props.value }) - ] }); -} -function TimelineStep(props) { - return /* @__PURE__ */ jsxs("div", { className: `feedback-step${props.emphasis ? " is-emphasis" : ""}`, children: [ - /* @__PURE__ */ jsx("span", { className: "feedback-step__index", children: props.index }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("h4", { children: props.title }), - /* @__PURE__ */ jsx("p", { children: props.text || "\u6CA1\u6709\u53EF\u5C55\u793A\u7684\u5185\u5BB9" }) - ] }) - ] }); -} -window.AkashicDashboard.registerPlugin({ - id: "proactive_feedback", - label: "\u4E3B\u52A8\u53CD\u9988", - viewLabel: "\u4E3B\u52A8\u53CD\u9988", - pageSize: 50, - rowKey: "id", - countTitle(total) { - return `\u5171 ${total} \u6761\u53CD\u9988`; - }, - columns: [ - { key: "created_at", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, - { key: "feedback_type", label: "\u7C7B\u578B", width: 126, renderCell: _typeCell }, - { key: "confidence", label: "\u7F6E\u4FE1\u5EA6", width: 84, renderCell: _confidenceCell }, - { key: "lag_seconds", label: "\u5EF6\u8FDF", width: 68, fmt: "lag", cellClass: "mono cell-metric", align: "right" }, - { key: "user_reply_preview", label: "\u7528\u6237\u56DE\u590D", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, - { key: "proactive_preview", label: "\u547D\u4E2D\u5185\u5BB9", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true } - ], - async getCount() { - try { - const overview = await api("/api/dashboard/proactive-feedback/overview"); - return overview.total || 0; - } catch { - return null; - } - }, - async fetchPage({ page, pageSize }) { - const params = new URLSearchParams(); - params.set("page", String(page)); - params.set("page_size", String(pageSize)); - const data = await api(`/api/dashboard/proactive-feedback/events?${params.toString()}`); - return { items: data.items || [], total: data.total || 0 }; - }, - async fetchDetail(item) { - return api(`/api/dashboard/proactive-feedback/events/${item.id}`); - }, - Detail: FeedbackDetail, - formatters: { - score: (value) => _score(value), - lag: (value) => _lag(value), - "mono-time": (value) => _shortTs(value) - } -}); diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx deleted file mode 100644 index a1ac814..0000000 --- a/dashboard_panel.tsx +++ /dev/null @@ -1,197 +0,0 @@ -/// -import { type ReactElement } from "react"; -import { Chip, api } from "@akashic/dashboard-ui"; - -interface Overview { - total: number; -} - -interface FetchPage { - items: Record[]; - total: number; -} - -function _score(value: unknown): string { - return typeof value === "number" ? value.toFixed(3) : "-"; -} - -function _shortTs(value: unknown): string { - const text = String(value || ""); - if (!text) return "-"; - const d = new Date(text); - if (Number.isNaN(d.getTime())) return text; - return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; -} - -function _lag(value: unknown): string { - if (typeof value !== "number") return "-"; - if (value < 60) return `${value}s`; - if (value < 3600) return `${Math.round(value / 60)}m`; - return `${(value / 3600).toFixed(1)}h`; -} - -function _tone(type: string): "success" | "warning" | "danger" | "accent" | "muted" { - if (type === "explicit_quote") return "accent"; - if (type === "topic_follow") return "success"; - if (type === "unscored") return "warning"; - if (type === "no_topic_follow") return "muted"; - return "muted"; -} - -function _feedbackTypeLabel(value: unknown): string { - const type = String(value || ""); - if (type === "explicit_quote") return "明确引用"; - if (type === "topic_follow") return "话题延续"; - if (type === "no_topic_follow") return "未延续话题"; - if (type === "unscored") return "待评分"; - return type || "-"; -} - -function _confidenceLabel(value: unknown): string { - const confidence = String(value || ""); - if (confidence === "gold") return "金标"; - if (confidence === "high") return "高"; - if (confidence === "medium") return "中"; - if (confidence === "low") return "低"; - return confidence || "-"; -} - -function _escape(value: unknown): string { - return String(value ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -function _cellText(value: unknown): string { - const text = String(value || "").trim(); - return _escape(text || "-"); -} - -function _typeCell(value: unknown): string { - const type = String(value || ""); - const tone = type === "explicit_quote" ? "accent" : type === "topic_follow" ? "success" : type === "unscored" ? "warning" : "muted"; - return `${_escape(_feedbackTypeLabel(type))}`; -} - -function _confidenceTone(value: unknown): "success" | "warning" | "muted" { - const confidence = String(value || ""); - if (confidence === "gold" || confidence === "high") return "success"; - if (confidence === "medium") return "warning"; - return "muted"; -} - -function _confidenceCell(value: unknown): string { - const confidence = String(value || "-"); - return `${_escape(_confidenceLabel(confidence))}`; -} - -function FeedbackDetail(props: { item: Record | null }): ReactElement { - const item = props.item; - if (!item) { - return
反馈详情
点开一条记录后,这里会显示用户回复、命中的 proactive 和助手后续回复。
; - } - const type = String(item.feedback_type || ""); - return ( -
-
-
-

主动消息效果

-

用户如何回应这次主动触达

- {String(item.session_key || "未关联会话")} -
- {_feedbackTypeLabel(type)} -
- -
- - - -
- -
-

对话链路

- - - -
- -
- 查看匹配依据与消息标识 -
-
匹配方式
{String(item.matched_by || "-")}
-
用户消息 ID
{String(item.user_message_id || "-")}
-
主动消息 ID
{String(item.proactive_message_id || "-")}
-
-
-
- ); -} - -function SummaryMetric(props: { label: string; value: string }): ReactElement { - return
{props.label}{props.value}
; -} - -function TimelineStep(props: { index: string; title: string; text: string; emphasis?: boolean }): ReactElement { - return ( -
- {props.index} -
-

{props.title}

-

{props.text || "没有可展示的内容"}

-
-
- ); -} - -window.AkashicDashboard.registerPlugin({ - id: "proactive_feedback", - label: "主动反馈", - viewLabel: "主动反馈", - pageSize: 50, - rowKey: "id", - - countTitle(total: number): string { - return `共 ${total} 条反馈`; - }, - - columns: [ - { key: "created_at", label: "时间", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, - { key: "feedback_type", label: "类型", width: 126, renderCell: _typeCell }, - { key: "confidence", label: "置信度", width: 84, renderCell: _confidenceCell }, - { key: "lag_seconds", label: "延迟", width: 68, fmt: "lag", cellClass: "mono cell-metric", align: "right" }, - { key: "user_reply_preview", label: "用户回复", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, - { key: "proactive_preview", label: "命中内容", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, - ], - - async getCount(): Promise { - try { - const overview = await api("/api/dashboard/proactive-feedback/overview"); - return overview.total || 0; - } catch { - return null; - } - }, - - async fetchPage({ page, pageSize }: { page: number; pageSize: number }) { - const params = new URLSearchParams(); - params.set("page", String(page)); - params.set("page_size", String(pageSize)); - const data = await api(`/api/dashboard/proactive-feedback/events?${params.toString()}`); - return { items: data.items || [], total: data.total || 0 }; - }, - - async fetchDetail(item: Record) { - return api>(`/api/dashboard/proactive-feedback/events/${item.id}`); - }, - - Detail: FeedbackDetail, - - formatters: { - score: (value: unknown) => _score(value), - lag: (value: unknown) => _lag(value), - "mono-time": (value: unknown) => _shortTs(value), - }, -}); diff --git a/plugin.py b/plugin.py index d27808f..cb21161 100644 --- a/plugin.py +++ b/plugin.py @@ -65,6 +65,12 @@ drift_skill_roots: tuple[str, ...] = () workspace_roots: tuple[str, ...] = () dashboard_module = "dashboard.py" +web_module = "web_module.js" +web_requires = ("workbench.panels.v1",) +web_provides = () +web_contract_digests = { + "workbench.panels.v1": "724b282c22c4b3f3a36967ab664c4dfd8bce4257665f99459000306938caf527", +} async def apply(ctx: Context, config: object) -> None: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e1046aa..487ea42 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1122,8 +1122,10 @@ async def test_plugin_installs_and_loads_from_ordinary_cache(tmp_path: Path) -> core_plugins = Path(inspect.getfile(PluginManager)).parents[2] / "plugins" manager = PluginManager( plugin_dirs=[ + core_plugins / "shell_ui", core_plugins / "models", core_plugins / "openai_compatible", + core_plugins / "workbench_ui", ], event_bus=EventBus(), tool_registry=None, @@ -1162,6 +1164,8 @@ async def test_manager_stable_candidate_ui_dashboard_and_cleanup(tmp_path: Path) "scorer.py", "mobile_panel.js", "mobile_panel.css", + "web_module.js", + "web_module.css", "akashic.plugin.toml", ): shutil.copy2(Path(__file__).parents[1] / filename, plugin_dir / filename) @@ -1172,8 +1176,10 @@ async def test_manager_stable_candidate_ui_dashboard_and_cleanup(tmp_path: Path) manager = PluginManager( plugin_dirs=[ tmp_path / "plugins", + core_plugins / "shell_ui", core_plugins / "models", core_plugins / "openai_compatible", + core_plugins / "workbench_ui", ], event_bus=EventBus(), tool_registry=None, @@ -1231,8 +1237,11 @@ async def test_manager_stable_candidate_ui_dashboard_and_cleanup(tmp_path: Path) candidate_snapshot = candidate.runtime_snapshot assert candidate_snapshot.mobile_ui_registry is not None dashboard_host.prepare_snapshot(candidate_snapshot) - assert len(candidate_snapshot.dashboard_bindings) == 1 - binding = candidate_snapshot.dashboard_bindings[0] + binding = next( + item + for item in candidate_snapshot.dashboard_bindings + if item.plugin_id == "proactive_feedback" + ) assert isinstance(binding, DashboardBinding) assert binding.validation is True assert binding.runtime_data_root is not None diff --git a/web_module.css b/web_module.css new file mode 100644 index 0000000..2bcdc39 --- /dev/null +++ b/web_module.css @@ -0,0 +1,37 @@ +.proactive-feedback-workbench-panel { max-width: 1180px; margin-inline: auto; } +.proactive-feedback-workbench-panel > header { display: flex; align-items: end; justify-content: space-between; gap: 20px; } +.proactive-feedback-workbench-panel h1, .proactive-feedback-workbench-panel h2, .proactive-feedback-workbench-panel h3, .proactive-feedback-workbench-panel h4, .proactive-feedback-workbench-panel p { margin: 0; } +.proactive-feedback-workbench-panel > header p, .proactive-feedback-panel-row span, .proactive-feedback-panel-row small, .proactive-feedback-workbench-panel article > header span { color: var(--ak-ink-secondary); } +.proactive-feedback-workbench-panel button, .proactive-feedback-workbench-panel select { min-height: 44px; } +.proactive-feedback-overview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin-block: 16px; background: var(--ak-rule-subtle); } +.proactive-feedback-overview div, .proactive-feedback-overview > p { margin: 0; padding: 14px; background: var(--ak-paper-quiet); } +.proactive-feedback-overview span { display: block; color: var(--ak-ink-secondary); font-size: 0.78rem; } +.proactive-feedback-overview strong { display: block; margin-block-start: 4px; } +.proactive-feedback-toolbar { display: flex; align-items: end; justify-content: space-between; gap: 16px; } +.proactive-feedback-toolbar label { display: grid; gap: 4px; font-size: 0.8rem; } +.proactive-feedback-toolbar p { color: var(--ak-ink-secondary); } +.proactive-feedback-panel-grid { display: grid; grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1.1fr); gap: 24px; margin-block-start: 16px; } +.proactive-feedback-panel-row { display: grid; width: 100%; gap: 4px; border: 0; border-block-end: 1px solid var(--ak-rule-subtle); padding: 12px; color: inherit; background: transparent; text-align: start; cursor: pointer; } +@media (hover: hover) { .proactive-feedback-panel-row:hover { background: var(--ak-paper-quiet); } } +.proactive-feedback-panel-grid footer { display: flex; align-items: center; justify-content: center; gap: 12px; padding-block: 16px; } +.proactive-feedback-panel-grid > article { min-width: 0; border-inline-start: 1px solid var(--ak-rule-subtle); padding-inline-start: 24px; } +.proactive-feedback-panel-grid article > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; } +.proactive-feedback-panel-grid article > header p { color: var(--ak-ink-secondary); font-size: 0.8rem; } +.proactive-feedback-detail-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } +.proactive-feedback-detail-metrics div { padding: 12px; background: var(--ak-paper-quiet); } +.proactive-feedback-detail-metrics dt { color: var(--ak-ink-secondary); font-size: 0.8rem; } +.proactive-feedback-detail-metrics dd { margin: 4px 0 0; font-family: ui-monospace, monospace; } +.proactive-feedback-timeline { display: grid; gap: 10px; margin-block: 18px; } +.proactive-feedback-step { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 12px; } +.proactive-feedback-step > span { display: grid; width: 30px; height: 30px; place-items: center; border: 1px solid var(--ak-rule-subtle); border-radius: 50%; color: var(--ak-ink-secondary); font-family: ui-monospace, monospace; } +.proactive-feedback-step h4 { margin-block: 4px; } +.proactive-feedback-step p { color: var(--ak-ink-secondary); white-space: pre-wrap; } +.proactive-feedback-step.is-emphasis p { padding: 12px; border-inline-start: 3px solid var(--ak-rule-focus); background: var(--ak-paper-quiet); } +.proactive-feedback-panel-grid details { border-block-start: 1px solid var(--ak-rule-subtle); padding-block-start: 12px; } +.proactive-feedback-panel-grid summary { min-height: 44px; cursor: pointer; } +.proactive-feedback-technical { display: grid; gap: 8px; } +.proactive-feedback-technical div { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 12px; } +.proactive-feedback-technical dt { color: var(--ak-ink-secondary); font-size: 0.8rem; } +.proactive-feedback-technical dd { overflow-wrap: anywhere; margin: 0; font-family: ui-monospace, monospace; } +.proactive-feedback-workbench-panel :focus-visible { outline: 2px solid var(--ak-rule-focus); outline-offset: 2px; } +@media (max-width: 820px) { .proactive-feedback-workbench-panel > header, .proactive-feedback-toolbar { align-items: start; flex-direction: column; } .proactive-feedback-overview, .proactive-feedback-detail-metrics, .proactive-feedback-panel-grid { grid-template-columns: 1fr; } .proactive-feedback-panel-grid > article { border-inline-start: 0; padding-inline-start: 0; } } diff --git a/web_module.js b/web_module.js new file mode 100644 index 0000000..56c2fe1 --- /dev/null +++ b/web_module.js @@ -0,0 +1,189 @@ +export function activate(ctx) { + return ctx.ui.inject("workbench.panels.v1", (mount) => mount.register({ + id: "proactive-feedback", + label: "主动反馈", + order: 50, + render(host) { + const panel = document.createElement("section"); + panel.className = "proactive-feedback-workbench-panel"; + panel.innerHTML = `

主动反馈

查看主动消息是否被继续,以及对应的回应链路。

正在读取反馈总览…

选择一条反馈查看完整回应链路。

`; + host.replaceChildren(panel); + const overview = panel.querySelector("[data-overview]"); + const refresh = panel.querySelector("[data-refresh]"); + const filter = panel.querySelector("[data-filter]"); + const status = panel.querySelector("[data-status]"); + const list = panel.querySelector("[data-list]"); + const detail = panel.querySelector("[data-detail]"); + const pageText = panel.querySelector("[data-page]"); + const previous = panel.querySelector("[data-previous]"); + const next = panel.querySelector("[data-next]"); + let page = 1; + let total = 0; + let disposed = false; + let overviewRequest = new AbortController(); + let listRequest = new AbortController(); + let detailRequest = new AbortController(); + + const loadOverview = async () => { + overviewRequest.abort(); + overviewRequest = new AbortController(); + const request = overviewRequest; + overview.textContent = "正在读取反馈总览…"; + try { + const data = await json(ctx, "/api/dashboard/proactive-feedback/overview", request.signal); + if (disposed || request.signal.aborted) return; + overview.innerHTML = renderOverview(data); + } catch (reason) { + if (!disposed && !request.signal.aborted) showError(overview, reason); + } + }; + + const loadList = async () => { + listRequest.abort(); + listRequest = new AbortController(); + const request = listRequest; + const requestedPage = page; + const params = new URLSearchParams({page: String(requestedPage), page_size: "25"}); + if (filter.value) params.set("feedback_type", filter.value); + status.textContent = "正在读取反馈记录…"; + try { + const data = await json(ctx, `/api/dashboard/proactive-feedback/events?${params}`, request.signal); + if (disposed || request.signal.aborted) return; + total = finiteNumber(data.total); + renderRows(list, data.items, openDetail); + const pages = Math.max(1, Math.ceil(total / 25)); + pageText.textContent = `${requestedPage} / ${pages}`; + previous.disabled = requestedPage <= 1; + next.disabled = requestedPage >= pages; + status.textContent = total ? `共 ${total} 条反馈` : "没有符合条件的反馈。"; + } catch (reason) { + if (!disposed && !request.signal.aborted) showError(status, reason); + } + }; + + const openDetail = async (eventId) => { + detailRequest.abort(); + detailRequest = new AbortController(); + const request = detailRequest; + detail.innerHTML = "

正在读取详情…

"; + try { + const item = await json(ctx, `/api/dashboard/proactive-feedback/events/${encodeURIComponent(eventId)}`, request.signal); + if (disposed || request.signal.aborted) return; + detail.innerHTML = renderDetail(item); + } catch (reason) { + if (!disposed && !request.signal.aborted) showError(detail, reason); + } + }; + + refresh.addEventListener("click", () => { + void loadOverview(); + void loadList(); + }); + filter.addEventListener("change", () => { + page = 1; + void loadList(); + }); + previous.addEventListener("click", () => { + if (page > 1) { + page -= 1; + void loadList(); + } + }); + next.addEventListener("click", () => { + if (page * 25 < total) { + page += 1; + void loadList(); + } + }); + void loadOverview(); + void loadList(); + return () => { + disposed = true; + overviewRequest.abort(); + listRequest.abort(); + detailRequest.abort(); + host.replaceChildren(); + }; + }, + })); +} + +function renderOverview(data) { + return `
总反馈${finiteNumber(data && data.total)}
话题延续率${percent(data && data.follow_rate)}
明确引用${finiteNumber(data && data.explicit_quote)}
高置信度${finiteNumber(data && data.high_confidence)}
`; +} + +function renderRows(target, items, openDetail) { + target.replaceChildren(); + if (!Array.isArray(items) || !items.length) { + target.innerHTML = "

没有可展示的反馈记录。

"; + return; + } + for (const item of items) { + const button = document.createElement("button"); + button.type = "button"; + button.className = "proactive-feedback-panel-row"; + button.innerHTML = `${escapeHtml(feedbackLabel(item.feedback_type))}${escapeHtml(shortTime(item.created_at))} · ${escapeHtml(confidenceLabel(item.confidence))} · ${escapeHtml(lagText(item.lag_seconds))}${escapeHtml(String(item.user_reply_preview || item.user_preview || "没有可展示的用户反馈"))}`; + button.addEventListener("click", () => void openDetail(item.id)); + target.append(button); + } +} + +function renderDetail(item) { + return `

主动消息效果

用户如何回应这次主动触达

${escapeHtml(String(item.session_key || "未关联会话"))}
${escapeHtml(feedbackLabel(item.feedback_type))}
置信度
${escapeHtml(confidenceLabel(item.confidence))}
响应延迟
${escapeHtml(lagText(item.lag_seconds))}
PUA 分数
${escapeHtml(score(item.pua_score))}

对话链路

${timelineStep("1", "主动消息", item.proactive_preview || item.quoted_preview)}${timelineStep("2", "用户反馈", item.user_reply_preview || item.user_preview, true)}${timelineStep("3", "助手后续", item.assistant_preview)}
查看匹配依据与消息标识
匹配方式
${escapeHtml(String(item.matched_by || "-"))}
用户消息 ID
${escapeHtml(String(item.user_message_id || "-"))}
主动消息 ID
${escapeHtml(String(item.proactive_message_id || "-"))}
`; +} + +function timelineStep(index, title, text, emphasis = false) { + return `
${index}

${title}

${escapeHtml(String(text || "没有可展示的内容"))}

`; +} + +async function json(ctx, path, signal) { + const response = await ctx.http.request(path, {method: "GET", signal}); + const body = await response.json(); + if (!response.ok) throw new Error(body?.detail || body?.message || `HTTP ${response.status}`); + return body; +} + +function feedbackLabel(value) { + return ({explicit_quote: "明确引用", topic_follow: "话题延续", no_topic_follow: "未延续话题", unscored: "待评分"})[value] || String(value || "-"); +} + +function confidenceLabel(value) { + return ({gold: "金标", high: "高", medium: "中", low: "低"})[value] || String(value || "-"); +} + +function score(value) { + const number = Number(value); + return Number.isFinite(number) ? number.toFixed(3) : "-"; +} + +function lagText(value) { + const number = Number(value); + if (!Number.isFinite(number)) return "-"; + if (number < 60) return `${number}s`; + if (number < 3600) return `${Math.round(number / 60)}m`; + return `${(number / 3600).toFixed(1)}h`; +} + +function percent(value) { + const number = Number(value); + return Number.isFinite(number) ? `${Math.round(number * 100)}%` : "-"; +} + +function finiteNumber(value) { + const number = Number(value); + return Number.isFinite(number) ? number : 0; +} + +function shortTime(value) { + const date = new Date(String(value || "")); + return Number.isNaN(date.getTime()) ? "-" : new Intl.DateTimeFormat("zh-CN", {month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false}).format(date); +} + +function showError(target, reason) { + target.setAttribute("role", "alert"); + target.textContent = reason instanceof Error ? reason.message : String(reason); +} + +function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, (character) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"})[character]); +} From 10c78f56535528398e5c3f5137185bbba483c226 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 08:35:19 +0800 Subject: [PATCH 2/9] ci: validate Web UI composition --- .github/workflows/plugin-api-v3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index a5d03fa..dc07282 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: 0607e546de923e9377b04fb841d01384b1a666c1 + ref: 14221ff778d660ab0e9c9249a8a8ff772861dbe9 path: .akashic-core - uses: actions/setup-python@v5 with: From 99fadb184a388936b9e1e074586a81f1e0ac5c1c Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 08:41:49 +0800 Subject: [PATCH 3/9] ci: follow final Core head --- .github/workflows/plugin-api-v3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index dc07282..a7d8f4e 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: 14221ff778d660ab0e9c9249a8a8ff772861dbe9 + ref: a4e87422a2448c4b148b03777da349b51c77ed16 path: .akashic-core - uses: actions/setup-python@v5 with: From e9d55a4c258fc78f947b9ead9630a5f6cd08bce5 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 08:47:29 +0800 Subject: [PATCH 4/9] ci: validate final Core release --- .github/workflows/plugin-api-v3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index a7d8f4e..5fe6171 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: a4e87422a2448c4b148b03777da349b51c77ed16 + ref: f72b9e2ce2031133462e68aeaa0379fa883daeb2 path: .akashic-core - uses: actions/setup-python@v5 with: From 6f0ddf3de78022270bd665fe4631ac5257caad9c Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 09:00:07 +0800 Subject: [PATCH 5/9] ci: validate deployed Core release --- .github/workflows/plugin-api-v3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index 5fe6171..d03d448 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: f72b9e2ce2031133462e68aeaa0379fa883daeb2 + ref: 69a9616f6f48f19dc109a6b3eef8fc1000825829 path: .akashic-core - uses: actions/setup-python@v5 with: From 3a96e8bf502bc6562d9e2f63cf63f8b576c8ba56 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 19:17:44 +0800 Subject: [PATCH 6/9] fix(web-ui): compose proactive panel through workbench --- akashic.plugin.toml | 2 +- dashboard_panel.css | 159 +++++++++++++++++++ dashboard_panel.tsx | 220 ++++++++++++++++++++++++++ plugin.py | 6 +- web_module.css | 211 ++++++++++++++++++++----- web_module.js | 369 ++++++++++++++++++++++---------------------- 6 files changed, 742 insertions(+), 225 deletions(-) create mode 100644 dashboard_panel.css create mode 100644 dashboard_panel.tsx diff --git a/akashic.plugin.toml b/akashic.plugin.toml index aa820b1..c5e4338 100644 --- a/akashic.plugin.toml +++ b/akashic.plugin.toml @@ -1,6 +1,6 @@ schema_version = 1 name = "proactive_feedback" -version = "3.0.0" +version = "3.0.1" api_version = 3 entrypoint = "plugin.py" diff --git a/dashboard_panel.css b/dashboard_panel.css new file mode 100644 index 0000000..a5d81a4 --- /dev/null +++ b/dashboard_panel.css @@ -0,0 +1,159 @@ +.feedback-empty { + padding: 24px 16px; +} + +.feedback-empty__title { + color: var(--ak-color-text-primary); + font-size: 20px; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.25; +} + +.feedback-empty__text { + margin-block-start: 8px; + color: var(--ak-color-text-secondary); + font-size: 13px; + line-height: 1.625; +} + +.feedback-detail { + display: grid; + gap: 20px; + padding: 24px; + color: var(--ak-color-text-primary); +} + +.feedback-detail__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.feedback-detail__header p { + margin: 0 0 4px; + color: var(--ak-color-action-primary); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; +} + +.feedback-detail__header h2 { + margin: 0; + font-size: 20px; + letter-spacing: -0.02em; +} + +.feedback-detail__header span { + display: block; + margin-block-start: 5px; + color: var(--ak-color-text-muted); + font-size: 12px; +} + +.feedback-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border-block: 1px solid var(--ak-color-border-default); +} + +.feedback-summary > div { + display: grid; + gap: 5px; + padding: 14px; +} + +.feedback-summary > div + div { border-inline-start: 1px solid var(--ak-color-border-default); } +.feedback-summary span { color: var(--ak-color-text-muted); font-size: 11px; } +.feedback-summary strong { font: 650 18px var(--mono); } + +.feedback-timeline { + display: grid; + gap: 0; +} + +.feedback-timeline > h3 { + margin: 0 0 10px; + font-size: 15px; +} + +.feedback-step { + position: relative; + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + gap: 12px; + padding-block: 10px; +} + +.feedback-step:not(:last-child)::after { + position: absolute; + top: 40px; + bottom: -10px; + left: 14px; + width: 1px; + content: ""; + background: var(--ak-color-border-strong); +} + +.feedback-step__index { + display: grid; + width: 30px; + height: 30px; + place-items: center; + border: 1px solid var(--ak-color-border-strong); + border-radius: 50%; + color: var(--ak-color-text-muted); + background: var(--ak-color-bg-canvas); + font: 650 11px var(--mono); +} + +.feedback-step.is-emphasis .feedback-step__index { + border-color: var(--ak-color-action-primary); + color: var(--ak-color-on-action-primary); + background: var(--ak-color-action-soft); +} + +.feedback-step h4 { margin: 3px 0 6px; font-size: 13px; } +.feedback-step p { + margin: 0; + color: var(--ak-color-text-secondary); + font-size: 13px; + line-height: 1.65; + white-space: pre-wrap; +} + +.feedback-step.is-emphasis p { + padding: 12px; + border-inline-start: 3px solid var(--ak-color-action-primary); + background: var(--ak-color-action-soft); +} + +.feedback-technical { + border-block-start: 1px solid var(--ak-color-border-default); +} + +.feedback-technical summary { + padding: 14px 2px; + font-size: 13px; + font-weight: 650; + cursor: pointer; +} + +.feedback-technical summary:focus-visible { + outline: 2px solid var(--ak-color-action-primary); + outline-offset: 2px; +} + +.feedback-technical dl { display: grid; gap: 8px; margin: 0 0 14px; } +.feedback-technical dl > div { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 12px; } +.feedback-technical dt { color: var(--ak-color-text-muted); font-size: 12px; } +.feedback-technical dd { overflow-wrap: anywhere; margin: 0; font: 12px var(--mono); } + +@media (max-width: 720px) { + .feedback-detail { padding: 18px; } + .feedback-detail__header { flex-direction: column; } + .feedback-summary { grid-template-columns: 1fr; } + .feedback-summary > div + div { border-block-start: 1px solid var(--ak-color-border-default); border-inline-start: 0; } + .feedback-technical dl > div { grid-template-columns: 1fr; gap: 3px; } +} diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx new file mode 100644 index 0000000..79d7aad --- /dev/null +++ b/dashboard_panel.tsx @@ -0,0 +1,220 @@ +import { type ReactElement } from "react"; +import { Chip, chipClass } from "@akashic/dashboard-ui"; +import "./dashboard_panel.css"; +import type { WebHostContextV1, WebUiDisposer } from "@akashic/web-ui-v1"; +import type { WorkbenchPanelEntry } 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("主动反馈工作台面板未激活"); + 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 { + total: number; +} + +interface FetchPage { + items: Record[]; + total: number; +} + +function _score(value: unknown): string { + return typeof value === "number" ? value.toFixed(3) : "-"; +} + +function _shortTs(value: unknown): string { + const text = String(value || ""); + if (!text) return "-"; + const d = new Date(text); + if (Number.isNaN(d.getTime())) return text; + return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; +} + +function _lag(value: unknown): string { + if (typeof value !== "number") return "-"; + if (value < 60) return `${value}s`; + if (value < 3600) return `${Math.round(value / 60)}m`; + return `${(value / 3600).toFixed(1)}h`; +} + +function _tone(type: string): "success" | "warning" | "danger" | "accent" | "muted" { + if (type === "explicit_quote") return "accent"; + if (type === "topic_follow") return "success"; + if (type === "unscored") return "warning"; + if (type === "no_topic_follow") return "muted"; + return "muted"; +} + +function _feedbackTypeLabel(value: unknown): string { + const type = String(value || ""); + if (type === "explicit_quote") return "明确引用"; + if (type === "topic_follow") return "话题延续"; + if (type === "no_topic_follow") return "未延续话题"; + if (type === "unscored") return "待评分"; + return type || "-"; +} + +function _confidenceLabel(value: unknown): string { + const confidence = String(value || ""); + if (confidence === "gold") return "金标"; + if (confidence === "high") return "高"; + if (confidence === "medium") return "中"; + if (confidence === "low") return "低"; + return confidence || "-"; +} + +function _escape(value: unknown): string { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function _cellText(value: unknown): string { + const text = String(value || "").trim(); + return _escape(text || "-"); +} + +function _typeCell(value: unknown): string { + const type = String(value || ""); + const tone = type === "explicit_quote" ? "accent" : type === "topic_follow" ? "success" : type === "unscored" ? "warning" : "muted"; + return `${_escape(_feedbackTypeLabel(type))}`; +} + +function _confidenceTone(value: unknown): "success" | "warning" | "muted" { + const confidence = String(value || ""); + if (confidence === "gold" || confidence === "high") return "success"; + if (confidence === "medium") return "warning"; + return "muted"; +} + +function _confidenceCell(value: unknown): string { + const confidence = String(value || "-"); + return `${_escape(_confidenceLabel(confidence))}`; +} + +function FeedbackDetail(props: { item: Record | null }): ReactElement { + const item = props.item; + if (!item) { + return
反馈详情
点开一条记录后,这里会显示用户回复、命中的 proactive 和助手后续回复。
; + } + const type = String(item.feedback_type || ""); + return ( +
+
+
+

主动消息效果

+

用户如何回应这次主动触达

+ {String(item.session_key || "未关联会话")} +
+ {_feedbackTypeLabel(type)} +
+ +
+ + + +
+ +
+

对话链路

+ + + +
+ +
+ 查看匹配依据与消息标识 +
+
匹配方式
{String(item.matched_by || "-")}
+
用户消息 ID
{String(item.user_message_id || "-")}
+
主动消息 ID
{String(item.proactive_message_id || "-")}
+
+
+
+ ); +} + +function SummaryMetric(props: { label: string; value: string }): ReactElement { + return
{props.label}{props.value}
; +} + +function TimelineStep(props: { index: string; title: string; text: string; emphasis?: boolean }): ReactElement { + return ( +
+ {props.index} +
+

{props.title}

+

{props.text || "没有可展示的内容"}

+
+
+ ); +} + +const panel = { + id: "proactive-feedback", + label: "主动反馈", + viewLabel: "主动反馈", + order: 70, + pageSize: 50, + rowKey: "id", + + countTitle(total: number): string { + return `共 ${total} 条反馈`; + }, + + columns: [ + { key: "created_at", label: "时间", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, + { key: "feedback_type", label: "类型", width: 126, renderCell: _typeCell }, + { key: "confidence", label: "置信度", width: 84, renderCell: _confidenceCell }, + { key: "lag_seconds", label: "延迟", width: 68, fmt: "lag", cellClass: "mono cell-metric", align: "right" }, + { key: "user_reply_preview", label: "用户回复", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, + { key: "proactive_preview", label: "命中内容", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, + ], + + async getCount({ signal }: { signal: AbortSignal }): Promise { + try { + const overview = await api("/api/dashboard/proactive-feedback/overview", { signal }); + return overview.total || 0; + } catch (error) { + if (signal.aborted) throw error; + return null; + } + }, + + async fetchPage({ page, pageSize, signal }: { page: number; pageSize: number; signal: AbortSignal }) { + const params = new URLSearchParams(); + params.set("page", String(page)); + params.set("page_size", String(pageSize)); + const data = await api(`/api/dashboard/proactive-feedback/events?${params.toString()}`, { signal }); + return { items: data.items || [], total: data.total || 0 }; + }, + + async fetchDetail(item: Record, { signal }: { signal: AbortSignal }) { + return api>(`/api/dashboard/proactive-feedback/events/${item.id}`, { signal }); + }, + + Detail: FeedbackDetail, + + formatters: { + score: (value: unknown) => _score(value), + lag: (value: unknown) => _lag(value), + "mono-time": (value: unknown) => _shortTs(value), + }, +} 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 cb21161..404a7ea 100644 --- a/plugin.py +++ b/plugin.py @@ -57,7 +57,7 @@ api_version = 3 name = "proactive_feedback" -version = "3.0.0" +version = "3.0.1" desc = "记录主动消息被继续的反馈,并提供桌面与移动只读投影。" author = "Akashic" inject = (SESSION_READ, UI_SLOTS, EMBEDDINGS) @@ -66,10 +66,10 @@ workspace_roots: tuple[str, ...] = () dashboard_module = "dashboard.py" web_module = "web_module.js" -web_requires = ("workbench.panels.v1",) +web_requires = ("workbench.panels.v2",) web_provides = () web_contract_digests = { - "workbench.panels.v1": "724b282c22c4b3f3a36967ab664c4dfd8bce4257665f99459000306938caf527", + "workbench.panels.v2": "17a005a381b362ae25a0499dbf95bf7a2c3ff0bb4e9b415e7357db458de6b5db", } diff --git a/web_module.css b/web_module.css index 2bcdc39..11f81c4 100644 --- a/web_module.css +++ b/web_module.css @@ -1,37 +1,174 @@ -.proactive-feedback-workbench-panel { max-width: 1180px; margin-inline: auto; } -.proactive-feedback-workbench-panel > header { display: flex; align-items: end; justify-content: space-between; gap: 20px; } -.proactive-feedback-workbench-panel h1, .proactive-feedback-workbench-panel h2, .proactive-feedback-workbench-panel h3, .proactive-feedback-workbench-panel h4, .proactive-feedback-workbench-panel p { margin: 0; } -.proactive-feedback-workbench-panel > header p, .proactive-feedback-panel-row span, .proactive-feedback-panel-row small, .proactive-feedback-workbench-panel article > header span { color: var(--ak-ink-secondary); } -.proactive-feedback-workbench-panel button, .proactive-feedback-workbench-panel select { min-height: 44px; } -.proactive-feedback-overview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin-block: 16px; background: var(--ak-rule-subtle); } -.proactive-feedback-overview div, .proactive-feedback-overview > p { margin: 0; padding: 14px; background: var(--ak-paper-quiet); } -.proactive-feedback-overview span { display: block; color: var(--ak-ink-secondary); font-size: 0.78rem; } -.proactive-feedback-overview strong { display: block; margin-block-start: 4px; } -.proactive-feedback-toolbar { display: flex; align-items: end; justify-content: space-between; gap: 16px; } -.proactive-feedback-toolbar label { display: grid; gap: 4px; font-size: 0.8rem; } -.proactive-feedback-toolbar p { color: var(--ak-ink-secondary); } -.proactive-feedback-panel-grid { display: grid; grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1.1fr); gap: 24px; margin-block-start: 16px; } -.proactive-feedback-panel-row { display: grid; width: 100%; gap: 4px; border: 0; border-block-end: 1px solid var(--ak-rule-subtle); padding: 12px; color: inherit; background: transparent; text-align: start; cursor: pointer; } -@media (hover: hover) { .proactive-feedback-panel-row:hover { background: var(--ak-paper-quiet); } } -.proactive-feedback-panel-grid footer { display: flex; align-items: center; justify-content: center; gap: 12px; padding-block: 16px; } -.proactive-feedback-panel-grid > article { min-width: 0; border-inline-start: 1px solid var(--ak-rule-subtle); padding-inline-start: 24px; } -.proactive-feedback-panel-grid article > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; } -.proactive-feedback-panel-grid article > header p { color: var(--ak-ink-secondary); font-size: 0.8rem; } -.proactive-feedback-detail-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } -.proactive-feedback-detail-metrics div { padding: 12px; background: var(--ak-paper-quiet); } -.proactive-feedback-detail-metrics dt { color: var(--ak-ink-secondary); font-size: 0.8rem; } -.proactive-feedback-detail-metrics dd { margin: 4px 0 0; font-family: ui-monospace, monospace; } -.proactive-feedback-timeline { display: grid; gap: 10px; margin-block: 18px; } -.proactive-feedback-step { display: grid; grid-template-columns: 30px minmax(0, 1fr); gap: 12px; } -.proactive-feedback-step > span { display: grid; width: 30px; height: 30px; place-items: center; border: 1px solid var(--ak-rule-subtle); border-radius: 50%; color: var(--ak-ink-secondary); font-family: ui-monospace, monospace; } -.proactive-feedback-step h4 { margin-block: 4px; } -.proactive-feedback-step p { color: var(--ak-ink-secondary); white-space: pre-wrap; } -.proactive-feedback-step.is-emphasis p { padding: 12px; border-inline-start: 3px solid var(--ak-rule-focus); background: var(--ak-paper-quiet); } -.proactive-feedback-panel-grid details { border-block-start: 1px solid var(--ak-rule-subtle); padding-block-start: 12px; } -.proactive-feedback-panel-grid summary { min-height: 44px; cursor: pointer; } -.proactive-feedback-technical { display: grid; gap: 8px; } -.proactive-feedback-technical div { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 12px; } -.proactive-feedback-technical dt { color: var(--ak-ink-secondary); font-size: 0.8rem; } -.proactive-feedback-technical dd { overflow-wrap: anywhere; margin: 0; font-family: ui-monospace, monospace; } -.proactive-feedback-workbench-panel :focus-visible { outline: 2px solid var(--ak-rule-focus); outline-offset: 2px; } -@media (max-width: 820px) { .proactive-feedback-workbench-panel > header, .proactive-feedback-toolbar { align-items: start; flex-direction: column; } .proactive-feedback-overview, .proactive-feedback-detail-metrics, .proactive-feedback-panel-grid { grid-template-columns: 1fr; } .proactive-feedback-panel-grid > article { border-inline-start: 0; padding-inline-start: 0; } } +/* dashboard_panel.css */ +.feedback-empty { + padding: 24px 16px; +} +.feedback-empty__title { + color: var(--ak-color-text-primary); + font-size: 20px; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.25; +} +.feedback-empty__text { + margin-block-start: 8px; + color: var(--ak-color-text-secondary); + font-size: 13px; + line-height: 1.625; +} +.feedback-detail { + display: grid; + gap: 20px; + padding: 24px; + color: var(--ak-color-text-primary); +} +.feedback-detail__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} +.feedback-detail__header p { + margin: 0 0 4px; + color: var(--ak-color-action-primary); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; +} +.feedback-detail__header h2 { + margin: 0; + font-size: 20px; + letter-spacing: -0.02em; +} +.feedback-detail__header span { + display: block; + margin-block-start: 5px; + color: var(--ak-color-text-muted); + font-size: 12px; +} +.feedback-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border-block: 1px solid var(--ak-color-border-default); +} +.feedback-summary > div { + display: grid; + gap: 5px; + padding: 14px; +} +.feedback-summary > div + div { + border-inline-start: 1px solid var(--ak-color-border-default); +} +.feedback-summary span { + color: var(--ak-color-text-muted); + font-size: 11px; +} +.feedback-summary strong { + font: 650 18px var(--mono); +} +.feedback-timeline { + display: grid; + gap: 0; +} +.feedback-timeline > h3 { + margin: 0 0 10px; + font-size: 15px; +} +.feedback-step { + position: relative; + display: grid; + grid-template-columns: 30px minmax(0, 1fr); + gap: 12px; + padding-block: 10px; +} +.feedback-step:not(:last-child)::after { + position: absolute; + top: 40px; + bottom: -10px; + left: 14px; + width: 1px; + content: ""; + background: var(--ak-color-border-strong); +} +.feedback-step__index { + display: grid; + width: 30px; + height: 30px; + place-items: center; + border: 1px solid var(--ak-color-border-strong); + border-radius: 50%; + color: var(--ak-color-text-muted); + background: var(--ak-color-bg-canvas); + font: 650 11px var(--mono); +} +.feedback-step.is-emphasis .feedback-step__index { + border-color: var(--ak-color-action-primary); + color: var(--ak-color-on-action-primary); + background: var(--ak-color-action-soft); +} +.feedback-step h4 { + margin: 3px 0 6px; + font-size: 13px; +} +.feedback-step p { + margin: 0; + color: var(--ak-color-text-secondary); + font-size: 13px; + line-height: 1.65; + white-space: pre-wrap; +} +.feedback-step.is-emphasis p { + padding: 12px; + border-inline-start: 3px solid var(--ak-color-action-primary); + background: var(--ak-color-action-soft); +} +.feedback-technical { + border-block-start: 1px solid var(--ak-color-border-default); +} +.feedback-technical summary { + padding: 14px 2px; + font-size: 13px; + font-weight: 650; + cursor: pointer; +} +.feedback-technical summary:focus-visible { + outline: 2px solid var(--ak-color-action-primary); + outline-offset: 2px; +} +.feedback-technical dl { + display: grid; + gap: 8px; + margin: 0 0 14px; +} +.feedback-technical dl > div { + display: grid; + grid-template-columns: 110px minmax(0, 1fr); + gap: 12px; +} +.feedback-technical dt { + color: var(--ak-color-text-muted); + font-size: 12px; +} +.feedback-technical dd { + overflow-wrap: anywhere; + margin: 0; + font: 12px var(--mono); +} +@media (max-width: 720px) { + .feedback-detail { + padding: 18px; + } + .feedback-detail__header { + flex-direction: column; + } + .feedback-summary { + grid-template-columns: 1fr; + } + .feedback-summary > div + div { + border-block-start: 1px solid var(--ak-color-border-default); + border-inline-start: 0; + } + .feedback-technical dl > div { + grid-template-columns: 1fr; + gap: 3px; + } +} diff --git a/web_module.js b/web_module.js index 56c2fe1..a3221b1 100644 --- a/web_module.js +++ b/web_module.js @@ -1,189 +1,190 @@ -export function activate(ctx) { - return ctx.ui.inject("workbench.panels.v1", (mount) => mount.register({ - id: "proactive-feedback", - label: "主动反馈", - order: 50, - render(host) { - const panel = document.createElement("section"); - panel.className = "proactive-feedback-workbench-panel"; - panel.innerHTML = `

主动反馈

查看主动消息是否被继续,以及对应的回应链路。

正在读取反馈总览…

选择一条反馈查看完整回应链路。

`; - host.replaceChildren(panel); - const overview = panel.querySelector("[data-overview]"); - const refresh = panel.querySelector("[data-refresh]"); - const filter = panel.querySelector("[data-filter]"); - const status = panel.querySelector("[data-status]"); - const list = panel.querySelector("[data-list]"); - const detail = panel.querySelector("[data-detail]"); - const pageText = panel.querySelector("[data-page]"); - const previous = panel.querySelector("[data-previous]"); - const next = panel.querySelector("[data-next]"); - let page = 1; - let total = 0; - let disposed = false; - let overviewRequest = new AbortController(); - let listRequest = new AbortController(); - let detailRequest = new AbortController(); - - const loadOverview = async () => { - overviewRequest.abort(); - overviewRequest = new AbortController(); - const request = overviewRequest; - overview.textContent = "正在读取反馈总览…"; - try { - const data = await json(ctx, "/api/dashboard/proactive-feedback/overview", request.signal); - if (disposed || request.signal.aborted) return; - overview.innerHTML = renderOverview(data); - } catch (reason) { - if (!disposed && !request.signal.aborted) showError(overview, reason); - } - }; - - const loadList = async () => { - listRequest.abort(); - listRequest = new AbortController(); - const request = listRequest; - const requestedPage = page; - const params = new URLSearchParams({page: String(requestedPage), page_size: "25"}); - if (filter.value) params.set("feedback_type", filter.value); - status.textContent = "正在读取反馈记录…"; - try { - const data = await json(ctx, `/api/dashboard/proactive-feedback/events?${params}`, request.signal); - if (disposed || request.signal.aborted) return; - total = finiteNumber(data.total); - renderRows(list, data.items, openDetail); - const pages = Math.max(1, Math.ceil(total / 25)); - pageText.textContent = `${requestedPage} / ${pages}`; - previous.disabled = requestedPage <= 1; - next.disabled = requestedPage >= pages; - status.textContent = total ? `共 ${total} 条反馈` : "没有符合条件的反馈。"; - } catch (reason) { - if (!disposed && !request.signal.aborted) showError(status, reason); - } - }; - - const openDetail = async (eventId) => { - detailRequest.abort(); - detailRequest = new AbortController(); - const request = detailRequest; - detail.innerHTML = "

正在读取详情…

"; - try { - const item = await json(ctx, `/api/dashboard/proactive-feedback/events/${encodeURIComponent(eventId)}`, request.signal); - if (disposed || request.signal.aborted) return; - detail.innerHTML = renderDetail(item); - } catch (reason) { - if (!disposed && !request.signal.aborted) showError(detail, reason); - } - }; - - refresh.addEventListener("click", () => { - void loadOverview(); - void loadList(); - }); - filter.addEventListener("change", () => { - page = 1; - void loadList(); - }); - previous.addEventListener("click", () => { - if (page > 1) { - page -= 1; - void loadList(); - } - }); - next.addEventListener("click", () => { - if (page * 25 < total) { - page += 1; - void loadList(); - } - }); - void loadOverview(); - void loadList(); - return () => { - disposed = true; - overviewRequest.abort(); - listRequest.abort(); - detailRequest.abort(); - host.replaceChildren(); - }; - }, - })); -} - -function renderOverview(data) { - return `
总反馈${finiteNumber(data && data.total)}
话题延续率${percent(data && data.follow_rate)}
明确引用${finiteNumber(data && data.explicit_quote)}
高置信度${finiteNumber(data && data.high_confidence)}
`; -} - -function renderRows(target, items, openDetail) { - target.replaceChildren(); - if (!Array.isArray(items) || !items.length) { - target.innerHTML = "

没有可展示的反馈记录。

"; - return; - } - for (const item of items) { - const button = document.createElement("button"); - button.type = "button"; - button.className = "proactive-feedback-panel-row"; - button.innerHTML = `${escapeHtml(feedbackLabel(item.feedback_type))}${escapeHtml(shortTime(item.created_at))} · ${escapeHtml(confidenceLabel(item.confidence))} · ${escapeHtml(lagText(item.lag_seconds))}${escapeHtml(String(item.user_reply_preview || item.user_preview || "没有可展示的用户反馈"))}`; - button.addEventListener("click", () => void openDetail(item.id)); - target.append(button); - } +// dashboard_panel.tsx +import { Chip, chipClass } from "@akashic/dashboard-ui"; +import { jsx, jsxs } from "react/jsx-runtime"; +var dashboardRequest = null; +async function api(path, init) { + if (!dashboardRequest) throw new Error("\u4E3B\u52A8\u53CD\u9988\u5DE5\u4F5C\u53F0\u9762\u677F\u672A\u6FC0\u6D3B"); + const response = await dashboardRequest(path, init); + const body = await response.json(); + if (!response.ok) throw new Error(String(body.detail ?? body.message ?? `HTTP ${response.status}`)); + return body; } - -function renderDetail(item) { - return `

主动消息效果

用户如何回应这次主动触达

${escapeHtml(String(item.session_key || "未关联会话"))}
${escapeHtml(feedbackLabel(item.feedback_type))}
置信度
${escapeHtml(confidenceLabel(item.confidence))}
响应延迟
${escapeHtml(lagText(item.lag_seconds))}
PUA 分数
${escapeHtml(score(item.pua_score))}

对话链路

${timelineStep("1", "主动消息", item.proactive_preview || item.quoted_preview)}${timelineStep("2", "用户反馈", item.user_reply_preview || item.user_preview, true)}${timelineStep("3", "助手后续", item.assistant_preview)}
查看匹配依据与消息标识
匹配方式
${escapeHtml(String(item.matched_by || "-"))}
用户消息 ID
${escapeHtml(String(item.user_message_id || "-"))}
主动消息 ID
${escapeHtml(String(item.proactive_message_id || "-"))}
`; +function _score(value) { + return typeof value === "number" ? value.toFixed(3) : "-"; } - -function timelineStep(index, title, text, emphasis = false) { - return `
${index}

${title}

${escapeHtml(String(text || "没有可展示的内容"))}

`; +function _shortTs(value) { + const text = String(value || ""); + if (!text) return "-"; + const d = new Date(text); + if (Number.isNaN(d.getTime())) return text; + return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } - -async function json(ctx, path, signal) { - const response = await ctx.http.request(path, {method: "GET", signal}); - const body = await response.json(); - if (!response.ok) throw new Error(body?.detail || body?.message || `HTTP ${response.status}`); - return body; +function _lag(value) { + if (typeof value !== "number") return "-"; + if (value < 60) return `${value}s`; + if (value < 3600) return `${Math.round(value / 60)}m`; + return `${(value / 3600).toFixed(1)}h`; +} +function _tone(type) { + if (type === "explicit_quote") return "accent"; + if (type === "topic_follow") return "success"; + if (type === "unscored") return "warning"; + if (type === "no_topic_follow") return "muted"; + return "muted"; +} +function _feedbackTypeLabel(value) { + const type = String(value || ""); + if (type === "explicit_quote") return "\u660E\u786E\u5F15\u7528"; + if (type === "topic_follow") return "\u8BDD\u9898\u5EF6\u7EED"; + if (type === "no_topic_follow") return "\u672A\u5EF6\u7EED\u8BDD\u9898"; + if (type === "unscored") return "\u5F85\u8BC4\u5206"; + return type || "-"; +} +function _confidenceLabel(value) { + const confidence = String(value || ""); + if (confidence === "gold") return "\u91D1\u6807"; + if (confidence === "high") return "\u9AD8"; + if (confidence === "medium") return "\u4E2D"; + if (confidence === "low") return "\u4F4E"; + return confidence || "-"; +} +function _escape(value) { + return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } - -function feedbackLabel(value) { - return ({explicit_quote: "明确引用", topic_follow: "话题延续", no_topic_follow: "未延续话题", unscored: "待评分"})[value] || String(value || "-"); -} - -function confidenceLabel(value) { - return ({gold: "金标", high: "高", medium: "中", low: "低"})[value] || String(value || "-"); -} - -function score(value) { - const number = Number(value); - return Number.isFinite(number) ? number.toFixed(3) : "-"; -} - -function lagText(value) { - const number = Number(value); - if (!Number.isFinite(number)) return "-"; - if (number < 60) return `${number}s`; - if (number < 3600) return `${Math.round(number / 60)}m`; - return `${(number / 3600).toFixed(1)}h`; -} - -function percent(value) { - const number = Number(value); - return Number.isFinite(number) ? `${Math.round(number * 100)}%` : "-"; -} - -function finiteNumber(value) { - const number = Number(value); - return Number.isFinite(number) ? number : 0; -} - -function shortTime(value) { - const date = new Date(String(value || "")); - return Number.isNaN(date.getTime()) ? "-" : new Intl.DateTimeFormat("zh-CN", {month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false}).format(date); -} - -function showError(target, reason) { - target.setAttribute("role", "alert"); - target.textContent = reason instanceof Error ? reason.message : String(reason); -} - -function escapeHtml(value) { - return String(value).replace(/[&<>"']/g, (character) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"})[character]); +function _cellText(value) { + const text = String(value || "").trim(); + return _escape(text || "-"); +} +function _typeCell(value) { + const type = String(value || ""); + const tone = type === "explicit_quote" ? "accent" : type === "topic_follow" ? "success" : type === "unscored" ? "warning" : "muted"; + return `${_escape(_feedbackTypeLabel(type))}`; +} +function _confidenceTone(value) { + const confidence = String(value || ""); + if (confidence === "gold" || confidence === "high") return "success"; + if (confidence === "medium") return "warning"; + return "muted"; +} +function _confidenceCell(value) { + const confidence = String(value || "-"); + return `${_escape(_confidenceLabel(confidence))}`; +} +function FeedbackDetail(props) { + const item = props.item; + if (!item) { + return /* @__PURE__ */ jsxs("div", { className: "feedback-empty", children: [ + /* @__PURE__ */ jsx("div", { className: "feedback-empty__title", children: "\u53CD\u9988\u8BE6\u60C5" }), + /* @__PURE__ */ jsx("div", { className: "feedback-empty__text", children: "\u70B9\u5F00\u4E00\u6761\u8BB0\u5F55\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u7528\u6237\u56DE\u590D\u3001\u547D\u4E2D\u7684 proactive \u548C\u52A9\u624B\u540E\u7EED\u56DE\u590D\u3002" }) + ] }); + } + const type = String(item.feedback_type || ""); + return /* @__PURE__ */ jsxs("main", { className: "feedback-detail", "aria-labelledby": "feedback-detail-title", children: [ + /* @__PURE__ */ jsxs("header", { className: "feedback-detail__header", children: [ + /* @__PURE__ */ jsxs("div", { children: [ + /* @__PURE__ */ jsx("p", { children: "\u4E3B\u52A8\u6D88\u606F\u6548\u679C" }), + /* @__PURE__ */ jsx("h2", { id: "feedback-detail-title", children: "\u7528\u6237\u5982\u4F55\u56DE\u5E94\u8FD9\u6B21\u4E3B\u52A8\u89E6\u8FBE" }), + /* @__PURE__ */ jsx("span", { children: String(item.session_key || "\u672A\u5173\u8054\u4F1A\u8BDD") }) + ] }), + /* @__PURE__ */ jsx(Chip, { tone: _tone(type), children: _feedbackTypeLabel(type) }) + ] }), + /* @__PURE__ */ jsxs("section", { className: "feedback-summary", "aria-label": "\u53CD\u9988\u5224\u65AD\u6458\u8981", children: [ + /* @__PURE__ */ jsx(SummaryMetric, { label: "\u7F6E\u4FE1\u5EA6", value: _confidenceLabel(item.confidence) }), + /* @__PURE__ */ jsx(SummaryMetric, { label: "\u54CD\u5E94\u5EF6\u8FDF", value: _lag(item.lag_seconds) }), + /* @__PURE__ */ jsx(SummaryMetric, { label: "PUA \u5206\u6570", value: _score(item.pua_score) }) + ] }), + /* @__PURE__ */ jsxs("section", { className: "feedback-timeline", "aria-labelledby": "feedback-timeline-title", children: [ + /* @__PURE__ */ jsx("h3", { id: "feedback-timeline-title", children: "\u5BF9\u8BDD\u94FE\u8DEF" }), + /* @__PURE__ */ jsx(TimelineStep, { index: "1", title: "\u4E3B\u52A8\u6D88\u606F", text: String(item.proactive_preview || item.quoted_preview || "") }), + /* @__PURE__ */ jsx(TimelineStep, { index: "2", title: "\u7528\u6237\u53CD\u9988", text: String(item.user_reply_preview || item.user_preview || ""), emphasis: true }), + /* @__PURE__ */ jsx(TimelineStep, { index: "3", title: "\u52A9\u624B\u540E\u7EED", text: String(item.assistant_preview || "") }) + ] }), + /* @__PURE__ */ jsxs("details", { className: "feedback-technical", children: [ + /* @__PURE__ */ jsx("summary", { children: "\u67E5\u770B\u5339\u914D\u4F9D\u636E\u4E0E\u6D88\u606F\u6807\u8BC6" }), + /* @__PURE__ */ jsxs("dl", { children: [ + /* @__PURE__ */ jsxs("div", { children: [ + /* @__PURE__ */ jsx("dt", { children: "\u5339\u914D\u65B9\u5F0F" }), + /* @__PURE__ */ jsx("dd", { children: String(item.matched_by || "-") }) + ] }), + /* @__PURE__ */ jsxs("div", { children: [ + /* @__PURE__ */ jsx("dt", { children: "\u7528\u6237\u6D88\u606F ID" }), + /* @__PURE__ */ jsx("dd", { children: String(item.user_message_id || "-") }) + ] }), + /* @__PURE__ */ jsxs("div", { children: [ + /* @__PURE__ */ jsx("dt", { children: "\u4E3B\u52A8\u6D88\u606F ID" }), + /* @__PURE__ */ jsx("dd", { children: String(item.proactive_message_id || "-") }) + ] }) + ] }) + ] }) + ] }); +} +function SummaryMetric(props) { + return /* @__PURE__ */ jsxs("div", { children: [ + /* @__PURE__ */ jsx("span", { children: props.label }), + /* @__PURE__ */ jsx("strong", { children: props.value }) + ] }); +} +function TimelineStep(props) { + return /* @__PURE__ */ jsxs("div", { className: `feedback-step${props.emphasis ? " is-emphasis" : ""}`, children: [ + /* @__PURE__ */ jsx("span", { className: "feedback-step__index", children: props.index }), + /* @__PURE__ */ jsxs("div", { children: [ + /* @__PURE__ */ jsx("h4", { children: props.title }), + /* @__PURE__ */ jsx("p", { children: props.text || "\u6CA1\u6709\u53EF\u5C55\u793A\u7684\u5185\u5BB9" }) + ] }) + ] }); +} +var panel = { + id: "proactive-feedback", + label: "\u4E3B\u52A8\u53CD\u9988", + viewLabel: "\u4E3B\u52A8\u53CD\u9988", + order: 70, + pageSize: 50, + rowKey: "id", + countTitle(total) { + return `\u5171 ${total} \u6761\u53CD\u9988`; + }, + columns: [ + { key: "created_at", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, + { key: "feedback_type", label: "\u7C7B\u578B", width: 126, renderCell: _typeCell }, + { key: "confidence", label: "\u7F6E\u4FE1\u5EA6", width: 84, renderCell: _confidenceCell }, + { key: "lag_seconds", label: "\u5EF6\u8FDF", width: 68, fmt: "lag", cellClass: "mono cell-metric", align: "right" }, + { key: "user_reply_preview", label: "\u7528\u6237\u56DE\u590D", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, + { key: "proactive_preview", label: "\u547D\u4E2D\u5185\u5BB9", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true } + ], + async getCount({ signal }) { + try { + const overview = await api("/api/dashboard/proactive-feedback/overview", { signal }); + return overview.total || 0; + } catch (error) { + if (signal.aborted) throw error; + return null; + } + }, + async fetchPage({ page, pageSize, signal }) { + const params = new URLSearchParams(); + params.set("page", String(page)); + params.set("page_size", String(pageSize)); + const data = await api(`/api/dashboard/proactive-feedback/events?${params.toString()}`, { signal }); + return { items: data.items || [], total: data.total || 0 }; + }, + async fetchDetail(item, { signal }) { + return api(`/api/dashboard/proactive-feedback/events/${item.id}`, { signal }); + }, + Detail: FeedbackDetail, + formatters: { + score: (value) => _score(value), + lag: (value) => _lag(value), + "mono-time": (value) => _shortTs(value) + } +}; +function activate(ctx) { + dashboardRequest = ctx.http.request; + const release = ctx.ui.inject("workbench.panels.v2", (mount) => mount.register(panel)); + return () => { + release(); + dashboardRequest = null; + }; } +export { + activate +}; From fe1690b6b84fa160fbf8c0d31080cbfde78f87f7 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 19:50:32 +0800 Subject: [PATCH 7/9] ci: pin Web UI Host SDK Core --- .github/workflows/plugin-api-v3.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index d03d448..66f2010 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: 69a9616f6f48f19dc109a6b3eef8fc1000825829 + ref: dab4137c19fd1c5ef01ab69bacbc918d214f85c9 path: .akashic-core - uses: actions/setup-python@v5 with: From e19bdb445e72ff3b1bba096b31b6fe451bb7304e Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 19:53:16 +0800 Subject: [PATCH 8/9] test: narrow dashboard binding before access --- tests/test_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 487ea42..eb4022b 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1240,9 +1240,9 @@ async def test_manager_stable_candidate_ui_dashboard_and_cleanup(tmp_path: Path) binding = next( item for item in candidate_snapshot.dashboard_bindings - if item.plugin_id == "proactive_feedback" + if isinstance(item, DashboardBinding) + and item.plugin_id == "proactive_feedback" ) - assert isinstance(binding, DashboardBinding) assert binding.validation is True assert binding.runtime_data_root is not None assert binding.runtime_data_root != formal_data.resolve() From 7e80a07615cbd779626e495eb5d2e52ca6c26f33 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 22:21:58 +0800 Subject: [PATCH 9/9] refactor(web-ui): adopt workbench mount lifecycle --- .github/workflows/plugin-api-v3.yml | 2 +- dashboard_panel.tsx | 18 ++- plugin.py | 2 +- web_module.css | 175 +------------------------ web_module.js | 191 +--------------------------- 5 files changed, 16 insertions(+), 372 deletions(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index 66f2010..c89437e 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: dab4137c19fd1c5ef01ab69bacbc918d214f85c9 + ref: d5e5177092d74de03588d8675f3504db36b7bacb path: .akashic-core - uses: actions/setup-python@v5 with: diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx index 79d7aad..cc4c556 100644 --- a/dashboard_panel.tsx +++ b/dashboard_panel.tsx @@ -1,8 +1,8 @@ import { type ReactElement } from "react"; -import { Chip, chipClass } from "@akashic/dashboard-ui"; +import { createRoot } from "react-dom/client"; import "./dashboard_panel.css"; import type { WebHostContextV1, WebUiDisposer } from "@akashic/web-ui-v1"; -import type { WorkbenchPanelEntry } from "@akashic/workbench-ui-v2"; +import type { WorkbenchDispatch, WorkbenchPanelEntry, WorkbenchUi } from "@akashic/workbench-ui-v2"; let dashboardRequest: WebHostContextV1["http"]["request"] | null = null; @@ -85,7 +85,7 @@ function _cellText(value: unknown): string { function _typeCell(value: unknown): string { const type = String(value || ""); const tone = type === "explicit_quote" ? "accent" : type === "topic_follow" ? "success" : type === "unscored" ? "warning" : "muted"; - return `${_escape(_feedbackTypeLabel(type))}`; + return `${_escape(_feedbackTypeLabel(type))}`; } function _confidenceTone(value: unknown): "success" | "warning" | "muted" { @@ -97,11 +97,13 @@ function _confidenceTone(value: unknown): "success" | "warning" | "muted" { function _confidenceCell(value: unknown): string { const confidence = String(value || "-"); - return `${_escape(_confidenceLabel(confidence))}`; + const tone = _confidenceTone(confidence); + return `${_escape(_confidenceLabel(confidence))}`; } -function FeedbackDetail(props: { item: Record | null }): ReactElement { +function FeedbackDetail(props: { item: Record | null; ui: WorkbenchUi }): ReactElement { const item = props.item; + const Chip = props.ui.Chip; if (!item) { return
反馈详情
点开一条记录后,这里会显示用户回复、命中的 proactive 和助手后续回复。
; } @@ -201,7 +203,11 @@ const panel = { return api>(`/api/dashboard/proactive-feedback/events/${item.id}`, { signal }); }, - Detail: FeedbackDetail, + renderDetail(item: Record | null, container: HTMLElement, dispatch: WorkbenchDispatch): WebUiDisposer { + const root = createRoot(container); + root.render(); + return () => root.unmount(); + }, formatters: { score: (value: unknown) => _score(value), diff --git a/plugin.py b/plugin.py index 404a7ea..5db52ca 100644 --- a/plugin.py +++ b/plugin.py @@ -69,7 +69,7 @@ web_requires = ("workbench.panels.v2",) web_provides = () web_contract_digests = { - "workbench.panels.v2": "17a005a381b362ae25a0499dbf95bf7a2c3ff0bb4e9b415e7357db458de6b5db", + "workbench.panels.v2": "fb6417c9bf532c1fdb344767d06065d5d3293da85deb64eff1e8088889a33bcb", } diff --git a/web_module.css b/web_module.css index 11f81c4..6047616 100644 --- a/web_module.css +++ b/web_module.css @@ -1,174 +1 @@ -/* dashboard_panel.css */ -.feedback-empty { - padding: 24px 16px; -} -.feedback-empty__title { - color: var(--ak-color-text-primary); - font-size: 20px; - font-weight: 600; - letter-spacing: -0.025em; - line-height: 1.25; -} -.feedback-empty__text { - margin-block-start: 8px; - color: var(--ak-color-text-secondary); - font-size: 13px; - line-height: 1.625; -} -.feedback-detail { - display: grid; - gap: 20px; - padding: 24px; - color: var(--ak-color-text-primary); -} -.feedback-detail__header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} -.feedback-detail__header p { - margin: 0 0 4px; - color: var(--ak-color-action-primary); - font-size: 11px; - font-weight: 700; - letter-spacing: 0.08em; -} -.feedback-detail__header h2 { - margin: 0; - font-size: 20px; - letter-spacing: -0.02em; -} -.feedback-detail__header span { - display: block; - margin-block-start: 5px; - color: var(--ak-color-text-muted); - font-size: 12px; -} -.feedback-summary { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - border-block: 1px solid var(--ak-color-border-default); -} -.feedback-summary > div { - display: grid; - gap: 5px; - padding: 14px; -} -.feedback-summary > div + div { - border-inline-start: 1px solid var(--ak-color-border-default); -} -.feedback-summary span { - color: var(--ak-color-text-muted); - font-size: 11px; -} -.feedback-summary strong { - font: 650 18px var(--mono); -} -.feedback-timeline { - display: grid; - gap: 0; -} -.feedback-timeline > h3 { - margin: 0 0 10px; - font-size: 15px; -} -.feedback-step { - position: relative; - display: grid; - grid-template-columns: 30px minmax(0, 1fr); - gap: 12px; - padding-block: 10px; -} -.feedback-step:not(:last-child)::after { - position: absolute; - top: 40px; - bottom: -10px; - left: 14px; - width: 1px; - content: ""; - background: var(--ak-color-border-strong); -} -.feedback-step__index { - display: grid; - width: 30px; - height: 30px; - place-items: center; - border: 1px solid var(--ak-color-border-strong); - border-radius: 50%; - color: var(--ak-color-text-muted); - background: var(--ak-color-bg-canvas); - font: 650 11px var(--mono); -} -.feedback-step.is-emphasis .feedback-step__index { - border-color: var(--ak-color-action-primary); - color: var(--ak-color-on-action-primary); - background: var(--ak-color-action-soft); -} -.feedback-step h4 { - margin: 3px 0 6px; - font-size: 13px; -} -.feedback-step p { - margin: 0; - color: var(--ak-color-text-secondary); - font-size: 13px; - line-height: 1.65; - white-space: pre-wrap; -} -.feedback-step.is-emphasis p { - padding: 12px; - border-inline-start: 3px solid var(--ak-color-action-primary); - background: var(--ak-color-action-soft); -} -.feedback-technical { - border-block-start: 1px solid var(--ak-color-border-default); -} -.feedback-technical summary { - padding: 14px 2px; - font-size: 13px; - font-weight: 650; - cursor: pointer; -} -.feedback-technical summary:focus-visible { - outline: 2px solid var(--ak-color-action-primary); - outline-offset: 2px; -} -.feedback-technical dl { - display: grid; - gap: 8px; - margin: 0 0 14px; -} -.feedback-technical dl > div { - display: grid; - grid-template-columns: 110px minmax(0, 1fr); - gap: 12px; -} -.feedback-technical dt { - color: var(--ak-color-text-muted); - font-size: 12px; -} -.feedback-technical dd { - overflow-wrap: anywhere; - margin: 0; - font: 12px var(--mono); -} -@media (max-width: 720px) { - .feedback-detail { - padding: 18px; - } - .feedback-detail__header { - flex-direction: column; - } - .feedback-summary { - grid-template-columns: 1fr; - } - .feedback-summary > div + div { - border-block-start: 1px solid var(--ak-color-border-default); - border-inline-start: 0; - } - .feedback-technical dl > div { - grid-template-columns: 1fr; - gap: 3px; - } -} +.feedback-empty{padding:24px 16px}.feedback-empty__title{color:var(--ak-color-text-primary);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.25}.feedback-empty__text{margin-block-start:8px;color:var(--ak-color-text-secondary);font-size:13px;line-height:1.625}.feedback-detail{display:grid;gap:20px;padding:24px;color:var(--ak-color-text-primary)}.feedback-detail__header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.feedback-detail__header p{margin:0 0 4px;color:var(--ak-color-action-primary);font-size:11px;font-weight:700;letter-spacing:.08em}.feedback-detail__header h2{margin:0;font-size:20px;letter-spacing:-.02em}.feedback-detail__header span{display:block;margin-block-start:5px;color:var(--ak-color-text-muted);font-size:12px}.feedback-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border-block:1px solid var(--ak-color-border-default)}.feedback-summary>div{display:grid;gap:5px;padding:14px}.feedback-summary>div+div{border-inline-start:1px solid var(--ak-color-border-default)}.feedback-summary span{color:var(--ak-color-text-muted);font-size:11px}.feedback-summary strong{font:650 18px var(--mono)}.feedback-timeline{display:grid;gap:0}.feedback-timeline>h3{margin:0 0 10px;font-size:15px}.feedback-step{position:relative;display:grid;grid-template-columns:30px minmax(0,1fr);gap:12px;padding-block:10px}.feedback-step:not(:last-child):after{position:absolute;top:40px;bottom:-10px;left:14px;width:1px;content:"";background:var(--ak-color-border-strong)}.feedback-step__index{display:grid;width:30px;height:30px;place-items:center;border:1px solid var(--ak-color-border-strong);border-radius:50%;color:var(--ak-color-text-muted);background:var(--ak-color-bg-canvas);font:650 11px var(--mono)}.feedback-step.is-emphasis .feedback-step__index{border-color:var(--ak-color-action-primary);color:var(--ak-color-on-action-primary);background:var(--ak-color-action-soft)}.feedback-step h4{margin:3px 0 6px;font-size:13px}.feedback-step p{margin:0;color:var(--ak-color-text-secondary);font-size:13px;line-height:1.65;white-space:pre-wrap}.feedback-step.is-emphasis p{padding:12px;border-inline-start:3px solid var(--ak-color-action-primary);background:var(--ak-color-action-soft)}.feedback-technical{border-block-start:1px solid var(--ak-color-border-default)}.feedback-technical summary{padding:14px 2px;font-size:13px;font-weight:650;cursor:pointer}.feedback-technical summary:focus-visible{outline:2px solid var(--ak-color-action-primary);outline-offset:2px}.feedback-technical dl{display:grid;gap:8px;margin:0 0 14px}.feedback-technical dl>div{display:grid;grid-template-columns:110px minmax(0,1fr);gap:12px}.feedback-technical dt{color:var(--ak-color-text-muted);font-size:12px}.feedback-technical dd{overflow-wrap:anywhere;margin:0;font:12px var(--mono)}@media(max-width:720px){.feedback-detail{padding:18px}.feedback-detail__header{flex-direction:column}.feedback-summary{grid-template-columns:1fr}.feedback-summary>div+div{border-block-start:1px solid var(--ak-color-border-default);border-inline-start:0}.feedback-technical dl>div{grid-template-columns:1fr;gap:3px}} diff --git a/web_module.js b/web_module.js index a3221b1..bc6e6a3 100644 --- a/web_module.js +++ b/web_module.js @@ -1,190 +1 @@ -// dashboard_panel.tsx -import { Chip, chipClass } from "@akashic/dashboard-ui"; -import { jsx, jsxs } from "react/jsx-runtime"; -var dashboardRequest = null; -async function api(path, init) { - if (!dashboardRequest) throw new Error("\u4E3B\u52A8\u53CD\u9988\u5DE5\u4F5C\u53F0\u9762\u677F\u672A\u6FC0\u6D3B"); - const response = await dashboardRequest(path, init); - const body = await response.json(); - if (!response.ok) throw new Error(String(body.detail ?? body.message ?? `HTTP ${response.status}`)); - return body; -} -function _score(value) { - return typeof value === "number" ? value.toFixed(3) : "-"; -} -function _shortTs(value) { - const text = String(value || ""); - if (!text) return "-"; - const d = new Date(text); - if (Number.isNaN(d.getTime())) return text; - return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; -} -function _lag(value) { - if (typeof value !== "number") return "-"; - if (value < 60) return `${value}s`; - if (value < 3600) return `${Math.round(value / 60)}m`; - return `${(value / 3600).toFixed(1)}h`; -} -function _tone(type) { - if (type === "explicit_quote") return "accent"; - if (type === "topic_follow") return "success"; - if (type === "unscored") return "warning"; - if (type === "no_topic_follow") return "muted"; - return "muted"; -} -function _feedbackTypeLabel(value) { - const type = String(value || ""); - if (type === "explicit_quote") return "\u660E\u786E\u5F15\u7528"; - if (type === "topic_follow") return "\u8BDD\u9898\u5EF6\u7EED"; - if (type === "no_topic_follow") return "\u672A\u5EF6\u7EED\u8BDD\u9898"; - if (type === "unscored") return "\u5F85\u8BC4\u5206"; - return type || "-"; -} -function _confidenceLabel(value) { - const confidence = String(value || ""); - if (confidence === "gold") return "\u91D1\u6807"; - if (confidence === "high") return "\u9AD8"; - if (confidence === "medium") return "\u4E2D"; - if (confidence === "low") return "\u4F4E"; - return confidence || "-"; -} -function _escape(value) { - return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); -} -function _cellText(value) { - const text = String(value || "").trim(); - return _escape(text || "-"); -} -function _typeCell(value) { - const type = String(value || ""); - const tone = type === "explicit_quote" ? "accent" : type === "topic_follow" ? "success" : type === "unscored" ? "warning" : "muted"; - return `${_escape(_feedbackTypeLabel(type))}`; -} -function _confidenceTone(value) { - const confidence = String(value || ""); - if (confidence === "gold" || confidence === "high") return "success"; - if (confidence === "medium") return "warning"; - return "muted"; -} -function _confidenceCell(value) { - const confidence = String(value || "-"); - return `${_escape(_confidenceLabel(confidence))}`; -} -function FeedbackDetail(props) { - const item = props.item; - if (!item) { - return /* @__PURE__ */ jsxs("div", { className: "feedback-empty", children: [ - /* @__PURE__ */ jsx("div", { className: "feedback-empty__title", children: "\u53CD\u9988\u8BE6\u60C5" }), - /* @__PURE__ */ jsx("div", { className: "feedback-empty__text", children: "\u70B9\u5F00\u4E00\u6761\u8BB0\u5F55\u540E\uFF0C\u8FD9\u91CC\u4F1A\u663E\u793A\u7528\u6237\u56DE\u590D\u3001\u547D\u4E2D\u7684 proactive \u548C\u52A9\u624B\u540E\u7EED\u56DE\u590D\u3002" }) - ] }); - } - const type = String(item.feedback_type || ""); - return /* @__PURE__ */ jsxs("main", { className: "feedback-detail", "aria-labelledby": "feedback-detail-title", children: [ - /* @__PURE__ */ jsxs("header", { className: "feedback-detail__header", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("p", { children: "\u4E3B\u52A8\u6D88\u606F\u6548\u679C" }), - /* @__PURE__ */ jsx("h2", { id: "feedback-detail-title", children: "\u7528\u6237\u5982\u4F55\u56DE\u5E94\u8FD9\u6B21\u4E3B\u52A8\u89E6\u8FBE" }), - /* @__PURE__ */ jsx("span", { children: String(item.session_key || "\u672A\u5173\u8054\u4F1A\u8BDD") }) - ] }), - /* @__PURE__ */ jsx(Chip, { tone: _tone(type), children: _feedbackTypeLabel(type) }) - ] }), - /* @__PURE__ */ jsxs("section", { className: "feedback-summary", "aria-label": "\u53CD\u9988\u5224\u65AD\u6458\u8981", children: [ - /* @__PURE__ */ jsx(SummaryMetric, { label: "\u7F6E\u4FE1\u5EA6", value: _confidenceLabel(item.confidence) }), - /* @__PURE__ */ jsx(SummaryMetric, { label: "\u54CD\u5E94\u5EF6\u8FDF", value: _lag(item.lag_seconds) }), - /* @__PURE__ */ jsx(SummaryMetric, { label: "PUA \u5206\u6570", value: _score(item.pua_score) }) - ] }), - /* @__PURE__ */ jsxs("section", { className: "feedback-timeline", "aria-labelledby": "feedback-timeline-title", children: [ - /* @__PURE__ */ jsx("h3", { id: "feedback-timeline-title", children: "\u5BF9\u8BDD\u94FE\u8DEF" }), - /* @__PURE__ */ jsx(TimelineStep, { index: "1", title: "\u4E3B\u52A8\u6D88\u606F", text: String(item.proactive_preview || item.quoted_preview || "") }), - /* @__PURE__ */ jsx(TimelineStep, { index: "2", title: "\u7528\u6237\u53CD\u9988", text: String(item.user_reply_preview || item.user_preview || ""), emphasis: true }), - /* @__PURE__ */ jsx(TimelineStep, { index: "3", title: "\u52A9\u624B\u540E\u7EED", text: String(item.assistant_preview || "") }) - ] }), - /* @__PURE__ */ jsxs("details", { className: "feedback-technical", children: [ - /* @__PURE__ */ jsx("summary", { children: "\u67E5\u770B\u5339\u914D\u4F9D\u636E\u4E0E\u6D88\u606F\u6807\u8BC6" }), - /* @__PURE__ */ jsxs("dl", { children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("dt", { children: "\u5339\u914D\u65B9\u5F0F" }), - /* @__PURE__ */ jsx("dd", { children: String(item.matched_by || "-") }) - ] }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("dt", { children: "\u7528\u6237\u6D88\u606F ID" }), - /* @__PURE__ */ jsx("dd", { children: String(item.user_message_id || "-") }) - ] }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("dt", { children: "\u4E3B\u52A8\u6D88\u606F ID" }), - /* @__PURE__ */ jsx("dd", { children: String(item.proactive_message_id || "-") }) - ] }) - ] }) - ] }) - ] }); -} -function SummaryMetric(props) { - return /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("span", { children: props.label }), - /* @__PURE__ */ jsx("strong", { children: props.value }) - ] }); -} -function TimelineStep(props) { - return /* @__PURE__ */ jsxs("div", { className: `feedback-step${props.emphasis ? " is-emphasis" : ""}`, children: [ - /* @__PURE__ */ jsx("span", { className: "feedback-step__index", children: props.index }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("h4", { children: props.title }), - /* @__PURE__ */ jsx("p", { children: props.text || "\u6CA1\u6709\u53EF\u5C55\u793A\u7684\u5185\u5BB9" }) - ] }) - ] }); -} -var panel = { - id: "proactive-feedback", - label: "\u4E3B\u52A8\u53CD\u9988", - viewLabel: "\u4E3B\u52A8\u53CD\u9988", - order: 70, - pageSize: 50, - rowKey: "id", - countTitle(total) { - return `\u5171 ${total} \u6761\u53CD\u9988`; - }, - columns: [ - { key: "created_at", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, - { key: "feedback_type", label: "\u7C7B\u578B", width: 126, renderCell: _typeCell }, - { key: "confidence", label: "\u7F6E\u4FE1\u5EA6", width: 84, renderCell: _confidenceCell }, - { key: "lag_seconds", label: "\u5EF6\u8FDF", width: 68, fmt: "lag", cellClass: "mono cell-metric", align: "right" }, - { key: "user_reply_preview", label: "\u7528\u6237\u56DE\u590D", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true }, - { key: "proactive_preview", label: "\u547D\u4E2D\u5185\u5BB9", flex: true, renderCell: _cellText, cellClass: "content-preview", rawTitle: true } - ], - async getCount({ signal }) { - try { - const overview = await api("/api/dashboard/proactive-feedback/overview", { signal }); - return overview.total || 0; - } catch (error) { - if (signal.aborted) throw error; - return null; - } - }, - async fetchPage({ page, pageSize, signal }) { - const params = new URLSearchParams(); - params.set("page", String(page)); - params.set("page_size", String(pageSize)); - const data = await api(`/api/dashboard/proactive-feedback/events?${params.toString()}`, { signal }); - return { items: data.items || [], total: data.total || 0 }; - }, - async fetchDetail(item, { signal }) { - return api(`/api/dashboard/proactive-feedback/events/${item.id}`, { signal }); - }, - Detail: FeedbackDetail, - formatters: { - score: (value) => _score(value), - lag: (value) => _lag(value), - "mono-time": (value) => _shortTs(value) - } -}; -function activate(ctx) { - dashboardRequest = ctx.http.request; - const release = ctx.ui.inject("workbench.panels.v2", (mount) => mount.register(panel)); - return () => { - release(); - dashboardRequest = null; - }; -} -export { - activate -}; +import{createRoot as k}from"react-dom/client";import{jsx as n,jsxs as a}from"react/jsx-runtime";var o=null;async function c(e,t){if(!o)throw new Error("主动反馈工作台面板未激活");let r=await o(e,t),i=await r.json();if(!r.ok)throw new Error(String(i.detail??i.message??`HTTP ${r.status}`));return i}function m(e){return typeof e=="number"?e.toFixed(3):"-"}function x(e){let t=String(e||"");if(!t)return"-";let r=new Date(t);return Number.isNaN(r.getTime())?t:`${r.getMonth()+1}-${String(r.getDate()).padStart(2,"0")} ${String(r.getHours()).padStart(2,"0")}:${String(r.getMinutes()).padStart(2,"0")}`}function f(e){return typeof e!="number"?"-":e<60?`${e}s`:e<3600?`${Math.round(e/60)}m`:`${(e/3600).toFixed(1)}h`}function h(e){return e==="explicit_quote"?"accent":e==="topic_follow"?"success":e==="unscored"?"warning":"muted"}function g(e){let t=String(e||"");return t==="explicit_quote"?"明确引用":t==="topic_follow"?"话题延续":t==="no_topic_follow"?"未延续话题":t==="unscored"?"待评分":t||"-"}function b(e){let t=String(e||"");return t==="gold"?"金标":t==="high"?"高":t==="medium"?"中":t==="low"?"低":t||"-"}function d(e){return String(e??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function u(e){let t=String(e||"").trim();return d(t||"-")}function v(e){let t=String(e||"");return`${d(g(t))}`}function w(e){let t=String(e||"");return t==="gold"||t==="high"?"success":t==="medium"?"warning":"muted"}function y(e){let t=String(e||"-");return`${d(b(t))}`}function _(e){let t=e.item,r=e.ui.Chip;if(!t)return a("div",{className:"feedback-empty",children:[n("div",{className:"feedback-empty__title",children:"反馈详情"}),n("div",{className:"feedback-empty__text",children:"点开一条记录后,这里会显示用户回复、命中的 proactive 和助手后续回复。"})]});let i=String(t.feedback_type||"");return a("main",{className:"feedback-detail","aria-labelledby":"feedback-detail-title",children:[a("header",{className:"feedback-detail__header",children:[a("div",{children:[n("p",{children:"主动消息效果"}),n("h2",{id:"feedback-detail-title",children:"用户如何回应这次主动触达"}),n("span",{children:String(t.session_key||"未关联会话")})]}),n(r,{tone:h(i),children:g(i)})]}),a("section",{className:"feedback-summary","aria-label":"反馈判断摘要",children:[n(l,{label:"置信度",value:b(t.confidence)}),n(l,{label:"响应延迟",value:f(t.lag_seconds)}),n(l,{label:"PUA 分数",value:m(t.pua_score)})]}),a("section",{className:"feedback-timeline","aria-labelledby":"feedback-timeline-title",children:[n("h3",{id:"feedback-timeline-title",children:"对话链路"}),n(s,{index:"1",title:"主动消息",text:String(t.proactive_preview||t.quoted_preview||"")}),n(s,{index:"2",title:"用户反馈",text:String(t.user_reply_preview||t.user_preview||""),emphasis:!0}),n(s,{index:"3",title:"助手后续",text:String(t.assistant_preview||"")})]}),a("details",{className:"feedback-technical",children:[n("summary",{children:"查看匹配依据与消息标识"}),a("dl",{children:[a("div",{children:[n("dt",{children:"匹配方式"}),n("dd",{children:String(t.matched_by||"-")})]}),a("div",{children:[n("dt",{children:"用户消息 ID"}),n("dd",{children:String(t.user_message_id||"-")})]}),a("div",{children:[n("dt",{children:"主动消息 ID"}),n("dd",{children:String(t.proactive_message_id||"-")})]})]})]})]})}function l(e){return a("div",{children:[n("span",{children:e.label}),n("strong",{children:e.value})]})}function s(e){return a("div",{className:`feedback-step${e.emphasis?" is-emphasis":""}`,children:[n("span",{className:"feedback-step__index",children:e.index}),a("div",{children:[n("h4",{children:e.title}),n("p",{children:e.text||"没有可展示的内容"})]})]})}var S={id:"proactive-feedback",label:"主动反馈",viewLabel:"主动反馈",order:70,pageSize:50,rowKey:"id",countTitle(e){return`共 ${e} 条反馈`},columns:[{key:"created_at",label:"时间",width:96,fmt:"mono-time",cellClass:"mono cell-time",rawTitle:!0},{key:"feedback_type",label:"类型",width:126,renderCell:v},{key:"confidence",label:"置信度",width:84,renderCell:y},{key:"lag_seconds",label:"延迟",width:68,fmt:"lag",cellClass:"mono cell-metric",align:"right"},{key:"user_reply_preview",label:"用户回复",flex:!0,renderCell:u,cellClass:"content-preview",rawTitle:!0},{key:"proactive_preview",label:"命中内容",flex:!0,renderCell:u,cellClass:"content-preview",rawTitle:!0}],async getCount({signal:e}){try{return(await c("/api/dashboard/proactive-feedback/overview",{signal:e})).total||0}catch(t){if(e.aborted)throw t;return null}},async fetchPage({page:e,pageSize:t,signal:r}){let i=new URLSearchParams;i.set("page",String(e)),i.set("page_size",String(t));let p=await c(`/api/dashboard/proactive-feedback/events?${i.toString()}`,{signal:r});return{items:p.items||[],total:p.total||0}},async fetchDetail(e,{signal:t}){return c(`/api/dashboard/proactive-feedback/events/${e.id}`,{signal:t})},renderDetail(e,t,r){let i=k(t);return i.render(n(_,{item:e,ui:r.ui})),()=>i.unmount()},formatters:{score:e=>m(e),lag:e=>f(e),"mono-time":e=>x(e)}};function N(e){o=e.http.request;let t=e.ui.inject("workbench.panels.v2",r=>r.register(S));return()=>{t(),o=null}}export{N as activate};