diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml
index b4ae1de..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: 9da3a988a2bf62b0f550bd4f6bb98c4eeb1f56f5
+ ref: d5e5177092d74de03588d8675f3504db36b7bacb
path: .akashic-core
- uses: actions/checkout@v4
with:
diff --git a/akashic.plugin.toml b/akashic.plugin.toml
index 56d0962..d6cba39 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.4"
api_version = 3
entrypoint = "plugin.py"
diff --git a/dashboard_panel.css b/dashboard_panel.css
index ca13718..5c7a0cc 100644
--- a/dashboard_panel.css
+++ b/dashboard_panel.css
@@ -1,3 +1,22 @@
+.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;
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
index c0e7a43..8c0f580 100644
--- a/dashboard_panel.tsx
+++ b/dashboard_panel.tsx
@@ -1,6 +1,18 @@
-///
import { type ReactElement } from "react";
-import { Chip, api } 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 { WorkbenchDispatch, WorkbenchPanelEntry, WorkbenchUi } 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;
@@ -48,13 +60,14 @@ 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 情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
;
+ return 情绪影响详情
选择一条记录,查看这次主动任务的情绪影响。
;
}
const delta = typeof item.threshold_delta === "number" ? item.threshold_delta : null;
return (
@@ -114,10 +127,11 @@ function TextDisclosure(props: { title: string; text: string }): ReactElement {
);
}
-window.AkashicDashboard.registerPlugin({
+const panel = {
id: "emotion",
label: "情绪决策",
viewLabel: "情绪决策",
+ order: 30,
pageSize: 50,
rowKey: "id",
@@ -135,32 +149,46 @@ window.AkashicDashboard.registerPlugin({
{ key: "tick_id", label: "任务", flex: true, cellClass: "mono content-preview", rawTitle: true },
],
- async getCount(): Promise {
+ async getCount({ signal }: { signal: AbortSignal }): Promise {
try {
- const overview = await api("/api/dashboard/emotion/overview");
+ const overview = await api("/api/dashboard/emotion/overview", { signal });
return overview.effect_count || 0;
- } catch {
+ } catch (error) {
+ if (signal.aborted) throw error;
return null;
}
},
- async fetchPage({ page, pageSize }: { page: number; pageSize: number }) {
+ 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()}`);
+ const data = await api(`/api/dashboard/emotion/effects?${params.toString()}`, { signal });
return { items: data.items || [], total: data.total || 0 };
},
- async fetchDetail(item: Record) {
- return api>(`/api/dashboard/emotion/effects/${item.id}`);
+ async fetchDetail(item: Record, { signal }: { signal: AbortSignal }) {
+ 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),
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/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..a46d6db 100644
--- a/plugin.py
+++ b/plugin.py
@@ -32,7 +32,7 @@
api_version = 3
name = "emotion"
-version = "3.0.2"
+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")
@@ -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.v2",)
+web_provides = ()
+web_contract_digests = {
+ "workbench.panels.v2": "fb6417c9bf532c1fdb344767d06065d5d3293da85deb64eff1e8088889a33bcb",
+}
_v3_emotion_root: Path | None = None
_v3_emotion_runtime: EmotionRuntime | None = None
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..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)
@@ -129,6 +132,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 +172,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=(),
)
@@ -278,6 +280,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."""
@@ -805,6 +834,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..08a03da
--- /dev/null
+++ b/web_module.css
@@ -0,0 +1 @@
+.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
new file mode 100644
index 0000000..95e1371
--- /dev/null
+++ b/web_module.js
@@ -0,0 +1 @@
+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};