myagent-ai 1.20.6 → 1.20.8

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.
@@ -35,7 +35,9 @@ class ChatBotManager:
35
35
 
36
36
  def __init__(self):
37
37
  self._bots: Dict[str, BaseChatBot] = {}
38
+ self._bot_tasks: Dict[str, asyncio.Task] = {} # key -> asyncio.Task
38
39
  self._session_map: Dict[str, str] = {} # session_id -> last_message
40
+ self._message_handler: Optional[Callable] = None
39
41
 
40
42
  def get_bot(self, key: str):
41
43
  """根据 key (id 或 platform 名) 获取 bot 实例"""
@@ -49,20 +51,62 @@ class ChatBotManager:
49
51
  """
50
52
  初始化所有聊天平台。
51
53
 
54
+ [v1.20.7] 修复: 先停止并移除已不存在的/被禁用的 bot,
55
+ 再创建新启用的 bot。之前只做添加不做移除,导致禁用平台后
56
+ bot 仍在后台运行。
57
+
52
58
  Args:
53
59
  platform_configs: 平台配置列表
54
60
  message_handler: 统一消息处理回调
55
61
  """
62
+ self._message_handler = message_handler
63
+
64
+ # 计算当前应该启用的平台 key 集合
65
+ new_keys = set()
66
+ for cfg in platform_configs:
67
+ if cfg.enabled:
68
+ key = cfg.id or cfg.platform
69
+ new_keys.add(key)
70
+
71
+ # 找出需要移除的(旧的 key 不在新的 key 集合中)
72
+ removed_keys = [k for k in self._bots if k not in new_keys]
73
+ for key in removed_keys:
74
+ bot = self._bots.pop(key, None)
75
+ task = self._bot_tasks.pop(key, None)
76
+ if bot:
77
+ logger.info(f"聊天平台已移除(禁用/删除): {key}")
78
+ # 尝试停止 bot(同步包装异步)
79
+ try:
80
+ loop = asyncio.get_event_loop()
81
+ if loop.is_running():
82
+ asyncio.ensure_future(self._safe_stop(key, bot))
83
+ else:
84
+ loop.run_until_complete(bot.stop())
85
+ except RuntimeError:
86
+ pass
87
+ if task and not task.done():
88
+ task.cancel()
89
+ logger.info(f"聊天平台 {key} 后台任务已取消")
90
+
91
+ # 创建或更新启用的平台
56
92
  for cfg in platform_configs:
57
93
  if not cfg.enabled:
58
94
  continue
95
+ key = cfg.id or cfg.platform
96
+ # 如果已经存在且配置没变,跳过重建
97
+ if key in self._bots:
98
+ continue
59
99
  try:
60
100
  bot = self._create_bot(cfg, message_handler)
61
101
  if bot:
62
- # 使用唯一 id 作为 key,支持多实例
63
- key = cfg.id or cfg.platform
64
102
  self._bots[key] = bot
65
103
  logger.info(f"聊天平台已配置: {cfg.display_name or key}")
104
+ # 如果已经在运行中(start_all 已调用),自动启动新 bot
105
+ loop = asyncio.get_event_loop()
106
+ if loop.is_running() and not any(
107
+ t for t in asyncio.all_tasks(loop) if t.get_name() == f"bot_{key}"
108
+ ):
109
+ asyncio.ensure_future(self._run_bot(key, bot))
66
110
  except Exception as e:
67
111
  logger.error(f"平台 {cfg.display_name or cfg.platform} 初始化失败: {e}")
68
112
 
@@ -143,7 +187,8 @@ class ChatBotManager:
143
187
  tasks = []
144
188
  for name, bot in self._bots.items():
145
189
  logger.info(f"启动聊天平台: {name}")
146
- task = asyncio.create_task(self._run_bot(name, bot))
190
+ task = asyncio.create_task(self._run_bot(name, bot), name=f"bot_{name}")
191
+ self._bot_tasks[name] = task
147
192
  tasks.append(task)
148
193
  await asyncio.gather(*tasks, return_exceptions=True)
149
194
 
@@ -151,16 +196,39 @@ class ChatBotManager:
151
196
  """安全运行单个聊天平台"""
152
197
  try:
153
198
  await bot.start()
199
+ except asyncio.CancelledError:
200
+ logger.info(f"聊天平台 {name} 已取消")
154
201
  except Exception as e:
155
202
  logger.error(f"聊天平台 {name} 运行异常: {e}")
156
203
 
204
+ async def _safe_stop(self, name: str, bot: BaseChatBot):
205
+ """安全停止单个 bot(不抛异常)"""
206
+ try:
207
+ await bot.stop()
208
+ logger.info(f"聊天平台 {name} 已停止")
209
+ except Exception as e:
210
+ logger.warning(f"停止聊天平台 {name} 异常: {e}")
211
+
212
+ async def stop_platform(self, key: str) -> bool:
213
+ """[v1.20.7] 停止并移除单个聊天平台"""
214
+ bot = self._bots.pop(key, None)
215
+ task = self._bot_tasks.pop(key, None)
216
+ if not bot:
217
+ return False
218
+ await self._safe_stop(key, bot)
219
+ if task and not task.done():
220
+ task.cancel()
221
+ return True
222
+
157
223
  async def stop_all(self):
158
224
  """停止所有聊天平台"""
