feat: support kat-coder & template value (#77)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ release/
|
|||||||
.npmrc
|
.npmrc
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
/.claude
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import {
|
|||||||
updateTomlCommonConfigSnippet,
|
updateTomlCommonConfigSnippet,
|
||||||
hasTomlCommonConfigSnippet,
|
hasTomlCommonConfigSnippet,
|
||||||
validateJsonConfig,
|
validateJsonConfig,
|
||||||
|
applyTemplateValues,
|
||||||
} from "../utils/providerConfigUtils";
|
} from "../utils/providerConfigUtils";
|
||||||
import { providerPresets } from "../config/providerPresets";
|
import { providerPresets } from "../config/providerPresets";
|
||||||
|
import type { TemplateValueConfig } from "../config/providerPresets";
|
||||||
import {
|
import {
|
||||||
codexProviderPresets,
|
codexProviderPresets,
|
||||||
generateThirdPartyAuth,
|
generateThirdPartyAuth,
|
||||||
@@ -26,6 +28,136 @@ import { X, AlertCircle, Save } from "lucide-react";
|
|||||||
import { isLinux } from "../lib/platform";
|
import { isLinux } from "../lib/platform";
|
||||||
// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件
|
// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件
|
||||||
|
|
||||||
|
type TemplateValueMap = Record<string, TemplateValueConfig>;
|
||||||
|
|
||||||
|
type TemplatePath = Array<string | number>;
|
||||||
|
|
||||||
|
const collectTemplatePaths = (
|
||||||
|
source: unknown,
|
||||||
|
templateKeys: string[],
|
||||||
|
currentPath: TemplatePath = [],
|
||||||
|
acc: TemplatePath[] = [],
|
||||||
|
): TemplatePath[] => {
|
||||||
|
if (typeof source === "string") {
|
||||||
|
const hasPlaceholder = templateKeys.some((key) =>
|
||||||
|
source.includes(`\${${key}}`),
|
||||||
|
);
|
||||||
|
if (hasPlaceholder) {
|
||||||
|
acc.push([...currentPath]);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(source)) {
|
||||||
|
source.forEach((item, index) =>
|
||||||
|
collectTemplatePaths(item, templateKeys, [...currentPath, index], acc),
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source && typeof source === "object") {
|
||||||
|
Object.entries(source).forEach(([key, value]) =>
|
||||||
|
collectTemplatePaths(value, templateKeys, [...currentPath, key], acc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValueAtPath = (source: any, path: TemplatePath) => {
|
||||||
|
return path.reduce<any>((acc, key) => {
|
||||||
|
if (acc === undefined || acc === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return acc[key as keyof typeof acc];
|
||||||
|
}, source);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setValueAtPath = (
|
||||||
|
target: any,
|
||||||
|
path: TemplatePath,
|
||||||
|
value: unknown,
|
||||||
|
): any => {
|
||||||
|
if (path.length === 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = target;
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
const key = path[i];
|
||||||
|
const nextKey = path[i + 1];
|
||||||
|
const isNextIndex = typeof nextKey === "number";
|
||||||
|
|
||||||
|
if (current[key as keyof typeof current] === undefined) {
|
||||||
|
current[key as keyof typeof current] = isNextIndex ? [] : {};
|
||||||
|
} else {
|
||||||
|
const currentValue = current[key as keyof typeof current];
|
||||||
|
if (isNextIndex && !Array.isArray(currentValue)) {
|
||||||
|
current[key as keyof typeof current] = [];
|
||||||
|
} else if (
|
||||||
|
!isNextIndex &&
|
||||||
|
(typeof currentValue !== "object" || currentValue === null)
|
||||||
|
) {
|
||||||
|
current[key as keyof typeof current] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current[key as keyof typeof current];
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalKey = path[path.length - 1];
|
||||||
|
current[finalKey as keyof typeof current] = value;
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyTemplateValuesToConfigString = (
|
||||||
|
presetConfig: any,
|
||||||
|
currentConfigString: string,
|
||||||
|
values: TemplateValueMap,
|
||||||
|
) => {
|
||||||
|
const replacedConfig = applyTemplateValues(presetConfig, values);
|
||||||
|
const templateKeys = Object.keys(values);
|
||||||
|
if (templateKeys.length === 0) {
|
||||||
|
return JSON.stringify(replacedConfig, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderPaths = collectTemplatePaths(presetConfig, templateKeys);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedConfig = currentConfigString.trim()
|
||||||
|
? JSON.parse(currentConfigString)
|
||||||
|
: {};
|
||||||
|
let targetConfig: any;
|
||||||
|
if (Array.isArray(parsedConfig)) {
|
||||||
|
targetConfig = [...parsedConfig];
|
||||||
|
} else if (parsedConfig && typeof parsedConfig === "object") {
|
||||||
|
targetConfig = JSON.parse(JSON.stringify(parsedConfig));
|
||||||
|
} else {
|
||||||
|
targetConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeholderPaths.length === 0) {
|
||||||
|
return JSON.stringify(targetConfig, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mutatedConfig = targetConfig;
|
||||||
|
|
||||||
|
for (const path of placeholderPaths) {
|
||||||
|
const nextValue = getValueAtPath(replacedConfig, path);
|
||||||
|
if (path.length === 0) {
|
||||||
|
mutatedConfig = nextValue;
|
||||||
|
} else {
|
||||||
|
setValueAtPath(mutatedConfig, path, nextValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(mutatedConfig, null, 2);
|
||||||
|
} catch {
|
||||||
|
return JSON.stringify(replacedConfig, null, 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet";
|
const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet";
|
||||||
const CODEX_COMMON_CONFIG_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
const CODEX_COMMON_CONFIG_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
||||||
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
||||||
@@ -71,6 +203,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
const [claudeModel, setClaudeModel] = useState("");
|
const [claudeModel, setClaudeModel] = useState("");
|
||||||
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
|
const [claudeSmallFastModel, setClaudeSmallFastModel] = useState("");
|
||||||
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
|
const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态
|
||||||
|
// 模板变量状态
|
||||||
|
const [templateValues, setTemplateValues] =
|
||||||
|
useState<Record<string, TemplateValueConfig>>({});
|
||||||
|
|
||||||
// Codex 特有的状态
|
// Codex 特有的状态
|
||||||
const [codexAuth, setCodexAuthState] = useState("");
|
const [codexAuth, setCodexAuthState] = useState("");
|
||||||
@@ -157,6 +292,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
});
|
});
|
||||||
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
const [codexCommonConfigError, setCodexCommonConfigError] = useState("");
|
||||||
const isUpdatingFromCodexCommonConfig = useRef(false);
|
const isUpdatingFromCodexCommonConfig = useRef(false);
|
||||||
|
|
||||||
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
// -1 表示自定义,null 表示未选择,>= 0 表示预设索引
|
||||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(
|
||||||
showPresets ? -1 : null
|
showPresets ? -1 : null
|
||||||
@@ -377,6 +513,22 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setError(currentSettingsError);
|
setError(currentSettingsError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedTemplatePreset && templateValueEntries.length > 0) {
|
||||||
|
for (const [key, config] of templateValueEntries) {
|
||||||
|
const entry = templateValues[key];
|
||||||
|
const resolvedValue = (
|
||||||
|
entry?.editorValue ??
|
||||||
|
entry?.defaultValue ??
|
||||||
|
config.defaultValue ??
|
||||||
|
""
|
||||||
|
).trim();
|
||||||
|
if (!resolvedValue) {
|
||||||
|
setError(`请填写 ${config.label}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Claude: 原有逻辑
|
// Claude: 原有逻辑
|
||||||
if (!formData.settingsConfig.trim()) {
|
if (!formData.settingsConfig.trim()) {
|
||||||
setError("请填写配置内容");
|
setError("请填写配置内容");
|
||||||
@@ -529,7 +681,30 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
||||||
const configString = JSON.stringify(preset.settingsConfig, null, 2);
|
let appliedSettingsConfig = preset.settingsConfig;
|
||||||
|
let initialTemplateValues: TemplateValueMap = {};
|
||||||
|
|
||||||
|
if (preset.templateValues) {
|
||||||
|
initialTemplateValues = Object.fromEntries(
|
||||||
|
Object.entries(preset.templateValues).map(([key, config]) => [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
editorValue: config.editorValue
|
||||||
|
? config.editorValue
|
||||||
|
: config.defaultValue ?? "",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
appliedSettingsConfig = applyTemplateValues(
|
||||||
|
preset.settingsConfig,
|
||||||
|
initialTemplateValues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateValues(initialTemplateValues);
|
||||||
|
|
||||||
|
const configString = JSON.stringify(appliedSettingsConfig, null, 2);
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: preset.name,
|
name: preset.name,
|
||||||
@@ -554,8 +729,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setCommonConfigError("");
|
setCommonConfigError("");
|
||||||
|
|
||||||
// 如果预设包含模型配置,初始化模型输入框
|
// 如果预设包含模型配置,初始化模型输入框
|
||||||
if (preset.settingsConfig && typeof preset.settingsConfig === "object") {
|
if (appliedSettingsConfig && typeof appliedSettingsConfig === "object") {
|
||||||
const config = preset.settingsConfig as { env?: Record<string, any> };
|
const config = appliedSettingsConfig as { env?: Record<string, any> };
|
||||||
if (config.env) {
|
if (config.env) {
|
||||||
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
setClaudeModel(config.env.ANTHROPIC_MODEL || "");
|
||||||
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
|
setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || "");
|
||||||
@@ -577,6 +752,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
// 处理点击自定义按钮
|
// 处理点击自定义按钮
|
||||||
const handleCustomClick = () => {
|
const handleCustomClick = () => {
|
||||||
setSelectedPreset(-1);
|
setSelectedPreset(-1);
|
||||||
|
setTemplateValues({});
|
||||||
|
|
||||||
// 设置自定义模板
|
// 设置自定义模板
|
||||||
const customTemplate = {
|
const customTemplate = {
|
||||||
@@ -803,6 +979,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
selectedPreset !== null ||
|
selectedPreset !== null ||
|
||||||
(!showPresets && hasApiKeyField(formData.settingsConfig));
|
(!showPresets && hasApiKeyField(formData.settingsConfig));
|
||||||
|
|
||||||
|
const selectedTemplatePreset =
|
||||||
|
!isCodex &&
|
||||||
|
selectedPreset !== null &&
|
||||||
|
selectedPreset >= 0 &&
|
||||||
|
selectedPreset < providerPresets.length
|
||||||
|
? providerPresets[selectedPreset]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const templateValueEntries: Array<[string, TemplateValueConfig]> =
|
||||||
|
selectedTemplatePreset?.templateValues
|
||||||
|
? (Object.entries(
|
||||||
|
selectedTemplatePreset.templateValues
|
||||||
|
) as Array<[string, TemplateValueConfig]>)
|
||||||
|
: [];
|
||||||
|
|
||||||
// 判断当前选中的预设是否是官方
|
// 判断当前选中的预设是否是官方
|
||||||
const isOfficialPreset =
|
const isOfficialPreset =
|
||||||
(selectedPreset !== null &&
|
(selectedPreset !== null &&
|
||||||
@@ -1133,6 +1324,74 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isCodex && selectedTemplatePreset && templateValueEntries.length > 0 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
参数配置 - {selectedTemplatePreset.name.trim()} *
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{templateValueEntries.map(([key, config]) => (
|
||||||
|
<div key={key} className="space-y-2">
|
||||||
|
<label className="sr-only" htmlFor={`template-${key}`}>
|
||||||
|
{config.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={`template-${key}`}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder={`${config.label} *`}
|
||||||
|
value={
|
||||||
|
templateValues[key]?.editorValue ??
|
||||||
|
config.editorValue ??
|
||||||
|
config.defaultValue ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setTemplateValues((prev) => {
|
||||||
|
const prevEntry = prev[key];
|
||||||
|
const nextEntry: TemplateValueConfig = {
|
||||||
|
...config,
|
||||||
|
...(prevEntry ?? {}),
|
||||||
|
editorValue: newValue,
|
||||||
|
};
|
||||||
|
const nextValues: TemplateValueMap = {
|
||||||
|
...prev,
|
||||||
|
[key]: nextEntry,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedTemplatePreset) {
|
||||||
|
try {
|
||||||
|
const configString = applyTemplateValuesToConfigString(
|
||||||
|
selectedTemplatePreset.settingsConfig,
|
||||||
|
formData.settingsConfig,
|
||||||
|
nextValues
|
||||||
|
);
|
||||||
|
setFormData((prevForm) => ({
|
||||||
|
...prevForm,
|
||||||
|
settingsConfig: configString,
|
||||||
|
}));
|
||||||
|
setSettingsConfigError(
|
||||||
|
validateSettingsConfig(configString)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("更新模板值失败:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextValues;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
aria-label={config.label}
|
||||||
|
autoComplete="off"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
{/* 基础 URL 输入框 - 仅在自定义模式下显示 */}
|
||||||
{!isCodex && showBaseUrlInput && (
|
{!isCodex && showBaseUrlInput && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
*/
|
*/
|
||||||
import { ProviderCategory } from "../types";
|
import { ProviderCategory } from "../types";
|
||||||
|
|
||||||
|
export interface TemplateValueConfig {
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
editorValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProviderPreset {
|
export interface ProviderPreset {
|
||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
@@ -11,6 +18,8 @@ export interface ProviderPreset {
|
|||||||
settingsConfig: object;
|
settingsConfig: object;
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
category?: ProviderCategory; // 新增:分类
|
category?: ProviderCategory; // 新增:分类
|
||||||
|
// 新增:模板变量定义,用于动态替换配置中的值
|
||||||
|
templateValues?: Record<string, TemplateValueConfig>; // editorValue 存储编辑器中的实时输入值
|
||||||
}
|
}
|
||||||
|
|
||||||
export const providerPresets: ProviderPreset[] = [
|
export const providerPresets: ProviderPreset[] = [
|
||||||
@@ -101,4 +110,26 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
},
|
},
|
||||||
category: "third_party",
|
category: "third_party",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "KAT-Coder 官方",
|
||||||
|
websiteUrl: "https://console.streamlake.ai/wanqing/",
|
||||||
|
apiKeyUrl: "https://console.streamlake.ai/console/wanqing/api-key",
|
||||||
|
settingsConfig: {
|
||||||
|
env: {
|
||||||
|
ANTHROPIC_BASE_URL: "https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy",
|
||||||
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
|
ANTHROPIC_MODEL: "KAT-Coder",
|
||||||
|
ANTHROPIC_SMALL_FAST_MODEL: "KAT-Coder",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: "cn_official",
|
||||||
|
templateValues: {
|
||||||
|
ENDPOINT_ID: {
|
||||||
|
label: "Vanchin Endpoint ID",
|
||||||
|
placeholder: "ep-xxx-xxx",
|
||||||
|
defaultValue: "",
|
||||||
|
editorValue: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// 供应商配置处理工具函数
|
// 供应商配置处理工具函数
|
||||||
|
|
||||||
|
import type { TemplateValueConfig } from "../config/providerPresets";
|
||||||
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
const isPlainObject = (value: unknown): value is Record<string, any> => {
|
||||||
return Object.prototype.toString.call(value) === "[object Object]";
|
return Object.prototype.toString.call(value) === "[object Object]";
|
||||||
};
|
};
|
||||||
@@ -173,6 +175,51 @@ export const getApiKeyFromConfig = (jsonString: string): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 模板变量替换
|
||||||
|
export const applyTemplateValues = (
|
||||||
|
config: any,
|
||||||
|
templateValues: Record<string, TemplateValueConfig> | undefined
|
||||||
|
): any => {
|
||||||
|
const resolvedValues = Object.fromEntries(
|
||||||
|
Object.entries(templateValues ?? {}).map(([key, value]) => {
|
||||||
|
const resolvedValue =
|
||||||
|
value.editorValue !== undefined
|
||||||
|
? value.editorValue
|
||||||
|
: value.defaultValue ?? "";
|
||||||
|
return [key, resolvedValue];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const replaceInString = (str: string): string => {
|
||||||
|
return Object.entries(resolvedValues).reduce((acc, [key, value]) => {
|
||||||
|
const placeholder = `\${${key}}`;
|
||||||
|
if (!acc.includes(placeholder)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return acc.split(placeholder).join(value ?? "");
|
||||||
|
}, str);
|
||||||
|
};
|
||||||
|
|
||||||
|
const traverse = (obj: any): any => {
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
return replaceInString(obj);
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(traverse);
|
||||||
|
}
|
||||||
|
if (obj && typeof obj === "object") {
|
||||||
|
const result: any = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
result[key] = traverse(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
return traverse(config);
|
||||||
|
};
|
||||||
|
|
||||||
// 判断配置中是否存在 API Key 字段
|
// 判断配置中是否存在 API Key 字段
|
||||||
export const hasApiKeyField = (jsonString: string): boolean => {
|
export const hasApiKeyField = (jsonString: string): boolean => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user