Skip to content

feat(deepseek): upload images via Files API with file_id references - #1113

Open
3316891527 wants to merge 7 commits into
AAswordman:devfrom
3316891527:feat/deepseek-files-api
Open

feat(deepseek): upload images via Files API with file_id references#1113
3316891527 wants to merge 7 commits into
AAswordman:devfrom
3316891527:feat/deepseek-files-api

Conversation

@3316891527

@3316891527 3316891527 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

变更说明 / Description

背景与动机 / Context and motivation

issue #1105 希望解决 DeepSeek 发送图片时以 Base64 内嵌易超出 API 限制的问题。经讨论收敛范围:本PR仅接入 DeepSeek 官方 Files API,不引入第三方图床(后续可能会有其他人跟进这个需求,但不在本人考虑和处理范围内),不增加独立配置开关(发送图片时默认自动使用,失败静默回退 Base64)。

改动范围 / Changes

  • DeepSeek Files API 四个端点全部接入:上传(POST /files,发送链路使用)、列出(GET /files,跨会话同图复用)、查询(GET /files/{id})、删除(DELETE /files/{id})。其中上传/列出接入发送链路,查询/删除为后续清理策略预留的公共能力。
  • Chat Completions 与 Responses 双协议支持:抽取共享上传器 DeepseekFileUploader,两条协议线共用;Responses 协议通过适配器将文件块转换为官方 input_image + file_id 引用。
  • 上传文件名采用图片内容 SHA-256 前缀命名(图片 id 为随机 UUID,跨会话不稳定),使“列出 -> 按文件名匹配 -> 复用 file_id”在任何会话下都成立,避免重复上传堆积。
  • 格式校验(JPEG/PNG/GIF/WebP)与大小校验(<=64MiB);上传失败静默回退 Base64 内嵌,不影响发图。
  • DeepSeek 配置界面新增一行说明文字(位于“支持识图”开关下方),非交互开关。
  • 聊天附件入口(照片/拍照/文件/记忆/当前屏幕/当前通知/当前位置/包)在无活跃对话时给出与发消息完全一致的原生 Toast 提示(“请新建对话”),避免静默无响应;同类无对话提示(摘要插入、批量删除、消息协调路径)一并统一为同一形式。
  • 8 语言文案(values 及 values-en / es / id / ko / ms / pt-rBR / ro)。
    不包含:第三方图床接入、Files API 管理界面、自动过期/清理策略(已预留删除端点能力)。

兼容性与风险 / Compatibility and risks

  • 基类 OpenAIProvider 新增两个 open 钩子(prepareMediaForRequest / buildImageContentPart),默认实现为空/原行为,其他 Provider 零影响。
  • 第三方或代理端点下上传失败时自动回退原 Base64 路径;端点推导逻辑对官方与自定义 endpoint 均有效。

关联 Issue / Related issue

Resolves #1105(范围收敛为 DeepSeek Files API,第三方图床不在本 PR)

验证方式 / Verification

检查或命令:GitHub Actions android-build.yml(assembleDebug)
环境与变体:Ubuntu runner,Android SDK 完整依赖
结果:构建通过(run 33965933246,success);含无对话提示形式修复(原生 Toast)的最新构建 run 33970039193,success
检查或命令:DeepSeek 官方 Files API 端点能力(curl)
环境与变体:真实 API key
结果:上传 / 列出 / 删除三个端点调用全部通过 ✅
检查或命令:实机 UI
环境与变体:Android 真机
结果:
- 配置界面说明文字位于“支持识图”开关下方 ✅
- 无活跃对话时点击附件入口出现提示 ✅(提示形式已改为与发消息一致的原生 Toast,待安装最新构建 APK 后复测确认)
- 发图端到端(logcat 出现“图片 xxx 已上传至 DeepSeek Files API”或平台 GET /files 列表新增文件)

证据 / Evidence

