myagent-ai 1.8.0 → 1.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.8.0",
3
+ "version": "1.8.2",
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
@@ -153,7 +153,7 @@ class ApiServer:
153
153
  # 消息队列(用于存放待执行的消息,key: session_id)
154
154
  self._msg_queues: Dict[str, List[Dict]] = {}
155
155
  # 任务列表内存存储(exec 模式,替代 task.md)
156
- self._task_list_store: dict[str, list] = {} # agent_path -> [{text, status}]
156
+ self._task_list_store: dict[str, list] = {} # session_id -> [{text, status}] (per-session, not per-agent)
157
157
  self._setup_routes()
158
158
  self._runner: Optional[web.AppRunner] = None
159
159
 
@@ -264,9 +264,13 @@ class ApiServer:
264
264
  # ── 会话管理 ──
265
265
  r.add_get("/api/sessions", self.handle_list_sessions)
266
266
  r.add_get("/api/sessions/{sid}/messages", self.handle_get_messages)
267
+ r.add_get("/api/session/messages", self.handle_get_messages_query) # query param version (supports / in sid)
267
268
  r.add_delete("/api/sessions/{sid}", self.handle_delete_session)
269
+ r.add_delete("/api/session", self.handle_delete_session_query) # query param version
268
270
  r.add_delete("/api/sessions/{sid}/messages", self.handle_clear_session_messages)
271
+ r.add_delete("/api/session/messages", self.handle_clear_session_messages_query) # query param version
269
272
  r.add_put("/api/sessions/{sid}/rename", self.handle_rename_session)
273
+ r.add_put("/api/session/rename", self.handle_rename_session_query) # query param version
270
274
  r.add_get("/api/memory/stats", self.handle_memory_stats)
271
275
  r.add_get("/api/memory/search", self.handle_memory_search)
272
276
  r.add_get("/api/memory/long-term", self.handle_list_long_term)
@@ -491,7 +495,7 @@ class ApiServer:
491
495
  clean_message, agent_system_prompt = self._build_agent_chat_context(agent_path, agent_cfg, message)
492
496
 
493
497
  # ── 执行模式: 将任务规划上下文注入到 system_prompt(而非用户消息)──
494
- task_plan_context = self._build_task_plan_context(agent_path, chat_mode, message)
498
+ task_plan_context = self._build_task_plan_context(agent_path, chat_mode, message, session_id=session_id)
495
499
  if task_plan_context:
496
500
  agent_system_prompt += "\n\n## 任务规划\n" + task_plan_context
497
501
 
@@ -518,7 +522,7 @@ class ApiServer:
518
522
  try:
519
523
  tl = self._extract_task_list_json(response)
520
524
  if tl is not None:
521
- self._task_list_store[agent_path] = tl
525
+ self._task_list_store[session_id] = tl
522
526
  except Exception as tp_err:
523
527
  logger.warning(f"任务列表更新失败: {tp_err}")
524
528
 
@@ -589,7 +593,7 @@ class ApiServer:
589
593
  model_chain = self._build_model_chain(agent_cfg, agent_path)
590
594
 
591
595
  # Build context with task plan injection into system_prompt (NOT user message)
592
- task_plan_context = self._build_task_plan_context(agent_path, chat_mode, message)
596
+ task_plan_context = self._build_task_plan_context(agent_path, chat_mode, message, session_id=session_id)
593
597
  clean_message, agent_system_prompt = self._build_agent_chat_context(agent_path, agent_cfg, message)
594
598
  if task_plan_context:
595
599
  agent_system_prompt += "\n\n## 任务规划\n" + task_plan_context
@@ -627,7 +631,7 @@ class ApiServer:
627
631
  try:
628
632
  tl = self._extract_task_list_json(full_response)
629
633
  if tl is not None:
630
- self._task_list_store[agent_path] = tl
634
+ self._task_list_store[session_id] = tl
631
635
  await response.write(("data: " + json.dumps({"type": "task_list_update", "tasks": tl}) + "\n\n").encode())
632
636
  except Exception:
633
637
  pass
@@ -678,7 +682,26 @@ class ApiServer:
678
682
 
