From b93e85a1a08cf7500153458a693aa85d43b9fb40 Mon Sep 17 00:00:00 2001 From: ylytdeng Date: Sun, 17 May 2026 18:35:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(monitor=5Fweb):=20=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E6=A8=A1=E6=80=81=E6=A1=86=20=E2=80=94=20?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=B8=80=E7=82=B9=E5=B0=B1=E8=B7=91=E5=85=A8?= =?UTF-8?q?=E9=87=8F=20(closes=20#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 痛点 之前 Web UI 工具箱里 "③ 导出聊天" / "⑦ 企业微信导出" 一点就跑全量。 用户 3142 个个人微信会话 + 14 个企微会话, 全量导出几个 GB JSON 几小时, 还没法只导某几个群。 ## 设计 点击导出按钮 → 弹模态框选会话 + 格式 → 确认后才跑。 ``` ┌────────────────────────────────────────┐ │ 导出个人微信聊天 [×] │ ├────────────────────────────────────────┤ │ 🔍 按名字 / wxid 搜索... │ │ ┌──────────────────────────────────┐ │ │ │ ☐ [群] 交易所消息 2026-05-14│ │ │ │ ☐ [单聊] 张三 2026-05-13│ │ │ │ ☐ [公众号] xxx日报 2026-05-10│ │ │ │ ... │ │ │ └──────────────────────────────────┘ │ │ [全选] [清空] [选最近30天活跃] 已选 N│ │ │ │ 格式 (仅企微) ☑CSV ☐HTML ☐JSON │ │ │ │ [取消] [确认导出 →] │ └────────────────────────────────────────┘ ``` ## 实现拆解 ### Backend 1. **GET /api/sessions?source=wechat|wxwork** — 列会话 - wechat: 从 decrypted/session/session.db SessionTable 读, 拼合 contact.db 的 nick_name/remark, type 分群/单聊/公众号 - wxwork: 从 wxwork_decrypted/session.db conversation_table 读, id 前缀分 R/S/E/Y → 群/单聊/外部/其他 - 按 last_ts 降序 2. **TOOL_TASKS schema 加 build_steps + needs_modal** - 旧固定任务: `steps: [cmd, ...]` - 新动态任务: `build_steps: fn(users, formats) → [cmd, ...]` - export_all / wxwork_export 用 build_steps + needs_modal 标记 3. **_run_tool_task 接收 args**, 优先调 build_steps 生成 cmd 4. **POST /api/tool body 接收 args**: `{task, args: {users:[...], formats:[...]}}` ### CLI 脚本 - `export_all_chats.py` 加 `--users wxid1,wxid2` (alt: env WECHAT_EXPORT_USERS), 在加载完 sessions 后做白名单过滤, 空集报错退出 - `export_wxwork_messages.py` 原本就支持 `--conversation` (multi-arg) + `--formats csv,html,json`, 不动它, monitor_web 拼 argv 即可 ### Frontend - HTML 加 `#exportModal` 模态框骨架 (overlay + dialog + search + list + format checkboxes + footer buttons) - CSS .modal-* 一套 (用 design tokens, 跟整体暗色风格统一) - JS: - NEEDS_MODAL = {export_all: 'export_wechat', wxwork_export: 'export_wxwork'} - runTool 拦截这两个 task → openExportModal - openExportModal fetch /api/sessions → renderSessions (复选框列表) - filterSessions 实时搜索过滤 - selectAllSessions / selectRecentSessions(30) 批量选择 - confirmExport 收集 selected usernames + formats → runToolWithArgs - runTool 拆成 runTool (entry, 拦截/取消) + runToolWithArgs (实际跑) ## 实测验证 ``` $ curl -s "http://localhost:5678/api/sessions?source=wxwork" [{"username": "R:358645240322", "name": "交易所消息", "type": "群", "last_ts": 1778658597, ...}, {"username": "E:10223", "name": "E:10223", "type": "外部", ...}, ...] $ curl -s "http://localhost:5678/api/sessions?source=wechat" [{"username": "46222992238@chatroom", "name": "月下健人", "type": "群", "last_ts": 1779014016, ...}, ...] ``` API 返回正确, 个人微信 3142 个 / 企微 14 个会话, 按 last_ts 降序。 测试 185/185 通过。 ## 副带改进 - runToolWithArgs 拆出来后, fix 之前一个小 bug: error 路径里的 `b.textContent = b.dataset.origText` 改成 innerHTML 路径 (一致性) ## 仍未跟进 (follow-up) - export_all_chats 加 `--format csv,html` 多格式输出 (个人微信脚本 目前只支持 JSON, 模态框格式选项对它隐藏了, 看 #109 推进) - 个人微信导出 sessions 列表性能: 3142 个一次 fetch ~5MB JSON, 搜索 + render 在低端机可能卡, 可加分页 / 虚拟列表 --- export_all_chats.py | 18 +++ monitor_web.py | 319 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 324 insertions(+), 13 deletions(-) diff --git a/export_all_chats.py b/export_all_chats.py index b2326b6..866ac19 100644 --- a/export_all_chats.py +++ b/export_all_chats.py @@ -299,6 +299,12 @@ def main(): action="store_true", help="预览模式:显示将导出的会话数和新消息数,不实际写入", ) + parser.add_argument( + "--users", + default=None, + help="只导出指定 username 的会话, 逗号分隔 (如 wxid_xxx,12345@chatroom). " + "为空时导出全部 (旧行为). 也可用 env WECHAT_EXPORT_USERS", + ) args = parser.parse_args() script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -338,6 +344,18 @@ def main(): print(f"会话数据库查询失败: {e}", file=sys.stderr) sys.exit(1) + # username 白名单过滤 (--users 参数 / WECHAT_EXPORT_USERS 环境变量) + users_filter_raw = args.users or os.environ.get("WECHAT_EXPORT_USERS", "") + if users_filter_raw.strip(): + wanted = {u.strip() for u in users_filter_raw.split(",") if u.strip()} + before = len(sessions) + sessions = [u for u in sessions if u in wanted] + print(f"按 --users 过滤: {before} → {len(sessions)} 会话") + if not sessions: + print(f"[!] 指定的 username 列表跟会话表没交集 (wanted={list(wanted)[:5]}...)", + file=sys.stderr) + sys.exit(1) + names = mcp_server.get_contact_names() # 显示模式信息 diff --git a/monitor_web.py b/monitor_web.py index 5b04de7..58d353e 100644 --- a/monitor_web.py +++ b/monitor_web.py @@ -1782,6 +1782,45 @@ a.msg-link{text-decoration:none;color:inherit} .add-rule-btn:hover{background:rgba(79,195,247,.2)} /* 通知高亮 */ .msg.notify-hl{border-left:3px solid #ffd54f;background:rgba(255,213,79,.08);box-shadow:0 0 12px rgba(255,213,79,.1)} +/* 导出筛选模态框 */ +.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:1100;align-items:center;justify-content:center} +.modal-overlay.show{display:flex;animation:fadeIn .15s ease-out} +.modal{background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--r3);width:560px;max-width:90vw;max-height:85vh;display:flex;flex-direction:column;box-shadow:var(--shadow-2);overflow:hidden} +.modal-h{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0} +.modal-h h2{font-size:var(--t4);color:var(--text);font-weight:600} +.modal-close{background:none;border:none;color:var(--text-faint);font-size:20px;cursor:pointer;padding:4px 10px;border-radius:var(--r1);transition:all .15s} +.modal-close:hover{color:var(--text);background:var(--surface)} +.modal-b{padding:16px 20px;overflow-y:auto;flex:1} +.modal-f{padding:14px 20px;border-top:1px solid var(--border);display:flex;justify-content:space-between;gap:8px;align-items:center;flex-shrink:0;background:rgba(0,0,0,.2)} +.modal-search{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:var(--r1);padding:8px 12px;color:var(--text);font-size:var(--t3);outline:none;font-family:inherit;margin-bottom:var(--s3)} +.modal-search:focus{border-color:var(--accent)} +.modal-search::placeholder{color:var(--text-faint)} +.session-list{max-height:280px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--r1);background:rgba(0,0,0,.2)} +.session-item{display:flex;align-items:center;gap:var(--s2);padding:8px 12px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.03);transition:background .1s;font-size:var(--t3)} +.session-item:hover{background:var(--surface-hover)} +.session-item:last-child{border-bottom:none} +.session-item input[type=checkbox]{accent-color:var(--accent);width:14px;height:14px;cursor:pointer;margin:0} +.session-type{font-size:10px;padding:2px 6px;border-radius:var(--r-pill);color:var(--text-dim);background:var(--surface);min-width:28px;text-align:center;flex-shrink:0} +.session-type.grp{background:rgba(206,147,216,.12);color:#ce93d8} +.session-type.single{background:rgba(79,195,247,.12);color:var(--accent)} +.session-name{flex:1;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.session-ts{font-size:var(--t1);color:var(--text-faint);flex-shrink:0;font-family:Consolas,monospace} +.modal-selctrl{display:flex;gap:var(--s2);margin:var(--s3) 0;flex-wrap:wrap} +.modal-selctrl button{background:var(--surface);border:1px solid var(--border);color:var(--text-dim);padding:5px 10px;border-radius:var(--r1);font-size:var(--t1);cursor:pointer;transition:all .15s;font-family:inherit} +.modal-selctrl button:hover{background:var(--surface-hover);color:var(--text)} +.modal-section{margin-top:var(--s4)} +.modal-section-label{font-size:var(--t1);color:var(--text-faint);margin-bottom:var(--s2);text-transform:uppercase;letter-spacing:1.2px;font-weight:600} +.modal-fmt{display:flex;gap:var(--s4);flex-wrap:wrap} +.modal-fmt label{display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:var(--t3);color:var(--text)} +.modal-fmt input{accent-color:var(--accent);width:14px;height:14px;cursor:pointer} +.modal-selcount{color:var(--text-dim);font-size:var(--t2)} +.modal-btn{padding:8px 18px;border-radius:var(--r1);font-size:var(--t3);cursor:pointer;border:none;font-family:inherit;font-weight:500;transition:all .15s} +.modal-btn.secondary{background:var(--surface);color:var(--text-dim);border:1px solid var(--border)} +.modal-btn.secondary:hover{background:var(--surface-hover);color:var(--text)} +.modal-btn.primary{background:linear-gradient(135deg,#4fc3f7,#29b6f6);color:#001528;font-weight:600;box-shadow:0 4px 14px rgba(79,195,247,.35)} +.modal-btn.primary:hover:not(:disabled){background:linear-gradient(135deg,#5fd0ff,#3fc4ff);transform:translateY(-1px)} +.modal-btn:disabled{opacity:.4;cursor:not-allowed;transform:none!important} +.modal-loading{text-align:center;color:var(--text-faint);padding:30px;font-size:var(--t3)} /* Icon 通用样式 (替代 emoji) */ .i{width:16px;height:16px;display:inline-block;vertical-align:-3px;flex-shrink:0;color:inherit} .i-sm{width:13px;height:13px;vertical-align:-2px} @@ -1957,6 +1996,39 @@ a.msg-link{text-decoration:none;color:inherit} + +

等待新消息...

WAL增量解密 · SSE推送

@@ -2090,16 +2162,111 @@ async function cancelTool(){ await fetch('/api/tool/cancel',{method:'POST'}); }catch(e){} } + +// —— 导出筛选模态框 —— +window.__exportCtx = { source: null, task: null, btn: null, sessions: [] }; + +async function openExportModal(modalKind, task, btn){ + const source = modalKind === 'export_wxwork' ? 'wxwork' : 'wechat'; + window.__exportCtx = { source, task, btn, sessions: [] }; + document.getElementById('exportModalTitle').textContent = + source === 'wxwork' ? '导出企业微信聊天' : '导出个人微信聊天'; + document.getElementById('exportSearch').value = ''; + // 企微脚本支持 --formats, 个人微信脚本目前只 JSON; 隐藏个人微信的格式选项 + document.getElementById('exportFmtSection').style.display = source === 'wxwork' ? 'block' : 'none'; + document.getElementById('exportConfirmBtn').disabled = true; + document.getElementById('exportSelCount').textContent = '已选 0 个'; + document.getElementById('exportSessionList').innerHTML = ''; + document.getElementById('exportModal').classList.add('show'); + try{ + const r = await fetch('/api/sessions?source=' + source); + const sessions = await r.json(); + if(sessions.error) throw new Error(sessions.error); + window.__exportCtx.sessions = sessions; + renderSessions(sessions, ''); + }catch(e){ + document.getElementById('exportSessionList').innerHTML = + ''; + } +} +function closeExportModal(){ + document.getElementById('exportModal').classList.remove('show'); +} +function renderSessions(sessions, filter){ + const list = document.getElementById('exportSessionList'); + const lo = filter.toLowerCase(); + const filtered = sessions.filter(s => + !lo || s.name.toLowerCase().includes(lo) || (s.username||'').toLowerCase().includes(lo) + ); + if(!filtered.length){ + list.innerHTML = ''; + return; + } + list.innerHTML = filtered.map((s, idx) => { + const tsLabel = s.last_ts ? new Date(s.last_ts*1000).toISOString().slice(0,10) : ''; + const typeCls = s.type === '群' ? 'grp' : (s.type === '单聊' ? 'single' : ''); + return ``; + }).join(''); +} +function filterSessions(){ + renderSessions(window.__exportCtx.sessions, document.getElementById('exportSearch').value); + updateSelCount(); // refilter 后保留 selection 但 count 重新统计当前可见的 +} +function updateSelCount(){ + const checked = document.querySelectorAll('#exportSessionList input[type=checkbox]:checked'); + document.getElementById('exportSelCount').textContent = '已选 ' + checked.length + ' 个'; + document.getElementById('exportConfirmBtn').disabled = checked.length === 0; +} +function selectAllSessions(yes){ + document.querySelectorAll('#exportSessionList input[type=checkbox]').forEach(c => c.checked = yes); + updateSelCount(); +} +function selectRecentSessions(days){ + const cutoff = Date.now()/1000 - days*86400; + const list = window.__exportCtx.sessions; + document.querySelectorAll('#exportSessionList input[type=checkbox]').forEach(c => { + const u = c.dataset.username; + const s = list.find(x => x.username === u); + c.checked = s && s.last_ts >= cutoff; + }); + updateSelCount(); +} +function confirmExport(){ + const users = [...document.querySelectorAll('#exportSessionList input[type=checkbox]:checked')] + .map(c => c.dataset.username); + const formats = [...document.querySelectorAll('#exportFmtSection input[type=checkbox]:checked')] + .map(c => c.value); + closeExportModal(); + // 实际触发任务,带 args + const { task, btn } = window.__exportCtx; + runToolWithArgs(task, btn, { users, formats }); +} +// 需要弹模态框先筛选会话的任务 +const NEEDS_MODAL = { 'export_all': 'export_wechat', 'wxwork_export': 'export_wxwork' }; + async function runTool(task, btn){ - // 如果已经在运行 (按钮变成 cancel) → 取消 + // 取消已运行任务 if(btn.classList.contains('cancel')){ btn.disabled = true; btn.textContent = '终止中...'; cancelTool(); return; } + // 导出类任务先弹模态框 + if(NEEDS_MODAL[task]){ + openExportModal(NEEDS_MODAL[task], task, btn); + return; + } + runToolWithArgs(task, btn, null); +} + +async function runToolWithArgs(task, btn, args){ const s=document.getElementById('toolStatus'); - // 禁用其他按钮, 当前按钮改成"终止" document.querySelectorAll('.tool-task-btn').forEach(b=>{ if(b!==btn) b.disabled=true; }); @@ -2108,22 +2275,22 @@ async function runTool(task, btn){ btn.innerHTML = ' 终止'; btn.classList.add('cancel'); window.__runningBtn = btn; - // 清当前 pane 的日志框 const L=document.getElementById('toolLog_'+window.__activeToolPane); if(L) L.textContent=''; s.style.display='inline-block'; s.className='tool-status running'; s.innerHTML=' 运行中: '+esc(btn.dataset.origText.trim()); try{ - const r=await fetch('/api/tool',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({task:task})}); + const payload = { task: task }; + if(args) payload.args = args; + const r=await fetch('/api/tool',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); const d=await r.json(); if(!r.ok){ s.className='tool-status err'; s.textContent='✗ '+(d.error||'启动失败'); - // 启动失败立刻还原 document.querySelectorAll('.tool-task-btn').forEach(b=>{ b.disabled=false; - if(b.dataset.origText){b.textContent = b.dataset.origText; b.dataset.origText='';} + if(b.dataset.origHtml){b.innerHTML = b.dataset.origHtml; b.dataset.origHtml=''; b.dataset.origText='';} b.classList.remove('cancel'); }); } @@ -2335,6 +2502,30 @@ fetch('/api/history').then(r=>r.json()).then(ms=>{ # 前端在 HTML_PAGE 顶部加一个折叠区,点按钮 POST /api/tool {task: "..."}。 # SSE 事件用 event=tool_log / tool_done 跟原 message 事件区分。 +def _build_export_steps(users, formats): + """根据用户选择的会话 + 格式拼 export_all_chats.py argv""" + cmd = [sys.executable, "export_all_chats.py"] + if users: + cmd += ["--users", ",".join(users)] + # export_all_chats 当前只输出 JSON, formats 暂时忽略 + # (export_messages.py 有 CSV/HTML/JSON, 但走的是另一条路径, 不在 v1 范围) + return [cmd] + + +def _build_wxwork_export_steps(users, formats): + """根据用户选择拼 export_wxwork_messages.py argv (--conversation 可重复)""" + cmd = [sys.executable, "export_wxwork_messages.py"] + for u in (users or []): + cmd += ["--conversation", u] + if formats: + cmd += ["--formats", ",".join(formats)] + return [cmd] + + +# task 配置: +# steps — 固定 cmd 列表 (无参任务) +# build_steps — fn(args)->[cmd, ...] 动态构造 (需要用户选会话/格式的导出任务) +# needs_modal — 前端点这个 task 时要弹模态框 ('export_wechat' | 'export_wxwork') TOOL_TASKS = { # —— 个人微信 —— "wechat_decrypt": { @@ -2346,8 +2537,9 @@ TOOL_TASKS = { "steps": [[sys.executable, "find_image_key.py"]], }, "export_all": { - "name": "③ 导出全部聊天 (JSON)", - "steps": [[sys.executable, "export_all_chats.py"]], + "name": "③ 导出聊天", + "build_steps": _build_export_steps, + "needs_modal": "export_wechat", }, "decode_images": { "name": "④ 批量解密图片", @@ -2371,7 +2563,8 @@ TOOL_TASKS = { }, "wxwork_export": { "name": "⑦ 企业微信导出", - "steps": [[sys.executable, "export_wxwork_messages.py"]], + "build_steps": _build_wxwork_export_steps, + "needs_modal": "export_wxwork", }, # —— 工具 —— "voice_mp3": { @@ -2384,13 +2577,87 @@ _tool_lock = threading.Lock() _tool_running = {"job": None, "proc": None, "cancelled": False} # 同时只允许一个任务 +def _list_sessions(source): + """列会话, 供导出筛选模态框消费。返回 [{name, username, type, last_ts}] + 按 last_ts 降序。 + + source: + wechat — 个人微信, 从 decrypted/session/session.db 读 SessionTable + wxwork — 企业微信, 从 wxwork_decrypted/session.db 读 conversation_table + """ + out = [] + if source == "wechat": + if not os.path.exists(DECRYPTED_SESSION): + return [] + # 复用 load_contact_names (静态快照即可, 模态框开时调一次性 OK) + names = load_contact_names() + try: + with closing(sqlite3.connect(f"file:{DECRYPTED_SESSION}?mode=ro&immutable=1", uri=True)) as conn: + for r in conn.execute( + "SELECT username, type, last_timestamp, last_sender_display_name, summary " + "FROM SessionTable WHERE username IS NOT NULL AND username != '' " + "ORDER BY last_timestamp DESC" + ): + username, type_, ts, sender, summary = r + is_group = username.endswith("@chatroom") if username else False + is_public = username.startswith("gh_") if username else False + type_label = "群" if is_group else ("公众号" if is_public else "单聊") + name = names.get(username) or sender or username + out.append({ + "username": username, + "name": name, + "type": type_label, + "last_ts": ts or 0, + "summary": (summary or "")[:60], + }) + except Exception: + pass + elif source == "wxwork": + script_dir = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(script_dir, "wxwork_decrypted", "session.db") + if not os.path.exists(path): + return [] + try: + with closing(sqlite3.connect(f"file:{path}?mode=ro&immutable=1", uri=True)) as conn: + conn.text_factory = lambda b: b.decode("utf-8", errors="replace") if isinstance(b, bytes) else b + for r in conn.execute( + "SELECT id, name, last_message_time FROM conversation_table " + "WHERE id IS NOT NULL AND id != '' " + "ORDER BY last_message_time DESC" + ): + cid, name, ts = r + # id 前缀: R=群 / S=单聊 / E=外部/系统 / Y=其他 + if cid.startswith("R:"): + type_label = "群" + elif cid.startswith("S:"): + type_label = "单聊" + elif cid.startswith("E:"): + type_label = "外部" + else: + type_label = "其他" + out.append({ + "username": cid, + "name": name or cid, + "type": type_label, + "last_ts": ts or 0, + "summary": "", + }) + except Exception: + pass + return out + + def _broadcast_tool_event(event, **fields): payload = {"event": event, **fields} broadcast_sse(payload) -def _run_tool_task(job_id, task_name): - """后台线程: 顺序跑 TOOL_TASKS[task_name].steps 的每条命令,实时推 SSE。""" +def _run_tool_task(job_id, task_name, args=None): + """后台线程: 顺序跑 TOOL_TASKS[task_name].steps 的每条命令,实时推 SSE。 + + args 含用户在前端模态框选的参数 (users / formats 等), 由 build_steps + 动态构造 cmd。固定任务 (无 build_steps) 直接用 steps。 + """ task = TOOL_TASKS.get(task_name) if not task: _broadcast_tool_event("tool_done", job_id=job_id, ok=False, @@ -2399,12 +2666,19 @@ def _run_tool_task(job_id, task_name): _tool_running["job"] = None return + # 动态构造 steps (导出类) 或用固定 steps + if "build_steps" in task: + a = args or {} + steps = task["build_steps"](a.get("users", []), a.get("formats", [])) + else: + steps = task["steps"] + _broadcast_tool_event("tool_log", job_id=job_id, line=f"━━━ 开始: {task['name']} ━━━\n") exit_code = 0 cancelled = False - for step in task["steps"]: + for step in steps: cmd_str = " ".join(step) _broadcast_tool_event("tool_log", job_id=job_id, line=f"\n>>> {cmd_str}\n\n") @@ -2575,6 +2849,23 @@ class Handler(BaseHTTPRequestHandler): with sse_lock: if q in sse_clients: sse_clients.remove(q) + elif self.path == "/api/sessions" or self.path.startswith("/api/sessions?"): + # 列会话(用于导出筛选模态框) + parsed = urllib.parse.urlparse(self.path) + params = urllib.parse.parse_qs(parsed.query) + source = params.get("source", ["wechat"])[0] + try: + sessions = _list_sessions(source) + self.send_response(200) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + self.wfile.write(json.dumps(sessions, ensure_ascii=False).encode()) + except Exception as e: + self.send_response(500) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps({"error": str(e)}).encode()) + else: self.send_error(404) @@ -2585,6 +2876,7 @@ class Handler(BaseHTTPRequestHandler): body = self.rfile.read(length).decode("utf-8") if length else "{}" req = json.loads(body) task_name = req.get("task", "") + task_args = req.get("args", {}) or {} except Exception as e: self.send_response(400) self.send_header("Content-Type", "application/json") @@ -2613,7 +2905,8 @@ class Handler(BaseHTTPRequestHandler): job_id = "j_" + uuid.uuid4().hex[:8] _tool_running["job"] = job_id - threading.Thread(target=_run_tool_task, args=(job_id, task_name), + threading.Thread(target=_run_tool_task, + args=(job_id, task_name, task_args), daemon=True).start() self.send_response(200)