feat: integrate 5 feature branches + daemon/job 命令层级化 + 跨平台后台引擎 + TypeScript 错误修复

Squashed merge of:
1. fix/mcp-tsc-errors — 修复上游 MCP 重构后的 tsc 错误和测试失败
2. feat/pipe-mute-disconnect — Pipe IPC 逻辑断开、/lang 命令、mute 状态机
3. feat/stub-recovery-all — 实现全部 stub 恢复 (task 001-012)
4. feat/kairos-activation — KAIROS 激活解除阻塞 + 工具实现
5. codex/openclaw-autonomy-pr — 自治权限系统、运行记录、managed flows

Additional:
6. daemon/job 命令层级化重构 (subcommand 架构)
7. 跨平台后台引擎抽象 (detached/tmux engines)
8. 修复 src/ 中 43 个预存在的 TypeScript 类型错误
9. 修复 langfuse isolated test mock 完整性
10. 修复 CodeRabbit 审查的 Critical/Major/Minor 问题
11. remote-control-server logger 抽象 (测试 stderr 静默化)
12. /simplify 审查修复 (代码复用、质量、效率)
This commit is contained in:
unraid
2026-04-14 14:19:00 +08:00
parent dad3ad2b8d
commit 637c9081f6
123 changed files with 13536 additions and 1887 deletions

View File

