From ab66d3573ecb0aa4839ca5bfe283a6c5bd0f97eb Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sat, 29 Aug 2026 02:19:46 +0800 Subject: [PATCH 1/7] fix: keep drift proposal replay identity stable --- akashic.plugin.toml | 2 +- db.py | 4 +++- plugin.py | 2 +- runtime.py | 5 +++-- tests/test_plugin.py | 27 +++++++++++++++++++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/akashic.plugin.toml b/akashic.plugin.toml index 56d0962..572c99e 100644 --- a/akashic.plugin.toml +++ b/akashic.plugin.toml @@ -1,5 +1,5 @@ schema_version = 1 name = "emotion" -version = "3.0.2" +version = "3.0.3" api_version = 3 entrypoint = "plugin.py" diff --git a/db.py b/db.py index 3e20679..2630ced 100644 --- a/db.py +++ b/db.py @@ -449,7 +449,7 @@ def prepare_drift_proposal( # 1. Replay a locally prepared/submitted proposal before admitting new evidence. existing = conn.execute( """ - SELECT proposal_id, revision, payload_json + SELECT proposal_id, revision, payload_json, created_at FROM emotion_drift_runs WHERE status IN ('prepared', 'submitted') ORDER BY created_at, proposal_id, revision LIMIT 1 @@ -546,6 +546,7 @@ def prepare_drift_proposal( "proposal_id": proposal_id, "revision": revision, "payload": payload, + "due_at": _aware_datetime(now), } @@ -722,6 +723,7 @@ def _drift_proposal_from_row(row: sqlite3.Row) -> dict[str, object]: "proposal_id": str(row["proposal_id"]), "revision": str(row["revision"]), "payload": payload, + "due_at": _aware_datetime(datetime.fromisoformat(str(row["created_at"]))), } diff --git a/plugin.py b/plugin.py index 2ba01ee..88cf96f 100644 --- a/plugin.py +++ b/plugin.py @@ -32,7 +32,7 @@ api_version = 3 name = "emotion" -version = "3.0.2" +version = "3.0.3" desc = "Timer-refreshed Emotion context and ordinary Drift preference projection." DRIFT_PROPOSALS = ServiceKey[DriftProposalServices]("drift.proposals.v1") DRIFT_WAKE = ServiceKey[DriftWakeServices]("drift.wake.v1") diff --git a/runtime.py b/runtime.py index 1aac0c8..ee7ef1a 100644 --- a/runtime.py +++ b/runtime.py @@ -112,12 +112,13 @@ async def tick_once(self) -> None: proposal_id = cast(str, proposal["proposal_id"]) revision = cast(str, proposal["revision"]) payload = cast(Mapping[str, object], proposal["payload"]) + due_at = cast(datetime, proposal["due_at"]) _ = self._proposals.propose( proposal_id, revision, payload, - now, - next_due=now + _REFRESH_INTERVAL, + due_at, + next_due=due_at + _REFRESH_INTERVAL, ) db = open_db(self._db_path) try: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e5e31d5..0e3da11 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -278,6 +278,33 @@ async def test_empty_tick_overwrites_current_without_appending_history(tmp_path: conn.close() +@pytest.mark.asyncio +async def test_submitted_proposal_replay_keeps_original_due_time(tmp_path: Path) -> None: + emotion_root = tmp_path / "emotion" + module._on_turn_committed(_feedback_turn(), root=emotion_root) + drift = DriftServices(tmp_path / "drift.sqlite3") + clock = [NOW] + runtime = module.EmotionRuntime( + cast(Any, object()), + emotion_root, + PluginTimers.candidate_validation(), + drift, + drift, + now=lambda: clock[0], + ) + + await runtime.tick_once() + clock[0] += timedelta(minutes=5) + await runtime.tick_once() + + proposals = cast( + tuple[dict[str, Any], ...], + drift.store.snapshot(clock[0])["proposals"], + ) + assert len(proposals) == 1 + assert proposals[0]["due_at"] == NOW.isoformat() + + def _create_formal_legacy_fixture(path: Path) -> None: """Create the exact original three-table formal schema without new migration code.""" From 36212bb2bbfc99811cc3e54c92e45b54fcc97996 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 07:41:41 +0800 Subject: [PATCH 2/7] feat: register emotion workbench panel --- dashboard_panel.css | 166 ---------------------------------------- dashboard_panel.js | 147 ------------------------------------ dashboard_panel.tsx | 166 ---------------------------------------- plugin.py | 6 ++ tests/test_plugin.py | 4 +- web_module.css | 26 +++++++ web_module.js | 175 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 209 insertions(+), 481 deletions(-) delete mode 100644 dashboard_panel.css delete mode 100644 dashboard_panel.js delete mode 100644 dashboard_panel.tsx create mode 100644 web_module.css create mode 100644 web_module.js diff --git a/dashboard_panel.css b/dashboard_panel.css deleted file mode 100644 index ca13718..0000000 --- a/dashboard_panel.css +++ /dev/null @@ -1,166 +0,0 @@ -.emotion-detail { - display: grid; - gap: 18px; - padding: 24px; - color: var(--ak-color-text-primary); -} - -.emotion-detail__header, -.emotion-section-heading { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; -} - -.emotion-detail__header p, -.emotion-section-heading p { - margin: 0 0 4px; - color: var(--ak-color-action-primary); - font-size: 11px; - font-weight: 700; - letter-spacing: 0.08em; -} - -.emotion-detail__header h2, -.emotion-section-heading h3 { - margin: 0; - letter-spacing: -0.02em; -} - -.emotion-detail__header h2 { font-size: 20px; } -.emotion-section-heading h3 { font-size: 16px; } - -.emotion-detail__header span, -.emotion-section-heading > span { - color: var(--ak-color-text-muted); - font-size: 12px; -} - -.emotion-detail__header > div > span { - display: block; - margin-block-start: 5px; -} - -.emotion-threshold { - display: grid; - grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(92px, auto); - align-items: center; - gap: 14px; - padding: 16px; - border: 1px solid var(--ak-color-border-default); - border-radius: 7px; - background: var(--ak-color-bg-surface-low); -} - -.emotion-threshold > div { - display: grid; - gap: 4px; -} - -.emotion-threshold span { color: var(--ak-color-text-muted); font-size: 11px; } -.emotion-threshold strong { font-family: var(--mono); font-size: 22px; } -.emotion-threshold__arrow { font-size: 20px !important; } - -.emotion-threshold__delta { - padding-inline-start: 14px; - border-inline-start: 1px solid var(--ak-color-border-default); -} - -.emotion-threshold__delta.is-up strong { color: var(--ak-color-status-warning); } -.emotion-threshold__delta.is-down strong { color: var(--ak-color-status-success); } - -.emotion-coordinates { - display: grid; - gap: 16px; - padding-block: 4px; -} - -.emotion-gauge { - display: grid; - grid-template-columns: 88px minmax(0, 1fr); - gap: 4px 14px; - align-items: center; -} - -.emotion-gauge__label { - display: flex; - justify-content: space-between; - gap: 8px; - font-size: 12px; -} - -.emotion-gauge__label code { color: var(--ak-color-text-muted); } - -.emotion-gauge__track { - position: relative; - height: 6px; - border-radius: 999px; - background: linear-gradient(90deg, var(--ak-color-bg-surface-high), var(--ak-color-action-soft)); -} - -.emotion-gauge__track::after { - position: absolute; - inset-block: -3px; - left: 50%; - width: 1px; - content: ""; - background: var(--ak-color-border-strong); -} - -.emotion-gauge__track i { - position: absolute; - top: 50%; - width: 12px; - height: 12px; - border: 2px solid var(--ak-color-bg-canvas); - border-radius: 50%; - background: var(--ak-color-action-primary); - box-shadow: 0 0 0 1px var(--ak-color-action-primary); - transform: translate(-50%, -50%); -} - -.emotion-gauge__ends { - display: flex; - grid-column: 2; - justify-content: space-between; - color: var(--ak-color-text-muted); - font-size: 10px; -} - -.emotion-disclosure { - border-block-start: 1px solid var(--ak-color-border-default); -} - -.emotion-disclosure summary { - padding: 14px 2px; - color: var(--ak-color-text-primary); - font-size: 13px; - font-weight: 650; - cursor: pointer; -} - -.emotion-disclosure summary:focus-visible { - outline: 2px solid var(--ak-color-action-primary); - outline-offset: 2px; -} - -.emotion-disclosure pre { - overflow: auto; - max-height: 320px; - margin: 0 0 14px; - padding: 14px; - border-radius: 5px; - color: var(--ak-color-text-secondary); - background: var(--ak-color-bg-surface-low); - font: 12px/1.65 var(--mono); - white-space: pre-wrap; -} - -@media (max-width: 720px) { - .emotion-detail { padding: 18px; } - .emotion-detail__header { align-items: flex-start; flex-direction: column; } - .emotion-threshold { grid-template-columns: 1fr auto 1fr; } - .emotion-threshold__delta { grid-column: 1 / -1; padding: 12px 0 0; border-inline-start: 0; border-block-start: 1px solid var(--ak-color-border-default); } - .emotion-gauge { grid-template-columns: 78px minmax(0, 1fr); } -} diff --git a/dashboard_panel.js b/dashboard_panel.js deleted file mode 100644 index fb40852..0000000 --- a/dashboard_panel.js +++ /dev/null @@ -1,147 +0,0 @@ -// ../emotion/dashboard_panel.tsx -import { Chip, api } from "@akashic/dashboard-ui"; -import { jsx, jsxs } from "react/jsx-runtime"; -function _score(value) { - return typeof value === "number" ? value.toFixed(3) : "-"; -} -function _delta(value) { - if (typeof value !== "number") return "-"; - return value > 0 ? `+${value.toFixed(3)}` : 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 _escape(value) { - return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); -} -function _effectLabel(value) { - const effect = String(value || ""); - if (effect === "raise_send_bar") return "\u63D0\u9AD8\u53D1\u9001\u9608\u503C"; - if (effect === "lower_send_bar") return "\u964D\u4F4E\u53D1\u9001\u9608\u503C"; - return effect || "-"; -} -function _toneCell(value) { - const text = String(value || "-"); - const tone = text === "raise_send_bar" ? "warning" : text === "lower_send_bar" ? "success" : "muted"; - return `${_escape(_effectLabel(text))}`; -} -function EmotionDetail(props) { - const item = props.item; - if (!item) { - return /* @__PURE__ */ jsxs("div", { className: "detail-empty", children: [ - /* @__PURE__ */ jsx("div", { className: "detail-empty-title", children: "\u60C5\u7EEA\u5F71\u54CD\u8BE6\u60C5" }), - /* @__PURE__ */ jsx("div", { className: "detail-empty-text", children: "\u9009\u62E9\u4E00\u6761\u8BB0\u5F55\uFF0C\u67E5\u770B\u8FD9\u6B21\u4E3B\u52A8\u4EFB\u52A1\u7684\u60C5\u7EEA\u5F71\u54CD\u3002" }) - ] }); - } - const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null; - return /* @__PURE__ */ jsxs("main", { className: "emotion-detail", "aria-labelledby": "emotion-detail-title", children: [ - /* @__PURE__ */ jsxs("header", { className: "emotion-detail__header", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("p", { children: "\u4E3B\u52A8\u51B3\u7B56\u8F93\u5165" }), - /* @__PURE__ */ jsx("h2", { id: "emotion-detail-title", children: "\u8FD9\u6B21\u60C5\u7EEA\u5982\u4F55\u6539\u53D8\u53D1\u9001\u9608\u503C" }), - /* @__PURE__ */ jsx("span", { children: String(item.tick_id || "\u672A\u5173\u8054\u4EFB\u52A1") }) - ] }), - /* @__PURE__ */ jsx(Chip, { tone: String(item.expected_effect) === "raise_send_bar" ? "warning" : "success", children: _effectLabel(item.expected_effect) }) - ] }), - /* @__PURE__ */ jsxs("section", { className: "emotion-threshold", "aria-label": "\u9608\u503C\u53D8\u5316", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("span", { children: "\u539F\u59CB\u9608\u503C" }), - /* @__PURE__ */ jsx("strong", { children: _score(item.base_threshold) }) - ] }), - /* @__PURE__ */ jsx("span", { className: "emotion-threshold__arrow", "aria-hidden": "true", children: "\u2192" }), - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("span", { children: "\u5E94\u7528\u60C5\u7EEA\u540E" }), - /* @__PURE__ */ jsx("strong", { children: _score(item.final_threshold) }) - ] }), - /* @__PURE__ */ jsxs("div", { className: `emotion-threshold__delta${delta !== null && delta > 0 ? " is-up" : " is-down"}`, children: [ - /* @__PURE__ */ jsx("span", { children: "\u53D8\u5316" }), - /* @__PURE__ */ jsx("strong", { children: _delta(delta) }) - ] }) - ] }), - /* @__PURE__ */ jsxs("section", { className: "emotion-coordinates", "aria-labelledby": "emotion-coordinates-title", children: [ - /* @__PURE__ */ jsxs("div", { className: "emotion-section-heading", children: [ - /* @__PURE__ */ jsxs("div", { children: [ - /* @__PURE__ */ jsx("p", { children: "VAD \u6A21\u578B" }), - /* @__PURE__ */ jsx("h3", { id: "emotion-coordinates-title", children: "\u60C5\u7EEA\u5750\u6807" }) - ] }), - /* @__PURE__ */ jsxs("span", { children: [ - "\u8BED\u6C14\uFF1A", - String(item.tone_label || "\u672A\u6807\u6CE8") - ] }) - ] }), - /* @__PURE__ */ jsx(VadGauge, { label: "\u6109\u60A6\u5EA6", low: "\u6D88\u6781", high: "\u79EF\u6781", value: item.valence }), - /* @__PURE__ */ jsx(VadGauge, { label: "\u5524\u9192\u5EA6", low: "\u5E73\u9759", high: "\u6FC0\u6D3B", value: item.arousal }), - /* @__PURE__ */ jsx(VadGauge, { label: "\u652F\u914D\u5EA6", low: "\u53D7\u63A7", high: "\u4E3B\u5BFC", value: item.dominance }) - ] }), - /* @__PURE__ */ jsx(TextDisclosure, { title: "\u67E5\u770B\u5199\u5165\u4E3B\u52A8\u6D41\u7A0B\u7684\u63D0\u793A\u8BCD", text: String(item.prompt_section || "") }), - /* @__PURE__ */ jsx(TextDisclosure, { title: "\u67E5\u770B\u6280\u672F\u5143\u6570\u636E", text: JSON.stringify(item.metadata || {}, null, 2) }) - ] }); -} -function VadGauge(props) { - const numeric = typeof props.value === "number" ? props.value : 0; - const position = Math.max(0, Math.min(100, (numeric + 1) / 2 * 100)); - return /* @__PURE__ */ jsxs("div", { className: "emotion-gauge", children: [ - /* @__PURE__ */ jsxs("div", { className: "emotion-gauge__label", children: [ - /* @__PURE__ */ jsx("strong", { children: props.label }), - /* @__PURE__ */ jsx("code", { children: _score(props.value) }) - ] }), - /* @__PURE__ */ jsx("div", { className: "emotion-gauge__track", "aria-hidden": "true", children: /* @__PURE__ */ jsx("i", { style: { left: `${position}%` } }) }), - /* @__PURE__ */ jsxs("div", { className: "emotion-gauge__ends", children: [ - /* @__PURE__ */ jsx("span", { children: props.low }), - /* @__PURE__ */ jsx("span", { children: props.high }) - ] }) - ] }); -} -function TextDisclosure(props) { - return /* @__PURE__ */ jsxs("details", { className: "emotion-disclosure", children: [ - /* @__PURE__ */ jsx("summary", { children: props.title }), - /* @__PURE__ */ jsx("pre", { children: props.text || "-" }) - ] }); -} -window.AkashicDashboard.registerPlugin({ - id: "emotion", - label: "\u60C5\u7EEA\u51B3\u7B56", - viewLabel: "\u60C5\u7EEA\u51B3\u7B56", - pageSize: 50, - rowKey: "id", - countTitle(total) { - return `\u5171 ${total} \u6761\u60C5\u7EEA\u5F71\u54CD`; - }, - columns: [ - { key: "created_at", label: "\u65F6\u95F4", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, - { key: "expected_effect", label: "\u5F71\u54CD", width: 132, renderCell: _toneCell }, - { key: "tone_label", label: "\u8BED\u6C14", width: 112 }, - { key: "valence", label: "\u6109\u60A6", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" }, - { key: "arousal", label: "\u5524\u9192", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" }, - { key: "threshold_delta", label: "\u9608\u503C\u53D8\u5316", width: 82, fmt: "delta", cellClass: "mono cell-metric", align: "right" }, - { key: "tick_id", label: "\u4EFB\u52A1", flex: true, cellClass: "mono content-preview", rawTitle: true } - ], - async getCount() { - try { - const overview = await api("/api/dashboard/emotion/overview"); - return overview.effect_count || 0; - } catch { - return null; - } - }, - async fetchPage({ page, pageSize }) { - const params = new URLSearchParams(); - params.set("page", String(page)); - params.set("page_size", String(pageSize)); - const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`); - return { items: data.items || [], total: data.total || 0 }; - }, - async fetchDetail(item) { - return api(`/api/dashboard/emotion/effects/${item.id}`); - }, - Detail: EmotionDetail, - formatters: { - score: (value) => _score(value), - delta: (value) => _delta(value), - "mono-time": (value) => _shortTs(value) - } -}); diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx deleted file mode 100644 index c0e7a43..0000000 --- a/dashboard_panel.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/// -import { type ReactElement } from "react"; -import { Chip, api } from "@akashic/dashboard-ui"; - -interface Overview { - state: Record | null; - effect_count: number; -} - -interface FetchPage { - items: Record[]; - total: number; -} - -function _score(value: unknown): string { - return typeof value === "number" ? value.toFixed(3) : "-"; -} - -function _delta(value: unknown): string { - if (typeof value !== "number") return "-"; - return value > 0 ? `+${value.toFixed(3)}` : 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 _escape(value: unknown): string { - return String(value ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -function _effectLabel(value: unknown): string { - const effect = String(value || ""); - if (effect === "raise_send_bar") return "提高发送阈值"; - if (effect === "lower_send_bar") return "降低发送阈值"; - return effect || "-"; -} - -function _toneCell(value: unknown): string { - const text = String(value || "-"); - const tone = text === "raise_send_bar" ? "warning" : text === "lower_send_bar" ? "success" : "muted"; - return `${_escape(_effectLabel(text))}`; -} - -function EmotionDetail(props: { item: Record | null }): ReactElement { - const item = props.item; - if (!item) { - return
情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
; - } - const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null; - return ( -
-
-
-

主动决策输入

-

这次情绪如何改变发送阈值

- {String(item.tick_id || "未关联任务")} -
- {_effectLabel(item.expected_effect)} -
- -
-
原始阈值{_score(item.base_threshold)}
- -
应用情绪后{_score(item.final_threshold)}
-
0 ? " is-up" : " is-down"}`}> - 变化{_delta(delta)} -
-
- -
-
-

VAD 模型

情绪坐标

- 语气:{String(item.tone_label || "未标注")} -
- - - -
- - - -
- ); -} - -function VadGauge(props: { label: string; low: string; high: string; value: unknown }): ReactElement { - const numeric = typeof props.value === "number" ? props.value : 0; - const position = Math.max(0, Math.min(100, ((numeric + 1) / 2) * 100)); - return ( -
-
{props.label}{_score(props.value)}
- -
{props.low}{props.high}
-
- ); -} - -function TextDisclosure(props: { title: string; text: string }): ReactElement { - return ( -
- {props.title} -
{props.text || "-"}
-
- ); -} - -window.AkashicDashboard.registerPlugin({ - id: "emotion", - label: "情绪决策", - viewLabel: "情绪决策", - pageSize: 50, - rowKey: "id", - - countTitle(total: number): string { - return `共 ${total} 条情绪影响`; - }, - - columns: [ - { key: "created_at", label: "时间", width: 96, fmt: "mono-time", cellClass: "mono cell-time", rawTitle: true }, - { key: "expected_effect", label: "影响", width: 132, renderCell: _toneCell }, - { key: "tone_label", label: "语气", width: 112 }, - { key: "valence", label: "愉悦", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" }, - { key: "arousal", label: "唤醒", width: 66, fmt: "score", cellClass: "mono cell-metric", align: "right" }, - { key: "threshold_delta", label: "阈值变化", width: 82, fmt: "delta", cellClass: "mono cell-metric", align: "right" }, - { key: "tick_id", label: "任务", flex: true, cellClass: "mono content-preview", rawTitle: true }, - ], - - async getCount(): Promise { - try { - const overview = await api("/api/dashboard/emotion/overview"); - return overview.effect_count || 0; - } catch { - return null; - } - }, - - async fetchPage({ page, pageSize }: { page: number; pageSize: number }) { - const params = new URLSearchParams(); - params.set("page", String(page)); - params.set("page_size", String(pageSize)); - const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`); - return { items: data.items || [], total: data.total || 0 }; - }, - - async fetchDetail(item: Record) { - return api>(`/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 = `

情绪决策

查看 Emotion 如何改变主动发送的语气与阈值。

正在读取当前状态…

选择一条情绪影响查看详情。

`; + 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)