新增: 媒体引用化存储与消息溯源
- media_store 模块: sha256 去重落盘 + 阿里云 OSS 双写(签名手写), OSS 失败降级本机引用 - 飞书图片入站: image 消息下载装配 vision 片, 3 次退避+120s 超时+残件清理防线 - 出站回传: 报告 [交付] 路径行自动上传飞书原生附件, 凭据/密钥名拦截兜底 - 溯源 V52: ai_messages.source 列, 飞书 msg_id/chat_id/发送者随消息落库 - spawn offload: 桌面粘贴图 base64 统一转落盘引用, SQLite 不进大字符串 - 引用统一 url 字段(https/file://)不自造 scheme, image_ref 发送前按需展开
This commit is contained in:
@@ -118,10 +118,12 @@ impl OpenAICompatProvider {
|
||||
// 多模态 content(须在 move m.tool_calls 之前算,借用 m)。
|
||||
// 含图消息走 content 数组(text/image_url);纯文本走字符串简写
|
||||
// (保持与现有纯文本端点零回归)。image_url 支持 data URI(base64)与 http(s) URL。
|
||||
// 本地 file:// 引用(#958)先展开为 base64 内嵌——OpenAI 端点不认本地路径
|
||||
let content = if m.has_image() {
|
||||
let parts: Vec<serde_json::Value> = m
|
||||
.flattened_parts()
|
||||
.into_iter()
|
||||
.map(crate::image_ref::inline_local_image_part)
|
||||
.map(|p| match p {
|
||||
crate::provider::ContentPart::Text { text } => serde_json::json!({
|
||||
"type": "text",
|
||||
@@ -1020,6 +1022,57 @@ mod tests {
|
||||
assert_eq!(arr[2]["image_url"]["url"], "https://x/a.png");
|
||||
}
|
||||
|
||||
/// 本地 file:// 引用(#958)在转换层展开为 data URI;引用缺失降级 Text 片不炸请求
|
||||
#[test]
|
||||
fn openai_convert_expands_local_file_ref() {
|
||||
use base64::Engine as _;
|
||||
let dir = std::env::temp_dir().join("df-ai-openai-ref-test");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("ref.png");
|
||||
std::fs::write(&path, b"\x89PNG ok").unwrap();
|
||||
|
||||
let provider = OpenAICompatProvider::new("https://api.openai.com", "k", "gpt-4o");
|
||||
let req = CompletionRequest {
|
||||
model: "gpt-4o".into(),
|
||||
messages: vec![
|
||||
ChatMessage::user_parts(
|
||||
"看图",
|
||||
vec![crate::provider::ContentPart::Image {
|
||||
url: Some(format!("file://{}", path.display())),
|
||||
base64: None,
|
||||
media_type: Some("image/png".into()),
|
||||
alt: None,
|
||||
}],
|
||||
),
|
||||
ChatMessage::user_parts(
|
||||
"坏引用",
|
||||
vec![crate::provider::ContentPart::Image {
|
||||
url: Some("file:///nonexistent/ref-missing.png".into()),
|
||||
base64: None,
|
||||
media_type: Some("image/png".into()),
|
||||
alt: None,
|
||||
}],
|
||||
),
|
||||
],
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
let out = provider.convert_request(req);
|
||||
let good = &out.messages[0];
|
||||
let arr = good.content.as_array().expect("含图 → 数组");
|
||||
assert_eq!(arr[1]["image_url"]["url"], format!("data:image/png;base64,{}", base64::engine::general_purpose::STANDARD.encode(b"\x89PNG ok")));
|
||||
|
||||
let bad = &out.messages[1];
|
||||
let arr = bad.content.as_array().expect("降级片仍在数组");
|
||||
assert_eq!(arr[1]["type"], "text");
|
||||
assert!(arr[1]["text"].as_str().unwrap().contains("[图片文件缺失"));
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
/// 纯文本消息 → content 仍是字符串简写(无图不数组化,对齐纯文本端点兼容)
|
||||
#[test]
|
||||
fn openai_convert_text_only_remains_string() {
|
||||
|
||||
Reference in New Issue
Block a user