@@ -0,0 +1,10 @@
/** Thin logging wrapper — silent in test environment, uses console in production. */
const isTest = process.env.NODE_ENV === "test" || (typeof Bun !== "undefined" && !!Bun.env.BUN_TEST);
export function log(...args: unknown[]): void {
if (!isTest) console.log(...args);
}
export function error(...args: unknown[]): void {
if (!isTest) console.error(...args);
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { createBunWebSocket } from "hono/bun";
import { validateApiKey } from "../../auth/api-key";
@@ -30,14 +31,14 @@ function authenticateRequest(c: any, label: string, expectedSessionId?: string):
const payload = verifyWorkerJwt(token);
if (payload) {
if (expectedSessionId && payload.session_id !== expectedSessionId) {
console.log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
return false;
}
return true;
}
}
console.log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
return false;
}
@@ -83,7 +84,7 @@ app.get(
const session = getSession(sessionId);
if (!session) {
console.log(`[WS] Upgrade rejected: session ${sessionId} not found`);
log(`[WS] Upgrade rejected: session ${sessionId} not found`);
return {
onOpen(_evt, ws) {
ws.close(4001, "session not found");
@@ -91,7 +92,7 @@ app.get(
};
}
console.log(`[WS] Upgrade accepted: session=${sessionId}`);
log(`[WS] Upgrade accepted: session=${sessionId}`);
return {
onOpen(_evt, ws) {
handleWebSocketOpen(ws as any, sessionId);
@@ -108,7 +109,7 @@ app.get(
handleWebSocketClose(ws as any, sessionId, closeEvt?.code, closeEvt?.reason);
},
onError(evt, ws) {
console.error(`[WS] Error on session=${sessionId}:`, evt);
logError(`[WS] Error on session=${sessionId}:`, evt);
handleWebSocketClose(ws as any, sessionId, 1006, "websocket error");
},
};

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import {
createSession,
@@ -22,7 +23,7 @@ app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
try {
await createWorkItem(body.environment_id, session.id);
} catch (err) {
console.error(`[RCS] Failed to create work item: ${(err as Error).message}`);
logError(`[RCS] Failed to create work item: ${(err as Error).message}`);
}
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, updateSessionStatus } from "../../services/session";
@@ -29,9 +30,9 @@ app.post("/sessions/:id/events", uuidAuth, async (c) => {
const body = await c.req.json();
const eventType = body.type || "user";
console.log(`[RC-DEBUG] web -> server: POST /web/sessions/${sessionId}/events type=${eventType} content=${JSON.stringify(body).slice(0, 200)}`);
log(`[RC-DEBUG] web -> server: POST /web/sessions/${sessionId}/events type=${eventType} content=${JSON.stringify(body).slice(0, 200)}`);
const event = publishSessionEvent(sessionId, eventType, body, "outbound");
console.log(`[RC-DEBUG] web -> server: published outbound event id=${event.id} type=${event.type} direction=${event.direction} subscribers=${getEventBus(sessionId).subscriberCount()}`);
log(`[RC-DEBUG] web -> server: published outbound event id=${event.id} type=${event.type} direction=${event.direction} subscribers=${getEventBus(sessionId).subscriberCount()}`);
return c.json({ status: "ok", event }, 200);
});

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../../logger";
import { Hono } from "hono";
import { uuidAuth } from "../../auth/middleware";
import { getSession, createSession } from "../../services/session";
@@ -28,7 +29,7 @@ app.post("/sessions", uuidAuth, async (c) => {
try {
await createWorkItem(body.environment_id, session.id);
} catch (err) {
console.error(`[RCS] Failed to create work item: ${(err as Error).message}`);
logError(`[RCS] Failed to create work item: ${(err as Error).message}`);
}
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
import { storeListSessions, storeUpdateSession } from "../store";
import { config } from "../config";
@@ -12,7 +13,7 @@ export function startDisconnectMonitor() {
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
@@ -23,7 +24,7 @@ export function startDisconnectMonitor() {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
storeUpdateSession(session.id, { status: "inactive" });
}
}

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import {
storeCreateWorkItem,
storeGetWorkItem,
@@ -35,7 +36,7 @@ export async function createWorkItem(environmentId: string, sessionId: string):
const secret = encodeWorkSecret();
const record = storeCreateWorkItem({ environmentId, sessionId, secret });
console.log(`[RCS] Work item created: ${record.id} for env=${environmentId} session=${sessionId}`);
log(`[RCS] Work item created: ${record.id} for env=${environmentId} session=${sessionId}`);
return record.id;
}

View File

@@ -1,3 +1,5 @@
import { log, error as logError } from "../logger";
export interface SessionEvent {
id: string;
sessionId: string;
@@ -33,12 +35,12 @@ export class EventBus {
createdAt: Date.now(),
};
this.events.push(full);
console.log(`[RC-DEBUG] bus publish: sessionId=${event.sessionId} type=${event.type} dir=${event.direction} seq=${full.seqNum} subscribers=${this.subscribers.size}`);
log(`[RC-DEBUG] bus publish: sessionId=${event.sessionId} type=${event.type} dir=${event.direction} seq=${full.seqNum} subscribers=${this.subscribers.size}`);
for (const cb of this.subscribers) {
try {
cb(full);
} catch (err) {
console.error(`[RC-DEBUG] bus subscriber error:`, err);
logError(`[RC-DEBUG] bus subscriber error:`, err);
}
}
return full;

View File

@@ -1,3 +1,4 @@
import { log, error as logError } from "../logger";
import type { Context } from "hono";
import type { SessionEvent } from "./event-bus";
import { getEventBus } from "./event-bus";
@@ -76,7 +77,7 @@ export function createSSEStream(c: Context, sessionId: string, fromSeqNum = 0) {
seqNum: event.seqNum,
});
try {
console.log(`[RC-DEBUG] SSE -> web: sessionId=${sessionId} type=${event.type} dir=${event.direction} seq=${event.seqNum}`);
log(`[RC-DEBUG] SSE -> web: sessionId=${sessionId} type=${event.type} dir=${event.direction} seq=${event.seqNum}`);
controller.enqueue(encoder.encode(`id: ${event.seqNum}\nevent: message\ndata: ${data}\n\n`));
} catch {
unsub();

View File

@@ -2,6 +2,7 @@ import type { WSContext } from "hono/ws";
import { getEventBus } from "./event-bus";
import type { SessionEvent } from "./event-bus";
import { publishSessionEvent } from "../services/transport";
import { log, error as logError } from "../logger";
// Per-connection cleanup, keyed by sessionId (only one WS per session)
interface CleanupEntry {
@@ -96,13 +97,13 @@ function toSDKMessage(event: SessionEvent): string {
/** Called from onOpen — subscribes to event bus, forwards outbound events to bridge WS */
export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
const openTime = Date.now();
console.log(`[RC-DEBUG] [WS] Open session=${sessionId}`);
log(`[RC-DEBUG] [WS] Open session=${sessionId}`);
activeConnections.add(ws);
// If there's an existing connection for this session, clean it up first
const existing = cleanupBySession.get(sessionId);
if (existing) {
console.log(`[WS] Replacing existing connection for session=${sessionId}`);
log(`[WS] Replacing existing connection for session=${sessionId}`);
existing.unsub();
clearInterval(existing.keepalive);
activeConnections.delete(existing.ws);
@@ -114,7 +115,7 @@ export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
// the full conversation history — assistant replies are inbound events.
const missed = bus.getEventsSince(0);
if (missed.length > 0) {
console.log(`[WS] Replaying ${missed.length} missed event(s)`);
log(`[WS] Replaying ${missed.length} missed event(s)`);
for (const event of missed) {
if (ws.readyState !== 1) break;
try {
@@ -130,10 +131,10 @@ export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
if (event.direction !== "outbound") return;
try {
const sdkMsg = toSDKMessage(event);
console.log(`[RC-DEBUG] [WS] -> bridge (outbound): type=${event.type} len=${sdkMsg.length} msg=${sdkMsg.slice(0, 300)}`);
log(`[RC-DEBUG] [WS] -> bridge (outbound): type=${event.type} len=${sdkMsg.length} msg=${sdkMsg.slice(0, 300)}`);
ws.send(sdkMsg);
} catch (err) {
console.error("[RC-DEBUG] [WS] send error:", err);
logError("[RC-DEBUG] [WS] send error:", err);
}
});
@@ -161,7 +162,7 @@ export function handleWebSocketMessage(ws: WSContext, sessionId: string, data: s
try {
ingestBridgeMessage(sessionId, JSON.parse(line));
} catch (err) {
console.error("[WS] parse error:", err);
logError("[WS] parse error:", err);
}
}
}
@@ -173,7 +174,7 @@ export function handleWebSocketClose(ws: WSContext, sessionId: string, code?: nu
const entry = cleanupBySession.get(sessionId);
const duration = entry ? Math.round((Date.now() - entry.openTime) / 1000) : -1;
console.log(`[WS] Close session=${sessionId} code=${code ?? "none"} reason=${reason || "(none)"} duration=${duration}s`);
log(`[WS] Close session=${sessionId} code=${code ?? "none"} reason=${reason || "(none)"} duration=${duration}s`);
if (entry) {
entry.unsub();
@@ -215,7 +216,7 @@ export function ingestBridgeMessage(sessionId: string, msg: Record<string, unkno
const eventType = deriveEventType(msg);
console.log(`[RC-DEBUG] [WS] <- bridge (inbound): sessionId=${sessionId} type=${eventType}${msg.uuid ? ` uuid=${msg.uuid}` : ""} msg=${JSON.stringify(msg).slice(0, 300)}`);
log(`[RC-DEBUG] [WS] <- bridge (inbound): sessionId=${sessionId} type=${eventType}${msg.uuid ? ` uuid=${msg.uuid}` : ""} msg=${JSON.stringify(msg).slice(0, 300)}`);
let payload: unknown;
@@ -255,7 +256,7 @@ export function closeAllConnections(): void {
const count = activeConnections.size;
if (count === 0) return;
console.log(`[WS] Gracefully closing ${count} active connection(s)...`);
log(`[WS] Gracefully closing ${count} active connection(s)...`);
for (const [sessionId, entry] of cleanupBySession) {
try {
entry.unsub();
@@ -269,5 +270,5 @@ export function closeAllConnections(): void {
}
cleanupBySession.clear();
activeConnections.clear();
console.log("[WS] All connections closed");
log("[WS] All connections closed");
}