159
- for name, bot in self._bots.items():
225
+ for name, bot in list(self._bots.items()):
160
226
  try:
161
227
  await bot.stop()
162
228
  except Exception as e:
163
229
  logger.error(f"停止 {name} 失败: {e}")
230
+ self._bots.clear()
231
+ self._bot_tasks.clear()
164
232
  logger.info("所有聊天平台已停止")
165
233
 
166
234
  async def send_to_all(self, text: str):
@@ -601,13 +601,16 @@ class WebControlManager:
601
601
  # 仅改写 HTML 属性中的 URL (不影响内联脚本)
602
602
  def rewrite_attr(match):
603
603
  attr_name = match.group(1).lower()
604
- q_char = match.group(2)
605
- url_val = match.group(3)
604
+ eq = match.group(2) # "=" (可能带空格)
605
+ q_open = match.group(3) # 开引号 " 或 '
606
+ url_val = match.group(4) # URL 值
607
+ q_close = match.group(5) # 闭引号 (同 q_open)
606
608
 
607
- if not url_val or url_val.startswith('data:') or url_val.startswith('blob:') or url_val.startswith('#') or url_val.startswith('javascript:'):
609
+ # 跳过特殊协议
610
+ if not url_val or url_val.startswith('data:') or url_val.startswith('blob:') or url_val.startswith('#') or url_val.startswith('javascript:') or url_val.startswith('mailto:') or url_val.startswith('tel:'):
608
611
  return match.group(0)
609
612
 
610
- # 已经是代理 URL
613
+ # 已经是代理 URL — 跳过
611
614
  if '/api/web_control/proxy' in url_val:
612
615
  return match.group(0)
613
616
 
@@ -619,23 +622,22 @@ class WebControlManager:
619
622
  elif not url_val.startswith('http://') and not url_val.startswith('https://'):
620
623
  url_val = urljoin(original_url, url_val)
621
624
 
622
- # 非同源资源: 某些 CDN 资源直接访问, 不走代理
625
+ # 非同源资源: CDN 图片/脚本/样式直接访问, 不走代理
623
626
  url_parsed = urlparse(url_val)
624
627
  if url_parsed.netloc and url_parsed.netloc != parsed.netloc:
625
- # 对于 src 属性的资源 (图片/脚本/样式/字体), 直接访问即可
626
628
  if attr_name in ('src', 'data-src', 'data-original'):
627
629
  return match.group(0)
628
- # 对于 href 属性的 CSS 字体等, 直接访问
629
- if attr_name == 'href' and any(url_val.endswith(ext) for ext in ('.css', '.woff', '.woff2', '.ttf', '.eot', '.svg')):
630
+ if attr_name == 'href' and any(url_val.lower().endswith(ext) for ext in ('.css', '.woff', '.woff2', '.ttf', '.eot', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico')):
630
631
  return match.group(0)
631
632
  # 其他外链 (导航链接) 走代理
632
633
  if attr_name == 'href':
633
- return f'{attr_name}={q_char}{proxy_base}{url_quote(url_val)}{q_char}'
634
+ return f'{attr_name}{eq}{q_open}{proxy_base}{url_quote(url_val)}{q_close}'
634
635
 
635
636
  # 同源资源走代理
636
- return f'{attr_name}={q_char}{proxy_base}{url_quote(url_val)}{q_char}'
637
+ return f'{attr_name}{eq}{q_open}{proxy_base}{url_quote(url_val)}{q_close}'
637
638
 
638
639
  # 匹配常见 URL 属性
640
+ # group1=属性名, group2="=", group3=开引号, group4=URL值, group5=闭引号
639
641
  url_pattern = re.compile(
640
642
  r'((?:src|href|action|data-src|data-original|poster|formaction|content|cite|background))'
641
643
  r'(\s*=\s*)'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.20.6",
3
+ "version": "1.20.8",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -280,16 +280,19 @@ class ApiServer:
280
280
  logger.info("通信管理器已热更新")
281
281
 
282
282
  def _hot_reload_chat_platforms(self):
283
- """热更新聊天平台:重新加载所有平台配置到 ChatBotManager"""
283
+ """热更新聊天平台:重新加载所有平台配置到 ChatBotManager
284
+
285
+ [v1.20.7] 修复: 调用 setup_platforms 时会自动停止已禁用的平台并启动新启用的平台。
286
+ """
284
287
  mgr = self.core.chat_manager
285
288
  if not mgr:
286
289
  return
287
- # 重新设置平台(会重建bot实例)
288
290
  platform_configs = self.core.config_mgr.config.chat_platforms
289
- # 停止旧的平台
290
- # 注意:这里不直接调用 async stop_all,仅更新配置引用
291
- # 实际的平台重启需要异步操作,在 handle_restart_platform 中处理
292
- mgr.setup_platforms(platform_configs, mgr._message_handler if hasattr(mgr, '_message_handler') else None)
291
+ # setup_platforms 会自动对比新旧配置,停止被移除/禁用的 bot,启动新启用的 bot
292
+ mgr.setup_platforms(
293
+ platform_configs,
294
+ mgr._message_handler if hasattr(mgr, '_message_handler') else None
295
+ )
293
296
  logger.info("聊天平台配置已热更新")
294
297
 
295
298
  def _setup_routes(self):