Explorar el Código

feat: add support for iFLYTEK Spark API (接入讯飞星火模型)

webws hace 1 año
padre
commit
b2c1644d69

+ 12 - 0
README.md

@@ -280,6 +280,18 @@ Alibaba Cloud Api Key.
 
 Alibaba Cloud Api Url.
 
+### `IFLYTEK_URL` (Optional)
+
+iflytek Api Url.
+
+### `IFLYTEK_API_KEY` (Optional)
+
+iflytek Api Key.
+
+### `IFLYTEK_API_SECRET` (Optional)
+
+iflytek Api Secret.
+
 ### `HIDE_USER_API_KEY` (optional)
 
 > Default: Empty

+ 14 - 0
README_CN.md

@@ -172,6 +172,20 @@ ByteDance Api Url.
 
 阿里云(千问)Api Url.
 
+### `IFLYTEK_URL` (可选)
+
+讯飞星火Api Url.
+
+### `IFLYTEK_API_KEY` (可选)
+
+讯飞星火Api Key.
+
+### `IFLYTEK_API_SECRET` (可选)
+
+讯飞星火Api Secret.
+
+
+
 ### `HIDE_USER_API_KEY` (可选)
 
 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。

+ 3 - 1
app/api/[provider]/[...path]/route.ts

@@ -9,7 +9,7 @@ import { handle as bytedanceHandler } from "../../bytedance";
 import { handle as alibabaHandler } from "../../alibaba";
 import { handle as moonshotHandler } from "../../moonshot";
 import { handle as stabilityHandler } from "../../stability";
