myagent-ai 1.15.20 → 1.15.22

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.
@@ -23,6 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  import importlib.util
25
25
  import os
26
+ import re
26
27
  import subprocess
27
28
  import sys
28
29
  import threading
@@ -213,7 +214,7 @@ def _pip_install(pip_names: List[str], category: str = "") -> Tuple[bool, str]:
213
214
 
214
215
 
215
216
  def _check_version(import_name: str, min_version: str) -> bool:
216
- """检查模块版本是否满足最低要求"""
217
+ """检查模块版本是否满足最低要求(使用简单的 tuple 比较,无需额外依赖)"""
217
218
  if not min_version:
218
219
  return True
219
220
  try:
@@ -221,10 +222,18 @@ def _check_version(import_name: str, min_version: str) -> bool:
221
222
  if spec is None:
222
223
  return False
223
224
  mod = importlib.import_module(import_name)
224
- ver = getattr(mod, "__version__", "0.0.0")
225
- # 简单版本比较
226
- from packaging.version import parse as parse_ver
227
- return parse_ver(ver) >= parse_ver(min_version)
225
+ ver_str = getattr(mod, "__version__", "0.0.0")
226
+ # 简单版本比较:将版本字符串转为 tuple 进行比较
227
+ def _ver_tuple(v: str):
228
+ parts = []
229
+ for p in re.split(r'[.\-]', v)[:3]:
230
+ m = re.match(r'(\d+)', str(p))
231
+ parts.append(int(m.group(1)) if m else 0)
232
+ return tuple(parts)
233
+ try:
234
+ return _ver_tuple(ver_str) >= _ver_tuple(min_version)
235
+ except (ValueError, TypeError):
236
+ return True
228
237
  except Exception:
229
238
  return True # 无法获取版本时保守通过
230
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.15.20",
3
+ "version": "1.15.22",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -128,11 +128,13 @@ class MCPClient:
128
128
 
129
129
  try:
130
130
  # 启动 MCP Server 子进程
131
+ # 注意: stderr 必须用 DEVNULL 或在后台线程中持续读取
132
+ # 否则 stderr 管道缓冲区满后子进程会阻塞,导致 JSON-RPC 超时
131
133
  self._process = subprocess.Popen(
132
134
  args,
133
135
  stdin=subprocess.PIPE,
134
136
  stdout=subprocess.PIPE,
135
- stderr=subprocess.PIPE,
137
+ stderr=subprocess.DEVNULL,
136
138
  env={**os.environ, "NO_COLOR": "1"},
137
139
  )
138
140
 
