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/9] 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/9] 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/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 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/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 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/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 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/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 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 60c7a216b82857ae5c6a8f0d3f116acb22cf70a2 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 19:17:44 +0800 Subject: [PATCH 7/9] fix(web-ui): compose emotion panel through workbench --- akashic.plugin.toml | 2 +- dashboard_panel.css | 185 +++++++++++++++++++++++++ dashboard_panel.tsx | 189 ++++++++++++++++++++++++++ plugin.py | 6 +- web_module.css | 221 ++++++++++++++++++++++++++---- web_module.js | 321 ++++++++++++++++++++++---------------------- 6 files changed, 730 insertions(+), 194 deletions(-) create mode 100644 dashboard_panel.css create mode 100644 dashboard_panel.tsx diff --git a/akashic.plugin.toml b/akashic.plugin.toml index 572c99e..d6cba39 100644 --- a/akashic.plugin.toml +++ b/akashic.plugin.toml @@ -1,5 +1,5 @@ schema_version = 1 name = "emotion" -version = "3.0.3" +version = "3.0.4" api_version = 3 entrypoint = "plugin.py" diff --git a/dashboard_panel.css b/dashboard_panel.css new file mode 100644 index 0000000..5c7a0cc --- /dev/null +++ b/dashboard_panel.css @@ -0,0 +1,185 @@ +.emotion-empty { + padding: 24px 16px; +} + +.emotion-empty__title { + color: var(--ak-color-text-primary); + font-size: 20px; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.25; +} + +.emotion-empty__text { + margin-block-start: 8px; + color: var(--ak-color-text-secondary); + font-size: 13px; + line-height: 1.625; +} + +.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.tsx b/dashboard_panel.tsx new file mode 100644 index 0000000..ee0a74d --- /dev/null +++ b/dashboard_panel.tsx @@ -0,0 +1,189 @@ +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("Emotion 工作台面板未激活"); + 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 { + 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 || "-"}
+
+ ); +} + +const panel = { + id: "emotion", + label: "情绪决策", + viewLabel: "情绪决策", + order: 30, + 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({ signal }: { signal: AbortSignal }): Promise { + try { + const overview = await api("/api/dashboard/emotion/overview", { signal }); + return overview.effect_count || 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/emotion/effects?${params.toString()}`, { signal }); + return { items: data.items || [], total: data.total || 0 }; + }, + + async fetchDetail(item: Record, { signal }: { signal: AbortSignal }) { + return api>(`/api/dashboard/emotion/effects/${item.id}`, { signal }); + }, + + Detail: EmotionDetail, + + formatters: { + score: (value: unknown) => _score(value), + delta: (value: unknown) => _delta(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 6860524..8f54ee3 100644 --- a/plugin.py +++ b/plugin.py @@ -32,7 +32,7 @@ api_version = 3 name = "emotion" -version = "3.0.3" +version = "3.0.4" desc = "Timer-refreshed Emotion context and ordinary Drift preference projection." DRIFT_PROPOSALS = ServiceKey[DriftProposalServices]("drift.proposals.v1") DRIFT_WAKE = ServiceKey[DriftWakeServices]("drift.wake.v1") @@ -41,10 +41,10 @@ drift_skill_roots = ("drift/skills",) 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", } _v3_emotion_root: Path | None = None _v3_emotion_runtime: EmotionRuntime | None = None diff --git a/web_module.css b/web_module.css index 0b82c53..a36fc0d 100644 --- a/web_module.css +++ b/web_module.css @@ -1,26 +1,195 @@ -.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; } } +/* dashboard_panel.css */ +.emotion-empty { + padding: 24px 16px; +} +.emotion-empty__title { + color: var(--ak-color-text-primary); + font-size: 20px; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.25; +} +.emotion-empty__text { + margin-block-start: 8px; + color: var(--ak-color-text-secondary); + font-size: 13px; + line-height: 1.625; +} +.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/web_module.js b/web_module.js index 8935bce..ba39a0b 100644 --- a/web_module.js +++ b/web_module.js @@ -1,175 +1,168 @@ -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))}
`; +// 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("Emotion \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 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 _score(value) { + return typeof value === "number" ? value.toFixed(3) : "-"; } - -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))}
`; +function _delta(value) { + if (typeof value !== "number") return "-"; + return value > 0 ? `+${value.toFixed(3)}` : value.toFixed(3); } - -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 _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 effectLabel(value) { - return ({raise_send_bar: "提高发送阈值", lower_send_bar: "降低发送阈值"})[value] || String(value || "-"); +function _escape(value) { + return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); } - -function score(value) { - const number = Number(value); - return Number.isFinite(number) ? number.toFixed(3) : "-"; +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 deltaText(value) { - const number = Number(value); - if (!Number.isFinite(number)) return "-"; - return number > 0 ? `+${number.toFixed(3)}` : number.toFixed(3); +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 finiteNumber(value) { - const number = Number(value); - return Number.isFinite(number) ? number : 0; +function EmotionDetail(props) { + const item = props.item; + if (!item) { + return /* @__PURE__ */ jsxs("div", { className: "emotion-empty", children: [ + /* @__PURE__ */ jsx("div", { className: "emotion-empty__title", children: "\u60C5\u7EEA\u5F71\u54CD\u8BE6\u60C5" }), + /* @__PURE__ */ jsx("div", { className: "emotion-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 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 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 showError(target, reason) { - target.setAttribute("role", "alert"); - target.textContent = reason instanceof Error ? reason.message : String(reason); +function TextDisclosure(props) { + return /* @__PURE__ */ jsxs("details", { className: "emotion-disclosure", children: [ + /* @__PURE__ */ jsx("summary", { children: props.title }), + /* @__PURE__ */ jsx("pre", { children: props.text || "-" }) + ] }); } - -function escapeHtml(value) { - return String(value).replace(/[&<>"']/g, (character) => ({"&": "&", "<": "<", ">": ">", '"': """, "'": "'"})[character]); +var panel = { + id: "emotion", + label: "\u60C5\u7EEA\u51B3\u7B56", + viewLabel: "\u60C5\u7EEA\u51B3\u7B56", + order: 30, + 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({ signal }) { + try { + const overview = await api("/api/dashboard/emotion/overview", { signal }); + return overview.effect_count || 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/emotion/effects?${params.toString()}`, { signal }); + return { items: data.items || [], total: data.total || 0 }; + }, + async fetchDetail(item, { signal }) { + return api(`/api/dashboard/emotion/effects/${item.id}`, { signal }); + }, + Detail: EmotionDetail, + formatters: { + score: (value) => _score(value), + delta: (value) => _delta(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 946d005826b70bf452a2935e4399b43979d484fa Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 19:50:31 +0800 Subject: [PATCH 8/9] test: sync Web UI Core contract --- .github/workflows/plugin-api-v3.yml | 2 +- tests/test_plugin.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index 796f4dc..e626b76 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/checkout@v4 with: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index c082333..ae01188 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -9,6 +9,7 @@ import sqlite3 import sys import warnings +from collections.abc import Mapping from contextlib import closing from datetime import UTC, datetime, timedelta from pathlib import Path @@ -96,7 +97,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] + ) -> Mapping[str, object] | None: return self.store.selection(accepted_turn) From 5d4ea418900317cd68b5821f39bf2aeec8911aea Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Sun, 30 Aug 2026 22:21:57 +0800 Subject: [PATCH 9/9] refactor(web-ui): adopt workbench mount lifecycle --- .github/workflows/plugin-api-v3.yml | 2 +- dashboard_panel.tsx | 15 ++- plugin.py | 2 +- web_module.css | 196 +--------------------------- web_module.js | 169 +----------------------- 5 files changed, 14 insertions(+), 370 deletions(-) diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml index e626b76..9b90101 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/checkout@v4 with: diff --git a/dashboard_panel.tsx b/dashboard_panel.tsx index ee0a74d..8c0f580 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; @@ -60,11 +60,12 @@ function _effectLabel(value: unknown): string { 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))}`; + return `${_escape(_effectLabel(text))}`; } -function EmotionDetail(props: { item: Record | null }): ReactElement { +function EmotionDetail(props: { item: Record | null; ui: WorkbenchUi }): ReactElement { const item = props.item; + const Chip = props.ui.Chip; if (!item) { return
情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
; } @@ -170,7 +171,11 @@ const panel = { return api>(`/api/dashboard/emotion/effects/${item.id}`, { signal }); }, - Detail: EmotionDetail, + 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 8f54ee3..a46d6db 100644 --- a/plugin.py +++ b/plugin.py @@ -44,7 +44,7 @@ web_requires = ("workbench.panels.v2",) web_provides = () web_contract_digests = { - "workbench.panels.v2": "17a005a381b362ae25a0499dbf95bf7a2c3ff0bb4e9b415e7357db458de6b5db", + "workbench.panels.v2": "fb6417c9bf532c1fdb344767d06065d5d3293da85deb64eff1e8088889a33bcb", } _v3_emotion_root: Path | None = None _v3_emotion_runtime: EmotionRuntime | None = None diff --git a/web_module.css b/web_module.css index a36fc0d..08a03da 100644 --- a/web_module.css +++ b/web_module.css @@ -1,195 +1 @@ -/* dashboard_panel.css */ -.emotion-empty { - padding: 24px 16px; -} -.emotion-empty__title { - color: var(--ak-color-text-primary); - font-size: 20px; - font-weight: 600; - letter-spacing: -0.025em; - line-height: 1.25; -} -.emotion-empty__text { - margin-block-start: 8px; - color: var(--ak-color-text-secondary); - font-size: 13px; - line-height: 1.625; -} -.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); - } -} +.emotion-empty{padding:24px 16px}.emotion-empty__title{color:var(--ak-color-text-primary);font-size:20px;font-weight:600;letter-spacing:-.025em;line-height:1.25}.emotion-empty__text{margin-block-start:8px;color:var(--ak-color-text-secondary);font-size:13px;line-height:1.625}.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:.08em}.emotion-detail__header h2,.emotion-section-heading h3{margin:0;letter-spacing:-.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/web_module.js b/web_module.js index ba39a0b..95e1371 100644 --- a/web_module.js +++ b/web_module.js @@ -1,168 +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("Emotion \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 _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: "emotion-empty", children: [ - /* @__PURE__ */ jsx("div", { className: "emotion-empty__title", children: "\u60C5\u7EEA\u5F71\u54CD\u8BE6\u60C5" }), - /* @__PURE__ */ jsx("div", { className: "emotion-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 || "-" }) - ] }); -} -var panel = { - id: "emotion", - label: "\u60C5\u7EEA\u51B3\u7B56", - viewLabel: "\u60C5\u7EEA\u51B3\u7B56", - order: 30, - 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({ signal }) { - try { - const overview = await api("/api/dashboard/emotion/overview", { signal }); - return overview.effect_count || 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/emotion/effects?${params.toString()}`, { signal }); - return { items: data.items || [], total: data.total || 0 }; - }, - async fetchDetail(item, { signal }) { - return api(`/api/dashboard/emotion/effects/${item.id}`, { signal }); - }, - Detail: EmotionDetail, - formatters: { - score: (value) => _score(value), - delta: (value) => _delta(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 g}from"react-dom/client";import{jsx as o,jsxs as i}from"react/jsx-runtime";var r=null;async function s(e,t){if(!r)throw new Error("Emotion 工作台面板未激活");let n=await r(e,t),a=await n.json();if(!n.ok)throw new Error(String(a.detail??a.message??`HTTP ${n.status}`));return a}function l(e){return typeof e=="number"?e.toFixed(3):"-"}function p(e){return typeof e!="number"?"-":e>0?`+${e.toFixed(3)}`:e.toFixed(3)}function h(e){let t=String(e||"");if(!t)return"-";let n=new Date(t);return Number.isNaN(n.getTime())?t:`${n.getMonth()+1}-${String(n.getDate()).padStart(2,"0")} ${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}`}function f(e){return String(e??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function u(e){let t=String(e||"");return t==="raise_send_bar"?"提高发送阈值":t==="lower_send_bar"?"降低发送阈值":t||"-"}function b(e){let t=String(e||"-");return`${f(u(t))}`}function x(e){let t=e.item,n=e.ui.Chip;if(!t)return i("div",{className:"emotion-empty",children:[o("div",{className:"emotion-empty__title",children:"情绪影响详情"}),o("div",{className:"emotion-empty__text",children:"选择一条记录,查看这次主动任务的情绪影响。"})]});let a=typeof t.threshold_delta=="number"?t.threshold_delta:null;return i("main",{className:"emotion-detail","aria-labelledby":"emotion-detail-title",children:[i("header",{className:"emotion-detail__header",children:[i("div",{children:[o("p",{children:"主动决策输入"}),o("h2",{id:"emotion-detail-title",children:"这次情绪如何改变发送阈值"}),o("span",{children:String(t.tick_id||"未关联任务")})]}),o(n,{tone:String(t.expected_effect)==="raise_send_bar"?"warning":"success",children:u(t.expected_effect)})]}),i("section",{className:"emotion-threshold","aria-label":"阈值变化",children:[i("div",{children:[o("span",{children:"原始阈值"}),o("strong",{children:l(t.base_threshold)})]}),o("span",{className:"emotion-threshold__arrow","aria-hidden":"true",children:"→"}),i("div",{children:[o("span",{children:"应用情绪后"}),o("strong",{children:l(t.final_threshold)})]}),i("div",{className:`emotion-threshold__delta${a!==null&&a>0?" is-up":" is-down"}`,children:[o("span",{children:"变化"}),o("strong",{children:p(a)})]})]}),i("section",{className:"emotion-coordinates","aria-labelledby":"emotion-coordinates-title",children:[i("div",{className:"emotion-section-heading",children:[i("div",{children:[o("p",{children:"VAD 模型"}),o("h3",{id:"emotion-coordinates-title",children:"情绪坐标"})]}),i("span",{children:["语气:",String(t.tone_label||"未标注")]})]}),o(d,{label:"愉悦度",low:"消极",high:"积极",value:t.valence}),o(d,{label:"唤醒度",low:"平静",high:"激活",value:t.arousal}),o(d,{label:"支配度",low:"受控",high:"主导",value:t.dominance})]}),o(m,{title:"查看写入主动流程的提示词",text:String(t.prompt_section||"")}),o(m,{title:"查看技术元数据",text:JSON.stringify(t.metadata||{},null,2)})]})}function d(e){let t=typeof e.value=="number"?e.value:0,n=Math.max(0,Math.min(100,(t+1)/2*100));return i("div",{className:"emotion-gauge",children:[i("div",{className:"emotion-gauge__label",children:[o("strong",{children:e.label}),o("code",{children:l(e.value)})]}),o("div",{className:"emotion-gauge__track","aria-hidden":"true",children:o("i",{style:{left:`${n}%`}})}),i("div",{className:"emotion-gauge__ends",children:[o("span",{children:e.low}),o("span",{children:e.high})]})]})}function m(e){return i("details",{className:"emotion-disclosure",children:[o("summary",{children:e.title}),o("pre",{children:e.text||"-"})]})}var _={id:"emotion",label:"情绪决策",viewLabel:"情绪决策",order:30,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:"expected_effect",label:"影响",width:132,renderCell:b},{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:!0,cellClass:"mono content-preview",rawTitle:!0}],async getCount({signal:e}){try{return(await s("/api/dashboard/emotion/overview",{signal:e})).effect_count||0}catch(t){if(e.aborted)throw t;return null}},async fetchPage({page:e,pageSize:t,signal:n}){let a=new URLSearchParams;a.set("page",String(e)),a.set("page_size",String(t));let c=await s(`/api/dashboard/emotion/effects?${a.toString()}`,{signal:n});return{items:c.items||[],total:c.total||0}},async fetchDetail(e,{signal:t}){return s(`/api/dashboard/emotion/effects/${e.id}`,{signal:t})},renderDetail(e,t,n){let a=g(t);return a.render(o(x,{item:e,ui:n.ui})),()=>a.unmount()},formatters:{score:e=>l(e),delta:e=>p(e),"mono-time":e=>h(e)}};function S(e){r=e.http.request;let t=e.ui.inject("workbench.panels.v2",n=>n.register(_));return()=>{t(),r=null}}export{S as activate};