mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
feat: 支持自托管的 remote-control-server (#214)
* feat: 支持自托管的 remote-control-server (#214) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { Hono } from "hono";
|
||||
import { createCodeSession, getSession, incrementEpoch } from "../../services/session";
|
||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||
import { generateWorkerJwt } from "../../auth/jwt";
|
||||
import { getBaseUrl, config } from "../../config";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
/** POST /v1/code/sessions — Create code session (wrapped response for TUI compat) */
|
||||
app.post("/", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||
const body = await c.req.json();
|
||||
const session = createCodeSession(body);
|
||||
return c.json({ session }, 200);
|
||||
});
|
||||
|
||||
/** POST /v1/code/sessions/:id/bridge — Get connection info + worker JWT */
|
||||
app.post("/:id/bridge", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||
const sessionId = c.req.param("id");
|
||||
const session = getSession(sessionId);
|
||||
if (!session) {
|
||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||
}
|
||||
|
||||
const epoch = incrementEpoch(sessionId);
|
||||
const expiresInSeconds = config.jwtExpiresIn;
|
||||
const workerJwt = generateWorkerJwt(sessionId, expiresInSeconds);
|
||||
|
||||
return c.json({
|
||||
api_base_url: getBaseUrl(),
|
||||
worker_epoch: epoch,
|
||||
worker_jwt: workerJwt,
|
||||
expires_in: expiresInSeconds,
|
||||
}, 200);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Hono } from "hono";
|
||||
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||
import { createSSEStream } from "../../transport/sse-writer";
|
||||
import { getSession } from "../../services/session";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
/** SSE /v1/code/sessions/:id/worker/events/stream — SSE event stream */
|
||||
app.get("/:id/worker/events/stream", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||
const sessionId = c.req.param("id");
|
||||
const session = getSession(sessionId);
|
||||
if (!session) {
|
||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||
}
|
||||
|
||||
// Support Last-Event-ID / from_sequence_num for reconnection
|
||||
const lastEventId = c.req.header("Last-Event-ID");
|
||||
const fromSeq = c.req.query("from_sequence_num");
|
||||
const fromSeqNum = fromSeq ? parseInt(fromSeq) : lastEventId ? parseInt(lastEventId) : 0;
|
||||
|
||||
return createSSEStream(c, sessionId, fromSeqNum);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Hono } from "hono";
|
||||
import { sessionIngressAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||
import { publishSessionEvent } from "../../services/transport";
|
||||
import { getSession, updateSessionStatus } from "../../services/session";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
/** POST /v1/code/sessions/:id/worker/events — Write events */
|
||||
app.post("/:id/worker/events", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||
const sessionId = c.req.param("id");
|
||||
const body = await c.req.json();
|
||||
|
||||
const events = Array.isArray(body) ? body : [body];
|
||||
const published = [];
|
||||
for (const evt of events) {
|
||||
const result = publishSessionEvent(sessionId, evt.type || "message", evt, "inbound");
|
||||
published.push(result);
|
||||
}
|
||||
|
||||
return c.json({ status: "ok", count: published.length }, 200);
|
||||
});
|
||||
|
||||
/** PUT /v1/code/sessions/:id/worker/state — Report worker state */
|
||||
app.put("/:id/worker/state", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||
const sessionId = c.req.param("id");
|
||||
const body = await c.req.json();
|
||||
|
||||
if (body.status) {
|
||||
updateSessionStatus(sessionId, body.status);
|
||||
}
|
||||
|
||||
return c.json({ status: "ok" }, 200);
|
||||
});
|
||||
|
||||
/** PUT /v1/code/sessions/:id/worker/external_metadata — Report worker metadata (no-op) */
|
||||
app.put("/:id/worker/external_metadata", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||
// TUI's CCRClient calls this for metadata reporting. Accept and discard.
|
||||
return c.json({ status: "ok" }, 200);
|
||||
});
|
||||
|
||||
/** POST /v1/code/sessions/:id/worker/events/:eventId/delivery — Delivery tracking (no-op) */
|
||||
app.post("/:id/worker/events/:eventId/delivery", acceptCliHeaders, sessionIngressAuth, async (c) => {
|
||||
// TUI's CCRClient reports event delivery status (received/processing/processed).
|
||||
// Accept and discard — event bus doesn't track per-event delivery.
|
||||
return c.json({ status: "ok" }, 200);
|
||||
});
|
||||
|
||||
export default app;
|
||||
19
packages/remote-control-server/src/routes/v2/worker.ts
Normal file
19
packages/remote-control-server/src/routes/v2/worker.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Hono } from "hono";
|
||||
import { getSession, incrementEpoch } from "../../services/session";
|
||||
import { apiKeyAuth, acceptCliHeaders } from "../../auth/middleware";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
/** POST /v1/code/sessions/:id/worker/register — Register worker */
|
||||
app.post("/:id/worker/register", acceptCliHeaders, apiKeyAuth, async (c) => {
|
||||
const sessionId = c.req.param("id");
|
||||
const session = getSession(sessionId);
|
||||
if (!session) {
|
||||
return c.json({ error: { type: "not_found", message: "Session not found" } }, 404);
|
||||
}
|
||||
|
||||
const epoch = incrementEpoch(sessionId);
|
||||
return c.json({ worker_epoch: epoch }, 200);
|
||||
});
|
||||
|
||||
export default app;
|
||||
Reference in New Issue
Block a user