myagent-ai 1.23.19 → 1.23.21
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/chatbot/whatsapp_bot.py +0 -0
- package/chatbot/whatsapp_bridge/bridge.mjs +0 -0
- package/chatbot/whatsapp_bridge/package.json +0 -0
- package/core/stt.py +0 -0
- package/core/tool_dispatcher.py +0 -0
- package/core/web_control.py +0 -0
- package/data/novnc/lib/base64.js +0 -0
- package/data/novnc/lib/crypto/aes.js +0 -0
- package/data/novnc/lib/crypto/bigint.js +0 -0
- package/data/novnc/lib/crypto/crypto.js +0 -0
- package/data/novnc/lib/crypto/des.js +0 -0
- package/data/novnc/lib/crypto/dh.js +0 -0
- package/data/novnc/lib/crypto/md5.js +0 -0
- package/data/novnc/lib/crypto/rsa.js +0 -0
- package/data/novnc/lib/decoders/copyrect.js +0 -0
- package/data/novnc/lib/decoders/hextile.js +0 -0
- package/data/novnc/lib/decoders/jpeg.js +0 -0
- package/data/novnc/lib/decoders/raw.js +0 -0
- package/data/novnc/lib/decoders/rre.js +0 -0
- package/data/novnc/lib/decoders/tight.js +0 -0
- package/data/novnc/lib/decoders/tightpng.js +0 -0
- package/data/novnc/lib/decoders/zrle.js +0 -0
- package/data/novnc/lib/deflator.js +0 -0
- package/data/novnc/lib/display.js +0 -0
- package/data/novnc/lib/encodings.js +0 -0
- package/data/novnc/lib/inflator.js +0 -0
- package/data/novnc/lib/input/domkeytable.js +0 -0
- package/data/novnc/lib/input/fixedkeys.js +0 -0
- package/data/novnc/lib/input/gesturehandler.js +0 -0
- package/data/novnc/lib/input/keyboard.js +0 -0
- package/data/novnc/lib/input/keysym.js +0 -0
- package/data/novnc/lib/input/keysymdef.js +0 -0
- package/data/novnc/lib/input/util.js +0 -0
- package/data/novnc/lib/input/vkeys.js +0 -0
- package/data/novnc/lib/input/xtscancodes.js +0 -0
- package/data/novnc/lib/ra2.js +0 -0
- package/data/novnc/lib/rfb.js +0 -0
- package/data/novnc/lib/util/browser.js +0 -0
- package/data/novnc/lib/util/cursor.js +0 -0
- package/data/novnc/lib/util/element.js +0 -0
- package/data/novnc/lib/util/events.js +0 -0
- package/data/novnc/lib/util/eventtarget.js +0 -0
- package/data/novnc/lib/util/int.js +0 -0
- package/data/novnc/lib/util/logging.js +0 -0
- package/data/novnc/lib/util/strings.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/utils/common.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/adler32.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/constants.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/crc32.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/deflate.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/gzheader.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inffast.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inflate.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/inftrees.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/messages.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/trees.js +0 -0
- package/data/novnc/lib/vendor/pako/lib/zlib/zstream.js +0 -0
- package/data/novnc/lib/websock.js +0 -0
- package/package.json +1 -1
- package/scripts/cli.py +0 -0
- package/skills/agent_tool_skill.py +0 -0
- package/web/ui/admin/admin-agents.js +570 -0
- package/web/ui/admin/admin-core.js +322 -0
- package/web/ui/admin/admin-dashboard.js +153 -0
- package/web/ui/admin/admin-executor.js +67 -0
- package/web/ui/admin/admin-files.js +81 -0
- package/web/ui/admin/admin-llm.js +190 -0
- package/web/ui/admin/admin-logs.js +69 -0
- package/web/ui/admin/admin-memory.js +91 -0
- package/web/ui/admin/admin-org.js +283 -0
- package/web/ui/admin/admin-permissions.js +147 -0
- package/web/ui/admin/admin-platforms.js +221 -0
- package/web/ui/admin/admin-sessions.js +182 -0
- package/web/ui/admin/admin-skills.js +217 -0
- package/web/ui/admin/admin-system.js +154 -0
- package/web/ui/admin/admin-tasks.js +131 -0
- package/web/ui/chat/chat_main.js +292 -304
- package/web/ui/chat/flow_engine.js +96 -41
- package/web/ui/index.html +15 -2776
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* ── admin-core.js — Shared infrastructure (loaded FIRST) ── */
|
|
2
|
+
|
|
3
|
+
const API='';
|
|
4
|
+
const pages={dashboard:'📊 仪表盘',agents:'🤖 Agent 管理',platforms:'🌐 聊天平台',organization:'🏢 组织管理',departments:'🏛 部门管理',sessions:'💬 会话管理',memory:'🧠 记忆管理',permissions:'🔑 权限管理',llm:'🧬 大模型设置',system:'⚙️ 系统配置',executor:'🔧 执行引擎',skills:'🛠 技能管理',files:'📁 工作目录',logs:'📜 查看日志',tasks:'📌 任务记录'};
|
|
5
|
+
let currentPage='dashboard';
|
|
6
|
+
let allAgentsCache=[];
|
|
7
|
+
let allModelsCache=[];
|
|
8
|
+
let allDeptsCache=[];
|
|
9
|
+
|
|
10
|
+
function esc(s){if(!s)return'';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
11
|
+
|
|
12
|
+
async function api(url,opts={}){
|
|
13
|
+
try{
|
|
14
|
+
const r=await fetch(API+url,{headers:{'Content-Type':'application/json'},...opts});
|
|
15
|
+
const rawText=await r.text();
|
|
16
|
+
let data;
|
|
17
|
+
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+')'}}
|
|
18
|
+
return data;
|
|
19
|
+
}catch(e){showToast('请求失败: '+e.message,'danger');return {error:e.message}}
|
|
20
|
+
}
|
|
21
|
+
function $(id){return document.getElementById(id)}
|
|
22
|
+
function escHtml(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
23
|
+
/* [v1.20.2] 安全地将任意文本插入 JS 模板字符串中 textarea 元素:转义反引号、${ 和 </textarea */
|
|
24
|
+
function escTpl(s){return String(s||'').replace(/\\/g,'\\\\').replace(/`/g,'\\`').replace(/\$\{/g,'\\${').replace(/<\/textarea/gi,'<\\/textarea')}
|
|
25
|
+
function fmtDate(d){if(!d)return'-';try{return new Date(d).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'})}catch(e){return d.slice(0,16)}}
|
|
26
|
+
function fmtTimeAgo(d){if(!d)return'-';const s=Math.floor((Date.now()-new Date(d))/1000);if(s<60)return s+'秒前';if(s<3600)return Math.floor(s/60)+'分钟前';if(s<86400)return Math.floor(s/3600)+'小时前';return Math.floor(s/86400)+'天前'}
|
|
27
|
+
|
|
28
|
+
function showToast(msg,type='info',duration=3000){
|
|
29
|
+
const colors={success:'var(--success)',danger:'var(--danger)',warn:'var(--warn)',info:'var(--primary)'};
|
|
30
|
+
const el=document.createElement('div');el.className='toast';el.style.background=colors[type]||colors.info;el.style.color='#fff';
|
|
31
|
+
el.style.whiteSpace='pre-line';el.style.maxWidth='420px';el.style.textAlign='left';
|
|
32
|
+
el.textContent=msg;document.body.appendChild(el);setTimeout(()=>el.remove(),duration);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── 版本号 & 更新检查 ──
|
|
36
|
+
async function loadVersion(){
|
|
37
|
+
try{
|
|
38
|
+
const r=await api('/api/status');
|
|
39
|
+
if(r.version){
|
|
40
|
+
$('sidebarVersion').textContent='v'+r.version;
|
|
41
|
+
}
|
|
42
|
+
}catch(e){}
|
|
43
|
+
}
|
|
44
|
+
async function checkUpdate(manual){
|
|
45
|
+
try{
|
|
46
|
+
const r=await api('/api/update/check',{method:'POST'});
|
|
47
|
+
if(r.error){if(manual) showToast('检查更新失败: '+r.error,'danger'); return}
|
|
48
|
+
if(r.has_update){
|
|
49
|
+
$('updateBadge').style.display='inline';
|
|
50
|
+
showToast('发现新版本 v'+r.latest_version,'warn');
|
|
51
|
+
}else{
|
|
52
|
+
$('updateBadge').style.display='none';
|
|
53
|
+
if(manual) showToast('已是最新版本 v'+r.current_version,'success');
|
|
54
|
+
}
|
|
55
|
+
}catch(e){if(manual) showToast('检查更新失败','danger')}
|
|
56
|
+
}
|
|
57
|
+
async function doUpdate(){
|
|
58
|
+
if(!confirm('确定要更新到最新版本吗?更新后将自动重启。'))return;
|
|
59
|
+
try{
|
|
60
|
+
const r=await api('/api/update/apply',{method:'POST',body:JSON.stringify({type:'full'})});
|
|
61
|
+
if(r.error){showToast('更新失败: '+r.error,'danger');return}
|
|
62
|
+
showToast('更新已开始,服务将自动重启...','success');
|
|
63
|
+
}catch(e){showToast('更新失败','danger')}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Theme Management ──
|
|
67
|
+
function initTheme(){const s=localStorage.getItem('myagent-theme')||'claude';document.documentElement.setAttribute('data-theme',s);updateThemeIcon(s)}
|
|
68
|
+
function toggleTheme(){const c=document.documentElement.getAttribute('data-theme')||'claude';const n=c==='claude'?'dark':'claude';document.documentElement.setAttribute('data-theme',n);localStorage.setItem('myagent-theme',n);updateThemeIcon(n)}
|
|
69
|
+
function updateThemeIcon(t){const b=document.getElementById('themeToggle');if(!b)return;if(t==='dark'){b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';b.title='切换到 Claude 风格'}else{b.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';b.title='切换到夜间模式'}}
|
|
70
|
+
// ── Sidebar Collapse ──
|
|
71
|
+
function toggleSidebar(){const s=document.getElementById('adminSidebar');const t=document.getElementById('sidebarToggle');if(!s)return;s.classList.toggle('collapsed');const isCollapsed=s.classList.contains('collapsed');t.textContent=isCollapsed?'▶':'◀';localStorage.setItem('myagent-admin-sidebar-collapsed',isCollapsed);closeMobileSidebar()}
|
|
72
|
+
// Initialize
|
|
73
|
+
initTheme();
|
|
74
|
+
document.getElementById('themeToggle')?.addEventListener('click',toggleTheme);
|
|
75
|
+
if(localStorage.getItem('myagent-admin-sidebar-collapsed')==='true'){document.getElementById('adminSidebar')?.classList.add('collapsed');const t=document.getElementById('sidebarToggle');if(t)t.textContent='▶'}
|
|
76
|
+
|
|
77
|
+
// ── Mobile Sidebar ──
|
|
78
|
+
function toggleMobileSidebar(){
|
|
79
|
+
const s=document.getElementById('adminSidebar');
|
|
80
|
+
const o=document.getElementById('adminMobileOverlay');
|
|
81
|
+
const isOpen=s.classList.contains('mobile-open');
|
|
82
|
+
if(isOpen){closeMobileSidebar();}else{s.classList.add('mobile-open');o.classList.add('active');}
|
|
83
|
+
}
|
|
84
|
+
function closeMobileSidebar(){
|
|
85
|
+
document.getElementById('adminSidebar').classList.remove('mobile-open');
|
|
86
|
+
document.getElementById('adminMobileOverlay').classList.remove('active');
|
|
87
|
+
}
|
|
88
|
+
// Show hamburger on mobile + tap sidebar to expand on mobile
|
|
89
|
+
function checkMobile(){
|
|
90
|
+
const btn=document.getElementById('mobileMenuBtn');
|
|
91
|
+
if(btn)btn.style.display=window.innerWidth<=768?'grid':'none';
|
|
92
|
+
}
|
|
93
|
+
window.addEventListener('resize',checkMobile);
|
|
94
|
+
checkMobile();
|
|
95
|
+
// [v1.18.10] 移动端点击侧滑栏窄栏区域也可展开
|
|
96
|
+
document.getElementById('adminSidebar')?.addEventListener('click',function(e){
|
|
97
|
+
if(window.innerWidth>768)return;
|
|
98
|
+
// 如果已经是展开状态,不处理(让 nav-item 的 onclick 正常执行)
|
|
99
|
+
if(this.classList.contains('mobile-open'))return;
|
|
100
|
+
// 只在点击侧滑栏本身(非具体按钮/链接)时展开
|
|
101
|
+
if(e.target.closest('.nav-item')||e.target.closest('a')||e.target.closest('button'))return;
|
|
102
|
+
toggleMobileSidebar();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
loadVersion();
|
|
106
|
+
setTimeout(()=>checkUpdate(false),30000);
|
|
107
|
+
|
|
108
|
+
function showConfirm(title,msg,onOk){
|
|
109
|
+
// 使用 data 属性存储回调,onclick 中调用它并正确处理 async
|
|
110
|
+
const id='confirmCallback';
|
|
111
|
+
window[id]=async()=>{try{await onOk();}catch(e){console.error(e);}delete window[id];};
|
|
112
|
+
$('modalContainer').innerHTML=`<div class="confirm-overlay" onclick="closeModal()"><div class="confirm-box" onclick="event.stopPropagation()">
|
|
113
|
+
<h4>${title}</h4><p>${msg}</p><div class="btns">
|
|
114
|
+
<button class="btn btn-danger" onclick="${id}();closeModal()">确认</button>
|
|
115
|
+
<button class="btn btn-ghost" onclick="closeModal()">取消</button></div></div></div>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function showPage(page, addHistory){
|
|
119
|
+
closeMobileSidebar();
|
|
120
|
+
currentPage=page;
|
|
121
|
+
$('content').setAttribute('data-page',page);
|
|
122
|
+
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
|
|
123
|
+
$('pageTitle').textContent=pages[page]||page;
|
|
124
|
+
const renderers=window._adminRenderers||{};
|
|
125
|
+
if(renderers[page]){
|
|
126
|
+
try{renderers[page]();}catch(e){console.error('Page render error:',e);$('content').innerHTML='<div class="empty" style="color:var(--danger)">页面加载失败: '+escHtml(e.message)+'</div>';}
|
|
127
|
+
}
|
|
128
|
+
// 记录到浏览器历史
|
|
129
|
+
if(addHistory!==false){
|
|
130
|
+
var _sub=window._navSubState||null;
|
|
131
|
+
var hash=page+(_sub?'~'+_sub:'');
|
|
132
|
+
history.pushState({page:page,sub:_sub},'','#'+hash);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 导航历史系统:记录子页面跳转(如 viewSession、viewSessionRaw)
|
|
137
|
+
// _navHistory: [{page,sub}] 最多6步
|
|
138
|
+
var _navHistory=[];
|
|
139
|
+
var _navSubState=null;
|
|
140
|
+
|
|
141
|
+
function navigateTo(page, sub, renderFn){
|
|
142
|
+
// 保存当前状态到历史
|
|
143
|
+
_navHistory.push({page:currentPage,sub:window._navSubState});
|
|
144
|
+
if(_navHistory.length>6)_navHistory.shift();
|
|
145
|
+
currentPage=page;
|
|
146
|
+
_navSubState=sub;
|
|
147
|
+
$('content').setAttribute('data-page',page);
|
|
148
|
+
// 更新 URL hash
|
|
149
|
+
var hash=page+(sub?'~'+sub:'');
|
|
150
|
+
history.pushState({page:page,sub:sub},'','#'+hash);
|
|
151
|
+
// 更新导航高亮
|
|
152
|
+
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===page));
|
|
153
|
+
// 渲染页面
|
|
154
|
+
if(renderFn){
|
|
155
|
+
try{renderFn();}catch(e){console.error('Navigate render error:',e);}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function goBack(){
|
|
160
|
+
if(_navHistory.length>0){
|
|
161
|
+
var prev=_navHistory.pop();
|
|
162
|
+
currentPage=prev.page;
|
|
163
|
+
_navSubState=prev.sub;
|
|
164
|
+
var hash=prev.page+(prev.sub?'~'+prev.sub:'');
|
|
165
|
+
history.pushState({page:prev.page,sub:prev.sub},'','#'+hash);
|
|
166
|
+
// 找到对应的渲染函数
|
|
167
|
+
var renderers=window._adminRenderers||{};
|
|
168
|
+
if(renderers[prev.page]){
|
|
169
|
+
try{renderers[prev.page]();}catch(e){}
|
|
170
|
+
}
|
|
171
|
+
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===prev.page));
|
|
172
|
+
}else{
|
|
173
|
+
// 没有内部历史,尝试浏览器后退
|
|
174
|
+
history.back();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 浏览器前进/后退按钮支持
|
|
179
|
+
window.addEventListener('popstate',function(e){
|
|
180
|
+
var s=e.state;
|
|
181
|
+
if(s&&s.page){
|
|
182
|
+
currentPage=s.page;
|
|
183
|
+
_navSubState=s.sub||null;
|
|
184
|
+
document.querySelectorAll('.nav-item').forEach((n,i)=>n.classList.toggle('active',Object.keys(pages)[i]===s.page));
|
|
185
|
+
var renderers=window._adminRenderers||{};
|
|
186
|
+
if(renderers[s.page]){
|
|
187
|
+
try{renderers[s.page]();}catch(e){}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
let _deptTreeNeedsRefresh=false;
|
|
193
|
+
function closeModal(){$('modalContainer').innerHTML='';if(_deptTreeNeedsRefresh){_deptTreeNeedsRefresh=false;renderDepartments()}}
|
|
194
|
+
|
|
195
|
+
// Init
|
|
196
|
+
(function(){
|
|
197
|
+
var hash=window.location.hash.slice(1)||'';
|
|
198
|
+
var page=hash?hash.split('~')[0]:'';
|
|
199
|
+
var sub=hash?hash.slice(page.length+1):'';
|
|
200
|
+
// 兼容旧 URL query param ?page=xxx
|
|
201
|
+
if(!page){
|
|
202
|
+
var params=new URLSearchParams(window.location.search);
|
|
203
|
+
page=params.get('page')||'dashboard';
|
|
204
|
+
}
|
|
205
|
+
if(sub)_navSubState=sub;
|
|
206
|
+
if(pages[page]){
|
|
207
|
+
history.replaceState({page:page,sub:sub||null},'','#'+page+(sub?'~'+sub:''));
|
|
208
|
+
showPage(page,false);
|
|
209
|
+
}else{
|
|
210
|
+
history.replaceState({page:'dashboard',sub:null},'','#dashboard');
|
|
211
|
+
showPage('dashboard',false);
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
setInterval(()=>{api('/api/status').catch(()=>{})},30000);
|
|
215
|
+
|
|
216
|
+
/* ── 头像上传 + 裁剪 ── */
|
|
217
|
+
var _cropState={prefix:'',dragging:false,sx:0,sy:0};
|
|
218
|
+
function handleAvatarUpload(input,prefix){
|
|
219
|
+
var file=input.files[0];if(!file)return;
|
|
220
|
+
if(file.size>5*1024*1024){showToast('图片不能超过 5MB','danger');return}
|
|
221
|
+
var reader=new FileReader();
|
|
222
|
+
reader.onload=function(e){
|
|
223
|
+
var img=$(prefix+'CropImg');img.src=e.target.result;
|
|
224
|
+
$(prefix+'CropArea').style.display='block';
|
|
225
|
+
$(prefix+'CropOverlay').style.display='none';
|
|
226
|
+
_cropState.prefix=prefix;
|
|
227
|
+
};
|
|
228
|
+
reader.readAsDataURL(file);
|
|
229
|
+
}
|
|
230
|
+
/* [v1.18.8] 修复: e.preventDefault() 阻止浏览器原生图片拖拽 + 坐标统一用 clientX/Y */
|
|
231
|
+
function startCrop(e,prefix){
|
|
232
|
+
e.preventDefault();e.stopPropagation();
|
|
233
|
+
var rect=e.target.getBoundingClientRect();
|
|
234
|
+
var cx=e.clientX-rect.left,cy=e.clientY-rect.top;
|
|
235
|
+
_cropState={prefix:prefix,dragging:true,sx:cx,sy:cy};
|
|
236
|
+
var overlay=$(prefix+'CropOverlay');
|
|
237
|
+
overlay.style.display='block';
|
|
238
|
+
overlay.style.left=cx+'px';overlay.style.top=cy+'px';
|
|
239
|
+
overlay.style.width='0';overlay.style.height='0';
|
|
240
|
+
document.addEventListener('mousemove',doCrop);document.addEventListener('mouseup',endCrop);
|
|
241
|
+
}
|
|
242
|
+
/* [v1.18.8] 触摸设备支持 */
|
|
243
|
+
function startCropTouch(e,prefix){
|
|
244
|
+
e.preventDefault();e.stopPropagation();
|
|
245
|
+
var t=e.touches[0];if(!t)return;
|
|
246
|
+
var rect=e.target.getBoundingClientRect();
|
|
247
|
+
var cx=t.clientX-rect.left,cy=t.clientY-rect.top;
|
|
248
|
+
_cropState={prefix:prefix,dragging:true,sx:cx,sy:cy};
|
|
249
|
+
var overlay=$(prefix+'CropOverlay');
|
|
250
|
+
overlay.style.display='block';
|
|
251
|
+
overlay.style.left=cx+'px';overlay.style.top=cy+'px';
|
|
252
|
+
overlay.style.width='0';overlay.style.height='0';
|
|
253
|
+
document.addEventListener('touchmove',doCropTouch,{passive:false});document.addEventListener('touchend',endCropTouch);
|
|
254
|
+
}
|
|
255
|
+
function doCropTouch(e){
|
|
256
|
+
e.preventDefault();
|
|
257
|
+
if(!_cropState.dragging||!e.touches[0])return;
|
|
258
|
+
var t=e.touches[0];
|
|
259
|
+
_doCropMove(t.clientX,t.clientY);
|
|
260
|
+
}
|
|
261
|
+
function endCropTouch(){
|
|
262
|
+
_cropState.dragging=false;
|
|
263
|
+
document.removeEventListener('touchmove',doCropTouch);document.removeEventListener('touchend',endCropTouch);
|
|
264
|
+
}
|
|
265
|
+
function doCrop(e){
|
|
266
|
+
if(!_cropState.dragging)return;
|
|
267
|
+
_doCropMove(e.clientX,e.clientY);
|
|
268
|
+
}
|
|
269
|
+
function _doCropMove(clientX,clientY){
|
|
270
|
+
var img=$(_cropState.prefix+'CropImg'),rect=img.getBoundingClientRect();
|
|
271
|
+
var overlay=$(_cropState.prefix+'CropOverlay');
|
|
272
|
+
var cx=clientX-rect.left,cy=clientY-rect.top;
|
|
273
|
+
var x1=Math.min(_cropState.sx,cx),y1=Math.min(_cropState.sy,cy);
|
|
274
|
+
var x2=Math.max(_cropState.sx,cx),y2=Math.max(_cropState.sy,cy);
|
|
275
|
+
x2=Math.min(x2,rect.width);y2=Math.min(y2,rect.height);
|
|
276
|
+
x1=Math.max(x1,0);y1=Math.max(y1,0);
|
|
277
|
+
var w=x2-x1,h=y2-y1;if(w<5||h<5)return;
|
|
278
|
+
overlay.style.left=x1+'px';overlay.style.top=y1+'px';overlay.style.width=w+'px';overlay.style.height=h+'px';
|
|
279
|
+
}
|
|
280
|
+
function endCrop(){_cropState.dragging=false;document.removeEventListener('mousemove',doCrop);document.removeEventListener('mouseup',endCrop);}
|
|
281
|
+
function cancelAvatarCrop(prefix){$(prefix+'CropArea').style.display='none';}
|
|
282
|
+
function confirmAvatarCrop(prefix,agentPath){
|
|
283
|
+
var img=$(prefix+'CropImg'),overlay=$(prefix+'CropOverlay');
|
|
284
|
+
if(!img||!overlay){showToast('裁剪数据异常','danger');return}
|
|
285
|
+
// [v1.18.7] 防止 clientWidth=0 导致 NaN
|
|
286
|
+
var clientW=Math.max(img.clientWidth,1),clientH=Math.max(img.clientHeight,1);
|
|
287
|
+
var naturalW=img.naturalWidth||clientW,naturalH=img.naturalHeight||clientH;
|
|
288
|
+
var scale=naturalW/clientW;
|
|
289
|
+
if(!isFinite(scale)||scale<=0)scale=1;
|
|
290
|
+
var cx=Math.round(parseFloat(overlay.style.left||'0')*scale);
|
|
291
|
+
var cy=Math.round(parseFloat(overlay.style.top||'0')*scale);
|
|
292
|
+
var cw=Math.round(parseFloat(overlay.style.width||'0')*scale);
|
|
293
|
+
var ch=Math.round(parseFloat(overlay.style.height||'0')*scale);
|
|
294
|
+
if(cw<10||ch<10||isNaN(cx)||isNaN(cy)){showToast('请拖动选择裁剪区域','danger');return}
|
|
295
|
+
var src=$(prefix+'CropImg').src;
|
|
296
|
+
if(!src||!src.startsWith('data:image')){showToast('请先上传图片','danger');return}
|
|
297
|
+
var blob=dataURItoBlob(src);
|
|
298
|
+
if(!blob||blob.size===0){showToast('图片数据异常','danger');return}
|
|
299
|
+
var formData=new FormData();
|
|
300
|
+
formData.append('file',blob,'avatar.png');
|
|
301
|
+
showToast('正在上传裁剪...','info');
|
|
302
|
+
fetch('/api/agents/'+encodeURIComponent(agentPath)+'/avatar?crop_x='+cx+'&crop_y='+cy+'&crop_w='+cw+'&crop_h='+ch+'&size=128',{
|
|
303
|
+
method:'POST',body:formData
|
|
304
|
+
}).then(r=>r.json()).then(d=>{
|
|
305
|
+
if(d.ok){$(prefix+'AvatarImage').value=d.url;$(prefix+'AvatarPreview').innerHTML='<img src="'+d.url+'" style="width:100%;height:100%;object-fit:cover">';$(prefix+'CropArea').style.display='none';showToast('头像已更新','success');}
|
|
306
|
+
else showToast(d.error||'上传失败','danger');
|
|
307
|
+
}).catch(e=>showToast('上传失败: '+e,'danger'));
|
|
308
|
+
}
|
|
309
|
+
function dataURItoBlob(dataURI){
|
|
310
|
+
if(!dataURI||typeof dataURI!=='string')return null;
|
|
311
|
+
var parts=dataURI.split(',');
|
|
312
|
+
if(parts.length<2)return null;
|
|
313
|
+
var mimeMatch=parts[0].match(/:(.*?);/);
|
|
314
|
+
var mime=mimeMatch?mimeMatch[1]:'image/png';
|
|
315
|
+
try{var b=atob(parts[1]),a=new Uint8Array(b.length);for(var i=0;i<b.length;i++)a[i]=b.charCodeAt(i);return new Blob([a],{type:mime});}
|
|
316
|
+
catch(e){return null}
|
|
317
|
+
}
|
|
318
|
+
async function removeAvatarImage(agentPath){
|
|
319
|
+
var ad=await api('/api/agents/'+encodeURIComponent(agentPath));if(!ad||!ad.avatar_image)return;
|
|
320
|
+
await api('/api/agents/'+encodeURIComponent(agentPath),{method:'PUT',body:JSON.stringify({avatar_image:''})});
|
|
321
|
+
$('eaAvatarImage').value='';$('eaAvatarPreview').innerHTML=escHtml(ad.avatar_emoji||'🤖');showToast('已移除头像图片','success');
|
|
322
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ========== Dashboard ==========
|
|
2
|
+
(function(){
|
|
3
|
+
try {
|
|
4
|
+
// ========== Dashboard ==========
|
|
5
|
+
async function renderDashboard(){
|
|
6
|
+
const s = await api('/api/status');
|
|
7
|
+
const gl = s.global_llm || {};
|
|
8
|
+
const models = s.models || [];
|
|
9
|
+
const mem = s.memory || {};
|
|
10
|
+
const q = s.queue || {};
|
|
11
|
+
const runningAgents = s.running_agents || [];
|
|
12
|
+
|
|
13
|
+
// ── 辅助函数 ──
|
|
14
|
+
const modeLabel = m => ({text:'文本',image:'图片',video:'视频',audio:'音频'}[m] || m);
|
|
15
|
+
const modeIcon = m => ({text:'T',image:'🖼',video:'🎬',audio:'🎤'}[m] || m);
|
|
16
|
+
const providerLabel = p => ({openai:'OpenAI',anthropic:'Anthropic',ollama:'Ollama',zhipu:'智谱',custom:'自定义',gemini:'Google'}[p] || p);
|
|
17
|
+
const hasImage = modes => (modes || []).includes('image');
|
|
18
|
+
const isGlobalFallback = m => m.is_global_fallback && m.enabled;
|
|
19
|
+
|
|
20
|
+
// 找到全局默认模型(从模型库中 is_global_fallback 的启用模型)
|
|
21
|
+
const globalModel = models.find(m => isGlobalFallback(m)) || null;
|
|
22
|
+
|
|
23
|
+
// input_modes 标签 HTML
|
|
24
|
+
const modesHtml = (modes) => {
|
|
25
|
+
if (!modes || modes.length === 0) return '';
|
|
26
|
+
return '<div class="dash-model-modes">' + modes.map(m =>
|
|
27
|
+
'<span class="dash-mode-tag ' + m + '">' + modeLabel(m) + '</span>'
|
|
28
|
+
).join('') + '</div>';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 模型卡片 HTML
|
|
32
|
+
const modelCardHtml = (m, isActive) => {
|
|
33
|
+
const multimodal = hasImage(m.input_modes);
|
|
34
|
+
return '<div class="dash-model-card' + (isActive ? ' active' : '') + '">' +
|
|
35
|
+
'<div class="dash-model-icon ' + (multimodal ? 'multimodal' : 'text') + '">' +
|
|
36
|
+
(multimodal ? '🖼' : '💬') +
|
|
37
|
+
'</div>' +
|
|
38
|
+
'<div class="dash-model-info">' +
|
|
39
|
+
'<div class="dash-model-name">' + esc(m.name || m.model || m.id) + '</div>' +
|
|
40
|
+
'<div class="dash-model-provider">' + providerLabel(m.provider) + (m.model && m.model !== m.id ? ' · ' + esc(m.model) : '') + '</div>' +
|
|
41
|
+
modesHtml(m.input_modes) +
|
|
42
|
+
'</div>' +
|
|
43
|
+
(isActive ? '<span class="dash-active-badge">默认</span>' : '') +
|
|
44
|
+
'</div>';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ── 顶部统计卡片 ──
|
|
48
|
+
var statsHtml = '<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(180px,1fr))">' +
|
|
49
|
+
'<div class="stat"><div class="label">版本</div><div class="value" style="font-size:18px">' + esc(s.version || '-') + '</div></div>' +
|
|
50
|
+
'<div class="stat"><div class="label">已注册技能</div><div class="value">' + (s.skills || 0) + '</div></div>' +
|
|
51
|
+
'<div class="stat"><div class="label">模型库</div><div class="value">' + models.length + '</div></div>' +
|
|
52
|
+
'<div class="stat"><div class="label">记忆条目</div><div class="value">' + (mem.total_count || 0) + '</div></div>' +
|
|
53
|
+
'<div class="stat"><div class="label">会话数</div><div class="value">' + (mem.session_count || 0) + '</div></div>' +
|
|
54
|
+
'<div class="stat"><div class="label">全局记忆</div><div class="value">' + (mem.global_count || 0) + '</div></div>' +
|
|
55
|
+
(q.total_submitted ? '<div class="stat"><div class="label">任务队列</div><div class="value">' + q.total_submitted + '</div></div>' : '') +
|
|
56
|
+
'</div>';
|
|
57
|
+
|
|
58
|
+
// ── 模型区域 ──
|
|
59
|
+
var modelSection = '';
|
|
60
|
+
if (models.length > 0) {
|
|
61
|
+
var enabledModels = models.filter(m => m.enabled);
|
|
62
|
+
var disabledModels = models.filter(m => !m.enabled);
|
|
63
|
+
|
|
64
|
+
modelSection = '<div class="dash-section">' +
|
|
65
|
+
'<div class="dash-section-title"><span class="icon">🤖</span> 模型配置</div>';
|
|
66
|
+
|
|
67
|
+
// 默认模型高亮展示
|
|
68
|
+
if (globalModel) {
|
|
69
|
+
modelSection += '<div style="margin-bottom:12px">' +
|
|
70
|
+
'<div style="font-size:12px;color:var(--text3);margin-bottom:8px">当前全局默认模型</div>' +
|
|
71
|
+
modelCardHtml(globalModel, true) +
|
|
72
|
+
'</div>';
|
|
73
|
+
} else {
|
|
74
|
+
// 没有在模型库中找到全局默认,用 config.llm 显示
|
|
75
|
+
modelSection += '<div style="margin-bottom:12px">' +
|
|
76
|
+
'<div style="font-size:12px;color:var(--text3);margin-bottom:8px">当前全局模型</div>' +
|
|
77
|
+
modelCardHtml({id:gl.model, name:gl.model, provider:gl.provider, model:gl.model, input_modes:gl.input_modes, enabled:true}, true) +
|
|
78
|
+
'</div>';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 按类型分类
|
|
82
|
+
var textModels = enabledModels.filter(m => !hasImage(m.input_modes));
|
|
83
|
+
var multiModels = enabledModels.filter(m => hasImage(m.input_modes));
|
|
84
|
+
|
|
85
|
+
// 多模态模型(支持图片的)
|
|
86
|
+
if (multiModels.length > 0) {
|
|
87
|
+
modelSection += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px;margin-top:12px">多模态模型(支持图片输入)</div>' +
|
|
88
|
+
'<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:8px">' +
|
|
89
|
+
multiModels.map(m => modelCardHtml(m, false)).join('') +
|
|
90
|
+
'</div>';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 纯文本模型
|
|
94
|
+
if (textModels.length > 0) {
|
|
95
|
+
modelSection += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px;margin-top:12px">文本模型</div>' +
|
|
96
|
+
'<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;margin-bottom:8px">' +
|
|
97
|
+
textModels.map(m => modelCardHtml(m, false)).join('') +
|
|
98
|
+
'</div>';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 已禁用模型
|
|
102
|
+
if (disabledModels.length > 0) {
|
|
103
|
+
modelSection += '<div style="font-size:12px;color:var(--text3);margin-bottom:8px;margin-top:12px;opacity:.6">已禁用 (' + disabledModels.length + ')</div>' +
|
|
104
|
+
'<div class="grid" style="grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px">' +
|
|
105
|
+
disabledModels.map(m => {
|
|
106
|
+
var card = modelCardHtml(m, false);
|
|
107
|
+
return card.replace('dash-model-card"', 'dash-model-card" style="opacity:.5"');
|
|
108
|
+
}).join('') +
|
|
109
|
+
'</div>';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
modelSection += '</div>';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 运行中的 Agent ──
|
|
116
|
+
var runningSection = '<div class="dash-section">' +
|
|
117
|
+
'<div class="dash-section-title"><span class="icon">⚡</span> 运行中的任务</div>';
|
|
118
|
+
|
|
119
|
+
if (runningAgents.length > 0) {
|
|
120
|
+
runningSection += '<div style="display:flex;flex-direction:column;gap:8px">';
|
|
121
|
+
for (var ra of runningAgents) {
|
|
122
|
+
var agentName = ra.agent_path || '未知';
|
|
123
|
+
var taskMsg = ra.message || '处理中...';
|
|
124
|
+
var timeStr = '';
|
|
125
|
+
if (ra.started_at) {
|
|
126
|
+
var elapsed = Math.round((Date.now() / 1000) - ra.started_at);
|
|
127
|
+
if (elapsed < 60) timeStr = elapsed + '秒前';
|
|
128
|
+
else if (elapsed < 3600) timeStr = Math.floor(elapsed / 60) + '分钟前';
|
|
129
|
+
else timeStr = Math.floor(elapsed / 3600) + '小时前';
|
|
130
|
+
}
|
|
131
|
+
runningSection += '<div class="dash-running-item">' +
|
|
132
|
+
'<div class="dash-running-dot"></div>' +
|
|
133
|
+
'<div style="flex:1;min-width:0">' +
|
|
134
|
+
'<div class="dash-running-agent">' + esc(agentName) + '</div>' +
|
|
135
|
+
'<div class="dash-running-task" title="' + esc(taskMsg) + '">' + esc(taskMsg) + '</div>' +
|
|
136
|
+
'</div>' +
|
|
137
|
+
(timeStr ? '<div class="dash-running-time">' + timeStr + '</div>' : '') +
|
|
138
|
+
'</div>';
|
|
139
|
+
}
|
|
140
|
+
runningSection += '</div>';
|
|
141
|
+
} else {
|
|
142
|
+
runningSection += '<div class="dash-empty">当前没有运行中的任务</div>';
|
|
143
|
+
}
|
|
144
|
+
runningSection += '</div>';
|
|
145
|
+
|
|
146
|
+
// ── 组装 ──
|
|
147
|
+
$('content').innerHTML = statsHtml + modelSection + runningSection;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
|
|
151
|
+
window._adminRenderers['dashboard'] = renderDashboard;
|
|
152
|
+
} catch(e) { console.error('[admin-dashboard] load error:', e); }
|
|
153
|
+
})();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ========== Executor ==========
|
|
2
|
+
(function(){
|
|
3
|
+
try {
|
|
4
|
+
// ========== Executor ==========
|
|
5
|
+
async function renderExecutor(){
|
|
6
|
+
const e=await api('/api/executor');if(!e)return;
|
|
7
|
+
const isSandbox=e.mode==='sandbox';const dockerOk=e.docker_available;
|
|
8
|
+
const lock=e.lock||{};
|
|
9
|
+
const sandboxType=e.sandbox_type||'';
|
|
10
|
+
const sandboxDesc=e.sandbox_desc||'';
|
|
11
|
+
let html=`<div class="card"><h3>执行模式</h3>
|
|
12
|
+
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
|
13
|
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:16px 24px;border-radius:var(--radius);border:2px solid ${!isSandbox?'var(--primary)':'var(--border)'};background:${!isSandbox?'#6366f122':'transparent'};flex:1;min-width:200px">
|
|
14
|
+
<input type="radio" name="execMode" value="local" ${!isSandbox?'checked':''} onchange="switchMode('local')">
|
|
15
|
+
<div><strong>🖥️ 本机执行</strong><br><span style="font-size:12px;color:var(--text2)">直接在本机运行代码,功能完整,速度最快</span></div></label>
|
|
16
|
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:16px 24px;border-radius:var(--radius);border:2px solid ${isSandbox?'var(--primary)':'var(--border)'};background:${isSandbox?'#6366f122':'transparent'};flex:1;min-width:200px">
|
|
17
|
+
<input type="radio" name="execMode" value="sandbox" ${isSandbox?'checked':''} onchange="switchMode('sandbox')" ${!dockerOk?'disabled':''}>
|
|
18
|
+
<div><strong>📦 沙盒执行</strong><br><span style="font-size:12px;color:var(--text2)">${dockerOk?'Docker 隔离容器':'轻量级进程沙盒'}${!dockerOk?' (Docker 不可用)':''}</span></div></label></div>
|
|
19
|
+
<div style="font-size:13px;color:var(--text2);display:flex;flex-wrap:wrap;gap:8px">
|
|
20
|
+
<span>当前模式: <span class="badge ${isSandbox?'badge-yellow':'badge-green'}">${isSandbox?'沙盒':'本机'}</span></span>
|
|
21
|
+
${isSandbox?`<span>沙盒类型: <span class="tag">${sandboxType==='docker'?'Docker':'轻量级进程'}</span></span>`:''}
|
|
22
|
+
<span>Docker: <span class="badge ${dockerOk?'badge-green':'badge-red'}">${dockerOk?'可用':'不可用'}</span></span>
|
|
23
|
+
<span>累计执行: <span class="tag">${e.execution_count||0} 次</span></span>
|
|
24
|
+
</div>
|
|
25
|
+
<div style="margin-top:8px;padding:10px 14px;border-radius:var(--radius);background:var(--surface);font-size:12px;color:var(--text2);line-height:1.6">${escHtml(sandboxDesc)}</div>
|
|
26
|
+
</div>`;
|
|
27
|
+
// 执行锁状态
|
|
28
|
+
html+=`<div class="card"><h3>🔒 全局执行锁</h3>
|
|
29
|
+
<div style="font-size:13px">
|
|
30
|
+
${lock.locked?
|
|
31
|
+
`<span class="badge badge-red">已锁定</span> <strong>${escHtml(lock.locked_by||'')}</strong> — 锁定于 ${escHtml(lock.locked_at||'')}
|
|
32
|
+
<div style="margin-top:8px"><button class="btn btn-sm btn-danger" onclick="releaseLock()">🔓 释放锁</button></div>`:
|
|
33
|
+
`<span class="badge badge-green">未锁定</span> — 所有 Agent 可自由运行`
|
|
34
|
+
}
|
|
35
|
+
<div style="margin-top:6px;font-size:12px;color:var(--text3)">${isSandbox?'沙盒模式不走全局锁,支持多 Agent 并发执行':'本机模式下多个 Agent 共享此锁,同一时间只有一个 Agent 可以执行代码'}</div>
|
|
36
|
+
</div></div>`;
|
|
37
|
+
// 沙盒设置
|
|
38
|
+
html+=`<div class="card"><h3>沙盒设置</h3><div class="form-row">
|
|
39
|
+
<div class="form-group"><label>Docker 镜像</label><input id="sbImage" value="${escHtml(e.sandbox_image||'python:3.12-slim')}"></div>
|
|
40
|
+
<div class="form-group"><label>内存限制</label><input id="sbMemory" value="${escHtml(e.sandbox_memory||'512m')}" placeholder="512m"></div>
|
|
41
|
+
<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>
|
|
42
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存设置</button></div></div>`;
|
|
43
|
+
// 执行参数
|
|
44
|
+
html+=`<div class="card"><h3>执行参数</h3><div class="form-row">
|
|
45
|
+
<div class="form-group"><label>超时时间 (秒)</label><input id="exTimeout" type="number" value="${e.timeout||300}"></div>
|
|
46
|
+
<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>
|
|
47
|
+
<div class="form-group"><label>最大输出长度</label><input id="exMaxOutput" type="number" value="${e.max_output_length||50000}"></div></div>
|
|
48
|
+
<div class="flex gap-8 mt-16"><button class="btn btn-primary" onclick="saveExecutor()">保存参数</button></div></div>`;
|
|
49
|
+
$('content').innerHTML=html;
|
|
50
|
+
}
|
|
51
|
+
async function switchMode(mode){const r=await api('/api/executor',{method:'PUT',body:JSON.stringify({execution_mode:mode})});if(!r.ok){showToast('切换失败: '+(r.error||''),'danger');renderExecutor();}else{showToast('已切换','success');renderExecutor();}}
|
|
52
|
+
async function saveExecutor(){
|
|
53
|
+
const body={sandbox_image:$('sbImage').value,sandbox_memory:$('sbMemory').value,sandbox_network:$('sbNetwork').value==='true',timeout:parseInt($('exTimeout').value),auto_fix:$('exAutoFix').value==='true',max_output_length:parseInt($('exMaxOutput').value)};
|
|
54
|
+
const r=await api('/api/executor',{method:'PUT',body:JSON.stringify(body)});
|
|
55
|
+
if(r.ok)showToast('已保存','success');else showToast('保存失败: '+(r.error||''),'danger');
|
|
56
|
+
renderExecutor();
|
|
57
|
+
}
|
|
58
|
+
async function releaseLock(){
|
|
59
|
+
const r=await api('/api/execution-lock',{method:'POST',body:JSON.stringify({action:'release'})});
|
|
60
|
+
if(r.ok)showToast('锁已释放','success');else showToast('释放失败: '+(r.error||''),'danger');
|
|
61
|
+
renderExecutor();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
|
|
65
|
+
window._adminRenderers['executor'] = renderExecutor;
|
|
66
|
+
} catch(e) { console.error('[admin-executor] load error:', e); }
|
|
67
|
+
})();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ========== Files ==========
|
|
2
|
+
(function(){
|
|
3
|
+
try {
|
|
4
|
+
// ========== Files ==========
|
|
5
|
+
var _workdirAgent=''; // 当前选中的 agent(空=全局工作目录)
|
|
6
|
+
async function renderFiles(){
|
|
7
|
+
try{
|
|
8
|
+
const [wd,agents]=await Promise.all([api('/api/workdir').catch(()=>({})),api('/api/agents').catch(()=>[])]);
|
|
9
|
+
const agentList=Array.isArray(agents)?agents.filter(a=>!a.system&&a.path!=='default'):[];
|
|
10
|
+
const wdPath=wd&&wd.path?wd.path:'';
|
|
11
|
+
let html=`<div class="flex items-center gap-8 mb-16 flex-wrap">
|
|
12
|
+
<span style="font-size:14px;color:var(--text2)">📁 工作目录: <strong>${escHtml(wdPath)}</strong></span>
|
|
13
|
+
<select id="workdirAgentSelect" onchange="onWorkdirAgentChange()" style="width:auto">
|
|
14
|
+
<option value="">全局工作目录</option>
|
|
15
|
+
${agentList.map(a=>`<option value="${escHtml(a.path)}" ${_workdirAgent===a.path?'selected':''}>${escHtml((a.avatar_emoji||'🤖')+' '+a.name)} (${escHtml(a.path)})</option>`).join('')}
|
|
16
|
+
</select>
|
|
17
|
+
<button class="btn btn-sm btn-ghost" onclick="changeWorkdir()">更改全局</button>
|
|
18
|
+
<button class="btn btn-sm btn-ghost" onclick="renderFiles()">🔄 刷新</button></div>
|
|
19
|
+
<div id="workdirContent">加载中...</div>`;
|
|
20
|
+
$('content').innerHTML=html;
|
|
21
|
+
loadWorkdirContent('');
|
|
22
|
+
}catch(e){console.error('renderFiles error:',e);$('content').innerHTML='<div class="empty" style="color:var(--danger)">加载失败: '+escHtml(e.message||'未知错误')+'</div>';}
|
|
23
|
+
}
|
|
24
|
+
function onWorkdirAgentChange(){
|
|
25
|
+
_workdirAgent=$('workdirAgentSelect').value;
|
|
26
|
+
loadWorkdirContent('');
|
|
27
|
+
}
|
|
28
|
+
async function loadWorkdirContent(subPath){
|
|
29
|
+
const params=new URLSearchParams();
|
|
30
|
+
if(subPath)params.set('path',subPath);
|
|
31
|
+
const files=await api('/api/workdir/files?'+params.toString());
|
|
32
|
+
const el=document.getElementById('workdirContent');
|
|
33
|
+
if(!el)return;
|
|
34
|
+
// 面包屑
|
|
35
|
+
var bcHtml='';
|
|
36
|
+
if(subPath){
|
|
37
|
+
var parts=subPath.split('/');var crumbs=['<span style="cursor:pointer;color:var(--primary)" onclick="loadWorkdirContent(\'\')">根目录</span>'];var acc='';
|
|
38
|
+
for(var i=0;i<parts.length;i++){acc+=(acc?'/':'')+parts[i];crumbs.push(' / <span style="cursor:pointer;color:var(--primary)" onclick="loadWorkdirContent(\''+acc+'\')">'+escHtml(parts[i])+'</span>');}
|
|
39
|
+
bcHtml=crumbs.join('');
|
|
40
|
+
}else{bcHtml='<span style="color:var(--text3)">根目录</span>';}
|
|
41
|
+
// Agent 工作目录信息
|
|
42
|
+
if(_workdirAgent){
|
|
43
|
+
var agentCfg=await api('/api/agents/'+encodeURIComponent(_workdirAgent)).catch(()=>null);
|
|
44
|
+
if(agentCfg&&agentCfg.work_dir){
|
|
45
|
+
bcHtml+=` <span style="margin-left:12px;font-size:11px;color:var(--text3)">Agent 工作目录: ${escHtml(agentCfg.work_dir)}</span>`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var html=`<div style="font-size:12px;margin-bottom:8px">${bcHtml}</div>`;
|
|
49
|
+
html+='<div class="table-wrap"><table><tr><th>名称</th><th>大小</th><th></th></tr>';
|
|
50
|
+
if(!files||!files.length){
|
|
51
|
+
html+='<tr><td colspan="3" class="empty">目录为空</td></tr>';
|
|
52
|
+
}else{
|
|
53
|
+
// 排序:目录在前
|
|
54
|
+
var dirs=files.filter(f=>f.type==='dir').sort((a,b)=>a.name.localeCompare(b.name));
|
|
55
|
+
var fils=files.filter(f=>f.type==='file').sort((a,b)=>a.name.localeCompare(b.name));
|
|
56
|
+
for(const d of dirs){
|
|
57
|
+
var dp=d.path||d.name;
|
|
58
|
+
html+=`<tr style="cursor:pointer" onclick="loadWorkdirContent('${escHtml(dp)}')"><td>📂 ${escHtml(d.name)}</td><td>-</td><td></td></tr>`;
|
|
59
|
+
}
|
|
60
|
+
for(const f of fils){
|
|
61
|
+
var fp=f.path||f.name;
|
|
62
|
+
var sizeStr=f.size>1048576?(f.size/1048576).toFixed(1)+' MB':f.size>1024?(f.size/1024).toFixed(1)+' KB':f.size+' B';
|
|
63
|
+
html+=`<tr><td>📄 ${escHtml(f.name)}</td><td>${sizeStr}</td>
|
|
64
|
+
<td><button class="btn btn-sm btn-ghost" onclick="downloadWorkdirFileGlobal('${escHtml(fp)}')">下载</button></td></tr>`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
html+='</table></div>';
|
|
68
|
+
el.innerHTML=html;
|
|
69
|
+
}
|
|
70
|
+
function downloadWorkdirFileGlobal(relPath){
|
|
71
|
+
var link=document.createElement('a');
|
|
72
|
+
link.href=API+'/api/workdir/download/'+encodeURIComponent(relPath);
|
|
73
|
+
link.download='';
|
|
74
|
+
document.body.appendChild(link);link.click();document.body.removeChild(link);
|
|
75
|
+
}
|
|
76
|
+
async function changeWorkdir(){const p=prompt('新路径:');if(!p)return;await api('/api/workdir',{method:'PUT',body:JSON.stringify({path:p})});showToast('已更新','success');renderFiles();}
|
|
77
|
+
|
|
78
|
+
if (typeof window._adminRenderers === 'undefined') window._adminRenderers = {};
|
|
79
|
+
window._adminRenderers['files'] = renderFiles;
|
|
80
|
+
} catch(e) { console.error('[admin-files] load error:', e); }
|
|
81
|
+
})();
|