fix(webui): drop legacy automation setup state

This commit is contained in:
chengyongru
2026-06-16 00:26:06 +08:00
parent 87aaf8991a
commit e6957de622
15 changed files with 20 additions and 111 deletions
+1
View File
@@ -56,6 +56,7 @@
## 📢 News ## 📢 News
- **2026-06-16** ⚠️ Breaking change: agent-turn automation jobs now require complete target chat or channel metadata. Cron jobs created by older versions without that metadata should be recreated from the target chat or channel; existing cron records remain in the cron store.
- **2026-06-01** 🚀 Released **v0.2.1****The Workbench Release** turns the packaged WebUI into a daily agent workbench: clearer Thought/response timelines, live file-edit activity, project workspaces, model and context controls, steadier sustained goals, CLI Apps + MCP extensions, and broader provider/channel support. Please see [release notes](https://github.com/HKUDS/nanobot/releases/tag/v0.2.1) for details. - **2026-06-01** 🚀 Released **v0.2.1****The Workbench Release** turns the packaged WebUI into a daily agent workbench: clearer Thought/response timelines, live file-edit activity, project workspaces, model and context controls, steadier sustained goals, CLI Apps + MCP extensions, and broader provider/channel support. Please see [release notes](https://github.com/HKUDS/nanobot/releases/tag/v0.2.1) for details.
- **2026-05-30** 🔐 Safer Matrix verification, bounded media downloads, clearer WebUI model timeline. - **2026-05-30** 🔐 Safer Matrix verification, bounded media downloads, clearer WebUI model timeline.
- **2026-05-29** 🧩 Extension registry, context-window tuning, document extraction controls. - **2026-05-29** 🧩 Extension registry, context-window tuning, document extraction controls.
-16
View File
@@ -122,16 +122,6 @@ def _serialize_job(
payload["created_at_ms"] = job.created_at_ms payload["created_at_ms"] = job.created_at_ms
payload["updated_at_ms"] = job.updated_at_ms payload["updated_at_ms"] = job.updated_at_ms
payload["payload"].update({"kind": job.payload.kind}) payload["payload"].update({"kind": job.payload.kind})
if _expose_origin_identifiers(job):
payload["payload"].update(
{
"session_key": job.payload.session_key,
"origin_channel": job.payload.origin_channel,
"origin_chat_id": job.payload.origin_chat_id,
}
)
elif job.payload.origin_channel:
payload["payload"]["origin_channel"] = job.payload.origin_channel
payload["state"].update( payload["state"].update(
{ {
"last_run_at_ms": job.state.last_run_at_ms, "last_run_at_ms": job.state.last_run_at_ms,
@@ -183,12 +173,6 @@ def _origin_payload(
"preview": preview, "preview": preview,
} }
def _expose_origin_identifiers(job: CronJob) -> bool:
channel = job.payload.origin_channel
return not channel or channel == "websocket"
def _session_preview(messages: Any) -> str: def _session_preview(messages: Any) -> str:
if not isinstance(messages, list): if not isinstance(messages, list):
return "" return ""
+7 -4
View File
@@ -842,7 +842,7 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
origin_channel="websocket", origin_channel="websocket",
origin_chat_id="abc", origin_chat_id="abc",
) )
legacy_job = cron.add_job( incomplete_job = cron.add_job(
name="english-quiz", name="english-quiz",
schedule=CronSchedule(kind="every", every_ms=3_600_000), schedule=CronSchedule(kind="every", every_ms=3_600_000),
message="Practice English", message="Practice English",
@@ -899,6 +899,7 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
) )
assert resp.status_code == 200 assert resp.status_code == 200
assert "wx-chat" not in resp.text assert "wx-chat" not in resp.text
assert "unified:default" not in resp.text
body = resp.json() body = resp.json()
by_id = {job["id"]: job for job in body["jobs"]} by_id = {job["id"]: job for job in body["jobs"]}
assert by_id[user_job.id]["protected"] is False assert by_id[user_job.id]["protected"] is False
@@ -906,10 +907,12 @@ async def test_webui_automations_route_lists_all_jobs_and_allows_user_actions(
assert by_id[user_job.id]["state"]["run_history"] == [] assert by_id[user_job.id]["state"]["run_history"] == []
assert by_id[user_job.id]["origin"]["session_key"] == "websocket:abc" assert by_id[user_job.id]["origin"]["session_key"] == "websocket:abc"
assert by_id[user_job.id]["origin"]["preview"] == "hi" assert by_id[user_job.id]["origin"]["preview"] == "hi"
assert by_id[legacy_job.id]["payload"]["session_key"] == "unified:default" assert "session_key" not in by_id[incomplete_job.id]["payload"]
assert by_id[legacy_job.id]["origin"] is None assert "origin_channel" not in by_id[incomplete_job.id]["payload"]
assert by_id[external_job.id]["payload"]["origin_channel"] == "weixin" assert "origin_chat_id" not in by_id[incomplete_job.id]["payload"]
assert by_id[incomplete_job.id]["origin"] is None
assert "session_key" not in by_id[external_job.id]["payload"] assert "session_key" not in by_id[external_job.id]["payload"]
assert "origin_channel" not in by_id[external_job.id]["payload"]
assert "origin_chat_id" not in by_id[external_job.id]["payload"] assert "origin_chat_id" not in by_id[external_job.id]["payload"]
assert by_id[external_job.id]["origin"]["channel"] == "weixin" assert by_id[external_job.id]["origin"]["channel"] == "weixin"
assert "session_key" not in by_id[external_job.id]["origin"] assert "session_key" not in by_id[external_job.id]["origin"]
+3 -24
View File
@@ -3698,7 +3698,6 @@ function AutomationDetailPanel({
const originHref = job.origin?.channel === "websocket" && job.origin.session_key const originHref = job.origin?.channel === "websocket" && job.origin.session_key
? `#/chat/${encodeURIComponent(job.origin.session_key)}` ? `#/chat/${encodeURIComponent(job.origin.session_key)}`
: null; : null;
const needsRecreation = automationNeedsRecreation(job);
const created = job.created_at_ms ? fmtDateTime(job.created_at_ms, locale) : null; const created = job.created_at_ms ? fmtDateTime(job.created_at_ms, locale) : null;
const updated = job.updated_at_ms ? fmtDateTime(job.updated_at_ms, locale) : null; const updated = job.updated_at_ms ? fmtDateTime(job.updated_at_ms, locale) : null;
const message = job.payload.message || tx("settings.automations.systemTask", "System-managed automation"); const message = job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
@@ -3787,15 +3786,6 @@ function AutomationDetailPanel({
</AutomationDetail> </AutomationDetail>
</div> </div>
{needsRecreation ? (
<div className="rounded-[16px] border border-amber-500/20 bg-amber-500/8 px-3 py-2 text-[12px] leading-5 text-amber-800 dark:text-amber-200">
{tx(
"settings.automations.legacyWarning",
"This older automation is missing its target chat. Recreate it from the chat or channel where it should run.",
)}
</div>
) : null}
{job.state.last_error ? ( {job.state.last_error ? (
<div className="rounded-[16px] border border-destructive/20 bg-destructive/8 px-3 py-2 text-[12px] leading-5 text-destructive"> <div className="rounded-[16px] border border-destructive/20 bg-destructive/8 px-3 py-2 text-[12px] leading-5 text-destructive">
{job.state.last_error} {job.state.last_error}
@@ -4227,19 +4217,14 @@ function AutomationDeleteDialog({
); );
} }
function automationNeedsRecreation(job: SessionAutomationJob): boolean {
return !job.protected && !job.origin && job.payload.kind === "agent_turn";
}
function automationNeedsAttention(job: SessionAutomationJob): boolean { function automationNeedsAttention(job: SessionAutomationJob): boolean {
return automationNeedsRecreation(job) || job.state.last_status === "error"; return job.state.last_status === "error";
} }
function automationStatusKey( function automationStatusKey(
job: SessionAutomationJob, job: SessionAutomationJob,
): "active" | "running" | "paused" | "failed" | "system" | "needs_setup" | "completed" | "idle" { ): "active" | "running" | "paused" | "failed" | "system" | "completed" | "idle" {
if (job.protected) return "system"; if (job.protected) return "system";
if (automationNeedsRecreation(job)) return "needs_setup";
if (job.state.pending) return "running"; if (job.state.pending) return "running";
if (!job.enabled) return "paused"; if (!job.enabled) return "paused";
if (job.state.last_status === "error") return "failed"; if (job.state.last_status === "error") return "failed";
@@ -4412,9 +4397,6 @@ function automationStatus(
): { label: string; tone: "neutral" | "success" | "warning" } { ): { label: string; tone: "neutral" | "success" | "warning" } {
const status = automationStatusKey(job); const status = automationStatusKey(job);
if (status === "system") return { label: tx("settings.automations.status.system", "System"), tone: "neutral" }; if (status === "system") return { label: tx("settings.automations.status.system", "System"), tone: "neutral" };
if (status === "needs_setup") {
return { label: tx("settings.automations.status.needsSetup", "Needs setup"), tone: "warning" };
}
if (status === "running") { if (status === "running") {
return { label: tx("settings.automations.status.running", "Running now"), tone: "warning" }; return { label: tx("settings.automations.status.running", "Running now"), tone: "warning" };
} }
@@ -4437,9 +4419,6 @@ function automationOriginLabel(
): string { ): string {
if (job.protected) return tx("settings.automations.origin.system", "System"); if (job.protected) return tx("settings.automations.origin.system", "System");
const origin = job.origin; const origin = job.origin;
if (!origin && job.payload.kind === "agent_turn") {
return tx("settings.automations.origin.legacy", "Recreate in target chat");
}
if (!origin) return tx("settings.automations.origin.unknown", "No linked chat"); if (!origin) return tx("settings.automations.origin.unknown", "No linked chat");
if (origin.channel !== "websocket") return automationChannelLabel(origin.channel, tx); if (origin.channel !== "websocket") return automationChannelLabel(origin.channel, tx);
return origin.title || origin.preview || origin.session_key || automationChannelLabel(origin.channel, tx); return origin.title || origin.preview || origin.session_key || automationChannelLabel(origin.channel, tx);
@@ -4574,7 +4553,7 @@ function formatAutomationNextTitle(
function automationStatusDotClass(job: SessionAutomationJob): string { function automationStatusDotClass(job: SessionAutomationJob): string {
const status = automationStatusKey(job); const status = automationStatusKey(job);
if (status === "active" || status === "running") return "bg-emerald-500"; if (status === "active" || status === "running") return "bg-emerald-500";
if (status === "failed" || status === "needs_setup") return "bg-amber-500"; if (status === "failed") return "bg-amber-500";
if (status === "system") return "bg-blue-500"; if (status === "system") return "bg-blue-500";
return "bg-muted-foreground/45"; return "bg-muted-foreground/45";
} }
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "System", "system": "System",
"pending": "Pending", "pending": "Pending",
"running": "Running now", "running": "Running now",
"needsSetup": "Needs setup",
"paused": "Paused", "paused": "Paused",
"failed": "Failed", "failed": "Failed",
"completed": "Completed", "completed": "Completed",
"noSchedule": "No schedule", "noSchedule": "No schedule",
"active": "Active" "active": "Active"
}, },
"legacyWarning": "This older automation is missing its target chat. Recreate it from the chat or channel where it should run.",
"origin": { "origin": {
"system": "System", "system": "System",
"unknown": "No linked chat", "unknown": "No linked chat"
"legacy": "Recreate in target chat"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "Sistema", "system": "Sistema",
"pending": "Pendiente", "pending": "Pendiente",
"running": "Ejecutándose ahora", "running": "Ejecutándose ahora",
"needsSetup": "Requiere configuración",
"paused": "Pausada", "paused": "Pausada",
"failed": "Fallida", "failed": "Fallida",
"completed": "Completada", "completed": "Completada",
"noSchedule": "Sin programación", "noSchedule": "Sin programación",
"active": "Activa" "active": "Activa"
}, },
"legacyWarning": "Esta automatización antigua no tiene chat de destino. Vuelve a crearla desde el chat o canal donde debe ejecutarse.",
"origin": { "origin": {
"system": "Sistema", "system": "Sistema",
"unknown": "Sin chat vinculado", "unknown": "Sin chat vinculado"
"legacy": "Recréala en el chat de destino"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "Système", "system": "Système",
"pending": "En attente", "pending": "En attente",
"running": "En cours dexécution", "running": "En cours dexécution",
"needsSetup": "Configuration requise",
"paused": "En pause", "paused": "En pause",
"failed": "Échouée", "failed": "Échouée",
"completed": "Terminée", "completed": "Terminée",
"noSchedule": "Aucun planning", "noSchedule": "Aucun planning",
"active": "En cours" "active": "En cours"
}, },
"legacyWarning": "Cette ancienne automatisation na pas de discussion cible. Recréez-la depuis la discussion ou le canal où elle doit sexécuter.",
"origin": { "origin": {
"system": "Système", "system": "Système",
"unknown": "Aucune discussion liée", "unknown": "Aucune discussion liée"
"legacy": "Recréez-la dans la discussion cible"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "Sistem", "system": "Sistem",
"pending": "Menunggu", "pending": "Menunggu",
"running": "Sedang berjalan", "running": "Sedang berjalan",
"needsSetup": "Perlu disiapkan",
"paused": "Dijeda", "paused": "Dijeda",
"failed": "Gagal", "failed": "Gagal",
"completed": "Selesai", "completed": "Selesai",
"noSchedule": "Tanpa jadwal", "noSchedule": "Tanpa jadwal",
"active": "Aktif" "active": "Aktif"
}, },
"legacyWarning": "Otomasi lama ini tidak memiliki chat tujuan. Buat ulang dari chat atau channel tempat otomasi akan berjalan.",
"origin": { "origin": {
"system": "Sistem", "system": "Sistem",
"unknown": "Tidak ada chat tertaut", "unknown": "Tidak ada chat tertaut"
"legacy": "Buat ulang di chat tujuan"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "システム", "system": "システム",
"pending": "待機中", "pending": "待機中",
"running": "実行中", "running": "実行中",
"needsSetup": "再設定が必要",
"paused": "一時停止", "paused": "一時停止",
"failed": "失敗", "failed": "失敗",
"completed": "完了", "completed": "完了",
"noSchedule": "スケジュールなし", "noSchedule": "スケジュールなし",
"active": "実行中" "active": "実行中"
}, },
"legacyWarning": "この古い自動タスクには対象チャットがありません。実行先のチャットまたは外部 channel から作り直してください。",
"origin": { "origin": {
"system": "システム", "system": "システム",
"unknown": "関連チャットなし", "unknown": "関連チャットなし"
"legacy": "対象チャットで作り直してください"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "시스템", "system": "시스템",
"pending": "대기 중", "pending": "대기 중",
"running": "실행 중", "running": "실행 중",
"needsSetup": "설정 필요",
"paused": "일시 중지", "paused": "일시 중지",
"failed": "실패", "failed": "실패",
"completed": "완료", "completed": "완료",
"noSchedule": "일정 없음", "noSchedule": "일정 없음",
"active": "활성" "active": "활성"
}, },
"legacyWarning": "이전 자동화에 대상 채팅이 없습니다. 실행되어야 하는 채팅 또는 외부 channel에서 다시 만드세요.",
"origin": { "origin": {
"system": "시스템", "system": "시스템",
"unknown": "연결된 채팅 없음", "unknown": "연결된 채팅 없음"
"legacy": "대상 채팅에서 다시 만드세요"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "Hệ thống", "system": "Hệ thống",
"pending": "Đang chờ", "pending": "Đang chờ",
"running": "Đang chạy", "running": "Đang chạy",
"needsSetup": "Cần thiết lập",
"paused": "Đã tạm dừng", "paused": "Đã tạm dừng",
"failed": "Thất bại", "failed": "Thất bại",
"completed": "Hoàn tất", "completed": "Hoàn tất",
"noSchedule": "Không có lịch", "noSchedule": "Không có lịch",
"active": "Đang chạy" "active": "Đang chạy"
}, },
"legacyWarning": "Tự động hóa cũ này thiếu cuộc trò chuyện đích. Hãy tạo lại từ cuộc trò chuyện hoặc channel nơi nó sẽ chạy.",
"origin": { "origin": {
"system": "Hệ thống", "system": "Hệ thống",
"unknown": "Chưa liên kết cuộc trò chuyện", "unknown": "Chưa liên kết cuộc trò chuyện"
"legacy": "Tạo lại trong cuộc trò chuyện đích"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "系统", "system": "系统",
"pending": "等待中", "pending": "等待中",
"running": "正在运行", "running": "正在运行",
"needsSetup": "需重新设置",
"paused": "已暂停", "paused": "已暂停",
"failed": "失败", "failed": "失败",
"completed": "已完成", "completed": "已完成",
"noSchedule": "无计划", "noSchedule": "无计划",
"active": "运行中" "active": "运行中"
}, },
"legacyWarning": "这个旧版自动任务缺少目标会话。请从它应该运行的聊天或外部 channel 中重新创建。",
"origin": { "origin": {
"system": "系统", "system": "系统",
"unknown": "未关联会话", "unknown": "未关联会话"
"legacy": "请在目标会话中重新创建"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
+1 -4
View File
@@ -525,18 +525,15 @@
"system": "系統", "system": "系統",
"pending": "等待中", "pending": "等待中",
"running": "正在執行", "running": "正在執行",
"needsSetup": "需重新設定",
"paused": "已暫停", "paused": "已暫停",
"failed": "失敗", "failed": "失敗",
"completed": "已完成", "completed": "已完成",
"noSchedule": "無排程", "noSchedule": "無排程",
"active": "執行中" "active": "執行中"
}, },
"legacyWarning": "這個舊版自動任務缺少目標會話。請從它應該執行的聊天或外部 channel 中重新建立。",
"origin": { "origin": {
"system": "系統", "system": "系統",
"unknown": "未關聯會話", "unknown": "未關聯會話"
"legacy": "請在目標會話中重新建立"
}, },
"channels": { "channels": {
"api": "API", "api": "API",
-3
View File
@@ -114,9 +114,6 @@ export interface SessionAutomationJob {
payload: { payload: {
message: string; message: string;
kind?: "agent_turn" | "system_event" | string; kind?: "agent_turn" | "system_event" | string;
session_key?: string | null;
origin_channel?: string | null;
origin_chat_id?: string | null;
}; };
state: { state: {
next_run_at_ms?: number | null; next_run_at_ms?: number | null;
-28
View File
@@ -372,7 +372,6 @@ describe("App layout", () => {
payload: { payload: {
message: "Check the repo status", message: "Check the repo status",
kind: "agent_turn", kind: "agent_turn",
session_key: "websocket:chat-a",
}, },
state: { state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0), next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),
@@ -388,26 +387,6 @@ describe("App layout", () => {
preview: "Check release blockers", preview: "Check release blockers",
}, },
}, },
{
id: "legacy-quiz",
name: "english-quiz",
enabled: true,
protected: false,
delete_after_run: false,
schedule: { kind: "cron", expr: "30 9-23 * * *", tz: "Asia/Shanghai" },
payload: {
message: "Practice English",
kind: "agent_turn",
session_key: "unified:default",
},
state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 11, 0, 0),
last_status: "ok",
pending: false,
run_history: [],
},
origin: null,
},
{ {
id: "external-quiz", id: "external-quiz",
name: "WeChat quiz", name: "WeChat quiz",
@@ -418,7 +397,6 @@ describe("App layout", () => {
payload: { payload: {
message: "Send a quiz", message: "Send a quiz",
kind: "agent_turn", kind: "agent_turn",
origin_channel: "weixin",
}, },
state: { state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 11, 30, 0), next_run_at_ms: Date.UTC(2026, 3, 17, 11, 30, 0),
@@ -464,9 +442,6 @@ describe("App layout", () => {
expect(screen.getAllByText("Daily repo check").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Daily repo check").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Check the repo status").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Check the repo status").length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText("Release prep").length).toBeGreaterThanOrEqual(1); expect(screen.getAllByText("Release prep").length).toBeGreaterThanOrEqual(1);
expect(screen.getByText("english-quiz")).toBeInTheDocument();
expect(screen.getByText("Recreate in target chat")).toBeInTheDocument();
expect(screen.queryByText("unified:default")).not.toBeInTheDocument();
expect(screen.getByText("WeChat quiz")).toBeInTheDocument(); expect(screen.getByText("WeChat quiz")).toBeInTheDocument();
expect(screen.getByText("WeChat")).toBeInTheDocument(); expect(screen.getByText("WeChat")).toBeInTheDocument();
expect(screen.queryByText("weixin:wx-chat")).not.toBeInTheDocument(); expect(screen.queryByText("weixin:wx-chat")).not.toBeInTheDocument();
@@ -490,7 +465,6 @@ describe("App layout", () => {
payload: { payload: {
message: "Old one-shot message", message: "Old one-shot message",
kind: "agent_turn", kind: "agent_turn",
session_key: "websocket:chat-a",
}, },
state: { state: {
next_run_at_ms: null, next_run_at_ms: null,
@@ -589,7 +563,6 @@ describe("App layout", () => {
payload: { payload: {
message: longMessage, message: longMessage,
kind: "agent_turn", kind: "agent_turn",
session_key: "websocket:chat-a",
}, },
state: { state: {
next_run_at_ms: Date.UTC(2026, 3, 18, 10, 0, 0), next_run_at_ms: Date.UTC(2026, 3, 18, 10, 0, 0),
@@ -650,7 +623,6 @@ describe("App layout", () => {
payload: { payload: {
message: "检查仓库状态", message: "检查仓库状态",
kind: "agent_turn", kind: "agent_turn",
session_key: "websocket:chat-a",
}, },
state: { state: {
next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0), next_run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0),