Files
Cyrene_v2/frontend/web/src/admin/modules/Automations.tsx
T
Aska 6dbb4a2e18
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 唤醒式架构批次——capacity/L4/会话编排/管理面板写CRUD/前端集成(77-81)
【capacity 唤醒式核心(77)】internal/runtime/capacity:(Decider L1启发式+L2上下文+人格覆盖→三态静息/唤醒/全能→允许工具集+capacity_awaken事件)。loop 外围允许集过滤(不硬阻断=能力≠边界红线)。config 加 Capacity(Enabled/MinConfidence/ToolSets)。wire buildCapacity 接入 loop。

【L4 记忆驱动意图(80)】recall.go:从记忆召回用户习惯(搞一下xx=办事)→唤醒L1/L2漏掉/升置信;群聊陌生人不读owner习惯(隔离红线);RecordIntent闭环(判定写回记忆越用越准);默认保守静息。

【会话消息编排(78)】gateway/session_orchestrator.go:每会话(模式/pending有界5/tail上下文尾巴20);Submit非阻塞(idle立即/busy入队ack/满拒绝);完成回调按序续接(tail+新消息合并增量注入非全量重放);Interrupt中断;REST 202/409+WS session_busy/pending_ack事件;不重造任务引擎复用loop。

【前端集成(79)】ws.ts 加 capacity_awaken/session_busy/pending_ack 解析;store 加 awakenState/sessionBusy;TopBar/ChatShell 加唤醒态徽标+忙态提示(不阻塞输入框);状态组件 status.tsx;Miuix风格。

【管理面板写CRUD(81)】20个写端点(POST/PUT/DELETE /admin/*,owner+审计落库):personas(真持久化YAML)/users apikey(签发掩码+撤销)/iot控制(真下发)/automations(建/触发/删)/llm(test真调用+临时)/channels(connect/close/身份测试actor白名单)/plugins(卸载)/memories(search真检索)/sessions(export)。无存储如实501不伪造。拆4文件满足≤500红线。前端9模块接真写。

【wire收口】buildCapacity接入loop+OnCapacity事件;server.go newOrchestrator接hub。

实机验证:iot ac.living control 200 真实改变 on:true;observability/metrics 200、health ok;全仓 build/vet/test 47包绿+前端tsc/vite。
2026-09-06 21:54:40 +08:00

155 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { useAsyncData } from "../../lib/useAsyncData";
import {
adminListAutomations,
adminCreateAutomation,
adminRunAutomation,
adminDeleteAutomation,
isNotImplemented,
} from "../../lib/api";
import type { AdminAutomationRecord } from "../../lib/types";
import { useAuditStore } from "../audit";
import { Notice, PageHeader } from "../components";
import { Button, Surface, Switch, Tag } from "../../components/primitives";
import { Icon } from "../../components/Icon";
const sample: AdminAutomationRecord[] = [
{ id: "r1", name: "睡前总结", kind: "reminder", cron: "0 22 * * *", enabled: true },
{ id: "r2", name: "晨间问候", kind: "cron", cron: "0 8 * * *", enabled: true },
{ id: "r3", name: "设备待机提醒", kind: "queue", cron: "—", enabled: false },
];
export function AutomationsModule(): React.ReactNode {
const { data, error, reload } = useAsyncData(adminListAutomations, []);
const log = useAuditStore((s) => s.log);
const backend = !isNotImplemented(error);
const rows = backend && data && data.length > 0 ? data : sample;
const [name, setName] = useState("");
const [kind, setKind] = useState("cron");
const [expr, setExpr] = useState("");
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const create = () => {
setBusy(true);
setMsg(null);
adminCreateAutomation({ name, kind, expression: expr })
.then((r) => {
log("automation.create", "ok");
setMsg(`已创建 ${r.name}(${r.kind})`);
setName("");
setExpr("");
reload();
})
.catch((e: unknown) => {
log("automation.create", "error");
setMsg(e instanceof Error ? e.message : "创建失败");
})
.finally(() => setBusy(false));
};
const run = (id: string, label: string) => {
setBusy(true);
setMsg(null);
adminRunAutomation(id)
.then((r) => {
log("automation.run", "ok");
setMsg(`已触发 ${label}:${JSON.stringify(r.output)}`);
})
.catch((e: unknown) => {
log("automation.run", "error");
setMsg(e instanceof Error ? e.message : "触发失败");
})
.finally(() => setBusy(false));
};
const del = (id: string, label: string) => {
setBusy(true);
setMsg(null);
adminDeleteAutomation(id)
.then(() => {
log("automation.delete", "ok");
setMsg(`已删除 ${label}`);
reload();
})
.catch((e: unknown) => {
log("automation.delete", "error");
setMsg(e instanceof Error ? e.message : "删除失败");
})
.finally(() => setBusy(false));
};
return (
<div className="space-y-5">
<PageHeader
title="自动化与提醒"
subtitle="提醒 + cron + 消息队列事件 + 后台任务(含终端任务) + 运行日志"
actions={
<Button variant="primary" disabled={busy || !name || !expr} onClick={create}>
新增自动化
</Button>
}
/>
{msg ? <Notice tone={msg.includes("失败") ? "err" : "ok"}>{msg}</Notice> : null}
{backend && !error ? <Notice tone="ok">后端已返回自动化配置(数据以接口为准)。</Notice> : null}
{error && !backend ? <Notice tone="warn">后端未提供 /api/v1/admin/automations,以下为示例壳(M10 提供真实数据)。</Notice> : null}
{error && backend ? <Notice tone="err">{error}</Notice> : null}
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">提醒 / CRON / 队列事件</h3>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">名称</span>
<input value={name} onChange={(e) => setName(e.target.value)} className={inputCls} />
</label>
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">类型</span>
<select value={kind} onChange={(e) => setKind(e.target.value)} className={inputCls}>
<option value="cron">cron</option>
<option value="event">event</option>
</select>
</label>
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">表达式(cron / bus subject)</span>
<input value={expr} onChange={(e) => setExpr(e.target.value)} placeholder="0 22 * * *" className={inputCls} />
</label>
</div>
{rows.map((r) => (
<div key={r.id} className="flex items-center gap-3 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2.5 text-sm">
<span className="font-medium">{r.name}</span>
<Tag tone="neutral">{r.kind}</Tag>
<span className="font-mono text-xs text-[var(--mu-text-lo)]">{r.cron || "—"}</span>
<span className="ml-auto flex items-center gap-2">
<Switch checked={r.enabled} disabled onChange={() => {}} label="启用" />
<Button size="sm" variant="ghost" disabled={busy} onClick={() => run(r.id, r.name)}>
触发
</Button>
<Button size="sm" variant="ghost" disabled={busy} onClick={() => del(r.id, r.name)}>
删除
</Button>
</span>
</div>
))}
</Surface>
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">后台任务 / 运行历史</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{["消息队列事件", "后台任务(含终端任务)", "运行日志"].map((x) => (
<div key={x} className="rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-4 text-center text-xs text-[var(--mu-text-mid)]">
<div className="mb-2 flex justify-center"><Icon name="package" size={20} /></div>
{x} 占位 —— 后端 M10+ 提供
</div>
))}
</div>
</Surface>
</div>
);
}
const inputCls =
"w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]";