679
683
  except Exception as e:
680
684
  logger.error(f"Stream chat error: {e}", exc_info=True)
681
- await response.write(("data: " + json.dumps({"type": "error", "error": str(e)}) + "\n\n").encode())
685
+ # ── 兜底保存:流式输出被中断时,确保已推送的内容不丢失 ──
686
+ # 场景:用户在流式输出过程中刷新页面,SSE 连接断开,_stream_process_message
687
+ # 中的保存逻辑未执行,导致 user 消息在但 assistant 消息丢失
688
+ try:
689
+ if self.core.memory and session_id:
690
+ # 检查最后一条消息是否为 user(说明 assistant 回复未保存)
691
+ conv = self.core.memory.get_conversation(session_id, limit=5)
692
+ if conv and conv[-1].role == "user":
693
+ logger.info(f"[{session_id}] 流式中断兜底保存:补充一条中断提示")
694
+ self.core.memory.add_short_term(
695
+ session_id=session_id,
696
+ role="assistant",
697
+ content="⚠️ [连接中断] 上次回复因页面刷新被中断,部分内容可能丢失。请重新发送指令继续。",
698
+ )
699
+ except Exception as save_err:
700
+ logger.warning(f"兜底保存失败: {save_err}")
701
+ try:
702
+ await response.write(("data: " + json.dumps({"type": "error", "error": str(e)}) + "\n\n").encode())
703
+ except Exception:
704
+ pass
682
705
  finally:
683
706
  # Release execution lock
684
707
  if needs_lock_check and self._execution_lock["locked_by"] == agent_path:
@@ -740,7 +763,7 @@ class ApiServer:
740
763
 
741
764
  return response
742
765
 
743
- def _build_task_plan_context(self, agent_path: str, chat_mode: str, original_message: str) -> str:
766
+ def _build_task_plan_context(self, agent_path: str, chat_mode: str, original_message: str, session_id: str = "") -> str:
744
767
  """构建任务规划上下文(仅 exec 模式,注入到 system_prompt 中)"""
745
768
  if chat_mode != "exec":
746
769
  return ""
@@ -757,10 +780,12 @@ class ApiServer:
757
780
  " - 执行完一个操作后停下来,等待结果反馈后再决定下一步\n"
758
781
  " - 不要一次性执行多个操作\n"
759
782
  "3. **回复格式**:先写纯文本分析/总结 → 再写 ```tasklist``` 更新进度 → 最后写 ```action``` 执行操作(如有)\n"
783
+ "4. **任务完成**:当所有步骤都标记为 done 时,用 ```action``` 输出 {\"type\": \"final_answer\", \"content\": \"...\"} 结束任务。\n"
760
784
  )
761
785
 
762
- # 从内存读取当前任务列表
763
- tasks = self._task_list_store.get(agent_path, [])
786
+ # 从内存读取当前任务列表(按 session 隔离)
787
+ store_key = session_id or agent_path
788
+ tasks = self._task_list_store.get(store_key, [])
764
789
  if not tasks:
765
790
  return base_instruction + "\n## 当前状态\n暂无任务计划。请先分析用户需求,拆分为具体步骤,然后用 ```tasklist``` 输出计划。"
766
791
 
@@ -768,6 +793,10 @@ class ApiServer:
768
793
  done = [f" - ✅ {t['text']}" for t in tasks if t.get("status") == "done"]
769
794
  running = [f" - 🔄 {t['text']}" for t in tasks if t.get("status") == "running"]
770
795
 
796
+ # 如果所有任务都已完成,不再注入任务上下文,让 LLM 自然回复
797
+ if not pending and not running:
798
+ return ""
799
+
771
800
  context = base_instruction + "\n## 当前任务进度\n"
772
801
  if done:
773
802
  context += "已完成:\n" + "\n".join(done) + "\n"
@@ -780,6 +809,7 @@ class ApiServer:
780
809
  "1. 用纯文本简要分析当前进展\n"
781
810
  "2. 用 ```tasklist``` 更新任务进度(标记已完成的步骤为 done,标记当前步骤为 running)\n"
