diff --git a/README-ES.md b/README-ES.md
index dbaf2b43..3f7c62b7 100644
--- a/README-ES.md
+++ b/README-ES.md
@@ -48,6 +48,18 @@ AutoClip utiliza IA para analizar los subtítulos de un vídeo, encontrar moment
## Agradecimientos ❤️
+
+
+
+ 88API
+
+
+ ¡Gracias a 88API por patrocinar AutoClip! Reúne GPT, Claude, Gemini, Grok, DeepSeek, Kimi y GLM para analizar subtítulos, seleccionar momentos destacados y generar títulos.
+ 🎨 Multimedia : Ofrece modelos de imagen, vídeo y audio como GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper y TTS. AutoClip utiliza API compatibles de análisis, portadas y transcripción.
+ 🏷️ Servicio y facturación : Según el patrocinador, opera mediante una empresa extranjera y ofrece atención humana, facturas y recargas con proporción 1:1; se aplican las condiciones de la plataforma.
+ 🎁 Nuevos usuarios : Obtén crédito de prueba para evaluar modelos mediante el enlace de referido , según las condiciones de la promoción. Guía de configuración (inglés)
+
+
diff --git a/README-FR.md b/README-FR.md
index 5842e9b0..7523b619 100644
--- a/README-FR.md
+++ b/README-FR.md
@@ -48,6 +48,18 @@ AutoClip utilise l’IA pour analyser les sous-titres, repérer les temps forts,
## Remerciements ❤️
+
+
+
+ 88API
+
+
+ Merci à 88API de sponsoriser AutoClip ! La plateforme réunit GPT, Claude, Gemini, Grok, DeepSeek, Kimi et GLM pour analyser les transcriptions, sélectionner les temps forts et générer des titres.
+ 🎨 Multimédia : Modèles d’image, de vidéo et d’audio, dont GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper et TTS. AutoClip utilise les API compatibles d’analyse, de couverture et de transcription.
+ 🏷️ Service et facturation : Le partenaire indique être exploité par une société à l’étranger et proposer une assistance humaine, des factures et un ratio de recharge de 1:1 ; les conditions de la plateforme s’appliquent.
+ 🎁 Nouveaux utilisateurs : Recevez un crédit d’essai pour tester les modèles via le lien de parrainage , selon les conditions de la promotion. Guide de configuration (anglais)
+
+
diff --git a/README-JA.md b/README-JA.md
index 90e5f562..8d6454cf 100644
--- a/README-JA.md
+++ b/README-JA.md
@@ -48,6 +48,18 @@ AutoClip は AI で動画の字幕を分析し、見どころを抽出してタ
## スポンサーへの感謝 ❤️
+
+
+
+ 88API
+
+
+ 88API Tokenプラットフォーム のご支援に感謝します!GPT、Claude、Gemini、Grok、DeepSeek、Kimi、GLM を集約し、字幕分析・見どころ選定・タイトル生成に活用できます。
+ 🎨 マルチメディア :GPT-Image、Seedance、Veo、MiniMax Hailuo H3、Kling、Whisper、TTS などの画像・動画・音声モデルを提供。AutoClip は対応する分析・カバー画像生成・文字起こし API を利用できます。
+ 🏷️ サービスと決済 :提携先によると海外法人が運営し、有人サポート・請求書・1:1 のチャージ比率を提供。条件はプラットフォームをご確認ください。
+ 🎁 新規登録特典 :専用紹介リンク から登録するとモデル検証用の体験クレジットを受け取れます。適用条件はキャンペーンページをご確認ください。 設定ガイド(英語)
+
+
diff --git a/README-KO.md b/README-KO.md
index ca7c1e79..923222d6 100644
--- a/README-KO.md
+++ b/README-KO.md
@@ -48,6 +48,18 @@ AutoClip은 AI로 영상 자막을 분석하고 하이라이트를 찾아 제목
## 후원에 감사드립니다 ❤️
+
+
+
+ 88API
+
+
+ 88API Token 플랫폼 의 AutoClip 후원에 감사드립니다! GPT, Claude, Gemini, Grok, DeepSeek, Kimi, GLM을 통합하여 자막 분석, 하이라이트 선택, 제목 생성에 활용할 수 있습니다.
+ 🎨 멀티미디어 : GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper, TTS 등 이미지·영상·음성 모델을 제공합니다. AutoClip에서는 호환되는 분석·표지 생성·자막 전사 API를 사용합니다.
+ 🏷️ 서비스 및 결제 : 파트너 안내에 따르면 해외 법인이 운영하며 상담원 지원, 정식 청구서, 1:1 충전 비율을 제공합니다. 세부 조건은 플랫폼을 확인하세요.
+ 🎁 신규 가입 혜택 : 전용 추천 링크 로 가입하면 모델 테스트용 체험 크레딧을 받을 수 있습니다. 이벤트 조건이 적용됩니다. 설정 가이드 (영어)
+
+
diff --git a/README-PT.md b/README-PT.md
index 40f94857..1f5acf09 100644
--- a/README-PT.md
+++ b/README-PT.md
@@ -48,6 +48,18 @@ O AutoClip usa IA para analisar legendas, encontrar destaques, criar títulos e
## Agradecimentos ❤️
+
+
+
+ 88API
+
+
+ Agradecemos à 88API por patrocinar o AutoClip! Reúne GPT, Claude, Gemini, Grok, DeepSeek, Kimi e GLM para analisar legendas, selecionar destaques e gerar títulos.
+ 🎨 Multimídia : Modelos de imagem, vídeo e áudio, incluindo GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper e TTS. O AutoClip utiliza APIs compatíveis de análise, capas e transcrição.
+ 🏷️ Serviço e faturamento : Segundo o parceiro, a operação é de uma empresa no exterior, com atendimento humano, faturas e proporção de recarga de 1:1; aplicam-se as condições da plataforma.
+ 🎁 Novos usuários : Receba créditos de teste para avaliar modelos pelo link de indicação , conforme as condições da promoção. Guia de configuração (inglês)
+
+
diff --git a/README-RU.md b/README-RU.md
index 0730c7f3..5b27e72b 100644
--- a/README-RU.md
+++ b/README-RU.md
@@ -48,6 +48,18 @@ AutoClip с помощью ИИ анализирует субтитры, нах
## Благодарим спонсоров ❤️
+
+
+
+ 88API
+
+
+ Благодарим 88API за поддержку AutoClip! Платформа объединяет GPT, Claude, Gemini, Grok, DeepSeek, Kimi и GLM для анализа субтитров, выбора ярких моментов и создания заголовков.
+ 🎨 Мультимедиа : Модели изображений, видео и аудио, включая GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper и TTS. AutoClip использует совместимые API анализа, создания обложек и транскрипции.
+ 🏷️ Сервис и оплата : По информации партнёра, сервис управляется зарубежной компанией и предлагает поддержку операторов, счета и пополнение в соотношении 1:1; действуют условия платформы.
+ 🎁 Новым пользователям : Получите пробный кредит для проверки моделей по реферальной ссылке на условиях акции. Инструкция по настройке (англ.)
+
+
diff --git a/README.md b/README.md
index 5fbd2a00..abf64a9b 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,18 @@
## 特别感谢 ❤️
+
+
+
+ 88API
+
+
+ 感谢 88API Token聚合平台 赞助 AutoClip!聚合 GPT、Claude、Gemini、Grok、DeepSeek、Kimi、GLM 等语言与编程模型,适合字幕分析、高光筛选与标题生成。
+ 🎨 多媒体能力 :平台提供 GPT-Image、Gemini、Grok 等图片模型,Seedance、Veo、MiniMax Hailuo H3、Kling、Grok 等视频模型,以及 Whisper、TTS 等语音能力;AutoClip 可接入兼容的分析、封面生图与字幕转写接口。
+ 🏷️ 服务与结算 :据合作方介绍,由海外企业运营,提供人工客服、正规发票,充值比例 1:1;具体模型、服务与结算条件以平台页面为准。
+ 🎁 新用户福利 :通过 专属推广链接注册 可获体验额度,用于测试模型能力,领取条件以活动页面为准。 接入说明
+
+
diff --git a/backend/api/v1/settings.py b/backend/api/v1/settings.py
index b6c6910b..0848357f 100644
--- a/backend/api/v1/settings.py
+++ b/backend/api/v1/settings.py
@@ -113,6 +113,7 @@ class ApiKeys(BaseModel):
glm: str = Field(default="", description="智谱 GLM API密钥")
grok: str = Field(default="", description="xAI Grok API密钥")
infistar: str = Field(default="", description="Infistar 无限星河 API密钥")
+ api88: str = Field(default="", description="88API API密钥")
seed: str = Field(default="", description="火山方舟 Seed / 豆包 API密钥")
jimeng_access: str = Field(default="", description="即梦AI访问密钥")
jimeng_secret: str = Field(default="", description="即梦AI秘密密钥")
@@ -121,7 +122,7 @@ class ApiKeys(BaseModel):
class ApiSettings(BaseModel):
"""API设置"""
api_keys: ApiKeys = Field(default_factory=ApiKeys, description="API密钥")
- api_provider: str = Field(default="dashscope", description="当前 LLM 提供商(dashscope / openai / gemini / deepseek / seed / kimi / glm / grok / infistar,或本地预设 ollama / lmstudio)")
+ api_provider: str = Field(default="dashscope", description="当前 LLM 提供商(dashscope / openai / gemini / deepseek / seed / kimi / glm / grok / infistar / api88,或本地预设 ollama / lmstudio)")
api_base_url: str = Field(default="", description="OpenAI 兼容接口地址;provider=openai 时空为官方地址,本地预设为空时用预设默认地址")
api_model: str = Field(default="qwen-plus", description="默认模型")
api_max_tokens: int = Field(default=4096, description="最大Token数")
@@ -314,6 +315,7 @@ async def get_settings():
glm="",
grok="",
infistar="",
+ api88="",
seed="",
jimeng_access="", # 默认值
jimeng_secret="" # 默认值
@@ -749,6 +751,7 @@ def _saved_provider_api_key(settings: DesktopSettings, provider: str) -> str:
"glm": keys.glm,
"grok": keys.grok,
"infistar": keys.infistar,
+ "api88": keys.api88,
"seed": keys.seed,
}.get((provider or "").strip().lower(), "") or ""
diff --git a/backend/cli.py b/backend/cli.py
index 4a848600..111cc943 100644
--- a/backend/cli.py
+++ b/backend/cli.py
@@ -59,7 +59,7 @@ def _err(msg: str) -> None:
def _add_llm_args(p: argparse.ArgumentParser) -> None:
g = p.add_argument_group("模型(不填则用桌面应用设置页里的配置)")
- g.add_argument("--provider", choices=PROVIDER_CHOICES, help="dashscope / openai / gemini / deepseek / seed / kimi / glm / grok / infistar,或本地预设 ollama / lmstudio")
+ g.add_argument("--provider", choices=PROVIDER_CHOICES, help="dashscope / openai / gemini / deepseek / seed / kimi / glm / grok / infistar / api88,或本地预设 ollama / lmstudio")
g.add_argument("--model", help="模型名,如 qwen-plus、gpt-4o-mini、qwen2.5:7b")
g.add_argument("--base-url", help="OpenAI 兼容接口地址(provider=openai 时用;ollama/lmstudio 有默认值)")
g.add_argument("--api-key", help="API Key(本地模型可不填)。也可用环境变量 AUTOCLIP_API_KEY")
@@ -206,6 +206,7 @@ def cmd_providers(args: argparse.Namespace) -> int:
("kimi", "Kimi(月之暗面)", "需要 key;国内直连"),
("glm", "智谱 GLM", "需要 key;国内直连"),
("grok", "Grok(xAI)", "需要 key"),
+ ("api88", "88API Token聚合平台(赞助)", "需要 key,新用户注册赠送体验额度"),
("infistar", "Infistar 无限星河(赞助)", "需要 key,专属链接注册可领体验额度"),
] + [
(p.key, p.display_name, f"无需 key;默认 {p.base_url}" + (f",默认模型 {p.default_model}" if p.default_model else ""))
diff --git a/backend/core/asr_model_catalog.py b/backend/core/asr_model_catalog.py
index 9f47024b..06aa11d4 100644
--- a/backend/core/asr_model_catalog.py
+++ b/backend/core/asr_model_catalog.py
@@ -3,7 +3,9 @@
Sources and intentionally unsupported integrations: docs/AI_MODEL_CONFIGURATION.md.
The public/account catalog still determines availability, not this routing table.
"""
+# 88API: https://88api.ai/zh/docs/api/audio/openai-audio/
MODELS = {
+ 'api88': ['whisper-1'],
'openai': ['whisper-1', 'gpt-4o-transcribe-diarize', 'gpt-4o-transcribe', 'gpt-4o-mini-transcribe'],
'dashscope': ['qwen-audio-3.1-asr-flash', 'qwen-audio-3.0-asr-flash', 'fun-asr-flash-2026-06-15',
'qwen-audio-3.1-asr-flash-filetrans', 'qwen-audio-3.0-asr-flash-filetrans',
@@ -15,6 +17,8 @@ DASHSCOPE_SYNC = frozenset(MODELS['dashscope'][:3])
def adapter(provider, model):
+ if provider == 'api88':
+ return 'openai' if model == 'whisper-1' else None
if provider == 'dashscope' and model in DASHSCOPE_SYNC:
return 'dashscope'
if provider in {'openai', 'infistar', 'compatible'}:
diff --git a/backend/core/cloud_presets.py b/backend/core/cloud_presets.py
index cf97874b..1b2ab5d2 100644
--- a/backend/core/cloud_presets.py
+++ b/backend/core/cloud_presets.py
@@ -1,5 +1,5 @@
"""
-云端 OpenAI 兼容预设:DeepSeek / Seed / Kimi / GLM / Grok / Infistar(赞助)。
+云端 OpenAI 兼容预设:DeepSeek / Seed / Kimi / GLM / Grok / Infistar / 88API(赞助)。
底层仍是 openai + 官方 base_url,和 Ollama 预设同一套路,但必须带各家自己的 key,
不能复用用户的 OpenAI key。设置页、CLI、MCP 都用 `--provider deepseek` 这种名字。
@@ -78,9 +78,17 @@ CLOUD_PRESETS: Dict[str, CloudPreset] = {
docs_url="https://www.infistar.cc/register?aff=XLK3BCM6&ref_source=link",
hint="赞助合作伙伴。一个 Key 调 Claude / GPT / Gemini / DeepSeek 等模型,专属链接注册可领体验额度。",
),
+ "api88": CloudPreset(
+ key="api88", display_name="88API Token聚合平台", base_url="https://88api.ai/v1",
+ default_model="", api_key_setting="api88_api_key",
+ docs_url="https://88api.ai/sign-up?aff=2PIc",
+ hint="赞助合作伙伴。新用户注册赠送体验额度,站内提供人工客服。",
+ ),
}
_ALIASES = {
+ "88api": "api88",
+ "88api.ai": "api88",
"deepseek-ai": "deepseek",
"moonshot": "kimi",
"moonshotai": "kimi",
@@ -110,7 +118,7 @@ def normalize_cloud_preset_key(name: Optional[str]) -> Optional[str]:
def resolve_cloud_preset(provider: Optional[str], base_url: Optional[str] = None) -> Optional[Tuple[str, str, CloudPreset]]:
"""
- 若 provider 是 deepseek / seed / kimi / glm / grok / infistar(或别名),返回 (openai, base_url, preset)。
+ 若 provider 是 deepseek / seed / kimi / glm / grok / infistar / api88(或别名),返回 (openai, base_url, preset)。
不是云端预设则返回 None。
"""
key = normalize_cloud_preset_key(provider)
diff --git a/backend/core/image_model_catalog.py b/backend/core/image_model_catalog.py
index f66d1bc1..ea886c9f 100644
--- a/backend/core/image_model_catalog.py
+++ b/backend/core/image_model_catalog.py
@@ -4,6 +4,7 @@ Checked 2026-09-29. Keep exact provider IDs: gateway aliases are not official ID
Sources are retained so catalog additions can be reviewed against provider docs.
"""
SOURCES = {
+ 'api88': 'https://88api.ai/zh/docs/api/image/openai-image/',
'seed': 'https://docs.volcengine.com/docs/ark/model-release-announcement?lang=zh',
'dashscope': 'https://help.aliyun.com/en/model-studio/image-model/',
'openai': 'https://developers.openai.com/api/docs/guides/image-generation',
@@ -12,6 +13,7 @@ SOURCES = {
'glm': 'https://docs.bigmodel.cn/cn/guide/models/image-generation/glm-image',
}
MODELS = {
+ 'api88': ['gpt-image-1', 'dall-e-3', 'dall-e-2'],
'seed': ['doubao-seedream-5-0-flash-260915', 'doubao-seedream-5-0-pro-260628', 'doubao-seedream-4-0-20260415'],
'openai': ['gpt-image-2.5-flare', 'gpt-image-2.5-sunburst', 'gpt-image-2', 'gpt-image-1.5', 'gpt-image-1', 'gpt-image-1-mini', 'chatgpt-image-latest'],
'gemini': ['gemini-3.1-flash-lite-image', 'gemini-3.1-flash-image', 'gemini-3-pro-image', 'gemini-2.5-flash-image'],
diff --git a/backend/core/llm_manager.py b/backend/core/llm_manager.py
index f1378adf..111b78ee 100644
--- a/backend/core/llm_manager.py
+++ b/backend/core/llm_manager.py
@@ -105,6 +105,7 @@ class LLMManager:
"glm_api_key": "",
"grok_api_key": "",
"infistar_api_key": "",
+ "api88_api_key": "",
"seed_api_key": "",
"model_name": "qwen-plus",
"chunk_size": 5000,
@@ -132,6 +133,7 @@ class LLMManager:
"glm_api_key": api_keys.get("glm", ""),
"grok_api_key": api_keys.get("grok", ""),
"infistar_api_key": api_keys.get("infistar", ""),
+ "api88_api_key": api_keys.get("api88", ""),
"seed_api_key": api_keys.get("seed", ""),
"model_name": api.get("api_model", "qwen-plus")
})
@@ -225,6 +227,7 @@ class LLMManager:
"glm_api_key": ("API_GLM_API_KEY", "ZHIPU_API_KEY", "GLM_API_KEY"),
"grok_api_key": ("API_GROK_API_KEY", "XAI_API_KEY", "GROK_API_KEY"),
"infistar_api_key": ("API_INFISTAR_API_KEY", "INFISTAR_API_KEY"),
+ "api88_api_key": ("API_API88_API_KEY", "API88_API_KEY"),
"seed_api_key": ("API_SEED_API_KEY", "ARK_API_KEY", "VOLCENGINE_API_KEY", "DOUBAO_API_KEY"),
}
diff --git a/backend/core/model_catalog.py b/backend/core/model_catalog.py
index 09349a00..030563ba 100644
--- a/backend/core/model_catalog.py
+++ b/backend/core/model_catalog.py
@@ -79,6 +79,7 @@ CURATED_MODELS: Dict[str, List[str]] = {
],
# 多模型网关,型号随账号而定:不内置名单,填好 key 后实时拉取
"infistar": [],
+ "api88": [],
}
DEFAULT_MODELS: Dict[str, str] = {
@@ -91,6 +92,7 @@ DEFAULT_MODELS: Dict[str, str] = {
"glm": "glm-5.3",
"grok": "grok-4.6",
"infistar": "",
+ "api88": "",
}
PROVIDER_LABELS: Dict[str, str] = {
@@ -103,6 +105,7 @@ PROVIDER_LABELS: Dict[str, str] = {
"glm": "GLM",
"grok": "Grok",
"infistar": "Infistar",
+ "api88": "88API",
}
CLOUD_PROVIDERS = frozenset(CURATED_MODELS)
@@ -138,6 +141,7 @@ IMAGE_MODELS: Dict[str, List[str]] = {
"dashscope": ["wanx2.1-t2i-turbo", "wanx2.1-t2i-plus"],
"seed": ["doubao-seedream-5-0-260128"],
"openai": ["gpt-image-1"],
+ "api88": ["gpt-image-1", "dall-e-3", "dall-e-2"],
# Infistar 是多模型网关,主流生图模型都能调,具体以账号可用型号为准
"infistar": ["gpt-image-1", "dall-e-3", "doubao-seedream-5-0-260128", "flux-1.1-pro", "imagen-4", "wanx2.1-t2i-turbo"],
}
diff --git a/backend/core/model_registry.py b/backend/core/model_registry.py
index d938426a..64462c78 100644
--- a/backend/core/model_registry.py
+++ b/backend/core/model_registry.py
@@ -139,7 +139,7 @@ def _catalog_record(provider, model, data):
def _catalog_capability(provider, model, data):
known = _VERIFIED.get((provider, model)) or _catalog_record(provider, model, data).get('capability')
- if not known and provider == 'infistar':
+ if not known and provider in {'infistar', 'api88'}:
# Public gateway IDs can match original vendors exactly. Require agreement;
# do not infer a capability from a name prefix or missing image tags.
matches = {models[model]['capability'] for models in data.get('metadata', {}).get('providers', {}).values()
diff --git a/backend/mcp_server.py b/backend/mcp_server.py
index 5bf483e8..4e4e8393 100644
--- a/backend/mcp_server.py
+++ b/backend/mcp_server.py
@@ -121,7 +121,7 @@ def _make_override(provider: Optional[str], model: Optional[str], base_url: Opti
description=(
"把一条本地视频切成高光片段(同步,耗时数分钟到数十分钟,期间会发进度)。"
"返回切片列表(标题 / 起止时间 / 评分 / mp4 路径)与合集。"
- "provider 可选 dashscope / openai / gemini / deepseek / seed / kimi / glm / grok / infistar / ollama / lmstudio;不填用桌面应用里已配置的模型。"
+ "provider 可选 dashscope / openai / gemini / deepseek / seed / kimi / glm / grok / infistar / api88 / ollama / lmstudio;不填用桌面应用里已配置的模型。"
),
)
async def clip_video(
@@ -220,7 +220,7 @@ def list_providers() -> Dict[str, Any]:
return {
"current": get_llm_manager().get_current_provider_info(),
- "cloud": ["dashscope", "openai", "gemini", "deepseek", "seed", "kimi", "glm", "grok", "infistar"],
+ "cloud": ["dashscope", "openai", "gemini", "deepseek", "seed", "kimi", "glm", "grok", "infistar", "api88"],
"local_presets": presets_as_dicts(),
}
diff --git a/backend/services/cover.py b/backend/services/cover.py
index 6bcb9db9..67851223 100644
--- a/backend/services/cover.py
+++ b/backend/services/cover.py
@@ -108,6 +108,9 @@ def _text_model_key(provider: str, base_url: str) -> str:
return str(settings.get("dashscope_api_key") or "")
if kind == "seedream":
return str(settings.get("seed_api_key") or "")
+ from urllib.parse import urlparse
+ if urlparse(base_url or "").hostname == "88api.ai":
+ return str(settings.get("api88_api_key") or "")
if "infistar" in host:
return str(settings.get("infistar_api_key") or "")
if not host or "api.openai.com" in host:
@@ -136,8 +139,8 @@ def text_model_image_service() -> dict[str, str] | None:
return None
if preset == "seed":
return {"provider": "seedream", "text_provider": "seed", "api_key": str(s.get("seed_api_key") or ""), "base_url": ""}
- if preset == "infistar":
- return {"provider": "openai", "text_provider": "infistar", "api_key": str(s.get("infistar_api_key") or ""),
+ if preset in {"infistar", "api88"}:
+ return {"provider": "openai", "text_provider": preset, "api_key": str(s.get(f"{preset}_api_key") or ""),
"base_url": str(s.get("openai_base_url") or "")}
if preset:
return None
diff --git a/backend/services/local_runner.py b/backend/services/local_runner.py
index 054d41ef..e71eafe3 100644
--- a/backend/services/local_runner.py
+++ b/backend/services/local_runner.py
@@ -25,7 +25,7 @@ from typing import Any, Callable, Dict, List, Optional
logger = logging.getLogger(__name__)
-PROVIDER_CHOICES = ("dashscope", "openai", "gemini", "deepseek", "seed", "kimi", "glm", "grok", "infistar", "ollama", "lmstudio")
+PROVIDER_CHOICES = ("dashscope", "openai", "gemini", "deepseek", "seed", "kimi", "glm", "grok", "infistar", "api88", "ollama", "lmstudio")
# ---------------------------------------------------------------- environment ---
@@ -145,7 +145,7 @@ def configure_llm(override: LLMOverride) -> Dict[str, Any]:
"dashscope": "dashscope_api_key", "openai": "openai_api_key",
"gemini": "gemini_api_key", "siliconflow": "siliconflow_api_key",
"deepseek": "deepseek_api_key",
- "kimi": "kimi_api_key", "glm": "glm_api_key", "grok": "grok_api_key", "infistar": "infistar_api_key",
+ "kimi": "kimi_api_key", "glm": "glm_api_key", "grok": "grok_api_key", "infistar": "infistar_api_key", "api88": "api88_api_key",
"seed": "seed_api_key",
}.get(provider)
if key_field:
diff --git a/backend/tests/test_ai_model_settings.py b/backend/tests/test_ai_model_settings.py
index 5c3ff4f5..2b31892e 100644
--- a/backend/tests/test_ai_model_settings.py
+++ b/backend/tests/test_ai_model_settings.py
@@ -346,3 +346,32 @@ def test_cloud_selection_routes_auto_without_loading_whisper(monkeypatch, tmp_pa
assert generate_subtitle_for_video(tmp_path / 'video.mp4') == tmp_path / 'result.srt'
assert calls[0][2] is value
assert configured_whisper_model('small') == 'small'
+
+
+def test_api88_gateway_catalog_routes_account_models_and_isolates_keys(monkeypatch):
+ async def no_metadata():
+ pass
+
+ async def account_models(url, **kwargs):
+ assert url == 'https://88api.ai/v1/models'
+ assert kwargs['headers']['Authorization'] == 'Bearer sk-88-only'
+ return {'data': [{'id': 'gpt-image-1'}, {'id': 'whisper-1'},
+ {'id': 'gpt-5-mini'}, {'id': 'tts-1'}, {'id': 'sora-2'}]}
+
+ monkeypatch.setattr(registry, '_ensure_metadata', no_metadata)
+ monkeypatch.setattr(registry.model_catalog, '_http_get_json', account_models)
+ c = ai.Connection(id='88', name='88API', provider='api88', api_key='sk-88-only')
+ result = asyncio.run(registry.discover(c, refresh=True))
+ by_id = {m['id']: m for m in result['models']}
+ assert by_id['gpt-image-1']['image'] and not by_id['gpt-image-1']['analysis']
+ assert by_id['whisper-1']['asr_supported'] and not by_id['whisper-1']['analysis']
+ assert by_id['gpt-5-mini']['analysis']
+ assert not by_id['tts-1']['analysis'] and not by_id['sora-2']['analysis']
+ assert ai.image_endpoint(c)['base_url'] == 'https://88api.ai/v1'
+ assert ai.image_endpoint(c)['api_key'] == 'sk-88-only'
+
+ config = ai.ModelSettings(connections=[c], analysis=ai.Assignment(connection_id='88', model='gpt-5-mini'),
+ transcription=ai.Transcription(provider='cloud', connection_id='88', model='whisper-1'))
+ response = ai.save(config)
+ assert 'sk-88-only' not in json.dumps(response)
+ assert ai.load().connections[0].api_key == 'sk-88-only'
diff --git a/backend/tests/test_cloud_presets.py b/backend/tests/test_cloud_presets.py
index 07aa3eb2..bea40446 100644
--- a/backend/tests/test_cloud_presets.py
+++ b/backend/tests/test_cloud_presets.py
@@ -31,7 +31,7 @@ def fake_openai(monkeypatch):
monkeypatch.setitem(sys.modules, "openai", module)
_FakeOpenAIClient.created.clear()
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
- for name in ("LLM_PROVIDER", "API_MODEL_NAME", "LLM_MODEL", "API_OPENAI_API_KEY", "OPENAI_API_KEY", "API_DEEPSEEK_API_KEY", "DEEPSEEK_API_KEY", "API_INFISTAR_API_KEY", "INFISTAR_API_KEY"):
+ for name in ("LLM_PROVIDER", "API_MODEL_NAME", "LLM_MODEL", "API_OPENAI_API_KEY", "OPENAI_API_KEY", "API_DEEPSEEK_API_KEY", "DEEPSEEK_API_KEY", "API_INFISTAR_API_KEY", "INFISTAR_API_KEY", "API_API88_API_KEY", "API88_API_KEY"):
monkeypatch.delenv(name, raising=False)
from backend.core import llm_manager as manager_module
monkeypatch.setattr(manager_module.config_sync_service, "is_sync_needed", lambda: False)
@@ -89,7 +89,7 @@ def test_manager_uses_seed_ark_key_not_openai(fake_openai, tmp_path):
def test_curated_lists_drop_retired_aliases():
assert "gpt-4o" not in CLOUD_PRESETS
- assert set(CLOUD_PRESETS) == {"deepseek", "seed", "kimi", "glm", "grok", "infistar"}
+ assert set(CLOUD_PRESETS) == {"deepseek", "seed", "kimi", "glm", "grok", "infistar", "api88"}
def test_infistar_sponsor_preset_uses_gateway_and_own_key(fake_openai, tmp_path):
@@ -120,3 +120,34 @@ def test_infistar_key_from_env(fake_openai, tmp_path, monkeypatch):
settings.write_text('{"api": {"api_keys": {}, "api_provider": "infistar", "api_model": "gpt-5-mini"}}', encoding="utf-8")
LLMManager(settings_file=settings).get_current_provider_info()
assert fake_openai.created[-1]["api_key"] == "sk-infistar-from-env"
+
+
+def test_api88_sponsor_preset_uses_gateway_and_own_key(fake_openai, tmp_path):
+ provider, url, preset = resolve_cloud_preset("api88")
+ assert provider == "openai"
+ assert url == "https://88api.ai/v1"
+ # 多模型网关不预设型号;专属注册链接带推广参数
+ assert preset.default_model == ""
+ assert "aff=" in preset.docs_url
+
+ settings = tmp_path / "settings.json"
+ settings.write_text(
+ '{"api": {"api_keys": {"openai": "sk-openai-should-not-be-used", "api88": "sk-api88-key-123"}, '
+ '"api_provider": "api88", "api_model": "deepseek-v4-pro"}}',
+ encoding="utf-8",
+ )
+ info = LLMManager(settings_file=settings).get_current_provider_info()
+ assert info["provider"] == "api88"
+ assert info["base_url"] == "https://88api.ai/v1"
+ assert info["model"] == "deepseek-v4-pro"
+ assert fake_openai.created[-1]["api_key"] == "sk-api88-key-123"
+ assert fake_openai.created[-1]["base_url"] == "https://88api.ai/v1"
+
+
+@pytest.mark.parametrize("env_name", ["API88_API_KEY", "API_API88_API_KEY"])
+def test_api88_key_from_env(fake_openai, tmp_path, monkeypatch, env_name):
+ monkeypatch.setenv(env_name, "sk-api88-from-env")
+ settings = tmp_path / "settings.json"
+ settings.write_text('{"api": {"api_keys": {}, "api_provider": "api88", "api_model": "gpt-5-mini"}}', encoding="utf-8")
+ LLMManager(settings_file=settings).get_current_provider_info()
+ assert fake_openai.created[-1]["api_key"] == "sk-api88-from-env"
diff --git a/backend/tests/test_cover.py b/backend/tests/test_cover.py
index b7cc4b40..5c2fe3d6 100644
--- a/backend/tests/test_cover.py
+++ b/backend/tests/test_cover.py
@@ -311,6 +311,12 @@ def test_cover_follows_text_model_service(data_dir, monkeypatch):
_text_keys(monkeypatch, llm_provider="openai", cloud_preset="infistar", infistar_api_key="sk-inf", openai_base_url="https://infistar.cc/v1")
cfg = cover.load_config()
assert (cfg.provider, cfg.base_url, cfg.api_key) == ("openai", "https://infistar.cc/v1", "sk-inf")
+ # 88API uses its own credentials and gateway when following the text service.
+ _text_keys(monkeypatch, llm_provider="openai", cloud_preset="api88", api88_api_key="sk-88", openai_base_url="https://88api.ai/v1")
+ cfg = cover.load_config()
+ assert (cfg.provider, cfg.base_url, cfg.api_key) == ("openai", "https://88api.ai/v1", "sk-88")
+ assert cover._text_model_key("openai", "https://88api.ai/v1") == "sk-88"
+ assert cover._text_model_key("openai", "https://88api.ai.evil.example/v1") == ""
# 换成没有生图的 DeepSeek:自动关掉,封面走截帧
_text_keys(monkeypatch, llm_provider="openai", cloud_preset="deepseek", deepseek_api_key="sk-ds")
cfg = cover.load_config()
diff --git a/backend/tests/test_model_catalog.py b/backend/tests/test_model_catalog.py
index b910755d..fa936c89 100644
--- a/backend/tests/test_model_catalog.py
+++ b/backend/tests/test_model_catalog.py
@@ -246,3 +246,24 @@ def test_infistar_has_no_builtin_list_and_fetches_gateway_models(monkeypatch):
assert live.source == "live"
# 网关里各家型号都保留,只去掉嵌入 / 视频
assert live.models == ["claude-sonnet-5-5", "deepseek-v4-pro", "gemini-3.8-flash"]
+
+
+def test_api88_has_no_builtin_list_and_fetches_gateway_models(monkeypatch):
+ # 没 key:不拿通义的型号冒充
+ empty = asyncio.run(model_catalog.list_available_models("api88"))
+ assert empty.models == [] and empty.default_model == "" and empty.source == "catalog"
+
+ _FakeAsyncClient.calls = []
+ _FakeAsyncClient.routes = {
+ "https://88api.ai/v1/models": (
+ {"data": [{"id": "claude-sonnet-5-5"}, {"id": "deepseek-v4-pro"}, {"id": "gemini-3.8-flash"},
+ {"id": "text-embedding-3-large"}, {"id": "sora-2"}]},
+ 200,
+ )
+ }
+ import httpx
+ monkeypatch.setattr(httpx, "AsyncClient", _FakeAsyncClient)
+ live = asyncio.run(model_catalog.list_available_models("api88", api_key="sk-api88-key-123", refresh=True))
+ assert live.source == "live"
+ # 网关里各家型号都保留,只去掉嵌入 / 视频
+ assert live.models == ["claude-sonnet-5-5", "deepseek-v4-pro", "gemini-3.8-flash"]
diff --git a/docs/88API_SETUP.en.md b/docs/88API_SETUP.en.md
new file mode 100644
index 00000000..592e4266
--- /dev/null
+++ b/docs/88API_SETUP.en.md
@@ -0,0 +1,59 @@
+# Use 88API with AutoClip
+
+[简体中文](88API_SETUP.md) · [Back to README](../README-EN.md)
+
+88API Token Platform sponsors AutoClip and provides OpenAI-compatible APIs for analysis, cover images and transcription.
+
+## Register and create an API key
+
+Register through the [AutoClip referral link](https://88api.ai/sign-up?aff=2PIc), then create an API key in the console. New users can receive trial credit; the amount, eligibility and terms are set by the platform. Live support is available on the platform. The referral link includes commission that supports project maintenance.
+
+## Configure AutoClip
+
+### New versions: select 88API directly
+
+1. Open **Settings → AI Models → AI service** and select **88API** in the recommended group. You can also configure it during first-run setup.
+2. Enter your 88API key. The preset base URL is `https://88api.ai/v1`.
+3. Wait for your account's model list and choose an analysis model. For visual analysis, select a multimodal model that accepts images.
+4. Test the connection and save. Verify the result with a short video before larger jobs.
+
+No default text model or account allowlist is assumed. Use exact model IDs available to your account. Image and audio previews shown without a key are reference catalogs, not proof of account access.
+
+- **Covers** can reuse the AI service key with supported OpenAI Images models such as account-listed `gpt-image-1` or `dall-e-3`. Other image models depend on their actual API and returned capability metadata.
+- **Transcription** supports account-listed `whisper-1` through the OpenAI audio API with timestamps.
+- The platform also offers video generation and TTS; these are not corresponding AutoClip features.
+
+### Older versions: use the OpenAI-compatible provider
+
+If 88API is not listed, select the custom OpenAI-compatible provider:
+
+| Setting | Value |
+| --- | --- |
+| Provider | Custom OpenAI-compatible API |
+| Base URL | `https://88api.ai/v1` |
+| API key | The API key created in the 88API console |
+| Model | An exact account model ID supporting Chat Completions |
+
+Include `/v1` only once and do not append `/chat/completions`. Select the model, test the connection and save.
+
+## CLI / MCP
+
+```bash
+export API88_API_KEY="your API key"
+autoclip run video.mp4 --provider api88 --model "exact account model ID"
+```
+
+`API_API88_API_KEY` and `--api-key` are also supported. Use `provider="api88"` with MCP.
+
+## Troubleshooting and references
+
+- 401: Check the key, token status, balance and surrounding whitespace.
+- 404: Check the base URL and exact model ID, and ensure the model supports the requested API.
+- Empty or failed model list: Check account permissions, refresh or enter the exact ID shown in the console.
+- A connection test does not validate every API. Verify actual analysis, cover generation or transcription separately.
+
+[Official setup](https://88api.ai/zh/docs/apps/) · [Image API](https://88api.ai/zh/docs/api/image/openai-image/) · [Audio API and timestamps](https://88api.ai/zh/docs/api/audio/openai-audio/)
+
+Required content is sent to the selected service and billed to your platform account. Keep API keys out of screenshots and public reports.
+
+[Back to README](../README-EN.md)
diff --git a/docs/88API_SETUP.md b/docs/88API_SETUP.md
new file mode 100644
index 00000000..4cfa062f
--- /dev/null
+++ b/docs/88API_SETUP.md
@@ -0,0 +1,59 @@
+# 在 AutoClip 中使用 88API
+
+[English](88API_SETUP.en.md) · [返回 README](../README.md)
+
+88API Token聚合平台是 AutoClip 的赞助合作伙伴,可通过 OpenAI 兼容接口用于字幕分析、封面生图和字幕转写。
+
+## 注册与获取 API Key
+
+通过 [AutoClip 专属推广链接](https://88api.ai/sign-up?aff=2PIc)注册,在控制台创建 API Key。新用户注册赠送体验额度,具体金额、领取条件与服务条款以平台页面为准;站内提供人工客服。推广链接含分成,用于支持项目维护。
+
+## 配置 AutoClip
+
+### 新版本:直接选择 88API
+
+1. 打开「设置 → AI 模型 → AI 服务」,在「推荐」分组选择 **88API**;首次使用也可在首页配置。
+2. 填入 88API Key。接口地址已预设为 `https://88api.ai/v1`,无需手动填写。
+3. 等待账号模型列表加载,选择用于字幕分析的模型;需要画面识别时,选择支持图片输入的多模态模型。
+4. 点击「测试连接」,然后保存。先用一小段素材验证实际分析效果。
+
+不预设默认文本模型或账号可用名单,准确模型 ID 与访问权限以账号返回的列表为准。无 Key 时显示的图片、语音目录只是参考,不代表账号权限。
+
+- **封面**:可复用 AI 服务的 Key,选择支持 OpenAI Images 接口的型号,例如账号列表中的 `gpt-image-1`、`dall-e-3`。其他图片模型是否可用取决于实际接口及返回的能力信息。
+- **字幕转写**:选择 88API 和账号可用的 `whisper-1`,通过 OpenAI 音频接口获取带时间戳字幕。
+- 平台也提供视频生成和 TTS;这些平台能力不等于 AutoClip 已提供对应功能。
+
+### 旧版本:通过 OpenAI 兼容接口接入
+
+如果提供商列表还没有 88API,选择「自定义 OpenAI 兼容接口」,填写以下内容:
+
+| 配置项 | 内容 |
+| --- | --- |
+| 提供商 | 自定义 OpenAI 兼容接口 |
+| 接口地址(Base URL) | `https://88api.ai/v1` |
+| API Key | 在 88API 控制台创建的 API Key |
+| 模型 | 当前账号可用、支持 Chat Completions 的准确模型 ID |
+
+地址中的 `/v1` 只填写一次,不要追加 `/chat/completions`。选择模型后点击「测试连接」,然后保存。
+
+## CLI / MCP
+
+```bash
+export API88_API_KEY="你的 API Key"
+autoclip run video.mp4 --provider api88 --model "账号可用的模型 ID"
+```
+
+也支持 `API_API88_API_KEY` 或 `--api-key`。MCP 使用 `provider="api88"`。
+
+## 排查与参考
+
+- 401:检查 Key、令牌状态、额度及前后空格。
+- 404:检查接口地址和模型 ID,确认模型支持所选接口。
+- 模型列表为空或获取失败:检查账号权限,刷新列表或输入控制台中的准确模型 ID。
+- 测试连接成功后仍应验证一次实际分析、封面或转写;不同接口可能有不同权限。
+
+[官方接入指南](https://88api.ai/zh/docs/apps/) · [图像接口](https://88api.ai/zh/docs/api/image/openai-image/) · [音频与时间戳格式](https://88api.ai/zh/docs/api/audio/openai-audio/)
+
+分析内容会发送至所选服务商,调用费用由该平台账户结算。不要将 API Key 放进截图或公开反馈。
+
+[返回 README](../README.md)
diff --git a/docs/sponsors/88api-logo.jpg b/docs/sponsors/88api-logo.jpg
new file mode 100644
index 00000000..edf82eb1
Binary files /dev/null and b/docs/sponsors/88api-logo.jpg differ
diff --git a/docs/sponsors/README.md b/docs/sponsors/README.md
index 01b6092f..84c349df 100644
--- a/docs/sponsors/README.md
+++ b/docs/sponsors/README.md
@@ -4,3 +4,5 @@
- `infistar-banner.png`:合作方横幅,由项目维护者在本次合作中提供。保留原图,当前 README 使用上方 Logo。
八语 README 统一使用 AutoClip 专属推广码 `XLK3BCM6`。品牌名称与 Logo 归相应权利人所有。
+
+- `88api-logo.jpg`:项目维护者于 2026-09-30 提供的 88API 官方 Logo;原图保存,README 中以 88 像素宽展示。
diff --git a/env.example b/env.example
index d5cb5731..2ec0689f 100644
--- a/env.example
+++ b/env.example
@@ -24,6 +24,7 @@ API_DASHSCOPE_API_KEY=your_dashscope_api_key_here
# API_GLM_API_KEY=
# API_GROK_API_KEY=
# API_INFISTAR_API_KEY=
+# API_API88_API_KEY= # or API88_API_KEY
# OpenAI 兼容接口地址(LLM_PROVIDER=openai 时生效,留空走 OpenAI 官方)。
# 智谱: https://open.bigmodel.cn/api/paas/v4 DeepSeek: https://api.deepseek.com/v1
# Seed / 火山方舟: https://ark.cn-beijing.volces.com/api/v3
diff --git a/frontend/src/analytics/events.ts b/frontend/src/analytics/events.ts
index fa2a714b..7947a849 100644
--- a/frontend/src/analytics/events.ts
+++ b/frontend/src/analytics/events.ts
@@ -5,6 +5,7 @@
* 所有 capture 都通过 trackEvent,未初始化 / 已关闭时自动 no-op。
*/
import { captureBusinessEvent } from './posthog'
+import type { SponsorId } from '../features/settings/providers'
export const AnalyticsEvent = {
/** 导入素材(上传/选择视频开始一个项目) */
@@ -60,7 +61,7 @@ export function trackProcessingFailed(props: {
}
export function trackSponsorLinkOpened(props: {
- sponsor: 'infistar'
+ sponsor: SponsorId
target: 'register' | 'guide'
placement: 'settings_model' | 'home_setup'
}): void {
diff --git a/frontend/src/analytics/experience.ts b/frontend/src/analytics/experience.ts
index 4a18cfd8..0d1fc862 100644
--- a/frontend/src/analytics/experience.ts
+++ b/frontend/src/analytics/experience.ts
@@ -11,7 +11,7 @@ const enums: Record = {
trigger: ['initial', 'import_blocked', 'auto', 'manual', 'provider_change', 'load', 'edit', 'portrait_preset'],
section: ['ai', 'publish', 'app', 'feedback'], role: ['analysis', 'vision', 'cover', 'transcription'],
reason: ['setup_required', 'provider', 'model', 'key'], mode: ['initial', 'update'],
- provider: ['dashscope', 'openai', 'gemini', 'deepseek', 'seed', 'kimi', 'glm', 'grok', 'infistar', 'compatible', 'ollama', 'lmstudio', 'other'],
+ provider: ['dashscope', 'openai', 'gemini', 'deepseek', 'seed', 'kimi', 'glm', 'grok', 'infistar', 'api88', 'compatible', 'ollama', 'lmstudio', 'other'],
source: ['live', 'cache', 'catalog'], outcome: ['completed', 'failed', 'blocked', 'unknown'],
resolution: ['created', 'reused'], material_origin: ['sample', 'user', 'unknown'],
analysis_mode: ['auto', 'subtitle', 'visual'], transcription_mode: ['whisper_local', 'cloud', 'unknown'],
diff --git a/frontend/src/features/settings/AIModelSettings.tsx b/frontend/src/features/settings/AIModelSettings.tsx
index f06a6fa3..96f635da 100644
--- a/frontend/src/features/settings/AIModelSettings.tsx
+++ b/frontend/src/features/settings/AIModelSettings.tsx
@@ -53,7 +53,7 @@ function TranscriptionSection({ m }: { m: ModelSettingsStore }) {
const main = mainConnection(settings)
const options = [
{ label: t('本机运行'), options: [{ value: 'whisper_local', label: t('Whisper · 本地') }] },
- ...providerPickerOptions().map(group => ({ ...group, options: group.options.filter(p => ['openai', 'dashscope', 'infistar', 'glm', 'compatible'].includes(p.value)) })).filter(group => group.options.length),
+ ...providerPickerOptions().map(group => ({ ...group, options: group.options.filter(p => ['openai', 'dashscope', 'infistar', 'api88', 'glm', 'compatible'].includes(p.value)) })).filter(group => group.options.length),
]
return
{t('字幕转写')}
@@ -61,7 +61,7 @@ function TranscriptionSection({ m }: { m: ModelSettingsStore }) {
value === 'whisper_local' ? m.setTranscriptionLocal(settings.transcription?.model && settings.transcription.provider === 'whisper_local' ? settings.transcription.model : 'base') : m.chooseProvider('transcription', value as ProviderKey)}
- optionRender={option => {option.value === 'whisper_local' ? t('Whisper · 本地') : PROVIDERS[option.value as ProviderKey]?.name}{option.value === 'infistar' && {t('赞助')} · {t('$5 免费体验额度')} } } />
+ optionRender={option => {option.value === 'whisper_local' ? t('Whisper · 本地') : PROVIDERS[option.value as ProviderKey]?.name}{PROVIDERS[option.value as ProviderKey]?.sponsor && {t('赞助')} · {PROVIDERS[option.value as ProviderKey]?.sponsor?.offer} } } />
{cloud && connection ? <>
{ const p = PROVIDERS[option.value as ProviderKey]; return {p.name}{p.sponsor && {t('赞助')} · {t('$5 免费体验额度')} } }} />
+ optionRender={option => { const p = PROVIDERS[option.value as ProviderKey]; return {p.name}{p.sponsor && {t('赞助')} · {p.sponsor.offer} } }} />
if (!connection) return <>{picker}>
return <>
{picker}
{preset?.sponsor &&
{preset.name}{t('赞助')}
- {t('通过专属链接注册,可领取 $5 免费体验额度。')}
+ {preset.sponsor.description}
{t('该链接含推广分成,用于支持项目维护;领取条件以活动页面为准。')}
- { trackSponsorLinkOpened({ sponsor: 'infistar', target: 'register', placement }); openExternalLink(preset.sponsor!.registerUrl) }}>{t('注册并领取体验额度')}
- { trackSponsorLinkOpened({ sponsor: 'infistar', target: 'guide', placement }); openExternalLink(preset.sponsor!.guideUrl) }}>{t('接入说明')}
+ { trackSponsorLinkOpened({ sponsor: preset.sponsor!.id, target: 'register', placement }); openExternalLink(preset.sponsor!.registerUrl) }}>{t('注册并领取体验额度')}
+ { trackSponsorLinkOpened({ sponsor: preset.sponsor!.id, target: 'guide', placement }); openExternalLink(preset.sponsor!.guideUrl) }}>{t('接入说明')}
}
{!preset?.local && (sharedKey && !editShared
diff --git a/frontend/src/features/settings/providers.ts b/frontend/src/features/settings/providers.ts
index 07f0ffb0..2c9e14d0 100644
--- a/frontend/src/features/settings/providers.ts
+++ b/frontend/src/features/settings/providers.ts
@@ -1,9 +1,10 @@
import { t } from '../../i18n'
-export type ProviderKey = 'dashscope' | 'openai' | 'compatible' | 'infistar' | 'gemini' | 'deepseek' | 'seed' | 'kimi' | 'glm' | 'grok' | 'ollama' | 'lmstudio'
+export type ProviderKey = 'dashscope' | 'openai' | 'compatible' | 'infistar' | 'api88' | 'gemini' | 'deepseek' | 'seed' | 'kimi' | 'glm' | 'grok' | 'ollama' | 'lmstudio'
type LocalPreset = { baseUrl: string; defaultModel: string; docsUrl: string; app: string }
type CloudPreset = { baseUrl: string; defaultModel: string }
-type Sponsor = { registerUrl: string; guideUrl: string }
+export type SponsorId = 'infistar' | 'api88'
+type Sponsor = { id: SponsorId; registerUrl: string; guideUrl: string; offer: string; description: string }
// 提供商会越来越多:下拉按组展示、可搜索,新增一家只在 PROVIDERS 里加一条并标 group
type ProviderGroup = 'sponsor' | 'cloud' | 'compatible' | 'local'
const PROVIDER_GROUPS: Array<{ key: ProviderGroup; label: () => string }> = [
@@ -17,7 +18,8 @@ export const PROVIDERS: Record {
+ if (id in mocks) return mocks[id]
+ if (id === 'react/jsx-runtime') return require(id)
+ throw Error(id)
+ } })
+ return exports
+}
+const providers = load('providers.ts', { '../../i18n': { t: text => text } })
+const plain = value => JSON.parse(JSON.stringify(value))
+function flatten(node) {
+ if (!React.isValidElement(node)) return [node]
+ return [node, ...React.Children.toArray(node.props.children).flatMap(flatten)]
+}
+
+test('sponsor selection, offer, registration link and attribution follow the chosen provider', () => {
+ const events = [], links = []
+ const Fields = load('ProviderFields.tsx', {
+ react: { useState: () => [false, () => {}] },
+ antd: { Input: { Password: 'password' }, Select: 'select' },
+ '../../i18n': { t: text => text }, '../../ui': { Btn: 'button', Row: 'row' },
+ '../../utils/externalLinks': { openExternalLink: url => links.push(url) },
+ '../../analytics/events': { trackSponsorLinkOpened: event => events.push(plain(event)) },
+ './providers': providers, './modelSettingsLogic': { presetKey: c => c.provider },
+ }).default
+ const options = providers.providerPickerOptions()
+ assert.deepEqual(Array.from(options[0].options, p => p.value), ['api88', 'infistar'])
+ for (const placement of ['home_setup', 'settings_model']) {
+ for (const provider of ['api88', 'infistar']) {
+ const preset = providers.PROVIDERS[provider]
+ const nodes = flatten(Fields({ connection: { provider, api_key: '' }, placement, onChoose() {}, onEdit() {} }))
+ const text = nodes.filter(n => typeof n === 'string').join(' ')
+ assert.ok(text.includes(preset.sponsor.description))
+ assert.equal(text.includes('$5'), provider === 'infistar')
+ const buttons = nodes.filter(n => n?.type === 'button')
+ buttons[0].props.onClick()
+ assert.equal(links.at(-1), preset.sponsor.registerUrl)
+ assert.deepEqual(events.at(-1), { sponsor: provider, target: 'register', placement })
+ buttons[1].props.onClick()
+ assert.equal(links.at(-1), preset.sponsor.guideUrl)
+ assert.deepEqual(events.at(-1), { sponsor: provider, target: 'guide', placement })
+ }
+ }
+ assert.equal(providers.PROVIDERS.api88.sponsor.registerUrl, 'https://88api.ai/sign-up?aff=2PIc')
+})
diff --git a/scripts/check_readmes.py b/scripts/check_readmes.py
index 623753fa..0d963f55 100644
--- a/scripts/check_readmes.py
+++ b/scripts/check_readmes.py
@@ -18,6 +18,7 @@ GUIDES = (
"docs/FAQ.md", "docs/FAQ.en.md", "docs/images/README.md",
"CONTRIBUTING.md", "docs/MULTI_LLM_PROVIDER_GUIDE.md",
"docs/INFISTAR_SETUP.md", "docs/INFISTAR_SETUP.en.md",
+ "docs/88API_SETUP.md", "docs/88API_SETUP.en.md",
)
FENCES = re.compile(r"^```[^\n]*\n(.*?)^```\s*$", re.MULTILINE | re.DOTALL)
LINKS = re.compile(r'\]\(([^\s)]+)\)|(?:href|src)="([^"]+)"')