Harden Claude app gateway and design CDP handling

This commit is contained in:
musistudio
2026-06-25 19:25:26 +08:00
parent e08b6ac601
commit f285e973ed
7 changed files with 81 additions and 47 deletions
+2 -13
View File
@@ -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
+16 -5
View File
@@ -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
View File
@@ -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);
})();`;
}
+4 -3
View File
@@ -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);
+12 -1
View File
@@ -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}.`,
+1 -7
View File
@@ -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(
+16 -6
View File
@@ -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[] {