782
811
  "3. 用 ```action``` 执行下一个待执行步骤(每次只执行一个操作)\n"
812
+ "4. 如果所有步骤已完成,用 {\"type\": \"final_answer\", \"content\": \"总结\"} 结束\n"
783
813
  )
784
814
  return context
785
815
 
@@ -1823,6 +1853,15 @@ class ApiServer:
1823
1853
  entries = self.core.memory.get_conversation(sid, limit=100)
1824
1854
  return web.json_response([{"role": e.role, "content": e.content, "time": e.created_at} for e in entries])
1825
1855
 
1856
+ async def handle_get_messages_query(self, request):
1857
+ """GET /api/session/messages?sid=... - 通过 query 参数获取会话消息(支持 session_id 中包含 /)"""
1858
+ sid = request.query.get("sid", "")
1859
+ if not sid:
1860
+ return web.json_response([])
1861
+ if not self.core.memory: return web.json_response([])
1862
+ entries = self.core.memory.get_conversation(sid, limit=100)
1863
+ return web.json_response([{"role": e.role, "content": e.content, "time": e.created_at} for e in entries])
1864
+
1826
1865
  async def handle_delete_session(self, request):
1827
1866
  """DELETE /api/sessions/{sid} - 彻底删除会话(所有记忆)"""
1828
1867
  sid = request.match_info["sid"]
@@ -1832,6 +1871,17 @@ class ApiServer:
1832
1871
  logger.info(f"会话 {sid} 已删除,共清除 {count} 条记忆")
1833
1872
  return web.json_response({"ok": True, "deleted": True})
1834
1873
 
1874
+ async def handle_delete_session_query(self, request):
1875
+ """DELETE /api/session?sid=... - 通过 query 参数删除会话(支持 session_id 中包含 /)"""
1876
+ sid = request.query.get("sid", "")
1877
+ if not sid:
1878
+ return web.json_response({"ok": False, "error": "missing sid"}, status=400)
1879
+ logger.info(f"删除会话: {sid}")
1880
+ if self.core.memory:
1881
+ count = self.core.memory.delete_session(sid)
1882
+ logger.info(f"会话 {sid} 已删除,共清除 {count} 条记忆")
1883
+ return web.json_response({"ok": True, "deleted": True})
1884
+
1835
1885
  async def handle_clear_session_messages(self, request):
1836
1886
  """DELETE /api/sessions/{sid}/messages - 仅清空会话对话历史(保留会话本身)"""
1837
1887
  sid = request.match_info["sid"]
@@ -1841,6 +1891,17 @@ class ApiServer:
1841
1891
  logger.info(f"会话 {sid} 消息已清空,共清除 {count} 条消息")
1842
1892
  return web.json_response({"ok": True, "cleared": True})
1843
1893
 
1894
+ async def handle_clear_session_messages_query(self, request):
1895
+ """DELETE /api/session/messages?sid=... - 通过 query 参数清空会话消息"""
1896
+ sid = request.query.get("sid", "")
1897
+ if not sid:
1898
+ return web.json_response({"ok": False, "error": "missing sid"}, status=400)
1899
+ logger.info(f"清空会话消息: {sid}")
1900
+ if self.core.memory:
1901
+ count = self.core.memory.clear_conversation(sid)
1902
+ logger.info(f"会话 {sid} 消息已清空,共清除 {count} 条消息")
1903
+ return web.json_response({"ok": True, "cleared": True})
1904
+
1844
1905
  async def handle_rename_session(self, request):
1845
1906
  """PUT /api/sessions/{sid}/rename - 重命名会话"""
1846
1907
  sid = request.match_info["sid"]
@@ -1857,6 +1918,24 @@ class ApiServer:
1857
1918
  self.core.memory.rename_session(sid, new_name)
1858
1919
  return web.json_response({"ok": True, "name": new_name})
1859
1920
 
