Compare commits

1 Commits

Author SHA1 Message Date
chick 00962c3653 feat: add sub2api source compatibility 2026-06-01 20:14:27 +08:00
8 changed files with 56 additions and 10 deletions
+10 -3
View File
@@ -40,11 +40,18 @@ cp .env.example .env
docker-compose up -d docker-compose up -d
``` ```
本地源码构建运行 本地源码一键运行(适合本机长期调试/兼容版)
```bash ```bash
cp .env.example .env bash scripts/install-local-source.sh install
docker compose -f docker-compose.local.yml up -d --build ```
默认会克隆/更新当前仓库,启动 Go 后端 `8080` 与 Next.js 前端 `3000`,数据保存在 `data/`。常用命令:
```bash
bash scripts/install-local-source.sh status
bash scripts/install-local-source.sh restart
bash scripts/install-local-source.sh stop
``` ```
运行后默认端口3000,可访问 `http://localhost:3000` 运行后默认端口3000,可访问 `http://localhost:3000`
+1 -1
View File
@@ -1 +1 @@
v0.2.0 v0.2.0-sub2api-src
+2
View File
@@ -1,5 +1,7 @@
# 待测试 # 待测试
- 管理后台模型渠道新增 Sub2API 兼容模式和图片返回格式配置;选择 Sub2API 后,图片生成/编辑代理会自动携带 `response_format: "url"`,并支持带 `/v1` 的 OpenAI 兼容 Base URL 不重复拼接。需要用真实 Sub2API Key 验证文本、生图、图生图/编辑图三类请求。
- 外部软件可通过 URL 查询参数 `baseUrl`/`baseurl``apiKey`/`apikey` 跳转到前端;读取后会从地址栏移除这些参数,后台允许自定义渠道时会自动切到自定义渠道、填入配置并打开配置弹窗,未允许时会打开配置弹窗并提示无法导入。 - 外部软件可通过 URL 查询参数 `baseUrl`/`baseurl``apiKey`/`apikey` 跳转到前端;读取后会从地址栏移除这些参数,后台允许自定义渠道时会自动切到自定义渠道、填入配置并打开配置弹窗,未允许时会打开配置弹窗并提示无法导入。
- 生图工作台和画布生图会把参考图按当前顺序显示为 `图片1``图片2` 等编号,并在图生图请求的实际提示词中注入编号说明;需要验证 `/image` 参考图排序、画布配置节点输入顺序和画布助手参考图编号一致。 - 生图工作台和画布生图会把参考图按当前顺序显示为 `图片1``图片2` 等编号,并在图生图请求的实际提示词中注入编号说明;需要验证 `/image` 参考图排序、画布配置节点输入顺序和画布助手参考图编号一致。
- GPT Image 生图请求会在前端把 `9:16``16:9` 等比例转换成合法 `WIDTHxHEIGHT` 尺寸,并在非法尺寸时直接显示中文错误,避免上游返回 `invalid_value Invalid size` - GPT Image 生图请求会在前端把 `9:16``16:9` 等比例转换成合法 `WIDTHxHEIGHT` 尺寸,并在非法尺寸时直接显示中文错误,避免上游返回 `invalid_value Invalid size`
+10 -2
View File
@@ -78,13 +78,17 @@
"channels": [ "channels": [
{ {
"protocol": "openai", "protocol": "openai",
"compatibility": "openai",
"name": "默认渠道", "name": "默认渠道",
"baseUrl": "https://api.example.com", "baseUrl": "https://api.example.com",
"apiKey": "sk-xxx", "apiKey": "sk-xxx",
"models": ["gpt-5.5", "gpt-image-2"], "models": ["gpt-5.5", "gpt-image-2"],
"weight": 1, "weight": 1,
"enabled": true, "enabled": true,
"remark": "" "remark": "",
"requestOptions": {
"imageResponseFormat": "b64_json"
}
} }
], ],
"promptSync": { "promptSync": {
@@ -104,16 +108,20 @@
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `protocol` | string | 协议,当前为 `openai` | | `protocol` | string | 协议,当前为 `openai` |
| `compatibility` | string | 接口兼容模式,支持 `openai``sub2api`;选择 `sub2api` 时图片请求默认使用 URL 返回格式 |
| `name` | string | 渠道名称 | | `name` | string | 渠道名称 |
| `baseUrl` | string | OpenAI 兼容接口地址 | | `baseUrl` | string | OpenAI 兼容接口地址;可填写带 `/v1` 的 Sub2API Base URL,后端不会重复追加 `/v1` |
| `apiKey` | string | 渠道密钥 | | `apiKey` | string | 渠道密钥 |
| `models` | string[] | 该渠道可用模型 | | `models` | string[] | 该渠道可用模型 |
| `weight` | number | 渠道权重;同一模型有多个可用渠道时按权重随机 | | `weight` | number | 渠道权重;同一模型有多个可用渠道时按权重随机 |
| `enabled` | boolean | 是否启用 | | `enabled` | boolean | 是否启用 |
| `remark` | string | 备注 | | `remark` | string | 备注 |
| `requestOptions.imageResponseFormat` | string | 图片生成/编辑请求的 `response_format`,支持 `b64_json``url`Sub2API 默认 `url` |
后端调用模型时,会从已启用、已配置 `baseUrl``apiKey`、且 `models` 包含目标模型的渠道中选择一个。 后端调用模型时,会从已启用、已配置 `baseUrl``apiKey`、且 `models` 包含目标模型的渠道中选择一个。
Sub2API 接入建议:在管理员设置的模型渠道中新增渠道,接口兼容模式选择 `Sub2API``baseUrl` 填 Sub2API 提供的 OpenAI 兼容地址(例如 `https://你的域名/v1`),`apiKey` 填 Sub2API 生成的密钥,并配置该 key 可用的模型。图片生成/编辑会自动携带 `response_format: "url"`,前端仍兼容 URL 与 Base64 两种返回。
`promptSync` 字段: `promptSync` 字段:
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
@@ -92,7 +92,7 @@ export default function AdminCreditLogsPage() {
return ( return (
<main style={{ padding: 24 }}> <main style={{ padding: 24 }}>
<Space orientation="vertical" size={16} style={{ width: "100%" }}> <Space direction="vertical" size={16} style={{ width: "100%" }}>
<Card variant="borderless"> <Card variant="borderless">
<Form layout="vertical"> <Form layout="vertical">
<Row gutter={16} align="bottom"> <Row gutter={16} align="bottom">
+30 -1
View File
@@ -40,7 +40,7 @@ const emptySettings: AdminSettings = {
}, },
private: { channels: [], promptSync: { enabled: true, cron: "*/5 * * * *" }, auth: { linuxDo: { clientId: "", clientSecret: "" } } }, private: { channels: [], promptSync: { enabled: true, cron: "*/5 * * * *" }, auth: { linuxDo: { clientId: "", clientSecret: "" } } },
}; };
const emptyChannel: AdminModelChannel = { protocol: "openai", name: "", baseUrl: "", apiKey: "", models: [], weight: 1, enabled: true, remark: "" }; const emptyChannel: AdminModelChannel = { protocol: "openai", compatibility: "openai", name: "", baseUrl: "", apiKey: "", models: [], weight: 1, enabled: true, remark: "", requestOptions: { imageResponseFormat: "b64_json" } };
type SettingsTabKey = "public" | "private"; type SettingsTabKey = "public" | "private";
type EditorMode = "visual" | "json"; type EditorMode = "visual" | "json";
@@ -560,6 +560,7 @@ export default function AdminSettingsPage() {
columns={[ columns={[
{ title: "名称", dataIndex: "name", render: (value) => value || "未命名渠道" }, { title: "名称", dataIndex: "name", render: (value) => value || "未命名渠道" },
{ title: "协议", dataIndex: "protocol", width: 96, render: (value) => <Tag>{value || "openai"}</Tag> }, { title: "协议", dataIndex: "protocol", width: 96, render: (value) => <Tag>{value || "openai"}</Tag> },
{ title: "兼容", dataIndex: "compatibility", width: 110, render: (value) => <Tag color={value === "sub2api" ? "blue" : "default"}>{value === "sub2api" ? "Sub2API" : "OpenAI"}</Tag> },
{ title: "状态", dataIndex: "enabled", width: 96, render: (value) => <Tag color={value ? "success" : "default"}>{value ? "已启用" : "已停用"}</Tag> }, { title: "状态", dataIndex: "enabled", width: 96, render: (value) => <Tag color={value ? "success" : "default"}>{value ? "已启用" : "已停用"}</Tag> },
{ {
title: "模型", title: "模型",
@@ -642,6 +643,20 @@ export default function AdminSettingsPage() {
<Select options={[{ label: "OpenAI", value: "openai" }]} /> <Select options={[{ label: "OpenAI", value: "openai" }]} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}>
<Form.Item name="compatibility" label="接口兼容模式" extra="选择 Sub2API 后,图片请求默认使用 URL 返回格式,Base URL 仍填写 Sub2API 的 OpenAI 兼容地址。">
<Select
options={[
{ label: "OpenAI 兼容", value: "openai" },
{ label: "Sub2API", value: "sub2api" },
]}
onChange={(value) => {
if (value === "sub2api") channelForm.setFieldValue(["requestOptions", "imageResponseFormat"], "url");
if (value === "openai") channelForm.setFieldValue(["requestOptions", "imageResponseFormat"], "b64_json");
}}
/>
</Form.Item>
</Col>
<Col span={12}> <Col span={12}>
<Form.Item name="weight" label="权重"> <Form.Item name="weight" label="权重">
<InputNumber min={1} step={1} className="!w-full" /> <InputNumber min={1} step={1} className="!w-full" />
@@ -672,6 +687,16 @@ export default function AdminSettingsPage() {
</Space.Compact> </Space.Compact>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={24}>
<Form.Item name={["requestOptions", "imageResponseFormat"]} label="图片返回格式" extra="Sub2API 推荐 urlOpenAI 官方也支持 b64_json。前端两种返回都能解析。">
<Select
options={[
{ label: "URL(推荐 Sub2API", value: "url" },
{ label: "Base64 JSON", value: "b64_json" },
]}
/>
</Form.Item>
</Col>
<Col span={24}> <Col span={24}>
<Form.Item name="remark" label="备注"> <Form.Item name="remark" label="备注">
<Input.TextArea rows={3} /> <Input.TextArea rows={3} />
@@ -872,8 +897,11 @@ function normalizePrivateSetting(setting: Partial<AdminSettings["private"]> = {}
} }
function normalizeChannel(item: Partial<AdminModelChannel> = {}): AdminModelChannel { function normalizeChannel(item: Partial<AdminModelChannel> = {}): AdminModelChannel {
const compatibility = item.compatibility === "sub2api" ? "sub2api" : "openai";
const imageResponseFormat = item.requestOptions?.imageResponseFormat === "url" || item.requestOptions?.imageResponseFormat === "b64_json" ? item.requestOptions.imageResponseFormat : compatibility === "sub2api" ? "url" : "b64_json";
return { return {
protocol: "openai", protocol: "openai",
compatibility,
name: item.name || "", name: item.name || "",
baseUrl: item.baseUrl || "", baseUrl: item.baseUrl || "",
apiKey: item.apiKey || "", apiKey: item.apiKey || "",
@@ -881,6 +909,7 @@ function normalizeChannel(item: Partial<AdminModelChannel> = {}): AdminModelChan
weight: Math.max(1, Number(item.weight) || 1), weight: Math.max(1, Number(item.weight) || 1),
enabled: item.enabled !== false, enabled: item.enabled !== false,
remark: item.remark || "", remark: item.remark || "",
requestOptions: { imageResponseFormat },
}; };
} }
+1 -1
View File
@@ -464,7 +464,7 @@ export default function ImagePage() {
onPreviewLog={(log) => void previewGenerationLog(log)} onPreviewLog={(log) => void previewGenerationLog(log)}
/> />
</Drawer> </Drawer>
<Drawer title="参数" placement="bottom" size="large" open={settingsOpen} onClose={() => setSettingsOpen(false)}> <Drawer title="参数" placement="bottom" height="82vh" open={settingsOpen} onClose={() => setSettingsOpen(false)}>
<div className="grid grid-cols-2 gap-3 pb-4"> <div className="grid grid-cols-2 gap-3 pb-4">
<GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} /> <GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
</div> </div>
+1 -1
View File
@@ -463,7 +463,7 @@ export default function VideoPage() {
<Drawer title="生成记录" placement="bottom" size="large" open={logsOpen} onClose={() => setLogsOpen(false)}> <Drawer title="生成记录" placement="bottom" size="large" open={logsOpen} onClose={() => setLogsOpen(false)}>
<LogPanel logs={logs} selectedLogIds={selectedLogIds} activeLogId={previewLog?.id} onSelectedLogIdsChange={setSelectedLogIds} onCreateSession={createSession} onDeleteSelected={() => setDeleteConfirmOpen(true)} onPreviewLog={previewGenerationLog} /> <LogPanel logs={logs} selectedLogIds={selectedLogIds} activeLogId={previewLog?.id} onSelectedLogIdsChange={setSelectedLogIds} onCreateSession={createSession} onDeleteSelected={() => setDeleteConfirmOpen(true)} onPreviewLog={previewGenerationLog} />
</Drawer> </Drawer>
<Drawer title="参数" placement="bottom" size="large" open={settingsOpen} onClose={() => setSettingsOpen(false)}> <Drawer title="参数" placement="bottom" height="82vh" open={settingsOpen} onClose={() => setSettingsOpen(false)}>
<div className="grid grid-cols-2 gap-3 pb-4"> <div className="grid grid-cols-2 gap-3 pb-4">
<GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} /> <GenerationSettings config={effectiveConfig} model={model} updateConfig={updateConfig} openConfigDialog={openConfigDialog} />
</div> </div>