myagent-ai 1.9.7 → 1.9.9

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.
@@ -551,20 +551,20 @@ class MainAgent(BaseAgent):
551
551
  )
552
552
 
553
553
  # Step 9: 处理 askuser(askuser 非空时,finish 必为 true)
554
- if parsed.askuser:
555
- logger.info(f"[{task_id}] 需要询问用户: {parsed.askuser[:100]}")
556
- context.working_memory["final_response"] = parsed.askuser
557
- context.working_memory["ask_user"] = parsed.askuser
554
+ if parsed.ask_user:
555
+ logger.info(f"[{task_id}] 需要询问用户: {parsed.ask_user[:100]}")
556
+ context.working_memory["final_response"] = parsed.ask_user
557
+ context.working_memory["ask_user"] = parsed.ask_user
558
558
  await self._emit_v2_event(
559
559
  "v2_ask_user",
560
- {"question": parsed.askuser},
560
+ {"question": parsed.ask_user},
561
561
  stream_callback,
562
562
  )
563
563
  if self.memory:
564
564
  self.memory.add_short_term(
565
565
  session_id=context.session_id,
566
566
  role="assistant",
567
- content=parsed.askuser,
567
+ content=parsed.ask_user,
568
568
  )
569
569
  break
570
570
 
package/main.py CHANGED
@@ -1460,6 +1460,8 @@ def main():
1460
1460
  parser.add_argument("--tray", action="store_true", help="以系统托盘模式运行")
1461
1461
  parser.add_argument("--web", type=int, nargs="?", const=8767, default=None,
1462
1462
  help="启动管理后台 Web UI (可选端口,默认8767)")
1463
+ parser.add_argument("--server", action="store_true", help="API 服务模式 (等同于 --web,绑定 0.0.0.0)")
1464
+ parser.add_argument("--host", type=str, default=None, help="Web UI 绑定地址 (默认 127.0.0.1,容器部署用 0.0.0.0)")
1463
1465
  parser.add_argument("--port", type=int, default=8767, help="Web UI 端口")
1464
1466
  parser.add_argument("--autostart", action="store_true", help="设置开机自启")
1465
1467
  parser.add_argument("--no-autostart", action="store_true", help="取消开机自启")
@@ -1492,7 +1494,13 @@ def main():
1492
1494
  # ══════════════════════════════════════════════════════════
1493
1495
  # 首次运行: 引导配置 LLM API Key
1494
1496
  # ══════════════════════════════════════════════════════════
1497
+ # --server 等同于 --web,但默认绑定 0.0.0.0(用于容器/Docker 部署)
1498
+ if args.server and not args.web:
1499
+ args.web = args.port
1500
+ if args.server and not args.host:
1501
+ args.host = "0.0.0.0"
1495
1502
  web_port = args.web if args.web else args.port if args.tray else None
1503
+ web_host = args.host or "127.0.0.1"
1496
1504
  if not web_port:
1497
1505
  # 仅 CLI 模式才用命令行配置向导
1498
1506
  # Web 模式下由 chat.html 的 Web 向导处理 (检测 /api/setup/status)
@@ -1525,7 +1533,7 @@ def main():
1525
1533
  if web_port:
1526
1534
  from web.api_server import ApiServer
1527
1535
  api_server = ApiServer(app)
1528
- await api_server.start(port=web_port)
1536
+ await api_server.start(port=web_port, host=web_host)
1529
1537
  app.logger.info(f"管理后台: http://127.0.0.1:{web_port}/ui/")
1530
1538
 
1531
1539
  # 非 tray 模式(纯 web 模式)自动打开浏览器
@@ -1582,7 +1590,7 @@ def main():
1582
1590
  if web_port:
1583
1591
  from web.api_server import ApiServer
1584
1592
  api_server = ApiServer(app)
1585
- await api_server.start(port=web_port)
1593
+ await api_server.start(port=web_port, host=web_host)
1586
1594
  if app.chat_manager:
1587
1595
  asyncio.create_task(app.chat_manager.start_all())
1588
1596
  app._running_service = True
@@ -1614,7 +1622,7 @@ def main():
1614
1622
  if web_port:
1615
1623
  from web.api_server import ApiServer
1616
1624
  api_server = ApiServer(app)
1617
- await api_server.start(port=web_port)
1625
+ await api_server.start(port=web_port, host=web_host)
1618
1626
  app.logger.info(f"Web 服务已重启: http://127.0.0.1:{web_port}/ui/")
1619
1627
 
1620
1628
  # 4. 启动聊天平台
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.9.7",
4
- "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
3
+ "version": "1.9.9",
4
+ "description": "\u672c\u5730\u684c\u9762\u7aef\u6267\u884c\u578bAI\u52a9\u624b - Open Interpreter \u98ce\u683c | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
7
7
  "myagent-ai": "start.js"
