>(`/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};