From c19472d08785e8dafc0e52b3c8f44426e5e99857 Mon Sep 17 00:00:00 2001 From: Codex Local Date: Sat, 26 Sep 2026 15:15:16 +0800 Subject: [PATCH 01/11] feat: add 16688 AI image provider prototype --- .env.16688 | 1 + .gitignore | 1 + docs/PROVIDERS.md | 17 ++ messages/en.json | 12 +- messages/ja.json | 12 +- messages/zh.json | 12 +- package.json | 2 + src/components/app/product-page-shell.test.ts | 8 +- .../app/workspace-api-config-dialog.tsx | 27 +-- src/config/generation-providers.ts | 13 +- .../adapters/newapi-16688-adapter.test.ts | 73 +++++++ src/core/adapters/newapi-16688-adapter.ts | 197 ++++++++++++++++++ .../providers/provider-config.test.ts | 11 +- .../beatcanvas/providers/provider-config.ts | 41 ++-- src/core/effects/output-storage.test.ts | 13 ++ src/core/effects/output-storage.ts | 85 ++++++-- .../newapi-16688-provider.ts | 37 ++++ .../generation-providers/registry.test.ts | 13 +- src/modules/config/service.ts | 2 + src/routes/api/config/16688.ts | 67 ++++++ 20 files changed, 570 insertions(+), 74 deletions(-) create mode 100644 .env.16688 create mode 100644 src/core/adapters/newapi-16688-adapter.test.ts create mode 100644 src/core/adapters/newapi-16688-adapter.ts create mode 100644 src/core/generation-providers/newapi-16688-provider.ts create mode 100644 src/routes/api/config/16688.ts diff --git a/.env.16688 b/.env.16688 new file mode 100644 index 0000000..ca42d53 --- /dev/null +++ b/.env.16688 @@ -0,0 +1 @@ +VITE_GENERATION_PROVIDER_ID=16688 diff --git a/.gitignore b/.gitignore index a39e639..d45142b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ yarn-error.log* # local secret files .env .env.* +!.env.16688 # typescript *.tsbuildinfo diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index a391516..cdac24b 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -35,6 +35,23 @@ An API with a different request or polling contract needs its own adapter. Forks A custom provider definition owns adapter construction, readiness checks, parameter validation, model bindings, upstream model names, and upload paths. Provider credentials must stay server-side. Switching providers does not change Canvas nodes, Editor clips, Asset IDs, or MCP requests; each submitted task also records its provider/model identity so polling does not silently follow a later default-provider change. +## 16688 Studio fork prototype + +The `feat/16688-provider-poc` fork registers `16688 AI` as an opt-in provider. +Use the `16688` Vite mode (`pnpm dev:16688` or `pnpm build:16688`) to make it +active while leaving the upstream BeatAPI development and test mode intact. +The prototype calls the fixed `https://api.16688.org/v1/images/generations` +endpoint with a user-owned New API key stored encrypted in local SQLite. It +binds only `gpt-image-2`, `gpt-image-2.5-flare`, and +`gpt-image-2.5-sunburst`. + +This first stage is deliberately text-to-image only. It requests inline Base64 +output, validates the response size and MIME type, and persists the result into +the local Project before exposing it as an Asset. Reference-image editing, +video, hosted accounts, payments, and shared balances are not claimed by this +prototype. Those capabilities require separate provider contracts and server +work; they must not silently fall back to BeatAPI. + ## Storage Storage is independently configurable from generation: diff --git a/messages/en.json b/messages/en.json index 3ca8c74..2287028 100644 --- a/messages/en.json +++ b/messages/en.json @@ -669,24 +669,24 @@ }, "apiConfig": { "triggerLabel": "API configuration", - "triggerConfiguredLabel": "API configuration — BeatAPI connected", - "loadingConnection": "Loading BeatAPI connection", + "triggerConfiguredLabel": "API configuration — provider connected", + "loadingConnection": "Loading API connection", "title": "Connections", - "description": "Connect BeatAPI when you want to use remote generation models.", + "description": "Connect your 16688 AI API key to use remote generation models.", "providerTab": "AI Provider", "storageTab": "Storage", "activeProvider": "Active provider", "defaultBadge": "Default", - "providerDescription": "BeatAPI is the default generation backend for this open-source workspace.", + "providerDescription": "16688 AI is the active generation backend for this workspace.", "endpointLabel": "Endpoint", "mediaLabel": "Media", "mediaValue": "Image + video", "keyLabel": "API Key", - "keyPlaceholder": "Enter your BeatAPI API key", + "keyPlaceholder": "Enter your 16688 AI API key", "save": "Save settings", "saving": "Saving…", "saved": "API key saved", - "getKey": "Get a BeatAPI API key →", + "getKey": "Get a 16688 AI API key →", "storage": { "intro": "Use BeatAPI Files by default, or connect your own public bucket.", "managedTitle": "BeatAPI Files", diff --git a/messages/ja.json b/messages/ja.json index 6a05122..3bda84c 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -669,24 +669,24 @@ }, "apiConfig": { "triggerLabel": "API設定", - "triggerConfiguredLabel": "API設定 — BeatAPI接続済み", - "loadingConnection": "BeatAPI接続を読み込み中", + "triggerConfiguredLabel": "API設定 — プロバイダー接続済み", + "loadingConnection": "API接続を読み込み中", "title": "接続", - "description": "リモート生成モデルを使用するときにBeatAPI APIキーを接続します。", + "description": "リモート生成モデルを使用するため、16688 AI APIキーを接続します。", "providerTab": "AIプロバイダー", "storageTab": "ストレージ", "activeProvider": "使用中のプロバイダー", "defaultBadge": "デフォルト", - "providerDescription": "BeatAPIは、このオープンソースワークスペースのデフォルト生成バックエンドです。", + "providerDescription": "16688 AIは、このワークスペースで使用中の生成バックエンドです。", "endpointLabel": "エンドポイント", "mediaLabel": "メディア", "mediaValue": "画像 + 動画", "keyLabel": "API Key", - "keyPlaceholder": "BeatAPI APIキーを入力", + "keyPlaceholder": "16688 AI APIキーを入力", "save": "設定を保存", "saving": "保存中…", "saved": "APIキーを保存しました", - "getKey": "BeatAPI APIキーを取得 →", + "getKey": "16688 AI APIキーを取得 →", "storage": { "intro": "デフォルトのBeatAPI Filesを使うか、ご自身の公開バケットに接続します。", "managedTitle": "BeatAPI Files", diff --git a/messages/zh.json b/messages/zh.json index 1ae6210..0e87d3d 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -669,24 +669,24 @@ }, "apiConfig": { "triggerLabel": "API 配置", - "triggerConfiguredLabel": "API 配置 — BeatAPI 已连接", - "loadingConnection": "正在读取 BeatAPI 连接", + "triggerConfiguredLabel": "API 配置 — 服务已连接", + "loadingConnection": "正在读取 API 连接", "title": "连接配置", - "description": "需要使用远程生成模型时,连接你的 BeatAPI API Key。", + "description": "需要使用远程生成模型时,连接你的 16688 AI API 密钥。", "providerTab": "AI Provider", "storageTab": "存储", "activeProvider": "当前提供商", "defaultBadge": "默认", - "providerDescription": "BeatAPI 是这套开源工作台默认使用的生成后端。", + "providerDescription": "16688 AI 是当前工作台的默认生成后端。", "endpointLabel": "接口地址", "mediaLabel": "支持类型", "mediaValue": "图片 + 视频", "keyLabel": "API Key", - "keyPlaceholder": "请输入 BeatAPI API Key", + "keyPlaceholder": "请输入 16688 AI API 密钥", "save": "保存设置", "saving": "保存中…", "saved": "API Key 已保存", - "getKey": "获取 BeatAPI API Key →", + "getKey": "前往 16688 AI 获取 API 密钥 →", "storage": { "intro": "默认使用 BeatAPI Files,也可以接入你自己的公网存储桶。", "managedTitle": "BeatAPI Files", diff --git a/package.json b/package.json index 14d108e..16a7d5b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "type": "module", "scripts": { "dev": "tsx scripts/prepare-paraglide.ts && vite dev --host 127.0.0.1 --port 3020 --strictPort", + "dev:16688": "tsx scripts/prepare-paraglide.ts && vite dev --mode 16688 --host 127.0.0.1 --port 3020 --strictPort", "dev:agent": "node scripts/dev-with-mcp.mjs", "test": "node scripts/run-tests.mjs", "test:e2e": "playwright test", @@ -35,6 +36,7 @@ "mcp:http": "tsx scripts/mcp-server.ts --http", "prebuild": "node scripts/db-setup.mjs", "build": "tsx scripts/prepare-paraglide.ts && vite build && node scripts/sanitize-output.mjs", + "build:16688": "tsx scripts/prepare-paraglide.ts && vite build --mode 16688 && node scripts/sanitize-output.mjs", "start": "node .output/server/index.mjs", "postinstall": "mkdir -p data && (test -f src/config/db/schema.ts || node scripts/db-setup.mjs)", "db:setup": "node scripts/db-setup.mjs", diff --git a/src/components/app/product-page-shell.test.ts b/src/components/app/product-page-shell.test.ts index db30ca3..5948d81 100644 --- a/src/components/app/product-page-shell.test.ts +++ b/src/components/app/product-page-shell.test.ts @@ -46,15 +46,15 @@ test('workspace header keeps mode switching, shared assets, and API configuratio assert.doesNotMatch(shellSource, /getUserInitial/); }); -test('API configuration presets the BeatAPI endpoint and saves key + host', () => { +test('API configuration follows the active provider and saves storage settings', () => { assert.match(apiConfigSource, /getBeatCanvasProviderPublicConfig/); - assert.match(apiConfigSource, /DEFAULT_BEATAPI_BASE_URL/); - assert.match(apiConfigSource, /\/api\/config\/beatapi/); + assert.match(apiConfigSource, /provider\.configPath/); + assert.match(apiConfigSource, /provider\.baseUrl/); + assert.match(apiConfigSource, /provider\.apiKeyUrl/); assert.match(apiConfigSource, /\/api\/config\/storage/); assert.match(apiConfigSource, /'beatapi' \| 's3'/); assert.match(apiConfigSource, /apiGet\('\/api\/config\/storage'\)/); assert.match(apiConfigSource, /apiPost\('\/api\/config\/storage'/); - assert.match(apiConfigSource, /https:\/\/beatapi\.io\/dashboard\/apikeys/); assert.doesNotMatch(apiConfigSource, /apiJson(?:Get|Post).*\/api\/config\/storage/); }); diff --git a/src/components/app/workspace-api-config-dialog.tsx b/src/components/app/workspace-api-config-dialog.tsx index 038f00d..06df732 100644 --- a/src/components/app/workspace-api-config-dialog.tsx +++ b/src/components/app/workspace-api-config-dialog.tsx @@ -11,8 +11,8 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { - DEFAULT_BEATAPI_BASE_URL, getBeatCanvasProviderPublicConfig, + type BeatCanvasProviderPublicConfig, } from '@/core/beatcanvas/providers/provider-config'; import { useTranslations } from '@/core/workspace-lib/shims/next-intl'; import { apiGet, apiPost } from '@/lib/api-client'; @@ -48,7 +48,7 @@ const saveButtonClassName = const configuredKeyMask = '••••••••••••••••••••••••'; -type BeatApiConfigState = { +type ProviderConfigState = { baseUrl: string; apiKeyConfigured: boolean; apiKeyPreview: string; @@ -70,16 +70,18 @@ type StorageConfigState = { function ApiConfigForm({ providerLabel, + provider, isDefault, state, loaded, onSaved, }: { providerLabel: string; + provider: BeatCanvasProviderPublicConfig; isDefault: boolean; - state: BeatApiConfigState | null; + state: ProviderConfigState | null; loaded: boolean; - onSaved: (state: BeatApiConfigState) => void; + onSaved: (state: ProviderConfigState) => void; }) { const t = useTranslations('AppShell.header.apiConfig'); const [apiKey, setApiKey] = useState(''); @@ -93,14 +95,14 @@ function ApiConfigForm({ setSaving(true); try { const result = await apiPost<{ apiKeyPreview?: string }>( - '/api/config/beatapi', + provider.configPath, { apiKey: nextKey } ); toast.success(t('saved')); setApiKey(''); setEditing(false); onSaved({ - baseUrl: DEFAULT_BEATAPI_BASE_URL, + baseUrl: provider.baseUrl, apiKeyConfigured: true, apiKeyPreview: result?.apiKeyPreview || maskApiKeyPreview(nextKey), }); @@ -124,7 +126,7 @@ function ApiConfigForm({
+ {mediaGatewayEnabled ? ( +
+
+

+ {t('mediaGatewayTitle')} +

+

+ {t('mediaGatewayHint')} +

+
+ setMediaBaseUrl(event.target.value)} + placeholder={t('mediaGatewayUrlPlaceholder')} + aria-label={t('mediaGatewayUrlLabel')} + autoComplete="off" + className={inputClassName} + /> + setMediaApiKey(event.target.value)} + placeholder={ + mediaConfigured + ? t('configuredPlaceholder') + : t('mediaGatewayKeyPlaceholder') + } + aria-label={t('mediaGatewayKeyLabel')} + autoComplete="off" + className={inputClassName} + /> +
+ ) : null} +