@@ -65,4 +65,4 @@
65
65
  "departments/",
66
66
  "web/"
67
67
  ]
68
- }
68
+ }
package/web/api_server.py CHANGED
@@ -1070,6 +1070,75 @@ class ApiServer:
1070
1070
  tasks.append({"text": text, "status": status})
1071
1071
  return tasks if tasks else None
1072
1072
 
1073
+ def _parse_v2_task_plan(self, plan_text: str) -> list | None:
1074
+ """将 V2 结构化输出的 Markdown 任务计划解析为 [{text, status}] 格式。
1075
+
1076
+ V2 的 task_plan 使用 Markdown 列表格式:
1077
+ - [ ] 未完成任务 → pending
1078
+ - [x] 已完成任务 → done
1079
+ - [ ] 正在进行的任务 → running (如果含特定关键词)
1080
+ - [ ] 受阻的任务 → blocked (如果含特定关键词)
1081
+
1082
+ 同时支持有序列表(1. 2. 3.)和无序列表(- *),
1083
+ 以及混合格式如 [pending] [running] [done] 标签。
1084
+
1085
+ Returns:
1086
+ list | None: 成功返回任务列表,失败返回 None
1087
+ """
1088
+ import re
1089
+ if not plan_text or not plan_text.strip():
1090
+ return None
1091
+
1092
+ tasks = []
1093
+ lines = plan_text.strip().split('\n')
1094
+
1095
+ for line in lines:
1096
+ line = line.strip()
1097
+ if not line:
1098
+ continue
1099
+
1100
+ text = ""
1101
+ status = "pending"
1102
+
1103
+ # 格式1: - [x] 或 - [ ] (Markdown checkbox)
1104
+ m = re.match(r'^[\-\*\d\.]+\s*\[([xX\s])\]\s*(.+)$', line)
1105
+ if m:
1106
+ checkbox = m.group(1).strip()
1107
+ text = m.group(2).strip()
1108
+ status = "done" if checkbox.lower() == 'x' else "pending"
1109
+ else:
1110
+ # 格式2: - [pending] / [running] / [done] / [blocked] 文本
1111
+ m = re.match(r'^[\-\*\d\.]+\s*\[(pending|running|done|blocked)\]\s*(.+)$', line, re.IGNORECASE)
1112
+ if m:
1113
+ status = m.group(1).lower()
1114
+ text = m.group(2).strip()
1115
+ else:
1116
+ # 格式3: - 纯文本 或 数字. 纯文本(作为 pending)
1117
+ m = re.match(r'^[\-\*\d\.\)]+\s+(.+)$', line)
1118
+ if m:
1119
+ text = m.group(1).strip()
1120
+ # 跳过标题行(如 "## 任务计划")
1121
+ if text.startswith('#'):
1122
+ continue
1123
+ else:
1124
+ # 跳过非列表行
1125
+ continue
1126
+
1127
+ if not text:
1128
+ continue
1129
+
1130
+ # 智能检测状态关键词(即使没有 checkbox/标签)
1131
+ if status == "pending":
1132
+ lower_text = text.lower()
1133
+ if any(kw in lower_text for kw in ['正在', '进行中', '执行中', '处理中']):
1134
+ status = "running"
1135
+ elif any(kw in lower_text for kw in ['受阻', '阻塞', '等待', '暂停']):
1136
+ status = "blocked"
1137
+
1138
+ tasks.append({"text": text, "status": status})
1139
+
1140
+ return tasks if tasks else None
1141
+
1073
1142
  def _merge_task_list(self, session_id: str, llm_task_list: list) -> list:
1074
1143
  """
1075
1144
  合并 LLM 输出的 tasklist 与服务端存储的现有任务状态。
@@ -1142,40 +1211,60 @@ class ApiServer:
1142
1211
  return merged
1143
1212
 
1144
1213
  async def handle_get_task_plan(self, request):
1145
- """GET /api/task-plan?agent=default - Get task list from memory."""
1214
+ """GET /api/task-plan?agent=default&session=xxx - Get task list from memory.
1215
+
1216
+ 优先使用 session_id 查找(与聊天流存储键一致),
1217
+ 回退到 agent_path 查找(手动创建的任务)。
1218
+ """
1146
1219
  agent_path = request.query.get("agent", "default")
1220
+ session_id = request.query.get("session", "")
1221
+ # 优先按 session_id 查找(聊天流生成的任务列表存储在此键下)
1222
+ if session_id:
1223
+ tasks = self._task_list_store.get(session_id, [])
1224
+ if tasks:
1225
+ return web.json_response({"agent": agent_path, "session": session_id, "tasks": tasks})
1226
+ # 回退按 agent_path 查找(手动添加的任务)
1147
1227
  tasks = self._task_list_store.get(agent_path, [])
1148
- return web.json_response({"agent": agent_path, "tasks": tasks})
1228
+ return web.json_response({"agent": agent_path, "session": session_id, "tasks": tasks})
1149
1229
 
1150
1230
  async def handle_update_task_plan(self, request):
1151
1231
  """PUT /api/task-plan - Overwrite entire task list."""
1152
1232
  data = await request.json()
1153
1233
  agent_path = data.get("agent", "default")
1234
+ session_id = data.get("session", "")
1154
1235
  tasks = data.get("tasks", [])
1155
- self._task_list_store[agent_path] = tasks
1156
- return web.json_response({"ok": True, "agent": agent_path, "tasks": tasks})
1236
+ # 优先按 session_id 存储(与聊天流一致),回退到 agent_path
1237
+ store_key = session_id or agent_path
1238
+ self._task_list_store[store_key] = tasks
1239
+ return web.json_response({"ok": True, "agent": agent_path, "session": session_id, "tasks": tasks})
1157
1240
 
1158
1241
  async def handle_add_task_item(self, request):
1159
1242
  """POST /api/task-plan - Add a new task item."""
1160
1243
  data = await request.json()
1161
1244
  agent_path = data.get("agent", "default")
1245
+ session_id = data.get("session", "")
1162
1246
  text = data.get("text", "").strip()
1163
1247
  if not text:
1164
1248
  return web.json_response({"error": "text is required"}, status=400)
1165
- tasks = list(self._task_list_store.get(agent_path, []))
1249
+ # 优先按 session_id 查找,回退到 agent_path
1250
+ store_key = session_id or agent_path
1251
+ tasks = list(self._task_list_store.get(store_key, []))
1166
1252
  tasks.append({"text": text, "status": "pending"})
1167
- self._task_list_store[agent_path] = tasks
1168
- return web.json_response({"ok": True, "tasks": tasks, "added": len(tasks) - 1})
1253
+ self._task_list_store[store_key] = tasks
1254
+ return web.json_response({"ok": True, "tasks": tasks, "added": len(tasks) - 1, "session": session_id})
1169
1255
 
1170
1256
  async def handle_delete_task_item(self, request):
1171
1257
  """DELETE /api/task-plan/{idx} - Delete task by index."""
1172
1258
  idx = int(request.match_info["idx"])
1173
1259
  agent_path = request.query.get("agent", "default")
1174
- tasks = list(self._task_list_store.get(agent_path, []))
1260
+ session_id = request.query.get("session", "")
1261
+ # 优先按 session_id 查找,回退到 agent_path
1262
+ store_key = session_id or agent_path
1263
+ tasks = list(self._task_list_store.get(store_key, []))
1175
1264
  if 0 <= idx < len(tasks):
1176
1265
  tasks.pop(idx)
1177
- self._task_list_store[agent_path] = tasks
1178
- return web.json_response({"ok": True, "tasks": tasks})
1266
+ self._task_list_store[store_key] = tasks
1267
+ return web.json_response({"ok": True, "tasks": tasks, "session": session_id})
1179
1268
 
1180
1269
  async def handle_shutdown(self, request):
1181
1270
  self.core._running = False
@@ -3042,9 +3131,30 @@ class ApiServer:
3042
3131
 
3043
3132
  # 收集 V2 输出中的 reasoning 文本作为最终响应
3044
3133
  _v2_reasoning_parts = []
3134
+ # V2 模式下追踪最新 task_plan(Markdown 格式)
3135
+ _v2_latest_task_plan = ""
3045
3136
 
3046
3137
  async def _v2_collecting_callback(event: dict):
3047
- """收集 reasoning 内容,同时转发所有事件"""
3138
+ """收集 reasoning 内容 + 同步 task_plan 到 _task_list_store,同时转发所有事件"""
3139
+ # ── 拦截 v2_task_plan 事件:解析 Markdown 并同步到 _task_list_store ──
3140
+ if event.get("type") == "v2_task_plan":
3141
+ nonlocal _v2_latest_task_plan
3142
+ plan_text = event.get("plan", "")
3143
+ if plan_text:
3144
+ _v2_latest_task_plan = plan_text
3145
+ # 解析 Markdown 任务列表为 [{text, status}] 格式
3146
+ parsed_tasks = self._parse_v2_task_plan(plan_text)
3147
+ if parsed_tasks:
3148
+ # 合并与存储
3149
+ merged = self._merge_task_list(session_id, parsed_tasks)
3150
+ self._task_list_store[session_id] = merged
3151
+ # 额外发送 task_list_update 事件(前端侧边栏 task panel 依赖此事件)
3152
+ try:
3153
+ await _write_sse({"type": "task_list_update", "tasks": merged})
3154
+ except Exception:
3155
+ pass
3156
+
3157
+ # 转发所有事件到前端
3048
3158
  await _write_sse(event)
3049
3159
  if event.get("type") == "v2_reasoning":
3050
3160
  content = event.get("content", "")
@@ -3061,6 +3171,15 @@ class ApiServer:
3061
3171
  stream_response=stream_response,
3062
3172
  )
3063
3173
 
3174
+ # V2 结束后:如果 task_list_store 中有任务,确保最终推送一次
3175
+ if chat_mode == "exec" and session_id in self._task_list_store:
3176
+ final_tasks = self._task_list_store[session_id]
3177
+ if final_tasks:
3178
+ try:
3179
+ await _write_sse({"type": "task_list_update", "tasks": final_tasks})
3180
+ except Exception:
3181
+ pass
3182
+
3064
3183
  # 返回最终响应
3065
3184
  final_response = v2_context.working_memory.get("final_response", "")
3066
3185
  # 优先使用 v2_reasoning 流式推送的文本(用户实际看到的内容),
@@ -4724,7 +4843,7 @@ class ApiServer:
4724
4843
  ok = mgr.clear_messages(gid)
4725
4844
  return web.json_response({"ok": ok})
4726
4845
 
4727
- async def start(self, port: int = 8767):
4846
+ async def start(self, port: int = 8767, host: str = "127.0.0.1"):
4728
4847
  # 加载禁用技能列表
4729
4848
  self._load_disabled_skills()
4730
4849
 
@@ -4744,9 +4863,9 @@ class ApiServer:
4744
4863
 
4745
4864
  self._runner = web.AppRunner(self.app)
4746
4865
  await self._runner.setup()
4747
- site = web.TCPSite(self._runner, "127.0.0.1", port)
4866
+ site = web.TCPSite(self._runner, host, port)
4748
4867
  await site.start()
4749
- logger.info(f"管理后台: http://127.0.0.1:{port}/ui/")
4868
+ logger.info(f"管理后台: http://{host}:{port}/ui/")
4750
4869
 
4751
4870
  async def stop(self):
4752
4871
  if self._runner: await self._runner.cleanup()
@@ -582,7 +582,10 @@ function hasTaskListChanged() {
582
582
  async function loadTaskPlan() {
583
583
  try {
584
584
  var agent = state.activeAgent || 'default';
585
- var data = await api('/api/task-plan?agent=' + encodeURIComponent(agent));
585
+ var session = state.activeSessionId || '';
586
+ var url = '/api/task-plan?agent=' + encodeURIComponent(agent);
587
+ if (session) url += '&session=' + encodeURIComponent(session);
588
+ var data = await api(url);
586
589
  state.taskItems = data.tasks || [];
587
590
  renderTaskList();
588
591
  // Auto-show and expand task panel if tasks exist
@@ -653,9 +656,10 @@ async function toggleTaskDone(idx) {
653
656
  }
654
657
  try {
655
658
  var agent = state.activeAgent || 'default';
659
+ var session = state.activeSessionId || '';
656
660
  await api('/api/task-plan', {
657
661
  method: 'PUT',
658
- body: JSON.stringify({agent: agent, tasks: state.taskItems})
662
+ body: JSON.stringify({agent: agent, session: session, tasks: state.taskItems})
659
663
  });
660
664
  renderTaskList();
661
665
  } catch (e) {
@@ -677,9 +681,10 @@ async function addTaskItem() {
677
681
  input.value = '';
678
682
  try {
679
683
  var agent = state.activeAgent || 'default';
684
+ var session = state.activeSessionId || '';
680
685
  var data = await api('/api/task-plan', {
681
686
  method: 'POST',
682
- body: JSON.stringify({agent: agent, text: text})
687
+ body: JSON.stringify({agent: agent, session: session, text: text})
683
688
  });
684
689
  state.taskItems = data.tasks || [];
685
690
  renderTaskList();
@@ -693,7 +698,10 @@ async function deleteTaskItem(idx) {
693
698
  if (idx < 0 || idx >= state.taskItems.length) return;
694
699
  try {
695
700
  var agent = state.activeAgent || 'default';
696
- var data = await api('/api/task-plan/' + idx + '?agent=' + encodeURIComponent(agent), {
701
+ var session = state.activeSessionId || '';
702
+ var url = '/api/task-plan/' + idx + '?agent=' + encodeURIComponent(agent);
703
+ if (session) url += '&session=' + encodeURIComponent(session);
704
+ var data = await api(url, {
697
705
  method: 'DELETE'
698
706
  });
699
707
  state.taskItems = data.tasks || [];