myagent-ai 1.32.0 → 1.32.3
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/core/browser_profile.py +7 -1
- package/core/web_control.py +34 -10
- package/package.json +1 -1
- package/web/api_server.py +132 -51
- package/web/ui/admin/admin-core.js +10 -2
- package/web/ui/admin/admin-sites.js +26 -8
- package/web/ui/admin/admin-skills.js +17 -4
- package/web/ui/chat/chat_main.js +44 -18
- package/worklog.md +14 -415
package/core/browser_profile.py
CHANGED
|
@@ -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
|
-
|
|
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"
|
package/core/web_control.py
CHANGED
|
@@ -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
|
-
|
|
719
|
-
|
|
720
|
-
hostname
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
#
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
// [v1.31.4] esc 现在也转义单引号,与 escHtml 统一(修复 XSS)
|
|
14
|
+
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
|
|
14
15
|
|
|
16
|
+
// [v1.31.4] 安全修复: 不自动设置 Content-Type,让 FormData 自动设置 boundary
|
|
15
17
|
async function api(url,opts={}){
|
|
16
18
|
try{
|
|
17
|
-
|
|
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
|
-
|
|
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"
|
|
86
|
-
actions += '<button class="btn btn-sm btn-ghost" style="color:var(--danger)"
|
|
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"
|
|
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"
|
|
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)"
|
|
96
|
-
actions += '<button class="btn btn-sm btn-ghost" style="color:var(--warn)"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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"
|
|
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/web/ui/chat/chat_main.js
CHANGED
|
@@ -2136,10 +2136,26 @@ async function loadAllAgentSessions() {
|
|
|
2136
2136
|
// 快速对话: 选中 agent + 切换执行模式 + 新建会话
|
|
2137
2137
|
function quickChatAgent(agentPath) {
|
|
2138
2138
|
// 选中 agent
|
|
2139
|
+
var _qPrevAgent = state.activeAgent;
|
|
2139
2140
|
state.activeAgent = agentPath;
|
|
2140
2141
|
StatePersistence.save('activeAgent', agentPath);
|
|
2141
2142
|
state.activeSessionId = null;
|
|
2142
2143
|
state.messages = [];
|
|
2144
|
+
// [fix] 切换到不同 agent 时,清除旧 agent 的待恢复会话 ID
|
|
2145
|
+
if (_qPrevAgent !== agentPath) {
|
|
2146
|
+
state._pendingSessionRestore = null;
|
|
2147
|
+
}
|
|
2148
|
+
// [fix] 切换到不同 agent 时,清除 URL 中旧 agent 的 session 参数
|
|
2149
|
+
if (_qPrevAgent !== agentPath) {
|
|
2150
|
+
try {
|
|
2151
|
+
var _qfixUrl = new URL(window.location.href);
|
|
2152
|
+
_qfixUrl.searchParams.delete('s');
|
|
2153
|
+
_qfixUrl.searchParams.delete('session');
|
|
2154
|
+
var _qAObj = findAgentByPath(agentPath);
|
|
2155
|
+
if (_qAObj && _qAObj.aid) _qfixUrl.searchParams.set('aid', _qAObj.aid);
|
|
2156
|
+
window.history.replaceState({}, '', _qfixUrl.toString());
|
|
2157
|
+
} catch(_) {}
|
|
2158
|
+
}
|
|
2143
2159
|
// 展开所有父节点
|
|
2144
2160
|
var parts = agentPath.split('/');
|
|
2145
2161
|
var cumPath = '';
|
|
@@ -2188,10 +2204,29 @@ async function selectAgent(agentPath) {
|
|
|
2188
2204
|
// 移动端:立即关闭右侧栏,不要等异步操作完成
|
|
2189
2205
|
if (isMobile()) closeMobileAgentPanel();
|
|
2190
2206
|
// Always reload sessions even if clicking the same agent
|
|
2207
|
+
var _prevAgent = state.activeAgent;
|
|
2191
2208
|
state.activeAgent = agentPath;
|
|
2192
2209
|
StatePersistence.save('activeAgent', agentPath);
|
|
2193
2210
|
state.activeSessionId = null;
|
|
2194
2211
|
state.messages = [];
|
|
2212
|
+
// [fix] 切换到不同 agent 时,清除旧 agent 的待恢复会话 ID,防止 loadSessions 选中旧 agent 的会话
|
|
2213
|
+
// 注意:仅在 agent 确实发生变化时才清除,避免影响 initChat 中同 agent 的会话恢复逻辑
|
|
2214
|
+
if (_prevAgent !== agentPath) {
|
|
2215
|
+
state._pendingSessionRestore = null;
|
|
2216
|
+
}
|
|
2217
|
+
// [fix] 切换到不同 agent 时,清除 URL 中旧 agent 的 session 参数,防止 loadSessions 从 URL 恢复旧会话
|
|
2218
|
+
if (_prevAgent !== agentPath) {
|
|
2219
|
+
try {
|
|
2220
|
+
var _fixUrl = new URL(window.location.href);
|
|
2221
|
+
_fixUrl.searchParams.delete('s');
|
|
2222
|
+
_fixUrl.searchParams.delete('session');
|
|
2223
|
+
var _selAObj = findAgentByPath(agentPath);
|
|
2224
|
+
if (_selAObj && _selAObj.aid) {
|
|
2225
|
+
_fixUrl.searchParams.set('aid', _selAObj.aid);
|
|
2226
|
+
}
|
|
2227
|
+
window.history.replaceState({}, '', _fixUrl.toString());
|
|
2228
|
+
} catch(_) {}
|
|
2229
|
+
}
|
|
2195
2230
|
// 递增序号,使任何进行中的 selectSession 请求失效
|
|
2196
2231
|
state._sessionLoadSeq++;
|
|
2197
2232
|
var parts = agentPath.split('/');
|
|
@@ -2220,16 +2255,12 @@ async function selectAgent(agentPath) {
|
|
|
2220
2255
|
await loadSessions();
|
|
2221
2256
|
|
|
2222
2257
|
// loadSessions 内部会 auto-select 最新 session;
|
|
2223
|
-
// 如果没有 session
|
|
2258
|
+
// 如果没有 session,则进入"新对话"状态
|
|
2224
2259
|
// [v1.18.9] 如果 loadSessions 已成功选中了 session,不要覆盖其标题
|
|
2225
2260
|
if (!state.activeSessionId || state.activeSessionId === '__new__') {
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
document.getElementById('headerTitle').textContent = '新对话';
|
|
2230
|
-
var details = await loadAgentDetails(agentPath);
|
|
2231
|
-
updateWelcomeCard(agentPath, details);
|
|
2232
|
-
renderMessages();
|
|
2261
|
+
// [fix] 对于没有历史会话的 agent,直接进入新对话状态
|
|
2262
|
+
// 确保 activeSessionId 被设为 '__new__',左侧栏同步显示空列表
|
|
2263
|
+
newChat();
|
|
2233
2264
|
}
|
|
2234
2265
|
// 如果 loadSessions 已经 auto-selected 了 session,UI 已由 selectSession 设置好,不再覆盖
|
|
2235
2266
|
|
|
@@ -2615,6 +2646,7 @@ async function loadSessions() {
|
|
|
2615
2646
|
|
|
2616
2647
|
// Auto-select most recent session if none selected
|
|
2617
2648
|
// 优先级: URL session 参数 > localStorage 持久化的 session > 最新 session
|
|
2649
|
+
// [fix] 所有候选 session ID 必须属于当前 agent 的会话列表,防止错乱到其它 agent 的历史对话
|
|
2618
2650
|
const urlParams = new URLSearchParams(window.location.search);
|
|
2619
2651
|
const urlSession = UrlCodec.decode(urlParams.get('s') || '') || UrlCodec.decode(urlParams.get('session') || '');
|
|
2620
2652
|
var targetSessionId = null;
|
|
@@ -2656,16 +2688,10 @@ async function loadSessions() {
|
|
|
2656
2688
|
// 默认选中最新 session
|
|
2657
2689
|
targetSessionId = state.sessions[0].id;
|
|
2658
2690
|
}
|
|
2659
|
-
// [v1.24.1
|
|
2660
|
-
//
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
if (fallbackSession && fallbackSession !== '__new__') {
|
|
2664
|
-
targetSessionId = fallbackSession;
|
|
2665
|
-
state._pendingSessionRestore = null;
|
|
2666
|
-
console.log('[loadSessions] Session not in list, force restoring:', targetSessionId);
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2691
|
+
// [fix] 移除 v1.24.1 的强制恢复逻辑:不再强制加载不属于当前 agent 的 session ID
|
|
2692
|
+
// 旧逻辑会在 URL/localStorage 残留旧 agent 的 session ID 时,强制加载该会话,
|
|
2693
|
+
// 导致点击新 agent 头像后错乱显示旧 agent 的历史对话。
|
|
2694
|
+
// 对于当前 agent 确实没有匹配 session 的情况,应该进入"新对话"状态,而非加载其它 agent 的会话。
|
|
2669
2695
|
|
|
2670
2696
|
if (targetSessionId) {
|
|
2671
2697
|
await selectSession(targetSessionId);
|
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:
|
|
4
|
+
Task: 全面审查并修复项目安全漏洞 (v1.32.1)
|
|
360
5
|
|
|
361
6
|
Work Log:
|
|
362
|
-
-
|
|
363
|
-
-
|
|
364
|
-
-
|
|
365
|
-
-
|
|
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、路径遍历、并发竞态、XSS、Content-Type 注入
|
|
10
|
+
- 更新版本号到 1.32.1 并完成本地 git commit
|
|
370
11
|
|
|
371
12
|
Stage Summary:
|
|
372
|
-
-
|
|
373
|
-
-
|
|
374
|
-
-
|
|
375
|
-
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 已失效,推送失败,代码仅本地提交
|