mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Refactor routing and provider integration
This commit is contained in:
parent
87a666df21
commit
528a8e0efa
264 changed files with 4717 additions and 1008 deletions
17
.dockerignore
Normal file
17
.dockerignore
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.git
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
node_modules
|
||||
dist
|
||||
packages/*/dist
|
||||
release
|
||||
release-local
|
||||
logs
|
||||
tmp
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
docs/node_modules
|
||||
docs/dist
|
||||
docs/.astro
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -14,3 +14,7 @@ tmp
|
|||
release-local
|
||||
logs
|
||||
.opencat
|
||||
test-results
|
||||
playwright-report
|
||||
blob-report
|
||||
.tmp
|
||||
54
Dockerfile
Normal file
54
Dockerfile
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
ARG NODE_IMAGE=node:22-bookworm
|
||||
|
||||
FROM ${NODE_IMAGE} AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY packages/cli/package.json packages/cli/package.json
|
||||
COPY packages/core/package.json packages/core/package.json
|
||||
COPY packages/electron/package.json packages/electron/package.json
|
||||
COPY packages/ui/package.json packages/ui/package.json
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build:docker
|
||||
|
||||
FROM ${NODE_IMAGE} AS runtime
|
||||
ENV NODE_ENV=production \
|
||||
CCR_DATA_DIR=/data \
|
||||
CCR_WEB_HOST=127.0.0.1 \
|
||||
CCR_WEB_PORT=3459 \
|
||||
CCR_NGINX_PORT=8080 \
|
||||
CCR_GATEWAY_HOST=127.0.0.1 \
|
||||
CCR_GATEWAY_PORT=3456 \
|
||||
CCR_GATEWAY_CORE_PORT=3457 \
|
||||
CCR_PUBLIC_HOST=127.0.0.1 \
|
||||
CCR_PUBLIC_PORT=3458
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates nginx \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY packages/core/package.json packages/core/package.json
|
||||
RUN npm ci --omit=dev --workspace=@claude-code-router/core --include-workspace-root=false \
|
||||
&& npm cache clean --force
|
||||
|
||||
COPY --from=build /app/packages/core/dist packages/core/dist
|
||||
COPY --from=build /app/packages/ui/dist/renderer /usr/share/nginx/html
|
||||
COPY docker/entrypoint.sh /usr/local/bin/ccr-docker-entrypoint
|
||||
COPY docker/pm2.config.cjs docker/pm2.config.cjs
|
||||
|
||||
RUN chmod +x /usr/local/bin/ccr-docker-entrypoint \
|
||||
&& mkdir -p /data /run/nginx /var/lib/nginx /var/log/nginx
|
||||
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:' + (process.env.CCR_NGINX_PORT || '8080') + '/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
|
||||
ENTRYPOINT ["ccr-docker-entrypoint"]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<h1 align="center">Claude Code Router Desktop</h1>
|
||||
<h1 align="center">Claude Code Router</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<h1 align="center">Claude Code Router Desktop</h1>
|
||||
<h1 align="center">Claude Code Router</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml } from "./esbuild.config.mjs";
|
||||
import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
|
||||
|
||||
const mode = process.argv.includes("--dev") ? "development" : "production";
|
||||
|
||||
|
|
@ -19,4 +19,6 @@ await Promise.all([
|
|||
buildStyles({ minify: mode === "production" })
|
||||
]);
|
||||
|
||||
console.log(`Built Electron app assets in ${mode} mode.`);
|
||||
syncUiRendererToRuntimeDists();
|
||||
|
||||
console.log(`Built monorepo package assets in ${mode} mode.`);
|
||||
|
|
|
|||
210
build/dev.mjs
210
build/dev.mjs
|
|
@ -5,12 +5,14 @@ import { spawn } from "node:child_process";
|
|||
import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
binPath,
|
||||
buildStyles,
|
||||
cleanDist,
|
||||
browserRendererHtmlInput,
|
||||
cliSourceRoot,
|
||||
coreSourceRoot,
|
||||
copyAppAssets,
|
||||
copyBrowserRendererHtml,
|
||||
copyCliRuntimeToElectronDist,
|
||||
copyMarketplacePlugins,
|
||||
copyModelCatalog,
|
||||
copyRendererHtml,
|
||||
|
|
@ -21,12 +23,12 @@ import {
|
|||
createRendererBuildOptions,
|
||||
createTrayRendererBuildOptions,
|
||||
createWebClientBridgeBuildOptions,
|
||||
cssInput,
|
||||
cssOutput,
|
||||
appAssetsInput,
|
||||
modelCatalogInput,
|
||||
projectRoot,
|
||||
rendererRoot,
|
||||
rendererHtmlInput,
|
||||
syncUiRendererToRuntimeDists,
|
||||
trayRendererHtmlInput,
|
||||
watchPlugin
|
||||
} from "./esbuild.config.mjs";
|
||||
|
|
@ -37,7 +39,12 @@ let pendingRestartReasons = [];
|
|||
const watchSignatures = new Map();
|
||||
let shuttingDown = false;
|
||||
const restartDelayMs = 160;
|
||||
const styleBuildDelayMs = 160;
|
||||
const stylePollIntervalMs = 1000;
|
||||
const ignoredSignatureEntries = new Set([".DS_Store"]);
|
||||
let styleBuildTimer = null;
|
||||
let styleBuildInFlight = false;
|
||||
let queuedStyleBuildReason = null;
|
||||
const ready = {
|
||||
browser: false,
|
||||
cli: false,
|
||||
|
|
@ -46,6 +53,32 @@ const ready = {
|
|||
tray: false,
|
||||
webBridge: false
|
||||
};
|
||||
const devTarget = parseDevTarget(process.argv.slice(2));
|
||||
const enabled = {
|
||||
cli: devTarget === "cli" || devTarget === "electron",
|
||||
electron: devTarget === "electron",
|
||||
ui: true
|
||||
};
|
||||
const coreSharedSourceRoot = path.join(coreSourceRoot, "shared");
|
||||
const styleWatchRoots = [rendererRoot, coreSharedSourceRoot].filter((watchRoot) => existsSync(watchRoot));
|
||||
const activeReadyNames = new Set([
|
||||
...(enabled.ui ? ["browser", "renderer", "tray", "webBridge"] : []),
|
||||
...(enabled.cli ? ["cli"] : []),
|
||||
...(enabled.electron ? ["main"] : [])
|
||||
]);
|
||||
|
||||
function parseDevTarget(args) {
|
||||
const target = args[0] ?? "electron";
|
||||
if (target === "--help" || target === "-h") {
|
||||
console.log("Usage: node build/dev.mjs [ui|cli|electron]");
|
||||
process.exit(0);
|
||||
}
|
||||
if (target === "ui" || target === "cli" || target === "electron") {
|
||||
return target;
|
||||
}
|
||||
console.error(`Unknown dev target "${target}". Expected ui, cli, or electron.`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function logDev(message) {
|
||||
console.log(`[dev] ${new Date().toISOString()} ${message}`);
|
||||
|
|
@ -57,6 +90,7 @@ function relativePath(file) {
|
|||
|
||||
function readyState() {
|
||||
return Object.entries(ready)
|
||||
.filter(([name]) => activeReadyNames.has(name))
|
||||
.map(([name, value]) => `${name}:${value ? "ready" : "pending"}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
|
@ -163,15 +197,71 @@ function handleWatchedInput(label, watchedPath, eventType, filename, options, on
|
|||
}
|
||||
|
||||
onChange();
|
||||
if (enabled.electron && options?.restart !== false) {
|
||||
scheduleRestart(reason);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleStyleBuild(reason) {
|
||||
queuedStyleBuildReason = reason;
|
||||
if (styleBuildTimer) {
|
||||
clearTimeout(styleBuildTimer);
|
||||
}
|
||||
styleBuildTimer = setTimeout(() => {
|
||||
styleBuildTimer = null;
|
||||
void rebuildStyles(queuedStyleBuildReason ?? reason);
|
||||
}, styleBuildDelayMs);
|
||||
}
|
||||
|
||||
async function rebuildStyles(reason) {
|
||||
if (styleBuildInFlight) {
|
||||
queuedStyleBuildReason = reason;
|
||||
return;
|
||||
}
|
||||
|
||||
styleBuildInFlight = true;
|
||||
queuedStyleBuildReason = null;
|
||||
try {
|
||||
logDev(`rebuilding styles: ${reason}`);
|
||||
await buildStyles({ minify: false });
|
||||
syncUiRendererToRuntimeDists();
|
||||
if (enabled.electron) {
|
||||
scheduleRestart(`styles rebuilt: ${reason}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logDev(`style rebuild failed: ${message}`);
|
||||
} finally {
|
||||
styleBuildInFlight = false;
|
||||
if (queuedStyleBuildReason) {
|
||||
const queuedReason = queuedStyleBuildReason;
|
||||
queuedStyleBuildReason = null;
|
||||
scheduleStyleBuild(queuedReason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pollStyleWatchRoots() {
|
||||
for (const styleWatchRoot of styleWatchRoots) {
|
||||
const label = `styles ${relativePath(styleWatchRoot)}`;
|
||||
const signature = contentSignature(styleWatchRoot);
|
||||
const previousSignature = watchSignatures.get(label);
|
||||
if (previousSignature === signature.key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
watchSignatures.set(label, signature.key);
|
||||
logDev(`watch event: ${label}; ${signature.summary}; content=changed`);
|
||||
scheduleStyleBuild(label);
|
||||
}
|
||||
}
|
||||
|
||||
function markReady(name, reason = `${name} esbuild completed`) {
|
||||
if (name === "browser" || name === "cli" || name === "main" || name === "renderer" || name === "tray" || name === "webBridge") {
|
||||
ready[name] = true;
|
||||
}
|
||||
logDev(`build ready: ${reason}; ${readyState()}`);
|
||||
if (ready.browser && ready.cli && ready.main && ready.renderer && ready.tray && ready.webBridge) {
|
||||
if (enabled.electron && Array.from(activeReadyNames).every((readyName) => ready[readyName])) {
|
||||
scheduleRestart(reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -221,114 +311,153 @@ function restartElectron() {
|
|||
});
|
||||
}
|
||||
|
||||
logDev("starting dev build");
|
||||
logDev(`starting dev build target=${devTarget} ui=${enabled.ui ? "on" : "off"} cli=${enabled.cli ? "on" : "off"} electron=${enabled.electron ? "on" : "off"}`);
|
||||
cleanDist();
|
||||
if (enabled.electron) {
|
||||
copyAppAssets();
|
||||
}
|
||||
if (enabled.cli || enabled.electron) {
|
||||
copyMarketplacePlugins();
|
||||
copyModelCatalog();
|
||||
}
|
||||
copyBrowserRendererHtml();
|
||||
copyRendererHtml();
|
||||
copyTrayRendererHtml();
|
||||
await buildStyles({ minify: false });
|
||||
|
||||
const tailwindProcess = spawn(binPath("tailwindcss"), ["-i", cssInput, "-o", cssOutput, "--watch"], {
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32"
|
||||
});
|
||||
logDev(`Tailwind watcher started pid=${tailwindProcess.pid ?? "unknown"} input=${relativePath(cssInput)} output=${relativePath(cssOutput)}`);
|
||||
tailwindProcess.on("exit", (code, signal) => {
|
||||
logDev(`Tailwind watcher exited code=${code ?? "null"} signal=${signal ?? "null"}`);
|
||||
});
|
||||
syncUiRendererToRuntimeDists();
|
||||
|
||||
rememberWatchSignature("home html", rendererHtmlInput);
|
||||
rememberWatchSignature("browser html", browserRendererHtmlInput);
|
||||
rememberWatchSignature("tray html", trayRendererHtmlInput);
|
||||
for (const styleWatchRoot of styleWatchRoots) {
|
||||
rememberWatchSignature(`styles ${relativePath(styleWatchRoot)}`, styleWatchRoot);
|
||||
}
|
||||
if (enabled.electron) {
|
||||
rememberWatchSignature("app assets", appAssetsInput);
|
||||
if (existsSync(modelCatalogInput)) {
|
||||
}
|
||||
if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) {
|
||||
rememberWatchSignature("model catalog", modelCatalogInput);
|
||||
}
|
||||
|
||||
const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => {
|
||||
handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, copyRendererHtml);
|
||||
handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, () => {
|
||||
copyRendererHtml();
|
||||
syncUiRendererToRuntimeDists();
|
||||
});
|
||||
});
|
||||
|
||||
const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => {
|
||||
handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, copyBrowserRendererHtml);
|
||||
handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, () => {
|
||||
copyBrowserRendererHtml();
|
||||
syncUiRendererToRuntimeDists();
|
||||
});
|
||||
});
|
||||
|
||||
const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => {
|
||||
handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, copyTrayRendererHtml);
|
||||
handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, () => {
|
||||
copyTrayRendererHtml();
|
||||
syncUiRendererToRuntimeDists();
|
||||
});
|
||||
});
|
||||
|
||||
const appAssetsWatcher = watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
|
||||
const stylePoller = setInterval(pollStyleWatchRoots, stylePollIntervalMs);
|
||||
|
||||
const appAssetsWatcher = enabled.electron
|
||||
? watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
|
||||
handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
|
||||
});
|
||||
})
|
||||
: { close: () => undefined };
|
||||
|
||||
const modelCatalogWatcher = existsSync(modelCatalogInput)
|
||||
const modelCatalogWatcher = (enabled.cli || enabled.electron) && existsSync(modelCatalogInput)
|
||||
? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => {
|
||||
handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog);
|
||||
})
|
||||
: { close: () => undefined };
|
||||
|
||||
const mainContext = await esbuild.context(
|
||||
const contexts = [];
|
||||
|
||||
if (enabled.electron) {
|
||||
contexts.push(
|
||||
await esbuild.context(
|
||||
createMainBuildOptions({
|
||||
mode: "development",
|
||||
plugins: [watchPlugin("main", (name) => markReady(name))]
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const cliContext = await esbuild.context(
|
||||
if (enabled.cli) {
|
||||
contexts.push(
|
||||
await esbuild.context(
|
||||
createCliBuildOptions({
|
||||
mode: "development",
|
||||
plugins: [watchPlugin("cli", (name) => markReady(name))]
|
||||
plugins: [
|
||||
watchPlugin("cli", (name) => {
|
||||
if (enabled.electron) {
|
||||
copyCliRuntimeToElectronDist();
|
||||
}
|
||||
markReady(name);
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const rendererContext = await esbuild.context(
|
||||
if (enabled.ui) {
|
||||
contexts.push(
|
||||
await esbuild.context(
|
||||
createRendererBuildOptions({
|
||||
mode: "development",
|
||||
plugins: [
|
||||
watchPlugin("renderer", (name) => {
|
||||
copyRendererHtml();
|
||||
syncUiRendererToRuntimeDists();
|
||||
markReady(name);
|
||||
})
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
const trayRendererContext = await esbuild.context(
|
||||
),
|
||||
await esbuild.context(
|
||||
createTrayRendererBuildOptions({
|
||||
mode: "development",
|
||||
plugins: [
|
||||
watchPlugin("tray", (name) => {
|
||||
copyTrayRendererHtml();
|
||||
syncUiRendererToRuntimeDists();
|
||||
markReady(name);
|
||||
})
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
const browserRendererContext = await esbuild.context(
|
||||
),
|
||||
await esbuild.context(
|
||||
createBrowserRendererBuildOptions({
|
||||
mode: "development",
|
||||
plugins: [
|
||||
watchPlugin("browser", (name) => {
|
||||
copyBrowserRendererHtml();
|
||||
syncUiRendererToRuntimeDists();
|
||||
markReady(name);
|
||||
})
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
const webClientBridgeContext = await esbuild.context(
|
||||
),
|
||||
await esbuild.context(
|
||||
createWebClientBridgeBuildOptions({
|
||||
mode: "development",
|
||||
plugins: [watchPlugin("webBridge", (name) => markReady(name))]
|
||||
plugins: [
|
||||
watchPlugin("webBridge", (name) => {
|
||||
syncUiRendererToRuntimeDists();
|
||||
markReady(name);
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all([mainContext.watch(), cliContext.watch(), rendererContext.watch(), trayRendererContext.watch(), browserRendererContext.watch(), webClientBridgeContext.watch()]);
|
||||
await Promise.all(contexts.map((context) => context.watch()));
|
||||
logDev("watchers are active");
|
||||
|
||||
async function shutdown() {
|
||||
|
|
@ -340,13 +469,16 @@ async function shutdown() {
|
|||
if (electronProcess) {
|
||||
electronProcess.kill();
|
||||
}
|
||||
tailwindProcess.kill();
|
||||
if (styleBuildTimer) {
|
||||
clearTimeout(styleBuildTimer);
|
||||
}
|
||||
htmlWatcher.close();
|
||||
browserHtmlWatcher.close();
|
||||
trayHtmlWatcher.close();
|
||||
clearInterval(stylePoller);
|
||||
appAssetsWatcher.close();
|
||||
modelCatalogWatcher.close();
|
||||
await Promise.all([mainContext.dispose(), cliContext.dispose(), rendererContext.dispose(), trayRendererContext.dispose(), browserRendererContext.dispose(), webClientBridgeContext.dispose()]);
|
||||
await Promise.all(contexts.map((context) => context.dispose()));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
|
|
|||
37
build/docker-build.mjs
Normal file
37
build/docker-build.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
buildBrowserRenderer,
|
||||
buildCoreServer,
|
||||
buildRenderer,
|
||||
buildStyles,
|
||||
buildTrayRenderer,
|
||||
buildWebClientBridge,
|
||||
cleanDist,
|
||||
copyBrowserRendererHtml,
|
||||
copyMarketplacePlugins,
|
||||
copyModelCatalog,
|
||||
copyRendererHtml,
|
||||
copyTrayRendererHtml,
|
||||
syncUiRendererToRuntimeDists
|
||||
} from "./esbuild.config.mjs";
|
||||
|
||||
const mode = process.argv.includes("--dev") ? "development" : "production";
|
||||
|
||||
cleanDist();
|
||||
copyMarketplacePlugins();
|
||||
copyModelCatalog();
|
||||
copyBrowserRendererHtml();
|
||||
copyRendererHtml();
|
||||
copyTrayRendererHtml();
|
||||
|
||||
await Promise.all([
|
||||
buildCoreServer({ mode }),
|
||||
buildBrowserRenderer({ mode }),
|
||||
buildRenderer({ mode }),
|
||||
buildTrayRenderer({ mode }),
|
||||
buildWebClientBridge({ mode }),
|
||||
buildStyles({ minify: mode === "production" })
|
||||
]);
|
||||
|
||||
syncUiRendererToRuntimeDists();
|
||||
|
||||
console.log(`Built Docker core server and UI assets in ${mode} mode.`);
|
||||
|
|
@ -8,16 +8,43 @@ import { fileURLToPath } from "node:url";
|
|||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const projectRoot = path.resolve(__dirname, "..");
|
||||
export const distDir = path.join(projectRoot, "dist");
|
||||
export const mainOutDir = path.join(distDir, "main");
|
||||
export const rendererOutDir = path.join(distDir, "renderer");
|
||||
export const appAssetsDir = path.join(distDir, "assets");
|
||||
export const packagesRoot = path.join(projectRoot, "packages");
|
||||
export const cliRoot = path.join(packagesRoot, "cli");
|
||||
export const coreRoot = path.join(packagesRoot, "core");
|
||||
export const electronRoot = path.join(packagesRoot, "electron");
|
||||
export const uiRoot = path.join(packagesRoot, "ui");
|
||||
export const cliSourceRoot = path.join(cliRoot, "src");
|
||||
export const coreSourceRoot = path.join(coreRoot, "src");
|
||||
export const electronSourceRoot = path.join(electronRoot, "src");
|
||||
export const uiSourceRoot = path.join(uiRoot, "src");
|
||||
export const legacyDistDir = path.join(projectRoot, "dist");
|
||||
export const cliDistDir = path.join(cliRoot, "dist");
|
||||
export const coreDistDir = path.join(coreRoot, "dist");
|
||||
export const electronDistDir = path.join(electronRoot, "dist");
|
||||
export const uiDistDir = path.join(uiRoot, "dist");
|
||||
export const distDir = electronDistDir;
|
||||
export const cliMainOutDir = path.join(cliDistDir, "main");
|
||||
export const coreMainOutDir = path.join(coreDistDir, "main");
|
||||
export const electronMainOutDir = path.join(electronDistDir, "main");
|
||||
export const mainOutDir = electronMainOutDir;
|
||||
export const rendererOutDir = path.join(uiDistDir, "renderer");
|
||||
export const cliRendererOutDir = path.join(cliDistDir, "renderer");
|
||||
export const coreRendererOutDir = path.join(coreDistDir, "renderer");
|
||||
export const electronRendererOutDir = path.join(electronDistDir, "renderer");
|
||||
export const runtimeRendererOutDirs = [cliRendererOutDir, coreRendererOutDir, electronRendererOutDir];
|
||||
export const appAssetsDir = path.join(electronDistDir, "assets");
|
||||
export const rendererAssetsDir = path.join(rendererOutDir, "assets");
|
||||
export const marketplacePluginsDir = path.join(distDir, "marketplace", "plugins");
|
||||
export const appAssetsInput = path.join(projectRoot, "assets");
|
||||
export const modelCatalogInput = path.join(projectRoot, "models.json");
|
||||
export const modelCatalogOutput = path.join(distDir, "models.json");
|
||||
export const rendererRoot = path.join(projectRoot, "src", "renderer");
|
||||
export const cliMarketplacePluginsDir = path.join(cliDistDir, "marketplace", "plugins");
|
||||
export const coreMarketplacePluginsDir = path.join(coreDistDir, "marketplace", "plugins");
|
||||
export const electronMarketplacePluginsDir = path.join(electronDistDir, "marketplace", "plugins");
|
||||
export const marketplacePluginsDir = electronMarketplacePluginsDir;
|
||||
export const appAssetsInput = path.join(electronRoot, "assets");
|
||||
export const modelCatalogInput = path.join(coreRoot, "models.json");
|
||||
export const cliModelCatalogOutput = path.join(cliDistDir, "models.json");
|
||||
export const coreModelCatalogOutput = path.join(coreDistDir, "models.json");
|
||||
export const electronModelCatalogOutput = path.join(electronDistDir, "models.json");
|
||||
export const modelCatalogOutput = electronModelCatalogOutput;
|
||||
export const rendererRoot = uiSourceRoot;
|
||||
export const rendererHtmlInput = path.join(rendererRoot, "pages", "home", "index.html");
|
||||
export const rendererHtmlOutput = path.join(rendererOutDir, "pages", "home", "index.html");
|
||||
export const browserRendererHtmlInput = path.join(rendererRoot, "pages", "browser", "index.html");
|
||||
|
|
@ -30,8 +57,10 @@ export const webClientBridgeOutput = path.join(rendererAssetsDir, "web-client-br
|
|||
const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"];
|
||||
const lightweightMcpBundleMaxBytes = 128 * 1024;
|
||||
const forbiddenLightweightMcpInputs = [
|
||||
{ prefix: "src/main/", reason: "main-process modules can pull in config, Electron, or native storage side effects" },
|
||||
{ prefix: "src/renderer/", reason: "renderer modules do not belong in stdio MCP subprocesses" },
|
||||
{ prefix: "packages/core/src/config/", reason: "config modules can pull in native storage side effects" },
|
||||
{ prefix: "packages/core/src/storage/", reason: "native SQLite storage is not allowed in lightweight MCP subprocesses" },
|
||||
{ prefix: "packages/electron/src/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" },
|
||||
{ prefix: "packages/ui/src/", reason: "UI modules do not belong in stdio MCP subprocesses" },
|
||||
{ prefix: "node_modules/better-sqlite3/", reason: "native SQLite is not allowed in lightweight MCP subprocesses" },
|
||||
{ prefix: "node_modules/electron/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" }
|
||||
];
|
||||
|
|
@ -45,15 +74,26 @@ const nodeExternals = [
|
|||
];
|
||||
|
||||
export function cleanDist() {
|
||||
rmSync(distDir, { force: true, recursive: true });
|
||||
rmSync(legacyDistDir, { force: true, recursive: true });
|
||||
rmSync(cliDistDir, { force: true, recursive: true });
|
||||
rmSync(coreDistDir, { force: true, recursive: true });
|
||||
rmSync(electronDistDir, { force: true, recursive: true });
|
||||
rmSync(uiDistDir, { force: true, recursive: true });
|
||||
ensureDist();
|
||||
}
|
||||
|
||||
export function ensureDist() {
|
||||
mkdirSync(mainOutDir, { recursive: true });
|
||||
mkdirSync(cliMainOutDir, { recursive: true });
|
||||
mkdirSync(coreMainOutDir, { recursive: true });
|
||||
mkdirSync(electronMainOutDir, { recursive: true });
|
||||
mkdirSync(appAssetsDir, { recursive: true });
|
||||
mkdirSync(marketplacePluginsDir, { recursive: true });
|
||||
mkdirSync(cliMarketplacePluginsDir, { recursive: true });
|
||||
mkdirSync(coreMarketplacePluginsDir, { recursive: true });
|
||||
mkdirSync(electronMarketplacePluginsDir, { recursive: true });
|
||||
mkdirSync(rendererAssetsDir, { recursive: true });
|
||||
for (const outputDir of runtimeRendererOutDirs) {
|
||||
mkdirSync(path.join(outputDir, "assets"), { recursive: true });
|
||||
}
|
||||
mkdirSync(path.dirname(rendererHtmlOutput), { recursive: true });
|
||||
mkdirSync(path.dirname(browserRendererHtmlOutput), { recursive: true });
|
||||
mkdirSync(path.dirname(trayRendererHtmlOutput), { recursive: true });
|
||||
|
|
@ -69,12 +109,16 @@ export function copyAppAssets() {
|
|||
export function copyModelCatalog() {
|
||||
ensureDist();
|
||||
if (existsSync(modelCatalogInput)) {
|
||||
cpSync(modelCatalogInput, modelCatalogOutput);
|
||||
cpSync(modelCatalogInput, cliModelCatalogOutput);
|
||||
cpSync(modelCatalogInput, coreModelCatalogOutput);
|
||||
cpSync(modelCatalogInput, electronModelCatalogOutput);
|
||||
}
|
||||
}
|
||||
|
||||
export function copyRendererHtml() {
|
||||
copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js");
|
||||
copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js", {
|
||||
beforeModuleScriptTags: [' <script src="../../assets/web-client-bridge.js"></script>']
|
||||
});
|
||||
}
|
||||
|
||||
export function copyTrayRendererHtml() {
|
||||
|
|
@ -89,14 +133,25 @@ export function copyMarketplacePlugins() {
|
|||
ensureDist();
|
||||
for (const filename of ["claude-design-plugin.cjs", "cursor-proxy-plugin.cjs"]) {
|
||||
const source = path.join(projectRoot, "examples", "plugins", filename);
|
||||
const target = path.join(marketplacePluginsDir, filename);
|
||||
if (existsSync(source)) {
|
||||
cpSync(source, target);
|
||||
cpSync(source, path.join(cliMarketplacePluginsDir, filename));
|
||||
cpSync(source, path.join(coreMarketplacePluginsDir, filename));
|
||||
cpSync(source, path.join(electronMarketplacePluginsDir, filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyRendererPageHtml(input, output, scriptName) {
|
||||
export function syncUiRendererToRuntimeDists() {
|
||||
ensureDist();
|
||||
for (const outputDir of runtimeRendererOutDirs) {
|
||||
rmSync(outputDir, { force: true, recursive: true });
|
||||
if (existsSync(rendererOutDir)) {
|
||||
cpSync(rendererOutDir, outputDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyRendererPageHtml(input, output, scriptName, options = {}) {
|
||||
ensureDist();
|
||||
const source = readFileSync(input, "utf8");
|
||||
const styleTag = ' <link rel="stylesheet" href="../../assets/main.css" />';
|
||||
|
|
@ -105,6 +160,12 @@ function copyRendererPageHtml(input, output, scriptName) {
|
|||
? source.replace(' <script type="module" src="./main.tsx"></script>', scriptTag)
|
||||
: source.replace("</body>", `${scriptTag}\n </body>`);
|
||||
|
||||
for (const extraScriptTag of options.beforeModuleScriptTags ?? []) {
|
||||
if (!hasScriptTag(html, extraScriptTag)) {
|
||||
html = html.replace(scriptTag, `${extraScriptTag}\n${scriptTag}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!html.includes('href="../../assets/main.css"')) {
|
||||
html = html.replace("</head>", `${styleTag}\n </head>`);
|
||||
}
|
||||
|
|
@ -112,18 +173,23 @@ function copyRendererPageHtml(input, output, scriptName) {
|
|||
writeFileSync(output, html, "utf8");
|
||||
}
|
||||
|
||||
function hasScriptTag(html, scriptTag) {
|
||||
const sourceMatch = scriptTag.match(/\bsrc="([^"]+)"/);
|
||||
return sourceMatch ? html.includes(sourceMatch[1]) : html.includes(scriptTag);
|
||||
}
|
||||
|
||||
export function createMainBuildOptions({ mode = "production", plugins = [] } = {}) {
|
||||
return {
|
||||
absWorkingDir: projectRoot,
|
||||
bundle: true,
|
||||
entryNames: "[name]",
|
||||
entryPoints: [
|
||||
path.join(projectRoot, "src", "main", "main.ts"),
|
||||
path.join(projectRoot, "src", "main", "browser-preload.ts"),
|
||||
path.join(projectRoot, "src", "server", "mcp", "browser-web-search-proxy-mcp.ts"),
|
||||
path.join(projectRoot, "src", "server", "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(projectRoot, "src", "server", "mcp", "fusion-tool-fallback-mcp.ts"),
|
||||
path.join(projectRoot, "src", "main", "preload.ts")
|
||||
path.join(electronSourceRoot, "main", "main.ts"),
|
||||
path.join(electronSourceRoot, "main", "browser-preload.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "browser-web-search-proxy-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
|
||||
path.join(electronSourceRoot, "main", "preload.ts")
|
||||
],
|
||||
external: nodeExternals,
|
||||
format: "cjs",
|
||||
|
|
@ -131,9 +197,9 @@ export function createMainBuildOptions({ mode = "production", plugins = [] } = {
|
|||
logLevel: "info",
|
||||
metafile: true,
|
||||
minify: mode === "production",
|
||||
outdir: mainOutDir,
|
||||
outdir: electronMainOutDir,
|
||||
platform: "node",
|
||||
plugins,
|
||||
plugins: [packageAliasPlugin(), ...plugins],
|
||||
sourcemap: mode !== "production",
|
||||
target: "node22"
|
||||
};
|
||||
|
|
@ -144,15 +210,42 @@ export function createCliBuildOptions({ mode = "production", plugins = [] } = {}
|
|||
absWorkingDir: projectRoot,
|
||||
bundle: true,
|
||||
entryNames: "[name]",
|
||||
entryPoints: [path.join(projectRoot, "src", "main", "cli.ts")],
|
||||
entryPoints: [
|
||||
path.join(cliSourceRoot, "cli.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts")
|
||||
],
|
||||
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
|
||||
format: "cjs",
|
||||
legalComments: "none",
|
||||
logLevel: "info",
|
||||
minify: mode === "production",
|
||||
outdir: mainOutDir,
|
||||
outdir: cliMainOutDir,
|
||||
platform: "node",
|
||||
plugins: [forbidCliElectronPlugin(), ...plugins],
|
||||
plugins: [forbidCliElectronPlugin(), packageAliasPlugin(), ...plugins],
|
||||
sourcemap: mode !== "production",
|
||||
target: "node22"
|
||||
};
|
||||
}
|
||||
|
||||
export function createCoreServerBuildOptions({ mode = "production", plugins = [] } = {}) {
|
||||
return {
|
||||
absWorkingDir: projectRoot,
|
||||
bundle: true,
|
||||
entryNames: "[name]",
|
||||
entryPoints: [
|
||||
path.join(coreSourceRoot, "entrypoints", "server.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts")
|
||||
],
|
||||
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
|
||||
format: "cjs",
|
||||
legalComments: "none",
|
||||
logLevel: "info",
|
||||
minify: mode === "production",
|
||||
outdir: coreMainOutDir,
|
||||
platform: "node",
|
||||
plugins: [forbidCliElectronPlugin(), packageAliasPlugin(), ...plugins],
|
||||
sourcemap: mode !== "production",
|
||||
target: "node22"
|
||||
};
|
||||
|
|
@ -183,7 +276,7 @@ export function createRendererBuildOptions({ mode = "production", plugins = [] }
|
|||
minify: mode === "production",
|
||||
outfile: path.join(rendererAssetsDir, "main.js"),
|
||||
platform: "browser",
|
||||
plugins: [rendererAliasPlugin(), ...plugins],
|
||||
plugins: [rendererAliasPlugin(), packageAliasPlugin(), ...plugins],
|
||||
publicPath: "../../assets",
|
||||
sourcemap: mode !== "production",
|
||||
target: "chrome120"
|
||||
|
|
@ -210,14 +303,14 @@ export function createWebClientBridgeBuildOptions({ mode = "production", plugins
|
|||
return {
|
||||
absWorkingDir: projectRoot,
|
||||
bundle: true,
|
||||
entryPoints: [path.join(projectRoot, "src", "main", "web-client-bridge.ts")],
|
||||
entryPoints: [path.join(uiSourceRoot, "web-client-bridge.ts")],
|
||||
format: "iife",
|
||||
legalComments: "none",
|
||||
logLevel: "info",
|
||||
minify: mode === "production",
|
||||
outfile: webClientBridgeOutput,
|
||||
platform: "browser",
|
||||
plugins,
|
||||
plugins: [packageAliasPlugin(), ...plugins],
|
||||
sourcemap: mode !== "production",
|
||||
target: "chrome120"
|
||||
};
|
||||
|
|
@ -239,11 +332,21 @@ export function watchPlugin(name, onEnd) {
|
|||
export async function buildMain(options = {}) {
|
||||
const [mainBuildResult] = await Promise.all([
|
||||
esbuild.build(createMainBuildOptions(options)),
|
||||
esbuild.build(createCliBuildOptions(options))
|
||||
buildCoreServer(options),
|
||||
buildCli(options)
|
||||
]);
|
||||
copyCliRuntimeToElectronDist();
|
||||
validateLightweightMcpBundles(mainBuildResult.metafile);
|
||||
}
|
||||
|
||||
export async function buildCli(options = {}) {
|
||||
await esbuild.build(createCliBuildOptions(options));
|
||||
}
|
||||
|
||||
export async function buildCoreServer(options = {}) {
|
||||
await esbuild.build(createCoreServerBuildOptions(options));
|
||||
}
|
||||
|
||||
export async function buildRenderer(options = {}) {
|
||||
await esbuild.build(createRendererBuildOptions(options));
|
||||
}
|
||||
|
|
@ -260,6 +363,14 @@ export async function buildWebClientBridge(options = {}) {
|
|||
await esbuild.build(createWebClientBridgeBuildOptions(options));
|
||||
}
|
||||
|
||||
export function copyCliRuntimeToElectronDist() {
|
||||
ensureDist();
|
||||
const cliRuntime = path.join(cliMainOutDir, "cli.js");
|
||||
if (existsSync(cliRuntime)) {
|
||||
cpSync(cliRuntime, path.join(electronMainOutDir, "cli.js"));
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildStyles({ minify = false } = {}) {
|
||||
ensureDist();
|
||||
const args = ["-i", cssInput, "-o", cssOutput];
|
||||
|
|
@ -305,6 +416,26 @@ function rendererAliasPlugin() {
|
|||
};
|
||||
}
|
||||
|
||||
function packageAliasPlugin() {
|
||||
return {
|
||||
name: "ccr-package-alias",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^@ccr\/cli\// }, (args) => {
|
||||
return { path: resolvePackageImport(cliSourceRoot, args.path.slice("@ccr/cli/".length)) };
|
||||
});
|
||||
build.onResolve({ filter: /^@ccr\/core\// }, (args) => {
|
||||
return { path: resolvePackageImport(coreSourceRoot, args.path.slice("@ccr/core/".length)) };
|
||||
});
|
||||
build.onResolve({ filter: /^@ccr\/electron\// }, (args) => {
|
||||
return { path: resolvePackageImport(electronSourceRoot, args.path.slice("@ccr/electron/".length)) };
|
||||
});
|
||||
build.onResolve({ filter: /^@ccr\/ui\// }, (args) => {
|
||||
return { path: resolvePackageImport(uiSourceRoot, args.path.slice("@ccr/ui/".length)) };
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function forbidCliElectronPlugin() {
|
||||
return {
|
||||
name: "forbid-cli-electron",
|
||||
|
|
@ -371,19 +502,23 @@ function normalizeBuildPath(value) {
|
|||
}
|
||||
|
||||
function resolveRendererImport(importPath) {
|
||||
const basePath = path.resolve(rendererRoot, importPath);
|
||||
return resolvePackageImport(rendererRoot, importPath);
|
||||
}
|
||||
|
||||
function resolvePackageImport(rootDir, importPath) {
|
||||
const packageBasePath = path.resolve(rootDir, importPath);
|
||||
const candidates = [
|
||||
basePath,
|
||||
`${basePath}.tsx`,
|
||||
`${basePath}.ts`,
|
||||
`${basePath}.jsx`,
|
||||
`${basePath}.js`,
|
||||
`${basePath}.json`,
|
||||
`${basePath}.css`,
|
||||
path.join(basePath, "index.tsx"),
|
||||
path.join(basePath, "index.ts"),
|
||||
path.join(basePath, "index.jsx"),
|
||||
path.join(basePath, "index.js")
|
||||
packageBasePath,
|
||||
`${packageBasePath}.tsx`,
|
||||
`${packageBasePath}.ts`,
|
||||
`${packageBasePath}.jsx`,
|
||||
`${packageBasePath}.js`,
|
||||
`${packageBasePath}.json`,
|
||||
`${packageBasePath}.css`,
|
||||
path.join(packageBasePath, "index.tsx"),
|
||||
path.join(packageBasePath, "index.ts"),
|
||||
path.join(packageBasePath, "index.jsx"),
|
||||
path.join(packageBasePath, "index.js")
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
|
|
@ -392,5 +527,5 @@ function resolveRendererImport(importPath) {
|
|||
}
|
||||
}
|
||||
|
||||
return basePath;
|
||||
return packageBasePath;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import { fileURLToPath } from "node:url";
|
|||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(__dirname, "..");
|
||||
const testsOutDir = path.join(projectRoot, "dist", "tests");
|
||||
const rendererRoot = path.join(projectRoot, "src", "renderer");
|
||||
const rendererRoot = path.join(projectRoot, "packages", "ui", "src");
|
||||
const cliSourceRoot = path.join(projectRoot, "packages", "cli", "src");
|
||||
const coreSourceRoot = path.join(projectRoot, "packages", "core", "src");
|
||||
const testSuites = [
|
||||
{ name: "main", testDir: path.join(projectRoot, "tests", "main") },
|
||||
{ name: "renderer", testDir: path.join(projectRoot, "tests", "renderer") }
|
||||
|
|
@ -53,7 +55,7 @@ for (const suite of selectedSuites) {
|
|||
logLevel: "info",
|
||||
outdir: path.join(testsOutDir, suite.name),
|
||||
platform: "node",
|
||||
plugins: [rendererAliasPlugin()],
|
||||
plugins: [rendererAliasPlugin(), packageAliasPlugin()],
|
||||
target: "node22"
|
||||
});
|
||||
}
|
||||
|
|
@ -87,8 +89,26 @@ function rendererAliasPlugin() {
|
|||
};
|
||||
}
|
||||
|
||||
function packageAliasPlugin() {
|
||||
return {
|
||||
name: "test-package-alias",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^@ccr\/cli\// }, (args) => {
|
||||
return { path: resolvePackageImport(cliSourceRoot, args.path.slice("@ccr/cli/".length)) };
|
||||
});
|
||||
build.onResolve({ filter: /^@ccr\/core\// }, (args) => {
|
||||
return { path: resolvePackageImport(coreSourceRoot, args.path.slice("@ccr/core/".length)) };
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveRendererImport(importPath) {
|
||||
const basePath = path.resolve(rendererRoot, importPath);
|
||||
return resolvePackageImport(rendererRoot, importPath);
|
||||
}
|
||||
|
||||
function resolvePackageImport(rootDir, importPath) {
|
||||
const basePath = path.resolve(rootDir, importPath);
|
||||
const candidates = [
|
||||
basePath,
|
||||
`${basePath}.tsx`,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/renderer/styles/globals.css",
|
||||
"css": "packages/ui/src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
|
|
|
|||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
services:
|
||||
ccr:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: claude-code-router:local
|
||||
environment:
|
||||
CCR_WEB_HOST: 127.0.0.1
|
||||
CCR_WEB_PORT: 3459
|
||||
CCR_NGINX_PORT: 8080
|
||||
CCR_GATEWAY_HOST: 127.0.0.1
|
||||
CCR_GATEWAY_PORT: 3456
|
||||
CCR_GATEWAY_CORE_PORT: 3457
|
||||
CCR_PUBLIC_HOST: 127.0.0.1
|
||||
CCR_PUBLIC_PORT: 3458
|
||||
ports:
|
||||
- "3458:8080"
|
||||
volumes:
|
||||
- ccr-data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
ccr-data:
|
||||
74
docker/README.md
Normal file
74
docker/README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Docker deployment
|
||||
|
||||
This image runs the core server package with PM2 and serves the built UI package
|
||||
through Nginx. Nginx is the only published entrypoint: it serves the UI, proxies
|
||||
management API calls to the internal core server, and proxies gateway API calls
|
||||
to the internal gateway listener.
|
||||
|
||||
## Build and run
|
||||
|
||||
```sh
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Then open:
|
||||
|
||||
- Web UI: <http://localhost:3458>
|
||||
- Gateway endpoint: <http://localhost:3458>
|
||||
|
||||
The container stores config and SQLite databases under `/data`, backed by the
|
||||
`ccr-data` volume in `docker-compose.yml`.
|
||||
|
||||
On a fresh data volume, the Web UI starts immediately. The gateway port is
|
||||
available through the same Nginx port, but the gateway only starts after at
|
||||
least one provider and model are configured.
|
||||
|
||||
## Image scripts
|
||||
|
||||
```sh
|
||||
npm run docker:build
|
||||
npm run docker:run
|
||||
```
|
||||
|
||||
## Smoke test
|
||||
|
||||
```sh
|
||||
npm run test:docker
|
||||
```
|
||||
|
||||
The smoke test builds the image, starts an isolated temporary container with a
|
||||
special-character `CCR_WEB_AUTH_TOKEN`, verifies that only the Nginx port is
|
||||
published, checks UI and RPC authentication, confirms legacy Docker config is
|
||||
migrated to the public Nginx router endpoint, and removes its temporary
|
||||
container and volume. Set `CCR_DOCKER_TEST_SKIP_BUILD=1` to reuse an already
|
||||
built image.
|
||||
|
||||
The Dockerfile defaults to `node:22-bookworm` for reliable native SQLite
|
||||
installation. To use a different Node base image:
|
||||
|
||||
```sh
|
||||
docker build --build-arg NODE_IMAGE=node:22-bookworm-slim -t claude-code-router:local .
|
||||
```
|
||||
|
||||
## Environment
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `CCR_WEB_HOST` | `127.0.0.1` | Internal core server bind host. |
|
||||
| `CCR_WEB_PORT` | `3459` | Internal core server port. |
|
||||
| `CCR_NGINX_PORT` | `8080` | Nginx port that serves UI and proxies management/gateway requests. |
|
||||
| `CCR_WEB_AUTH_TOKEN` | generated | Shared management UI token used by Nginx redirects and the core server. |
|
||||
| `CCR_GATEWAY_HOST` | `127.0.0.1` | Internal gateway bind host. |
|
||||
| `CCR_GATEWAY_PORT` | `3456` | Internal gateway port proxied by Nginx. |
|
||||
| `CCR_GATEWAY_CORE_PORT` | `3457` | Internal core gateway port used for first-run config. |
|
||||
| `CCR_PUBLIC_HOST` | `127.0.0.1` | Host used for the first-run public router endpoint. |
|
||||
| `CCR_PUBLIC_PORT` | `3458` | Port used for the first-run public router endpoint. |
|
||||
| `CCR_PUBLIC_BASE_URL` | `http://127.0.0.1:3458` | Full public router endpoint override. |
|
||||
| `CCR_DATA_DIR` | `/data` | Container data root. |
|
||||
| `CCR_NO_GATEWAY` | `0` | Set to `1` to run only the Web UI management service. |
|
||||
| `CCR_DOCKER_INIT_CONFIG` | `1` | Set to `0` to disable first-run `config.json` bootstrap. |
|
||||
| `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | Sync existing Docker config to the Nginx public router endpoint on startup. |
|
||||
|
||||
The first-run bootstrap writes a minimal legacy `config.json` only when neither
|
||||
`config.json` nor `config.sqlite` exists in the mounted data directory. Once the
|
||||
UI saves settings into SQLite, existing persisted configuration takes priority.
|
||||
207
docker/entrypoint.sh
Normal file
207
docker/entrypoint.sh
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CCR_DATA_DIR="${CCR_DATA_DIR:-/data}"
|
||||
CCR_WEB_HOST="${CCR_WEB_HOST:-127.0.0.1}"
|
||||
CCR_WEB_PORT="${CCR_WEB_PORT:-3459}"
|
||||
CCR_NGINX_PORT="${CCR_NGINX_PORT:-8080}"
|
||||
CCR_GATEWAY_HOST="${CCR_GATEWAY_HOST:-127.0.0.1}"
|
||||
CCR_GATEWAY_PORT="${CCR_GATEWAY_PORT:-3456}"
|
||||
CCR_GATEWAY_CORE_PORT="${CCR_GATEWAY_CORE_PORT:-3457}"
|
||||
CCR_PUBLIC_HOST="${CCR_PUBLIC_HOST:-127.0.0.1}"
|
||||
CCR_PUBLIC_PORT="${CCR_PUBLIC_PORT:-3458}"
|
||||
CCR_PUBLIC_BASE_URL="${CCR_PUBLIC_BASE_URL:-http://${CCR_PUBLIC_HOST}:${CCR_PUBLIC_PORT}}"
|
||||
CCR_NO_GATEWAY="${CCR_NO_GATEWAY:-0}"
|
||||
|
||||
if [ -z "${CCR_WEB_AUTH_TOKEN:-}" ]; then
|
||||
CCR_WEB_AUTH_TOKEN="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")"
|
||||
fi
|
||||
CCR_WEB_AUTH_TOKEN_QUERY="$(node -e "process.stdout.write(encodeURIComponent(process.argv[1] || ''))" "${CCR_WEB_AUTH_TOKEN}")"
|
||||
|
||||
export HOME="${CCR_DATA_DIR}"
|
||||
export CCR_DATA_DIR
|
||||
export CCR_GATEWAY_CORE_PORT
|
||||
export CCR_GATEWAY_HOST
|
||||
export CCR_GATEWAY_PORT
|
||||
export CCR_NGINX_PORT
|
||||
export CCR_NO_GATEWAY
|
||||
export CCR_PUBLIC_BASE_URL
|
||||
export CCR_PUBLIC_HOST
|
||||
export CCR_PUBLIC_PORT
|
||||
export CCR_WEB_AUTH_TOKEN
|
||||
export CCR_WEB_AUTH_TOKEN_QUERY
|
||||
export CCR_WEB_HOST
|
||||
export CCR_WEB_PORT
|
||||
|
||||
CONFIG_DIR="${HOME}/.claude-code-router"
|
||||
CONFIG_FILE="${CONFIG_DIR}/config.json"
|
||||
APP_CONFIG_DB_FILE="${CONFIG_DIR}/config.sqlite"
|
||||
|
||||
mkdir -p "${CONFIG_DIR}" "${CONFIG_DIR}/app-data" /run/nginx /var/lib/nginx /var/log/nginx
|
||||
|
||||
if [ "${CCR_DOCKER_INIT_CONFIG:-1}" != "0" ] && [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${APP_CONFIG_DB_FILE}" ]; then
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const configDir = path.join(process.env.HOME, ".claude-code-router");
|
||||
const configFile = path.join(configDir, "config.json");
|
||||
const gatewayHost = process.env.CCR_GATEWAY_HOST || "0.0.0.0";
|
||||
const gatewayPort = Number(process.env.CCR_GATEWAY_PORT || "3456");
|
||||
const gatewayCorePort = Number(process.env.CCR_GATEWAY_CORE_PORT || "3457");
|
||||
const publicBaseUrl = (process.env.CCR_PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.CCR_PUBLIC_PORT || "3458"}`).replace(/\/+$/, "");
|
||||
|
||||
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(configFile, `${JSON.stringify({
|
||||
HOST: gatewayHost,
|
||||
PORT: gatewayPort,
|
||||
gateway: {
|
||||
coreHost: "127.0.0.1",
|
||||
corePort: gatewayCorePort,
|
||||
enabled: true,
|
||||
host: gatewayHost,
|
||||
port: gatewayPort
|
||||
},
|
||||
routerEndpoint: publicBaseUrl
|
||||
}, null, 2)}\n`, { mode: 0o600 });
|
||||
NODE
|
||||
fi
|
||||
|
||||
if [ "${CCR_DOCKER_SYNC_PUBLIC_ENDPOINT:-1}" != "0" ]; then
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const configDir = path.join(process.env.HOME, ".claude-code-router");
|
||||
const configFile = path.join(configDir, "config.json");
|
||||
const appConfigDbFile = path.join(configDir, "config.sqlite");
|
||||
const gatewayHost = process.env.CCR_GATEWAY_HOST || "127.0.0.1";
|
||||
const gatewayPort = Number(process.env.CCR_GATEWAY_PORT || "3456");
|
||||
const gatewayCorePort = Number(process.env.CCR_GATEWAY_CORE_PORT || "3457");
|
||||
const publicBaseUrl = (process.env.CCR_PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.CCR_PUBLIC_PORT || "3458"}`).replace(/\/+$/, "");
|
||||
|
||||
function syncConfig(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
value.HOST = gatewayHost;
|
||||
value.PORT = gatewayPort;
|
||||
value.gateway = {
|
||||
...(value.gateway && typeof value.gateway === "object" && !Array.isArray(value.gateway) ? value.gateway : {}),
|
||||
coreHost: "127.0.0.1",
|
||||
corePort: gatewayCorePort,
|
||||
enabled: true,
|
||||
host: gatewayHost,
|
||||
port: gatewayPort
|
||||
};
|
||||
value.routerEndpoint = publicBaseUrl;
|
||||
return value;
|
||||
}
|
||||
|
||||
function syncJsonFile() {
|
||||
if (!fs.existsSync(configFile)) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(fs.readFileSync(configFile, "utf8"));
|
||||
fs.writeFileSync(configFile, `${JSON.stringify(syncConfig(parsed), null, 2)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
function syncSqliteConfig() {
|
||||
if (!fs.existsSync(appConfigDbFile)) {
|
||||
return;
|
||||
}
|
||||
let Database;
|
||||
try {
|
||||
Database = require("better-sqlite3");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const db = new Database(appConfigDbFile);
|
||||
try {
|
||||
const row = db.prepare("select value_json from app_config where key = ?").get("default");
|
||||
if (!row?.value_json) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(row.value_json);
|
||||
db.prepare("update app_config set value_json = ?, updated_at = ? where key = ?")
|
||||
.run(JSON.stringify(syncConfig(parsed)), new Date().toISOString(), "default");
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
syncJsonFile();
|
||||
syncSqliteConfig();
|
||||
NODE
|
||||
fi
|
||||
|
||||
cat > /etc/nginx/conf.d/default.conf <<EOF
|
||||
server {
|
||||
listen ${CCR_NGINX_PORT};
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index pages/home/index.html;
|
||||
absolute_redirect off;
|
||||
|
||||
client_max_body_size 8m;
|
||||
|
||||
location = / {
|
||||
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
|
||||
}
|
||||
|
||||
location = /pages/home/index.html {
|
||||
if (\$arg_ccr_web_token = "") {
|
||||
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
|
||||
}
|
||||
try_files /pages/home/index.html =404;
|
||||
}
|
||||
|
||||
location = /api/ccr/rpc {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host ${CCR_WEB_HOST}:${CCR_WEB_PORT};
|
||||
proxy_set_header Origin http://${CCR_WEB_HOST}:${CCR_WEB_PORT};
|
||||
proxy_set_header Referer http://${CCR_WEB_HOST}:${CCR_WEB_PORT}/;
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host \$host;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_pass http://${CCR_WEB_HOST}:${CCR_WEB_PORT};
|
||||
}
|
||||
|
||||
location = /health {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host ${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host \$host;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_pass http://${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
|
||||
}
|
||||
|
||||
location ~ ^/(v1|v1beta|mcp|messages|chat/completions|responses|interactions)(/|$) {
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host ${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
|
||||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host \$host;
|
||||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||||
proxy_pass http://${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /pages/home/index.html;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
if [ -x /app/node_modules/.bin/pm2-runtime ]; then
|
||||
exec /app/node_modules/.bin/pm2-runtime docker/pm2.config.cjs
|
||||
fi
|
||||
|
||||
exec /app/packages/core/node_modules/.bin/pm2-runtime docker/pm2.config.cjs
|
||||
35
docker/pm2.config.cjs
Normal file
35
docker/pm2.config.cjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const noGateway = /^(1|true|yes)$/i.test(process.env.CCR_NO_GATEWAY || "");
|
||||
const serverArgs = [
|
||||
"--host",
|
||||
process.env.CCR_WEB_HOST || "127.0.0.1",
|
||||
"--port",
|
||||
process.env.CCR_WEB_PORT || "3459",
|
||||
"--no-open"
|
||||
];
|
||||
|
||||
if (noGateway) {
|
||||
serverArgs.push("--no-gateway");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "ccr-core-server",
|
||||
script: "/app/packages/core/dist/main/server.js",
|
||||
args: serverArgs,
|
||||
cwd: "/app",
|
||||
interpreter: "node",
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "ccr-nginx",
|
||||
script: "/usr/sbin/nginx",
|
||||
args: ["-g", "daemon off;"],
|
||||
cwd: "/app",
|
||||
interpreter: "none"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
"packages/electron/dist",
|
||||
"package.json"
|
||||
],
|
||||
"mac": {
|
||||
|
|
|
|||
1568
package-lock.json
generated
1568
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "claude-code-router",
|
||||
"name": "claude-code-router-monorepo",
|
||||
"version": "3.0.7",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"description": "Local Claude Code Router gateway with CLI and web management UI.",
|
||||
"repository": {
|
||||
|
|
@ -18,15 +19,9 @@
|
|||
"gateway",
|
||||
"router"
|
||||
],
|
||||
"main": "dist/main/main.js",
|
||||
"bin": {
|
||||
"ccr": "dist/main/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"README_zh.md"
|
||||
"main": "packages/electron/dist/main/main.js",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
|
@ -35,9 +30,13 @@
|
|||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node build/dev.mjs",
|
||||
"build": "npm run build:assets && electron-builder",
|
||||
"dev": "npm run dev:cli",
|
||||
"dev:ui": "node build/dev.mjs ui",
|
||||
"dev:cli": "node build/dev.mjs cli",
|
||||
"dev:electron": "node build/dev.mjs electron",
|
||||
"build": "node build/build.mjs && electron-builder",
|
||||
"build:assets": "node build/build.mjs",
|
||||
"build:docker": "node build/docker-build.mjs",
|
||||
"build:app:mac": "npm run build:app:mac:local",
|
||||
"build:app:mac:local": "npm run build:assets && electron-builder --config build/electron-builder.local.cjs --mac --publish never",
|
||||
"build:app:mac:release": "node build/macos-release-preflight.mjs && npm run build:assets && electron-builder --mac --publish never",
|
||||
|
|
@ -45,7 +44,12 @@
|
|||
"prepack": "npm run build:assets",
|
||||
"prepublishOnly": "npm run typecheck",
|
||||
"preview": "npm run build:assets && electron .",
|
||||
"docker:build": "docker build -t claude-code-router:local .",
|
||||
"docker:run": "docker run --rm -p 3458:8080 -v ccr-data:/data claude-code-router:local",
|
||||
"test": "node build/test.mjs && node build/run-tests.mjs",
|
||||
"test:docker": "node tests/docker/docker-smoke.mjs",
|
||||
"test:e2e": "npm run build:assets && playwright test",
|
||||
"test:e2e:install": "playwright install chromium",
|
||||
"test:main": "node build/test.mjs main && node build/run-tests.mjs main",
|
||||
"test:renderer": "node build/test.mjs renderer && node build/run-tests.mjs renderer",
|
||||
"typecheck": "tsc --noEmit",
|
||||
|
|
@ -63,6 +67,7 @@
|
|||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@tailwindcss/cli": "^4.3.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^22.10.2",
|
||||
|
|
|
|||
21
packages/cli/LICENSE
Normal file
21
packages/cli/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 musistudio
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
329
packages/cli/README.md
Normal file
329
packages/cli/README.md
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
<h1 align="center">Claude Code Router Desktop</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a>
|
||||
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
|
||||
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
|
||||
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
|
||||
<a href="https://ccrdesk.top/"><img alt="Documentation" src="https://img.shields.io/badge/Docs-ccrdesk.top-0ea5e9?style=flat" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop screenshot" />
|
||||
</p>
|
||||
|
||||
Claude Code Router Desktop is a local gateway and desktop control panel for routing agent requests from Claude Code, Codex, ZCode, and compatible clients to the model provider you actually want to use.
|
||||
|
||||
## Why Use CCR
|
||||
|
||||
- Use one local endpoint for multiple agent tools instead of configuring every client separately.
|
||||
- Route requests with default routing, conditional rules, fallback targets, and request rewrites instead of editing client configuration by hand.
|
||||
- Mix providers without changing your workflow. CCR supports OpenAI-compatible APIs, Anthropic Messages, Gemini Generate Content, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Kimi Code, Mistral, Z.AI, Bailian, and custom providers.
|
||||
- Control cost and reliability with fallback routing, API key rotation, usage statistics, and request logs.
|
||||
|
||||
## Features
|
||||
|
||||
- **Overview dashboard**: inspect system status, usage widgets, account balances, model distribution, and share cards.
|
||||
- **Provider management**: add provider presets or custom endpoints, probe protocol support, test model connectivity, manage credentials, and monitor supported account balances where available.
|
||||
- **Routing rules**: configure default routing, conditional and model-prefix rules, fallback handling, and request rewrites.
|
||||
- **Agent Config**: configure Claude Code, Codex, and ZCode launch entries, models, scopes, and multi-instance app profiles.
|
||||
- **Gateway compatibility**: translate supported client requests through the local CCR model gateway.
|
||||
- **Proxy mode**: capture supported API traffic through a local proxy with optional system proxy integration and network capture.
|
||||
- **Fusion models**: combine a base model with vision, web search, or MCP tools into a reusable selectable model.
|
||||
|
||||
## Documentation
|
||||
|
||||
Read the full documentation at [ccrdesk.top](https://ccrdesk.top/).
|
||||
|
||||
## Download And Install
|
||||
|
||||
1. Open the [GitHub Releases page](https://github.com/musistudio/claude-code-router/releases).
|
||||
2. Download the package for your platform:
|
||||
- macOS Apple Silicon: `Claude-Code-Router_<version>-mac-Apple-Silicon-arm64.dmg` or `.zip`
|
||||
- macOS Intel: `Claude-Code-Router_<version>-mac-Intel-x64.dmg` or `.zip`
|
||||
- Windows: `Claude Code Router_<version>.exe`
|
||||
- Linux: `Claude Code Router_<version>.AppImage`
|
||||
3. Install and launch **Claude Code Router**.
|
||||
4. On first launch, CCR creates its local configuration database:
|
||||
- macOS/Linux: `~/.claude-code-router/config.sqlite`
|
||||
- Windows: `%APPDATA%\Claude Code Router\config.sqlite`
|
||||
|
||||
CCR stores runtime configuration in SQLite. A legacy `config.json` is read only once for migration when no SQLite config exists.
|
||||
|
||||
After the service is started from the **Server** page, CCR listens on `http://localhost:8080` by default. The **Server** page controls the gateway `Host`, `Port`, proxy mode, system proxy, network capture, and CA certificate status.
|
||||
|
||||
## Quick Start
|
||||
|
||||
CCR can be configured entirely from the desktop UI. Use this setup order for a clean first run.
|
||||
|
||||
### 1. Add a provider
|
||||
|
||||
Open **Providers**, click **Add Provider**, then choose a built-in preset or **Other / custom API endpoint**. Fill in the provider name, base URL, protocol, API key, and model list. Run protocol probing and model connectivity checks when available, then save the provider.
|
||||
|
||||
### 2. Configure routing
|
||||
|
||||
Open **Routing** to add conditional rules, configure request rewrites, and set fallback behavior.
|
||||
|
||||
Use **Add Routing Rule** for request conditions, model-prefix routing, or rule-level fallback targets.
|
||||
|
||||
### 3. Start the gateway
|
||||
|
||||
Open **Server** and click **Start**. After the page shows Running, CCR listens on `http://localhost:8080`. Enable **Auto start** if you want CCR to start the local gateway whenever the desktop app opens.
|
||||
|
||||
### 4. Connect your agent tool
|
||||
|
||||
Open **Agent Config** and choose the client you want to use. Configure Claude Code, Codex, or ZCode, select the target model and effect scope, then apply the config. For app entries, use the **Open Agent** action to open the target app through CCR.
|
||||
|
||||
### 5. Monitor and adjust
|
||||
|
||||
Use **Settings → Logs & Observability** to enable request logs and agent observability. Use **Logs** to confirm `request model`, `resolved provider`, `resolved model`, status, tokens, latency, and errors; use the tray window for quick token and account status.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Codex support is powered by [musistudio/codexl](https://github.com/musistudio/codexl).
|
||||
|
||||
## Support & Sponsoring
|
||||
|
||||
<div align="center">
|
||||
|
||||
<p>If you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated.</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="220">
|
||||
<a href="https://ko-fi.com/F1F31GN2GM">
|
||||
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi" />
|
||||
</a>
|
||||
<br />
|
||||
<sub>One-time support via Ko-fi</sub>
|
||||
</td>
|
||||
<td align="center" width="220">
|
||||
<a href="https://paypal.me/musistudio1999">
|
||||
<img src="https://img.shields.io/badge/PayPal-Sponsor-003087?logo=paypal&logoColor=white" alt="Sponsor with PayPal" />
|
||||
</a>
|
||||
<br />
|
||||
<sub>International sponsorship</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="220">
|
||||
<strong>Alipay</strong>
|
||||
<br />
|
||||
<img src="/blog/images/alipay.jpg" width="160" alt="Alipay QR code" />
|
||||
</td>
|
||||
<td align="center" width="220">
|
||||
<strong>WeChat Pay</strong>
|
||||
<br />
|
||||
<img src="/blog/images/wechat.jpg" width="160" alt="WeChat Pay QR code" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
### Our Sponsors
|
||||
|
||||
<div align="center">
|
||||
|
||||
<p>A huge thank you to all our sponsors for their generous support.</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td align="center" width="330">
|
||||
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
|
||||
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="Zhipu icon" />
|
||||
<br />
|
||||
<strong>Z智谱</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://aihubmix.com/">
|
||||
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&sz=128" width="42" height="42" alt="AIHubmix icon" />
|
||||
<br />
|
||||
<strong>AIHubmix</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://ai.burncloud.com">
|
||||
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud icon" />
|
||||
<br />
|
||||
<strong>BurnCloud</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://share.302.ai/ZGVF9w">
|
||||
<img src="https://www.google.com/s2/favicons?domain=302.ai&sz=128" width="42" height="42" alt="302.AI icon" />
|
||||
<br />
|
||||
<strong>302.AI</strong>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="330">
|
||||
<a href="https://runapi.co/register?aff=IX1t">
|
||||
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI icon" />
|
||||
<br />
|
||||
<strong>RunAPI</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://teamorouter.com/">
|
||||
<img src="/docs/public/provider-icons/teamorouter.png" width="42" height="42" alt="TeamoRouter icon" />
|
||||
<br />
|
||||
<strong>TeamoRouter</strong>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>Community Sponsors</h4>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td align="center" width="220">@Simon Leischnig</td>
|
||||
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
|
||||
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
|
||||
<td align="center" width="220">@*o</td>
|
||||
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
|
||||
<td align="center" width="220">@*说</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*更</td>
|
||||
<td align="center" width="220">@K*g</td>
|
||||
<td align="center" width="220">@R*R</td>
|
||||
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
|
||||
<td align="center" width="220">@*苗</td>
|
||||
<td align="center" width="220">@*划</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
|
||||
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
|
||||
<td align="center" width="220">@S*r</td>
|
||||
<td align="center" width="220">@*晖</td>
|
||||
<td align="center" width="220">@*敏</td>
|
||||
<td align="center" width="220">@Z*z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*然</td>
|
||||
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
|
||||
<td align="center" width="220">@*苗</td>
|
||||
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
|
||||
<td align="center" width="220">@*应</td>
|
||||
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*飞</td>
|
||||
<td align="center" width="220">@董*</td>
|
||||
<td align="center" width="220">@*汀</td>
|
||||
<td align="center" width="220">@*涯</td>
|
||||
<td align="center" width="220">@*:-)</td>
|
||||
<td align="center" width="220">@**磊</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*琢</td>
|
||||
<td align="center" width="220">@*成</td>
|
||||
<td align="center" width="220">@Z*o</td>
|
||||
<td align="center" width="220">@*琨</td>
|
||||
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
|
||||
<td align="center" width="220">@*_</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@Z*m</td>
|
||||
<td align="center" width="220">@*鑫</td>
|
||||
<td align="center" width="220">@c*y</td>
|
||||
<td align="center" width="220">@*昕</td>
|
||||
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
|
||||
<td align="center" width="220">@b*g</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*亿</td>
|
||||
<td align="center" width="220">@*辉</td>
|
||||
<td align="center" width="220">@JACK</td>
|
||||
<td align="center" width="220">@*光</td>
|
||||
<td align="center" width="220">@W*l</td>
|
||||
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
|
||||
<td align="center" width="220">@二吉吉</td>
|
||||
<td align="center" width="220">@a*g</td>
|
||||
<td align="center" width="220">@*林</td>
|
||||
<td align="center" width="220">@*咸</td>
|
||||
<td align="center" width="220">@*明</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@S*y</td>
|
||||
<td align="center" width="220">@f*o</td>
|
||||
<td align="center" width="220">@*智</td>
|
||||
<td align="center" width="220">@F*t</td>
|
||||
<td align="center" width="220">@r*c</td>
|
||||
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*军</td>
|
||||
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
|
||||
<td align="center" width="220">@*王</td>
|
||||
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
|
||||
<td align="center" width="220">@*王</td>
|
||||
<td align="center" width="220">@zcutlip</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
|
||||
<td align="center" width="220">@*更</td>
|
||||
<td align="center" width="220">@*.</td>
|
||||
<td align="center" width="220">@F*t</td>
|
||||
<td align="center" width="220">@*政</td>
|
||||
<td align="center" width="220">@*铭</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*叶</td>
|
||||
<td align="center" width="220">@七*o</td>
|
||||
<td align="center" width="220">@*青</td>
|
||||
<td align="center" width="220">@**晨</td>
|
||||
<td align="center" width="220">@*远</td>
|
||||
<td align="center" width="220">@*霄</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@**吉</td>
|
||||
<td align="center" width="220">@**飞</td>
|
||||
<td align="center" width="220">@**驰</td>
|
||||
<td align="center" width="220">@x*g</td>
|
||||
<td align="center" width="220">@**东</td>
|
||||
<td align="center" width="220">@*落</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@哆*k</td>
|
||||
<td align="center" width="220">@*涛</td>
|
||||
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
|
||||
<td align="center" width="220">@*呢</td>
|
||||
<td align="center" width="220">@d*u</td>
|
||||
<td align="center" width="220">@crizcraig</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">s*s</td>
|
||||
<td align="center" width="220">*火</td>
|
||||
<td align="center" width="220">*勤</td>
|
||||
<td align="center" width="220">**锟</td>
|
||||
<td align="center" width="220">*涛</td>
|
||||
<td align="center" width="220">**明</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">*知</td>
|
||||
<td align="center" width="220">*语</td>
|
||||
<td align="center" width="220">*瓜</td>
|
||||
<td align="center" width="220"></td>
|
||||
<td align="center" width="220"></td>
|
||||
<td align="center" width="220"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>If your name is masked, please contact me via my homepage email to update it with your GitHub username.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
328
packages/cli/README_zh.md
Normal file
328
packages/cli/README_zh.md
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
<h1 align="center">Claude Code Router Desktop</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a>
|
||||
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
|
||||
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
|
||||
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
|
||||
<a href="https://ccrdesk.top/"><img alt="文档" src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-ccrdesk.top-0ea5e9?style=flat" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop 项目截图" />
|
||||
</p>
|
||||
|
||||
Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
|
||||
|
||||
## 为什么使用 CCR
|
||||
|
||||
- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。
|
||||
- 在不改变工作流的情况下混用不同 Provider。CCR 支持 OpenAI 兼容 API、Anthropic Messages、Gemini Generate Content、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Kimi Code、Mistral、Z.AI、百炼以及自定义 Provider。
|
||||
- 通过 fallback 路由、API Key 轮换、用量统计和请求日志来控制成本和可靠性。
|
||||
|
||||
## 功能和特性
|
||||
|
||||
- **概览仪表盘**:查看系统状态、用量组件、账号余额、模型分布和分享卡片。
|
||||
- **Provider 管理**:添加预设或自定义端点,探测协议支持,检测模型连通性,管理凭据,并在可用时查看账号余额。
|
||||
- **路由规则**:配置条件路由、模型前缀规则、失败降级和请求改写。
|
||||
- **Agent配置**:为 Claude Code、Codex 和 ZCode 配置启动入口、模型、作用范围和多开 App 配置。
|
||||
- **网关兼容层**:通过本地 CCR 模型网关转换支持的客户端请求。
|
||||
- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。
|
||||
- **Fusion 组合模型**:把基础模型与视觉、联网搜索或 MCP 工具组合成新的可选模型。
|
||||
|
||||
## 文档
|
||||
|
||||
完整文档见 [ccrdesk.top](https://ccrdesk.top/)。
|
||||
|
||||
## 下载和安装
|
||||
|
||||
1. 打开 [GitHub Releases 页面](https://github.com/musistudio/claude-code-router/releases)。
|
||||
2. 按系统下载对应安装包:
|
||||
- macOS Apple 芯片:`Claude-Code-Router_<version>-mac-Apple-Silicon-arm64.dmg` 或 `.zip`
|
||||
- macOS Intel 芯片:`Claude-Code-Router_<version>-mac-Intel-x64.dmg` 或 `.zip`
|
||||
- Windows:`Claude Code Router_<version>.exe`
|
||||
- Linux:`Claude Code Router_<version>.AppImage`
|
||||
3. 安装并启动 **Claude Code Router**。
|
||||
4. 首次启动后,CCR 会创建本地配置数据库:
|
||||
- macOS/Linux:`~/.claude-code-router/config.sqlite`
|
||||
- Windows:`%APPDATA%\Claude Code Router\config.sqlite`
|
||||
|
||||
CCR 的运行配置存储在 SQLite 中。旧版 `config.json` 只会在没有 SQLite 配置时作为迁移来源读取一次。
|
||||
|
||||
从 **服务** 页面启动后,CCR 默认监听 `http://localhost:8080`。**服务** 页面负责配置网关 `Host`、`Port`、代理模式、系统代理、网络捕获和 CA 证书状态。
|
||||
|
||||
## 快速开始
|
||||
|
||||
CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序操作。
|
||||
|
||||
### 1. 添加 Provider
|
||||
|
||||
打开 **供应商**,点击 **添加供应商**,选择内置预设或 **其他 / 自定义 API 端点**。按表单填写 Provider 名称、基础 URL、协议、API Key 和模型列表。可用时先运行协议探测和模型连通性检查,然后保存 Provider。
|
||||
|
||||
### 2. 设置路由
|
||||
|
||||
打开 **路由**,添加条件规则,配置请求改写和失败降级。
|
||||
|
||||
如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。
|
||||
|
||||
### 3. 启动网关
|
||||
|
||||
打开 **服务**,点击 **启动**。页面显示运行中后,CCR 会在本机监听 `http://localhost:8080`。如果希望每次打开桌面应用时自动启动网关,可以启用自动启动。
|
||||
|
||||
### 4. 连接 Agent 工具
|
||||
|
||||
打开 **Agent配置**,选择要使用的客户端。配置 Claude Code、Codex 或 ZCode,选择目标模型和作用范围,然后应用配置。对于 App 入口,可以使用 **打开 Agent** 操作通过 CCR 打开目标应用。
|
||||
|
||||
### 5. 日常查看和调整
|
||||
|
||||
到 **设置 → 日志与观测** 打开请求日志和 Agent 观测。使用 **日志** 确认 `request model`、`resolved provider`、`resolved model`、状态码、tokens、耗时和错误;使用托盘窗口快速查看 Token 和账号状态。
|
||||
|
||||
## 致谢
|
||||
|
||||
对 Codex 的支持来自于 [musistudio/codexl](https://github.com/musistudio/codexl) 这个项目。
|
||||
|
||||
## 支持与赞助
|
||||
|
||||
<div align="center">
|
||||
|
||||
<p>如果你觉得这个项目有帮助,欢迎赞助项目开发。非常感谢你的支持。</p>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="220">
|
||||
<a href="https://ko-fi.com/F1F31GN2GM">
|
||||
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="通过 Ko-fi 赞助" />
|
||||
</a>
|
||||
<br />
|
||||
<sub>通过 Ko-fi 单次赞助</sub>
|
||||
</td>
|
||||
<td align="center" width="220">
|
||||
<a href="https://paypal.me/musistudio1999">
|
||||
<img src="https://img.shields.io/badge/PayPal-Sponsor-003087?logo=paypal&logoColor=white" alt="通过 PayPal 赞助" />
|
||||
</a>
|
||||
<br />
|
||||
<sub>国际赞助通道</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="220">
|
||||
<strong>支付宝</strong>
|
||||
<br />
|
||||
<img src="/blog/images/alipay.jpg" width="160" alt="支付宝收款码" />
|
||||
</td>
|
||||
<td align="center" width="220">
|
||||
<strong>微信支付</strong>
|
||||
<br />
|
||||
<img src="/blog/images/wechat.jpg" width="160" alt="微信支付收款码" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
### 我们的赞助商
|
||||
|
||||
<div align="center">
|
||||
|
||||
<p>非常感谢所有赞助商的慷慨支持。</p>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td align="center" width="330">
|
||||
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
|
||||
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="智谱图标" />
|
||||
<br />
|
||||
<strong>Z智谱</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://aihubmix.com/">
|
||||
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&sz=128" width="42" height="42" alt="AIHubmix 图标" />
|
||||
<br />
|
||||
<strong>AIHubmix</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://ai.burncloud.com">
|
||||
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud 图标" />
|
||||
<br />
|
||||
<strong>BurnCloud</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://share.302.ai/ZGVF9w">
|
||||
<img src="https://www.google.com/s2/favicons?domain=302.ai&sz=128" width="42" height="42" alt="302.AI 图标" />
|
||||
<br />
|
||||
<strong>302.AI</strong>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="330">
|
||||
<a href="https://runapi.co/register?aff=IX1t">
|
||||
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI 图标" />
|
||||
<br />
|
||||
<strong>RunAPI</strong>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" width="330">
|
||||
<a href="https://teamorouter.com/">
|
||||
<img src="/docs/public/provider-icons/teamorouter.png" width="42" height="42" alt="TeamoRouter 图标" />
|
||||
<br />
|
||||
<strong>TeamoRouter</strong>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h4>社区赞助者</h4>
|
||||
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td align="center" width="220">@Simon Leischnig</td>
|
||||
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
|
||||
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
|
||||
<td align="center" width="220">@*o</td>
|
||||
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
|
||||
<td align="center" width="220">@*说</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*更</td>
|
||||
<td align="center" width="220">@K*g</td>
|
||||
<td align="center" width="220">@R*R</td>
|
||||
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
|
||||
<td align="center" width="220">@*苗</td>
|
||||
<td align="center" width="220">@*划</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
|
||||
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
|
||||
<td align="center" width="220">@S*r</td>
|
||||
<td align="center" width="220">@*晖</td>
|
||||
<td align="center" width="220">@*敏</td>
|
||||
<td align="center" width="220">@Z*z</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*然</td>
|
||||
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
|
||||
<td align="center" width="220">@*苗</td>
|
||||
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
|
||||
<td align="center" width="220">@*应</td>
|
||||
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*飞</td>
|
||||
<td align="center" width="220">@董*</td>
|
||||
<td align="center" width="220">@*汀</td>
|
||||
<td align="center" width="220">@*涯</td>
|
||||
<td align="center" width="220">@*:-)</td>
|
||||
<td align="center" width="220">@**磊</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*琢</td>
|
||||
<td align="center" width="220">@*成</td>
|
||||
<td align="center" width="220">@Z*o</td>
|
||||
<td align="center" width="220">@*琨</td>
|
||||
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
|
||||
<td align="center" width="220">@*_</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@Z*m</td>
|
||||
<td align="center" width="220">@*鑫</td>
|
||||
<td align="center" width="220">@c*y</td>
|
||||
<td align="center" width="220">@*昕</td>
|
||||
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
|
||||
<td align="center" width="220">@b*g</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*亿</td>
|
||||
<td align="center" width="220">@*辉</td>
|
||||
<td align="center" width="220">@JACK</td>
|
||||
<td align="center" width="220">@*光</td>
|
||||
<td align="center" width="220">@W*l</td>
|
||||
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
|
||||
<td align="center" width="220">@二吉吉</td>
|
||||
<td align="center" width="220">@a*g</td>
|
||||
<td align="center" width="220">@*林</td>
|
||||
<td align="center" width="220">@*咸</td>
|
||||
<td align="center" width="220">@*明</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@S*y</td>
|
||||
<td align="center" width="220">@f*o</td>
|
||||
<td align="center" width="220">@*智</td>
|
||||
<td align="center" width="220">@F*t</td>
|
||||
<td align="center" width="220">@r*c</td>
|
||||
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*军</td>
|
||||
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
|
||||
<td align="center" width="220">@*王</td>
|
||||
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
|
||||
<td align="center" width="220">@*王</td>
|
||||
<td align="center" width="220">@zcutlip</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
|
||||
<td align="center" width="220">@*更</td>
|
||||
<td align="center" width="220">@*.</td>
|
||||
<td align="center" width="220">@F*t</td>
|
||||
<td align="center" width="220">@*政</td>
|
||||
<td align="center" width="220">@*铭</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@*叶</td>
|
||||
<td align="center" width="220">@七*o</td>
|
||||
<td align="center" width="220">@*青</td>
|
||||
<td align="center" width="220">@**晨</td>
|
||||
<td align="center" width="220">@*远</td>
|
||||
<td align="center" width="220">@*霄</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@**吉</td>
|
||||
<td align="center" width="220">@**飞</td>
|
||||
<td align="center" width="220">@**驰</td>
|
||||
<td align="center" width="220">@x*g</td>
|
||||
<td align="center" width="220">@**东</td>
|
||||
<td align="center" width="220">@*落</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">@哆*k</td>
|
||||
<td align="center" width="220">@*涛</td>
|
||||
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
|
||||
<td align="center" width="220">@*呢</td>
|
||||
<td align="center" width="220">@d*u</td>
|
||||
<td align="center" width="220">@crizcraig</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">s*s</td>
|
||||
<td align="center" width="220">*火</td>
|
||||
<td align="center" width="220">*勤</td>
|
||||
<td align="center" width="220">**锟</td>
|
||||
<td align="center" width="220">*涛</td>
|
||||
<td align="center" width="220">**明</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" width="220">*知</td>
|
||||
<td align="center" width="220">*语</td>
|
||||
<td align="center" width="220">*瓜</td>
|
||||
<td align="center" width="220"></td>
|
||||
<td align="center" width="220"></td>
|
||||
<td align="center" width="220"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>如果你的名字被打码,请通过我的主页邮箱联系我更新为 GitHub 用户名。</sub>
|
||||
|
||||
</div>
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目基于 [MIT License](LICENSE) 发布。
|
||||
48
packages/cli/package.json
Normal file
48
packages/cli/package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "@musistudio/claude-code-router",
|
||||
"version": "3.0.7",
|
||||
"license": "MIT",
|
||||
"description": "Local Claude Code Router gateway with CLI and web management UI.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+ssh://git@github.com/musistudio/claude-code-router.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/musistudio/claude-code-router/issues"
|
||||
},
|
||||
"homepage": "https://github.com/musistudio/claude-code-router#readme",
|
||||
"keywords": [
|
||||
"claude-code",
|
||||
"codex",
|
||||
"llm",
|
||||
"gateway",
|
||||
"router"
|
||||
],
|
||||
"main": "dist/main/cli.js",
|
||||
"bin": {
|
||||
"ccr": "dist/main/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"README_zh.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "npm --prefix ../.. run build:assets",
|
||||
"prepublishOnly": "npm --prefix ../.. run typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@the-next-ai/ai-gateway": "^1.0.4",
|
||||
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"node-forge": "^1.4.0",
|
||||
"undici": "^7.27.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { accessSync, constants as fsConstants, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { assertAvailableGatewayModels, type ProfileConfig, type ProfileOpenSurface } from "../shared/app";
|
||||
import { botGatewayProfileEnv } from "./bot-gateway-env";
|
||||
import { applyClaudeAppGatewayConfig } from "./claude-app-gateway-service";
|
||||
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "./claude-app-launch";
|
||||
import { launchCodexAppProfile, launchZcodeAppProfile } from "./codex-app-launch";
|
||||
import { loadAppConfig } from "./config";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import { applyProfileConfig, applyProfileRuntimeConfig } from "./profile-service";
|
||||
import { ensureProfileGateway } from "./profile-launch-service";
|
||||
import { buildProfileLaunchPlan, defaultProfileOpenSurface, findProfileForOpen, profileLaunchSpawnCommand, resolveProfileOpenSurface } from "./profile-launch-core";
|
||||
import { startWebManagementServer } from "./web-management-server";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import { applyClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "@ccr/core/agents/claude-app/launch";
|
||||
import { launchCodexAppProfile, launchZcodeAppProfile } from "@ccr/core/agents/codex/app-launch";
|
||||
import { loadAppConfig } from "@ccr/core/config/config";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import { applyProfileConfig, applyProfileRuntimeConfig } from "@ccr/core/profiles/service";
|
||||
import { ensureProfileGateway } from "@ccr/core/profiles/launch-service";
|
||||
import { buildProfileLaunchPlan, defaultProfileOpenSurface, findProfileForOpen, profileLaunchSpawnCommand, resolveProfileOpenSurface } from "@ccr/core/profiles/launch-core";
|
||||
import { openSystemExternal, startWebManagementServer } from "@ccr/core/web/management-server";
|
||||
import { assertAvailableGatewayModels, type ProfileConfig, type ProfileOpenSurface } from "@ccr/core/contracts/app";
|
||||
|
||||
type ProfileCliOptions = {
|
||||
agentArgs: string[];
|
||||
|
|
@ -23,7 +24,7 @@ type ProfileCliOptions = {
|
|||
};
|
||||
|
||||
type WebCliOptions = {
|
||||
command: "start" | "web";
|
||||
command: "start" | "ui" | "web";
|
||||
daemonChild: boolean;
|
||||
help: boolean;
|
||||
host?: string;
|
||||
|
|
@ -42,14 +43,19 @@ type CliOptions = ProfileCliOptions | StopCliOptions | WebCliOptions;
|
|||
type ServiceState = {
|
||||
host?: string;
|
||||
pid: number;
|
||||
serviceToken?: string;
|
||||
startedAt: string;
|
||||
startGateway: boolean;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const serviceStateFileName = "service.json";
|
||||
const serviceInstanceTokenEnv = "CCR_SERVICE_INSTANCE_TOKEN";
|
||||
const serviceRpcTimeoutMs = 2_000;
|
||||
const serviceStartTimeoutMs = 30_000;
|
||||
const serviceStopTimeoutMs = 10_000;
|
||||
const webAuthHeader = "x-ccr-web-auth";
|
||||
const webAuthQueryParam = "ccr_web_token";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const delegatedExitCode = delegateManagedDesktopCliToExternalCli();
|
||||
|
|
@ -67,6 +73,14 @@ async function main(): Promise<void> {
|
|||
await startService(options);
|
||||
return;
|
||||
}
|
||||
if (options.command === "ui") {
|
||||
if (options.help) {
|
||||
printUiHelp(0);
|
||||
return;
|
||||
}
|
||||
await openManagementUi(options);
|
||||
return;
|
||||
}
|
||||
if (options.command === "stop") {
|
||||
if (options.help) {
|
||||
printStopHelp(0);
|
||||
|
|
@ -175,6 +189,9 @@ function parseArgs(args: string[]): CliOptions {
|
|||
if (args[0] === "start") {
|
||||
return parseWebArgs(args.slice(1), "start");
|
||||
}
|
||||
if (args[0] === "ui") {
|
||||
return parseWebArgs(args.slice(1), "ui", true);
|
||||
}
|
||||
if (args[0] === "stop") {
|
||||
return parseStopArgs(args.slice(1));
|
||||
}
|
||||
|
|
@ -244,12 +261,12 @@ function parseStopArgs(args: string[]): StopCliOptions {
|
|||
return options;
|
||||
}
|
||||
|
||||
function parseWebArgs(args: string[], command: WebCliOptions["command"]): WebCliOptions {
|
||||
function parseWebArgs(args: string[], command: WebCliOptions["command"], defaultOpen = false): WebCliOptions {
|
||||
const options: WebCliOptions = {
|
||||
command,
|
||||
daemonChild: false,
|
||||
help: false,
|
||||
open: false,
|
||||
open: defaultOpen,
|
||||
startGateway: true
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
|
|
@ -303,24 +320,31 @@ function parseWebArgs(args: string[], command: WebCliOptions["command"]): WebCli
|
|||
|
||||
async function startService(options: WebCliOptions): Promise<void> {
|
||||
const current = readServiceState();
|
||||
if (current && isProcessRunning(current.pid)) {
|
||||
const currentVerification = current ? await verifyServiceState(current) : undefined;
|
||||
if (current && currentVerification?.ok) {
|
||||
process.stdout.write(`CCR service is already running at ${current.url} (pid ${current.pid}).\n`);
|
||||
if (options.open) {
|
||||
await openManagementUrl(current.url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearServiceState();
|
||||
if (current) {
|
||||
clearServiceState(current.pid);
|
||||
}
|
||||
|
||||
const serviceToken = generateServiceToken();
|
||||
const childArgs = [
|
||||
currentCliScript(),
|
||||
"serve",
|
||||
"--daemon-child",
|
||||
...(options.host ? ["--host", options.host] : []),
|
||||
...(options.port ? ["--port", String(options.port)] : []),
|
||||
...(options.open ? ["--open"] : ["--no-open"]),
|
||||
"--no-open",
|
||||
...(options.startGateway ? [] : ["--no-gateway"])
|
||||
];
|
||||
const child = spawn(process.execPath, childArgs, {
|
||||
detached: true,
|
||||
env: serviceChildEnv(),
|
||||
env: serviceChildEnv(serviceToken),
|
||||
stdio: "ignore",
|
||||
windowsHide: true
|
||||
});
|
||||
|
|
@ -335,10 +359,31 @@ async function startService(options: WebCliOptions): Promise<void> {
|
|||
throw new Error(`CCR service did not report ready within ${serviceStartTimeoutMs}ms.`);
|
||||
}
|
||||
process.stdout.write(`CCR service started at ${state.url} (pid ${state.pid}).\n`);
|
||||
if (options.open) {
|
||||
await openManagementUrl(state.url);
|
||||
}
|
||||
}
|
||||
|
||||
function serviceChildEnv(): NodeJS.ProcessEnv {
|
||||
async function openManagementUi(options: WebCliOptions): Promise<void> {
|
||||
await startService({
|
||||
...options,
|
||||
command: "start"
|
||||
});
|
||||
}
|
||||
|
||||
async function openManagementUrl(url: string): Promise<void> {
|
||||
try {
|
||||
await openSystemExternal(url);
|
||||
process.stdout.write(`Opened CCR management UI at ${url}\n`);
|
||||
} catch (error) {
|
||||
process.stderr.write(`Failed to open browser: ${formatError(error)}\n`);
|
||||
process.stdout.write(`CCR management UI is available at ${url}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function serviceChildEnv(serviceToken: string): NodeJS.ProcessEnv {
|
||||
const env = { ...process.env };
|
||||
env[serviceInstanceTokenEnv] = serviceToken;
|
||||
if (process.versions.electron) {
|
||||
env.ELECTRON_RUN_AS_NODE = "1";
|
||||
} else {
|
||||
|
|
@ -355,9 +400,11 @@ async function runWebServer(options: WebCliOptions): Promise<void> {
|
|||
startGateway: options.startGateway
|
||||
});
|
||||
if (options.daemonChild) {
|
||||
const serviceToken = process.env[serviceInstanceTokenEnv]?.trim() || undefined;
|
||||
writeServiceState({
|
||||
host: options.host,
|
||||
pid: process.pid,
|
||||
...(serviceToken ? { serviceToken } : {}),
|
||||
startedAt: new Date().toISOString(),
|
||||
startGateway: options.startGateway,
|
||||
url: runtime.url
|
||||
|
|
@ -389,15 +436,19 @@ async function stopService(): Promise<void> {
|
|||
process.stdout.write("CCR service is not running.\n");
|
||||
return;
|
||||
}
|
||||
if (!isProcessRunning(state.pid)) {
|
||||
const verification = await verifyServiceState(state);
|
||||
if (!verification.ok) {
|
||||
clearServiceState(state.pid);
|
||||
process.stdout.write("CCR service is not running.\n");
|
||||
return;
|
||||
}
|
||||
process.kill(state.pid, "SIGTERM");
|
||||
const stopped = await waitForProcessExit(state.pid, serviceStopTimeoutMs);
|
||||
if (!stopped && isProcessRunning(state.pid)) {
|
||||
throw new Error(`CCR service pid ${state.pid} did not stop within ${serviceStopTimeoutMs}ms.`);
|
||||
|
||||
await callServiceRpc(state, "quitApp");
|
||||
const stopped = verification.trustedPid
|
||||
? await waitForProcessExit(state.pid, serviceStopTimeoutMs)
|
||||
: await waitForServiceUnavailable(state, serviceStopTimeoutMs);
|
||||
if (!stopped) {
|
||||
throw new Error(`CCR service did not stop within ${serviceStopTimeoutMs}ms.`);
|
||||
}
|
||||
clearServiceState(state.pid);
|
||||
process.stdout.write("CCR service stopped.\n");
|
||||
|
|
@ -407,11 +458,13 @@ function printHelp(exitCode: number): void {
|
|||
const output = [
|
||||
"Usage:",
|
||||
" ccr start [--host <host>] [--port <port>] [--open] [--no-gateway]",
|
||||
" ccr ui [--host <host>] [--port <port>] [--no-gateway]",
|
||||
" ccr stop",
|
||||
" ccr <profile-name-or-id> [cli|app] [-- <agent args>]",
|
||||
"",
|
||||
"Examples:",
|
||||
" ccr start",
|
||||
" ccr ui",
|
||||
" ccr stop",
|
||||
" ccr Codex",
|
||||
" ccr default-codex -- --model gpt-5-codex",
|
||||
|
|
@ -432,7 +485,31 @@ function printStartHelp(exitCode: number): void {
|
|||
" --port <port> Management server port. Defaults to 3458.",
|
||||
" --open Open the management page in the default browser.",
|
||||
" --no-open Do not open the management page.",
|
||||
" --no-gateway Start only the web management server."
|
||||
" --no-gateway Start only the web management server.",
|
||||
"",
|
||||
"Environment:",
|
||||
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
|
||||
].join("\n");
|
||||
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
||||
stream.write(`${output}\n`);
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
|
||||
function printUiHelp(exitCode: number): void {
|
||||
const output = [
|
||||
"Usage:",
|
||||
" ccr ui [--host <host>] [--port <port>] [--no-gateway]",
|
||||
"",
|
||||
"Starts the background CCR service if needed and opens the management UI in the default browser.",
|
||||
"",
|
||||
"Options:",
|
||||
" --host <host> Management server host. Defaults to 127.0.0.1.",
|
||||
" --port <port> Management server port. Defaults to 3458.",
|
||||
" --no-open Start or find the service and print the management URL without opening a browser.",
|
||||
" --no-gateway Start only the web management server when the service is not already running.",
|
||||
"",
|
||||
"Environment:",
|
||||
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
|
||||
].join("\n");
|
||||
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
||||
stream.write(`${output}\n`);
|
||||
|
|
@ -460,7 +537,10 @@ function printWebHelp(exitCode: number): void {
|
|||
" --host <host> Management server host. Defaults to 127.0.0.1.",
|
||||
" --port <port> Management server port. Defaults to 3458.",
|
||||
" --open Open the management page in the default browser.",
|
||||
" --no-gateway Start only the web management server."
|
||||
" --no-gateway Start only the web management server.",
|
||||
"",
|
||||
"Environment:",
|
||||
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
|
||||
].join("\n");
|
||||
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
||||
stream.write(`${output}\n`);
|
||||
|
|
@ -481,6 +561,7 @@ function readServiceState(): ServiceState | undefined {
|
|||
return {
|
||||
host: parsed.host,
|
||||
pid,
|
||||
serviceToken: typeof parsed.serviceToken === "string" && parsed.serviceToken.trim() ? parsed.serviceToken.trim() : undefined,
|
||||
startedAt: parsed.startedAt || "",
|
||||
startGateway: parsed.startGateway !== false,
|
||||
url: parsed.url
|
||||
|
|
@ -608,7 +689,7 @@ async function waitForServiceState(pid: number | undefined, timeoutMs: number):
|
|||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const state = readServiceState();
|
||||
if (state && (!pid || state.pid === pid) && isProcessRunning(state.pid)) {
|
||||
if (state && (!pid || state.pid === pid) && (await verifyServiceState(state)).ok) {
|
||||
return state;
|
||||
}
|
||||
await delay(150);
|
||||
|
|
@ -640,6 +721,98 @@ function isProcessRunning(pid: number | undefined): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
type ServiceStateVerification =
|
||||
| { ok: true; trustedPid: boolean }
|
||||
| { ok: false };
|
||||
|
||||
type ServiceIdentity = {
|
||||
pid?: unknown;
|
||||
serviceTokenConfigured?: unknown;
|
||||
serviceTokenMatches?: unknown;
|
||||
};
|
||||
|
||||
async function verifyServiceState(state: ServiceState): Promise<ServiceStateVerification> {
|
||||
if (!isProcessRunning(state.pid)) {
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
if (state.serviceToken) {
|
||||
const identity = await callServiceRpc<ServiceIdentity>(state, "getServiceIdentity", [state.serviceToken]).catch(() => undefined);
|
||||
if (identity?.serviceTokenMatches === true && Number(identity.pid) === state.pid) {
|
||||
return { ok: true, trustedPid: true };
|
||||
}
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
const appInfo = await callServiceRpc<{ name?: unknown }>(state, "getAppInfo").catch(() => undefined);
|
||||
return appInfo?.name === "Claude Code Router"
|
||||
? { ok: true, trustedPid: false }
|
||||
: { ok: false };
|
||||
}
|
||||
|
||||
async function waitForServiceUnavailable(state: ServiceState, timeoutMs: number): Promise<boolean> {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const appInfo = await callServiceRpc<{ name?: unknown }>(state, "getAppInfo").catch(() => undefined);
|
||||
if (appInfo?.name !== "Claude Code Router") {
|
||||
return true;
|
||||
}
|
||||
await delay(150);
|
||||
}
|
||||
const appInfo = await callServiceRpc<{ name?: unknown }>(state, "getAppInfo").catch(() => undefined);
|
||||
return appInfo?.name !== "Claude Code Router";
|
||||
}
|
||||
|
||||
async function callServiceRpc<T>(state: ServiceState, method: string, args: unknown[] = []): Promise<T> {
|
||||
const endpoint = serviceRpcEndpoint(state.url);
|
||||
const authToken = serviceAuthToken(state.url);
|
||||
if (!endpoint || !authToken) {
|
||||
throw new Error("CCR service state does not include a usable management URL.");
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), serviceRpcTimeoutMs);
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
body: JSON.stringify({ args, method }),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
[webAuthHeader]: authToken
|
||||
},
|
||||
method: "POST",
|
||||
signal: controller.signal
|
||||
});
|
||||
const payload = await response.json().catch(() => undefined) as { ok?: boolean; value?: T } | undefined;
|
||||
if (!response.ok || !payload?.ok) {
|
||||
throw new Error(`CCR service RPC ${method} failed with HTTP ${response.status}`);
|
||||
}
|
||||
return payload.value as T;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
function serviceRpcEndpoint(url: string): string | undefined {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return `${parsed.origin}/api/ccr/rpc`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function serviceAuthToken(url: string): string {
|
||||
try {
|
||||
return new URL(url).searchParams.get(webAuthQueryParam)?.trim() ?? "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function generateServiceToken(): string {
|
||||
return randomBytes(32).toString("base64url");
|
||||
}
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
7
packages/cli/tsconfig.json
Normal file
7
packages/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts"]
|
||||
}
|
||||
21
packages/core/package.json
Normal file
21
packages/core/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@claude-code-router/core",
|
||||
"version": "3.0.7",
|
||||
"private": true,
|
||||
"description": "Claude Code Router core gateway, routing, provider, and storage services.",
|
||||
"main": "dist/main/server.js",
|
||||
"bin": {
|
||||
"ccr-core-server": "dist/main/server.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@the-next-ai/ai-gateway": "^1.0.4",
|
||||
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"node-forge": "^1.4.0",
|
||||
"pm2": "^6.0.13",
|
||||
"undici": "^7.27.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import os from "node:os";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import type { AppConfig, BotGatewayRuntimeConfig, ProfileConfig, ProfileOpenSurface } from "../shared/app";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import type { AppConfig, BotGatewayRuntimeConfig, ProfileConfig, ProfileOpenSurface } from "@ccr/core/contracts/app";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
|
||||
const requireFromHere = createRequire(__filename);
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import type { BotHandoffScanTarget } from "../shared/app";
|
||||
import { windowsSystemCommand } from "./windows-system";
|
||||
import type { BotHandoffScanTarget } from "@ccr/core/contracts/app";
|
||||
import { windowsSystemCommand } from "@ccr/core/platform/windows-system";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
|
|
@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import type {
|
||||
BotGatewayQrLoginCancelRequest,
|
||||
BotGatewayQrLoginCancelResult,
|
||||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
BotGatewayQrLoginWaitRequest,
|
||||
BotGatewayQrLoginWaitResult,
|
||||
BotGatewayRuntimeConfig
|
||||
} from "../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
type BotGatewayClientWithRequest = {
|
||||
close?: () => Promise<void> | void;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { AppConfig } from "./app";
|
||||
import { normalizeProfileScopeValue } from "./app";
|
||||
import type { AppConfig } from "@ccr/core/contracts/app";
|
||||
import { normalizeProfileScopeValue } from "@ccr/core/contracts/app";
|
||||
|
||||
export const CLAUDE_APP_FALLBACK_MODEL = "claude-sonnet-4-5";
|
||||
export const CLAUDE_APP_ONE_MILLION_CONTEXT_SUFFIX = "[1m]";
|
||||
|
|
@ -2,16 +2,16 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
||||
import { resolveRuntimeAppPath } from "./app-paths";
|
||||
import { saveAppConfig } from "./config";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import { resolveRuntimeAppPath } from "@ccr/core/runtime/app-paths";
|
||||
import { saveAppConfig } from "@ccr/core/config/config";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import {
|
||||
buildClaudeAppGatewayInferenceModels,
|
||||
type ClaudeAppGatewayInferenceModel,
|
||||
type ClaudeAppGatewayModelRouteOptions
|
||||
} from "../shared/claude-app-gateway";
|
||||
import { NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ClaudeAppGatewayApplyResult } from "../shared/app";
|
||||
import { findModelCatalogEntry } from "../server/gateway/model-catalog";
|
||||
} from "@ccr/core/agents/claude-app/gateway-routes";
|
||||
import { NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ClaudeAppGatewayApplyResult } from "@ccr/core/contracts/app";
|
||||
import { findModelCatalogEntry } from "@ccr/core/gateway/model-catalog";
|
||||
|
||||
const CLAUDE_APP_CONFIG_ID = "8f69f2f1-3275-4ad8-9317-4aa7e972f311";
|
||||
const CLAUDE_APP_CONFIG_NAME = "Claude Code Router";
|
||||
|
|
@ -2,12 +2,12 @@ import { spawn, type ChildProcess } from "node:child_process";
|
|||
import { mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AppConfig, ProfileConfig } from "../shared/app";
|
||||
import { botGatewayProfileEnv } from "./bot-gateway-env";
|
||||
import { prepareClaudeAppCdpUserDataDir, reserveClaudeAppCdpPort, scheduleClaudeAppDesignCdp } from "./claude-app-cdp";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
|
||||
import { resolveClaudeCodeSettingsFile } from "./profile-launch-core";
|
||||
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "./windows-app-discovery";
|
||||
import type { AppConfig, ProfileConfig } from "@ccr/core/contracts/app";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import { prepareClaudeAppCdpUserDataDir, reserveClaudeAppCdpPort, scheduleClaudeAppDesignCdp } from "@ccr/core/agents/claude-app/cdp";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
|
||||
import { resolveClaudeCodeSettingsFile } from "@ccr/core/profiles/launch-core";
|
||||
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "@ccr/core/platform/windows-app-discovery";
|
||||
|
||||
type ClaudeAppLookupResult = {
|
||||
checked: string[];
|
||||
|
|
@ -2,12 +2,12 @@ import { spawn, type ChildProcess } from "node:child_process";
|
|||
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AppConfig, ProfileConfig } from "../shared/app";
|
||||
import { botGatewayProfileEnv } from "./bot-gateway-env";
|
||||
import { codexModelCatalogJson } from "./codex-model-catalog";
|
||||
import { buildProfileLaunchPlan, resolveCodexConfigFile } from "./profile-launch-core";
|
||||
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "./windows-app-discovery";
|
||||
import { writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "./zcode-profile-config";
|
||||
import type { AppConfig, ProfileConfig } from "@ccr/core/contracts/app";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import { codexModelCatalogJson } from "@ccr/core/agents/codex/model-catalog";
|
||||
import { buildProfileLaunchPlan, resolveCodexConfigFile } from "@ccr/core/profiles/launch-core";
|
||||
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "@ccr/core/platform/windows-app-discovery";
|
||||
import { writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "@ccr/core/agents/zcode/profile-config";
|
||||
|
||||
type CodexAppLookupResult = {
|
||||
checked: string[];
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME } from "../shared/app";
|
||||
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelProfileConfig } from "../shared/app";
|
||||
import { BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME } from "@ccr/core/contracts/app";
|
||||
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelProfileConfig } from "@ccr/core/contracts/app";
|
||||
import {
|
||||
findModelCatalogEntry,
|
||||
modelCatalogMaxInputTokens,
|
||||
readCatalogCapability,
|
||||
type ModelCatalogEntry
|
||||
} from "../server/gateway/model-catalog";
|
||||
} from "@ccr/core/gateway/model-catalog";
|
||||
|
||||
const fusionModelProviderName = "Fusion";
|
||||
const codexDefaultContextWindow = 128_000;
|
||||
|
|
@ -5,7 +5,7 @@ import type {
|
|||
LocalAgentProviderImportResult,
|
||||
ProviderAccountConfig,
|
||||
ProviderAccountMappingConfig
|
||||
} from "../../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
bearerAuthPlugin,
|
||||
findOauthTokenSet,
|
||||
|
|
@ -16,7 +16,7 @@ import {
|
|||
uniqueProviderName,
|
||||
uniqueStrings,
|
||||
type OAuthTokenSet
|
||||
} from "./shared";
|
||||
} from "@ccr/core/agents/local-providers/shared";
|
||||
|
||||
const claudeDefaultModels = ["claude-sonnet-4-20250514"];
|
||||
|
||||
|
|
@ -9,8 +9,8 @@ import type {
|
|||
ProviderAccountMappingConfig,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountMeterDetail
|
||||
} from "../../shared/app";
|
||||
import { normalizeProviderBaseUrl } from "../../shared/provider-url";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { normalizeProviderBaseUrl } from "@ccr/core/providers/url";
|
||||
import {
|
||||
isRecord,
|
||||
localAgentProviderApiKey,
|
||||
|
|
@ -26,7 +26,7 @@ import {
|
|||
uniqueProviderName,
|
||||
uniqueStrings,
|
||||
type OAuthTokenSet
|
||||
} from "./shared";
|
||||
} from "@ccr/core/agents/local-providers/shared";
|
||||
|
||||
export const codexDefaultBaseUrl = "https://chatgpt.com/backend-api/codex";
|
||||
|
||||
|
|
@ -2,13 +2,13 @@ import type {
|
|||
LocalAgentProviderCandidate,
|
||||
LocalAgentProviderImportRequest,
|
||||
LocalAgentProviderImportResult
|
||||
} from "../shared/app";
|
||||
import { claudeCodeCandidate, importClaudeCodeProvider } from "./local-agent-providers/claude-code";
|
||||
import { codexCandidate, importCodexProvider } from "./local-agent-providers/codex";
|
||||
import { importZcodeProvider, zcodeCandidate } from "./local-agent-providers/zcode";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { claudeCodeCandidate, importClaudeCodeProvider } from "@ccr/core/agents/local-providers/claude-code";
|
||||
import { codexCandidate, importCodexProvider } from "@ccr/core/agents/local-providers/codex";
|
||||
import { importZcodeProvider, zcodeCandidate } from "@ccr/core/agents/local-providers/zcode";
|
||||
|
||||
export { codexDefaultBaseUrl, readCodexAuth } from "./local-agent-providers/codex";
|
||||
export { localAgentProviderApiKey, type OAuthTokenSet } from "./local-agent-providers/shared";
|
||||
export { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/codex";
|
||||
export { localAgentProviderApiKey, type OAuthTokenSet } from "@ccr/core/agents/local-providers/shared";
|
||||
|
||||
export function getLocalAgentProviderCandidates(): LocalAgentProviderCandidate[] {
|
||||
return [
|
||||
|
|
@ -5,7 +5,7 @@ import type {
|
|||
LocalAgentProviderKind,
|
||||
ProviderAccountConfig,
|
||||
ProviderDeepLinkPayload
|
||||
} from "../../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
export type OAuthTokenSet = {
|
||||
accountId?: string;
|
||||
|
|
@ -4,8 +4,8 @@ import type {
|
|||
LocalAgentProviderCandidate,
|
||||
LocalAgentProviderImportResult,
|
||||
ProviderAccountConfig
|
||||
} from "../../shared/app";
|
||||
import { findProviderPresetByBaseUrl } from "../presets";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { findProviderPresetByBaseUrl } from "@ccr/core/providers/presets/index";
|
||||
import {
|
||||
apiKeyAuthPlugin,
|
||||
cloneProviderAccountConfig,
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
uniqueProviderName,
|
||||
uniqueStrings,
|
||||
type ApiTokenSet
|
||||
} from "./shared";
|
||||
} from "@ccr/core/agents/local-providers/shared";
|
||||
|
||||
type ZcodeConfiguredProvider = {
|
||||
apiKey: string;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AppConfig, ProfileConfig } from "../shared/app";
|
||||
import { normalizeRouteSelector } from "../server/gateway/claude-code-router-plugin";
|
||||
import { buildCodexModelCatalogIds } from "./codex-model-catalog";
|
||||
import type { AppConfig, ProfileConfig } from "@ccr/core/contracts/app";
|
||||
import { normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
|
||||
import { buildCodexModelCatalogIds } from "@ccr/core/agents/codex/model-catalog";
|
||||
|
||||
export type ZcodeProfileConfigWriteResult = {
|
||||
backupFile?: string;
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { API_KEYS_DB_FILE, LEGACY_API_KEYS_DB_FILES } from "./constants";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "./sqlite-native";
|
||||
import type { ApiKeyConfig, ApiKeyLimitConfig } from "../shared/app";
|
||||
import { API_KEYS_DB_FILE, LEGACY_API_KEYS_DB_FILES } from "@ccr/core/config/constants";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
import type { ApiKeyConfig, ApiKeyLimitConfig } from "@ccr/core/contracts/app";
|
||||
|
||||
type SqlDatabase = BetterSqliteDatabase;
|
||||
type SqlValue = bigint | Buffer | number | string | null;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { APP_CONFIG_DB_FILE, LEGACY_APP_CONFIG_DB_FILES } from "./constants";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "./sqlite-native";
|
||||
import { APP_CONFIG_DB_FILE, LEGACY_APP_CONFIG_DB_FILES } from "@ccr/core/config/constants";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
|
||||
type SqlDatabase = BetterSqliteDatabase;
|
||||
type SqlValue = bigint | Buffer | number | string | null;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { loadPersistedAppConfig, replacePersistedAppConfig } from "./app-config-store";
|
||||
import { loadPersistedApiKeys, replacePersistedApiKeys } from "./api-key-store";
|
||||
import { CONFIG_FILE, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "./constants";
|
||||
import { normalizeCodexProviderAccountConfig } from "./local-agent-providers/codex";
|
||||
import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent } from "../shared/app";
|
||||
import { createDefaultAppConfig } from "../shared/default-config";
|
||||
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "./presets";
|
||||
import { loadPersistedAppConfig, replacePersistedAppConfig } from "@ccr/core/config/app-config-store";
|
||||
import { loadPersistedApiKeys, replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { CONFIG_FILE, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "@ccr/core/config/constants";
|
||||
import { normalizeCodexProviderAccountConfig } from "@ccr/core/agents/local-providers/codex";
|
||||
import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent } from "@ccr/core/contracts/app";
|
||||
import { createDefaultAppConfig } from "@ccr/core/config/default-config";
|
||||
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
|
||||
import type {
|
||||
AppConfig,
|
||||
ApiKeyConfig,
|
||||
|
|
@ -54,7 +54,7 @@ import type {
|
|||
TrayWidgetType,
|
||||
TrayWidgetVariant,
|
||||
TrayWindowModuleId
|
||||
} from "../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
type LoadedProfileConfig = Partial<Omit<ProfileRuntimeConfig, "claudeCode" | "codex" | "profiles">> & {
|
||||
claudeCode?: Partial<ClaudeCodeProfileConfig>;
|
||||
|
|
@ -1,20 +1,18 @@
|
|||
import path from "node:path";
|
||||
import { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR, resolveRuntimeAppPath } from "./app-paths";
|
||||
import { copyMissingDirectoryContents } from "./storage-migration";
|
||||
import { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR, resolveRuntimeAppPath, resolveRuntimeConfigDir, resolveRuntimeDataDir } from "@ccr/core/runtime/app-paths";
|
||||
import { copyMissingDirectoryContents } from "@ccr/core/storage/migration";
|
||||
|
||||
export { IPC_CHANNELS } from "../shared/ipc-channels";
|
||||
export { IPC_CHANNELS } from "@ccr/core/contracts/ipc-channels";
|
||||
export const LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIGDIR, "config.json");
|
||||
|
||||
export { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR };
|
||||
|
||||
export const CONFIGDIR = process.platform === "win32"
|
||||
? path.join(resolveRuntimeAppPath("appData"), APP_STORAGE_NAME)
|
||||
: LEGACY_CONFIGDIR;
|
||||
export const CONFIGDIR = resolveRuntimeConfigDir();
|
||||
export const LEGACY_WINDOWS_CONFIGDIR = path.join(resolveRuntimeAppPath("appData"), APP_NAME);
|
||||
export const LEGACY_WINDOWS_CONFIG_FILE = path.join(LEGACY_WINDOWS_CONFIGDIR, "config.json");
|
||||
export const CONFIG_FILE = path.join(CONFIGDIR, "config.json");
|
||||
export const ONBOARDING_FINISHED_FILE = path.join(CONFIGDIR, ".onboard_finished");
|
||||
export const DATADIR = resolveRuntimeAppPath("userData");
|
||||
export const DATADIR = resolveRuntimeDataDir();
|
||||
export const APP_CONFIG_DB_FILE = path.join(CONFIGDIR, "config.sqlite");
|
||||
export const API_KEYS_DB_FILE = path.join(DATADIR, "api-keys.sqlite");
|
||||
export const LEGACY_APP_CONFIG_DB_FILES = process.platform === "win32" ? [path.join(LEGACY_WINDOWS_CONFIGDIR, "config.sqlite")] : [];
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
DEFAULT_TRAY_WINDOW_MODULES,
|
||||
type AppConfig,
|
||||
type ProxyRouteTarget
|
||||
} from "./app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
export const DEFAULT_PROXY_TARGETS: ProxyRouteTarget[] = [
|
||||
{ host: "api.anthropic.com", paths: ["/v1/messages", "/v1/messages/count_tokens"] },
|
||||
|
|
@ -6,8 +6,8 @@ import type {
|
|||
ProviderDeepLinkPayload,
|
||||
ProviderDeepLinkRequest,
|
||||
ProviderManifestDeepLinkPayload
|
||||
} from "./app";
|
||||
import { providerUrlWithDefaultScheme } from "./provider-url";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
|
||||
export const appDeepLinkProtocol = "ccr";
|
||||
export const providerDeepLinkHost = "provider";
|
||||
129
packages/core/src/entrypoints/server.ts
Normal file
129
packages/core/src/entrypoints/server.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env node
|
||||
import { startWebManagementServer } from "@ccr/core/web/management-server";
|
||||
|
||||
type CoreServerOptions = {
|
||||
help: boolean;
|
||||
host?: string;
|
||||
open: boolean;
|
||||
port?: number;
|
||||
startGateway: boolean;
|
||||
};
|
||||
|
||||
export async function runCoreServer(args = process.argv.slice(2)): Promise<void> {
|
||||
const options = parseCoreServerArgs(args);
|
||||
if (options.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const runtime = await startWebManagementServer({
|
||||
host: options.host,
|
||||
open: options.open,
|
||||
port: options.port,
|
||||
startGateway: options.startGateway
|
||||
});
|
||||
process.stdout.write(`CCR core server is running at ${runtime.url}\n`);
|
||||
|
||||
let closing = false;
|
||||
const shutdown = (signal: NodeJS.Signals) => {
|
||||
if (closing) {
|
||||
return;
|
||||
}
|
||||
closing = true;
|
||||
void runtime.close().finally(() => {
|
||||
process.exit(signal === "SIGINT" ? 130 : 143);
|
||||
});
|
||||
};
|
||||
process.once("SIGINT", shutdown);
|
||||
process.once("SIGTERM", shutdown);
|
||||
await new Promise(() => undefined);
|
||||
}
|
||||
|
||||
function parseCoreServerArgs(args: string[]): CoreServerOptions {
|
||||
const options: CoreServerOptions = {
|
||||
help: false,
|
||||
open: false,
|
||||
startGateway: true
|
||||
};
|
||||
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
options.help = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--open") {
|
||||
options.open = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--no-open") {
|
||||
options.open = false;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--gateway") {
|
||||
options.startGateway = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--no-gateway") {
|
||||
options.startGateway = false;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--host") {
|
||||
index += 1;
|
||||
options.host = requiredArg(args[index], "--host");
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--host=")) {
|
||||
options.host = requiredArg(arg.slice("--host=".length), "--host");
|
||||
continue;
|
||||
}
|
||||
if (arg === "--port") {
|
||||
index += 1;
|
||||
options.port = parsePort(requiredArg(args[index], "--port"));
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--port=")) {
|
||||
options.port = parsePort(requiredArg(arg.slice("--port=".length), "--port"));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown core server option: ${arg}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
process.stdout.write([
|
||||
"Usage:",
|
||||
" ccr-core-server [--host <host>] [--port <port>] [--no-gateway]",
|
||||
"",
|
||||
"Options:",
|
||||
" --host <host> Management server host. Defaults to CCR_WEB_HOST or 127.0.0.1.",
|
||||
" --port <port> Management server port. Defaults to CCR_WEB_PORT or 3458.",
|
||||
" --no-gateway Start only the web management server.",
|
||||
"",
|
||||
"Environment:",
|
||||
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
|
||||
].join("\n") + "\n");
|
||||
}
|
||||
|
||||
function requiredArg(value: string | undefined, flag: string): string {
|
||||
if (!value) {
|
||||
throw new Error(`Missing value for ${flag}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parsePort(value: string): number {
|
||||
const port = Number(value);
|
||||
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||
throw new Error(`Invalid port: ${value}`);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
runCoreServer().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
|
@ -2,8 +2,8 @@ import { createRequire } from "node:module";
|
|||
import { EventEmitter } from "node:events";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { availableGatewayModelIds, type AppConfig, type RouterBuiltInAgentRuleId, type RouterConfig, type RouterFallbackConfig, type RouterRule, type RouterRuleCondition, type RouterRuleRewrite } from "../../shared/app";
|
||||
import { CONFIGDIR } from "../../main/constants";
|
||||
import { availableGatewayModelIds, type AppConfig, type RouterBuiltInAgentRuleId, type RouterConfig, type RouterFallbackConfig, type RouterRule, type RouterRuleCondition, type RouterRuleRewrite } from "@ccr/core/contracts/app";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
|
||||
type HeaderValue = string | string[] | undefined;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { loadModelCatalogPayload } from "../../main/model-catalog-file";
|
||||
import { loadModelCatalogPayload } from "@ccr/core/models/catalog-file";
|
||||
|
||||
const claudeCodeDefaultContextTokens = 200_000;
|
||||
let modelCatalogIndex: ModelCatalogIndex | undefined;
|
||||
|
|
@ -22,35 +22,35 @@ import type {
|
|||
VirtualModelFusionVisionConfig,
|
||||
VirtualModelFusionWebSearchConfig,
|
||||
VirtualModelFusionWebSearchProvider
|
||||
} from "../../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import {
|
||||
CLAUDE_APP_FALLBACK_MODEL,
|
||||
buildClaudeAppGatewayModelRoutes,
|
||||
inferClaudeAppGatewayTargetModel,
|
||||
resolveClaudeAppGatewayRouteModel,
|
||||
type ClaudeAppGatewayModelRouteOptions
|
||||
} from "../../shared/claude-app-gateway";
|
||||
} from "@ccr/core/agents/claude-app/gateway-routes";
|
||||
import {
|
||||
BUILTIN_FUSION_VISION_TOOL_NAME,
|
||||
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
|
||||
NO_AVAILABLE_GATEWAY_MODELS_MESSAGE,
|
||||
ROUTER_FALLBACK_MAX_RETRY_COUNT,
|
||||
hasAvailableGatewayModels
|
||||
} from "../../shared/app";
|
||||
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue } from "../../main/presets";
|
||||
import { normalizeProviderBaseUrl as normalizeProviderBaseUrlInput } from "../../shared/provider-url";
|
||||
import { backendService } from "../backend-service";
|
||||
import { RAW_TRACE_SPOOL_DIR } from "../../main/constants";
|
||||
import { loadPersistedApiKeys } from "../../main/api-key-store";
|
||||
import { codexDefaultBaseUrl, readCodexAuth } from "../../main/local-agent-provider-service";
|
||||
import { fetchWithSystemProxy, getSystemProxyUrlForProtocol } from "../../main/system-proxy-fetch";
|
||||
import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "../mcp/network-capture-mcp";
|
||||
import { pluginService } from "../../main/plugins/service";
|
||||
import { proxyService } from "../proxy/service";
|
||||
import { createSseErrorDetector, recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "../../main/request-log-store";
|
||||
import { recordGatewayUsageCapture } from "../../main/usage-store";
|
||||
import { ClaudeCodeRouterPlugin, normalizeRouteSelector } from "./claude-code-router-plugin";
|
||||
import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "./remote-control-service";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue } from "@ccr/core/providers/presets/index";
|
||||
import { normalizeProviderBaseUrl as normalizeProviderBaseUrlInput } from "@ccr/core/providers/url";
|
||||
import { backendService } from "@ccr/core/plugins/backend-service";
|
||||
import { RAW_TRACE_SPOOL_DIR } from "@ccr/core/config/constants";
|
||||
import { loadPersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/service";
|
||||
import { fetchWithSystemProxy, getSystemProxyUrlForProtocol } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "@ccr/core/mcp/network-capture-mcp";
|
||||
import { pluginService } from "@ccr/core/plugins/service";
|
||||
import { proxyService } from "@ccr/core/proxy/service";
|
||||
import { createSseErrorDetector, recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "@ccr/core/observability/request-log-store";
|
||||
import { recordGatewayUsageCapture } from "@ccr/core/usage/store";
|
||||
import { ClaudeCodeRouterPlugin, normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
|
||||
import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "@ccr/core/gateway/remote-control-service";
|
||||
import {
|
||||
claudeCodeEffectiveMaxInputTokens,
|
||||
findModelCatalogEntry,
|
||||
|
|
@ -59,7 +59,7 @@ import {
|
|||
readCatalogCapability,
|
||||
type ModelCatalogCapabilities,
|
||||
type ModelCatalogEntry
|
||||
} from "./model-catalog";
|
||||
} from "@ccr/core/gateway/model-catalog";
|
||||
|
||||
type CoreGatewayProvider = {
|
||||
apikey?: string;
|
||||
|
|
@ -388,11 +388,12 @@ class GatewayService {
|
|||
const runtimeId = randomUUID();
|
||||
const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https");
|
||||
this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId, this.coreAuthToken);
|
||||
const managedChild = this.child;
|
||||
writeManagedCoreGatewayMarker(config, this.child, runtimeId);
|
||||
this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`));
|
||||
this.child.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`));
|
||||
this.child.on("exit", (code, signal) => {
|
||||
void this.handleCoreGatewayExit(code, signal);
|
||||
void this.handleCoreGatewayExit(managedChild, code, signal);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -495,8 +496,8 @@ class GatewayService {
|
|||
});
|
||||
}
|
||||
|
||||
private async handleCoreGatewayExit(code: number | null, signal: NodeJS.Signals | null): Promise<void> {
|
||||
if (this.status.state === "stopped") {
|
||||
private async handleCoreGatewayExit(child: ChildProcess, code: number | null, signal: NodeJS.Signals | null): Promise<void> {
|
||||
if (this.child !== child || this.status.state === "stopped") {
|
||||
return;
|
||||
}
|
||||
removeManagedCoreGatewayMarker(this.config);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import packageJson from "../../../package.json";
|
||||
import packageJson from "../../package.json";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { ProxyNetworkExchange } from "../../shared/app";
|
||||
import { proxyService } from "../proxy/service";
|
||||
import type { ProxyNetworkExchange } from "@ccr/core/contracts/app";
|
||||
import { proxyService } from "@ccr/core/proxy/service";
|
||||
|
||||
type JsonPrimitive = boolean | null | number | string;
|
||||
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { fetchWithSystemProxy } from "../../main/system-proxy-fetch";
|
||||
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import type {
|
||||
GatewayMcpRemoteServerConfig,
|
||||
GatewayMcpServerConfig,
|
||||
GatewayMcpStdioServerConfig,
|
||||
GatewayMcpToolInfo
|
||||
} from "../../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
type JsonRpcMessage = {
|
||||
error?: unknown;
|
||||
|
|
@ -24,8 +24,11 @@ export function modelCatalogPathCandidates(): string[] {
|
|||
process.env.CCR_MODEL_CATALOG_PATH?.trim() || "",
|
||||
process.env.CCR_MODELS_JSON_PATH?.trim() || "",
|
||||
pathResolve(process.cwd(), "models.json"),
|
||||
pathResolve(process.cwd(), "packages", "core", "models.json"),
|
||||
pathResolve(process.cwd(), "packages", "cli", "models.json"),
|
||||
pathResolve(__dirname, "..", "models.json"),
|
||||
pathResolve(__dirname, "..", "assets", "models.json"),
|
||||
pathResolve(__dirname, "..", "..", "models.json"),
|
||||
pathResolve(__dirname, "..", "..", "..", "models.json")
|
||||
]);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { fetchWithSystemProxy } from "./system-proxy-fetch";
|
||||
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
|
||||
type ModelPricingSource = "litellm" | "models.dev" | "openrouter";
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { REQUEST_LOGS_DB_FILE } from "./constants";
|
||||
import { estimateUsageCostUsd } from "./model-pricing-service";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "./sqlite-native";
|
||||
import { normalizeUsageInputTokens } from "./usage-normalization";
|
||||
import { REQUEST_LOGS_DB_FILE } from "@ccr/core/config/constants";
|
||||
import { estimateUsageCostUsd } from "@ccr/core/models/pricing-service";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
|
||||
import { normalizeUsageInputTokens } from "@ccr/core/usage/normalization";
|
||||
import type {
|
||||
AgentAnalysisAgentRow,
|
||||
AgentAnalysisFilter,
|
||||
|
|
@ -38,7 +38,7 @@ import type {
|
|||
RequestLogRetryAttempt,
|
||||
RequestLogStatusFilter,
|
||||
UsageStatsRange
|
||||
} from "../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
type SqlDatabase = BetterSqliteDatabase;
|
||||
type SqlValue = bigint | Buffer | number | string | null;
|
||||
|
|
@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|||
import { readdirSync, statSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { windowsSystemCommand } from "./windows-system";
|
||||
import { windowsSystemCommand } from "@ccr/core/platform/windows-system";
|
||||
|
||||
export type WindowsDesktopAppDiscoveryOptions = {
|
||||
appDirs: string[];
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import path from "node:path";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase, type BetterSqliteStatement } from "../main/sqlite-native";
|
||||
import { createBetterSqliteDatabase, type BetterSqliteDatabase, type BetterSqliteStatement } from "@ccr/core/storage/sqlite-native";
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
export type SqliteValue = bigint | Buffer | number | string | Uint8Array | null;
|
||||
|
|
@ -14,9 +14,9 @@ import type {
|
|||
ProviderAccountMeter,
|
||||
ProviderAccountPluginConnectorConfig,
|
||||
ProviderAccountSnapshot
|
||||
} from "../../shared/app";
|
||||
import { backendService, type RegisteredHttpBackend, type SqliteStore, type SqliteStoreOptions } from "../../server/backend-service";
|
||||
import { CONFIGDIR, DATADIR } from "../constants";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { backendService, type RegisteredHttpBackend, type SqliteStore, type SqliteStoreOptions } from "@ccr/core/plugins/backend-service";
|
||||
import { CONFIGDIR, DATADIR } from "@ccr/core/config/constants";
|
||||
|
||||
type MaybePromise<T> = T | Promise<T>;
|
||||
type PluginLogger = {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import path from "node:path";
|
||||
import type { AppConfig, ProfileConfig, ProfileOpenSurface } from "../shared/app";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
|
||||
import { resolveZcodeConfigFile } from "./zcode-profile-config";
|
||||
import type { AppConfig, ProfileConfig, ProfileOpenSurface } from "@ccr/core/contracts/app";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
|
||||
import { resolveZcodeConfigFile } from "@ccr/core/agents/zcode/profile-config";
|
||||
|
||||
export type ProfileLaunchPlan = {
|
||||
args: string[];
|
||||
|
|
@ -2,18 +2,18 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|||
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { assertAvailableGatewayModels, type AppConfig, type ProfileOpenCommandResult, type ProfileOpenRequest, type ProfileOpenResult, type ProfileRuntimeEntry, type ProfileRuntimeStatus, type ProfileStopResult } from "../shared/app";
|
||||
import { botGatewayProfileEnv } from "./bot-gateway-env";
|
||||
import { applyClaudeAppGatewayConfig, readClaudeAppGatewayApiKeyCandidates } from "./claude-app-gateway-service";
|
||||
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "./claude-app-launch";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
|
||||
import { launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles } from "./codex-app-launch";
|
||||
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import { gatewayService } from "../server/gateway/service";
|
||||
import { buildProfileLaunchPlan, findProfileForOpen, profileLaunchSpawnCommand, profileOpenCommand, resolveClaudeCodeSettingsFile, resolveProfileOpenSurface } from "./profile-launch-core";
|
||||
import { applyProfileConfig, cleanupGeneratedBinBackups } from "./profile-service";
|
||||
import { broadcastWindowsEnvironmentChanged, windowsSystemCommand } from "./windows-system";
|
||||
import { assertAvailableGatewayModels, type AppConfig, type ProfileOpenCommandResult, type ProfileOpenRequest, type ProfileOpenResult, type ProfileRuntimeEntry, type ProfileRuntimeStatus, type ProfileStopResult } from "@ccr/core/contracts/app";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import { applyClaudeAppGatewayConfig, readClaudeAppGatewayApiKeyCandidates } from "@ccr/core/agents/claude-app/gateway-service";
|
||||
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "@ccr/core/agents/claude-app/launch";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
|
||||
import { launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles } from "@ccr/core/agents/codex/app-launch";
|
||||
import { codexCliMiddlewareRuntimeScript } from "@ccr/core/agents/codex/cli-middleware-runtime";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import { gatewayService } from "@ccr/core/gateway/service";
|
||||
import { buildProfileLaunchPlan, findProfileForOpen, profileLaunchSpawnCommand, profileOpenCommand, resolveClaudeCodeSettingsFile, resolveProfileOpenSurface } from "@ccr/core/profiles/launch-core";
|
||||
import { applyProfileConfig, cleanupGeneratedBinBackups } from "@ccr/core/profiles/service";
|
||||
import { broadcastWindowsEnvironmentChanged, windowsSystemCommand } from "@ccr/core/platform/windows-system";
|
||||
|
||||
const ccrPathBlockStart = "# >>> Claude Code Router CLI >>>";
|
||||
const ccrPathBlockEnd = "# <<< Claude Code Router CLI <<<";
|
||||
|
|
@ -2,16 +2,16 @@ import { randomBytes } from "node:crypto";
|
|||
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "../shared/app";
|
||||
import { replacePersistedApiKeys } from "./api-key-store";
|
||||
import { botGatewayProfileEnv } from "./bot-gateway-env";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment";
|
||||
import { writeCodexCompatibleAppModelCatalog } from "./codex-app-launch";
|
||||
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime";
|
||||
import { codexModelCatalogJson } from "./codex-model-catalog";
|
||||
import { CONFIGDIR } from "./constants";
|
||||
import { resolveZcodeConfigFile, writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "./zcode-profile-config";
|
||||
import { normalizeRouteSelector } from "../server/gateway/claude-code-router-plugin";
|
||||
import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "@ccr/core/contracts/app";
|
||||
import { replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
|
||||
import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
|
||||
import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
|
||||
import { writeCodexCompatibleAppModelCatalog } from "@ccr/core/agents/codex/app-launch";
|
||||
import { codexCliMiddlewareRuntimeScript } from "@ccr/core/agents/codex/cli-middleware-runtime";
|
||||
import { codexModelCatalogJson } from "@ccr/core/agents/codex/model-catalog";
|
||||
import { CONFIGDIR } from "@ccr/core/config/constants";
|
||||
import { resolveZcodeConfigFile, writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "@ccr/core/agents/zcode/profile-config";
|
||||
import { normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
|
||||
|
||||
const managedRootStart = "# BEGIN CCR managed profile";
|
||||
const managedRootEnd = "# END CCR managed profile";
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { loadAppConfig } from "./config";
|
||||
import { attachCodexRateLimitResetCreditDetails } from "./local-agent-providers/codex";
|
||||
import { localAgentProviderApiKey, readCodexAuth } from "./local-agent-provider-service";
|
||||
import { pluginService } from "./plugins/service";
|
||||
import { getUsageTotalsSince } from "./usage-store";
|
||||
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey } from "./presets";
|
||||
import { fetchWithSystemProxy } from "./system-proxy-fetch";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "../shared/provider-url";
|
||||
import { loadAppConfig } from "@ccr/core/config/config";
|
||||
import { attachCodexRateLimitResetCreditDetails } from "@ccr/core/agents/local-providers/codex";
|
||||
import { localAgentProviderApiKey, readCodexAuth } from "@ccr/core/agents/local-providers/service";
|
||||
import { pluginService } from "@ccr/core/plugins/service";
|
||||
import { getUsageTotalsSince } from "@ccr/core/usage/store";
|
||||
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
|
||||
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import type {
|
||||
AppConfig,
|
||||
GatewayProviderConfig,
|
||||
|
|
@ -36,7 +36,7 @@ import type {
|
|||
ProviderAccountStandardConnectorConfig,
|
||||
ProviderCredentialConfig,
|
||||
ProviderAccountStatus
|
||||
} from "../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
type CacheEntry = {
|
||||
expiresAt: number;
|
||||
|
|
@ -2,10 +2,10 @@ import { createHash } from "node:crypto";
|
|||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { PROVIDER_ICON_CACHE_DIR } from "./constants";
|
||||
import { fetchWithSystemProxy } from "./system-proxy-fetch";
|
||||
import type { ProviderIconDetectionRequest, ProviderIconDetectionResult } from "../shared/app";
|
||||
import { compactProviderUrl, providerUrlWithDefaultScheme } from "../shared/provider-url";
|
||||
import { PROVIDER_ICON_CACHE_DIR } from "@ccr/core/config/constants";
|
||||
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import type { ProviderIconDetectionRequest, ProviderIconDetectionResult } from "@ccr/core/contracts/app";
|
||||
import { compactProviderUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
|
||||
const faviconServiceUrl = "https://t0.gstatic.com/faviconV2";
|
||||
const maxProviderIconBytes = 1024 * 1024;
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { lookup } from "node:dns/promises";
|
||||
import https from "node:https";
|
||||
import net from "node:net";
|
||||
import { parseProviderManifestPayload } from "../shared/deep-link";
|
||||
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey, providerIdentitySafetyIssue } from "./presets";
|
||||
import { providerUrlWithDefaultScheme } from "../shared/provider-url";
|
||||
import { parseProviderManifestPayload } from "@ccr/core/contracts/deep-link";
|
||||
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey, providerIdentitySafetyIssue } from "@ccr/core/providers/presets/index";
|
||||
import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import type {
|
||||
GatewayProviderConfig,
|
||||
ProviderAccountConnectorConfig,
|
||||
|
|
@ -12,7 +12,7 @@ import type {
|
|||
ProviderDeepLinkPayload,
|
||||
ProviderManifestFetchRequest,
|
||||
ProviderManifestFetchResult
|
||||
} from "../shared/app";
|
||||
} from "@ccr/core/contracts/app";
|
||||
|
||||
type SafeAddress = {
|
||||
address: string;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ProviderCatalogModelsRequest, ProviderCatalogModelsResult } from "../shared/app";
|
||||
import { providerUrlWithDefaultScheme } from "../shared/provider-url";
|
||||
import { loadModelCatalogPayload } from "./model-catalog-file";
|
||||
import { findProviderPreset, findProviderPresetByBaseUrl } from "./presets";
|
||||
import type { ProviderCatalogModelsRequest, ProviderCatalogModelsResult } from "@ccr/core/contracts/app";
|
||||
import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
import { loadModelCatalogPayload } from "@ccr/core/models/catalog-file";
|
||||
import { findProviderPreset, findProviderPresetByBaseUrl } from "@ccr/core/providers/presets/index";
|
||||
|
||||
type CatalogProviderEntry = {
|
||||
apiUrls: string[];
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const anthropicProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const bailianProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const deepSeekProviderAccountConfig: ProviderAccountConfig = {
|
||||
connectors: [
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const geminiProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
import { anthropicProviderPreset } from "./anthropic";
|
||||
import { bailianProviderPreset } from "./bailian";
|
||||
import { deepSeekProviderPreset } from "./deepseek";
|
||||
import { geminiProviderPreset } from "./gemini";
|
||||
import { kimiCodingProviderPreset } from "./kimi-coding";
|
||||
import { mistralProviderPreset } from "./mistral";
|
||||
import { moonshotChinaProviderPreset, moonshotGlobalProviderPreset } from "./moonshot";
|
||||
import { openaiProviderPreset } from "./openai";
|
||||
import { openRouterProviderPreset } from "./openrouter";
|
||||
import { runApiProviderPreset } from "./runapi";
|
||||
import { siliconFlowProviderPreset } from "./siliconflow";
|
||||
import { teamoRouterProviderPreset } from "./teamorouter";
|
||||
import { zaiGlobalCodingProviderPreset } from "./zai-global-coding";
|
||||
import { zaiGlobalGeneralProviderPreset } from "./zai-global-general";
|
||||
import { zhipuCnCodingProviderPreset } from "./zhipu-cn-coding";
|
||||
import { zhipuCnGeneralProviderPreset } from "./zhipu-cn-general";
|
||||
import { anthropicProviderPreset } from "@ccr/core/providers/presets/anthropic/index";
|
||||
import { bailianProviderPreset } from "@ccr/core/providers/presets/bailian/index";
|
||||
import { deepSeekProviderPreset } from "@ccr/core/providers/presets/deepseek/index";
|
||||
import { geminiProviderPreset } from "@ccr/core/providers/presets/gemini/index";
|
||||
import { kimiCodingProviderPreset } from "@ccr/core/providers/presets/kimi-coding/index";
|
||||
import { mistralProviderPreset } from "@ccr/core/providers/presets/mistral/index";
|
||||
import { moonshotChinaProviderPreset, moonshotGlobalProviderPreset } from "@ccr/core/providers/presets/moonshot/index";
|
||||
import { openaiProviderPreset } from "@ccr/core/providers/presets/openai/index";
|
||||
import { openRouterProviderPreset } from "@ccr/core/providers/presets/openrouter/index";
|
||||
import { runApiProviderPreset } from "@ccr/core/providers/presets/runapi/index";
|
||||
import { siliconFlowProviderPreset } from "@ccr/core/providers/presets/siliconflow/index";
|
||||
import { teamoRouterProviderPreset } from "@ccr/core/providers/presets/teamorouter/index";
|
||||
import { zaiGlobalCodingProviderPreset } from "@ccr/core/providers/presets/zai-global-coding/index";
|
||||
import { zaiGlobalGeneralProviderPreset } from "@ccr/core/providers/presets/zai-global-general/index";
|
||||
import { zhipuCnCodingProviderPreset } from "@ccr/core/providers/presets/zhipu-cn-coding/index";
|
||||
import { zhipuCnGeneralProviderPreset } from "@ccr/core/providers/presets/zhipu-cn-general/index";
|
||||
import {
|
||||
findProviderPresetByBaseUrlInList,
|
||||
findProviderPresetInList,
|
||||
|
|
@ -22,8 +22,8 @@ import {
|
|||
providerEndpointCanReceiveProviderApiKeyInList,
|
||||
providerIdentitySafetyIssueInList,
|
||||
providerPresetMatchesBaseUrl
|
||||
} from "../../shared/provider-preset-utils";
|
||||
import type { ProviderIdentitySafetyIssue, ProviderPreset } from "../../shared/provider-presets";
|
||||
} from "@ccr/core/providers/presets/utils";
|
||||
import type { ProviderIdentitySafetyIssue, ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const providerPresets: ProviderPreset[] = [
|
||||
openaiProviderPreset,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const kimiCodingProviderAccountConfig: ProviderAccountConfig = {
|
||||
connectors: [
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const mistralProviderAccountConfig: ProviderAccountConfig = {
|
||||
connectors: [
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const moonshotGlobalProviderAccountConfig: ProviderAccountConfig = {
|
||||
connectors: [
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const openaiProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const openRouterProviderAccountConfig: ProviderAccountConfig = {
|
||||
connectors: [
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const runApiProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const siliconFlowProviderAccountConfig: ProviderAccountConfig = {
|
||||
connectors: [
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets";
|
||||
import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
export const teamoRouterProviderPreset: ProviderPreset = {
|
||||
account: defaultProviderAccountConfig,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GatewayProviderProtocol, ProviderAccountConfig } from "./app";
|
||||
import type { GatewayProviderProtocol, ProviderAccountConfig } from "@ccr/core/contracts/app";
|
||||
|
||||
export type ProviderPresetEndpoint = {
|
||||
baseUrl: string;
|
||||
|
|
@ -3,8 +3,8 @@ import {
|
|||
type ProviderIdentitySafetyIssue,
|
||||
type ProviderPreset,
|
||||
type ProviderPresetEndpoint
|
||||
} from "./provider-presets";
|
||||
import { providerUrlWithDefaultScheme } from "./provider-url";
|
||||
} from "@ccr/core/providers/presets/types";
|
||||
import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
|
||||
|
||||
export function findProviderPresetInList(
|
||||
presets: ProviderPreset[],
|
||||
|
|
@ -188,7 +188,11 @@ function providerEndpointMatchesBaseUrl(endpointBaseUrl: string, baseUrl: string
|
|||
|
||||
const endpointPath = normalizeProviderPresetPath(endpoint.pathname);
|
||||
const candidatePath = normalizeProviderPresetPath(candidate.pathname);
|
||||
return endpointPath === "/" || candidatePath === "/" || candidatePath === endpointPath || candidatePath.startsWith(`${endpointPath}/`);
|
||||
return endpointPath === "/" ||
|
||||
candidatePath === "/" ||
|
||||
candidatePath === endpointPath ||
|
||||
candidatePath.startsWith(`${endpointPath}/`) ||
|
||||
endpointPath.startsWith(`${candidatePath}/`);
|
||||
}
|
||||
|
||||
function providerEndpointHost(baseUrl: string): string | undefined {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const zaiQuotaMapping: ProviderAccountMappingConfig = {
|
||||
meters: [
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const zaiQuotaMapping: ProviderAccountMappingConfig = {
|
||||
meters: [
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const zhipuQuotaMapping: ProviderAccountMappingConfig = {
|
||||
meters: [
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app";
|
||||
import type { ProviderPreset } from "../../../shared/provider-presets";
|
||||
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
|
||||
import type { ProviderPreset } from "@ccr/core/providers/presets/types";
|
||||
|
||||
const zhipuQuotaMapping: ProviderAccountMappingConfig = {
|
||||
meters: [
|
||||
|
|
@ -10,15 +10,15 @@ import type {
|
|||
GatewayProviderProbeRequest,
|
||||
GatewayProviderProbeResult,
|
||||
GatewayProviderProtocol
|
||||
} from "../shared/app";
|
||||
import { providerApiKeySafetyIssue } from "./presets";
|
||||
import { fetchWithSystemProxy } from "./system-proxy-fetch";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { providerApiKeySafetyIssue } from "@ccr/core/providers/presets/index";
|
||||
import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
|
||||
import {
|
||||
compactProviderUrl,
|
||||
parseProviderBaseUrl,
|
||||
providerBaseUrlForProtocol,
|
||||
type ParsedProviderBaseUrl
|
||||
} from "../shared/provider-url";
|
||||
} from "@ccr/core/providers/url";
|
||||
|
||||
type ModelSource = NonNullable<GatewayProviderProbeResult["modelSource"]>;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { GatewayProviderProtocol } from "./app";
|
||||
import type { GatewayProviderProtocol } from "@ccr/core/contracts/app";
|
||||
|
||||
export type ParsedProviderBaseUrl = {
|
||||
anthropicBaseUrl: string;
|
||||
|
|
@ -4,7 +4,7 @@ import net from "node:net";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import forge from "node-forge";
|
||||
import { CERTDIR, PROXY_CA_CERT_DER_FILE, PROXY_CA_CERT_FILE, PROXY_CA_KEY_FILE } from "../../main/constants";
|
||||
import { CERTDIR, PROXY_CA_CERT_DER_FILE, PROXY_CA_CERT_FILE, PROXY_CA_KEY_FILE } from "@ccr/core/config/constants";
|
||||
|
||||
const pki = forge.pki;
|
||||
const certificateDirectoryMode = 0o700;
|
||||
|
|
@ -21,10 +21,10 @@ import type {
|
|||
ProxyNetworkSnapshot,
|
||||
ProxyRouteTarget,
|
||||
ProxyStatus
|
||||
} from "../../shared/app";
|
||||
import { PROXY_CA_CERT_FILE } from "../../main/constants";
|
||||
import { windowsSystemCommand } from "../../main/windows-system";
|
||||
import { pluginService, type GatewayPluginProxyRouteMatch } from "../../main/plugins/service";
|
||||
} from "@ccr/core/contracts/app";
|
||||
import { PROXY_CA_CERT_FILE } from "@ccr/core/config/constants";
|
||||
import { windowsSystemCommand } from "@ccr/core/platform/windows-system";
|
||||
import { pluginService, type GatewayPluginProxyRouteMatch } from "@ccr/core/plugins/service";
|
||||
import {
|
||||
createCertificateForHost,
|
||||
ensureProxyCertificateAuthority,
|
||||
|
|
@ -36,8 +36,8 @@ import {
|
|||
readProxyCertificateFingerprintSha256,
|
||||
readProxyCertificateAuthority,
|
||||
type CertificateAuthority
|
||||
} from "./certificates";
|
||||
import { formatUpstreamProxy, readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "./system-proxy";
|
||||
} from "@ccr/core/proxy/certificates";
|
||||
import { formatUpstreamProxy, readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "@ccr/core/proxy/system-proxy";
|
||||
|
||||
type MitmServer = {
|
||||
host: string;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { ProxyAgent, type Dispatcher } from "undici";
|
||||
import { loadAppConfig } from "./config";
|
||||
import { readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "../server/proxy/system-proxy";
|
||||
import type { AppConfig } from "../shared/app";
|
||||
import { loadAppConfig } from "@ccr/core/config/config";
|
||||
import { readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "@ccr/core/proxy/system-proxy";
|
||||
import type { AppConfig } from "@ccr/core/contracts/app";
|
||||
|
||||
type FetchInitWithDispatcher = RequestInit & {
|
||||
dispatcher?: Dispatcher;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue