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.
- package/agents/main_agent.py +6 -6
- package/main.py +11 -3
- package/package.json +3 -3
- package/web/api_server.py +133 -14
- package/web/ui/chat/chat_main.js +12 -4
package/agents/main_agent.py
CHANGED
|
@@ -551,20 +551,20 @@ class MainAgent(BaseAgent):
|
|
|
551
551
|
)
|
|
552
552
|
|
|
553
553
|
# Step 9: 处理 askuser(askuser 非空时,finish 必为 true)
|
|
554
|
-
if parsed.
|
|
555
|
-
logger.info(f"[{task_id}] 需要询问用户: {parsed.
|
|
556
|
-
context.working_memory["final_response"] = parsed.
|
|
557
|
-
context.working_memory["ask_user"] = parsed.
|
|
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.
|
|
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.
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
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[
|
|
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,
|
|
4866
|
+
site = web.TCPSite(self._runner, host, port)
|
|
4748
4867
|
await site.start()
|
|
4749
|
-
logger.info(f"管理后台: http://
|
|
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()
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -582,7 +582,10 @@ function hasTaskListChanged() {
|
|
|
582
582
|
async function loadTaskPlan() {
|
|
583
583
|
try {
|
|
584
584
|
var agent = state.activeAgent || 'default';
|
|
585
|
-
var
|
|
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
|
|
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 || [];
|