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 CHANGED
@@ -242,7 +242,9 @@ class BaseAgent(ABC):
242
242
  if role == "system":
243
243
  system_msg = content
244
244
  continue
245
- anth_messages.append({"role": role, "content": content})
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,
@@ -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
- anth_messages.append({"role": m.role, "content": m.content})
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
- anth_messages.append({"role": m.role, "content": m.content})
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.12",
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
@@ -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
- var reader = new FileReader();
2809
- reader.onload = function(e) {
2810
- var base64 = e.target.result.split(',')[1];
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: file.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
- } else {
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
- renderAttachmentPreview();
2827
- updateSendBtnState();
2828
- };
2829
- reader.readAsDataURL(file);
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
- var reader = new FileReader();
2846
- reader.onload = function(ev) {
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: f.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
- reader.readAsDataURL(f);
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
- var reader = new FileReader();
2872
- reader.onload = function(ev) {
2873
- var base64 = ev.target.result.split(',')[1];
2874
- if (file.type.startsWith('image/')) {
2875
- window._pendingImages.push({ type: file.type, data: base64, name: file.name, size: file.size });
2876
- } else {
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
- renderAttachmentPreview();
2880
- updateSendBtnState();
2881
- };
2882
- reader.readAsDataURL(file);
2950
+ renderAttachmentPreview();
2951
+ updateSendBtnState();
2952
+ };
2953
+ reader.readAsDataURL(file);
2954
+ }
2883
2955
  })(dt.files[i]);
2884
2956
  }
2885
2957
  }