feat(tasks,server,web): bridge outbox events to the SSE bus (U3)

OutboxRelay gains a `mirror` option: listed types are claimed even without a
task route and, after the claim transaction commits, published in order to
the realtime bus (publish failures are logged, delivery state unaffected).
The worker mirrors feed.*, decision.*, download.*, organization.* and
subscription.* (feed.due excluded) with their business-id payloads, so the
home page, inbox, task stream and feed tables finally receive them. Feed
tables refresh on feed.fetched / feed.health_changed; the task stream also
listens to feed and decision events.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:43:49 +08:00
1 parent 04401309fb
commit 615917bb45
6 files changed
+136 -6

No files matched your search

+14
View File
@@ -6,6 +6,16 @@ import { type EventBusPort, sseConnectionCount } from "../http/sse.ts";
import { type HeartbeatWriter, startHeartbeatWriter } from "../modules/system/heartbeat.ts";
import type { ServerContext, ServerModule } from "../modules/types.ts";
/**
* outbox → SSE 桥:这些前缀的 outbox 事件在中继投递成功后镜像到实时总线(payload 原样,带业务 id),
* 首页、收件箱、任务、来源与下载页据此失效刷新。feed.due 是调度器内部节拍,不推给浏览器。
*/
const SSE_MIRROR_PREFIXES = ["feed.", "decision.", "download.", "organization.", "subscription."] as const;
const SSE_MIRROR_EXCLUDED = new Set<string>([EventType.feedDue]);
export const SSE_MIRRORED_OUTBOX_TYPES: readonly string[] = Object.values(EventType).filter(
(t) => SSE_MIRROR_PREFIXES.some((p) => t.startsWith(p)) && !SSE_MIRROR_EXCLUDED.has(t),
);
/** agent 组任务:整段会话占一槽数分钟,与文件复制等 io 任务分池 */
export const AGENT_TASKS = ["runAgent", "resolveDecision"] as const;
@@ -146,6 +156,10 @@ export async function startWorker({ logger, heartbeatMs, bus, runtime, lagProbe
engine: ctx.engine,
routes: collectOutboxRoutes(modules, ctx),
logSink: tasksLogSink(logger),
mirror: {
types: SSE_MIRRORED_OUTBOX_TYPES,
publish: (ev) => ctx.events.publish(ev.type, ev.payload),
},
...(runtime.outboxIntervalMs ? { intervalMs: runtime.outboxIntervalMs } : {}),
});
runtime.setRelay?.(relay);