myagent-ai 1.16.12 → 1.16.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/base.py +3 -1
- package/agents/main_agent.py +35 -0
- package/core/llm.py +42 -2
- package/package.json +2 -2
- package/web/api_server.py +5 -2
- package/web/ui/chat/chat_main.js +102 -30
package/agents/base.py
CHANGED
|
@@ -242,7 +242,9 @@ class BaseAgent(ABC):
|
|
|
242
242
|
if role == "system":
|
|
243
243
|
system_msg = content
|
|
244
244
|
continue
|
|
245
|
-
|
|
245
|
+
# 转换 OpenAI Vision 格式为 Anthropic 格式
|
|
246
|
+
anth_content = self.llm._convert_to_anthropic_content(content)
|
|
247
|
+
anth_messages.append({"role": role, "content": anth_content})
|
|
246
248
|
|
|
247
249
|
create_kwargs = {
|
|
248
250
|
"model": self.llm.model,
|
package/agents/main_agent.py
CHANGED
|
@@ -693,6 +693,41 @@ class MainAgent(BaseAgent):
|
|
|
693
693
|
)
|
|
694
694
|
break
|
|
695
695
|
|
|
696
|
+
# [v1.16.13] 特殊处理模型不支持图片输入 — 去掉图片用纯文本重试
|
|
697
|
+
_vision_keywords = ["doesn't support image", "does not support image", "model_incompatible", "image input", "not support vision", "unsupported multimodal", "image capability"]
|
|
698
|
+
if any(kw in _llm_error.lower() for kw in _vision_keywords) and context.metadata.get("user_images"):
|
|
699
|
+
logger.warning(f"[{task_id}] 模型不支持图片输入,去掉图片用纯文本重试")
|
|
700
|
+
context.metadata["user_images"] = []
|
|
701
|
+
# 用纯文本消息替换最后一条多模态消息
|
|
702
|
+
_text_only_msg = context.user_message or "请处理上述上下文。"
|
|
703
|
+
if len(messages) > 0 and isinstance(messages[-1].content, list):
|
|
704
|
+
messages[-1] = Message(role="user", content=_text_only_msg)
|
|
705
|
+
# 重试 LLM 调用
|
|
706
|
+
if stream_response and self.llm:
|
|
707
|
+
response = await self._call_llm_stream(
|
|
708
|
+
messages, text_delta_callback=text_delta_callback,
|
|
709
|
+
stream_response=stream_response,
|
|
710
|
+
)
|
|
711
|
+
else:
|
|
712
|
+
response = await self._call_llm(messages)
|
|
713
|
+
if response.success:
|
|
714
|
+
# 纯文本重试成功,给回复加上提示前缀
|
|
715
|
+
_vision_prefix = "⚠️ 当前模型不支持图片识别,已自动使用纯文本模式处理(图片未发送给模型)。\n\n"
|
|
716
|
+
llm_raw = _vision_prefix + response.content
|
|
717
|
+
context.working_memory["final_response"] = llm_raw
|
|
718
|
+
await self._emit_v2_event("v2_reasoning", {"content": llm_raw}, stream_callback)
|
|
719
|
+
if self.memory:
|
|
720
|
+
self.memory.add_session(
|
|
721
|
+
session_id=context.session_id,
|
|
722
|
+
role="assistant",
|
|
723
|
+
content=llm_raw,
|
|
724
|
+
)
|
|
725
|
+
break
|
|
726
|
+
else:
|
|
727
|
+
# 纯文本也失败了,走下面的通用错误处理
|
|
728
|
+
_llm_error = response.error or ""
|
|
729
|
+
logger.error(f"[{task_id}] 纯文本重试也失败: {_llm_error}")
|
|
730
|
+
|
|
696
731
|
# 其他 LLM 错误
|
|
697
732
|
error_msg = f"LLM 调用失败: {response.error}"
|
|
698
733
|
context.working_memory["final_response"] = error_msg
|
package/core/llm.py
CHANGED
|
@@ -238,6 +238,41 @@ class LLMClient:
|
|
|
238
238
|
# 所有使用 OpenAI 兼容接口的提供商
|
|
239
239
|
_OPENAI_COMPATIBLE_PROVIDERS = ("openai", "custom", "modelscope", "deepseek", "moonshot", "qwen", "dashscope")
|
|
240
240
|
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _convert_to_anthropic_content(content):
|
|
243
|
+
"""将 OpenAI Vision 格式的 content 转换为 Anthropic 格式
|
|
244
|
+
|
|
245
|
+
OpenAI 格式: [{"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}]
|
|
246
|
+
Anthropic 格式: [{"type": "text", "text": "..."}, {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": "..."}}]
|
|
247
|
+
"""
|
|
248
|
+
if not isinstance(content, list):
|
|
249
|
+
return content
|
|
250
|
+
|
|
251
|
+
import re
|
|
252
|
+
anth_content = []
|
|
253
|
+
for item in content:
|
|
254
|
+
if isinstance(item, dict) and item.get("type") == "image_url":
|
|
255
|
+
url = item.get("image_url", {}).get("url", "")
|
|
256
|
+
# 解析 data URI: data:image/png;base64,xxxxx
|
|
257
|
+
match = re.match(r'^data:([^;]+);base64,(.+)$', url, re.DOTALL)
|
|
258
|
+
if match:
|
|
259
|
+
media_type = match.group(1)
|
|
260
|
+
b64_data = match.group(2)
|
|
261
|
+
anth_content.append({
|
|
262
|
+
"type": "image",
|
|
263
|
+
"source": {
|
|
264
|
+
"type": "base64",
|
|
265
|
+
"media_type": media_type,
|
|
266
|
+
"data": b64_data,
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
else:
|
|
270
|
+
# 非 data URI 格式(如 http URL),Anthropic 也支持但方式不同,暂保留原格式
|
|
271
|
+
anth_content.append(item)
|
|
272
|
+
else:
|
|
273
|
+
anth_content.append(item)
|
|
274
|
+
return anth_content
|
|
275
|
+
|
|
241
276
|
def _ensure_client(self):
|
|
242
277
|
"""延迟初始化 LLM 客户端"""
|
|
243
278
|
if self._client is not None:
|
|
@@ -552,7 +587,9 @@ class LLMClient:
|
|
|
552
587
|
if m.role == "system":
|
|
553
588
|
system_msg = m.content
|
|
554
589
|
continue
|
|
555
|
-
|
|
590
|
+
# 转换 OpenAI Vision 格式为 Anthropic 格式
|
|
591
|
+
anth_content = self._convert_to_anthropic_content(m.content)
|
|
592
|
+
anth_messages.append({"role": m.role, "content": anth_content})
|
|
556
593
|
|
|
557
594
|
create_kwargs = {
|
|
558
595
|
"model": self.model,
|
|
@@ -669,6 +706,7 @@ class LLMClient:
|
|
|
669
706
|
logger.error(f"流式调用不支持提供商: {self.provider}")
|
|
670
707
|
except Exception as e:
|
|
671
708
|
logger.error(f"流式 LLM 调用失败: {e}")
|
|
709
|
+
raise
|
|
672
710
|
|
|
673
711
|
async def _stream_openai(self, kwargs: dict) -> AsyncGenerator[str, None]:
|
|
674
712
|
"""OpenAI / 兼容接口 (含 Zhipu) 流式调用
|
|
@@ -715,7 +753,9 @@ class LLMClient:
|
|
|
715
753
|
if m.role == "system":
|
|
716
754
|
system_msg = m.content
|
|
717
755
|
continue
|
|
718
|
-
|
|
756
|
+
# 转换 OpenAI Vision 格式为 Anthropic 格式
|
|
757
|
+
anth_content = self._convert_to_anthropic_content(m.content)
|
|
758
|
+
anth_messages.append({"role": m.role, "content": anth_content})
|
|
719
759
|
|
|
720
760
|
create_kwargs = {
|
|
721
761
|
"model": self.model,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagent-ai",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.14",
|
|
4
4
|
"description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
|
|
5
5
|
"main": "main.py",
|
|
6
6
|
"bin": {
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"python": ">=3.10",
|
|
44
44
|
"node": ">=18"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
package/web/api_server.py
CHANGED
|
@@ -852,6 +852,7 @@ class ApiServer:
|
|
|
852
852
|
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
853
853
|
chat_mode=chat_mode, stream_response=proxy,
|
|
854
854
|
voice_text=voice_text,
|
|
855
|
+
user_images=user_images, user_files=user_files,
|
|
855
856
|
)
|
|
856
857
|
elif self.core.main_agent and self.core.llm:
|
|
857
858
|
full_response = await self._stream_process_message(
|
|
@@ -3700,7 +3701,7 @@ class ApiServer:
|
|
|
3700
3701
|
async def _try_model_chain_stream(self, model_chain, message, session_id,
|
|
3701
3702
|
agent_path=None, agent_system_prompt=None,
|
|
3702
3703
|
chat_mode="", stream_response=None,
|
|
3703
|
-
voice_text=""):
|
|
3704
|
+
voice_text="", user_images=None, user_files=None):
|
|
3704
3705
|
"""流式版本的模型链调用,逐token输出到SSE
|
|
3705
3706
|
|
|
3706
3707
|
使用 asyncio.Lock 保护共享的 self.core.llm,防止并发请求互相干扰。
|
|
@@ -3716,12 +3717,13 @@ class ApiServer:
|
|
|
3716
3717
|
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
3717
3718
|
chat_mode=chat_mode, stream_response=stream_response,
|
|
3718
3719
|
voice_text=voice_text,
|
|
3720
|
+
user_images=user_images, user_files=user_files,
|
|
3719
3721
|
)
|
|
3720
3722
|
|
|
3721
3723
|
async def _try_model_chain_stream_inner(self, model_chain, message, session_id,
|
|
3722
3724
|
agent_path=None, agent_system_prompt=None,
|
|
3723
3725
|
chat_mode="", stream_response=None,
|
|
3724
|
-
voice_text=""):
|
|
3726
|
+
voice_text="", user_images=None, user_files=None):
|
|
3725
3727
|
"""_try_model_chain_stream 的实际执行体(已在 _model_chain_lock 保护下)"""
|
|
3726
3728
|
llm = self.core.llm
|
|
3727
3729
|
full_text = ""
|
|
@@ -3753,6 +3755,7 @@ class ApiServer:
|
|
|
3753
3755
|
message, session_id, stream_response,
|
|
3754
3756
|
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
3755
3757
|
chat_mode=chat_mode, voice_text=voice_text,
|
|
3758
|
+
user_images=user_images, user_files=user_files,
|
|
3756
3759
|
)
|
|
3757
3760
|
if result and not result.startswith("⚠️") and not result.startswith("❌"):
|
|
3758
3761
|
return result
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2776,6 +2776,15 @@ function initAttachmentUI() {
|
|
|
2776
2776
|
window._pendingImages = [];
|
|
2777
2777
|
window._pendingFiles = [];
|
|
2778
2778
|
|
|
2779
|
+
// [v1.16.13] 图片压缩配置
|
|
2780
|
+
window._IMAGE_COMPRESS = {
|
|
2781
|
+
maxWidth: 2048, // 最大宽度
|
|
2782
|
+
maxHeight: 2048, // 最大高度
|
|
2783
|
+
quality: 0.85, // JPEG 压缩质量
|
|
2784
|
+
maxSizeBytes: 4 * 1024 * 1024, // 单张图片最大 4MB (base64后约 5.3MB)
|
|
2785
|
+
skipCompression: false, // 跳过压缩(如果图片已经足够小)
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2779
2788
|
// 创建隐藏的文件输入
|
|
2780
2789
|
if (!document.getElementById('imageFileInput')) {
|
|
2781
2790
|
var imgInput = document.createElement('input');
|
|
@@ -2799,34 +2808,90 @@ function initAttachmentUI() {
|
|
|
2799
2808
|
}
|
|
2800
2809
|
}
|
|
2801
2810
|
|
|
2811
|
+
// [v1.16.13] 图片压缩函数 — 缩放尺寸 + JPEG 压缩,返回 Promise<{base64, width, height}>
|
|
2812
|
+
function compressImage(file) {
|
|
2813
|
+
return new Promise(function(resolve, reject) {
|
|
2814
|
+
var cfg = window._IMAGE_COMPRESS || {};
|
|
2815
|
+
var reader = new FileReader();
|
|
2816
|
+
reader.onerror = function() { reject(new Error('读取文件失败')); };
|
|
2817
|
+
reader.onload = function(e) {
|
|
2818
|
+
var img = new Image();
|
|
2819
|
+
img.onerror = function() { reject(new Error('图片加载失败')); };
|
|
2820
|
+
img.onload = function() {
|
|
2821
|
+
var w = img.naturalWidth;
|
|
2822
|
+
var h = img.naturalHeight;
|
|
2823
|
+
// 如果图片已足够小且体积未超限,直接返回原始数据
|
|
2824
|
+
var rawBase64 = e.target.result.split(',')[1];
|
|
2825
|
+
if (cfg.skipCompression && file.size <= (cfg.maxSizeBytes || 4194304) && w <= (cfg.maxWidth || 2048) && h <= (cfg.maxHeight || 2048)) {
|
|
2826
|
+
resolve({ base64: rawBase64, width: w, height: h, type: file.type });
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
// 计算缩放比例
|
|
2830
|
+
var maxW = cfg.maxWidth || 2048;
|
|
2831
|
+
var maxH = cfg.maxHeight || 2048;
|
|
2832
|
+
var ratio = Math.min(maxW / w, maxH / h, 1);
|
|
2833
|
+
var newW = Math.round(w * ratio);
|
|
2834
|
+
var newH = Math.round(h * ratio);
|
|
2835
|
+
// Canvas 绘制并压缩
|
|
2836
|
+
var canvas = document.createElement('canvas');
|
|
2837
|
+
canvas.width = newW;
|
|
2838
|
+
canvas.height = newH;
|
|
2839
|
+
var ctx = canvas.getContext('2d');
|
|
2840
|
+
ctx.drawImage(img, 0, 0, newW, newH);
|
|
2841
|
+
// 如果是 PNG 透明图且不大,保留 PNG;否则转 JPEG
|
|
2842
|
+
var useJpeg = file.type !== 'image/png' || file.size > 500000;
|
|
2843
|
+
var mimeType = useJpeg ? 'image/jpeg' : file.type;
|
|
2844
|
+
var quality = useJpeg ? (cfg.quality || 0.85) : undefined;
|
|
2845
|
+
var dataUrl = canvas.toDataURL(mimeType, quality);
|
|
2846
|
+
var b64 = dataUrl.split(',')[1];
|
|
2847
|
+
// 如果压缩后反而更大(罕见),用原始数据
|
|
2848
|
+
if (b64.length > rawBase64.length) {
|
|
2849
|
+
b64 = rawBase64;
|
|
2850
|
+
mimeType = file.type;
|
|
2851
|
+
}
|
|
2852
|
+
resolve({ base64: b64, width: newW, height: newH, type: mimeType });
|
|
2853
|
+
};
|
|
2854
|
+
img.src = e.target.result;
|
|
2855
|
+
};
|
|
2856
|
+
reader.readAsDataURL(file);
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2802
2860
|
function handleFileSelect(input, type) {
|
|
2803
2861
|
var files = input.files;
|
|
2804
2862
|
if (!files || files.length === 0) return;
|
|
2805
2863
|
|
|
2806
2864
|
for (var i = 0; i < files.length; i++) {
|
|
2807
2865
|
(function(file) {
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
if (type === 'image' && file.type.startsWith('image/')) {
|
|
2866
|
+
if (type === 'image' && file.type.startsWith('image/')) {
|
|
2867
|
+
// [v1.16.13] 图片文件使用压缩
|
|
2868
|
+
compressImage(file).then(function(result) {
|
|
2812
2869
|
window._pendingImages.push({
|
|
2813
|
-
type:
|
|
2814
|
-
data: base64,
|
|
2870
|
+
type: result.type,
|
|
2871
|
+
data: result.base64,
|
|
2815
2872
|
name: file.name,
|
|
2816
2873
|
size: file.size,
|
|
2817
2874
|
});
|
|
2818
|
-
|
|
2875
|
+
renderAttachmentPreview();
|
|
2876
|
+
updateSendBtnState();
|
|
2877
|
+
}).catch(function(err) {
|
|
2878
|
+
console.error('图片压缩失败:', err);
|
|
2879
|
+
});
|
|
2880
|
+
} else {
|
|
2881
|
+
var reader = new FileReader();
|
|
2882
|
+
reader.onload = function(e) {
|
|
2883
|
+
var base64 = e.target.result.split(',')[1];
|
|
2819
2884
|
window._pendingFiles.push({
|
|
2820
2885
|
type: file.type,
|
|
2821
2886
|
data: base64,
|
|
2822
2887
|
name: file.name,
|
|
2823
2888
|
size: file.size,
|
|
2824
2889
|
});
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2890
|
+
renderAttachmentPreview();
|
|
2891
|
+
updateSendBtnState();
|
|
2892
|
+
};
|
|
2893
|
+
reader.readAsDataURL(file);
|
|
2894
|
+
}
|
|
2830
2895
|
})(files[i]);
|
|
2831
2896
|
}
|
|
2832
2897
|
}
|
|
@@ -2842,19 +2907,19 @@ function handlePasteEvent(e) {
|
|
|
2842
2907
|
var file = items[i].getAsFile();
|
|
2843
2908
|
if (file) {
|
|
2844
2909
|
(function(f) {
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
var base64 = ev.target.result.split(',')[1];
|
|
2910
|
+
// [v1.16.13] 粘贴图片使用压缩
|
|
2911
|
+
compressImage(f).then(function(result) {
|
|
2848
2912
|
window._pendingImages.push({
|
|
2849
|
-
type:
|
|
2850
|
-
data: base64,
|
|
2913
|
+
type: result.type,
|
|
2914
|
+
data: result.base64,
|
|
2851
2915
|
name: 'paste-' + Date.now() + '.png',
|
|
2852
2916
|
size: f.size,
|
|
2853
2917
|
});
|
|
2854
2918
|
renderAttachmentPreview();
|
|
2855
2919
|
updateSendBtnState();
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2920
|
+
}).catch(function(err) {
|
|
2921
|
+
console.error('粘贴图片压缩失败:', err);
|
|
2922
|
+
});
|
|
2858
2923
|
})(file);
|
|
2859
2924
|
}
|
|
2860
2925
|
}
|
|
@@ -2868,18 +2933,25 @@ function handleDropEvent(e) {
|
|
|
2868
2933
|
if (!dt || !dt.files || dt.files.length === 0) return;
|
|
2869
2934
|
for (var i = 0; i < dt.files.length; i++) {
|
|
2870
2935
|
(function(file) {
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2936
|
+
if (file.type.startsWith('image/')) {
|
|
2937
|
+
// [v1.16.13] 拖放图片使用压缩
|
|
2938
|
+
compressImage(file).then(function(result) {
|
|
2939
|
+
window._pendingImages.push({ type: result.type, data: result.base64, name: file.name, size: file.size });
|
|
2940
|
+
renderAttachmentPreview();
|
|
2941
|
+
updateSendBtnState();
|
|
2942
|
+
}).catch(function(err) {
|
|
2943
|
+
console.error('拖放图片压缩失败:', err);
|
|
2944
|
+
});
|
|
2945
|
+
} else {
|
|
2946
|
+
var reader = new FileReader();
|
|
2947
|
+
reader.onload = function(ev) {
|
|
2948
|
+
var base64 = ev.target.result.split(',')[1];
|
|
2877
2949
|
window._pendingFiles.push({ type: file.type, data: base64, name: file.name, size: file.size });
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2950
|
+
renderAttachmentPreview();
|
|
2951
|
+
updateSendBtnState();
|
|
2952
|
+
};
|
|
2953
|
+
reader.readAsDataURL(file);
|
|
2954
|
+
}
|
|
2883
2955
|
})(dt.files[i]);
|
|
2884
2956
|
}
|
|
2885
2957
|
}
|