1921
+ async def handle_rename_session_query(self, request):
1922
+ """PUT /api/session/rename?sid=... - 通过 query 参数重命名会话"""
1923
+ sid = request.query.get("sid", "")
1924
+ if not sid:
1925
+ return web.json_response({"ok": False, "error": "missing sid"}, status=400)
1926
+ try:
1927
+ body = await request.json()
1928
+ except Exception:
1929
+ body = {}
1930
+ new_name = (body.get("name") or "").strip()
1931
+ if not new_name:
1932
+ return web.json_response({"ok": False, "error": "名称不能为空"}, status=400)
1933
+ if len(new_name) > 100:
1934
+ return web.json_response({"ok": False, "error": "名称不能超过100个字符"}, status=400)
1935
+ if self.core.memory:
1936
+ self.core.memory.rename_session(sid, new_name)
1937
+ return web.json_response({"ok": True, "name": new_name})
1938
+
1860
1939
  # --- Memory ---
1861
1940
  async def handle_memory_stats(self, request):
1862
1941
  return web.json_response(self.core.memory.get_stats() if self.core.memory else {})
@@ -2625,7 +2704,7 @@ class ApiServer:
2625
2704
  iteration = 0
2626
2705
  # 追踪连续无 action 迭代次数,防止无限重新提示
2627
2706
  _consecutive_no_action = 0
2628
- _MAX_NO_ACTION_RETRIES = 5 # 提高重试次数,给 LLM 更多机会完成剩余任务
2707
+ _MAX_NO_ACTION_RETRIES = 2 # 限制重试次数,避免重复循环规划
2629
2708
  # ── 追踪所有流式推送的纯文本(用于刷新后恢复) ──
2630
2709
  _all_streamed_text_parts = [] # 每轮迭代推送的纯文本片段
2631
2710
 
@@ -2635,7 +2714,7 @@ class ApiServer:
2635
2714
  # ── 执行模式:每轮迭代前刷新任务进度上下文 ──
2636
2715
  # 这样 LLM 在每次调用时都能看到最新的任务列表状态
2637
2716
  if chat_mode == "exec":
2638
- fresh_task_context = self._build_task_plan_context(agent_path, chat_mode, user_message)
2717
+ fresh_task_context = self._build_task_plan_context(agent_path, chat_mode, user_message, session_id=session_id)
2639
2718
  if fresh_task_context:
2640
2719
  # 替换掉之前的任务规划上下文(保留非任务规划部分)
2641
2720
  base_prompt = context.metadata.get("agent_override_prompt", "")
@@ -2828,8 +2907,8 @@ class ApiServer:
2828
2907
  if chat_mode == "exec":
2829
2908
  task_list = self._extract_task_list_json(content)
2830
2909
  if task_list is not None:
2831
- # 保存到内存(替代 task.md)
2832
- self._task_list_store[agent_path] = task_list
2910
+ # 保存到内存(按 session 隔离,避免跨会话任务泄漏)
2911
+ self._task_list_store[session_id] = task_list
2833
2912
  await _write_sse({"type": "task_list_update", "tasks": task_list})
2834
2913
 
2835
2914
  # ── Check for tool calls (OpenAI function calling) ──
@@ -2938,6 +3017,29 @@ class ApiServer:
2938
3017
 
2939
3018
  result_summary = agent._summarize_action_results(results)
2940
3019
 
3020
+ # ── 自动更新任务状态:将 running 任务标记为 done ──
3021
+ # 修复:执行成功后不依赖 LLM 下次输出才更新状态,避免重复执行同一任务
3022
+ if chat_mode == "exec":
3023
+ current_tasks = list(self._task_list_store.get(session_id, []))
3024
+ updated = False
3025
+ if results and results[0].get("success", False):
3026
+ # 找到第一个 running 的任务,标记为 done
3027
+ for t in current_tasks:
3028
+ if t.get("status") == "running":
3029
+ t["status"] = "done"
3030
+ updated = True
3031
+ break
3032
+ # 如果没有 running 的,找第一个 pending 的标记为 done(兼容 LLM 未设置 running 的情况)
3033
+ if not updated:
3034
+ for t in current_tasks:
3035
+ if t.get("status") == "pending":
3036
+ t["status"] = "done"
3037
+ updated = True
3038
+ break
3039
+ if updated:
3040
+ self._task_list_store[session_id] = current_tasks
3041
+ await _write_sse({"type": "task_list_update", "tasks": current_tasks})
3042
+
2941
3043
  # Handle timeout diagnostics (same as _process_inner)