@@ -62,6 +62,27 @@ class WebSearchSkill(Skill):
62
62
  """
63
63
  num = min(max(num, 1), 20)
64
64
 
65
+ # 确保搜索依赖已安装(requests, beautifulsoup4, lxml)
66
+ try:
67
+ import requests
68
+ from bs4 import BeautifulSoup
69
+ except ImportError as imp_err:
70
+ logger.warning(f"搜索依赖缺失: {imp_err},尝试自动安装...")
71
+ from core.deps_checker import ensure_skill_deps
72
+ if not ensure_skill_deps("search"):
73
+ return SkillResult(
74
+ success=False,
75
+ error=f"搜索依赖安装失败,请手动运行: pip install requests beautifulsoup4 lxml ({imp_err})",
76
+ )
77
+ try:
78
+ import requests # noqa: F811
79
+ from bs4 import BeautifulSoup # noqa: F811
80
+ except ImportError as imp_err:
81
+ return SkillResult(
82
+ success=False,
83
+ error=f"搜索依赖不可用,请手动运行: pip install requests beautifulsoup4 lxml ({imp_err})",
84
+ )
85
+
65
86
  try:
66
87
  # 尝试 DuckDuckGo HTML
67
88
  results = await self._duckduckgo_html_search(query, num)
@@ -139,9 +160,6 @@ class WebSearchSkill(Skill):
139
160
  break
140
161
 
141
162
  return results
142
- except ImportError:
143
- logger.debug("beautifulsoup4 未安装")
144
- return []
145
163
  except Exception as e:
146
164
  logger.warning(f"DuckDuckGo HTML 搜索失败: {e}")
147
165
  return []
@@ -216,8 +234,6 @@ class WebSearchSkill(Skill):
216
234
  break
217
235
 
218
236
  return results
219
- except ImportError:
220
- return []
221
237
  except Exception as e:
222
238
  logger.warning(f"Bing 搜索失败: {e}")
223
239
  return []
@@ -234,9 +250,18 @@ class WebReadSkill(Skill):
234
250
  ]
235
251
 
236
252
  async def execute(self, url: str = "", extract_text: bool = True, **kwargs) -> SkillResult:
253
+ # 确保搜索依赖已安装
237
254
  try:
238
255
  import requests
239
256
  from bs4 import BeautifulSoup
257
+ except ImportError as imp_err:
258
+ logger.warning(f"网页读取依赖缺失: {imp_err},尝试自动安装...")
259
+ from core.deps_checker import ensure_skill_deps
260
+ if not ensure_skill_deps("search"):
261
+ return SkillResult(success=False, error=f"依赖安装失败,请手动运行: pip install requests beautifulsoup4 lxml ({imp_err})")
262
+ try:
263
+ import requests # noqa: F811
264
+ from bs4 import BeautifulSoup # noqa: F811
240
265
 
241
266
  loop = asyncio.get_event_loop()
242
267
 
@@ -293,8 +318,6 @@ class WebReadSkill(Skill):
293
318
  },
294
319
  message=f"已读取: {title} ({len(content)} 字符)",
295
320
  )
296
- except ImportError:
297
- return SkillResult(success=False, error="请安装依赖: pip install requests beautifulsoup4")
298
321
  except Exception as e:
299
322
  return SkillResult(success=False, error=f"网页读取失败: {e}")
300
323
 
@@ -314,8 +337,16 @@ class URLReadSkill(Skill):
314
337
 
315
338
  async def execute(self, url: str = "", method: str = "GET",
316
339
  headers: dict = None, body: str = "", **kwargs) -> SkillResult:
340
+ # 确保依赖已安装
317
341
  try:
318
342
  import requests
343
+ except ImportError as imp_err:
344
+ logger.warning(f"URL读取依赖缺失: {imp_err},尝试自动安装...")
345
+ from core.deps_checker import ensure_skill_deps
346
+ if not ensure_skill_deps("search"):
347
+ return SkillResult(success=False, error=f"依赖安装失败,请手动运行: pip install requests ({imp_err})")
348
+ try:
349
+ import requests # noqa: F811
319
350
 
320
351
  headers = headers or {}
321
352
  loop = asyncio.get_event_loop()
package/web/ui/index.html CHANGED
@@ -52,6 +52,9 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
52
52
  .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:16px}
53
53
  .card h3{font-size:15px;margin-bottom:12px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px}
54
54
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
55
+ .grid-2{grid-template-columns:repeat(2,1fr)}
56
+ .grid-3{grid-template-columns:repeat(3,1fr)}
57
+ .grid-4{grid-template-columns:repeat(4,1fr)}
55
58
  .stat{background:var(--surface2);padding:16px;border-radius:var(--radius)}
56
59
  .stat .label{font-size:12px;color:var(--text2);margin-bottom:4px}
57
60
  .stat .value{font-size:28px;font-weight:700}
@@ -184,7 +187,17 @@ tr:hover{background:var(--surface2)}
184
187
  .content{padding:16px}
185
188
  .grid{grid-template-columns:1fr}
186
189
  .form-row{grid-template-columns:1fr}
187
- .table-wrap{overflow-x:auto}
190
+ .table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
191
+ .card{padding:12px;margin-bottom:12px}
192
+ .card h3{font-size:14px;margin-bottom:8px}
193
+ .stat{padding:12px}
194
+ .stat .label{font-size:11px}
195
+ .stat .value{font-size:20px}
196
+ .grid,.grid-2,.grid-3,.grid-4{grid-template-columns:repeat(2,1fr)!important}
197
+ .btn-sm{padding:6px 12px;font-size:12px;min-height:32px}
198
+ th,td{padding:6px 8px;font-size:12px;white-space:nowrap}
199
+ th{font-size:11px}
200
+ .badge{padding:2px 6px;font-size:10px}
188
201
  .modal{width:95%;max-height:90vh;padding:16px}
189
202
  .modal-wide{max-width:95%}
190
203
  .tabs{gap:0;overflow-x:auto}
@@ -192,6 +205,18 @@ tr:hover{background:var(--surface2)}
192
205
  .agent-card{flex-direction:column;align-items:flex-start}
193
206
  .agent-card .flex.flex-col{flex-direction:row;gap:4px}
194
207
  }
208
+ @media(max-width:480px){
209
+ .content{padding:12px}
210
+ .card{padding:10px}
211
+ .grid,.grid-2,.grid-3,.grid-4{grid-template-columns:1fr!important}
212
+ .header h2{font-size:15px}
213
+ .btn-sm{padding:8px 12px;font-size:12px;min-height:36px}
214
+ .stat .value{font-size:18px}
215
+ .stat .label{font-size:10px}
216
+ .modal{width:98%;padding:12px}
217
+ .form-group{margin-bottom:10px}
218
+ .form-group label{font-size:12px}
219
+ }
195
220
  </style>
196
221
  </head>
197
222
  <body>
@@ -635,9 +660,9 @@ async function loadAgentKB(){
635
660
  <h4 style="font-size:14px;color:var(--text2)">知识库文件 (${files.length})</h4>
636
661
  <button class="btn btn-sm btn-primary" onclick="uploadAgentKB('${escHtml(path)}',false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadAgentKB('${escHtml(path)}',true)">📁 上传文件夹</button></div>`;
637
662
  if(files.length===0){html+='<div class="empty">暂无知识库文件</div>';}
638
- else{html+='<table><tr><th>文件名</th><th>大小</th><th></th></tr>';
663
+ else{html+='<div class="table-wrap"><table><tr><th>文件名</th><th>大小</th><th></th></tr>';
639
664
  for(const f of files){html+=`<tr><td>${escHtml(f.name||f.filename||'')}</td><td>${f.size||'-'}</td><td><button class="btn btn-sm btn-danger" onclick="deleteAgentKB('${escHtml(path)}','${escHtml(f.name||f.filename||'')}')">删除</button></td></tr>`}
640
- html+='</table>';}
665
+ html+='</table></div>';}
641
666
  $('kbContent').innerHTML=html;
642
667
  }
643
668
 
@@ -672,9 +697,9 @@ async function loadAgentSessions(){
672
697
  const sessions=Array.isArray(data)?data:(data?.sessions||[]);
673
698
  let html=`<div class="flex justify-between items-center mb-16"><h4 style="font-size:14px;color:var(--text2)">会话 (${sessions.length})</h4></div>`;
674
699
  if(sessions.length===0){html+='<div class="empty">暂无会话</div>';}
675
- else{html+='<table><tr><th>会话</th><th>消息数</th><th>最后活动</th><th></th></tr>';
700
+ else{html+='<div class="table-wrap"><table><tr><th>会话</th><th>消息数</th><th>最后活动</th><th></th></tr>';
676
701
  for(const s of sessions){const dn=s.display_name||s.id;html+=`<tr><td style="max-width:200px;overflow:hidden;text-overflow:ellipsis" title="${escHtml(s.id)}">${escHtml(dn.length>25?dn.slice(0,25)+'...':dn)}</td><td>${s.messages||0}</td><td>${fmtTimeAgo(s.last)}</td><td><button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="enterSession('${escHtml(s.id)}','${escHtml(path)}')">切入</button> <button class="btn btn-sm btn-ghost" onclick="viewSessionMsgs('${escHtml(s.id)}')">查看</button></td></tr>`}
677
- html+='</table>';}
702
+ html+='</table></div>';}
678
703
  $('sessionsContent').innerHTML=html;
679
704
  }
680
705
 
@@ -745,7 +770,7 @@ async function loadAgentPerms(){
745
770
  html+='<div class="card" style="margin-bottom:16px">';
746
771
  html+='<h3 style="font-size:14px;color:var(--text2);margin-bottom:8px">🔑 功能权限</h3>';
747
772
  html+='<p style="color:var(--text2);font-size:12px;margin-bottom:12px">精细控制 Agent 的各项能力。未设置的项目将使用全局默认值。</p>';
748
- html+='<div class="grid" style="grid-template-columns:repeat(3,1fr);gap:12px">';
773
+ html+='<div class="grid grid-3" style="gap:12px">';
749
774
  for(const p of perms){
750
775
  const label=labels[p]||p;
751
776
  const defVal=defaults[p]!==false;
@@ -891,7 +916,7 @@ async function doDeletePlatform(name){
891
916
  // ========== Sessions ==========
892
917
  async function renderSessions(){
893
918
  const ss=await api('/api/sessions');
894
- let html='<table><tr><th>会话</th><th>Agent</th><th>消息数</th><th>最后活动</th><th>操作</th></tr>';
919
+ let html='<div class="table-wrap"><table><tr><th>会话</th><th>Agent</th><th>消息数</th><th>最后活动</th><th>操作</th></tr>';
895
920
  for(const s of (ss||[])){
896
921
  // 从 session_id 提取 agent 名 (格式: agent_web_timestamp)
897
922
  const parts=(s.id||'').split('_web_');
@@ -901,7 +926,7 @@ async function renderSessions(){
901
926
  <td><button class="btn btn-sm" style="background:var(--success);color:#fff" onclick="enterSession('${escHtml(s.id)}','${escHtml(agentName)}')">切入</button>
902
927
  <button class="btn btn-sm btn-ghost" onclick="viewSession('${s.id}')">查看</button>
903
928
  <button class="btn btn-sm btn-danger" onclick="clearSession('${s.id}')">清除</button></td></tr>`;}
904
- html+='</table>';if(!ss||!ss.length)html='<div class="empty">暂无会话</div>';
929
+ html+='</table></div>';if(!ss||!ss.length)html='<div class="empty">暂无会话</div>';
905
930
  $('content').innerHTML=html;
906
931
  }
907
932
  async function viewSession(sid){
@@ -975,7 +1000,7 @@ async function renderMemory(){
975
1000
  tabHtml+=`<button class="btn ${active}" onclick="_memCategory='${c.k}';renderMemory()">${c.l} (${count})</button>`;
976
1001
  }
977
1002
  tabHtml+='</div>';
978
- let html=`<div class="grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:16px">
1003
+ let html=`<div class="grid grid-3" style="margin-bottom:16px">
979
1004
  <div class="stat"><div class="label">总计</div><div class="value">${stats.total_count||0}</div></div>
980
1005
  <div class="stat"><div class="label">全局记忆</div><div class="value">${stats.global_count||0}</div></div>
981
1006
  <div class="stat"><div class="label">会话记忆</div><div class="value">${stats.session_count||0}</div></div></div>`;
@@ -989,7 +1014,7 @@ async function renderMemory(){
989
1014
  thHtml+='<th>会话</th>';
990
1015
  if(!isSession)thHtml+='<th>重要性</th>';
991
1016
  thHtml+='<th></th></tr>';
992
- html+='<table>'+thHtml;
1017
+ html+='<div class="table-wrap"><table>'+thHtml;
993
1018
  for(const e of lt){
994
1019
  const content=(e.content||e.summary||'')||(e.role==='user'?'[用户消息]':e.role==='assistant'?'[助手回复]':'[系统]');
995
1020
 
@@ -1002,7 +1027,7 @@ async function renderMemory(){
1002
1027
  if(!isSession)html+='<td>'+(e.importance!=null?e.importance.toFixed(2):'')+'</td>';
1003
1028
  html+='<td><button class="btn btn-sm btn-danger" onclick="deleteMemory(\''+e.id+'\')">删除</button></td></tr>';
1004
1029
  }
1005
- html+='</table>';
1030
+ html+='</table></div>';
1006
1031
  }else{
1007
1032
  html+='<div class="empty">暂无'+(_memCategory==='session'?'会话':'全局')+'记忆</div>';
1008
1033
  }
@@ -1013,12 +1038,12 @@ async function searchMemory(){
1013
1038
  const r=await api('/api/memory/search?q='+encodeURIComponent(q));
1014
1039
  let html='<h3>搜索结果: '+(r.length||0)+' 条</h3>';
1015
1040
  if(r&&r.length){
1016
- html+='<table><tr><th>Key</th><th>内容</th><th>分类</th><th>角色</th><th>会话</th></tr>';
1041
+ html+='<div class="table-wrap"><table><tr><th>Key</th><th>内容</th><th>分类</th><th>角色</th><th>会话</th></tr>';
1017
1042
  for(const e of r){
1018
1043
  const content=(e.content||'').slice(0,300);
1019
1044
  html+=`<tr><td style="white-space:nowrap">${escHtml(e.key||'')}</td><td style="max-width:600px;word-break:break-word;font-size:13px">${escHtml(content)}</td><td>${e.category||''}</td><td>${escHtml(e.role||'')}</td><td style="font-size:12px;color:var(--text3)">${escHtml((e.session_id||'').split('_web_')[0])}</td></tr>`;
1020
1045
  }
1021
- html+='</table>';
1046
+ html+='</table></div>';
1022
1047
  }else{
1023
1048
  html+='<div class="empty">未找到匹配的记忆</div>';
1024
1049
  }
@@ -1040,7 +1065,7 @@ async function renderPermissions(){
1040
1065
 
1041
1066
  // 全局默认权限
1042
1067
  let html='<div class="card"><h3>全局默认权限</h3><p style="color:var(--text2);font-size:13px;margin-bottom:12px">新 Agent 将继承这些默认权限设置</p>';
1043
- html+='<div class="grid" style="grid-template-columns:repeat(3,1fr);gap:12px">';
1068
+ html+='<div class="grid grid-3" style="gap:12px">';
1044
1069
  for(const p of perms){
1045
1070
  const label=labels[p]||p;
1046
1071
  const val=defaults[p]!==false?'checked':'';
@@ -1051,7 +1076,7 @@ async function renderPermissions(){
1051
1076
  // Agent 权限覆盖
1052
1077
  const agentKeys=Object.keys(agents);
1053
1078
  if(agentKeys.length>0){
1054
- html+='<div class="card"><h3>Agent 权限覆盖</h3><p style="color:var(--text2);font-size:13px;margin-bottom:12px">以下 Agent 使用自定义权限(覆盖默认值)</p><table><tr><th>Agent</th>';
1079
+ html+='<div class="card"><h3>Agent 权限覆盖</h3><p style="color:var(--text2);font-size:13px;margin-bottom:12px">以下 Agent 使用自定义权限(覆盖默认值)</p><div class="table-wrap"><table><tr><th>Agent</th>';
1055
1080
  for(const p of perms){html+=`<th>${labels[p]||p}</th>`;}
1056
1081
  html+='<th>操作</th></tr>';
1057
1082
  for(const name of agentKeys){
@@ -1063,7 +1088,7 @@ async function renderPermissions(){
1063
1088
  }
1064
1089
  html+=`<td><button class="btn btn-sm btn-ghost" onclick="editAgentPerms('${escHtml(name)}')">编辑</button> <button class="btn btn-sm btn-danger" onclick="resetAgentPerms('${escHtml(name)}')">重置</button></td></tr>`;
1065
1090
  }
1066
- html+='</table></div>';
1091
+ html+='</table></div></div>';
1067
1092
  }
1068
1093
 
1069
1094
  $('content').innerHTML=html;
@@ -1114,7 +1139,7 @@ async function editAgentPerms(name){
1114
1139
 
1115
1140
  // 功能权限
1116
1141
  html+='<div style="font-size:13px;color:var(--text2);margin-bottom:8px;font-weight:600">🔑 功能权限</div>';
1117
- html+='<div class="grid" style="grid-template-columns:repeat(3,1fr);gap:12px">';
1142
+ html+='<div class="grid grid-3" style="gap:12px">';
1118
1143
  for(const p of perms){
1119
1144
  const label=labels[p]||p;
1120
1145
  const defVal=defaults[p]!==false;
@@ -1174,7 +1199,7 @@ async function renderLLM(){
1174
1199
  allModelsCache=Array.isArray(models)?models:[];
1175
1200
  let html='';
1176
1201
  // 用量统计
1177
- html+=`<div class="card"><h3>用量统计</h3><div class="grid" style="grid-template-columns:repeat(4,1fr)">
1202
+ html+=`<div class="card"><h3>用量统计</h3><div class="grid grid-4">
1178
1203
  <div class="stat"><div class="label">调用次数</div><div class="value">${u.call_count||0}</div></div>
1179
1204
  <div class="stat"><div class="label">Prompt Tokens</div><div class="value">${u.total_prompt_tokens||0}</div></div>
1180
1205
  <div class="stat"><div class="label">Completion Tokens</div><div class="value">${u.total_completion_tokens||0}</div></div>
@@ -1191,7 +1216,7 @@ async function renderLLM(){
1191
1216
  if(!modelList.length){
1192
1217
  html+='<div class="empty">暂无自定义模型,点击上方按钮添加。</div>';
1193
1218
  }else{
1194
- html+='<table style="font-size:12px"><tr><th>ID</th><th>名称</th><th>Provider</th><th>模型</th><th>上下文</th><th>输入</th><th>推理</th><th>兜底</th><th>状态</th><th>操作</th></tr>';
1219
+ html+='<div class="table-wrap"><table style="font-size:12px"><tr><th>ID</th><th>名称</th><th>Provider</th><th>模型</th><th>上下文</th><th>输入</th><th>推理</th><th>兜底</th><th>状态</th><th>操作</th></tr>';
1195
1220
  const providerColors={openai:'badge-green',anthropic:'badge-yellow',ollama:'badge-purple',zhipu:'badge-blue',custom:'badge-red',deepseek:'badge-blue',moonshot:'badge-purple',qwen:'badge-yellow',modelscope:'badge-purple'};
1196
1221
  for(const m of modelList){
1197
1222
  const badgeClass=providerColors[m.provider]||'badge-green';
@@ -1211,7 +1236,7 @@ async function renderLLM(){
1211
1236
  <button class="btn btn-sm btn-success" onclick="testModel(encodeURIComponent('${escHtml(m.id)}'))">测试</button>
1212
1237
  <button class="btn btn-sm btn-danger" onclick="deleteModel('${escHtml(m.id)}','${escHtml(String(m.name||'').replace(/'/g,"\\'"))}')">删除</button></td></tr>`;
1213
1238
  }
1214
- html+='</table>';
1239
+ html+='</table></div>';
1215
1240
  }
1216
1241
  html+='</div>';
1217
1242
  $('content').innerHTML=html;
@@ -1343,12 +1368,12 @@ async function renderExecutor(){
1343
1368
  <input type="radio" name="execMode" value="sandbox" ${isSandbox?'checked':''} onchange="switchMode('sandbox')" ${!dockerOk?'disabled':''}>
1344
1369
  <div><strong>📦 沙盒执行 (Docker)</strong><br><span style="font-size:12px;color:var(--text2)">在隔离容器中运行,更安全${!dockerOk?' (Docker 不可用)':''}</span></div></label></div>
1345
1370
  <div style="font-size:13px;color:var(--text2)">当前模式: <span class="badge ${isSandbox?'badge-yellow':'badge-green'}">${isSandbox?'沙盒 (Docker)':'本机'}</span> Docker 状态: <span class="badge ${dockerOk?'badge-green':'badge-red'}">${dockerOk?'可用':'不可用'}</span> 累计执行: <span class="tag">${e.execution_count||0} 次</span></div></div>`;
1346
- html+=`<div class="card"><h3>沙盒设置</h3><div class="grid" style="grid-template-columns:1fr 1fr">
1371
+ html+=`<div class="card"><h3>沙盒设置</h3><div class="form-row">
1347
1372
  <div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${e.sandbox_image||'python:3.12-slim'}"></div>
1348
1373
  <div class="form-group"><label>内存限制</label><input id="sbMemory" value="${e.sandbox_memory||'512m'}" placeholder="512m"></div>
1349
1374
  <div class="form-group"><label>网络访问</label><select id="sbNetwork"><option ${!e.sandbox_network?'selected':''} value="false">禁止 (更安全)</option><option ${e.sandbox_network?'selected':''} value="true">允许</option></select></div></div>
1350
1375
  <div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存设置</button></div></div>`;
1351
- html+=`<div class="card"><h3>执行参数</h3><div class="grid" style="grid-template-columns:1fr 1fr 1fr">
1376
+ html+=`<div class="card"><h3>执行参数</h3><div class="form-row">
1352
1377
  <div class="form-group"><label>超时时间 (秒)</label><input id="exTimeout" type="number" value="${e.timeout||300}"></div>
1353
1378
  <div class="form-group"><label>自动重试</label><input id="exRetries" type="number" value="2"></div>
1354
1379
  <div class="form-group"><label>自动修复</label><select id="exAutoFix"><option ${e.auto_fix?'selected':''} value="true">开启</option><option ${!e.auto_fix?'selected':''} value="false">关闭</option></select></div></div></div>`;
@@ -1420,9 +1445,9 @@ async function viewSkillDetail(name){
1420
1445
  </div>
1421
1446
  <div class="form-group"><label>危险操作</label><div>${s.dangerous?'<span class="badge badge-red">是</span>':'<span class="badge badge-green">否</span>'}</div></div>
1422
1447
  <div class="form-group"><label>参数 (${(s.parameters||[]).length})</label>
1423
- <table><tr><th>名称</th><th>类型</th><th>必需</th><th>描述</th></tr>
1448
+ <div class="table-wrap"><table><tr><th>名称</th><th>类型</th><th>必需</th><th>描述</th></tr>
1424
1449
  ${(s.parameters||[]).map(p=>`<tr><td><code>${escHtml(p.name)}</code></td><td>${p.type||'string'}</td><td>${p.required?'✅':'❌'}</td><td>${escHtml(p.description||'')}</td></tr>`).join('')}
1425
- </table></div>`;
1450
+ </table></div></div>`;
1426
1451
  $('modalContainer').innerHTML=`<div class="modal-overlay" onclick="closeModal()"><div class="modal" onclick="event.stopPropagation()">${html}<div class="flex gap-8 mt-16"><button class="btn btn-ghost" onclick="closeModal()">关闭</button></div></div></div>`;
1427
1452
  }
1428
1453
 
@@ -1433,12 +1458,12 @@ async function renderFiles(){
1433
1458
  <span style="font-size:14px;color:var(--text2)">工作目录: ${wd.path}</span>
1434
1459
  <button class="btn btn-sm btn-ghost" onclick="changeWorkdir()">更改</button>
1435
1460
  <button class="btn btn-sm btn-ghost" onclick="renderFiles()">刷新</button></div>`;
1436
- html+='<table><tr><th>名称</th><th>类型</th><th>大小</th></tr>';
1461
+ html+='<div class="table-wrap"><table><tr><th>名称</th><th>类型</th><th>大小</th></tr>';
1437
1462
  for(const f of (files||[])){
1438
1463
  const icon=f.type==='dir'?'📁':'📄';const size=f.type==='file'?(f.size>1024?(f.size/1024).toFixed(1)+'KB':f.size+'B'):'-';
1439
1464
  html+=`<tr><td>${icon} ${escHtml(f.name)}</td><td>${f.type}</td><td>${size}</td></tr>`;}
1440
1465
  if(!files||!files.length)html+='<tr><td colspan="3" class="empty">目录为空</td></tr>';
1441
- html+='</table>';$('content').innerHTML=html;
1466
+ html+='</table></div>';$('content').innerHTML=html;
1442
1467
  }
1443
1468
  async function changeWorkdir(){const p=prompt('新路径:');if(!p)return;await api('/api/workdir',{method:'PUT',body:JSON.stringify({path:p})});renderFiles();}
1444
1469
 
@@ -1471,7 +1496,7 @@ async function renderTasks(){
1471
1496
  const tasks=r.tasks||[];
1472
1497
  const statusCounts={pending:0,running:0,completed:0,failed:0};
1473
1498
  for(const t of tasks){statusCounts[t.status]=(statusCounts[t.status]||0)+1;}
1474
- let html=`<div class="grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:16px">
1499
+ let html=`<div class="grid grid-4" style="margin-bottom:16px">
1475
1500
  <div class="stat"><div class="label">待处理</div><div class="value">${statusCounts.pending}</div></div>
1476
1501
  <div class="stat"><div class="label">运行中</div><div class="value">${statusCounts.running}</div></div>
1477
1502
  <div class="stat"><div class="label">已完成</div><div class="value" style="color:var(--success)">${statusCounts.completed}</div></div>
@@ -1489,7 +1514,7 @@ async function renderTasks(){
1489
1514
  if(tasks.length===0){html+='<div class="empty">暂无任务记录</div>';}
1490
1515
  else{
1491
1516
  html+='<div style="max-height:calc(100vh - 340px);overflow-y:auto" id="taskList">';
1492
- html+='<table><tr><th>任务ID</th><th>描述</th><th>群聊</th><th>状态</th><th>时间</th><th>操作</th></tr>';
1517
+ html+='<div class="table-wrap"><table><tr><th>任务ID</th><th>描述</th><th>群聊</th><th>状态</th><th>时间</th><th>操作</th></tr>';
1493
1518
  for(const t of tasks){
1494
1519
  const statusBadge=t.status==='completed'?'<span class="badge badge-green">已完成</span>':
1495
1520
  t.status==='running'?'<span class="badge badge-blue">运行中</span>':
@@ -1511,7 +1536,7 @@ async function renderTasks(){
1511
1536
  <button class="btn btn-sm btn-danger" onclick="deleteTask('${escHtml(t.task_id)}')">删除</button>
1512
1537
  </td></tr>`;
1513
1538
  }
1514
- html+='</table></div>';
1539
+ html+='</table></div></div>';
1515
1540
  }
1516
1541
  $('content').innerHTML=html;
1517
1542
  }
@@ -1554,7 +1579,7 @@ async function renderOrganization(){
1554
1579
  </div>
1555
1580
  <div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveOrgConfig()">保存配置</button></div></div>`;
1556
1581
  html+=`<div class="card"><h3>组织信息</h3>
1557
- <div class="grid" style="grid-template-columns:1fr 1fr">
1582
+ <div class="form-row">
1558
1583
  <div class="form-group"><label>组织名称</label><input id="orgName" value="${escHtml(inf.name||'')}" placeholder="我的组织"></div>
1559
1584
  <div class="form-group"><label>组织描述</label><input id="orgDesc" value="${escHtml(inf.description||'')}" placeholder="组织简介"></div>
1560
1585
  <div class="form-group"><label>联系方式</label><input id="orgContact" value="${escHtml(inf.contact||'')}" placeholder="联系邮箱或电话"></div>
@@ -1581,13 +1606,13 @@ async function loadOrgKnowledge(){
1581
1606
  const files=await api('/api/organization/knowledge');
1582
1607
  const el=document.getElementById('orgKBList');if(!el)return;
1583
1608
  if(!files||!files.length){el.innerHTML='<div class="empty">暂无知识库文件</div>';return}
1584
- let html='<table><tr><th>文件名</th><th>大小</th><th>操作</th></tr>';
1609
+ let html='<div class="table-wrap"><table><tr><th>文件名</th><th>大小</th><th>操作</th></tr>';
1585
1610
  for(const f of files){
1586
1611
  html+=`<tr><td>${escHtml(f.name||f.path)}</td><td>${f.size||'-'}</td>
1587
1612
  <td><button class="btn btn-sm btn-ghost" onclick="viewOrgKBFile('${escHtml(f.path||f.name)}')">查看</button>
1588
1613
  <button class="btn btn-sm btn-danger" onclick="deleteOrgKBFile('${escHtml(f.path||f.name)}')">删除</button></td></tr>`;
1589
1614
  }
1590
- html+='</table>';el.innerHTML=html;
1615
+ html+='</table></div>';el.innerHTML=html;
1591
1616
  }
1592
1617
  function uploadOrgKnowledge(folderMode){
1593
1618
  const input=document.createElement('input');input.type='file';input.multiple=true;
@@ -1773,13 +1798,13 @@ async function loadDeptKB(path){
1773
1798
  let html='<h4 style="margin-bottom:8px">部门知识库</h4>';
1774
1799
  html+='<div style="margin-bottom:8px"><button class="btn btn-sm btn-primary" onclick="uploadDeptKB(\''+escHtml(path)+'\',false)">上传文件</button> <button class="btn btn-sm btn-secondary" onclick="uploadDeptKB(\''+escHtml(path)+'\',true)">📁 上传文件夹</button></div>';
1775
1800
  if(!files||!files.length){html+='<div class="empty" style="margin-top:12px">暂无知识库文件</div>';el.innerHTML=html;return}
1776
- html+='<table><tr><th>文件</th><th>操作</th></tr>';
1801
+ html+='<div class="table-wrap"><table><tr><th>文件</th><th>操作</th></tr>';
1777
1802
  for(const f of files){
1778
1803
  html+=`<tr><td>${escHtml(f.name||f.path)}</td>
1779
1804
  <td><button class="btn btn-sm btn-ghost" onclick="viewDeptKBFile('${escHtml(path)}','${escHtml(f.path||f.name)}')">查看</button>
1780
1805
  <button class="btn btn-sm btn-danger" onclick="deleteDeptKBFile('${escHtml(path)}','${escHtml(f.path||f.name)}')">删除</button></td></tr>`;
1781
1806
  }
1782
- html+='</table>';el.innerHTML=html;
1807
+ html+='</table></div>';el.innerHTML=html;
1783
1808
  }
1784
1809
  async function uploadDeptKB(path,folderMode){
1785
1810
  const input=document.createElement('input');input.type='file';input.multiple=true;