检查清单 / Checklist

  • 我已记录可复现验证和未运行项原因 / Reproducible verification and reasons for unrun checks are recorded
  • 日常开发 PR 的目标分支为 dev;如目标为 main,我已说明这是维护者发布同步 / The target branch is dev for regular work; if it is main, I explained why this is a maintainer-led release sync
  • 我已确认 Candidate checks 覆盖改动范围,并会处理技术失败项 / Candidate checks covers the change scope and technical failures will be addressed
  • 最终 diff 无无关、临时、生成、二进制或敏感内容 / Final diff has no unrelated, temporary, generated, binary, or secret content
  • 已提供对应的回归、UI、文档/字符串或兼容性证据 / Relevant regression, UI, docs/strings, or compatibility evidence is provided

@CATMIAOZHI
CATMIAOZHI self-requested a review September 7, 2026 00:16

@CATMIAOZHI CATMIAOZHI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

BLOCK:3 项 P2,详见行内评论。

审计提交:5a2f284e142874e2b4d5ea5e1fdffdfc622d98ba,目标分支 dev。已先请求 CATMIAOZHI review,再完成独立工作树全 diff、实际调用链和独立 agent 复核;git diff --check 通过。

验证范围:上游仓库该 HEAD 的 check-runs 为 0,审计前无 review 线程,已有普通评论也已核查。当前 PR workflow 仅匹配 development,本 PR 的目标为 dev;构建/单测 push workflow 匹配 main。已核对工作流实际任务及 Actions Variables(GITEE_OWNER、GITEE_REPO)。已核实作者 fork 的 Android Build 对应本次 HEAD 且成功;该运行只有构建步骤,没有 JVM 单测执行步骤,因此不将其视为单测通过。遵守目标仓库 AGENTS.md,本次未执行本地编译、测试、Lint 或真机验证,未修改源码。


/** 给请求附加认证头与自定义头(排除会破坏 multipart 边界的 Content-Type 覆写) */
private suspend fun applyAuthAndCustomHeaders(builder: Request.Builder) {
val apiKey = apiKeyProvider.getApiKey().trim()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 为媒体上传与聊天请求固定同一个 API key

MultiApiKeyProvider 每次 getApiKey() 都推进轮询索引,最终 OpenAIProvider.createRequest 又取一次 key。两个可用 key 的空缓存首图流程会变成 list=A、upload=B、chat=A;DeepSeek 官方文档 明确文件归属 API key,所以聊天请求无法引用另一 key 上传的文件。缓存仅按图片 ID 保存,后续换 key 也会继续复用错误归属。请让列表、上传、聊天及缓存共用一次请求选定的 key,重试换 key 时重新准备对应媒体。

.build()
val builder = Request.Builder().url(filesEndpoint).post(body)
applyAuthAndCustomHeaders(builder)
withContext(Dispatchers.IO) { httpClient.newCall(builder.build()).execute() }.use { response ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 将文件请求接入停止生成的取消链

这里创建的 Call 没有进入 OpenAIProvider.activeCall,也没有协程取消到 Call.cancel() 的连接。上传或列表请求很慢时,点击停止只会取消聊天 activeCall 和发送 Job,而阻塞 execute() 仍要等网络返回;MessageProcessingDelegate 的停止路径随后 join 发送 Job,导致停止和 loading 收尾一起等待。请将文件 Call 接入取消机制,并保留 CancellationException 的传播;第 118 行的列表请求也需处理。

@@ -963,12 +963,7 @@ open class OpenAIProvider(

if ((allowUserRichContent || allowResponsesToolImages) && supportsVision) {
imageLinks.forEach { link ->

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] 工具返回图片也需要经过新的文件引用构建入口

这里只替换 buildContentField 的图片路径。DeepSeek Chat Completions 在原生工具调用返回截图时,仍经 DeepseekProvider 的 TOOL_RESULT 分支调用 appendReadableImageMessageIfNeeded;该方法继续硬编码 image_url + Base64,不调用 buildImageContentPart。这些图片会先被 prepareMedia 上传,但聊天请求仍发送完整内嵌图片,无法获得本 PR 的文件复用及规避内嵌大小限制的效果。请将该实际工具图片发送路径也接入同一 hook;Responses 的工具图片走适配器,不是此处漏掉的路径。

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.

2 participants