mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-10-30 19:31:53 +08:00
Harden Claude app gateway and design CDP handling
This commit is contained in:
@@ -39,23 +39,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and publish macOS artifacts
|
||||
- name: Build and publish ad-hoc macOS artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_KEYCHAIN: ${{ secrets.APPLE_KEYCHAIN }}
|
||||
APPLE_KEYCHAIN_PROFILE: ${{ secrets.APPLE_KEYCHAIN_PROFILE }}
|
||||
run: |
|
||||
node build/macos-release-preflight.mjs
|
||||
npm run build:assets
|
||||
npx electron-builder --mac --publish always
|
||||
npx electron-builder --config build/electron-builder.local.cjs --mac --publish always
|
||||
|
||||
windows:
|
||||
name: Windows
|
||||
|
||||
@@ -7904,6 +7904,7 @@ function renderDefaultDesignIndex(me, scriptPath, stylePath, options = {}) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Claude Design</title>
|
||||
<link rel="icon" type="image/png" href="/design/favicon.png"/>
|
||||
${claudeAppDesktopFeaturesScript()}
|
||||
<script>
|
||||
// FOUC guard: set data-theme before any stylesheet loads so the first
|
||||
// paint uses the right palette. Mirrors hooks/useTheme.ts. The stored
|
||||
@@ -7935,7 +7936,6 @@ ${stylePreloads}
|
||||
<body>
|
||||
${designMeJsonScript(me)}
|
||||
${designModelPreferenceResetScript(me)}
|
||||
${claudeAppDesktopFeaturesScript()}
|
||||
${designMeGlobalScript(me)}
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
@@ -7960,25 +7960,36 @@ function injectDesignMeIntoHtml(html, me) {
|
||||
nextHtml = nextHtml.replace(meJsonPattern, meJsonScript);
|
||||
}
|
||||
|
||||
const earlySnippets = [];
|
||||
const snippets = [];
|
||||
if (!nextHtml.includes("ccr-claude-app-desktop-features")) {
|
||||
earlySnippets.push(claudeAppDesktopFeaturesScript());
|
||||
}
|
||||
if (!meJsonPattern.test(nextHtml)) {
|
||||
snippets.push(meJsonScript);
|
||||
}
|
||||
if (!nextHtml.includes("ccr-claude-design-model-reset")) {
|
||||
snippets.push(designModelPreferenceResetScript(me));
|
||||
}
|
||||
if (!nextHtml.includes("ccr-claude-app-desktop-features")) {
|
||||
snippets.push(claudeAppDesktopFeaturesScript());
|
||||
}
|
||||
if (!nextHtml.includes("__OMELETTE_ME__")) {
|
||||
snippets.push(designMeGlobalScript(me));
|
||||
}
|
||||
if (earlySnippets.length) {
|
||||
nextHtml = injectHtmlAfterHeadOpen(nextHtml, earlySnippets.join("\n "));
|
||||
}
|
||||
if (!snippets.length) {
|
||||
return nextHtml;
|
||||
}
|
||||
return injectHtmlAfterBodyOpen(nextHtml, snippets.join("\n "));
|
||||
}
|
||||
|
||||
function injectHtmlAfterHeadOpen(html, snippet) {
|
||||
if (/<head\b[^>]*>/i.test(html)) {
|
||||
return html.replace(/<head\b([^>]*)>/i, `<head$1>\n ${snippet}`);
|
||||
}
|
||||
return injectHtmlAfterBodyOpen(html, snippet);
|
||||
}
|
||||
|
||||
function injectHtmlAfterBodyOpen(html, snippet) {
|
||||
if (/<body\b[^>]*>/i.test(html)) {
|
||||
return html.replace(/<body\b([^>]*)>/i, `<body$1>\n ${snippet}`);
|
||||
@@ -7995,7 +8006,7 @@ function designMeGlobalScript(me) {
|
||||
}
|
||||
|
||||
function claudeAppDesktopFeaturesScript() {
|
||||
return `<script id="ccr-claude-app-desktop-features">(function(){try{var forced={claudeDesignWindow:{status:'supported'}};function merge(value){var next=Object.assign({},value||window.desktopBootFeatures||{},forced);window.desktopBootFeatures=next;return next;}merge(window.desktopBootFeatures);window.claude=window.claude||{};window.claude.settings=window.claude.settings||{};var existing=window.claude.settings.AppFeatures||{};if(existing.__ccrClaudeDesignPatched){return;}var previous=existing.getSupportedFeatures;window.claude.settings.AppFeatures=Object.assign({},existing,{__ccrClaudeDesignPatched:true,getSupportedFeatures:function(){if(typeof previous==='function'){return Promise.resolve(previous.call(existing)).then(merge,function(){return merge();});}return Promise.resolve(merge());}});}catch(e){}})();</script>`;
|
||||
return `<script id="ccr-claude-app-desktop-features">(function(){try{var root=globalThis;var forced={claudeDesignWindow:{status:'supported'}};function merge(value){return Object.assign({},value||{},forced);}var bootFeatures=merge(root.desktopBootFeatures);try{Object.defineProperty(root,'desktopBootFeatures',{configurable:true,get:function(){return bootFeatures;},set:function(value){bootFeatures=merge(value);}});}catch(e){root.desktopBootFeatures=bootFeatures;}function patch(container){if(!container){return;}var existing=container.AppFeatures||{};if(existing.__ccrClaudeDesignPatched){return;}var previous=existing.getSupportedFeatures;container.AppFeatures=Object.assign({},existing,{__ccrClaudeDesignPatched:true,getSupportedFeatures:function(){if(typeof previous==='function'){return Promise.resolve(previous.call(existing)).then(merge,function(){return merge();});}return Promise.resolve(merge());}});}root["claude.settings"]=root["claude.settings"]||{};patch(root["claude.settings"]);root.claude=root.claude||{};root.claude.settings=root.claude.settings||{};patch(root.claude.settings);}catch(e){}})();</script>`;
|
||||
}
|
||||
|
||||
function designModelPreferenceResetScript(me) {
|
||||
|
||||
+30
-12
@@ -45,7 +45,14 @@ const claudeAppDesignCdpConnectTimeoutMs = 15_000;
|
||||
const claudeAppDesignCdpKeepAliveMs = 45_000;
|
||||
const claudeAppDesignCdpPollIntervalMs = 250;
|
||||
|
||||
export function shouldEnableClaudeAppDesignCdp(): boolean {
|
||||
return process.env.CCR_CLAUDE_APP_DESIGN_CDP?.trim().toLowerCase() === "true";
|
||||
}
|
||||
|
||||
export async function reserveClaudeAppCdpPort(logger: ClaudeAppCdpLogger = console): Promise<number | undefined> {
|
||||
if (!shouldEnableClaudeAppDesignCdp()) {
|
||||
return undefined;
|
||||
}
|
||||
const configured = Number(process.env.CCR_CLAUDE_APP_CDP_PORT);
|
||||
if (Number.isInteger(configured) && configured > 0 && configured <= 65535) {
|
||||
return configured;
|
||||
@@ -63,7 +70,7 @@ export function prepareClaudeAppCdpUserDataDir(userDataDir: string): void {
|
||||
}
|
||||
|
||||
export function scheduleClaudeAppDesignCdp(options: ClaudeAppDesignCdpOptions): void {
|
||||
if (process.env.CCR_CLAUDE_APP_DESIGN_CDP?.trim().toLowerCase() === "false") {
|
||||
if (!shouldEnableClaudeAppDesignCdp()) {
|
||||
return;
|
||||
}
|
||||
const logger = options.logger || console;
|
||||
@@ -215,19 +222,30 @@ function claudeAppDesignFeatureScript(): string {
|
||||
} catch (_) {
|
||||
globalThis.desktopBootFeatures = bootFeatures;
|
||||
}
|
||||
function patchAppFeatures(container) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const existing = container.AppFeatures || {};
|
||||
if (existing.__ccrClaudeDesignPatched) {
|
||||
return;
|
||||
}
|
||||
const previous = existing.getSupportedFeatures;
|
||||
container.AppFeatures = Object.assign({}, existing, {
|
||||
__ccrClaudeDesignPatched: true,
|
||||
getSupportedFeatures() {
|
||||
if (typeof previous === "function") {
|
||||
return Promise.resolve(previous.call(existing)).then(merge, () => merge());
|
||||
}
|
||||
return Promise.resolve(merge());
|
||||
}
|
||||
});
|
||||
}
|
||||
globalThis["claude.settings"] = globalThis["claude.settings"] || {};
|
||||
patchAppFeatures(globalThis["claude.settings"]);
|
||||
globalThis.claude = globalThis.claude || {};
|
||||
globalThis.claude.settings = globalThis.claude.settings || {};
|
||||
const existing = globalThis.claude.settings.AppFeatures || {};
|
||||
const previous = existing.getSupportedFeatures;
|
||||
globalThis.claude.settings.AppFeatures = Object.assign({}, existing, {
|
||||
__ccrClaudeDesignPatched: true,
|
||||
getSupportedFeatures() {
|
||||
if (typeof previous === "function") {
|
||||
return Promise.resolve(previous.call(existing)).then(merge, () => merge());
|
||||
}
|
||||
return Promise.resolve(merge());
|
||||
}
|
||||
});
|
||||
patchAppFeatures(globalThis.claude.settings);
|
||||
})();`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1984,11 +1984,12 @@ function collaborationModes() {
|
||||
function modelList(params, existingResult) {
|
||||
const runtimeAgent = codexRuntimeAgent();
|
||||
const isClaudeCodeRuntime = normalizeRemoteFrontendMode(agentEnv(runtimeAgent, "REMOTE_FRONTEND_MODE", "CORE_MODE")) === "claude-code";
|
||||
const configured = normalizeModelSelector(agentEnv(runtimeAgent, "MODEL") || nonEmptyEnv("CODEXL_CLAUDE_CODE_MODEL") || (isClaudeCodeRuntime ? DEFAULT_MODEL : ""));
|
||||
const configured = normalizeModelSelector(agentEnv(runtimeAgent, "MODEL") || nonEmptyEnv("CODEXL_CLAUDE_CODE_MODEL"));
|
||||
const selected = configured || (isClaudeCodeRuntime ? DEFAULT_MODEL : "");
|
||||
const fallbackIds = isClaudeCodeRuntime
|
||||
? [configured, DEFAULT_MODEL, "claude-opus-4-5", "claude-haiku-4-5"].filter(Boolean)
|
||||
? [configured].filter(Boolean)
|
||||
: [configured].filter((model) => model && !isClaudeCodeOnlyModel(model));
|
||||
const models = mergeModelListItems(extractModelListItems(existingResult), [...catalogModelIds(), ...fallbackIds], configured);
|
||||
const models = mergeModelListItems(extractModelListItems(existingResult), [...catalogModelIds(), ...fallbackIds], selected);
|
||||
const offset = Number(params.cursor || 0) || 0;
|
||||
const limit = Number(params.limit || models.length) || models.length;
|
||||
const data = models.slice(offset, offset + limit);
|
||||
|
||||
@@ -140,7 +140,18 @@ async function openClaudeAppProfile(config: AppConfig, profile: ReturnType<typeo
|
||||
dataDir: resolveClaudeAppProfileUserDataDir(CONFIGDIR, profile)
|
||||
});
|
||||
await ensureGatewayConfigRunning(profileGatewayConfig, "Claude App");
|
||||
activateProfileAppWindow(registerProfileApp(profile, "app", await launchClaudeAppProfile(CONFIGDIR, profile, profileGatewayConfig)));
|
||||
const entry = registerProfileApp(profile, "app", await launchClaudeAppProfile(CONFIGDIR, profile, profileGatewayConfig));
|
||||
const started = await waitForProfileAppStart(entry, 12000);
|
||||
if (!started) {
|
||||
cleanupProfileAppEntry(profileRuntimeKey(profile.id, "app"), entry);
|
||||
sendProfileProcessSignal(entry.pid, "SIGTERM");
|
||||
throw new Error([
|
||||
`Claude App did not open a window for ${profile.name || profile.id}.`,
|
||||
`Command: ${entry.command}`,
|
||||
`User data: ${entry.userDataDir}`
|
||||
].join(" "));
|
||||
}
|
||||
activateProfileAppWindow(entry);
|
||||
startClaudeAppBotWorker(config, profile);
|
||||
return {
|
||||
message: `Opened Claude App with ${profile.name || profile.id}.`,
|
||||
|
||||
@@ -3574,13 +3574,7 @@ function isClaudeCodeUserAgent(headers: IncomingHttpHeaders): boolean {
|
||||
return false;
|
||||
}
|
||||
const normalized = userAgent.toLowerCase();
|
||||
return normalized.includes("@anthropic-ai/claude-code") ||
|
||||
normalized.includes("claudecode") ||
|
||||
normalized.includes("claude_code") ||
|
||||
normalized.includes("claude-code") ||
|
||||
normalized.includes("claude_cli") ||
|
||||
normalized.includes("claude-cli") ||
|
||||
normalized.includes("claude cli");
|
||||
return normalized.includes("claude");
|
||||
}
|
||||
|
||||
function prepareCursorOpenAICompatChatBody(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { normalizeProfileScopeValue } from "./app";
|
||||
export const CLAUDE_APP_FALLBACK_MODEL = "claude-sonnet-4-5";
|
||||
export const CLAUDE_APP_ONE_MILLION_CONTEXT_SUFFIX = "[1m]";
|
||||
|
||||
const CLAUDE_APP_ROUTE_NAMES = [
|
||||
const CLAUDE_APP_LEGACY_ROUTE_NAMES = [
|
||||
"claude-sonnet-4-5",
|
||||
"claude-haiku-4-5",
|
||||
"claude-opus-4-5",
|
||||
@@ -22,6 +22,7 @@ const CLAUDE_APP_ROUTE_NAMES = [
|
||||
export type ClaudeAppGatewayModelRoute = {
|
||||
displayName: string;
|
||||
id: string;
|
||||
legacyId?: string;
|
||||
oneMillionContext: boolean;
|
||||
targetModel: string;
|
||||
};
|
||||
@@ -40,15 +41,16 @@ export function buildClaudeAppGatewayModelRoutes(
|
||||
config: Pick<AppConfig, "Providers" | "Router" | "profile" | "virtualModelProfiles">,
|
||||
options: ClaudeAppGatewayModelRouteOptions = {}
|
||||
): ClaudeAppGatewayModelRoute[] {
|
||||
const targetModels = claudeAppGatewayTargetModels(config).slice(0, CLAUDE_APP_ROUTE_NAMES.length);
|
||||
const targetModels = claudeAppGatewayTargetModels(config);
|
||||
const displayNames = claudeAppGatewayDisplayNames(targetModels);
|
||||
return targetModels.map((rawTargetModel, index) => {
|
||||
const targetModel = stripClaudeAppGatewayOneMillionContextSuffix(rawTargetModel);
|
||||
const oneMillionContext = claudeAppGatewaySupportsOneMillionContext(rawTargetModel, options);
|
||||
const routeId = claudeAppGatewayRouteId(index);
|
||||
const routeId = claudeAppGatewayRouteId(rawTargetModel);
|
||||
return {
|
||||
displayName: oneMillionContext ? `${displayNames[index]} (1M context)` : displayNames[index],
|
||||
id: routeId,
|
||||
legacyId: claudeAppGatewayLegacyRouteId(index),
|
||||
oneMillionContext,
|
||||
targetModel
|
||||
};
|
||||
@@ -63,8 +65,11 @@ export function resolveClaudeAppGatewayRouteModel(
|
||||
const normalized = model.trim().toLowerCase();
|
||||
return buildClaudeAppGatewayModelRoutes(config, options).find((route) => {
|
||||
const routeId = route.id.toLowerCase();
|
||||
const legacyRouteId = route.legacyId?.toLowerCase();
|
||||
return routeId === normalized ||
|
||||
stripClaudeAppGatewayOneMillionContextSuffix(routeId).toLowerCase() === normalized;
|
||||
stripClaudeAppGatewayOneMillionContextSuffix(routeId).toLowerCase() === normalized ||
|
||||
legacyRouteId === normalized ||
|
||||
(legacyRouteId ? stripClaudeAppGatewayOneMillionContextSuffix(legacyRouteId).toLowerCase() === normalized : false);
|
||||
})?.targetModel;
|
||||
}
|
||||
|
||||
@@ -153,8 +158,13 @@ function claudeAppGatewaySupportsOneMillionContext(
|
||||
Boolean(options.supportsOneMillionContext?.(baseModel));
|
||||
}
|
||||
|
||||
function claudeAppGatewayRouteId(index: number): string {
|
||||
return CLAUDE_APP_ROUTE_NAMES[index] ?? CLAUDE_APP_FALLBACK_MODEL;
|
||||
function claudeAppGatewayLegacyRouteId(index: number): string | undefined {
|
||||
return CLAUDE_APP_LEGACY_ROUTE_NAMES[index];
|
||||
}
|
||||
|
||||
function claudeAppGatewayRouteId(model: string): string {
|
||||
const normalized = model.trim();
|
||||
return normalized.toLowerCase().startsWith("claude-") ? normalized : `claude-${normalized}`;
|
||||
}
|
||||
|
||||
function claudeAppGatewayDisplayNames(models: string[]): string[] {
|
||||
|
||||
Reference in New Issue
Block a user