>(`/api/dashboard/emotion/effects/${item.id}`);
- },
-
- Detail: EmotionDetail,
-
- formatters: {
- score: (value: unknown) => _score(value),
- delta: (value: unknown) => _delta(value),
- "mono-time": (value: unknown) => _shortTs(value),
- },
-});
diff --git a/plugin.py b/plugin.py
index 88cf96f..6860524 100644
--- a/plugin.py
+++ b/plugin.py
@@ -40,6 +40,12 @@
workspace_roots = ("emotion",)
drift_skill_roots = ("drift/skills",)
dashboard_module = "dashboard.py"
+web_module = "web_module.js"
+web_requires = ("workbench.panels.v1",)
+web_provides = ()
+web_contract_digests = {
+ "workbench.panels.v1": "724b282c22c4b3f3a36967ab664c4dfd8bce4257665f99459000306938caf527",
+}
_v3_emotion_root: Path | None = None
_v3_emotion_runtime: EmotionRuntime | None = None
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 0e3da11..c082333 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -129,6 +129,7 @@ async def _mount_candidate(
inject=module.inject,
runtime=PluginRuntime(
plugin_id="emotion",
+ generation_id=root.generation_id,
plugin_dir=Path(__file__).parents[1],
data_dir=tmp_path / "plugin-data",
workspace=emotion_root.parent,
@@ -168,8 +169,6 @@ def _before_turn(channel: str, at: datetime) -> BeforeTurnCtx:
chat_id="chat",
content="tick",
timestamp=at,
- retrieved_memory_block="",
- retrieval_trace_raw=None,
history_messages=(),
)
@@ -832,6 +831,7 @@ async def mount_emotion() -> None:
inject=module.inject,
runtime=PluginRuntime(
plugin_id="emotion",
+ generation_id=root.generation_id,
plugin_dir=Path(__file__).parents[1],
data_dir=tmp_path / "plugin-data" / "emotion",
workspace=emotion_root.parent,
diff --git a/web_module.css b/web_module.css
new file mode 100644
index 0000000..0b82c53
--- /dev/null
+++ b/web_module.css
@@ -0,0 +1,26 @@
+.emotion-workbench-panel { max-width: 1180px; margin-inline: auto; }
+.emotion-workbench-panel > header { display: flex; align-items: end; justify-content: space-between; gap: 20px; }
+.emotion-workbench-panel h1, .emotion-workbench-panel h2, .emotion-workbench-panel h3, .emotion-workbench-panel p { margin: 0; }
+.emotion-workbench-panel > header p, .emotion-panel-row span, .emotion-panel-row small, .emotion-workbench-panel article > header span { color: var(--ak-ink-secondary); }
+.emotion-workbench-panel button { min-height: 44px; }
+.emotion-overview { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; margin-block: 16px; background: var(--ak-rule-subtle); }
+.emotion-overview > div, .emotion-overview > p { margin: 0; padding: 14px; background: var(--ak-paper-quiet); }
+.emotion-overview span, .emotion-overview small { display: block; color: var(--ak-ink-secondary); font-size: 0.78rem; }
+.emotion-overview strong { display: block; margin-block: 4px; }
+.emotion-panel-grid { display: grid; grid-template-columns: minmax(260px, 0.8fr) minmax(340px, 1.2fr); gap: 24px; margin-block-start: 16px; }
+.emotion-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) { .emotion-panel-row:hover { background: var(--ak-paper-quiet); } }
+.emotion-panel-grid footer { display: flex; align-items: center; justify-content: center; gap: 12px; padding-block: 16px; }
+.emotion-panel-grid > article { min-width: 0; border-inline-start: 1px solid var(--ak-rule-subtle); padding-inline-start: 24px; }
+.emotion-panel-grid article > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; }
+.emotion-panel-grid article > header p { color: var(--ak-ink-secondary); font-size: 0.8rem; }
+.emotion-detail-metrics, .emotion-detail-coordinates { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
+.emotion-detail-metrics div, .emotion-detail-coordinates div { padding: 12px; background: var(--ak-paper-quiet); }
+.emotion-detail-metrics dt, .emotion-detail-coordinates dt { color: var(--ak-ink-secondary); font-size: 0.8rem; }
+.emotion-detail-metrics dd, .emotion-detail-coordinates dd { margin: 4px 0 0; font-family: ui-monospace, monospace; }
+.emotion-panel-grid article > section { margin-block: 18px; }
+.emotion-panel-grid details { margin-block-start: 12px; border-block-start: 1px solid var(--ak-rule-subtle); padding-block-start: 12px; }
+.emotion-panel-grid summary { min-height: 44px; cursor: pointer; }
+.emotion-panel-grid pre { overflow: auto; white-space: pre-wrap; }
+.emotion-workbench-panel :focus-visible { outline: 2px solid var(--ak-rule-focus); outline-offset: 2px; }
+@media (max-width: 820px) { .emotion-workbench-panel > header { align-items: start; flex-direction: column; } .emotion-overview, .emotion-detail-metrics, .emotion-detail-coordinates, .emotion-panel-grid { grid-template-columns: 1fr; } .emotion-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..8935bce
--- /dev/null
+++ b/web_module.js
@@ -0,0 +1,175 @@
+export function activate(ctx) {
+ return ctx.ui.inject("workbench.panels.v1", (mount) => mount.register({
+ id: "emotion-decisions",
+ label: "情绪决策",
+ order: 40,
+ render(host) {
+ const panel = document.createElement("section");
+ panel.className = "emotion-workbench-panel";
+ panel.innerHTML = ``;
+ host.replaceChildren(panel);
+ const overview = panel.querySelector("[data-overview]");
+ const refresh = panel.querySelector("[data-refresh]");
+ 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/emotion/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;
+ status.textContent = "正在读取情绪影响…";
+ try {
+ const data = await json(ctx, `/api/dashboard/emotion/effects?page=${requestedPage}&page_size=25`, 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 (effectId) => {
+ detailRequest.abort();
+ detailRequest = new AbortController();
+ const request = detailRequest;
+ detail.innerHTML = "正在读取详情…
";
+ try {
+ const item = await json(ctx, `/api/dashboard/emotion/effects/${encodeURIComponent(effectId)}`, 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();
+ });
+ 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) {
+ const state = data && typeof data.state === "object" && data.state ? data.state : null;
+ const behavior = data && typeof data.current_behavior === "object" && data.current_behavior
+ ? data.current_behavior
+ : null;
+ if (!state || !behavior) {
+ return `还没有可用的情绪状态。已记录 ${finiteNumber(data && data.effect_count)} 条情绪影响。
`;
+ }
+ return `当前语气${escapeHtml(behavior.tone_label || "未标注")}
发送阈值${escapeHtml(effectLabel(behavior.expected_effect))}${escapeHtml(deltaText(behavior.threshold_delta))}
情绪坐标愉悦 ${escapeHtml(score(state.valence))} · 唤醒 ${escapeHtml(score(state.arousal))} · 支配 ${escapeHtml(score(state.dominance))}
`;
+}
+
+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 = "emotion-panel-row";
+ button.innerHTML = `${escapeHtml(effectLabel(item.expected_effect))}${escapeHtml(shortTime(item.created_at))} · ${escapeHtml(String(item.tone_label || "未标注"))}愉悦 ${escapeHtml(score(item.valence))} · 唤醒 ${escapeHtml(score(item.arousal))} · 阈值 ${escapeHtml(deltaText(item.threshold_delta))}`;
+ button.addEventListener("click", () => void openDetail(item.id));
+ target.append(button);
+ }
+}
+
+function renderDetail(item) {
+ const delta = deltaText(item.threshold_delta);
+ return `主动决策输入
这次情绪如何改变发送阈值
${escapeHtml(String(item.tick_id || "未关联任务"))} ${escapeHtml(effectLabel(item.expected_effect))}- 原始阈值
- ${escapeHtml(score(item.base_threshold))}
- 应用情绪后
- ${escapeHtml(score(item.final_threshold))}
- 变化
- ${escapeHtml(delta)}
情绪坐标
语气:${escapeHtml(String(item.tone_label || "未标注"))}
- 愉悦度
- ${escapeHtml(score(item.valence))}
- 唤醒度
- ${escapeHtml(score(item.arousal))}
- 支配度
- ${escapeHtml(score(item.dominance))}
查看写入主动流程的提示词
${escapeHtml(String(item.prompt_section || "-"))}查看技术元数据
${escapeHtml(JSON.stringify(item.metadata || {}, null, 2))} `;
+}
+
+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 effectLabel(value) {
+ return ({raise_send_bar: "提高发送阈值", lower_send_bar: "降低发送阈值"})[value] || String(value || "-");
+}
+
+function score(value) {
+ const number = Number(value);
+ return Number.isFinite(number) ? number.toFixed(3) : "-";
+}
+
+function deltaText(value) {
+ const number = Number(value);
+ if (!Number.isFinite(number)) return "-";
+ return number > 0 ? `+${number.toFixed(3)}` : number.toFixed(3);
+}
+
+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 d4589dbd56aa420a17eaa7011f02f827cfe05c4a Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 08:35:19 +0800
Subject: [PATCH 3/7] 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 b4ae1de..d1e0bcf 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: 9da3a988a2bf62b0f550bd4f6bb98c4eeb1f56f5
+ ref: 14221ff778d660ab0e9c9249a8a8ff772861dbe9
path: .akashic-core
- uses: actions/checkout@v4
with:
From 282159cd31f1395474de08d593cd546290cf7f22 Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 08:41:49 +0800
Subject: [PATCH 4/7] 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 d1e0bcf..1ff50b4 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/checkout@v4
with:
From 4519d717baa3ae037c2129994ffe42bb4ac08a1b Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 08:47:28 +0800
Subject: [PATCH 5/7] 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 1ff50b4..d4119e5 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/checkout@v4
with:
From 800544b8a8b2053ce96ddb505cc808f59b3a957e Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 09:00:07 +0800
Subject: [PATCH 6/7] 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 d4119e5..796f4dc 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/checkout@v4
with:
From d85dfdafef21c46759fdb367e1113f5a7277f381 Mon Sep 17 00:00:00 2001
From: huashen <2494946808@qq.com>
Date: Sun, 30 Aug 2026 09:55:14 +0800
Subject: [PATCH 7/7] fix drift fixture return type
---
tests/test_plugin.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index c082333..8cb9084 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -38,7 +38,7 @@
)
from agent.plugin_composition.ui_slots import PluginUiSlots
from bus.events_lifecycle import TurnCommitted
-from plugins.drift.store import DriftStore
+from plugins.drift.store import DriftSelectionReceipt, DriftStore
NOW = datetime(2026, 8, 23, 8, tzinfo=UTC)
@@ -96,7 +96,9 @@ def __init__(self, path: Path) -> None:
def propose(self, *args: object, **kwargs: object) -> dict[str, object]:
return self.store.propose(*args, **kwargs) # pyright: ignore[reportArgumentType]
- def selection(self, accepted_turn: dict[str, object]) -> dict[str, object] | None:
+ def selection(
+ self, accepted_turn: dict[str, object]
+ ) -> DriftSelectionReceipt | None:
return self.store.selection(accepted_turn)