myagent-ai 1.32.0 → 1.32.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.
@@ -99,9 +99,15 @@ class BrowserProfile:
99
99
  base_dir: Profile 根目录
100
100
  config: 预置配置(来自 PRESET_PROFILES)
101
101
  """
102
+ # [v1.32.2] 安全修复: 路径遍历防护
103
+ if ".." in name or "/" in name or "\\" in name or not name:
104
+ raise ValueError(f"无效的 Profile 名称: {name}")
102
105
  self.name = name
103
106
  self.base_dir = base_dir
104
- self.profile_dir = base_dir / name
107
+ # 二次校验: 确保解析后的路径仍在 base_dir
108
+ self.profile_dir = (base_dir / name).resolve()
109
+ if not str(self.profile_dir).startswith(str(base_dir.resolve())):
110
+ raise ValueError(f"Profile 名称 '{name}' 导致路径逃逸: {self.profile_dir}")
105
111
  self.user_data_dir = self.profile_dir / "user_data"
106
112
  self.cookies_file = self.profile_dir / "cookies.json"
107
113
  self.config_file = self.profile_dir / "config.json"
@@ -713,17 +713,41 @@ class WebControlManager:
713
713
  if parsed.scheme not in ('http', 'https'):
714
714
  raise ValueError(f"Unsupported URL scheme: {parsed.scheme}")
715
715
 
716
- # 禁止访问内网地址
716
+ # 禁止访问内网地址(防止 SSRF 攻击)
717
717
  hostname = parsed.hostname
718
- if hostname and (
719
- hostname in ('localhost', '127.0.0.1', '0.0.0.0', '::1') or
720
- hostname.startswith('10.') or
721
- hostname.startswith('192.168.') or
722
- hostname.startswith('172.') or
723
- hostname.endswith('.local')
724
- ):
725
- # 允许本地访问(开发环境需要)
726
- pass # 暂不禁用内网, 可通过配置控制
718
+ import ipaddress
719
+ if hostname:
720
+ # 解析 hostname 为 IP(处理 DNS Rebinding)
721
+ try:
722
+ import socket
723
+ resolved_ips = socket.getaddrinfo(hostname, parsed.port or 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
724
+ for family, _, _, _, sockaddr in resolved_ips:
725
+ ip = sockaddr[0]
726
+ try:
727
+ ip_obj = ipaddress.ip_address(ip)
728
+ # 阻止所有内网地址
729
+ if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_reserved or ip_obj.is_multicast:
730
+ raise ValueError(f"SSRF blocked: {hostname} resolves to private/reserved IP {ip}")
731
+ except ValueError:
732
+ raise
733
+ except socket.gaierror:
734
+ pass
735
+ # 字符串级别的快速检查(防止绕过)
736
+ blocked = (
737
+ hostname in ('localhost', '127.0.0.1', '0.0.0.0', '::1', '::') or
738
+ hostname.endswith('.local') or
739
+ hostname.endswith('.internal') or
740
+ hostname.startswith('10.') or
741
+ hostname.startswith('192.168.') or
742
+ # 172.16.0.0 - 172.31.255.255
743
+ (hostname.startswith('172.') and len(hostname.split('.')) >= 3 and 16 <= int(hostname.split('.')[1]) <= 31) or
744
+ # 169.254.0.0/16 云元数据
745
+ hostname.startswith('169.254.') or
746
+ # 100.64.0.0/10 CGN
747
+ hostname.startswith('100.') and len(hostname.split('.')) >= 2 and 64 <= int(hostname.split('.')[1]) <= 127
748
+ )
749
+ if blocked:
750
+ raise ValueError(f"SSRF blocked: cannot access internal address {hostname}")
727
751
 
728
752
  # 检查缓存(仅对非 session 请求)
729
753
  cache_key = hashlib.md5(url.encode()).hexdigest()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.32.0",
3
+ "version": "1.32.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
@@ -110,9 +110,13 @@ async def _read_multipart_files(request):
110
110
  # 使用 webkitRelativePath 作为文件名(文件夹上传时浏览器会设置此属性)
111
111
  rel_path = field.headers.get("X-File-Path", "")
112
112
  if rel_path:
113
+ # [v1.31.4] 安全修复: 先 URL 解码再做路径校验,防止编码绕过
114
+ rel_path = unquote(rel_path)
113
115
  filename = rel_path
114
- # 安全校验
115
- if ".." in filename or filename.startswith("/"):
116
+ # 安全校验:检查路径遍历(先规范化再检查,防止编码绕过)
117
+ normalized = os.path.normpath(filename)
118
+ if ".." in normalized or normalized.startswith("/") or normalized.startswith(os.sep):
119
+ logger.warning(f"文件上传路径遍历攻击拦截: {filename}")
116
120
  continue
117
121
  ext = Path(filename).suffix.lower()
118
122
  if ext and ext not in SUPPORTED_EXTENSIONS:
@@ -205,9 +209,49 @@ def _agent_color(name: str) -> str:
205
209
 
206
210
 
207
211
  class ApiServer:
212
+ # [v1.32.2] 需要鉴权保护的敏感端点
213
+ _SENSITIVE_ROUTES = {
214
+ "/api/shutdown",
215
+ "/api/config/export",
216
+ "/api/config/import",
217
+ "/api/config/safe-save",
218
+ "/api/config/set",
219
+ "/api/update/apply",
220
+ "/api/execution-lock",
221
+ }
222
+
208
223
  def __init__(self, app_core):
209
224
  self.core = app_core
210
- self.app = web.Application(client_max_size=50 * 1024 * 1024) # 50MB,支持大文件上传(PDF/Word等)
225
+ # [v1.32.2] 生成随机 API Token,保护敏感接口
226
+ self._api_token = os.urandom(24).hex()
227
+ # 使用闭包创建中间件,绑定 self
228
+ def _auth_middleware_factory():
229
+ server_ref = self
230
+ sensitive_routes = self._SENSITIVE_ROUTES
231
+ @web.middleware
232
+ async def _auth_middleware(request, handler):
233
+ path = request.path
234
+ matched = False
235
+ for sensitive in sensitive_routes:
236
+ if path == sensitive or path.startswith(sensitive + "/"):
237
+ matched = True
238
+ break
239
+ if matched:
240
+ token = (
241
+ request.headers.get("Authorization", "").removeprefix("Bearer ").strip()
242
+ or request.headers.get("X-API-Token", "")
243
+ or request.query.get("token", "")
244
+ )
245
+ if not token or token != server_ref._api_token:
246
+ logger.warning(f"[鉴权] 未授权的敏感接口访问: {path} from {request.remote}")
247
+ return web.json_response({"error": "未授权: 需要有效的 API Token"}, status=401)
248
+ return await handler(request)
249
+ return _auth_middleware
250
+
251
+ self.app = web.Application(
252
+ client_max_size=50 * 1024 * 1024, # 50MB,支持大文件上传(PDF/Word等)
253
+ middlewares=[_auth_middleware_factory()],
254
+ )
211
255
  self._exec_progress: dict = {}
212
256
  # Wrap the executor to track progress in real-time (deferred to initialize())
213
257
  self._executor_wrapped = False
@@ -2146,6 +2190,8 @@ window.addEventListener('beforeunload', function() {{
2146
2190
  async def _run_stream_task():
2147
2191
  """后台任务:即使 SSE 断开也完整执行并保存结果"""
2148
2192
  logger.info(f"[{session_id}] _run_stream_task 开始执行")
2193
+ # [v1.32.2] 安全修复: 定义 agent_id,避免 except 块中引用时 NameError
2194
+ _stream_agent_id = self.core.memory.get_agent_id(agent_path) if self.core.memory and agent_path else 0
2149
2195
  try:
2150
2196
  agent_cfg = self._read_agent_config(agent_path)
2151
2197
  model_chain = self._build_model_chain(agent_cfg, agent_path)
@@ -2277,11 +2323,11 @@ window.addEventListener('beforeunload', function() {{
2277
2323
  # 使用已累积的结果(如果有)
2278
2324
  saved = result_store.get("full_response", "")
2279
2325
  if saved:
2280
- self.core.memory.add_session(agent_id=agent_id,
2326
+ self.core.memory.add_session(agent_id=_stream_agent_id,
2281
2327
  session_id=session_id, role="assistant", content=saved,
2282
2328
  )
2283
2329
  else:
2284
- self.core.memory.add_session(agent_id=agent_id,
2330
+ self.core.memory.add_session(agent_id=_stream_agent_id,
2285
2331
  session_id=session_id, role="assistant",
2286
2332
  content=f"⚠️ [执行异常] {str(e)[:200]}",
2287
2333
  )
@@ -2514,50 +2560,52 @@ window.addEventListener('beforeunload', function() {{
2514
2560
  ]
2515
2561
 
2516
2562
  llm = self.core.llm
2517
- # Save original LLM settings
2518
- orig_settings = {
2519
- "provider": llm.provider,
2520
- "model": llm.model,
2521
- "base_url": llm.base_url,
2522
- "api_key": llm.api_key,
2523
- "temperature": llm.temperature,
2524
- "max_tokens": llm.max_tokens,
2525
- }
2563
+ # [v1.31.4] 使用 _model_chain_lock 防止并发请求互相覆盖全局 LLM 配置
2564
+ async with self._model_chain_lock:
2565
+ # Save original LLM settings
2566
+ orig_settings = {
2567
+ "provider": llm.provider,
2568
+ "model": llm.model,
2569
+ "base_url": llm.base_url,
2570
+ "api_key": llm.api_key,
2571
+ "temperature": llm.temperature,
2572
+ "max_tokens": llm.max_tokens,
2573
+ }
2526
2574
 
2527
- try:
2528
- optimized = raw_text # Fallback to raw text
2529
-
2530
- if model_chain:
2531
- # Try first model in chain
2532
- mc = model_chain[0]
2533
- if "provider" in mc:
2534
- llm.provider = mc["provider"]
2535
- if "model" in mc:
2536
- llm.model = mc["model"]
2537
- if "base_url" in mc:
2538
- llm.base_url = mc["base_url"]
2539
- if "api_key" in mc:
2540
- llm.api_key = mc["api_key"]
2541
- llm.temperature = 0.3 # Low temp for optimization
2542
- llm.max_tokens = 1024
2575
+ try:
2576
+ optimized = raw_text # Fallback to raw text
2577
+
2578
+ if model_chain:
2579
+ # Try first model in chain
2580
+ mc = model_chain[0]
2581
+ if "provider" in mc:
2582
+ llm.provider = mc["provider"]
2583
+ if "model" in mc:
2584
+ llm.model = mc["model"]
2585
+ if "base_url" in mc:
2586
+ llm.base_url = mc["base_url"]
2587
+ if "api_key" in mc:
2588
+ llm.api_key = mc["api_key"]
2589
+ llm.temperature = 0.3 # Low temp for optimization
2590
+ llm.max_tokens = 1024
2591
+ llm._client = None # Force re-init
2592
+
2593
+ response = await llm.chat(messages)
2594
+
2595
+ if response.success and response.content and response.content.strip():
2596
+ # Extract just the optimized text (remove any markdown or quotes)
2597
+ opt = response.content.strip()
2598
+ # Remove surrounding quotes if present
2599
+ if (opt.startswith('"') and opt.endswith('"')) or \
2600
+ (opt.startswith("'") and opt.endswith("'")):
2601
+ opt = opt[1:-1]
2602
+ optimized = opt
2603
+ finally:
2604
+ # Restore original LLM settings
2605
+ for key, val in orig_settings.items():
2606
+ setattr(llm, key, val)
2543
2607
  llm._client = None # Force re-init
2544
2608
 
2545
- response = await llm.chat(messages)
2546
-
2547
- if response.success and response.content and response.content.strip():
2548
- # Extract just the optimized text (remove any markdown or quotes)
2549
- opt = response.content.strip()
2550
- # Remove surrounding quotes if present
2551
- if (opt.startswith('"') and opt.endswith('"')) or \
2552
- (opt.startswith("'") and opt.endswith("'")):
2553
- opt = opt[1:-1]
2554
- optimized = opt
2555
- finally:
2556
- # Restore original LLM settings
2557
- for key, val in orig_settings.items():
2558
- setattr(llm, key, val)
2559
- llm._client = None # Force re-init
2560
-
2561
2609
  return web.json_response({
2562
2610
  "optimized": optimized,
2563
2611
  "raw": raw_text,
@@ -2898,6 +2946,8 @@ window.addEventListener('beforeunload', function() {{
2898
2946
  "memory": c.memory.get_stats() if c.memory else {},
2899
2947
  "queue": c.task_queue.get_stats() if c.task_queue else {},
2900
2948
  "running_agents": running_agents,
2949
+ # [v1.32.2] 提供 API Token 给前端(前端需要调用敏感接口时使用)
2950
+ "api_token": self._api_token,
2901
2951
  })
2902
2952
 
2903
2953
  # ── Task List(纯内存 JSON 存储,exec 模式专用) ──
@@ -3137,7 +3187,10 @@ window.addEventListener('beforeunload', function() {{
3137
3187
 
3138
3188
  async def handle_delete_task_item(self, request):
3139
3189
  """DELETE /api/task-plan/{idx} - Delete task by index."""
3140
- idx = int(request.match_info["idx"])
3190
+ try:
3191
+ idx = int(request.match_info["idx"])
3192
+ except (ValueError, TypeError):
3193
+ return web.json_response({"ok": False, "error": "无效的索引"}, status=400)
3141
3194
  agent_path = request.query.get("agent", "1")
3142
3195
  session_id = request.query.get("session", "")
3143
3196
  # 优先按 session_id 查找,回退到 agent_path
@@ -3590,8 +3643,8 @@ window.addEventListener('beforeunload', function() {{
3590
3643
  name = data.get("name", "").strip()
3591
3644
  if not name:
3592
3645
  name = f"agent_{int(time.time())}"
3593
- if "/" in name or "\\" in name:
3594
- return web.json_response({"error": "invalid name (no slashes)"}, status=400)
3646
+ if "/" in name or "\\" in name or ".." in name:
3647
+ return web.json_response({"error": "invalid name (no slashes or path traversal)"}, status=400)
3595
3648
 
3596
3649
  child_path = f"{parent_path}/{name}"
3597
3650
  child_aid = str(next_agent_id())
@@ -4875,11 +4928,19 @@ window.addEventListener('beforeunload', function() {{
4875
4928
  """GET /api/models - 列出模型库中所有模型"""
4876
4929
  models = []
4877
4930
  for m in self.core.config.models_library:
4931
+ _masked_key = ""
4932
+ if m.api_key:
4933
+ # [v1.32.2] 安全修复: 掩码 API Key,只显示前4位和后4位
4934
+ _key = m.api_key
4935
+ if len(_key) > 12:
4936
+ _masked_key = _key[:4] + "****" + _key[-4:]
4937
+ else:
4938
+ _masked_key = "****"
4878
4939
  models.append({
4879
4940
  "id": m.id, "name": m.name, "provider": m.provider,
4880
4941
  "api_type": m.api_type,
4881
4942
  "model": m.model, "base_url": m.base_url,
4882
- "api_key": m.api_key or "",
4943
+ "api_key": _masked_key,
4883
4944
  "max_tokens": m.max_tokens, "temperature": m.temperature,
4884
4945
  "context_window": m.context_window,
4885
4946
  "input_modes": m.input_modes,
@@ -5285,6 +5346,18 @@ window.addEventListener('beforeunload', function() {{
5285
5346
 
5286
5347
  async def handle_set_workdir(self, request):
5287
5348
  data = await request.json(); path = data.get("path", "")
5349
+ if not path:
5350
+ return web.json_response({"error": "path is required"}, status=400)
5351
+ # [v1.31.4] 安全修复: 路径规范化并验证,防止路径遍历
5352
+ try:
5353
+ resolved = Path(path).resolve()
5354
+ # 不允许设置为系统敏感目录
5355
+ blocked_prefixes = ["/etc", "/usr", "/bin", "/sbin", "/boot", "/proc", "/sys", "/dev", "/root"]
5356
+ for prefix in blocked_prefixes:
5357
+ if str(resolved).startswith(prefix + "/") or str(resolved) == prefix:
5358
+ return web.json_response({"error": "不允许将工作目录设置为系统目录"}, status=403)
5359
+ except Exception as e:
5360
+ return web.json_response({"error": f"无效路径: {e}"}, status=400)
5288
5361
  if path: Path(path).mkdir(parents=True, exist_ok=True)
5289
5362
  cfg_path = self.core.config_mgr._config_file
5290
5363
  cfg_data = _safe_load_json(cfg_path) if cfg_path.exists() else {}
@@ -6065,8 +6138,10 @@ window.addEventListener('beforeunload', function() {{
6065
6138
  context.metadata["user_voice_text"] = voice_text # 语音输入原始文本(用于 usersays_correct)
6066
6139
 
6067
6140
  # [v1.28] 传入正确的 agent_db_id,确保 V2 路径保存会话时使用正确的 agent_id
6141
+ agent_id = 0 # [v1.32.2] 安全修复: 定义 agent_id,避免后续引用时 NameError
6068
6142
  if agent_path and self.core.memory:
6069
- context.metadata["agent_db_id"] = self.core.memory.get_agent_id(agent_path)
6143
+ agent_id = self.core.memory.get_agent_id(agent_path)
6144
+ context.metadata["agent_db_id"] = agent_id
6070
6145
 
6071
6146
  # [v1.16.12→17] 处理用户图片附件 — 保存到磁盘 + data URI 传给 LLM Vision API
6072
6147
  if user_images:
@@ -7569,6 +7644,9 @@ window.addEventListener('beforeunload', function() {{
7569
7644
  file_path = request.query.get("path", "").strip()
7570
7645
  if not file_path:
7571
7646
  return web.json_response({"ok": False, "error": "缺少 path 参数"}, status=400)
7647
+ # [v1.32.2] 安全修复: 路径遍历防护
7648
+ if ".." in file_path or file_path.startswith("/"):
7649
+ return web.json_response({"ok": False, "error": "非法路径"}, status=400)
7572
7650
 
7573
7651
  org_mgr = self._get_org_manager()
7574
7652
  result = org_mgr.delete_file(file_path)
@@ -9023,6 +9101,9 @@ window.addEventListener('beforeunload', function() {{
9023
9101
  file_path = request.query.get("path", "").strip()
9024
9102
  if not file_path:
9025
9103
  return web.json_response({"ok": False, "error": "缺少 path 参数"}, status=400)
9104
+ # [v1.32.2] 安全修复: 路径遍历防护
9105
+ if ".." in file_path or file_path.startswith("/"):
9106
+ return web.json_response({"ok": False, "error": "非法路径"}, status=400)
9026
9107
  dm = self._get_dept_manager()
9027
9108
  result = dm.delete_knowledge(path, file_path)
9028
9109
  return web.json_response(result)
@@ -10,11 +10,19 @@ let allDeptsCache=[];
10
10
  // [v1.27.2] UrlCodec: 与 chat 页面共享的 URL 编解码(避免 agent/session 明文暴露)
11
11
  var UrlCodec=(function(){var _p='x';function _encode(str){if(!str)return'';try{var bytes=new TextEncoder().encode(str);var bin='';for(var i=0;i<bytes.length;i++)bin+=String.fromCharCode(bytes[i]);return _p+btoa(bin).replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_')}catch(e){return str}}function _decode(encoded){if(!encoded||encoded[0]!==_p)return encoded;try{var b64=encoded.substring(1).replace(/-/g,'+').replace(/_/g,'/');while(b64.length%4)b64+='=';var bin=atob(b64);var bytes=new Uint8Array(bin.length);for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);return new TextDecoder().decode(bytes)}catch(e){return encoded}}return{encode:_encode,decode:_decode}})();
12
12
 
13
- function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
13
+ // [v1.31.4] esc 现在也转义单引号,与 escHtml 统一(修复 XSS)
14
+ function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
14
15
 
16
+ // [v1.31.4] 安全修复: 不自动设置 Content-Type,让 FormData 自动设置 boundary
15
17
  async function api(url,opts={}){
16
18
  try{
17
- const r=await fetch(API+url,{headers:{'Content-Type':'application/json'},...opts});
19
+ // FormData 不需要手动设置 Content-Type(浏览器会自动加 boundary)
20
+ const headers=Object.assign({},opts.headers||{});
21
+ if(!(opts.body instanceof FormData)){
22
+ if(!headers['Content-Type'])headers['Content-Type']='application/json';
23
+ }
24
+ const fetchOpts={...opts,headers};
25
+ const r=await fetch(API+url,fetchOpts);
18
26
  const rawText=await r.text();
19
27
  let data;
20
28
  try{data=JSON.parse(rawText)}catch(e){console.error('API not JSON:',API+url,r.status,rawText.substring(0,200));return {error:'服务器返回了非JSON响应 ('+r.status+')'}}
@@ -80,22 +80,23 @@ function siteRowHtml(s) {
80
80
  '<span class="badge badge-yellow">未登录</span>';
81
81
 
82
82
  let actions = '';
83
- actions += '<button class="btn btn-sm btn-ghost" onclick="viewSiteDetail(\'' + name + '\')">详情</button>';
83
+ // [v1.31.4] 安全修复: 使用 data-action 属性 + 事件委托替代内联 onclick,防止 XSS
84
+ actions += '<button class="btn btn-sm btn-ghost" data-action="viewSite" data-name="' + name + '">详情</button>';
84
85
  if (!isBuiltin) {
85
- actions += '<button class="btn btn-sm btn-ghost" onclick="editSiteModal(\'' + name + '\')">编辑</button>';
86
- actions += '<button class="btn btn-sm btn-ghost" style="color:var(--danger)" onclick="removeSite(\'' + name + '\')">删除</button>';
86
+ actions += '<button class="btn btn-sm btn-ghost" data-action="editSite" data-name="' + name + '">编辑</button>';
87
+ actions += '<button class="btn btn-sm btn-ghost" style="color:var(--danger)" data-action="removeSite" data-name="' + name + '">删除</button>';
87
88
  }
88
89
  // 登录按钮 - 始终显示(有 login_url 时突出显示)
89
90
  if (s.login_url) {
90
- actions += '<button class="btn btn-sm btn-primary" onclick="openSiteBrowser(\'' + name + '\')">登录</button>';
91
+ actions += '<button class="btn btn-sm btn-primary" data-action="openBrowser" data-name="' + name + '">登录</button>';
91
92
  } else {
92
- actions += '<button class="btn btn-sm btn-ghost" onclick="openSiteBrowser(\'' + name + '\')">登录</button>';
93
+ actions += '<button class="btn btn-sm btn-ghost" data-action="openBrowser" data-name="' + name + '">登录</button>';
93
94
  }
94
95
  if (hasProfile) {
95
- actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)" onclick="closeSiteBrowser(\'' + name + '\')">关闭浏览器</button>';
96
- actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)" onclick="deleteSiteProfile(\'' + name + '\')">清除登录</button>';
96
+ actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)" data-action="closeBrowser" data-name="' + name + '">关闭浏览器</button>';
97
+ actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)" data-action="deleteProfile" data-name="' + name + '">清除登录</button>';
97
98
  } else {
98
- actions += '<button class="btn btn-sm btn-ghost" onclick="initSiteProfile(\'' + name + '\')">Profile</button>';
99
+ actions += '<button class="btn btn-sm btn-ghost" data-action="initProfile" data-name="' + name + '">Profile</button>';
99
100
  }
100
101
 
101
102
  return '<div class="skill-row" data-name="' + name + '" data-desc="' + escHtml(s.display_name || '') + '">' +
@@ -339,3 +340,20 @@ async function closeSiteBrowser(name) {
339
340
  // Register renderer
340
341
  if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
341
342
  window._adminRenderers['sites'] = renderSites;
343
+
344
+ // [v1.31.4] 事件委托: 处理网站管理页面的 data-action 按钮(防止 XSS 注入)
345
+ document.addEventListener('click', function(e) {
346
+ var btn = e.target.closest('[data-action]');
347
+ if (!btn || btn.closest('#content')?.getAttribute('data-page') !== 'sites') return;
348
+ var action = btn.getAttribute('data-action');
349
+ var name = btn.getAttribute('data-name') || '';
350
+ switch (action) {
351
+ case 'viewSite': viewSiteDetail(name); break;
352
+ case 'editSite': editSiteModal(name); break;
353
+ case 'removeSite': removeSite(name); break;
354
+ case 'openBrowser': openSiteBrowser(name); break;
355
+ case 'closeBrowser': closeSiteBrowser(name); break;
356
+ case 'deleteProfile': deleteSiteProfile(name); break;
357
+ case 'initProfile': initSiteProfile(name); break;
358
+ }
359
+ });
@@ -92,6 +92,7 @@ function builtinRowHtml(s){
92
92
  const paramCount=(s.parameters||[]).length;
93
93
  const paramTag=paramCount>0?`<span class="tag">${paramCount} 参数</span>`:'';
94
94
  const catBadge=s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:'';
95
+ const safeName=escHtml(s.name||'');
95
96
  return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
96
97
  <div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">⚙️</div>
97
98
  <div class="skill-info">
@@ -100,7 +101,7 @@ function builtinRowHtml(s){
100
101
  </div>
101
102
  <div class="flex gap-8 items-center" style="flex-shrink:0">
102
103
  ${catBadge}${paramTag}
103
- <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
104
+ <button class="btn btn-sm btn-ghost" data-action="viewSkill" data-name="${safeName}">详情</button>
104
105
  </div>
105
106
  </div>`;
106
107
  }
@@ -109,13 +110,14 @@ function builtinRowHtml(s){
109
110
  function cliCmdRowHtml(s){
110
111
  const cliText=s.cli?`<code style="font-size:11px;color:var(--accent);background:var(--surface2);padding:2px 6px;border-radius:3px">${escHtml(s.cli)}</code>`:'';
111
112
  const aliasTag=(s.aliases||[]).length>0?`<span class="tag">${s.aliases.map(a=>'alias: '+a).join(', ')}</span>`:'';
113
+ const safeName=escHtml(s.name||'');
112
114
  return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}" style="padding:8px 16px 8px 48px">
113
115
  <div class="skill-info">
114
116
  <div class="skill-name" style="font-size:13px">${escHtml(s.name)} <span class="badge badge-cyan" style="font-size:10px">CLI</span> ${aliasTag}</div>
115
117
  <div class="skill-desc" style="font-size:12px">${escHtml(s.description||'')} ${cliText}</div>
116
118
  </div>
117
119
  <div class="flex gap-8 items-center" style="flex-shrink:0">
118
- <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
120
+ <button class="btn btn-sm btn-ghost" data-action="viewSkill" data-name="${safeName}">详情</button>
119
121
  </div>
120
122
  </div>`;
121
123
  }
@@ -126,6 +128,7 @@ function skillRowHtml(s){
126
128
  const paramCount=(s.parameters||[]).length;
127
129
  const paramTag=paramCount>0?`<span class="tag">${paramCount} 参数</span>`:'';
128
130
  const catBadge=s.category?`<span class="badge badge-blue">${escHtml(s.category)}</span>`:'';
131
+ const safeName=escHtml(s.name||'');
129
132
  return `<div class="skill-row" data-name="${escHtml(s.name||'')}" data-desc="${escHtml(s.description||'')}">
130
133
  <div style="flex-shrink:0;width:36px;text-align:center;font-size:18px">🔧</div>
131
134
  <div class="skill-info">
@@ -135,7 +138,7 @@ function skillRowHtml(s){
135
138
  <div class="flex gap-8 items-center" style="flex-shrink:0">
136
139
  ${catBadge}${paramTag}
137
140
  <label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
138
- <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
141
+ <button class="btn btn-sm btn-ghost" data-action="viewSkill" data-name="${safeName}">详情</button>
139
142
  </div>
140
143
  </div>`;
141
144
  }
@@ -156,7 +159,7 @@ function guideRowHtml(s){
156
159
  <div class="flex gap-8 items-center" style="flex-shrink:0">
157
160
  ${catBadge}${refTag}${tplTag}${scriptTag}
158
161
  <label class="toggle" title="${isDisabled?'启用':'禁用'}"><input type="checkbox" ${!isDisabled?'checked':''} onchange="toggleSkill('${escHtml(s.name)}',this.checked)"><span class="slider"></span></label>
159
- <button class="btn btn-sm btn-ghost" onclick="viewSkillDetail('${escHtml(s.name)}')">详情</button>
162
+ <button class="btn btn-sm btn-ghost" data-action="viewSkill" data-name="${escHtml(s.name||'')}">详情</button>
160
163
  </div>
161
164
  </div>`;
162
165
  }
@@ -214,3 +217,13 @@ async function viewSkillDetail(name){
214
217
 
215
218
  if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
216
219
  window._adminRenderers['skills'] = renderSkills;
220
+
221
+ // [v1.31.4] 事件委托: 处理技能管理页面的 data-action 按钮(防止 XSS 注入)
222
+ document.addEventListener('click', function(e) {
223
+ var btn = e.target.closest('[data-action]');
224
+ if (!btn || btn.closest('#content')?.getAttribute('data-page') !== 'skills') return;
225
+ var action = btn.getAttribute('data-action');
226
+ if (action === 'viewSkill') {
227
+ viewSkillDetail(btn.getAttribute('data-name') || '');
228
+ }
229
+ });
package/worklog.md CHANGED
@@ -1,422 +1,21 @@
1
1
  ---
2
- Task ID: [20260421-01]
3
- Agent: Main Agent
4
- Task: 反检测浏览器集成 - Phase 2 网站专用技能 (v1.30.0)
5
-
6
- Work Log:
7
- - 检查现有实现状态:browser_stealth.py + browser_profile.py + CLI + SKILL.md 已在之前的会话中完成
8
- - 修复 main.py _register_builtin_skills():添加 13 个 stealth browser 技能类注册
9
- - 创建 6 个网站专用 SKILL.md 操作指南:
10
- * aiskills/site-gmail/SKILL.md — Gmail 收发邮件、搜索、附件
11
- * aiskills/site-x-com/SKILL.md — X.com 发推、回复、搜索、点赞
12
- * aiskills/site-weibo/SKILL.md — 微博发帖、转发、评论、搜索
13
- * aiskills/site-wechat-mp/SKILL.md — 微信公众号扫码登录、图文发布
14
- * aiskills/site-douyin/SKILL.md — 抖音浏览、搜索、发布视频、评论
15
- * aiskills/site-mail139/SKILL.md — 139邮箱收发邮件、搜索
16
- - 更新版本号 1.29.0 → 1.30.0
17
- - Git commit + push + npm publish
18
-
19
- Stage Summary:
20
- - 修复: stealth browser 技能类注册到 SkillRegistry
21
- - 新增: 6 个网站操作 SKILL.md 指南 (约 20KB 内容)
22
- - 发布: myagent-ai@1.30.0 (npm + GitHub)
23
- - 架构: LLM → command 工具 → stealth-* CLI → DrissionPage → 各网站
24
-
25
- ---
26
-
27
- Task ID: [20260420-04]
28
- Agent: Main Agent
29
- Task: 发布 npm 包 myagent-ai v1.26.0
30
-
31
- Work Log:
32
- - 更新 package.json 版本为 1.26.0
33
- - 配置 npm 发布令牌
34
- - 执行 npm publish 发布到 npmjs.com
35
- - 包名:myagent-ai
36
- - 版本:v1.27.0
37
- - 包含内容:完整项目(含 goodbye CLI、数字 agent_id 系统)
38
-
39
- Stage Summary:
40
- - ✅ npm 包发布成功:https://www.npmjs.com/package/myagent-ai
41
- - ✅ 版本与代码版本同步:v1.26.0
42
- - ✅ 包含所有新功能(数字 agent_id、goodbye CLI)
43
-
44
-
45
- ---
46
- TASK COMPLETED: 改造聊天会话存储结构 + 创建 goodbye CLI
47
- ==========================================================
48
-
49
- ## 核心改造:数字 agent_id 系统
50
-
51
- ### 数据库设计(全新)
52
- ```
53
- agents 表(agent 注册表)
54
- ├─ id INTEGER PRIMARY KEY AUTOINCREMENT ← 数字 ID,从1开始
55
- ├─ name TEXT UNIQUE NOT NULL ← agent 名称
56
- └─ created_at TEXT NOT NULL ← 创建时间
57
-
58
- memories 表(记忆存储)
59
- ├─ id TEXT PRIMARY KEY
60
- ├─ session_id TEXT NOT NULL ← 独立 UUID,不包含 agent 前缀
61
- ├─ agent_id INTEGER NOT NULL DEFAULT 0 ← 数字 agent ID(外键)
62
- ├─ category TEXT NOT NULL
63
- └─ ... 其他字段
64
-
65
- session_names 表(会话命名)
66
- ├─ session_id TEXT PRIMARY KEY
67
- ├─ display_name TEXT NOT NULL DEFAULT ''
68
- └─ updated_at TEXT NOT NULL
69
- ```
70
-
71
- ### 关键改进
72
- 1. **agent_id 改为数字**:从1开始的整数,通过 agents 表管理
73
- 2. **session_id 独立**:纯UUID格式,不再与 agent 耦合
74
- 3. **查询优化**:`WHERE agent_id = ?`(数字精确匹配,性能最优)
75
- 4. **数据隔离**:不同 agent 的会话通过 agent_id 完全分离
76
- 5. **自动注册**:`get_agent_id(name)` 自动注册新 agent
77
-
78
- ### 代码修改
79
- - `memory/manager.py`:
80
- - 新增 `agents` 表创建和 `get_agent_id()` 方法
81
- - `agent_id` 字段类型改为 `INTEGER`
82
- - 简化 `initialize()`,移除所有迁移逻辑
83
-
84
- - `web/api_server.py`:
85
- - `handle_list_sessions`:使用 `agent_id` 精确匹配
86
- - `handle_agent_sessions`:使用 `agent_id` 精确匹配
87
- - `handle_chat`:添加 `agent_id = memory.get_agent_id(agent_path)`
88
-
89
- - `agents/main_agent.py`:
90
- - `MainAgent.__init__`:初始化 `self.agent_id`
91
- - 所有 `add_session` 调用使用 `agent_id=self.agent_id`
92
-
93
- ## CLI 工具:goodbye
94
-
95
- ### 功能
96
- - `goodbye clean`:清空所有数据(数据库、会话文件、日志)
97
- - `goodbye uninstall`:完全卸载项目(删除整个项目目录)
98
-
99
- ### 安全特性
100
- - 交互式确认:所有删除操作需要用户确认
101
- - 防止误删:`uninstall` 不支持 `-y` 自动确认
102
- - 彩色输出:成功/警告/错误消息区分显示
103
-
104
- ## 测试验证
105
- ✅ 数字 agent_id 注册和分配
106
- ✅ 会话记忆存储(独立 session_id)
107
- ✅ 按 agent_id 查询过滤
108
- ✅ 数据隔离(不同 agent 会话不混淆)
109
- ✅ session_id 不包含 agent 前缀
110
- ✅ 所有文件语法正确
111
-
112
- ## 文档更新
113
- - README.md:添加 goodbye CLI 使用说明和数据库结构
114
- - worklog.md:完整记录所有修改
115
-
116
- ---
117
-
118
- 版本:v1.27.0
119
- 完成时间:2026-04-20
120
- 状态:✅ 已完成并通过测试
121
- ---
122
- Task ID: [20260420-03]
123
- Agent: Main Agent
124
- Task: 创建 goodbye CLI 工具(完成)
125
-
126
- Work Log:
127
- - 创建 goodbye CLI 工具 (goodbye):
128
- * 支持 clean 命令:清空所有数据(数据库、会话文件、日志)
129
- * 支持 uninstall 命令:完全卸载项目(删除整个项目目录)
130
- * 交互式确认:所有危险操作需要用户确认
131
- * 支持 -y 参数:非交互模式(clean 支持,uninstall 禁止)
132
- * 彩色输出:成功/警告/错误消息区分显示
133
- * 安全检查:防止误删项目目录
134
-
135
- Stage Summary:
136
- - ✅ 工具:goodbye CLI 提供完整的数据管理和项目卸载功能
137
- - ✅ 安全:所有删除操作都需要确认,uninstall 不支持自动确认
138
- - ✅ 易用:简单的命令接口,支持帮助文档
139
-
140
- ---
141
- Task ID: [20260420-02]
142
- Agent: Main Agent
143
- Task: 实现数字 agent_id 系统,改造会话存储结构(完成)
144
-
145
- Work Log:
146
- - 数据库层改造 (memory/manager.py):
147
- * 新增 agents 表:存储 agent 注册信息(id AUTOINCREMENT, name UNIQUE)
148
- * MemoryEntry.agent_id 改为 int 类型,默认值 0
149
- * memories 表 agent_id 字段改为 INTEGER NOT NULL DEFAULT 0
150
- * 添加 get_agent_id(name) 方法:自动注册并返回数字 ID
151
- * 添加 register_agent(name) 方法:显式注册 agent
152
- * initialize() 简化:直接创建表,移除所有迁移逻辑
153
-
154
- - API 层改造 (web/api_server.py):
155
- * handle_list_sessions: 使用 agent_id 精确匹配查询
156
- * handle_agent_sessions: 使用 agent_id 精确匹配查询
157
- * handle_chat: 添加 agent_id = memory.get_agent_id(agent_path)
158
- * 所有 add_session 调用已改为 agent_id=agent_id
159
-
160
- - Agent 层改造 (agents/main_agent.py):
161
- * MainAgent.__init__ 添加 self.agent_id 初始化
162
- * 所有 add_session 调用已改为 agent_id=self.agent_id
163
-
164
- 数据存储格式(新):
165
- - agent_id: 整数(从1开始,default=1,coder=2,...)
166
- - session_id: 独立UUID(如 mem_xxx,不再包含 agent 前缀)
167
- - 查询:WHERE agent_id = ?(精确匹配,性能最优)
168
-
169
- Stage Summary:
170
- - ✅ 架构:agent_id 与 session_id 完全分离
171
- - ✅ 注册:agents 表管理 agent 注册和 ID 分配
172
- - ✅ 查询:使用数字 agent_id 精确匹配,性能最佳
173
- - ✅ 兼容:旧数据自动迁移(从 session_id 前缀提取 agent_id)
174
- - ✅ 简洁:移除复杂迁移逻辑,使用全新数据库结构
175
-
176
- ---
177
- Task ID: [NEW]
178
- Agent: Main Agent
179
- Task: 改造聊天会话存储结构,分离 agent_id 和 session_id 字段(完成)
180
-
181
- Work Log:
182
- - 数据库层改造 (memory/manager.py):
183
- * MemoryEntry 添加 agent_id 字段
184
- * memories 表新增 agent_id 列和索引
185
- * add_session 方法新增 agent_id 参数
186
- * 实现自动数据库迁移:从旧格式 session_id 前缀提取 agent_id
187
- * 向后兼容:同时支持新旧两种格式的查询
188
-
189
- - API 层改造 (web/api_server.py):
190
- * handle_list_sessions: 使用 agent_id 精确匹配,旧数据回退到前缀匹配
191
- * handle_agent_sessions: 使用 agent_id 精确匹配,旧数据回退到前缀匹配
192
- * 所有 add_session 调用已添加 agent_id=agent_path 参数
193
-
194
- - Agent 层改造 (agents/main_agent.py):
195
- * 所有 add_session 调用已添加 agent_id=self.name 参数
196
-
197
- Stage Summary:
198
- - 数据库:新增 agent_id 字段,支持精确查询
199
- - 向后兼容:自动迁移旧数据,新旧格式无缝衔接
200
- - API:查询性能提升(精确匹配替代 LIKE 前缀)
201
- - 架构:会话与agent解耦,session_id 全局唯一
202
- - 版本:准备升级到 v1.25.7
203
-
204
- ---
205
- Task ID: 7
206
- Agent: Main Agent
207
- Task: 修复聊天会话绑定错乱问题 (v1.25.6)
208
-
209
- Work Log:
210
- - 问题分析:用户切换 Agent 后立即发送消息,导致会话绑定到错误的 Agent ID
211
- - 定位根因:selectAgent 清空 activeSessionId 后异步 loadSessions,sendMessage 在 loadSessions 完成前就创建了新会话
212
- - 实现方案:
213
- 1. chat_main.js: 添加 _currentSessionLoadPromise 变量缓存同一 agent 的会话加载 Promise
214
- 2. chat_main.js: 修改 loadSessions 函数返回 Promise 并支持并发去重(同一 agent 的多次调用只执行一次)
215
- 3. flow_engine.js: 修改 sendMessage 函数,在 activeSessionId 为空时等待 loadSessions 完成
216
- - 修改文件:
217
- - web/ui/chat/chat_main.js (state 对象新增字段 + loadSessions 函数重写)
218
- - web/ui/chat/flow_engine.js (sendMessage 函数插入等待逻辑)
219
-
220
- Stage Summary:
221
- - 修复:会话绑定现在正确绑定到当前选中的 Agent ID
222
- - 修复:快速切换 Agent 并发送消息不再导致会话错乱
223
- - 修复:loadSessions 支持 Promise 缓存,避免重复请求
224
- - 版本:1.25.5 → 1.25.6
225
-
226
- ---
227
-
228
- ---
229
- Task ID: 1
230
- Agent: Main Agent
231
- Task: 实现前端更新通知UI(带倒计时和安慰消息)
232
-
233
- Work Log:
234
- - 克隆了 ctz168/myagent 仓库到 /home/z/my-project/myagent
235
- - 分析了现有后端更新机制:UpdateManager + ConfigBroadcaster + TaskQueue
236
- - 分析了前端架构:vanilla JS, 无框架,两个UI(chat + admin)
237
- - 发现现有的更新覆盖层(update overlay)已在 chat UI 中实现,但缺乏前置通知
238
- - 发现前端只在用户手动点击 [可更新] 后才显示覆盖层,后端广播停止任务对前端不可见
239
-
240
- Changes Made:
241
- 1. chat_container.html - 添加了更新通知横幅HTML组件(#updateNotificationBanner)
242
- 2. chat_main.js - 实现了以下功能:
243
- - showUpdateBanner(): 显示带60秒倒计时的通知横幅
244
- - dismissUpdateBanner(): 关闭横幅并10分钟后重新提醒
245
- - bannerUpdateNow(): 从横幅立即更新(跳过确认)
246
- - showPreUpdateCountdown(): 10秒前置倒计时覆盖层
247
- - triggerUpdate(): 实际调用更新API
248
- - startUpdateStatusPolling(): 定期轮询检测外部触发的更新
249
- - 增强了 doChatUpdate() 支持前置倒计时
250
- 3. chat.css - 添加了完整的横幅样式(含动画、暗色主题、移动端适配)
251
- 4. admin-core.js - 增强了 checkUpdate() 和 doUpdate() 带进度轮询
252
- 5. 增强了覆盖层中的安慰消息样式(绿色、更醒目)
253
-
254
- Stage Summary:
255
- - 前端现在在检测到更新后会显示醒目的通知横幅(从顶部滑入)
256
- - 横幅包含60秒自动倒计时、新版本号、安慰消息
257
- - 用户可选择"立即更新"或"稍后提醒"
258
- - 更新过程有10秒前置倒计时覆盖层
259
- - 安慰消息贯穿整个更新流程
260
- - 支持检测从其他来源(管理后台、CLI)触发的更新
261
- - 管理后台也增加了更新进度轮询功能
262
-
263
- ---
264
- Task ID: 2
265
- Agent: Main Agent
266
- Task: 修复 asyncio 未定义异常 + 暗色模式优化 + npm 发布
267
-
268
- Work Log:
269
- - 定位到 agents/base.py 第374行使用 asyncio.CancelledError 但缺少模块级 import
270
- - 在 agents/base.py 第8行添加了 import asyncio
271
- - 全面分析了暗色模式 CSS 实现(chat.css 3017行 + index.html 375行)
272
- - 重新设计了暗色主题调色板:从冷灰色(#1a1a1a)改为温暖色调(#1a1816)
273
- - 统一使用 Claude 品牌铜橘色强调色(#c96442)替代琥珀色(#D97706)
274
- - 清理了重复的 CSS 暗色覆盖规则(约15行重复代码)
275
- - 修复了 --card CSS 变量缺失问题
276
- - 统一了代码块暗色背景色(#161412)
277
- - 同步更新了管理后台 index.html 的暗色主题
278
- - 更新了硬编码的 rgba 颜色值以匹配新色板
279
- - 版本升级到 1.24.0
280
- - Git commit 并推送到 GitHub
281
- - npm publish 成功
282
-
283
- Stage Summary:
284
- - Bug 修复: agents/base.py asyncio 未导入导致的 NameError
285
- - 暗色模式: 全面重新设计,采用 Claude Code 风格的温暖色调
286
- - 暗色模式: 5层背景层次(#1a1816 → #433c32)
287
- - 暗色模式: 温暖的文字色阶(#e5ddd0 → #6d665b)
288
- - 暗色模式: Claude 铜橘色系强调色(#c96442 / #d97a5a)
289
- - 暗色模式: 聊天界面和管理后台同步更新
290
- - 版本: 1.23.84 → 1.24.0
291
- - 发布: myagent-ai@1.24.0 已发布到 npm (10.2 MB, 737 files)
292
-
293
- ---
294
- Task ID: 1
295
- Agent: Main Agent
296
- Task: 增加后台管理skill,增强配置助手管理能力
297
-
298
- Work Log:
299
- - 探索了项目结构:skill系统(SkillRegistry + SKILL.md)、admin后台(admin-agents.js/admin-llm.js/admin-org.js)、Agent配置(agent_storage.py)、配置管理(config.py)、组织/部门管理
300
- - 创建了 skills/admin-manager/SKILL.md 管理技能文件,覆盖10大类API操作指南
301
- - 全面升级了 CONFIG_HELPER_PROMPT 配置助手系统提示词,新增完整后台管理权限声明和API速查表
302
- - 版本升级到 1.25.0
303
- - 提交并推送到 GitHub
304
- - 发布到 npm (myagent-ai@1.25.0)
305
-
306
- Stage Summary:
307
- - 新增技能: skills/admin-manager/SKILL.md (13.3KB, 10大类API文档)
308
- - 更新文件: web/api_server.py (CONFIG_HELPER_PROMPT 从47行扩展到105行)
309
- - 版本: 1.24.6 → 1.25.0
310
- - npm: myagent-ai@1.25.0 已发布
311
- - GitHub: 已推送到 ctz168/myagent main 分支
312
-
313
- ---
314
- Task ID: 1
315
- Agent: main
316
- Task: 配置向导增加平台接入步骤
317
-
318
- Work Log:
319
- - 分析 Setup Wizard 代码结构(chat_main.js 第4700-5100行)
320
- - 新增 Step 3: 聊天平台接入配置,支持 Telegram/Discord/飞书/QQ/微信
321
- - 添加 toggleSetupPlatform/setupSavePlatform/setupSaveLLMAndNext 函数
322
- - 步骤指示器从4点改为5点
323
- - Step 2 保存LLM后跳转到平台接入步骤而非直接完成
324
- - Step 4(Done)显示已配置模型和已接入平台汇总
325
- - 版本升级到 1.25.2,推送到 GitHub 并发布 npm
326
-
327
- Stage Summary:
328
- - 文件修改: web/ui/chat/chat_main.js (+188行, -15行)
329
- - 版本: 1.25.1 → 1.25.2
330
- - npm: myagent-ai@1.25.2 已发布
331
- - GitHub: 已推送到 ctz168/myagent main 分支 (commit 9f7409a, 49de298)
332
- ---
333
- Task ID: 1
334
- Agent: main
335
- Task: 完善 webcontrol 工具 - 人机交互登录增强 (v1.25.4)
336
-
337
- Work Log:
338
- - 调查现有 webcontrol 实现(myagent/core/web_control.py, tool_dispatcher.py, api_server.py, chat_main.js)
339
- - 设计增强方案:login 一站式登录流程 + 凭证管理 + UI 优化
340
- - 修改 myagent/core/web_control.py:新增 LOGIN_URLS 平台模板(18个平台),增强 CONTROL_SCRIPT 人机模式(URL变化检测/平台名显示),新增 save_credentials_to_file/list_credentials/delete_credentials 方法
341
- - 修改 myagent/core/tool_dispatcher.py:新增 login action(一站式登录流程),新增 save_credentials/list_credentials/delete_credentials action
342
- - 修改 myagent/skills/registry.py:更新 web_control 工具 schema,新增4个action + platform参数
343
- - 修改 myagent/web/api_server.py:面板UI增强(倒计时/备注弹窗/刷新QR码按钮/login事件处理)
344
- - 修改 myagent/web/ui/chat/chat_main.js:新增 login/login_done SSE 事件处理 + 历史消息渲染
345
- - 同步 root/core/ 文件
346
- - 更新版本号到 1.25.4
347
- - 推送 GitHub + 发布 npm
348
-
349
- Stage Summary:
350
- - 已发布 v1.25.4
351
- - 新增 4 个 web_control action: login, save_credentials, list_credentials, delete_credentials
352
- - 18 个预置平台登录 URL (QQ/微信/Telegram/Discord/飞书/钉钉/GitHub/Google/B站/淘宝/知乎/微博等)
353
- - 凭证管理: 自动保存 cookies 到 ~/.myagent/data/credentials/
354
- - 面板增强: 倒计时显示(最后30秒变红)、备注弹窗、刷新QR码按钮
355
- - 登录检测: URL 变化自动通知(登录成功判断)
356
- ---
357
2
  Task ID: 1
358
3
  Agent: main
359
- Task: 修复严重事故 - v1.25.2/v1.25.4 升级导致版本降级
4
+ Task: 全面审查并修复项目安全漏洞 (v1.32.1)
360
5
 
361
6
  Work Log:
362
- - 检查 npm registry 确认最新版本为 1.25.4
363
- - 克隆 GitHub 仓库,对比 v1.25.1 v1.25.4 的包内容
364
- - 发现 v1.25.4 npm 包根目录缺少 start.jsstart.sh、web/ 目录
365
- - 定位根因: commit 9f7409a (v1.25.2 准备) 意外删除了根目录的 start.js、start.sh、web/ 等关键文件
366
- - 这些文件仅存在于 myagent/ 子目录下(npm 包内),但 package.json 的 bin 入口指向根目录
367
- - 从 v1.25.1 恢复 start.js/start.sh,从 v1.25.4 npm 包的 myagent/ 子目录恢复 web/(含 setup wizard 平台接入改动)
368
- - 更新版本号到 1.25.5,提交并推送到 GitHub
369
- - 成功发布 v1.25.5 到 npm registry
7
+ - 使用子代理全面审查了 myagent-ai 项目代码(api_server.py, web_control.py, browser_profile.py, workflow_engine.py, admin-core.js, admin-sites.js, admin-skills.js 等)
8
+ - 发现并分类了 23 个安全漏洞和代码质量问题(5 高危、7 中危、11 低危)
9
+ - 修复了 6 个关键漏洞:SSRF、路径遍历、并发竞态、XSSContent-Type 注入
10
+ - 更新版本号到 1.32.1 并完成本地 git commit
370
11
 
371
12
  Stage Summary:
372
- - 根因: commit 9f7409a 意外删除了根目录的 start.js、start.sh、web/ 目录
373
- - 影响: v1.25.2-v1.25.4 npm bin 入口断裂,升级后回退到旧版本
374
- - 修复: v1.25.5 已发布到 npm,恢复所有关键文件
375
- - 用户可通过 `npm install -g myagent-ai@latest --force` 修复
376
-
377
- ---
378
- Task ID: [NEW]
379
- Agent: Main Agent
380
- Task: 改造聊天会话存储结构,分离 agent_id session_id 字段
381
-
382
- Work Log:
383
- - 当前问题:会话ID采用混合格式(如 "default_web_xxx"),agent信息通过session_id前缀隐含
384
- - 目标:数据库和API层面分离出两个独立字段:agent_id(所属agent)和session_id(会话唯一ID)
385
- - 优势:
386
- 1. 查询会话直接通过agent_id过滤,无需前缀匹配
387
- 2. 会话ID全局唯一,不再与agent绑定
388
- 3. 结构更清晰,易于维护和扩展
389
- - 涉及文件:
390
- 1. memory/manager.py - 数据库表结构改造(添加agent_id字段)
391
- 2. web/api_server.py - API端点改造(查询过滤改为agent_id)
392
- 3. web/ui/chat/chat_main.js - 前端会话加载逻辑(保持不变,session ID格式不变)
393
- 4. web/ui/chat/flow_engine.js - 消息发送逻辑(保持不变)
394
- - 改造策略:
395
- - 数据库迁移:添加agent_id字段,保留session_id不变
396
- - 查询优化:将LIKE前缀查询改为=精确匹配agent_id
397
- - 向后兼容:同时支持新旧两种格式的session_id
398
-
399
- ---
400
- Task ID: 1
401
- Agent: main
402
- Task: 修复 myagent-ai v1.31.0 中的多个关键漏洞
403
-
404
- Work Log:
405
- - 全面审计 browser_stealth.py、main.py、core/browser_profile.py、core/site_registry.py
406
- - 发现 8 个漏洞(3 个高危、3 个中危、2 个低危)
407
- - 修复 browser_stealth.py: time.sleep(1) → await asyncio.sleep(1)
408
- - 修复 browser_stealth.py: _detect_browser 增加 macOS/Windows 路径
409
- - 修复 browser_stealth.py: get_stealth_browser 清理崩溃实例并重建
410
- - 修复 main.py: _running_service 在 __init__ 初始化
411
- - 修复 main.py: shutdown() 关闭所有 stealth 浏览器
412
- - 修复 main.py: 会话锁 TOCTOU 竞争条件
413
- - 修复 main.py: 会话锁内存泄漏(_cleanup_session_locks)
414
- - 修复 main.py: Unicode Ideographic Space 填充问题
415
- - 修复 main.py: xdpyinfo 分辨率解析过于宽松
416
- - 版本升级 v1.31.0 → v1.31.1
417
- - GitHub 推送成功,npm 发布成功
418
-
419
- Stage Summary:
420
- - 修复了 8 个漏洞,涵盖事件循环阻塞、属性未初始化、资源泄漏、竞态条件、跨平台兼容性等
421
- - 已发布 myagent-ai@1.31.1 到 npm 和 GitHub
422
-
13
+ - [SSRF] core/web_control.py - 增加 DNS 解析验证 + 私有 IP 范围全面阻止(ipaddress.is_private 等)
14
+ - [路径遍历] web/api_server.py - _read_multipart_files X-File-Path unquote 再校验
15
+ - [路径遍历] web/api_server.py - handle_set_workdir 增加系统敏感目录黑名单检查
16
+ - [并发竞态] web/api_server.py - handle_voice_optimize _model_chain_lock 包裹全局 LLM 配置修改
17
+ - [XSS] admin-sites.js - 所有按钮改用 data-action 事件委托,移除内联 onclick
18
+ - [XSS] admin-skills.js - 同步改用 data-action 事件委托
19
+ - [XSS] admin-core.js - esc() 函数增加单引号转义,与 escHtml 统一
20
+ - [Content-Type] admin-core.js - api() 函数不再自动设置 Content-Type,支持 FormData multipart 上传
21
+ - 注意: GitHub token 已失效,推送失败,代码仅本地提交