Compare commits

..

5 Commits

Author SHA1 Message Date
YoVinchen 0fbe55f5c3 fix(deeplink): use primary endpoint for usage script base_url
- Fix usage_script.base_url getting comma-separated string when multiple endpoints
- Add i18n support for primary endpoint label in DeepLinkImportDialog
2026-01-12 14:58:52 +08:00
YoVinchen dea9718174 Merge branch 'main' into feat/deeplink-multi-endpoints 2026-01-12 09:30:43 +08:00
YoVinchen dacb751ea8 fix(deeplink): auto-infer homepage and improve multi-endpoint display
- Auto-infer homepage from primary endpoint when not provided
- Display multiple endpoints as list in import dialog (primary marked)
- Update deeplink parser in deplink.html to show multi-endpoint info
- Add test for homepage inference from endpoint
- Minor log format fix in live.rs
2026-01-12 02:38:43 +08:00
YoVinchen 37016cc499 feat(deeplink): add usage query fields to deeplink generator
Add form fields for usage query configuration in deeplink HTML generator:
- usageEnabled, usageBaseUrl, usageApiKey
- usageScript, usageAutoInterval
- usageAccessToken, usageUserId
2026-01-12 02:13:29 +08:00
YoVinchen 98a3bc938f feat(deeplink): support comma-separated multiple endpoints in URL
Allow importing multiple API endpoints via single endpoint parameter.
First URL becomes primary endpoint, rest are added as custom endpoints.
2026-01-12 02:12:32 +08:00
8 changed files with 8 additions and 77 deletions
-3
View File
@@ -147,9 +147,6 @@ pub struct ProviderMeta {
/// 用量查询脚本配置 /// 用量查询脚本配置
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub usage_script: Option<UsageScript>, pub usage_script: Option<UsageScript>,
/// 请求地址管理:测速后自动选择最佳端点
#[serde(rename = "endpointAutoSelect", skip_serializing_if = "Option::is_none")]
pub endpoint_auto_select: Option<bool>,
/// 合作伙伴标记(前端使用 isPartner,保持字段名一致) /// 合作伙伴标记(前端使用 isPartner,保持字段名一致)
#[serde(rename = "isPartner", skip_serializing_if = "Option::is_none")] #[serde(rename = "isPartner", skip_serializing_if = "Option::is_none")]
pub is_partner: Option<bool>, pub is_partner: Option<bool>,
+6 -30
View File
@@ -1,4 +1,4 @@
import { useMemo, useState, useEffect, useRef } from "react"; import { useMemo, useState, useEffect } from "react";
import { GripVertical, ChevronDown, ChevronUp } from "lucide-react"; import { GripVertical, ChevronDown, ChevronUp } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { import type {
@@ -149,10 +149,6 @@ export function ProviderCard({
// 多套餐默认展开 // 多套餐默认展开
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
// 操作按钮容器 ref,用于动态计算宽度
const actionsRef = useRef<HTMLDivElement>(null);
const [actionsWidth, setActionsWidth] = useState(0);
// 当检测到多套餐时自动展开 // 当检测到多套餐时自动展开
useEffect(() => { useEffect(() => {
if (hasMultiplePlans) { if (hasMultiplePlans) {
@@ -160,20 +156,6 @@ export function ProviderCard({
} }
}, [hasMultiplePlans]); }, [hasMultiplePlans]);
// 动态获取操作按钮宽度
useEffect(() => {
if (actionsRef.current) {
const updateWidth = () => {
const width = actionsRef.current?.offsetWidth || 0;
setActionsWidth(width);
};
updateWidth();
// 监听窗口大小变化
window.addEventListener("resize", updateWidth);
return () => window.removeEventListener("resize", updateWidth);
}
}, [onTest, onOpenTerminal]); // 按钮数量可能变化时重新计算
const handleOpenWebsite = () => { const handleOpenWebsite = () => {
if (!isClickableUrl) { if (!isClickableUrl) {
return; return;
@@ -299,13 +281,10 @@ export function ProviderCard({
</div> </div>
</div> </div>
<div <div className="relative flex items-center ml-auto min-w-0">
className="relative flex items-center ml-auto min-w-0 gap-3"
style={{ "--actions-width": `${actionsWidth || 320}px` } as React.CSSProperties}
>
{/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */} {/* 用量信息区域 - hover 时向左移动,为操作按钮腾出空间 */}
<div className="ml-auto"> <div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[14.5rem] group-focus-within:-translate-x-[14.5rem] sm:group-hover:-translate-x-[16rem] sm:group-focus-within:-translate-x-[16rem]">
<div className="flex items-center gap-1 transition-transform duration-200 group-hover:-translate-x-[var(--actions-width)] group-focus-within:-translate-x-[var(--actions-width)]"> <div className="flex items-center gap-1">
{/* 多套餐时显示套餐数量,单套餐时显示详细信息 */} {/* 多套餐时显示套餐数量,单套餐时显示详细信息 */}
{hasMultiplePlans ? ( {hasMultiplePlans ? (
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400"> <div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
@@ -350,11 +329,8 @@ export function ProviderCard({
</div> </div>
</div> </div>
{/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入,与用量信息保持间距 */} {/* 操作按钮区域 - 绝对定位在右侧,hover 时滑入 */}
<div <div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0">
ref={actionsRef}
className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1.5 pl-3 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-2 group-hover:translate-x-0 group-focus-within:translate-x-0"
>
<ProviderActions <ProviderActions
isCurrent={isCurrent} isCurrent={isCurrent}
isTesting={isTesting} isTesting={isTesting}
@@ -36,8 +36,6 @@ interface ClaudeFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange?: (endpoints: string[]) => void; onCustomEndpointsChange?: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model Selector // Model Selector
shouldShowModelSelector: boolean; shouldShowModelSelector: boolean;
@@ -85,8 +83,6 @@ export function ClaudeFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelSelector, shouldShowModelSelector,
claudeModel, claudeModel,
reasoningModel, reasoningModel,
@@ -174,8 +170,6 @@ export function ClaudeFormFields({
initialEndpoints={speedTestEndpoints} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}
/> />
)} )}
@@ -25,8 +25,6 @@ interface CodexFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange?: (endpoints: string[]) => void; onCustomEndpointsChange?: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model Name // Model Name
shouldShowModelField?: boolean; shouldShowModelField?: boolean;
@@ -52,8 +50,6 @@ export function CodexFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelField = true, shouldShowModelField = true,
modelName = "", modelName = "",
onModelNameChange, onModelNameChange,
@@ -134,8 +130,6 @@ export function CodexFormFields({
initialEndpoints={speedTestEndpoints} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}
/> />
)} )}
@@ -30,8 +30,6 @@ interface EndpointSpeedTestProps {
initialEndpoints: EndpointCandidate[]; initialEndpoints: EndpointCandidate[];
visible?: boolean; visible?: boolean;
onClose: () => void; onClose: () => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// 新建模式:当自定义端点列表变化时回传(仅包含 isCustom 的条目) // 新建模式:当自定义端点列表变化时回传(仅包含 isCustom 的条目)
// 编辑模式:不使用此回调,端点直接保存到后端 // 编辑模式:不使用此回调,端点直接保存到后端
onCustomEndpointsChange?: (urls: string[]) => void; onCustomEndpointsChange?: (urls: string[]) => void;
@@ -87,8 +85,6 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
initialEndpoints, initialEndpoints,
visible = true, visible = true,
onClose, onClose,
autoSelect,
onAutoSelectChange,
onCustomEndpointsChange, onCustomEndpointsChange,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -97,6 +93,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
); );
const [customUrl, setCustomUrl] = useState(""); const [customUrl, setCustomUrl] = useState("");
const [addError, setAddError] = useState<string | null>(null); const [addError, setAddError] = useState<string | null>(null);
const [autoSelect, setAutoSelect] = useState(true);
const [isTesting, setIsTesting] = useState(false); const [isTesting, setIsTesting] = useState(false);
const [lastError, setLastError] = useState<string | null>(null); const [lastError, setLastError] = useState<string | null>(null);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@@ -491,9 +488,7 @@ const EndpointSpeedTest: React.FC<EndpointSpeedTestProps> = ({
<input <input
type="checkbox" type="checkbox"
checked={autoSelect} checked={autoSelect}
onChange={(event) => { onChange={(event) => setAutoSelect(event.target.checked)}
onAutoSelectChange(event.target.checked);
}}
className="h-3.5 w-3.5 rounded border-border-default bg-background text-primary focus:ring-2 focus:ring-primary/20" className="h-3.5 w-3.5 rounded border-border-default bg-background text-primary focus:ring-2 focus:ring-primary/20"
/> />
{t("endpointTest.autoSelect")} {t("endpointTest.autoSelect")}
@@ -29,8 +29,6 @@ interface GeminiFormFieldsProps {
isEndpointModalOpen: boolean; isEndpointModalOpen: boolean;
onEndpointModalToggle: (open: boolean) => void; onEndpointModalToggle: (open: boolean) => void;
onCustomEndpointsChange: (endpoints: string[]) => void; onCustomEndpointsChange: (endpoints: string[]) => void;
autoSelect: boolean;
onAutoSelectChange: (checked: boolean) => void;
// Model // Model
shouldShowModelField: boolean; shouldShowModelField: boolean;
@@ -57,8 +55,6 @@ export function GeminiFormFields({
isEndpointModalOpen, isEndpointModalOpen,
onEndpointModalToggle, onEndpointModalToggle,
onCustomEndpointsChange, onCustomEndpointsChange,
autoSelect,
onAutoSelectChange,
shouldShowModelField, shouldShowModelField,
model, model,
onModelChange, onModelChange,
@@ -146,8 +142,6 @@ export function GeminiFormFields({
initialEndpoints={speedTestEndpoints} initialEndpoints={speedTestEndpoints}
visible={isEndpointModalOpen} visible={isEndpointModalOpen}
onClose={() => onEndpointModalToggle(false)} onClose={() => onEndpointModalToggle(false)}
autoSelect={autoSelect}
onAutoSelectChange={onAutoSelectChange}
onCustomEndpointsChange={onCustomEndpointsChange} onCustomEndpointsChange={onCustomEndpointsChange}
/> />
)} )}
@@ -124,9 +124,6 @@ export function ProviderForm({
return []; return [];
}, },
); );
const [endpointAutoSelect, setEndpointAutoSelect] = useState<boolean>(
() => initialData?.meta?.endpointAutoSelect ?? true,
);
// 使用 category hook // 使用 category hook
const { category } = useProviderCategory({ const { category } = useProviderCategory({
@@ -144,7 +141,6 @@ export function ProviderForm({
if (!initialData) { if (!initialData) {
setDraftCustomEndpoints([]); setDraftCustomEndpoints([]);
} }
setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true);
}, [appId, initialData]); }, [appId, initialData]);
const defaultValues: ProviderFormData = useMemo( const defaultValues: ProviderFormData = useMemo(
@@ -651,13 +647,6 @@ export function ProviderForm({
} }
} }
const baseMeta: ProviderMeta | undefined =
payload.meta ?? (initialData?.meta ? { ...initialData.meta } : undefined);
payload.meta = {
...(baseMeta ?? {}),
endpointAutoSelect,
};
onSubmit(payload); onSubmit(payload);
}; };
@@ -867,8 +856,6 @@ export function ProviderForm({
onCustomEndpointsChange={ onCustomEndpointsChange={
isEditMode ? undefined : setDraftCustomEndpoints isEditMode ? undefined : setDraftCustomEndpoints
} }
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
shouldShowModelSelector={category !== "official"} shouldShowModelSelector={category !== "official"}
claudeModel={claudeModel} claudeModel={claudeModel}
reasoningModel={reasoningModel} reasoningModel={reasoningModel}
@@ -902,8 +889,6 @@ export function ProviderForm({
onCustomEndpointsChange={ onCustomEndpointsChange={
isEditMode ? undefined : setDraftCustomEndpoints isEditMode ? undefined : setDraftCustomEndpoints
} }
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
shouldShowModelField={category !== "official"} shouldShowModelField={category !== "official"}
modelName={codexModelName} modelName={codexModelName}
onModelNameChange={handleCodexModelNameChange} onModelNameChange={handleCodexModelNameChange}
@@ -932,8 +917,6 @@ export function ProviderForm({
isEndpointModalOpen={isEndpointModalOpen} isEndpointModalOpen={isEndpointModalOpen}
onEndpointModalToggle={setIsEndpointModalOpen} onEndpointModalToggle={setIsEndpointModalOpen}
onCustomEndpointsChange={setDraftCustomEndpoints} onCustomEndpointsChange={setDraftCustomEndpoints}
autoSelect={endpointAutoSelect}
onAutoSelectChange={setEndpointAutoSelect}
shouldShowModelField={true} shouldShowModelField={true}
model={geminiModel} model={geminiModel}
onModelChange={handleGeminiModelChange} onModelChange={handleGeminiModelChange}
-2
View File
@@ -92,8 +92,6 @@ export interface ProviderMeta {
custom_endpoints?: Record<string, CustomEndpoint>; custom_endpoints?: Record<string, CustomEndpoint>;
// 用量查询脚本配置 // 用量查询脚本配置
usage_script?: UsageScript; usage_script?: UsageScript;
// 请求地址管理:测速后自动选择最佳端点
endpointAutoSelect?: boolean;
// 是否为官方合作伙伴 // 是否为官方合作伙伴
isPartner?: boolean; isPartner?: boolean;
// 合作伙伴促销 key(用于后端识别 PackyCode 等) // 合作伙伴促销 key(用于后端识别 PackyCode 等)