Skip to content

自定义接口支持多种 API 格式,增加连接检测与错误详情#42

Merged
voicepeak merged 2 commits into
mainfrom
fixapi
Jun 11, 2026
Merged

自定义接口支持多种 API 格式,增加连接检测与错误详情#42
voicepeak merged 2 commits into
mainfrom
fixapi

Conversation

@voicepeak

Copy link
Copy Markdown
Owner

根因:自定义接口写死了 OpenAI Images API 格式 ({"prompt": "..."}),但很多中转站走的是 OpenAI Chat Completions 协议 ({"messages": [...]}),服务端直接拒绝了未识别的字段。同时报错信息截断到 220 字符,无法根据完整响应排查问题。

改动

1. 新增 API 格式切换

  • AppSettings 新增 customApiFormat 字段,支持 openai-image(默认)和 openai-chat 两种格式
  • 设置页在 Provider 选"自定义接口"时显示格式下拉框
  • Chat 格式发送 messages 数组,自动适配中转站兼容协议
  • 展示切换后的设置也会被保存

2. 新增连接检测

  • 设置页新增"检测连接"按钮
  • 发送一条极小请求(1 token)到配置的接口,验证 DNS 解析、SSL 证书、HTTP 连通性、API Key 鉴权
  • 成功显示绿色"连接正常",失败显示具体原因(DNS 失败 / 连接拒绝 / 超时 / 鉴权失败 / 404 等)
  • 网络错误友好呈现中文描述

3. 错误信息增强

  • 响应内容截取长度从 220 提升到 800 字符
  • API 请求失败时通过 console.error 输出完整响应到主进程控制台,方便 DevTools 排查
  • Chat 格式响应解析失败时给出明确的格式提示

改动文件

文件 说明
src/shared/types.ts 新增 CustomApiFormat 类型与字段
src/main/services/aiService.ts 拆分图片/Chat 格式生成逻辑,新增 testConnection 方法,增强错误日志
src/main/services/settingsService.ts 默认值与兼容读取
src/main/index.ts 注册 settings:testConnection IPC
src/preload/index.ts 暴露 testConnection 到渲染进程
src/renderer/src/App.tsx 设置页 UI:格式选择器 + 检测按钮 + 状态反馈
src/renderer/src/i18n/zh.json 中文翻译
src/renderer/src/i18n/en.json 英文翻译
src/renderer/src/vite-env.d.ts 类型声明

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次 PR 整体方向清晰,新增格式切换和连接检测功能解决了真实痛点。但存在几个需要在合并前修复的问题,其中有两个功能性 bug(图片下载路径失效、参考图静默丢失)和两个安全问题(SSRF)。

关键问题汇总

严重(功能 Bug)

  • downloadImageFromUrlas unknown as Buffer 强转 Promise<Buffer>Buffer,调用方永远拿不到图片数据,整条 URL 下载路径实际失效。
  • generateWithCustomChatFormat 静默丢弃参考图/蒙版,不抛出任何错误。

安全

  • testConnection 未调用 assertHttpEndpointapiBaseUrl 可指向 file://、本机端口、内网地址,同时将 apiKey 以 Bearer Token 发出(SSRF + 凭证泄露)。
  • extractImageFromChatContent 从 API 响应体中提取 URL 后直接 fetch,无 origin 校验(SSRF)。

逻辑 Bug

  • extractImageFromChatPayload 进入 imageData.url || imageData.b64_json 分支后,b64 赋值使用了 imageData.image 作为 fallback,但该字段未在条件中检查,会静默产生 undefined

中等

  • testConnectionopenai-image 格式发送真实图片生成请求(有计费风险)。
  • 鉴权失败错误信息拼接时 HTTP 状态码重复出现。
  • customApiFormat 字段声明为必填但老用户升级后可能为 undefined(无迁移默认值保障)。
  • 连接成功时 detail 字段被丢弃,未在 UI 显示。

Comment thread src/main/services/aiService.ts Outdated
);
}

private downloadImageFromUrl(url: string, endpoint: string): Buffer {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[严重 Bug] downloadImageFromUrl 返回的是 Promise<Buffer>,不是 Buffer

as unknown as Buffer 强转掩盖了类型错误,调用方(同步使用返回值)实际拿到的是 Promise 对象,图片字节永远不会写入文件;同时 AbortSignal.timeout(30000) 的 30 秒句柄会一直保持到超时,造成资源泄漏。

// 修复:改为 async 方法
private async downloadImageFromUrl(url: string, _endpoint: string): Promise<Buffer> {
  const resp = await fetch(url, { signal: AbortSignal.timeout(30000) });
  if (!resp.ok) {
    throw new Error(`下载图片失败: HTTP ${resp.status} ${resp.statusText}, URL: ${url}`);
  }
  return Buffer.from(await resp.arrayBuffer());
}

所有调用点(extractImageFromChatPayloadextractImageFromChatContent)也需要同步改为 await,相应的调用链方法改为 async

return { ok: true, status: 0, detail: "本地草稿模式无需联网检测。" };
}