-
+import { handle as iflytekHandler } from "../../iflytek";
 async function handle(
   req: NextRequest,
   { params }: { params: { provider: string; path: string[] } },
@@ -34,6 +34,8 @@ async function handle(
       return moonshotHandler(req, { params });
     case ApiPath.Stability:
       return stabilityHandler(req, { params });
+    case ApiPath.Iflytek:
+      return iflytekHandler(req, { params });
     default:
       return openaiHandler(req, { params });
   }

+ 4 - 0
app/api/auth.ts

@@ -88,6 +88,10 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
       case ModelProvider.Moonshot:
         systemApiKey = serverConfig.moonshotApiKey;
         break;
+      case ModelProvider.Iflytek:
+        systemApiKey =
+          serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
+        break;
       case ModelProvider.GPT:
       default:
         if (req.nextUrl.pathname.includes("azure/deployments")) {

+ 131 - 0
app/api/iflytek.ts

@@ -0,0 +1,131 @@
+import { getServerSideConfig } from "@/app/config/server";
+import {
+  Iflytek,
+  IFLYTEK_BASE_URL,
+  ApiPath,
+  ModelProvider,
+  ServiceProvider,
+} from "@/app/constant";
+import { prettyObject } from "@/app/utils/format";
+import { NextRequest, NextResponse } from "next/server";
+import { auth } from "@/app/api/auth";
+import { isModelAvailableInServer } from "@/app/utils/model";
+import type { RequestPayload } from "@/app/client/platforms/openai";
+// iflytek
+
+const serverConfig = getServerSideConfig();
+
+export async function handle(
+  req: NextRequest,
+  { params }: { params: { path: string[] } },
+) {
+  console.log("[Iflytek Route] params ", params);
+
+  if (req.method === "OPTIONS") {
+    return NextResponse.json({ body: "OK" }, { status: 200 });
+  }
+
+  const authResult = auth(req, ModelProvider.Iflytek);
+  if (authResult.error) {
+    return NextResponse.json(authResult, {
+      status: 401,
+    });
+  }
+
+  try {
+    const response = await request(req);
+    return response;
+  } catch (e) {
+    console.error("[Iflytek] ", e);
+    return NextResponse.json(prettyObject(e));
+  }
+}
+
+async function request(req: NextRequest) {
+  const controller = new AbortController();
+
+  // iflytek use base url or just remove the path
+  let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Iflytek, "");
+
+  let baseUrl = serverConfig.iflytekUrl || IFLYTEK_BASE_URL;
+
+  if (!baseUrl.startsWith("http")) {
+    baseUrl = `https://${baseUrl}`;
+  }
+
+  if (baseUrl.endsWith("/")) {
+    baseUrl = baseUrl.slice(0, -1);
+  }
+
+  console.log("[Proxy] ", path);
+  console.log("[Base Url]", baseUrl);
+
+  const timeoutId = setTimeout(
+    () => {
+      controller.abort();
+    },
+    10 * 60 * 1000,
+  );
+
+  const fetchUrl = `${baseUrl}${path}`;
+  const fetchOptions: RequestInit = {
+    headers: {
+      "Content-Type": "application/json",
+      Authorization: req.headers.get("Authorization") ?? "",
+    },
+    method: req.method,
+    body: req.body,
+    redirect: "manual",
+    // @ts-ignore
+    duplex: "half",
+    signal: controller.signal,
+  };
+
+  // try to refuse some request to some models
+  if (serverConfig.customModels && req.body) {
+    try {
+      const clonedBody = await req.text();
+      fetchOptions.body = clonedBody;
+
+      const jsonBody = JSON.parse(clonedBody) as { model?: string };
+
+      // not undefined and is false
+      if (
+        isModelAvailableInServer(
+          serverConfig.customModels,
+          jsonBody?.model as string,
+          ServiceProvider.Iflytek as string,
+        )
+      ) {
+        return NextResponse.json(
+          {
+            error: true,
+            message: `you are not allowed to use ${jsonBody?.model} model`,
+          },
+          {
+            status: 403,
+          },
+        );
+      }
+    } catch (e) {
+      console.error(`[Iflytek] filter`, e);
+    }
+  }
+  try {
+    const res = await fetch(fetchUrl, fetchOptions);
+
+    // to prevent browser prompt for credentials
+    const newHeaders = new Headers(res.headers);
+    newHeaders.delete("www-authenticate");
+    // to disable nginx buffering
+    newHeaders.set("X-Accel-Buffering", "no");
+
+    return new Response(res.body, {
+      status: res.status,
+      statusText: res.statusText,
+      headers: newHeaders,
+    });
+  } finally {
+    clearTimeout(timeoutId);
+  }
+}

+ 12 - 0
app/client/api.ts

@@ -14,6 +14,7 @@ import { DoubaoApi } from "./platforms/bytedance";
 import { QwenApi } from "./platforms/alibaba";
 import { HunyuanApi } from "./platforms/tencent";
 import { MoonshotApi } from "./platforms/moonshot";
+import { SparkApi } from "./platforms/iflytek";
 
 export const ROLES = ["system", "user", "assistant"] as const;
 export type MessageRole = (typeof ROLES)[number];
@@ -128,6 +129,9 @@ export class ClientApi {
       case ModelProvider.Moonshot:
         this.llm = new MoonshotApi();
         break;
+      case ModelProvider.Iflytek:
+        this.llm = new SparkApi();
+        break;
       default:
         this.llm = new ChatGPTApi();
     }
@@ -211,6 +215,7 @@ export function getHeaders() {
     const isByteDance = modelConfig.providerName === ServiceProvider.ByteDance;
     const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
     const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
+    const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
     const isEnabledAccessControl = accessStore.enabledAccessControl();
     const apiKey = isGoogle
       ? accessStore.googleApiKey
@@ -224,6 +229,10 @@ export function getHeaders() {
       ? accessStore.alibabaApiKey
       : isMoonshot
       ? accessStore.moonshotApiKey
+      : isIflytek
+      ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
+        ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
+        : ""
       : accessStore.openaiApiKey;
     return {
       isGoogle,
@@ -233,6 +242,7 @@ export function getHeaders() {
       isByteDance,
       isAlibaba,
       isMoonshot,
+      isIflytek,
       apiKey,
       isEnabledAccessControl,
     };
@@ -286,6 +296,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
       return new ClientApi(ModelProvider.Hunyuan);
     case ServiceProvider.Moonshot:
       return new ClientApi(ModelProvider.Moonshot);
+    case ServiceProvider.Iflytek:
+      return new ClientApi(ModelProvider.Iflytek);
     default:
       return new ClientApi(ModelProvider.GPT);
   }

+ 240 - 0
app/client/platforms/iflytek.ts

@@ -0,0 +1,240 @@
+"use client";
+import {
+  ApiPath,
+  DEFAULT_API_HOST,
+  Iflytek,
+  REQUEST_TIMEOUT_MS,
+} from "@/app/constant";
+import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
+
+import { ChatOptions, getHeaders, LLMApi, LLMModel } from "../api";
+import Locale from "../../locales";
+import {
+  EventStreamContentType,
+  fetchEventSource,
+} from "@fortaine/fetch-event-source";
+import { prettyObject } from "@/app/utils/format";
+import { getClientConfig } from "@/app/config/client";
+import { getMessageTextContent } from "@/app/utils";
+
+import { OpenAIListModelResponse, RequestPayload } from "./openai";
+
+export class SparkApi implements LLMApi {
+  private disableListModels = true;
+
+  path(path: string): string {
+    const accessStore = useAccessStore.getState();
+
+    let baseUrl = "";
+
+    if (accessStore.useCustomConfig) {
+      baseUrl = accessStore.iflytekUrl;
+    }
+
+    if (baseUrl.length === 0) {
+      const isApp = !!getClientConfig()?.isApp;
+      const apiPath = ApiPath.Iflytek;
+      baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
+    }
+
+    if (baseUrl.endsWith("/")) {
+      baseUrl = baseUrl.slice(0, baseUrl.length - 1);
+    }
+    if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Iflytek)) {
+      baseUrl = "https://" + baseUrl;
+    }
+
+    console.log("[Proxy Endpoint] ", baseUrl, path);
+
+    return [baseUrl, path].join("/");
+  }
+
+  extractMessage(res: any) {
+    return res.choices?.at(0)?.message?.content ?? "";
+  }
+
+  async chat(options: ChatOptions) {
+    const messages: ChatOptions["messages"] = [];
+    for (const v of options.messages) {
+      const content = getMessageTextContent(v);
+      messages.push({ role: v.role, content });
+    }
+
+    const modelConfig = {
+      ...useAppConfig.getState().modelConfig,
+      ...useChatStore.getState().currentSession().mask.modelConfig,
+      ...{
+        model: options.config.model,
+        providerName: options.config.providerName,
+      },
+    };
+
+    const requestPayload: RequestPayload = {
+      messages,
+      stream: options.config.stream,
+      model: modelConfig.model,
+      temperature: modelConfig.temperature,
+      presence_penalty: modelConfig.presence_penalty,
+      frequency_penalty: modelConfig.frequency_penalty,
+      top_p: modelConfig.top_p,
+      // max_tokens: Math.max(modelConfig.max_tokens, 1024),
+      // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
+    };
+
+    console.log("[Request] Spark payload: ", requestPayload);
+
+    const shouldStream = !!options.config.stream;
+    const controller = new AbortController();
+    options.onController?.(controller);
+
+    try {
+      const chatPath = this.path(Iflytek.ChatPath);
+      const chatPayload = {
+        method: "POST",
+        body: JSON.stringify(requestPayload),
+        signal: controller.signal,
+        headers: getHeaders(),
+      };
+
+      // Make a fetch request
+      const requestTimeoutId = setTimeout(
+        () => controller.abort(),
+        REQUEST_TIMEOUT_MS,
+      );
+
+      if (shouldStream) {
+        let responseText = "";
+        let remainText = "";
+        let finished = false;
+
+        // Animate response text to make it look smooth
+        function animateResponseText() {
+          if (finished || controller.signal.aborted) {
+            responseText += remainText;
+            console.log("[Response Animation] finished");
+            return;
+          }
+
+          if (remainText.length > 0) {
+            const fetchCount = Math.max(1, Math.round(remainText.length / 60));
+            const fetchText = remainText.slice(0, fetchCount);
+            responseText += fetchText;
+            remainText = remainText.slice(fetchCount);
+            options.onUpdate?.(responseText, fetchText);
+          }
+
+          requestAnimationFrame(animateResponseText);
+        }
+
+        // Start animation
+        animateResponseText();
+
+        const finish = () => {
+          if (!finished) {
+            finished = true;
+            options.onFinish(responseText + remainText);
+          }
+        };
+
+        controller.signal.onabort = finish;
+
+        fetchEventSource(chatPath, {
+          ...chatPayload,
+          async onopen(res) {
+            clearTimeout(requestTimeoutId);
+            const contentType = res.headers.get("content-type");
+            console.log("[Spark] request response content type: ", contentType);
+
+            if (contentType?.startsWith("text/plain")) {
+              responseText = await res.clone().text();
+              return finish();
+            }
+
+            // Handle different error scenarios
+            if (
+              !res.ok ||
+              !res.headers
+                .get("content-type")
+                ?.startsWith(EventStreamContentType) ||
+              res.status !== 200
+            ) {
+              let extraInfo = await res.clone().text();
+              try {
+                const resJson = await res.clone().json();
+                extraInfo = prettyObject(resJson);
+              } catch {}
+
+              if (res.status === 401) {
+                extraInfo = Locale.Error.Unauthorized;
+              }
+
+              options.onError?.(
+                new Error(
+                  `Request failed with status ${res.status}: ${extraInfo}`,
+                ),
+              );
+              return finish();
+            }
+          },
+          onmessage(msg) {
+            if (msg.data === "[DONE]" || finished) {
+              return finish();
+            }
+            const text = msg.data;
+            try {
+              const json = JSON.parse(text);
+              const choices = json.choices as Array<{
+                delta: { content: string };
+              }>;
+              const delta = choices[0]?.delta?.content;
+
+              if (delta) {
+                remainText += delta;
+              }
+            } catch (e) {
+              console.error("[Request] parse error", text);
+              options.onError?.(new Error(`Failed to parse response: ${text}`));
+            }
+          },
+          onclose() {
+            finish();
+          },
+          onerror(e) {
+            options.onError?.(e);
+            throw e;
+          },
+          openWhenHidden: true,
+        });
+      } else {
+        const res = await fetch(chatPath, chatPayload);
+        clearTimeout(requestTimeoutId);
+
+        if (!res.ok) {
+          const errorText = await res.text();
+          options.onError?.(
+            new Error(`Request failed with status ${res.status}: ${errorText}`),
+          );
+          return;
+        }
+
+        const resJson = await res.json();
+        const message = this.extractMessage(resJson);
+        options.onFinish(message);
+      }
+    } catch (e) {
+      console.log("[Request] failed to make a chat request", e);
+      options.onError?.(e as Error);
+    }
+  }
+
+  async usage() {
+    return {
+      used: 0,
+      total: 0,
+    };
+  }
+
+  async models(): Promise<LLMModel[]> {
+    return [];
+  }
+}

+ 56 - 0
app/components/settings.tsx

@@ -68,6 +68,7 @@ import {
   SlotID,
   UPDATE_URL,
   Stability,
+  Iflytek,
 } from "../constant";
 import { Prompt, SearchService, usePromptStore } from "../store/prompt";
 import { ErrorBoundary } from "./error";
@@ -1172,6 +1173,60 @@ export function Settings() {
       </ListItem>
     </>
   );
+  const lflytekConfigComponent = accessStore.provider ===
+    ServiceProvider.Iflytek && (
+    <>
+      <ListItem
+        title={Locale.Settings.Access.Iflytek.Endpoint.Title}
+        subTitle={
+          Locale.Settings.Access.Iflytek.Endpoint.SubTitle +
+          Iflytek.ExampleEndpoint
+        }
+      >
+        <input
+          type="text"
+          value={accessStore.iflytekUrl}
+          placeholder={Iflytek.ExampleEndpoint}
+          onChange={(e) =>
+            accessStore.update(
+              (access) => (access.iflytekUrl = e.currentTarget.value),
+            )
+          }
+        ></input>
+      </ListItem>
+      <ListItem
+        title={Locale.Settings.Access.Iflytek.ApiKey.Title}
+        subTitle={Locale.Settings.Access.Iflytek.ApiKey.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.iflytekApiKey}
+          type="text"
+          placeholder={Locale.Settings.Access.Iflytek.ApiKey.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.iflytekApiKey = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+
+      <ListItem
+        title={Locale.Settings.Access.Iflytek.ApiSecret.Title}
+        subTitle={Locale.Settings.Access.Iflytek.ApiSecret.SubTitle}
+      >
+        <PasswordInput
+          value={accessStore.iflytekApiSecret}
+          type="text"
+          placeholder={Locale.Settings.Access.Iflytek.ApiSecret.Placeholder}
+          onChange={(e) => {
+            accessStore.update(
+              (access) => (access.iflytekApiSecret = e.currentTarget.value),
+            );
+          }}
+        />
+      </ListItem>
+    </>
+  );
 
   return (
     <ErrorBoundary>
@@ -1475,6 +1530,7 @@ export function Settings() {
                   {tencentConfigComponent}
                   {moonshotConfigComponent}
                   {stabilityConfigComponent}
+                  {lflytekConfigComponent}
                 </>
               )}
             </>

+ 11 - 0
app/config/server.ts

@@ -66,6 +66,11 @@ declare global {
       MOONSHOT_URL?: string;
       MOONSHOT_API_KEY?: string;
 
+      // iflytek only
+      IFLYTEK_URL?: string;
+      IFLYTEK_API_KEY?: string;
+      IFLYTEK_API_SECRET?: string;
+
       // custom template for preprocessing user input
       DEFAULT_INPUT_TEMPLATE?: string;
     }
@@ -131,6 +136,7 @@ export const getServerSideConfig = () => {
   const isBytedance = !!process.env.BYTEDANCE_API_KEY;
   const isAlibaba = !!process.env.ALIBABA_API_KEY;
   const isMoonshot = !!process.env.MOONSHOT_API_KEY;
+  const isIflytek = !!process.env.IFLYTEK_API_KEY;
   // const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
   // const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
   // const randomIndex = Math.floor(Math.random() * apiKeys.length);
@@ -188,6 +194,11 @@ export const getServerSideConfig = () => {
     moonshotUrl: process.env.MOONSHOT_URL,
     moonshotApiKey: getApiKey(process.env.MOONSHOT_API_KEY),
 
+    isIflytek,
+    iflytekUrl: process.env.IFLYTEK_URL,
+    iflytekApiKey: process.env.IFLYTEK_API_KEY,
+    iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
+
     cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
     cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
     cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),

+ 28 - 0
app/constant.ts

@@ -26,6 +26,7 @@ export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
 export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
 
 export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
+export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
 
 export const CACHE_URL_PREFIX = "/api/cache";
 export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
@@ -53,6 +54,7 @@ export enum ApiPath {
   Alibaba = "/api/alibaba",
   Tencent = "/api/tencent",
   Moonshot = "/api/moonshot",
+  Iflytek = "/api/iflytek",
   Stability = "/api/stability",
   Artifacts = "/api/artifacts",
 }
@@ -109,6 +111,7 @@ export enum ServiceProvider {
   Tencent = "Tencent",
   Moonshot = "Moonshot",
   Stability = "Stability",
+  Iflytek = "Iflytek",
 }
 
 // Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
@@ -130,6 +133,7 @@ export enum ModelProvider {
   Qwen = "Qwen",
   Hunyuan = "Hunyuan",
   Moonshot = "Moonshot",
+  Iflytek = "Iflytek",
 }
 
 export const Stability = {
@@ -206,6 +210,11 @@ export const Moonshot = {
   ChatPath: "v1/chat/completions",
 };
 
+export const Iflytek = {
+  ExampleEndpoint: IFLYTEK_BASE_URL,
+  ChatPath: "v1/chat/completions",
+};
+
 export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
 // export const DEFAULT_SYSTEM_TEMPLATE = `
 // You are ChatGPT, a large language model trained by {{ServiceProvider}}.
@@ -325,6 +334,14 @@ const tencentModels = [
 
 const moonshotModes = ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"];
 
+const iflytekModels = [
+  "general",
+  "generalv3",
+  "pro-128k",
+  "generalv3.5",
+  "4.0Ultra",
+];
+
 let seq = 1000; // 内置的模型序号生成器从1000开始
 export const DEFAULT_MODELS = [
   ...openaiModels.map((name) => ({
@@ -426,6 +443,17 @@ export const DEFAULT_MODELS = [
       sorted: 9,
     },
   })),
+  ...iflytekModels.map((name) => ({
+    name,
+    available: true,
+    sorted: seq++,
+    provider: {
+      id: "iflytek",
+      providerName: "Iflytek",
+      providerType: "iflytek",
+      sorted: 10,
+    },
+  })),
 ] as const;
 
 export const CHAT_PAGE_SIZE = 15;

+ 16 - 0
app/locales/cn.ts

@@ -436,6 +436,22 @@ const cn = {
           SubTitle: "样例:",
         },
       },
+      Iflytek: {
+        ApiKey: {
+          Title: "ApiKey",
+          SubTitle: "从讯飞星火控制台获取的 APIKey",
+          Placeholder: "APIKey",
+        },
+        ApiSecret: {
+          Title: "ApiSecret",
+          SubTitle: "从讯飞星火控制台获取的 APISecret",
+          Placeholder: "APISecret",
+        },
+        Endpoint: {
+          Title: "接口地址",
+          SubTitle: "样例:",
+        },
+      },
       CustomModel: {
         Title: "自定义模型名",
         SubTitle: "增加自定义模型可选项,使用英文逗号隔开",

+ 16 - 0
app/locales/en.ts

@@ -420,6 +420,22 @@ const en: LocaleType = {
           SubTitle: "Example: ",
         },
       },
+      Iflytek: {
+        ApiKey: {
+          Title: "Iflytek API Key",
+          SubTitle: "Use a Iflytek API Key",
+          Placeholder: "Iflytek API Key",
+        },
+        ApiSecret: {
+          Title: "Iflytek API Secret",
+          SubTitle: "Use a Iflytek API Secret",
+          Placeholder: "Iflytek API Secret",
+        },
+        Endpoint: {
+          Title: "Endpoint Address",
+          SubTitle: "Example: ",
+        },
+      },
       CustomModel: {
         Title: "Custom Models",
         SubTitle: "Custom model options, seperated by comma",

+ 13 - 0
app/store/access.ts

@@ -51,6 +51,10 @@ const DEFAULT_STABILITY_URL = isApp
   ? DEFAULT_API_HOST + "/api/proxy/stability"
   : ApiPath.Stability;
 
+const DEFAULT_IFLYTEK_URL = isApp
+  ? DEFAULT_API_HOST + "/api/proxy/iflytek"
+  : ApiPath.Iflytek;
+
 const DEFAULT_ACCESS_STATE = {
   accessCode: "",
   useCustomConfig: false,
@@ -103,6 +107,11 @@ const DEFAULT_ACCESS_STATE = {
   tencentSecretKey: "",
   tencentSecretId: "",
 
+  // iflytek
+  iflytekUrl: DEFAULT_IFLYTEK_URL,
+  iflytekApiKey: "",
+  iflytekApiSecret: "",
+
   // server config
   needCode: true,
   hideUserApiKey: false,
@@ -158,6 +167,9 @@ export const useAccessStore = createPersistStore(
     isValidMoonshot() {
       return ensure(get(), ["moonshotApiKey"]);
     },
+    isValidIflytek() {
+      return ensure(get(), ["iflytekApiKey"]);
+    },
 
     isAuthorized() {
       this.fetch();
@@ -173,6 +185,7 @@ export const useAccessStore = createPersistStore(
         this.isValidAlibaba() ||
         this.isValidTencent ||
         this.isValidMoonshot() ||
+        this.isValidIflytek() ||
         !this.enabledAccessControl() ||
         (this.enabledAccessControl() && ensure(get(), ["accessCode"]))
       );