mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
fix(proxy): resolve merge conflict in claude adapter — combine smart URL path detection with beta=true
Merge both features from feat/smart-url-path-detection and main: - Smart URL path detection: skip appending endpoint when base_url already ends with API path - URL suffix preservation: preserve query/fragment from base_url via split_url_suffix - v1 dedup: boundary-safe deduplication of /v1/v1 - ?beta=true: auto-append for /v1/messages endpoints (DuckCoding compatibility) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -53,6 +53,81 @@ export function CommonConfigEditor({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Mirror value prop to local state so checkbox toggles and JsonEditor stay in sync
|
||||
// (parent uses form.getValues which doesn't trigger re-renders)
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleLocalChange = useCallback(
|
||||
(newValue: string) => {
|
||||
setLocalValue(newValue);
|
||||
onChange(newValue);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const toggleStates = useMemo(() => {
|
||||
try {
|
||||
const config = JSON.parse(localValue);
|
||||
return {
|
||||
hideAttribution:
|
||||
config?.attribution?.commit === "" && config?.attribution?.pr === "",
|
||||
alwaysThinking: config?.alwaysThinkingEnabled === true,
|
||||
teammates:
|
||||
config?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === "1" ||
|
||||
config?.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS === 1,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
hideAttribution: false,
|
||||
alwaysThinking: false,
|
||||
teammates: false,
|
||||
};
|
||||
}
|
||||
}, [localValue]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(toggleKey: string, checked: boolean) => {
|
||||
try {
|
||||
const config = JSON.parse(localValue || "{}");
|
||||
|
||||
switch (toggleKey) {
|
||||
case "hideAttribution":
|
||||
if (checked) {
|
||||
config.attribution = { commit: "", pr: "" };
|
||||
} else {
|
||||
delete config.attribution;
|
||||
}
|
||||
break;
|
||||
case "alwaysThinking":
|
||||
if (checked) {
|
||||
config.alwaysThinkingEnabled = true;
|
||||
} else {
|
||||
delete config.alwaysThinkingEnabled;
|
||||
}
|
||||
break;
|
||||
case "teammates":
|
||||
if (!config.env) config.env = {};
|
||||
if (checked) {
|
||||
config.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
||||
} else {
|
||||
delete config.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
||||
if (Object.keys(config.env).length === 0) delete config.env;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
handleLocalChange(JSON.stringify(config, null, 2));
|
||||
} catch {
|
||||
// Don't modify if JSON is invalid
|
||||
}
|
||||
},
|
||||
[localValue, handleLocalChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
@@ -91,9 +166,40 @@ export function CommonConfigEditor({
|
||||
{commonConfigError}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
|
||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toggleStates.hideAttribution}
|
||||
onChange={(e) =>
|
||||
handleToggle("hideAttribution", e.target.checked)
|
||||
}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
<span>{t("claudeConfig.hideAttribution")}</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toggleStates.alwaysThinking}
|
||||
onChange={(e) => handleToggle("alwaysThinking", e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
<span>{t("claudeConfig.alwaysThinking")}</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={toggleStates.teammates}
|
||||
onChange={(e) => handleToggle("teammates", e.target.checked)}
|
||||
className="w-4 h-4 text-blue-500 bg-white dark:bg-gray-800 border-border-default rounded focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-2"
|
||||
/>
|
||||
<span>{t("claudeConfig.enableTeammates")}</span>
|
||||
</label>
|
||||
</div>
|
||||
<JsonEditor
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
value={localValue}
|
||||
onChange={handleLocalChange}
|
||||
placeholder={`{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://your-api-endpoint.com",
|
||||
|
||||
@@ -38,7 +38,7 @@ export function DirectorySettings({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
{/* CC Switch 配置目录 - 独立区块 */}
|
||||
<section className="space-y-4">
|
||||
<header className="space-y-1">
|
||||
@@ -131,7 +131,7 @@ export function DirectorySettings({
|
||||
onReset={() => onResetDirectory("opencode")}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export function ImportExportSection({
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="space-y-4 rounded-xl glass-card p-6 border border-white/10">
|
||||
<div className="space-y-4 rounded-lg border border-border bg-muted/40 p-6">
|
||||
{/* Import and Export Buttons Side by Side */}
|
||||
<div className="grid grid-cols-2 gap-4 items-stretch">
|
||||
{/* Import Button */}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -162,6 +163,7 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
password: config?.password ?? "",
|
||||
remoteRoot: config?.remoteRoot ?? "cc-switch-sync",
|
||||
profile: config?.profile ?? "default",
|
||||
autoSync: config?.autoSync ?? false,
|
||||
}));
|
||||
|
||||
// Preset selector — derived from initial URL, updated on user selection
|
||||
@@ -196,6 +198,7 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
password: config.password ?? "",
|
||||
remoteRoot: config.remoteRoot ?? "cc-switch-sync",
|
||||
profile: config.profile ?? "default",
|
||||
autoSync: config.autoSync ?? false,
|
||||
});
|
||||
setPasswordTouched(false);
|
||||
setPresetId(detectPreset(config.baseUrl ?? ""));
|
||||
@@ -237,6 +240,16 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
}
|
||||
}, [form.baseUrl, presetId]);
|
||||
|
||||
const handleAutoSyncChange = useCallback((checked: boolean) => {
|
||||
setForm((prev) => ({ ...prev, autoSync: checked }));
|
||||
setDirty(true);
|
||||
setJustSaved(false);
|
||||
if (justSavedTimerRef.current) {
|
||||
clearTimeout(justSavedTimerRef.current);
|
||||
justSavedTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const buildSettings = useCallback((): WebDavSyncSettings | null => {
|
||||
const baseUrl = form.baseUrl.trim();
|
||||
if (!baseUrl) return null;
|
||||
@@ -247,6 +260,7 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
password: form.password,
|
||||
remoteRoot: form.remoteRoot.trim() || "cc-switch-sync",
|
||||
profile: form.profile.trim() || "default",
|
||||
autoSync: form.autoSync,
|
||||
};
|
||||
}, [form]);
|
||||
|
||||
@@ -433,6 +447,9 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
const lastSyncDisplay = lastSyncAt
|
||||
? new Date(lastSyncAt * 1000).toLocaleString()
|
||||
: null;
|
||||
const lastError = config?.status?.lastError?.trim();
|
||||
const showAutoSyncError =
|
||||
!!lastError && config?.status?.lastErrorSource === "auto";
|
||||
|
||||
// ─── Render ─────────────────────────────────────────────
|
||||
|
||||
@@ -559,6 +576,23 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<label className="w-40 text-xs font-medium text-foreground shrink-0">
|
||||
{t("settings.webdavSync.autoSync")}
|
||||
<span className="block text-[10px] font-normal text-muted-foreground">
|
||||
{t("settings.webdavSync.autoSyncHint")}
|
||||
</span>
|
||||
</label>
|
||||
<div className="pt-1">
|
||||
<Switch
|
||||
checked={form.autoSync}
|
||||
onCheckedChange={handleAutoSyncChange}
|
||||
aria-label={t("settings.webdavSync.autoSync")}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last sync time */}
|
||||
@@ -567,6 +601,17 @@ export function WebdavSyncSection({ config }: WebdavSyncSectionProps) {
|
||||
{t("settings.webdavSync.lastSync", { time: lastSyncDisplay })}
|
||||
</p>
|
||||
)}
|
||||
{showAutoSyncError && (
|
||||
<div className="rounded-lg border border-red-300/70 bg-red-50/80 px-3 py-2 text-xs text-red-900 dark:border-red-500/50 dark:bg-red-950/30 dark:text-red-200">
|
||||
<p className="font-medium">
|
||||
{t("settings.webdavSync.autoSyncLastErrorTitle")}
|
||||
</p>
|
||||
<p className="mt-1 break-all whitespace-pre-wrap">{lastError}</p>
|
||||
<p className="mt-1 text-[11px] text-red-700/90 dark:text-red-300/80">
|
||||
{t("settings.webdavSync.autoSyncLastErrorHint")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Config buttons + save status */}
|
||||
<div className="flex flex-wrap items-center gap-3 pt-2">
|
||||
|
||||
@@ -213,14 +213,12 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
|
||||
const query = searchQuery.toLowerCase();
|
||||
return byStatus.filter((skill) => {
|
||||
const name = skill.name?.toLowerCase() || "";
|
||||
const description = skill.description?.toLowerCase() || "";
|
||||
const directory = skill.directory?.toLowerCase() || "";
|
||||
const repo =
|
||||
skill.repoOwner && skill.repoName
|
||||
? `${skill.repoOwner}/${skill.repoName}`.toLowerCase()
|
||||
: "";
|
||||
|
||||
return (
|
||||
name.includes(query) ||
|
||||
description.includes(query) ||
|
||||
directory.includes(query)
|
||||
);
|
||||
return name.includes(query) || repo.includes(query);
|
||||
});
|
||||
}, [skills, searchQuery, filterRepo, filterStatus]);
|
||||
|
||||
|
||||
@@ -306,6 +306,7 @@ interface ImportSkillsDialogProps {
|
||||
name: string;
|
||||
description?: string;
|
||||
foundIn: string[];
|
||||
path: string;
|
||||
}>;
|
||||
onImport: (directories: string[]) => void;
|
||||
onClose: () => void;
|
||||
@@ -362,8 +363,11 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
|
||||
{skill.description}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground/70 mt-1">
|
||||
{t("skills.foundIn")}: {skill.foundIn.join(", ")}
|
||||
<div
|
||||
className="text-xs text-muted-foreground/50 mt-1 truncate"
|
||||
title={skill.path}
|
||||
>
|
||||
{skill.path}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user