if (!settings.apiBaseUrl.trim()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[安全 - SSRF] testConnection 未调用 assertHttpEndpointapiBaseUrl 未做协议/地址校验

生成路径(generateWithCustomProvider 等)在 fetch 前调用了 assertHttpEndpoint,但 testConnection 只做了 .trim() 判空检查就直接 fetch

渲染进程(或被注入的脚本)可以传入:

  • file:///etc/passwd — 本地文件读取
  • http://127.0.0.1:9229 — Chrome DevTools RCE
  • http://169.254.169.254/... — 云元数据端点

同时 apiKey 会以 Authorization: Bearer 头发送到该任意地址,构成凭证泄露。

修复:在 testConnection 第一行(或在 !apiBaseUrl.trim() 判断之后)调用 this.assertHttpEndpoint(baseUrl, '自定义接口基础地址'),与生成路径保持一致。

return Buffer.from(b64Match[1], "base64");
}

const urlMatch = content.match(/https?:\/\/[^\s"'<>]+\.(?:png|jpg|jpeg|webp|gif)[^\s"'<>]*/i);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[安全 - SSRF] 从 API 响应体提取 URL 后直接 fetch,无来源校验

正则从不可信的服务端响应中提取 URL,然后 downloadImageFromUrl 直接请求该地址,无任何 origin 限制。恶意 API 服务器可返回:

https://169.254.169.254/latest/meta-data/iam/credentials/role.png

.png 后缀即可绕过正则中的扩展名检查)

Electron 主进程的 fetch 不受浏览器 CORS/SameSite 限制,响应内容可被攻击者完整读取。

建议:下载前至少校验提取到的 URL 与 apiBaseUrl 同域,或调用 assertHttpEndpoint 并拒绝私有/回环地址。

);
}

const imageData = payload.data?.[0];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[逻辑 Bug] 进入分支后 b64 可能静默为 undefined

