diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0d7b6a..6e359c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## [未发布] ### 新增 +- 新增赞助合作伙伴 88API:八语 README、首页与设置页推荐入口、独立 API Key、模型发现、兼容封面与 Whisper 转写;CLI / MCP 使用 `api88`。 - 模型设置新增赞助合作伙伴「Infistar 无限星河」:接口地址已预设,填好 Key 后自动列出账号可用的模型;设置页提供专属注册链接(可领 $5 体验额度)和接入说明。CLI / MCP 支持 `--provider infistar` 与 `INFISTAR_API_KEY`。 - 设置 → AI 模型重新整理为「AI 服务 / 字幕转写 / 封面 / 高级」四段:选一家服务、填好 Key,分析模型自动选好;画面识别和 AI 封面首次配置默认开启,画面识别注明适合游戏画面、口播较少的内容。供应商分组中的赞助伙伴改为「推荐」。 - 首次打开应用会弹出「连接 AI 服务」引导,未连接时导入会提示先去连接;不再需要自己摸索设置页。 diff --git a/README-EN.md b/README-EN.md index fa8025a2..adb88d1b 100644 --- a/README-EN.md +++ b/README-EN.md @@ -48,6 +48,18 @@ AutoClip uses AI to analyze video transcripts, find highlights, write titles, an ## Special thanks ❤️ + + + +
+ 88API
+ 88API +
+ Thank you to 88API Token Platform for sponsoring AutoClip! It brings together GPT, Claude, Gemini, Grok, DeepSeek, Kimi and GLM for transcript analysis, highlight selection and title generation.
+ 🎨 Media capabilities: The platform offers image, video and audio models, including GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper and TTS. AutoClip uses compatible analysis, cover-image and transcription APIs.
+ 🏷️ Service and billing: The partner reports overseas corporate operation, live support, invoices and a 1:1 top-up ratio; platform terms apply.
+ 🎁 New-user offer: Receive trial credit to test models through our referral registration link, subject to promotion terms. Setup guide +
Infistar
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
+ 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) +
Infistar
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
+ 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) +
Infistar
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 +
+ 88API Tokenプラットフォーム のご支援に感謝します!GPT、Claude、Gemini、Grok、DeepSeek、Kimi、GLM を集約し、字幕分析・見どころ選定・タイトル生成に活用できます。
+ 🎨 マルチメディア:GPT-Image、Seedance、Veo、MiniMax Hailuo H3、Kling、Whisper、TTS などの画像・動画・音声モデルを提供。AutoClip は対応する分析・カバー画像生成・文字起こし API を利用できます。
+ 🏷️ サービスと決済:提携先によると海外法人が運営し、有人サポート・請求書・1:1 のチャージ比率を提供。条件はプラットフォームをご確認ください。
+ 🎁 新規登録特典:専用紹介リンクから登録するとモデル検証用の体験クレジットを受け取れます。適用条件はキャンペーンページをご確認ください。 設定ガイド(英語) +
Infistar
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 +
+ 88API Token 플랫폼의 AutoClip 후원에 감사드립니다! GPT, Claude, Gemini, Grok, DeepSeek, Kimi, GLM을 통합하여 자막 분석, 하이라이트 선택, 제목 생성에 활용할 수 있습니다.
+ 🎨 멀티미디어: GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper, TTS 등 이미지·영상·음성 모델을 제공합니다. AutoClip에서는 호환되는 분석·표지 생성·자막 전사 API를 사용합니다.
+ 🏷️ 서비스 및 결제: 파트너 안내에 따르면 해외 법인이 운영하며 상담원 지원, 정식 청구서, 1:1 충전 비율을 제공합니다. 세부 조건은 플랫폼을 확인하세요.
+ 🎁 신규 가입 혜택: 전용 추천 링크로 가입하면 모델 테스트용 체험 크레딧을 받을 수 있습니다. 이벤트 조건이 적용됩니다. 설정 가이드 (영어) +
Infistar
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
+ 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) +
Infistar
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 +
+ Благодарим 88API за поддержку AutoClip! Платформа объединяет GPT, Claude, Gemini, Grok, DeepSeek, Kimi и GLM для анализа субтитров, выбора ярких моментов и создания заголовков.
+ 🎨 Мультимедиа: Модели изображений, видео и аудио, включая GPT-Image, Seedance, Veo, MiniMax Hailuo H3, Kling, Whisper и TTS. AutoClip использует совместимые API анализа, создания обложек и транскрипции.
+ 🏷️ Сервис и оплата: По информации партнёра, сервис управляется зарубежной компанией и предлагает поддержку операторов, счета и пополнение в соотношении 1:1; действуют условия платформы.
+ 🎁 Новым пользователям: Получите пробный кредит для проверки моделей по реферальной ссылке на условиях акции. Инструкция по настройке (англ.) +
Infistar
diff --git a/README.md b/README.md index 5fbd2a00..abf64a9b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,18 @@ ## 特别感谢 ❤️ + + + +
+ 88API
+ 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;具体模型、服务与结算条件以平台页面为准。
+ 🎁 新用户福利:通过 专属推广链接注册可获体验额度,用于测试模型能力,领取条件以活动页面为准。 接入说明 +
Infistar
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 }) { { 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)="([^"]+)"')