Conversation
There was a problem hiding this comment.
本次 PR 整体方向清晰,新增格式切换和连接检测功能解决了真实痛点。但存在几个需要在合并前修复的问题,其中有两个功能性 bug(图片下载路径失效、参考图静默丢失)和两个安全问题(SSRF)。
关键问题汇总
严重(功能 Bug)
downloadImageFromUrl以as unknown as Buffer强转Promise<Buffer>为Buffer,调用方永远拿不到图片数据,整条 URL 下载路径实际失效。generateWithCustomChatFormat静默丢弃参考图/蒙版,不抛出任何错误。
安全
testConnection未调用assertHttpEndpoint,apiBaseUrl可指向file://、本机端口、内网地址,同时将apiKey以 Bearer Token 发出(SSRF + 凭证泄露)。extractImageFromChatContent从 API 响应体中提取 URL 后直接fetch,无 origin 校验(SSRF)。
逻辑 Bug
extractImageFromChatPayload进入imageData.url || imageData.b64_json分支后,b64赋值使用了imageData.image作为 fallback,但该字段未在条件中检查,会静默产生undefined。
中等
testConnection对openai-image格式发送真实图片生成请求(有计费风险)。- 鉴权失败错误信息拼接时 HTTP 状态码重复出现。
customApiFormat字段声明为必填但老用户升级后可能为undefined(无迁移默认值保障)。- 连接成功时
detail字段被丢弃,未在 UI 显示。
| ); | ||
| } | ||
|
|
||
| private downloadImageFromUrl(url: string, endpoint: string): Buffer { |
There was a problem hiding this comment.
[严重 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());
}所有调用点(extractImageFromChatPayload、extractImageFromChatContent)也需要同步改为 await,相应的调用链方法改为 async。
| return { ok: true, status: 0, detail: "本地草稿模式无需联网检测。" }; | ||
| } | ||
|
|
||
| if (!settings.apiBaseUrl.trim()) { |
There was a problem hiding this comment.
[安全 - SSRF] testConnection 未调用 assertHttpEndpoint,apiBaseUrl 未做协议/地址校验
生成路径(generateWithCustomProvider 等)在 fetch 前调用了 assertHttpEndpoint,但 testConnection 只做了 .trim() 判空检查就直接 fetch。
渲染进程(或被注入的脚本)可以传入:
file:///etc/passwd— 本地文件读取http://127.0.0.1:9229— Chrome DevTools RCEhttp://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); |
There was a problem hiding this comment.
[安全 - 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]; |
There was a problem hiding this comment.
[逻辑 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_json 为 undefined,b64 退而取 imageData.image(同样通常为 undefined)。后续代码将用 undefined 当 base64 处理,产生空 Buffer 或抛出不明确的错误。
同理,url fallback 到 payload.url 没有实际意义(非标准字段)。
建议:将条件扩展为 imageData?.url || imageData?.b64_json || imageData?.image,或去掉 imageData.image fallback,并在 url/b64 均为 undefined 时提前抛出明确错误。
| private async generateWithCustomChatFormat( | ||
| args: GenerateImageArgs, | ||
| endpoint: string, | ||
| _useReference: boolean |
There was a problem hiding this comment.
[功能缺失] 参考图和蒙版在 openai-chat 格式下被静默丢弃
_useReference 参数(含参考图/蒙版信息)完全没有使用,但调用方不会收到任何错误提示,用户配置的参考图会无声消失。
建议:在方法入口判断 _useReference,若为 true 则抛出用户可读的错误,例如:
if (_useReference) {
throw new Error('自定义接口 Chat 格式暂不支持参考图/蒙版,请切换至 openai-image 格式或移除参考图。');
}| } | ||
|
|
||
| if (response.status === 401 || response.status === 403) { | ||
| return { ok: false, status: response.status, detail: `鉴权失败(${response.status}):请检查 API Key 是否正确。` + detail }; |
There was a problem hiding this comment.
鉴权失败的错误信息将 HTTP 状态码拼接了两次:前缀 鉴权失败(${response.status}) 和 detail(已包含 HTTP ${response.status} · ...)会一起展示给用户。
建议去掉前缀里的状态码,或将 detail 中的状态码去除,避免信息重复。
| }); | ||
| } else { | ||
| testUrl = baseUrl; | ||
| testBody = JSON.stringify({ |
There was a problem hiding this comment.
[中等 - 计费风险] openai-image 格式的连接检测会触发真实图片生成请求
发送了 n:1, size:"256x256" 的完整图片生成请求。与 Chat 格式使用 max_tokens:1 不同,图片 API 没有等价的最小化参数,每次点击「检测连接」都可能产生计费。
如果提供商支持 GET /v1/models 等只读端点,可改为先请求该端点验证连通性和鉴权,再回落到实际生成请求(或明确在 UI 提示「检测将消耗少量配额」)。
| } | ||
|
|
||
| export type AIProvider = "openai" | "custom" | "local-draft"; | ||
| export type CustomApiFormat = "openai-image" | "openai-chat"; |
There was a problem hiding this comment.
customApiFormat 声明为必填字段,但老用户升级后,本地持久化的 AppSettings 中没有此键,运行时值为 undefined。虽然 generateWithCustomProvider 等路径已用 || "openai-image" 做了兜底,但类型与实际不符,且其他未做兜底的代码路径可能受影响。
建议将类型改为 customApiFormat?: CustomApiFormat,或在 settingsService.ts 的 getDefaults 中显式提供缺省值并验证 readLooseString 对缺失键的行为。
|
@fennoai 修复你提到的内容 |
已完成修复了上次 Review 中提到的 9 个问题,涉及 Bug 修复
安全修复(SSRF)
其他问题
|
- 修复 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>
|
@fennoai 你的修复出现了问题Build Desktop App / macOS Intel (pull_request)Failing after 2m |
构建失败原因已确认,已重新触发 CI根因:macOS Intel Runner 基础设施偶发问题,与代码无关。 构建日志的关键错误: 这是 GitHub Actions macOS Intel Runner 的已知偶发性问题——在 佐证:
已操作: 已通过 如果重跑后仍失败,建议在 |
根因:自定义接口写死了 OpenAI Images API 格式 (
{"prompt": "..."}),但很多中转站走的是 OpenAI Chat Completions 协议 ({"messages": [...]}),服务端直接拒绝了未识别的字段。同时报错信息截断到 220 字符,无法根据完整响应排查问题。改动
1. 新增 API 格式切换
AppSettings新增customApiFormat字段,支持openai-image(默认)和openai-chat两种格式messages数组,自动适配中转站兼容协议2. 新增连接检测
3. 错误信息增强
console.error输出完整响应到主进程控制台,方便 DevTools 排查改动文件
src/shared/types.tsCustomApiFormat类型与字段src/main/services/aiService.tstestConnection方法,增强错误日志src/main/services/settingsService.tssrc/main/index.tssettings:testConnectionIPCsrc/preload/index.tstestConnection到渲染进程src/renderer/src/App.tsxsrc/renderer/src/i18n/zh.jsonsrc/renderer/src/i18n/en.jsonsrc/renderer/src/vite-env.d.ts