mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
* feat: 适配 zed acp 协议 * docs: 完善 acp 文档 * feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎 Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct detectMimeFromBase64 to decode raw bytes from base64 Cherry-picked from origin/lint/preview (ee36954). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构 Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes. - 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层 - 修复 --daemon-worker=kind 等号格式解析 - 修复 daemon/bg fast path 缺少 setShellIfWindows() - 修复 checkPathExists 用 existsSync 替代 execSync('dir') - 7 个 spawn 站点迁移到 CliLaunchSpec Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge tsconfig.base.json into tsconfig.json with full compiler options The cherry-pick from637c908dropped jsx/strict/etc settings when removing tsconfig.base.json. This commit restores them in a single tsconfig.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge tsconfig.base.json into tsconfig.json with full compiler options The cherry-pick from637c908dropped jsx/strict/etc settings when removing tsconfig.base.json. This commit restores them in a single tsconfig.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
import { log, error as logError } from "../../logger";
|
|
import { Hono } from "hono";
|
|
import { createBunWebSocket } from "hono/bun";
|
|
import { validateApiKey } from "../../auth/api-key";
|
|
import { verifyWorkerJwt } from "../../auth/jwt";
|
|
import {
|
|
handleWebSocketOpen,
|
|
handleWebSocketMessage,
|
|
handleWebSocketClose,
|
|
ingestBridgeMessage,
|
|
} from "../../transport/ws-handler";
|
|
import { getSession, resolveExistingSessionId } from "../../services/session";
|
|
|
|
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
|
|
|
const app = new Hono();
|
|
|
|
/** Authenticate via API key or worker JWT in Authorization header or ?token= query param */
|
|
function authenticateRequest(c: any, label: string, expectedSessionId?: string): boolean {
|
|
const authHeader = c.req.header("Authorization");
|
|
const queryToken = c.req.query("token");
|
|
const token = authHeader?.replace("Bearer ", "") || queryToken;
|
|
|
|
// Try API key first
|
|
if (validateApiKey(token)) {
|
|
return true;
|
|
}
|
|
|
|
// Try JWT verification — validate session_id matches if provided
|
|
if (token) {
|
|
const payload = verifyWorkerJwt(token);
|
|
if (payload) {
|
|
if (expectedSessionId && payload.session_id !== expectedSessionId) {
|
|
log(`[Auth] ${label}: FAILED — JWT session_id mismatch`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
log(`[Auth] ${label}: FAILED — no valid API key or JWT`);
|
|
return false;
|
|
}
|
|
|
|
/** POST /v2/session_ingress/session/:sessionId/events — HTTP POST (HybridTransport writes) */
|
|
app.post("/session/:sessionId/events", async (c) => {
|
|
const requestedSessionId = c.req.param("sessionId")!;
|
|
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
|
|
|
|
if (!authenticateRequest(c, `POST session/${sessionId}`, sessionId)) {
|
|
return c.json({ error: { type: "unauthorized", message: "Invalid auth" } }, 401);
|
|
}
|
|
|
|
const session = getSession(sessionId);
|
|
if (!session) {
|
|
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
|
}
|
|
|
|
const body = await c.req.json();
|
|
const events = Array.isArray(body.events) ? body.events : [body];
|
|
|
|
let count = 0;
|
|
for (const msg of events) {
|
|
if (!msg || typeof msg !== "object") continue;
|
|
ingestBridgeMessage(sessionId, msg as Record<string, unknown>);
|
|
count++;
|
|
}
|
|
|
|
return c.json({ status: "ok" }, 200);
|
|
});
|
|
|
|
/** WS /v2/session_ingress/ws/:sessionId — WebSocket transport */
|
|
app.get(
|
|
"/ws/:sessionId",
|
|
upgradeWebSocket(async (c) => {
|
|
const requestedSessionId = c.req.param("sessionId")!;
|
|
const sessionId = resolveExistingSessionId(requestedSessionId) ?? requestedSessionId;
|
|
|
|
if (!authenticateRequest(c, `WS ${sessionId}`, sessionId)) {
|
|
return {
|
|
onOpen(_evt, ws) {
|
|
ws.close(4003, "unauthorized");
|
|
},
|
|
};
|
|
}
|
|
|
|
const session = getSession(sessionId);
|
|
if (!session) {
|
|
log(`[WS] Upgrade rejected: session ${sessionId} not found`);
|
|
return {
|
|
onOpen(_evt, ws) {
|
|
ws.close(4001, "session not found");
|
|
},
|
|
};
|
|
}
|
|
|
|
log(`[WS] Upgrade accepted: session=${sessionId}`);
|
|
return {
|
|
onOpen(_evt, ws) {
|
|
handleWebSocketOpen(ws as any, sessionId);
|
|
},
|
|
onMessage(evt, ws) {
|
|
const data =
|
|
typeof evt.data === "string"
|
|
? evt.data
|
|
: new TextDecoder().decode(evt.data as ArrayBuffer);
|
|
handleWebSocketMessage(ws as any, sessionId, data);
|
|
},
|
|
onClose(evt, ws) {
|
|
const closeEvt = evt as unknown as CloseEvent;
|
|
handleWebSocketClose(ws as any, sessionId, closeEvt?.code, closeEvt?.reason);
|
|
},
|
|
onError(evt, ws) {
|
|
logError(`[WS] Error on session=${sessionId}:`, evt);
|
|
handleWebSocketClose(ws as any, sessionId, 1006, "websocket error");
|
|
},
|
|
};
|
|
}),
|
|
);
|
|
|
|
export { websocket };
|
|
export default app;
|