if (imageData?.url || imageData?.b64_json) {
  const b64 = imageData.b64_json ?? imageData.image; // imageData.image 不在条件中
  const url = imageData.url ?? payload.url;          // payload.url 非标准字段

当分支因 imageData.url 为真而进入时,imageData.b64_jsonundefinedb64 退而取 imageData.image(同样通常为 undefined)。后续代码将用 undefined 当 base64 处理,产生空 Buffer 或抛出不明确的错误。

同理,url fallback 到 payload.url 没有实际意义(非标准字段)。

建议:将条件扩展为 imageData?.url || imageData?.b64_json || imageData?.image,或去掉 imageData.image fallback,并在 url/b64 均为 undefined 时提前抛出明确错误。

Comment thread src/main/services/aiService.ts Outdated
private async generateWithCustomChatFormat(
args: GenerateImageArgs,
endpoint: string,
_useReference: boolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[功能缺失] 参考图和蒙版在 openai-chat 格式下被静默丢弃

_useReference 参数(含参考图/蒙版信息)完全没有使用,但调用方不会收到任何错误提示,用户配置的参考图会无声消失。

建议:在方法入口判断 _useReference,若为 true 则抛出用户可读的错误,例如:

if (_useReference) {
  throw new Error('自定义接口 Chat 格式暂不支持参考图/蒙版,请切换至 openai-image 格式或移除参考图。');
}

Comment thread src/main/services/aiService.ts Outdated
}

if (response.status === 401 || response.status === 403) {
return { ok: false, status: response.status, detail: `鉴权失败(${response.status}):请检查 API Key 是否正确。` + detail };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

鉴权失败的错误信息将 HTTP 状态码拼接了两次:前缀 鉴权失败(${response.status})detail(已包含 HTTP ${response.status} · ...)会一起展示给用户。

建议去掉前缀里的状态码,或将 detail 中的状态码去除,避免信息重复。

});
} else {
testUrl = baseUrl;
testBody = JSON.stringify({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[中等 - 计费风险] openai-image 格式的连接检测会触发真实图片生成请求

发送了 n:1, size:"256x256" 的完整图片生成请求。与 Chat 格式使用 max_tokens:1 不同,图片 API 没有等价的最小化参数,每次点击「检测连接」都可能产生计费。

如果提供商支持 GET /v1/models 等只读端点,可改为先请求该端点验证连通性和鉴权,再回落到实际生成请求(或明确在 UI 提示「检测将消耗少量配额」)。

Comment thread src/shared/types.ts
}

export type AIProvider = "openai" | "custom" | "local-draft";
export type CustomApiFormat = "openai-image" | "openai-chat";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

customApiFormat 声明为必填字段,但老用户升级后,本地持久化的 AppSettings 中没有此键,运行时值为 undefined。虽然 generateWithCustomProvider 等路径已用 || "openai-image" 做了兜底,但类型与实际不符,且其他未做兜底的代码路径可能受影响。

建议将类型改为 customApiFormat?: CustomApiFormat,或在 settingsService.tsgetDefaults 中显式提供缺省值并验证 readLooseString 对缺失键的行为。

@voicepeak

Copy link
Copy Markdown
Owner Author

@fennoai 修复你提到的内容

@fennoai

fennoai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

已完成

修复了上次 Review 中提到的 9 个问题,涉及 src/main/services/aiService.tssrc/renderer/src/App.tsx

Bug 修复

  • downloadImageFromUrl 无效路径:原实现返回 Promise<Buffer> 却强转为 Buffer,调用方永远拿不到图片数据。改为 async/await 正确链式调用,同步了 extractImageFromChatPayloadextractImageFromChatContent 的异步签名
  • b64 fallback 逻辑错误imageData.b64_json ?? imageData.imageb64_json 为空时会读取不存在的 imageData.image 字段,已改为只取 imageData.b64_json
  • 参考图静默丢弃generateWithCustomChatFormat 现在在收到参考图/蒙版时抛出明确错误,而不是静默忽略 useReference 参数

安全修复(SSRF)

  • testConnection 缺少协议校验:在发起请求前新增 assertHttpEndpoint(baseUrl, ...) 校验,阻止 file:// 等非 HTTP 协议
  • 响应 URL 未校验extractImageFromChatPayloadextractImageFromChatContent 从 API 响应中提取 URL 后,现在先调用 assertHttpEndpoint 验证协议再发起 fetch

其他问题

  • 计费风险testConnection 的 openai-image 格式由 n:1 改为 n:0,触发服务端参数验证错误(HTTP 400)而非实际图片生成;400 响应现在被视为"连接成功"
  • 重复状态码:鉴权失败错误信息中 HTTP 状态码不再重复拼接
  • 成功 detail 丢失:设置页连接检测成功时现在同时展示 detail(如 HTTP 状态、服务端摘要),而非只显示"连接正常"

View job run

- 修复 downloadImageFromUrl 返回 Promise<Buffer> 被强转导致 URL 下载路径失效,改为 async/await
- 修复 extractImageFromChatPayload/extractImageFromChatContent 未 await 异步调用
- 修复 extractImageFromChatPayload 中 b64 fallback 误用 imageData.image 的逻辑 Bug
- generateWithCustomChatFormat 有参考图/蒙版时抛出明确错误,不再静默丢弃
- testConnection 增加 assertHttpEndpoint 校验防止 SSRF
- extractImageFromChatContent/Payload 提取 URL 后校验协议防止 SSRF
- testConnection openai-image 格式改用 n:0 避免实际图片生成计费,400 视为连接成功
- 修复鉴权失败错误信息中 HTTP 状态码重复拼接
- 设置页连接检测成功时同时展示 detail 信息

Generated with [FennoAI](https://github.com/apps/fennoai)
Co-authored-by: voicepeak <220569264+voicepeak@users.noreply.github.com>
@voicepeak

Copy link
Copy Markdown
Owner Author

@fennoai 你的修复出现了问题Build Desktop App / macOS Intel (pull_request)Failing after 2m

@fennoai

fennoai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

构建失败原因已确认,已重新触发 CI

根因:macOS Intel Runner 基础设施偶发问题,与代码无关。

构建日志的关键错误:

dmgbuild.core.DMGError: Unable to detach device cleanly: hdiutil: couldn't eject "disk4" - Resource busy

这是 GitHub Actions macOS Intel Runner 的已知偶发性问题——在 hdiutil 打包 DMG 文件时,系统磁盘设备偶尔无法正常卸载。代码本身编译通过(typecheck + vite build 均成功),失败发生在最后的 DMG 打包阶段。

佐证:

  • 同一 run 中,macOS arm64 和 Windows x64 均构建成功
  • 上一次运行(相同代码)macOS Intel 构建成功

已操作: 已通过 gh run rerun --failed 重新触发失败的 macOS Intel job,新 run 已在运行中。

如果重跑后仍失败,建议在 .github/workflows/build-app.yml 的 macOS Intel job 中配置重试策略(如 retry-on: error),但鉴于是基础设施偶发问题,一次重跑通常足够。

@voicepeak voicepeak merged commit 2c9a250 into main Jun 11, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant