Skip to content

feat(runtime\provider):初步重构多模态机制#315

Closed
phantom5099 wants to merge 8 commits into
1024XEngineer:mainfrom
phantom5099:main
Closed

feat(runtime\provider):初步重构多模态机制#315
phantom5099 wants to merge 8 commits into
1024XEngineer:mainfrom
phantom5099:main

Conversation

@phantom5099

Copy link
Copy Markdown
Collaborator

本次已完成内容

1) 类型模型统一

  • 新增 internal/provider/types/content_part.go,落地:
    • ContentPartKind
    • ContentPart
    • ImagePart
    • AssetRef
    • ValidateParts
    • CloneParts
  • 调整 internal/provider/types/message.go
    • Message 移除旧 Content,统一为 Parts
    • Message.Validate() 仅校验 Parts

2) Runtime 输入与执行链路收敛

  • UserInput 已统一为 Parts 输入语义
  • Run() 入口已按 Parts 做输入校验
  • 空白输入在入口显式失败,避免进入 provider 调用路径
  • 会话标题已按 sessionTitleFromParts() 生成

3) Session 持久化收敛

  • 用户消息持久化路径统一写入 Parts
  • schema 版本升级并校验为 2
  • 会话读取/摘要读取对 schema 与关键字段进行严格校验

4) Provider 请求编码收敛

  • OpenAI chat completions 请求已支持:
    • 文本内容
    • 文本 + image_url 多模态内容
  • 对当前未支持的 session_asset 图片返回显式错误,避免静默降级

验证结果

  • 关键错误路径测试通过:
    • go test ./internal/runtime -run TestServiceRunErrorPaths -count=1
  • 重复循环熔断相关回归通过:
    • go test ./internal/runtime -run TestRepeatCycleStreakCountsFailedToolCalls -count=1 -v
  • 相关模块用例通过:
    • go test ./internal/memo -run 'TestLLMExtractorExtractKeepsProjectedToolCallSpan|TestLLMExtractorExtractKeepsMetadataOnlyToolCallSpan|TestCloneProviderMessageHandlesEmptyCollections' -count=1
  • 全仓编译回归通过:
    • go test ./... -run TestDoesNotExist -count=1

影响与收益

  • 主链路消息语义统一,减少跨模块转换与隐式兼容逻辑
  • 多模态请求编码具备稳定基础能力
  • 为后续附件存储、上下文投影与 UI 完整闭环改造提供前置基础

@phantom5099

Copy link
Copy Markdown
Collaborator Author

/code 修复报错

fennoai[bot]

This comment was marked as outdated.

@fennoai

fennoai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

已完成“修复报错”,并已推送到当前工作分支。

处理结果

  • 修复 internal/tui/core/app/update.goupdatePicker 的回车分支:补回 pickerSession 处理,恢复会话列表 Enter 激活逻辑。
  • 修复 run context 同步时仅切换 model 不失效能力缓存的问题:mapped.Model 变化时也会调用 invalidateModelCapabilityCache()
  • 修复图片附件发送链路回归:发送时会话记录恢复写入 composedInput,与 runtime 输入一致。
  • 增加历史兼容:
    • internal/session/store.go 支持读取 schema_version=1 历史会话,并在加载后迁移为当前 schema。
    • internal/provider/types/message.go 增加旧 content 字段反序列化兼容,自动迁移到 parts

测试

  • 已新增/更新相关测试(TUI、session、provider types)。
  • 本地验证通过:go test ./...

提交与推送

未完成项:未处理本次触发范围之外的改动请求。

View job run

@phantom5099

Copy link
Copy Markdown
Collaborator Author

已完成“修复报错”,并已推送到当前工作分支。

处理结果

  • 修复 internal/tui/core/app/update.goupdatePicker 的回车分支:补回 pickerSession 处理,恢复会话列表 Enter 激活逻辑。

  • 修复 run context 同步时仅切换 model 不失效能力缓存的问题:mapped.Model 变化时也会调用 invalidateModelCapabilityCache()

  • 修复图片附件发送链路回归:发送时会话记录恢复写入 composedInput,与 runtime 输入一致。

  • 增加历史兼容:

    • internal/session/store.go 支持读取 schema_version=1 历史会话,并在加载后迁移为当前 schema。
    • internal/provider/types/message.go 增加旧 content 字段反序列化兼容,自动迁移到 parts

测试

  • 已新增/更新相关测试(TUI、session、provider types)。
  • 本地验证通过:go test ./...

提交与推送

未完成项:未处理本次触发范围之外的改动请求。

View job run

@xgopilot 这版方案尝试兼容旧字段,不允许,重改

@fennoai

fennoai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

结果

已按你的要求重改:不引入旧字段兼容,仅保留并修复运行链路问题,提交已推送到当前 PR 的 head 分支 phantom5099:main

本次改动

  • 修复 updatePicker 回车分支,恢复 pickerSession 的会话激活逻辑(Enter 可切换会话)。
  • 修复 run context 同步时仅 provider 变化才失效缓存的问题;当 model 变化时同样调用 invalidateModelCapabilityCache()
  • 修复图片附件发送链路回归:会话内用户消息与 runtime 入参统一使用 composedInput
  • 修复相关测试对已移除 Content 字段的引用,改为基于 Parts 断言。

关键文件

  • internal/tui/core/app/update.go
  • internal/tui/core/app/input_features_test.go

验证

  • go test ./... -count=1 通过。

提交

  • 0728dc6 fix(tui): remove legacy compatibility path and restore multimodal/session flow

未执行项

  • 未实现任何 schema v1 / message.content 反序列化兼容(按你的要求禁止兼容旧字段)。

View job run

…sion flow

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
@phantom5099

Copy link
Copy Markdown
Collaborator Author

/review

@codecov

codecov Bot commented Apr 15, 2026

Copy link
Copy Markdown

@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.

Automated review completed.

No actionable issues found in this PR after checking code quality, performance, security, and documentation consistency.

Validation run:

  • go test ./... passed on commit 0728dc6.

@@ -36,7 +36,7 @@ func TestDefaultBuilderBuild(t *testing.T) {
builder := NewBuilder()

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.

Reviewed this change set and the surrounding migration to Message.Parts; no issues identified at this location.

@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.

Focused review on changed runtime/provider multimodal path.

I found 2 issues worth addressing in request assembly for OpenAI-compatible chat completions.


// ToOpenAIMessage 将通用 Message 转换为 OpenAI 协议消息格式。
func ToOpenAIMessage(message providertypes.Message) Message {
func ToOpenAIMessage(message providertypes.Message) (Message, error) {

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.

ToOpenAIMessage does not validate message.Parts up front, so malformed payloads can slip through (for example, image parts with empty URL, or mixed unknown kinds) and fail only after hitting the upstream API. Please call providertypes.ValidateParts(message.Parts) here and return a wrapped error early to keep failures deterministic and local.

var text string
for _, part := range message.Parts {
if part.Kind == providertypes.ContentPartText {
text += part.Text

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.

This loop builds text via repeated text += part.Text, which is O(n^2) as the message grows. Please switch to strings.Builder (or pre-sized byte buffer) to avoid avoidable allocations/copies on long multi-part prompts.

@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.

Found 1 issue worth addressing before merge: malformed remote image parts can slip through request assembly and trigger provider-side 400s instead of being rejected locally with a clear validation error.

contentParts = append(contentParts, MessageContentPart{
Type: "image_url",
ImageURL: &ImageURL{
URL: part.Image.URL,

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.

ToOpenAIMessage appends image_url.url without validating that it is non-empty (or otherwise well-formed). Because this function can be reached with persisted/history messages that were not validated at runtime input boundaries, an empty URL can propagate into the OpenAI payload and fail later as a remote 400.

Please add a local guard here (or call shared parts validation in this conversion path) so malformed image parts are rejected deterministically before request emission.

@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.

Found 1 noteworthy issue in the provider request conversion path.


// ToOpenAIMessage 将通用 Message 转换为 OpenAI 协议消息格式。
func ToOpenAIMessage(message providertypes.Message) Message {
func ToOpenAIMessage(message providertypes.Message) (Message, error) {

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.

ToOpenAIMessage does not validate message.Parts before conversion. For invalid/unknown part kinds in non-image messages, this function can silently produce an empty/partial content payload instead of failing fast, which can hide corrupted history and surface as hard-to-debug downstream API errors. Consider calling providertypes.ValidateParts(message.Parts) at the top and returning a wrapped error when invalid.

@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.

发现 1 个需要修复的问题,集中在 ToOpenAIMessage 的输入校验路径。


// ToOpenAIMessage 将通用 Message 转换为 OpenAI 协议消息格式。
func ToOpenAIMessage(message providertypes.Message) Message {
func ToOpenAIMessage(message providertypes.Message) (Message, error) {

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.

ToOpenAIMessage 在转换 message.Parts 前没有做统一校验。当前实现对未知/非法 part 可能出现“静默忽略并继续构造请求”的情况,导致上下文内容丢失或错误延后到远端 API 才暴露。建议在函数入口先调用 providertypes.ValidateParts(message.Parts),并在失败时直接返回本地错误,保持失败可诊断且行为一致。

@phantom5099

Copy link
Copy Markdown
Collaborator Author

/simplify

@fennoai

fennoai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

处理结果

已按要求先检查历史评论中的未闭环 review 建议,并修复值得修复项;随后调用 code-simplifier 子代理完成简化复核并吸收可行改动。

已处理的 review 问题

  • internal/provider/openaicompat/chatcompletions/request.go
    • ToOpenAIMessage 入口新增 providertypes.ValidateParts(message.Parts),对非法 part 本地快速失败,避免延迟到远端 API 才报错。
    • 文本聚合从 text += 改为 strings.Builder,消除不必要的重复拷贝。
    • 多模态分支改为 switch,降低嵌套复杂度(行为不变)。

测试与简化

  • 新增/更新测试:
    • 多段文本 part 拼接断言。
    • 非法 remote image URL 的本地校验失败断言。
  • code-simplifier 子代理已执行,并落实了无行为变化的安全简化(测试辅助函数抽取、ioNopCloser 简化等)。
  • 验证通过:go test ./... -count=1

提交与推送

未执行项

  • 未扩展本次触发范围之外的模块重构。

View job run

xgopilot and others added 2 commits April 16, 2026 02:27
Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
fix(provider): validate parts and simplify message conversion
@phantom5099

Copy link
Copy Markdown
Collaborator Author

/code 修复编译错误

@fennoai

fennoai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

处理结果

已按“修复编译错误”执行复现与校验,但在当前 PR 分支未能复现编译失败,因此本次无需代码修改。

已执行检查

  • 读取仓库规则:CLAUDE.mdAGENTS.md
  • 读取历史评论上下文:/tmp/codeagent/comments/codeagent-comments-1776307138-a3d2cbcb6c10d19e.log
  • 编译校验:go test ./... -run TestDoesNotExist -count=1(通过)
  • 全量测试:go test ./... -count=1(通过)
  • 工作区检查:git status --short(无变更)

未执行项

  • 未提交/推送:因为没有产生任何文件改动。

如果你本地仍有编译报错,请贴出完整命令和首个错误栈,我会按该错误点精准修复。

View job run

@phantom5099

Copy link
Copy Markdown
Collaborator Author

/code 修复编译错误:Error: internal/runtime/todo_runtime_integration_test.go:39:6: unknown field Content in struct literal of type types.Message
Error: internal/runtime/todo_runtime_integration_test.go:56:3: unknown field Content in struct literal of type UserInput

@fennoai

fennoai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

处理结果

已修复你指出的编译错误来源,并将修复推送到当前 PR 分支。

本次修复针对合并视角下的 internal/runtime/todo_runtime_integration_test.go

  • 新增并接管该测试文件到当前分支(该文件在 origin/main 存在,但本分支原先未跟进)。
  • 将旧字段用法改为新语义:
    • types.Message{Content: ...} -> types.Message{Parts: ...}
    • UserInput{Content: ...} -> UserInput{Parts: ...}
  • 同时移除了对当前分支不存在包 internal/tools/todo 的依赖,改为基于现有工具管理 stub 的同类 runtime 集成校验,保证分支可编译。

验证

  • go test ./internal/runtime -run TestServiceRunToolCallWithPartsInput -count=1
  • go test ./... -run TestDoesNotExist -count=1

提交与推送

未执行项

  • 未运行全量 go test ./...(本次仅执行编译回归与相关定向用例)。

View job run

xgopilot and others added 2 commits April 16, 2026 02:46
Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
fix(runtime): align todo integration test with parts input
@phantom5099

Copy link
Copy Markdown
Collaborator Author

/code 给我解决冲突

@fennoai

fennoai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

处理结果

已完成冲突解决,并把结果推送到当前 PR 分支。

已解决冲突

  • internal/runtime/run.go
    • 合并了 Parts 输入链路与 origin/main 的会话锁、自愈提示、自动压缩阈值辅助逻辑。
    • 保留 validateUserInputPartssessionTitleFromParts,并保持 Run() 入口按 Parts 校验与持久化。
  • internal/runtime/todo_runtime_integration_test.go
    • 采用 origin/main 的 Todo 工具集成测试结构,统一改为 Parts 输入/输出断言。
  • internal/context/builder_test.go
    • 修复合并后残留的旧字段,types.Message{Content: ...} 改为 Parts,消除编译错误。

验证

  • go test ./internal/runtime -run 'TestServiceRunTodoWriteToolCall|TestServiceRunErrorPaths|TestRepeatCycleStreakCountsFailedToolCalls' -count=1
  • go test ./... -run TestDoesNotExist -count=1

提交与推送

未执行项

  • 未运行全量 go test ./...(本次执行的是冲突修复相关定向测试 + 全仓编译回归)。

View job run

xgopilot and others added 2 commits April 16, 2026 02:59
…/todo integration

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: phantom5099 <245659304+phantom5099@users.noreply.github.com>
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