2942
3044
  has_timeout = any(r.get("timed_out") for r in results)
2943
3045
  timeout_detail = ""
@@ -3018,7 +3120,7 @@ class ApiServer:
3018
3120
  # 导致循环在此处 break,任务未完成就停止。
3019
3121
  # 修复:检查任务列表中是否还有未完成步骤,如果有则重新提示 LLM 继续。
3020
3122
  if chat_mode == "exec":
3021
- current_tasks = self._task_list_store.get(agent_path, [])
3123
+ current_tasks = self._task_list_store.get(session_id, [])
3022
3124
  pending_count = sum(
3023
3125
  1 for t in current_tasks
3024
3126
  if t.get("status") in ("pending", "running", "blocked")
@@ -3073,6 +3175,7 @@ class ApiServer:
3073
3175
 
3074
3176
  # Save assistant response to memory
3075
3177
  # ── 优先使用流式累积文本(包含所有迭代的纯文本),回退到 final_response ──
3178
+ # 注意:即使客户端中途断开(刷新页面),也要保存已有内容
3076
3179
  saved_response = final_response
3077
3180
  if not saved_response and _all_streamed_text_parts:
3078
3181
  saved_response = "\n\n".join(p for p in _all_streamed_text_parts if p.strip())
@@ -1712,13 +1712,23 @@ input,textarea,select{font:inherit}
1712
1712
  .agent-panel{
1713
1713
  position:fixed;
1714
1714
  right:0;top:0;
1715
- width:85vw;max-width:340px;
1715
+ width:85vw!important;max-width:340px;min-width:260px!important;
1716
1716
  height:100vh;
1717
1717
  z-index:50;
1718
1718
  transform:translateX(100%);
1719
1719
  transition:transform .3s cubic-bezier(.4,0,.2,1);
1720
1720
  box-shadow:none;
1721
1721
  }
1722
+ /* Override desktop collapsed state on mobile */
1723
+ .agent-panel.collapsed{
1724
+ width:85vw!important;min-width:260px!important;
1725
+ }
1726
+ .agent-panel.collapsed .agent-panel-header h3,
1727
+ .agent-panel.collapsed .agent-list,
1728
+ .agent-panel.collapsed .agent-create-btn,
1729
+ .agent-panel.collapsed .agent-collapse-btn span,
1730
+ .agent-panel.collapsed .agent-panel-footer{display:flex!important}
1731
+ .agent-panel.collapsed .agent-collapse-btn span{display:inline!important}
1722
1732
  .agent-panel.mobile-open{
1723
1733
  transform:translateX(0);
1724
1734
  box-shadow:-4px 0 24px rgba(0,0,0,.15);
@@ -1753,8 +1763,15 @@ input,textarea,select{font:inherit}
1753
1763
  .messages-inner{
1754
1764
  max-width:100%;
1755
1765
  }
1756
- .message-bubble{
1757
- max-width:88%;
1766
+ .message-row{gap:6px}
1767
+ .message-avatar{
1768
+ width:28px;height:28px;font-size:12px;
1769
+ }
1770
+ .message-row.assistant .message-bubble{
1771
+ max-width:100%;
1772
+ }
1773
+ .message-row.user .message-bubble{
1774
+ max-width:80%;
1758
1775
  }
1759
1776
  .message-bubble pre{
1760
1777
  font-size:12px;
@@ -162,6 +162,7 @@ const state = {
162
162
  abortController: null,
163
163
  execTimerInterval: null, // polling interval ID for execution progress
164
164
  systemStatus: null,
165
+ _sessionLoadSeq: 0, // 防止 selectSession 并发竞态的序号
165
166
  // ── Execution mode escalation ──
166
167
  escalated: false, // 当前会话是否临时提权到 local
167
168
  execLockInfo: null, // 全局锁信息 {locked, locked_by, locked_at}
@@ -1103,6 +1104,8 @@ async function selectAgent(agentPath) {
1103
1104
  StatePersistence.save('activeAgent', agentPath);
1104
1105
  state.activeSessionId = null;
1105
1106
  state.messages = [];
1107
+ // 递增序号,使任何进行中的 selectSession 请求失效
1108
+ state._sessionLoadSeq++;
1106
1109
  var parts = agentPath.split('/');
1107
1110
  var cumPath = '';
1108
1111
  for (var i = 0; i < parts.length; i++) {
@@ -1528,7 +1531,7 @@ async function clearSessionFromMenu(id) {
1528
1531
  if (!id || id === '__new__') return;
1529
1532
  if (!confirm('确定清空此对话的消息?会话本身不会被删除。')) return;
1530
1533
  try {
1531
- const resp = await fetch(`/api/sessions/${encodeURIComponent(id)}/messages`, {
1534
+ const resp = await fetch(`/api/session/messages?sid=${encodeURIComponent(id)}`, {
1532
1535
  method: 'DELETE',
1533
1536
  headers: { 'Content-Type': 'application/json' }
1534
1537
  });
@@ -1631,6 +1634,8 @@ async function selectSession(id) {
1631
1634
  document.getElementById('stopBtn').style.display = 'none';
1632
1635
 
1633
1636
  state.activeSessionId = id;
1637
+ // 递增序号,用于检测并发竞态
1638
+ const mySeq = ++state._sessionLoadSeq;
1634
1639
  const session = state.sessions.find(s => s.id === id);
1635
1640
  document.getElementById('headerTitle').textContent = session ? session.name : formatSessionName(id);
1636
1641
  document.getElementById('welcomeCard').style.display = 'none';
@@ -1638,13 +1643,16 @@ async function selectSession(id) {
1638
1643
 
1639
1644
  // Load messages
1640
1645
  try {
1641
- const data = await api(`/api/sessions/${encodeURIComponent(id)}/messages`);
1646
+ const data = await api(`/api/session/messages?sid=${encodeURIComponent(id)}`);
1647
+ // 竞态保护:如果在此请求期间又切换了其他会话,丢弃过期结果
1648
+ if (mySeq !== state._sessionLoadSeq) return;
1642
1649
  state.messages = (data || []).map(m => ({
1643
1650
  role: m.role,
1644
1651
  content: m.content,
1645
1652
  time: m.time || '',
1646
1653
  }));
1647
1654
  } catch (e) {
1655
+ if (mySeq !== state._sessionLoadSeq) return;
1648
1656
  state.messages = [];
1649
1657
  toast('加载消息失败', 'error');
1650
1658
  }
@@ -1656,7 +1664,7 @@ async function selectSession(id) {
1656
1664
  async function deleteSession(id) {
1657
1665
  if (!id || id === '__new__') return;
1658
1666
  try {
1659
- const resp = await fetch(`/api/sessions/${encodeURIComponent(id)}`, {
1667
+ const resp = await fetch(`/api/session?sid=${encodeURIComponent(id)}`, {
1660
1668
  method: 'DELETE',
1661
1669
  headers: { 'Content-Type': 'application/json' }
1662
1670
  });
@@ -1721,7 +1729,7 @@ async function finishRenameSession(id, newName) {
1721
1729
  return;
1722
1730
  }
1723
1731
  try {
1724
- await api(`/api/sessions/${encodeURIComponent(id)}/rename`, {
1732
+ await api(`/api/session/rename?sid=${encodeURIComponent(id)}`, {
1725
1733
  method: 'PUT',
1726
1734
  body: JSON.stringify({ name: newName }),
1727
1735
  });
@@ -1753,7 +1761,7 @@ async function clearCurrentChat() {
1753
1761
  }
1754
1762
  if (!confirm('确定清空当前对话消息?会话本身不会被删除。')) return;
1755
1763
  try {
1756
- const resp = await fetch(`/api/sessions/${encodeURIComponent(state.activeSessionId)}/messages`, {
1764
+ const resp = await fetch(`/api/session/messages?sid=${encodeURIComponent(state.activeSessionId)}`, {
1757
1765
  method: 'DELETE',
1758
1766
  headers: { 'Content-Type': 'application/json' }
1759
1767
  });